From 46eaf497ae659fcbf80998b966ad908549a363c2 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 17 Apr 2025 15:27:21 -0700 Subject: [PATCH 0001/1056] fix: pass multithreaded with log level (#4173) --- sqlmesh/core/engine_adapter/base.py | 2 ++ sqlmesh/core/engine_adapter/databricks.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 0e75949ca0..a61bd48f7e 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -140,6 +140,7 @@ def __init__( self._register_comments = register_comments self._pre_ping = pre_ping self._pretty_sql = pretty_sql + self._multithreaded = multithreaded def with_log_level(self, level: int) -> EngineAdapter: adapter = self.__class__( @@ -150,6 +151,7 @@ def with_log_level(self, level: int) -> EngineAdapter: execute_log_level=level, register_comments=self._register_comments, null_connection=True, + multithreaded=self._multithreaded, **self._extra_config, ) diff --git a/sqlmesh/core/engine_adapter/databricks.py b/sqlmesh/core/engine_adapter/databricks.py index e22c7867a4..3671f7dbdc 100644 --- a/sqlmesh/core/engine_adapter/databricks.py +++ b/sqlmesh/core/engine_adapter/databricks.py @@ -45,7 +45,7 @@ class DatabricksEngineAdapter(SparkEngineAdapter): def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: super().__init__(*args, **kwargs) - self._set_spark_engine_adapter_if_needed(kwargs.get("multithreaded", False)) + self._set_spark_engine_adapter_if_needed() @classmethod def can_access_spark_session(cls, disable_spark_session: bool) -> bool: @@ -92,7 +92,7 @@ def _use_spark_session(self) -> bool: def is_spark_session_connection(self) -> bool: return isinstance(self.connection, SparkSessionConnection) - def _set_spark_engine_adapter_if_needed(self, multithreaded: bool) -> None: + def _set_spark_engine_adapter_if_needed(self) -> None: self._spark_engine_adapter = None if not self._use_spark_session or self.is_spark_session_connection: @@ -117,7 +117,7 @@ def _set_spark_engine_adapter_if_needed(self, multithreaded: bool) -> None: partial(connection, spark=spark, catalog=catalog), default_catalog=catalog, execute_log_level=self._execute_log_level, - multithreaded=multithreaded, + multithreaded=self._multithreaded, sql_gen_kwargs=self._sql_gen_kwargs, register_comments=self._register_comments, pre_ping=self._pre_ping, From 1cc9da45000f876df8a348299a32d3f169277cbc Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 18 Apr 2025 19:55:35 +0300 Subject: [PATCH 0002/1056] Feat: support dynamic blueprinting for Python models, add docs (#4177) --- docs/concepts/models/python_models.md | 4 ++++ docs/concepts/models/sql_models.md | 8 ++++++++ sqlmesh/core/model/decorator.py | 27 ++++++++++++++++++++++++-- sqlmesh/core/model/definition.py | 2 +- tests/core/test_model.py | 28 +++++++++++++++++++++++---- 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/docs/concepts/models/python_models.md b/docs/concepts/models/python_models.md index 3e33ef1129..c735dfa133 100644 --- a/docs/concepts/models/python_models.md +++ b/docs/concepts/models/python_models.md @@ -367,6 +367,10 @@ def entrypoint( ) ``` +!!! note + + Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints="@gen_blueprints()"`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files. + ## Examples ### Basic The following is an example of a Python model returning a static Pandas DataFrame. diff --git a/docs/concepts/models/sql_models.md b/docs/concepts/models/sql_models.md index 67d4831fcd..9a6023a5ea 100644 --- a/docs/concepts/models/sql_models.md +++ b/docs/concepts/models/sql_models.md @@ -175,6 +175,10 @@ SELECT FROM customer2.some_source ``` +!!! note + + Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints @gen_blueprints()`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files. + ## Python-based definition The Python-based definition of SQL models consists of a single python function, decorated with SQLMesh's `@model` [decorator](https://wiki.python.org/moin/PythonDecorators). The decorator is required to have the `is_sql` keyword argument set to `True` to distinguish it from [Python models](./python_models.md) that return DataFrame instances. @@ -258,6 +262,10 @@ def entrypoint(evaluator: MacroEvaluator) -> str | exp.Expression: The two models produced from this template are the same as in the [example](#SQL-model-blueprinting) for SQL-based blueprinting. +!!! note + + Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints="@gen_blueprints()"`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files. + ## Automatic dependencies SQLMesh parses your SQL, so it understands what the code does and how it relates to other models. There is no need for you to manually specify dependencies to other models with special tags or commands. diff --git a/sqlmesh/core/model/decorator.py b/sqlmesh/core/model/decorator.py index acc602783f..952b8276b0 100644 --- a/sqlmesh/core/model/decorator.py +++ b/sqlmesh/core/model/decorator.py @@ -24,7 +24,7 @@ ) from sqlmesh.core.model.kind import ModelKindName, _ModelKind from sqlmesh.utils import registry_decorator, DECORATOR_RETURN_TYPE -from sqlmesh.utils.errors import ConfigError +from sqlmesh.utils.errors import ConfigError, raise_config_error from sqlmesh.utils.metaprogramming import build_env, serialize_env @@ -96,9 +96,32 @@ def models( default_catalog_per_gateway: t.Optional[t.Dict[str, str]] = None, **loader_kwargs: t.Any, ) -> t.List[Model]: + blueprints = self.kwargs.pop("blueprints", None) + + if isinstance(blueprints, str): + blueprints = parse_one(blueprints, dialect=dialect) + + if isinstance(blueprints, MacroFunc): + from sqlmesh.core.model.definition import render_expression + + blueprints = render_expression( + expression=blueprints, + module_path=module_path, + macros=loader_kwargs.get("macros"), + jinja_macros=loader_kwargs.get("jinja_macros"), + variables=get_variables(None), + path=path, + dialect=dialect, + default_catalog=loader_kwargs.get("default_catalog"), + ) + if not blueprints: + raise_config_error("Failed to render blueprints property", path) + + blueprints = blueprints[0] + return create_models_from_blueprints( gateway=self.kwargs.get("gateway"), - blueprints=self.kwargs.pop("blueprints", None), + blueprints=blueprints, get_variables=get_variables, loader=self.model, path=path, diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index e64a3092b9..a80262ffcf 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -1849,7 +1849,7 @@ def _extract_blueprints(blueprints: t.Any, path: Path) -> t.List[t.Any]: return blueprints raise_config_error( - "Expected a list or tuple consisting of key-value mappings for" + "Expected a list or tuple consisting of key-value mappings for " f"the 'blueprints' property, got '{blueprints}' instead", path, ) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 5132aade83..a569ee45ae 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -8312,9 +8312,9 @@ def entrypoint(evaluator): def test_dynamic_blueprinting(tmp_path: Path) -> None: init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) - dynamic_template = tmp_path / "models/dynamic_template.sql" - dynamic_template.parent.mkdir(parents=True, exist_ok=True) - dynamic_template.write_text( + dynamic_template_sql = tmp_path / "models/dynamic_template.sql" + dynamic_template_sql.parent.mkdir(parents=True, exist_ok=True) + dynamic_template_sql.write_text( """ MODEL ( name @customer.some_table, @@ -8330,6 +8330,24 @@ def test_dynamic_blueprinting(tmp_path: Path) -> None: """ ) + dynamic_template_py = tmp_path / "models/dynamic_template.py" + dynamic_template_py.parent.mkdir(parents=True, exist_ok=True) + dynamic_template_py.write_text( + """ +from sqlmesh import model + +@model( + "@{customer}.some_other_table", + kind="FULL", + blueprints="@gen_blueprints()", + is_sql=True, +) +def entrypoint(evaluator): + field_a = evaluator.blueprint_var("field_a") + return f"SELECT {field_a}, @BLUEPRINT_VAR('field_b') AS field_b FROM @customer.some_source" +""" + ) + gen_blueprints = tmp_path / "macros/gen_blueprints.py" gen_blueprints.parent.mkdir(parents=True, exist_ok=True) gen_blueprints.write_text( @@ -8347,9 +8365,11 @@ def gen_blueprints(evaluator): config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")), paths=tmp_path ) - assert len(ctx.models) == 2 + assert len(ctx.models) == 4 assert '"memory"."customer1"."some_table"' in ctx.models assert '"memory"."customer2"."some_table"' in ctx.models + assert '"memory"."customer1"."some_other_table"' in ctx.models + assert '"memory"."customer2"."some_other_table"' in ctx.models def test_single_blueprint(tmp_path: Path) -> None: From c13c40fc32ca495b18c79c3aae309a6daba1f3ee Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Fri, 18 Apr 2025 20:13:03 +0300 Subject: [PATCH 0003/1056] Fix: Make date range inclusive for audits ran in `sqlmesh plan` (#4179) --- sqlmesh/core/model/definition.py | 9 ++-- sqlmesh/core/snapshot/evaluator.py | 2 +- tests/core/test_context.py | 70 ++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index a80262ffcf..4eebb2a272 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -491,10 +491,11 @@ def render_audit_query( pass if self.time_column: - where = self.time_column.column.between( - self.convert_to_time_column(start or c.EPOCH, columns_to_types), - self.convert_to_time_column(end or c.EPOCH, columns_to_types), - ) + low, high = [ + self.convert_to_time_column(dt, columns_to_types) + for dt in make_inclusive(start or c.EPOCH, end or c.EPOCH, self.dialect) + ] + where = self.time_column.column.between(low, high) else: where = None diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 402deaf70d..bff25c3005 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -555,7 +555,7 @@ def audit( if wap_id is not None: logger.info( - "Publishing evalaution results for snapshot %s, WAP ID '%s'", + "Publishing evaluation results for snapshot %s, WAP ID '%s'", snapshot.snapshot_id, wap_id, ) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index b7070fc2b1..f14f472b27 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1923,3 +1923,73 @@ def create_log_view(evaluator, view_name): # Validate the schema is retrieved using resolve_template for the environment-specific schema assert log_schema["my_schema"][0] == "db__dev" + + +def test_plan_audit_intervals(tmp_path: pathlib.Path, capsys, caplog): + ctx = Context( + paths=tmp_path, config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ) + + ctx.upsert_model( + load_sql_based_model( + parse( + """ + MODEL ( + name sqlmesh_audit.date_example, + kind INCREMENTAL_BY_TIME_RANGE( + time_column(date_id, '%Y-%m-%d') + ), + cron '@daily', + partitioned_by (date_id), + audits [unique_combination_of_columns(columns=(date_id))] + ); + + WITH sample_table AS ( + SELECT + DATE('2025-02-01') as date_id, + ) + SELECT date_id FROM sample_table WHERE date_id BETWEEN @start_ds AND @end_ds + """ + ) + ) + ) + + ctx.upsert_model( + load_sql_based_model( + parse( + """ + MODEL ( + name sqlmesh_audit.timestamp_example, + kind INCREMENTAL_BY_TIME_RANGE( + time_column(timestamp_id, '%Y-%m-%d %H:%M:%S') + ), + cron '@daily', + partitioned_by (timestamp_id), + audits [unique_combination_of_columns(columns=(timestamp_id))] + ); + + WITH sample_table AS ( + SELECT + TIMESTAMP('2025-02-01') as timestamp_id, + ) + SELECT timestamp_id FROM sample_table WHERE timestamp_id BETWEEN @start_ts AND @end_ts + """ + ) + ) + ) + + ctx.plan( + environment="dev", auto_apply=True, no_prompts=True, start="2025-02-01", end="2025-02-01" + ) + + # Case 1: The timestamp audit should be in the inclusive range ['2025-02-01 00:00:00', '2025-02-01 23:59:59.999999'] + assert ( + """SELECT COUNT(*) FROM (SELECT ("timestamp_id") AS "timestamp_id" FROM (SELECT * FROM "sqlmesh__sqlmesh_audit"."sqlmesh_audit__timestamp_example__2797548448" AS "sqlmesh_audit__timestamp_example__2797548448" WHERE "timestamp_id" BETWEEN CAST('2025-02-01 00:00:00' AS TIMESTAMP) AND CAST('2025-02-01 23:59:59.999999' AS TIMESTAMP)) AS "_q_0" WHERE TRUE GROUP BY ("timestamp_id") HAVING COUNT(*) > 1) AS "audit\"""" + in caplog.text + ) + + # Case 2: The date audit should be in the inclusive range ['2025-02-01', '2025-02-01'] + assert ( + """SELECT COUNT(*) FROM (SELECT ("date_id") AS "date_id" FROM (SELECT * FROM "sqlmesh__sqlmesh_audit"."sqlmesh_audit__date_example__4100277424" AS "sqlmesh_audit__date_example__4100277424" WHERE "date_id" BETWEEN CAST('2025-02-01' AS DATE) AND CAST('2025-02-01' AS DATE)) AS "_q_0" WHERE TRUE GROUP BY ("date_id") HAVING COUNT(*) > 1) AS "audit\"""" + in caplog.text + ) From cf3b0fa5a526553f106e432379be7e80817e6ee9 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 18 Apr 2025 10:45:49 -0700 Subject: [PATCH 0004/1056] Chore: Deprecate Airflow integration (#4180) --- .circleci/continue_config.yml | 40 - .gitignore | 9 - Makefile | 41 +- docs/comparisons.md | 1 - docs/concepts/models/sql_models.md | 2 +- docs/concepts/overview.md | 4 +- docs/development.md | 4 - docs/faq/faq.md | 7 +- docs/guides/configuration.md | 87 +-- docs/guides/connections.md | 4 +- docs/guides/migrations.md | 5 - docs/guides/scheduling.md | 85 --- docs/integrations/airflow.md | 161 ---- docs/integrations/dbt.md | 27 - docs/integrations/engines/bigquery.md | 43 -- docs/integrations/engines/clickhouse.md | 37 - docs/integrations/engines/databricks.md | 70 +- docs/integrations/engines/duckdb.md | 3 - docs/integrations/engines/motherduck.md | 2 +- docs/integrations/engines/mssql.md | 24 - docs/integrations/engines/mysql.md | 28 - docs/integrations/engines/postgres.md | 24 - docs/integrations/engines/redshift.md | 25 - docs/integrations/engines/snowflake.md | 26 +- docs/integrations/engines/spark.md | 30 - docs/integrations/engines/trino.md | 34 +- docs/integrations/github.md | 7 +- docs/integrations/overview.md | 1 - docs/prerequisites.md | 4 - docs/reference/cli.md | 2 +- docs/reference/configuration.md | 44 +- docs/reference/notebook.md | 2 +- examples/airflow/Dockerfile.template | 75 -- examples/airflow/Makefile | 56 -- examples/airflow/README.md | 45 -- examples/airflow/__init__.py | 0 examples/airflow/dags/sqlmesh_integration.py | 9 - examples/airflow/docker_compose_decorator.py | 125 --- examples/airflow/requirements.txt | 1 - examples/airflow/spark_conf/hive-site.xml | 25 - .../airflow/spark_conf/spark-defaults.conf | 7 - examples/sushi/config.py | 22 - examples/sushi_dbt/config.py | 7 - mkdocs.yml | 1 - pyproject.toml | 8 +- pytest.ini | 1 - sqlmesh/cli/example_project.py | 18 - sqlmesh/cli/main.py | 2 +- sqlmesh/core/analytics/collector.py | 2 +- sqlmesh/core/config/__init__.py | 8 +- sqlmesh/core/config/scheduler.py | 307 +------- sqlmesh/core/constants.py | 1 - sqlmesh/core/model/definition.py | 2 +- sqlmesh/core/notification_target.py | 3 +- sqlmesh/core/plan/__init__.py | 2 - sqlmesh/core/plan/evaluator.py | 193 +---- sqlmesh/magics.py | 2 +- sqlmesh/schedulers/airflow/__init__.py | 1 - sqlmesh/schedulers/airflow/api.py | 176 ----- sqlmesh/schedulers/airflow/client.py | 304 -------- sqlmesh/schedulers/airflow/common.py | 130 ---- sqlmesh/schedulers/airflow/dag_generator.py | 716 ------------------ sqlmesh/schedulers/airflow/hooks/__init__.py | 0 sqlmesh/schedulers/airflow/hooks/bigquery.py | 61 -- .../schedulers/airflow/hooks/clickhouse.py | 34 - sqlmesh/schedulers/airflow/hooks/redshift.py | 31 - sqlmesh/schedulers/airflow/integration.py | 250 ------ sqlmesh/schedulers/airflow/mwaa_client.py | 91 --- .../schedulers/airflow/operators/__init__.py | 0 sqlmesh/schedulers/airflow/operators/base.py | 45 -- .../schedulers/airflow/operators/bigquery.py | 53 -- .../airflow/operators/clickhouse.py | 32 - .../airflow/operators/databricks.py | 139 ---- sqlmesh/schedulers/airflow/operators/mssql.py | 28 - sqlmesh/schedulers/airflow/operators/mysql.py | 28 - .../airflow/operators/notification.py | 30 - .../schedulers/airflow/operators/postgres.py | 32 - .../schedulers/airflow/operators/redshift.py | 31 - .../schedulers/airflow/operators/sensor.py | 100 --- .../schedulers/airflow/operators/snowflake.py | 32 - .../airflow/operators/spark_submit.py | 160 ---- .../schedulers/airflow/operators/targets.py | 349 --------- sqlmesh/schedulers/airflow/operators/trino.py | 28 - sqlmesh/schedulers/airflow/plan.py | 268 ------- sqlmesh/schedulers/airflow/plugin.py | 68 -- sqlmesh/schedulers/airflow/state_sync.py | 370 --------- sqlmesh/schedulers/airflow/util.py | 188 ----- tests/common_fixtures.py | 9 - tests/conftest.py | 2 - tests/core/test_config.py | 38 - tests/core/test_plan_evaluator.py | 105 --- tests/dbt/test_integration.py | 1 - tests/fixtures/dbt/sushi_test/config.py | 9 +- tests/schedulers/airflow/__init__.py | 0 tests/schedulers/airflow/conftest.py | 49 -- .../schedulers/airflow/operators/__init__.py | 0 .../schedulers/airflow/operators/fixtures.py | 10 - .../airflow/operators/test_sensor.py | 168 ---- .../airflow/operators/test_targets.py | 216 ------ tests/schedulers/airflow/test_client.py | 457 ----------- tests/schedulers/airflow/test_common.py | 14 - .../schedulers/airflow/test_dag_generator.py | 175 ----- tests/schedulers/airflow/test_end_to_end.py | 59 -- tests/schedulers/airflow/test_integration.py | 154 ---- tests/schedulers/airflow/test_mwaa_client.py | 159 ---- tests/schedulers/airflow/test_plan.py | 617 --------------- 106 files changed, 34 insertions(+), 7788 deletions(-) delete mode 100644 docs/integrations/airflow.md delete mode 100644 examples/airflow/Dockerfile.template delete mode 100644 examples/airflow/Makefile delete mode 100644 examples/airflow/README.md delete mode 100644 examples/airflow/__init__.py delete mode 100644 examples/airflow/dags/sqlmesh_integration.py delete mode 100644 examples/airflow/docker_compose_decorator.py delete mode 100644 examples/airflow/requirements.txt delete mode 100644 examples/airflow/spark_conf/hive-site.xml delete mode 100644 examples/airflow/spark_conf/spark-defaults.conf delete mode 100644 sqlmesh/schedulers/airflow/__init__.py delete mode 100644 sqlmesh/schedulers/airflow/api.py delete mode 100644 sqlmesh/schedulers/airflow/client.py delete mode 100644 sqlmesh/schedulers/airflow/common.py delete mode 100644 sqlmesh/schedulers/airflow/dag_generator.py delete mode 100644 sqlmesh/schedulers/airflow/hooks/__init__.py delete mode 100644 sqlmesh/schedulers/airflow/hooks/bigquery.py delete mode 100644 sqlmesh/schedulers/airflow/hooks/clickhouse.py delete mode 100644 sqlmesh/schedulers/airflow/hooks/redshift.py delete mode 100644 sqlmesh/schedulers/airflow/integration.py delete mode 100644 sqlmesh/schedulers/airflow/mwaa_client.py delete mode 100644 sqlmesh/schedulers/airflow/operators/__init__.py delete mode 100644 sqlmesh/schedulers/airflow/operators/base.py delete mode 100644 sqlmesh/schedulers/airflow/operators/bigquery.py delete mode 100644 sqlmesh/schedulers/airflow/operators/clickhouse.py delete mode 100644 sqlmesh/schedulers/airflow/operators/databricks.py delete mode 100644 sqlmesh/schedulers/airflow/operators/mssql.py delete mode 100644 sqlmesh/schedulers/airflow/operators/mysql.py delete mode 100644 sqlmesh/schedulers/airflow/operators/notification.py delete mode 100644 sqlmesh/schedulers/airflow/operators/postgres.py delete mode 100644 sqlmesh/schedulers/airflow/operators/redshift.py delete mode 100644 sqlmesh/schedulers/airflow/operators/sensor.py delete mode 100644 sqlmesh/schedulers/airflow/operators/snowflake.py delete mode 100644 sqlmesh/schedulers/airflow/operators/spark_submit.py delete mode 100644 sqlmesh/schedulers/airflow/operators/targets.py delete mode 100644 sqlmesh/schedulers/airflow/operators/trino.py delete mode 100644 sqlmesh/schedulers/airflow/plan.py delete mode 100644 sqlmesh/schedulers/airflow/plugin.py delete mode 100644 sqlmesh/schedulers/airflow/state_sync.py delete mode 100644 sqlmesh/schedulers/airflow/util.py delete mode 100644 tests/common_fixtures.py delete mode 100644 tests/schedulers/airflow/__init__.py delete mode 100644 tests/schedulers/airflow/conftest.py delete mode 100644 tests/schedulers/airflow/operators/__init__.py delete mode 100644 tests/schedulers/airflow/operators/fixtures.py delete mode 100644 tests/schedulers/airflow/operators/test_sensor.py delete mode 100644 tests/schedulers/airflow/operators/test_targets.py delete mode 100644 tests/schedulers/airflow/test_client.py delete mode 100644 tests/schedulers/airflow/test_common.py delete mode 100644 tests/schedulers/airflow/test_dag_generator.py delete mode 100644 tests/schedulers/airflow/test_end_to_end.py delete mode 100644 tests/schedulers/airflow/test_integration.py delete mode 100644 tests/schedulers/airflow/test_mwaa_client.py delete mode 100644 tests/schedulers/airflow/test_plan.py diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 1e4f695f58..9e9077ab1f 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -143,39 +143,6 @@ jobs: name: Run tests command: npm --prefix web/client run test - airflow_docker_tests: - machine: - image: ubuntu-2204:2022.10.2 - docker_layer_caching: true - resource_class: large - environment: - PYTEST_XDIST_AUTO_NUM_WORKERS: 8 - SQLMESH__DISABLE_ANONYMIZED_ANALYTICS: "1" - steps: - - checkout - - run: - name: Install envsubst - command: sudo apt-get update && sudo apt-get install gettext-base - - run: - name: Setup python env - command: | - pip3 install --upgrade pip - pip3 install ruamel.yaml==0.16.0 - python3 --version - - run: - name: Run Airflow slow tests - command: make airflow-docker-test-with-env - no_output_timeout: 15m - - run: - name: Collect Airflow logs - command: | - tar -czf ./airflow_logs.tgz -C ./examples/airflow/logs . - mkdir -p /tmp/airflow_logs - cp ./airflow_logs.tgz /tmp/airflow_logs/ - when: on_fail - - store_artifacts: - path: /tmp/airflow_logs - trigger_private_tests: docker: - image: cimg/python:3.12.0 @@ -281,13 +248,6 @@ workflows: - "3.10" - "3.11" - "3.12" - - airflow_docker_tests: - requires: - - style_and_cicd_tests - filters: - branches: - only: - - main - engine_tests_docker: name: engine_<< matrix.engine >> matrix: diff --git a/.gitignore b/.gitignore index f4b62e829b..6305387c02 100644 --- a/.gitignore +++ b/.gitignore @@ -138,15 +138,6 @@ dmypy.json *~ *# -# Airflow example -examples/airflow/Dockerfile -examples/airflow/docker-compose.yaml -examples/airflow/airflow.sh -examples/airflow/.env -examples/airflow/logs -examples/airflow/plugins -examples/airflow/warehouse - *.duckdb *.duckdb.wal diff --git a/Makefile b/Makefile index 3f0bbdc72c..ab1509344b 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ ui-style: SKIP=ruff,ruff-format,mypy pre-commit run --all-files doc-test: - PYTEST_PLUGINS=tests.common_fixtures python -m pytest --doctest-modules sqlmesh/core sqlmesh/utils + python -m pytest --doctest-modules sqlmesh/core sqlmesh/utils package: pip3 install build && python3 -m build @@ -33,24 +33,6 @@ package-tests: publish-tests: package-tests pip3 install twine && python3 -m twine upload -r tobiko-private tests/dist/* -airflow-init: - export AIRFLOW_ENGINE_OPERATOR=spark && make -C ./examples/airflow init - -airflow-run: - make -C ./examples/airflow run - -airflow-stop: - make -C ./examples/airflow stop - -airflow-clean: - make -C ./examples/airflow clean - -airflow-psql: - make -C ./examples/airflow psql - -airflow-spark-sql: - make -C ./examples/airflow spark-sql - docs-serve: mkdocs serve @@ -91,27 +73,10 @@ cicd-test: pytest -n auto -m "fast or slow" --junitxml=test-results/junit-cicd.xml && pytest -m "isolated" core-fast-test: - pytest -n auto -m "fast and not web and not github and not dbt and not airflow and not jupyter" + pytest -n auto -m "fast and not web and not github and not dbt and not jupyter" core-slow-test: - pytest -n auto -m "(fast or slow) and not web and not github and not dbt and not airflow and not jupyter" - -airflow-fast-test: - pytest -n auto -m "fast and airflow" - -airflow-test: - pytest -n auto -m "(fast or slow) and airflow" - -airflow-local-test: - export AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@localhost/airflow && \ - pytest -n 1 -m "docker and airflow" - -airflow-docker-test: - make -C ./examples/airflow docker-test - -airflow-local-test-with-env: install-dev airflow-clean airflow-init airflow-run airflow-local-test airflow-stop - -airflow-docker-test-with-env: install-dev airflow-clean airflow-init airflow-run airflow-docker-test airflow-stop + pytest -n auto -m "(fast or slow) and not web and not github and not dbt and not jupyter" engine-slow-test: pytest -n auto -m "(fast or slow) and engine" diff --git a/docs/comparisons.md b/docs/comparisons.md index fef5b9bc65..ef6049acd6 100644 --- a/docs/comparisons.md +++ b/docs/comparisons.md @@ -37,7 +37,6 @@ SQLMesh aims to be dbt format-compatible. Importing existing dbt projects with m | `Virtual Data Environments` | ❌ | [✅](../concepts/environments) | `Open-source CI/CD bot` | ❌ | [✅](../integrations/github) | `Data consistency enforcement` | ❌ | ✅ -| `Native Airflow integration` | ❌ | [✅](../integrations/airflow) | Interfaces | `CLI` | ✅ | [✅](../reference/cli) | `Paid UI` | ✅ | ❌ diff --git a/docs/concepts/models/sql_models.md b/docs/concepts/models/sql_models.md index 9a6023a5ea..5be04ecca6 100644 --- a/docs/concepts/models/sql_models.md +++ b/docs/concepts/models/sql_models.md @@ -281,7 +281,7 @@ JOIN countries SQLMesh will detect that the model depends on both `employees` and `countries`. When executing this model, it will ensure that `employees` and `countries` are executed first. -External dependencies not defined in SQLMesh are also supported. SQLMesh can either depend on them implicitly through the order in which they are executed, or through signals if you are using [Airflow](../../integrations/airflow.md). +External dependencies not defined in SQLMesh are also supported. SQLMesh can either depend on them implicitly through the order in which they are executed, or through [signals](../../guides/signals.md). Although automatic dependency detection works most of the time, there may be specific cases for which you want to define dependencies manually. You can do so in the `MODEL` DDL with the [dependencies property](./overview.md#properties). diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md index b1c7f288a2..fcfd8cf248 100644 --- a/docs/concepts/overview.md +++ b/docs/concepts/overview.md @@ -68,6 +68,4 @@ SQLMesh automatically runs audits when you apply a `plan` to an environment, or ## Infrastructure and orchestration Every company's data infrastructure is different. SQLMesh is flexible with regard to which engines and orchestration frameworks you use — its only requirement is access to the target SQL/analytics engine. -SQLMesh keeps track of model versions and processed data intervals using your existing infrastructure. If SQLMesh is configured without an external orchestrator (such as Airflow), it automatically creates a `sqlmesh` schema in your data warehouse for its internal metadata. - -If SQLMesh is configured with Airflow, then it will store all its metadata in the Airflow database. Read more about how [SQLMesh integrates with Airflow](../integrations/airflow.md). +SQLMesh keeps track of model versions and processed data intervals using your existing infrastructure. SQLMesh it automatically creates a `sqlmesh` schema in your data warehouse for its internal metadata. diff --git a/docs/development.md b/docs/development.md index 1681958602..90fd47e1a7 100644 --- a/docs/development.md +++ b/docs/development.md @@ -25,10 +25,6 @@ Run more comprehensive tests that run on each commit: ```bash make slow-test ``` -Run Airflow tests that will run when PR is merged to main: -```bash -make airflow-docker-test-with-env -``` Install docs dependencies: ```bash make install-doc diff --git a/docs/faq/faq.md b/docs/faq/faq.md index 23334f8a9e..b4a0d7e4d4 100644 --- a/docs/faq/faq.md +++ b/docs/faq/faq.md @@ -167,14 +167,17 @@ ## Scheduling ??? question "How do I run SQLMesh models on a schedule?" - You can run SQLMesh models using the [built-in scheduler](../guides/scheduling.md#built-in-scheduler) or with the native [Airflow integration](../integrations/airflow.md). + You can run SQLMesh models using the [built-in scheduler](../guides/scheduling.md#built-in-scheduler) or using [Tobiko Cloud](../cloud/features/scheduler/scheduler.md) Both approaches use each model's `cron` parameter to determine when the model should run - see the [question about `cron` above](#cron-question) for more information. The built-in scheduler works by executing the command `sqlmesh run`. A sensible approach to running on your project on a schedule is to use Linux’s `cron` tool to execute `sqlmesh run` on a cadence at least as frequent as your briefest SQLMesh model `cron` parameter. For example, if your most frequent model’s `cron` is hour, the `cron` tool should execute `sqlmesh run` at least every hour. ??? question "How do I use SQLMesh with Airflow?" - SQLMesh has first-class support for Airflow - learn more [here](../integrations/airflow.md). + Tobiko Cloud offers first-class support for Airflow - learn more [here](../cloud/features/scheduler/airflow.md) + +??? question "How do I use SQLMesh with Dagster?" + Tobiko Cloud offers first-class support for Dagster - learn more [here](../cloud/features/scheduler/dagster.md) ## Warnings and Errors diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index e59273d218..e66ff78979 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -501,7 +501,7 @@ These pages describe the connection configuration options for each execution eng Configuration for the state backend connection if different from the data warehouse connection. -The data warehouse connection is used to store SQLMesh state if the `state_connection` key is not specified, unless the configuration uses an Airflow or Google Cloud Composer scheduler. If using one of those schedulers, the scheduler's database is used (not the data warehouse) unless an [Airflow Connection has been configured](../integrations/airflow.md#state-connection). +The data warehouse connection is used to store SQLMesh state if the `state_connection` key is not specified. Unlike data transformations, storing state information requires database transactions. Data warehouses aren’t optimized for executing transactions, and storing state information in them can slow down your project or produce corrupted data due to simultaneous writes to the same table. Therefore, production SQLMesh deployments should use a dedicated state connection. @@ -675,7 +675,7 @@ Configuration for a connection used to run unit tests. An in-memory DuckDB datab ### Scheduler -Identifies which scheduler backend to use. The scheduler backend is used both for storing metadata and for executing [plans](../concepts/plans.md). By default, the scheduler type is set to `builtin`, which uses the existing SQL engine to store metadata. Use the `airflow` type integrate with Airflow. +Identifies which scheduler backend to use. The scheduler backend is used both for storing metadata and for executing [plans](../concepts/plans.md). By default, the scheduler type is set to `builtin`, which uses the existing SQL engine to store metadata. These options are in the [scheduler](../reference/configuration.md#scheduler) section of the configuration reference page. @@ -716,89 +716,6 @@ Example configuration: No additional configuration options are supported by this scheduler type. -#### Airflow - -Example configuration: - -=== "YAML" - - ```yaml linenums="1" - gateways: - my_gateway: - scheduler: - type: airflow - airflow_url: - username: - password: - ``` - -=== "Python" - - An Airflow scheduler is specified with an `AirflowSchedulerConfig` object. - - ```python linenums="1" - from sqlmesh.core.config import ( - Config, - ModelDefaultsConfig, - GatewayConfig, - AirflowSchedulerConfig, - ) - - config = Config( - model_defaults=ModelDefaultsConfig(dialect=), - gateways={ - "my_gateway": GatewayConfig( - scheduler=AirflowSchedulerConfig( - airflow_url=, - username=, - password=, - ), - ), - } - ) - ``` - -See [Airflow Integration Guide](../integrations/airflow.md) for information about how to integrate Airflow with SQLMesh. See the [configuration reference page](../reference/configuration.md#airflow) for a list of all parameters. - -#### Cloud Composer - -The Google Cloud Composer scheduler type shares the same configuration options as the `airflow` type, except for `username` and `password`. Cloud Composer relies on `gcloud` authentication, so the `username` and `password` options are not required. - -Example configuration: - -=== "YAML" - - ```yaml linenums="1" - gateways: - my_gateway: - scheduler: - type: cloud_composer - airflow_url: - ``` - -=== "Python" - - An Google Cloud Composer scheduler is specified with an `CloudComposerSchedulerConfig` object. - - ```python linenums="1" - from sqlmesh.core.config import ( - Config, - ModelDefaultsConfig, - GatewayConfig, - CloudComposerSchedulerConfig, - ) - - config = Config( - model_defaults=ModelDefaultsConfig(dialect=), - gateways={ - "my_gateway": GatewayConfig( - scheduler=CloudComposerSchedulerConfig( - airflow_url=, - ), - ), - } - ) - ``` ### Gateway/connection defaults diff --git a/docs/guides/connections.md b/docs/guides/connections.md index 166c64eb56..e0dca0f7a4 100644 --- a/docs/guides/connections.md +++ b/docs/guides/connections.md @@ -2,8 +2,6 @@ ## Overview -**Note:** The following guide only applies when using the built-in scheduler. Connections are configured differently when using an external scheduler such as Airflow. See the [Scheduling guide](scheduling.md) for more details. - In order to deploy models and to apply changes to them, you must configure a connection to your Data Warehouse and, optionally, connection to the database where the SQLMesh state is stored. This can be done in either the `config.yaml` file in your project folder, or the one in `~/.sqlmesh`. Each connection is configured as part of a gateway which has a unique name associated with it. The gateway name can be used to select a specific combination of connection settings when using the CLI. For example: @@ -23,7 +21,7 @@ sqlmesh --gateway local_db plan ## State connection -By default, the data warehouse connection is also used to store the SQLMesh state, unless the configuration uses an Airflow or Google Cloud Composer scheduler. If using one of those schedulers, the state connection defaults to the scheduler's database. +By default, the data warehouse connection is also used to store the SQLMesh state. The state connection can be changed by providing different connection settings in the `state_connection` key of the gateway configuration: diff --git a/docs/guides/migrations.md b/docs/guides/migrations.md index 2847e1b3af..f65a34460a 100644 --- a/docs/guides/migrations.md +++ b/docs/guides/migrations.md @@ -36,8 +36,3 @@ Migrations should ideally run when no one will be running plan/apply. Migrations should not be run in parallel. Due to these constraints, it is better for a person responsible for managing SQLMesh to manually issue migrations. Therefore, it is not recommended to issue migrations from CI/CD pipelines. - -### Airflow Scheduler Migrations - -If using Airflow, migrations are automatically run after the SQLMesh version is upgraded and cluster is restarted. -Therefore, migrations **should not** be run manually. diff --git a/docs/guides/scheduling.md b/docs/guides/scheduling.md index a533a1f39b..7c5b6c27dc 100644 --- a/docs/guides/scheduling.md +++ b/docs/guides/scheduling.md @@ -4,7 +4,6 @@ SQLMesh currently offers three ways of scheduling model evaluation: * Using [SQLMesh's built-in scheduler](#built-in-scheduler) * Using [Tobiko Cloud](../cloud/features/scheduler/scheduler.md) -* By [integrating with Airflow](#integrating-with-airflow) ## Built-in scheduler @@ -30,87 +29,3 @@ sqlmesh_example.example_incremental_model ━━━━━━━━━━━━ ``` **Note:** The `sqlmesh run` command performs model evaluation based on the missing data intervals identified at the time of running. It does not run continuously, and will exit once evaluation is complete. You must run this command periodically with a cron job, a CI/CD tool like Jenkins, or in a similar fashion. - - -## Integrating with Airflow - -### Configuring the Airflow cluster - -SQLMesh natively integrates with the popular open source workflow orchestrator [Apache Airflow](https://airflow.apache.org/), both self-hosted and managed (e.g. Google Cloud Composer, Amazon MWAA, Astronomer). - -To integrate with [Airflow](../integrations/airflow.md), ensure that you meet the [prerequisites](/prerequisites), then perform the following: - -1. Install the SQLMesh Python package on all nodes of the Airflow cluster using the following command: - - pip install sqlmesh - - **Note:** The Airflow webserver must be restarted after installation. - -2. Within the Airflow `dags/` folder, create a file called `sqlmesh.py`. - -3. Within that file add the following, making sure to replace "spark" with your engine and `spark_catalog` with your default catalog: - - from sqlmesh.schedulers.airflow.integration import SQLMeshAirflow - - sqlmesh_airflow = SQLMeshAirflow("spark", default_catalog="spark_catalog") - - for dag in sqlmesh_airflow.dags: - globals()[dag.dag_id] = dag - - The example above uses `spark` as the engine of choice. Other engines can be configured instead by providing a corresponding string as an argument to the `SQLMeshAirflow` constructor. Supported strings are `"spark"`, `"databricks"`, `"snowflake"`, `"bigquery"`, `"redshift"`, `"trino"`, `"mssql"` and `"mysql"`. See the [Airflow Cluster Configuration](../integrations/airflow.md#airflow-cluster-configuration) for full list of arguments and their descriptions. - -After setup is completed, the `sqlmesh_janitor_dag` DAG should become available in the Airflow UI when filtered by the `sqlmesh` tag: - -![Airflow UI after successful setup](scheduling/airflow_successful_setup.png) - -### Configuring the client - -On the client side, you must configure the connection to your Airflow cluster in the `config.yaml` file as follows: - - default_scheduler: - type: airflow - airflow_url: http://localhost:8080/ - username: airflow - password: airflow - -Alternatively, the configuration above can be generated automatically as part of the project initialization using the `airflow` template: -```bash -sqlmesh init [PROJECT SQL DIALECT] -t airflow -``` - -For Airflow configuration types specific to Google Cloud Composer, configure the file as follows: - - default_scheduler: - type: cloud_composer - airflow_url: https:/XXXXXXXX.composer.googleusercontent.com/ - -**Note:** Guidelines for integrating with managed offerings other than Google Cloud Composer will be added later. - -### Running the `plan` command - -Run the `sqlmesh plan` command to apply all changes on the target Airflow cluster. - -Below is example output from running the `sqlmesh plan` command in the example project generated by the `sqlmesh init` command: -```bash -$ sqlmesh plan -====================================================================== -Successfully Ran 1 tests against duckdb ----------------------------------------------------------------------- -Differences from the `prod` environment: - -Models -└── Added Models: - ├── sqlmesh_example.example_incremental_model - └── sqlmesh_example.example_full_model -Models needing backfill (missing dates): -├── sqlmesh_example.example_incremental_model: (2020-01-01, 2023-02-13) -└── sqlmesh_example.example_full_model: (2023-02-13, 2023-02-13) -Enter the backfill start date (eg. '1 year', '2020-01-01') or blank for the beginning of history: 2023-02-13 -Apply - Backfill Tables [y/n]: y -Waiting for the plan application DAG 'sqlmesh_plan_application__prod__fb88a0c6_16f9_4a3e_93ec_7f8026bc878c' to be provisioned on Airflow -Track plan application progress using link -``` - -Once the command runs, the following DAGs will become available within the Airflow UI: - -![Airflow UI after successful plan application](scheduling/airflow_successful_plan_apply.png) diff --git a/docs/integrations/airflow.md b/docs/integrations/airflow.md deleted file mode 100644 index 63a23de7c9..0000000000 --- a/docs/integrations/airflow.md +++ /dev/null @@ -1,161 +0,0 @@ -# Airflow - -SQLMesh provides first-class support for Airflow with the following capabilities: - -* A Directed Acyclic Graph (DAG) generated dynamically for each model version. Each DAG accounts for all its upstream dependencies defined within SQLMesh, and only runs after upstream DAGs succeed for the time period being processed. -* Each plan application leads to the creation of a dynamically-generated DAG dedicated specifically to that Plan. -* The Airflow [Database Backend](https://airflow.apache.org/docs/apache-airflow/stable/howto/set-up-database.html) is used for persistence of the SQLMesh state, meaning no external storage or additional configuration is required for SQLMesh to work. -* The janitor DAG runs periodically and automatically to clean up DAGs and other SQLMesh artifacts that are no longer needed. -* Support for any SQL engine can be added by providing a custom Airflow Operator. - -## Airflow cluster configuration -To enable SQLMesh support on a target Airflow cluster, the SQLMesh package should first be installed on that cluster. Ensure it is installed with the extras for your engine if needed; for example: `sqlmesh[databricks]` for Databricks. Check [setup.py](https://github.com/TobikoData/sqlmesh/blob/main/setup.py) for a list of extras. - -!!! note - The Airflow Webserver instance(s) must be restarted after **installation** and every time the SQLMesh package is **upgraded**. - -Once the package is installed, the following Python module must be created in the `dags/` folder of the target DAG repository with the following contents: - -```python linenums="1" -from sqlmesh.schedulers.airflow.integration import SQLMeshAirflow - -sqlmesh_airflow = SQLMeshAirflow("spark", default_catalog="spark_catalog") - -for dag in sqlmesh_airflow.dags: - globals()[dag.dag_id] = dag -``` -The name of the module file can be arbitrary, but we recommend something descriptive such as `sqlmesh.py` or `sqlmesh_integration.py`. - -`SQLMeshAirflow` has two required arguments (`engine_operator` and `default_catalog`). Details on these and additional optional arguments below: - -| Argument | Description | Type | Required | -|---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------:|:--------:| -| `engine_operator` | Name or operator to use for creating models. See [Engine Support](#engine-support) for list of options | string or BaseOperator | Y | -| `default_catalog` | The default catalog (also called "database" in other engines) to use when models are defined that do not contain a catalog in their name. This should match the default catalog applied by the connection. | string | Y | -| `engine_operator_args` | The dictionary of arguments that will be passed into the evaluate engine operator during its construction. This can be used to customize parameters such as connection ID. | dict | N | -| `ddl_engine_operator` | The type of the Airflow operator that will be used for environment management. These operations are SQL only. `engine_operator` is used if not provided | string or BaseOperator | N | -| `ddl_engine_operator_args` | Args to be passed into just the environment management operator. This can be used to customize parameters such as connection ID. | dict | N | -| `janitor_interval` | Defines how often the janitor DAG runs. The janitor DAG removes platform-managed DAG instances that are pending deletion from Airflow. Default: 1 hour. | timedelta | N | -| `plan_application_dag_ttl` | Determines the time-to-live period for finished plan application DAGs. Once this period is exceeded, finished plan application DAGs are deleted by the janitor. Default: 2 days. | timedelta | N | -| `external_table_sensor_factory` | A factory function that creates a sensor operator for a given signal payload. See [External signals](#external-signals) for more info | function | N | -| `sensor_mode` | The mode to use for SQLMesh sensors. Supported values are "poke" and "reschedule". See https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/sensors.html for more details. Default: "reschedule" | string | N | -| `high_water_mark_sensor_args` | The dictionary of arguments that will be passed into the high water mark sensor during its construction. | dict | N | -| `external_sensor_args` | The dictionary of arguments that will be passed into the external sensor during its construction. | dict | N | -| `generate_cadence_dags` | Whether to generate cadence DAGs for model versions that are currently deployed to production. | bool | N | - - -### State connection - -By default, SQLMesh uses the Airflow's database connection to read and write its state. - -To configure a different storage backend for the SQLMesh state you need to create a new [Airflow Connection](https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html) with ID `sqlmesh_state_db` and type `Generic`. The configuration should be provided in the connection's `extra` field in JSON format. - -![SQLMesh state connection](airflow/airflow_sqlmesh_state_connection.png) - -Refer to the [Connection Configuration](../reference/configuration.md#connection) for supported fields. - -## SQLMesh client configuration -In your SQLMesh repository, create the following configuration within config.yaml: -```yaml linenums="1" -default_scheduler: - type: airflow - airflow_url: https://:/ - username: - password: -``` - -## External signals - -Sometimes there is a need to postpone the model evaluation until certain external conditions are met. - -For example, a model might refer to an external table and should only be evaluated when the data actually lands upstream. This can be achieved using external signals. - -Signals are defined as part of the model's definition using arbitrary key-value pairs. Additionally, `@start_*` and `@end_*` [macros](../concepts/macros/macro_variables.md) can be used within these values. The macro values will be resolved accordingly at the time of evaluation. - -```sql linenums="1" -MODEL ( - name test_db.test_name, - signals [ - ( - table_name = 'upstream_table_a', - ds = @end_ds, - ), - ( - table_name = 'upstream_table_b', - ds = @end_ds, - hour = @end_hour, - ), - ], -) -``` - -Note that in the example above, `table_name`, `ds`, and `hour` are arbitrary keys defined by the user. - -Now, as part of the SQLMesh integration module, a function needs to be passed into the `SQLMeshAirflow` constructor. This function should accept signal payload and return an Airflow Sensor instance representing this signal. - -```python linenums="1" -import typing as t -from airflow.sensors.base import BaseSensorOperator -from sqlmesh.schedulers.airflow.integration import SQLMeshAirflow - - -def create_external_sensor(signal: t.Dict[str, t.Any]) -> BaseSensorOperator: - table_name = signal["table_name"] - ds = signal["ds"] - hour = signal["hour"] - return MyCustomSensor(partition=f"{table_name}/ds={ds}/hour={hour:02}") - - -sqlmesh_airflow = SQLMeshAirflow( - "spark", - default_catalog="spark_catalog", - external_table_sensor_factory=create_external_sensor, -) -``` - -The `create_external_sensor` function in the example above takes the `signal` dictionary as an argument and returns an instance of `BaseSensorOperator`. The keys in the signal dictionary match the keys provided in the model definition. - -## Engine support -SQLMesh supports a variety of engines in Airflow. Support for each engine is provided by a custom Airflow operator implementation. Below is a list of links to operators supported out of the box with information on how to configure them. - -* [BigQuery](engines/bigquery.md#airflow-scheduler) -* [Databricks](engines/databricks.md#airflow-scheduler) -* [MSSQL](engines/mssql.md#airflow-scheduler) -* [Postgres](engines/postgres.md#airflow-scheduler) -* [Redshift](engines/redshift.md#airflow-scheduler) -* [Snowflake](engines/snowflake.md#airflow-scheduler) -* [Spark](engines/spark.md#airflow-scheduler) -* [Trino](engines/trino.md#airflow-scheduler) -* [MySQL](engines/mysql.md#airflow-scheduler) - -## Managed Airflow instances - -Multiple companies offer managed Airflow instances that integrate with their products. This section describes SQLMesh support for some of the options. - -### Google Cloud Composer - -SQLMesh fully supports Airflow hosted on [Google Cloud Composer](https://cloud.google.com/composer/docs/composer-3/composer-overview) - see the [configuration reference page](../reference/configuration.md#cloud-composer) for more information. - -### Astronomer - -Astronomer provides [managed Airflow instances](https://www.astronomer.io/product/) running on AWS, GCP, and Azure. SQLMesh fully supports Airflow hosted by Astronomer. - -### AWS MWAA - -Due to MWAA not supporting the Airflow REST API, users are required to configure an external state connection for both the [client](../guides/connections.md#state-connection) and [Airflow cluster](#state-connection) to point to the same database. - -Additional dependencies need to be installed: -```bash -pip install "sqlmesh[mwaa]" -``` - -Additionally, the scheduler needs to be configured accordingly: -```yaml linenums="1" -default_scheduler: - type: mwaa - environment: -``` - -### YC Airflow - -SQLMesh fully supports Airflow hosted on Yandex [managed Airflow instances](https://yandex.cloud/en/services/managed-airflow) - see the [configuration reference page](../reference/configuration.md#yc-airflow) for more information. diff --git a/docs/integrations/dbt.md b/docs/integrations/dbt.md index bd1fa9a7f1..50f0bb6c4b 100644 --- a/docs/integrations/dbt.md +++ b/docs/integrations/dbt.md @@ -272,33 +272,6 @@ SQLMesh does not have its own package manager; however, SQLMesh's dbt adapter is ## Documentation Model documentation is available in the [SQLMesh UI](../quickstart/ui.md#2-open-the-sqlmesh-web-ui). -## Using Airflow -To use SQLMesh and dbt projects with Airflow, first configure SQLMesh to use Airflow as described in the [Airflow integrations documentation](./airflow.md). - -Then, install dbt-core within airflow. - -Finally, replace the contents of `config.py` with: - -```bash -> from pathlib import Path -> -> from sqlmesh.core.config import AirflowSchedulerConfig -> from sqlmesh.dbt.loader import sqlmesh_config -> -> config = sqlmesh_config( -> Path(__file__).parent, -> default_scheduler=AirflowSchedulerConfig( -> airflow_url="https://:/", -> username="", -> password="", -> ) -> ) -``` - -See the [Airflow configuration documentation](https://airflow.apache.org/docs/apache-airflow/2.1.0/configurations-ref.html) for a list of all AirflowSchedulerConfig configuration options. Note: only the python config file format is supported for dbt at this time. - -The project is now configured to use airflow. Going forward, this also means that the engine configured in airflow will be used instead of the target engine specified in profiles.yml. - ## Supported dbt jinja methods SQLMesh supports running dbt projects using the majority of dbt jinja methods, including: diff --git a/docs/integrations/engines/bigquery.md b/docs/integrations/engines/bigquery.md index a98d49ac1e..2e12954f8a 100644 --- a/docs/integrations/engines/bigquery.md +++ b/docs/integrations/engines/bigquery.md @@ -165,49 +165,6 @@ pip install "sqlmesh[bigquery]" | `priority` | The priority of the underlying job. (Default: `INTERACTIVE`) | string | N | | `maximum_bytes_billed` | The maximum number of bytes to be billed for the underlying job. | int | N | -## Airflow Scheduler -**Engine Name:** `bigquery` - -In order to share a common implementation across local and Airflow, SQLMesh BigQuery implements its own hook and operator. - -### Installation - -To enable support for this operator, the Airflow BigQuery provider package should be installed on the target Airflow cluster along with SQLMesh with the BigQuery extra: -``` -pip install "apache-airflow-providers-google" -pip install "sqlmesh[bigquery]" -``` - -### Connection info - -The operator requires an [Airflow connection](https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html) to determine the target BigQuery account. Please see [GoogleBaseHook](https://airflow.apache.org/docs/apache-airflow-providers-google/stable/_api/airflow/providers/google/common/hooks/base_google/index.html#airflow.providers.google.common.hooks.base_google.GoogleBaseHook) and [GCP connection](https://airflow.apache.org/docs/apache-airflow-providers-google/stable/connections/gcp.html)for more details. Use the `sqlmesh_google_cloud_bigquery_default` (by default) connection ID instead of the `google_cloud_default` one in the Airflow guide. - -By default, the connection ID is set to `sqlmesh_google_cloud_bigquery_default`, but it can be overridden using the `engine_operator_args` parameter to the `SQLMeshAirflow` instance as in the example below: -```python linenums="1" -sqlmesh_airflow = SQLMeshAirflow( - "bigquery", - default_catalog="", - engine_operator_args={ - "bigquery_conn_id": "" - }, -) -``` - -#### Optional Arguments - -* `location`: Sets the default location for datasets and tables. If not set, BigQuery defaults to US for new datasets. See `location` in [Connection options](#connection-options) for more details. - -```python linenums="1" -sqlmesh_airflow = SQLMeshAirflow( - "bigquery", - default_catalog="", - engine_operator_args={ - "bigquery_conn_id": "", - "location": "" - }, -) -``` - ## Authentication Methods - [oauth](https://google-auth.readthedocs.io/en/master/reference/google.auth.html#google.auth.default) (default) - Related Credential Configuration: diff --git a/docs/integrations/engines/clickhouse.md b/docs/integrations/engines/clickhouse.md index 2f6627c310..ba565dccba 100644 --- a/docs/integrations/engines/clickhouse.md +++ b/docs/integrations/engines/clickhouse.md @@ -393,40 +393,3 @@ If a model has many records in each partition, you may see additional performanc ## Local/Built-in Scheduler **Engine Adapter Type**: `clickhouse` - -## Airflow Scheduler -**Engine Name:** `clickhouse` - -In order to share a common implementation across local and Airflow, SQLMesh ClickHouse implements its own hook and operator. - -By default, the connection ID is set to `sqlmesh_clickhouse_default`, but can be overridden using the `engine_operator_args` parameter to the `SQLMeshAirflow` instance as in the example below: -```python linenums="1" -from sqlmesh.schedulers.airflow import NO_DEFAULT_CATALOG - -sqlmesh_airflow = SQLMeshAirflow( - "clickhouse", - default_catalog=NO_DEFAULT_CATALOG, - engine_operator_args={ - "sqlmesh_clickhouse_conn_id": "" - }, -) -``` - -Note: `NO_DEFAULT_CATALOG` is required for ClickHouse since ClickHouse doesn't support catalogs. - -### Connection options - -| Option | Description | Type | Required | -|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------:|:--------:| -| `type` | Engine type name - must be `clickhouse` | string | Y | -| `user` | The username to log in to your server. | string | Y | -| `host` | The hostname of your server. Do not include the `http://` or `https://` prefix. | string | Y | -| `port` | The port to connect to your server. Default: 8123 for non-encrypted connections, 8443 for encrypted connections and ClickHouse Cloud. | int | Y | -| `cluster` | Name of the ClickHouse cluster on which SQLMesh should create objects. Should not be specified for standalone ClickHouse servers or ClickHouse Cloud. | string | N | -| `use_compression` | Enable compression for ClickHouse HTTP inserts and query results. Default: True | bool | N | -| `compression_method` | Use a specific compression method for inserts and query results - allowed values `lz4`, `zstd`, `br`, or `gzip`. | str | N | -| `query_limit` | Maximum number of rows to return for any query response. Default: 0 (unlimited rows) | int | N | -| `connect_timeout` | HTTP connection timeout in seconds. Default: 10 | int | N | -| `send_receive_timeout` | Send/receive timeout for the HTTP connection in seconds. Default: 300 | int | N | -| `verify` | Validate the ClickHouse server TLS/SSL certificate (hostname, expiration, etc.) if using HTTPS/TLS. Default: True | bool | N | -| `connection_settings` | Arbitrary ClickHouse settings passed to the [`clickhouse-connect` client](https://clickhouse.com/docs/en/integrations/python#client-initialization). | dict[str, any] | N | \ No newline at end of file diff --git a/docs/integrations/engines/databricks.md b/docs/integrations/engines/databricks.md index 32157bc60f..d6a935f163 100644 --- a/docs/integrations/engines/databricks.md +++ b/docs/integrations/engines/databricks.md @@ -2,7 +2,7 @@ This page provides information about how to use SQLMesh with the Databricks SQL engine. It begins with a description of the three methods for connecting SQLMesh to Databricks. -After that is a [Connection Quickstart](#connection-quickstart) that demonstrates how to connect to Databricks, or you can skip directly to information about using Databricks with the [built-in](#localbuilt-in-scheduler) or [airflow](#airflow-scheduler) schedulers. +After that is a [Connection Quickstart](#connection-quickstart) that demonstrates how to connect to Databricks, or you can skip directly to information about using Databricks with the [built-in](#localbuilt-in-scheduler). ## Databricks connection methods @@ -266,74 +266,6 @@ The only relevant SQLMesh configuration parameter is the optional `catalog` para | `disable_databricks_connect` | When running locally, disable the use of Databricks Connect for all model operations (so use SQL Connector for all models) | bool | N | | `disable_spark_session` | Do not use SparkSession if it is available (like when running in a notebook). | bool | N | -## Airflow Scheduler -**Engine Name:** `databricks` / `databricks-submit` / `databricks-sql`. - -Databricks has multiple operators to help differentiate running a SQL query from running a Python script. - -### Engine: `databricks` (Recommended) - -When evaluating models, the SQLMesh Databricks integration implements the [DatabricksSubmitRunOperator](https://airflow.apache.org/docs/apache-airflow-providers-databricks/1.0.0/operators.html). This is needed to be able to run either SQL or Python scripts on the Databricks cluster. - -When performing environment management operations, the SQLMesh Databricks integration is similar to the [DatabricksSqlOperator](https://airflow.apache.org/docs/apache-airflow-providers-databricks/stable/operators/sql.html#databrickssqloperator), and relies on the same [DatabricksSqlHook](https://airflow.apache.org/docs/apache-airflow-providers-databricks/stable/_api/airflow/providers/databricks/hooks/databricks_sql/index.html#airflow.providers.databricks.hooks.databricks_sql.DatabricksSqlHook) implementation. -All environment management operations are SQL-based, and the overhead of submitting jobs can be avoided. - -### Engine: `databricks-submit` - -Whether evaluating models or performing environment management operations, the SQLMesh Databricks integration implements the [DatabricksSubmitRunOperator](https://airflow.apache.org/docs/apache-airflow-providers-databricks/1.0.0/operators.html). - -### Engine: `databricks-sql` - -Forces the SQLMesh Databricks integration to use the operator based on the [DatabricksSqlOperator](https://airflow.apache.org/docs/apache-airflow-providers-databricks/stable/operators/sql.html#databrickssqloperator) for all operations. If your project is pure SQL operations, then this is an option. - -To enable support for this operator, the Airflow Databricks provider package should be installed on the target Airflow cluster along with the SQLMesh package with databricks extra as follows: -``` -pip install apache-airflow-providers-databricks -sqlmesh[databricks] -``` - -The operator requires an [Airflow connection](https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html) to determine the target Databricks cluster. Refer to [Databricks connection](https://airflow.apache.org/docs/apache-airflow-providers-databricks/stable/connections/databricks.html) for more details. SQLMesh requires that `http_path` be defined in the connection since it uses this to determine the cluster for both SQL and submit operators. - -Example format: `databricks://?token=&http_path=` - -By default, the connection ID is set to `databricks_default`, but it can be overridden using both the `engine_operator_args` and the `ddl_engine_operator_args` parameters to the `SQLMeshAirflow` instance. -In addition, one special configuration that the SQLMesh Airflow evaluation operator requires is a dbfs path to store an application to load a given SQLMesh model. Also, a payload is stored that contains the information required for SQLMesh to do the loading. This must be defined in the `evaluate_engine_operator_args` parameter. Example of defining both: - -```python linenums="1" -from sqlmesh.schedulers.airflow.integration import SQLMeshAirflow - -sqlmesh_airflow = SQLMeshAirflow( - "databricks", - default_catalog="", - engine_operator_args={ - "databricks_conn_id": "", - "dbfs_location": "dbfs:/FileStore/sqlmesh", - }, - ddl_engine_operator_args={ - "databricks_conn_id": "", - } -) - -for dag in sqlmesh_airflow.dags: - globals()[dag.dag_id] = dag -``` - -!!! note - If your Databricks connection is configured to run on serverless [DBSQL](https://www.databricks.com/product/databricks-sql), then you need to define `existing_cluster_id` or `new_cluster` in your `engine_operator_args`. - - Example: - - ```python linenums="1" - sqlmesh_airflow = SQLMeshAirflow( - "databricks", - default_catalog="", - engine_operator_args={ - "dbfs_location": "dbfs:/FileStore/sqlmesh", - "existing_cluster_id": "1234-123456-slid123", - } - ) - ``` - ## Model table properties to support altering tables If you are making a change to the structure of a table that is [forward only](../../guides/incremental_time.md#forward-only-models), then you may need to add the following to your model's `physical_properties`: diff --git a/docs/integrations/engines/duckdb.md b/docs/integrations/engines/duckdb.md index 1540f4e0ad..6685059dc4 100644 --- a/docs/integrations/engines/duckdb.md +++ b/docs/integrations/engines/duckdb.md @@ -144,6 +144,3 @@ DuckDB can read data directly from cloud services via extensions (e.g., [httpfs] Loading credentials at runtime using `load_aws_credentials()` or similar functions may fail when using SQLMesh. Instead, create persistent and automatically used authentication credentials with the [DuckDB secrets manager](https://duckdb.org/docs/configuration/secrets_manager.html) (available in DuckDB v0.10.0 or greater). - -## Airflow Scheduler -DuckDB only works when running locally; therefore it does not support Airflow. diff --git a/docs/integrations/engines/motherduck.md b/docs/integrations/engines/motherduck.md index 6c8693a31a..e15ba9f6e4 100644 --- a/docs/integrations/engines/motherduck.md +++ b/docs/integrations/engines/motherduck.md @@ -2,7 +2,7 @@ This page provides information about how to use SQLMesh with MotherDuck. -It begins with a [Connection Quickstart](#connection-quickstart) that demonstrates how to connect to MotherDuck, or you can skip directly to information about using MotherDuck with the built-in or airflow schedules. +It begins with a [Connection Quickstart](#connection-quickstart) that demonstrates how to connect to MotherDuck, or you can skip directly to information about using MotherDuck with the built-in scheduler. ## Connection quickstart diff --git a/docs/integrations/engines/mssql.md b/docs/integrations/engines/mssql.md index 32dbc0191d..1650319d07 100644 --- a/docs/integrations/engines/mssql.md +++ b/docs/integrations/engines/mssql.md @@ -24,27 +24,3 @@ pip install "sqlmesh[mssql]" | `appname` | The application name to use for the connection | string | N | | `conn_properties` | The list of connection properties | list[string] | N | | `autocommit` | Is autocommit mode enabled. Default: false | bool | N | - -## Airflow Scheduler -**Engine Name:** `mssql` - -The SQLMesh MsSql Operator is similar to the [MsSqlOperator](https://airflow.apache.org/docs/apache-airflow-providers-microsoft-mssql/stable/_api/airflow/providers/microsoft/mssql/operators/mssql/index.html), and relies on the same [MsSqlHook](https://airflow.apache.org/docs/apache-airflow-providers-microsoft-mssql/stable/_api/airflow/providers/microsoft/mssql/hooks/mssql/index.html) implementation. - -To enable support for this operator, the Airflow Microsoft MSSQL provider package should be installed on the target Airflow cluster along with SQLMesh with the mssql extra: -``` -pip install "apache-airflow-providers-microsoft-mssql" -pip install "sqlmesh[mssql]" -``` - -The operator requires an [Airflow connection](https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html) to determine the target MSSQL account. Refer to [MSSQL connection](https://airflow.apache.org/docs/apache-airflow-providers-microsoft-mssql/stable/connections/mssql.html) for more details. - -By default, the connection ID is set to `mssql_default`, but can be overridden using the `engine_operator_args` parameter to the `SQLMeshAirflow` instance as in the example below: -```python linenums="1" -sqlmesh_airflow = SQLMeshAirflow( - "mssql", - default_catalog="", - engine_operator_args={ - "mssql_conn_id": "" - }, -) -``` \ No newline at end of file diff --git a/docs/integrations/engines/mysql.md b/docs/integrations/engines/mysql.md index 77bd96d42b..e8426a3f5a 100644 --- a/docs/integrations/engines/mysql.md +++ b/docs/integrations/engines/mysql.md @@ -19,31 +19,3 @@ pip install "sqlmesh[mysql]" | `port` | The port number of the MySQL server | int | N | | `charset` | The character set used for the connection | string | N | | `ssl_disabled` | Is SSL disabled | bool | N | - -## Airflow Scheduler -**Engine Name:** `mysql` - -The SQLMesh MySQL Operator is similar to the [MySQLOperator](https://airflow.apache.org/docs/apache-airflow-providers-mysql/stable/index.html), and relies on the same [MySqlHook](https://airflow.apache.org/docs/apache-airflow-providers-mysql/1.0.0/_api/airflow/providers/mysql/hooks/mysql/index.html) implementation. - -To enable support for this operator, the Airflow MySQL provider package should be installed on the target Airflow cluster along with SQLMesh with the mysql extra: -``` -pip install "apache-airflow-providers-mysql" -pip install "sqlmesh[mysql]" -``` - -The operator requires an [Airflow connection](https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html) to determine the target MySQL account. Refer to [MySQL connection](https://airflow.apache.org/docs/apache-airflow-providers-mysql/stable/connections/mysql.html) for more details. - -By default, the connection ID is set to `mysql_default`, but can be overridden using the `engine_operator_args` parameter to the `SQLMeshAirflow` instance as in the example below: -```python linenums="1" -from sqlmesh.schedulers.airflow import NO_DEFAULT_CATALOG - -sqlmesh_airflow = SQLMeshAirflow( - "mysql", - default_catalog=NO_DEFAULT_CATALOG, - engine_operator_args={ - "mysql_conn_id": "" - }, -) -``` - -Note: `NO_DEFAULT_CATALOG` is required for MySQL since MySQL doesn't support catalogs. \ No newline at end of file diff --git a/docs/integrations/engines/postgres.md b/docs/integrations/engines/postgres.md index b030965fd5..cf1d3e4ce8 100644 --- a/docs/integrations/engines/postgres.md +++ b/docs/integrations/engines/postgres.md @@ -23,27 +23,3 @@ pip install "sqlmesh[postgres]" | `role` | The role to use for authentication with the Postgres server | string | N | | `sslmode` | The security of the connection to the Postgres server | string | N | | `application_name` | The name of the application to use for the connection | string | N | - -## Airflow Scheduler -**Engine Name:** `postgres` - -The SQLMesh Postgres Operator is similar to the [PostgresOperator](https://airflow.apache.org/docs/apache-airflow-providers-postgres/stable/_api/airflow/providers/postgres/operators/postgres/index.html), and relies on the same [PostgresHook](https://airflow.apache.org/docs/apache-airflow-providers-postgres/stable/_api/airflow/providers/postgres/hooks/postgres/index.html) implementation. - -To enable support for this operator, the Airflow Postgres provider package should be installed on the target Airflow cluster along with SQLMesh with the Postgres extra: -``` -pip install "apache-airflow-providers-postgres" -pip install "sqlmesh[postgres]" -``` - -The operator requires an [Airflow connection](https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html) to determine the target Postgres account. Refer to [Postgres connection](https://airflow.apache.org/docs/apache-airflow-providers-postgres/stable/connections/postgres.html) for more details. - -By default, the connection ID is set to `postgres_default`, but can be overridden using the `engine_operator_args` parameter to the `SQLMeshAirflow` instance as in the example below: -```python linenums="1" -sqlmesh_airflow = SQLMeshAirflow( - "postgres", - default_catalog="", - engine_operator_args={ - "postgres_conn_id": "" - }, -) -``` \ No newline at end of file diff --git a/docs/integrations/engines/redshift.md b/docs/integrations/engines/redshift.md index 1ee85d43c1..9341844f7d 100644 --- a/docs/integrations/engines/redshift.md +++ b/docs/integrations/engines/redshift.md @@ -33,28 +33,3 @@ pip install "sqlmesh[redshift]" | `serverless_acct_id` | The account ID of the serverless cluster | string | N | | `serverless_work_group` | The name of work group for serverless end point | string | N | | `enable_merge` | Whether the incremental_by_unique_key model kind will use the native Redshift MERGE operation or SQLMesh's logical merge. (Default: `False`) | bool | N | - - -## Airflow Scheduler -**Engine Name:** `redshift` - -In order to share a common implementation across local and Airflow, SQLMesh's Redshift engine implements its own hook and operator. - -To enable support for this operator, the Airflow Redshift provider package should be installed on the target Airflow cluster along with SQLMesh with the Redshift extra: -``` -pip install "apache-airflow-providers-amazon" -pip install "sqlmesh[redshift]" -``` - -The operator requires an [Airflow connection](https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html) to determine the target Redshift account. Refer to [AmazonRedshiftConnection](https://airflow.apache.org/docs/apache-airflow-providers-amazon/stable/connections/redshift.html#authenticating-to-amazon-redshift) for details on how to define a connection string. - -By default, the connection ID is set to `sqlmesh_redshift_default`, but it can be overridden using the `engine_operator_args` parameter to the `SQLMeshAirflow` instance as in the example below: -```python linenums="1" -sqlmesh_airflow = SQLMeshAirflow( - "redshift", - default_catalog="", - engine_operator_args={ - "redshift_conn_id": "" - }, -) -``` \ No newline at end of file diff --git a/docs/integrations/engines/snowflake.md b/docs/integrations/engines/snowflake.md index a45a47aa7b..0a137d5d04 100644 --- a/docs/integrations/engines/snowflake.md +++ b/docs/integrations/engines/snowflake.md @@ -2,7 +2,7 @@ This page provides information about how to use SQLMesh with the Snowflake SQL engine. -It begins with a [Connection Quickstart](#connection-quickstart) that demonstrates how to connect to Snowflake, or you can skip directly to information about using Snowflake with the [built-in](#localbuilt-in-scheduler) or [airflow](#airflow-scheduler) schedulers. +It begins with a [Connection Quickstart](#connection-quickstart) that demonstrates how to connect to Snowflake, or you can skip directly to information about using Snowflake with the [built-in](#localbuilt-in-scheduler). ## Connection quickstart @@ -513,30 +513,6 @@ __Private Key Bytes__ The authenticator method is assumed to be `snowflake_jwt` when `private_key` is provided, but it can also be explicitly provided in the connection configuration. -## Airflow Scheduler -**Engine Name:** `snowflake` - -The SQLMesh Snowflake Operator is similar to the [SnowflakeOperator](https://airflow.apache.org/docs/apache-airflow-providers-snowflake/stable/operators/snowflake.html), and relies on the same [SnowflakeHook](https://airflow.apache.org/docs/apache-airflow-providers-snowflake/stable/_api/airflow/providers/snowflake/hooks/snowflake/index.html) implementation. - -To enable support for this operator, the Airflow Snowflake provider package should be installed on the target Airflow cluster along with SQLMesh with the Snowflake extra: -``` -pip install "apache-airflow-providers-snowflake[common.sql]" -pip install "sqlmesh[snowflake]" -``` - -The operator requires an [Airflow connection](https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html) to determine the target Snowflake account. Refer to [Snowflake connection](https://airflow.apache.org/docs/apache-airflow-providers-snowflake/stable/connections/snowflake.html) for more details. - -By default, the connection ID is set to `snowflake_default`, but can be overridden using the `engine_operator_args` parameter to the `SQLMeshAirflow` instance as in the example below: -```python linenums="1" -sqlmesh_airflow = SQLMeshAirflow( - "snowflake", - default_catalog="", - engine_operator_args={ - "snowflake_conn_id": "" - }, -) -``` - ## Configuring Virtual Warehouses The Snowflake Virtual Warehouse a model should use can be specified in the `session_properties` attribute of the model definition: diff --git a/docs/integrations/engines/spark.md b/docs/integrations/engines/spark.md index b9d22fdc4a..652d26a614 100644 --- a/docs/integrations/engines/spark.md +++ b/docs/integrations/engines/spark.md @@ -14,36 +14,6 @@ NOTE: Spark may not be used for the SQLMesh [state connection](../../reference/c | `catalog` | The catalog to use when issuing commands. See [Catalog Support](#catalog-support) for details | string | N | | `config` | Key/value pairs to set for the Spark Configuration. | dict | N | -## Airflow Scheduler -**Engine Name:** `spark` - -The SQLMesh Spark operator is very similar to the Airflow [SparkSubmitOperator](https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/stable/operators.html#sparksubmitoperator), and relies on the same [SparkSubmitHook](https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/stable/_api/airflow/providers/apache/spark/hooks/spark_submit/index.html#airflow.providers.apache.spark.hooks.spark_submit.SparkSubmitHook) implementation. - -To enable support for this operator, the Airflow Spark provider package should be installed on the target Airflow cluster as follows: -``` -pip install apache-airflow-providers-apache-spark -``` - -The operator requires an [Airflow connection](https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html) to determine the target cluster, queue, and deploy mode in which the Spark Job should be submitted. Refer to [Apache Spark connection](https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/stable/connections/spark.html) for more details. - -By default, the connection ID is set to `spark_default`, but it can be overridden using the `engine_operator_args` parameter to the `SQLMeshAirflow` instance as in the example below: -```python linenums="1" -sqlmesh_airflow = SQLMeshAirflow( - "spark", - default_catalog="", - engine_operator_args={ - "connection_id": "" - }, -) -``` -Similarly, the `engine_operator_args` parameter can be used to override other job submission parameters, such as number of allocated cores, executors, and so forth. The full list of parameters that can be overridden can be found in `sqlmesh.schedulers.airflow.operators.spark_submit.SQLMeshSparkSubmitOperator`. - -**Cluster mode** -

- -Each Spark job submitted by SQLMesh is a PySpark application that depends on the SQLMesh library in its Driver process (but not in Executors). This means that if the Airflow connection is configured to submit jobs in `cluster` mode as opposed to `client` mode, the user must ensure that the SQLMesh Python library is installed on each node of a cluster where Spark jobs are submitted. This is because there is no way to know in advance which specific node to which a Driver process will be scheduled. No additional configuration is required if the deploy mode is set to `client`. - - ## Catalog Support SQLMesh's Spark integration is only designed/tested with a single catalog usage in mind. diff --git a/docs/integrations/engines/trino.md b/docs/integrations/engines/trino.md index 46c1c623cf..031eafae22 100644 --- a/docs/integrations/engines/trino.md +++ b/docs/integrations/engines/trino.md @@ -203,39 +203,7 @@ SELECT ... This will cause SQLMesh to set the specified `LOCATION` when issuing a `CREATE TABLE` statement. -## Airflow Scheduler -**Engine Name:** `trino` - -The SQLMesh Trino Operator is similar to the [TrinoOperator](https://airflow.apache.org/docs/apache-airflow-providers-trino/stable/operators/trino.html), and relies on the same [TrinoHook](https://airflow.apache.org/docs/apache-airflow-providers-trino/stable/_api/airflow/providers/trino/hooks/trino/index.html) implementation. - -To enable support for this operator, the Airflow Trino provider package should be installed on the target Airflow cluster along with SQLMesh with the Trino extra: -``` -pip install "apache-airflow-providers-trino" -pip install "sqlmesh[trino]" -``` - -The operator requires an [Airflow connection](https://airflow.apache.org/docs/apache-airflow/stable/howto/connection.html) to determine the target Trino account. Refer to [Trino connection](https://airflow.apache.org/docs/apache-airflow-providers-trino/stable/connections.html) for more details. - -By default, the connection ID is set to `trino_default`, but can be overridden using the `engine_operator_args` parameter to the `SQLMeshAirflow` instance as in the example below: -```python linenums="1" -sqlmesh_airflow = SQLMeshAirflow( - "trino", - default_catalog="", - engine_operator_args={ - "trino_conn_id": "" - }, -) -``` -```yaml linenums="1" -gateway_name: - connection: - type: trino - user: [user] - host: [host] - catalog: [catalog] -``` - -### Authentication +## Authentication === "No Auth" | Option | Description | Type | Required | diff --git a/docs/integrations/github.md b/docs/integrations/github.md index 5b950f480b..18f6a41d7c 100644 --- a/docs/integrations/github.md +++ b/docs/integrations/github.md @@ -397,7 +397,7 @@ Commands: ``` ## Example Synchronized Full Workflow -This workflow involves configuring a SQLMesh connection to Databricks and configuring access to GCP to talk to Cloud Composer (Airflow). +This workflow involves configuring a SQLMesh connection to Databricks. ```yaml name: SQLMesh Bot @@ -451,11 +451,6 @@ jobs: - name: Install Dependencies run: pip install -r requirements.txt shell: bash - - id: auth - name: Authenticate to Google Cloud - uses: google-github-actions/auth@v1 - with: - credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}' - name: Run CI/CD Bot run: | sqlmesh_cicd -p ${{ github.workspace }} github --token ${{ secrets.GITHUB_TOKEN }} run-all diff --git a/docs/integrations/overview.md b/docs/integrations/overview.md index 07a01da56c..9f829ceab7 100644 --- a/docs/integrations/overview.md +++ b/docs/integrations/overview.md @@ -3,7 +3,6 @@ ## Tools SQLMesh supports integrations with the following tools: -* [Airflow](airflow.md) * [dbt](dbt.md) * [dlt](dlt.md) * [GitHub Actions](github.md) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 638132cdc4..11acfca64e 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -17,10 +17,6 @@ python --version **Note:** If `python --version` returns 2.x, replace all `python` commands with `python3`, and `pip` with `pip3`. -## Additional prerequisites for integrations - -If integrating with Airflow, you'll also need to install the SQLMesh Python package on all nodes of the Airflow cluster. For more information, refer to [Integrate with Airflow](./guides/scheduling.md#integrating-with-airflow). - ## Next steps Now that your machine meets the prerequisites, [install SQLMesh](installation.md). diff --git a/docs/reference/cli.md b/docs/reference/cli.md index cd110e1b42..dd958d17f8 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -242,7 +242,7 @@ Usage: sqlmesh init [OPTIONS] [SQL_DIALECT] Create a new SQLMesh repository. Options: - -t, --template TEXT Project template. Supported values: airflow, dbt, + -t, --template TEXT Project template. Supported values: dbt, dlt, default, empty. --dlt-pipeline TEXT DLT pipeline for which to generate a SQLMesh project. For use with dlt template. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index fc18727dfa..e428cef3c5 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -135,7 +135,7 @@ gateways: gate1: connection: ... - state_connection: # defaults to `connection` if omitted and not using airflow or google cloud composer scheduler + state_connection: # defaults to `connection` if omitted ... test_connection: # defaults to `connection` if omitted ... @@ -159,7 +159,7 @@ A named gateway key may define any or all of a data warehouse connection, state Some connections use default values if not specified: - The `connection` key may be omitted if a [`default_connection`](#default-connectionsscheduler) is specified. -- The state connection defaults to `connection` unless the configuration uses an Airflow or Google Cloud Composer scheduler. If using one of those schedulers, the state connection defaults to the scheduler's database. +- The state connection defaults to `connection` if omitted. - The test connection defaults to `connection` if omitted. NOTE: Spark and Trino engines may not be used for the state connection. @@ -212,7 +212,7 @@ These pages describe the connection configuration options for each execution eng Identifies which scheduler backend to use. The scheduler backend is used both for storing metadata and for executing [plans](../concepts/plans.md). -By default, the scheduler type is set to `builtin` and uses the gateway's connection to store metadata. Use the `airflow` type to integrate with Airflow. +By default, the scheduler type is set to `builtin` and uses the gateway's connection to store metadata. Below is the list of configuration options specific to each corresponding scheduler type. Find additional details in the [configuration overview scheduler section](../guides/configuration.md#scheduler). @@ -222,44 +222,6 @@ Below is the list of configuration options specific to each corresponding schedu No configuration options are supported by this scheduler type. -#### Airflow - -**Type:** `airflow` - -See [Airflow Integration Guide](../integrations/airflow.md) for information about how to integrate Airflow with SQLMesh. - -| Option | Description | Type | Required | -| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-----: | :------: | -| `airflow_url` | The URL of the Airflow Webserver | string | Y | -| `username` | The Airflow username | string | Y | -| `password` | The Airflow password | string | Y | -| `dag_run_poll_interval_secs` | Determines, in seconds, how often a running DAG can be polled (Default: `10`) | int | N | -| `dag_creation_poll_interval_secs` | Determines, in seconds, how often SQLMesh should check whether a DAG has been created (Default: `30`) | int | N | -| `dag_creation_max_retry_attempts` | Determines the maximum number of attempts that SQLMesh will make while checking for whether a DAG has been created (Default: `10`) | int | N | -| `backfill_concurrent_tasks` | The number of concurrent tasks used for model backfilling during plan application (Default: `4`) | int | N | -| `ddl_concurrent_tasks` | The number of concurrent tasks used for DDL operations like table/view creation, deletion, and so forth (Default: `4`) | int | N | -| `max_snapshot_ids_per_request` | The maximum number of snapshot IDs that can be sent in a single HTTP GET request to the Airflow Webserver (Default: `None`) | int | N | -| `use_state_connection` | Whether to use the `state_connection` configuration to bypass Airflow Webserver and access the SQLMesh state directly (Default: `false`) | boolean | N | -| `default_catalog_override` | Overrides the default catalog value for this project. If specified, this value takes precedence over the default catalog value set on the Airflow side. This only applies in the [multi-repo](../guides/multi_repo.md) setup when different projects require different default catalog values (Default: `None`) | string | N | - - -#### Cloud Composer - -**Type:** `cloud_composer` - -The Google Cloud Composer scheduler type shares the same configuration options as the `airflow` type, except for `username` and `password`. Cloud Composer relies on `gcloud` authentication, so the `username` and `password` options are not required. - -#### YC Airflow - -**Type:** `yc_airflow` - -Yandex Managed Airflow shares similar configuration options with the standard `airflow` type, with the following exceptions: - -- `max_snapshot_ids_per_request`: This option is deprecated and not supported. -- Authentication: YC Airflow requires additional credentials, including both a `token` and a combination of `username` and `password`. - -Unlike the `airflow` type, YC Airflow leverages Yandex Cloud's internal authentication mechanisms. Therefore, all requests to the Airflow API must include a valid Yandex Cloud IAM-token for authentication. - ## Gateway/connection defaults The default gateway and connection keys specify what should happen when gateways or connections are not explicitly specified. Find additional details in the configuration overview page [gateway/connection defaults section](../guides/configuration.md#gatewayconnection-defaults). diff --git a/docs/reference/notebook.md b/docs/reference/notebook.md index 92b078f827..50564be34a 100644 --- a/docs/reference/notebook.md +++ b/docs/reference/notebook.md @@ -86,7 +86,7 @@ positional arguments: options: --template TEMPLATE, -t TEMPLATE - Project template. Supported values: airflow, dbt, + Project template. Supported values: dbt, dlt, default, empty. --dlt-pipeline PIPELINE DLT pipeline for which to generate a SQLMesh project. diff --git a/examples/airflow/Dockerfile.template b/examples/airflow/Dockerfile.template deleted file mode 100644 index 6e74b230a7..0000000000 --- a/examples/airflow/Dockerfile.template +++ /dev/null @@ -1,75 +0,0 @@ -FROM apache/spark:3.5.0-python3 AS spark - -FROM apache/airflow:$AIRFLOW_VERSION-python3.9 - -USER root - -# Fix the airflow user UID -ENV AIRFLOW_UID=$AIRFLOW_UID -RUN usermod -u $AIRFLOW_UID airflow - -# Workaround the expired MySQL GPG key. -RUN rm -f /etc/apt/sources.list.d/mysql.list - -RUN apt-get autoclean -RUN apt-get update - -# Install system packages -RUN apt install -y default-jdk gcc g++ make git - -ENV JAVA_HOME="/usr/lib/jvm/default-java/" - -# Install Spark -COPY --from=spark /opt/spark /opt/spark -RUN chown -R airflow /opt/spark -ENV SPARK_HOME="/opt/spark" -ENV PATH="$PATH:$SPARK_HOME/bin" - -# Install Postgres driver and Iceberg for Spark -RUN curl https://jdbc.postgresql.org/download/postgresql-42.5.0.jar -o /opt/spark/jars/postgresql-42.5.0.jar && \ - curl -L https://search.maven.org/remotecontent?filepath=org/apache/iceberg/iceberg-spark-runtime-3.5_2.12/1.5.1/iceberg-spark-runtime-3.5_2.12-1.5.1.jar -o /opt/spark/jars/iceberg-spark-runtime-3.5_2.12-1.5.1.jar - -# Install Hadoop -RUN curl https://dlcdn.apache.org/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz -o hadoop-3.3.6.tar.gz && \ - tar xf hadoop-3.3.6.tar.gz -C /opt/ && \ - mv /opt/hadoop-3.3.6 /opt/hadoop - -ENV HADOOP_HOME="/opt/hadoop" - -# Install Hive -RUN curl https://storage.googleapis.com/tobiko_public/airflow/apache-hive-3.1.3-bin.tar.gz -o apache-hive-3.1.3-bin.tar.gz && \ - tar xf apache-hive-3.1.3-bin.tar.gz -C /opt/ && \ - mv /opt/apache-hive-3.1.3-bin /opt/hive - -ENV HIVE_HOME="/opt/hive" - -# Airflow connections -ENV AIRFLOW_CONN_SPARK_DEFAULT="spark://local?deploy-mode=client" - -# Airflow configuration -ENV AIRFLOW__SCHEDULER__MIN_FILE_PROCESS_INTERVAL=3 - -# SQLMesh configuration -ENV SQLMESH__DISABLE_ANONYMIZED_ANALYTICS=1 - -USER airflow - -# Install Spark provider for Airflow -# skip install pyspark since it's part of the image -RUN pip install apache-airflow-providers-apache-spark==4.8.0 --no-deps -RUN pip install apache-airflow-providers-databricks==6.4.0 \ - apache-airflow-providers-github==2.6.0 \ - apache-airflow-providers-common-sql==1.13.0 \ - pandas==1.5.2 # spark 3.4 and pandas 2.0 have issues with casting timestamp - -# Install Deps -USER root -ADD pyproject.toml /opt/sqlmesh/pyproject.toml -RUN mkdir /opt/sqlmesh/sqlmesh && touch /opt/sqlmesh/sqlmesh/__init__.py && chown -R airflow /opt/sqlmesh - -ADD examples/custom_materializations /opt/custom_materializations -RUN chown -R airflow /opt/custom_materializations - -USER airflow -RUN cd /opt/sqlmesh && pip install -e .[dbt] -RUN cd /opt/custom_materializations && pip install -e . diff --git a/examples/airflow/Makefile b/examples/airflow/Makefile deleted file mode 100644 index 6a0b618776..0000000000 --- a/examples/airflow/Makefile +++ /dev/null @@ -1,56 +0,0 @@ -AIRFLOW_VERSION ?= 2.9.1 -AIRFLOW_IMAGE_NAME ?= airflow-sqlmesh -AIRFLOW_UID ?= $(shell id -u) - -install-requirements: - pip3 install -r requirements.txt - -download-docker-compose: - curl -LfO 'https://airflow.apache.org/docs/apache-airflow/$(AIRFLOW_VERSION)/docker-compose.yaml' - -decorate-docker-compose: install-requirements download-docker-compose - python3 ./docker_compose_decorator.py - -download-cli: - curl -LfO 'https://airflow.apache.org/docs/apache-airflow/$(AIRFLOW_VERSION)/airflow.sh' && chmod +x airflow.sh - -package-sqlmesh: - make -C ../../ package - -init-folders: - mkdir -p ./dags ./logs ./plugins ./warehouse - -init-airflow-dockerfile: - export AIRFLOW_VERSION=$(AIRFLOW_VERSION) AIRFLOW_UID=$(AIRFLOW_UID) && cat Dockerfile.template | envsubst '$$AIRFLOW_VERSION,$$AIRFLOW_UID' > Dockerfile - -build-airflow-image: init-airflow-dockerfile - cd ../../ && docker build -t $(AIRFLOW_IMAGE_NAME) -f ./examples/airflow/Dockerfile . - -create-metastore-db: build-airflow-image decorate-docker-compose - export AIRFLOW_IMAGE_NAME=$(AIRFLOW_IMAGE_NAME) AIRFLOW_UID=$(AIRFLOW_UID) && docker-compose up --force-recreate create-metastore-db - -provision-metastore-tables: build-airflow-image decorate-docker-compose create-metastore-db - export AIRFLOW_IMAGE_NAME=$(AIRFLOW_IMAGE_NAME) AIRFLOW_UID=$(AIRFLOW_UID) && docker-compose up --force-recreate provision-metastore-tables - -init-airflow: decorate-docker-compose - export AIRFLOW_IMAGE_NAME=$(AIRFLOW_IMAGE_NAME) AIRFLOW_UID=$(AIRFLOW_UID) && docker-compose up --force-recreate airflow-init - -init: decorate-docker-compose download-cli package-sqlmesh init-folders build-airflow-image init-airflow provision-metastore-tables - -run: build-airflow-image - export AIRFLOW_IMAGE_NAME=$(AIRFLOW_IMAGE_NAME) AIRFLOW_UID=$(AIRFLOW_UID) && docker-compose up --force-recreate -d - -stop: - docker-compose down - -clean: decorate-docker-compose - docker-compose down --volumes --remove-orphans && docker rmi -f $(AIRFLOW_IMAGE_NAME) && rm -rf ./logs/* && rm -rf ./warehouse/* - -psql: - docker-compose exec -it postgres psql -U airflow airflow - -spark-sql: - docker-compose exec -it airflow-worker spark-sql - -docker-test: decorate-docker-compose - docker-compose up --force-recreate --exit-code-from sqlmesh-tests sqlmesh-tests diff --git a/examples/airflow/README.md b/examples/airflow/README.md deleted file mode 100644 index fdc8942ae1..0000000000 --- a/examples/airflow/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# SQLMesh Airflow Examples - -## Requirements -1. Docker -2. Docker Compose - -**Note:** the Docker instance must be configured to use 4GB of memory for all containers to run properly. - -## Install and Run -Initialize the Airflow environment first. This should only be done once: -```bash -make init -``` -Run the Airflow cluster in Docker: -```bash -make run -``` -The UI should now become available at [http://localhost:8080/](http://localhost:8080/). The account created has the login `airflow` and the password `airflow`. - -Terminate the Airflow cluster: -```bash -make stop -``` -Clean the environment: -```bash -make clean -``` -Re-create and re-launch the environment in one command: -```bash -make clean init run -``` -Access the Postgres instance with psql: -```bash -make psql -``` -Run the Spark SQL REPL on a running cluster: -```bash -make spark-sql -``` - -## CLI -After installation is complete the Airflow CLI script will become available: -```bash -./airflow.sh -``` diff --git a/examples/airflow/__init__.py b/examples/airflow/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/airflow/dags/sqlmesh_integration.py b/examples/airflow/dags/sqlmesh_integration.py deleted file mode 100644 index 2e1619703e..0000000000 --- a/examples/airflow/dags/sqlmesh_integration.py +++ /dev/null @@ -1,9 +0,0 @@ -import os - -from sqlmesh.schedulers.airflow.integration import SQLMeshAirflow - -engine_operator = os.environ.get("AIRFLOW_ENGINE_OPERATOR", "spark") -sqlmesh_airflow = SQLMeshAirflow(engine_operator, default_catalog="spark_catalog") - -for dag in sqlmesh_airflow.dags: - globals()[dag.dag_id] = dag diff --git a/examples/airflow/docker_compose_decorator.py b/examples/airflow/docker_compose_decorator.py deleted file mode 100644 index e61ea0a699..0000000000 --- a/examples/airflow/docker_compose_decorator.py +++ /dev/null @@ -1,125 +0,0 @@ -import os - -from ruamel.yaml import YAML - -DOCKER_COMPOSE_YAML = "docker-compose.yaml" - -yaml = YAML(typ="safe") -yaml.default_flow_style = False - - -with open(DOCKER_COMPOSE_YAML, "r", encoding="utf-8") as fd: - docker_compose = yaml.load(fd) - -docker_compose["x-airflow-common"]["volumes"].extend( - [ - "./spark_conf:/opt/spark/conf", - "./spark_conf:/opt/hive/conf", - "./warehouse:/opt/warehouse", - "../../:/opt/sqlmesh", - ] -) - -# Dont load Airflow example DAGs because they cause visual pollution -docker_compose["x-airflow-common"]["environment"]["AIRFLOW__CORE__LOAD_EXAMPLES"] = "false" - -docker_compose["services"]["postgres"]["ports"] = ["5432:5432"] - -docker_compose["services"]["create-metastore-db"] = { - "command": [ - "psql", - "-U", - "airflow", - "--host", - "postgres", - "-c", - "CREATE DATABASE metastore_db", - ], - "environment": { - "PGPASSWORD": "airflow", - }, - "image": docker_compose["services"]["postgres"]["image"], - "depends_on": { - "postgres": { - "condition": "service_healthy", - }, - }, - "profiles": [ - "sqlmesh-warehouse-init", - ], -} - -docker_compose["services"]["provision-metastore-tables"] = { - "entrypoint": "/bin/bash", - "command": [ - "-c", - "/opt/hive/bin/schematool -dbType postgres -initSchema", - ], - "image": "airflow-sqlmesh", - "user": "airflow", - "volumes": [ - "./spark_conf:/opt/spark/conf", - "./spark_conf:/opt/hive/conf", - "./warehouse:/opt/warehouse", - ], - "depends_on": { - "postgres": { - "condition": "service_healthy", - }, - }, - "profiles": [ - "sqlmesh-warehouse-init", - ], -} - -docker_compose["services"]["sqlmesh-tests"] = { - "entrypoint": "/bin/bash", - "command": [ - "-c", - "make install-dev && pytest -m 'airflow and docker'", - ], - "image": "airflow-sqlmesh", - "user": "airflow", - "volumes": [ - "./spark_conf:/opt/spark/conf", - "./spark_conf:/opt/hive/conf", - "./warehouse:/opt/warehouse", - "../../:/opt/sqlmesh", - ], - "environment": { - "AIRFLOW__DATABASE__SQL_ALCHEMY_CONN": "postgresql+psycopg2://airflow:airflow@postgres/airflow", - "IS_DOCKER": "true", - }, - "working_dir": "/opt/sqlmesh", - "profiles": [ - "sqlmesh-tests", - ], -} - -engine_operator = os.environ.get("AIRFLOW_ENGINE_OPERATOR", "spark").lower() -for airflow_component in ["airflow-scheduler", "airflow-worker"]: - environment_variables = {"AIRFLOW_ENGINE_OPERATOR": engine_operator} - if engine_operator == "databricks": - if not all( - variable in os.environ - for variable in [ - "DATABRICKS_SERVER_HOSTNAME", - "DATABRICKS_TOKEN", - "DATABRICKS_HTTP_PATH", - ] - ): - raise RuntimeError( - "Tried to use Databricks Airflow Engine operator but did not define `DATABRICKS_SERVER_HOSTNAME`, `DATABRICKS_TOKEN`, `DATABRICKS_HTTP_PATH`" - ) - environment_variables["AIRFLOW_CONN_DATABRICKS_DEFAULT"] = ( - "databricks://${DATABRICKS_SERVER_HOSTNAME}?token=${DATABRICKS_TOKEN}&http_path=${DATABRICKS_HTTP_PATH}" - ) - if os.getenv("DEMO_GITHUB_PAT"): - environment_variables["AIRFLOW_CONN_GITHUB_DEFAULT"] = ( - '{"conn_type": "github", "password": "${DEMO_GITHUB_PAT}"}' - ) - docker_compose["services"][airflow_component]["environment"].update(environment_variables) - - -with open(DOCKER_COMPOSE_YAML, "w", encoding="utf-8") as fd: - yaml.dump(docker_compose, fd) diff --git a/examples/airflow/requirements.txt b/examples/airflow/requirements.txt deleted file mode 100644 index 58d360844e..0000000000 --- a/examples/airflow/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -ruamel.yaml diff --git a/examples/airflow/spark_conf/hive-site.xml b/examples/airflow/spark_conf/hive-site.xml deleted file mode 100644 index 62b857aab7..0000000000 --- a/examples/airflow/spark_conf/hive-site.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - javax.jdo.option.ConnectionURL - jdbc:postgresql://postgres:5432/metastore_db - JDBC connect string for a JDBC metastore - - - javax.jdo.option.ConnectionDriverName - org.postgresql.Driver - Driver class name for a JDBC metastore - - - javax.jdo.option.ConnectionUserName - airflow - - - javax.jdo.option.ConnectionPassword - airflow - - - hive.metastore.warehouse.dir - /opt/warehouse - location of default database for the warehouse - - diff --git a/examples/airflow/spark_conf/spark-defaults.conf b/examples/airflow/spark_conf/spark-defaults.conf deleted file mode 100644 index 6904b823eb..0000000000 --- a/examples/airflow/spark_conf/spark-defaults.conf +++ /dev/null @@ -1,7 +0,0 @@ -spark.hadoop.hive.exec.dynamic.partition true -spark.hadoop.hive.exec.dynamic.partition.mode nonstrict -spark.sql.sources.partitionOverwriteMode dynamic - -spark.sql.extensions org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions -spark.sql.catalog.spark_catalog org.apache.iceberg.spark.SparkSessionCatalog -spark.sql.catalog.spark_catalog.type hive diff --git a/examples/sushi/config.py b/examples/sushi/config.py index e8df58884a..66adfc5531 100644 --- a/examples/sushi/config.py +++ b/examples/sushi/config.py @@ -1,7 +1,6 @@ import os from sqlmesh.core.config import ( - AirflowSchedulerConfig, AutoCategorizationMode, BigQueryConnectionConfig, CategorizerConfig, @@ -11,7 +10,6 @@ GatewayConfig, ModelDefaultsConfig, PlanConfig, - SparkConnectionConfig, ) from sqlmesh.core.config.linter import LinterConfig from sqlmesh.core.notification_target import ( @@ -76,26 +74,6 @@ model_defaults=model_defaults, ) -airflow_config = Config( - default_scheduler=AirflowSchedulerConfig(), - gateways=GatewayConfig( - connection=SparkConnectionConfig( - config_dir=os.path.join(CURRENT_FILE_PATH, "..", "airflow", "spark_conf"), - config={ - "spark.hadoop.javax.jdo.option.ConnectionURL": "jdbc:postgresql://localhost:5432/metastore_db" - }, - ) - ), - model_defaults=model_defaults_iceberg, -) - - -airflow_config_docker = Config( - default_scheduler=AirflowSchedulerConfig(airflow_url="http://airflow-webserver:8080/"), - gateways=GatewayConfig(connection=SparkConnectionConfig()), - model_defaults=model_defaults_iceberg, -) - # A DuckDB config with a physical schema map. map_config = Config( default_connection=DuckDBConnectionConfig(), diff --git a/examples/sushi_dbt/config.py b/examples/sushi_dbt/config.py index d5cdd7b874..e7e28c98e4 100644 --- a/examples/sushi_dbt/config.py +++ b/examples/sushi_dbt/config.py @@ -1,14 +1,7 @@ from pathlib import Path -from sqlmesh.core.config import AirflowSchedulerConfig from sqlmesh.dbt.loader import sqlmesh_config config = sqlmesh_config(Path(__file__).parent) test_config = config - - -airflow_config = sqlmesh_config( - Path(__file__).parent, - default_scheduler=AirflowSchedulerConfig(), -) diff --git a/mkdocs.yml b/mkdocs.yml index ad4f10cb24..212bb49535 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -71,7 +71,6 @@ nav: - Integrations: - "Overview": integrations/overview.md - Tools: - - integrations/airflow.md - integrations/dbt.md - integrations/dlt.md - integrations/github.md diff --git a/pyproject.toml b/pyproject.toml index 8e3bc9a08f..c1485a77df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,9 +47,6 @@ clickhouse = ["clickhouse-connect"] databricks = ["databricks-sql-connector"] dev = [ "agate==1.7.1", - "apache-airflow==2.9.1", - "apache-airflow-providers-fab==1.5.3", # pip is having trouble resolving this dependency of airflow - "opentelemetry-proto==1.27.0", # pip was having trouble resolving this transitive dependency of airflow "beautifulsoup4", "clickhouse-connect", "cryptography", @@ -68,6 +65,7 @@ dev = [ "google-auth", "google-cloud-bigquery", "google-cloud-bigquery-storage", + "httpx", "mypy~=1.13.0", "pandas-stubs", "pre-commit", @@ -131,9 +129,6 @@ sqlmesh = "sqlmesh.cli.main:cli" sqlmesh_cicd = "sqlmesh.cicd.bot:bot" sqlmesh_lsp = "sqlmesh.lsp.main:main" -[project.entry-points."airflow.plugins"] -sqlmesh_airflow = "sqlmesh.schedulers.airflow.plugin:SqlmeshAirflowPlugin" - [project.urls] Homepage = "https://sqlmesh.com/" Documentation = "https://sqlmesh.readthedocs.io/en/stable/" @@ -180,7 +175,6 @@ disable_error_code = "annotation-unchecked" [[tool.mypy.overrides]] module = [ "api.*", - "airflow.*", "astor.*", "IPython.*", "hyperscript.*", diff --git a/pytest.ini b/pytest.ini index a4910b0983..c5f5449f75 100644 --- a/pytest.ini +++ b/pytest.ini @@ -11,7 +11,6 @@ markers = # Test Domain Markers # default: core functionality - airflow: test for Airflow scheduler cli: test for CLI dbt: test for dbt adapter github: test for Github CI/CD bot diff --git a/sqlmesh/cli/example_project.py b/sqlmesh/cli/example_project.py index 7af8c5d0a6..ba674ad5e5 100644 --- a/sqlmesh/cli/example_project.py +++ b/sqlmesh/cli/example_project.py @@ -15,7 +15,6 @@ class ProjectTemplate(Enum): - AIRFLOW = "airflow" DBT = "dbt" DLT = "dlt" DEFAULT = "default" @@ -87,23 +86,6 @@ def _gen_config( model_defaults: dialect: {dialect} start: {start or yesterday_ds()} -""", - ProjectTemplate.AIRFLOW: f"""gateways: - {dialect}: - connection: -{connection_settings} - -default_gateway: {dialect} - -default_scheduler: - type: airflow - airflow_url: http://localhost:8080/ - username: airflow - password: airflow - -model_defaults: - dialect: {dialect} - start: {yesterday_ds()} """, ProjectTemplate.DBT: """from pathlib import Path diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 38b3cb395c..50ee33e85c 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -130,7 +130,7 @@ def cli( "-t", "--template", type=str, - help="Project template. Supported values: airflow, dbt, dlt, default, empty.", + help="Project template. Supported values: dbt, dlt, default, empty.", ) @click.option( "--dlt-pipeline", diff --git a/sqlmesh/core/analytics/collector.py b/sqlmesh/core/analytics/collector.py index e2779d02d1..cfdb60aadd 100644 --- a/sqlmesh/core/analytics/collector.py +++ b/sqlmesh/core/analytics/collector.py @@ -157,7 +157,7 @@ def on_plan_apply_start( plan: The plan that is being applied. engine_type: The type of the target engine. state_sync_type: The type of the engine used to store the SQLMesh state. - scheduler_type: The type of the scheduler being used. Eg. "builtin" or "airflow". + scheduler_type: The type of the scheduler being used. Eg. "builtin". """ self._add_event( "PLAN_APPLY_START", diff --git a/sqlmesh/core/config/__init__.py b/sqlmesh/core/config/__init__.py index 6017fc8895..af84818858 100644 --- a/sqlmesh/core/config/__init__.py +++ b/sqlmesh/core/config/__init__.py @@ -34,10 +34,4 @@ from sqlmesh.core.config.plan import PlanConfig as PlanConfig from sqlmesh.core.config.root import Config as Config from sqlmesh.core.config.run import RunConfig as RunConfig -from sqlmesh.core.config.scheduler import ( - AirflowSchedulerConfig as AirflowSchedulerConfig, - BuiltInSchedulerConfig as BuiltInSchedulerConfig, - CloudComposerSchedulerConfig as CloudComposerSchedulerConfig, - MWAASchedulerConfig as MWAASchedulerConfig, - YCAirflowSchedulerConfig as YCAirflowSchedulerConfig, -) +from sqlmesh.core.config.scheduler import BuiltInSchedulerConfig as BuiltInSchedulerConfig diff --git a/sqlmesh/core/config/scheduler.py b/sqlmesh/core/config/scheduler.py index 1917a466d1..66c70c354f 100644 --- a/sqlmesh/core/config/scheduler.py +++ b/sqlmesh/core/config/scheduler.py @@ -4,29 +4,21 @@ import typing as t from pydantic import Field -from requests import Session from sqlglot.helper import subclasses from sqlmesh.core.config.base import BaseConfig -from sqlmesh.core.config.common import concurrent_tasks_validator -from sqlmesh.core.console import Console, get_console +from sqlmesh.core.console import get_console from sqlmesh.core.plan import ( - AirflowPlanEvaluator, BuiltInPlanEvaluator, - MWAAPlanEvaluator, PlanEvaluator, ) from sqlmesh.core.config import DuckDBConnectionConfig from sqlmesh.core.state_sync import EngineAdapterStateSync, StateSync -from sqlmesh.schedulers.airflow.client import AirflowClient -from sqlmesh.schedulers.airflow.mwaa_client import MWAAClient from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.hashing import md5 -from sqlmesh.utils.pydantic import model_validator, field_validator +from sqlmesh.utils.pydantic import field_validator if t.TYPE_CHECKING: - from google.auth.transport.requests import AuthorizedSession - from sqlmesh.core.context import GenericContext from sqlmesh.utils.config import sensitive_fields, excluded_fields @@ -148,301 +140,6 @@ def get_default_catalog(self, context: GenericContext) -> t.Optional[str]: return context.engine_adapter.default_catalog -class _BaseAirflowSchedulerConfig(_EngineAdapterStateSyncSchedulerConfig): - airflow_url: str - dag_run_poll_interval_secs: int - dag_creation_poll_interval_secs: int - dag_creation_max_retry_attempts: int - - backfill_concurrent_tasks: int - ddl_concurrent_tasks: int - - use_state_connection: bool - - default_catalog_override: t.Optional[str] - - @abc.abstractmethod - def get_client(self, console: t.Optional[Console] = None) -> AirflowClient: - """Constructs the Airflow Client instance.""" - - def create_state_sync(self, context: GenericContext) -> StateSync: - if self.use_state_connection: - return super().create_state_sync(context) - - from sqlmesh.schedulers.airflow.state_sync import HttpStateSync - - return HttpStateSync( - client=self.get_client(context.console), - dag_run_poll_interval_secs=self.dag_run_poll_interval_secs, - console=context.console, - ) - - def state_sync_fingerprint(self, context: GenericContext) -> str: - if self.use_state_connection: - return super().state_sync_fingerprint(context) - return md5([self.airflow_url]) - - def create_plan_evaluator(self, context: GenericContext) -> PlanEvaluator: - return AirflowPlanEvaluator( - airflow_client=self.get_client(context.console), - dag_run_poll_interval_secs=self.dag_run_poll_interval_secs, - dag_creation_poll_interval_secs=self.dag_creation_poll_interval_secs, - dag_creation_max_retry_attempts=self.dag_creation_max_retry_attempts, - console=context.console, - notification_targets=context.notification_targets, - backfill_concurrent_tasks=self.backfill_concurrent_tasks, - ddl_concurrent_tasks=self.ddl_concurrent_tasks, - users=context.users, - state_sync=context.state_sync if self.use_state_connection else None, - ) - - def get_default_catalog(self, context: GenericContext) -> t.Optional[str]: - default_catalog = self.get_client(context.console).default_catalog - return self.default_catalog_override or default_catalog - - -def _max_snapshot_ids_per_request_validator(v: t.Any) -> t.Optional[int]: - get_console().log_warning( - "The `max_snapshot_ids_per_request` field is deprecated and will be removed in a future release." - ) - return None - - -max_snapshot_ids_per_request_validator: t.Any = field_validator( - "max_snapshot_ids_per_request", mode="before" -)(_max_snapshot_ids_per_request_validator) - - -class AirflowSchedulerConfig(_BaseAirflowSchedulerConfig, BaseConfig): - """The Airflow Scheduler configuration. - - Args: - airflow_url: The URL of the Airflow Webserver. - username: The Airflow username. - password: The Airflow password. - dag_run_poll_interval_secs: Determines how often a running DAG can be polled (in seconds). - dag_creation_poll_interval_secs: Determines how often SQLMesh should check whether a DAG has been created (in seconds). - dag_creation_max_retry_attempts: Determines the maximum number of attempts that SQLMesh will make while checking for - whether a DAG has been created. - backfill_concurrent_tasks: The number of concurrent tasks used for model backfilling during plan application. - ddl_concurrent_tasks: The number of concurrent tasks used for DDL operations (table / view creation, deletion, etc). - max_snapshot_ids_per_request: The maximum number of snapshot IDs that can be sent in a single HTTP GET request to the Airflow Webserver (Deprecated). - use_state_connection: Whether to use the `state_connection` configuration to access the SQLMesh state. - default_catalog_override: Overrides the default catalog value for this project. If specified, this value takes precedence - over the default catalog value set on the Airflow side. - """ - - airflow_url: str = "http://localhost:8080/" - username: str = "airflow" - password: str = "airflow" - token: t.Optional[str] = None - dag_run_poll_interval_secs: int = 10 - dag_creation_poll_interval_secs: int = 30 - dag_creation_max_retry_attempts: int = 10 - - backfill_concurrent_tasks: int = 4 - ddl_concurrent_tasks: int = 4 - - max_snapshot_ids_per_request: t.Optional[int] = None - use_state_connection: bool = False - - default_catalog_override: t.Optional[str] = None - - type_: t.Literal["airflow"] = Field(alias="type", default="airflow") - - _concurrent_tasks_validator = concurrent_tasks_validator - _max_snapshot_ids_per_request_validator = max_snapshot_ids_per_request_validator - - def get_client(self, console: t.Optional[Console] = None) -> AirflowClient: - session = Session() - if self.token is None: - session.auth = (self.username, self.password) - else: - session.headers.update({"Authorization": f"Bearer {self.token}"}) - - return AirflowClient( - session=session, - airflow_url=self.airflow_url, - console=console, - ) - - -class YCAirflowSchedulerConfig(_BaseAirflowSchedulerConfig, BaseConfig): - """The Yandex Cloud Managed Airflow Scheduler configuration. - - Args: - airflow_url: The URL of the Airflow Webserver. - username: The Airflow username. - password: The Airflow password. - dag_run_poll_interval_secs: Determines how often a running DAG can be polled (in seconds). - dag_creation_poll_interval_secs: Determines how often SQLMesh should check whether a DAG has been created (in seconds). - dag_creation_max_retry_attempts: Determines the maximum number of attempts that SQLMesh will make while checking for - whether a DAG has been created. - backfill_concurrent_tasks: The number of concurrent tasks used for model backfilling during plan application. - ddl_concurrent_tasks: The number of concurrent tasks used for DDL operations (table / view creation, deletion, etc). - max_snapshot_ids_per_request: The maximum number of snapshot IDs that can be sent in a single HTTP GET request to the Airflow Webserver (Deprecated). - use_state_connection: Whether to use the `state_connection` configuration to access the SQLMesh state. - default_catalog_override: Overrides the default catalog value for this project. If specified, this value takes precedence - over the default catalog value set on the Airflow side. - token: The IAM-token for API authentification. - """ - - airflow_url: str - username: str - password: str - token: str - dag_run_poll_interval_secs: int = 10 - dag_creation_poll_interval_secs: int = 30 - dag_creation_max_retry_attempts: int = 10 - - backfill_concurrent_tasks: int = 4 - ddl_concurrent_tasks: int = 4 - - use_state_connection: bool = False - - default_catalog_override: t.Optional[str] = None - - _concurrent_tasks_validator = concurrent_tasks_validator - - type_: t.Literal["yc_airflow"] = Field(alias="type", default="yc_airflow") - - def get_client(self, console: t.Optional[Console] = None) -> AirflowClient: - session = Session() - - session.auth = (self.username, self.password) - session.headers.update({"X-Cloud-Authorization": f"Bearer {self.token}"}) - - return AirflowClient( - session=session, - airflow_url=self.airflow_url, - console=console, - ) - - -class CloudComposerSchedulerConfig(_BaseAirflowSchedulerConfig, BaseConfig, extra="allow"): - """The Google Cloud Composer configuration. - - Args: - airflow_url: The URL of the Airflow Webserver. - dag_run_poll_interval_secs: Determines how often a running DAG can be polled (in seconds). - dag_creation_poll_interval_secs: Determines how often SQLMesh should check whether a DAG has been created (in seconds). - dag_creation_max_retry_attempts: Determines the maximum number of attempts that SQLMesh will make while checking for - whether a DAG has been created. - backfill_concurrent_tasks: The number of concurrent tasks used for model backfilling during plan application. - ddl_concurrent_tasks: The number of concurrent tasks used for DDL operations (table / view creation, deletion, etc). - max_snapshot_ids_per_request: The maximum number of snapshot IDs that can be sent in a single HTTP GET request to the Airflow Webserver (Deprecated). - use_state_connection: Whether to use the `state_connection` configuration to access the SQLMesh state. - default_catalog_override: Overrides the default catalog value for this project. If specified, this value takes precedence - over the default catalog value set on the Airflow side. - """ - - airflow_url: str - dag_run_poll_interval_secs: int = 10 - dag_creation_poll_interval_secs: int = 30 - dag_creation_max_retry_attempts: int = 10 - - backfill_concurrent_tasks: int = 4 - ddl_concurrent_tasks: int = 4 - - max_snapshot_ids_per_request: t.Optional[int] = 20 - use_state_connection: bool = False - - default_catalog_override: t.Optional[str] = None - - type_: t.Literal["cloud_composer"] = Field(alias="type", default="cloud_composer") - - _concurrent_tasks_validator = concurrent_tasks_validator - _max_snapshot_ids_per_request_validator = max_snapshot_ids_per_request_validator - - def __init__(self, **data: t.Any) -> None: - super().__init__(**data) - self._session: t.Optional[AuthorizedSession] = data.get("session") - - @property - def session(self) -> AuthorizedSession: - import google.auth - from google.auth.transport.requests import AuthorizedSession - - if self._session is None: - self._session = AuthorizedSession( - google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])[0] - ) - self._session.headers.update({"Content-Type": "application/json"}) - return self._session - - def get_client(self, console: t.Optional[Console] = None) -> AirflowClient: - return AirflowClient( - airflow_url=self.airflow_url, - session=self.session, - console=console, - ) - - @model_validator(mode="before") - def check_supported_fields(cls, data: t.Any) -> t.Any: - if not isinstance(data, dict): - return data - - allowed_field_names = {field.alias or name for name, field in cls.all_field_infos().items()} - allowed_field_names.add("session") - - for field_name in data: - if field_name not in allowed_field_names: - raise ValueError(f"Unsupported Field: {field_name}") - - return data - - -class MWAASchedulerConfig(_EngineAdapterStateSyncSchedulerConfig, BaseConfig): - """The AWS MWAA Scheduler configuration. - - Args: - environment: The name of the MWAA environment. - dag_run_poll_interval_secs: Determines how often a running DAG can be polled (in seconds). - dag_creation_poll_interval_secs: Determines how often SQLMesh should check whether a DAG has been created (in seconds). - dag_creation_max_retry_attempts: Determines the maximum number of attempts that SQLMesh will make while checking for - whether a DAG has been created. - backfill_concurrent_tasks: The number of concurrent tasks used for model backfilling during plan application. - ddl_concurrent_tasks: The number of concurrent tasks used for DDL operations (table / view creation, deletion, etc). - default_catalog_override: Overrides the default catalog value for this project. If specified, this value takes precedence - over the default catalog value set on the Airflow side. - """ - - environment: str - dag_run_poll_interval_secs: int = 10 - dag_creation_poll_interval_secs: int = 30 - dag_creation_max_retry_attempts: int = 10 - - backfill_concurrent_tasks: int = 4 - ddl_concurrent_tasks: int = 4 - - default_catalog_override: t.Optional[str] = None - - type_: t.Literal["mwaa"] = Field(alias="type", default="mwaa") - - _concurrent_tasks_validator = concurrent_tasks_validator - - def get_client(self, console: t.Optional[Console] = None) -> MWAAClient: - return MWAAClient(self.environment, console=console) - - def create_plan_evaluator(self, context: GenericContext) -> PlanEvaluator: - return MWAAPlanEvaluator( - client=self.get_client(context.console), - state_sync=context.state_sync, - console=context.console, - dag_run_poll_interval_secs=self.dag_run_poll_interval_secs, - dag_creation_poll_interval_secs=self.dag_creation_poll_interval_secs, - dag_creation_max_retry_attempts=self.dag_creation_max_retry_attempts, - notification_targets=context.notification_targets, - backfill_concurrent_tasks=self.backfill_concurrent_tasks, - ddl_concurrent_tasks=self.ddl_concurrent_tasks, - users=context.users, - ) - - def get_default_catalog(self, context: GenericContext) -> t.Optional[str]: - default_catalog = self.get_client(context.console).default_catalog - return self.default_catalog_override or default_catalog - - SCHEDULER_CONFIG_TO_TYPE = { tpe.all_field_infos()["type_"].default: tpe for tpe in subclasses(__name__, BaseConfig, exclude=(BaseConfig,)) diff --git a/sqlmesh/core/constants.py b/sqlmesh/core/constants.py index 1ae55672b5..131d3a990b 100644 --- a/sqlmesh/core/constants.py +++ b/sqlmesh/core/constants.py @@ -87,7 +87,6 @@ BUILTIN = "builtin" -AIRFLOW = "airflow" DBT = "dbt" NATIVE = "native" HYBRID = "hybrid" diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 4eebb2a272..ffd6b4e474 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2128,7 +2128,7 @@ def load_sql_based_model( raise_config_error("Model must have a name", path) if "default_catalog" in meta_fields: raise_config_error( - "`default_catalog` cannot be set on a per-model basis. It must be set at the connection level or in Airflow.", + "`default_catalog` cannot be set on a per-model basis. It must be set at the connection level.", path, ) diff --git a/sqlmesh/core/notification_target.py b/sqlmesh/core/notification_target.py index 8e739dcb21..fba6e36f66 100644 --- a/sqlmesh/core/notification_target.py +++ b/sqlmesh/core/notification_target.py @@ -70,8 +70,7 @@ class NotificationEvent(str, Enum): class BaseNotificationTarget(PydanticModel, frozen=True): """ Base notification target model. Provides a command for sending notifications that is currently only used - by the built-in scheduler. Other schedulers like Airflow use the configuration of the target itself - to create the notification constructs appropriate for the scheduler. + by the built-in scheduler. Notification functions follow a naming convention of `notify_` + NotificationEvent value. """ diff --git a/sqlmesh/core/plan/__init__.py b/sqlmesh/core/plan/__init__.py index c686e79d18..c918c30554 100644 --- a/sqlmesh/core/plan/__init__.py +++ b/sqlmesh/core/plan/__init__.py @@ -6,9 +6,7 @@ SnapshotIntervals as SnapshotIntervals, ) from sqlmesh.core.plan.evaluator import ( - AirflowPlanEvaluator as AirflowPlanEvaluator, BuiltInPlanEvaluator as BuiltInPlanEvaluator, - MWAAPlanEvaluator as MWAAPlanEvaluator, PlanEvaluator as PlanEvaluator, update_intervals_for_new_snapshots as update_intervals_for_new_snapshots, ) diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index cd44e3d900..907f391589 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -22,9 +22,6 @@ from sqlmesh.core.console import Console, get_console from sqlmesh.core.environment import EnvironmentNamingInfo, execute_environment_statements from sqlmesh.core.macros import RuntimeStage -from sqlmesh.core.notification_target import ( - NotificationTarget, -) from sqlmesh.core.snapshot.definition import Interval, to_view_mapping from sqlmesh.core.plan.definition import EvaluatablePlan from sqlmesh.core.scheduler import Scheduler @@ -40,12 +37,8 @@ from sqlmesh.utils import CompletionStatus from sqlmesh.core.state_sync import StateSync from sqlmesh.core.state_sync.base import PromotionResult -from sqlmesh.core.user import User -from sqlmesh.schedulers.airflow import common as airflow_common -from sqlmesh.schedulers.airflow.client import AirflowClient, BaseAirflowClient -from sqlmesh.schedulers.airflow.mwaa_client import MWAAClient from sqlmesh.utils.concurrency import NodeExecutionFailedError -from sqlmesh.utils.errors import SQLMeshError, PlanError +from sqlmesh.utils.errors import PlanError from sqlmesh.utils.dag import DAG from sqlmesh.utils.date import now @@ -549,190 +542,6 @@ def _restatement_intervals_across_all_environments( return set(snapshots_to_restate.values()) -class BaseAirflowPlanEvaluator(PlanEvaluator): - def __init__( - self, - console: t.Optional[Console], - blocking: bool, - dag_run_poll_interval_secs: int, - dag_creation_poll_interval_secs: int, - dag_creation_max_retry_attempts: int, - ): - self.blocking = blocking - self.dag_run_poll_interval_secs = dag_run_poll_interval_secs - self.dag_creation_poll_interval_secs = dag_creation_poll_interval_secs - self.dag_creation_max_retry_attempts = dag_creation_max_retry_attempts - self.console = console or get_console() - - def evaluate( - self, plan: EvaluatablePlan, circuit_breaker: t.Optional[t.Callable[[], bool]] = None - ) -> None: - plan_request_id = plan.plan_id - self._apply_plan(plan, plan_request_id) - - analytics.collector.on_plan_apply_start( - plan=plan, - engine_type=None, - state_sync_type=None, - scheduler_type=c.AIRFLOW, - ) - - if self.blocking: - plan_application_dag_id = airflow_common.plan_application_dag_id( - plan.environment.name, plan_request_id - ) - - self.console.log_status_update( - f"Waiting for the plan application DAG '{plan_application_dag_id}' to be provisioned on Airflow" - ) - - plan_application_dag_run_id = self.client.wait_for_first_dag_run( - plan_application_dag_id, - self.dag_creation_poll_interval_secs, - self.dag_creation_max_retry_attempts, - ) - - self.client.print_tracking_url( - plan_application_dag_id, - plan_application_dag_run_id, - "plan application", - ) - plan_application_succeeded = self.client.wait_for_dag_run_completion( - plan_application_dag_id, - plan_application_dag_run_id, - self.dag_run_poll_interval_secs, - ) - if not plan_application_succeeded: - msg = "Plan application failed." - logger.info(msg) - raise PlanError(msg) - - self.console.log_success("Plan applied successfully") - - @property - def client(self) -> BaseAirflowClient: - raise NotImplementedError - - def _apply_plan(self, plan: EvaluatablePlan, plan_request_id: str) -> None: - raise NotImplementedError - - -class StateBasedAirflowPlanEvaluator(BaseAirflowPlanEvaluator): - backfill_concurrent_tasks: int - ddl_concurrent_tasks: int - notification_targets: t.Optional[t.List[NotificationTarget]] - users: t.Optional[t.List[User]] - - def _apply_plan(self, plan: EvaluatablePlan, plan_request_id: str) -> None: - from sqlmesh.schedulers.airflow.plan import PlanDagState, create_plan_dag_spec - - plan_application_request = airflow_common.PlanApplicationRequest( - plan=plan, - notification_targets=self.notification_targets or [], - backfill_concurrent_tasks=self.backfill_concurrent_tasks, - ddl_concurrent_tasks=self.ddl_concurrent_tasks, - users=self.users or [], - ) - plan_dag_spec = create_plan_dag_spec(plan_application_request, self.state_sync) - PlanDagState.from_state_sync(self.state_sync).add_dag_spec(plan_dag_spec) - - @property - def state_sync(self) -> StateSync: - raise NotImplementedError - - -class AirflowPlanEvaluator(StateBasedAirflowPlanEvaluator): - def __init__( - self, - airflow_client: AirflowClient, - console: t.Optional[Console] = None, - blocking: bool = True, - dag_run_poll_interval_secs: int = 10, - dag_creation_poll_interval_secs: int = 30, - dag_creation_max_retry_attempts: int = 10, - notification_targets: t.Optional[t.List[NotificationTarget]] = None, - backfill_concurrent_tasks: int = 1, - ddl_concurrent_tasks: int = 1, - users: t.Optional[t.List[User]] = None, - state_sync: t.Optional[StateSync] = None, - ): - super().__init__( - console, - blocking, - dag_run_poll_interval_secs, - dag_creation_poll_interval_secs, - dag_creation_max_retry_attempts, - ) - self._airflow_client = airflow_client - self.notification_targets = notification_targets or [] - self.backfill_concurrent_tasks = backfill_concurrent_tasks - self.ddl_concurrent_tasks = ddl_concurrent_tasks - self.users = users or [] - - self._state_sync = state_sync - - @property - def client(self) -> BaseAirflowClient: - return self._airflow_client - - @property - def state_sync(self) -> StateSync: - if self._state_sync is None: - raise SQLMeshError("State Sync is not configured") - return self._state_sync - - def _apply_plan(self, plan: EvaluatablePlan, plan_request_id: str) -> None: - if self._state_sync is not None: - super()._apply_plan(plan, plan_request_id) - return - - self._airflow_client.apply_plan( - plan, - notification_targets=self.notification_targets, - backfill_concurrent_tasks=self.backfill_concurrent_tasks, - ddl_concurrent_tasks=self.ddl_concurrent_tasks, - users=self.users, - ) - - -class MWAAPlanEvaluator(StateBasedAirflowPlanEvaluator): - def __init__( - self, - client: MWAAClient, - state_sync: StateSync, - console: t.Optional[Console] = None, - blocking: bool = True, - dag_run_poll_interval_secs: int = 10, - dag_creation_poll_interval_secs: int = 30, - dag_creation_max_retry_attempts: int = 10, - notification_targets: t.Optional[t.List[NotificationTarget]] = None, - backfill_concurrent_tasks: int = 1, - ddl_concurrent_tasks: int = 1, - users: t.Optional[t.List[User]] = None, - ): - super().__init__( - console, - blocking, - dag_run_poll_interval_secs, - dag_creation_poll_interval_secs, - dag_creation_max_retry_attempts, - ) - self._mwaa_client = client - self._state_sync = state_sync - self.notification_targets = notification_targets or [] - self.backfill_concurrent_tasks = backfill_concurrent_tasks - self.ddl_concurrent_tasks = ddl_concurrent_tasks - self.users = users or [] - - @property - def client(self) -> BaseAirflowClient: - return self._mwaa_client - - @property - def state_sync(self) -> StateSync: - return self._state_sync - - def update_intervals_for_new_snapshots( snapshots: t.Collection[Snapshot], state_sync: StateSync ) -> None: diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 6bf28a8496..51f5bb1c3b 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -156,7 +156,7 @@ def context(self, line: str) -> None: "--template", "-t", type=str, - help="Project template. Supported values: airflow, dbt, default, empty.", + help="Project template. Supported values: dbt, default, empty.", ) @argument( "--dlt-pipeline", diff --git a/sqlmesh/schedulers/airflow/__init__.py b/sqlmesh/schedulers/airflow/__init__.py deleted file mode 100644 index f5c62217b8..0000000000 --- a/sqlmesh/schedulers/airflow/__init__.py +++ /dev/null @@ -1 +0,0 @@ -NO_DEFAULT_CATALOG = "NO_DEFAULT_CATALOG" diff --git a/sqlmesh/schedulers/airflow/api.py b/sqlmesh/schedulers/airflow/api.py deleted file mode 100644 index cf84d82986..0000000000 --- a/sqlmesh/schedulers/airflow/api.py +++ /dev/null @@ -1,176 +0,0 @@ -from __future__ import annotations - -import json -import logging -import typing as t -from functools import wraps - -from airflow.api_connexion import security -from airflow.www.app import csrf -from flask import Blueprint, Response, jsonify, make_response, request - -from sqlmesh.core import constants as c -from sqlmesh.core.snapshot import SnapshotId, SnapshotNameVersion -from sqlmesh.schedulers.airflow import common, util -from sqlmesh.schedulers.airflow.plan import PlanDagState, create_plan_dag_spec -from sqlmesh.utils.errors import SQLMeshError -from sqlmesh.utils.pydantic import PydanticModel - -logger = logging.getLogger(__name__) - - -sqlmesh_api_v1 = Blueprint( - c.SQLMESH, - __name__, - url_prefix=f"/{common.SQLMESH_API_BASE_PATH}", -) - - -def check_authentication(func: t.Callable) -> t.Callable: - @wraps(func) - def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: - security.check_authentication() - return func(*args, **kwargs) - - return wrapper - - -@sqlmesh_api_v1.route("/plans", methods=["POST"]) -@csrf.exempt -@check_authentication -def apply_plan() -> Response: - try: - plan = common.PlanApplicationRequest.parse_obj(request.json or {}) - with util.scoped_state_sync() as state_sync: - spec = create_plan_dag_spec(plan, state_sync) - PlanDagState.from_state_sync(state_sync).add_dag_spec(spec) - return make_response(jsonify(request_id=spec.request_id), 201) - except Exception as ex: - logger.exception("Failed to create a plan DAG spec from request:\n%s", request.json) - return _error(str(ex)) - - -@sqlmesh_api_v1.route("/environments/") -@csrf.exempt -@check_authentication -def get_environment(name: str) -> Response: - with util.scoped_state_sync() as state_sync: - environment = state_sync.get_environment(name) - if environment is None: - return _error(f"Environment '{name}' was not found", 404) - return _success(environment) - - -@sqlmesh_api_v1.route("/environments") -@csrf.exempt -@check_authentication -def get_environments() -> Response: - with util.scoped_state_sync() as state_sync: - environments = state_sync.get_environments() - return _success(common.EnvironmentsResponse(environments=environments)) - - -@sqlmesh_api_v1.route("/environments//max_interval_end_per_model", methods=["POST"]) -@csrf.exempt -@check_authentication -def max_interval_end_per_model(name: str) -> Response: - max_interval_end_per_model_request = common.MaxIntervalEndPerModelRequest.parse_obj( - request.json or {} - ) - models = max_interval_end_per_model_request.models - with util.scoped_state_sync() as state_sync: - interval_end_per_model = state_sync.max_interval_end_per_model( - name, - set(models) if models is not None else None, - ensure_finalized_snapshots=max_interval_end_per_model_request.ensure_finalized_snapshots, - ) - response = common.IntervalEndResponse( - environment=name, interval_end_per_model=interval_end_per_model - ) - return _success(response) - - -@sqlmesh_api_v1.route("/environments/", methods=["DELETE"]) -@csrf.exempt -@check_authentication -def invalidate_environment(name: str) -> Response: - with util.scoped_state_sync() as state_sync: - try: - state_sync.invalidate_environment(name) - except SQLMeshError as ex: - return _error(str(ex), 400) - - return _success(common.InvalidateEnvironmentResponse(name=name)) - - -@sqlmesh_api_v1.route("/snapshots/search", methods=["POST"]) -@csrf.exempt -@check_authentication -def get_snapshots() -> Response: - snapshots_request = common.SnapshotsRequest.parse_obj(request.json or {}) - snapshot_ids = snapshots_request.snapshot_ids - with util.scoped_state_sync() as state_sync: - if snapshots_request.check_existence: - existing_snapshot_ids = ( - state_sync.snapshots_exist(snapshot_ids) if snapshot_ids is not None else set() - ) - return _success(common.SnapshotIdsResponse(snapshot_ids=existing_snapshot_ids)) - - snapshots = list(state_sync.get_snapshots(snapshot_ids).values()) - return _success(common.SnapshotsResponse(snapshots=snapshots)) - - -@sqlmesh_api_v1.route("/models") -@csrf.exempt -@check_authentication -def nodes_exist() -> Response: - with util.scoped_state_sync() as state_sync: - names = _csv_arg("names") - exclude_external = "exclude_external" in request.args - existing_models = state_sync.nodes_exist(names, exclude_external=exclude_external) - return _success(common.ExistingModelsResponse(names=list(existing_models))) - - -@sqlmesh_api_v1.route("/versions") -@csrf.exempt -@check_authentication -def get_versions() -> Response: - with util.scoped_state_sync() as state_sync: - versions = state_sync.get_versions() - assert versions - return _success(versions) - - -T = t.TypeVar("T", bound=PydanticModel) - - -def _success(data: T, status_code: int = 200) -> Response: - response = make_response(data.json(), status_code) - response.mimetype = "application/json" - return response - - -def _error(message: str, status_code: int = 400) -> Response: - return make_response(jsonify(message=message), status_code) - - -def _snapshot_ids_from_request() -> t.Optional[t.List[SnapshotId]]: - if "ids" not in request.args: - return None - - raw_ids = json.loads(request.args["ids"]) - return [SnapshotId.parse_obj(i) for i in raw_ids] - - -def _snapshot_name_versions_from_request() -> t.Optional[t.List[SnapshotNameVersion]]: - if "versions" not in request.args: - return None - - raw_versions = json.loads(request.args["versions"]) - return [SnapshotNameVersion.parse_obj(v) for v in raw_versions] - - -def _csv_arg(arg: str) -> t.List[str]: - if arg not in request.args: - return [] - return [v.strip() for v in request.args[arg].split(",")] diff --git a/sqlmesh/schedulers/airflow/client.py b/sqlmesh/schedulers/airflow/client.py deleted file mode 100644 index c616f715e1..0000000000 --- a/sqlmesh/schedulers/airflow/client.py +++ /dev/null @@ -1,304 +0,0 @@ -import abc -import time -import typing as t -import uuid -from urllib.parse import urlencode, urljoin - -import requests - -from sqlmesh.core.console import Console -from sqlmesh.core.environment import Environment -from sqlmesh.core.notification_target import NotificationTarget -from sqlmesh.core.plan.definition import EvaluatablePlan -from sqlmesh.core.snapshot import Snapshot, SnapshotId -from sqlmesh.core.state_sync import Versions -from sqlmesh.core.user import User -from sqlmesh.schedulers.airflow import common, NO_DEFAULT_CATALOG -from sqlmesh.utils.errors import ( - ApiServerError, - NotFoundError, - SQLMeshError, - raise_for_status, -) - -DAG_RUN_PATH_TEMPLATE = "api/v1/dags/{}/dagRuns" - - -PLANS_PATH = f"{common.SQLMESH_API_BASE_PATH}/plans" -ENVIRONMENTS_PATH = f"{common.SQLMESH_API_BASE_PATH}/environments" -SNAPSHOTS_PATH = f"{common.SQLMESH_API_BASE_PATH}/snapshots/search" -SEEDS_PATH = f"{common.SQLMESH_API_BASE_PATH}/seeds" -INTERVALS_PATH = f"{common.SQLMESH_API_BASE_PATH}/intervals" -MODELS_PATH = f"{common.SQLMESH_API_BASE_PATH}/models" -VERSIONS_PATH = f"{common.SQLMESH_API_BASE_PATH}/versions" - - -class BaseAirflowClient(abc.ABC): - def __init__(self, airflow_url: str, console: t.Optional[Console]): - self._airflow_url = airflow_url - if not self._airflow_url.endswith("/"): - self._airflow_url += "/" - self._console = console - - @property - def default_catalog(self) -> t.Optional[str]: - default_catalog = self.get_variable(common.DEFAULT_CATALOG_VARIABLE_NAME) - if not default_catalog: - raise SQLMeshError( - "Must define `default_catalog` when creating `SQLMeshAirflow` object. See docs for more info: https://sqlmesh.readthedocs.io/en/stable/integrations/airflow/#airflow-cluster-configuration" - ) - if default_catalog == NO_DEFAULT_CATALOG: - return None - return default_catalog - - def print_tracking_url(self, dag_id: str, dag_run_id: str, op_name: str) -> None: - if not self._console: - return - - tracking_url = self.dag_run_tracking_url(dag_id, dag_run_id) - # TODO: Figure out generalized solution for links - self._console.log_status_update( - f"Track [green]{op_name}[/green] progress using [link={tracking_url}]link[/link]" - ) - - def dag_run_tracking_url(self, dag_id: str, dag_run_id: str) -> str: - url_params = urlencode( - dict( - dag_id=dag_id, - run_id=dag_run_id, - ) - ) - return urljoin(self._airflow_url, f"dagrun_details?{url_params}") - - def wait_for_dag_run_completion( - self, dag_id: str, dag_run_id: str, poll_interval_secs: int - ) -> bool: - """Blocks until the given DAG Run completes. - - Args: - dag_id: The DAG ID. - dag_run_id: The DAG Run ID. - poll_interval_secs: The number of seconds to wait between polling for the DAG Run state. - - Returns: - True if the DAG Run completed successfully, False otherwise. - """ - loading_id = self._console_loading_start() - - while True: - state = self.get_dag_run_state(dag_id, dag_run_id) - if state in ("failed", "success"): - if self._console and loading_id: - self._console.loading_stop(loading_id) - return state == "success" - - time.sleep(poll_interval_secs) - - def wait_for_first_dag_run(self, dag_id: str, poll_interval_secs: int, max_retries: int) -> str: - """Blocks until the first DAG Run for the given DAG ID is created. - - Args: - dag_id: The DAG ID. - poll_interval_secs: The number of seconds to wait between polling for the DAG Run. - max_retries: The maximum number of retries. - - Returns: - The ID of the first DAG Run for the given DAG ID. - """ - - loading_id = self._console_loading_start() - - attempt_num = 1 - - try: - while True: - try: - first_dag_run_id = self.get_first_dag_run_id(dag_id) - if first_dag_run_id is None: - raise SQLMeshError(f"Missing a DAG Run for DAG '{dag_id}'") - return first_dag_run_id - except ApiServerError: - raise - except SQLMeshError: - if attempt_num > max_retries: - raise - - attempt_num += 1 - time.sleep(poll_interval_secs) - finally: - if self._console and loading_id: - self._console.loading_stop(loading_id) - - @abc.abstractmethod - def get_first_dag_run_id(self, dag_id: str) -> t.Optional[str]: - """Returns the ID of the first DAG Run for the given DAG ID, or None if no DAG Runs exist. - - Args: - dag_id: The DAG ID. - - Returns: - The ID of the first DAG Run for the given DAG ID, or None if no DAG Runs exist. - """ - - @abc.abstractmethod - def get_dag_run_state(self, dag_id: str, dag_run_id: str) -> str: - """Returns the state of the given DAG Run. - - Args: - dag_id: The DAG ID. - dag_run_id: The DAG Run ID. - - Returns: - The state of the given DAG Run. - """ - - @abc.abstractmethod - def get_variable(self, key: str) -> t.Optional[str]: - """Returns the value of an Airflow variable with the given key. - - Args: - key: The variable key. - - Returns: - The variable value or None if no variable with the given key exists. - """ - - def _console_loading_start(self) -> t.Optional[uuid.UUID]: - if self._console: - return self._console.loading_start() - return None - - -class AirflowClient(BaseAirflowClient): - def __init__( - self, - session: requests.Session, - airflow_url: str, - console: t.Optional[Console] = None, - ): - super().__init__(airflow_url, console) - self._session = session - - def apply_plan( - self, - plan: EvaluatablePlan, - notification_targets: t.Optional[t.List[NotificationTarget]] = None, - backfill_concurrent_tasks: int = 1, - ddl_concurrent_tasks: int = 1, - users: t.Optional[t.List[User]] = None, - ) -> None: - request = common.PlanApplicationRequest( - plan=plan, - notification_targets=notification_targets or [], - backfill_concurrent_tasks=backfill_concurrent_tasks, - ddl_concurrent_tasks=ddl_concurrent_tasks, - users=users or [], - ) - self._post(PLANS_PATH, request.json()) - - def get_snapshots(self, snapshot_ids: t.Optional[t.List[SnapshotId]]) -> t.List[Snapshot]: - snapshots_request = common.SnapshotsRequest(snapshot_ids=snapshot_ids) - response = self._post(SNAPSHOTS_PATH, snapshots_request.json()) - return common.SnapshotsResponse.parse_obj(response).snapshots - - def snapshots_exist(self, snapshot_ids: t.List[SnapshotId]) -> t.Set[SnapshotId]: - snapshots_request = common.SnapshotsRequest(snapshot_ids=snapshot_ids, check_existence=True) - response = self._post(SNAPSHOTS_PATH, snapshots_request.json()) - return set(common.SnapshotIdsResponse.parse_obj(response).snapshot_ids) - - def nodes_exist(self, names: t.Iterable[str], exclude_external: bool = False) -> t.Set[str]: - flags = ["exclude_external"] if exclude_external else [] - return set( - common.ExistingModelsResponse.parse_obj( - self._get(MODELS_PATH, *flags, names=",".join(names)) - ).names - ) - - def get_environment(self, environment: str) -> t.Optional[Environment]: - try: - response = self._get(f"{ENVIRONMENTS_PATH}/{environment}") - return Environment.parse_obj(response) - except NotFoundError: - return None - - def get_environments(self) -> t.List[Environment]: - response = self._get(ENVIRONMENTS_PATH) - return common.EnvironmentsResponse.parse_obj(response).environments - - def max_interval_end_per_model( - self, - environment: str, - models: t.Optional[t.Collection[str]], - ensure_finalized_snapshots: bool, - ) -> t.Dict[str, int]: - max_interval_end_per_model_request = common.MaxIntervalEndPerModelRequest( - models=models, ensure_finalized_snapshots=ensure_finalized_snapshots - ) - response = self._post( - f"{ENVIRONMENTS_PATH}/{environment}/max_interval_end_per_model", - max_interval_end_per_model_request.json(), - ) - return common.IntervalEndResponse.parse_obj(response).interval_end_per_model - - def invalidate_environment(self, environment: str) -> None: - response = self._session.delete( - urljoin(self._airflow_url, f"{ENVIRONMENTS_PATH}/{environment}") - ) - raise_for_status(response) - - def get_versions(self) -> Versions: - return Versions.parse_obj(self._get(VERSIONS_PATH)) - - def get_dag_run_state(self, dag_id: str, dag_run_id: str) -> str: - url = f"{DAG_RUN_PATH_TEMPLATE.format(dag_id)}/{dag_run_id}" - return self._get(url)["state"].lower() - - def get_janitor_dag(self) -> t.Dict[str, t.Any]: - return self._get_dag(common.JANITOR_DAG_ID) - - def get_snapshot_dag(self, name: str, version: str) -> t.Dict[str, t.Any]: - return self._get_dag(common.dag_id_for_name_version(name, version)) - - def get_all_dags(self) -> t.Dict[str, t.Any]: - return self._get("api/v1/dags") - - def get_first_dag_run_id(self, dag_id: str) -> t.Optional[str]: - dag_runs_response = self._get(f"{DAG_RUN_PATH_TEMPLATE.format(dag_id)}", limit="1") - dag_runs = dag_runs_response["dag_runs"] - if not dag_runs: - return None - return dag_runs[0]["dag_run_id"] - - def get_variable(self, key: str) -> t.Optional[str]: - try: - variables_response = self._get(f"api/v1/variables/{key}") - return variables_response["value"] - except NotFoundError: - return None - - def close(self) -> None: - self._session.close() - - def _get_dag(self, dag_id: str) -> t.Dict[str, t.Any]: - return self._get(f"api/v1/dags/{dag_id}") - - def _get(self, path: str, *flags: str, **params: str) -> t.Dict[str, t.Any]: - response = self._session.get(self._url(path, *flags, **params)) - raise_for_status(response) - return response.json() - - def _post(self, path: str, data: str, *flags: str, **params: str) -> t.Dict[str, t.Any]: - response = self._session.post( - self._url(path, *flags, **params), - data=data, - headers={"Content-Type": "application/json"}, - ) - raise_for_status(response) - return response.json() - - def _url(self, path: str, *flags: str, **params: str) -> str: - all_params = [*flags, *([urlencode(params)] if params else [])] - query_string = "&".join(all_params) - if query_string: - path = f"{path}?{query_string}" - return urljoin(self._airflow_url, path) diff --git a/sqlmesh/schedulers/airflow/common.py b/sqlmesh/schedulers/airflow/common.py deleted file mode 100644 index 01fc40a188..0000000000 --- a/sqlmesh/schedulers/airflow/common.py +++ /dev/null @@ -1,130 +0,0 @@ -from __future__ import annotations - -import typing as t - -from sqlmesh.core import constants as c -from sqlmesh.core.environment import Environment -from sqlmesh.core.notification_target import NotificationTarget -from sqlmesh.core.plan.definition import EvaluatablePlan -from sqlmesh.core.snapshot import ( - DeployabilityIndex, - Snapshot, - SnapshotId, - SnapshotInfoLike, - SnapshotIntervals, - SnapshotTableInfo, -) -from sqlmesh.core.user import User -from sqlmesh.utils import sanitize_name -from sqlmesh.utils.date import TimeLike, DatetimeRanges -from sqlmesh.utils.pydantic import PydanticModel - -JANITOR_DAG_ID = "sqlmesh_janitor_dag" -JANITOR_TASK_ID = "janitor_task" - -SQLMESH_AIRFLOW_TAG = "sqlmesh" -SNAPSHOT_AIRFLOW_TAG = "sqlmesh_snapshot" -PLAN_AIRFLOW_TAG = "sqlmesh_plan" - -SNAPSHOT_CLEANUP_COMMAND_XCOM_KEY = "snapshot_cleanup_command" - -DEFAULT_CATALOG_VARIABLE_NAME = "sqlmesh_default_catalog" - -AIRFLOW_LOCAL_URL = "http://localhost:8080/" - -SQLMESH_API_BASE_PATH: str = f"{c.SQLMESH}/api/v1" - -SnapshotToDatetimeRanges = t.Dict[Snapshot, DatetimeRanges] - - -class PlanApplicationRequest(PydanticModel): - plan: EvaluatablePlan - notification_targets: t.List[NotificationTarget] - backfill_concurrent_tasks: int - ddl_concurrent_tasks: int - users: t.List[User] - - -class BackfillIntervalsPerSnapshot(PydanticModel): - snapshot_id: SnapshotId - intervals: DatetimeRanges - before_promote: bool = True - - -class PlanDagSpec(PydanticModel): - request_id: str - environment: Environment - new_snapshots: t.List[Snapshot] - backfill_intervals_per_snapshot: t.List[BackfillIntervalsPerSnapshot] - demoted_snapshots: t.List[SnapshotTableInfo] - unpaused_dt: t.Optional[TimeLike] = None - no_gaps: bool - notification_targets: t.List[NotificationTarget] - backfill_concurrent_tasks: int - ddl_concurrent_tasks: int - users: t.List[User] - is_dev: bool - allow_destructive_snapshots: t.Set[str] - forward_only: t.Optional[bool] = None - dag_start_ts: t.Optional[int] = None - deployability_index: DeployabilityIndex = DeployabilityIndex.all_deployable() - deployability_index_for_creation: DeployabilityIndex = DeployabilityIndex.all_deployable() - no_gaps_snapshot_names: t.Optional[t.Set[str]] = None - models_to_backfill: t.Optional[t.Set[str]] = None - ensure_finalized_snapshots: bool = False - directly_modified_snapshots: t.Optional[t.List[SnapshotId]] = None - indirectly_modified_snapshots: t.Optional[t.Dict[str, t.List[SnapshotId]]] = None - removed_snapshots: t.Optional[t.List[SnapshotId]] = None - execution_time: t.Optional[TimeLike] = None - - -class EnvironmentsResponse(PydanticModel): - environments: t.List[Environment] - - -class SnapshotsRequest(PydanticModel): - snapshot_ids: t.Optional[t.List[SnapshotId]] = None - check_existence: bool = False - - -class SnapshotsResponse(PydanticModel): - snapshots: t.List[Snapshot] - - -class SnapshotIntervalsResponse(PydanticModel): - snapshot_intervals: t.List[SnapshotIntervals] - - -class SnapshotIdsResponse(PydanticModel): - snapshot_ids: t.List[SnapshotId] - - -class ExistingModelsResponse(PydanticModel): - names: t.List[str] - - -class InvalidateEnvironmentResponse(PydanticModel): - name: str - - -class MaxIntervalEndPerModelRequest(PydanticModel): - models: t.Optional[t.List[str]] = None - ensure_finalized_snapshots: bool = False - - -class IntervalEndResponse(PydanticModel): - environment: str - interval_end_per_model: t.Dict[str, int] - - -def dag_id_for_snapshot_info(info: SnapshotInfoLike) -> str: - assert info.version - return dag_id_for_name_version(info.name, info.version) - - -def dag_id_for_name_version(name: str, version: str) -> str: - return f"sqlmesh_snapshot_{sanitize_name(name)}_{version}_dag" - - -def plan_application_dag_id(environment: str, request_id: str) -> str: - return f"sqlmesh_plan_application__{environment}__{request_id}" diff --git a/sqlmesh/schedulers/airflow/dag_generator.py b/sqlmesh/schedulers/airflow/dag_generator.py deleted file mode 100644 index bcee379805..0000000000 --- a/sqlmesh/schedulers/airflow/dag_generator.py +++ /dev/null @@ -1,716 +0,0 @@ -from __future__ import annotations - -import logging -import os -import typing as t - -import pendulum -from airflow import DAG -from airflow.models import BaseOperator -from airflow.operators.python import PythonOperator -from airflow.sensors.base import BaseSensorOperator - -from sqlmesh.core.environment import Environment, EnvironmentNamingInfo -from sqlmesh.core.notification_target import NotificationTarget -from sqlmesh.core.plan import PlanStatus -from sqlmesh.core.snapshot import ( - DeployabilityIndex, - Snapshot, - SnapshotId, - SnapshotIdLike, - SnapshotTableInfo, -) -from sqlmesh.core.state_sync import StateReader -from sqlmesh.schedulers.airflow import common, util -from sqlmesh.schedulers.airflow.operators import targets -from sqlmesh.schedulers.airflow.operators.sensor import ( - ExternalSensor, - HighWaterMarkSensor, -) -from sqlmesh.schedulers.airflow.operators.notification import ( - BaseNotificationOperatorProvider, -) -from sqlmesh.utils import sanitize_name -from sqlmesh.utils.date import TimeLike, to_datetime, yesterday_timestamp -from sqlmesh.utils.errors import SQLMeshError - -try: - from airflow.operators.empty import EmptyOperator -except ImportError: - from airflow.operators.dummy import DummyOperator as EmptyOperator # type: ignore - -logger = logging.getLogger(__name__) - - -TASK_ID_DATE_FORMAT = "%Y-%m-%d_%H-%M-%S" - -NOTIFICATION_TARGET_TO_OPERATOR_PROVIDER: t.Dict[ - t.Type[NotificationTarget], BaseNotificationOperatorProvider -] = {} - -DAG_DEFAULT_ARGS = { - # `AIRFLOW__CORE__DEFAULT_TASK_RETRY_DELAY` support added in 2.4.0 - # We can't use `AIRFLOW__CORE__DEFAULT_TASK_RETRY_DELAY` because cloud composer doesn't allow you to set config - # from an environment variable - "retry_delay": int( - os.getenv( - "SQLMESH_AIRFLOW_DEFAULT_TASK_RETRY_DELAY", - os.getenv("AIRFLOW__CORE__DEFAULT_TASK_RETRY_DELAY", "300"), - ) - ), -} - -AIRFLOW_TAG_CHARACTER_LIMIT = 100 - - -class SnapshotDagGenerator: - def __init__( - self, - engine_operator: t.Type[BaseOperator], - engine_operator_args: t.Optional[t.Dict[str, t.Any]], - ddl_engine_operator: t.Type[BaseOperator], - ddl_engine_operator_args: t.Optional[t.Dict[str, t.Any]], - external_table_sensor_factory: t.Optional[ - t.Callable[[t.Dict[str, t.Any]], BaseSensorOperator] - ], - sensor_mode: str, - high_water_mark_sensor_args: t.Optional[t.Dict[str, t.Any]], - external_sensor_args: t.Optional[t.Dict[str, t.Any]], - state_reader: StateReader, - ): - self._engine_operator = engine_operator - self._engine_operator_args = engine_operator_args or {} - self._ddl_engine_operator = ddl_engine_operator - self._ddl_engine_operator_args = ddl_engine_operator_args or {} - self._external_table_sensor_factory = external_table_sensor_factory - self._state_reader = state_reader - self._sensor_mode = sensor_mode - self._high_water_mark_sensor_args = high_water_mark_sensor_args or {} - self._external_sensor_args = external_sensor_args or {} - - def generate_cadence_dags(self, snapshots: t.Iterable[SnapshotIdLike]) -> t.List[DAG]: - dags = [] - snapshots = self._state_reader.get_snapshots(snapshots) - for snapshot in snapshots.values(): - if snapshot.unpaused_ts and not snapshot.is_symbolic and not snapshot.is_seed: - dags.append(self._create_cadence_dag_for_snapshot(snapshot, snapshots)) - return dags - - def generate_plan_application_dag(self, spec: common.PlanDagSpec) -> t.Optional[DAG]: - try: - return self._create_plan_application_dag(spec) - except Exception: - logger.exception("Failed to generate the plan application DAG '%s'", spec.request_id) - return None - - def _create_cadence_dag_for_snapshot( - self, snapshot: Snapshot, snapshots: t.Dict[SnapshotId, Snapshot] - ) -> DAG: - dag_id = common.dag_id_for_snapshot_info(snapshot.table_info) - logger.info( - "Generating the cadence DAG '%s' for snapshot %s", - dag_id, - snapshot.snapshot_id, - ) - - if not snapshot.unpaused_ts: - raise SQLMeshError( - f"Can't create a cadence DAG for the paused snapshot {snapshot.snapshot_id}" - ) - - end_date = None - if snapshot.node.end: - end_date = pendulum.instance(to_datetime(snapshot.node.end)) - - with DAG( - dag_id=dag_id, - schedule_interval=snapshot.node.cron, - start_date=pendulum.instance(to_datetime(snapshot.unpaused_ts)), - end_date=end_date, - max_active_runs=1, - catchup=True, - is_paused_upon_creation=False, - tags=[ - common.SQLMESH_AIRFLOW_TAG, - common.SNAPSHOT_AIRFLOW_TAG, - snapshot.node.name[-AIRFLOW_TAG_CHARACTER_LIMIT:], - *[tag[:AIRFLOW_TAG_CHARACTER_LIMIT] for tag in snapshot.node.tags], - ], - default_args={ - **DAG_DEFAULT_ARGS, - "email": snapshot.node.owner, - "email_on_failure": True, - }, - ) as dag: - hwm_sensor_tasks = self._create_hwm_sensors(snapshot, snapshots) - - evaluator_task = self._create_snapshot_evaluation_operator( - snapshots=snapshots, - snapshot=snapshot, - task_id="snapshot_evaluator", - ) - - hwm_sensor_tasks >> evaluator_task - - return dag - - def _create_plan_application_dag(self, plan_dag_spec: common.PlanDagSpec) -> DAG: - dag_id = common.plan_application_dag_id( - plan_dag_spec.environment.name, plan_dag_spec.request_id - ) - logger.info( - "Generating the plan application DAG '%s' for environment '%s'", - dag_id, - plan_dag_spec.environment.name, - ) - - all_snapshots = { - **{s.snapshot_id: s for s in plan_dag_spec.new_snapshots}, - **self._state_reader.get_snapshots(plan_dag_spec.environment.snapshots), - } - - snapshots_to_create = [ - all_snapshots[snapshot.snapshot_id] - for snapshot in plan_dag_spec.environment.snapshots - if snapshot.snapshot_id in all_snapshots - and ( - plan_dag_spec.models_to_backfill is None - or snapshot.name in plan_dag_spec.models_to_backfill - ) - ] - - with DAG( - dag_id=dag_id, - schedule_interval="@once", - start_date=pendulum.instance( - to_datetime(plan_dag_spec.dag_start_ts or yesterday_timestamp()) - ), - max_active_tasks=plan_dag_spec.backfill_concurrent_tasks, - catchup=False, - is_paused_upon_creation=False, - default_args=DAG_DEFAULT_ARGS, - tags=[ - common.SQLMESH_AIRFLOW_TAG, - common.PLAN_AIRFLOW_TAG, - plan_dag_spec.environment.name, - ], - ) as dag: - start_task = EmptyOperator(task_id="plan_application_start") - end_task = EmptyOperator(task_id="plan_application_end") - - (create_start_task, create_end_task) = self._create_creation_tasks( - snapshots_to_create, - plan_dag_spec.new_snapshots, - plan_dag_spec.ddl_concurrent_tasks, - plan_dag_spec.deployability_index_for_creation, - plan_dag_spec.allow_destructive_snapshots, - plan_dag_spec.request_id, - ) - - ( - backfill_before_promote_start_task, - backfill_before_promote_end_task, - ) = self._create_backfill_tasks( - [i for i in plan_dag_spec.backfill_intervals_per_snapshot if i.before_promote], - all_snapshots, - plan_dag_spec.deployability_index, - plan_dag_spec.environment.plan_id, - "before_promote", - plan_dag_spec.execution_time, - ) - - ( - backfill_after_promote_start_task, - backfill_after_promote_end_task, - ) = self._create_backfill_tasks( - [i for i in plan_dag_spec.backfill_intervals_per_snapshot if not i.before_promote], - all_snapshots, - plan_dag_spec.deployability_index, - plan_dag_spec.environment.plan_id, - "after_promote", - plan_dag_spec.execution_time, - ) - - ( - promote_start_task, - promote_end_task, - ) = self._create_promotion_demotion_tasks(plan_dag_spec, all_snapshots) - - start_task >> create_start_task - create_end_task >> backfill_before_promote_start_task - backfill_before_promote_end_task >> promote_start_task - - update_views_task_pair = self._create_update_views_tasks(plan_dag_spec, all_snapshots) - if update_views_task_pair: - backfill_after_promote_end_task >> update_views_task_pair[0] - before_finalize_task = update_views_task_pair[1] - else: - before_finalize_task = backfill_after_promote_end_task - - unpause_snapshots_task = self._create_unpause_snapshots_task(plan_dag_spec) - if unpause_snapshots_task: - if not plan_dag_spec.ensure_finalized_snapshots: - # Only unpause right after updatign the environment record if we don't - # have to use the finalized snapshots for subsequent plan applications. - promote_end_task >> unpause_snapshots_task - unpause_snapshots_task >> backfill_after_promote_start_task - else: - # Otherwise, unpause right before finalizing the environment. - promote_end_task >> backfill_after_promote_start_task - before_finalize_task >> unpause_snapshots_task - before_finalize_task = unpause_snapshots_task - else: - promote_end_task >> backfill_after_promote_start_task - - finalize_task = self._create_finalize_task(plan_dag_spec.environment) - before_finalize_task >> finalize_task - finalize_task >> end_task - - on_plan_apply_end_task = PythonOperator( - task_id="on_plan_apply_end", - python_callable=on_plan_apply_end, - op_kwargs={"plan_id": plan_dag_spec.environment.plan_id}, - trigger_rule="all_done", - ) - finalize_task >> on_plan_apply_end_task - - self._add_notification_target_tasks(plan_dag_spec, start_task, finalize_task) - return dag - - def _add_notification_target_tasks( - self, - request: common.PlanDagSpec, - start_task: BaseOperator, - end_task: BaseOperator, - ) -> None: - for notification_target in request.notification_targets: - notification_operator_provider = NOTIFICATION_TARGET_TO_OPERATOR_PROVIDER.get( - type(notification_target) - ) - if not notification_operator_provider: - continue - plan_start_notification_task = notification_operator_provider.operator( - notification_target, PlanStatus.STARTED, request - ) - plan_success_notification_task = notification_operator_provider.operator( - notification_target, PlanStatus.FINISHED, request - ) - plan_failed_notification_task = notification_operator_provider.operator( - notification_target, PlanStatus.FAILED, request - ) - if plan_start_notification_task: - start_task >> plan_start_notification_task - if plan_success_notification_task: - end_task >> plan_success_notification_task - if plan_failed_notification_task: - end_task >> plan_failed_notification_task - - def _create_creation_tasks( - self, - snapshots_to_create: t.List[Snapshot], - new_snapshots: t.List[Snapshot], - ddl_concurrent_tasks: int, - deployability_index: DeployabilityIndex, - allow_destructive_snapshots: t.Set[str], - request_id: str, - ) -> t.Tuple[BaseOperator, BaseOperator]: - start_task = EmptyOperator(task_id="snapshot_creation_start") - end_task = EmptyOperator(task_id="snapshot_creation_end", trigger_rule="none_failed") - - current_task: BaseOperator = start_task - - if snapshots_to_create: - creation_task = self._create_snapshot_create_tables_operator( - snapshots_to_create, - ddl_concurrent_tasks, - deployability_index, - allow_destructive_snapshots, - "snapshot_creation__create_tables", - ) - current_task >> creation_task - current_task = creation_task - - if new_snapshots: - update_state_task = PythonOperator( - task_id="snapshot_creation__update_state", - python_callable=creation_update_state_task, - op_kwargs={"new_snapshots": new_snapshots, "request_id": request_id}, - ) - current_task >> update_state_task - current_task = update_state_task - - current_task >> end_task - - return (start_task, end_task) - - def _create_promotion_demotion_tasks( - self, - request: common.PlanDagSpec, - snapshots: t.Dict[SnapshotId, Snapshot], - ) -> t.Tuple[BaseOperator, BaseOperator]: - update_state_task = PythonOperator( - task_id="snapshot_promotion_update_state", - python_callable=promotion_update_state_task, - op_kwargs={ - "environment": request.environment, - "no_gaps_snapshot_names": ( - request.no_gaps_snapshot_names if request.no_gaps else set() - ), - }, - ) - - start_task = update_state_task - end_task: BaseOperator = update_state_task - - if request.environment.promoted_snapshots and not request.is_dev and request.unpaused_dt: - migrate_tables_task = self._create_snapshot_migrate_tables_operator( - [ - snapshots[s.snapshot_id] - for s in request.environment.promoted_snapshots - if snapshots[s.snapshot_id].is_paused - ], - request.ddl_concurrent_tasks, - request.allow_destructive_snapshots, - "snapshot_promotion_migrate_tables", - ) - update_state_task >> migrate_tables_task - end_task = migrate_tables_task - - return (start_task, end_task) - - def _create_unpause_snapshots_task( - self, request: common.PlanDagSpec - ) -> t.Optional[BaseOperator]: - if request.is_dev or not request.unpaused_dt: - return None - return PythonOperator( - task_id="snapshot_promotion_unpause_snapshots", - python_callable=promotion_unpause_snapshots_task, - op_kwargs={ - "environment": request.environment, - "unpaused_dt": request.unpaused_dt, - }, - trigger_rule="none_failed", - ) - - def _create_update_views_tasks( - self, request: common.PlanDagSpec, snapshots: t.Dict[SnapshotId, Snapshot] - ) -> t.Optional[t.Tuple[BaseOperator, BaseOperator]]: - create_views_task = None - delete_views_task = None - - environment_naming_info = request.environment.naming_info - - if request.environment.promoted_snapshots: - create_views_task = self._create_snapshot_promotion_operator( - [snapshots[x.snapshot_id] for x in request.environment.promoted_snapshots], - environment_naming_info, - request.ddl_concurrent_tasks, - request.deployability_index, - "snapshot_promotion_create_views", - ) - - if request.demoted_snapshots: - delete_views_task = self._create_snapshot_demotion_operator( - request.demoted_snapshots, - environment_naming_info, - request.ddl_concurrent_tasks, - "snapshot_promotion_delete_views", - ) - - if create_views_task and delete_views_task: - create_views_task >> delete_views_task - return create_views_task, delete_views_task - if create_views_task: - return create_views_task, create_views_task - if delete_views_task: - return delete_views_task, delete_views_task - return None - - def _create_finalize_task(self, environment: Environment) -> BaseOperator: - return PythonOperator( - task_id="snapshot_promotion_finalize", - python_callable=promotion_finalize_task, - op_kwargs={"environment": environment}, - ) - - def _create_backfill_tasks( - self, - backfill_intervals: t.List[common.BackfillIntervalsPerSnapshot], - snapshots: t.Dict[SnapshotId, Snapshot], - deployability_index: DeployabilityIndex, - plan_id: str, - task_id_suffix: str, - execution_time: t.Optional[TimeLike], - ) -> t.Tuple[BaseOperator, BaseOperator]: - snapshot_to_tasks = {} - for intervals_per_snapshot in backfill_intervals: - sid = intervals_per_snapshot.snapshot_id - - if not intervals_per_snapshot.intervals: - logger.info("Skipping backfill for snapshot %s", sid) - continue - - snapshot = snapshots[sid] - sanitized_model_name = sanitize_name(snapshot.node.name) - - snapshot_task_pairs: t.List[t.Tuple[BaseOperator, BaseOperator]] = [] - - snapshot_start_task = EmptyOperator( - task_id=f"snapshot_backfill__{sanitized_model_name}__{snapshot.identifier}__start" - ) - snapshot_end_task = EmptyOperator( - task_id=f"snapshot_backfill__{sanitized_model_name}__{snapshot.identifier}__end" - ) - - task_id_prefix = f"snapshot_backfill__{sanitized_model_name}__{snapshot.identifier}" - for batch_idx, (start, end) in enumerate(intervals_per_snapshot.intervals): - evaluation_task = self._create_snapshot_evaluation_operator( - snapshots=snapshots, - snapshot=snapshot, - task_id=f"{task_id_prefix}__{start.strftime(TASK_ID_DATE_FORMAT)}__{end.strftime(TASK_ID_DATE_FORMAT)}", - start=start, - end=end, - deployability_index=deployability_index, - plan_id=plan_id, - execution_time=execution_time, - batch_index=batch_idx, - ) - external_sensor_task = self._create_external_sensor(snapshot, start=start, end=end) - if external_sensor_task: - ( - snapshot_start_task - >> external_sensor_task - >> evaluation_task - >> snapshot_end_task - ) - snapshot_task_pairs.append((external_sensor_task, evaluation_task)) - else: - snapshot_start_task >> evaluation_task >> snapshot_end_task - snapshot_task_pairs.append((evaluation_task, evaluation_task)) - - batch_concurrency = snapshot.node.batch_concurrency - if snapshot.depends_on_past: - batch_concurrency = 1 - - if not intervals_per_snapshot.intervals: - snapshot_start_task >> snapshot_end_task - elif batch_concurrency: - for i in range(batch_concurrency, len(snapshot_task_pairs)): - snapshot_task_pairs[i - batch_concurrency][1] >> snapshot_task_pairs[i][0] - - snapshot_to_tasks[snapshot.snapshot_id] = ( - snapshot_start_task, - snapshot_end_task, - ) - - backfill_start_task = EmptyOperator(task_id=f"snapshot_backfill_{task_id_suffix}_start") - backfill_end_task = EmptyOperator(task_id=f"snapshot_backfill_{task_id_suffix}_end") - - if not snapshot_to_tasks: - backfill_start_task >> backfill_end_task - return (backfill_start_task, backfill_end_task) - - entry_tasks = [] - parent_ids_to_backfill = set() - for sid, (start_task, _) in snapshot_to_tasks.items(): - has_parents_to_backfill = False - for p_sid in snapshots[sid].parents: - if p_sid in snapshot_to_tasks: - snapshot_to_tasks[p_sid][1] >> start_task - parent_ids_to_backfill.add(p_sid) - has_parents_to_backfill = True - - if not has_parents_to_backfill: - entry_tasks.append(start_task) - - backfill_start_task >> entry_tasks - - exit_tasks = [ - end_task - for sid, (_, end_task) in snapshot_to_tasks.items() - if sid not in parent_ids_to_backfill - ] - for task in exit_tasks: - task >> backfill_end_task - - return (backfill_start_task, backfill_end_task) - - def _create_snapshot_promotion_operator( - self, - snapshots: t.List[Snapshot], - environment_naming_info: EnvironmentNamingInfo, - ddl_concurrent_tasks: int, - deployability_index: DeployabilityIndex, - task_id: str, - ) -> BaseOperator: - return self._ddl_engine_operator( - **self._ddl_engine_operator_args, - target=targets.SnapshotPromotionTarget( - snapshots=snapshots, - environment_naming_info=environment_naming_info, - ddl_concurrent_tasks=ddl_concurrent_tasks, - deployability_index=deployability_index, - ), - task_id=task_id, - ) - - def _create_snapshot_demotion_operator( - self, - snapshots: t.List[SnapshotTableInfo], - environment_naming_info: EnvironmentNamingInfo, - ddl_concurrent_tasks: int, - task_id: str, - ) -> BaseOperator: - return self._ddl_engine_operator( - **self._ddl_engine_operator_args, - target=targets.SnapshotDemotionTarget( - snapshots=snapshots, - environment_naming_info=environment_naming_info, - ddl_concurrent_tasks=ddl_concurrent_tasks, - ), - task_id=task_id, - ) - - def _create_snapshot_create_tables_operator( - self, - new_snapshots: t.List[Snapshot], - ddl_concurrent_tasks: int, - deployability_index: DeployabilityIndex, - allow_destructive_snapshots: t.Set[str], - task_id: str, - ) -> BaseOperator: - return self._ddl_engine_operator( - **self._ddl_engine_operator_args, - target=targets.SnapshotCreateTablesTarget( - new_snapshots=new_snapshots, - ddl_concurrent_tasks=ddl_concurrent_tasks, - deployability_index=deployability_index, - allow_destructive_snapshots=allow_destructive_snapshots, - ), - task_id=task_id, - ) - - def _create_snapshot_migrate_tables_operator( - self, - snapshots: t.List[Snapshot], - ddl_concurrent_tasks: int, - allow_destructive_snapshots: t.Set[str], - task_id: str, - ) -> BaseOperator: - return self._ddl_engine_operator( - **self._ddl_engine_operator_args, - target=targets.SnapshotMigrateTablesTarget( - snapshots=snapshots, - ddl_concurrent_tasks=ddl_concurrent_tasks, - allow_destructive_snapshots=allow_destructive_snapshots, - ), - task_id=task_id, - ) - - def _create_snapshot_evaluation_operator( - self, - snapshots: t.Dict[SnapshotId, Snapshot], - snapshot: Snapshot, - task_id: str, - start: t.Optional[TimeLike] = None, - end: t.Optional[TimeLike] = None, - execution_time: t.Optional[TimeLike] = None, - deployability_index: t.Optional[DeployabilityIndex] = None, - plan_id: t.Optional[str] = None, - batch_index: int = 0, - ) -> BaseOperator: - parent_snapshots = {snapshots[sid].name: snapshots[sid] for sid in snapshot.parents} - - return self._engine_operator( - **self._engine_operator_args, - target=targets.SnapshotEvaluationTarget( - snapshot=snapshot, - parent_snapshots=parent_snapshots, - start=start, - end=end, - deployability_index=deployability_index or DeployabilityIndex.all_deployable(), - plan_id=plan_id, - execution_time=execution_time, - batch_index=batch_index, - ), - task_id=task_id, - ) - - def _create_hwm_sensors( - self, snapshot: Snapshot, snapshots: t.Dict[SnapshotId, Snapshot] - ) -> t.List[BaseSensorOperator]: - output: t.List[BaseSensorOperator] = [] - for upstream_snapshot_id in snapshot.parents: - upstream_snapshot = snapshots[upstream_snapshot_id] - if not upstream_snapshot.is_symbolic and not upstream_snapshot.is_seed: - output.append( - HighWaterMarkSensor( - target_snapshot_info=upstream_snapshot.table_info, - this_snapshot=snapshot, - task_id=f"{sanitize_name(upstream_snapshot.node.name)}_{upstream_snapshot.version}_high_water_mark_sensor", - mode=self._sensor_mode, - **self._high_water_mark_sensor_args, - ) - ) - - external_sensor = self._create_external_sensor(snapshot) - if external_sensor: - output.append(external_sensor) - - return output - - def _create_external_sensor( - self, - snapshot: Snapshot, - start: t.Optional[TimeLike] = None, - end: t.Optional[TimeLike] = None, - ) -> t.Optional[BaseSensorOperator]: - if self._external_table_sensor_factory and snapshot.model.signals: - return ExternalSensor( - snapshot=snapshot, - external_table_sensor_factory=self._external_table_sensor_factory, - task_id="external_high_water_mark_sensor", - mode=self._sensor_mode, - start=start, - end=end, - **self._external_sensor_args, - ) - return None - - -def creation_update_state_task(new_snapshots: t.Collection[Snapshot], request_id: str) -> None: - with util.scoped_state_sync() as state_sync: - state_sync.push_snapshots(new_snapshots) - - from sqlmesh.core.analytics import collector - - collector.on_snapshots_created(new_snapshots=new_snapshots, plan_id=request_id) - - -def promotion_update_state_task( - environment: Environment, - no_gaps_snapshot_names: t.Optional[t.Set[str]], -) -> None: - with util.scoped_state_sync() as state_sync: - state_sync.promote(environment, no_gaps_snapshot_names=no_gaps_snapshot_names) - - -def promotion_unpause_snapshots_task( - environment: Environment, - unpaused_dt: t.Optional[TimeLike], -) -> None: - if environment.snapshots and unpaused_dt: - with util.scoped_state_sync() as state_sync: - state_sync.unpause_snapshots(environment.snapshots, unpaused_dt) - - -def promotion_finalize_task(environment: Environment) -> None: - with util.scoped_state_sync() as state_sync: - state_sync.finalize(environment) - - -def on_plan_apply_end(plan_id: str) -> None: - from sqlmesh.core.analytics import collector - - collector.on_plan_apply_end(plan_id=plan_id) diff --git a/sqlmesh/schedulers/airflow/hooks/__init__.py b/sqlmesh/schedulers/airflow/hooks/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sqlmesh/schedulers/airflow/hooks/bigquery.py b/sqlmesh/schedulers/airflow/hooks/bigquery.py deleted file mode 100644 index a12a7d8b2e..0000000000 --- a/sqlmesh/schedulers/airflow/hooks/bigquery.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import annotations - -import typing as t - -from airflow.providers.common.sql.hooks.sql import DbApiHook -from airflow.providers.google.common.hooks.base_google import GoogleBaseHook - -if t.TYPE_CHECKING: - from google.cloud.bigquery.dbapi import Connection - - -class SQLMeshBigQueryHook(GoogleBaseHook, DbApiHook): - """ - Interact with BigQuery. This hook uses the Google Cloud connection. We didn't use the Airflow BigQueryHook - because it implements an Airflow specific version of the BigQuery DB API that is different then the DB API - provided from Google's python package. - - :param gcp_conn_id: The Airflow connection used for GCP credentials. - :param delegate_to: This performs a task on one host with reference to other hosts. - :param impersonation_chain: This is the optional service account to impersonate using short term - credentials. - """ - - conn_name_attr = "sqlmesh_gcp_conn_id" - default_conn_name = "sqlmesh_google_cloud_bigquery_default" - conn_type = "sqlmeshgcpbigquery" - hook_name = "SQLMesh Google Bigquery" - - def __init__( - self, - gcp_conn_id: str = default_conn_name, - delegate_to: t.Optional[str] = None, - impersonation_chain: t.Optional[t.Union[str, t.Sequence[str]]] = None, - location: t.Optional[str] = None, - ) -> None: - GoogleBaseHook.__init__( - self, - gcp_conn_id=gcp_conn_id, - delegate_to=delegate_to, - impersonation_chain=impersonation_chain, - ) - self.location = location - - def get_conn(self) -> Connection: - """Returns a BigQuery DBAPI connection object.""" - from google.api_core.client_info import ClientInfo - from google.cloud.bigquery import Client - from google.cloud.bigquery.dbapi import Connection - - # This method is private in older versions of the BigQuery library and public later. So we check for both - try: - creds, project_id = self._get_credentials_and_project_id() # type: ignore - except AttributeError: - creds, project_id = self.get_credentials_and_project_id() # type: ignore - client = Client( - project=project_id, - credentials=creds, - location=self.location, - client_info=ClientInfo(user_agent="sqlmesh"), - ) - return Connection(client=client) diff --git a/sqlmesh/schedulers/airflow/hooks/clickhouse.py b/sqlmesh/schedulers/airflow/hooks/clickhouse.py deleted file mode 100644 index 8057f12a67..0000000000 --- a/sqlmesh/schedulers/airflow/hooks/clickhouse.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import annotations - -import typing as t - -from airflow.providers.common.sql.hooks.sql import DbApiHook - -if t.TYPE_CHECKING: - from clickhouse_connect.dbapi.connection import Connection - - -class SQLMeshClickHouseHook(DbApiHook): - """ - Uses the ClickHouse Python DB API connector. - """ - - conn_name_attr = "sqlmesh_clickhouse_conn_id" - default_conn_name = "sqlmesh_clickhouse_default" - conn_type = "sqlmesh_clickhouse" - hook_name = "SQLMesh ClickHouse" - - def get_conn(self) -> Connection: - """Returns a ClickHouse connection object""" - from clickhouse_connect.dbapi import connect - - db = self.get_connection(getattr(self, t.cast(str, self.conn_name_attr))) - - return connect( - host=db.host, - port=db.port, - username=db.login, - password=db.password, - database=db.schema, - **db.extra_dejson, - ) diff --git a/sqlmesh/schedulers/airflow/hooks/redshift.py b/sqlmesh/schedulers/airflow/hooks/redshift.py deleted file mode 100644 index ba52071ba8..0000000000 --- a/sqlmesh/schedulers/airflow/hooks/redshift.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -import typing as t - -import redshift_connector -from airflow.providers.common.sql.hooks.sql import DbApiHook - - -class SQLMeshRedshiftHook(DbApiHook): - """ - Uses the Redshift Python DB API connector. - """ - - conn_name_attr = "sqlmesh_redshift_conn_id" - default_conn_name = "sqlmesh_redshift_default" - conn_type = "sqlmesh_redshift" - hook_name = "SQLMesh Redshift" - connector = redshift_connector - - def get_conn(self) -> redshift_connector.Connection: - """Returns a Redshift connection object""" - db = self.get_connection(getattr(self, t.cast(str, self.conn_name_attr))) - - return self.connector.connect( - host=db.host, - port=db.port, - user=db.login, - password=db.password, - database=db.schema, - **db.extra_dejson, - ) diff --git a/sqlmesh/schedulers/airflow/integration.py b/sqlmesh/schedulers/airflow/integration.py deleted file mode 100644 index 06f5db2bbd..0000000000 --- a/sqlmesh/schedulers/airflow/integration.py +++ /dev/null @@ -1,250 +0,0 @@ -from __future__ import annotations - -import logging -import typing as t -from datetime import datetime, timedelta - -from airflow import DAG -from airflow.models import BaseOperator, TaskInstance, Variable -from airflow.operators.python import PythonOperator -from airflow.sensors.base import BaseSensorOperator -from airflow.utils.session import provide_session -from sqlalchemy.orm import Session - -from sqlmesh.core import constants as c -from sqlmesh.core.state_sync import StateReader -from sqlmesh.engines import commands -from sqlmesh.schedulers.airflow import common, util -from sqlmesh.schedulers.airflow.dag_generator import SnapshotDagGenerator -from sqlmesh.schedulers.airflow.operators import targets -from sqlmesh.schedulers.airflow.plan import PlanDagState - -if t.TYPE_CHECKING: - pass - -logger = logging.getLogger(__name__) - - -class SQLMeshAirflow: - """The entry point for the SQLMesh integration with Airflow. - - The instance of this class should be created in a module that is part of the - Airflow DAGs folder. Its primary purpose is to create DAG objects for the operational - needs of the platform, as well as for model evaluation and backfills. - - Please note that the user must pass created DAGs into the - Airflow scheduler. See the example below: - - Example: - Create a new python module in the Airflow DAGs folder called "sqlmesh_integration.py" - with the following content: - - from sqlmesh.schedulers.airflow.integration import SQLMeshAirflow - - for dag in SQLMeshAirflow("spark").dags: - globals()[dag.dag_id] = dag - - Args: - engine_operator: The type of the Airflow operator that will be used for model evaluation. - If a string value is passed, an automatic operator discovery is attempted based - on the engine name specified in the string. - default_catalog: The default catalog to use when models are defined that do not contain a catalog in their name. This should match the default catalog applied by the connection. - engine_operator_args: The dictionary of arguments that will be passed into the evaluate engine - operator during its construction. - This can be used to customize parameters such as connection ID. - ddl_engine_operator: The type of the Airflow operator that will be used for environment management. - These operations are SQL only. - If a string value is passed, an automatic operator discovery is attempted based - on the engine name specified in the string. - ddl_engine_operator_args: Args to be passed into just the environment management operator. - This can be used to customize parameters such as connection ID. - If not specified, and the operator is the same as `engine_operator`, falls back to using `engine_operator_args`. - janitor_interval: Defines how often the janitor DAG runs. - The janitor DAG removes platform-managed DAG instances that are pending - deletion from Airflow. Default: 1 hour. - plan_application_dag_ttl: Determines the time-to-live period for finished plan application DAGs. - Once this period is exceeded, finished plan application DAGs are deleted by the janitor. Default: 2 days. - external_table_sensor_factory: A factory function that creates a sensor operator for a given signal payload. - sensor_mode: The mode to use for SQLMesh sensors. Supported values are "poke" and "reschedule". - See https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/sensors.html for more details. Default: "reschedule". - high_water_mark_sensor_args: The dictionary of arguments that will be passed into the high water mark sensor operator during its construction. - external_sensor_args: The dictionary of arguments that will be passed into the external sensor operator during its construction. - generate_cadence_dags: Whether to generate cadence DAGs for model versions that are currently deployed to production. - """ - - def __init__( - self, - engine_operator: t.Union[str, t.Type[BaseOperator]], - default_catalog: str, - engine_operator_args: t.Optional[t.Dict[str, t.Any]] = None, - ddl_engine_operator: t.Optional[t.Union[str, t.Type[BaseOperator]]] = None, - ddl_engine_operator_args: t.Optional[t.Dict[str, t.Any]] = None, - janitor_interval: timedelta = timedelta(hours=1), - plan_application_dag_ttl: timedelta = timedelta(days=2), - external_table_sensor_factory: t.Optional[ - t.Callable[[t.Dict[str, t.Any]], BaseSensorOperator] - ] = None, - sensor_mode: str = "reschedule", - high_water_mark_sensor_args: t.Optional[t.Dict[str, t.Any]] = None, - external_sensor_args: t.Optional[t.Dict[str, t.Any]] = None, - generate_cadence_dags: bool = True, - ): - if isinstance(engine_operator, str): - if not ddl_engine_operator: - ddl_engine_operator = util.discover_engine_operator(engine_operator, sql_only=True) - engine_operator = util.discover_engine_operator(engine_operator, sql_only=False) - - if isinstance(ddl_engine_operator, str): - ddl_engine_operator = util.discover_engine_operator(ddl_engine_operator, sql_only=True) - - engine_operator_args = engine_operator_args or {} - ddl_engine_operator_args = ddl_engine_operator_args or {} - - self._engine_operator = engine_operator - self._engine_operator_args = engine_operator_args - self._ddl_engine_operator = ddl_engine_operator or engine_operator - if self._engine_operator == self._ddl_engine_operator: - self._ddl_engine_operator_args = {**engine_operator_args, **ddl_engine_operator_args} - else: - self._ddl_engine_operator_args = ddl_engine_operator_args or {} - self._janitor_interval = janitor_interval - self._plan_application_dag_ttl = plan_application_dag_ttl - self._external_table_sensor_factory = external_table_sensor_factory - self._generate_cadence_dags = generate_cadence_dags - self._default_catalog = default_catalog - self._sensor_mode = sensor_mode - self._high_water_mark_sensor_args = high_water_mark_sensor_args or {} - self._external_sensor_args = external_sensor_args or {} - - @classmethod - def set_default_catalog(cls, default_catalog: str) -> None: - current_value = Variable.get(common.DEFAULT_CATALOG_VARIABLE_NAME, default_var=None) - if not current_value: - Variable.set(common.DEFAULT_CATALOG_VARIABLE_NAME, default_catalog) - if current_value != default_catalog: - Variable.update(common.DEFAULT_CATALOG_VARIABLE_NAME, default_catalog) - - @property - def dags(self) -> t.List[DAG]: - """Returns all DAG instances that must be registered with the Airflow scheduler - for the integration to work. - - Returns: - The list of DAG instances managed by the platform. - """ - self.set_default_catalog(self._default_catalog) - with util.scoped_state_sync() as state_sync: - dag_generator = self._create_dag_generator(state_sync) - - if self._generate_cadence_dags: - prod_env = state_sync.get_environment(c.PROD) - cadence_dags = ( - dag_generator.generate_cadence_dags(prod_env.snapshots) if prod_env else [] - ) - _delete_orphaned_snapshot_dags({d.dag_id for d in cadence_dags}) - else: - cadence_dags = [] - - plan_dag_specs = PlanDagState.from_state_sync(state_sync).get_dag_specs() - plan_application_dags = [ - dag_generator.generate_plan_application_dag(s) for s in plan_dag_specs - ] - - system_dags = [ - self._create_janitor_dag(), - ] - - return system_dags + cadence_dags + [d for d in plan_application_dags if d] - - def _create_janitor_dag(self) -> DAG: - dag = self._create_system_dag(common.JANITOR_DAG_ID, self._janitor_interval) - janitor_task_op = PythonOperator( - task_id=common.JANITOR_TASK_ID, - python_callable=_janitor_task, - op_kwargs={"plan_application_dag_ttl": self._plan_application_dag_ttl}, - dag=dag, - ) - - table_cleanup_task_op = self._ddl_engine_operator( - **self._ddl_engine_operator_args, - target=targets.SnapshotCleanupTarget(), - task_id="snapshot_table_cleanup_task", - dag=dag, - ) - - janitor_task_op >> table_cleanup_task_op - - return dag - - def _create_system_dag(self, dag_id: str, schedule_interval: t.Optional[timedelta]) -> DAG: - return DAG( - dag_id=dag_id, - default_args=dict( - execution_timeout=timedelta(minutes=10), - retries=0, - ), - schedule_interval=schedule_interval, - start_date=datetime(2023, 1, 1), - max_active_runs=1, - catchup=False, - is_paused_upon_creation=False, - tags=[common.SQLMESH_AIRFLOW_TAG], - ) - - def _create_dag_generator(self, state_reader: StateReader) -> SnapshotDagGenerator: - return SnapshotDagGenerator( - self._engine_operator, - self._engine_operator_args, - self._ddl_engine_operator, - self._ddl_engine_operator_args, - self._external_table_sensor_factory, - self._sensor_mode, - self._high_water_mark_sensor_args, - self._external_sensor_args, - state_reader, - ) - - -@provide_session -def _janitor_task( - plan_application_dag_ttl: timedelta, - ti: TaskInstance, - session: Session = util.PROVIDED_SESSION, -) -> None: - with util.scoped_state_sync() as state_sync: - expired_environments = state_sync.delete_expired_environments() - expired_snapshots = state_sync.delete_expired_snapshots() - ti.xcom_push( - key=common.SNAPSHOT_CLEANUP_COMMAND_XCOM_KEY, - value=commands.CleanupCommandPayload( - environments=expired_environments, - tasks=expired_snapshots, - ).json(), - session=session, - ) - - prod_env = state_sync.get_environment(c.PROD) - if prod_env: - active_snapshot_dag_ids = { - common.dag_id_for_snapshot_info(s) for s in prod_env.snapshots - } - _delete_orphaned_snapshot_dags(active_snapshot_dag_ids, session=session) - - plan_application_dag_ids = util.get_finished_plan_application_dag_ids( - ttl=plan_application_dag_ttl, session=session - ) - logger.info("Deleting expired Plan Application DAGs: %s", plan_application_dag_ids) - PlanDagState.from_state_sync(state_sync).delete_dag_specs(plan_application_dag_ids) - util.delete_dags(plan_application_dag_ids, session=session) - - state_sync.compact_intervals() - - -@provide_session -def _delete_orphaned_snapshot_dags( - active_snapshot_dag_ids: t.Set[str], session: Session = util.PROVIDED_SESSION -) -> None: - all_snapshot_dag_ids = set(util.get_snapshot_dag_ids(session=session)) - orphaned_snapshot_dag_ids = all_snapshot_dag_ids - active_snapshot_dag_ids - logger.info("Deleting orphaned Snapshot DAGs: %s", orphaned_snapshot_dag_ids) - util.delete_dags(orphaned_snapshot_dag_ids, session=session) diff --git a/sqlmesh/schedulers/airflow/mwaa_client.py b/sqlmesh/schedulers/airflow/mwaa_client.py deleted file mode 100644 index 4a4028fec0..0000000000 --- a/sqlmesh/schedulers/airflow/mwaa_client.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import annotations - -import base64 -import json -import logging -import typing as t -from urllib.parse import urljoin - -from requests import Session - -from sqlmesh.core.console import Console -from sqlmesh.schedulers.airflow.client import BaseAirflowClient, raise_for_status -from sqlmesh.utils.date import now_timestamp -from sqlmesh.utils.errors import NotFoundError - -logger = logging.getLogger(__name__) - - -TOKEN_TTL_MS = 30 * 1000 - - -class MWAAClient(BaseAirflowClient): - def __init__(self, environment: str, console: t.Optional[Console] = None): - airflow_url, auth_token = url_and_auth_token_for_environment(environment) - super().__init__(airflow_url, console) - - self._environment = environment - self._last_token_refresh_ts = now_timestamp() - self.__session: Session = _create_session(auth_token) - - def get_first_dag_run_id(self, dag_id: str) -> t.Optional[str]: - dag_runs = self._list_dag_runs(dag_id) - if dag_runs: - return dag_runs[-1]["run_id"] - return None - - def get_dag_run_state(self, dag_id: str, dag_run_id: str) -> str: - dag_runs = self._list_dag_runs(dag_id) or [] - for dag_run in dag_runs: - if dag_run["run_id"] == dag_run_id: - return dag_run["state"].lower() - raise NotFoundError(f"DAG run '{dag_run_id}' was not found for DAG '{dag_id}'") - - def get_variable(self, key: str) -> t.Optional[str]: - stdout, stderr = self._post(f"variables get {key}") - if "does not exist" in stderr: - return None - return stdout - - def _list_dag_runs(self, dag_id: str) -> t.Optional[t.List[t.Dict[str, t.Any]]]: - stdout, stderr = self._post(f"dags list-runs -o json -d {dag_id}") - if stdout: - return json.loads(stdout) - return None - - def _post(self, data: str) -> t.Tuple[str, str]: - response = self._session.post(urljoin(self._airflow_url, "aws_mwaa/cli"), data=data) - raise_for_status(response) - response_body = response.json() - - cli_stdout = base64.b64decode(response_body["stdout"]).decode("utf8").strip() - cli_stderr = base64.b64decode(response_body["stderr"]).decode("utf8").strip() - return cli_stdout, cli_stderr - - @property - def _session(self) -> Session: - current_ts = now_timestamp() - if current_ts - self._last_token_refresh_ts > TOKEN_TTL_MS: - _, auth_token = url_and_auth_token_for_environment(self._environment) - self.__session = _create_session(auth_token) - self._last_token_refresh_ts = current_ts - return self.__session - - -def _create_session(auth_token: str) -> Session: - session = Session() - session.headers.update({"Authorization": f"Bearer {auth_token}", "Content-Type": "text/plain"}) - return session - - -def url_and_auth_token_for_environment(environment_name: str) -> t.Tuple[str, str]: - import boto3 - - logger.info("Fetching the MWAA CLI token") - - client = boto3.client("mwaa") - cli_token = client.create_cli_token(Name=environment_name) - - url = f"https://{cli_token['WebServerHostname']}/" - auth_token = cli_token["CliToken"] - return url, auth_token diff --git a/sqlmesh/schedulers/airflow/operators/__init__.py b/sqlmesh/schedulers/airflow/operators/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sqlmesh/schedulers/airflow/operators/base.py b/sqlmesh/schedulers/airflow/operators/base.py deleted file mode 100644 index b4d22b47ec..0000000000 --- a/sqlmesh/schedulers/airflow/operators/base.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - -import typing as t - -from airflow.models import BaseOperator -from airflow.utils.context import Context -from airflow.providers.common.sql.hooks.sql import DbApiHook - - -from sqlmesh.schedulers.airflow.operators.targets import BaseTarget - - -class BaseDbApiOperator(BaseOperator): - """The base class for DB API operators. - - Args: - target: The target that will be executed by this operator instance. - conn_id: The Airflow connection id. - dialect: The target SQL dialect. - hook_type: The type of the DB API hook. - """ - - def __init__( - self, - *, - target: BaseTarget, - conn_id: str, - dialect: str, - hook_type: t.Type[DbApiHook], - **kwargs: t.Any, - ) -> None: - super().__init__(**kwargs) - self._hook_type = hook_type - self._target = target - self._conn_id = conn_id - self._dialect = dialect - self._hook_params = kwargs - - def get_db_hook(self) -> DbApiHook: - """Gets the Hook which contains the DB API connection object.""" - return self._hook_type(self._conn_id, **self._hook_params) - - def execute(self, context: Context) -> None: - """Executes the desired target against the configured connection.""" - self._target.execute(context, lambda: self.get_db_hook().get_conn(), self._dialect) diff --git a/sqlmesh/schedulers/airflow/operators/bigquery.py b/sqlmesh/schedulers/airflow/operators/bigquery.py deleted file mode 100644 index ab261717ee..0000000000 --- a/sqlmesh/schedulers/airflow/operators/bigquery.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -import typing as t - -from airflow.models import BaseOperator -from airflow.utils.context import Context - -from sqlmesh.schedulers.airflow.hooks.bigquery import SQLMeshBigQueryHook -from sqlmesh.schedulers.airflow.operators.targets import BaseTarget - - -class SQLMeshBigQueryOperator(BaseOperator): - """The operator that evaluates a SQLMesh model snapshot on Bigquery - - Args: - target: The target that will be executed by this operator instance. - bigquery_conn_id: The Airflow connection id for the bigquery target. - """ - - def __init__( - self, - *, - target: BaseTarget, - bigquery_conn_id: str = SQLMeshBigQueryHook.default_conn_name, - delegate_to: str | None = None, - impersonation_chain: str | t.Sequence[str] | None = None, - location: t.Optional[str] = None, - **kwargs: t.Any, - ) -> None: - super().__init__(**kwargs) - self._target = target - self._bigquery_conn_id = bigquery_conn_id - self._delegate_to = delegate_to - self._impersonation_chain = impersonation_chain - self._location = location - - def get_db_hook(self) -> SQLMeshBigQueryHook: - """Gets the BigQuery Hook which contains the DB API connection object""" - return SQLMeshBigQueryHook( - self._bigquery_conn_id, - delegate_to=self._delegate_to, - impersonation_chain=self._impersonation_chain, - location=self._location, - ) - - def execute(self, context: Context) -> None: - """Executes the desired target against the configured BigQuery connection""" - self._target.execute( - context, - lambda: self.get_db_hook().get_conn(), - "bigquery", - job_retries=self.get_db_hook().num_retries, - ) diff --git a/sqlmesh/schedulers/airflow/operators/clickhouse.py b/sqlmesh/schedulers/airflow/operators/clickhouse.py deleted file mode 100644 index c9b3301ade..0000000000 --- a/sqlmesh/schedulers/airflow/operators/clickhouse.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -import typing as t - - -from sqlmesh.schedulers.airflow.hooks.clickhouse import SQLMeshClickHouseHook -from sqlmesh.schedulers.airflow.operators.base import BaseDbApiOperator -from sqlmesh.schedulers.airflow.operators.targets import BaseTarget - - -class SQLMeshClickHouseOperator(BaseDbApiOperator): - """The operator that evaluates a SQLMesh model snapshot on a ClickHouse target - - Args: - target: The target that will be executed by this operator instance. - postgres_conn_id: The Airflow connection id for the postgres target. - """ - - def __init__( - self, - *, - target: BaseTarget, - clickhouse_conn_id: str = SQLMeshClickHouseHook.default_conn_name, - **kwargs: t.Any, - ) -> None: - super().__init__( - target=target, - conn_id=clickhouse_conn_id, - dialect="clickhouse", - hook_type=SQLMeshClickHouseHook, - **kwargs, - ) diff --git a/sqlmesh/schedulers/airflow/operators/databricks.py b/sqlmesh/schedulers/airflow/operators/databricks.py deleted file mode 100644 index 1762562aba..0000000000 --- a/sqlmesh/schedulers/airflow/operators/databricks.py +++ /dev/null @@ -1,139 +0,0 @@ -from __future__ import annotations - -import os -import tempfile -import typing as t - -from airflow.providers.databricks.hooks.databricks_base import BaseDatabricksHook -from airflow.providers.databricks.hooks.databricks_sql import DatabricksSqlHook -from airflow.providers.databricks.operators.databricks import ( - DatabricksSubmitRunOperator, -) -from airflow.utils.context import Context - -import sqlmesh -from sqlmesh import utils -from sqlmesh.engines import commands -from sqlmesh.schedulers.airflow.operators.base import BaseDbApiOperator -from sqlmesh.schedulers.airflow.operators.targets import BaseTarget - - -class SQLMeshDatabricksSubmitOperator(DatabricksSubmitRunOperator): - """Operator for submitting Databricks jobs to a Databricks cluster using the submit run API. - - Args: - target: The target that will be executed by this operator instance. - dbfs_location: The dbfs location where the app.py file and payload will be copied to. - existing_cluster_id: The id of the cluster to run the job on. Either this or new_cluster must be specified. - new_cluster: The specification for a new cluster to run the job on. Either this or existing_cluster_id must be specified. - """ - - def __init__( - self, - target: BaseTarget, - dbfs_location: t.Optional[str] = None, - **kwargs: t.Any, - ) -> None: - if not dbfs_location: - raise ValueError( - "dbfs_location is required for Databricks connections. See documentation for more details." - ) - if not dbfs_location.startswith("dbfs:/"): - raise ValueError( - "dbfs_location must start with 'dbfs:/'. See documentation for more details." - ) - super().__init__(**kwargs) - self._target = target - self._dbfs_location = os.path.join(dbfs_location, utils.random_id()) - - def execute(self, context: Context) -> None: - """Executes the target against the configured databricks cluster using the submit run API. - - SQLMesh copies the app.py file to the dbfs location specified in the operator. It also copies a file containing - the target's payload to the dbfs location. The app.py file is then executed with the target's payload as an - argument. - - TODO: Add support for `idempotency token`. This would allow this operator to reattach to an existing run if it - exists instead of creating a new one. We would need to make sure this is done correctly by have the dbfs - path use that token for the path instead of a random ID. Consider using a plan ID but also consider how - cadence runs and restatements will also work. - """ - from databricks_cli.dbfs.api import DbfsApi - from databricks_cli.sdk.api_client import ApiClient - - if "new_cluster" not in self.json and "existing_cluster_id" not in self.json: - http_path = self._hook.databricks_conn.extra_dejson.get("http_path") - if not http_path: - raise ValueError( - "Must provide a cluster to run on or new cluster specification. See documentation for more details." - ) - cluster_id = http_path.split("/")[-1] - if "-" not in cluster_id: - raise ValueError( - "Must provide a non-DBSQL cluster to execute against. See documentation for more details." - ) - self.json["existing_cluster_id"] = cluster_id - - api_client = ApiClient( - host=f"https://{self._hook.host}", - token=self._hook._get_token(raise_error=False), - user=self._hook.databricks_conn.login, - password=self._hook.databricks_conn.password, - ) - dbfs_api = DbfsApi(api_client) - - local_app_path = os.path.join( - os.path.dirname(os.path.abspath(sqlmesh.__file__)), "engines/spark/app.py" - ) - remote_app_path = os.path.join(self._dbfs_location, "app.py") - dbfs_api.cp(recursive=False, overwrite=True, src=local_app_path, dst=remote_app_path) - - command_payload = self._target.serialized_command_payload(context) - with tempfile.TemporaryDirectory() as tmp: - local_payload_path = os.path.join(tmp, commands.COMMAND_PAYLOAD_FILE_NAME) - with open(local_payload_path, "w", encoding="utf-8") as payload_fd: - payload_fd.write(command_payload) - remote_payload_path = os.path.join( - self._dbfs_location, commands.COMMAND_PAYLOAD_FILE_NAME - ) - dbfs_api.cp( - recursive=False, overwrite=True, src=local_payload_path, dst=remote_payload_path - ) - task_arguments = { - "dialect": "databricks", - "default_catalog": self._target.default_catalog, - "command_type": self._target.command_type.value if self._target.command_type else None, - "ddl_concurrent_tasks": self._target.ddl_concurrent_tasks, - "payload_path": remote_payload_path, - } - python_task = { - "python_file": remote_app_path, - "parameters": [f"--{k}={v}" for k, v in task_arguments.items() if v is not None], - } - self.json["spark_python_task"] = python_task - super().execute(context) - self._target.post_hook(context) - - -class SQLMeshDatabricksSQLOperator(BaseDbApiOperator): - """Operator for running just SQL operations against Databricks. - - Args: - target: The target that will be executed by this operator instance. - databricks_conn_id: The Airflow connection id for the databricks target. - """ - - def __init__( - self, - *, - target: BaseTarget, - databricks_conn_id: str = BaseDatabricksHook.default_conn_name, - **kwargs: t.Any, - ) -> None: - super().__init__( - target=target, - conn_id=databricks_conn_id, - dialect="databricks", - hook_type=DatabricksSqlHook, - **kwargs, - ) diff --git a/sqlmesh/schedulers/airflow/operators/mssql.py b/sqlmesh/schedulers/airflow/operators/mssql.py deleted file mode 100644 index d9d84eceba..0000000000 --- a/sqlmesh/schedulers/airflow/operators/mssql.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -import typing as t - -from airflow.providers.microsoft.mssql.hooks.mssql import MsSqlHook - -from sqlmesh.schedulers.airflow.operators.base import BaseDbApiOperator -from sqlmesh.schedulers.airflow.operators.targets import BaseTarget - - -class SQLMeshMsSqlOperator(BaseDbApiOperator): - """The operator that evaluates a SQLMesh model snapshot on a mssql target - - Args: - target: The target that will be executed by this operator instance. - mssql_conn_id: The Airflow connection id for the mssql target. - """ - - def __init__( - self, - *, - target: BaseTarget, - mssql_conn_id: str = MsSqlHook.default_conn_name, - **kwargs: t.Any, - ) -> None: - super().__init__( - target=target, conn_id=mssql_conn_id, dialect="mssql", hook_type=MsSqlHook, **kwargs - ) diff --git a/sqlmesh/schedulers/airflow/operators/mysql.py b/sqlmesh/schedulers/airflow/operators/mysql.py deleted file mode 100644 index 7dd5ae855e..0000000000 --- a/sqlmesh/schedulers/airflow/operators/mysql.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -import typing as t - -from airflow.providers.mysql.hooks.mysql import MySqlHook - -from sqlmesh.schedulers.airflow.operators.base import BaseDbApiOperator -from sqlmesh.schedulers.airflow.operators.targets import BaseTarget - - -class SQLMeshMySqlOperator(BaseDbApiOperator): - """The operator that evaluates a SQLMesh model snapshot on a mysql target - - Args: - target: The target that will be executed by this operator instance. - mysql_conn_id: The Airflow connection id for the mysql target. - """ - - def __init__( - self, - *, - target: BaseTarget, - mysql_conn_id: str = MySqlHook.default_conn_name, - **kwargs: t.Any, - ) -> None: - super().__init__( - target=target, conn_id=mysql_conn_id, dialect="mysql", hook_type=MySqlHook, **kwargs - ) diff --git a/sqlmesh/schedulers/airflow/operators/notification.py b/sqlmesh/schedulers/airflow/operators/notification.py deleted file mode 100644 index 5e6eedab88..0000000000 --- a/sqlmesh/schedulers/airflow/operators/notification.py +++ /dev/null @@ -1,30 +0,0 @@ -import abc -import typing as t - -from airflow.models import BaseOperator - -from sqlmesh.core.notification_target import BaseNotificationTarget -from sqlmesh.core.plan import PlanStatus -from sqlmesh.schedulers.airflow import common - -NT = t.TypeVar("NT", bound=BaseNotificationTarget) - - -class BaseNotificationOperatorProvider(abc.ABC, t.Generic[NT]): - @abc.abstractmethod - def operator( - self, - target: NT, - plan_status: PlanStatus, - plan_dag_spec: common.PlanDagSpec, - **dag_kwargs: t.Any, - ) -> t.Optional[BaseOperator]: - pass - - def get_trigger_rule(self, plan_status: PlanStatus) -> str: - if plan_status.is_failed: - return "one_failed" - return "all_success" - - def get_task_id(self, target: NT, plan_status: PlanStatus) -> str: - return f"plan_{plan_status.value}_{target.type_}_notification" diff --git a/sqlmesh/schedulers/airflow/operators/postgres.py b/sqlmesh/schedulers/airflow/operators/postgres.py deleted file mode 100644 index 49dbb1686e..0000000000 --- a/sqlmesh/schedulers/airflow/operators/postgres.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -import typing as t - -from airflow.providers.postgres.hooks.postgres import PostgresHook - -from sqlmesh.schedulers.airflow.operators.base import BaseDbApiOperator -from sqlmesh.schedulers.airflow.operators.targets import BaseTarget - - -class SQLMeshPostgresOperator(BaseDbApiOperator): - """The operator that evaluates a SQLMesh model snapshot on a Postgres target - - Args: - target: The target that will be executed by this operator instance. - postgres_conn_id: The Airflow connection id for the postgres target. - """ - - def __init__( - self, - *, - target: BaseTarget, - postgres_conn_id: str = PostgresHook.default_conn_name, - **kwargs: t.Any, - ) -> None: - super().__init__( - target=target, - conn_id=postgres_conn_id, - dialect="postgres", - hook_type=PostgresHook, - **kwargs, - ) diff --git a/sqlmesh/schedulers/airflow/operators/redshift.py b/sqlmesh/schedulers/airflow/operators/redshift.py deleted file mode 100644 index 0d1f5c9240..0000000000 --- a/sqlmesh/schedulers/airflow/operators/redshift.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -import typing as t - - -from sqlmesh.schedulers.airflow.hooks.redshift import SQLMeshRedshiftHook -from sqlmesh.schedulers.airflow.operators.base import BaseDbApiOperator -from sqlmesh.schedulers.airflow.operators.targets import BaseTarget - - -class SQLMeshRedshiftOperator(BaseDbApiOperator): - """The operator that evaluates a SQLMesh model snapshot on Redshift cluster - - Args: - target: The target that will be executed by this operator instance. - redshift_conn_id: The Airflow connection id for the Redshift target. - """ - - def __init__( - self, - target: BaseTarget, - redshift_conn_id: str = SQLMeshRedshiftHook.default_conn_name, - **kwargs: t.Any, - ) -> None: - super().__init__( - target=target, - conn_id=redshift_conn_id, - dialect="redshift", - hook_type=SQLMeshRedshiftHook, - **kwargs, - ) diff --git a/sqlmesh/schedulers/airflow/operators/sensor.py b/sqlmesh/schedulers/airflow/operators/sensor.py deleted file mode 100644 index 3a7e72d8da..0000000000 --- a/sqlmesh/schedulers/airflow/operators/sensor.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import annotations - -import logging -import typing as t -from datetime import datetime - -from airflow.models import DagRun -from airflow.sensors.base import BaseSensorOperator -from airflow.utils.context import Context - -from sqlmesh.core.snapshot import Snapshot, SnapshotTableInfo -from sqlmesh.schedulers.airflow import util -from sqlmesh.utils.date import TimeLike, now, to_datetime - -if t.TYPE_CHECKING: - from airflow.sensors.base import PokeReturnValue - -logger = logging.getLogger(__name__) - - -class HighWaterMarkSensor(BaseSensorOperator): - def __init__( - self, - target_snapshot_info: SnapshotTableInfo, - this_snapshot: Snapshot, - mode: str = "reschedule", - **kwargs: t.Any, - ) -> None: - super().__init__( - mode=mode, - **kwargs, - ) - self.target_snapshot_info = target_snapshot_info - self.this_snapshot = this_snapshot - - def poke(self, context: Context) -> bool: - dag_run = context["dag_run"] - - with util.scoped_state_sync() as state_sync: - target_snapshot = state_sync.get_snapshots([self.target_snapshot_info])[ - self.target_snapshot_info.snapshot_id - ] - if target_snapshot.intervals: - current_high_water_mark = to_datetime(target_snapshot.intervals[-1][1]) - else: - current_high_water_mark = None - - target_high_water_mark = self._compute_target_high_water_mark( - dag_run, # type: ignore - target_snapshot, - ) - - logger.info( - "The current high water mark for snapshot %s is '%s' (target is '%s')", - self.target_snapshot_info.snapshot_id, - current_high_water_mark, - target_high_water_mark, - ) - if current_high_water_mark is not None: - return current_high_water_mark >= target_high_water_mark - return False - - def _compute_target_high_water_mark( - self, dag_run: DagRun, target_snapshot: Snapshot - ) -> datetime: - target_date = to_datetime(dag_run.data_interval_end) - target_prev = to_datetime(target_snapshot.node.interval_unit.cron_floor(target_date)) - this_prev = to_datetime(self.this_snapshot.node.interval_unit.cron_floor(target_date)) - return min(target_prev, this_prev) - - -class ExternalSensor(BaseSensorOperator): - def __init__( - self, - snapshot: Snapshot, - external_table_sensor_factory: t.Callable[[t.Dict[str, t.Any]], BaseSensorOperator], - mode: str = "reschedule", - start: t.Optional[TimeLike] = None, - end: t.Optional[TimeLike] = None, - **kwargs: t.Any, - ): - super().__init__( - mode=mode, - **kwargs, - ) - self.snapshot = snapshot - self.external_table_sensor_factory = external_table_sensor_factory - self.start = start - self.end = end - - def poke(self, context: Context) -> t.Union[bool, PokeReturnValue]: - interval_unit = self.snapshot.node.interval_unit - dag_run = context["dag_run"] - signals = self.snapshot.model.render_signals( - start=interval_unit.cron_floor(self.start or dag_run.data_interval_start), # type: ignore - end=interval_unit.cron_floor(self.end or dag_run.data_interval_end), # type: ignore - execution_time=now(minute_floor=False), - ) - delegates = [self.external_table_sensor_factory(signal) for signal in signals] - return all(d.poke(context) for d in delegates) diff --git a/sqlmesh/schedulers/airflow/operators/snowflake.py b/sqlmesh/schedulers/airflow/operators/snowflake.py deleted file mode 100644 index b015d3f1d2..0000000000 --- a/sqlmesh/schedulers/airflow/operators/snowflake.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -import typing as t - -from airflow.providers.snowflake.hooks.snowflake import SnowflakeHook - -from sqlmesh.schedulers.airflow.operators.base import BaseDbApiOperator -from sqlmesh.schedulers.airflow.operators.targets import BaseTarget - - -class SQLMeshSnowflakeOperator(BaseDbApiOperator): - """The operator that evaluates a SQLMesh model snapshot on a Snowflake target - - Args: - target: The target that will be executed by this operator instance. - databricks_conn_id: The Airflow connection id for the snowflake target. - """ - - def __init__( - self, - *, - target: BaseTarget, - snowflake_conn_id: str = SnowflakeHook.default_conn_name, - **kwargs: t.Any, - ) -> None: - super().__init__( - target=target, - conn_id=snowflake_conn_id, - dialect="snowflake", - hook_type=SnowflakeHook, - **kwargs, - ) diff --git a/sqlmesh/schedulers/airflow/operators/spark_submit.py b/sqlmesh/schedulers/airflow/operators/spark_submit.py deleted file mode 100644 index 720f448381..0000000000 --- a/sqlmesh/schedulers/airflow/operators/spark_submit.py +++ /dev/null @@ -1,160 +0,0 @@ -import os -import tempfile -import typing as t - -from airflow.models import BaseOperator -from airflow.providers.apache.spark.hooks.spark_submit import SparkSubmitHook -from airflow.utils.context import Context - -import sqlmesh -from sqlmesh.engines import commands -from sqlmesh.schedulers.airflow.operators.targets import ( - BaseTarget, - SnapshotEvaluationTarget, -) - - -class SQLMeshSparkSubmitOperator(BaseOperator): - """The operator which evaluates a SQLMesh model snapshot using a dedicated Spark job instance. - - It requires the "spark-submit" binary to be available in the PATH or the spark_home - attribute to be set in the connection extras. - - Args: - target: The target that will be executed by this operator instance. - application_name: The name of the submitted application (default sqlmesh-spark). - spark_conf: Spark configuration properties. - connection_id: The Airflow connection ID as described in - https://airflow.apache.org/docs/apache-airflow-providers-apache-spark/stable/connections/spark.html - (default spark_default). - total_executor_cores: (Srandalone & Mesos only) The total number of cores for all executors. - executor_cores: (Standalone, YARN and Kubernetes only) The number of cores per executor. - executor_memory: The amount of memory allocated to each executor (e.g. 1024M, 2G). - driver_memory: The amount of memory allocated to the driver (e.g. 1024M, 2G). - keytab: The full path to the file that contains the keytab. - principal: The name of the Kerberos principal used for the keytab. - proxy_user: The name of a user which should be impersonated when submitting the application. - num_executors: The number of executors that will be allocateed to the application. - """ - - def __init__( - self, - *, - target: BaseTarget, - application_name: str = "sqlmesh-spark", - spark_conf: t.Optional[t.Dict[str, t.Any]] = None, - connection_id: str = "spark_default", - total_executor_cores: t.Optional[int] = None, - executor_cores: t.Optional[int] = None, - executor_memory: t.Optional[str] = None, - driver_memory: t.Optional[str] = None, - keytab: t.Optional[str] = None, - principal: t.Optional[str] = None, - proxy_user: t.Optional[str] = None, - num_executors: t.Optional[int] = None, - **kwargs: t.Any, - ) -> None: - super().__init__(**kwargs) - self._target = target - self._application_name = application_name - self._spark_conf = spark_conf or {} - self._total_executor_cores = total_executor_cores - self._executor_cores = executor_cores - self._executor_memory = executor_memory - self._driver_memory = driver_memory - self._keytab = keytab - self._principal = principal - self._proxy_user = proxy_user - self._num_executors = num_executors - self._connection_id = connection_id - self._application = os.path.join( - os.path.dirname(os.path.abspath(sqlmesh.__file__)), "engines/spark/app.py" - ) - self._hook: t.Optional[SparkSubmitHook] = None - - def execute(self, context: Context) -> None: - command_payload = self._target.serialized_command_payload(context) - with tempfile.TemporaryDirectory() as tmp: - payload_file_path = os.path.join(tmp, commands.COMMAND_PAYLOAD_FILE_NAME) - with open(payload_file_path, "w", encoding="utf-8") as payload_fd: - payload_fd.write(command_payload) - - if self._hook is None: - if ( - isinstance(self._target, SnapshotEvaluationTarget) - and self._target.snapshot.is_model - ): - session_properties = self._target.snapshot.model.session_properties - executor_cores: t.Optional[int] = session_properties.pop( # type: ignore - "spark.executor.cores", self._executor_cores - ) - executor_memory: t.Optional[str] = session_properties.pop( # type: ignore - "spark.executor.memory", self._executor_memory - ) - driver_memory: t.Optional[str] = session_properties.pop( # type: ignore - "spark.driver.memory", self._driver_memory - ) - num_executors: t.Optional[int] = session_properties.pop( # type: ignore - "spark.executor.instances", self._num_executors - ) - spark_conf: t.Dict[str, t.Any] = {**self._spark_conf, **session_properties} - else: - executor_cores = self._executor_cores - executor_memory = self._executor_memory - driver_memory = self._driver_memory - num_executors = self._num_executors - spark_conf = self._spark_conf - - self._hook = self._get_hook( - self._target.command_type, - payload_file_path, - self._target.ddl_concurrent_tasks, - spark_conf, - executor_cores, - executor_memory, - driver_memory, - num_executors, - ) - self._hook.submit(self._application) - self._target.post_hook(context) - - def on_kill(self) -> None: - if self._hook is None: - self._hook = self._get_hook(None, None, None, None, None, None, None, None) - self._hook.on_kill() - - def _get_hook( - self, - command_type: t.Optional[commands.CommandType], - command_payload_file_path: t.Optional[str], - ddl_concurrent_tasks: t.Optional[int], - spark_conf: t.Optional[t.Dict[str, t.Any]], - executor_cores: t.Optional[int], - executor_memory: t.Optional[str], - driver_memory: t.Optional[str], - num_executors: t.Optional[int], - ) -> SparkSubmitHook: - application_args = { - "dialect": "spark", - "default_catalog": self._target.default_catalog, - "command_type": command_type.value if command_type else None, - "ddl_concurrent_tasks": ddl_concurrent_tasks, - "payload_path": ( - command_payload_file_path.split("/")[-1] if command_payload_file_path else None - ), - } - return SparkSubmitHook( - conf=spark_conf, - conn_id=self._connection_id, - total_executor_cores=self._total_executor_cores, - executor_cores=executor_cores, - executor_memory=executor_memory, - driver_memory=driver_memory, - keytab=self._keytab, - principal=self._principal, - proxy_user=self._proxy_user, - name=self._application_name, - num_executors=num_executors, - application_args=[f"--{k}={v}" for k, v in application_args.items() if v is not None], - files=command_payload_file_path, - ) diff --git a/sqlmesh/schedulers/airflow/operators/targets.py b/sqlmesh/schedulers/airflow/operators/targets.py deleted file mode 100644 index b9e924b109..0000000000 --- a/sqlmesh/schedulers/airflow/operators/targets.py +++ /dev/null @@ -1,349 +0,0 @@ -import abc -import logging -import typing as t - -from airflow.exceptions import AirflowSkipException -from airflow.models import Variable -from airflow.utils.context import Context -from airflow.utils.session import provide_session -from sqlalchemy.orm import Session - -from sqlmesh.core.engine_adapter import create_engine_adapter -from sqlmesh.core.environment import EnvironmentNamingInfo -from sqlmesh.core.snapshot import ( - DeployabilityIndex, - Snapshot, - SnapshotEvaluator, - SnapshotTableInfo, -) -from sqlmesh.engines import commands -from sqlmesh.schedulers.airflow import common, util, NO_DEFAULT_CATALOG -from sqlmesh.utils.date import TimeLike -from sqlmesh.utils.pydantic import PydanticModel - -CP = t.TypeVar("CP", bound=PydanticModel) - - -class BaseTarget(abc.ABC, t.Generic[CP]): - command_type: commands.CommandType - command_handler: t.Callable[[SnapshotEvaluator, CP], None] - ddl_concurrent_tasks: int - - @property - def default_catalog(self) -> t.Optional[str]: - default_catalog = Variable.get(common.DEFAULT_CATALOG_VARIABLE_NAME) - if default_catalog == NO_DEFAULT_CATALOG: - return None - return default_catalog - - def serialized_command_payload(self, context: Context) -> str: - """Returns the serialized command payload for the Spark application. - - Args: - context: Airflow task context. - - Returns: - The serialized command payload. - """ - return self._get_command_payload_or_skip(context).json() - - def execute( - self, - context: Context, - connection_factory: t.Callable[[], t.Any], - dialect: str, - **kwargs: t.Any, - ) -> None: - """Executes this target. - - Args: - context: Airflow task context. - connection_factory: a callable which produces a new Database API compliant - connection on every call. - dialect: The dialect with which this adapter is associated. - """ - payload = self._get_command_payload_or_skip(context) - snapshot_evaluator = SnapshotEvaluator( - create_engine_adapter( - connection_factory, - dialect, - multithreaded=self.ddl_concurrent_tasks > 1, - execute_log_level=logging.INFO, - default_catalog=self.default_catalog, - **kwargs, - ), - ddl_concurrent_tasks=self.ddl_concurrent_tasks, - ) - try: - self.command_handler(snapshot_evaluator, payload) - self.post_hook(context) - finally: - snapshot_evaluator.close() - - def post_hook(self, context: Context, **kwargs: t.Any) -> None: - """The hook that should be invoked once the processing of this target - is complete. - - Args: - context: Airflow task context. - """ - - @abc.abstractmethod - def _get_command_payload(self, context: Context) -> t.Optional[CP]: - """Constructs the command payload. - - Args: - context: Airflow task context. - - Returns: - The command payload or None if there is no command to execute - and the target must be skipped. - """ - - def _get_command_payload_or_skip(self, context: Context) -> CP: - payload = self._get_command_payload(context) - if not payload: - self.post_hook(context) - raise AirflowSkipException - return payload - - -class SnapshotEvaluationTarget(PydanticModel, BaseTarget[commands.EvaluateCommandPayload]): - """The target which contains attributes necessary to evaluate a given snapshot. - - Args: - snapshot: The snapshot which should be evaluated. - parent_snapshots: All upstream snapshots to use for expansion and mapping of physical locations. - start: The start of the interval to evaluate. - end: The end of the interval to evaluate. - execution_time: The date/time time reference to use for execution time. Defaults to now. - deployability_index: Determines snapshots that are deployable in the context of this evaluation. - batch_index: For snapshots that are part of a batch, this is their position in the batch - """ - - command_type: commands.CommandType = commands.CommandType.EVALUATE - command_handler: t.Callable[[SnapshotEvaluator, commands.EvaluateCommandPayload], None] = ( - commands.evaluate - ) - ddl_concurrent_tasks: int = 1 - - snapshot: Snapshot - parent_snapshots: t.Dict[str, Snapshot] - start: t.Optional[TimeLike] = None - end: t.Optional[TimeLike] = None - execution_time: t.Optional[TimeLike] = None - deployability_index: DeployabilityIndex - plan_id: t.Optional[str] = None - batch_index: int = 0 - - def post_hook( - self, - context: Context, - **kwargs: t.Any, - ) -> None: - with util.scoped_state_sync() as state_sync: - state_sync.add_interval( - self.snapshot, - self._get_start(context), - self._get_end(context), - is_dev=not self.deployability_index.is_deployable(self.snapshot), - ) - - def _get_command_payload(self, context: Context) -> t.Optional[commands.EvaluateCommandPayload]: - return commands.EvaluateCommandPayload( - snapshot=self.snapshot, - parent_snapshots=self.parent_snapshots, - start=self._get_start(context), - end=self._get_end(context), - execution_time=self._get_execution_time(context), - deployability_index=self.deployability_index, - batch_index=self.batch_index, - ) - - def _get_start(self, context: Context) -> TimeLike: - if self.start: - return self.start - - start = self.snapshot.node.interval_unit.cron_floor(context["dag_run"].data_interval_start) # type: ignore - if not self.snapshot.is_model: - return start - - return self.snapshot.model.lookback_start(start) - - def _get_end(self, context: Context) -> TimeLike: - return self.end or self.snapshot.node.interval_unit.cron_floor( - context["dag_run"].data_interval_end # type: ignore - ) - - def _get_execution_time(self, context: Context) -> TimeLike: - return self.execution_time or context["dag_run"].logical_date - - -class SnapshotPromotionTarget(PydanticModel, BaseTarget[commands.PromoteCommandPayload]): - """The target which contains attributes necessary to perform snapshot promotion in a given environment. - - The promotion means creation of views associated with the environment which target physical tables - associated with the given list of snapshots. - - Args: - snapshots: The list of snapshots that should be promoted in the target environment. - environment_naming_info: Naming information for the target environment. - ddl_concurrent_tasks: The number of concurrent tasks used for DDL - operations (table / view creation, deletion, etc). Default: 1. - deployability_index: Determines snapshots that are deployable in the context of this promotion. - """ - - command_type: commands.CommandType = commands.CommandType.PROMOTE - command_handler: t.Callable[[SnapshotEvaluator, commands.PromoteCommandPayload], None] = ( - commands.promote - ) - - snapshots: t.List[Snapshot] - environment_naming_info: EnvironmentNamingInfo - ddl_concurrent_tasks: int - deployability_index: DeployabilityIndex - - def _get_command_payload(self, context: Context) -> t.Optional[commands.PromoteCommandPayload]: - return commands.PromoteCommandPayload( - snapshots=self.snapshots, - environment_naming_info=self.environment_naming_info, - deployability_index=self.deployability_index, - ) - - -class SnapshotDemotionTarget(PydanticModel, BaseTarget[commands.DemoteCommandPayload]): - """The target which contains attributes necessary to perform snapshot demotion in a given environment. - - The demotion means deletion of views that match names of provided snapshots in the target environment. - - Args: - snapshots: The list of snapshots that should be demoted in the target environment. - environment_naming_info: Naming information for the target environment. - """ - - command_type: commands.CommandType = commands.CommandType.DEMOTE - command_handler: t.Callable[[SnapshotEvaluator, commands.DemoteCommandPayload], None] = ( - commands.demote - ) - - snapshots: t.List[SnapshotTableInfo] - environment_naming_info: EnvironmentNamingInfo - ddl_concurrent_tasks: int - - def _get_command_payload(self, context: Context) -> t.Optional[commands.DemoteCommandPayload]: - return commands.DemoteCommandPayload( - snapshots=self.snapshots, - environment_naming_info=self.environment_naming_info, - ) - - -class SnapshotCleanupTarget(PydanticModel, BaseTarget[commands.CleanupCommandPayload]): - """The target which contains attributes necessary to perform table cleanup of expired snapshots""" - - command_type: commands.CommandType = commands.CommandType.CLEANUP - command_handler: t.Callable[[SnapshotEvaluator, commands.CleanupCommandPayload], None] = ( - commands.cleanup - ) - ddl_concurrent_tasks: int = 1 - - @provide_session - def post_hook( - self, - context: Context, - session: Session = util.PROVIDED_SESSION, - **kwargs: t.Any, - ) -> None: - _delete_xcom( - common.SNAPSHOT_CLEANUP_COMMAND_XCOM_KEY, - common.JANITOR_TASK_ID, - context, - session, - ) - - def _get_command_payload(self, context: Context) -> t.Optional[commands.CleanupCommandPayload]: - command = commands.CleanupCommandPayload.parse_raw( - context["ti"].xcom_pull(key=common.SNAPSHOT_CLEANUP_COMMAND_XCOM_KEY) - ) - if not command.tasks and not command.environments: - return None - return command - - -class SnapshotCreateTablesTarget(PydanticModel, BaseTarget[commands.CreateTablesCommandPayload]): - """The target which creates physical tables for the given set of new snapshots.""" - - command_type: commands.CommandType = commands.CommandType.CREATE_TABLES - command_handler: t.Callable[[SnapshotEvaluator, commands.CreateTablesCommandPayload], None] = ( - commands.create_tables - ) - - new_snapshots: t.List[Snapshot] - ddl_concurrent_tasks: int - deployability_index: DeployabilityIndex - allow_destructive_snapshots: t.Set[str] - - def _get_command_payload( - self, context: Context - ) -> t.Optional[commands.CreateTablesCommandPayload]: - if not self.new_snapshots: - return None - - return commands.CreateTablesCommandPayload( - target_snapshot_ids=[s.snapshot_id for s in self.new_snapshots], - snapshots=_get_snapshots_with_parents(self.new_snapshots), - deployability_index=self.deployability_index, - allow_destructive_snapshots=self.allow_destructive_snapshots, - ) - - -class SnapshotMigrateTablesTarget(PydanticModel, BaseTarget[commands.MigrateTablesCommandPayload]): - """The target which updates schemas of existing physical tables to bring them in correspondance - with schemas of target snapshots. - """ - - command_type: commands.CommandType = commands.CommandType.MIGRATE_TABLES - command_handler: t.Callable[[SnapshotEvaluator, commands.MigrateTablesCommandPayload], None] = ( - commands.migrate_tables - ) - - snapshots: t.List[Snapshot] - ddl_concurrent_tasks: int - allow_destructive_snapshots: t.Set[str] - - def _get_command_payload( - self, context: Context - ) -> t.Optional[commands.MigrateTablesCommandPayload]: - if not self.snapshots: - return None - - return commands.MigrateTablesCommandPayload( - target_snapshot_ids=[s.snapshot_id for s in self.snapshots], - snapshots=_get_snapshots_with_parents(self.snapshots), - allow_destructive_snapshots=self.allow_destructive_snapshots, - ) - - -def _get_snapshots_with_parents(snapshots: t.Iterable[Snapshot]) -> t.List[Snapshot]: - snapshots_by_id = {s.snapshot_id: s for s in snapshots} - - parent_snapshot_ids = {p_sid for snapshot in snapshots for p_sid in snapshot.parents} - missing_parent_ids = parent_snapshot_ids - set(snapshots_by_id.keys()) - - existing_snapshots = list(snapshots_by_id.values()) - - if not missing_parent_ids: - return existing_snapshots - - with util.scoped_state_sync() as state_sync: - return existing_snapshots + list(state_sync.get_snapshots(missing_parent_ids).values()) - - -def _delete_xcom(key: str, task_id: str, context: Context, session: Session) -> None: - ti = context["ti"] - util.delete_xcoms( - ti.dag_id, - {key}, - task_id=task_id, - run_id=ti.run_id, - session=session, - ) diff --git a/sqlmesh/schedulers/airflow/operators/trino.py b/sqlmesh/schedulers/airflow/operators/trino.py deleted file mode 100644 index a6267fe117..0000000000 --- a/sqlmesh/schedulers/airflow/operators/trino.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -import typing as t - -from airflow.providers.trino.hooks.trino import TrinoHook - -from sqlmesh.schedulers.airflow.operators.base import BaseDbApiOperator -from sqlmesh.schedulers.airflow.operators.targets import BaseTarget - - -class SQLMeshTrinoOperator(BaseDbApiOperator): - """The operator that evaluates a SQLMesh model snapshot on a Trino target - - Args: - target: The target that will be executed by this operator instance. - trino_conn_id: The Airflow connection id for the trino target. - """ - - def __init__( - self, - *, - target: BaseTarget, - trino_conn_id: str = TrinoHook.default_conn_name, - **kwargs: t.Any, - ) -> None: - super().__init__( - target=target, conn_id=trino_conn_id, dialect="trino", hook_type=TrinoHook, **kwargs - ) diff --git a/sqlmesh/schedulers/airflow/plan.py b/sqlmesh/schedulers/airflow/plan.py deleted file mode 100644 index f0e6d27406..0000000000 --- a/sqlmesh/schedulers/airflow/plan.py +++ /dev/null @@ -1,268 +0,0 @@ -from __future__ import annotations - -import typing as t - -import pandas as pd -from sqlglot import exp - -from sqlmesh import Snapshot -from sqlmesh.core.engine_adapter import EngineAdapter -from sqlmesh.core.environment import Environment -from sqlmesh.core.plan import update_intervals_for_new_snapshots -from sqlmesh.core.snapshot import DeployabilityIndex, SnapshotTableInfo, SnapshotId -from sqlmesh.core.snapshot.definition import Interval as SnapshotInterval, missing_intervals -from sqlmesh.core.state_sync import EngineAdapterStateSync, StateSync -from sqlmesh.core.state_sync.base import DelegatingStateSync -from sqlmesh.schedulers.airflow import common -from sqlmesh.utils.date import now, to_timestamp, TimeLike, to_datetime -from sqlmesh.utils.errors import SQLMeshError - - -class PlanDagState: - def __init__(self, engine_adapter: EngineAdapter, plan_dags_table: exp.Table): - self.engine_adapter = engine_adapter - - self._plan_dags_table = plan_dags_table - - self._plan_dag_columns_to_types = { - "request_id": exp.DataType.build("text"), - "dag_id": exp.DataType.build("text"), - "dag_spec": exp.DataType.build("text"), - } - - @classmethod - def from_state_sync(cls, state_sync: StateSync) -> PlanDagState: - while isinstance(state_sync, DelegatingStateSync): - state_sync = state_sync.state_sync - if not isinstance(state_sync, EngineAdapterStateSync): - raise SQLMeshError(f"Unsupported state sync {state_sync.__class__.__name__}") - return cls(state_sync.engine_adapter, state_sync.plan_dags_table) - - def add_dag_spec(self, spec: common.PlanDagSpec) -> None: - """Adds a new DAG spec to the state. - - Args: - spec: the plan DAG spec to add. - """ - df = pd.DataFrame( - [ - { - "request_id": spec.request_id, - "dag_id": common.plan_application_dag_id( - spec.environment.name, spec.request_id - ), - "dag_spec": spec.json(), - } - ] - ) - self.engine_adapter.insert_append( - self._plan_dags_table, - df, - columns_to_types=self._plan_dag_columns_to_types, - ) - - def get_dag_specs(self) -> t.List[common.PlanDagSpec]: - """Returns all DAG specs in the state.""" - query = exp.select("dag_spec").from_(self._plan_dags_table) - return [ - common.PlanDagSpec.parse_raw(row[0]) - for row in self.engine_adapter.fetchall( - query, ignore_unsupported_errors=True, quote_identifiers=True - ) - ] - - def delete_dag_specs(self, dag_ids: t.Collection[str]) -> None: - """Deletes the DAG specs with the given DAG IDs.""" - if not dag_ids: - return - self.engine_adapter.delete_from( - self._plan_dags_table, - where=exp.column("dag_id").isin(*dag_ids), - ) - - -def airflow_compute_interval_params( - snapshots: t.Collection[Snapshot], - *, - start: TimeLike, - end: TimeLike, - deployability_index: t.Optional[DeployabilityIndex] = None, - execution_time: t.Optional[TimeLike] = None, - restatements: t.Optional[t.Dict[SnapshotId, SnapshotInterval]] = None, - interval_end_per_model: t.Optional[t.Dict[str, int]] = None, - ignore_cron: bool = False, - end_bounded: bool = False, -) -> common.SnapshotToDatetimeRanges: - """Find the optimal date interval parameters based on what needs processing and maximal batch size. - - For each node name, find all dependencies and look for a stored snapshot from the metastore. If a snapshot is found, - calculate the missing intervals that need to be processed given the passed in start and end intervals. - - If a snapshot's node specifies a batch size, consecutive intervals are merged into batches of a size that is less than - or equal to the configured one. If no batch size is specified, then it uses the intervals that correspond to the node's cron expression. - For example, if a node is supposed to run daily and has 70 days to backfill with a batch size set to 30, there would be 2 jobs - with 30 days and 1 job with 10. - - Args: - snapshots: A set of target snapshots for which intervals should be computed. - start: Start of the interval. - end: End of the interval. - deployability_index: Determines snapshots that are deployable in the context of this evaluation. - execution_time: The date/time reference to use for execution time. - restatements: A dict of snapshot names being restated and their intervals. - interval_end_per_model: The mapping from model FQNs to target end dates. - ignore_cron: Whether to ignore the node's cron schedule. - end_bounded: If set to true, the returned intervals will be bounded by the target end date, disregarding lookback, - allow_partials, and other attributes that could cause the intervals to exceed the target end date. - - Returns: - A dict containing all snapshots needing to be run with their associated interval params. - """ - snapshot_batches = {} - - for snapshot, intervals in missing_intervals( - snapshots, - start=start, - end=end, - execution_time=execution_time, - restatements=restatements, - deployability_index=deployability_index, - interval_end_per_model=interval_end_per_model, - ignore_cron=ignore_cron, - end_bounded=end_bounded, - ).items(): - batches = [] - batch_size = snapshot.node.batch_size - next_batch: t.List[t.Tuple[int, int]] = [] - - for interval in intervals: - if (batch_size and len(next_batch) >= batch_size) or ( - next_batch and interval[0] != next_batch[-1][-1] - ): - batches.append((next_batch[0][0], next_batch[-1][-1])) - next_batch = [] - next_batch.append(interval) - if next_batch: - batches.append((next_batch[0][0], next_batch[-1][-1])) - snapshot_batches[snapshot] = [(to_datetime(s), to_datetime(e)) for s, e in batches] - - return snapshot_batches - - -def create_plan_dag_spec( - request: common.PlanApplicationRequest, state_sync: StateSync -) -> common.PlanDagSpec: - plan = request.plan - new_snapshots = {s.snapshot_id: s for s in plan.new_snapshots} - stored_snapshots = state_sync.get_snapshots([*new_snapshots, *plan.environment.snapshots]) - all_snapshots = {**new_snapshots, **stored_snapshots} - - all_snaphots_by_name = {s.name: s for s in all_snapshots.values()} - restatements = { - all_snaphots_by_name[n].snapshot_id: i - for n, i in plan.restatements.items() - if n in all_snaphots_by_name - } - - duplicated_snapshots = set(stored_snapshots).intersection(new_snapshots) - if duplicated_snapshots: - raise SQLMeshError( - f"Snapshots {duplicated_snapshots} already exist. " - "Make sure your code base is up to date and try re-creating the plan" - ) - - update_intervals_for_new_snapshots(new_snapshots.values(), state_sync) - - now_dt = now() - end = plan.environment.end_at or now_dt - unpaused_dt = end if not plan.is_dev and not plan.restatements else None - - if plan.restatements: - intervals_to_remove = [ - (s, restatements[s.snapshot_id]) - for s in all_snapshots.values() - if s.snapshot_id in restatements and s.snapshot_id not in new_snapshots - ] - state_sync.remove_intervals( - intervals_to_remove, - remove_shared_versions=not plan.is_dev, - ) - for s, interval in intervals_to_remove: - all_snapshots[s.snapshot_id].remove_interval(interval) - - deployability_index_for_creation = DeployabilityIndex.create(all_snapshots, start=plan.start) - deployability_index_for_evaluation = ( - deployability_index_for_creation if plan.is_dev else DeployabilityIndex.all_deployable() - ) - - if not plan.skip_backfill: - backfill_batches = airflow_compute_interval_params( - [s for s in all_snapshots.values() if plan.is_selected_for_backfill(s.name)], - start=plan.environment.start_at, - end=end, - execution_time=plan.execution_time or now(), - deployability_index=deployability_index_for_evaluation, - restatements=restatements, - interval_end_per_model=plan.interval_end_per_model, - end_bounded=plan.end_bounded, - ) - else: - backfill_batches = {} - - backfill_intervals_per_snapshot = [ - common.BackfillIntervalsPerSnapshot( - snapshot_id=s.snapshot_id, - intervals=intervals, - before_promote=plan.is_dev or deployability_index_for_creation.is_representative(s), - ) - for s, intervals in backfill_batches.items() - ] - - no_gaps_snapshot_names = ( - { - s.name - for s in all_snapshots.values() - if deployability_index_for_creation.is_representative(s) - } - if plan.no_gaps and not plan.is_dev - else None - if plan.no_gaps - else set() - ) - - return common.PlanDagSpec( - request_id=plan.plan_id, - environment=plan.environment, - new_snapshots=plan.new_snapshots, - backfill_intervals_per_snapshot=backfill_intervals_per_snapshot, - demoted_snapshots=_get_demoted_snapshots(plan.environment, state_sync), - unpaused_dt=unpaused_dt, - no_gaps=plan.no_gaps, - notification_targets=request.notification_targets, - backfill_concurrent_tasks=request.backfill_concurrent_tasks, - ddl_concurrent_tasks=request.ddl_concurrent_tasks, - users=request.users, - is_dev=plan.is_dev, - allow_destructive_snapshots=plan.allow_destructive_models, - forward_only=plan.forward_only, - dag_start_ts=to_timestamp(now_dt), - deployability_index=deployability_index_for_evaluation, - deployability_index_for_creation=deployability_index_for_creation, - no_gaps_snapshot_names=no_gaps_snapshot_names, - models_to_backfill=plan.models_to_backfill, - ensure_finalized_snapshots=plan.ensure_finalized_snapshots, - directly_modified_snapshots=plan.directly_modified_snapshots, - indirectly_modified_snapshots=plan.indirectly_modified_snapshots, - removed_snapshots=plan.removed_snapshots, - execution_time=plan.execution_time, - ) - - -def _get_demoted_snapshots( - new_environment: Environment, state_sync: StateSync -) -> t.List[SnapshotTableInfo]: - current_environment = state_sync.get_environment(new_environment.name) - if current_environment: - preserved_snapshot_names = {s.name for s in new_environment.snapshots} - return [s for s in current_environment.snapshots if s.name not in preserved_snapshot_names] - return [] diff --git a/sqlmesh/schedulers/airflow/plugin.py b/sqlmesh/schedulers/airflow/plugin.py deleted file mode 100644 index 6813bcc30b..0000000000 --- a/sqlmesh/schedulers/airflow/plugin.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import logging -import os -import time -import typing as t - -from airflow.models import Variable -from airflow.plugins_manager import AirflowPlugin - -from sqlmesh.core import constants as c -from sqlmesh.schedulers.airflow import util, NO_DEFAULT_CATALOG -from sqlmesh.schedulers.airflow.api import sqlmesh_api_v1 -from sqlmesh.schedulers.airflow.common import ( - DEFAULT_CATALOG_VARIABLE_NAME, -) -from sqlmesh.utils import str_to_bool -from sqlmesh.utils.errors import SQLMeshError - -logger = logging.getLogger(__name__) - - -class SqlmeshAirflowPlugin(AirflowPlugin): - name = c.SQLMESH - flask_blueprints = [sqlmesh_api_v1] - - @classmethod - def on_load(cls, *args: t.Any, **kwargs: t.Any) -> None: - if os.environ.get("MWAA_AIRFLOW_COMPONENT", "").lower() == "webserver": - # When using MWAA, the Webserver instance might not have access to the external state database. - logger.info("MWAA Webserver instance detected. Skipping SQLMesh state migration...") - return - - if str_to_bool(os.environ.get(c.DISABLE_SQLMESH_STATE_MIGRATION, "0")): - logger.info("SQLMesh state migration disabled. Must be handled outside of Airflow") - return - - # We want to different an expected None default catalog (where the user set `NO_DEFAULT_CATALOG`) - # and where the default catalog is not set at all. - default_catalog = Variable.get( - DEFAULT_CATALOG_VARIABLE_NAME, default_var="MISSING_REQUIRED_CATALOG" - ) - if default_catalog == NO_DEFAULT_CATALOG: - # If the user explicitly set `NO_DEFAULT_CATALOG` we want to set the default catalog to None. - default_catalog = None - - with util.scoped_state_sync() as state_sync: - try: - # If default catalog is required but missing (and not explicitly set to None) we want to raise unless - # this is a fresh install since we know nothing needs to be migrated and - # the client will prevent making any changes until the default catalog is set. - if default_catalog == "MISSING_REQUIRED_CATALOG": - versions = state_sync.get_versions(validate=False) - if versions.schema_version != 0: - raise SQLMeshError( - "Must define `default_catalog` when creating `SQLMeshAirflow` object. See docs for more info: https://sqlmesh.readthedocs.io/en/stable/integrations/airflow/#airflow-cluster-configuration" - ) - logger.info("Migrating SQLMesh state ...") - state_sync.migrate(default_catalog=default_catalog) - except Exception as ex: - # This method is called once for each Gunicorn worker spawned by the Airflow Webserver, - # which leads to SQLMesh schema being initialized concurrently from multiple processes. - # There is a known issue in Postgres (https://stackoverflow.com/a/29908840) which occurs - # due to a race condition when a new schema is being created concurrently. Here we retry - # the schema initialization once as a workaround. - logger.warning("Failed to initialize the SQLMesh State Sync: %s. Retrying...", ex) - time.sleep(1) - state_sync.migrate(default_catalog=default_catalog) diff --git a/sqlmesh/schedulers/airflow/state_sync.py b/sqlmesh/schedulers/airflow/state_sync.py deleted file mode 100644 index fecf15199a..0000000000 --- a/sqlmesh/schedulers/airflow/state_sync.py +++ /dev/null @@ -1,370 +0,0 @@ -from __future__ import annotations - -import logging -import typing as t - -from sqlmesh.core.console import Console -from sqlmesh.core.environment import Environment, EnvironmentStatements -from sqlmesh.core.snapshot import ( - Snapshot, - SnapshotId, - SnapshotIdLike, - SnapshotInfoLike, - SnapshotTableCleanupTask, - SnapshotNameVersion, -) -from sqlmesh.core.snapshot.definition import Interval, SnapshotIntervals -from sqlmesh.core.state_sync import StateSync, Versions -from sqlmesh.core.state_sync.base import PromotionResult -from sqlmesh.schedulers.airflow.client import AirflowClient -from sqlmesh.core.state_sync.common import StateStream - -if t.TYPE_CHECKING: - from sqlmesh.utils.date import TimeLike - -logger = logging.getLogger(__name__) - - -class HttpStateSync(StateSync): - """Reads state of models and snapshot through the Airflow REST API. - - Args: - airflow_url: URL pointing to the airflow rest api. - username: Username for Airflow. - password: Password for Airflow. - blocking_updates: Indicates whether calls that cause state updates should be blocking. - dag_run_poll_interval_secs: Determines how frequently the state of a DAG run should be checked. - Used to block on calls that update the state. - console: Used to print out tracking URLs. - """ - - def __init__( - self, - client: AirflowClient, - blocking_updates: bool = True, - dag_run_poll_interval_secs: int = 2, - console: t.Optional[Console] = None, - ): - self._client = client - self.blocking_updates = blocking_updates - self.dag_run_poll_interval_secs = dag_run_poll_interval_secs - self.console = console - - def get_environment(self, environment: str) -> t.Optional[Environment]: - """Fetches the environment if it exists. - - Args: - environment: The environment - - Returns: - The environment object. - """ - return self._client.get_environment(environment) - - def get_environments(self) -> t.List[Environment]: - """Fetches all environments. - - Returns: - A list of all environments. - """ - return self._client.get_environments() - - def get_environments_summary(self) -> t.Dict[str, int]: - """Fetches all environment names along with expiry datetime.""" - raise NotImplementedError( - "get_environments_summary method is not implemented for the Airflow state sync." - ) - - def max_interval_end_per_model( - self, - environment: str, - models: t.Optional[t.Set[str]] = None, - ensure_finalized_snapshots: bool = False, - ) -> t.Dict[str, int]: - """Returns the max interval end per model for the given environment. - - Args: - environment: The target environment. - models: The models to get the max interval end for. If None, all models are considered. - ensure_finalized_snapshots: Whether to use snapshots from the latest finalized environment state, - or to use whatever snapshots are in the current environment state even if the environment is not finalized. - - Returns: - A dictionary of model FQNs to their respective interval ends in milliseconds since epoch. - """ - return self._client.max_interval_end_per_model( - environment, models=models, ensure_finalized_snapshots=ensure_finalized_snapshots - ) - - def get_snapshots( - self, - snapshot_ids: t.Optional[t.Iterable[SnapshotIdLike]], - ) -> t.Dict[SnapshotId, Snapshot]: - """Gets multiple snapshots from the rest api. - - Because of the limitations of the Airflow API, this method is inherently inefficient. - It's impossible to bulkfetch the snapshots and thus every snapshot needs to make an individual - call to the rest api. Multiple threads can be used, but it could possibly have detrimental effects - on the production server. - """ - snapshots = self._client.get_snapshots( - [s.snapshot_id for s in snapshot_ids] if snapshot_ids is not None else None - ) - return {snapshot.snapshot_id: snapshot for snapshot in snapshots} - - def snapshots_exist(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> t.Set[SnapshotId]: - """Checks if multiple snapshots exist in the state sync. - - Args: - snapshot_ids: Iterable of snapshot ids to bulk check. - - Returns: - A set of existing snapshot IDs. - """ - if not snapshot_ids: - return set() - return self._client.snapshots_exist([s.snapshot_id for s in snapshot_ids]) - - def nodes_exist(self, names: t.Iterable[str], exclude_external: bool = False) -> t.Set[str]: - """Returns the node names that exist in the state sync. - - Args: - names: Iterable of node names to check. - exclude_external: Whether to exclude external models from the output. - - Returns: - A set of all the existing node names. - """ - return self._client.nodes_exist(names, exclude_external=exclude_external) - - def _get_versions(self, lock_for_update: bool = False) -> Versions: - """Queries the store to get the migration. - - Args: - lock_for_update: Whether or not the usage of this method plans to update the row. - - Returns: - The versions object. - """ - return self._client.get_versions() - - def push_snapshots(self, snapshots: t.Iterable[Snapshot]) -> None: - """Push snapshots into the state sync. - - This method only allows for pushing new snapshots. If existing snapshots are found, - this method should raise an error. - - Raises: - SQLMeshError when existing snapshots are pushed. - - Args: - snapshots: A list of snapshots to save in the state sync. - """ - raise NotImplementedError("Pushing snapshots is not supported by the Airflow state sync.") - - def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None: - """Delete snapshots from the state sync. - - Args: - snapshot_ids: A list of snapshot like objects to delete. - """ - raise NotImplementedError("Deleting snapshots is not supported by the Airflow state sync.") - - def delete_expired_snapshots( - self, ignore_ttl: bool = False - ) -> t.List[SnapshotTableCleanupTask]: - """Removes expired snapshots. - - Expired snapshots are snapshots that have exceeded their time-to-live - and are no longer in use within an environment. - - Returns: - The list of table cleanup tasks. - """ - raise NotImplementedError( - "Deleting expired snapshots is not supported by the Airflow state sync." - ) - - def invalidate_environment(self, name: str) -> None: - """Invalidates the target environment by setting its expiration timestamp to now. - - Args: - name: The name of the environment to invalidate. - """ - self._client.invalidate_environment(name) - - def add_interval( - self, - snapshot: Snapshot, - start: TimeLike, - end: TimeLike, - is_dev: bool = False, - ) -> None: - """Add an interval to a snapshot and sync it to the store. - - Snapshots must be pushed before adding intervals to them. - - Args: - snapshot: The snapshot like object to add an interval to. - start: The start of the interval to add. - end: The end of the interval to add. - is_dev: Indicates whether the given interval is being added while in - development mode. - """ - raise NotImplementedError("Adding intervals is not supported by the Airflow state sync.") - - def add_snapshots_intervals(self, snapshots_intervals: t.Sequence[SnapshotIntervals]) -> None: - raise NotImplementedError("Adding intervals is not supported by the Airflow state sync.") - - def remove_intervals( - self, - snapshot_intervals: t.Sequence[t.Tuple[SnapshotInfoLike, Interval]], - remove_shared_versions: bool = False, - ) -> None: - """Remove an interval from a list of snapshots and sync it to the store. - - Because multiple snapshots can be pointing to the same version or physical table, this method - can also grab all snapshots tied to the passed in version. - - Args: - snapshots: The snapshot info like object to remove intervals from. - start: The start of the interval to add. - end: The end of the interval to add. - all_snapshots: All snapshots can be passed in to skip fetching matching snapshot versions. - """ - raise NotImplementedError("Removing intervals is not supported by the Airflow state sync.") - - def refresh_snapshot_intervals(self, snapshots: t.Collection[Snapshot]) -> t.List[Snapshot]: - """Updates given snapshots with latest intervals from the state. - - Args: - snapshots: The snapshots to refresh. - - Returns: - The updated snapshots. - """ - logger.warning("Refreshing snapshot intervals is not supported by the Airflow state sync") - return list(snapshots) - - def promote( - self, - environment: Environment, - no_gaps_snapshot_names: t.Optional[t.Set[str]] = None, - environment_statements: t.Optional[t.List[EnvironmentStatements]] = None, - ) -> PromotionResult: - """Update the environment to reflect the current state. - - This method verifies that snapshots have been pushed. - - Args: - environment: The environment to promote. - no_gaps_snapshot_names: A set of snapshot names to check for data gaps. If None, - all snapshots will be checked. The data gap check ensures that models that are already a - part of the target environment have no data gaps when compared against previous - snapshots for same models. - - Returns: - A promotion result object containing added, removed, and removed environment naming info - """ - raise NotImplementedError( - "Promoting environments is not supported by the Airflow state sync." - ) - - def finalize(self, environment: Environment) -> None: - """Finalize the target environment, indicating that this environment has been - fully promoted and is ready for use. - - Args: - environment: The target environment to finalize. - """ - raise NotImplementedError( - "Finalizing environments is not supported by the Airflow state sync." - ) - - def delete_expired_environments(self) -> t.List[Environment]: - """Removes expired environments. - - Expired environments are environments that have exceeded their time-to-live value. - - Returns: - The list of removed environments. - """ - raise NotImplementedError( - "Deleting expired environments is not supported by the Airflow state sync." - ) - - def unpause_snapshots( - self, snapshots: t.Collection[SnapshotInfoLike], unpaused_dt: TimeLike - ) -> None: - """Unpauses target snapshots. - - Unpaused snapshots are scheduled for evaluation on a recurring basis. - Once unpaused a snapshot can't be paused again. - - Args: - snapshots: Target snapshots. - unpaused_dt: The datetime object which indicates when target snapshots - were unpaused. - """ - raise NotImplementedError("Unpausing snapshots is not supported by the Airflow state sync.") - - def compact_intervals(self) -> None: - """Compacts intervals for all snapshots. - - Compaction process involves merging of existing interval records into new records and - then deleting the old ones. - """ - raise NotImplementedError( - "Compacting intervals is not supported by the Airflow state sync." - ) - - def update_auto_restatements( - self, next_auto_restatement_ts: t.Dict[SnapshotNameVersion, t.Optional[int]] - ) -> None: - raise NotImplementedError( - "Updating auto restatements is not supported by the Airflow state sync." - ) - - def get_environment_statements(self, environment: str) -> t.List[EnvironmentStatements]: - """Fetches the environment's statements from the environment_statements table. - Args: - environment: The environment name - - Returns: - A list of the environment statements. - - """ - logger.warning( - "Fetching environment statements is not supported by the Airflow state sync." - ) - return [] - - def migrate( - self, - default_catalog: t.Optional[str], - skip_backup: bool = False, - promoted_snapshots_only: bool = True, - ) -> None: - """Migrate the state sync to the latest SQLMesh / SQLGlot version.""" - raise NotImplementedError("Migration is not supported by the Airflow state sync.") - - def rollback(self) -> None: - """Rollback to previous backed up state.""" - raise NotImplementedError("Rollback is not supported by the Airflow state sync.") - - def recycle(self) -> None: - """Closes all open connections and releases all allocated resources associated with any thread - except the calling one.""" - - def close(self) -> None: - """Closes all open connections and releases all allocated resources.""" - - def state_type(self) -> str: - """Returns the type of state sync.""" - return "airflow_http" - - def export(self, environment_names: t.Optional[t.List[str]] = None) -> StateStream: - raise NotImplementedError("State export is not supported by the Airflow state sync") - - def import_(self, stream: StateStream, clear: bool = True) -> None: - raise NotImplementedError("State import is not supported by the Airflow state sync") diff --git a/sqlmesh/schedulers/airflow/util.py b/sqlmesh/schedulers/airflow/util.py deleted file mode 100644 index 321331a4b1..0000000000 --- a/sqlmesh/schedulers/airflow/util.py +++ /dev/null @@ -1,188 +0,0 @@ -from __future__ import annotations - -import contextlib -import json -import logging -import typing as t -from datetime import timedelta - -from airflow import settings -from airflow.api.common.experimental.delete_dag import delete_dag -from airflow.exceptions import AirflowException, DagNotFound -from airflow.models import BaseOperator, DagRun, DagTag, XCom -from airflow.models.connection import Connection -from airflow.utils.session import provide_session -from airflow.utils.state import DagRunState -from sqlalchemy.orm import Session - -from sqlmesh.core import constants as c -from sqlmesh.core.config import parse_connection_config -from sqlmesh.core.engine_adapter import create_engine_adapter -from sqlmesh.core.state_sync import CachingStateSync, EngineAdapterStateSync, StateSync -from sqlmesh.schedulers.airflow import common -from sqlmesh.utils.date import now -from sqlmesh.utils.errors import SQLMeshError - -logger = logging.getLogger(__name__) - - -# Used to omit Optional for session instances supplied by -# Airflow at runtime. This makes the type signature cleaner -# and prevents mypy from complaining. -PROVIDED_SESSION: Session = t.cast(Session, None) - - -SQLMESH_STATE_CONN_ID = "sqlmesh_state_db" - - -@contextlib.contextmanager -def scoped_state_sync() -> t.Iterator[StateSync]: - state_schema = c.SQLMESH - try: - connection = Connection.get_connection_from_secrets(SQLMESH_STATE_CONN_ID) - - connection_config_dict = json.loads(connection.extra) - state_schema = connection_config_dict.pop("state_schema", state_schema) - if "type" not in connection_config_dict: - logger.info( - "SQLMesh connection in Airflow did not have type defined. " - "Therefore using Airflow database connection" - ) - raise AirflowException - - logger.info("Using connection '%s' for state sync", connection.conn_id) - - connection_config = parse_connection_config(connection_config_dict) - engine_adapter = connection_config.create_engine_adapter() - except AirflowException: - logger.info("Using the Airflow database connection for state sync") - - dialect = settings.engine.dialect.name - engine_adapter = create_engine_adapter( - settings.engine.raw_connection, dialect, multithreaded=True - ) - - try: - yield CachingStateSync(EngineAdapterStateSync(engine_adapter, state_schema)) # type: ignore - finally: - engine_adapter.close() - - -@provide_session -def get_snapshot_dag_ids(session: Session = PROVIDED_SESSION) -> t.List[str]: - dag_tags = session.query(DagTag).filter(DagTag.name == common.SNAPSHOT_AIRFLOW_TAG).all() - return [tag.dag_id for tag in dag_tags] - - -@provide_session -def get_finished_plan_application_dag_ids( - ttl: t.Optional[timedelta] = None, session: Session = PROVIDED_SESSION -) -> t.Set[str]: - dag_ids = ( - session.query(DagTag.dag_id) - .join(DagRun, DagTag.dag_id == DagRun.dag_id) - .filter( - DagTag.name == common.PLAN_AIRFLOW_TAG, - DagRun.state.in_((DagRunState.SUCCESS, DagRunState.FAILED)), - ) - ) - if ttl is not None: - dag_ids = dag_ids.filter(DagRun.last_scheduling_decision <= now() - ttl) - return {dag_id[0] for dag_id in dag_ids.all()} - - -@provide_session -def delete_dags(dag_ids: t.Set[str], session: Session = PROVIDED_SESSION) -> None: - for dag_id in dag_ids: - try: - delete_dag(dag_id, session=session) - except DagNotFound: - logger.warning("DAG '%s' was not found", dag_id) - except AirflowException: - logger.warning("Failed to delete DAG '%s'", dag_id, exc_info=True) - - -@provide_session -def delete_xcoms( - dag_id: str, - keys: t.Set[str], - task_id: t.Optional[str] = None, - run_id: t.Optional[str] = None, - session: Session = PROVIDED_SESSION, -) -> None: - query = session.query(XCom).filter(XCom.dag_id == dag_id, XCom.key.in_(keys)) - if task_id is not None: - query = query.filter_by(task_id=task_id) - if run_id is not None: - query = query.filter_by(run_id=run_id) - query.delete(synchronize_session=False) - - -def discover_engine_operator(name: str, sql_only: bool = False) -> t.Type[BaseOperator]: - name = name.lower() - - try: - if name == "clickhouse": - from sqlmesh.schedulers.airflow.operators.clickhouse import SQLMeshClickHouseOperator - - return SQLMeshClickHouseOperator - if name == "spark": - from sqlmesh.schedulers.airflow.operators.spark_submit import ( - SQLMeshSparkSubmitOperator, - ) - - return SQLMeshSparkSubmitOperator - if name in ("databricks", "databricks-submit", "databricks-sql"): - if name == "databricks-submit" or (name == "databricks" and not sql_only): - from sqlmesh.schedulers.airflow.operators.databricks import ( - SQLMeshDatabricksSubmitOperator, - ) - - return SQLMeshDatabricksSubmitOperator - if name == "databricks-sql" or (name == "databricks" and sql_only): - from sqlmesh.schedulers.airflow.operators.databricks import ( - SQLMeshDatabricksSQLOperator, - ) - - return SQLMeshDatabricksSQLOperator - if name == "snowflake": - from sqlmesh.schedulers.airflow.operators.snowflake import ( - SQLMeshSnowflakeOperator, - ) - - return SQLMeshSnowflakeOperator - if name == "bigquery": - from sqlmesh.schedulers.airflow.operators.bigquery import ( - SQLMeshBigQueryOperator, - ) - - return SQLMeshBigQueryOperator - if name == "redshift": - from sqlmesh.schedulers.airflow.operators.redshift import ( - SQLMeshRedshiftOperator, - ) - - return SQLMeshRedshiftOperator - if name in ("postgres", "postgresql"): - from sqlmesh.schedulers.airflow.operators.postgres import ( - SQLMeshPostgresOperator, - ) - - return SQLMeshPostgresOperator - if name == "trino": - from sqlmesh.schedulers.airflow.operators.trino import SQLMeshTrinoOperator - - return SQLMeshTrinoOperator - if name == "mssql": - from sqlmesh.schedulers.airflow.operators.mssql import SQLMeshMsSqlOperator - - return SQLMeshMsSqlOperator - - if name == "mysql": - from sqlmesh.schedulers.airflow.operators.mysql import SQLMeshMySqlOperator - - return SQLMeshMySqlOperator - except ImportError: - raise SQLMeshError(f"Failed to automatically discover an operator for '{name}'.'") - - raise ValueError(f"Unsupported engine name '{name}'.") diff --git a/tests/common_fixtures.py b/tests/common_fixtures.py deleted file mode 100644 index 91d09d470f..0000000000 --- a/tests/common_fixtures.py +++ /dev/null @@ -1,9 +0,0 @@ -import pytest -from pytest_mock.plugin import MockerFixture - -from sqlmesh.schedulers.airflow.client import AirflowClient - - -@pytest.fixture(scope="function") -def mock_airflow_client(mocker: MockerFixture) -> AirflowClient: - return AirflowClient(airflow_url="", session=mocker.Mock()) diff --git a/tests/conftest.py b/tests/conftest.py index 56c50a5851..a690222cf4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,8 +41,6 @@ from sqlmesh.utils.date import TimeLike, to_date from sqlmesh.core.engine_adapter.shared import CatalogSupport -pytest_plugins = ["tests.common_fixtures"] - T = t.TypeVar("T", bound=EngineAdapter) diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 2fd4640626..8690e2ef34 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -16,8 +16,6 @@ BigQueryConnectionConfig, MotherDuckConnectionConfig, BuiltInSchedulerConfig, - MWAASchedulerConfig, - AirflowSchedulerConfig, ) from sqlmesh.core.config.connection import DuckDBAttachOptions, RedshiftConnectionConfig from sqlmesh.core.config.feature_flag import DbtFeatureFlag, FeatureFlag @@ -306,7 +304,6 @@ def test_load_config_from_env_no_config_vars(): with mock.patch.dict( os.environ, { - "SQLMESH__AIRFLOW__DISABLE_STATE_MIGRATION": "1", "DUMMY_ENV_VAR": "dummy", }, ): @@ -415,31 +412,6 @@ def test_load_config_from_python_module_invalid_config_object(tmp_path): load_config_from_python_module(Config, config_path) -def test_cloud_composer_scheduler_config(tmp_path_factory): - config_path = tmp_path_factory.mktemp("yaml_config") / "config.yaml" - with open(config_path, "w", encoding="utf-8") as fd: - fd.write( - """ -gateways: - another_gateway: - connection: - type: duckdb - database: test_db - scheduler: - type: cloud_composer - airflow_url: https://airflow.url - -model_defaults: - dialect: bigquery - """ - ) - - assert load_config_from_paths( - Config, - project_paths=[config_path], - ) - - @pytest.mark.parametrize( [ "mapping", @@ -691,14 +663,6 @@ def test_scheduler_config(tmp_path_factory): fd.write( """ gateways: - airflow_gateway: - scheduler: - type: airflow - airflow_url: https://airflow.url - mwaa_gateway: - scheduler: - type: mwaa - environment: test_environment builtin_gateway: scheduler: type: builtin @@ -716,8 +680,6 @@ def test_scheduler_config(tmp_path_factory): project_paths=[config_path], ) assert isinstance(config.default_scheduler, BuiltInSchedulerConfig) - assert isinstance(config.get_gateway("airflow_gateway").scheduler, AirflowSchedulerConfig) - assert isinstance(config.get_gateway("mwaa_gateway").scheduler, MWAASchedulerConfig) assert isinstance(config.get_gateway("builtin_gateway").scheduler, BuiltInSchedulerConfig) diff --git a/tests/core/test_plan_evaluator.py b/tests/core/test_plan_evaluator.py index 18aba9d663..8e886c227d 100644 --- a/tests/core/test_plan_evaluator.py +++ b/tests/core/test_plan_evaluator.py @@ -5,16 +5,13 @@ from sqlmesh.core.context import Context from sqlmesh.core.model import FullKind, SqlModel, ViewKind from sqlmesh.core.plan import ( - AirflowPlanEvaluator, BuiltInPlanEvaluator, - MWAAPlanEvaluator, Plan, PlanBuilder, update_intervals_for_new_snapshots, ) from sqlmesh.core.snapshot import SnapshotChangeCategory from sqlmesh.utils.date import to_timestamp -from sqlmesh.utils.errors import SQLMeshError @pytest.fixture @@ -81,71 +78,6 @@ def test_builtin_evaluator_push(sushi_context: Context, make_snapshot): assert sushi_context.engine_adapter.table_exists(new_view_model_snapshot.table_name()) -def test_airflow_evaluator(sushi_plan: Plan, mocker: MockerFixture): - airflow_client_mock = mocker.Mock() - airflow_client_mock.wait_for_dag_run_completion.return_value = True - airflow_client_mock.wait_for_first_dag_run.return_value = "test_plan_application_dag_run_id" - - evaluatable_plan = sushi_plan.to_evaluatable() - - evaluator = AirflowPlanEvaluator(airflow_client_mock) - evaluator.evaluate(evaluatable_plan) - - airflow_client_mock.apply_plan.assert_called_once_with( - evaluatable_plan, - notification_targets=[], - backfill_concurrent_tasks=1, - ddl_concurrent_tasks=1, - users=[], - ) - - airflow_client_mock.wait_for_dag_run_completion.assert_called_once() - airflow_client_mock.wait_for_first_dag_run.assert_called_once() - - -def test_airflow_evaluator_plan_application_dag_fails(sushi_plan: Plan, mocker: MockerFixture): - airflow_client_mock = mocker.Mock() - airflow_client_mock.wait_for_dag_run_completion.return_value = False - airflow_client_mock.wait_for_first_dag_run.return_value = "test_plan_application_dag_run_id" - - evaluator = AirflowPlanEvaluator(airflow_client_mock) - - with pytest.raises(SQLMeshError): - evaluator.evaluate(sushi_plan.to_evaluatable()) - - airflow_client_mock.apply_plan.assert_called_once() - airflow_client_mock.wait_for_dag_run_completion.assert_called_once() - airflow_client_mock.wait_for_first_dag_run.assert_called_once() - - -def test_mwaa_evaluator(sushi_plan: Plan, mocker: MockerFixture): - mwaa_client_mock = mocker.Mock() - mwaa_client_mock.wait_for_dag_run_completion.return_value = True - mwaa_client_mock.wait_for_first_dag_run.return_value = "test_plan_application_dag_run_id" - mwaa_client_mock.set_variable.return_value = "", "" - - state_sync_mock = mocker.Mock() - - plan_dag_spec_mock = mocker.Mock() - - create_plan_dag_spec_mock = mocker.patch("sqlmesh.schedulers.airflow.plan.create_plan_dag_spec") - create_plan_dag_spec_mock.return_value = plan_dag_spec_mock - - plan_dag_state_mock = mocker.Mock() - mocker.patch( - "sqlmesh.schedulers.airflow.plan.PlanDagState.from_state_sync", - return_value=plan_dag_state_mock, - ) - - evaluator = MWAAPlanEvaluator(mwaa_client_mock, state_sync_mock) - evaluator.evaluate(sushi_plan.to_evaluatable()) - - plan_dag_state_mock.add_dag_spec.assert_called_once_with(plan_dag_spec_mock) - - mwaa_client_mock.wait_for_dag_run_completion.assert_called_once() - mwaa_client_mock.wait_for_first_dag_run.assert_called_once() - - @pytest.mark.parametrize( "change_category", [SnapshotChangeCategory.BREAKING, SnapshotChangeCategory.FORWARD_ONLY] ) @@ -179,40 +111,3 @@ def test_update_intervals_for_new_snapshots( else: assert not snapshot.dev_intervals state_sync_mock.add_snapshots_intervals.assert_not_called() - - -def test_state_based_airflow_evaluator_with_restatements( - sushi_context: Context, mocker: MockerFixture -): - model_fqn = sushi_context.get_model("sushi.waiter_revenue_by_day").fqn - downstream_model_fqn = sushi_context.get_model("sushi.top_waiters").fqn - - plan = PlanBuilder( - sushi_context._context_diff("prod"), - sushi_context.engine_adapter.SCHEMA_DIFFER, - restate_models=[sushi_context.get_model("sushi.waiter_revenue_by_day").fqn], - ).build() - - mwaa_client_mock = mocker.Mock() - mwaa_client_mock.wait_for_dag_run_completion.return_value = True - mwaa_client_mock.wait_for_first_dag_run.return_value = "test_plan_application_dag_run_id" - mwaa_client_mock.set_variable.return_value = "", "" - - state_sync_mock = mocker.Mock() - - plan_dag_spec_mock = mocker.Mock() - - create_plan_dag_spec_mock = mocker.patch("sqlmesh.schedulers.airflow.plan.create_plan_dag_spec") - create_plan_dag_spec_mock.return_value = plan_dag_spec_mock - - plan_dag_state_mock = mocker.Mock() - mocker.patch( - "sqlmesh.schedulers.airflow.plan.PlanDagState.from_state_sync", - return_value=plan_dag_state_mock, - ) - - evaluator = MWAAPlanEvaluator(mwaa_client_mock, state_sync_mock) - evaluator.evaluate(plan.to_evaluatable()) - - plan_application_request = create_plan_dag_spec_mock.call_args[0][0] - assert plan_application_request.plan.restatements.keys() == {model_fqn, downstream_model_fqn} diff --git a/tests/dbt/test_integration.py b/tests/dbt/test_integration.py index 794ed0fbc8..d4e5247b52 100644 --- a/tests/dbt/test_integration.py +++ b/tests/dbt/test_integration.py @@ -137,7 +137,6 @@ def _make_function( f.write( """from pathlib import Path -from sqlmesh.core.config import AirflowSchedulerConfig from sqlmesh.dbt.loader import sqlmesh_config config = sqlmesh_config(Path(__file__).parent) diff --git a/tests/fixtures/dbt/sushi_test/config.py b/tests/fixtures/dbt/sushi_test/config.py index 6f7f94530f..422f53553b 100644 --- a/tests/fixtures/dbt/sushi_test/config.py +++ b/tests/fixtures/dbt/sushi_test/config.py @@ -1,6 +1,6 @@ from pathlib import Path -from sqlmesh.core.config import AirflowSchedulerConfig, ModelDefaultsConfig +from sqlmesh.core.config import ModelDefaultsConfig from sqlmesh.dbt.loader import sqlmesh_config variables = {"start": "Jan 1 2022"} @@ -12,10 +12,3 @@ test_config = config - - -airflow_config = sqlmesh_config( - Path(__file__).parent, - default_scheduler=AirflowSchedulerConfig(), - variables=variables, -) diff --git a/tests/schedulers/airflow/__init__.py b/tests/schedulers/airflow/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/schedulers/airflow/conftest.py b/tests/schedulers/airflow/conftest.py deleted file mode 100644 index 789e543247..0000000000 --- a/tests/schedulers/airflow/conftest.py +++ /dev/null @@ -1,49 +0,0 @@ -import logging -import os - -import pytest -from tenacity import retry, stop_after_attempt, wait_fixed - -from sqlmesh.core.config import AirflowSchedulerConfig -from sqlmesh.schedulers.airflow.client import AirflowClient -from sqlmesh.utils import str_to_bool - -logger = logging.getLogger(__name__) - - -@pytest.fixture(scope="session") -def is_docker() -> bool: - return str_to_bool(os.environ.get("IS_DOCKER")) - - -@pytest.fixture(scope="session") -def airflow_host(is_docker: bool) -> str: - return "airflow-webserver" if is_docker else "localhost" - - -@pytest.fixture(scope="session") -def airflow_scheduler_backend(airflow_host: str) -> AirflowSchedulerConfig: - return _get_airflow_scheduler_backend(airflow_host) - - -@pytest.fixture(scope="session") -def airflow_client(airflow_scheduler_backend: AirflowSchedulerConfig) -> AirflowClient: - return airflow_scheduler_backend.get_client() - - -@retry(wait=wait_fixed(3), stop=stop_after_attempt(10), reraise=True) -def _get_airflow_scheduler_backend(airflow_host: str) -> AirflowSchedulerConfig: - backend = AirflowSchedulerConfig(airflow_url=f"http://{airflow_host}:8080/") - client = backend.get_client() - - try: - client.get_all_dags() - except Exception: - logger.info( - "Failed to fetch the list of DAGs from Airflow. Make sure the test Airflow cluster is running" - ) - raise - - logger.info("The Airflow Client is ready") - - return backend diff --git a/tests/schedulers/airflow/operators/__init__.py b/tests/schedulers/airflow/operators/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/schedulers/airflow/operators/fixtures.py b/tests/schedulers/airflow/operators/fixtures.py deleted file mode 100644 index d77ac00267..0000000000 --- a/tests/schedulers/airflow/operators/fixtures.py +++ /dev/null @@ -1,10 +0,0 @@ -import os -from unittest import mock - -import pytest - - -@pytest.fixture -def set_airflow_as_library(): - with mock.patch.dict(os.environ, {"_AIRFLOW__AS_LIBRARY": "1"}): - yield diff --git a/tests/schedulers/airflow/operators/test_sensor.py b/tests/schedulers/airflow/operators/test_sensor.py deleted file mode 100644 index c13795dbf0..0000000000 --- a/tests/schedulers/airflow/operators/test_sensor.py +++ /dev/null @@ -1,168 +0,0 @@ -from unittest.mock import call - -import pytest -from airflow.utils.context import Context -from pytest_mock.plugin import MockerFixture - -from sqlmesh.core.dialect import parse_one -from sqlmesh.core.model import SqlModel -from sqlmesh.core.snapshot import SnapshotChangeCategory -from sqlmesh.schedulers.airflow.operators.sensor import ( - ExternalSensor, - HighWaterMarkSensor, -) -from sqlmesh.utils.date import to_datetime - -pytest_plugins = ["tests.schedulers.airflow.operators.fixtures"] -pytestmark = pytest.mark.airflow - - -def test_no_current_hwm(mocker: MockerFixture, make_snapshot, random_name, set_airflow_as_library): - this_snapshot = make_snapshot(SqlModel(name="this", query=parse_one("select 1, ds"))) - this_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - - target_snapshot = make_snapshot(SqlModel(name="target", query=parse_one("select 2, ds"))) - target_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - - task = HighWaterMarkSensor( - target_snapshot_info=target_snapshot.table_info, - this_snapshot=this_snapshot, - task_id="test_hwm_task", - ) - - get_snapshots_mock = mocker.patch( - "sqlmesh.core.state_sync.cache.CachingStateSync.get_snapshots" - ) - get_snapshots_mock.return_value = {target_snapshot.snapshot_id: target_snapshot} - - dag_run_mock = mocker.Mock() - dag_run_mock.data_interval_end = to_datetime("2022-01-01") - - context = Context(dag_run=dag_run_mock) # type: ignore - assert not task.poke(context) - - get_snapshots_mock.assert_called_once_with([target_snapshot.table_info]) - - -def test_current_hwm_below_target(mocker: MockerFixture, make_snapshot, set_airflow_as_library): - this_snapshot = make_snapshot( - SqlModel(name="this", query=parse_one("select 1, ds")), version="a" - ) - this_snapshot.change_category = SnapshotChangeCategory.BREAKING - - target_snapshot_v1 = make_snapshot( - SqlModel(name="that", query=parse_one("select 2, ds")), version="b" - ) - target_snapshot_v1.change_category = SnapshotChangeCategory.BREAKING - - target_snapshot_v2 = make_snapshot( - SqlModel(name="that", query=parse_one("select 3, ds")), version="b" - ) - target_snapshot_v2.change_category = SnapshotChangeCategory.FORWARD_ONLY - - target_snapshot_v2.add_interval("2022-01-01", "2022-01-01") - - task = HighWaterMarkSensor( - target_snapshot_info=target_snapshot_v1.table_info, - this_snapshot=this_snapshot, - task_id="test_hwm_task", - ) - - get_snapshots_mock = mocker.patch( - "sqlmesh.core.state_sync.cache.CachingStateSync.get_snapshots" - ) - get_snapshots_mock.return_value = {target_snapshot_v1.snapshot_id: target_snapshot_v1} - - dag_run_mock = mocker.Mock() - dag_run_mock.data_interval_end = to_datetime("2022-01-03") - - context = Context(dag_run=dag_run_mock) # type: ignore - - assert not task.poke(context) - - get_snapshots_mock.assert_called_once_with([target_snapshot_v1.table_info]) - - -def test_current_hwm_above_target(mocker: MockerFixture, make_snapshot, set_airflow_as_library): - this_snapshot = make_snapshot( - SqlModel(name="this", query=parse_one("select 1, ds")), version="a" - ) - this_snapshot.change_category = SnapshotChangeCategory.BREAKING - - target_snapshot_v1 = make_snapshot( - SqlModel(name="that", query=parse_one("select 2, ds")), version="b" - ) - target_snapshot_v1.change_category = SnapshotChangeCategory.BREAKING - target_snapshot_v1.add_interval("2022-01-01", "2022-01-02") - - task = HighWaterMarkSensor( - target_snapshot_info=target_snapshot_v1.table_info, - this_snapshot=this_snapshot, - task_id="test_hwm_task", - ) - - get_snapshots_mock = mocker.patch( - "sqlmesh.core.state_sync.cache.CachingStateSync.get_snapshots" - ) - get_snapshots_mock.return_value = {target_snapshot_v1.snapshot_id: target_snapshot_v1} - - dag_run_mock = mocker.Mock() - dag_run_mock.data_interval_end = to_datetime("2022-01-03") - - context = Context(dag_run=dag_run_mock) # type: ignore - - assert task.poke(context) - - get_snapshots_mock.assert_called_once_with([target_snapshot_v1.table_info]) - - -def test_external_sensor(mocker: MockerFixture, make_snapshot, set_airflow_as_library): - snapshot = make_snapshot( - SqlModel( - name="this", - query=parse_one("select 1"), - signals=[ - ("", {"table_name": "test_table_name_a", "ds": parse_one("@end_ds")}), - ( - "", - { - "table_name": "test_table_name_b", - "ds": parse_one("@end_ds"), - "hour": parse_one("@end_hour"), - }, - ), - ], - ) - ) - - external_sensor_mock = mocker.Mock() - external_sensor_mock.poke.return_value = True - - factory_mock = mocker.Mock() - factory_mock.return_value = external_sensor_mock - - dag_run_mock = mocker.Mock() - dag_run_mock.data_interval_start = to_datetime("2023-01-01") - dag_run_mock.data_interval_end = to_datetime("2023-01-02") - - context = Context(dag_run=dag_run_mock) # type: ignore - - task = ExternalSensor( - snapshot=snapshot, - external_table_sensor_factory=factory_mock, - task_id="test_hwm_task", - ) - assert task.poke(context) - - factory_mock.assert_has_calls( - [ - call({"table_name": "test_table_name_a", "ds": "2023-01-01"}), - call({"table_name": "test_table_name_b", "ds": "2023-01-01", "hour": 23}), - ] - ) - external_sensor_mock.poke.assert_has_calls( - [ - call(context), - call(context), - ] - ) diff --git a/tests/schedulers/airflow/operators/test_targets.py b/tests/schedulers/airflow/operators/test_targets.py deleted file mode 100644 index 2c5436159d..0000000000 --- a/tests/schedulers/airflow/operators/test_targets.py +++ /dev/null @@ -1,216 +0,0 @@ -import typing as t -from unittest.mock import call - -import pytest -from airflow.exceptions import AirflowSkipException -from airflow.utils.context import Context -from pytest_mock.plugin import MockerFixture -from sqlglot import parse_one - -from sqlmesh.core.environment import Environment -from sqlmesh.core.model import Model, Seed, SeedKind, SeedModel, SqlModel -from sqlmesh.core.snapshot import ( - DeployabilityIndex, - SnapshotChangeCategory, - SnapshotTableCleanupTask, -) -from sqlmesh.engines import commands -from sqlmesh.schedulers.airflow.operators import targets -from sqlmesh.utils.date import to_datetime - -pytest_plugins = ["tests.schedulers.airflow.operators.fixtures"] -pytestmark = pytest.mark.airflow - - -@pytest.fixture -def model() -> Model: - return SqlModel( - name="test_model", - query=parse_one("SELECT a, ds FROM tbl"), - ) - - -def test_evaluation_target_execute( - mocker: MockerFixture, make_snapshot: t.Callable, model: Model, set_airflow_as_library -): - interval_ds = to_datetime("2022-01-01") - logical_ds = to_datetime("2022-01-02") - - dag_run_mock = mocker.Mock() - dag_run_mock.data_interval_start = interval_ds - dag_run_mock.data_interval_end = interval_ds - dag_run_mock.logical_date = logical_ds - - context = Context(dag_run=dag_run_mock) # type: ignore - - evaluator_evaluate_mock = mocker.patch( - "sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.evaluate" - ) - evaluator_evaluate_mock.return_value = None - - add_interval_mock = mocker.patch("sqlmesh.core.state_sync.cache.CachingStateSync.add_interval") - - variable_get_mock = mocker.patch("sqlmesh.schedulers.airflow.operators.targets.Variable.get") - - variable_get_mock.return_value = "default_catalog" - - snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - parent_snapshots = {snapshot.name: snapshot} - - deployability_index = DeployabilityIndex.all_deployable() - - target = targets.SnapshotEvaluationTarget( - snapshot=snapshot, - parent_snapshots=parent_snapshots, - deployability_index=deployability_index, - ) - target.execute(context, lambda: mocker.Mock(), "spark") - - add_interval_mock.assert_called_once_with(snapshot, interval_ds, interval_ds, is_dev=False) - - evaluator_evaluate_mock.assert_called_once_with( - snapshot, - start=interval_ds, - end=interval_ds, - execution_time=logical_ds, - snapshots=parent_snapshots, - deployability_index=deployability_index, - batch_index=0, - ) - - -def test_evaluation_target_execute_seed_model( - mocker: MockerFixture, make_snapshot: t.Callable, set_airflow_as_library -): - interval_ds = to_datetime("2022-01-01") - logical_ds = to_datetime("2022-01-02") - - dag_run_mock = mocker.Mock() - dag_run_mock.data_interval_start = interval_ds - dag_run_mock.data_interval_end = interval_ds - dag_run_mock.logical_date = logical_ds - - variable_get_mock = mocker.patch("sqlmesh.schedulers.airflow.operators.targets.Variable.get") - - variable_get_mock.return_value = "default_catalog" - - context = Context(dag_run=dag_run_mock) # type: ignore - - snapshot = make_snapshot( - SeedModel( - name="a", - kind=SeedKind(path="./path/to/seed"), - seed=Seed(content="content"), - column_hashes={"col": "hash1"}, - depends_on=set(), - ).to_dehydrated() - ) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - - evaluator_evaluate_mock = mocker.patch( - "sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.evaluate" - ) - evaluator_evaluate_mock.return_value = None - - add_interval_mock = mocker.patch("sqlmesh.core.state_sync.cache.CachingStateSync.add_interval") - - get_snapshots_mock = mocker.patch( - "sqlmesh.core.state_sync.cache.CachingStateSync.get_snapshots" - ) - get_snapshots_mock.return_value = {snapshot.snapshot_id: snapshot} - - deployability_index = DeployabilityIndex.all_deployable() - - target = targets.SnapshotEvaluationTarget( - snapshot=snapshot, parent_snapshots={}, deployability_index=deployability_index - ) - target.execute(context, lambda: mocker.Mock(), "spark") - - add_interval_mock.assert_called_once_with(snapshot, interval_ds, interval_ds, is_dev=False) - - evaluator_evaluate_mock.assert_called_once_with( - snapshot, - start=interval_ds, - end=interval_ds, - execution_time=logical_ds, - snapshots={snapshot.name: snapshot}, - deployability_index=deployability_index, - batch_index=0, - ) - - -def test_cleanup_target_execute( - mocker: MockerFixture, make_snapshot: t.Callable, model: Model, set_airflow_as_library -): - snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - - environment = Environment( - name="test_env", snapshots=[snapshot.table_info], start_at="", plan_id="test_plan_id" - ) - - cleanup_task = SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=False) - - command = commands.CleanupCommandPayload( - environments=[environment], - tasks=[cleanup_task], - ) - - task_instance_mock = mocker.Mock() - task_instance_mock.xcom_pull.return_value = command.json() - - variable_get_mock = mocker.patch("sqlmesh.schedulers.airflow.operators.targets.Variable.get") - - variable_get_mock.return_value = "default_catalog" - - context = Context(ti=task_instance_mock) # type: ignore - - evaluator_cleanup_mock = mocker.patch( - "sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.cleanup" - ) - - delete_xcom_mock = mocker.patch("sqlmesh.schedulers.airflow.operators.targets._delete_xcom") - - target = targets.SnapshotCleanupTarget() - - evaluator_adapter_mock = mocker.MagicMock() - target.execute(context, lambda: evaluator_adapter_mock, "spark") - - evaluator_adapter_mock.cursor().execute.assert_has_calls( - [call("DROP SCHEMA IF EXISTS `default__test_env` CASCADE")] - ) - evaluator_cleanup_mock.assert_called_once_with([cleanup_task]) - - task_instance_mock.xcom_pull.assert_called_once_with(key="snapshot_cleanup_command") - - delete_xcom_mock.assert_called_once() - - -def test_cleanup_target_skip_execution( - mocker: MockerFixture, make_snapshot: t.Callable, model: Model, set_airflow_as_library -): - snapshot = make_snapshot(model) - snapshot.version = "test_version" - - task_instance_mock = mocker.Mock() - task_instance_mock.xcom_pull.return_value = commands.CleanupCommandPayload( - tasks=[], environments=[] - ).json() - - context = Context(ti=task_instance_mock) # type: ignore - - evaluator_demote_mock = mocker.patch("sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.demote") - evaluator_cleanup_mock = mocker.patch( - "sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.cleanup" - ) - - delete_xcom_mock = mocker.patch("sqlmesh.schedulers.airflow.operators.targets._delete_xcom") - - target = targets.SnapshotCleanupTarget() - with pytest.raises(AirflowSkipException): - target.execute(context, lambda: mocker.Mock(), "spark") - - evaluator_demote_mock.assert_not_called() - evaluator_cleanup_mock.assert_not_called() - delete_xcom_mock.assert_called_once() diff --git a/tests/schedulers/airflow/test_client.py b/tests/schedulers/airflow/test_client.py deleted file mode 100644 index 94ed8cb5e8..0000000000 --- a/tests/schedulers/airflow/test_client.py +++ /dev/null @@ -1,457 +0,0 @@ -import json - -import pytest -import requests -from pytest_mock.plugin import MockerFixture -from sqlglot import parse_one - -from sqlmesh.core.config import EnvironmentSuffixTarget -from sqlmesh.core.environment import Environment -from sqlmesh.core.model import IncrementalByTimeRangeKind, SqlModel -from sqlmesh.core.node import NodeType -from sqlmesh.core.plan import EvaluatablePlan -from sqlmesh.core.snapshot import Snapshot, SnapshotChangeCategory -from sqlmesh.schedulers.airflow import common -from sqlmesh.schedulers.airflow.client import AirflowClient -from sqlmesh.utils.date import to_timestamp - -pytestmark = pytest.mark.airflow - - -@pytest.fixture -def snapshot() -> Snapshot: - snapshot = Snapshot.from_node( - SqlModel( - name="test_model", - kind=IncrementalByTimeRangeKind(time_column="ds", dialect="spark"), - storage_format="parquet", - partitioned_by=["a"], - query=parse_one("SELECT a, ds FROM tbl"), - pre_statements=[ - parse_one("@DEF(key, 'value')"), - ], - dialect="spark", - ), - nodes={}, - ttl="in 1 week", - ) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot.updated_ts = 1665014400000 - snapshot.created_ts = 1665014400000 - return snapshot - - -def test_apply_plan(mocker: MockerFixture, snapshot: Snapshot): - apply_plan_response_mock = mocker.Mock() - apply_plan_response_mock.json.return_value = {"request_id": "test_request_id"} - apply_plan_response_mock.status_code = 200 - apply_plan_mock = mocker.patch("requests.Session.post") - apply_plan_mock.return_value = apply_plan_response_mock - - environment = Environment( - name="test_env", - snapshots=[snapshot.table_info], - start_at="2022-01-01", - end_at="2022-01-01", - plan_id="test_plan_id", - previous_plan_id="previous_plan_id", - promoted_snapshot_ids=[snapshot.snapshot_id], - ) - - plan = EvaluatablePlan( - start=environment.start_at, - end=environment.end_at, - new_snapshots=[snapshot], - environment=environment, - no_gaps=False, - skip_backfill=False, - empty_backfill=False, - restatements={snapshot.name: (to_timestamp("2024-01-01"), to_timestamp("2024-01-02"))}, - is_dev=False, - allow_destructive_models=set(), - forward_only=False, - end_bounded=False, - ensure_finalized_snapshots=False, - directly_modified_snapshots=[snapshot.snapshot_id], - indirectly_modified_snapshots={}, - removed_snapshots=[], - requires_backfill=True, - models_to_backfill={'"test_model"'}, - disabled_restatement_models=set(), - ) - - client = AirflowClient(airflow_url=common.AIRFLOW_LOCAL_URL, session=requests.Session()) - client.apply_plan(plan) - - apply_plan_mock.assert_called_once() - args, data = apply_plan_mock.call_args_list[0] - - assert args[0] == "http://localhost:8080/sqlmesh/api/v1/plans" - assert data["headers"] == {"Content-Type": "application/json"} - assert json.loads(data["data"]) == { - "plan": { - "start": "2022-01-01", - "end": "2022-01-01", - "new_snapshots": [ - { - "created_ts": 1665014400000, - "ttl": "in 1 week", - "fingerprint": snapshot.fingerprint.dict(), - "intervals": [], - "dev_intervals": [], - "dev_table_suffix": "dev", - "pending_restatement_intervals": [], - "node": { - "audits": [], - "audit_definitions": {}, - "clustered_by": [], - "cron": "@daily", - "dialect": "spark", - "pre_statements": ["@DEF(key, 'value')"], - "kind": { - "name": "INCREMENTAL_BY_TIME_RANGE", - "time_column": {"column": "`ds`"}, - "forward_only": False, - "on_destructive_change": "ERROR", - "partition_by_time_column": True, - "disable_restatement": False, - "dialect": "spark", - }, - "mapping_schema": {}, - "name": "test_model", - "partitioned_by": ["`a`"], - "query": "SELECT a, ds FROM tbl", - "references": [], - "project": "", - "python_env": {}, - "storage_format": "parquet", - "jinja_macros": { - "create_builtins_module": "sqlmesh.utils.jinja", - "global_objs": {}, - "packages": {}, - "root_macros": {}, - "top_level_packages": [], - }, - "source_type": "sql", - "tags": [], - "grains": [], - "allow_partials": False, - "signals": [], - "enabled": True, - "extract_dependencies_from_query": True, - }, - "name": '"test_model"', - "parents": [], - "previous_versions": [], - "updated_ts": 1665014400000, - "version": snapshot.version, - "dev_version": snapshot.dev_version, - "change_category": snapshot.change_category, - "migrated": False, - "unrestorable": False, - } - ], - "environment": { - "name": "test_env", - "snapshots": [ - { - "fingerprint": snapshot.fingerprint.dict(), - "name": '"test_model"', - "node_type": NodeType.MODEL, - "previous_versions": [], - "version": snapshot.version, - "dev_version": snapshot.dev_version, - "physical_schema": "sqlmesh__default", - "change_category": snapshot.change_category, - "parents": [], - "kind_name": "INCREMENTAL_BY_TIME_RANGE", - "dev_table_suffix": "dev", - } - ], - "start_at": "2022-01-01", - "end_at": "2022-01-01", - "gateway_managed": False, - "plan_id": "test_plan_id", - "previous_plan_id": "previous_plan_id", - "promoted_snapshot_ids": [ - { - "name": '"test_model"', - "identifier": snapshot.identifier, - } - ], - "suffix_target": "schema", - "normalize_name": True, - "requirements": {}, - }, - "no_gaps": False, - "skip_backfill": False, - "empty_backfill": False, - "is_dev": False, - "forward_only": False, - "allow_destructive_models": [], - "models_to_backfill": ['"test_model"'], - "end_bounded": False, - "ensure_finalized_snapshots": False, - "directly_modified_snapshots": [ - {"identifier": snapshot.identifier, "name": '"test_model"'} - ], - "indirectly_modified_snapshots": {}, - "removed_snapshots": [], - "restatements": { - '"test_model"': [to_timestamp("2024-01-01"), to_timestamp("2024-01-02")] - }, - "requires_backfill": True, - "disabled_restatement_models": [], - }, - "notification_targets": [], - "backfill_concurrent_tasks": 1, - "ddl_concurrent_tasks": 1, - "users": [], - } - - common.PlanApplicationRequest.parse_raw(data["data"]) - - -def test_get_snapshots(mocker: MockerFixture, snapshot: Snapshot): - snapshots = common.SnapshotsResponse(snapshots=[snapshot]) - - get_snapshots_response_mock = mocker.Mock() - get_snapshots_response_mock.status_code = 200 - get_snapshots_response_mock.json.return_value = snapshots.dict() - get_snapshots_mock = mocker.patch("requests.Session.post") - get_snapshots_mock.return_value = get_snapshots_response_mock - - client = AirflowClient(airflow_url=common.AIRFLOW_LOCAL_URL, session=requests.Session()) - result = client.get_snapshots([snapshot.snapshot_id]) - - assert result == [snapshot] - - args, data = get_snapshots_mock.call_args_list[0] - assert args[0] == "http://localhost:8080/sqlmesh/api/v1/snapshots/search" - assert data["headers"] == {"Content-Type": "application/json"} - assert json.loads(data["data"]) == { - "snapshot_ids": [snapshot.snapshot_id.dict()], - "check_existence": False, - } - - -def test_snapshots_exist(mocker: MockerFixture, snapshot: Snapshot): - snapshot_ids = common.SnapshotIdsResponse(snapshot_ids=[snapshot.snapshot_id]) - - snapshots_exist_response_mock = mocker.Mock() - snapshots_exist_response_mock.status_code = 200 - snapshots_exist_response_mock.json.return_value = snapshot_ids.dict() - snapshots_exist_mock = mocker.patch("requests.Session.post") - snapshots_exist_mock.return_value = snapshots_exist_response_mock - - client = AirflowClient(airflow_url=common.AIRFLOW_LOCAL_URL, session=requests.Session()) - result = client.snapshots_exist([snapshot.snapshot_id]) - - assert result == {snapshot.snapshot_id} - - args, data = snapshots_exist_mock.call_args_list[0] - assert args[0] == "http://localhost:8080/sqlmesh/api/v1/snapshots/search" - assert data["headers"] == {"Content-Type": "application/json"} - assert json.loads(data["data"]) == { - "snapshot_ids": [snapshot.snapshot_id.dict()], - "check_existence": True, - } - - -def test_models_exist(mocker: MockerFixture, snapshot: Snapshot): - model_names = ["model_a", "model_b"] - - models_exist_response_mock = mocker.Mock() - models_exist_response_mock.status_code = 200 - models_exist_response_mock.json.return_value = common.ExistingModelsResponse( - names=model_names - ).dict() - models_exist_mock = mocker.patch("requests.Session.get") - models_exist_mock.return_value = models_exist_response_mock - - client = AirflowClient(airflow_url=common.AIRFLOW_LOCAL_URL, session=requests.Session()) - result = client.nodes_exist(model_names, exclude_external=True) - - assert result == set(model_names) - - models_exist_mock.assert_called_once_with( - "http://localhost:8080/sqlmesh/api/v1/models?exclude_external&names=model_a%2Cmodel_b" - ) - - -def test_get_environment(mocker: MockerFixture, snapshot: Snapshot): - environment = Environment( - name="test", - snapshots=[snapshot.table_info], - start_at="2022-01-01", - end_at="2022-01-01", - plan_id="test_plan_id", - previous_plan_id=None, - suffix_target=EnvironmentSuffixTarget.TABLE, - ) - - get_environment_response_mock = mocker.Mock() - get_environment_response_mock.status_code = 200 - get_environment_response_mock.json.return_value = environment.dict() - get_environment_mock = mocker.patch("requests.Session.get") - get_environment_mock.return_value = get_environment_response_mock - - client = AirflowClient(airflow_url=common.AIRFLOW_LOCAL_URL, session=requests.Session()) - result = client.get_environment("dev") - - assert result is not None - assert result.dict() == environment.dict() - - get_environment_mock.assert_called_once_with( - "http://localhost:8080/sqlmesh/api/v1/environments/dev" - ) - - -def test_get_environments(mocker: MockerFixture, snapshot: Snapshot): - environment = Environment( - name="test", - snapshots=[snapshot.table_info], - start_at="2022-01-01", - end_at="2022-01-01", - plan_id="test_plan_id", - previous_plan_id=None, - ) - environments = common.EnvironmentsResponse(environments=[environment]) - - get_environments_response_mock = mocker.Mock() - get_environments_response_mock.status_code = 200 - get_environments_response_mock.json.return_value = environments.dict() - get_environments_mock = mocker.patch("requests.Session.get") - get_environments_mock.return_value = get_environments_response_mock - - client = AirflowClient(airflow_url=common.AIRFLOW_LOCAL_URL, session=requests.Session()) - result = client.get_environments() - - assert len(result) == 1 - assert result[0].dict() == environment.dict() - - get_environments_mock.assert_called_once_with( - "http://localhost:8080/sqlmesh/api/v1/environments" - ) - - -@pytest.mark.parametrize("ensure_finalized_snapshots", [True, False]) -def test_max_interval_end_per_model( - mocker: MockerFixture, snapshot: Snapshot, ensure_finalized_snapshots: bool -): - response = common.IntervalEndResponse( - environment="test_environment", - interval_end_per_model={"model_name": to_timestamp("2023-01-01")}, - ) - - max_interval_end_response_mock = mocker.Mock() - max_interval_end_response_mock.status_code = 200 - max_interval_end_response_mock.json.return_value = response.dict() - max_interval_end_mock = mocker.patch("requests.Session.post") - max_interval_end_mock.return_value = max_interval_end_response_mock - - client = AirflowClient(airflow_url=common.AIRFLOW_LOCAL_URL, session=requests.Session()) - result = client.max_interval_end_per_model( - "test_environment", {"a.b.c"}, ensure_finalized_snapshots - ) - - assert result == response.interval_end_per_model - - args, data = max_interval_end_mock.call_args_list[0] - assert ( - args[0] - == "http://localhost:8080/sqlmesh/api/v1/environments/test_environment/max_interval_end_per_model" - ) - assert data["headers"] == {"Content-Type": "application/json"} - assert json.loads(data["data"]) == { - "models": ["a.b.c"], - "ensure_finalized_snapshots": ensure_finalized_snapshots, - } - - -def test_max_interval_end_per_model_no_models(mocker: MockerFixture, snapshot: Snapshot): - response = common.IntervalEndResponse( - environment="test_environment", - interval_end_per_model={"model_name": to_timestamp("2023-01-01")}, - ) - - max_interval_end_response_mock = mocker.Mock() - max_interval_end_response_mock.status_code = 200 - max_interval_end_response_mock.json.return_value = response.dict() - max_interval_end_mock = mocker.patch("requests.Session.post") - max_interval_end_mock.return_value = max_interval_end_response_mock - - client = AirflowClient(airflow_url=common.AIRFLOW_LOCAL_URL, session=requests.Session()) - result = client.max_interval_end_per_model("test_environment", None, False) - - assert result == response.interval_end_per_model - - args, data = max_interval_end_mock.call_args_list[0] - assert ( - args[0] - == "http://localhost:8080/sqlmesh/api/v1/environments/test_environment/max_interval_end_per_model" - ) - assert data["headers"] == {"Content-Type": "application/json"} - assert json.loads(data["data"]) == { - "ensure_finalized_snapshots": False, - } - - -def test_get_dag_run_state(mocker: MockerFixture): - get_dag_run_state_mock = mocker.Mock() - get_dag_run_state_mock.status_code = 200 - get_dag_run_state_mock.json.return_value = {"state": "failed"} - get_snapshot_mock = mocker.patch("requests.Session.get") - get_snapshot_mock.return_value = get_dag_run_state_mock - - client = AirflowClient(airflow_url=common.AIRFLOW_LOCAL_URL, session=requests.Session()) - result = client.get_dag_run_state("test_dag_id", "test_dag_run_id") - - assert result == "failed" - - get_snapshot_mock.assert_called_once_with( - "http://localhost:8080/api/v1/dags/test_dag_id/dagRuns/test_dag_run_id" - ) - - -def test_invalidat_environment(mocker: MockerFixture): - delete_environment_response_mock = mocker.Mock() - delete_environment_response_mock.status_code = 200 - delete_environment_response_mock.json.return_value = {"name": "test_environment"} - delete_environment_mock = mocker.patch("requests.Session.delete") - delete_environment_mock.return_value = delete_environment_response_mock - - client = AirflowClient(airflow_url=common.AIRFLOW_LOCAL_URL, session=requests.Session()) - client.invalidate_environment("test_environment") - - delete_environment_mock.assert_called_once_with( - "http://localhost:8080/sqlmesh/api/v1/environments/test_environment" - ) - - -def test_get_variable(mocker: MockerFixture): - get_variable_response_mock = mocker.Mock() - get_variable_response_mock.status_code = 200 - get_variable_response_mock.json.return_value = {"value": "test_value", "key": "test_key"} - get_variable_mock = mocker.patch("requests.Session.get") - get_variable_mock.return_value = get_variable_response_mock - - client = AirflowClient(airflow_url=common.AIRFLOW_LOCAL_URL, session=requests.Session()) - assert client.get_variable("test_key") == "test_value" - - get_variable_mock.assert_called_once_with("http://localhost:8080/api/v1/variables/test_key") - - -def test_url_no_trailing_slash(mocker: MockerFixture, snapshot: Snapshot): - get_variable_response_mock = mocker.Mock() - get_variable_response_mock.status_code = 200 - get_variable_response_mock.json.return_value = {"value": "test_value", "key": "test_key"} - get_variable_mock = mocker.patch("requests.Session.get") - get_variable_mock.return_value = get_variable_response_mock - - client = AirflowClient(airflow_url="http://localhost:8080/prefix", session=requests.Session()) - client.get_variable("test_key") - - get_variable_mock.assert_called_once_with( - "http://localhost:8080/prefix/api/v1/variables/test_key" - ) diff --git a/tests/schedulers/airflow/test_common.py b/tests/schedulers/airflow/test_common.py deleted file mode 100644 index f33bb500c5..0000000000 --- a/tests/schedulers/airflow/test_common.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -import pytest - -from sqlmesh.schedulers.airflow import common - -pytestmark = pytest.mark.airflow - - -def test_snapshot_dag_id(): - assert ( - common.dag_id_for_name_version('"test_schema"."test_table"', "version") - == "sqlmesh_snapshot__test_schema___test_table__version_dag" - ) diff --git a/tests/schedulers/airflow/test_dag_generator.py b/tests/schedulers/airflow/test_dag_generator.py deleted file mode 100644 index c9a410117a..0000000000 --- a/tests/schedulers/airflow/test_dag_generator.py +++ /dev/null @@ -1,175 +0,0 @@ -import typing as t -from pytest_mock.plugin import MockerFixture - -from sqlglot import parse_one -from airflow.models import BaseOperator -from airflow.utils.context import Context - -from sqlmesh.core.config import EnvironmentSuffixTarget -from sqlmesh.core.environment import Environment -from sqlmesh.core.model import IncrementalByUniqueKeyKind, SqlModel -from sqlmesh.core.snapshot import ( - Snapshot, - SnapshotChangeCategory, -) -from sqlmesh.schedulers.airflow.dag_generator import SnapshotDagGenerator -from sqlmesh.schedulers.airflow import common -from sqlmesh.schedulers.airflow.operators.targets import BaseTarget, SnapshotEvaluationTarget -from sqlmesh.schedulers.airflow.operators.sensor import HighWaterMarkSensor -from sqlmesh.utils.date import to_datetime, to_timestamp - - -class TestSubmitOperator(BaseOperator): - __test__ = False # prevent pytest trying to collect this as a test class - - def __init__( - self, - *, - target: BaseTarget, - **kwargs: t.Any, - ) -> None: - super().__init__(**kwargs) - self.target = target - - -def test_generate_plan_application_dag__batch_index_populated(mocker: MockerFixture, make_snapshot): - model = SqlModel( - name="test_model", - kind=IncrementalByUniqueKeyKind(unique_key="item_id", batch_size=1), - cron="@daily", - start="2020-01-01", - end="2020-01-07", - storage_format="ICEBERG", - query=parse_one(""" - SELECT item_id::int AS item_id, event_date::date AS event_date - FROM ( - VALUES - (2, '2020-01-01'), - (1, '2020-01-01'), - (3, '2020-01-03'), - (1, '2020-01-04'), - (1, '2020-01-05'), - (1, '2020-01-06'), - (1, '2020-01-07') - ) AS t(item_id, event_date) - WHERE event_date BETWEEN @start_date AND @end_date - """), - ) - - snapshot: Snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - - state_reader_mock = mocker.Mock() - state_reader_mock.get_snapshots.return_value = {} - - generator = SnapshotDagGenerator( - engine_operator=TestSubmitOperator, - engine_operator_args={}, - ddl_engine_operator=TestSubmitOperator, - ddl_engine_operator_args={}, - external_table_sensor_factory=None, - sensor_mode="reschedule", - state_reader=state_reader_mock, - external_sensor_args=None, - high_water_mark_sensor_args=None, - ) - - environment_name = "test_env" - new_environment = Environment( - name=environment_name, - snapshots=[], - start_at="2020-01-01", - end_at="2020-01-10", - plan_id="test_plan_id", - suffix_target=EnvironmentSuffixTarget.TABLE, - catalog_name_override="test_catalog", - ) - - dag_plan = common.PlanDagSpec( - request_id="test_request_id", - environment=new_environment, - new_snapshots=[snapshot], - backfill_intervals_per_snapshot=[ - common.BackfillIntervalsPerSnapshot( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_datetime("2020-01-01"), to_datetime("2020-01-02")), - (to_datetime("2020-01-02"), to_datetime("2020-01-03")), - (to_datetime("2020-01-03"), to_datetime("2020-01-04")), - ], - ) - ], - demoted_snapshots=[], - no_gaps=True, - notification_targets=[], - backfill_concurrent_tasks=1, - ddl_concurrent_tasks=1, - users=[], - is_dev=False, - allow_destructive_snapshots=set(), - execution_time=to_datetime("2024-01-01"), - ) - - dag = generator.generate_plan_application_dag(dag_plan) - assert dag is not None - - backfill_tasks = [ - t - for t in dag.tasks - if "backfill__test_model" in t.task_id - and not t.task_id.endswith("__start") - and not t.task_id.endswith("__end") - ] - assert len(backfill_tasks) == 3 - - for batch_idx, task in enumerate(backfill_tasks): - target: SnapshotEvaluationTarget = task.target # type: ignore - assert target is not None - command = target._get_command_payload(context=t.cast(Context, None)) - assert command is not None - assert target.batch_index == batch_idx - assert command.batch_index == batch_idx - - -def test_sensor_mode_override(mocker: MockerFixture, make_snapshot): - snapshot_a = make_snapshot( - SqlModel(name="a", kind=dict(name="FULL"), query=parse_one("select 1 as a, ds")), - ) - snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot_a.unpaused_ts = to_timestamp("2024-01-01") - - snapshot_b = make_snapshot( - SqlModel(name="b", kind=dict(name="FULL"), query=parse_one("select a, ds from a")), - nodes={snapshot_a.name: snapshot_a.node}, - ) - snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot_b.unpaused_ts = to_timestamp("2024-01-01") - - state_reader_mock = mocker.Mock() - state_reader_mock.get_snapshots.return_value = { - snapshot_a.snapshot_id: snapshot_a, - snapshot_b.snapshot_id: snapshot_b, - } - - generator = SnapshotDagGenerator( - engine_operator=TestSubmitOperator, - engine_operator_args={}, - ddl_engine_operator=TestSubmitOperator, - ddl_engine_operator_args={}, - external_table_sensor_factory=None, - sensor_mode="poke", - state_reader=state_reader_mock, - external_sensor_args=None, - high_water_mark_sensor_args=None, - ) - - dags = generator.generate_cadence_dags([snapshot_a, snapshot_b]) - assert len(dags) == 2 - - assert len(dags[0].tasks) == 1 - assert isinstance(dags[0].tasks[0], TestSubmitOperator) - - assert len(dags[1].tasks) == 2 - assert isinstance(dags[1].tasks[0], HighWaterMarkSensor) - assert isinstance(dags[1].tasks[1], TestSubmitOperator) - assert dags[1].tasks[0].mode == "poke" diff --git a/tests/schedulers/airflow/test_end_to_end.py b/tests/schedulers/airflow/test_end_to_end.py deleted file mode 100644 index 21ef821d0d..0000000000 --- a/tests/schedulers/airflow/test_end_to_end.py +++ /dev/null @@ -1,59 +0,0 @@ -from datetime import timedelta - -import pytest -from pytest_mock.plugin import MockerFixture -from tenacity import retry, stop_after_attempt, wait_fixed - -from sqlmesh.core.context import Context -from sqlmesh.schedulers.airflow.client import AirflowClient -from sqlmesh.utils.date import now, to_date -from tests.conftest import SushiDataValidator - -pytestmark = [ - pytest.mark.airflow, - pytest.mark.docker, -] - - -@pytest.fixture(autouse=True) -def wait_for_airflow(airflow_client: AirflowClient): - @retry(wait=wait_fixed(2), stop=stop_after_attempt(15), reraise=True) - def get_receiver_dag() -> None: - airflow_client.get_janitor_dag() - - get_receiver_dag() - - -def test_sushi(mocker: MockerFixture, is_docker: bool): - end = now() - start = to_date(end - timedelta(days=7)) - yesterday = to_date(end - timedelta(days=1)) - - airflow_config = "airflow_config_docker" if is_docker else "airflow_config" - context = Context(paths="./examples/sushi", config=airflow_config) - assert context.default_catalog == "spark_catalog" - for fqn in context.models: - assert fqn.startswith('"spark_catalog"."') - data_validator = SushiDataValidator.from_context(context) - - context.plan( - environment="test_dev", - start=start, - end=end, - skip_tests=True, - no_prompts=True, - auto_apply=True, - ) - - data_validator.validate( - "sushi.customer_revenue_lifetime", start, yesterday, env_name="test_dev" - ) - - # Ensure that the plan has been applied successfully. - no_change_plan = context.plan_builder( - environment="test_dev_two", - start=start, - end=end, - skip_tests=True, - ).build() - assert not no_change_plan.requires_backfill diff --git a/tests/schedulers/airflow/test_integration.py b/tests/schedulers/airflow/test_integration.py deleted file mode 100644 index 2bd6c2cb14..0000000000 --- a/tests/schedulers/airflow/test_integration.py +++ /dev/null @@ -1,154 +0,0 @@ -import typing as t -from datetime import timedelta - -import pytest -from sqlglot import parse_one -from tenacity import retry, stop_after_attempt, wait_fixed - -from sqlmesh.core import constants as c -from sqlmesh.core.environment import Environment -from sqlmesh.core.model import IncrementalByTimeRangeKind, Model, SqlModel -from sqlmesh.core.plan import EvaluatablePlan -from sqlmesh.core.snapshot import Snapshot, SnapshotChangeCategory -from sqlmesh.schedulers.airflow import common -from sqlmesh.schedulers.airflow.client import AirflowClient -from sqlmesh.utils import random_id -from sqlmesh.utils.date import yesterday, now -from sqlmesh.utils.errors import SQLMeshError - -pytestmark = [ - pytest.mark.airflow, - pytest.mark.docker, -] - - -DAG_CREATION_WAIT_INTERVAL = 3 -DAG_CREATION_RETRY_ATTEMPTS = 5 -DAG_RUN_POLL_INTERVAL = 1 - - -def test_system_dags(airflow_client: AirflowClient): - @retry(wait=wait_fixed(2), stop=stop_after_attempt(15), reraise=True) - def get_system_dags() -> t.List[t.Dict[str, t.Any]]: - return [ - airflow_client.get_janitor_dag(), - ] - - system_dags = get_system_dags() - assert all(d["is_active"] for d in system_dags) - - -def test_apply_plan_create_backfill_promote( - airflow_client: AirflowClient, make_snapshot, random_name -): - model_name = random_name() - snapshot = make_snapshot(_create_model(model_name)) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - - environment_name = _random_environment_name() - environment = _create_environment(snapshot, name=environment_name) - environment.start_at = yesterday() - timedelta(days=1) - environment.end_at = None - - assert airflow_client.get_variable(common.DEFAULT_CATALOG_VARIABLE_NAME) == "spark_catalog" - - assert airflow_client.get_environment(environment_name) is None - - _apply_plan_and_block(airflow_client, [snapshot], environment, is_dev=False) - - assert airflow_client.get_environment(environment_name).snapshots == [ # type: ignore - snapshot.table_info - ] - - # Make sure that the same Snapshot can't be added again. - with pytest.raises(SQLMeshError, match=r"Snapshots.*already exist.*"): - airflow_client.apply_plan(_create_evaluatable_plan([snapshot], environment)) - - # Verify full environment demotion. - environment.snapshots_ = [] - environment.previous_plan_id = environment.plan_id - environment.plan_id = "new_plan_id" - _apply_plan_and_block(airflow_client, [], environment) - assert not airflow_client.get_environment(environment_name).snapshots # type: ignore - - -def _apply_plan_and_block( - airflow_client: AirflowClient, - new_snapshots: t.List[Snapshot], - environment: Environment, - is_dev: t.Optional[bool] = None, -) -> None: - plan = _create_evaluatable_plan(new_snapshots, environment, is_dev) - airflow_client.apply_plan(plan) - - plan_application_dag_id = common.plan_application_dag_id(environment.name, plan.plan_id) - plan_application_dag_run_id = airflow_client.wait_for_first_dag_run( - plan_application_dag_id, DAG_CREATION_WAIT_INTERVAL, DAG_CREATION_RETRY_ATTEMPTS - ) - assert airflow_client.wait_for_dag_run_completion( - plan_application_dag_id, plan_application_dag_run_id, DAG_RUN_POLL_INTERVAL - ) - - -def _create_evaluatable_plan( - new_snapshots: t.List[Snapshot], - environment: Environment, - is_dev: t.Optional[bool] = None, -) -> EvaluatablePlan: - if is_dev is None: - is_dev = environment.name != c.PROD - return EvaluatablePlan( - start=environment.start_at, - end=environment.end_at or now(), - new_snapshots=new_snapshots, - environment=environment, - no_gaps=False, - skip_backfill=False, - empty_backfill=False, - restatements={}, - is_dev=is_dev, - allow_destructive_models=set(), - forward_only=False, - end_bounded=True, - ensure_finalized_snapshots=False, - directly_modified_snapshots=[], - indirectly_modified_snapshots={}, - removed_snapshots=[], - requires_backfill=True, - disabled_restatement_models=set(), - ) - - -@retry(wait=wait_fixed(3), stop=stop_after_attempt(5), reraise=True) -def _get_snapshot_dag( - airflow_client: AirflowClient, model_name: str, version: str -) -> t.Dict[str, t.Any]: - return airflow_client.get_snapshot_dag(model_name, version) - - -def _create_model(name: str) -> Model: - return SqlModel( - name=name, - kind=IncrementalByTimeRangeKind(time_column="ds", batch_size=30), - description="Dummy table", - owner="jen", - cron="@daily", - start="2020-01-01", - partitioned_by=["ds"], - query=parse_one("SELECT '2022-01-01'::TEXT AS ds, 1::INT AS one"), - ) - - -def _create_environment(snapshot: Snapshot, name: t.Optional[str] = None) -> Environment: - return Environment( - name=name or _random_environment_name(), - snapshots=[snapshot.table_info], - start_at="2022-01-01", - end_at="2022-01-01", - plan_id="test_plan_id", - previous_plan_id=None, - ) - - -def _random_environment_name() -> str: - return f"test_environment_{random_id()[-8:]}" diff --git a/tests/schedulers/airflow/test_mwaa_client.py b/tests/schedulers/airflow/test_mwaa_client.py deleted file mode 100644 index a551fe0a78..0000000000 --- a/tests/schedulers/airflow/test_mwaa_client.py +++ /dev/null @@ -1,159 +0,0 @@ -import base64 -import json - -import pytest -from pytest_mock.plugin import MockerFixture - -from sqlmesh.schedulers.airflow.mwaa_client import MWAAClient - -pytestmark = pytest.mark.airflow - - -def test_get_first_dag_run_id(mocker: MockerFixture): - list_runs_response_mock = mocker.Mock() - list_runs_response_mock.json.return_value = { - "stdout": _encode_output(json.dumps([{"run_id": "test_run_id", "state": "success"}])), - "stderr": "", - } - list_runs_response_mock.status_code = 200 - list_runs_mock = mocker.patch("requests.Session.post") - list_runs_mock.return_value = list_runs_response_mock - - url_and_auth_token_mock = mocker.patch( - "sqlmesh.schedulers.airflow.mwaa_client.url_and_auth_token_for_environment" - ) - url_and_auth_token_mock.return_value = ("https://test_airflow_host", "test_token") - - client = MWAAClient("test_environment") - - assert client.get_first_dag_run_id("test_dag_id") == "test_run_id" - - list_runs_mock.assert_called_once_with( - "https://test_airflow_host/aws_mwaa/cli", - data="dags list-runs -o json -d test_dag_id", - ) - url_and_auth_token_mock.assert_called_once_with("test_environment") - - -def test_get_dag_run_state(mocker: MockerFixture): - list_runs_response_mock = mocker.Mock() - list_runs_response_mock.json.return_value = { - "stdout": _encode_output( - json.dumps( - [ - {"run_id": "test_run_id_a", "state": "success"}, - {"run_id": "test_run_id_b", "state": "failed"}, - ] - ) - ), - "stderr": "", - } - list_runs_response_mock.status_code = 200 - list_runs_mock = mocker.patch("requests.Session.post") - list_runs_mock.return_value = list_runs_response_mock - - url_and_auth_token_mock = mocker.patch( - "sqlmesh.schedulers.airflow.mwaa_client.url_and_auth_token_for_environment" - ) - url_and_auth_token_mock.return_value = ("https://test_airflow_host", "test_token") - - client = MWAAClient("test_environment") - - assert client.get_dag_run_state("test_dag_id", "test_run_id_b") == "failed" - - list_runs_mock.assert_called_once_with( - "https://test_airflow_host/aws_mwaa/cli", - data="dags list-runs -o json -d test_dag_id", - ) - url_and_auth_token_mock.assert_called_once_with("test_environment") - - -def test_get_variable(mocker: MockerFixture): - get_variable_response_mock = mocker.Mock() - get_variable_response_mock.json.return_value = { - "stdout": _encode_output("test_value"), - "stderr": "", - } - get_variable_response_mock.status_code = 200 - get_variable_mock = mocker.patch("requests.Session.post") - get_variable_mock.return_value = get_variable_response_mock - - url_and_auth_token_mock = mocker.patch( - "sqlmesh.schedulers.airflow.mwaa_client.url_and_auth_token_for_environment" - ) - url_and_auth_token_mock.return_value = ("https://test_airflow_host", "test_token") - - client = MWAAClient("test_environment") - - assert client.get_variable("test_key") == "test_value" - - get_variable_mock.assert_called_once_with( - "https://test_airflow_host/aws_mwaa/cli", - data="variables get test_key", - ) - url_and_auth_token_mock.assert_called_once_with("test_environment") - - -def test_get_variable_not_found(mocker: MockerFixture): - get_variable_response_mock = mocker.Mock() - get_variable_response_mock.json.return_value = { - "stdout": "", - "stderr": _encode_output("Variable test_key does not exist"), - } - get_variable_response_mock.status_code = 200 - get_variable_mock = mocker.patch("requests.Session.post") - get_variable_mock.return_value = get_variable_response_mock - - url_and_auth_token_mock = mocker.patch( - "sqlmesh.schedulers.airflow.mwaa_client.url_and_auth_token_for_environment" - ) - url_and_auth_token_mock.return_value = ("https://test_airflow_host", "test_token") - - client = MWAAClient("test_environment") - - assert client.get_variable("test_key") is None - - get_variable_mock.assert_called_once_with( - "https://test_airflow_host/aws_mwaa/cli", - data="variables get test_key", - ) - - -def test_token_refresh(mocker: MockerFixture): - list_runs_response_mock = mocker.Mock() - list_runs_response_mock.json.return_value = { - "stdout": _encode_output(json.dumps([{"run_id": "test_run_id", "state": "success"}])), - "stderr": "", - } - list_runs_response_mock.status_code = 200 - list_runs_mock = mocker.patch("requests.Session.post") - list_runs_mock.return_value = list_runs_response_mock - - url_and_auth_token_mock = mocker.patch( - "sqlmesh.schedulers.airflow.mwaa_client.url_and_auth_token_for_environment" - ) - url_and_auth_token_mock.return_value = ("https://test_airflow_host", "test_token") - - now_mock = mocker.patch("sqlmesh.schedulers.airflow.mwaa_client.now_timestamp") - now_mock.return_value = 0 - - client = MWAAClient("test_environment") - client.get_first_dag_run_id("test_dag_id") - - now_mock.return_value = 15000 # 15 seconds later - client.get_first_dag_run_id("test_dag_id") - - now_mock.return_value = 31000 # 31 seconds later - client.get_first_dag_run_id("test_dag_id") - - now_mock.return_value = 45000 # 45 seconds later - client.get_first_dag_run_id("test_dag_id") - - now_mock.return_value = 63000 # 63 seconds later - client.get_first_dag_run_id("test_dag_id") - - assert url_and_auth_token_mock.call_count == 3 - - -def _encode_output(out: str) -> str: - return base64.b64encode(out.encode("utf-8")).decode("utf-8") diff --git a/tests/schedulers/airflow/test_plan.py b/tests/schedulers/airflow/test_plan.py deleted file mode 100644 index 11b409cc67..0000000000 --- a/tests/schedulers/airflow/test_plan.py +++ /dev/null @@ -1,617 +0,0 @@ -import typing as t -from datetime import datetime -from unittest import mock - -import pytest -from _pytest.fixtures import FixtureRequest -from _pytest.monkeypatch import MonkeyPatch -from pytest_mock.plugin import MockerFixture -from sqlglot import parse_one - -from sqlmesh.core.config import EnvironmentSuffixTarget -from sqlmesh.core.context import Context -from sqlmesh.core.environment import Environment -from sqlmesh.core.model import ( - IncrementalByTimeRangeKind, - ModelKindName, - create_sql_model, -) -from sqlmesh.core.node import NodeType -from sqlmesh.core.plan.definition import EvaluatablePlan -from sqlmesh.core.snapshot import ( - DeployabilityIndex, - Snapshot, - SnapshotChangeCategory, - SnapshotFingerprint, - SnapshotTableInfo, -) -from sqlmesh.schedulers.airflow import common -from sqlmesh.schedulers.airflow.plan import PlanDagState, create_plan_dag_spec -from sqlmesh.utils.date import to_datetime, to_timestamp, now -from sqlmesh.utils.errors import SQLMeshError - -pytestmark = pytest.mark.airflow - - -@pytest.fixture -def snapshot(make_snapshot, random_name) -> Snapshot: - result = make_snapshot( - create_sql_model( - random_name(), - parse_one("SELECT 1, ds"), - kind=IncrementalByTimeRangeKind(time_column="ds"), - start="2022-01-01", - ), - ) - result.categorize_as(SnapshotChangeCategory.BREAKING) - return result - - -@pytest.fixture -def depends_on_self_snapshot(make_snapshot, random_name) -> Snapshot: - name = random_name() - result = make_snapshot( - create_sql_model( - name, - parse_one(f"SELECT 1, ds FROM {name}"), - kind=IncrementalByTimeRangeKind(time_column="ds", batch_size=1), - start="2022-01-01", - ), - ) - result.categorize_as(SnapshotChangeCategory.BREAKING) - return result - - -@pytest.mark.parametrize( - "snapshot_fixture, expected_intervals, paused_forward_only", - [ - ("snapshot", [(to_datetime("2022-01-01"), to_datetime("2022-01-05"))], False), - ("snapshot", [(to_datetime("2022-01-01"), to_datetime("2022-01-05"))], True), - ( - "depends_on_self_snapshot", - [ - (to_datetime("2022-01-01"), to_datetime("2022-01-02")), - (to_datetime("2022-01-02"), to_datetime("2022-01-03")), - (to_datetime("2022-01-03"), to_datetime("2022-01-04")), - (to_datetime("2022-01-04"), to_datetime("2022-01-05")), - ], - False, - ), - ], -) -def test_create_plan_dag_spec( - mocker: MockerFixture, - snapshot_fixture: str, - expected_intervals: t.List[t.Tuple[datetime, datetime]], - paused_forward_only: bool, - random_name, - request: FixtureRequest, -): - the_snapshot = request.getfixturevalue(snapshot_fixture) - the_snapshot.categorize_as( - SnapshotChangeCategory.FORWARD_ONLY - if paused_forward_only - else SnapshotChangeCategory.BREAKING - ) - - environment_name = random_name() - new_environment = Environment( - name=environment_name, - snapshots=[the_snapshot.table_info], - start_at="2022-01-01", - end_at="2022-01-04", - plan_id="test_plan_id", - suffix_target=EnvironmentSuffixTarget.TABLE, - catalog_name_override="test_catalog", - ) - - plan = EvaluatablePlan( - start=new_environment.start_at, - end=new_environment.end_at, - new_snapshots=[the_snapshot], - environment=new_environment, - no_gaps=True, - skip_backfill=False, - empty_backfill=False, - restatements={}, - is_dev=False, - forward_only=True, - models_to_backfill=None, - end_bounded=False, - ensure_finalized_snapshots=False, - directly_modified_snapshots=[the_snapshot.snapshot_id], - indirectly_modified_snapshots={}, - removed_snapshots=[], - interval_end_per_model=None, - allow_destructive_models=set(), - requires_backfill=True, - disabled_restatement_models=set(), - ) - - plan_request = common.PlanApplicationRequest( - plan=plan, - notification_targets=[], - backfill_concurrent_tasks=1, - ddl_concurrent_tasks=1, - users=[], - ) - - deleted_snapshot = SnapshotTableInfo( - name="test_schema.deleted_model", - fingerprint=SnapshotFingerprint(data_hash="1", metadata_hash="1"), - version="test_version", - physical_schema="test_physical_schema", - parents=[], - change_category=SnapshotChangeCategory.BREAKING, - kind_name=ModelKindName.FULL, - node_type=NodeType.MODEL, - dev_table_suffix="dev", - ) - old_environment = Environment( - name=environment_name, - snapshots=[deleted_snapshot], - start_at="2022-01-01", - end_at="2022-01-01", - plan_id="test_plan_id", - suffix_target=EnvironmentSuffixTarget.SCHEMA, - ) - - state_sync_mock = mocker.Mock() - state_sync_mock.get_snapshots.return_value = {} - state_sync_mock.get_environment.return_value = old_environment - state_sync_mock.get_snapshot_intervals.return_value = [] - state_sync_mock.refresh_snapshot_intervals.return_value = [] - - expected_no_gaps_snapshot_names = {the_snapshot.name} if not paused_forward_only else set() - - with mock.patch( - "sqlmesh.schedulers.airflow.plan.now", - side_effect=lambda: to_datetime("2023-01-01"), - ): - plan_spec = create_plan_dag_spec(plan_request, state_sync_mock) - - assert plan_spec == common.PlanDagSpec( - request_id="test_plan_id", - environment=new_environment, - new_snapshots=[the_snapshot], - backfill_intervals_per_snapshot=[ - common.BackfillIntervalsPerSnapshot( - snapshot_id=the_snapshot.snapshot_id, - intervals=expected_intervals, - before_promote=not paused_forward_only, - ) - ], - demoted_snapshots=[deleted_snapshot], - unpaused_dt="2022-01-04", - no_gaps=True, - notification_targets=[], - backfill_concurrent_tasks=1, - ddl_concurrent_tasks=1, - users=[], - is_dev=False, - allow_destructive_snapshots=set(), - forward_only=True, - dag_start_ts=to_timestamp("2023-01-01"), - no_gaps_snapshot_names=expected_no_gaps_snapshot_names, - deployability_index_for_creation=( - DeployabilityIndex.all_deployable() - if not paused_forward_only - else DeployabilityIndex.none_deployable() - ), - directly_modified_snapshots=[the_snapshot.snapshot_id], - indirectly_modified_snapshots={}, - removed_snapshots=[], - ) - - state_sync_mock.get_snapshots.assert_called_once() - state_sync_mock.get_environment.assert_called_once() - state_sync_mock.refresh_snapshot_intervals.assert_called_once() - list(state_sync_mock.refresh_snapshot_intervals.call_args_list[0][0][0]) == [the_snapshot] - - -@pytest.mark.parametrize( - "snapshot_fixture, expected_intervals", - [ - ( - "snapshot", - [(to_datetime("2022-01-02"), to_datetime("2022-01-04"))], - ), - ( - "depends_on_self_snapshot", - [ - (to_datetime("2022-01-02"), to_datetime("2022-01-03")), - (to_datetime("2022-01-03"), to_datetime("2022-01-04")), - ], - ), - ], -) -def test_restatement( - mocker: MockerFixture, - monkeypatch: MonkeyPatch, - snapshot_fixture: str, - expected_intervals: t.List[t.Tuple[datetime, datetime]], - random_name, - request: FixtureRequest, -): - the_snapshot = request.getfixturevalue(snapshot_fixture) - environment_name = random_name() - new_environment = Environment( - name=environment_name, - snapshots=[the_snapshot.table_info], - start_at="2022-01-01", - end_at="2022-01-07", - plan_id="test_plan_id", - ) - - the_snapshot.add_interval("2022-01-01", "2022-01-07") - - plan = EvaluatablePlan( - start=new_environment.start_at, - end=new_environment.end_at, - new_snapshots=[], - environment=new_environment, - no_gaps=True, - skip_backfill=False, - empty_backfill=False, - restatements={ - the_snapshot.name: ( - to_timestamp("2022-01-02"), - to_timestamp("2022-01-04"), - ) - }, - is_dev=False, - forward_only=True, - models_to_backfill=None, - end_bounded=False, - ensure_finalized_snapshots=False, - directly_modified_snapshots=[], - indirectly_modified_snapshots={}, - removed_snapshots=[], - interval_end_per_model=None, - allow_destructive_models=set(), - requires_backfill=True, - disabled_restatement_models=set(), - ) - - plan_request = common.PlanApplicationRequest( - plan=plan, - notification_targets=[], - backfill_concurrent_tasks=1, - ddl_concurrent_tasks=1, - users=[], - ) - old_environment = Environment( - name=environment_name, - snapshots=[the_snapshot.table_info], - start_at="2022-01-01", - end_at="2022-01-07", - plan_id="test_plan_id", - ) - - state_sync_mock = mocker.Mock() - state_sync_mock.get_snapshots.return_value = {the_snapshot.snapshot_id: the_snapshot} - state_sync_mock.get_environment.return_value = old_environment - state_sync_mock.refresh_snapshot_intervals.return_value = [the_snapshot] - - now_value = "2022-01-09T23:59:59+00:00" - with mock.patch( - "sqlmesh.schedulers.airflow.plan.now", side_effect=lambda: to_datetime(now_value) - ): - plan_spec = create_plan_dag_spec(plan_request, state_sync_mock) - - assert plan_spec == common.PlanDagSpec( - request_id="test_plan_id", - environment=new_environment, - new_snapshots=[], - backfill_intervals_per_snapshot=[ - common.BackfillIntervalsPerSnapshot( - snapshot_id=the_snapshot.snapshot_id, - intervals=expected_intervals, - ) - ], - demoted_snapshots=[], - unpaused_dt=None, - no_gaps=True, - notification_targets=[], - backfill_concurrent_tasks=1, - ddl_concurrent_tasks=1, - users=[], - is_dev=False, - allow_destructive_snapshots=set(), - forward_only=True, - dag_start_ts=to_timestamp(now_value), - no_gaps_snapshot_names={the_snapshot.name}, - directly_modified_snapshots=[], - indirectly_modified_snapshots={}, - removed_snapshots=[], - ) - - state_sync_mock.get_snapshots.assert_called_once() - state_sync_mock.get_environment.assert_called_once() - state_sync_mock.refresh_snapshot_intervals.assert_called_once() - - state_sync_mock.remove_intervals.assert_called_once_with( - [(the_snapshot, (to_timestamp("2022-01-02"), to_timestamp("2022-01-04")))], - remove_shared_versions=True, - ) - - assert the_snapshot.intervals == [ - (to_timestamp("2022-01-01"), to_timestamp("2022-01-02")), - (to_timestamp("2022-01-04"), to_timestamp("2022-01-08")), - ] - - -def test_select_models_for_backfill(mocker: MockerFixture, random_name, make_snapshot): - snapshot_a = make_snapshot( - create_sql_model( - "a", - parse_one("SELECT 1, ds"), - kind=IncrementalByTimeRangeKind(time_column="ds"), - start="2022-01-01", - ), - ) - snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING) - - snapshot_b = make_snapshot( - create_sql_model( - "b", - parse_one("SELECT 2, ds"), - kind=IncrementalByTimeRangeKind(time_column="ds"), - start="2022-01-01", - ), - ) - snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING) - - environment_name = random_name() - new_environment = Environment( - name=environment_name, - snapshots=[snapshot_a.table_info, snapshot_b.table_info], - start_at="2022-01-01", - end_at="2022-01-04", - plan_id="test_plan_id", - suffix_target=EnvironmentSuffixTarget.TABLE, - ) - - plan = EvaluatablePlan( - start=new_environment.start_at, - end=new_environment.end_at, - new_snapshots=[snapshot_a, snapshot_b], - environment=new_environment, - no_gaps=True, - skip_backfill=False, - empty_backfill=False, - restatements={}, - is_dev=False, - forward_only=True, - models_to_backfill={snapshot_b.name}, - end_bounded=False, - ensure_finalized_snapshots=False, - directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], - indirectly_modified_snapshots={}, - removed_snapshots=[], - interval_end_per_model=None, - allow_destructive_models=set(), - requires_backfill=True, - disabled_restatement_models=set(), - ) - - plan_request = common.PlanApplicationRequest( - plan=plan, - notification_targets=[], - backfill_concurrent_tasks=1, - ddl_concurrent_tasks=1, - users=[], - ) - - state_sync_mock = mocker.Mock() - state_sync_mock.get_snapshots.return_value = {} - state_sync_mock.get_environment.return_value = None - state_sync_mock.get_snapshot_intervals.return_value = [] - state_sync_mock.refresh_snapshot_intervals.return_value = [] - - with mock.patch( - "sqlmesh.schedulers.airflow.plan.now", - side_effect=lambda: to_datetime("2023-01-01"), - ): - plan_spec = create_plan_dag_spec(plan_request, state_sync_mock) - - assert plan_spec == common.PlanDagSpec( - request_id="test_plan_id", - environment=new_environment, - new_snapshots=[snapshot_a, snapshot_b], - backfill_intervals_per_snapshot=[ - common.BackfillIntervalsPerSnapshot( - snapshot_id=snapshot_b.snapshot_id, - intervals=[(to_datetime("2022-01-01"), to_datetime("2022-01-05"))], - before_promote=True, - ) - ], - demoted_snapshots=[], - unpaused_dt="2022-01-04", - no_gaps=True, - notification_targets=[], - backfill_concurrent_tasks=1, - ddl_concurrent_tasks=1, - users=[], - is_dev=False, - forward_only=True, - allow_destructive_snapshots=set(), - dag_start_ts=to_timestamp("2023-01-01"), - deployability_index=DeployabilityIndex.all_deployable(), - no_gaps_snapshot_names={'"a"', '"b"'}, - models_to_backfill={snapshot_b.name}, - directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], - indirectly_modified_snapshots={}, - removed_snapshots=[], - ) - - -def test_create_plan_dag_spec_duplicated_snapshot( - mocker: MockerFixture, snapshot: Snapshot, random_name -): - environment_name = random_name() - new_environment = Environment( - name=environment_name, - snapshots=[snapshot.table_info], - start_at="2022-01-01", - end_at="2022-01-01", - plan_id="test_plan_id", - ) - - plan = EvaluatablePlan( - start=new_environment.start_at, - end=new_environment.end_at, - new_snapshots=[snapshot], - environment=new_environment, - no_gaps=False, - skip_backfill=False, - empty_backfill=False, - restatements={}, - is_dev=False, - forward_only=False, - models_to_backfill=None, - end_bounded=False, - ensure_finalized_snapshots=False, - directly_modified_snapshots=[], - indirectly_modified_snapshots={}, - removed_snapshots=[], - interval_end_per_model=None, - allow_destructive_models=set(), - requires_backfill=True, - disabled_restatement_models=set(), - ) - - plan_request = common.PlanApplicationRequest( - plan=plan, - notification_targets=[], - backfill_concurrent_tasks=1, - ddl_concurrent_tasks=1, - users=[], - ) - - dag_run_mock = mocker.Mock() - dag_run_mock.conf = plan_request.dict() - - state_sync_mock = mocker.Mock() - state_sync_mock.get_snapshots.return_value = {snapshot.snapshot_id: snapshot} - - with pytest.raises(SQLMeshError): - create_plan_dag_spec(plan_request, state_sync_mock) - - state_sync_mock.get_snapshots.assert_called_once() - - -@pytest.mark.parametrize("unbounded_end", [None, ""]) -def test_create_plan_dag_spec_unbounded_end( - mocker: MockerFixture, - snapshot: Snapshot, - make_snapshot, - random_name, - unbounded_end: t.Optional[str], -): - unrelated_snapshot = make_snapshot(create_sql_model(random_name(), parse_one("SELECT 2, ds"))) - unrelated_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - - environment_name = random_name() - new_environment = Environment( - name=environment_name, - snapshots=[snapshot.table_info], - start_at="2022-01-01", - end_at=unbounded_end, - plan_id="test_plan_id", - ) - - plan = EvaluatablePlan( - start=new_environment.start_at, - end=now(), - new_snapshots=[], - environment=new_environment, - no_gaps=True, - skip_backfill=False, - empty_backfill=False, - restatements={}, - is_dev=False, - forward_only=False, - models_to_backfill=None, - end_bounded=False, - ensure_finalized_snapshots=False, - directly_modified_snapshots=[], - indirectly_modified_snapshots={}, - removed_snapshots=[], - interval_end_per_model=None, - allow_destructive_models=set(), - requires_backfill=True, - disabled_restatement_models=set(), - ) - - plan_request = common.PlanApplicationRequest( - plan=plan, - notification_targets=[], - backfill_concurrent_tasks=1, - ddl_concurrent_tasks=1, - users=[], - ) - - state_sync_mock = mocker.Mock() - state_sync_mock.get_snapshots.return_value = { - snapshot.snapshot_id: snapshot, - unrelated_snapshot.snapshot_id: unrelated_snapshot, - } - state_sync_mock.get_environment.return_value = None - state_sync_mock.get_snapshot_intervals.return_value = [] - state_sync_mock.refresh_snapshot_intervals.return_value = [] - - create_plan_dag_spec(plan_request, state_sync_mock) - - state_sync_mock.get_snapshots.assert_called_once() - state_sync_mock.get_environment.assert_called_once() - state_sync_mock.refresh_snapshot_intervals.assert_called_once() - - -def test_plan_dag_state(snapshot: Snapshot, sushi_context: Context, random_name): - environment_name = random_name() - environment = Environment( - name=environment_name, - snapshots=[snapshot.table_info], - start_at=to_timestamp("2022-01-01"), - end_at=None, - plan_id="test_plan_id", - ) - plan_dag_spec = common.PlanDagSpec( - request_id="test_plan_id", - environment=environment, - new_snapshots=[], - backfill_intervals_per_snapshot=[], - demoted_snapshots=[], - unpaused_dt=None, - no_gaps=True, - notification_targets=[], - backfill_concurrent_tasks=1, - ddl_concurrent_tasks=1, - users=[], - is_dev=False, - allow_destructive_snapshots=set(), - forward_only=True, - dag_start_ts=to_timestamp("2023-01-01"), - ) - - plan_dag_state = PlanDagState.from_state_sync(sushi_context.state_sync) - - def get_hydrated_dag_specs(): - state_plan_dag_specs = plan_dag_state.get_dag_specs() - for state_plan_dag_spec in state_plan_dag_specs: - state_plan_dag_spec.environment.snapshots - return state_plan_dag_specs - - assert not plan_dag_state.get_dag_specs() - - plan_dag_state.add_dag_spec(plan_dag_spec) - assert get_hydrated_dag_specs() == [plan_dag_spec] - - plan_dag_state.delete_dag_specs([]) - assert get_hydrated_dag_specs() == [plan_dag_spec] - - plan_dag_state.delete_dag_specs( - [common.plan_application_dag_id(environment_name, "test_plan_id")] - ) - assert not plan_dag_state.get_dag_specs() From 044854f567b0f74149019a803c555c42f8a96760 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 18 Apr 2025 22:56:49 +0300 Subject: [PATCH 0005/1056] Fix(motherduck): Attach multiple catalogs in MotherDuck (#4178) --- docs/integrations/engines/motherduck.md | 54 +++++++++++++------------ sqlmesh/core/config/connection.py | 18 ++++----- tests/core/test_connection_config.py | 39 +++++++++++++++++- 3 files changed, 72 insertions(+), 39 deletions(-) diff --git a/docs/integrations/engines/motherduck.md b/docs/integrations/engines/motherduck.md index e15ba9f6e4..704a7fa1a3 100644 --- a/docs/integrations/engines/motherduck.md +++ b/docs/integrations/engines/motherduck.md @@ -1,6 +1,6 @@ # MotherDuck -This page provides information about how to use SQLMesh with MotherDuck. +This page provides information about how to use SQLMesh with MotherDuck. It begins with a [Connection Quickstart](#connection-quickstart) that demonstrates how to connect to MotherDuck, or you can skip directly to information about using MotherDuck with the built-in scheduler. @@ -8,25 +8,25 @@ It begins with a [Connection Quickstart](#connection-quickstart) that demonstrat Connecting to cloud warehouses involves a few steps, so this connection quickstart provides the info you need to get up and running with MotherDuck. -It demonstrates connecting to MotherDuck with the `duckdb` library bundled with SQLMesh. +It demonstrates connecting to MotherDuck with the `duckdb` library bundled with SQLMesh. MotherDuck provides a single way to authorize a connection. This quickstart demonstrates authenticating with a token. -!!! tip +!!! tip This quick start assumes you are familiar with basic SQLMesh commands and functionality. - If you’re not familiar, work through the [SQLMesh Quickstart](../../quick_start.md) before continuing. + If you’re not familiar, work through the [SQLMesh Quickstart](../../quick_start.md) before continuing. ### Prerequisites Before working through this quickstart guide, ensure that: -1. You have a motherduck account and an access token. -2. Your computer has SQLMesh installed with the DuckDB extra available. - 1. Install from command line with the command `pip install “sqlmesh[duckdb]”` -3. You have initialized a SQLMesh example project on your computer - 1. Open a command line interface and navigate to the directory where the project files should go. - 2. Initialize the project with the command `sqlmesh init motherduck` +1. You have a motherduck account and an access token. +2. Your computer has SQLMesh installed with the DuckDB extra available. + 1. Install from command line with the command `pip install “sqlmesh[duckdb]”` +3. You have initialized a SQLMesh example project on your computer + 1. Open a command line interface and navigate to the directory where the project files should go. + 2. Initialize the project with the command `sqlmesh init duckdb`, since `duckdb` is the dialect. #### Access control permissions @@ -38,30 +38,32 @@ We now have what is required to configure SQLMesh’s connection to MotherDuck. We start the configuration by adding a gateway named `motherduck` to our example project’s config.yaml file and making it our `default gateway`, as well as adding our token, persistent, and ephemeral catalogs. -```yaml -gateways: - motherduck: - connection: - type: motherduck - catalogs: - persistent: ‘md:’ - ephemeral: ‘:memory:’ - token: \ - -default\_gateway: motherduck +```yaml +gateways: + motherduck: + connection: + type: motherduck + catalogs: + persistent: "md:" + ephemeral: ":memory:" + token: + +default_gateway: motherduck ``` +Catalogs can be defined to connect to anything that [DuckDB can be attached to](./duckdb.md#other-connection-catalogs-example). + !!! warning Best practice for storing secrets like tokens is placing them in [environment variables that the configuration file loads dynamically](../../guides/configuration.md#environment-variables). For simplicity, this guide instead places the value directly in the configuration file. This code demonstrates how to use the environment variable `MOTHERDUCK_TOKEN` for the configuration's `token` parameter: ```yaml linenums="1" hl_lines="5" - gateways: - motherduck: - connection: - type: motherduck - token: {{ env_var('MOTHERDUCK_TOKEN') }} + gateways: + motherduck: + connection: + type: motherduck + token: {{ env_var('MOTHERDUCK_TOKEN') }} ``` ### Check connection diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 0c4390c82d..c598b29a3d 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -150,16 +150,12 @@ def to_sql(self, alias: str) -> str: options_sql = f" ({', '.join(options)})" if options else "" alias_sql = "" # TODO: Add support for Postgres schema. Currently adding it blocks access to the information_schema - if self.type == "motherduck": - # MotherDuck does not support aliasing - md_db = self.path.replace("md:", "") - if md_db != alias.replace('"', ""): - raise ConfigError( - f"MotherDuck does not support assigning an alias different from the database name {md_db}." - ) - else: - alias_sql += f" AS {alias}" - return f"ATTACH '{self.path}'{alias_sql}{options_sql}" + + # MotherDuck does not support aliasing + alias_sql = ( + f" AS {alias}" if not (self.type == "motherduck" or self.path.startswith("md:")) else "" + ) + return f"ATTACH IF NOT EXISTS '{self.path}'{alias_sql}{options_sql}" class BaseDuckDBConnectionConfig(ConnectionConfig): @@ -264,7 +260,7 @@ def init(cursor: duckdb.DuckDBPyConnection) -> None: if isinstance(path_options, DuckDBAttachOptions): query = path_options.to_sql(alias) else: - query = f"ATTACH '{path_options}'" + query = f"ATTACH IF NOT EXISTS '{path_options}'" if not path_options.startswith("md:"): query += f" AS {alias}" cursor.execute(query) diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index e2048d09aa..c576d7e3dd 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -597,12 +597,47 @@ def test_duckdb_attach_options(): assert ( options.to_sql(alias="db") - == "ATTACH 'dbname=postgres user=postgres host=127.0.0.1' AS db (TYPE POSTGRES, READ_ONLY)" + == "ATTACH IF NOT EXISTS 'dbname=postgres user=postgres host=127.0.0.1' AS db (TYPE POSTGRES, READ_ONLY)" ) options = DuckDBAttachOptions(type="duckdb", path="test.db", read_only=False) - assert options.to_sql(alias="db") == "ATTACH 'test.db' AS db" + assert options.to_sql(alias="db") == "ATTACH IF NOT EXISTS 'test.db' AS db" + + +def test_motherduck_attach_catalog(make_config): + config = make_config( + type="motherduck", + catalogs={ + "test1": "md:test1", + "test2": DuckDBAttachOptions( + type="motherduck", + path="md:test2", + ), + }, + ) + assert isinstance(config, MotherDuckConnectionConfig) + assert config.get_catalog() == "test1" + + assert config.catalogs.get("test2").read_only is False + assert config.catalogs.get("test2").path == "md:test2" + assert not config.is_recommended_for_state_sync + + +def test_motherduck_attach_options(): + options = DuckDBAttachOptions( + type="postgres", path="dbname=postgres user=postgres host=127.0.0.1", read_only=True + ) + + assert ( + options.to_sql(alias="db") + == "ATTACH IF NOT EXISTS 'dbname=postgres user=postgres host=127.0.0.1' AS db (TYPE POSTGRES, READ_ONLY)" + ) + + options = DuckDBAttachOptions(type="motherduck", path="md:test.db", read_only=False) + + # Here the alias should be ignored compared to duckdb + assert options.to_sql(alias="db") == "ATTACH IF NOT EXISTS 'md:test.db'" def test_duckdb_multithreaded_connection_factory(make_config): From 67e5e722fe61a3a53624b59728ec748e30272f9f Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 18 Apr 2025 13:58:20 -0700 Subject: [PATCH 0006/1056] Fix: Forward-only models can't be categorized manually (#4184) --- sqlmesh/core/console.py | 2 ++ sqlmesh/core/plan/builder.py | 14 +++++----- tests/core/test_plan.py | 52 ++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 04857e821e..abf7916a0e 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -1625,6 +1625,8 @@ def _prompt_categorize( self._show_categorized_snapshots(plan, default_catalog) for snapshot in plan.uncategorized: + if snapshot.is_model and snapshot.model.forward_only: + continue if not no_diff: self.show_sql(plan.context_diff.text_diff(snapshot.name)) tree = Tree( diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 3ba2725c52..dd3888112a 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -33,7 +33,7 @@ yesterday_ds, to_timestamp, ) -from sqlmesh.utils.errors import NoChangesPlanError, PlanError, SQLMeshError +from sqlmesh.utils.errors import NoChangesPlanError, PlanError logger = logging.getLogger(__name__) @@ -197,16 +197,16 @@ def set_choice(self, snapshot: Snapshot, choice: SnapshotChangeCategory) -> Plan if self._forward_only: raise PlanError("Choice setting is not supported by a forward-only plan.") if not self._is_new_snapshot(snapshot): - raise SQLMeshError( - f"A choice can't be changed for the existing version of '{snapshot.name}'." + raise PlanError( + f"A choice can't be changed for the existing version of {snapshot.name}." ) if ( not self._context_diff.directly_modified(snapshot.name) and snapshot.snapshot_id not in self._context_diff.added ): - raise SQLMeshError( - f"Only directly modified models can be categorized ({snapshot.name})." - ) + raise PlanError(f"Only directly modified models can be categorized ({snapshot.name}).") + if snapshot.is_model and snapshot.model.forward_only: + raise PlanError(f"Forward-only model {snapshot.name} cannot be categorized manually.") self._choices[snapshot.snapshot_id] = choice self._latest_plan = None @@ -215,7 +215,7 @@ def set_choice(self, snapshot: Snapshot, choice: SnapshotChangeCategory) -> Plan def apply(self) -> None: """Builds and applies the plan.""" if not self._apply: - raise SQLMeshError("Plan was not initialized with an applier.") + raise PlanError("Plan was not initialized with an applier.") self._apply(self.build()) def build(self) -> Plan: diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 11d9461165..89e8d3c7fe 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -3053,3 +3053,55 @@ def test_plan_environment_statements_diff(make_snapshot): ) assert stripped == expected_output console_output.close() + + +def test_set_choice_for_forward_only_model(make_snapshot): + snapshot = make_snapshot( + SqlModel( + name="a", + query=parse_one("select 1, ds"), + dialect="duckdb", + kind=IncrementalByTimeRangeKind(time_column="ds", forward_only=True), + ) + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + updated_snapshot = make_snapshot( + SqlModel( + name="a", + query=parse_one("select 3, ds"), + kind=IncrementalByTimeRangeKind(time_column="ds", forward_only=True), + dialect="duckdb", + ) + ) + updated_snapshot.previous_versions = snapshot.all_versions + + context_diff = ContextDiff( + environment="test_environment", + is_new_environment=True, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={updated_snapshot.name: (updated_snapshot, snapshot)}, + snapshots={updated_snapshot.snapshot_id: updated_snapshot}, + new_snapshots={updated_snapshot.snapshot_id: updated_snapshot}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + ) + + schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER + plan_builder = PlanBuilder(context_diff, schema_differ, is_dev=True) + + with pytest.raises(PlanError, match='Forward-only model "a" cannot be categorized manually.'): + plan_builder.set_choice(updated_snapshot, SnapshotChangeCategory.BREAKING) + + plan = plan_builder.build() + assert ( + plan.snapshots[updated_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.FORWARD_ONLY + ) From f28341a79a03035f49e0a5a4572198725a57947c Mon Sep 17 00:00:00 2001 From: Andrew Madson <121112108+andymadson@users.noreply.github.com> Date: Fri, 18 Apr 2025 18:38:50 -0700 Subject: [PATCH 0007/1056] Update CLI documentation descriptions to match current output format (#4183) Co-authored-by: Trey Spiller <1831878+treysp@users.noreply.github.com> --- docs/quickstart/cli.md | 142 ++++++++++++++++++++++++----------------- 1 file changed, 82 insertions(+), 60 deletions(-) diff --git a/docs/quickstart/cli.md b/docs/quickstart/cli.md index 403246a591..cf990eb704 100644 --- a/docs/quickstart/cli.md +++ b/docs/quickstart/cli.md @@ -148,17 +148,18 @@ $ sqlmesh plan ====================================================================== Successfully Ran 1 tests against duckdb ---------------------------------------------------------------------- + `prod` environment will be initialized Models: └── Added: - ├── sqlmesh_example.seed_model + ├── sqlmesh_example.full_model ├── sqlmesh_example.incremental_model - └── sqlmesh_example.full_model -Models needing backfill (missing dates): -├── sqlmesh_example.full_model: 2020-01-01 - 2023-05-31 -├── sqlmesh_example.incremental_model: 2020-01-01 - 2023-05-31 -└── sqlmesh_example.seed_model: 2023-05-31 - 2023-05-31 + └── sqlmesh_example.seed_model +Models needing backfill: +├── sqlmesh_example.full_model: [full refresh] +├── sqlmesh_example.incremental_model: [2020-01-01 - 2025-04-17] +└── sqlmesh_example.seed_model: [full refresh] Apply - Backfill Tables [y/n]: ``` @@ -168,10 +169,7 @@ Line 5 describes what environments the plan will affect when applied - a new `pr Lines 7-11 of the output show that SQLMesh detected three new models relative to the current empty environment. -Lines 12-15 list each model that will be executed by the plan, along with the date intervals that will be run. Note that `full_model` and `incremental_model` both show `2020-01-01` as their start date because: - -1. The incremental model specifies that date in the `start` property of its `MODEL` statement and -2. The full model depends on the incremental model. +Lines 12-16 list each model that will be executed by the plan, along with the date intervals or refresh types. For both `full_model` and `seed_model`, it shows `[full refresh]`, while for `incremental_model` it shows a specific date range `[2020-01-01 - 2025-04-17]`. The incremental model date range begins from 2020-01-01 because the `full` model kind always fully rebuilds its table. The `seed_model` date range begins on the same day the plan was made because `SEED` models have no temporality associated with them other than whether they have been modified since the previous SQLMesh plan. @@ -254,20 +252,24 @@ Line 16 asks you whether to proceed with executing the model backfills described ```bash linenums="1" Apply - Backfill Tables [y/n]: y -Creating physical tables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 -All model versions have been created successfully +Updating physical layer ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 -[1/1] sqlmesh_example.seed_model evaluated in 0.01s -[1/1] sqlmesh_example.incremental_model evaluated in 0.01s -[1/1] sqlmesh_example.full_model evaluated in 0.02s -Evaluating models ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 +✔ Physical layer updated -All model batches have been executed successfully +[1/1] sqlmesh_example.seed_model [insert seed file] +0.02s +[1/1] sqlmesh_example.incremental_model [insert 2020-01-01 - +2025-04-17] 0.03s +[1/1] sqlmesh_example.full_model [full refresh, audits ✔1] +0.05s +Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 -Virtually Updating 'prod' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 0:00:00 +✔ Model batches executed -The target environment has been updated successfully +Updating virtual layer ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 + +✔ Virtual layer updated ``` SQLMesh performs three actions when applying the plan: @@ -276,11 +278,13 @@ SQLMesh performs three actions when applying the plan: - Evaluating/running the models - Virtually updating the plan's target environment -Line 2 provides a progress bar and elapsed time for the first step of creating new model versions (very fast in this simple project). Line 4 reports that the first step has completed. +Lines 2-4 show the progress and completion of the first step - updating the physical layer (creating new model versions). + +Lines 6-11 show the execution of each model with their specific operations and timing. Line 6 shows the seed model being inserted, line 8 shows the incremental model being inserted for the specified date range, and line 10 shows the full model being processed with its audit check passing. -Lines 6-8 show the run time for each model in the project. Line 9 provides a progress bar and total elapsed time for the second step of evaluating the models. Line 11 reports that the second step has completed. +Lines 12-14 show the progress and completion of the second step - executing model batches. -Line 13 provides a progress bar and total elapsed time for the third step of virtually updating the plan's target environment. Line 15 reports that the third step has completed and the `prod` environment now points to the tables created during model execution. +Lines 16-18 show the progress and completion of the final step - virtually updating the plan's target environment, which makes the data available for querying. You've now created a new production environment with all of history backfilled. @@ -323,10 +327,11 @@ Run `sqlmesh plan dev` to create a development environment called `dev`: $ sqlmesh plan dev ====================================================================== Successfully Ran 1 tests against duckdb ----------------------------------------------------------------------- +---------------------------------------------------------------------- New environment `dev` will be created from `prod` + Differences from the `prod` environment: Models: @@ -334,52 +339,63 @@ Models: │ └── sqlmesh_example__dev.incremental_model └── Indirectly Modified: └── sqlmesh_example__dev.full_model ---- - -+++ - -@@ -10,6 +10,7 @@ +--- + ++++ + +@@ -14,6 +14,7 @@ + SELECT - id, - item_id, + id, + item_id, + 'z' AS new_column, - event_date + event_date FROM sqlmesh_example.seed_model WHERE -Directly Modified: sqlmesh_example__dev.incremental_model (Non-breaking) + +Directly Modified: sqlmesh_example__dev.incremental_model +(Non-breaking) └── Indirectly Modified Children: - └── sqlmesh_example__dev.full_model (Indirect Non-breaking) + └── sqlmesh_example__dev.full_model (Indirect Non-breaking) Models needing backfill: -└── sqlmesh_example__dev.incremental_model: [2020-01-01 - 2023-05-31] -Apply - Backfill Tables [y/n]: y +└── sqlmesh_example__dev.incremental_model: [2020-01-01 - 2025-04-17] +Apply - Backfill Tables [y/n]: ``` Line 6 of the output states that a new environment `dev` will be created from the existing `prod` environment. -Lines 8-14 summarize the differences between the modified model and the `prod` environment, detecting that we directly modified `incremental_model` and that `full_model` was indirectly modified because it selects from the incremental model. Note that the model schemas are `sqlmesh_example__dev`, indicating that they are being created in the `dev` environment. +Lines 10-15 summarize the differences between the modified model and the `prod` environment, detecting that we directly modified `incremental_model` and that `full_model` was indirectly modified because it selects from the incremental model. Note that the model schemas are `sqlmesh_example__dev`, indicating that they are being created in the `dev` environment. -On line 28, we see that SQLMesh automatically classified the change as `Non-breaking` because it understood that the change was additive (added a column not used by `full_model`) and did not invalidate any data already in `prod`. +On line 31, we see that SQLMesh automatically classified the change as `Non-breaking` because it understood that the change was additive (added a column not used by `full_model`) and did not invalidate any data already in `prod`. Enter `y` at the prompt and press `Enter` to apply the plan and execute the backfill: ```bash linenums="1" Apply - Backfill Tables [y/n]: y -Creating physical tables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 2/2 • 0:00:00 -Model versions created successfully +Updating physical layer ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 2/2 • 0:00:00 + +✔ Physical layer updated -[1/1] sqlmesh_example__dev.incremental_model evaluated in 0.01s -Evaluating models ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1/1 • 0:00:00 +[1/1] sqlmesh_example__dev.incremental_model [insert 2020-01-01 - +2025-04-17] 0.03s +Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1/1 • 0:00:00 -Model batches executed successfully +✔ Model batches executed -Virtually Updating 'dev' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 0:00:00 +Updating virtual layer ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 2/2 • 0:00:00 -Target environment updated successfully +✔ Virtual layer updated ``` -Line 6 of the output shows that SQLMesh applied the change and evaluated `sqlmesh_example__dev.incremental_model`. +Lines 3-5 show the progress and completion of updating the physical layer. + +Line 7 shows that SQLMesh applied the change and evaluated `sqlmesh_example__dev.incremental_model` for the date range from 2020-01-01 to 2025-04-17. + +Lines 9-11 show the progress and completion of executing model batches. + +Lines 13-15 show the progress and completion of updating the virtual layer. SQLMesh did not need to backfill anything for the `full_model` since the change was `Non-breaking`. @@ -431,7 +447,8 @@ Enter `y` and press `Enter` at the `Apply - Virtual Update [y/n]:` prompt to app $ sqlmesh plan ====================================================================== Successfully Ran 1 tests against duckdb ----------------------------------------------------------------------- +---------------------------------------------------------------------- + Differences from the `prod` environment: Models: @@ -439,31 +456,36 @@ Models: │ └── sqlmesh_example.incremental_model └── Indirectly Modified: └── sqlmesh_example.full_model ---- - -+++ - -@@ -10,6 +10,7 @@ +--- + ++++ + +@@ -14,6 +14,7 @@ + SELECT - id, - item_id, + id, + item_id, + 'z' AS new_column, - event_date + event_date FROM sqlmesh_example.seed_model WHERE -Directly Modified: sqlmesh_example.incremental_model (Non-breaking) + +Directly Modified: sqlmesh_example.incremental_model (Non-breaking) └── Indirectly Modified Children: └── sqlmesh_example.full_model (Indirect Non-breaking) Apply - Virtual Update [y/n]: y -Virtually Updating 'prod' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 0:00:00 -Target environment updated successfully +SKIP: No physical layer updates to perform + +SKIP: No model batches to execute + +Updating virtual layer ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 2/2 • 0:00:00 -Virtual Update executed successfully +✔ Virtual layer updated ``` -Note that a backfill was not necessary and only a Virtual Update occurred. +Note that a backfill was not necessary and only a Virtual Update occurred, as indicated by the "SKIP: No physical layer updates to perform" and "SKIP: No model batches to execute" messages. This is because the changes were already calculated and executed in the `dev` environment, and SQLMesh is smart enough to recognize that it only needs to update the virtual references to the existing tables rather than recomputing everything. ### 5.2 Validate updates in prod Double-check that the data updated in `prod` by running `sqlmesh fetchdf "select * from sqlmesh_example.incremental_model"`: @@ -490,4 +512,4 @@ From here, you can: * [Learn more about SQLMesh CLI commands](../reference/cli.md) * [Set up a connection to a database or SQL engine](../guides/connections.md) * [Learn more about SQLMesh concepts](../concepts/overview.md) -* [Join our Slack community](https://tobikodata.com/slack) +* [Join our Slack community](https://tobikodata.com/slack) \ No newline at end of file From 8a7dbda57e073057b3570685a1a01b83983330cd Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sat, 19 Apr 2025 18:57:17 +0100 Subject: [PATCH 0008/1056] feat: adds auth to vscode extension (#4188) --- vscode/extension/package-lock.json | 12 +- vscode/extension/package.json | 19 ++- vscode/extension/src/auth/auth.ts | 199 +++++++++++++++++++++++ vscode/extension/src/commands/signin.ts | 8 + vscode/extension/src/commands/signout.ts | 7 + vscode/extension/src/extension.ts | 38 ++++- vscode/extension/src/utilities/exec.ts | 67 ++++++++ vscode/extension/src/utilities/sleep.ts | 8 + 8 files changed, 347 insertions(+), 11 deletions(-) create mode 100644 vscode/extension/src/auth/auth.ts create mode 100644 vscode/extension/src/commands/signin.ts create mode 100644 vscode/extension/src/commands/signout.ts create mode 100644 vscode/extension/src/utilities/exec.ts create mode 100644 vscode/extension/src/utilities/sleep.ts diff --git a/vscode/extension/package-lock.json b/vscode/extension/package-lock.json index 64644b57c3..16e4ac0dd1 100644 --- a/vscode/extension/package-lock.json +++ b/vscode/extension/package-lock.json @@ -11,7 +11,8 @@ "@types/fs-extra": "^11.0.4", "@vscode/python-extension": "^1.0.5", "fs-extra": "^11.3.0", - "vscode-languageclient": "^9.0.1" + "vscode-languageclient": "^9.0.1", + "zod": "^3.24.3" }, "devDependencies": { "@types/mocha": "^10.0.10", @@ -7129,6 +7130,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/vscode/extension/package.json b/vscode/extension/package.json index f345e34caa..6b0500c29b 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -19,6 +19,12 @@ ], "main": "./dist/extension.js", "contributes": { + "authentication": [ + { + "id": "tobikodata", + "label": "Tobiko" + } + ], "commands": [ { "command": "sqlmesh.format", @@ -29,6 +35,16 @@ "command": "sqlmesh.restart", "title": "Restart SQLMesh Servers", "description": "SQLMesh" + }, + { + "command": "sqlmesh.signin", + "title": "Sign in to Tobiko Cloud", + "description": "SQLMesh" + }, + { + "command": "sqlmesh.signout", + "title": "Sign out from Tobiko Cloud", + "description": "SQLMesh" } ] }, @@ -52,7 +68,8 @@ "@types/fs-extra": "^11.0.4", "@vscode/python-extension": "^1.0.5", "fs-extra": "^11.3.0", - "vscode-languageclient": "^9.0.1" + "vscode-languageclient": "^9.0.1", + "zod": "^3.24.3" }, "devDependencies": { "@types/mocha": "^10.0.10", diff --git a/vscode/extension/src/auth/auth.ts b/vscode/extension/src/auth/auth.ts new file mode 100644 index 0000000000..461ec6cbf3 --- /dev/null +++ b/vscode/extension/src/auth/auth.ts @@ -0,0 +1,199 @@ +import { + env, + Uri, + AuthenticationProvider, + AuthenticationProviderAuthenticationSessionsChangeEvent, + AuthenticationSession, + Event, + EventEmitter, + window, +} from "vscode" +import { get_tcloud_bin } from "../utilities/sqlmesh/sqlmesh" +import { err, isErr, ok, Result } from "../utilities/functional/result" +import { execAsync } from "../utilities/exec" +import { getProjectRoot } from "../utilities/common/utilities" +import z from "zod" +import { traceError } from "../utilities/common/log" + +export const AUTH_TYPE = "tobikodata" +export const AUTH_NAME = "Tobiko" + +const tokenSchema = z.object({ + iss: z.string(), + aud: z.string(), + sub: z.string(), + scope: z.string(), + iat: z.number(), + exp: z.number(), + email: z.string(), +}) +const statusResponseSchema = z.object({ + is_logged_in: z.boolean(), + id_token: tokenSchema, +}) + +type StatusResponse = z.infer; + +const loginUrlResponseSchema = z.object({ + url: z.string(), + verifier_code: z.string(), +}) + +export class AuthenticationProviderTobikoCloud + implements AuthenticationProvider +{ + static id = AUTH_TYPE + static name = AUTH_NAME + + private _sessionChangeEmitter = + new EventEmitter() + + onDidChangeSessions: Event = + this._sessionChangeEmitter.event + + /** + * Get the status of the authentication provider from the cli + * @returns true if the user is logged in with the id token, false otherwise + */ + private async get_status(): Promise> { + const workspacePath = await getProjectRoot() + const tcloudBin = await get_tcloud_bin() + if (isErr(tcloudBin)) { + return err(tcloudBin.error) + } + const tcloudBinPath = tcloudBin.value + const result = await execAsync( + tcloudBinPath, + ["auth", "vscode", "status"], + { + cwd: workspacePath.uri.fsPath, + } + ) + if (result.exitCode !== 0) { + return err("Failed to get tcloud auth status") + } + const status = result.stdout + const statusToJson = JSON.parse(status) + const statusResponse = statusResponseSchema.parse(statusToJson) + return ok(statusResponse) + } + + async getSessions(): Promise { + const status = await this.get_status() + if (isErr(status)) { + return [] + } + const statusResponse = status.value + if (!statusResponse.is_logged_in) { + return [] + } + const token = statusResponse.id_token + const session = { + id: token.email, + account: { + id: token.email, + label: "Tobiko", + }, + scopes: token.scope.split(" "), + accessToken: "", + } + return [session] + } + + async createSession(): Promise { + const workspacePath = await getProjectRoot() + const tcloudBin = await get_tcloud_bin() + if (isErr(tcloudBin)) { + throw new Error("Failed to get tcloud bin") + } + const tcloudBinPath = tcloudBin.value + const result = await execAsync( + tcloudBinPath, + ["auth", "vscode", "login-url"], + { + cwd: workspacePath.uri.fsPath, + } + ) + if (result.exitCode !== 0) { + throw new Error("Failed to get tcloud login url") + } + const resultToJson = JSON.parse(result.stdout) + const urlCode = loginUrlResponseSchema.parse(resultToJson) + const url = urlCode.url + + const ac = new AbortController() + const timeout = setTimeout(() => ac.abort(), 1000 * 60 * 5) + const backgroundServerForLogin = execAsync( + tcloudBinPath, + ["auth", "vscode", "start-server", urlCode.verifier_code], + { + cwd: workspacePath.uri.fsPath, + signal: ac.signal, + } + ) + + const messageResult = await window.showInformationMessage( + "Please login to Tobiko Cloud", + { + modal: true, + }, + "Sign in with browser", + "Cancel" + ) + + if (messageResult === "Sign in with browser") { + await env.openExternal(Uri.parse(url)) + } + if (messageResult === "Cancel") { + ac.abort() + throw new Error("Login cancelled") + } + + try { + const output = await backgroundServerForLogin + if (output.exitCode !== 0) { + throw new Error(`Failed to start server: ${output.stderr}`) + } + } catch (error) { + traceError(`Server error: ${error}`) + throw error + } + + clearTimeout(timeout) + + const status = await this.get_status() + if (isErr(status)) { + throw new Error("Failed to get tcloud auth status") + } + const statusResponse = status.value + if (!statusResponse.is_logged_in) { + throw new Error("Failed to login to tcloud") + } + const scopes = statusResponse.id_token.scope.split(" ") + const session: AuthenticationSession = { + id: AuthenticationProviderTobikoCloud.id, + account: { + id: AuthenticationProviderTobikoCloud.id, + label: "Tobiko", + }, + scopes: scopes, + accessToken: "" + } + return session + } + + async removeSession(): Promise { + const tcloudBin = await get_tcloud_bin() + const workspacePath = await getProjectRoot() + if (isErr(tcloudBin)) { + throw new Error("Failed to get tcloud bin") + } + const tcloudBinPath = tcloudBin.value + const result = await execAsync(tcloudBinPath, ["auth", "logout"], { + cwd: workspacePath.uri.fsPath, + }) + if (result.exitCode !== 0) { + throw new Error("Failed to logout from tcloud") + } + } +} diff --git a/vscode/extension/src/commands/signin.ts b/vscode/extension/src/commands/signin.ts new file mode 100644 index 0000000000..0620ffdaa0 --- /dev/null +++ b/vscode/extension/src/commands/signin.ts @@ -0,0 +1,8 @@ +import { AuthenticationProviderTobikoCloud } from "../auth/auth" +import * as vscode from "vscode" + + +export const signIn = (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { + await authenticationProvider.createSession() + await vscode.window.showInformationMessage("Signed in successfully") +} \ No newline at end of file diff --git a/vscode/extension/src/commands/signout.ts b/vscode/extension/src/commands/signout.ts new file mode 100644 index 0000000000..8e1f6cba5f --- /dev/null +++ b/vscode/extension/src/commands/signout.ts @@ -0,0 +1,7 @@ +import { AuthenticationProviderTobikoCloud } from "../auth/auth" +import * as vscode from "vscode" + +export const signOut = (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { + await authenticationProvider.removeSession() + await vscode.window.showInformationMessage("Signed out successfully") +} \ No newline at end of file diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 62ade2f971..de58050ccf 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -12,10 +12,11 @@ import { traceInfo, traceVerbose, } from "./utilities/common/log" -import { - onDidChangePythonInterpreter, -} from "./utilities/common/python" +import { onDidChangePythonInterpreter } from "./utilities/common/python" import { LSPClient } from "./lsp/lsp" +import { AuthenticationProviderTobikoCloud } from "./auth/auth" +import { signOut } from "./commands/signout" +import { signIn } from "./commands/signin" let lspClient: LSPClient | undefined @@ -29,17 +30,36 @@ export async function activate(context: vscode.ExtensionContext) { ) traceInfo("Activating SQLMesh extension") - context.subscriptions.push(vscode.commands.registerCommand( - "sqlmesh.format", - async () => { + traceInfo("Registering authentication provider") + const authProvider = new AuthenticationProviderTobikoCloud() + context.subscriptions.push( + vscode.authentication.registerAuthenticationProvider( + AuthenticationProviderTobikoCloud.id, + AuthenticationProviderTobikoCloud.name, + authProvider, + { supportsMultipleAccounts: false } + ) + ) + traceInfo("Authentication provider registered") + + context.subscriptions.push( + vscode.commands.registerCommand("sqlmesh.signin", signIn(authProvider)) + ) + + context.subscriptions.push( + vscode.commands.registerCommand("sqlmesh.signout", signOut(authProvider)) + ) + + context.subscriptions.push( + vscode.commands.registerCommand("sqlmesh.format", async () => { const out = await actual_callout.format() if (out === 0) { vscode.window.showInformationMessage("Project formatted successfully") } else { vscode.window.showErrorMessage("Project format failed") } - } - )) + }) + ) lspClient = new LSPClient() await lspClient.start() @@ -64,7 +84,7 @@ export async function activate(context: vscode.ExtensionContext) { }) ) - traceInfo("Extension activated") + traceInfo("Extension activated") } // This method is called when your extension is deactivated diff --git a/vscode/extension/src/utilities/exec.ts b/vscode/extension/src/utilities/exec.ts new file mode 100644 index 0000000000..83e10016ff --- /dev/null +++ b/vscode/extension/src/utilities/exec.ts @@ -0,0 +1,67 @@ +import { exec, ExecOptions } from "child_process" +import { traceInfo } from "./common/log" + +export interface ExecResult { + exitCode: number; + stdout: string; + stderr: string; +} + +export interface CancellableExecOptions extends ExecOptions { + /** When `abort()` is called on this signal the child process is killed. */ + signal?: AbortSignal; +} + +export function execAsync( + command: string, + args: string[], + options: CancellableExecOptions = {} +): Promise { + return new Promise((resolve, reject) => { + // Pass the signal straight through to `exec` + traceInfo(`Executing command: ${command} ${args.join(" ")}`) + const child = exec( + `${command} ${args.join(" ")}`, + options, + (error, stdout, stderr) => { + if (error) { + resolve({ + exitCode: typeof error.code === "number" ? error.code : 1, + stdout, + stderr, + }) + return + } + + resolve({ + exitCode: child.exitCode ?? 0, + stdout, + stderr, + }) + } + ) + + /* ---------- Tie the Promise life‑cycle to the AbortSignal ---------- */ + + if (options.signal) { + // If the caller aborts: kill the child and reject the promise + const onAbort = () => { + // `SIGTERM` is the default; use `SIGKILL` if you need something stronger + child.kill() + reject(new Error("Process cancelled")) + } + + if (options.signal.aborted) { + onAbort() + return + } + options.signal.addEventListener("abort", onAbort, { once: true }) + + // Clean‑up the event listener when the promise settles + const cleanup = () => + options.signal!.removeEventListener("abort", onAbort) + child.once("exit", cleanup) + child.once("error", cleanup) + } + }) +} \ No newline at end of file diff --git a/vscode/extension/src/utilities/sleep.ts b/vscode/extension/src/utilities/sleep.ts new file mode 100644 index 0000000000..e7e70f3133 --- /dev/null +++ b/vscode/extension/src/utilities/sleep.ts @@ -0,0 +1,8 @@ +/** + * Utility function that creates a promise which resolves after the specified time. + * @param ms The time to sleep in milliseconds + * @returns A promise that resolves after the specified time + */ +export async function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} From b6d5bcf3b2fadf876a6a20e53ac8e1d0ec61d9d1 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 20 Apr 2025 09:44:57 +0100 Subject: [PATCH 0009/1056] docs: adding better readme to vscode extension (#4190) --- vscode/extension/README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/vscode/extension/README.md b/vscode/extension/README.md index f2e7584554..3a1d2ab40c 100644 --- a/vscode/extension/README.md +++ b/vscode/extension/README.md @@ -1 +1,28 @@ # SQLMesh VSCode Extension + +## Functionality + +The following section outlines all the functionality in the Visual Studio Code extension. It is broken up into logical sections. + +### Authentication + +The extension allows you to manage your tcloud session within visual studio code + +- Sign in +- Sign out +- Sign in with specified flow + +Can see in the bottom left your user account. + +### Formatting + +The extension allows you to: + +- format individual documents explicitely +- format individual documents on save +- format a whole project + +### Linting + +The extension allows you to see linting errors and warnings in line. + From beb24f01561247d7a223ccc8bdb433b295dd8ee5 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 20 Apr 2025 09:45:12 +0100 Subject: [PATCH 0010/1056] feat: add device flow to vscode extension (#4189) --- vscode/extension/package.json | 5 + vscode/extension/src/auth/auth.ts | 275 +++++++++++++----- vscode/extension/src/commands/signin.ts | 13 +- .../src/commands/signinSpecifyFlow.ts | 33 +++ vscode/extension/src/extension.ts | 6 + .../extension/src/utilities/isCodespaces.ts | 9 + 6 files changed, 267 insertions(+), 74 deletions(-) create mode 100644 vscode/extension/src/commands/signinSpecifyFlow.ts create mode 100644 vscode/extension/src/utilities/isCodespaces.ts diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 6b0500c29b..1af179b455 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -41,6 +41,11 @@ "title": "Sign in to Tobiko Cloud", "description": "SQLMesh" }, + { + "command": "sqlmesh.signinSpecifyFlow", + "title": "Sign in to Tobiko Cloud (Specify Auth Flow)", + "description": "SQLMesh" + }, { "command": "sqlmesh.signout", "title": "Sign out from Tobiko Cloud", diff --git a/vscode/extension/src/auth/auth.ts b/vscode/extension/src/auth/auth.ts index 461ec6cbf3..e4c7159e07 100644 --- a/vscode/extension/src/auth/auth.ts +++ b/vscode/extension/src/auth/auth.ts @@ -19,13 +19,13 @@ export const AUTH_TYPE = "tobikodata" export const AUTH_NAME = "Tobiko" const tokenSchema = z.object({ - iss: z.string(), - aud: z.string(), - sub: z.string(), - scope: z.string(), - iat: z.number(), - exp: z.number(), - email: z.string(), + iss: z.string(), + aud: z.string(), + sub: z.string(), + scope: z.string(), + iat: z.number(), + exp: z.number(), + email: z.string(), }) const statusResponseSchema = z.object({ is_logged_in: z.boolean(), @@ -39,6 +39,14 @@ const loginUrlResponseSchema = z.object({ verifier_code: z.string(), }) +const deviceCodeResponseSchema = z.object({ + device_code: z.string(), + user_code: z.string(), + verification_uri: z.string(), + verification_uri_complete: z.string(), + expires_in: z.number(), +}) + export class AuthenticationProviderTobikoCloud implements AuthenticationProvider { @@ -91,8 +99,8 @@ export class AuthenticationProviderTobikoCloud const session = { id: token.email, account: { - id: token.email, - label: "Tobiko", + id: token.sub, + label: token.email, }, scopes: token.scope.split(" "), accessToken: "", @@ -101,6 +109,60 @@ export class AuthenticationProviderTobikoCloud } async createSession(): Promise { + await this.sign_in_oauth_flow() + const status = await this.get_status() + if (isErr(status)) { + throw new Error("Failed to get tcloud auth status") + } + const statusResponse = status.value + if (!statusResponse.is_logged_in) { + throw new Error("Failed to login to tcloud") + } + const token = statusResponse.id_token + const session: AuthenticationSession = { + id: token.email, + account: { + id: token.email, + label: "Tobiko", + }, + scopes: token.scope.split(" "), + accessToken: "", + } + this._sessionChangeEmitter.fire({ + added: [session], + removed: [], + changed: [], + }) + return session + } + + async removeSession(): Promise { + // Get current sessions before logging out + const currentSessions = await this.getSessions() + const tcloudBin = await get_tcloud_bin() + const workspacePath = await getProjectRoot() + if (isErr(tcloudBin)) { + throw new Error("Failed to get tcloud bin") + } + const tcloudBinPath = tcloudBin.value + const result = await execAsync(tcloudBinPath, ["auth", "logout"], { + cwd: workspacePath.uri.fsPath, + }) + if (result.exitCode !== 0) { + throw new Error("Failed to logout from tcloud") + } + + // Emit event with the actual sessions that were removed + if (currentSessions.length > 0) { + this._sessionChangeEmitter.fire({ + added: [], + removed: currentSessions, + changed: [], + }) + } + } + + async sign_in_oauth_flow(): Promise { const workspacePath = await getProjectRoot() const tcloudBin = await get_tcloud_bin() if (isErr(tcloudBin)) { @@ -117,83 +179,156 @@ export class AuthenticationProviderTobikoCloud if (result.exitCode !== 0) { throw new Error("Failed to get tcloud login url") } - const resultToJson = JSON.parse(result.stdout) - const urlCode = loginUrlResponseSchema.parse(resultToJson) - const url = urlCode.url - const ac = new AbortController() - const timeout = setTimeout(() => ac.abort(), 1000 * 60 * 5) - const backgroundServerForLogin = execAsync( - tcloudBinPath, - ["auth", "vscode", "start-server", urlCode.verifier_code], - { - cwd: workspacePath.uri.fsPath, - signal: ac.signal, + try { + const resultToJson = JSON.parse(result.stdout) + const urlCode = loginUrlResponseSchema.parse(resultToJson) + const url = urlCode.url + + if (!url) { + throw new Error("Invalid login URL received") } - ) - const messageResult = await window.showInformationMessage( - "Please login to Tobiko Cloud", - { - modal: true, - }, - "Sign in with browser", - "Cancel" - ) + const ac = new AbortController() + const timeout = setTimeout(() => ac.abort(), 1000 * 60 * 5) + const backgroundServerForLogin = execAsync( + tcloudBinPath, + ["auth", "vscode", "start-server", urlCode.verifier_code], + { + cwd: workspacePath.uri.fsPath, + signal: ac.signal, + } + ) - if (messageResult === "Sign in with browser") { - await env.openExternal(Uri.parse(url)) - } - if (messageResult === "Cancel") { - ac.abort() - throw new Error("Login cancelled") - } + const messageResult = await window.showInformationMessage( + "Please login to Tobiko Cloud", + { + modal: true, + }, + "Sign in with browser", + "Cancel" + ) - try { - const output = await backgroundServerForLogin - if (output.exitCode !== 0) { - throw new Error(`Failed to start server: ${output.stderr}`) + if (messageResult === "Sign in with browser") { + await env.openExternal(Uri.parse(url)) + } else { + // Always abort the server if not proceeding with sign in + ac.abort() + clearTimeout(timeout) + if (messageResult === "Cancel") { + throw new Error("Login cancelled") + } + return } - } catch (error) { - traceError(`Server error: ${error}`) - throw error - } - clearTimeout(timeout) - - const status = await this.get_status() - if (isErr(status)) { - throw new Error("Failed to get tcloud auth status") - } - const statusResponse = status.value - if (!statusResponse.is_logged_in) { - throw new Error("Failed to login to tcloud") - } - const scopes = statusResponse.id_token.scope.split(" ") - const session: AuthenticationSession = { - id: AuthenticationProviderTobikoCloud.id, - account: { - id: AuthenticationProviderTobikoCloud.id, - label: "Tobiko", - }, - scopes: scopes, - accessToken: "" + try { + const output = await backgroundServerForLogin + if (output.exitCode !== 0) { + throw new Error( + `Failed to complete authentication: ${output.stderr}` + ) + } + // Get updated session and notify about the change + const sessions = await this.getSessions() + if (sessions.length > 0) { + this._sessionChangeEmitter.fire({ + added: sessions, + removed: [], + changed: [], + }) + } + } catch (error) { + if (error instanceof Error && error.name === "AbortError") { + throw new Error("Authentication timeout or aborted") + } + traceError(`Server error: ${error}`) + throw error + } finally { + clearTimeout(timeout) + } + } catch (error) { + if (error instanceof Error && error.message === "Login cancelled") { + throw error + } + traceError(`Authentication flow error: ${error}`) + throw new Error("Failed to complete authentication flow") } - return session } - async removeSession(): Promise { - const tcloudBin = await get_tcloud_bin() + async sign_in_device_flow(): Promise { const workspacePath = await getProjectRoot() + const tcloudBin = await get_tcloud_bin() if (isErr(tcloudBin)) { throw new Error("Failed to get tcloud bin") } const tcloudBinPath = tcloudBin.value - const result = await execAsync(tcloudBinPath, ["auth", "logout"], { - cwd: workspacePath.uri.fsPath, - }) + const result = await execAsync( + tcloudBinPath, + ["auth", "vscode", "device"], + { + cwd: workspacePath.uri.fsPath, + } + ) if (result.exitCode !== 0) { - throw new Error("Failed to logout from tcloud") + throw new Error("Failed to get device code") + } + + try { + const resultToJson = JSON.parse(result.stdout) + const deviceCodeResponse = deviceCodeResponseSchema.parse(resultToJson) + + const ac = new AbortController() + const timeout = setTimeout(() => ac.abort(), 1000 * 60 * 5) + const waiting = execAsync( + tcloudBinPath, + ["auth", "vscode", "poll_device", deviceCodeResponse.device_code], + { + cwd: workspacePath.uri.fsPath, + signal: ac.signal, + } + ) + + const messageResult = await window.showInformationMessage( + `Confirm the code ${deviceCodeResponse.user_code} at ${deviceCodeResponse.verification_uri}`, + { + modal: true, + }, + "Open browser", + "Cancel" + ) + + if (messageResult === "Open browser") { + await env.openExternal(Uri.parse(deviceCodeResponse.verification_uri_complete)) + } + if (messageResult === "Cancel") { + ac.abort() + throw new Error("Login cancelled") + } + + try { + const output = await waiting + if (output.exitCode !== 0) { + throw new Error(`Failed to authenticate: ${output.stderr}`) + } + + // Get updated session and notify about the change + const sessions = await this.getSessions() + if (sessions.length > 0) { + this._sessionChangeEmitter.fire({ + added: sessions, + removed: [], + changed: [], + }) + } + } catch (error) { + traceError(`Authentication error: ${error}`) + throw error + } finally { + clearTimeout(timeout) + } + } catch (error) { + traceError(`JSON parsing error: ${error}`) + throw new Error("Failed to parse device code response") } } } diff --git a/vscode/extension/src/commands/signin.ts b/vscode/extension/src/commands/signin.ts index 0620ffdaa0..d560b05a57 100644 --- a/vscode/extension/src/commands/signin.ts +++ b/vscode/extension/src/commands/signin.ts @@ -1,8 +1,13 @@ import { AuthenticationProviderTobikoCloud } from "../auth/auth" import * as vscode from "vscode" +import { isCodespaces } from "../utilities/isCodespaces" - -export const signIn = (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { - await authenticationProvider.createSession() +export const signIn = + (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { + if (isCodespaces()) { + await authenticationProvider.sign_in_device_flow() + } else { + await authenticationProvider.createSession() + } await vscode.window.showInformationMessage("Signed in successfully") -} \ No newline at end of file + } diff --git a/vscode/extension/src/commands/signinSpecifyFlow.ts b/vscode/extension/src/commands/signinSpecifyFlow.ts new file mode 100644 index 0000000000..a77930af96 --- /dev/null +++ b/vscode/extension/src/commands/signinSpecifyFlow.ts @@ -0,0 +1,33 @@ +import { AuthenticationProviderTobikoCloud } from "../auth/auth" +import { traceInfo } from "../utilities/common/log" +import { window } from "vscode" + +export const signInSpecifyFlow = (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { + traceInfo("Sign in specify flow") + const flowOptions = [ + { label: "OAuth Flow", description: "Sign in using OAuth flow in your browser" }, + { label: "Device Flow", description: "Sign in using a device code" } + ] + const selectedFlow = await window.showQuickPick(flowOptions, { + placeHolder: "Select authentication flow method", + ignoreFocusOut: true + }) + if (!selectedFlow) { + traceInfo("Sign in cancelled by user") + return + } + if (selectedFlow.label === "OAuth Flow") { + await authenticationProvider.sign_in_oauth_flow() + await authenticationProvider.getSessions() + await window.showInformationMessage("Sign in success") + return + } else if (selectedFlow.label === "Device Flow") { + await authenticationProvider.sign_in_device_flow() + await authenticationProvider.getSessions() + await window.showInformationMessage("Sign in success") + return + } else { + traceInfo("Invalid flow selected") + return + } +} \ No newline at end of file diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index de58050ccf..7a936720f6 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -17,6 +17,7 @@ import { LSPClient } from "./lsp/lsp" import { AuthenticationProviderTobikoCloud } from "./auth/auth" import { signOut } from "./commands/signout" import { signIn } from "./commands/signin" +import { signInSpecifyFlow } from "./commands/signinSpecifyFlow" let lspClient: LSPClient | undefined @@ -46,6 +47,11 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("sqlmesh.signin", signIn(authProvider)) ) + context.subscriptions.push( + vscode.commands.registerCommand("sqlmesh.signinSpecifyFlow", signInSpecifyFlow(authProvider) + ) + ) + context.subscriptions.push( vscode.commands.registerCommand("sqlmesh.signout", signOut(authProvider)) ) diff --git a/vscode/extension/src/utilities/isCodespaces.ts b/vscode/extension/src/utilities/isCodespaces.ts new file mode 100644 index 0000000000..db908e8023 --- /dev/null +++ b/vscode/extension/src/utilities/isCodespaces.ts @@ -0,0 +1,9 @@ +/** + * isCodespaces checks if the current environment is a Codespaces + * + * @returns true if the current environment is a Codespaces, false otherwise + */ +export const isCodespaces = () => { + return process.env.CODESPACES === 'true' || + !!process.env.GITHUB_CODESPACE_TOKEN +} \ No newline at end of file From 7a2275766c5693992d3a7d2a4647eca339f00fe6 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 20 Apr 2025 17:56:44 +0100 Subject: [PATCH 0011/1056] ci(vscode): fix vscode job (#4192) --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 426165a09b..58b6d11cc5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,7 +22,7 @@ orbs: jobs: vscode-extension-setup: docker: - - image: cimg/node:20.19.0 + - image: cimg/node:20.19.0-browsers resource_class: small steps: - checkout @@ -130,4 +130,4 @@ workflows: - trigger_private_renovate: <<: *on_tag_filter requires: - - publish \ No newline at end of file + - publish From dbf69c0c5074932d3478163ba5bbbdb39af7ac00 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 20 Apr 2025 18:58:56 +0100 Subject: [PATCH 0012/1056] fix(vscode): improve naming of lsp channel (#4193) --- vscode/extension/src/lsp/lsp.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 79651310ff..6085b4a192 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -16,7 +16,7 @@ export class LSPClient implements Disposable { public async start(): Promise> { if (!outputChannel) { - outputChannel = window.createOutputChannel('sqlmesh_actual_lsp_implementation') + outputChannel = window.createOutputChannel('sqlmesh-lsp') } const sqlmesh = await sqlmesh_lsp_exec() @@ -62,7 +62,7 @@ export class LSPClient implements Disposable { // } } - this.client = new LanguageClient('sqlmesh-lsp-example', 'SQLMesh Language Server', serverOptions, clientOptions) + this.client = new LanguageClient('sqlmesh-lsp', 'SQLMesh Language Server', serverOptions, clientOptions) await this.client.start() return ok(undefined) } From 29714e0c057e44db65dea33d3c87e7ccb8999040 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 20 Apr 2025 18:59:04 +0100 Subject: [PATCH 0013/1056] fix(vscode): force extension to run on host (#4194) --- vscode/extension/package.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 1af179b455..bcdd39c6e3 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -17,6 +17,9 @@ "activationEvents": [ "onLanguage:sql" ], + "extensionKind": [ + "workspace" + ], "main": "./dist/extension.js", "contributes": { "authentication": [ @@ -42,9 +45,9 @@ "description": "SQLMesh" }, { - "command": "sqlmesh.signinSpecifyFlow", - "title": "Sign in to Tobiko Cloud (Specify Auth Flow)", - "description": "SQLMesh" + "command": "sqlmesh.signinSpecifyFlow", + "title": "Sign in to Tobiko Cloud (Specify Auth Flow)", + "description": "SQLMesh" }, { "command": "sqlmesh.signout", From f01a5a46ce19c107af70922b21180e826a8a3d31 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 20 Apr 2025 19:26:06 +0100 Subject: [PATCH 0014/1056] feat(vscode): add icon to extension (#4195) --- vscode/extension/.vscodeignore | 1 + vscode/extension/assets/logo.png | Bin 0 -> 17615 bytes vscode/extension/assets/logo.svg | 9 +++++++++ vscode/extension/package.json | 1 + 4 files changed, 11 insertions(+) create mode 100644 vscode/extension/assets/logo.png create mode 100644 vscode/extension/assets/logo.svg diff --git a/vscode/extension/.vscodeignore b/vscode/extension/.vscodeignore index d255964eae..6c3f539892 100644 --- a/vscode/extension/.vscodeignore +++ b/vscode/extension/.vscodeignore @@ -12,3 +12,4 @@ vsc-extension-quickstart.md **/*.map **/*.ts **/.vscode-test.* +assets/logo.svg diff --git a/vscode/extension/assets/logo.png b/vscode/extension/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..95108a963d433ea442d339eb11e4f16128f2f68d GIT binary patch literal 17615 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 JF6*2UngByh4Cw#> literal 0 HcmV?d00001 diff --git a/vscode/extension/assets/logo.svg b/vscode/extension/assets/logo.svg new file mode 100644 index 0000000000..66cc27fc25 --- /dev/null +++ b/vscode/extension/assets/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/vscode/extension/package.json b/vscode/extension/package.json index bcdd39c6e3..88c65e6a77 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -8,6 +8,7 @@ "type": "git", "url": "https://github.com/tobikodata/sqlmesh" }, + "icon": "assets/logo.png", "engines": { "vscode": "^1.98.0" }, From 7f00f3d193a7a109301698fc52bd828fb74646a2 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 20 Apr 2025 20:07:22 +0100 Subject: [PATCH 0015/1056] chore(vscode): not pacakge esbuild.js file (#4196) --- vscode/extension/.vscodeignore | 1 + 1 file changed, 1 insertion(+) diff --git a/vscode/extension/.vscodeignore b/vscode/extension/.vscodeignore index 6c3f539892..28abe96f07 100644 --- a/vscode/extension/.vscodeignore +++ b/vscode/extension/.vscodeignore @@ -13,3 +13,4 @@ vsc-extension-quickstart.md **/*.ts **/.vscode-test.* assets/logo.svg +esbuild.js From ddd7f83e4bf938470fa4fe6eb3c64ad812324f7d Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 20 Apr 2025 20:57:48 +0100 Subject: [PATCH 0016/1056] fix(vscode): specify python requirement (#4197) --- vscode/extension/package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 88c65e6a77..b8ebb7d409 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -8,6 +8,7 @@ "type": "git", "url": "https://github.com/tobikodata/sqlmesh" }, + "main": "./dist/extension.js", "icon": "assets/logo.png", "engines": { "vscode": "^1.98.0" @@ -21,7 +22,9 @@ "extensionKind": [ "workspace" ], - "main": "./dist/extension.js", + "extensionDependencies": [ + "ms-python.python" + ], "contributes": { "authentication": [ { From 5a8e2302d29f17baaf13c482d655a2f7d23c7e71 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:34:25 +0300 Subject: [PATCH 0017/1056] Chore(vscode): Fix typo in readme of extension (#4199) --- vscode/extension/README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/vscode/extension/README.md b/vscode/extension/README.md index 3a1d2ab40c..f84ce74191 100644 --- a/vscode/extension/README.md +++ b/vscode/extension/README.md @@ -2,27 +2,27 @@ ## Functionality -The following section outlines all the functionality in the Visual Studio Code extension. It is broken up into logical sections. +The following section outlines all the functionality in the Visual Studio Code extension. It is broken up into logical sections. -### Authentication +### Authentication -The extension allows you to manage your tcloud session within visual studio code +The extension allows you to manage your tcloud session within Visual Studio Code. -- Sign in -- Sign out -- Sign in with specified flow +- Sign in +- Sign out +- Sign in with a specified flow -Can see in the bottom left your user account. +You can see your user account in the bottom left. -### Formatting +### Formatting The extension allows you to: -- format individual documents explicitely -- format individual documents on save -- format a whole project +- Format individual documents explicitly +- Format individual documents on save +- Format a whole project -### Linting +### Linting -The extension allows you to see linting errors and warnings in line. +The extension allows you to see linting errors and warnings inline. From 3321a2af08e4f2e858c4246ea1e64331a63d08e4 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 21 Apr 2025 15:23:06 +0100 Subject: [PATCH 0018/1056] refactor(vscode): share is python model present code (#4200) --- vscode/extension/src/utilities/python.ts | 40 +++++++++++++++++++ .../src/utilities/sqlmesh/sqlmesh.ts | 35 +--------------- 2 files changed, 42 insertions(+), 33 deletions(-) create mode 100644 vscode/extension/src/utilities/python.ts diff --git a/vscode/extension/src/utilities/python.ts b/vscode/extension/src/utilities/python.ts new file mode 100644 index 0000000000..154fe3cba8 --- /dev/null +++ b/vscode/extension/src/utilities/python.ts @@ -0,0 +1,40 @@ +import { getInterpreterDetails } from "./common/python" +import { err, ok, Result } from "./functional/result" +import { traceInfo } from "./common/log" +import { promisify } from "util" +import { execFile } from "child_process" + +/** isPythonModuleInstallled returns true if the given python module is installed. + * + * @param moduleName - The name of the python module to check. + * @returns True if the module is installed, false otherwise. + */ +export const isPythonModuleInstalled = async (moduleName: string): Promise> => { + const interpreterDetails = await getInterpreterDetails() + if (!interpreterDetails.path) { + return err("No Python interpreter found") + } + const pythonPath = interpreterDetails.path[0] + const checkScript = ` +import sys +if sys.version_info >= (3, 12): + from importlib import metadata +else: + import importlib_metadata as metadata + +try: + metadata.version('${moduleName}') + print("true") +except metadata.PackageNotFoundError: + print("false") +` + try { + const execFileAsync = promisify(execFile) + const { stdout } = await execFileAsync(pythonPath, ["-c", checkScript]) + const isInstalled = stdout.trim() === "true" + traceInfo(`${moduleName} is installed: ${isInstalled}`) + + return ok(stdout.trim() === "true") + } catch (error) { + return err(`Failed to check tcloud installation: ${error}`) + }} diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index aca7ecf399..95818159ef 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -5,6 +5,7 @@ import { Result, err, isErr, ok } from "../functional/result" import { getProjectRoot } from "../common/utilities" import { execFile } from "child_process" import { promisify } from "util" +import { isPythonModuleInstalled } from "../python" export type sqlmesh_exec = { @@ -22,39 +23,7 @@ export type sqlmesh_exec = { export const is_tcloud_installed = async (): Promise< Result > => { - const interpreterDetails = await getInterpreterDetails() - if (!interpreterDetails.path) { - return err("No Python interpreter found") - } - traceVerbose( - `Using interpreter from Python extension: ${interpreterDetails.path.join( - " " - )}` - ) - - const pythonPath = interpreterDetails.path[0] - const checkScript = ` -import sys -if sys.version_info >= (3, 12): - from importlib import metadata -else: - import importlib_metadata as metadata - -try: - metadata.version('tcloud') - print("true") -except metadata.PackageNotFoundError: - print("false") -` - try { - const execFileAsync = promisify(execFile) - traceVerbose(`Checking tcloud installation with script: ${checkScript}`) - const { stdout } = await execFileAsync(pythonPath, ["-c", checkScript]) - traceVerbose(`tcloud installation check result: ${stdout.trim()}`) - return ok(stdout.trim() === "true") - } catch (error) { - return err(`Failed to check tcloud installation: ${error}`) - } + return isPythonModuleInstalled("tcloud") } /** From d49e1f3b78ada369d2e01b3243fa3c1899938b07 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 21 Apr 2025 15:34:03 +0100 Subject: [PATCH 0019/1056] feat(vscode): improve tcloud project detection (#4201) --- .../src/utilities/sqlmesh/sqlmesh.ts | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 95818159ef..e04569925b 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -6,7 +6,7 @@ import { getProjectRoot } from "../common/utilities" import { execFile } from "child_process" import { promisify } from "util" import { isPythonModuleInstalled } from "../python" - +import fs from "fs" export type sqlmesh_exec = { workspacePath: string; @@ -16,13 +16,18 @@ export type sqlmesh_exec = { }; /** - * Check if tcloud is installed in the current Python environment. + * Returns true if the current project is a Tcloud project. To detect this we, + * 1. Check if the project has a tcloud.yaml file in the project root. If it does, we assume it's a Tcloud project. + * 2. Check if the project has tcloud installed in the Python environment. * * @returns A Result indicating whether tcloud is installed. */ -export const is_tcloud_installed = async (): Promise< - Result -> => { +export const isTcloudProject = async (): Promise> => { + const projectRoot = await getProjectRoot() + const tcloudYamlPath = path.join(projectRoot.uri.fsPath, "tcloud.yaml") + if (fs.existsSync(tcloudYamlPath)) { + return ok(true) + } return isPythonModuleInstalled("tcloud") } @@ -60,7 +65,7 @@ export const sqlmesh_exec = async (): Promise> => { } if (interpreterDetails.isVirtualEnvironment) { traceLog("Using virtual environment") - const tcloudInstalled = await is_tcloud_installed() + const tcloudInstalled = await isTcloudProject() if (isErr(tcloudInstalled)) { return tcloudInstalled } @@ -77,7 +82,7 @@ export const sqlmesh_exec = async (): Promise> => { VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), PATH: interpreterDetails.binPath!, }, - args:[], + args: [], }) } const binPath = path.join(interpreterDetails.binPath!, "sqlmesh") @@ -123,7 +128,7 @@ export const sqlmesh_lsp_exec = async (): Promise< } if (interpreterDetails.isVirtualEnvironment) { traceLog("Using virtual environment") - const tcloudInstalled = await is_tcloud_installed() + const tcloudInstalled = await isTcloudProject() if (isErr(tcloudInstalled)) { return tcloudInstalled } @@ -137,9 +142,9 @@ export const sqlmesh_lsp_exec = async (): Promise< await execFileAsync(tcloudBin.value, ["install_sqlmesh"], { cwd: workspacePath, env: { - PYTHONPATH: interpreterDetails.path?.[0], - VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), - PATH: interpreterDetails.binPath!, + PYTHONPATH: interpreterDetails.path?.[0], + VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), + PATH: interpreterDetails.binPath!, }, }) } From 6559a0029642c0fe8afc0cd765d7a7e48d9ea92e Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:45:52 +0100 Subject: [PATCH 0020/1056] refactor(vscode): moving format code into function (#4204) --- vscode/extension/src/commands/format.ts | 49 ++++++++++++++----------- vscode/extension/src/extension.ts | 22 +++-------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/vscode/extension/src/commands/format.ts b/vscode/extension/src/commands/format.ts index 6202ffc008..ebedd4c45b 100644 --- a/vscode/extension/src/commands/format.ts +++ b/vscode/extension/src/commands/format.ts @@ -1,29 +1,36 @@ import { traceError, traceLog } from "../utilities/common/log" -import { execSync } from 'child_process' +import { execSync } from "child_process" import { sqlmesh_exec } from "../utilities/sqlmesh/sqlmesh" import { isErr } from "../utilities/functional/result" +import * as vscode from "vscode" -interface CLICallout { - format: () => Promise +export const format = async () => { + traceLog("Calling format") + const out = await internalFormat() + if (out === 0) { + vscode.window.showInformationMessage("Project formatted successfully") + } else { + vscode.window.showErrorMessage("Project format failed") + } } -export const actual_callout: CLICallout = { - format: async () => { - traceLog("Calling format") - try { - const exec = await sqlmesh_exec() - if (isErr(exec)) { - traceError(exec.error) - return 1 - } - execSync(`${exec.value.bin} format`, { encoding: 'utf-8', cwd: exec.value.workspacePath, env: exec.value.env }) - return 0 - } catch (error: any) { - traceError('Error executing sqlmesh format:', error.message) - traceError(error.stdout) - traceError(error.stderr) - return error.status || 1 - } +const internalFormat = async (): Promise => { + try { + const exec = await sqlmesh_exec() + if (isErr(exec)) { + traceError(exec.error) + return 1 } + execSync(`${exec.value.bin} format`, { + encoding: "utf-8", + cwd: exec.value.workspacePath, + env: exec.value.env, + }) + return 0 + } catch (error: any) { + traceError("Error executing sqlmesh format:", error.message) + traceError(error.stdout) + traceError(error.stderr) + return error.status || 1 + } } - diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 7a936720f6..d7fa8840d3 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -1,7 +1,5 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below import * as vscode from "vscode" -import { actual_callout } from "./commands/format" +import { format } from "./commands/format" import { createOutputChannel, onDidChangeConfiguration, @@ -48,23 +46,16 @@ export async function activate(context: vscode.ExtensionContext) { ) context.subscriptions.push( - vscode.commands.registerCommand("sqlmesh.signinSpecifyFlow", signInSpecifyFlow(authProvider) + vscode.commands.registerCommand( + "sqlmesh.signinSpecifyFlow", + signInSpecifyFlow(authProvider) ) ) - context.subscriptions.push( vscode.commands.registerCommand("sqlmesh.signout", signOut(authProvider)) ) - context.subscriptions.push( - vscode.commands.registerCommand("sqlmesh.format", async () => { - const out = await actual_callout.format() - if (out === 0) { - vscode.window.showInformationMessage("Project formatted successfully") - } else { - vscode.window.showErrorMessage("Project format failed") - } - }) + vscode.commands.registerCommand("sqlmesh.format", format) ) lspClient = new LSPClient() @@ -77,12 +68,11 @@ export async function activate(context: vscode.ExtensionContext) { await lspClient.restart() } } - context.subscriptions.push( onDidChangePythonInterpreter(async () => { await restart() }), - onDidChangeConfiguration(async (_: vscode.ConfigurationChangeEvent) => { + onDidChangeConfiguration(async () => { await restart() }), registerCommand(`sqlmesh.restart`, async () => { From effeba3d49247b90ff3c387fd463e2d52b2f8b9f Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 21 Apr 2025 08:48:20 -0700 Subject: [PATCH 0021/1056] Fix: Columns should be sourced from the target table and not the temporary merge table (#4191) --- sqlmesh/core/engine_adapter/bigquery.py | 2 +- tests/core/engine_adapter/test_bigquery.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 12338d4a52..e60e606a36 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -611,7 +611,7 @@ def insert_overwrite_by_partition( if columns_to_types is None or columns_to_types[ partition_column.name ] == exp.DataType.build("unknown"): - columns_to_types = self.columns(temp_table_name) + columns_to_types = self.columns(table_name) partition_type_sql = columns_to_types[partition_column.name].sql(dialect=self.dialect) diff --git a/tests/core/engine_adapter/test_bigquery.py b/tests/core/engine_adapter/test_bigquery.py index 048a23cac0..fd5499bf81 100644 --- a/tests/core/engine_adapter/test_bigquery.py +++ b/tests/core/engine_adapter/test_bigquery.py @@ -116,9 +116,7 @@ def test_insert_overwrite_by_partition_query_unknown_column_types( }, ) - columns_mock.assert_called_once_with( - exp.to_table(f"test_schema.__temp_test_table_{temp_table_id}") - ) + columns_mock.assert_called_once_with(table_name) sql_calls = _to_sql_calls(execute_mock) assert sql_calls == [ From 1473bc61828919e51280404f543cebc938a4c061 Mon Sep 17 00:00:00 2001 From: Toby Mao Date: Mon, 21 Apr 2025 08:52:49 -0700 Subject: [PATCH 0022/1056] feat: add the ability to check intervals (#4187) --- docs/guides/signals.md | 16 ++++++++++ docs/reference/cli.md | 20 ++++++++++++- docs/reference/notebook.md | 25 ++++++++++++++-- mkdocs.yml | 1 + sqlmesh/cli/main.py | 36 ++++++++++++++++++++++ sqlmesh/core/console.py | 25 ++++++++++++++++ sqlmesh/core/context.py | 61 ++++++++++++++++++++++++++++++++++++-- sqlmesh/magics.py | 32 ++++++++++++++++++++ tests/core/test_context.py | 33 +++++++++++++++++++++ 9 files changed, 244 insertions(+), 5 deletions(-) diff --git a/docs/guides/signals.md b/docs/guides/signals.md index bc76d9d655..693594a51b 100644 --- a/docs/guides/signals.md +++ b/docs/guides/signals.md @@ -131,3 +131,19 @@ from sqlmesh import signal, DatetimeRanges, ExecutionContext def one_week_ago(batch: DatetimeRanges, context: ExecutionContext) -> t.Union[bool, DatetimeRanges]: return len(context.engine_adapter.fetchdf("SELECT 1")) > 1 ``` + +### Testing Signals +Signals only evaluate on `run` or with `check_intervals`. + +To test signals with the [check_intervals](../reference/cli.md#check_intervals) command: + +1. Deploy your changes to an environment with `sqlmesh plan my_dev`. +2. Run `sqlmesh check_intervals my_dev`. + + * To check a subset of models use the --select-model flag. + * To turn off signals and just check missing intervals, use the --no-signals flag. + +3. To iterate, make changes to the signal, and redeploy with step 1. + +!!! note + `check_intervals` only works on remote models in an environment. Local signal changes are never run. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index dd958d17f8..35586b9573 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -65,6 +65,24 @@ Options: --help Show this message and exit. ``` +## check_intervals + +``` +Usage: sqlmesh check_intervals [OPTIONS] [ENVIRONMENT] + + Show missing intervals in an environment, respecting signals. + +Options: + --no-signals Disable signal checks and only show missing intervals. + --select-model TEXT Select specific models to show missing intervals for. + -s, --start TEXT The start datetime of the interval for which this + command will be applied. + -e, --end TEXT The end datetime of the interval for which this command + will be applied. + --help Show this message and exit. +``` + + ## clean ``` @@ -584,4 +602,4 @@ Options: --model TEXT A model to lint. Multiple models can be linted. If no models are specified, every model will be linted. --help Show this message and exit. -``` \ No newline at end of file +``` diff --git a/docs/reference/notebook.md b/docs/reference/notebook.md index 50564be34a..60a7e16e7a 100644 --- a/docs/reference/notebook.md +++ b/docs/reference/notebook.md @@ -438,6 +438,27 @@ options: Execution time. ``` +#### check_intervals +``` +%check_intervals [--no-signals] [--select-model [SELECT_MODEL ...]] + [--start START] [--end END] + [environment] + +Show missing intervals in an environment, respecting signals. + +positional arguments: + environment The environment to check intervals for. + +options: + --no-signals Disable signal checks and only show missing intervals. + --select-model <[SELECT_MODEL ...]> + Select specific model changes that should be included + in the plan. + --start START, -s START + Start date of intervals to check for. + --end END, -e END End date of intervals to check for. +``` + #### rollback ``` %rollback @@ -499,10 +520,10 @@ options: #### lint ``` -%lint [--models ...] +%lint [--models ...] Run the linter on the target models(s) positional arguments: --models A model to lint. Multiple models can be linted. If no models are specified, every model will be linted. -``` \ No newline at end of file +``` diff --git a/mkdocs.yml b/mkdocs.yml index 212bb49535..f6858b6646 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -36,6 +36,7 @@ nav: - guides/observer.md - Advanced usage: - guides/customizing_sqlmesh.md + - guides/signals.md - Concepts: - concepts/overview.md - Development: diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 50ee33e85c..0fbf2f8817 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -693,6 +693,42 @@ def audit( obj.audit(models=models, start=start, end=end, execution_time=execution_time) +@cli.command("check_intervals") +@click.option( + "--no-signals", + is_flag=True, + help="Disable signal checks and only show missing intervals.", + default=False, +) +@click.argument("environment", required=False) +@click.option( + "--select-model", + type=str, + multiple=True, + help="Select specific models to show missing intervals for.", +) +@opt.start_time +@opt.end_time +@click.pass_context +@error_handler +@cli_analytics +def check_intervals( + ctx: click.Context, + environment: t.Optional[str], + no_signals: bool, + select_model: t.List[str], + start: TimeLike, + end: TimeLike, +) -> None: + """Show missing intervals in an environment, respecting signals.""" + context = ctx.obj + context.console.show_intervals( + context.check_intervals( + environment, no_signals=no_signals, select_models=select_model, start=start, end=end + ) + ) + + @cli.command("fetchdf") @click.argument("sql") @click.pass_context diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index abf7916a0e..6991e902b3 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -191,6 +191,10 @@ class EnvironmentsConsole(abc.ABC): def print_environments(self, environments_summary: t.Dict[str, int]) -> None: """Prints all environment names along with expiry datetime.""" + @abc.abstractmethod + def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None: + """Show ready intervals""" + class DifferenceConsole(abc.ABC): """Console for displaying environment differences""" @@ -658,6 +662,9 @@ def show_row_diff( def print_environments(self, environments_summary: t.Dict[str, int]) -> None: pass + def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None: + pass + def show_linter_violations( self, violations: t.List[RuleViolation], model: Model, is_error: bool = False ) -> None: @@ -2091,6 +2098,24 @@ def print_environments(self, environments_summary: t.Dict[str, int]) -> None: output_str = "\n".join([str(len(output)), *output]) self.log_status_update(f"Number of SQLMesh environments are: {output_str}") + def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None: + complete = Tree(f"[b]Complete Intervals[/b]") + incomplete = Tree(f"[b]Missing Intervals[/b]") + + for snapshot, intervals in sorted(snapshot_intervals.items(), key=lambda s: s[0].node.name): + if intervals.intervals: + incomplete.add( + f"{snapshot.node.name}: [{intervals.format_intervals(snapshot.node.interval_unit)}]" + ) + else: + complete.add(snapshot.node.name) + + if complete.children: + self._print(complete) + + if incomplete.children: + self._print(incomplete) + def print_connection_config(self, config: ConnectionConfig, title: str = "Connection") -> None: tree = Tree(f"[b]{title}:[/b]") tree.add(f"Type: [bold cyan]{config.type_}[/bold cyan]") diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 0aee177078..81b3042585 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -87,7 +87,7 @@ NotificationTarget, NotificationTargetManager, ) -from sqlmesh.core.plan import Plan, PlanBuilder +from sqlmesh.core.plan import Plan, PlanBuilder, SnapshotIntervals from sqlmesh.core.reference import ReferenceGraph from sqlmesh.core.scheduler import Scheduler, CompletionStatus from sqlmesh.core.schema_loader import create_external_models_file @@ -97,6 +97,7 @@ Snapshot, SnapshotEvaluator, SnapshotFingerprint, + missing_intervals, to_table_mapping, ) from sqlmesh.core.snapshot.definition import get_next_model_interval_start @@ -469,11 +470,12 @@ def execution_context( self, deployability_index: t.Optional[DeployabilityIndex] = None, engine_adapter: t.Optional[EngineAdapter] = None, + snapshots: t.Optional[t.Dict[str, Snapshot]] = None, ) -> ExecutionContext: """Returns an execution context.""" return ExecutionContext( engine_adapter=engine_adapter or self.engine_adapter, - snapshots=self.snapshots, + snapshots=snapshots or self.snapshots, deployability_index=deployability_index, default_dialect=self.default_dialect, default_catalog=self.default_catalog, @@ -1893,6 +1895,61 @@ def rewrite(self, sql: str, dialect: str = "") -> exp.Expression: dialect=dialect or self.default_dialect, ) + @python_api_analytics + def check_intervals( + self, + environment: t.Optional[str], + no_signals: bool, + select_models: t.Collection[str], + start: t.Optional[TimeLike] = None, + end: t.Optional[TimeLike] = None, + ) -> t.Dict[Snapshot, SnapshotIntervals]: + """Check intervals for a given environment. + + Args: + environment: The environment or prod if None. + select_models: A list of model selection strings to show intervals for. + start: The start of the intervals to check. + end: The end of the intervals to check. + """ + + environment = environment or c.PROD + env = self.state_reader.get_environment(environment) + if not env: + raise SQLMeshError(f"Environment '{environment}' was not found.") + + snapshots = {k.name: v for k, v in self.state_sync.get_snapshots(env.snapshots).items()} + + missing = { + k.name: v + for k, v in missing_intervals( + snapshots.values(), start=start, end=end, execution_time=end + ).items() + } + + if select_models: + selected: t.Collection[str] = self._select_models_for_run( + select_models, True, snapshots.values() + ) + else: + selected = snapshots.keys() + + results = {} + execution_context = self.execution_context(snapshots=snapshots) + + for fqn in selected: + snapshot = snapshots[fqn] + intervals = missing.get(fqn) or [] + + results[snapshot] = SnapshotIntervals( + snapshot.snapshot_id, + intervals + if no_signals + else snapshot.check_ready_intervals(intervals, execution_context), + ) + + return results + @python_api_analytics def migrate(self) -> None: """Migrates SQLMesh to the current running version. diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 51f5bb1c3b..e9ffd9aaf2 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -1007,6 +1007,38 @@ def audit(self, context: Context, line: str) -> None: models=args.models, start=args.start, end=args.end, execution_time=args.execution_time ) + @magic_arguments() + @argument("environment", nargs="?", type=str, help="The environment to check intervals for.") + @argument( + "--no-signals", + action="store_true", + help="Disable signal checks and only show missing intervals.", + default=False, + ) + @argument( + "--select-model", + type=str, + nargs="*", + help="Select specific model changes that should be included in the plan.", + ) + @argument("--start", "-s", type=str, help="Start date of intervals to check for.") + @argument("--end", "-e", type=str, help="End date of intervals to check for.") + @line_magic + @pass_sqlmesh_context + def check_intervals(self, context: Context, line: str) -> None: + """Show missing intervals in an environment, respecting signals.""" + args = parse_argstring(self.check_intervals, line) + + context.console.show_intervals( + context.check_intervals( + environment=args.environment, + no_signals=args.no_signals, + select_models=args.select_model, + start=args.start, + end=args.end, + ) + ) + @magic_arguments() @argument( "--skip-connection", diff --git a/tests/core/test_context.py b/tests/core/test_context.py index f14f472b27..e12cf53ca8 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1993,3 +1993,36 @@ def test_plan_audit_intervals(tmp_path: pathlib.Path, capsys, caplog): """SELECT COUNT(*) FROM (SELECT ("date_id") AS "date_id" FROM (SELECT * FROM "sqlmesh__sqlmesh_audit"."sqlmesh_audit__date_example__4100277424" AS "sqlmesh_audit__date_example__4100277424" WHERE "date_id" BETWEEN CAST('2025-02-01' AS DATE) AND CAST('2025-02-01' AS DATE)) AS "_q_0" WHERE TRUE GROUP BY ("date_id") HAVING COUNT(*) > 1) AS "audit\"""" in caplog.text ) + + +def test_check_intervals(sushi_context, mocker): + with pytest.raises( + SQLMeshError, + match="Environment 'dev' was not found", + ): + sushi_context.check_intervals(environment="dev", no_signals=False, select_models=[]) + + spy = mocker.spy(sqlmesh.core.snapshot.definition, "_check_ready_intervals") + intervals = sushi_context.check_intervals(environment=None, no_signals=False, select_models=[]) + + min_intervals = 19 + assert spy.call_count == 1 + assert len(intervals) >= min_intervals + + for i in intervals.values(): + assert not i.intervals + + spy.reset_mock() + intervals = sushi_context.check_intervals(environment=None, no_signals=True, select_models=[]) + assert spy.call_count == 0 + assert len(intervals) >= min_intervals + + intervals = sushi_context.check_intervals( + environment=None, no_signals=False, select_models=["*waiter_as_customer*"] + ) + assert len(intervals) == 1 + + intervals = sushi_context.check_intervals( + environment=None, no_signals=False, select_models=["*waiter_as_customer*"], end="next week" + ) + assert tuple(intervals.values())[0].intervals From 8000bfbc83cc398132c9dcae4f5f32c9867c9845 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 21 Apr 2025 10:53:32 -0500 Subject: [PATCH 0023/1056] Fix: column width for execution progress bar with standalone audit (#4186) --- sqlmesh/core/console.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 6991e902b3..fd9c9e89e4 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -3379,7 +3379,13 @@ def _calculate_audit_str_len(batched_intervals: t.Dict[Snapshot, t.List[Interval # until after evaluation occurs, but we must determine the annotation column width here. # Therefore, we add enough padding for the longest possible audits result string. audit_str_len = 0 + audit_base_str_len = len(f", audits ") + 1 # +1 for check/X for snapshot in batched_intervals: + if snapshot.is_audit: + # +1 for "1" audit count, +1 for red X + audit_str_len = max( + audit_str_len, audit_base_str_len + (2 if not snapshot.audit.blocking else 1) + ) if snapshot.is_model and snapshot.model.audits: num_audits = len(snapshot.model.audits_with_args) num_nonblocking_audits = sum( @@ -3388,11 +3394,14 @@ def _calculate_audit_str_len(batched_intervals: t.Dict[Snapshot, t.List[Interval if not audit[0].blocking or ("blocking" in audit[1] and audit[1]["blocking"] == exp.false()) ) - # make enough room for all audits to pass - audit_len = len(f", audits {CHECK_MARK}{str(num_audits)}") - if num_nonblocking_audits: - # and add enough room for all nonblocking audits to fail - audit_len += len(f" {RED_X_MARK}{str(num_nonblocking_audits)}") + 1 + if num_audits == 1: + # +1 for "1" audit count, +1 for red X + audit_len = audit_base_str_len + (2 if num_nonblocking_audits else 1) + else: + audit_len = audit_base_str_len + len(str(num_audits)) + if num_nonblocking_audits: + # +1 for space, +1 for red X + audit_len += len(str(num_nonblocking_audits)) + 2 audit_str_len = max(audit_str_len, audit_len) return audit_str_len From b166ff29ce6558ba35307247a63277e4649d9952 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 21 Apr 2025 08:53:41 -0700 Subject: [PATCH 0024/1056] chore: document multi-repo migrations (#4185) --- docs/guides/multi_repo.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/guides/multi_repo.md b/docs/guides/multi_repo.md index d0b478d37c..bf34c7d21a 100644 --- a/docs/guides/multi_repo.md +++ b/docs/guides/multi_repo.md @@ -184,6 +184,14 @@ gateways: Even if you do not have a need for multiple repos now, consider adding a `project` key so that you can easily support multiple repos in the future. +## Running migrations with multiple repositories + +When doing a [migration](./migrations.md), pass in a single repo path using the `-p` flag. It doesn't matter which repo you choose. + +``` +$ sqlmesh -p examples/multi/repo_1 migrate +``` + ## Multi-Repo dbt projects SQLMesh also supports multiple repos for dbt projects, allowing it to correctly detect changes and orchestrate backfills even when changes span multiple dbt projects. From 786033f00bb6bfb151e7c92b9e6fe127f6487510 Mon Sep 17 00:00:00 2001 From: Afzal Jasani Date: Mon, 21 Apr 2025 08:53:49 -0700 Subject: [PATCH 0025/1056] Docs: Fix/clean up okta saml attributes image (#4182) --- .../features/single_sign_on/okta_setup_4.png | Bin 25837 -> 20957 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/cloud/features/single_sign_on/okta_setup_4.png b/docs/cloud/features/single_sign_on/okta_setup_4.png index d0684fa1be66b77f66de6f5decaadee86a9b5eb0..e11e4111a29a1111612f7192ad3a8555f1238ff0 100644 GIT binary patch literal 20957 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 literal 25837 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 From 27d09c4edc70a07c643b42c6f098d73bdfdd8089 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:54:28 +0300 Subject: [PATCH 0026/1056] Fix: When initialising multiple connections pass concurrent tasks (#4176) --- sqlmesh/core/config/connection.py | 16 +++++++--- sqlmesh/core/context.py | 2 +- tests/core/test_config.py | 49 ++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index c598b29a3d..be0eee114a 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -109,11 +109,15 @@ def connection_validator(self) -> t.Callable[[], None]: """A function that validates the connection configuration""" return self.create_engine_adapter().ping - def create_engine_adapter(self, register_comments_override: bool = False) -> EngineAdapter: + def create_engine_adapter( + self, register_comments_override: bool = False, concurrent_tasks: t.Optional[int] = None + ) -> EngineAdapter: """Returns a new instance of the Engine Adapter.""" + + concurrent_tasks = concurrent_tasks or self.concurrent_tasks return self._engine_adapter( self._connection_factory_with_kwargs, - multithreaded=self.concurrent_tasks > 1, + multithreaded=concurrent_tasks > 1, default_catalog=self.get_catalog(), cursor_init=self._cursor_init, register_comments=register_comments_override or self.register_comments, @@ -284,7 +288,9 @@ def init(cursor: duckdb.DuckDBPyConnection) -> None: return init - def create_engine_adapter(self, register_comments_override: bool = False) -> EngineAdapter: + def create_engine_adapter( + self, register_comments_override: bool = False, concurrent_tasks: t.Optional[int] = None + ) -> EngineAdapter: """Checks if another engine adapter has already been created that shares a catalog that points to the same data file. If so, it uses that same adapter instead of creating a new one. As a result, any additional configuration associated with the new adapter will be ignored.""" @@ -315,7 +321,9 @@ def create_engine_adapter(self, register_comments_override: bool = False) -> Eng logger.info(f"Creating new DuckDB adapter for data files: {masked_files}") else: logger.info("Creating new DuckDB adapter for in-memory database") - adapter = super().create_engine_adapter(register_comments_override) + adapter = super().create_engine_adapter( + register_comments_override, concurrent_tasks=concurrent_tasks + ) for data_file in data_files: key = data_file if isinstance(data_file, str) else data_file.path BaseDuckDBConnectionConfig._data_file_to_adapter[key] = adapter diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 81b3042585..a1ab32bac3 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -2254,7 +2254,7 @@ def engine_adapters(self) -> t.Dict[str, EngineAdapter]: for gateway_name in self.config.gateways: if gateway_name != self.selected_gateway: connection = self.config.get_connection(gateway_name) - adapter = connection.create_engine_adapter() + adapter = connection.create_engine_adapter(concurrent_tasks=self.concurrent_tasks) self._engine_adapters[gateway_name] = adapter return self._engine_adapters diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 8690e2ef34..c271b69522 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -26,6 +26,7 @@ ) from sqlmesh.core.context import Context from sqlmesh.core.engine_adapter.athena import AthenaEngineAdapter +from sqlmesh.core.engine_adapter.duckdb import DuckDBEngineAdapter from sqlmesh.core.engine_adapter.redshift import RedshiftEngineAdapter from sqlmesh.core.notification_target import ConsoleNotificationTarget from sqlmesh.core.user import User @@ -709,6 +710,10 @@ def test_multi_gateway_config(tmp_path, mocker: MockerFixture): aws_secret_access_key: accesskey work_group: group s3_warehouse_location: s3://location + duckdb: + connection: + type: duckdb + database: db.db default_gateway: redshift @@ -725,11 +730,53 @@ def test_multi_gateway_config(tmp_path, mocker: MockerFixture): ctx = Context(paths=tmp_path, config=config) assert isinstance(ctx._connection_config, RedshiftConnectionConfig) - assert len(ctx.engine_adapters) == 2 + assert len(ctx.engine_adapters) == 3 assert isinstance(ctx.engine_adapters["athena"], AthenaEngineAdapter) assert isinstance(ctx.engine_adapters["redshift"], RedshiftEngineAdapter) + assert isinstance(ctx.engine_adapters["duckdb"], DuckDBEngineAdapter) assert ctx.engine_adapter == ctx._get_engine_adapter("redshift") + # The duckdb engine adapter should be have been set as multithreaded as well + assert ctx.engine_adapters["duckdb"]._multithreaded + + +def test_multi_gateway_single_threaded_config(tmp_path): + config_path = tmp_path / "config_duck_athena.yaml" + with open(config_path, "w", encoding="utf-8") as fd: + fd.write( + """ +gateways: + duckdb: + connection: + type: duckdb + database: db.db + athena: + connection: + type: athena + aws_access_key_id: '1234' + aws_secret_access_key: accesskey + work_group: group + s3_warehouse_location: s3://location +default_gateway: duckdb +model_defaults: + dialect: duckdb + """ + ) + + config = load_config_from_paths( + Config, + project_paths=[config_path], + ) + + ctx = Context(paths=tmp_path, config=config) + assert isinstance(ctx._connection_config, DuckDBConnectionConfig) + assert len(ctx.engine_adapters) == 2 + assert ctx.engine_adapter == ctx._get_engine_adapter("duckdb") + assert isinstance(ctx.engine_adapters["athena"], AthenaEngineAdapter) + + # In this case athena should use 1 concurrent task as the default gateway is duckdb + assert not ctx.engine_adapters["athena"]._multithreaded + def test_trino_schema_location_mapping_syntax(tmp_path): config_path = tmp_path / "config_trino.yaml" From d513f240841bd8ce13f4d09cd8d96658db314fa2 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 21 Apr 2025 08:55:05 -0700 Subject: [PATCH 0027/1056] fix: prevent past ttl values for environment and snapshot (#4158) --- sqlmesh/core/config/root.py | 62 ++++++++++++------- tests/integrations/github/cicd/test_config.py | 45 ++++++++++++++ 2 files changed, 85 insertions(+), 22 deletions(-) diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index 66658988e5..f17d2cc015 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -41,6 +41,7 @@ from sqlmesh.core.loader import Loader, SqlMeshLoader from sqlmesh.core.notification_target import NotificationTarget from sqlmesh.core.user import User +from sqlmesh.utils.date import to_timestamp, now, now_timestamp from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.pydantic import field_validator, model_validator @@ -88,7 +89,7 @@ class Config(BaseConfig): after_all: SQL statements or macros to be executed at the end of the `sqlmesh plan` and `sqlmesh run` commands. """ - gateways: t.Dict[str, GatewayConfig] = {"": GatewayConfig()} + gateways: GatewayDict = {"": GatewayConfig()} default_connection: SerializableConnectionConfig = DuckDBConnectionConfig() default_test_connection_: t.Optional[SerializableConnectionConfig] = Field( default=None, alias="default_test_connection" @@ -97,8 +98,8 @@ class Config(BaseConfig): default_gateway: str = "" notification_targets: t.List[NotificationTarget] = [] project: str = "" - snapshot_ttl: str = c.DEFAULT_SNAPSHOT_TTL - environment_ttl: t.Optional[str] = c.DEFAULT_ENVIRONMENT_TTL + snapshot_ttl: NoPastTTLString = c.DEFAULT_SNAPSHOT_TTL + environment_ttl: t.Optional[NoPastTTLString] = c.DEFAULT_ENVIRONMENT_TTL ignore_patterns: t.List[str] = c.IGNORE_PATTERNS time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT users: t.List[User] = [] @@ -108,12 +109,12 @@ class Config(BaseConfig): loader_kwargs: t.Dict[str, t.Any] = {} env_vars: t.Dict[str, str] = {} username: str = "" - physical_schema_mapping: t.Dict[re.Pattern, str] = {} + physical_schema_mapping: RegexKeyDict = {} environment_suffix_target: EnvironmentSuffixTarget = Field( default=EnvironmentSuffixTarget.default ) gateway_managed_virtual_layer: bool = False - environment_catalog_mapping: t.Dict[re.Pattern, str] = {} + environment_catalog_mapping: RegexKeyDict = {} default_target_environment: str = c.PROD log_limit: int = c.DEFAULT_LOG_LIMIT cicd_bot: t.Optional[CICDBotConfig] = None @@ -154,23 +155,6 @@ class Config(BaseConfig): _scheduler_config_validator = scheduler_config_validator # type: ignore _variables_validator = variables_validator - @field_validator("gateways", mode="before") - @classmethod - def _gateways_ensure_dict(cls, value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: - try: - if not isinstance(value, GatewayConfig): - GatewayConfig.parse_obj(value) - return {"": value} - except Exception: - return value - - @field_validator("environment_catalog_mapping", "physical_schema_mapping", mode="before") - @classmethod - def _validate_regex_keys( - cls, value: t.Dict[str | re.Pattern, t.Any] - ) -> t.Dict[re.Pattern, t.Any]: - return compile_regex_mapping(value) - @model_validator(mode="before") def _normalize_and_validate_fields(cls, data: t.Any) -> t.Any: if not isinstance(data, dict): @@ -302,3 +286,37 @@ def dialect(self) -> t.Optional[str]: @property def fingerprint(self) -> str: return str(zlib.crc32(pickle.dumps(self.dict(exclude={"loader", "notification_targets"})))) + + +def validate_no_past_ttl(v: str) -> str: + current_time = now() + if to_timestamp(v, relative_base=current_time) < to_timestamp(current_time): + raise ValueError( + f"TTL '{v}' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`." + ) + return v + + +def gateways_ensure_dict(value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + try: + if not isinstance(value, GatewayConfig): + GatewayConfig.parse_obj(value) + return {"": value} + except Exception: + return value + + +def validate_regex_key_dict(value: t.Dict[str | re.Pattern, t.Any]) -> t.Dict[re.Pattern, t.Any]: + return compile_regex_mapping(value) + + +if t.TYPE_CHECKING: + NoPastTTLString = str + GatewayDict = t.Dict[str, GatewayConfig] + RegexKeyDict = t.Dict[re.Pattern, str] +else: + from pydantic.functional_validators import BeforeValidator + + NoPastTTLString = t.Annotated[str, BeforeValidator(validate_no_past_ttl)] + GatewayDict = t.Annotated[t.Dict[str, GatewayConfig], BeforeValidator(gateways_ensure_dict)] + RegexKeyDict = t.Annotated[t.Dict[re.Pattern, str], BeforeValidator(validate_regex_key_dict)] diff --git a/tests/integrations/github/cicd/test_config.py b/tests/integrations/github/cicd/test_config.py index 394015c705..cb2d9ac3fd 100644 --- a/tests/integrations/github/cicd/test_config.py +++ b/tests/integrations/github/cicd/test_config.py @@ -200,3 +200,48 @@ def test_validation(tmp_path): ValueError, match="merge_method must be set if enable_deploy_command is True" ): load_config_from_paths(Config, project_paths=[tmp_path / "config.yaml"]) + + +def test_ttl_in_past(tmp_path): + create_temp_file( + tmp_path, + pathlib.Path("config.yaml"), + """ +environment_ttl: in 1 week +model_defaults: + dialect: duckdb +""", + ) + + config = load_config_from_paths(Config, project_paths=[tmp_path / "config.yaml"]) + assert config.environment_ttl == "in 1 week" + + create_temp_file( + tmp_path, + pathlib.Path("config.yaml"), + """ +environment_ttl: 1 week +model_defaults: + dialect: duckdb +""", + ) + with pytest.raises( + ValueError, + match="TTL '1 week' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`.", + ): + load_config_from_paths(Config, project_paths=[tmp_path / "config.yaml"]) + + create_temp_file( + tmp_path, + pathlib.Path("config.yaml"), + """ +snapshot_ttl: 1 week +model_defaults: + dialect: duckdb + """, + ) + with pytest.raises( + ValueError, + match="TTL '1 week' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`.", + ): + load_config_from_paths(Config, project_paths=[tmp_path / "config.yaml"]) From f2f7cdeeb30e335585619172eac5b12490286c1c Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Mon, 21 Apr 2025 18:58:07 +0300 Subject: [PATCH 0028/1056] Feat: Introduce `format` flag for models and audits (#4203) --- docs/concepts/models/overview.md | 3 ++ docs/reference/model_configuration.md | 2 +- sqlmesh/core/audit/definition.py | 1 + sqlmesh/core/config/model.py | 1 + sqlmesh/core/context.py | 3 +- sqlmesh/core/model/common.py | 1 + sqlmesh/core/model/meta.py | 1 + tests/core/test_audit.py | 29 ++++++++++++++++++ tests/core/test_format.py | 44 +++++++++++++++++++++++++++ tests/core/test_model.py | 22 ++++++++++++++ 10 files changed, 105 insertions(+), 2 deletions(-) diff --git a/docs/concepts/models/overview.md b/docs/concepts/models/overview.md index 86ff1bc8bc..dd9fd0d767 100644 --- a/docs/concepts/models/overview.md +++ b/docs/concepts/models/overview.md @@ -457,6 +457,9 @@ to `false` causes SQLMesh to disable query canonicalization & simplification. Th ### ignored_rules : Specifies which linter rules should be ignored/excluded for this model. +### formatting +: Whether the model will be formatted. All models are formatted by default. Setting this to `false` causes SQLMesh to ignore this model during `sqlmesh format`. + ## Incremental Model Properties These properties can be specified in an incremental model's `kind` definition. diff --git a/docs/reference/model_configuration.md b/docs/reference/model_configuration.md index bd92478212..8cef798bd9 100644 --- a/docs/reference/model_configuration.md +++ b/docs/reference/model_configuration.md @@ -40,7 +40,7 @@ Configuration options for SQLMesh model properties. Supported by all model kinds | `gateway` | Specifies the gateway to use for the execution of this model. When not specified, the default gateway is used. | str | N | | `optimize_query` | Whether the model's query should be optimized. This attribute is `true` by default. Setting it to `false` causes SQLMesh to disable query canonicalization & simplification. This should be turned off only if the optimized query leads to errors such as surpassing text limit. | bool | N | | `ignored_rules` | A list of linter rule names (or "ALL") to be ignored/excluded for this model | str \| array[str] | N | - +| `formatting` | Whether the model will be formatted. All models are formatted by default. Setting this to `false` causes SQLMesh to ignore this model during `sqlmesh format`. | bool | N | ### Model defaults The SQLMesh project-level configuration must contain the `model_defaults` key and must specify a value for its `dialect` key. Other values are set automatically unless explicitly overridden in the model definition. Learn more about project-level configuration in the [configuration guide](../guides/configuration.md). diff --git a/sqlmesh/core/audit/definition.py b/sqlmesh/core/audit/definition.py index 673658fa30..76d8dea997 100644 --- a/sqlmesh/core/audit/definition.py +++ b/sqlmesh/core/audit/definition.py @@ -71,6 +71,7 @@ class AuditMixin(AuditCommonMetaMixin): defaults: t.Dict[str, exp.Expression] expressions_: t.Optional[t.List[exp.Expression]] jinja_macros: JinjaMacroRegistry + formatting: t.Optional[bool] = Field(default=None, exclude=True) @property def expressions(self) -> t.List[exp.Expression]: diff --git a/sqlmesh/core/config/model.py b/sqlmesh/core/config/model.py index ea6713b5a9..fab74799d9 100644 --- a/sqlmesh/core/config/model.py +++ b/sqlmesh/core/config/model.py @@ -60,6 +60,7 @@ class ModelDefaultsConfig(BaseConfig): allow_partials: t.Optional[t.Union[str, bool]] = None interval_unit: t.Optional[t.Union[str, IntervalUnit]] = None enabled: t.Optional[t.Union[str, bool]] = None + formatting: t.Optional[t.Union[str, bool]] = None _model_kind_validator = model_kind_validator _on_destructive_change_validator = on_destructive_change_validator diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index a1ab32bac3..45b125b60b 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1091,9 +1091,10 @@ def format( for target in filtered_targets: if ( - target._path is None + target._path is None or target.formatting is False ): # 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: before = file.read() expressions = parse(before, default_dialect=self.config_for_node(target).dialect) diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index c4a14e9104..9e0aed8783 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -386,6 +386,7 @@ def sorted_python_env_payloads(python_env: t.Dict[str, Executable]) -> t.List[st "allow_partials", "enabled", "optimize_query", + "formatting", mode="before", check_fields=False, )(parse_bool) diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 3c62d3db79..514a83cb3a 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -80,6 +80,7 @@ class ModelMeta(_Node): ignored_rules_: t.Optional[t.Set[str]] = Field( default=None, exclude=True, alias="ignored_rules" ) + formatting: t.Optional[bool] = Field(default=None, exclude=True) _bool_validator = bool_validator _model_kind_validator = model_kind_validator diff --git a/tests/core/test_audit.py b/tests/core/test_audit.py index 7290322ffb..1befb1148c 100644 --- a/tests/core/test_audit.py +++ b/tests/core/test_audit.py @@ -1,3 +1,4 @@ +import json import pytest from sqlglot import exp, parse_one @@ -959,3 +960,31 @@ def test_multiple_audits_with_same_name(): # Testing that audit arguments are identical for second and third audit # This establishes that identical audits are preserved assert model.audits[1][1] == model.audits[2][1] + + +def test_audit_formatting_flag_serde(): + expressions = parse( + """ + AUDIT ( + name my_audit, + dialect bigquery, + formatting false, + ); + + SELECT * FROM db.table WHERE col = @VAR('test_var') + """ + ) + + audit = load_audit( + expressions, + path="/path/to/audit", + dialect="bigquery", + variables={"test_var": "test_val", "test_var_unused": "unused_val"}, + ) + + audit_json = audit.json() + + assert "formatting" not in json.loads(audit_json) + + deserialized_audit = ModelAudit.parse_raw(audit_json) + assert deserialized_audit.dict() == audit.dict() diff --git a/tests/core/test_format.py b/tests/core/test_format.py index 40548f2d4f..a334e86a7d 100644 --- a/tests/core/test_format.py +++ b/tests/core/test_format.py @@ -8,6 +8,7 @@ from sqlmesh.core.model import SqlModel, load_sql_based_model from tests.utils.test_filesystem import create_temp_file from unittest.mock import call +from sqlmesh.core.config import ModelDefaultsConfig def test_format_files(tmp_path: pathlib.Path, mocker: MockerFixture): @@ -100,3 +101,46 @@ def test_format_files(tmp_path: pathlib.Path, mocker: MockerFixture): upd4 == "MODEL (\n name audit.model,\n audits (\n inline_audit\n )\n);\n\nSELECT\n 3 AS item_id;\n\nAUDIT (\n name inline_audit\n);\n\nSELECT\n *\nFROM @this_model\nWHERE\n item_id < 0" ) + + +def test_ignore_formating_files(tmp_path: pathlib.Path): + models_dir = pathlib.Path("models") + audits_dir = pathlib.Path("audits") + + # Case 1: Model and Audit are not formatted if the flag is set to false (overriding defaults) + model1_text = "MODEL(name this.model1, dialect 'duckdb', formatting false); SELECT 1 col" + model1 = create_temp_file(tmp_path, pathlib.Path(models_dir, "model_1.sql"), model1_text) + + audit1_text = "AUDIT(name audit1, dialect 'duckdb', formatting false); SELECT col1 col2 FROM @this_model WHERE foo < 0;" + audit1 = create_temp_file(tmp_path, pathlib.Path(audits_dir, "audit_1.sql"), audit1_text) + + audit2_text = "AUDIT(name audit2, dialect 'duckdb', standalone true, formatting false); SELECT col1 col2 FROM @this_model WHERE foo < 0;" + audit2 = create_temp_file(tmp_path, pathlib.Path(audits_dir, "audit_2.sql"), audit2_text) + + Context( + paths=tmp_path, config=Config(model_defaults=ModelDefaultsConfig(formatting=True)) + ).format() + + assert model1.read_text(encoding="utf-8") == model1_text + assert audit1.read_text(encoding="utf-8") == audit1_text + assert audit2.read_text(encoding="utf-8") == audit2_text + + # Case 2: Model is formatted (or not) based on it's flag and the defaults flag + model2_text = "MODEL(name this.model2, dialect 'duckdb'); SELECT 1 col" + model2 = create_temp_file(tmp_path, pathlib.Path(models_dir, "model_2.sql"), model2_text) + + model3_text = "MODEL(name this.model3, dialect 'duckdb', formatting true); SELECT 1 col" + model3 = create_temp_file(tmp_path, pathlib.Path(models_dir, "model_3.sql"), model3_text) + + Context( + paths=tmp_path, config=Config(model_defaults=ModelDefaultsConfig(formatting=False)) + ).format() + + # Case 2.1: Model is not formatted if the defaults flag is set to false + assert model2.read_text(encoding="utf-8") == model2_text + + # Case 2.2: Model is formatted if it's flag is set to true, overriding defaults + assert ( + model3.read_text(encoding="utf-8") + == "MODEL (\n name this.model3,\n dialect 'duckdb',\n formatting TRUE\n);\n\nSELECT\n 1 AS col" + ) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index a569ee45ae..cd8c29d42e 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9029,3 +9029,25 @@ def test_var_in_def(assert_exp_eq): SELECT '1970-01-01' AS "ds" """, ) + + +def test_formatting_flag_serde(): + expressions = d.parse( + """ + MODEL( + name test_model, + formatting False, + ); + SELECT * FROM tbl; + """, + default_dialect="duckdb", + ) + + model = load_sql_based_model(expressions) + + model_json = model.json() + + assert "formatting" not in json.loads(model_json) + + deserialized_model = SqlModel.parse_raw(model_json) + assert deserialized_model.dict() == model.dict() From 34039b3d27000d3d30923497119d8814181e171f Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:00:02 +0300 Subject: [PATCH 0029/1056] Docs: Update multi engine guide with gateway managed virtual layer info (#4171) Co-authored-by: Trey Spiller <1831878+treysp@users.noreply.github.com> --- docs/guides/configuration.md | 33 +++ docs/guides/multi_engine.md | 235 ++++++++++++++++-- .../athena_redshift_snowflake.png | Bin 0 -> 127569 bytes docs/guides/multi_engine/postgres_duckdb.png | Bin 0 -> 77148 bytes docs/reference/configuration.md | 2 +- 5 files changed, 251 insertions(+), 19 deletions(-) create mode 100644 docs/guides/multi_engine/athena_redshift_snowflake.png create mode 100644 docs/guides/multi_engine/postgres_duckdb.png diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index e66ff78979..bf8db05a1e 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -865,6 +865,39 @@ This may be useful in cases where the name casing needs to be preserved, since t See [here](https://sqlglot.com/sqlglot/dialects/dialect.html#NormalizationStrategy) to learn more about the supported normalization strategies. +##### Gateway-specific model defaults + +You can also define gateway specific `model_defaults` in the `gateways` section, which override the global defaults for that gateway. + +```yaml linenums="1" hl_lines="6 14" +gateways: + redshift: + connection: + type: redshift + model_defaults: + dialect: "snowflake,normalization_strategy=case_insensitive" + snowflake: + connection: + type: snowflake + +default_gateway: snowflake + +model_defaults: + dialect: snowflake + start: 2025-02-05 +``` + +This allows you to tailor the behavior of models for each gateway without affecting the global `model_defaults`. + +For example, in some SQL engines identifiers like table and column names are case-sensitive, but they are case-insensitive in other engines. By default, a project that uses both types of engines would need to ensure the models for each engine aligned with the engine's normalization behavior, which makes project maintenance and debugging more challenging. + +Gateway-specific `model_defaults` allow you to change how SQLMesh performs identifier normalization *by engine* to align the different engines' behavior. + +In the example above, the project's default dialect is `snowflake` (line 14). The `redshift` gateway configuration overrides that global default dialect with `"snowflake,normalization_strategy=case_insensitive"` (line 6). + +That value tells SQLMesh that the `redshift` gateway's models will be written in the Snowflake SQL dialect (so need to be transpiled from Snowflake to Redshift), but that the resulting Redshift SQL should treat identifiers as case-insensitive to match Snowflake's behavior. + + #### Model Kinds Model kinds are required in each model file's `MODEL` DDL statement. They may optionally be used to specify a default kind in the model defaults configuration key. diff --git a/docs/guides/multi_engine.md b/docs/guides/multi_engine.md index f46c50e361..ad4a8a3cd0 100644 --- a/docs/guides/multi_engine.md +++ b/docs/guides/multi_engine.md @@ -2,28 +2,36 @@ Organizations typically connect to a data warehouse through a single engine to ensure data consistency. However, there are cases where the processing capabilities of one engine may be better suited to specific tasks than another. -By decoupling storage from compute and with growing support for open table formats like Apache Iceberg and Hive, different engines can now interact with the same data. +Companies are increasingly decoupling how/where data is stored from the how computations are run on the data, requiring interoperability across platforms and tools. Open table formats like Apache Iceberg, Delta Lake, and Hive provide a common storage format that can be used by multiple SQL engines. -With SQLMesh's new multi-engine feature, users can leverage multiple engine adapters within a single project, offering the flexibility to choose the best engine for each task. +SQLMesh enables this decoupling by supporting multiple engine adapters within a single project, giving you the flexibility to choose the best engine for each computational task. You can specify the engine each model uses, based on what computations the model performs or other organization-specific considerations. -This feature allows you to run each model on a specified engine, provided the data catalog is shared and the engines support read/write operations on it. +## Configuring a Project with Multiple Engines +Configuring your project to use multiple engines follows a simple process: -## Configuring project with multiple engines +- Include all required [gateway connections](../reference/configuration.md#connection) in your configuration. +- Specify the `gateway` to be used for execution in the `MODEL` DDL. -To configure a SQLMesh project with multiple engines, simply include all required gateway [connections](../reference/configuration.md#connection) in your configuration. +If no gateway is explicitly defined for a model, the [default_gateway](../reference/configuration.md#default-gateway) of the project is used. -Next, specify the appropriate `gateway` in the `MODEL` DDL for each model. If no gateway is explicitly defined, the default gateway will be used. +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. -The [virtual layer](../concepts/glossary.md#virtual-layer) will be created within the engine corresponding to the default gateway. +Alternatively, each gateway can create the virtual layer views for the models it runs. Use this approach by setting the [gateway_managed_virtual_layer](#gateway-managed-virtual-layer) flag to `true` in your project configuration. -### Example +### Shared Virtual Layer -Below is a simple example of setting up a project with connections to both DuckDB and PostgreSQL. +To dive deeper, in SQLMesh the [physical layer](../concepts/glossary.md#physical-layer) is the concrete data storage layer, where it stores and manages data in database tables and materialized views. + +While, the [virtual layer](../concepts/glossary.md#virtual-layer) consists of views, one for each model, each pointing to a snapshot table in the physical layer. + +In a multi-engine project with a shared data catalog, the model-specific gateway is responsible for the physical layer, while the default gateway is used for managing the virtual layer. + +#### Example: DuckDB + PostgreSQL -In this setup, the PostgreSQL engine is set as the default, so it will be used to manage views in the virtual layer. +Below is a simple example of setting up a project with connections to both DuckDB and PostgreSQL. -Meanwhile, the DuckDB's [attach](https://duckdb.org/docs/sql/statements/attach.html) feature enables read-write access to the PostgreSQL catalog's physical tables. +In this setup, the PostgreSQL engine is set as the default, so it will be used to manage views in the virtual layer. Meanwhile, DuckDB's [attach](https://duckdb.org/docs/sql/statements/attach.html) feature enables read-write access to the PostgreSQL catalog's physical tables. === "YAML" @@ -81,7 +89,7 @@ Meanwhile, the DuckDB's [attach](https://duckdb.org/docs/sql/statements/attach.h port=5432, user="postgres", password="password", - database="main_db", + database="main_db", ) ), }, @@ -89,8 +97,7 @@ Meanwhile, the DuckDB's [attach](https://duckdb.org/docs/sql/statements/attach.h ) ``` -Given this configuration, when a model’s gateway is set to duckdb, it will be materialized within the PostgreSQL `main_db` catalog, but it will be evaluated using DuckDB’s engine. - +Given this configuration, when a model’s gateway is set to DuckDB, the DuckDB engine will perform the calculations before materializing the physical table in the PostgreSQL `main_db` catalog. ```sql linenums="1" MODEL ( @@ -100,12 +107,204 @@ MODEL ( ); SELECT - l_orderkey, + l_orderkey, l_shipdate -FROM +FROM iceberg_scan('data/bucket/lineitem_iceberg', allow_moved_paths = true); ``` -In this model, the DuckDB engine can be used to scan and load data from an iceberg table and create the physical table in the PostgreSQL database. +The `order_ship_date` model specifies the DuckDB engine, which will perform the computations used to create the physical table in the PostgreSQL database. + +This allows you to efficiently scan data from an Iceberg table, or even query tables directly from S3 when used with the [HTTPFS](https://duckdb.org/docs/stable/extensions/httpfs/overview.html) extension. + +![PostgreSQL + DuckDB](./multi_engine/postgres_duckdb.png) + +In models where no gateway is specified, such as the `customer_orders` model, the default PostgreSQL engine will both create the physical table and the views in the virtual layer. + +### Gateway-Managed Virtual Layer + +By default, all virtual layer views are created in the project's default gateway. + +If your project's engines don’t have a mutually accessible catalog or your raw data is located in different engines, you may prefer for each model's virtual layer view to exist in the gateway that ran the model. This allows a single SQLMesh project to manage isolated sets of models in different gateways, which is sometimes necessary for data governance or security concerns. + +To enable this, set `gateway_managed_virtual_layer` to `true` in your configuration. By default, this flag is set to false. + +#### Example: Redshift + Athena + Snowflake + +Consider a scenario where you need to create a project with models in Redshift, Athena and Snowflake, where each engine hosts its models' virtual layer views. + +First, add the connections to your configuration and set the `gateway_managed_virtual_layer` flag to `true`: + +=== "YAML" + +```yaml linenums="1" hl_lines="30" +gateways: + redshift: + connection: + type: redshift + user: + password: + host: + database: + variables: + gw_var: 'redshift' + athena: + connection: + type: athena + aws_access_key_id: + aws_secret_access_key: + s3_warehouse_location: + variables: + gw_var: 'athena' + snowflake: + connection: + type: snowflake + account: + user: + database: + warehouse: + variables: + gw_var: 'snowflake' + +default_gateway: redshift +gateway_managed_virtual_layer: true + +variables: + gw_var: 'global' + global_var: 5 +``` + +=== "Python" + +```python linenums="1" hl_lines="48" +from sqlmesh.core.config import ( + Config, + ModelDefaultsConfig, + GatewayConfig, + RedshiftConnectionConfig, + AthenaConnectionConfig, + SnowflakeConnectionConfig, +) + +config = Config( + model_defaults=ModelDefaultsConfig(dialect="redshift"), + gateways={ + "redshift": GatewayConfig( + connection=RedshiftConnectionConfig( + user="", + password="", + host="", + database="", + ), + variables={ + "gw_var": "redshift" + }, + ), + "athena": GatewayConfig( + connection=AthenaConnectionConfig( + aws_access_key_id="", + aws_secret_access_key="", + region_name="", + s3_warehouse_location="", + ), + variables={ + "gw_var": "athena" + }, + ), + "snowflake": GatewayConfig( + connection=SnowflakeConnectionConfig( + account="", + user="", + database="", + warehouse="", + ), + variables={ + "gw_var": "snowflake" + }, + ), + }, + default_gateway="redshift", + gateway_managed_virtual_layer=True, + variables={ + "gw_var": "global", + "global_var": 5, + }, +) +``` + +Note that gateway-specific variables take precedence over global ones. In the example above, the `gw_var` used in a model will resolve to the value specified in the model's gateway. + +For further customization, you can also enable [gateway-specific model defaults](../guides/configuration.md#gateway-specific-model-defaults). This allows you to define custom behaviors, such as specifying a dialect with case-insensitivity normalization. + +The default gateway is `redshift` In the example configuration above, so all models without a `gateway` specification will run on redshift, as in this `order_dates` model: + +```sql linenums="1" +MODEL ( + name redshift_schema.order_dates, + table_format iceberg, +); + +SELECT + order_date, + order_id +FROM + bucket.raw_data; +``` + +For the `athena_schema.order_status` model, we explicitly specify the `athena` gateway: + +```sql linenums="1" hl_lines="4" +MODEL ( + name athena_schema.order_status, + table_format iceberg, + gateway athena, +); + +SELECT + order_id, + status +FROM + bucket.raw_data; +``` + +Finally, specifying the `snowflake` gateway for the `customer_orders` model ensures it is isolated from the rest and reads from a table within the Snowflake database: + +```sql linenums="1" hl_lines="4" +MODEL ( + name snowflake_schema.customer_orders, + table_format iceberg, + gateway snowflake +); + +SELECT + customer_id, + orders +FROM + bronze_schema.customer_data; +``` + + +![Athena + Redshift + Snowflake](./multi_engine/athena_redshift_snowflake.png) + +When you run the plan, the catalogs for each model will be set automatically based on the gateway’s connection and each corresponding model will be executed by the specified engine: + +```bash +❯ sqlmesh plan + +`prod` environment will be initialized + +Models: +└── Added: + ├── awsdatacatalog.athena_schema.order_status # each model uses its gateway's catalog and schema + ├── redshift_schema.order_dates + └── silver.snowflake_schema.customers +Models needing backfill: +├── awsdatacatalog.athena_schema.order_status: [full refresh] +├── redshift_schema.order_dates: [full refresh] +└── silver.snowflake_schema.customer_orders: [full refresh] +Apply - Backfill Tables [y/n]: y +``` + +The views of the virtual layer will also be created by each corresponding engine. -While the PostgreSQL engine is responsible for creating the model's view for the virtual layer. +This approach provides isolation between your models, while maintaining centralized control over your project. diff --git a/docs/guides/multi_engine/athena_redshift_snowflake.png b/docs/guides/multi_engine/athena_redshift_snowflake.png new file mode 100644 index 0000000000000000000000000000000000000000..db2cff2d171b97c0ce8a38b08918e111e0d46b76 GIT binary patch literal 127569 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

k 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 z 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 Date: Mon, 21 Apr 2025 09:07:05 -0700 Subject: [PATCH 0030/1056] Fix: formatting --- sqlmesh/core/config/root.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index f17d2cc015..b802d75209 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -41,9 +41,9 @@ from sqlmesh.core.loader import Loader, SqlMeshLoader from sqlmesh.core.notification_target import NotificationTarget from sqlmesh.core.user import User -from sqlmesh.utils.date import to_timestamp, now, now_timestamp +from sqlmesh.utils.date import to_timestamp, now from sqlmesh.utils.errors import ConfigError -from sqlmesh.utils.pydantic import field_validator, model_validator +from sqlmesh.utils.pydantic import model_validator if t.TYPE_CHECKING: from sqlmesh.core._typing import Self From 692ffff4b3e5cc3f7b8925bad0f34cb5ed3a5c28 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:01:19 -0700 Subject: [PATCH 0031/1056] fix: allow subclassing of config again (#4209) --- sqlmesh/core/config/root.py | 73 +++++++++++++++++-------------------- tests/core/test_config.py | 7 ++++ 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index b802d75209..e4ad29eb7a 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -43,7 +43,7 @@ from sqlmesh.core.user import User from sqlmesh.utils.date import to_timestamp, now from sqlmesh.utils.errors import ConfigError -from sqlmesh.utils.pydantic import model_validator +from sqlmesh.utils.pydantic import model_validator, field_validator if t.TYPE_CHECKING: from sqlmesh.core._typing import Self @@ -89,7 +89,7 @@ class Config(BaseConfig): after_all: SQL statements or macros to be executed at the end of the `sqlmesh plan` and `sqlmesh run` commands. """ - gateways: GatewayDict = {"": GatewayConfig()} + gateways: t.Dict[str, GatewayConfig] = {"": GatewayConfig()} default_connection: SerializableConnectionConfig = DuckDBConnectionConfig() default_test_connection_: t.Optional[SerializableConnectionConfig] = Field( default=None, alias="default_test_connection" @@ -98,8 +98,8 @@ class Config(BaseConfig): default_gateway: str = "" notification_targets: t.List[NotificationTarget] = [] project: str = "" - snapshot_ttl: NoPastTTLString = c.DEFAULT_SNAPSHOT_TTL - environment_ttl: t.Optional[NoPastTTLString] = c.DEFAULT_ENVIRONMENT_TTL + snapshot_ttl: str = c.DEFAULT_SNAPSHOT_TTL + environment_ttl: t.Optional[str] = c.DEFAULT_ENVIRONMENT_TTL ignore_patterns: t.List[str] = c.IGNORE_PATTERNS time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT users: t.List[User] = [] @@ -109,12 +109,12 @@ class Config(BaseConfig): loader_kwargs: t.Dict[str, t.Any] = {} env_vars: t.Dict[str, str] = {} username: str = "" - physical_schema_mapping: RegexKeyDict = {} + physical_schema_mapping: t.Dict[re.Pattern, str] = {} environment_suffix_target: EnvironmentSuffixTarget = Field( default=EnvironmentSuffixTarget.default ) gateway_managed_virtual_layer: bool = False - environment_catalog_mapping: RegexKeyDict = {} + environment_catalog_mapping: t.Dict[re.Pattern, str] = {} default_target_environment: str = c.PROD log_limit: int = c.DEFAULT_LOG_LIMIT cicd_bot: t.Optional[CICDBotConfig] = None @@ -155,6 +155,33 @@ class Config(BaseConfig): _scheduler_config_validator = scheduler_config_validator # type: ignore _variables_validator = variables_validator + @field_validator("gateways", mode="before") + @classmethod + def _gateways_ensure_dict(cls, value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + try: + if not isinstance(value, GatewayConfig): + GatewayConfig.parse_obj(value) + return {"": value} + except Exception: + return value + + @field_validator("environment_catalog_mapping", "physical_schema_mapping", mode="before") + @classmethod + def _validate_regex_keys( + cls, value: t.Dict[str | re.Pattern, t.Any] + ) -> t.Dict[re.Pattern, t.Any]: + return compile_regex_mapping(value) + + @field_validator("snapshot_ttl", "environment_ttl", mode="before") + @classmethod + def validate_no_past_ttl(cls, v: str) -> str: + current_time = now() + if to_timestamp(v, relative_base=current_time) < to_timestamp(current_time): + raise ValueError( + f"TTL '{v}' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`." + ) + return v + @model_validator(mode="before") def _normalize_and_validate_fields(cls, data: t.Any) -> t.Any: if not isinstance(data, dict): @@ -286,37 +313,3 @@ def dialect(self) -> t.Optional[str]: @property def fingerprint(self) -> str: return str(zlib.crc32(pickle.dumps(self.dict(exclude={"loader", "notification_targets"})))) - - -def validate_no_past_ttl(v: str) -> str: - current_time = now() - if to_timestamp(v, relative_base=current_time) < to_timestamp(current_time): - raise ValueError( - f"TTL '{v}' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`." - ) - return v - - -def gateways_ensure_dict(value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: - try: - if not isinstance(value, GatewayConfig): - GatewayConfig.parse_obj(value) - return {"": value} - except Exception: - return value - - -def validate_regex_key_dict(value: t.Dict[str | re.Pattern, t.Any]) -> t.Dict[re.Pattern, t.Any]: - return compile_regex_mapping(value) - - -if t.TYPE_CHECKING: - NoPastTTLString = str - GatewayDict = t.Dict[str, GatewayConfig] - RegexKeyDict = t.Dict[re.Pattern, str] -else: - from pydantic.functional_validators import BeforeValidator - - NoPastTTLString = t.Annotated[str, BeforeValidator(validate_no_past_ttl)] - GatewayDict = t.Annotated[t.Dict[str, GatewayConfig], BeforeValidator(gateways_ensure_dict)] - RegexKeyDict = t.Annotated[t.Dict[re.Pattern, str], BeforeValidator(validate_regex_key_dict)] diff --git a/tests/core/test_config.py b/tests/core/test_config.py index c271b69522..658397abdc 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -974,3 +974,10 @@ class TestConfig(DuckDBConnectionConfig): pass TestConfig() + + +# @pytest.mark.isolated +def test_config_subclassing() -> None: + class ConfigSubclass(Config): ... + + ConfigSubclass() From aee881aaf12b09ebccc15ecdb9eceb3c6384314c Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:43:25 -0700 Subject: [PATCH 0032/1056] fix: allow subclassing of config again (#4210) --- sqlmesh/core/config/root.py | 71 ++++++++++++++++++++----------------- tests/core/test_config.py | 1 - 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index e4ad29eb7a..b624ea66fb 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -6,6 +6,7 @@ import zlib from pydantic import Field +from pydantic.functional_validators import BeforeValidator from sqlglot import exp from sqlglot.helper import first from sqlglot.optimizer.normalize_identifiers import normalize_identifiers @@ -43,11 +44,42 @@ from sqlmesh.core.user import User from sqlmesh.utils.date import to_timestamp, now from sqlmesh.utils.errors import ConfigError -from sqlmesh.utils.pydantic import model_validator, field_validator +from sqlmesh.utils.pydantic import model_validator + + +def validate_no_past_ttl(v: str) -> str: + current_time = now() + if to_timestamp(v, relative_base=current_time) < to_timestamp(current_time): + raise ValueError( + f"TTL '{v}' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`." + ) + return v + + +def gateways_ensure_dict(value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + try: + if not isinstance(value, GatewayConfig): + GatewayConfig.parse_obj(value) + return {"": value} + except Exception: + return value + + +def validate_regex_key_dict(value: t.Dict[str | re.Pattern, t.Any]) -> t.Dict[re.Pattern, t.Any]: + return compile_regex_mapping(value) + if t.TYPE_CHECKING: from sqlmesh.core._typing import Self + NoPastTTLString = str + GatewayDict = t.Dict[str, GatewayConfig] + RegexKeyDict = t.Dict[re.Pattern, str] +else: + NoPastTTLString = t.Annotated[str, BeforeValidator(validate_no_past_ttl)] + GatewayDict = t.Annotated[t.Dict[str, GatewayConfig], BeforeValidator(gateways_ensure_dict)] + RegexKeyDict = t.Annotated[t.Dict[re.Pattern, str], BeforeValidator(validate_regex_key_dict)] + class Config(BaseConfig): """An object used by a Context to configure your SQLMesh project. @@ -89,7 +121,7 @@ class Config(BaseConfig): after_all: SQL statements or macros to be executed at the end of the `sqlmesh plan` and `sqlmesh run` commands. """ - gateways: t.Dict[str, GatewayConfig] = {"": GatewayConfig()} + gateways: GatewayDict = {"": GatewayConfig()} default_connection: SerializableConnectionConfig = DuckDBConnectionConfig() default_test_connection_: t.Optional[SerializableConnectionConfig] = Field( default=None, alias="default_test_connection" @@ -98,8 +130,8 @@ class Config(BaseConfig): default_gateway: str = "" notification_targets: t.List[NotificationTarget] = [] project: str = "" - snapshot_ttl: str = c.DEFAULT_SNAPSHOT_TTL - environment_ttl: t.Optional[str] = c.DEFAULT_ENVIRONMENT_TTL + snapshot_ttl: NoPastTTLString = c.DEFAULT_SNAPSHOT_TTL + environment_ttl: t.Optional[NoPastTTLString] = c.DEFAULT_ENVIRONMENT_TTL ignore_patterns: t.List[str] = c.IGNORE_PATTERNS time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT users: t.List[User] = [] @@ -109,12 +141,12 @@ class Config(BaseConfig): loader_kwargs: t.Dict[str, t.Any] = {} env_vars: t.Dict[str, str] = {} username: str = "" - physical_schema_mapping: t.Dict[re.Pattern, str] = {} + physical_schema_mapping: RegexKeyDict = {} environment_suffix_target: EnvironmentSuffixTarget = Field( default=EnvironmentSuffixTarget.default ) gateway_managed_virtual_layer: bool = False - environment_catalog_mapping: t.Dict[re.Pattern, str] = {} + environment_catalog_mapping: RegexKeyDict = {} default_target_environment: str = c.PROD log_limit: int = c.DEFAULT_LOG_LIMIT cicd_bot: t.Optional[CICDBotConfig] = None @@ -155,33 +187,6 @@ class Config(BaseConfig): _scheduler_config_validator = scheduler_config_validator # type: ignore _variables_validator = variables_validator - @field_validator("gateways", mode="before") - @classmethod - def _gateways_ensure_dict(cls, value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: - try: - if not isinstance(value, GatewayConfig): - GatewayConfig.parse_obj(value) - return {"": value} - except Exception: - return value - - @field_validator("environment_catalog_mapping", "physical_schema_mapping", mode="before") - @classmethod - def _validate_regex_keys( - cls, value: t.Dict[str | re.Pattern, t.Any] - ) -> t.Dict[re.Pattern, t.Any]: - return compile_regex_mapping(value) - - @field_validator("snapshot_ttl", "environment_ttl", mode="before") - @classmethod - def validate_no_past_ttl(cls, v: str) -> str: - current_time = now() - if to_timestamp(v, relative_base=current_time) < to_timestamp(current_time): - raise ValueError( - f"TTL '{v}' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`." - ) - return v - @model_validator(mode="before") def _normalize_and_validate_fields(cls, data: t.Any) -> t.Any: if not isinstance(data, dict): diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 658397abdc..eef458302d 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -976,7 +976,6 @@ class TestConfig(DuckDBConnectionConfig): TestConfig() -# @pytest.mark.isolated def test_config_subclassing() -> None: class ConfigSubclass(Config): ... From 76178f07f44e608a8b7fc6cb10563a14ac96257e Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:32:40 +0300 Subject: [PATCH 0033/1056] Chore(docs): Adjust indentation for MkDocs tabs to group content (#4212) --- docs/guides/multi_engine.md | 174 ++++++++++++++++++------------------ 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/docs/guides/multi_engine.md b/docs/guides/multi_engine.md index ad4a8a3cd0..f22187f25f 100644 --- a/docs/guides/multi_engine.md +++ b/docs/guides/multi_engine.md @@ -137,106 +137,106 @@ First, add the connections to your configuration and set the `gateway_managed_vi === "YAML" -```yaml linenums="1" hl_lines="30" -gateways: - redshift: - connection: - type: redshift - user: - password: - host: - database: - variables: - gw_var: 'redshift' - athena: - connection: - type: athena - aws_access_key_id: - aws_secret_access_key: - s3_warehouse_location: - variables: - gw_var: 'athena' - snowflake: - connection: - type: snowflake - account: - user: - database: - warehouse: - variables: - gw_var: 'snowflake' + ```yaml linenums="1" hl_lines="30" + gateways: + redshift: + connection: + type: redshift + user: + password: + host: + database: + variables: + gw_var: 'redshift' + athena: + connection: + type: athena + aws_access_key_id: + aws_secret_access_key: + s3_warehouse_location: + variables: + gw_var: 'athena' + snowflake: + connection: + type: snowflake + account: + user: + database: + warehouse: + variables: + gw_var: 'snowflake' -default_gateway: redshift -gateway_managed_virtual_layer: true + default_gateway: redshift + gateway_managed_virtual_layer: true -variables: - gw_var: 'global' - global_var: 5 -``` + variables: + gw_var: 'global' + global_var: 5 + ``` === "Python" -```python linenums="1" hl_lines="48" -from sqlmesh.core.config import ( - Config, - ModelDefaultsConfig, - GatewayConfig, - RedshiftConnectionConfig, - AthenaConnectionConfig, - SnowflakeConnectionConfig, -) - -config = Config( - model_defaults=ModelDefaultsConfig(dialect="redshift"), - gateways={ - "redshift": GatewayConfig( - connection=RedshiftConnectionConfig( - user="", - password="", - host="", - database="", + ```python linenums="1" hl_lines="48" + from sqlmesh.core.config import ( + Config, + ModelDefaultsConfig, + GatewayConfig, + RedshiftConnectionConfig, + AthenaConnectionConfig, + SnowflakeConnectionConfig, + ) + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="redshift"), + gateways={ + "redshift": GatewayConfig( + connection=RedshiftConnectionConfig( + user="", + password="", + host="", + database="", + ), + variables={ + "gw_var": "redshift" + }, ), - variables={ - "gw_var": "redshift" - }, - ), - "athena": GatewayConfig( - connection=AthenaConnectionConfig( - aws_access_key_id="", - aws_secret_access_key="", - region_name="", - s3_warehouse_location="", + "athena": GatewayConfig( + connection=AthenaConnectionConfig( + aws_access_key_id="", + aws_secret_access_key="", + region_name="", + s3_warehouse_location="", + ), + variables={ + "gw_var": "athena" + }, ), - variables={ - "gw_var": "athena" - }, - ), - "snowflake": GatewayConfig( - connection=SnowflakeConnectionConfig( - account="", - user="", - database="", - warehouse="", + "snowflake": GatewayConfig( + connection=SnowflakeConnectionConfig( + account="", + user="", + database="", + warehouse="", + ), + variables={ + "gw_var": "snowflake" + }, ), - variables={ - "gw_var": "snowflake" - }, - ), - }, - default_gateway="redshift", - gateway_managed_virtual_layer=True, - variables={ - "gw_var": "global", - "global_var": 5, - }, -) -``` + }, + default_gateway="redshift", + gateway_managed_virtual_layer=True, + variables={ + "gw_var": "global", + "global_var": 5, + }, + ) + ``` Note that gateway-specific variables take precedence over global ones. In the example above, the `gw_var` used in a model will resolve to the value specified in the model's gateway. For further customization, you can also enable [gateway-specific model defaults](../guides/configuration.md#gateway-specific-model-defaults). This allows you to define custom behaviors, such as specifying a dialect with case-insensitivity normalization. -The default gateway is `redshift` In the example configuration above, so all models without a `gateway` specification will run on redshift, as in this `order_dates` model: +In the example configuration above the default gateway is `redshift`, so all models without a `gateway` specification will run on redshift, as in this `order_dates` model: ```sql linenums="1" MODEL ( From 0fdd953bae6a7f9d3d641790cba211ec926772ae Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:38:15 +0100 Subject: [PATCH 0034/1056] chore(vscode): update dependencies (#4213) --- vscode/extension/package-lock.json | 993 ++++++++++++++++++----------- 1 file changed, 624 insertions(+), 369 deletions(-) diff --git a/vscode/extension/package-lock.json b/vscode/extension/package-lock.json index 16e4ac0dd1..4756a10b38 100644 --- a/vscode/extension/package-lock.json +++ b/vscode/extension/package-lock.json @@ -127,9 +127,9 @@ } }, "node_modules/@azure/identity": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.8.0.tgz", - "integrity": "sha512-l9ALUGHtFB/JfsqmA+9iYAp2a+cCwdNO/cyIr2y7nJLJsz1aae6qVP8XxT7Kbudg0IQRSIMXj0+iivFdbD1xPA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.9.1.tgz", + "integrity": "sha512-986D7Cf1AOwYqSDtO/FnMAyk/Jc8qpftkGsxuehoh4F85MhQ4fICBGX/44+X1y78lN4Sqib3Bsoaoh/FvOGgmg==", "dev": true, "license": "MIT", "dependencies": { @@ -141,11 +141,8 @@ "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", - "@azure/msal-node": "^3.2.3", - "events": "^3.0.0", - "jws": "^4.0.0", + "@azure/msal-node": "^3.5.0", "open": "^10.1.0", - "stoppable": "^1.1.0", "tslib": "^2.2.0" }, "engines": { @@ -166,22 +163,22 @@ } }, "node_modules/@azure/msal-browser": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.10.0.tgz", - "integrity": "sha512-48X2VwOtHk8A1CI00E8tAqko0+3qQh53u5bOPySzdojL3T/Ad4GgRnN0c0oLJ1/PcTm4D4QybHYG3LBOX0l3/g==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.11.0.tgz", + "integrity": "sha512-0p5Ut3wORMP+975AKvaSPIO4UytgsfAvJ7RxaTx+nkP+Hpkmm93AuiMkBWKI2x9tApU/SLgIyPz/ZwLYUIWb5Q==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "15.5.0" + "@azure/msal-common": "15.5.1" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.5.0.tgz", - "integrity": "sha512-u97AJ6m4PB24/Plms9e9iydRcOaxxrHWkan1px5GeWGJfakY1D/r1DmY1+Typ8zWC/5JbNzH1GYpXrorPymz5g==", + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.5.1.tgz", + "integrity": "sha512-oxK0khbc4Bg1bKQnqDr7ikULhVL2OHgSrIq0Vlh4b6+hm4r0lr6zPMQE8ZvmacJuh+ZZGKBM5iIObhF1q1QimQ==", "dev": true, "license": "MIT", "engines": { @@ -189,13 +186,13 @@ } }, "node_modules/@azure/msal-node": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.0.tgz", - "integrity": "sha512-9cLUmcOZ5FODz3uAhS2C9A1U7xDUTCHVcaNQBYpOd5qCKdKM6ft/ydAfw27vEntuaDgnh5jytOAKsEzEbtoQ1Q==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.1.tgz", + "integrity": "sha512-dkgMYM5B6tI88r/oqf5bYd93WkenQpaWwiszJDk7avVjso8cmuKRTW97dA1RMi6RhihZFLtY1VtWxU9+sW2T5g==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "15.5.0", + "@azure/msal-common": "15.5.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -636,9 +633,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz", - "integrity": "sha512-WhCn7Z7TauhBtmzhvKpoQs0Wwb/kBcy4CwpuI0/eEIr2Lx2auxmulAzLr91wVZJaz47iUZdkXOK7WlAfxGKCnA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", "dev": true, "license": "MIT", "dependencies": { @@ -714,9 +711,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -775,9 +772,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", - "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", + "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", "dev": true, "license": "MIT", "engines": { @@ -808,19 +805,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1119,17 +1103,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", - "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.0.tgz", + "integrity": "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/type-utils": "8.30.1", - "@typescript-eslint/utils": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/type-utils": "8.31.0", + "@typescript-eslint/utils": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1149,16 +1133,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", - "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.0.tgz", + "integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/typescript-estree": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4" }, "engines": { @@ -1174,14 +1158,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", - "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.0.tgz", + "integrity": "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1" + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1192,14 +1176,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", - "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.0.tgz", + "integrity": "sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.30.1", - "@typescript-eslint/utils": "8.30.1", + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/utils": "8.31.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1216,9 +1200,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", - "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", + "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", "dev": true, "license": "MIT", "engines": { @@ -1230,14 +1214,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", - "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz", + "integrity": "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1257,16 +1241,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", - "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.0.tgz", + "integrity": "sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/typescript-estree": "8.30.1" + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1281,13 +1265,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", - "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz", + "integrity": "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/types": "8.31.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1549,82 +1533,6 @@ "win32" ] }, - "node_modules/@vscode/vsce/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@vscode/vsce/node_modules/azure-devops-node-api": { - "version": "12.5.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", - "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", - "dev": true, - "license": "MIT", - "dependencies": { - "tunnel": "0.0.6", - "typed-rest-client": "^1.8.4" - } - }, - "node_modules/@vscode/vsce/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@vscode/vsce/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@vscode/vsce/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vscode/vsce/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@vscode/vsce/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@vscode/vsce/node_modules/glob": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", @@ -1665,16 +1573,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@vscode/vsce/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@vscode/vsce/node_modules/jackspeak": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", @@ -1691,16 +1589,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@vscode/vsce/node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, "node_modules/@vscode/vsce/node_modules/lru-cache": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", @@ -1711,31 +1599,6 @@ "node": "20 || >=22" } }, - "node_modules/@vscode/vsce/node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/@vscode/vsce/node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "dev": true, - "license": "MIT" - }, "node_modules/@vscode/vsce/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1777,33 +1640,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@vscode/vsce/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@vscode/vsce/node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vscode/vsce/node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "dev": true, - "license": "MIT" - }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -2115,19 +1951,16 @@ } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "color-convert": "^1.9.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=4" } }, "node_modules/anymatch": { @@ -2158,6 +1991,17 @@ "dev": true, "license": "MIT" }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2446,9 +2290,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001713", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz", - "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==", + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", "dev": true, "funding": [ { @@ -2468,33 +2312,31 @@ "peer": true }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=4" } }, "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/cheerio": { @@ -2639,6 +2481,42 @@ "node": ">=8" } }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2703,22 +2581,19 @@ } }, "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "color-name": "1.1.3" } }, "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, "license": "MIT" }, @@ -2736,12 +2611,14 @@ } }, "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", - "peer": true + "engines": { + "node": ">=18" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -3041,9 +2918,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", - "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==", + "version": "1.5.140", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.140.tgz", + "integrity": "sha512-o82Rj+ONp4Ip7Cl1r7lrqx/pXhbp/lh9DpKcMNscFJdh8ebyRofnc7Sh01B4jx403RI0oqTBvlZ7OBIZLMr2+Q==", "dev": true, "license": "ISC", "peer": true @@ -3216,33 +3093,30 @@ } }, "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8.0" } }, "node_modules/eslint": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", - "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", + "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.24.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.25.1", + "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3319,6 +3193,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3330,6 +3220,56 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", @@ -3356,6 +3296,16 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3369,6 +3319,19 @@ "node": "*" } }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/espree": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", @@ -3452,6 +3415,7 @@ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -3856,13 +3820,13 @@ "license": "MIT" }, "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/has-symbols": { @@ -3930,19 +3894,6 @@ "node": ">=10" } }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4286,6 +4237,16 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/istanbul-lib-report/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4345,6 +4306,17 @@ "node": ">= 10.13.0" } }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -4446,29 +4418,6 @@ "npm": ">=6" } }, - "node_modules/jsonwebtoken/node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -4483,9 +4432,9 @@ } }, "node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", "dev": true, "license": "MIT", "dependencies": { @@ -4495,13 +4444,13 @@ } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "dev": true, "license": "MIT", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, @@ -4562,6 +4511,16 @@ "immediate": "~3.0.5" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -4638,36 +4597,118 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, - "license": "ISC" + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } }, "node_modules/make-dir": { "version": "4.0.0", @@ -4685,6 +4726,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4695,6 +4754,13 @@ "node": ">= 0.4" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4881,6 +4947,22 @@ "node": ">=8" } }, + "node_modules/mocha/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/mocha/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -4893,6 +4975,26 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/mocha/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/mocha/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/mocha/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4900,6 +5002,19 @@ "dev": true, "license": "MIT" }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mocha/node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -4921,6 +5036,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/mocha/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -5155,9 +5280,9 @@ } }, "node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz", + "integrity": "sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==", "dev": true, "license": "MIT", "dependencies": { @@ -5462,6 +5587,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -6098,17 +6230,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4", - "npm": ">=6" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -6363,6 +6484,14 @@ } } }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6481,6 +6610,82 @@ "webpack": "^5.0.0" } }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6551,6 +6756,13 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "node_modules/underscore": { "version": "1.13.7", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", @@ -6625,6 +6837,13 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6724,9 +6943,9 @@ } }, "node_modules/webpack": { - "version": "5.99.5", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.5.tgz", - "integrity": "sha512-q+vHBa6H9qwBLUlHL4Y7L0L1/LlyBKZtS9FHNCQmtayxjI5RKC9yD8gpvLeqGv5lCQp1Re04yi0MF40pf30Pvg==", + "version": "5.99.6", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz", + "integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==", "dev": true, "license": "MIT", "peer": true, @@ -6911,6 +7130,42 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", From b84700b87ba3299dc3d62df325984ca69da74b26 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:38:35 +0100 Subject: [PATCH 0035/1056] feat(vscode): before running commands check if signed in (#4206) --- vscode/extension/eslint.config.mjs | 6 +-- vscode/extension/src/auth/auth.ts | 15 +++++++ vscode/extension/src/commands/format.ts | 43 +++++++++++------- vscode/extension/src/extension.ts | 18 +++++--- vscode/extension/src/lsp/lsp.ts | 14 +++--- vscode/extension/src/utilities/errors.ts | 28 ++++++++++++ .../src/utilities/sqlmesh/sqlmesh.ts | 44 +++++++++++++++---- 7 files changed, 129 insertions(+), 39 deletions(-) create mode 100644 vscode/extension/src/utilities/errors.ts diff --git a/vscode/extension/eslint.config.mjs b/vscode/extension/eslint.config.mjs index 5b14c4d294..d8d2c9985a 100644 --- a/vscode/extension/eslint.config.mjs +++ b/vscode/extension/eslint.config.mjs @@ -20,9 +20,9 @@ export default [{ format: ["camelCase", "PascalCase"], }], - curly: "warn", - eqeqeq: "warn", - "no-throw-literal": "warn", + curly: "error", + eqeqeq: "error", + "no-throw-literal": "error", semi: ["error", "never"] }, }]; \ No newline at end of file diff --git a/vscode/extension/src/auth/auth.ts b/vscode/extension/src/auth/auth.ts index e4c7159e07..3c2902ef36 100644 --- a/vscode/extension/src/auth/auth.ts +++ b/vscode/extension/src/auth/auth.ts @@ -332,3 +332,18 @@ export class AuthenticationProviderTobikoCloud } } } + +/** + * Checks if the user is currently signed into Tobiko Cloud. + * @returns A promise that resolves to true if the user is signed in, false otherwise. + */ +export async function isSignedIntoTobikoCloud(): Promise { + try { + const authProvider = new AuthenticationProviderTobikoCloud() + const sessions = await authProvider.getSessions() + return sessions.length > 0 + } catch (error) { + traceError(`Error checking authentication status: ${error}`) + return false + } +} diff --git a/vscode/extension/src/commands/format.ts b/vscode/extension/src/commands/format.ts index ebedd4c45b..777335c505 100644 --- a/vscode/extension/src/commands/format.ts +++ b/vscode/extension/src/commands/format.ts @@ -1,36 +1,45 @@ -import { traceError, traceLog } from "../utilities/common/log" +import { traceLog } from "../utilities/common/log" import { execSync } from "child_process" import { sqlmesh_exec } from "../utilities/sqlmesh/sqlmesh" -import { isErr } from "../utilities/functional/result" +import { err, isErr, ok, Result } from "../utilities/functional/result" import * as vscode from "vscode" +import { ErrorType, handleNotSginedInError } from "../utilities/errors" +import { AuthenticationProviderTobikoCloud } from "../auth/auth" -export const format = async () => { - traceLog("Calling format") - const out = await internalFormat() - if (out === 0) { +export const format = + (authProvider: AuthenticationProviderTobikoCloud) => async () => { + traceLog("Calling format") + const out = await internalFormat() + if (isErr(out)) { + if (out.error.type === "not_signed_in") { + handleNotSginedInError(authProvider) + return + } else { + vscode.window.showErrorMessage( + `Project format failed: ${out.error.message}` + ) + return + } + } vscode.window.showInformationMessage("Project formatted successfully") - } else { - vscode.window.showErrorMessage("Project format failed") } -} -const internalFormat = async (): Promise => { +const internalFormat = async (): Promise> => { try { const exec = await sqlmesh_exec() if (isErr(exec)) { - traceError(exec.error) - return 1 + return exec } execSync(`${exec.value.bin} format`, { encoding: "utf-8", cwd: exec.value.workspacePath, env: exec.value.env, }) - return 0 + return ok(0) } catch (error: any) { - traceError("Error executing sqlmesh format:", error.message) - traceError(error.stdout) - traceError(error.stderr) - return error.status || 1 + return err({ + type: "generic", + message: `Error executing sqlmesh format: ${error.message}`, + }) } } diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index d7fa8840d3..f4e88d5c52 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -1,5 +1,5 @@ -import * as vscode from "vscode" import { format } from "./commands/format" +import * as vscode from "vscode" import { createOutputChannel, onDidChangeConfiguration, @@ -16,6 +16,8 @@ import { AuthenticationProviderTobikoCloud } from "./auth/auth" import { signOut } from "./commands/signout" import { signIn } from "./commands/signin" import { signInSpecifyFlow } from "./commands/signinSpecifyFlow" +import { isErr } from "./utilities/functional/result" +import { handleNotSginedInError } from "./utilities/errors" let lspClient: LSPClient | undefined @@ -44,7 +46,6 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand("sqlmesh.signin", signIn(authProvider)) ) - context.subscriptions.push( vscode.commands.registerCommand( "sqlmesh.signinSpecifyFlow", @@ -55,19 +56,26 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("sqlmesh.signout", signOut(authProvider)) ) context.subscriptions.push( - vscode.commands.registerCommand("sqlmesh.format", format) + vscode.commands.registerCommand("sqlmesh.format", format(authProvider)) ) lspClient = new LSPClient() - await lspClient.start() + const result = await lspClient.start() + if (isErr(result)) { + handleNotSginedInError(authProvider) + } context.subscriptions.push(lspClient) const restart = async () => { if (lspClient) { traceVerbose("Restarting LSP client") - await lspClient.restart() + const result = await lspClient.restart() + if (isErr(result)) { + handleNotSginedInError(authProvider) + } } } + context.subscriptions.push( onDidChangePythonInterpreter(async () => { await restart() diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 6085b4a192..bdeea35272 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -4,6 +4,7 @@ import { sqlmesh_lsp_exec } from "../utilities/sqlmesh/sqlmesh" import { err, isErr, ok, Result } from "../utilities/functional/result" import { getWorkspaceFolders } from "../utilities/common/vscodeapi" import { traceError } from "../utilities/common/log" +import { ErrorType } from "../utilities/errors" let outputChannel: OutputChannel | undefined @@ -14,7 +15,7 @@ export class LSPClient implements Disposable { this.client = undefined } - public async start(): Promise> { + public async start(): Promise> { if (!outputChannel) { outputChannel = window.createOutputChannel('sqlmesh-lsp') } @@ -22,12 +23,15 @@ export class LSPClient implements Disposable { const sqlmesh = await sqlmesh_lsp_exec() if (isErr(sqlmesh)) { traceError(`Failed to get sqlmesh_lsp_exec: ${sqlmesh.error}`) - return sqlmesh + return sqlmesh } const workspaceFolders = getWorkspaceFolders() if (workspaceFolders.length !== 1) { traceError(`Invalid number of workspace folders: ${workspaceFolders.length}`) - return err("Invalid number of workspace folders") + return err({ + type: "generic", + message: "Invalid number of workspace folders", + }) } let folder = workspaceFolders[0] @@ -67,9 +71,9 @@ export class LSPClient implements Disposable { return ok(undefined) } - public async restart() { + public async restart(): Promise> { await this.stop() - await this.start() + return await this.start() } public async stop() { diff --git a/vscode/extension/src/utilities/errors.ts b/vscode/extension/src/utilities/errors.ts new file mode 100644 index 0000000000..29ab5813cd --- /dev/null +++ b/vscode/extension/src/utilities/errors.ts @@ -0,0 +1,28 @@ +import { window } from "vscode" +import { AuthenticationProviderTobikoCloud } from "../auth/auth" +import { signIn } from "../commands/signin" +import { traceInfo } from "./common/log" + +/** + * Represents different types of errors that can occur in the application. + */ +export type ErrorType = + | { type: "generic"; message: string } + | { type: "not_signed_in" }; + +/** + * Handles the case where the user is not signed in to Tobiko Cloud. + * @param authProvider - The authentication provider to use for signing in. + */ +export const handleNotSginedInError = async ( + authProvider: AuthenticationProviderTobikoCloud +): Promise => { + traceInfo("handleNotSginedInError") + const result = await window.showInformationMessage( + "Please sign in to Tobiko Cloud to use SQLMesh", + "Sign In" + ) + if (result === "Sign In") { + await signIn(authProvider)() + } +} diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index e04569925b..7b1fd25b83 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -7,6 +7,8 @@ import { execFile } from "child_process" import { promisify } from "util" import { isPythonModuleInstalled } from "../python" import fs from "fs" +import { ErrorType } from "../errors" +import { isSignedIntoTobikoCloud } from "../../auth/auth" export type sqlmesh_exec = { workspacePath: string; @@ -51,7 +53,7 @@ export const get_tcloud_bin = async (): Promise> => { * * @returns The sqlmesh executable for the current workspace. */ -export const sqlmesh_exec = async (): Promise> => { +export const sqlmesh_exec = async (): Promise> => { const projectRoot = await getProjectRoot() const workspacePath = projectRoot.uri.fsPath const interpreterDetails = await getInterpreterDetails() @@ -65,14 +67,26 @@ export const sqlmesh_exec = async (): Promise> => { } if (interpreterDetails.isVirtualEnvironment) { traceLog("Using virtual environment") - const tcloudInstalled = await isTcloudProject() - if (isErr(tcloudInstalled)) { - return tcloudInstalled + const isTcloudInstalled = await isTcloudProject() + if (isErr(isTcloudInstalled)) { + return err({ + type: "generic", + message: isTcloudInstalled.error, + }) } - if (tcloudInstalled.value) { + if (isTcloudInstalled.value) { const tcloudBin = await get_tcloud_bin() if (isErr(tcloudBin)) { - return tcloudBin + return err({ + type: "generic", + message: tcloudBin.error, + }) + } + const isSignedIn = await isSignedIntoTobikoCloud() + if (!isSignedIn) { + return err({ + type: "not_signed_in", + }) } return ok({ bin: `${tcloudBin.value} sqlmesh`, @@ -113,7 +127,7 @@ export const sqlmesh_exec = async (): Promise> => { * @returns The sqlmesh_lsp executable for the current workspace. */ export const sqlmesh_lsp_exec = async (): Promise< - Result + Result > => { const projectRoot = await getProjectRoot() const workspacePath = projectRoot.uri.fsPath @@ -130,13 +144,25 @@ export const sqlmesh_lsp_exec = async (): Promise< traceLog("Using virtual environment") const tcloudInstalled = await isTcloudProject() if (isErr(tcloudInstalled)) { - return tcloudInstalled + return err({ + type: "generic", + message: tcloudInstalled.error, + }) } if (tcloudInstalled.value) { traceLog("Tcloud installed, installing sqlmesh") const tcloudBin = await get_tcloud_bin() if (isErr(tcloudBin)) { - return tcloudBin + return err({ + type: "generic", + message: tcloudBin.error, + }) + } + const isSignedIn = await isSignedIntoTobikoCloud() + if (!isSignedIn) { + return err({ + type: "not_signed_in", + }) } const execFileAsync = promisify(execFile) await execFileAsync(tcloudBin.value, ["install_sqlmesh"], { From b46a68c39c0f46feaa6001a0a94576b76cf7ac80 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 22 Apr 2025 12:39:44 +0300 Subject: [PATCH 0036/1056] Chore!: bump sqlglot to v26.16.0 (#4172) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c1485a77df..4c484c492a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.13.2", + "sqlglot[rs]~=26.16.0", "tenacity", "time-machine", "json-stream" From 7d2363de8f7e686b9feaaa688d0451ce504b36f4 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 22 Apr 2025 12:01:55 +0100 Subject: [PATCH 0037/1056] fix(vscode): make id token optional/nullable (#4215) --- vscode/extension/src/auth/auth.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vscode/extension/src/auth/auth.ts b/vscode/extension/src/auth/auth.ts index 3c2902ef36..86c82a70ae 100644 --- a/vscode/extension/src/auth/auth.ts +++ b/vscode/extension/src/auth/auth.ts @@ -27,9 +27,10 @@ const tokenSchema = z.object({ exp: z.number(), email: z.string(), }) + const statusResponseSchema = z.object({ is_logged_in: z.boolean(), - id_token: tokenSchema, + id_token: tokenSchema.optional().nullable(), }) type StatusResponse = z.infer; @@ -298,7 +299,9 @@ export class AuthenticationProviderTobikoCloud ) if (messageResult === "Open browser") { - await env.openExternal(Uri.parse(deviceCodeResponse.verification_uri_complete)) + await env.openExternal( + Uri.parse(deviceCodeResponse.verification_uri_complete) + ) } if (messageResult === "Cancel") { ac.abort() From 99b34d2b0b74f46d35b0e0ce32c053c81bcccffe Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 22 Apr 2025 13:23:02 +0100 Subject: [PATCH 0038/1056] fix(vscode): small typing inconsistency (#4216) --- vscode/extension/src/auth/auth.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vscode/extension/src/auth/auth.ts b/vscode/extension/src/auth/auth.ts index 86c82a70ae..0f9e124ea9 100644 --- a/vscode/extension/src/auth/auth.ts +++ b/vscode/extension/src/auth/auth.ts @@ -97,6 +97,9 @@ export class AuthenticationProviderTobikoCloud return [] } const token = statusResponse.id_token + if (!token) { + throw new Error("Invalid state from tcloud, failed to get token.") + } const session = { id: token.email, account: { @@ -120,6 +123,9 @@ export class AuthenticationProviderTobikoCloud throw new Error("Failed to login to tcloud") } const token = statusResponse.id_token + if (!token) { + throw new Error("Failed to get tcloud token") + } const session: AuthenticationSession = { id: token.email, account: { From aa4dfe8cb4310d40c0afc1abe570f134054d6120 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:22:49 +0100 Subject: [PATCH 0039/1056] feat(vscode): allow specifying openapi output (#4218) --- web/server/openapi.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/web/server/openapi.py b/web/server/openapi.py index b7f2e56158..302688dd53 100644 --- a/web/server/openapi.py +++ b/web/server/openapi.py @@ -11,6 +11,13 @@ def generate_openapi_spec(app: FastAPI, path: str) -> None: if __name__ == "__main__": - app = create_app() + import argparse + + parser = argparse.ArgumentParser(description="Generate OpenAPI specification") + parser.add_argument( + "--output", default="web/client/openapi.json", help="Path to output OpenAPI spec file" + ) + args = parser.parse_args() - generate_openapi_spec(app, "web/client/openapi.json") + app = create_app() + generate_openapi_spec(app, args.output) From fe7ed3167a9e1c817026a633a63badb2e52547a0 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 22 Apr 2025 10:08:55 -0700 Subject: [PATCH 0040/1056] Feat!: First-class support for calling python macros from jinja context (#4211) --- sqlmesh/core/macros.py | 7 ++- sqlmesh/core/model/definition.py | 4 +- sqlmesh/core/renderer.py | 80 +++++++++++++++++++------------- tests/core/test_model.py | 39 +++++++++++++++- 4 files changed, 91 insertions(+), 39 deletions(-) diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index beca4ea72d..8872845731 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -150,10 +150,9 @@ def __init__( self, dialect: DialectType = "", python_env: t.Optional[t.Dict[str, Executable]] = None, - jinja_env: t.Optional[Environment] = None, schema: t.Optional[MappingSchema] = None, runtime_stage: RuntimeStage = RuntimeStage.LOADING, - resolve_table: t.Optional[t.Callable[[str | exp.Expression], str]] = None, + resolve_table: t.Optional[t.Callable[[str | exp.Table], str]] = None, resolve_tables: t.Optional[t.Callable[[exp.Expression], exp.Expression]] = None, snapshots: t.Optional[t.Dict[str, Snapshot]] = None, default_catalog: t.Optional[str] = None, @@ -177,7 +176,7 @@ def __init__( self.columns_to_types_called = False self.default_catalog = default_catalog - self._jinja_env: t.Optional[Environment] = jinja_env + self._jinja_env: t.Optional[Environment] = None self._schema = schema self._resolve_table = resolve_table self._resolve_tables = resolve_tables @@ -431,7 +430,7 @@ def get_snapshot(self, model_name: TableName | exp.Column) -> t.Optional[Snapsho ) ) - def resolve_table(self, table: str | exp.Expression) -> str: + def resolve_table(self, table: str | exp.Table) -> str: """Gets the physical table name for a given model.""" if not self._resolve_table: raise SQLMeshError( diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index ffd6b4e474..1b6ac7c58e 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2476,9 +2476,7 @@ def _create_model( for _, kwargs in model.signals: statements.extend((signal_kwarg, True) for signal_kwarg in kwargs.values()) - python_env = python_env or {} - - make_python_env( + python_env = make_python_env( statements, jinja_macro_references, module_path, diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index 316e0d5ec1..fee9dcb5ee 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -3,6 +3,7 @@ import logging import typing as t from contextlib import contextmanager +from functools import partial from pathlib import Path from sqlglot import exp, parse @@ -134,7 +135,35 @@ def _render( if this_snapshot and (kind := this_snapshot.model_kind_name): kwargs["model_kind_name"] = kind.name - expressions = [self._expression] + def _resolve_table(table: str | exp.Table) -> str: + return self._resolve_table( + d.normalize_model_name(table, self._default_catalog, self._dialect), + snapshots=snapshots, + table_mapping=table_mapping, + deployability_index=deployability_index, + ).sql(dialect=self._dialect, identify=True, comments=False) + + macro_evaluator = MacroEvaluator( + self._dialect, + python_env=self._python_env, + schema=self.schema, + runtime_stage=runtime_stage, + resolve_table=_resolve_table, + resolve_tables=lambda e: self._resolve_tables( + e, + snapshots=snapshots, + table_mapping=table_mapping, + deployability_index=deployability_index, + start=start, + end=end, + execution_time=execution_time, + runtime_stage=runtime_stage, + ), + snapshots=snapshots, + default_catalog=self._default_catalog, + path=self._path, + environment_naming_info=environment_naming_info, + ) start_time, end_time = ( make_inclusive(start or c.EPOCH, end or c.EPOCH, self._dialect) @@ -153,18 +182,17 @@ def _render( variables = kwargs.pop("variables", {}) jinja_env_kwargs = { - **{**render_kwargs, **prepare_env(self._python_env), **variables}, + **{ + **render_kwargs, + **_prepare_python_env_for_jinja(macro_evaluator, self._python_env), + **variables, + }, "snapshots": snapshots or {}, "table_mapping": table_mapping, "deployability_index": deployability_index, "default_catalog": self._default_catalog, "runtime_stage": runtime_stage.value, - "resolve_table": lambda table: self._resolve_table( - d.normalize_model_name(table, self._default_catalog, self._dialect), - snapshots=snapshots, - table_mapping=table_mapping, - deployability_index=deployability_index, - ).sql(dialect=self._dialect, identify=True, comments=False), + "resolve_table": _resolve_table, } if this_model: render_kwargs["this_model"] = this_model @@ -174,6 +202,7 @@ def _render( jinja_env = self._jinja_macro_registry.build_environment(**jinja_env_kwargs) + expressions = [self._expression] if isinstance(self._expression, d.Jinja): try: expressions = [] @@ -190,29 +219,6 @@ def _render( f"Could not render or parse jinja at '{self._path}'.\n{ex}" ) from ex - macro_evaluator = MacroEvaluator( - self._dialect, - python_env=self._python_env, - jinja_env=jinja_env, - schema=self.schema, - runtime_stage=runtime_stage, - resolve_table=jinja_env.globals["resolve_table"], # type: ignore - resolve_tables=lambda e: self._resolve_tables( - e, - snapshots=snapshots, - table_mapping=table_mapping, - deployability_index=deployability_index, - start=start, - end=end, - execution_time=execution_time, - runtime_stage=runtime_stage, - ), - snapshots=snapshots, - default_catalog=self._default_catalog, - path=self._path, - environment_naming_info=environment_naming_info, - ) - macro_evaluator.locals.update(render_kwargs) if variables: @@ -637,3 +643,15 @@ def _optimize_query(self, query: exp.Query, all_deps: t.Set[str]) -> exp.Query: annotate_types(select) return query + + +def _prepare_python_env_for_jinja( + evaluator: MacroEvaluator, + python_env: t.Dict[str, Executable], +) -> t.Dict[str, t.Any]: + prepared_env = prepare_env(python_env) + # Pass the evaluator to all macro functions + return { + key: partial(value, evaluator) if callable(value) else value + for key, value in prepared_env.items() + } diff --git a/tests/core/test_model.py b/tests/core/test_model.py index cd8c29d42e..270414dba0 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -2803,7 +2803,7 @@ def test_parse_expression_list_with_jinja(): def test_no_depends_on_runtime_jinja_query(): @macro() - def runtime_macro(**kwargs) -> None: + def runtime_macro(evaluator, **kwargs) -> None: from sqlmesh.utils.errors import ParsetimeAdapterCallError raise ParsetimeAdapterCallError("") @@ -9051,3 +9051,40 @@ def test_formatting_flag_serde(): deserialized_model = SqlModel.parse_raw(model_json) assert deserialized_model.dict() == model.dict() + + +def test_call_python_macro_from_jinja(): + def noop() -> None: + print("noop") + + @macro() + def test_runtime_stage(evaluator): + noop() + return evaluator.runtime_stage + + expressions = d.parse( + """ + MODEL ( + name db.table, + dialect spark, + owner owner_name, + ); + + JINJA_QUERY_BEGIN; + SELECT '{{ test_runtime_stage() }}' AS a, '{{ test_runtime_stage_jinja('bla') }}' AS b; + JINJA_END; + """ + ) + + jinja_macros = JinjaMacroRegistry( + root_macros={ + "test_runtime_stage_jinja": MacroInfo( + definition="{% macro test_runtime_stage_jinja(value) %}{{ test_runtime_stage() }}_{{ value }}{% endmacro %}", + depends_on=[], + ) + } + ) + + model = load_sql_based_model(expressions, jinja_macros=jinja_macros) + assert model.render_query().sql() == "SELECT 'loading' AS a, 'loading_bla' AS b" + assert set(model.python_env) == {"noop", "test_runtime_stage"} From 443b885edebf37739b7894e492548c64d4734a89 Mon Sep 17 00:00:00 2001 From: Sung Won Chung Date: Tue, 22 Apr 2025 12:55:00 -0700 Subject: [PATCH 0041/1056] SQLMesh CLI Crash Course (#4174) Co-authored-by: Sung Won Chung Co-authored-by: Trey Spiller <1831878+treysp@users.noreply.github.com> --- docs/cloud/cloud_index/tobiko-cloud.png | Bin 569284 -> 232718 bytes docs/examples/overview.md | 5 +- docs/examples/sqlmesh_cli_crash_course.md | 1245 +++++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 1249 insertions(+), 2 deletions(-) create mode 100644 docs/examples/sqlmesh_cli_crash_course.md diff --git a/docs/cloud/cloud_index/tobiko-cloud.png b/docs/cloud/cloud_index/tobiko-cloud.png index 2c3491c59434bd1fef2450d9185c6358ce8c1142..ed2ed69d95aa55a8525575bec427dfca04b9381d 100644 GIT binary patch literal 232718 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

!zVi@H?7m!gKJ&ZNL@1O@x9utTh4)5)DjRBxAC` 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π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 X8-EmE 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 z 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
)t%k16MOgCBwS?lI8TWX?n2t7pB$A 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­IH(FrJSWtqb$lm_3! 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 diff --git a/docs/examples/overview.md b/docs/examples/overview.md index 4f87c1453c..a252b3f9c2 100644 --- a/docs/examples/overview.md +++ b/docs/examples/overview.md @@ -20,9 +20,10 @@ This page links to a few different types of examples: ## Walkthroughs -We're proud to present our very first walkthrough, an in-depth description of incremental by time range models: +Walkthroughs are easy to follow and provide lots of information in a self-contained format. -- [Incremental by Time Range: Full Walkthrough](./incremental_time_full_walkthrough.md) (BigQuery SQL engine) +- Get the SQLMesh workflow under your fingers with the [SQLMesh CLI Crash Course](./sqlmesh_cli_crash_course.md) +- See the end-to-end workflow in action with the [Incremental by Time Range: Full Walkthrough](./incremental_time_full_walkthrough.md) (BigQuery SQL engine) ## Projects diff --git a/docs/examples/sqlmesh_cli_crash_course.md b/docs/examples/sqlmesh_cli_crash_course.md new file mode 100644 index 0000000000..008e5e4422 --- /dev/null +++ b/docs/examples/sqlmesh_cli_crash_course.md @@ -0,0 +1,1245 @@ +# SQLMesh CLI Crash Course + +
+ +This doc is designed to get you intimate with a **majority** of the SQLMesh workflows you’ll use to build *and* maintain transformation data pipelines. The goal is to get SQLMesh into muscle memory in 30 minutes or less. + +This doc is inspired by community observations, face-to-face conversations, live screenshares, and debugging sessions. This is *not* an exhaustive list but is rooted in lived experience. + +You can follow along in the [open source GitHub repo](https://github.com/sungchun12/sqlmesh-cli-crash-course). + +If you're new to how SQLMesh uses virtual data environments, [watch this quick explainer](https://www.loom.com/share/216835d64b3a4d56b2e061fa4bd9ee76?sid=88b3289f-e19b-4ccc-8b88-3faf9d7c9ce3). + +!!! tip + + Put this page on your second monitor or in a side by side window to swiftly copy/paste into your terminal. + +## Development Workflow + +You’ll use these commands 80% of the time because this is how you apply the changes you make to models. The workflow is: + +1. Make changes to your models directly in SQL and python files (pre-made in examples below) +2. Plan the changes in your dev environment +3. Apply the changes to your dev environment +4. Audit the changes (test data quality) +5. Run data diff against prod +6. Apply the changes to prod + +### Preview, Apply, and Audit Changes in `dev` + +You can make changes quickly and confidently through one simple command: `sqlmesh plan dev` + +- Plan the changes in your dev environment. +- Apply the changes to your dev environment by entering `y` at the prompt. +- Audit the changes (test data quality). This happens automatically when you apply the changes to dev. + +Note: If you run this without making any changes, SQLMesh will prompt you to make changes or use the `--include-unmodified` flag like this `sqlmesh plan dev --include-unmodified`. We recommend you make changes first before running this command to avoid creating a lot of noise in your dev environment with extraneous virtual layer views. + +=== "SQLMesh" + + ```bash + sqlmesh plan dev + ``` + + ```bash + sqlmesh plan + ``` + + If you want to move faster, you can add the `--auto-apply` flag to skip the manual prompt and apply the plan. You should do this when you're familiar with the plan output, and don't need to see tiny changes in the diff output before applying the plan. + + ```bash + sqlmesh plan --auto-apply + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh plan dev + ``` + + ```bash + tcloud sqlmesh plan + ``` + + If you want to move faster, you can add the `--auto-apply` flag to skip the manual prompt and apply the plan. You should do this when you're familiar with the plan output, and don't need to see tiny changes in the diff output before applying the plan. + + ```bash + tcloud sqlmesh plan --auto-apply + ``` + +??? "Example Output" + I made a breaking change to `incremental_model` and `full_model`. + + SQLMesh: + + - Showed me the models impacted by the changes. + - Showed me the changes that will be made to the models. + - Showed me the models that need to be backfilled. + - Prompted me to apply the changes to `dev`. + - Showed me the audit failures that raise as warnings. + - Updated the physical layer to validate the SQL. + - Executed the model batches by inserting the data into the physical layer. + - Updated the virtual layer's view pointers to reflect the changes. + + ```bash + > sqlmesh plan dev + Differences from the `dev` environment: + + Models: + ├── Directly Modified: + │ ├── sqlmesh_example__dev.incremental_model + │ └── sqlmesh_example__dev.full_model + └── Indirectly Modified: + └── sqlmesh_example__dev.view_model + + --- + + +++ + + @@ -9,7 +9,8 @@ + + SELECT + item_id, + COUNT(DISTINCT id) AS num_orders, + - 6 AS new_column + + new_column + FROM sqlmesh_example.incremental_model + GROUP BY + - item_id + + item_id, + + new_column + + Directly Modified: sqlmesh_example__dev.full_model (Breaking) + + --- + + +++ + + @@ -15,7 +15,7 @@ + + id, + item_id, + event_date, + - 5 AS new_column + + 7 AS new_column + FROM sqlmesh_example.seed_model + WHERE + event_date BETWEEN @start_date AND @end_date + + Directly Modified: sqlmesh_example__dev.incremental_model (Breaking) + └── Indirectly Modified Children: + └── sqlmesh_example__dev.view_model (Indirect Breaking) + Models needing backfill: + ├── sqlmesh_example__dev.full_model: [full refresh] + ├── sqlmesh_example__dev.incremental_model: [2020-01-01 - 2025-04-16] + └── sqlmesh_example__dev.view_model: [recreate view] + Apply - Backfill Tables [y/n]: y + + Updating physical layer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 2/2 • 0:00:00 + + ✔ Physical layer updated + + [1/1] sqlmesh_example__dev.incremental_model [insert 2020-01-01 - 2025-04-16] 0.03s + Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0% • pending • 0:00:00 + sqlmesh_example__dev.incremental_model . + [WARNING] sqlmesh_example__dev.full_model: 'assert_positive_order_ids' audit error: 2 rows failed. Learn more in logs: + /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/logs/sqlmesh_2025_04_18_10_33_43.log + [1/1] sqlmesh_example__dev.full_model [full refresh, audits ❌1] 0.01s + Executing model batches ━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━ 33.3% • 1/3 • 0:00:00 + sqlmesh_example__dev.full_model . + [WARNING] sqlmesh_example__dev.view_model: 'assert_positive_order_ids' audit error: 2 rows failed. Learn more in logs: + /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/logs/sqlmesh_2025_04_18_10_33_43.log + [1/1] sqlmesh_example__dev.view_model [recreate view, audits ✔2 ❌1] 0.01s + Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 + + ✔ Model batches executed + + Updating virtual layer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 + + ✔ Virtual layer updated + ``` + +### Run Data Diff Against Prod + +Run data diff against prod. This is a good way to verify the changes are behaving as expected **after** applying them to `dev`. + +=== "SQLMesh" + + ```bash + sqlmesh table_diff prod:dev sqlmesh_example.full_model --show-sample + ``` + + ```bash + sqlmesh table_diff : --show-sample + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh table_diff prod:dev sqlmesh_example.full_model --show-sample + ``` + + ```bash + tcloud sqlmesh table_diff : --show-sample + ``` + +??? "Example Output" + I compare the `prod` and `dev` environments for `sqlmesh_example.full_model`. + + - Verified environments and models to diff along with the join on grain configured. + - Showed me schema diffs between the environments. + - Showed me row count diffs between the environments. + - Showed me common rows stats between the environments. + - Showed me sample data differences between the environments. + - This is where your human judgement comes in to verify the changes are behaving as expected. + + Model definition: + ```sql linenums="1" hl_lines="6" + -- models/full_model.sql + MODEL ( + name sqlmesh_example.full_model, + kind FULL, + cron '@daily', + grain item_id, -- grain is optional BUT necessary for table diffs to work correctly. It's your primary key that is unique and not null. + audits (assert_positive_order_ids), + ); + + SELECT + item_id, + COUNT(DISTINCT id) AS num_orders, + new_column + FROM + sqlmesh_example.incremental_model + GROUP BY item_id, new_column + ``` + + Table diff: + ```bash + > sqlmesh table_diff prod:dev sqlmesh_example.full_model --show-sample + Table Diff + ├── Model: + │ └── sqlmesh_example.full_model + ├── Environment: + │ ├── Source: prod + │ └── Target: dev + ├── Tables: + │ ├── Source: db.sqlmesh_example.full_model + │ └── Target: db.sqlmesh_example__dev.full_model + └── Join On: + └── item_id + + Schema Diff Between 'PROD' and 'DEV' environments for model 'sqlmesh_example.full_model': + └── Schemas match + + + Row Counts: + └── PARTIAL MATCH: 5 rows (100.0%) + + COMMON ROWS column comparison stats: + pct_match + num_orders 100.0 + new_column 0.0 + + + COMMON ROWS sample data differences: + Column: new_column + ┏━━━━━━━━━┳━━━━━━┳━━━━━┓ + ┃ item_id ┃ PROD ┃ DEV ┃ + ┡━━━━━━━━━╇━━━━━━╇━━━━━┩ + │ -11 │ 5 │ 7 │ + │ -3 │ 5 │ 7 │ + │ 1 │ 5 │ 7 │ + │ 3 │ 5 │ 7 │ + │ 9 │ 5 │ 7 │ + └─────────┴──────┴─────┘ + ``` + +### Apply Changes to Prod + +After you feel confident about the changes, apply them to `prod`. + +!!! warning "Apply the changes to prod" + We recommend only applying changes to `prod` [**using CI/CD**](../integrations/github.md) as best practice. + For learning purposes and hot fixes, you can manually apply the changes to prod by entering `y` at the prompt. + +=== "SQLMesh" + + ```bash + sqlmesh plan + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh plan + ``` + +??? "Example Output" + After I feel confident about the changes, I apply them to `prod`. + + SQLMesh: + + - Showed me the models impacted by the changes. + - Showed me the changes that will be made to the models. + - Showed me the models that need to be backfilled. None in this case as it was already backfilled earlier in `dev`. + - Prompted me to apply the changes to `prod`. + - Showed me physical layer and execution steps are skipped as the changes were already applied to `dev`. + - Updated the virtual layer view pointers to reflect the changes. + + ```bash + > sqlmesh plan + Differences from the `prod` environment: + + Models: + ├── Directly Modified: + │ ├── sqlmesh_example.full_model + │ └── sqlmesh_example.incremental_model + └── Indirectly Modified: + └── sqlmesh_example.view_model + + --- + + +++ + + @@ -9,7 +9,8 @@ + + SELECT + item_id, + COUNT(DISTINCT id) AS num_orders, + - 5 AS new_column + + new_column + FROM sqlmesh_example.incremental_model + GROUP BY + - item_id + + item_id, + + new_column + + Directly Modified: sqlmesh_example.full_model (Breaking) + + --- + + +++ + + @@ -15,7 +15,7 @@ + + id, + item_id, + event_date, + - 5 AS new_column + + 7 AS new_column + FROM sqlmesh_example.seed_model + WHERE + event_date BETWEEN @start_date AND @end_date + + Directly Modified: sqlmesh_example.incremental_model (Breaking) + └── Indirectly Modified Children: + └── sqlmesh_example.view_model (Indirect Breaking) + Apply - Virtual Update [y/n]: y + + SKIP: No physical layer updates to perform + + SKIP: No model batches to execute + + Updating virtual layer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 + + ✔ Virtual layer updated + ``` + +--- + +## Enhanced Testing Workflow + +You'll use these commands to validate your changes are behaving as expected. Audits (data tests) are a great first step, and you'll want to grow from there to feel confident about your pipelines. The workflow is as follows: + +1. Create and audit external models outside of SQLMesh's control (ex: data loaded in by Fivetran, Airbyte, etc.) +2. Automatically generate unit tests for your models +3. Ad hoc query the data directly in the CLI +4. Lint your models to catch known syntax errors + +--- + +### Create and Audit External Models + +Sometimes models `SELECT` from tables/views that are outside of SQLMesh's control. SQLMesh can automatically parse their fully qualified names from model definitions (ex: `bigquery-public-data`.`ga4_obfuscated_sample_ecommerce`.`events_20210131`) and determine their full schemas and column data types. + +These "external model" schemas are used for column level lineage. You can also add audits to test data quality. If an audit fails, SQLMesh prevents downstream models from wastefully running. + +=== "SQLMesh" + + ```bash + sqlmesh create_external_models + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh create_external_models + ``` + +??? "Example Output" + Note: this is an example from a separate Tobiko Cloud project, so you can't follow along in the Github repo. + + - Generated external models from the `bigquery-public-data`.`ga4_obfuscated_sample_ecommerce`.`events_20210131` table parsed in the model's SQL. + - Added an audit to the external model to ensure `event_date` is not NULL. + - Viewed a plan preview of the changes that will be made for the external model. + + ```sql linenums="1" hl_lines="29" title="models/external_model_example.sql" + MODEL ( + name tcloud_demo.external_model + ); + + SELECT + event_date, + event_timestamp, + event_name, + event_params, + event_previous_timestamp, + event_value_in_usd, + event_bundle_sequence_id, + event_server_timestamp_offset, + user_id, + user_pseudo_id, + privacy_info, + user_properties, + user_first_touch_timestamp, + user_ltv, + device, + geo, + app_info, + traffic_source, + stream_id, + platform, + event_dimensions, + ecommerce + /* items */ + FROM bigquery-public-data.ga4_obfuscated_sample_ecommerce.events_20210131 -- I fully qualified the external table name and sqlmesh will automatically create the external model + ``` + + `sqlmesh create_external_models` output file: + + ```yaml linenums="1" hl_lines="2 3 4" title="external_models.yaml" + - name: '`bigquery-public-data`.`ga4_obfuscated_sample_ecommerce`.`events_20210131`' + audits: # I added this audit manually to the external model YAML file + - name: not_null + columns: "[event_date]" + columns: + event_date: STRING + event_timestamp: INT64 + event_name: STRING + event_params: ARRAY>> + event_previous_timestamp: INT64 + event_value_in_usd: FLOAT64 + event_bundle_sequence_id: INT64 + event_server_timestamp_offset: INT64 + user_id: STRING + user_pseudo_id: STRING + privacy_info: STRUCT + user_properties: ARRAY>> + user_first_touch_timestamp: INT64 + user_ltv: STRUCT + device: STRUCT> + geo: STRUCT + app_info: STRUCT + traffic_source: STRUCT + stream_id: INT64 + platform: STRING + event_dimensions: STRUCT + ecommerce: STRUCT + items: ARRAY> + gateway: public-demo + ``` + + ```bash + > sqlmesh plan dev_sung + Differences from the `dev_sung` environment: + + Models: + └── Metadata Updated: + └── "bigquery-public-data".ga4_obfuscated_sample_ecommerce__dev_sung.events_20210131 + + --- + + +++ + + @@ -29,5 +29,6 @@ + + ecommerce STRUCT, + items ARRAY> + ), + + audits (not_null('columns' = [event_date])), + gateway `public-demo` + ) + + Metadata Updated: "bigquery-public-data".ga4_obfuscated_sample_ecommerce__dev_sung.events_20210131 + Models needing backfill: + └── "bigquery-public-data".ga4_obfuscated_sample_ecommerce__dev_sung.events_20210131: [full refresh] + Apply - Backfill Tables [y/n]: + ``` + +### Automatically Generate Unit Tests + +You can ensure business logic is working as expected by running your models against static sample data. + +Unit tests run *before* a plan is applied automatically. This is great for testing complex business logic (ex: `CASE WHEN` conditions) *before* you backfill data. No need to write them manually, either! + +=== "SQLMesh" + + Create a unit test based on 5 rows from the upstream `sqlmesh_example.incremental_model`. + + ```bash + sqlmesh create_test sqlmesh_example.full_model \ + --query sqlmesh_example.incremental_model \ + "select * from sqlmesh_example.incremental_model limit 5" + ``` + + ```bash + sqlmesh create_test \ + --query \ + "select * from limit 5" + ``` + + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh create_test demo.stg_payments \ + --query demo.seed_raw_payments \ + "select * from demo.seed_raw_payments limit 5" + ``` + + ```bash + tcloud sqlmesh create_test \ + --query \ + "select * from limit 5" + ``` + +??? "Example Output" + + SQLMesh: + + - Generated unit tests for the `sqlmesh_example.full_model` model by live querying the data. + - Ran the tests and they passed locally in DuckDB. + - If you're using a cloud data warehouse, this will transpile your SQL syntax to its equivalent in duckdb. + - This runs fast and free on your local machine. + + Generated test definition file: + + ```yaml linenums="1" title="tests/test_full_model.yaml" + test_full_model: + model: '"db"."sqlmesh_example"."full_model"' + inputs: + '"db"."sqlmesh_example"."incremental_model"': + - id: -11 + item_id: -11 + event_date: 2020-01-01 + new_column: 7 + - id: 1 + item_id: 1 + event_date: 2020-01-01 + new_column: 7 + - id: 3 + item_id: 3 + event_date: 2020-01-03 + new_column: 7 + - id: 4 + item_id: 1 + event_date: 2020-01-04 + new_column: 7 + - id: 5 + item_id: 1 + event_date: 2020-01-05 + new_column: 7 + outputs: + query: + - item_id: 3 + num_orders: 1 + new_column: 7 + - item_id: 1 + num_orders: 3 + new_column: 7 + - item_id: -11 + num_orders: 1 + new_column: 7 + ``` + + Manually execute tests with `sqlmesh test`: + + ```bash + (demo) ➜ demo git:(main) ✗ sqlmesh test + . + ---------------------------------------------------------------------- + Ran 1 test in 0.053s + + OK + ``` + + ```bash + # what do we see if the test fails? + (demo) ➜ demo git:(main) ✗ sqlmesh test + F + ====================================================================== + FAIL: test_full_model (/Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/tests/test_full_model.yaml) + None + ---------------------------------------------------------------------- + AssertionError: Data mismatch (exp: expected, act: actual) + + new_column + exp act + 0 0.0 7.0 + + ---------------------------------------------------------------------- + Ran 1 test in 0.020s + + FAILED (failures=1) + ``` + +### Run Ad-Hoc Queries + +You can run live queries directly from the CLI. This is great to validate the look and feel of your changes without context switching to your query console. + +Pro tip: run this after `sqlmesh table_diff` to get a full picture of your changes. + +=== "SQLMesh" + + ```bash + sqlmesh fetchdf "select * from sqlmesh_example__dev.full_model limit 5" + ``` + + ```bash + # construct arbitrary query + sqlmesh fetchdf "select * from . limit 5" # double underscore in schema name is important. Not needed for prod. + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh fetchdf "select * from sqlmesh_example__dev.full_model limit 5" + ``` + + ```bash + # construct arbitrary query + tcloud sqlmesh fetchdf "select * from . limit 5" # double underscore in schema name is important. Not needed for prod. + ``` + +??? "Example Output" + ```bash + item_id num_orders new_column + 0 9 1 7 + 1 -11 1 7 + 2 3 1 7 + 3 -3 1 7 + 4 1 4 7 + ``` + +### Linting + +If enabled, linting runs automatically during development. The linting rules can be overridden per model, too. + +This is a great way to catch SQL issues before wasting runtime in your data warehouse. It runs automatically, or you can run it manually to proactively check for any issues. + +=== "SQLMesh" + + ```bash + sqlmesh lint + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh lint + ``` + +??? "Example Output" + + You add linting rules in your `config.yaml` file. + + ```yaml linenums="1" hl_lines="13-17" title="config.yaml" + gateways: + duckdb: + connection: + type: duckdb + database: db.db + + default_gateway: duckdb + + model_defaults: + dialect: duckdb + start: 2025-03-26 + + linter: + enabled: true + rules: ["ambiguousorinvalidcolumn", "invalidselectstarexpansion"] # raise errors for these rules + warn_rules: ["noselectstar", "nomissingaudits"] + # ignored_rules: ["noselectstar"] + ``` + + ```bash + > sqlmesh lint + [WARNING] Linter warnings for /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/lint_warn.sql: + - noselectstar: Query should not contain SELECT * on its outer most projections, even if it can be + expanded. + - nomissingaudits: Model `audits` must be configured to test data quality. + [WARNING] Linter warnings for + /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/incremental_by_partition.sql: + - nomissingaudits: Model `audits` must be configured to test data quality. + [WARNING] Linter warnings for /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/seed_model.sql: + - nomissingaudits: Model `audits` must be configured to test data quality. + [WARNING] Linter warnings for + /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/incremental_by_unique_key.sql: + - nomissingaudits: Model `audits` must be configured to test data quality. + [WARNING] Linter warnings for + /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/incremental_model.sql: + - nomissingaudits: Model `audits` must be configured to test data quality. + ``` + +## Debugging Workflow + +You'll use these commands as needed to validate that your changes are behaving as expected. This is great to get more details beyond the defaults above. The workflow is as follows: + +1. Render the model to verify the SQL is looking as expected. +2. Run SQLMesh in verbose mode so you can verify its behavior. +3. View the logs easily in your terminal. + +### Render your SQL Changes + +This is a great way to verify that your model's SQL is looking as expected before applying the changes. It is especially important if you're migrating from one query engine to another (ex: postgres to databricks). + +=== "SQLMesh" + + ```bash + sqlmesh render sqlmesh_example.incremental_model + ``` + + ```bash + sqlmesh render sqlmesh_example.incremental_model --dialect databricks + ``` + + ```bash + sqlmesh render --dialect + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh render sqlmesh_example.incremental_model + ``` + + ```bash + tcloud sqlmesh render sqlmesh_example.incremental_model --dialect databricks + ``` + + ```bash + tcloud sqlmesh render --dialect + ``` + +??? "Example Output" + + Model definition: + + ```sql linenums="1" title="models/incremental_model.sql" + MODEL ( + name sqlmesh_example.incremental_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column event_date + ), + start '2020-01-01', + cron '@daily', + grain (id, event_date) + ); + + SELECT + id, + item_id, + event_date, + 7 as new_column + FROM + sqlmesh_example.seed_model + WHERE + event_date BETWEEN @start_date AND @end_date + ``` + + SQLMesh returns the full SQL code in the default or target dialect. + + ```sql hl_lines="11" + > sqlmesh render sqlmesh_example.incremental_model + -- rendered sql in default dialect + SELECT + "seed_model"."id" AS "id", + "seed_model"."item_id" AS "item_id", + "seed_model"."event_date" AS "event_date", + 7 AS "new_column" + FROM "db"."sqlmesh__sqlmesh_example"."sqlmesh_example__seed_model__3294646944" AS "seed_model" /* + db.sqlmesh_example.seed_model */ + WHERE + "seed_model"."event_date" <= CAST('1970-01-01' AS DATE) -- placeholder dates for date macros + AND "seed_model"."event_date" >= CAST('1970-01-01' AS DATE) + ``` + + ```sql + > sqlmesh render sqlmesh_example.incremental_model --dialect databricks + -- rendered sql in databricks dialect + SELECT + `seed_model`.`id` AS `id`, + `seed_model`.`item_id` AS `item_id`, + `seed_model`.`event_date` AS `event_date`, + 7 AS `new_column` + FROM `db`.`sqlmesh__sqlmesh_example`.`sqlmesh_example__seed_model__3294646944` AS `seed_model` /* + db.sqlmesh_example.seed_model */ + WHERE + `seed_model`.`event_date` <= CAST('1970-01-01' AS DATE) + AND `seed_model`.`event_date` >= CAST('1970-01-01' AS DATE) + ``` + +### Apply Plan Changes in Verbose Mode + +Verbose mode lets you see detailed operations in the physical and virtual layers. This is useful to see exactly what SQLMesh is doing every step. After, you can copy/paste the fully qualified table/view name into your query console to validate the data (if that's your preference). + +=== "SQLMesh" + + ```bash + sqlmesh plan dev -vv + ``` + + ```bash + sqlmesh plan -vv + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh plan dev -vv + ``` + + ```bash + tcloud sqlmesh plan -vv + ``` + +??? "Example Output" + + ```bash hl_lines="48-50" + > sqlmesh plan dev -vv + [WARNING] Linter warnings for + /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/incremental_by_partition.sql: + - nomissingaudits: Model `audits` must be configured to test data quality. + [WARNING] Linter warnings for /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/seed_model.sql: + - nomissingaudits: Model `audits` must be configured to test data quality. + [WARNING] Linter warnings for + /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/incremental_by_unique_key.sql: + - nomissingaudits: Model `audits` must be configured to test data quality. + [WARNING] Linter warnings for + /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/incremental_model.sql: + - nomissingaudits: Model `audits` must be configured to test data quality. + + Differences from the `dev` environment: + + Models: + ├── Directly Modified: + │ └── db.sqlmesh_example__dev.incremental_model + └── Indirectly Modified: + ├── db.sqlmesh_example__dev.full_model + └── db.sqlmesh_example__dev.view_model + + --- + + +++ + + @@ -15,7 +15,7 @@ + + id, + item_id, + event_date, + - 9 AS new_column + + 7 AS new_column + FROM sqlmesh_example.seed_model + WHERE + event_date BETWEEN @start_date AND @end_date + + Directly Modified: db.sqlmesh_example__dev.incremental_model (Breaking) + └── Indirectly Modified Children: + ├── db.sqlmesh_example__dev.full_model (Breaking) + └── db.sqlmesh_example__dev.view_model (Indirect Breaking) + Apply - Virtual Update [y/n]: y + + SKIP: No physical layer updates to perform + + SKIP: No model batches to execute + + db.sqlmesh_example__dev.incremental_model updated # you'll notice that it's updated vs. promoted because we changed the existing view definition + db.sqlmesh_example__dev.full_model updated + db.sqlmesh_example__dev.view_model updated + Updating virtual layer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 + + ✔ Virtual layer updated + ``` + +### View Logs Easily + +Each time you perform a SQLMesh command, it creates a log file in the `logs` directory. You can view them by manually navigating to the correct file name with latest timestamp or with this simple shell command. + +This is useful to see the exact queries that were executed to apply your changes. Admittedly, this is outside of native functionality, but it's a quick and easy way to view logs. + +```bash +# install this open source tool that enhances the default `cat` command +# https://github.com/sharkdp/bat +brew install bat # installation command if using homebrew +``` + +```bash +bat --theme='ansi' $(ls -t logs/ | head -n 1 | sed 's/^/logs\//') +``` + +- In simple terms this command works like this: "Show me the contents of the newest log file in the `logs/` directory, with nice formatting and syntax highlighting.” +- press `q` to quit out of big files in the terminal + +??? "Example Output" + + This is the log file for the `sqlmesh plan dev` command. If you want to see the log file directly, you can click on the file path in the output to open it in your code editor. + + ```bash + ──────┬────────────────────────────────────────────────────────────────────────────────────────────── + │ File: logs/sqlmesh_2025_04_18_12_34_35.log + ──────┼────────────────────────────────────────────────────────────────────────────────────────────── + 1 │ 2025-04-18 12:34:35,715 - MainThread - sqlmesh.core.config.connection - INFO - Creating new D + │ uckDB adapter for data files: {'db.db'} (connection.py:319) + 2 │ 2025-04-18 12:34:35,951 - MainThread - sqlmesh.core.console - WARNING - Linter warnings for / + │ Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/incremental_by_partition.sql: + 3 │ - nomissingaudits: Model `audits` must be configured to test data quality. (console.py:1848) + 4 │ 2025-04-18 12:34:35,953 - MainThread - sqlmesh.core.console - WARNING - Linter warnings for / + │ Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/seed_model.sql: + 5 │ - nomissingaudits: Model `audits` must be configured to test data quality. (console.py:1848) + 6 │ 2025-04-18 12:34:35,953 - MainThread - sqlmesh.core.console - WARNING - Linter warnings for / + │ Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/incremental_by_unique_key.sql: + 7 │ - nomissingaudits: Model `audits` must be configured to test data quality. (console.py:1848) + 8 │ 2025-04-18 12:34:35,953 - MainThread - sqlmesh.core.console - WARNING - Linter warnings for / + │ Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/models/incremental_model.sql: + 9 │ - nomissingaudits: Model `audits` must be configured to test data quality. (console.py:1848) + 10 │ 2025-04-18 12:34:35,954 - MainThread - sqlmesh.core.config.connection - INFO - Using existing + │ DuckDB adapter due to overlapping data file: db.db (connection.py:309) + 11 │ 2025-04-18 12:34:37,071 - MainThread - sqlmesh.core.snapshot.evaluator - INFO - Listing data + │ objects in schema db.sqlmesh__sqlmesh_example (evaluator.py:338) + 12 │ 2025-04-18 12:34:37,072 - MainThread - sqlmesh.core.engine_adapter.base - INFO - Executing SQ + │ L: SELECT CURRENT_CATALOG() (base.py:2128) + 13 │ 2025-04-18 12:34:37,072 - MainThread - sqlmesh.core.engine_adapter.base - INFO - Executing SQ + │ L: SELECT CURRENT_CATALOG() (base.py:2128) + ``` + +## Run on Production Schedule + +SQLMesh schedules your transformation on a per-model basis in proper DAG order. This makes it easy to configure how often each step in your pipeline runs to backfill data. + +SQLMesh won't schedule models whose upstream models are late or failed, and they will rerun from point of failure by default! + +Example scenario and model DAG: + +`stg_transactions`(cron: `@hourly`) -> `fct_transcations`(cron: `@daily`). All times in UTC. + +1. `stg_transactions` runs hourly +2. `fct_transcations` runs at 12am UTC if `stg_transactions` is fresh and updated since its most recent hour interval +3. If `stg_transactions` failed from 11pm-11:59:59pm, it will prevent `fct_transcations` from running and put it in a `pending` state +4. If `fct_transactions` is `pending` past its full interval (1 full day), it will be put in a `late` state +5. Once `stg_transactions` runs successfully either from a retry or a fix from a pull request, `fct_transactions` will rerun from the point of failure. This is true even if `fct_transactions` has been `late` for several days. + +Note: `pending` and `late` states are only supported in Tobiko Cloud. In SQLMesh, it will only understand if the model is ready or not ready to execute without mention of these states. + +If you're using open source SQLMesh, you can run this command in your orchestrator (ex: Dagster, GitHub Actions, etc.) every 5 minutes or at your lowest model cron schedule (ex: every 1 hour). Don't worry! It will only run executions that need to be run. + +If you're using Tobiko Cloud, this configures automatically without additional configuration. + +### Run Models + +This command is intended be run on a schedule. It will skip the physical and virtual layer updates and simply execute the model batches. + +=== "SQLMesh" + + ```bash + sqlmesh run + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh run + ``` + +??? "Example Output" + + This is what it looks like if models are ready to run. + + ```bash + > sqlmesh run + [1/1] sqlmesh_example.incremental_model [insert 2025-04-17 - 2025-04-17] + 0.01s + [1/1] sqlmesh_example.incremental_unique_model [insert/update rows] + 0.01s + [1/1] sqlmesh_example_v3.incremental_partition_model [insert partitions] + 0.01s + Executing model batches ━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━ 40.0% • 2/5 • 0:00:00 + sqlmesh_example_v3.incremental_partition_model . + [WARNING] sqlmesh_example.full_model: 'assert_positive_order_ids' audit error: 2 rows failed. Learn + more in logs: /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/logs/sqlmesh_2025_04_18_12_48_35.log + [1/1] sqlmesh_example.full_model [full refresh, audits ❌1] + 0.01s + Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━ 80.0% • 4/5 • 0:00:00 + sqlmesh_example.view_model . + [WARNING] sqlmesh_example.view_model: 'assert_positive_order_ids' audit error: 2 rows failed. Learn + more in logs: /Users/sung/Desktop/git_repos/sqlmesh-cli-revamp/logs/sqlmesh_2025_04_18_12_48_35.log + [1/1] sqlmesh_example.view_model [recreate view, audits ✔2 ❌1] + 0.01s + Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 5/5 • 0:00:00 + + ✔ Model batches executed + + Run finished for environment 'prod' + ``` + + This is what it looks like if no models are ready to run. + + ```bash + > sqlmesh run + No models are ready to run. Please wait until a model `cron` interval has elapsed. + + Next run will be ready at 2025-04-18 05:00PM PDT (2025-04-19 12:00AM UTC). + ``` + +### Run Models with Incomplete Intervals (Warning) + +You can run models that execute backfills each time you invoke a `run`, whether ad hoc or on a schedule. + +!!! warning "Run Models with Incomplete Intervals" + This only applies to incremental models that have `allow_partials` set to `true`. + This is generally not recommended for production environments as you risk shipping incomplete data which will be perceived as broken data. + +=== "SQLMesh" + + ```bash + sqlmesh run --ignore-cron + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh run --ignore-cron + ``` + +??? "Example Output" + + Model definition: + ```sql linenums="1" hl_lines="15" title="models/incremental_model.sql" + MODEL ( + name sqlmesh_example.incremental_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column event_date + ), + start '2020-01-01', + cron '@daily', + grain (id, event_date), + audits( UNIQUE_VALUES(columns = ( + id, + )), NOT_NULL(columns = ( + id, + event_date + ))), + allow_partials true + ); + + SELECT + id, + item_id, + event_date, + 16 as new_column + FROM + sqlmesh_example.seed_model + WHERE + event_date BETWEEN @start_date AND @end_date + ``` + + ```bash + > sqlmesh run --ignore-cron + [1/1] sqlmesh_example.incremental_model [insert 2025-04-19 - 2025-04-19, audits ✔2] 0.05s + Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1/1 • 0:00:00 + + ✔ Model batches executed + + Run finished for environment 'prod' + ``` + +## Forward-Only Development Workflow + +This is an advanced workflow and specifically designed for large incremental models (ex: > 200 million rows) that take a long time to run even during development. It solves for: + +- Transforming data with schema evolution in `struct` and nested `array` data types. +- Retaining history of a calculated column and applying a new calculation to new rows going forward. +- Retain history of a column with complex conditional `CASE WHEN` logic and apply new conditions to new rows going forward. + +When you modify a forward-only model and apply the plan to `prod` after the dev workflow, it will NOT backfill historical data. It will only execute model batches for new intervals **going forward in time** (i.e., only for new rows). + +If you want to see a full walkthrough, [go here](incremental_time_full_walkthrough.md). + +=== "SQLMesh" + + ```bash + sqlmesh plan dev --forward-only + ``` + + ```bash + sqlmesh plan --forward-only + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh plan dev --forward-only + ``` + + ```bash + tcloud sqlmesh plan --forward-only + ``` + +??? "Example Output" + + - I applied a change to a new column + - It impacts 2 downstream models + - I enforced a forward-only plan to avoid backfilling historical data for the incremental model (ex: `preview` language in the CLI output) + - I previewed the changes in a clone of the incremental impacted (clones will NOT be reused in production) along with the full and view models (these are NOT clones). + + ```bash + > sqlmesh plan dev + Differences from the `dev` environment: + + Models: + ├── Directly Modified: + │ └── sqlmesh_example__dev.incremental_model + └── Indirectly Modified: + ├── sqlmesh_example__dev.view_model + └── sqlmesh_example__dev.full_model + + --- + + +++ + + @@ -16,7 +16,7 @@ + + id, + item_id, + event_date, + - 9 AS new_column + + 10 AS new_column + FROM sqlmesh_example.seed_model + WHERE + event_date BETWEEN @start_date AND @end_date + + Directly Modified: sqlmesh_example__dev.incremental_model (Forward-only) + └── Indirectly Modified Children: + ├── sqlmesh_example__dev.full_model (Forward-only) + └── sqlmesh_example__dev.view_model (Forward-only) + Models needing backfill: + ├── sqlmesh_example__dev.full_model: [full refresh] (preview) + ├── sqlmesh_example__dev.incremental_model: [2025-04-17 - 2025-04-17] (preview) + └── sqlmesh_example__dev.view_model: [recreate view] (preview) + Apply - Preview Tables [y/n]: y + + Updating physical layer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 + + ✔ Physical layer updated + + [1/1] sqlmesh_example__dev.incremental_model [insert 2025-04-17 - 2025-04-17] 0.01s + [1/1] sqlmesh_example__dev.full_model [full refresh, audits ✔1] 0.01s + [1/1] sqlmesh_example__dev.view_model [recreate view, audits ✔3] 0.01s + Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 + + ✔ Model batches executed + + Updating virtual layer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 + + ✔ Virtual layer updated + ``` + + When the plan is applied to `prod`, it will only execute model batches for new intervals (new rows). This will NOT re-use `preview` models (backfilled data) in development. + + ```bash + > sqlmesh plan + Differences from the `prod` environment: + + Models: + ├── Directly Modified: + │ └── sqlmesh_example.incremental_model + └── Indirectly Modified: + ├── sqlmesh_example.view_model + └── sqlmesh_example.full_model + + --- + + +++ + + @@ -9,13 +9,14 @@ + + disable_restatement FALSE, + on_destructive_change 'ERROR' + ), + - grains ((id, event_date)) + + grains ((id, event_date)), + + allow_partials TRUE + ) + SELECT + id, + item_id, + event_date, + - 7 AS new_column + + 10 AS new_column + FROM sqlmesh_example.seed_model + WHERE + event_date BETWEEN @start_date AND @end_date + + Directly Modified: sqlmesh_example.incremental_model (Forward-only) + └── Indirectly Modified Children: + ├── sqlmesh_example.full_model (Forward-only) + └── sqlmesh_example.view_model (Forward-only) + Apply - Virtual Update [y/n]: y + + SKIP: No physical layer updates to perform + + SKIP: No model batches to execute + + Updating virtual layer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 + + ✔ Virtual layer updated + ``` + + +## Miscellaneous + +If you notice you have a lot of old development schemas/data, you can clean them up with the following command. This process runs automatically during the `sqlmesh run` command. This defaults to deleting data older than 7 days. + +=== "SQLMesh" + + ```bash + sqlmesh janitor + ``` + +=== "Tobiko Cloud" + + ```bash + tcloud sqlmesh janitor + ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index f6858b6646..c139ca2ab9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -68,6 +68,7 @@ nav: - Examples: - examples/overview.md - Walkthroughs: + - examples/sqlmesh_cli_crash_course.md - examples/incremental_time_full_walkthrough.md - Integrations: - "Overview": integrations/overview.md From 98519c28a67891b4a74b5dc057715151398429bb Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:15:10 -0700 Subject: [PATCH 0042/1056] fix: compat with dlt 1.10.0 release (#4223) --- sqlmesh/integrations/dlt.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sqlmesh/integrations/dlt.py b/sqlmesh/integrations/dlt.py index 7ae0881e47..023b43f173 100644 --- a/sqlmesh/integrations/dlt.py +++ b/sqlmesh/integrations/dlt.py @@ -49,7 +49,10 @@ def generate_dlt_models_and_settings( if db_type == "filesystem": connection_config = None else: - client = pipeline._sql_job_client(schema) + if dlt.__version__ >= "1.10.0": + client = pipeline.destination_client() + else: + client = pipeline._sql_job_client(schema) # type: ignore config = client.config credentials = config.credentials configs = { From f96488ce57476024a911951ca329ea58e1c461c3 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:29:06 -0700 Subject: [PATCH 0043/1056] feat: add batch_size support to scd type 2 kinds (#4220) --- docs/concepts/models/model_kinds.md | 77 ++++++++++++++++--- sqlmesh/core/engine_adapter/base.py | 12 ++- sqlmesh/core/engine_adapter/trino.py | 2 +- sqlmesh/core/model/kind.py | 10 ++- sqlmesh/core/snapshot/evaluator.py | 2 +- ...080_add_batch_size_to_scd_type_2_models.py | 5 ++ tests/core/test_model.py | 2 + tests/core/test_scheduler.py | 67 ++++++++++++++++ 8 files changed, 160 insertions(+), 17 deletions(-) create mode 100644 sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py diff --git a/docs/concepts/models/model_kinds.md b/docs/concepts/models/model_kinds.md index e777ad0eba..d01cc738a6 100644 --- a/docs/concepts/models/model_kinds.md +++ b/docs/concepts/models/model_kinds.md @@ -1241,12 +1241,13 @@ This is the most accurate representation of the menu based on the source data pr ### Shared Configuration Options -| Name | Description | Type | -|-------------------------|-----------------------------------------------------------------------------------------------------------------|---------------------------| -| unique_key | Unique key used for identifying rows between source and target | List of strings or string | -| valid_from_name | The name of the `valid_from` column to create in the target table. Default: `valid_from` | string | -| valid_to_name | The name of the `valid_to` column to create in the target table. Default: `valid_to` | string | -| invalidate_hard_deletes | If set to `true`, when a record is missing from the source table it will be marked as invalid. Default: `false` | bool | +| Name | Description | Type | +|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| +| unique_key | Unique key used for identifying rows between source and target | List of strings or string | +| valid_from_name | The name of the `valid_from` column to create in the target table. Default: `valid_from` | string | +| valid_to_name | The name of the `valid_to` column to create in the target table. Default: `valid_to` | string | +| invalidate_hard_deletes | If set to `true`, when a record is missing from the source table it will be marked as invalid. Default: `false` | bool | +| batch_size | The maximum number of intervals that can be evaluated in a single backfill task. If this is `None`, all intervals will be processed as part of a single task. See [Processing Source Table with Historical Data](#processing-source-table-with-historical-data) for more info on this use case. (Default: `None`) | int | !!! tip "Important" @@ -1273,10 +1274,66 @@ This is the most accurate representation of the menu based on the source data pr ### SCD Type 2 By Column Configuration Options -| Name | Description | Type | -|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| -| columns | The name of the columns to check for changes. `*` to represent that all columns should be checked. | List of strings or string | -| execution_time_as_valid_from | By default, when the model is first loaded `valid_from` is set to `1970-01-01 00:00:00` and future new rows will have `execution_time` of when the pipeline ran. This changes the behavior to always use `execution_time`. Default: `false` | bool | +| Name | Description | Type | +|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| +| columns | The name of the columns to check for changes. `*` to represent that all columns should be checked. | List of strings or string | +| execution_time_as_valid_from | By default, when the model is first loaded `valid_from` is set to `1970-01-01 00:00:00` and future new rows will have `execution_time` of when the pipeline ran. This changes the behavior to always use `execution_time`. Default: `false` | bool | +| updated_at_name | If sourcing from a table that includes as timestamp to use as valid_from, set this property to that column. See [Processing Source Table with Historical Data](#processing-source-table-with-historical-data) for more info on this use case. (Default: `None`) | int | + + +### Processing Source Table with Historical Data + +The most common case for SCD Type 2 is creating history for a table that it doesn't have it already. +In the example of the restaurant menu, the menu just tells you what is offered right now, but you want to know what was offered over time. +In this case, the default setting of `None` for `batch_size` is the best option. + +Another use case though is processing a source table that already has history in it. +A common example of this is a "daily snapshot" table that is created by a source system that takes a snapshot of the data at the end of each day. +If your source table has historical records, like a "daily snapshot" table, then set `batch_size` to `1` to process each interval (each day if a `@daily` cron) in sequential order. +That way the historical records will be properly captured in the SCD Type 2 table. + +#### Example - Source from Daily Snapshot Table + +```sql linenums="1" +MODEL ( + name db.table, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key id, + columns [some_value], + updated_at_name ds, + batch_size 1 + ), + start '2025-01-01', + cron '@daily' +); +SELECT + id, + some_value, + ds +FROM + source_table +WHERE + ds between @start_ds and @end_ds +``` + +This will process each day of the source table in sequential order (if more than one day to process), checking `some_value` column to see if it changed. If it did change, `valid_from` will be set to match the `ds` column (except for first value which would be `1970-01-01 00:00:00`). + +If the source data was the following: + +| id | some_value | ds | +|----|------------|:-----------:| +| 1 | 1 | 2025-01-01 | +| 1 | 2 | 2025-01-02 | +| 1 | 3 | 2025-01-03 | +| 1 | 3 | 2025-01-04 | + +Then the resulting SCD Type 2 table would be: + +| id | some_value | ds | valid_from | valid_to | +|----|------------|:-----------:|:-------------------:|:-------------------:| +| 1 | 1 | 2025-01-01 | 1970-01-01 00:00:00 | 2025-01-02 00:00:00 | +| 1 | 2 | 2025-01-02 | 2025-01-02 00:00:00 | 2025-01-03 00:00:00 | +| 1 | 3 | 2025-01-03 | 2025-01-03 00:00:00 | NULL | ### Querying SCD Type 2 Models diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index a61bd48f7e..7596973ffe 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -1411,7 +1411,7 @@ def scd_type_2_by_time( unique_key: t.Sequence[exp.Expression], valid_from_col: exp.Column, valid_to_col: exp.Column, - execution_time: TimeLike, + execution_time: t.Union[TimeLike, exp.Column], updated_at_col: exp.Column, invalidate_hard_deletes: bool = True, updated_at_as_valid_from: bool = False, @@ -1445,7 +1445,7 @@ def scd_type_2_by_column( unique_key: t.Sequence[exp.Expression], valid_from_col: exp.Column, valid_to_col: exp.Column, - execution_time: TimeLike, + execution_time: t.Union[TimeLike, exp.Column], check_columns: t.Union[exp.Star, t.Sequence[exp.Column]], invalidate_hard_deletes: bool = True, execution_time_as_valid_from: bool = False, @@ -1479,7 +1479,7 @@ def _scd_type_2( unique_key: t.Sequence[exp.Expression], valid_from_col: exp.Column, valid_to_col: exp.Column, - execution_time: TimeLike, + execution_time: t.Union[TimeLike, exp.Column], invalidate_hard_deletes: bool = True, updated_at_col: t.Optional[exp.Column] = None, check_columns: t.Optional[t.Union[exp.Star, t.Sequence[exp.Column]]] = None, @@ -1554,7 +1554,11 @@ def remove_managed_columns( # column names and then remove them from the unmanaged_columns if check_columns and check_columns == exp.Star(): check_columns = [exp.column(col) for col in unmanaged_columns_to_types] - execution_ts = to_time_column(execution_time, time_data_type, self.dialect, nullable=True) + execution_ts = ( + exp.cast(execution_time, time_data_type, dialect=self.dialect) + if isinstance(execution_time, exp.Column) + else to_time_column(execution_time, time_data_type, self.dialect, nullable=True) + ) if updated_at_as_valid_from: if not updated_at_col: raise SQLMeshError( diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index c629b41d7f..303c30258d 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -228,7 +228,7 @@ def _scd_type_2( unique_key: t.Sequence[exp.Expression], valid_from_col: exp.Column, valid_to_col: exp.Column, - execution_time: TimeLike, + execution_time: t.Union[TimeLike, exp.Column], invalidate_hard_deletes: bool = True, updated_at_col: t.Optional[exp.Column] = None, check_columns: t.Optional[t.Union[exp.Star, t.Sequence[exp.Column]]] = None, diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index 8cf5c2c6db..aef9ca5607 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -672,6 +672,7 @@ class _SCDType2Kind(_Incremental): valid_to_name: SQLGlotColumn = Field(exp.column("valid_to"), validate_default=True) invalidate_hard_deletes: SQLGlotBool = False time_data_type: exp.DataType = Field(exp.DataType.build("TIMESTAMP"), validate_default=True) + batch_size: t.Optional[SQLGlotPositiveInt] = None forward_only: SQLGlotBool = True disable_restatement: SQLGlotBool = True @@ -711,6 +712,7 @@ def data_hash_values(self) -> t.List[t.Optional[str]]: gen(self.valid_to_name), str(self.invalidate_hard_deletes), gen(self.time_data_type), + gen(self.batch_size) if self.batch_size is not None else None, ] @property @@ -781,6 +783,7 @@ class SCDType2ByColumnKind(_SCDType2Kind): name: t.Literal[ModelKindName.SCD_TYPE_2_BY_COLUMN] = ModelKindName.SCD_TYPE_2_BY_COLUMN columns: SQLGlotListOfColumnsOrStar execution_time_as_valid_from: SQLGlotBool = False + updated_at_name: t.Optional[SQLGlotColumn] = None @property def data_hash_values(self) -> t.List[t.Optional[str]]: @@ -789,7 +792,12 @@ def data_hash_values(self) -> t.List[t.Optional[str]]: if isinstance(self.columns, list) else [gen(self.columns)] ) - return [*super().data_hash_values, *columns_sql, str(self.execution_time_as_valid_from)] + return [ + *super().data_hash_values, + *columns_sql, + str(self.execution_time_as_valid_from), + gen(self.updated_at_name) if self.updated_at_name is not None else None, + ] def to_expression( self, expressions: t.Optional[t.List[exp.Expression]] = None, **kwargs: t.Any diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index bff25c3005..47adf8f1b7 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -1761,7 +1761,7 @@ def insert( unique_key=model.unique_key, valid_from_col=model.kind.valid_from_name, valid_to_col=model.kind.valid_to_name, - execution_time=kwargs["execution_time"], + execution_time=model.kind.updated_at_name or kwargs["execution_time"], check_columns=model.kind.columns, invalidate_hard_deletes=model.kind.invalidate_hard_deletes, execution_time_as_valid_from=model.kind.execution_time_as_valid_from, diff --git a/sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py b/sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py new file mode 100644 index 0000000000..8b40fc33c8 --- /dev/null +++ b/sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py @@ -0,0 +1,5 @@ +"""Add batch_size to SCD Type 2 models and add updated_at_name to by time which changes their data hash.""" + + +def migrate(state_sync, **kwargs): # type: ignore + pass diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 270414dba0..89d7e23ac7 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -4403,6 +4403,7 @@ def test_scd_type_2_by_column_overrides(): forward_only False, disable_restatement False, invalidate_hard_deletes False, + batch_size 1 ), ); SELECT @@ -4428,6 +4429,7 @@ def test_scd_type_2_by_column_overrides(): assert scd_type_2_model.kind.is_scd_type_2 assert scd_type_2_model.kind.is_materialized assert scd_type_2_model.kind.time_data_type == exp.DataType.build("TIMESTAMPTZ") + assert scd_type_2_model.kind.batch_size == 1 assert not scd_type_2_model.kind.invalidate_hard_deletes assert not scd_type_2_model.kind.forward_only assert not scd_type_2_model.kind.disable_restatement diff --git a/tests/core/test_scheduler.py b/tests/core/test_scheduler.py index 41e5e540de..aab767d85f 100644 --- a/tests/core/test_scheduler.py +++ b/tests/core/test_scheduler.py @@ -13,6 +13,7 @@ IncrementalByTimeRangeKind, IncrementalByUniqueKeyKind, TimeColumn, + SCDType2ByColumnKind, ) from sqlmesh.core.node import IntervalUnit from sqlmesh.core.scheduler import ( @@ -810,3 +811,69 @@ def signal_base(batch: DatetimeRanges): snapshot_b: [(to_timestamp("2023-01-01"), to_timestamp("2023-01-04"))], snapshot_c: [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))], } + + +@pytest.mark.parametrize( + "batch_size, expected_batches", + [ + ( + 1, + [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + ], + ), + ( + None, + [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-04")), + ], + ), + ], +) +def test_scd_type_2_batch_size( + mocker: MockerFixture, + make_snapshot, + get_batched_missing_intervals, + batch_size: t.Optional[int], + expected_batches: t.List[t.Tuple[int, int]], +): + """ + Test that SCD_TYPE_2_BY_COLUMN models are batched correctly based on batch_size. + With batch_size=1, we expect 3 separate batches for 3 days. + Without a specified batch_size, we expect a single batch for the entire period. + """ + start = to_datetime("2023-01-01") + end = to_datetime("2023-01-04") + + # Configure kind params + kind_params = {} + if batch_size is not None: + kind_params["batch_size"] = batch_size + + # Create the model and snapshot + model = SqlModel( + name="test_scd_model", + kind=SCDType2ByColumnKind(columns="valid_to", unique_key=["id"], **kind_params), + cron="@daily", + start=start, + query=parse_one("SELECT id, valid_from, valid_to FROM source"), + ) + snapshot = make_snapshot(model) + + # Setup scheduler + snapshot_evaluator = SnapshotEvaluator(adapters=mocker.MagicMock(), ddl_concurrent_tasks=1) + scheduler = Scheduler( + snapshots=[snapshot], + snapshot_evaluator=snapshot_evaluator, + state_sync=mocker.MagicMock(), + max_workers=2, + default_catalog=None, + ) + + # Get batches for the time period + batches = get_batched_missing_intervals(scheduler, start, end, end)[snapshot] + + # Verify batches match expectations + assert batches == expected_batches From 357cb43cc66dc93a809ceec13c4d0b7477d2d3fd Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Wed, 23 Apr 2025 12:24:27 +0300 Subject: [PATCH 0044/1056] Fix: Replicate `formatting` to pydantic classes (#4227) --- sqlmesh/core/audit/definition.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/audit/definition.py b/sqlmesh/core/audit/definition.py index 76d8dea997..691ca110a2 100644 --- a/sqlmesh/core/audit/definition.py +++ b/sqlmesh/core/audit/definition.py @@ -71,7 +71,7 @@ class AuditMixin(AuditCommonMetaMixin): defaults: t.Dict[str, exp.Expression] expressions_: t.Optional[t.List[exp.Expression]] jinja_macros: JinjaMacroRegistry - formatting: t.Optional[bool] = Field(default=None, exclude=True) + formatting: t.Optional[bool] @property def expressions(self) -> t.List[exp.Expression]: @@ -126,6 +126,7 @@ class ModelAudit(PydanticModel, AuditMixin, frozen=True): defaults: t.Dict[str, exp.Expression] = {} expressions_: t.Optional[t.List[exp.Expression]] = Field(default=None, alias="expressions") jinja_macros: JinjaMacroRegistry = JinjaMacroRegistry() + formatting: t.Optional[bool] = Field(default=None, exclude=True) _path: t.Optional[Path] = None @@ -159,6 +160,7 @@ class StandaloneAudit(_Node, AuditMixin): default_catalog: t.Optional[str] = None depends_on_: t.Optional[t.Set[str]] = Field(default=None, alias="depends_on") python_env: t.Dict[str, Executable] = {} + formatting: t.Optional[bool] = Field(default=None, exclude=True) source_type: t.Literal["audit"] = "audit" From f3715be2828581ce277bb298bea8e9c7cbd1795f Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:12:01 +0100 Subject: [PATCH 0045/1056] chore(vscode): move to central npm modules (#4219) --- .circleci/config.yml | 2 +- .circleci/continue_config.yml | 8 +- package-lock.json | 18717 +++++++++++++++++++++++++++ package.json | 6 + vscode/extension/package-lock.json | 7399 ----------- web/Dockerfile.app | 14 +- web/client/package-lock.json | 10251 --------------- web/docker-compose.build.yml | 2 +- web/docker-compose.yml | 5 +- 9 files changed, 18743 insertions(+), 17661 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 vscode/extension/package-lock.json delete mode 100644 web/client/package-lock.json diff --git a/.circleci/config.yml b/.circleci/config.yml index 58b6d11cc5..44f909bf9a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -77,7 +77,7 @@ jobs: - checkout - run: name: Install packages - command: npm --prefix web/client ci + command: npm ci - run: name: Build UI command: npm --prefix web/client run build diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 9e9077ab1f..71301a62f6 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -124,19 +124,19 @@ jobs: ui_test: docker: - - image: mcr.microsoft.com/playwright:v1.40.1-jammy + - image: mcr.microsoft.com/playwright:v1.52.0-jammy resource_class: medium steps: - halt_unless_client - checkout - restore_cache: keys: - - v1-nm-cache-{{ checksum "web/client/package-lock.json" }} + - v1-nm-cache-{{ checksum "package-lock.json" }} - run: name: Install packages - command: npm --prefix web/client ci + command: npm ci - save_cache: - key: v1-nm-cache-{{ checksum "web/client/package-lock.json" }} + key: v1-nm-cache-{{ checksum "package-lock.json" }} paths: - /root/.npm - run: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..b919d6fe49 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,18717 @@ +{ + "name": "sqlmesh", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "workspaces": [ + "vscode/extension", + "web/client" + ] + }, + "node_modules/@75lb/deep-merge": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.2.tgz", + "integrity": "sha512-08K9ou5VNbheZFxM5tDWoqjA3ImC50DiuuJ2tj1yEPRfkp8lLLg6XAaJ4On+a0yAXor/8ay5gHnAIshRM44Kpw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "typical": "^7.1.1" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/@75lb/deep-merge/node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", + "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", + "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz", + "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "11.7.2", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asyncapi/specs": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-6.8.1.tgz", + "integrity": "sha512-czHoAk3PeXTLR+X8IUaD+IpT+g+zUvkcgMDJVothBsan+oHN3jfcFcFUNdOPAAFoUCQN1hXF1dWuphWy05THlA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.11" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", + "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.3.tgz", + "integrity": "sha512-/wGw8fJ4mdpJ1Cum7s1S+VQyXt1ihwKLzfabS1O/RDADnmzVc01dHn44qD0BvGH6KlZNzOMW95tEpKqhkCChPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.1.tgz", + "integrity": "sha512-zHeoI3NCs53lLBbWNzQycjnYKsA1CVKlnzSNuSFcUDwBp8HHVObePxrM7HaX+Ha5Ks639H7chNC9HOaIhNS03w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", + "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", + "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.9.1.tgz", + "integrity": "sha512-986D7Cf1AOwYqSDtO/FnMAyk/Jc8qpftkGsxuehoh4F85MhQ4fICBGX/44+X1y78lN4Sqib3Bsoaoh/FvOGgmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz", + "integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.11.0.tgz", + "integrity": "sha512-0p5Ut3wORMP+975AKvaSPIO4UytgsfAvJ7RxaTx+nkP+Hpkmm93AuiMkBWKI2x9tApU/SLgIyPz/ZwLYUIWb5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.5.1.tgz", + "integrity": "sha512-oxK0khbc4Bg1bKQnqDr7ikULhVL2OHgSrIq0Vlh4b6+hm4r0lr6zPMQE8ZvmacJuh+ZZGKBM5iIObhF1q1QimQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.1.tgz", + "integrity": "sha512-dkgMYM5B6tI88r/oqf5bYd93WkenQpaWwiszJDk7avVjso8cmuKRTW97dA1RMi6RhihZFLtY1VtWxU9+sW2T5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.5.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", + "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.7.tgz", + "integrity": "sha512-mZnFTsL4lW5p9ch8uKNKeRU3xGGxr1QpESLilfON2E3fQzOa/OygEMkaDvERvXDJWJA9U9oN/D4w0ZuUzNO4+g==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz", + "integrity": "sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz", + "integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz", + "integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", + "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz", + "integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.36.5", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.5.tgz", + "integrity": "sha512-cd+FZEUlu3GQCYnguYm3EkhJ8KJVisqqUsCOKedBoAt/d9c76JUUap6U0UrpElln5k6VyrEOYliMuDAKIeDQLg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", + "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, + "node_modules/@headlessui/react": { + "version": "1.7.19", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", + "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", + "license": "MIT", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.60", + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@ibm-cloud/openapi-ruleset": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset/-/openapi-ruleset-1.30.1.tgz", + "integrity": "sha512-xVTkMwbjQ/bKfTXXb+/KEDsw8vRoZd6uxgFkoqfzMZ8/rJg2WP5hIpY7z6RegQ1pfF6LZ7RYS0u6h8xQo17ezw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ibm-cloud/openapi-ruleset-utilities": "1.8.1", + "@stoplight/spectral-formats": "^1.8.2", + "@stoplight/spectral-functions": "^1.9.3", + "@stoplight/spectral-rulesets": "^1.21.3", + "chalk": "^4.1.2", + "jsonschema": "^1.5.0", + "lodash": "^4.17.21", + "loglevel": "^1.9.2", + "loglevel-plugin-prefix": "0.8.4", + "minimatch": "^6.2.0", + "validator": "^13.11.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ibm-cloud/openapi-ruleset-utilities": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset-utilities/-/openapi-ruleset-utilities-1.8.1.tgz", + "integrity": "sha512-SayiOoNs7KaZiBFTV4/IQE3eigW3YsS7W1SXYovv4DBXxp0mSLWGDoZXHAjGs1vH8KubD7KyCK8+0Y3SoDZioA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ibm-cloud/openapi-ruleset/node_modules/minimatch": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/ternary": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", + "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lit/react": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.7.tgz", + "integrity": "sha512-cencnwwLXQKiKxjfFzSgZRngcWJzUDZi/04E0fSaF86wZgchMdvTyu+lE36DrUfvuus3bH8+xLPrhM1cTjwpzw==", + "license": "BSD-3-Clause", + "peerDependencies": { + "@types/react": "17 || 18 || 19" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@orval/angular": { + "version": "6.31.0", + "resolved": "https://registry.npmjs.org/@orval/angular/-/angular-6.31.0.tgz", + "integrity": "sha512-cVV/vh6biGUe5FMR0kaOL+pYkD5lM/oHpyHVU19d2eY/hxKCG58/CagUNVDxbowcSalzGpt7NbZOqpauc2cNOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "6.31.0" + } + }, + "node_modules/@orval/axios": { + "version": "6.31.0", + "resolved": "https://registry.npmjs.org/@orval/axios/-/axios-6.31.0.tgz", + "integrity": "sha512-OqWFJ6bDKftsSW3VI7Ouqcb3W4hDhkk8XzDkb/iisn3Dn1rkSE/wafdlHCm+62VQps4esYXaP1+7/HSk/2+Y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "6.31.0" + } + }, + "node_modules/@orval/core": { + "version": "6.31.0", + "resolved": "https://registry.npmjs.org/@orval/core/-/core-6.31.0.tgz", + "integrity": "sha512-ubOPpxzLgOCGbAQsq/dzfe/MIgB4LYWRyuwgnkV2GkL8Zq7cIWfmZU09GTJZQ6cO35OclFfbbyNve0cRMfSBeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.0", + "@ibm-cloud/openapi-ruleset": "^1.14.2", + "acorn": "^8.11.2", + "ajv": "^8.12.0", + "chalk": "^4.1.2", + "compare-versions": "^6.1.0", + "debug": "^4.3.4", + "esbuild": "^0.19.11", + "esutils": "2.0.3", + "fs-extra": "^11.2.0", + "globby": "11.1.0", + "lodash.get": "^4.4.2", + "lodash.isempty": "^4.4.0", + "lodash.omit": "^4.5.0", + "lodash.uniq": "^4.5.0", + "lodash.uniqby": "^4.7.0", + "lodash.uniqwith": "^4.5.0", + "micromatch": "^4.0.5", + "openapi3-ts": "4.2.2", + "swagger2openapi": "^7.0.8" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@orval/core/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@orval/core/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/@orval/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@orval/fetch": { + "version": "6.31.0", + "resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-6.31.0.tgz", + "integrity": "sha512-K4pD0TqRX3n1QgsfdzcCLxZPj4WFr4xd51VS5PhtK7wewy+EwaTp5AZeeMT+o8dL4HQcwLsKaXA1HH1YiAuOrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "6.31.0" + } + }, + "node_modules/@orval/hono": { + "version": "6.31.0", + "resolved": "https://registry.npmjs.org/@orval/hono/-/hono-6.31.0.tgz", + "integrity": "sha512-mM5WISLugu1quNkNUqYwp+StV/Z5/STm33VdPTWkoZyPJtV4NmEUZKPsowk0EN7sBF2kW+aYcp8lsNMXxXfHaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "6.31.0", + "@orval/zod": "6.31.0", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@orval/mock": { + "version": "6.31.0", + "resolved": "https://registry.npmjs.org/@orval/mock/-/mock-6.31.0.tgz", + "integrity": "sha512-UBag0IyL0eDVdXWgIMS/YxDF57Q3XC4VRDqcuZ1lB77rfBZ4UiVqTJleczQoIqMGkdtJJlBABgWzRRts1K4img==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "6.31.0", + "lodash.get": "^4.4.2", + "lodash.omit": "^4.5.0", + "openapi3-ts": "^4.2.2" + } + }, + "node_modules/@orval/query": { + "version": "6.31.0", + "resolved": "https://registry.npmjs.org/@orval/query/-/query-6.31.0.tgz", + "integrity": "sha512-aVyvSU5IbpRQnVbhChNlLX2XDnmoT1cDJ59NEFS3byhiJf1EG5XlzVve98je/BHAsVROrUC8+o6XoIjCtYbW5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "6.31.0", + "lodash.omitby": "^4.6.0" + } + }, + "node_modules/@orval/swr": { + "version": "6.31.0", + "resolved": "https://registry.npmjs.org/@orval/swr/-/swr-6.31.0.tgz", + "integrity": "sha512-J9W/kym9jc94GizbTozpuY76yaZRN98rf3ahj+2+eW8+NRW1dVFui32Gew1qj9rcCSA54BwRMONgEn3Xqx6W6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "6.31.0" + } + }, + "node_modules/@orval/zod": { + "version": "6.31.0", + "resolved": "https://registry.npmjs.org/@orval/zod/-/zod-6.31.0.tgz", + "integrity": "sha512-v6wqGZf4s3tpWrnmMHlEBfhTLeebu5W3HmhP8vQ5BPkm8AB2asiZqzK3Ne9Y19Rvyx6X4FGnhnalKYkz+XxJ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "6.31.0", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz", + "integrity": "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", + "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.11.tgz", + "integrity": "sha512-+gQXta3KxghZ/UDjeAQuCmeeRtYqGc4rT4EHCEnxEzT7RWasye2x9d8tSpIZxhzh123vCqEEktgIbrtZScirBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.11", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.7.tgz", + "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.4.tgz", + "integrity": "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.11.tgz", + "integrity": "sha512-sbFI4Qaw02J0ogmR9tOMsSqsdrGNpUanlPYAqTE2JJafow8ecHtykg4fSTjNHBdDl4deiKMK+RhTEwyVhP7UDA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-roving-focus": "1.1.7", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz", + "integrity": "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.6.tgz", + "integrity": "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz", + "integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.7.tgz", + "integrity": "sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz", + "integrity": "sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.4", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.3", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.2", + "@radix-ui/react-portal": "1.0.3", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", + "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", + "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", + "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", + "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", + "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-rect": "1.0.1", + "@radix-ui/react-use-size": "1.0.1", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", + "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", + "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", + "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", + "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-select/node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", + "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==", + "license": "MIT" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==", + "license": "MIT" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", + "license": "MIT" + }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/json": { + "version": "3.21.7", + "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", + "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.3", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "jsonc-parser": "~2.2.1", + "lodash": "^4.17.21", + "safe-stable-stringify": "^1.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-readers/-/json-ref-readers-1.2.2.tgz", + "integrity": "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-fetch": "^2.6.0", + "tslib": "^1.14.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stoplight/json-ref-resolver": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz", + "integrity": "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.21.0", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^12.3.0 || ^13.0.0", + "@types/urijs": "^1.19.19", + "dependency-graph": "~0.11.0", + "fast-memoize": "^2.5.2", + "immer": "^9.0.6", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "urijs": "^1.19.11" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json/node_modules/jsonc-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", + "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/ordered-object-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", + "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/path": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz", + "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/spectral-core": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.20.0.tgz", + "integrity": "sha512-5hBP81nCC1zn1hJXL/uxPNRKNcB+/pEIHgCjPRpl/w/qy9yC9ver04tw1W0l/PMiv0UeB5dYgozXVQ4j5a6QQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "~3.21.0", + "@stoplight/path": "1.3.2", + "@stoplight/spectral-parsers": "^1.0.0", + "@stoplight/spectral-ref-resolver": "^1.0.4", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "~13.6.0", + "@types/es-aggregate-error": "^1.0.2", + "@types/json-schema": "^7.0.11", + "ajv": "^8.17.1", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "es-aggregate-error": "^1.0.7", + "jsonpath-plus": "^10.3.0", + "lodash": "~4.17.21", + "lodash.topath": "^4.5.2", + "minimatch": "3.1.2", + "nimma": "0.2.3", + "pony-cause": "^1.1.1", + "simple-eval": "1.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", + "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/ajv-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/spectral-core/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@stoplight/spectral-formats": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-formats/-/spectral-formats-1.8.2.tgz", + "integrity": "sha512-c06HB+rOKfe7tuxg0IdKDEA5XnjL2vrn/m/OVIIxtINtBzphZrOgtRn7epQ5bQF5SWp84Ue7UJWaGgDwVngMFw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.17.0", + "@stoplight/spectral-core": "^1.19.2", + "@types/json-schema": "^7.0.7", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-functions": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.0.tgz", + "integrity": "sha512-MxOFqDyZHbyN4rbHyKBnUXN6R9DrjiSXFHlTNMHgM2bHlCoecXk0uWPz2wa9sqFJjprCg9W1XT70shK566SZHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "^3.17.1", + "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-formats": "^1.8.1", + "@stoplight/spectral-runtime": "^1.1.2", + "ajv": "^8.17.1", + "ajv-draft-04": "~1.0.0", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "lodash": "~4.17.21", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/spectral-parsers": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.5.tgz", + "integrity": "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "~3.21.0", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml": "~4.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/spectral-ref-resolver": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.5.tgz", + "integrity": "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json-ref-readers": "1.2.2", + "@stoplight/json-ref-resolver": "~3.1.6", + "@stoplight/spectral-runtime": "^1.1.2", + "dependency-graph": "0.11.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-rulesets": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.22.0.tgz", + "integrity": "sha512-l2EY2jiKKLsvnPfGy+pXC0LeGsbJzcQP5G/AojHgf+cwN//VYxW1Wvv4WKFx/CLmLxc42mJYF2juwWofjWYNIQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@asyncapi/specs": "^6.8.0", + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "^3.17.0", + "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-formats": "^1.8.1", + "@stoplight/spectral-functions": "^1.9.1", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "^13.6.0", + "@types/json-schema": "^7.0.7", + "ajv": "^8.17.1", + "ajv-formats": "~2.1.1", + "json-schema-traverse": "^1.0.0", + "leven": "3.1.0", + "lodash": "~4.17.21", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-rulesets/node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/spectral-rulesets/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-rulesets/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/spectral-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-runtime/-/spectral-runtime-1.1.4.tgz", + "integrity": "sha512-YHbhX3dqW0do6DhiPSgSGQzr6yQLlWybhKwWx0cqxjMwxej3TqLv3BXMfIUYFKKUqIwH4Q2mV8rrMM8qD2N0rQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.20.1", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "abort-controller": "^3.0.0", + "lodash": "^4.17.21", + "node-fetch": "^2.7.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/types": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", + "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz", + "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.5", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml-ast-parser": "0.0.50", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=10.8" + } + }, + "node_modules/@stoplight/yaml-ast-parser": { + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz", + "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@stoplight/yaml/node_modules/@stoplight/types": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@swc/core": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.21.tgz", + "integrity": "sha512-/Y3BJLcwd40pExmdar8MH2UGGvCBrqNN7hauOMckrEX2Ivcbv3IMhrbGX4od1dnF880Ed8y/E9aStZCIQi0EGw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.21" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.11.21", + "@swc/core-darwin-x64": "1.11.21", + "@swc/core-linux-arm-gnueabihf": "1.11.21", + "@swc/core-linux-arm64-gnu": "1.11.21", + "@swc/core-linux-arm64-musl": "1.11.21", + "@swc/core-linux-x64-gnu": "1.11.21", + "@swc/core-linux-x64-musl": "1.11.21", + "@swc/core-win32-arm64-msvc": "1.11.21", + "@swc/core-win32-ia32-msvc": "1.11.21", + "@swc/core-win32-x64-msvc": "1.11.21" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.21.tgz", + "integrity": "sha512-v6gjw9YFWvKulCw3ZA1dY+LGMafYzJksm1mD4UZFZ9b36CyHFowYVYug1ajYRIRqEvvfIhHUNV660zTLoVFR8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.21.tgz", + "integrity": "sha512-CUiTiqKlzskwswrx9Ve5NhNoab30L1/ScOfQwr1duvNlFvarC8fvQSgdtpw2Zh3MfnfNPpyLZnYg7ah4kbT9JQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.21.tgz", + "integrity": "sha512-YyBTAFM/QPqt1PscD8hDmCLnqPGKmUZpqeE25HXY8OLjl2MUs8+O4KjwPZZ+OGxpdTbwuWFyMoxjcLy80JODvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.21.tgz", + "integrity": "sha512-DQD+ooJmwpNsh4acrftdkuwl5LNxxg8U4+C/RJNDd7m5FP9Wo4c0URi5U0a9Vk/6sQNh9aSGcYChDpqCDWEcBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.21.tgz", + "integrity": "sha512-y1L49+snt1a1gLTYPY641slqy55QotPdtRK9Y6jMi4JBQyZwxC8swWYlQWb+MyILwxA614fi62SCNZNznB3XSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.21.tgz", + "integrity": "sha512-NesdBXv4CvVEaFUlqKj+GA4jJMNUzK2NtKOrUNEtTbXaVyNiXjFCSaDajMTedEB0jTAd9ybB0aBvwhgkJUWkWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.21.tgz", + "integrity": "sha512-qFV60pwpKVOdmX67wqQzgtSrUGWX9Cibnp1CXyqZ9Mmt8UyYGvmGu7p6PMbTyX7vdpVUvWVRf8DzrW2//wmVHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.21.tgz", + "integrity": "sha512-DJJe9k6gXR/15ZZVLv1SKhXkFst8lYCeZRNHH99SlBodvu4slhh/MKQ6YCixINRhCwliHrpXPym8/5fOq8b7Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.21.tgz", + "integrity": "sha512-TqEXuy6wedId7bMwLIr9byds+mKsaXVHctTN88R1UIBPwJA92Pdk0uxDgip0pEFzHB/ugU27g6d8cwUH3h2eIw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.21.tgz", + "integrity": "sha512-BT9BNNbMxdpUM1PPAkYtviaV0A8QcXttjs2MDtOeSqqvSJaPtyM+Fof2/+xSwQDmDEFzbGCcn75M5+xy3lGqpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/container-queries": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", + "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.2.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "4.36.1", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz", + "integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.6", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz", + "integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", + "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@testing-library/react/node_modules/@testing-library/dom": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/react/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai-subset": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz", + "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/chai": "<5.2.0" + } + }, + "node_modules/@types/command-line-args": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.0.tgz", + "integrity": "sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA==", + "license": "MIT" + }, + "node_modules/@types/command-line-usage": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.2.tgz", + "integrity": "sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg==", + "license": "MIT" + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/diff": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz", + "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/es-aggregate-error": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", + "integrity": "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", + "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/pad-left": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/pad-left/-/pad-left-2.1.1.tgz", + "integrity": "sha512-Xd22WCRBydkGSApl5Bw0PhAOHKSVjNL3E3AwzKaps96IMraPqy5BvZIsBVK6JLwdybUzjHnuWVwpDd0JjTfHXA==", + "license": "MIT" + }, + "node_modules/@types/pluralize": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.30.tgz", + "integrity": "sha512-kVww6xZrW/db5BR9OqiT71J9huRdQ+z/r+LbDuT7/EK50mCmj5FoaIARnVv0rvjUS/YpDox0cDU9lpQT011VBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", + "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.6.tgz", + "integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/urijs": { + "version": "1.19.25", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.25.tgz", + "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.99.1", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.99.1.tgz", + "integrity": "sha512-cQlqxHZ040ta6ovZXnXRxs3fJiTmlurkIWOfZVcLSZPcm9J4ikFpXuB7gihofGn5ng+kDVma5EmJIclfk0trPQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.0.tgz", + "integrity": "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/type-utils": "8.31.0", + "@typescript-eslint/utils": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.0.tgz", + "integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.0.tgz", + "integrity": "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.0.tgz", + "integrity": "sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/utils": "8.31.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", + "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz", + "integrity": "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.0.tgz", + "integrity": "sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz", + "integrity": "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.31.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@uidotdev/usehooks": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz", + "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.23.10", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.10.tgz", + "integrity": "sha512-zpbmSeNs3OU/f/Eyd6brFnjsBUYwv2mFjWxlAsIRSwTlW+skIT60rQHFBSfsj/5UVSxSLWVeUYczN7AyXvgTGQ==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.23.10", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.10.tgz", + "integrity": "sha512-AbN4eVHOL4ckRuIXpZxkzEqL/1ChVA+BSdEnAKjIB68pLQvKsVoYbiFP8zkXkYc4+Fcgq5KbAjvYqdo4ewemKw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.23.10", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", + "integrity": "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/core": "^1.11.21" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6" + } + }, + "node_modules/@vitest/expect": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "0.34.6", + "p-limit": "^4.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.4.3", + "loupe": "^2.3.6", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/python-extension": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@vscode/python-extension/-/python-extension-1.0.5.tgz", + "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==", + "license": "MIT", + "engines": { + "node": ">=16.17.1", + "vscode": "^1.78.0" + } + }, + "node_modules/@vscode/test-cli": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz", + "integrity": "sha512-B0mMH4ia+MOOtwNiLi79XhA+MLmUItIC8FckEuKrVAVriIuSWjt7vv4+bF8qVFiNFe4QRfzPaIZk39FZGWEwHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mocha": "^10.0.2", + "c8": "^9.1.0", + "chokidar": "^3.5.3", + "enhanced-resolve": "^5.15.0", + "glob": "^10.3.10", + "minimatch": "^9.0.3", + "mocha": "^10.2.0", + "supports-color": "^9.4.0", + "yargs": "^17.7.2" + }, + "bin": { + "vscode-test": "out/bin.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vscode/test-electron": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", + "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.3.2.tgz", + "integrity": "sha512-XQ4IhctYalSTMwLnMS8+nUaGbU7v99Qm2sOoGfIEf2QC7jpiLXZZMh7NwArEFsKX4gHTJLx0/GqAUlCdC3gKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^11.0.0", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.5.tgz", + "integrity": "sha512-GfYWrsT/vypTMDMgWDm75iDmAOMe7F71sZECJ+Ws6/xyIfmB3ELVnVN+LwMFAvmXY+e6eWhR2EzNGF/zAhWY3Q==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.2", + "@vscode/vsce-sign-alpine-x64": "2.0.2", + "@vscode/vsce-sign-darwin-arm64": "2.0.2", + "@vscode/vsce-sign-darwin-x64": "2.0.2", + "@vscode/vsce-sign-linux-arm": "2.0.2", + "@vscode/vsce-sign-linux-arm64": "2.0.2", + "@vscode/vsce-sign-linux-x64": "2.0.2", + "@vscode/vsce-sign-win32-arm64": "2.0.2", + "@vscode/vsce-sign-win32-x64": "2.0.2" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", + "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", + "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", + "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", + "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", + "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", + "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", + "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@vscode/vsce/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/vsce/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@vscode/vsce/node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@vscode/vsce/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@vscode/vsce/node_modules/minimatch/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@vscode/vsce/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apache-arrow": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-13.0.0.tgz", + "integrity": "sha512-3gvCX0GDawWz6KFNC28p65U+zGh/LZ6ZNKWNu74N6CQlKzxeoWHpi4CgEQsgRSEMuyrIIXi1Ea2syja7dwcHvw==", + "license": "Apache-2.0", + "dependencies": { + "@types/command-line-args": "5.2.0", + "@types/command-line-usage": "5.0.2", + "@types/node": "20.3.0", + "@types/pad-left": "2.1.1", + "command-line-args": "5.2.1", + "command-line-usage": "7.0.1", + "flatbuffers": "23.5.26", + "json-bignum": "^0.0.3", + "pad-left": "^2.1.0", + "tslib": "^2.5.3" + }, + "bin": { + "arrow2csv": "bin/arrow2csv.js" + } + }, + "node_modules/apache-arrow/node_modules/@types/node": { + "version": "20.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.0.tgz", + "integrity": "sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "dev": true, + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c8": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", + "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=14.14.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "license": "MIT", + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.1.tgz", + "integrity": "sha512-NCyznE//MuTjwi3y84QVUGEOT+P5oto1e1Pk/jFPVdPPfsG03qpTIl3yw6etR+v73d0lXsoojRpvbru2sqePxQ==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^3.0.0", + "typical": "^7.1.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", + "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-equal/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "license": "MIT", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.140", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.140.tgz", + "integrity": "sha512-o82Rj+ONp4Ip7Cl1r7lrqx/pXhbp/lh9DpKcMNscFJdh8ebyRofnc7Sh01B4jx403RI0oqTBvlZ7OBIZLMr2+Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", + "license": "EPL-2.0" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-aggregate-error": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.13.tgz", + "integrity": "sha512-KkzhUUuD2CUMqEc8JEqsXEMDHzDPE8RCjZeUBitsnB1eNcAJWQPiciKsMXe3Yytj4Flw1XLl46Qcf9OxvZha7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", + "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.25.1", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "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.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-n": { + "version": "16.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz", + "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "builtins": "^5.0.1", + "eslint-plugin-es-x": "^7.5.0", + "get-tsconfig": "^4.7.0", + "globals": "^13.24.0", + "ignore": "^5.2.4", + "is-builtin-module": "^3.2.1", + "is-core-module": "^2.12.1", + "minimatch": "^3.1.2", + "resolve": "^1.22.2", + "semver": "^7.5.3" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", + "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-memoize": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", + "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "license": "MIT", + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatbuffers": { + "version": "23.5.26", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-23.5.26.tgz", + "integrity": "sha512-vE+SI9vrJDwi1oETtTIFldC/o9GsVKRM+s6EL0nQgxXlYV1Vc4Tk30hj4xGICftInKQKj1F3up2n8UbIVobISQ==", + "license": "SEE LICENSE IN LICENSE" + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-encoding-sniffer/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-client": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "devOptional": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/json-bignum": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz", + "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpath-plus": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", + "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonschema": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", + "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==", + "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.omitby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.omitby/-/lodash.omitby-4.6.0.tgz", + "integrity": "sha512-5OrRcIVR75M288p4nbI2WLAf3ndw2GD9fyNv3Bc15+WCxJDdZ4lYndSxGd7hnG6PVjiJTeJE2dHEGhIuKGicIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniqwith": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz", + "integrity": "sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/nimma": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", + "integrity": "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsep-plugin/regex": "^1.0.1", + "@jsep-plugin/ternary": "^1.0.2", + "astring": "^1.8.1", + "jsep": "^1.2.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + }, + "optionalDependencies": { + "jsonpath-plus": "^6.0.1 || ^10.1.0", + "lodash.topath": "^4.5.2" + } + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "http2-client": "^1.2.5" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es6-promise": "^3.2.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, + "license": "MIT" + }, + "node_modules/oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "fast-safe-stringify": "^2.0.7" + } + }, + "node_modules/oas-linter": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", + "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@exodus/schemasafe": "^1.0.0-rc.2", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-linter/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oas-resolver": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", + "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "resolve": "resolve.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-resolver/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oas-schema-walker": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", + "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", + "dev": true, + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", + "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.2.2", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "reftools": "^1.1.9", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz", + "integrity": "sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/openapi3-ts": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.2.2.tgz", + "integrity": "sha512-+9g4actZKeb3czfi9gVQ4Br2Ju3KwhCAQJBNaKgye5KggqcBLIhFHH+nIkcm0BUX00TrAJl6dH4JWgM4G4JWrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.3.4" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/orval": { + "version": "6.31.0", + "resolved": "https://registry.npmjs.org/orval/-/orval-6.31.0.tgz", + "integrity": "sha512-515KTDQ4VRJCT+4DsMrK/QROWRq4PXrjgxAoEx3jmP7j+aQBGbx8WhidIF6aX1UgbTxw47Lq7QVp9mbnD0lnWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.0", + "@orval/angular": "6.31.0", + "@orval/axios": "6.31.0", + "@orval/core": "6.31.0", + "@orval/fetch": "6.31.0", + "@orval/hono": "6.31.0", + "@orval/mock": "6.31.0", + "@orval/query": "6.31.0", + "@orval/swr": "6.31.0", + "@orval/zod": "6.31.0", + "ajv": "^8.12.0", + "cac": "^6.7.14", + "chalk": "^4.1.2", + "chokidar": "^3.6.0", + "enquirer": "^2.4.1", + "execa": "^5.1.1", + "find-up": "5.0.0", + "fs-extra": "^11.2.0", + "lodash.uniq": "^4.5.0", + "openapi3-ts": "4.2.2", + "string-argv": "^0.3.2", + "tsconfck": "^2.0.1" + }, + "bin": { + "orval": "dist/bin/orval.js" + } + }, + "node_modules/orval/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/orval/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/pad-left": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pad-left/-/pad-left-2.1.0.tgz", + "integrity": "sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA==", + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pony-cause": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", + "integrity": "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==", + "dev": true, + "license": "0BSD", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", + "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "license": "MIT", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "license": "MIT", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", + "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", + "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-split": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/react-split/-/react-split-2.0.14.tgz", + "integrity": "sha512-bKWydgMgaKTg/2JGQnaJPg51T6dmumTWZppFgEbbY0Fbme0F5TuatAScCLaqommbGQQf/ZT1zaejuPDriscISA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.5.7", + "split.js": "^1.6.0" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reftools": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", + "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", + "dev": true, + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", + "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.1.tgz", + "integrity": "sha512-jjlZ7UknkyQxGnHF1w8wDgWfdtnW0hBX7tmDp04zBwDBZ/6tPJI1+RWfBHGMA4+0nAjGptp+eDpIYP6mldJbqg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "node_modules/should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.4.0" + } + }, + "node_modules/should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "node_modules/should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "node_modules/should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-eval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", + "integrity": "sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsep": "^1.3.6" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/split.js": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz", + "integrity": "sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==", + "license": "MIT" + }, + "node_modules/sqlmesh": { + "resolved": "vscode/extension", + "link": true + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-read-all": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/stream-read-all/-/stream-read-all-3.0.1.tgz", + "integrity": "sha512-EWZT9XOceBPlVJRrYcykW8jyRSZYbkb/0ZK36uLEmoWVO5gxBOnntNTseNzfREsqxqdfEGQrD8SXQ3QWbBmq8A==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" + }, + "node_modules/style-to-js": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", + "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.8" + } + }, + "node_modules/style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger2openapi": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", + "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "node-fetch": "^2.6.1", + "node-fetch-h2": "^2.3.0", + "node-readfiles": "^0.2.0", + "oas-kit-common": "^1.0.8", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "oas-validator": "^5.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "boast": "boast.js", + "oas-validate": "oas-validate.js", + "swagger2openapi": "swagger2openapi.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/swagger2openapi/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/table-layout": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-3.0.2.tgz", + "integrity": "sha512-rpyNZYRw+/C+dYkcQ3Pr+rLxW4CfHpXjPDnG7lYhdRoUcZTUt+KEsX+94RGp/aVp/MQU35JCITv2T/beY4m+hw==", + "license": "MIT", + "dependencies": { + "@75lb/deep-merge": "^1.1.1", + "array-back": "^6.2.2", + "command-line-args": "^5.2.1", + "command-line-usage": "^7.0.0", + "stream-read-all": "^3.0.1", + "typical": "^7.1.1", + "wordwrapjs": "^5.1.0" + }, + "bin": { + "table-layout": "bin/cli.js" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/table-layout/node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/terser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thememirror": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/thememirror/-/thememirror-2.0.1.tgz", + "integrity": "sha512-d5i6FVvWWPkwrm4cHLI3t9AT1OrkAt7Ig8dtdYSofgF7C/eiyNuq6zQzSTusWTde3jpW9WLvA9J/fzNKMUsd0w==", + "license": "MIT", + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tobiko": { + "resolved": "web/client", + "link": true + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/tsconfck": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.2.tgz", + "integrity": "sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^14.13.1 || ^16 || >=18" + }, + "peerDependencies": { + "typescript": "^4.3.5 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.21.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", + "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.13.tgz", + "integrity": "sha512-Hgp8IF/yZDzKsN1hQWOuQZbrKiaFsbQud+07jJ8h9m9PaHWkpvZ5u55Xw5yYjWRXwRQ4jwFlJvY7T7FUJG9MCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.4.0", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-css-injected-by-js": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.2.tgz", + "integrity": "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": ">2.0.0-0" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/vitest": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "license": "MIT", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.99.6", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz", + "integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrapjs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz", + "integrity": "sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "vscode/extension": { + "name": "sqlmesh", + "version": "0.0.1", + "dependencies": { + "@types/fs-extra": "^11.0.4", + "@vscode/python-extension": "^1.0.5", + "fs-extra": "^11.3.0", + "vscode-languageclient": "^9.0.1", + "zod": "^3.24.3" + }, + "devDependencies": { + "@types/mocha": "^10.0.10", + "@types/node": "20.x", + "@types/vscode": "^1.98.0", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", + "@vscode/test-cli": "^0.0.10", + "@vscode/test-electron": "^2.4.1", + "@vscode/vsce": "^3.3.2", + "esbuild": "^0.25.2", + "eslint": "^9.23.0", + "ts-loader": "^9.5.2", + "typescript": "^5.8.2" + }, + "engines": { + "vscode": "^1.98.0" + } + }, + "web/client": { + "name": "tobiko", + "version": "0.0.0", + "dependencies": { + "@codemirror/autocomplete": "^6.16.2", + "@codemirror/commands": "^6.6.0", + "@codemirror/lang-python": "^6.1.6", + "@codemirror/lang-sql": "^6.6.4", + "@codemirror/language": "^6.10.2", + "@codemirror/legacy-modes": "^6.4.0", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.28.1", + "@headlessui/react": "^1.7.17", + "@heroicons/react": "^2.0.18", + "@lit/react": "^1.0.6", + "@radix-ui/react-context-menu": "^2.1.4", + "@radix-ui/react-select": "^1.2.2", + "@tailwindcss/container-queries": "^0.1.1", + "@tanstack/react-query": "^4.33.0", + "@tanstack/react-table": "^8.9.2", + "@tanstack/react-virtual": "^3.0.0-beta.56", + "@uidotdev/usehooks": "^2.2.0", + "@uiw/react-codemirror": "^4.21.12", + "apache-arrow": "^13.0.0", + "clsx": "^2.0.0", + "diff": "^5.2.0", + "elkjs": "^0.8.2", + "pluralize": "^8.0.0", + "react": "^18.2.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", + "react-router-dom": "^6.15.0", + "react-split": "^2.0.14", + "reactflow": "^11.8.3", + "thememirror": "^2.0.1", + "zustand": "^4.4.1" + }, + "devDependencies": { + "@playwright/test": "^1.37.1", + "@testing-library/jest-dom": "^6.1.2", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.4.3", + "@types/diff": "^5.2.1", + "@types/pluralize": "^0.0.30", + "@types/react": "^18.2.21", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.5.0", + "@vitejs/plugin-react-swc": "^3.3.2", + "autoprefixer": "^10.4.15", + "eslint": "^8.48.0", + "eslint-config-prettier": "^9.0.0", + "eslint-config-standard-with-typescript": "^39.0.0", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-n": "^16.0.2", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.33.2", + "jsdom": "^22.1.0", + "orval": "^6.22.1", + "postcss": "^8.4.29", + "prettier": "^3.0.3", + "tailwindcss": "^3.3.3", + "typescript": "^5.2.2", + "vite": "^4.4.9", + "vite-plugin-css-injected-by-js": "^3.5.2", + "vitest": "^0.34.3" + } + }, + "web/client/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "web/client/node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "web/client/node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "web/client/node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "web/client/node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "web/client/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "web/client/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "web/client/node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "web/client/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "web/client/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "web/client/node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "web/client/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "web/client/node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "web/client/node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "web/client/node_modules/eslint-config-standard": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", + "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0" + } + }, + "web/client/node_modules/eslint-config-standard-with-typescript": { + "version": "39.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-39.1.1.tgz", + "integrity": "sha512-t6B5Ep8E4I18uuoYeYxINyqcXb2UbC0SOOTxRtBSt2JUs+EzeXbfe2oaiPs71AIdnoWhXDO2fYOHz8df3kV84A==", + "deprecated": "Please use eslint-config-love, instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/parser": "^6.4.0", + "eslint-config-standard": "17.1.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.4.0", + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0", + "typescript": "*" + } + }, + "web/client/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "web/client/node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "web/client/node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "web/client/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "web/client/node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "web/client/node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "web/client/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "web/client/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "web/client/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "web/client/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "web/client/node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..204b5919f1 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "workspaces": [ + "vscode/extension", + "web/client" + ] +} diff --git a/vscode/extension/package-lock.json b/vscode/extension/package-lock.json deleted file mode 100644 index 4756a10b38..0000000000 --- a/vscode/extension/package-lock.json +++ /dev/null @@ -1,7399 +0,0 @@ -{ - "name": "sqlmesh", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "sqlmesh", - "version": "0.0.1", - "dependencies": { - "@types/fs-extra": "^11.0.4", - "@vscode/python-extension": "^1.0.5", - "fs-extra": "^11.3.0", - "vscode-languageclient": "^9.0.1", - "zod": "^3.24.3" - }, - "devDependencies": { - "@types/mocha": "^10.0.10", - "@types/node": "20.x", - "@types/vscode": "^1.98.0", - "@typescript-eslint/eslint-plugin": "^8.28.0", - "@typescript-eslint/parser": "^8.28.0", - "@vscode/test-cli": "^0.0.10", - "@vscode/test-electron": "^2.4.1", - "@vscode/vsce": "^3.3.2", - "esbuild": "^0.25.2", - "eslint": "^9.23.0", - "ts-loader": "^9.5.2", - "typescript": "^5.8.2" - }, - "engines": { - "vscode": "^1.98.0" - } - }, - "node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-auth": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", - "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-util": "^1.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-client": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.3.tgz", - "integrity": "sha512-/wGw8fJ4mdpJ1Cum7s1S+VQyXt1ihwKLzfabS1O/RDADnmzVc01dHn44qD0BvGH6KlZNzOMW95tEpKqhkCChPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.4.0", - "@azure/core-rest-pipeline": "^1.9.1", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.6.1", - "@azure/logger": "^1.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-rest-pipeline": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.1.tgz", - "integrity": "sha512-zHeoI3NCs53lLBbWNzQycjnYKsA1CVKlnzSNuSFcUDwBp8HHVObePxrM7HaX+Ha5Ks639H7chNC9HOaIhNS03w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.8.0", - "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.11.0", - "@azure/logger": "^1.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-tracing": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", - "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-util": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", - "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/identity": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.9.1.tgz", - "integrity": "sha512-986D7Cf1AOwYqSDtO/FnMAyk/Jc8qpftkGsxuehoh4F85MhQ4fICBGX/44+X1y78lN4Sqib3Bsoaoh/FvOGgmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.2", - "@azure/core-rest-pipeline": "^1.17.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.11.0", - "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^4.2.0", - "@azure/msal-node": "^3.5.0", - "open": "^10.1.0", - "tslib": "^2.2.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/logger": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz", - "integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/msal-browser": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.11.0.tgz", - "integrity": "sha512-0p5Ut3wORMP+975AKvaSPIO4UytgsfAvJ7RxaTx+nkP+Hpkmm93AuiMkBWKI2x9tApU/SLgIyPz/ZwLYUIWb5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/msal-common": "15.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-common": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.5.1.tgz", - "integrity": "sha512-oxK0khbc4Bg1bKQnqDr7ikULhVL2OHgSrIq0Vlh4b6+hm4r0lr6zPMQE8ZvmacJuh+ZZGKBM5iIObhF1q1QimQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-node": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.1.tgz", - "integrity": "sha512-dkgMYM5B6tI88r/oqf5bYd93WkenQpaWwiszJDk7avVjso8cmuKRTW97dA1RMi6RhihZFLtY1VtWxU9+sW2T5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/msal-common": "15.5.1", - "jsonwebtoken": "^9.0.0", - "uuid": "^8.3.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", - "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.13.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/fs-extra": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", - "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", - "license": "MIT", - "dependencies": { - "@types/jsonfile": "*", - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsonfile": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", - "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/mocha": { - "version": "10.0.10", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", - "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.17.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", - "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/@types/vscode": { - "version": "1.99.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.99.1.tgz", - "integrity": "sha512-cQlqxHZ040ta6ovZXnXRxs3fJiTmlurkIWOfZVcLSZPcm9J4ikFpXuB7gihofGn5ng+kDVma5EmJIclfk0trPQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.0.tgz", - "integrity": "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.31.0", - "@typescript-eslint/type-utils": "8.31.0", - "@typescript-eslint/utils": "8.31.0", - "@typescript-eslint/visitor-keys": "8.31.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.0.tgz", - "integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.31.0", - "@typescript-eslint/types": "8.31.0", - "@typescript-eslint/typescript-estree": "8.31.0", - "@typescript-eslint/visitor-keys": "8.31.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.0.tgz", - "integrity": "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.31.0", - "@typescript-eslint/visitor-keys": "8.31.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.0.tgz", - "integrity": "sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.31.0", - "@typescript-eslint/utils": "8.31.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", - "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz", - "integrity": "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.31.0", - "@typescript-eslint/visitor-keys": "8.31.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.0.tgz", - "integrity": "sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.31.0", - "@typescript-eslint/types": "8.31.0", - "@typescript-eslint/typescript-estree": "8.31.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz", - "integrity": "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.31.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vscode/python-extension": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@vscode/python-extension/-/python-extension-1.0.5.tgz", - "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==", - "license": "MIT", - "engines": { - "node": ">=16.17.1", - "vscode": "^1.78.0" - } - }, - "node_modules/@vscode/test-cli": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz", - "integrity": "sha512-B0mMH4ia+MOOtwNiLi79XhA+MLmUItIC8FckEuKrVAVriIuSWjt7vv4+bF8qVFiNFe4QRfzPaIZk39FZGWEwHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mocha": "^10.0.2", - "c8": "^9.1.0", - "chokidar": "^3.5.3", - "enhanced-resolve": "^5.15.0", - "glob": "^10.3.10", - "minimatch": "^9.0.3", - "mocha": "^10.2.0", - "supports-color": "^9.4.0", - "yargs": "^17.7.2" - }, - "bin": { - "vscode-test": "out/bin.mjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vscode/test-electron": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", - "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", - "jszip": "^3.10.1", - "ora": "^8.1.0", - "semver": "^7.6.2" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@vscode/vsce": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.3.2.tgz", - "integrity": "sha512-XQ4IhctYalSTMwLnMS8+nUaGbU7v99Qm2sOoGfIEf2QC7jpiLXZZMh7NwArEFsKX4gHTJLx0/GqAUlCdC3gKCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/identity": "^4.1.0", - "@vscode/vsce-sign": "^2.0.0", - "azure-devops-node-api": "^12.5.0", - "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.9", - "cockatiel": "^3.1.2", - "commander": "^12.1.0", - "form-data": "^4.0.0", - "glob": "^11.0.0", - "hosted-git-info": "^4.0.2", - "jsonc-parser": "^3.2.0", - "leven": "^3.1.0", - "markdown-it": "^14.1.0", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^7.5.2", - "tmp": "^0.2.3", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.5.0", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" - }, - "bin": { - "vsce": "vsce" - }, - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "keytar": "^7.7.0" - } - }, - "node_modules/@vscode/vsce-sign": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.5.tgz", - "integrity": "sha512-GfYWrsT/vypTMDMgWDm75iDmAOMe7F71sZECJ+Ws6/xyIfmB3ELVnVN+LwMFAvmXY+e6eWhR2EzNGF/zAhWY3Q==", - "dev": true, - "hasInstallScript": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optionalDependencies": { - "@vscode/vsce-sign-alpine-arm64": "2.0.2", - "@vscode/vsce-sign-alpine-x64": "2.0.2", - "@vscode/vsce-sign-darwin-arm64": "2.0.2", - "@vscode/vsce-sign-darwin-x64": "2.0.2", - "@vscode/vsce-sign-linux-arm": "2.0.2", - "@vscode/vsce-sign-linux-arm64": "2.0.2", - "@vscode/vsce-sign-linux-x64": "2.0.2", - "@vscode/vsce-sign-win32-arm64": "2.0.2", - "@vscode/vsce-sign-win32-x64": "2.0.2" - } - }, - "node_modules/@vscode/vsce-sign-alpine-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", - "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "alpine" - ] - }, - "node_modules/@vscode/vsce-sign-alpine-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", - "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "alpine" - ] - }, - "node_modules/@vscode/vsce-sign-darwin-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", - "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@vscode/vsce-sign-darwin-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", - "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@vscode/vsce-sign-linux-arm": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", - "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@vscode/vsce-sign-linux-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", - "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@vscode/vsce-sign-linux-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", - "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@vscode/vsce-sign-win32-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", - "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vscode/vsce-sign-win32-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", - "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vscode/vsce/node_modules/glob": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", - "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/vsce/node_modules/jackspeak": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", - "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/vsce/node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@vscode/vsce/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@vscode/vsce/node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@vscode/vsce/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/azure-devops-node-api": { - "version": "12.5.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", - "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", - "dev": true, - "license": "MIT", - "dependencies": { - "tunnel": "0.0.6", - "typed-rest-client": "^1.8.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/c8": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", - "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^3.1.1", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.1.6", - "test-exclude": "^6.0.0", - "v8-to-istanbul": "^9.0.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1" - }, - "bin": { - "c8": "bin/c8.js" - }, - "engines": { - "node": ">=14.14.0" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001715", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", - "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0", - "peer": true - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cheerio": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "encoding-sniffer": "^0.2.0", - "htmlparser2": "^9.1.0", - "parse5": "^7.1.2", - "parse5-htmlparser2-tree-adapter": "^7.0.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^6.19.5", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=18.17" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/cliui/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/cockatiel": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", - "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.140", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.140.tgz", - "integrity": "sha512-o82Rj+ONp4Ip7Cl1r7lrqx/pXhbp/lh9DpKcMNscFJdh8ebyRofnc7Sh01B4jx403RI0oqTBvlZ7OBIZLMr2+Q==", - "dev": true, - "license": "ISC", - "peer": true - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/encoding-sniffer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", - "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", - "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.25.1", - "@eslint/plugin-kit": "^0.2.8", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "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.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/keytar": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", - "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.1" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/mocha": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", - "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^8.1.0", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/mocha/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/mocha/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/mocha/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/mocha/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true, - "license": "ISC" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/node-abi": { - "version": "3.74.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", - "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz", - "integrity": "sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ora/node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, - "license": "(MIT AND Zlib)" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-semver": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^5.1.0" - } - }, - "node_modules/parse-semver/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^4.5.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC", - "peer": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "mute-stream": "~0.0.4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, - "license": "ISC" - }, - "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-loader": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", - "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ts-loader/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ts-loader/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ts-loader/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typed-rest-client": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", - "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "qs": "^6.9.1", - "tunnel": "0.0.6", - "underscore": "^1.12.1" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici": { - "version": "6.21.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", - "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "license": "MIT" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "dev": true, - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageclient": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", - "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", - "license": "MIT", - "dependencies": { - "minimatch": "^5.1.0", - "semver": "^7.3.7", - "vscode-languageserver-protocol": "3.17.5" - }, - "engines": { - "vscode": "^1.82.0" - } - }, - "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" - }, - "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack": { - "version": "5.99.6", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz", - "integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yazl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/web/Dockerfile.app b/web/Dockerfile.app index 73e1bb336e..e9ae89be6e 100644 --- a/web/Dockerfile.app +++ b/web/Dockerfile.app @@ -6,7 +6,17 @@ ENV PATH /app/node_modules/.bin:$PATH RUN apt-get update && apt-get -y install libnss3 libatk-bridge2.0-0 libdrm-dev libxkbcommon-dev libgbm-dev libasound-dev libatspi2.0-0 libxshmfence-dev -COPY web/client/package*.json . +# Copy package files for workspaces +COPY package.json /app/package.json +COPY package-lock.json /app/package-lock.json +COPY web/client/package.json /app/web/client/package.json +COPY vscode/extension/package.json /app/vscode/extension/package.json RUN npm install -g npm@latest && \ - npm install --no-audit --no-fund --no-package-lock + npm install --no-audit --no-fund + +# Copy the rest of the application +COPY web/client/ /app/web/client/ + +# Install dependencies +RUN npm install --no-audit --no-fund diff --git a/web/client/package-lock.json b/web/client/package-lock.json deleted file mode 100644 index f8ab80f1f3..0000000000 --- a/web/client/package-lock.json +++ /dev/null @@ -1,10251 +0,0 @@ -{ - "name": "tobiko", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "tobiko", - "version": "0.0.0", - "dependencies": { - "@codemirror/autocomplete": "^6.16.2", - "@codemirror/commands": "^6.6.0", - "@codemirror/lang-python": "^6.1.6", - "@codemirror/lang-sql": "^6.6.4", - "@codemirror/language": "^6.10.2", - "@codemirror/legacy-modes": "^6.4.0", - "@codemirror/state": "^6.4.1", - "@codemirror/view": "^6.28.1", - "@headlessui/react": "^1.7.17", - "@heroicons/react": "^2.0.18", - "@lit/react": "^1.0.6", - "@radix-ui/react-context-menu": "^2.1.4", - "@radix-ui/react-select": "^1.2.2", - "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^4.33.0", - "@tanstack/react-table": "^8.9.2", - "@tanstack/react-virtual": "^3.0.0-beta.56", - "@uidotdev/usehooks": "^2.2.0", - "@uiw/react-codemirror": "^4.21.12", - "apache-arrow": "^13.0.0", - "clsx": "^2.0.0", - "diff": "^5.2.0", - "elkjs": "^0.8.2", - "pluralize": "^8.0.0", - "react": "^18.2.0", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", - "react-dom": "^18.2.0", - "react-markdown": "^9.0.1", - "react-router-dom": "^6.15.0", - "react-split": "^2.0.14", - "reactflow": "^11.8.3", - "thememirror": "^2.0.1", - "zustand": "^4.4.1" - }, - "devDependencies": { - "@playwright/test": "^1.37.1", - "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "^14.0.0", - "@testing-library/user-event": "^14.4.3", - "@types/diff": "^5.2.1", - "@types/pluralize": "^0.0.30", - "@types/react": "^18.2.21", - "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.5.0", - "@vitejs/plugin-react-swc": "^3.3.2", - "autoprefixer": "^10.4.15", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-config-standard-with-typescript": "^39.0.0", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-n": "^16.0.2", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.33.2", - "jsdom": "^22.1.0", - "orval": "^6.22.1", - "postcss": "^8.4.29", - "prettier": "^3.0.3", - "tailwindcss": "^3.3.3", - "typescript": "^5.2.2", - "vite": "^4.4.9", - "vite-plugin-css-injected-by-js": "^3.5.2", - "vitest": "^0.34.3" - } - }, - "node_modules/@75lb/deep-merge": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.2.tgz", - "integrity": "sha512-08K9ou5VNbheZFxM5tDWoqjA3ImC50DiuuJ2tj1yEPRfkp8lLLg6XAaJ4On+a0yAXor/8ay5gHnAIshRM44Kpw==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21", - "typical": "^7.1.1" - }, - "engines": { - "node": ">=12.17" - } - }, - "node_modules/@75lb/deep-merge/node_modules/typical": { - "version": "7.1.1", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.3.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "9.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "js-yaml": "^3.13.1" - } - }, - "node_modules/@apidevtools/json-schema-ref-parser/node_modules/argparse": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { - "version": "3.14.1", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@apidevtools/openapi-schemas": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@apidevtools/swagger-methods": { - "version": "3.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@apidevtools/swagger-parser": { - "version": "10.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "9.0.6", - "@apidevtools/openapi-schemas": "^2.1.0", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "ajv": "^8.6.3", - "ajv-draft-04": "^1.0.0", - "call-me-maybe": "^1.0.1" - }, - "peerDependencies": { - "openapi-types": ">=7" - } - }, - "node_modules/@apidevtools/swagger-parser/node_modules/ajv": { - "version": "8.12.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@asyncapi/specs": { - "version": "4.3.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.11" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/runtime": { - "version": "7.23.5", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@codemirror/autocomplete": { - "version": "6.16.2", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz", - "integrity": "sha512-MjfDrHy0gHKlPWsvSsikhO1+BOh+eBHNgfH1OXs1+DAf30IonQldgMM3kxLDTG9ktE7kDLaA1j/l7KMPA4KNfw==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - }, - "peerDependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz", - "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/lang-python": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.6.tgz", - "integrity": "sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==", - "dependencies": { - "@codemirror/autocomplete": "^6.3.2", - "@codemirror/language": "^6.8.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.2.1", - "@lezer/python": "^1.1.4" - } - }, - "node_modules/@codemirror/lang-sql": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.6.4.tgz", - "integrity": "sha512-n+FVfKGut+frOvor9dU5pFUalcP614WBNQ9IT1kOUj1t6LFLjWHi2I9DdxXnJuxqFV9jTyYF79coDV3ilSJqCw==", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz", - "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.1.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/legacy-modes": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.4.0.tgz", - "integrity": "sha512-5m/K+1A6gYR0e+h/dEde7LoGimMjRtWXZFg4Lo70cc8HzjSdHe3fLwjWMR0VRl5KFT1SxalSap7uMgPKF28wBA==", - "dependencies": { - "@codemirror/language": "^6.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.4.2", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.5.5", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", - "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" - }, - "node_modules/@codemirror/theme-one-dark": { - "version": "6.1.2", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/highlight": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.28.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.1.tgz", - "integrity": "sha512-BUWr+zCJpMkA/u69HlJmR+YkV4yPpM81HeMkOMZuwFa8iM5uJdEPKAs1icIRZKkKmy0Ub1x9/G3PQLTXdpBxrQ==", - "dependencies": { - "@codemirror/state": "^6.4.0", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.8", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.55.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@exodus/schemasafe": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@floating-ui/core": { - "version": "1.5.2", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.1.3" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.5.3", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.4.2", - "@floating-ui/utils": "^0.1.3" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.0.4", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.5.1" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.1.6", - "license": "MIT" - }, - "node_modules/@headlessui/react": { - "version": "1.7.17", - "license": "MIT", - "dependencies": { - "client-only": "^0.0.1" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^16 || ^17 || ^18", - "react-dom": "^16 || ^17 || ^18" - } - }, - "node_modules/@heroicons/react": { - "version": "2.0.18", - "license": "MIT", - "peerDependencies": { - "react": ">= 16" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@ibm-cloud/openapi-ruleset": { - "version": "1.14.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@ibm-cloud/openapi-ruleset-utilities": "1.3.0", - "@stoplight/spectral-formats": "^1.5.0", - "@stoplight/spectral-functions": "^1.7.2", - "@stoplight/spectral-rulesets": "^1.16.0", - "chalk": "^4.1.1", - "lodash": "^4.17.21", - "loglevel": "^1.8.1", - "loglevel-plugin-prefix": "0.8.4", - "minimatch": "^6.1.6", - "validator": "^13.7.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ibm-cloud/openapi-ruleset-utilities": { - "version": "1.3.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ibm-cloud/openapi-ruleset/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@ibm-cloud/openapi-ruleset/node_modules/minimatch": { - "version": "6.2.0", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@jsep-plugin/assignment": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", - "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@jsep-plugin/regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", - "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@jsep-plugin/ternary": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", - "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@lezer/common": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", - "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.3.14", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/python": { - "version": "1.1.9", - "license": "MIT", - "dependencies": { - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lit/react": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.6.tgz", - "integrity": "sha512-QIss8MPh6qUoFJmuaF4dSHts3qCsA36S3HcOLiNPShxhgYPr4XJRnCBKPipk85sR9xr6TQrOcDMfexwbNdJHYA==", - "license": "BSD-3-Clause", - "peerDependencies": { - "@types/react": "17 || 18" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@orval/angular": { - "version": "6.22.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "6.22.1" - } - }, - "node_modules/@orval/axios": { - "version": "6.22.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "6.22.1" - } - }, - "node_modules/@orval/core": { - "version": "6.22.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "^10.1.0", - "@ibm-cloud/openapi-ruleset": "^1.14.2", - "acorn": "^8.11.2", - "ajv": "^8.12.0", - "chalk": "^4.1.2", - "compare-versions": "^6.1.0", - "debug": "^4.3.4", - "esbuild": "^0.19.5", - "esutils": "2.0.3", - "fs-extra": "^11.2.0", - "globby": "11.1.0", - "lodash.get": "^4.4.2", - "lodash.isempty": "^4.4.0", - "lodash.omit": "^4.5.0", - "lodash.uniq": "^4.5.0", - "lodash.uniqby": "^4.7.0", - "lodash.uniqwith": "^4.5.0", - "micromatch": "^4.0.5", - "openapi-types": "^12.1.3", - "openapi3-ts": "^3.2.0", - "swagger2openapi": "^7.0.8" - } - }, - "node_modules/@orval/core/node_modules/ajv": { - "version": "8.12.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@orval/core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@orval/mock": { - "version": "6.22.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "6.22.1", - "lodash.get": "^4.4.2", - "lodash.omit": "^4.5.0", - "openapi3-ts": "^3.0.0" - } - }, - "node_modules/@orval/query": { - "version": "6.22.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "6.22.1", - "lodash.omitby": "^4.6.0", - "vitest": "^0.34.6" - } - }, - "node_modules/@orval/swr": { - "version": "6.22.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "6.22.1" - } - }, - "node_modules/@orval/zod": { - "version": "6.22.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "6.22.1", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/@playwright/test": { - "version": "1.40.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.40.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@radix-ui/number": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.1.5", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-menu": "2.0.6", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.0.6", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.1.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "1.2.2", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.3", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.2", - "@radix-ui/react-portal": "1.0.3", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@react-dnd/asap": { - "version": "5.0.2", - "license": "MIT" - }, - "node_modules/@react-dnd/invariant": { - "version": "4.0.2", - "license": "MIT" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "4.0.2", - "license": "MIT" - }, - "node_modules/@reactflow/background": { - "version": "11.3.6", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.10.1", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/controls": { - "version": "11.2.6", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.10.1", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/core": { - "version": "11.10.1", - "license": "MIT", - "dependencies": { - "@types/d3": "^7.4.0", - "@types/d3-drag": "^3.0.1", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-drag": "^3.0.0", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/minimap": { - "version": "11.7.6", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.10.1", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/node-resizer": { - "version": "2.2.6", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.10.1", - "classcat": "^5.0.4", - "d3-drag": "^3.0.0", - "d3-selection": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/node-toolbar": { - "version": "1.3.6", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.10.1", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@remix-run/router": { - "version": "1.13.1", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/json": { - "version": "3.21.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/ordered-object-literal": "^1.0.3", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^13.6.0", - "jsonc-parser": "~2.2.1", - "lodash": "^4.17.21", - "safe-stable-stringify": "^1.1" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/json-ref-readers": { - "version": "1.2.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-fetch": "^2.6.0", - "tslib": "^1.14.1" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "node_modules/@stoplight/json-ref-resolver": { - "version": "3.1.6", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "^3.21.0", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^12.3.0 || ^13.0.0", - "@types/urijs": "^1.19.19", - "dependency-graph": "~0.11.0", - "fast-memoize": "^2.5.2", - "immer": "^9.0.6", - "lodash": "^4.17.21", - "tslib": "^2.6.0", - "urijs": "^1.19.11" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/ordered-object-literal": { - "version": "1.0.5", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/path": { - "version": "1.3.2", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/spectral-core": { - "version": "1.19.4", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.19.4.tgz", - "integrity": "sha512-8hnZXfssTlV99SKo8J8BwMt5LsiBFHkCh0V3P7j8IPcCNl//bpG92U4TpYy7AwmUms/zCLX7sxNQC6AZ+bkfzg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "~3.21.0", - "@stoplight/path": "1.3.2", - "@stoplight/spectral-parsers": "^1.0.0", - "@stoplight/spectral-ref-resolver": "^1.0.4", - "@stoplight/spectral-runtime": "^1.1.2", - "@stoplight/types": "~13.6.0", - "@types/es-aggregate-error": "^1.0.2", - "@types/json-schema": "^7.0.11", - "ajv": "^8.17.1", - "ajv-errors": "~3.0.0", - "ajv-formats": "~2.1.0", - "es-aggregate-error": "^1.0.7", - "jsonpath-plus": "10.2.0", - "lodash": "~4.17.21", - "lodash.topath": "^4.5.2", - "minimatch": "3.1.2", - "nimma": "0.2.3", - "pony-cause": "^1.1.1", - "simple-eval": "1.0.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types": { - "version": "13.6.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/ajv-errors": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.0.1" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/spectral-formats": { - "version": "1.6.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "^3.17.0", - "@stoplight/spectral-core": "^1.8.0", - "@types/json-schema": "^7.0.7", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@stoplight/spectral-functions": { - "version": "1.7.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "^3.17.1", - "@stoplight/spectral-core": "^1.7.0", - "@stoplight/spectral-formats": "^1.0.0", - "@stoplight/spectral-runtime": "^1.1.0", - "ajv": "^8.6.3", - "ajv-draft-04": "~1.0.0", - "ajv-errors": "~3.0.0", - "ajv-formats": "~2.1.0", - "lodash": "~4.17.21", - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv": { - "version": "8.12.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv-draft-04": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv-errors": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.0.1" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/spectral-parsers": { - "version": "1.0.3", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "~3.21.0", - "@stoplight/types": "^13.6.0", - "@stoplight/yaml": "~4.2.3", - "tslib": "^2.3.1" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@stoplight/spectral-ref-resolver": { - "version": "1.0.4", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json-ref-readers": "1.2.2", - "@stoplight/json-ref-resolver": "~3.1.6", - "@stoplight/spectral-runtime": "^1.1.2", - "dependency-graph": "0.11.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@stoplight/spectral-rulesets": { - "version": "1.18.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@asyncapi/specs": "^4.1.0", - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "^3.17.0", - "@stoplight/spectral-core": "^1.8.1", - "@stoplight/spectral-formats": "^1.5.0", - "@stoplight/spectral-functions": "^1.5.1", - "@stoplight/spectral-runtime": "^1.1.1", - "@stoplight/types": "^13.6.0", - "@types/json-schema": "^7.0.7", - "ajv": "^8.8.2", - "ajv-formats": "~2.1.0", - "json-schema-traverse": "^1.0.0", - "lodash": "~4.17.21", - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/ajv": { - "version": "8.12.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/spectral-runtime": { - "version": "1.1.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "^3.17.0", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^12.3.0", - "abort-controller": "^3.0.0", - "lodash": "^4.17.21", - "node-fetch": "^2.6.7", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@stoplight/spectral-runtime/node_modules/@stoplight/types": { - "version": "12.5.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/types": { - "version": "13.20.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@stoplight/yaml": { - "version": "4.2.3", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/ordered-object-literal": "^1.0.1", - "@stoplight/types": "^13.0.0", - "@stoplight/yaml-ast-parser": "0.0.48", - "tslib": "^2.2.0" - }, - "engines": { - "node": ">=10.8" - } - }, - "node_modules/@stoplight/yaml-ast-parser": { - "version": "0.0.48", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/core": { - "version": "1.3.100", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.100", - "@swc/core-darwin-x64": "1.3.100", - "@swc/core-linux-arm64-gnu": "1.3.100", - "@swc/core-linux-arm64-musl": "1.3.100", - "@swc/core-linux-x64-gnu": "1.3.100", - "@swc/core-linux-x64-musl": "1.3.100", - "@swc/core-win32-arm64-msvc": "1.3.100", - "@swc/core-win32-ia32-msvc": "1.3.100", - "@swc/core-win32-x64-msvc": "1.3.100" - }, - "peerDependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.100", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.100", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.2", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.5", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@tailwindcss/container-queries": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", - "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", - "peerDependencies": { - "tailwindcss": ">=3.2.0" - } - }, - "node_modules/@tanstack/query-core": { - "version": "4.36.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "4.36.1", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "4.36.1", - "use-sync-external-store": "^1.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/@tanstack/react-table": { - "version": "8.10.7", - "license": "MIT", - "dependencies": { - "@tanstack/table-core": "8.10.7" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" - } - }, - "node_modules/@tanstack/react-virtual": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@tanstack/table-core": { - "version": "8.10.7", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.0.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@testing-library/dom": { - "version": "9.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.3.1", - "@babel/runtime": "^7.9.2", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - }, - "peerDependencies": { - "@jest/globals": ">= 28", - "@types/jest": ">= 28", - "jest": ">= 28", - "vitest": ">= 0.32" - }, - "peerDependenciesMeta": { - "@jest/globals": { - "optional": true - }, - "@types/jest": { - "optional": true - }, - "jest": { - "optional": true - }, - "vitest": { - "optional": true - } - } - }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/react": { - "version": "14.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^9.0.0", - "@types/react-dom": "^18.0.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "4.3.11", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai-subset": { - "version": "1.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "*" - } - }, - "node_modules/@types/command-line-args": { - "version": "5.2.0", - "license": "MIT" - }, - "node_modules/@types/command-line-usage": { - "version": "5.0.2", - "license": "MIT" - }, - "node_modules/@types/d3": { - "version": "7.4.3", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "license": "MIT" - }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "license": "MIT" - }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.6", - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "license": "MIT", - "dependencies": { - "@types/d3-dsv": "*" - } - }, - "node_modules/@types/d3-force": { - "version": "3.0.9", - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.6", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.8", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.10", - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.6", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.8", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/diff": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.1.tgz", - "integrity": "sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/es-aggregate-error": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/geojson": { - "version": "7946.0.13", - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" - }, - "node_modules/@types/node": { - "version": "20.3.0", - "license": "MIT" - }, - "node_modules/@types/pad-left": { - "version": "2.1.1", - "license": "MIT" - }, - "node_modules/@types/pluralize": { - "version": "0.0.30", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.11", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.2.42", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.2.17", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "license": "MIT" - }, - "node_modules/@types/semver": { - "version": "7.5.6", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" - }, - "node_modules/@types/urijs": { - "version": "1.19.25", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.13.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.13.2", - "@typescript-eslint/type-utils": "6.13.2", - "@typescript-eslint/utils": "6.13.2", - "@typescript-eslint/visitor-keys": "6.13.2", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.13.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "6.13.2", - "@typescript-eslint/types": "6.13.2", - "@typescript-eslint/typescript-estree": "6.13.2", - "@typescript-eslint/visitor-keys": "6.13.2", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.13.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.13.2", - "@typescript-eslint/visitor-keys": "6.13.2" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.13.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.13.2", - "@typescript-eslint/utils": "6.13.2", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.13.2", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.13.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.13.2", - "@typescript-eslint/visitor-keys": "6.13.2", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.13.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.13.2", - "@typescript-eslint/types": "6.13.2", - "@typescript-eslint/typescript-estree": "6.13.2", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.13.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.13.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@uidotdev/usehooks": { - "version": "2.4.1", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.21.21", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@codemirror/autocomplete": ">=6.0.0", - "@codemirror/commands": ">=6.0.0", - "@codemirror/language": ">=6.0.0", - "@codemirror/lint": ">=6.0.0", - "@codemirror/search": ">=6.0.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/view": ">=6.0.0" - } - }, - "node_modules/@uiw/react-codemirror": { - "version": "4.21.21", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.6", - "@codemirror/commands": "^6.1.0", - "@codemirror/state": "^6.1.1", - "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.21.21", - "codemirror": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.11.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/theme-one-dark": ">=6.0.0", - "@codemirror/view": ">=6.0.0", - "codemirror": ">=6.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "license": "ISC" - }, - "node_modules/@vitejs/plugin-react-swc": { - "version": "3.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@swc/core": "^1.3.96" - }, - "peerDependencies": { - "vite": "^4 || ^5" - } - }, - "node_modules/@vitest/expect": { - "version": "0.34.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "0.34.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "0.34.6", - "p-limit": "^4.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/snapshot": { - "version": "0.34.6", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/snapshot/node_modules/react-is": { - "version": "18.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/spy": { - "version": "0.34.6", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^2.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "0.34.6", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils/node_modules/ansi-styles": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/node_modules/react-is": { - "version": "18.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/abab": { - "version": "2.0.6", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/acorn": { - "version": "8.11.2", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/apache-arrow": { - "version": "13.0.0", - "license": "Apache-2.0", - "dependencies": { - "@types/command-line-args": "5.2.0", - "@types/command-line-usage": "5.0.2", - "@types/node": "20.3.0", - "@types/pad-left": "2.1.1", - "command-line-args": "5.2.1", - "command-line-usage": "7.0.1", - "flatbuffers": "23.5.26", - "json-bignum": "^0.0.3", - "pad-left": "^2.1.0", - "tslib": "^2.5.3" - }, - "bin": { - "arrow2csv": "bin/arrow2csv.js" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.3", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aria-query": { - "version": "5.1.3", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/array-back": { - "version": "3.1.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/astring": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", - "dev": true, - "license": "MIT", - "bin": { - "astring": "bin/astring" - } - }, - "node_modules/asynciterator.prototype": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.16", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.22.2", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/builtins": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001689", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", - "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chai": { - "version": "4.3.10", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk-template": { - "version": "0.4.0", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/chalk-template?sponsor=1" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/check-error": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/classcat": { - "version": "5.0.4", - "license": "MIT" - }, - "node_modules/client-only": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clsx": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/codemirror": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/command-line-args": { - "version": "5.2.1", - "license": "MIT", - "dependencies": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", - "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/command-line-usage": { - "version": "7.0.1", - "license": "MIT", - "dependencies": { - "array-back": "^6.2.2", - "chalk-template": "^0.4.0", - "table-layout": "^3.0.0", - "typical": "^7.1.1" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/command-line-usage/node_modules/array-back": { - "version": "6.2.2", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/command-line-usage/node_modules/typical": { - "version": "7.1.1", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-versions": { - "version": "6.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/crelt": { - "version": "1.0.6", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssstyle": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "rrweb-cssom": "^0.6.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/d3-color": { - "version": "3.1.0", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/data-urls": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.4.3", - "dev": true, - "license": "MIT" - }, - "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/deep-eql": { - "version": "4.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-equal": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dependency-graph": { - "version": "0.11.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "license": "Apache-2.0" - }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "license": "MIT" - }, - "node_modules/dnd-core": { - "version": "16.0.1", - "license": "MIT", - "dependencies": { - "@react-dnd/asap": "^5.0.1", - "@react-dnd/invariant": "^4.0.1", - "redux": "^4.2.0" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "dev": true, - "license": "MIT" - }, - "node_modules/domexception": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.608", - "dev": true, - "license": "ISC" - }, - "node_modules/elkjs": { - "version": "0.8.2", - "license": "EPL-2.0" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/enquirer": { - "version": "2.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-abstract": { - "version": "1.22.3", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-aggregate-error": { - "version": "1.0.11", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.0", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.0.15", - "dev": true, - "license": "MIT", - "dependencies": { - "asynciterator.prototype": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "es-set-tostringtag": "^2.0.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.0.1" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es6-promise": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.19.8", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.19.8", - "@esbuild/android-arm64": "0.19.8", - "@esbuild/android-x64": "0.19.8", - "@esbuild/darwin-arm64": "0.19.8", - "@esbuild/darwin-x64": "0.19.8", - "@esbuild/freebsd-arm64": "0.19.8", - "@esbuild/freebsd-x64": "0.19.8", - "@esbuild/linux-arm": "0.19.8", - "@esbuild/linux-arm64": "0.19.8", - "@esbuild/linux-ia32": "0.19.8", - "@esbuild/linux-loong64": "0.19.8", - "@esbuild/linux-mips64el": "0.19.8", - "@esbuild/linux-ppc64": "0.19.8", - "@esbuild/linux-riscv64": "0.19.8", - "@esbuild/linux-s390x": "0.19.8", - "@esbuild/linux-x64": "0.19.8", - "@esbuild/netbsd-x64": "0.19.8", - "@esbuild/openbsd-x64": "0.19.8", - "@esbuild/sunos-x64": "0.19.8", - "@esbuild/win32-arm64": "0.19.8", - "@esbuild/win32-ia32": "0.19.8", - "@esbuild/win32-x64": "0.19.8" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.55.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.55.0", - "@humanwhocodes/config-array": "^0.11.13", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-compat-utils": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-config-standard": { - "version": "17.1.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-promise": "^6.0.0" - } - }, - "node_modules/eslint-config-standard-with-typescript": { - "version": "39.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/parser": "^6.4.0", - "eslint-config-standard": "17.1.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^6.4.0", - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-promise": "^6.0.0", - "typescript": "*" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-es-x": { - "version": "7.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.1.2", - "@eslint-community/regexpp": "^4.6.0", - "eslint-compat-utils": "^0.1.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "eslint": ">=8" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.14.2" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-n": { - "version": "16.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "builtins": "^5.0.1", - "eslint-plugin-es-x": "^7.1.0", - "get-tsconfig": "^4.7.0", - "ignore": "^5.2.4", - "is-builtin-module": "^3.2.1", - "is-core-module": "^2.12.1", - "minimatch": "^3.1.2", - "resolve": "^1.22.2", - "semver": "^7.5.3" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-promise": { - "version": "6.1.1", - "dev": true, - "license": "ISC", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-memoize": { - "version": "2.5.2", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.15.0", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-replace": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "array-back": "^3.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatbuffers": { - "version": "23.5.26", - "license": "SEE LICENSE IN LICENSE" - }, - "node_modules/flatted": { - "version": "3.2.9", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-extra": { - "version": "11.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.23.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", - "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" - }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http2-client": { - "version": "1.3.5", - "dev": true, - "license": "MIT" - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "9.0.21", - "devOptional": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" - }, - "node_modules/internal-slot": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/invariant": { - "version": "2.2.4", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-map": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.12", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" - } - }, - "node_modules/jiti": { - "version": "1.21.0", - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "22.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "abab": "^2.0.6", - "cssstyle": "^3.0.0", - "data-urls": "^4.0.0", - "decimal.js": "^10.4.3", - "domexception": "^4.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.4", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.1", - "ws": "^8.13.0", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsep": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", - "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - } - }, - "node_modules/json-bignum": { - "version": "0.0.3", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsonc-parser": { - "version": "2.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonpath-plus": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.2.0.tgz", - "integrity": "sha512-T9V+8iNYKFL2n2rF+w02LBOT2JjDnTjioaNFrxRy0Bv1y/hNsqR/EBK7Ojy2ythRHwmz2cRIls+9JitQGZC/sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jsep-plugin/assignment": "^1.3.0", - "@jsep-plugin/regex": "^1.0.4", - "jsep": "^1.4.0" - }, - "bin": { - "jsonpath": "bin/jsonpath-cli.js", - "jsonpath-plus": "bin/jsonpath-cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "license": "MIT" - }, - "node_modules/local-pkg": { - "version": "0.4.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "license": "MIT" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isempty": { - "version": "4.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.omit": { - "version": "4.5.0", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.omitby": { - "version": "4.6.0", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.topath": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", - "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniqby": { - "version": "4.7.0", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniqwith": { - "version": "4.5.0", - "dev": true, - "license": "MIT" - }, - "node_modules/loglevel": { - "version": "1.8.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" - } - }, - "node_modules/loglevel-plugin-prefix": { - "version": "0.8.4", - "dev": true, - "license": "MIT" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "2.3.7", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", - "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.3.tgz", - "integrity": "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", - "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromark": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", - "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", - "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", - "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", - "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", - "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", - "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", - "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", - "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", - "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", - "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", - "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", - "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", - "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", - "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", - "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", - "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", - "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mlly": { - "version": "1.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.10.0", - "pathe": "^1.1.1", - "pkg-types": "^1.0.3", - "ufo": "^1.3.0" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/nimma": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", - "integrity": "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsep-plugin/regex": "^1.0.1", - "@jsep-plugin/ternary": "^1.0.2", - "astring": "^1.8.1", - "jsep": "^1.2.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - }, - "optionalDependencies": { - "jsonpath-plus": "^6.0.1 || ^10.1.0", - "lodash.topath": "^4.5.2" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch-h2": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "http2-client": "^1.2.5" - }, - "engines": { - "node": "4.x || >=6.0.0" - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/node-readfiles": { - "version": "0.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es6-promise": "^3.2.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.14", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nwsapi": { - "version": "2.2.7", - "dev": true, - "license": "MIT" - }, - "node_modules/oas-kit-common": { - "version": "1.0.8", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "fast-safe-stringify": "^2.0.7" - } - }, - "node_modules/oas-linter": { - "version": "3.2.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@exodus/schemasafe": "^1.0.0-rc.2", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-linter/node_modules/yaml": { - "version": "1.10.2", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/oas-resolver": { - "version": "2.5.6", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "node-fetch-h2": "^2.3.0", - "oas-kit-common": "^1.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "resolve": "resolve.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-resolver/node_modules/yaml": { - "version": "1.10.2", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/oas-schema-walker": { - "version": "1.1.5", - "dev": true, - "license": "BSD-3-Clause", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-validator": { - "version": "5.0.8", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "call-me-maybe": "^1.0.1", - "oas-kit-common": "^1.0.8", - "oas-linter": "^3.2.2", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "reftools": "^1.1.9", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-validator/node_modules/yaml": { - "version": "1.10.2", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" - } - }, - "node_modules/object.hasown": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/openapi-types": { - "version": "12.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/openapi3-ts": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yaml": "^2.2.1" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/orval": { - "version": "6.22.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "^10.1.0", - "@orval/angular": "6.22.1", - "@orval/axios": "6.22.1", - "@orval/core": "6.22.1", - "@orval/mock": "6.22.1", - "@orval/query": "6.22.1", - "@orval/swr": "6.22.1", - "@orval/zod": "6.22.1", - "ajv": "^8.12.0", - "cac": "^6.7.14", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "enquirer": "^2.4.1", - "execa": "^5.1.1", - "find-up": "5.0.0", - "fs-extra": "^11.2.0", - "lodash.uniq": "^4.5.0", - "openapi-types": "^12.1.3", - "openapi3-ts": "^3.2.0", - "string-argv": "^0.3.2", - "tsconfck": "^2.0.1" - }, - "bin": { - "orval": "dist/bin/orval.js" - } - }, - "node_modules/orval/node_modules/ajv": { - "version": "8.12.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/orval/node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pad-left": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "repeat-string": "^1.5.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", - "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" - }, - "node_modules/parse5": { - "version": "7.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, - "node_modules/pkg-types/node_modules/jsonc-parser": { - "version": "3.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/playwright": { - "version": "1.40.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.40.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.40.1", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/pluralize": { - "version": "8.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pony-cause": { - "version": "1.1.1", - "dev": true, - "license": "0BSD", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/postcss": { - "version": "8.4.32", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" - }, - "node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "dev": true, - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "18.2.0", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dnd": { - "version": "16.0.1", - "license": "MIT", - "dependencies": { - "@react-dnd/invariant": "^4.0.1", - "@react-dnd/shallowequal": "^4.0.1", - "dnd-core": "^16.0.1", - "fast-deep-equal": "^3.1.3", - "hoist-non-react-statics": "^3.3.2" - }, - "peerDependencies": { - "@types/hoist-non-react-statics": ">= 3.3.1", - "@types/node": ">= 12", - "@types/react": ">= 16", - "react": ">= 16.14" - }, - "peerDependenciesMeta": { - "@types/hoist-non-react-statics": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-dnd-html5-backend": { - "version": "16.0.1", - "license": "MIT", - "dependencies": { - "dnd-core": "^16.0.1" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/react-markdown": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz", - "integrity": "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==", - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "html-url-attributes": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "unified": "^11.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.5.5", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.4", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.1", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-router": { - "version": "6.20.1", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.13.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.20.1", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.13.1", - "react-router": "6.20.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-split": { - "version": "2.0.14", - "license": "MIT", - "dependencies": { - "prop-types": "^15.5.7", - "split.js": "^1.6.0" - }, - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.1", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "invariant": "^2.2.4", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/reactflow": { - "version": "11.10.1", - "license": "MIT", - "dependencies": { - "@reactflow/background": "11.3.6", - "@reactflow/controls": "11.2.6", - "@reactflow/core": "11.10.1", - "@reactflow/minimap": "11.7.6", - "@reactflow/node-resizer": "2.2.6", - "@reactflow/node-toolbar": "1.3.6" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/redux": { - "version": "4.2.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reftools": { - "version": "1.1.9", - "dev": true, - "license": "BSD-3-Clause", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "license": "MIT" - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", - "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.8", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rrweb-cssom": { - "version": "0.6.0", - "dev": true, - "license": "MIT" - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/saxes": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.23.0", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.5.4", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/should": { - "version": "13.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "should-equal": "^2.0.0", - "should-format": "^3.0.3", - "should-type": "^1.4.0", - "should-type-adaptors": "^1.0.1", - "should-util": "^1.0.0" - } - }, - "node_modules/should-equal": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.4.0" - } - }, - "node_modules/should-format": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.3.0", - "should-type-adaptors": "^1.0.1" - } - }, - "node_modules/should-type": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/should-type-adaptors": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.3.0", - "should-util": "^1.0.0" - } - }, - "node_modules/should-util": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/side-channel": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-eval": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", - "integrity": "sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "jsep": "^1.3.6" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/split.js": { - "version": "1.6.5", - "license": "MIT" - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stackback": { - "version": "0.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.6.0", - "dev": true, - "license": "MIT" - }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stream-read-all": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "regexp.prototype.flags": "^1.5.0", - "set-function-name": "^2.0.0", - "side-channel": "^1.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/style-mod": { - "version": "4.1.0", - "license": "MIT" - }, - "node_modules/style-to-object": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", - "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", - "dependencies": { - "inline-style-parser": "0.2.4" - } - }, - "node_modules/sucrase": { - "version": "3.34.0", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "7.1.6", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swagger2openapi": { - "version": "7.0.8", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "call-me-maybe": "^1.0.1", - "node-fetch": "^2.6.1", - "node-fetch-h2": "^2.3.0", - "node-readfiles": "^0.2.0", - "oas-kit-common": "^1.0.8", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "oas-validator": "^5.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "boast": "boast.js", - "oas-validate": "oas-validate.js", - "swagger2openapi": "swagger2openapi.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/swagger2openapi/node_modules/yaml": { - "version": "1.10.2", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "dev": true, - "license": "MIT" - }, - "node_modules/table-layout": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "@75lb/deep-merge": "^1.1.1", - "array-back": "^6.2.2", - "command-line-args": "^5.2.1", - "command-line-usage": "^7.0.0", - "stream-read-all": "^3.0.1", - "typical": "^7.1.1", - "wordwrapjs": "^5.1.0" - }, - "bin": { - "table-layout": "bin/cli.js" - }, - "engines": { - "node": ">=12.17" - } - }, - "node_modules/table-layout/node_modules/array-back": { - "version": "6.2.2", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/table-layout/node_modules/typical": { - "version": "7.1.1", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/tailwindcss": { - "version": "3.3.6", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/thememirror": { - "version": "2.0.1", - "license": "MIT", - "peerDependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinybench": { - "version": "2.5.1", - "dev": true, - "license": "MIT" - }, - "node_modules/tinypool": { - "version": "0.7.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.3", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/tr46": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-api-utils": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.13.0" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "license": "Apache-2.0" - }, - "node_modules/tsconfck": { - "version": "2.1.2", - "dev": true, - "license": "MIT", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^14.13.1 || ^16 || >=18" - }, - "peerDependencies": { - "typescript": "^4.3.5 || ^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/tsconfig-paths": { - "version": "3.14.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.3.3", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typical": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ufo": { - "version": "1.3.2", - "dev": true, - "license": "MIT" - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/urijs": { - "version": "1.19.11", - "dev": true, - "license": "MIT" - }, - "node_modules/url-parse": { - "version": "1.5.10", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/utility-types": { - "version": "3.10.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/validator": { - "version": "13.11.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", - "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "0.34.6", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "mlly": "^1.4.0", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-plugin-css-injected-by-js": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.2.tgz", - "integrity": "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "vite": ">2.0.0-0" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.18.20", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/vitest": { - "version": "0.34.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^4.3.5", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.34.6", - "@vitest/runner": "0.34.6", - "@vitest/snapshot": "0.34.6", - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "acorn": "^8.9.0", - "acorn-walk": "^8.2.0", - "cac": "^6.7.14", - "chai": "^4.3.10", - "debug": "^4.3.4", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.3.3", - "strip-literal": "^1.0.1", - "tinybench": "^2.5.0", - "tinypool": "^0.7.0", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", - "vite-node": "0.34.6", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@vitest/browser": "*", - "@vitest/ui": "*", - "happy-dom": "*", - "jsdom": "*", - "playwright": "*", - "safaridriver": "*", - "webdriverio": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "playwright": { - "optional": true - }, - "safaridriver": { - "optional": true - }, - "webdriverio": { - "optional": true - } - } - }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "license": "MIT" - }, - "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "12.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", - "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.13", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/why-is-node-running": { - "version": "2.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wordwrapjs": { - "version": "5.1.0", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.3.4", - "license": "ISC", - "engines": { - "node": ">= 14" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zustand": { - "version": "4.4.7", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "1.2.0" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/web/docker-compose.build.yml b/web/docker-compose.build.yml index 04254f003c..640bf5ba14 100644 --- a/web/docker-compose.build.yml +++ b/web/docker-compose.build.yml @@ -1,5 +1,5 @@ version: "3.11" services: app: - command: bash -c "npm --prefix /app ci && npm --prefix /app run build" + command: bash -c "cd ../.. && npm ci && npm --prefix /app/web/client run build" networks: [] diff --git a/web/docker-compose.yml b/web/docker-compose.yml index e2f95200ea..c8f7e352ad 100644 --- a/web/docker-compose.yml +++ b/web/docker-compose.yml @@ -18,12 +18,11 @@ services: context: .. dockerfile: web/Dockerfile.app command: npm run dev -- --host 0.0.0.0 --port 8001 - working_dir: /app + working_dir: /app/web/client ports: - 8001:8001 volumes: - - ../web/client:/app - - /app/node_modules + - ../web/client:/app/web/client tty: true networks: - tobiko-development From fbf5345da9a47dd98e915a1c6f99cac882f9a31c Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:12:49 +0100 Subject: [PATCH 0046/1056] docs: improve dev docs for extension (#4217) --- docs/development.md | 70 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/docs/development.md b/docs/development.md index 90fd47e1a7..0b416b9f07 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,47 +1,103 @@ # Contribute to development -SQLMesh is licensed under [Apache 2.0](https://github.com/TobikoData/sqlmesh/blob/main/LICENSE). We encourage community contribution and would love for you to get involved. + +SQLMesh is licensed under [Apache 2.0](https://github.com/TobikoData/sqlmesh/blob/main/LICENSE). We encourage community contribution and would love for you to get involved. The following document outlines the process to contribute to SQLMesh. ## Prerequisites + +Before you begin, ensure you have the following installed on your machine. Exactly how to install these is dependent on your operating system. + * Docker * Docker Compose V2 * OpenJDK >= 11 * Python >= 3.9 < 3.13 -## Commands reference +## Virtual environment setup + +We do recommend using a virtual environment to develop SQLMesh. + +```bash +python -m venv .venv +source .venv/bin/activate +``` + +Once you have activated your virtual environment, you can install the dependencies by running the following command. -Install dev dependencies: ```bash make install-dev ``` + +Optionally, you can use pre-commit to automatically run linters/formatters: + +```bash +make install-pre-commit +``` + +## Python development + Run linters and formatters: + ```bash make style ``` + Run faster tests for quicker local feedback: + ```bash make fast-test ``` + Run more comprehensive tests that run on each commit: + ```bash make slow-test ``` -Install docs dependencies: + +## Documentation + +In order to run the documentation server, you will need to install the dependencies by running the following command. + ```bash make install-doc ``` -Run docs server: + +Once you have installed the dependencies, you can run the documentation server by running the following command. + ```bash make docs-serve ``` + Run docs tests: + ```bash make doc-test ``` + +## UI development + +In addition to the Python development, you can also develop the UI. + +The UI is built using React and Typescript. To run the UI, you will need to install the dependencies by running the following command. + +```bash +npm install +``` + Run ide: + ```bash make ui-up ``` -(Optional) Use pre-commit to automatically run linters/formatters: + +## Developing the VSCode extension + +Similar to UI development, you can also develop the VSCode extension. To do so, make sure you have the dependencies installed by running the following command inside the `vscode/extension` directory. + ```bash -make install-pre-commit +npm install +``` + +Once that is done, developing the VSCode extension is most easily done by launching the `Run Extensions` debug task from a Visual Studio Code workspace opened at the root of the SQLMesh repository. By default, the VSCode extension will run the SQLMesh server locally and open a new Visual Studio Code window that allows you to try out the SQLMesh IDE. It opens the `examples/sushi` project by default. To set up Visual Studio Code to run the `Run Extensions` debug task, you can run the following command which will copy the `launch.json` and `tasks.json` files to the `.vscode` directory. + +```bash +make vscode_settings ``` From ef8834db0281ae9fda287d3ad942f24cc9a2722d Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 23 Apr 2025 20:39:24 +0300 Subject: [PATCH 0047/1056] Fix: address staged file path edge case in snowflake parsing (#4233) --- sqlmesh/core/dialect.py | 1 + tests/core/test_model.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index f3cc64ce96..4bd8e86564 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -500,6 +500,7 @@ def _parse_table_parts( self._curr and self._prev.token_type in (TokenType.L_PAREN, TokenType.R_PAREN) and self._curr.text.upper() not in ("FILE_FORMAT", "PATTERN") + and not (table.args.get("format") or table.args.get("pattern")) ) ): self._retreat(index) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 89d7e23ac7..825b39ea90 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -7463,6 +7463,23 @@ def test_staged_file_path(): query = model.render_query() assert query.sql(dialect="snowflake") == "SELECT * FROM @a.b/c/d.csv (FILE_FORMAT => 'b.ff')" + expressions = d.parse( + """ + MODEL (name test, dialect snowflake); + + SELECT + * + FROM @variable (FILE_FORMAT => 'foo'), @non_variable (FILE_FORMAT => 'bar') + LIMIT 100 + """ + ) + model = load_sql_based_model(expressions, variables={"variable": "some_path"}) + query = model.render_query() + assert ( + query.sql(dialect="snowflake") + == """SELECT * FROM 'some_path' (FILE_FORMAT => 'foo') AS "SOME_PATH", @non_variable (FILE_FORMAT => 'bar') LIMIT 100""" + ) + def test_cache(): expressions = d.parse( From 666419dd513c201347cfcf1c1d63b93b074deda8 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 23 Apr 2025 12:02:50 -0700 Subject: [PATCH 0048/1056] Fix: Plan application should succeeed even when the snapshot query in state is unrenderable (#4235) --- sqlmesh/core/context.py | 2 ++ sqlmesh/core/model/definition.py | 6 +++- sqlmesh/core/snapshot/cache.py | 22 ++++++++++--- sqlmesh/core/state_sync/cache.py | 3 ++ tests/core/test_integration.py | 54 ++++++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 6 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 45b125b60b..4dacff6d5c 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -2144,6 +2144,8 @@ def table_name(self, model_name: str, dev: bool) -> str: def clear_caches(self) -> None: for path in self.configs: rmtree(path / c.CACHE) + if isinstance(self.state_sync, CachingStateSync): + self.state_sync.clear_cache() def export_state( self, diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 1b6ac7c58e..34f5dd5511 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -1337,7 +1337,11 @@ def columns_to_types(self) -> t.Optional[t.Dict[str, exp.DataType]]: if self.columns_to_types_ is not None: self._columns_to_types = self.columns_to_types_ elif self._columns_to_types is None: - query = self._query_renderer.render() + try: + query = self._query_renderer.render() + except Exception: + logger.exception("Failed to render query for model %s", self.fqn) + return None if query is None: return None diff --git a/sqlmesh/core/snapshot/cache.py b/sqlmesh/core/snapshot/cache.py index f99dca64a2..436427eb82 100644 --- a/sqlmesh/core/snapshot/cache.py +++ b/sqlmesh/core/snapshot/cache.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import typing as t from pathlib import Path @@ -13,6 +14,9 @@ from sqlmesh.utils.cache import FileCache +logger = logging.getLogger(__name__) + + class SnapshotCache: def __init__(self, path: Path): self._snapshot_cache: FileCache[Snapshot] = FileCache(path, prefix="snapshot") @@ -70,7 +74,12 @@ def get_or_load( self._update_node_hash_cache(snapshot) if snapshot.is_model and c.MAX_FORK_WORKERS == 1: - self._optimized_query_cache.with_optimized_query(snapshot.model) + try: + self._optimized_query_cache.with_optimized_query(snapshot.model) + except Exception: + logger.exception( + "Failed to cache optimized query for snapshot %s", snapshot.snapshot_id + ) self.put(snapshot) @@ -82,10 +91,13 @@ def put(self, snapshot: Snapshot) -> None: if self._snapshot_cache.exists(entry_name): return - if snapshot.is_model: - # make sure we preload full_depends_on - snapshot.model.full_depends_on - self._snapshot_cache.put(entry_name, value=snapshot) + try: + if snapshot.is_model: + # make sure we preload full_depends_on + snapshot.model.full_depends_on + self._snapshot_cache.put(entry_name, value=snapshot) + except Exception: + logger.exception("Failed to cache snapshot %s", snapshot.snapshot_id) def clear(self) -> None: self._snapshot_cache.clear() diff --git a/sqlmesh/core/state_sync/cache.py b/sqlmesh/core/state_sync/cache.py index ab40f186e9..959f5eab86 100644 --- a/sqlmesh/core/state_sync/cache.py +++ b/sqlmesh/core/state_sync/cache.py @@ -143,3 +143,6 @@ def unpause_snapshots( ) -> None: self.snapshot_cache.clear() self.state_sync.unpause_snapshots(snapshots, unpaused_dt) + + def clear_cache(self) -> None: + self.snapshot_cache.clear() diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 3a27845019..bb4f7a9b22 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -1,6 +1,7 @@ from __future__ import annotations import typing as t +import json from collections import Counter from datetime import timedelta from unittest import mock @@ -3995,6 +3996,59 @@ def test_empty_backfill_new_model(init_and_plan_context: t.Callable): assert snapshot.intervals[-1][1] <= to_timestamp("2023-01-08") +@time_machine.travel("2023-01-08 15:00:00 UTC") +@pytest.mark.parametrize("forward_only", [False, True]) +def test_plan_repairs_unrenderable_snapshot_state( + init_and_plan_context: t.Callable, forward_only: bool +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + target_snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") + assert target_snapshot + + # Manually corrupt the snapshot's query + raw_snapshot = context.state_sync.state_sync.engine_adapter.fetchone( + f"SELECT snapshot FROM sqlmesh._snapshots WHERE name = '{target_snapshot.name}' AND identifier = '{target_snapshot.identifier}'" + )[0] # type: ignore + parsed_snapshot = json.loads(raw_snapshot) + parsed_snapshot["node"]["query"] = "SELECT @missing_macro()" + context.state_sync.state_sync.engine_adapter.update_table( + "sqlmesh._snapshots", + {"snapshot": json.dumps(parsed_snapshot)}, + f"name = '{target_snapshot.name}' AND identifier = '{target_snapshot.identifier}'", + ) + + context.clear_caches() + + target_snapshot_in_state = context.state_sync.get_snapshots([target_snapshot.snapshot_id])[ + target_snapshot.snapshot_id + ] + with pytest.raises(Exception): + target_snapshot_in_state.model.render_query_or_raise() + + # Repair the snapshot by creating a new version of it + context.upsert_model(target_snapshot.model.name, stamp="repair") + target_snapshot = context.get_snapshot(target_snapshot.name) + + plan_builder = context.plan_builder("prod", forward_only=forward_only) + plan = plan_builder.build() + assert plan.directly_modified == {target_snapshot.snapshot_id} + if not forward_only: + assert {i.snapshot_id for i in plan.missing_intervals} == {target_snapshot.snapshot_id} + plan_builder.set_choice(target_snapshot, SnapshotChangeCategory.NON_BREAKING) + plan = plan_builder.build() + + context.apply(plan) + + context.clear_caches() + assert context.get_snapshot(target_snapshot.name).model.render_query_or_raise() + target_snapshot_in_state = context.state_sync.get_snapshots([target_snapshot.snapshot_id])[ + target_snapshot.snapshot_id + ] + assert target_snapshot_in_state.model.render_query_or_raise() + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_dbt_requirements(sushi_dbt_context: Context): assert set(sushi_dbt_context.requirements) == {"dbt-core", "dbt-duckdb"} From 930dd5db2805b2bd75f43e59aaec4647ee5fe470 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Wed, 23 Apr 2025 15:49:17 -0500 Subject: [PATCH 0049/1056] Fix: remove extraneous spacing between CLI annotation and runtime (#4231) --- sqlmesh/core/console.py | 78 +++++++++++++++++++++-------------------- tests/cli/test_cli.py | 70 ++++++++++++++++++++++++++++++------ 2 files changed, 100 insertions(+), 48 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index fd9c9e89e4..8febdec8ec 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -891,7 +891,7 @@ def update_snapshot_evaluation_progress( self.evaluation_column_widths["duration"] ) - msg = f"{batch} {display_name} {annotation} {duration}".replace( + msg = f"{batch} {display_name} {annotation} {duration}".replace( CHECK_MARK, GREEN_CHECK_MARK ) @@ -3359,54 +3359,56 @@ def _create_evaluation_model_annotation(snapshot: Snapshot, interval_info: t.Opt return interval_info if interval_info else "" -def _calculate_interval_str_len(batched_intervals: t.Dict[Snapshot, t.List[Interval]]) -> int: +def _calculate_interval_str_len(snapshot: Snapshot, intervals: t.List[Interval]) -> int: interval_str_len = 0 - for snapshot, intervals in batched_intervals.items(): - for interval in intervals: - interval_str_len = max( - interval_str_len, - len( - _create_evaluation_model_annotation( - snapshot, _format_evaluation_model_interval(snapshot, interval) - ) - ), - ) + for interval in intervals: + interval_str_len = max( + interval_str_len, + len( + _create_evaluation_model_annotation( + snapshot, _format_evaluation_model_interval(snapshot, interval) + ) + ), + ) return interval_str_len -def _calculate_audit_str_len(batched_intervals: t.Dict[Snapshot, t.List[Interval]]) -> int: +def _calculate_audit_str_len(snapshot: Snapshot) -> int: # The annotation includes audit results. We cannot build the audits result string # until after evaluation occurs, but we must determine the annotation column width here. # Therefore, we add enough padding for the longest possible audits result string. audit_str_len = 0 audit_base_str_len = len(f", audits ") + 1 # +1 for check/X - for snapshot in batched_intervals: - if snapshot.is_audit: + if snapshot.is_audit: + # +1 for "1" audit count, +1 for red X + audit_str_len = max( + audit_str_len, audit_base_str_len + (2 if not snapshot.audit.blocking else 1) + ) + if snapshot.is_model and snapshot.model.audits: + num_audits = len(snapshot.model.audits_with_args) + num_nonblocking_audits = sum( + 1 + for audit in snapshot.model.audits_with_args + if not audit[0].blocking + or ("blocking" in audit[1] and audit[1]["blocking"] == exp.false()) + ) + if num_audits == 1: # +1 for "1" audit count, +1 for red X - audit_str_len = max( - audit_str_len, audit_base_str_len + (2 if not snapshot.audit.blocking else 1) - ) - if snapshot.is_model and snapshot.model.audits: - num_audits = len(snapshot.model.audits_with_args) - num_nonblocking_audits = sum( - 1 - for audit in snapshot.model.audits_with_args - if not audit[0].blocking - or ("blocking" in audit[1] and audit[1]["blocking"] == exp.false()) - ) - if num_audits == 1: - # +1 for "1" audit count, +1 for red X - audit_len = audit_base_str_len + (2 if num_nonblocking_audits else 1) - else: - audit_len = audit_base_str_len + len(str(num_audits)) - if num_nonblocking_audits: - # +1 for space, +1 for red X - audit_len += len(str(num_nonblocking_audits)) + 2 - audit_str_len = max(audit_str_len, audit_len) + audit_len = audit_base_str_len + (2 if num_nonblocking_audits else 1) + else: + audit_len = audit_base_str_len + len(str(num_audits)) + if num_nonblocking_audits: + # +1 for space, +1 for red X + audit_len += len(str(num_nonblocking_audits)) + 2 + audit_str_len = max(audit_str_len, audit_len) return audit_str_len def _calculate_annotation_str_len(batched_intervals: t.Dict[Snapshot, t.List[Interval]]) -> int: - return _calculate_interval_str_len(batched_intervals) + _calculate_audit_str_len( - batched_intervals - ) + annotation_str_len = 0 + for snapshot, intervals in batched_intervals.items(): + annotation_str_len = max( + annotation_str_len, + _calculate_interval_str_len(snapshot, intervals) + _calculate_audit_str_len(snapshot), + ) + return annotation_str_len diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 12b9e8343d..cf911c5f35 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -182,7 +182,7 @@ def test_plan(runner, tmp_path): assert_plan_success(result) # 'Models needing backfill' section and eval progress bar should display the same inclusive intervals assert "sqlmesh_example.incremental_model: [2020-01-01 - 2022-12-31]" in result.output - assert "sqlmesh_example.incremental_model [insert 2020-01-01 - 2022-12-31]" in result.output + assert "sqlmesh_example.incremental_model [insert 2020-01-01 - 2022-12-31]" in result.output def test_plan_skip_tests(runner, tmp_path): @@ -243,7 +243,7 @@ def test_plan_restate_model(runner, tmp_path): assert result.exit_code == 0 assert_duckdb_test(result) assert "No changes to plan: project files match the `prod` environment" in result.output - assert "sqlmesh_example.full_model [full refresh" in result.output + assert "sqlmesh_example.full_model [full refresh" in result.output assert_model_batches_executed(result) assert_virtual_layer_updated(result) @@ -553,6 +553,56 @@ def test_plan_dev_no_changes(runner, tmp_path): assert_virtual_layer_updated(result) +def test_plan_dev_longnames(runner, tmp_path): + create_example_project(tmp_path) + + long_model_names = { + "full": f"full_{'a' * 80}", + "incremental": f"incremental_{'b' * 80}", + "seed": f"seed_{'c' * 80}", + } + for model_name in long_model_names: + with open(tmp_path / "models" / f"{model_name}_model.sql", "r") as f: + model_text = f.read() + for more_model_names in long_model_names: + model_text = model_text.replace( + f"sqlmesh_example.{more_model_names}_model", + f"sqlmesh_example.{long_model_names[more_model_names]}_model", + ) + with open(tmp_path / "models" / f"{model_name}_model.sql", "w") as f: + f.write(model_text) + + # Input: `y` to apply and backfill + result = runner.invoke( + cli, + [ + "--log-file-dir", + tmp_path, + "--paths", + tmp_path, + "plan", + "dev_butamuchlongerenvironmentname", + "--skip-tests", + "--no-prompts", + "--auto-apply", + ], + ) + assert result.exit_code == 0 + assert ( + "sqlmesh_example__dev_butamuchlongerenvironmentname.seed_cccccccccccccccccccccccc\ncccccccccccccccccccccccccccccccccccccccccccccccccccccccc_model [insert \nseed file]" + in result.output + ) + assert ( + "sqlmesh_example__dev_butamuchlongerenvironmentname.incremental_bbbbbbbbbbbbbbbbb\nbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_model [insert " + in result.output + ) + assert ( + "sqlmesh_example__dev_butamuchlongerenvironmentname.full_aaaaaaaaaaaaaaaaaaaaaaaa\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_model [full \nrefresh" + in result.output + ) + assert_backfill_success(result) + + def test_plan_nonbreaking(runner, tmp_path): create_example_project(tmp_path) init_prod_and_backfill(runner, tmp_path) @@ -568,8 +618,8 @@ def test_plan_nonbreaking(runner, tmp_path): assert "+ 'a' AS new_col" in result.output assert "Directly Modified: sqlmesh_example.incremental_model (Non-breaking)" in result.output assert "sqlmesh_example.full_model (Indirect Non-breaking)" in result.output - assert "sqlmesh_example.incremental_model [insert" in result.output - assert "sqlmesh_example.full_model [full refresh" not in result.output + assert "sqlmesh_example.incremental_model [insert" in result.output + assert "sqlmesh_example.full_model [full refresh" not in result.output assert_backfill_success(result) @@ -626,8 +676,8 @@ def test_plan_breaking(runner, tmp_path): assert result.exit_code == 0 assert "+ item_id + 1 AS item_id," in result.output assert "Directly Modified: sqlmesh_example.full_model (Breaking)" in result.output - assert "sqlmesh_example.full_model [full refresh" in result.output - assert "sqlmesh_example.incremental_model [insert" not in result.output + assert "sqlmesh_example.full_model [full refresh" in result.output + assert "sqlmesh_example.incremental_model [insert" not in result.output assert_backfill_success(result) @@ -665,8 +715,8 @@ def test_plan_dev_select(runner, tmp_path): assert "+ item_id + 1 AS item_id," not in result.output assert "Directly Modified: sqlmesh_example__dev.full_model (Breaking)" not in result.output # only incremental_model backfilled - assert "sqlmesh_example__dev.incremental_model [insert" in result.output - assert "sqlmesh_example__dev.full_model [full refresh" not in result.output + assert "sqlmesh_example__dev.incremental_model [insert" in result.output + assert "sqlmesh_example__dev.full_model [full refresh" not in result.output assert_backfill_success(result) @@ -704,8 +754,8 @@ def test_plan_dev_backfill(runner, tmp_path): "Directly Modified: sqlmesh_example__dev.incremental_model (Non-breaking)" in result.output ) # only incremental_model backfilled - assert "sqlmesh_example__dev.incremental_model [insert" in result.output - assert "sqlmesh_example__dev.full_model [full refresh" not in result.output + assert "sqlmesh_example__dev.incremental_model [insert" in result.output + assert "sqlmesh_example__dev.full_model [full refresh" not in result.output assert_backfill_success(result) From 21a3a20b8b752a093565cf0bd414423ddebc64bb Mon Sep 17 00:00:00 2001 From: Toby Mao Date: Wed, 23 Apr 2025 19:59:22 -0700 Subject: [PATCH 0050/1056] fix!: add exp and SQL to type coerce so that it works for signals (#4236) --- sqlmesh/core/macros.py | 28 +++++++++++++++++++--------- tests/core/test_snapshot.py | 13 ++++++++++--- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index 8872845731..5bbcbf21a6 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -75,14 +75,24 @@ class MacroStrTemplate(Template): EXPRESSIONS_NAME_MAP = {} SQL = t.NewType("SQL", str) -SUPPORTED_TYPES = { - "t": t, - "typing": t, - "List": t.List, - "Tuple": t.Tuple, - "Union": t.Union, - "DatetimeRanges": DatetimeRanges, -} + +@lru_cache() +def get_supported_types() -> t.Dict[str, t.Any]: + from sqlmesh.core.context import ExecutionContext + + return { + "t": t, + "typing": t, + "List": t.List, + "Tuple": t.Tuple, + "Union": t.Union, + "DatetimeRanges": DatetimeRanges, + "exp": exp, + "SQL": SQL, + "MacroEvaluator": MacroEvaluator, + "ExecutionContext": ExecutionContext, + } + for klass in sqlglot.Parser.EXPRESSION_PARSERS: name = klass if isinstance(klass, str) else klass.__name__ # type: ignore @@ -1305,7 +1315,7 @@ def call_macro( bound.apply_defaults() try: - annotations = t.get_type_hints(func, localns=SUPPORTED_TYPES) + annotations = t.get_type_hints(func, localns=get_supported_types()) except (NameError, TypeError): # forward references aren't handled annotations = {} diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index 370d70b084..720a356390 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -21,6 +21,7 @@ from sqlmesh.core.context import Context from sqlmesh.core.dialect import parse, parse_one from sqlmesh.core.environment import EnvironmentNamingInfo +from sqlmesh.core.macros import SQL from sqlmesh.core.model import ( FullKind, IncrementalByTimeRangeKind, @@ -2909,8 +2910,14 @@ def test_apply_auto_restatements_disable_restatement_downstream(make_snapshot): def test_render_signal(make_snapshot, mocker): @signal() - def check_types(batch, env: str, default: int = 0): - if env != "in_memory" or not default == 0: + def check_types(batch, env: str, sql: list[SQL], table: exp.Table, default: int = 0): + if not ( + env == "in_memory" + and default == 0 + and isinstance(sql, list) + and isinstance(sql[0], str) + and isinstance(table, exp.Table) + ): raise return True @@ -2919,7 +2926,7 @@ def check_types(batch, env: str, default: int = 0): """ MODEL ( name test_schema.test_model, - signals check_types(env := @gateway) + signals check_types(env := @gateway, sql := [a.b], table := b.c) ); SELECT a FROM tbl; """ From 5201abfcba3dea2490441928287570c128c02798 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 23 Apr 2025 20:09:52 -0700 Subject: [PATCH 0051/1056] =?UTF-8?q?Fix:=20Make=20sure=20backfill=20is=20?= =?UTF-8?q?not=20triggered=20for=20models=20downstream=20of=20met=E2=80=A6?= =?UTF-8?q?adata=20changes=20(#4237)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmesh/core/plan/builder.py | 8 +++++++- sqlmesh/core/snapshot/definition.py | 4 +++- tests/core/test_context.py | 2 +- tests/core/test_integration.py | 26 ++++++++++++++++++++++++++ tests/core/test_snapshot.py | 4 ++-- 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index dd3888112a..f042116879 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -590,7 +590,9 @@ def _categorize_snapshot( if p_id in snapshot.parents: direct_parent_categories.add(parent.change_category) - if not direct_parent_categories or direct_parent_categories.intersection( + if snapshot.is_model and snapshot.model.forward_only: + snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + elif not direct_parent_categories or direct_parent_categories.intersection( {SnapshotChangeCategory.BREAKING, SnapshotChangeCategory.INDIRECT_BREAKING} ): snapshot.categorize_as(SnapshotChangeCategory.INDIRECT_BREAKING) @@ -619,6 +621,10 @@ def _apply_effective_from(self) -> None: snapshot.effective_from = self._effective_from def _is_forward_only_change(self, s_id: SnapshotId) -> bool: + if not self._context_diff.directly_modified( + s_id.name + ) and not self._context_diff.indirectly_modified(s_id.name): + return False snapshot = self._context_diff.snapshots[s_id] if snapshot.name in self._context_diff.modified_snapshots: _, old = self._context_diff.modified_snapshots[snapshot.name] diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index e930659b36..2bbb806e50 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1473,7 +1473,9 @@ def create( ) if this_deployable: snapshot = snapshots[node] - is_forward_only_model = snapshot.is_model and snapshot.model.forward_only + is_forward_only_model = ( + snapshot.is_model and snapshot.model.forward_only and not snapshot.is_metadata + ) has_auto_restatement = ( snapshot.is_model and snapshot.model.auto_restatement_cron is not None ) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index e12cf53ca8..eb68ba8db3 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -197,7 +197,7 @@ def test_render_sql_model(sushi_context, assert_exp_eq, copy_to_temp_path: t.Cal def test_render_non_deployable_parent(sushi_context, assert_exp_eq, copy_to_temp_path: t.Callable): model = sushi_context.get_model("sushi.waiter_revenue_by_day") forward_only_kind = model.kind.copy(update={"forward_only": True}) - model = model.copy(update={"kind": forward_only_kind}) + model = model.copy(update={"kind": forward_only_kind, "stamp": "trigger forward-only change"}) sushi_context.upsert_model(model) sushi_context.plan("dev", no_prompts=True, auto_apply=True) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index bb4f7a9b22..b52889c850 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -4049,6 +4049,32 @@ def test_plan_repairs_unrenderable_snapshot_state( assert target_snapshot_in_state.model.render_query_or_raise() +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_no_backfill_for_model_downstream_of_metadata_change(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + # Make sushi.waiter_revenue_by_day a forward-only model. + forward_only_model = context.get_model("sushi.waiter_revenue_by_day") + updated_model_kind = forward_only_model.kind.copy(update={"forward_only": True}) + forward_only_model = forward_only_model.copy(update={"kind": updated_model_kind}) + context.upsert_model(forward_only_model) + + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + + # Make a metadata change upstream of the forward-only model. + context.upsert_model("sushi.orders", owner="new_owner") + + plan = context.plan_builder("test_dev").build() + assert plan.has_changes + assert not plan.directly_modified + assert not plan.indirectly_modified + assert not plan.missing_intervals + assert all( + snapshot.change_category == SnapshotChangeCategory.METADATA + for snapshot in plan.new_snapshots + ) + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_dbt_requirements(sushi_dbt_context: Context): assert set(sushi_dbt_context.requirements) == {"dbt-core", "dbt-duckdb"} diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index 720a356390..f27ad8f3c1 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -2034,11 +2034,11 @@ def test_deployability_index_categorized_forward_only_model(make_snapshot): snapshot_a = make_snapshot(model_a) snapshot_a.previous_versions = snapshot_a_old.all_versions - snapshot_a.categorize_as(SnapshotChangeCategory.METADATA) + snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) snapshot_b = make_snapshot(SqlModel(name="b", query=parse_one("SELECT 1"))) snapshot_b.parents = (snapshot_a.snapshot_id,) - snapshot_b.categorize_as(SnapshotChangeCategory.METADATA) + snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) deployability_index = DeployabilityIndex.create( {s.snapshot_id: s for s in [snapshot_a, snapshot_b]} From 9823b9e8a89a3b465915644ac496c30d3003bbc3 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:47:06 +0300 Subject: [PATCH 0052/1056] Fix: Support on run start / on run end hooks of dbt packages (#4222) --- sqlmesh/core/context.py | 3 +- sqlmesh/core/loader.py | 12 +- sqlmesh/dbt/loader.py | 104 +++++++++--------- sqlmesh/dbt/manifest.py | 38 +++++-- sqlmesh/dbt/package.py | 16 ++- sqlmesh/utils/jinja.py | 28 +++++ tests/dbt/test_adapter.py | 89 ++++++++++++--- tests/dbt/test_transformation.py | 41 ++++++- tests/fixtures/dbt/sushi_test/dbt_project.yml | 4 +- .../packages/customers/dbt_project.yml | 8 ++ .../customers/macros/packaged_tables.sql | 5 + 11 files changed, 261 insertions(+), 87 deletions(-) create mode 100644 tests/fixtures/dbt/sushi_test/packages/customers/macros/packaged_tables.sql diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 4dacff6d5c..634db636ec 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -610,8 +610,7 @@ def load(self, update_schemas: bool = True) -> GenericContext[C]: self._standalone_audits.update(project.standalone_audits) self._requirements.update(project.requirements) self._excluded_requirements.update(project.excluded_requirements) - if project.environment_statements: - self._environment_statements.append(project.environment_statements) + self._environment_statements.extend(project.environment_statements) config = loader.config self._linters[config.project] = Linter.from_rules( diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index 69c8e2f423..3c688b8527 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -61,7 +61,7 @@ class LoadedProject: metrics: UniqueKeyDict[str, Metric] requirements: t.Dict[str, str] excluded_requirements: t.Set[str] - environment_statements: t.Optional[EnvironmentStatements] + environment_statements: t.List[EnvironmentStatements] user_rules: RuleSet @@ -187,9 +187,9 @@ def _load_audits( ) -> UniqueKeyDict[str, Audit]: """Loads all audits.""" - def _load_environment_statements(self, macros: MacroRegistry) -> EnvironmentStatements | None: + def _load_environment_statements(self, macros: MacroRegistry) -> t.List[EnvironmentStatements]: """Loads environment statements.""" - return None + return [] def load_materializations(self) -> None: """Loads custom materializations.""" @@ -651,7 +651,7 @@ def _load_metrics(self) -> UniqueKeyDict[str, MetricMeta]: return metrics - def _load_environment_statements(self, macros: MacroRegistry) -> EnvironmentStatements | None: + def _load_environment_statements(self, macros: MacroRegistry) -> t.List[EnvironmentStatements]: """Loads environment statements.""" if self.config.before_all or self.config.after_all: @@ -673,8 +673,8 @@ def _load_environment_statements(self, macros: MacroRegistry) -> EnvironmentStat path=self.config_path, ) - return EnvironmentStatements(**statements, python_env=python_env) - return None + return [EnvironmentStatements(**statements, python_env=python_env)] + return [] def _load_linting_rules(self) -> RuleSet: user_rules: UniqueKeyDict[str, type[Rule]] = UniqueKeyDict("rules") diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index a270ca1745..2cf686f2c9 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -29,8 +29,8 @@ from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.jinja import ( JinjaMacroRegistry, - MacroInfo, extract_macro_references_and_variables, + make_jinja_registry, ) if sys.version_info >= (3, 12): @@ -238,59 +238,65 @@ def _load_requirements(self) -> t.Tuple[t.Dict[str, str], t.Set[str]]: return requirements, excluded_requirements - def _load_environment_statements(self, macros: MacroRegistry) -> EnvironmentStatements | None: + def _load_environment_statements(self, macros: MacroRegistry) -> t.List[EnvironmentStatements]: """Loads dbt's on_run_start, on_run_end hooks into sqlmesh's before_all, after_all statements respectively.""" - on_run_start: t.List[str] = [] - on_run_end: t.List[str] = [] - jinja_root_macros: t.Dict[str, MacroInfo] = {} - variables: t.Dict[str, t.Any] = self._get_variables() + environment_statements: t.List[EnvironmentStatements] = [] dialect = self.config.dialect for project in self._load_projects(): - context = project.context.copy() - if manifest := context._manifest: - on_run_start.extend(manifest._on_run_start or []) - on_run_end.extend(manifest._on_run_end or []) - - if root_package := context.jinja_macros.root_package_name: - if root_macros := context.jinja_macros.packages.get(root_package): - jinja_root_macros |= root_macros - context.set_and_render_variables(context.variables, root_package) - variables |= context.variables - - if statements := on_run_start + on_run_end: - jinja_macro_references, used_variables = extract_macro_references_and_variables( - *(gen(stmt) for stmt in statements) - ) - jinja_macros = context.jinja_macros - jinja_macros.root_macros = jinja_root_macros - jinja_macros = ( - jinja_macros.trim(jinja_macro_references) - if not jinja_macros.trimmed - else jinja_macros - ) - - python_env = make_python_env( - [s for stmt in statements for s in d.parse(stmt, default_dialect=dialect)], - jinja_macro_references=jinja_macro_references, - module_path=self.config_path, - macros=macros, - variables=variables, - used_variables=used_variables, - path=self.config_path, - ) + context = project.context + hooks_by_package_name: t.Dict[str, EnvironmentStatements] = {} + for package_name, package in project.packages.items(): + context.set_and_render_variables(package.variables, package_name) + on_run_start: t.List[str] = [ + on_run_hook.sql + for on_run_hook in sorted(package.on_run_start.values(), key=lambda h: h.index) + ] + on_run_end: t.List[str] = [ + on_run_hook.sql + for on_run_hook in sorted(package.on_run_end.values(), key=lambda h: h.index) + ] - return EnvironmentStatements( - before_all=[ - d.jinja_statement(stmt).sql(dialect=dialect) for stmt in on_run_start or [] - ], - after_all=[ - d.jinja_statement(stmt).sql(dialect=dialect) for stmt in on_run_end or [] - ], - python_env=python_env, - jinja_macros=jinja_macros, - ) - return None + if statements := on_run_start + on_run_end: + jinja_references, used_variables = extract_macro_references_and_variables( + *(gen(stmt) for stmt in statements) + ) + + jinja_registry = make_jinja_registry( + context.jinja_macros, package_name, jinja_references + ) + + python_env = make_python_env( + [s for stmt in statements for s in d.parse(stmt, default_dialect=dialect)], + jinja_macro_references=jinja_references, + module_path=self.config_path, + macros=macros, + variables=context.variables, + used_variables=used_variables, + path=self.config_path, + ) + + hooks_by_package_name[package_name] = EnvironmentStatements( + before_all=[ + d.jinja_statement(stmt).sql(dialect=dialect) + for stmt in on_run_start or [] + ], + after_all=[ + d.jinja_statement(stmt).sql(dialect=dialect) + for stmt in on_run_end or [] + ], + python_env=python_env, + jinja_macros=jinja_registry, + ) + # Project hooks should be executed first and then rest of the packages + environment_statements = [ + statements + for _, statements in sorted( + hooks_by_package_name.items(), + key=lambda item: 0 if item[0] == context.project_name else 1, + ) + ] + return environment_statements def _compute_yaml_max_mtime_per_subfolder(self, root: Path) -> t.Dict[Path, float]: if not root.is_dir(): diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 02387c61ca..fd67ef35c5 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -27,7 +27,7 @@ from sqlmesh.dbt.basemodel import Dependencies from sqlmesh.dbt.builtin import BUILTIN_FILTERS, BUILTIN_GLOBALS, OVERRIDDEN_MACROS from sqlmesh.dbt.model import ModelConfig -from sqlmesh.dbt.package import MacroConfig +from sqlmesh.dbt.package import HookConfig, MacroConfig from sqlmesh.dbt.seed import SeedConfig from sqlmesh.dbt.source import SourceConfig from sqlmesh.dbt.target import TargetConfig @@ -54,6 +54,7 @@ SeedConfigs = t.Dict[str, SeedConfig] SourceConfigs = t.Dict[str, SourceConfig] MacroConfigs = t.Dict[str, MacroConfig] +HookConfigs = t.Dict[str, HookConfig] IGNORED_PACKAGES = {"elementary"} @@ -94,8 +95,8 @@ def __init__( self.project_path / c.CACHE, "jinja_calls" ) - self._on_run_start: t.Optional[t.List[str]] = None - self._on_run_end: t.Optional[t.List[str]] = None + self._on_run_start_per_package: t.Dict[str, HookConfigs] = defaultdict(dict) + self._on_run_end_per_package: t.Dict[str, HookConfigs] = defaultdict(dict) def tests(self, package_name: t.Optional[str] = None) -> TestConfigs: self._load_all() @@ -117,6 +118,14 @@ def macros(self, package_name: t.Optional[str] = None) -> MacroConfigs: self._load_all() return self._macros_per_package[package_name or self._project_name] + def on_run_start(self, package_name: t.Optional[str] = None) -> HookConfigs: + self._load_all() + return self._on_run_start_per_package[package_name or self._project_name] + + def on_run_end(self, package_name: t.Optional[str] = None) -> HookConfigs: + self._load_all() + return self._on_run_end_per_package[package_name or self._project_name] + @property def all_macros(self) -> t.Dict[str, t.Dict[str, MacroInfo]]: self._load_all() @@ -136,6 +145,7 @@ def _load_all(self) -> None: self._load_sources() self._load_tests() self._load_models_and_seeds() + self._load_on_run_start_end() self._is_loaded = True self._call_cache.put("", value={k: v for k, (v, used) in self._calls.items() if used}) @@ -274,6 +284,23 @@ def _load_models_and_seeds(self) -> None: **node_config, ) + def _load_on_run_start_end(self) -> None: + for node in self._manifest.nodes.values(): + if node.resource_type == "operation" and ( + set(node.tags) & {"on-run-start", "on-run-end"} + ): + sql = node.raw_code if DBT_VERSION >= (1, 3) else node.raw_sql # type: ignore + node_name = node.name + node_path = Path(node.original_file_path) + if "on-run-start" in node.tags: + self._on_run_start_per_package[node.package_name][node_name] = HookConfig( + sql=sql, index=node.index or 0, path=node_path + ) + else: + self._on_run_end_per_package[node.package_name][node_name] = HookConfig( + sql=sql, index=node.index or 0, path=node_path + ) + @property def _manifest(self) -> Manifest: if not self.__manifest: @@ -315,11 +342,6 @@ def _load_manifest(self) -> Manifest: runtime_config = RuntimeConfig.from_parts(project, profile, args) - if runtime_config.on_run_start: - self._on_run_start = runtime_config.on_run_start - if runtime_config.on_run_end: - self._on_run_end = runtime_config.on_run_end - self._project_name = project.project_name if DBT_VERSION >= (1, 8): diff --git a/sqlmesh/dbt/package.py b/sqlmesh/dbt/package.py index d67bc4a508..8d2dc191f5 100644 --- a/sqlmesh/dbt/package.py +++ b/sqlmesh/dbt/package.py @@ -28,6 +28,14 @@ class MacroConfig(PydanticModel): path: Path +class HookConfig(PydanticModel): + """Class to contain on run start / on run end hooks.""" + + sql: str + index: int + path: Path + + class Package(PydanticModel): """Class to contain package configuration""" @@ -38,6 +46,8 @@ class Package(PydanticModel): models: t.Dict[str, ModelConfig] variables: t.Dict[str, t.Any] macros: t.Dict[str, MacroConfig] + on_run_start: t.Dict[str, HookConfig] + on_run_end: t.Dict[str, HookConfig] files: t.Set[Path] @property @@ -83,6 +93,8 @@ def load(self, package_root: Path) -> Package: models = _fix_paths(self._context.manifest.models(package_name), package_root) seeds = _fix_paths(self._context.manifest.seeds(package_name), package_root) macros = _fix_paths(self._context.manifest.macros(package_name), package_root) + on_run_start = _fix_paths(self._context.manifest.on_run_start(package_name), package_root) + on_run_end = _fix_paths(self._context.manifest.on_run_end(package_name), package_root) sources = self._context.manifest.sources(package_name) config_paths = { @@ -102,10 +114,12 @@ def load(self, package_root: Path) -> Package: variables=package_variables, macros=macros, files=config_paths, + on_run_start=on_run_start, + on_run_end=on_run_end, ) -T = t.TypeVar("T", TestConfig, ModelConfig, MacroConfig, SeedConfig) +T = t.TypeVar("T", TestConfig, ModelConfig, MacroConfig, SeedConfig, HookConfig) def _fix_paths(configs: t.Dict[str, T], package_root: Path) -> t.Dict[str, T]: diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index a4d7492861..403b8caba9 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -608,3 +608,31 @@ def create_builtin_globals( c.GATEWAY: lambda: variables.get(c.GATEWAY, None), **global_vars, } + + +def make_jinja_registry( + jinja_macros: JinjaMacroRegistry, package_name: str, jinja_references: t.Set[MacroReference] +) -> JinjaMacroRegistry: + """ + Creates a Jinja macro registry for a specific package. + + This function takes an existing Jinja macro registry and returns a new + registry that includes only the macros associated with the specified + package and trims the registry to include only the macros referenced + in the provided set of macro references. + + Args: + jinja_macros: The original Jinja macro registry containing all macros. + package_name: The name of the package for which to create the registry. + jinja_references: A set of macro references to retain in the new registry. + + Returns: + A new JinjaMacroRegistry containing only the macros for the specified + package and the referenced macros. + """ + + jinja_registry = jinja_macros.copy() + jinja_registry.root_macros = jinja_registry.packages.get(package_name) or {} + jinja_registry = jinja_registry.trim(jinja_references) + + return jinja_registry diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index d77a8f75fd..dc671ec469 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -278,37 +278,45 @@ def test_quote_as_configured(): def test_on_run_start_end(copy_to_temp_path): project_root = "tests/fixtures/dbt/sushi_test" sushi_context = Context(paths=copy_to_temp_path(project_root)) - assert len(sushi_context._environment_statements) == 1 - environment_statements = sushi_context._environment_statements[0] + assert len(sushi_context._environment_statements) == 2 - assert environment_statements.before_all == [ - "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);\nJINJA_END;" + # Root project's on run start / on run end should be first by checking the macros + root_environment_statements = sushi_context._environment_statements[0] + assert "create_tables" in root_environment_statements.jinja_macros.root_macros + + # Validate order of execution to be correct + assert root_environment_statements.before_all == [ + "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);\nJINJA_END;", + "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS to_be_executed_last (col VARCHAR);\nJINJA_END;", ] - assert environment_statements.after_all == [ - "JINJA_STATEMENT_BEGIN;\n{{ create_tables(schemas) }}\nJINJA_END;" + assert root_environment_statements.after_all == [ + "JINJA_STATEMENT_BEGIN;\n{{ create_tables(schemas) }}\nJINJA_END;", + "JINJA_STATEMENT_BEGIN;\nDROP TABLE to_be_executed_last;\nJINJA_END;", ] - assert "create_tables" in environment_statements.jinja_macros.root_macros + + assert root_environment_statements.jinja_macros.root_package_name == "sushi" rendered_before_all = render_statements( - environment_statements.before_all, + root_environment_statements.before_all, dialect=sushi_context.default_dialect, - python_env=environment_statements.python_env, - jinja_macros=environment_statements.jinja_macros, + python_env=root_environment_statements.python_env, + jinja_macros=root_environment_statements.jinja_macros, runtime_stage=RuntimeStage.BEFORE_ALL, ) rendered_after_all = render_statements( - environment_statements.after_all, + root_environment_statements.after_all, dialect=sushi_context.default_dialect, - python_env=environment_statements.python_env, - jinja_macros=environment_statements.jinja_macros, + python_env=root_environment_statements.python_env, + jinja_macros=root_environment_statements.jinja_macros, snapshots=sushi_context.snapshots, runtime_stage=RuntimeStage.AFTER_ALL, environment_naming_info=EnvironmentNamingInfo(name="dev"), ) assert rendered_before_all == [ - "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table TEXT, evaluation_time TEXT)" + "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table TEXT, evaluation_time TEXT)", + "CREATE TABLE IF NOT EXISTS to_be_executed_last (col TEXT)", ] # The jinja macro should have resolved the schemas for this environment and generated corresponding statements @@ -316,5 +324,58 @@ def test_on_run_start_end(copy_to_temp_path): [ "CREATE OR REPLACE TABLE schema_table_snapshots__dev AS SELECT 'snapshots__dev' AS schema", "CREATE OR REPLACE TABLE schema_table_sushi__dev AS SELECT 'sushi__dev' AS schema", + "DROP TABLE to_be_executed_last", + ] + ) + + # Nested dbt_packages on run start / on run end + packaged_environment_statements = sushi_context._environment_statements[1] + + # Validate order of execution to be correct + assert packaged_environment_statements.before_all == [ + "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS to_be_executed_first (col VARCHAR);\nJINJA_END;", + "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS analytic_stats_packaged_project (physical_table VARCHAR, evaluation_time VARCHAR);\nJINJA_END;", + ] + assert packaged_environment_statements.after_all == [ + "JINJA_STATEMENT_BEGIN;\nDROP TABLE to_be_executed_first\nJINJA_END;", + "JINJA_STATEMENT_BEGIN;\n{{ packaged_tables(schemas) }}\nJINJA_END;", + ] + + assert "packaged_tables" in packaged_environment_statements.jinja_macros.root_macros + assert packaged_environment_statements.jinja_macros.root_package_name == "sushi" + + rendered_before_all = render_statements( + packaged_environment_statements.before_all, + dialect=sushi_context.default_dialect, + python_env=packaged_environment_statements.python_env, + jinja_macros=packaged_environment_statements.jinja_macros, + runtime_stage=RuntimeStage.BEFORE_ALL, + ) + + rendered_after_all = render_statements( + packaged_environment_statements.after_all, + dialect=sushi_context.default_dialect, + python_env=packaged_environment_statements.python_env, + jinja_macros=packaged_environment_statements.jinja_macros, + snapshots=sushi_context.snapshots, + runtime_stage=RuntimeStage.AFTER_ALL, + environment_naming_info=EnvironmentNamingInfo(name="dev"), + ) + + # Validate order of execution to match dbt's + assert rendered_before_all == [ + "CREATE TABLE IF NOT EXISTS to_be_executed_first (col TEXT)", + "CREATE TABLE IF NOT EXISTS analytic_stats_packaged_project (physical_table TEXT, evaluation_time TEXT)", + ] + + # This on run end statement should be executed first + assert rendered_after_all[0] == "DROP TABLE to_be_executed_first" + + # The table names is an indication of the rendering of the dbt_packages statements + assert sorted(rendered_after_all) == sorted( + [ + "DROP TABLE to_be_executed_first", + "CREATE OR REPLACE TABLE schema_table_snapshots__dev_nested_package AS SELECT 'snapshots__dev' AS schema", + "CREATE OR REPLACE TABLE schema_table_sushi__dev_nested_package AS SELECT 'sushi__dev' AS schema", ] ) diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 16677ee5a7..b6c1aac01a 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1003,12 +1003,41 @@ def test_dbt_version(sushi_test_project: Project): @pytest.mark.xdist_group("dbt_manifest") def test_dbt_on_run_start_end(sushi_test_project: Project): - context = sushi_test_project.context - assert context._manifest - assert context._manifest._on_run_start == [ - "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);" - ] - assert context._manifest._on_run_end == ["{{ create_tables(schemas) }}"] + # Validate perservation of dbt's order of execution + assert sushi_test_project.packages["sushi"].on_run_start["sushi-on-run-start-0"].index == 0 + assert sushi_test_project.packages["sushi"].on_run_start["sushi-on-run-start-1"].index == 1 + assert sushi_test_project.packages["sushi"].on_run_end["sushi-on-run-end-0"].index == 0 + assert sushi_test_project.packages["sushi"].on_run_end["sushi-on-run-end-1"].index == 1 + assert ( + sushi_test_project.packages["customers"].on_run_start["customers-on-run-start-0"].index == 0 + ) + assert ( + sushi_test_project.packages["customers"].on_run_start["customers-on-run-start-1"].index == 1 + ) + assert sushi_test_project.packages["customers"].on_run_end["customers-on-run-end-0"].index == 0 + assert sushi_test_project.packages["customers"].on_run_end["customers-on-run-end-1"].index == 1 + + assert ( + sushi_test_project.packages["customers"].on_run_start["customers-on-run-start-0"].sql + == "CREATE TABLE IF NOT EXISTS to_be_executed_first (col VARCHAR);" + ) + assert ( + sushi_test_project.packages["customers"].on_run_start["customers-on-run-start-1"].sql + == "CREATE TABLE IF NOT EXISTS analytic_stats_packaged_project (physical_table VARCHAR, evaluation_time VARCHAR);" + ) + assert ( + sushi_test_project.packages["customers"].on_run_end["customers-on-run-end-1"].sql + == "{{ packaged_tables(schemas) }}" + ) + + assert ( + sushi_test_project.packages["sushi"].on_run_start["sushi-on-run-start-0"].sql + == "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);" + ) + assert ( + sushi_test_project.packages["sushi"].on_run_end["sushi-on-run-end-0"].sql + == "{{ create_tables(schemas) }}" + ) @pytest.mark.xdist_group("dbt_manifest") diff --git a/tests/fixtures/dbt/sushi_test/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_project.yml index dd7486821e..d40fabf525 100644 --- a/tests/fixtures/dbt/sushi_test/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/dbt_project.yml @@ -61,5 +61,7 @@ vars: on-run-start: - 'CREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);' + - 'CREATE TABLE IF NOT EXISTS to_be_executed_last (col VARCHAR);' on-run-end: - - '{{ create_tables(schemas) }}' \ No newline at end of file + - '{{ create_tables(schemas) }}' + - 'DROP TABLE to_be_executed_last;' \ No newline at end of file diff --git a/tests/fixtures/dbt/sushi_test/packages/customers/dbt_project.yml b/tests/fixtures/dbt/sushi_test/packages/customers/dbt_project.yml index 7b09f72a45..c7b89da8f0 100644 --- a/tests/fixtures/dbt/sushi_test/packages/customers/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/packages/customers/dbt_project.yml @@ -30,3 +30,11 @@ vars: some_other_var: 5 yet_another_var: 5 'customers:customer_id': "bla" + + +on-run-start: + - 'CREATE TABLE IF NOT EXISTS to_be_executed_first (col VARCHAR);' + - 'CREATE TABLE IF NOT EXISTS analytic_stats_packaged_project (physical_table VARCHAR, evaluation_time VARCHAR);' +on-run-end: + - 'DROP TABLE to_be_executed_first' + - '{{ packaged_tables(schemas) }}' \ No newline at end of file diff --git a/tests/fixtures/dbt/sushi_test/packages/customers/macros/packaged_tables.sql b/tests/fixtures/dbt/sushi_test/packages/customers/macros/packaged_tables.sql new file mode 100644 index 0000000000..51ce04f06d --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/packages/customers/macros/packaged_tables.sql @@ -0,0 +1,5 @@ +{% macro packaged_tables(schemas) %} + {% for schema in schemas %} + create or replace table schema_table_{{schema}}_nested_package as select '{{schema}}' as schema; + {% endfor%} +{% endmacro %} \ No newline at end of file From f8e630441c27fe6296cba0f27ffb42bdebaf126c Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:14:19 +0300 Subject: [PATCH 0053/1056] Chore: bump sqlglot to v26.16.1 (#4239) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4c484c492a..210d2c2575 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.16.0", + "sqlglot[rs]~=26.16.1", "tenacity", "time-machine", "json-stream" From 8db14ddedb42e233d23b2f8a7f8413c50e7d7efd Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:42:07 +0100 Subject: [PATCH 0054/1056] chore(vscode): add banning of shadowing (#4240) --- vscode/extension/eslint.config.mjs | 3 ++- vscode/extension/src/extension.ts | 4 ++-- vscode/extension/src/utilities/common/settings.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/vscode/extension/eslint.config.mjs b/vscode/extension/eslint.config.mjs index d8d2c9985a..b1b7e34e80 100644 --- a/vscode/extension/eslint.config.mjs +++ b/vscode/extension/eslint.config.mjs @@ -23,6 +23,7 @@ export default [{ curly: "error", eqeqeq: "error", "no-throw-literal": "error", - semi: ["error", "never"] + semi: ["error", "never"], + "no-shadow": "error", }, }]; \ No newline at end of file diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index f4e88d5c52..b04370ea6c 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -69,8 +69,8 @@ export async function activate(context: vscode.ExtensionContext) { const restart = async () => { if (lspClient) { traceVerbose("Restarting LSP client") - const result = await lspClient.restart() - if (isErr(result)) { + const restartResult = await lspClient.restart() + if (isErr(restartResult)) { handleNotSginedInError(authProvider) } } diff --git a/vscode/extension/src/utilities/common/settings.ts b/vscode/extension/src/utilities/common/settings.ts index c593bcb603..3f689ce5f8 100644 --- a/vscode/extension/src/utilities/common/settings.ts +++ b/vscode/extension/src/utilities/common/settings.ts @@ -34,8 +34,8 @@ function resolveVariables(value: string[], workspace?: WorkspaceFolder): string[ }) return value.map((s) => { - for (const [key, value] of substitutions) { - s = s.replace(key, value) + for (const [k, v] of substitutions) { + s = s.replace(k, v) } return s }) From 633fe0994e28ccdd1218e4464166a6575d906cc9 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:52:11 +0100 Subject: [PATCH 0055/1056] feat(vscode): add installation progress bar for sqlmesh enterprise (#4230) --- .../src/utilities/sqlmesh/sqlmesh.ts | 136 ++++++++++++++++-- 1 file changed, 122 insertions(+), 14 deletions(-) diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 7b1fd25b83..90879c5c93 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -1,14 +1,15 @@ import path from "path" -import { traceLog, traceVerbose } from "../common/log" +import { traceInfo, traceLog, traceVerbose } from "../common/log" import { getInterpreterDetails } from "../common/python" import { Result, err, isErr, ok } from "../functional/result" import { getProjectRoot } from "../common/utilities" -import { execFile } from "child_process" -import { promisify } from "util" import { isPythonModuleInstalled } from "../python" import fs from "fs" import { ErrorType } from "../errors" import { isSignedIntoTobikoCloud } from "../../auth/auth" +import { execAsync } from "../exec" +import z from "zod" +import { ProgressLocation, window } from "vscode" export type sqlmesh_exec = { workspacePath: string; @@ -48,12 +49,114 @@ export const get_tcloud_bin = async (): Promise> => { return ok(binPath) } +const isSqlmeshInstalledSchema = z.object({ + is_installed: z.boolean(), +}) + +/** + * Returns true if the current project is a sqlmesh enterprise project is installed and updated. + * + * @returns A Result indicating whether sqlmesh enterprise is installed and updated. + */ +export const isSqlmeshEnterpriseInstalled = async (): Promise< + Result +> => { + traceInfo("Checking if sqlmesh enterprise is installed") + const tcloudBin = await get_tcloud_bin() + if (isErr(tcloudBin)) { + return err(tcloudBin.error) + } + const called = await execAsync(tcloudBin.value, ["is_sqlmesh_installed"]) + if (called.exitCode !== 0) { + return err( + `Failed to check if sqlmesh enterprise is installed: ${called.stderr}` + ) + } + const parsed = isSqlmeshInstalledSchema.safeParse(JSON.parse(called.stdout)) + if (!parsed.success) { + return err( + `Failed to parse sqlmesh enterprise installation status: ${parsed.error}` + ) + } + return ok(parsed.data.is_installed) +} + +/** + * Install sqlmesh enterprise. + * + * @returns A Result indicating whether sqlmesh enterprise was installed. + */ +export const installSqlmeshEnterprise = async ( + abortController: AbortController +): Promise> => { + const tcloudBin = await get_tcloud_bin() + if (isErr(tcloudBin)) { + return err(tcloudBin.error) + } + const called = await execAsync(tcloudBin.value, ["install_sqlmesh"], { + signal: abortController.signal, + }) + if (called.exitCode !== 0) { + return err(`Failed to install sqlmesh enterprise: ${called.stderr}`) + } + return ok(true) +} + +/** + * Checks if sqlmesh enterprise is installed and updated. If not, it will install it. + * This will also create a progress message in vscode in order to inform the user that sqlmesh enterprise is being installed. + * + * @returns A Result indicating whether sqlmesh enterprise was installed in the call. + */ +export const ensureSqlmeshEnterpriseInstalled = async (): Promise< + Result +> => { + traceInfo("Ensuring sqlmesh enterprise is installed") + const isInstalled = await isSqlmeshEnterpriseInstalled() + if (isErr(isInstalled)) { + return err(isInstalled.error) + } + if (isInstalled.value) { + traceInfo("Sqlmesh enterprise is installed") + return ok(false) + } + traceInfo("Sqlmesh enterprise is not installed, installing...") + const abortController = new AbortController() + const installResult = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Installing sqlmesh enterprise...", + cancellable: true, + }, + async (progress, token) => { + // Connect the cancellation token to our abort controller + token.onCancellationRequested(() => { + abortController.abort() + traceInfo("Sqlmesh enterprise installation cancelled") + window.showInformationMessage("Installation cancelled") + }) + progress.report({ message: "Installing sqlmesh enterprise..." }) + const result = await installSqlmeshEnterprise(abortController) + if (isErr(result)) { + return result + } + return ok(true) + } + ) + if (isErr(installResult)) { + return installResult + } + return ok(true) +} + /** * Get the sqlmesh executable for the current workspace. * * @returns The sqlmesh executable for the current workspace. */ -export const sqlmesh_exec = async (): Promise> => { +export const sqlmesh_exec = async (): Promise< + Result +> => { const projectRoot = await getProjectRoot() const workspacePath = projectRoot.uri.fsPath const interpreterDetails = await getInterpreterDetails() @@ -72,7 +175,7 @@ export const sqlmesh_exec = async (): Promise> = return err({ type: "generic", message: isTcloudInstalled.error, - }) + }) } if (isTcloudInstalled.value) { const tcloudBin = await get_tcloud_bin() @@ -88,6 +191,13 @@ export const sqlmesh_exec = async (): Promise> = type: "not_signed_in", }) } + const ensured = await ensureSqlmeshEnterpriseInstalled() + if (isErr(ensured)) { + return err({ + type: "generic", + message: ensured.error, + }) + } return ok({ bin: `${tcloudBin.value} sqlmesh`, workspacePath, @@ -164,15 +274,13 @@ export const sqlmesh_lsp_exec = async (): Promise< type: "not_signed_in", }) } - const execFileAsync = promisify(execFile) - await execFileAsync(tcloudBin.value, ["install_sqlmesh"], { - cwd: workspacePath, - env: { - PYTHONPATH: interpreterDetails.path?.[0], - VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), - PATH: interpreterDetails.binPath!, - }, - }) + const ensured = await ensureSqlmeshEnterpriseInstalled() + if (isErr(ensured)) { + return err({ + type: "generic", + message: ensured.error, + }) + } } const binPath = path.join(interpreterDetails.binPath!, "sqlmesh_lsp") traceLog(`Bin path: ${binPath}`) From d67cf251f22bce653d90f4292bd5f70cca50e2fa Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 24 Apr 2025 09:45:29 -0500 Subject: [PATCH 0056/1056] Docs: add Unity Catalog requirement to Databricks docs (#4242) --- docs/integrations/engines/databricks.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/integrations/engines/databricks.md b/docs/integrations/engines/databricks.md index d6a935f163..b4206b22b5 100644 --- a/docs/integrations/engines/databricks.md +++ b/docs/integrations/engines/databricks.md @@ -50,12 +50,17 @@ Before working through this connection quickstart, ensure that: 1. You have a Databricks account with access to an appropriate Databricks Workspace - The Workspace must support authenticating with [personal access tokens](https://docs.databricks.com/en/dev-tools/auth/pat.html) (Databricks [Community Edition workspaces do not](https://docs.databricks.com/en/admin/access-control/tokens.html)) - Your account must have Workspace Access and Create Compute permissions (these permissions are enabled by default) -2. Your computer has [SQLMesh installed](../../installation.md) with the [Databricks extra available](../../installation.md#install-extras) +2. Your Databricks compute resources have [Unity Catalog](https://docs.databricks.com/aws/en/data-governance/unity-catalog/) activated +3. Your computer has [SQLMesh installed](../../installation.md) with the [Databricks extra available](../../installation.md#install-extras) - Install from the command line with the command `pip install "sqlmesh[databricks]"` -3. You have initialized a [SQLMesh example project](../../quickstart/cli#1-create-the-sqlmesh-project) on your computer +4. You have initialized a [SQLMesh example project](../../quickstart/cli#1-create-the-sqlmesh-project) on your computer - Open a command line interface and navigate to the directory where the project files should go - Initialize the project with the command `sqlmesh init duckdb` +!!! important "Unity Catalog required" + + Databricks compute resources used by SQLMesh must have [Unity Catalog](https://docs.databricks.com/aws/en/data-governance/unity-catalog/) activated. + ### Get connection info The first step to configuring a Databricks connection is gathering the necessary information from your Databricks compute instance. From 94f2fa85036f876e85ddc942f2c29739eeb437fa Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 24 Apr 2025 19:56:08 +0100 Subject: [PATCH 0057/1056] fix(vscode): fix building of extension (#4244) --- vscode/extension/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode/extension/package.json b/vscode/extension/package.json index b8ebb7d409..35a1f24842 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -72,7 +72,7 @@ "check-types": "tsc --noEmit", "watch": "node esbuild.js --watch", "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", - "vscode:package": "vsce package", + "vscode:package": "vsce package --no-dependencies", "vscode:prepublish": "cp ../../LICENSE . && npm run package", "package": "npm run check-types && node esbuild.js --production" }, From 6365efb0e286a9a1f49d7a3b9ed03af1cdb80076 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 24 Apr 2025 14:28:33 -0700 Subject: [PATCH 0058/1056] Chore: Upgrade sqlglot to 26.16.2 (#4246) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 210d2c2575..36cfe53bcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.16.1", + "sqlglot[rs]~=26.16.2", "tenacity", "time-machine", "json-stream" From af974f8b207d52334bebebdbeed104f86e4efec5 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 24 Apr 2025 15:28:04 -0700 Subject: [PATCH 0059/1056] Fix: Create snapshot records before creating physical tables (#4245) --- sqlmesh/core/plan/evaluator.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 907f391589..356811cbb6 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -255,6 +255,11 @@ def _push( plan: The plan to source snapshots from. deployability_index: Indicates which snapshots are deployable in the context of this creation. """ + self.state_sync.push_snapshots(plan.new_snapshots) + analytics.collector.on_snapshots_created( + new_snapshots=plan.new_snapshots, plan_id=plan.plan_id + ) + promoted_snapshot_ids = ( set(plan.environment.promoted_snapshot_ids) if plan.environment.promoted_snapshot_ids is not None @@ -300,11 +305,6 @@ def _should_create(s: Snapshot) -> bool: success=completion_status is not None and completion_status.is_success ) - self.state_sync.push_snapshots(plan.new_snapshots) - - analytics.collector.on_snapshots_created( - new_snapshots=plan.new_snapshots, plan_id=plan.plan_id - ) return completion_status def _promote( From 3c1a44a5d0a40bd3ab41adcc8b609386ee8b0e9d Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 25 Apr 2025 18:21:36 +1200 Subject: [PATCH 0060/1056] Fix!: Change how `partitioned_by` is parsed so that partition expressions with specialized AST nodes are captured (#4224) --- sqlmesh/core/dialect.py | 6 + sqlmesh/core/model/meta.py | 19 ++- .../migrations/v0081_update_partitioned_by.py | 91 +++++++++++++ tests/core/engine_adapter/test_athena.py | 48 +++++++ tests/core/test_context.py | 9 +- tests/core/test_model.py | 128 ++++++++++++++++++ tests/core/test_snapshot.py | 34 +++++ 7 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 sqlmesh/migrations/v0081_update_partitioned_by.py diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 4bd8e86564..bd58f94db0 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -610,6 +610,12 @@ def parse(self: Parser) -> t.Optional[exp.Expression]: value = self.expression(ModelKind, this=kind.value, expressions=props) elif key == "expression": value = self._parse_conjunction() + elif key == "partitioned_by": + partitioned_by = self._parse_partitioned_by() + if isinstance(partitioned_by.this, exp.Schema): + value = exp.tuple_(*partitioned_by.this.expressions) + else: + value = partitioned_by.this else: value = self._parse_bracket(self._parse_field(any_token=True)) diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 514a83cb3a..29c82bc33f 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -5,7 +5,7 @@ from typing_extensions import Self from pydantic import Field -from sqlglot import Dialect, exp +from sqlglot import Dialect, exp, parse_one from sqlglot.helper import ensure_collection, ensure_list from sqlglot.optimizer.normalize_identifiers import normalize_identifiers @@ -39,6 +39,7 @@ field_validator, list_of_fields_validator, model_validator, + get_dialect, ) if t.TYPE_CHECKING: @@ -182,6 +183,22 @@ def _gateway_validator(cls, v: t.Any) -> t.Optional[str]: def _partition_and_cluster_validator( cls, v: t.Any, info: ValidationInfo ) -> t.List[exp.Expression]: + if ( + isinstance(v, list) + and all(isinstance(i, str) for i in v) + and info.field_name == "partitioned_by_" + ): + # this branch gets hit when we are deserializing from json because `partitioned_by` is stored as a List[str] + # however, we should only invoke this if the list contains strings because this validator is also + # called by Python models which might pass a List[exp.Expression] + string_to_parse = ( + f"({','.join(v)})" # recreate the (a, b, c) part of "partitioned_by (a, b, c)" + ) + parsed = parse_one( + string_to_parse, into=exp.PartitionedByProperty, dialect=get_dialect(info) + ) + v = parsed.this.expressions if isinstance(parsed.this, exp.Schema) else v + expressions = list_of_fields_validator(v, info.data) for expression in expressions: diff --git a/sqlmesh/migrations/v0081_update_partitioned_by.py b/sqlmesh/migrations/v0081_update_partitioned_by.py new file mode 100644 index 0000000000..a88f8c3810 --- /dev/null +++ b/sqlmesh/migrations/v0081_update_partitioned_by.py @@ -0,0 +1,91 @@ +"""Remove superfluous exp.Paren references from partitioned_by""" + +import json + +import pandas as pd +from sqlglot import exp + +from sqlmesh.utils.migration import index_text_type +from sqlmesh.utils.migration import blob_text_type + + +def migrate(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + index_type = index_text_type(engine_adapter.dialect) + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + new_snapshots = [] + updated = False + + for ( + name, + identifier, + version, + snapshot, + kind_name, + updated_ts, + unpaused_ts, + ttl_ms, + unrestorable, + ) in engine_adapter.fetchall( + exp.select( + "name", + "identifier", + "version", + "snapshot", + "kind_name", + "updated_ts", + "unpaused_ts", + "ttl_ms", + "unrestorable", + ).from_(snapshots_table), + quote_identifiers=True, + ): + parsed_snapshot = json.loads(snapshot) + + if partitioned_by := parsed_snapshot["node"].get("partitioned_by"): + new_partitioned_by = [] + for item in partitioned_by: + # rewrite '(foo)' to 'foo' + if item.startswith("(") and item.endswith(")"): + item = item[1:-1] + updated = True + new_partitioned_by.append(item) + parsed_snapshot["node"]["partitioned_by"] = new_partitioned_by + + new_snapshots.append( + { + "name": name, + "identifier": identifier, + "version": version, + "snapshot": json.dumps(parsed_snapshot), + "kind_name": kind_name, + "updated_ts": updated_ts, + "unpaused_ts": unpaused_ts, + "ttl_ms": ttl_ms, + "unrestorable": unrestorable, + } + ) + + if new_snapshots and updated: + engine_adapter.delete_from(snapshots_table, "TRUE") + blob_type = blob_text_type(engine_adapter.dialect) + + engine_adapter.insert_append( + snapshots_table, + pd.DataFrame(new_snapshots), + columns_to_types={ + "name": exp.DataType.build(index_type), + "identifier": exp.DataType.build(index_type), + "version": exp.DataType.build(index_type), + "snapshot": exp.DataType.build(blob_type), + "kind_name": exp.DataType.build(index_type), + "updated_ts": exp.DataType.build("bigint"), + "unpaused_ts": exp.DataType.build("bigint"), + "ttl_ms": exp.DataType.build("bigint"), + "unrestorable": exp.DataType.build("boolean"), + }, + ) diff --git a/tests/core/engine_adapter/test_athena.py b/tests/core/engine_adapter/test_athena.py index 46aabc7426..f96f5656a2 100644 --- a/tests/core/engine_adapter/test_athena.py +++ b/tests/core/engine_adapter/test_athena.py @@ -435,3 +435,51 @@ def test_drop_partitions_from_metastore_uses_batches( # third call 50-62 assert calls[2][1]["PartitionsToDelete"][0]["Values"][0] == "50" assert calls[2][1]["PartitionsToDelete"][-1]["Values"][0] == "62" + + +def test_iceberg_partition_transforms(adapter: AthenaEngineAdapter): + expressions = d.parse( + """ + MODEL ( + name test_table, + kind FULL, + table_format iceberg, + partitioned_by (month(business_date), bucket(4, colb), colc) + ); + + SELECT 1::timestamp AS business_date, 2::varchar as colb, 'foo' as colc; + """ + ) + model: SqlModel = t.cast(SqlModel, load_sql_based_model(expressions)) + + assert model.partitioned_by == [ + exp.Month(this=exp.column("business_date", quoted=True)), + exp.PartitionedByBucket( + this=exp.column("colb", quoted=True), expression=exp.Literal.number(4) + ), + exp.column("colc", quoted=True), + ] + + adapter.s3_warehouse_location = "s3://bucket/prefix/" + + adapter.create_table( + table_name=model.name, + columns_to_types=model.columns_to_types_or_raise, + partitioned_by=model.partitioned_by, + table_format=model.table_format, + ) + + adapter.ctas( + table_name=model.name, + columns_to_types=model.columns_to_types_or_raise, + partitioned_by=model.partitioned_by, + query_or_df=model.ctas_query(), + table_format=model.table_format, + ) + + assert to_sql_calls(adapter) == [ + # Hive syntax - create table + """CREATE TABLE IF NOT EXISTS `test_table` (`business_date` TIMESTAMP, `colb` STRING, `colc` STRING) PARTITIONED BY (MONTH(`business_date`), BUCKET(4, `colb`), `colc`) LOCATION 's3://bucket/prefix/test_table/' TBLPROPERTIES ('table_type'='iceberg')""", + # Trino syntax - CTAS + """CREATE TABLE IF NOT EXISTS "test_table" WITH (table_type='iceberg', partitioning=ARRAY['MONTH(business_date)', 'BUCKET(colb, 4)', 'colc'], location='s3://bucket/prefix/test_table/', is_external=false) AS SELECT CAST("business_date" AS TIMESTAMP) AS "business_date", CAST("colb" AS VARCHAR) AS "colb", CAST("colc" AS VARCHAR) AS "colc" FROM (SELECT CAST(1 AS TIMESTAMP) AS "business_date", CAST(2 AS VARCHAR) AS "colb", 'foo' AS "colc" LIMIT 0) AS "_subquery\"""", + ] diff --git a/tests/core/test_context.py b/tests/core/test_context.py index eb68ba8db3..cde653b1fd 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1978,19 +1978,22 @@ def test_plan_audit_intervals(tmp_path: pathlib.Path, capsys, caplog): ) ) - ctx.plan( + plan = ctx.plan( environment="dev", auto_apply=True, no_prompts=True, start="2025-02-01", end="2025-02-01" ) + date_snapshot = next(s for s in plan.new_snapshots if "date_example" in s.name) + timestamp_snapshot = next(s for s in plan.new_snapshots if "timestamp_example" in s.name) + # Case 1: The timestamp audit should be in the inclusive range ['2025-02-01 00:00:00', '2025-02-01 23:59:59.999999'] assert ( - """SELECT COUNT(*) FROM (SELECT ("timestamp_id") AS "timestamp_id" FROM (SELECT * FROM "sqlmesh__sqlmesh_audit"."sqlmesh_audit__timestamp_example__2797548448" AS "sqlmesh_audit__timestamp_example__2797548448" WHERE "timestamp_id" BETWEEN CAST('2025-02-01 00:00:00' AS TIMESTAMP) AND CAST('2025-02-01 23:59:59.999999' AS TIMESTAMP)) AS "_q_0" WHERE TRUE GROUP BY ("timestamp_id") HAVING COUNT(*) > 1) AS "audit\"""" + f"""SELECT COUNT(*) FROM (SELECT ("timestamp_id") AS "timestamp_id" FROM (SELECT * FROM "sqlmesh__sqlmesh_audit"."sqlmesh_audit__timestamp_example__{timestamp_snapshot.version}" AS "sqlmesh_audit__timestamp_example__{timestamp_snapshot.version}" WHERE "timestamp_id" BETWEEN CAST('2025-02-01 00:00:00' AS TIMESTAMP) AND CAST('2025-02-01 23:59:59.999999' AS TIMESTAMP)) AS "_q_0" WHERE TRUE GROUP BY ("timestamp_id") HAVING COUNT(*) > 1) AS "audit\"""" in caplog.text ) # Case 2: The date audit should be in the inclusive range ['2025-02-01', '2025-02-01'] assert ( - """SELECT COUNT(*) FROM (SELECT ("date_id") AS "date_id" FROM (SELECT * FROM "sqlmesh__sqlmesh_audit"."sqlmesh_audit__date_example__4100277424" AS "sqlmesh_audit__date_example__4100277424" WHERE "date_id" BETWEEN CAST('2025-02-01' AS DATE) AND CAST('2025-02-01' AS DATE)) AS "_q_0" WHERE TRUE GROUP BY ("date_id") HAVING COUNT(*) > 1) AS "audit\"""" + f"""SELECT COUNT(*) FROM (SELECT ("date_id") AS "date_id" FROM (SELECT * FROM "sqlmesh__sqlmesh_audit"."sqlmesh_audit__date_example__{date_snapshot.version}" AS "sqlmesh_audit__date_example__{date_snapshot.version}" WHERE "date_id" BETWEEN CAST('2025-02-01' AS DATE) AND CAST('2025-02-01' AS DATE)) AS "_q_0" WHERE TRUE GROUP BY ("date_id") HAVING COUNT(*) > 1) AS "audit\"""" in caplog.text ) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 825b39ea90..fe9e47873c 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -1514,6 +1514,134 @@ def test_render_definition_with_defaults(): ) == d.format_model_expressions(expected_expressions) +def test_render_definition_partitioned_by(): + # no parenthesis in definition, no parenthesis when rendered + model = load_sql_based_model( + d.parse( + f""" + MODEL ( + name db.table, + kind FULL, + partitioned_by a + ); + + select 1 as a; + """ + ) + ) + + assert model.partitioned_by == [exp.column("a", quoted=True)] + assert ( + model.render_definition()[0].sql(pretty=True) + == """MODEL ( + name db.table, + kind FULL, + partitioned_by "a" +)""" + ) + + # single column wrapped in parenthesis in defintion, no parenthesis in rendered + model = load_sql_based_model( + d.parse( + f""" + MODEL ( + name db.table, + kind FULL, + partitioned_by (a) + ); + + select 1 as a; + """ + ) + ) + + assert model.partitioned_by == [exp.column("a", quoted=True)] + assert ( + model.render_definition()[0].sql(pretty=True) + == """MODEL ( + name db.table, + kind FULL, + partitioned_by "a" +)""" + ) + + # multiple columns wrapped in parenthesis in definition, parenthesis in rendered + model = load_sql_based_model( + d.parse( + f""" + MODEL ( + name db.table, + kind FULL, + partitioned_by (a, b) + ); + + select 1 as a, 2 as b; + """ + ) + ) + + assert model.partitioned_by == [exp.column("a", quoted=True), exp.column("b", quoted=True)] + assert ( + model.render_definition()[0].sql(pretty=True) + == """MODEL ( + name db.table, + kind FULL, + partitioned_by ("a", "b") +)""" + ) + + # multiple columns not wrapped in parenthesis in the definition is an error + with pytest.raises(ParseError, match=r"keyword: 'value' missing"): + load_sql_based_model( + d.parse( + f""" + MODEL ( + name db.table, + kind FULL, + partitioned_by a, b + ); + + select 1 as a, 2 as b; + """ + ) + ) + + # Iceberg transforms / functions + model = load_sql_based_model( + d.parse( + f""" + MODEL ( + name db.table, + kind FULL, + partitioned_by (day(a), truncate(b, 4), bucket(c, 3)) + ); + + select 1 as a, 2 as b, 3 as c; + """ + ), + dialect="trino", + ) + + assert model.partitioned_by == [ + exp.Day(this=exp.column("a", quoted=True)), + exp.PartitionByTruncate( + this=exp.column("b", quoted=True), expression=exp.Literal.number(4) + ), + exp.PartitionedByBucket( + this=exp.column("c", quoted=True), expression=exp.Literal.number(3) + ), + ] + assert ( + model.render_definition()[0].sql(pretty=True) + == """MODEL ( + name db.table, + dialect trino, + kind FULL, + partitioned_by (DAY("a"), TRUNCATE("b", 4), BUCKET("c", 3)) +)""" + ) + + def test_cron(): daily = _Node(name="x", cron="@daily") assert to_datetime(daily.cron_prev("2020-01-01")) == to_datetime("2019-12-31") diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index f27ad8f3c1..387c799e95 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -2938,3 +2938,37 @@ def check_types(batch, env: str, sql: list[SQL], table: exp.Table, default: int ) snapshot_a = make_snapshot(sql_model) assert snapshot_a.check_ready_intervals([(0, 1)], mocker.Mock()) == [(0, 1)] + + +def test_partitioned_by_roundtrip(make_snapshot: t.Callable): + sql_model = load_sql_based_model( + parse(""" + MODEL ( + name test_schema.test_model, + kind full, + partitioned_by (a, bucket(4, b), truncate(3, c), month(d)) + ); + SELECT a, b, c, d FROM tbl; + """) + ) + snapshot = make_snapshot(sql_model) + assert isinstance(snapshot, Snapshot) + assert isinstance(snapshot.node, SqlModel) + + assert snapshot.node.partitioned_by == [ + exp.column("a", quoted=True), + exp.PartitionedByBucket( + this=exp.column("b", quoted=True), expression=exp.Literal.number(4) + ), + exp.PartitionByTruncate( + this=exp.column("c", quoted=True), expression=exp.Literal.number(3) + ), + exp.Month(this=exp.column("d", quoted=True)), + ] + + # roundtrip through json and ensure we get correct AST nodes on the other end + serialized = snapshot.json() + deserialized = snapshot.parse_raw(serialized) + + assert isinstance(deserialized.node, SqlModel) + assert deserialized.node.partitioned_by == snapshot.node.partitioned_by From a5d25efaa3158f60b5d3a33306a9123e9e915dd3 Mon Sep 17 00:00:00 2001 From: Toby Mao Date: Fri, 25 Apr 2025 03:45:49 -0700 Subject: [PATCH 0061/1056] feat: add context to linter rules (#4247) --- examples/sushi/linter/user.py | 3 +++ sqlmesh/core/context.py | 2 +- sqlmesh/core/linter/definition.py | 13 ++++++++----- sqlmesh/core/linter/rule.py | 7 +++++++ 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/examples/sushi/linter/user.py b/examples/sushi/linter/user.py index a4a33c0efc..3f83c76e3a 100644 --- a/examples/sushi/linter/user.py +++ b/examples/sushi/linter/user.py @@ -2,6 +2,7 @@ import typing as t +from sqlmesh.core.context import Context from sqlmesh.core.linter.rule import Rule, RuleViolation from sqlmesh.core.model import Model @@ -10,4 +11,6 @@ class NoMissingOwner(Rule): """All models should have an owner specified.""" def check_model(self, model: Model) -> t.Optional[RuleViolation]: + assert isinstance(self.context, Context) + assert len(self.context.models) > 10 return self.violation() if not model.owner else None diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 634db636ec..4a8f99add1 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -2537,7 +2537,7 @@ def lint_models( # Linter may be `None` if the context is not loaded yet if linter := self._linters.get(model.project): lint_violation, violations = ( - linter.lint_model(model, console=self.console) or found_error + linter.lint_model(model, self, console=self.console) or found_error ) if lint_violation: found_error = True diff --git a/sqlmesh/core/linter/definition.py b/sqlmesh/core/linter/definition.py index f3bc3c89e0..a8ebdec0e8 100644 --- a/sqlmesh/core/linter/definition.py +++ b/sqlmesh/core/linter/definition.py @@ -11,6 +11,9 @@ from sqlmesh.core.linter.rule import Rule, RuleViolation from sqlmesh.core.console import LinterConsole, get_console +if t.TYPE_CHECKING: + from sqlmesh.core.context import GenericContext + def select_rules(all_rules: RuleSet, rule_names: t.Set[str]) -> RuleSet: if "all" in rule_names: @@ -52,7 +55,7 @@ def from_rules(cls, all_rules: RuleSet, config: LinterConfig) -> Linter: return Linter(config.enabled, all_rules, rules, warn_rules) def lint_model( - self, model: Model, console: LinterConsole = get_console() + self, model: Model, context: GenericContext, console: LinterConsole = get_console() ) -> t.Tuple[bool, t.List[AnnotatedRuleViolation]]: if not self.enabled: return False, [] @@ -62,8 +65,8 @@ def lint_model( rules = self.rules.difference(ignored_rules) warn_rules = self.warn_rules.difference(ignored_rules) - error_violations = rules.check_model(model) - warn_violations = warn_rules.check_model(model) + error_violations = rules.check_model(model, context) + warn_violations = warn_rules.check_model(model, context) all_violations: t.List[AnnotatedRuleViolation] = [ AnnotatedRuleViolation( @@ -96,11 +99,11 @@ class RuleSet(Mapping[str, type[Rule]]): def __init__(self, rules: Iterable[type[Rule]] = ()) -> None: self._underlying = {rule.name: rule for rule in rules} - def check_model(self, model: Model) -> t.List[RuleViolation]: + def check_model(self, model: Model, context: GenericContext) -> t.List[RuleViolation]: violations = [] for rule in self._underlying.values(): - violation = rule().check_model(model) + violation = rule(context).check_model(model) if violation: violations.append(violation) diff --git a/sqlmesh/core/linter/rule.py b/sqlmesh/core/linter/rule.py index ceee996641..4105e27d4c 100644 --- a/sqlmesh/core/linter/rule.py +++ b/sqlmesh/core/linter/rule.py @@ -9,6 +9,10 @@ import typing as t +if t.TYPE_CHECKING: + from sqlmesh.core.context import GenericContext + + class _Rule(abc.ABCMeta): def __new__(cls: Type[_Rule], clsname: str, bases: t.Tuple, attrs: t.Dict) -> _Rule: attrs["name"] = clsname.lower() @@ -20,6 +24,9 @@ class Rule(abc.ABC, metaclass=_Rule): name = "rule" + def __init__(self, context: GenericContext): + self.context = context + @abc.abstractmethod def check_model(self, model: Model) -> t.Optional[RuleViolation]: """The evaluation function that'll check for a violation of this rule.""" From da8e09118147bc162961f7e3be85db1ce94af813 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:08:39 +0300 Subject: [PATCH 0062/1056] Chore: document the state_connection kwarg of sqlmesh_config (dbt adapter) (#4253) --- docs/integrations/dbt.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/integrations/dbt.md b/docs/integrations/dbt.md index 50f0bb6c4b..e46e2fef39 100644 --- a/docs/integrations/dbt.md +++ b/docs/integrations/dbt.md @@ -57,9 +57,15 @@ Models **require** a start date for backfilling data through use of the `start` ### Configuration -SQLMesh determines a project's configuration settings from its dbt configuration files. +SQLMesh derives a project's configuration from its dbt configuration files. This section outlines additional settings specific to SQLMesh that can be defined. -This section describes using runtime variables to create multiple configurations and how to disable SQLMesh's automatic model description and comment registration. +#### Selecting a different state connection + +[Certain engines](https://sqlmesh.readthedocs.io/en/stable/guides/configuration/?h=unsupported#state-connection), like Trino, cannot be used to store SQLMesh's state. + +As a workaround, we recommend specifying a supported state engine using the `state_connection` argument instead. + +Learn more about how to configure state connections in Python [here](https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#state-connection). #### Runtime vars From 4041b4288a579418549d3bad3eb6b397274fd1e6 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 25 Apr 2025 11:59:05 -0700 Subject: [PATCH 0063/1056] fix: wait for modules before creating ui routes (#4255) --- web/client/src/App.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/web/client/src/App.tsx b/web/client/src/App.tsx index 2883dc550f..adc6b6f8fd 100644 --- a/web/client/src/App.tsx +++ b/web/client/src/App.tsx @@ -9,7 +9,8 @@ import { EnumErrorKey, useNotificationCenter, } from './library/pages/root/context/notificationCenter' -import { isNotNil } from './utils' +import { isArrayNotEmpty, isNotNil } from './utils' +import NotFound from './library/pages/root/NotFound' const IS_HEADLESS: boolean = Boolean((window as any).__IS_HEADLESS__ ?? false) const Header: Optional JSX.Element>> = @@ -50,14 +51,19 @@ export default function App(): JSX.Element { )}
- {isFetching && ( + {isFetching ? ( Building Modules... + ) : ( + Loading Page...}> + {isArrayNotEmpty(modules.list) ? ( + + ) : ( + + )} + )} - Loading Page...}> - -
{isNotNil(Footer) && ( <> From c1ae64f6e13dee5ebf340ee2fee23e11f4acd01b Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 25 Apr 2025 13:41:37 -0700 Subject: [PATCH 0064/1056] Fix: Expand the parent's model query during query rendering if the parent is not categorized (#4254) --- sqlmesh/core/context.py | 39 +++++++++++++++++++++++------ sqlmesh/core/engine_adapter/base.py | 1 + sqlmesh/core/snapshot/definition.py | 5 ++++ tests/core/test_context.py | 30 ++-------------------- tests/core/test_integration.py | 19 ++++++++++++++ 5 files changed, 58 insertions(+), 36 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 4a8f99add1..c0b7511849 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -901,13 +901,7 @@ def get_snapshot( """ if isinstance(node_or_snapshot, Snapshot): return node_or_snapshot - if isinstance(node_or_snapshot, str) and not self.standalone_audits.get(node_or_snapshot): - node_or_snapshot = normalize_model_name( - node_or_snapshot, - dialect=self.default_dialect, - default_catalog=self.default_catalog, - ) - fqn = node_or_snapshot if isinstance(node_or_snapshot, str) else node_or_snapshot.fqn + fqn = self._node_or_snapshot_to_fqn(node_or_snapshot) snapshot = self.snapshots.get(fqn) if raise_if_missing and not snapshot: @@ -1052,7 +1046,22 @@ def evaluate( execution_time: The date/time time reference to use for execution time. limit: A limit applied to the model. """ - snapshot = self.get_snapshot(model_or_snapshot, raise_if_missing=True) + snapshots = self.snapshots + fqn = self._node_or_snapshot_to_fqn(model_or_snapshot) + if fqn not in snapshots: + raise SQLMeshError(f"Cannot find snapshot for '{fqn}'") + snapshot = snapshots[fqn] + + # Expand all uncategorized parents since physical tables don't exist for them yet + expand = [ + parent + for parent in self.dag.upstream(snapshot.model.fqn) + if (parent_snapshot := snapshots.get(parent)) + and parent_snapshot.is_model + and parent_snapshot.model.is_sql + and not parent_snapshot.categorized + ] + df = self.snapshot_evaluator.evaluate_and_fetch( snapshot, start=start, @@ -1060,6 +1069,7 @@ def evaluate( execution_time=execution_time, snapshots=self.snapshots, limit=limit or c.DEFAULT_MAX_LIMIT, + expand=expand, ) if df is None: @@ -2448,6 +2458,19 @@ def _nodes_to_snapshots(self, nodes: t.Dict[str, Node]) -> t.Dict[str, Snapshot] snapshots[snapshot.name] = snapshot return snapshots + def _node_or_snapshot_to_fqn(self, node_or_snapshot: NodeOrSnapshot) -> str: + if isinstance(node_or_snapshot, Snapshot): + return node_or_snapshot.name + if isinstance(node_or_snapshot, str) and not self.standalone_audits.get(node_or_snapshot): + return normalize_model_name( + node_or_snapshot, + dialect=self.default_dialect, + default_catalog=self.default_catalog, + ) + if not isinstance(node_or_snapshot, str): + return node_or_snapshot.fqn + return node_or_snapshot + @property def _plan_preview_enabled(self) -> bool: if self.config.plan.enable_preview is not None: diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 7596973ffe..aa28066a84 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -152,6 +152,7 @@ def with_log_level(self, level: int) -> EngineAdapter: register_comments=self._register_comments, null_connection=True, multithreaded=self._multithreaded, + pretty_sql=self._pretty_sql, **self._extra_config, ) diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 2bbb806e50..22eed6f3c8 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1014,6 +1014,11 @@ def categorize_as(self, category: SnapshotChangeCategory) -> None: self.change_category = category + @property + def categorized(self) -> bool: + """Whether the snapshot has been categorized.""" + return self.change_category is not None and self.version is not None + def table_name(self, is_deployable: bool = True) -> str: """Full table name pointing to the materialized location of the snapshot. diff --git a/tests/core/test_context.py b/tests/core/test_context.py index cde653b1fd..fa6eaddb4f 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1409,7 +1409,6 @@ def test_environment_statements(tmp_path: pathlib.Path): ], after_all=[ "@grant_schema_usage()", - "@grant_select_privileges()", "@grant_usage_role(@schemas, 'admin')", ], ) @@ -1429,29 +1428,6 @@ def test_environment_statements(tmp_path: pathlib.Path): expression, ) - create_temp_file( - tmp_path, - pathlib.Path(macros_dir, "grant_select_file.py"), - """ -from sqlmesh.core.macros import macro -from sqlmesh.core.snapshot.definition import to_view_mapping - -@macro() -def grant_select_privileges(evaluator): - if evaluator._environment_naming_info and evaluator.runtime_stage == 'before_all': - mapping = to_view_mapping( - evaluator._snapshots.values(), evaluator._environment_naming_info - ) - return [ - stmt - for stmt in [ - f"GRANT SELECT ON VIEW {view_name} TO ROLE admin_role;" - for view_name in mapping.values() - ] - ] -""", - ) - create_temp_file( tmp_path, pathlib.Path(macros_dir, "grant_schema_file.py"), @@ -1499,8 +1475,8 @@ def grant_usage_role(evaluator, schemas, role): after_all = environment_statements.after_all python_env = environment_statements.python_env - assert isinstance(python_env["to_view_mapping"], Executable) - assert isinstance(python_env["grant_select_privileges"], Executable) + assert isinstance(python_env["grant_schema_usage"], Executable) + assert isinstance(python_env["grant_usage_role"], Executable) before_all_rendered = render_statements( statements=before_all, @@ -1524,7 +1500,6 @@ def grant_usage_role(evaluator, schemas, role): assert after_all_rendered == [ "GRANT USAGE ON SCHEMA db TO user_role", - "GRANT SELECT ON VIEW memory.db.test_after_model TO ROLE admin_role", 'GRANT USAGE ON SCHEMA "db" TO "admin"', ] @@ -1539,7 +1514,6 @@ def grant_usage_role(evaluator, schemas, role): assert after_all_rendered_dev == [ "GRANT USAGE ON SCHEMA db__dev TO user_role", - "GRANT SELECT ON VIEW memory.db__dev.test_after_model TO ROLE admin_role", 'GRANT USAGE ON SCHEMA "db__dev" TO "admin"', ] diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index b52889c850..5bbfe30929 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -4075,6 +4075,25 @@ def test_no_backfill_for_model_downstream_of_metadata_change(init_and_plan_conte ) +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_evaluate_uncategorized_snapshot(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Add a new projection + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + # Downstream model references the new projection + downstream_model = context.get_model("sushi.top_waiters") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, downstream_model), literal=False)) + + df = context.evaluate( + "sushi.top_waiters", start="2023-01-05", end="2023-01-06", execution_time=now() + ) + assert set(df["one"].tolist()) == {1} + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_dbt_requirements(sushi_dbt_context: Context): assert set(sushi_dbt_context.requirements) == {"dbt-core", "dbt-duckdb"} From 65ed5796792e67d2a640ce254c967076ae5f1bdc Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Sat, 26 Apr 2025 02:32:18 +0300 Subject: [PATCH 0065/1056] Fix: 'duplicate definitions' python env bug due to module import order (#4249) --- sqlmesh/utils/metaprogramming.py | 22 +++++- tests/core/test_model.py | 126 +++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/sqlmesh/utils/metaprogramming.py b/sqlmesh/utils/metaprogramming.py index 8374230797..b383899262 100644 --- a/sqlmesh/utils/metaprogramming.py +++ b/sqlmesh/utils/metaprogramming.py @@ -61,6 +61,16 @@ def _code_globals(code: types.CodeType) -> t.Dict[str, None]: return variables +def _globals_match(obj1: t.Any, obj2: t.Any) -> bool: + return type(obj1) == type(obj2) and ( + obj1 == obj2 + or ( + getattr(obj1, "__module__", None) == getattr(obj2, "__module__", None) + and getattr(obj1, "__name__", None) == getattr(obj2, "__name__", None) + ) + ) + + def func_globals(func: t.Callable) -> t.Dict[str, t.Any]: """Finds all global references and closures in a function and nested functions. @@ -287,9 +297,17 @@ def build_env( def walk(obj: t.Any, name: str, is_metadata: t.Optional[bool] = None) -> None: obj_module = inspect.getmodule(obj) - if name in visited or (obj_module and obj_module.__name__ == "builtins"): + if obj_module and obj_module.__name__ == "builtins": return + if name in visited: + if name not in env or _globals_match(env[name][0], obj): + return + + raise SQLMeshError( + f"Cannot store {obj} in environment, duplicate definitions found for '{name}'" + ) + visited.add(name) name_missing_from_env = name not in env @@ -320,7 +338,7 @@ def walk(obj: t.Any, name: str, is_metadata: t.Optional[bool] = None) -> None: ): env[name] = (obj, is_metadata) return - elif env[name][0] != obj: + elif not _globals_match(env[name][0], obj): raise SQLMeshError( f"Cannot store {obj} in environment, duplicate definitions found for '{name}'" ) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index fe9e47873c..647ae8d7f7 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9235,3 +9235,129 @@ def test_runtime_stage(evaluator): model = load_sql_based_model(expressions, jinja_macros=jinja_macros) assert model.render_query().sql() == "SELECT 'loading' AS a, 'loading_bla' AS b" assert set(model.python_env) == {"noop", "test_runtime_stage"} + + +def test_python_env_references_are_unequal_but_point_to_same_definition(tmp_path: Path) -> None: + # This tests for regressions against an edge case bug which was due to reloading modules + # in sqlmesh.utils.metaprogramming.import_python_file. Depending on the module loading + # order, we could get a "duplicate symbol in python env" error, even though the references + # essentially pointed to the same definition (e.g. function or class). + init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + + db_path = str(tmp_path / "db.db") + db_connection = DuckDBConnectionConfig(database=db_path) + config = Config( + gateways={"duckdb": GatewayConfig(connection=db_connection)}, + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + ) + + file_a = tmp_path / "macros" / "a.py" + file_b = tmp_path / "macros" / "b.py" + file_c = tmp_path / "macros" / "c.py" + + file_a.write_text( + """from macros.c import target + +def f1(): + target() +""" + ) + file_b.write_text( + """from sqlmesh import macro + +from macros.a import f1 +from macros.c import target + +@macro() +def first_macro(evaluator): + f1() + +@macro() +def second_macro(evaluator): + target() +""" + ) + file_c.write_text( + """def target(): + pass +""" + ) + + model_file = tmp_path / "models" / "model.sql" + model_file.write_text("MODEL (name a); @first_macro(); @second_macro(); SELECT 1 AS c") + + ctx = Context(paths=tmp_path, config=config, load=False) + loader = ctx._loaders[0] + + original_glob_paths = loader._glob_paths + + def _patched_glob_paths(path, *args, **kwargs): + if path == tmp_path / "macros": + yield from [file_a, file_c, file_b] + else: + yield from original_glob_paths(path, *args, **kwargs) + + # We force the import order to be a.py -> c.py -> b.py: + # + # 1. a.py is loaded, so "macros", "macros.a" and "macros.c" are loaded in sys.modules + # 2. c.py is loaded, so "macros" and "macros.c" are reloaded in sys.modules + # 3. b.py is loaded, so "macros" is reloaded and "macros.b" is loaded in sys.modules + # + # (1) => id(sys.modules["macros.a"].target) == id(sys.modules["macros.c"].target) == X + # (2) => id(sys.modules["macros.c"].target) == Y != X == id(sys.modules["macros.a"].target) + # (3) => affects neither sys.modules["macros.a"] nor sys.modules["macros.c"], just loads the macros + # + # At this point we have two different function instances, one in sys.modules["macros.a"] and one + # in sys.modules["macros.c"], which encapsulate the same definition (source code). This used to + # lead to a crash, because we prohibit unequal objects with the same name in the python env. + with patch.object(loader, "_glob_paths", side_effect=_patched_glob_paths): + ctx.load() + + model = ctx.models['"a"'] + python_env = model.python_env + + assert len(python_env) == 4 + assert python_env.get("target") == Executable( + payload="def target():\n pass", name="target", path="macros/c.py" + ) + + +def test_unequal_duplicate_python_env_references_are_prohibited(tmp_path: Path) -> None: + init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + + db_path = str(tmp_path / "db.db") + db_connection = DuckDBConnectionConfig(database=db_path) + config = Config( + gateways={"duckdb": GatewayConfig(connection=db_connection)}, + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + ) + + file_a = tmp_path / "macros" / "unimportant_macro.py" + file_b = tmp_path / "macros" / "just_f.py" + + file_a.write_text( + """from sqlmesh import macro +from macros.just_f import f + +a = False + +@macro() +def unimportant_macro(evaluator): + print(a) + f() + return 1 +""" + ) + file_b.write_text( + """a = 0 + +def f(): + print(a) +""" + ) + + model_file = tmp_path / "models" / "model.sql" + model_file.write_text("MODEL (name m); SELECT @unimportant_macro() AS unimportant_macro") + + with pytest.raises(SQLMeshError, match=r"duplicate definitions found"): + Context(paths=tmp_path, config=config) From c8365901403681f592e818da97ea0393d9fd4f8d Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Sat, 26 Apr 2025 02:41:43 +0300 Subject: [PATCH 0066/1056] Feat: return non-zero code when at least one audit fails in sqlmesh audit (#4258) --- sqlmesh/cli/main.py | 3 ++- sqlmesh/core/context.py | 7 ++++++- sqlmesh/magics.py | 4 ++-- tests/core/test_context.py | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 0fbf2f8817..6bdb50be4a 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -690,7 +690,8 @@ def audit( execution_time: t.Optional[TimeLike] = None, ) -> None: """Run audits for the target model(s).""" - obj.audit(models=models, start=start, end=end, execution_time=execution_time) + if not obj.audit(models=models, start=start, end=end, execution_time=execution_time): + exit(1) @cli.command("check_intervals") diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index c0b7511849..692a5079de 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1827,7 +1827,7 @@ def audit( *, models: t.Optional[t.Iterator[str]] = None, execution_time: t.Optional[TimeLike] = None, - ) -> None: + ) -> bool: """Audit models. Args: @@ -1835,6 +1835,9 @@ def audit( end: The end of the interval to audit. models: The models to audit. All models will be audited if not specified. execution_time: The date/time time reference to use for execution time. Defaults to now. + + Returns: + False if any of the audits failed, True otherwise. """ snapshots = ( @@ -1845,6 +1848,7 @@ def audit( num_audits = sum(len(snapshot.node.audits_with_args) for snapshot in snapshots) self.console.log_status_update(f"Found {num_audits} audit(s).") + errors = [] skipped_count = 0 for snapshot in snapshots: @@ -1884,6 +1888,7 @@ def audit( ) self.console.log_status_update("Done.") + return not errors @python_api_analytics def rewrite(self, sql: str, dialect: str = "") -> exp.Expression: diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index e9ffd9aaf2..020d4ba8d1 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -1000,10 +1000,10 @@ def run_test(self, context: Context, line: str) -> None: @argument("--execution-time", type=str, help="Execution time.") @line_magic @pass_sqlmesh_context - def audit(self, context: Context, line: str) -> None: + def audit(self, context: Context, line: str) -> bool: """Run audit(s)""" args = parse_argstring(self.audit, line) - context.audit( + return context.audit( models=args.models, start=args.start, end=args.end, execution_time=args.execution_time ) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index fa6eaddb4f..1949413953 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -2003,3 +2003,41 @@ def test_check_intervals(sushi_context, mocker): environment=None, no_signals=False, select_models=["*waiter_as_customer*"], end="next week" ) assert tuple(intervals.values())[0].intervals + + +def test_audit(): + context = Context(config=Config()) + + parsed_model = parse( + """ + MODEL ( + name dummy, + audits ( + not_null_non_blocking(columns=[c]) + ) + ); + + SELECT NULL AS c + """ + ) + context.upsert_model(load_sql_based_model(parsed_model)) + context.plan(no_prompts=True, auto_apply=True) + + assert context.audit(models=["dummy"], start="2020-01-01", end="2020-01-01") is False + + parsed_model = parse( + """ + MODEL ( + name dummy, + audits ( + not_null_non_blocking(columns=[c]) + ) + ); + + SELECT 1 AS c + """ + ) + context.upsert_model(load_sql_based_model(parsed_model)) + context.plan(no_prompts=True, auto_apply=True) + + assert context.audit(models=["dummy"], start="2020-01-01", end="2020-01-01") is True From 99e9629e9fe4138d66d28a0bdccfc9a0df25af31 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Sat, 26 Apr 2025 02:52:23 +0300 Subject: [PATCH 0067/1056] Fix: address Snowflake table parsing bug (#4259) --- sqlmesh/core/dialect.py | 4 ++-- tests/core/test_dialect.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index bd58f94db0..ad5be2e884 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -482,9 +482,9 @@ def _parse_table_parts( ) table_arg = table.this - name = table_arg.name + name = table_arg.name if isinstance(table_arg, exp.Var) else "" - if isinstance(table_arg, exp.Var) and name.startswith(SQLMESH_MACRO_PREFIX): + if name.startswith(SQLMESH_MACRO_PREFIX): # In these cases, we don't want to produce a `StagedFilePath` node: # # - @'...' needs to parsed as a string template diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 19b01d70a1..76a5e66b66 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -699,3 +699,7 @@ def test_model_name_cannot_be_string(): ) assert "\\'name\\' property cannot be a string value" in str(parse_error) + + +def test_parse_snowflake_create_schema_ddl(): + assert parse_one("CREATE SCHEMA d.s", dialect="snowflake").sql() == "CREATE SCHEMA d.s" From fb5c52f3ab48b85088c0389907e7103f54288981 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Mon, 28 Apr 2025 20:00:00 +1200 Subject: [PATCH 0068/1056] Feat: Enable Iceberg support for Snowflake (#4262) --- .circleci/config.yml | 3 +- docs/integrations/engines/snowflake.md | 56 +++++++++++++++-- sqlmesh/core/engine_adapter/snowflake.py | 35 ++++++++++- sqlmesh/core/snapshot/evaluator.py | 2 + .../integration/test_integration_snowflake.py | 47 ++++++++++++++ tests/core/engine_adapter/test_snowflake.py | 62 +++++++++++++++++++ tests/core/test_snapshot_evaluator.py | 1 + 7 files changed, 199 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 44f909bf9a..a69c4cbc22 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,7 +17,7 @@ on_tag_filter: &on_tag_filter only: /^v.+/ orbs: - path-filtering: circleci/path-filtering@1.1.0 + path-filtering: circleci/path-filtering@1.2.0 jobs: vscode-extension-setup: @@ -116,6 +116,7 @@ workflows: pytest.ini|setup.cfg|setup.py python true \.circleci/.*|Makefile|\.pre-commit-config\.yaml common true vscode/extensions/.* vscode true + tag: "3.9" - vscode-extension-setup: <<: *on_main_or_tag_filter diff --git a/docs/integrations/engines/snowflake.md b/docs/integrations/engines/snowflake.md index 0a137d5d04..30de0bfd14 100644 --- a/docs/integrations/engines/snowflake.md +++ b/docs/integrations/engines/snowflake.md @@ -525,7 +525,7 @@ MODEL ( ), ); ``` - + ## Custom View and Table types SQLMesh supports custom view and table types for Snowflake models. You can apply these modifiers to either the physical layer or virtual layer of a model using the `physical_properties` and `virtual_properties` attributes respectively. For example: @@ -535,8 +535,8 @@ SQLMesh supports custom view and table types for Snowflake models. You can apply A table can be exposed through a `SECURE` view in the virtual layer by specifying the `creatable_type` property and setting it to `SECURE`: ```sql linenums="1" -Model ( - name = schema_name.model_name, +MODEL ( + name schema_name.model_name, virtual_properties ( creatable_type = SECURE ) @@ -550,8 +550,8 @@ SELECT a FROM schema_name.model_b; A model can use a `TRANSIENT` table in the physical layer by specifying the `creatable_type` property and setting it to `TRANSIENT`: ```sql linenums="1" -Model ( - name = schema_name.model_name, +MODEL ( + name schema_name.model_name, physical_properties ( creatable_type = TRANSIENT ) @@ -560,6 +560,52 @@ Model ( SELECT a FROM schema_name.model_b; ``` +### Iceberg Tables + +In order for Snowflake to be able to create an Iceberg table, there must be an [External Volume](https://docs.snowflake.com/en/user-guide/tables-iceberg-configure-external-volume) configured to store the Iceberg table data on. + +Once that is configured, you can create a model backed by an Iceberg table by using `table_format iceberg` like so: + +```sql linenums="1" hl_lines="4 6-7" +MODEL ( + name schema_name.model_name, + kind FULL, + table_format iceberg, + physical_properties ( + catalog = 'snowflake', + external_volume = '' + ) +); +``` + +To prevent having to specify `catalog = 'snowflake'` and `external_volume = ''` on every model, see the Snowflake documentation for: + + - [Configuring a default Catalog](https://docs.snowflake.com/en/user-guide/tables-iceberg-configure-catalog-integration#set-a-default-catalog-at-the-account-database-or-schema-level) + - [Configuring a default External Volume](https://docs.snowflake.com/en/user-guide/tables-iceberg-configure-external-volume#set-a-default-external-volume-at-the-account-database-or-schema-level) + +Alternatively you can also use [model defaults](../../guides/configuration.md#model-defaults) to set defaults at the SQLMesh level instead. + +To utilize the wide variety of [optional properties](https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table-snowflake#optional-parameters) that Snowflake makes available for Iceberg tables, simply specify them as `physical_properties`: + +```sql linenums="1" hl_lines="8" +MODEL ( + name schema_name.model_name, + kind FULL, + table_format iceberg, + physical_properties ( + catalog = 'snowflake', + external_volume = 'my_external_volume', + base_location = 'my/product_reviews/' + ) +); +``` + +!!! warning "External catalogs" + + Setting `catalog = 'snowflake'` to use Snowflake's internal catalog is a good default because SQLMesh needs to be able to write to the tables it's managing and Snowflake [does not support](https://docs.snowflake.com/en/user-guide/tables-iceberg#catalog-options) writing to Iceberg tables configured under external catalogs. + + You can however still reference a table from an external catalog in your model as a normal [external table](../../concepts/models/external_models.md). + ## Troubleshooting ### Frequent Authentication Prompts diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index 8801158b3b..ab01bf6722 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -112,6 +112,38 @@ def snowpark(self) -> t.Optional[SnowparkSession]: def catalog_support(self) -> CatalogSupport: return CatalogSupport.FULL_SUPPORT + def _create_table( + self, + table_name_or_schema: t.Union[exp.Schema, TableName], + expression: t.Optional[exp.Expression], + exists: bool = True, + replace: bool = False, + columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + table_description: t.Optional[str] = None, + column_descriptions: t.Optional[t.Dict[str, str]] = None, + table_kind: t.Optional[str] = None, + **kwargs: t.Any, + ) -> None: + table_format = kwargs.get("table_format") + if table_format and isinstance(table_format, str): + table_format = table_format.upper() + if not table_kind: + table_kind = f"{table_format} TABLE" + elif table_kind == self.MANAGED_TABLE_KIND: + table_kind = f"DYNAMIC {table_format} TABLE" + + super()._create_table( + table_name_or_schema=table_name_or_schema, + expression=expression, + exists=exists, + replace=replace, + columns_to_types=columns_to_types, + table_description=table_description, + column_descriptions=column_descriptions, + table_kind=table_kind, + **kwargs, + ) + def create_managed_table( self, table_name: TableName, @@ -230,7 +262,8 @@ def _build_table_properties_exp( if table_properties: table_properties = {k.upper(): v for k, v in table_properties.items()} # if we are creating a non-dynamic table; remove any properties that are only valid for dynamic tables - if table_kind != self.MANAGED_TABLE_KIND: + # this is necessary because we create "normal" tables from the same managed model definition for dev previews and the "normal" tables dont support these parameters + if "DYNAMIC" not in (table_kind or "").upper(): for prop in {"WAREHOUSE", "TARGET_LAG", "REFRESH_MODE", "INITIALIZE"}: table_properties.pop(prop, None) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 47adf8f1b7..48d74f440e 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -2098,6 +2098,7 @@ def create( table_properties=kwargs.get("physical_properties", model.physical_properties), table_description=model.description, column_descriptions=model.column_descriptions, + table_format=model.table_format, ) elif not is_table_deployable: # Only create the dev preview table as a normal table. @@ -2134,6 +2135,7 @@ def insert( table_properties=kwargs.get("physical_properties", model.physical_properties), table_description=model.description, column_descriptions=model.column_descriptions, + table_format=model.table_format, ) elif not is_snapshot_deployable: # Snapshot isnt deployable; update the preview table instead diff --git a/tests/core/engine_adapter/integration/test_integration_snowflake.py b/tests/core/engine_adapter/integration/test_integration_snowflake.py index 18030f43d3..ddc7ec9f2a 100644 --- a/tests/core/engine_adapter/integration/test_integration_snowflake.py +++ b/tests/core/engine_adapter/integration/test_integration_snowflake.py @@ -163,3 +163,50 @@ def _get_data_object(table: exp.Table) -> DataObject: metadata = _get_data_object(target_table_1) assert not metadata.is_clustered + + +def test_create_iceberg_table(ctx: TestContext, engine_adapter: SnowflakeEngineAdapter) -> None: + # Note: this test relies on a default Catalog and External Volume being configured in Snowflake + # ref: https://docs.snowflake.com/en/user-guide/tables-iceberg-configure-catalog-integration#set-a-default-catalog-at-the-account-database-or-schema-level + # ref: https://docs.snowflake.com/en/user-guide/tables-iceberg-configure-external-volume#set-a-default-external-volume-at-the-account-database-or-schema-level + # This has been done on the Snowflake account used by CI + + model_name = ctx.table("TEST") + managed_model_name = ctx.table("TEST_DYNAMIC") + sqlmesh = ctx.create_context() + + model = load_sql_based_model( + d.parse(f""" + MODEL ( + name {model_name}, + kind FULL, + table_format iceberg, + dialect 'snowflake' + ); + + select 1 as "ID", 'foo' as "NAME"; + """) + ) + + managed_model = load_sql_based_model( + d.parse(f""" + MODEL ( + name {managed_model_name}, + kind MANAGED, + physical_properties ( + target_lag = '20 minutes' + ), + table_format iceberg, + dialect 'snowflake' + ); + + select "ID", "NAME" from {model_name}; + """) + ) + + sqlmesh.upsert_model(model) + sqlmesh.upsert_model(managed_model) + + result = sqlmesh.plan(auto_apply=True) + + assert len(result.new_snapshots) == 2 diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index fc7ec7a26b..098d329079 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -19,6 +19,13 @@ pytestmark = [pytest.mark.engine, pytest.mark.snowflake] +@pytest.fixture +def snowflake_mocked_engine_adapter( + make_mocked_engine_adapter: t.Callable, +) -> SnowflakeEngineAdapter: + return make_mocked_engine_adapter(SnowflakeEngineAdapter) + + def test_get_temp_table(mocker: MockerFixture, make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter) @@ -270,11 +277,25 @@ def test_create_managed_table(make_mocked_engine_adapter: t.Callable, mocker: Mo }, ) + # table_format=iceberg + adapter.create_managed_table( + table_name="test_table", + query=query, + columns_to_types=columns_to_types, + table_properties={ + "target_lag": exp.Literal.string("20 minutes"), + "catalog": exp.Literal.string("snowflake"), + "external_volume": exp.Literal.string("test"), + }, + table_format="iceberg", + ) + assert to_sql_calls(adapter) == [ """CREATE OR REPLACE DYNAMIC TABLE "test_table" TARGET_LAG='20 minutes' WAREHOUSE="default_warehouse" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT "a", "b" FROM "source_table") AS "_subquery\"""", """CREATE OR REPLACE DYNAMIC TABLE "test_table" TARGET_LAG='20 minutes' WAREHOUSE="foo" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT "a", "b" FROM "source_table") AS "_subquery\"""", """CREATE OR REPLACE DYNAMIC TABLE "test_table" CLUSTER BY ("a") TARGET_LAG='20 minutes' WAREHOUSE="default_warehouse" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT "a", "b" FROM "source_table") AS "_subquery\"""", """CREATE OR REPLACE DYNAMIC TABLE "test_table" TARGET_LAG='20 minutes' REFRESH_MODE='auto' INITIALIZE='on_create' WAREHOUSE="default_warehouse" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT "a", "b" FROM "source_table") AS "_subquery\"""", + """CREATE OR REPLACE DYNAMIC ICEBERG TABLE "test_table" TARGET_LAG='20 minutes' CATALOG='snowflake' EXTERNAL_VOLUME='test' WAREHOUSE="default_warehouse" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT "a", "b" FROM "source_table") AS "_subquery\"""", ] @@ -666,3 +687,44 @@ def test_clone_table(mocker: MockerFixture, make_mocked_engine_adapter: t.Callab adapter.cursor.execute.assert_called_once_with( 'CREATE TABLE "target_table" CLONE "source_table"' ) + + +def test_table_format_iceberg(snowflake_mocked_engine_adapter: SnowflakeEngineAdapter) -> None: + adapter = snowflake_mocked_engine_adapter + + model = load_sql_based_model( + expressions=d.parse(""" + MODEL ( + name test.table, + kind full, + table_format iceberg, + physical_properties ( + catalog = 'snowflake', + external_volume = 'test' + ) + ); + SELECT a::INT; + """) + ) + assert isinstance(model, SqlModel) + assert model.table_format == "iceberg" + + adapter.create_table( + table_name=model.name, + columns_to_types=model.columns_to_types_or_raise, + table_format=model.table_format, + table_properties=model.physical_properties, + ) + + adapter.ctas( + table_name=model.name, + query_or_df=model.render_query_or_raise(), + columns_to_types=model.columns_to_types_or_raise, + table_format=model.table_format, + table_properties=model.physical_properties, + ) + + assert to_sql_calls(adapter) == [ + 'CREATE ICEBERG TABLE IF NOT EXISTS "test"."table" ("a" INT) CATALOG=\'snowflake\' EXTERNAL_VOLUME=\'test\'', + 'CREATE ICEBERG TABLE IF NOT EXISTS "test"."table" CATALOG=\'snowflake\' EXTERNAL_VOLUME=\'test\' AS SELECT CAST("a" AS INT) AS "a" FROM (SELECT CAST("a" AS INT) AS "a") AS "_subquery"', + ] diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 6de055cad3..dbe8d9c3de 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -3367,6 +3367,7 @@ def test_create_managed(adapter_mock, make_snapshot, mocker: MockerFixture): table_properties=model.physical_properties, table_description=model.description, column_descriptions=model.column_descriptions, + table_format=None, ) From 99b1512e373887fe3f413d50f6463010786b8905 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:11:31 +0100 Subject: [PATCH 0069/1056] fix: fix the lsp initialization (#4265) --- sqlmesh/lsp/main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 290362afc4..66da134f52 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -21,12 +21,11 @@ class LSPContext: def __init__(self, context: Context) -> None: self.context = context - map: t.Dict[str, t.List[str]] = defaultdict(list[str]) + map: t.Dict[str, t.List[str]] = defaultdict(list) for model in context.models.values(): - if model._path is None: + if model._path is not None: path = Path(model._path).resolve() map[f"file://{path.as_posix()}"].append(model.name) - self.map = map From 31a9b5e1b8116cdbe3b8a7538dbbdee17f5a2de3 Mon Sep 17 00:00:00 2001 From: Chris Rericha <67359577+crericha@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:29:05 -0400 Subject: [PATCH 0070/1056] Feat: Add additional fields to environment summary (#4267) --- sqlmesh/core/console.py | 14 ++++--- sqlmesh/core/environment.py | 46 +++++++++++++++------ sqlmesh/core/state_sync/base.py | 11 +++-- sqlmesh/core/state_sync/db/environment.py | 24 +++++++---- sqlmesh/core/state_sync/db/facade.py | 6 +-- tests/core/state_sync/test_export_import.py | 6 ++- tests/core/state_sync/test_state_sync.py | 8 ++-- 7 files changed, 77 insertions(+), 38 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 8febdec8ec..ffe63a80de 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -28,7 +28,7 @@ from rich.tree import Tree from sqlglot import exp -from sqlmesh.core.environment import EnvironmentNamingInfo +from sqlmesh.core.environment import EnvironmentNamingInfo, EnvironmentSummary from sqlmesh.core.linter.rule import RuleViolation from sqlmesh.core.model import Model from sqlmesh.core.snapshot import ( @@ -188,7 +188,7 @@ class EnvironmentsConsole(abc.ABC): """Console for displaying environments""" @abc.abstractmethod - def print_environments(self, environments_summary: t.Dict[str, int]) -> None: + def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None: """Prints all environment names along with expiry datetime.""" @abc.abstractmethod @@ -659,7 +659,7 @@ def show_row_diff( ) -> None: pass - def print_environments(self, environments_summary: t.Dict[str, int]) -> None: + def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None: pass def show_intervals(self, snapshot_intervals: t.Dict[Snapshot, SnapshotIntervals]) -> None: @@ -2089,11 +2089,13 @@ def show_row_diff( self.console.print(f"\n[b][green]{target_name} ONLY[/green] sample rows:[/b]") self.console.print(row_diff.t_sample.to_string(index=False), end="\n\n") - def print_environments(self, environments_summary: t.Dict[str, int]) -> None: + def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None: """Prints all environment names along with expiry datetime.""" output = [ - f"{name} - {time_like_to_str(ts)}" if ts else f"{name} - No Expiry" - for name, ts in environments_summary.items() + f"{summary.name} - {time_like_to_str(summary.expiration_ts)}" + if summary.expiration_ts + else f"{summary.name} - No Expiry" + for summary in environments_summary ] output_str = "\n".join([str(len(output)), *output]) self.log_status_update(f"Number of SQLMesh environments are: {output_str}") diff --git a/sqlmesh/core/environment.py b/sqlmesh/core/environment.py index be3f557999..ea326d6d26 100644 --- a/sqlmesh/core/environment.py +++ b/sqlmesh/core/environment.py @@ -95,32 +95,46 @@ def from_environment_catalog_mapping( return cls(**construction_kwargs) -class Environment(EnvironmentNamingInfo): - """Represents an isolated environment. - - Environments are isolated workspaces that hold pointers to physical tables. +class EnvironmentSummary(PydanticModel): + """Represents summary information of an isolated environment. Args: - snapshots: The snapshots that are part of this environment. + name: The name of the environment. start_at: The start time of the environment. end_at: The end time of the environment. plan_id: The ID of the plan that last updated this environment. previous_plan_id: The ID of the previous plan that updated this environment. expiration_ts: The timestamp when this environment will expire. finalized_ts: The timestamp when this environment was finalized. - promoted_snapshot_ids: The IDs of the snapshots that are promoted in this environment - (i.e. for which the views are created). If not specified, all snapshots are promoted. - previous_finalized_snapshots: Snapshots that were part of this environment last time it was finalized. - requirements: A mapping of library versions for all the snapshots in this environment. """ - snapshots_: t.List[t.Any] = Field(alias="snapshots") + name: str start_at: TimeLike end_at: t.Optional[TimeLike] = None plan_id: str previous_plan_id: t.Optional[str] = None expiration_ts: t.Optional[int] = None finalized_ts: t.Optional[int] = None + + @property + def expired(self) -> bool: + return self.expiration_ts is not None and self.expiration_ts <= now_timestamp() + + +class Environment(EnvironmentNamingInfo, EnvironmentSummary): + """Represents an isolated environment. + + Environments are isolated workspaces that hold pointers to physical tables. + + Args: + snapshots: The snapshots that are part of this environment. + promoted_snapshot_ids: The IDs of the snapshots that are promoted in this environment + (i.e. for which the views are created). If not specified, all snapshots are promoted. + previous_finalized_snapshots: Snapshots that were part of this environment last time it was finalized. + requirements: A mapping of library versions for all the snapshots in this environment. + """ + + snapshots_: t.List[t.Any] = Field(alias="snapshots") promoted_snapshot_ids_: t.Optional[t.List[t.Any]] = Field( default=None, alias="promoted_snapshot_ids" ) @@ -203,8 +217,16 @@ def naming_info(self) -> EnvironmentNamingInfo: ) @property - def expired(self) -> bool: - return self.expiration_ts is not None and self.expiration_ts <= now_timestamp() + def summary(self) -> EnvironmentSummary: + return EnvironmentSummary( + name=self.name, + start_at=self.start_at, + end_at=self.end_at, + plan_id=self.plan_id, + previous_plan_id=self.previous_plan_id, + expiration_ts=self.expiration_ts, + finalized_ts=self.finalized_ts, + ) def _convert_list_to_models_and_store( self, field: str, type_: t.Type[PydanticType] diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index 771dd94172..da4a8ecb20 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -9,7 +9,12 @@ from sqlglot import __version__ as SQLGLOT_VERSION from sqlmesh import migrations -from sqlmesh.core.environment import Environment, EnvironmentNamingInfo, EnvironmentStatements +from sqlmesh.core.environment import ( + Environment, + EnvironmentNamingInfo, + EnvironmentStatements, + EnvironmentSummary, +) from sqlmesh.core.snapshot import ( Snapshot, SnapshotId, @@ -137,11 +142,11 @@ def get_environments(self) -> t.List[Environment]: """ @abc.abstractmethod - def get_environments_summary(self) -> t.Dict[str, int]: + def get_environments_summary(self) -> t.List[EnvironmentSummary]: """Fetches all environment names along with expiry datetime. Returns: - A dict of all environment names along with expiry datetime. + A list of all environment summaries. """ @abc.abstractmethod diff --git a/sqlmesh/core/state_sync/db/environment.py b/sqlmesh/core/state_sync/db/environment.py index a63caf6259..4ba563e179 100644 --- a/sqlmesh/core/state_sync/db/environment.py +++ b/sqlmesh/core/state_sync/db/environment.py @@ -12,7 +12,7 @@ fetchall, fetchone, ) -from sqlmesh.core.environment import Environment, EnvironmentStatements +from sqlmesh.core.environment import Environment, EnvironmentStatements, EnvironmentSummary from sqlmesh.utils.migration import index_text_type, blob_text_type from sqlmesh.utils.date import now_timestamp, time_like_to_str from sqlmesh.utils.errors import SQLMeshError @@ -211,18 +211,19 @@ def get_environments(self) -> t.List[Environment]: for row in fetchall(self.engine_adapter, self._environments_query()) ] - def get_environments_summary(self) -> t.Dict[str, int]: - """Fetches all environment names along with expiry datetime. + def get_environments_summary(self) -> t.List[EnvironmentSummary]: + """Fetches summaries for all environments. Returns: - A dict of all environment names along with expiry datetime. + A list of all environment summaries. """ - return dict( - fetchall( + return [ + self._environment_summmary_from_row(row) + for row in fetchall( self.engine_adapter, - self._environments_query(required_fields=["name", "expiration_ts"]), - ), - ) + self._environments_query(required_fields=list(EnvironmentSummary.all_fields())), + ) + ] def get_environment( self, environment: str, lock_for_update: bool = False @@ -286,6 +287,11 @@ def get_environment_statements(self, environment: str) -> t.List[EnvironmentStat def _environment_from_row(self, row: t.Tuple[str, ...]) -> Environment: return Environment(**{field: row[i] for i, field in enumerate(Environment.all_fields())}) + def _environment_summmary_from_row(self, row: t.Tuple[str, ...]) -> EnvironmentSummary: + return EnvironmentSummary( + **{field: row[i] for i, field in enumerate(EnvironmentSummary.all_fields())} + ) + def _environments_query( self, where: t.Optional[str | exp.Expression] = None, diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 0285b0981a..0d1d54ef27 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -26,7 +26,7 @@ from sqlmesh.core.console import Console, get_console from sqlmesh.core.engine_adapter import EngineAdapter -from sqlmesh.core.environment import Environment, EnvironmentStatements +from sqlmesh.core.environment import Environment, EnvironmentStatements, EnvironmentSummary from sqlmesh.core.snapshot import ( Snapshot, SnapshotId, @@ -330,11 +330,11 @@ def get_environments(self) -> t.List[Environment]: """ return self.environment_state.get_environments() - def get_environments_summary(self) -> t.Dict[str, int]: + def get_environments_summary(self) -> t.List[EnvironmentSummary]: """Fetches all environment names along with expiry datetime. Returns: - A dict of all environment names along with expiry datetime. + A list of all environment summaries. """ return self.environment_state.get_environments_summary() diff --git a/tests/core/state_sync/test_export_import.py b/tests/core/state_sync/test_export_import.py index 4e4aee5861..8989d28f6b 100644 --- a/tests/core/state_sync/test_export_import.py +++ b/tests/core/state_sync/test_export_import.py @@ -437,7 +437,11 @@ def test_import_partial( import_state(state_sync, output_file, clear=False) # StateSync should have "prod", "dev" and "dev2". - assert sorted(list(state_sync.get_environments_summary().keys())) == ["dev", "dev2", "prod"] + assert sorted(list(env.name for env in state_sync.get_environments_summary())) == [ + "dev", + "dev2", + "prod", + ] assert not context.plan(environment="dev", skip_tests=True).has_changes assert not context.plan(environment="dev2", skip_tests=True).has_changes diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index 992bd0ec13..175a14f40d 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -3464,8 +3464,8 @@ def test_get_environments_summary( ) state_sync.promote(prod) - actual = state_sync.get_environments_summary() - expected = {"prod": None, "test_environment_a": env_a_ttl, "test_environment_b": env_b_ttl} + actual = set(state_sync.get_environments_summary()) + expected = {prod.summary, env_a.summary, env_b.summary} assert actual == expected @@ -3493,12 +3493,12 @@ def test_get_environments_summary_only_prod( ) state_sync.promote(prod) actual = state_sync.get_environments_summary() - expected = {"prod": None} + expected = [prod.summary] assert actual == expected def test_get_environments_summary_no_env(state_sync: EngineAdapterStateSync) -> None: - assert state_sync.get_environments_summary() == {} + assert state_sync.get_environments_summary() == [] @time_machine.travel("2020-01-05 00:00:00 UTC") From e2adfe3714b8c794cf348123e022304fd37119f9 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 28 Apr 2025 12:53:29 -0700 Subject: [PATCH 0071/1056] Fix: Don't log SQL in the engine adapter if the statement contains values (#4269) --- sqlmesh/core/engine_adapter/base.py | 25 ++++++++++++++++++--- tests/core/engine_adapter/test_base.py | 30 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index aa28066a84..29e870011b 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -2126,11 +2126,30 @@ def execute( else e ), ) - self._log_sql(sql) + self._log_sql( + sql, + expression=e if isinstance(e, exp.Expression) else None, + quote_identifiers=quote_identifiers, + ) self._execute(sql, **kwargs) - def _log_sql(self, sql: str) -> None: - logger.log(self._execute_log_level, "Executing SQL: %s", sql) + def _log_sql( + self, + sql: str, + expression: t.Optional[exp.Expression] = None, + quote_identifiers: bool = True, + ) -> None: + if not logger.isEnabledFor(self._execute_log_level): + return + + sql_to_log = sql + if expression is not None and not isinstance(expression, exp.Query): + values = expression.find(exp.Values) + if values: + values.set("expressions", [exp.to_identifier("")]) + sql_to_log = self._to_sql(expression, quote=quote_identifiers) + + logger.log(self._execute_log_level, "Executing SQL: %s", sql_to_log) def _execute(self, sql: str, **kwargs: t.Any) -> None: self.cursor.execute(sql, **kwargs) diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index fdbe6dac82..edf222460f 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -3071,3 +3071,33 @@ def test_insert_overwrite_by_partition_query_insert_overwrite_strategy( assert sql_calls == [ 'INSERT OVERWRITE TABLE "test_schema"."test_table" ("a", "ds", "b") SELECT "a", "ds", "b" FROM "tbl"' ] + + +def test_log_sql(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): + adapter = make_mocked_engine_adapter(EngineAdapter) + + mock_logger = mocker.patch("sqlmesh.core.engine_adapter.base.logger") + + df = pd.DataFrame({"id": [1, 2, 3], "value": ["test1", "test2", "test3"]}) + + adapter.execute(parse_one("SELECT 1")) + adapter.execute(parse_one("INSERT INTO test SELECT * FROM source")) + adapter.execute(parse_one("INSERT INTO test (id, value) VALUES (1, 'test')")) + adapter.insert_append("test", df) + adapter.replace_query("test", df) + + assert mock_logger.log.call_count == 5 + assert mock_logger.log.call_args_list[0][0][2] == "SELECT 1" + assert mock_logger.log.call_args_list[1][0][2] == 'INSERT INTO "test" SELECT * FROM "source"' + assert ( + mock_logger.log.call_args_list[2][0][2] + == 'INSERT INTO "test" ("id", "value") VALUES ""' + ) + assert ( + mock_logger.log.call_args_list[3][0][2] + == 'INSERT INTO "test" ("id", "value") SELECT CAST("id" AS BIGINT) AS "id", CAST("value" AS TEXT) AS "value" FROM (VALUES "") AS "t"("id", "value")' + ) + assert ( + mock_logger.log.call_args_list[4][0][2] + == 'CREATE OR REPLACE TABLE "test" AS SELECT CAST("id" AS BIGINT) AS "id", CAST("value" AS TEXT) AS "value" FROM (SELECT CAST("id" AS BIGINT) AS "id", CAST("value" AS TEXT) AS "value" FROM (VALUES "") AS "t"("id", "value")) AS "_subquery"' + ) From bafdf6693cca878d249f37c7b7fae9a49bb0dd72 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:05:09 +0300 Subject: [PATCH 0072/1056] Chore: add pyarrow as a dependency for the databricks target (#4274) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 36cfe53bcf..4efc35cef1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ bigquery = [ ] bigframes = ["bigframes>=1.32.0"] clickhouse = ["clickhouse-connect"] -databricks = ["databricks-sql-connector"] +databricks = ["databricks-sql-connector[pyarrow]"] dev = [ "agate==1.7.1", "beautifulsoup4", From d79c90ab0bc880b8f8a9605719b9b4d1e4ff9def Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:01:58 +0100 Subject: [PATCH 0073/1056] feat(vscode): lower api requirement to allow use with cursor (#4276) --- package-lock.json | 467 ++++++++++++++-------------------- vscode/extension/package.json | 6 +- 2 files changed, 194 insertions(+), 279 deletions(-) diff --git a/package-lock.json b/package-lock.json index b919d6fe49..71b3c12b67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -494,9 +495,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", "cpu": [ "ppc64" ], @@ -511,9 +512,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", "cpu": [ "arm" ], @@ -528,9 +529,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", "cpu": [ "arm64" ], @@ -545,9 +546,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", "cpu": [ "x64" ], @@ -562,9 +563,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", "cpu": [ "arm64" ], @@ -579,9 +580,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", "cpu": [ "x64" ], @@ -596,9 +597,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", "cpu": [ "arm64" ], @@ -613,9 +614,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", "cpu": [ "x64" ], @@ -630,9 +631,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", "cpu": [ "arm" ], @@ -647,9 +648,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", "cpu": [ "arm64" ], @@ -664,9 +665,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", "cpu": [ "ia32" ], @@ -681,9 +682,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", "cpu": [ "loong64" ], @@ -698,9 +699,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", "cpu": [ "mips64el" ], @@ -715,9 +716,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", "cpu": [ "ppc64" ], @@ -732,9 +733,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", "cpu": [ "riscv64" ], @@ -749,9 +750,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", "cpu": [ "s390x" ], @@ -766,9 +767,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", "cpu": [ "x64" ], @@ -783,9 +784,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", "cpu": [ "arm64" ], @@ -800,9 +801,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", "cpu": [ "x64" ], @@ -817,9 +818,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", "cpu": [ "arm64" ], @@ -834,9 +835,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", "cpu": [ "x64" ], @@ -851,9 +852,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", "cpu": [ "x64" ], @@ -868,9 +869,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", "cpu": [ "arm64" ], @@ -885,9 +886,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", "cpu": [ "ia32" ], @@ -902,9 +903,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", "cpu": [ "x64" ], @@ -3572,20 +3573,6 @@ "ajv": ">=8" } }, - "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", - "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, "node_modules/@stoplight/spectral-core/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -3884,9 +3871,9 @@ } }, "node_modules/@stoplight/types": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", - "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", + "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4267,24 +4254,23 @@ } }, "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", + "aria-query": "5.1.3", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" }, "engines": { - "node": ">=18" + "node": ">=14" } }, "node_modules/@testing-library/jest-dom": { @@ -4361,36 +4347,6 @@ "react-dom": "^18.0.0" } }, - "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", - "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/react/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, "node_modules/@testing-library/user-event": { "version": "14.6.1", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", @@ -4847,13 +4803,10 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", - "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } + "version": "20.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.0.tgz", + "integrity": "sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ==", + "license": "MIT" }, "node_modules/@types/pad-left": { "version": "2.1.1", @@ -4915,24 +4868,24 @@ "license": "MIT" }, "node_modules/@types/vscode": { - "version": "1.99.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.99.1.tgz", - "integrity": "sha512-cQlqxHZ040ta6ovZXnXRxs3fJiTmlurkIWOfZVcLSZPcm9J4ikFpXuB7gihofGn5ng+kDVma5EmJIclfk0trPQ==", + "version": "1.96.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.96.0.tgz", + "integrity": "sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.0.tgz", - "integrity": "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", + "integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.31.0", - "@typescript-eslint/type-utils": "8.31.0", - "@typescript-eslint/utils": "8.31.0", - "@typescript-eslint/visitor-keys": "8.31.0", + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/type-utils": "8.31.1", + "@typescript-eslint/utils": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -4952,16 +4905,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.0.tgz", - "integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz", + "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.31.0", - "@typescript-eslint/types": "8.31.0", - "@typescript-eslint/typescript-estree": "8.31.0", - "@typescript-eslint/visitor-keys": "8.31.0", + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/typescript-estree": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", "debug": "^4.3.4" }, "engines": { @@ -4977,14 +4930,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.0.tgz", - "integrity": "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz", + "integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.0", - "@typescript-eslint/visitor-keys": "8.31.0" + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4995,14 +4948,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.0.tgz", - "integrity": "sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz", + "integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.31.0", - "@typescript-eslint/utils": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.1", + "@typescript-eslint/utils": "8.31.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -5019,9 +4972,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", - "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz", + "integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==", "dev": true, "license": "MIT", "engines": { @@ -5033,14 +4986,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz", - "integrity": "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz", + "integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.0", - "@typescript-eslint/visitor-keys": "8.31.0", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5060,16 +5013,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.0.tgz", - "integrity": "sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz", + "integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.31.0", - "@typescript-eslint/types": "8.31.0", - "@typescript-eslint/typescript-estree": "8.31.0" + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/typescript-estree": "8.31.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5084,13 +5037,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz", - "integrity": "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz", + "integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/types": "8.31.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -5665,9 +5618,9 @@ } }, "node_modules/@vscode/vsce/node_modules/glob": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", - "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", + "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", "dev": true, "license": "ISC", "dependencies": { @@ -6186,12 +6139,6 @@ "arrow2csv": "bin/arrow2csv.js" } }, - "node_modules/apache-arrow/node_modules/@types/node": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.0.tgz", - "integrity": "sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ==", - "license": "MIT" - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -6218,13 +6165,13 @@ } }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "dequal": "^2.0.3" + "deep-equal": "^2.0.5" } }, "node_modules/array-back": { @@ -8444,9 +8391,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8457,31 +8404,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" } }, "node_modules/escalade": { @@ -9422,9 +9369,9 @@ "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -13639,21 +13586,6 @@ "node": ">=18" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -14336,13 +14268,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -14741,24 +14666,10 @@ "license": "MIT" }, "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], "license": "MIT" }, "node_modules/safe-push-apply": { @@ -15355,13 +15266,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -16484,9 +16388,10 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, "license": "MIT" }, "node_modules/unified": { @@ -18094,8 +17999,8 @@ }, "devDependencies": { "@types/mocha": "^10.0.10", - "@types/node": "20.x", - "@types/vscode": "^1.98.0", + "@types/node": "20.11.25", + "@types/vscode": "1.96.0", "@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/parser": "^8.28.0", "@vscode/test-cli": "^0.0.10", @@ -18107,7 +18012,17 @@ "typescript": "^5.8.2" }, "engines": { - "vscode": "^1.98.0" + "vscode": "^1.96.0" + } + }, + "vscode/extension/node_modules/@types/node": { + "version": "20.11.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", + "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" } }, "web/client": { diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 35a1f24842..66d18e507b 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -11,7 +11,7 @@ "main": "./dist/extension.js", "icon": "assets/logo.png", "engines": { - "vscode": "^1.98.0" + "vscode": "^1.96.0" }, "categories": [ "Other" @@ -85,8 +85,8 @@ }, "devDependencies": { "@types/mocha": "^10.0.10", - "@types/node": "20.x", - "@types/vscode": "^1.98.0", + "@types/node": "20.11.25", + "@types/vscode": "1.96.0", "@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/parser": "^8.28.0", "@vscode/test-cli": "^0.0.10", From 738da48a45fbc102fa55fc7a317965981a257b4c Mon Sep 17 00:00:00 2001 From: Sung Won Chung Date: Tue, 29 Apr 2025 11:19:30 -0700 Subject: [PATCH 0074/1056] New Hybrid Diagram (#4277) --- ...d-executors_standard-hybrid-deployment.png | Bin 641261 -> 282926 bytes .../scheduler/hybrid_executors_overview.md | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cloud/features/scheduler/hybrid_executors/hybrid-executors_standard-hybrid-deployment.png b/docs/cloud/features/scheduler/hybrid_executors/hybrid-executors_standard-hybrid-deployment.png index 8c5acd37316a409c93453df9e8efb36723d8d372..2877933d1f5295b660f9535dbe116f556d57e675 100644 GIT binary patch literal 282926 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

F|Kr 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

Nw6X(X;Ysbr6!U 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�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 z%0D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

n@DU>1&r7Dw%i^E?mSKVGee;Nh)8TAyBjx zDm##Gb6d7@O(=DX!d?nogNjC2o{~GZWT@GjdV=pc?0?zp2U%QtXBX6#1I5C|xd}99 z2gdkk4_4)())zV{Gl<;pZZvgQ1gDNla(+X%w`%2A5`mpTtuNy~y?4y3q)dB|1UO`kZ;g-#Yu;Ln z&~D#a;M#9y3#*jQxr9?ZK>txmhs3OGx#A>E&dXj=73b#AeXj;cl!L|-$E@fr?mJSi z%^SO)yGp8~xj;kjxp6_{=Diwo|xWMtEVwQ_T*9wfdWoQ}m z%A!*qDkHFD>X7bYt`aD9q~p*ct%~M-*YkPoxiG_J_W>zY)3+RB{r$Is;vjTcm(Qj-vu*Pb~PvnD3Xk}a|y;9V^h#L9fAO^?|^_3~6$sQSCitHDb zT?=eat?tk*Fv$$!p!PcWiuz8IvnN%1(mu z+<@-78}Vz~pk^p8UdyBs*BcB#NP&J^IkMp7$~_MDtK5t+C=84jd|8&mkF3L=cU zPnr&{M)e7e;_Pm2QC+)I^4&x!U!vM=p;Ql zBhc7eEviUZ+hq0)mFqIey4;K6IUx+RC= z|D418penB#EO;YNP|x!wDajKdEugyq-yY*^;$+WmR3v*vt7rPwXhOtF;fzt9-2ENo zD0jSoL`7Nzdf2~c{Zr>jT|XFSQLTb6!1ON6bu3S8Pr-lEDN6`$$YHoO#`opUM^Eor zB~WBWBKjB=(BZdzb-!`zK`%x6bbd-;p#eHxU z_|@M8oo#1`%uaxVj$a?vHPeZ%%DE3|loGWhyUB@l0V zwmylo7nSs9JSdVE%A(!UdQ6$syMJC^(a}MJ7NzotK}Lw2G}yncu?r%xI5qCyliP41 zkY5H?Cfu^~&g!kXEm;yPGm^PUj)%*z)0*o!mr2hu@o^w~fJ^Ln(e-j-a*L?1A;HTP zunqeUR>CC}!(H>`*Ly1c53J-7^j)UgCSy9HvO?n8kBLtJuekjTB%W(O^Ip|=h+4l1 zu<#D0Plg>Y3Fe_+1nYM#d&=25}RZwaQG`X!I2$)g`ACcTFL1XvcKECPQ3e zP%AWrvtufcc*MFo_L{;&(=>COrP(Y6nn!P_bGD;9M$RE0eBz{)pRG;jdL4 zcTCDim}E8IZH-U|ucjZid6tD!D9O9!j<`XJW6LW_`ek{hw*jt|0V)^!hvMr`S!d-X zYW*$~Phicnb~BIP9#xF4hg<8KHT%H1;fgEDWHGJ^SYxkV^*2JVYI03OCAI9Y!`&;+ z&}MsVLZC0(@W(Dz$lNSp|BD^3;UviwqSpOu`kt`+YShR>{C5BHzThG>9UF(2mewmm z|9p#~j=LijAu+GF#O`i?Tj^byb*33LB|*~?_z#BwTv3PxRJj0;Q}FwGhhEfx;qtxX z{b|(rscuxh^uk(7T89Jf%xl(*PfvnA__-upiWY>nL|vKy=m^DKnVosD)pZrxqN`@A zJjwIvEW~XaS`*XzAs#|U{U8}X=5~j+_U*B%@c7v~mJ0l#W$_F#vjba>@^W9D&mzaw z4+?u3+T9=WvMiY>9KO`!Y1e(veSvEw?#mi}T)Y@hJ$)**ZVS!jEi=9jIk`!l>F55D z9nmFm$z{D(ScK~iYcI8ugw-rIK0nF7D!nYpJ+vvG{Ef_R1;KB=f_$ z5V9s}5imIFex4sQ57``WmZt?o}&2I!9}fk zw?NuBJjqLX*IMp<1~VXr;mc-KL3(6@E#}=a)CP=E&e zU2!Hv3W$%%VGNTF9ABYbPskL;#8e*js@5R;;~obJmAUMgA4_0E3y0^)tYEp?cFD#0 zap2rljC(DMU2stS5}8dt<7JO%=!?jI%jN*&Mjh-F<$H7=8n+M27%a_YdZu4B^Y?`HZS@AwaKucCk*jXPKiGeO)(-t#G!x zUUO=5wme&Vmh_yAKDqL8v+M|4@VnCcp2wV~q9bD=Y^%tF#-<=o=$o+VgLnRR<{Q<< z(3$vVnIds(H3BlsGw0GZrk{r zggw3jaqvNX+Ipsz$be#<@0L3-EC6>{m%z!mTLz6iwJI!#aYwp&D=%64(CT)vH&9kjXt|YD4k7`1Yhm-^OsZ zttM0W6agt(7>0$Co)eILs=I0Z(Ze%m;iQseb=Z8j%x&{?HH{ux!fWnn%F=XB$5$f# zIEtm`Q+)YT)fOeeYt_%u{IdT}me$qY<#0Egj)mhnrjc7gEtRZmSXT|K_gm%5(qZ=O zN%m3N3$m&2q4Tf`!kjg{?1wRN2XQ)!zRn~i4T3>DypCE7t&W`4$gpi8v#c^jIz*KZ zQ}l`l+Jq&-q_|*v^G2ti!EUWqHJql})Xg?Fs%fUEr}NuchZ%*f_B% zWM?v*t`B~-rjlFTab;w@r*MvGYC@pg#0RB}UsJF@{bm3Yc@Huzix5wI{5;Vz=xtx> z?QxKU@@ zZp_%w$Ba>&`c+OvM0pT0@T_A+*++5Jj9V?;;$gjL(^m*D>#`aOA%gwV)?+Mr*t>xY zNDPS53Y@a61xK0EQKh+R~1Bny(1ZL6ab{gWQ_@0cX>@^Fcn(+aU~aF!$ct=eZq)QaN*#D?pD4SXisl zTUutvDFJbsVC@jtk+G~|WmVs3Jz@Q5eVAPab;km)|J%`!MD5V;L+OZl=m%G zL96BxCTC@NeH?V{Z7IyB9oDoo+%Z8!7&enu$gcCKKv_v+z#Y0sJz9cX&%?T{rU)T* zbIT9tn(uJ}Q)NOL2AooY4*>2+g=Ok_GN9Cu4d#=M+ zk|{YdnBZv~OFLm|iLyN4r$~0+Qjp19fk^=Eu$5ZQW9-9u9fp0I;7B#K+hg(OyKwq2 z)I2<}Cws0zP+RAHM?=YsJj0+MgWdwRpKPrQLE+~R6!{G2SzYlL$jP0xmrHkz$%QngYI69YOO`ik@DsXTX0c?c-PYoBpkl(HpRry`@QG!?jYUqcaAvyTu17%AD zYco2ke-y;~I96^rQb6bmT#|WbrU@Tv4RgfdceDj>n5Sy!Y#o;NvhR>{^#0)#M?PP8JjPg2`D|7JE5TlFp*m>J`pmQ&A z?<2cCV8q-xpZO%CQ|~ns^o*|}m|L}0scMb;zelHT=YGQ_$isw*pq^ZXd-U`qZ0cQ8 zzH*EYs^4MD$TBzGInbO-Fc8cIxpLCERtY~qH#ax?WzX%>YK}nZwfCO-sP59mEosbj z55{a$7$Ba&(lf0W&k@2oJ?%#@OWF$Gizntx%)I;V%aG%UElm$jNV4W)9u zT@B`R?TqC&Nm$vEtpIJCaON7XOZ}W&dI>J=Qkb-wQK`x!(6aRg!IBZuST285yQb%R ziU{a^jO#i>%_=xms9Xdg7|6jXLHO>7Bp}!oZ=SwmBIVjj zd&?;K(cHQuP^FVB!@oSc3tn2dg6@Z3QiNI=+Jtb4xHURLE1d~t)H$b(K=QNRVGSPY zUu?Zgj7qKf{4#JawI$SZH!O~&$buX@1}@){{L}wFNgYQXnPo&Pc%1=3{Fu z6=Tf`zKJB&(oion&Wd%_osspL^*Cu)1&qYEqDHV z-5QX5KoUb<%R9s#KU8&->iWVn=`Bkjtxpz~+Ea?bU@Bg(2VU2{V<}qvwJ{vx#uZix z-*QhLRg2u;2iy!cG}BiMC&(%Q_s-~HbuQwiiQVkyJw#bDvZr?+*8m7&j%DxMF}FCX zLM40_^{D?;J!GYip4jEpCcOUh&I7t)(JO^3m?M?*{7K-iw8M&Fj?b4z*h{7aG6}7Z zC2Wl+K(e+tMG;Vj^`7)9iUiQ=xD**XuWAY+UrQgl@paf`zuaXsZRLyB2a_F9K>Y9V zmcQ)RC5^66WOyuPUr)mwI0<%20COVwU3OQr8r4zX$V)N{Q86~tul4Pcr^BiQwo&U4 z!Im0B5Yf+RnNSBS2E)9lO+~rQk#`X-(qh75vY_Tnw>16|J?KK*j*8PVQ$qN2%RS97G z04>}6up<9)n!5^ANpdp^a3CpuW?V4VYVF#4x4nmhK3nVGzCFh17Wv9_JoyLOLm^A> zI+Ulor=74;P%*r9dk;lVl12Rh1p0LE{{4A82HnH{61lhVYEXQ~l65|Q;poOLkR180 zWKV~k5LvT%_iJwRC~ne6jE}%r%~!2}{N2xEJ4-x;2a&oCT8^bJG8ZxamzH}fM+c+O zPohHGe$gTTgCDYK#e-Fs!&<;GreyIj-?oFw~#NT9ldcUJ1z>jJ^nq0DxX z;gszerTOog9-=ufdGljB&M5cb4#yP|pn+bZJBK=lQQ}%XCSZotQ6QJVBWL_wm$-$vXv}`hCDTcm?1X zk?`kOM#E#n*RfDVHbxxJ%t*W-XTxdox$V!#`nf#E`U4k9{(@XEw_2jqwyq~1JD9Ec z9xrAjZB2>^3$>M}LXVWtdzw9^9Y8Jj2*RztNPbmV&4+Ntexl9~vaxjOf6((WuDqo+ z|B}9X5*bw1d5vgAwZ0HzqC@9wp^=)C|CX>$fw;53!3)wY(@KzxyH=p>_^0gGp}GnM zN4C%?n#gujKs-(k?P*L$-xkhTJzO2%e{4bvbE&NMXI*|NI0-n5BWDA*RR)x=$aNlq zgiSQ~qy6DLxoc#7GV0{V+=I?ck-}jKM!M77!O(2&?qzvI=#}{Z#yC+0Cs4mP3rH3@ zoBPGcT6-#!zJL7|H%daHQ5pQlu5x&bVv;)?C=14xgK1jbDB$V1X!&(0{%CyU*a!*& z(6<+DRO-|S)yR+*gdRWwPFQg7;CA|c&D-7<&+?zGjr7MQFZ6Mzi(*P!5qhh+%j=t) zMe@0U)}XoDJ=NsP%Iv>4zeAa7Z~>;O!!~f<&E^1`b7v-|wIOBKyDpN3_O0MDqR9Vne1mGNZ^*JGBs!u!30HK)2v4c3HHKpJ+Rd zNEADTvI-;PjG&hktv3=)Mkyd{x)zE2 zgpOSKPH&=od*JXj&-*`c$HUt#lVS=3p*7)eD|}VkQ;dm-tjQwFAcBjjnW%DN0;_j0 zuHsvh@!&PLz7?tO)96#!@*}Op=#o?X(|A*eQ3p#oEwbdg$%T23%me-R&&&@Flcb5X zlAPAV>+9=X+vSv~PKg*^6P3tyMY)oM$Q!(!pL^WiGY*Xh3Q@gQK!D^XllJmpX z2f!c;ulTK*F3Ub-^yh8!Kw8HMeu*xqTq5_kit%f)JykEYTF0ZeEN>IB^56l7T=s;@ zW(q!)0P+i{+B9B-yH${DvXsBHw=`1pqEMavli|ASCLk_V~YNm_znnOXOUj zxY+QK2TRZou=1YpTA)L#Jk)VOW7HTuir~etB)SQh&4D&%B(_KIfXaC?!C&br9&(M? zWBRKr&qi~SD7&2;P32<(2UpZUM9EI<_Nr7zJ!o=q%A@?df6{cILY!#8aV6@)=lI7n zooC}7N34JmEW_iBXLY6 z6Pk?#mMR+-Jii#z+-VRjS$$T9O z+~V~C60g}#w|_VkvoCL%ggE-@L?mosb+)!j*Rah(#de-^PfK7I7a7U6O7f9wxpY72 z?X!C6x&>DW>8K$h?y0u1)baccRs5NQ_20N0Hq($SFm%r8tq)P>3205Kj;X=NZUbK7 zKM~FU?}3}-k%&mazK8glTuVZI-0E{{q3n;!?I7)~h)^GG;dzvD@vm9{n9AHZoNNd@ z&hphI#aN+D7c2eI4jVB~AO&|SlFz6J1}p&~u! zvvK3>yYi{H2dqRc=N{D~mcp;E!yN0C$J^o@)5XAunYLBo0z3BtLNnn9WcJ)yme05~ z%W~bmgx{c%IJuCch=&kJue+hj)=~VI!veIQ(bDMgj`*h>dlk0Av@Hp`&NHE^ogYkH zD{97||KwbIab_~QFtfhC#&q!9m-*jJTN8>FpDePsnMS%z(>W5{7)y-2hCxS+W>TzH zsBK5ay4Hl>jfGTwdj#A^*~i~<$?Jr=2TaF(Z5|rmq<3T--)Y-VTDh_^`aS$&*$9a1 zG9Sp&n5W6X-D*1~G@-x2HTw%6x|K;5XIRcb1F>pb8N1bG>J%OI2V1=hNWgms4>AQ+ zF!Lh!fO5%*u;j=H!@T%6LN416C0m@0e4h4K5$()WF$Tr^&xF%9R}@jEpnPQFSa#*mOd7@+hqZSRtysJR80xu1>>BSg#4fhU{(6ux?dHq<02))Y|x1>%M zwsn@CCJJwR{uxUxowKN(387YqnFa!1WV}Gn z2b}BySmcBU;`^ob?HxO$&I1l`X1HWMe7J*jD$Lm?@a@q8gD;@p_{PHvdlTz@2fP!p zP%3i=fE|Yhu^TtHv+@b$C?P7f*O@Cr37F0P*fy#{PA|xT(u36wLr>u?6cGGn@ZSnc zgAr^Xw@|rz#_f74Pks7JMqQKE1S$B{eIV&p_ZoXtt9xgw3edjSPYc}^M_!Q<JP41v`cf3viO4;c{zB+W9CBMrhI<(k{22s)eJDYEgR~9SjohK!QOcDIRkpdSWlo3 zX7q5Z$YA^WNcf|f32=3wo&kyU!h=x#X`H+S=hpvHwOs3+`PTb$R9e)LXvnp%$kGIQqwxbYSv|<8+d+B55`)N!& zH5^M>DQlP04;06jn)c=q#_@tGiut#BDiQw2f{fWw8 zDQb8afjG{!pN#x1-c0?nVCV;qup}kjbpwS@&;;81qbnmtT~jnAX_+<~VU@GIp)@^n zj*eS!qs6Spo%CEyvCQ1<_AWro$we%o%f+xxnh2gj2vff6d>)@oG29Gw96eQ2J-NFL zI?}*lA;_7tZHT+>kWvF;!mG&CN zg+H}PLz}hk`~%a@N}(wF`VBaqnZ|gH)RSHyK6G*KS?GAU(L4|kY-M!9wgyJE1Y~7fyTfIB4hf%lfoisnbt!{my*--TO}pIM;6k9X zxBQo0puC!_qf9w9NZBQ8cR-UN`s{$SQzY`TEAXL&N*sQV(8Kd(9)^y z(5rykp{g8N^qb5*q>JFMdylot#bCFF*t{A+A$)*?Kq^1_}ZVi69GEJJu`D+Xv zsEf_oFZY}Jyi29QWVKABXr={wp+lSgVLfq&cVH}7&g$3&((O7 z?42^*Pw42yO4QOlk(eZ7wdflsT>|nL;~~6?7-IH9`%R3uEVZi#rr@8PYJ;7y)B+w~oslST5<>yqiru}5OMzu@%Wj*fWE zy`Ysx5b#SltpF-#tuNPg*wdFDjJD(9+m7^DXB*<5fbL=m(yly)VqRz4B4kBS^>yFZ z>{r*?VpsZt+}z5JU#Mm{Ti_wiRI`T@19-<@t{$ImTFl>gHliJ-7`_#vb&pbH$`dHg z8V9l&Ue@vCT|NY|X5vn^m-dj}d+T^2wVp;_74MU+%|F*0o4Rt=;AtwH!M55m5!#+( z-kjLH8>R2TxYQ>#ZpVtg_VHsGAW+-hXAm(l+d zs{MAjRI&nkRQX`R*bhMKh4b}BGY3^6>BR@=xL4(BSS5rvIr%EpQyycNDN#8l&eX&L z<|rP;t*Yr0h67=_HLbS27oi(*o|vH=0YMwx{(u)Mp%_~KgS~;5%!D+kh1EPG!wAo~ zWDnqSq_{9YjLlG*W9(I4}m!5yMM}>p-*){{w-8K)%B7vkVhp; zif(6?HV_3Cap`4eHPZt$O#f+#SrYrz&X>GaDYxwJBS!msB7&@dK90g);W>qiy4X-F zRBo!9!+;xov9YdjhbS72Ht z`r;$Qbvm)0fbG!(YN0LAHM~X}l$4lQM!Y%2hGTSLek@{5s8#bbP`-Dtx+?S!R#oSK z3mk9$PI~Eiyc#ebzPddS{Q`hCE!?j+OwS#*K>1>+y?c39x^!LiR4;Son=P?%wr@Tg zZLU<2v`*@zA%rEhw#lAWsZZ<)B(GPJ91f7m(~3^$dF={Cp!=#{U%Pp;2ZFy62V9!- zUH%oG5FI3X+w*o0x>^XHy9s20ugyl#a^wO>@j}Q{H!gThs~{21(;0PXzL4GA4pe@COF8%ji3Aaj#X6X#SNg_(=m>jIbK-F zD_h%*{mtnf(ukkx7>*p{gsr2@s=MAm(jFXh_ycGCajc?}UO+{-Ia6*&;k-v8Tksp` zm9IU&jLv^q?%yW#i^u;2+<%?xMXXAuU~&e(3I`}uMNRL;Z&@6MD+n}u0D^+j{f^r8 zPq%=UC&$|Ssat={<9sLwMSyGR>y8nRJa}?Ii&Yri@*_+9BdDGI1N(iF4@ANS< z@c;Sh-$VUZGq(z=10Y>@z`NSh!)2hNe3j5iugb4^dQ}rX5)yI;)N}K4nA-7Df6g!Z zKwZ0|yA};$=kW*LO@3?t1&wT}(yyNZvo8~;!bKl^>rmD{0;*!Tf#?PgF*88PqXUqi z_Gy_6+X*f8F~AL-L!0N3Ns{vTNb1nu6X5e%K7Xc9|2lmBuM}dy<&~3@lVVJH|2|2C zX7rz1Q`dlnyXcJ}{eqvW3emSb{E-t!t&I4$jH97eLxt^Mq*cU7* zd{4^2F|-0GTOeCY*qcw2QHgH;0Z#u;&;N|5fb%dQzubvL`o$(3QHyI3`7p};nm{3= z7f>I~<7jsk5eE{#E@j&c6!bgKFXIVHA^mZkz2Lav-gYAxB;>lq4S>4t>|?G(LAw9) z>JAMmP3^6`kc)9qqN{)!a=bfYbM70&T(sF-X^4^xr4;l4i(4pM(whgy8Lp){U zfaVpOKyS8Rcl!Wm##gdiJm||YV8q3k*G8Tn=l|^$Ghql8T>^x0q#gubN*>k-D`0US z%3M3?tmEvRl-+?y-sF z!N1ug(Pj%=Z>f#ADO++-iloC={11uqZx&LgaV;vS9V8c&l#~D* zl1?SdJu;v+cHsPAGD)RBhGwYIe&ztA;@_a|kNtgkv&BAA=QdOHi##*7${8~Et=vI9 z)XY0TJj}at073c?Hdl2J9ZBc8U+u%j<`UblK#%{?+E$W9tvNa%-szMg!oq9`;?BP> z8uu+JVTm3-lb*W>VCXtxTmWu3(1|VrjttzA#DjIG1mJ+FWf$0pR1+_Fp^5h~){ctn zf4Qa$Y)YFap|fo;qD7#-J2OdiB%alV^YAW!))L3PTQzN&*u)`)gT#N*#=mDr*f?Sn zfHe*-UHFmOkzUyIw91V5PgCR16Y^RmGV_?S6cSwoNKQ5-Muwe5ZZWtF$lDBO+gw=q zbw}#Y?}*dv64{?*?uF&mptuLe57!r$eX`f#NzO_Vs-UQdr!?SO25yck+pk$$4NvPX%b`d!#tS z)2iA2N0II$5YSE-K3Jzu7O2G?cMc37k^{MV zu+9P7Ms8Vs`9ELyCt3fKhlIA?xQ62F}sVCL_{y;ujtV%UB4;X)$`h`528dF?si7A*zjv;{x+U-&bnSB3r)%foS6 zI}xW11MNpHb9VDDQGl_mJiTOC`fr={Pp16)JG-hAz!3uNd*Kxo*$bzOi{_0wCckv@ zUnf*1`w8ySwYgUf4S8V=M*BRGNmO}%0C<@G_--8m9-#kBM&tlW&99&EQqscEUyp{&jPVN{x&2cP882u)(zi!f>1N_$yU9#sC4@k5m5l`zZI?x4r zgXP8hK@}qmKy$EpfK8OMe3$(_n;(PK5#Y@5Kh3{?M^{Mdnd)4u^f)YJ1_0gm0S7yS z`pYIf=YqjOed2@eYP82+@IjaI#PM;JVQa+8gUD6@rk?kz|1-jND0Pr$g`w^xo zxpg_=_|53z;-cs$SL$$X#^q(C7U9oocEToSxtTE@EKivE2 zHQN*v6jWNV8VG;?=5Me6k7fLIX1_l&5cC2f5@y>7o2rlJm5_w05&P@O7ODWQ``cI? zieCDjC#7s@0%(9N+w6VhXW8jE8H<$LYmGFR++19q^ZT7EM}VR!vhH@nKKM7P_Nv6b zjdTYv%wW!HFX&X(dKIy}WpTTy*N=^EuzluNM(|F@uj;Tg}W?3n0{8=@0mQ=@B3?T#|!8)p|hNoc{npeD{@(%b9&T7njHurS$ZCUr%tF(m1CI!|9|i8 zKbw0Q-i$*F?vo0juFVFZi$ThJJpR7-qM!WE%jCQ2U40}VAZz3Rjjv$IH;vJ`>wMUp z%Eq0o__(3t>qUZ-$w*=p_2R{g!B{J1(gI(AHZNW58o*`)g_NAnSo(*gj5 z_v*W}vViD5X0ea6IL0iCX&-ed8XkLrhP|68Fz6wP}wX6WV z;5=ev`u#1|Dfp58z1IGqiZAJjGCyX24BP{xC#Cs=KM4>heHPlKyBB75K)J6>_9a;c zhIptMxJ2w>Z?6uT7Mzz^aqzh_}RnN&vSh+506tI z&Q8ev&@Pz2x0eSBMj|g4*^)ly zKMzDBvd$BjTID)Ij12&s{V)R^9>^(EVBaA>d#AIhKJs zRg?i#+{Av#-E+PNNhNaRFb?J0a5*Fj^S#V~c0GN7hHpE-U}2uJgRc3##`%T06KU=6 z1qWN(U{7u)rqL_$bOCMvK-s${%PuTIRZBNieX1=BXcyMgO67bkM_%;F!Z}|nJMu2k zynPS^#PHw;9bh{|5d=LnmN#}sxAAT^_*Vn5G7)UsKrkK>Lv)_vfFXVhX;%PLKyy1j~?0dzl(bK(ijv z)~~Rf8ra#)4~+hcdF$7b|6Ke3=fOey)54WZ6<=CA2H_k#wsTeL7fGEzcqQC2%g4tL z?L|lPT)fjR0*-WhE4XEWS%1R@Cw5MA!x(Y84>4$&aY=jUsX~AQ8{z~MV1cqH_CI5i zfI&nVyegI-OBd_G8+y%Oj3_9QD~0Ub73>?noUtNzl*`wg)8#Amj(gi%(ll zK*QK<`K=YqJdzMml~oinmd;u4U<)rRTGrPaFhcfb%?z1r=V#z*mOs#Ha^}&DFX1&- zqYZLWO?}zz_l_o`#C+(bOcVnub3PcBKxyAN)X5@Uxz>YFZdc>+n8rmXw+nNh&2h3Q zl#rWeBf8qIwF$!x`@`jnr%2>Yi@|m}G`Q64=&_QLrDyP$f1$#^@6F!`^lu;4UAYW| z9Y{WRS+g)-iy+VP=>HA&{@7ma2f@pi05_q2VE?@`l#g?(;Sj&TKqPkbxuaF^>SsgC zb5ms9XYA9etjpSLe|Z&PooI}umnzS^2yZDKc^v6O#g1iu4b+`O#2pBdg&F`&jt<+| zYk`8n)Xe;>6gFB};8&S|`=Dd0xNDP*D{%OIb#@i$J)kABYOzamrFC4okz+Uhclv0j zw=G1uR)oA}n!Z}Oakb_vx#$dCr|s^tMo>gd47-QVW{q(`l_KeQ(uNOd&% zdYSfq85^|&eaKf_5BYEl!pH=5K?lA)emCObhE6>_z5H5>wx-LWc*lVIw&CXB>cyqu zr1vI1q><8|>CzAl3ohAw1Qx3%?RoU`g3xN#oFvrtaCE(JwKH%rf?U zqm=0);O9{pz&W_)ljmVo+#|)3`Be5LK>$AAWmWK?QEh7((gyF4CA<;r1 zg^9V^t)~?X!%lUCH8&Yj#=)*ksae_$Z8N9a6ldksx7JgV*ZJ|s)r%5yjjAm3!P@y6 zy=sVCqU&;Atqjx;-*|H*ZTjFzv3+-UcZrWQkMrR$e(qJfZ$n}LE_5o_p)UKj#$NT@ zV4jw(X;b*()Qt^x(yS>`YwkZM1lU#@%RFG6{fHPPY1}zN8y4k)9%b7GG1F)W#PS5m zpKSaI9AZg%h}W29&P1MqygcaT753|LT>|goDgw9k=KXI)^IwCFMqj)e^>0IE*wctB zsL;kiO$g5llj{U6Z(=+FNA5jc{scx3806N|nbF#sm^$uWuuS(TOx_d9q2*0^Jl-+p zM~wnGLT5E*D96ep4aRq;sHaz_SK;Hu?)dZfiVsH@cj}zxTBEvZteqEA_?Bm9X9vZ4=}hOe1p3jP_$)mW|T1Z9iYIo&!&u_P@qNeFK|#qkwdrHP5VSk`+M zN9g8Rtgo!tHF@;TZ1X9VRFSFo46~R@O*~Q}@_&2%du8<4~qjLw}b){rEhk zv}uRhGCZf5w5AQNUKZYsk1|DC^HlIliO|vn-0R5?433VTUSh9{2p$Q?dMD9nnNu7D^h{v9MHqA##Ax&ZPh! zp2unv^M)ee%?!#))&Z8l_~!QA!EO5yT-dv{sj5jwa%%)j+})ZiTO_7qU!lGCV6m+_ z_x1aB9esV+fUAqkBS9IZZR;UsvUq79;0Xoc10hPfD}1nwq&Me2D@34agzZ=p66533;_a(H>uI)5%ir6KE12OROpTvlAm-#` z?sh1uaXfXoLrxdMVLdNxx@H<$T#>fo>M!+T$Bo=KrdXG%DH)MsLqi&SUl}t~d$OUn zR_BY8$Hf;O6XiyRO+QrT@4o<@oXPrUO3PBw+TLZy!QXFjDCj_}iPEmUotJNZ?Sk$+ zcODF@8SsPPh6?$G5KDBcPmhj{IOQqG6;QNeIhkpJ_2Og zT!orHrCmBGLD^`$Sbt4+UnBknTBn)jdN*V?GJk--bFk~l6ufQ-tXH$pX4Z zO7)$tRnTdKp)W+jW`Cdl_(Kx^Y(oF$trS+^w7N)fIK!*KUBu)1yrBuD3^|*~^R;5k zOGrov(AKVeA}TyGT8r7h#r(`~1hO>Z$TXJ7H-&Krx4u}^?C-x4v)8H+c_*}QHXcHkauezmKI%0y4%4KUsutg`0Xpz8&wWnGjW%lx@U4AC95&| zCP3cq;sUazuk5)k4;v~gFV|A|s6HxcRW%Mj4_A%&k0|SJp7ZBxs<|4hlMX6E*)>uN z@`X~5c^=Mdk$kZOR?pahiMp0Y;z_D+RwU4f8ekwfoE{z{gTNp%@z&~;OnXj6J$=)v zdgL5aLSmxEi{SE#lB)Gcf6rgj^KA!)V3DfU&g<8i>?#P@XzA|@U{Zyu;0RQ0vWA8h z>W*!1*2b;&f`^PjQ!Ebd37F8cn}*t?@IBeb++4TrMY0}(4Vht@Uk# z_+95ThgK|WMCl>0o>7V3R-7fP7S}RB9H7n^xIY6nj-1kbFrRh$F}i9N-W=rEBzm3u zBa2VADMa0*`>U+a+VbOys$vJT^4C^0L$a&3v_7R*C-_nzgL2ntq$d1hU>`IaqU~Nj znebMx6_sz$wbhY2`G30HKf5*PP2AAaA*E}x?YFE7yG_|nub}>v+3b}06mUmB&={`AV*%@h}pvE_*$}*@4>I-h-mEJ z@&jQRYct&bC0GG{SJr-gHC$8V3fJ4)u*aZi z?F9-3YP~DPR*?xZAEk-|EfO}vrHC|n!Eel0uEU#nizdXEWVe<-8P>0`Sr`)DqdY_- zYa(%c8o~_~FYUzP8*6a6a4Q?)!xi6(kBFD(>!PZfQin`8%oS8gK8bDxGAE+L>#K~7 z^wG%63;Cs8pgjRsf^j%{pJKBP!a|n(gR5>touNn32Th@w?5+hFU!Pn7Yt^S%i zg}fQhUk!SpJ6W`seKbT*#{~sSOc4AV7s9W9vXKopbYH74P$w%|o6J;c7n1UnqbR~R z6E(b&+i6V16k67eSE=(s=CuQ@dH?pdYqFB9t~%^ZG;($xr(cf|p||^r*i_OuexND+ z&`x)AhOt>BsoAtM__p%C&;%60V89 zkr9@5%SvDH~a1d@dkW2pvM`o#=)v0YJ`h+*ADJh zBVlD>44eS?W2%cR3u%7%P#kA;ZI+~7*yZxdJ(fC-fNa1)uGewreHc7$VS`a^lDtA= zaMi+DJxvrZ#9%Y>%z|Fqd8)q7!H$S;Assu06IHO0uzfw}SSM`>)Tg!dLLKeH8PXhs z+Ozw9Xw|phQ_~RJ8a&;<&%ORTmr+Ox+MN}TdCiYg6u{~S<}fu;0HwRb(Z45x8|pH` z^jtvZj);g{-(seDs$7yanQBju=<*)s&`@z#3+4Ms$-`UDs7*zeg1d-UG z)K7w9P!6ubw8r9B4BBt5pfMN+bShZ#Y-)cQgGVl!Zr?F4kuvlGf!EN`5Rap#qHeCW zQ^z{be$1M3_A80<1rUYx=M`KFW@Gj#|-*VIMPz=a9J0DoQYjpcx#1{KMh7JvLH zZUo^b4eGcYn-+=DhgD)u!ggU_>O%T8U`-ybuG`%;=z?9md0z>V`#La~r*|xxBZO`G z)0MIPo%jNJXvMV`n_cqW}rl(^XF6nPW=`IfF-aq}^5#hLHBXGm?YFflO^O8sZjcl?Ea zlH$MhjK!|grH)7*`P3aJ zWWJI(dPi9L)r@y?XID(L_Bap3n5Z z(fFGQW22*&)MAn1=BjS)`=qX&axW>)`h3=_;CI^Y@9ph{Lq=+2&kL!-pMlm9XT>~f z#{53-R$T;yEm%o+KA1*{mS#cYPraId7EEtQ0Na0%Ybo->H4a)r;lb*6K?#$;ninryKM07MW?w_eF++S#e&17R@x}05?lx>vfJi?yI$F0=2qib}lZ1 zX@kkKe60lVNq~7Im!aNhb>Zb4)~t^4m7BGR3HiD#?k;$zmCb2og=V3l;*oO{X@m85 zfE{qRuv{(kN@q9@l6mbvus8pUNdJdlFi-$f1k0^{19E?4!O%J~$q!bv z;<%*d8R8h$(QCnyQ%CtDhylLkAvGRc3=Rqm)XI+knugoO=wD@amm)fiU{xHm{OB8K zE-UAseFsqg-S59GF6Lf~+1}gp_}=_Em8Z)~T>voz_Zbqo&~UA*vPIaa*pZkt>&uc{ z+c~b?jmKfqLAI2ls_f_)QJ%ihA#ESerAD z?k2*;nHBz)Jpad;|NC!^Fav1)&%zEi$w2WX*rT;-XbIzkd2uPRR6grG|>>aa;i(~9U2=jCgUZ@-eeuKi$B zO#o%aw(t3}l)i!D%M;xoB}So-fN+JppL$Xt7Y!=LKhUm!iue9^%?CHt3rt=@^N{lK zFIy?o*+r)La}vSX^YpjTIB#O)zFIvtCAs6S5omG7vEXT6sJZprSC&pKnzDU|d|{>b zw2@WW$FIq=30mdC$h@6B>^(h^MaG+tLH`0tm|^_TeDlIma!l+8^l^`vmwhDdF*?F`oR6t_*kU-SpQIa#TZ1jTmxV(suQBa_ z4szy@_{*ACW}M~w$wYZP67H$Z{QZEIg}h&1w^C~1VEyy;kkR&&1=rZ>t0^|S21RT= zmDJ4j4j(tyBuvVA-z-JnD7hu#mM z^EPyayEvrg3GIgIHRns!L}bG);3h{v4^Y=<%oFnN%086_Edj}Ag4*uPP!#Aqz=;HW zj1xRtE7>g`J%9H3GJP7A{3i8MGAixqfYs(XT3T8@40+m(AjDQ_t)p?I?_HQ%KI|yA zd#Jk+J;Yp$n%Ve>uNjfHd;|&6TsG-u5ieTqEJoFPjySv{EwOD1&Y6X2{`}~EXuD3p z!`45z0Dk=E|MJ2>1%!P347OFrG60767=jfMshx}t0$OC|eJe(01aN&-MR{g2`IjyPk_N&YbXC{vKVVkZHuQ{%TLo=7 zRW|3rUhkf_L!#mN`^h^agc5A#NPst9YD)Jm+s(Eb=4-ZD+uFKq_f6cBld@Wzx8hR% zAXTd6?<4%!U+y10_Wx3+AcO)C1}VnDhag*gN!29*CvCzsb@8j~cwgBKXf2pX7hg+A zNFa+yto+^%x@RS^)9JUe9I?kQl%c_@024bPYLLuh_Al znuf-yz{6X*%djH)om#bXvG`kJa#TGE_&6&W?k^)$U)BC_NB_vX{ch?13nl;v0u|Y# zbHwPu@HrzrAwDa{Ch3xQKzQ|HwuS5z07<-mbL-_3L*`=k)&7Dg5xJVoVe|ro;l3+F zNsZ^x8*MlhS1g*`z+`9cqc2_Es{quNd?#>y1b>JNgy0_lcrdMGokgFRP!Hs$o-w9+ z|LTbzf&%VHc z)>mYULyb)#=d(m_-+t6FC_IOH3FC_szN74j-b8iLSqNByXzcFEo0a z6RDUQ@`SOB+qL}1zG@m zq4+)V(=?X(W*@&AmAQ9e@(2(MMMwxngnc;y-%Epm^{CmWo?dzweRg&O$9;ERL3CfW zZ}a`szBr77^?p~-!_k-Kg8znm{h@a}z+eR6{`8y&3maXa zB~<9a3;xTrpQ^}t~CegSy0>6{WKCR_H;LQKG=kV)q8Jq_b z03YvQ!J7fSHS>1>^5M5db(BxNX#-&(#ws?$aD-Jc#>c}leC=#a;=Xw6lLKLbjqVIRuE!3JH3f}578;cG}zy3cQ;r~})3pu{eV5HK2zAmT*6i(eg zkGJ&ktyBZMUr_=b6GUtS{%HwVc%sVn^z71inG|xgD~g?xlaSBZ#U{&sRChoo`bcW- zsYOA#`$Vi;bw!D1mC*Klu5+wZKBstneLY8fTB!$_66F)Uo)EY?%fA|q3(n~5s%eP*3*;$_({dMlM zFV*kMM)HnIB~NM%1ESus=ijsr|1~W3B6k4^67hr0(ZOU3Qj|6H#UZ1`#^Rz?MU8;n zvfs6cD|;(Zl`lQXG-8J1=)T_Zudzf;N=TsdH4qu5X=mY~XjXp3rIiJsUL=lpBqtac z3(miVvuF};tN8$Mc*l2<3da=5v7=c*ylPmtt!Q}Q2s~~D9q=E>=s&D5w!r*tPxW6a zz90=q#UY@S@3I=NTAQU66gDTRa5WUKDqRxjS~F&r{!Gi+z6EAmT1h zUZRnZIc(-1ImnBzc6vNq9^kV!P=86zY9n~NV#st_ibc0)+?PF0X%plpXeX2V^2=zV z@MXPk$;KA)x!dF2911(O3A&NV{|DgJUti9mFQ`o-9|gG`bG{|X8Hn~OjXbDzt`G~m z5u4k;Jk~HX#t7j(oVMGP)uRwk3irHHz8=#24M1W2$vLYFnjYZ)X4)~KQf+7yjN|=h zmM|h`&$|mMJNyUJ(C=9Pjm(H1VvZW*12k04y9efjDt%x z%3XlJK(AZAH7Rv)jrc!$ENr{+!)KLifEv|Cz7YEl#0K3LwWMp+{8e$J4o=p=_S_d9 z;kGlXsbAhatKPycHyc$|8?^?oaOC9V{-PD*QcAc_-TFiZt21}c(+W#w+deN5>WVN) zeVR!36~rNd?QP2NT6e-Lw6rF_jB2yt?eN$0@CaBhzzerwtxp5=_&ONh)(kmB27cRy zIP`n~r8$u9vLDoLN|A_+J*h-#_uC&$8cc~UdnT9&J*(1?`me|Mai0J2#UoIp&{K55 z!p<~`63|1}6A!*yG2w>N$Kv*bfNxT&_zC3pBWEUc4LPQZSK=f(<3_lygnS4EUSYVI_qey4|Empz9=@3s7 z3Dimqk6kCIxr$#!u1M|dfv(-NB`Z{w-;L2|#jRw$f43IX55mcuxg&b6-*3;r5;rsi zEVK9Yzs8Ok@>`WFH@|2SmRa*^rC7TtaF{B(Y(TFu=j0js!A(#+M6%XaS4+RJ+gxpD zC@~?~sTEF}+6Jjudrmti1EEl!R)RfZZIJ}DBA4CDJl$~WM+kFkcMeM+hlL@=Z{m|O zPEE8ZlD7w6*HtL83P^v1?m`!~g`KcmDGTzv)He!6$F52o6LL40uqM!U;{$ah# z3TjgimzDdP_e}{f#s3jzm!({l^Km+&o^7~5Mn;CBUa@a^3(P^+Tt$j&D+sNfPl&17 zzzC)Nt1XSQ+CZTSKWi)>OX8j~T~UNMv;lL{n0Ql~?j-CqGBsxUs(GEp{obUTK#_@un@O1w@leKd zNQ{d!R^0nmb4QGzHeXb?#O(}!f)=78fdRl`_&FM^4L6>T=oQEP@yLHX`+xFcl==G- z6-?_`ke)R*;!1yfTnq7_Q+#BC;Y6fBaN z@bv%S&$-DO@&)EivsRlaMMXtrlwG~5Jkboxf7JJqHDD2qkWB@F48VS`e*?@5VC5Oz zyWNLNve|HN(eP%Au}Y+c;Q@=X;8=Nid9&&)1C~!ZzViQ*=LWz2R2MuwClV|&-Lr}Y zPoAV@YTT>#uQXA+El}d_?%q0`f6qjylQTi>3b|{I^GBAT15mv0A?xny@@oq^gXpHw z)7GZSbD9Zz!GaL&=e60*}?5uNe76q8jSr(L?w>YI1Cp)cO7 z@7gIC7*oj>SXKROVm~ zh0^mP%S!{Z-A=fC{QT$7otqDrjh?(SK?5?S)!Av0hcW!Te=oe;#w+lxLa|*4M@F>+WtyzCpxpxW+Hu~3r#@~}w zE-t9;`CQTJKj_@R=QRf4u(Go9b6hF;j;w(g>Cx=Th(l9*dnCg^OMGR0t~Y_nk*TDz zf_SEvf<-%8ANoRvoc91Un=(DlAjZ5wGgyX|vkikTT`_m1REaUr8y*vCm)HebH!?CJ zBAlvkG(YVX=M#=f((vxx^jW{j}WHu0|6LD z;K``rX>c+~Ppiroe_x-b4h#(FlCfJW?)+)v z4aSTb`POV%8xzG#4?Ucn)k{{Xi~O~2WtV$NLgwNxgHgLZV#XH}k8k#xsP>|eDkIw7u>_A)I1OxZo@Va&hOHOaOU2HR)w_RUuv*ErUu ze?e6hK|2&e!|>!-YR=wG;xXiIEfFV&c1Z_dkK zn?E@4dR_(k0{}t0cO#*OB?IF|Uswc|M(_%#UlP|vG;H6UBX;kbE-NeRSzA$nZ-Ls+ z$`g(TE!9f9maQpvK6gC^NAduf5+c&Ph>u3ij_&Tepbq}I!`NY_5#P{p&f%yG;+5pEkS zpYULqLone58jRarm2}0PT?XH zIPw$AJkzp>UZJM$Ggq#zDk)JZF^+$na*LZl?%Xn9o63m( z5`TYcwossLqAOJR;iZo*p|qq+j#>kTW~n(uy2WWf7@bNmqhwmx)zxJ)I*NHIMV*kE zVQFcJxnFHbJ4sUMQ5RpqE;W&HB_=MA{+*DKopuhRPQul$ceg}e(KL%ZWO)VK*%hB} zm1DGc*r-1=3sThxh6%7(%GTpV!M_Q5`2~6tnDozIU5=l8V-jvQ+jR1aPU8f|e77CV z9Bzm$nBrneuJCFIY-Jmb+d?VO%w59x7T<}8j;27>B#2X3V zhnza685n9!TB-ciigpE_jcv_GyfoiyZf=fviSTCM8MztOKv`9Ew}t@Of2Gqv{Z;3l zTl|xEk;adF6dm-$89#=Y_Zr-~Bv)?5EVOu!=tbARd1$d3oH3gU!7^-G22&`$X7n48 zC5IqE3vF{rh`r33S@j)qT+Mp`28^=)^l^a6Oc^FLfESp3&|et4yvdD;4>7YkFUSrl0qE zjm2`%Sb#@~+rXG#%^ZtCP{odzzB$D%eOgXpVdmR_66klaSvO)Va;(q*twC?h2Q9fJ zTR?*GZUuFGf66Me8Vh21^KX;45c+9dcE$2!&Sg!Dd0|(S=PMfIKe(9p+3)8eBxv6iR$;fD;?P9}UJa4ET z0UL66$D}(f@o-5kgU9!W!TR+@DaThD$A#1z$676B;DXb?c?yO`Qt2VqU;hF%!RfD{ z(+&F~+pYDJzvR;UIzE04kBq%iR&sMo^BY!6(&wUCuwYE@M%tU`HFT zg6ysw%!q!*inv4*zPH)@x|wWrtF6^hlmwy&FsohmTr@pv?Lc|%tBEtIT*B6+2p|Y^ zOdq;;=`2M5u+Bf9T2|9wPGu}O@m5vmu}EKjE#`Fht50_u$8~DS87HHwc(p!yErtkl ztazafH#>+h^3MLwo%gihGG0Hk4T;77=~l#&bV2KJ$t^qJ$Db4%!=wD9$#_`0TxU_{ zi79cEh_zmERtQV&oHjm+0>-PRIs$!7@)N_mSg#`4&q2&Duf%^2*y;I*Su6Xz&L5U> zuqObB2~^ak`{RixVH#tDgOz&=ml720jeO2Jj#oh-5Z$-R4ypFLuVNT-9z)QSFiV+l z$rFzbq>R3DocxWw`sZ(XC8rSi__-a%D2fL@>zTC() zX!L29Qz9hJR6j6ZfGn&2=q|GP71#Rh^BBaEu0!W7Q^7Xh*BXF_^Vr=ydJ4T^m+AGQ zhpQ6kkr6fK*L{4yzClbEiocTOTB0hdS8gNZrjviy^oodEpZY63KyZkhEf|DCS6)KGj|e7=$`jsYB|0G0TfFu+f^0qJt`_BO1g+HWDaRmO zrk58NC-d?BHn9h|EnNa41v?0=*b@o04cL>_y1Uq zb@Y7=3~>6ND77DUBKQwh=n7Bcpzd;hU|zV6C1NCJRIiY;A#3;zMbyCn6&KK)>$+iF zQ78I|v4rUKx^z;t(WuWUf2SqCV6>jxy&p8ZU?j9kEIH2qv3CwWC^R^sioLA9g51W< z;Za{)`8{)okO%wpW9{r6g?=%0XKzK#q^-OU==D2wec-Xg{3{*Pl#uR;@;pR$RoDB} z_g8nCm-6%Skd=EoAE=$1sWBmYl zHj&IVs-&iN52F3PKE*<0&PQzS)g=HR@ig1u8ZNA^)+*Hw81nvH>)4e2eq~#lC`F&! z)uLQ!1z9frbh_-J++^y}jvybPQf0d8e*AdGil(IpNFB;>4K#fVI{xBOyFR}}s ziR>f<+c-dff$E=89lPr+_nhFkbbqA9d4zkb_-j&}T3WeO9fWf>o*Y@nggn6X@bU3c z^4Qth7QEM5E0ac108~+Ac~z6*Fur+7RXeEAt|Zq*u+yY$v+GmBMHnqyy@*ZvR~OaA z4~#a(ZgT3*>4v^9U^CydRNA%mCst_pw+}-M3{dh{zV{O_n^CGB2O-R8ius-NC zTU;YR*}`X%zo!{qCYYmbzCDlkLuCQI+6!s&CQEg-nM=s8-EaoU(#91HaqYAH)^u^D zLz9k&+3W|RJkWVu*L^+SsCyVz{l-wx0l>I>Qh7xpI;y(rN6}0wm=W)ho&simfhF6O zbUdfYoS?P&aOw!D>%cNgG;B;t`DCt8l-DvJiY#^PHLM++Ag;8&h#KteC_OtBz^RE< zc>L+64mU`g5@sE7AxEobDf*MR2$+oE5udp^N-Rqj2QEKf8 z8yz%QuN76O-&<`z;7OO4k*3((*l5d>!}y;>ea*YLO&HGbP0vG-y@3LFx?bR zNLuVh^%nj;auq@Kfvr69gHzPGpJUD5$=P+Iy8LKa=WhUGb^a^B0kfR1ZCtD-cTO+10+*t+7RTynO>(Ybre4gT! zf(eJj0u8Ef$!2p4ypE-lf?g6-7DdC!-r~lo`5BEtlq#jM#(7rNUORhctbYZH12Co0#)x-k=0#i_ZYpLdm0l*=3J0K2U)HcEzdTT$fDo(kLS3i&m- z)e^m!!5I)y3xYrZ&mtCEoEpbZtFqke4Z1k*1AN+@x6k>Gy=xRyti*B7!+Hy}pt;R# zKF10R3bs8tlSU;aY3Bc~8f;!+wh&7;k(r3~pSRW70&X+_Q8w-b1=XSj!q052n+_gz z-ftD=WcqbKS>{J%PO)xbAg=;fQx>`t7a-DC!m>SRj2W-w==Rj3vRP%cgdgi?xqi@} zxtUsX(3k!6h)Y!EMt~0|a@~5Q?62A_vJwV!s5&OmEyMDN5_s=Wa^CR)=(rkisu;7Y z!!4I$clSGNB}(JOP6EZFO49s&JKND!K=Ft(>pe>j#twSmb!m8W#Wok|6jkW-`3mZ9 z&&9lmdHlypso&%bI7xt#W+{m}!FjKGIga*^6{6s$m(#`MtJF-9(lxN&ZP4Ci)6MCg zBI&NHsToOn35E`d2CA(i0E}-$uiDO4l%%@1Z$YVnzkKXN3=YL?-1P#@+fA#aG#T&( z2c+h$&#l)ES>F$U8#6gIDKw(*ePBGT(<)uRy0B&4IcZb|4@Lt{)qv$DSrENJd}#$#e{CZ8>o)8xCI)qLX%UPmeN# z)F^B!U#gP$^F1u7JzAeC`9L=GDH-2aaZpTsq4XpkUZg?+I@z&qRH3tt_%79sJ3LzJb>vc7hlCvkJF&^6jKhBC}H$MK8p z*@>>t#TZCfX_aG27(@j-HpG%7F#~((rS@c!Z0Y5V3K|UQ6)L%S-71dO7RHc`05v^I__$GjR#~kR_3C- zc9!BFi%)(tnT0JlYIiz5;Mamq`ebXt@{b4R&k@XjFJO!i^pe^_yq8$ z@w`E2QddeuXaluw9Y~2%ff4W9^+R=@PM~<2X${*9?D2z{BF(-vs#|A>p=U2o~yC{;S@luu?9@pHS4_v z(S$J$Ge9k%O9%>crX(PZ8`j+aapoiKsuknV$qEHQnKS;_5S0*f>h41_!ef)i=GT~` zQ4r(AyPOWl;{jK-NzSHm-h)|C1?02r_)s4}-L0nk{bqfKN8Ru%sf6Guxa>yN?TCY; zrK%encU|`4bTl=ymJ`u$@4W&I$7Y1ETUpHuv{kZKi;LDB%0MN~hXK@Usb1%{qNK^z z()Mn4>L&{q*Em+ICwHefit+d(v@LRS*vAY^&`S(l%Qp$OFw>-@7a_Tu`rEP^V_QaB!fTfLsbHIjs%|DS=kB!b zfGtJ`pe9VxH!_kr4yPYi;)7gkv89e*AOzU+0!}MSslYYYzTCXwm)wHlAA8JqEz4`= zi|#CTX$%XHyY6TVdu$^>fY#oC+TLpmpq4h(;vlVdAi*-K*xu?FsMi^g+g9vPB&J%g zc{wK~)@q6WuxbXs;DC+dRdFWUHRt)f*O^Yv269|5V*}MCBSF^VgVS+ILIE5mLtiNI z&||ygl+R+4JTcuDKe2jz?IyfR#&@I&Kao9v_LBWKX%2mnF?JUSeaOOzhL56 zHoaJaArAs3-mN8M3w{=b#JsVP<9Lc99?3UYJ%avPA3)4XdbudIz&Z)BGDNQ9*@IBe3ZMpZD z_{6ZZ?f6Rrx#N1%I1uDr?P$xw^w_VfNw!#g0sERHaQb!|d+(z3A;_u^pbq5^zw#0Mw~v ztiTsakx)JiqFlcx2|3A=b8Jd4hXWTXcTqqjNB~LD9Z&zRCZ!?-OsjFmJOcen_8VhP z-%2y8iPaHQG4}h>?5?t|DF}X;*)ZI3c=Ay1+=qI-nX|CbNI4z$qYW<3uNG6yy%qBD zGq2dw^p^@aq)XT2`^1GT?Sy6RT%Az!wcl^f@JZt?36$!*m1YAFBfORg@wwY@BciKeifWIw#hEpy3Of004nWK@H zK-}icPu(LihTscP!WWlrNnJ+%!u8Veyuk%0=#SJ5z#>7bIV?4Iy0f1;zNkHofZ#Bm z{#!M;%%ctwWsAp^l=I(uQuvr2xvqN{C~!!$bro6y%t)Fx7&w@fYQaQAB3c>c+d=kN zQo&d#RFZQhs9w3d&#SYaK>SoI^+g~35?66vMDCh&5r~%^zqNg!^6s?q20+sgXP;=- ziF7Kq+@UO`&M@k2iHK4RearU3kh(Q@ZiW}GM}G@Q80MvXQ)Xwis^3w>3?45o!@3P1 zs+DpEyPripSADKbldKBPwAN4`120Q&p*Y8#T6d*SKf~QyKrWWJurg>E= zr4`=kp4U$3z9q~aC;Y2)5@mydw>biPr}q*Hl)Y};N1K7%;)!kmnj)*hFh=%jTOkv8 zg@5rIl81@6C!)JIFc{2EN+doaNJ3v%H&5T8Bs5x}buPckhEaB(>X+1MmKSfw23B|C zvn^UGN7ehoVG>)Q60d?XlZb7{WftTKa1H?3?|`lH!Gr0`j~f11LiFR||M4QJ0%zBI zgIs1a(#^dBiK;gLeZ}oj%=_-lq;j$jA_ir>Ka$Ady8p+a+&iw3&Czwd$U<|XLuDaU^6>9Y@$q|eLvzI7zOiu8%@7d+M! zgKN-F`qLZ_!&HwpOAxFTFb%oIJ2BZBhC4$b3syOr(eOBQa_WSMTy}}NfzXqzUf-Q3 z(jh8q%*y2l>Vh|h4442lE!dMVZ^{AbTU9DEm$LsT(J9I*xUOTTg*RsJPxK^@;Gn+0ts)KCxo(`&dtqL9edy`_u*2h z^-u2mINW`>24WfG z*9uzlRO>bQR{BD7Kd%c~w;0JnO*kXan9lOhGQl$xKvJzoQ(HAUFxS!|*1vJ8VtS0t z+jn*9dF~%hPELY8*(|619OF($+7ZL+Pd@wiOE}VG-9sPIHkKAWq&1=v>rGsqP#x)T zLi0#8d!4|+$eb;bj9^_KxSNBZ5Hc)}^rc!%{<`|ui%ym%WVbiUZ6EZE48G&2>4-UT z&9%Z(PTA)BVE^?feJFj@5*nSpUp1pbJ|ykHbUoD_?JMhZ95gyI2fI_q9muoTR@2_` zcyy_L$PiYK$t$HNXPAr|f01!9<2zsR-O(khlcuaA2XCF?--HLFfvk4KJ!l)q@;LJ( z2KkhYpVb{~&$$%C??n<&@+OB05>()Era<0>%7;pO(4e59C_#wai{>WkZi@>!BV^-= zD&4NVdE0i!vUPA>(vF#ZpZ^Kr*1j8*uRV$iXS+_MVa`8OCI5?C{Eky_FfYpHy?Ga1 zE~F26K)Y8tOZP+0pyp@5;GP=n;?m*J;Zfzz+-A7^bVmrtM;v$_#b9NCVyrmZlm_*M z(P3{24n8;7q}>BS4{$l)GS4yQOFe4rK|DE@Ufk1tbpWEIUSfb8%81)(259s21VXyn|^$|bSkA$X_qTFv?bBsRa! zW83}%idS1CMF$yLH#EE`8lK40 z#fVn8vphVkwP4oyutbxalG?D@mN_7CZ@Fnp4)iHcHtt!+^rJ^zI4gS%r}4BBbz$z| zBUU;G{SMei!Ir#VplR-#DC$G&daXo~dI8s)&uHgxeq84X9C4f~Qw7XaGf@6|khigD zX&B-VGJK_3zeiMo-QNyYd3CgL$ox~Btj>-j!D;XjF$9zTiGUKS6%3SyeL4H(r`F{o zT<8rEwyVP0+S)3w)nmz(uOioODPt{iFp$P+IM<<>`+DwN0qaB7c31IjCHYz&cX6as6; z>Q(bSmV25VAf4WLPj-Szqu-6u=|-FhLhC$5_ZOaKE6XN869>;O7V4hdSPomnOY@8a z@u{!L=QKe%YtQ+Cq0C#@x`n0UNtU1oLrZ<-pcB+j;vT_zk!9sO$11iiO{ZZntyRy% zLV&DEJgn8Q?!62ef8Wnlzw${2e7rv%hvU`YFkbFp7L*?-kI#(+ zC?(kM+Tu!4QGP;=V2x-p?0GB`E$v zM!zICwr$oYat>+;AbW*fdmfXk9_KvI#IhPe3(W~H{RLWCaEe?LSWR(SPtW7d6zCFe zN2oH!>Alp#e*iYls!Tc7FdD|JmScU`FVF-+`Ybj?KK*&7Won0483`56 zZJi`=kta`Mk9PxeUBLkW%_aN^btR;$ZKN&vw3BWa zBzE{L)m5jj6BMdQ>gA&hDuno}qs~q&bYz?)3RW@Q@E;AVZyMsQk2limydZmvy*Ff$ zW#~z>RdNI@bS42rMZOm$P*#On=7$*RVWK&}$5L1S1|9j}R#-#cj*6N>a#$=;8=(2( z7RV?R(r|yzvz+o}-~w!*cWe5@1b?`w{{*4SUrlv%l!-;$D&Iq%QW0{!iO``u9*r|V z;n~N}07K_?^_@HFmGW)|Cs6qHPHQaQdg$Eg$z8eaacg<<=~P3TmDmc=-l2R@dpx>s zS~L6VH8n*9J==$;KQ(6ozEBkc>y}WKa?C<6shZ|Bu>g-`*Q|vl)8Pd4an%^(4r9#p z2%glbF7cDC?D6!~74xzmvPcf&t!~Z-#=2e{efI4&cJ92I-0ht+00$F8Usql2%oIHlw(QGaF3^vE1MI%my>)l8 ze>22p^ph+Zqq_UWjdts~1KhKPZ7-?hR2L98aYwxyp$hVq-^B6ldNlH)pH3Y?~~e zr84UM-2f2#zr6hi7r>GIb)*mW4^I$rk6aK|IXq}M^gr_WJ-(_k292QDect7%(=ocC z@dM~fin{G?Z4=un>2o;j8_}xSk?SnLa-tUIC%*$D!5~I3T*6y%p zY_3Y`Edh?LRm+6Yt!Z}KrECd~+n{CNQq`#A9h`_P1MH*yh^y5jp8|L@Vp{_`yQ%c^ zO1M%YpJB?cIZk-k+lTV^^Tvh~v(dmtRdVtM23Dk^9d8iBc?rkV+1B_v^V4nGRM*O5 z(zdq&@n1*3BH$!GD5qr9K}mDMeI4lqKDweTC#RF02^juja61Ma;2Vd#0DOsT-dmhWI&am;*n-$$tBd%2X8m4fkb+zH}yFZnF z*-@LC#E1dzF&o87g-Jy=OJEz#9x5HqmB>pDb_H#O`o z>mL>zvWT=q`^V)ajO3dgvEBXQ1~l;O@Ad#rDu?X>;7ukz9&SyZ60Ozx0Lq_zWWZz9^XU7WeE=pF#PF5*PnXiufl*D# z{ypU+Oh;!-aZ2r19qV9;&a&Yfd^s=B+505;UFs3Ih0=QRxDF>HJ(wmJ7uiJDG8^3Z zVrBb@0Xc`Zj7k+t;Qn(QpxPrm}X_ESpJ7rTCM5<>#4B4*vZ*XQ!tZs)5@^P zgxehQ|fn2V$@ENz;#RAQIhd2NZp;i0B!09t`an;9ffVO zs@Patv#%XCyNizw`BJkx4CZ>v_+)<=)#BRK?Xf=TMg^yVCI`M{>dfKZUU+JnbLnyd zz*2q-$f6~tQv@4uYgA=_+j#~`+4rhvY+86+j&T2{ZUtB0H5kUJ@!pUj3%y;HS$8r~R0d!mS$6XA{Pg-J*Fl-b7r$KH3xQ~iIBmnhOU zvy&(?Dtq2{k{L3~7TGg``d7lPlpOL}eL}g(msn#RWF*bbG;1uWShX!9HW%t}MXM6`4px%l0h5!s$ zoIg{`k9Gn8E}Hr%;RF-sfL#3eJVAird8_%xy+H|gDM#GkMeOdk8EI@-^Lw<(BA{_z zhVXOlU^dMO?M&0^8$Mi00chObgtfJAGzCNCL#SR@A#2-S<2w!MOntl^t0f-&A=hqd z%)Ywkqc=C`68p|u3{cMl(%i-m-u@%EG;BS~TJxQp#GnC;76Gli(fbpct&XYJHZ&9) zT%To3%NEpLq`~B>%HuT9Vtko{t$9GkU2Jg>vyJ3z{^F;wx>(jd1=Z4z3|pE!hB2w= zbB`8U(KQ!TdPjBK2?$Ia(DC zJ0jaPrrZ%RpRc6p&2h>bN87htS}ch)edQd$%>qrd%(o|1O}W|G-+6C|bwbp7r zsiiiN1%^aCrQ?bn0Z}7EAIlB2&!ua-EutasI5L-{vUtyD#KDP-DA7$CiG7PPj0Uxd z>qYZoPRicTDQ+bOqHN*mWQbSNjqK=t!}i)*xqssriXYM^e>8L;sB{Q?&2A4GGso#21^yY&NLHei%q=BqpDwa*c6q8V!VnodM8ZXjZZmGh_KsiM&@2ViXS)DWEPf8$1XimXHr?THG zut@{toa-i^FM^X6*wbB=nQGsofO;V2iYByVv{^U~v^acUF0ebcVamAfbj(WZ2NcaS zc)$(sKT=jvA$!h0JT?wa|MF6q@8LL_r%c0@!S%?WSUAuyqbyNh^j3_Ce$uI~o3L}9 z=!#aQEWxul3(fG`!|Y;g5e%w!Kq6sdzIujbyvYT5Zd1`3}Lq-7%%d+pd6J_vK^ zlTqBS{~|6uAd(DaZA+3KNgGToQ2S$u($O}f#a{Q6!XVxfLoYM(273Wkbth^4Tc}|= zNm`E*43MYrbSi)aG|dn2j5g2Qb6E7QP^OLLh9WcIbs179qqso9VAt_~gGNf;=W%;g z1X~XS7jAJ8G3#`~yj+vr;L#4K2nD%X5B5Y&9kH^vn%uG*0xr~q&@zuzwYIi`f?043 zJOn07eWi6*8PlLno;BZ0e|%C;e4)bEKsejp`8UN*S*Dp_5a#LVf{|tfGwBp#ZXwA+ zqFwNmayhVIKsPiHt#(|%O5_0p@DvO5#Z7u#Ypd#Xo)>DqEPk$Vrj03fI8-SmAZbG zZX~dPCe+@G8Z^#U9ZyhiM9zeZ#hRb2G!{2hWc^P>e7q25+u51=N$iWEc{m18qh7LFA-> z!y6^#+;Ob10QdzC*(vP+UzPFj;PY8?OnV!V!BBkAU@zn5)4gcBjsPhZ_G(a9ezymP zy?`aiWCZtXGn>%q#>me$%_adG0ff~f^k63=xeP;I+lMGn!f|KM5_Q*xc4-7_MrWuP z3gY!Rn!0(1nLQOYvJ>1NFFgeME-{Wgw~)(JLJpgBSlq#&YF(n9GTN&&tZP+{Qp~lgr6Kr+x9RqSkjumiSIj853g0PVi@r)Rl4+n_>f`_YH_FM1uUgX>=WU4;ujv>C$(f%nykYkq*Y=OI*NAEd` zV!eCJBQ-!NF+2?gEy@?+d1J!55wdWMNK5)2g}#KUlQ#Bgq|ndw-I0~x6ya6Tc( z2GVG!1+Y%v1Q!^DT@6-eMoPU-+RmJZHe|$J#vT*srh-n+&|h!eagS48i@CpzS+236 zL6~*Co4!gJ}Q;D+9lnt%KA^U8LRyjrS+k8%o_T!oF4GU+}=Vwp%D`F?5 zYI|{{~WCko$Nua3@?h*Kw=LJPj=d+q7WQd_z;9@}Jl;BN> zf5A+fGF1$hZi_Jjj#wBiQB@Ij$aGPX4=(OLff|#pcno2aJ>fKmzM z;UjZ3X4irTfO*O>9S^ew>hyrLMC)|AAp^TtU=YJNLize_oa~@4-DGjCehJ)G$6BDl zRAw_}rv5ihnw9-qe)+B=;?|fkNn52%cZp9|(T>c{=I#?!PAXa64;B0riKx_U$Muav zyN-Bhmxr`i-)}N#vIiahhfW2HFOc8Ca(~+JPjcW#%SZ!X5+yNm!pCkV zyUk%d+kK1*VUc@z;vjN#N5LNJ16jU|x;=sJAjv4JW|fz~k#B=$R*-NXl^0GP7P7x| zwknPrS=w{LnOanYR&r7ffSyrljZE&rPk@d)c}1f7Smt14a<;zugev|!;k4Y`TyU_h z1bYs_ZG3uaY6{Mqg1AJQ#$?kGPPWi^kLFBsE9l(jRXN|Lxu|s^vF=>=i54svzZ=Lo z?~CT5x1<{Fz7LQgxj4Z9yY}x#xRx(2@HHKK$PHM^Bzb{tIJ7_U*0_oA!K(gq!yLf> zUHE**+;_6~!#oEsZ`rHQplbU_uQ;G{t8_hTh?)wLB1(jGzm5FXISn8^6O4x?2HYq+ zN?tk~iV7u)%Ppny>A-4VUcCm!&FihA8|X1iN8t$GQIG?OBBai<$_T9m z^raXsl1=DD?>QOArCtG4gM;&JvOTsX`dIuD;#TD1>eQO74IeVjlz{_9)!8^9!VG2y z`CSC1xuj1X8!-!hGY4k-?w+D~m%B3$n3SGxo`o=%Ns4#EdM9@aUzQ-TxuxEJO$k&y zA2D_8+!Gx2##6)ibzZiPrmF?;3-YhJI2<^@p_X2M1o+ZOt1c6~f-SR%L|+qjX>tP7ov=BbtAQNfV9P;(k^0@l*OOqnq@*$5{l zghtso9#}G`#3dc0E0k!=WAZ`0)r1`nZG$%{%|A_S(E{#`Hl5yLksKnu;slk&9})f| zXu20E`@Hd53+9WUey(?b0xfN9@OFs2+L?eFdPJdxWuo|{3o{x(U*BhU#^u#;Yk1%n zs!J|n30#uVNlt(hLdLc4aMOJ@;CTQ-BfB033<*t-kB^U~2KB(H+-V?Aa%Md@B+eOv zVN*qe!@~#E_Of6fRaI2XHaHs(8#O+Z!4q?@g4RCYRBQaUMrvA`4+md-f<*nau$(b> z&2iTEmI){Y5-Se!Z?Db;FcX34sTJLOv!Dg-J$vGLsCQ2g&CJBG1?Ndm%*HN|tmo`| z3Jesp0D3!Ou@l^-_1)HY&-m@~&XInZuvo?vncd&1QDA-L6-_I8R+-ZZyKZGK^> zu}SKC1)K30fLpc4VE)A=uLLN_V=h!jS?R*IrLOduq1V)##uGm zuMA#EN|gYbI0T3{8K4x7{`Q`(ctA2}q4pCs@~4q^fKi3`G8($O0$UlJYoUj>Wg>Lr z#u#(cxofJP6ab?hObD|B+-PeXKr>@8_5tT!!1M?=8ISEF_G)3TM>NJC_vwS?w(k8P zAK;i?`9O$TmE}wxHrZLbqvyO?bNmZFmBC9Gm7T~6#7cmfK0lbC&SF>@VGb)JQy%Gb z6OB6wGw03~Z!JFgZ7 z(}Zx93o_c=h=Ba-a84s$P&O>!NqBrn^Gi8ng6QLYm&t(Yp7B=|%`c~8;3G{*0rIUr2sYsQ1U9x6_2s$d=;a>zg?T-ARuqEO2~4TiJ2AT?uacgNe;=xv4^Bjk)3M!qYU}$xRPaEc&Azbw7K_hd zeq^TZg@swrPa!Qc^D43=r4hJNEsrB@L0m@_X?t;5DtsF~s|VJch!sT$eGE*?Y+GUVC}fvtfL|ik>@%j}$}D`#@4kyw$#rb_*E=dwHzp*ndwKaAQ$* z%!BS56N*92a8OKE#|S$8<0C)+bpm>i48UQ~fgr7~9w-PU0LxtQlyKN30JouD97-)K zD&|+17$kj!z<&sIQzup@n|kjTWY|?6C|#|Xzw$dF8nR6lsm)lI@MW8gj!pcY3IW;u z^I3}OGN{#M%h=7wWoKtUnOqNg($FTM)~GB$@!J_Jv%* z_Vwv{z6K8MAKzj{u*FSYG6&ZZB~*X_jHuuSLtT=-fcnwy?rx;qS&OeN+@S|tY;;&! zb{$H=R{*a5)29efO(@X)M_a@0MElWa8<(blf`BfBI>`%*+hv zy>=M}>^7G_PTF|j%ZV{aoxZfxrHj)~3P6{q%Nd~CYbdvkRI1DCb!cIo|E+z~x_8!| zVP8fY=iZ51OJeu=Ni-M?2C^?vxuA_1 zt+K$kEa7Tm_--Z3==%9ZMHVnfYK3J{4}$);ezjxhpl)m$j2{FeT_-?`ejBjYI(UCr zPZ9tjnk^~&EmW~Y+Fw@#g1 zJ}~8tA6Mk&^^=fTv!B zX2qbSqSD&joRi|W${t*z+Mfcjgy!#;uf_?eA*R!S?)%$c-)c+iQ89GE7UuPg>E*#-bcJ+stQv@=Pk_mmxX5%j?>ZX0#??W~oKVx6_Ria@8y4Q(N4GuYX?~ z^;n`hHA$pE0z5gC3R;hf0_^vhN-@5t;x~uc!7+o>b0A^~j?V-eok!-({{XH1x5zMmAc?h%YFE;B%b#aL zM;h+KSzw+R=#)3yFMn<=VzzL{4_RnKWm~&Wpp&T>O}KY)dQ?M@)T5&1@H4&5L<}~I zl`wjwed%; zK-ZvLPeE1HI9CgAwPU{}NT8M&c`Zg3dh00@H3syu@^TPlYBG`r7Tp+V04daKBrm&B zLH})GcJV+?U|aRuEbpnGa-YeD@dUD_X(sA87@saHQWbY5EX3JxLe=#TSlfL+U`UsYKt z^=~FP(GtRKQ~7MS{wBgeo27XWCTZAfKUO2JTAGfL{xu+zfQhVN(2F7i_5V!!zD6B6 zJ5+7o5MIh%k}l@7UnT>KL5wJl2523Ul+3rbG+uKcrwqEJCTA8yY;+%@^Uid6r%sOW z&0l-i_$(F>sg8VmhOc8v%ify*CYX<3`dUqJxvfaR?k?jOcLeu5Z$GH9{3$_02+;B; zh?T3?F_b_jytbq8fOStL6Q)Bt(&2;?pbG>NukMdQxA9CtK1Rl=jd}_Eo;dxO21q)S zjMr@)B~%ezP|4;_>yuq-Tk}Vg9q6+(UxpT0gk3kk^P9l*TC)6l#~kVSDypXaQ5$D{ zaK2BMZvfBl?bDinWg(s0grvqlsZ>@N`a9Td!Q{g1RmUCH!1IT;vIT@@hHK@Ae#%$h zLHJuA=RpQ0-u&|kwQbUGz3At#LXAMY)@mT$^5^nmtLuW2Au;$lOy|h@H2Nnu4w8X% zKqq9stHAi*pm3drv0KH|hWH6n1h!SnY+UgF>dHJT~DcH;D2~QkpxzjlPCTcka!YVQwA0N^f%;_3Lrxphv2_J@seQxfoQ%@ zP)eqMOiMr9jvul+gmKnB`@qq8jrT{2yGT zqfii~v(noAf3+ycw!+$?akH(kwkndh71q`;2;Ej#zi$M7)yvxoYl~LL|3YE4pF6C$ z)C*vJjr{+K^3u7WmK-P5qLNf6WFdfRQ!(M9EjaQ`aJ1^}7sv?sbEREs#lQj{VUOK~ z`mq%?sfLNZyUl?4>ZSevBq{v4uTmh7v+aBOaOsWA^v@ASMa`f9pvvtjYGN6B17mi< zQ7^GZ&!_20*`M=0-kkzwYfNIw(yI4kiOI>v<-B3s#Jh z{1u6)4VSkli0XBLpU6eu&;6f9fb2xubC6U-`B`EjeuXPO>RKhALXzr4gZJlkKdrz| z`*Udph&TX&!y@?7f9{NqkebICn#k;9j;J^wJOF(a)z^!2?#-t}22|XvkjZbjLxtr?lhZZYDgKZ$y(1NsC*>bzAJ z^CdayJ~UbLh=BFdslS5+3J66YUH4o;D``aE+)2* zzr=P)W+2rVBrjM4O|Y#BK16{>DX6QXE2JVo~jx zUE!p=EDk~G_cp&wDcr;JU!L;E3bCCGnwvdhGq4{O=CALCI6{?YM4v99jiTZv#Kqsv zzUK;UbDufU$)xY5j_$?pwakvq_Kpehtx*2*ICWFs-V8}mWCH>ke7oF0H{b}ngXJ+F z6%`euxSbZ??O!w`yG7iMXJTB*f2eQ0f`m0o{1MqKKMb`69SF!UU@YpfCA=*X-m zsFuY^$@a856;b8f>B>wryQ)7JShWDbmqckr`X#a6f)-QG@GwK6B^%M7m_W9SHE{6( zhp^e5IzF~o`=L)=@IzFQzf4rWb@5ad#4i$>m;2c3;R|84XyfBcf3nP0Hq-4!{gPx@ z!Th{^e}C7A#86TkTGRg$7rT(S5SNo~G0LdwW0w5>Gyv}L=Q%me0~3{aeT8ODr=w4~ z{g^Q~!yHS`RQLfVDe7G=POm=B)T)Qvdl`K&5~FRTPFm)>#-@*`JvuU*hAjK_Oay2X zcozN~B$CLZo3o{|OJb~=-ejyAE~x!5>h>@w?9S4U7hD1wY(4ZWI#4_x`gj4SWKsHj z@?=2Wy5FLWWwS*G*;3{Ww&YSveo6O&_@w+Zt$GR1o|$t7+tS9N)__89pjXQ3fTG+S zmQTD+;X#S!sK268^{+z+NdXEda6*15Ixp#6peeiXIf^g(8mNrQRY{7D`+&LwVLPDa zSR(S_)k1UgC3Ep{Hy@(f3!9Qz8v_hE41jUHa*KLt9W0nZDp$Bup4X2|feB9C=5;#~ z(NX?>0yAa~MA+Jitaia_L>GJpU!#w-)42sluKE*U0@B|$HyLFFjVLFBG%waKL z?m)BqwV#vSzHz*0JPJmRA$ESCNtG7ig-jirA3jREBwHQ@qZnT&an1e0K=Ly!X z`u34}JUxXsO9gHJVgn1yIM2*9IY8vF1sK(EDv^QRc@za$LUrf}n38d#mvKai8b zA5)Sy3vPk4gN{sWp-VCGeerS)+6ax)9{c(N;nU_gH*^#8 z;u7EhYidt1ID^ZeyY|S*$rUN$48aK=tK@kMv-9(K_Mp)8My$I@V4wb@W&oxr-90OU z+$e|$YRtSR;TT5ESUh>3 zH?tVBD{3RddlsJ_T#`5|d&2Ekkho{gPhxqu9{3^of2OSPkv7}aOB1%a4cQ2Q!;1!D zohZb}bQbNDpPh-BFk__vwX$|=w)xfbQ||`rC^m_>pW|Kc5Mfhu^Rw^=N06`dP;9qF zSPma4qk&L7ETAJ$ahx(HNJ?<>y%-V_qN1Q+JNBNY%9S0B8l`qR> zb`*87A`T!eEEW=p`p=t$cbY7i$M zRr*v;%w~{`xetAEnELy9fJo3Kkqu`8Uilz9_b>o<&(sVSrHCs?o0y1~=XL5XC)j@6(P+D2m3hR3gcu{F{9(Bo*Y)id632e?CI(Jnw|gGzG zbEJH{!*EgY=VS%#K&GtoDfh3Y@d#u$jB!gnN;LF;fs8+pWvzkpR1|3<)X;^-qz=c+ zCn_l-5)+gAQRo}e?u6!DQ++=(GeY8Hq`~m0oGUhxL@(-pz_&@Rbdax27i0VQ4EwsyT7020sp*)=k zX`#kf9^qe^RfkB+3u#S6Oldp$X;-3_%3)wdat~LRYRUp%MV05q(>U5!%@_%S-~i79 z}}KDR}}KDRdH3S$?jMz;C-su>L1y1WwA3m8BD^148U zgFNp&mHbVW^dR946N4J(Pe*?&GXKp|JT*;F7ViF$cK@bJRwIrx@o=zkzVZ9b2Akjn zLbl7Gi1dd0x8=0_%A3!EYL7DK0iEC6jg=RzrHxg0pl89(`DPL*FIQ1*m?Qp$x2}&0 zO>o*j_;B5)Ex+jJAYKOuWH4jQS}y6>3M2m*-gn@%KiBcZi2r?w1NsMKyM!K<-{qN= zvY2&w_`79#Tn0rd7nKAzExp#?@}KV%I_4uFt+GGn_WrpuBOAg(5djK{9=v>rQf>Jw zNZFvA<~zhKJ#!K{2*ls+6W)fv{IM@XE`n0Cqo$-wXa4?ov`!%z4G?&C>IvOnkW3Fi z_1?J-&VZj2>k`g?ci~}T0D;$s1?1P=$=`kJ`ulqn$Xo5V7R-NvEc^iiOPXC1SBd=p z#h3qYDXi&(c2hXS#Wp=y*0^0>953w-_`gvTs4N(Mm6A;FKBaa~|IQd0CGp|Y!5?p- zOB3w9f^~Mk9~s02VtNjjNqvVaTG|eq!~5w7XEia-8iW>+==wLjK|jv1_r?RwA$o#J zUy?M&8sS^V?`x*a#SbUNg+fdMT4arL+u*Wwp}g};a`0Pg5>ubQAgtaYg;farf5p~i z1rVF1UI3`P`mObabNt_6h7iCE@Swk9=X`Ca08xti3l_l`(xigr{0-e11_446ql5M@ zXc>XK^zhHgXARSAyzv4ms-%(%ND(g?0;M*!|tEy&OWo;&` z@PlWzRn}Hj&9=(gVpQON>~U?YtZkL`Gc~jAW&J=!Z0e_NFKhi6=ccvGwwJX795xSA#|5v30(-MeVZo z?--zQ5(D8CoB#c{p!1+o6vjrw`4?&dPhM%y#;B8DYQOq3eqDQfV#KR=dY zcW}6Ev9xag9oq!Cq?)M_yUz*y_XO{%4yY0x-%M$5Z(2dfy@TYFS$mpRKRzq2RvyBW zO_ZX_WLbD!eVtv{4=p-q>Zp>!cc?cVP*?pBedBkHj*&%06r#{H{=u-+h6`zE2sw7FcM%W~x&!Hgv=(iSe$k$Lf_sgOHoVOl-Tx?Czv3vstM;bo>|2E~$a{b=UxU}F zs6Fig7snj~&lACQa92GK)UrQdxib5IQ{}IM)V5Es!miaF?=A6PPPH$-I)1MspNlc8 z@Cd)%p8TxE>+@Ew<+*FBKAxt68VwPNq*tp8hc~b!X&C@u9XJNoYXP$XE!E02$jm=E zA$QWa%*_wdgQnk>n2?YZqnE4_UlSS|6IUy5A$CFaQpT&LCV8`R6p41KoYQ(VSS|ar z7Uoo@`>PlSSggn$vgwYpe-&uUJJzcKmh8Yib9Z%nwsDd6?A*rZLrX8+WT~8FDul{B ze|$Dxe?VWDmHDhah1ZzZqy7v^Gi-T=k7bcW6E!8+8WF|PveGM)=^?bxzEI^s?r^Sv zOoyp~sWOCv(p0pb80jj4TTL$CR-uSxuN{9d2X&Ih_iJ2UEb0ZM0psrE0r3Z+A}lUI zs?fz}NXZU_avqlUV+yx3827t9yx3s#;L(3ZnA(Ou&4wkDQR0 zr}l$a!f7*IQ(l`{6}l$+zOz*+=VCd@i3o!T`GVsDI9E9J$X@&L_l!s6^dFBohf&`l zSoviwP)m0(fIMZul)L(CupPy+pRQ`KO$dn$2W>`iw8g1U$<9!ERs=Iu%*RXQKg;pk zxy-ANWYn%tXdMo2%d?AdPS#t#zeb2Ifds(T0R=EdQr2sGoi1sO^$0yI8ErzrP;bm2 z?z6y-wh#RYGBt4uG##0YNLFvD?|l)Tdn-0BmVcRZDZ(M#?`S^1(Pd?mQ|c~N9NkRC zQdlUc51WDy5~DW9{3)g(XbYqBo`#r-#xU zREW-G8Z;jyINnMpmiOrc_bB7&uL4uSdF||=xgu? zD3jOLooTEbD~>I~!(mIcVv=RCSb0Sx-W5ev2}KjLw7G-B#*1qe^tv>y3s4p_A>+30 zj~n3*l81Ku%^vC^iJ1P8$`1Pr7BO*hFF3JqK5En!U38cKSZuNaR_aNnUQ-r&k*g0j z@rf{cfGEQkSAT8rBZLDZn!0@d{(aPa4u%d6wv*Sqr10pDV6|X%mL#dDJoR(`yiCzD zViYy>7YncKTTv?;!xe@L^r`5XOTRc<%HtxCUk=mxj*p{jRp$%AhJ95V@fU-gVR@-0 zCu**+p0AmT#*&q&@j{UiWH=|sy(|!UF!H7#zRgH4Oe$pY>LKYhB6;H#AsOt%A@;GJ z&z_%m#S%G;%xlI;)jU1NV`n@?wblH1MS+^v#k$(M7;Q$N1p0w( z1)m~omNbufSrs{#ez5xGpT?0Lt{p=h zG*&xEKg1?*(J(({b7pwx%>!deUd}iwPOgaaaJhr-s7PE{8!6w3hfH|UD;wQ%SZJCg zOoyH9l=&+&{j76l>6yvV+x|4B_6>i`1!g{lUg;E*E0U237_i{YV|E2?EHtj$B<CSW-5;PRv>%g>vUcbCVDZnEHmaAn*Rf|yXLn#h7 zF~*hM-gt9-z(i){hFBJ+rPrWP)erv9wYOS74Y;jb47D9-1X|j_?o?2EDdI}XedA<} z31Ei%$Hnf%E?dHoDU8VxyJI)9?(J@TQ2!85W8!4xLYblGj|JD)?}zarl{_qmcvDhn z8G0z+l9s=%v7{qT&?cwa54GN!6DE3i;dShLGhvZaD|8hmkLDIMY(4+ch}fAJ2dgDp zAccGRbsJwm?g6>Sg`WCr^T(l;*kZX(tTluT^kqrUT18~(5r7?dc(}ad%Ke;ITSovi z)2nt9V;#Ur8YKykvvn@K_F1UclBapE@AZxMc4hyL03%M^ITm;0WvsIkP^rDZ?Da#M zwiA7!G}sB^HBzP)(b|j`#jU(%fp%bGwdMX!b97N0s>{enWV8v4PXZ$kxS00IQO;c< z>^8Lc&V@?*`1(3Gzo-+iYT&G_;oCZ)rD%4OgAMbrhe-?nzF1$1bDyf1 zK2}z)FY&?p9^?g@)3nBm30WYoAnIuP0|VuY88ASuUIyxYV;x)(#a@p-WD31CR17qX-^=7hI;I~S8Uw>m>&2pRv#)NH|4LO?CnW83be7rGWy8IqPH{eWo+7F6=Cf_p}c4v%k!)3ayS8eqon92wN2@% zWrbjH%A~9G4G^K}^VV$S&k~X(%2k?6iMkanf$f^RXUcn=hndM*eN4$q%4PXao4tSx z0OnH6Nd9h$+)vj6b-`B3%>nw&d*4`P$NW&X71-nFV2>rr%Odp*`WI4SMNoW?O$lSb z9|<5FWYqG(Yc)y!n5k$Pm{P4wuFcED{gY?4I3n%yA)&1Kwa>dg@yZ&&BQHL6ekX-C z3rzMAV8qZdVE@{Am8UNYvO8n$?cVLZ@5c3s9uTjfI^`qli8NE$LnEVX6L?ux8H1bL zDYXo+QC4(XzZjmD#eZ6-ClAAya{H1SYXAJJQAKU>Fz;MIoVQi6#Qvd6IQK*1PM~-_ zn}er<4}deLo#?g{sQHn0UO_oT^gmnRWt4!GLNCx1X0Gy<3UOkgjP$C&LKXLV2yn+J z79uJoa`@L2CLkIe5ZkmO){$Fg(~UUsI|7q5g&stF)}Bz{c~1e{=L<4cUQ#@a$&*8k z2CuAbd$8Vw)Fn7xBlL-A`|IIVwQn+0uvZ$_!k*(Ui@Ng z1s!ud6PT2|Z9DgcuA_v$hZ+@Q1yvZpBX!?LZau4=ZEpq08N?Zs6Cbs70J~D%l3G}i zf0rlJ@q6E9bL0aFGer8ORaZA-prDXXAqLW8(Yp=83P}k`QE|22oFfZew`7|3*RNLT zEC2fOqK80G$f&$j;X9i6DK*D2GM%+oyEbp`W!h?I;$$=`0*K}m()yVtVDmDtMdT3x z8;#~v6z0bG-2g6gN}wiicm_5VJX+o_Ab6^~GC`C|(*rmF$K$w!V}}X+LxP3VK1uM+ zQpukBWfq5o;eb5~?!D@}0if$|HKY)uKdtc|-$F6fEhR-&J`fP~l2T>>!GqY2!sFxi zIwNYna~T}J%-q&-fQVy~t-XH3o>VM6)NTvEucZ#|Wr&a)C0*0S7m@3176vv7nLLkic&GY^(LZ$*k zl!9+x{zW;CN3i5+|D&Q1CO1Gbl@4%~153;9RcW%H z7+qlwP!M1eY-Mj=oDHnaS)6h6sZk7UeK&mlt(mG{uyAE9Amux&Jg)CZR%QZ%^SzVr(MhW_{MJSa#D%oM3Cdl zPsMV1TBRa7l zaT?<#^J`2GnZTeN)zljswBwrzgNL`~%}q3S%md?-i_s_N^+dVxX?mxi$iZ)3qL>sd zoYM0$o|!Eo(AWquDW2nOUtjuJDS?^%YJsN)9)fgM;uhnuojL+uE;?*fSb7#cg!YhX zfvV%;ReN#}P8ymlsXq;zj3AHU=!+qYl+T}e7C6E(RE>0j;ZCZViOzXmIS&N_<5n`bykhRAg;pCv+sY#?=!^ejgA^+xu zG8%g0A!p--iE_Rd7z#tY|9Z)!T$OWQq@tYlfV zlOyp(U}sB>G3lB5q9hX2sG&H6K%Tf<_iK91d(#Ac;R;4~ZiJBLu1+BTNBz(b*k9e| z?mfCUS1|;{=&RQ(E9&^F9?$pAI#QS!orv%4j7{kxB&-SPF=u4x*nyYy-YF1G%D0^x z4lc_exv%imx4(877F8*S47XC^M#BRR+s)R*sLHXEq3P>|d><4fVJ3}4A~IU1O|QN#OFyb5OiIZ$=BwIVucg%F^hk5K zLvf~rk`{wPhS2O`Lyo zgp<4a0#&ak+kuRyS)JF;xCieU7yzttWBcRln-YR zWATcl_$19-AcT?ghEgJtH-jmEa%k3jBx72kKsTw57(1=zB)=tWqj(Mesxb+2N4SymGoPB&CZ@P ziBEAtf&&ewCIyt51Z5nWcp^FJi|XJJPP2s*jK+~!i_Py!5hF>Y9OlyrY)(N_g4FC^GzFj#rP z282<$+co9m4OCv5B3{k}%OrWvfANVsinOXE(U3JH=$G`?7_*Yd>`P66M8qnO7-XX+2(HR9kHI*3c2SO-9|9FhuEgA<~^U6MrY?Xx0#Cw(<+lXoBz;o?2;57NV*4w`&D1 z+SBZ3R93Oq9=}3^cjHP%R#>CIPGhG=qj-O=|j@CBA1cQ=sIdV?wJkB6j zxNXMbg+n(JdHcXY7GMp4h-ovKNwGvWcfy%)4P)|>nNI{x6RL~R-t3vXt!eI6k;a^k z$ME82j|m&7IS^{&-8Gli^uB-S%(1feD!zQbNp3RLndIB%YzObgzIIX!TF?va3BcsZ zxo9|fNIHn=X5LlHcmlyzZW00}yqENP`LCdBM++CCea z?(PcCaPKxrA%8PiR!%yzZ{|hjJ9zo<;5~P@+(wvltI8zvQvl{n+g`)gT~i=)VYHZX zHfatfo^V!L3k=sBN6w~przu!H!xZX>J8i^wpwVv~HeTQ8{brN)LMX0X0xpxPLeNN9 zQK+0Uu%NuDqA&6vmNEkb1nYF!%fIUuo@no|!j&571>?2_&P8PfKX>~V^}9d~kmV8P z$?8ki-jJghMECFc;wQd!Jv*5MBgM!HQkuNNXWHfNgaf1L6vi^`FXw2Uc~J&OoMYhV z!B0B9P?cunXhV!c#NNLN#LH)Troygx zREJbQc8xY;qy^*{7?+!#A&JTCGMm+W6*Bm_jxYZ(qT_Z)+35SDx(mV6cQ2;eS{X5S z#Dq^1OihnI3JVE(o6S*Qggb8kE@1?VM=3cY1`Fog5#5>~X_`*EVYnn1(>})AC*jwu zo|_P-y<(R_;_Lc_i+C5{;t_=2sh>`s<*_^Cb&3MdSIG@ENy}l1AGJcbCIKjhQu`YzOW7g<%g2<((Z^7<4*S%MXqj9tl7l) zAoLIFyoOs^r9=|y{kAQGUOEcTF7F=O2#JP5r-hG!E14*qHnoI}=*%TI>@p)2!%4m8 zZo&o!I5I?&pG>_V9bkhMCR99>>sFwcw8=Q%9dNc~11Axh2clOk@>PbC!RJC66PRa`ARX%GaAQl}E76PpGsxk#gHczG z-sS2u5j4{r7qRmDaPqfPsUn?cAK!(Lv{7cB4tePD`Sa8)jO}3_+!9@kYcl;sHmmqE zMpAp0nu&|VJO(OJM@6!eZS~=EJodRxZLT2B$!XjkArXflbf?-GZA>cS;nF%|KcRky zo#?G;9$#lB@vBl(viDy|2(qt{gsR#u)Cfv@mq1@UQ1y7Hw*QU&@}=?TB)CLUN+#8#wxMAj4N4W&Zfqnh2ht{)WW^M z)s*mhLa1zno|-VltIc{N%*z9$(Zz$feIjL&T)J@N(jsm$$CKVaEy)7~B?w45dR-F7 z#(dmGL*aNji}xW|g4Wv?R&@S`t?nZRZ_Ts`MTk>uzVy0h9lDdbzpTgoVWhKL-eRG4 zpuXJ51PHud>)<4jS4u9Lu)inpto1Gg{Tbh!l~dr1X`Y+u&`Xid!^daP*^H~cL{N>A z-|ug|s}tx>*}+j}byOvm^+{IedE3CAx%mh|U-V4wJtunx+CBLTdGNG!XB0?WQnHxz zmDFHn!nF%Y{FpEC`g!fF(tG8ea+PhIEHp=!Ao4@vHxvWQI7Yny*~@re0)hHt3z@DR zJzsYC)e7b|oq*vZri|omZp?U+=S+45ww{uqI51(#>`b$((DgjqBgN680?{dF&x34J*_nO@c5ck5L7JYt{NOu!Wu9o=!H?EsN)ts7Z96_%hj8h2 zdYz6wUi0zs6Y-CQMcz-F_xfB*w`_}43~vd%dTS%5>~#YnkJgt>v}??SjsAfQ2SN8@ z?i(US79G5YFnmjXNyWKQ9uJFROlUSHok%4+*g-RE7nYe6zV={bPXCw|447G4n< z#~)#c(3;E**X?m9aT(DSo~xT=MLUyVI!)X6neAb&>9EOQi=HD}*`=iXBXt~#^d9AN zpY$%M7s*Vrwq~is54BC5hn*$|dIwI~ASZN#!5TJ{tdgw7#n{A#ulj=B-M2&M`2xgp zuCMvEJ@?M1IlJ5on%PUEvJ{&WRPH}^@07ZlC`0Va&|?m7#UpCq<)cXS=#-+>GBkT* zm|w=^I{*H~TQv>NS#1Ify74YvNq?(r+Lm-O{te8BHsI8XgUC3w`eU~>w%HFue2~j% zBM(E#hzP}`9A;tXg!@x3Ya%_`cQLUSV!I6=Jn@#L^qB(@b(1f>;W7t;nX5i$c}5V7 znvm1Wa|ERDQ^xyVZjTV;vc$*>zy6jn750I|k<>0UE0j5TqBCD&fw`PbgBB^fTFDGS zM6-#bY=V(@AV(+!kFq2gj^%PAB-VKpxa0+CHKr+o> zX4Ht+x!@J3JZX96snsB4>%#qkDvdJSPRBUl-5AwE>J_RRW-6T1{ZT6I8TFry2wXWx zxz3S6<${Z?hb*fe(@5TiP?@_2b6kbqETJ?JQ9dVbFUvX4`Y_ki@k+n2aJ{`v%7g|j zc@$Qg@Y5IZQb2$7r_CnH+1&U{C0r}7TgswOzeUSbUaM+iWy1IcQEC_ zp*Bovh2zbAAM%Onl1SnDEW}aleq`+#h?^58vOX4!MM7|1>gCN@de7q zoS$ykJ>jD{*4pU!Vy}~eo58|MTCUl$Cc5}s|6-(f4YPaqA<4GN9u{mYeXI;SST(x{ zZUG8Jj8gE(qmJm19OMd3mRT?l7uR?PFvnmJFCXZfq>i41tPPdl-<8smOk>VQHTT7? zkwyBC5K1Z&5^37l*T@lYv45x{Cd2Qq+UR?ccqUBN)%JcHR5y zgR)1XIK`V`MR=Ss(RI|F|~~X!mRNBum8% z)v9n+I=ylflM&SJc7I3H0l)AT&ONFZ9`KGPk59FYCImL4kUgTUX`}5~i~HhQ)V{gu z``(AGu82rS=tg?WSadfEM)VowvFqSCvj*4Fx@uohMUnNu-ntGMZG4I7QK8JqB z$3_boTIJ)PlzSSS`20tNoGA*auMhCq_47R#d}XlUPDvI&CL!m^!AG|8jnothr>jr$cwWY6T!xxUXSwYVDKS<&KO z76E&l88heNILui+&VO3<(wlLO7a=?rPbloPuXj3OKTa}u@iGxV)Z)7K;a#q2&++2M z(7KDY=pM0Eqiv+cnUKwrHQv_!V*O1IW{@KLxs(yeDY#8vos+Bx7Z>Y|&qHjY!ukQN zI7hpahU=9r_L_UeQ6J+?(qC+BGQLnhdZ94U0yfO=fI&^)kdj9yYu{!1Ir!=I-~ubE zx1^H|Go6JJ_xpVfk`j2hoSr;_MLE)N*?lp|mu2-uj0GmKl0Td9j8bIt?liu1sg#r> zz~I6JB^u*Gzpuv35bn_?rVpo8=5(G6o2s)zGct$$aq6Dbt6Lxp3hoKob!mqh69Mi;kH{FZVk>N2Pjx z;NI|1T`Aqb{n-H1e(X>yRYagv4@m>RYhB{wN*VWylw3U@(%fxvPcGKXsOFr_wKE!jz<%@k4|x-#sn>|ND@F&Yj^uv#4>&fLF&7em(Yg&ICaeAvVEtouHj98v*%&EvcD zxJ>OL@}VKh?B1#sbZ?v@KORiB#%ANq_E&Wl>NUEQ=3*7srG{oYtei97s%9~8WuS7; zNU#fA2e(0;=@Vg^i4=N@`a2`h{@B z3}0bDtwV0Dd;Ys;b-qc}+KS;K^ug&*2zC2+VQe2JgSm*~U}gG`0jMr-#Nva%04~<)444cfF*=AmzL8X}sIqio^ct zbBHEPphx4*9vvMAY;%Tu96uEzDQ_G#+s_GXoyAR4?=RomYl^07Jy&|zJ(Yz8ul(_Q zaSegT#!kTok(@QYya;AKznRW78M z9~Y1&*QG?ac@ZgZW04s*dCaJ{)LO}AvG5KVLJzktDoPQNpf^1;Iv!$?w{R>?jgFbB zUW7%TPA0aSiEtxpMvVz@;;qcsjS5>g$k7zwqrp@i(^zU6J6w4Wrq$IlC_Cnti#Xus z-x%uu(8A>)Q=ivM5Ne;d;9E1RqeHjO}^*)nUas*Re;f?N5-B5*z#a^O#$gOp>0y z+zyC|?f9+?XJM+(wkhwZntQTn_@7y)NQ?*t8Gk|dHFomJ_c52aJL>4%)B)F4t-rBv zbg-p*K+659vGCutCX9cXvp4_qX;r_nv$3T>O3a zAD{iW->vUjYpyxR8gtCKtn?>evY$L2)ZXQ)_AZ?n=UB7njKT;O4$&RsId)>0r7i94 zNgq%zK5ci-f6e4(Z*#OiNVwdn=y2NG^h(DV z;_v_KegYe-whZyyOpAEse@m%fgNUI5UvD!zkltEa)6&U=@CAA}x8ur7s8P22{UnsE zXnN|JhwR4iiN-VExf{C^s*$ox3baK`JsWM%8*C>ef}2C<;-OA1`(m~y_L)wPzVx3Q zkRz>i988MY@3$OGHhz3sAra*ZomoBJLtF7z zQ*F>QK`nQTZ}u|nd{9BAnjGz%c72k;V$L5+X$~8)f?H!hAM~F!dzMbNO&Xx(>Lti# zA8d%L*^jlZpUqZBR!&B4cPSg7%(xS?xgF$9(kYZhuv*>u$m7l$>p)Z&gxEPO;w?qc zgHKq#b=2;ocK-ev82|0}2W$KEtoTalhQBrR#i1*R8GWO`6y@gjY?mxHu{=@-V zgN+BnUwEa%a<{a+%p_5pt2c&nf-lr@qxt9iGZNpE@zw8jy)OGijtnJ%!6d3BV~qCE ztSXzWme8P^+T||klh9lAq?`i{rm72a2}EC=4NWO^Z+<&}LW#?y@@B`zXyE}{)T;Mw z_tRr9sKM5)(8-H4OMNr#eKgC+QzhKsyhM+IAq(i%m#ulrn)~zpKY#FAwvo_XOtw^b z8M@`;XfVH~|E8nAv#_TcLlQr{SL$VC=_xzE{zR%&pq=luzG$FL|AdoS@w|Cr++{oP z73A;1?r+9~E+-^$uxVEO?=r2weD!ZX=mDv`+gZ-X^U4bf!K3CzjS4%P_2zC@#c^En zhcpCxL*~n;%sRtFJ$+{-Jf!45sDAWqPk*X5zW3e4M$qNwP_H;?>QD1XluEaj!8K)FM=mnm_J-yuI^^siw#~iEDqCZL20UN-e~y(sU!m!OV5X zj;hVH>3YJ+9`i@O-FmN`H%%Hi%y(;2%F@yKn$;?)F-U^SdJg5kgqFcNR$U@QhZ#wY zM_L>vnRIIIw{osJXxEGM6;Oza6F9o9HnSa3m->{F%*XhD)vqy}Vb?rnAnYMtkgjug zJ7Wz>cHMrL@BU%@TT8Xa`6A>91GB*6bl-E4Da&@!3ESiRt^uE)^zz~gKu9Y{d;y&c zI$Xru9R;m4(3(Y^haUKyq{30N!&E=s;pKVot(M-0V^#fHyP7Gp(g;{nUm0Qpw$$ng zs*X>E6ah|W^(M_RUq^-7cSkK>56U5n5JO_YgO%vnTi*nZ%fCi74Rz?{A)tOJr|4`W z6;wA!zgT>^N273o5zL-QNw_S>Tv29wbhZu^IB(;7TyG$kc<6LmB<6rxJ($flNz;3V z6v*{dd{Y9qTIu5E!xG_mITFoJ?NnQuMK~jESmFYw?emPmczk82mKQQcx`GG0-RpM6 zdVs(F??wbM0`duqMxMBvSNQlVeuj!7D!XXPTyqSN=UmfnPo6tLK_sP-{?qqI&!3{D z15VU#rd%QJzC8urgfm-MmRFxt%>xzQI}{4J2#?QCHpK<~SXzMh*Fn@w_#E2HuN!VR z7Agzgrk?v_ti)Cg?D&O!ZFdOd^59LaK zpS8MEf7JCXoM?I5uF+;Xk<$pnP3Mi-(aT49T!P_(sd~Pu&2va>jkx6gY?CLaTOa>) zQ{S18S4LO0CT2bStDL91-A+GF)^-Q_1X3iPI9568yR}lfli2T?7+mkX@D4W|pM5z4 zA-HJ3rC_4Mv~XG4o)j~3@FiP`ci!*wI6RxY$Sq=D3Fe&I(ZlQvCXopy3ar{Y#g;m- zN-YY)n{cvm%uDm3j@s+)ysZYk&N{%`Nf%qJSz};zUXSnffFN^hOS<2w#MS=1UQfGT znNz0@_kve#&m!m5r{3m#Qkd8&-$0_?76ven95c@fsuE#sRMM-0J46!Y^mVG3KFi-+ z(UsWzZ_lucOs)QiUE&XBoejId+tLgTj5PbA$yExRv^o1HiPQTmKoEx$punFE`yt|0Z;O5Te zo{veyjW2PEE(nbJ@R|D-Yys@d@Bpt>gL7P8Bx18~;qvqjXo;Pb zV|+iFSpplq9o7P~Ed=WAck-L;9;RVG2yojY!H~ejC$1TAOnD~o$wg!^ao!(?s3(WJ z!oO9C7Sc^9HZj|IsxKr;9^}}@pT;(%3RC5>e>C*vzKZGmdMWJ0QH$Vw>Ei5y zC;DKHvCM|_Bd@isL^C)g<$3KQkC!){oo~3~VX)PeHQABBpVh{kPN*p{gN?%U%=3y% zPfMUkNIs6|btdO$8#C^z>n8RCTcqbF7x2g&vE6w`GgoRgCH`BG+qif|{8`}u6NpPW zE%GJ%iK2UI^;}X&&667m8L5K2w@*UNc02Vo&!tOvU(tU{49E$|>9Sw&n*G}XU0OsT z9H2u{eSs5jRrI;^^j{H5s25Q~QIS;VI5|T7>0=djf;XYqN-ZJe2|NUEHCtHh%^r^F z(7$e3Iice(Fm;=a7sTN0T*)j?kG}^kA?OON01IhvlTT_UDs5jx)p}d#E1LGrCW7ZV z?{kbc%F$^>-n7bkJFf-ux~Olo4OwrdaN~dKl;c0K9TW9)7%T@pA?zili^fB3Wh1Ab zM(a4HCm?05zjg8OQLWi!lAjxGSUEGy#CvUHU^wGzF$Go&iGb5zgAZztq*aNLNPlr{ zG-|!rZAjItP(oj-f1?$}&z67Z@EjS&$WQH1nfsm}tkFBZ0G|b}+i|-*e*V#u!6)NV zHOKSzG4a9P%db?}tV1ZWTqKygmc;}ulk}>C_%3G93f?trP#EiS^*TjLEb;oU&l)b( zR63Td9xwzxCHn5m(mlQeH6bidAC z7xw996RGL_7xoY2onNYMT({k>B45eb<;(N(T--C-HLpYb8ANmjc4`gzOKYP^h?h*x zK(B9hOVLhKYohQ6Uh%2z=Fd`DquyhHa+PyNyCJe~+M*v@I=dVD*aMmiA`ce)X{ z*u!N^9#>wOZ0P3WD=y}wp0hS2TXjr%Awf{SxyL^**)3o{w;0ri8lXldyV{LN5Y3fW zwitPi>nB)MJGJ2+eUW;=+w1tw&u5{my#fC?XSCAQAfHJmd#m#LK&Qq0dScmX)4KK9 zcao13th|o{QncF3N*gJxmQLzp^uOx6Z4i%h98c$-DIau_kn()$$J6yN&4@p9tU5iL zJj)$h2tQO!fp+u=#Q;#-EZg^hKa4+aTcexQDAok;&uR?QuE!{%arKdbE9DDvcPzVDe`)-2Tf3WG$W%W@N z%EdAWdQmP%YhbBbhRXE3G2h^-`(5YAB?v5=lx=iA)$_jZR6hTO=g~@Nbj5sW?0XPx zC={zFD{SNhdo3ex&(B6^mY=6 zN)(UVg5IFDunFzgJf@fHI6ID>7r=-pd?#Z5P-W-9;Vhm?@aG$>-yTBYg4B094uDvNZ?+4R#>OMzAcB$-^e$l z3Kn9_%+rr%zLgyiZRuTC5^eJGK=1K{rs1kEcg3b!)Fj)M2eZ0fIiiA-dnIKmEqgCV{iC&7CuywO?%h(Oa?vD=O3Wi zR%ms$lyqpAy-7zuIk|Msy^66zoz&{B5yYid#So5sbbHOfvSd zlGq1MU^JX|6(Wy{EIn!{Xscbve$Gs1jWtg->$#mb;7O927!nelnAq?8&NIDy)if(< z*e*q+w#qzXr5x7gEO~z_6+vK>lS1Oi}ta>aOH!_8x8rH*9W992VpR43&x->Wu=S77l2;t@I zYIbMTF_fUBpyZQHckS3ao*q9mvn>z7tmbi;i`}cn*mMHkrHxSc)?z^f9h~E{Tv$b$ zui~H<#A2+EH=1cu^K2EaF~vM6889;fj?BJ_ZFnZ93vs6vW(+@5fm8g>{#vD?* z^~jpK&@(%n5N}SeL)D-mUG%&&62@q#4*&78D-O!|tec(WX=MwwYHswN1TJ3eGT-PB z>YC%RIgK9ux9bLu9s_Rl?0f`_S7e*6BM^dsZh38s`U=DT@4y|@v!hj=98X5{%rvxa zJYDR(CKDN{=5~&k*GDeD0jMU7Tg9p7b_ime=*&NINS+d{(=r~h%jP#ObV1fOE9j zH`e>q4~K!RQGRNIA^;fo=9+d|p5;fdMR_nQ+ybi{r?Ya5mdZQ*2L?IcbypsBK%rS3MS(f;x@dTw=AOqmtr2-ADl}{kc2`NjATuDb z?5|1t>I8@ou;FkygW^-O;|PTf-AES$XwPPR#7VgKtgG zr{Q6)-<;!y7-w7V*w-Ph@3?vW3{aaJK7xd_@>|V6#o{nnGNax1TCba!=VHZ%)m&6g zD(|l$CtiD|`S*GLFR8Cogy>T$Bc;vSXL3Uf9q7v~A+$lMLXRWa-;F=XRKT zg+rcHDq41Uthv=Yx}EKgA=Y!;GQM$~qkOh)<$#>(7r+*_C3QrdvX+KTEn-JCN?=gM zX$a8P+AXb9N8Jlim8xNPsS(sY`T<%?diI#n+G3MxocoRgbXO(;8bERtuT&xI@)Z8% zsIT|{xqcI%dq8g+*0y+PtsUXvogWJ2)wD={6*&13i~Mk{Uy%dqzX+or6reL?NM!kL zax1_hMvtLAg`qEVz-#vX8A0ayp5F$MS)56~1etq4q|8YU=4`$<2$=oAAAZ3RCp&4V zIgfu4ToSP_Rka_r#}laVQ(2N?>t!&V;NyrG&5B)hlyo<%9l-Z6={Qz~`46t_J`Otd zq-0ggc71G7DlNl9(wSjJnzu(Ms^+Rr&|8q5WtO(%Rm5anK_To=_x<}hfbWGRsqb&= zNfgfz?A2YIynn!vWcm0Uh1}iw$exEW;4t-`;q6LsUXX9$#LDw-shew;slN#AsFQz4jHkZ=y)=urcSf5Pad9@xJdc2mzCqP%euX zH`@%tg+k$095K!BjM=ain@W5UM6rAQo9 z!I@}!NqQ+Jsb69sx(l1(6VQZrPt*CGl9pd@N0ax{DOiCjRE?0Yj9gA3mF#;rx^wI95m zI_%0a-t8$_X3JY%pOP4954hcLrk>t?jBcsrk>vI%fiojg;QX*Ox@R=NUrt?pEj5_& zK7(P)f!JYVL_0E|dSs6IU()yGsTI1ql?uarv#*40t3b!+B2NB4ws5&iBd%vOd9I+a zJYs6fe0H4Y9J~a)a75T=j)S5UI}}bW2HI(MguxcS&nwkpsUDgtJ~|0{(M&bE$D8aD z$+{4q&UoH!k9|$4qi(c%V(Q7)!~i(N?xnfpTl{%4?_y|gx`(6sd z1qDwf*VWRXAXlK#IPcbkvaXg);84-QZfa{$nwBPs@+8PN7`O!9q~|yA4qqI#bp9Z5 z`yal2*QQi@2SMrIgw7}c%=Kx20g-j*vd84i#D zX@7tE=;8l{hy!tGC|lTU$r*FSZe3$s|p#an+*!O<6vlM{!lX*u$=2gsVArV~>&OSF1( zx}t2XLax}%cWL0ag8RnAT?sAzI}p=Df)sw{ONj@Yf7?D$8)T#R>-Oy@aRtw56so9R z#{cYoR@8CJ=c$_BvpAaZXor&IU58I5 z*R$oaAdYd=z4K?sQCCf-1iXt0Le`aD;lE=9ArFL5UF?fJRpaa)dWV{7+oS6C$@(oH z!BN+$NeT|X7+=>mg|x8%mhZ@ZjPr<rh~ln2U`pg<@l=}xWcq| zv1sUL8LSH*e?ibbo~m|Ddx2i9V`UvWH{@SA=9V`P34Nul~&3KZlCP`Z91U)`~ zFx!gUs9`kayE)1o)u9m>p3{;yqgHip^~7(@ub(E?wvx8*z)@?ot_C{05x`fX1ar|! zPjtk4I!;?L8$7DTU~$m-U7-F}NekXbVrmZHHYyu~s?k$wPq(m2yy+dp-D>EgKQn2b z$2RRpvu+*D3NoiAVzRTI;0=eYgQUJVOUG^PhnCnN)bpErK8`^@=>%5l)&AM-ifF?E z1IJiur@i*et%|9nlE*D{%ic_9rY9Z4FfQ;&b)1({`}_N03X8@i5bT$NXG?OGFQb{X z$p7edW+8z z!!15Ph19Q8k*1{m>=du>)gR->2?fr~c^SzEdjXOyuIhAvlCEIN`CD3AM`}?PUY;)F zkansckfLd1Zrvl5{;DA$!*wJ=BQRnC6K7@{igt_r9z7Yc85F)A(sGt>OMen~w=~RJ z^-JD>5y&2MV(lrXzVqa)LV2B!*{ErAU#L6LMTx!86PeQUI+Ps{cHDpbT>zbc#19F&9$Yje{ zrc}5%pF`q`$(L6I2LujnuDV}X;WHG`3fqS=HTxr`t*xz?Y{X#xnRKe~ktnh%Yf7k+ zZ4u*;q{Ph(4O8ju*T>4a@L}WFdU}4pTLrNPo71Tnl1o%CJIIMy&;1$76<(5eG9`2v zKa6B8CJ$tAJAWPevbr@QjB~KkAJc=79>3r%Om@`N6nJ#0$Des1P{g?Cn90H26=g00 z>hb1cfyy^j(~7pZs$L1`}y9DSiRb#CT~0SUJ`E@vHb_B zDAuqZLf?bDxk*fg$*IF0e(}kvVLaJjXm5{;7F1i<&yCiyoz~4H_SS5%nAJ(gx4U+A zk1q)2f?nh=AE{cQ4_ML`ll$5MbZMZNjnVj~Mk=UW({0^Yd}X`LdTMJ`vf=A&x<>R- zB$>zPTG^N1k<(4$R(u z(n39T80q~C*czx&YiR`P3t{c0byf*icpiAn6w*b&G;v}|{7Jws);276zE8msZ#{gO6Zj2CcU#22y9=@6W;Ys4un@FXiqCy^}A?xtU7OUdM7fy>1oeJAb2ZG5f z`3$(Z%ZtL}oeHDKCIF+%tlNGO8RIK41%q%d@XuX+~N7@M~~L^al_2_#tT zeXk~w6gf5#IOA<;dJ!#9oT@oIw&{ojB&Kz#7}D==-i+HS44fa;g@cHOnJm^Fwf&pd zMmYo~X06)WChJAY>^H8P)Ou2S6lITR*?BM&yJ)>$i(9_qo{w=Pz5o4~xJ<>mg+hSa z+_OY;@i1o?)!bXLeRgjgRk^n}qi?%;o*W-{;b;Y$+(9UtV?%INauk!t=8tVgD0sD*9AG%Q)8pUZr z^A573SuLH!e0)5x`OA}CM6b44dy<_KI2kiLXaX<`Zw|z-%+n&J=wL#@s zK9`0%CB>~r9?_lNFQYjUUr{HmM1jPO*(UHFRGRW`dJv;3v)NYGi3s`7XqMYm5!LjK ztJ~OG=Tv(P-7X*M4(&nUdBM&*^LVyeE0%;Jo=ed!z>aY9{H^<*My1A?3W4R(oQt>^ zx1;8#iIEcFYB#z=a}aWHBG|7w=XYHL!T+|JsNyeKm{HWPxw#f6r|qoX@4N5AXDek&)g}j?Wx$O{On*csR%Q zm3|%X!TZpA2?TSA8yf~$!G-J66bj5cpHfXtO;Zdu_9JUro3gpKyNAZN9ttw()Wn%h zgZ&5$3=n*lBrQsL660W|jI36dR(Y-IguC_ecc^9e4@;U=t+v!NyW5!@G?@`t<5-WT ze(u+8jctSp)D^ONy?XZcwHiC|-6<2HD-fEw5d zpE2*=wzf7w(<6_|&;0`ZXrAZ+V7l|n_(lLlkD89lQU7(ipi;>{oSG2^G06slSno*z zMdw3apu}YZLq+sMmfjdv{1U@Di&M5UUK)G7Ej3Rfc0ydm;b*~FEL3IPJvvjMu!5XS zTx>ig#v)tdNgl<>0RJF@q_ zA5K)#o{+!{H+Aaf#Z2dTfqk%?GCj_2?&&u)f{OE8!RwbH{Ac^fKLLIUN9&%* zFAQ5MN?T{A@rcPASqFhnyzu)p;-{5=t7`|HW#P<5ce;4uMFV}jxWXBQZdy1|6PB+4 z?hwfJhb<_(n41HjP=KzJj))BUfgi=^v57cavrYtsB>Y@{-;ciCyezw6A(8K%Q^Y`l z%Y7t)?Qldl_k$U1H9otAVB?*Z@Eh{g*AZH4WX5NO^@Jy$VzRlh689tq|0Erz9ejK3 zI8{vo+)H$ zX~|}x!~9IUASJMEi3vajM}Jzn-==(ooUE#_SJw;e z!dBR`E>lolwL1E7PNV$9yzpi}=w#(p+j8(;_cn=k5D9M=y7xp;%mn{CV>xrSnkS5hzx}7$0UgRSJQ+n#k7%&Ycs0OJrlrkRn4=*_f2$c^LjI zygInhxr&7Bh$Q)Nx^!?Pg=nYNZEd{TVX7iBYbd7FR7tX5k4myuC8ODT6qrbsY;1+4 zXBNflhnMHzuZc>&4uAn+zE9dO+BIjC1VImXBcn7`-$baXnydpY%J>uKt(k_zIoDq> z#ATQUa^-+_L)heeplS?xgH2jjVKNB#d;KEsB3?G?LN=4pscn zJsb!*!q&SFhm7}V%e7<6=wzEKX?PF}tB`0;W>VJ%epmtTh%N%A!6yrt3gvIQL0`e_ z%clut_aD#rGtb#NNViHZ-sxe*d!~8CB>p*@rvf^GP~Wu z+C3)c&$sl@o#({UaeHIA^F$oPes+jKn595_k=Rs@wqk7eM0TBWX>oRBY)XY8iYV61 z>L(>pI|J;=vc1>pvjoYB^n-#V$pgo9K%7{8F82`yoh)QbS2Q?$hsp1b&wV$4I2Cj$Az26UlKKzp zWW)m|uYd0%NY7pY-3Z_piAw50i5Wshqst3MsUmsxfinw!^%sxni(htto)3a`1*I*< z)@~2`Z$Jp-0Wy%)jOJucL-`NDah3E(t$3dS>wQ`=?jp+zpGJ*93y^7tC9M1S10its zBf=CiAw4q$`pI+zX$)xvKMU4SEs2Anj%{aqqCCfE0z6EU=O$X5#|@^W)cu_oGm&th zfDGwJ%EWsviY7RW{ z{`^1283AuqA}TF&nX-Unp-CIbmVQ{z`WXGs`w0IN7KQW(AvS2nUiY}Ts*N{}K-apo zQD-8sD+Rf`P^87w(+s;M5m((ci(pQM3dQt4BS! z4>`Nd#)}{-(9X1{onS+~`R+W>#(WG~hAwm4rBDeavqSn%-s>WfSAY9vB`qW(EJdvs zDwXuoK$&`S1IuYRMQ{r@&m-YOjNtjsABYzD;m(ZlzRcJ;`B>V8tL66TYeDJi3yR^& zW#AfVm_nQ^V?jfy1sd*S>K>?9I?I(iD>)OvW1!DB7VnoQ@)L!q(@y|Pr=hMsoZA-E zlw$F1vjI<6)RY&OMD;q5*M$u-|MHu^Zwg2uBKhmBu^+KO@=c%p{T1~HykG^3kVr}q zEM~;00o5Y0qpOE?fePuE+`kOD88N0Y^+fLc;38)ofr8NEApgRL=+l*+p%yBPv+40SZqM^GQB`fYa^D_8Uv;;)~|zhQf78 zY5M?e-bIK7=OuaF^bqie7V6a-um$U|xA1&QPk*jcM8 zuqwQbvkiZ@{wepkK%EAtoQoQog=gC92O9g8s3;#d5;+CU-ao=WY4@Hq#L8^sveL#T zlaCVG9?=|Nq56_Iw?acD3S;L)ZcEp#F9eyV+va!nIwclmb<}`*oOl_TLx3A08$%^L zJaM9$dRUk^yK~irk9)OvY$ZCZ8m>!#yhDIdN4=}28NkfPv8MX(sYiH#X$1X}T^Lq* ze$mX&;=Qtm@8hkHFU0=7tiP^!@>dLm}jlx?4mb|cUi?MDe zCu;=+^WoVw$5ZZXL~Q_Se1}6Q!mpn--lO;+5Ve_ynKszWf)hr)e}g4%cpJ*)Uu`)nMt(8BfmW0HkVM*GCCWKzT4MO(OyRQeY}13#?K#j65)WHe_xesz)y0t1o`{@4|Zfy)aqrHdFB(aZShM0cQ{`gfbG9ZSNC z(Vj)4Qo%Q{AJaT9hZ3pC;b6i5jUy+C4`~FWXls$fj?D}Kkp}@>Hze{%^1ABya}&T57u{Rsb5X7HMQPXQcfqBl&q;*H-5g##$c`I@8M^A802 zm+wGll0SgI^ZUH&xT}8q$1nP1^vul}x#v_&RSP}iK>J%sPuoG;0k1#r9Ho*Z`>5l# zwe3=eTM5z)`x?Bxt?(2~R<@Mhdm&aj=P-)dYNS_n<#Tcfi1%qjs$|%E>c3ouQFVIZ>b3fA`Z>RSLa{SX)YLUdXzyE5y21b|cg>bRULfliGtz736%`%snxmvGE zI!6D3C=Ps?gphk}Pm41pNfZKzXrnaZM-f0T(D5x_f1LiJv!A$im+7whTs7KZ6v$Jp zd1v+=LXS<87=dsJI+s#r{&G`77b1iQJ_d^) zvBr0PJLi9`(p{Qo@UlSzDIm9vHTxvKhkRZ4n4#bOONiXY#Aphd?q$SwmEp>30UT>Z zPkEWDsTD=;qld#-(S!)9--@?|h7lBqTXFmUb;q#$0|?k|a6f;e!g?)5dT+um+9DOJ zB4tZoJZOLYM=7Eh1535I$*B*_?jQG9U-Dn@7mk2m6)Y|DODiN$+}cM8Ue1B7gR|%# zzxmGu{rxMUayYmMf1O`lQUYX`(rEn4p#FJec9girkA03CT#Lm@`@8t}pM9PW_W2Jb z)XVN0@(e4(9Ue!Y@sPGhUb5P&4cZO4?W1EZpi@(RqNlJM#lp3Ga9ZA{1pF)J20 z{z`7+nArcmYX6!@7AnZT$OC8n&x4GAnlTF<-*~eC*vyTIc(W(}x(r3n0Enh#jgJALT9oMSs3!Opq!RNkY~FT;@(Bnr zDkI;;PZBzr)=1l0;@`(35d6y^uCTkeLfnAOYPIS^y*$gWN`!F!n7b1qHs0UPogW%_ zLB*=gC`#3h$;oZUSqv)d-TNE*??B&S+`GM#qfMoVbTn^3V}jV{pk+){4Xn{5kF*xl zTjX67O?HAj&a!YHbKLgLn_=k#%kN!o@|x%rl*zMODj0!V%ptcCV59Y31E;@pVae|h zJXF3vBEY&l1~U04@UwuT9Di|PA?xPm=ReOyyQA+uth+ZvT&cJIW50uHLQo_cY)N<; ziCBG$IX4qC0}5~P0?tXx!9_e22@~ONv`_7RzI-@~6*KGrw;B*^?mJ$?%;tBPTdeIL zR1LT{3z)b8(xro)f^@~$*)#ieJ|Hgv8+AY_JCClN#>~Gvz>Xc<3gAVjN8iVsmm^>E z@@hdX;x<^R9WRZ)J4Ik-;pFl!WgTh^V4y{#g^>}}kMH`ZXZAV&$BlDYR4moTmevvg z;lglDG~D(`Hw?uDs(I-@E@^`<^^HtjBOH+gBBrYFUKd6h@&IFANBo)OPKDh=Trgtz z6l3ym_2;$O4HaMNS|A%=IbXT~vA(&VMru2XF-XPng?A6(%hLFS#Pjv?uN+{2Vho=)vmM&sj8?P=LiTz?rBRb>f}wcUHbW7JNVxp zR7k%RX04zNb)<&N8LjHMMUOV4L@=>%L^|`kp1h!|FWOyt6;k^F?!lRCkb=Fzepi($ zsBkW#l^)9mkP3N0e-~@$Q*Nl3`BZ6QSe;L9bbbiKd~01CP&?rHxCmx zp!8~m&l8oc5Rzp{3MfB0H25Pz-~v0MXVf!8J&7*)HT zWS<5*Y>hO)!F z0wZB~w)Sre_%Cr=Nd;gf!|2A^%ijs5-ZTk?2eO`pkng_@haq=g$rNK1dwkml*{fxa zocLg>=w#L2R8BJlLzIVk)sF)9>xv6+r2us3ht@{oOn!8tUFBZv=%5*;1-j9+&fdfT zig4}xFE0^qVZU0FTx0&+8(A4%)}W@@Ix&2(-qDzXQ8EL#XrW|g$Xtrjt|5INUfGY! z6Mrepq=3-O?`*?=d22zuKp6lWVTp9G_5R{9F8^bC^pGWfml7VWT{VH+idS;VbfwN0 zI@Vk{nVE3@icH=V;;L|Aa4TlOx%)Qs5kh*<$F?>@AoYn|1`bbBz>S#Knw{;M9m*=r zU5pCYwE>v4uJ(}tq0EHI+K83iJ@B8`4&B_=`~+m1O#)~)e)lq6@Qckbi2eb5d6AVK z!?Elg`UTV91|t(Dma_$ya5f35r&KPaQhU4(MJUclKnE5Tx)N~>vW)z&ZfRg(Ad3x; z;sMKZgr*?A@L0<)W*{weqZDLkXCW(M4Z|XG519pGC;AhKKxVcN?e&Z?E4Rvc>`#%3 z9p1`yJy-aMdvrV$^^3zC8m z)ISsOI9iIo=0X2%HEPU&imLYN{QKX*XN3$QvSlf%i@zBw>mRpO{(aqr#^K|Vx#Ki{ zVj0fZmkUY)S0jL2Prg!p(6$zHTdg;g(0=x&Mr9hSO@wrGw_>!Ag5Lhrv@}}lB>LgI zaEZl^(|w9M2hiN9t+}64US99Gvq&=l;z4y&T&?)!R&xaq2^lG|ovbpmx-Vmqu_%DV zPN?I~~fX?_|h*T6#YV`sN*u_TN6J;V@=%MTmsA;h|y+f)tg2K1vBooLCi_iaF9~ z%UCotZ}fCQQoDN05(deFrz1Lg%}aTG(Um393@rQb(*$K+#?Q+h@bXyhX5@pYQ4K3=bp5R5=~RNRfoN)B_3fhwAV_x= zwT2^VK{K+)agTh?-CGtUz!12tyGf4~F?yp9s|xxz1VR?@1nw|lz`^A0(h?Y!NU zNr!mrZp`q(;kDucRrPbD!ilF_hR)F5O7lkpAD;*SSMwS&r$Td1(4vyjgJ$;T%ISvq z!hPOW`n7A9oq>E(Ek2wqj4loM+r6rP?YjSbNG3sm*?!p{8EH9iSbo&cD(18RG8jfi zB%>d}#+re%PxLx!&kz7Byy>{FkbfL3yYW0X28w(d3KZ($9=i@7J+zhtznnB0b5bPM zbh@9X9)c_$kRV;OP6}Y+r-9j9GT4Dh92;z;8n7&u2-_lHH?p*cQLjrY~^m zcz9~SJ!k1uF;k-KQr=~j01@pttMJpNsJ07d+xSNS{#Zca?oVnWtI<|B0RhU7LN9Ce z#o6A%Fu|E}e)2}&hUJ%mD*B^?Yill50|>X|-qwnu457;OS;t?^uIVAbl{!@O18blt0u-?+dbjqkCYv z5AfeVkF7BbkkEPHNRqx{x0?NS@zkh80>u2HRMsb+{cOo0luZ@EmN8ADV68<9bYQjDH5YIP^~Y^R z1Ax#0E_ZzFD+Qt#HOo%S!8M$I&{}-F%dM*w7+;>y2;F3A8JCm4dKq01&rgY*m8`Xt#^Q zTxAH8#$SxO9dxc&-xLg0mOZx`Kn|vf#4tcC(cgs)#!_cM-G9L8GlJ89 z6O-2<)EI`O&a=w&w;hQXg-cIm_ASomGD`5Drlr8!3Hc#|6!<@m<$r(W$jc^0Go}VfF<>b;< zV~!lsVug+}AedTfR2wa;bo?#8AtHZxn-lU6#o*tfZSr?89Kx!yv|mmfjm#`x;KP~K z)u8y>(A2S9bM@Z+?G`6KEaz`!^Gc_zgP0A zHG-VFyv^0BsVE=zA}$a0pTYS0e4Mxs%8$jbG_BftAiGZ*O6B~; z`dG*zJ#gm|{A%9z_kd*nX3*K{E9-!@;$je9Dd9p=!2eK-tK}h9tM$6lI4LlwyU`Lm z#}K_;Mzp^d!yn@;E=CIB@>O`;*_@u4Ke&~V3f|NZ1x^R-&_e<9JrOL|T8?KF zaCq~9VWvqs0m2qmGF-i9qYl~z6nfIB*4Q3d?$qtDb;6ehQULr)pr z_w=L%APH{Mw!w{VPxXcnY?c_ss*o}=OUoT~0%OkSu-d@p_{o1AHlYkyGa-dq6&jR` zD|>)}CpbW`SGCa&_(hJvuBbN!x7c>R2W`K6b$L!8-{38|w~s9T>|T)Gq)>hf1Vmrg z+PQoqj3D+;ykfl7E^^jwn8f%Ol!OFQduWz5+)?g!l6u!5r@UB9RXdH>&a#Bxg1Byc zp-+eGvUOZSTWy8c0jw5H7)~({uosJfhpAf~uURac=SFO9>B~iLrlnq2QNaO@CIT>4 zN~kGfSFa{w7Cf6}LjRKQ^0FmQBi#7o$Z*L0!uj%7zxdWUL|J0NSkozS;B};;C*>Y1 zd|!@cx-fTF+G+aO%Gslerl^<$ZLw5$+#2a;3|2kT6OGd;FEO8b9Vu>M7ET-DAueoi z7=^XV9eZvhL&h9(io-l!Q9jNq8z#60Pi_nHGTv=QuFs=5Zv}N`@y_v;oA&#}gy3io zWRl@k;B+sf7k~is&cJt4+`!b=x~Gjz^E~%86F&z#o`OEkod=GoUJu4W zFM2jXE5^I{sY2((D0maFTW&$tP`ACkaE}wqB?3rOb$Ue_yV?n1%C-dd3VNkgM@y`0(Nvn}uP~#QaHzq5YN)6VfAtNLaUd zbBlEuu*zE}7hn`9(E4^B4P{<8;;MFWeyc&4CF(o{k`A0iU$;M|e#laFde=B00XJ-* zb1k-AG1_@!;cCC*o!zAh8rE;iU5x9BN{)kODRVeBwgfcNZyGRp9k%Rc$2XBK zI8nt5kSzZj|A{fcLou|eti6B5_uE0l?4aB98rDSaBahSa3F0Go)qFqj*drE!W}YSC zzbW{WcY_FKpWhAqchdrv2MBBSE>2EI6V)#C^z_++r@GR7=aVS0(AR&kQvjFGlpM~I zUO2Yq5&fCzqQH(xN(vg3s=sI$rI^`)+(0riHWnYK47y_931LFY(?9wmSCLb?M3jR{ zTY%re(V4_>dAmQ-R5efQghp`W2%CB*aCcM~AdsH_$KG3qMY*nh!#bjZsFZ*rC8<&h zN|#8ZfPi!%7j6-^sD^AQ#~JX|1iTHfM(8DR&G}^X7d$4w4f~*yI?N+Df=I2x(Xiu;BI=ap@w8|$C7#R72tgUlC zD>RGsXH7}i5Et=7dkiLD!x{wIbV_)<#l1YaNk0wUHJ3>iMQuyk-BEo<5Y*hu8;DkguQB z92f3XS1DXv4?Q85toaV;S3k1OL_Atbdaj|SV+?Qo2MD9xfjdqbg=$rBTdSib^>*&D zr^1FTdB2Rg8@A>erUYSeQpZ4UyD6=!{5wvcK!Izg7xRf0tw+QUMZCTQ4!Sohz5- zvTb)~PL+8C?O%;?F3&68_f#A^C@H8_fl@QgK}L9N+r8ddb!KJWv-nkhVdX9zpQnu6 zO8FlEh|7?WHwlspxdK;wHP9IVXpQFD-fmle2l-EQP{OOS$j--a53?tF5&SO;)e0=H z?Xj5`aAg_fD6{bG3;{mmj6`g#u3D~47#}gq2|@gm4B{D19#|UAn>uV+Trxnt4ZY%%LQ@wR>_(2R}DF7(GpfH7OV9Z-(a6bf4*2*!_E!pjD^vEzj z0T~FKsjgxG9g+2hcVSPd5ev*EGf*$3zt3X%RE1f=g{$r`*|^?EwpV(R#IU3>f8Ba= z4?s@$?hYXCf-b!2tk5Nmw;{cGE}$S)9g(_wE}`1d>7f#I13z-xDmuWr{u;nISp{te zNCn`fX@4OAqfqmpDGIa@RlMWan_U`w^_-F$fOPwS;_<$C^mF*R#2%nod`;}^o7MSN zxIco`2tWg-v`oALI>K;Nl;os5S#pn<#Rp~b_PY0HdS$B#)70i!r%pg0VOFHE z8*iOJ;pX+NVM%&)^OEK#?nf4HyLqh?EualvfX!7BldWees$579GSYUNQ-7S4DXQ`K zr1gDuc?boxyj)A z@n_4<>0n)&$gD<6+2g&Ax@tbL%fiykMJ8aS(Zt=nvKP_JvQV^8PcX#BxxRS(icnow zUKK?4N$EpJ>y-KVX*y~uRl+nX6#5c&)aaw}3{=LXWGtzCM87l$!)9oQ<@K817vT4u z811$^IXY^D;*l)Cd+@iZ=Rge;y%I=~!X@td5?M3tz0LaouBL&__4k&EKP?CJ zG^AXLphzDtE|W2dBSv%^MOlbw@;{{Y_J-k#AarSs`; zU1Q(enNtE$niXT-t)qntR74-xw&A6W)Jq&P%#?nnr(?D+dqqe4X;n+~nix1ZWZI>^ zjTR@P>M~dvd=iUgzz4o@x(dn&Nkfk?`&vga}+SH__ZGKESQCUE^_p@AI@r|HrCg>IqG+pg;G@@((y0t5yrw%^8RTv`1ZQKuyM1HR zW-CWRC#4%%+egy^7Jnz1+A_vMgQv{8>Q-@r3d>EP!tkdi0?DA3zET zPmNvY>yP!(!3?yQx3{sO;VmLBgYHzfNlC34r{h2pT!B);!6!3Md?Z-oPbUW5nmeXD$ znfd6dW==py;=LPG9dq6ayiZf>+ZaR0yq=jLH!E>P6ujlB3Np0~WPW=GJ)3EdzK!bY;{MiI ztjwlxI44Crn3@p;3EA$u?cS`zNW~C~=SkemFxE`e%~k zq9#;6={6S^7YhsCWjs7Q)WVR@{PFc0Uz>XV3=7ZtmMtzWE-#x4T2%`J-fR!;>h3#p7etbcu4Q@O2^=a>?~)d(a*L&B zt6_4bs4Ef`i(NGW`fuw?x58GGzf7DKHiQeJz^HC~_O`V%L*~uioizP%%W5;V(apy* zb9wGl(-P#7{My<+#9V%9CacPhsPLs)lMAq?%eBp3XIN&fgx338B>BQg>uZ zTG&n4AQ%3w=8h^aeGGuqWl${lb)9z16>JU!qcCQ7y+n1 zT2)mQMSNkeVO4u54J|Dp0f9JC&(F3x3}Zua+x`4&rUy61K2DcjXNcw+U8(b`;?>SM z?^w7>Um7uKDMF8!+Q?cOIx#taA0l?<*6Y_3sm}3l+f@4d{nq=j61V2>#SbVJytx@i z*w>rJiY87N%)TNst(D6+ToDq~Z<0(6MPfD-t2c2lUiW#Eb1w+x818;m{N9xJnY(E& z#y79yvVhvj3wmi}O3PS>Ql7wz9W!Q>44OD>(e*S=POeI92e_^yYUhwpzV09%m6I0_ z|MSQE%n&636mUlYH-&TA=<4eFUsH=d@~hIH_y;aBGBGtbHdZD8G0n=#ial@gv!>Vd zQhv8-(To?o&C}jvaZ9y?loZx39_KZ@OnZRlvg}FxG9f$J6vS&kW+^hEIbEL1GTgRi zQ$?%pDJ93{m2<`SLs>&*J!Q4z+lO{oK3(hazMw=GT%_?B-Ecdr#h;%B+lONsUx+*K zZKO}TDi=e)J{|`gD|KZ1_Ik#^OXKE9y-CXGbH^cxnE7DD>$&kO z_sc|&M20I^s%>&DOe$8&))D^?8XOo{$jYS0P}I#NUX*1zUAIyA^7ro8vPJv|2s ztmsC2X84}49A%~xCn+MpylzUX9z7$Yq?8m{PuI^-xuS@nq9X3Z#pYF8cO_91#f3WO z6ua1QA_=eL>Ua?#RAC!4i?Sl(#OcG*5AM0m=F2whV3qLg_Qe{;hhN|zCXBugQ|CkxsBh-eNBZegg(rgZU=Aatz`lQZixif>BHBJ9(T zI|m&+Q&DaxelK7MaB(YP#C%?LUPa|oRod#!Ji!?i1F}H&_t8MDx8y+p)#$wdw7>lp z?{#dK2lV5aM!U2NOvT$x9}x~eaSG5o9s(<>rqEUNr)hwaqKi%;j~A%@Ril%igu1!8 z+4{?lS*9f?*Gx-3s_gjTJwQ3p=}Vo8-`)yr7`=l#i`l#`MK4h?+31YwAFrAeVtn#4 z6xH9?44ka%;>valZ!)4TM>?uj4i{E8$Uj|ul_qi?f$YW_G>+gJdShT_+BitEoq0{8kVg?b0I4 z;)*2%6>hx$p4|m$d8sfvi7+jyP*msLXX0UhkSkrJETx6i*W1~Ot^+5iY99Swm|bGH z?wj0@O=O;eL@_6)r4P|J&*`YuJO(8NWeMb!l5c@|4sK6C3rVd(&ijbkBfr9b6YA&Z z2j-Pty?XWd#q^)u_ka5`p!v@ey57i#B~eduiO|=t8U)pOY4bSJ_+nQY zso~nxaOGcdz1=)ini=6|j^7*p@a#VME7Rr6oeX-OrHd~X$aYuwW-VhMm(-h-I*hj^ zJ#6R;Mk^p+B=^qhC};JK`?x>OR~>+1zqb}TtuwNX2%x651SVh*__LUp+JtE5v4Ltc zcWY+n&=e^gB60vw(8gH(P7F7e8iNowu4PoP=Tru0jef|exVdsJXYcd2nFkIV(PqIy zKV9nSiL{c81r>E5Vf+jy2hNPE$ma=c4y z)g3?i!Jr1QUcomKuHyuUg?WYntoj@$Wm~r+#`2U3dEt|@FE%$fL6rHq>W0IQb?DUV z0~Lj{q>7A?AKV_EfGnK$yuSy7ZSU(t?N?hxCv4;a$Dja*;?`1@^$c$z%+UTAa11raHga zMi$_Et|Zr9>j@Jp`l>j&_mH?$;dT?7KI ze4C$CT=;Q-dm^;D`mN@)qpoRfZEay;VQXvqho&?ATODTmjG!b6xF$unSy-|%GBQjq zGyPTW|DtN4xF;#D=B{EFyJi@aSevcBdcwxLnO`A;Me=mKm`sS-P;i5!FSGp=@D3hbVFYK-EPnb07>467`dHHMA zDxtKjuC8u$^s%hUOesX(dBM|&oQJ^mUKLuX^5Jl#mDQG5jJ&xXlh-5F={k?z8oj zUJpP`(8ie}z3p%OsxPmN`3?WHi?#>for=gk>;3V?VT0W8QesKg<|be%IO3vyHXAUN zS*B1}#8>zYW~bwxCh!!ktyyVqv$4@e7wUQ#9GO8%5FG~2WN2vU<;$1-+QEd0e5?A| zuRiBDHFKzsnW6N8X&KayBa_CDoyWpoqWT|g{G&?KoufOBLv^mkc5-$}|=iQFmTChk>+^IWMP911; z6;gQ;#Lh(Btd-UJPhPUW>_6}cobg*_qE6! z+qEj6>&2{ULM+Q&7Mr zsq-moC<_aV^j|&*!T|nI2mpip`0?W`mJ1CHP2XJXrJsH)_L8-R2qdh++1KxRmmx(PJ*a~EBX?G+;>nD5R;oL7EEeu*8;uvYX&Kp^ zVzSXft}VzB(b$^iD5W<=*oMC-wA?Zl+-y*D`q-}Chd1>ET9crx;$-5uTp8~9>G~%x zlh&xI~ms$z91=T!;PqzEal@n z@#(2ZRNJwxtK?tEfQj?;5}ox{@oaAnC*rcZUR`cAYMHy4)NH&zhw$1!)r!pDOA!st zkGns5gJ}rw* zLu)^~hABLtlbkZK>%K-?{udOLl-$^JMn*(SQc)DZ+iMCv}o3KS=@p7EEHN-l1 zu5y3QyR-g-N^V|W-rkx=kM~^B+&gi)0Uq06!NDPJExL5kxjDME{FG} z@@Is@AD`u0TP?>Mc%kA6f0C!?3zuG+dhl>F4QPD?t= zV*vB$LcaOZa-A31!>zIO^&~IaIN&D2cGs1EW+joJr2|<4`_{F+hNGyc=voV;LOZGR ziM;Mnd>%EN;E)iQ-O2=rYTMfU-|$(z8jw6HHD8^c8&ePulM*Y`oiA`Wl3x&F7-$UJ z3qTcSReP`dKPHyxDB2s@cEU|=_gjzlN>eQu;IZBE$p+^T@-d?%E%1R*R?EVr7Tw3NTmY-GI*#!X}%`xitE#VACV=8Lf>M6^~ zlx;V^cwlV~+lTURLB32|r#-4@;}K*c+kPsN}w-8JUpmeE-mgm+md8D!;B9?>D+ z^E!V|U%O;xWZ-bXapDO0*pKouzcCc3qp8CD{EH9Zu+yzS>$S^fx-rZF7o)X&lEiJX zWWHnIQn?M&K_e2DHbLWBxA1#P34j_1B)hRbE9x{}9b$hx4P4|-Cn&7DR2D9YNYb1e zOPr}$fo;8=WvA#nWU06@X0h|VM25^hL_8{jsYoQYV6p{A1UZUH9_hZ-k`b4qD zTG82)|Kb#UW!e*9=#T6fg5fI>%Ct@r5D<(O(cWFR zJ1k$5;`=cw53+>e$w#}M`?_Zvt}!%kckayhEBqdU9XR}_!B7zIvFdLKPe3S)2a8~! zz;N>^NO5lGEgn=j{&HeH6-{Gf#y&~s$v6gWrt^K8t^ z*NJ-5(d@5cPv&#&udcYUFrm_48zb$KDb)S|%>lLbp5)JCNxPM8ZP|N<>LfhOJn`a)o-ZTE@bvPkd=aIb(gjpD zd1Jrok6rmF6l6tVNYQ!QzJeN6^7c9%Shc{#x0|i29}KE2Z$h4l9gLKxF0UBRp`np# z2k))6M?BH8Qo0^-He+K#MQtz6&qOM&;gPrK0M1rBmB$5fKpU0%TwE-vvOm1bL%sR4k>}NSAlNQZK41Bj z<1x?$_4?}S#4iwCdW!Kj=_sg<=$)uuL*WoMlfiq@inou@$Y7whWMD-eau(WjGP9bhV`J9l&Dh9;{S^RDW22sNha4Zu zUMp`F5ze`tB8S!?1~SxPh%R~Kjx}P!=1@0UspN_*?}&uWEbrx%k^_g6T@EC0q6LYt z6C|VZIN+eRnKI<~06ypWvvTzrkhEb$Pxy)`dBYTvXENl?AtHbR`P8!egD;OEuDDk8 z)#$PcHjUJA4r9RDX6h1C%Y|5cm&HMI2S0i~{_;)omb=iw3Cp3qF$8H7JoU)Ce~;SB z4qAk<)|U?gY89xb(8&KUA#ASt!QezdmqB?5>DBDC+prpOYnn+TIf& zCagEnNNem~XzHu*eEf9HS>{WVwo!-Ta9w5ncoQSzgHaBOac})wklky(;fPV@9M;H9 z+vL`8mo2-7B)-oNL0T7@4eB+qIqIKMLil$`@)Zd6LreFPb%W!6(gJLS3x?a{J8u#G zdHATy5GB>V>Gt@-43_P~D?E6BLUC$HPg|joaA-(|+fUeg2m=`Xy*(ygpE+1bQgXoa zOvRp@&dXl3r>-8im>3-gR46jJ3)+XwjMj>4X6Ci2bEt3dLmDjzDVl-4bL5#IezWu9 zVtb@BQT#jmh8p@)N>Mj&$suX&X69bqS-*uke!;cAgPk(Q+1tVN<7#~~Ws~_ag_gJA zYNZo_X%@FTCpml!I!BSR?IJM%3S%ceEfXBo#D|BPN&&(>xYhra*L{Nlf?r=>-_u(} zl$4Zi?^0EdDDWv0C?2h(Ahh>m^j4uv=a1(k`5RACI56T)$uzWHUR3M>Rl3zDRa>A2 zvar9bLDRIO)96FoPAPql{QSsx-jG+?kU8R+2mZTwn=oDq&)bsJC)Q$Ka?5PRP@0aH z7AwnV;6T~FWo)lQdN~+LMOt?+)Qk!~u;4bFxU&RG{JigR4tRF>!GY)X-u4y04ch3` z?A{PA$d4Eq(DBNALM7{?tM)PYDS#|tq59sxCN)T?rI(x&QB25G&H0? z1**(msSkcEL{TizF^GSrEmHJ30cO9#rCG{mVjuo;aP^t?TwIVGQcdW_me)s`z1WML zB)3Jzac+}lT^zoBYmMkk#S^a7TjC*#-1#5gnw=FO7lN|C1$$tC+m$G2wZMJ6rd(HM zrzkN5_{4y-sq?hYqrx$Gr8JJQS#3H}MT-)eZ7B)`ncz-9>@66>Vf5s`)^1Q$m-T_;$G~qel2-`Y2i`q4e{dkTeI|3weBN8>A7i)u)b=RPPxE_wMBo zD?+PUq*n|%x&;&%OE}X#X(Y+<^dbsPb-0aJt+yMp^=eLWX=8A{P@4& zvkc#W&N;IbqPUY&P(QxgXZ%5E6y*A}OB|y!1=-!K)_AXKo4CsKX+Jq2XD-84^XVVV zHc7dVVtAf@I|+Qh^Y}fRTl32g9Q7V?o%`@5fr0s6TJh$Kjiz2Z_$lZXMv?hM>}Y`d z-6}tsuvYXnY*)<61GA4sCgP`7C+xY=5L*wtx3w15gO&p-&}?%RMlEZ~gXZjVMQ&GX zpVfKYjHd!%bp24QXF#&0(AnVW*S0nOR}dxXJ~uqt>jQ71j~q`EjO_>uKc2B!7$lK9 zw({dHDd-;lhSTT@6R(B}td-lQO&4dSx!YyE9YB{|qQx$!OeJqrHy(MqM?! z(&~Ybg5J%&=}Ady_m2QI>My?LcOfV)Q`nmy+DlmvxOM02M?g*zq?bUl&jvYAYu;W* z)Wv@EcgfK}6DlFuo+`}zB>G=zERM5Lr%-~6*l@wVezKh$jT3vLbU*Dy)%ycmz)uTN zDc$@KFtMF&boN{=F1(t$TVj^aZhzvX;&gE|6yn$d3EM$$f<^NKXfk-RbXghETdgE_ZroDIXBB}hGURgTgVE&lX zR_0wv=oIbs)=WCJy3})|8)i=nH$VG$XvF2pT6jqr`AC*-uqlQ&?VZG0iI6@!e14%(|oCn>ATZo%;g|A8<}9OOu*eTvzmC@qIe-fk(~Bka51g!KE3Sssi$;xQQg$)Hduq3v=l ztx8IliOY7s@W-2r7mt1aEcopxZ73*}ZNA|HH4ADiBlf>s{wYwWL`9_**`bs{!7@#u zZ13PZqv}DqFY6$~=CajVo*dYGW`cL&0$HLRPA$*QYI}U7lk*&tYay3X(|!n_*m+A7;TDaq>CPQ~TEb#k&fM?GiNZP>brUE$i`QND z$EZB#6&+-Day5<+_I^#qlhFj9cuGM56ym+zpL>ViGWu4g2Rc9R$8hyr=>sVS_~W-F zrl$_dVckHrTY2?)qtBspZ@sB-0(FL6{F{3T4r6?zcBX6N3k6)ySpgAR5`kKmtjzwG zDCGhHaASg7j{V8YAO;b=c`>GwRpBqRLAO-V)X~wgw=d{51m%AlhoSYqKsfQ=Ke-c` z_p~5At`i0P!EI-^3x5=E-<=VRQjg*0d%wMN<5)q?lWwIJikRAN!FvL*^tF*mPO$XjTA*l?}biw%5Ou^{HpLP0E02~m-PoE|Bcdu)g zda-lwy2$OL68zn@{jrV&f7~~jU(irw9yA(AS)V#{FxK)3vUD!Y2_v5KxYZv<1G?4jQSAd9$~|ye`fj2j{czc){p(7pT5Uo==+~Mh#nN(?0aAA|L)KFvP{>lf|hKL+a;9kqW9 z){k8A|A46fL|DJ-IQCnflc*}H#Ig1_iV zqyLi<{6$CRpOoOQ%JBauQ-X|*FHU{K0{AD|`X}1@0R{dsUjG=cA5q|+^x%)E@*hNd z|D*^1gG|;B6w|Z+-x;i^w3Do?tOtmRA|jEF%e86TJ8tu}wY8m{o$^VY-LZ7uzev%e zzb?nL*g0eK2~M` zbMf3C!*z+64Nah(oRO5T?4E56`7L$?;&Gz~XkxzeTKLKb46euV>Yix7Kl(p#Ek6I)oB>dt?`rmbt6uL%M zR#u`{dtTLs*7!WF#!XI56~18;|M{cOd8;Y9UUTi6T{xkGhOAPNygc08j(XK{L<6NR zrR~1R;1bZn)Ww#>Qhh!C_U99yynK>;ktZCB9-2Tm?0xFph%Eq8#j95*t0m8K9zwYt zzQ->-kn2PF?ibise(&r~5FmdX_2kJDA5tDp4vtIx4{0qIA_V5>MsI7CW}pDdfM~hFD8rRdu=X4F&nM{aVfFiFZrHtX)z9A^M5n;3YC-&5qIQjEji*CH>K}K%#+27j= z=62AVVmb2mus228jX&@-iC>GKY;U6>QiK-oT8Lj)xR8~Ju`zF7)Oy-W7b>sU{PZHp z5+*^E*_6>-m{J;QYVRH?Z~H`I0LeXQpI0N`zRSSC5X)|M`#U}U15^AnOa3pJFKIex zxGl10D#=Yq4mCBUS8q3CPdM-GR;OCQ z&G((YE3#qLaBCDs`N<)VU&E;Sy5K8-zoCI2ckv*Oo_zM(+DY+jr$*zWh|f3>9YSMWDCH3@f^k6GsR$H&jh%gYOkhy(=&+A9k^QN<>EEY$Uim~<_ro%$j$Ty^02ZTU;HeZ7YHZZCu(#)|9BtoPO#1P7{4WUh0hz`R zX41Ob^$x#AA04Pk8X}DFO=DwYVM)}?%u}L))AF#A5@&G38w=2st=-0V@7`?)c4`T2 z!Ihm3esl36RQTgkunniJovm&Di>^P~*8fxb9KOQeAD|VWcaFB?S>0i0@|1K`=To8J zWdM8fW_K&q?p_RNJEo^!RnsvdyddkEy$W2F9=$@@eb#}#K6m_(h=|l`%Pm4>ov$wo zFp;W@+L85@B6>DbeZRG@7P<$p_~B%;QD?rDv!JQyf}YD$Q&Uw{!V2qMUf~VR%_7$= z+1BOOc{6ho?kI1O1|JaAvvO{|2>EAMwRBDym=nW9K7Rc8!!UZm?^QL~fDz-t6x%nD z9_$(>Y*;M-zgv@#YLbaoNs)JcpNjGfTP31Zda{WC##qfJ=a0GCi+a$EH+(|qadC0( zmfZ)NYyN&$|NZ|^9YA@2X_On^zJi|=T0mp@gaFJ_`0+)EOQPD^WCS`VHnVpD*0l0H zNl9?yW3#D*e|)}%_67iZP19O+F9D;^u)^8j+f^)cVdJU0P*TX@i2>LsuzDgN#%pRs z5mu@$G6XF9*3-mPDxVPWYY6F?*x~MO))AzY|CERS{|>5!_^8y;FlIR zHa2#4iV`9iFWdOGgvYGv>uv$G-4zo8cKAoZSSj^V=iNHSvE8wS9c*0N_extu*H%;t z{SK^x<`qal^8AM|BzKRhh6YoNa=`EPohyOjQ%^4KJ+wd*jOlrK-SPz16fhfW>r*d2 zQ*FUgSI0Cb-rZ^Ac}1<{z)nnay}&q|wir0~xS~$Bwi{!%WP9Nn0ycp~g@r0XA*3P0 zPbh)bT&79uO3A!dJ5A~(oZ@5151BtR##yJJTByaSovl>zflwx03?rLS`fYo7_3Lb# zk?a!8gF%gNbp7QAXe>}c#TWWx4%ZAo#i3|-n@J19dg{g$NJ&U?A~ZrQr(>)p-}z3x zwYRfVf;EZ2WhN(^BLhzCP4$cpH39D@WLimo^$UCb8JGAt6PgJ27uu9nh$Ubq=i(a? z{+U_VmdD1%mhLQItlrn$+CJ_?caH}w{Y0m-;mWJYi)1vXZ7ja#LRZy&Xz+gdUxzzk}<1C`u2jyfU#Ff6hl$Rdrx!h|Q3@RJZrJ_S0rwXVy`` zPCz*Xgw6JzHe?sbC1)8N9MoNZtu|;(;Ke&v-!HWF_P5E%U#`diOo%z$XYFJDrV3`M zeGR7oR8+xj5E(e{ZduvbgrA>mFZx$p20z9Tial>?39$0 zInnB+0Uv`1K`UAc?$G58TljWyaq-N?M!}`EpRGjrCF=<#1a47j%_#Y|vy_=*hJ}ZB zGLT32xK&ABwNOJ@JH&dvlwDX@=sxFw+J4xb_fB05Bl4!#7QE1Aa&nTV*Uzwe3T05$ zPDyk6^yw7z%P;>o2#ltS1J)CHM3fvJ*Xe>N`tta=j*w2kOC;J|h1GD4pyctXsRZMl zne^p?X;M^O2V!Ys$*&F7>#hA(A+6Z5;Pw3J7k328WmU@8nOG$p?Qt74c53eYMhE1( z2S0TWj2b-YpgFv@DbXm#5PK~x%8_~}uxZaXJTh_tiIiWYKD)KISMmYjspcQx3>@`( z@;7hZh)p%cH89tNZN+`+ur@bmuHPUeB!^FG|F&MBa?w9v&u-H#i;p z{wIUKzvxD&gBRST@OW|fg7w#ZUn1Ry5itziLYg*Jt4Mg^n5DwKZl}Jr-rimxGQR8L z;g+?`0WNCr;(G7IEMr+3Az^)XVr!*z*e|}zkAHnQ0~nQRqQOHyo4&ZLvGM*ik+qEt z;5z*@WWR}#5eW&&yQa{ZWuTPX)H~<#xOpThteMk-9y7)9CRzuEAr;|6LsB9uum4ws zr~ptA9-I4}027STIHFF@&b+8h<@dutc>7NXx1) zByjJa-u&Mgp3x)#o?#XM_5NnyTbQ6!mVDgY7dpXOfSins2OT<4uYeXOOG`Orej!u4 z=?ektK)jciGgD2LtLXye*%W4r{oBL0W?wfVD^gO^zTx?O(`>(opf^0gEh~KoC&iu$ zDgzKK;+TCyLrn9TJ?lVtV^0FJ6l9%f;Ns}0+I`c5lZPi|wm(2Uv6NH0s4s=qK;dVo zSpS*5{F}QM2=-W@{Efphzu6!(b?p4YLWhy5**9S45!=PH>ib1Lkuej%(0r&PB_(y< z*?7F{N@ zsHKT;)d&-d7HRSm=b;G4jG@bVap8P`<w;Y;$7&p4@J@T zuJT=I0rjLjd-3JewuQSL`pGIxg?tEazEi7>ER=EIO~Ws~HyZlteP9}gtObFq|CQwL zAnh$}6T3ldA6ecJrsO4`A#9twZbh}F%_^z2epkmOHb7(&T|a*f zfVy~1U3WgEbzd$>C!}|F`W1kOwPw&eo?PhIwG$dIpz;-J=bI+t*IYZfx3I6Ydt3R+ zf2T~K?+$_~+)l~@BogkbQ` ziFMWrI1yv^r+?Dc0DaC%?&B8BGn9@XVuhR0X&^MT+BY{49=THuWxSio-l2>;n zQwteN)MP5-V|5ys0_>mnnqtK2Xt!JB-M?hkAC%mUj?$SXF!hlrl)}Pq`!u^38yHo)z*p$rH#)p z6M0=fQv!n}PQ?PPT~Fs5HsI*zys>!2txU+Q>ReFRA_$OlcV4`Bk^9{9vLsKpE?!>g z{+@$_1DEk;-r6+ot+3Au4JlT!Lg(0vP5tli`QUMhwZly1*U)OH17gRQ-3GZ!tZAMI z2}d|?YNsY8B?*OJpE%3`{`y(V$bjzM_Y*zw7oS`mKcU8Ypr=Pv50ONfT6bEdR3kP? zjAoAegRVqUQ){qa z8)>H;w50cypdpm}62}!`!wet$!oka{UNe_!xl&g8Pv1vp|feu&R*Wg{PTs7QbP zK`{!rbSFnc57!g}L?D1V^zM96MROZtA0I)2C;(OGu;czxS2_fTGpK1v){l!fF{nkY zqjjkc&&*uEQ;vz1(714XHQMtv?cEO2N>UKk)j1*@;M-4n5BH`s;cy@ zuL4rUZrGfW?1dZ3mMhT=@lh;XHrM~#{_Ag+@xM4;SrL$-W|cpd+x#h!Bp6gNg}34h zDYObcR??S)-BE0@N+H1Gd+}NG5<$1J7huj^b0Xir@2dt3o!jMgX*BV&c{K2&)7tLdhP7%r;eK4qC#9tRX?f#zuZ#>_l6WRk(!y zHL6fOgP$PU{WnKiKFG8V7Rw&)a4VCAYV-5)tWNCLo(7pyhb%WN4g3UG_MyH$d#il> zU2+UKKD0=(9-oJV-T;2m9{(11uP0cQ6JG?X;%kd^OUuj5OVzW^0ya5&gX-)~l0gwM z{TiyOmN^pdR>ovzj67|}{Vl&CwEsop{+H@p69Zfd(@5O4!}R|!{y`N}f!A)$)Spaa zx8bzO8M&A^;Fgzgj6HSE&&rZ5NOwQB+I9_q=|pKkM@(()?faHu?#D?4FANTFqy!;EzCpW&P2v(dfT>>a6jippz01L}>8_ZEx2@Vnnxz2I@rTK$mIj}*EosQp` z?64#U8OIaavWL{Mq>{#I3y+z0PoMVgYDeh%$0`zq6@b_+;|>q2o^oH;f#sjgs5+T$pO z)Vz1m%P#{B;#aSvyh}_vU8^M}Q)$tu;1l)T3I6w;i=W-&zvP*AaeI1u6R%Ez{Km*L ze3EXM1O>T7P$|gP_vz^L(Z&^8G7ObC(fy(60QesRO$cl^*+}^zg_DnuPy9aP`uz_f zr(b-Q8FP+M#Hb~J>R?qU4nrosX8SMX;wLKJ@?eXQgptKz zD!4&G69F``cdRIsUah03e`sLft>gp3Xmw5rzJ3?r4rZs1IR{#|0%PiYhN_&6?Y(o? zdj2QifsdGOY}5!0((16+c4LMVJZ_tt1I#BRqPBO{w_WoZV;IPeM+{`rV!$Q*jQ9JH z_0!JxtNym={%55AcXZsu^~GkGx3TGWe}Dv&3cj$qnr+!e6+knzJvJ7Tjp)mj!q#r> zD=*B+dDx-zj_Aq!!eB97UE`eC=;ZbJ=Q-Glu4T5|O$>35XBvmn9Ic96_IX|6DM1Lw zqCtbf)ReTEWXoA&U}Am(WM5pk;RXzomfRlV6IQKx<79NT=H^&uo(yt-ZV&{urY)mQ zlZFDj6{~R56;LMRlmBqX0;+l&AN84S-9D92a=8M0=zVV}Q~FxqO1^a*q2%CLpPlMT zg@t)%_O_6vrRC}pqFOcGCw}GkndsvX3xGGmqoDrcF#Tl7u(-K0S#XWEbuPH&dh|9(-K%bodWBs>aUtQIt~=gp>0hvf;dWWn$Nm{P-xUFEj1PQ ziRxC)&gk%^8z(_Igpe;B^|_&>xENk1AoKJhC~;AQu$`mB(p3i*W$E6{a}Kdek6&e( zAuSs#2aQ84hUezy=2?CC8VRHr!(KatH-ZP&f_EznFQ^inpv}J5qEZZKA0ReAhgN7$ z!eSrI*%n4zVi4N}R)-Wy8aehomv*RP{@aHe3Q(5@E6eZX!Jbki-}F33Nfb#4vTh%w zYbGg^YyGr?15B3jvPP;l5(aoTn$VoK!Ltn~4*T<6^!zMnssN=s8KzSmv4hu@Gu5JIKHwNwE@ zJ-p@*p2HPq2*>S9i`{SU--4~&Z+cm){v-=<%H2H6dyuu&FW35}y5sWm^A(3NOifK& z1m|)_X{#+V1ev>=HaG1UUtTSSJ8w;EHGBG&wEN5qYBMN3P}3;>cjW2c{EYM!pg|z_ z4rv%1kiZXGLbjIvQbCXPSLiaYUbU7H7P`2#GsDJw93ljP z`B2So`S9)BxPM9Vx^ZP*1;QWKGt_oq?0jv#}o2{+!`B| zm)dzEgdV_qm-5FKAOY_+RO`)2!c^H^xba9A);wt)r-e%=LY$g8cKYHicp~sjsx)^o z1WNdIcL?g3S-w#1a%(kZ&LrzSh{$@Ia^3{uT{Xka_>XI6F2E* zl;ZS>Nd!^RVYkb#hi3d1AVb@j@_j*qA`$ep-TSl@TK#C?-MV zTT0y%&qCglLBf2l2+uLXb-Va%Sb`MYz$((TZQWxyn zG0LDlY=M<6yyszZQyfO}E6mvq1d|mdB_$En>Qd&hPwbvGaBvf!FC2(#2!7K!bLmA0 z@CHU=HZ89-zGA_g28oiD>F+Q28_t{qLW8G&!He3W^DX=QS@VXLQ&3P48LuOD=WbZW zEpBB|R7Lu&;oFai3F>st~d#_g2u2X-^ z;dXtPM%`ler&>W!3-~kmVVD~JB1XQgck7e7@|V%1ZF$Rk1y0ioarbnmPgyW`zsyD! zPJwFj%*-bN;X~2<4V-=f^Z+xCwgrE4Cp7iZnwp#O?yx(+#Ln-oP6s3)6SegPY&df> zaoQn)sK#m^G6|0~%pv-4Ay5M5#0#dfw86d}jz4}HH#IG7HZbrdvMJLH@0O~9HbHR5 zM3eE&-a^|ZF12Fs$Kwt>lMd5>>b{~g1*%E7XG-;F1Ic9;ZNwrs~QTq+D;xAN`S#{dKnedXLgnK#99bXnj+KP#xT0>af*HE1FMSey(D(yu7?6 zrKK(zjdf(nqOvLI@#ohw;8%n!Bw-d*yM7?6wKH`AMN@pli-F9Q+Z z{k8%;EzT9+`dekqk$fI?_X);6jFNlI%te_Ii)#tfQLu9_PU*lAX9@!|!|j^4AHVCj`>ybe^XUIe99rYOO_niP9uI6?h~O{5 zuwZ#=Qk3bp+$d{9wzE7g(R=Sz3KJ6(ro&F5oq#i;^Aikx^8$&|HaddB2{i(T5$*K? z&0UkBl13|p`G_31Cv{&5){{BS8wt{lOy?TjbqxRd`M$|PG7O09ZNB65-}+_J2_P%= z(-Di**Kb1IfawvN-CLb*Us_rM*6~wa;Do^X6`V{yhYT1@N$y^vb?Y!#iqsOqZyrmy ztrILa;I26f8?3+n{B@Rw*hXVgmY0Lp2gk^6p~%vkaXt~~R0>x z=E~B_WHY8zSeRL%#9E+JF^f6Rv^r7SZcL#IM#Dq$Aw>%km(&-N{kMk}Ey6$8FGh@s zgCxi#>bm&dyp?c1AL5xX!&sYu4UtR})Z3js5oh0zTLUdi=b#a)?4YOuGaXD=*%aZ( zvxsh-_6C)tHODG8?=k%MI}Ux2%MgWmzVVawD;ceRQLb?c-M&{4TMToD6(2M;p#FIQdV}$&dSc{q(vcQ zWT!;g+522&Bq4k65VH6FK8~`g>wOjH`uuLU-%WqLZaU|6p3legu^;!x(|(M20KNzTwsdHWbDOfwPvO<6~Ls6r*AZ#dz8pnil?p02U)BGSe^*!M^U zHpxJ3`O8;yDWxfGpOUCyXtEmPXigiXBMMITXDUh=IrM%(XyItdn3588umL+rl zofEs<<#Io$j(5I!S>0Nn*=f{rrMlFe`9kNcb?uppA-ehF!pF;$xc=Ub!LlVX4>4~s zTRsAx3;yG7O^sJxiV>~&vfduh5>k^loJ{GI*H+pp{?=MNFEDl%F7U!10-4BRGuarfYSV!G54MqtGeF`sK6 zS)2v1XeoX(Iq*Y}TS2gQ`0-TBa#3pEOj;L)vnQFT5p}PgX9AMBN!482FfK*>$<) z+?6~p)!Io}-^z*?h9(ZqkWak zeq`WP{)@+|oFGfR`d776XJ7k0s_C8<*wVoazyRl@-N>bb zLxm&quaG$jqjizF9XR43<9u5RCNn9h z2Y_o(yJGZcc^h^((0CzHmg?f{OhS?UWFYVMUAk9kqeSoK6~^N1PXEPl0tLC|xQk@4 zDO5M!$O*cnR^9ZR$XCgl%rkfEtS1Bc?erTJ3aYRgv+U+;MeukttMTY!%Y~#MJvogR z&*0CUD3pFGRs2rEqDo)C05`k9KITQA|FNWgkn_Z%isOJEtBO{Gbp%xRflD5&{`Wk2 zm=kcVc_5z+>CSuCwpVIBbj+EX8VxV`ND4&?^#rWBsh;Qx#j|+flfX13^eJ6T(xT?4 zBa_8RkXq>h2!6XcCdCP3|9dYJ!h(w6QPAtGel$5V z6P>>`g&@PkB>oXq2`#Zkx4r#l*E=V!Y0CTC0f3HMwkRH|#1LU@MFv;b@DbJjxdVK5 zp*W^bxr#C^?z!Uf1MXAP1JP-X!z%QthC_w!YL6}X-n}QwfA;uZqeqRzaw=lI((w^} zh3?53oU2P`%M>f{2EVwt7+trRogOdIicd?cGh**)Z*LEk3S>Y8F@6;HIwTs4xPZ*9v)TVTjm=Q zY{G|}6SMEVfl0vsnN%W*R8TqNCIvw(Sl>?aB5$gk;QHR++=>DR1cB>d7&^2C{ ze2K&s4RH)L+-RhB;lu2#4e0&n3IvK%i8-{P(>+zuA=Ni4h6R%BZ5`M<#4kT{-^-$A+Pd zWBltPs7?!-CLJ$t`;(znErySBGUL!O zarf=TvJ1Wm;@7|#H3)w^m^B|Umg$4uejc`DU^^D-|?u|P8nxLkmRt|GQFGI9;; zOU*no#vrj>iqkX=J6%Dxc$aVxYeaf^N+;qyC?XU$PI6P^TNWiuLT7Ly+~D|RYbA7- zQql?+&`>%RRn^LN6S|`#kVzNv0Z#LDhNc+#+N(LWUeKLA3EkO@ch$LIKUyr0C@r)a zllhtNn2c*4k-=aN3Z&8pE``<)eh!d_Z=^%2;>dKGxa75w?1=&B2zMoQy>baxdvD9a_UMKRar2NnjhQ&=R z$~^|9N$;VKju*jIx+-uTsoM1CoHp=wd;FAq!fJ5R>4$6o#BGSq=ajsOa@!^|^CtUv zl!?EASwXUz4rVVcnipIl(I{hF+Dyx%dzDpHNjy|Y31`QeU$#J@w3l6qFC#UT8;0N* z8%oSECu|ef-ah3cs(5Gt98LLzo`H|w;B0y3E+?!z?e1voREPj${L@*#A=6IVk6(T=XDcMi6fcR(~}+g7%RpVTE! zcYmq>&7nOKjhqSxW^%Ro;G9u!uo;X^-7Pa>V`B&2`|bgSp5ozoo$%eAL}mFxKeFvh zO;jsrv{HF2D~^9PB1rJnwHjr_msbjv?U>PNS(!6#e^F9UIHsnTSb%k4)n_~0!b~zC ze9dN>eux;itEMHBd7;C@TPTQ{b7F4J)^lH4KiWI%qho8+!IM$u4Bv6d^f#9BZ&IhF z(i|A+q9w3msoeOY_pfV@TrUmY9o2C98gx~j4Z&7cQ6Zsq?hc~=j zT$63hLOV`-9JgRkV%?k(MvIOwWAKsIc^#7F*4=O*&?RxtlX%>p?{VZ^KD@_wO?Ysp z(|vVTcy)`O=^CjX6bM`~opR*WvM7TG25I5>r#W#hO53}Nsv)5qGFa9&m>l=0&)?s_ ztgH;G8k*bf_&(}Onzz20=WVx3yx(g_F+d5#lKV4gq;snyVaD_(TFR7NX)%}B9;>pM zO7St?)j;dob+hre{n8Uoh)Cx8aiOzE{+7Y`ASwuZGaT9XWxZk)n#$BqI(haW96(9S<&O5QvJJ zOG_p}>s|aP=GqOw!@6w}-I=Mxw#ZwkQaD+5jlX;E-g|KqoZoYKErtrUA*r*VO-f3n zg~rjX6EJTp>NK9{{cD7|;y1Jx{75uEbcS0v5Z*hllM{V>#I8yv*$^2Vddu(LwUcj| ztXnV_J;TH#4q%itBlhLSJmeTQp?kwz&_S9**lEuvv+A7l+F0UfK!Gv3WuLzYm0Pc| zber6i^mJ?6nO++Ezngov@S*dNZrg~i?$C59Ailx;^oE#J!JLQC%ZKiZcbDX@j5LAa zV7Zp2kgfthn$LDdL9u$lIy1eR1>{ih3-*2;G}Y&4Ca)*_8G*@Jg1u1H(O)^w2&+=G z85(1il$1j7GSky%2HQlmqr1n%K1)iP3ym+t81g25Ci$dsrKNp?5%-CXEZ|~rm;|Ys z3{s4UBu*BL8?xs5&4BlsrL{G$O;e#Sw6ShO`Y{uS=o=WwGrgitOT0vap0^hx zfJA%8MVUmCRgA;alp$%)Y&Z}hQaF$kYar4fSw7cwZzJL+ES|VpP<+u0L?j3ZiK4?m zxtkHxIy2E(b3S=*pm@S)c3>338_V+HTJOWhxf-Hvrg8^?=Laojgzf1lud0}KR>P<) z_op-9b|8s&i&h$TG1EXEvbd|r+_soeIW|sfhgTecG2**}J3WJ4*)Zq{qPjt~sAil@ zp#DZ0lojsd7bL==)Qze4UZ`bSACA0bSCZ5pBQ_7-}*l%g6a?V>< z5&4$8z0s+!dxhZc`BY!tA-VuM@b2{$n1u&!GP+`Cxkp7{WAR5?+!bzm#k(T-)hCZu? zkL?bv&^hm$^5JzN!tyqwjpP2E_jC)cwyc}2mc0skLWr5OLyj$)0fBxjv=)~xgP{mQ z(Q_tZv|8HAUFb%0XS25lH9y@}L$|WlfUf{Zo;Dit^sMA$dWqCCvWG{}F!G_&AeMQX zqkCl@kC+`p+b*yQ-F+fvwN-p9 zhubywI6FK0B-zy;C+BH&_G33qsdi{_o^NZV>%TtG+}u1lcu3@2emYIL@8hTFxSD%6 zz0>{xBG2d=@Jx#w^^TW7zR+Ex7T;TiYL{_{6kbIZ`SBYGg(9LKY-~H80|;_>DgkpD zZgp&!OBangq=@*&cIpk-5Xmv(+zb0kmr7cW^t?Uc8H)jO&v9-$55I7ot@(DZ*iYQMd{7 zJdC|;S!0%uM3V<*aygD8=@22iwx?XNnoIWG=57*r4+YJ&JTB)_P>i{ zzdVAmk4r~8m~4g$s6%KzB-l`=gtoWayeceHN2KkpQyKSVy&hSKN&FxB z;%(W63q6Z*5tLT|wb1&Mk;K~2@N9{0tcsh8f;0+>>_X9wEKkC$qV| zJ{h%x(wZdlPtY4u@D-c`>#l5MH$Q96GGoi~{KT;p$>R0y_$VEeD6ijQV_Gr`p@@nD zMjvy2j*tn4Ti zubqI}=kQaqB~q_{@C0a>IHLM=gDb+%q=&~8 z(S}YId*}i7q{_Y2iuNt8Y9@Pk-ZO0wzi|+sx6G6bPgU{xSP#{s@I%q19vxOSb#>y3 z=Gu9d@x|`fayF|5l!5Z@ly*6WImM}fZe!84MJOy*pVdvV2!&Mqx1GeDf=1$={UWc4 zpmB4zyow6AwPL1C7q}0J8jX>4Lo+1!R50@30)uun2c490>VEcs*cv#y(G>2sa`b%F zAOl2-RN;_^_rw03wj~Fiyb=2xb)UG;H_=M&ub{pz7J^~R2t^A?^HGCh+NWu8P3JE- z4QDf8P>s-oO*rs)8MZ4-Pg{BUtS3Hx zsyYt&Uz?2RnXX!Q><)*6gCoQ_X9AdcEM8beCe93E<=Q?K&hX*B4@!Z_x;{XMc5j9BpszLS+u@^;X9WP#%W$DzV#$i>`?@stM&1I zXaUV6BP08OGjyf8bG`%1dyvSAucM77-ypK+g*C4;8lS3g5NSPFWD)nS;Xv`Gl=HNR zWDg!8{p^&{PgOiaI>*&*iQA^8tXh&9WVsn5+OHvL<>;E{dfAWI`V4bvnwEB|&6q@O z9X9msiPv{D`5-G-C4Xem(OOh_>u}=I0iiX0jS}rZevKQLo}j)=dgrs8d9Qg1%lEc% z(O;eD&i48^5iYvIqplx^!(y~Pd=XWOt#W-LXj;s6!I+h_@G!RV_3J0>_jIALut+un z(N@DuT1{m={rZ&gcFEYJBzdn&f4#o?OU^DXboQ)S4bSJe8hA8VOKfDZ-9D>6uRP;A(0*)Ur+I z=d&p6Y6on+_7(~b>xlHaD9Uq zgNbpfn(lckYgW>OPVw%L;b@V z9Va?1_tiYW8POC8kpGMZz>}~%I+8CYY5u_cZ9UV_=_}(ia$c{ehr{;{E4^@?KKv-J zoHyeWZ8YN%@?lmFk&iQnB&f4`wpc!s4|oX(Yz^zz?+<#?4svJ=F5NG&4(?xcvwkQ} z!7NBY(+92}^I1MHij_GaC2w6E3euEPl0;}zyDFWb@z)bBJ*9ajLf_^*O{JCO z`15y1JJ>7(Wnn(2MiityG_r;c*#$-)Y_^0RaL6j27<_E$F7M21Q1jk3zjipuqlAP_ zbm*AZN$L|-ilt+Fu#<^~qfjjTW^;k_lYx2jR#`-d$(_aI)&S@@{K}u`iTdEx`MYY= z*FyLR-d!*HQZkkm_`J`Uw3Fpy>-j#wP4hiHc{uHJ6R!YCLe*01-S^pmMLA#G`Y#xZ z5b#FCa{yZY?Oh58y>l#=LGyhG>)bU-fuYe3FjFGNwO~JZ72vzPR|4 zgsNq{?(D7J-d)9K2o}ZyW55moJE*gvL)~0HMf5=|umuVa+|Car&#f~DYRGmT^obcY zc@unm_j71bRw@sf&dpbmkQ|3_Jn(I{(#0CcQIe{CxMpzG+8+J3JJ@b^{+!L=0NkD* zyZ+>`0W0Zf9J_4fQxxry%t*h!!|K6F!#;C$?Y;r?182O5Fj#O_Z$!*{k7MO7aV$Cy z`pG0(>~06dbcCs12MMQUW@bKpx`Lz=-g=(1S;UWYmWWcIUrg>8<{A zS|&koynd&^nUbyOuYY)3@rkg<{uSvj$jmQ#&k9??dF+WPs9ePFE9k;6si~<&n<)>? z&CQjD+&Of52x^?OXVk8>-vXpOjGpp%if*Mr0+{z?D~t-TDIgEX-WokHt9w!^u|i=~ z=aUIt5Wma)@Y&Cqq@BL9zai=0tr|6dx!<;!P^6A;Q}p#O<843lM^h_Z*VPWky;*&j_enKsf7R&&{Y7aJwO_$L+L zNSURED3k#5PV`}n!=zAS!HNw$T&gKgD>I_h%Y|UBj5D`Y*eKFachih z9wK`yr5WCb01>J_0^{e>Lm8^YE(!t6mCH4(^%^pyF&K5P9&Oeoqu#x(oM~5TS(hN< z@+VI@dEW8&P-*Pm^E5yY$|d49@nefsv62+5*It z`3z7jEK)hAqyekfIHUK;UF_f3%fz+mvF~@ut5Eip1yEOWGS=%O513xv;8(kKtU&$f z{e3+~@(iGkI4dk-mbtxf_p&gkAGjoEwAP5s54!=u;y&_!pY zA0k@1VbCTv?MQZ&MPG}d_w|cD1Yte`%19Zmd2ZYH)WS2M1U^UQhbNX)VsQEuJ6QCj zlCq`I&&~I-fiaxvcTIJMedOf3G)^XN*WmN+OHqk+lZ0dBtGeDOxp1543TvQJShRrk z{@ih)>}idrj;kT2c`?Uiu(syK5EQFS_t#I^3doG{BXt`64%xnj+2k{ig&rnHE+*R6 zu8Nk<19cIYoU*d%%hG{uC9x>9XJM?58?jRTYr8KAF{#q<#KhjNaHF%SI&iX+=?P`x6!NQGtG)Eh zUw^D1kK{6@Z5tjq5HajI*WRLTL_vA?@=As(a6KGki7N(lMnpuc5MHm(+ z;CVGGi6pi<53IrVhBSymniNEnA@+6L*pI}&7`ZOUAX)uLAl@d)#SkE?p9=4F%=5wh%?coL|lVYuvSh5t)&BShNzBA1>&#j z^Q`nAhqn+ncxNy%p2MTD9tM}h2w(ed&2{7xnuMcxEDvKkn09gRZ1qR{)io3+koae; z2$bGq|1`qajOz^a%^57%NxN@a&2Dxyl^olwrL~pXn7QfUd`p4ormU>2`IskL&}21K zMhu3~JfBWZ{Gii>bHyv!NP<}Yd`*JB^{ci*j>5+D4^c0vU3hl3zGsNK-r6j&{|E=v z3o}-Cv3?>L$VwWb*ItJzbpmVEP#r1H`EB!p0*-$YBq)wMG{3PBbf~#bCitR7H;3T7 zH~GBZHWez|DN}*1vb*ow@bkbebLm4(1u#vuYt(Aqwuxt9Tj{VZNWoYn6~C{-$qp$> zljo^9*yrOcNxU1Jqz@7+lRcH_!_zhNTaP-mqh4|HK`{bTL-h43d6)OxpM?o@Y01ge z@sDh?>|ipWeiBD-*dkU{y%_N(d3?J*_oA6wzvUb|JfI!u{$%Oq9<=Re+kHQ%SG1nx ztV~j!N~9e2e>Lhl1fn{YJ0(=#%xwpEI;($iC(ME>TY#b56kx-io9qm9 zBJq6DCESZiyaReur}qG-alb_Stc$u(%TE9+vaz9om_DtkUqr?n>70uGPk{<7k_S1$ z;tYo7_ndMunq+{Zm_QG$xF<-uybN&gE+fs-$Q|k^lcwb#=qIHqv&@esa-ajS=64XT zQr6tPz_OaKOM6(`!$jMz0}XuGD>wt6UgR5VkTVjd(uCevJd_G7_8nc4W$`)(6?S2C z4O%{slz!2hY_;M+(~fT6DXwdg@k zmwwz}+8}JsJ68*( z>KEcp)7*N$v7%cC3Wk4g7C?|PlD~kwX!*)Q^xh3^Y#Mq-w{g;noeT0}fxm>gqpbPt zGPM7sRTFVDjcp2>W_4gQu=2Vyai>WqVJt;-dBWyOAy|73ZysHHGuYj~@@?u$NKual zGn`rAu8&8N6*_FBPSoDhovG8ab5MNB8IFL2qn+yYfHt!<|IjDw7(KA~J~<)O zJT^A=<%=F);QUaZfhrRTXy<8P_j{H+aJh!Q{Kp%5;zr1Y zi`GNesiVDVukA?=-$KB1a&!;%Y`U81cpIW?1@&xJ2_ApRZ^jIR%L)NN7mI)S%^ykF zg9R7!y(sHSCCTF2!_@!~mRl0d^*sD9;!Mrj zrZuQmUFuB{eGezsZI)&flXx+oF0XA6E?u(bJHo5~++9S{yrqwOsY1C@m#tkh{&2_a z(%DhsojK%AE5@fkK21{AH3HYUvXahbifL<7d-HTS>J3)t$Xa~7-$}9a-W^Qe=!x@~ z1=2|-=QjViS!*hMhYEnRQ?gEd9x+aNSlxq$f(56xO+4$^ALt_5H)ugNSU-5*ug__E zzP1I@MpYc?>j@N==&}5;lL;af)P8lV?=8E)3GCY5vC+}dv4kvPQ}eWj8a2`<_r~$ z_4rDtQIs5N%~Fc21WO0`O#I>=o?KGut{brm*7I{G1H{GJ=PYw2Lr;+Hw6o2_BuWzP zt6_r&BU_EF`-3#G%21Vt(@;%AH8p#R<|+>bz_X(-ztAR?ZZIfKrSY8}9jtvlV()Er z{K@05$>l{y{fAfmzH}gaA){tspoR7Qscu5HBFeg0xxIYLW;NJcH_vtW&!#`T-3J(d zj+W->C$&t=tbqwn4|NX_$fx&odHToFF(Uo92}+KuAFb{pnx^Cx|Loj3D|Kx}QvxQ| zLFEEYH}C1%9|`zHPMwpi@^Glqsd0UN7Gca~{m|O$DmoPXt7PwwTTsG-JumqDJ+3=> z1=1R9t*syW^=*!RJM!a)*Yg*YA{xPD>9@km|E5=~pP}Gt)!00xajOhCby7j6exo{x zGeY8*G$1W@jc`AuAxE%}HG0cnQCJPlTv#-OTc^OOBO3YnuvET)y zLq^Ls#PCO5p+bKw)lnOZLj<%&&VwZo|DoP)$(L zpp(E5AHhF&U$oCQi*m5O!i6CEv2V_gGP91qAiL2u_aapV+$H&6d_YYj2r6db1nlf6 zxArGhx25%(O+(tIHKmP9qU>am9>WhhZ1JmW^7ewxKvYNeL1CF&myX!%5Pnzn1%WX= z_Oy%)NY?Q9cKaj2uE3>Wact?=Yn6WToux1T{sSr&lIDDg+eav3MS=5;PvuaH&CASN0&wi=)t=jLG#F(u2mh=f<%Fr&^*eN)F*#NnTSk zcwd@2hQ}=xniyKSc$_;WL|p!$aY%zSKZCg$mG_PFfAwNieozEx`UDrgmib|$$Z>3H zwY1rxiOG?nc7pzFySVYdfPmNb^D~|NB?q0`xF`s6{mHLro?I$Gu7i13&{+cRXluBw zBKN~Ygdhh5K?b?YA-O1x9~Qgf`@er+RQFJ!WY<`KjMkQv3C14IG6DxA)JkGj^nbql zPoS0jMZ_>T9`~Q*52*-T6jfoyfVral&$k2|_RszXX4r29`;?zXk7-Ige{99yF#aRo zUT6tf*-q)w{{sRB$w3ucH^JZW|A61$fs?}H;)dZ|*pk*<`H+Baa|`d4+5V55g9)!| zK>`FQK!`0L$NwJ?Fw6tIYsG7!m;OogFu2t`(d8`@>VWUZ5cH#bbyIyL@=$yz4?H*J`#-_UqJllP4%Yn=rA1F;ch z{i@gZ4?fREl(kL*V*P&?WqnbEx`6kk6y-iE>b6x$JAuvEfISNC{$fzP#WVT}#=@5y z0Hev!G%Wjv$~a^g0dX`u%4gA?3n}bC^jUOnHVKUr7LbPH%y+agDL0&c8`6URw1( z-z{_n)&4#Gp2}ndWJa3R= z{7QhB1OYc%Z69a^nY7LvRsWs|=8qipEo>DEEt{NABKrq9`TC-N(8xbOoEUN7g0ylr z_HU;kf@EUp1%$aNlcdC|Zx|j$ela_XaVdb#FY~wT$|*}y&}+t%>s_d!mkg?TwsQr0BQPABjZtFt4AL08 zKkPL6n(7}eug?cPyO1zvYU`=O8Y7P%F*g6M6u!O-C5#AU*NI$$hX~*R43$>Qv*2oW z*dN~|QP^SU8Sb_JQ2owN#;Ik4f!S99y=+WRO-Sm+RJ$5Rz ztVV;(24e;Szgh-BW-l(G=Oiwi$Q4d2Ct1ArR}&KX22nE`2n}YXtjxUkotMvJ z*@&(F!msKHBB-#bY+AZh8b#^)v7vu_Gd13Mn(yRq8xC}Tb(xTo1Huj2w}53|mvLI+ zYYdDIZR})S38|sxhu86K)kI5`tp5)hIz)WaXIM^_WXIReJRO zp@*054<%*5Kc)lfk}wsTk^aJcEU{|4cd$GQ+O=36qm1T5-*vID-j&20U+6*p`UWGP z|01t1G*tkKn8HPSHKRcTCY*?C899fYqev0UifaPaNeV!u0bdxObovHVVU+}z^>20j zt1$TTzPzsT?6y@iE0KlR!p}-B#ahfhFdqJ6WW7JW#GL{`SV(uc5s~ojF|2Pw6;y+O z=>#y(!@(i}AUziP?035t>9lYH>i#GT-oEkalGee&*b1FU-EOj#@|Z}%$;IVxY-aP* z$OAi;e`wX&{>n*?5`pX%+sJ-vB8$-kAiAlMQLF+JwJNi5syl7XFyhQ=2@yGfZ^s1# z)N;;kwg20Ylh=_=>6>%7unrOQD4VdP;?`B$`K^ViS0Mt;P9g^v-yng1U#1V*q5w^C zjTwM=acQ`+;j23#*$LmxX*^^hOyw{x!`RRblrxrOKwzLbYy47^q{MCo7BAP2ZfU-Uf>vWeCj3!7D*iBd<+#73UDb!_m8U>2-~8zl?Us>Ux8KXMk*++?-%~c_ z!p-NeF-R{q?l(6hw0(rx&3QHRbzLiM1`b|WtyN|IRA=|iQSp}x_qg51pm&dLwfL=l z9r)W)9!`0<|Hk7I+e}2D&d38UlUqr(N9UHzi-h8 z_OQ4=X>3*h-Wd8~1qF2^55?f|8!QG}XjY{i1PA~( zOXI;4&^8EH2@A29lV$S=<(4kp3A=C_=+Pr;263ytxY~sM%hFJ;Xx!oJ9dXJlIq4t2 z#08Y-yj{AEUSgla123Ok4fpx2Rj%p7LHX#YKYdzfRS>fdo4u$Vf20m8gp%feaj4e( z7Ei3@!^lI>s--{F7rr!G^|z$rcRu{_EU1ee4Hj2dnzi3~!7nO2I1wLc%t^~ni(VWk zzNYj3rKJh{1su#_(&n)`np?|J{2M1}y$0iVs6VrI?S|@?wgkl5fgbAc(Mf)#@AF@5 z2~6jPP_2tOH-zffwZhMg|AtWgvlaT$E}{QYHv7?MaTdU8*2RR?YM!r6v8UdozW46M zq`^)niy}S@7A7t+eqK7opu}PyS1mFHqv)%r`<;SZtW?kjr85xX}6KSTYCy> zBYn4a+HIuoeiCf`A~`qGcWbBJM*8ljskvVOS) Date: Tue, 29 Apr 2025 23:02:40 +0300 Subject: [PATCH 0075/1056] Fix: Add custom exception for macro evaluation errors (#4270) --- sqlmesh/core/macros.py | 21 +++++--- sqlmesh/core/renderer.py | 6 ++- sqlmesh/utils/metaprogramming.py | 14 +++-- tests/core/test_macros.py | 93 +++++++++++++------------------- 4 files changed, 65 insertions(+), 69 deletions(-) diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index 5bbcbf21a6..b9bf3f54f3 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -41,7 +41,12 @@ from sqlmesh.utils.date import DatetimeRanges, to_datetime, to_date from sqlmesh.utils.errors import MacroEvalError, SQLMeshError from sqlmesh.utils.jinja import JinjaMacroRegistry, has_jinja -from sqlmesh.utils.metaprogramming import Executable, SqlValue, prepare_env, print_exception +from sqlmesh.utils.metaprogramming import ( + Executable, + SqlValue, + format_evaluated_code_exception, + prepare_env, +) if t.TYPE_CHECKING: from sqlglot.dialects.dialect import DialectType @@ -220,15 +225,17 @@ def send( func = self.macros.get(normalize_macro_name(name)) if not callable(func): - raise SQLMeshError(f"Macro '{name}' does not exist.") + raise MacroEvalError(f"Macro '{name}' does not exist.") try: return call_macro( func, self.dialect, self._path, provided_args=(self, *args), provided_kwargs=kwargs ) # type: ignore except Exception as e: - print_exception(e, self.python_env) - raise MacroEvalError("Error trying to eval macro.") from e + raise MacroEvalError( + f"An error occurred during evaluation of '{name}'\n\n" + + format_evaluated_code_exception(e, self.python_env) + ) def transform( self, expression: exp.Expression @@ -371,10 +378,10 @@ def eval_expression(self, node: t.Any) -> t.Any: code = self.generator.generate(node) return eval(code, self.env, self.locals) except Exception as e: - print_exception(e, self.python_env) raise MacroEvalError( - f"Error trying to eval macro.\n\nGenerated code: {code}\n\nOriginal sql: {node}" - ) from e + f"Error trying to eval macro.\n\nGenerated code: {code}\n\nOriginal sql: {node}\n\n" + + format_evaluated_code_exception(e, self.python_env) + ) def parse_one( self, sql: str | exp.Expression, into: t.Optional[exp.IntoType] = None, **opts: t.Any diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index fee9dcb5ee..cc389dd87c 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -228,7 +228,9 @@ def _resolve_table(table: str | exp.Table) -> str: try: macro_evaluator.evaluate(definition) except Exception as ex: - raise_config_error(f"Failed to evaluate macro '{definition}'. {ex}", self._path) + raise_config_error( + f"Failed to evaluate macro '{definition}'.\n\n{ex}\n", self._path + ) resolved_expressions: t.List[t.Optional[exp.Expression]] = [] @@ -237,7 +239,7 @@ def _resolve_table(table: str | exp.Table) -> str: transformed_expressions = ensure_list(macro_evaluator.transform(expression)) except Exception as ex: raise_config_error( - f"Failed to resolve macros for\n{expression.sql(dialect=self._dialect, pretty=True)}\n{ex}", + f"Failed to resolve macros for\n\n{expression.sql(dialect=self._dialect, pretty=True)}\n\n{ex}\n", self._path, ) diff --git a/sqlmesh/utils/metaprogramming.py b/sqlmesh/utils/metaprogramming.py index b383899262..c426ff19eb 100644 --- a/sqlmesh/utils/metaprogramming.py +++ b/sqlmesh/utils/metaprogramming.py @@ -554,12 +554,16 @@ def format_evaluated_code_exception( tb: t.List[str] = [] indent = "" + skip_patterns = re.compile( + r"Traceback \(most recent call last\):|" + r'File ".*?core/model/definition\.py|' + r'File ".*?core/snapshot/definition\.py|' + r'File ".*?core/macros\.py|' + r'File ".*?inspect\.py' + ) + for error_line in format_exception(exception): - traceback_match = error_line.startswith("Traceback (most recent call last):") - model_def_match = re.search('File ".*?core/model/definition.py', error_line) - snapshot_def_match = re.search('File ".*?core/snapshot/definition.py', error_line) - core_macros_match = re.search('File ".*?core/macros.py', error_line) - if traceback_match or model_def_match or snapshot_def_match or core_macros_match: + if skip_patterns.search(error_line): continue error_match = re.search("^.*?Error: ", error_line) diff --git a/tests/core/test_macros.py b/tests/core/test_macros.py index 23fbce5198..5b79d5ff90 100644 --- a/tests/core/test_macros.py +++ b/tests/core/test_macros.py @@ -672,36 +672,26 @@ def test_positional_follows_kwargs(macro_evaluator): def test_macro_parameter_resolution(macro_evaluator): - with pytest.raises(MacroEvalError) as e: + with pytest.raises(MacroEvalError, match=".*missing a required argument: 'pos_only'"): macro_evaluator.evaluate(parse_one("@test_arg_resolution()")) - assert str(e.value.__cause__) == "missing a required argument: 'pos_only'" - with pytest.raises(MacroEvalError) as e: + with pytest.raises(MacroEvalError, match=".*missing a required argument: 'pos_only'"): macro_evaluator.evaluate(parse_one("@test_arg_resolution(a1 := 1)")) - assert str(e.value.__cause__) == "missing a required argument: 'pos_only'" - with pytest.raises(MacroEvalError) as e: + with pytest.raises(MacroEvalError, match=".*missing a required argument: 'a1'"): macro_evaluator.evaluate(parse_one("@test_arg_resolution(1)")) - assert str(e.value.__cause__) == "missing a required argument: 'a1'" - with pytest.raises(MacroEvalError) as e: + with pytest.raises(MacroEvalError, match=".*missing a required argument: 'a1'"): macro_evaluator.evaluate(parse_one("@test_arg_resolution(1, a2 := 2)")) - assert str(e.value.__cause__) == "missing a required argument: 'a1'" - with pytest.raises(MacroEvalError) as e: + with pytest.raises( + MacroEvalError, + match=".*'pos_only' parameter is positional only, but was passed as a keyword|.*missing a required positional-only argument: 'pos_only'|.*missing a required argument: 'a1'", + ): macro_evaluator.evaluate(parse_one("@test_arg_resolution(pos_only := 1)")) - # The CI was failing for Python 3.12 with the latter message, but other versions fail - # with the former one. This ensures we capture both. - assert str(e.value.__cause__) in ( - "'pos_only' parameter is positional only, but was passed as a keyword", - "missing a required positional-only argument: 'pos_only'", - "missing a required argument: 'a1'", - ) - - with pytest.raises(MacroEvalError) as e: + with pytest.raises(MacroEvalError, match=".*too many positional arguments"): macro_evaluator.evaluate(parse_one("@test_arg_resolution(1, 2, 3)")) - assert str(e.value.__cause__) == "too many positional arguments" def test_macro_metadata_flag(): @@ -808,28 +798,25 @@ def test_deduplicate(assert_exp_eq, dialect, sql, expected_sql): def test_deduplicate_error_handling(macro_evaluator): # Test error handling: non-list partition_by - with pytest.raises(SQLMeshError) as e: + with pytest.raises( + SQLMeshError, + match="partition_by must be a list of columns: \\[, cast\\( as \\)\\]", + ): macro_evaluator.evaluate(parse_one("@deduplicate(my_table, user_id, ['timestamp DESC'])")) - assert ( - str(e.value.__cause__) - == "partition_by must be a list of columns: [, cast( as )]" - ) # Test error handling: non-list order_by - with pytest.raises(SQLMeshError) as e: + with pytest.raises( + SQLMeshError, + match="order_by must be a list of strings, optional - nulls ordering: \\[' nulls '\\]", + ): macro_evaluator.evaluate(parse_one("@deduplicate(my_table, [user_id], 'timestamp DESC')")) - assert ( - str(e.value.__cause__) - == "order_by must be a list of strings, optional - nulls ordering: [' nulls ']" - ) # Test error handling: empty order_by - with pytest.raises(SQLMeshError) as e: + with pytest.raises( + SQLMeshError, + match="order_by must be a list of strings, optional - nulls ordering: \\[' nulls '\\]", + ): macro_evaluator.evaluate(parse_one("@deduplicate(my_table, [user_id], [])")) - assert ( - str(e.value.__cause__) - == "order_by must be a list of strings, optional - nulls ordering: [' nulls ']" - ) @pytest.mark.parametrize( @@ -991,34 +978,32 @@ def test_date_spine(assert_exp_eq, dialect, date_part): def test_date_spine_error_handling(macro_evaluator): # Test error handling: invalid datepart - with pytest.raises(SQLMeshError) as e: + with pytest.raises( + MacroEvalError, + match=".*Invalid datepart 'invalid'. Expected: 'day', 'week', 'month', 'quarter', or 'year'", + ): macro_evaluator.evaluate(parse_one("@date_spine('invalid', '2022-01-01', '2024-12-31')")) - assert ( - str(e.value.__cause__) - == "Invalid datepart 'invalid'. Expected: 'day', 'week', 'month', 'quarter', or 'year'" - ) # Test error handling: invalid start_date format - with pytest.raises(SQLMeshError) as e: + with pytest.raises( + MacroEvalError, + match=".*Invalid date format - start_date and end_date must be in format: YYYY-MM-DD", + ): macro_evaluator.evaluate(parse_one("@date_spine('day', '2022/01/01', '2024-12-31')")) - assert str(e.value.__cause__).startswith( - "Invalid date format - start_date and end_date must be in format: YYYY-MM-DD" - ) # Test error handling: invalid end_date format - with pytest.raises(SQLMeshError) as e: + with pytest.raises( + MacroEvalError, + match=".*Invalid date format - start_date and end_date must be in format: YYYY-MM-DD", + ): macro_evaluator.evaluate(parse_one("@date_spine('day', '2022-01-01', '2024/12/31')")) - assert str(e.value.__cause__).startswith( - "Invalid date format - start_date and end_date must be in format: YYYY-MM-DD" - ) # Test error handling: start_date after end_date - with pytest.raises(SQLMeshError) as e: + with pytest.raises( + MacroEvalError, + match=".*Invalid date range - start_date '2024-12-31' is after end_date '2022-01-01'.", + ): macro_evaluator.evaluate(parse_one("@date_spine('day', '2024-12-31', '2022-01-01')")) - assert ( - str(e.value.__cause__) - == "Invalid date range - start_date '2024-12-31' is after end_date '2022-01-01'." - ) def test_macro_union(assert_exp_eq, macro_evaluator: MacroEvaluator): @@ -1044,11 +1029,9 @@ def test_resolve_template_literal(): # Creating # This macro can work during creating / evaluating but only if @this_model is present in the context evaluator = MacroEvaluator(runtime_stage=RuntimeStage.CREATING) - with pytest.raises(SQLMeshError) as e: + with pytest.raises(MacroEvalError, match=".*this_model must be present"): evaluator.transform(parsed_sql) - assert "this_model must be present" in str(e.value.__cause__) - evaluator.locals.update( {"this_model": exp.to_table("test_catalog.sqlmesh__test.test__test_model__2517971505")} ) From 64c33058e89f2f97fc72e727e052a7bbcba5e38a Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:20:28 +0100 Subject: [PATCH 0076/1056] chore: update typescript (#4279) --- package-lock.json | 2 +- web/client/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71b3c12b67..fe563445ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18088,7 +18088,7 @@ "postcss": "^8.4.29", "prettier": "^3.0.3", "tailwindcss": "^3.3.3", - "typescript": "^5.2.2", + "typescript": "^5.8.2", "vite": "^4.4.9", "vite-plugin-css-injected-by-js": "^3.5.2", "vitest": "^0.34.3" diff --git a/web/client/package.json b/web/client/package.json index 9ef19cdc87..4eb00c1902 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -75,7 +75,7 @@ "postcss": "^8.4.29", "prettier": "^3.0.3", "tailwindcss": "^3.3.3", - "typescript": "^5.2.2", + "typescript": "^5.8.2", "vite": "^4.4.9", "vite-plugin-css-injected-by-js": "^3.5.2", "vitest": "^0.34.3" From c2a9695a153a132a15960290c4af3167deed19ff Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:31:08 +0100 Subject: [PATCH 0077/1056] chore: update eslint (#4280) --- .circleci/continue_config.yml | 25 +- .pre-commit-config.yaml | 32 - Makefile | 2 +- package-lock.json | 1586 +---------------- package.json | 6 +- web/client/.eslintrc.cjs | 57 - web/client/eslint.config.mjs | 26 + web/client/package.json | 13 +- .../src/library/components/table/Table.tsx | 1 - web/client/src/tests/utils.tsx | 1 - 10 files changed, 118 insertions(+), 1631 deletions(-) delete mode 100644 web/client/.eslintrc.cjs create mode 100644 web/client/eslint.config.mjs diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 71301a62f6..5ae0b3dc57 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -96,31 +96,24 @@ jobs: ui_style: docker: - - image: cimg/python:3.9 + - image: cimg/node:20.19.0 resource_class: small steps: - halt_unless_client - checkout - - run: - command: | - cp .pre-commit-config.yaml pre-commit-cache-key.txt - python --version --version >> pre-commit-cache-key.txt - restore_cache: keys: - - v1-pc-cache-{{ checksum "pre-commit-cache-key.txt" }} - - run: - name: Install pre-commit - command: pip install pre-commit - - run: - name: Fix Git URL override - command: git config --global --unset url."ssh://git@github.com".insteadOf + - v1-nm-cache-{{ checksum "package-lock.json" }} - run: - name: Run linters and code style checks - command: make ui-style + name: Install packages + command: npm ci - save_cache: - key: v1-pc-cache-{{ checksum "pre-commit-cache-key.txt" }} + key: v1-nm-cache-{{ checksum "package-lock.json" }} paths: - - ~/.cache/pre-commit + - /root/.npm + - run: + name: Run linters and code style checks + command: npm run lint ui_test: docker: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3057e365ec..96eca9b6a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,35 +23,3 @@ repos: files: *files require_serial: true exclude: ^(tests/fixtures) - - repo: https://github.com/pre-commit/mirrors-prettier - rev: "fc26039" - hooks: - - id: prettier - name: prettier - files: ^(web/client) - entry: prettier --write --ignore-path web/client/.prettierignore - exclude: ^(web/client/node_modules) - require_serial: true - language: node - - repo: https://github.com/pre-commit/mirrors-eslint - rev: "4620ec5" - hooks: - - id: eslint - name: eslint - files: ^(web/client) - exclude: ^(web/client/node_modules) - entry: eslint --fix - additional_dependencies: - [ - "@typescript-eslint/eslint-plugin@6.5.0", - "@typescript-eslint/parser@6.5.0", - eslint@8.48.0, - eslint-config-prettier@9.0.0, - eslint-config-standard-with-typescript@39.0.0, - eslint-plugin-import@2.28.1, - eslint-plugin-n@16.0.2, - eslint-plugin-promise@6.1.1, - eslint-plugin-react@7.33.2, - ] - require_serial: true - language: node diff --git a/Makefile b/Makefile index ab1509344b..55b4d37225 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ py-style: SKIP=prettier,eslint pre-commit run --all-files ui-style: - SKIP=ruff,ruff-format,mypy pre-commit run --all-files + npm run lint doc-test: python -m pytest --doctest-modules sqlmesh/core sqlmesh/utils diff --git a/package-lock.json b/package-lock.json index fe563445ab..19849931b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1201,46 +1201,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1255,14 +1215,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/@humanwhocodes/retry": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", @@ -3421,13 +3373,6 @@ "node": ">=14.0.0" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4764,13 +4709,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/jsonfile": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", @@ -4847,13 +4785,6 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -6200,27 +6131,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -6231,104 +6141,6 @@ "node": ">=8" } }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", @@ -6651,29 +6463,6 @@ "license": "MIT", "peer": true }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/builtins": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", - "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.0.0" - } - }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -7928,19 +7717,6 @@ "redux": "^4.2.0" } }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -8287,34 +8063,6 @@ "dev": true, "license": "MIT" }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", @@ -8352,19 +8100,6 @@ "node": ">= 0.4" } }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-to-primitive": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", @@ -8515,426 +8250,77 @@ } } }, - "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "semver": "^7.5.4" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "eslint": ">=6.0.0" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependencies": { - "eslint": ">=7.0.0" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "debug": "^3.2.7" + "is-glob": "^4.0.3" }, "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "node": ">=10.13.0" } }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-es-x": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", - "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/ota-meshi", - "https://opencollective.com/eslint" - ], - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.1.2", - "@eslint-community/regexpp": "^4.11.0", - "eslint-compat-utils": "^0.5.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": ">=8" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", - "hasown": "^2.0.2", - "is-core-module": "^2.15.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.0", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-n": { - "version": "16.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz", - "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "builtins": "^5.0.1", - "eslint-plugin-es-x": "^7.5.0", - "get-tsconfig": "^4.7.0", - "globals": "^13.24.0", - "ignore": "^5.2.4", - "is-builtin-module": "^3.2.1", - "is-core-module": "^2.12.1", - "minimatch": "^3.1.2", - "resolve": "^1.22.2", - "semver": "^7.5.3" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-n/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-n/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-n/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-promise": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", - "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -9534,19 +8920,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -10222,22 +9595,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "dev": true, - "license": "MIT", - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -10473,16 +9830,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -10754,24 +10101,6 @@ "node": ">=8" } }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -11003,19 +10332,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/jsonc-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", @@ -11097,22 +10413,6 @@ "npm": ">=6" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -12282,6 +11582,7 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", + "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12922,75 +12223,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -14462,16 +13694,6 @@ "node": ">=4" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -14515,69 +13737,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/rollup": { "version": "3.29.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", @@ -15326,45 +14485,6 @@ "node": ">=8" } }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -15478,16 +14598,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -15933,13 +15043,6 @@ "node": "*" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/thememirror": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/thememirror/-/thememirror-2.0.1.tgz", @@ -16145,19 +15248,6 @@ } } }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -16211,19 +15301,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -16328,6 +15405,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz", + "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.31.1", + "@typescript-eslint/parser": "8.31.1", + "@typescript-eslint/utils": "8.31.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/typical": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", @@ -18065,6 +17165,7 @@ "zustand": "^4.4.1" }, "devDependencies": { + "@eslint/js": "^9.25.1", "@playwright/test": "^1.37.1", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "^14.0.0", @@ -18073,560 +17174,19 @@ "@types/pluralize": "^0.0.30", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.5.0", "@vitejs/plugin-react-swc": "^3.3.2", "autoprefixer": "^10.4.15", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-config-standard-with-typescript": "^39.0.0", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-n": "^16.0.2", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.33.2", "jsdom": "^22.1.0", "orval": "^6.22.1", "postcss": "^8.4.29", "prettier": "^3.0.3", "tailwindcss": "^3.3.3", - "typescript": "^5.8.2", + "typescript": "^5.8.3", + "typescript-eslint": "^8.31.1", "vite": "^4.4.9", "vite-plugin-css-injected-by-js": "^3.5.2", "vitest": "^0.34.3" } - }, - "web/client/node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "web/client/node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "web/client/node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "web/client/node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "web/client/node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "web/client/node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "web/client/node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "web/client/node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "web/client/node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "web/client/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "web/client/node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "web/client/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "web/client/node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "web/client/node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "web/client/node_modules/eslint-config-standard": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", - "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-promise": "^6.0.0" - } - }, - "web/client/node_modules/eslint-config-standard-with-typescript": { - "version": "39.1.1", - "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-39.1.1.tgz", - "integrity": "sha512-t6B5Ep8E4I18uuoYeYxINyqcXb2UbC0SOOTxRtBSt2JUs+EzeXbfe2oaiPs71AIdnoWhXDO2fYOHz8df3kV84A==", - "deprecated": "Please use eslint-config-love, instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/parser": "^6.4.0", - "eslint-config-standard": "17.1.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^6.4.0", - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-promise": "^6.0.0", - "typescript": "*" - } - }, - "web/client/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "web/client/node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "web/client/node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "web/client/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "web/client/node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "web/client/node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "web/client/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "web/client/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "web/client/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "web/client/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "web/client/node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } } } } diff --git a/package.json b/package.json index 204b5919f1..c6adc005f2 100644 --- a/package.json +++ b/package.json @@ -2,5 +2,9 @@ "workspaces": [ "vscode/extension", "web/client" - ] + ], + "scripts": { + "lint": "npm run lint --workspaces", + "lint:fix": "npm run lint:fix --workspaces" + } } diff --git a/web/client/.eslintrc.cjs b/web/client/.eslintrc.cjs deleted file mode 100644 index 900582dd16..0000000000 --- a/web/client/.eslintrc.cjs +++ /dev/null @@ -1,57 +0,0 @@ -const OFF = 0 -const ERROR = 2 - -module.exports = { - root: true, - env: { - browser: true, - es2021: true, - }, - extends: ['plugin:react/recommended', 'standard-with-typescript', 'prettier'], - parser: '@typescript-eslint/parser', - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json', - }, - plugins: ['react', '@typescript-eslint'], - rules: { - 'react/jsx-uses-react': OFF, - 'react/react-in-jsx-scope': OFF, - 'no-use-before-define': OFF, - '@typescript-eslint/promise-function-async': OFF, - '@typescript-eslint/no-non-null-assertion': OFF, - 'no-return-await': OFF, - '@typescript-eslint/return-await': OFF, - '@typescript-eslint/no-use-before-define': [ - ERROR, - { - variables: true, - functions: false, - classes: false, - allowNamedExports: true, - }, - ], - '@typescript-eslint/no-dynamic-delete': OFF, - '@typescript-eslint/naming-convention': [ - ERROR, - { - selector: 'variable', - format: ['camelCase', 'PascalCase', 'UPPER_CASE', 'snake_case'], - }, - ], - '@typescript-eslint/no-confusing-void-expression': OFF, - }, - ignorePatterns: [ - 'src/api/client.ts', - 'src/utils/tbk-components.js', - 'test-results', - 'playwright', - 'playwright-report', - 'dist', - ], - settings: { - react: { - version: '18.2', - }, - }, -} diff --git a/web/client/eslint.config.mjs b/web/client/eslint.config.mjs new file mode 100644 index 0000000000..a0dacd1520 --- /dev/null +++ b/web/client/eslint.config.mjs @@ -0,0 +1,26 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: [ + 'dist/**/*', + 'node_modules/**', + 'src/utils/tbk-components.js', + '**/*.cjs', + '**/*.mjs', + 'src/api/client.ts', + ], + }, + eslint.configs.recommended, + tseslint.configs.recommended, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + 'no-empty': 'off', + }, + }, +) diff --git a/web/client/package.json b/web/client/package.json index 4eb00c1902..ba89f7694e 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -7,6 +7,7 @@ "preview": "vite preview --no-open", "prettier": "prettier --write .", "lint:fix": "eslint --fix .", + "lint": "eslint src", "format": "npm run prettier && npm run lint:fix", "test": "npm run generate:api npm run test:unit && npm run test:e2e", "test:unit:watch": "NODE_ENV=development vitest --watch=true", @@ -52,6 +53,7 @@ "zustand": "^4.4.1" }, "devDependencies": { + "@eslint/js": "^9.25.1", "@playwright/test": "^1.37.1", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "^14.0.0", @@ -60,22 +62,15 @@ "@types/pluralize": "^0.0.30", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.5.0", "@vitejs/plugin-react-swc": "^3.3.2", "autoprefixer": "^10.4.15", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-config-standard-with-typescript": "^39.0.0", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-n": "^16.0.2", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.33.2", "jsdom": "^22.1.0", "orval": "^6.22.1", "postcss": "^8.4.29", "prettier": "^3.0.3", "tailwindcss": "^3.3.3", - "typescript": "^5.8.2", + "typescript": "^5.8.3", + "typescript-eslint": "^8.31.1", "vite": "^4.4.9", "vite-plugin-css-injected-by-js": "^3.5.2", "vitest": "^0.34.3" diff --git a/web/client/src/library/components/table/Table.tsx b/web/client/src/library/components/table/Table.tsx index 55d27e7ee7..e083bfb360 100644 --- a/web/client/src/library/components/table/Table.tsx +++ b/web/client/src/library/components/table/Table.tsx @@ -22,7 +22,6 @@ import { EnumSize } from '~/types/enum' import { isArrayNotEmpty, isNotNil } from '@utils/index' declare module '@tanstack/table-core' { - // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ColumnMeta { type: string } diff --git a/web/client/src/tests/utils.tsx b/web/client/src/tests/utils.tsx index 179130ebce..9c4cb6af91 100644 --- a/web/client/src/tests/utils.tsx +++ b/web/client/src/tests/utils.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/export */ import { cleanup, render, type RenderResult } from '@testing-library/react' import { afterEach } from 'vitest' From 06c9cb9e78e91289876e049a190165a01acc732c Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:25:24 +0100 Subject: [PATCH 0078/1056] ci: make vscode job also run in pr (#4281) --- .circleci/config.yml | 58 ++++++++--------------- .circleci/continue_config.yml | 87 +++++++++++++++++++++-------------- 2 files changed, 71 insertions(+), 74 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a69c4cbc22..9cf01656fa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,23 +20,6 @@ orbs: path-filtering: circleci/path-filtering@1.2.0 jobs: - vscode-extension-setup: - docker: - - image: cimg/node:20.19.0-browsers - resource_class: small - steps: - - checkout - - run: - name: Install VSCode extension dependencies - command: | - cd vscode/extension - npm ci - - run: - name: Run VSCode extension CI - command: | - cd vscode/extension - npm run ci - publish: docker: - image: cimg/python:3.10 @@ -86,25 +69,25 @@ jobs: paths: - dist trigger_private_renovate: - docker: - - image: cimg/base:2021.11 - resource_class: small - steps: - - run: - name: Trigger private renovate - command: | - curl --request POST \ - --url $TOBIKO_PRIVATE_CIRCLECI_URL \ - --header "Circle-Token: $TOBIKO_PRIVATE_CIRCLECI_KEY" \ - --header "content-type: application/json" \ - --data '{ - "branch":"main", - "parameters":{ - "run_main_pr":false, - "run_sqlmesh_commit":false, - "run_renovate":true - } - }' + docker: + - image: cimg/base:2021.11 + resource_class: small + steps: + - run: + name: Trigger private renovate + command: | + curl --request POST \ + --url $TOBIKO_PRIVATE_CIRCLECI_URL \ + --header "Circle-Token: $TOBIKO_PRIVATE_CIRCLECI_KEY" \ + --header "content-type: application/json" \ + --data '{ + "branch":"main", + "parameters":{ + "run_main_pr":false, + "run_sqlmesh_commit":false, + "run_renovate":true + } + }' workflows: setup-workflow: @@ -117,9 +100,6 @@ workflows: \.circleci/.*|Makefile|\.pre-commit-config\.yaml common true vscode/extensions/.* vscode true tag: "3.9" - - - vscode-extension-setup: - <<: *on_main_or_tag_filter - gh-release: <<: *on_tag_filter - ui-build: diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 5ae0b3dc57..79b01e90ac 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -34,6 +34,22 @@ commands: - run: circleci-agent step halt jobs: + vscode_test: + docker: + - image: cimg/node:20.19.1-browsers + resource_class: small + steps: + - checkout + - run: + name: Install VSCode extension dependencies + command: | + cd vscode/extension + npm ci + - run: + name: Run VSCode extension CI + command: | + cd vscode/extension + npm run ci doc_tests: docker: - image: cimg/python:3.10 @@ -127,7 +143,7 @@ jobs: - v1-nm-cache-{{ checksum "package-lock.json" }} - run: name: Install packages - command: npm ci + command: npm ci - save_cache: key: v1-nm-cache-{{ checksum "package-lock.json" }} paths: @@ -137,37 +153,37 @@ jobs: command: npm --prefix web/client run test trigger_private_tests: - docker: - - image: cimg/python:3.12.0 - resource_class: small - steps: - - checkout - - run: - name: Install setuptools scm - command: pip install setuptools_scm - - run: - name: Trigger private tests - command: | - export COMMIT_MESSAGE="$(git log --format=%s -n 1 $CIRCLE_SHA1)" - export FORMATTED_COMMIT_MESSAGE="${COMMIT_MESSAGE//\"/\\\"}" - # returns a version string like 0.1.0.dev11 - export PACKAGE_VERSION="$(python ./.circleci/get_scm_version.py)" - curl --request POST \ - --url $TOBIKO_PRIVATE_CIRCLECI_URL \ - --header "Circle-Token: $TOBIKO_PRIVATE_CIRCLECI_KEY" \ - --header "content-type: application/json" \ - --data '{ - "branch":"main", - "parameters":{ - "run_main_pr":false, - "run_sqlmesh_commit":true, - "sqlmesh_branch":"'$CIRCLE_BRANCH'", - "sqlmesh_commit_author":"'$CIRCLE_USERNAME'", - "sqlmesh_commit_hash":"'$CIRCLE_SHA1'", - "sqlmesh_commit_message":"'"$FORMATTED_COMMIT_MESSAGE"'", - "sqlmesh_package_version":"'$PACKAGE_VERSION'" - } - }' + docker: + - image: cimg/python:3.12.0 + resource_class: small + steps: + - checkout + - run: + name: Install setuptools scm + command: pip install setuptools_scm + - run: + name: Trigger private tests + command: | + export COMMIT_MESSAGE="$(git log --format=%s -n 1 $CIRCLE_SHA1)" + export FORMATTED_COMMIT_MESSAGE="${COMMIT_MESSAGE//\"/\\\"}" + # returns a version string like 0.1.0.dev11 + export PACKAGE_VERSION="$(python ./.circleci/get_scm_version.py)" + curl --request POST \ + --url $TOBIKO_PRIVATE_CIRCLECI_URL \ + --header "Circle-Token: $TOBIKO_PRIVATE_CIRCLECI_KEY" \ + --header "content-type: application/json" \ + --data '{ + "branch":"main", + "parameters":{ + "run_main_pr":false, + "run_sqlmesh_commit":true, + "sqlmesh_branch":"'$CIRCLE_BRANCH'", + "sqlmesh_commit_author":"'$CIRCLE_USERNAME'", + "sqlmesh_commit_hash":"'$CIRCLE_SHA1'", + "sqlmesh_commit_message":"'"$FORMATTED_COMMIT_MESSAGE"'", + "sqlmesh_package_version":"'$PACKAGE_VERSION'" + } + }' engine_tests_docker: parameters: @@ -271,9 +287,9 @@ workflows: - clickhouse-cloud - athena filters: - branches: - only: - - main + branches: + only: + - main - trigger_private_tests: requires: - style_and_cicd_tests @@ -283,4 +299,5 @@ workflows: - main - ui_style - ui_test + - vscode_test - migration_test From 97264b767fc92f46eaac0d3cbafe8fbed23a1bef Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:52:14 +0100 Subject: [PATCH 0079/1056] chore: make ui-up work again (#4282) --- web/Dockerfile.api | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/Dockerfile.api b/web/Dockerfile.api index ded45d6059..da9438ec87 100644 --- a/web/Dockerfile.api +++ b/web/Dockerfile.api @@ -3,6 +3,8 @@ FROM python:3.11 WORKDIR /sqlmesh COPY pyproject.toml pyproject.toml +COPY Makefile Makefile +COPY examples/custom_materializations/ examples/custom_materializations/ COPY sqlmesh/_version.py sqlmesh/_version.py -RUN pip install -e .[dev,web] +RUN make install-dev From e3a5526d9accdcaad715059dd3cb39201c78cac7 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:59:13 +0100 Subject: [PATCH 0080/1056] chore: update orval in web (#4283) --- package-lock.json | 787 +++++++++++++--------------------------- web/client/package.json | 2 +- 2 files changed, 258 insertions(+), 531 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19849931b1..eecc4fb1ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -1137,6 +1136,20 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.3.0.tgz", + "integrity": "sha512-frvArO0+s5Viq68uSod5SieLPVM2cLpXoQ1e07lURwgADXpL/MOypM7jPz9otks0g2DIe2YedDAeVrDyYJZRxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.3.0", + "@shikijs/langs": "^3.3.0", + "@shikijs/themes": "^3.3.0", + "@shikijs/types": "^3.3.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, "node_modules/@headlessui/react": { "version": "1.7.19", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", @@ -1230,13 +1243,13 @@ } }, "node_modules/@ibm-cloud/openapi-ruleset": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset/-/openapi-ruleset-1.30.1.tgz", - "integrity": "sha512-xVTkMwbjQ/bKfTXXb+/KEDsw8vRoZd6uxgFkoqfzMZ8/rJg2WP5hIpY7z6RegQ1pfF6LZ7RYS0u6h8xQo17ezw==", + "version": "1.31.0", + "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset/-/openapi-ruleset-1.31.0.tgz", + "integrity": "sha512-J2bkqFCA89VVkRO0alC0LaSkCZhtntndFNthWOxFyh7wBfazEcvLOWJFIqc87exL40DcgEUJTt/sXkL5ovW+Hg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@ibm-cloud/openapi-ruleset-utilities": "1.8.1", + "@ibm-cloud/openapi-ruleset-utilities": "1.9.0", "@stoplight/spectral-formats": "^1.8.2", "@stoplight/spectral-functions": "^1.9.3", "@stoplight/spectral-rulesets": "^1.21.3", @@ -1253,9 +1266,9 @@ } }, "node_modules/@ibm-cloud/openapi-ruleset-utilities": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset-utilities/-/openapi-ruleset-utilities-1.8.1.tgz", - "integrity": "sha512-SayiOoNs7KaZiBFTV4/IQE3eigW3YsS7W1SXYovv4DBXxp0mSLWGDoZXHAjGs1vH8KubD7KyCK8+0Y3SoDZioA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset-utilities/-/openapi-ruleset-utilities-1.9.0.tgz", + "integrity": "sha512-AoFbSarOqFBYH+1TZ9Ahkm2IWYSi5v0pBk88fpV+5b3qGJukypX8PwvCWADjuyIccKg48/F73a6hTTkBzDQ2UA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1510,445 +1523,52 @@ } }, "node_modules/@orval/angular": { - "version": "6.31.0", - "resolved": "https://registry.npmjs.org/@orval/angular/-/angular-6.31.0.tgz", - "integrity": "sha512-cVV/vh6biGUe5FMR0kaOL+pYkD5lM/oHpyHVU19d2eY/hxKCG58/CagUNVDxbowcSalzGpt7NbZOqpauc2cNOA==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/angular/-/angular-7.9.0.tgz", + "integrity": "sha512-GzgEdZxK/9wQMLN2bziTlPSD9bkRXwYf1PoUM+RTXj6MGw0aZVWNTMCnp3dFWp9VemThP0kK2geBFqhxC2Bgxg==", "dev": true, "license": "MIT", "dependencies": { - "@orval/core": "6.31.0" + "@orval/core": "7.9.0" } }, "node_modules/@orval/axios": { - "version": "6.31.0", - "resolved": "https://registry.npmjs.org/@orval/axios/-/axios-6.31.0.tgz", - "integrity": "sha512-OqWFJ6bDKftsSW3VI7Ouqcb3W4hDhkk8XzDkb/iisn3Dn1rkSE/wafdlHCm+62VQps4esYXaP1+7/HSk/2+Y8A==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/axios/-/axios-7.9.0.tgz", + "integrity": "sha512-e77WvQGfFTkkrJIH66v/DpKdZ1eQBlu4NxOt2gCxvBYFP2dxDR2ajsM7uXxdGxi0iqZIS92+opzhxLIo6TVyDQ==", "dev": true, "license": "MIT", "dependencies": { - "@orval/core": "6.31.0" + "@orval/core": "7.9.0" } }, "node_modules/@orval/core": { - "version": "6.31.0", - "resolved": "https://registry.npmjs.org/@orval/core/-/core-6.31.0.tgz", - "integrity": "sha512-ubOPpxzLgOCGbAQsq/dzfe/MIgB4LYWRyuwgnkV2GkL8Zq7cIWfmZU09GTJZQ6cO35OclFfbbyNve0cRMfSBeA==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/core/-/core-7.9.0.tgz", + "integrity": "sha512-/Nn6/ARmpevAY7Vl9RRXY2WkJx/q0LIUEE2Eh15bGgzAQIYUcD9aFr9zM5hX2b3lR/fZ8721hFsq0vM9O5ZzXw==", "dev": true, "license": "MIT", "dependencies": { - "@apidevtools/swagger-parser": "^10.1.0", - "@ibm-cloud/openapi-ruleset": "^1.14.2", - "acorn": "^8.11.2", - "ajv": "^8.12.0", + "@apidevtools/swagger-parser": "^10.1.1", + "@ibm-cloud/openapi-ruleset": "^1.29.4", + "acorn": "^8.14.1", + "ajv": "^8.17.1", "chalk": "^4.1.2", - "compare-versions": "^6.1.0", - "debug": "^4.3.4", - "esbuild": "^0.19.11", + "compare-versions": "^6.1.1", + "debug": "^4.4.0", + "esbuild": "^0.25.1", "esutils": "2.0.3", - "fs-extra": "^11.2.0", + "fs-extra": "^11.3.0", "globby": "11.1.0", - "lodash.get": "^4.4.2", "lodash.isempty": "^4.4.0", - "lodash.omit": "^4.5.0", "lodash.uniq": "^4.5.0", "lodash.uniqby": "^4.7.0", "lodash.uniqwith": "^4.5.0", - "micromatch": "^4.0.5", - "openapi3-ts": "4.2.2", + "micromatch": "^4.0.8", + "openapi3-ts": "4.4.0", "swagger2openapi": "^7.0.8" } }, - "node_modules/@orval/core/node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@orval/core/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -1966,45 +1586,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@orval/core/node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" - } - }, "node_modules/@orval/core/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2012,70 +1593,91 @@ "dev": true, "license": "MIT" }, + "node_modules/@orval/core/node_modules/openapi3-ts": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", + "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.5.0" + } + }, "node_modules/@orval/fetch": { - "version": "6.31.0", - "resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-6.31.0.tgz", - "integrity": "sha512-K4pD0TqRX3n1QgsfdzcCLxZPj4WFr4xd51VS5PhtK7wewy+EwaTp5AZeeMT+o8dL4HQcwLsKaXA1HH1YiAuOrA==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-7.9.0.tgz", + "integrity": "sha512-gIw2a3jXd1If/NpewVq7C6XDfnG2RPMt4PKR/RtEBeDKasXkoJeS2DBvZp/TyC+lt9oMgketF3bmzo/st09uhA==", "dev": true, "license": "MIT", "dependencies": { - "@orval/core": "6.31.0" + "@orval/core": "7.9.0" } }, "node_modules/@orval/hono": { - "version": "6.31.0", - "resolved": "https://registry.npmjs.org/@orval/hono/-/hono-6.31.0.tgz", - "integrity": "sha512-mM5WISLugu1quNkNUqYwp+StV/Z5/STm33VdPTWkoZyPJtV4NmEUZKPsowk0EN7sBF2kW+aYcp8lsNMXxXfHaw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/hono/-/hono-7.9.0.tgz", + "integrity": "sha512-80VoS5W4I0uUo7Y6sIxr0xGYNX3oIL8sKWru+mYIZdp2L4W1lVHBi4zkpk7u0u9Obv7vmAuPtozV5+QIV0zWBg==", "dev": true, "license": "MIT", "dependencies": { - "@orval/core": "6.31.0", - "@orval/zod": "6.31.0", + "@orval/core": "7.9.0", + "@orval/zod": "7.9.0", "lodash.uniq": "^4.5.0" } }, + "node_modules/@orval/mcp": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/mcp/-/mcp-7.9.0.tgz", + "integrity": "sha512-zMtW4jUKXGiXyJUylVy58kCu/Jf1yF9wp3ul2Guy1vbjlhVeOO1ugCYQ1sYNYH10vN0ajTS0/2pXhTuCR7PxHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0", + "@orval/zod": "7.9.0" + } + }, "node_modules/@orval/mock": { - "version": "6.31.0", - "resolved": "https://registry.npmjs.org/@orval/mock/-/mock-6.31.0.tgz", - "integrity": "sha512-UBag0IyL0eDVdXWgIMS/YxDF57Q3XC4VRDqcuZ1lB77rfBZ4UiVqTJleczQoIqMGkdtJJlBABgWzRRts1K4img==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/mock/-/mock-7.9.0.tgz", + "integrity": "sha512-Ixhb+I4VTIfUl0qxDq8LekBnXM2gpD4kS7OFVqX9rdx0ZwZl7y4xArnKSXk5qgDPjo4eOWSmVA3onuL2WCit/g==", "dev": true, "license": "MIT", "dependencies": { - "@orval/core": "6.31.0", - "lodash.get": "^4.4.2", - "lodash.omit": "^4.5.0", + "@orval/core": "7.9.0", "openapi3-ts": "^4.2.2" } }, "node_modules/@orval/query": { - "version": "6.31.0", - "resolved": "https://registry.npmjs.org/@orval/query/-/query-6.31.0.tgz", - "integrity": "sha512-aVyvSU5IbpRQnVbhChNlLX2XDnmoT1cDJ59NEFS3byhiJf1EG5XlzVve98je/BHAsVROrUC8+o6XoIjCtYbW5Q==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/query/-/query-7.9.0.tgz", + "integrity": "sha512-IPKP4l00dZw0AOt+7PPB82WNdmpPThsWlYvk4YmdLEZVWCuaUaJ9KyLTT2R+XqoksuuuKTJ/IK0KO7VcjMiHiA==", "dev": true, "license": "MIT", "dependencies": { - "@orval/core": "6.31.0", + "@orval/core": "7.9.0", + "@orval/fetch": "7.9.0", "lodash.omitby": "^4.6.0" } }, "node_modules/@orval/swr": { - "version": "6.31.0", - "resolved": "https://registry.npmjs.org/@orval/swr/-/swr-6.31.0.tgz", - "integrity": "sha512-J9W/kym9jc94GizbTozpuY76yaZRN98rf3ahj+2+eW8+NRW1dVFui32Gew1qj9rcCSA54BwRMONgEn3Xqx6W6A==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/swr/-/swr-7.9.0.tgz", + "integrity": "sha512-f06MifzMPrnXYdgt2rLLnurJ0YlXexSMyVlXAHhaJENtSVM4zlJp69rA6OULLr1i1biNGTWHSonOizKOf+cNcw==", "dev": true, "license": "MIT", "dependencies": { - "@orval/core": "6.31.0" + "@orval/core": "7.9.0", + "@orval/fetch": "7.9.0" } }, "node_modules/@orval/zod": { - "version": "6.31.0", - "resolved": "https://registry.npmjs.org/@orval/zod/-/zod-6.31.0.tgz", - "integrity": "sha512-v6wqGZf4s3tpWrnmMHlEBfhTLeebu5W3HmhP8vQ5BPkm8AB2asiZqzK3Ne9Y19Rvyx6X4FGnhnalKYkz+XxJ8Q==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/zod/-/zod-7.9.0.tgz", + "integrity": "sha512-LkkofL+iSswsBVWCr3bPZ6un8065wB7x34DZVnF/gN3kBxy8A35I9X0Eld4EajI/1rbMmxzt7wHUYAf5NlJWHQ==", "dev": true, "license": "MIT", "dependencies": { - "@orval/core": "6.31.0", + "@orval/core": "7.9.0", "lodash.uniq": "^4.5.0" } }, @@ -3373,6 +2975,55 @@ "node": ">=14.0.0" } }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.3.0.tgz", + "integrity": "sha512-l0vIw+GxeNU7uGnsu6B+Crpeqf+WTQ2Va71cHb5ZYWEVEPdfYwY5kXwYqRJwHrxz9WH+pjSpXQz+TJgAsrkA5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.3.0.tgz", + "integrity": "sha512-zt6Kf/7XpBQKSI9eqku+arLkAcDQ3NHJO6zFjiChI8w0Oz6Jjjay7pToottjQGjSDCFk++R85643WbyINcuL+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.3.0.tgz", + "integrity": "sha512-tXeCvLXBnqq34B0YZUEaAD1lD4lmN6TOHAhnHacj4Owh7Ptb/rf5XCDeROZt2rEOk5yuka3OOW2zLqClV7/SOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.3.0.tgz", + "integrity": "sha512-KPCGnHG6k06QG/2pnYGbFtFvpVJmC3uIpXrAiPrawETifujPBv0Se2oUxm5qYgjCvGJS9InKvjytOdN+bGuX+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3518,6 +3169,20 @@ "ajv": ">=8" } }, + "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", + "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, "node_modules/@stoplight/spectral-core/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -3593,9 +3258,9 @@ } }, "node_modules/@stoplight/spectral-functions": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.0.tgz", - "integrity": "sha512-MxOFqDyZHbyN4rbHyKBnUXN6R9DrjiSXFHlTNMHgM2bHlCoecXk0uWPz2wa9sqFJjprCg9W1XT70shK566SZHg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.1.tgz", + "integrity": "sha512-obu8ZfoHxELOapfGsCJixKZXZcffjg+lSoNuttpmUFuDzVLT3VmH8QkPXfOGOL5Pz80BR35ClNAToDkdnYIURg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3816,9 +3481,9 @@ } }, "node_modules/@stoplight/types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", - "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", + "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10586,14 +10251,6 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "license": "MIT" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -10650,14 +10307,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.omit": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==", - "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.omitby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.omitby/-/lodash.omitby-4.6.0.tgz", @@ -10783,6 +10432,13 @@ "node": ">=10" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -12410,26 +12066,27 @@ } }, "node_modules/orval": { - "version": "6.31.0", - "resolved": "https://registry.npmjs.org/orval/-/orval-6.31.0.tgz", - "integrity": "sha512-515KTDQ4VRJCT+4DsMrK/QROWRq4PXrjgxAoEx3jmP7j+aQBGbx8WhidIF6aX1UgbTxw47Lq7QVp9mbnD0lnWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "^10.1.0", - "@orval/angular": "6.31.0", - "@orval/axios": "6.31.0", - "@orval/core": "6.31.0", - "@orval/fetch": "6.31.0", - "@orval/hono": "6.31.0", - "@orval/mock": "6.31.0", - "@orval/query": "6.31.0", - "@orval/swr": "6.31.0", - "@orval/zod": "6.31.0", - "ajv": "^8.12.0", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/orval/-/orval-7.9.0.tgz", + "integrity": "sha512-kFftcVojM4wRddRktqJPI/P9uYRpgiwCFOxF82G7XqDrczX9XDu8b5ialof+Z1LIuVZL4CvLV0Y184mRgrJrUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.1", + "@orval/angular": "7.9.0", + "@orval/axios": "7.9.0", + "@orval/core": "7.9.0", + "@orval/fetch": "7.9.0", + "@orval/hono": "7.9.0", + "@orval/mcp": "7.9.0", + "@orval/mock": "7.9.0", + "@orval/query": "7.9.0", + "@orval/swr": "7.9.0", + "@orval/zod": "7.9.0", + "ajv": "^8.17.1", "cac": "^6.7.14", "chalk": "^4.1.2", - "chokidar": "^3.6.0", + "chokidar": "^4.0.3", "enquirer": "^2.4.1", "execa": "^5.1.1", "find-up": "5.0.0", @@ -12437,7 +12094,10 @@ "lodash.uniq": "^4.5.0", "openapi3-ts": "4.2.2", "string-argv": "^0.3.2", - "tsconfck": "^2.0.1" + "tsconfck": "^2.0.1", + "typedoc": "^0.28.0", + "typedoc-plugin-markdown": "^4.4.2", + "typescript": "^5.6.3" }, "bin": { "orval": "dist/bin/orval.js" @@ -12460,6 +12120,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/orval/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/orval/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -12467,6 +12143,20 @@ "dev": true, "license": "MIT" }, + "node_modules/orval/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -15391,6 +15081,43 @@ "underscore": "^1.12.1" } }, + "node_modules/typedoc": { + "version": "0.28.3", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.3.tgz", + "integrity": "sha512-5svOCTfXvVSh6zbZKSQluZhR8yN2tKpTeHZxlmWpE6N5vc3R8k/jhg9nnD6n5tN9/ObuQTojkONrOxFdUFUG9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.2.2", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.7.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + } + }, + "node_modules/typedoc-plugin-markdown": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.6.3.tgz", + "integrity": "sha512-86oODyM2zajXwLs4Wok2mwVEfCwCnp756QyhLGX2IfsdRYr1DXLCgJgnLndaMUjJD7FBhnLk2okbNE9PdLxYRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typedoc": "0.28.x" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -17177,7 +16904,7 @@ "@vitejs/plugin-react-swc": "^3.3.2", "autoprefixer": "^10.4.15", "jsdom": "^22.1.0", - "orval": "^6.22.1", + "orval": "^7.9.0", "postcss": "^8.4.29", "prettier": "^3.0.3", "tailwindcss": "^3.3.3", diff --git a/web/client/package.json b/web/client/package.json index ba89f7694e..080f06282c 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -65,7 +65,7 @@ "@vitejs/plugin-react-swc": "^3.3.2", "autoprefixer": "^10.4.15", "jsdom": "^22.1.0", - "orval": "^6.22.1", + "orval": "^7.9.0", "postcss": "^8.4.29", "prettier": "^3.0.3", "tailwindcss": "^3.3.3", From 99729af1e551b14fba8675aa68e90b4a9b435a3e Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Apr 2025 14:18:13 +0100 Subject: [PATCH 0081/1056] chore: update vite in web (#4284) --- package-lock.json | 3702 ++++++++++++++++----------------------- web/client/package.json | 7 +- web/docker-compose.yml | 1 + 3 files changed, 1488 insertions(+), 2222 deletions(-) diff --git a/package-lock.json b/package-lock.json index eecc4fb1ad..30a827834d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -493,1242 +493,905 @@ "w3c-keyname": "^2.2.4" } }, - "node_modules/@esbuild/aix-ppc64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "aix" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", - "cpu": [ - "arm" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=18" + "node": "*" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", - "cpu": [ - "arm" - ], + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", - "cpu": [ - "ia32" - ], + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=18" + "node": "*" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", - "cpu": [ - "loong64" - ], + "node_modules/@eslint/js": { + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", + "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", - "cpu": [ - "mips64el" - ], + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", - "cpu": [ - "ppc64" - ], + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", - "cpu": [ - "riscv64" - ], - "dev": true, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@floating-ui/utils": "^0.2.9" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", - "cpu": [ - "s390x" - ], - "dev": true, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", - "cpu": [ - "arm64" - ], + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.3.0.tgz", + "integrity": "sha512-frvArO0+s5Viq68uSod5SieLPVM2cLpXoQ1e07lURwgADXpL/MOypM7jPz9otks0g2DIe2YedDAeVrDyYJZRxA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@shikijs/engine-oniguruma": "^3.3.0", + "@shikijs/langs": "^3.3.0", + "@shikijs/themes": "^3.3.0", + "@shikijs/types": "^3.3.0", + "@shikijs/vscode-textmate": "^10.0.2" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@headlessui/react": { + "version": "1.7.19", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", + "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.60", + "client-only": "^0.0.1" + }, "engines": { - "node": ">=18" + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", - "cpu": [ - "x64" - ], + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", - "cpu": [ - "x64" - ], + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, "engines": { - "node": ">=18" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", - "cpu": [ - "arm64" - ], + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", - "cpu": [ - "ia32" - ], + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", - "cpu": [ - "x64" - ], + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "node_modules/@ibm-cloud/openapi-ruleset": { + "version": "1.31.0", + "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset/-/openapi-ruleset-1.31.0.tgz", + "integrity": "sha512-J2bkqFCA89VVkRO0alC0LaSkCZhtntndFNthWOxFyh7wBfazEcvLOWJFIqc87exL40DcgEUJTt/sXkL5ovW+Hg==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "@ibm-cloud/openapi-ruleset-utilities": "1.9.0", + "@stoplight/spectral-formats": "^1.8.2", + "@stoplight/spectral-functions": "^1.9.3", + "@stoplight/spectral-rulesets": "^1.21.3", + "chalk": "^4.1.2", + "jsonschema": "^1.5.0", + "lodash": "^4.17.21", + "loglevel": "^1.9.2", + "loglevel-plugin-prefix": "0.8.4", + "minimatch": "^6.2.0", + "validator": "^13.11.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=16.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "node_modules/@ibm-cloud/openapi-ruleset-utilities": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset-utilities/-/openapi-ruleset-utilities-1.9.0.tgz", + "integrity": "sha512-AoFbSarOqFBYH+1TZ9Ahkm2IWYSi5v0pBk88fpV+5b3qGJukypX8PwvCWADjuyIccKg48/F73a6hTTkBzDQ2UA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=16.0.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "node_modules/@ibm-cloud/openapi-ruleset/node_modules/minimatch": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", "dev": true, - "license": "Apache-2.0", + "license": "ISC", "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": "*" + "node": ">=12" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=8" } }, - "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.15" + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=6.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@eslint/js": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", - "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "node_modules/@jsep-plugin/ternary": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", + "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.13.0", - "levn": "^0.4.1" - }, + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" } }, - "node_modules/@exodus/schemasafe": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", - "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", - "dev": true, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", "license": "MIT" }, - "node_modules/@floating-ui/core": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@lezer/common": "^1.0.0" } }, - "node_modules/@floating-ui/dom": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.9" + "@lezer/common": "^1.0.0" } }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "node_modules/@lezer/python": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lit/react": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.7.tgz", + "integrity": "sha512-cencnwwLXQKiKxjfFzSgZRngcWJzUDZi/04E0fSaF86wZgchMdvTyu+lE36DrUfvuus3bH8+xLPrhM1cTjwpzw==", + "license": "BSD-3-Clause", "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@types/react": "17 || 18 || 19" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, - "node_modules/@gerrit0/mini-shiki": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.3.0.tgz", - "integrity": "sha512-frvArO0+s5Viq68uSod5SieLPVM2cLpXoQ1e07lURwgADXpL/MOypM7jPz9otks0g2DIe2YedDAeVrDyYJZRxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/engine-oniguruma": "^3.3.0", - "@shikijs/langs": "^3.3.0", - "@shikijs/themes": "^3.3.0", - "@shikijs/types": "^3.3.0", - "@shikijs/vscode-textmate": "^10.0.2" - } - }, - "node_modules/@headlessui/react": { - "version": "1.7.19", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", - "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "license": "MIT", "dependencies": { - "@tanstack/react-virtual": "^3.0.0-beta.60", - "client-only": "^0.0.1" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^16 || ^17 || ^18", - "react-dom": "^16 || ^17 || ^18" + "node": ">= 8" } }, - "node_modules/@heroicons/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", - "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "license": "MIT", - "peerDependencies": { - "react": ">= 16 || ^19.0.0-rc" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", "engines": { - "node": ">=18.18.0" + "node": ">= 8" } }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": ">=18.18.0" + "node": ">= 8" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "node_modules/@orval/angular": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/angular/-/angular-7.9.0.tgz", + "integrity": "sha512-GzgEdZxK/9wQMLN2bziTlPSD9bkRXwYf1PoUM+RTXj6MGw0aZVWNTMCnp3dFWp9VemThP0kK2geBFqhxC2Bgxg==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@orval/axios": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/axios/-/axios-7.9.0.tgz", + "integrity": "sha512-e77WvQGfFTkkrJIH66v/DpKdZ1eQBlu4NxOt2gCxvBYFP2dxDR2ajsM7uXxdGxi0iqZIS92+opzhxLIo6TVyDQ==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "node_modules/@orval/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/core/-/core-7.9.0.tgz", + "integrity": "sha512-/Nn6/ARmpevAY7Vl9RRXY2WkJx/q0LIUEE2Eh15bGgzAQIYUcD9aFr9zM5hX2b3lR/fZ8721hFsq0vM9O5ZzXw==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.1", + "@ibm-cloud/openapi-ruleset": "^1.29.4", + "acorn": "^8.14.1", + "ajv": "^8.17.1", + "chalk": "^4.1.2", + "compare-versions": "^6.1.1", + "debug": "^4.4.0", + "esbuild": "^0.25.1", + "esutils": "2.0.3", + "fs-extra": "^11.3.0", + "globby": "11.1.0", + "lodash.isempty": "^4.4.0", + "lodash.uniq": "^4.5.0", + "lodash.uniqby": "^4.7.0", + "lodash.uniqwith": "^4.5.0", + "micromatch": "^4.0.8", + "openapi3-ts": "4.4.0", + "swagger2openapi": "^7.0.8" } }, - "node_modules/@ibm-cloud/openapi-ruleset": { - "version": "1.31.0", - "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset/-/openapi-ruleset-1.31.0.tgz", - "integrity": "sha512-J2bkqFCA89VVkRO0alC0LaSkCZhtntndFNthWOxFyh7wBfazEcvLOWJFIqc87exL40DcgEUJTt/sXkL5ovW+Hg==", + "node_modules/@orval/core/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@ibm-cloud/openapi-ruleset-utilities": "1.9.0", - "@stoplight/spectral-formats": "^1.8.2", - "@stoplight/spectral-functions": "^1.9.3", - "@stoplight/spectral-rulesets": "^1.21.3", - "chalk": "^4.1.2", - "jsonschema": "^1.5.0", - "lodash": "^4.17.21", - "loglevel": "^1.9.2", - "loglevel-plugin-prefix": "0.8.4", - "minimatch": "^6.2.0", - "validator": "^13.11.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "engines": { - "node": ">=16.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@ibm-cloud/openapi-ruleset-utilities": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset-utilities/-/openapi-ruleset-utilities-1.9.0.tgz", - "integrity": "sha512-AoFbSarOqFBYH+1TZ9Ahkm2IWYSi5v0pBk88fpV+5b3qGJukypX8PwvCWADjuyIccKg48/F73a6hTTkBzDQ2UA==", + "node_modules/@orval/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.0.0" - } + "license": "MIT" }, - "node_modules/@ibm-cloud/openapi-ruleset/node_modules/minimatch": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", - "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", + "node_modules/@orval/core/node_modules/openapi3-ts": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", + "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "yaml": "^2.5.0" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", + "node_modules/@orval/fetch": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-7.9.0.tgz", + "integrity": "sha512-gIw2a3jXd1If/NpewVq7C6XDfnG2RPMt4PKR/RtEBeDKasXkoJeS2DBvZp/TyC+lt9oMgketF3bmzo/st09uhA==", + "dev": true, + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" + "@orval/core": "7.9.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@orval/hono": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/hono/-/hono-7.9.0.tgz", + "integrity": "sha512-80VoS5W4I0uUo7Y6sIxr0xGYNX3oIL8sKWru+mYIZdp2L4W1lVHBi4zkpk7u0u9Obv7vmAuPtozV5+QIV0zWBg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@orval/core": "7.9.0", + "@orval/zod": "7.9.0", + "lodash.uniq": "^4.5.0" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "node_modules/@orval/mcp": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/mcp/-/mcp-7.9.0.tgz", + "integrity": "sha512-zMtW4jUKXGiXyJUylVy58kCu/Jf1yF9wp3ul2Guy1vbjlhVeOO1ugCYQ1sYNYH10vN0ajTS0/2pXhTuCR7PxHw==", "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "@orval/core": "7.9.0", + "@orval/zod": "7.9.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@orval/mock": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/mock/-/mock-7.9.0.tgz", + "integrity": "sha512-Ixhb+I4VTIfUl0qxDq8LekBnXM2gpD4kS7OFVqX9rdx0ZwZl7y4xArnKSXk5qgDPjo4eOWSmVA3onuL2WCit/g==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@orval/core": "7.9.0", + "openapi3-ts": "^4.2.2" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@orval/query": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/query/-/query-7.9.0.tgz", + "integrity": "sha512-IPKP4l00dZw0AOt+7PPB82WNdmpPThsWlYvk4YmdLEZVWCuaUaJ9KyLTT2R+XqoksuuuKTJ/IK0KO7VcjMiHiA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@orval/core": "7.9.0", + "@orval/fetch": "7.9.0", + "lodash.omitby": "^4.6.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "node_modules/@orval/swr": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/swr/-/swr-7.9.0.tgz", + "integrity": "sha512-f06MifzMPrnXYdgt2rLLnurJ0YlXexSMyVlXAHhaJENtSVM4zlJp69rA6OULLr1i1biNGTWHSonOizKOf+cNcw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "@orval/core": "7.9.0", + "@orval/fetch": "7.9.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "node_modules/@orval/zod": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/zod/-/zod-7.9.0.tgz", + "integrity": "sha512-LkkofL+iSswsBVWCr3bPZ6un8065wB7x34DZVnF/gN3kBxy8A35I9X0Eld4EajI/1rbMmxzt7wHUYAf5NlJWHQ==", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@orval/core": "7.9.0", + "lodash.uniq": "^4.5.0" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jsep-plugin/assignment": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", - "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", - "dev": true, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "license": "MIT", + "optional": true, "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" + "node": ">=14" } }, - "node_modules/@jsep-plugin/regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", - "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@jsep-plugin/ternary": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", - "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", - "dev": true, + "node_modules/@radix-ui/number": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" + "dependencies": { + "@babel/runtime": "^7.13.10" } }, - "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, - "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz", + "integrity": "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "node_modules/@radix-ui/react-collection": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", + "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@lezer/python": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", - "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lit/react": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.7.tgz", - "integrity": "sha512-cencnwwLXQKiKxjfFzSgZRngcWJzUDZi/04E0fSaF86wZgchMdvTyu+lE36DrUfvuus3bH8+xLPrhM1cTjwpzw==", - "license": "BSD-3-Clause", "peerDependencies": { - "@types/react": "17 || 18 || 19" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "engines": { - "node": ">= 8" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@orval/angular": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/angular/-/angular-7.9.0.tgz", - "integrity": "sha512-GzgEdZxK/9wQMLN2bziTlPSD9bkRXwYf1PoUM+RTXj6MGw0aZVWNTMCnp3dFWp9VemThP0kK2geBFqhxC2Bgxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0" - } - }, - "node_modules/@orval/axios": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/axios/-/axios-7.9.0.tgz", - "integrity": "sha512-e77WvQGfFTkkrJIH66v/DpKdZ1eQBlu4NxOt2gCxvBYFP2dxDR2ajsM7uXxdGxi0iqZIS92+opzhxLIo6TVyDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0" - } - }, - "node_modules/@orval/core": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/core/-/core-7.9.0.tgz", - "integrity": "sha512-/Nn6/ARmpevAY7Vl9RRXY2WkJx/q0LIUEE2Eh15bGgzAQIYUcD9aFr9zM5hX2b3lR/fZ8721hFsq0vM9O5ZzXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "^10.1.1", - "@ibm-cloud/openapi-ruleset": "^1.29.4", - "acorn": "^8.14.1", - "ajv": "^8.17.1", - "chalk": "^4.1.2", - "compare-versions": "^6.1.1", - "debug": "^4.4.0", - "esbuild": "^0.25.1", - "esutils": "2.0.3", - "fs-extra": "^11.3.0", - "globby": "11.1.0", - "lodash.isempty": "^4.4.0", - "lodash.uniq": "^4.5.0", - "lodash.uniqby": "^4.7.0", - "lodash.uniqwith": "^4.5.0", - "micromatch": "^4.0.8", - "openapi3-ts": "4.4.0", - "swagger2openapi": "^7.0.8" - } - }, - "node_modules/@orval/core/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@orval/core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@orval/core/node_modules/openapi3-ts": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", - "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", - "dev": true, - "license": "MIT", - "dependencies": { - "yaml": "^2.5.0" - } - }, - "node_modules/@orval/fetch": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-7.9.0.tgz", - "integrity": "sha512-gIw2a3jXd1If/NpewVq7C6XDfnG2RPMt4PKR/RtEBeDKasXkoJeS2DBvZp/TyC+lt9oMgketF3bmzo/st09uhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0" - } - }, - "node_modules/@orval/hono": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/hono/-/hono-7.9.0.tgz", - "integrity": "sha512-80VoS5W4I0uUo7Y6sIxr0xGYNX3oIL8sKWru+mYIZdp2L4W1lVHBi4zkpk7u0u9Obv7vmAuPtozV5+QIV0zWBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "@orval/zod": "7.9.0", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/@orval/mcp": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/mcp/-/mcp-7.9.0.tgz", - "integrity": "sha512-zMtW4jUKXGiXyJUylVy58kCu/Jf1yF9wp3ul2Guy1vbjlhVeOO1ugCYQ1sYNYH10vN0ajTS0/2pXhTuCR7PxHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "@orval/zod": "7.9.0" - } - }, - "node_modules/@orval/mock": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/mock/-/mock-7.9.0.tgz", - "integrity": "sha512-Ixhb+I4VTIfUl0qxDq8LekBnXM2gpD4kS7OFVqX9rdx0ZwZl7y4xArnKSXk5qgDPjo4eOWSmVA3onuL2WCit/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "openapi3-ts": "^4.2.2" - } - }, - "node_modules/@orval/query": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/query/-/query-7.9.0.tgz", - "integrity": "sha512-IPKP4l00dZw0AOt+7PPB82WNdmpPThsWlYvk4YmdLEZVWCuaUaJ9KyLTT2R+XqoksuuuKTJ/IK0KO7VcjMiHiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "@orval/fetch": "7.9.0", - "lodash.omitby": "^4.6.0" - } - }, - "node_modules/@orval/swr": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/swr/-/swr-7.9.0.tgz", - "integrity": "sha512-f06MifzMPrnXYdgt2rLLnurJ0YlXexSMyVlXAHhaJENtSVM4zlJp69rA6OULLr1i1biNGTWHSonOizKOf+cNcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "@orval/fetch": "7.9.0" - } - }, - "node_modules/@orval/zod": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/zod/-/zod-7.9.0.tgz", - "integrity": "sha512-LkkofL+iSswsBVWCr3bPZ6un8065wB7x34DZVnF/gN3kBxy8A35I9X0Eld4EajI/1rbMmxzt7wHUYAf5NlJWHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@playwright/test": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", - "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.52.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@radix-ui/number": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", - "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz", - "integrity": "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==", + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.11.tgz", + "integrity": "sha512-+gQXta3KxghZ/UDjeAQuCmeeRtYqGc4rT4EHCEnxEzT7RWasye2x9d8tSpIZxhzh123vCqEEktgIbrtZScirBg==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.0" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.11", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -1745,16 +1408,32 @@ } } }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", - "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.7.tgz", + "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==", "license": "MIT", "dependencies": { + "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-slot": "1.2.0" + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1771,110 +1450,10 @@ } } }, - "node_modules/@radix-ui/react-compose-refs": { + "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.11.tgz", - "integrity": "sha512-+gQXta3KxghZ/UDjeAQuCmeeRtYqGc4rT4EHCEnxEzT7RWasye2x9d8tSpIZxhzh123vCqEEktgIbrtZScirBg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-menu": "2.1.11", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.7.tgz", - "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2975,118 +2554,391 @@ "node": ">=14.0.0" } }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.3.0.tgz", - "integrity": "sha512-l0vIw+GxeNU7uGnsu6B+Crpeqf+WTQ2Va71cHb5ZYWEVEPdfYwY5kXwYqRJwHrxz9WH+pjSpXQz+TJgAsrkA5A==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", + "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@shikijs/types": "3.3.0", - "@shikijs/vscode-textmate": "^10.0.2" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@shikijs/langs": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.3.0.tgz", - "integrity": "sha512-zt6Kf/7XpBQKSI9eqku+arLkAcDQ3NHJO6zFjiChI8w0Oz6Jjjay7pToottjQGjSDCFk++R85643WbyINcuL+g==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", + "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@shikijs/types": "3.3.0" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@shikijs/themes": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.3.0.tgz", - "integrity": "sha512-tXeCvLXBnqq34B0YZUEaAD1lD4lmN6TOHAhnHacj4Owh7Ptb/rf5XCDeROZt2rEOk5yuka3OOW2zLqClV7/SOg==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", + "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@shikijs/types": "3.3.0" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@shikijs/types": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.3.0.tgz", - "integrity": "sha512-KPCGnHG6k06QG/2pnYGbFtFvpVJmC3uIpXrAiPrawETifujPBv0Se2oUxm5qYgjCvGJS9InKvjytOdN+bGuX+Q==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", + "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", + "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", + "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@stoplight/json": { - "version": "3.21.7", - "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", - "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", + "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/ordered-object-literal": "^1.0.3", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^13.6.0", - "jsonc-parser": "~2.2.1", - "lodash": "^4.17.21", - "safe-stable-stringify": "^1.1" - }, - "engines": { - "node": ">=8.3.0" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@stoplight/json-ref-readers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@stoplight/json-ref-readers/-/json-ref-readers-1.2.2.tgz", - "integrity": "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", + "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-fetch": "^2.6.0", - "tslib": "^1.14.1" - }, - "engines": { - "node": ">=8.3.0" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", + "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "0BSD" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@stoplight/json-ref-resolver": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz", - "integrity": "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", + "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "^3.21.0", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^12.3.0 || ^13.0.0", - "@types/urijs": "^1.19.19", - "dependency-graph": "~0.11.0", - "fast-memoize": "^2.5.2", - "immer": "^9.0.6", - "lodash": "^4.17.21", - "tslib": "^2.6.0", - "urijs": "^1.19.11" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", + "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", + "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", + "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", + "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", + "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", + "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", + "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", + "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", + "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", + "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.3.0.tgz", + "integrity": "sha512-l0vIw+GxeNU7uGnsu6B+Crpeqf+WTQ2Va71cHb5ZYWEVEPdfYwY5kXwYqRJwHrxz9WH+pjSpXQz+TJgAsrkA5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.3.0.tgz", + "integrity": "sha512-zt6Kf/7XpBQKSI9eqku+arLkAcDQ3NHJO6zFjiChI8w0Oz6Jjjay7pToottjQGjSDCFk++R85643WbyINcuL+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.3.0.tgz", + "integrity": "sha512-tXeCvLXBnqq34B0YZUEaAD1lD4lmN6TOHAhnHacj4Owh7Ptb/rf5XCDeROZt2rEOk5yuka3OOW2zLqClV7/SOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.3.0.tgz", + "integrity": "sha512-KPCGnHG6k06QG/2pnYGbFtFvpVJmC3uIpXrAiPrawETifujPBv0Se2oUxm5qYgjCvGJS9InKvjytOdN+bGuX+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/json": { + "version": "3.21.7", + "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", + "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.3", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "jsonc-parser": "~2.2.1", + "lodash": "^4.17.21", + "safe-stable-stringify": "^1.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-readers/-/json-ref-readers-1.2.2.tgz", + "integrity": "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-fetch": "^2.6.0", + "tslib": "^1.14.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stoplight/json-ref-resolver": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz", + "integrity": "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.21.0", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^12.3.0 || ^13.0.0", + "@types/urijs": "^1.19.19", + "dependency-graph": "~0.11.0", + "fast-memoize": "^2.5.2", + "immer": "^9.0.6", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "urijs": "^1.19.11" }, "engines": { "node": ">=8.3.0" @@ -3169,20 +3021,6 @@ "ajv": ">=8" } }, - "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", - "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, "node_modules/@stoplight/spectral-core/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -3481,9 +3319,9 @@ } }, "node_modules/@stoplight/types": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", - "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", + "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3532,9 +3370,9 @@ } }, "node_modules/@swc/core": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.21.tgz", - "integrity": "sha512-/Y3BJLcwd40pExmdar8MH2UGGvCBrqNN7hauOMckrEX2Ivcbv3IMhrbGX4od1dnF880Ed8y/E9aStZCIQi0EGw==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", + "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -3550,16 +3388,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.21", - "@swc/core-darwin-x64": "1.11.21", - "@swc/core-linux-arm-gnueabihf": "1.11.21", - "@swc/core-linux-arm64-gnu": "1.11.21", - "@swc/core-linux-arm64-musl": "1.11.21", - "@swc/core-linux-x64-gnu": "1.11.21", - "@swc/core-linux-x64-musl": "1.11.21", - "@swc/core-win32-arm64-msvc": "1.11.21", - "@swc/core-win32-ia32-msvc": "1.11.21", - "@swc/core-win32-x64-msvc": "1.11.21" + "@swc/core-darwin-arm64": "1.11.24", + "@swc/core-darwin-x64": "1.11.24", + "@swc/core-linux-arm-gnueabihf": "1.11.24", + "@swc/core-linux-arm64-gnu": "1.11.24", + "@swc/core-linux-arm64-musl": "1.11.24", + "@swc/core-linux-x64-gnu": "1.11.24", + "@swc/core-linux-x64-musl": "1.11.24", + "@swc/core-win32-arm64-msvc": "1.11.24", + "@swc/core-win32-ia32-msvc": "1.11.24", + "@swc/core-win32-x64-msvc": "1.11.24" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -3571,9 +3409,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.21.tgz", - "integrity": "sha512-v6gjw9YFWvKulCw3ZA1dY+LGMafYzJksm1mD4UZFZ9b36CyHFowYVYug1ajYRIRqEvvfIhHUNV660zTLoVFR8g==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", + "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", "cpu": [ "arm64" ], @@ -3588,9 +3426,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.21.tgz", - "integrity": "sha512-CUiTiqKlzskwswrx9Ve5NhNoab30L1/ScOfQwr1duvNlFvarC8fvQSgdtpw2Zh3MfnfNPpyLZnYg7ah4kbT9JQ==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", + "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", "cpu": [ "x64" ], @@ -3605,9 +3443,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.21.tgz", - "integrity": "sha512-YyBTAFM/QPqt1PscD8hDmCLnqPGKmUZpqeE25HXY8OLjl2MUs8+O4KjwPZZ+OGxpdTbwuWFyMoxjcLy80JODvg==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", + "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", "cpu": [ "arm" ], @@ -3622,9 +3460,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.21.tgz", - "integrity": "sha512-DQD+ooJmwpNsh4acrftdkuwl5LNxxg8U4+C/RJNDd7m5FP9Wo4c0URi5U0a9Vk/6sQNh9aSGcYChDpqCDWEcBw==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", + "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", "cpu": [ "arm64" ], @@ -3639,9 +3477,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.21.tgz", - "integrity": "sha512-y1L49+snt1a1gLTYPY641slqy55QotPdtRK9Y6jMi4JBQyZwxC8swWYlQWb+MyILwxA614fi62SCNZNznB3XSA==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", + "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", "cpu": [ "arm64" ], @@ -3656,9 +3494,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.21.tgz", - "integrity": "sha512-NesdBXv4CvVEaFUlqKj+GA4jJMNUzK2NtKOrUNEtTbXaVyNiXjFCSaDajMTedEB0jTAd9ybB0aBvwhgkJUWkWA==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", + "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", "cpu": [ "x64" ], @@ -3673,9 +3511,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.21.tgz", - "integrity": "sha512-qFV60pwpKVOdmX67wqQzgtSrUGWX9Cibnp1CXyqZ9Mmt8UyYGvmGu7p6PMbTyX7vdpVUvWVRf8DzrW2//wmVHg==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", + "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", "cpu": [ "x64" ], @@ -3690,9 +3528,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.21.tgz", - "integrity": "sha512-DJJe9k6gXR/15ZZVLv1SKhXkFst8lYCeZRNHH99SlBodvu4slhh/MKQ6YCixINRhCwliHrpXPym8/5fOq8b7Ig==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", + "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", "cpu": [ "arm64" ], @@ -3707,9 +3545,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.21.tgz", - "integrity": "sha512-TqEXuy6wedId7bMwLIr9byds+mKsaXVHctTN88R1UIBPwJA92Pdk0uxDgip0pEFzHB/ugU27g6d8cwUH3h2eIw==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", + "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", "cpu": [ "ia32" ], @@ -3724,9 +3562,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.21.tgz", - "integrity": "sha512-BT9BNNbMxdpUM1PPAkYtviaV0A8QcXttjs2MDtOeSqqvSJaPtyM+Fof2/+xSwQDmDEFzbGCcn75M5+xy3lGqpA==", + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", + "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", "cpu": [ "x64" ], @@ -3988,23 +3826,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/chai": { - "version": "4.3.20", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", - "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai-subset": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz", - "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/chai": "<5.2.0" - } - }, "node_modules/@types/command-line-args": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.0.tgz", @@ -4749,177 +4570,118 @@ } }, "node_modules/@vitest/expect": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", - "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz", + "integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "chai": "^4.3.10" + "@vitest/spy": "3.1.2", + "@vitest/utils": "3.1.2", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", - "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "node_modules/@vitest/mocker": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz", + "integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "0.34.6", - "p-limit": "^4.0.0", - "pathe": "^1.1.1" + "@vitest/spy": "3.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" }, "funding": { "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "node_modules/@vitest/pretty-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", + "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", "dev": true, "license": "MIT", "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "tinyrainbow": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "node_modules/@vitest/runner": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz", + "integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.20" + "dependencies": { + "@vitest/utils": "3.1.2", + "pathe": "^2.0.3" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", - "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz", + "integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==", "dev": true, "license": "MIT", "dependencies": { - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "pretty-format": "^29.5.0" + "@vitest/pretty-format": "3.1.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/snapshot/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/spy": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", - "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz", + "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^2.1.1" + "tinyspy": "^3.0.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", - "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz", + "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==", "dev": true, "license": "MIT", "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" + "@vitest/pretty-format": "3.1.2", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/@vscode/python-extension": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@vscode/python-extension/-/python-extension-1.0.5.tgz", @@ -5032,34 +4794,6 @@ "@vscode/vsce-sign-win32-x64": "2.0.2" } }, - "node_modules/@vscode/vsce-sign-alpine-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", - "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "alpine" - ] - }, - "node_modules/@vscode/vsce-sign-alpine-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", - "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "alpine" - ] - }, "node_modules/@vscode/vsce-sign-darwin-arm64": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", @@ -5074,90 +4808,6 @@ "darwin" ] }, - "node_modules/@vscode/vsce-sign-darwin-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", - "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@vscode/vsce-sign-linux-arm": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", - "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@vscode/vsce-sign-linux-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", - "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@vscode/vsce-sign-linux-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", - "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@vscode/vsce-sign-win32-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", - "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vscode/vsce-sign-win32-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", - "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@vscode/vsce/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -5579,19 +5229,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -5829,13 +5466,13 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/astring": { @@ -6301,22 +5938,20 @@ } }, "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -6403,16 +6038,13 @@ } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/cheerio": { @@ -6758,13 +6390,6 @@ "dev": true, "license": "MIT" }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -7119,14 +6744,11 @@ } }, "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -7342,16 +6964,6 @@ "node": ">=0.3.1" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -7733,8 +7345,7 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -8072,6 +7683,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -8145,6 +7766,16 @@ "node": ">=6" } }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -8496,16 +8127,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -10210,19 +9831,6 @@ "node": ">=6.11.5" } }, - "node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -10410,14 +10018,11 @@ } }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } + "license": "MIT" }, "node_modules/lru-cache": { "version": "6.0.0", @@ -11260,26 +10865,6 @@ "license": "MIT", "optional": true }, - "node_modules/mlly": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", - "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.14.0", - "pathe": "^2.0.1", - "pkg-types": "^1.3.0", - "ufo": "^1.5.4" - } - }, - "node_modules/mlly/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/mocha": { "version": "10.8.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", @@ -12398,20 +11983,20 @@ } }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "license": "MIT", "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/pend": { @@ -12457,25 +12042,6 @@ "node": ">= 6" } }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/pkg-types/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/playwright": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", @@ -13428,19 +12994,42 @@ } }, "node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", + "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", "dev": true, "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.1", + "@rollup/rollup-android-arm64": "4.40.1", + "@rollup/rollup-darwin-arm64": "4.40.1", + "@rollup/rollup-darwin-x64": "4.40.1", + "@rollup/rollup-freebsd-arm64": "4.40.1", + "@rollup/rollup-freebsd-x64": "4.40.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", + "@rollup/rollup-linux-arm-musleabihf": "4.40.1", + "@rollup/rollup-linux-arm64-gnu": "4.40.1", + "@rollup/rollup-linux-arm64-musl": "4.40.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", + "@rollup/rollup-linux-riscv64-gnu": "4.40.1", + "@rollup/rollup-linux-riscv64-musl": "4.40.1", + "@rollup/rollup-linux-s390x-gnu": "4.40.1", + "@rollup/rollup-linux-x64-gnu": "4.40.1", + "@rollup/rollup-linux-x64-musl": "4.40.1", + "@rollup/rollup-win32-arm64-msvc": "4.40.1", + "@rollup/rollup-win32-ia32-msvc": "4.40.1", + "@rollup/rollup-win32-x64-msvc": "4.40.1", "fsevents": "~2.3.2" } }, @@ -14324,19 +13913,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/style-mod": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", @@ -14772,20 +14348,82 @@ "dev": true, "license": "MIT" }, - "node_modules/tinypool": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", - "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, - "license": "MIT", - "engines": { + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, "license": "MIT", "engines": { @@ -14981,16 +14619,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -15171,13 +14799,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -15426,670 +15047,312 @@ } } }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utility-types": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/validator": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", - "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "4.5.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.13.tgz", - "integrity": "sha512-Hgp8IF/yZDzKsN1hQWOuQZbrKiaFsbQud+07jJ8h9m9PaHWkpvZ5u55Xw5yYjWRXwRQ4jwFlJvY7T7FUJG9MCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", - "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "mlly": "^1.4.0", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-plugin-css-injected-by-js": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.2.tgz", - "integrity": "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "vite": ">2.0.0-0" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 4" } }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "bin": { + "uuid": "dist/bin/uuid" } }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10.12.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], + "node_modules/validator": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 0.10" } }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], + "node_modules/vite": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", + "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=12" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], + "node_modules/vite-node": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz", + "integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, "engines": { - "node": ">=12" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], + "node_modules/vite-plugin-css-injected-by-js": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.2.tgz", + "integrity": "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "peerDependencies": { + "vite": ">2.0.0-0" } }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { - "node": ">=12" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { "node": ">=12" }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/vitest": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", - "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz", + "integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^4.3.5", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.34.6", - "@vitest/runner": "0.34.6", - "@vitest/snapshot": "0.34.6", - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "acorn": "^8.9.0", - "acorn-walk": "^8.2.0", - "cac": "^6.7.14", - "chai": "^4.3.10", - "debug": "^4.3.4", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.3.3", - "strip-literal": "^1.0.1", - "tinybench": "^2.5.0", - "tinypool": "^0.7.0", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", - "vite-node": "0.34.6", - "why-is-node-running": "^2.2.2" + "@vitest/expect": "3.1.2", + "@vitest/mocker": "3.1.2", + "@vitest/pretty-format": "^3.1.2", + "@vitest/runner": "3.1.2", + "@vitest/snapshot": "3.1.2", + "@vitest/spy": "3.1.2", + "@vitest/utils": "3.1.2", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.13", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.1.2", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": ">=v14.18.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@vitest/browser": "*", - "@vitest/ui": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.1.2", + "@vitest/ui": "3.1.2", "happy-dom": "*", - "jsdom": "*", - "playwright": "*", - "safaridriver": "*", - "webdriverio": "*" + "jsdom": "*" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { + "@types/debug": { "optional": true }, - "happy-dom": { + "@types/node": { "optional": true }, - "jsdom": { + "@vitest/browser": { "optional": true }, - "playwright": { + "@vitest/ui": { "optional": true }, - "safaridriver": { + "happy-dom": { "optional": true }, - "webdriverio": { + "jsdom": { "optional": true } } @@ -16894,6 +16157,7 @@ "devDependencies": { "@eslint/js": "^9.25.1", "@playwright/test": "^1.37.1", + "@swc/core": "^1.11.24", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", @@ -16901,7 +16165,7 @@ "@types/pluralize": "^0.0.30", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react-swc": "^3.3.2", + "@vitejs/plugin-react-swc": "^3.9.0", "autoprefixer": "^10.4.15", "jsdom": "^22.1.0", "orval": "^7.9.0", @@ -16910,9 +16174,9 @@ "tailwindcss": "^3.3.3", "typescript": "^5.8.3", "typescript-eslint": "^8.31.1", - "vite": "^4.4.9", + "vite": "^6.3.4", "vite-plugin-css-injected-by-js": "^3.5.2", - "vitest": "^0.34.3" + "vitest": "^3.1.2" } } } diff --git a/web/client/package.json b/web/client/package.json index 080f06282c..f8525f72e6 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -55,6 +55,7 @@ "devDependencies": { "@eslint/js": "^9.25.1", "@playwright/test": "^1.37.1", + "@swc/core": "^1.11.24", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", @@ -62,7 +63,7 @@ "@types/pluralize": "^0.0.30", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react-swc": "^3.3.2", + "@vitejs/plugin-react-swc": "^3.9.0", "autoprefixer": "^10.4.15", "jsdom": "^22.1.0", "orval": "^7.9.0", @@ -71,8 +72,8 @@ "tailwindcss": "^3.3.3", "typescript": "^5.8.3", "typescript-eslint": "^8.31.1", - "vite": "^4.4.9", + "vite": "^6.3.4", "vite-plugin-css-injected-by-js": "^3.5.2", - "vitest": "^0.34.3" + "vitest": "^3.1.2" } } diff --git a/web/docker-compose.yml b/web/docker-compose.yml index c8f7e352ad..0474a152f4 100644 --- a/web/docker-compose.yml +++ b/web/docker-compose.yml @@ -23,6 +23,7 @@ services: - 8001:8001 volumes: - ../web/client:/app/web/client + - /app/web/client/node_modules tty: true networks: - tobiko-development From 694d94478dc0b3df2a12359a366eb1ce38a528e5 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:59:50 +0100 Subject: [PATCH 0082/1056] chore(vscode): centralise formatting code (#4272) --- .prettierignore | 39 + web/client/.prettierrc.cjs => .prettierrc.cjs | 0 package-lock.json | 29908 +++++++--------- package.json | 21 +- tooling/vscode/extensions.json | 6 +- tooling/vscode/launch.json | 30 +- tooling/vscode/settings.json | 28 +- tooling/vscode/tasks.json | 99 +- vscode/extension/.vscode-test.mjs | 6 +- vscode/extension/esbuild.js | 46 +- vscode/extension/eslint.config.mjs | 44 +- vscode/extension/src/auth/auth.ts | 114 +- vscode/extension/src/commands/format.ts | 26 +- vscode/extension/src/commands/signin.ts | 8 +- .../src/commands/signinSpecifyFlow.ts | 52 +- vscode/extension/src/commands/signout.ts | 11 +- vscode/extension/src/extension.ts | 60 +- vscode/extension/src/lsp/lsp.ts | 160 +- vscode/extension/src/test/extension.test.ts | 10 +- .../src/utilities/common/constants.ts | 21 +- vscode/extension/src/utilities/common/log.ts | 58 +- .../extension/src/utilities/common/python.ts | 154 +- .../src/utilities/common/settings.ts | 204 +- .../src/utilities/common/utilities.ts | 105 +- .../src/utilities/common/vscodeapi.ts | 49 +- vscode/extension/src/utilities/errors.ts | 22 +- vscode/extension/src/utilities/exec.ts | 34 +- .../src/utilities/functional/result.ts | 18 +- .../extension/src/utilities/isCodespaces.ts | 9 +- vscode/extension/src/utilities/python.ts | 27 +- .../src/utilities/sqlmesh/sqlmesh.ts | 110 +- vscode/extension/tsconfig.json | 44 +- web/client/.eslintrc.cjs | 57 + web/client/.prettierignore | 13 - web/client/eslint.config.mjs | 4 +- web/client/package.json | 12 +- web/client/public/css/design.css | 3 +- .../src/library/components/editor/hooks.ts | 4 +- .../components/fileExplorer/FileExplorer.tsx | 8 +- .../library/components/graph/ModelColumns.tsx | 18 +- .../library/components/graph/ModelNode.tsx | 12 +- .../src/library/components/graph/help.ts | 14 +- .../library/components/plan/PlanActions.tsx | 4 +- .../components/plan/PlanApplyStageTracker.tsx | 10 +- .../components/sourceList/SourceListItem.tsx | 4 +- .../src/library/components/table/Table.tsx | 2 +- .../components/tableDiff/TableDiff.tsx | 24 +- web/docker-compose.build.yml | 5 +- web/docker-compose.yml | 7 +- 49 files changed, 14731 insertions(+), 16993 deletions(-) create mode 100644 .prettierignore rename web/client/.prettierrc.cjs => .prettierrc.cjs (100%) create mode 100644 web/client/.eslintrc.cjs delete mode 100644 web/client/.prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..1504efe92f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,39 @@ +web/client/**/*.py +web/client/.prettierignore +web/client/.gitignore +web/client/node_modules/ +web/client/test-results/ +web/client/playwright-report/ +web/client/playwright/.cache/ +web/client/dist +web/client/public/favicons/ +web/client/public/fonts/ +web/client/src/styles/fonts/ +web/client/src/assets/fonts/ +web/client/tsconfig.tsbuildinfo +web/client/src/utils/tbk-components.js + +node_modules/ +vscode/extension/node_modules/ +vscode/extension/dist +vscode/extension/out +vscode/extension/tsconfig.tsbuildinfo +vscode/extension/.vscode-test/ + +sqlmesh +docs +tests +examples +posts +.circleci +README.md +mkdocs.yml +.readthedocs.yaml +.pre-commit-config.yaml +package-lock.json +**/*.md +.ruff_cache +.pytest_cache +.venv +.vscode +build \ No newline at end of file diff --git a/web/client/.prettierrc.cjs b/.prettierrc.cjs similarity index 100% rename from web/client/.prettierrc.cjs rename to .prettierrc.cjs diff --git a/package-lock.json b/package-lock.json index 30a827834d..d482cb090d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16183 +1,13733 @@ { - "name": "sqlmesh", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "workspaces": [ - "vscode/extension", - "web/client" - ] - }, - "node_modules/@75lb/deep-merge": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.2.tgz", - "integrity": "sha512-08K9ou5VNbheZFxM5tDWoqjA3ImC50DiuuJ2tj1yEPRfkp8lLLg6XAaJ4On+a0yAXor/8ay5gHnAIshRM44Kpw==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21", - "typical": "^7.1.1" - }, - "engines": { - "node": ">=12.17" - } - }, - "node_modules/@75lb/deep-merge/node_modules/typical": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", - "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", - "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.7.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", - "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" - } - }, - "node_modules/@apidevtools/openapi-schemas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", - "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@apidevtools/swagger-methods": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", - "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@apidevtools/swagger-parser": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz", - "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "11.7.2", - "@apidevtools/openapi-schemas": "^2.1.0", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "ajv": "^8.17.1", - "ajv-draft-04": "^1.0.0", - "call-me-maybe": "^1.0.2" - }, - "peerDependencies": { - "openapi-types": ">=7" - } - }, - "node_modules/@apidevtools/swagger-parser/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", - "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@asyncapi/specs": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-6.8.1.tgz", - "integrity": "sha512-czHoAk3PeXTLR+X8IUaD+IpT+g+zUvkcgMDJVothBsan+oHN3jfcFcFUNdOPAAFoUCQN1hXF1dWuphWy05THlA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.11" - } - }, - "node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-auth": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", - "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-util": "^1.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-client": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.3.tgz", - "integrity": "sha512-/wGw8fJ4mdpJ1Cum7s1S+VQyXt1ihwKLzfabS1O/RDADnmzVc01dHn44qD0BvGH6KlZNzOMW95tEpKqhkCChPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.4.0", - "@azure/core-rest-pipeline": "^1.9.1", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.6.1", - "@azure/logger": "^1.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-rest-pipeline": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.1.tgz", - "integrity": "sha512-zHeoI3NCs53lLBbWNzQycjnYKsA1CVKlnzSNuSFcUDwBp8HHVObePxrM7HaX+Ha5Ks639H7chNC9HOaIhNS03w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.8.0", - "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.11.0", - "@azure/logger": "^1.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-tracing": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", - "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-util": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", - "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/identity": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.9.1.tgz", - "integrity": "sha512-986D7Cf1AOwYqSDtO/FnMAyk/Jc8qpftkGsxuehoh4F85MhQ4fICBGX/44+X1y78lN4Sqib3Bsoaoh/FvOGgmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.2", - "@azure/core-rest-pipeline": "^1.17.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.11.0", - "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^4.2.0", - "@azure/msal-node": "^3.5.0", - "open": "^10.1.0", - "tslib": "^2.2.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/logger": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz", - "integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/msal-browser": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.11.0.tgz", - "integrity": "sha512-0p5Ut3wORMP+975AKvaSPIO4UytgsfAvJ7RxaTx+nkP+Hpkmm93AuiMkBWKI2x9tApU/SLgIyPz/ZwLYUIWb5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/msal-common": "15.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-common": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.5.1.tgz", - "integrity": "sha512-oxK0khbc4Bg1bKQnqDr7ikULhVL2OHgSrIq0Vlh4b6+hm4r0lr6zPMQE8ZvmacJuh+ZZGKBM5iIObhF1q1QimQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-node": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.1.tgz", - "integrity": "sha512-dkgMYM5B6tI88r/oqf5bYd93WkenQpaWwiszJDk7avVjso8cmuKRTW97dA1RMi6RhihZFLtY1VtWxU9+sW2T5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/msal-common": "15.5.1", - "jsonwebtoken": "^9.0.0", - "uuid": "^8.3.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", - "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@codemirror/autocomplete": { - "version": "6.18.6", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", - "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", - "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/lang-python": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.7.tgz", - "integrity": "sha512-mZnFTsL4lW5p9ch8uKNKeRU3xGGxr1QpESLilfON2E3fQzOa/OygEMkaDvERvXDJWJA9U9oN/D4w0ZuUzNO4+g==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.3.2", - "@codemirror/language": "^6.8.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.2.1", - "@lezer/python": "^1.1.4" - } - }, - "node_modules/@codemirror/lang-sql": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz", - "integrity": "sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz", - "integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.1.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/legacy-modes": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz", - "integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.8.5", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", - "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.35.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.5.10", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz", - "integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", - "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", - "license": "MIT", - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/theme-one-dark": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", - "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/highlight": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.36.5", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.5.tgz", - "integrity": "sha512-cd+FZEUlu3GQCYnguYm3EkhJ8KJVisqqUsCOKedBoAt/d9c76JUUap6U0UrpElln5k6VyrEOYliMuDAKIeDQLg==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.5.0", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", - "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.13.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@exodus/schemasafe": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", - "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@floating-ui/core": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", - "license": "MIT" - }, - "node_modules/@gerrit0/mini-shiki": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.3.0.tgz", - "integrity": "sha512-frvArO0+s5Viq68uSod5SieLPVM2cLpXoQ1e07lURwgADXpL/MOypM7jPz9otks0g2DIe2YedDAeVrDyYJZRxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/engine-oniguruma": "^3.3.0", - "@shikijs/langs": "^3.3.0", - "@shikijs/themes": "^3.3.0", - "@shikijs/types": "^3.3.0", - "@shikijs/vscode-textmate": "^10.0.2" - } - }, - "node_modules/@headlessui/react": { - "version": "1.7.19", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", - "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", - "license": "MIT", - "dependencies": { - "@tanstack/react-virtual": "^3.0.0-beta.60", - "client-only": "^0.0.1" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^16 || ^17 || ^18", - "react-dom": "^16 || ^17 || ^18" - } - }, - "node_modules/@heroicons/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", - "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", - "license": "MIT", - "peerDependencies": { - "react": ">= 16 || ^19.0.0-rc" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@ibm-cloud/openapi-ruleset": { - "version": "1.31.0", - "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset/-/openapi-ruleset-1.31.0.tgz", - "integrity": "sha512-J2bkqFCA89VVkRO0alC0LaSkCZhtntndFNthWOxFyh7wBfazEcvLOWJFIqc87exL40DcgEUJTt/sXkL5ovW+Hg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@ibm-cloud/openapi-ruleset-utilities": "1.9.0", - "@stoplight/spectral-formats": "^1.8.2", - "@stoplight/spectral-functions": "^1.9.3", - "@stoplight/spectral-rulesets": "^1.21.3", - "chalk": "^4.1.2", - "jsonschema": "^1.5.0", - "lodash": "^4.17.21", - "loglevel": "^1.9.2", - "loglevel-plugin-prefix": "0.8.4", - "minimatch": "^6.2.0", - "validator": "^13.11.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ibm-cloud/openapi-ruleset-utilities": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset-utilities/-/openapi-ruleset-utilities-1.9.0.tgz", - "integrity": "sha512-AoFbSarOqFBYH+1TZ9Ahkm2IWYSi5v0pBk88fpV+5b3qGJukypX8PwvCWADjuyIccKg48/F73a6hTTkBzDQ2UA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ibm-cloud/openapi-ruleset/node_modules/minimatch": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", - "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jsep-plugin/assignment": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", - "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@jsep-plugin/regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", - "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@jsep-plugin/ternary": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", - "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", - "license": "MIT" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/python": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", - "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lit/react": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.7.tgz", - "integrity": "sha512-cencnwwLXQKiKxjfFzSgZRngcWJzUDZi/04E0fSaF86wZgchMdvTyu+lE36DrUfvuus3bH8+xLPrhM1cTjwpzw==", - "license": "BSD-3-Clause", - "peerDependencies": { - "@types/react": "17 || 18 || 19" - } - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@orval/angular": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/angular/-/angular-7.9.0.tgz", - "integrity": "sha512-GzgEdZxK/9wQMLN2bziTlPSD9bkRXwYf1PoUM+RTXj6MGw0aZVWNTMCnp3dFWp9VemThP0kK2geBFqhxC2Bgxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0" - } - }, - "node_modules/@orval/axios": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/axios/-/axios-7.9.0.tgz", - "integrity": "sha512-e77WvQGfFTkkrJIH66v/DpKdZ1eQBlu4NxOt2gCxvBYFP2dxDR2ajsM7uXxdGxi0iqZIS92+opzhxLIo6TVyDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0" - } - }, - "node_modules/@orval/core": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/core/-/core-7.9.0.tgz", - "integrity": "sha512-/Nn6/ARmpevAY7Vl9RRXY2WkJx/q0LIUEE2Eh15bGgzAQIYUcD9aFr9zM5hX2b3lR/fZ8721hFsq0vM9O5ZzXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "^10.1.1", - "@ibm-cloud/openapi-ruleset": "^1.29.4", - "acorn": "^8.14.1", - "ajv": "^8.17.1", - "chalk": "^4.1.2", - "compare-versions": "^6.1.1", - "debug": "^4.4.0", - "esbuild": "^0.25.1", - "esutils": "2.0.3", - "fs-extra": "^11.3.0", - "globby": "11.1.0", - "lodash.isempty": "^4.4.0", - "lodash.uniq": "^4.5.0", - "lodash.uniqby": "^4.7.0", - "lodash.uniqwith": "^4.5.0", - "micromatch": "^4.0.8", - "openapi3-ts": "4.4.0", - "swagger2openapi": "^7.0.8" - } - }, - "node_modules/@orval/core/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@orval/core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@orval/core/node_modules/openapi3-ts": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", - "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", - "dev": true, - "license": "MIT", - "dependencies": { - "yaml": "^2.5.0" - } - }, - "node_modules/@orval/fetch": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-7.9.0.tgz", - "integrity": "sha512-gIw2a3jXd1If/NpewVq7C6XDfnG2RPMt4PKR/RtEBeDKasXkoJeS2DBvZp/TyC+lt9oMgketF3bmzo/st09uhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0" - } - }, - "node_modules/@orval/hono": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/hono/-/hono-7.9.0.tgz", - "integrity": "sha512-80VoS5W4I0uUo7Y6sIxr0xGYNX3oIL8sKWru+mYIZdp2L4W1lVHBi4zkpk7u0u9Obv7vmAuPtozV5+QIV0zWBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "@orval/zod": "7.9.0", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/@orval/mcp": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/mcp/-/mcp-7.9.0.tgz", - "integrity": "sha512-zMtW4jUKXGiXyJUylVy58kCu/Jf1yF9wp3ul2Guy1vbjlhVeOO1ugCYQ1sYNYH10vN0ajTS0/2pXhTuCR7PxHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "@orval/zod": "7.9.0" - } - }, - "node_modules/@orval/mock": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/mock/-/mock-7.9.0.tgz", - "integrity": "sha512-Ixhb+I4VTIfUl0qxDq8LekBnXM2gpD4kS7OFVqX9rdx0ZwZl7y4xArnKSXk5qgDPjo4eOWSmVA3onuL2WCit/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "openapi3-ts": "^4.2.2" - } - }, - "node_modules/@orval/query": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/query/-/query-7.9.0.tgz", - "integrity": "sha512-IPKP4l00dZw0AOt+7PPB82WNdmpPThsWlYvk4YmdLEZVWCuaUaJ9KyLTT2R+XqoksuuuKTJ/IK0KO7VcjMiHiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "@orval/fetch": "7.9.0", - "lodash.omitby": "^4.6.0" - } - }, - "node_modules/@orval/swr": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/swr/-/swr-7.9.0.tgz", - "integrity": "sha512-f06MifzMPrnXYdgt2rLLnurJ0YlXexSMyVlXAHhaJENtSVM4zlJp69rA6OULLr1i1biNGTWHSonOizKOf+cNcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "@orval/fetch": "7.9.0" - } - }, - "node_modules/@orval/zod": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@orval/zod/-/zod-7.9.0.tgz", - "integrity": "sha512-LkkofL+iSswsBVWCr3bPZ6un8065wB7x34DZVnF/gN3kBxy8A35I9X0Eld4EajI/1rbMmxzt7wHUYAf5NlJWHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.9.0", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@playwright/test": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", - "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.52.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@radix-ui/number": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", - "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz", - "integrity": "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", - "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-slot": "1.2.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.11.tgz", - "integrity": "sha512-+gQXta3KxghZ/UDjeAQuCmeeRtYqGc4rT4EHCEnxEzT7RWasye2x9d8tSpIZxhzh123vCqEEktgIbrtZScirBg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-menu": "2.1.11", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.7.tgz", - "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.4.tgz", - "integrity": "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.11.tgz", - "integrity": "sha512-sbFI4Qaw02J0ogmR9tOMsSqsdrGNpUanlPYAqTE2JJafow8ecHtykg4fSTjNHBdDl4deiKMK+RhTEwyVhP7UDA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.7", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.4", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.4", - "@radix-ui/react-portal": "1.1.6", - "@radix-ui/react-presence": "1.1.3", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-roving-focus": "1.1.7", - "@radix-ui/react-slot": "1.2.0", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz", - "integrity": "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.6.tgz", - "integrity": "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz", - "integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", - "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.7.tgz", - "integrity": "sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz", - "integrity": "sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.3", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.2", - "@radix-ui/react-portal": "1.0.3", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", - "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", - "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", - "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", - "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", - "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", - "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", - "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", - "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", - "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", - "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", - "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@radix-ui/react-select/node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", - "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", - "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", - "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@react-dnd/asap": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", - "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==", - "license": "MIT" - }, - "node_modules/@react-dnd/invariant": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", - "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==", - "license": "MIT" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", - "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", - "license": "MIT" - }, - "node_modules/@reactflow/background": { - "version": "11.3.14", - "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", - "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/controls": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", - "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/core": { - "version": "11.11.4", - "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", - "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", - "license": "MIT", - "dependencies": { - "@types/d3": "^7.4.0", - "@types/d3-drag": "^3.0.1", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-drag": "^3.0.0", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/minimap": { - "version": "11.7.14", - "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", - "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/node-resizer": { - "version": "2.2.14", - "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", - "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.4", - "d3-drag": "^3.0.0", - "d3-selection": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/node-toolbar": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", - "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.3.0.tgz", - "integrity": "sha512-l0vIw+GxeNU7uGnsu6B+Crpeqf+WTQ2Va71cHb5ZYWEVEPdfYwY5kXwYqRJwHrxz9WH+pjSpXQz+TJgAsrkA5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.3.0", - "@shikijs/vscode-textmate": "^10.0.2" - } - }, - "node_modules/@shikijs/langs": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.3.0.tgz", - "integrity": "sha512-zt6Kf/7XpBQKSI9eqku+arLkAcDQ3NHJO6zFjiChI8w0Oz6Jjjay7pToottjQGjSDCFk++R85643WbyINcuL+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.3.0" - } - }, - "node_modules/@shikijs/themes": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.3.0.tgz", - "integrity": "sha512-tXeCvLXBnqq34B0YZUEaAD1lD4lmN6TOHAhnHacj4Owh7Ptb/rf5XCDeROZt2rEOk5yuka3OOW2zLqClV7/SOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.3.0" - } - }, - "node_modules/@shikijs/types": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.3.0.tgz", - "integrity": "sha512-KPCGnHG6k06QG/2pnYGbFtFvpVJmC3uIpXrAiPrawETifujPBv0Se2oUxm5qYgjCvGJS9InKvjytOdN+bGuX+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/json": { - "version": "3.21.7", - "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", - "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/ordered-object-literal": "^1.0.3", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^13.6.0", - "jsonc-parser": "~2.2.1", - "lodash": "^4.17.21", - "safe-stable-stringify": "^1.1" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/json-ref-readers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@stoplight/json-ref-readers/-/json-ref-readers-1.2.2.tgz", - "integrity": "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-fetch": "^2.6.0", - "tslib": "^1.14.1" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@stoplight/json-ref-resolver": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz", - "integrity": "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "^3.21.0", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^12.3.0 || ^13.0.0", - "@types/urijs": "^1.19.19", - "dependency-graph": "~0.11.0", - "fast-memoize": "^2.5.2", - "immer": "^9.0.6", - "lodash": "^4.17.21", - "tslib": "^2.6.0", - "urijs": "^1.19.11" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/json/node_modules/jsonc-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", - "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/ordered-object-literal": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", - "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/path": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz", - "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/spectral-core": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.20.0.tgz", - "integrity": "sha512-5hBP81nCC1zn1hJXL/uxPNRKNcB+/pEIHgCjPRpl/w/qy9yC9ver04tw1W0l/PMiv0UeB5dYgozXVQ4j5a6QQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "~3.21.0", - "@stoplight/path": "1.3.2", - "@stoplight/spectral-parsers": "^1.0.0", - "@stoplight/spectral-ref-resolver": "^1.0.4", - "@stoplight/spectral-runtime": "^1.1.2", - "@stoplight/types": "~13.6.0", - "@types/es-aggregate-error": "^1.0.2", - "@types/json-schema": "^7.0.11", - "ajv": "^8.17.1", - "ajv-errors": "~3.0.0", - "ajv-formats": "~2.1.1", - "es-aggregate-error": "^1.0.7", - "jsonpath-plus": "^10.3.0", - "lodash": "~4.17.21", - "lodash.topath": "^4.5.2", - "minimatch": "3.1.2", - "nimma": "0.2.3", - "pony-cause": "^1.1.1", - "simple-eval": "1.0.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", - "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/ajv-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.0.1" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/spectral-core/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@stoplight/spectral-formats": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-formats/-/spectral-formats-1.8.2.tgz", - "integrity": "sha512-c06HB+rOKfe7tuxg0IdKDEA5XnjL2vrn/m/OVIIxtINtBzphZrOgtRn7epQ5bQF5SWp84Ue7UJWaGgDwVngMFw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "^3.17.0", - "@stoplight/spectral-core": "^1.19.2", - "@types/json-schema": "^7.0.7", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-functions": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.1.tgz", - "integrity": "sha512-obu8ZfoHxELOapfGsCJixKZXZcffjg+lSoNuttpmUFuDzVLT3VmH8QkPXfOGOL5Pz80BR35ClNAToDkdnYIURg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "^3.17.1", - "@stoplight/spectral-core": "^1.19.4", - "@stoplight/spectral-formats": "^1.8.1", - "@stoplight/spectral-runtime": "^1.1.2", - "ajv": "^8.17.1", - "ajv-draft-04": "~1.0.0", - "ajv-errors": "~3.0.0", - "ajv-formats": "~2.1.1", - "lodash": "~4.17.21", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", - "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv-draft-04": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", - "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.0.1" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/spectral-parsers": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.5.tgz", - "integrity": "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "~3.21.0", - "@stoplight/types": "^14.1.1", - "@stoplight/yaml": "~4.3.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", - "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@stoplight/spectral-ref-resolver": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.5.tgz", - "integrity": "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json-ref-readers": "1.2.2", - "@stoplight/json-ref-resolver": "~3.1.6", - "@stoplight/spectral-runtime": "^1.1.2", - "dependency-graph": "0.11.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-rulesets": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.22.0.tgz", - "integrity": "sha512-l2EY2jiKKLsvnPfGy+pXC0LeGsbJzcQP5G/AojHgf+cwN//VYxW1Wvv4WKFx/CLmLxc42mJYF2juwWofjWYNIQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@asyncapi/specs": "^6.8.0", - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "^3.17.0", - "@stoplight/spectral-core": "^1.19.4", - "@stoplight/spectral-formats": "^1.8.1", - "@stoplight/spectral-functions": "^1.9.1", - "@stoplight/spectral-runtime": "^1.1.2", - "@stoplight/types": "^13.6.0", - "@types/json-schema": "^7.0.7", - "ajv": "^8.17.1", - "ajv-formats": "~2.1.1", - "json-schema-traverse": "^1.0.0", - "leven": "3.1.0", - "lodash": "~4.17.21", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", - "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/spectral-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-runtime/-/spectral-runtime-1.1.4.tgz", - "integrity": "sha512-YHbhX3dqW0do6DhiPSgSGQzr6yQLlWybhKwWx0cqxjMwxej3TqLv3BXMfIUYFKKUqIwH4Q2mV8rrMM8qD2N0rQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "^3.20.1", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^13.6.0", - "abort-controller": "^3.0.0", - "lodash": "^4.17.21", - "node-fetch": "^2.7.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", - "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@stoplight/yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz", - "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/ordered-object-literal": "^1.0.5", - "@stoplight/types": "^14.1.1", - "@stoplight/yaml-ast-parser": "0.0.50", - "tslib": "^2.2.0" - }, - "engines": { - "node": ">=10.8" - } - }, - "node_modules/@stoplight/yaml-ast-parser": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz", - "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@stoplight/yaml/node_modules/@stoplight/types": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", - "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@swc/core": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", - "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.21" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.24", - "@swc/core-darwin-x64": "1.11.24", - "@swc/core-linux-arm-gnueabihf": "1.11.24", - "@swc/core-linux-arm64-gnu": "1.11.24", - "@swc/core-linux-arm64-musl": "1.11.24", - "@swc/core-linux-x64-gnu": "1.11.24", - "@swc/core-linux-x64-musl": "1.11.24", - "@swc/core-win32-arm64-msvc": "1.11.24", - "@swc/core-win32-ia32-msvc": "1.11.24", - "@swc/core-win32-x64-msvc": "1.11.24" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", - "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", - "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", - "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", - "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", - "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", - "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", - "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", - "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", - "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", - "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", - "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" - } - }, - "node_modules/@tailwindcss/container-queries": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", - "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", - "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=3.2.0" - } - }, - "node_modules/@tanstack/query-core": { - "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", - "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", - "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "4.36.1", - "use-sync-external-store": "^1.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/@tanstack/react-table": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", - "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", - "license": "MIT", - "dependencies": { - "@tanstack/table-core": "8.21.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz", - "integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.13.6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/table-core": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", - "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.6", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz", - "integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@testing-library/dom": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", - "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", - "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.21", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/jest-dom/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/react": { - "version": "14.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", - "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^9.0.0", - "@types/react-dom": "^18.0.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/command-line-args": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.0.tgz", - "integrity": "sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA==", - "license": "MIT" - }, - "node_modules/@types/command-line-usage": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.2.tgz", - "integrity": "sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg==", - "license": "MIT" - }, - "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", - "license": "MIT" - }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", - "license": "MIT" - }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", - "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", - "license": "MIT", - "dependencies": { - "@types/d3-dsv": "*" - } - }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", - "license": "MIT" - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/diff": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz", - "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/es-aggregate-error": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", - "integrity": "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/fs-extra": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", - "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", - "license": "MIT", - "dependencies": { - "@types/jsonfile": "*", - "@types/node": "*" - } - }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsonfile": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", - "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mocha": { - "version": "10.0.10", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", - "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.0.tgz", - "integrity": "sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ==", - "license": "MIT" - }, - "node_modules/@types/pad-left": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@types/pad-left/-/pad-left-2.1.1.tgz", - "integrity": "sha512-Xd22WCRBydkGSApl5Bw0PhAOHKSVjNL3E3AwzKaps96IMraPqy5BvZIsBVK6JLwdybUzjHnuWVwpDd0JjTfHXA==", - "license": "MIT" - }, - "node_modules/@types/pluralize": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.30.tgz", - "integrity": "sha512-kVww6xZrW/db5BR9OqiT71J9huRdQ+z/r+LbDuT7/EK50mCmj5FoaIARnVv0rvjUS/YpDox0cDU9lpQT011VBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.20", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", - "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.6.tgz", - "integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@types/urijs": { - "version": "1.19.25", - "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.25.tgz", - "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/vscode": { - "version": "1.96.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.96.0.tgz", - "integrity": "sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", - "integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/type-utils": "8.31.1", - "@typescript-eslint/utils": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz", - "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz", - "integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz", - "integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/utils": "8.31.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz", - "integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz", - "integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz", - "integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz", - "integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.31.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@uidotdev/usehooks": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz", - "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.23.10", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.10.tgz", - "integrity": "sha512-zpbmSeNs3OU/f/Eyd6brFnjsBUYwv2mFjWxlAsIRSwTlW+skIT60rQHFBSfsj/5UVSxSLWVeUYczN7AyXvgTGQ==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@codemirror/autocomplete": ">=6.0.0", - "@codemirror/commands": ">=6.0.0", - "@codemirror/language": ">=6.0.0", - "@codemirror/lint": ">=6.0.0", - "@codemirror/search": ">=6.0.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/view": ">=6.0.0" - } - }, - "node_modules/@uiw/react-codemirror": { - "version": "4.23.10", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.10.tgz", - "integrity": "sha512-AbN4eVHOL4ckRuIXpZxkzEqL/1ChVA+BSdEnAKjIB68pLQvKsVoYbiFP8zkXkYc4+Fcgq5KbAjvYqdo4ewemKw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.6", - "@codemirror/commands": "^6.1.0", - "@codemirror/state": "^6.1.1", - "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.23.10", - "codemirror": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.11.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/theme-one-dark": ">=6.0.0", - "@codemirror/view": ">=6.0.0", - "codemirror": ">=6.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@vitejs/plugin-react-swc": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", - "integrity": "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@swc/core": "^1.11.21" - }, - "peerDependencies": { - "vite": "^4 || ^5 || ^6" - } - }, - "node_modules/@vitest/expect": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz", - "integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz", - "integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.1.2", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", - "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz", - "integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.1.2", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz", - "integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.1.2", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz", - "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz", - "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.1.2", - "loupe": "^3.1.3", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vscode/python-extension": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@vscode/python-extension/-/python-extension-1.0.5.tgz", - "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==", - "license": "MIT", - "engines": { - "node": ">=16.17.1", - "vscode": "^1.78.0" - } - }, - "node_modules/@vscode/test-cli": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz", - "integrity": "sha512-B0mMH4ia+MOOtwNiLi79XhA+MLmUItIC8FckEuKrVAVriIuSWjt7vv4+bF8qVFiNFe4QRfzPaIZk39FZGWEwHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mocha": "^10.0.2", - "c8": "^9.1.0", - "chokidar": "^3.5.3", - "enhanced-resolve": "^5.15.0", - "glob": "^10.3.10", - "minimatch": "^9.0.3", - "mocha": "^10.2.0", - "supports-color": "^9.4.0", - "yargs": "^17.7.2" - }, - "bin": { - "vscode-test": "out/bin.mjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vscode/test-electron": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", - "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", - "jszip": "^3.10.1", - "ora": "^8.1.0", - "semver": "^7.6.2" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@vscode/vsce": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.3.2.tgz", - "integrity": "sha512-XQ4IhctYalSTMwLnMS8+nUaGbU7v99Qm2sOoGfIEf2QC7jpiLXZZMh7NwArEFsKX4gHTJLx0/GqAUlCdC3gKCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/identity": "^4.1.0", - "@vscode/vsce-sign": "^2.0.0", - "azure-devops-node-api": "^12.5.0", - "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.9", - "cockatiel": "^3.1.2", - "commander": "^12.1.0", - "form-data": "^4.0.0", - "glob": "^11.0.0", - "hosted-git-info": "^4.0.2", - "jsonc-parser": "^3.2.0", - "leven": "^3.1.0", - "markdown-it": "^14.1.0", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^7.5.2", - "tmp": "^0.2.3", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.5.0", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" - }, - "bin": { - "vsce": "vsce" - }, - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "keytar": "^7.7.0" - } - }, - "node_modules/@vscode/vsce-sign": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.5.tgz", - "integrity": "sha512-GfYWrsT/vypTMDMgWDm75iDmAOMe7F71sZECJ+Ws6/xyIfmB3ELVnVN+LwMFAvmXY+e6eWhR2EzNGF/zAhWY3Q==", - "dev": true, - "hasInstallScript": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optionalDependencies": { - "@vscode/vsce-sign-alpine-arm64": "2.0.2", - "@vscode/vsce-sign-alpine-x64": "2.0.2", - "@vscode/vsce-sign-darwin-arm64": "2.0.2", - "@vscode/vsce-sign-darwin-x64": "2.0.2", - "@vscode/vsce-sign-linux-arm": "2.0.2", - "@vscode/vsce-sign-linux-arm64": "2.0.2", - "@vscode/vsce-sign-linux-x64": "2.0.2", - "@vscode/vsce-sign-win32-arm64": "2.0.2", - "@vscode/vsce-sign-win32-x64": "2.0.2" - } - }, - "node_modules/@vscode/vsce-sign-darwin-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", - "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@vscode/vsce/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@vscode/vsce/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@vscode/vsce/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@vscode/vsce/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vscode/vsce/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@vscode/vsce/node_modules/glob": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", - "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/vsce/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@vscode/vsce/node_modules/jackspeak": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", - "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/vsce/node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@vscode/vsce/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@vscode/vsce/node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@vscode/vsce/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/vsce/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/apache-arrow": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-13.0.0.tgz", - "integrity": "sha512-3gvCX0GDawWz6KFNC28p65U+zGh/LZ6ZNKWNu74N6CQlKzxeoWHpi4CgEQsgRSEMuyrIIXi1Ea2syja7dwcHvw==", - "license": "Apache-2.0", - "dependencies": { - "@types/command-line-args": "5.2.0", - "@types/command-line-usage": "5.0.2", - "@types/node": "20.3.0", - "@types/pad-left": "2.1.1", - "command-line-args": "5.2.1", - "command-line-usage": "7.0.1", - "flatbuffers": "23.5.26", - "json-bignum": "^0.0.3", - "pad-left": "^2.1.0", - "tslib": "^2.5.3" - }, - "bin": { - "arrow2csv": "bin/arrow2csv.js" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/astring": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", - "dev": true, - "license": "MIT", - "bin": { - "astring": "bin/astring" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/azure-devops-node-api": { - "version": "12.5.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", - "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", - "dev": true, - "license": "MIT", - "dependencies": { - "tunnel": "0.0.6", - "typed-rest-client": "^1.8.4" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/c8": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", - "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^3.1.1", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.1.6", - "test-exclude": "^6.0.0", - "v8-to-istanbul": "^9.0.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1" - }, - "bin": { - "c8": "bin/c8.js" - }, - "engines": { - "node": ">=14.14.0" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001715", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", - "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk-template": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", - "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/chalk-template?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/cheerio": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "encoding-sniffer": "^0.2.0", - "htmlparser2": "^9.1.0", - "parse5": "^7.1.2", - "parse5-htmlparser2-tree-adapter": "^7.0.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^6.19.5", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=18.17" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/classcat": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", - "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", - "license": "MIT" - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cockatiel": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", - "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/codemirror": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", - "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/command-line-args": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", - "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", - "license": "MIT", - "dependencies": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", - "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/command-line-usage": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.1.tgz", - "integrity": "sha512-NCyznE//MuTjwi3y84QVUGEOT+P5oto1e1Pk/jFPVdPPfsG03qpTIl3yw6etR+v73d0lXsoojRpvbru2sqePxQ==", - "license": "MIT", - "dependencies": { - "array-back": "^6.2.2", - "chalk-template": "^0.4.0", - "table-layout": "^3.0.0", - "typical": "^7.1.1" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/command-line-usage/node_modules/array-back": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", - "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/command-line-usage/node_modules/typical": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", - "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/compare-versions": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", - "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssstyle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "rrweb-cssom": "^0.6.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/data-urls": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", - "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/data-urls/node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decimal.js": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", - "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", - "dev": true, - "license": "MIT" - }, - "node_modules/decode-named-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", - "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-equal/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dependency-graph": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", - "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "license": "Apache-2.0" - }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, - "node_modules/dnd-core": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", - "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", - "license": "MIT", - "dependencies": { - "@react-dnd/asap": "^5.0.1", - "@react-dnd/invariant": "^4.0.1", - "redux": "^4.2.0" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "license": "MIT", - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.140", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.140.tgz", - "integrity": "sha512-o82Rj+ONp4Ip7Cl1r7lrqx/pXhbp/lh9DpKcMNscFJdh8ebyRofnc7Sh01B4jx403RI0oqTBvlZ7OBIZLMr2+Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/elkjs": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", - "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", - "license": "EPL-2.0" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/encoding-sniffer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", - "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/enquirer/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-regex": "^1.2.1", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-aggregate-error": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.13.tgz", - "integrity": "sha512-KkzhUUuD2CUMqEc8JEqsXEMDHzDPE8RCjZeUBitsnB1eNcAJWQPiciKsMXe3Yytj4Flw1XLl46Qcf9OxvZha7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.2", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-get-iterator/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.3", - "@esbuild/android-arm": "0.25.3", - "@esbuild/android-arm64": "0.25.3", - "@esbuild/android-x64": "0.25.3", - "@esbuild/darwin-arm64": "0.25.3", - "@esbuild/darwin-x64": "0.25.3", - "@esbuild/freebsd-arm64": "0.25.3", - "@esbuild/freebsd-x64": "0.25.3", - "@esbuild/linux-arm": "0.25.3", - "@esbuild/linux-arm64": "0.25.3", - "@esbuild/linux-ia32": "0.25.3", - "@esbuild/linux-loong64": "0.25.3", - "@esbuild/linux-mips64el": "0.25.3", - "@esbuild/linux-ppc64": "0.25.3", - "@esbuild/linux-riscv64": "0.25.3", - "@esbuild/linux-s390x": "0.25.3", - "@esbuild/linux-x64": "0.25.3", - "@esbuild/netbsd-arm64": "0.25.3", - "@esbuild/netbsd-x64": "0.25.3", - "@esbuild/openbsd-arm64": "0.25.3", - "@esbuild/openbsd-x64": "0.25.3", - "@esbuild/sunos-x64": "0.25.3", - "@esbuild/win32-arm64": "0.25.3", - "@esbuild/win32-ia32": "0.25.3", - "@esbuild/win32-x64": "0.25.3" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", - "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.25.1", - "@eslint/plugin-kit": "^0.2.8", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "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.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/expect-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-memoize": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", - "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-replace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", - "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", - "license": "MIT", - "dependencies": { - "array-back": "^3.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatbuffers": { - "version": "23.5.26", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-23.5.26.tgz", - "integrity": "sha512-vE+SI9vrJDwi1oETtTIFldC/o9GsVKRM+s6EL0nQgxXlYV1Vc4Tk30hj4xGICftInKQKj1F3up2n8UbIVobISQ==", - "license": "SEE LICENSE IN LICENSE" - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-encoding-sniffer/node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http2-client": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", - "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", - "dev": true, - "license": "MIT" - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "devOptional": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", - "license": "MIT" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", - "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "abab": "^2.0.6", - "cssstyle": "^3.0.0", - "data-urls": "^4.0.0", - "decimal.js": "^10.4.3", - "domexception": "^4.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.4", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.1", - "ws": "^8.13.0", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/jsdom/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/jsdom/node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/jsdom/node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/jsep": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", - "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - } - }, - "node_modules/json-bignum": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz", - "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonpath-plus": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", - "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jsep-plugin/assignment": "^1.3.0", - "@jsep-plugin/regex": "^1.0.4", - "jsep": "^1.4.0" - }, - "bin": { - "jsonpath": "bin/jsonpath-cli.js", - "jsonpath-plus": "bin/jsonpath-cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jsonschema": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", - "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/keytar": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", - "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.1" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isempty": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", - "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.omitby": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.omitby/-/lodash.omitby-4.6.0.tgz", - "integrity": "sha512-5OrRcIVR75M288p4nbI2WLAf3ndw2GD9fyNv3Bc15+WCxJDdZ4lYndSxGd7hnG6PVjiJTeJE2dHEGhIuKGicIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.topath": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", - "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniqby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", - "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniqwith": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz", - "integrity": "sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/loglevel": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", - "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" - } - }, - "node_modules/loglevel-plugin-prefix": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", - "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/mocha": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", - "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^8.1.0", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/mocha/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/mocha/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true, - "license": "ISC" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/nimma": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", - "integrity": "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsep-plugin/regex": "^1.0.1", - "@jsep-plugin/ternary": "^1.0.2", - "astring": "^1.8.1", - "jsep": "^1.2.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - }, - "optionalDependencies": { - "jsonpath-plus": "^6.0.1 || ^10.1.0", - "lodash.topath": "^4.5.2" - } - }, - "node_modules/node-abi": { - "version": "3.74.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", - "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch-h2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", - "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", - "dev": true, - "license": "MIT", - "dependencies": { - "http2-client": "^1.2.5" - }, - "engines": { - "node": "4.x || >=6.0.0" - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/node-readfiles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", - "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es6-promise": "^3.2.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nwsapi": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", - "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", - "dev": true, - "license": "MIT" - }, - "node_modules/oas-kit-common": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", - "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "fast-safe-stringify": "^2.0.7" - } - }, - "node_modules/oas-linter": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", - "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@exodus/schemasafe": "^1.0.0-rc.2", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-linter/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/oas-resolver": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", - "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "node-fetch-h2": "^2.3.0", - "oas-kit-common": "^1.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "resolve": "resolve.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-resolver/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/oas-schema-walker": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", - "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", - "dev": true, - "license": "BSD-3-Clause", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-validator": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", - "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "call-me-maybe": "^1.0.1", - "oas-kit-common": "^1.0.8", - "oas-linter": "^3.2.2", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "reftools": "^1.1.9", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-validator/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz", - "integrity": "sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/openapi3-ts": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.2.2.tgz", - "integrity": "sha512-+9g4actZKeb3czfi9gVQ4Br2Ju3KwhCAQJBNaKgye5KggqcBLIhFHH+nIkcm0BUX00TrAJl6dH4JWgM4G4JWrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "yaml": "^2.3.4" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ora/node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/orval": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/orval/-/orval-7.9.0.tgz", - "integrity": "sha512-kFftcVojM4wRddRktqJPI/P9uYRpgiwCFOxF82G7XqDrczX9XDu8b5ialof+Z1LIuVZL4CvLV0Y184mRgrJrUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "^10.1.1", - "@orval/angular": "7.9.0", - "@orval/axios": "7.9.0", - "@orval/core": "7.9.0", - "@orval/fetch": "7.9.0", - "@orval/hono": "7.9.0", - "@orval/mcp": "7.9.0", - "@orval/mock": "7.9.0", - "@orval/query": "7.9.0", - "@orval/swr": "7.9.0", - "@orval/zod": "7.9.0", - "ajv": "^8.17.1", - "cac": "^6.7.14", - "chalk": "^4.1.2", - "chokidar": "^4.0.3", - "enquirer": "^2.4.1", - "execa": "^5.1.1", - "find-up": "5.0.0", - "fs-extra": "^11.2.0", - "lodash.uniq": "^4.5.0", - "openapi3-ts": "4.2.2", - "string-argv": "^0.3.2", - "tsconfck": "^2.0.1", - "typedoc": "^0.28.0", - "typedoc-plugin-markdown": "^4.4.2", - "typescript": "^5.6.3" - }, - "bin": { - "orval": "dist/bin/orval.js" - } - }, - "node_modules/orval/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/orval/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/orval/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/orval/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/pad-left": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pad-left/-/pad-left-2.1.0.tgz", - "integrity": "sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA==", - "license": "MIT", - "dependencies": { - "repeat-string": "^1.5.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, - "license": "(MIT AND Zlib)" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/parse-semver": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^5.1.0" - } - }, - "node_modules/parse-semver/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^4.5.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.52.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pony-cause": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", - "integrity": "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==", - "dev": true, - "license": "0BSD", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/property-information": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", - "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dnd": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", - "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", - "license": "MIT", - "dependencies": { - "@react-dnd/invariant": "^4.0.1", - "@react-dnd/shallowequal": "^4.0.1", - "dnd-core": "^16.0.1", - "fast-deep-equal": "^3.1.3", - "hoist-non-react-statics": "^3.3.2" - }, - "peerDependencies": { - "@types/hoist-non-react-statics": ">= 3.3.1", - "@types/node": ">= 12", - "@types/react": ">= 16", - "react": ">= 16.14" - }, - "peerDependenciesMeta": { - "@types/hoist-non-react-statics": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-dnd-html5-backend": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", - "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", - "license": "MIT", - "dependencies": { - "dnd-core": "^16.0.1" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-markdown": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", - "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "html-url-attributes": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "unified": "^11.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", - "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-router": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", - "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", - "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-split": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/react-split/-/react-split-2.0.14.tgz", - "integrity": "sha512-bKWydgMgaKTg/2JGQnaJPg51T6dmumTWZppFgEbbY0Fbme0F5TuatAScCLaqommbGQQf/ZT1zaejuPDriscISA==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.5.7", - "split.js": "^1.6.0" - }, - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/reactflow": { - "version": "11.11.4", - "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", - "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", - "license": "MIT", - "dependencies": { - "@reactflow/background": "11.3.14", - "@reactflow/controls": "11.2.14", - "@reactflow/core": "11.11.4", - "@reactflow/minimap": "11.7.14", - "@reactflow/node-resizer": "2.2.14", - "@reactflow/node-toolbar": "1.3.14" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "mute-stream": "~0.0.4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reftools": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", - "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", - "dev": true, - "license": "BSD-3-Clause", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.1", - "@rollup/rollup-android-arm64": "4.40.1", - "@rollup/rollup-darwin-arm64": "4.40.1", - "@rollup/rollup-darwin-x64": "4.40.1", - "@rollup/rollup-freebsd-arm64": "4.40.1", - "@rollup/rollup-freebsd-x64": "4.40.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", - "@rollup/rollup-linux-arm64-gnu": "4.40.1", - "@rollup/rollup-linux-arm64-musl": "4.40.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-musl": "4.40.1", - "@rollup/rollup-linux-s390x-gnu": "4.40.1", - "@rollup/rollup-linux-x64-gnu": "4.40.1", - "@rollup/rollup-linux-x64-musl": "4.40.1", - "@rollup/rollup-win32-arm64-msvc": "4.40.1", - "@rollup/rollup-win32-ia32-msvc": "4.40.1", - "@rollup/rollup-win32-x64-msvc": "4.40.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", - "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==", - "dev": true, - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, - "license": "ISC" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.1.tgz", - "integrity": "sha512-jjlZ7UknkyQxGnHF1w8wDgWfdtnW0hBX7tmDp04zBwDBZ/6tPJI1+RWfBHGMA4+0nAjGptp+eDpIYP6mldJbqg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/should": { - "version": "13.2.3", - "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", - "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "should-equal": "^2.0.0", - "should-format": "^3.0.3", - "should-type": "^1.4.0", - "should-type-adaptors": "^1.0.1", - "should-util": "^1.0.0" - } - }, - "node_modules/should-equal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", - "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.4.0" - } - }, - "node_modules/should-format": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", - "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.3.0", - "should-type-adaptors": "^1.0.1" - } - }, - "node_modules/should-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", - "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/should-type-adaptors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", - "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.3.0", - "should-util": "^1.0.0" - } - }, - "node_modules/should-util": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", - "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", - "dev": true, - "license": "MIT" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/simple-eval": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", - "integrity": "sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "jsep": "^1.3.6" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/split.js": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz", - "integrity": "sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==", - "license": "MIT" - }, - "node_modules/sqlmesh": { - "resolved": "vscode/extension", - "link": true - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "dev": true, - "license": "MIT" - }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stream-read-all": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/stream-read-all/-/stream-read-all-3.0.1.tgz", - "integrity": "sha512-EWZT9XOceBPlVJRrYcykW8jyRSZYbkb/0ZK36uLEmoWVO5gxBOnntNTseNzfREsqxqdfEGQrD8SXQ3QWbBmq8A==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", - "license": "MIT" - }, - "node_modules/style-to-js": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", - "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.8" - } - }, - "node_modules/style-to-object": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", - "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.4" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swagger2openapi": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", - "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "call-me-maybe": "^1.0.1", - "node-fetch": "^2.6.1", - "node-fetch-h2": "^2.3.0", - "node-readfiles": "^0.2.0", - "oas-kit-common": "^1.0.8", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "oas-validator": "^5.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "boast": "boast.js", - "oas-validate": "oas-validate.js", - "swagger2openapi": "swagger2openapi.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/swagger2openapi/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/table-layout": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-3.0.2.tgz", - "integrity": "sha512-rpyNZYRw+/C+dYkcQ3Pr+rLxW4CfHpXjPDnG7lYhdRoUcZTUt+KEsX+94RGp/aVp/MQU35JCITv2T/beY4m+hw==", - "license": "MIT", - "dependencies": { - "@75lb/deep-merge": "^1.1.1", - "array-back": "^6.2.2", - "command-line-args": "^5.2.1", - "command-line-usage": "^7.0.0", - "stream-read-all": "^3.0.1", - "typical": "^7.1.1", - "wordwrapjs": "^5.1.0" - }, - "bin": { - "table-layout": "bin/cli.js" - }, - "engines": { - "node": ">=12.17" - } - }, - "node_modules/table-layout/node_modules/array-back": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", - "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/table-layout/node_modules/typical": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", - "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/thememirror": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/thememirror/-/thememirror-2.0.1.tgz", - "integrity": "sha512-d5i6FVvWWPkwrm4cHLI3t9AT1OrkAt7Ig8dtdYSofgF7C/eiyNuq6zQzSTusWTde3jpW9WLvA9J/fzNKMUsd0w==", - "license": "MIT", - "peerDependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tobiko": { - "resolved": "web/client", - "link": true - }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "license": "Apache-2.0" - }, - "node_modules/ts-loader": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", - "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/tsconfck": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.2.tgz", - "integrity": "sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==", - "dev": true, - "license": "MIT", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^14.13.1 || ^16 || >=18" - }, - "peerDependencies": { - "typescript": "^4.3.5 || ^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-rest-client": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", - "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "qs": "^6.9.1", - "tunnel": "0.0.6", - "underscore": "^1.12.1" - } - }, - "node_modules/typedoc": { - "version": "0.28.3", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.3.tgz", - "integrity": "sha512-5svOCTfXvVSh6zbZKSQluZhR8yN2tKpTeHZxlmWpE6N5vc3R8k/jhg9nnD6n5tN9/ObuQTojkONrOxFdUFUG9w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@gerrit0/mini-shiki": "^3.2.2", - "lunr": "^2.3.9", - "markdown-it": "^14.1.0", - "minimatch": "^9.0.5", - "yaml": "^2.7.1" - }, - "bin": { - "typedoc": "bin/typedoc" - }, - "engines": { - "node": ">= 18", - "pnpm": ">= 10" - }, - "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" - } - }, - "node_modules/typedoc-plugin-markdown": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.6.3.tgz", - "integrity": "sha512-86oODyM2zajXwLs4Wok2mwVEfCwCnp756QyhLGX2IfsdRYr1DXLCgJgnLndaMUjJD7FBhnLk2okbNE9PdLxYRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "typedoc": "0.28.x" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz", - "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.31.1", - "@typescript-eslint/parser": "8.31.1", - "@typescript-eslint/utils": "8.31.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/typical": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", - "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici": { - "version": "6.21.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", - "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/urijs": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", - "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "dev": true, - "license": "MIT" - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utility-types": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/validator": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", - "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", - "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz", - "integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.0", - "es-module-lexer": "^1.6.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-plugin-css-injected-by-js": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.2.tgz", - "integrity": "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "vite": ">2.0.0-0" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitest": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz", - "integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "3.1.2", - "@vitest/mocker": "3.1.2", - "@vitest/pretty-format": "^3.1.2", - "@vitest/runner": "3.1.2", - "@vitest/snapshot": "3.1.2", - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", - "chai": "^5.2.0", - "debug": "^4.4.0", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.2", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.2", - "@vitest/ui": "3.1.2", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageclient": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", - "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", - "license": "MIT", - "dependencies": { - "minimatch": "^5.1.0", - "semver": "^7.3.7", - "vscode-languageserver-protocol": "3.17.5" - }, - "engines": { - "vscode": "^1.82.0" - } - }, - "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" - }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "license": "MIT" - }, - "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/webpack": { - "version": "5.99.6", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz", - "integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", - "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wordwrapjs": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz", - "integrity": "sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12" - } - }, - "node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser/node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yazl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zustand": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", - "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "vscode/extension": { - "name": "sqlmesh", - "version": "0.0.1", - "dependencies": { - "@types/fs-extra": "^11.0.4", - "@vscode/python-extension": "^1.0.5", - "fs-extra": "^11.3.0", - "vscode-languageclient": "^9.0.1", - "zod": "^3.24.3" - }, - "devDependencies": { - "@types/mocha": "^10.0.10", - "@types/node": "20.11.25", - "@types/vscode": "1.96.0", - "@typescript-eslint/eslint-plugin": "^8.28.0", - "@typescript-eslint/parser": "^8.28.0", - "@vscode/test-cli": "^0.0.10", - "@vscode/test-electron": "^2.4.1", - "@vscode/vsce": "^3.3.2", - "esbuild": "^0.25.2", - "eslint": "^9.23.0", - "ts-loader": "^9.5.2", - "typescript": "^5.8.2" - }, - "engines": { - "vscode": "^1.96.0" - } - }, - "vscode/extension/node_modules/@types/node": { - "version": "20.11.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", - "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "web/client": { - "name": "tobiko", - "version": "0.0.0", - "dependencies": { - "@codemirror/autocomplete": "^6.16.2", - "@codemirror/commands": "^6.6.0", - "@codemirror/lang-python": "^6.1.6", - "@codemirror/lang-sql": "^6.6.4", - "@codemirror/language": "^6.10.2", - "@codemirror/legacy-modes": "^6.4.0", - "@codemirror/state": "^6.4.1", - "@codemirror/view": "^6.28.1", - "@headlessui/react": "^1.7.17", - "@heroicons/react": "^2.0.18", - "@lit/react": "^1.0.6", - "@radix-ui/react-context-menu": "^2.1.4", - "@radix-ui/react-select": "^1.2.2", - "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^4.33.0", - "@tanstack/react-table": "^8.9.2", - "@tanstack/react-virtual": "^3.0.0-beta.56", - "@uidotdev/usehooks": "^2.2.0", - "@uiw/react-codemirror": "^4.21.12", - "apache-arrow": "^13.0.0", - "clsx": "^2.0.0", - "diff": "^5.2.0", - "elkjs": "^0.8.2", - "pluralize": "^8.0.0", - "react": "^18.2.0", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", - "react-dom": "^18.2.0", - "react-markdown": "^9.0.1", - "react-router-dom": "^6.15.0", - "react-split": "^2.0.14", - "reactflow": "^11.8.3", - "thememirror": "^2.0.1", - "zustand": "^4.4.1" - }, - "devDependencies": { - "@eslint/js": "^9.25.1", - "@playwright/test": "^1.37.1", - "@swc/core": "^1.11.24", - "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "^14.0.0", - "@testing-library/user-event": "^14.4.3", - "@types/diff": "^5.2.1", - "@types/pluralize": "^0.0.30", - "@types/react": "^18.2.21", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react-swc": "^3.9.0", - "autoprefixer": "^10.4.15", - "jsdom": "^22.1.0", - "orval": "^7.9.0", - "postcss": "^8.4.29", - "prettier": "^3.0.3", - "tailwindcss": "^3.3.3", - "typescript": "^5.8.3", - "typescript-eslint": "^8.31.1", - "vite": "^6.3.4", - "vite-plugin-css-injected-by-js": "^3.5.2", - "vitest": "^3.1.2" - } + "name": "sqlmesh", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sqlmesh", + "workspaces": [ + "vscode/extension", + "web/client" + ], + "devDependencies": { + "prettier": "^3.5.2" + } + }, + "node_modules/@75lb/deep-merge": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "typical": "^7.1.1" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/@75lb/deep-merge/node_modules/typical": { + "version": "7.3.0", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "11.7.2", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@asyncapi/specs": { + "version": "6.8.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.11" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.19.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.5.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.5.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.8.1", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.1.7", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.8.0", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.0", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.1", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.5", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.10", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.36.5", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.3", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.25.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "license": "MIT" + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.3.0", + "@shikijs/langs": "^3.3.0", + "@shikijs/themes": "^3.3.0", + "@shikijs/types": "^3.3.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@headlessui/react": { + "version": "1.7.19", + "license": "MIT", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.60", + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@ibm-cloud/openapi-ruleset": { + "version": "1.31.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ibm-cloud/openapi-ruleset-utilities": "1.9.0", + "@stoplight/spectral-formats": "^1.8.2", + "@stoplight/spectral-functions": "^1.9.3", + "@stoplight/spectral-rulesets": "^1.21.3", + "chalk": "^4.1.2", + "jsonschema": "^1.5.0", + "lodash": "^4.17.21", + "loglevel": "^1.9.2", + "loglevel-plugin-prefix": "0.8.4", + "minimatch": "^6.2.0", + "validator": "^13.11.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ibm-cloud/openapi-ruleset-utilities": { + "version": "1.9.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ibm-cloud/openapi-ruleset/node_modules/minimatch": { + "version": "6.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/ternary": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.18", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lit/react": { + "version": "1.0.7", + "license": "BSD-3-Clause", + "peerDependencies": { + "@types/react": "17 || 18 || 19" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@orval/angular": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0" + } + }, + "node_modules/@orval/axios": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0" + } + }, + "node_modules/@orval/core": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.1", + "@ibm-cloud/openapi-ruleset": "^1.29.4", + "acorn": "^8.14.1", + "ajv": "^8.17.1", + "chalk": "^4.1.2", + "compare-versions": "^6.1.1", + "debug": "^4.4.0", + "esbuild": "^0.25.1", + "esutils": "2.0.3", + "fs-extra": "^11.3.0", + "globby": "11.1.0", + "lodash.isempty": "^4.4.0", + "lodash.uniq": "^4.5.0", + "lodash.uniqby": "^4.7.0", + "lodash.uniqwith": "^4.5.0", + "micromatch": "^4.0.8", + "openapi3-ts": "4.4.0", + "swagger2openapi": "^7.0.8" + } + }, + "node_modules/@orval/core/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@orval/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@orval/core/node_modules/openapi3-ts": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.5.0" + } + }, + "node_modules/@orval/fetch": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0" + } + }, + "node_modules/@orval/hono": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0", + "@orval/zod": "7.9.0", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@orval/mcp": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0", + "@orval/zod": "7.9.0" + } + }, + "node_modules/@orval/mock": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0", + "openapi3-ts": "^4.2.2" + } + }, + "node_modules/@orval/query": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0", + "@orval/fetch": "7.9.0", + "lodash.omitby": "^4.6.0" + } + }, + "node_modules/@orval/swr": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0", + "@orval/fetch": "7.9.0" + } + }, + "node_modules/@orval/zod": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "7.9.0", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.11", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.11", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.11", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-roving-focus": "1.1.7", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.4", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.6", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.4", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.3", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.2", + "@radix-ui/react-portal": "1.0.3", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-rect": "1.0.1", + "@radix-ui/react-use-size": "1.0.1", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-select/node_modules/react-remove-scroll": { + "version": "2.5.5", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "license": "MIT" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "license": "MIT" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "license": "MIT" + }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", + "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", + "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.1", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", + "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", + "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", + "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", + "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", + "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", + "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", + "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", + "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", + "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", + "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", + "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", + "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", + "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", + "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", + "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", + "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", + "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/json": { + "version": "3.21.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.3", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "jsonc-parser": "~2.2.1", + "lodash": "^4.17.21", + "safe-stable-stringify": "^1.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers": { + "version": "1.2.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-fetch": "^2.6.0", + "tslib": "^1.14.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stoplight/json-ref-resolver": { + "version": "3.1.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.21.0", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^12.3.0 || ^13.0.0", + "@types/urijs": "^1.19.19", + "dependency-graph": "~0.11.0", + "fast-memoize": "^2.5.2", + "immer": "^9.0.6", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "urijs": "^1.19.11" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json/node_modules/jsonc-parser": { + "version": "2.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/ordered-object-literal": { + "version": "1.0.5", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/path": { + "version": "1.3.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/spectral-core": { + "version": "1.20.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "~3.21.0", + "@stoplight/path": "1.3.2", + "@stoplight/spectral-parsers": "^1.0.0", + "@stoplight/spectral-ref-resolver": "^1.0.4", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "~13.6.0", + "@types/es-aggregate-error": "^1.0.2", + "@types/json-schema": "^7.0.11", + "ajv": "^8.17.1", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "es-aggregate-error": "^1.0.7", + "jsonpath-plus": "^10.3.0", + "lodash": "~4.17.21", + "lodash.topath": "^4.5.2", + "minimatch": "3.1.2", + "nimma": "0.2.3", + "pony-cause": "^1.1.1", + "simple-eval": "1.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/ajv-errors": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/spectral-core/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@stoplight/spectral-formats": { + "version": "1.8.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.17.0", + "@stoplight/spectral-core": "^1.19.2", + "@types/json-schema": "^7.0.7", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-functions": { + "version": "1.10.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "^3.17.1", + "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-formats": "^1.8.1", + "@stoplight/spectral-runtime": "^1.1.2", + "ajv": "^8.17.1", + "ajv-draft-04": "~1.0.0", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "lodash": "~4.17.21", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv-draft-04": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv-errors": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/spectral-parsers": { + "version": "1.0.5", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "~3.21.0", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml": "~4.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": { + "version": "14.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/spectral-ref-resolver": { + "version": "1.0.5", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json-ref-readers": "1.2.2", + "@stoplight/json-ref-resolver": "~3.1.6", + "@stoplight/spectral-runtime": "^1.1.2", + "dependency-graph": "0.11.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-rulesets": { + "version": "1.22.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@asyncapi/specs": "^6.8.0", + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "^3.17.0", + "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-formats": "^1.8.1", + "@stoplight/spectral-functions": "^1.9.1", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "^13.6.0", + "@types/json-schema": "^7.0.7", + "ajv": "^8.17.1", + "ajv-formats": "~2.1.1", + "json-schema-traverse": "^1.0.0", + "leven": "3.1.0", + "lodash": "~4.17.21", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-rulesets/node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/spectral-rulesets/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-rulesets/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/spectral-runtime": { + "version": "1.1.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.20.1", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "abort-controller": "^3.0.0", + "lodash": "^4.17.21", + "node-fetch": "^2.7.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/types": { + "version": "13.6.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/yaml": { + "version": "4.3.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.5", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml-ast-parser": "0.0.50", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=10.8" + } + }, + "node_modules/@stoplight/yaml-ast-parser": { + "version": "0.0.50", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@stoplight/yaml/node_modules/@stoplight/types": { + "version": "14.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@swc/core": { + "version": "1.11.24", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.21" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.11.24", + "@swc/core-darwin-x64": "1.11.24", + "@swc/core-linux-arm-gnueabihf": "1.11.24", + "@swc/core-linux-arm64-gnu": "1.11.24", + "@swc/core-linux-arm64-musl": "1.11.24", + "@swc/core-linux-x64-gnu": "1.11.24", + "@swc/core-linux-x64-musl": "1.11.24", + "@swc/core-win32-arm64-msvc": "1.11.24", + "@swc/core-win32-ia32-msvc": "1.11.24", + "@swc/core-win32-x64-msvc": "1.11.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.11.24", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", + "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.21", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/container-queries": { + "version": "0.1.1", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.2.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.36.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.36.1", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "4.36.1", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.6", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.6", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "9.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "14.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/command-line-args": { + "version": "5.2.0", + "license": "MIT" + }, + "node_modules/@types/command-line-usage": { + "version": "5.0.2", + "license": "MIT" + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/diff": { + "version": "5.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/es-aggregate-error": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.3.0", + "license": "MIT" + }, + "node_modules/@types/pad-left": { + "version": "2.1.1", + "license": "MIT" + }, + "node_modules/@types/pluralize": { + "version": "0.0.30", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.20", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.6", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/@types/urijs": { + "version": "1.19.25", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.96.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.31.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/type-utils": "8.31.1", + "@typescript-eslint/utils": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.31.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/typescript-estree": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.31.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.31.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.31.1", + "@typescript-eslint/utils": "8.31.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.31.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.31.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.31.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/typescript-estree": "8.31.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.31.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.31.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@uidotdev/usehooks": { + "version": "2.4.1", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.23.10", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.23.10", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.23.10", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/core": "^1.11.21" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6" + } + }, + "node_modules/@vitest/expect": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.2", + "@vitest/utils": "3.1.2", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.2", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vscode/python-extension": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">=16.17.1", + "vscode": "^1.78.0" + } + }, + "node_modules/@vscode/test-cli": { + "version": "0.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mocha": "^10.0.2", + "c8": "^9.1.0", + "chokidar": "^3.5.3", + "enhanced-resolve": "^5.15.0", + "glob": "^10.3.10", + "minimatch": "^9.0.3", + "mocha": "^10.2.0", + "supports-color": "^9.4.0", + "yargs": "^17.7.2" + }, + "bin": { + "vscode-test": "out/bin.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vscode/test-electron": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^11.0.0", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.5", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.2", + "@vscode/vsce-sign-alpine-x64": "2.0.2", + "@vscode/vsce-sign-darwin-arm64": "2.0.2", + "@vscode/vsce-sign-darwin-x64": "2.0.2", + "@vscode/vsce-sign-linux-arm": "2.0.2", + "@vscode/vsce-sign-linux-arm64": "2.0.2", + "@vscode/vsce-sign-linux-x64": "2.0.2", + "@vscode/vsce-sign-win32-arm64": "2.0.2", + "@vscode/vsce-sign-win32-x64": "2.0.2" + } + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.2", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce/node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@vscode/vsce/node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/vsce/node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@vscode/vsce/node_modules/glob": { + "version": "11.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/jackspeak": { + "version": "4.1.0", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/lru-cache": { + "version": "11.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@vscode/vsce/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@vscode/vsce/node_modules/minimatch/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@vscode/vsce/node_modules/path-scurry": { + "version": "2.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/abab": { + "version": "2.0.6", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apache-arrow": { + "version": "13.0.0", + "license": "Apache-2.0", + "dependencies": { + "@types/command-line-args": "5.2.0", + "@types/command-line-usage": "5.0.2", + "@types/node": "20.3.0", + "@types/pad-left": "2.1.1", + "command-line-args": "5.2.1", + "command-line-usage": "7.0.1", + "flatbuffers": "23.5.26", + "json-bignum": "^0.0.3", + "pad-left": "^2.1.0", + "tslib": "^2.5.3" + }, + "bin": { + "arrow2csv": "bin/arrow2csv.js" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.1.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/array-back": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "dev": true, + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.24.4", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c8": { + "version": "9.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=14.14.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001715", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "0.4.0", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cheerio": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/codemirror": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "license": "MIT", + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "7.0.1", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^3.0.0", + "typical": "^7.1.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "6.2.2", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "7.3.0", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/compare-versions": { + "version": "6.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/crelt": { + "version": "1.0.6", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-equal/node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "license": "Apache-2.0" + }, + "node_modules/diff": { + "version": "5.2.0", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/dnd-core": { + "version": "16.0.1", + "license": "MIT", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domexception": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "5.0.3", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.140", + "dev": true, + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.8.2", + "license": "EPL-2.0" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-aggregate-error": { + "version": "1.0.13", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator/node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.3", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.25.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.25.1", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "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.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-memoize": { + "version": "2.5.2", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatbuffers": { + "version": "23.5.26", + "license": "SEE LICENSE IN LICENSE" + }, + "node_modules/flatted": { + "version": "3.3.3", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "10.4.5", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "dev": true, + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-encoding-sniffer/node_modules/whatwg-encoding": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-client": { + "version": "1.3.5", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/immer": { + "version": "9.0.21", + "devOptional": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "license": "MIT" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "22.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsep": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/json-bignum": { + "version": "0.0.3", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpath-plus": { + "version": "10.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonschema": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isempty": { + "version": "4.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.omitby": { + "version": "4.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.topath": { + "version": "4.5.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniqwith": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loglevel": { + "version": "1.9.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "dev": true, + "license": "MIT" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/mocha": { + "version": "10.8.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.9", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "dev": true, + "license": "ISC" + }, + "node_modules/mz": { + "version": "2.7.0", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/nimma": { + "version": "0.2.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsep-plugin/regex": "^1.0.1", + "@jsep-plugin/ternary": "^1.0.2", + "astring": "^1.8.1", + "jsep": "^1.2.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + }, + "optionalDependencies": { + "jsonpath-plus": "^6.0.1 || ^10.1.0", + "lodash.topath": "^4.5.2" + } + }, + "node_modules/node-abi": { + "version": "3.74.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-h2": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "http2-client": "^1.2.5" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-readfiles": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es6-promise": "^3.2.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "dev": true, + "license": "MIT" + }, + "node_modules/oas-kit-common": { + "version": "1.0.8", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "fast-safe-stringify": "^2.0.7" + } + }, + "node_modules/oas-linter": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@exodus/schemasafe": "^1.0.0-rc.2", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-linter/node_modules/yaml": { + "version": "1.10.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oas-resolver": { + "version": "2.5.6", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "resolve": "resolve.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-resolver/node_modules/yaml": { + "version": "1.10.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oas-schema-walker": { + "version": "1.1.5", + "dev": true, + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator": { + "version": "5.0.8", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.2.2", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "reftools": "^1.1.9", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator/node_modules/yaml": { + "version": "1.10.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/openapi3-ts": { + "version": "4.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.3.4" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/orval": { + "version": "7.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.1", + "@orval/angular": "7.9.0", + "@orval/axios": "7.9.0", + "@orval/core": "7.9.0", + "@orval/fetch": "7.9.0", + "@orval/hono": "7.9.0", + "@orval/mcp": "7.9.0", + "@orval/mock": "7.9.0", + "@orval/query": "7.9.0", + "@orval/swr": "7.9.0", + "@orval/zod": "7.9.0", + "ajv": "^8.17.1", + "cac": "^6.7.14", + "chalk": "^4.1.2", + "chokidar": "^4.0.3", + "enquirer": "^2.4.1", + "execa": "^5.1.1", + "find-up": "5.0.0", + "fs-extra": "^11.2.0", + "lodash.uniq": "^4.5.0", + "openapi3-ts": "4.2.2", + "string-argv": "^0.3.2", + "tsconfck": "^2.0.1", + "typedoc": "^0.28.0", + "typedoc-plugin-markdown": "^4.4.2", + "typescript": "^5.6.3" + }, + "bin": { + "orval": "dist/bin/orval.js" + } + }, + "node_modules/orval/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/orval/node_modules/chokidar": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/orval/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/orval/node_modules/readdirp": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "license": "BlueOak-1.0.0" + }, + "node_modules/pad-left": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "license": "MIT" + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "license": "ISC" + }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pony-cause": { + "version": "1.1.1", + "dev": true, + "license": "0BSD", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.0.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dnd": { + "version": "16.0.1", + "license": "MIT", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "license": "MIT", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "9.1.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "6.30.0", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.0", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-split": { + "version": "2.0.14", + "license": "MIT", + "dependencies": { + "prop-types": "^15.5.7", + "split.js": "^1.6.0" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/reactflow": { + "version": "11.11.4", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/read": { + "version": "1.0.7", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "4.2.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reftools": { + "version": "1.1.9", + "dev": true, + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.40.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.1", + "@rollup/rollup-android-arm64": "4.40.1", + "@rollup/rollup-darwin-arm64": "4.40.1", + "@rollup/rollup-darwin-x64": "4.40.1", + "@rollup/rollup-freebsd-arm64": "4.40.1", + "@rollup/rollup-freebsd-x64": "4.40.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", + "@rollup/rollup-linux-arm-musleabihf": "4.40.1", + "@rollup/rollup-linux-arm64-gnu": "4.40.1", + "@rollup/rollup-linux-arm64-musl": "4.40.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", + "@rollup/rollup-linux-riscv64-gnu": "4.40.1", + "@rollup/rollup-linux-riscv64-musl": "4.40.1", + "@rollup/rollup-linux-s390x-gnu": "4.40.1", + "@rollup/rollup-linux-x64-gnu": "4.40.1", + "@rollup/rollup-linux-x64-musl": "4.40.1", + "@rollup/rollup-win32-arm64-msvc": "4.40.1", + "@rollup/rollup-win32-ia32-msvc": "4.40.1", + "@rollup/rollup-win32-x64-msvc": "4.40.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "dev": true, + "license": "ISC" + }, + "node_modules/saxes": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/should": { + "version": "13.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "node_modules/should-equal": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.4.0" + } + }, + "node_modules/should-format": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "node_modules/should-type": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/should-type-adaptors": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "node_modules/should-util": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-eval": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "jsep": "^1.3.6" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/simple-get": { + "version": "4.0.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/split.js": { + "version": "1.6.5", + "license": "MIT" + }, + "node_modules/sqlmesh": { + "resolved": "vscode/extension", + "link": true + }, + "node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-read-all": { + "version": "3.0.1", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "license": "MIT" + }, + "node_modules/style-to-js": { + "version": "1.1.16", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.8" + } + }, + "node_modules/style-to-object": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "9.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger2openapi": { + "version": "7.0.8", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "node-fetch": "^2.6.1", + "node-fetch-h2": "^2.3.0", + "node-readfiles": "^0.2.0", + "oas-kit-common": "^1.0.8", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "oas-validator": "^5.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "boast": "boast.js", + "oas-validate": "oas-validate.js", + "swagger2openapi": "swagger2openapi.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/swagger2openapi/node_modules/yaml": { + "version": "1.10.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/table-layout": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "@75lb/deep-merge": "^1.1.1", + "array-back": "^6.2.2", + "command-line-args": "^5.2.1", + "command-line-usage": "^7.0.0", + "stream-read-all": "^3.0.1", + "typical": "^7.1.1", + "wordwrapjs": "^5.1.0" + }, + "bin": { + "table-layout": "bin/cli.js" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "6.2.2", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/table-layout/node_modules/typical": { + "version": "7.3.0", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-fs": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/terser": { + "version": "5.39.0", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/thememirror": { + "version": "2.0.1", + "license": "MIT", + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tobiko": { + "resolved": "web/client", + "link": true + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "license": "Apache-2.0" + }, + "node_modules/ts-loader": { + "version": "9.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/tsconfck": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^14.13.1 || ^16 || >=18" + }, + "peerDependencies": { + "typescript": "^4.3.5 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typedoc": { + "version": "0.28.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.2.2", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.7.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + } + }, + "node_modules/typedoc-plugin-markdown": { + "version": "4.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typedoc": "0.28.x" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.31.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.31.1", + "@typescript-eslint/parser": "8.31.1", + "@typescript-eslint/utils": "8.31.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.21.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urijs": { + "version": "1.19.11", + "dev": true, + "license": "MIT" + }, + "node_modules/url-join": { + "version": "4.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/url-parse": { + "version": "1.5.10", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-css-injected-by-js": { + "version": "3.5.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": ">2.0.0-0" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.1.2", + "@vitest/mocker": "3.1.2", + "@vitest/pretty-format": "^3.1.2", + "@vitest/runner": "3.1.2", + "@vitest/snapshot": "3.1.2", + "@vitest/spy": "3.1.2", + "@vitest/utils": "3.1.2", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.13", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.1.2", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.1.2", + "@vitest/ui": "3.1.2", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "license": "MIT", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.6", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.99.6", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "12.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/which": { + "version": "2.0.2", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrapjs": { + "version": "5.1.0", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.7.1", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.24.3", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "4.5.6", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "vscode/extension": { + "name": "sqlmesh", + "version": "0.0.1", + "dependencies": { + "@types/fs-extra": "^11.0.4", + "@vscode/python-extension": "^1.0.5", + "fs-extra": "^11.3.0", + "vscode-languageclient": "^9.0.1", + "zod": "^3.24.3" + }, + "devDependencies": { + "@types/mocha": "^10.0.10", + "@types/node": "20.11.25", + "@types/vscode": "1.96.0", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", + "@vscode/test-cli": "^0.0.10", + "@vscode/test-electron": "^2.4.1", + "@vscode/vsce": "^3.3.2", + "esbuild": "^0.25.2", + "eslint": "^9.23.0", + "ts-loader": "^9.5.2", + "typescript": "^5.8.2" + }, + "engines": { + "vscode": "^1.96.0" + } + }, + "vscode/extension/node_modules/@types/node": { + "version": "20.11.25", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "web/client": { + "name": "tobiko", + "version": "0.0.0", + "dependencies": { + "@codemirror/autocomplete": "^6.16.2", + "@codemirror/commands": "^6.6.0", + "@codemirror/lang-python": "^6.1.6", + "@codemirror/lang-sql": "^6.6.4", + "@codemirror/language": "^6.10.2", + "@codemirror/legacy-modes": "^6.4.0", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.28.1", + "@headlessui/react": "^1.7.17", + "@heroicons/react": "^2.0.18", + "@lit/react": "^1.0.6", + "@radix-ui/react-context-menu": "^2.1.4", + "@radix-ui/react-select": "^1.2.2", + "@tailwindcss/container-queries": "^0.1.1", + "@tanstack/react-query": "^4.33.0", + "@tanstack/react-table": "^8.9.2", + "@tanstack/react-virtual": "^3.0.0-beta.56", + "@uidotdev/usehooks": "^2.2.0", + "@uiw/react-codemirror": "^4.21.12", + "apache-arrow": "^13.0.0", + "clsx": "^2.0.0", + "diff": "^5.2.0", + "elkjs": "^0.8.2", + "pluralize": "^8.0.0", + "react": "^18.2.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", + "react-router-dom": "^6.15.0", + "react-split": "^2.0.14", + "reactflow": "^11.8.3", + "thememirror": "^2.0.1", + "zustand": "^4.4.1" + }, + "devDependencies": { + "@eslint/js": "^9.25.1", + "@playwright/test": "^1.37.1", + "@swc/core": "^1.11.24", + "@testing-library/jest-dom": "^6.1.2", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.4.3", + "@types/diff": "^5.2.1", + "@types/pluralize": "^0.0.30", + "@types/react": "^18.2.21", + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react-swc": "^3.9.0", + "autoprefixer": "^10.4.15", + "jsdom": "^22.1.0", + "orval": "^7.9.0", + "postcss": "^8.4.29", + "tailwindcss": "^3.3.3", + "typescript": "^5.8.3", + "typescript-eslint": "^8.31.1", + "vite": "^6.3.4", + "vite-plugin-css-injected-by-js": "^3.5.2", + "vitest": "^3.1.2" + }, + "optionalDependencies": { + "@swc/core-linux-x64-gnu": "^1.11.24" + } } + } } diff --git a/package.json b/package.json index c6adc005f2..e5d1971f1a 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,15 @@ { - "workspaces": [ - "vscode/extension", - "web/client" - ], - "scripts": { - "lint": "npm run lint --workspaces", - "lint:fix": "npm run lint:fix --workspaces" - } + "workspaces": [ + "vscode/extension", + "web/client" + ], + "scripts": { + "fmt": "prettier --write .", + "fmt:check": "prettier --check .", + "lint": "npm run fmt:check && npm run lint --workspaces", + "lint:fix": "npm run fmt && npm run lint:fix --workspaces" + }, + "devDependencies": { + "prettier": "^3.5.2" + } } diff --git a/tooling/vscode/extensions.json b/tooling/vscode/extensions.json index dd01eb3555..9e5e0b733f 100644 --- a/tooling/vscode/extensions.json +++ b/tooling/vscode/extensions.json @@ -1,5 +1,9 @@ { // See http://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format - "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher", "ms-vscode.extension-test-runner"] + "recommendations": [ + "dbaeumer.vscode-eslint", + "amodio.tsl-problem-matcher", + "ms-vscode.extension-test-runner" + ] } diff --git a/tooling/vscode/launch.json b/tooling/vscode/launch.json index 6d80bcdfec..ee2f759dbf 100644 --- a/tooling/vscode/launch.json +++ b/tooling/vscode/launch.json @@ -3,20 +3,18 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [ - { - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "args": [ - "${workspaceFolder}/examples/sushi", - "--extensionDevelopmentPath=${workspaceFolder}/vscode/extension", - ], - "outFiles": [ - "${workspaceFolder}/vscode/extension/dist/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "${workspaceFolder}/examples/sushi", + "--extensionDevelopmentPath=${workspaceFolder}/vscode/extension" + ], + "outFiles": ["${workspaceFolder}/vscode/extension/dist/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + } + ] } diff --git a/tooling/vscode/settings.json b/tooling/vscode/settings.json index 40dde5ba6a..2c1f18f540 100644 --- a/tooling/vscode/settings.json +++ b/tooling/vscode/settings.json @@ -1,17 +1,17 @@ // Place your settings in this file to overwrite default and user settings. { - "files.exclude": { - "vscode/extension/out": false, // set this to true to hide the "out" folder with the compiled JS files - "vscode/extension/dist": false, // set this to true to hide the "dist" folder with the compiled JS files - "vscode/react/node_modules": false, - "vscode/react/dist": false - }, - "search.exclude": { - "vscode/extension/out": true, // set this to false to include "out" folder in search results - "vscode/extension/dist": true, // set this to false to include "dist" folder in search results - "vscode/react/node_modules": true, - "vscode/react/dist": true - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" + "files.exclude": { + "vscode/extension/out": false, // set this to true to hide the "out" folder with the compiled JS files + "vscode/extension/dist": false, // set this to true to hide the "dist" folder with the compiled JS files + "vscode/react/node_modules": false, + "vscode/react/dist": false + }, + "search.exclude": { + "vscode/extension/out": true, // set this to false to include "out" folder in search results + "vscode/extension/dist": true, // set this to false to include "dist" folder in search results + "vscode/react/node_modules": true, + "vscode/react/dist": true + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off" } diff --git a/tooling/vscode/tasks.json b/tooling/vscode/tasks.json index 756cffddbc..0b409a74a4 100644 --- a/tooling/vscode/tasks.json +++ b/tooling/vscode/tasks.json @@ -1,55 +1,52 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format { - "version": "2.0.0", - "tasks": [ - { - "label": "extension-watch", - "type": "npm", - "script": "watch", - "problemMatcher": "$ts-webpack-watch", - "isBackground": true, - "presentation": { - "reveal": "never", - "group": "watchers" - }, - "group": { - "kind": "build", - }, - "options": { - "cwd": "${workspaceFolder}/vscode/extension" - } - }, - { - "label": "extension-watch-develop", - "group": { - "kind": "build", - "isDefault": true - }, - "dependsOn": ["extension-watch"], - "dependsOrder": "parallel" - }, - { - "type": "npm", - "script": "watch-tests", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "presentation": { - "reveal": "never", - "group": "watchers" - }, - "group": "build", - "options": { - "cwd": "${workspaceFolder}/vscode/extension" - } - }, - { - "label": "tasks: watch-tests", - "dependsOn": [ - "npm: watch", - "npm: watch-tests" - ], - "problemMatcher": [] - } - ] + "version": "2.0.0", + "tasks": [ + { + "label": "extension-watch", + "type": "npm", + "script": "watch", + "problemMatcher": "$ts-webpack-watch", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + }, + "group": { + "kind": "build" + }, + "options": { + "cwd": "${workspaceFolder}/vscode/extension" + } + }, + { + "label": "extension-watch-develop", + "group": { + "kind": "build", + "isDefault": true + }, + "dependsOn": ["extension-watch"], + "dependsOrder": "parallel" + }, + { + "type": "npm", + "script": "watch-tests", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + }, + "group": "build", + "options": { + "cwd": "${workspaceFolder}/vscode/extension" + } + }, + { + "label": "tasks: watch-tests", + "dependsOn": ["npm: watch", "npm: watch-tests"], + "problemMatcher": [] + } + ] } diff --git a/vscode/extension/.vscode-test.mjs b/vscode/extension/.vscode-test.mjs index b62ba25f01..6a36b3f6e8 100644 --- a/vscode/extension/.vscode-test.mjs +++ b/vscode/extension/.vscode-test.mjs @@ -1,5 +1,5 @@ -import { defineConfig } from '@vscode/test-cli'; +import { defineConfig } from '@vscode/test-cli' export default defineConfig({ - files: 'out/test/**/*.test.js', -}); + files: 'out/test/**/*.test.js', +}) diff --git a/vscode/extension/esbuild.js b/vscode/extension/esbuild.js index 9933811e7e..40a11421e2 100644 --- a/vscode/extension/esbuild.js +++ b/vscode/extension/esbuild.js @@ -1,7 +1,7 @@ -const esbuild = require('esbuild'); +const esbuild = require('esbuild') -const production = process.argv.includes('--production'); -const watch = process.argv.includes('--watch'); +const production = process.argv.includes('--production') +const watch = process.argv.includes('--watch') async function main() { const ctx = await esbuild.context({ @@ -17,14 +17,14 @@ async function main() { logLevel: 'warning', plugins: [ /* add to the end of plugins array */ - esbuildProblemMatcherPlugin - ] - }); + esbuildProblemMatcherPlugin, + ], + }) if (watch) { - await ctx.watch(); + await ctx.watch() } else { - await ctx.rebuild(); - await ctx.dispose(); + await ctx.rebuild() + await ctx.dispose() } } @@ -36,20 +36,22 @@ const esbuildProblemMatcherPlugin = { setup(build) { build.onStart(() => { - console.log('[watch] build started'); - }); + console.log('[watch] build started') + }) build.onEnd(result => { result.errors.forEach(({ text, location }) => { - console.error(`✘ [ERROR] ${text}`); - if (location == null) return; - console.error(` ${location.file}:${location.line}:${location.column}:`); - }); - console.log('[watch] build finished'); - }); - } -}; + console.error(`✘ [ERROR] ${text}`) + if (location == null) return + console.error( + ` ${location.file}:${location.line}:${location.column}:`, + ) + }) + console.log('[watch] build finished') + }) + }, +} main().catch(e => { - console.error(e); - process.exit(1); -}); + console.error(e) + process.exit(1) +}) diff --git a/vscode/extension/eslint.config.mjs b/vscode/extension/eslint.config.mjs index b1b7e34e80..d8625b2e6c 100644 --- a/vscode/extension/eslint.config.mjs +++ b/vscode/extension/eslint.config.mjs @@ -1,29 +1,35 @@ -import typescriptEslint from "@typescript-eslint/eslint-plugin"; -import tsParser from "@typescript-eslint/parser"; +import typescriptEslint from '@typescript-eslint/eslint-plugin' +import tsParser from '@typescript-eslint/parser' -export default [{ - files: ["**/*.ts"], -}, { +export default [ + { + files: ['**/*.ts'], + }, + { plugins: { - "@typescript-eslint": typescriptEslint, + '@typescript-eslint': typescriptEslint, }, languageOptions: { - parser: tsParser, - ecmaVersion: 2022, - sourceType: "module", + parser: tsParser, + ecmaVersion: 2022, + sourceType: 'module', }, rules: { - "@typescript-eslint/naming-convention": ["warn", { - selector: "import", - format: ["camelCase", "PascalCase"], - }], + '@typescript-eslint/naming-convention': [ + 'warn', + { + selector: 'import', + format: ['camelCase', 'PascalCase'], + }, + ], - curly: "error", - eqeqeq: "error", - "no-throw-literal": "error", - semi: ["error", "never"], - "no-shadow": "error", + curly: 'error', + eqeqeq: 'error', + 'no-throw-literal': 'error', + semi: ['error', 'never'], + 'no-shadow': 'error', }, -}]; \ No newline at end of file + }, +] diff --git a/vscode/extension/src/auth/auth.ts b/vscode/extension/src/auth/auth.ts index 0f9e124ea9..71366d4099 100644 --- a/vscode/extension/src/auth/auth.ts +++ b/vscode/extension/src/auth/auth.ts @@ -7,16 +7,16 @@ import { Event, EventEmitter, window, -} from "vscode" -import { get_tcloud_bin } from "../utilities/sqlmesh/sqlmesh" -import { err, isErr, ok, Result } from "../utilities/functional/result" -import { execAsync } from "../utilities/exec" -import { getProjectRoot } from "../utilities/common/utilities" -import z from "zod" -import { traceError } from "../utilities/common/log" +} from 'vscode' +import { get_tcloud_bin } from '../utilities/sqlmesh/sqlmesh' +import { err, isErr, ok, Result } from '../utilities/functional/result' +import { execAsync } from '../utilities/exec' +import { getProjectRoot } from '../utilities/common/utilities' +import z from 'zod' +import { traceError } from '../utilities/common/log' -export const AUTH_TYPE = "tobikodata" -export const AUTH_NAME = "Tobiko" +export const AUTH_TYPE = 'tobikodata' +export const AUTH_NAME = 'Tobiko' const tokenSchema = z.object({ iss: z.string(), @@ -33,7 +33,7 @@ const statusResponseSchema = z.object({ id_token: tokenSchema.optional().nullable(), }) -type StatusResponse = z.infer; +type StatusResponse = z.infer const loginUrlResponseSchema = z.object({ url: z.string(), @@ -73,13 +73,13 @@ export class AuthenticationProviderTobikoCloud const tcloudBinPath = tcloudBin.value const result = await execAsync( tcloudBinPath, - ["auth", "vscode", "status"], + ['auth', 'vscode', 'status'], { cwd: workspacePath.uri.fsPath, - } + }, ) if (result.exitCode !== 0) { - return err("Failed to get tcloud auth status") + return err('Failed to get tcloud auth status') } const status = result.stdout const statusToJson = JSON.parse(status) @@ -98,7 +98,7 @@ export class AuthenticationProviderTobikoCloud } const token = statusResponse.id_token if (!token) { - throw new Error("Invalid state from tcloud, failed to get token.") + throw new Error('Invalid state from tcloud, failed to get token.') } const session = { id: token.email, @@ -106,8 +106,8 @@ export class AuthenticationProviderTobikoCloud id: token.sub, label: token.email, }, - scopes: token.scope.split(" "), - accessToken: "", + scopes: token.scope.split(' '), + accessToken: '', } return [session] } @@ -116,24 +116,24 @@ export class AuthenticationProviderTobikoCloud await this.sign_in_oauth_flow() const status = await this.get_status() if (isErr(status)) { - throw new Error("Failed to get tcloud auth status") + throw new Error('Failed to get tcloud auth status') } const statusResponse = status.value if (!statusResponse.is_logged_in) { - throw new Error("Failed to login to tcloud") + throw new Error('Failed to login to tcloud') } const token = statusResponse.id_token if (!token) { - throw new Error("Failed to get tcloud token") + throw new Error('Failed to get tcloud token') } const session: AuthenticationSession = { id: token.email, account: { id: token.email, - label: "Tobiko", + label: 'Tobiko', }, - scopes: token.scope.split(" "), - accessToken: "", + scopes: token.scope.split(' '), + accessToken: '', } this._sessionChangeEmitter.fire({ added: [session], @@ -149,14 +149,14 @@ export class AuthenticationProviderTobikoCloud const tcloudBin = await get_tcloud_bin() const workspacePath = await getProjectRoot() if (isErr(tcloudBin)) { - throw new Error("Failed to get tcloud bin") + throw new Error('Failed to get tcloud bin') } const tcloudBinPath = tcloudBin.value - const result = await execAsync(tcloudBinPath, ["auth", "logout"], { + const result = await execAsync(tcloudBinPath, ['auth', 'logout'], { cwd: workspacePath.uri.fsPath, }) if (result.exitCode !== 0) { - throw new Error("Failed to logout from tcloud") + throw new Error('Failed to logout from tcloud') } // Emit event with the actual sessions that were removed @@ -173,18 +173,18 @@ export class AuthenticationProviderTobikoCloud const workspacePath = await getProjectRoot() const tcloudBin = await get_tcloud_bin() if (isErr(tcloudBin)) { - throw new Error("Failed to get tcloud bin") + throw new Error('Failed to get tcloud bin') } const tcloudBinPath = tcloudBin.value const result = await execAsync( tcloudBinPath, - ["auth", "vscode", "login-url"], + ['auth', 'vscode', 'login-url'], { cwd: workspacePath.uri.fsPath, - } + }, ) if (result.exitCode !== 0) { - throw new Error("Failed to get tcloud login url") + throw new Error('Failed to get tcloud login url') } try { @@ -193,37 +193,37 @@ export class AuthenticationProviderTobikoCloud const url = urlCode.url if (!url) { - throw new Error("Invalid login URL received") + throw new Error('Invalid login URL received') } const ac = new AbortController() const timeout = setTimeout(() => ac.abort(), 1000 * 60 * 5) const backgroundServerForLogin = execAsync( tcloudBinPath, - ["auth", "vscode", "start-server", urlCode.verifier_code], + ['auth', 'vscode', 'start-server', urlCode.verifier_code], { cwd: workspacePath.uri.fsPath, signal: ac.signal, - } + }, ) const messageResult = await window.showInformationMessage( - "Please login to Tobiko Cloud", + 'Please login to Tobiko Cloud', { modal: true, }, - "Sign in with browser", - "Cancel" + 'Sign in with browser', + 'Cancel', ) - if (messageResult === "Sign in with browser") { + if (messageResult === 'Sign in with browser') { await env.openExternal(Uri.parse(url)) } else { // Always abort the server if not proceeding with sign in ac.abort() clearTimeout(timeout) - if (messageResult === "Cancel") { - throw new Error("Login cancelled") + if (messageResult === 'Cancel') { + throw new Error('Login cancelled') } return } @@ -231,9 +231,7 @@ export class AuthenticationProviderTobikoCloud try { const output = await backgroundServerForLogin if (output.exitCode !== 0) { - throw new Error( - `Failed to complete authentication: ${output.stderr}` - ) + throw new Error(`Failed to complete authentication: ${output.stderr}`) } // Get updated session and notify about the change const sessions = await this.getSessions() @@ -245,8 +243,8 @@ export class AuthenticationProviderTobikoCloud }) } } catch (error) { - if (error instanceof Error && error.name === "AbortError") { - throw new Error("Authentication timeout or aborted") + if (error instanceof Error && error.name === 'AbortError') { + throw new Error('Authentication timeout or aborted') } traceError(`Server error: ${error}`) throw error @@ -254,11 +252,11 @@ export class AuthenticationProviderTobikoCloud clearTimeout(timeout) } } catch (error) { - if (error instanceof Error && error.message === "Login cancelled") { + if (error instanceof Error && error.message === 'Login cancelled') { throw error } traceError(`Authentication flow error: ${error}`) - throw new Error("Failed to complete authentication flow") + throw new Error('Failed to complete authentication flow') } } @@ -266,18 +264,18 @@ export class AuthenticationProviderTobikoCloud const workspacePath = await getProjectRoot() const tcloudBin = await get_tcloud_bin() if (isErr(tcloudBin)) { - throw new Error("Failed to get tcloud bin") + throw new Error('Failed to get tcloud bin') } const tcloudBinPath = tcloudBin.value const result = await execAsync( tcloudBinPath, - ["auth", "vscode", "device"], + ['auth', 'vscode', 'device'], { cwd: workspacePath.uri.fsPath, - } + }, ) if (result.exitCode !== 0) { - throw new Error("Failed to get device code") + throw new Error('Failed to get device code') } try { @@ -288,11 +286,11 @@ export class AuthenticationProviderTobikoCloud const timeout = setTimeout(() => ac.abort(), 1000 * 60 * 5) const waiting = execAsync( tcloudBinPath, - ["auth", "vscode", "poll_device", deviceCodeResponse.device_code], + ['auth', 'vscode', 'poll_device', deviceCodeResponse.device_code], { cwd: workspacePath.uri.fsPath, signal: ac.signal, - } + }, ) const messageResult = await window.showInformationMessage( @@ -300,18 +298,18 @@ export class AuthenticationProviderTobikoCloud { modal: true, }, - "Open browser", - "Cancel" + 'Open browser', + 'Cancel', ) - if (messageResult === "Open browser") { + if (messageResult === 'Open browser') { await env.openExternal( - Uri.parse(deviceCodeResponse.verification_uri_complete) + Uri.parse(deviceCodeResponse.verification_uri_complete), ) } - if (messageResult === "Cancel") { + if (messageResult === 'Cancel') { ac.abort() - throw new Error("Login cancelled") + throw new Error('Login cancelled') } try { @@ -337,7 +335,7 @@ export class AuthenticationProviderTobikoCloud } } catch (error) { traceError(`JSON parsing error: ${error}`) - throw new Error("Failed to parse device code response") + throw new Error('Failed to parse device code response') } } } diff --git a/vscode/extension/src/commands/format.ts b/vscode/extension/src/commands/format.ts index 777335c505..ac342f3849 100644 --- a/vscode/extension/src/commands/format.ts +++ b/vscode/extension/src/commands/format.ts @@ -1,27 +1,27 @@ -import { traceLog } from "../utilities/common/log" -import { execSync } from "child_process" -import { sqlmesh_exec } from "../utilities/sqlmesh/sqlmesh" -import { err, isErr, ok, Result } from "../utilities/functional/result" -import * as vscode from "vscode" -import { ErrorType, handleNotSginedInError } from "../utilities/errors" -import { AuthenticationProviderTobikoCloud } from "../auth/auth" +import { traceLog } from '../utilities/common/log' +import { execSync } from 'child_process' +import { sqlmesh_exec } from '../utilities/sqlmesh/sqlmesh' +import { err, isErr, ok, Result } from '../utilities/functional/result' +import * as vscode from 'vscode' +import { ErrorType, handleNotSginedInError } from '../utilities/errors' +import { AuthenticationProviderTobikoCloud } from '../auth/auth' export const format = (authProvider: AuthenticationProviderTobikoCloud) => async () => { - traceLog("Calling format") + traceLog('Calling format') const out = await internalFormat() if (isErr(out)) { - if (out.error.type === "not_signed_in") { + if (out.error.type === 'not_signed_in') { handleNotSginedInError(authProvider) return } else { vscode.window.showErrorMessage( - `Project format failed: ${out.error.message}` + `Project format failed: ${out.error.message}`, ) return } } - vscode.window.showInformationMessage("Project formatted successfully") + vscode.window.showInformationMessage('Project formatted successfully') } const internalFormat = async (): Promise> => { @@ -31,14 +31,14 @@ const internalFormat = async (): Promise> => { return exec } execSync(`${exec.value.bin} format`, { - encoding: "utf-8", + encoding: 'utf-8', cwd: exec.value.workspacePath, env: exec.value.env, }) return ok(0) } catch (error: any) { return err({ - type: "generic", + type: 'generic', message: `Error executing sqlmesh format: ${error.message}`, }) } diff --git a/vscode/extension/src/commands/signin.ts b/vscode/extension/src/commands/signin.ts index d560b05a57..e59c2b2161 100644 --- a/vscode/extension/src/commands/signin.ts +++ b/vscode/extension/src/commands/signin.ts @@ -1,6 +1,6 @@ -import { AuthenticationProviderTobikoCloud } from "../auth/auth" -import * as vscode from "vscode" -import { isCodespaces } from "../utilities/isCodespaces" +import { AuthenticationProviderTobikoCloud } from '../auth/auth' +import * as vscode from 'vscode' +import { isCodespaces } from '../utilities/isCodespaces' export const signIn = (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { @@ -9,5 +9,5 @@ export const signIn = } else { await authenticationProvider.createSession() } - await vscode.window.showInformationMessage("Signed in successfully") + await vscode.window.showInformationMessage('Signed in successfully') } diff --git a/vscode/extension/src/commands/signinSpecifyFlow.ts b/vscode/extension/src/commands/signinSpecifyFlow.ts index a77930af96..8e277b28b0 100644 --- a/vscode/extension/src/commands/signinSpecifyFlow.ts +++ b/vscode/extension/src/commands/signinSpecifyFlow.ts @@ -1,33 +1,37 @@ -import { AuthenticationProviderTobikoCloud } from "../auth/auth" -import { traceInfo } from "../utilities/common/log" -import { window } from "vscode" +import { AuthenticationProviderTobikoCloud } from '../auth/auth' +import { traceInfo } from '../utilities/common/log' +import { window } from 'vscode' -export const signInSpecifyFlow = (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { - traceInfo("Sign in specify flow") +export const signInSpecifyFlow = + (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { + traceInfo('Sign in specify flow') const flowOptions = [ - { label: "OAuth Flow", description: "Sign in using OAuth flow in your browser" }, - { label: "Device Flow", description: "Sign in using a device code" } + { + label: 'OAuth Flow', + description: 'Sign in using OAuth flow in your browser', + }, + { label: 'Device Flow', description: 'Sign in using a device code' }, ] const selectedFlow = await window.showQuickPick(flowOptions, { - placeHolder: "Select authentication flow method", - ignoreFocusOut: true + placeHolder: 'Select authentication flow method', + ignoreFocusOut: true, }) if (!selectedFlow) { - traceInfo("Sign in cancelled by user") - return + traceInfo('Sign in cancelled by user') + return } - if (selectedFlow.label === "OAuth Flow") { - await authenticationProvider.sign_in_oauth_flow() - await authenticationProvider.getSessions() - await window.showInformationMessage("Sign in success") - return - } else if (selectedFlow.label === "Device Flow") { - await authenticationProvider.sign_in_device_flow() - await authenticationProvider.getSessions() - await window.showInformationMessage("Sign in success") - return + if (selectedFlow.label === 'OAuth Flow') { + await authenticationProvider.sign_in_oauth_flow() + await authenticationProvider.getSessions() + await window.showInformationMessage('Sign in success') + return + } else if (selectedFlow.label === 'Device Flow') { + await authenticationProvider.sign_in_device_flow() + await authenticationProvider.getSessions() + await window.showInformationMessage('Sign in success') + return } else { - traceInfo("Invalid flow selected") - return + traceInfo('Invalid flow selected') + return } -} \ No newline at end of file + } diff --git a/vscode/extension/src/commands/signout.ts b/vscode/extension/src/commands/signout.ts index 8e1f6cba5f..614723a70f 100644 --- a/vscode/extension/src/commands/signout.ts +++ b/vscode/extension/src/commands/signout.ts @@ -1,7 +1,8 @@ -import { AuthenticationProviderTobikoCloud } from "../auth/auth" -import * as vscode from "vscode" +import { AuthenticationProviderTobikoCloud } from '../auth/auth' +import * as vscode from 'vscode' -export const signOut = (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { +export const signOut = + (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { await authenticationProvider.removeSession() - await vscode.window.showInformationMessage("Signed out successfully") -} \ No newline at end of file + await vscode.window.showInformationMessage('Signed out successfully') + } diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index b04370ea6c..4b010da8b1 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -1,62 +1,58 @@ -import { format } from "./commands/format" -import * as vscode from "vscode" +import { format } from './commands/format' +import * as vscode from 'vscode' import { createOutputChannel, onDidChangeConfiguration, registerCommand, -} from "./utilities/common/vscodeapi" -import { - registerLogger, - traceInfo, - traceVerbose, -} from "./utilities/common/log" -import { onDidChangePythonInterpreter } from "./utilities/common/python" -import { LSPClient } from "./lsp/lsp" -import { AuthenticationProviderTobikoCloud } from "./auth/auth" -import { signOut } from "./commands/signout" -import { signIn } from "./commands/signin" -import { signInSpecifyFlow } from "./commands/signinSpecifyFlow" -import { isErr } from "./utilities/functional/result" -import { handleNotSginedInError } from "./utilities/errors" +} from './utilities/common/vscodeapi' +import { registerLogger, traceInfo, traceVerbose } from './utilities/common/log' +import { onDidChangePythonInterpreter } from './utilities/common/python' +import { LSPClient } from './lsp/lsp' +import { AuthenticationProviderTobikoCloud } from './auth/auth' +import { signOut } from './commands/signout' +import { signIn } from './commands/signin' +import { signInSpecifyFlow } from './commands/signinSpecifyFlow' +import { isErr } from './utilities/functional/result' +import { handleNotSginedInError } from './utilities/errors' let lspClient: LSPClient | undefined // This method is called when your extension is activated // Your extension is activated the very first time the command is executed export async function activate(context: vscode.ExtensionContext) { - const extensionOutputChannel = createOutputChannel("sqlmesh") + const extensionOutputChannel = createOutputChannel('sqlmesh') context.subscriptions.push( extensionOutputChannel, - registerLogger(extensionOutputChannel) + registerLogger(extensionOutputChannel), ) - traceInfo("Activating SQLMesh extension") + traceInfo('Activating SQLMesh extension') - traceInfo("Registering authentication provider") + traceInfo('Registering authentication provider') const authProvider = new AuthenticationProviderTobikoCloud() context.subscriptions.push( vscode.authentication.registerAuthenticationProvider( AuthenticationProviderTobikoCloud.id, AuthenticationProviderTobikoCloud.name, authProvider, - { supportsMultipleAccounts: false } - ) + { supportsMultipleAccounts: false }, + ), ) - traceInfo("Authentication provider registered") + traceInfo('Authentication provider registered') context.subscriptions.push( - vscode.commands.registerCommand("sqlmesh.signin", signIn(authProvider)) + vscode.commands.registerCommand('sqlmesh.signin', signIn(authProvider)), ) context.subscriptions.push( vscode.commands.registerCommand( - "sqlmesh.signinSpecifyFlow", - signInSpecifyFlow(authProvider) - ) + 'sqlmesh.signinSpecifyFlow', + signInSpecifyFlow(authProvider), + ), ) context.subscriptions.push( - vscode.commands.registerCommand("sqlmesh.signout", signOut(authProvider)) + vscode.commands.registerCommand('sqlmesh.signout', signOut(authProvider)), ) context.subscriptions.push( - vscode.commands.registerCommand("sqlmesh.format", format(authProvider)) + vscode.commands.registerCommand('sqlmesh.format', format(authProvider)), ) lspClient = new LSPClient() @@ -68,7 +64,7 @@ export async function activate(context: vscode.ExtensionContext) { const restart = async () => { if (lspClient) { - traceVerbose("Restarting LSP client") + traceVerbose('Restarting LSP client') const restartResult = await lspClient.restart() if (isErr(restartResult)) { handleNotSginedInError(authProvider) @@ -85,10 +81,10 @@ export async function activate(context: vscode.ExtensionContext) { }), registerCommand(`sqlmesh.restart`, async () => { await restart() - }) + }), ) - traceInfo("Extension activated") + traceInfo('Extension activated') } // This method is called when your extension is deactivated diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index bdeea35272..2b3c8e9d61 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -1,89 +1,99 @@ -import { window, OutputChannel, Disposable } from "vscode" -import { ServerOptions, LanguageClientOptions, LanguageClient, TransportKind } from "vscode-languageclient/node" -import { sqlmesh_lsp_exec } from "../utilities/sqlmesh/sqlmesh" -import { err, isErr, ok, Result } from "../utilities/functional/result" -import { getWorkspaceFolders } from "../utilities/common/vscodeapi" -import { traceError } from "../utilities/common/log" -import { ErrorType } from "../utilities/errors" +import { window, OutputChannel, Disposable } from 'vscode' +import { + ServerOptions, + LanguageClientOptions, + LanguageClient, + TransportKind, +} from 'vscode-languageclient/node' +import { sqlmesh_lsp_exec } from '../utilities/sqlmesh/sqlmesh' +import { err, isErr, ok, Result } from '../utilities/functional/result' +import { getWorkspaceFolders } from '../utilities/common/vscodeapi' +import { traceError } from '../utilities/common/log' +import { ErrorType } from '../utilities/errors' let outputChannel: OutputChannel | undefined export class LSPClient implements Disposable { - private client: LanguageClient | undefined + private client: LanguageClient | undefined - constructor() { - this.client = undefined - } - - public async start(): Promise> { - if (!outputChannel) { - outputChannel = window.createOutputChannel('sqlmesh-lsp') - } + constructor() { + this.client = undefined + } - const sqlmesh = await sqlmesh_lsp_exec() - if (isErr(sqlmesh)) { - traceError(`Failed to get sqlmesh_lsp_exec: ${sqlmesh.error}`) - return sqlmesh - } - const workspaceFolders = getWorkspaceFolders() - if (workspaceFolders.length !== 1) { - traceError(`Invalid number of workspace folders: ${workspaceFolders.length}`) - return err({ - type: "generic", - message: "Invalid number of workspace folders", - }) - } - - let folder = workspaceFolders[0] - const workspacePath = workspaceFolders[0].uri.fsPath - let serverOptions: ServerOptions = { - run: { - command: sqlmesh.value.bin, - transport: TransportKind.stdio, - options: { - cwd: workspacePath, - }, - args: sqlmesh.value.args, - }, - debug: { - command: sqlmesh.value.bin, - transport: TransportKind.stdio, - options: { - cwd: workspacePath, - }, - args: sqlmesh.value.args, - } - } - let clientOptions: LanguageClientOptions = { - documentSelector: [ - { scheme: 'file', pattern: `**/*.sql` } - ], - workspaceFolder: folder, - diagnosticCollectionName: 'sqlmesh', - outputChannel: outputChannel, - // synchronize: { - // fileEvents: workspace.createFileSystemWatcher('**/*.{sql,py}'), - // } - } - - this.client = new LanguageClient('sqlmesh-lsp', 'SQLMesh Language Server', serverOptions, clientOptions) - await this.client.start() - return ok(undefined) + public async start(): Promise> { + if (!outputChannel) { + outputChannel = window.createOutputChannel('sqlmesh-lsp') } - public async restart(): Promise> { - await this.stop() - return await this.start() + const sqlmesh = await sqlmesh_lsp_exec() + if (isErr(sqlmesh)) { + traceError(`Failed to get sqlmesh_lsp_exec: ${sqlmesh.error}`) + return sqlmesh + } + const workspaceFolders = getWorkspaceFolders() + if (workspaceFolders.length !== 1) { + traceError( + `Invalid number of workspace folders: ${workspaceFolders.length}`, + ) + return err({ + type: 'generic', + message: 'Invalid number of workspace folders', + }) } - public async stop() { - if (this.client) { - await this.client.stop() - this.client = undefined - } + let folder = workspaceFolders[0] + const workspacePath = workspaceFolders[0].uri.fsPath + let serverOptions: ServerOptions = { + run: { + command: sqlmesh.value.bin, + transport: TransportKind.stdio, + options: { + cwd: workspacePath, + }, + args: sqlmesh.value.args, + }, + debug: { + command: sqlmesh.value.bin, + transport: TransportKind.stdio, + options: { + cwd: workspacePath, + }, + args: sqlmesh.value.args, + }, + } + let clientOptions: LanguageClientOptions = { + documentSelector: [{ scheme: 'file', pattern: `**/*.sql` }], + workspaceFolder: folder, + diagnosticCollectionName: 'sqlmesh', + outputChannel: outputChannel, + // synchronize: { + // fileEvents: workspace.createFileSystemWatcher('**/*.{sql,py}'), + // } } - public async dispose() { - await this.stop() + this.client = new LanguageClient( + 'sqlmesh-lsp', + 'SQLMesh Language Server', + serverOptions, + clientOptions, + ) + await this.client.start() + return ok(undefined) + } + + public async restart(): Promise> { + await this.stop() + return await this.start() + } + + public async stop() { + if (this.client) { + await this.client.stop() + this.client = undefined } + } + + public async dispose() { + await this.stop() + } } diff --git a/vscode/extension/src/test/extension.test.ts b/vscode/extension/src/test/extension.test.ts index f95e200482..101bd6dde3 100644 --- a/vscode/extension/src/test/extension.test.ts +++ b/vscode/extension/src/test/extension.test.ts @@ -6,10 +6,10 @@ import * as vscode from 'vscode' // import * as myExtension from '../../extension'; suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.') + vscode.window.showInformationMessage('Start all tests.') - test('Sample test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)) - assert.strictEqual(-1, [1, 2, 3].indexOf(0)) - }) + test('Sample test', () => { + assert.strictEqual(-1, [1, 2, 3].indexOf(5)) + assert.strictEqual(-1, [1, 2, 3].indexOf(0)) + }) }) diff --git a/vscode/extension/src/utilities/common/constants.ts b/vscode/extension/src/utilities/common/constants.ts index 1a968d1815..274b60bc9a 100644 --- a/vscode/extension/src/utilities/common/constants.ts +++ b/vscode/extension/src/utilities/common/constants.ts @@ -5,7 +5,20 @@ import * as path from 'path' const folderName = path.basename(__dirname) export const EXTENSION_ROOT_DIR = - folderName === 'common' ? path.dirname(path.dirname(__dirname)) : path.dirname(__dirname) -export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'bundled') -export const SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `lsp_server.py`) -export const DEBUG_SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `_debug_server.py`) \ No newline at end of file + folderName === 'common' + ? path.dirname(path.dirname(__dirname)) + : path.dirname(__dirname) +export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join( + EXTENSION_ROOT_DIR, + 'bundled', +) +export const SERVER_SCRIPT_PATH = path.join( + BUNDLED_PYTHON_SCRIPTS_DIR, + 'tool', + `lsp_server.py`, +) +export const DEBUG_SERVER_SCRIPT_PATH = path.join( + BUNDLED_PYTHON_SCRIPTS_DIR, + 'tool', + `_debug_server.py`, +) diff --git a/vscode/extension/src/utilities/common/log.ts b/vscode/extension/src/utilities/common/log.ts index e2172f1813..9498be3432 100644 --- a/vscode/extension/src/utilities/common/log.ts +++ b/vscode/extension/src/utilities/common/log.ts @@ -3,57 +3,57 @@ import * as util from 'util' import { Disposable, LogOutputChannel } from 'vscode' -type Arguments = unknown[]; +type Arguments = unknown[] class OutputChannelLogger { - constructor(private readonly channel: LogOutputChannel) {} + constructor(private readonly channel: LogOutputChannel) {} - public traceLog(...data: Arguments): void { - this.channel.appendLine(util.format(...data)) - } + public traceLog(...data: Arguments): void { + this.channel.appendLine(util.format(...data)) + } - public traceError(...data: Arguments): void { - this.channel.error(util.format(...data)) - } + public traceError(...data: Arguments): void { + this.channel.error(util.format(...data)) + } - public traceWarn(...data: Arguments): void { - this.channel.warn(util.format(...data)) - } + public traceWarn(...data: Arguments): void { + this.channel.warn(util.format(...data)) + } - public traceInfo(...data: Arguments): void { - this.channel.info(util.format(...data)) - } + public traceInfo(...data: Arguments): void { + this.channel.info(util.format(...data)) + } - public traceVerbose(...data: Arguments): void { - this.channel.debug(util.format(...data)) - } + public traceVerbose(...data: Arguments): void { + this.channel.debug(util.format(...data)) + } } let channel: OutputChannelLogger | undefined export function registerLogger(logChannel: LogOutputChannel): Disposable { - channel = new OutputChannelLogger(logChannel) - return { - dispose: () => { - channel = undefined - }, - } + channel = new OutputChannelLogger(logChannel) + return { + dispose: () => { + channel = undefined + }, + } } export function traceLog(...args: Arguments): void { - channel?.traceLog(...args) + channel?.traceLog(...args) } export function traceError(...args: Arguments): void { - channel?.traceError(...args) + channel?.traceError(...args) } export function traceWarn(...args: Arguments): void { - channel?.traceWarn(...args) + channel?.traceWarn(...args) } export function traceInfo(...args: Arguments): void { - channel?.traceInfo(...args) + channel?.traceInfo(...args) } export function traceVerbose(...args: Arguments): void { - channel?.traceVerbose(...args) -} \ No newline at end of file + channel?.traceVerbose(...args) +} diff --git a/vscode/extension/src/utilities/common/python.ts b/vscode/extension/src/utilities/common/python.ts index 0a5db8698e..a093a97128 100644 --- a/vscode/extension/src/utilities/common/python.ts +++ b/vscode/extension/src/utilities/common/python.ts @@ -1,100 +1,118 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - import { commands, Disposable, Event, EventEmitter, Uri } from 'vscode' import { traceError, traceLog } from './log' import { PythonExtension, ResolvedEnvironment } from '@vscode/python-extension' import path from 'path' export interface IInterpreterDetails { - path?: string[]; - resource?: Uri; - isVirtualEnvironment?: boolean; - binPath?: string; + path?: string[] + resource?: Uri + isVirtualEnvironment?: boolean + binPath?: string } -const onDidChangePythonInterpreterEvent = new EventEmitter() -export const onDidChangePythonInterpreter: Event = onDidChangePythonInterpreterEvent.event +const onDidChangePythonInterpreterEvent = + new EventEmitter() +export const onDidChangePythonInterpreter: Event = + onDidChangePythonInterpreterEvent.event let _api: PythonExtension | undefined async function getPythonExtensionAPI(): Promise { - if (_api) { - return _api - } - _api = await PythonExtension.api() + if (_api) { return _api + } + _api = await PythonExtension.api() + return _api } -export async function initializePython(disposables: Disposable[]): Promise { - try { - const api = await getPythonExtensionAPI() +export async function initializePython( + disposables: Disposable[], +): Promise { + try { + const api = await getPythonExtensionAPI() + + if (api) { + disposables.push( + api.environments.onDidChangeActiveEnvironmentPath(async e => { + const environment = await api.environments.resolveEnvironment(e.path) + const isVirtualEnv = environment?.environment !== undefined + const binPath = isVirtualEnv + ? environment?.environment?.folderUri.fsPath + : undefined - if (api) { - disposables.push( - api.environments.onDidChangeActiveEnvironmentPath(async (e) => { - const environment = await api.environments.resolveEnvironment(e.path) - const isVirtualEnv = environment?.environment !== undefined - const binPath = isVirtualEnv ? environment?.environment?.folderUri.fsPath : undefined - - onDidChangePythonInterpreterEvent.fire({ - path: [e.path], - resource: e.resource?.uri, - isVirtualEnvironment: isVirtualEnv, - binPath - }) - }), - ) + onDidChangePythonInterpreterEvent.fire({ + path: [e.path], + resource: e.resource?.uri, + isVirtualEnvironment: isVirtualEnv, + binPath, + }) + }), + ) - traceLog('Waiting for interpreter from python extension.') - onDidChangePythonInterpreterEvent.fire(await getInterpreterDetails()) - } - } catch (error) { - traceError('Error initializing python: ', error) + traceLog('Waiting for interpreter from python extension.') + onDidChangePythonInterpreterEvent.fire(await getInterpreterDetails()) } + } catch (error) { + traceError('Error initializing python: ', error) + } } -export async function resolveInterpreter(interpreter: string[]): Promise { - const api = await getPythonExtensionAPI() - return api?.environments.resolveEnvironment(interpreter[0]) +export async function resolveInterpreter( + interpreter: string[], +): Promise { + const api = await getPythonExtensionAPI() + return api?.environments.resolveEnvironment(interpreter[0]) } -export async function getInterpreterDetails(resource?: Uri): Promise { - const api = await getPythonExtensionAPI() - const environment = await api?.environments.resolveEnvironment( - api?.environments.getActiveEnvironmentPath(resource), - ) - if (environment?.executable.uri && checkVersion(environment)) { - const isVirtualEnv = environment.environment !== undefined - const binPath = isVirtualEnv ? environment.environment?.folderUri.fsPath : undefined - - return { - path: [environment?.executable.uri.fsPath], - resource, - isVirtualEnvironment: isVirtualEnv, - binPath: binPath ? path.join(binPath, "bin") : undefined - } +export async function getInterpreterDetails( + resource?: Uri, +): Promise { + const api = await getPythonExtensionAPI() + const environment = await api?.environments.resolveEnvironment( + api?.environments.getActiveEnvironmentPath(resource), + ) + if (environment?.executable.uri && checkVersion(environment)) { + const isVirtualEnv = environment.environment !== undefined + const binPath = isVirtualEnv + ? environment.environment?.folderUri.fsPath + : undefined + + return { + path: [environment?.executable.uri.fsPath], + resource, + isVirtualEnvironment: isVirtualEnv, + binPath: binPath ? path.join(binPath, 'bin') : undefined, } - return { path: undefined, resource } + } + return { path: undefined, resource } } export async function getDebuggerPath(): Promise { - const api = await getPythonExtensionAPI() - return api?.debug.getDebuggerPackagePath() + const api = await getPythonExtensionAPI() + return api?.debug.getDebuggerPackagePath() } -export async function runPythonExtensionCommand(command: string, ...rest: any[]) { - await getPythonExtensionAPI() - return await commands.executeCommand(command, ...rest) +export async function runPythonExtensionCommand( + command: string, + ...rest: any[] +) { + await getPythonExtensionAPI() + return await commands.executeCommand(command, ...rest) } -export function checkVersion(resolved: ResolvedEnvironment | undefined): boolean { - const version = resolved?.version - if (version?.major === 3 && version?.minor >= 8) { - return true - } - traceError(`Python version ${version?.major}.${version?.minor} is not supported.`) - traceError(`Selected python path: ${resolved?.executable.uri?.fsPath}`) - traceError('Supported versions are 3.8 and above.') - return false -} \ No newline at end of file +export function checkVersion( + resolved: ResolvedEnvironment | undefined, +): boolean { + const version = resolved?.version + if (version?.major === 3 && version?.minor >= 8) { + return true + } + traceError( + `Python version ${version?.major}.${version?.minor} is not supported.`, + ) + traceError(`Selected python path: ${resolved?.executable.uri?.fsPath}`) + traceError('Supported versions are 3.8 and above.') + return false +} diff --git a/vscode/extension/src/utilities/common/settings.ts b/vscode/extension/src/utilities/common/settings.ts index 3f689ce5f8..000dfc5534 100644 --- a/vscode/extension/src/utilities/common/settings.ts +++ b/vscode/extension/src/utilities/common/settings.ts @@ -1,114 +1,150 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ConfigurationChangeEvent, ConfigurationScope, WorkspaceConfiguration, WorkspaceFolder } from 'vscode' +import { + ConfigurationChangeEvent, + ConfigurationScope, + WorkspaceConfiguration, + WorkspaceFolder, +} from 'vscode' import { getInterpreterDetails } from './python' import { getConfiguration, getWorkspaceFolders } from './vscodeapi' export interface ISettings { - cwd: string; - workspace: string; - args: string[]; - path: string[]; - interpreter: string[]; - importStrategy: string; - showNotifications: string; + cwd: string + workspace: string + args: string[] + path: string[] + interpreter: string[] + importStrategy: string + showNotifications: string } -export function getExtensionSettings(namespace: string, includeInterpreter?: boolean): Promise { - return Promise.all(getWorkspaceFolders().map((w) => getWorkspaceSettings(namespace, w, includeInterpreter))) +export function getExtensionSettings( + namespace: string, + includeInterpreter?: boolean, +): Promise { + return Promise.all( + getWorkspaceFolders().map(w => + getWorkspaceSettings(namespace, w, includeInterpreter), + ), + ) } -function resolveVariables(value: string[], workspace?: WorkspaceFolder): string[] { - const substitutions = new Map() - const home = process.env.HOME || process.env.USERPROFILE - if (home) { - substitutions.set('${userHome}', home) - } - if (workspace) { - substitutions.set('${workspaceFolder}', workspace.uri.fsPath) - } - substitutions.set('${cwd}', process.cwd()) - getWorkspaceFolders().forEach((w) => { - substitutions.set('${workspaceFolder:' + w.name + '}', w.uri.fsPath) - }) +function resolveVariables( + value: string[], + workspace?: WorkspaceFolder, +): string[] { + const substitutions = new Map() + const home = process.env.HOME || process.env.USERPROFILE + if (home) { + substitutions.set('${userHome}', home) + } + if (workspace) { + substitutions.set('${workspaceFolder}', workspace.uri.fsPath) + } + substitutions.set('${cwd}', process.cwd()) + getWorkspaceFolders().forEach(w => { + substitutions.set('${workspaceFolder:' + w.name + '}', w.uri.fsPath) + }) - return value.map((s) => { - for (const [k, v] of substitutions) { - s = s.replace(k, v) - } - return s - }) + return value.map(s => { + for (const [k, v] of substitutions) { + s = s.replace(k, v) + } + return s + }) } -export function getInterpreterFromSetting(namespace: string, scope?: ConfigurationScope) { - const config = getConfiguration(namespace, scope) - return config.get('interpreter') +export function getInterpreterFromSetting( + namespace: string, + scope?: ConfigurationScope, +) { + const config = getConfiguration(namespace, scope) + return config.get('interpreter') } export async function getWorkspaceSettings( - namespace: string, - workspace: WorkspaceFolder, - includeInterpreter?: boolean, + namespace: string, + workspace: WorkspaceFolder, + includeInterpreter?: boolean, ): Promise { - const config = getConfiguration(namespace, workspace.uri) + const config = getConfiguration(namespace, workspace.uri) - let interpreter: string[] = [] - if (includeInterpreter) { - interpreter = getInterpreterFromSetting(namespace, workspace) ?? [] - if (interpreter.length === 0) { - interpreter = (await getInterpreterDetails(workspace.uri)).path ?? [] - } + let interpreter: string[] = [] + if (includeInterpreter) { + interpreter = getInterpreterFromSetting(namespace, workspace) ?? [] + if (interpreter.length === 0) { + interpreter = (await getInterpreterDetails(workspace.uri)).path ?? [] } + } - const workspaceSetting = { - cwd: workspace.uri.fsPath, - workspace: workspace.uri.toString(), - args: resolveVariables(config.get(`args`) ?? [], workspace), - path: resolveVariables(config.get(`path`) ?? [], workspace), - interpreter: resolveVariables(interpreter, workspace), - importStrategy: config.get(`importStrategy`) ?? 'useBundled', - showNotifications: config.get(`showNotifications`) ?? 'off', - } - return workspaceSetting + const workspaceSetting = { + cwd: workspace.uri.fsPath, + workspace: workspace.uri.toString(), + args: resolveVariables(config.get(`args`) ?? [], workspace), + path: resolveVariables(config.get(`path`) ?? [], workspace), + interpreter: resolveVariables(interpreter, workspace), + importStrategy: config.get(`importStrategy`) ?? 'useBundled', + showNotifications: config.get(`showNotifications`) ?? 'off', + } + return workspaceSetting } -function getGlobalValue(config: WorkspaceConfiguration, key: string, defaultValue: T): T { - const inspect = config.inspect(key) - return inspect?.globalValue ?? inspect?.defaultValue ?? defaultValue +function getGlobalValue( + config: WorkspaceConfiguration, + key: string, + defaultValue: T, +): T { + const inspect = config.inspect(key) + return inspect?.globalValue ?? inspect?.defaultValue ?? defaultValue } -export async function getGlobalSettings(namespace: string, includeInterpreter?: boolean): Promise { - const config = getConfiguration(namespace) +export async function getGlobalSettings( + namespace: string, + includeInterpreter?: boolean, +): Promise { + const config = getConfiguration(namespace) - let interpreter: string[] = [] - if (includeInterpreter) { - interpreter = getGlobalValue(config, 'interpreter', []) - if (interpreter === undefined || interpreter.length === 0) { - interpreter = (await getInterpreterDetails()).path ?? [] - } + let interpreter: string[] = [] + if (includeInterpreter) { + interpreter = getGlobalValue(config, 'interpreter', []) + if (interpreter === undefined || interpreter.length === 0) { + interpreter = (await getInterpreterDetails()).path ?? [] } + } - const setting = { - cwd: process.cwd(), - workspace: process.cwd(), - args: getGlobalValue(config, 'args', []), - path: getGlobalValue(config, 'path', []), - interpreter: interpreter, - importStrategy: getGlobalValue(config, 'importStrategy', 'useBundled'), - showNotifications: getGlobalValue(config, 'showNotifications', 'off'), - } - return setting + const setting = { + cwd: process.cwd(), + workspace: process.cwd(), + args: getGlobalValue(config, 'args', []), + path: getGlobalValue(config, 'path', []), + interpreter: interpreter, + importStrategy: getGlobalValue( + config, + 'importStrategy', + 'useBundled', + ), + showNotifications: getGlobalValue( + config, + 'showNotifications', + 'off', + ), + } + return setting } -export function checkIfConfigurationChanged(e: ConfigurationChangeEvent, namespace: string): boolean { - const settings = [ - `${namespace}.args`, - `${namespace}.path`, - `${namespace}.interpreter`, - `${namespace}.importStrategy`, - `${namespace}.showNotifications`, - ] - const changed = settings.map((s) => e.affectsConfiguration(s)) - return changed.includes(true) -} \ No newline at end of file +export function checkIfConfigurationChanged( + e: ConfigurationChangeEvent, + namespace: string, +): boolean { + const settings = [ + `${namespace}.args`, + `${namespace}.path`, + `${namespace}.interpreter`, + `${namespace}.importStrategy`, + `${namespace}.showNotifications`, + ] + const changed = settings.map(s => e.affectsConfiguration(s)) + return changed.includes(true) +} diff --git a/vscode/extension/src/utilities/common/utilities.ts b/vscode/extension/src/utilities/common/utilities.ts index cb696ccb49..671351162c 100644 --- a/vscode/extension/src/utilities/common/utilities.ts +++ b/vscode/extension/src/utilities/common/utilities.ts @@ -8,60 +8,69 @@ import { Trace } from 'vscode-jsonrpc/node' import { getWorkspaceFolders } from './vscodeapi' function logLevelToTrace(logLevel: LogLevel): Trace { - switch (logLevel) { - case LogLevel.Error: - case LogLevel.Warning: - case LogLevel.Info: - return Trace.Messages + switch (logLevel) { + case LogLevel.Error: + case LogLevel.Warning: + case LogLevel.Info: + return Trace.Messages - case LogLevel.Debug: - case LogLevel.Trace: - return Trace.Verbose + case LogLevel.Debug: + case LogLevel.Trace: + return Trace.Verbose - case LogLevel.Off: - default: - return Trace.Off - } + case LogLevel.Off: + default: + return Trace.Off + } } -export function getLSClientTraceLevel(channelLogLevel: LogLevel, globalLogLevel: LogLevel): Trace { - if (channelLogLevel === LogLevel.Off) { - return logLevelToTrace(globalLogLevel) - } - if (globalLogLevel === LogLevel.Off) { - return logLevelToTrace(channelLogLevel) - } - const level = logLevelToTrace(channelLogLevel <= globalLogLevel ? channelLogLevel : globalLogLevel) - return level +export function getLSClientTraceLevel( + channelLogLevel: LogLevel, + globalLogLevel: LogLevel, +): Trace { + if (channelLogLevel === LogLevel.Off) { + return logLevelToTrace(globalLogLevel) + } + if (globalLogLevel === LogLevel.Off) { + return logLevelToTrace(channelLogLevel) + } + const level = logLevelToTrace( + channelLogLevel <= globalLogLevel ? channelLogLevel : globalLogLevel, + ) + return level } export async function getProjectRoot(): Promise { - const workspaces: readonly WorkspaceFolder[] = getWorkspaceFolders() - if (workspaces.length === 0) { - return { - uri: Uri.file(process.cwd()), - name: path.basename(process.cwd()), - index: 0, - } - } else if (workspaces.length === 1) { - return workspaces[0] - } else { - let rootWorkspace = workspaces[0] - let root = undefined - for (const w of workspaces) { - if (await fs.pathExists(w.uri.fsPath)) { - root = w.uri.fsPath - rootWorkspace = w - break - } - } + const workspaces: readonly WorkspaceFolder[] = getWorkspaceFolders() + if (workspaces.length === 0) { + return { + uri: Uri.file(process.cwd()), + name: path.basename(process.cwd()), + index: 0, + } + } else if (workspaces.length === 1) { + return workspaces[0] + } else { + let rootWorkspace = workspaces[0] + let root = undefined + for (const w of workspaces) { + if (await fs.pathExists(w.uri.fsPath)) { + root = w.uri.fsPath + rootWorkspace = w + break + } + } - for (const w of workspaces) { - if (root && root.length > w.uri.fsPath.length && (await fs.pathExists(w.uri.fsPath))) { - root = w.uri.fsPath - rootWorkspace = w - } - } - return rootWorkspace + for (const w of workspaces) { + if ( + root && + root.length > w.uri.fsPath.length && + (await fs.pathExists(w.uri.fsPath)) + ) { + root = w.uri.fsPath + rootWorkspace = w + } } -} \ No newline at end of file + return rootWorkspace + } +} diff --git a/vscode/extension/src/utilities/common/vscodeapi.ts b/vscode/extension/src/utilities/common/vscodeapi.ts index 4bfbb1c4e8..942ede4325 100644 --- a/vscode/extension/src/utilities/common/vscodeapi.ts +++ b/vscode/extension/src/utilities/common/vscodeapi.ts @@ -1,43 +1,50 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - - import { - commands, - ConfigurationScope, - Disposable, - LogOutputChannel, - Uri, - window, - workspace, - WorkspaceConfiguration, - WorkspaceFolder, + commands, + ConfigurationScope, + Disposable, + LogOutputChannel, + Uri, + window, + workspace, + WorkspaceConfiguration, + WorkspaceFolder, } from 'vscode' export function createOutputChannel(name: string): LogOutputChannel { - return window.createOutputChannel(name, { log: true }) + return window.createOutputChannel(name, { log: true }) } -export function getConfiguration(config: string, scope?: ConfigurationScope): WorkspaceConfiguration { - return workspace.getConfiguration(config, scope) +export function getConfiguration( + config: string, + scope?: ConfigurationScope, +): WorkspaceConfiguration { + return workspace.getConfiguration(config, scope) } -export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { - return commands.registerCommand(command, callback, thisArg) +export function registerCommand( + command: string, + callback: (...args: any[]) => any, + thisArg?: any, +): Disposable { + return commands.registerCommand(command, callback, thisArg) } export const { onDidChangeConfiguration } = workspace export function isVirtualWorkspace(): boolean { - const isVirtual = workspace.workspaceFolders && workspace.workspaceFolders.every((f) => f.uri.scheme !== 'file') - return !!isVirtual + const isVirtual = + workspace.workspaceFolders && + workspace.workspaceFolders.every(f => f.uri.scheme !== 'file') + return !!isVirtual } export function getWorkspaceFolders(): readonly WorkspaceFolder[] { - return workspace.workspaceFolders ?? [] + return workspace.workspaceFolders ?? [] } export function getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { - return workspace.getWorkspaceFolder(uri) -} \ No newline at end of file + return workspace.getWorkspaceFolder(uri) +} diff --git a/vscode/extension/src/utilities/errors.ts b/vscode/extension/src/utilities/errors.ts index 29ab5813cd..a1f9ba3e47 100644 --- a/vscode/extension/src/utilities/errors.ts +++ b/vscode/extension/src/utilities/errors.ts @@ -1,28 +1,28 @@ -import { window } from "vscode" -import { AuthenticationProviderTobikoCloud } from "../auth/auth" -import { signIn } from "../commands/signin" -import { traceInfo } from "./common/log" +import { window } from 'vscode' +import { AuthenticationProviderTobikoCloud } from '../auth/auth' +import { signIn } from '../commands/signin' +import { traceInfo } from './common/log' /** * Represents different types of errors that can occur in the application. */ export type ErrorType = - | { type: "generic"; message: string } - | { type: "not_signed_in" }; + | { type: 'generic'; message: string } + | { type: 'not_signed_in' } /** * Handles the case where the user is not signed in to Tobiko Cloud. * @param authProvider - The authentication provider to use for signing in. */ export const handleNotSginedInError = async ( - authProvider: AuthenticationProviderTobikoCloud + authProvider: AuthenticationProviderTobikoCloud, ): Promise => { - traceInfo("handleNotSginedInError") + traceInfo('handleNotSginedInError') const result = await window.showInformationMessage( - "Please sign in to Tobiko Cloud to use SQLMesh", - "Sign In" + 'Please sign in to Tobiko Cloud to use SQLMesh', + 'Sign In', ) - if (result === "Sign In") { + if (result === 'Sign In') { await signIn(authProvider)() } } diff --git a/vscode/extension/src/utilities/exec.ts b/vscode/extension/src/utilities/exec.ts index 83e10016ff..8edacdaff5 100644 --- a/vscode/extension/src/utilities/exec.ts +++ b/vscode/extension/src/utilities/exec.ts @@ -1,32 +1,32 @@ -import { exec, ExecOptions } from "child_process" -import { traceInfo } from "./common/log" +import { exec, ExecOptions } from 'child_process' +import { traceInfo } from './common/log' export interface ExecResult { - exitCode: number; - stdout: string; - stderr: string; + exitCode: number + stdout: string + stderr: string } export interface CancellableExecOptions extends ExecOptions { /** When `abort()` is called on this signal the child process is killed. */ - signal?: AbortSignal; + signal?: AbortSignal } export function execAsync( command: string, args: string[], - options: CancellableExecOptions = {} + options: CancellableExecOptions = {}, ): Promise { return new Promise((resolve, reject) => { // Pass the signal straight through to `exec` - traceInfo(`Executing command: ${command} ${args.join(" ")}`) + traceInfo(`Executing command: ${command} ${args.join(' ')}`) const child = exec( - `${command} ${args.join(" ")}`, + `${command} ${args.join(' ')}`, options, (error, stdout, stderr) => { if (error) { resolve({ - exitCode: typeof error.code === "number" ? error.code : 1, + exitCode: typeof error.code === 'number' ? error.code : 1, stdout, stderr, }) @@ -38,7 +38,7 @@ export function execAsync( stdout, stderr, }) - } + }, ) /* ---------- Tie the Promise life‑cycle to the AbortSignal ---------- */ @@ -48,20 +48,20 @@ export function execAsync( const onAbort = () => { // `SIGTERM` is the default; use `SIGKILL` if you need something stronger child.kill() - reject(new Error("Process cancelled")) + reject(new Error('Process cancelled')) } if (options.signal.aborted) { onAbort() return } - options.signal.addEventListener("abort", onAbort, { once: true }) + options.signal.addEventListener('abort', onAbort, { once: true }) // Clean‑up the event listener when the promise settles const cleanup = () => - options.signal!.removeEventListener("abort", onAbort) - child.once("exit", cleanup) - child.once("error", cleanup) + options.signal!.removeEventListener('abort', onAbort) + child.once('exit', cleanup) + child.once('error', cleanup) } }) -} \ No newline at end of file +} diff --git a/vscode/extension/src/utilities/functional/result.ts b/vscode/extension/src/utilities/functional/result.ts index a4d23587cb..753b8c2e22 100644 --- a/vscode/extension/src/utilities/functional/result.ts +++ b/vscode/extension/src/utilities/functional/result.ts @@ -1,27 +1,27 @@ /** * A result is a value that can be either an ok or an error */ -export type Result = - | { ok: true; value: T } - | { ok: false; error: E }; +export type Result = { ok: true; value: T } | { ok: false; error: E } /** * returns true if the result is an error */ -export const isErr = (result: Result): result is { ok: false; error: E } => { +export const isErr = ( + result: Result, +): result is { ok: false; error: E } => { return !result.ok } /** * returns an ok version `Result` from a value `T` */ -export const ok = (value: T): { ok: true, value: T} => { - return { ok: true, value } +export const ok = (value: T): { ok: true; value: T } => { + return { ok: true, value } } /** * returns an error version `Result` from an error `E` */ -export const err = (error: E): { ok: false, error: E } => { - return { ok: false, error } -} \ No newline at end of file +export const err = (error: E): { ok: false; error: E } => { + return { ok: false, error } +} diff --git a/vscode/extension/src/utilities/isCodespaces.ts b/vscode/extension/src/utilities/isCodespaces.ts index db908e8023..16d9d441b8 100644 --- a/vscode/extension/src/utilities/isCodespaces.ts +++ b/vscode/extension/src/utilities/isCodespaces.ts @@ -1,9 +1,10 @@ /** * isCodespaces checks if the current environment is a Codespaces - * + * * @returns true if the current environment is a Codespaces, false otherwise */ export const isCodespaces = () => { - return process.env.CODESPACES === 'true' || - !!process.env.GITHUB_CODESPACE_TOKEN -} \ No newline at end of file + return ( + process.env.CODESPACES === 'true' || !!process.env.GITHUB_CODESPACE_TOKEN + ) +} diff --git a/vscode/extension/src/utilities/python.ts b/vscode/extension/src/utilities/python.ts index 154fe3cba8..610a1205fe 100644 --- a/vscode/extension/src/utilities/python.ts +++ b/vscode/extension/src/utilities/python.ts @@ -1,18 +1,20 @@ -import { getInterpreterDetails } from "./common/python" -import { err, ok, Result } from "./functional/result" -import { traceInfo } from "./common/log" -import { promisify } from "util" -import { execFile } from "child_process" +import { getInterpreterDetails } from './common/python' +import { err, ok, Result } from './functional/result' +import { traceInfo } from './common/log' +import { promisify } from 'util' +import { execFile } from 'child_process' /** isPythonModuleInstallled returns true if the given python module is installed. - * + * * @param moduleName - The name of the python module to check. * @returns True if the module is installed, false otherwise. */ -export const isPythonModuleInstalled = async (moduleName: string): Promise> => { +export const isPythonModuleInstalled = async ( + moduleName: string, +): Promise> => { const interpreterDetails = await getInterpreterDetails() if (!interpreterDetails.path) { - return err("No Python interpreter found") + return err('No Python interpreter found') } const pythonPath = interpreterDetails.path[0] const checkScript = ` @@ -30,11 +32,12 @@ except metadata.PackageNotFoundError: ` try { const execFileAsync = promisify(execFile) - const { stdout } = await execFileAsync(pythonPath, ["-c", checkScript]) - const isInstalled = stdout.trim() === "true" + const { stdout } = await execFileAsync(pythonPath, ['-c', checkScript]) + const isInstalled = stdout.trim() === 'true' traceInfo(`${moduleName} is installed: ${isInstalled}`) - return ok(stdout.trim() === "true") + return ok(stdout.trim() === 'true') } catch (error) { return err(`Failed to check tcloud installation: ${error}`) - }} + } +} diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 90879c5c93..67b45f2392 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -1,22 +1,22 @@ -import path from "path" -import { traceInfo, traceLog, traceVerbose } from "../common/log" -import { getInterpreterDetails } from "../common/python" -import { Result, err, isErr, ok } from "../functional/result" -import { getProjectRoot } from "../common/utilities" -import { isPythonModuleInstalled } from "../python" -import fs from "fs" -import { ErrorType } from "../errors" -import { isSignedIntoTobikoCloud } from "../../auth/auth" -import { execAsync } from "../exec" -import z from "zod" -import { ProgressLocation, window } from "vscode" +import path from 'path' +import { traceInfo, traceLog, traceVerbose } from '../common/log' +import { getInterpreterDetails } from '../common/python' +import { Result, err, isErr, ok } from '../functional/result' +import { getProjectRoot } from '../common/utilities' +import { isPythonModuleInstalled } from '../python' +import fs from 'fs' +import { ErrorType } from '../errors' +import { isSignedIntoTobikoCloud } from '../../auth/auth' +import { execAsync } from '../exec' +import z from 'zod' +import { ProgressLocation, window } from 'vscode' export type sqlmesh_exec = { - workspacePath: string; - bin: string; - env: Record; - args: string[]; -}; + workspacePath: string + bin: string + env: Record + args: string[] +} /** * Returns true if the current project is a Tcloud project. To detect this we, @@ -27,11 +27,11 @@ export type sqlmesh_exec = { */ export const isTcloudProject = async (): Promise> => { const projectRoot = await getProjectRoot() - const tcloudYamlPath = path.join(projectRoot.uri.fsPath, "tcloud.yaml") + const tcloudYamlPath = path.join(projectRoot.uri.fsPath, 'tcloud.yaml') if (fs.existsSync(tcloudYamlPath)) { return ok(true) } - return isPythonModuleInstalled("tcloud") + return isPythonModuleInstalled('tcloud') } /** @@ -42,10 +42,10 @@ export const isTcloudProject = async (): Promise> => { export const get_tcloud_bin = async (): Promise> => { const interpreterDetails = await getInterpreterDetails() if (!interpreterDetails.path) { - return err("No Python interpreter found") + return err('No Python interpreter found') } const pythonPath = interpreterDetails.path[0] - const binPath = path.join(path.dirname(pythonPath), "tcloud") + const binPath = path.join(path.dirname(pythonPath), 'tcloud') return ok(binPath) } @@ -61,21 +61,21 @@ const isSqlmeshInstalledSchema = z.object({ export const isSqlmeshEnterpriseInstalled = async (): Promise< Result > => { - traceInfo("Checking if sqlmesh enterprise is installed") + traceInfo('Checking if sqlmesh enterprise is installed') const tcloudBin = await get_tcloud_bin() if (isErr(tcloudBin)) { return err(tcloudBin.error) } - const called = await execAsync(tcloudBin.value, ["is_sqlmesh_installed"]) + const called = await execAsync(tcloudBin.value, ['is_sqlmesh_installed']) if (called.exitCode !== 0) { return err( - `Failed to check if sqlmesh enterprise is installed: ${called.stderr}` + `Failed to check if sqlmesh enterprise is installed: ${called.stderr}`, ) } const parsed = isSqlmeshInstalledSchema.safeParse(JSON.parse(called.stdout)) if (!parsed.success) { return err( - `Failed to parse sqlmesh enterprise installation status: ${parsed.error}` + `Failed to parse sqlmesh enterprise installation status: ${parsed.error}`, ) } return ok(parsed.data.is_installed) @@ -87,13 +87,13 @@ export const isSqlmeshEnterpriseInstalled = async (): Promise< * @returns A Result indicating whether sqlmesh enterprise was installed. */ export const installSqlmeshEnterprise = async ( - abortController: AbortController + abortController: AbortController, ): Promise> => { const tcloudBin = await get_tcloud_bin() if (isErr(tcloudBin)) { return err(tcloudBin.error) } - const called = await execAsync(tcloudBin.value, ["install_sqlmesh"], { + const called = await execAsync(tcloudBin.value, ['install_sqlmesh'], { signal: abortController.signal, }) if (called.exitCode !== 0) { @@ -111,37 +111,37 @@ export const installSqlmeshEnterprise = async ( export const ensureSqlmeshEnterpriseInstalled = async (): Promise< Result > => { - traceInfo("Ensuring sqlmesh enterprise is installed") + traceInfo('Ensuring sqlmesh enterprise is installed') const isInstalled = await isSqlmeshEnterpriseInstalled() if (isErr(isInstalled)) { return err(isInstalled.error) } if (isInstalled.value) { - traceInfo("Sqlmesh enterprise is installed") + traceInfo('Sqlmesh enterprise is installed') return ok(false) } - traceInfo("Sqlmesh enterprise is not installed, installing...") + traceInfo('Sqlmesh enterprise is not installed, installing...') const abortController = new AbortController() const installResult = await window.withProgress( { location: ProgressLocation.Notification, - title: "Installing sqlmesh enterprise...", + title: 'Installing sqlmesh enterprise...', cancellable: true, }, async (progress, token) => { // Connect the cancellation token to our abort controller token.onCancellationRequested(() => { abortController.abort() - traceInfo("Sqlmesh enterprise installation cancelled") - window.showInformationMessage("Installation cancelled") + traceInfo('Sqlmesh enterprise installation cancelled') + window.showInformationMessage('Installation cancelled') }) - progress.report({ message: "Installing sqlmesh enterprise..." }) + progress.report({ message: 'Installing sqlmesh enterprise...' }) const result = await installSqlmeshEnterprise(abortController) if (isErr(result)) { return result } return ok(true) - } + }, ) if (isErr(installResult)) { return installResult @@ -164,16 +164,16 @@ export const sqlmesh_exec = async (): Promise< if (interpreterDetails.path) { traceVerbose( `Using interpreter from Python extension: ${interpreterDetails.path.join( - " " - )}` + ' ', + )}`, ) } if (interpreterDetails.isVirtualEnvironment) { - traceLog("Using virtual environment") + traceLog('Using virtual environment') const isTcloudInstalled = await isTcloudProject() if (isErr(isTcloudInstalled)) { return err({ - type: "generic", + type: 'generic', message: isTcloudInstalled.error, }) } @@ -181,20 +181,20 @@ export const sqlmesh_exec = async (): Promise< const tcloudBin = await get_tcloud_bin() if (isErr(tcloudBin)) { return err({ - type: "generic", + type: 'generic', message: tcloudBin.error, }) } const isSignedIn = await isSignedIntoTobikoCloud() if (!isSignedIn) { return err({ - type: "not_signed_in", + type: 'not_signed_in', }) } const ensured = await ensureSqlmeshEnterpriseInstalled() if (isErr(ensured)) { return err({ - type: "generic", + type: 'generic', message: ensured.error, }) } @@ -209,7 +209,7 @@ export const sqlmesh_exec = async (): Promise< args: [], }) } - const binPath = path.join(interpreterDetails.binPath!, "sqlmesh") + const binPath = path.join(interpreterDetails.binPath!, 'sqlmesh') traceLog(`Bin path: ${binPath}`) return ok({ bin: binPath, @@ -223,7 +223,7 @@ export const sqlmesh_exec = async (): Promise< }) } else { return ok({ - bin: "sqlmesh", + bin: 'sqlmesh', workspacePath, env: {}, args: [], @@ -246,43 +246,43 @@ export const sqlmesh_lsp_exec = async (): Promise< if (interpreterDetails.path) { traceVerbose( `Using interpreter from Python extension: ${interpreterDetails.path.join( - " " - )}` + ' ', + )}`, ) } if (interpreterDetails.isVirtualEnvironment) { - traceLog("Using virtual environment") + traceLog('Using virtual environment') const tcloudInstalled = await isTcloudProject() if (isErr(tcloudInstalled)) { return err({ - type: "generic", + type: 'generic', message: tcloudInstalled.error, }) } if (tcloudInstalled.value) { - traceLog("Tcloud installed, installing sqlmesh") + traceLog('Tcloud installed, installing sqlmesh') const tcloudBin = await get_tcloud_bin() if (isErr(tcloudBin)) { return err({ - type: "generic", + type: 'generic', message: tcloudBin.error, }) } const isSignedIn = await isSignedIntoTobikoCloud() if (!isSignedIn) { return err({ - type: "not_signed_in", + type: 'not_signed_in', }) } const ensured = await ensureSqlmeshEnterpriseInstalled() if (isErr(ensured)) { return err({ - type: "generic", + type: 'generic', message: ensured.error, }) } } - const binPath = path.join(interpreterDetails.binPath!, "sqlmesh_lsp") + const binPath = path.join(interpreterDetails.binPath!, 'sqlmesh_lsp') traceLog(`Bin path: ${binPath}`) return ok({ bin: binPath, @@ -290,13 +290,13 @@ export const sqlmesh_lsp_exec = async (): Promise< env: { PYTHONPATH: interpreterDetails.path?.[0], VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), - PATH: path.join(path.dirname(interpreterDetails.binPath!), "bin"), + PATH: path.join(path.dirname(interpreterDetails.binPath!), 'bin'), }, args: [], }) } else { return ok({ - bin: "sqlmesh_lsp", + bin: 'sqlmesh_lsp', workspacePath, env: {}, args: [], diff --git a/vscode/extension/tsconfig.json b/vscode/extension/tsconfig.json index 3a581b7e11..2f8323e699 100644 --- a/vscode/extension/tsconfig.json +++ b/vscode/extension/tsconfig.json @@ -1,28 +1,20 @@ { - "compilerOptions": { - "module": "Node16", - "target": "ES2022", - "lib": [ - "ES2022", - "DOM" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true, /* enable all strict type-checking options */ - /* Additional Checks */ - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUnusedParameters": true, - "noUnusedLocals": true, - "paths": { - "@bus/*": ["../bus/src/*"] - } - }, - "include": [ - "src/**/*", - "../bus/src/**/*" - ], - "exclude": [ - "node_modules", "../node_modules" - ] + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "sourceMap": true, + "rootDir": "src", + "strict": true /* enable all strict type-checking options */, + /* Additional Checks */ + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "paths": { + "@bus/*": ["../bus/src/*"] + } + }, + "include": ["src/**/*", "../bus/src/**/*"], + "exclude": ["node_modules", "../node_modules"] } diff --git a/web/client/.eslintrc.cjs b/web/client/.eslintrc.cjs new file mode 100644 index 0000000000..17d429510b --- /dev/null +++ b/web/client/.eslintrc.cjs @@ -0,0 +1,57 @@ +const OFF = 0 +const ERROR = 2 + +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + }, + extends: ['plugin:react/recommended', 'standard-with-typescript'], + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json', + }, + plugins: ['react', '@typescript-eslint'], + rules: { + 'react/jsx-uses-react': OFF, + 'react/react-in-jsx-scope': OFF, + 'no-use-before-define': OFF, + '@typescript-eslint/promise-function-async': OFF, + '@typescript-eslint/no-non-null-assertion': OFF, + 'no-return-await': OFF, + '@typescript-eslint/return-await': OFF, + '@typescript-eslint/no-use-before-define': [ + ERROR, + { + variables: true, + functions: false, + classes: false, + allowNamedExports: true, + }, + ], + '@typescript-eslint/no-dynamic-delete': OFF, + '@typescript-eslint/naming-convention': [ + ERROR, + { + selector: 'variable', + format: ['camelCase', 'PascalCase', 'UPPER_CASE', 'snake_case'], + }, + ], + '@typescript-eslint/no-confusing-void-expression': OFF, + }, + ignorePatterns: [ + 'src/api/client.ts', + 'src/utils/tbk-components.js', + 'test-results', + 'playwright', + 'playwright-report', + 'dist', + ], + settings: { + react: { + version: '18.2', + }, + }, +} diff --git a/web/client/.prettierignore b/web/client/.prettierignore deleted file mode 100644 index 7e2a617265..0000000000 --- a/web/client/.prettierignore +++ /dev/null @@ -1,13 +0,0 @@ -**/*.py -.prettierignore -.gitignore -node_modules/ -/test-results/ -/playwright-report/ -/playwright/.cache/ -dist -/public/favicons/ -/public/fonts/ -/src/styles/fonts/ -/src/assets/fonts/ -tsconfig.tsbuildinfo \ No newline at end of file diff --git a/web/client/eslint.config.mjs b/web/client/eslint.config.mjs index a0dacd1520..dbf0ec076b 100644 --- a/web/client/eslint.config.mjs +++ b/web/client/eslint.config.mjs @@ -1,5 +1,5 @@ -import eslint from '@eslint/js'; -import tseslint from 'typescript-eslint'; +import eslint from '@eslint/js' +import tseslint from 'typescript-eslint' export default tseslint.config( { diff --git a/web/client/package.json b/web/client/package.json index f8525f72e6..4791f66b3c 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -5,11 +5,9 @@ "dev": "npm run generate:api && vite", "build": "npm run generate:api && tsc && vite build", "preview": "vite preview --no-open", - "prettier": "prettier --write .", - "lint:fix": "eslint --fix .", - "lint": "eslint src", - "format": "npm run prettier && npm run lint:fix", - "test": "npm run generate:api npm run test:unit && npm run test:e2e", + "lint": "eslint", + "lint:fix": "eslint --fix", + "test": "npm run generate:api && npm run test:unit && npm run test:e2e", "test:unit:watch": "NODE_ENV=development vitest --watch=true", "test:unit": "NODE_ENV=testing vitest --watch=false", "test:e2e": "NODE_ENV=testing playwright test", @@ -68,12 +66,14 @@ "jsdom": "^22.1.0", "orval": "^7.9.0", "postcss": "^8.4.29", - "prettier": "^3.0.3", "tailwindcss": "^3.3.3", "typescript": "^5.8.3", "typescript-eslint": "^8.31.1", "vite": "^6.3.4", "vite-plugin-css-injected-by-js": "^3.5.2", "vitest": "^3.1.2" + }, + "optionalDependencies": { + "@swc/core-linux-x64-gnu": "^1.11.24" } } diff --git a/web/client/public/css/design.css b/web/client/public/css/design.css index c8bcb82ecf..c3e771ed02 100644 --- a/web/client/public/css/design.css +++ b/web/client/public/css/design.css @@ -439,7 +439,8 @@ --shadow-l: 0 var(--step) calc(var(--half) * 4) var(--color-shadow-10); --shadow-xl: 0 var(--step) calc(var(--half) * 5) var(--color-shadow-10); - --tooltip-shadow: 0 var(--step-2) calc(var(--step) * 4) -1px var(--color-shadow-30), + --tooltip-shadow: + 0 var(--step-2) calc(var(--step) * 4) -1px var(--color-shadow-30), inset 0 0 0 1px var(--color-gray-300); --tooltip-background: var(--color-light); --tooltip-text: var(--color-gray-600); diff --git a/web/client/src/library/components/editor/hooks.ts b/web/client/src/library/components/editor/hooks.ts index ae4bb08f8d..0b6aa67f2c 100644 --- a/web/client/src/library/components/editor/hooks.ts +++ b/web/client/src/library/components/editor/hooks.ts @@ -99,8 +99,8 @@ function useSQLMeshModelExtensions( m.columns.map(c => c.name), ) : (Object.keys(lineage) - .flatMap( - modelName => models.get(modelName)?.columns.map(c => c.name), + .flatMap(modelName => + models.get(modelName)?.columns.map(c => c.name), ) .filter(Boolean) as string[]), ) diff --git a/web/client/src/library/components/fileExplorer/FileExplorer.tsx b/web/client/src/library/components/fileExplorer/FileExplorer.tsx index 01a82d0008..ed7235fd3e 100644 --- a/web/client/src/library/components/fileExplorer/FileExplorer.tsx +++ b/web/client/src/library/components/fileExplorer/FileExplorer.tsx @@ -270,10 +270,10 @@ function FileExplorerArtifactContainer({ isTopGroupInActiveRange(artifact) ? 'rounded-t-md' : isBottomGroupInActiveRange(artifact) - ? 'rounded-b-md' - : isMiddleGroupInActiveRange(artifact) - ? '' - : inActiveRange(artifact) && 'rounded-md', + ? 'rounded-b-md' + : isMiddleGroupInActiveRange(artifact) + ? '' + : inActiveRange(artifact) && 'rounded-md', inActiveRange(artifact) && activeRange.length > 1 && activeRange.indexOf(artifact) < activeRange.length - 1 diff --git a/web/client/src/library/components/graph/ModelColumns.tsx b/web/client/src/library/components/graph/ModelColumns.tsx index c3f5827768..640d3f8832 100644 --- a/web/client/src/library/components/graph/ModelColumns.tsx +++ b/web/client/src/library/components/graph/ModelColumns.tsx @@ -111,7 +111,7 @@ export default function ModelColumns({ if (isNil(lineageCache)) { const mainNodeLineage = isNil(mainNode) ? undefined - : lineage[mainNode] ?? lineageCache?.[mainNode] + : (lineage[mainNode] ?? lineageCache?.[mainNode]) newLineageCache = lineage currentConnections = new Map() @@ -471,10 +471,10 @@ function ModelColumn({ isError ? 'text-danger-500' : isTimeout - ? 'text-warning-500' - : isEmpty - ? 'text-neutral-400 dark:text-neutral-600' - : 'text-prose', + ? 'text-warning-500' + : isEmpty + ? 'text-neutral-400 dark:text-neutral-600' + : 'text-prose', )} /> @@ -495,10 +495,10 @@ function ModelColumn({ isError ? 'text-danger-500' : isTimeout - ? 'text-warning-500' - : isEmpty - ? 'text-neutral-400 dark:text-neutral-600' - : 'text-prose', + ? 'text-warning-500' + : isEmpty + ? 'text-neutral-400 dark:text-neutral-600' + : 'text-prose', )} /> diff --git a/web/client/src/library/components/graph/ModelNode.tsx b/web/client/src/library/components/graph/ModelNode.tsx index 5812fc4386..a96f9f2968 100644 --- a/web/client/src/library/components/graph/ModelNode.tsx +++ b/web/client/src/library/components/graph/ModelNode.tsx @@ -166,20 +166,20 @@ export default function ModelNode({ isCTE ? 'border-accent-500 bg-accent-500 text-accent-500 dark:border-accent-300 dark:bg-accent-300 dark:text-accent-300' : isModelUnknown - ? 'border-neutral-500 bg-neutral-500 text-neutral-500 dark:border-neutral-300 dark:bg-neutral-300 dark:text-neutral-300' - : 'border-secondary-500 bg-secondary-500 text-secondary-500 dark:bg-primary-500 dark:border-primary-500 dark:text-primary-500', + ? 'border-neutral-500 bg-neutral-500 text-neutral-500 dark:border-neutral-300 dark:bg-neutral-300 dark:text-neutral-300' + : 'border-secondary-500 bg-secondary-500 text-secondary-500 dark:bg-primary-500 dark:border-primary-500 dark:text-primary-500', isMainNode ? 'ring-8 ring-brand-50' : isModelExternal || isModelSeed - ? 'ring-8 ring-accent-50' - : '', + ? 'ring-8 ring-accent-50' + : '', ] : highlighted, isSelected && isCTE ? 'ring-8 ring-accent-50' : isSelected && isModelUnknown - ? 'ring-8 ring-neutral-50' - : isSelected && 'ring-8 ring-secondary-50 dark:ring-primary-50', + ? 'ring-8 ring-neutral-50' + : isSelected && 'ring-8 ring-secondary-50 dark:ring-primary-50', )} style={{ maxWidth: isNil(nodeData.width) diff --git a/web/client/src/library/components/graph/help.ts b/web/client/src/library/components/graph/help.ts index 06ef7fabc9..4a5416c648 100644 --- a/web/client/src/library/components/graph/help.ts +++ b/web/client/src/library/components/graph/help.ts @@ -178,15 +178,15 @@ function getNodeMap({ type: isNotNil(model) ? (model.type as LineageNodeModelType) : // If model name present in lineage but not in global models - // it means either this is a CTE or model is UNKNOWN - // CTEs only have connections between columns - // where UNKNOWN model has connection only from another model - unknownModels.has(modelName) - ? EnumLineageNodeModelType.unknown - : EnumLineageNodeModelType.cte, + // it means either this is a CTE or model is UNKNOWN + // CTEs only have connections between columns + // where UNKNOWN model has connection only from another model + unknownModels.has(modelName) + ? EnumLineageNodeModelType.unknown + : EnumLineageNodeModelType.cte, }) const columnsCount = withColumns - ? models.get(modelName)?.columns?.length ?? 0 + ? (models.get(modelName)?.columns?.length ?? 0) : 0 const maxWidth = Math.min( diff --git a/web/client/src/library/components/plan/PlanActions.tsx b/web/client/src/library/components/plan/PlanActions.tsx index aa38b7d6ee..7fdb0fe8cb 100644 --- a/web/client/src/library/components/plan/PlanActions.tsx +++ b/web/client/src/library/components/plan/PlanActions.tsx @@ -175,8 +175,8 @@ export default function PlanActions({ planAction.isRun ? handleRun : planCancel.isSuccessful - ? undefined - : handleApply + ? undefined + : handleApply } ref={setFocus} variant={ diff --git a/web/client/src/library/components/plan/PlanApplyStageTracker.tsx b/web/client/src/library/components/plan/PlanApplyStageTracker.tsx index c0270511c3..e513627bbb 100644 --- a/web/client/src/library/components/plan/PlanApplyStageTracker.tsx +++ b/web/client/src/library/components/plan/PlanApplyStageTracker.tsx @@ -133,7 +133,7 @@ export default function PlanApplyStageTracker(): JSX.Element { start={planApply.evaluationStart} end={ planApply.isFinished - ? planApply.evaluationEnd ?? planCancel.meta?.end + ? (planApply.evaluationEnd ?? planCancel.meta?.end) : undefined } > @@ -708,14 +708,14 @@ function Stage({ const variant = isStatusSuccess ? EnumVariant.Success : isStatusFail - ? EnumVariant.Danger - : EnumVariant.Info + ? EnumVariant.Danger + : EnumVariant.Info const [titleSuccess, titleFail, titleDefault] = states const text = isStatusSuccess ? titleSuccess : isStatusFail - ? titleFail - : titleDefault + ? titleFail + : titleDefault return ( 0 ? virtualRows?.[0]?.start ?? 0 : 0 + const paddingTop = virtualRows.length > 0 ? (virtualRows?.[0]?.start ?? 0) : 0 const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end ?? 0) diff --git a/web/client/src/library/components/tableDiff/TableDiff.tsx b/web/client/src/library/components/tableDiff/TableDiff.tsx index bf927bbac0..31a24af051 100644 --- a/web/client/src/library/components/tableDiff/TableDiff.tsx +++ b/web/client/src/library/components/tableDiff/TableDiff.tsx @@ -101,10 +101,10 @@ export default function TableDiff({ diff }: { diff: any }): JSX.Element { header in diff.schema_diff.added ? 'border-t-2 border-l-2 border-r-2 border-success-500' : header in diff.schema_diff.removed - ? 'border-t-2 border-l-2 border-r-2 border-danger-500' - : grain.includes(header) - ? 'border-brand-500 border-l-2 border-t-2 border-r-2' - : 'border-r border-b border-neutral-100 dark:border-neutral-700 last:border-r-0', + ? 'border-t-2 border-l-2 border-r-2 border-danger-500' + : grain.includes(header) + ? 'border-brand-500 border-l-2 border-t-2 border-r-2' + : 'border-r border-b border-neutral-100 dark:border-neutral-700 last:border-r-0', grain.includes(header) ? 'bg-brand-10' : 'bg-neutral-5', )} > @@ -199,10 +199,10 @@ export default function TableDiff({ diff }: { diff: any }): JSX.Element { header in diff.schema_diff.added ? 'bg-success-10 border-l-2 border-r-2 border-success-500 text-success-500 font-bold' : header in diff.schema_diff.removed - ? 'bg-danger-5 border-l-2 border-r-2 border-danger-500 !text-danger-500 font-bold' - : grain.includes(header) - ? 'border-brand-500 border-l-2 border-r-2' - : 'border-r border-b border-neutral-100 dark:border-neutral-700 last:border-r-0', + ? 'bg-danger-5 border-l-2 border-r-2 border-danger-500 !text-danger-500 font-bold' + : grain.includes(header) + ? 'border-brand-500 border-l-2 border-r-2' + : 'border-r border-b border-neutral-100 dark:border-neutral-700 last:border-r-0', isDeletedRow(diff, rowKey, diff.on) && '!bg-danger-5 text-danger-500 font-bold', isAddedRow(diff, rowKey, diff.on) && @@ -284,10 +284,10 @@ export default function TableDiff({ diff }: { diff: any }): JSX.Element { header in diff.schema_diff.added ? 'border-b-2 border-l-2 border-r-2 border-success-500' : header in diff.schema_diff.removed - ? 'border-b-2 border-l-2 border-r-2 border-danger-500' - : grain.includes(header) - ? 'border-brand-500 border-l-2 border-b-2 border-r-2' - : 'border-r border-t border-neutral-100 dark:border-neutral-700 last:border-r-0', + ? 'border-b-2 border-l-2 border-r-2 border-danger-500' + : grain.includes(header) + ? 'border-brand-500 border-l-2 border-b-2 border-r-2' + : 'border-r border-t border-neutral-100 dark:border-neutral-700 last:border-r-0', grain.includes(header) ? 'bg-brand-10' : 'bg-neutral-10', diff --git a/web/docker-compose.build.yml b/web/docker-compose.build.yml index 640bf5ba14..2b59000441 100644 --- a/web/docker-compose.build.yml +++ b/web/docker-compose.build.yml @@ -1,5 +1,6 @@ -version: "3.11" +version: '3.11' services: app: - command: bash -c "cd ../.. && npm ci && npm --prefix /app/web/client run build" + command: + bash -c "cd ../.. && npm ci && npm --prefix /app/web/client run build" networks: [] diff --git a/web/docker-compose.yml b/web/docker-compose.yml index 0474a152f4..de4581d44d 100644 --- a/web/docker-compose.yml +++ b/web/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3.11" +version: '3.11' services: generate-openapi-spec: image: tobiko-api @@ -37,7 +37,10 @@ services: build: context: .. dockerfile: web/Dockerfile.api - command: python -m uvicorn web.server.app:app --host 0.0.0.0 --port 8000 --reload --reload-dir /sqlmesh/web/server --reload-dir /sqlmesh/sqlmesh --timeout-keep-alive 300 --timeout-graceful-shutdown 1 + command: + python -m uvicorn web.server.app:app --host 0.0.0.0 --port 8000 --reload + --reload-dir /sqlmesh/web/server --reload-dir /sqlmesh/sqlmesh + --timeout-keep-alive 300 --timeout-graceful-shutdown 1 ports: - 8000:8000 volumes: From 5d7431a352b9a73f335d02c17c450d0e58817ee7 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Apr 2025 21:27:34 +0100 Subject: [PATCH 0083/1056] chore(vscode): delete unused eslint file (#4286) --- web/client/.eslintrc.cjs | 57 ---------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 web/client/.eslintrc.cjs diff --git a/web/client/.eslintrc.cjs b/web/client/.eslintrc.cjs deleted file mode 100644 index 17d429510b..0000000000 --- a/web/client/.eslintrc.cjs +++ /dev/null @@ -1,57 +0,0 @@ -const OFF = 0 -const ERROR = 2 - -module.exports = { - root: true, - env: { - browser: true, - es2021: true, - }, - extends: ['plugin:react/recommended', 'standard-with-typescript'], - parser: '@typescript-eslint/parser', - parserOptions: { - tsconfigRootDir: __dirname, - project: './tsconfig.json', - }, - plugins: ['react', '@typescript-eslint'], - rules: { - 'react/jsx-uses-react': OFF, - 'react/react-in-jsx-scope': OFF, - 'no-use-before-define': OFF, - '@typescript-eslint/promise-function-async': OFF, - '@typescript-eslint/no-non-null-assertion': OFF, - 'no-return-await': OFF, - '@typescript-eslint/return-await': OFF, - '@typescript-eslint/no-use-before-define': [ - ERROR, - { - variables: true, - functions: false, - classes: false, - allowNamedExports: true, - }, - ], - '@typescript-eslint/no-dynamic-delete': OFF, - '@typescript-eslint/naming-convention': [ - ERROR, - { - selector: 'variable', - format: ['camelCase', 'PascalCase', 'UPPER_CASE', 'snake_case'], - }, - ], - '@typescript-eslint/no-confusing-void-expression': OFF, - }, - ignorePatterns: [ - 'src/api/client.ts', - 'src/utils/tbk-components.js', - 'test-results', - 'playwright', - 'playwright-report', - 'dist', - ], - settings: { - react: { - version: '18.2', - }, - }, -} From 7938dbd4e08e1de12e722fce97229d4c7b674e42 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Apr 2025 21:46:47 +0100 Subject: [PATCH 0084/1056] chore(vscode): make eslint stricter (#4287) --- .circleci/continue_config.yml | 1 - package-lock.json | 8 +-- vscode/extension/eslint.config.mjs | 49 ++++++++----------- vscode/extension/package.json | 5 +- vscode/extension/src/auth/auth.ts | 20 ++++++-- vscode/extension/src/commands/format.ts | 2 +- vscode/extension/src/extension.ts | 4 +- vscode/extension/src/lsp/lsp.ts | 8 +-- .../src/utilities/common/vscodeapi.ts | 6 +-- vscode/extension/src/utilities/exec.ts | 3 +- .../src/utilities/sqlmesh/sqlmesh.ts | 4 +- 11 files changed, 56 insertions(+), 54 deletions(-) diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 79b01e90ac..39ac222a43 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -43,7 +43,6 @@ jobs: - run: name: Install VSCode extension dependencies command: | - cd vscode/extension npm ci - run: name: Run VSCode extension CI diff --git a/package-lock.json b/package-lock.json index d482cb090d..9508be421b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -12493,6 +12492,8 @@ }, "node_modules/typescript-eslint": { "version": "8.31.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz", + "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==", "dev": true, "license": "MIT", "dependencies": { @@ -13641,15 +13642,14 @@ "@types/mocha": "^10.0.10", "@types/node": "20.11.25", "@types/vscode": "1.96.0", - "@typescript-eslint/eslint-plugin": "^8.28.0", - "@typescript-eslint/parser": "^8.28.0", "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.4.1", "@vscode/vsce": "^3.3.2", "esbuild": "^0.25.2", "eslint": "^9.23.0", "ts-loader": "^9.5.2", - "typescript": "^5.8.2" + "typescript": "^5.8.2", + "typescript-eslint": "^8.31.1" }, "engines": { "vscode": "^1.96.0" diff --git a/vscode/extension/eslint.config.mjs b/vscode/extension/eslint.config.mjs index d8625b2e6c..651221da64 100644 --- a/vscode/extension/eslint.config.mjs +++ b/vscode/extension/eslint.config.mjs @@ -1,35 +1,28 @@ -import typescriptEslint from '@typescript-eslint/eslint-plugin' -import tsParser from '@typescript-eslint/parser' +import eslint from '@eslint/js' +import tseslint from 'typescript-eslint' -export default [ +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.strict, + tseslint.configs.stylistic, + tseslint.configs.recommendedTypeChecked, { - files: ['**/*.ts'], - }, - { - plugins: { - '@typescript-eslint': typescriptEslint, - }, - languageOptions: { - parser: tsParser, - ecmaVersion: 2022, - sourceType: 'module', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, }, - + }, + { rules: { - '@typescript-eslint/naming-convention': [ - 'warn', - { - selector: 'import', - format: ['camelCase', 'PascalCase'], - }, - ], - - curly: 'error', - eqeqeq: 'error', - 'no-throw-literal': 'error', - semi: ['error', 'never'], - 'no-shadow': 'error', + '@typescript-eslint/switch-exhaustiveness-check': 'error', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', }, }, -] +) diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 66d18e507b..fa898a4bce 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -87,14 +87,13 @@ "@types/mocha": "^10.0.10", "@types/node": "20.11.25", "@types/vscode": "1.96.0", - "@typescript-eslint/eslint-plugin": "^8.28.0", - "@typescript-eslint/parser": "^8.28.0", "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.4.1", "@vscode/vsce": "^3.3.2", "esbuild": "^0.25.2", "eslint": "^9.23.0", "ts-loader": "^9.5.2", - "typescript": "^5.8.2" + "typescript": "^5.8.2", + "typescript-eslint": "^8.31.1" } } diff --git a/vscode/extension/src/auth/auth.ts b/vscode/extension/src/auth/auth.ts index 71366d4099..7da8e599b4 100644 --- a/vscode/extension/src/auth/auth.ts +++ b/vscode/extension/src/auth/auth.ts @@ -82,7 +82,7 @@ export class AuthenticationProviderTobikoCloud return err('Failed to get tcloud auth status') } const status = result.stdout - const statusToJson = JSON.parse(status) + const statusToJson: any = JSON.parse(status) const statusResponse = statusResponseSchema.parse(statusToJson) return ok(statusResponse) } @@ -188,7 +188,7 @@ export class AuthenticationProviderTobikoCloud } try { - const resultToJson = JSON.parse(result.stdout) + const resultToJson: any = JSON.parse(result.stdout) const urlCode = loginUrlResponseSchema.parse(resultToJson) const url = urlCode.url @@ -197,7 +197,12 @@ export class AuthenticationProviderTobikoCloud } const ac = new AbortController() - const timeout = setTimeout(() => ac.abort(), 1000 * 60 * 5) + const timeout = setTimeout( + () => { + ac.abort() + }, + 1000 * 60 * 5, + ) const backgroundServerForLogin = execAsync( tcloudBinPath, ['auth', 'vscode', 'start-server', urlCode.verifier_code], @@ -279,11 +284,16 @@ export class AuthenticationProviderTobikoCloud } try { - const resultToJson = JSON.parse(result.stdout) + const resultToJson: any = JSON.parse(result.stdout) const deviceCodeResponse = deviceCodeResponseSchema.parse(resultToJson) const ac = new AbortController() - const timeout = setTimeout(() => ac.abort(), 1000 * 60 * 5) + const timeout = setTimeout( + () => { + ac.abort() + }, + 1000 * 60 * 5, + ) const waiting = execAsync( tcloudBinPath, ['auth', 'vscode', 'poll_device', deviceCodeResponse.device_code], diff --git a/vscode/extension/src/commands/format.ts b/vscode/extension/src/commands/format.ts index ac342f3849..a340fcee3d 100644 --- a/vscode/extension/src/commands/format.ts +++ b/vscode/extension/src/commands/format.ts @@ -12,7 +12,7 @@ export const format = const out = await internalFormat() if (isErr(out)) { if (out.error.type === 'not_signed_in') { - handleNotSginedInError(authProvider) + await handleNotSginedInError(authProvider) return } else { vscode.window.showErrorMessage( diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 4b010da8b1..ef449057c1 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -58,7 +58,7 @@ export async function activate(context: vscode.ExtensionContext) { lspClient = new LSPClient() const result = await lspClient.start() if (isErr(result)) { - handleNotSginedInError(authProvider) + await handleNotSginedInError(authProvider) } context.subscriptions.push(lspClient) @@ -67,7 +67,7 @@ export async function activate(context: vscode.ExtensionContext) { traceVerbose('Restarting LSP client') const restartResult = await lspClient.restart() if (isErr(restartResult)) { - handleNotSginedInError(authProvider) + await handleNotSginedInError(authProvider) } } } diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 2b3c8e9d61..4e4564e81f 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -27,7 +27,7 @@ export class LSPClient implements Disposable { const sqlmesh = await sqlmesh_lsp_exec() if (isErr(sqlmesh)) { - traceError(`Failed to get sqlmesh_lsp_exec: ${sqlmesh.error}`) + traceError(`Failed to get sqlmesh_lsp_exec, ${sqlmesh.error.type}`) return sqlmesh } const workspaceFolders = getWorkspaceFolders() @@ -41,9 +41,9 @@ export class LSPClient implements Disposable { }) } - let folder = workspaceFolders[0] + const folder = workspaceFolders[0] const workspacePath = workspaceFolders[0].uri.fsPath - let serverOptions: ServerOptions = { + const serverOptions: ServerOptions = { run: { command: sqlmesh.value.bin, transport: TransportKind.stdio, @@ -61,7 +61,7 @@ export class LSPClient implements Disposable { args: sqlmesh.value.args, }, } - let clientOptions: LanguageClientOptions = { + const clientOptions: LanguageClientOptions = { documentSelector: [{ scheme: 'file', pattern: `**/*.sql` }], workspaceFolder: folder, diagnosticCollectionName: 'sqlmesh', diff --git a/vscode/extension/src/utilities/common/vscodeapi.ts b/vscode/extension/src/utilities/common/vscodeapi.ts index 942ede4325..6687d60933 100644 --- a/vscode/extension/src/utilities/common/vscodeapi.ts +++ b/vscode/extension/src/utilities/common/vscodeapi.ts @@ -35,9 +35,9 @@ export function registerCommand( export const { onDidChangeConfiguration } = workspace export function isVirtualWorkspace(): boolean { - const isVirtual = - workspace.workspaceFolders && - workspace.workspaceFolders.every(f => f.uri.scheme !== 'file') + const isVirtual = workspace.workspaceFolders?.every( + f => f.uri.scheme !== 'file', + ) return !!isVirtual } diff --git a/vscode/extension/src/utilities/exec.ts b/vscode/extension/src/utilities/exec.ts index 8edacdaff5..b57e2d488c 100644 --- a/vscode/extension/src/utilities/exec.ts +++ b/vscode/extension/src/utilities/exec.ts @@ -58,8 +58,9 @@ export function execAsync( options.signal.addEventListener('abort', onAbort, { once: true }) // Clean‑up the event listener when the promise settles - const cleanup = () => + const cleanup = () => { options.signal!.removeEventListener('abort', onAbort) + } child.once('exit', cleanup) child.once('error', cleanup) } diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 67b45f2392..40b01e76a6 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -11,7 +11,7 @@ import { execAsync } from '../exec' import z from 'zod' import { ProgressLocation, window } from 'vscode' -export type sqlmesh_exec = { +export interface sqlmesh_exec { workspacePath: string bin: string env: Record @@ -75,7 +75,7 @@ export const isSqlmeshEnterpriseInstalled = async (): Promise< const parsed = isSqlmeshInstalledSchema.safeParse(JSON.parse(called.stdout)) if (!parsed.success) { return err( - `Failed to parse sqlmesh enterprise installation status: ${parsed.error}`, + `Failed to parse sqlmesh enterprise installation status: ${parsed.error.message}`, ) } return ok(parsed.data.is_installed) From 1109d97bc5a67051540743aafe4bf32b484efa42 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 30 Apr 2025 16:29:53 -0700 Subject: [PATCH 0085/1056] Fix: Support blueprint variables in jinja macros (#4289) --- docs/concepts/macros/jinja_macros.md | 25 ++++++++++++++ sqlmesh/core/constants.py | 1 + sqlmesh/utils/jinja.py | 10 ++++-- tests/core/test_model.py | 49 ++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/docs/concepts/macros/jinja_macros.md b/docs/concepts/macros/jinja_macros.md index 7018ecc9cb..49b5f81912 100644 --- a/docs/concepts/macros/jinja_macros.md +++ b/docs/concepts/macros/jinja_macros.md @@ -122,6 +122,31 @@ Access gateway variables in models using the same methods as [global variables]( Gateway-specific variable values take precedence over variables with the same name specified in the configuration file's root `variables` key. +### Blueprint variables + +Blueprint variables are defined as a property of the `MODEL` statement, and serve as a mechanism for [creating model templates](../models/sql_models.md): + +```sql linenums="1" +MODEL ( + name @customer.some_table, + kind FULL, + blueprints ( + (customer := customer1, field_a := x, field_b := y), + (customer := customer2, field_a := z) + ) +); + +JINJA_QUERY_BEGIN; +SELECT + {{ blueprint_var('field_a') }} + {{ blueprint_var('field_b', 'default_b') }} AS field_b +FROM {{ blueprint_var('customer') }}.some_source +JINJA_END; +``` + +Blueprint variables can be accessed using the `{{ blueprint_var() }}` macro function, which also supports specifying default values in case the variable is undefined (similar to `{{ var() }}`). + + ### Local variables Define your own variables with the Jinja statement `{% set ... %}`. For example, we could specify the name of the `num_orders` column in the `sqlmesh_example.full_model` like this: diff --git a/sqlmesh/core/constants.py b/sqlmesh/core/constants.py index 131d3a990b..60c6a3eedf 100644 --- a/sqlmesh/core/constants.py +++ b/sqlmesh/core/constants.py @@ -79,6 +79,7 @@ SQLMESH_BLUEPRINT_VARS = "__sqlmesh__blueprint__vars__" VAR = "var" +BLUEPRINT_VAR = "blueprint_var" GATEWAY = "gateway" SQLMESH_MACRO = "__sqlmesh__macro__" diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index 403b8caba9..dcb09296b8 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -15,6 +15,7 @@ from sqlmesh.core import dialect as d from sqlmesh.utils import AttributeDict from sqlmesh.utils.pydantic import PRIVATE_FIELDS, PydanticModel, field_serializer, field_validator +from sqlmesh.utils.metaprogramming import SqlValue if t.TYPE_CHECKING: @@ -593,7 +594,10 @@ def jinja_call_arg_name(node: nodes.Node) -> str: def create_var(variables: t.Dict[str, t.Any]) -> t.Callable: def _var(var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]: - return variables.get(var_name.lower(), default) + value = variables.get(var_name.lower(), default) + if isinstance(value, SqlValue): + return value.sql + return value return _var @@ -603,10 +607,12 @@ def create_builtin_globals( ) -> t.Dict[str, t.Any]: global_vars.pop(c.GATEWAY, None) variables = global_vars.pop(c.SQLMESH_VARS, None) or {} + blueprint_variables = global_vars.pop(c.SQLMESH_BLUEPRINT_VARS, None) or {} return { + **global_vars, c.VAR: create_var(variables), c.GATEWAY: lambda: variables.get(c.GATEWAY, None), - **global_vars, + c.BLUEPRINT_VAR: create_var(blueprint_variables), } diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 647ae8d7f7..722cc0065b 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -8662,6 +8662,55 @@ def test_blueprint_variable_precedence_sql(tmp_path: Path, assert_exp_eq: t.Call ) +def test_blueprint_variable_jinja(tmp_path: Path, assert_exp_eq: t.Callable) -> None: + init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + + blueprint_variables = tmp_path / "models/blueprint_variables.sql" + blueprint_variables.parent.mkdir(parents=True, exist_ok=True) + blueprint_variables.write_text( + """ + MODEL ( + name s.@{bp_name}, + blueprints ( + (bp_name := m1, var1 := 'v1', var2 := v2), + (bp_name := m2, var1 := 'v3'), + ), + ); + + @DEF(bp_name, override); + + JINJA_QUERY_BEGIN; + SELECT + {{ blueprint_var('var1') }} AS var1, + '{{ blueprint_var('var2') }}' AS var2, + '{{ blueprint_var('var2', 'var2_default') }}' AS var2_default, + '{{ blueprint_var('bp_name') }}' AS bp_name + FROM s.{{ blueprint_var('bp_name') }}_source; + JINJA_END; + """ + ) + ctx = Context( + config=Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + variables={"var2": "1"}, + ), + paths=tmp_path, + ) + assert len(ctx.models) == 2 + + m1 = ctx.get_model("s.m1", raise_if_missing=True) + m2 = ctx.get_model("s.m2", raise_if_missing=True) + + assert_exp_eq( + m1.render_query(), + """SELECT 'v1' AS "var1", 'v2' AS "var2", 'v2' AS "var2_default", 'm1' AS "bp_name" FROM "memory"."s"."m1_source" AS "m1_source" """, + ) + assert_exp_eq( + m2.render_query(), + """SELECT 'v3' AS "var1", 'None' AS "var2", 'var2_default' AS "var2_default", 'm2' AS "bp_name" FROM "memory"."s"."m2_source" AS "m2_source" """, + ) + + def test_blueprint_variable_precedence_python(tmp_path: Path, mocker: MockerFixture) -> None: init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) From a52053fc5acf368bfab343de4d778ea1c1ecef2b Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 1 May 2025 10:34:47 +0100 Subject: [PATCH 0086/1056] feat(vscode): better errors for missing sqlmesh_lsp (#4288) --- vscode/extension/eslint.config.mjs | 1 + vscode/extension/src/commands/format.ts | 33 +++++++++---- vscode/extension/src/extension.ts | 44 +++++++++++++++-- vscode/extension/src/utilities/errors.ts | 47 ++++++++++++++++++ .../src/utilities/sqlmesh/sqlmesh.ts | 49 +++++++++++++++++++ 5 files changed, 160 insertions(+), 14 deletions(-) diff --git a/vscode/extension/eslint.config.mjs b/vscode/extension/eslint.config.mjs index 651221da64..be80cd9ab1 100644 --- a/vscode/extension/eslint.config.mjs +++ b/vscode/extension/eslint.config.mjs @@ -16,6 +16,7 @@ export default tseslint.config( }, { rules: { + 'no-fallthrough': 'error', '@typescript-eslint/switch-exhaustiveness-check': 'error', '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-explicit-any': 'off', diff --git a/vscode/extension/src/commands/format.ts b/vscode/extension/src/commands/format.ts index a340fcee3d..59d4c22177 100644 --- a/vscode/extension/src/commands/format.ts +++ b/vscode/extension/src/commands/format.ts @@ -3,22 +3,35 @@ import { execSync } from 'child_process' import { sqlmesh_exec } from '../utilities/sqlmesh/sqlmesh' import { err, isErr, ok, Result } from '../utilities/functional/result' import * as vscode from 'vscode' -import { ErrorType, handleNotSginedInError } from '../utilities/errors' +import { + ErrorType, + handleNotSginedInError, + handleSqlmeshLspNotFoundError, + handleSqlmeshLspDependenciesMissingError, +} from '../utilities/errors' import { AuthenticationProviderTobikoCloud } from '../auth/auth' export const format = - (authProvider: AuthenticationProviderTobikoCloud) => async () => { + (authProvider: AuthenticationProviderTobikoCloud) => + async (): Promise => { traceLog('Calling format') const out = await internalFormat() if (isErr(out)) { - if (out.error.type === 'not_signed_in') { - await handleNotSginedInError(authProvider) - return - } else { - vscode.window.showErrorMessage( - `Project format failed: ${out.error.message}`, - ) - return + switch (out.error.type) { + case 'not_signed_in': + await handleNotSginedInError(authProvider) + return + case 'sqlmesh_lsp_not_found': + await handleSqlmeshLspNotFoundError() + return + case 'sqlmesh_lsp_dependencies_missing': + await handleSqlmeshLspDependenciesMissingError(out.error) + return + case 'generic': + await vscode.window.showErrorMessage( + `Project format failed: ${out.error.message}`, + ) + return } } vscode.window.showInformationMessage('Project formatted successfully') diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index ef449057c1..cf41d2c031 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -13,7 +13,11 @@ import { signOut } from './commands/signout' import { signIn } from './commands/signin' import { signInSpecifyFlow } from './commands/signinSpecifyFlow' import { isErr } from './utilities/functional/result' -import { handleNotSginedInError } from './utilities/errors' +import { + handleNotSginedInError, + handleSqlmeshLspNotFoundError, + handleSqlmeshLspDependenciesMissingError, +} from './utilities/errors' let lspClient: LSPClient | undefined @@ -58,17 +62,49 @@ export async function activate(context: vscode.ExtensionContext) { lspClient = new LSPClient() const result = await lspClient.start() if (isErr(result)) { - await handleNotSginedInError(authProvider) + switch (result.error.type) { + case 'not_signed_in': + await handleNotSginedInError(authProvider) + break + case 'sqlmesh_lsp_not_found': + await handleSqlmeshLspNotFoundError() + break + case 'sqlmesh_lsp_dependencies_missing': + await handleSqlmeshLspDependenciesMissingError(result.error) + break + case 'generic': + await vscode.window.showErrorMessage( + `Failed to start LSP: ${result.error.message}`, + ) + break + } + } else { + context.subscriptions.push(lspClient) } - context.subscriptions.push(lspClient) const restart = async () => { if (lspClient) { traceVerbose('Restarting LSP client') const restartResult = await lspClient.restart() if (isErr(restartResult)) { - await handleNotSginedInError(authProvider) + switch (restartResult.error.type) { + case 'not_signed_in': + await handleNotSginedInError(authProvider) + return + case 'sqlmesh_lsp_not_found': + await handleSqlmeshLspNotFoundError() + return + case 'sqlmesh_lsp_dependencies_missing': + await handleSqlmeshLspDependenciesMissingError(restartResult.error) + return + case 'generic': + await vscode.window.showErrorMessage( + `Failed to restart LSP: ${restartResult.error.message}`, + ) + return + } } + context.subscriptions.push(lspClient) } } diff --git a/vscode/extension/src/utilities/errors.ts b/vscode/extension/src/utilities/errors.ts index a1f9ba3e47..9a1d02c162 100644 --- a/vscode/extension/src/utilities/errors.ts +++ b/vscode/extension/src/utilities/errors.ts @@ -9,6 +9,16 @@ import { traceInfo } from './common/log' export type ErrorType = | { type: 'generic'; message: string } | { type: 'not_signed_in' } + | { type: 'sqlmesh_lsp_not_found' } + // sqlmesh_lsp_dependencies_missing is used when the sqlmesh_lsp is found but the lsp extras are missing. + | SqlmeshLspDependenciesMissingError + +interface SqlmeshLspDependenciesMissingError { + type: 'sqlmesh_lsp_dependencies_missing' + is_missing_pygls: boolean + is_missing_lsprotocol: boolean + is_tobiko_cloud: boolean +} /** * Handles the case where the user is not signed in to Tobiko Cloud. @@ -26,3 +36,40 @@ export const handleNotSginedInError = async ( await signIn(authProvider)() } } + +/** + * Handles the case where the sqlmesh_lsp is not found. + */ +export const handleSqlmeshLspNotFoundError = async (): Promise => { + traceInfo('handleSqlmeshLspNotFoundError') + await window.showErrorMessage( + 'SQLMesh LSP not found, please check installation', + ) +} + +/** + * Handles the case where the sqlmesh_lsp is found but the lsp extras are missing. + */ +export const handleSqlmeshLspDependenciesMissingError = async ( + error: SqlmeshLspDependenciesMissingError, +): Promise => { + traceInfo('handleSqlmeshLspDependenciesMissingError') + if (error.is_tobiko_cloud) { + await window.showErrorMessage( + 'LSP dependencies missing, make sure to include `lsp` in the `extras` section of your `tcloud.yaml` file.', + ) + } else { + const install = await window.showErrorMessage( + 'LSP dependencies missing, make sure to install `sqlmesh[lsp]`.', + 'Install', + ) + if (install === 'Install') { + const terminal = window.createTerminal({ + name: 'SQLMesh LSP Install', + hideFromUser: false, + }) + terminal.show() + terminal.sendText("pip install 'sqlmesh[lsp]'", false) + } + } +} diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 40b01e76a6..05bb66a00b 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -231,6 +231,46 @@ export const sqlmesh_exec = async (): Promise< } } +/** + * Ensure that the sqlmesh_lsp dependencies are installed. + * + * @returns A Result indicating whether the sqlmesh_lsp dependencies were installed. + */ +export const ensureSqlmeshLspDependenciesInstalled = async (): Promise< + Result +> => { + const isPyglsInstalled = await isPythonModuleInstalled('pygls') + if (isErr(isPyglsInstalled)) { + return err({ + type: 'generic', + message: isPyglsInstalled.error, + }) + } + const isLsprotocolInstalled = await isPythonModuleInstalled('lsprotocol') + if (isErr(isLsprotocolInstalled)) { + return err({ + type: 'generic', + message: isLsprotocolInstalled.error, + }) + } + const isTobikoCloudInstalled = await isTcloudProject() + if (isErr(isTobikoCloudInstalled)) { + return err({ + type: 'generic', + message: isTobikoCloudInstalled.error, + }) + } + if (!isPyglsInstalled.value || !isLsprotocolInstalled.value) { + return err({ + type: 'sqlmesh_lsp_dependencies_missing', + is_missing_pygls: !isPyglsInstalled.value, + is_missing_lsprotocol: !isLsprotocolInstalled.value, + is_tobiko_cloud: isTobikoCloudInstalled.value, + }) + } + return ok(undefined) +} + /** * Get the sqlmesh_lsp executable for the current workspace. * @@ -282,8 +322,17 @@ export const sqlmesh_lsp_exec = async (): Promise< }) } } + const ensuredDependencies = await ensureSqlmeshLspDependenciesInstalled() + if (isErr(ensuredDependencies)) { + return ensuredDependencies + } const binPath = path.join(interpreterDetails.binPath!, 'sqlmesh_lsp') traceLog(`Bin path: ${binPath}`) + if (!fs.existsSync(binPath)) { + return err({ + type: 'sqlmesh_lsp_not_found', + }) + } return ok({ bin: binPath, workspacePath, From fd19aa0508f0bdbc6c6eff6cb604346d927c0315 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 1 May 2025 10:45:30 +0100 Subject: [PATCH 0087/1056] chore: update sqlglot (#4291) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4efc35cef1..9f0c41d196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.16.2", + "sqlglot[rs]~=26.16.3", "tenacity", "time-machine", "json-stream" From 4e8228ec05a65ef9bd4438d49614d9eb8fbd097e Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Thu, 1 May 2025 19:01:26 +0300 Subject: [PATCH 0088/1056] Fix: Enviroment & snapshot cleanup order (#4228) --- sqlmesh/core/context.py | 22 +++++-- sqlmesh/core/state_sync/base.py | 30 ++++++++- sqlmesh/core/state_sync/cache.py | 7 +- sqlmesh/core/state_sync/db/environment.py | 51 ++++++++++----- sqlmesh/core/state_sync/db/facade.py | 29 +++++++-- sqlmesh/core/state_sync/db/snapshot.py | 33 +++++++--- tests/core/state_sync/test_state_sync.py | 2 +- tests/core/test_context.py | 4 +- tests/core/test_integration.py | 79 +++++++++++++++++++++++ 9 files changed, 212 insertions(+), 45 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 692a5079de..4a906fdef4 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -117,7 +117,7 @@ from sqlmesh.core.user import User from sqlmesh.utils import UniqueKeyDict, Verbosity from sqlmesh.utils.dag import DAG -from sqlmesh.utils.date import TimeLike, now_ds, to_timestamp, format_tz_datetime +from sqlmesh.utils.date import TimeLike, now_ds, to_timestamp, format_tz_datetime, now_timestamp from sqlmesh.utils.errors import ( CircuitBreakerError, ConfigError, @@ -2350,11 +2350,14 @@ def _context_diff( ) def _run_janitor(self, ignore_ttl: bool = False) -> None: + current_ts = now_timestamp() + # Clean up expired environments by removing their views and schemas - self._cleanup_environments() + self._cleanup_environments(current_ts=current_ts) - # Identify and delete expired snapshots - cleanup_targets = self.state_sync.delete_expired_snapshots(ignore_ttl=ignore_ttl) + cleanup_targets = self.state_sync.get_expired_snapshots( + ignore_ttl=ignore_ttl, current_ts=current_ts + ) # Remove the expired snapshots tables self.snapshot_evaluator.cleanup( @@ -2362,10 +2365,15 @@ def _run_janitor(self, ignore_ttl: bool = False) -> None: on_complete=self.console.update_cleanup_progress, ) + # Delete the expired snapshot records from the state sync + self.state_sync.delete_expired_snapshots(ignore_ttl=ignore_ttl, current_ts=current_ts) + self.state_sync.compact_intervals() - def _cleanup_environments(self) -> None: - expired_environments = self.state_sync.delete_expired_environments() + def _cleanup_environments(self, current_ts: t.Optional[int] = None) -> None: + current_ts = current_ts or now_timestamp() + + expired_environments = self.state_sync.get_expired_environments(current_ts=current_ts) cleanup_expired_views( default_adapter=self.engine_adapter, @@ -2375,6 +2383,8 @@ def _cleanup_environments(self) -> None: console=self.console, ) + self.state_sync.delete_expired_environments(current_ts=current_ts) + def _try_connection(self, connection_name: str, validator: t.Callable[[], None]) -> None: connection_name = connection_name.capitalize() try: diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index da4a8ecb20..84bf550906 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -281,6 +281,28 @@ def export(self, environment_names: t.Optional[t.List[str]] = None) -> StateStre environment_names: An optional list of environment names to export. If not specified, all environments will be exported. """ + @abc.abstractmethod + def get_expired_snapshots( + self, current_ts: int, ignore_ttl: bool = False + ) -> t.List[SnapshotTableCleanupTask]: + """Aggregates the id's of the expired snapshots and creates a list of table cleanup tasks. + + Expired snapshots are snapshots that have exceeded their time-to-live + and are no longer in use within an environment. + + Returns: + The list of table cleanup tasks. + """ + + @abc.abstractmethod + def get_expired_environments(self, current_ts: int) -> t.List[Environment]: + """Returns the expired environments. + + Expired environments are environments that have exceeded their time-to-live value. + Returns: + The list of environments to remove, the filter to remove environments. + """ + class StateSync(StateReader, abc.ABC): """Abstract base class for snapshot and environment state management.""" @@ -309,7 +331,7 @@ def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None: @abc.abstractmethod def delete_expired_snapshots( - self, ignore_ttl: bool = False + self, ignore_ttl: bool = False, current_ts: t.Optional[int] = None ) -> t.List[SnapshotTableCleanupTask]: """Removes expired snapshots. @@ -321,7 +343,7 @@ def delete_expired_snapshots( all snapshots that are not referenced in any environment Returns: - The list of table cleanup tasks. + The list of snapshot table cleanup tasks. """ @abc.abstractmethod @@ -391,7 +413,9 @@ def finalize(self, environment: Environment) -> None: """ @abc.abstractmethod - def delete_expired_environments(self) -> t.List[Environment]: + def delete_expired_environments( + self, current_ts: t.Optional[int] = None + ) -> t.List[Environment]: """Removes expired environments. Expired environments are environments that have exceeded their time-to-live value. diff --git a/sqlmesh/core/state_sync/cache.py b/sqlmesh/core/state_sync/cache.py index 959f5eab86..37d63787c2 100644 --- a/sqlmesh/core/state_sync/cache.py +++ b/sqlmesh/core/state_sync/cache.py @@ -111,10 +111,13 @@ def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None: self.state_sync.delete_snapshots(snapshot_ids) def delete_expired_snapshots( - self, ignore_ttl: bool = False + self, ignore_ttl: bool = False, current_ts: t.Optional[int] = None ) -> t.List[SnapshotTableCleanupTask]: + current_ts = current_ts or now_timestamp() self.snapshot_cache.clear() - return self.state_sync.delete_expired_snapshots(ignore_ttl=ignore_ttl) + return self.state_sync.delete_expired_snapshots( + current_ts=current_ts, ignore_ttl=ignore_ttl + ) def add_snapshots_intervals(self, snapshots_intervals: t.Sequence[SnapshotIntervals]) -> None: for snapshot_intervals in snapshots_intervals: diff --git a/sqlmesh/core/state_sync/db/environment.py b/sqlmesh/core/state_sync/db/environment.py index 4ba563e179..e98f1633f6 100644 --- a/sqlmesh/core/state_sync/db/environment.py +++ b/sqlmesh/core/state_sync/db/environment.py @@ -162,43 +162,51 @@ def finalize(self, environment: Environment) -> None: where=environment_filter, ) - def delete_expired_environments(self) -> t.List[Environment]: - """Deletes expired environments. + def get_expired_environments(self, current_ts: int) -> t.List[Environment]: + """Returns the expired environments. + Expired environments are environments that have exceeded their time-to-live value. Returns: - A list of deleted environments. + The list of environments to remove, the filter to remove environments. """ - now_ts = now_timestamp() - filter_expr = exp.LTE( - this=exp.column("expiration_ts"), - expression=exp.Literal.number(now_ts), - ) - rows = fetchall( self.engine_adapter, self._environments_query( - where=filter_expr, + where=self._create_expiration_filter_expr(current_ts), lock_for_update=True, ), ) - environments = [self._environment_from_row(r) for r in rows] + expired_environments = [self._environment_from_row(r) for r in rows] + + return expired_environments + + def delete_expired_environments( + self, current_ts: t.Optional[int] = None + ) -> t.List[Environment]: + """Deletes expired environments. + + Returns: + A list of deleted environments. + """ + current_ts = current_ts or now_timestamp() + expired_environments = self.get_expired_environments(current_ts=current_ts) self.engine_adapter.delete_from( self.environments_table, - where=filter_expr, + where=self._create_expiration_filter_expr(current_ts), ) # Delete the expired environments' corresponding environment statements - if expired_environments := [ + if expired_environments_exprs := [ exp.EQ(this=exp.column("environment_name"), expression=exp.Literal.string(env.name)) - for env in environments + for env in expired_environments ]: self.engine_adapter.delete_from( self.environment_statements_table, - where=exp.or_(*expired_environments), + where=exp.or_(*expired_environments_exprs), ) - return environments + return expired_environments def get_environments(self) -> t.List[Environment]: """Fetches all environments. @@ -308,6 +316,17 @@ def _environments_query( return query.lock(copy=False) return query + def _create_expiration_filter_expr(self, current_ts: int) -> exp.Expression: + """Creates a SQLGlot filter expression to find expired environments. + + Args: + current_ts: The current timestamp. + """ + return exp.LTE( + this=exp.column("expiration_ts"), + expression=exp.Literal.number(current_ts), + ) + def _environment_to_df(environment: Environment) -> pd.DataFrame: return pd.DataFrame( diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 0d1d54ef27..e2a8523a9c 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -60,7 +60,7 @@ from sqlmesh.core.state_sync.db.snapshot import SnapshotState from sqlmesh.core.state_sync.db.version import VersionState from sqlmesh.core.state_sync.db.migrator import StateMigrator -from sqlmesh.utils.date import TimeLike, to_timestamp, time_like_to_str +from sqlmesh.utils.date import TimeLike, to_timestamp, time_like_to_str, now_timestamp from sqlmesh.utils.errors import ConflictingPlanError, SQLMeshError logger = logging.getLogger(__name__) @@ -273,19 +273,36 @@ def unpause_snapshots( def invalidate_environment(self, name: str) -> None: self.environment_state.invalidate_environment(name) + def get_expired_snapshots( + self, current_ts: int, ignore_ttl: bool = False + ) -> t.List[SnapshotTableCleanupTask]: + return self.snapshot_state.get_expired_snapshots( + self.environment_state.get_environments(), current_ts=current_ts, ignore_ttl=ignore_ttl + ) + + def get_expired_environments(self, current_ts: int) -> t.List[Environment]: + return self.environment_state.get_expired_environments(current_ts=current_ts) + @transactional() def delete_expired_snapshots( - self, ignore_ttl: bool = False + self, ignore_ttl: bool = False, current_ts: t.Optional[int] = None ) -> t.List[SnapshotTableCleanupTask]: - expired_snapshot_ids, cleanup_targets = self.snapshot_state.delete_expired_snapshots( - self.environment_state.get_environments(), ignore_ttl=ignore_ttl + current_ts = current_ts or now_timestamp() + expired_snapshot_ids, cleanup_targets = self.snapshot_state._get_expired_snapshots( + self.environment_state.get_environments(), ignore_ttl=ignore_ttl, current_ts=current_ts ) + + self.snapshot_state.delete_snapshots(expired_snapshot_ids) self.interval_state.cleanup_intervals(cleanup_targets, expired_snapshot_ids) + return cleanup_targets @transactional() - def delete_expired_environments(self) -> t.List[Environment]: - return self.environment_state.delete_expired_environments() + def delete_expired_environments( + self, current_ts: t.Optional[int] = None + ) -> t.List[Environment]: + current_ts = current_ts or now_timestamp() + return self.environment_state.delete_expired_environments(current_ts=current_ts) def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None: self.snapshot_state.delete_snapshots(snapshot_ids) diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index e46f7d0151..0f5eaac062 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -193,19 +193,34 @@ def unpause_snapshots( if unrestorable_snapshots: self._update_snapshots(unrestorable_snapshots, unrestorable=True) - def delete_expired_snapshots( - self, environments: t.Iterable[Environment], ignore_ttl: bool = False - ) -> t.Tuple[t.Set[SnapshotId], t.List[SnapshotTableCleanupTask]]: - """Deletes expired snapshots. + def get_expired_snapshots( + self, + environments: t.Iterable[Environment], + current_ts: int, + ignore_ttl: bool = False, + ) -> t.List[SnapshotTableCleanupTask]: + """Aggregates the id's of the expired snapshots and creates a list of table cleanup tasks. - Args: - ignore_ttl: Whether to ignore the TTL of the snapshots. + Expired snapshots are snapshots that have exceeded their time-to-live + and are no longer in use within an environment. Returns: - A tuple of expired snapshot IDs and cleanup targets. + The set of expired snapshot ids. + The list of table cleanup tasks. """ - current_ts = now_timestamp(minute_floor=False) + _, cleanup_targets = self._get_expired_snapshots( + environments=environments, + current_ts=current_ts, + ignore_ttl=ignore_ttl, + ) + return cleanup_targets + def _get_expired_snapshots( + self, + environments: t.Iterable[Environment], + current_ts: int, + ignore_ttl: bool = False, + ) -> t.Tuple[t.Set[SnapshotId], t.List[SnapshotTableCleanupTask]]: expired_query = exp.select("name", "identifier", "version").from_(self.snapshots_table) if not ignore_ttl: @@ -269,8 +284,6 @@ def _is_snapshot_used(snapshot: SharedVersionSnapshot) -> bool: ) ) - if expired_snapshot_ids: - self.delete_snapshots(expired_snapshot_ids) return expired_snapshot_ids, cleanup_targets def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None: diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index 175a14f40d..c450c22562 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -1255,7 +1255,7 @@ def test_delete_expired_snapshots_promoted( env.snapshots_ = [] state_sync.promote(env) - now_timestamp_mock = mocker.patch("sqlmesh.core.state_sync.db.snapshot.now_timestamp") + now_timestamp_mock = mocker.patch("sqlmesh.core.state_sync.db.facade.now_timestamp") now_timestamp_mock.return_value = now_timestamp() + 11000 assert state_sync.delete_expired_snapshots() == [ diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 1949413953..5609404012 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -775,7 +775,7 @@ def test_janitor(sushi_context, mocker: MockerFixture) -> None: adapter_mock.dialect = "duckdb" state_sync_mock = mocker.MagicMock() - state_sync_mock.delete_expired_environments.return_value = [ + state_sync_mock.get_expired_environments.return_value = [ Environment( name="test_environment", suffix_target=EnvironmentSuffixTarget.TABLE, @@ -798,6 +798,8 @@ def test_janitor(sushi_context, mocker: MockerFixture) -> None: sushi_context._engine_adapters = {sushi_context.config.default_gateway: adapter_mock} sushi_context._state_sync = state_sync_mock + state_sync_mock.get_expired_snapshots.return_value = [] + sushi_context._run_janitor() # Assert that the schemas are dropped just twice for the schema based environment # Make sure that external model schemas/tables are not dropped diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 5bbfe30929..fbadf19c13 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -5609,3 +5609,82 @@ def test_plan_environment_statements_doesnt_cause_extra_diff(tmp_path: Path): # second plan - nothing has changed so should report no changes assert not ctx.plan(auto_apply=True, no_prompts=True).has_changes + + +def test_janitor_cleanup_order(mocker: MockerFixture, tmp_path: Path): + def setup_scenario(): + models_dir = tmp_path / "models" + + if not models_dir.exists(): + models_dir.mkdir() + + model1_path = models_dir / "model1.sql" + + with open(model1_path, "w") as f: + f.write("MODEL(name test.model1, kind FULL); SELECT 1 AS col") + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + ) + ctx = Context(paths=[tmp_path], config=config) + + ctx.plan("dev", no_prompts=True, auto_apply=True) + + model1_snapshot = ctx.get_snapshot("test.model1") + + # Delete the model file to cause a snapshot expiration + model1_path.unlink() + + ctx.load() + + ctx.plan("dev", no_prompts=True, auto_apply=True) + + # Invalidate the environment to cause an environment cleanup + ctx.invalidate_environment("dev") + + try: + ctx._run_janitor(ignore_ttl=True) + except: + pass + + return ctx, model1_snapshot + + # Case 1: Assume that the snapshot cleanup yields an error, the snapshot records + # should still exist in the state sync so the next janitor can retry + mocker.patch( + "sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.cleanup", + side_effect=Exception("snapshot cleanup error"), + ) + ctx, model1_snapshot = setup_scenario() + + # - Check that the snapshot record exists in the state sync + state_snapshot = ctx.state_sync.state_sync.get_snapshots([model1_snapshot.snapshot_id]) + assert state_snapshot + + # - Run the janitor again, this time it should succeed + mocker.patch("sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.cleanup") + ctx._run_janitor(ignore_ttl=True) + + # - Check that the snapshot record does not exist in the state sync anymore + state_snapshot = ctx.state_sync.state_sync.get_snapshots([model1_snapshot.snapshot_id]) + assert not state_snapshot + + # Case 2: Assume that the view cleanup yields an error, the enviroment + # record should still exist + mocker.patch( + "sqlmesh.core.context.cleanup_expired_views", side_effect=Exception("view cleanup error") + ) + ctx, model1_snapshot = setup_scenario() + + views = ctx.fetchdf("FROM duckdb_views() SELECT * EXCLUDE(sql) WHERE NOT internal") + assert views.empty + + # - Check that the environment record exists in the state sync + assert ctx.state_sync.get_environment("dev") + + # - Run the janitor again, this time it should succeed + mocker.patch("sqlmesh.core.context.cleanup_expired_views") + ctx._run_janitor(ignore_ttl=True) + + # - Check that the environment record does not exist in the state sync anymore + assert not ctx.state_sync.get_environment("dev") From f3dd3d67605dd321f1dbfde7c8e5ec6d23c07604 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Thu, 1 May 2025 19:01:40 +0300 Subject: [PATCH 0089/1056] Fix: Avoid concurrent dialect patching in model testing (#4266) --- sqlmesh/core/test/definition.py | 80 ++++++++++++++++++------- sqlmesh/core/test/result.py | 3 +- sqlmesh/core/test/runner.py | 7 ++- tests/core/test_test.py | 100 ++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 26 deletions(-) diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index c79897783c..a40cc99061 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -1,10 +1,11 @@ from __future__ import annotations import datetime +import threading import typing as t import unittest from collections import Counter -from contextlib import AbstractContextManager, nullcontext +from contextlib import nullcontext, contextmanager, AbstractContextManager from itertools import chain from pathlib import Path from unittest.mock import patch @@ -46,6 +47,8 @@ class ModelTest(unittest.TestCase): __test__ = False + CONCURRENT_RENDER_LOCK = threading.Lock() + def __init__( self, body: t.Dict[str, t.Any], @@ -57,6 +60,7 @@ def __init__( path: Path | None = None, preserve_fixtures: bool = False, default_catalog: str | None = None, + concurrency: bool = False, ) -> None: """ModelTest encapsulates a unit test for a model. @@ -79,6 +83,7 @@ def __init__( self.preserve_fixtures = preserve_fixtures self.default_catalog = default_catalog self.dialect = dialect + self.concurrency = concurrency self._fixture_table_cache: t.Dict[str, exp.Table] = {} self._normalized_column_name_cache: t.Dict[str, str] = {} @@ -310,6 +315,7 @@ def create_test( path: Path | None, preserve_fixtures: bool = False, default_catalog: str | None = None, + concurrency: bool = False, ) -> t.Optional[ModelTest]: """Create a SqlModelTest or a PythonModelTest. @@ -353,6 +359,7 @@ def create_test( path, preserve_fixtures, default_catalog, + concurrency, ) def __str__(self) -> str: @@ -512,10 +519,34 @@ def _normalize_column_name(self, name: str) -> str: return normalized_name - def _execute(self, query: exp.Query) -> pd.DataFrame: + @contextmanager + def _concurrent_render_context(self) -> t.Iterator[None]: + """ + Context manager that ensures that the tests are executed safely in a concurrent environment. + This is needed in case `execution_time` is set, as we'd then have to: + - Freeze time through `time_machine` (not thread safe) + - Globally patch the SQLGlot dialect so that any date/time nodes are evaluated at the `execution_time` during generation + """ + import time_machine + + lock_ctx: AbstractContextManager = ( + self.CONCURRENT_RENDER_LOCK if self.concurrency else nullcontext() + ) + time_ctx: AbstractContextManager = nullcontext() + dialect_patch_ctx: AbstractContextManager = nullcontext() + + if self._execution_time: + time_ctx = time_machine.travel(self._execution_time, tick=False) + dialect_patch_ctx = patch.dict( + self._test_adapter_dialect.generator_class.TRANSFORMS, self._transforms + ) + + with lock_ctx, time_ctx, dialect_patch_ctx: + yield + + def _execute(self, query: exp.Query | str) -> pd.DataFrame: """Executes the given query using the testing engine adapter and returns a DataFrame.""" - with patch.dict(self._test_adapter_dialect.generator_class.TRANSFORMS, self._transforms): - return self.engine_adapter.fetchdf(query) + return self.engine_adapter.fetchdf(query) def _create_df( self, @@ -570,13 +601,25 @@ def test_ctes(self, ctes: t.Dict[str, exp.Expression], recursive: bool = False) for alias, cte in ctes.items(): cte_query = cte_query.with_(alias, cte.this, recursive=recursive) - actual = self._execute(cte_query) + with self._concurrent_render_context(): + # Similar to the model's query, we render the CTE query under the locked context + # so that the execution (fetchdf) can continue concurrently between the threads + sql = cte_query.sql( + self._test_adapter_dialect, pretty=self.engine_adapter._pretty_sql + ) + + actual = self._execute(sql) expected = self._create_df(values, columns=cte_query.named_selects, partial=partial) self.assert_equal(expected, actual, sort=sort, partial=partial) def runTest(self) -> None: - query = self._render_model_query() + with self._concurrent_render_context(): + # Render the model's query and generate the SQL under the locked context so that + # execution (fetchdf) can continue concurrently between the threads + query = self._render_model_query() + sql = query.sql(self._test_adapter_dialect, pretty=self.engine_adapter._pretty_sql) + with_clause = query.args.get("with") if with_clause: @@ -593,7 +636,7 @@ def runTest(self) -> None: partial = values.get("partial") sort = query.args.get("order") is None - actual = self._execute(query) + actual = self._execute(sql) expected = self._create_df(values, columns=self.model.columns_to_types, partial=partial) self.assert_equal(expected, actual, sort=sort, partial=partial) @@ -626,6 +669,7 @@ def __init__( path: Path | None = None, preserve_fixtures: bool = False, default_catalog: str | None = None, + concurrency: bool = False, ) -> None: """PythonModelTest encapsulates a unit test for a Python model. @@ -651,6 +695,7 @@ def __init__( path, preserve_fixtures, default_catalog, + concurrency, ) self.context = TestExecutionContext( @@ -674,22 +719,13 @@ def runTest(self) -> None: def _execute_model(self) -> pd.DataFrame: """Executes the python model and returns a DataFrame.""" - if self._execution_time: - import time_machine - - time_ctx: AbstractContextManager = time_machine.travel(self._execution_time, tick=False) - else: - time_ctx = nullcontext() + with self._concurrent_render_context(): + variables = self.body.get("vars", {}).copy() + time_kwargs = {key: variables.pop(key) for key in TIME_KWARG_KEYS if key in variables} + df = next(self.model.render(context=self.context, **time_kwargs, **variables)) - with patch.dict(self._test_adapter_dialect.generator_class.TRANSFORMS, self._transforms): - with time_ctx: - variables = self.body.get("vars", {}).copy() - time_kwargs = { - key: variables.pop(key) for key in TIME_KWARG_KEYS if key in variables - } - df = next(self.model.render(context=self.context, **time_kwargs, **variables)) - assert not isinstance(df, exp.Expression) - return df if isinstance(df, pd.DataFrame) else df.toPandas() + assert not isinstance(df, exp.Expression) + return df if isinstance(df, pd.DataFrame) else df.toPandas() def generate_test( diff --git a/sqlmesh/core/test/result.py b/sqlmesh/core/test/result.py index e5baa166bd..7515955191 100644 --- a/sqlmesh/core/test/result.py +++ b/sqlmesh/core/test/result.py @@ -100,7 +100,8 @@ def log_test_report(self, test_duration: float) -> None: for test_case, failure in failures: stream.writeln(unittest.TextTestResult.separator1) stream.writeln(f"FAIL: {test_case}") - stream.writeln(f"{test_case.shortDescription()}") + if test_description := test_case.shortDescription(): + stream.writeln(test_description) stream.writeln(unittest.TextTestResult.separator2) stream.writeln(failure) diff --git a/sqlmesh/core/test/runner.py b/sqlmesh/core/test/runner.py index a070a6e34b..5775e93710 100644 --- a/sqlmesh/core/test/runner.py +++ b/sqlmesh/core/test/runner.py @@ -120,6 +120,9 @@ def run_tests( default_catalog_dialect=default_catalog_dialect, ) + # 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]: @@ -132,6 +135,7 @@ def _run_single_test( path=metadata.path, default_catalog=default_catalog, preserve_fixtures=preserve_fixtures, + concurrency=num_workers > 1, ) if not test: @@ -159,9 +163,6 @@ def _run_single_test( test_results = [] - # Ensure workers are not greater than the number of tests - num_workers = min(len(model_test_metadata) or 1, default_test_connection.concurrent_tasks) - start_time = time.perf_counter() try: with ThreadPoolExecutor(max_workers=num_workers) as pool: diff --git a/tests/core/test_test.py b/tests/core/test_test.py index df9ff3ea5e..74acdf76ad 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -2370,3 +2370,103 @@ def test_number_of_tests_found(tmp_path: Path) -> None: # Case 3: The "new_test.yaml::test_example_full_model2" should amount to a single subtest results = context.test(tests=[f"{test_file}::test_example_full_model2"]) assert len(results.successes) == 1 + + +def test_freeze_time_concurrent(tmp_path: Path) -> None: + tests_dir = tmp_path / "tests" + tests_dir.mkdir() + + macros_dir = tmp_path / "macros" + macros_dir.mkdir() + + macro_file = macros_dir / "test_datetime_now.py" + macro_file.write_text( + """ +from sqlglot import exp +import datetime +from sqlmesh.core.macros import macro + +@macro() +def test_datetime_now(evaluator): + return exp.cast(exp.Literal.string(datetime.datetime.now(tz=datetime.timezone.utc)), exp.DataType.Type.DATE) + +@macro() +def test_sqlglot_expr(evaluator): + return exp.CurrentDate().sql(evaluator.dialect) + """ + ) + + models_dir = tmp_path / "models" + models_dir.mkdir() + sql_model1 = models_dir / "sql_model1.sql" + sql_model1.write_text( + """ + MODEL(NAME sql_model1); + SELECT @test_datetime_now() AS col_exec_ds_time, @test_sqlglot_expr() AS col_current_date; + """ + ) + + for model_name in ["sql_model1", "sql_model2", "py_model"]: + for i in range(5): + test_2019 = tmp_path / "tests" / f"test_2019_{model_name}_{i}.yaml" + test_2019.write_text( + f""" + test_2019_{model_name}_{i}: + model: {model_name} + vars: + execution_time: '2019-12-01' + outputs: + query: + rows: + - col_exec_ds_time: '2019-12-01' + col_current_date: '2019-12-01' + """ + ) + + test_2025 = tmp_path / "tests" / f"test_2025_{model_name}_{i}.yaml" + test_2025.write_text( + f""" + test_2025_{model_name}_{i}: + model: {model_name} + vars: + execution_time: '2025-12-01' + outputs: + query: + rows: + - col_exec_ds_time: '2025-12-01' + col_current_date: '2025-12-01' + """ + ) + + ctx = Context( + paths=tmp_path, + config=Config(default_test_connection=DuckDBConnectionConfig(concurrent_tasks=8)), + ) + + @model( + "py_model", + columns={"col_exec_ds_time": "timestamp_ntz", "col_current_date": "timestamp_ntz"}, + ) + def execute(context, start, end, execution_time, **kwargs): + datetime_now_utc = datetime.datetime.now(tz=datetime.timezone.utc) + + context.engine_adapter.execute(exp.select("CURRENT_DATE()")) + current_date = context.engine_adapter.cursor.fetchone()[0] + + return pd.DataFrame( + [{"col_exec_ds_time": datetime_now_utc, "col_current_date": current_date}] + ) + + python_model = model.get_registry()["py_model"].model(module_path=Path("."), path=Path(".")) + ctx.upsert_model(python_model) + + ctx.upsert_model( + _create_model( + meta="MODEL(NAME sql_model2)", + query="SELECT @execution_ds::timestamp_ntz AS col_exec_ds_time, current_date()::date AS col_current_date", + default_catalog=ctx.default_catalog, + ) + ) + + results = ctx.test() + assert len(results.successes) == 30 From 2ab08afb871e27f1c1c0b25e2bd36887372fac1b Mon Sep 17 00:00:00 2001 From: blecourt-private <63446990+blecourt-private@users.noreply.github.com> Date: Thu, 1 May 2025 20:24:45 +0200 Subject: [PATCH 0090/1056] Fix: Add guards in render definition for on virtual update statements (#4293) --- sqlmesh/core/model/definition.py | 6 +++-- tests/core/test_context.py | 43 ++++++++++++++++++-------------- tests/core/test_model.py | 37 +++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 34f5dd5511..ff46232d30 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -1319,12 +1319,14 @@ def render_definition( result.extend(self.render_pre_statements()) result.append(self.render_query() or self.query) result.extend(self.render_post_statements()) - result.extend(self.render_on_virtual_update()) + if virtual_update := self.render_on_virtual_update(): + result.append(d.VirtualUpdateStatement(expressions=virtual_update)) else: result.extend(self.pre_statements) result.append(self.query) result.extend(self.post_statements) - result.extend(self.on_virtual_update) + if self.on_virtual_update: + result.append(d.VirtualUpdateStatement(expressions=self.on_virtual_update)) return result diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 5609404012..3fdef03f17 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1290,25 +1290,30 @@ def test_rendered_diff(): plan = ctx.plan("dev", auto_apply=True, no_prompts=True, diff_rendered=True) - assert '''@@ -4,13 +4,13 @@ - - CREATE TABLE IF NOT EXISTS "foo" AS - ( - SELECT -- FALSE OR TRUE -+ TRUE - ) - SELECT -- 6 AS "_col_0" -+ 7 AS "_col_0" - CREATE TABLE IF NOT EXISTS "foo2" AS - ( - SELECT -- TRUE AND FALSE -+ TRUE - ) --DROP VIEW "test" -+DROP VIEW IF EXISTS "test"''' in plan.context_diff.text_diff('"test"') + assert plan.context_diff.text_diff('"test"') == ( + "--- \n\n" + "+++ \n\n" + "@@ -4,15 +4,15 @@\n\n" + ' CREATE TABLE IF NOT EXISTS "foo" AS\n' + " (\n" + " SELECT\n" + "- FALSE OR TRUE\n" + "+ TRUE\n" + " )\n" + " SELECT\n" + '- 6 AS "_col_0"\n' + '+ 7 AS "_col_0"\n' + ' CREATE TABLE IF NOT EXISTS "foo2" AS\n' + " (\n" + " SELECT\n" + "- TRUE AND FALSE\n" + "+ TRUE\n" + " )\n" + " ON_VIRTUAL_UPDATE_BEGIN;\n" + '-DROP VIEW "test";\n' + '+DROP VIEW IF EXISTS "test";\n' + " ON_VIRTUAL_UPDATE_END;" + ) def test_plan_enable_preview_default(sushi_context: Context, sushi_dbt_context: Context): diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 722cc0065b..2a72903f52 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -1642,6 +1642,43 @@ def test_render_definition_partitioned_by(): ) +def test_render_definition_with_virtual_update_statements(): + # model has virtual update statements + model = load_sql_based_model( + d.parse( + f""" + MODEL ( + name db.table, + kind FULL + ); + + select 1 as a; + + ON_VIRTUAL_UPDATE_BEGIN; + GRANT SELECT ON VIEW @this_model TO ROLE role_name + ON_VIRTUAL_UPDATE_END; + """ + ) + ) + + assert model.on_virtual_update == [ + exp.Grant( + privileges=[exp.GrantPrivilege(this=exp.Var(this="SELECT"))], + kind="VIEW", + securable=exp.Table(this=d.MacroVar(this="this_model")), + principals=[ + exp.GrantPrincipal(this=exp.Identifier(this="role_name", quoted=False), kind="ROLE") + ], + ) + ] + assert ( + model.render_definition()[-1].sql(pretty=True) + == """ON_VIRTUAL_UPDATE_BEGIN; +GRANT SELECT ON VIEW @this_model TO ROLE role_name; +ON_VIRTUAL_UPDATE_END;""" + ) + + def test_cron(): daily = _Node(name="x", cron="@daily") assert to_datetime(daily.cron_prev("2020-01-01")) == to_datetime("2019-12-31") From 3271ae13f90c13f6d7ce458b135310cd3aa54392 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 1 May 2025 15:33:31 -0700 Subject: [PATCH 0091/1056] Fix!: Improve the table_name command (#4299) --- docs/guides/table_migration.md | 2 +- docs/reference/cli.md | 8 +++-- sqlmesh/cli/main.py | 16 +++++++--- sqlmesh/core/context.py | 42 +++++++++++++++++------- sqlmesh/magics.py | 13 ++++++-- tests/core/test_integration.py | 58 ++++++++++++++++++++++++++++++++-- 6 files changed, 115 insertions(+), 24 deletions(-) diff --git a/docs/guides/table_migration.md b/docs/guides/table_migration.md index 4072649bcf..5556986316 100644 --- a/docs/guides/table_migration.md +++ b/docs/guides/table_migration.md @@ -131,7 +131,7 @@ Consider an existing table named `my_schema.existing_table`. Migrating this tabl c. Create the model in the SQLMesh project without backfilling any data by running `sqlmesh plan [environment name] --empty-backfill --start 2024-01-01`, replacing "[environment name]" with an environment name other than `prod` and using the same start date from the `MODEL` DDL in step 3b. -4. Determine the name of the model's snapshot physical table by running `sqlmesh table_name my_schema.existing_table`. For example, it might return `sqlmesh__my_schema.existing_table_123456`. +4. Determine the name of the model's snapshot physical table by running `sqlmesh table_name --env [environment name] my_schema.existing_table`. For example, it might return `sqlmesh__my_schema.existing_table_123456`. 5. Rename the original table `my_schema.existing_table_temp` to `sqlmesh__my_schema.existing_table_123456` The model would have code similar to: diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 35586b9573..cd2c852b1b 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -559,9 +559,11 @@ Usage: sqlmesh table_name [OPTIONS] MODEL_NAME Prints the name of the physical table for the given model. Options: - --dev Print the name of the snapshot table used for previews in - development environments. - --help Show this message and exit. + --environment, --env TEXT The environment to source the model version from. + --prod If set, return the name of the physical table + that will be used in production for the model + version promoted in the target environment. + --help Show this message and exit. ``` ## test diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 6bdb50be4a..cfec00c9b1 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -29,6 +29,7 @@ "run", "environments", "invalidate", + "table_name", ) SKIP_CONTEXT_COMMANDS = ("init", "ui") @@ -988,17 +989,24 @@ def clean(obj: Context) -> None: @cli.command("table_name") @click.argument("model_name", required=True) @click.option( - "--dev", + "--environment", + "--env", + help="The environment to source the model version from.", +) +@click.option( + "--prod", is_flag=True, - help="Print the name of the snapshot table used for previews in development environments.", default=False, + help="If set, return the name of the physical table that will be used in production for the model version promoted in the target environment.", ) @click.pass_obj @error_handler @cli_analytics -def table_name(obj: Context, model_name: str, dev: bool) -> None: +def table_name( + obj: Context, model_name: str, environment: t.Optional[str] = None, prod: bool = False +) -> None: """Prints the name of the physical table for the given model.""" - print(obj.table_name(model_name, dev)) + print(obj.table_name(model_name, environment, prod)) @cli.command("dlt_refresh") diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 4a906fdef4..18aadde20f 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -2135,25 +2135,45 @@ def _apply(self, plan: Plan, circuit_breaker: t.Optional[t.Callable[[], bool]]) ) @python_api_analytics - def table_name(self, model_name: str, dev: bool) -> str: - """Returns the name of the pysical table for the given model name. + def table_name( + self, model_name: str, environment: t.Optional[str] = None, prod: bool = False + ) -> str: + """Returns the name of the pysical table for the given model name in the target environment. Args: model_name: The name of the model. - dev: Whether to use the deployability index for the table name. + environment: The environment to source the model version from. + prod: If True, return the name of the physical table that will be used in production for the model version + promoted in the target environment. Returns: The name of the physical table. """ - deployability_index = ( - DeployabilityIndex.create(self.snapshots.values()) - if dev - else DeployabilityIndex.all_deployable() + environment = environment or self.config.default_target_environment + fqn = self._node_or_snapshot_to_fqn(model_name) + target_env = self.state_reader.get_environment(environment) + if not target_env: + raise SQLMeshError(f"Environment '{environment}' was not found.") + + snapshot_info = None + for s in target_env.snapshots: + if s.name == fqn: + snapshot_info = s + break + if not snapshot_info: + raise SQLMeshError( + f"Model '{model_name}' was not found in environment '{environment}'." + ) + + if target_env.name == c.PROD or prod: + return snapshot_info.table_name() + + snapshots = self.state_reader.get_snapshots(target_env.snapshots) + deployability_index = DeployabilityIndex.create(snapshots) + + return snapshot_info.table_name( + is_deployable=deployability_index.is_deployable(snapshot_info.snapshot_id) ) - snapshot = self.get_snapshot(model_name) - if not snapshot: - raise SQLMeshError(f"Model '{model_name}' was not found.") - return snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot)) def clear_caches(self) -> None: for path in self.configs: diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 020d4ba8d1..80554a60a2 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -721,16 +721,23 @@ def table_diff(self, context: Context, line: str) -> None: help="The name of the model to get the table name for.", ) @argument( - "--dev", + "--environment", + type=str, + help="The environment to source the model version from.", + ) + @argument( + "--prod", action="store_true", - help="Print the name of the snapshot table used for previews in development environments.", + help="If set, return the name of the physical table that will be used in production for the model version promoted in the target environment.", ) @line_magic @pass_sqlmesh_context def table_name(self, context: Context, line: str) -> None: """Prints the name of the physical table for the given model.""" args = parse_argstring(self.table_name, line) - context.console.log_status_update(context.table_name(args.model_name, args.dev)) + context.console.log_status_update( + context.table_name(args.model_name, args.environment, args.prod) + ) @magic_arguments() @argument( diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index fbadf19c13..2fca1681a6 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -61,7 +61,7 @@ SnapshotTableInfo, ) from sqlmesh.utils.date import TimeLike, now, to_date, to_datetime, to_timestamp -from sqlmesh.utils.errors import NoChangesPlanError +from sqlmesh.utils.errors import NoChangesPlanError, SQLMeshError from sqlmesh.utils.pydantic import validate_string from tests.conftest import DuckDBMetadata, SushiDataValidator from tests.utils.test_helpers import use_terminal_console @@ -3034,7 +3034,7 @@ def _dates_in_table(table_name: str) -> t.List[str]: ] # mess with A independently of SQLMesh to prove a whole day gets restated for B instead of just 1hr - snapshot_table_name = ctx.table_name("test.a", False) + snapshot_table_name = ctx.table_name("test.a", "dev") engine_adapter.execute( f"delete from {snapshot_table_name} where cast(ts as date) == '2024-01-01'" ) @@ -4094,6 +4094,60 @@ def test_evaluate_uncategorized_snapshot(init_and_plan_context: t.Callable): assert set(df["one"].tolist()) == {1} +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_table_name(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") + assert snapshot + assert ( + context.table_name("sushi.waiter_revenue_by_day", "prod") + == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{snapshot.version}" + ) + + with pytest.raises(SQLMeshError, match="Environment 'dev' was not found."): + context.table_name("sushi.waiter_revenue_by_day", "dev") + + with pytest.raises( + SQLMeshError, match="Model 'sushi.missing' was not found in environment 'prod'." + ): + context.table_name("sushi.missing", "prod") + + # Add a new projection + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + context.plan("dev_a", auto_apply=True, no_prompts=True, skip_tests=True) + + new_snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") + assert new_snapshot.version != snapshot.version + + assert ( + context.table_name("sushi.waiter_revenue_by_day", "dev_a") + == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{new_snapshot.version}" + ) + + # Make a forward-only change + context.upsert_model(model, stamp="forward_only") + + context.plan("dev_b", auto_apply=True, no_prompts=True, skip_tests=True, forward_only=True) + + forward_only_snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") + assert forward_only_snapshot.version == snapshot.version + assert forward_only_snapshot.dev_version != snapshot.version + + assert ( + context.table_name("sushi.waiter_revenue_by_day", "dev_b") + == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{forward_only_snapshot.dev_version}__dev" + ) + + assert ( + context.table_name("sushi.waiter_revenue_by_day", "dev_b", prod=True) + == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{snapshot.version}" + ) + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_dbt_requirements(sushi_dbt_context: Context): assert set(sushi_dbt_context.requirements) == {"dbt-core", "dbt-duckdb"} From 029acc6c351d7cc31f5e288faea138877d8dafd0 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 2 May 2025 18:08:32 +0100 Subject: [PATCH 0092/1056] feat: add go to definition to lsp (#4297) --- Makefile | 2 +- sqlmesh/lsp/context.py | 19 ++++ sqlmesh/lsp/main.py | 55 +++++++---- sqlmesh/lsp/reference.py | 161 ++++++++++++++++++++++++++++++++ tests/lsp/test_context.py | 19 ++++ tests/lsp/test_reference.py | 48 ++++++++++ vscode/extension/src/lsp/lsp.ts | 4 +- 7 files changed, 290 insertions(+), 18 deletions(-) create mode 100644 sqlmesh/lsp/context.py create mode 100644 sqlmesh/lsp/reference.py create mode 100644 tests/lsp/test_context.py create mode 100644 tests/lsp/test_reference.py diff --git a/Makefile b/Makefile index 55b4d37225..6b0a8e0c58 100644 --- a/Makefile +++ b/Makefile @@ -109,7 +109,7 @@ guard-%: fi engine-%-install: - pip3 install -e ".[dev,web,slack,${*}]" ./examples/custom_materializations + pip3 install -e ".[dev,web,slack,lsp,${*}]" ./examples/custom_materializations engine-docker-%-up: docker compose -f ./tests/core/engine_adapter/integration/docker/compose.${*}.yaml up -d diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py new file mode 100644 index 0000000000..08db9bb56e --- /dev/null +++ b/sqlmesh/lsp/context.py @@ -0,0 +1,19 @@ +from collections import defaultdict +from pathlib import Path +from sqlmesh.core.context import Context +import typing as t + + +class LSPContext: + """ + A context that is used for linting. It contains the context and a reverse map of file uri to model names . + """ + + def __init__(self, context: Context) -> None: + self.context = context + map: t.Dict[str, t.List[str]] = defaultdict(list) + for model in context.models.values(): + if model._path is not None: + path = Path(model._path).resolve() + map[f"file://{path.as_posix()}"].append(model.name) + self.map = map diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 66da134f52..ef64e1875c 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -1,7 +1,6 @@ #!/usr/bin/env python """A Language Server Protocol (LSP) server for SQL with SQLMesh integration, refactored without globals.""" -from collections import defaultdict import logging import typing as t from pathlib import Path @@ -12,21 +11,8 @@ from sqlmesh._version import __version__ from sqlmesh.core.context import Context from sqlmesh.core.linter.definition import AnnotatedRuleViolation - - -class LSPContext: - """ - A context that is used for linting. It contains the context and a reverse map of file uri to model names . - """ - - def __init__(self, context: Context) -> None: - self.context = context - map: t.Dict[str, t.List[str]] = defaultdict(list) - for model in context.models.values(): - if model._path is not None: - path = Path(model._path).resolve() - map[f"file://{path.as_posix()}"].append(model.name) - self.map = map +from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.reference import get_model_definitions_for_a_path class SQLMeshLanguageServer: @@ -144,6 +130,43 @@ def formatting( ls.show_message(f"Error formatting SQL: {e}", types.MessageType.Error) return [] + @self.server.feature(types.TEXT_DOCUMENT_DEFINITION) + def goto_definition( + ls: LanguageServer, params: types.DefinitionParams + ) -> t.List[types.LocationLink]: + """Jump to an object's definition.""" + try: + self._ensure_context_for_document(params.text_document.uri) + document = ls.workspace.get_document(params.text_document.uri) + if self.lsp_context is None: + raise RuntimeError(f"No context found for document: {document.path}") + + references = get_model_definitions_for_a_path( + self.lsp_context, params.text_document.uri + ) + if not references: + return [] + + return [ + types.LocationLink( + target_uri=reference.uri, + target_selection_range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=0, character=0), + ), + target_range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=0, character=0), + ), + origin_selection_range=reference.range, + ) + for reference in references + ] + + except Exception as e: + ls.show_message(f"Error getting references: {e}", types.MessageType.Error) + return [] + def _context_get_or_load(self, document_uri: str) -> LSPContext: if self.lsp_context is None: self._ensure_context_for_document(document_uri) diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py new file mode 100644 index 0000000000..06b88871be --- /dev/null +++ b/sqlmesh/lsp/reference.py @@ -0,0 +1,161 @@ +from lsprotocol.types import Range, Position +import typing as t + +from sqlmesh.core.dialect import normalize_model_name +from sqlmesh.core.model.definition import SqlModel +from sqlmesh.lsp.context import LSPContext +from sqlglot import exp + +from sqlmesh.utils.pydantic import PydanticModel + + +class Reference(PydanticModel): + range: Range + uri: str + + +def get_model_definitions_for_a_path( + lint_context: LSPContext, document_uri: str +) -> t.List[Reference]: + """ + Get the model references for a given path. + + Works for models and audits. + Works for targeting sql and python models. + + Steps: + - Get the parsed query + - Find all table objects using find_all exp.Table + - Match the string against all model names + - Need to normalize it before matching + - Try get_model before normalization + - Match to models that the model refers to + """ + # Ensure the path is a sql model + if not document_uri.endswith(".sql"): + return [] + + # Get the model + models = lint_context.map[document_uri] + if not models: + return [] + model = lint_context.context.get_model(model_or_snapshot=models[0], raise_if_missing=False) + if model is None or not isinstance(model, SqlModel): + return [] + + # Find all possible references + references = [] + tables = list(model.query.find_all(exp.Table)) + if len(tables) == 0: + return [] + + read_file = open(model._path, "r").readlines() + + for table in tables: + depends_on = model.depends_on + + # Normalize the table reference + reference_name = table.sql(dialect=model.dialect) + normalized_reference_name = normalize_model_name( + reference_name, + default_catalog=lint_context.context.default_catalog, + dialect=model.dialect, + ) + if normalized_reference_name not in depends_on: + continue + + # Get the referenced model uri + referenced_model = lint_context.context.get_model( + model_or_snapshot=normalized_reference_name, raise_if_missing=False + ) + if referenced_model is None: + continue + referenced_model_path = referenced_model._path + # Check whether the path exists + if not referenced_model_path.is_file(): + continue + referenced_model_uri = f"file://{referenced_model_path}" + + # Extract metadata for positioning + table_meta = TokenPositionDetails.from_meta(table.this.meta) + table_range = _range_from_token_position_details(table_meta, read_file) + start_pos = table_range.start + end_pos = table_range.end + + # If there's a catalog or database qualifier, adjust the start position + catalog_or_db = table.args.get("catalog") or table.args.get("db") + if catalog_or_db is not None: + catalog_or_db_meta = TokenPositionDetails.from_meta(catalog_or_db.meta) + catalog_or_db_range = _range_from_token_position_details(catalog_or_db_meta, read_file) + start_pos = catalog_or_db_range.start + + references.append( + Reference(uri=referenced_model_uri, range=Range(start=start_pos, end=end_pos)) + ) + + return references + + +class TokenPositionDetails(PydanticModel): + """ + Details about a token's position in the source code. + + Attributes: + line (int): The line that the token ends on. + col (int): The column that the token ends on. + start (int): The start index of the token. + end (int): The ending index of the token. + """ + + line: int + col: int + start: int + end: int + + @staticmethod + def from_meta(meta: t.Dict[str, int]) -> "TokenPositionDetails": + return TokenPositionDetails( + line=meta["line"], + col=meta["col"], + start=meta["start"], + end=meta["end"], + ) + + +def _range_from_token_position_details( + token_position_details: TokenPositionDetails, read_file: t.List[str] +) -> Range: + """ + Convert a TokenPositionDetails object to a Range object. + + :param token_position_details: Details about a token's position + :param read_file: List of lines from the file + :return: A Range object representing the token's position + """ + # Convert from 1-indexed to 0-indexed for line only + end_line_0 = token_position_details.line - 1 + end_col_0 = token_position_details.col + + # Find the start line and column by counting backwards from the end position + start_pos = token_position_details.start + end_pos = token_position_details.end + + # Initialize with the end position + start_line_0 = end_line_0 + start_col_0 = end_col_0 - (end_pos - start_pos + 1) + + # If start_col_0 is negative, we need to go back to previous lines + while start_col_0 < 0 and start_line_0 > 0: + start_line_0 -= 1 + start_col_0 += len(read_file[start_line_0]) + # Account for newline character + if start_col_0 >= 0: + break + start_col_0 += 1 # For the newline character + + # Ensure we don't have negative values + start_col_0 = max(0, start_col_0) + return Range( + start=Position(line=start_line_0, character=start_col_0), + end=Position(line=end_line_0, character=end_col_0), + ) diff --git a/tests/lsp/test_context.py b/tests/lsp/test_context.py new file mode 100644 index 0000000000..4bfd094cb3 --- /dev/null +++ b/tests/lsp/test_context.py @@ -0,0 +1,19 @@ +import pytest +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext + + +@pytest.mark.fast +def test_lsp_context(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + assert lsp_context is not None + assert lsp_context.context is not None + assert lsp_context.map is not None + + # find one model in the map + active_customers_key = next( + key for key in lsp_context.map.keys() if key.endswith("models/active_customers.sql") + ) + assert lsp_context.map[active_customers_key] == ["sushi.active_customers"] diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py new file mode 100644 index 0000000000..4ef25107ad --- /dev/null +++ b/tests/lsp/test_reference.py @@ -0,0 +1,48 @@ +import pytest +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.reference import get_model_definitions_for_a_path + + +@pytest.mark.fast +def test_reference() -> None: + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + active_customers_uri = next( + uri for uri, models in lsp_context.map.items() if "sushi.active_customers" in models + ) + sushi_customers_uri = next( + uri for uri, models in lsp_context.map.items() if "sushi.customers" in models + ) + + references = get_model_definitions_for_a_path(lsp_context, active_customers_uri) + + assert len(references) == 1 + assert references[0].uri == sushi_customers_uri + + # Check that the reference in the correct range is sushi.customers + path = active_customers_uri.removeprefix("file://") + read_file = open(path, "r").readlines() + # Get the string range in the read file + reference_range = references[0].range + start_line = reference_range.start.line + end_line = reference_range.end.line + start_character = reference_range.start.character + end_character = reference_range.end.character + # Get the string from the file + + # If the reference spans multiple lines, handle it accordingly + if start_line == end_line: + # Reference is on a single line + line_content = read_file[start_line] + referenced_text = line_content[start_character:end_character] + else: + # Reference spans multiple lines + referenced_text = read_file[start_line][ + start_character: + ] # First line from start_character to end + for line_num in range(start_line + 1, end_line): # Middle lines (if any) + referenced_text += read_file[line_num] + referenced_text += read_file[end_line][:end_character] # Last line up to end_character + assert referenced_text == "sushi.customers" diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 4e4564e81f..63b932a0d5 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -27,7 +27,9 @@ export class LSPClient implements Disposable { const sqlmesh = await sqlmesh_lsp_exec() if (isErr(sqlmesh)) { - traceError(`Failed to get sqlmesh_lsp_exec, ${sqlmesh.error.type}`) + traceError( + `Failed to get sqlmesh_lsp_exec, ${JSON.stringify(sqlmesh.error)}`, + ) return sqlmesh } const workspaceFolders = getWorkspaceFolders() From fce766bae10aba9de048450ed73954d960516fd5 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 2 May 2025 14:19:08 -0700 Subject: [PATCH 0093/1056] Fix: Iterate through the whole DAG before raising an error when creating physical tables (#4305) --- sqlmesh/core/plan/evaluator.py | 9 ++++++--- sqlmesh/core/snapshot/__init__.py | 5 ++++- sqlmesh/core/snapshot/evaluator.py | 13 ++++++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 356811cbb6..4bada8526b 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -33,6 +33,7 @@ SnapshotId, SnapshotInfoLike, SnapshotTableInfo, + SnapshotCreationFailedError, ) from sqlmesh.utils import CompletionStatus from sqlmesh.core.state_sync import StateSync @@ -291,12 +292,14 @@ def _should_create(s: Snapshot) -> bool: ), on_complete=self.console.update_creation_progress, ) - except NodeExecutionFailedError as ex: + except SnapshotCreationFailedError as ex: self.console.stop_creation_progress(success=False) progress_stopped = True - logger.info(str(ex), exc_info=ex) - self.console.log_failed_models([ex]) + for error in ex.errors: + logger.info(str(error), exc_info=error) + + self.console.log_failed_models(ex.errors) raise PlanError("Plan application failed.") finally: diff --git a/sqlmesh/core/snapshot/__init__.py b/sqlmesh/core/snapshot/__init__.py index 74129bed67..da44278aa8 100644 --- a/sqlmesh/core/snapshot/__init__.py +++ b/sqlmesh/core/snapshot/__init__.py @@ -26,4 +26,7 @@ table_name as table_name, to_table_mapping as to_table_mapping, ) -from sqlmesh.core.snapshot.evaluator import SnapshotEvaluator as SnapshotEvaluator +from sqlmesh.core.snapshot.evaluator import ( + SnapshotEvaluator as SnapshotEvaluator, + SnapshotCreationFailedError as SnapshotCreationFailedError, +) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 48d74f440e..1e0a4e440e 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -66,6 +66,7 @@ from sqlmesh.utils.concurrency import ( concurrent_apply_to_snapshots, concurrent_apply_to_values, + NodeExecutionFailedError, ) from sqlmesh.utils.date import TimeLike, now, time_like_to_str from sqlmesh.utils.errors import ( @@ -87,6 +88,13 @@ logger = logging.getLogger(__name__) +class SnapshotCreationFailedError(SQLMeshError): + def __init__(self, errors: t.List[NodeExecutionFailedError[SnapshotId]]): + messages = "\n\n".join(f"{error}\n {error.__cause__}" for error in errors) + super().__init__(f"Physical table creation failed:\n\n{messages}") + self.errors = errors + + class SnapshotEvaluator: """Evaluates a snapshot given runtime arguments through an arbitrary EngineAdapter. @@ -397,7 +405,7 @@ def _create_snapshots( ) -> None: """Internal method to create tables in parallel.""" with self.concurrent_context(): - concurrent_apply_to_snapshots( + errors, _ = concurrent_apply_to_snapshots( snapshots_to_create, lambda s: self._create_snapshot( s, @@ -408,7 +416,10 @@ def _create_snapshots( allow_destructive_snapshots=allow_destructive_snapshots, ), self.ddl_concurrent_tasks, + raise_on_error=False, ) + if errors: + raise SnapshotCreationFailedError(errors) def migrate( self, From 83e7053293510e75198eb033e6fb2d61c3f9d2e6 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Sat, 3 May 2025 00:42:05 +0300 Subject: [PATCH 0094/1056] Fix: Enable unit testing in Python models with upstream tables (#4302) --- sqlmesh/core/test/context.py | 6 ++++- tests/core/test_test.py | 52 +++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/test/context.py b/sqlmesh/core/test/context.py index 30fcb318db..653f62bfe8 100644 --- a/sqlmesh/core/test/context.py +++ b/sqlmesh/core/test/context.py @@ -39,8 +39,12 @@ def __init__( @cached_property def _model_tables(self) -> t.Dict[str, str]: """Returns a mapping of model names to tables.""" + + # Include upstream dependencies to ensure they can be resolved during test execution return { - name: self._test._test_fixture_table(name).sql() for name, model in self._models.items() + name: self._test._test_fixture_table(name).sql() + for model in self._models.values() + for name in [model.name, *model.depends_on] } def with_variables( diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 74acdf76ad..740efdd993 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -21,7 +21,7 @@ GatewayConfig, ModelDefaultsConfig, ) -from sqlmesh.core.context import Context +from sqlmesh.core.context import Context, ExecutionContext from sqlmesh.core.console import get_console from sqlmesh.core.dialect import parse from sqlmesh.core.engine_adapter import EngineAdapter @@ -2470,3 +2470,53 @@ def execute(context, start, end, execution_time, **kwargs): results = ctx.test() assert len(results.successes) == 30 + + +def test_python_model_upstream_table(sushi_context) -> None: + @model( + "test_upstream_table_python", + columns={"customer_id": "int", "zip": "str"}, + ) + def upstream_table_python(context, **kwargs): + demographics_external_table = context.resolve_table("memory.raw.demographics") + return context.fetchdf( + exp.select("customer_id", "zip").from_(demographics_external_table), + ) + + python_model = model.get_registry()["test_upstream_table_python"].model( + module_path=Path("."), + path=Path("."), + ) + + context = ExecutionContext(sushi_context.engine_adapter, sushi_context.snapshots, None, None) + df = list(python_model.render(context=context))[0] + + # Verify the actual model output matches the expected actual external table's values + assert df.to_dict(orient="records") == [{"customer_id": 1, "zip": "00000"}] + + # Use different input values for the test and verify the outputs + _check_successful_or_raise( + _create_test( + body=load_yaml(""" +test_test_upstream_table_python: + model: test_upstream_table_python + inputs: + memory.raw.demographics: + - customer_id: 12 + zip: "S11HA" + - customer_id: 555 + zip: "94401" + outputs: + query: + - customer_id: 12 + zip: "S11HA" + - customer_id: 555 + zip: "94401" +"""), + test_name="test_test_upstream_table_python", + model=model.get_registry()["test_upstream_table_python"].model( + module_path=Path("."), path=Path(".") + ), + context=sushi_context, + ).run() + ) From 971f85430dacadc81404eb6175df9c06dd157168 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Sat, 3 May 2025 14:47:55 -0700 Subject: [PATCH 0095/1056] Fix: Report models for which physical table creation was skipped due to a failure upstream (#4308) --- sqlmesh/core/plan/evaluator.py | 1 + sqlmesh/core/snapshot/evaluator.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 4bada8526b..fdf022dfa0 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -299,6 +299,7 @@ def _should_create(s: Snapshot) -> bool: for error in ex.errors: logger.info(str(error), exc_info=error) + self.console.log_skipped_models({s.name for s in ex.skipped}) self.console.log_failed_models(ex.errors) raise PlanError("Plan application failed.") diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 1e0a4e440e..c7b918be05 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -89,10 +89,13 @@ class SnapshotCreationFailedError(SQLMeshError): - def __init__(self, errors: t.List[NodeExecutionFailedError[SnapshotId]]): + def __init__( + self, errors: t.List[NodeExecutionFailedError[SnapshotId]], skipped: t.List[SnapshotId] + ): messages = "\n\n".join(f"{error}\n {error.__cause__}" for error in errors) super().__init__(f"Physical table creation failed:\n\n{messages}") self.errors = errors + self.skipped = skipped class SnapshotEvaluator: @@ -405,7 +408,7 @@ def _create_snapshots( ) -> None: """Internal method to create tables in parallel.""" with self.concurrent_context(): - errors, _ = concurrent_apply_to_snapshots( + errors, skipped = concurrent_apply_to_snapshots( snapshots_to_create, lambda s: self._create_snapshot( s, @@ -419,7 +422,7 @@ def _create_snapshots( raise_on_error=False, ) if errors: - raise SnapshotCreationFailedError(errors) + raise SnapshotCreationFailedError(errors, skipped) def migrate( self, From 8e3ac170d6d57f5e6b77593ee0af36c70a8f3404 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Mon, 5 May 2025 16:05:21 +1200 Subject: [PATCH 0096/1056] Fix: table_diff - correctly handle nulls in boolean columns when displaying the row diff (#4310) --- sqlmesh/core/console.py | 4 +++ tests/core/test_table_diff.py | 53 ++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index ffe63a80de..98a49a1e6c 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -2205,6 +2205,10 @@ def _cells_match(x: t.Any, y: t.Any) -> bool: # Convert array-like objects to list for consistent comparison def _normalize(val: t.Any) -> t.Any: + # Convert Pandas null to Python null for the purposes of comparison to prevent errors like the following on boolean fields: + # - TypeError: boolean value of NA is ambiguous + if pd.isnull(val): + val = None return list(val) if isinstance(val, (pd.Series, np.ndarray)) else val return _normalize(x) == _normalize(y) diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index 9c2b07138a..f01ad4a6d7 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -9,7 +9,7 @@ from rich.console import Console from sqlmesh.core.console import TerminalConsole from sqlmesh.core.context import Context -from sqlmesh.core.config import AutoCategorizationMode, CategorizerConfig +from sqlmesh.core.config import AutoCategorizationMode, CategorizerConfig, DuckDBConnectionConfig from sqlmesh.core.model import SqlModel, load_sql_based_model from sqlmesh.core.table_diff import TableDiff import numpy as np @@ -511,3 +511,54 @@ def test_data_diff_array_dict(sushi_context_fixed_date): stripped_output = strip_ansi_codes(output) stripped_expected = expected_output.strip() assert stripped_output == stripped_expected + + +def test_data_diff_nullable_booleans(): + engine_adapter = DuckDBConnectionConfig().create_engine_adapter() + + columns_to_types = {"key": exp.DataType.build("int"), "value": exp.DataType.build("boolean")} + + engine_adapter.create_table("table_diff_source", columns_to_types) + engine_adapter.create_table("table_diff_target", columns_to_types) + + engine_adapter.execute( + "insert into table_diff_source (key, value) values (1, true), (2, false), (3, null)" + ) + engine_adapter.execute( + "insert into table_diff_target (key, value) values (1, false), (2, null), (3, true)" + ) + + table_diff = TableDiff( + adapter=engine_adapter, + source="table_diff_source", + target="table_diff_target", + source_alias="dev", + target_alias="prod", + on=["key"], + ) + + diff = table_diff.row_diff() + + output = capture_console_output("show_row_diff", row_diff=diff) + + expected_output = """ +Row Counts: +└── PARTIAL MATCH: 3 rows (100.0%) + +COMMON ROWS column comparison stats: + pct_match +value 0.0 + + +COMMON ROWS sample data differences: +Column: value +┏━━━━━┳━━━━━━━┳━━━━━━━┓ +┃ key ┃ DEV ┃ PROD ┃ +┡━━━━━╇━━━━━━━╇━━━━━━━┩ +│ 1 │ True │ False │ +│ 2 │ False │ │ +│ 3 │ │ True │ +└─────┴───────┴───────┘ +""" + + assert strip_ansi_codes(output) == expected_output.strip() From c6a2bffa102ed953aba81dad7a9318f68d1c4025 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 5 May 2025 12:04:21 +0300 Subject: [PATCH 0097/1056] Chore: update bigquery integration test related to info. schema (#4311) --- .../engine_adapter/integration/test_integration_bigquery.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index 44f8ded023..56ed949bec 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -229,6 +229,7 @@ def _mutate_config(_: str, config: Config) -> None: "table_type": exp.DataType.build("TEXT"), "is_insertable_into": exp.DataType.build("TEXT"), "is_typed": exp.DataType.build("TEXT"), + "managed_table_type": exp.DataType.build("TEXT"), "creation_time": exp.DataType.build("TIMESTAMPTZ"), "base_table_catalog": exp.DataType.build("TEXT"), "base_table_schema": exp.DataType.build("TEXT"), @@ -259,6 +260,7 @@ def _mutate_config(_: str, config: Config) -> None: " `tables`.`table_type` AS `table_type`,\n" " `tables`.`is_insertable_into` AS `is_insertable_into`,\n" " `tables`.`is_typed` AS `is_typed`,\n" + " `tables`.`managed_table_type` AS `managed_table_type`,\n" " `tables`.`creation_time` AS `creation_time`,\n" " `tables`.`base_table_catalog` AS `base_table_catalog`,\n" " `tables`.`base_table_schema` AS `base_table_schema`,\n" From 6e99e934179279e42a8b587db59e2730e051d9e4 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 5 May 2025 13:08:35 +0300 Subject: [PATCH 0098/1056] Chore: update bigquery integration test related to info. schema (#4312) --- .../engine_adapter/integration/test_integration_bigquery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index 56ed949bec..c0ccd6f055 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -260,7 +260,6 @@ def _mutate_config(_: str, config: Config) -> None: " `tables`.`table_type` AS `table_type`,\n" " `tables`.`is_insertable_into` AS `is_insertable_into`,\n" " `tables`.`is_typed` AS `is_typed`,\n" - " `tables`.`managed_table_type` AS `managed_table_type`,\n" " `tables`.`creation_time` AS `creation_time`,\n" " `tables`.`base_table_catalog` AS `base_table_catalog`,\n" " `tables`.`base_table_schema` AS `base_table_schema`,\n" @@ -275,7 +274,8 @@ def _mutate_config(_: str, config: Config) -> None: " `tables`.`replication_status` AS `replication_status`,\n" " `tables`.`replication_error` AS `replication_error`,\n" " `tables`.`is_change_history_enabled` AS `is_change_history_enabled`,\n" - " `tables`.`sync_status` AS `sync_status`\n" + " `tables`.`sync_status` AS `sync_status`,\n" + " `tables`.`managed_table_type` AS `managed_table_type`\n" f"FROM {dependency} AS `tables`" ) From 03a07f1e1a6373f4d8556d02ab32bf8c36055895 Mon Sep 17 00:00:00 2001 From: Tomasz Zorawik <67728999+xardasos@users.noreply.github.com> Date: Mon, 5 May 2025 22:04:18 +0200 Subject: [PATCH 0099/1056] Fix: avoid redundant object traversals when building python envs (#4295) --- sqlmesh/utils/metaprogramming.py | 64 ++++++++++++++--------------- tests/utils/test_metaprogramming.py | 50 +++++++++++++++++++++- 2 files changed, 81 insertions(+), 33 deletions(-) diff --git a/sqlmesh/utils/metaprogramming.py b/sqlmesh/utils/metaprogramming.py index c426ff19eb..099d866960 100644 --- a/sqlmesh/utils/metaprogramming.py +++ b/sqlmesh/utils/metaprogramming.py @@ -338,43 +338,43 @@ def walk(obj: t.Any, name: str, is_metadata: t.Optional[bool] = None) -> None: ): env[name] = (obj, is_metadata) return + + if inspect.isclass(obj): + for var in decorator_vars(obj): + if obj_module and var in obj_module.__dict__: + walk(obj_module.__dict__[var], var, is_metadata) + + for base in obj.__bases__: + walk(base, base.__qualname__, is_metadata) + + for k, v in obj.__dict__.items(): + if k.startswith("__"): + continue + + # Traverse methods in a class to find global references + if isinstance(v, (classmethod, staticmethod)): + v = v.__func__ + + if callable(v): + # Walk the method if it's part of the object, else it's a global function and we just store it + if v.__qualname__.startswith(obj.__qualname__): + for k, v in func_globals(v).items(): + walk(v, k, is_metadata) + else: + walk(v, v.__name__, is_metadata) + elif callable(obj): + for k, v in func_globals(obj).items(): + walk(v, k, is_metadata) + + # We store the object in the environment after its dependencies, because otherwise we + # could crash at environment hydration time, since dicts are ordered and the top-level + # objects would be loaded before their dependencies. + env[name] = (obj, is_metadata) elif not _globals_match(env[name][0], obj): raise SQLMeshError( f"Cannot store {obj} in environment, duplicate definitions found for '{name}'" ) - if inspect.isclass(obj): - for var in decorator_vars(obj): - if obj_module and var in obj_module.__dict__: - walk(obj_module.__dict__[var], var, is_metadata) - - for base in obj.__bases__: - walk(base, base.__qualname__, is_metadata) - - for k, v in obj.__dict__.items(): - if k.startswith("__"): - continue - - # Traverse methods in a class to find global references - if isinstance(v, (classmethod, staticmethod)): - v = v.__func__ - - if callable(v): - # Walk the method if it's part of the object, else it's a global function and we just store it - if v.__qualname__.startswith(obj.__qualname__): - for k, v in func_globals(v).items(): - walk(v, k, is_metadata) - else: - walk(v, v.__name__, is_metadata) - elif callable(obj): - for k, v in func_globals(obj).items(): - walk(v, k, is_metadata) - - # We store the object in the environment after its dependencies, because otherwise we - # could crash at environment hydration time, since dicts are ordered and the top-level - # objects would be loaded before their dependencies. - env[name] = (obj, is_metadata) - # The "metadata only" annotation of the object is transitive walk(obj, name, is_metadata_obj or getattr(obj, c.SQLMESH_METADATA, None)) diff --git a/tests/utils/test_metaprogramming.py b/tests/utils/test_metaprogramming.py index 1e3fe071d2..1bef8b7a91 100644 --- a/tests/utils/test_metaprogramming.py +++ b/tests/utils/test_metaprogramming.py @@ -17,6 +17,7 @@ import tests.utils.test_date as test_date from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core import constants as c +from sqlmesh.core.macros import RuntimeStage from sqlmesh.utils.errors import SQLMeshError from sqlmesh.utils.metaprogramming import ( Executable, @@ -47,7 +48,7 @@ def test_print_exception(mocker: MockerFixture): except Exception as ex: print_exception(ex, test_env, out_mock) - expected_message = r""" File ".*?/tests/utils/test_metaprogramming\.py", line 46, in test_print_exception + expected_message = r""" File ".*?/tests/utils/test_metaprogramming\.py", line 47, in test_print_exception eval\("test_fun\(\)", env\) File "", line 1, in @@ -140,6 +141,18 @@ def closure(z: int) -> int: return closure(y) + other_func(Y) +def macro1() -> str: + print("macro1 hello there") + print(RuntimeStage.CREATING) + return "1" + + +def macro2() -> str: + print("macro2 hello there") + print(RuntimeStage.LOADING) + return "2" + + def test_func_globals() -> None: assert func_globals(main_func) == { "Y": 2, @@ -405,3 +418,38 @@ def function_with_custom_decorator(): # Every object is treated as "metadata only", transitively assert all(is_metadata for (_, is_metadata) in env.values()) assert serialized_env == expected_env + + +def test_serialize_env_with_enum_import_appearing_in_two_functions() -> None: + path = Path("tests/utils") + env: t.Dict[str, t.Tuple[t.Any, t.Optional[bool]]] = {} + + build_env(macro1, env=env, name="macro1", path=path) + build_env(macro2, env=env, name="macro2", path=path) + + serialized_env = serialize_env(env, path=path) # type: ignore + assert prepare_env(serialized_env) + + expected_env = { + "RuntimeStage": Executable( + payload="from sqlmesh.core.macros import RuntimeStage", kind=ExecutableKind.IMPORT + ), + "macro1": Executable( + payload="""def macro1(): + print('macro1 hello there') + print(RuntimeStage.CREATING) + return '1'""", + name="macro1", + path="test_metaprogramming.py", + ), + "macro2": Executable( + payload="""def macro2(): + print('macro2 hello there') + print(RuntimeStage.LOADING) + return '2'""", + name="macro2", + path="test_metaprogramming.py", + ), + } + + assert serialized_env == expected_env From 7e09d182e44bec268d1f864b19fecfe24d7ae2d3 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 6 May 2025 11:03:23 +0100 Subject: [PATCH 0100/1056] fix(vscode): support aliased references (#4309) --- sqlmesh/lsp/reference.py | 21 ++++++++++++++------- tests/lsp/test_reference.py | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 06b88871be..1a957d6d9d 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -55,13 +55,20 @@ def get_model_definitions_for_a_path( depends_on = model.depends_on # Normalize the table reference - reference_name = table.sql(dialect=model.dialect) - normalized_reference_name = normalize_model_name( - reference_name, - default_catalog=lint_context.context.default_catalog, - dialect=model.dialect, - ) - if normalized_reference_name not in depends_on: + unaliased = table.copy() + if unaliased.args.get("alias") is not None: + unaliased.set("alias", None) + reference_name = unaliased.sql(dialect=model.dialect) + try: + normalized_reference_name = normalize_model_name( + reference_name, + default_catalog=lint_context.context.default_catalog, + dialect=model.dialect, + ) + if normalized_reference_name not in depends_on: + continue + except Exception: + # Skip references that cannot be normalized continue # Get the referenced model uri diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index 4ef25107ad..cf700420ef 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -46,3 +46,20 @@ def test_reference() -> None: referenced_text += read_file[line_num] referenced_text += read_file[end_line][:end_character] # Last line up to end_character assert referenced_text == "sushi.customers" + + +@pytest.mark.fast +def test_reference_with_alias() -> None: + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + waiter_revenue_by_day_uri = next( + uri for uri, models in lsp_context.map.items() if "sushi.waiter_revenue_by_day" in models + ) + + references = get_model_definitions_for_a_path(lsp_context, waiter_revenue_by_day_uri) + assert len(references) == 3 + + assert references[0].uri.endswith("orders.py") + assert references[1].uri.endswith("order_items.py") + assert references[2].uri.endswith("items.py") From d0a1d4d93cf2330749c3c25e7a89e144fa4394f4 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 6 May 2025 13:49:12 +0300 Subject: [PATCH 0101/1056] Feat: Make this_env and views available in macros of before after all (#4298) --- docs/concepts/macros/macro_variables.md | 3 +- docs/guides/configuration.md | 27 ++++++---------- sqlmesh/core/macros.py | 26 +++++++++++++++ sqlmesh/core/renderer.py | 22 ++++++++----- tests/core/test_context.py | 43 +++++++++++++++++++------ 5 files changed, 84 insertions(+), 37 deletions(-) diff --git a/docs/concepts/macros/macro_variables.md b/docs/concepts/macros/macro_variables.md index bea6036e0e..2b1b591f3b 100644 --- a/docs/concepts/macros/macro_variables.md +++ b/docs/concepts/macros/macro_variables.md @@ -140,4 +140,5 @@ SQLMesh provides additional predefined variables used to modify model behavior b The following variables are also available in [`before_all` and `after_all` statements](../../guides/configuration.md#before_all-and-after_all-statements), as well as in macros invoked within them. * @this_env - A string value containing the name of the current [environment](../environments.md). -* @schemas - A list of the schema names of the [virtual layer](../../concepts/glossary.md#virtual-layer) of the current environment. \ No newline at end of file +* @schemas - A list of the schema names of the [virtual layer](../../concepts/glossary.md#virtual-layer) of the current environment. +* @views - A list of the view names of the [virtual layer](../../concepts/glossary.md#virtual-layer) of the current environment. \ No newline at end of file diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index bf8db05a1e..561996594c 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -675,7 +675,7 @@ Configuration for a connection used to run unit tests. An in-memory DuckDB datab ### Scheduler -Identifies which scheduler backend to use. The scheduler backend is used both for storing metadata and for executing [plans](../concepts/plans.md). By default, the scheduler type is set to `builtin`, which uses the existing SQL engine to store metadata. +Identifies which scheduler backend to use. The scheduler backend is used both for storing metadata and for executing [plans](../concepts/plans.md). By default, the scheduler type is set to `builtin`, which uses the existing SQL engine to store metadata. These options are in the [scheduler](../reference/configuration.md#scheduler) section of the configuration reference page. @@ -1019,20 +1019,18 @@ For example, rather than using an `on_virtual_update` statement in each model to ```python linenums="1" from sqlmesh.core.macros import macro -from sqlmesh.core.snapshot.definition import to_view_mapping @macro() def grant_select_privileges(evaluator): - if evaluator._environment_naming_info: - mapping = to_view_mapping( - evaluator._snapshots.values(), evaluator._environment_naming_info - ) + if evaluator.views: return [ - f"GRANT SELECT ON VIEW {view_name} TO ROLE admin_role;" - for view_name in mapping.values() + f"GRANT SELECT ON VIEW {view_name} /* sqlglot.meta replace=false */ TO ROLE admin_role;" + for view_name in evaluator.views ] ``` +By including the comment `/* sqlglot.meta replace=false */`, you further ensure that the evaluator does not replace the view name with the physical table name during rendering. + ##### Example: Granting Schema Privileges Similarly, you can define a macro to grant schema usage privileges and, as demonstrated in the configuration above, using `this_env` macro conditionally execute it only in the production environment. @@ -1042,21 +1040,14 @@ from sqlmesh import macro @macro() def grant_schema_usage(evaluator): - if evaluator._environment_naming_info: - schemas = { - snapshot.qualified_view_name.schema_for_environment( - evaluator._environment_naming_info - ) - for snapshot in evaluator._snapshots.values() - if snapshot.is_model - } + if evaluator.this_env == "prod" and evaluator.schemas: return [ f"GRANT USAGE ON SCHEMA {schema} TO admin_role;" - for schema in schemas + for schema in evaluator.schemas ] ``` -As demonstrated in these examples, the `environment_naming_info` is available within the macro evaluator for macros invoked within the `before_all` and `after_all` statements. Additionally, the macro `this_env` provides access to the current environment name, which can be helpful for more advanced use cases that require fine-grained control over their behaviour. +As demonstrated in these examples, the `schemas` and `views` are available within the macro evaluator for macros invoked within the `before_all` and `after_all` statements. Additionally, the macro `this_env` provides access to the current environment name, which can be helpful for more advanced use cases that require fine-grained control over their behaviour. ### Linting diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index b9bf3f54f3..4aa75d1709 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -491,6 +491,32 @@ def gateway(self) -> t.Optional[str]: """Returns the gateway name.""" return self.var(c.GATEWAY) + @property + def snapshots(self) -> t.Dict[str, Snapshot]: + """Returns the snapshots if available.""" + return self._snapshots + + @property + def this_env(self) -> str: + """Returns the name of the current environment in before after all.""" + if "this_env" not in self.locals: + raise SQLMeshError("Environment name is only available in before_all and after_all") + return self.locals["this_env"] + + @property + def schemas(self) -> t.List[str]: + """Returns the schemas of the current environment in before after all macros.""" + if "schemas" not in self.locals: + raise SQLMeshError("Schemas are only available in before_all and after_all") + return self.locals["schemas"] + + @property + def views(self) -> t.List[str]: + """Returns the views of the current environment in before after all macros.""" + if "views" not in self.locals: + raise SQLMeshError("Views are only available in before_all and after_all") + return self.locals["views"] + def var(self, var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]: """Returns the value of the specified variable, or the default value if it doesn't exist.""" return (self.locals.get(c.SQLMESH_VARS) or {}).get(var_name.lower(), default) diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index cc389dd87c..fa07c20931 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -110,17 +110,23 @@ def _render( if environment_naming_info is not None: kwargs["this_env"] = getattr(environment_naming_info, "name") if snapshots: - schemas = set( - [ - s.qualified_view_name.schema_for_environment( - environment_naming_info, dialect=self._dialect + schemas, views = set(), [] + for snapshot in snapshots.values(): + if snapshot.is_model and not snapshot.is_symbolic: + schemas.add( + snapshot.qualified_view_name.schema_for_environment( + environment_naming_info, dialect=self._dialect + ) + ) + views.append( + snapshot.display_name( + environment_naming_info, self._default_catalog, self._dialect + ) ) - for s in snapshots.values() - if s.is_model and not s.is_symbolic - ] - ) if schemas: kwargs["schemas"] = list(schemas) + if views: + kwargs["views"] = views this_model = kwargs.pop("this_model", None) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 3fdef03f17..f502a2c7ff 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1417,6 +1417,7 @@ def test_environment_statements(tmp_path: pathlib.Path): after_all=[ "@grant_schema_usage()", "@grant_usage_role(@schemas, 'admin')", + "@grant_select_privileges()", ], ) @@ -1435,6 +1436,21 @@ def test_environment_statements(tmp_path: pathlib.Path): expression, ) + create_temp_file( + tmp_path, + pathlib.Path(macros_dir, "grant_select_privileges.py"), + """ +from sqlmesh.core.macros import macro +@macro() +def grant_select_privileges(evaluator): + if evaluator.this_env and evaluator.views: + return [ + f"GRANT SELECT ON VIEW {view_name} /* sqlglot.meta replace=false */ TO ROLE admin_role;" + for view_name in evaluator.views + ] +""", + ) + create_temp_file( tmp_path, pathlib.Path(macros_dir, "grant_schema_file.py"), @@ -1484,6 +1500,7 @@ def grant_usage_role(evaluator, schemas, role): assert isinstance(python_env["grant_schema_usage"], Executable) assert isinstance(python_env["grant_usage_role"], Executable) + assert isinstance(python_env["grant_select_privileges"], Executable) before_all_rendered = render_statements( statements=before_all, @@ -1502,13 +1519,16 @@ def grant_usage_role(evaluator, schemas, role): python_env=python_env, snapshots=snapshots, environment_naming_info=EnvironmentNamingInfo(name="prod"), - runtime_stage=RuntimeStage.BEFORE_ALL, + runtime_stage=RuntimeStage.AFTER_ALL, ) - assert after_all_rendered == [ - "GRANT USAGE ON SCHEMA db TO user_role", - 'GRANT USAGE ON SCHEMA "db" TO "admin"', - ] + assert sorted(after_all_rendered) == sorted( + [ + "GRANT USAGE ON SCHEMA db TO user_role", + 'GRANT USAGE ON SCHEMA "db" TO "admin"', + "GRANT SELECT ON VIEW memory.db.test_after_model /* sqlglot.meta replace=false */ TO ROLE admin_role", + ] + ) after_all_rendered_dev = render_statements( statements=after_all, @@ -1516,13 +1536,16 @@ def grant_usage_role(evaluator, schemas, role): python_env=python_env, snapshots=snapshots, environment_naming_info=EnvironmentNamingInfo(name="dev"), - runtime_stage=RuntimeStage.BEFORE_ALL, + runtime_stage=RuntimeStage.AFTER_ALL, ) - assert after_all_rendered_dev == [ - "GRANT USAGE ON SCHEMA db__dev TO user_role", - 'GRANT USAGE ON SCHEMA "db__dev" TO "admin"', - ] + assert sorted(after_all_rendered_dev) == sorted( + [ + "GRANT USAGE ON SCHEMA db__dev TO user_role", + 'GRANT USAGE ON SCHEMA "db__dev" TO "admin"', + "GRANT SELECT ON VIEW memory.db__dev.test_after_model /* sqlglot.meta replace=false */ TO ROLE admin_role", + ] + ) def test_plan_environment_statements(tmp_path: pathlib.Path): From f8005292a446e92f2bd2874dfbe7804858d4387d Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 6 May 2025 16:22:26 +0100 Subject: [PATCH 0102/1056] feat(vscode): add completion provider (#4300) --- sqlmesh/lsp/completions.py | 77 +++++++++++++++++++ sqlmesh/lsp/custom.py | 22 ++++++ sqlmesh/lsp/main.py | 10 +++ tests/lsp/test_completions.py | 44 +++++++++++ vscode/extension/src/completion/completion.ts | 36 +++++++++ vscode/extension/src/extension.ts | 9 +++ vscode/extension/src/lsp/custom.ts | 19 +++++ vscode/extension/src/lsp/lsp.ts | 20 +++++ 8 files changed, 237 insertions(+) create mode 100644 sqlmesh/lsp/completions.py create mode 100644 sqlmesh/lsp/custom.py create mode 100644 tests/lsp/test_completions.py create mode 100644 vscode/extension/src/completion/completion.ts create mode 100644 vscode/extension/src/lsp/custom.ts diff --git a/sqlmesh/lsp/completions.py b/sqlmesh/lsp/completions.py new file mode 100644 index 0000000000..6c0c172815 --- /dev/null +++ b/sqlmesh/lsp/completions.py @@ -0,0 +1,77 @@ +from functools import lru_cache +from sqlglot import Dialect, Tokenizer +from sqlmesh.lsp.custom import AllModelsResponse +import typing as t +from sqlmesh.lsp.context import LSPContext + + +def get_sql_completions(context: t.Optional[LSPContext], file_uri: str) -> AllModelsResponse: + """ + Return a list of completions for a given file. + """ + return AllModelsResponse( + models=list(get_models(context, file_uri)), + keywords=list(get_keywords(context, file_uri)), + ) + + +def get_models(context: t.Optional[LSPContext], file_uri: t.Optional[str]) -> t.Set[str]: + """ + Return a list of models for a given file. + + If there is no context, return an empty list. + If there is a context, return a list of all models bar the ones the file itself defines. + """ + if context is None: + return set() + all_models = set(model for models in context.map.values() for model in models) + if file_uri is not None: + models_file_refers_to = context.map[file_uri] + for model in models_file_refers_to: + all_models.discard(model) + return all_models + + +def get_keywords(context: t.Optional[LSPContext], file_uri: t.Optional[str]) -> t.Set[str]: + """ + Return a list of sql keywords for a given file. + If no context is provided, return ANSI SQL keywords. + + If a context is provided but no file_uri is provided, returns the keywords + for the default dialect of the context. + + If both a context and a file_uri are provided, returns the keywords + for the dialect of the model that the file belongs to. + """ + if file_uri is not None and context is not None: + models = context.map[file_uri] + if models: + model = models[0] + model_from_context = context.context.get_model(model) + if model_from_context is not None: + if model_from_context.dialect: + return get_keywords_from_tokenizer(model_from_context.dialect) + if context is not None: + return get_keywords_from_tokenizer(context.context.default_dialect) + return get_keywords_from_tokenizer(None) + + +@lru_cache() +def get_keywords_from_tokenizer(dialect: t.Optional[str] = None) -> t.Set[str]: + """ + Return a list of sql keywords for a given dialect. This is separate from + the direct use of Tokenizer.KEYWORDS.keys() because that returns a set of + keywords that are expanded, e.g. "ORDER BY" -> ["ORDER", "BY"]. + """ + tokenizer = Tokenizer + if dialect is not None: + try: + tokenizer = Dialect.get_or_raise(dialect).tokenizer_class + except Exception: + pass + + expanded_keywords = set() + for keyword in tokenizer.KEYWORDS.keys(): + parts = keyword.split(" ") + expanded_keywords.update(parts) + return expanded_keywords diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py new file mode 100644 index 0000000000..b8133f3b55 --- /dev/null +++ b/sqlmesh/lsp/custom.py @@ -0,0 +1,22 @@ +from lsprotocol import types +import typing as t +from sqlmesh.utils.pydantic import PydanticModel + +ALL_MODELS_FEATURE = "sqlmesh/all_models" + + +class AllModelsRequest(PydanticModel): + """ + Request to get all the models that are in the current project. + """ + + textDocument: types.TextDocumentIdentifier + + +class AllModelsResponse(PydanticModel): + """ + Response to get all the models that are in the current project. + """ + + models: t.List[str] + keywords: t.List[str] diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index ef64e1875c..aeaceecab6 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -11,7 +11,9 @@ from sqlmesh._version import __version__ from sqlmesh.core.context import Context from sqlmesh.core.linter.definition import AnnotatedRuleViolation +from sqlmesh.lsp.completions import get_sql_completions from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.custom import ALL_MODELS_FEATURE, AllModelsRequest, AllModelsResponse from sqlmesh.lsp.reference import get_model_definitions_for_a_path @@ -38,6 +40,14 @@ def __init__( def _register_features(self) -> None: """Register LSP features on the internal LanguageServer instance.""" + @self.server.feature(ALL_MODELS_FEATURE) + def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse: + try: + context = self._context_get_or_load(params.textDocument.uri) + return get_sql_completions(context, params.textDocument.uri) + except Exception as e: + return get_sql_completions(None, params.textDocument.uri) + @self.server.feature(types.TEXT_DOCUMENT_DID_OPEN) def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> None: context = self._context_get_or_load(params.text_document.uri) diff --git a/tests/lsp/test_completions.py b/tests/lsp/test_completions.py new file mode 100644 index 0000000000..0a30c17505 --- /dev/null +++ b/tests/lsp/test_completions.py @@ -0,0 +1,44 @@ +import pytest +from sqlglot import Tokenizer +from sqlmesh.core.context import Context +from sqlmesh.lsp.completions import get_keywords_from_tokenizer, get_sql_completions +from sqlmesh.lsp.context import LSPContext + + +TOKENIZER_KEYWORDS = set(Tokenizer.KEYWORDS.keys()) + + +@pytest.mark.fast +def test_get_keywords_from_tokenizer(): + assert len(get_keywords_from_tokenizer()) > len(TOKENIZER_KEYWORDS) + + +@pytest.mark.fast +def test_get_sql_completions_no_context(): + completions = get_sql_completions(None, None) + assert len(completions.keywords) > len(TOKENIZER_KEYWORDS) + assert len(completions.models) == 0 + + +@pytest.mark.fast +def test_get_sql_completions_with_context_no_file_uri(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + completions = get_sql_completions(lsp_context, None) + assert len(completions.keywords) > len(TOKENIZER_KEYWORDS) + assert "sushi.active_customers" in completions.models + assert "sushi.customers" in completions.models + + +@pytest.mark.fast +def test_get_sql_completions_with_context_and_file_uri(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + file_uri = next( + key for key in lsp_context.map.keys() if key.endswith("models/active_customers.sql") + ) + completions = get_sql_completions(lsp_context, file_uri) + assert len(completions.keywords) > len(TOKENIZER_KEYWORDS) + assert "sushi.active_customers" not in completions.models diff --git a/vscode/extension/src/completion/completion.ts b/vscode/extension/src/completion/completion.ts new file mode 100644 index 0000000000..651b61854a --- /dev/null +++ b/vscode/extension/src/completion/completion.ts @@ -0,0 +1,36 @@ +import * as vscode from 'vscode' +import { LSPClient } from '../lsp/lsp' +import { isErr } from '../utilities/functional/result' + +export const selector: vscode.DocumentSelector = { + pattern: '**/*.sql', +} + +export const completionProvider = ( + lsp: LSPClient, +): vscode.CompletionItemProvider => { + return { + async provideCompletionItems(document) { + const result = await lsp.call_custom_method('sqlmesh/all_models', { + textDocument: { + uri: document.uri.fsPath, + }, + }) + if (isErr(result)) { + return [] + } + const modelCompletions = result.value.models.map( + model => + new vscode.CompletionItem(model, vscode.CompletionItemKind.Reference), + ) + const keywordCompletions = result.value.keywords.map( + keyword => + new vscode.CompletionItem(keyword, vscode.CompletionItemKind.Keyword), + ) + return new vscode.CompletionList([ + ...modelCompletions, + ...keywordCompletions, + ]) + }, + } +} diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index cf41d2c031..e322fb161d 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -18,6 +18,8 @@ import { handleSqlmeshLspNotFoundError, handleSqlmeshLspDependenciesMissingError, } from './utilities/errors' +import { completionProvider } from './completion/completion' +import { selector } from './completion/completion' let lspClient: LSPClient | undefined @@ -82,6 +84,13 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(lspClient) } + context.subscriptions.push( + vscode.languages.registerCompletionItemProvider( + selector, + completionProvider(lspClient), + ), + ) + const restart = async () => { if (lspClient) { traceVerbose('Restarting LSP client') diff --git a/vscode/extension/src/lsp/custom.ts b/vscode/extension/src/lsp/custom.ts new file mode 100644 index 0000000000..6cb77ae5d8 --- /dev/null +++ b/vscode/extension/src/lsp/custom.ts @@ -0,0 +1,19 @@ +export interface AllModelsMethod { + method: 'sqlmesh/all_models' + request: AllModelsRequest + response: AllModelsResponse +} + +// @eslint-disable-next-line @typescript-eslint/consistent-type-definition +export type CustomLSPMethods = AllModelsMethod + +interface AllModelsRequest { + textDocument: { + uri: string + } +} + +interface AllModelsResponse { + models: string[] + keywords: string[] +} diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 63b932a0d5..c2bbc06504 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -10,6 +10,7 @@ import { err, isErr, ok, Result } from '../utilities/functional/result' import { getWorkspaceFolders } from '../utilities/common/vscodeapi' import { traceError } from '../utilities/common/log' import { ErrorType } from '../utilities/errors' +import { CustomLSPMethods } from './custom' let outputChannel: OutputChannel | undefined @@ -98,4 +99,23 @@ export class LSPClient implements Disposable { public async dispose() { await this.stop() } + + public async call_custom_method< + Method extends CustomLSPMethods['method'], + Request extends Extract['request'], + Response extends Extract['response'], + >(method: Method, request: Request): Promise> { + if (!this.client) { + return err('lsp client not ready') + } + try { + const result = await this.client.sendRequest(method, request) + return ok(result) + } catch (error) { + traceError( + `lsp '${method}' request ${JSON.stringify(request)} failed: ${JSON.stringify(error)}`, + ) + return err(JSON.stringify(error)) + } + } } From 99cca24a74f972be0769f41ffa544a5b5f02e3cb Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 6 May 2025 19:27:07 +0300 Subject: [PATCH 0103/1056] Feat!: Add support for concurrent table diff of multiple models (#4256) --- docs/guides/model_selection.md | 77 +++- docs/guides/tablediff.md | 49 +++ docs/reference/cli.md | 3 +- docs/reference/notebook.md | 4 +- sqlmesh/cli/main.py | 12 +- sqlmesh/core/console.py | 463 ++++++++++++++++------ sqlmesh/core/context.py | 226 ++++++++--- sqlmesh/core/model/meta.py | 14 + sqlmesh/magics.py | 9 +- sqlmesh/utils/git.py | 18 +- tests/core/test_table_diff.py | 151 ++++++- tests/integrations/jupyter/test_magics.py | 22 +- tests/web/test_main.py | 3 +- web/server/api/endpoints/table_diff.py | 11 +- 14 files changed, 852 insertions(+), 210 deletions(-) diff --git a/docs/guides/model_selection.md b/docs/guides/model_selection.md index 1d01c280da..db098a1538 100644 --- a/docs/guides/model_selection.md +++ b/docs/guides/model_selection.md @@ -2,7 +2,7 @@ This guide describes how to select specific models to include in a SQLMesh plan, which can be useful when modifying a subset of the models in a SQLMesh project. -Note: the selector syntax described below is also used for the SQLMesh `plan` [`--allow-destructive-model` selector](../concepts/plans.md#destructive-changes). +Note: the selector syntax described below is also used for the SQLMesh `plan` [`--allow-destructive-model` selector](../concepts/plans.md#destructive-changes) and for the `table_diff` command to [diff a selection of models](./tablediff.md#diffing-multiple-models-across-environments). ## Background @@ -221,6 +221,81 @@ Models: └── sushi.customer_revenue_lifetime ``` +#### Select with tags + +If we specify the `--select-model` option with a tag selector like `"tag:reporting"`, all models with the "reporting" tag will be selected. Tags are case-insensitive and support wildcards: + +```bash +❯ sqlmesh plan dev --select-model "tag:reporting*" +New environment `dev` will be created from `prod` + +Differences from the `prod` environment: + +Models: +├── Directly Modified: +│ ├── sushi.daily_revenue +│ └── sushi.monthly_revenue +└── Indirectly Modified: + └── sushi.revenue_dashboard +``` + +#### Select with git changes + +The git-based selector allows you to select models whose files have changed compared to a target branch (default: main). This includes: +- Untracked files (new files not in git) +- Uncommitted changes in working directory +- Committed changes different from the target branch + +For example: + +```bash +❯ sqlmesh plan dev --select-model "git:feature" +New environment `dev` will be created from `prod` + +Differences from the `prod` environment: + +Models: +├── Directly Modified: +│ └── sushi.items # Changed in feature branch +└── Indirectly Modified: + ├── sushi.order_items + └── sushi.daily_revenue +``` + +You can also combine git selection with upstream/downstream indicators: + +```bash +❯ sqlmesh plan dev --select-model "git:feature+" +# Selects changed models and their downstream dependencies + +❯ sqlmesh plan dev --select-model "+git:feature" +# Selects changed models and their upstream dependencies +``` + +#### Complex selections with logical operators + +The model selector supports combining multiple conditions using logical operators: + +- `&` (AND): Both conditions must be true +- `|` (OR): Either condition must be true +- `^` (NOT): Negates a condition + +For example: + +```bash +❯ sqlmesh plan dev --select-model "(tag:finance & ^tag:deprecated)" +# Selects models with finance tag that don't have deprecated tag + +❯ sqlmesh plan dev --select-model "(+model_a | model_b+)" +# Selects model_a and its upstream deps OR model_b and its downstream deps + +❯ sqlmesh plan dev --select-model "(tag:finance & git:main)" +# Selects changed models that also have the finance tag + +❯ sqlmesh plan dev --select-model "^(tag:test) & metrics.*" +# Selects models in metrics schema that don't have the test tag +``` + ### Backfill examples #### No backfill selection diff --git a/docs/guides/tablediff.md b/docs/guides/tablediff.md index 193c253a35..5c048462c2 100644 --- a/docs/guides/tablediff.md +++ b/docs/guides/tablediff.md @@ -122,6 +122,55 @@ Under the hood, SQLMesh stores temporary data in the database to perform the com The default schema for these temporary tables is `sqlmesh_temp` but can be changed with the `--temp-schema` option. The schema can be specified as a `CATALOG.SCHEMA` or `SCHEMA`. + +## Diffing multiple models across environments + +SQLMesh allows you to compare multiple models across environments at once using model selection expressions. This is useful when you want to validate changes across a set of related models or the entire project. + +To diff multiple models, use the `--select-model` (or `-m` for short) option with the table diff command: + +```bash +sqlmesh table_diff prod:dev --select-model "sqlmesh_example.*" +``` + +When diffing multiple models, SQLMesh will: + +1. Show the models returned by the selector that exist in both environments and have differences +2. Compare these models and display the data diff of each model + +> Note: Models will only be data diffed if there's a breaking change that impacts them. + +The `--select-model` option supports a powerful selection syntax that lets you choose models using patterns, tags, dependencies and git status. For complete details, see the [model selection guide](./model_selection.md). + +> Note: Surround your selection pattern in single or double quotes. Ex: `'*'`, `"sqlmesh_example.*"` + +Here are some common examples: + +```bash +# Select all models in a schema +sqlmesh table_diff prod:dev -m "sqlmesh_example.*" + +# Select a model and its dependencies +sqlmesh table_diff prod:dev -m "+model_name" # include upstream deps +sqlmesh table_diff prod:dev -m "model_name+" # include downstream deps + +# Select models by tag +sqlmesh table_diff prod:dev -m "tag:finance" + +# Select models with git changes +sqlmesh table_diff prod:dev -m "git:feature" + +# Use logical operators for complex selections +sqlmesh table_diff prod:dev -m "(metrics.* & ^tag:deprecated)" # models in the metrics schema that aren't deprecated + +# Combine multiple selectors +sqlmesh table_diff prod:dev -m "tag:finance" -m "metrics.*_daily" +``` + +When multiple selectors are provided, they are combined with OR logic, meaning a model matching any of the selectors will be included. + +> Note: All models being compared must have their `grain` defined that is unique and not null, as this is used to perform the join between the tables in the two environments. + ## Diffing tables or views Compare specific tables or views with the SQLMesh CLI interface by using the command `sqlmesh table_diff [source table]:[target table]`. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index cd2c852b1b..d7b578116c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -529,7 +529,7 @@ Options: ``` Usage: sqlmesh table_diff [OPTIONS] SOURCE:TARGET [MODEL] - Show the diff between two tables. + Show the diff between two tables or multiple models across two environments. Options: -o, --on TEXT The column to join on. Can be specified multiple @@ -548,6 +548,7 @@ Options: --temp-schema TEXT Schema used for temporary tables. It can be `CATALOG.SCHEMA` or `SCHEMA`. Default: `sqlmesh_temp` + -m, --select-model TEXT Select specific models to table diff. --help Show this message and exit. ``` diff --git a/docs/reference/notebook.md b/docs/reference/notebook.md index 60a7e16e7a..313b7295ee 100644 --- a/docs/reference/notebook.md +++ b/docs/reference/notebook.md @@ -293,7 +293,7 @@ Create a schema file containing external model schemas. %table_diff [--on [ON ...]] [--skip-columns [SKIP_COLUMNS ...]] [--model MODEL] [--where WHERE] [--limit LIMIT] [--show-sample] [--decimals DECIMALS] [--skip-grain-check] - [--temp-schema SCHEMA] + [--temp-schema SCHEMA] [--select-model [SELECT_MODEL ...]] SOURCE:TARGET Show the diff between two tables. @@ -320,6 +320,8 @@ options: --skip-grain-check Disable the check for a primary key (grain) that is missing or is not unique. --temp-schema SCHEMA The schema to use for temporary tables. + --select-model <[SELECT_MODEL ...]> + Select specific models to diff using a pattern. ``` #### model diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index cfec00c9b1..a06f9719a2 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -892,18 +892,26 @@ def create_external_models(obj: Context, **kwargs: t.Any) -> None: type=str, help="Schema used for temporary tables. It can be `CATALOG.SCHEMA` or `SCHEMA`. Default: `sqlmesh_temp`", ) +@click.option( + "--select-model", + "-m", + type=str, + multiple=True, + help="Specify one or more models to data diff. Use wildcards to diff multiple models. Ex: '*' (all models with applied plan diffs), 'demo.model+' (this and downstream models), 'git:feature_branch' (models with direct modifications in this branch only)", +) @click.pass_obj @error_handler @cli_analytics def table_diff( obj: Context, source_to_target: str, model: t.Optional[str], **kwargs: t.Any ) -> None: - """Show the diff between two tables.""" + """Show the diff between two tables or a selection of models when they are specified.""" source, target = source_to_target.split(":") + select_models = {model} if model else kwargs.pop("select_model", None) obj.table_diff( source=source, target=target, - model_or_snapshot=model, + select_models=select_models, **kwargs, ) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 98a49a1e6c..027760d10f 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -218,6 +218,57 @@ def show_model_difference_summary( """Displays a summary of differences for the given models.""" +class TableDiffConsole(abc.ABC): + """Console for displaying table differences""" + + @abc.abstractmethod + def show_table_diff( + self, + table_diffs: t.List[TableDiff], + show_sample: bool = True, + skip_grain_check: bool = False, + temp_schema: t.Optional[str] = None, + ) -> None: + """Display the table diff between two or multiple tables.""" + + @abc.abstractmethod + def update_table_diff_progress(self, model: str) -> None: + """Update table diff progress bar""" + + @abc.abstractmethod + def start_table_diff_progress(self, models_to_diff: int) -> None: + """Start table diff progress bar""" + + @abc.abstractmethod + def start_table_diff_model_progress(self, model: str) -> None: + """Start table diff model progress""" + + @abc.abstractmethod + def stop_table_diff_progress(self, success: bool) -> None: + """Stop table diff progress bar""" + + @abc.abstractmethod + def show_table_diff_details( + self, + models_to_diff: t.List[str], + ) -> None: + """Display information about which tables are going to be diffed""" + + @abc.abstractmethod + def show_table_diff_summary(self, table_diff: TableDiff) -> None: + """Display information about the tables being diffed and how they are being joined""" + + @abc.abstractmethod + def show_schema_diff(self, schema_diff: SchemaDiff) -> None: + """Show table schema diff.""" + + @abc.abstractmethod + def show_row_diff( + self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False + ) -> None: + """Show table summary diff.""" + + class BaseConsole(abc.ABC): @abc.abstractmethod def log_error(self, message: str) -> None: @@ -258,6 +309,7 @@ class Console( JanitorConsole, EnvironmentsConsole, DifferenceConsole, + TableDiffConsole, BaseConsole, abc.ABC, ): @@ -424,20 +476,6 @@ def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID: def loading_stop(self, id: uuid.UUID) -> None: """Stop loading for the given id.""" - @abc.abstractmethod - def show_table_diff_summary(self, table_diff: TableDiff) -> None: - """Display information about the tables being diffed and how they are being joined""" - - @abc.abstractmethod - def show_schema_diff(self, schema_diff: SchemaDiff) -> None: - """Show table schema diff.""" - - @abc.abstractmethod - def show_row_diff( - self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False - ) -> None: - """Show table summary diff.""" - class NoopConsole(Console): def start_plan_evaluation(self, plan: EvaluatablePlan) -> None: @@ -648,6 +686,40 @@ def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID: def loading_stop(self, id: uuid.UUID) -> None: pass + def show_table_diff( + self, + table_diffs: t.List[TableDiff], + show_sample: bool = True, + skip_grain_check: bool = False, + temp_schema: t.Optional[str] = None, + ) -> None: + for table_diff in table_diffs: + self.show_table_diff_summary(table_diff) + self.show_schema_diff(table_diff.schema_diff()) + self.show_row_diff( + table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check), + show_sample=show_sample, + skip_grain_check=skip_grain_check, + ) + + def update_table_diff_progress(self, model: str) -> None: + pass + + def start_table_diff_progress(self, models_to_diff: int) -> None: + pass + + def start_table_diff_model_progress(self, model: str) -> None: + pass + + def stop_table_diff_progress(self, success: bool) -> None: + pass + + def show_table_diff_details( + self, + models_to_diff: t.List[str], + ) -> None: + pass + def show_table_diff_summary(self, table_diff: TableDiff) -> None: pass @@ -697,6 +769,7 @@ class TerminalConsole(Console): """A rich based implementation of the console.""" TABLE_DIFF_SOURCE_BLUE = "#0248ff" + TABLE_DIFF_TARGET_GREEN = "green" def __init__( self, @@ -746,6 +819,11 @@ def __init__( self.state_import_snapshot_task: t.Optional[TaskID] = None self.state_import_environment_task: t.Optional[TaskID] = None + self.table_diff_progress: t.Optional[Progress] = None + self.table_diff_model_progress: t.Optional[Progress] = None + self.table_diff_model_tasks: t.Dict[str, TaskID] = {} + self.table_diff_progress_live: t.Optional[Live] = None + self.verbosity = verbosity self.dialect = dialect self.ignore_warnings = ignore_warnings @@ -1544,39 +1622,58 @@ def _show_summary_tree_for( ) tree.add(self._limit_model_names(removed_tree, self.verbosity)) if modified_snapshot_ids: - direct = Tree("[bold][direct]Directly Modified:") - indirect = Tree("[bold][indirect]Indirectly Modified:") - metadata = Tree("[bold][metadata]Metadata Updated:") - for s_id in modified_snapshot_ids: - name = s_id.name - display_name = context_diff.snapshots[s_id].display_name( - environment_naming_info, - default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, - dialect=self.dialect, - ) - if context_diff.directly_modified(name): - direct.add( - f"[direct]{display_name}" - if no_diff - else Syntax(f"{display_name}\n{context_diff.text_diff(name)}", "sql") - ) - elif context_diff.indirectly_modified(name): - indirect.add(f"[indirect]{display_name}") - elif context_diff.metadata_updated(name): - metadata.add( - f"[metadata]{display_name}" - if no_diff - else Syntax(f"{display_name}\n{context_diff.text_diff(name)}", "sql") - ) + tree = self._add_modified_models( + context_diff, + modified_snapshot_ids, + tree, + environment_naming_info, + default_catalog, + no_diff, + ) - if direct.children: - tree.add(direct) - if indirect.children: - tree.add(self._limit_model_names(indirect, self.verbosity)) - if metadata.children: - tree.add(metadata) self._print(tree) + def _add_modified_models( + self, + context_diff: ContextDiff, + modified_snapshot_ids: t.Set[SnapshotId], + tree: Tree, + environment_naming_info: EnvironmentNamingInfo, + default_catalog: t.Optional[str] = None, + no_diff: bool = True, + ) -> Tree: + direct = Tree("[bold][direct]Directly Modified:") + indirect = Tree("[bold][indirect]Indirectly Modified:") + metadata = Tree("[bold][metadata]Metadata Updated:") + for s_id in modified_snapshot_ids: + name = s_id.name + display_name = context_diff.snapshots[s_id].display_name( + environment_naming_info, + default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, + dialect=self.dialect, + ) + if context_diff.directly_modified(name): + direct.add( + f"[direct]{display_name}" + if no_diff + else Syntax(f"{display_name}\n{context_diff.text_diff(name)}", "sql") + ) + elif context_diff.indirectly_modified(name): + indirect.add(f"[indirect]{display_name}") + elif context_diff.metadata_updated(name): + metadata.add( + f"[metadata]{display_name}" + if no_diff + else Syntax(f"{display_name}\n{context_diff.text_diff(name)}", "sql") + ) + if direct.children: + tree.add(direct) + if indirect.children: + tree.add(self._limit_model_names(indirect, self.verbosity)) + if metadata.children: + tree.add(metadata) + return tree + def _show_options_after_categorization( self, plan_builder: PlanBuilder, @@ -1882,6 +1979,71 @@ def loading_stop(self, id: uuid.UUID) -> None: self.loading_status[id].stop() del self.loading_status[id] + def show_table_diff_details( + self, + models_to_diff: t.List[str], + ) -> None: + """Display information about which tables are going to be diffed""" + + if models_to_diff: + m_tree = Tree("\n[b]Models to compare:") + for m in models_to_diff: + m_tree.add(f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m}[/{self.TABLE_DIFF_SOURCE_BLUE}]") + self._print(m_tree) + self._print("") + + def start_table_diff_progress(self, models_to_diff: int) -> None: + if not self.table_diff_progress: + self.table_diff_progress = make_progress_bar( + "Calculating model differences", self.console + ) + self.table_diff_model_progress = Progress( + TextColumn("{task.fields[view_name]}", justify="right"), + SpinnerColumn(spinner_name="simpleDots"), + console=self.console, + ) + + progress_table = Table.grid() + progress_table.add_row(self.table_diff_progress) + progress_table.add_row(self.table_diff_model_progress) + + self.table_diff_progress_live = Live(progress_table, refresh_per_second=10) + self.table_diff_progress_live.start() + + self.table_diff_model_task = self.table_diff_progress.add_task( + "Diffing", total=models_to_diff + ) + + def start_table_diff_model_progress(self, model: str) -> None: + if self.table_diff_model_progress and model not in self.table_diff_model_tasks: + self.table_diff_model_tasks[model] = self.table_diff_model_progress.add_task( + f"Diffing {model}...", + view_name=model, + total=1, + ) + + def update_table_diff_progress(self, model: str) -> None: + if self.table_diff_progress: + self.table_diff_progress.update(self.table_diff_model_task, refresh=True, advance=1) + if self.table_diff_model_progress and model in self.table_diff_model_tasks: + model_task_id = self.table_diff_model_tasks[model] + self.table_diff_model_progress.remove_task(model_task_id) + + def stop_table_diff_progress(self, success: bool) -> None: + if self.table_diff_progress_live: + self.table_diff_progress_live.stop() + self.table_diff_progress_live = None + self.log_status_update("") + + if success: + self.log_success(f"Table diff completed successfully!") + else: + self.log_error("Table diff failed!") + + self.table_diff_progress = None + self.table_diff_model_progress = None + self.table_diff_model_tasks = {} + def show_table_diff_summary(self, table_diff: TableDiff) -> None: tree = Tree("\n[b]Table Diff") @@ -1897,7 +2059,9 @@ def show_table_diff_summary(self, table_diff: TableDiff) -> None: ) envs.add(source) - target = Tree(f"Target: [green]{table_diff.target_alias}[/green]") + target = Tree( + f"Target: [{self.TABLE_DIFF_TARGET_GREEN}]{table_diff.target_alias}[/{self.TABLE_DIFF_TARGET_GREEN}]" + ) envs.add(target) tree.add(envs) @@ -1907,7 +2071,9 @@ def show_table_diff_summary(self, table_diff: TableDiff) -> None: tables.add( f"Source: [{self.TABLE_DIFF_SOURCE_BLUE}]{table_diff.source}[/{self.TABLE_DIFF_SOURCE_BLUE}]" ) - tables.add(f"Target: [green]{table_diff.target}[/green]") + tables.add( + f"Target: [{self.TABLE_DIFF_TARGET_GREEN}]{table_diff.target}[/{self.TABLE_DIFF_TARGET_GREEN}]" + ) tree.add(tables) @@ -1928,7 +2094,7 @@ def show_schema_diff(self, schema_diff: SchemaDiff) -> None: if schema_diff.target_alias: target_name = schema_diff.target_alias.upper() - first_line = f"\n[b]Schema Diff Between '[{self.TABLE_DIFF_SOURCE_BLUE}]{source_name}[/{self.TABLE_DIFF_SOURCE_BLUE}]' and '[green]{target_name}[/green]'" + first_line = f"\n[b]Schema Diff Between '[{self.TABLE_DIFF_SOURCE_BLUE}]{source_name}[/{self.TABLE_DIFF_SOURCE_BLUE}]' and '[{self.TABLE_DIFF_TARGET_GREEN}]{target_name}[/{self.TABLE_DIFF_TARGET_GREEN}]'" if schema_diff.model_name: first_line = ( first_line + f" environments for model '[blue]{schema_diff.model_name}[/blue]'" @@ -2032,7 +2198,7 @@ def show_row_diff( column_styles = { source_name: self.TABLE_DIFF_SOURCE_BLUE, - target_name: "green", + target_name: self.TABLE_DIFF_TARGET_GREEN, } for column, [source_column, target_column] in columns.items(): @@ -2089,6 +2255,57 @@ def show_row_diff( self.console.print(f"\n[b][green]{target_name} ONLY[/green] sample rows:[/b]") self.console.print(row_diff.t_sample.to_string(index=False), end="\n\n") + def show_table_diff( + self, + table_diffs: t.List[TableDiff], + show_sample: bool = True, + skip_grain_check: bool = False, + temp_schema: t.Optional[str] = None, + ) -> None: + """ + Display the table diff between all mismatched tables. + """ + if len(table_diffs) > 1: + mismatched_tables = [] + fully_matched = [] + for table_diff in table_diffs: + if ( + table_diff.schema_diff().source_schema == table_diff.schema_diff().target_schema + ) and ( + table_diff.row_diff( + temp_schema=temp_schema, skip_grain_check=skip_grain_check + ).full_match_pct + == 100 + ): + fully_matched.append(table_diff) + else: + mismatched_tables.append(table_diff) + table_diffs = mismatched_tables if mismatched_tables else [] + if fully_matched: + m_tree = Tree("\n[b]Identical Tables") + for m in fully_matched: + m_tree.add( + f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m.source}[/{self.TABLE_DIFF_SOURCE_BLUE}] - [{self.TABLE_DIFF_TARGET_GREEN}]{m.target}[/{self.TABLE_DIFF_TARGET_GREEN}]" + ) + self._print(m_tree) + + if mismatched_tables: + m_tree = Tree("\n[b]Mismatched Tables") + for m in mismatched_tables: + m_tree.add( + f"[{self.TABLE_DIFF_SOURCE_BLUE}]{m.source}[/{self.TABLE_DIFF_SOURCE_BLUE}] - [{self.TABLE_DIFF_TARGET_GREEN}]{m.target}[/{self.TABLE_DIFF_TARGET_GREEN}]" + ) + self._print(m_tree) + + for table_diff in table_diffs: + self.show_table_diff_summary(table_diff) + self.show_schema_diff(table_diff.schema_diff()) + self.show_row_diff( + table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check), + show_sample=show_sample, + skip_grain_check=skip_grain_check, + ) + def print_environments(self, environments_summary: t.List[EnvironmentSummary]) -> None: """Prints all environment names along with expiry datetime.""" output = [ @@ -2643,23 +2860,11 @@ def show_model_difference_summary( no_diff: Hide the actual SQL differences. """ added_snapshots = {context_diff.snapshots[s_id] for s_id in context_diff.added} - added_snapshot_models = {s for s in added_snapshots if s.is_model} - if added_snapshot_models: + if added_snapshots: self._print("\n**Added Models:**") - added_models = sorted(added_snapshot_models) - list_length = len(added_models) - if ( - self.verbosity < Verbosity.VERY_VERBOSE - and list_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD - ): - self._print(added_models[0]) - self._print(f"- `.... {list_length - 2} more ....`\n") - self._print(added_models[-1]) - else: - for snapshot in added_models: - self._print( - f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" - ) + self._print_models_with_threshold( + environment_naming_info, {s for s in added_snapshots if s.is_model}, default_catalog + ) added_snapshot_audits = {s for s in added_snapshots if s.is_audit} if added_snapshot_audits: @@ -2670,23 +2875,13 @@ def show_model_difference_summary( ) removed_snapshot_table_infos = set(context_diff.removed_snapshots.values()) - removed_model_snapshot_table_infos = {s for s in removed_snapshot_table_infos if s.is_model} - if removed_model_snapshot_table_infos: + if removed_snapshot_table_infos: self._print("\n**Removed Models:**") - removed_models = sorted(removed_model_snapshot_table_infos) - list_length = len(removed_models) - if ( - self.verbosity < Verbosity.VERY_VERBOSE - and list_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD - ): - self._print(removed_models[0]) - self._print(f"- `.... {list_length - 2} more ....`\n") - self._print(removed_models[-1]) - else: - for snapshot_table_info in removed_models: - self._print( - f"- `{snapshot_table_info.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" - ) + self._print_models_with_threshold( + environment_naming_info, + {s for s in removed_snapshot_table_infos if s.is_model}, + default_catalog, + ) removed_audit_snapshot_table_infos = {s for s in removed_snapshot_table_infos if s.is_audit} if removed_audit_snapshot_table_infos: @@ -2700,48 +2895,72 @@ def show_model_difference_summary( current_snapshot for current_snapshot, _ in context_diff.modified_snapshots.values() } if modified_snapshots: - directly_modified = [] - indirectly_modified = [] - metadata_modified = [] - for snapshot in modified_snapshots: - if context_diff.directly_modified(snapshot.name): - directly_modified.append(snapshot) - elif context_diff.indirectly_modified(snapshot.name): - indirectly_modified.append(snapshot) - elif context_diff.metadata_updated(snapshot.name): - metadata_modified.append(snapshot) - if directly_modified: - self._print("\n**Directly Modified:**") - for snapshot in sorted(directly_modified): - self._print( - f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" - ) - if not no_diff: - self._print(f"```diff\n{context_diff.text_diff(snapshot.name)}\n```") - if indirectly_modified: - self._print("\n**Indirectly Modified:**") - indirectly_modified = sorted(indirectly_modified) - modified_length = len(indirectly_modified) - if ( - self.verbosity < Verbosity.VERY_VERBOSE - and modified_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD - ): - self._print( - f"- `{indirectly_modified[0].display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`\n" - f"- `.... {modified_length - 2} more ....`\n" - f"- `{indirectly_modified[-1].display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" - ) - else: - for snapshot in indirectly_modified: - self._print( - f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" - ) - if metadata_modified: - self._print("\n**Metadata Updated:**") - for snapshot in sorted(metadata_modified): - self._print( - f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" - ) + self._print_modified_models( + context_diff, modified_snapshots, environment_naming_info, default_catalog, no_diff + ) + + def _print_models_with_threshold( + self, + environment_naming_info: EnvironmentNamingInfo, + snapshot_table_infos: t.Set[SnapshotInfoLike], + default_catalog: t.Optional[str] = None, + ) -> None: + models = sorted(snapshot_table_infos) + list_length = len(models) + if ( + self.verbosity < Verbosity.VERY_VERBOSE + and list_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD + ): + self._print( + f"- `{models[0].display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" + ) + self._print(f"- `.... {list_length - 2} more ....`\n") + self._print( + f"- `{models[-1].display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" + ) + else: + for snapshot_table_info in models: + self._print( + f"- `{snapshot_table_info.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" + ) + + def _print_modified_models( + self, + context_diff: ContextDiff, + modified_snapshots: t.Set[Snapshot], + environment_naming_info: EnvironmentNamingInfo, + default_catalog: t.Optional[str] = None, + no_diff: bool = True, + ) -> None: + directly_modified = [] + indirectly_modified = [] + metadata_modified = [] + for snapshot in modified_snapshots: + if context_diff.directly_modified(snapshot.name): + directly_modified.append(snapshot) + elif context_diff.indirectly_modified(snapshot.name): + indirectly_modified.append(snapshot) + elif context_diff.metadata_updated(snapshot.name): + metadata_modified.append(snapshot) + if directly_modified: + self._print("\n**Directly Modified:**") + for snapshot in sorted(directly_modified): + self._print( + f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" + ) + if not no_diff: + self._print(f"```diff\n{context_diff.text_diff(snapshot.name)}\n```") + if indirectly_modified: + self._print("\n**Indirectly Modified:**") + self._print_models_with_threshold( + environment_naming_info, set(indirectly_modified), default_catalog + ) + if metadata_modified: + self._print("\n**Metadata Updated:**") + for snapshot in sorted(metadata_modified): + self._print( + f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" + ) def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> None: """Displays the models with missing dates.""" diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 18aadde20f..6e1ff1eca1 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -116,6 +116,7 @@ ) from sqlmesh.core.user import User from sqlmesh.utils import UniqueKeyDict, Verbosity +from sqlmesh.utils.concurrency import concurrent_apply_to_values from sqlmesh.utils.dag import DAG from sqlmesh.utils.date import TimeLike, now_ds, to_timestamp, format_tz_datetime, now_timestamp from sqlmesh.utils.errors import ( @@ -1568,9 +1569,9 @@ def table_diff( self, source: str, target: str, - on: t.List[str] | exp.Condition | None = None, - skip_columns: t.List[str] | None = None, - model_or_snapshot: t.Optional[ModelOrSnapshot] = None, + on: t.Optional[t.List[str] | exp.Condition] = None, + skip_columns: t.Optional[t.List[str]] = None, + select_models: t.Optional[t.Collection[str]] = None, where: t.Optional[str | exp.Condition] = None, limit: int = 20, show: bool = True, @@ -1578,7 +1579,7 @@ def table_diff( decimals: int = 3, skip_grain_check: bool = False, temp_schema: t.Optional[str] = None, - ) -> TableDiff: + ) -> t.List[TableDiff]: """Show a diff between two tables. Args: @@ -1587,7 +1588,7 @@ def table_diff( on: The join condition, table aliases must be "s" and "t" for source and target. If omitted, the table's grain will be used. skip_columns: The columns to skip when computing the table diff. - model_or_snapshot: The model or snapshot to use when environments are passed in. + select_models: The models or snapshots to use when environments are passed in. where: An optional where statement to filter results. limit: The limit of the sample dataframe. show: Show the table diff output in the console. @@ -1597,53 +1598,191 @@ def table_diff( temp_schema: The schema to use for temporary tables. Returns: - The TableDiff object containing schema and summary differences. + The list of TableDiff objects containing schema and summary differences. """ - source_alias, target_alias = source, target - adapter = self.engine_adapter + table_diffs: t.List[TableDiff] = [] - if model_or_snapshot: - model = self.get_model(model_or_snapshot, raise_if_missing=True) - adapter = self._get_engine_adapter(model.gateway) + # Diffs multiple or a single model across two environments + if select_models: source_env = self.state_reader.get_environment(source) target_env = self.state_reader.get_environment(target) - if not source_env: raise SQLMeshError(f"Could not find environment '{source}'") if not target_env: - raise SQLMeshError(f"Could not find environment '{target}')") - - # Compare the virtual layer instead of the physical layer because the virtual layer is guaranteed to point - # to the correct/active snapshot for the model in the specified environment, taking into account things like dev previews - source = next( - snapshot for snapshot in source_env.snapshots if snapshot.name == model.fqn - ).qualified_view_name.for_environment(source_env.naming_info, adapter.dialect) - - target = next( - snapshot for snapshot in target_env.snapshots if snapshot.name == model.fqn - ).qualified_view_name.for_environment(target_env.naming_info, adapter.dialect) - - source_alias = source_env.name - target_alias = target_env.name - - if not on: - on = [] - for expr in [ref.expression for ref in model.all_references if ref.unique]: - if isinstance(expr, exp.Tuple): - on.extend( - [key.this.sql(dialect=adapter.dialect) for key in expr.expressions] + raise SQLMeshError(f"Could not find environment '{target}'") + criteria = ", ".join(f"'{c}'" for c in select_models) + try: + selected_models = self._new_selector().expand_model_selections(select_models) + if not selected_models: + self.console.log_status_update( + f"No models matched the selection criteria: {criteria}" + ) + except Exception as e: + raise SQLMeshError(e) + + models_to_diff: t.List[ + t.Tuple[Model, EngineAdapter, str, str, t.Optional[t.List[str] | exp.Condition]] + ] = [] + models_without_grain: t.List[Model] = [] + source_snapshots_to_name = { + snapshot.name: snapshot for snapshot in source_env.snapshots + } + target_snapshots_to_name = { + snapshot.name: snapshot for snapshot in target_env.snapshots + } + + for model_fqn in selected_models: + model = self._models[model_fqn] + adapter = self._get_engine_adapter(model.gateway) + source_snapshot = source_snapshots_to_name.get(model.fqn) + target_snapshot = target_snapshots_to_name.get(model.fqn) + + if target_snapshot and source_snapshot: + if ( + source_snapshot.fingerprint.data_hash + != target_snapshot.fingerprint.data_hash + ): + # Compare the virtual layer instead of the physical layer because the virtual layer is guaranteed to point + # to the correct/active snapshot for the model in the specified environment, taking into account things like dev previews + source = source_snapshot.qualified_view_name.for_environment( + source_env.naming_info, adapter.dialect ) - else: - # Handle a single Column or Paren expression - on.append(expr.this.sql(dialect=adapter.dialect)) + target = target_snapshot.qualified_view_name.for_environment( + target_env.naming_info, adapter.dialect + ) + model_on = on or model.on + models_to_diff.append((model, adapter, source, target, model_on)) + if not model_on: + models_without_grain.append(model) + + if models_to_diff: + self.console.show_table_diff_details( + [model[0].name for model in models_to_diff], + ) + if models_without_grain: + model_names = "\n".join( + f"─ {model.name} \n at '{model._path}'" for model in models_without_grain + ) + raise SQLMeshError( + f"SQLMesh doesn't know how to join the tables for the following models:\n{model_names}\n" + "\nPlease specify the `grain` in each model definition. Must be unique and not null." + ) + + self.console.start_table_diff_progress(len(models_to_diff)) + try: + tasks_num = min(len(models_to_diff), self.concurrent_tasks) + table_diffs = concurrent_apply_to_values( + list(models_to_diff), + lambda model_info: self._model_diff( + model=model_info[0], + adapter=model_info[1], + source=model_info[2], + target=model_info[3], + on=model_info[4], + source_alias=source_env.name, + target_alias=target_env.name, + limit=limit, + decimals=decimals, + skip_columns=skip_columns, + where=where, + show=show, + temp_schema=temp_schema, + skip_grain_check=skip_grain_check, + ), + tasks_num=tasks_num, + ) + self.console.stop_table_diff_progress(success=True) + except: + self.console.stop_table_diff_progress(success=False) + raise + elif selected_models: + self.console.log_status_update( + f"No models contain differences with the selection criteria: {criteria}" + ) + + else: + table_diffs = [ + self._table_diff( + source=source, + target=target, + source_alias=source, + target_alias=target, + limit=limit, + decimals=decimals, + adapter=self.engine_adapter, + on=on, + skip_columns=skip_columns, + where=where, + ) + ] + if show: + self.console.show_table_diff(table_diffs, show_sample, skip_grain_check, temp_schema) + + return table_diffs + + def _model_diff( + self, + model: Model, + adapter: EngineAdapter, + source: str, + target: str, + source_alias: str, + target_alias: str, + limit: int, + decimals: int, + on: t.Optional[t.List[str] | exp.Condition] = None, + skip_columns: t.Optional[t.List[str]] = None, + where: t.Optional[str | exp.Condition] = None, + show: bool = True, + temp_schema: t.Optional[str] = None, + skip_grain_check: bool = False, + ) -> TableDiff: + self.console.start_table_diff_model_progress(model.name) + + table_diff = self._table_diff( + on=on, + skip_columns=skip_columns, + where=where, + limit=limit, + decimals=decimals, + model=model, + adapter=adapter, + source=source, + target=target, + source_alias=source_alias, + target_alias=target_alias, + ) + + if show: + # Trigger row_diff in parallel execution so it's available for ordered display later + table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check) + + self.console.update_table_diff_progress(model.name) + + return table_diff + + def _table_diff( + self, + source: str, + target: str, + source_alias: str, + target_alias: str, + limit: int, + decimals: int, + adapter: EngineAdapter, + on: t.Optional[t.List[str] | exp.Condition] = None, + model: t.Optional[Model] = None, + skip_columns: t.Optional[t.List[str]] = None, + where: t.Optional[str | exp.Condition] = None, + ) -> TableDiff: if not on: raise SQLMeshError( "SQLMesh doesn't know how to join the two tables. Specify the `grains` in each model definition or pass join column names in separate `-o` flags." ) - table_diff = TableDiff( + return TableDiff( adapter=adapter.with_log_level(logger.getEffectiveLevel()), source=source, target=target, @@ -1652,20 +1791,11 @@ def table_diff( where=where, source_alias=source_alias, target_alias=target_alias, - model_name=model.name if model_or_snapshot else None, - model_dialect=model.dialect if model_or_snapshot else None, limit=limit, decimals=decimals, + model_name=model.name if model else None, + model_dialect=model.dialect if model else None, ) - if show: - self.console.show_table_diff_summary(table_diff) - self.console.show_schema_diff(table_diff.schema_diff()) - self.console.show_row_diff( - table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check), - show_sample=show_sample, - skip_grain_check=skip_grain_check, - ) - return table_diff @python_api_analytics def get_dag( diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 29c82bc33f..85d99992fc 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -450,6 +450,20 @@ def all_references(self) -> t.List[Reference]: Reference(model_name=self.name, expression=e, unique=True) for e in self.references ] + @property + def on(self) -> t.List[str]: + """The grains to be used as join condition in table_diff.""" + + on: t.List[str] = [] + for expr in [ref.expression for ref in self.all_references if ref.unique]: + if isinstance(expr, exp.Tuple): + on.extend([key.this.sql(dialect=self.dialect) for key in expr.expressions]) + else: + # Handle a single Column or Paren expression + on.append(expr.this.sql(dialect=self.dialect)) + + return on + @property def managed_columns(self) -> t.Dict[str, exp.DataType]: return getattr(self.kind, "managed_columns", {}) diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 80554a60a2..4b4ec7f23e 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -686,6 +686,12 @@ def create_external_models(self, context: Context, line: str) -> None: default=3, help="The number of decimal places to keep when comparing floating point columns. Default: 3", ) + @argument( + "--select-model", + type=str, + nargs="*", + help="Specify one or more models to data diff. Use wildcards to diff multiple models. Ex: '*' (all models with applied plan diffs), 'demo.model+' (this and downstream models), 'git:feature_branch' (models with direct modifications in this branch only)", + ) @argument( "--skip-grain-check", action="store_true", @@ -700,12 +706,13 @@ def table_diff(self, context: Context, line: str) -> None: """ args = parse_argstring(self.table_diff, line) source, target = args.source_to_target.split(":") + select_models = {args.model} if args.model else args.select_model or None context.table_diff( source=source, target=target, on=args.on, skip_columns=args.skip_columns, - model_or_snapshot=args.model, + select_models=select_models, where=args.where, limit=args.limit, show_sample=args.show_sample, diff --git a/sqlmesh/utils/git.py b/sqlmesh/utils/git.py index 9a558dec9a..00410e776c 100644 --- a/sqlmesh/utils/git.py +++ b/sqlmesh/utils/git.py @@ -27,7 +27,23 @@ def _execute_list_output(self, commands: t.List[str], base_path: Path) -> t.List return [(base_path / o).absolute() for o in self._execute(commands).split("\n") if o] def _execute(self, commands: t.List[str]) -> str: - result = subprocess.run(["git"] + commands, cwd=self._work_dir, stdout=subprocess.PIPE) + result = subprocess.run( + ["git"] + commands, + cwd=self._work_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + # If the Git command failed, extract and raise the error message in the console + if result.returncode != 0: + stderr_output = result.stderr.decode("utf-8").strip() + error_message = next( + (line for line in stderr_output.splitlines() if line.lower().startswith("fatal:")), + stderr_output, + ) + raise RuntimeError(f"Git error: {error_message}") + return result.stdout.decode("utf-8").strip() @cached_property diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index f01ad4a6d7..11a98524e8 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -110,8 +110,8 @@ def test_data_diff(sushi_context_fixed_date, capsys, caplog): source="source_dev", target="target_dev", on=exp.condition("s.customer_id = t.customer_id AND s.event_date = t.event_date"), - model_or_snapshot="sushi.customer_revenue_by_day", - ) + select_models={"sushi.customer_revenue_by_day"}, + )[0] # verify queries were actually logged to the log file, this helps immensely with debugging console_output = capsys.readouterr() @@ -169,7 +169,7 @@ def test_data_diff_decimals(sushi_context_fixed_date): source="table_diff_source", target="table_diff_target", on=["key"], - ) + )[0] assert diff.row_diff().full_match_count == 3 assert diff.row_diff().partial_match_count == 0 @@ -178,7 +178,7 @@ def test_data_diff_decimals(sushi_context_fixed_date): target="table_diff_target", on=["key"], decimals=4, - ) + )[0] row_diff = diff.row_diff() joined_sample_columns = row_diff.joined_sample.columns @@ -291,9 +291,9 @@ def test_grain_check(sushi_context_fixed_date): source="source_dev", target="target_dev", on=["'key_1'", "key_2"], - model_or_snapshot="SUSHI.GRAIN_ITEMS", + select_models={"memory.sushi*"}, skip_grain_check=False, - ) + )[0] row_diff = diff.row_diff() assert row_diff.full_match_count == 7 @@ -420,9 +420,9 @@ def test_tables_and_grain_inferred_from_model(sushi_context_fixed_date: Context) sushi_context_fixed_date.plan(environment="unit_test", auto_apply=True, include_unmodified=True) table_diff = sushi_context_fixed_date.table_diff( - source="unit_test", target="prod", model_or_snapshot="sushi.waiter_revenue_by_day" - ) - + source="unit_test", target="prod", select_models={"sushi.waiter_revenue_by_day"} + )[0] + assert isinstance(table_diff, TableDiff) assert table_diff.source == "memory.sushi__unit_test.waiter_revenue_by_day" assert table_diff.target == "memory.sushi.waiter_revenue_by_day" @@ -562,3 +562,136 @@ def test_data_diff_nullable_booleans(): """ assert strip_ansi_codes(output) == expected_output.strip() + + +@pytest.mark.slow +def test_data_diff_multiple_models(sushi_context_fixed_date, capsys, caplog): + # Create first analytics model + expressions = d.parse( + """ + MODEL (name memory.sushi.analytics_1, kind full, grain(key), tags (finance),); + SELECT + key, + value, + FROM + (VALUES + (1, 3), + (2, 4), + ) AS t (key, value) + """ + ) + model_s = load_sql_based_model(expressions, dialect="snowflake") + sushi_context_fixed_date.upsert_model(model_s) + + # Create second analytics model from analytics_1 + expressions_2 = d.parse( + """ + MODEL (name memory.sushi.analytics_2, kind full, grain(key), tags (finance),); + SELECT + key, + value as amount, + FROM + memory.sushi.analytics_1 + """ + ) + model_s2 = load_sql_based_model(expressions_2, dialect="snowflake") + sushi_context_fixed_date.upsert_model(model_s2) + + sushi_context_fixed_date.plan( + "source_dev", + no_prompts=True, + auto_apply=True, + skip_tests=True, + start="2023-01-31", + end="2023-01-31", + ) + + # Modify first model + model = sushi_context_fixed_date.models['"MEMORY"."SUSHI"."ANALYTICS_1"'] + modified_model = model.dict() + modified_model["query"] = ( + exp.select("*") + .from_(model.query.subquery()) + .union("SELECT key, value FROM (VALUES (1, 6),(2,3),) AS t (key, value)") + ) + modified_sqlmodel = SqlModel(**modified_model) + sushi_context_fixed_date.upsert_model(modified_sqlmodel) + + # Modify second model + model2 = sushi_context_fixed_date.models['"MEMORY"."SUSHI"."ANALYTICS_2"'] + modified_model2 = model2.dict() + modified_model2["query"] = ( + exp.select("*") + .from_(model2.query.subquery()) + .union("SELECT key, amount FROM (VALUES (5, 150.2),(6,250.2),) AS t (key, amount)") + ) + modified_sqlmodel2 = SqlModel(**modified_model2) + sushi_context_fixed_date.upsert_model(modified_sqlmodel2) + + sushi_context_fixed_date.auto_categorize_changes = CategorizerConfig( + sql=AutoCategorizationMode.FULL + ) + sushi_context_fixed_date.plan( + "target_dev", + create_from="source_dev", + no_prompts=True, + auto_apply=True, + skip_tests=True, + start="2023-01-31", + end="2023-01-31", + ) + + # Get diffs for both models + selector = {"tag:finance & memory.sushi.analytics*"} + diffs = sushi_context_fixed_date.table_diff( + source="source_dev", + target="target_dev", + on=["key"], + select_models=selector, + skip_grain_check=False, + ) + + assert len(diffs) == 2 + + # Check analytics_1 diff + diff1 = next(d for d in diffs if "ANALYTICS_1" in d.source) + row_diff1 = diff1.row_diff() + assert row_diff1.full_match_count == 2 + assert row_diff1.full_match_pct == 50.0 + assert row_diff1.s_only_count == 0 + assert row_diff1.t_only_count == 0 + assert row_diff1.stats["join_count"] == 4 + assert row_diff1.stats["null_grain_count"] == 0 + assert row_diff1.stats["s_count"] == 4 + assert row_diff1.stats["distinct_count_s"] == 2 + assert row_diff1.stats["t_count"] == 4 + assert row_diff1.stats["distinct_count_t"] == 2 + assert row_diff1.s_sample.shape == (0, 2) + assert row_diff1.t_sample.shape == (0, 2) + + # Check analytics_2 diff + diff2 = next(d for d in diffs if "ANALYTICS_2" in d.source) + row_diff2 = diff2.row_diff() + assert row_diff2.full_match_count == 2 + assert row_diff2.full_match_pct == 40.0 + assert row_diff2.s_only_count == 0 + assert row_diff2.t_only_count == 2 + assert row_diff2.stats["join_count"] == 4 + assert row_diff2.stats["null_grain_count"] == 0 + assert row_diff2.stats["s_count"] == 4 + assert row_diff2.stats["distinct_count_s"] == 2 + assert row_diff2.stats["t_count"] == 6 + assert row_diff2.stats["distinct_count_t"] == 4 + assert row_diff2.s_sample.shape == (0, 2) + assert row_diff2.t_sample.shape == (2, 2) + + # This selector shouldn't return any diffs since both models have this tag + selector = {"^tag:finance"} + diffs = sushi_context_fixed_date.table_diff( + source="source_dev", + target="target_dev", + on=["key"], + select_models=selector, + skip_grain_check=False, + ) + assert len(diffs) == 0 diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index 9519668ab3..6bfc4b8df3 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -641,26 +641,10 @@ def test_table_diff(notebook, loaded_sushi_context, convert_all_html_output_to_t assert not output.stdout assert not output.stderr - assert len(output.outputs) == 5 + + assert len(output.outputs) == 1 assert convert_all_html_output_to_text(output) == [ - """Table Diff -├── Model: -│ └── sushi.top_waiters -├── Environment: -│ ├── Source: dev -│ └── Target: prod -├── Tables: -│ ├── Source: memory.sushi__dev.top_waiters -│ └── Target: memory.sushi.top_waiters -└── Join On: - └── waiter_id""", - """Schema Diff Between 'DEV' and 'PROD' environments for model 'sushi.top_waiters': -└── Schemas match""", - """Row Counts: -└── FULL MATCH: 8 rows (100.0%)""", - """COMMON ROWS column comparison stats:""", - """pct_match -revenue 100.0""", + "No models contain differences with the selection criteria: 'sushi.top_waiters'" ] diff --git a/tests/web/test_main.py b/tests/web/test_main.py index 5ecbba8d1f..99b268cf0d 100644 --- a/tests/web/test_main.py +++ b/tests/web/test_main.py @@ -520,8 +520,7 @@ def test_table_diff(client: TestClient, web_sushi_context: Context) -> None: }, ) assert response.status_code == 200 - assert "schema_diff" in response.json() - assert "row_diff" in response.json() + assert response.json() == None def test_test(client: TestClient, web_sushi_context: Context) -> None: diff --git a/web/server/api/endpoints/table_diff.py b/web/server/api/endpoints/table_diff.py index 7d0da2cc01..3439327102 100644 --- a/web/server/api/endpoints/table_diff.py +++ b/web/server/api/endpoints/table_diff.py @@ -23,17 +23,22 @@ def get_table_diff( temp_schema: t.Optional[str] = None, limit: int = 20, context: Context = Depends(get_loaded_context), -) -> TableDiff: +) -> t.Optional[TableDiff]: """Calculate differences between tables, taking into account schema and row level differences.""" - diff = context.table_diff( + table_diffs = context.table_diff( source=source, target=target, on=exp.condition(on) if on else None, - model_or_snapshot=model_or_snapshot, + select_models={model_or_snapshot} if model_or_snapshot else None, where=where, limit=limit, show=False, ) + + if not table_diffs: + return None + diff = table_diffs[0] if isinstance(table_diffs, list) else table_diffs + _schema_diff = diff.schema_diff() _row_diff = diff.row_diff(temp_schema=temp_schema) schema_diff = SchemaDiff( From 3117d7616cc4b313dd99757d5be0c88c81a8516c Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 6 May 2025 21:57:30 +0300 Subject: [PATCH 0104/1056] Fix!: exclude Semicolon expressions from model state (#4257) --- sqlmesh/core/model/common.py | 4 +- ...rn_if_incorrectly_duplicated_statements.py | 68 ++++++++++++++ tests/core/test_model.py | 93 +++++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index 9e0aed8783..61d331f306 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -262,7 +262,9 @@ def parse_expression( if isinstance(v, list): return [ - d.parse_one(e, dialect=dialect) if not isinstance(e, exp.Expression) else e for e in v + e if isinstance(e, exp.Expression) else d.parse_one(e, dialect=dialect) + for e in v + if not isinstance(e, exp.Semicolon) ] if isinstance(v, str): diff --git a/sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py b/sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py new file mode 100644 index 0000000000..7fb9affb1d --- /dev/null +++ b/sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py @@ -0,0 +1,68 @@ +""" +This script's goal is to warn users if there are two adjacent expressions in a SQL +model that are equivalent. + +Context: + +We used to include `Semicolon` expressions in the model's state, which led to a bug +where the expression preceding the semicolon would be duplicated in pre_statements +or post_statements. For example, the query in the model below would be incorrectly +included in its post_statements list: + +``` +MODEL ( + name test +); + +SELECT 1 AS c; + +-- foo +``` + +We now don't include `Semicolon` expressions in the model's state, which fixes this +issue, but unfortunately migrating existing snapshots is not possible because we do +not have a signal in state to detect whether an expression was incorrectly duplicated. + +If a SQL model suffered from this issue, then there would be two adjacent equivalent +expressions in it, so we use that as a heuristic to warn the user accordingly. +""" + +import json + +from sqlglot import exp + +from sqlmesh.core.console import get_console + + +def migrate(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + warning = ( + "SQLMesh detected that it may not be able to fully migrate the state database. This should not impact " + "the migration process, but may result in unexpected changes being reported by the next `sqlmesh plan` " + "command. Please run `sqlmesh diff prod` after the migration has completed, before making any new " + "changes. If any unexpected changes are reported, consider running a forward-only plan to apply these " + "changes and avoid unnecessary backfills: sqlmesh plan prod --forward-only. " + "See https://sqlmesh.readthedocs.io/en/stable/concepts/plans/#forward-only-plans for more details.\n" + ) + + for (snapshot,) in engine_adapter.fetchall( + exp.select("snapshot").from_(snapshots_table), quote_identifiers=True + ): + parsed_snapshot = json.loads(snapshot) + node = parsed_snapshot["node"] + + if node.get("source_type") == "sql": + expressions = [ + *node.get("pre_statements", []), + node["query"], + *node.get("post_statements", []), + ] + for e1, e2 in zip(expressions, expressions[1:]): + if e1 == e2: + get_console().log_warning(warning) + return diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 2a72903f52..3d9f3a81cf 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9332,6 +9332,7 @@ def test_python_env_references_are_unequal_but_point_to_same_definition(tmp_path db_path = str(tmp_path / "db.db") db_connection = DuckDBConnectionConfig(database=db_path) + config = Config( gateways={"duckdb": GatewayConfig(connection=db_connection)}, model_defaults=ModelDefaultsConfig(dialect="duckdb"), @@ -9447,3 +9448,95 @@ def f(): with pytest.raises(SQLMeshError, match=r"duplicate definitions found"): Context(paths=tmp_path, config=config) + + +def test_semicolon_is_not_included_in_model_state(tmp_path, assert_exp_eq): + init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + + db_connection = DuckDBConnectionConfig(database=str(tmp_path / "db.db")) + config = Config( + gateways={"duckdb": GatewayConfig(connection=db_connection)}, + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + ) + + model_file = tmp_path / "models" / "model_with_semicolon.sql" + model_file.write_text( + """ + MODEL ( + name sqlmesh_example.incremental_model_with_semicolon, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column event_date + ), + start '2020-01-01', + cron '@daily', + grain (id, event_date) + ); + + SELECT + 1 AS id, + 1 AS item_id, + CAST('2020-01-01' AS DATE) AS event_date + ; + + --Just a comment + """ + ) + + ctx = Context(paths=tmp_path, config=config) + model = ctx.get_model("sqlmesh_example.incremental_model_with_semicolon") + + assert not model.pre_statements + assert not model.post_statements + + assert_exp_eq( + model.render_query(), + 'SELECT 1 AS "id", 1 AS "item_id", CAST(\'2020-01-01\' AS DATE) AS "event_date"', + ) + ctx.format() + + assert ( + model_file.read_text() + == """MODEL ( + name sqlmesh_example.incremental_model_with_semicolon, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column event_date + ), + start '2020-01-01', + cron '@daily', + grain (id, event_date) +); + +SELECT + 1 AS id, + 1 AS item_id, + '2020-01-01'::DATE AS event_date; + +/* Just a comment */""" + ) + + ctx.plan(no_prompts=True, auto_apply=True) + + model_file = tmp_path / "models" / "model_with_semicolon.sql" + model_file.write_text( + """ + MODEL ( + name sqlmesh_example.incremental_model_with_semicolon, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column event_date + ), + start '2020-01-01', + cron '@daily', + grain (id, event_date) + ); + + SELECT + 1 AS id, + 1 AS item_id, + CAST('2020-01-01' AS DATE) AS event_date + """ + ) + + ctx.load() + plan = ctx.plan(no_prompts=True, auto_apply=True) + + assert not plan.context_diff.modified_snapshots From 3d368c871c5c4e6218a57f4a4131151ca85496bc Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 6 May 2025 22:09:30 +0300 Subject: [PATCH 0105/1056] Feat: add suport for `@EACH` dynamic blueprints, improve docs (#4317) --- docs/concepts/models/python_models.md | 27 ++++++++++++- docs/concepts/models/sql_models.md | 56 +++++++++++++++++++++++++-- sqlmesh/core/model/decorator.py | 3 ++ sqlmesh/core/model/definition.py | 8 +++- tests/core/test_model.py | 53 +++++++++++++++++++++++-- 5 files changed, 137 insertions(+), 10 deletions(-) diff --git a/docs/concepts/models/python_models.md b/docs/concepts/models/python_models.md index c735dfa133..1e6e2e2bd8 100644 --- a/docs/concepts/models/python_models.md +++ b/docs/concepts/models/python_models.md @@ -367,9 +367,32 @@ def entrypoint( ) ``` -!!! note +Blueprint variable mappings can also be constructed dynamically, e.g., by using a macro: `blueprints="@gen_blueprints()"`. This is useful in cases where the `blueprints` list needs to be sourced from external sources, such as CSV files. + +For example, the definition of the `gen_blueprints` may look like this: + +```python linenums="1" +from sqlmesh import macro + +@macro() +def gen_blueprints(evaluator): + return ( + "((customer := customer1, field_a := x, field_b := y)," + " (customer := customer2, field_a := z, field_b := w))" + ) +``` - Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints="@gen_blueprints()"`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files. +It's also possible to use the `@EACH` macro, combined with a global list variable (`@values`): + +```python linenums="1" + +@model( + "@{customer}.some_table", + blueprints="@EACH(@values, x -> (customer := schema_@x))", + ... +) +... +``` ## Examples ### Basic diff --git a/docs/concepts/models/sql_models.md b/docs/concepts/models/sql_models.md index 5be04ecca6..c57a70bde6 100644 --- a/docs/concepts/models/sql_models.md +++ b/docs/concepts/models/sql_models.md @@ -175,9 +175,33 @@ SELECT FROM customer2.some_source ``` -!!! note +Blueprint variable mappings can also be constructed dynamically, e.g., by using a macro: `blueprints @gen_blueprints()`. This is useful in cases where the `blueprints` list needs to be sourced from external sources, such as CSV files. + +For example, the definition of the `gen_blueprints` may look like this: + +```python linenums="1" +from sqlmesh import macro + +@macro() +def gen_blueprints(evaluator): + return ( + "((customer := customer1, field_a := x, field_b := y)," + " (customer := customer2, field_a := z, field_b := w))" + ) +``` + +It's also possible to use the `@EACH` macro, combined with a global list variable (`@values`): - Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints @gen_blueprints()`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files. +```sql linenums="1" +MODEL ( + name @customer.some_table, + kind FULL, + blueprints @EACH(@values, x -> (customer := schema_@x)), +); + +SELECT + 1 AS c +``` ## Python-based definition @@ -262,9 +286,33 @@ def entrypoint(evaluator: MacroEvaluator) -> str | exp.Expression: The two models produced from this template are the same as in the [example](#SQL-model-blueprinting) for SQL-based blueprinting. -!!! note +Blueprint variable mappings can also be constructed dynamically, e.g., by using a macro: `blueprints="@gen_blueprints()"`. This is useful in cases where the `blueprints` list needs to be sourced from external sources, such as CSV files. + +For example, the definition of the `gen_blueprints` may look like this: + +```python linenums="1" +from sqlmesh import macro + +@macro() +def gen_blueprints(evaluator): + return ( + "((customer := customer1, field_a := x, field_b := y)," + " (customer := customer2, field_a := z, field_b := w))" + ) +``` - Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints="@gen_blueprints()"`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files. +It's also possible to use the `@EACH` macro, combined with a global list variable (`@values`): + +```python linenums="1" + +@model( + "@{customer}.some_table", + is_sql=True, + blueprints="@EACH(@values, x -> (customer := schema_@x))", + ... +) +... +``` ## Automatic dependencies diff --git a/sqlmesh/core/model/decorator.py b/sqlmesh/core/model/decorator.py index 952b8276b0..0151e9ec76 100644 --- a/sqlmesh/core/model/decorator.py +++ b/sqlmesh/core/model/decorator.py @@ -117,6 +117,9 @@ def models( if not blueprints: raise_config_error("Failed to render blueprints property", path) + if len(blueprints) > 1: + blueprints = [exp.Tuple(expressions=blueprints)] + blueprints = blueprints[0] return create_models_from_blueprints( diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index ff46232d30..3dd7342944 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -1966,7 +1966,13 @@ def load_sql_based_models( if not rendered_blueprints: raise_config_error("Failed to render blueprints property", path) - blueprints = t.cast(t.List, rendered_blueprints)[0] + # Help mypy see that rendered_blueprints can't be None + assert rendered_blueprints + + if len(rendered_blueprints) > 1: + rendered_blueprints = [exp.Tuple(expressions=rendered_blueprints)] + + blueprints = rendered_blueprints[0] return create_models_from_blueprints( gateway=gateway, diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 3d9f3a81cf..3686b35479 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -8493,10 +8493,10 @@ def entrypoint(evaluator): ) -def test_dynamic_blueprinting(tmp_path: Path) -> None: +def test_dynamic_blueprinting_using_custom_macro(tmp_path: Path) -> None: init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) - dynamic_template_sql = tmp_path / "models/dynamic_template.sql" + dynamic_template_sql = tmp_path / "models/dynamic_template_custom_macro.sql" dynamic_template_sql.parent.mkdir(parents=True, exist_ok=True) dynamic_template_sql.write_text( """ @@ -8514,7 +8514,7 @@ def test_dynamic_blueprinting(tmp_path: Path) -> None: """ ) - dynamic_template_py = tmp_path / "models/dynamic_template.py" + dynamic_template_py = tmp_path / "models/dynamic_template_custom_macro.py" dynamic_template_py.parent.mkdir(parents=True, exist_ok=True) dynamic_template_py.write_text( """ @@ -8556,6 +8556,53 @@ def gen_blueprints(evaluator): assert '"memory"."customer2"."some_other_table"' in ctx.models +def test_dynamic_blueprinting_using_each(tmp_path: Path) -> None: + init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + + dynamic_template_sql = tmp_path / "models/dynamic_template_each.sql" + dynamic_template_sql.parent.mkdir(parents=True, exist_ok=True) + dynamic_template_sql.write_text( + """ + MODEL ( + name @customer.some_table, + kind FULL, + blueprints @EACH(@values, x -> (customer := schema_@x)), + ); + + SELECT + 1 AS c + """ + ) + + dynamic_template_py = tmp_path / "models/dynamic_template_each.py" + dynamic_template_py.parent.mkdir(parents=True, exist_ok=True) + dynamic_template_py.write_text( + """ +from sqlmesh import model + +@model( + "@{customer}.some_other_table", + kind="FULL", + blueprints="@EACH(@values, x -> (customer := schema_@x))", + is_sql=True, +) +def entrypoint(evaluator): + return "SELECT 1 AS c" +""" + ) + + model_defaults = ModelDefaultsConfig(dialect="duckdb") + variables = {"values": ["customer1", "customer2"]} + config = Config(model_defaults=model_defaults, variables=variables) + ctx = Context(config=config, paths=tmp_path) + + assert len(ctx.models) == 4 + assert '"memory"."schema_customer1"."some_table"' in ctx.models + assert '"memory"."schema_customer2"."some_table"' in ctx.models + assert '"memory"."schema_customer1"."some_other_table"' in ctx.models + assert '"memory"."schema_customer2"."some_other_table"' in ctx.models + + def test_single_blueprint(tmp_path: Path) -> None: init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) From a35fb91fbfe75166b5454183667017b5761433d8 Mon Sep 17 00:00:00 2001 From: Howard Liu Date: Wed, 7 May 2025 06:48:29 -0700 Subject: [PATCH 0106/1056] Chore: fix columns example in tests doc (#4324) --- docs/concepts/tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index 04f204f3ea..c1714ea982 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -605,7 +605,7 @@ An optional dictionary that maps columns to their types: ```yaml linenums="1" : columns: - - : + : ... ``` From 57f0baa70cd59cff113ba62b9bcf032461679853 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 7 May 2025 18:20:10 +0300 Subject: [PATCH 0107/1056] Fix: use maybe_parse for python model depends_on instead of parse_one (#4326) --- sqlmesh/core/model/definition.py | 2 +- tests/core/test_model.py | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 3dd7342944..d863aff038 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2295,7 +2295,7 @@ def create_python_model( else: depends_on_rendered = render_expression( expression=exp.Array( - expressions=[d.parse_one(dep, dialect=dialect) for dep in depends_on or []] + expressions=[exp.maybe_parse(dep, dialect=dialect) for dep in depends_on or []] ), module_path=module_path, macros=macros, diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 3686b35479..39110023a1 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -8845,6 +8845,54 @@ def entrypoint(context, *args, **kwargs): assert t.cast(pd.DataFrame, list(m.render(context=context))[0]).to_dict() == {"x": {0: 1}} +def test_python_model_depends_on_blueprints(tmp_path: Path) -> None: + sql_model = tmp_path / "models" / "base_blueprints.sql" + sql_model.parent.mkdir(parents=True, exist_ok=True) + sql_model.write_text( + """ + MODEL ( + name test_schema1.@{model_name}, + blueprints ((model_name := foo), (model_name := bar)), + kind FULL + ); + + SELECT 1 AS id + """ + ) + + py_model = tmp_path / "models" / "depends_on_with_blueprint_vars.py" + py_model.parent.mkdir(parents=True, exist_ok=True) + py_model.write_text( + """ +import pandas as pd +from sqlmesh import model + +@model( + "test_schema2.@model_name", + columns={ + "id": "int", + }, + blueprints=[ + {"model_name": "foo"}, + {"model_name": "bar"}, + ], + depends_on=["test_schema1.@{model_name}"], +) +def entrypoint(context, *args, **kwargs): + table = context.resolve_table(f"test_schema1.{context.blueprint_var('model_name')}") + return context.fetchdf(f"SELECT * FROM {table}")""" + ) + + ctx = Context( + config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")), + paths=tmp_path, + ) + assert len(ctx.models) == 4 + + ctx.plan(no_prompts=True, auto_apply=True) + assert ctx.fetchdf("SELECT * FROM test_schema2.foo").to_dict() == {"id": {0: 1}} + + @time_machine.travel("2020-01-01 00:00:00 UTC") def test_dynamic_date_spine_model(assert_exp_eq): @macro() From 128c7454fb32fbb5c70fae913d89b64ba47df03e Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 7 May 2025 18:33:46 +0300 Subject: [PATCH 0108/1056] Chore: detect invalid audit refs and raise early (#4323) --- sqlmesh/core/model/definition.py | 8 ++++++++ tests/core/test_model.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index d863aff038..a49e074b6b 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2478,6 +2478,14 @@ def _create_model( model.audit_definitions.update(audit_definitions) + from sqlmesh.core.audit.builtin import BUILT_IN_AUDITS + + # Ensure that all audits referenced in the model are defined + available_audits = BUILT_IN_AUDITS.keys() | model.audit_definitions.keys() + for referenced_audit, *_ in model.audits: + if referenced_audit not in available_audits: + raise_config_error(f"Audit '{referenced_audit}' is undefined", location=path) + # Any macro referenced in audits or signals needs to be treated as metadata-only statements.extend((audit.query, True) for audit in audit_definitions.values()) for _, audit_args in model.audits: diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 39110023a1..ee8dc04314 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -1342,7 +1342,18 @@ def test_audits(): """ ) - model = load_sql_based_model(expressions, path=Path("./examples/sushi/models/test_model.sql")) + audit_definitions = { + audit_name: load_audit( + d.parse(f"AUDIT (name {audit_name}); SELECT 1 WHERE FALSE"), dialect="duckdb" + ) + for audit_name in ("audit_a", "audit_b", "audit_c") + } + + model = load_sql_based_model( + expressions, + path=Path("./examples/sushi/models/test_model.sql"), + audit_definitions=audit_definitions, + ) assert model.audits == [ ("audit_a", {}), ("audit_b", {"key": exp.Literal.string("value")}), @@ -9635,3 +9646,19 @@ def test_semicolon_is_not_included_in_model_state(tmp_path, assert_exp_eq): plan = ctx.plan(no_prompts=True, auto_apply=True) assert not plan.context_diff.modified_snapshots + + +def test_invalid_audit_reference(): + sql = """ + MODEL ( + name test, + audits (not_nulll (columns := (id))) + ); + + SELECT + 1 AS id + """ + expressions = d.parse(sql) + + with pytest.raises(ConfigError, match="Audit 'not_nulll' is undefined"): + load_sql_based_model(expressions) From 1780e5ef612a13aa62fe8ad6bb25da7a0975701b Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 7 May 2025 18:01:29 +0100 Subject: [PATCH 0109/1056] refactor(vscode): move result into shared bus (#4327) --- package-lock.json | 5 ++++- package.json | 1 + vscode/bus/.gitignore | 2 ++ vscode/bus/package.json | 19 +++++++++++++++++++ .../functional => bus/src}/result.ts | 0 vscode/bus/tsconfig.json | 17 +++++++++++++++++ vscode/extension/src/auth/auth.ts | 2 +- vscode/extension/src/commands/format.ts | 2 +- vscode/extension/src/completion/completion.ts | 2 +- vscode/extension/src/extension.ts | 2 +- vscode/extension/src/lsp/lsp.ts | 2 +- vscode/extension/src/utilities/python.ts | 2 +- .../src/utilities/sqlmesh/sqlmesh.ts | 2 +- vscode/extension/tsconfig.json | 4 ++-- 14 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 vscode/bus/.gitignore create mode 100644 vscode/bus/package.json rename vscode/{extension/src/utilities/functional => bus/src}/result.ts (100%) create mode 100644 vscode/bus/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 9508be421b..1907f7b27d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -696,7 +697,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { diff --git a/package.json b/package.json index e5d1971f1a..f35c443814 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "web/client" ], "scripts": { + "ci": "npm run ci --workspaces", "fmt": "prettier --write .", "fmt:check": "prettier --check .", "lint": "npm run fmt:check && npm run lint --workspaces", diff --git a/vscode/bus/.gitignore b/vscode/bus/.gitignore new file mode 100644 index 0000000000..de4d1f007d --- /dev/null +++ b/vscode/bus/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/vscode/bus/package.json b/vscode/bus/package.json new file mode 100644 index 0000000000..93e3b1bf89 --- /dev/null +++ b/vscode/bus/package.json @@ -0,0 +1,19 @@ +{ + "name": "sqlmesh-extension-bus", + "private": true, + "version": "0.0.1", + "scripts": { + "ci": "npm run lint", + "build": "tsc", + "dev": "tsc -w", + "lint": "tsc --noEmit" + }, + "files": [ + "/dist" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "devDependencies": { + "typescript": "^5.5.4" + } +} diff --git a/vscode/extension/src/utilities/functional/result.ts b/vscode/bus/src/result.ts similarity index 100% rename from vscode/extension/src/utilities/functional/result.ts rename to vscode/bus/src/result.ts diff --git a/vscode/bus/tsconfig.json b/vscode/bus/tsconfig.json new file mode 100644 index 0000000000..8766fa1941 --- /dev/null +++ b/vscode/bus/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "declaration": true, + "outDir": "./dist", + "strict": true, + "strictNullChecks": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noEmit": true, + "moduleResolution": "node", + "baseUrl": "./" + }, + "include": ["src/**/*"] +} diff --git a/vscode/extension/src/auth/auth.ts b/vscode/extension/src/auth/auth.ts index 7da8e599b4..2b467880d1 100644 --- a/vscode/extension/src/auth/auth.ts +++ b/vscode/extension/src/auth/auth.ts @@ -9,7 +9,7 @@ import { window, } from 'vscode' import { get_tcloud_bin } from '../utilities/sqlmesh/sqlmesh' -import { err, isErr, ok, Result } from '../utilities/functional/result' +import { err, isErr, ok, Result } from '@bus/result' import { execAsync } from '../utilities/exec' import { getProjectRoot } from '../utilities/common/utilities' import z from 'zod' diff --git a/vscode/extension/src/commands/format.ts b/vscode/extension/src/commands/format.ts index 59d4c22177..623fb6c67b 100644 --- a/vscode/extension/src/commands/format.ts +++ b/vscode/extension/src/commands/format.ts @@ -1,7 +1,7 @@ import { traceLog } from '../utilities/common/log' import { execSync } from 'child_process' import { sqlmesh_exec } from '../utilities/sqlmesh/sqlmesh' -import { err, isErr, ok, Result } from '../utilities/functional/result' +import { err, isErr, ok, Result } from '@bus/result' import * as vscode from 'vscode' import { ErrorType, diff --git a/vscode/extension/src/completion/completion.ts b/vscode/extension/src/completion/completion.ts index 651b61854a..8e8a101c50 100644 --- a/vscode/extension/src/completion/completion.ts +++ b/vscode/extension/src/completion/completion.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode' import { LSPClient } from '../lsp/lsp' -import { isErr } from '../utilities/functional/result' +import { isErr } from '@bus/result' export const selector: vscode.DocumentSelector = { pattern: '**/*.sql', diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index e322fb161d..f16286eb9f 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -12,7 +12,7 @@ import { AuthenticationProviderTobikoCloud } from './auth/auth' import { signOut } from './commands/signout' import { signIn } from './commands/signin' import { signInSpecifyFlow } from './commands/signinSpecifyFlow' -import { isErr } from './utilities/functional/result' +import { isErr } from '@bus/result' import { handleNotSginedInError, handleSqlmeshLspNotFoundError, diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index c2bbc06504..5755122f36 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -6,7 +6,7 @@ import { TransportKind, } from 'vscode-languageclient/node' import { sqlmesh_lsp_exec } from '../utilities/sqlmesh/sqlmesh' -import { err, isErr, ok, Result } from '../utilities/functional/result' +import { err, isErr, ok, Result } from '@bus/result' import { getWorkspaceFolders } from '../utilities/common/vscodeapi' import { traceError } from '../utilities/common/log' import { ErrorType } from '../utilities/errors' diff --git a/vscode/extension/src/utilities/python.ts b/vscode/extension/src/utilities/python.ts index 610a1205fe..c056a6c89b 100644 --- a/vscode/extension/src/utilities/python.ts +++ b/vscode/extension/src/utilities/python.ts @@ -1,5 +1,5 @@ import { getInterpreterDetails } from './common/python' -import { err, ok, Result } from './functional/result' +import { err, ok, Result } from '@bus/result' import { traceInfo } from './common/log' import { promisify } from 'util' import { execFile } from 'child_process' diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 05bb66a00b..a6cda4a941 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -1,7 +1,7 @@ import path from 'path' import { traceInfo, traceLog, traceVerbose } from '../common/log' import { getInterpreterDetails } from '../common/python' -import { Result, err, isErr, ok } from '../functional/result' +import { Result, err, isErr, ok } from '@bus/result' import { getProjectRoot } from '../common/utilities' import { isPythonModuleInstalled } from '../python' import fs from 'fs' diff --git a/vscode/extension/tsconfig.json b/vscode/extension/tsconfig.json index 2f8323e699..2d06f6f342 100644 --- a/vscode/extension/tsconfig.json +++ b/vscode/extension/tsconfig.json @@ -4,17 +4,17 @@ "target": "ES2022", "lib": ["ES2022", "DOM"], "sourceMap": true, - "rootDir": "src", "strict": true /* enable all strict type-checking options */, /* Additional Checks */ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noUnusedParameters": true, "noUnusedLocals": true, + "types": ["mocha"], "paths": { "@bus/*": ["../bus/src/*"] } }, "include": ["src/**/*", "../bus/src/**/*"], - "exclude": ["node_modules", "../node_modules"] + "exclude": ["node_modules", "../node_modules", "../../node_modules"] } From 47724462b16b4256d33400b78c595bee6c716d95 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 7 May 2025 11:10:46 -0700 Subject: [PATCH 0110/1056] Fix: Correctly categorize an orphaned indirectly modified snapshot created as a result of a merge of 2 or more directly modified parents (#4329) --- sqlmesh/core/plan/builder.py | 80 +++++++++++++++++++++++- tests/core/test_integration.py | 108 +++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index f042116879..b70b9d1e3f 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -592,19 +592,97 @@ def _categorize_snapshot( if snapshot.is_model and snapshot.model.forward_only: snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) - elif not direct_parent_categories or direct_parent_categories.intersection( + elif direct_parent_categories.intersection( {SnapshotChangeCategory.BREAKING, SnapshotChangeCategory.INDIRECT_BREAKING} ): snapshot.categorize_as(SnapshotChangeCategory.INDIRECT_BREAKING) + elif not direct_parent_categories: + snapshot.categorize_as(self._get_orphaned_indirect_change_category(snapshot)) elif SnapshotChangeCategory.FORWARD_ONLY in all_upstream_categories: # FORWARD_ONLY must take precedence over INDIRECT_NON_BREAKING snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + elif all_upstream_categories == {SnapshotChangeCategory.METADATA}: + snapshot.categorize_as(SnapshotChangeCategory.METADATA) else: snapshot.categorize_as(SnapshotChangeCategory.INDIRECT_NON_BREAKING) else: # Metadata updated. snapshot.categorize_as(SnapshotChangeCategory.METADATA) + def _get_orphaned_indirect_change_category( + self, indirect_snapshot: Snapshot + ) -> SnapshotChangeCategory: + """Sometimes an indirectly changed downstream snapshot ends up with no directly changed parents introduced in the same plan. + This may happen when 2 or more parent models were changed independently in different plans and then the changes were + merged together and applied in a single plan. As a result, a combination of 2 or more previously changed parents produces + a new downstream snapshot not previously seen. + + This function is used to infer the correct change category for such downstream snapshots based on change categories of their parents. + """ + previous_snapshot = self._context_diff.modified_snapshots[indirect_snapshot.name][1] + previous_parent_snapshot_ids = {p.name: p for p in previous_snapshot.parents} + + current_parent_snapshots = [ + self._context_diff.snapshots[p_id] + for p_id in indirect_snapshot.parents + if p_id in self._context_diff.snapshots + ] + + indirect_category: t.Optional[SnapshotChangeCategory] = None + for current_parent_snapshot in current_parent_snapshots: + if current_parent_snapshot.name not in previous_parent_snapshot_ids: + # This is a new parent so falling back to INDIRECT_BREAKING + return SnapshotChangeCategory.INDIRECT_BREAKING + pevious_parent_snapshot_id = previous_parent_snapshot_ids[current_parent_snapshot.name] + + if current_parent_snapshot.snapshot_id == pevious_parent_snapshot_id: + # There were no new versions of this parent since the previous version of this snapshot, + # so we can skip it + continue + + # Find the previous snapshot ID of the same parent in the historical chain + previous_parent_found = False + previous_parent_categories = set() + for pv in reversed(current_parent_snapshot.all_versions): + pv_snapshot_id = pv.snapshot_id(current_parent_snapshot.name) + if pv_snapshot_id == pevious_parent_snapshot_id: + previous_parent_found = True + break + previous_parent_categories.add(pv.change_category) + + if not previous_parent_found: + # The previous parent is not in the historical chain so falling back to INDIRECT_BREAKING + return SnapshotChangeCategory.INDIRECT_BREAKING + + if previous_parent_categories.intersection( + {SnapshotChangeCategory.BREAKING, SnapshotChangeCategory.INDIRECT_BREAKING} + ): + # One of the new parents in the chain was breaking so this indirect snapshot is breaking + return SnapshotChangeCategory.INDIRECT_BREAKING + + if SnapshotChangeCategory.FORWARD_ONLY in previous_parent_categories: + # One of the new parents in the chain was forward-only so this indirect snapshot is forward-only + indirect_category = SnapshotChangeCategory.FORWARD_ONLY + elif ( + previous_parent_categories.intersection( + { + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.INDIRECT_NON_BREAKING, + } + ) + and indirect_category != SnapshotChangeCategory.FORWARD_ONLY + ): + # All changes in the chain were non-breaking so this indirect snapshot can be non-breaking too + indirect_category = SnapshotChangeCategory.INDIRECT_NON_BREAKING + elif ( + previous_parent_categories == {SnapshotChangeCategory.METADATA} + and indirect_category is None + ): + # All changes in the chain were metadata so this indirect snapshot can be metadata too + indirect_category = SnapshotChangeCategory.METADATA + + return indirect_category or SnapshotChangeCategory.INDIRECT_BREAKING + def _apply_effective_from(self) -> None: if self._effective_from: if not self._forward_only: diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 2fca1681a6..c9578ad48f 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -2247,6 +2247,114 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot_migration( assert count > 0 +@time_machine.travel("2023-01-08 15:00:00 UTC") +@pytest.mark.parametrize( + "parent_a_category,parent_b_category,expected_child_category", + [ + ( + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.INDIRECT_BREAKING, + ), + ( + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.INDIRECT_NON_BREAKING, + ), + ( + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.INDIRECT_NON_BREAKING, + ), + ( + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.INDIRECT_BREAKING, + ), + ( + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.METADATA, + ), + ( + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.METADATA, + ), + ( + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.INDIRECT_BREAKING, + ), + ( + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.INDIRECT_NON_BREAKING, + ), + ( + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.METADATA, + ), + ( + SnapshotChangeCategory.FORWARD_ONLY, + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.INDIRECT_BREAKING, + ), + ( + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.FORWARD_ONLY, + SnapshotChangeCategory.FORWARD_ONLY, + ), + ( + SnapshotChangeCategory.FORWARD_ONLY, + SnapshotChangeCategory.FORWARD_ONLY, + SnapshotChangeCategory.FORWARD_ONLY, + ), + ], +) +def test_rebase_two_changed_parents( + init_and_plan_context: t.Callable, + parent_a_category: SnapshotChangeCategory, # This change is deployed to prod first + parent_b_category: SnapshotChangeCategory, # This change is deployed to prod second + expected_child_category: SnapshotChangeCategory, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + initial_model_a = context.get_model("sushi.orders") + initial_model_b = context.get_model("sushi.items") + + # Make change A and deploy it to dev_a + context.upsert_model(initial_model_a.name, stamp="1") + plan_builder = context.plan_builder("dev_a", skip_tests=True) + plan_builder.set_choice(context.get_snapshot(initial_model_a.name), parent_a_category) + context.apply(plan_builder.build()) + + # Make change B and deploy it to dev_b + context.upsert_model(initial_model_a) + context.upsert_model(initial_model_b.name, stamp="1") + plan_builder = context.plan_builder("dev_b", skip_tests=True) + plan_builder.set_choice(context.get_snapshot(initial_model_b.name), parent_b_category) + context.apply(plan_builder.build()) + + # Deploy change A to prod + context.upsert_model(initial_model_a.name, stamp="1") + context.upsert_model(initial_model_b) + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + + # Apply change B in addition to A and plan against prod + context.upsert_model(initial_model_b.name, stamp="1") + plan = context.plan_builder("prod", skip_tests=True).build() + + # Validate the category of child snapshots + direct_child_snapshot = plan.snapshots[context.get_snapshot("sushi.order_items").snapshot_id] + assert direct_child_snapshot.change_category == expected_child_category + + indirect_child_snapshot = plan.snapshots[context.get_snapshot("sushi.top_waiters").snapshot_id] + assert indirect_child_snapshot.change_category == expected_child_category + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_unaligned_start_snapshot_with_non_deployable_downstream(init_and_plan_context: t.Callable): context, _ = init_and_plan_context("examples/sushi") From 767755e4afc9313e744e79c80b040df0a76e89a5 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 7 May 2025 23:48:07 +0300 Subject: [PATCH 0111/1056] Fix: Pop select model in tablediff to ensure backwards compatibility (#4325) --- sqlmesh/cli/main.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index a06f9719a2..c230d78896 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -17,7 +17,7 @@ from sqlmesh.core.config import load_configs from sqlmesh.core.context import Context from sqlmesh.utils.date import TimeLike -from sqlmesh.utils.errors import MissingDependencyError +from sqlmesh.utils.errors import MissingDependencyError, SQLMeshError from pathlib import Path logger = logging.getLogger(__name__) @@ -907,7 +907,14 @@ def table_diff( ) -> None: """Show the diff between two tables or a selection of models when they are specified.""" source, target = source_to_target.split(":") - select_models = {model} if model else kwargs.pop("select_model", None) + select_model = kwargs.pop("select_model", None) + + if model and select_model: + raise SQLMeshError( + "The --select-model option cannot be used together with a model argument. Please choose one of them." + ) + + select_models = {model} if model else select_model obj.table_diff( source=source, target=target, From 983aff7b79ae5221acb036e703cf255c601dc509 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 7 May 2025 13:50:18 -0700 Subject: [PATCH 0112/1056] Fix: Support dialects with normalization strategies for dbt projects (#4330) --- sqlmesh/dbt/builtin.py | 5 ++++- tests/core/test_integration.py | 8 ++++++++ tests/core/test_model.py | 8 ++++---- tests/fixtures/dbt/sushi_test/config.py | 6 ++++++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 075d973c75..b734034b2f 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -12,6 +12,7 @@ from dbt import version from dbt.adapters.base import BaseRelation, Column from ruamel.yaml import YAMLError +from sqlglot import Dialect from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.snapshot.definition import DeployabilityIndex @@ -49,7 +50,9 @@ def warn(self, msg: str) -> str: class Api: def __init__(self, dialect: t.Optional[str]) -> None: if dialect: - config_class = TARGET_TYPE_TO_CONFIG_CLASS[dialect] + config_class = TARGET_TYPE_TO_CONFIG_CLASS[ + Dialect.get_or_raise(dialect).__class__.__name__.lower() + ] self.Relation = config_class.relation_class self.Column = config_class.column_class self.quote_policy = config_class.quote_policy diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index c9578ad48f..88711495f2 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -4263,6 +4263,14 @@ def test_dbt_requirements(sushi_dbt_context: Context): assert sushi_dbt_context.requirements["dbt-duckdb"].startswith("1.") +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_dbt_dialect_with_normalization_strategy(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context( + "tests/fixtures/dbt/sushi_test", config="test_config_with_normalization_strategy" + ) + assert context.default_dialect == "duckdb,normalization_strategy=LOWERCASE" + + @pytest.mark.parametrize( "context_fixture", ["sushi_context", "sushi_dbt_context", "sushi_test_dbt_context", "sushi_no_default_catalog"], diff --git a/tests/core/test_model.py b/tests/core/test_model.py index ee8dc04314..20f08a259d 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -2221,7 +2221,7 @@ def my_model(context, **kwargs): m = model.get_registry()["my_model"].model( module_path=Path("."), path=Path("."), - dialect="duckdb", + dialect="duckdb,normalization_strategy=LOWERCASE", ) assert list(m.pre_statements) == [ @@ -2231,14 +2231,14 @@ def my_model(context, **kwargs): d.parse_one("DROP TABLE x"), ] assert m.enabled - assert m.dialect == "duckdb" + assert m.dialect == "duckdb,normalization_strategy=lowercase" assert m.depends_on == {'"foo"', '"bar"."baz"'} - assert m.columns_to_types == {"col": exp.DataType.build("int")} + assert m.columns_to_types == {"COL": exp.DataType.build("int")} assert_exp_eq( m.ctas_query(), """ SELECT - CAST(NULL AS INT) AS "col" + CAST(NULL AS INT) AS "COL" FROM (VALUES (1)) AS t(dummy) WHERE diff --git a/tests/fixtures/dbt/sushi_test/config.py b/tests/fixtures/dbt/sushi_test/config.py index 422f53553b..d82291f793 100644 --- a/tests/fixtures/dbt/sushi_test/config.py +++ b/tests/fixtures/dbt/sushi_test/config.py @@ -12,3 +12,9 @@ test_config = config + +test_config_with_normalization_strategy = sqlmesh_config( + Path(__file__).parent, + variables=variables, + model_defaults=ModelDefaultsConfig(dialect="duckdb,normalization_strategy=LOWERCASE"), +) From 894bc7879c0ed4a9e460d06f1a4a8326c0f5c938 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Wed, 7 May 2025 16:57:08 -0500 Subject: [PATCH 0113/1056] Fix: use MarkdownConsole in non-interactive contexts (#4306) --- docs/reference/configuration.md | 34 ++- sqlmesh/__init__.py | 33 ++- sqlmesh/core/console.py | 107 +++++++-- tests/cli/test_cli.py | 8 +- .../github/cicd/test_github_commands.py | 12 +- .../github/cicd/test_integration.py | 25 +- tests/integrations/jupyter/test_magics.py | 214 +++++++++++++++++- 7 files changed, 379 insertions(+), 54 deletions(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 6b8c6383d6..34dcd9d27b 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -248,7 +248,10 @@ For example, you might have a specific connection where your tests should run re ## Debug mode -To enable debug mode set the `SQLMESH_DEBUG` environment variable to one of the following values: "1", "true", "t", "yes" or "y". +Enable debug mode in one of two ways: + +- Pass the `--debug` flag between the CLI command and the subcommand. For example, `sqlmesh --debug plan`. +- Set the `SQLMESH_DEBUG` environment variable to one of the following values: "1", "true", "t", "yes" or "y". Enabling this mode ensures that full backtraces are printed when using CLI. The default log level is set to `DEBUG` when this mode is enabled. @@ -256,12 +259,20 @@ Example enabling debug mode for the CLI command `sqlmesh plan`: === "Bash" + ```bash + $ sqlmesh --debug plan + ``` + ```bash $ SQLMESH_DEBUG=1 sqlmesh plan ``` === "MS Powershell" + ```powershell + PS> sqlmesh --debug plan + ``` + ```powershell PS> $env:SQLMESH_DEBUG=1 PS> sqlmesh plan @@ -269,11 +280,32 @@ Example enabling debug mode for the CLI command `sqlmesh plan`: === "MS CMD" + ```cmd + C:\> sqlmesh --debug plan + ``` + ```cmd C:\> set SQLMESH_DEBUG=1 C:\> sqlmesh plan ``` +## Runtime Environment + +SQLMesh can run in different runtime environments. For example, you might run it in a regular command-line terminal, in a Jupyter notebook, or in Github's CI/CD platform. + +When it starts up, SQLMesh automatically detects the runtime environment and adjusts its behavior accordingly. For example, it registers `%magic` commands if in a Jupyter notebook and adjusts logging behavior if in a CI/CD environment. + +If necessary, you may force SQLMesh to use a specific runtime environment by setting the `SQLMESH_RUNTIME_ENVIRONMENT` environment variable. + +It accepts the following values, which will cause SQLMesh to behave as if it were in the runtime environment in parentheses: + +- `terminal` (CLI console) +- `databricks` (Databricks notebook) +- `google_colab` (Google Colab notebook) +- `jupyter` (Jupyter notebook) +- `debugger` (Debugging output) +- `ci` (CI/CD or other non-interactive environment) + ## Anonymized usage information We strive to make SQLMesh the best data transformation tool on the market. Part of accomplishing that is continually fixing bugs, adding features, and improving SQLMesh's performance. diff --git a/sqlmesh/__init__.py b/sqlmesh/__init__.py index 5c1cec69b0..a033b652d7 100644 --- a/sqlmesh/__init__.py +++ b/sqlmesh/__init__.py @@ -33,6 +33,7 @@ from sqlmesh.utils import ( debug_mode_enabled as debug_mode_enabled, enable_debug_mode as enable_debug_mode, + str_to_bool, ) from sqlmesh.utils.date import DatetimeRanges as DatetimeRanges @@ -54,6 +55,7 @@ class RuntimeEnv(str, Enum): GOOGLE_COLAB = "google_colab" # Not currently officially supported JUPYTER = "jupyter" DEBUGGER = "debugger" + CI = "ci" # CI or other envs that shouldn't use emojis @classmethod def get(cls) -> RuntimeEnv: @@ -62,6 +64,16 @@ def get(cls) -> RuntimeEnv: Unlike the rich implementation we try to split out by notebook type instead of treating it all as Jupyter. """ + runtime_env_var = os.getenv("SQLMESH_RUNTIME_ENVIRONMENT") + if runtime_env_var: + try: + return RuntimeEnv(runtime_env_var) + except ValueError: + valid_values = [f'"{member.value}"' for member in RuntimeEnv] + raise ValueError( + f"Invalid SQLMESH_RUNTIME_ENVIRONMENT value: {runtime_env_var}. Must be one of {', '.join(valid_values)}." + ) + try: shell = get_ipython() # type: ignore if os.getenv("DATABRICKS_RUNTIME_VERSION"): @@ -75,6 +87,10 @@ def get(cls) -> RuntimeEnv: if debug_mode_enabled(): return RuntimeEnv.DEBUGGER + + if is_cicd_environment() or not is_interactive_environment(): + return RuntimeEnv.CI + return RuntimeEnv.TERMINAL @property @@ -93,9 +109,24 @@ def is_jupyter(self) -> bool: def is_google_colab(self) -> bool: return self == RuntimeEnv.GOOGLE_COLAB + @property + def is_ci(self) -> bool: + return self == RuntimeEnv.CI + @property def is_notebook(self) -> bool: - return not self.is_terminal + return not self.is_terminal and not self.is_ci + + +def is_cicd_environment() -> bool: + for key in ("CI", "GITHUB_ACTIONS", "TRAVIS", "CIRCLECI", "GITLAB_CI", "BUILDKITE"): + if str_to_bool(os.environ.get(key, "false")): + return True + return False + + +def is_interactive_environment() -> bool: + return sys.stdin.isatty() and sys.stdout.isatty() if RuntimeEnv.get().is_notebook: diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 027760d10f..167561a671 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -78,9 +78,6 @@ PROGRESS_BAR_WIDTH = 40 LINE_WRAP_WIDTH = 100 -CHECK_MARK = "\u2714" -GREEN_CHECK_MARK = f"[green]{CHECK_MARK}[/green]" -RED_X_MARK = "\u274c" class LinterConsole(abc.ABC): @@ -770,6 +767,11 @@ class TerminalConsole(Console): TABLE_DIFF_SOURCE_BLUE = "#0248ff" TABLE_DIFF_TARGET_GREEN = "green" + AUDIT_PASS_MARK = "\u2714" + GREEN_AUDIT_PASS_MARK = f"[green]{AUDIT_PASS_MARK}[/green]" + AUDIT_FAIL_MARK = "\u274c" + AUDIT_PADDING = 0 + CHECK_MARK = f"{AUDIT_PASS_MARK} " def __init__( self, @@ -879,7 +881,9 @@ def start_evaluation_progress( progress_table.add_row(self.evaluation_total_progress) progress_table.add_row(self.evaluation_model_progress) - self.evaluation_progress_live = Live(progress_table, refresh_per_second=10) + self.evaluation_progress_live = Live( + progress_table, console=self.console, refresh_per_second=10 + ) self.evaluation_progress_live.start() batch_sizes = { @@ -891,7 +895,7 @@ def start_evaluation_progress( # determine column widths self.evaluation_column_widths["annotation"] = ( - _calculate_annotation_str_len(batched_intervals) + _calculate_annotation_str_len(batched_intervals, self.AUDIT_PADDING) + 3 # brackets and opening escape backslash ) self.evaluation_column_widths["name"] = max( @@ -956,13 +960,16 @@ def update_snapshot_evaluation_progress( ) audits_str = "" if num_audits_passed: - audits_str += f" {CHECK_MARK}{num_audits_passed}" + audits_str += f" {self.AUDIT_PASS_MARK}{num_audits_passed}" if num_audits_failed: - audits_str += f" {RED_X_MARK}{num_audits_failed}" + audits_str += f" {self.AUDIT_FAIL_MARK}{num_audits_failed}" audits_str = f", audits{audits_str}" if audits_str else "" annotation_len = self.evaluation_column_widths["annotation"] + # don't adjust the annotation_len if we're using AUDIT_PADDING annotation = f"\\[{annotation + audits_str}]".ljust( - annotation_len - 1 if num_audits_failed else annotation_len + annotation_len - 1 + if num_audits_failed and self.AUDIT_PADDING == 0 + else annotation_len ) duration = f"{(duration_ms / 1000.0):.2f}s".ljust( @@ -970,7 +977,7 @@ def update_snapshot_evaluation_progress( ) msg = f"{batch} {display_name} {annotation} {duration}".replace( - CHECK_MARK, GREEN_CHECK_MARK + self.AUDIT_PASS_MARK, self.GREEN_AUDIT_PASS_MARK ) self.evaluation_progress_live.console.print(msg) @@ -989,7 +996,7 @@ def stop_evaluation_progress(self, success: bool = True) -> None: if self.evaluation_progress_live: self.evaluation_progress_live.stop() if success: - self.log_success(f"{GREEN_CHECK_MARK} Model batches executed") + self.log_success(f"{self.CHECK_MARK}Model batches executed") self.evaluation_progress_live = None self.evaluation_total_progress = None @@ -1053,7 +1060,7 @@ def stop_creation_progress(self, success: bool = True) -> None: self.creation_progress.stop() self.creation_progress = None if success: - self.log_success(f"\n{GREEN_CHECK_MARK} Physical layer updated") + self.log_success(f"\n{self.CHECK_MARK}Physical layer updated") self.environment_naming_info = EnvironmentNamingInfo() self.default_catalog = None @@ -1154,7 +1161,7 @@ def stop_promotion_progress(self, success: bool = True) -> None: self.promotion_progress.stop() self.promotion_progress = None if success: - self.log_success(f"\n{GREEN_CHECK_MARK} Virtual layer updated") + self.log_success(f"\n{self.CHECK_MARK}Virtual layer updated") self.environment_naming_info = EnvironmentNamingInfo() self.default_catalog = None @@ -2807,6 +2814,12 @@ class MarkdownConsole(CaptureTerminalConsole): where you want to display a plan or test results in markdown. """ + CHECK_MARK = "" + AUDIT_PASS_MARK = "passed " + GREEN_AUDIT_PASS_MARK = AUDIT_PASS_MARK + AUDIT_FAIL_MARK = "failed " + AUDIT_PADDING = 7 + def __init__(self, **kwargs: t.Any) -> None: super().__init__(**{**kwargs, "console": RichConsole(no_color=True)}) @@ -2822,23 +2835,28 @@ def show_environment_difference_summary( no_diff: Hide the actual environment statements differences. """ if context_diff.is_new_environment: - self._print( - f"**New environment `{context_diff.environment}` will be created from `{context_diff.create_from}`**\n" + msg = ( + f"\n**`{context_diff.environment}` environment will be initialized**" + if not context_diff.create_from_env_exists + else f"\n**New environment `{context_diff.environment}` will be created from `{context_diff.create_from}`**" ) + self._print(msg) if not context_diff.has_snapshot_changes: return if not context_diff.has_changes: - self._print(f"**No differences when compared to `{context_diff.environment}`**\n") + self._print( + f"\n**No changes to plan: project files match the `{context_diff.environment}` environment**\n" + ) return - self._print(f"**Summary of differences against `{context_diff.environment}`:**\n") + self._print(f"\n**Summary of differences from `{context_diff.environment}`:**") if context_diff.has_requirement_changes: - self._print(f"Requirements:\n{context_diff.requirements_diff()}") + self._print(f"\nRequirements:\n{context_diff.requirements_diff()}") if context_diff.has_environment_statements_changes and not no_diff: - self._print("[bold]Environment statements:\n") + self._print("\nEnvironment statements:\n") for _, diff in context_diff.environment_statements_diff( include_python_env=not context_diff.is_new_environment ): @@ -2984,7 +3002,7 @@ def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> N dialect=self.dialect, ) snapshots.append( - f"* `{display_name}`: [{_format_missing_intervals(snapshot, missing)}]{preview_modifier}" + f"* `{display_name}`: \\[{_format_missing_intervals(snapshot, missing)}]{preview_modifier}" ) length = len(snapshots) @@ -3033,6 +3051,21 @@ def _show_categorized_snapshots(self, plan: Plan, default_catalog: t.Optional[st self._print(tree) self._print("\n```") + def stop_evaluation_progress(self, success: bool = True) -> None: + super().stop_evaluation_progress(success) + self._print("\n") + + def stop_creation_progress(self, success: bool = True) -> None: + super().stop_creation_progress(success) + self._print("\n") + + def stop_promotion_progress(self, success: bool = True) -> None: + super().stop_promotion_progress(success) + self._print("\n") + + def log_success(self, message: str) -> None: + self._print(message) + def log_test_results( self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str ) -> None: @@ -3082,6 +3115,12 @@ def log_warning(self, short_message: str, long_message: t.Optional[str] = None) logger.warning(long_message or short_message) self._print(f"```\n\\[WARNING] {short_message}```\n\n") + def _print(self, value: t.Any, **kwargs: t.Any) -> None: + self.console.print(value, **kwargs) + with self.console.capture() as capture: + self.console.print(value, **kwargs) + self._captured_outputs.append(capture.get()) + class DatabricksMagicConsole(CaptureTerminalConsole): """ @@ -3473,6 +3512,7 @@ def create_console( RuntimeEnv.TERMINAL: TerminalConsole, RuntimeEnv.GOOGLE_COLAB: NotebookMagicConsole, RuntimeEnv.DEBUGGER: DebuggerTerminalConsole, + RuntimeEnv.CI: MarkdownConsole, } rich_console_kwargs: t.Dict[str, t.Any] = {"theme": srich.theme} if runtime_env.is_jupyter or runtime_env.is_google_colab: @@ -3598,7 +3638,7 @@ def _calculate_interval_str_len(snapshot: Snapshot, intervals: t.List[Interval]) return interval_str_len -def _calculate_audit_str_len(snapshot: Snapshot) -> int: +def _calculate_audit_str_len(snapshot: Snapshot, audit_padding: int = 0) -> int: # The annotation includes audit results. We cannot build the audits result string # until after evaluation occurs, but we must determine the annotation column width here. # Therefore, we add enough padding for the longest possible audits result string. @@ -3619,21 +3659,38 @@ def _calculate_audit_str_len(snapshot: Snapshot) -> int: ) if num_audits == 1: # +1 for "1" audit count, +1 for red X - audit_len = audit_base_str_len + (2 if num_nonblocking_audits else 1) + # if audit_padding is > 0 we're using "failed" instead of red X + audit_len = ( + audit_base_str_len + + (2 if num_nonblocking_audits else 1) + + ( + audit_padding - 1 + if num_nonblocking_audits and audit_padding > 0 + else audit_padding + ) + ) else: - audit_len = audit_base_str_len + len(str(num_audits)) + audit_len = audit_base_str_len + len(str(num_audits)) + audit_padding if num_nonblocking_audits: # +1 for space, +1 for red X - audit_len += len(str(num_nonblocking_audits)) + 2 + # if audit_padding is > 0 we're using "failed" instead of red X + audit_len += ( + len(str(num_nonblocking_audits)) + + 2 + + (audit_padding - 1 if audit_padding > 0 else audit_padding) + ) audit_str_len = max(audit_str_len, audit_len) return audit_str_len -def _calculate_annotation_str_len(batched_intervals: t.Dict[Snapshot, t.List[Interval]]) -> int: +def _calculate_annotation_str_len( + batched_intervals: t.Dict[Snapshot, t.List[Interval]], audit_padding: int = 0 +) -> int: annotation_str_len = 0 for snapshot, intervals in batched_intervals.items(): annotation_str_len = max( annotation_str_len, - _calculate_interval_str_len(snapshot, intervals) + _calculate_audit_str_len(snapshot), + _calculate_interval_str_len(snapshot, intervals) + + _calculate_audit_str_len(snapshot, audit_padding), ) return annotation_str_len diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index cf911c5f35..eaaa04017c 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -8,7 +8,8 @@ from click.testing import CliRunner import time_machine import json - +from unittest.mock import MagicMock +from sqlmesh import RuntimeEnv from sqlmesh.cli.example_project import ProjectTemplate, init_example_project from sqlmesh.cli.main import cli from sqlmesh.core.context import Context @@ -20,6 +21,11 @@ pytestmark = pytest.mark.slow +@pytest.fixture(autouse=True) +def mock_runtime_env(monkeypatch): + monkeypatch.setattr("sqlmesh.RuntimeEnv.get", MagicMock(return_value=RuntimeEnv.TERMINAL)) + + @pytest.fixture(scope="session") def runner() -> CliRunner: return CliRunner() diff --git a/tests/integrations/github/cicd/test_github_commands.py b/tests/integrations/github/cicd/test_github_commands.py index 8c305bc20e..0d2d303fb7 100644 --- a/tests/integrations/github/cicd/test_github_commands.py +++ b/tests/integrations/github/cicd/test_github_commands.py @@ -144,7 +144,8 @@ def test_run_all_success_with_approvers_approved(
:ship: Prod Plan Being Applied -**New environment `prod` will be created from `prod`**""" + +**`prod` environment will be initialized**""" ) with open(github_output_file, "r", encoding="utf-8") as f: output = f.read() @@ -271,7 +272,8 @@ def test_run_all_success_with_approvers_approved_merge_delete(
:ship: Prod Plan Being Applied -**New environment `prod` will be created from `prod`**""" + +**`prod` environment will be initialized**""" ) with open(github_output_file, "r", encoding="utf-8") as f: output = f.read() @@ -918,7 +920,8 @@ def raise_on_prod_plan(plan: Plan):
:ship: Prod Plan Being Applied -**New environment `prod` will be created from `prod`**""" + +**`prod` environment will be initialized**""" ) with open(github_output_file, "r", encoding="utf-8") as f: @@ -1210,7 +1213,8 @@ def test_comment_command_deploy_prod(
:ship: Prod Plan Being Applied -**New environment `prod` will be created from `prod`**""" + +**`prod` environment will be initialized**""" ) with open(github_output_file, "r", encoding="utf-8") as f: diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index 1fe37c6bee..6ab5330351 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -287,8 +287,7 @@ def test_merge_pr_has_non_breaking_change( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan_summary = """**Summary of differences against `prod`:** - + expected_prod_plan_summary = """\n**Summary of differences from `prod`:** **Directly Modified:** - `sushi.waiter_revenue_by_day` @@ -484,8 +483,7 @@ def test_merge_pr_has_non_breaking_change_diff_start( assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" - expected_prod_plan = """**Summary of differences against `prod`:** - + expected_prod_plan = """\n**Summary of differences from `prod`:** **Directly Modified:** - `sushi.waiter_revenue_by_day` @@ -827,7 +825,9 @@ def test_merge_pr_has_no_changes( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan_summary = "**No differences when compared to `prod`**\n\n\n" + expected_prod_plan_summary = ( + "\n**No changes to plan: project files match the `prod` environment**\n\n\n" + ) assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" assert prod_plan_preview_checks_runs[2]["output"]["summary"] == expected_prod_plan_summary @@ -990,8 +990,7 @@ def test_no_merge_since_no_deploy_signal( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan = """**Summary of differences against `prod`:** - + expected_prod_plan = """\n**Summary of differences from `prod`:** **Directly Modified:** - `sushi.waiter_revenue_by_day` @@ -1171,8 +1170,7 @@ def test_no_merge_since_no_deploy_signal_no_approvers_defined( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan = """**Summary of differences against `prod`:** - + expected_prod_plan = """\n**Summary of differences from `prod`:** **Directly Modified:** - `sushi.waiter_revenue_by_day` @@ -1341,8 +1339,7 @@ def test_deploy_comment_pre_categorized( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan = """**Summary of differences against `prod`:** - + expected_prod_plan = """\n**Summary of differences from `prod`:** **Directly Modified:** - `sushi.waiter_revenue_by_day` @@ -1680,8 +1677,7 @@ def test_overlapping_changes_models( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan_summary = """**Summary of differences against `prod`:** - + expected_prod_plan_summary = """\n**Summary of differences from `prod`:** **Directly Modified:** - `sushi.customers` @@ -1875,8 +1871,7 @@ def test_pr_delete_model( == """
PR Environment Summary
ModelChange TypeDates Loaded
"memory"."sushi"."top_waiters"BreakingREMOVED
""" ) - expected_prod_plan_summary = """**Summary of differences against `prod`:** - + expected_prod_plan_summary = """\n**Summary of differences from `prod`:** **Removed Models:** - `sushi.top_waiters` diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index 6bfc4b8df3..847d16648c 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -22,6 +22,9 @@ SUSHI_EXAMPLE_PATH = pathlib.Path("./examples/sushi") SUCCESS_STYLE = "color: #008000; text-decoration-color: #008000" NEUTRAL_STYLE = "color: #008080; text-decoration-color: #008080" +BOLD_ONLY = "font-weight: bold" +BOLD_NEUTRAL_STYLE = f"{NEUTRAL_STYLE}; {BOLD_ONLY}" +BOLD_SUCCESS_STYLE = f"{SUCCESS_STYLE}; {BOLD_ONLY}" RICH_PRE_STYLE = "white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace" FREEZE_TIME = "2023-01-01 00:00:00 UTC" @@ -289,9 +292,6 @@ def test_plan( with capture_output() as output: notebook.run_line_magic(magic_name="plan", line="--no-prompts --auto-apply") - # TODO: Should this be going to stdout? This is printing the status updates for when each batch finishes for - # the models and how long it took - assert len(output.stdout.strip().split("\n")) == 46 assert not output.stderr assert len(output.outputs) == 4 text_output = convert_all_html_output_to_text(output) @@ -321,15 +321,215 @@ def test_run_dag( notebook.run_line_magic(magic_name="run_dag", line="") assert not output.stdout.startswith( - "'Evaluating models ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 18/18" + "'Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 18/18" ) assert not output.stderr - assert len(output.outputs) == 2 - assert convert_all_html_output_to_text(output) == [ + assert len(output.outputs) == 6 + html_text_actual = convert_all_html_output_to_text(output) + html_text_expected = [ + "[2K", + "Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━ 93.8% • 15/16 • 0:00:", + "[1/1] sushi.waiter_as_customer_by_day [insert 2023-01-01 - 2023-01-02, audits ✔2]", + "", "✔ Model batches executed", "Run finished for environment 'prod'", ] - assert get_all_html_output(output) == [ + for txt_actual, txt_expected in zip(html_text_actual, html_text_expected): + assert txt_actual.startswith(txt_expected) + actual_html_output = get_all_html_output(output) + # Replace dynamic elapsed time with 00 + for i, chunk in enumerate(actual_html_output): + pattern = 'font\-weight: bold">0\.\\d{2}s ' + import re + + actual_html_output[i] = re.sub(pattern, 'font-weight: bold">0.00s ', chunk) + expected_html_output = [ + str( + h( + "pre", + {"style": RICH_PRE_STYLE}, + "\x1b", + h( + "span", + {"style": BOLD_ONLY}, + "[", + autoescape=False, + ), + "2K", + autoescape=False, + ) + ), + str( + h( + "pre", + {"style": RICH_PRE_STYLE}, + h( + "span", + {"style": "color: #000080; text-decoration-color: #000080; font-weight: bold"}, + "Executing model batches", + autoescape=False, + ), + " ", + h( + "span", + {"style": "color: #f92672; text-decoration-color: #f92672"}, + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸", + autoescape=False, + ), + h( + "span", + {"style": "color: #3a3a3a; text-decoration-color: #3a3a3a"}, + "━━", + autoescape=False, + ), + " ", + h( + "span", + {"style": "color: #800080; text-decoration-color: #800080"}, + "93.8%", + autoescape=False, + ), + " • ", + h( + "span", + {"style": SUCCESS_STYLE}, + "15/16", + autoescape=False, + ), + " • ", + h( + "span", + {"style": "color: #808000; text-decoration-color: #808000"}, + "0:00:00", + autoescape=False, + ), + "sushi.waiter_as_customer_by_day ", + h( + "span", + {"style": SUCCESS_STYLE}, + ".. ", + autoescape=False, + ), + " ", + autoescape=False, + ) + ), + str( + h( + "pre", + {"style": RICH_PRE_STYLE}, + h( + "span", + {"style": BOLD_ONLY}, + "[", + autoescape=False, + ), + h( + "span", + {"style": BOLD_NEUTRAL_STYLE}, + "1", + autoescape=False, + ), + "/", + h( + "span", + {"style": BOLD_NEUTRAL_STYLE}, + "1", + autoescape=False, + ), + h( + "span", + {"style": BOLD_ONLY}, + "]", + autoescape=False, + ), + " sushi.waiter_as_customer_by_day ", + h( + "span", + {"style": BOLD_ONLY}, + "[", + autoescape=False, + ), + "insert ", + h( + "span", + {"style": BOLD_NEUTRAL_STYLE}, + "2023", + autoescape=False, + ), + "-", + h( + "span", + {"style": BOLD_NEUTRAL_STYLE}, + "01", + autoescape=False, + ), + "-", + h( + "span", + {"style": BOLD_NEUTRAL_STYLE}, + "01", + autoescape=False, + ), + " - ", + h( + "span", + {"style": BOLD_NEUTRAL_STYLE}, + "2023", + autoescape=False, + ), + "-", + h( + "span", + {"style": BOLD_NEUTRAL_STYLE}, + "01", + autoescape=False, + ), + "-", + h( + "span", + {"style": BOLD_NEUTRAL_STYLE}, + "02", + autoescape=False, + ), + ", audits ", + h( + "span", + {"style": SUCCESS_STYLE}, + "✔", + autoescape=False, + ), + h( + "span", + {"style": BOLD_NEUTRAL_STYLE}, + "2", + autoescape=False, + ), + h( + "span", + {"style": BOLD_ONLY}, + "]", + autoescape=False, + ), + " ", + h( + "span", + {"style": BOLD_NEUTRAL_STYLE}, + "0.", + autoescape=False, + ), + "00s ", + autoescape=False, + ) + ), + str( + h( + "pre", + {"style": RICH_PRE_STYLE}, + "", + autoescape=False, + ) + ), str( h( "pre", From e51ca569e74e40d3d57e1651cd4c6f69430d24da Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 7 May 2025 15:30:56 -0700 Subject: [PATCH 0114/1056] chore: remove docs on deprecate custom measures (#4332) --- docs/cloud/features/alerts_notifications.md | 4 +- docs/guides/observer.md | 101 +------------------- 2 files changed, 2 insertions(+), 103 deletions(-) diff --git a/docs/cloud/features/alerts_notifications.md b/docs/cloud/features/alerts_notifications.md index bc47a353bb..98fe65a0ac 100644 --- a/docs/cloud/features/alerts_notifications.md +++ b/docs/cloud/features/alerts_notifications.md @@ -32,7 +32,7 @@ Tobiko Cloud sends an alert based on a *trigger*. There are two types of trigger Events are tied to steps in the SQLMesh `plan` and `run` processes. For example, you could alert whenever a `plan` succeeded or a `run` failed. -Measures are based on either [automatically calculated measures](../../guides/observer.md#measures) like run time or [custom measure you define](../../guides/observer.md#custom-measures) via SQL queries. +Measures are [automatically calculated](../../guides/observer.md#measures) at run time. Choose whether the alert will be triggered by a Measure or Event in the alert's Trigger Type field. @@ -75,8 +75,6 @@ Some measures, like run time, are most useful when accumulated over an entire `p Configure a cumulative measure alert by choosing an Artifact type of Plan or Run. -Other measure alerts are useful when applied to individual models or steps in a `plan` or `run`. For example, you might know that your `users` model should process around 1000 new users each day. You could [create a measure](../../guides/observer.md#custom-measures) to track the number of new users and an alert that notifies you if that number drops below 500. - Configure a non-cumulative measure alert by choosing an Artifact type of Measure. ![Image showing the add measure Alert page Artifact field](./alerts_notifications/add_measure_alert_artifact.png) diff --git a/docs/guides/observer.md b/docs/guides/observer.md index f19bf37c38..734e634d98 100644 --- a/docs/guides/observer.md +++ b/docs/guides/observer.md @@ -51,7 +51,7 @@ The five entities in bold - models, audits, environments, runs, and plans - prov We now describe the specific measures SQLMesh captures about each entity. -SQLMesh performs its primary actions during **plans** and **runs**, so most measures are generated when they occur. Both plans and runs are executed in a specific **environment**, so all of their measures are environment-specific. +SQLMesh performs its primary actions during **plans** and **runs**, so measures are automatically generated when they occur. Both plans and runs are executed in a specific **environment**, so all of their measures are environment-specific. These measures are recorded and stored for each plan or run in a specific environment: @@ -62,8 +62,6 @@ These measures are recorded and stored for each plan or run in a specific enviro - The model versions evaluated during the plan/run - Each model's run time -Additionally, you can define [custom measures](#custom-measures) that will be captured for each model. - ## Installation SQLMesh Observer is part of the `sqlmesh-enterprise` Python library and is installed via `pip`. @@ -209,100 +207,3 @@ Next, the Loaded Intervals section displays the time intervals that have been lo The model information page concludes with a list of most frequent audits the model has failed, the most frequent time intervals that failed, and the largest historical model run times: ![SQLMesh Observer historical outliers](./observer/observer_model-information-4.png){ loading=lazy } - -## Custom measures - -SQLMesh Observer allows you to calculate and track custom measures in addition to the ones it [automatically calculates](#data). - -### Definition - -Each custom measure is associated with a model and is defined by a SQL query in the model file. - -The `@measure` macro is used to define custom measures. The body of the `@measure` macro is the query, and each column in the query defines a separate measure. - -A measure's name is the name of the column that defined it. Measure names must be unique within a model, but a name may be used in multiple models. - -A model may contain more than one `@measure` macro specification. The `@measure` macros must be specified after the model's primary query. They will be executed during a SQLMesh `plan` or `run` after the primary model query is executed. - -This example shows a model definition that includes a measure query defining two measures: `row_count` (the total number of rows in the table) and `num_col_avg` (the average value of the model's `numeric_col` column). - -```sql -MODEL ( - name custom_measure.example, - kind FULL -); - -SELECT - numeric_col -FROM - custom_measure.upstream; - -@measure( -- Measure query specified in the `@measure` macro - SELECT - COUNT(*) AS row_count, -- Table's row count - AVG(numeric_col) AS num_col_avg -- Average value of `numeric_col` - FROM custom_measure.example -- Select FROM the name of the model -); -``` - -Every time the `custom_measure.example` model is executed, Observer will execute the measure query and store the value it returns. - -By default, the measure's timestamp will be the execution time of the `plan`/`run` that captured the measure. [Incremental by time range](../concepts/models/model_kinds.md#incremental_by_time_range) models may specify [custom timestamps](#custom-time-column). - -An Observer chart allows you to select which measure to display. The chart displays the value of the selected measure on the y-axis and the execution time of the associated `plan`/`run` on the x-axis, allowing you to monitor whether the value has meaningfully changed since the previous execution. - -### Incremental by time models - -#### Custom time column - -In the previous example, Observer automatically associated each measure value with the execution time of the `plan` or `run` that executed it. - -For [incremental by time range models](../concepts/models/model_kinds.md#incremental_by_time_range), you can customize how measures are associated with time by including your own time column in the measure query. - -The time column must be named `ts` and may be of any datetime data type (e.g., date string, `DATE`, `TIMESTAMP`, etc.). Custom times are typically derived from a datetime column in the model data and are most useful when the measure groups by the datetime. - -For example, this incremental model stores the date of each data point in the `event_datestring` column. We could measure each day's row count and numeric column average with this measure query: - -```sql -MODEL ( - name custom_measure.incremental_example - kind INCREMENTAL_BY_TIME_RANGE ( - time_column event_datestring - ) -); - -SELECT - event_datestring, - numeric_col -FROM - custom_measure.upstream -WHERE - event_datestring BETWEEN @start_ds AND @end_ds; - -@measure( - SELECT - event_datestring AS ts, -- Custom measure time column `ts` - COUNT(*) AS daily_row_count, -- Daily row count - AVG(numeric_col) AS daily_num_col_avg -- Daily average value of `numeric_col` - FROM custom_measure.incremental_example - WHERE event_datestring BETWEEN @start_ds AND @end_ds -- Filter measure on time - GROUP BY event_datestring -- Group measure by time -); -``` - -The measure query both filters and groups the data based on the model's time column `event_datestring`. The filtering and grouping ensures that only one measure value is ever calculated for a specific day of data. - -NOTE: the custom time column approach will not work correctly if the model's [`lookback` argument](../concepts/models/overview.md#lookback) is specified because a given day's data will be processed every time it is in the lookback window. - -#### Execution and custom times - -A model may contain multiple measure queries, so both execution time and custom time measures may be specified for the same model. - -These two measure types help answer different questions: - -1. Execution time: has something meaningfully changed **on this `plan`/`run`** compared to previous plans/runs? -2. Custom time: has something meaningfully changed **in a specific time point's data** compared to other time points? - -If multiple time points of data are processed during each model execution, an anomaly at a specific time may not be detectable from an execution time measure alone. - -Custom time measures enable monitoring at the temporal granularity of the data itself. From c0bf0071a6b7d28389aeba45ee1e3904baf612ad Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 8 May 2025 01:33:38 +0300 Subject: [PATCH 0115/1056] Fix!: use sql() instead of gen() for time data type data hash computation (#4321) --- sqlmesh/core/model/kind.py | 2 +- ...se_sql_for_scd_time_data_type_data_hash.py | 5 +++ tests/core/test_model.py | 40 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index aef9ca5607..e59287adfd 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -711,7 +711,7 @@ def data_hash_values(self) -> t.List[t.Optional[str]]: gen(self.valid_from_name), gen(self.valid_to_name), str(self.invalidate_hard_deletes), - gen(self.time_data_type), + self.time_data_type.sql(self.dialect), gen(self.batch_size) if self.batch_size is not None else None, ] diff --git a/sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py b/sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py new file mode 100644 index 0000000000..a6ad2bb553 --- /dev/null +++ b/sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py @@ -0,0 +1,5 @@ +"""Use sql(...) instead of gen when computing the data hash of the time data type.""" + + +def migrate(state_sync, **kwargs): # type: ignore + pass diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 20f08a259d..6a7d7f34fd 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9662,3 +9662,43 @@ def test_invalid_audit_reference(): with pytest.raises(ConfigError, match="Audit 'not_nulll' is undefined"): load_sql_based_model(expressions) + + +def test_scd_time_data_type_does_not_cause_diff_after_deserialization() -> None: + for dialect in ( + "athena", + "bigquery", + "clickhouse", + "databricks", + "duckdb", + "dune", + "hive", + "mysql", + "postgres", + "presto", + "redshift", + "snowflake", + "spark", + "trino", + "tsql", + ): + sql = f""" + MODEL ( + name test_schema.test_model, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key ARRAY(col), + columns ARRAY(col), + invalidate_hard_deletes TRUE, + on_destructive_change error + ), + dialect {dialect} + ); + + SELECT + 1 AS col + """ + + model = load_sql_based_model(d.parse(sql)) + deserialized_model = SqlModel.parse_raw(model.json()) + + assert model.data_hash == deserialized_model.data_hash From 25e62621f002e39335442b8a272cbb695fdb4928 Mon Sep 17 00:00:00 2001 From: Philippe Laflamme <484152+plaflamme@users.noreply.github.com> Date: Wed, 7 May 2025 19:35:14 -0400 Subject: [PATCH 0116/1056] fix: generate `TIMESTAMP` for BQ `_PARTITIONTIME` instead of `DATETIME` (#4313) Co-authored-by: Jo <46752250+georgesittas@users.noreply.github.com> --- sqlmesh/core/engine_adapter/bigquery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index e60e606a36..df9653f5d5 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -289,7 +289,7 @@ def create_mapping_schema( and bq_table.time_partitioning and not bq_table.time_partitioning.field ): - columns["_PARTITIONTIME"] = exp.DataType.build("TIMESTAMP") + columns["_PARTITIONTIME"] = exp.DataType.build("TIMESTAMP", dialect="bigquery") if bq_table.time_partitioning.type_ == "DAY": columns["_PARTITIONDATE"] = exp.DataType.build("DATE") From 51fe5107805063e970e199fbe2f236b07e813e29 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 8 May 2025 13:50:28 +0300 Subject: [PATCH 0117/1056] Fix: Make flaky magics dag test deterministic (#4334) --- tests/integrations/jupyter/test_magics.py | 27 ++++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index 847d16648c..99d290c517 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -324,18 +324,23 @@ def test_run_dag( "'Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 18/18" ) assert not output.stderr - assert len(output.outputs) == 6 + + # At least 6 outputs expected as the number of models in the particular batch might vary + assert len(output.outputs) >= 6 + html_text_actual = convert_all_html_output_to_text(output) - html_text_expected = [ - "[2K", - "Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━ 93.8% • 15/16 • 0:00:", - "[1/1] sushi.waiter_as_customer_by_day [insert 2023-01-01 - 2023-01-02, audits ✔2]", - "", - "✔ Model batches executed", - "Run finished for environment 'prod'", - ] - for txt_actual, txt_expected in zip(html_text_actual, html_text_expected): - assert txt_actual.startswith(txt_expected) + + # Check for key elements in the output + assert any("[2K" in text for text in html_text_actual) + assert any("Executing model batches" in text for text in html_text_actual) + assert any("✔ Model batches executed" in text for text in html_text_actual) + assert any("Run finished for environment 'prod'" in text for text in html_text_actual) + + # Check the final messages + final_outputs = [text for text in html_text_actual if text.strip()] + assert final_outputs[-2] == "✔ Model batches executed" + assert final_outputs[-1] == "Run finished for environment 'prod'" + actual_html_output = get_all_html_output(output) # Replace dynamic elapsed time with 00 for i, chunk in enumerate(actual_html_output): From 0531201ba6ac2fec5e4b080b07efe6f3c5172b64 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 8 May 2025 14:39:16 +0300 Subject: [PATCH 0118/1056] Fix: Gracefully handle execution errors in before after all (#4333) --- sqlmesh/core/environment.py | 47 ++++++++++------- tests/core/test_integration.py | 64 +++++++++++++++++++++++ tests/integrations/jupyter/test_magics.py | 4 +- 3 files changed, 95 insertions(+), 20 deletions(-) diff --git a/sqlmesh/core/environment.py b/sqlmesh/core/environment.py index ea326d6d26..9da2af9462 100644 --- a/sqlmesh/core/environment.py +++ b/sqlmesh/core/environment.py @@ -14,6 +14,7 @@ from sqlmesh.core.snapshot import SnapshotId, SnapshotTableInfo, Snapshot from sqlmesh.utils import word_characters_only from sqlmesh.utils.date import TimeLike, now_timestamp +from sqlmesh.utils.errors import SQLMeshError from sqlmesh.utils.jinja import JinjaMacroRegistry from sqlmesh.utils.metaprogramming import Executable from sqlmesh.utils.pydantic import PydanticModel, field_validator, ValidationInfo @@ -261,25 +262,35 @@ def execute_environment_statements( end: t.Optional[TimeLike] = None, execution_time: t.Optional[TimeLike] = None, ) -> None: - rendered_expressions = [ - expr - for statements in environment_statements - for expr in render_statements( - statements=getattr(statements, runtime_stage.value), - dialect=adapter.dialect, - default_catalog=default_catalog, - python_env=statements.python_env, - jinja_macros=statements.jinja_macros, - snapshots=snapshots, - start=start, - end=end, - execution_time=execution_time, - environment_naming_info=environment_naming_info, - runtime_stage=runtime_stage, - engine_adapter=adapter, + try: + rendered_expressions = [ + expr + for statements in environment_statements + for expr in render_statements( + statements=getattr(statements, runtime_stage.value), + dialect=adapter.dialect, + default_catalog=default_catalog, + python_env=statements.python_env, + jinja_macros=statements.jinja_macros, + snapshots=snapshots, + start=start, + end=end, + execution_time=execution_time, + environment_naming_info=environment_naming_info, + runtime_stage=runtime_stage, + engine_adapter=adapter, + ) + ] + except Exception as e: + raise SQLMeshError( + f"An error occurred during rendering of the '{runtime_stage.value}' statements:\n\n{e}" ) - ] if rendered_expressions: with adapter.transaction(): for expr in rendered_expressions: - adapter.execute(expr) + try: + adapter.execute(expr) + except Exception as e: + raise SQLMeshError( + f"An error occurred during execution of the following '{runtime_stage.value}' statement:\n\n{expr}\n\n{e}" + ) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 88711495f2..1778b1e72c 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -18,6 +18,7 @@ from pytest_mock.plugin import MockerFixture from sqlglot import exp from sqlglot.expressions import DataType +import re from sqlmesh import CustomMaterialization from sqlmesh.cli.example_project import init_example_project @@ -5501,6 +5502,69 @@ def test_plan_production_environment_statements(tmp_path: Path): ctx.fetchdf("select * from not_create") +def test_environment_statements_error_handling(tmp_path: Path): + model_a = """ + MODEL ( + name test_schema.a, + kind FULL, + ); + + SELECT 1 AS account_id + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + for path, defn in {"a.sql": model_a}.items(): + with open(models_dir / path, "w") as f: + f.write(defn) + + before_all = [ + "CREATE TABLE identical_table (physical_schema_name VARCHAR)", + "CREATE TABLE identical_table (physical_schema_name VARCHAR)", + ] + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + before_all=before_all, + ) + ctx = Context(paths=[tmp_path], config=config) + + expected_error_message = re.escape( + """An error occurred during execution of the following 'before_all' statement: + +CREATE TABLE identical_table (physical_schema_name TEXT) + +Catalog Error: Table with name "identical_table" already exists!""" + ) + + with pytest.raises(SQLMeshError, match=expected_error_message): + ctx.plan(auto_apply=True, no_prompts=True) + + after_all = [ + "@bad_macro()", + ] + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + after_all=after_all, + ) + ctx = Context(paths=[tmp_path], config=config) + + expected_error_message = re.escape( + """An error occurred during rendering of the 'after_all' statements: + +Failed to resolve macros for + +@bad_macro() + +Macro 'bad_macro' does not exist.""" + ) + + with pytest.raises(SQLMeshError, match=expected_error_message): + ctx.plan(auto_apply=True, no_prompts=True) + + @time_machine.travel("2025-03-08 00:00:00 UTC") def test_tz(init_and_plan_context): context, _ = init_and_plan_context("examples/sushi") diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index 99d290c517..883221740a 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -325,8 +325,8 @@ def test_run_dag( ) assert not output.stderr - # At least 6 outputs expected as the number of models in the particular batch might vary - assert len(output.outputs) >= 6 + # At least 4 outputs expected as the number of models in the particular batch might vary + assert len(output.outputs) >= 4 html_text_actual = convert_all_html_output_to_text(output) From 06ea05aea960f849c8230162d8073b8167304055 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 8 May 2025 20:46:22 +0200 Subject: [PATCH 0119/1056] chore(vscode): add full path to model and make call sync (#4336) --- web/client/openapi.json | 109 ++++++++++++------------- web/client/src/models/sqlmesh-model.ts | 2 + web/server/api/endpoints/lineage.py | 2 +- web/server/api/endpoints/models.py | 1 + web/server/models.py | 4 + 5 files changed, 61 insertions(+), 57 deletions(-) diff --git a/web/client/openapi.json b/web/client/openapi.json index fd84525cc0..bf1cef0809 100644 --- a/web/client/openapi.json +++ b/web/client/openapi.json @@ -11,12 +11,7 @@ "content": { "application/json": { "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_initiate_apply_api_commands_apply_post" - } - ], - "title": "Body" + "$ref": "#/components/schemas/Body_initiate_apply_api_commands_apply_post" } } } @@ -154,14 +149,10 @@ } }, { - "name": "verbose", + "name": "verbosity", "in": "query", "required": false, - "schema": { - "type": "boolean", - "default": false, - "title": "Verbose" - } + "schema": { "$ref": "#/components/schemas/Verbosity", "default": 0 } } ], "responses": { @@ -249,12 +240,7 @@ "content": { "application/json": { "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_write_file_api_files__path__post" - } - ], - "title": "Body" + "$ref": "#/components/schemas/Body_write_file_api_files__path__post" } } } @@ -336,12 +322,7 @@ "content": { "application/json": { "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_write_directory_api_directories__path__post" - } - ], - "title": "Body" + "$ref": "#/components/schemas/Body_write_directory_api_directories__path__post" } } } @@ -402,12 +383,7 @@ "content": { "application/json": { "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Body_initiate_plan_api_plan_post" - } - ], - "title": "Body" + "$ref": "#/components/schemas/Body_initiate_plan_api_plan_post" } } } @@ -782,7 +758,13 @@ "description": "Successful Response", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/TableDiff" } + "schema": { + "anyOf": [ + { "$ref": "#/components/schemas/TableDiff" }, + { "type": "null" } + ], + "title": "Response Get Table Diff Api Table Diff Get" + } } } }, @@ -890,7 +872,7 @@ "name": { "type": "string", "title": "Name" }, "view_name": { "type": "string", "title": "View Name" }, "node_type": { - "allOf": [{ "$ref": "#/components/schemas/NodeType" }], + "$ref": "#/components/schemas/NodeType", "default": "model" }, "parents": { @@ -917,7 +899,7 @@ "name": { "type": "string", "title": "Name" }, "view_name": { "type": "string", "title": "View Name" }, "node_type": { - "allOf": [{ "$ref": "#/components/schemas/NodeType" }], + "$ref": "#/components/schemas/NodeType", "default": "model" }, "parents": { @@ -1041,7 +1023,7 @@ "name": { "type": "string", "title": "Name" }, "view_name": { "type": "string", "title": "View Name" }, "node_type": { - "allOf": [{ "$ref": "#/components/schemas/NodeType" }], + "$ref": "#/components/schemas/NodeType", "default": "model" }, "parents": { @@ -1081,7 +1063,7 @@ "name": { "type": "string", "title": "Name" }, "view_name": { "type": "string", "title": "View Name" }, "node_type": { - "allOf": [{ "$ref": "#/components/schemas/NodeType" }], + "$ref": "#/components/schemas/NodeType", "default": "model" }, "parents": { @@ -1102,7 +1084,7 @@ "name": { "type": "string", "title": "Name" }, "view_name": { "type": "string", "title": "View Name" }, "node_type": { - "allOf": [{ "$ref": "#/components/schemas/NodeType" }], + "$ref": "#/components/schemas/NodeType", "default": "model" }, "parents": { @@ -1157,22 +1139,6 @@ "Environment": { "properties": { "name": { "type": "string", "title": "Name", "default": "prod" }, - "suffix_target": { - "allOf": [ - { "$ref": "#/components/schemas/EnvironmentSuffixTarget" } - ], - "default": "schema" - }, - "catalog_name_override": { - "anyOf": [{ "type": "string" }, { "type": "null" }], - "title": "Catalog Name Override" - }, - "normalize_name": { - "type": "boolean", - "title": "Normalize Name", - "default": true - }, - "snapshots": { "items": {}, "type": "array", "title": "Snapshots" }, "start_at": { "anyOf": [ { "type": "string", "format": "date" }, @@ -1207,6 +1173,25 @@ "anyOf": [{ "type": "integer" }, { "type": "null" }], "title": "Finalized Ts" }, + "suffix_target": { + "$ref": "#/components/schemas/EnvironmentSuffixTarget", + "default": "schema" + }, + "catalog_name_override": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Catalog Name Override" + }, + "normalize_name": { + "type": "boolean", + "title": "Normalize Name", + "default": true + }, + "gateway_managed": { + "type": "boolean", + "title": "Gateway Managed", + "default": false + }, + "snapshots": { "items": {}, "type": "array", "title": "Snapshots" }, "promoted_snapshot_ids": { "anyOf": [{ "items": {}, "type": "array" }, { "type": "null" }], "title": "Promoted Snapshot Ids" @@ -1224,9 +1209,9 @@ }, "additionalProperties": false, "type": "object", - "required": ["snapshots", "start_at", "plan_id"], + "required": ["start_at", "plan_id", "snapshots"], "title": "Environment", - "description": "Represents an isolated environment.\n\nEnvironments are isolated workspaces that hold pointers to physical tables.\n\nArgs:\n snapshots: The snapshots that are part of this environment.\n start_at: The start time of the environment.\n end_at: The end time of the environment.\n plan_id: The ID of the plan that last updated this environment.\n previous_plan_id: The ID of the previous plan that updated this environment.\n expiration_ts: The timestamp when this environment will expire.\n finalized_ts: The timestamp when this environment was finalized.\n promoted_snapshot_ids: The IDs of the snapshots that are promoted in this environment\n (i.e. for which the views are created). If not specified, all snapshots are promoted.\n previous_finalized_snapshots: Snapshots that were part of this environment last time it was finalized.\n requirements: A mapping of library versions for all the snapshots in this environment." + "description": "Represents an isolated environment.\n\nEnvironments are isolated workspaces that hold pointers to physical tables.\n\nArgs:\n snapshots: The snapshots that are part of this environment.\n promoted_snapshot_ids: The IDs of the snapshots that are promoted in this environment\n (i.e. for which the views are created). If not specified, all snapshots are promoted.\n previous_finalized_snapshots: Snapshots that were part of this environment last time it was finalized.\n requirements: A mapping of library versions for all the snapshots in this environment." }, "EnvironmentSuffixTarget": { "type": "string", @@ -1398,6 +1383,7 @@ "name": { "type": "string", "title": "Name" }, "fqn": { "type": "string", "title": "Fqn" }, "path": { "type": "string", "title": "Path" }, + "full_path": { "type": "string", "title": "Full Path" }, "dialect": { "type": "string", "title": "Dialect" }, "type": { "$ref": "#/components/schemas/ModelType" }, "columns": { @@ -1435,6 +1421,7 @@ "name", "fqn", "path", + "full_path", "dialect", "type", "columns", @@ -2061,7 +2048,11 @@ "type": "object", "title": "Stats" }, - "sample": { "type": "object", "title": "Sample" }, + "sample": { + "additionalProperties": true, + "type": "object", + "title": "Sample" + }, "source_count": { "type": "integer", "title": "Source Count" }, "target_count": { "type": "integer", "title": "Target Count" }, "count_pct_change": { "type": "number", "title": "Count Pct Change" } @@ -2213,7 +2204,7 @@ "TrackableMeta": { "properties": { "status": { - "allOf": [{ "$ref": "#/components/schemas/Status" }], + "$ref": "#/components/schemas/Status", "default": "init" }, "start": { "type": "integer", "title": "Start" }, @@ -2240,6 +2231,12 @@ "type": "object", "required": ["loc", "msg", "type"], "title": "ValidationError" + }, + "Verbosity": { + "type": "integer", + "enum": [0, 1, 2], + "title": "Verbosity", + "description": "Verbosity levels for SQLMesh output." } } } diff --git a/web/client/src/models/sqlmesh-model.ts b/web/client/src/models/sqlmesh-model.ts index 0fd15a3af1..0d5be3ecd0 100644 --- a/web/client/src/models/sqlmesh-model.ts +++ b/web/client/src/models/sqlmesh-model.ts @@ -25,6 +25,7 @@ export class ModelSQLMeshModel< name: string fqn: string path: string + full_path: string dialect: string type: ModelType columns: Column[] @@ -50,6 +51,7 @@ export class ModelSQLMeshModel< this.fqn = encodeURI(this.initial.fqn) this.default_catalog = this.initial.default_catalog this.path = this.initial.path + this.full_path = this.initial.full_path this.dialect = this.initial.dialect this.description = this.initial.description this.sql = this.initial.sql diff --git a/web/server/api/endpoints/lineage.py b/web/server/api/endpoints/lineage.py index 29473f37b1..b674f33e27 100644 --- a/web/server/api/endpoints/lineage.py +++ b/web/server/api/endpoints/lineage.py @@ -146,7 +146,7 @@ async def column_lineage( @router.get("/{model_name:str}") -async def model_lineage( +def model_lineage( model_name: str, context: Context = Depends(get_loaded_context), ) -> t.Dict[str, t.Set[str]]: diff --git a/web/server/api/endpoints/models.py b/web/server/api/endpoints/models.py index df8da251d0..ab681bf4e0 100644 --- a/web/server/api/endpoints/models.py +++ b/web/server/api/endpoints/models.py @@ -125,6 +125,7 @@ def serialize_model(context: Context, model: Model, render_query: bool = False) name=model.name, fqn=model.fqn, path=str(model._path.absolute().relative_to(context.path)), + full_path=str(model._path.absolute()), dialect=dialect, columns=columns, details=details, diff --git a/web/server/models.py b/web/server/models.py index ddf1d5dc6c..82e17986c3 100644 --- a/web/server/models.py +++ b/web/server/models.py @@ -172,6 +172,10 @@ class Model(PydanticModel): name: str fqn: str path: str + full_path: str + """ + As opposed to path, which is relative to the project root, full_path is the absolute path to the model file. + """ dialect: str type: ModelType columns: t.List[Column] From 7530c3c58e87bc77c129cbcdb042bc84f9514edd Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 8 May 2025 21:27:38 +0200 Subject: [PATCH 0120/1056] refactor(vscode): improve lsp tests (#4342) --- sqlmesh/lsp/context.py | 1 + tests/lsp/test_reference.py | 47 +++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 08db9bb56e..6a44ef32f3 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -16,4 +16,5 @@ def __init__(self, context: Context) -> None: if model._path is not None: path = Path(model._path).resolve() map[f"file://{path.as_posix()}"].append(model.name) + self.map = map diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index cf700420ef..a01599400d 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -25,26 +25,7 @@ def test_reference() -> None: path = active_customers_uri.removeprefix("file://") read_file = open(path, "r").readlines() # Get the string range in the read file - reference_range = references[0].range - start_line = reference_range.start.line - end_line = reference_range.end.line - start_character = reference_range.start.character - end_character = reference_range.end.character - # Get the string from the file - - # If the reference spans multiple lines, handle it accordingly - if start_line == end_line: - # Reference is on a single line - line_content = read_file[start_line] - referenced_text = line_content[start_character:end_character] - else: - # Reference spans multiple lines - referenced_text = read_file[start_line][ - start_character: - ] # First line from start_character to end - for line_num in range(start_line + 1, end_line): # Middle lines (if any) - referenced_text += read_file[line_num] - referenced_text += read_file[end_line][:end_character] # Last line up to end_character + referenced_text = get_string_from_range(read_file, references[0].range) assert referenced_text == "sushi.customers" @@ -60,6 +41,32 @@ def test_reference_with_alias() -> None: references = get_model_definitions_for_a_path(lsp_context, waiter_revenue_by_day_uri) assert len(references) == 3 + path = waiter_revenue_by_day_uri.removeprefix("file://") + read_file = open(path, "r").readlines() + assert references[0].uri.endswith("orders.py") + assert get_string_from_range(read_file, references[0].range) == "sushi.orders" assert references[1].uri.endswith("order_items.py") + assert get_string_from_range(read_file, references[1].range) == "sushi.order_items" assert references[2].uri.endswith("items.py") + assert get_string_from_range(read_file, references[2].range) == "sushi.items" + + +def get_string_from_range(file_lines, range_obj) -> str: + start_line = range_obj.start.line + end_line = range_obj.end.line + start_character = range_obj.start.character + end_character = range_obj.end.character + + # If the reference spans multiple lines, handle it accordingly + if start_line == end_line: + # Reference is on a single line + line_content = file_lines[start_line] + return line_content[start_character:end_character] + + # Reference spans multiple lines + result = file_lines[start_line][start_character:] # First line from start_character to end + for line_num in range(start_line + 1, end_line): # Middle lines (if any) + result += file_lines[line_num] + result += file_lines[end_line][:end_character] # Last line up to end_character + return result From ba90166b1ac97b12e63a12ed11eba691e66adebe Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 8 May 2025 21:45:39 +0200 Subject: [PATCH 0121/1056] feat(vscode): initialize on python as well (#4343) --- sqlmesh/lsp/main.py | 29 +++++++++++++++++++++++++++++ vscode/extension/package.json | 3 ++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index aeaceecab6..d890caf9e8 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -40,6 +40,35 @@ def __init__( def _register_features(self) -> None: """Register LSP features on the internal LanguageServer instance.""" + @self.server.feature(types.INITIALIZE) + def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: + """Initialize the server when the client connects.""" + try: + if params.workspace_folders: + # Try to find a SQLMesh config file in any workspace folder (only at the root level) + for folder in params.workspace_folders: + folder_path = Path(self._uri_to_path(folder.uri)) + # Only check for config files directly in the workspace directory + for ext in ("py", "yml", "yaml"): + config_path = folder_path / f"config.{ext}" + if config_path.exists(): + try: + # Use user-provided instantiator to build the context + created_context = self.context_class(paths=[folder_path]) + self.lsp_context = LSPContext(created_context) + ls.show_message( + f"Loaded SQLMesh context from {config_path}", + types.MessageType.Info, + ) + return # Exit after successfully loading any config + except Exception as e: + ls.show_message( + f"Error loading context from {config_path}: {e}", + types.MessageType.Warning, + ) + except Exception as e: + ls.show_message(f"Error initializing SQLMesh context: {e}", types.MessageType.Error) + @self.server.feature(ALL_MODELS_FEATURE) def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse: try: diff --git a/vscode/extension/package.json b/vscode/extension/package.json index fa898a4bce..24f5a972d7 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -17,7 +17,8 @@ "Other" ], "activationEvents": [ - "onLanguage:sql" + "onLanguage:sql", + "onLanguage:python" ], "extensionKind": [ "workspace" From ab09f9bf0d08c5bcbca6b97bd1b4c5db9b40eb3a Mon Sep 17 00:00:00 2001 From: Tori Wei <41123940+toriwei@users.noreply.github.com> Date: Thu, 8 May 2025 13:17:50 -0700 Subject: [PATCH 0122/1056] feat: store user provided plan flags (#4319) --- sqlmesh/core/context.py | 66 ++++++++++++++++++++++++++------- sqlmesh/core/plan/builder.py | 10 ++++- sqlmesh/core/plan/definition.py | 5 +++ tests/core/test_plan.py | 47 +++++++++++++++++++++++ 4 files changed, 113 insertions(+), 15 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 6e1ff1eca1..1328b8fea5 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -88,6 +88,7 @@ NotificationTargetManager, ) from sqlmesh.core.plan import Plan, PlanBuilder, SnapshotIntervals +from sqlmesh.core.plan.definition import UserProvidedFlags from sqlmesh.core.reference import ReferenceGraph from sqlmesh.core.scheduler import Scheduler, CompletionStatus from sqlmesh.core.schema_loader import create_external_models_file @@ -1161,11 +1162,11 @@ def plan( end: t.Optional[TimeLike] = None, execution_time: t.Optional[TimeLike] = None, create_from: t.Optional[str] = None, - skip_tests: bool = False, + skip_tests: t.Optional[bool] = None, restate_models: t.Optional[t.Iterable[str]] = None, - no_gaps: bool = False, - skip_backfill: bool = False, - empty_backfill: bool = False, + no_gaps: t.Optional[bool] = None, + skip_backfill: t.Optional[bool] = None, + empty_backfill: t.Optional[bool] = None, forward_only: t.Optional[bool] = None, allow_destructive_models: t.Optional[t.Collection[str]] = None, no_prompts: t.Optional[bool] = None, @@ -1178,9 +1179,9 @@ def plan( categorizer_config: t.Optional[CategorizerConfig] = None, enable_preview: t.Optional[bool] = None, no_diff: t.Optional[bool] = None, - run: bool = False, - diff_rendered: bool = False, - skip_linter: bool = False, + run: t.Optional[bool] = None, + diff_rendered: t.Optional[bool] = None, + skip_linter: t.Optional[bool] = None, ) -> Plan: """Interactively creates a plan. @@ -1278,11 +1279,11 @@ def plan_builder( end: t.Optional[TimeLike] = None, execution_time: t.Optional[TimeLike] = None, create_from: t.Optional[str] = None, - skip_tests: bool = False, + skip_tests: t.Optional[bool] = None, restate_models: t.Optional[t.Iterable[str]] = None, - no_gaps: bool = False, - skip_backfill: bool = False, - empty_backfill: bool = False, + no_gaps: t.Optional[bool] = None, + skip_backfill: t.Optional[bool] = None, + empty_backfill: t.Optional[bool] = None, forward_only: t.Optional[bool] = None, allow_destructive_models: t.Optional[t.Collection[str]] = None, no_auto_categorization: t.Optional[bool] = None, @@ -1292,9 +1293,9 @@ def plan_builder( backfill_models: t.Optional[t.Collection[str]] = None, categorizer_config: t.Optional[CategorizerConfig] = None, enable_preview: t.Optional[bool] = None, - run: bool = False, - diff_rendered: bool = False, - skip_linter: bool = False, + run: t.Optional[bool] = None, + diff_rendered: t.Optional[bool] = None, + skip_linter: t.Optional[bool] = None, ) -> PlanBuilder: """Creates a plan builder. @@ -1335,6 +1336,42 @@ def plan_builder( Returns: The plan builder. """ + kwargs: t.Dict[str, t.Optional[UserProvidedFlags]] = { + "start": start, + "end": end, + "execution_time": execution_time, + "create_from": create_from, + "skip_tests": skip_tests, + "restate_models": list(restate_models) if restate_models is not None else None, + "no_gaps": no_gaps, + "skip_backfill": skip_backfill, + "empty_backfill": empty_backfill, + "forward_only": forward_only, + "allow_destructive_models": list(allow_destructive_models) + if allow_destructive_models is not None + else None, + "no_auto_categorization": no_auto_categorization, + "effective_from": effective_from, + "include_unmodified": include_unmodified, + "select_models": list(select_models) if select_models is not None else None, + "backfill_models": list(backfill_models) if backfill_models is not None else None, + "enable_preview": enable_preview, + "run": run, + "diff_rendered": diff_rendered, + "skip_linter": skip_linter, + } + user_provided_flags: t.Dict[str, UserProvidedFlags] = { + k: v for k, v in kwargs.items() if v is not None + } + + skip_tests = skip_tests or False + no_gaps = no_gaps or False + skip_backfill = skip_backfill or False + empty_backfill = empty_backfill or False + run = run or False + diff_rendered = diff_rendered or False + skip_linter = skip_linter or False + environment = environment or self.config.default_target_environment environment = Environment.sanitize_name(environment) is_dev = environment != c.PROD @@ -1469,6 +1506,7 @@ def plan_builder( engine_schema_differ=self.engine_adapter.SCHEMA_DIFFER, interval_end_per_model=max_interval_end_per_model, console=self.console, + user_provided_flags=user_provided_flags, ) def apply( diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index b70b9d1e3f..eed05e04eb 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -15,7 +15,12 @@ ) from sqlmesh.core.context_diff import ContextDiff from sqlmesh.core.environment import EnvironmentNamingInfo -from sqlmesh.core.plan.definition import Plan, SnapshotMapping, earliest_interval_start +from sqlmesh.core.plan.definition import ( + Plan, + SnapshotMapping, + UserProvidedFlags, + earliest_interval_start, +) from sqlmesh.core.schema_diff import SchemaDiffer, has_drop_alteration, get_dropped_column_names from sqlmesh.core.snapshot import ( DeployabilityIndex, @@ -107,6 +112,7 @@ def __init__( ensure_finalized_snapshots: bool = False, interval_end_per_model: t.Optional[t.Dict[str, int]] = None, console: t.Optional[PlanBuilderConsole] = None, + user_provided_flags: t.Optional[t.Dict[str, UserProvidedFlags]] = None, ): self._context_diff = context_diff self._no_gaps = no_gaps @@ -134,6 +140,7 @@ def __init__( self._engine_schema_differ = engine_schema_differ self._console = console or get_console() self._choices: t.Dict[SnapshotId, SnapshotChangeCategory] = {} + self._user_provided_flags = user_provided_flags self._start = start if not self._start and ( @@ -280,6 +287,7 @@ def build(self) -> Plan: execution_time=self._execution_time, end_bounded=self._end_bounded, ensure_finalized_snapshots=self._ensure_finalized_snapshots, + user_provided_flags=self._user_provided_flags, ) self._latest_plan = plan return plan diff --git a/sqlmesh/core/plan/definition.py b/sqlmesh/core/plan/definition.py index 26d4b507ca..0a0e3ee739 100644 --- a/sqlmesh/core/plan/definition.py +++ b/sqlmesh/core/plan/definition.py @@ -28,6 +28,7 @@ from sqlmesh.utils.pydantic import PydanticModel SnapshotMapping = t.Dict[SnapshotId, t.Set[SnapshotId]] +UserProvidedFlags = t.Union[TimeLike, str, bool, t.List[str]] class Plan(PydanticModel, frozen=True): @@ -63,6 +64,8 @@ class Plan(PydanticModel, frozen=True): effective_from: t.Optional[TimeLike] = None execution_time: t.Optional[TimeLike] = None + user_provided_flags: t.Optional[t.Dict[str, UserProvidedFlags]] = None + @cached_property def start(self) -> TimeLike: if self.provided_start is not None: @@ -262,6 +265,7 @@ def to_evaluatable(self) -> EvaluatablePlan: if s.is_model and s.model.disable_restatement }, environment_statements=self.context_diff.environment_statements, + user_provided_flags=self.user_provided_flags, ) @cached_property @@ -294,6 +298,7 @@ class EvaluatablePlan(PydanticModel): execution_time: t.Optional[TimeLike] = None disabled_restatement_models: t.Set[str] environment_statements: t.Optional[t.List[EnvironmentStatements]] = None + user_provided_flags: t.Optional[t.Dict[str, UserProvidedFlags]] = None def is_selected_for_backfill(self, model_fqn: str) -> bool: return self.models_to_backfill is None or model_fqn in self.models_to_backfill diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 89e8d3c7fe..6daec19554 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -3105,3 +3105,50 @@ def test_set_choice_for_forward_only_model(make_snapshot): plan.snapshots[updated_snapshot.snapshot_id].change_category == SnapshotChangeCategory.FORWARD_ONLY ) + + +def test_user_provided_flags(sushi_context: Context): + expected_flags = { + "run": True, + "execution_time": "2025-01-01", + } + plan_a = sushi_context.plan(no_prompts=True, run=True, execution_time="2025-01-01") + assert plan_a.user_provided_flags == expected_flags + evaluatable_plan = plan_a.to_evaluatable() + assert evaluatable_plan.user_provided_flags == expected_flags + + plan_b = sushi_context.plan() + assert plan_b.user_provided_flags == {} + evaluatable_plan_b = plan_b.to_evaluatable() + assert evaluatable_plan_b.user_provided_flags == {} + + context_diff = ContextDiff( + environment="test_environment", + is_new_environment=True, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={}, + snapshots={}, + new_snapshots={}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + ) + plan_builder = PlanBuilder( + context_diff, + DuckDBEngineAdapter.SCHEMA_DIFFER, + forward_only=True, + user_provided_flags={"forward_only": True}, + ).build() + assert plan_builder.user_provided_flags == {"forward_only": True} + plan_builder = PlanBuilder( + context_diff, + DuckDBEngineAdapter.SCHEMA_DIFFER, + ).build() + assert plan_builder.user_provided_flags == None From 0c323d60f1efbed1ea968e881b6f91a1621e01a8 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 9 May 2025 00:10:22 +0300 Subject: [PATCH 0123/1056] Chore!: bump sqlglot to v26.17.1 (#4338) --- examples/sushi/models/marketing.sql | 2 -- pyproject.toml | 2 +- .../integration/test_integration.py | 29 +++++++++++++++++++ tests/core/test_model.py | 4 +-- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/examples/sushi/models/marketing.sql b/examples/sushi/models/marketing.sql index 06f8d609fd..445fbf7787 100644 --- a/examples/sushi/models/marketing.sql +++ b/examples/sushi/models/marketing.sql @@ -20,7 +20,5 @@ FROM customer_id: 'int', status: 'text', updated_at: 'timestamp', - valid_from: 'timestamp', - valid_to: 'timestamp', } ) diff --git a/pyproject.toml b/pyproject.toml index 9f0c41d196..cfbe1cb293 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.16.3", + "sqlglot[rs]~=26.17.1", "tenacity", "time-machine", "json-stream" diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index ac73e866b8..5dc5f9b71b 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -1446,6 +1446,35 @@ def test_sushi(ctx: TestContext, tmp_path_factory: pytest.TempPathFactory): f"CREATE VIEW {raw_test_schema}.demographics ON CLUSTER cluster1 AS SELECT 1 AS customer_id, '00000' AS zip;" ) + # DuckDB parses TIMESTAMP into Type.TIMESTAMPNTZ which generates into TIMESTAMP_NTZ for + # Spark, but this type is not supported in Spark's DDL statements so we make it a TIMESTAMP + if ctx.dialect == "spark": + for model_key, model in context._models.items(): + model_columns = model.columns_to_types + + updated_model_columns = {} + for k, v in model_columns.items(): + updated_model_columns[k] = v + if v.this == exp.DataType.Type.TIMESTAMPNTZ: + v.set("this", exp.DataType.Type.TIMESTAMP) + + update_fields = { + "columns_to_types": updated_model_columns, + "columns_to_types_": updated_model_columns, + "columns_to_types_or_raise": updated_model_columns, + } + + # We get rid of the sushi.marketing post statement here because it asserts that + # updated_at is a 'timestamp', which is parsed using duckdb in assert_has_columns + # and the assertion fails because we now have TIMESTAMPs and not TIMESTAMPNTZs in + # the columns_to_types mapping + if '"marketing"' in model_key: + update_fields["post_statements_"] = [] + + context._models.update( + {model_key: context._models[model_key].copy(update=update_fields)} + ) + if ctx.dialect == "athena": for model_name in {"customer_revenue_lifetime"}: model_key = next(k for k in context._models if model_name in k) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 6a7d7f34fd..7cf9173cb3 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -250,7 +250,7 @@ def test_model_union_query(sushi_context, assert_exp_eq): """SELECT CAST("marketing"."customer_id" AS INT) AS "customer_id", CAST("marketing"."status" AS TEXT) AS "status", - CAST("marketing"."updated_at" AS TIMESTAMP) AS "updated_at", + CAST("marketing"."updated_at" AS TIMESTAMPNTZ) AS "updated_at", CAST("marketing"."valid_from" AS TIMESTAMP) AS "valid_from", CAST("marketing"."valid_to" AS TIMESTAMP) AS "valid_to" FROM "memory"."sushi"."marketing" AS "marketing" @@ -258,7 +258,7 @@ def test_model_union_query(sushi_context, assert_exp_eq): SELECT CAST("marketing"."customer_id" AS INT) AS "customer_id", CAST("marketing"."status" AS TEXT) AS "status", - CAST("marketing"."updated_at" AS TIMESTAMP) AS "updated_at", + CAST("marketing"."updated_at" AS TIMESTAMPNTZ) AS "updated_at", CAST("marketing"."valid_from" AS TIMESTAMP) AS "valid_from", CAST("marketing"."valid_to" AS TIMESTAMP) AS "valid_to" FROM "memory"."sushi"."marketing" AS "marketing" From 3462a5dc9f5e90c0659fabfaf8a285f36b157625 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 8 May 2025 23:18:46 +0200 Subject: [PATCH 0124/1056] feat(vscode): go to definition for standalone audits (#4344) --- .gitignore | 3 +++ sqlmesh/lsp/completions.py | 46 ++++++++++++++++++++++---------- sqlmesh/lsp/context.py | 43 ++++++++++++++++++++++++++---- sqlmesh/lsp/main.py | 14 +++++++--- sqlmesh/lsp/reference.py | 52 +++++++++++++++++++++++++++---------- tests/lsp/test_context.py | 7 +++-- tests/lsp/test_reference.py | 46 +++++++++++++++++++++++++++++--- 7 files changed, 168 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index 6305387c02..563f2013f2 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,6 @@ tests/_version.py # spark metastore_db/ spark-warehouse/ + +# claude +.claude/ \ No newline at end of file diff --git a/sqlmesh/lsp/completions.py b/sqlmesh/lsp/completions.py index 6c0c172815..f27c48a4c5 100644 --- a/sqlmesh/lsp/completions.py +++ b/sqlmesh/lsp/completions.py @@ -2,7 +2,7 @@ from sqlglot import Dialect, Tokenizer from sqlmesh.lsp.custom import AllModelsResponse import typing as t -from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.context import AuditTarget, LSPContext, ModelTarget def get_sql_completions(context: t.Optional[LSPContext], file_uri: str) -> AllModelsResponse: @@ -24,11 +24,20 @@ def get_models(context: t.Optional[LSPContext], file_uri: t.Optional[str]) -> t. """ if context is None: return set() - all_models = set(model for models in context.map.values() for model in models) - if file_uri is not None: - models_file_refers_to = context.map[file_uri] - for model in models_file_refers_to: - all_models.discard(model) + + all_models = set() + # Extract model names from ModelInfo objects + for file_info in context.map.values(): + if isinstance(file_info, ModelTarget): + all_models.update(file_info.names) + + # Remove models from the current file + if file_uri is not None and file_uri in context.map: + file_info = context.map[file_uri] + if isinstance(file_info, ModelTarget): + for model in file_info.names: + all_models.discard(model) + return all_models @@ -43,16 +52,25 @@ def get_keywords(context: t.Optional[LSPContext], file_uri: t.Optional[str]) -> If both a context and a file_uri are provided, returns the keywords for the dialect of the model that the file belongs to. """ - if file_uri is not None and context is not None: - models = context.map[file_uri] - if models: - model = models[0] - model_from_context = context.context.get_model(model) - if model_from_context is not None: - if model_from_context.dialect: - return get_keywords_from_tokenizer(model_from_context.dialect) + if file_uri is not None and context is not None and file_uri in context.map: + file_info = context.map[file_uri] + + # Handle ModelInfo objects + if isinstance(file_info, ModelTarget) and file_info.names: + model_name = file_info.names[0] + model_from_context = context.context.get_model(model_name) + if model_from_context is not None and model_from_context.dialect: + return get_keywords_from_tokenizer(model_from_context.dialect) + + # Handle AuditInfo objects + elif isinstance(file_info, AuditTarget) and file_info.name: + audit = context.context.standalone_audits.get(file_info.name) + if audit is not None and audit.dialect: + return get_keywords_from_tokenizer(audit.dialect) + if context is not None: return get_keywords_from_tokenizer(context.context.default_dialect) + return get_keywords_from_tokenizer(None) diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 6a44ef32f3..e8139465b3 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -1,20 +1,53 @@ -from collections import defaultdict +from dataclasses import dataclass from pathlib import Path from sqlmesh.core.context import Context import typing as t +@dataclass +class ModelTarget: + """Information about models in a file.""" + + names: t.List[str] + + +@dataclass +class AuditTarget: + """Information about standalone audits in a file.""" + + name: str + + class LSPContext: """ - A context that is used for linting. It contains the context and a reverse map of file uri to model names . + A context that is used for linting. It contains the context and a reverse map of file uri to + model names and standalone audit names. """ def __init__(self, context: Context) -> None: self.context = context - map: t.Dict[str, t.List[str]] = defaultdict(list) + + # Add models to the map + model_map: t.Dict[str, ModelTarget] = {} for model in context.models.values(): if model._path is not None: path = Path(model._path).resolve() - map[f"file://{path.as_posix()}"].append(model.name) + uri = f"file://{path.as_posix()}" + if uri in model_map: + model_map[uri].names.append(model.name) + else: + model_map[uri] = ModelTarget(names=[model.name]) + + # Add standalone audits to the map + audit_map: t.Dict[str, AuditTarget] = {} + for audit in context.standalone_audits.values(): + if audit._path is not None: + path = Path(audit._path).resolve() + uri = f"file://{path.as_posix()}" + if uri not in audit_map: + audit_map[uri] = AuditTarget(name=audit.name) - self.map = map + self.map: t.Dict[str, t.Union[ModelTarget, AuditTarget]] = { + **model_map, + **audit_map, + } diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index d890caf9e8..cfcfddb0f3 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -12,7 +12,7 @@ from sqlmesh.core.context import Context from sqlmesh.core.linter.definition import AnnotatedRuleViolation from sqlmesh.lsp.completions import get_sql_completions -from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.context import LSPContext, ModelTarget from sqlmesh.lsp.custom import ALL_MODELS_FEATURE, AllModelsRequest, AllModelsResponse from sqlmesh.lsp.reference import get_model_definitions_for_a_path @@ -91,8 +91,10 @@ def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> Non models = context.map[params.text_document.uri] if models is None: return + if not isinstance(models, ModelTarget): + return self.lint_cache[params.text_document.uri] = context.context.lint_models( - models, + models.names, raise_on_error=False, ) ls.publish_diagnostics( @@ -108,8 +110,10 @@ def did_change(ls: LanguageServer, params: types.DidChangeTextDocumentParams) -> models = context.map[params.text_document.uri] if models is None: return + if not isinstance(models, ModelTarget): + return self.lint_cache[params.text_document.uri] = context.context.lint_models( - models, + models.names, raise_on_error=False, ) ls.publish_diagnostics( @@ -125,8 +129,10 @@ def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> Non models = context.map[params.text_document.uri] if models is None: return + if not isinstance(models, ModelTarget): + return self.lint_cache[params.text_document.uri] = context.context.lint_models( - models, + models.names, raise_on_error=False, ) ls.publish_diagnostics( diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 1a957d6d9d..84fffbe63e 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -3,7 +3,7 @@ from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core.model.definition import SqlModel -from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget from sqlglot import exp from sqlmesh.utils.pydantic import PydanticModel @@ -20,7 +20,7 @@ def get_model_definitions_for_a_path( """ Get the model references for a given path. - Works for models and audits. + Works for models and standalone audits. Works for targeting sql and python models. Steps: @@ -31,39 +31,63 @@ def get_model_definitions_for_a_path( - Try get_model before normalization - Match to models that the model refers to """ - # Ensure the path is a sql model + # Ensure the path is a sql file if not document_uri.endswith(".sql"): return [] - # Get the model - models = lint_context.map[document_uri] - if not models: + # Get the file info from the context map + if document_uri not in lint_context.map: return [] - model = lint_context.context.get_model(model_or_snapshot=models[0], raise_if_missing=False) - if model is None or not isinstance(model, SqlModel): + + file_info = lint_context.map[document_uri] + + # Process based on whether it's a model or standalone audit + if isinstance(file_info, ModelTarget): + # It's a model + model = lint_context.context.get_model( + model_or_snapshot=file_info.names[0], raise_if_missing=False + ) + if model is None or not isinstance(model, SqlModel): + return [] + + query = model.query + dialect = model.dialect + depends_on = model.depends_on + file_path = model._path + elif isinstance(file_info, AuditTarget): + # It's a standalone audit + audit = lint_context.context.standalone_audits.get(file_info.name) + if audit is None: + return [] + + query = audit.query + dialect = audit.dialect + depends_on = audit.depends_on + file_path = audit._path + else: return [] # Find all possible references references = [] - tables = list(model.query.find_all(exp.Table)) + + # Get SQL query and find all table references + tables = list(query.find_all(exp.Table)) if len(tables) == 0: return [] - read_file = open(model._path, "r").readlines() + read_file = open(file_path, "r").readlines() for table in tables: - depends_on = model.depends_on - # Normalize the table reference unaliased = table.copy() if unaliased.args.get("alias") is not None: unaliased.set("alias", None) - reference_name = unaliased.sql(dialect=model.dialect) + reference_name = unaliased.sql(dialect=dialect) try: normalized_reference_name = normalize_model_name( reference_name, default_catalog=lint_context.context.default_catalog, - dialect=model.dialect, + dialect=dialect, ) if normalized_reference_name not in depends_on: continue diff --git a/tests/lsp/test_context.py b/tests/lsp/test_context.py index 4bfd094cb3..a27a5a6a78 100644 --- a/tests/lsp/test_context.py +++ b/tests/lsp/test_context.py @@ -1,6 +1,6 @@ import pytest from sqlmesh.core.context import Context -from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.context import LSPContext, ModelTarget @pytest.mark.fast @@ -16,4 +16,7 @@ def test_lsp_context(): active_customers_key = next( key for key in lsp_context.map.keys() if key.endswith("models/active_customers.sql") ) - assert lsp_context.map[active_customers_key] == ["sushi.active_customers"] + + # Check that the value is a ModelInfo with the expected model name + assert isinstance(lsp_context.map[active_customers_key], ModelTarget) + assert "sushi.active_customers" in lsp_context.map[active_customers_key].names diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index a01599400d..ae0b4c899d 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -1,6 +1,6 @@ import pytest from sqlmesh.core.context import Context -from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget from sqlmesh.lsp.reference import get_model_definitions_for_a_path @@ -9,11 +9,16 @@ def test_reference() -> None: context = Context(paths=["examples/sushi"]) lsp_context = LSPContext(context) + # Find model URIs active_customers_uri = next( - uri for uri, models in lsp_context.map.items() if "sushi.active_customers" in models + uri + for uri, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.active_customers" in info.names ) sushi_customers_uri = next( - uri for uri, models in lsp_context.map.items() if "sushi.customers" in models + uri + for uri, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names ) references = get_model_definitions_for_a_path(lsp_context, active_customers_uri) @@ -35,7 +40,9 @@ def test_reference_with_alias() -> None: lsp_context = LSPContext(context) waiter_revenue_by_day_uri = next( - uri for uri, models in lsp_context.map.items() if "sushi.waiter_revenue_by_day" in models + uri + for uri, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.waiter_revenue_by_day" in info.names ) references = get_model_definitions_for_a_path(lsp_context, waiter_revenue_by_day_uri) @@ -52,6 +59,37 @@ def test_reference_with_alias() -> None: assert get_string_from_range(read_file, references[2].range) == "sushi.items" +@pytest.mark.fast +def test_standalone_audit_reference() -> None: + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Find the standalone audit URI + audit_uri = next( + uri + for uri, info in lsp_context.map.items() + if isinstance(info, AuditTarget) and info.name == "assert_item_price_above_zero" + ) + + # Find the items model URI + items_uri = next( + uri + for uri, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.items" in info.names + ) + + references = get_model_definitions_for_a_path(lsp_context, audit_uri) + + assert len(references) == 1 + assert references[0].uri == items_uri + + # Check that the reference in the correct range is sushi.items + path = audit_uri.removeprefix("file://") + read_file = open(path, "r").readlines() + referenced_text = get_string_from_range(read_file, references[0].range) + assert referenced_text == "sushi.items" + + def get_string_from_range(file_lines, range_obj) -> str: start_line = range_obj.start.line end_line = range_obj.end.line From a83740927ca9257fe2e383cc37c402fe7c7536e4 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 8 May 2025 16:06:05 -0700 Subject: [PATCH 0125/1056] Fix: Include the module path when diffing python code (#4346) --- sqlmesh/core/model/common.py | 14 ++++++++++---- tests/core/test_model.py | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index 61d331f306..310883fff7 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -361,10 +361,16 @@ def sort_python_env(python_env: t.Dict[str, Executable]) -> t.List[t.Tuple[str, def sorted_python_env_payloads(python_env: t.Dict[str, Executable]) -> t.List[str]: """Returns the payloads of the sorted python env.""" - return [ - v.payload if v.is_import or v.is_definition else f"{k} = {v.payload}" - for k, v in sort_python_env(python_env) - ] + + def _executable_to_str(k: str, v: Executable) -> str: + result = f"# {v.path}\n" if v.path is not None else "" + if v.is_import or v.is_definition: + result += v.payload + else: + result += f"{k} = {v.payload}" + return result + + return [_executable_to_str(k, v) for k, v in sort_python_env(python_env)] expression_validator: t.Callable = field_validator( diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 7cf9173cb3..9565d6d9fb 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9702,3 +9702,26 @@ def test_scd_time_data_type_does_not_cause_diff_after_deserialization() -> None: deserialized_model = SqlModel.parse_raw(model.json()) assert model.data_hash == deserialized_model.data_hash + + +def test_python_env_includes_file_path_in_render_definition(): + @model( + "db.test_model_path", + kind=dict( + name=ModelKindName.SCD_TYPE_2_BY_TIME, + unique_key=["id"], + disable_restatement=False, + ), + columns={"id": "string", "name": "string"}, + optimize_query=False, + ) + def test_model(context, **kwargs): + return pd.DataFrame([{"id": context.var("1")}]) + + python_model = model.get_registry()["db.test_model_path"].model( + module_path=Path("."), path=Path("."), dialect="duckdb" + ) + + model_executable_str = python_model.render_definition()[1].sql() + # Make sure the file path is included in the render definition + assert "# tests/core/test_model.py" in model_executable_str From a5e67d0db89e9cd6f273c873f151e8ccc7142a93 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 9 May 2025 08:13:45 +0300 Subject: [PATCH 0126/1056] Fix: Adjust condition in table diff to filter in forward only models (#4340) --- sqlmesh/core/context.py | 6 +-- tests/core/test_table_diff.py | 93 +++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 1328b8fea5..30df773e19 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1677,9 +1677,9 @@ def table_diff( target_snapshot = target_snapshots_to_name.get(model.fqn) if target_snapshot and source_snapshot: - if ( - source_snapshot.fingerprint.data_hash - != target_snapshot.fingerprint.data_hash + if (source_snapshot.fingerprint != target_snapshot.fingerprint) and ( + (source_snapshot.version != target_snapshot.version) + or (source_snapshot.is_forward_only or target_snapshot.is_forward_only) ): # Compare the virtual layer instead of the physical layer because the virtual layer is guaranteed to point # to the correct/active snapshot for the model in the specified environment, taking into account things like dev previews diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index 11a98524e8..e0bdbe51d7 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -695,3 +695,96 @@ def test_data_diff_multiple_models(sushi_context_fixed_date, capsys, caplog): skip_grain_check=False, ) assert len(diffs) == 0 + + +@pytest.mark.slow +def test_data_diff_forward_only(sushi_context_fixed_date, capsys, caplog): + expressions = d.parse( + """ + MODEL (name memory.sushi.full_1, kind full, grain(key),); + SELECT + key, + value, + FROM + (VALUES + (1, 3), + (2, 4), + ) AS t (key, value) + """ + ) + model_s = load_sql_based_model(expressions, dialect="snowflake") + sushi_context_fixed_date.upsert_model(model_s) + + # Create second analytics model sourcing from first + expressions_2 = d.parse( + """ + MODEL (name memory.sushi.full_2, kind full, grain(key),); + SELECT + key, + value as amount, + FROM + memory.sushi.full_1 + """ + ) + model_s2 = load_sql_based_model(expressions_2, dialect="snowflake") + sushi_context_fixed_date.upsert_model(model_s2) + + sushi_context_fixed_date.plan( + "target_dev", + no_prompts=True, + auto_apply=True, + skip_tests=True, + start="2023-01-31", + end="2023-01-31", + ) + + model = sushi_context_fixed_date.models['"MEMORY"."SUSHI"."FULL_1"'] + modified_model = model.dict() + modified_model["query"] = exp.select("*").from_("(VALUES (12, 6),(5,3),) AS t (key, value)") + modified_sqlmodel = SqlModel(**modified_model) + sushi_context_fixed_date.upsert_model(modified_sqlmodel) + + sushi_context_fixed_date.auto_categorize_changes = CategorizerConfig( + sql=AutoCategorizationMode.FULL + ) + + plan_builder = sushi_context_fixed_date.plan_builder( + "source_dev", skip_tests=True, forward_only=True + ) + plan = plan_builder.build() + + sushi_context_fixed_date.apply(plan) + + # Get diffs for both models + selector = {"*full*"} + diffs = sushi_context_fixed_date.table_diff( + source="source_dev", + target="target_dev", + on=["key"], + select_models=selector, + skip_grain_check=False, + ) + + # Both models should be diffed + assert len(diffs) == 2 + + # Check full_1 diff + diff1 = next(d for d in diffs if "FULL_1" in d.source) + row_diff1 = diff1.row_diff() + diff2 = next(d for d in diffs if "FULL_2" in d.source) + row_diff2 = diff2.row_diff() + + # Both diffs should show the same matches + for row_diff in [row_diff1, row_diff2]: + assert row_diff.full_match_count == 0 + assert row_diff.full_match_pct == 0.0 + assert row_diff.s_only_count == 2 + assert row_diff.t_only_count == 2 + assert row_diff.stats["join_count"] == 0 + assert row_diff.stats["null_grain_count"] == 0 + assert row_diff.stats["s_count"] == 2 + assert row_diff.stats["distinct_count_s"] == 2 + assert row_diff.stats["t_count"] == 2 + assert row_diff.stats["distinct_count_t"] == 2 + assert row_diff.s_sample.shape == (2, 2) + assert row_diff.t_sample.shape == (2, 2) From d53e787827230afa73c7a0a7d8adcabc5d9d7044 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 9 May 2025 10:00:19 +0200 Subject: [PATCH 0127/1056] fix(vscode): filter go to definitions by position (#4345) Co-authored-by: Claude --- sqlmesh/lsp/main.py | 11 +++--- sqlmesh/lsp/reference.py | 33 +++++++++++++++++ tests/lsp/test_reference.py | 70 ++++++++++++++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index cfcfddb0f3..4c1fd9b98a 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -14,7 +14,10 @@ from sqlmesh.lsp.completions import get_sql_completions from sqlmesh.lsp.context import LSPContext, ModelTarget from sqlmesh.lsp.custom import ALL_MODELS_FEATURE, AllModelsRequest, AllModelsResponse -from sqlmesh.lsp.reference import get_model_definitions_for_a_path +from sqlmesh.lsp.reference import ( + get_model_definitions_for_a_path, + filter_references_by_position, +) class SQLMeshLanguageServer: @@ -189,9 +192,7 @@ def goto_definition( references = get_model_definitions_for_a_path( self.lsp_context, params.text_document.uri ) - if not references: - return [] - + filtered_references = filter_references_by_position(references, params.position) return [ types.LocationLink( target_uri=reference.uri, @@ -205,7 +206,7 @@ def goto_definition( ), origin_selection_range=reference.range, ) - for reference in references + for reference in filtered_references ] except Exception as e: diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 84fffbe63e..a9fbf84109 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -14,6 +14,39 @@ class Reference(PydanticModel): uri: str +def filter_references_by_position( + references: t.List[Reference], position: Position +) -> t.List[Reference]: + """ + Filter references to only include those that contain the given position. + + Args: + references: List of Reference objects + position: The cursor position to check + + Returns: + List of Reference objects that contain the position + """ + filtered_references = [] + + for reference in references: + # Check if position is within the reference range + range_start = reference.range.start + range_end = reference.range.end + + # Position is within range if it's after or at start and before or at end + if ( + range_start.line < position.line + or (range_start.line == position.line and range_start.character <= position.character) + ) and ( + range_end.line > position.line + or (range_end.line == position.line and range_end.character >= position.character) + ): + filtered_references.append(reference) + + return filtered_references + + def get_model_definitions_for_a_path( lint_context: LSPContext, document_uri: str ) -> t.List[Reference]: diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index ae0b4c899d..d8e9abe7a4 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -1,7 +1,8 @@ import pytest +from lsprotocol.types import Position from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget -from sqlmesh.lsp.reference import get_model_definitions_for_a_path +from sqlmesh.lsp.reference import get_model_definitions_for_a_path, filter_references_by_position @pytest.mark.fast @@ -108,3 +109,70 @@ def get_string_from_range(file_lines, range_obj) -> str: result += file_lines[line_num] result += file_lines[end_line][:end_character] # Last line up to end_character return result + + +@pytest.mark.fast +def test_filter_references_by_position() -> None: + """Test that we can filter references correctly based on cursor position.""" + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Use a file with multiple references (waiter_revenue_by_day) + waiter_revenue_by_day_uri = next( + uri + for uri, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.waiter_revenue_by_day" in info.names + ) + + # Get all references in the file + all_references = get_model_definitions_for_a_path(lsp_context, waiter_revenue_by_day_uri) + assert len(all_references) == 3 + + # Get file contents to locate positions for testing + path = waiter_revenue_by_day_uri.removeprefix("file://") + with open(path, "r") as file: + read_file = file.readlines() + + # Test positions for each reference + for i, reference in enumerate(all_references): + # Position inside the reference - should return exactly one reference + middle_line = (reference.range.start.line + reference.range.end.line) // 2 + middle_char = (reference.range.start.character + reference.range.end.character) // 2 + position_inside = Position(line=middle_line, character=middle_char) + filtered = filter_references_by_position(all_references, position_inside) + assert len(filtered) == 1 + assert filtered[0].uri == reference.uri + assert filtered[0].range == reference.range + + # For testing outside position, use a position before the current reference + # or after the last reference for the last one + if i == 0: + outside_line = reference.range.start.line + outside_char = max(0, reference.range.start.character - 5) + else: + prev_ref = all_references[i - 1] + outside_line = prev_ref.range.end.line + outside_char = prev_ref.range.end.character + 5 + + position_outside = Position(line=outside_line, character=outside_char) + filtered_outside = filter_references_by_position(all_references, position_outside) + assert reference not in filtered_outside, ( + f"Reference {i} should not match position outside its range" + ) + + # Test case: cursor at beginning of file - no references should match + position_start = Position(line=0, character=0) + filtered_start = filter_references_by_position(all_references, position_start) + assert len(filtered_start) == 0 or all( + ref.range.start.line == 0 and ref.range.start.character <= 0 for ref in filtered_start + ) + + # Test case: cursor at end of file - no references should match (unless there's a reference at the end) + last_line = len(read_file) - 1 + last_char = len(read_file[last_line]) - 1 + position_end = Position(line=last_line, character=last_char) + filtered_end = filter_references_by_position(all_references, position_end) + assert len(filtered_end) == 0 or all( + ref.range.end.line >= last_line and ref.range.end.character >= last_char + for ref in filtered_end + ) From 00cc5ed6d889b2177dcdadffe0374482c88abcfa Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 9 May 2025 16:03:05 +0300 Subject: [PATCH 0128/1056] Fix: databricks timestampntz handling (#4350) --- sqlmesh/core/engine_adapter/spark.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sqlmesh/core/engine_adapter/spark.py b/sqlmesh/core/engine_adapter/spark.py index da55d21d19..367bd5d4af 100644 --- a/sqlmesh/core/engine_adapter/spark.py +++ b/sqlmesh/core/engine_adapter/spark.py @@ -105,6 +105,7 @@ def _sqlglot_to_spark_primitive_mapping(self) -> t.Dict[t.Any, t.Any]: exp.DataType.Type.BINARY: spark_types.BinaryType, exp.DataType.Type.BOOLEAN: spark_types.BooleanType, exp.DataType.Type.DATE: spark_types.DateType, + exp.DataType.Type.TIMESTAMPNTZ: spark_types.TimestampNTZType, exp.DataType.Type.DATETIME: spark_types.TimestampNTZType, exp.DataType.Type.TIMESTAMPLTZ: spark_types.TimestampType, exp.DataType.Type.TIMESTAMP: spark_types.TimestampType, From d7b186641bcb45e3cd98a143115c3f373fd05e05 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 9 May 2025 15:19:04 +0200 Subject: [PATCH 0129/1056] feat: add on hover to models on lsp (#4351) --- sqlmesh/lsp/main.py | 39 +++++++++++++++++---- sqlmesh/lsp/reference.py | 70 ++++++++++++++++++++++++++----------- tests/lsp/test_reference.py | 11 +++--- 3 files changed, 87 insertions(+), 33 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 4c1fd9b98a..9a4d5ec802 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -15,8 +15,7 @@ from sqlmesh.lsp.context import LSPContext, ModelTarget from sqlmesh.lsp.custom import ALL_MODELS_FEATURE, AllModelsRequest, AllModelsResponse from sqlmesh.lsp.reference import ( - get_model_definitions_for_a_path, - filter_references_by_position, + get_references, ) @@ -178,6 +177,34 @@ def formatting( ls.show_message(f"Error formatting SQL: {e}", types.MessageType.Error) return [] + @self.server.feature(types.TEXT_DOCUMENT_HOVER) + def hover(ls: LanguageServer, params: types.HoverParams) -> t.Optional[types.Hover]: + """Provide hover information for an object.""" + try: + self._ensure_context_for_document(params.text_document.uri) + document = ls.workspace.get_document(params.text_document.uri) + if self.lsp_context is None: + raise RuntimeError(f"No context found for document: {document.path}") + + references = get_references( + self.lsp_context, params.text_document.uri, params.position + ) + if not references: + return None + reference = references[0] + if not reference.description: + return None + return types.Hover( + contents=types.MarkupContent( + kind=types.MarkupKind.Markdown, value=reference.description + ), + range=reference.range, + ) + + except Exception as e: + ls.show_message(f"Error getting hover information: {e}", types.MessageType.Error) + return None + @self.server.feature(types.TEXT_DOCUMENT_DEFINITION) def goto_definition( ls: LanguageServer, params: types.DefinitionParams @@ -189,10 +216,9 @@ def goto_definition( if self.lsp_context is None: raise RuntimeError(f"No context found for document: {document.path}") - references = get_model_definitions_for_a_path( - self.lsp_context, params.text_document.uri + references = get_references( + self.lsp_context, params.text_document.uri, params.position ) - filtered_references = filter_references_by_position(references, params.position) return [ types.LocationLink( target_uri=reference.uri, @@ -206,9 +232,8 @@ def goto_definition( ), origin_selection_range=reference.range, ) - for reference in filtered_references + for reference in references ] - except Exception as e: ls.show_message(f"Error getting references: {e}", types.MessageType.Error) return [] diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index a9fbf84109..51b15c3718 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -10,40 +10,64 @@ class Reference(PydanticModel): + """ + A reference to a model. + + Attributes: + range: The range of the reference in the source file + uri: The uri of the referenced model + description: The description of the referenced model + """ + range: Range uri: str + description: t.Optional[str] = None -def filter_references_by_position( - references: t.List[Reference], position: Position -) -> t.List[Reference]: +def by_position(position: Position) -> t.Callable[[Reference], bool]: """ - Filter references to only include those that contain the given position. + Filter reference to only filter references that contain the given position. Args: - references: List of Reference objects position: The cursor position to check Returns: - List of Reference objects that contain the position + A function that returns True if the reference contains the position, False otherwise """ - filtered_references = [] - - for reference in references: - # Check if position is within the reference range - range_start = reference.range.start - range_end = reference.range.end - # Position is within range if it's after or at start and before or at end - if ( - range_start.line < position.line - or (range_start.line == position.line and range_start.character <= position.character) + def contains_position(r: Reference) -> bool: + return ( + r.range.start.line < position.line + or ( + r.range.start.line == position.line + and r.range.start.character <= position.character + ) ) and ( - range_end.line > position.line - or (range_end.line == position.line and range_end.character >= position.character) - ): - filtered_references.append(reference) + r.range.end.line > position.line + or (r.range.end.line == position.line and r.range.end.character >= position.character) + ) + + return contains_position + +def get_references( + lint_context: LSPContext, document_uri: str, position: Position +) -> t.List[Reference]: + """ + Get references at a specific position in a document. + + Used for hover information. + + Args: + lint_context: The LSP context + document_uri: The URI of the document + position: The position to check for references + + Returns: + A list of references at the given position + """ + references = get_model_definitions_for_a_path(lint_context, document_uri) + filtered_references = list(filter(by_position(position), references)) return filtered_references @@ -154,7 +178,11 @@ def get_model_definitions_for_a_path( start_pos = catalog_or_db_range.start references.append( - Reference(uri=referenced_model_uri, range=Range(start=start_pos, end=end_pos)) + Reference( + uri=referenced_model_uri, + range=Range(start=start_pos, end=end_pos), + description=referenced_model.description, + ) ) return references diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index d8e9abe7a4..7db39c54a8 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -2,7 +2,7 @@ from lsprotocol.types import Position from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget -from sqlmesh.lsp.reference import get_model_definitions_for_a_path, filter_references_by_position +from sqlmesh.lsp.reference import get_model_definitions_for_a_path, by_position @pytest.mark.fast @@ -54,6 +54,7 @@ def test_reference_with_alias() -> None: assert references[0].uri.endswith("orders.py") assert get_string_from_range(read_file, references[0].range) == "sushi.orders" + assert references[0].description == "Table of sushi orders." assert references[1].uri.endswith("order_items.py") assert get_string_from_range(read_file, references[1].range) == "sushi.order_items" assert references[2].uri.endswith("items.py") @@ -139,7 +140,7 @@ def test_filter_references_by_position() -> None: middle_line = (reference.range.start.line + reference.range.end.line) // 2 middle_char = (reference.range.start.character + reference.range.end.character) // 2 position_inside = Position(line=middle_line, character=middle_char) - filtered = filter_references_by_position(all_references, position_inside) + filtered = list(filter(by_position(position_inside), all_references)) assert len(filtered) == 1 assert filtered[0].uri == reference.uri assert filtered[0].range == reference.range @@ -155,14 +156,14 @@ def test_filter_references_by_position() -> None: outside_char = prev_ref.range.end.character + 5 position_outside = Position(line=outside_line, character=outside_char) - filtered_outside = filter_references_by_position(all_references, position_outside) + filtered_outside = list(filter(by_position(position_outside), all_references)) assert reference not in filtered_outside, ( f"Reference {i} should not match position outside its range" ) # Test case: cursor at beginning of file - no references should match position_start = Position(line=0, character=0) - filtered_start = filter_references_by_position(all_references, position_start) + filtered_start = list(filter(by_position(position_start), all_references)) assert len(filtered_start) == 0 or all( ref.range.start.line == 0 and ref.range.start.character <= 0 for ref in filtered_start ) @@ -171,7 +172,7 @@ def test_filter_references_by_position() -> None: last_line = len(read_file) - 1 last_char = len(read_file[last_line]) - 1 position_end = Position(line=last_line, character=last_char) - filtered_end = filter_references_by_position(all_references, position_end) + filtered_end = list(filter(by_position(position_end), all_references)) assert len(filtered_end) == 0 or all( ref.range.end.line >= last_line and ref.range.end.character >= last_char for ref in filtered_end From 9fb24040af797e1658cbbff1edbe65f98ca650eb Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 9 May 2025 16:20:25 +0300 Subject: [PATCH 0130/1056] Fix: Extend the debugger console with the diff methods (#4349) --- sqlmesh/core/console.py | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 167561a671..24030a9b8e 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -3472,6 +3472,53 @@ def show_row_diff( ) -> None: self._write(row_diff) + def show_table_diff( + self, + table_diffs: t.List[TableDiff], + show_sample: bool = True, + skip_grain_check: bool = False, + temp_schema: t.Optional[str] = None, + ) -> None: + for table_diff in table_diffs: + self.show_table_diff_summary(table_diff) + self.show_schema_diff(table_diff.schema_diff()) + self.show_row_diff( + table_diff.row_diff(temp_schema=temp_schema, skip_grain_check=skip_grain_check), + show_sample=show_sample, + skip_grain_check=skip_grain_check, + ) + + def update_table_diff_progress(self, model: str) -> None: + self._write(f"Finished table diff for: {model}") + + def start_table_diff_progress(self, models_to_diff: int) -> None: + self._write("Table diff started") + + def start_table_diff_model_progress(self, model: str) -> None: + self._write(f"Calculating differences for: {model}") + + def stop_table_diff_progress(self, success: bool) -> None: + self._write(f"Table diff finished with success={success}") + + def show_table_diff_details( + self, + models_to_diff: t.List[str], + ) -> None: + if models_to_diff: + models = "\n".join(models_to_diff) + self._write(f"Models to compare: {models}") + + def show_table_diff_summary(self, table_diff: TableDiff) -> None: + if table_diff.model_name: + self._write(f"Model: {table_diff.model_name}") + self._write(f"Source env: {table_diff.source_alias}") + self._write(f"Target env: {table_diff.target_alias}") + self._write(f"Source table: {table_diff.source}") + self._write(f"Target table: {table_diff.target}") + _, _, key_column_names = table_diff.key_columns + keys = ", ".join(key_column_names) + self._write(f"Join On: {keys}") + _CONSOLE: Console = NoopConsole() From 5b2fa5b2fb94f5f6870de33649c340a4066651d9 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 9 May 2025 19:13:45 +0300 Subject: [PATCH 0131/1056] Feat: Add destroy command to remove entire project resources (#4328) --- docs/reference/cli.md | 12 +++ docs/reference/notebook.md | 7 ++ sqlmesh/cli/main.py | 13 +++ sqlmesh/core/console.py | 46 +++++++++ sqlmesh/core/context.py | 29 ++++++ sqlmesh/core/state_sync/base.py | 7 +- sqlmesh/core/state_sync/db/environment.py | 5 +- sqlmesh/core/state_sync/db/facade.py | 18 +++- sqlmesh/magics.py | 7 ++ tests/core/test_integration.py | 119 ++++++++++++++++++++++ tests/integrations/jupyter/test_magics.py | 36 +++++++ 11 files changed, 291 insertions(+), 8 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d7b578116c..c28f4f4184 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -23,6 +23,7 @@ Commands: create_external_models Create a schema file containing external model... create_test Generate a unit test fixture for a given model. dag Render the DAG as an html file. + destroy The destroy command removes all project resources. diff Show the diff between the local state and the... dlt_refresh Attaches to a DLT pipeline with the option to... environments Prints the list of SQLMesh environments with... @@ -143,6 +144,17 @@ Options: --help Show this message and exit. ``` +## destroy + +``` +Usage: sqlmesh destroy + + Removes all project resources, including warehouse objects, state tables, the SQLMesh cache and any build artifacts. + +Options: + --help Show this message and exit. +``` + ## dlt_refresh ``` diff --git a/docs/reference/notebook.md b/docs/reference/notebook.md index 313b7295ee..1e4e9d84d3 100644 --- a/docs/reference/notebook.md +++ b/docs/reference/notebook.md @@ -232,6 +232,13 @@ options: --file FILE, -f FILE An optional file path to write the HTML output to. ``` +#### destroy +``` +%destroy + +Removes all project resources, including warehouse objects, state tables, the SQLMesh cache and any build artifacts. +``` + #### dlt_refresh ``` %dlt_refresh PIPELINE [--table] TABLE [--force] diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index c230d78896..c60506d023 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -546,6 +546,19 @@ def janitor(ctx: click.Context, ignore_ttl: bool, **kwargs: t.Any) -> None: ctx.obj.run_janitor(ignore_ttl, **kwargs) +@cli.command("destroy") +@click.pass_context +@error_handler +@cli_analytics +def destroy(ctx: click.Context, **kwargs: t.Any) -> None: + """ + The destroy command removes all project resources. + + This includes engine-managed objects, state tables, the SQLMesh cache and any build artifacts. + """ + ctx.obj.destroy(**kwargs) + + @cli.command("dag") @click.argument("file", required=True) @click.option( diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 24030a9b8e..ad28d4ddd2 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -181,6 +181,26 @@ def stop_cleanup(self, success: bool = True) -> None: """ +class DestroyConsole(abc.ABC): + """Console for describing a destroy operation""" + + @abc.abstractmethod + def start_destroy(self) -> bool: + """Start a destroy operation. + + Returns: + Whether or not the destroy operation should proceed + """ + + @abc.abstractmethod + def stop_destroy(self, success: bool = True) -> None: + """Indicates the destroy operation has ended + + Args: + success: Whether or not the cleanup completed successfully + """ + + class EnvironmentsConsole(abc.ABC): """Console for displaying environments""" @@ -304,6 +324,7 @@ class Console( StateExporterConsole, StateImporterConsole, JanitorConsole, + DestroyConsole, EnvironmentsConsole, DifferenceConsole, TableDiffConsole, @@ -744,6 +765,12 @@ def print_connection_config( ) -> None: pass + def start_destroy(self) -> bool: + return True + + def stop_destroy(self, success: bool = True) -> None: + pass + def make_progress_bar( message: str, @@ -1092,6 +1119,25 @@ def stop_cleanup(self, success: bool = False) -> None: else: self.log_error("Cleanup failed!") + def start_destroy(self) -> bool: + self.log_warning( + ( + "This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n" + "The operation is irreversible and may disrupt any currently running or scheduled plans.\n" + "Use this command only when you intend to fully reset the project." + ) + ) + if not self._confirm("Proceed?"): + self.log_error("Destroy aborted!") + return False + return True + + def stop_destroy(self, success: bool = False) -> None: + if success: + self.log_success("Destroy completed successfully.") + else: + self.log_error("Destroy failed!") + def start_promotion_progress( self, snapshots: t.List[SnapshotTableInfo], diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 30df773e19..bd4de951bd 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -832,6 +832,19 @@ def run_janitor(self, ignore_ttl: bool) -> bool: return success + @python_api_analytics + def destroy(self) -> bool: + success = False + + if self.console.start_destroy(): + try: + self._destroy() + success = True + finally: + self.console.stop_destroy(success=success) + + return success + @t.overload def get_model( self, model_or_snapshot: ModelOrSnapshot, raise_if_missing: Literal[True] = True @@ -2537,6 +2550,22 @@ def _context_diff( gateway_managed_virtual_layer=self.gateway_managed_virtual_layer, ) + def _destroy(self) -> None: + # Invalidate all environments, including prod + for environment in self.state_reader.get_environments(): + self.state_sync.invalidate_environment(name=environment.name, protect_prod=False) + self.console.log_success(f"Environment '{environment.name}' invalidated.") + + # Run janitor to clean up all objects + self._run_janitor(ignore_ttl=True) + + # Remove state tables, including backup tables + self.state_sync.remove_state(including_backup=True) + self.console.log_status_update("State tables removed.") + + # Finally clear caches + self.clear_caches() + def _run_janitor(self, ignore_ttl: bool = False) -> None: current_ts = now_timestamp() diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index 84bf550906..3f9f43343d 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -347,13 +347,18 @@ def delete_expired_snapshots( """ @abc.abstractmethod - def invalidate_environment(self, name: str) -> None: + def invalidate_environment(self, name: str, protect_prod: bool = True) -> None: """Invalidates the target environment by setting its expiration timestamp to now. Args: name: The name of the environment to invalidate. + protect_prod: If True, prevents invalidation of the production environment. """ + @abc.abstractmethod + def remove_state(self, including_backup: bool = False) -> None: + """Removes the state store objects.""" + @abc.abstractmethod def remove_intervals( self, diff --git a/sqlmesh/core/state_sync/db/environment.py b/sqlmesh/core/state_sync/db/environment.py index e98f1633f6..932545ef2f 100644 --- a/sqlmesh/core/state_sync/db/environment.py +++ b/sqlmesh/core/state_sync/db/environment.py @@ -108,14 +108,15 @@ def update_environment_statements( columns_to_types=self._environment_statements_columns_to_types, ) - def invalidate_environment(self, name: str) -> None: + def invalidate_environment(self, name: str, protect_prod: bool = True) -> None: """Invalidates the environment. Args: name: The name of the environment + protect_prod: If True, prevents invalidation of the production environment. """ name = name.lower() - if name == c.PROD: + if protect_prod and name == c.PROD: raise SQLMeshError("Cannot invalidate the production environment.") filter_expr = exp.column("name").eq(name) diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index e2a8523a9c..7cd5e5a624 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -59,7 +59,7 @@ from sqlmesh.core.state_sync.db.environment import EnvironmentState from sqlmesh.core.state_sync.db.snapshot import SnapshotState from sqlmesh.core.state_sync.db.version import VersionState -from sqlmesh.core.state_sync.db.migrator import StateMigrator +from sqlmesh.core.state_sync.db.migrator import StateMigrator, _backup_table_name from sqlmesh.utils.date import TimeLike, to_timestamp, time_like_to_str, now_timestamp from sqlmesh.utils.errors import ConflictingPlanError, SQLMeshError @@ -270,8 +270,8 @@ def unpause_snapshots( ) -> None: self.snapshot_state.unpause_snapshots(snapshots, unpaused_dt, self.interval_state) - def invalidate_environment(self, name: str) -> None: - self.environment_state.invalidate_environment(name) + def invalidate_environment(self, name: str, protect_prod: bool = True) -> None: + self.environment_state.invalidate_environment(name, protect_prod) def get_expired_snapshots( self, current_ts: int, ignore_ttl: bool = False @@ -313,18 +313,26 @@ def snapshots_exist(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> t.Set[Sna def nodes_exist(self, names: t.Iterable[str], exclude_external: bool = False) -> t.Set[str]: return self.snapshot_state.nodes_exist(names, exclude_external) - def reset(self, default_catalog: t.Optional[str]) -> None: - """Resets the state store to the state when it was first initialized.""" + def remove_state(self, including_backup: bool = False) -> None: + """Removes the state store objects.""" for table in ( self.snapshot_state.snapshots_table, self.snapshot_state.auto_restatements_table, self.environment_state.environments_table, + self.environment_state.environment_statements_table, self.interval_state.intervals_table, self.plan_dags_table, self.version_state.versions_table, ): self.engine_adapter.drop_table(table) + if including_backup: + self.engine_adapter.drop_table(_backup_table_name(table)) + self.snapshot_state.clear_cache() + + def reset(self, default_catalog: t.Optional[str]) -> None: + """Resets the state store to the state when it was first initialized.""" + self.remove_state() self.migrate(default_catalog) @transactional() diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 4b4ec7f23e..d6e540357c 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -1111,6 +1111,13 @@ def lint(self, context: Context, line: str) -> None: args = parse_argstring(self.lint, line) context.lint_models(args.models) + @magic_arguments() + @line_magic + @pass_sqlmesh_context + def destroy(self, context: Context, line: str) -> None: + """Removes all project resources, engine-managed objects, state tables and clears the SQLMesh cache.""" + context.destroy() + def register_magics() -> None: try: diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 1778b1e72c..1d276535c4 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -5922,3 +5922,122 @@ def setup_scenario(): # - Check that the environment record does not exist in the state sync anymore assert not ctx.state_sync.get_environment("dev") + + +@use_terminal_console +def test_destroy(copy_to_temp_path): + # Testing project with two gateways to verify cleanup is performed across engines + paths = copy_to_temp_path("tests/fixtures/multi_virtual_layer") + path = Path(paths[0]) + first_db_path = str(path / "db_1.db") + second_db_path = str(path / "db_2.db") + + config = Config( + gateways={ + "first": GatewayConfig( + connection=DuckDBConnectionConfig(database=first_db_path), + variables={"overriden_var": "gateway_1"}, + ), + "second": GatewayConfig( + connection=DuckDBConnectionConfig(database=second_db_path), + variables={"overriden_var": "gateway_2"}, + ), + }, + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + model_naming=NameInferenceConfig(infer_names=True), + default_gateway="first", + gateway_managed_virtual_layer=True, + variables={"overriden_var": "global", "global_one": 88}, + ) + + context = Context(paths=paths, config=config) + plan = context.plan_builder().build() + assert len(plan.new_snapshots) == 4 + context.apply(plan) + + # Confirm cache exists + cache_path = Path(path) / ".cache" + assert cache_path.exists() + assert len(list(cache_path.iterdir())) > 0 + + model = context.get_model("db_1.first_schema.model_one") + + context.upsert_model(model.copy(update={"query": model.query.select("'c' AS extra")})) + plan = context.plan_builder().build() + context.apply(plan) + + state_environments = context.state_reader.get_environments() + state_snapshots = context.state_reader.get_snapshots(context.snapshots.values()) + + assert len(state_snapshots) == len(state_environments[0].snapshots) + + # Create dev environment with changed models + model = context.get_model("db_2.second_schema.model_one") + context.upsert_model(model.copy(update={"query": model.query.select("'d' AS extra")})) + model = context.get_model("first_schema.model_two") + context.upsert_model(model.copy(update={"query": model.query.select("'d2' AS col")})) + plan = context.plan_builder("dev").build() + context.apply(plan) + + dev_environment = context.state_sync.get_environment("dev") + assert dev_environment is not None + + state_environments = context.state_reader.get_environments() + state_snapshots = context.state_reader.get_snapshots(context.snapshots.values()) + assert ( + len(state_snapshots) + == len(state_environments[0].snapshots) + == len(state_environments[1].snapshots) + ) + + # The state tables at this point should be able to be retrieved + state_tables = { + "_environments", + "_snapshots", + "_intervals", + "_auto_restatements", + "_environment_statements", + "_intervals", + "_plan_dags", + "_versions", + } + for table_name in state_tables: + context.fetchdf(f"SELECT * FROM db_1.sqlmesh.{table_name}") + + # The actual tables as well + context.engine_adapters["second"].fetchdf(f"SELECT * FROM db_2.second_schema.model_one") + context.engine_adapters["second"].fetchdf(f"SELECT * FROM db_2.second_schema.model_two") + context.fetchdf(f"SELECT * FROM db_1.first_schema.model_one") + context.fetchdf(f"SELECT * FROM db_1.first_schema.model_two") + + # Use the destroy command to remove all data objects and state + context._destroy() + + # Ensure all tables have been removed + for table_name in state_tables: + with pytest.raises( + Exception, match=f"Catalog Error: Table with name {table_name} does not exist!" + ): + context.fetchdf(f"SELECT * FROM db_1.sqlmesh.{table_name}") + + # Validate tables have been deleted as well + with pytest.raises( + Exception, match=r"Catalog Error: Table with name model_two does not exist!" + ): + context.fetchdf("SELECT * FROM db_1.first_schema.model_two") + with pytest.raises( + Exception, match=r"Catalog Error: Table with name model_one does not exist!" + ): + context.fetchdf("SELECT * FROM db_1.first_schema.model_one") + + with pytest.raises( + Exception, match=r"Catalog Error: Table with name model_two does not exist!" + ): + context.engine_adapters["second"].fetchdf("SELECT * FROM db_2.second_schema.model_two") + with pytest.raises( + Exception, match=r"Catalog Error: Table with name model_one does not exist!" + ): + context.engine_adapters["second"].fetchdf("SELECT * FROM db_2.second_schema.model_one") + + # Ensure the cache has been removed + assert not cache_path.exists() diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index 883221740a..d427f7789a 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -890,3 +890,39 @@ def test_lint(notebook, sushi_context): assert len(output.outputs) == 2 assert "Linter warnings for" in output.outputs[0].data["text/plain"] + + +@pytest.mark.slow +def test_destroy( + notebook, + loaded_sushi_context, + convert_all_html_output_to_text, + get_all_html_output, + monkeypatch, +): + # Mock input to return 'y' for the confirmation prompt + monkeypatch.setattr("builtins.input", lambda: "y") + + with capture_output() as output: + notebook.run_line_magic(magic_name="destroy", line="") + + assert not output.stdout + assert not output.stderr + text_output = convert_all_html_output_to_text(output) + expected_messages = [ + "[WARNING] This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n" + "The operation is irreversible and may disrupt any currently running or scheduled plans.\n" + "Use this command only when you intend to fully reset the project.", + "Environment 'prod' invalidated.", + "Deleted object memory.sushi", + 'Deleted object "memory"."raw"."model1"', + 'Deleted object "memory"."raw"."model1"', + 'Deleted object "memory"."raw"."model2"', + 'Deleted object "memory"."raw"."model2"', + 'Deleted object "memory"."raw"."demographics"', + 'Deleted object "memory"."raw"."demographics"', + "State tables removed.", + "Destroy completed successfully.", + ] + for message in expected_messages: + assert message in text_output From c42bce4a753c560011cf8a3d549c9d8bd373b18f Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Fri, 9 May 2025 14:35:19 -0500 Subject: [PATCH 0132/1056] Fix: make logger respect --ignore-warnings (#4354) --- sqlmesh/__init__.py | 3 ++- sqlmesh/cli/main.py | 8 ++++++- sqlmesh/magics.py | 7 +++++- tests/cli/test_cli.py | 53 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/sqlmesh/__init__.py b/sqlmesh/__init__.py index a033b652d7..e48ffbef5b 100644 --- a/sqlmesh/__init__.py +++ b/sqlmesh/__init__.py @@ -172,6 +172,7 @@ def configure_logging( write_to_file: bool = True, log_limit: int = c.DEFAULT_LOG_LIMIT, log_file_dir: t.Optional[t.Union[str, Path]] = None, + ignore_warnings: bool = False, ) -> None: # Remove noisy grpc logs that are not useful for users os.environ["GRPC_VERBOSITY"] = os.environ.get("GRPC_VERBOSITY", "NONE") @@ -186,7 +187,7 @@ def configure_logging( if write_to_stdout: stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(CustomFormatter()) - stdout_handler.setLevel(level) + stdout_handler.setLevel(logging.ERROR if ignore_warnings else level) logger.addHandler(stdout_handler) log_file_dir = log_file_dir or c.DEFAULT_LOG_FILE_DIR diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index c60506d023..14b9ebd42c 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -102,7 +102,13 @@ def cli( configs = load_configs(config, Context.CONFIG_TYPE, paths) log_limit = list(configs.values())[0].log_limit - configure_logging(debug, log_to_stdout, log_limit=log_limit, log_file_dir=log_file_dir) + configure_logging( + debug, + log_to_stdout, + log_limit=log_limit, + log_file_dir=log_file_dir, + ignore_warnings=ignore_warnings, + ) configure_console(ignore_warnings=ignore_warnings) try: diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index d6e540357c..313b98ab52 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -134,7 +134,12 @@ def context(self, line: str) -> None: args = parse_argstring(self.context, line) configs = load_configs(args.config, Context.CONFIG_TYPE, args.paths) log_limit = list(configs.values())[0].log_limit - configure_logging(args.debug, log_limit=log_limit, log_file_dir=args.log_file_dir) + configure_logging( + args.debug, + log_limit=log_limit, + log_file_dir=args.log_file_dir, + ignore_warnings=args.ignore_warnings, + ) configure_console(ignore_warnings=args.ignore_warnings) try: context = Context(paths=args.paths, config=configs, gateway=args.gateway) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index eaaa04017c..c0b1722c06 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1706,3 +1706,56 @@ def test_dbt_init(tmp_path): config = sqlmesh_config(Path(__file__).parent) """ ) + + +def test_ignore_warnings(runner: CliRunner, tmp_path: Path) -> None: + create_example_project(tmp_path) + + # Add non-blocking audit to generate WARNING + with open(tmp_path / "models" / "full_model.sql", "w", encoding="utf-8") as f: + f.write(""" +MODEL ( + name sqlmesh_example.full_model, + kind FULL, + cron '@daily', + grain item_id, + audits (full_nonblocking_audit), +); + +SELECT + item_id, + COUNT(DISTINCT id) AS num_orders, +FROM + sqlmesh_example.incremental_model +GROUP BY item_id; + +AUDIT ( + name full_nonblocking_audit, + blocking false, +); +select 1 as a; +""") + + audit_warning = "[WARNING] sqlmesh_example.full_model: 'full_nonblocking_audit' audit error: " + + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "plan", "--no-prompts", "--auto-apply", "--skip-tests"], + ) + assert result.exit_code == 0 + assert audit_warning in result.output + + result = runner.invoke( + cli, + [ + "--ignore-warnings", + "--paths", + str(tmp_path), + "plan", + "--no-prompts", + "--auto-apply", + "--skip-tests", + ], + ) + assert result.exit_code == 0 + assert audit_warning not in result.output From e3858d4b2514e1c9c609ee6b05046a605d418510 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Mon, 12 May 2025 09:19:04 +1200 Subject: [PATCH 0133/1056] Fix(table_diff): Allow diffing of empty tables (#4347) --- sqlmesh/core/console.py | 6 +++++ sqlmesh/core/table_diff.py | 11 +++++++++- tests/core/test_table_diff.py | 41 +++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index ad28d4ddd2..867497e90f 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -2181,6 +2181,12 @@ def show_schema_diff(self, schema_diff: SchemaDiff) -> None: def show_row_diff( self, row_diff: RowDiff, show_sample: bool = True, skip_grain_check: bool = False ) -> None: + if row_diff.empty: + self.console.print( + "\n[b][red]Neither the source nor the target table contained any records[/red][/b]" + ) + return + source_name = row_diff.source if row_diff.source_alias: source_name = row_diff.source_alias.upper() diff --git a/sqlmesh/core/table_diff.py b/sqlmesh/core/table_diff.py index e4b1271ebe..7b59acb912 100644 --- a/sqlmesh/core/table_diff.py +++ b/sqlmesh/core/table_diff.py @@ -83,6 +83,15 @@ def target_count(self) -> int: """Count of the target.""" return int(self.stats["t_count"]) + @property + def empty(self) -> bool: + return ( + self.source_count == 0 + and self.target_count == 0 + and self.s_only_count == 0 + and self.t_only_count == 0 + ) + @property def count_pct_change(self) -> float: """The percentage change of the counts.""" @@ -446,7 +455,7 @@ def name(e: exp.Expression) -> str: summary_query = exp.select(*summary_sums).from_(table) - stats_df = self.adapter.fetchdf(summary_query, quote_identifiers=True) + stats_df = self.adapter.fetchdf(summary_query, quote_identifiers=True).fillna(0) stats_df["s_only_count"] = stats_df["s_count"] - stats_df["join_count"] stats_df["t_only_count"] = stats_df["t_count"] - stats_df["join_count"] stats = stats_df.iloc[0].to_dict() diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index e0bdbe51d7..7356e77379 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -788,3 +788,44 @@ def test_data_diff_forward_only(sushi_context_fixed_date, capsys, caplog): assert row_diff.stats["distinct_count_t"] == 2 assert row_diff.s_sample.shape == (2, 2) assert row_diff.t_sample.shape == (2, 2) + + +def test_data_diff_empty_tables(): + engine_adapter = DuckDBConnectionConfig().create_engine_adapter() + + columns_to_types_src = { + "key": exp.DataType.build("int"), + "value": exp.DataType.build("varchar"), + } + columns_to_types_target = { + "key": exp.DataType.build("int"), + "value2": exp.DataType.build("varchar"), + } + + engine_adapter.create_table("table_diff_source", columns_to_types_src) + engine_adapter.create_table("table_diff_target", columns_to_types_target) + + table_diff = TableDiff( + adapter=engine_adapter, + source="table_diff_source", + target="table_diff_target", + source_alias="dev", + target_alias="prod", + on=["key"], + ) + + # should show the schema diff + schema_diff = table_diff.schema_diff() + assert len(schema_diff.added) == 1 + assert schema_diff.added[0][0] == "value2" + assert len(schema_diff.removed) == 1 + assert schema_diff.removed[0][0] == "value" + + # should not error on the row diff + row_diff = table_diff.row_diff() + assert row_diff.empty + + output = capture_console_output("show_row_diff", row_diff=row_diff) + assert ( + strip_ansi_codes(output) == "Neither the source nor the target table contained any records" + ) From e773b59a626cc7607930f014f045c78f60add72f Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 12 May 2025 12:00:10 +0200 Subject: [PATCH 0134/1056] feat: add code href that links to the rule (#4355) --- sqlmesh/core/linter/rule.py | 34 +++++++++++++++++++++++ sqlmesh/lsp/main.py | 9 ++++++ tests/core/test_rule.py | 55 +++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 tests/core/test_rule.py diff --git a/sqlmesh/core/linter/rule.py b/sqlmesh/core/linter/rule.py index 4105e27d4c..003f9b813a 100644 --- a/sqlmesh/core/linter/rule.py +++ b/sqlmesh/core/linter/rule.py @@ -8,11 +8,20 @@ import typing as t +from sqlmesh.utils.pydantic import PydanticModel + if t.TYPE_CHECKING: from sqlmesh.core.context import GenericContext +class RuleLocation(PydanticModel): + """The location of a rule in a file.""" + + file_path: str + start_line: t.Optional[int] = None + + class _Rule(abc.ABCMeta): def __new__(cls: Type[_Rule], clsname: str, bases: t.Tuple, attrs: t.Dict) -> _Rule: attrs["name"] = clsname.lower() @@ -40,6 +49,31 @@ def violation(self, violation_msg: t.Optional[str] = None) -> RuleViolation: """Create a RuleViolation instance for this rule""" return RuleViolation(rule=self, violation_msg=violation_msg or self.summary) + def get_definition_location(self) -> RuleLocation: + """Return the file path and position information for this rule. + + This method returns information about where this rule is defined, + which can be used in diagnostics to link to the rule's documentation. + + Returns: + A dictionary containing file path and position information. + """ + import inspect + + # Get the file where the rule class is defined + file_path = inspect.getfile(self.__class__) + + try: + # Get the source code and line number + source_lines, start_line = inspect.getsourcelines(self.__class__) + return RuleLocation( + file_path=file_path, + start_line=start_line, + ) + except (IOError, TypeError): + # Fall back to just returning the file path if we can't get source lines + return RuleLocation(file_path=file_path) + def __repr__(self) -> str: return self.name diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 9a4d5ec802..e48550b3d7 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -293,6 +293,12 @@ def _diagnostic_to_lsp_diagnostic( return None with open(diagnostic.model._path, "r", encoding="utf-8") as file: lines = file.readlines() + + # Get rule definition location for diagnostics link + rule_location = diagnostic.rule.get_definition_location() + rule_uri = f"file://{rule_location.file_path}#L{rule_location.start_line}" + + # Use URI format to create a link for "related information" return types.Diagnostic( range=types.Range( start=types.Position(line=0, character=0), @@ -302,6 +308,9 @@ def _diagnostic_to_lsp_diagnostic( severity=types.DiagnosticSeverity.Error if diagnostic.violation_type == "error" else types.DiagnosticSeverity.Warning, + source="sqlmesh", + code=diagnostic.rule.name, + code_description=types.CodeDescription(href=rule_uri), ) @staticmethod diff --git a/tests/core/test_rule.py b/tests/core/test_rule.py new file mode 100644 index 0000000000..012e11729d --- /dev/null +++ b/tests/core/test_rule.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import inspect +import typing as t +from unittest.mock import MagicMock + +import pytest +from sqlmesh.core.model import Model +from sqlmesh.core.linter.rule import Rule, RuleViolation + + +class TestRule(Rule): + """A test rule for testing the get_definition_location method.""" + + def check_model(self, model: Model) -> t.Optional[RuleViolation]: + """The evaluation function that'll check for a violation of this rule.""" + return None + + +def test_get_definition_location(): + """Test the get_definition_location method returns correct file and line information.""" + # Create a mock context + mock_context = MagicMock() + rule = TestRule(mock_context) + + # Get the expected location using the inspect module + expected_file = inspect.getfile(TestRule) + expected_source, expected_start_line = inspect.getsourcelines(TestRule) + expected_end_line = expected_start_line + len(expected_source) - 1 + + # Get the location using the Rule method + location = rule.get_definition_location() + + # Assert the file path matches + assert location.file_path == expected_file + + # Assert the line numbers match + assert location.start_line == expected_start_line + + # Test the fallback case for a class without source + with pytest.MonkeyPatch.context() as mp: + # Mock inspect.getsourcelines to raise an exception + def mock_getsourcelines(*args, **kwargs): + raise IOError("Mock error") + + mp.setattr(inspect, "getsourcelines", mock_getsourcelines) + + # Get the location with the mocked function + fallback_location = rule.get_definition_location() + + # It should still have the file path + assert fallback_location.file_path == expected_file + + # But not the line numbers + assert fallback_location.start_line is None From 3717cd51f6f172d45ef13760b01911e2c894ac91 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 12 May 2025 15:24:41 +0200 Subject: [PATCH 0135/1056] chore(vscode): use with open correctly (#4360) --- sqlmesh/lsp/reference.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 51b15c3718..cf45fe9aff 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -132,7 +132,8 @@ def get_model_definitions_for_a_path( if len(tables) == 0: return [] - read_file = open(file_path, "r").readlines() + with open(file_path, "r", encoding="utf-8") as file: + read_file = file.readlines() for table in tables: # Normalize the table reference From fb6bafbd7ca051bf510e40862ff209f4d7807f18 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 12 May 2025 15:46:11 +0200 Subject: [PATCH 0136/1056] chore(vscode): remove use of deprecated function (#4361) --- sqlmesh/lsp/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index e48550b3d7..9751cb9205 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -151,7 +151,7 @@ def formatting( """Format the document using SQLMesh `format_model_expressions`.""" try: self._ensure_context_for_document(params.text_document.uri) - document = ls.workspace.get_document(params.text_document.uri) + document = ls.workspace.get_text_document(params.text_document.uri) if self.lsp_context is None: raise RuntimeError(f"No context found for document: {document.path}") @@ -182,7 +182,7 @@ def hover(ls: LanguageServer, params: types.HoverParams) -> t.Optional[types.Hov """Provide hover information for an object.""" try: self._ensure_context_for_document(params.text_document.uri) - document = ls.workspace.get_document(params.text_document.uri) + document = ls.workspace.get_text_document(params.text_document.uri) if self.lsp_context is None: raise RuntimeError(f"No context found for document: {document.path}") @@ -212,7 +212,7 @@ def goto_definition( """Jump to an object's definition.""" try: self._ensure_context_for_document(params.text_document.uri) - document = ls.workspace.get_document(params.text_document.uri) + document = ls.workspace.get_text_document(params.text_document.uri) if self.lsp_context is None: raise RuntimeError(f"No context found for document: {document.path}") From 107f7141cc558d7a6d296b1112961f5d14f2c758 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 12 May 2025 10:50:23 -0500 Subject: [PATCH 0137/1056] Chore: fix allow-destructive option name in docs (#4356) --- docs/concepts/plans.md | 2 +- docs/guides/incremental_time.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/concepts/plans.md b/docs/concepts/plans.md index ff8561004a..7903fe249f 100644 --- a/docs/concepts/plans.md +++ b/docs/concepts/plans.md @@ -299,7 +299,7 @@ Forward-only plans treats all of the plan's model changes as forward-only. In th SQLMesh determines what to do for each model based on this setting hierarchy: the [model's `on_destructive_change` value](../guides/incremental_time.md#destructive-changes) (if present), the `on_destructive_change` [model defaults](../reference/model_configuration.md#model-defaults) value (if present), and the SQLMesh global default of `error`. -If you want to temporarily allow destructive changes to models that don't allow them, use the `plan` command's `--allow-destructive-change` selector to specify which models. Learn more about model selectors [here](../guides/model_selection.md). +If you want to temporarily allow destructive changes to models that don't allow them, use the `plan` command's `--allow-destructive-model` selector to specify which models. Learn more about model selectors [here](../guides/model_selection.md). ### Effective date Changes that are part of the forward-only plan can also be applied retroactively to the production environment by specifying the effective date: diff --git a/docs/guides/incremental_time.md b/docs/guides/incremental_time.md index 8878e6e436..7c773f7edc 100644 --- a/docs/guides/incremental_time.md +++ b/docs/guides/incremental_time.md @@ -194,4 +194,4 @@ The SQLMesh `plan` [`--forward-only` option](../concepts/plans.md#forward-only-p SQLMesh determines what to do for each model based on this setting hierarchy: the model's `on_destructive_change` value (if present), the `on_destructive_change` [model defaults](../reference/model_configuration.md#model-defaults) value (if present), and the SQLMesh global default of `error`. -If you want to temporarily allow destructive changes to models that don't allow them, use the `plan` command's [`--allow-destructive-change` selector](../concepts/plans.md#destructive-changes) to specify which models. Learn more about model selectors [here](../guides/model_selection.md). +If you want to temporarily allow destructive changes to models that don't allow them, use the `plan` command's [`--allow-destructive-model` selector](../concepts/plans.md#destructive-changes) to specify which models. Learn more about model selectors [here](../guides/model_selection.md). From 94a29604f00456c20612416f6039f5300ed33b29 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 12 May 2025 18:13:59 +0200 Subject: [PATCH 0138/1056] feat(vscode): add custom api call method (#4352) --- sqlmesh/lsp/api.py | 61 +++++++++++++++++++++++++++++++++++++++++++++ sqlmesh/lsp/main.py | 25 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 sqlmesh/lsp/api.py diff --git a/sqlmesh/lsp/api.py b/sqlmesh/lsp/api.py new file mode 100644 index 0000000000..20d5318008 --- /dev/null +++ b/sqlmesh/lsp/api.py @@ -0,0 +1,61 @@ +""" +This module maps the LSP custom API calls to the SQLMesh web api. + +Allowing the LSP to call the web api without having to know the details of the web api +and thus passing through the details of the web api to the LSP, so that both the LSP +and the web api can communicate with the same process, avoiding the need to have a +separate process for the web api. +""" + +import typing as t +from pydantic import field_validator +from sqlmesh.utils.pydantic import PydanticModel +from web.server.models import Model + +API_FEATURE = "sqlmesh/api" + + +class ApiRequest(PydanticModel): + """ + Request to call the SQLMesh API. + This is a generic request that can be used to call any API endpoint. + """ + + requestId: str + url: str + method: t.Optional[str] = "GET" + params: t.Optional[t.Dict[str, t.Any]] = None + body: t.Optional[t.Dict[str, t.Any]] = None + + +class ApiResponseGetModels(PydanticModel): + """ + Response from the SQLMesh API for the get_models endpoint. + """ + + data: t.List[Model] + + @field_validator("data", mode="before") + def sanitize_datetime_fields(cls, data: t.List[Model]) -> t.List[Model]: + """ + Convert datetime objects to None to avoid serialization issues. + """ + if isinstance(data, list): + for model in data: + if hasattr(model, "details") and model.details: + # Convert datetime fields to None to avoid serialization issues + for field in ["stamp", "start", "cron_prev", "cron_next"]: + if ( + hasattr(model.details, field) + and getattr(model.details, field) is not None + ): + setattr(model.details, field, None) + return data + + +class ApiResponseGetLineage(PydanticModel): + """ + Response from the SQLMesh API for the get_lineage endpoint. + """ + + data: t.Dict[str, t.List[str]] diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 9751cb9205..1763c4098a 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -11,12 +11,20 @@ from sqlmesh._version import __version__ from sqlmesh.core.context import Context from sqlmesh.core.linter.definition import AnnotatedRuleViolation +from sqlmesh.lsp.api import ( + API_FEATURE, + ApiRequest, + ApiResponseGetLineage, + ApiResponseGetModels, +) from sqlmesh.lsp.completions import get_sql_completions from sqlmesh.lsp.context import LSPContext, ModelTarget from sqlmesh.lsp.custom import ALL_MODELS_FEATURE, AllModelsRequest, AllModelsResponse from sqlmesh.lsp.reference import ( get_references, ) +from web.server.api.endpoints.lineage import model_lineage +from web.server.api.endpoints.models import get_models class SQLMeshLanguageServer: @@ -79,6 +87,23 @@ def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsRespons except Exception as e: return get_sql_completions(None, params.textDocument.uri) + @self.server.feature(API_FEATURE) + def api( + ls: LanguageServer, request: ApiRequest + ) -> t.Union[ApiResponseGetModels, ApiResponseGetLineage]: + ls.log_trace(f"API request: {request}") + if self.lsp_context is None: + raise RuntimeError("No context found") + if request.url == "/api/models": + response = ApiResponseGetModels(data=get_models(self.lsp_context.context)) + return response + if request.url.startswith("/api/lineage"): + name = request.url.split("/")[-1] + lineage = model_lineage(name, self.lsp_context.context) + non_set_lineage = {k: v for k, v in lineage.items() if v is not None} + return ApiResponseGetLineage(data=non_set_lineage) + raise NotImplementedError(f"API request not implemented: {request.url}") + @self.server.feature(types.TEXT_DOCUMENT_DID_OPEN) def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> None: context = self._context_get_or_load(params.text_document.uri) From 40c6cd5c3e5fa17006c8fa0eaccb4ff07a7c511b Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 12 May 2025 19:13:42 +0200 Subject: [PATCH 0139/1056] chore: update minor dependencies in ui (#4365) --- package-lock.json | 149 +++++++++++++++++++++++++++++++++------------- 1 file changed, 107 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1907f7b27d..e53be81529 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -336,7 +335,9 @@ } }, "node_modules/@codemirror/lang-python": { - "version": "6.1.7", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.0.tgz", + "integrity": "sha512-+oLTR88uLib84tvb4XmOBBq/dgrctvPXueP3Wjotu4zmHLM2KW2wfswJ6r1BKlfJNcGgdWX1AgUeGEf3E2H5LA==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.3.2", @@ -413,7 +414,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.36.5", + "version": "6.36.8", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.8.tgz", + "integrity": "sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -1093,13 +1096,17 @@ }, "node_modules/@radix-ui/primitive": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, "node_modules/@radix-ui/react-arrow": { - "version": "1.1.4", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.6.tgz", + "integrity": "sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.0" + "@radix-ui/react-primitive": "2.1.2" }, "peerDependencies": { "@types/react": "*", @@ -1117,13 +1124,15 @@ } }, "node_modules/@radix-ui/react-collection": { - "version": "1.1.4", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", + "integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-slot": "1.2.0" + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -1142,6 +1151,8 @@ }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1155,6 +1166,8 @@ }, "node_modules/@radix-ui/react-context": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1167,13 +1180,15 @@ } }, "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.11", + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.14.tgz", + "integrity": "sha512-RUHvrJE2qKAd9pQ50HZZsePio4SMWEh8v6FWQwg/4t6K1fuxfb4Ec40VEVvni6V7nFxmj9srU4UZc7aYp8x0LQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-menu": "2.1.11", - "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-menu": "2.1.14", + "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, @@ -1194,6 +1209,8 @@ }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1206,12 +1223,14 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.7", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz", + "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, @@ -1232,6 +1251,8 @@ }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1244,11 +1265,13 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.4", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.6.tgz", + "integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { @@ -1268,6 +1291,8 @@ }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -1283,24 +1308,26 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.1.11", + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.14.tgz", + "integrity": "sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-collection": "1.1.6", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-dismissable-layer": "1.1.9", "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.6", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.4", - "@radix-ui/react-portal": "1.1.6", - "@radix-ui/react-presence": "1.1.3", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-roving-focus": "1.1.7", - "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-popper": "1.2.6", + "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-roving-focus": "1.1.9", + "@radix-ui/react-slot": "1.2.2", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" @@ -1321,14 +1348,16 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.2.4", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz", + "integrity": "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.4", + "@radix-ui/react-arrow": "1.1.6", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", @@ -1351,10 +1380,12 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.1.6", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz", + "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { @@ -1373,7 +1404,9 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.1.3", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -1395,10 +1428,12 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "2.1.0", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", + "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.0" + "@radix-ui/react-slot": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -1416,16 +1451,18 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.7", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz", + "integrity": "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-collection": "1.1.6", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, @@ -1894,7 +1931,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.0", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -1911,6 +1950,8 @@ }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1924,6 +1965,8 @@ }, "node_modules/@radix-ui/react-use-controllable-state": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", @@ -1941,6 +1984,8 @@ }, "node_modules/@radix-ui/react-use-effect-event": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -1957,6 +2002,8 @@ }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" @@ -1973,6 +2020,8 @@ }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2002,6 +2051,8 @@ }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.1" @@ -2018,6 +2069,8 @@ }, "node_modules/@radix-ui/react-use-size": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -2111,6 +2164,8 @@ }, "node_modules/@radix-ui/rect": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, "node_modules/@react-dnd/asap": { @@ -3559,7 +3614,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.20", + "version": "18.3.21", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.21.tgz", + "integrity": "sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -3567,7 +3624,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.3.6", + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", "peerDependencies": { @@ -3775,7 +3834,9 @@ } }, "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.23.10", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.12.tgz", + "integrity": "sha512-l9vuiXOTFDBetYrRLDmz3jDxQHDsrVAZ2Y6dVfmrqi2AsulsDu+y7csW0JsvaMqo79rYkaIZg8yeqmDgMb7VyQ==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -3800,14 +3861,16 @@ } }, "node_modules/@uiw/react-codemirror": { - "version": "4.23.10", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.12.tgz", + "integrity": "sha512-yseqWdzoAAGAW7i/NiU8YrfSLVOEBjQvSx1KpDTFVV/nn0AlAZoDVTIPEBgdXrPlVUQoCrwgpEaj3uZCklk9QA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.23.10", + "@uiw/codemirror-extensions-basic-setup": "4.23.12", "codemirror": "^6.0.0" }, "funding": { @@ -10638,6 +10701,8 @@ }, "node_modules/react-remove-scroll": { "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", From dcd73e02c74bec759cffe8e27c3c7a5dd06dfc99 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 12 May 2025 19:37:33 +0200 Subject: [PATCH 0140/1056] chore: update zustand in ui (#4366) --- package-lock.json | 31 ++++++++++++++++++++++++++++++- web/client/package.json | 2 +- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index e53be81529..f929be60ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13768,7 +13768,7 @@ "react-split": "^2.0.14", "reactflow": "^11.8.3", "thememirror": "^2.0.1", - "zustand": "^4.4.1" + "zustand": "^5.0.0" }, "devDependencies": { "@eslint/js": "^9.25.1", @@ -13796,6 +13796,35 @@ "optionalDependencies": { "@swc/core-linux-x64-gnu": "^1.11.24" } + }, + "web/client/node_modules/zustand": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz", + "integrity": "sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/web/client/package.json b/web/client/package.json index 4791f66b3c..c3d5f25ca3 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -48,7 +48,7 @@ "react-split": "^2.0.14", "reactflow": "^11.8.3", "thememirror": "^2.0.1", - "zustand": "^4.4.1" + "zustand": "^5.0.0" }, "devDependencies": { "@eslint/js": "^9.25.1", From 223bc8eff0c68fc12e8112b579a1030d498332e7 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 12 May 2025 22:52:01 +0200 Subject: [PATCH 0141/1056] chore: update apache arrow in ui (#4368) --- package-lock.json | 136 +++++++++++++++++++++++++++++----------- web/client/package.json | 2 +- 2 files changed, 101 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index f929be60ef..3bad645661 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3087,6 +3087,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@swc/types": { "version": "0.1.21", "dev": true, @@ -3295,11 +3304,15 @@ "license": "MIT" }, "node_modules/@types/command-line-args": { - "version": "5.2.0", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", + "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", "license": "MIT" }, "node_modules/@types/command-line-usage": { - "version": "5.0.2", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", + "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==", "license": "MIT" }, "node_modules/@types/d3": { @@ -3597,11 +3610,18 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.3.0", - "license": "MIT" + "version": "20.17.46", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz", + "integrity": "sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } }, - "node_modules/@types/pad-left": { - "version": "2.1.1", + "node_modules/@types/node/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, "node_modules/@types/pluralize": { @@ -4567,24 +4587,83 @@ } }, "node_modules/apache-arrow": { - "version": "13.0.0", + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-19.0.1.tgz", + "integrity": "sha512-APmMLzS4qbTivLrPdQXexGM4JRr+0g62QDaobzEvip/FdQIrv2qLy0mD5Qdmw4buydtVJgbFeKR8f59I6PPGDg==", "license": "Apache-2.0", "dependencies": { - "@types/command-line-args": "5.2.0", - "@types/command-line-usage": "5.0.2", - "@types/node": "20.3.0", - "@types/pad-left": "2.1.1", - "command-line-args": "5.2.1", - "command-line-usage": "7.0.1", - "flatbuffers": "23.5.26", + "@swc/helpers": "^0.5.11", + "@types/command-line-args": "^5.2.3", + "@types/command-line-usage": "^5.0.4", + "@types/node": "^20.13.0", + "command-line-args": "^6.0.1", + "command-line-usage": "^7.0.1", + "flatbuffers": "^24.3.25", "json-bignum": "^0.0.3", - "pad-left": "^2.1.0", - "tslib": "^2.5.3" + "tslib": "^2.6.2" }, "bin": { "arrow2csv": "bin/arrow2csv.js" } }, + "node_modules/apache-arrow/node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/apache-arrow/node_modules/command-line-args": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", + "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "find-replace": "^5.0.2", + "lodash.camelcase": "^4.3.0", + "typical": "^7.2.0" + }, + "engines": { + "node": ">=12.20" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/apache-arrow/node_modules/find-replace": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", + "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/apache-arrow/node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/arg": { "version": "5.0.2", "license": "MIT" @@ -6763,8 +6842,10 @@ } }, "node_modules/flatbuffers": { - "version": "23.5.26", - "license": "SEE LICENSE IN LICENSE" + "version": "24.12.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.12.23.tgz", + "integrity": "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==", + "license": "Apache-2.0" }, "node_modules/flatted": { "version": "3.3.3", @@ -10033,16 +10114,6 @@ "version": "1.0.1", "license": "BlueOak-1.0.0" }, - "node_modules/pad-left": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "repeat-string": "^1.5.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pako": { "version": "1.0.11", "dev": true, @@ -10961,13 +11032,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/repeat-string": { - "version": "1.6.1", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/require-directory": { "version": "2.1.1", "dev": true, @@ -13754,7 +13818,7 @@ "@tanstack/react-virtual": "^3.0.0-beta.56", "@uidotdev/usehooks": "^2.2.0", "@uiw/react-codemirror": "^4.21.12", - "apache-arrow": "^13.0.0", + "apache-arrow": "^19.0.0", "clsx": "^2.0.0", "diff": "^5.2.0", "elkjs": "^0.8.2", diff --git a/web/client/package.json b/web/client/package.json index c3d5f25ca3..b57d5ecaaf 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -34,7 +34,7 @@ "@tanstack/react-virtual": "^3.0.0-beta.56", "@uidotdev/usehooks": "^2.2.0", "@uiw/react-codemirror": "^4.21.12", - "apache-arrow": "^13.0.0", + "apache-arrow": "^19.0.0", "clsx": "^2.0.0", "diff": "^5.2.0", "elkjs": "^0.8.2", From adf5e5d463e3dcecc2c00e518c4db24d123e5c21 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 12 May 2025 23:51:49 +0200 Subject: [PATCH 0142/1056] chore: update react testing library (#4371) --- package-lock.json | 183 ++++++++++++---------------------------- web/client/package.json | 2 +- 2 files changed, 55 insertions(+), 130 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bad645661..5e3d9620b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -282,6 +283,7 @@ "version": "7.26.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", @@ -295,6 +297,7 @@ "version": "7.25.9", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -3197,21 +3200,24 @@ } }, "node_modules/@testing-library/dom": { - "version": "9.3.4", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", + "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/@testing-library/jest-dom": { @@ -3261,23 +3267,6 @@ "node": ">=8" } }, - "node_modules/@testing-library/react": { - "version": "14.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^9.0.0", - "@types/react-dom": "^18.0.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, "node_modules/@testing-library/user-event": { "version": "14.6.1", "dev": true, @@ -3301,7 +3290,8 @@ "node_modules/@types/aria-query": { "version": "5.0.4", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/command-line-args": { "version": "5.2.3", @@ -4684,11 +4674,13 @@ } }, "node_modules/aria-query": { - "version": "5.1.3", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/array-back": { @@ -5827,42 +5819,6 @@ "node": ">=6" } }, - "node_modules/deep-equal": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-equal/node_modules/isarray": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, "node_modules/deep-extend": { "version": "0.6.0", "dev": true, @@ -6031,7 +5987,8 @@ "node_modules/dom-accessibility-api": { "version": "0.5.16", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-serializer": { "version": "2.0.0", @@ -6301,30 +6258,6 @@ "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-get-iterator/node_modules/isarray": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, "node_modules/es-module-lexer": { "version": "1.6.0", "dev": true, @@ -7542,21 +7475,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-arguments": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "dev": true, @@ -8601,6 +8519,7 @@ "version": "1.5.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9782,21 +9701,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "dev": true, @@ -10532,6 +10436,7 @@ "version": "27.5.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -10545,6 +10450,7 @@ "version": "5.2.0", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -10743,7 +10649,8 @@ "node_modules/react-is": { "version": "17.0.2", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-markdown": { "version": "9.1.0", @@ -11342,7 +11249,9 @@ "peer": true }, "node_modules/semver": { - "version": "7.7.1", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11690,18 +11599,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/stream-read-all": { "version": "3.0.1", "license": "MIT", @@ -13839,7 +13736,7 @@ "@playwright/test": "^1.37.1", "@swc/core": "^1.11.24", "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.4.3", "@types/diff": "^5.2.1", "@types/pluralize": "^0.0.30", @@ -13861,6 +13758,34 @@ "@swc/core-linux-x64-gnu": "^1.11.24" } }, + "web/client/node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "web/client/node_modules/zustand": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz", diff --git a/web/client/package.json b/web/client/package.json index b57d5ecaaf..7157176714 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -55,7 +55,7 @@ "@playwright/test": "^1.37.1", "@swc/core": "^1.11.24", "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.4.3", "@types/diff": "^5.2.1", "@types/pluralize": "^0.0.30", From c7571fe5e4be6cf0dd23abb8827a1b4365479791 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 13 May 2025 02:32:05 +0300 Subject: [PATCH 0143/1056] Chore: typo fixes (#4372) --- docs/guides/notifications.md | 2 +- docs/guides/scheduling.md | 2 +- docs/guides/signals.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/notifications.md b/docs/guides/notifications.md index 85beae6c3b..03405b8252 100644 --- a/docs/guides/notifications.md +++ b/docs/guides/notifications.md @@ -130,7 +130,7 @@ This example stops all notifications other than those for `User1`: SQLMesh notifications are triggered by events. The events that should trigger a notification are specified in the notification target's `notify_on` field. -Notifications are support for [`plan` application](../concepts/plans.md) start/end/failure, [`run`](../reference/cli.md#run) start/end/failure, and [`audit`](../concepts/audits.md) failures. +Notifications are supported for [`plan` application](../concepts/plans.md) start/end/failure, [`run`](../reference/cli.md#run) start/end/failure, and [`audit`](../concepts/audits.md) failures. For `plan` and `run` start/end, the target environment name is included in the notification message. For failures, the Python exception or error text is included in the notification message. diff --git a/docs/guides/scheduling.md b/docs/guides/scheduling.md index 7c5b6c27dc..80d58db366 100644 --- a/docs/guides/scheduling.md +++ b/docs/guides/scheduling.md @@ -1,6 +1,6 @@ # Scheduling guide -SQLMesh currently offers three ways of scheduling model evaluation: +SQLMesh currently offers two ways of scheduling model evaluation: * Using [SQLMesh's built-in scheduler](#built-in-scheduler) * Using [Tobiko Cloud](../cloud/features/scheduler/scheduler.md) diff --git a/docs/guides/signals.md b/docs/guides/signals.md index 693594a51b..acbe7031f4 100644 --- a/docs/guides/signals.md +++ b/docs/guides/signals.md @@ -28,7 +28,7 @@ Signal checking functions examines a batch of time intervals. The function is al To define a signal, create a `signals` directory in your project folder. Define your signal in a file named `__init__.py` in that directory (you can have additional python file names as well). -A signal is a function that accepts a batch (DateTimeRanges: t.List[t.Tuple[datetime, datetime]]) and returns a batch or a boolean. It needs use the @signal decorator. +A signal is a function that accepts a batch (`DateTimeRanges: t.List[t.Tuple[datetime, datetime]]`) and returns a batch or a boolean. It needs to use the `@signal` decorator. We now demonstrate signals of varying complexity. From d905941209dfb44978a6e6ddf39a957b5e0f73f6 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 13 May 2025 12:56:52 +1200 Subject: [PATCH 0144/1056] Fix!: Allow python models to emit DataFrame's with a different column order (#4348) --- pyproject.toml | 7 +- sqlmesh/core/engine_adapter/base.py | 5 ++ sqlmesh/core/engine_adapter/mssql.py | 7 +- sqlmesh/core/engine_adapter/snowflake.py | 25 ++++-- sqlmesh/core/engine_adapter/spark.py | 6 ++ .../engine_adapter/integration/__init__.py | 21 +++-- .../integration/test_integration.py | 84 ++++++++++++++++++- .../integration/test_integration_bigquery.py | 47 +++++++++++ .../integration/test_integration_snowflake.py | 47 +++++++++++ tests/core/engine_adapter/test_base.py | 28 ++++--- tests/core/engine_adapter/test_snowflake.py | 9 +- 11 files changed, 245 insertions(+), 41 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cfbe1cb293..d1cff70651 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,9 +40,9 @@ athena = ["PyAthena[Pandas]"] azuresql = ["pymssql"] bigquery = [ "google-cloud-bigquery[pandas]", - "google-cloud-bigquery-storage" + "google-cloud-bigquery-storage", + "bigframes>=1.32.0" ] -bigframes = ["bigframes>=1.32.0"] clickhouse = ["clickhouse-connect"] databricks = ["databricks-sql-connector[pyarrow]"] dev = [ @@ -107,8 +107,7 @@ slack = ["slack_sdk"] snowflake = [ "cryptography", "snowflake-connector-python[pandas,secure-local-storage]", - # as at 2024-08-05, snowflake-snowpark-python is only available up to Python 3.11 - "snowflake-snowpark-python; python_version<'3.12'", + "snowflake-snowpark-python", ] trino = ["trino"] web = [ diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 29e870011b..1108432436 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -246,7 +246,12 @@ def _df_to_source_queries( assert isinstance(df, pd.DataFrame) num_rows = len(df.index) batch_size = sys.maxsize if batch_size == 0 else batch_size + + # we need to ensure that the order of the columns in columns_to_types columns matches the order of the values + # they can differ if a user specifies columns() on a python model in a different order than what's in the DataFrame's emitted by that model + df = df[list(columns_to_types)] values = list(df.itertuples(index=False, name=None)) + return [ SourceQuery( query_factory=partial( diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index f80f1816a3..9a198d5324 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -218,10 +218,13 @@ def query_factory() -> Query: # as later calls. if not self.table_exists(temp_table): columns_to_types_create = columns_to_types.copy() - self._convert_df_datetime(df, columns_to_types_create) + ordered_df = df[ + list(columns_to_types_create) + ] # reorder DataFrame so it matches columns_to_types + self._convert_df_datetime(ordered_df, columns_to_types_create) self.create_table(temp_table, columns_to_types_create) rows: t.List[t.Tuple[t.Any, ...]] = list( - df.replace({np.nan: None}).itertuples(index=False, name=None) # type: ignore + ordered_df.replace({np.nan: None}).itertuples(index=False, name=None) # type: ignore ) conn = self._connection_pool.get() conn.bulk_copy(temp_table.sql(dialect=self.dialect), rows) diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index ab01bf6722..c0a21bdc11 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -288,8 +288,25 @@ def _df_to_source_queries( is_snowpark_dataframe = snowpark and isinstance(df, snowpark.dataframe.DataFrame) def query_factory() -> Query: + # The catalog needs to be normalized before being passed to Snowflake's library functions because they + # just wrap whatever they are given in quotes without checking if its already quoted + database = ( + normalize_identifiers(temp_table.catalog, dialect=self.dialect) + if temp_table.catalog + else None + ) + if is_snowpark_dataframe: - df.createOrReplaceTempView(temp_table.sql(dialect=self.dialect, identify=True)) # type: ignore + temp_table.set("catalog", database) + df_renamed = df.rename( + { + col: exp.to_identifier(col).sql(dialect=self.dialect, identify=True) + for col in columns_to_types + } + ) # type: ignore + df_renamed.createOrReplaceTempView( + temp_table.sql(dialect=self.dialect, identify=True) + ) # type: ignore elif isinstance(df, pd.DataFrame): from snowflake.connector.pandas_tools import write_pandas @@ -325,11 +342,7 @@ def query_factory() -> Query: df, temp_table.name, schema=temp_table.db or None, - database=normalize_identifiers(temp_table.catalog, dialect=self.dialect).sql( - dialect=self.dialect - ) - if temp_table.catalog - else None, + database=database.sql(dialect=self.dialect) if database else None, chunk_size=self.DEFAULT_BATCH_SIZE, overwrite=True, table_type="temp", diff --git a/sqlmesh/core/engine_adapter/spark.py b/sqlmesh/core/engine_adapter/spark.py index 367bd5d4af..024f7d2b48 100644 --- a/sqlmesh/core/engine_adapter/spark.py +++ b/sqlmesh/core/engine_adapter/spark.py @@ -279,10 +279,16 @@ def _ensure_pyspark_df( ) -> PySparkDataFrame: pyspark_df = self.try_get_pyspark_df(generic_df) if pyspark_df: + if columns_to_types: + # ensure Spark dataframe column order matches columns_to_types + pyspark_df = pyspark_df.select(*columns_to_types) return pyspark_df df = self.try_get_pandas_df(generic_df) if df is None: raise SQLMeshError("Ensure PySpark DF can only be run on a PySpark or Pandas DataFrame") + if columns_to_types: + # ensure Pandas dataframe column order matches columns_to_types + df = df[list(columns_to_types)] kwargs = ( dict(schema=self.sqlglot_to_spark_types(columns_to_types)) if columns_to_types else {} ) diff --git a/tests/core/engine_adapter/integration/__init__.py b/tests/core/engine_adapter/integration/__init__.py index cd93c8ae4c..abd804a449 100644 --- a/tests/core/engine_adapter/integration/__init__.py +++ b/tests/core/engine_adapter/integration/__init__.py @@ -359,7 +359,7 @@ def get_table_comment( FROM pg_class c INNER JOIN pg_description d ON c.oid = d.objoid AND d.objsubid = 0 INNER JOIN pg_namespace n ON c.relnamespace = n.oid - WHERE + WHERE c.relname = '{table_name}' AND n.nspname= '{schema_name}' AND c.relkind = '{"v" if table_kind == "VIEW" else "r"}' @@ -465,12 +465,12 @@ def get_column_comments( INNER JOIN pg_namespace n ON c.relnamespace = n.oid INNER JOIN pg_attribute a ON c.oid = a.attrelid INNER JOIN pg_description d - ON + ON a.attnum = d.objsubid AND d.objoid = c.oid WHERE n.nspname = '{schema_name}' - AND c.relname = '{table_name}' + AND c.relname = '{table_name}' AND c.relkind = '{"v" if table_kind == "VIEW" else "r"}' ; """ @@ -494,6 +494,7 @@ def create_context( self, config_mutator: t.Optional[t.Callable[[str, Config], None]] = None, path: t.Optional[pathlib.Path] = None, + ephemeral_state_connection: bool = True, ) -> Context: private_sqlmesh_dir = pathlib.Path(pathlib.Path().home(), ".sqlmesh") config = load_config_from_paths( @@ -509,14 +510,12 @@ def create_context( config.gateways = {self.gateway: config.gateways[self.gateway]} gateway_config = config.gateways[self.gateway] - if ( - (sc := gateway_config.state_connection) - and (conn := gateway_config.connection) - and sc.type_ == "duckdb" - ): - # if duckdb is being used as the state connection, set concurrent_tasks=1 on the main connection - # to prevent duckdb from being accessed from multiple threads and getting deadlocked - conn.concurrent_tasks = 1 + if ephemeral_state_connection: + # Override whatever state connection has been configured on the integration test config to use in-memory DuckDB instead + # This is so tests that initialize a SQLMesh context can run concurrently without clobbering each others state + from sqlmesh.core.config.connection import DuckDBConnectionConfig + + gateway_config.state_connection = DuckDBConnectionConfig() if "athena" in self.gateway: conn = gateway_config.connection diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 5dc5f9b71b..cbdd559c18 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -2721,7 +2721,9 @@ def _use_warehouse_as_state_connection(gateway_name: str, config: Config): config.gateways[gateway_name].state_schema = test_schema - sqlmesh_context = ctx.create_context(config_mutator=_use_warehouse_as_state_connection) + sqlmesh_context = ctx.create_context( + config_mutator=_use_warehouse_as_state_connection, ephemeral_state_connection=False + ) assert sqlmesh_context.config.get_state_schema(ctx.gateway) == test_schema state_sync = ( @@ -2732,3 +2734,83 @@ def _use_warehouse_as_state_connection(gateway_name: str, config: Config): # will throw if one of the migrations produces an error, which can happen if we forget to take quoting or normalization into account sqlmesh_context.migrate() + + +def test_python_model_column_order(ctx: TestContext, tmp_path: pathlib.Path): + if ctx.test_type == "pyspark" and ctx.dialect in ("spark", "databricks"): + # dont skip + pass + elif ctx.test_type != "df": + pytest.skip("python model column order test only needs to be run once per db") + + schema = ctx.add_test_suffix(TEST_SCHEMA) + + (tmp_path / "models").mkdir() + + # note: this model deliberately defines the columns in the @model definition to be in a different order than what + # is returned by the DataFrame within the model + model_path = tmp_path / "models" / "python_model.py" + if ctx.test_type == "pyspark": + # python model that emits a PySpark dataframe + model_path.write_text( + """ +from pyspark.sql import DataFrame, Row +import typing as t +from sqlmesh import ExecutionContext, model + +@model( + "TEST_SCHEMA.model", + columns={ + "id": "int", + "name": "varchar" + } +) +def execute( + context: ExecutionContext, + **kwargs: t.Any, +) -> DataFrame: + return context.spark.createDataFrame([ + Row(name="foo", id=1) + ]) + """.replace("TEST_SCHEMA", schema) + ) + else: + # python model that emits a Pandas DataFrame + model_path.write_text( + """ +import pandas as pd +import typing as t +from sqlmesh import ExecutionContext, model + +@model( + "TEST_SCHEMA.model", + columns={ + "id": "int", + "name": "varchar" + } +) +def execute( + context: ExecutionContext, + **kwargs: t.Any, +) -> pd.DataFrame: + return pd.DataFrame([ + {"name": "foo", "id": 1} + ]) + """.replace("TEST_SCHEMA", schema) + ) + + sqlmesh_ctx = ctx.create_context(path=tmp_path) + + assert len(sqlmesh_ctx.models) == 1 + + plan = sqlmesh_ctx.plan(auto_apply=True) + assert len(plan.new_snapshots) == 1 + + engine_adapter = sqlmesh_ctx.engine_adapter + + query = exp.select("*").from_( + exp.to_table(f"{schema}.model", dialect=ctx.dialect), dialect=ctx.dialect + ) + df = engine_adapter.fetchdf(query, quote_identifiers=True) + assert len(df) == 1 + assert df.iloc[0].to_dict() == {"id": 1, "name": "foo"} diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index c0ccd6f055..bf98e42a69 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -433,3 +433,50 @@ def test_table_diff_table_name_matches_column_name(ctx: TestContext): assert row_diff.stats["join_count"] == 1 assert row_diff.full_match_count == 1 + + +def test_bigframe_python_model_column_order(ctx: TestContext, tmp_path: Path): + model_name = ctx.table("TEST") + + (tmp_path / "models").mkdir() + + # note: this model deliberately defines the columns in the @model definition to be in a different order than what + # is returned by the DataFrame within the model + model_path = tmp_path / "models" / "python_model.py" + + # python model that emits a BigFrame dataframe + model_path.write_text( + """ +from bigframes.pandas import DataFrame +import typing as t +from sqlmesh import ExecutionContext, model + +@model( + 'MODEL_NAME', + columns={ + "id": "int", + "name": "varchar" + }, + dialect="bigquery" +) +def execute( + context: ExecutionContext, + **kwargs: t.Any, +) -> DataFrame: + return DataFrame({'name': ['foo'], 'id': [1]}, session=context.bigframe) +""".replace("MODEL_NAME", model_name.sql(dialect="bigquery")) + ) + + sqlmesh_ctx = ctx.create_context(path=tmp_path) + + assert len(sqlmesh_ctx.models) == 1 + + plan = sqlmesh_ctx.plan(auto_apply=True) + assert len(plan.new_snapshots) == 1 + + engine_adapter = sqlmesh_ctx.engine_adapter + + query = exp.select("*").from_(model_name) + df = engine_adapter.fetchdf(query, quote_identifiers=True) + assert len(df) == 1 + assert df.iloc[0].to_dict() == {"id": 1, "name": "foo"} diff --git a/tests/core/engine_adapter/integration/test_integration_snowflake.py b/tests/core/engine_adapter/integration/test_integration_snowflake.py index ddc7ec9f2a..7c02e1b0a7 100644 --- a/tests/core/engine_adapter/integration/test_integration_snowflake.py +++ b/tests/core/engine_adapter/integration/test_integration_snowflake.py @@ -1,6 +1,7 @@ import typing as t import pytest from sqlglot import exp +from pathlib import Path from sqlglot.optimizer.qualify_columns import quote_identifiers from sqlglot.helper import seq_get from sqlmesh.core.engine_adapter import SnowflakeEngineAdapter @@ -210,3 +211,49 @@ def test_create_iceberg_table(ctx: TestContext, engine_adapter: SnowflakeEngineA result = sqlmesh.plan(auto_apply=True) assert len(result.new_snapshots) == 2 + + +def test_snowpark_python_model_column_order(ctx: TestContext, tmp_path: Path): + model_name = ctx.table("TEST") + + (tmp_path / "models").mkdir() + + # note: this model deliberately defines the columns in the @model definition to be in a different order than what + # is returned by the DataFrame within the model + model_path = tmp_path / "models" / "python_model.py" + + # python model that emits a Snowpark DataFrame + model_path.write_text( + """ +from snowflake.snowpark.dataframe import DataFrame +import typing as t +from sqlmesh import ExecutionContext, model + +@model( + 'MODEL_NAME', + columns={ + "id": "int", + "name": "varchar" + } +) +def execute( + context: ExecutionContext, + **kwargs: t.Any, +) -> DataFrame: + return context.snowpark.create_dataframe([["foo", 1]], schema=["name", "id"]) +""".replace("MODEL_NAME", model_name.sql(dialect="snowflake")) + ) + + sqlmesh_ctx = ctx.create_context(path=tmp_path) + + assert len(sqlmesh_ctx.models) == 1 + + plan = sqlmesh_ctx.plan(auto_apply=True) + assert len(plan.new_snapshots) == 1 + + engine_adapter = sqlmesh_ctx.engine_adapter + + query = exp.select("*").from_(plan.environment.snapshots[0].fully_qualified_table) + df = engine_adapter.fetchdf(query, quote_identifiers=True) + assert len(df) == 1 + assert df.iloc[0].to_dict() == {"id": 1, "name": "foo"} diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index edf222460f..b242aa409b 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -966,7 +966,7 @@ def test_merge_upsert(make_mocked_engine_adapter: t.Callable, assert_exp_eq): def test_merge_upsert_pandas(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) - df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + df = pd.DataFrame({"id": [1, 2, 3], "ts": [4, 5, 6], "val": [1, 2, 3]}) adapter.merge( target_table="target", source_table=df, @@ -978,7 +978,7 @@ def test_merge_upsert_pandas(make_mocked_engine_adapter: t.Callable): unique_key=[exp.to_identifier("id")], ) adapter.cursor.execute.assert_called_once_with( - 'MERGE INTO "target" AS "__MERGE_TARGET__" USING (SELECT CAST("id" AS INT) AS "id", CAST("ts" AS TIMESTAMP) AS "ts", CAST("val" AS INT) AS "val" FROM (VALUES (1, 4), (2, 5), (3, 6)) AS "t"("id", "ts", "val")) AS "__MERGE_SOURCE__" ON "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id" ' + 'MERGE INTO "target" AS "__MERGE_TARGET__" USING (SELECT CAST("id" AS INT) AS "id", CAST("ts" AS TIMESTAMP) AS "ts", CAST("val" AS INT) AS "val" FROM (VALUES (1, 4, 1), (2, 5, 2), (3, 6, 3)) AS "t"("id", "ts", "val")) AS "__MERGE_SOURCE__" ON "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id" ' 'WHEN MATCHED THEN UPDATE SET "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id", "__MERGE_TARGET__"."ts" = "__MERGE_SOURCE__"."ts", "__MERGE_TARGET__"."val" = "__MERGE_SOURCE__"."val" ' 'WHEN NOT MATCHED THEN INSERT ("id", "ts", "val") VALUES ("__MERGE_SOURCE__"."id", "__MERGE_SOURCE__"."ts", "__MERGE_SOURCE__"."val")' ) @@ -995,7 +995,7 @@ def test_merge_upsert_pandas(make_mocked_engine_adapter: t.Callable): unique_key=[exp.to_identifier("id"), exp.to_identifier("ts")], ) adapter.cursor.execute.assert_called_once_with( - 'MERGE INTO "target" AS "__MERGE_TARGET__" USING (SELECT CAST("id" AS INT) AS "id", CAST("ts" AS TIMESTAMP) AS "ts", CAST("val" AS INT) AS "val" FROM (VALUES (1, 4), (2, 5), (3, 6)) AS "t"("id", "ts", "val")) AS "__MERGE_SOURCE__" ON "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id" AND "__MERGE_TARGET__"."ts" = "__MERGE_SOURCE__"."ts" ' + 'MERGE INTO "target" AS "__MERGE_TARGET__" USING (SELECT CAST("id" AS INT) AS "id", CAST("ts" AS TIMESTAMP) AS "ts", CAST("val" AS INT) AS "val" FROM (VALUES (1, 4, 1), (2, 5, 2), (3, 6, 3)) AS "t"("id", "ts", "val")) AS "__MERGE_SOURCE__" ON "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id" AND "__MERGE_TARGET__"."ts" = "__MERGE_SOURCE__"."ts" ' 'WHEN MATCHED THEN UPDATE SET "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id", "__MERGE_TARGET__"."ts" = "__MERGE_SOURCE__"."ts", "__MERGE_TARGET__"."val" = "__MERGE_SOURCE__"."val" ' 'WHEN NOT MATCHED THEN INSERT ("id", "ts", "val") VALUES ("__MERGE_SOURCE__"."id", "__MERGE_SOURCE__"."ts", "__MERGE_SOURCE__"."val")' ) @@ -1175,23 +1175,23 @@ def test_merge_filter(make_mocked_engine_adapter: t.Callable, assert_exp_eq): """ MERGE INTO "target" AS "__MERGE_TARGET__" USING ( - SELECT "ID", "ts", "val" + SELECT "ID", "ts", "val" FROM "source" ) AS "__MERGE_SOURCE__" ON ( - "__MERGE_SOURCE__"."ID" > 0 + "__MERGE_SOURCE__"."ID" > 0 AND "__MERGE_TARGET__"."ts" < TIMESTAMP("2020-02-05") ) AND "__MERGE_TARGET__"."ID" = "__MERGE_SOURCE__"."ID" -WHEN MATCHED THEN - UPDATE SET +WHEN MATCHED THEN + UPDATE SET "__MERGE_TARGET__"."val" = "__MERGE_SOURCE__"."val", "__MERGE_TARGET__"."ts" = COALESCE("__MERGE_SOURCE__"."ts", "__MERGE_TARGET__"."ts") -WHEN NOT MATCHED THEN - INSERT ("ID", "ts", "val") +WHEN NOT MATCHED THEN + INSERT ("ID", "ts", "val") VALUES ( - "__MERGE_SOURCE__"."ID", - "__MERGE_SOURCE__"."ts", + "__MERGE_SOURCE__"."ID", + "__MERGE_SOURCE__"."ts", "__MERGE_SOURCE__"."val" ); """, @@ -1585,7 +1585,11 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "id2": [4, 5, 6], "name": ["muffins", "chips", "soda"], "price": [4.0, 5.0, 6.0], - "updated_at": ["2020-01-01 10:00:00", "2020-01-02 15:00:00", "2020-01-03 12:00:00"], + "test_updated_at": [ + "2020-01-01 10:00:00", + "2020-01-02 15:00:00", + "2020-01-03 12:00:00", + ], } ) adapter.scd_type_2_by_time( diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index 098d329079..54cf377517 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -424,6 +424,10 @@ def test_replace_query_snowpark_dataframe( from snowflake.snowpark.dataframe import DataFrame as SnowparkDataFrame session = Session.builder.config("local_testing", True).create() + # df.createOrReplaceTempView() throws "[Local Testing] Mocking SnowflakePlan Rename is not supported" when used against the Snowflake local_testing session + # since we cant trace any queries from the Snowpark library anyway, we just suppress this and verify the cleanup queries issued by our EngineAdapter + session._conn._suppress_not_implemented_error = True + df: SnowparkDataFrame = session.create_dataframe([(1, "name")], schema=["ID", "NAME"]) assert isinstance(df, SnowparkDataFrame) @@ -439,11 +443,6 @@ def test_replace_query_snowpark_dataframe( columns_to_types={"ID": exp.DataType.build("INT"), "NAME": exp.DataType.build("VARCHAR")}, ) - # the Snowflake library generates "CREATE TEMPORARY VIEW" from a direct DataFrame call - # which doesnt pass through our EngineAdapter so we cant capture it - spy.assert_called() - assert "__temp_foo_e6wjkjj6" in spy.call_args[0][0] - # verify that DROP VIEW is called instead of DROP TABLE assert to_sql_calls(adapter) == [ 'CREATE OR REPLACE TABLE "foo" AS SELECT CAST("ID" AS INT) AS "ID", CAST("NAME" AS VARCHAR) AS "NAME" FROM (SELECT CAST("ID" AS INT) AS "ID", CAST("NAME" AS VARCHAR) AS "NAME" FROM "__temp_foo_e6wjkjj6") AS "_subquery"', From a015761e5d8e7201c30700c87165a1ba95efd98e Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 13 May 2025 11:16:05 +0200 Subject: [PATCH 0145/1056] chore: add dependabot for npm (#4373) --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..aff82a102d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" From c41a91cc12c555bea8266a6c7bb9a4f9cbb0754b Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 13 May 2025 11:17:46 +0200 Subject: [PATCH 0146/1056] chore: update minor node dependencies (#4374) --- package-lock.json | 4591 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 4063 insertions(+), 528 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5e3d9620b0..5a1fd32ae1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,31 +13,17 @@ "prettier": "^3.5.2" } }, - "node_modules/@75lb/deep-merge": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21", - "typical": "^7.1.1" - }, - "engines": { - "node": ">=12.17" - } - }, - "node_modules/@75lb/deep-merge/node_modules/typical": { - "version": "7.3.0", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, "node_modules/@adobe/css-tools": { "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", + "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==", "dev": true, "license": "MIT" }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "license": "MIT", "engines": { "node": ">=10" @@ -48,6 +34,8 @@ }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "11.7.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", + "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", "dev": true, "license": "MIT", "dependencies": { @@ -64,6 +52,8 @@ }, "node_modules/@apidevtools/openapi-schemas": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", "dev": true, "license": "MIT", "engines": { @@ -72,11 +62,15 @@ }, "node_modules/@apidevtools/swagger-methods": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", "dev": true, "license": "MIT" }, "node_modules/@apidevtools/swagger-parser": { "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz", + "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", "dev": true, "license": "MIT", "dependencies": { @@ -94,6 +88,8 @@ }, "node_modules/@apidevtools/swagger-parser/node_modules/ajv": { "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { @@ -109,6 +105,8 @@ }, "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -122,11 +120,15 @@ }, "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/@asyncapi/specs": { "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-6.8.1.tgz", + "integrity": "sha512-czHoAk3PeXTLR+X8IUaD+IpT+g+zUvkcgMDJVothBsan+oHN3jfcFcFUNdOPAAFoUCQN1hXF1dWuphWy05THlA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -135,6 +137,8 @@ }, "node_modules/@azure/abort-controller": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, "license": "MIT", "dependencies": { @@ -146,6 +150,8 @@ }, "node_modules/@azure/core-auth": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", + "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", "dev": true, "license": "MIT", "dependencies": { @@ -158,13 +164,15 @@ } }, "node_modules/@azure/core-client": { - "version": "1.9.3", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.4.tgz", + "integrity": "sha512-f7IxTD15Qdux30s2qFARH+JxgwxWLG2Rlr4oSkPGuLWm+1p5y1+C04XGLA0vmX6EtqfutmjvpNmAfgwVIS5hpw==", "dev": true, "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.4.0", - "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-rest-pipeline": "^1.20.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.6.1", "@azure/logger": "^1.0.0", @@ -175,7 +183,9 @@ } }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.19.1", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.20.0.tgz", + "integrity": "sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g==", "dev": true, "license": "MIT", "dependencies": { @@ -184,8 +194,7 @@ "@azure/core-tracing": "^1.0.1", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", + "@typespec/ts-http-runtime": "^0.2.2", "tslib": "^2.6.2" }, "engines": { @@ -194,6 +203,8 @@ }, "node_modules/@azure/core-tracing": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", + "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", "dev": true, "license": "MIT", "dependencies": { @@ -204,11 +215,14 @@ } }, "node_modules/@azure/core-util": { - "version": "1.11.0", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.12.0.tgz", + "integrity": "sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ==", "dev": true, "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.0.0", + "@typespec/ts-http-runtime": "^0.2.2", "tslib": "^2.6.2" }, "engines": { @@ -217,6 +231,8 @@ }, "node_modules/@azure/identity": { "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.9.1.tgz", + "integrity": "sha512-986D7Cf1AOwYqSDtO/FnMAyk/Jc8qpftkGsxuehoh4F85MhQ4fICBGX/44+X1y78lN4Sqib3Bsoaoh/FvOGgmg==", "dev": true, "license": "MIT", "dependencies": { @@ -237,10 +253,13 @@ } }, "node_modules/@azure/logger": { - "version": "1.1.4", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.2.0.tgz", + "integrity": "sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA==", "dev": true, "license": "MIT", "dependencies": { + "@typespec/ts-http-runtime": "^0.2.2", "tslib": "^2.6.2" }, "engines": { @@ -248,18 +267,22 @@ } }, "node_modules/@azure/msal-browser": { - "version": "4.11.0", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.12.0.tgz", + "integrity": "sha512-WD1lmVWchg7wn1mI7Tr4v7QPyTwK+8Nuyje3jRpOFENLRLEBsdK8VVdTw3C+TypZmYn4cOAdj3zREnuFXgvfIA==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "15.5.1" + "@azure/msal-common": "15.6.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "15.5.1", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.6.0.tgz", + "integrity": "sha512-EotmBz42apYGjqiIV9rDUdptaMptpTn4TdGf3JfjLvFvinSe9BJ6ywU92K9ky+t/b0ghbeTSe9RfqlgLh8f2jA==", "dev": true, "license": "MIT", "engines": { @@ -267,11 +290,13 @@ } }, "node_modules/@azure/msal-node": { - "version": "3.5.1", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.3.tgz", + "integrity": "sha512-c5mifzHX5mwm5JqMIlURUyp6LEEdKF1a8lmcNRLBo0lD7zpSYPHupa4jHyhJyg9ccLwszLguZJdk2h3ngnXwNw==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "15.5.1", + "@azure/msal-common": "15.6.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -280,21 +305,25 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "peer": true, @@ -303,22 +332,25 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.0", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true, "license": "MIT" }, "node_modules/@codemirror/autocomplete": { "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -329,6 +361,8 @@ }, "node_modules/@codemirror/commands": { "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", + "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -352,6 +386,8 @@ }, "node_modules/@codemirror/lang-sql": { "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz", + "integrity": "sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -364,6 +400,8 @@ }, "node_modules/@codemirror/language": { "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz", + "integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -376,6 +414,8 @@ }, "node_modules/@codemirror/legacy-modes": { "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz", + "integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0" @@ -383,6 +423,8 @@ }, "node_modules/@codemirror/lint": { "version": "6.8.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", + "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -392,6 +434,8 @@ }, "node_modules/@codemirror/search": { "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz", + "integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -401,6 +445,8 @@ }, "node_modules/@codemirror/state": { "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", "dependencies": { "@marijn/find-cluster-break": "^1.0.0" @@ -408,6 +454,8 @@ }, "node_modules/@codemirror/theme-one-dark": { "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -427,8 +475,78 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.3", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "cpu": [ "arm64" ], @@ -442,8 +560,350 @@ "node": ">=18" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -459,8 +919,23 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", "engines": { @@ -469,6 +944,8 @@ }, "node_modules/@eslint/config-array": { "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -482,6 +959,8 @@ }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -491,6 +970,8 @@ }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -501,7 +982,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -510,6 +993,8 @@ }, "node_modules/@eslint/core": { "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -521,6 +1006,8 @@ }, "node_modules/@eslint/eslintrc": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -543,6 +1030,8 @@ }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -552,6 +1041,8 @@ }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -562,7 +1053,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.25.1", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", + "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", "dev": true, "license": "MIT", "engines": { @@ -571,6 +1064,8 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -579,6 +1074,8 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -591,26 +1088,34 @@ }, "node_modules/@exodus/schemasafe": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", "dev": true, "license": "MIT" }, "node_modules/@floating-ui/core": { - "version": "1.6.9", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", + "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.13", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", + "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.6.0", + "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/react-dom": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.0.0" @@ -622,22 +1127,28 @@ }, "node_modules/@floating-ui/utils": { "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.3.0", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.4.0.tgz", + "integrity": "sha512-48lKoQegmfJ0iyR/jRz5OrYOSM3WewG9YWCPqUvYFEC54shQO8RsAaspaK/2PRHVVnjekRqfAFvq8pwCpIo5ig==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^3.3.0", - "@shikijs/langs": "^3.3.0", - "@shikijs/themes": "^3.3.0", - "@shikijs/types": "^3.3.0", + "@shikijs/engine-oniguruma": "^3.4.0", + "@shikijs/langs": "^3.4.0", + "@shikijs/themes": "^3.4.0", + "@shikijs/types": "^3.4.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@headlessui/react": { "version": "1.7.19", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", + "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", "license": "MIT", "dependencies": { "@tanstack/react-virtual": "^3.0.0-beta.60", @@ -653,6 +1164,8 @@ }, "node_modules/@heroicons/react": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", "license": "MIT", "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" @@ -660,6 +1173,8 @@ }, "node_modules/@humanfs/core": { "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -668,6 +1183,8 @@ }, "node_modules/@humanfs/node": { "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -680,6 +1197,8 @@ }, "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -692,6 +1211,8 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -717,7 +1238,9 @@ } }, "node_modules/@ibm-cloud/openapi-ruleset": { - "version": "1.31.0", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset/-/openapi-ruleset-1.31.1.tgz", + "integrity": "sha512-3WK2FREmDA2aadCjD71PE7tx5evyvmhg80ts1kXp2IzXIA0ZJ7guGM66tj40kxaqwpMSGchwEnnfYswntav76g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -739,6 +1262,8 @@ }, "node_modules/@ibm-cloud/openapi-ruleset-utilities": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset-utilities/-/openapi-ruleset-utilities-1.9.0.tgz", + "integrity": "sha512-AoFbSarOqFBYH+1TZ9Ahkm2IWYSi5v0pBk88fpV+5b3qGJukypX8PwvCWADjuyIccKg48/F73a6hTTkBzDQ2UA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -747,6 +1272,8 @@ }, "node_modules/@ibm-cloud/openapi-ruleset/node_modules/minimatch": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", "dev": true, "license": "ISC", "dependencies": { @@ -761,6 +1288,8 @@ }, "node_modules/@isaacs/cliui": { "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -776,6 +1305,8 @@ }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, "license": "MIT", "engines": { @@ -784,6 +1315,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -796,6 +1329,8 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -803,6 +1338,8 @@ }, "node_modules/@jridgewell/set-array": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -810,6 +1347,8 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "license": "MIT", "peer": true, @@ -820,10 +1359,14 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -832,11 +1375,15 @@ }, "node_modules/@jsdevtools/ono": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "dev": true, "license": "MIT" }, "node_modules/@jsep-plugin/assignment": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", "dev": true, "license": "MIT", "engines": { @@ -848,6 +1395,8 @@ }, "node_modules/@jsep-plugin/regex": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", "dev": true, "license": "MIT", "engines": { @@ -859,6 +1408,8 @@ }, "node_modules/@jsep-plugin/ternary": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", + "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", "dev": true, "license": "MIT", "engines": { @@ -870,10 +1421,14 @@ }, "node_modules/@lezer/common": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", "license": "MIT" }, "node_modules/@lezer/highlight": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" @@ -881,6 +1436,8 @@ }, "node_modules/@lezer/lr": { "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" @@ -888,6 +1445,8 @@ }, "node_modules/@lezer/python": { "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -897,6 +1456,8 @@ }, "node_modules/@lit/react": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.7.tgz", + "integrity": "sha512-cencnwwLXQKiKxjfFzSgZRngcWJzUDZi/04E0fSaF86wZgchMdvTyu+lE36DrUfvuus3bH8+xLPrhM1cTjwpzw==", "license": "BSD-3-Clause", "peerDependencies": { "@types/react": "17 || 18 || 19" @@ -904,10 +1465,36 @@ }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz", + "integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -919,6 +1506,8 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "license": "MIT", "engines": { "node": ">= 8" @@ -926,6 +1515,8 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -937,6 +1528,8 @@ }, "node_modules/@orval/angular": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/angular/-/angular-7.9.0.tgz", + "integrity": "sha512-GzgEdZxK/9wQMLN2bziTlPSD9bkRXwYf1PoUM+RTXj6MGw0aZVWNTMCnp3dFWp9VemThP0kK2geBFqhxC2Bgxg==", "dev": true, "license": "MIT", "dependencies": { @@ -945,6 +1538,8 @@ }, "node_modules/@orval/axios": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/axios/-/axios-7.9.0.tgz", + "integrity": "sha512-e77WvQGfFTkkrJIH66v/DpKdZ1eQBlu4NxOt2gCxvBYFP2dxDR2ajsM7uXxdGxi0iqZIS92+opzhxLIo6TVyDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -953,6 +1548,8 @@ }, "node_modules/@orval/core": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/core/-/core-7.9.0.tgz", + "integrity": "sha512-/Nn6/ARmpevAY7Vl9RRXY2WkJx/q0LIUEE2Eh15bGgzAQIYUcD9aFr9zM5hX2b3lR/fZ8721hFsq0vM9O5ZzXw==", "dev": true, "license": "MIT", "dependencies": { @@ -978,6 +1575,8 @@ }, "node_modules/@orval/core/node_modules/ajv": { "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { @@ -993,11 +1592,15 @@ }, "node_modules/@orval/core/node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/@orval/core/node_modules/openapi3-ts": { "version": "4.4.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", + "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", "dev": true, "license": "MIT", "dependencies": { @@ -1006,6 +1609,8 @@ }, "node_modules/@orval/fetch": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-7.9.0.tgz", + "integrity": "sha512-gIw2a3jXd1If/NpewVq7C6XDfnG2RPMt4PKR/RtEBeDKasXkoJeS2DBvZp/TyC+lt9oMgketF3bmzo/st09uhA==", "dev": true, "license": "MIT", "dependencies": { @@ -1014,6 +1619,8 @@ }, "node_modules/@orval/hono": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/hono/-/hono-7.9.0.tgz", + "integrity": "sha512-80VoS5W4I0uUo7Y6sIxr0xGYNX3oIL8sKWru+mYIZdp2L4W1lVHBi4zkpk7u0u9Obv7vmAuPtozV5+QIV0zWBg==", "dev": true, "license": "MIT", "dependencies": { @@ -1024,6 +1631,8 @@ }, "node_modules/@orval/mcp": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/mcp/-/mcp-7.9.0.tgz", + "integrity": "sha512-zMtW4jUKXGiXyJUylVy58kCu/Jf1yF9wp3ul2Guy1vbjlhVeOO1ugCYQ1sYNYH10vN0ajTS0/2pXhTuCR7PxHw==", "dev": true, "license": "MIT", "dependencies": { @@ -1033,6 +1642,8 @@ }, "node_modules/@orval/mock": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/mock/-/mock-7.9.0.tgz", + "integrity": "sha512-Ixhb+I4VTIfUl0qxDq8LekBnXM2gpD4kS7OFVqX9rdx0ZwZl7y4xArnKSXk5qgDPjo4eOWSmVA3onuL2WCit/g==", "dev": true, "license": "MIT", "dependencies": { @@ -1042,6 +1653,8 @@ }, "node_modules/@orval/query": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/query/-/query-7.9.0.tgz", + "integrity": "sha512-IPKP4l00dZw0AOt+7PPB82WNdmpPThsWlYvk4YmdLEZVWCuaUaJ9KyLTT2R+XqoksuuuKTJ/IK0KO7VcjMiHiA==", "dev": true, "license": "MIT", "dependencies": { @@ -1052,6 +1665,8 @@ }, "node_modules/@orval/swr": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/swr/-/swr-7.9.0.tgz", + "integrity": "sha512-f06MifzMPrnXYdgt2rLLnurJ0YlXexSMyVlXAHhaJENtSVM4zlJp69rA6OULLr1i1biNGTWHSonOizKOf+cNcw==", "dev": true, "license": "MIT", "dependencies": { @@ -1061,6 +1676,8 @@ }, "node_modules/@orval/zod": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@orval/zod/-/zod-7.9.0.tgz", + "integrity": "sha512-LkkofL+iSswsBVWCr3bPZ6un8065wB7x34DZVnF/gN3kBxy8A35I9X0Eld4EajI/1rbMmxzt7wHUYAf5NlJWHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1070,6 +1687,8 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "license": "MIT", "optional": true, "engines": { @@ -1078,6 +1697,8 @@ }, "node_modules/@playwright/test": { "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1092,6 +1713,8 @@ }, "node_modules/@radix-ui/number": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -1486,6 +2109,8 @@ }, "node_modules/@radix-ui/react-select": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz", + "integrity": "sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1528,6 +2153,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -1535,6 +2162,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", + "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1557,6 +2186,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", + "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1582,6 +2213,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -1598,6 +2231,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -1614,6 +2249,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -1630,6 +2267,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", + "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1656,6 +2295,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -1672,6 +2313,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", + "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1696,6 +2339,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1713,6 +2358,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", + "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1744,6 +2391,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", + "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1766,6 +2415,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1788,6 +2439,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1805,6 +2458,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -1821,6 +2476,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1838,6 +2495,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1855,6 +2514,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -1871,6 +2532,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", + "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1888,6 +2551,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", + "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -1905,6 +2570,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", + "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -1912,6 +2579,8 @@ }, "node_modules/@radix-ui/react-select/node_modules/react-remove-scroll": { "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.3", @@ -2038,6 +2707,8 @@ }, "node_modules/@radix-ui/react-use-previous": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -2090,6 +2761,8 @@ }, "node_modules/@radix-ui/react-visually-hidden": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", + "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -2112,6 +2785,8 @@ }, "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -2128,6 +2803,8 @@ }, "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -2150,6 +2827,8 @@ }, "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -2173,18 +2852,26 @@ }, "node_modules/@react-dnd/asap": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==", "license": "MIT" }, "node_modules/@react-dnd/invariant": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==", "license": "MIT" }, "node_modules/@react-dnd/shallowequal": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", "license": "MIT" }, "node_modules/@reactflow/background": { "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", "license": "MIT", "dependencies": { "@reactflow/core": "11.11.4", @@ -2196,8 +2883,38 @@ "react-dom": ">=17" } }, + "node_modules/@reactflow/background/node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/@reactflow/controls": { "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", "license": "MIT", "dependencies": { "@reactflow/core": "11.11.4", @@ -2209,8 +2926,38 @@ "react-dom": ">=17" } }, + "node_modules/@reactflow/controls/node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/@reactflow/core": { "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", "license": "MIT", "dependencies": { "@types/d3": "^7.4.0", @@ -2228,8 +2975,38 @@ "react-dom": ">=17" } }, + "node_modules/@reactflow/core/node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/@reactflow/minimap": { "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", "license": "MIT", "dependencies": { "@reactflow/core": "11.11.4", @@ -2245,14 +3022,87 @@ "react-dom": ">=17" } }, - "node_modules/@reactflow/node-resizer": { - "version": "2.2.14", + "node_modules/@reactflow/minimap/node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer/node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", "license": "MIT", "dependencies": { "@reactflow/core": "11.11.4", - "classcat": "^5.0.4", - "d3-drag": "^3.0.0", - "d3-selection": "^3.0.0", + "classcat": "^5.0.3", "zustand": "^4.4.1" }, "peerDependencies": { @@ -2260,30 +3110,47 @@ "react-dom": ">=17" } }, - "node_modules/@reactflow/node-toolbar": { - "version": "1.3.14", + "node_modules/@reactflow/node-toolbar/node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", "license": "MIT", "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" }, "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } } }, "node_modules/@remix-run/router": { "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", "cpu": [ "arm" ], @@ -2295,9 +3162,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", "cpu": [ "arm64" ], @@ -2309,7 +3176,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.1", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", "cpu": [ "arm64" ], @@ -2321,9 +3190,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", "cpu": [ "x64" ], @@ -2335,9 +3204,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", "cpu": [ "arm64" ], @@ -2349,9 +3218,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", "cpu": [ "x64" ], @@ -2363,9 +3232,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", "cpu": [ "arm" ], @@ -2377,9 +3246,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", "cpu": [ "arm" ], @@ -2391,9 +3260,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", "cpu": [ "arm64" ], @@ -2405,9 +3274,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", "cpu": [ "arm64" ], @@ -2419,9 +3288,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", "cpu": [ "loong64" ], @@ -2433,9 +3302,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", "cpu": [ "ppc64" ], @@ -2447,9 +3316,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", "cpu": [ "riscv64" ], @@ -2461,9 +3330,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", "cpu": [ "riscv64" ], @@ -2475,9 +3344,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", "cpu": [ "s390x" ], @@ -2489,9 +3358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", "cpu": [ "x64" ], @@ -2503,9 +3372,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", "cpu": [ "x64" ], @@ -2517,9 +3386,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", "cpu": [ "arm64" ], @@ -2531,9 +3400,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", "cpu": [ "ia32" ], @@ -2545,9 +3414,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", "cpu": [ "x64" ], @@ -2559,32 +3428,40 @@ ] }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.3.0", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.4.0.tgz", + "integrity": "sha512-zwcWlZ4OQuJ/+1t32ClTtyTU1AiDkK1lhtviRWoq/hFqPjCNyLj22bIg9rB7BfoZKOEOfrsGz7No33BPCf+WlQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.3.0", + "@shikijs/types": "3.4.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "3.3.0", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.4.0.tgz", + "integrity": "sha512-bQkR+8LllaM2duU9BBRQU0GqFTx7TuF5kKlw/7uiGKoK140n1xlLAwCgXwSxAjJ7Htk9tXTFwnnsJTCU5nDPXQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.3.0" + "@shikijs/types": "3.4.0" } }, "node_modules/@shikijs/themes": { - "version": "3.3.0", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.4.0.tgz", + "integrity": "sha512-YPP4PKNFcFGLxItpbU0ZW1Osyuk8AyZ24YEFaq04CFsuCbcqydMvMUTi40V2dkc0qs1U2uZFrnU6s5zI6IH+uA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.3.0" + "@shikijs/types": "3.4.0" } }, "node_modules/@shikijs/types": { - "version": "3.3.0", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.4.0.tgz", + "integrity": "sha512-EUT/0lGiE//7j5N/yTMNMT3eCWNcHJLrRKxT0NDXWIfdfSmFJKfPX7nMmRBrQnWboAzIsUziCThrYMMhjbMS1A==", "dev": true, "license": "MIT", "dependencies": { @@ -2594,11 +3471,15 @@ }, "node_modules/@shikijs/vscode-textmate": { "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "dev": true, "license": "MIT" }, "node_modules/@stoplight/json": { "version": "3.21.7", + "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", + "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2615,6 +3496,8 @@ }, "node_modules/@stoplight/json-ref-readers": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-readers/-/json-ref-readers-1.2.2.tgz", + "integrity": "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2627,11 +3510,15 @@ }, "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, "license": "0BSD" }, "node_modules/@stoplight/json-ref-resolver": { "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz", + "integrity": "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2652,11 +3539,15 @@ }, "node_modules/@stoplight/json/node_modules/jsonc-parser": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", + "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", "dev": true, "license": "MIT" }, "node_modules/@stoplight/ordered-object-literal": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", + "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2665,6 +3556,8 @@ }, "node_modules/@stoplight/path": { "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz", + "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2673,6 +3566,8 @@ }, "node_modules/@stoplight/spectral-core": { "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.20.0.tgz", + "integrity": "sha512-5hBP81nCC1zn1hJXL/uxPNRKNcB+/pEIHgCjPRpl/w/qy9yC9ver04tw1W0l/PMiv0UeB5dYgozXVQ4j5a6QQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2704,6 +3599,8 @@ }, "node_modules/@stoplight/spectral-core/node_modules/@stoplight/better-ajv-errors": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2719,6 +3616,8 @@ }, "node_modules/@stoplight/spectral-core/node_modules/ajv": { "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { @@ -2734,6 +3633,8 @@ }, "node_modules/@stoplight/spectral-core/node_modules/ajv-errors": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2742,6 +3643,8 @@ }, "node_modules/@stoplight/spectral-core/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -2751,11 +3654,15 @@ }, "node_modules/@stoplight/spectral-core/node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/@stoplight/spectral-core/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -2767,6 +3674,8 @@ }, "node_modules/@stoplight/spectral-formats": { "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-formats/-/spectral-formats-1.8.2.tgz", + "integrity": "sha512-c06HB+rOKfe7tuxg0IdKDEA5XnjL2vrn/m/OVIIxtINtBzphZrOgtRn7epQ5bQF5SWp84Ue7UJWaGgDwVngMFw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2781,6 +3690,8 @@ }, "node_modules/@stoplight/spectral-functions": { "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.1.tgz", + "integrity": "sha512-obu8ZfoHxELOapfGsCJixKZXZcffjg+lSoNuttpmUFuDzVLT3VmH8QkPXfOGOL5Pz80BR35ClNAToDkdnYIURg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2802,6 +3713,8 @@ }, "node_modules/@stoplight/spectral-functions/node_modules/@stoplight/better-ajv-errors": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2817,6 +3730,8 @@ }, "node_modules/@stoplight/spectral-functions/node_modules/ajv": { "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { @@ -2832,6 +3747,8 @@ }, "node_modules/@stoplight/spectral-functions/node_modules/ajv-draft-04": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2845,6 +3762,8 @@ }, "node_modules/@stoplight/spectral-functions/node_modules/ajv-errors": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2853,11 +3772,15 @@ }, "node_modules/@stoplight/spectral-functions/node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/@stoplight/spectral-parsers": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.5.tgz", + "integrity": "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2872,6 +3795,8 @@ }, "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": { "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2884,6 +3809,8 @@ }, "node_modules/@stoplight/spectral-ref-resolver": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.5.tgz", + "integrity": "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2899,6 +3826,8 @@ }, "node_modules/@stoplight/spectral-rulesets": { "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.22.0.tgz", + "integrity": "sha512-l2EY2jiKKLsvnPfGy+pXC0LeGsbJzcQP5G/AojHgf+cwN//VYxW1Wvv4WKFx/CLmLxc42mJYF2juwWofjWYNIQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2924,6 +3853,8 @@ }, "node_modules/@stoplight/spectral-rulesets/node_modules/@stoplight/better-ajv-errors": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2939,6 +3870,8 @@ }, "node_modules/@stoplight/spectral-rulesets/node_modules/ajv": { "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { @@ -2954,11 +3887,15 @@ }, "node_modules/@stoplight/spectral-rulesets/node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/@stoplight/spectral-runtime": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-runtime/-/spectral-runtime-1.1.4.tgz", + "integrity": "sha512-YHbhX3dqW0do6DhiPSgSGQzr6yQLlWybhKwWx0cqxjMwxej3TqLv3BXMfIUYFKKUqIwH4Q2mV8rrMM8qD2N0rQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2976,6 +3913,8 @@ }, "node_modules/@stoplight/types": { "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", + "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2988,6 +3927,8 @@ }, "node_modules/@stoplight/yaml": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz", + "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3002,11 +3943,15 @@ }, "node_modules/@stoplight/yaml-ast-parser": { "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz", + "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==", "dev": true, "license": "Apache-2.0" }, "node_modules/@stoplight/yaml/node_modules/@stoplight/types": { "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3019,6 +3964,8 @@ }, "node_modules/@swc/core": { "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", + "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -3056,6 +4003,8 @@ }, "node_modules/@swc/core-darwin-arm64": { "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", + "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", "cpu": [ "arm64" ], @@ -3069,6 +4018,74 @@ "node": ">=10" } }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", + "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", + "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", + "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", + "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/core-linux-x64-gnu": { "version": "1.11.24", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", @@ -3085,8 +4102,78 @@ "node": ">=10" } }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", + "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", + "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", + "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", + "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true, "license": "Apache-2.0" }, @@ -3101,6 +4188,8 @@ }, "node_modules/@swc/types": { "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3109,6 +4198,8 @@ }, "node_modules/@tailwindcss/container-queries": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", + "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", "license": "MIT", "peerDependencies": { "tailwindcss": ">=3.2.0" @@ -3116,6 +4207,8 @@ }, "node_modules/@tanstack/query-core": { "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", "license": "MIT", "funding": { "type": "github", @@ -3124,6 +4217,8 @@ }, "node_modules/@tanstack/react-query": { "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", "license": "MIT", "dependencies": { "@tanstack/query-core": "4.36.1", @@ -3149,6 +4244,8 @@ }, "node_modules/@tanstack/react-table": { "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", "license": "MIT", "dependencies": { "@tanstack/table-core": "8.21.3" @@ -3166,10 +4263,12 @@ } }, "node_modules/@tanstack/react-virtual": { - "version": "3.13.6", + "version": "3.13.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.8.tgz", + "integrity": "sha512-meS2AanUg50f3FBSNoAdBSRAh8uS0ue01qm7zrw65KGJtiXB9QXfybqZwkh4uFpRv2iX/eu5tjcH5wqUpwYLPg==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.13.6" + "@tanstack/virtual-core": "3.13.8" }, "funding": { "type": "github", @@ -3182,6 +4281,8 @@ }, "node_modules/@tanstack/table-core": { "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", "license": "MIT", "engines": { "node": ">=12" @@ -3192,7 +4293,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.13.6", + "version": "3.13.8", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.8.tgz", + "integrity": "sha512-BT6w89Hqy7YKaWewYzmecXQzcJh6HTBbKYJIIkMaNU49DZ06LoTV3z32DWWEdUsgW6n1xTmwTLs4GtWrZC261w==", "license": "MIT", "funding": { "type": "github", @@ -3222,6 +4325,8 @@ }, "node_modules/@testing-library/jest-dom": { "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", "dev": true, "license": "MIT", "dependencies": { @@ -3241,6 +4346,8 @@ }, "node_modules/@testing-library/jest-dom/node_modules/chalk": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dev": true, "license": "MIT", "dependencies": { @@ -3253,11 +4360,15 @@ }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", "dev": true, "license": "MIT" }, "node_modules/@testing-library/jest-dom/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -3267,8 +4378,38 @@ "node": ">=8" } }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@testing-library/user-event": { "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "dev": true, "license": "MIT", "engines": { @@ -3281,6 +4422,8 @@ }, "node_modules/@tootallnate/once": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true, "license": "MIT", "engines": { @@ -3289,6 +4432,8 @@ }, "node_modules/@types/aria-query": { "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, "license": "MIT", "peer": true @@ -3307,6 +4452,8 @@ }, "node_modules/@types/d3": { "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", "license": "MIT", "dependencies": { "@types/d3-array": "*", @@ -3343,10 +4490,14 @@ }, "node_modules/@types/d3-array": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", "license": "MIT" }, "node_modules/@types/d3-axis": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -3354,6 +4505,8 @@ }, "node_modules/@types/d3-brush": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -3361,14 +4514,20 @@ }, "node_modules/@types/d3-chord": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, "node_modules/@types/d3-contour": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", "license": "MIT", "dependencies": { "@types/d3-array": "*", @@ -3377,14 +4536,20 @@ }, "node_modules/@types/d3-delaunay": { "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", "license": "MIT" }, "node_modules/@types/d3-dispatch": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", "license": "MIT" }, "node_modules/@types/d3-drag": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -3392,14 +4557,20 @@ }, "node_modules/@types/d3-dsv": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, "node_modules/@types/d3-fetch": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", "license": "MIT", "dependencies": { "@types/d3-dsv": "*" @@ -3407,14 +4578,20 @@ }, "node_modules/@types/d3-force": { "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", "license": "MIT" }, "node_modules/@types/d3-format": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", "license": "MIT" }, "node_modules/@types/d3-geo": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", "license": "MIT", "dependencies": { "@types/geojson": "*" @@ -3422,10 +4599,14 @@ }, "node_modules/@types/d3-hierarchy": { "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "license": "MIT", "dependencies": { "@types/d3-color": "*" @@ -3433,22 +4614,32 @@ }, "node_modules/@types/d3-path": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, "node_modules/@types/d3-polygon": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", "license": "MIT" }, "node_modules/@types/d3-quadtree": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", "license": "MIT" }, "node_modules/@types/d3-random": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "license": "MIT", "dependencies": { "@types/d3-time": "*" @@ -3456,14 +4647,20 @@ }, "node_modules/@types/d3-scale-chromatic": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", "license": "MIT" }, "node_modules/@types/d3-selection": { "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "license": "MIT" }, "node_modules/@types/d3-shape": { "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -3471,18 +4668,26 @@ }, "node_modules/@types/d3-time": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, "node_modules/@types/d3-time-format": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, "node_modules/@types/d3-transition": { "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -3490,6 +4695,8 @@ }, "node_modules/@types/d3-zoom": { "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", "license": "MIT", "dependencies": { "@types/d3-interpolate": "*", @@ -3498,6 +4705,8 @@ }, "node_modules/@types/debug": { "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "license": "MIT", "dependencies": { "@types/ms": "*" @@ -3505,11 +4714,15 @@ }, "node_modules/@types/diff": { "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz", + "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==", "dev": true, "license": "MIT" }, "node_modules/@types/es-aggregate-error": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", + "integrity": "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==", "dev": true, "license": "MIT", "dependencies": { @@ -3518,6 +4731,8 @@ }, "node_modules/@types/eslint": { "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", "peer": true, @@ -3528,6 +4743,8 @@ }, "node_modules/@types/eslint-scope": { "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "license": "MIT", "peer": true, @@ -3538,10 +4755,14 @@ }, "node_modules/@types/estree": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "license": "MIT" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", "license": "MIT", "dependencies": { "@types/estree": "*" @@ -3549,6 +4770,8 @@ }, "node_modules/@types/fs-extra": { "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", "license": "MIT", "dependencies": { "@types/jsonfile": "*", @@ -3557,10 +4780,14 @@ }, "node_modules/@types/geojson": { "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, "node_modules/@types/hast": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -3568,16 +4795,22 @@ }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/jsonfile": { "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -3585,6 +4818,8 @@ }, "node_modules/@types/mdast": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -3592,35 +4827,37 @@ }, "node_modules/@types/mocha": { "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", "dev": true, "license": "MIT" }, "node_modules/@types/ms": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.46", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz", - "integrity": "sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==", + "version": "20.11.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", + "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~5.26.4" } }, - "node_modules/@types/node/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "license": "MIT" - }, "node_modules/@types/pluralize": { "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.30.tgz", + "integrity": "sha512-kVww6xZrW/db5BR9OqiT71J9huRdQ+z/r+LbDuT7/EK50mCmj5FoaIARnVv0rvjUS/YpDox0cDU9lpQT011VBA==", "dev": true, "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", "license": "MIT" }, "node_modules/@types/react": { @@ -3645,32 +4882,40 @@ }, "node_modules/@types/unist": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, "node_modules/@types/urijs": { "version": "1.19.25", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.25.tgz", + "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==", "dev": true, "license": "MIT" }, "node_modules/@types/vscode": { "version": "1.96.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.96.0.tgz", + "integrity": "sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.31.1", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/type-utils": "8.31.1", - "@typescript-eslint/utils": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3685,15 +4930,27 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.31.1", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "engines": { @@ -3709,12 +4966,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.31.1", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1" + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3725,14 +4984,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.31.1", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/utils": "8.31.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3747,7 +5008,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.31.1", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "dev": true, "license": "MIT", "engines": { @@ -3759,18 +5022,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.31.1", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3784,14 +5049,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.31.1", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3806,11 +5073,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.31.1", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -3821,19 +5090,25 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", + "node_modules/@typespec/ts-http-runtime": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.2.2.tgz", + "integrity": "sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=18.0.0" } }, "node_modules/@uidotdev/usehooks": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz", + "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==", "license": "MIT", "engines": { "node": ">=16" @@ -3898,10 +5173,14 @@ }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", + "integrity": "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3912,12 +5191,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", + "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", + "@vitest/spy": "3.1.3", + "@vitest/utils": "3.1.3", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -3926,11 +5207,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", + "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.2", + "@vitest/spy": "3.1.3", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -3951,7 +5234,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", + "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", "dev": true, "license": "MIT", "dependencies": { @@ -3962,11 +5247,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", + "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.2", + "@vitest/utils": "3.1.3", "pathe": "^2.0.3" }, "funding": { @@ -3974,11 +5261,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", + "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.2", + "@vitest/pretty-format": "3.1.3", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -3987,7 +5276,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", + "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3998,11 +5289,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", + "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.2", + "@vitest/pretty-format": "3.1.3", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -4012,6 +5305,8 @@ }, "node_modules/@vscode/python-extension": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@vscode/python-extension/-/python-extension-1.0.5.tgz", + "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==", "license": "MIT", "engines": { "node": ">=16.17.1", @@ -4020,6 +5315,8 @@ }, "node_modules/@vscode/test-cli": { "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz", + "integrity": "sha512-B0mMH4ia+MOOtwNiLi79XhA+MLmUItIC8FckEuKrVAVriIuSWjt7vv4+bF8qVFiNFe4QRfzPaIZk39FZGWEwHA==", "dev": true, "license": "MIT", "dependencies": { @@ -4042,6 +5339,8 @@ }, "node_modules/@vscode/test-electron": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", + "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", "dev": true, "license": "MIT", "dependencies": { @@ -4057,6 +5356,8 @@ }, "node_modules/@vscode/vsce": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.3.2.tgz", + "integrity": "sha512-XQ4IhctYalSTMwLnMS8+nUaGbU7v99Qm2sOoGfIEf2QC7jpiLXZZMh7NwArEFsKX4gHTJLx0/GqAUlCdC3gKCw==", "dev": true, "license": "MIT", "dependencies": { @@ -4097,23 +5398,55 @@ }, "node_modules/@vscode/vsce-sign": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.5.tgz", + "integrity": "sha512-GfYWrsT/vypTMDMgWDm75iDmAOMe7F71sZECJ+Ws6/xyIfmB3ELVnVN+LwMFAvmXY+e6eWhR2EzNGF/zAhWY3Q==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.2", + "@vscode/vsce-sign-alpine-x64": "2.0.2", + "@vscode/vsce-sign-darwin-arm64": "2.0.2", + "@vscode/vsce-sign-darwin-x64": "2.0.2", + "@vscode/vsce-sign-linux-arm": "2.0.2", + "@vscode/vsce-sign-linux-arm64": "2.0.2", + "@vscode/vsce-sign-linux-x64": "2.0.2", + "@vscode/vsce-sign-win32-arm64": "2.0.2", + "@vscode/vsce-sign-win32-x64": "2.0.2" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", + "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", + "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE.txt", - "optionalDependencies": { - "@vscode/vsce-sign-alpine-arm64": "2.0.2", - "@vscode/vsce-sign-alpine-x64": "2.0.2", - "@vscode/vsce-sign-darwin-arm64": "2.0.2", - "@vscode/vsce-sign-darwin-x64": "2.0.2", - "@vscode/vsce-sign-linux-arm": "2.0.2", - "@vscode/vsce-sign-linux-arm64": "2.0.2", - "@vscode/vsce-sign-linux-x64": "2.0.2", - "@vscode/vsce-sign-win32-arm64": "2.0.2", - "@vscode/vsce-sign-win32-x64": "2.0.2" - } + "optional": true, + "os": [ + "alpine" + ] }, "node_modules/@vscode/vsce-sign-darwin-arm64": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", "cpu": [ "arm64" ], @@ -4124,8 +5457,94 @@ "darwin" ] }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", + "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", + "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", + "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", + "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", + "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@vscode/vsce/node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", "dependencies": { @@ -4137,6 +5556,8 @@ }, "node_modules/@vscode/vsce/node_modules/chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4150,6 +5571,8 @@ }, "node_modules/@vscode/vsce/node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", "dependencies": { @@ -4158,11 +5581,15 @@ }, "node_modules/@vscode/vsce/node_modules/color-name": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, "license": "MIT" }, "node_modules/@vscode/vsce/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { @@ -4171,6 +5598,8 @@ }, "node_modules/@vscode/vsce/node_modules/glob": { "version": "11.0.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", + "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4193,6 +5622,8 @@ }, "node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": { "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4207,6 +5638,8 @@ }, "node_modules/@vscode/vsce/node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", "engines": { @@ -4215,6 +5648,8 @@ }, "node_modules/@vscode/vsce/node_modules/jackspeak": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -4229,6 +5664,8 @@ }, "node_modules/@vscode/vsce/node_modules/lru-cache": { "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", "dev": true, "license": "ISC", "engines": { @@ -4237,6 +5674,8 @@ }, "node_modules/@vscode/vsce/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -4248,6 +5687,8 @@ }, "node_modules/@vscode/vsce/node_modules/minimatch/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -4257,6 +5698,8 @@ }, "node_modules/@vscode/vsce/node_modules/path-scurry": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -4272,6 +5715,8 @@ }, "node_modules/@vscode/vsce/node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", "dependencies": { @@ -4283,6 +5728,8 @@ }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", "peer": true, @@ -4293,24 +5740,32 @@ }, "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, "license": "MIT", "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, "license": "MIT", "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, "license": "MIT", "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", "peer": true, @@ -4322,12 +5777,16 @@ }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, "license": "MIT", "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", "peer": true, @@ -4340,6 +5799,8 @@ }, "node_modules/@webassemblyjs/ieee754": { "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", "peer": true, @@ -4349,6 +5810,8 @@ }, "node_modules/@webassemblyjs/leb128": { "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -4358,12 +5821,16 @@ }, "node_modules/@webassemblyjs/utf8": { "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, "license": "MIT", "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", "peer": true, @@ -4380,6 +5847,8 @@ }, "node_modules/@webassemblyjs/wasm-gen": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", "peer": true, @@ -4393,6 +5862,8 @@ }, "node_modules/@webassemblyjs/wasm-opt": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", "peer": true, @@ -4405,6 +5876,8 @@ }, "node_modules/@webassemblyjs/wasm-parser": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", "peer": true, @@ -4419,6 +5892,8 @@ }, "node_modules/@webassemblyjs/wast-printer": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", "peer": true, @@ -4429,23 +5904,32 @@ }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, "license": "BSD-3-Clause", "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, "license": "Apache-2.0", "peer": true }, "node_modules/abab": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true, "license": "BSD-3-Clause" }, "node_modules/abort-controller": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "dev": true, "license": "MIT", "dependencies": { @@ -4455,8 +5939,24 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", "bin": { @@ -4468,6 +5968,8 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4476,6 +5978,8 @@ }, "node_modules/agent-base": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "dev": true, "license": "MIT", "engines": { @@ -4484,6 +5988,8 @@ }, "node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -4499,6 +6005,8 @@ }, "node_modules/ajv-formats": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", "dependencies": { @@ -4515,6 +6023,8 @@ }, "node_modules/ajv-formats/node_modules/ajv": { "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { @@ -4530,11 +6040,15 @@ }, "node_modules/ajv-formats/node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/ansi-colors": { "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "license": "MIT", "engines": { @@ -4543,6 +6057,8 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -4550,6 +6066,8 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4563,10 +6081,14 @@ }, "node_modules/any-promise": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -4596,75 +6118,38 @@ "arrow2csv": "bin/arrow2csv.js" } }, - "node_modules/apache-arrow/node_modules/array-back": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", - "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/apache-arrow/node_modules/command-line-args": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", - "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", + "node_modules/apache-arrow/node_modules/@types/node": { + "version": "20.17.46", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz", + "integrity": "sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==", "license": "MIT", "dependencies": { - "array-back": "^6.2.2", - "find-replace": "^5.0.2", - "lodash.camelcase": "^4.3.0", - "typical": "^7.2.0" - }, - "engines": { - "node": ">=12.20" - }, - "peerDependencies": { - "@75lb/nature": "latest" - }, - "peerDependenciesMeta": { - "@75lb/nature": { - "optional": true - } - } - }, - "node_modules/apache-arrow/node_modules/find-replace": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", - "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@75lb/nature": "latest" - }, - "peerDependenciesMeta": { - "@75lb/nature": { - "optional": true - } + "undici-types": "~6.19.2" } }, - "node_modules/apache-arrow/node_modules/typical": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", - "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", - "license": "MIT", - "engines": { - "node": ">=12.17" - } + "node_modules/apache-arrow/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" }, "node_modules/arg": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -4684,14 +6169,18 @@ } }, "node_modules/array-back": { - "version": "3.1.0", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=12.17" } }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { @@ -4707,6 +6196,8 @@ }, "node_modules/array-union": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "license": "MIT", "engines": { @@ -4715,6 +6206,8 @@ }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4735,6 +6228,8 @@ }, "node_modules/assertion-error": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -4743,6 +6238,8 @@ }, "node_modules/astring": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", "dev": true, "license": "MIT", "bin": { @@ -4751,6 +6248,8 @@ }, "node_modules/async-function": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", "engines": { @@ -4759,11 +6258,15 @@ }, "node_modules/asynckit": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/autoprefixer": { "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", "dev": true, "funding": [ { @@ -4800,6 +6303,8 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4814,6 +6319,8 @@ }, "node_modules/azure-devops-node-api": { "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", "dev": true, "license": "MIT", "dependencies": { @@ -4823,6 +6330,8 @@ }, "node_modules/bail": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", "license": "MIT", "funding": { "type": "github", @@ -4831,10 +6340,14 @@ }, "node_modules/balanced-match": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { @@ -4855,6 +6368,8 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "license": "MIT", "engines": { "node": ">=8" @@ -4865,6 +6380,8 @@ }, "node_modules/bl": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", "optional": true, @@ -4876,6 +6393,8 @@ }, "node_modules/bl/node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", "optional": true, @@ -4888,13 +6407,38 @@ "node": ">= 6" } }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/boolbase": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, "license": "ISC" }, "node_modules/brace-expansion": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4902,6 +6446,8 @@ }, "node_modules/braces": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -4912,11 +6458,15 @@ }, "node_modules/browser-stdout": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true, "license": "ISC" }, "node_modules/browserslist": { - "version": "4.24.4", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -4934,10 +6484,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -4948,6 +6498,8 @@ }, "node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { @@ -4972,6 +6524,8 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "license": "MIT", "engines": { @@ -4980,17 +6534,23 @@ }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/buffer-from": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT", "peer": true }, "node_modules/bundle-name": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5003,8 +6563,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/c8": { "version": "9.1.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", + "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", "dev": true, "license": "ISC", "dependencies": { @@ -5029,6 +6601,8 @@ }, "node_modules/cac": { "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, "license": "MIT", "engines": { @@ -5037,6 +6611,8 @@ }, "node_modules/call-bind": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { @@ -5054,6 +6630,8 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5066,6 +6644,8 @@ }, "node_modules/call-bound": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { @@ -5081,11 +6661,15 @@ }, "node_modules/call-me-maybe": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "dev": true, "license": "MIT" }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -5094,6 +6678,8 @@ }, "node_modules/camelcase": { "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", "engines": { @@ -5105,13 +6691,17 @@ }, "node_modules/camelcase-css": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001715", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -5131,6 +6721,8 @@ }, "node_modules/ccount": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "license": "MIT", "funding": { "type": "github", @@ -5139,6 +6731,8 @@ }, "node_modules/chai": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", "dependencies": { @@ -5154,6 +6748,8 @@ }, "node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -5168,6 +6764,8 @@ }, "node_modules/chalk-template": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", "license": "MIT", "dependencies": { "chalk": "^4.1.2" @@ -5181,6 +6779,8 @@ }, "node_modules/chalk/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -5191,6 +6791,8 @@ }, "node_modules/character-entities": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", "license": "MIT", "funding": { "type": "github", @@ -5199,6 +6801,8 @@ }, "node_modules/character-entities-html4": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "license": "MIT", "funding": { "type": "github", @@ -5207,6 +6811,8 @@ }, "node_modules/character-entities-legacy": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", "license": "MIT", "funding": { "type": "github", @@ -5215,6 +6821,8 @@ }, "node_modules/character-reference-invalid": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", "license": "MIT", "funding": { "type": "github", @@ -5223,6 +6831,8 @@ }, "node_modules/check-error": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, "license": "MIT", "engines": { @@ -5231,6 +6841,8 @@ }, "node_modules/cheerio": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", "dev": true, "license": "MIT", "dependencies": { @@ -5255,6 +6867,8 @@ }, "node_modules/cheerio-select": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5271,6 +6885,8 @@ }, "node_modules/chokidar": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -5293,12 +6909,16 @@ }, "node_modules/chownr": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, "license": "ISC", "optional": true }, "node_modules/chrome-trace-event": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", "peer": true, @@ -5308,10 +6928,14 @@ }, "node_modules/classcat": { "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", "license": "MIT" }, "node_modules/cli-cursor": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", "dependencies": { @@ -5326,6 +6950,8 @@ }, "node_modules/cli-spinners": { "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "license": "MIT", "engines": { @@ -5337,10 +6963,14 @@ }, "node_modules/client-only": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { @@ -5354,11 +6984,15 @@ }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -5372,6 +7006,8 @@ }, "node_modules/cliui/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -5383,6 +7019,8 @@ }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5399,6 +7037,8 @@ }, "node_modules/clsx": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -5406,6 +7046,8 @@ }, "node_modules/cockatiel": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", "dev": true, "license": "MIT", "engines": { @@ -5414,6 +7056,8 @@ }, "node_modules/codemirror": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -5427,6 +7071,8 @@ }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5437,10 +7083,14 @@ }, "node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", "dependencies": { @@ -5452,6 +7102,8 @@ }, "node_modules/comma-separated-tokens": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "license": "MIT", "funding": { "type": "github", @@ -5459,47 +7111,47 @@ } }, "node_modules/command-line-args": { - "version": "5.2.1", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", + "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", "license": "MIT", "dependencies": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", + "array-back": "^6.2.2", + "find-replace": "^5.0.2", "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" + "typical": "^7.2.0" }, "engines": { - "node": ">=4.0.0" + "node": ">=12.20" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } } }, "node_modules/command-line-usage": { - "version": "7.0.1", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", "license": "MIT", "dependencies": { "array-back": "^6.2.2", "chalk-template": "^0.4.0", - "table-layout": "^3.0.0", + "table-layout": "^4.1.0", "typical": "^7.1.1" }, "engines": { "node": ">=12.20.0" } }, - "node_modules/command-line-usage/node_modules/array-back": { - "version": "6.2.2", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/command-line-usage/node_modules/typical": { - "version": "7.3.0", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, "node_modules/commander": { "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { @@ -5508,30 +7160,99 @@ }, "node_modules/compare-versions": { "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", "dev": true, "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/core-util-is": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true, "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/crelt": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5544,6 +7265,8 @@ }, "node_modules/css-select": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5559,6 +7282,8 @@ }, "node_modules/css-what": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5570,11 +7295,15 @@ }, "node_modules/css.escape": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true, "license": "MIT" }, "node_modules/cssesc": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -5585,6 +7314,8 @@ }, "node_modules/cssstyle": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", "dev": true, "license": "MIT", "dependencies": { @@ -5596,10 +7327,14 @@ }, "node_modules/csstype": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, "node_modules/d3-color": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "engines": { "node": ">=12" @@ -5607,6 +7342,8 @@ }, "node_modules/d3-dispatch": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", "license": "ISC", "engines": { "node": ">=12" @@ -5614,6 +7351,8 @@ }, "node_modules/d3-drag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -5625,6 +7364,8 @@ }, "node_modules/d3-ease": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "license": "BSD-3-Clause", "engines": { "node": ">=12" @@ -5632,6 +7373,8 @@ }, "node_modules/d3-interpolate": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -5642,6 +7385,8 @@ }, "node_modules/d3-selection": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", "engines": { "node": ">=12" @@ -5649,6 +7394,8 @@ }, "node_modules/d3-timer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", "engines": { "node": ">=12" @@ -5656,6 +7403,8 @@ }, "node_modules/d3-transition": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", "license": "ISC", "dependencies": { "d3-color": "1 - 3", @@ -5673,6 +7422,8 @@ }, "node_modules/d3-zoom": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -5687,6 +7438,8 @@ }, "node_modules/data-urls": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", "dev": true, "license": "MIT", "dependencies": { @@ -5700,6 +7453,8 @@ }, "node_modules/data-urls/node_modules/whatwg-mimetype": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "license": "MIT", "engines": { @@ -5708,6 +7463,8 @@ }, "node_modules/data-view-buffer": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5724,6 +7481,8 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5740,6 +7499,8 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5756,6 +7517,8 @@ }, "node_modules/debug": { "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5771,6 +7534,8 @@ }, "node_modules/decamelize": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true, "license": "MIT", "engines": { @@ -5782,11 +7547,15 @@ }, "node_modules/decimal.js": { "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", "dev": true, "license": "MIT" }, "node_modules/decode-named-character-reference": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", + "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -5798,6 +7567,8 @@ }, "node_modules/decompress-response": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", "optional": true, @@ -5813,6 +7584,8 @@ }, "node_modules/deep-eql": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", "engines": { @@ -5821,6 +7594,8 @@ }, "node_modules/deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "license": "MIT", "optional": true, @@ -5830,11 +7605,15 @@ }, "node_modules/deep-is": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/default-browser": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -5850,6 +7629,8 @@ }, "node_modules/default-browser-id": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", "dev": true, "license": "MIT", "engines": { @@ -5861,6 +7642,8 @@ }, "node_modules/define-data-property": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { @@ -5877,6 +7660,8 @@ }, "node_modules/define-lazy-prop": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", "engines": { @@ -5888,6 +7673,8 @@ }, "node_modules/define-properties": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -5904,14 +7691,28 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dependency-graph": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", "dev": true, "license": "MIT", "engines": { @@ -5920,6 +7721,8 @@ }, "node_modules/dequal": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", "engines": { "node": ">=6" @@ -5927,6 +7730,8 @@ }, "node_modules/detect-libc": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -5936,10 +7741,14 @@ }, "node_modules/detect-node-es": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, "node_modules/devlop": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "license": "MIT", "dependencies": { "dequal": "^2.0.0" @@ -5951,10 +7760,14 @@ }, "node_modules/didyoumean": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, "node_modules/diff": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -5962,6 +7775,8 @@ }, "node_modules/dir-glob": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "license": "MIT", "dependencies": { @@ -5973,10 +7788,14 @@ }, "node_modules/dlv": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, "node_modules/dnd-core": { "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", "license": "MIT", "dependencies": { "@react-dnd/asap": "^5.0.1", @@ -5986,12 +7805,16 @@ }, "node_modules/dom-accessibility-api": { "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, "license": "MIT", "peer": true }, "node_modules/dom-serializer": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", "dependencies": { @@ -6005,6 +7828,8 @@ }, "node_modules/domelementtype": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { @@ -6016,6 +7841,9 @@ }, "node_modules/domexception": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", "dev": true, "license": "MIT", "dependencies": { @@ -6027,6 +7855,8 @@ }, "node_modules/domhandler": { "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6041,6 +7871,8 @@ }, "node_modules/domutils": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6054,6 +7886,8 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { @@ -6067,31 +7901,60 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { - "version": "1.5.140", + "version": "1.5.152", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz", + "integrity": "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==", "dev": true, "license": "ISC" }, "node_modules/elkjs": { "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", "license": "EPL-2.0" }, "node_modules/emoji-regex": { "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", "dev": true, "license": "MIT", "dependencies": { @@ -6104,6 +7967,8 @@ }, "node_modules/end-of-stream": { "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, "license": "MIT", "optional": true, @@ -6113,6 +7978,8 @@ }, "node_modules/enhanced-resolve": { "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -6125,6 +7992,8 @@ }, "node_modules/enquirer": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6137,6 +8006,8 @@ }, "node_modules/enquirer/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -6148,6 +8019,8 @@ }, "node_modules/entities": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -6159,6 +8032,8 @@ }, "node_modules/es-abstract": { "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "dev": true, "license": "MIT", "dependencies": { @@ -6223,6 +8098,8 @@ }, "node_modules/es-aggregate-error": { "version": "1.0.13", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.13.tgz", + "integrity": "sha512-KkzhUUuD2CUMqEc8JEqsXEMDHzDPE8RCjZeUBitsnB1eNcAJWQPiciKsMXe3Yytj4Flw1XLl46Qcf9OxvZha7A==", "dev": true, "license": "MIT", "dependencies": { @@ -6244,6 +8121,8 @@ }, "node_modules/es-define-property": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { @@ -6252,6 +8131,8 @@ }, "node_modules/es-errors": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { @@ -6259,12 +8140,16 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { @@ -6276,6 +8161,8 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { @@ -6290,6 +8177,8 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { @@ -6306,11 +8195,15 @@ }, "node_modules/es6-promise": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", "dev": true, "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.3", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6321,43 +8214,54 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.3", - "@esbuild/android-arm": "0.25.3", - "@esbuild/android-arm64": "0.25.3", - "@esbuild/android-x64": "0.25.3", - "@esbuild/darwin-arm64": "0.25.3", - "@esbuild/darwin-x64": "0.25.3", - "@esbuild/freebsd-arm64": "0.25.3", - "@esbuild/freebsd-x64": "0.25.3", - "@esbuild/linux-arm": "0.25.3", - "@esbuild/linux-arm64": "0.25.3", - "@esbuild/linux-ia32": "0.25.3", - "@esbuild/linux-loong64": "0.25.3", - "@esbuild/linux-mips64el": "0.25.3", - "@esbuild/linux-ppc64": "0.25.3", - "@esbuild/linux-riscv64": "0.25.3", - "@esbuild/linux-s390x": "0.25.3", - "@esbuild/linux-x64": "0.25.3", - "@esbuild/netbsd-arm64": "0.25.3", - "@esbuild/netbsd-x64": "0.25.3", - "@esbuild/openbsd-arm64": "0.25.3", - "@esbuild/openbsd-x64": "0.25.3", - "@esbuild/sunos-x64": "0.25.3", - "@esbuild/win32-arm64": "0.25.3", - "@esbuild/win32-ia32": "0.25.3", - "@esbuild/win32-x64": "0.25.3" + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" } }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -6368,7 +8272,9 @@ } }, "node_modules/eslint": { - "version": "9.25.1", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", + "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6378,11 +8284,12 @@ "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.25.1", + "@eslint/js": "9.26.0", "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", + "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -6406,7 +8313,8 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "optionator": "^0.9.3", + "zod": "^3.24.2" }, "bin": { "eslint": "bin/eslint.js" @@ -6428,6 +8336,8 @@ }, "node_modules/eslint-scope": { "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6442,11 +8352,13 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6454,6 +8366,8 @@ }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -6461,19 +8375,10 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -6485,6 +8390,8 @@ }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -6496,6 +8403,8 @@ }, "node_modules/espree": { "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6510,19 +8419,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esquery": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6534,6 +8434,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6545,6 +8447,8 @@ }, "node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -6553,6 +8457,8 @@ }, "node_modules/estree-util-is-identifier-name": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", "license": "MIT", "funding": { "type": "opencollective", @@ -6561,6 +8467,8 @@ }, "node_modules/estree-walker": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -6569,14 +8477,28 @@ }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "dev": true, "license": "MIT", "engines": { @@ -6585,6 +8507,8 @@ }, "node_modules/events": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", "peer": true, @@ -6592,8 +8516,33 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "license": "MIT", "dependencies": { @@ -6616,11 +8565,15 @@ }, "node_modules/execa/node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/expand-template": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, "license": "(MIT OR WTFPL)", "optional": true, @@ -6630,22 +8583,89 @@ }, "node_modules/expect-type": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/extend": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -6660,26 +8680,36 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-memoize": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", + "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", "dev": true, "license": "MIT" }, "node_modules/fast-safe-stringify": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", "dev": true, "funding": [ { @@ -6695,6 +8725,8 @@ }, "node_modules/fastq": { "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -6702,6 +8734,8 @@ }, "node_modules/fd-slicer": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "license": "MIT", "dependencies": { @@ -6710,6 +8744,8 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6721,6 +8757,8 @@ }, "node_modules/fill-range": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -6729,18 +8767,45 @@ "node": ">=8" } }, - "node_modules/find-replace": { - "version": "3.0.0", + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, "license": "MIT", "dependencies": { - "array-back": "^3.0.1" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">=4.0.0" + "node": ">= 0.8" + } + }, + "node_modules/find-replace": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", + "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } } }, "node_modules/find-up": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -6756,6 +8821,8 @@ }, "node_modules/flat": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, "license": "BSD-3-Clause", "bin": { @@ -6764,6 +8831,8 @@ }, "node_modules/flat-cache": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -6782,11 +8851,15 @@ }, "node_modules/flatted": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, "node_modules/for-each": { "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { @@ -6801,6 +8874,8 @@ }, "node_modules/foreground-child": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -6815,6 +8890,8 @@ }, "node_modules/form-data": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "dev": true, "license": "MIT", "dependencies": { @@ -6827,8 +8904,43 @@ "node": ">= 6" } }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, "license": "MIT", "engines": { @@ -6839,14 +8951,28 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, "license": "MIT", "optional": true }, "node_modules/fs-extra": { "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -6859,11 +8985,16 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.2", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -6875,6 +9006,8 @@ }, "node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6882,6 +9015,8 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6901,6 +9036,8 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, "license": "MIT", "funding": { @@ -6909,6 +9046,8 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", "engines": { @@ -6917,6 +9056,8 @@ }, "node_modules/get-east-asian-width": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", "dev": true, "license": "MIT", "engines": { @@ -6928,6 +9069,8 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6951,6 +9094,8 @@ }, "node_modules/get-nonce": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", "license": "MIT", "engines": { "node": ">=6" @@ -6958,6 +9103,8 @@ }, "node_modules/get-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { @@ -6970,6 +9117,8 @@ }, "node_modules/get-stream": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, "license": "MIT", "engines": { @@ -6981,6 +9130,8 @@ }, "node_modules/get-symbol-description": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { @@ -6997,12 +9148,16 @@ }, "node_modules/github-from-package": { "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, "license": "MIT", "optional": true }, "node_modules/glob": { "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -7021,6 +9176,8 @@ }, "node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -7031,12 +9188,16 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, "license": "BSD-2-Clause", "peer": true }, "node_modules/globals": { "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -7048,6 +9209,8 @@ }, "node_modules/globalthis": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7063,6 +9226,8 @@ }, "node_modules/globby": { "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "license": "MIT", "dependencies": { @@ -7082,6 +9247,8 @@ }, "node_modules/gopd": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { @@ -7093,15 +9260,21 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, "node_modules/has-bigints": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", "engines": { @@ -7113,6 +9286,8 @@ }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { "node": ">=8" @@ -7120,6 +9295,8 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { @@ -7131,6 +9308,8 @@ }, "node_modules/has-proto": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7145,6 +9324,8 @@ }, "node_modules/has-symbols": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -7156,6 +9337,8 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { @@ -7170,6 +9353,8 @@ }, "node_modules/hasown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7180,6 +9365,8 @@ }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -7205,6 +9392,8 @@ }, "node_modules/hast-util-whitespace": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -7216,6 +9405,8 @@ }, "node_modules/he": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, "license": "MIT", "bin": { @@ -7224,6 +9415,8 @@ }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" @@ -7231,10 +9424,14 @@ }, "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/hosted-git-info": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "license": "ISC", "dependencies": { @@ -7246,6 +9443,8 @@ }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", "dev": true, "license": "MIT", "dependencies": { @@ -7257,6 +9456,8 @@ }, "node_modules/html-encoding-sniffer/node_modules/whatwg-encoding": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", "dev": true, "license": "MIT", "dependencies": { @@ -7268,11 +9469,15 @@ }, "node_modules/html-escaper": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, "node_modules/html-url-attributes": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -7281,6 +9486,8 @@ }, "node_modules/htmlparser2": { "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -7297,8 +9504,27 @@ "entities": "^4.5.0" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -7311,11 +9537,15 @@ }, "node_modules/http2-client": { "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", "dev": true, "license": "MIT" }, "node_modules/https-proxy-agent": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { @@ -7328,6 +9558,8 @@ }, "node_modules/human-signals": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -7336,6 +9568,8 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -7347,6 +9581,8 @@ }, "node_modules/ieee754": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, "funding": [ { @@ -7367,6 +9603,8 @@ }, "node_modules/ignore": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -7375,11 +9613,15 @@ }, "node_modules/immediate": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "dev": true, "license": "MIT" }, "node_modules/immer": { "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", "devOptional": true, "license": "MIT", "funding": { @@ -7389,6 +9631,8 @@ }, "node_modules/import-fresh": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7404,6 +9648,8 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -7412,6 +9658,8 @@ }, "node_modules/indent-string": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { @@ -7420,6 +9668,9 @@ }, "node_modules/inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", "dependencies": { @@ -7429,21 +9680,29 @@ }, "node_modules/inherits": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true, "license": "ISC", "optional": true }, "node_modules/inline-style-parser": { "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "license": "MIT" }, "node_modules/internal-slot": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { @@ -7455,8 +9714,20 @@ "node": ">= 0.4" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", "license": "MIT", "funding": { "type": "github", @@ -7465,6 +9736,8 @@ }, "node_modules/is-alphanumerical": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -7477,6 +9750,8 @@ }, "node_modules/is-array-buffer": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { @@ -7493,6 +9768,8 @@ }, "node_modules/is-async-function": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7511,6 +9788,8 @@ }, "node_modules/is-bigint": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7525,6 +9804,8 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -7535,6 +9816,8 @@ }, "node_modules/is-boolean-object": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { @@ -7550,6 +9833,8 @@ }, "node_modules/is-callable": { "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", "engines": { @@ -7561,6 +9846,8 @@ }, "node_modules/is-core-module": { "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -7574,6 +9861,8 @@ }, "node_modules/is-data-view": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", "dependencies": { @@ -7590,6 +9879,8 @@ }, "node_modules/is-date-object": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { @@ -7605,6 +9896,8 @@ }, "node_modules/is-decimal": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", "license": "MIT", "funding": { "type": "github", @@ -7613,6 +9906,8 @@ }, "node_modules/is-docker": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", "bin": { @@ -7627,6 +9922,8 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7634,6 +9931,8 @@ }, "node_modules/is-finalizationregistry": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", "dependencies": { @@ -7648,6 +9947,8 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -7655,6 +9956,8 @@ }, "node_modules/is-generator-function": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7672,6 +9975,8 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -7682,6 +9987,8 @@ }, "node_modules/is-hexadecimal": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", "license": "MIT", "funding": { "type": "github", @@ -7690,6 +9997,8 @@ }, "node_modules/is-inside-container": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", "dependencies": { @@ -7707,6 +10016,8 @@ }, "node_modules/is-interactive": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "dev": true, "license": "MIT", "engines": { @@ -7718,6 +10029,8 @@ }, "node_modules/is-map": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { @@ -7729,6 +10042,8 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -7736,6 +10051,8 @@ }, "node_modules/is-number-object": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { @@ -7751,6 +10068,8 @@ }, "node_modules/is-plain-obj": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", "engines": { "node": ">=12" @@ -7761,11 +10080,22 @@ }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true, "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { @@ -7783,6 +10113,8 @@ }, "node_modules/is-set": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, "license": "MIT", "engines": { @@ -7794,6 +10126,8 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { @@ -7808,6 +10142,8 @@ }, "node_modules/is-stream": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", "engines": { @@ -7819,6 +10155,8 @@ }, "node_modules/is-string": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { @@ -7834,6 +10172,8 @@ }, "node_modules/is-symbol": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { @@ -7850,6 +10190,8 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7864,6 +10206,8 @@ }, "node_modules/is-unicode-supported": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, "license": "MIT", "engines": { @@ -7875,6 +10219,8 @@ }, "node_modules/is-weakmap": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", "engines": { @@ -7886,6 +10232,8 @@ }, "node_modules/is-weakref": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { @@ -7900,6 +10248,8 @@ }, "node_modules/is-weakset": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7915,6 +10265,8 @@ }, "node_modules/is-wsl": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, "license": "MIT", "dependencies": { @@ -7929,15 +10281,21 @@ }, "node_modules/isarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -7946,6 +10304,8 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7959,6 +10319,8 @@ }, "node_modules/istanbul-lib-report/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -7970,6 +10332,8 @@ }, "node_modules/istanbul-reports": { "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7982,6 +10346,8 @@ }, "node_modules/jackspeak": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -7995,6 +10361,8 @@ }, "node_modules/jest-worker": { "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", "peer": true, @@ -8009,6 +10377,8 @@ }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "peer": true, @@ -8024,6 +10394,8 @@ }, "node_modules/jiti": { "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -8031,10 +10403,14 @@ }, "node_modules/js-tokens": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { @@ -8046,6 +10422,8 @@ }, "node_modules/jsdom": { "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", "dev": true, "license": "MIT", "dependencies": { @@ -8087,6 +10465,8 @@ }, "node_modules/jsdom/node_modules/agent-base": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8098,6 +10478,8 @@ }, "node_modules/jsdom/node_modules/http-proxy-agent": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "dev": true, "license": "MIT", "dependencies": { @@ -8111,6 +10493,8 @@ }, "node_modules/jsdom/node_modules/https-proxy-agent": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "license": "MIT", "dependencies": { @@ -8123,6 +10507,8 @@ }, "node_modules/jsdom/node_modules/whatwg-encoding": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", "dev": true, "license": "MIT", "dependencies": { @@ -8134,6 +10520,8 @@ }, "node_modules/jsdom/node_modules/whatwg-mimetype": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "license": "MIT", "engines": { @@ -8142,6 +10530,8 @@ }, "node_modules/jsep": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", "engines": { @@ -8150,38 +10540,52 @@ }, "node_modules/json-bignum": { "version": "0.0.3", + "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz", + "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==", "engines": { "node": ">=0.8" } }, "node_modules/json-buffer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, "license": "MIT", "peer": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/jsonc-parser": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true, "license": "MIT" }, "node_modules/jsonfile": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -8192,6 +10596,8 @@ }, "node_modules/jsonpath-plus": { "version": "10.3.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", + "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", "dev": true, "license": "MIT", "dependencies": { @@ -8209,6 +10615,8 @@ }, "node_modules/jsonpointer": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "dev": true, "license": "MIT", "engines": { @@ -8217,6 +10625,8 @@ }, "node_modules/jsonschema": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", + "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", "dev": true, "license": "MIT", "engines": { @@ -8225,6 +10635,8 @@ }, "node_modules/jsonwebtoken": { "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8246,6 +10658,8 @@ }, "node_modules/jszip": { "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dev": true, "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { @@ -8256,17 +10670,21 @@ } }, "node_modules/jwa": { - "version": "1.4.1", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "dev": true, "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jws": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "dev": true, "license": "MIT", "dependencies": { @@ -8276,6 +10694,8 @@ }, "node_modules/keytar": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8287,6 +10707,8 @@ }, "node_modules/keyv": { "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -8295,6 +10717,8 @@ }, "node_modules/leven": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, "license": "MIT", "engines": { @@ -8303,6 +10727,8 @@ }, "node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8315,6 +10741,8 @@ }, "node_modules/lie": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8323,6 +10751,8 @@ }, "node_modules/lilconfig": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", "engines": { "node": ">=14" @@ -8333,10 +10763,14 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, "node_modules/linkify-it": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8345,6 +10779,8 @@ }, "node_modules/loader-runner": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, "license": "MIT", "peer": true, @@ -8354,6 +10790,8 @@ }, "node_modules/locate-path": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -8368,84 +10806,119 @@ }, "node_modules/lodash": { "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "license": "MIT" }, "node_modules/lodash.includes": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "dev": true, "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "dev": true, "license": "MIT" }, "node_modules/lodash.isempty": { "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", "dev": true, "license": "MIT" }, "node_modules/lodash.isinteger": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "dev": true, "license": "MIT" }, "node_modules/lodash.isnumber": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", "dev": true, "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.omitby": { "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.omitby/-/lodash.omitby-4.6.0.tgz", + "integrity": "sha512-5OrRcIVR75M288p4nbI2WLAf3ndw2GD9fyNv3Bc15+WCxJDdZ4lYndSxGd7hnG6PVjiJTeJE2dHEGhIuKGicIQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.once": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true, "license": "MIT" }, "node_modules/lodash.topath": { "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", "dev": true, "license": "MIT" }, "node_modules/lodash.uniq": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.uniqby": { "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", "dev": true, "license": "MIT" }, "node_modules/lodash.uniqwith": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz", + "integrity": "sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==", "dev": true, "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "license": "MIT", "dependencies": { @@ -8461,6 +10934,8 @@ }, "node_modules/loglevel": { "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", "dev": true, "license": "MIT", "engines": { @@ -8473,11 +10948,15 @@ }, "node_modules/loglevel-plugin-prefix": { "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", "dev": true, "license": "MIT" }, "node_modules/longest-streak": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", "license": "MIT", "funding": { "type": "github", @@ -8486,6 +10965,8 @@ }, "node_modules/loose-envify": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -8496,11 +10977,15 @@ }, "node_modules/loupe": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", "dev": true, "license": "MIT" }, "node_modules/lru-cache": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", "dependencies": { @@ -8512,11 +10997,15 @@ }, "node_modules/lunr": { "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true, "license": "MIT" }, "node_modules/lz-string": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", "peer": true, @@ -8526,6 +11015,8 @@ }, "node_modules/magic-string": { "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "license": "MIT", "dependencies": { @@ -8534,6 +11025,8 @@ }, "node_modules/make-dir": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { @@ -8548,6 +11041,8 @@ }, "node_modules/markdown-it": { "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, "license": "MIT", "dependencies": { @@ -8564,6 +11059,8 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { @@ -8572,6 +11069,8 @@ }, "node_modules/mdast-util-from-markdown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -8594,6 +11093,8 @@ }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -8610,6 +11111,8 @@ }, "node_modules/mdast-util-mdx-jsx": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -8632,6 +11135,8 @@ }, "node_modules/mdast-util-mdxjs-esm": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -8648,6 +11153,8 @@ }, "node_modules/mdast-util-phrasing": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -8660,6 +11167,8 @@ }, "node_modules/mdast-util-to-hast": { "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -8679,6 +11188,8 @@ }, "node_modules/mdast-util-to-markdown": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -8698,6 +11209,8 @@ }, "node_modules/mdast-util-to-string": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0" @@ -8709,16 +11222,45 @@ }, "node_modules/mdurl": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true, "license": "MIT" }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "license": "MIT", "engines": { "node": ">= 8" @@ -8726,6 +11268,8 @@ }, "node_modules/micromark": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "funding": [ { "type": "GitHub Sponsors", @@ -8759,6 +11303,8 @@ }, "node_modules/micromark-core-commonmark": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "funding": [ { "type": "GitHub Sponsors", @@ -8791,6 +11337,8 @@ }, "node_modules/micromark-factory-destination": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", "funding": [ { "type": "GitHub Sponsors", @@ -8810,6 +11358,8 @@ }, "node_modules/micromark-factory-label": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", "funding": [ { "type": "GitHub Sponsors", @@ -8830,6 +11380,8 @@ }, "node_modules/micromark-factory-space": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -8848,6 +11400,8 @@ }, "node_modules/micromark-factory-title": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", "funding": [ { "type": "GitHub Sponsors", @@ -8868,6 +11422,8 @@ }, "node_modules/micromark-factory-whitespace": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", "funding": [ { "type": "GitHub Sponsors", @@ -8888,6 +11444,8 @@ }, "node_modules/micromark-util-character": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -8906,6 +11464,8 @@ }, "node_modules/micromark-util-chunked": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", "funding": [ { "type": "GitHub Sponsors", @@ -8923,6 +11483,8 @@ }, "node_modules/micromark-util-classify-character": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -8942,6 +11504,8 @@ }, "node_modules/micromark-util-combine-extensions": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", "funding": [ { "type": "GitHub Sponsors", @@ -8960,6 +11524,8 @@ }, "node_modules/micromark-util-decode-numeric-character-reference": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", "funding": [ { "type": "GitHub Sponsors", @@ -8977,6 +11543,8 @@ }, "node_modules/micromark-util-decode-string": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", "funding": [ { "type": "GitHub Sponsors", @@ -8997,6 +11565,8 @@ }, "node_modules/micromark-util-encode": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "funding": [ { "type": "GitHub Sponsors", @@ -9011,6 +11581,8 @@ }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", "funding": [ { "type": "GitHub Sponsors", @@ -9025,6 +11597,8 @@ }, "node_modules/micromark-util-normalize-identifier": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", "funding": [ { "type": "GitHub Sponsors", @@ -9042,6 +11616,8 @@ }, "node_modules/micromark-util-resolve-all": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", "funding": [ { "type": "GitHub Sponsors", @@ -9059,6 +11635,8 @@ }, "node_modules/micromark-util-sanitize-uri": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "funding": [ { "type": "GitHub Sponsors", @@ -9078,6 +11656,8 @@ }, "node_modules/micromark-util-subtokenize": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "funding": [ { "type": "GitHub Sponsors", @@ -9098,6 +11678,8 @@ }, "node_modules/micromark-util-symbol": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -9112,6 +11694,8 @@ }, "node_modules/micromark-util-types": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "funding": [ { "type": "GitHub Sponsors", @@ -9126,6 +11710,8 @@ }, "node_modules/micromatch": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -9137,6 +11723,8 @@ }, "node_modules/mime": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "license": "MIT", "bin": { @@ -9147,7 +11735,9 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { @@ -9155,11 +11745,13 @@ } }, "node_modules/mime-types": { - "version": "2.1.35", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -9167,6 +11759,8 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "license": "MIT", "engines": { @@ -9175,6 +11769,8 @@ }, "node_modules/mimic-function": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, "license": "MIT", "engines": { @@ -9186,6 +11782,8 @@ }, "node_modules/mimic-response": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", "optional": true, @@ -9198,6 +11796,8 @@ }, "node_modules/min-indent": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, "license": "MIT", "engines": { @@ -9206,6 +11806,8 @@ }, "node_modules/minimatch": { "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -9219,6 +11821,8 @@ }, "node_modules/minimist": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", "optional": true, @@ -9228,6 +11832,8 @@ }, "node_modules/minipass": { "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -9235,12 +11841,16 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true, "license": "MIT", "optional": true }, "node_modules/mocha": { "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, "license": "MIT", "dependencies": { @@ -9275,6 +11885,8 @@ }, "node_modules/mocha/node_modules/cliui": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "license": "ISC", "dependencies": { @@ -9285,11 +11897,16 @@ }, "node_modules/mocha/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/mocha/node_modules/glob": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -9308,6 +11925,8 @@ }, "node_modules/mocha/node_modules/minimatch": { "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "license": "ISC", "dependencies": { @@ -9319,6 +11938,8 @@ }, "node_modules/mocha/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -9332,6 +11953,8 @@ }, "node_modules/mocha/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -9343,6 +11966,8 @@ }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9357,6 +11982,8 @@ }, "node_modules/mocha/node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9373,6 +12000,8 @@ }, "node_modules/mocha/node_modules/yargs": { "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "license": "MIT", "dependencies": { @@ -9390,6 +12019,8 @@ }, "node_modules/mocha/node_modules/yargs-parser": { "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "license": "ISC", "engines": { @@ -9398,15 +12029,21 @@ }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true, "license": "ISC" }, "node_modules/mz": { "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -9416,6 +12053,8 @@ }, "node_modules/nanoid": { "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -9432,23 +12071,41 @@ }, "node_modules/napi-build-utils": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "dev": true, "license": "MIT", "optional": true }, "node_modules/natural-compare": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/nimma": { "version": "0.2.3", + "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", + "integrity": "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -9466,7 +12123,9 @@ } }, "node_modules/node-abi": { - "version": "3.74.0", + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", "dev": true, "license": "MIT", "optional": true, @@ -9479,12 +12138,16 @@ }, "node_modules/node-addon-api": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, "license": "MIT", "optional": true }, "node_modules/node-fetch": { "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "license": "MIT", "dependencies": { @@ -9504,6 +12167,8 @@ }, "node_modules/node-fetch-h2": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", "dev": true, "license": "MIT", "dependencies": { @@ -9515,16 +12180,22 @@ }, "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, "license": "MIT" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "license": "MIT", "dependencies": { @@ -9534,6 +12205,8 @@ }, "node_modules/node-readfiles": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", "dev": true, "license": "MIT", "dependencies": { @@ -9542,11 +12215,15 @@ }, "node_modules/node-releases": { "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9554,6 +12231,8 @@ }, "node_modules/normalize-range": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, "license": "MIT", "engines": { @@ -9562,6 +12241,8 @@ }, "node_modules/npm-run-path": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "license": "MIT", "dependencies": { @@ -9573,6 +12254,8 @@ }, "node_modules/nth-check": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9584,11 +12267,15 @@ }, "node_modules/nwsapi": { "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", "dev": true, "license": "MIT" }, "node_modules/oas-kit-common": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9597,6 +12284,8 @@ }, "node_modules/oas-linter": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", + "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9610,6 +12299,8 @@ }, "node_modules/oas-linter/node_modules/yaml": { "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, "license": "ISC", "engines": { @@ -9618,6 +12309,8 @@ }, "node_modules/oas-resolver": { "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", + "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9636,6 +12329,8 @@ }, "node_modules/oas-resolver/node_modules/yaml": { "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, "license": "ISC", "engines": { @@ -9644,6 +12339,8 @@ }, "node_modules/oas-schema-walker": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", + "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", "dev": true, "license": "BSD-3-Clause", "funding": { @@ -9652,6 +12349,8 @@ }, "node_modules/oas-validator": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", + "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9670,6 +12369,8 @@ }, "node_modules/oas-validator/node_modules/yaml": { "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, "license": "ISC", "engines": { @@ -9678,6 +12379,8 @@ }, "node_modules/object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9685,6 +12388,8 @@ }, "node_modules/object-hash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "license": "MIT", "engines": { "node": ">= 6" @@ -9692,6 +12397,8 @@ }, "node_modules/object-inspect": { "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", "engines": { @@ -9703,6 +12410,8 @@ }, "node_modules/object-keys": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", "engines": { @@ -9711,6 +12420,8 @@ }, "node_modules/object.assign": { "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { @@ -9728,8 +12439,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { @@ -9738,6 +12464,8 @@ }, "node_modules/onetime": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "license": "MIT", "dependencies": { @@ -9751,7 +12479,9 @@ } }, "node_modules/open": { - "version": "10.1.1", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", "dev": true, "license": "MIT", "dependencies": { @@ -9769,12 +12499,16 @@ }, "node_modules/openapi-types": { "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/openapi3-ts": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.2.2.tgz", + "integrity": "sha512-+9g4actZKeb3czfi9gVQ4Br2Ju3KwhCAQJBNaKgye5KggqcBLIhFHH+nIkcm0BUX00TrAJl6dH4JWgM4G4JWrw==", "dev": true, "license": "MIT", "dependencies": { @@ -9783,6 +12517,8 @@ }, "node_modules/optionator": { "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -9799,6 +12535,8 @@ }, "node_modules/ora": { "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", "dev": true, "license": "MIT", "dependencies": { @@ -9821,6 +12559,8 @@ }, "node_modules/ora/node_modules/chalk": { "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, "license": "MIT", "engines": { @@ -9832,11 +12572,15 @@ }, "node_modules/ora/node_modules/emoji-regex": { "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, "license": "MIT" }, "node_modules/ora/node_modules/is-unicode-supported": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, "license": "MIT", "engines": { @@ -9848,6 +12592,8 @@ }, "node_modules/ora/node_modules/log-symbols": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", "dev": true, "license": "MIT", "dependencies": { @@ -9863,6 +12609,8 @@ }, "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, "license": "MIT", "engines": { @@ -9874,6 +12622,8 @@ }, "node_modules/ora/node_modules/string-width": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9890,6 +12640,8 @@ }, "node_modules/orval": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/orval/-/orval-7.9.0.tgz", + "integrity": "sha512-kFftcVojM4wRddRktqJPI/P9uYRpgiwCFOxF82G7XqDrczX9XDu8b5ialof+Z1LIuVZL4CvLV0Y184mRgrJrUA==", "dev": true, "license": "MIT", "dependencies": { @@ -9926,6 +12678,8 @@ }, "node_modules/orval/node_modules/ajv": { "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { @@ -9941,6 +12695,8 @@ }, "node_modules/orval/node_modules/chokidar": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { @@ -9955,11 +12711,15 @@ }, "node_modules/orval/node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/orval/node_modules/readdirp": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "engines": { @@ -9972,6 +12732,8 @@ }, "node_modules/own-keys": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, "license": "MIT", "dependencies": { @@ -9988,6 +12750,8 @@ }, "node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10002,6 +12766,8 @@ }, "node_modules/p-locate": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -10016,15 +12782,21 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, "node_modules/pako": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "dev": true, "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -10036,6 +12808,8 @@ }, "node_modules/parse-entities": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", @@ -10053,10 +12827,14 @@ }, "node_modules/parse-entities/node_modules/@types/unist": { "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, "node_modules/parse-semver": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10065,6 +12843,8 @@ }, "node_modules/parse-semver/node_modules/semver": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -10072,11 +12852,13 @@ } }, "node_modules/parse5": { - "version": "7.2.1", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^4.5.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -10084,6 +12866,8 @@ }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "dev": true, "license": "MIT", "dependencies": { @@ -10096,6 +12880,8 @@ }, "node_modules/parse5-parser-stream": { "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "dev": true, "license": "MIT", "dependencies": { @@ -10105,8 +12891,33 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -10115,6 +12926,8 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", "engines": { @@ -10123,6 +12936,8 @@ }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" @@ -10130,10 +12945,14 @@ }, "node_modules/path-parse": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -10148,10 +12967,24 @@ }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "license": "MIT", "engines": { @@ -10160,11 +12993,15 @@ }, "node_modules/pathe": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/pathval": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "license": "MIT", "engines": { @@ -10173,15 +13010,21 @@ }, "node_modules/pend": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -10192,6 +13035,8 @@ }, "node_modules/pify": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10199,13 +13044,27 @@ }, "node_modules/pirates": { "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "license": "MIT", "engines": { "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/playwright": { "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10223,6 +13082,8 @@ }, "node_modules/playwright-core": { "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -10232,8 +13093,25 @@ "node": ">=18" } }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pluralize": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "license": "MIT", "engines": { "node": ">=4" @@ -10241,6 +13119,8 @@ }, "node_modules/pony-cause": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", + "integrity": "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==", "dev": true, "license": "0BSD", "engines": { @@ -10249,6 +13129,8 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { @@ -10257,6 +13139,8 @@ }, "node_modules/postcss": { "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -10283,6 +13167,8 @@ }, "node_modules/postcss-import": { "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -10298,6 +13184,8 @@ }, "node_modules/postcss-js": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -10315,6 +13203,8 @@ }, "node_modules/postcss-load-config": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "funding": [ { "type": "opencollective", @@ -10348,6 +13238,8 @@ }, "node_modules/postcss-nested": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "funding": [ { "type": "opencollective", @@ -10371,6 +13263,8 @@ }, "node_modules/postcss-selector-parser": { "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -10382,10 +13276,14 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, "node_modules/prebuild-install": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", "dev": true, "license": "MIT", "optional": true, @@ -10412,6 +13310,8 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -10420,6 +13320,8 @@ }, "node_modules/prettier": { "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", "bin": { @@ -10434,6 +13336,8 @@ }, "node_modules/pretty-format": { "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", "peer": true, @@ -10448,6 +13352,8 @@ }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "peer": true, @@ -10460,11 +13366,15 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, "license": "MIT" }, "node_modules/prop-types": { "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -10474,18 +13384,38 @@ }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/property-information": { - "version": "7.0.0", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/psl": { "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", "dev": true, "license": "MIT", "dependencies": { @@ -10497,6 +13427,8 @@ }, "node_modules/pump": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, "license": "MIT", "optional": true, @@ -10507,6 +13439,8 @@ }, "node_modules/punycode": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -10515,6 +13449,8 @@ }, "node_modules/punycode.js": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "dev": true, "license": "MIT", "engines": { @@ -10523,6 +13459,8 @@ }, "node_modules/qs": { "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -10537,11 +13475,15 @@ }, "node_modules/querystringify": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true, "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "funding": [ { "type": "github", @@ -10558,16 +13500,46 @@ ], "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "^5.1.0" + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, "node_modules/rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "optional": true, @@ -10583,6 +13555,8 @@ }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "license": "MIT", "optional": true, @@ -10592,6 +13566,8 @@ }, "node_modules/react": { "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -10602,6 +13578,8 @@ }, "node_modules/react-dnd": { "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", "license": "MIT", "dependencies": { "@react-dnd/invariant": "^4.0.1", @@ -10630,6 +13608,8 @@ }, "node_modules/react-dnd-html5-backend": { "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", "license": "MIT", "dependencies": { "dnd-core": "^16.0.1" @@ -10637,6 +13617,8 @@ }, "node_modules/react-dom": { "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -10648,12 +13630,16 @@ }, "node_modules/react-is": { "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, "license": "MIT", "peer": true }, "node_modules/react-markdown": { "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -10704,6 +13690,8 @@ }, "node_modules/react-remove-scroll-bar": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.2", @@ -10724,6 +13712,8 @@ }, "node_modules/react-router": { "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", + "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", "license": "MIT", "dependencies": { "@remix-run/router": "1.23.0" @@ -10737,6 +13727,8 @@ }, "node_modules/react-router-dom": { "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", + "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", "license": "MIT", "dependencies": { "@remix-run/router": "1.23.0", @@ -10752,6 +13744,8 @@ }, "node_modules/react-split": { "version": "2.0.14", + "resolved": "https://registry.npmjs.org/react-split/-/react-split-2.0.14.tgz", + "integrity": "sha512-bKWydgMgaKTg/2JGQnaJPg51T6dmumTWZppFgEbbY0Fbme0F5TuatAScCLaqommbGQQf/ZT1zaejuPDriscISA==", "license": "MIT", "dependencies": { "prop-types": "^15.5.7", @@ -10763,6 +13757,8 @@ }, "node_modules/react-style-singleton": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", @@ -10783,6 +13779,8 @@ }, "node_modules/reactflow": { "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", "license": "MIT", "dependencies": { "@reactflow/background": "11.3.14", @@ -10799,6 +13797,8 @@ }, "node_modules/read": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "dev": true, "license": "ISC", "dependencies": { @@ -10810,6 +13810,8 @@ }, "node_modules/read-cache": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -10817,6 +13819,8 @@ }, "node_modules/readable-stream": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", "dependencies": { @@ -10829,8 +13833,17 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -10841,6 +13854,8 @@ }, "node_modules/redent": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, "license": "MIT", "dependencies": { @@ -10853,6 +13868,8 @@ }, "node_modules/redux": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.9.2" @@ -10860,6 +13877,8 @@ }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { @@ -10881,18 +13900,18 @@ }, "node_modules/reftools": { "version": "1.1.9", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", + "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", "dev": true, "license": "BSD-3-Clause", "funding": { "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "license": "MIT" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { @@ -10912,6 +13931,8 @@ }, "node_modules/remark-parse": { "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -10926,6 +13947,8 @@ }, "node_modules/remark-rehype": { "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -10941,6 +13964,8 @@ }, "node_modules/require-directory": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { @@ -10949,6 +13974,8 @@ }, "node_modules/require-from-string": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { @@ -10957,11 +13984,15 @@ }, "node_modules/requires-port": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true, "license": "MIT" }, "node_modules/resolve": { "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -10980,6 +14011,8 @@ }, "node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -10988,6 +14021,8 @@ }, "node_modules/restore-cursor": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { @@ -11003,6 +14038,8 @@ }, "node_modules/restore-cursor/node_modules/onetime": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11017,6 +14054,8 @@ }, "node_modules/reusify": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -11024,7 +14063,9 @@ } }, "node_modules/rollup": { - "version": "4.40.1", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", "dev": true, "license": "MIT", "dependencies": { @@ -11038,36 +14079,57 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.1", - "@rollup/rollup-android-arm64": "4.40.1", - "@rollup/rollup-darwin-arm64": "4.40.1", - "@rollup/rollup-darwin-x64": "4.40.1", - "@rollup/rollup-freebsd-arm64": "4.40.1", - "@rollup/rollup-freebsd-x64": "4.40.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", - "@rollup/rollup-linux-arm64-gnu": "4.40.1", - "@rollup/rollup-linux-arm64-musl": "4.40.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-musl": "4.40.1", - "@rollup/rollup-linux-s390x-gnu": "4.40.1", - "@rollup/rollup-linux-x64-gnu": "4.40.1", - "@rollup/rollup-linux-x64-musl": "4.40.1", - "@rollup/rollup-win32-arm64-msvc": "4.40.1", - "@rollup/rollup-win32-ia32-msvc": "4.40.1", - "@rollup/rollup-win32-x64-msvc": "4.40.1", + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/rrweb-cssom": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", "dev": true, "license": "MIT" }, "node_modules/run-applescript": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", "dev": true, "license": "MIT", "engines": { @@ -11079,6 +14141,8 @@ }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "funding": [ { "type": "github", @@ -11100,6 +14164,8 @@ }, "node_modules/safe-array-concat": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -11118,16 +14184,36 @@ }, "node_modules/safe-array-concat/node_modules/isarray": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/safe-buffer": { - "version": "5.1.2", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, "node_modules/safe-push-apply": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, "license": "MIT", "dependencies": { @@ -11143,11 +14229,15 @@ }, "node_modules/safe-push-apply/node_modules/isarray": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/safe-regex-test": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "license": "MIT", "dependencies": { @@ -11164,21 +14254,29 @@ }, "node_modules/safe-stable-stringify": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", + "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==", "dev": true, "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "license": "MIT" }, "node_modules/sax": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true, "license": "ISC" }, "node_modules/saxes": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", "dependencies": { @@ -11190,13 +14288,17 @@ }, "node_modules/scheduler": { "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } }, "node_modules/schema-utils": { - "version": "4.3.1", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", "peer": true, @@ -11216,6 +14318,8 @@ }, "node_modules/schema-utils/node_modules/ajv": { "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "peer": true, @@ -11232,6 +14336,8 @@ }, "node_modules/schema-utils/node_modules/ajv-keywords": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", "peer": true, @@ -11244,6 +14350,8 @@ }, "node_modules/schema-utils/node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT", "peer": true @@ -11260,16 +14368,59 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/set-function-length": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { @@ -11286,6 +14437,8 @@ }, "node_modules/set-function-name": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11300,6 +14453,8 @@ }, "node_modules/set-proto": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, "license": "MIT", "dependencies": { @@ -11313,11 +14468,22 @@ }, "node_modules/setimmediate": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true, "license": "MIT" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -11328,6 +14494,8 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" @@ -11335,6 +14503,8 @@ }, "node_modules/should": { "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11347,6 +14517,8 @@ }, "node_modules/should-equal": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", "dev": true, "license": "MIT", "dependencies": { @@ -11355,6 +14527,8 @@ }, "node_modules/should-format": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -11364,11 +14538,15 @@ }, "node_modules/should-type": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", "dev": true, "license": "MIT" }, "node_modules/should-type-adaptors": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", "dev": true, "license": "MIT", "dependencies": { @@ -11378,11 +14556,15 @@ }, "node_modules/should-util": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", "dev": true, "license": "MIT" }, "node_modules/side-channel": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { @@ -11401,6 +14583,8 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, "license": "MIT", "dependencies": { @@ -11416,6 +14600,8 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { @@ -11433,6 +14619,8 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { @@ -11451,11 +14639,15 @@ }, "node_modules/siginfo": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/signal-exit": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "engines": { "node": ">=14" @@ -11466,6 +14658,8 @@ }, "node_modules/simple-concat": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "dev": true, "funding": [ { @@ -11486,6 +14680,8 @@ }, "node_modules/simple-eval": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", + "integrity": "sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11497,6 +14693,8 @@ }, "node_modules/simple-get": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, "funding": [ { @@ -11522,6 +14720,8 @@ }, "node_modules/slash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { @@ -11530,6 +14730,8 @@ }, "node_modules/source-map": { "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -11538,6 +14740,8 @@ }, "node_modules/source-map-js": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -11545,6 +14749,8 @@ }, "node_modules/source-map-support": { "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", "peer": true, @@ -11555,6 +14761,8 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "peer": true, @@ -11564,6 +14772,8 @@ }, "node_modules/space-separated-tokens": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "license": "MIT", "funding": { "type": "github", @@ -11572,6 +14782,8 @@ }, "node_modules/split.js": { "version": "1.6.5", + "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz", + "integrity": "sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==", "license": "MIT" }, "node_modules/sqlmesh": { @@ -11580,16 +14792,32 @@ }, "node_modules/stackback": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", "dev": true, "license": "MIT" }, "node_modules/stdin-discarder": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, "license": "MIT", "engines": { @@ -11599,23 +14827,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stream-read-all": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/string-argv": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, "license": "MIT", "engines": { @@ -11624,6 +14856,8 @@ }, "node_modules/string-width": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -11640,6 +14874,8 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11652,10 +14888,14 @@ }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11666,6 +14906,8 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", "dependencies": { @@ -11686,6 +14928,8 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11703,6 +14947,8 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", "dependencies": { @@ -11719,6 +14965,8 @@ }, "node_modules/stringify-entities": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "license": "MIT", "dependencies": { "character-entities-html4": "^2.0.0", @@ -11731,6 +14979,8 @@ }, "node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -11745,6 +14995,8 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11755,6 +15007,8 @@ }, "node_modules/strip-ansi/node_modules/ansi-regex": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { "node": ">=12" @@ -11765,6 +15019,8 @@ }, "node_modules/strip-final-newline": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, "license": "MIT", "engines": { @@ -11773,6 +15029,8 @@ }, "node_modules/strip-indent": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11784,6 +15042,8 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -11795,10 +15055,14 @@ }, "node_modules/style-mod": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", "license": "MIT" }, "node_modules/style-to-js": { "version": "1.1.16", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", + "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", "license": "MIT", "dependencies": { "style-to-object": "1.0.8" @@ -11806,6 +15070,8 @@ }, "node_modules/style-to-object": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", "license": "MIT", "dependencies": { "inline-style-parser": "0.2.4" @@ -11813,6 +15079,8 @@ }, "node_modules/sucrase": { "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -11833,6 +15101,8 @@ }, "node_modules/sucrase/node_modules/commander": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "license": "MIT", "engines": { "node": ">= 6" @@ -11840,6 +15110,8 @@ }, "node_modules/supports-color": { "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", "dev": true, "license": "MIT", "engines": { @@ -11851,6 +15123,8 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -11861,6 +15135,8 @@ }, "node_modules/swagger2openapi": { "version": "7.0.8", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", + "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11887,6 +15163,8 @@ }, "node_modules/swagger2openapi/node_modules/yaml": { "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, "license": "ISC", "engines": { @@ -11895,44 +15173,28 @@ }, "node_modules/symbol-tree": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, "license": "MIT" }, "node_modules/table-layout": { - "version": "3.0.2", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", "license": "MIT", "dependencies": { - "@75lb/deep-merge": "^1.1.1", "array-back": "^6.2.2", - "command-line-args": "^5.2.1", - "command-line-usage": "^7.0.0", - "stream-read-all": "^3.0.1", - "typical": "^7.1.1", "wordwrapjs": "^5.1.0" }, - "bin": { - "table-layout": "bin/cli.js" - }, - "engines": { - "node": ">=12.17" - } - }, - "node_modules/table-layout/node_modules/array-back": { - "version": "6.2.2", - "license": "MIT", - "engines": { - "node": ">=12.17" - } - }, - "node_modules/table-layout/node_modules/typical": { - "version": "7.3.0", - "license": "MIT", "engines": { "node": ">=12.17" } }, "node_modules/tailwindcss": { "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -11968,6 +15230,8 @@ }, "node_modules/tailwindcss/node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -11978,6 +15242,8 @@ }, "node_modules/tapable": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, "license": "MIT", "engines": { @@ -11986,6 +15252,8 @@ }, "node_modules/tar-fs": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "dev": true, "license": "MIT", "optional": true, @@ -11998,6 +15266,8 @@ }, "node_modules/tar-stream": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", "optional": true, @@ -12014,6 +15284,8 @@ }, "node_modules/tar-stream/node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", "optional": true, @@ -12028,6 +15300,8 @@ }, "node_modules/terser": { "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -12046,6 +15320,8 @@ }, "node_modules/terser-webpack-plugin": { "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, "license": "MIT", "peer": true, @@ -12080,12 +15356,16 @@ }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT", "peer": true }, "node_modules/test-exclude": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "license": "ISC", "dependencies": { @@ -12099,6 +15379,8 @@ }, "node_modules/test-exclude/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -12108,6 +15390,9 @@ }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -12127,6 +15412,8 @@ }, "node_modules/test-exclude/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -12138,6 +15425,8 @@ }, "node_modules/thememirror": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/thememirror/-/thememirror-2.0.1.tgz", + "integrity": "sha512-d5i6FVvWWPkwrm4cHLI3t9AT1OrkAt7Ig8dtdYSofgF7C/eiyNuq6zQzSTusWTde3jpW9WLvA9J/fzNKMUsd0w==", "license": "MIT", "peerDependencies": { "@codemirror/language": "^6.0.0", @@ -12147,6 +15436,8 @@ }, "node_modules/thenify": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -12154,6 +15445,8 @@ }, "node_modules/thenify-all": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -12164,16 +15457,22 @@ }, "node_modules/tinybench": { "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "dev": true, "license": "MIT", "dependencies": { @@ -12189,6 +15488,8 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12202,6 +15503,8 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", "engines": { @@ -12213,6 +15516,8 @@ }, "node_modules/tinypool": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", "dev": true, "license": "MIT", "engines": { @@ -12221,6 +15526,8 @@ }, "node_modules/tinyrainbow": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -12229,6 +15536,8 @@ }, "node_modules/tinyspy": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, "license": "MIT", "engines": { @@ -12237,6 +15546,8 @@ }, "node_modules/tmp": { "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, "license": "MIT", "engines": { @@ -12245,6 +15556,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -12257,8 +15570,20 @@ "resolved": "web/client", "link": true }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -12273,6 +15598,8 @@ }, "node_modules/tough-cookie/node_modules/universalify": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "license": "MIT", "engines": { @@ -12281,6 +15608,8 @@ }, "node_modules/tr46": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", "dev": true, "license": "MIT", "dependencies": { @@ -12292,6 +15621,8 @@ }, "node_modules/trim-lines": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "license": "MIT", "funding": { "type": "github", @@ -12300,6 +15631,8 @@ }, "node_modules/trough": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "license": "MIT", "funding": { "type": "github", @@ -12308,6 +15641,8 @@ }, "node_modules/ts-api-utils": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -12319,10 +15654,14 @@ }, "node_modules/ts-interface-checker": { "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, "node_modules/ts-loader": { "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -12342,6 +15681,8 @@ }, "node_modules/tsconfck": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.2.tgz", + "integrity": "sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==", "dev": true, "license": "MIT", "bin": { @@ -12361,10 +15702,14 @@ }, "node_modules/tslib": { "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tunnel": { "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true, "license": "MIT", "engines": { @@ -12373,6 +15718,8 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -12385,6 +15732,8 @@ }, "node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -12394,8 +15743,25 @@ "node": ">= 0.8.0" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { @@ -12409,6 +15775,8 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", "dependencies": { @@ -12427,6 +15795,8 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12447,6 +15817,8 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "license": "MIT", "dependencies": { @@ -12466,6 +15838,8 @@ }, "node_modules/typed-rest-client": { "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", "dev": true, "license": "MIT", "dependencies": { @@ -12475,7 +15849,9 @@ } }, "node_modules/typedoc": { - "version": "0.28.3", + "version": "0.28.4", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.4.tgz", + "integrity": "sha512-xKvKpIywE1rnqqLgjkoq0F3wOqYaKO9nV6YkkSat6IxOWacUCc/7Es0hR3OPmkIqkPoEn7U3x+sYdG72rstZQA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -12498,6 +15874,8 @@ }, "node_modules/typedoc-plugin-markdown": { "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.6.3.tgz", + "integrity": "sha512-86oODyM2zajXwLs4Wok2mwVEfCwCnp756QyhLGX2IfsdRYr1DXLCgJgnLndaMUjJD7FBhnLk2okbNE9PdLxYRw==", "dev": true, "license": "MIT", "engines": { @@ -12509,6 +15887,8 @@ }, "node_modules/typescript": { "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -12520,15 +15900,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz", - "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", + "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.31.1", - "@typescript-eslint/parser": "8.31.1", - "@typescript-eslint/utils": "8.31.1" + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", + "@typescript-eslint/utils": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12543,19 +15923,25 @@ } }, "node_modules/typical": { - "version": "4.0.0", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12.17" } }, "node_modules/uc.micro": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true, "license": "MIT" }, "node_modules/unbox-primitive": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { @@ -12573,11 +15959,15 @@ }, "node_modules/underscore": { "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", "dev": true, "license": "MIT" }, "node_modules/undici": { - "version": "6.21.2", + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", "dev": true, "license": "MIT", "engines": { @@ -12586,11 +15976,14 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "dev": true, + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, "node_modules/unified": { "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -12608,6 +16001,8 @@ }, "node_modules/unist-util-is": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -12619,6 +16014,8 @@ }, "node_modules/unist-util-position": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -12630,6 +16027,8 @@ }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -12641,6 +16040,8 @@ }, "node_modules/unist-util-visit": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -12654,6 +16055,8 @@ }, "node_modules/unist-util-visit-parents": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -12666,13 +16069,27 @@ }, "node_modules/universalify": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "license": "MIT", "engines": { "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -12702,6 +16119,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -12710,16 +16129,22 @@ }, "node_modules/urijs": { "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", "dev": true, "license": "MIT" }, "node_modules/url-join": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true, "license": "MIT" }, "node_modules/url-parse": { "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12729,6 +16154,8 @@ }, "node_modules/use-callback-ref": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -12748,6 +16175,8 @@ }, "node_modules/use-sidecar": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", @@ -12768,6 +16197,8 @@ }, "node_modules/use-sync-external-store": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -12775,10 +16206,14 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/utility-types": { "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", "dev": true, "license": "MIT", "engines": { @@ -12787,6 +16222,8 @@ }, "node_modules/uuid": { "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "license": "MIT", "bin": { @@ -12795,6 +16232,8 @@ }, "node_modules/v8-to-istanbul": { "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, "license": "ISC", "dependencies": { @@ -12808,14 +16247,28 @@ }, "node_modules/validator": { "version": "13.15.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vfile": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -12828,6 +16281,8 @@ }, "node_modules/vfile-message": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -12839,7 +16294,9 @@ } }, "node_modules/vite": { - "version": "6.3.4", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12912,13 +16369,15 @@ } }, "node_modules/vite-node": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", + "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", - "es-module-lexer": "^1.6.0", + "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, @@ -12934,6 +16393,8 @@ }, "node_modules/vite-plugin-css-injected-by-js": { "version": "3.5.2", + "resolved": "https://registry.npmjs.org/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.2.tgz", + "integrity": "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12942,6 +16403,8 @@ }, "node_modules/vite/node_modules/fdir": { "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12953,20 +16416,10 @@ } } }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", "engines": { @@ -12977,17 +16430,19 @@ } }, "node_modules/vitest": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", + "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.2", - "@vitest/mocker": "3.1.2", - "@vitest/pretty-format": "^3.1.2", - "@vitest/runner": "3.1.2", - "@vitest/snapshot": "3.1.2", - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", + "@vitest/expect": "3.1.3", + "@vitest/mocker": "3.1.3", + "@vitest/pretty-format": "^3.1.3", + "@vitest/runner": "3.1.3", + "@vitest/snapshot": "3.1.3", + "@vitest/spy": "3.1.3", + "@vitest/utils": "3.1.3", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", @@ -13000,7 +16455,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.2", + "vite-node": "3.1.3", "why-is-node-running": "^2.3.0" }, "bin": { @@ -13016,8 +16471,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.2", - "@vitest/ui": "3.1.2", + "@vitest/browser": "3.1.3", + "@vitest/ui": "3.1.3", "happy-dom": "*", "jsdom": "*" }, @@ -13047,6 +16502,8 @@ }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -13054,6 +16511,8 @@ }, "node_modules/vscode-languageclient": { "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", "license": "MIT", "dependencies": { "minimatch": "^5.1.0", @@ -13066,6 +16525,8 @@ }, "node_modules/vscode-languageclient/node_modules/minimatch": { "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -13076,6 +16537,8 @@ }, "node_modules/vscode-languageserver-protocol": { "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "license": "MIT", "dependencies": { "vscode-jsonrpc": "8.2.0", @@ -13084,14 +16547,20 @@ }, "node_modules/vscode-languageserver-types": { "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", "license": "MIT" }, "node_modules/w3c-keyname": { "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", "dev": true, "license": "MIT", "dependencies": { @@ -13103,6 +16572,8 @@ }, "node_modules/watchpack": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "license": "MIT", "peer": true, @@ -13116,6 +16587,8 @@ }, "node_modules/webidl-conversions": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -13123,13 +16596,16 @@ } }, "node_modules/webpack": { - "version": "5.99.6", + "version": "5.99.8", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz", + "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -13146,7 +16622,7 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", @@ -13170,6 +16646,8 @@ }, "node_modules/webpack-sources": { "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true, "license": "MIT", "peer": true, @@ -13179,6 +16657,8 @@ }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -13192,6 +16672,8 @@ }, "node_modules/webpack/node_modules/estraverse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -13199,8 +16681,35 @@ "node": ">=4.0" } }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13212,6 +16721,8 @@ }, "node_modules/whatwg-mimetype": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { @@ -13220,6 +16731,8 @@ }, "node_modules/whatwg-url": { "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13232,6 +16745,8 @@ }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -13245,6 +16760,8 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", "dependencies": { @@ -13263,6 +16780,8 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13289,11 +16808,15 @@ }, "node_modules/which-builtin-type/node_modules/isarray": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/which-collection": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "license": "MIT", "dependencies": { @@ -13311,6 +16834,8 @@ }, "node_modules/which-typed-array": { "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "license": "MIT", "dependencies": { @@ -13331,6 +16856,8 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -13346,6 +16873,8 @@ }, "node_modules/word-wrap": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -13354,6 +16883,8 @@ }, "node_modules/wordwrapjs": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz", + "integrity": "sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==", "license": "MIT", "engines": { "node": ">=12.17" @@ -13361,11 +16892,15 @@ }, "node_modules/workerpool": { "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true, "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -13382,6 +16917,8 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -13397,10 +16934,14 @@ }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -13413,6 +16954,8 @@ }, "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -13423,6 +16966,8 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -13433,11 +16978,15 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/ws": { - "version": "8.18.1", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "dev": true, "license": "MIT", "engines": { @@ -13458,6 +17007,8 @@ }, "node_modules/xml-name-validator": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -13466,6 +17017,8 @@ }, "node_modules/xml2js": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "license": "MIT", "dependencies": { @@ -13478,6 +17031,8 @@ }, "node_modules/xmlbuilder": { "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true, "license": "MIT", "engines": { @@ -13486,11 +17041,15 @@ }, "node_modules/xmlchars": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, "license": "MIT" }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { @@ -13499,11 +17058,15 @@ }, "node_modules/yallist": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "license": "ISC" }, "node_modules/yaml": { "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -13514,6 +17077,8 @@ }, "node_modules/yargs": { "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { @@ -13531,6 +17096,8 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { @@ -13539,6 +17106,8 @@ }, "node_modules/yargs-unparser": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "license": "MIT", "dependencies": { @@ -13553,6 +17122,8 @@ }, "node_modules/yargs-unparser/node_modules/is-plain-obj": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true, "license": "MIT", "engines": { @@ -13561,11 +17132,15 @@ }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -13579,6 +17154,8 @@ }, "node_modules/yargs/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -13590,6 +17167,8 @@ }, "node_modules/yauzl": { "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "license": "MIT", "dependencies": { @@ -13599,6 +17178,8 @@ }, "node_modules/yazl": { "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", "dev": true, "license": "MIT", "dependencies": { @@ -13607,6 +17188,8 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -13617,25 +17200,37 @@ } }, "node_modules/zod": { - "version": "3.24.3", + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "node_modules/zustand": { - "version": "4.5.6", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz", + "integrity": "sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==", "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, "engines": { - "node": ">=12.7.0" + "node": ">=12.20.0" }, "peerDependencies": { - "@types/react": ">=16.8", + "@types/react": ">=18.0.0", "immer": ">=9.0.6", - "react": ">=16.8" + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" }, "peerDependenciesMeta": { "@types/react": { @@ -13646,11 +17241,16 @@ }, "react": { "optional": true + }, + "use-sync-external-store": { + "optional": true } } }, "node_modules/zwitch": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", "license": "MIT", "funding": { "type": "github", @@ -13684,14 +17284,6 @@ "vscode": "^1.96.0" } }, - "vscode/extension/node_modules/@types/node": { - "version": "20.11.25", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "web/client": { "name": "tobiko", "version": "0.0.0", @@ -13757,63 +17349,6 @@ "optionalDependencies": { "@swc/core-linux-x64-gnu": "^1.11.24" } - }, - "web/client/node_modules/@testing-library/react": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", - "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "web/client/node_modules/zustand": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz", - "integrity": "sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } } } } From 1b8a5e7459fa4918ee8ecde1ff5a20e3a24c9215 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 13 May 2025 11:22:46 +0200 Subject: [PATCH 0147/1056] chore: fix dependabot file formatting (#4380) --- .github/dependabot.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index aff82a102d..db9ba4179a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,6 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "weekly" + interval: 'weekly' From 9003aa7508f70ea04362ae387a7a9d5e0928386b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 11:28:54 +0200 Subject: [PATCH 0148/1056] Chore(deps): Bump react-markdown from 9.1.0 to 10.1.0 (#4383) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 9 ++++----- web/client/package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a1fd32ae1..d86839f473 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -13637,9 +13636,9 @@ "peer": true }, "node_modules/react-markdown": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", - "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -17316,7 +17315,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", - "react-markdown": "^9.0.1", + "react-markdown": "^10.1.0", "react-router-dom": "^6.15.0", "react-split": "^2.0.14", "reactflow": "^11.8.3", diff --git a/web/client/package.json b/web/client/package.json index 7157176714..65d732fec0 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -43,7 +43,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", - "react-markdown": "^9.0.1", + "react-markdown": "^10.1.0", "react-router-dom": "^6.15.0", "react-split": "^2.0.14", "reactflow": "^11.8.3", From 873fa7707bc03b6432a7e5945956aa3dbad0258e Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 13 May 2025 11:37:36 +0200 Subject: [PATCH 0149/1056] chore: update diff dependency (#4384) --- package-lock.json | 21 ++++++++++++--------- web/client/package.json | 3 +-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index d86839f473..4d0a26bc52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -4711,13 +4712,6 @@ "@types/ms": "*" } }, - "node_modules/@types/diff": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz", - "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/es-aggregate-error": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", @@ -7767,6 +7761,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -17308,7 +17303,7 @@ "@uiw/react-codemirror": "^4.21.12", "apache-arrow": "^19.0.0", "clsx": "^2.0.0", - "diff": "^5.2.0", + "diff": "^8.0.0", "elkjs": "^0.8.2", "pluralize": "^8.0.0", "react": "^18.2.0", @@ -17329,7 +17324,6 @@ "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.4.3", - "@types/diff": "^5.2.1", "@types/pluralize": "^0.0.30", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", @@ -17348,6 +17342,15 @@ "optionalDependencies": { "@swc/core-linux-x64-gnu": "^1.11.24" } + }, + "web/client/node_modules/diff": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.0.tgz", + "integrity": "sha512-DJkPOAHudnz8swaqtm8cYmR9YfHLVDmoIH02+MqJiI/V9PxCf0WG+TBMduL7FZfnO53LhUXaPMo8Iw/uUJXLRA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } } } } diff --git a/web/client/package.json b/web/client/package.json index 65d732fec0..c7b05aeeff 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -36,7 +36,7 @@ "@uiw/react-codemirror": "^4.21.12", "apache-arrow": "^19.0.0", "clsx": "^2.0.0", - "diff": "^5.2.0", + "diff": "^8.0.0", "elkjs": "^0.8.2", "pluralize": "^8.0.0", "react": "^18.2.0", @@ -57,7 +57,6 @@ "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.4.3", - "@types/diff": "^5.2.1", "@types/pluralize": "^0.0.30", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", From 507178d2ff4980fa10aadba6e36c2eecc1664dc9 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 13 May 2025 11:45:00 +0200 Subject: [PATCH 0150/1056] chore: update pluralize deps (#4385) --- package-lock.json | 9 ++++----- web/client/package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d0a26bc52..6d38022bc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -4841,9 +4840,9 @@ } }, "node_modules/@types/pluralize": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.30.tgz", - "integrity": "sha512-kVww6xZrW/db5BR9OqiT71J9huRdQ+z/r+LbDuT7/EK50mCmj5FoaIARnVv0rvjUS/YpDox0cDU9lpQT011VBA==", + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.33.tgz", + "integrity": "sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==", "dev": true, "license": "MIT" }, @@ -17324,7 +17323,7 @@ "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.4.3", - "@types/pluralize": "^0.0.30", + "@types/pluralize": "^0.0.33", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react-swc": "^3.9.0", diff --git a/web/client/package.json b/web/client/package.json index c7b05aeeff..1214cad1dd 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -57,7 +57,7 @@ "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.4.3", - "@types/pluralize": "^0.0.30", + "@types/pluralize": "^0.0.33", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react-swc": "^3.9.0", From 7530bf775d941b5c4399670297842c10d82e8d99 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 13 May 2025 12:58:54 +0300 Subject: [PATCH 0151/1056] Chore: Add legends for clarity in multiengine docs (#4386) --- docs/guides/multi_engine.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/guides/multi_engine.md b/docs/guides/multi_engine.md index f22187f25f..f2ccd31394 100644 --- a/docs/guides/multi_engine.md +++ b/docs/guides/multi_engine.md @@ -117,7 +117,8 @@ The `order_ship_date` model specifies the DuckDB engine, which will perform the This allows you to efficiently scan data from an Iceberg table, or even query tables directly from S3 when used with the [HTTPFS](https://duckdb.org/docs/stable/extensions/httpfs/overview.html) extension. -![PostgreSQL + DuckDB](./multi_engine/postgres_duckdb.png) +![Figure 1: PostgreSQL + DuckDB](./multi_engine/postgres_duckdb.png) +*Figure 1: The gateways denote the execution engine, while both the virtual layer’s views and the physical layer's tables reside in Postgres* In models where no gateway is specified, such as the `customer_orders` model, the default PostgreSQL engine will both create the physical table and the views in the virtual layer. @@ -284,7 +285,8 @@ FROM ``` -![Athena + Redshift + Snowflake](./multi_engine/athena_redshift_snowflake.png) +![Figure 2: Athena + Redshift + Snowflake](./multi_engine/athena_redshift_snowflake.png) +*Figure 2: The gateways represent the execution engine and indicate where the virtual layer’s views and the physical layer's tables reside* When you run the plan, the catalogs for each model will be set automatically based on the gateway’s connection and each corresponding model will be executed by the specified engine: From 4067f40ead771b029c8aaa41a94a038900580e99 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 13 May 2025 12:07:15 +0200 Subject: [PATCH 0152/1056] chore: update react query (#4387) --- package-lock.json | 30 ++++++++++-------------------- web/client/package.json | 2 +- web/client/src/api/index.ts | 2 +- web/client/src/main.tsx | 2 +- 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d38022bc8..a3dfe78ab0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -4205,9 +4206,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", - "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "version": "5.76.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.76.0.tgz", + "integrity": "sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg==", "license": "MIT", "funding": { "type": "github", @@ -4215,30 +4216,19 @@ } }, "node_modules/@tanstack/react-query": { - "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", - "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "version": "5.76.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.76.0.tgz", + "integrity": "sha512-dZLYzVuUFZJkenxd8o01oyFimeLBmSkaUviPHuDzXe7LSLO4WTTx92jwJlNUXOOHzg6t0XknklZ15cjhYNSDjA==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "4.36.1", - "use-sync-external-store": "^1.2.0" + "@tanstack/query-core": "5.76.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } + "react": "^18 || ^19" } }, "node_modules/@tanstack/react-table": { @@ -17295,7 +17285,7 @@ "@radix-ui/react-context-menu": "^2.1.4", "@radix-ui/react-select": "^1.2.2", "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^4.33.0", + "@tanstack/react-query": "^5.0.0", "@tanstack/react-table": "^8.9.2", "@tanstack/react-virtual": "^3.0.0-beta.56", "@uidotdev/usehooks": "^2.2.0", diff --git a/web/client/package.json b/web/client/package.json index 1214cad1dd..91e98e7036 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -29,7 +29,7 @@ "@radix-ui/react-context-menu": "^2.1.4", "@radix-ui/react-select": "^1.2.2", "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^4.33.0", + "@tanstack/react-query": "^5.0.0", "@tanstack/react-table": "^8.9.2", "@tanstack/react-virtual": "^3.0.0-beta.56", "@uidotdev/usehooks": "^2.2.0", diff --git a/web/client/src/api/index.ts b/web/client/src/api/index.ts index 5e6c99dd28..2cd92b08dd 100644 --- a/web/client/src/api/index.ts +++ b/web/client/src/api/index.ts @@ -542,7 +542,7 @@ function useQueryWithTimeout< } const q = useQuery({ - cacheTime: 0, + gcTime: 0, enabled: false, queryKey: options.queryKey, queryFn, diff --git a/web/client/src/main.tsx b/web/client/src/main.tsx index 0ab5e7c8a7..3486e3b408 100644 --- a/web/client/src/main.tsx +++ b/web/client/src/main.tsx @@ -20,7 +20,7 @@ export interface PropsComponent extends HTMLAttributes {} const client = new QueryClient({ queryCache: new QueryCache({ onError(error, query) { - ;(query.meta as ApiQueryMeta).onError(error as ErrorIDE) + ;(query.meta as ApiQueryMeta).onError(error as unknown as ErrorIDE) }, onSuccess(_, query) { ;(query.meta as ApiQueryMeta).onSuccess() From 4f8bce911ac6a4554b89d04ced9233574138c856 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 13 May 2025 12:33:07 +0200 Subject: [PATCH 0153/1056] chore: update jsdom (#4389) --- package-lock.json | 463 +++++++++++++++++++--------------------- web/client/package.json | 2 +- 2 files changed, 223 insertions(+), 242 deletions(-) diff --git a/package-lock.json b/package-lock.json index a3dfe78ab0..69be9927b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -125,6 +124,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.7.tgz", + "integrity": "sha512-Ok5fYhtwdyJQmU1PpEv6Si7Y+A4cYb8yNM9oiIJC9TzXPMuN9fvdonKJqcnz9TbFqV6bQ8z0giRq0iaOpGZV2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@asyncapi/specs": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-6.8.1.tgz", @@ -475,6 +495,121 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.3.tgz", + "integrity": "sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.9.tgz", + "integrity": "sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", @@ -4409,16 +4544,6 @@ "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -5900,14 +6025,6 @@ "license": "Apache-2.0", "peer": true }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -7295,16 +7412,17 @@ } }, "node_modules/cssstyle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz", + "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==", "dev": true, "license": "MIT", "dependencies": { - "rrweb-cssom": "^0.6.0" + "@asamuzakjp/css-color": "^3.1.2", + "rrweb-cssom": "^0.8.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/csstype": { @@ -7419,28 +7537,17 @@ } }, "node_modules/data-urls": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", - "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=14" - } - }, - "node_modules/data-urls/node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/data-view-buffer": { @@ -7822,20 +7929,6 @@ ], "license": "BSD-2-Clause" }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "license": "MIT", - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -9425,29 +9518,16 @@ } }, "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-encoding-sniffer/node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { - "iconv-lite": "0.6.3" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/html-escaper": { @@ -10404,41 +10484,38 @@ } }, "node_modules/jsdom": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", - "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "cssstyle": "^3.0.0", - "data-urls": "^4.0.0", - "decimal.js": "^10.4.3", - "domexception": "^4.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.4", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.1", - "ws": "^8.13.0", - "xml-name-validator": "^4.0.0" + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -10446,71 +10523,6 @@ } } }, - "node_modules/jsdom/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/jsdom/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/jsdom/node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/jsdom/node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/jsep": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", @@ -13395,19 +13407,6 @@ "node": ">= 0.10" } }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -13456,13 +13455,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -13965,13 +13957,6 @@ "node": ">=0.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -14103,9 +14088,9 @@ } }, "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, "license": "MIT" }, @@ -15527,6 +15512,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -15564,42 +15569,29 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" + "node": ">=16" } }, "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/trim-lines": { @@ -16124,17 +16116,6 @@ "dev": true, "license": "MIT" }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -16541,16 +16522,16 @@ "license": "MIT" }, "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", "dependencies": { - "xml-name-validator": "^4.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/watchpack": { @@ -16713,17 +16694,17 @@ } }, "node_modules/whatwg-url": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", - "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "^4.1.1", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/which": { @@ -16989,13 +16970,13 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xml2js": { @@ -17318,7 +17299,7 @@ "@types/react-dom": "^18.2.7", "@vitejs/plugin-react-swc": "^3.9.0", "autoprefixer": "^10.4.15", - "jsdom": "^22.1.0", + "jsdom": "^26.1.0", "orval": "^7.9.0", "postcss": "^8.4.29", "tailwindcss": "^3.3.3", diff --git a/web/client/package.json b/web/client/package.json index 91e98e7036..128e70e9e3 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -62,7 +62,7 @@ "@types/react-dom": "^18.2.7", "@vitejs/plugin-react-swc": "^3.9.0", "autoprefixer": "^10.4.15", - "jsdom": "^22.1.0", + "jsdom": "^26.1.0", "orval": "^7.9.0", "postcss": "^8.4.29", "tailwindcss": "^3.3.3", From 00797d2b59c45a666296ce1fa8dde36cdfee1712 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 13 May 2025 13:01:17 +0200 Subject: [PATCH 0154/1056] chore: update react router (#4390) --- package-lock.json | 80 +++++++++---------- web/client/package.json | 2 +- web/client/src/App.tsx | 2 +- .../components/editor/EditorPreview.tsx | 2 +- .../historyNavigation/historyNavigation.tsx | 2 +- .../moduleNavigation/ModuleLink.tsx | 2 +- .../library/components/plan/PlanActions.tsx | 2 +- .../library/components/search/SearchList.tsx | 2 +- .../components/sourceList/SourceListItem.tsx | 2 +- .../src/library/pages/audits/Audits.tsx | 2 +- .../pages/data-catalog/DataCatalog.tsx | 2 +- web/client/src/library/pages/data/Data.tsx | 2 +- .../src/library/pages/errors/Content.tsx | 2 +- .../src/library/pages/errors/Errors.tsx | 2 +- .../src/library/pages/lineage/Lineage.tsx | 2 +- .../src/library/pages/models/Models.tsx | 2 +- web/client/src/library/pages/plan/Content.tsx | 2 +- web/client/src/library/pages/plan/Plan.tsx | 2 +- .../src/library/pages/root/NotFound.tsx | 2 +- web/client/src/library/pages/root/Root.tsx | 2 +- web/client/src/library/pages/tests/Tests.tsx | 2 +- web/client/src/routes.tsx | 2 +- 22 files changed, 59 insertions(+), 63 deletions(-) diff --git a/package-lock.json b/package-lock.json index 69be9927b4..89eeb14b16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3273,15 +3273,6 @@ } } }, - "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.40.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", @@ -13685,38 +13676,6 @@ } } }, - "node_modules/react-router": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", - "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", - "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, "node_modules/react-split": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/react-split/-/react-split-2.0.14.tgz", @@ -14385,6 +14344,12 @@ "node": ">= 18" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -17281,7 +17246,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", - "react-router-dom": "^6.15.0", + "react-router": "^7.0.0", "react-split": "^2.0.14", "reactflow": "^11.8.3", "thememirror": "^2.0.1", @@ -17313,6 +17278,15 @@ "@swc/core-linux-x64-gnu": "^1.11.24" } }, + "web/client/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "web/client/node_modules/diff": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.0.tgz", @@ -17321,6 +17295,28 @@ "engines": { "node": ">=0.3.1" } + }, + "web/client/node_modules/react-router": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", + "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } } } } diff --git a/web/client/package.json b/web/client/package.json index 128e70e9e3..6e93613dab 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -44,7 +44,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", - "react-router-dom": "^6.15.0", + "react-router": "^7.0.0", "react-split": "^2.0.14", "reactflow": "^11.8.3", "thememirror": "^2.0.1", diff --git a/web/client/src/App.tsx b/web/client/src/App.tsx index adc6b6f8fd..4ee9ae24fe 100644 --- a/web/client/src/App.tsx +++ b/web/client/src/App.tsx @@ -1,5 +1,5 @@ import { useEffect, Suspense, lazy } from 'react' -import { RouterProvider } from 'react-router-dom' +import { RouterProvider } from 'react-router' import { Divider } from '@components/divider/Divider' import { getBrowserRouter } from './routes' import { useApiModules } from './api' diff --git a/web/client/src/library/components/editor/EditorPreview.tsx b/web/client/src/library/components/editor/EditorPreview.tsx index 5478e9ab71..110ff4d65c 100644 --- a/web/client/src/library/components/editor/EditorPreview.tsx +++ b/web/client/src/library/components/editor/EditorPreview.tsx @@ -9,7 +9,7 @@ import { EnumSize, EnumVariant } from '~/types/enum' import { EnumFileExtensions } from '@models/file' import { CodeEditorDefault } from './EditorCode' import { EnumRoutes } from '~/routes' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from 'react-router' import TableDiff from '@components/tableDiff/TableDiff' import TabList from '@components/tab/Tab' import { useSQLMeshModelExtensions } from './hooks' diff --git a/web/client/src/library/components/historyNavigation/historyNavigation.tsx b/web/client/src/library/components/historyNavigation/historyNavigation.tsx index 9cba4683f7..df9fd43489 100644 --- a/web/client/src/library/components/historyNavigation/historyNavigation.tsx +++ b/web/client/src/library/components/historyNavigation/historyNavigation.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from 'react-router-dom' +import { useNavigate } from 'react-router' import { ChevronRightIcon, ChevronLeftIcon } from '@heroicons/react/20/solid' export default function HistoryNavigation(): JSX.Element { diff --git a/web/client/src/library/components/moduleNavigation/ModuleLink.tsx b/web/client/src/library/components/moduleNavigation/ModuleLink.tsx index 19342614fa..2e9e6856fe 100644 --- a/web/client/src/library/components/moduleNavigation/ModuleLink.tsx +++ b/web/client/src/library/components/moduleNavigation/ModuleLink.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import { NavLink } from 'react-router-dom' +import { NavLink } from 'react-router' import { isFalse } from '@utils/index' export default function ModuleLink({ diff --git a/web/client/src/library/components/plan/PlanActions.tsx b/web/client/src/library/components/plan/PlanActions.tsx index 7fdb0fe8cb..6241556fd5 100644 --- a/web/client/src/library/components/plan/PlanActions.tsx +++ b/web/client/src/library/components/plan/PlanActions.tsx @@ -7,7 +7,7 @@ import { EnumPlanAction, ModelPlanAction } from '@models/plan-action' import { useStorePlan } from '@context/plan' import { useStoreContext } from '@context/context' import { Transition } from '@headlessui/react' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from 'react-router' import { SelectEnvironment } from '@components/environmentDetails/SelectEnvironment' import { AddEnvironment } from '@components/environmentDetails/AddEnvironment' import { usePlan } from './context' diff --git a/web/client/src/library/components/search/SearchList.tsx b/web/client/src/library/components/search/SearchList.tsx index fd81c2fb99..fae23f2303 100644 --- a/web/client/src/library/components/search/SearchList.tsx +++ b/web/client/src/library/components/search/SearchList.tsx @@ -9,7 +9,7 @@ import { } from '@utils/index' import { EnumSize, type Size } from '~/types/enum' import { EMPTY_STRING, filterListBy, highlightMatch } from './help' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from 'react-router' import clsx from 'clsx' import { Popover, Transition } from '@headlessui/react' import { useClickAway } from '@uidotdev/usehooks' diff --git a/web/client/src/library/components/sourceList/SourceListItem.tsx b/web/client/src/library/components/sourceList/SourceListItem.tsx index d9dc8ee02b..dedf62e9c5 100644 --- a/web/client/src/library/components/sourceList/SourceListItem.tsx +++ b/web/client/src/library/components/sourceList/SourceListItem.tsx @@ -1,6 +1,6 @@ import { isNotNil } from '@utils/index' import clsx from 'clsx' -import { NavLink } from 'react-router-dom' +import { NavLink } from 'react-router' import { EnumVariant, type Variant } from '~/types/enum' export default function SourceListItem({ diff --git a/web/client/src/library/pages/audits/Audits.tsx b/web/client/src/library/pages/audits/Audits.tsx index 95b685b416..9f96463d39 100644 --- a/web/client/src/library/pages/audits/Audits.tsx +++ b/web/client/src/library/pages/audits/Audits.tsx @@ -1,4 +1,4 @@ -import { Outlet, useLocation } from 'react-router-dom' +import { Outlet, useLocation } from 'react-router' import Page from '../root/Page' import { useStoreProject } from '@context/project' import SourceList from '@components/sourceList/SourceList' diff --git a/web/client/src/library/pages/data-catalog/DataCatalog.tsx b/web/client/src/library/pages/data-catalog/DataCatalog.tsx index 1eb39ccb78..daa1f73fee 100644 --- a/web/client/src/library/pages/data-catalog/DataCatalog.tsx +++ b/web/client/src/library/pages/data-catalog/DataCatalog.tsx @@ -1,7 +1,7 @@ import Documentation from '@components/documentation/Documentation' import ModelLineage from '@components/graph/ModelLineage' import { useStoreContext } from '@context/context' -import { useNavigate, useParams } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router' import NotFound from '../root/NotFound' import { EnumRoutes } from '~/routes' import LineageFlowProvider from '@components/graph/context' diff --git a/web/client/src/library/pages/data/Data.tsx b/web/client/src/library/pages/data/Data.tsx index 9aa2c6d6e0..24cd3a1913 100644 --- a/web/client/src/library/pages/data/Data.tsx +++ b/web/client/src/library/pages/data/Data.tsx @@ -1,5 +1,5 @@ import { useStoreContext } from '@context/context' -import { useParams } from 'react-router-dom' +import { useParams } from 'react-router' import { isNil, toDate, toDateFormat } from '@utils/index' import { useEffect } from 'react' import Table from '@components/table/Table' diff --git a/web/client/src/library/pages/errors/Content.tsx b/web/client/src/library/pages/errors/Content.tsx index 3e9402dce1..d0cb5362aa 100644 --- a/web/client/src/library/pages/errors/Content.tsx +++ b/web/client/src/library/pages/errors/Content.tsx @@ -1,4 +1,4 @@ -import { useParams } from 'react-router-dom' +import { useParams } from 'react-router' import NotFound from '../root/NotFound' import { EnumRoutes } from '~/routes' import { useNotificationCenter } from '../root/context/notificationCenter' diff --git a/web/client/src/library/pages/errors/Errors.tsx b/web/client/src/library/pages/errors/Errors.tsx index 98f003134f..e9d69a2df3 100644 --- a/web/client/src/library/pages/errors/Errors.tsx +++ b/web/client/src/library/pages/errors/Errors.tsx @@ -6,7 +6,7 @@ import Page from '../root/Page' import SourceList from '@components/sourceList/SourceList' import SourceListItem from '@components/sourceList/SourceListItem' import { EnumRoutes } from '~/routes' -import { Outlet, useLocation, useNavigate } from 'react-router-dom' +import { Outlet, useLocation, useNavigate } from 'react-router' import { EnumSize, EnumVariant } from '~/types/enum' import { useEffect, useMemo } from 'react' import { isArrayEmpty, isNil, isNotNil } from '@utils/index' diff --git a/web/client/src/library/pages/lineage/Lineage.tsx b/web/client/src/library/pages/lineage/Lineage.tsx index f24e5429e7..40f3d6409d 100644 --- a/web/client/src/library/pages/lineage/Lineage.tsx +++ b/web/client/src/library/pages/lineage/Lineage.tsx @@ -1,5 +1,5 @@ import { useStoreContext } from '@context/context' -import { useNavigate, useParams } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router' import { isNil } from '@utils/index' import LineageFlowProvider from '@components/graph/context' import ModelLineage from '@components/graph/ModelLineage' diff --git a/web/client/src/library/pages/models/Models.tsx b/web/client/src/library/pages/models/Models.tsx index 9d7c310a4f..0ba6883bab 100644 --- a/web/client/src/library/pages/models/Models.tsx +++ b/web/client/src/library/pages/models/Models.tsx @@ -1,4 +1,4 @@ -import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom' +import { Outlet, useLocation, useNavigate, useParams } from 'react-router' import { useEffect, useMemo } from 'react' import { isArrayEmpty, diff --git a/web/client/src/library/pages/plan/Content.tsx b/web/client/src/library/pages/plan/Content.tsx index 990e8d5f2d..321501edac 100644 --- a/web/client/src/library/pages/plan/Content.tsx +++ b/web/client/src/library/pages/plan/Content.tsx @@ -1,4 +1,4 @@ -import { useParams } from 'react-router-dom' +import { useParams } from 'react-router' import { isNil } from '@utils/index' import PlanProvider from '@components/plan/context' import { useStoreContext } from '@context/context' diff --git a/web/client/src/library/pages/plan/Plan.tsx b/web/client/src/library/pages/plan/Plan.tsx index ba4c501429..7d66ea4d7b 100644 --- a/web/client/src/library/pages/plan/Plan.tsx +++ b/web/client/src/library/pages/plan/Plan.tsx @@ -1,7 +1,7 @@ import Page from '../root/Page' import { useStoreContext } from '@context/context' import { EnumRoutes } from '~/routes' -import { Outlet, useLocation, useNavigate } from 'react-router-dom' +import { Outlet, useLocation, useNavigate } from 'react-router' import { useEffect } from 'react' import { useStorePlan } from '@context/plan' import { isNotNil } from '@utils/index' diff --git a/web/client/src/library/pages/root/NotFound.tsx b/web/client/src/library/pages/root/NotFound.tsx index 875fd32a07..f6b0ae3105 100644 --- a/web/client/src/library/pages/root/NotFound.tsx +++ b/web/client/src/library/pages/root/NotFound.tsx @@ -1,5 +1,5 @@ import Container from '@components/container/Container' -import { Link } from 'react-router-dom' +import { Link } from 'react-router' import { ButtonLink } from '@components/button/Button' import { EnumVariant } from '~/types/enum' import { isNotNil } from '@utils/index' diff --git a/web/client/src/library/pages/root/Root.tsx b/web/client/src/library/pages/root/Root.tsx index 911906be5f..6f609d73a3 100644 --- a/web/client/src/library/pages/root/Root.tsx +++ b/web/client/src/library/pages/root/Root.tsx @@ -3,7 +3,7 @@ import { useStoreContext } from '@context/context' import ModuleNavigation from '@components/moduleNavigation/ModuleNavigation' import Navigation from './Navigation' import Container from '@components/container/Container' -import { Outlet, useLocation, useNavigate } from 'react-router-dom' +import { Outlet, useLocation, useNavigate } from 'react-router' import { EnumErrorKey, type ErrorIDE, diff --git a/web/client/src/library/pages/tests/Tests.tsx b/web/client/src/library/pages/tests/Tests.tsx index 0412ebce30..7179199541 100644 --- a/web/client/src/library/pages/tests/Tests.tsx +++ b/web/client/src/library/pages/tests/Tests.tsx @@ -1,4 +1,4 @@ -import { Outlet, useLocation } from 'react-router-dom' +import { Outlet, useLocation } from 'react-router' import Page from '../root/Page' import { useStoreProject } from '@context/project' import SourceList from '@components/sourceList/SourceList' diff --git a/web/client/src/routes.tsx b/web/client/src/routes.tsx index 98bf939451..f79cd1c9b2 100644 --- a/web/client/src/routes.tsx +++ b/web/client/src/routes.tsx @@ -1,4 +1,4 @@ -import { type RouteObject, createBrowserRouter } from 'react-router-dom' +import { type RouteObject, createBrowserRouter } from 'react-router' import { Suspense, lazy } from 'react' import NotFound from './library/pages/root/NotFound' import Loading from '@components/loading/Loading' From 2620b7ea61fa4aa4e2fa25fd1690462c9eee40df Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 13 May 2025 16:14:03 +0200 Subject: [PATCH 0155/1056] chore: update radix select in web (#4392) --- package-lock.json | 633 ++---------------- web/client/package.json | 3 +- .../src/library/components/input/Selector.tsx | 2 +- 3 files changed, 72 insertions(+), 566 deletions(-) diff --git a/package-lock.json b/package-lock.json index 89eeb14b16..8e126fa419 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1847,13 +1847,10 @@ } }, "node_modules/@radix-ui/number": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", - "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" }, "node_modules/@radix-ui/primitive": { "version": "1.1.2", @@ -2243,325 +2240,38 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz", - "integrity": "sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.3", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.2", - "@radix-ui/react-portal": "1.0.3", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", - "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", - "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", - "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", - "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", - "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz", + "integrity": "sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", - "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", - "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", - "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.9", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.6", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.6", + "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -2572,171 +2282,6 @@ } } }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", - "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", - "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", - "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@radix-ui/react-select/node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-slot": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", @@ -2841,16 +2386,13 @@ } }, "node_modules/@radix-ui/react-use-previous": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", - "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -2895,61 +2437,18 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", - "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz", + "integrity": "sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" + "@radix-ui/react-primitive": "2.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -2960,25 +2459,6 @@ } } }, - "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", @@ -17229,7 +16709,7 @@ "@heroicons/react": "^2.0.18", "@lit/react": "^1.0.6", "@radix-ui/react-context-menu": "^2.1.4", - "@radix-ui/react-select": "^1.2.2", + "@radix-ui/react-select": "^2.2.4", "@tailwindcss/container-queries": "^0.1.1", "@tanstack/react-query": "^5.0.0", "@tanstack/react-table": "^8.9.2", @@ -17263,6 +16743,7 @@ "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react-swc": "^3.9.0", + "ajv": "^8.17.1", "autoprefixer": "^10.4.15", "jsdom": "^26.1.0", "orval": "^7.9.0", @@ -17278,6 +16759,23 @@ "@swc/core-linux-x64-gnu": "^1.11.24" } }, + "web/client/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "web/client/node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -17288,14 +16786,21 @@ } }, "web/client/node_modules/diff": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.0.tgz", - "integrity": "sha512-DJkPOAHudnz8swaqtm8cYmR9YfHLVDmoIH02+MqJiI/V9PxCf0WG+TBMduL7FZfnO53LhUXaPMo8Iw/uUJXLRA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.1.tgz", + "integrity": "sha512-rEaM3KmVm78zE3dFZaop3aCQa2MTm+T4kcigUFLVU/KbOYdiY6JnL2g2puOYnct3QFw9pjZadaCbCZ1O8ArMlQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, + "web/client/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "web/client/node_modules/react-router": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", diff --git a/web/client/package.json b/web/client/package.json index 6e93613dab..27e541641e 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -27,7 +27,7 @@ "@heroicons/react": "^2.0.18", "@lit/react": "^1.0.6", "@radix-ui/react-context-menu": "^2.1.4", - "@radix-ui/react-select": "^1.2.2", + "@radix-ui/react-select": "^2.2.4", "@tailwindcss/container-queries": "^0.1.1", "@tanstack/react-query": "^5.0.0", "@tanstack/react-table": "^8.9.2", @@ -61,6 +61,7 @@ "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react-swc": "^3.9.0", + "ajv": "^8.17.1", "autoprefixer": "^10.4.15", "jsdom": "^26.1.0", "orval": "^7.9.0", diff --git a/web/client/src/library/components/input/Selector.tsx b/web/client/src/library/components/input/Selector.tsx index 3f2d1939a2..c28164a382 100644 --- a/web/client/src/library/components/input/Selector.tsx +++ b/web/client/src/library/components/input/Selector.tsx @@ -65,7 +65,7 @@ export default React.forwardRef( /> - + Date: Tue, 13 May 2025 16:43:10 +0200 Subject: [PATCH 0156/1056] chore: update headless ui (#4393) --- package-lock.json | 144 ++++++++++++++++-- web/client/package.json | 2 +- .../components/editor/EditorPreview.tsx | 35 +++-- .../library/components/graph/ModelLineage.tsx | 1 + .../library/components/plan/PlanActions.tsx | 1 + .../components/plan/PlanApplyStageTracker.tsx | 3 + .../tasksOverview/TasksOverview.tsx | 1 + 7 files changed, 156 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e126fa419..8bbdb3a35b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "sqlmesh", "workspaces": [ "vscode/extension", "web/client" @@ -1247,6 +1248,21 @@ "@floating-ui/utils": "^0.2.9" } }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/react-dom": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", @@ -1281,20 +1297,23 @@ } }, "node_modules/@headlessui/react": { - "version": "1.7.19", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", - "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.3.tgz", + "integrity": "sha512-hgOJGXPifPlOczIeSwX8OjLWRJ5XdYApZFf7DeCbCrO1PXHkPhNTRrA9ZwJsgAG7SON1i2JcvIreF/kbgtJeaQ==", "license": "MIT", "dependencies": { - "@tanstack/react-virtual": "^3.0.0-beta.60", - "client-only": "^0.0.1" + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.6", + "use-sync-external-store": "^1.5.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "react": "^16 || ^17 || ^18", - "react-dom": "^16 || ^17 || ^18" + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "node_modules/@heroicons/react": { @@ -2465,6 +2484,73 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@react-aria/focus": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.2.tgz", + "integrity": "sha512-Q3rouk/rzoF/3TuH6FzoAIKrl+kzZi9LHmr8S5EqLAOyP9TXIKG34x2j42dZsAhrw7TbF9gA8tBKwnCNH4ZV+Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.0", + "@react-aria/utils": "^3.28.2", + "@react-types/shared": "^3.29.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.0.tgz", + "integrity": "sha512-GgIsDLlO8rDU/nFn6DfsbP9rfnzhm8QFjZkB9K9+r+MTSCn7bMntiWQgMM+5O6BiA8d7C7x4zuN4bZtc0RBdXQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.8", + "@react-aria/utils": "^3.28.2", + "@react-stately/flags": "^3.1.1", + "@react-types/shared": "^3.29.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.8.tgz", + "integrity": "sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.28.2", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.2.tgz", + "integrity": "sha512-J8CcLbvnQgiBn54eeEvQQbIOfBF3A1QizxMw9P4cl9MkeR03ug7RnjTIdJY/n2p7t59kLeAB3tqiczhcj+Oi5w==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.8", + "@react-stately/flags": "^3.1.1", + "@react-stately/utils": "^3.10.6", + "@react-types/shared": "^3.29.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@react-dnd/asap": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", @@ -2483,6 +2569,36 @@ "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", "license": "MIT" }, + "node_modules/@react-stately/flags": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.1.tgz", + "integrity": "sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.6.tgz", + "integrity": "sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.29.0.tgz", + "integrity": "sha512-IDQYu/AHgZimObzCFdNl1LpZvQW/xcfLt3v20sorl5qRucDVj4S9os98sVTZ4IRIBjmS+MkjqpR5E70xan7ooA==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@reactflow/background": { "version": "11.3.14", "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", @@ -6531,12 +6647,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -14591,6 +14701,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/table-layout": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", @@ -16705,7 +16821,7 @@ "@codemirror/legacy-modes": "^6.4.0", "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.28.1", - "@headlessui/react": "^1.7.17", + "@headlessui/react": "^2.0.0", "@heroicons/react": "^2.0.18", "@lit/react": "^1.0.6", "@radix-ui/react-context-menu": "^2.1.4", diff --git a/web/client/package.json b/web/client/package.json index 27e541641e..41c56e5ff2 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -23,7 +23,7 @@ "@codemirror/legacy-modes": "^6.4.0", "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.28.1", - "@headlessui/react": "^1.7.17", + "@headlessui/react": "^2.0.0", "@heroicons/react": "^2.0.18", "@lit/react": "^1.0.6", "@radix-ui/react-context-menu": "^2.1.4", diff --git a/web/client/src/library/components/editor/EditorPreview.tsx b/web/client/src/library/components/editor/EditorPreview.tsx index 110ff4d65c..b40aa0deab 100644 --- a/web/client/src/library/components/editor/EditorPreview.tsx +++ b/web/client/src/library/components/editor/EditorPreview.tsx @@ -1,5 +1,5 @@ -import { Suspense, lazy, useEffect, useMemo, useState } from 'react' -import { Tab } from '@headlessui/react' +import { Fragment, Suspense, lazy, useEffect, useMemo, useState } from 'react' +import { TabGroup, TabPanel, TabPanels } from '@headlessui/react' import clsx from 'clsx' import { includes, isArrayEmpty, isFalse, isNotNil } from '~/utils' import { type EditorTab, useStoreEditor } from '~/context/editor' @@ -155,7 +155,8 @@ export default function EditorPreview({

No Data To Preview

) : ( - - + {isNotNil(previewTable) && ( - - + )} {isNotNil(previewQuery) && tab.file.isRemote && ( - @@ -211,10 +212,11 @@ export default function EditorPreview({ className="text-xs" /> - + )} {showLineage && ( - - + )} {isNotNil(previewDiff?.row_diff) && ( - - + )} {showErrors && ( - @@ -262,10 +265,10 @@ export default function EditorPreview({ ))} - + )} - - + + )} ) diff --git a/web/client/src/library/components/graph/ModelLineage.tsx b/web/client/src/library/components/graph/ModelLineage.tsx index e3721a4912..0723b18a57 100644 --- a/web/client/src/library/components/graph/ModelLineage.tsx +++ b/web/client/src/library/components/graph/ModelLineage.tsx @@ -442,6 +442,7 @@ function GraphControls({ nodes = [] }: { nodes: Node[] }): JSX.Element {
diff --git a/web/client/src/library/components/plan/PlanActions.tsx b/web/client/src/library/components/plan/PlanActions.tsx index 6241556fd5..baab36c726 100644 --- a/web/client/src/library/components/plan/PlanActions.tsx +++ b/web/client/src/library/components/plan/PlanActions.tsx @@ -163,6 +163,7 @@ export default function PlanActions({ leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" className="trasition-all duration-300 ease-in-out" + as="div" > {showPlanActionButton && (
PR Environment Summary
ModelChange TypeDates Loaded
sushi.waiter_revenue_by_dayNon-breaking2022-12-25 - 2022-12-31
""" + ) + + assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping + prod_plan_preview_checks_runs = controller._check_run_mapping[ + "SQLMesh - Prod Plan Preview" + ].all_kwargs + assert len(prod_plan_preview_checks_runs) == 3 + assert GithubCheckStatus(prod_plan_preview_checks_runs[0]["status"]).is_queued + assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress + assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed + assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success + expected_prod_plan_summary = """\n**Summary of differences from `prod`:** + +**Directly Modified:** +- `sushi.waiter_revenue_by_day` +```diff +--- + ++++ + +@@ -16,7 +16,8 @@ + + SELECT + CAST(o.waiter_id AS INT) AS waiter_id, + CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, +- CAST(o.event_date AS DATE) AS event_date ++ CAST(o.event_date AS DATE) AS event_date, ++ 1 AS new_col + FROM sushi.orders AS o + LEFT JOIN sushi.order_items AS oi + ON o.id = oi.order_id AND o.event_date = oi.event_date +``` + +**Indirectly Modified:** +- `sushi.top_waiters` + +""" + assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" + assert prod_plan_preview_checks_runs[2]["output"]["summary"] == expected_prod_plan_summary + + assert "SQLMesh - Prod Environment Synced" not in controller._check_run_mapping + + assert "SQLMesh - Has Required Approval" in controller._check_run_mapping + approval_checks_runs = controller._check_run_mapping[ + "SQLMesh - Has Required Approval" + ].all_kwargs + assert len(approval_checks_runs) == 3 + assert GithubCheckStatus(approval_checks_runs[0]["status"]).is_queued + assert GithubCheckStatus(approval_checks_runs[1]["status"]).is_in_progress + assert GithubCheckStatus(approval_checks_runs[2]["status"]).is_completed + assert GithubCheckConclusion(approval_checks_runs[2]["conclusion"]).is_success + assert ( + approval_checks_runs[2]["output"]["title"] + == "Obtained approval from required approvers: test_github" + ) + assert ( + approval_checks_runs[2]["output"]["summary"] + == """**List of possible required approvers:** +- `test_github` +""" + ) + + assert len(get_environment_objects(controller, "hello_world_2")) == 2 + assert get_num_days_loaded(controller, "hello_world_2", "waiter_revenue_by_day") == 7 + assert "new_col" in get_columns(controller, "hello_world_2", "waiter_revenue_by_day") + assert "new_col" not in get_columns(controller, None, "waiter_revenue_by_day") + + assert not mock_pull_request.merge.called + + assert len(created_comments) == 1 + assert ( + created_comments[0].body + == """:robot: **SQLMesh Bot Info** :robot: +- :eyes: To **review** this PR's changes, use virtual data environment: + - `hello_world_2`""" + ) + + with open(github_output_file, "r", encoding="utf-8") as f: + output = f.read() + assert ( + output + == "run_unit_tests=success\nhas_required_approval=success\ncreated_pr_environment=true\npr_environment_name=hello_world_2\npr_environment_synced=success\nprod_plan_preview=success\n" + ) From fc8bc925feecad006b5c83eb8ab4101f301a7584 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 22 May 2025 11:39:33 +0200 Subject: [PATCH 0228/1056] fix(vscode): python fix for cll (#4500) --- sqlmesh/lsp/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 29cafdd639..59575cda47 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -119,9 +119,11 @@ def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]: # /api/lineage/{model}/{column} model_name = urllib.parse.unquote(path_parts[2]) column = urllib.parse.unquote(path_parts[3]) - logging.info(f"Column lineage request: {model_name} {column}") + models_only = False + if hasattr(request, "params"): + models_only = bool(getattr(request.params, "models_only", False)) column_lineage_response = column_lineage( - model_name, column, False, self.lsp_context.context + model_name, column, models_only, self.lsp_context.context ) return ApiResponseGetColumnLineage(data=column_lineage_response).model_dump( mode="json" From 50802b37060d44b23995d876df4f1672963d703a Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 22 May 2025 12:06:11 +0200 Subject: [PATCH 0229/1056] fix(vscode): fix lineage graphs (#4501) --- vscode/react/src/components/graph/ModelColumns.tsx | 2 +- vscode/react/src/components/graph/ModelNode.tsx | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/vscode/react/src/components/graph/ModelColumns.tsx b/vscode/react/src/components/graph/ModelColumns.tsx index 16969e20f3..694a7dd9b4 100644 --- a/vscode/react/src/components/graph/ModelColumns.tsx +++ b/vscode/react/src/components/graph/ModelColumns.tsx @@ -402,7 +402,7 @@ function ModelColumn({ refetch: getColumnLineage, isFetching, isError, - } = useApiColumnLineage(nodeId, column.name) + } = useApiColumnLineage(nodeId, column.name, { models_only: true }) useEffect(() => { if (isNil(selectManually)) return diff --git a/vscode/react/src/components/graph/ModelNode.tsx b/vscode/react/src/components/graph/ModelNode.tsx index fa0333de50..755c84be61 100644 --- a/vscode/react/src/components/graph/ModelNode.tsx +++ b/vscode/react/src/components/graph/ModelNode.tsx @@ -53,9 +53,6 @@ export default function ModelNode({ const modelsArray = Object.values(models) const decodedId = decodeURIComponent(id) const model = modelsArray.find((m: Model) => m.fqn === decodedId) - if (!model) { - throw new Error(`Model not found: ${id}`) - } const modelColumns = model?.columns ?? [] Object.keys(lineage[decodedId]?.columns ?? {}).forEach((column: string) => { From a1c48404b8ef476e72e95deab0d8d3b7bbaadbcc Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 22 May 2025 13:57:17 +0300 Subject: [PATCH 0230/1056] Chore!: bump sqlglot to v26.19.0 (#4504) --- pyproject.toml | 2 +- tests/core/test_model.py | 9 ++++++--- tests/lsp/test_completions.py | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 781593fdbe..25e69683fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.17.1", + "sqlglot[rs]~=26.19.0", "tenacity", "time-machine", "json-stream" diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 2d796b0322..a6607a74ab 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -5067,7 +5067,8 @@ def test_when_matched(): unique_key ("purchase_order_id"), when_matched ( WHEN MATCHED AND __MERGE_SOURCE__._operation = 1 THEN DELETE - WHEN MATCHED AND __MERGE_SOURCE__._operation <> 1 THEN UPDATE SET __MERGE_TARGET__.purchase_order_id = 1 + WHEN MATCHED AND __MERGE_SOURCE__._operation <> 1 THEN UPDATE SET + __MERGE_TARGET__.purchase_order_id = 1 ), batch_concurrency 1, forward_only FALSE, @@ -5118,7 +5119,8 @@ def fingerprint_merge( kind INCREMENTAL_BY_UNIQUE_KEY ( unique_key ("purchase_order_id"), when_matched ( - WHEN MATCHED AND __MERGE_SOURCE__.salary <> __MERGE_TARGET__.salary THEN UPDATE SET ARRAY('target.update_datetime = source.update_datetime', 'target.salary = source.salary') + WHEN MATCHED AND __MERGE_SOURCE__.salary <> __MERGE_TARGET__.salary THEN UPDATE SET + ARRAY('target.update_datetime = source.update_datetime', 'target.salary = source.salary') ), batch_concurrency 1, forward_only FALSE, @@ -7495,7 +7497,8 @@ def test_merge_filter(): unique_key ("purchase_order_id"), when_matched ( WHEN MATCHED AND {MERGE_SOURCE_ALIAS}._operation = 1 THEN DELETE - WHEN MATCHED AND {MERGE_SOURCE_ALIAS}._operation <> 1 THEN UPDATE SET {MERGE_TARGET_ALIAS}.purchase_order_id = 1 + WHEN MATCHED AND {MERGE_SOURCE_ALIAS}._operation <> 1 THEN UPDATE SET + {MERGE_TARGET_ALIAS}.purchase_order_id = 1 ), merge_filter ( {MERGE_SOURCE_ALIAS}.ds > ( diff --git a/tests/lsp/test_completions.py b/tests/lsp/test_completions.py index 7344cc8d29..2110c5f3af 100644 --- a/tests/lsp/test_completions.py +++ b/tests/lsp/test_completions.py @@ -11,13 +11,13 @@ @pytest.mark.fast def test_get_keywords_from_tokenizer(): - assert len(get_keywords_from_tokenizer()) > len(TOKENIZER_KEYWORDS) + assert len(get_keywords_from_tokenizer()) >= len(TOKENIZER_KEYWORDS) @pytest.mark.fast def test_get_sql_completions_no_context(): completions = get_sql_completions(None, None) - assert len(completions.keywords) > len(TOKENIZER_KEYWORDS) + assert len(completions.keywords) >= len(TOKENIZER_KEYWORDS) assert len(completions.models) == 0 From a58a59f28923f7b997629451f17ce57b82ac7b39 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 22 May 2025 15:14:00 +0300 Subject: [PATCH 0231/1056] Chore: add pyproject.toml in examples/custom_materializations (#4506) --- examples/custom_materializations/pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/custom_materializations/pyproject.toml diff --git a/examples/custom_materializations/pyproject.toml b/examples/custom_materializations/pyproject.toml new file mode 100644 index 0000000000..b680813c48 --- /dev/null +++ b/examples/custom_materializations/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools >= 61.0", "setuptools_scm"] +build-backend = "setuptools.build_meta" From ac25787cf4cfffb8fb3654392c6c321ef07a7941 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 22 May 2025 09:10:23 -0700 Subject: [PATCH 0232/1056] chore: make check status message clearer (#4507) --- sqlmesh/integrations/github/cicd/command.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index 86bb642573..9ccb8f563d 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -55,7 +55,7 @@ def check_required_approvers(ctx: click.Context) -> None: """Checks if a required approver has provided approval on the PR.""" if not _check_required_approvers(ctx.obj["github"]): raise CICDBotError( - "Required approver has not approved the PR. See check status for more information." + "Required approver has not approved the PR. See Pull Requests Checks for more information." ) @@ -105,7 +105,7 @@ def _run_linter(controller: GithubController) -> bool: def run_tests(ctx: click.Context) -> None: """Runs the unit tests""" if not _run_tests(ctx.obj["github"]): - raise CICDBotError("Failed to run tests. See check status for more information.") + raise CICDBotError("Failed to run tests. See Pull Requests Checks for more information.") def _update_pr_environment(controller: GithubController) -> bool: @@ -132,7 +132,7 @@ def update_pr_environment(ctx: click.Context) -> None: """Creates or updates the PR environments""" if not _update_pr_environment(ctx.obj["github"]): raise CICDBotError( - "Failed to update PR environment. See check status for more information." + "Failed to update PR environment. See Pull Requests Checks for more information." ) @@ -164,7 +164,7 @@ def gen_prod_plan(ctx: click.Context) -> None: controller.update_prod_plan_preview_check(status=GithubCheckStatus.IN_PROGRESS) if not _gen_prod_plan(controller): raise CICDBotError( - "Failed to generate production plan. See check status for more information." + "Failed to generate production plan. See Pull Requests Checks for more information." ) @@ -203,7 +203,9 @@ def _deploy_production(controller: GithubController) -> bool: def deploy_production(ctx: click.Context) -> None: """Deploys the production environment""" if not _deploy_production(ctx.obj["github"]): - raise CICDBotError("Failed to deploy to production. See check status for more information.") + raise CICDBotError( + "Failed to deploy to production. See Pull Requests Checks for more information." + ) def _run_all(controller: GithubController) -> None: @@ -256,7 +258,9 @@ def _run_all(controller: GithubController) -> None: skip_reason="Linter or Unit Test(s) failed so skipping deploying to production", ) - raise CICDBotError("Linter or Unit Test(s) failed. See check status for more information.") + raise CICDBotError( + "Linter or Unit Test(s) failed. See Pull Requests Checks for more information." + ) pr_environment_updated = _update_pr_environment(controller) prod_plan_generated = False @@ -295,7 +299,7 @@ def _run_all(controller: GithubController) -> None: or (has_required_approval and controller.pr_targets_prod_branch and not deployed_to_prod) ): raise CICDBotError( - "A step of the run-all check failed. See check status for more information." + "A step of the run-all check failed. See Pull Requests Checks for more information." ) From a392953f68fafbdf1e32a1bd2eb3bb9c99205a16 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 22 May 2025 20:21:33 +0300 Subject: [PATCH 0233/1056] Fix: prompt if there are any uncategorized snapshots in a plan (#4498) --- sqlmesh/core/context.py | 7 ++++-- tests/core/test_context.py | 47 +++++++++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index e2e3a8a5cc..f53e9843c4 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1269,8 +1269,11 @@ def plan( skip_linter=skip_linter, ) - if no_auto_categorization: + plan = plan_builder.build() + + if no_auto_categorization or plan.uncategorized: # Prompts are required if the auto categorization is disabled + # or if there are any uncategorized snapshots in the plan no_prompts = False self.console.plan( @@ -1281,7 +1284,7 @@ def plan( no_prompts=no_prompts if no_prompts is not None else self.config.plan.no_prompts, ) - return plan_builder.build() + return plan @python_api_analytics def plan_builder( diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 7664594c87..d55c5c4285 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -14,17 +14,21 @@ from sqlglot import ParseError, exp, parse_one, Dialect from sqlglot.errors import SchemaError -from sqlmesh.core.config.gateway import GatewayConfig import sqlmesh.core.constants +from sqlmesh.cli.example_project import init_example_project from sqlmesh.core import dialect as d, constants as c from sqlmesh.core.config import ( + load_configs, + AutoCategorizationMode, + CategorizerConfig, Config, DuckDBConnectionConfig, EnvironmentSuffixTarget, + GatewayConfig, + LinterConfig, ModelDefaultsConfig, + PlanConfig, SnowflakeConnectionConfig, - LinterConfig, - load_configs, ) from sqlmesh.core.context import Context from sqlmesh.core.console import create_console, get_console @@ -2071,3 +2075,40 @@ def test_audit(): context.plan(no_prompts=True, auto_apply=True) assert context.audit(models=["dummy"], start="2020-01-01", end="2020-01-01") is True + + +def test_prompt_if_uncategorized_snapshot(mocker: MockerFixture, tmp_path: Path) -> None: + init_example_project(tmp_path, dialect="duckdb") + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + plan=PlanConfig( + auto_categorize_changes=CategorizerConfig( + external=AutoCategorizationMode.OFF, + python=AutoCategorizationMode.OFF, + sql=AutoCategorizationMode.OFF, + seed=AutoCategorizationMode.OFF, + ), + ), + ) + context = Context(paths=tmp_path, config=config) + context.plan(no_prompts=True, auto_apply=True) + + incremental_model = context.get_model("sqlmesh_example.incremental_model") + incremental_model_query = incremental_model.render_query() + new_incremental_model_query = t.cast(exp.Select, incremental_model_query).select("1 AS z") + context.upsert_model("sqlmesh_example.incremental_model", query=new_incremental_model_query) + + mock_console = mocker.Mock() + spy_plan = mocker.spy(mock_console, "plan") + context.console = mock_console + + context.plan() + + calls = spy_plan.mock_calls + assert len(calls) == 1 + + # Show that the presence of uncategorized snapshots forces no_prompts to + # False instead of respecting the default plan config value, which is True + assert calls[0].kwargs["no_prompts"] == False + assert context.config.plan.no_prompts == True From edf91e16b9f0803b2206e4722fc910c7de4556c4 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 22 May 2025 19:49:04 +0200 Subject: [PATCH 0234/1056] fix: explictely require packaging (#4508) --- .circleci/config.yml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f3c0354c84..f8b00cf07c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -97,7 +97,7 @@ workflows: mapping: | web/client/.* client true (sqlmesh|tests|examples|web/server)/.* python true - pytest.ini|setup.cfg|setup.py python true + pytest.ini|setup.cfg|setup.py|pyproject.toml python true \.circleci/.*|Makefile|\.pre-commit-config\.yaml common true vscode/extensions/.* vscode true tag: "3.9" diff --git a/pyproject.toml b/pyproject.toml index 25e69683fc..c84f6b2a08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "importlib-metadata; python_version<'3.12'", "ipywidgets", "jinja2", + "packaging", "pandas", "pydantic>=2.0.0", "requests", From 1b9c04b7e1162a9aa3fe8ffd9dfdf570746e5447 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 22 May 2025 19:53:18 +0200 Subject: [PATCH 0235/1056] feat(vscode): improve readme (#4489) --- vscode/extension/README.md | 101 +++++++++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 16 deletions(-) diff --git a/vscode/extension/README.md b/vscode/extension/README.md index 609b0f7af7..64f6c3e130 100644 --- a/vscode/extension/README.md +++ b/vscode/extension/README.md @@ -1,27 +1,96 @@ -# SQLMesh VSCode Extension +# SQLMesh Visual Studio Code Extension -## Functionality +**Transform your data engineering workflow with intelligent SQL development and powerful lineage visualization.** -The following section outlines all the functionality in the Visual Studio Code extension. It is broken up into logical sections. +## Overview -### Authentication +The SQLMesh VSCode extension brings the power of SQLMesh directly into your editor with intelligent code assistance, interactive lineage visualization, and seamless integration with Tobiko Cloud. -The extension allows you to manage your tcloud session within Visual Studio Code. +## 🚀 Quick Start -- Sign in -- Sign out -- Sign in with a specified flow +1. **Install the extension** from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=tobikodata.sqlmesh) +2. **Set up your Python environment** with SQLMesh +3. **Configure your Python interpreter** in VSCode +4. **Start building data pipelines!** -You can see your user account in the bottom left. +For a more detailed guide, see the [VSCode Extension Guide](https://sqlmesh.readthedocs.io/en/stable/guides/vscode/). -### Formatting +## ✨ Features -The extension allows you to: +### 🔗 Interactive Lineage Visualization +- **Real-time lineage graphs** showing data flow between models +- **Interactive exploration** with clickable nodes +- **Model dependency tracking** across your entire project -- Format individual documents explicitly -- Format individual documents on save -- Format a whole project +### 🧠 Intelligent Code Assistance +- **Smart auto-completion** for model names and SQLMesh keywords +- **Hover tooltips** with model descriptions and metadata +- **Go-to-definition** navigation for model references +- **Real-time error detection** with inline diagnostics -### Linting +### 🎨 Code Formatting & Quality +- **Automatic formatting** for SQLMesh models +- **Integrated linter** with built-in and custom rules +- **Format on save** support +- **Project-wide formatting** commands -The extension allows you to see linting errors and warnings inline. +### ☁️ Tobiko Cloud Integration +- **Seamless authentication** within VSCode +- **Cloud project management** +- **Secure credential handling** + +## 📖 Usage + +Here's an overview of the extension's features: + +### Viewing Model Lineage +1. Open any SQLMesh model file +2. Navigate to the "Lineage" tab in the panel +3. Explore your data pipeline visually + +### Using Auto-completion +- Start typing model names or SQLMesh keywords +- Press `Ctrl+Space` to trigger suggestions +- Navigate with arrow keys and press `Enter` to accept + +### Formatting Code +- **Single file**: Right-click → "Format Document" +- **Entire project**: Command Palette → "Format SQLMesh project" +- **Auto-format**: Enable "Format on Save" in VSCode settings + +### Managing Tobiko Cloud Authentication +- **Sign in**: Command Palette → "Sign in to Tobiko Cloud" +- **Sign out**: Command Palette → "Sign out of Tobiko Cloud" +- **View status**: Check the bottom-left status bar + +## 🐛 Troubleshooting + +If you encounter issues, please refer to the [VSCode Extension Guide](https://sqlmesh.readthedocs.io/en/stable/guides/vscode/) for troubleshooting steps. + +## 📚 Documentation + +- [Full SQLMesh Documentation](https://sqlmesh.readthedocs.io/) +- [VSCode Extension Guide](https://sqlmesh.readthedocs.io/en/stable/guides/vscode/) +- [Tobiko Cloud Documentation](https://docs.tobiko.cloud/) + +## 🤝 Contributing + +We welcome contributions! Please: + +1. [Report bugs](https://github.com/tobikodata/sqlmesh/issues) you encounter +2. [Request features](https://github.com/tobikodata/sqlmesh/issues) you'd like to see +3. Share feedback on your experience + +## 📄 License + +This extension is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for details. + +## 🔗 Links + +- [SQLMesh GitHub Repository](https://github.com/tobikodata/sqlmesh) +- [Tobiko Data Website](https://tobikodata.com) +- [Extension Marketplace Page](https://marketplace.visualstudio.com/items?itemName=tobikodata.sqlmesh) + +--- + +**Happy data engineering!** 🚀 \ No newline at end of file From 00a47a06e0f74b85929e99f11af73eefb67522ee Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 22 May 2025 11:29:35 -0700 Subject: [PATCH 0236/1056] Fix: Improve the field location reporting in config errors (#4511) --- sqlmesh/utils/pydantic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlmesh/utils/pydantic.py b/sqlmesh/utils/pydantic.py index cfe3ac042f..8cc1db617e 100644 --- a/sqlmesh/utils/pydantic.py +++ b/sqlmesh/utils/pydantic.py @@ -221,7 +221,8 @@ def _formatted_validation_errors(error: pydantic.ValidationError) -> t.List[str] for e in error.errors(): msg = e["msg"] loc: t.Optional[t.Tuple] = e.get("loc") - result.append(f"Invalid field '{loc[0]}':\n {msg}" if loc else msg) + loc_str = ".".join(loc) if loc else None + result.append(f"Invalid field '{loc_str}':\n {msg}" if loc_str else msg) return result From d08ece1824ad443742b08ff587b99f9664fc4d10 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 22 May 2025 21:31:42 +0300 Subject: [PATCH 0237/1056] Chore!: configure logging before loading config to log errors properly (#4509) Co-authored-by: Iaroslav Zeigerman --- sqlmesh/__init__.py | 21 ++++++++++++++++----- sqlmesh/cli/main.py | 10 ++++++---- sqlmesh/core/config/connection.py | 12 ++++++++++-- sqlmesh/core/context.py | 2 +- sqlmesh/magics.py | 16 +++++++++++----- tests/core/test_connection_config.py | 10 ++++++++-- 6 files changed, 52 insertions(+), 19 deletions(-) diff --git a/sqlmesh/__init__.py b/sqlmesh/__init__.py index e48ffbef5b..63f109d489 100644 --- a/sqlmesh/__init__.py +++ b/sqlmesh/__init__.py @@ -166,11 +166,24 @@ def format(self, record: logging.LogRecord) -> str: return formatter.format(record) +def remove_excess_logs( + log_file_dir: t.Optional[t.Union[str, Path]] = None, + log_limit: int = c.DEFAULT_LOG_LIMIT, +) -> None: + if log_limit <= 0: + return + + log_file_dir = log_file_dir or c.DEFAULT_LOG_FILE_DIR + log_path_prefix = Path(log_file_dir) / LOG_FILENAME_PREFIX + + for path in list(sorted(glob.glob(f"{log_path_prefix}*.log"), reverse=True))[log_limit:]: + os.remove(path) + + def configure_logging( force_debug: bool = False, write_to_stdout: bool = False, write_to_file: bool = True, - log_limit: int = c.DEFAULT_LOG_LIMIT, log_file_dir: t.Optional[t.Union[str, Path]] = None, ignore_warnings: bool = False, ) -> None: @@ -192,20 +205,18 @@ def configure_logging( log_file_dir = log_file_dir or c.DEFAULT_LOG_FILE_DIR log_path_prefix = Path(log_file_dir) / LOG_FILENAME_PREFIX + if write_to_file: os.makedirs(str(log_file_dir), exist_ok=True) filename = f"{log_path_prefix}{datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}.log" file_handler = logging.FileHandler(filename, mode="w", encoding="utf-8") + # the log files should always log at least info so that users will always have # minimal info for debugging even if they specify "ignore_warnings" file_handler.setLevel(level) file_handler.setFormatter(logging.Formatter(LOG_FORMAT)) logger.addHandler(file_handler) - if log_limit > 0: - for path in list(sorted(glob.glob(f"{log_path_prefix}*.log"), reverse=True))[log_limit:]: - os.remove(path) - if debug: import faulthandler diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 5b5fcf1541..2ef9e25426 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -7,7 +7,7 @@ import click -from sqlmesh import configure_logging +from sqlmesh import configure_logging, remove_excess_logs from sqlmesh.cli import error_handler from sqlmesh.cli import options as opt from sqlmesh.cli.example_project import ProjectTemplate, init_example_project @@ -100,17 +100,19 @@ def cli( if ctx.invoked_subcommand in SKIP_LOAD_COMMANDS: load = False - configs = load_configs(config, Context.CONFIG_TYPE, paths) - log_limit = list(configs.values())[0].log_limit configure_logging( debug, log_to_stdout, - log_limit=log_limit, log_file_dir=log_file_dir, ignore_warnings=ignore_warnings, ) configure_console(ignore_warnings=ignore_warnings) + configs = load_configs(config, Context.CONFIG_TYPE, paths) + log_limit = list(configs.values())[0].log_limit + + remove_excess_logs(log_file_dir, log_limit) + try: context = Context( paths=paths, diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 1e1afc0500..8c75516058 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -26,7 +26,7 @@ ) from sqlmesh.core.engine_adapter.shared import CatalogSupport from sqlmesh.core.engine_adapter import EngineAdapter -from sqlmesh.utils import str_to_bool +from sqlmesh.utils import debug_mode_enabled, str_to_bool from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.pydantic import ( ValidationInfo, @@ -67,8 +67,16 @@ def validate(cls: t.Any, data: t.Any) -> t.Any: try: importlib.import_module(import_name) except ImportError: + if debug_mode_enabled(): + raise + + logger.exception("Failed to import the engine library") + raise ConfigError( - f"Failed to import the '{engine_type}' engine library. Please run `pip install \"sqlmesh[{extra_name}]\"`." + f"Failed to import the '{engine_type}' engine library. This may be due to a missing " + "or incompatible installation. Please ensure the required dependency is installed by " + f'running: `pip install "sqlmesh[{extra_name}]"`. For more details, check the logs ' + "in the 'logs/' folder, or rerun the command with the '--debug' flag." ) return data diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index f53e9843c4..b5c9f7b65d 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -35,7 +35,6 @@ import abc import collections -from itertools import chain import logging import sys import time @@ -44,6 +43,7 @@ import unittest.result from functools import cached_property from io import StringIO +from itertools import chain from pathlib import Path from shutil import rmtree from types import MappingProxyType diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index c79f6bb4f9..6929cc8158 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -129,18 +129,23 @@ def _shell(self) -> t.Any: @line_magic def context(self, line: str) -> None: """Sets the context in the user namespace.""" - from sqlmesh import configure_logging + from sqlmesh import configure_logging, remove_excess_logs args = parse_argstring(self.context, line) - configs = load_configs(args.config, Context.CONFIG_TYPE, args.paths) - log_limit = list(configs.values())[0].log_limit + log_file_dir = args.log_file_dir + configure_logging( args.debug, - log_limit=log_limit, - log_file_dir=args.log_file_dir, + log_file_dir=log_file_dir, ignore_warnings=args.ignore_warnings, ) configure_console(ignore_warnings=args.ignore_warnings) + + configs = load_configs(args.config, Context.CONFIG_TYPE, args.paths) + log_limit = list(configs.values())[0].log_limit + + remove_excess_logs(log_file_dir, log_limit) + try: context = Context(paths=args.paths, config=configs, gateway=args.gateway) self._shell.user_ns["context"] = context @@ -148,6 +153,7 @@ def context(self, line: str) -> None: if args.debug: logger.exception("Failed to initialize SQLMesh context") raise + context.console.log_success(f"SQLMesh project context set to: {', '.join(args.paths)}") @magic_arguments() diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 8680e09496..890a9b03c6 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -1016,7 +1016,10 @@ def test_engine_import_validator(): with pytest.raises( ConfigError, match=re.escape( - """Failed to import the 'bigquery' engine library. Please run `pip install "sqlmesh[bigquery]"`.""" + "Failed to import the 'bigquery' engine library. This may be due to a missing " + "or incompatible installation. Please ensure the required dependency is installed by " + 'running: `pip install "sqlmesh[bigquery]"`. For more details, check the logs ' + "in the 'logs/' folder, or rerun the command with the '--debug' flag." ), ): @@ -1028,7 +1031,10 @@ class TestConfigA(PydanticModel): with pytest.raises( ConfigError, match=re.escape( - """Failed to import the 'bigquery' engine library. Please run `pip install "sqlmesh[bigquery_extra]"`.""" + "Failed to import the 'bigquery' engine library. This may be due to a missing " + "or incompatible installation. Please ensure the required dependency is installed by " + 'running: `pip install "sqlmesh[bigquery_extra]"`. For more details, check the logs ' + "in the 'logs/' folder, or rerun the command with the '--debug' flag." ), ): From 2343309b73c555952bde89a0031d4e3c7446265c Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 23 May 2025 06:38:22 +1200 Subject: [PATCH 0238/1056] Chore: Test retry flakiness and migrate pytest config to pyproject.toml (#4496) --- Makefile | 2 +- pyproject.toml | 47 +++++++++++++++++++ pytest.ini | 45 ------------------ .../engine_adapter/integration/conftest.py | 30 ++++++------ .../integration/docker/compose.trino.yaml | 2 +- 5 files changed, 63 insertions(+), 63 deletions(-) delete mode 100644 pytest.ini diff --git a/Makefile b/Makefile index 7c2aa11dfa..c99936eff7 100644 --- a/Makefile +++ b/Makefile @@ -170,7 +170,7 @@ clickhouse-cloud-test: guard-CLICKHOUSE_CLOUD_HOST guard-CLICKHOUSE_CLOUD_USERNA pytest -n 1 -m "clickhouse_cloud" --retries 3 --junitxml=test-results/junit-clickhouse-cloud.xml athena-test: guard-AWS_ACCESS_KEY_ID guard-AWS_SECRET_ACCESS_KEY guard-ATHENA_S3_WAREHOUSE_LOCATION engine-athena-install - pytest -n auto -m "athena" --retries 3 --retry-delay 10 --junitxml=test-results/junit-athena.xml + pytest -n auto -m "athena" --retries 3 --junitxml=test-results/junit-athena.xml vscode_settings: mkdir -p .vscode diff --git a/pyproject.toml b/pyproject.toml index c84f6b2a08..e2538056ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -215,6 +215,53 @@ module = [ ] ignore_missing_imports = true +[tool.pytest.ini_options] +markers = [ + # Test Type Markers + # Tests are ordered from fastest to slowest + "fast: fast tests (automatically applied if no type markers)", + "slow: slow tests that typically involve interacting with a local DB (like DuckDB)", + "docker: test that involves interacting with a Docker container", + "remote: test that involves interacting with a remote DB", + "cicdonly: test that only runs on CI/CD", + "isolated: tests that need to run sequentially usually because they use fork", + + # Test Domain Markers + # default: core functionality + "cli: test for CLI", + "dbt: test for dbt adapter", + "github: test for Github CI/CD bot", + "jupyter: tests for Jupyter integration", + "web: tests for web UI", + + # Engine Adapters + "engine: test all engine adapters", + "athena: test for Athena", + "bigquery: test for BigQuery", + "clickhouse: test for Clickhouse (standalone mode / cluster mode)", + "clickhouse_cloud: test for Clickhouse (cloud mode)", + "databricks: test for Databricks", + "duckdb: test for DuckDB", + "motherduck: test for MotherDuck", + "mssql: test for MSSQL", + "mysql: test for MySQL", + "postgres: test for Postgres", + "redshift: test for Redshift", + "snowflake: test for Snowflake", + "spark: test for Spark", + "trino: test for Trino (all connectors)", + "risingwave: test for Risingwave" +] +addopts = "-n 0 --dist=loadgroup" +asyncio_default_fixture_loop_scope = "session" +log_cli = false # Set this to true to enable logging during tests +log_cli_format = "%(asctime)s.%(msecs)03d %(filename)s:%(lineno)d %(levelname)s %(message)s" +log_cli_level = "INFO" +filterwarnings = [ + "ignore:The localize method is no longer necessary, as this time zone supports the fold attribute" +] +retry_delay = 10 + [tool.ruff.lint] select = [ "F401", diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index d81359961d..0000000000 --- a/pytest.ini +++ /dev/null @@ -1,45 +0,0 @@ -[pytest] -markers = - # Test Type Markers - # Tests are ordered from fastest to slowest - fast: fast tests (automatically applied if no type markers) - slow: slow tests that typically involve interacting with a local DB (like DuckDB) - docker: test that involves interacting with a Docker container - remote: test that involves interacting with a remote DB - cicdonly: test that only runs on CI/CD - isolated: tests that need to run sequentially usually because they use fork - - # Test Domain Markers - # default: core functionality - cli: test for CLI - dbt: test for dbt adapter - github: test for Github CI/CD bot - jupyter: tests for Jupyter integration - web: tests for web UI - # Engine Adapters - engine: test all engine adapters - athena: test for Athena - bigquery: test for BigQuery - clickhouse: test for Clickhouse (standalone mode / cluster mode) - clickhouse_cloud: test for Clickhouse (cloud mode) - databricks: test for Databricks - duckdb: test for DuckDB - motherduck: test for MotherDuck - mssql: test for MSSQL - mysql: test for MySQL - postgres: test for Postgres - redshift: test for Redshift - snowflake: test for Snowflake - spark: test for Spark - trino: test for Trino (all connectors) - risingwave: test for Risingwave -addopts = -n 0 --dist=loadgroup - -asyncio_default_fixture_loop_scope = session - -# Set this to True to enable logging during tests -log_cli = False -log_cli_format = %(asctime)s.%(msecs)03d %(filename)s:%(lineno)d %(levelname)s %(message)s -log_cli_level = INFO -filterwarnings = - ignore:The localize method is no longer necessary, as this time zone supports the fold attribute diff --git a/tests/core/engine_adapter/integration/conftest.py b/tests/core/engine_adapter/integration/conftest.py index 35724af15a..f072ca77f5 100644 --- a/tests/core/engine_adapter/integration/conftest.py +++ b/tests/core/engine_adapter/integration/conftest.py @@ -106,25 +106,23 @@ def _create( is_remote=is_remote, ) - skip = False try: ctx.init() - except Exception: - # We need to catch this exception because if there is an error during setup, pytest-retry aborts immediately - # instead of retrying + except: + # pytest-retry doesnt work if there are errors in fixture setup (ref: https://github.com/str0zzapreti/pytest-retry/issues/33 ) + # what we can do is log the exception and return a partially-initialized context to the test, which should + # throw an exception when it tries to access something that didnt init properly and thus trigger pytest-retry to retry logger.exception("Context init failed") - skip = True - - if not skip: - with ctx.engine_adapter.session({}): - yield ctx - - try: - ctx.cleanup() - except Exception: - # We need to catch this exception because if there is an error during teardown, pytest-retry aborts immediately - # instead of retrying - logger.exception("Context cleanup failed") + + with ctx.engine_adapter.session({}): + yield ctx + + try: + ctx.cleanup() + except: + # We need to catch this exception because if there is an error during teardown, pytest-retry aborts immediately + # instead of retrying + logger.exception("Context cleanup failed") return _create diff --git a/tests/core/engine_adapter/integration/docker/compose.trino.yaml b/tests/core/engine_adapter/integration/docker/compose.trino.yaml index a3d904b8d6..f5ae25fa4e 100644 --- a/tests/core/engine_adapter/integration/docker/compose.trino.yaml +++ b/tests/core/engine_adapter/integration/docker/compose.trino.yaml @@ -28,7 +28,7 @@ services: # Trino Stack trino: - image: 'trinodb/trino:465' + image: 'trinodb/trino:475' ports: - '8080:8080' volumes: From ebf8f474624f2b984fc863b62032d3c385501ef4 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 22 May 2025 12:13:03 -0700 Subject: [PATCH 0239/1056] Fix: Relax the error regex in state sync tests (#4512) --- tests/core/state_sync/test_state_sync.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index 1129420b9f..f3937f48c1 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -949,7 +949,7 @@ def test_promote_snapshots_no_gaps(state_sync: EngineAdapterStateSync, make_snap state_sync.add_interval(new_snapshot_missing_interval, "2022-01-01", "2022-01-02") with pytest.raises( SQLMeshError, - match=r'Detected missing intervals for model "a", interrupting your current plan. Please re-apply your plan to resolve this error.', + match=r".*Detected missing intervals for model .*, interrupting your current plan. Please re-apply your plan to resolve this error.*", ): promote_snapshots(state_sync, [new_snapshot_missing_interval], "prod", no_gaps=True) @@ -1061,7 +1061,7 @@ def test_start_date_gap(state_sync: EngineAdapterStateSync, make_snapshot: t.Cal state_sync.add_interval(snapshot, "2022-01-03", "2022-01-04") with pytest.raises( SQLMeshError, - match=r'Detected missing intervals for model "a", interrupting your current plan. Please re-apply your plan to resolve this error.', + match=r".*Detected missing intervals for model .*, interrupting your current plan. Please re-apply your plan to resolve this error.*", ): promote_snapshots(state_sync, [snapshot], "prod", no_gaps=True) From 410dcbf3652d87af6f8b42218f7c00612a1939da Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 22 May 2025 22:59:14 +0300 Subject: [PATCH 0240/1056] Chore: remove setup.py in favor of pyproject.toml for custom materializations example (#4514) --- examples/custom_materializations/pyproject.toml | 13 +++++++++++++ examples/custom_materializations/setup.py | 15 --------------- 2 files changed, 13 insertions(+), 15 deletions(-) delete mode 100644 examples/custom_materializations/setup.py diff --git a/examples/custom_materializations/pyproject.toml b/examples/custom_materializations/pyproject.toml index b680813c48..70eb2d7f9e 100644 --- a/examples/custom_materializations/pyproject.toml +++ b/examples/custom_materializations/pyproject.toml @@ -1,3 +1,16 @@ [build-system] requires = ["setuptools >= 61.0", "setuptools_scm"] build-backend = "setuptools.build_meta" + +[project] +name = "custom_materializations" +requires-python = ">=3.9" +version = "0.1.0" +dependencies = ["sqlmesh"] + +[project.entry-points."sqlmesh.materializations"] +custom_full_materialization = "custom_materializations.full:CustomFullMaterialization" +custom_full_with_custom_kind = "custom_materializations.custom_kind:CustomFullWithCustomKindMaterialization" + +[tool.setuptools.packages.find] +include = ["custom_materializations"] diff --git a/examples/custom_materializations/setup.py b/examples/custom_materializations/setup.py deleted file mode 100644 index b90200bed9..0000000000 --- a/examples/custom_materializations/setup.py +++ /dev/null @@ -1,15 +0,0 @@ -from setuptools import find_packages, setup - -setup( - name="custom_materializations", - packages=find_packages(include=["custom_materializations"]), - entry_points={ - "sqlmesh.materializations": [ - "custom_full_materialization = custom_materializations.full:CustomFullMaterialization", - "custom_full_with_custom_kind = custom_materializations.custom_kind:CustomFullWithCustomKindMaterialization", - ], - }, - install_requires=[ - "sqlmesh", - ], -) From 597492fb380ea0dbe7bc19b26dbbf26f613f9439 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 22 May 2025 23:53:21 +0300 Subject: [PATCH 0241/1056] Chore: detect invalid signal refs and raise early (#4515) --- sqlmesh/core/model/definition.py | 19 ++++++++++++------- tests/core/test_model.py | 25 ++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index c19cf5d72c..8a282636bb 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2488,22 +2488,27 @@ def _create_model( model.audit_definitions.update(audit_definitions) - from sqlmesh.core.audit.builtin import BUILT_IN_AUDITS + # Any macro referenced in audits or signals needs to be treated as metadata-only + statements.extend((audit.query, True) for audit in audit_definitions.values()) # Ensure that all audits referenced in the model are defined + from sqlmesh.core.audit.builtin import BUILT_IN_AUDITS + available_audits = BUILT_IN_AUDITS.keys() | model.audit_definitions.keys() - for referenced_audit, *_ in model.audits: + for referenced_audit, audit_args in model.audits: if referenced_audit not in available_audits: raise_config_error(f"Audit '{referenced_audit}' is undefined", location=path) - # Any macro referenced in audits or signals needs to be treated as metadata-only - statements.extend((audit.query, True) for audit in audit_definitions.values()) - for _, audit_args in model.audits: statements.extend( (audit_arg_expression, True) for audit_arg_expression in audit_args.values() ) - for _, kwargs in model.signals: + signal_definitions = signal_definitions or UniqueKeyDict("signals") + + for referenced_signal, kwargs in model.signals: + if referenced_signal and referenced_signal not in signal_definitions: + raise_config_error(f"Signal '{referenced_signal}' is undefined", location=path) + statements.extend((signal_kwarg, True) for signal_kwarg in kwargs.values()) python_env = make_python_env( @@ -2523,7 +2528,7 @@ def _create_model( env: t.Dict[str, t.Tuple[t.Any, t.Optional[bool]]] = {} for signal_name, _ in model.signals: - if signal_definitions and signal_name in signal_definitions: + if signal_name and signal_name in signal_definitions: func = signal_definitions[signal_name].func setattr(func, c.SQLMESH_METADATA, True) build_env(func, env=env, name=signal_name, path=module_path) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index a6607a74ab..fadbaa8b08 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -4919,6 +4919,10 @@ def test_signals(): model = load_sql_based_model(expressions) assert model.signals[0][1] == {"arg": exp.Literal.number(1)} + @signal() + def my_signal(batch): + return True + expressions = d.parse( """ MODEL ( @@ -4946,7 +4950,10 @@ def test_signals(): """ ) - model = load_sql_based_model(expressions) + model = load_sql_based_model( + expressions, + signal_definitions={"my_signal": signal.get_registry()["my_signal"]}, + ) assert model.signals == [ ( "my_signal", @@ -9837,6 +9844,22 @@ def test_invalid_audit_reference(): load_sql_based_model(expressions) +def test_invalid_signal_reference(): + sql = """ + MODEL ( + name test, + signals (s()) + ); + + SELECT + 1 AS id + """ + expressions = d.parse(sql) + + with pytest.raises(ConfigError, match="Signal 's' is undefined"): + load_sql_based_model(expressions) + + def test_scd_time_data_type_does_not_cause_diff_after_deserialization() -> None: for dialect in ( "athena", From 73e2ef4a8d811461b8777eedec96101a710b9790 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 22 May 2025 16:01:35 -0500 Subject: [PATCH 0242/1056] Fix: only warn for disabled restatements if snapshot in restatement subgraph (#4516) --- sqlmesh/core/plan/builder.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index eed05e04eb..0e8bc481f4 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -344,6 +344,15 @@ def _build_restatements( for s_id in dag: snapshot = self._context_diff.snapshots[s_id] + # Since we are traversing the graph in topological order and the largest interval range is pushed down + # the graph we just have to check our immediate parents in the graph and not the whole upstream graph. + restating_parents = [ + self._context_diff.snapshots[s] for s in snapshot.parents if s in restatements + ] + + if not restating_parents and snapshot.name not in restate_models: + continue + if not forward_only_preview_needed: if self._is_dev and not snapshot.is_paused: self._console.log_warning( @@ -362,15 +371,6 @@ def _build_restatements( logger.info("Skipping restatement for model '%s'", snapshot.name) continue - # Since we are traversing the graph in topological order and the largest interval range is pushed down - # the graph we just have to check our immediate parents in the graph and not the whole upstream graph. - restating_parents = [ - self._context_diff.snapshots[s] for s in snapshot.parents if s in restatements - ] - - if not restating_parents and snapshot.name not in restate_models: - continue - possible_intervals = { restatements[p.snapshot_id] for p in restating_parents if p.is_incremental } From 657cf0d9072b18306adacdf71e4b091be314f6a4 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 22 May 2025 15:30:38 -0700 Subject: [PATCH 0243/1056] Chore: Run audits for metadata snaphots after creating snapshots and their tables (#4517) --- sqlmesh/core/plan/evaluator.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 0400e6cbb8..0d2a25b4fa 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -116,6 +116,7 @@ def evaluate( after_promote_snapshots = all_names - before_promote_snapshots deployability_index_for_evaluation = DeployabilityIndex.all_deployable() + # Step 1: Run before_all environment statements before doing anything else execute_environment_statements( adapter=self.snapshot_evaluator.adapter, environment_statements=plan.environment_statements or [], @@ -128,15 +129,24 @@ def evaluate( execution_time=plan.execution_time, ) - self._run_audits_for_metadata_snapshots(plan, new_snapshots) - + # Step 2:Store new snapshot records in the state and create physical tables for them push_completion_status = self._push(plan, snapshots, deployability_index_for_creation) if push_completion_status.is_nothing_to_do: self.console.log_success( "" if plan.restatements else "\nSKIP: No physical layer updates to perform" ) + + # Step 3: Update the intervals for the new forward-only snapshots update_intervals_for_new_snapshots(plan.new_snapshots, self.state_sync) + + # Step 4: Run audits without evaluations for snapshots that capture audit metadata changes + self._run_audits_for_metadata_snapshots(plan, new_snapshots) + + # Step 5: Remove intervals for snapshots that need to be restated self._restate(plan, snapshots_by_name) + + # Step 6: Backfill missing intervals for snapshots that can be backfilled before updating + # the schema of production tables first_bf_completion_status = self._backfill( plan, snapshots_by_name, @@ -144,9 +154,15 @@ def evaluate( deployability_index_for_evaluation, circuit_breaker=circuit_breaker, ) + + # Step 7: Update the target environment record in the state and migrate table schemas for forward-only + # snapshots if deploying to production promotion_result = self._promote( plan, snapshots, before_promote_snapshots, deployability_index_for_creation ) + + # Step 8: Backfill missing intervals for snapshots that can be backfilled only after updating + # the schema of production tables second_bf_completion_status = self._backfill( plan, snapshots_by_name, @@ -154,15 +170,20 @@ def evaluate( deployability_index_for_evaluation, circuit_breaker=circuit_breaker, ) + if ( first_bf_completion_status.is_nothing_to_do and second_bf_completion_status.is_nothing_to_do ): self.console.log_success("SKIP: No model batches to execute") + + # Step 9: Update environment views to point at new physical tables and finalize the environment + # record in the state self._update_views( plan, snapshots, promotion_result, deployability_index_for_evaluation ) + # Step 10: Run after_all environment statements execute_environment_statements( adapter=self.snapshot_evaluator.adapter, environment_statements=plan.environment_statements or [], From 18911b3421d0cec071f67525e678ab3c808131b3 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 23 May 2025 17:40:27 +1200 Subject: [PATCH 0244/1056] Feat: Allow supplying complex connection config fields as JSON strings (#4519) --- sqlmesh/core/config/connection.py | 38 ++++++++++++++++++++++++++++ sqlmesh/utils/pydantic.py | 18 +++++++++++++ tests/core/test_config.py | 34 +++++++++++++++++++++++++ tests/core/test_connection_config.py | 36 ++++++++++++++++++++++++++ tests/utils/test_pydantic.py | 27 +++++++++++++++++++- 5 files changed, 152 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 8c75516058..1859794599 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -13,6 +13,7 @@ import pydantic from pydantic import Field +from pydantic_core import from_json from packaging import version from sqlglot import exp from sqlglot.helper import subclasses @@ -33,6 +34,7 @@ field_validator, model_validator, validation_error_message, + get_concrete_types_from_typehint, ) from sqlmesh.utils.aws import validate_s3_uri @@ -177,6 +179,42 @@ def get_catalog(self) -> t.Optional[str]: return self.db return None + @model_validator(mode="before") + @classmethod + def _expand_json_strings_to_concrete_types(cls, data: t.Any) -> t.Any: + """ + There are situations where a connection config class has a field that is some kind of complex type + (eg a list of strings or a dict) but the value is being supplied from a source such as an environment variable + + When this happens, the value is supplied as a string rather than a Python object. We need some way + of turning this string into the corresponding Python list or dict. + + Rather than doing this piecemeal on every config subclass, this provides a generic implementatation + to identify fields that may be be supplied as JSON strings and handle them transparently + """ + if data and isinstance(data, dict): + for maybe_json_field_name in cls._get_list_and_dict_field_names(): + if (value := data.get(maybe_json_field_name)) and isinstance(value, str): + # crude JSON check as we dont want to try and parse every string we get + value = value.strip() + if value.startswith("{") or value.startswith("["): + data[maybe_json_field_name] = from_json(value) + + return data + + @classmethod + def _get_list_and_dict_field_names(cls) -> t.Set[str]: + field_names = set() + for name, field in cls.model_fields.items(): + if field.annotation: + field_types = get_concrete_types_from_typehint(field.annotation) + + # check if the field type is something that could concievably be supplied as a json string + if any(ft is t for t in (list, tuple, set, dict) for ft in field_types): + field_names.add(name) + + return field_names + class DuckDBAttachOptions(BaseConfig): type: str diff --git a/sqlmesh/utils/pydantic.py b/sqlmesh/utils/pydantic.py index 8cc1db617e..3de15773a3 100644 --- a/sqlmesh/utils/pydantic.py +++ b/sqlmesh/utils/pydantic.py @@ -306,6 +306,24 @@ def cron_validator(v: t.Any) -> str: return v +def get_concrete_types_from_typehint(typehint: type[t.Any]) -> set[type[t.Any]]: + concrete_types = set() + unpacked = t.get_origin(typehint) + if unpacked is None: + if type(typehint) == type(type): + return {typehint} + elif unpacked is t.Union: + for item in t.get_args(typehint): + if str(item).startswith("typing."): + concrete_types |= get_concrete_types_from_typehint(item) + else: + concrete_types.add(item) + else: + concrete_types.add(unpacked) + + return concrete_types + + if t.TYPE_CHECKING: SQLGlotListOfStrings = t.List[str] SQLGlotString = str diff --git a/tests/core/test_config.py b/tests/core/test_config.py index e580ed945b..d93a5fb1e2 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -985,3 +985,37 @@ def test_config_subclassing() -> None: class ConfigSubclass(Config): ... ConfigSubclass() + + +def test_config_complex_types_supplied_as_json_strings_from_env(tmp_path: Path) -> None: + config_path = tmp_path / "config.yaml" + config_path.write_text(""" + gateways: + bigquery: + connection: + type: bigquery + project: unit-test + + default_gateway: bigquery + + model_defaults: + dialect: bigquery +""") + with mock.patch.dict( + os.environ, + { + "SQLMESH__GATEWAYS__BIGQUERY__CONNECTION__SCOPES": ' ["a","b","c"]', # note: leading whitespace is deliberate + "SQLMESH__GATEWAYS__BIGQUERY__CONNECTION__KEYFILE_JSON": '{ "foo": "bar" }', + }, + ): + config = load_config_from_paths( + Config, + project_paths=[config_path], + ) + + conn = config.gateways["bigquery"].connection + assert isinstance(conn, BigQueryConnectionConfig) + + assert conn.project == "unit-test" + assert conn.scopes == ("a", "b", "c") + assert conn.keyfile_json == {"foo": "bar"} diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 890a9b03c6..f30aba119b 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -617,6 +617,27 @@ def test_duckdb_attach_options(): assert options.to_sql(alias="db") == "ATTACH IF NOT EXISTS 'test.db' AS db" +def test_duckdb_config_json_strings(make_config): + config = make_config( + type="duckdb", + extensions='["foo","bar"]', + catalogs="""{ + "test1": "test1.duckdb", + "test2": { + "type": "duckdb", + "path": "test2.duckdb" + } + }""", + ) + assert isinstance(config, DuckDBConnectionConfig) + + assert config.extensions == ["foo", "bar"] + + assert config.get_catalog() == "test1" + assert config.catalogs.get("test1") == "test1.duckdb" + assert config.catalogs.get("test2").path == "test2.duckdb" + + def test_motherduck_attach_catalog(make_config): config = make_config( type="motherduck", @@ -779,6 +800,21 @@ def test_bigquery(make_config): make_config(type="bigquery", quota_project="quota_project", check_import=False) +def test_bigquery_config_json_string(make_config): + config = make_config( + type="bigquery", + project="project", + # these can be present as strings if they came from env vars + scopes='["a","b","c"]', + keyfile_json='{"foo":"bar"}', + ) + + assert isinstance(config, BigQueryConnectionConfig) + + assert config.scopes == ("a", "b", "c") + assert config.keyfile_json == {"foo": "bar"} + + def test_postgres(make_config): config = make_config( type="postgres", diff --git a/tests/utils/test_pydantic.py b/tests/utils/test_pydantic.py index 9246d5ed0c..b07d45acb1 100644 --- a/tests/utils/test_pydantic.py +++ b/tests/utils/test_pydantic.py @@ -1,7 +1,9 @@ +import typing as t +import pytest from functools import cached_property from sqlmesh.utils.date import TimeLike, to_date, to_datetime -from sqlmesh.utils.pydantic import PydanticModel +from sqlmesh.utils.pydantic import PydanticModel, get_concrete_types_from_typehint def test_datetime_date_serialization() -> None: @@ -62,3 +64,26 @@ class TestModel(PydanticModel): name: str assert TestModel(name="foo").dict(by_alias=True) + + +@pytest.mark.parametrize( + "input,output", + [ + (t.Dict[str, t.Any], {dict}), + (dict, {dict}), + (t.List[str], {list}), + (list, {list}), + (t.Tuple[str, ...], {tuple}), + (tuple, {tuple}), + (t.Set[str], {set}), + (set, {set}), + (t.Optional[t.Dict[str, t.Any]], {dict, type(None)}), + (t.Optional[t.List[str]], {list, type(None)}), + ( + t.Union[str, t.List[str], t.Dict[str, t.Any], t.Optional[t.Set[str]]], + {str, list, dict, set, type(None)}, + ), + ], +) +def test_get_concrete_types_from_typehint(input: t.Any, output: set[type]) -> None: + assert get_concrete_types_from_typehint(input) == output From 419336a15d71d860b006349dbe9111ab1a783a7d Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 23 May 2025 11:20:47 +0200 Subject: [PATCH 0245/1056] chore(vscode): move e2e test to use temp folder each time (#4521) --- vscode/extension/tests/lineage.spec.ts | 92 ++++++++++++++------------ 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index 2a679eed86..4d7e758a17 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -1,52 +1,60 @@ import { test, _electron as electron, expect } from '@playwright/test'; import path from 'path'; import fs from 'fs-extra'; +import os from 'os'; // Absolute path to the VS Code executable you downloaded in step 1. const VS_CODE_EXE = fs.readJsonSync('.vscode-test/paths.json').executablePath; // Where your extension lives on disk -const EXT_PATH = path.join(__dirname, '..'); -// Where the sushi project lives which we open in the webview -const PROJECT_PATH = path.join(__dirname, '..', '..', '..', 'examples', 'sushi'); +const EXT_PATH = path.resolve(__dirname, '..'); +// Where the sushi project lives which we copy from +const SUSHI_SOURCE_PATH = path.join(__dirname, '..', '..', '..', 'examples', 'sushi'); test('Lineage panel renders correctly', async () => { - const ciArgs = process.env.CI ? [ - '--disable-gpu', - '--headless', - '--no-sandbox', - '--disable-dev-shm-usage', // Prevents memory issues in Docker/CI - '--window-position=-10000,0', // Ensures window is off-screen - ] : []; - const args = [ - ...ciArgs, - `--extensionDevelopmentPath=${EXT_PATH}`, - '--disable-workspace-trust', // no modal prompt - '--disable-telemetry', - '--user-data-dir=/tmp/vscode-test', // throwaway profile - `${PROJECT_PATH}`, - ]; - const electronApp = await electron.launch({ - executablePath: VS_CODE_EXE, - args, - }); - - // ➋ Grab the first window that appears (the Workbench) - const window = await electronApp.firstWindow(); - - // Wait for VS Code to be ready - await window.waitForLoadState('domcontentloaded'); - await window.waitForLoadState('networkidle'); - - await expect(window.locator("text=lineage").first()).toBeVisible(); - - // ➌ Trigger our command exactly like a user would - await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); - await window.keyboard.type('Lineage: Focus On View'); - await window.keyboard.press('Enter'); - - // Wait for "Loaded SQLmesh Context" text to appear - const loadedContextText = window.locator('text=Loaded SQLmesh Context'); - await expect(loadedContextText).toBeVisible({ timeout: 10000 }); - - await electronApp.close(); + // Create a temporary directory and copy sushi example into it + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); + await fs.copy(SUSHI_SOURCE_PATH, tempDir); + + try { + const ciArgs = process.env.CI ? [ + '--disable-gpu', + '--headless', + '--no-sandbox', + '--disable-dev-shm-usage', // Prevents memory issues in Docker/CI + '--window-position=-10000,0', // Ensures window is off-screen + ] : []; + const args = [ + ...ciArgs, + `--extensionDevelopmentPath=${EXT_PATH}`, + '--disable-workspace-trust', // no modal prompt + '--disable-telemetry', + '--user-data-dir=/tmp/vscode-test', // throwaway profile + `${tempDir}`, // Use the temporary directory instead of PROJECT_PATH + ]; + const electronApp = await electron.launch({ + executablePath: VS_CODE_EXE, + args, + }); + + // ➋ Grab the first window that appears (the Workbench) + const window = await electronApp.firstWindow(); + + // Wait for VS Code to be ready + await window.waitForLoadState('domcontentloaded'); + await window.waitForLoadState('networkidle'); + + // ➌ Trigger our command exactly like a user would + await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); + await window.keyboard.type('Lineage: Focus On View'); + await window.keyboard.press('Enter'); + + // Wait for "Loaded SQLmesh Context" text to appear + const loadedContextText = window.locator('text=Loaded SQLMesh Context'); + await expect(loadedContextText.first()).toBeVisible({ timeout: 10000 }); + + await electronApp.close(); + } finally { + // Clean up the temporary directory + await fs.remove(tempDir); + } }); From 01114b552b5008895e2d0edd531298a7fb0f07a1 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 23 May 2025 16:33:50 +0200 Subject: [PATCH 0246/1056] feat(vscode): add ability to specify project path (#4520) --- sqlmesh/lsp/main.py | 29 ++- vscode/extension/package.json | 11 ++ vscode/extension/src/lsp/lsp.ts | 10 +- vscode/extension/src/utilities/config.ts | 77 ++++++++ .../src/utilities/sqlmesh/sqlmesh.ts | 36 +++- vscode/extension/tests/lineage.spec.ts | 181 ++++++++++++++---- 6 files changed, 292 insertions(+), 52 deletions(-) create mode 100644 vscode/extension/src/utilities/config.ts diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 59575cda47..c3d17d261e 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -69,10 +69,7 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: # Use user-provided instantiator to build the context created_context = self.context_class(paths=[folder_path]) self.lsp_context = LSPContext(created_context) - ls.show_message( - f"Loaded SQLMesh context from {config_path}", - types.MessageType.Info, - ) + loaded_sqlmesh_message(ls, folder_path) return # Exit after successfully loading any config except Exception as e: ls.show_message( @@ -94,6 +91,9 @@ def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsRespons @self.server.feature(API_FEATURE) def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]: ls.log_trace(f"API request: {request}") + if self.lsp_context is None: + current_path = Path.cwd() + self._ensure_context_in_folder(current_path) if self.lsp_context is None: raise RuntimeError("No context found") @@ -291,6 +291,20 @@ def _context_get_or_load(self, document_uri: URI) -> LSPContext: raise RuntimeError("No context found") return self.lsp_context + def _ensure_context_in_folder(self, folder_uri: Path) -> None: + if self.lsp_context is not None: + return + for ext in ("py", "yml", "yaml"): + config_path = folder_uri / f"config.{ext}" + if config_path.exists(): + try: + created_context = self.context_class(paths=[folder_uri]) + self.lsp_context = LSPContext(created_context) + loaded_sqlmesh_message(self.server, folder_uri) + return + except Exception as e: + self.server.show_message(f"Error loading context: {e}", types.MessageType.Error) + def _ensure_context_for_document( self, document_uri: URI, @@ -382,6 +396,13 @@ def start(self) -> None: self.server.start_io() +def loaded_sqlmesh_message(ls: LanguageServer, folder: Path) -> None: + ls.show_message( + f"Loaded SQLMesh context from {folder}", + types.MessageType.Info, + ) + + def main() -> None: # Example instantiator that just uses the same signature as your original `Context` usage. sqlmesh_server = SQLMeshLanguageServer(context_class=Context) diff --git a/vscode/extension/package.json b/vscode/extension/package.json index fd86d7cdcb..536b9e1203 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -27,6 +27,17 @@ "ms-python.python" ], "contributes": { + "configuration": { + "type": "object", + "title": "SQLMesh", + "properties": { + "sqlmesh.projectPath": { + "type": "string", + "default": "", + "markdownDescription": "The path to the SQLMesh project. If not set, the extension will try to find the project root automatically. If set, the extension will use the project root as the workspace path, e.g. it will run `sqlmesh` and `sqlmesh_lsp` in the project root. The path can be absolute `/Users/sqlmesh_user/sqlmesh_project/sushi` or relative `./project_folder/sushi` to the workspace root." + } + } + }, "viewsContainers": { "panel": [ { diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 1efa03aa9d..50129406c1 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -8,7 +8,7 @@ import { import { sqlmeshLspExec } from '../utilities/sqlmesh/sqlmesh' import { err, isErr, ok, Result } from '@bus/result' import { getWorkspaceFolders } from '../utilities/common/vscodeapi' -import { traceError } from '../utilities/common/log' +import { traceError, traceInfo } from '../utilities/common/log' import { ErrorType } from '../utilities/errors' import { CustomLSPMethods } from './custom' @@ -43,9 +43,7 @@ export class LSPClient implements Disposable { message: 'Invalid number of workspace folders', }) } - - const folder = workspaceFolders[0] - const workspacePath = workspaceFolders[0].uri.fsPath + const workspacePath = sqlmesh.value.workspacePath const serverOptions: ServerOptions = { run: { command: sqlmesh.value.bin, @@ -66,11 +64,13 @@ export class LSPClient implements Disposable { } const clientOptions: LanguageClientOptions = { documentSelector: [{ scheme: 'file', pattern: `**/*.sql` }], - workspaceFolder: folder, diagnosticCollectionName: 'sqlmesh', outputChannel: outputChannel, } + traceInfo( + `Starting SQLMesh Language Server with workspace path: ${workspacePath} with server options ${JSON.stringify(serverOptions)} and client options ${JSON.stringify(clientOptions)}`, + ) this.client = new LanguageClient( 'sqlmesh-lsp', 'SQLMesh Language Server', diff --git a/vscode/extension/src/utilities/config.ts b/vscode/extension/src/utilities/config.ts new file mode 100644 index 0000000000..e77a39ce55 --- /dev/null +++ b/vscode/extension/src/utilities/config.ts @@ -0,0 +1,77 @@ +import { workspace, WorkspaceFolder } from 'vscode' +import path from 'path' +import fs from 'fs' +import { Result, err, ok } from '@bus/result' +import { traceVerbose, traceInfo } from './common/log' + +export interface SqlmeshConfiguration { + projectPath: string +} + +/** + * Get the SQLMesh configuration from VS Code settings. + * + * @returns The SQLMesh configuration + */ +export function getSqlmeshConfiguration(): SqlmeshConfiguration { + const config = workspace.getConfiguration('sqlmesh') + const projectPath = config.get('projectPath', '') + return { + projectPath, + } +} + +/** + * Validate and resolve the project path from configuration. + * If no project path is configured, use the workspace folder. + * If the project path is configured, it must be a directory that contains a SQLMesh project. + * + * @param workspaceFolder The current workspace folder + * @returns A Result containing the resolved project path or an error + */ +export function resolveProjectPath( + workspaceFolder: WorkspaceFolder, +): Result { + const config = getSqlmeshConfiguration() + + if (!config.projectPath) { + // If no project path is configured, use the workspace folder + traceVerbose('No project path configured, using workspace folder') + return ok(workspaceFolder.uri.fsPath) + } + let resolvedPath: string + + // Check if the path is absolute + if (path.isAbsolute(config.projectPath)) { + resolvedPath = config.projectPath + } else { + // Resolve relative path from workspace root + resolvedPath = path.join(workspaceFolder.uri.fsPath, config.projectPath) + } + + // Normalize the path + resolvedPath = path.normalize(resolvedPath) + + // Validate that the path exists + if (!fs.existsSync(resolvedPath)) { + return err(`Configured project path does not exist: ${resolvedPath}`) + } + + // Validate that it's a directory + const stats = fs.statSync(resolvedPath) + if (!stats.isDirectory()) { + return err(`Configured project path is not a directory: ${resolvedPath}`) + } + + // Check if it contains SQLMesh project files (config.yaml, config.yml, or config.py) + const configFiles = ['config.yaml', 'config.yml', 'config.py'] + const hasConfigFile = configFiles.some(file => + fs.existsSync(path.join(resolvedPath, file)), + ) + if (!hasConfigFile) { + traceInfo(`Warning: No SQLMesh configuration file found in ${resolvedPath}`) + } + + traceVerbose(`Using project path: ${resolvedPath}`) + return ok(resolvedPath) +} diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index aa11033c11..6adb0fa1a2 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -11,6 +11,7 @@ import { execAsync } from '../exec' import z from 'zod' import { ProgressLocation, window } from 'vscode' import { IS_WINDOWS } from '../isWindows' +import { resolveProjectPath } from '../config' export interface SqlmeshExecInfo { workspacePath: string @@ -28,8 +29,12 @@ export interface SqlmeshExecInfo { */ export const isTcloudProject = async (): Promise> => { const projectRoot = await getProjectRoot() - const tcloudYamlPath = path.join(projectRoot.uri.fsPath, 'tcloud.yaml') - const tcloudYmlPath = path.join(projectRoot.uri.fsPath, 'tcloud.yml') + const resolvedPath = resolveProjectPath(projectRoot) + if (isErr(resolvedPath)) { + return err(resolvedPath.error) + } + const tcloudYamlPath = path.join(resolvedPath.value, 'tcloud.yaml') + const tcloudYmlPath = path.join(resolvedPath.value, 'tcloud.yml') const isTcloudYamlFilePresent = fs.existsSync(tcloudYamlPath) const isTcloudYmlFilePresent = fs.existsSync(tcloudYmlPath) if (isTcloudYamlFilePresent || isTcloudYmlFilePresent) { @@ -83,8 +88,15 @@ export const isSqlmeshEnterpriseInstalled = async (): Promise< return tcloudBin } const projectRoot = await getProjectRoot() + const resolvedPath = resolveProjectPath(projectRoot) + if (isErr(resolvedPath)) { + return err({ + type: 'generic', + message: resolvedPath.error, + }) + } const called = await execAsync(tcloudBin.value, ['is_sqlmesh_installed'], { - cwd: projectRoot.uri.fsPath, + cwd: resolvedPath.value, }) if (called.exitCode !== 0) { return err({ @@ -183,7 +195,14 @@ export const sqlmeshExec = async (): Promise< > => { const sqlmesh = IS_WINDOWS ? 'sqlmesh.exe' : 'sqlmesh' const projectRoot = await getProjectRoot() - const workspacePath = projectRoot.uri.fsPath + const resolvedPath = resolveProjectPath(projectRoot) + if (isErr(resolvedPath)) { + return err({ + type: 'generic', + message: resolvedPath.error, + }) + } + const workspacePath = resolvedPath.value const interpreterDetails = await getInterpreterDetails() traceLog(`Interpreter details: ${JSON.stringify(interpreterDetails)}`) if (interpreterDetails.path) { @@ -300,7 +319,14 @@ export const sqlmeshLspExec = async (): Promise< > => { const sqlmeshLSP = IS_WINDOWS ? 'sqlmesh_lsp.exe' : 'sqlmesh_lsp' const projectRoot = await getProjectRoot() - const workspacePath = projectRoot.uri.fsPath + const resolvedPath = resolveProjectPath(projectRoot) + if (isErr(resolvedPath)) { + return err({ + type: 'generic', + message: resolvedPath.error, + }) + } + const workspacePath = resolvedPath.value const interpreterDetails = await getInterpreterDetails() traceLog(`Interpreter details: ${JSON.stringify(interpreterDetails)}`) if (interpreterDetails.path) { diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index 4d7e758a17..5bac457ef9 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -1,4 +1,4 @@ -import { test, _electron as electron, expect } from '@playwright/test'; +import { test, _electron as electron, expect, ElectronApplication, Page } from '@playwright/test'; import path from 'path'; import fs from 'fs-extra'; import os from 'os'; @@ -10,51 +10,156 @@ const EXT_PATH = path.resolve(__dirname, '..'); // Where the sushi project lives which we copy from const SUSHI_SOURCE_PATH = path.join(__dirname, '..', '..', '..', 'examples', 'sushi'); -test('Lineage panel renders correctly', async () => { - // Create a temporary directory and copy sushi example into it - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); - await fs.copy(SUSHI_SOURCE_PATH, tempDir); - - try { - const ciArgs = process.env.CI ? [ - '--disable-gpu', - '--headless', - '--no-sandbox', - '--disable-dev-shm-usage', // Prevents memory issues in Docker/CI - '--window-position=-10000,0', // Ensures window is off-screen - ] : []; - const args = [ - ...ciArgs, - `--extensionDevelopmentPath=${EXT_PATH}`, - '--disable-workspace-trust', // no modal prompt - '--disable-telemetry', - '--user-data-dir=/tmp/vscode-test', // throwaway profile - `${tempDir}`, // Use the temporary directory instead of PROJECT_PATH - ]; - const electronApp = await electron.launch({ - executablePath: VS_CODE_EXE, - args, - }); - - // ➋ Grab the first window that appears (the Workbench) - const window = await electronApp.firstWindow(); - - // Wait for VS Code to be ready - await window.waitForLoadState('domcontentloaded'); - await window.waitForLoadState('networkidle'); - - // ➌ Trigger our command exactly like a user would +/** + * Helper function to launch VS Code and test lineage with given project path config + */ +async function testLineageWithProjectPath( + window: Page, +): Promise { + // Trigger lineage command await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); await window.keyboard.type('Lineage: Focus On View'); await window.keyboard.press('Enter'); // Wait for "Loaded SQLmesh Context" text to appear const loadedContextText = window.locator('text=Loaded SQLMesh Context'); - await expect(loadedContextText.first()).toBeVisible({ timeout: 10000 }); + await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 }); +} +/** + * Launch VS Code and return the window and a function to close the app. + * @param workspaceDir The workspace directory to open. + * @returns The window and a function to close the app. + */ +export const startVSCode = async (workspaceDir: string): Promise<{ + window: Page, + close: () => Promise, +}> => { + const userDataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-user-data-')); + const ciArgs = process.env.CI ? [ + '--disable-gpu', + '--headless', + '--no-sandbox', + '--disable-dev-shm-usage', + '--window-position=-10000,0', + ] : []; + const args = [ + ...ciArgs, + `--extensionDevelopmentPath=${EXT_PATH}`, + '--disable-workspace-trust', + '--disable-telemetry', + `--user-data-dir=${userDataDir}`, + workspaceDir, + ]; + const electronApp = await electron.launch({ + executablePath: VS_CODE_EXE, + args, + }); + const window = await electronApp.firstWindow(); + await window.waitForLoadState('domcontentloaded'); + await window.waitForLoadState('networkidle'); + await window.waitForTimeout(2_000); + return { window, close: async () => { await electronApp.close(); - } finally { - // Clean up the temporary directory + await fs.remove(userDataDir); + } }; +} + +test('Lineage panel renders correctly - no project path config (default)', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); + await fs.copy(SUSHI_SOURCE_PATH, tempDir); + try { + const { window, close } = await startVSCode(tempDir); + await testLineageWithProjectPath(window); + await close(); + } finally { await fs.remove(tempDir); } }); + +test('Lineage panel renders correctly - relative project path', async () => { + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-')); + const projectDir = path.join(workspaceDir, 'projects', 'sushi'); + await fs.copy(SUSHI_SOURCE_PATH, projectDir); + + const settings = { + "sqlmesh.projectPath": "./projects/sushi", + }; + await fs.ensureDir(path.join(workspaceDir, '.vscode')); + await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 }); + + try { + const { window, close } = await startVSCode(workspaceDir); + await testLineageWithProjectPath(window); + await close(); + } finally { + await fs.remove(workspaceDir); + } +}); + +test('Lineage panel renders correctly - absolute project path', async () => { + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-')); + const projectDir = path.join(workspaceDir, 'projects', 'sushi'); + await fs.ensureDir(path.join(workspaceDir, '.vscode')); + await fs.copy(SUSHI_SOURCE_PATH, projectDir); + await fs.ensureDir(path.join(workspaceDir, '.vscode')); + const settings = { + "sqlmesh.projectPath": projectDir, + }; + await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 }); + + try { + const { window, close } = await startVSCode(workspaceDir); + await testLineageWithProjectPath(window); + await close(); + } finally { + await fs.remove(workspaceDir); + } +}); + + +test("Lineage panel renders correctly - relative project outside of workspace", async () => { + const tempFolder = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-')); + const projectDir = path.join(tempFolder, 'projects', 'sushi'); + await fs.copy(SUSHI_SOURCE_PATH, projectDir); + + const workspaceDir = path.join(tempFolder, 'workspace'); + await fs.ensureDir(workspaceDir); + + const settings = { + "sqlmesh.projectPath": "./../projects/sushi", + }; + await fs.ensureDir(path.join(workspaceDir, '.vscode')); + await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 }); + + try { + const { window, close } = await startVSCode(workspaceDir); + await testLineageWithProjectPath(window); + await close(); + } finally { + await fs.remove(tempFolder); + } +}); + +test("Lineage panel renders correctly - absolute path project outside of workspace", async () => { + const tempFolder = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-')); + const projectDir = path.join(tempFolder, 'projects', 'sushi'); + await fs.copy(SUSHI_SOURCE_PATH, projectDir); + + const workspaceDir = path.join(tempFolder, 'workspace'); + await fs.ensureDir(workspaceDir); + + const settings = { + "sqlmesh.projectPath": projectDir, + }; + await fs.ensureDir(path.join(workspaceDir, '.vscode')); + await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 }); + + try { + const { window, close } = await startVSCode(workspaceDir); + await testLineageWithProjectPath(window); + await close(); + } finally { + await fs.remove(tempFolder); + } +}); \ No newline at end of file From ab4df0fdf71bbf2cbcc3a57cccd2f9f1f96b8625 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 23 May 2025 21:51:01 +0300 Subject: [PATCH 0247/1056] Feat: expose model fqn in the macro evaluator (#4510) --- sqlmesh/core/macros.py | 8 ++++++++ sqlmesh/core/renderer.py | 1 + tests/core/test_model.py | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index 5326804a7b..db530481cd 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -173,6 +173,7 @@ def __init__( default_catalog: t.Optional[str] = None, path: Path = Path(), environment_naming_info: t.Optional[EnvironmentNamingInfo] = None, + model_fqn: t.Optional[str] = None, ): self.dialect = dialect self.generator = MacroDialect().generator() @@ -198,6 +199,7 @@ def __init__( self._snapshots = snapshots if snapshots is not None else {} self._path = path self._environment_naming_info = environment_naming_info + self._model_fqn = model_fqn prepare_env(self.python_env, self.env) for k, v in self.python_env.items(): @@ -476,6 +478,12 @@ def this_model(self) -> str: raise SQLMeshError("Model name is not available in the macro evaluator.") return this_model.sql(dialect=self.dialect, identify=True, comments=False) + @property + def this_model_fqn(self) -> str: + if self._model_fqn is None: + raise SQLMeshError("Model name is not available in the macro evaluator.") + return self._model_fqn + @property def engine_adapter(self) -> EngineAdapter: engine_adapter = self.locals.get("engine_adapter") diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index fa07c20931..c683fc5862 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -169,6 +169,7 @@ def _resolve_table(table: str | exp.Table) -> str: default_catalog=self._default_catalog, path=self._path, environment_naming_info=environment_naming_info, + model_fqn=self._model_fqn, ) start_time, end_time = ( diff --git a/tests/core/test_model.py b/tests/core/test_model.py index fadbaa8b08..735242743f 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9993,6 +9993,7 @@ def test_extract_schema_in_post_statement(tmp_path: Path) -> None: SELECT c FROM x; ON_VIRTUAL_UPDATE_BEGIN; @check_schema('y'); + @check_self_schema(); ON_VIRTUAL_UPDATE_END; """ ) @@ -10007,6 +10008,11 @@ def test_extract_schema_in_post_statement(tmp_path: Path) -> None: def check_schema(evaluator, model_name: str): if evaluator.runtime_stage != 'loading': assert evaluator.columns_to_types(model_name) == {"c": exp.DataType.build("INT")} + +@macro() +def check_self_schema(evaluator): + if evaluator.runtime_stage != 'loading': + assert evaluator.columns_to_types(evaluator.this_model_fqn) == {"c": exp.DataType.build("INT")} """) context = Context(paths=tmp_path, config=config) From c5d3bdb611ac3030025684da90d912231aeced58 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sat, 24 May 2025 16:36:34 +0200 Subject: [PATCH 0248/1056] fix(vscode): fix installation of sqlmesh (#4526) --- vscode/extension/src/utilities/sqlmesh/sqlmesh.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 6adb0fa1a2..707e57f76b 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -126,8 +126,17 @@ export const installSqlmeshEnterprise = async ( if (isErr(tcloudBin)) { return tcloudBin } + const projectRoot = await getProjectRoot() + const resolvedPath = resolveProjectPath(projectRoot) + if (isErr(resolvedPath)) { + return err({ + type: 'generic', + message: resolvedPath.error, + }) + } const called = await execAsync(tcloudBin.value, ['install_sqlmesh'], { signal: abortController.signal, + cwd: resolvedPath.value, }) if (called.exitCode !== 0) { return err({ From 815bd98b4313d50bcab004678c354eb381d73ee7 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 26 May 2025 02:16:42 +0300 Subject: [PATCH 0249/1056] Chore!: bump sqlglot to v26.20.0 (#4530) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e2538056ff..965d7f41fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.19.0", + "sqlglot[rs]~=26.20.0", "tenacity", "time-machine", "json-stream" From b1f9960d5de89a28e70f0081c00d8634f4fdfab0 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Mon, 26 May 2025 11:31:40 +1200 Subject: [PATCH 0250/1056] Fix(snowflake): Correctly handle COPY GRANTS property in materialized views (#4518) --- tests/core/engine_adapter/test_snowflake.py | 65 +++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index 54cf377517..0f79f7aafd 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -15,6 +15,7 @@ from sqlmesh.utils.errors import SQLMeshError from sqlmesh.utils import optional_import from tests.core.engine_adapter import to_sql_calls +from sqlmesh.core.model.kind import ViewKind pytestmark = [pytest.mark.engine, pytest.mark.snowflake] @@ -727,3 +728,67 @@ def test_table_format_iceberg(snowflake_mocked_engine_adapter: SnowflakeEngineAd 'CREATE ICEBERG TABLE IF NOT EXISTS "test"."table" ("a" INT) CATALOG=\'snowflake\' EXTERNAL_VOLUME=\'test\'', 'CREATE ICEBERG TABLE IF NOT EXISTS "test"."table" CATALOG=\'snowflake\' EXTERNAL_VOLUME=\'test\' AS SELECT CAST("a" AS INT) AS "a" FROM (SELECT CAST("a" AS INT) AS "a") AS "_subquery"', ] + + +def test_create_view_with_schema_and_grants( + snowflake_mocked_engine_adapter: SnowflakeEngineAdapter, +): + adapter = snowflake_mocked_engine_adapter + + model_v = load_sql_based_model( + d.parse(f""" + MODEL ( + name test.v, + kind VIEW, + description 'normal **view** from integration test', + dialect 'snowflake' + ); + + select 1 as "ID", 'foo' as "NAME"; + """) + ) + + model_mv = load_sql_based_model( + d.parse(f""" + MODEL ( + name test.mv, + kind VIEW ( + materialized true + ), + description 'materialized **view** from integration test', + dialect 'snowflake' + ); + + select 1 as "ID", 'foo' as "NAME"; + """) + ) + + assert isinstance(model_v.kind, ViewKind) + assert isinstance(model_mv.kind, ViewKind) + + adapter.create_view( + "target_view", + model_v.render_query_or_raise(), + model_v.columns_to_types, + materialized=model_v.kind.materialized, + view_properties=model_v.render_physical_properties(), + table_description=model_v.description, + column_descriptions=model_v.column_descriptions, + ) + + adapter.create_view( + "target_materialized_view", + model_mv.render_query_or_raise(), + model_mv.columns_to_types, + materialized=model_mv.kind.materialized, + view_properties=model_mv.render_physical_properties(), + table_description=model_mv.description, + column_descriptions=model_mv.column_descriptions, + ) + + assert to_sql_calls(adapter) == [ + # normal view - COPY GRANTS goes after the column list + """CREATE OR REPLACE VIEW "target_view" ("ID", "NAME") COPY GRANTS COMMENT='normal **view** from integration test' AS SELECT 1 AS "ID", 'foo' AS "NAME\"""", + # materialized view - COPY GRANTS goes before the column list + """CREATE OR REPLACE MATERIALIZED VIEW "target_materialized_view" COPY GRANTS ("ID", "NAME") COMMENT='materialized **view** from integration test' AS SELECT 1 AS "ID", 'foo' AS "NAME\"""", + ] From 9cec846731c46017f1cffd5992abf5f1c47289cf Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 26 May 2025 17:13:47 +0300 Subject: [PATCH 0251/1056] Chore: document this_model and this_model_fqn (#4524) Co-authored-by: Trey Spiller --- docs/concepts/macros/sqlmesh_macros.md | 67 ++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/docs/concepts/macros/sqlmesh_macros.md b/docs/concepts/macros/sqlmesh_macros.md index 62c21f0953..4e5601d2e1 100644 --- a/docs/concepts/macros/sqlmesh_macros.md +++ b/docs/concepts/macros/sqlmesh_macros.md @@ -1681,6 +1681,73 @@ def some_macro(evaluator): ... ``` +#### Accessing model, physical table, and virtual layer view names + +All SQLMesh models have a name in their `MODEL` specification. We refer to that as the model's "unresolved" name because it may not correspond to any specific object in the SQL engine. + +When SQLMesh renders and executes a model, it converts the model name into three forms at different stages: + +1. The *fully qualified* name + + - If the model name is of the form `schema.table`, SQLMesh determines the correct catalog and adds it, like `catalog.schema.table` + - SQLMesh quotes each component of the name using the SQL engine's quoting and case-sensitivity rules, like `"catalog"."schema"."table"` + +2. The *resolved* physical table name + + - The qualified name of the model's underlying physical table + +3. The *resolved* virtual layer view name + + - The qualified name of the model's virtual layer view in the environment where the model is being executed + +You can access any of these three forms in a Python macro through properties of the `evaluation` context object. + +Access the unresolved, fully-qualified name through the `this_model_fqn` property. + +```python linenums="1" +from sqlmesh.core.macros import macro + +@macro() +def some_macro(evaluator): + # Example: + # Name in model definition: landing.customers + # Value returned here: '"datalake"."landing"."customers"' + unresolved_model_fqn = evaluator.this_model_fqn + ... +``` + +Access the resolved physical table and virtual layer view names through the `this_model` property. + +The `this_model` property returns different names depending on the runtime stage: + +- `promoting` runtime stage: `this_model` resolves to the virtual layer view name + + - Example + - Model name is `db.test_model` + - `plan` is running in the `dev` environment + - `this_model` resolves to `"catalog"."db__dev"."test_model"` (note the `__dev` suffix in the schema name) + +- All other runtime stages: `this_model` resolves to the physical table name + + - Example + - Model name is `db.test_model` + - `plan` is running in any environment + - `this_model` resolves to `"catalog"."sqlmesh__project"."project__test_model__684351896"` + +```python linenums="1" +from sqlmesh.core.macros import macro + +@macro() +def some_macro(evaluator): + if evaluator.runtime_stage == "promoting": + # virtual layer view name '"catalog"."db__dev"."test_model"' + resolved_name = evaluator.this_model + else: + # physical table name '"catalog"."sqlmesh__project"."project__test_model__684351896"' + resolved_name = evaluator.this_model + ... +``` + #### Accessing model schemas Model schemas can be accessed within a Python macro function through its evaluation context's `column_to_types()` method, if the column types can be statically determined. For instance, a schema of an [external model](../models/external_models.md) can be accessed only after the `sqlmesh create_external_models` command has been executed. From 90127907df401ffe7c88d972f514afc06949f695 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 26 May 2025 16:50:16 +0200 Subject: [PATCH 0252/1056] feat(vscode): add info to hover (#4529) --- sqlmesh/lsp/description.py | 30 ++++++++++++++++++++++++++++++ sqlmesh/lsp/main.py | 5 +++-- sqlmesh/lsp/reference.py | 9 ++++++--- tests/lsp/test_reference.py | 14 +++++++++++++- 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 sqlmesh/lsp/description.py diff --git a/sqlmesh/lsp/description.py b/sqlmesh/lsp/description.py new file mode 100644 index 0000000000..82ea93dd8d --- /dev/null +++ b/sqlmesh/lsp/description.py @@ -0,0 +1,30 @@ +from sqlmesh.core.model.definition import ( + ExternalModel, + PythonModel, + SeedModel, + SqlModel, +) +import typing as t + + +def generate_markdown_description( + model: t.Union[SqlModel, ExternalModel, PythonModel, SeedModel], +) -> t.Optional[str]: + columns = model.columns_to_types + column_descriptions = model.column_descriptions + + columns_table = ( + "\n".join( + [ + f"| {column} | {column_type} | {column_descriptions.get(column, '')} |" + for column, column_type in columns.items() + ] + ) + if columns + else "" + ) + + table_header = "\n\n| Column | Type | Description |\n|--------|------|-------------|\n" + return ( + f"{model.description}{table_header}{columns_table}" if columns_table else model.description + ) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index c3d17d261e..69b5252e93 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -239,11 +239,12 @@ def hover(ls: LanguageServer, params: types.HoverParams) -> t.Optional[types.Hov if not references: return None reference = references[0] - if not reference.description: + if not reference.markdown_description: return None return types.Hover( contents=types.MarkupContent( - kind=types.MarkupKind.Markdown, value=reference.description + kind=types.MarkupKind.Markdown, + value=reference.markdown_description, ), range=reference.range, ) diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index ff73b24f28..1f646dadf6 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -5,6 +5,7 @@ from sqlmesh.core.model.definition import SqlModel from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget from sqlglot import exp +from sqlmesh.lsp.description import generate_markdown_description from sqlmesh.lsp.uri import URI from sqlmesh.utils.pydantic import PydanticModel @@ -16,12 +17,12 @@ class Reference(PydanticModel): Attributes: range: The range of the reference in the source file uri: The uri of the referenced model - description: The description of the referenced model + markdown_description: The markdown description of the referenced model """ range: Range uri: str - description: t.Optional[str] = None + markdown_description: t.Optional[str] = None def by_position(position: Position) -> t.Callable[[Reference], bool]: @@ -176,11 +177,13 @@ def get_model_definitions_for_a_path( catalog_or_db_range = _range_from_token_position_details(catalog_or_db_meta, read_file) start_pos = catalog_or_db_range.start + description = generate_markdown_description(referenced_model) + references.append( Reference( uri=referenced_model_uri.value, range=Range(start=start_pos, end=end_pos), - description=referenced_model.description, + markdown_description=description, ) ) diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index d6787dffce..08e92e8fca 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -60,7 +60,19 @@ def test_reference_with_alias() -> None: assert references[0].uri.endswith("orders.py") assert get_string_from_range(read_file, references[0].range) == "sushi.orders" - assert references[0].description == "Table of sushi orders." + assert ( + references[0].markdown_description + == """Table of sushi orders. + +| Column | Type | Description | +|--------|------|-------------| +| id | INT | | +| customer_id | INT | | +| waiter_id | INT | | +| start_ts | INT | | +| end_ts | INT | | +| event_date | DATE | |""" + ) assert references[1].uri.endswith("order_items.py") assert get_string_from_range(read_file, references[1].range) == "sushi.order_items" assert references[2].uri.endswith("items.py") From d5651541c99ba88bb911d9493497ba803fcea50e Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Mon, 26 May 2025 18:03:54 +0300 Subject: [PATCH 0253/1056] Chore!: Make `default_connection` optional (#4522) --- sqlmesh/core/config/root.py | 8 ++++++-- tests/conftest.py | 28 +++++++++++++++++++++++++++- tests/core/test_integration.py | 21 ++++++++++++++++++++- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index b624ea66fb..85e4ca77c4 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -122,7 +122,7 @@ class Config(BaseConfig): """ gateways: GatewayDict = {"": GatewayConfig()} - default_connection: SerializableConnectionConfig = DuckDBConnectionConfig() + default_connection: t.Optional[SerializableConnectionConfig] = None default_test_connection_: t.Optional[SerializableConnectionConfig] = Field( default=None, alias="default_test_connection" ) @@ -280,7 +280,11 @@ def get_gateway(self, name: t.Optional[str] = None) -> GatewayConfig: return self.gateways def get_connection(self, gateway_name: t.Optional[str] = None) -> ConnectionConfig: - return self.get_gateway(gateway_name).connection or self.default_connection + connection = self.get_gateway(gateway_name).connection or self.default_connection + if connection is None: + msg = f" for gateway '{gateway_name}'" if gateway_name else "" + raise ConfigError(f"No connection configured{msg}.") + return connection def get_state_connection( self, gateway_name: t.Optional[str] = None diff --git a/tests/conftest.py b/tests/conftest.py index 224fba7d4a..19953e7f5b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,11 @@ from __future__ import annotations + import datetime import logging import typing as t import uuid +from contextlib import nullcontext from pathlib import Path from shutil import copytree, rmtree from tempfile import TemporaryDirectory @@ -21,7 +23,8 @@ from sqlglot.helper import ensure_list from sqlglot.optimizer.normalize_identifiers import normalize_identifiers -from sqlmesh.core.config import BaseDuckDBConnectionConfig +from sqlmesh.core.config import Config, BaseDuckDBConnectionConfig, DuckDBConnectionConfig +from sqlmesh.core.config.connection import ConnectionConfig from sqlmesh.core.context import Context from sqlmesh.core.engine_adapter import MSSQLEngineAdapter, SparkEngineAdapter from sqlmesh.core.engine_adapter.base import EngineAdapter @@ -526,3 +529,26 @@ def _make_function(table_name: str, random_id: str) -> exp.Table: return temp_table return _make_function + + +@pytest.fixture(scope="function", autouse=True) +def set_default_connection(request): + request = request.node.get_closest_marker("set_default_connection") + disable = request and request.kwargs.get("disable") + + if disable: + ctx = nullcontext() + else: + original_get_connection = Config.get_connection + + def _lax_get_connection(self, gateway_name: t.Optional[str] = None) -> ConnectionConfig: + try: + connection = original_get_connection(self, gateway_name) + except: + connection = DuckDBConnectionConfig() + return connection + + ctx = mock.patch("sqlmesh.core.config.Config.get_connection", _lax_get_connection) + + with ctx: + yield diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 4f04f7edab..576fc4128d 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -64,7 +64,7 @@ SnapshotTableInfo, ) from sqlmesh.utils.date import TimeLike, now, to_date, to_datetime, to_timestamp -from sqlmesh.utils.errors import NoChangesPlanError, SQLMeshError, PlanError +from sqlmesh.utils.errors import NoChangesPlanError, SQLMeshError, PlanError, ConfigError from sqlmesh.utils.pydantic import validate_string from tests.conftest import DuckDBMetadata, SushiDataValidator from tests.utils.test_helpers import use_terminal_console @@ -6129,3 +6129,22 @@ def setup_senario(model_before: str, model_after: str): 'Binder Error: Referenced column "this_col_does_not_exist" not found in \nFROM clause!' in output.stdout ) + + +@pytest.mark.set_default_connection(disable=True) +def test_missing_connection_config(): + # This is testing the actual implementation of Config.get_connection + # To make writing tests easier, it's patched by the autouse fixture provide_sqlmesh_default_connection + # Case 1: No default_connection or gateways specified should raise a ConfigError + with pytest.raises(ConfigError): + ctx = Context(config=Config()) + + # Case 2: No connection specified in the gateway should raise a ConfigError + with pytest.raises(ConfigError): + ctx = Context(config=Config(gateways={"incorrect": GatewayConfig()})) + + # Case 3: Specifying a default_connection or connection in the gateway should work + ctx = Context(config=Config(default_connection=DuckDBConnectionConfig())) + ctx = Context( + config=Config(gateways={"default": GatewayConfig(connection=DuckDBConnectionConfig())}) + ) From 76f52e6eb864f84ac4d15a87559f70f718bfe395 Mon Sep 17 00:00:00 2001 From: Will Sweet Date: Mon, 26 May 2025 11:07:00 -0400 Subject: [PATCH 0254/1056] Add iceberg support to table_diff (#4441) --- sqlmesh/core/table_diff.py | 23 ++++++- tests/core/engine_adapter/test_athena.py | 82 ++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/table_diff.py b/sqlmesh/core/table_diff.py index 7b59acb912..8ea7f1e9cc 100644 --- a/sqlmesh/core/table_diff.py +++ b/sqlmesh/core/table_diff.py @@ -8,6 +8,7 @@ from sqlmesh.core.dialect import to_schema from sqlmesh.core.engine_adapter.mixins import RowDiffMixin +from sqlmesh.core.engine_adapter.athena import AthenaEngineAdapter from sqlglot import exp, parse_one from sqlglot.helper import ensure_list from sqlglot.optimizer.normalize_identifiers import normalize_identifiers @@ -15,6 +16,7 @@ from sqlglot.optimizer.scope import find_all_in_scope from sqlmesh.utils.pydantic import PydanticModel +from sqlmesh.utils.errors import SQLMeshError if t.TYPE_CHECKING: from sqlmesh.core._typing import TableName @@ -431,7 +433,26 @@ def name(e: exp.Expression) -> str: schema = to_schema(temp_schema, dialect=self.dialect) temp_table = exp.table_("diff", db=schema.db, catalog=schema.catalog, quoted=True) - with self.adapter.temp_table(query, name=temp_table) as table: + temp_table_kwargs = {} + if isinstance(self.adapter, AthenaEngineAdapter): + # Athena has two table formats: Hive (the default) and Iceberg. TableDiff requires that + # the formats be the same for the source, target, and temp tables. + source_table_type = self.adapter._query_table_type(self.source_table) + target_table_type = self.adapter._query_table_type(self.target_table) + + if source_table_type == "iceberg" and target_table_type == "iceberg": + temp_table_kwargs["table_format"] = "iceberg" + # Sets the temp table's format to Iceberg. + # If neither source nor target table is Iceberg, it defaults to Hive (Athena's default). + elif source_table_type == "iceberg" or target_table_type == "iceberg": + raise SQLMeshError( + f"Source table '{self.source}' format '{source_table_type}' and target table '{self.target}' format '{target_table_type}' " + f"do not match for Athena. Diffing between different table formats is not supported." + ) + + with self.adapter.temp_table( + query, name=temp_table, columns_to_types=None, **temp_table_kwargs + ) as table: summary_sums = [ exp.func("SUM", "s_exists").as_("s_count"), exp.func("SUM", "t_exists").as_("t_count"), diff --git a/tests/core/engine_adapter/test_athena.py b/tests/core/engine_adapter/test_athena.py index f96f5656a2..2fd45ac3df 100644 --- a/tests/core/engine_adapter/test_athena.py +++ b/tests/core/engine_adapter/test_athena.py @@ -10,6 +10,7 @@ from sqlmesh.core.model import load_sql_based_model from sqlmesh.core.model.definition import SqlModel from sqlmesh.utils.errors import SQLMeshError +from sqlmesh.core.table_diff import TableDiff from tests.core.engine_adapter import to_sql_calls @@ -21,6 +22,16 @@ def adapter(make_mocked_engine_adapter: t.Callable) -> AthenaEngineAdapter: return make_mocked_engine_adapter(AthenaEngineAdapter) +@pytest.fixture +def table_diff(adapter: AthenaEngineAdapter) -> TableDiff: + return TableDiff( + adapter=adapter, + source="source_table", + target="target_table", + on=["id"], + ) + + @pytest.mark.parametrize( "config_s3_warehouse_location,table_properties,table,expected_location", [ @@ -483,3 +494,74 @@ def test_iceberg_partition_transforms(adapter: AthenaEngineAdapter): # Trino syntax - CTAS """CREATE TABLE IF NOT EXISTS "test_table" WITH (table_type='iceberg', partitioning=ARRAY['MONTH(business_date)', 'BUCKET(colb, 4)', 'colc'], location='s3://bucket/prefix/test_table/', is_external=false) AS SELECT CAST("business_date" AS TIMESTAMP) AS "business_date", CAST("colb" AS VARCHAR) AS "colb", CAST("colc" AS VARCHAR) AS "colc" FROM (SELECT CAST(1 AS TIMESTAMP) AS "business_date", CAST(2 AS VARCHAR) AS "colb", 'foo' AS "colc" LIMIT 0) AS "_subquery\"""", ] + + +@pytest.mark.parametrize( + "source_format, target_format, expected_temp_format, expect_error", + [ + ("hive", "hive", None, False), + ("iceberg", "hive", None, True), # Expect error for mismatched formats + ("hive", "iceberg", None, True), # Expect error for mismatched formats + ("iceberg", "iceberg", "iceberg", False), + (None, "iceberg", None, True), # Source doesn't exist or type unknown, target is iceberg + ( + "iceberg", + None, + "iceberg", + True, + ), # Target doesn't exist or type unknown, source is iceberg + (None, "hive", None, False), # Source doesn't exist or type unknown, target is hive + ("hive", None, None, False), # Target doesn't exist or type unknown, source is hive + (None, None, None, False), # Both don't exist or types unknown + ], +) +def test_table_diff_temp_table_format( + table_diff: TableDiff, + mocker: MockerFixture, + source_format: t.Optional[str], + target_format: t.Optional[str], + expected_temp_format: t.Optional[str], + expect_error: bool, +): + adapter = t.cast(AthenaEngineAdapter, table_diff.adapter) + + # Mock _query_table_type to return specified formats + def mock_query_table_type(table_name: exp.Table) -> t.Optional[str]: + if table_name.name == "source_table": + return source_format + if table_name.name == "target_table": + return target_format + return "hive" # Default for other tables if any + + mocker.patch.object(adapter, "_query_table_type", side_effect=mock_query_table_type) + + # Mock temp_table to capture kwargs + mock_temp_table = mocker.patch.object(adapter, "temp_table", autospec=True) + mock_temp_table.return_value.__enter__.return_value = exp.to_table("diff_table") + + # Mock fetchdf and other calls made within row_diff to avoid actual DB interaction + mocker.patch.object(adapter, "fetchdf", return_value=pd.DataFrame()) + mocker.patch.object(adapter, "get_data_objects", return_value=[]) + mocker.patch.object(adapter, "columns", return_value={"id": exp.DataType.build("int")}) + + if expect_error: + with pytest.raises( + SQLMeshError, + match="do not match for Athena. Diffing between different table formats is not supported.", + ): + table_diff.row_diff() + mock_temp_table.assert_not_called() # temp_table should not be called if formats mismatch + return + + try: + table_diff.row_diff() + except Exception: + pass # We only care about the temp_table call args for non-error cases + + mock_temp_table.assert_called_once() + _, called_kwargs = mock_temp_table.call_args + + if expected_temp_format: + assert called_kwargs.get("table_format") == expected_temp_format + else: + assert "table_format" not in called_kwargs From 909548b9f420f3b6a9ee6c7f360968abfa0827f6 Mon Sep 17 00:00:00 2001 From: Nico Becker <70146154+ncbkr@users.noreply.github.com> Date: Mon, 26 May 2025 17:38:28 +0200 Subject: [PATCH 0255/1056] feat: set query label session property in bq session (#4314) --- sqlmesh/core/engine_adapter/bigquery.py | 26 ++++- sqlmesh/core/model/common.py | 1 - sqlmesh/core/model/meta.py | 38 ++++++++ tests/core/engine_adapter/test_bigquery.py | 13 +++ tests/core/test_model.py | 107 ++++++++++++++++++++- 5 files changed, 182 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 52ab2d95e8..dd0e0498e3 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -183,7 +183,31 @@ def query_factory() -> Query: def _begin_session(self, properties: SessionProperties) -> None: from google.cloud.bigquery import QueryJobConfig - job = self.client.query("SELECT 1;", job_config=QueryJobConfig(create_session=True)) + query_label_property = properties.get("query_label") + parsed_query_label: list[tuple[str, str]] = [] + if isinstance(query_label_property, (exp.Array, exp.Paren, exp.Tuple)): + label_tuples = ( + [query_label_property.unnest()] + if isinstance(query_label_property, exp.Paren) + else query_label_property.expressions + ) + + # query_label is a Paren, Array or Tuple of 2-tuples and validated at load time + parsed_query_label.extend( + (label_tuple.expressions[0].name, label_tuple.expressions[1].name) + for label_tuple in label_tuples + ) + + if parsed_query_label: + query_label_str = ",".join([":".join(label) for label in parsed_query_label]) + query = f'SET @@query_label = "{query_label_str}";SELECT 1;' + else: + query = "SELECT 1;" + + job = self.client.query( + query, + job_config=QueryJobConfig(create_session=True), + ) session_info = job.session_info session_id = session_info.session_id if session_info else None self._session_id = session_id diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index 436b4a17df..694e6d572a 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -441,7 +441,6 @@ def _executable_to_str(k: str, v: Executable) -> str: properties_validator: t.Callable = field_validator( "physical_properties_", "virtual_properties_", - "session_properties_", "materialization_properties_", mode="before", check_fields=False, diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 29800d45c7..61a657ba6b 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -17,6 +17,7 @@ default_catalog_validator, depends_on_validator, properties_validator, + parse_properties, ) from sqlmesh.core.model.kind import ( CustomKind, @@ -310,6 +311,43 @@ def _refs_validator(cls, vs: t.Any, info: ValidationInfo) -> t.List[exp.Expressi def ignored_rules_validator(cls, vs: t.Any) -> t.Any: return LinterConfig._validate_rules(vs) + @field_validator("session_properties_", mode="before") + def session_properties_validator(cls, v: t.Any, info: ValidationInfo) -> t.Any: + # use the generic properties validator to parse the session properties + parsed_session_properties = parse_properties(type(cls), v, info) + if not parsed_session_properties: + return parsed_session_properties + + for eq in parsed_session_properties: + if eq.name == "query_label": + query_label = eq.right + if not ( + isinstance(query_label, exp.Array) + or isinstance(query_label, exp.Tuple) + or isinstance(query_label, exp.Paren) + ): + raise ConfigError( + "Invalid value for `session_properties.query_label`. Must be an array or tuple." + ) + + label_tuples: t.List[exp.Expression] = ( + [query_label.unnest()] + if isinstance(query_label, exp.Paren) + else query_label.expressions + ) + + for label_tuple in label_tuples: + if not ( + isinstance(label_tuple, exp.Tuple) + and len(label_tuple.expressions) == 2 + and all(isinstance(label, exp.Literal) for label in label_tuple.expressions) + ): + raise ConfigError( + "Invalid entry in `session_properties.query_label`. Must be tuples of string literals with length 2." + ) + + return parsed_session_properties + @model_validator(mode="before") def _pre_root_validator(cls, data: t.Any) -> t.Any: if not isinstance(data, dict): diff --git a/tests/core/engine_adapter/test_bigquery.py b/tests/core/engine_adapter/test_bigquery.py index fd5499bf81..06e08d17b0 100644 --- a/tests/core/engine_adapter/test_bigquery.py +++ b/tests/core/engine_adapter/test_bigquery.py @@ -531,6 +531,7 @@ def test_begin_end_session(mocker: MockerFixture): adapter = BigQueryEngineAdapter(lambda: connection_mock, job_retries=0) + # starting a session without session properties with adapter.session({}): assert adapter._connection_pool.get_attribute("session_id") is not None adapter.execute("SELECT 2;") @@ -551,6 +552,18 @@ def test_begin_end_session(mocker: MockerFixture): assert execute_b_call[1]["query"] == "SELECT 3;" assert not execute_b_call[1]["job_config"].connection_properties + # starting a new session with session property query_label and array value + with adapter.session({"query_label": parse_one("[('key1', 'value1'), ('key2', 'value2')]")}): + adapter.execute("SELECT 4;") + begin_new_session_call = connection_mock._client.query.call_args_list[3] + assert begin_new_session_call[0][0] == 'SET @@query_label = "key1:value1,key2:value2";SELECT 1;' + + # starting a new session with session property query_label and Paren value + with adapter.session({"query_label": parse_one("(('key1', 'value1'))")}): + adapter.execute("SELECT 5;") + begin_new_session_call = connection_mock._client.query.call_args_list[5] + assert begin_new_session_call[0][0] == 'SET @@query_label = "key1:value1";SELECT 1;' + def _to_sql_calls(execute_mock: t.Any, identify: bool = True) -> t.List[str]: output = [] diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 735242743f..f5d604eeae 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -3757,7 +3757,10 @@ def my_model(context, **kwargs): """('key_a' = 'value_a', 'key_b' = 1, 'key_c' = TRUE, 'key_d' = 2.0)""" ) - with pytest.raises(ConfigError, match=r"Invalid property 'invalid'.*"): + with pytest.raises( + ConfigError, + match=r"Invalid property 'invalid'. Properties must be specified as key-value pairs = . ", + ): load_sql_based_model( d.parse( """ @@ -4418,6 +4421,108 @@ def test_model_session_properties(sushi_context): "warehouse": "test_warehouse", } + model = load_sql_based_model( + d.parse( + """ + MODEL ( + name test_schema.test_model, + session_properties ( + 'query_label' = [ + ('key1', 'value1'), + ('key2', 'value2') + ] + ) + ); + SELECT a FROM tbl; + """, + default_dialect="bigquery", + ) + ) + assert model.session_properties == { + "query_label": parse_one("[('key1', 'value1'), ('key2', 'value2')]") + } + + model = load_sql_based_model( + d.parse( + """ + MODEL ( + name test_schema.test_model, + session_properties ( + 'query_label' = ( + ('key1', 'value1') + ) + ) + ); + SELECT a FROM tbl; + """, + default_dialect="bigquery", + ) + ) + assert model.session_properties == {"query_label": parse_one("(('key1', 'value1'))")} + + with pytest.raises( + ConfigError, + match=r"Invalid value for `session_properties.query_label`. Must be an array or tuple.", + ): + load_sql_based_model( + d.parse( + """ + MODEL ( + name test_schema.test_model, + session_properties ( + 'query_label' = 'invalid value' + ) + ); + SELECT a FROM tbl; + """, + default_dialect="bigquery", + ) + ) + + with pytest.raises( + ConfigError, + match=r"Invalid entry in `session_properties.query_label`. Must be tuples of string literals with length 2.", + ): + load_sql_based_model( + d.parse( + """ + MODEL ( + name test_schema.test_model, + session_properties ( + 'query_label' = ( + ('key1', 'value1', 'another_value') + ) + ) + ); + SELECT a FROM tbl; + """, + default_dialect="bigquery", + ) + ) + + with pytest.raises( + ConfigError, + match=r"Invalid entry in `session_properties.query_label`. Must be tuples of string literals with length 2.", + ): + load_sql_based_model( + d.parse( + """ + MODEL ( + name test_schema.test_model, + session_properties ( + 'query_label' = ( + 'some value', + 'another value', + 'yet another value', + ) + ) + ); + SELECT a FROM tbl; + """, + default_dialect="bigquery", + ) + ) + def test_model_jinja_macro_rendering(): expressions = d.parse( From 53b06e45d12446ea25fcc64e05c37ba18ef0bad7 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Mon, 26 May 2025 18:56:03 +0300 Subject: [PATCH 0256/1056] Chore: Ensure model search fails gracefully (#4535) --- sqlmesh/core/context.py | 20 ++++++++++++++------ tests/core/test_integration.py | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index b5c9f7b65d..80a35f597a 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -869,7 +869,12 @@ def get_model( Returns: The expected model. """ - if isinstance(model_or_snapshot, str): + if isinstance(model_or_snapshot, Snapshot): + return model_or_snapshot.model + if not isinstance(model_or_snapshot, str): + return model_or_snapshot + + try: # We should try all dialects referenced in the project for cases when models use mixed dialects. for dialect in self._all_dialects: normalized_name = normalize_model_name( @@ -879,13 +884,16 @@ def get_model( ) if normalized_name in self._models: return self._models[normalized_name] - elif isinstance(model_or_snapshot, Snapshot): - return model_or_snapshot.model - else: - return model_or_snapshot + except: + pass if raise_if_missing: - raise SQLMeshError(f"Cannot find model for '{model_or_snapshot}'") + if model_or_snapshot.endswith((".sql", ".py")): + msg = "Resolving models by path is not supported, please pass in the model name instead." + else: + msg = f"Cannot find model with name '{model_or_snapshot}'" + + raise SQLMeshError(msg) return None diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 576fc4128d..8f6af61a64 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -6148,3 +6148,24 @@ def test_missing_connection_config(): ctx = Context( config=Config(gateways={"default": GatewayConfig(connection=DuckDBConnectionConfig())}) ) + + +@use_terminal_console +def test_render_path_instead_of_model(tmp_path: Path): + create_temp_file(tmp_path, Path("models/test.sql"), "MODEL (name test_model); SELECT 1 AS col") + ctx = Context(paths=tmp_path, config=Config()) + + # Case 1: Fail gracefully when the user is passing in a path instead of a model name + for test_model in ["models/test.sql", "models/test.py"]: + with pytest.raises( + SQLMeshError, + match="Resolving models by path is not supported, please pass in the model name instead.", + ): + ctx.render(test_model) + + # Case 2: Fail gracefully when the model name is not found + with pytest.raises(SQLMeshError, match="Cannot find model with name 'incorrect_model'"): + ctx.render("incorrect_model") + + # Case 3: Render the model successfully + assert ctx.render("test_model").sql() == 'SELECT 1 AS "col"' From 780ebae4ea849e46221ca332e8a297edb36d2020 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 26 May 2025 19:02:57 +0200 Subject: [PATCH 0257/1056] chore: add blueprint model to sushi (#4537) --- examples/sushi/models/blueprint.sql | 21 ++++++++ tests/core/analytics/test_collector.py | 2 +- tests/core/test_context.py | 2 +- tests/core/test_integration.py | 50 ++++++++++++++----- tests/core/test_plan.py | 2 + .../github/cicd/test_integration.py | 4 +- tests/integrations/jupyter/test_magics.py | 4 +- 7 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 examples/sushi/models/blueprint.sql diff --git a/examples/sushi/models/blueprint.sql b/examples/sushi/models/blueprint.sql new file mode 100644 index 0000000000..54f797dba4 --- /dev/null +++ b/examples/sushi/models/blueprint.sql @@ -0,0 +1,21 @@ +MODEL ( + name @name, + kind FULL, + description "Count of customers by status, done with a fancy unnecessary blueprint", + grain status, + blueprints ( + ( + name := sushi.count_customers_active, + blueprint_status := 'active', + ), + ( + name := sushi.count_customers_inactive, + blueprint_status := 'inactive', + ) + ) +); + +SELECT + COUNT(customer_id) AS count_customers +FROM sushi.customers +WHERE status = @blueprint_status; \ No newline at end of file diff --git a/tests/core/analytics/test_collector.py b/tests/core/analytics/test_collector.py index b82e9cd5cd..957db3a003 100644 --- a/tests/core/analytics/test_collector.py +++ b/tests/core/analytics/test_collector.py @@ -183,7 +183,7 @@ def test_on_plan_apply( { "seq_num": 0, "event_type": "PLAN_APPLY_START", - "event": f'{{"plan_id": "{plan_id}", "engine_type": "bigquery", "state_sync_type": "mysql", "scheduler_type": "builtin", "is_dev": false, "skip_backfill": false, "no_gaps": false, "forward_only": false, "ensure_finalized_snapshots": false, "has_restatements": false, "directly_modified_count": 19, "indirectly_modified_count": 0, "environment_name_hash": "d6e4a9b6646c62fc48baa6dd6150d1f7"}}', + "event": f'{{"plan_id": "{plan_id}", "engine_type": "bigquery", "state_sync_type": "mysql", "scheduler_type": "builtin", "is_dev": false, "skip_backfill": false, "no_gaps": false, "forward_only": false, "ensure_finalized_snapshots": false, "has_restatements": false, "directly_modified_count": 21, "indirectly_modified_count": 0, "environment_name_hash": "d6e4a9b6646c62fc48baa6dd6150d1f7"}}', **common_fields, } ), diff --git a/tests/core/test_context.py b/tests/core/test_context.py index d55c5c4285..3f732911d5 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -818,7 +818,7 @@ def test_janitor(sushi_context, mocker: MockerFixture) -> None: ) # Assert that the views are dropped for each snapshot just once and make sure that the name used is the # view name with the environment as a suffix - assert adapter_mock.drop_view.call_count == 14 + assert adapter_mock.drop_view.call_count == 16 adapter_mock.drop_view.assert_has_calls( [ call( diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 8f6af61a64..e5e7366760 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -476,10 +476,16 @@ def test_full_history_restatement_model_regular_plan_preview_enabled( waiter_as_customer_snapshot = context.get_snapshot( "sushi.waiter_as_customer_by_day", raise_if_missing=True ) + count_customers_active_snapshot = context.get_snapshot( + "sushi.count_customers_active", raise_if_missing=True + ) + count_customers_inactive_snapshot = context.get_snapshot( + "sushi.count_customers_inactive", raise_if_missing=True + ) plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() - assert len(plan.new_snapshots) == 4 + assert len(plan.new_snapshots) == 6 assert ( plan.context_diff.snapshots[snapshot.snapshot_id].change_category == SnapshotChangeCategory.FORWARD_ONLY @@ -505,6 +511,18 @@ def test_full_history_restatement_model_regular_plan_preview_enabled( (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), ], ), + SnapshotIntervals( + snapshot_id=count_customers_active_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=count_customers_inactive_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), SnapshotIntervals( snapshot_id=customers_snapshot.snapshot_id, intervals=[ @@ -1517,6 +1535,8 @@ def test_run_with_select_models( "assert_item_price_above_zero": to_timestamp("2023-01-08"), '"memory"."sushi"."active_customers"': to_timestamp("2023-01-08"), '"memory"."sushi"."customers"': to_timestamp("2023-01-08"), + '"memory"."sushi"."count_customers_active"': to_timestamp("2023-01-08"), + '"memory"."sushi"."count_customers_inactive"': to_timestamp("2023-01-08"), } @@ -1555,6 +1575,8 @@ def test_plan_with_run( "assert_item_price_above_zero": to_timestamp("2023-01-09"), '"memory"."sushi"."active_customers"': to_timestamp("2023-01-09"), '"memory"."sushi"."customers"': to_timestamp("2023-01-09"), + '"memory"."sushi"."count_customers_active"': to_timestamp("2023-01-09"), + '"memory"."sushi"."count_customers_inactive"': to_timestamp("2023-01-09"), } @@ -1592,6 +1614,8 @@ def test_run_with_select_models_no_auto_upstream( "assert_item_price_above_zero": to_timestamp("2023-01-08"), '"memory"."sushi"."active_customers"': to_timestamp("2023-01-08"), '"memory"."sushi"."customers"': to_timestamp("2023-01-08"), + '"memory"."sushi"."count_customers_active"': to_timestamp("2023-01-08"), + '"memory"."sushi"."count_customers_inactive"': to_timestamp("2023-01-08"), } @@ -5185,7 +5209,7 @@ def test_environment_suffix_target_table(init_and_plan_context: t.Callable): assert set(metadata.schemas) - starting_schemas == {"raw"} prod_views = {x for x in metadata.qualified_views if x.db in environments_schemas} # Make sure that all models are present - assert len(prod_views) == 14 + assert len(prod_views) == 16 apply_to_environment(context, "dev") # Make sure no new schemas are created assert set(metadata.schemas) - starting_schemas == {"raw"} @@ -5243,9 +5267,9 @@ def get_default_catalog_and_non_tables( user_default_tables, non_default_tables, ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) - assert len(prod_views) == 14 + assert len(prod_views) == 16 assert len(dev_views) == 0 - assert len(user_default_tables) == 17 + assert len(user_default_tables) == 21 assert state_metadata.schemas == ["sqlmesh"] assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( { @@ -5262,9 +5286,9 @@ def get_default_catalog_and_non_tables( user_default_tables, non_default_tables, ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) - assert len(prod_views) == 14 - assert len(dev_views) == 14 - assert len(user_default_tables) == 17 + assert len(prod_views) == 16 + assert len(dev_views) == 16 + assert len(user_default_tables) == 21 assert len(non_default_tables) == 0 assert state_metadata.schemas == ["sqlmesh"] assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( @@ -5282,9 +5306,9 @@ def get_default_catalog_and_non_tables( user_default_tables, non_default_tables, ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) - assert len(prod_views) == 14 - assert len(dev_views) == 28 - assert len(user_default_tables) == 17 + assert len(prod_views) == 16 + assert len(dev_views) == 32 + assert len(user_default_tables) == 21 assert len(non_default_tables) == 0 assert state_metadata.schemas == ["sqlmesh"] assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( @@ -5303,9 +5327,9 @@ def get_default_catalog_and_non_tables( user_default_tables, non_default_tables, ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) - assert len(prod_views) == 14 - assert len(dev_views) == 14 - assert len(user_default_tables) == 17 + assert len(prod_views) == 16 + assert len(dev_views) == 16 + assert len(user_default_tables) == 21 assert len(non_default_tables) == 0 assert state_metadata.schemas == ["sqlmesh"] assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 6daec19554..1c0830085d 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -832,6 +832,8 @@ def test_restate_models(sushi_context_pre_scheduling: Context): '"memory"."sushi"."waiter_as_customer_by_day"', '"memory"."sushi"."waiter_names"', '"memory"."sushi"."waiters"', + '"memory"."sushi"."count_customers_active"', + '"memory"."sushi"."count_customers_inactive"', } diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index 3c5f37db64..732780d3e4 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -1705,6 +1705,8 @@ def test_overlapping_changes_models( **Indirectly Modified:** - `sushi.active_customers` +- `sushi.count_customers_active` +- `sushi.count_customers_inactive` - `sushi.waiter_as_customer_by_day` """ @@ -1744,7 +1746,7 @@ def test_overlapping_changes_models( """ ) - assert len(get_environment_objects(controller, "hello_world_2")) == 4 + assert len(get_environment_objects(controller, "hello_world_2")) == 6 assert "new_col" in get_columns(controller, "hello_world_2", "customers") assert mock_pull_request.merge.called diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index ab19607bf0..ac9be5cc9c 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -756,7 +756,7 @@ def test_info(notebook, sushi_context, convert_all_html_output_to_text, get_all_ assert not output.stderr assert len(output.outputs) == 6 assert convert_all_html_output_to_text(output) == [ - "Models: 18", + "Models: 20", "Macros: 8", "", "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", @@ -764,7 +764,7 @@ def test_info(notebook, sushi_context, convert_all_html_output_to_text, get_all_ "Data warehouse connection succeeded", ] assert get_all_html_output(output) == [ - "
Models: 18
", + "
Models: 20
", "
Macros: 8
", "
",
         '
Connection:  type: duckdb  concurrent_tasks: 1  register_comments: true  pre_ping: false  pretty_sql: false  extensions: []  connector_config: {}  secrets: None
', From a6992e010b184a7f2b3ee2259d8b315f950dae2a Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 26 May 2025 19:20:55 +0200 Subject: [PATCH 0258/1056] feat(vscode): render model (#4533) --- sqlmesh/lsp/context.py | 38 +++++++++ sqlmesh/lsp/custom.py | 26 ++++++ sqlmesh/lsp/main.py | 17 +++- vscode/extension/package.json | 17 +++- vscode/extension/src/commands/renderModel.ts | 90 ++++++++++++++++++++ vscode/extension/src/extension.ts | 8 ++ vscode/extension/src/lsp/custom.ts | 26 +++++- vscode/extension/tests/lineage.spec.ts | 45 +--------- vscode/extension/tests/render.spec.ts | 38 +++++++++ vscode/extension/tests/utils.ts | 50 +++++++++++ 10 files changed, 307 insertions(+), 48 deletions(-) create mode 100644 vscode/extension/src/commands/renderModel.ts create mode 100644 vscode/extension/tests/render.spec.ts create mode 100644 vscode/extension/tests/utils.ts diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 928073ef16..89ca236563 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -3,6 +3,10 @@ from sqlmesh.core.context import Context import typing as t +from sqlmesh.core.model.definition import SqlModel +from sqlmesh.lsp.custom import RenderModelEntry +from sqlmesh.lsp.uri import URI + @dataclass class ModelTarget: @@ -49,3 +53,37 @@ def __init__(self, context: Context) -> None: **model_map, **audit_map, } + + +def render_model(context: LSPContext, uri: URI) -> t.Iterator[RenderModelEntry]: + target = context.map[uri.to_path()] + if isinstance(target, AuditTarget): + audit = context.context.standalone_audits[target.name] + definition = audit.render_definition( + include_python=False, + render_query=True, + ) + rendered_query = [render.sql(dialect=audit.dialect, pretty=True) for render in definition] + yield RenderModelEntry( + name=audit.name, + fqn=audit.fqn, + description=audit.description, + rendered_query="\n\n".join(rendered_query), + ) + if isinstance(target, ModelTarget): + for name in target.names: + model = context.context.get_model(name) + if isinstance(model, SqlModel): + rendered_query = [ + render.sql(dialect=model.dialect, pretty=True) + for render in model.render_definition( + include_python=False, + render_query=True, + ) + ] + yield RenderModelEntry( + name=model.name, + fqn=model.fqn, + description=model.description, + rendered_query="\n\n".join(rendered_query), + ) diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py index b8133f3b55..6021d225ab 100644 --- a/sqlmesh/lsp/custom.py +++ b/sqlmesh/lsp/custom.py @@ -20,3 +20,29 @@ class AllModelsResponse(PydanticModel): models: t.List[str] keywords: t.List[str] + + +RENDER_MODEL_FEATURE = "sqlmesh/render_model" + + +class RenderModelRequest(PydanticModel): + textDocumentUri: str + + +class RenderModelEntry(PydanticModel): + """ + An entry in the rendered model. + """ + + name: str + fqn: str + description: str + rendered_query: str + + +class RenderModelResponse(PydanticModel): + """ + Response to render a model. + """ + + models: t.List[RenderModelEntry] diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 69b5252e93..260a640b16 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -20,8 +20,15 @@ ApiResponseGetModels, ) from sqlmesh.lsp.completions import get_sql_completions -from sqlmesh.lsp.context import LSPContext, ModelTarget -from sqlmesh.lsp.custom import ALL_MODELS_FEATURE, AllModelsRequest, AllModelsResponse +from sqlmesh.lsp.context import LSPContext, ModelTarget, render_model as render_model_context +from sqlmesh.lsp.custom import ( + ALL_MODELS_FEATURE, + RENDER_MODEL_FEATURE, + AllModelsRequest, + AllModelsResponse, + RenderModelRequest, + RenderModelResponse, +) from sqlmesh.lsp.reference import ( get_references, ) @@ -88,6 +95,12 @@ def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsRespons except Exception as e: return get_sql_completions(None, uri) + @self.server.feature(RENDER_MODEL_FEATURE) + def render_model(ls: LanguageServer, params: RenderModelRequest) -> RenderModelResponse: + uri = URI(params.textDocumentUri) + context = self._context_get_or_load(uri) + return RenderModelResponse(models=list(render_model_context(context, uri))) + @self.server.feature(API_FEATURE) def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]: ls.log_trace(f"API request: {request}") diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 536b9e1203..f3b44fec26 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -87,8 +87,23 @@ "command": "sqlmesh.signout", "title": "Sign out from Tobiko Cloud", "description": "SQLMesh" + }, + { + "command": "sqlmesh.renderModel", + "title": "Render Model", + "description": "Render the model in the current editor", + "icon": "$(open-preview)" } - ] + ], + "menus": { + "editor/title": [ + { + "command": "sqlmesh.renderModel", + "when": "resourceExtname == .sql", + "group": "navigation" + } + ] + } }, "scripts": { "ci": "pnpm run lint && pnpm run compile", diff --git a/vscode/extension/src/commands/renderModel.ts b/vscode/extension/src/commands/renderModel.ts new file mode 100644 index 0000000000..4c888d16c3 --- /dev/null +++ b/vscode/extension/src/commands/renderModel.ts @@ -0,0 +1,90 @@ +import * as vscode from 'vscode' +import { LSPClient } from '../lsp/lsp' +import { isErr } from '@bus/result' +import { RenderModelEntry } from '../lsp/custom' + +export function renderModel(lspClient?: LSPClient) { + return async () => { + // Get the current active editor + const activeEditor = vscode.window.activeTextEditor + + if (!activeEditor) { + vscode.window.showErrorMessage('No active editor found') + return + } + + if (!lspClient) { + vscode.window.showErrorMessage('LSP client not available') + return + } + + // Get the current document URI + const documentUri = activeEditor.document.uri.toString(true) + + // Call the render model API + const result = await lspClient.call_custom_method('sqlmesh/render_model', { + textDocumentUri: documentUri, + }) + + if (isErr(result)) { + vscode.window.showErrorMessage(`Failed to render model: ${result.error}`) + return + } + + // Check if we got any models + if (!result.value.models || result.value.models.length === 0) { + vscode.window.showInformationMessage( + 'No models found in the current file', + ) + return + } + + // If multiple models, let user choose + let selectedModel: RenderModelEntry + if (result.value.models.length > 1) { + const items = result.value.models.map((model: RenderModelEntry) => ({ + label: model.name, + description: model.fqn, + detail: model.description, + model: model, + })) + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a model to render', + }) + + if (!selected) { + return + } + + selectedModel = selected.model + } else { + selectedModel = result.value.models[0] + } + + // Create a new untitled document with the rendered SQL + const document = await vscode.workspace.openTextDocument({ + language: 'sql', + content: selectedModel.rendered_query, + }) + + // Determine the view column for side-by-side display + let viewColumn: vscode.ViewColumn + if (activeEditor) { + // Open beside the current editor + viewColumn = activeEditor.viewColumn + ? activeEditor.viewColumn + 1 + : vscode.ViewColumn.Two + } else { + // If no active editor, open in column two + viewColumn = vscode.ViewColumn.Two + } + + // Open the document in the editor as a preview (preview: true is default) + await vscode.window.showTextDocument(document, { + viewColumn: viewColumn, + preview: true, + preserveFocus: false, + }) + } +} diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 65a28551ae..77c0f175cd 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -12,6 +12,7 @@ import { AuthenticationProviderTobikoCloud } from './auth/auth' import { signOut } from './commands/signout' import { signIn } from './commands/signin' import { signInSpecifyFlow } from './commands/signinSpecifyFlow' +import { renderModel } from './commands/renderModel' import { isErr } from '@bus/result' import { handleNotSginedInError, @@ -64,6 +65,13 @@ export async function activate(context: vscode.ExtensionContext) { lspClient = new LSPClient() + context.subscriptions.push( + vscode.commands.registerCommand( + 'sqlmesh.renderModel', + renderModel(lspClient), + ), + ) + context.subscriptions.push( vscode.languages.registerCompletionItemProvider( selector, diff --git a/vscode/extension/src/lsp/custom.ts b/vscode/extension/src/lsp/custom.ts index d0fdf8b8a2..7d16c5c7c8 100644 --- a/vscode/extension/src/lsp/custom.ts +++ b/vscode/extension/src/lsp/custom.ts @@ -4,8 +4,32 @@ export interface AllModelsMethod { response: AllModelsResponse } +export interface RenderModelMethod { + method: 'sqlmesh/render_model' + request: RenderModelRequest + response: RenderModelResponse +} + +interface RenderModelRequest { + textDocumentUri: string +} + +interface RenderModelResponse { + models: RenderModelEntry[] +} + +export interface RenderModelEntry { + name: string + fqn: string + description: string + rendered_query: string +} + // @eslint-disable-next-line @typescript-eslint/consistent-type-definition -export type CustomLSPMethods = AllModelsMethod | AbstractAPICall +export type CustomLSPMethods = + | AllModelsMethod + | AbstractAPICall + | RenderModelMethod interface AllModelsRequest { textDocument: { diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index 5bac457ef9..ccb2203006 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -2,13 +2,8 @@ import { test, _electron as electron, expect, ElectronApplication, Page } from ' import path from 'path'; import fs from 'fs-extra'; import os from 'os'; +import { startVSCode, SUSHI_SOURCE_PATH } from './utils'; -// Absolute path to the VS Code executable you downloaded in step 1. -const VS_CODE_EXE = fs.readJsonSync('.vscode-test/paths.json').executablePath; -// Where your extension lives on disk -const EXT_PATH = path.resolve(__dirname, '..'); -// Where the sushi project lives which we copy from -const SUSHI_SOURCE_PATH = path.join(__dirname, '..', '..', '..', 'examples', 'sushi'); /** * Helper function to launch VS Code and test lineage with given project path config @@ -26,44 +21,6 @@ async function testLineageWithProjectPath( await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 }); } -/** - * Launch VS Code and return the window and a function to close the app. - * @param workspaceDir The workspace directory to open. - * @returns The window and a function to close the app. - */ -export const startVSCode = async (workspaceDir: string): Promise<{ - window: Page, - close: () => Promise, -}> => { - const userDataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-user-data-')); - const ciArgs = process.env.CI ? [ - '--disable-gpu', - '--headless', - '--no-sandbox', - '--disable-dev-shm-usage', - '--window-position=-10000,0', - ] : []; - const args = [ - ...ciArgs, - `--extensionDevelopmentPath=${EXT_PATH}`, - '--disable-workspace-trust', - '--disable-telemetry', - `--user-data-dir=${userDataDir}`, - workspaceDir, - ]; - const electronApp = await electron.launch({ - executablePath: VS_CODE_EXE, - args, - }); - const window = await electronApp.firstWindow(); - await window.waitForLoadState('domcontentloaded'); - await window.waitForLoadState('networkidle'); - await window.waitForTimeout(2_000); - return { window, close: async () => { - await electronApp.close(); - await fs.remove(userDataDir); - } }; -} test('Lineage panel renders correctly - no project path config (default)', async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts new file mode 100644 index 0000000000..9b7f929271 --- /dev/null +++ b/vscode/extension/tests/render.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; +import fs from 'fs-extra'; +import os from 'os'; +import { startVSCode, SUSHI_SOURCE_PATH } from './utils'; + +test('Render works correctly', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); + await fs.copy(SUSHI_SOURCE_PATH, tempDir); + + try { + const { window, close } = await startVSCode(tempDir); + + // Wait for the models folder to be visible + await window.waitForSelector('text=models'); + + // Click on the models folder, excluding external_models + await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); + + // Open the customer_revenue_lifetime model + await window.getByRole('treeitem', { name: 'customers.sql', exact: true }).locator('a').click(); + + await window.waitForSelector('text=grain'); + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Render the model + await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); + await window.keyboard.type('Render Model'); + await window.keyboard.press('Enter'); + + // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window + await expect(window.locator('text="marketing"."customer_id" AS')).toBeVisible(); + + await close(); + } finally { + await fs.remove(tempDir); + } + }); \ No newline at end of file diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts new file mode 100644 index 0000000000..bceceb0f70 --- /dev/null +++ b/vscode/extension/tests/utils.ts @@ -0,0 +1,50 @@ +import path from 'path'; +import fs from 'fs-extra'; +import os from 'os'; +import { _electron as electron, Page } from '@playwright/test'; + +// Absolute path to the VS Code executable you downloaded in step 1. +export const VS_CODE_EXE = fs.readJsonSync('.vscode-test/paths.json').executablePath; +// Where your extension lives on disk +export const EXT_PATH = path.resolve(__dirname, '..'); +// Where the sushi project lives which we copy from +export const SUSHI_SOURCE_PATH = path.join(__dirname, '..', '..', '..', 'examples', 'sushi'); + +/** + * Launch VS Code and return the window and a function to close the app. + * @param workspaceDir The workspace directory to open. + * @returns The window and a function to close the app. + */ +export const startVSCode = async (workspaceDir: string): Promise<{ + window: Page, + close: () => Promise, + }> => { + const userDataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-user-data-')); + const ciArgs = process.env.CI ? [ + '--disable-gpu', + '--headless', + '--no-sandbox', + '--disable-dev-shm-usage', + '--window-position=-10000,0', + ] : []; + const args = [ + ...ciArgs, + `--extensionDevelopmentPath=${EXT_PATH}`, + '--disable-workspace-trust', + '--disable-telemetry', + `--user-data-dir=${userDataDir}`, + workspaceDir, + ]; + const electronApp = await electron.launch({ + executablePath: VS_CODE_EXE, + args, + }); + const window = await electronApp.firstWindow(); + await window.waitForLoadState('domcontentloaded'); + await window.waitForLoadState('networkidle'); + await window.waitForTimeout(2_000); + return { window, close: async () => { + await electronApp.close(); + await fs.remove(userDataDir); + } }; + } From 66b42523a280513ab975472b1a1dc5af6f5e166f Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 26 May 2025 21:14:58 +0300 Subject: [PATCH 0259/1056] Feat: more cores for loading (#4427) Co-authored-by: tobymao --- Makefile | 6 +- sqlmesh/core/loader.py | 275 +++++++++++++++++++++++---------- sqlmesh/core/model/cache.py | 28 ++-- sqlmesh/core/model/schema.py | 47 ++---- sqlmesh/core/snapshot/cache.py | 27 ++-- sqlmesh/dbt/loader.py | 13 ++ sqlmesh/utils/process.py | 73 +++++++++ tests/conftest.py | 2 +- tests/core/test_integration.py | 10 +- tests/core/test_loader.py | 6 +- tests/core/test_model.py | 31 ++-- tests/test_forking.py | 33 +++- 12 files changed, 388 insertions(+), 163 deletions(-) create mode 100644 sqlmesh/utils/process.py diff --git a/Makefile b/Makefile index c99936eff7..a2534a3a42 100644 --- a/Makefile +++ b/Makefile @@ -64,13 +64,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 -n auto -m "fast and not cicdonly" --junitxml=test-results/junit-fast-test.xml && pytest -m "isolated" && pytest -m "registry_isolation" slow-test: - pytest -n auto -m "(fast or slow) and not cicdonly" && pytest -m "isolated" + pytest -n auto -m "(fast or slow) and not cicdonly" && pytest -m "isolated" && pytest -m "registry_isolation" cicd-test: - pytest -n auto -m "fast or slow" --junitxml=test-results/junit-cicd.xml && pytest -m "isolated" + pytest -n auto -m "fast or slow" --junitxml=test-results/junit-cicd.xml && pytest -m "isolated" && pytest -m "registry_isolation" core-fast-test: pytest -n auto -m "fast and not web and not github and not dbt and not jupyter" diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index 4fec342f2a..5965e6c1b4 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -4,7 +4,6 @@ import glob import itertools import linecache -import logging import os import re import typing as t @@ -12,6 +11,7 @@ from dataclasses import dataclass from pathlib import Path from pydantic import ValidationError +import concurrent.futures from sqlglot.errors import SqlglotError from sqlglot import exp @@ -19,6 +19,7 @@ from sqlmesh.core import constants as c from sqlmesh.core.audit import Audit, ModelAudit, StandaloneAudit, load_multiple_audits +from sqlmesh.core.console import Console from sqlmesh.core.dialect import parse from sqlmesh.core.environment import EnvironmentStatements from sqlmesh.core.linter.rule import Rule @@ -28,7 +29,6 @@ from sqlmesh.core.model import ( Model, ModelCache, - SeedModel, create_external_model, load_sql_based_models, ) @@ -41,6 +41,7 @@ from sqlmesh.utils.jinja import JinjaMacroRegistry, MacroExtractor from sqlmesh.utils.metaprogramming import import_python_file from sqlmesh.utils.pydantic import validation_error_message +from sqlmesh.utils.process import create_process_pool_executor from sqlmesh.utils.yaml import YAML, load as yaml_load @@ -48,8 +49,6 @@ from sqlmesh.core.context import GenericContext -logger = logging.getLogger(__name__) - GATEWAY_PATTERN = re.compile(r"gateway:\s*([^\s]+)") @@ -73,17 +72,113 @@ def get_or_load_models( self, target_path: Path, loader: t.Callable[[], t.List[Model]] ) -> t.List[Model]: """Get or load all models from cache.""" + pass + + @abc.abstractmethod + def put(self, models: t.List[Model], path: Path) -> bool: + """Store models in the cache associated with the given path. + + Args: + models: List of models to cache + path: File path to associate with the cached models + + Returns: + True if the models were successfully cached, + False otherwise (empty list, not a list, unsupported model types) + """ + pass + + @abc.abstractmethod + def get(self, path: Path) -> t.List[Model]: + """Retrieve models from the cache for a given path. + + Args: + path: File path to look up in the cache + + Returns: + List of cached models associated with the path, an empty list if no cache entry exists + """ + pass + + +_defaults: t.Optional[t.Dict[str, t.Any]] = None +_cache: t.Optional[CacheBase] = None +_config_essentials: t.Optional[t.Dict[str, t.Any]] = None +_selected_gateway: t.Optional[str] = None + + +def _init_model_defaults( + config_essentials: t.Dict[str, t.Any], + selected_gateway: t.Optional[str], + model_loading_defaults: t.Optional[t.Dict[str, t.Any]] = None, + cache: t.Optional[CacheBase] = None, + console: t.Optional[Console] = None, +) -> None: + global _defaults, _cache, _config_essentials, _selected_gateway + _defaults = model_loading_defaults + _cache = cache + _config_essentials = config_essentials + _selected_gateway = selected_gateway + + # Set the console passed from the parent process + if console is not None: + from sqlmesh.core.console import set_console + + set_console(console) + + +def load_sql_models(path: Path) -> t.List[Model]: + assert _defaults + assert _cache + + with open(path, "r", encoding="utf-8") as file: + expressions = parse(file.read(), default_dialect=_defaults["dialect"]) + models = load_sql_based_models(expressions, path=Path(path).absolute(), **_defaults) + + return [] if _cache.put(models, path) else models + + +def get_variables(gateway_name: t.Optional[str] = None) -> t.Dict[str, t.Any]: + assert _config_essentials + + gateway_name = gateway_name or _selected_gateway + + try: + gateway = _config_essentials["gateways"].get(gateway_name) + except ConfigError: + from sqlmesh.core.console import get_console + + get_console().log_warning( + f"Gateway '{gateway_name}' not found in project '{_config_essentials['project']}'." + ) + gateway = None + + return { + **_config_essentials["variables"], + **(gateway.variables if gateway else {}), + c.GATEWAY: gateway_name, + } class Loader(abc.ABC): """Abstract base class to load macros and models for a context""" def __init__(self, context: GenericContext, path: Path) -> None: + from sqlmesh.core.console import get_console + self._path_mtimes: t.Dict[Path, float] = {} self.context = context self.config_path = path self.config = self.context.configs[self.config_path] self._variables_by_gateway: t.Dict[str, t.Dict[str, t.Any]] = {} + self._console = get_console() + + self.config_essentials = { + "project": self.config.project, + "variables": self.config.variables, + "gateways": self.config.gateways, + } + _init_model_defaults(self.config_essentials, self.context.selected_gateway) def load(self) -> LoadedProject: """ @@ -243,19 +338,21 @@ def _load(path: Path) -> t.List[Model]: for row in YAML().load(file.read()) ] except Exception as ex: - self._raise_failed_to_load_model_error(path, ex) - raise + raise ConfigError(self._failed_to_load_model_error(path, ex)) for path in paths_to_load: self._track_file(path) external_models = cache.get_or_load_models(path, lambda: _load(path)) + # external models with no explicit gateway defined form the base set for model in external_models: if model.gateway is None: if model.fqn in models: - self._raise_failed_to_load_model_error( - path, f"Duplicate external model name: '{model.name}'." + raise ConfigError( + self._failed_to_load_model_error( + path, f"Duplicate external model name: '{model.name}'." + ) ) models[model.fqn] = model @@ -264,8 +361,10 @@ def _load(path: Path) -> t.List[Model]: for model in external_models: if model.gateway == gateway: if model.fqn in models and models[model.fqn].gateway == gateway: - self._raise_failed_to_load_model_error( - path, f"Duplicate external model name: '{model.name}'." + raise ConfigError( + self._failed_to_load_model_error( + path, f"Duplicate external model name: '{model.name}'." + ) ) models.update({model.fqn: model}) @@ -352,33 +451,11 @@ def _track_file(self, path: Path) -> None: """Project file to track for modifications""" self._path_mtimes[path] = path.stat().st_mtime - def _get_variables(self, gateway_name: t.Optional[str] = None) -> t.Dict[str, t.Any]: - gateway_name = gateway_name or self.context.selected_gateway - - if gateway_name not in self._variables_by_gateway: - try: - gateway = self.config.get_gateway(gateway_name) - except ConfigError: - from sqlmesh.core.console import get_console - - get_console().log_warning( - f"Gateway '{gateway_name}' not found in project '{self.config.project}'." - ) - gateway = None - - self._variables_by_gateway[gateway_name] = { - **self.config.variables, - **(gateway.variables if gateway else {}), - c.GATEWAY: gateway_name, - } - - return self._variables_by_gateway[gateway_name] - - def _raise_failed_to_load_model_error(self, path: Path, error: t.Union[str, Exception]) -> None: + def _failed_to_load_model_error(self, path: Path, error: t.Union[str, Exception]) -> str: base_message = f"Failed to load model definition at '{path}':" if isinstance(error, ValidationError): - raise ConfigError(validation_error_message(error, base_message)) - raise ConfigError(f"{base_message}\n {error}") + return validation_error_message(error, base_message) + return f"{base_message}\n {error}" class SqlMeshLoader(Loader): @@ -442,14 +519,15 @@ def _load_models( 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) + + sql_models = self._load_sql_models(macros, jinja_macros, audits, signals, cache, gateway) external_models = self._load_external_models(audits, cache, gateway) python_models = self._load_python_models(macros, jinja_macros, audits, signals) 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 ValueError(f"Duplicate model name(s) found: {', '.join(duplicates)}.") + raise ConfigError(f"Duplicate model name(s) found: {', '.join(duplicates)}.") return UniqueKeyDict("models", **sql_models, **external_models, **python_models) @@ -460,9 +538,12 @@ def _load_sql_models( audits: UniqueKeyDict[str, ModelAudit], signals: UniqueKeyDict[str, signal], cache: CacheBase, + gateway: t.Optional[str], ) -> UniqueKeyDict[str, Model]: """Loads the sql models into a Dict""" models: UniqueKeyDict[str, Model] = UniqueKeyDict("models") + paths: t.Set[Path] = set() + cached_paths: UniqueKeyDict[Path, t.List[Model]] = UniqueKeyDict("cached_paths") for path in self._glob_paths( self.config_path / c.MODELS, @@ -473,48 +554,63 @@ def _load_sql_models( continue self._track_file(path) + paths.add(path) + if cached_models := cache.get(path): + cached_paths[path] = cached_models - def _load() -> t.List[Model]: - try: - with open(path, "r", encoding="utf-8") as file: - expressions = parse( - file.read(), default_dialect=self.config.model_defaults.dialect - ) - - return load_sql_based_models( - expressions, - self._get_variables, - defaults=self.config.model_defaults.dict(), - macros=macros, - jinja_macros=jinja_macros, - audit_definitions=audits, - default_audits=self.config.model_defaults.audits, - path=Path(path).absolute(), - module_path=self.config_path, - dialect=self.config.model_defaults.dialect, - time_column_format=self.config.time_column_format, - physical_schema_mapping=self.config.physical_schema_mapping, - project=self.config.project, - default_catalog=self.context.default_catalog, - infer_names=self.config.model_naming.infer_names, - signal_definitions=signals, - default_catalog_per_gateway=self.context.default_catalog_per_gateway, - ) - except Exception as ex: - self._raise_failed_to_load_model_error(path, ex) - raise - - for model in cache.get_or_load_models(path, _load): - if model.fqn in models: - self._raise_failed_to_load_model_error( - path, f"Duplicate SQL model name: '{model.name}'." - ) + for path, cached_models in cached_paths.items(): + paths.remove(path) + for model in cached_models: if model.enabled: models[model.fqn] = model - if isinstance(model, SeedModel): - seed_path = model.seed_path - self._track_file(seed_path) + if paths: + model_loading_defaults = dict( + get_variables=get_variables, + defaults=self.config.model_defaults.dict(), + macros=macros, + jinja_macros=jinja_macros, + audit_definitions=audits, + default_audits=self.config.model_defaults.audits, + module_path=self.config_path, + dialect=self.config.model_defaults.dialect, + time_column_format=self.config.time_column_format, + physical_schema_mapping=self.config.physical_schema_mapping, + project=self.config.project, + default_catalog=self.context.default_catalog, + infer_names=self.config.model_naming.infer_names, + signal_definitions=signals, + default_catalog_per_gateway=self.context.default_catalog_per_gateway, + ) + + with create_process_pool_executor( + initializer=_init_model_defaults, + initargs=( + self.config_essentials, + gateway, + model_loading_defaults, + cache, + self._console, + ), + max_workers=c.MAX_FORK_WORKERS, + ) as pool: + futures_to_paths = {pool.submit(load_sql_models, path): path for path in paths} + for future in concurrent.futures.as_completed(futures_to_paths): + path = futures_to_paths[future] + try: + loaded = future.result() + for model in loaded or cache.get(path): + if model.fqn in models: + raise ConfigError( + self._failed_to_load_model_error( + path, f"Duplicate SQL model name: '{model.name}'." + ) + ) + elif model.enabled: + model._path = path + models[model.fqn] = model + except Exception as ex: + raise ConfigError(self._failed_to_load_model_error(path, ex)) return models @@ -548,7 +644,7 @@ def _load_python_models( registered |= new for name in new: for model in registry[name].models( - self._get_variables, + get_variables, path=path, module_path=self.config_path, defaults=self.config.model_defaults.dict(), @@ -566,7 +662,7 @@ def _load_python_models( if model.enabled: models[model.fqn] = model except Exception as ex: - self._raise_failed_to_load_model_error(path, ex) + raise ConfigError(self._failed_to_load_model_error(path, ex)) finally: model_registry._dialect = None @@ -616,7 +712,7 @@ def _load_audits( """Loads all the model audits.""" audits_by_name: UniqueKeyDict[str, Audit] = UniqueKeyDict("audits") audits_max_mtime: t.Optional[float] = None - variables = self._get_variables() + variables = get_variables() for path in self._glob_paths( self.config_path / c.AUDITS, @@ -692,7 +788,7 @@ def _load_environment_statements(self, macros: MacroRegistry) -> t.List[Environm module_path=self.config_path, jinja_macro_references=None, macros=macros, - variables=self._get_variables(), + variables=get_variables(), path=self.config_path, ) @@ -727,7 +823,7 @@ def _load_model_test_file(self, path: Path) -> dict[str, ModelTestMetadata]: gateway_line = GATEWAY_PATTERN.search(source) gateway = YAML().load(gateway_line.group(0))["gateway"] if gateway_line else None - contents = yaml_load(source, variables=self._get_variables(gateway)) + contents = yaml_load(source, variables=get_variables(gateway)) for test_name, value in contents.items(): model_test_metadata[test_name] = ModelTestMetadata( @@ -785,11 +881,30 @@ def get_or_load_models( self._model_cache_entry_id(target_path), loader=loader, ) + for model in models: model._path = target_path return models + def put(self, models: t.List[Model], path: Path) -> bool: + return self._model_cache.put( + models, + self._cache_entry_name(path), + self._model_cache_entry_id(path), + ) + + def get(self, path: Path) -> t.List[Model]: + models = self._model_cache.get( + self._cache_entry_name(path), + self._model_cache_entry_id(path), + ) + + for model in models: + model._path = path + + return models + def _cache_entry_name(self, target_path: Path) -> str: return "__".join(target_path.relative_to(self.config_path).parts).replace( target_path.suffix, "" diff --git a/sqlmesh/core/model/cache.py b/sqlmesh/core/model/cache.py index 3357747c3c..a01c1d788c 100644 --- a/sqlmesh/core/model/cache.py +++ b/sqlmesh/core/model/cache.py @@ -1,9 +1,7 @@ from __future__ import annotations import logging -import multiprocessing as mp import typing as t -from concurrent.futures import ProcessPoolExecutor from pathlib import Path from sqlglot import exp @@ -15,7 +13,7 @@ from sqlmesh.core.model.definition import ExternalModel, Model, SqlModel, _Model from sqlmesh.utils.cache import FileCache from sqlmesh.utils.hashing import crc32 -from sqlmesh.utils.windows import IS_WINDOWS +from sqlmesh.utils.process import PoolExecutor, create_process_pool_executor from dataclasses import dataclass @@ -46,12 +44,10 @@ def get_or_load( self, name: str, entry_id: str = "", *, loader: t.Callable[[], t.List[Model]] ) -> t.List[Model]: """Returns an existing cached model definition or loads and caches a new one. - Args: name: The name of the entry. entry_id: The unique entry identifier. Used for cache invalidation. loader: Used to load a new model definition when no cached instance was found. - Returns: The model definition. """ @@ -66,9 +62,22 @@ def get_or_load( model.full_depends_on self._file_cache.put(name, entry_id, value=models) - return models + def put(self, models: t.List[Model], name: str, entry_id: str = "") -> bool: + if models and isinstance(seq_get(models, 0), (SqlModel, ExternalModel)): + # make sure we preload full_depends_on + for model in models: + model.full_depends_on + + self._file_cache.put(name, entry_id, value=models) + return True + + return False + + def get(self, name: str, entry_id: str = "") -> t.List[Model]: + return self._file_cache.get(name, entry_id) or [] + @dataclass class OptimizedQueryCacheEntry: @@ -149,11 +158,8 @@ def _entry_name(model: SqlModel) -> str: return f"{model.name}_{crc32(hash_data)}" -def optimized_query_cache_pool(optimized_query_cache: OptimizedQueryCache) -> ProcessPoolExecutor: - # fork doesnt work on Windows. ref: https://docs.python.org/3/library/multiprocessing.html#multiprocessing-start-methods - context_type = "spawn" if IS_WINDOWS else "fork" - return ProcessPoolExecutor( - mp_context=mp.get_context(context_type), +def optimized_query_cache_pool(optimized_query_cache: OptimizedQueryCache) -> PoolExecutor: + return create_process_pool_executor( initializer=_init_optimized_query_cache, initargs=(optimized_query_cache,), max_workers=c.MAX_FORK_WORKERS, diff --git a/sqlmesh/core/model/schema.py b/sqlmesh/core/model/schema.py index 86c628f610..9d3d38da6e 100644 --- a/sqlmesh/core/model/schema.py +++ b/sqlmesh/core/model/schema.py @@ -28,10 +28,7 @@ def update_model_schemas( schema = MappingSchema(normalize=False) optimized_query_cache: OptimizedQueryCache = OptimizedQueryCache(context_path / c.CACHE) - if c.MAX_FORK_WORKERS == 1: - _update_model_schemas_sequential(dag, models, schema, optimized_query_cache) - else: - _update_model_schemas_parallel(dag, models, schema, optimized_query_cache) + _update_model_schemas(dag, models, schema, optimized_query_cache) def _update_schema_with_model(schema: MappingSchema, model: Model) -> None: @@ -49,25 +46,7 @@ def _update_schema_with_model(schema: MappingSchema, model: Model) -> None: raise -def _update_model_schemas_sequential( - dag: DAG[str], - models: UniqueKeyDict[str, Model], - schema: MappingSchema, - optimized_query_cache: OptimizedQueryCache, -) -> None: - for name in dag.sorted: - model = models.get(name) - - # External models don't exist in the context, so we need to skip them - if not model: - continue - - model.update_schema(schema) - optimized_query_cache.with_optimized_query(model) - _update_schema_with_model(schema, model) - - -def _update_model_schemas_parallel( +def _update_model_schemas( dag: DAG[str], models: UniqueKeyDict[str, Model], schema: MappingSchema, @@ -107,12 +86,16 @@ def process_models(completed_model: t.Optional[Model] = None) -> None: while futures: for future in as_completed(futures): - futures.remove(future) - fqn, entry_name, data_hash, metadata_hash, mapping_schema = future.result() - model = models[fqn] - model._data_hash = data_hash - model._metadata_hash = metadata_hash - model.set_mapping_schema(mapping_schema) - optimized_query_cache.with_optimized_query(model, entry_name) - _update_schema_with_model(schema, model) - process_models(completed_model=model) + try: + futures.remove(future) + fqn, entry_name, data_hash, metadata_hash, mapping_schema = future.result() + model = models[fqn] + model._data_hash = data_hash + model._metadata_hash = metadata_hash + if model.mapping_schema != mapping_schema: + model.set_mapping_schema(mapping_schema) + optimized_query_cache.with_optimized_query(model, entry_name) + _update_schema_with_model(schema, model) + process_models(completed_model=model) + except Exception as ex: + raise SchemaError(f"Failed to update model schemas\n\n{ex}") diff --git a/sqlmesh/core/snapshot/cache.py b/sqlmesh/core/snapshot/cache.py index 436427eb82..d46b5f0620 100644 --- a/sqlmesh/core/snapshot/cache.py +++ b/sqlmesh/core/snapshot/cache.py @@ -55,20 +55,19 @@ def get_or_load( for snapshot in loaded_snapshots: snapshots[snapshot.snapshot_id] = snapshot - if c.MAX_FORK_WORKERS != 1: - with optimized_query_cache_pool(self._optimized_query_cache) as executor: - for key, entry_name in executor.map( - load_optimized_query, - ( - (snapshot.model, s_id) - for s_id, snapshot in snapshots.items() - if snapshot.is_model - ), - ): - if entry_name: - self._optimized_query_cache.with_optimized_query( - snapshots[key].model, entry_name - ) + with optimized_query_cache_pool(self._optimized_query_cache) as executor: + for key, entry_name in executor.map( + load_optimized_query, + ( + (snapshot.model, s_id) + for s_id, snapshot in snapshots.items() + if snapshot.is_model + ), + ): + if entry_name: + self._optimized_query_cache.with_optimized_query( + snapshots[key].model, entry_name + ) for snapshot in snapshots.values(): self._update_node_hash_cache(snapshot) diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 2cf686f2c9..230f25e6ca 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -355,6 +355,19 @@ def get_or_load_models( return models + def put(self, models: t.List[Model], path: Path) -> bool: + return self._model_cache.put( + models, + self._cache_entry_name(path), + self._cache_entry_id(path), + ) + + def get(self, path: Path) -> t.List[Model]: + return self._model_cache.get( + self._cache_entry_name(path), + self._cache_entry_id(path), + ) + def _cache_entry_name(self, target_path: Path) -> str: try: path_for_name = target_path.absolute().relative_to( diff --git a/sqlmesh/utils/process.py b/sqlmesh/utils/process.py new file mode 100644 index 0000000000..453fee78f5 --- /dev/null +++ b/sqlmesh/utils/process.py @@ -0,0 +1,73 @@ +# mypy: disable-error-code=no-untyped-def + +from concurrent.futures import Future, ProcessPoolExecutor +import typing as t +import multiprocessing as mp +from sqlmesh.utils.windows import IS_WINDOWS + + +class SynchronousPoolExecutor: + """A mock implementation of the ProcessPoolExecutor for synchronous use. + + This executor runs functions synchronously in the same process, avoiding the issues + with forking in test environments or when forking isn't possible (non-posix). + """ + + def __init__(self, max_workers=None, mp_context=None, initializer=None, initargs=()): + if initializer is not None: + try: + initializer(*initargs) + except BaseException as ex: + raise RuntimeError(f"Exception in initializer: {ex}") + + def __enter__(self): + return self + + def __exit__(self, *args): + self.shutdown(wait=True) + return False + + def shutdown(self, wait=True, cancel_futures=False): + """No-op method to match ProcessPoolExecutor API. + + Since this executor runs synchronously, there are no background processes + or resources to shut down and all futures will have completed already. + """ + pass + + def submit(self, fn, *args, **kwargs): + """Execute the function synchronously and return a Future with the result.""" + future = Future() + try: + result = fn(*args, **kwargs) + future.set_result(result) + except Exception as e: + future.set_exception(e) + return future + + def map(self, fn, *iterables, timeout=None, chunksize=1): + """Synchronous implementation of ProcessPoolExecutor.map. + + This executes the function for each set of inputs from the iterables in the + current process using Python's built-in map, rather than distributing work. + """ + return map(fn, *iterables) + + +PoolExecutor = t.Union[SynchronousPoolExecutor, ProcessPoolExecutor] + + +def create_process_pool_executor( + initializer: t.Callable, initargs: t.Tuple, max_workers: t.Optional[int] +) -> PoolExecutor: + if max_workers == 1 or IS_WINDOWS: + return SynchronousPoolExecutor( + initializer=initializer, + initargs=initargs, + ) + return ProcessPoolExecutor( + mp_context=mp.get_context("fork"), + initializer=initializer, + initargs=initargs, + max_workers=max_workers, + ) diff --git a/tests/conftest.py b/tests/conftest.py index 19953e7f5b..b3e7af187b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -189,7 +189,7 @@ def validate( def pytest_collection_modifyitems(items, *args, **kwargs): - test_type_markers = {"fast", "slow", "docker", "remote", "isolated"} + test_type_markers = {"fast", "slow", "docker", "remote", "isolated", "registry_isolation"} for item in items: for marker in item.iter_markers(): if marker.name in test_type_markers: diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index e5e7366760..fb5f38a4c9 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -4157,12 +4157,12 @@ def test_plan_repairs_unrenderable_snapshot_state( f"name = '{target_snapshot.name}' AND identifier = '{target_snapshot.identifier}'", ) - context.clear_caches() - - target_snapshot_in_state = context.state_sync.get_snapshots([target_snapshot.snapshot_id])[ - target_snapshot.snapshot_id - ] with pytest.raises(Exception): + context_copy = context.copy() + context_copy.clear_caches() + target_snapshot_in_state = context_copy.state_sync.get_snapshots( + [target_snapshot.snapshot_id] + )[target_snapshot.snapshot_id] target_snapshot_in_state.model.render_query_or_raise() # Repair the snapshot by creating a new version of it diff --git a/tests/core/test_loader.py b/tests/core/test_loader.py index a31b8c8b04..6ee9a44060 100644 --- a/tests/core/test_loader.py +++ b/tests/core/test_loader.py @@ -85,14 +85,14 @@ def test_duplicate_model_names_different_kind(tmp_path: Path, sample_models): path_3.write_text(model_3["contents"]) with pytest.raises( - ValueError, match=r'Duplicate model name\(s\) found: "memory"."test_schema"."test_model".' + ConfigError, match=r'Duplicate model name\(s\) found: "memory"."test_schema"."test_model".' ): Context(paths=tmp_path, config=config) @pytest.mark.parametrize("sample_models", ["sql", "external"], indirect=True) def test_duplicate_model_names_same_kind(tmp_path: Path, sample_models): - """Test same (SQL and external) models with duplicate model names raises ValueError.""" + """Test same (SQL and external) models with duplicate model names raises ConfigError.""" def duplicate_model_path(fpath): return Path(fpath).parent / ("duplicate" + Path(fpath).suffix) @@ -115,7 +115,7 @@ def duplicate_model_path(fpath): Context(paths=tmp_path, config=config) -@pytest.mark.isolated +@pytest.mark.registry_isolation def test_duplicate_python_model_names_raise_error(tmp_path: Path) -> None: """Test python models with duplicate model names raises ConfigError if the functions are not identical.""" init_example_project(tmp_path, dialect="duckdb") diff --git a/tests/core/test_model.py b/tests/core/test_model.py index f5d604eeae..3b8a54d10a 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -2941,7 +2941,7 @@ def test_model_cache(tmp_path: Path, mocker: MockerFixture): expressions = d.parse( """ MODEL ( - name db.seed, + name db.model_sql, ); SELECT 1, ds; """ @@ -2949,18 +2949,25 @@ def test_model_cache(tmp_path: Path, mocker: MockerFixture): model = load_sql_based_model([e for e in expressions if e]) - loader = mocker.Mock(return_value=[model]) - - assert cache.get_or_load("test_model", "test_entry_a", loader=loader)[0].dict() == model.dict() - assert cache.get_or_load("test_model", "test_entry_a", loader=loader)[0].dict() == model.dict() + assert cache.put([model], "test_model", "test_entry_a") + assert cache.get("test_model", "test_entry_a")[0].dict() == model.dict() - assert cache.get_or_load("test_model", "test_entry_b", loader=loader)[0].dict() == model.dict() - assert cache.get_or_load("test_model", "test_entry_b", loader=loader)[0].dict() == model.dict() + expressions = d.parse( + """ + MODEL ( + name db.model_seed, + kind SEED ( + path '../seeds/waiter_names.csv', + ), + ); + """ + ) - assert cache.get_or_load("test_model", "test_entry_a", loader=loader)[0].dict() == model.dict() - assert cache.get_or_load("test_model", "test_entry_a", loader=loader)[0].dict() == model.dict() + seed_model = load_sql_based_model( + expressions, path=Path("./examples/sushi/models/test_model.sql") + ) - assert loader.call_count == 2 + assert not cache.put([seed_model], "test_model", "test_entry_b") @pytest.mark.slow @@ -2983,7 +2990,7 @@ def test_model_cache_gateway(tmp_path: Path, mocker: MockerFixture): assert patched_cache_put.call_count == 0 Context(paths=tmp_path, config=config, gateway="secondary") - assert patched_cache_put.call_count == 4 + assert patched_cache_put.call_count == 2 @pytest.mark.slow @@ -3001,7 +3008,7 @@ def test_model_cache_default_catalog(tmp_path: Path, mocker: MockerFixture): PropertyMock(return_value=None), ): Context(paths=tmp_path) - assert patched_cache_put.call_count == 4 + assert patched_cache_put.call_count == 2 def test_model_ctas_query(): diff --git a/tests/test_forking.py b/tests/test_forking.py index ef9e3d4873..616b3bd4db 100644 --- a/tests/test_forking.py +++ b/tests/test_forking.py @@ -3,6 +3,7 @@ from sqlmesh import Context from sqlmesh.core.model import schema +import concurrent.futures pytestmark = pytest.mark.isolated @@ -10,12 +11,21 @@ def test_parallel_load(assert_exp_eq, mocker): mocker.patch("sqlmesh.core.constants.MAX_FORK_WORKERS", 2) - spy = mocker.spy(schema, "_update_model_schemas_parallel") + + spy_update_schemas = mocker.spy(schema, "_update_model_schemas") + process_pool_executor = mocker.spy(concurrent.futures.ProcessPoolExecutor, "__init__") + as_completed = mocker.spy(concurrent.futures, "as_completed") + context = Context(paths="examples/sushi") if hasattr(os, "fork"): - spy.assert_called() + process_pool_executor.assert_called() + as_completed.assert_called() + executor_args = process_pool_executor.call_args + assert executor_args[1]["max_workers"] == 2 + assert len(context.models) == 20 + spy_update_schemas.assert_called() assert_exp_eq( context.render("sushi.customers"), """ @@ -40,3 +50,22 @@ def test_parallel_load(assert_exp_eq, mocker): ) context.plan(no_prompts=True, auto_apply=True) + + +def test_parallel_load_multi_repo(assert_exp_eq, mocker): + mocker.patch("sqlmesh.core.constants.MAX_FORK_WORKERS", 2) + + process_pool_executor = mocker.spy(concurrent.futures.ProcessPoolExecutor, "__init__") + context = Context(paths=["examples/multi/repo_1", "examples/multi/repo_2"], gateway="memory") + + if hasattr(os, "fork"): + executor_args = process_pool_executor.call_args + assert executor_args[1]["max_workers"] == 2 + assert len(context.models) == 5 + + assert_exp_eq( + context.render("memory.bronze.a"), + 'SELECT 1 AS "col_a", \'b\' AS "col_b", 1 AS "one", \'repo_1\' AS "dup"', + ) + + context.plan(no_prompts=True, auto_apply=True) From bf3354a20641658d49f26930b28b4e720415adfe Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 26 May 2025 11:33:05 -0700 Subject: [PATCH 0260/1056] Fix: Get rid of recursion when calculating skipped nodes (#4538) --- sqlmesh/utils/concurrency.py | 21 ++++++++++++++------- tests/utils/test_concurrency.py | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/sqlmesh/utils/concurrency.py b/sqlmesh/utils/concurrency.py index 06186cf387..c5f78645f6 100644 --- a/sqlmesh/utils/concurrency.py +++ b/sqlmesh/utils/concurrency.py @@ -105,16 +105,23 @@ def _skip_next_nodes(self, parent: H) -> None: self._finished_future.set_result(None) return - skipped_nodes = [node for node, deps in self._unprocessed_nodes.items() if parent in deps] + skipped_nodes = {node for node, deps in self._unprocessed_nodes.items() if parent in deps} - self._skipped_nodes.extend(skipped_nodes) + while skipped_nodes: + self._skipped_nodes.extend(skipped_nodes) - for skipped_node in skipped_nodes: - self._unprocessed_nodes_num -= 1 - self._unprocessed_nodes.pop(skipped_node) + for skipped_node in skipped_nodes: + self._unprocessed_nodes_num -= 1 + self._unprocessed_nodes.pop(skipped_node) + + skipped_nodes = { + node + for node, deps in self._unprocessed_nodes.items() + if skipped_nodes.intersection(deps) + } - for skipped_node in skipped_nodes: - self._skip_next_nodes(skipped_node) + if not self._unprocessed_nodes_num: + self._finished_future.set_result(None) def _init_state(self) -> None: self._unprocessed_nodes = self.dag.graph diff --git a/tests/utils/test_concurrency.py b/tests/utils/test_concurrency.py index 892c6ef485..5e1e4326f7 100644 --- a/tests/utils/test_concurrency.py +++ b/tests/utils/test_concurrency.py @@ -139,7 +139,7 @@ def raise_(snapshot): assert len(errors) == 1 assert errors[0].node == failed_snapshot.snapshot_id - assert skipped == [snapshot_a.snapshot_id, snapshot_b.snapshot_id, snapshot_c.snapshot_id] + assert set(skipped) == {snapshot_a.snapshot_id, snapshot_b.snapshot_id, snapshot_c.snapshot_id} @pytest.mark.parametrize("tasks_num", [1, 3]) From 9dbb7b8a6c10dcd8f5f411395e59544a784715bc Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 26 May 2025 21:47:53 +0300 Subject: [PATCH 0261/1056] Fix: ensure paths exist before treating them as relative (metaprogramming) (#4539) --- sqlmesh/utils/metaprogramming.py | 2 +- tests/core/test_model.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/sqlmesh/utils/metaprogramming.py b/sqlmesh/utils/metaprogramming.py index 099d866960..9bfb8efc4e 100644 --- a/sqlmesh/utils/metaprogramming.py +++ b/sqlmesh/utils/metaprogramming.py @@ -37,7 +37,7 @@ def _is_relative_to(path: t.Optional[Path | str], other: t.Optional[Path | str]) if isinstance(other, str): other = Path(other) - if "site-packages" in str(path): + if "site-packages" in str(path) or not path.exists() or not other.exists(): return False try: diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 3b8a54d10a..3ce36b4c3b 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -10129,3 +10129,24 @@ def check_self_schema(evaluator): context = Context(paths=tmp_path, config=config) context.plan(no_prompts=True, auto_apply=True) + + +def test_model_relies_on_os_getenv(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + + (tmp_path / "macros" / "getenv_macro.py").write_text( + """ +from os import getenv +from sqlmesh import macro + +@macro() +def getenv_macro(evaluator): + getenv("foo", None) + return 1""" + ) + (tmp_path / "models" / "model.sql").write_text( + "MODEL (name test); SELECT @getenv_macro() AS foo" + ) + + monkeypatch.chdir(tmp_path) + ctx = Context(paths=tmp_path) From 2808e662142b27bef4653c58166186d38064f492 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 26 May 2025 21:19:17 +0200 Subject: [PATCH 0262/1056] chore: add nested cte to sushi for testing (#4540) --- examples/sushi/models/customers.sql | 12 ++++++++++-- .../integrations/github/cicd/test_integration.py | 4 ++-- tests/test_forking.py | 15 +++++++++++++-- tests/web/test_lineage.py | 15 +++++++++++++-- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/examples/sushi/models/customers.sql b/examples/sushi/models/customers.sql index fe8b3c5e57..6429bacbc1 100644 --- a/examples/sushi/models/customers.sql +++ b/examples/sushi/models/customers.sql @@ -17,7 +17,7 @@ CREATE VIEW raw.demographics AS ( SELECT 1 AS customer_id, '00000' AS zip ); -WITH current_marketing AS ( +WITH current_marketing_outer AS ( SELECT customer_id, status @@ -29,7 +29,15 @@ SELECT DISTINCT m.status, d.zip FROM sushi.orders AS o -LEFT JOIN current_marketing AS m +LEFT JOIN ( + WITH current_marketing AS ( + SELECT + customer_id, + status + FROM current_marketing_outer + ) + SELECT * FROM current_marketing +) AS m ON o.customer_id = m.customer_id LEFT JOIN raw.demographics AS d ON o.customer_id = d.customer_id diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index 732780d3e4..1b26052c64 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -1695,8 +1695,8 @@ def test_overlapping_changes_models( + d.zip, + 1 AS new_col FROM sushi.orders AS o - LEFT JOIN current_marketing AS m - ON o.customer_id = m.customer_id + LEFT JOIN ( + WITH current_marketing AS ( ``` - `sushi.waiter_names` ```diff diff --git a/tests/test_forking.py b/tests/test_forking.py index 616b3bd4db..5752c641c1 100644 --- a/tests/test_forking.py +++ b/tests/test_forking.py @@ -29,7 +29,7 @@ def test_parallel_load(assert_exp_eq, mocker): assert_exp_eq( context.render("sushi.customers"), """ -WITH "current_marketing" AS ( +WITH "current_marketing_outer" AS ( SELECT "marketing"."customer_id" AS "customer_id", "marketing"."status" AS "status" @@ -42,7 +42,18 @@ def test_parallel_load(assert_exp_eq, mocker): "m"."status" AS "status", "d"."zip" AS "zip" FROM "memory"."sushi"."orders" AS "o" -LEFT JOIN "current_marketing" AS "m" +LEFT JOIN ( + WITH "current_marketing" AS ( + SELECT + "current_marketing_outer"."customer_id" AS "customer_id", + "current_marketing_outer"."status" AS "status" + FROM "current_marketing_outer" AS "current_marketing_outer" + ) + SELECT + "current_marketing"."customer_id" AS "customer_id", + "current_marketing"."status" AS "status" + FROM "current_marketing" AS "current_marketing" +) AS "m" ON "m"."customer_id" = "o"."customer_id" LEFT JOIN "memory"."raw"."demographics" AS "d" ON "d"."customer_id" = "o"."customer_id" diff --git a/tests/web/test_lineage.py b/tests/web/test_lineage.py index d8efe1fd79..345d492f34 100644 --- a/tests/web/test_lineage.py +++ b/tests/web/test_lineage.py @@ -47,7 +47,7 @@ def test_get_lineage(client: TestClient, web_sushi_context: Context) -> None: "customer_id": { "expression": 'CAST("o"."customer_id" AS INT) AS "customer_id" /* this comment should not be registered */', "models": {'"memory"."sushi"."orders"': ["customer_id"]}, - "source": '''WITH "current_marketing" AS ( + "source": '''WITH "current_marketing_outer" AS ( SELECT "marketing"."customer_id" AS "customer_id", "marketing"."status" AS "status" @@ -58,7 +58,18 @@ def test_get_lineage(client: TestClient, web_sushi_context: Context) -> None: SELECT DISTINCT CAST("o"."customer_id" AS INT) AS "customer_id" /* this comment should not be registered */ FROM "memory"."sushi"."orders" AS "o" -LEFT JOIN "current_marketing" AS "m" +LEFT JOIN ( + WITH "current_marketing" AS ( + SELECT + "current_marketing_outer"."customer_id" AS "customer_id", + "current_marketing_outer"."status" AS "status" + FROM "current_marketing_outer" AS "current_marketing_outer" + ) + SELECT + "current_marketing"."customer_id" AS "customer_id", + "current_marketing"."status" AS "status" + FROM "current_marketing" AS "current_marketing" +) AS "m" ON "m"."customer_id" = "o"."customer_id" LEFT JOIN "memory"."raw"."demographics" AS "d" ON "d"."customer_id" = "o"."customer_id"''', From 4d183b2f0937178e9306a5e37c009beebd8b0bce Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 26 May 2025 22:31:10 +0300 Subject: [PATCH 0263/1056] Chore!: bump sqlglot to v26.21.0 (#4541) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 965d7f41fe..d77a1f19c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.20.0", + "sqlglot[rs]~=26.21.0", "tenacity", "time-machine", "json-stream" From 01f7af80949f56a149dee6c240f05165faa87dc5 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 27 May 2025 07:56:30 +1200 Subject: [PATCH 0264/1056] Fix(mssql): Use truncate+insert for FULL models instead of merge (#4531) --- sqlmesh/core/engine_adapter/mssql.py | 48 ++++++++++++++- tests/core/engine_adapter/test_mssql.py | 78 +++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index 9a198d5324..240352b1f0 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -10,7 +10,11 @@ from sqlglot import exp from sqlmesh.core.dialect import to_schema -from sqlmesh.core.engine_adapter.base import EngineAdapterWithIndexSupport +from sqlmesh.core.engine_adapter.base import ( + EngineAdapterWithIndexSupport, + EngineAdapter, + InsertOverwriteStrategy, +) from sqlmesh.core.engine_adapter.mixins import ( GetCurrentCatalogFromFunctionMixin, InsertOverwriteWithMergeMixin, @@ -281,3 +285,45 @@ def _rename_table( # The function that renames tables in MSSQL takes string literals as arguments instead of identifiers, # so we shouldn't quote the identifiers. self.execute(exp.rename_table(old_table_name, new_table_name), quote_identifiers=False) + + def _insert_overwrite_by_condition( + self, + table_name: TableName, + source_queries: t.List[SourceQuery], + columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + where: t.Optional[exp.Condition] = None, + insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None, + **kwargs: t.Any, + ) -> None: + if not where or where == exp.true(): + # this is a full table replacement, call the base strategy to do DELETE+INSERT + # which will result in TRUNCATE+INSERT due to how we have overridden self.delete_from() + return EngineAdapter._insert_overwrite_by_condition( + self, + table_name=table_name, + source_queries=source_queries, + columns_to_types=columns_to_types, + where=where, + insert_overwrite_strategy_override=InsertOverwriteStrategy.DELETE_INSERT, + **kwargs, + ) + + # For actual conditional overwrites, use MERGE from InsertOverwriteWithMergeMixin + return super()._insert_overwrite_by_condition( + table_name=table_name, + source_queries=source_queries, + columns_to_types=columns_to_types, + where=where, + insert_overwrite_strategy_override=insert_overwrite_strategy_override, + **kwargs, + ) + + def delete_from(self, table_name: TableName, where: t.Union[str, exp.Expression]) -> None: + if where == exp.true(): + # "A TRUNCATE TABLE operation can be rolled back within a transaction." + # ref: https://learn.microsoft.com/en-us/sql/t-sql/statements/truncate-table-transact-sql?view=sql-server-ver15#remarks + return self.execute( + exp.TruncateTable(expressions=[exp.to_table(table_name, dialect=self.dialect)]) + ) + + return super().delete_from(table_name, where) diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index cd961c96ac..a5e8aa8ecf 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -24,9 +24,12 @@ pytestmark = [pytest.mark.engine, pytest.mark.mssql] -def test_columns(make_mocked_engine_adapter: t.Callable): - adapter = make_mocked_engine_adapter(MSSQLEngineAdapter) +@pytest.fixture +def adapter(make_mocked_engine_adapter: t.Callable) -> MSSQLEngineAdapter: + return make_mocked_engine_adapter(MSSQLEngineAdapter) + +def test_columns(adapter: MSSQLEngineAdapter): adapter.cursor.fetchall.return_value = [ ("decimal_ps", "decimal", None, 5, 4), ("decimal", "decimal", None, 18, 0), @@ -504,7 +507,8 @@ def test_replace_query(make_mocked_engine_adapter: t.Callable): assert to_sql_calls(adapter) == [ """SELECT 1 FROM [information_schema].[tables] WHERE [table_name] = 'test_table';""", - "MERGE INTO [test_table] AS [__MERGE_TARGET__] USING (SELECT [a] AS [a] FROM [tbl]) AS [__MERGE_SOURCE__] ON (1 = 0) WHEN NOT MATCHED BY SOURCE THEN DELETE WHEN NOT MATCHED THEN INSERT ([a]) VALUES ([a]);", + "TRUNCATE TABLE [test_table];", + "INSERT INTO [test_table] ([a]) SELECT [a] FROM [tbl];", ] @@ -551,7 +555,8 @@ def temp_table_exists(table: exp.Table) -> bool: assert to_sql_calls(adapter) == [ f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = '{temp_table_name}') EXEC('CREATE TABLE [{temp_table_name}] ([a] INTEGER, [b] INTEGER)');""", - "MERGE INTO [test_table] AS [__MERGE_TARGET__] USING (SELECT CAST([a] AS INTEGER) AS [a], CAST([b] AS INTEGER) AS [b] FROM [__temp_test_table_abcdefgh]) AS [__MERGE_SOURCE__] ON (1 = 0) WHEN NOT MATCHED BY SOURCE THEN DELETE WHEN NOT MATCHED THEN INSERT ([a], [b]) VALUES ([a], [b]);", + "TRUNCATE TABLE [test_table];", + f"INSERT INTO [test_table] ([a], [b]) SELECT CAST([a] AS INTEGER) AS [a], CAST([b] AS INTEGER) AS [b] FROM [{temp_table_name}];", f"DROP TABLE IF EXISTS [{temp_table_name}];", ] @@ -751,3 +756,68 @@ def test_create_table_from_query(make_mocked_engine_adapter: t.Callable, mocker: "CREATE VIEW [__temp_ctas_test_random_id] AS SELECT * FROM (SELECT TOP 1 * FROM [t]);" in to_sql_calls(adapter) ) + + +def test_replace_query_strategy(adapter: MSSQLEngineAdapter, mocker: MockerFixture): + # ref issue 4472: https://github.com/TobikoData/sqlmesh/issues/4472 + # The FULL strategy calls EngineAdapter.replace_query() which calls _insert_overwrite_by_condition() should use DELETE+INSERT and not MERGE + expressions = d.parse( + f""" + MODEL ( + name db.table, + kind FULL, + dialect tsql + ); + + select a, b from db.upstream_table; + """ + ) + model = load_sql_based_model(expressions) + + exists_mock = mocker.patch( + "sqlmesh.core.engine_adapter.mssql.MSSQLEngineAdapter.table_exists", + return_value=False, + ) + + assert not adapter.table_exists("test_table") + + # initial - table doesnt exist + adapter.replace_query( + "test_table", + model.render_query_or_raise(), + table_format=model.table_format, + storage_format=model.storage_format, + partitioned_by=model.partitioned_by, + partition_interval_unit=model.partition_interval_unit, + clustered_by=model.clustered_by, + table_properties=model.physical_properties, + table_description=model.description, + column_descriptions=model.column_descriptions, + columns_to_types=model.columns_to_types_or_raise, + ) + + # subsequent - table exists + exists_mock.return_value = True + assert adapter.table_exists("test_table") + + adapter.replace_query( + "test_table", + model.render_query_or_raise(), + table_format=model.table_format, + storage_format=model.storage_format, + partitioned_by=model.partitioned_by, + partition_interval_unit=model.partition_interval_unit, + clustered_by=model.clustered_by, + table_properties=model.physical_properties, + table_description=model.description, + column_descriptions=model.column_descriptions, + columns_to_types=model.columns_to_types_or_raise, + ) + + assert to_sql_calls(adapter) == [ + # initial - create table if not exists + "IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = 'test_table') EXEC('SELECT * INTO [test_table] FROM (SELECT [a] AS [a], [b] AS [b] FROM [db].[upstream_table] AS [upstream_table]) AS temp');", + # subsequent - truncate + insert + "TRUNCATE TABLE [test_table];", + "INSERT INTO [test_table] ([a], [b]) SELECT [a] AS [a], [b] AS [b] FROM [db].[upstream_table] AS [upstream_table];", + ] From 4dbff925b6dba8ef6e8b12915e27915eae7a5e4b Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 26 May 2025 22:35:30 +0200 Subject: [PATCH 0265/1056] chore: update node dependencies (#4542) --- package.json | 2 +- pnpm-lock.yaml | 2818 +++++++++++++++++---------------- vscode/bus/package.json | 2 +- vscode/extension/package.json | 20 +- vscode/react/package.json | 50 +- web/client/package.json | 86 +- 6 files changed, 1568 insertions(+), 1410 deletions(-) diff --git a/package.json b/package.json index 6d5be2b017..d6383cfc5d 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,6 @@ "lint:fix": "pnpm run fmt && pnpm run -r lint:fix" }, "devDependencies": { - "prettier": "^3.5.2" + "prettier": "^3.5.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 369a53edc3..d36c3f873c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,13 +9,13 @@ importers: .: devDependencies: prettier: - specifier: ^3.5.2 + specifier: ^3.5.3 version: 3.5.3 vscode/bus: devDependencies: typescript: - specifier: ^5.5.4 + specifier: ^5.8.3 version: 5.8.3 vscode/extension: @@ -36,14 +36,14 @@ importers: specifier: ^9.0.1 version: 9.0.1 zod: - specifier: ^3.25.4 - version: 3.25.4 + specifier: ^3.25.28 + version: 3.25.28 devDependencies: '@eslint/js': - specifier: ^9.25.1 - version: 9.26.0 + specifier: ^9.27.0 + version: 9.27.0 '@playwright/test': - specifier: ^1.48.2 + specifier: ^1.52.0 version: 1.52.0 '@types/mocha': specifier: ^10.0.10 @@ -58,89 +58,89 @@ importers: specifier: ^0.0.10 version: 0.0.10 '@vscode/test-electron': - specifier: ^2.4.1 + specifier: ^2.5.2 version: 2.5.2 '@vscode/vsce': - specifier: ^3.3.2 - version: 3.3.2 + specifier: ^3.4.2 + version: 3.4.2 esbuild: - specifier: ^0.25.2 + specifier: ^0.25.4 version: 0.25.4 eslint: - specifier: ^9.23.0 - version: 9.26.0(jiti@2.4.2) + specifier: ^9.27.0 + version: 9.27.0(jiti@2.4.2) ts-loader: specifier: ^9.5.2 version: 9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.4)) tsx: - specifier: ^4.19.2 + specifier: ^4.19.4 version: 4.19.4 typescript: - specifier: ^5.8.2 + specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.31.1 - version: 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.32.1 + version: 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) vscode/react: dependencies: '@headlessui/react': - specifier: ^2.0.0 - version: 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^2.2.4 + version: 2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': - specifier: ^2.0.18 + specifier: ^2.2.0 version: 2.2.0(react@18.3.1) '@radix-ui/react-select': - specifier: ^2.2.4 - version: 2.2.4(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^2.2.5 + version: 2.2.5(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tailwindcss/postcss': - specifier: ^4.1.3 - version: 4.1.6 + specifier: ^4.1.7 + version: 4.1.7 '@tailwindcss/vite': - specifier: ^4.1.3 - version: 4.1.6(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1)) + specifier: ^4.1.7 + version: 4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) '@tanstack/react-query': - specifier: ^5.76.1 - version: 5.76.1(react@18.3.1) + specifier: ^5.77.2 + version: 5.77.2(react@18.3.1) '@tanstack/react-router': - specifier: ^1.114.3 - version: 1.120.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.120.10 + version: 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-router-devtools': - specifier: ^1.114.3 - version: 1.120.3(@tanstack/react-router@1.120.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.3)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3) + specifier: ^1.120.10 + version: 1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.10)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3) '@tanstack/react-virtual': - specifier: ^3.13.6 - version: 3.13.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^3.13.9 + version: 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-plugin': - specifier: ^1.114.3 - version: 1.120.3(@tanstack/react-router@1.120.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1))(webpack@5.99.8) + specifier: ^1.120.10 + version: 1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8) apache-arrow: specifier: ^19.0.1 version: 19.0.1 clsx: - specifier: ^2.0.0 + specifier: ^2.1.1 version: 2.1.1 elkjs: specifier: ^0.8.2 version: 0.8.2 orval: - specifier: ^7.8.0 + specifier: ^7.9.0 version: 7.9.0(openapi-types@12.1.3) react: - specifier: ^18.2.0 + specifier: ^18.3.1 version: 18.3.1 react-dom: - specifier: ^18.2.0 + specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-router: - specifier: ^7.0.0 - version: 7.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.6.1 + version: 7.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) reactflow: - specifier: ^11.8.3 - version: 11.11.4(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^11.11.4 + version: 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwindcss: - specifier: ^4.1.3 - version: 4.1.6 + specifier: ^4.1.7 + version: 4.1.7 vscode-uri: specifier: ^3.1.0 version: 3.1.0 @@ -149,29 +149,29 @@ importers: specifier: ^10.4.0 version: 10.4.0 '@testing-library/react': - specifier: ^16.2.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^16.3.0 + version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react': - specifier: ^18.2.0 - version: 18.3.21 + specifier: ^18.3.22 + version: 18.3.22 '@types/react-dom': - specifier: ^18.2.0 - version: 18.3.7(@types/react@18.3.21) + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.22) '@vitejs/plugin-react': - specifier: ^4.3.4 - version: 4.4.1(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1)) + specifier: ^4.5.0 + version: 4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) jsdom: - specifier: ^26.0.0 + specifier: ^26.1.0 version: 26.1.0 typescript: - specifier: ^5.7.2 + specifier: ^5.8.3 version: 5.8.3 vite: - specifier: ^6.1.0 - version: 6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + specifier: ^6.3.5 + version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) vitest: - specifier: ^3.0.5 - version: 3.1.3(@types/debug@4.1.12)(@types/node@20.17.46)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + specifier: ^3.1.4 + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -179,71 +179,71 @@ importers: web/client: dependencies: '@codemirror/autocomplete': - specifier: ^6.16.2 + specifier: ^6.18.6 version: 6.18.6 '@codemirror/commands': - specifier: ^6.6.0 + specifier: ^6.8.1 version: 6.8.1 '@codemirror/lang-python': - specifier: ^6.1.6 - version: 6.2.0 + specifier: ^6.2.1 + version: 6.2.1 '@codemirror/lang-sql': - specifier: ^6.6.4 + specifier: ^6.8.0 version: 6.8.0 '@codemirror/language': - specifier: ^6.10.2 + specifier: ^6.11.0 version: 6.11.0 '@codemirror/legacy-modes': - specifier: ^6.4.0 + specifier: ^6.5.1 version: 6.5.1 '@codemirror/state': - specifier: ^6.4.1 + specifier: ^6.5.2 version: 6.5.2 '@codemirror/view': - specifier: ^6.28.1 + specifier: ^6.36.8 version: 6.36.8 '@headlessui/react': - specifier: ^2.0.0 - version: 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^2.2.4 + version: 2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': - specifier: ^2.0.18 + specifier: ^2.2.0 version: 2.2.0(react@18.3.1) '@lit/react': - specifier: ^1.0.6 - version: 1.0.7(@types/react@18.3.21) + specifier: ^1.0.7 + version: 1.0.7(@types/react@18.3.22) '@radix-ui/react-context-menu': - specifier: ^2.1.4 - version: 2.2.14(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^2.2.15 + version: 2.2.15(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-select': - specifier: ^2.2.4 - version: 2.2.4(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^2.2.5 + version: 2.2.5(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tailwindcss/container-queries': specifier: ^0.1.1 version: 0.1.1(tailwindcss@3.4.17) '@tanstack/react-query': - specifier: ^5.76.1 - version: 5.76.1(react@18.3.1) + specifier: ^5.77.2 + version: 5.77.2(react@18.3.1) '@tanstack/react-table': - specifier: ^8.9.2 + specifier: ^8.21.3 version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-virtual': - specifier: ^3.0.0-beta.56 - version: 3.13.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^3.13.9 + version: 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@uidotdev/usehooks': - specifier: ^2.2.0 + specifier: ^2.4.1 version: 2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@uiw/react-codemirror': - specifier: ^4.21.12 + specifier: ^4.23.12 version: 4.23.12(@babel/runtime@7.27.1)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.8)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) apache-arrow: - specifier: ^19.0.0 + specifier: ^19.0.1 version: 19.0.1 clsx: - specifier: ^2.0.0 + specifier: ^2.1.1 version: 2.1.1 diff: - specifier: ^8.0.0 - version: 8.0.1 + specifier: ^8.0.2 + version: 8.0.2 elkjs: specifier: ^0.8.2 version: 0.8.2 @@ -251,75 +251,75 @@ importers: specifier: ^8.0.0 version: 8.0.0 react: - specifier: ^18.2.0 + specifier: ^18.3.1 version: 18.3.1 react-dnd: specifier: ^16.0.1 - version: 16.0.1(@types/node@20.17.46)(@types/react@18.3.21)(react@18.3.1) + version: 16.0.1(@types/node@22.15.21)(@types/react@18.3.22)(react@18.3.1) react-dnd-html5-backend: specifier: ^16.0.1 version: 16.0.1 react-dom: - specifier: ^18.2.0 + specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@18.3.21)(react@18.3.1) + version: 10.1.0(@types/react@18.3.22)(react@18.3.1) react-router: - specifier: ^7.0.0 - version: 7.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.6.1 + version: 7.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-split: specifier: ^2.0.14 version: 2.0.14(react@18.3.1) reactflow: - specifier: ^11.8.3 - version: 11.11.4(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^11.11.4 + version: 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) thememirror: specifier: ^2.0.1 version: 2.0.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.8) zustand: - specifier: ^5.0.0 - version: 5.0.4(@types/react@18.3.21)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) + specifier: ^5.0.5 + version: 5.0.5(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) devDependencies: '@eslint/js': - specifier: ^9.25.1 - version: 9.26.0 + specifier: ^9.27.0 + version: 9.27.0 '@playwright/test': - specifier: ^1.37.1 + specifier: ^1.52.0 version: 1.52.0 '@swc/core': - specifier: ^1.11.24 - version: 1.11.24(@swc/helpers@0.5.17) + specifier: ^1.11.29 + version: 1.11.29(@swc/helpers@0.5.17) '@testing-library/jest-dom': - specifier: ^6.1.2 + specifier: ^6.6.3 version: 6.6.3 '@testing-library/react': - specifier: ^16.0.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^16.3.0 + version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@testing-library/user-event': - specifier: ^14.4.3 + specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.0) '@types/pluralize': specifier: ^0.0.33 version: 0.0.33 '@types/react': - specifier: ^18.2.21 - version: 18.3.21 + specifier: ^18.3.22 + version: 18.3.22 '@types/react-dom': - specifier: ^18.2.7 - version: 18.3.7(@types/react@18.3.21) + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.22) '@vitejs/plugin-react-swc': - specifier: ^3.9.0 - version: 3.9.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1)) + specifier: ^3.10.0 + version: 3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) ajv: specifier: ^8.17.1 version: 8.17.1 autoprefixer: - specifier: ^10.4.15 + specifier: ^10.4.21 version: 10.4.21(postcss@8.5.3) eslint: - specifier: ^9.23.0 - version: 9.26.0(jiti@2.4.2) + specifier: ^9.27.0 + version: 9.27.0(jiti@2.4.2) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -327,35 +327,35 @@ importers: specifier: ^7.9.0 version: 7.9.0(openapi-types@12.1.3) postcss: - specifier: ^8.4.29 + specifier: ^8.5.3 version: 8.5.3 tailwindcss: - specifier: ^3.3.3 + specifier: ^3.4.17 version: 3.4.17 typescript: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.31.1 - version: 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.32.1 + version: 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) vite: - specifier: ^6.3.4 - version: 6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + specifier: ^6.3.5 + version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1)) + version: 3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) vitest: - specifier: ^3.1.2 - version: 3.1.3(@types/debug@4.1.12)(@types/node@20.17.46)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + specifier: ^3.1.4 + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) optionalDependencies: '@swc/core-linux-x64-gnu': - specifier: ^1.11.24 - version: 1.11.24 + specifier: ^1.11.29 + version: 1.11.29 packages: - '@adobe/css-tools@4.4.2': - resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} + '@adobe/css-tools@4.4.3': + resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} @@ -381,12 +381,18 @@ packages: peerDependencies: openapi-types: '>=7' - '@asamuzakjp/css-color@3.1.7': - resolution: {integrity: sha512-Ok5fYhtwdyJQmU1PpEv6Si7Y+A4cYb8yNM9oiIJC9TzXPMuN9fvdonKJqcnz9TbFqV6bQ8z0giRq0iaOpGZV2g==} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} '@asyncapi/specs@6.8.1': resolution: {integrity: sha512-czHoAk3PeXTLR+X8IUaD+IpT+g+zUvkcgMDJVothBsan+oHN3jfcFcFUNdOPAAFoUCQN1hXF1dWuphWy05THlA==} + '@azu/format-text@1.0.2': + resolution: {integrity: sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==} + + '@azu/style-format@1.0.1': + resolution: {integrity: sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==} + '@azure/abort-controller@2.1.2': resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} engines: {node: '>=18.0.0'} @@ -411,8 +417,8 @@ packages: resolution: {integrity: sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ==} engines: {node: '>=18.0.0'} - '@azure/identity@4.9.1': - resolution: {integrity: sha512-986D7Cf1AOwYqSDtO/FnMAyk/Jc8qpftkGsxuehoh4F85MhQ4fICBGX/44+X1y78lN4Sqib3Bsoaoh/FvOGgmg==} + '@azure/identity@4.10.0': + resolution: {integrity: sha512-iT53Sre2NJK6wzMWnvpjNiR3md597LZ3uK/5kQD2TkrY9vqhrY5bt2KwELNjkOWQ9n8S/92knj/QEykTtjMNqQ==} engines: {node: '>=18.0.0'} '@azure/logger@1.2.0': @@ -535,8 +541,8 @@ packages: '@codemirror/commands@6.8.1': resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==} - '@codemirror/lang-python@6.2.0': - resolution: {integrity: sha512-+oLTR88uLib84tvb4XmOBBq/dgrctvPXueP3Wjotu4zmHLM2KW2wfswJ6r1BKlfJNcGgdWX1AgUeGEf3E2H5LA==} + '@codemirror/lang-python@6.2.1': + resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} '@codemirror/lang-sql@6.8.0': resolution: {integrity: sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==} @@ -758,24 +764,24 @@ packages: resolution: {integrity: sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.13.0': - resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} + '@eslint/core@0.14.0': + resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.26.0': - resolution: {integrity: sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==} + '@eslint/js@9.27.0': + resolution: {integrity: sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.8': - resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} + '@eslint/plugin-kit@0.3.1': + resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@exodus/schemasafe@1.3.0': @@ -802,11 +808,11 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} - '@gerrit0/mini-shiki@3.4.0': - resolution: {integrity: sha512-48lKoQegmfJ0iyR/jRz5OrYOSM3WewG9YWCPqUvYFEC54shQO8RsAaspaK/2PRHVVnjekRqfAFvq8pwCpIo5ig==} + '@gerrit0/mini-shiki@3.4.2': + resolution: {integrity: sha512-3jXo5bNjvvimvdbIhKGfFxSnKCX+MA8wzHv55ptzk/cx8wOzT+BRcYgj8aFN3yTiTs+zvQQiaZFr7Jce1ZG3fw==} - '@headlessui/react@2.2.3': - resolution: {integrity: sha512-hgOJGXPifPlOczIeSwX8OjLWRJ5XdYApZFf7DeCbCrO1PXHkPhNTRrA9ZwJsgAG7SON1i2JcvIreF/kbgtJeaQ==} + '@headlessui/react@2.2.4': + resolution: {integrity: sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==} engines: {node: '>=10'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -919,10 +925,6 @@ packages: '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} - '@modelcontextprotocol/sdk@1.11.2': - resolution: {integrity: sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==} - engines: {node: '>=18'} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -980,8 +982,8 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} - '@radix-ui/react-arrow@1.1.6': - resolution: {integrity: sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==} + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -993,8 +995,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collection@1.1.6': - resolution: {integrity: sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==} + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1015,8 +1017,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-context-menu@2.2.14': - resolution: {integrity: sha512-RUHvrJE2qKAd9pQ50HZZsePio4SMWEh8v6FWQwg/4t6K1fuxfb4Ec40VEVvni6V7nFxmj9srU4UZc7aYp8x0LQ==} + '@radix-ui/react-context-menu@2.2.15': + resolution: {integrity: sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1046,8 +1048,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dismissable-layer@1.1.9': - resolution: {integrity: sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==} + '@radix-ui/react-dismissable-layer@1.1.10': + resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1068,8 +1070,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-focus-scope@1.1.6': - resolution: {integrity: sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==} + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1090,8 +1092,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-menu@2.1.14': - resolution: {integrity: sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==} + '@radix-ui/react-menu@2.1.15': + resolution: {integrity: sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1103,8 +1105,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popper@1.2.6': - resolution: {integrity: sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==} + '@radix-ui/react-popper@1.2.7': + resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1116,8 +1118,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-portal@1.1.8': - resolution: {integrity: sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==} + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1142,8 +1144,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.2': - resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==} + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1155,8 +1157,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-roving-focus@1.1.9': - resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==} + '@radix-ui/react-roving-focus@1.1.10': + resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1168,8 +1170,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-select@2.2.4': - resolution: {integrity: sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q==} + '@radix-ui/react-select@2.2.5': + resolution: {integrity: sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1181,8 +1183,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slot@1.2.2': - resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==} + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1262,8 +1264,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-visually-hidden@1.2.2': - resolution: {integrity: sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==} + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1278,14 +1280,14 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@react-aria/focus@3.20.2': - resolution: {integrity: sha512-Q3rouk/rzoF/3TuH6FzoAIKrl+kzZi9LHmr8S5EqLAOyP9TXIKG34x2j42dZsAhrw7TbF9gA8tBKwnCNH4ZV+Q==} + '@react-aria/focus@3.20.3': + resolution: {integrity: sha512-rR5uZUMSY4xLHmpK/I8bP1V6vUNHFo33gTvrvNUsAKKqvMfa7R2nu5A6v97dr5g6tVH6xzpdkPsOJCWh90H2cw==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-aria/interactions@3.25.0': - resolution: {integrity: sha512-GgIsDLlO8rDU/nFn6DfsbP9rfnzhm8QFjZkB9K9+r+MTSCn7bMntiWQgMM+5O6BiA8d7C7x4zuN4bZtc0RBdXQ==} + '@react-aria/interactions@3.25.1': + resolution: {integrity: sha512-ntLrlgqkmZupbbjekz3fE/n3eQH2vhncx8gUp0+N+GttKWevx7jos11JUBjnJwb1RSOPgRUFcrluOqBp0VgcfQ==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -1296,8 +1298,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-aria/utils@3.28.2': - resolution: {integrity: sha512-J8CcLbvnQgiBn54eeEvQQbIOfBF3A1QizxMw9P4cl9MkeR03ug7RnjTIdJY/n2p7t59kLeAB3tqiczhcj+Oi5w==} + '@react-aria/utils@3.29.0': + resolution: {integrity: sha512-jSOrZimCuT1iKNVlhjIxDkAhgF7HSp3pqyT6qjg/ZoA0wfqCi/okmrMPiWSAKBnkgX93N8GYTLT3CIEO6WZe9Q==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -1319,8 +1321,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/shared@3.29.0': - resolution: {integrity: sha512-IDQYu/AHgZimObzCFdNl1LpZvQW/xcfLt3v20sorl5qRucDVj4S9os98sVTZ4IRIBjmS+MkjqpR5E70xan7ooA==} + '@react-types/shared@3.29.1': + resolution: {integrity: sha512-KtM+cDf2CXoUX439rfEhbnEdAgFZX20UP2A35ypNIawR7/PFFPjQDWyA2EnClCcW/dLWJDEPX2U8+EJff8xqmQ==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -1360,121 +1362,173 @@ packages: react: '>=17' react-dom: '>=17' - '@rollup/rollup-android-arm-eabi@4.40.2': - resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==} + '@rolldown/pluginutils@1.0.0-beta.9': + resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} + + '@rollup/rollup-android-arm-eabi@4.41.1': + resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.40.2': - resolution: {integrity: sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==} + '@rollup/rollup-android-arm64@4.41.1': + resolution: {integrity: sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.40.2': - resolution: {integrity: sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==} + '@rollup/rollup-darwin-arm64@4.41.1': + resolution: {integrity: sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.40.2': - resolution: {integrity: sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==} + '@rollup/rollup-darwin-x64@4.41.1': + resolution: {integrity: sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.40.2': - resolution: {integrity: sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==} + '@rollup/rollup-freebsd-arm64@4.41.1': + resolution: {integrity: sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.40.2': - resolution: {integrity: sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==} + '@rollup/rollup-freebsd-x64@4.41.1': + resolution: {integrity: sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.40.2': - resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==} + '@rollup/rollup-linux-arm-gnueabihf@4.41.1': + resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.40.2': - resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==} + '@rollup/rollup-linux-arm-musleabihf@4.41.1': + resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.40.2': - resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==} + '@rollup/rollup-linux-arm64-gnu@4.41.1': + resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.40.2': - resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==} + '@rollup/rollup-linux-arm64-musl@4.41.1': + resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.40.2': - resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==} + '@rollup/rollup-linux-loongarch64-gnu@4.41.1': + resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': - resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==} + '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': + resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.40.2': - resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==} + '@rollup/rollup-linux-riscv64-gnu@4.41.1': + resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.40.2': - resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==} + '@rollup/rollup-linux-riscv64-musl@4.41.1': + resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.40.2': - resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==} + '@rollup/rollup-linux-s390x-gnu@4.41.1': + resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.40.2': - resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==} + '@rollup/rollup-linux-x64-gnu@4.41.1': + resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.40.2': - resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==} + '@rollup/rollup-linux-x64-musl@4.41.1': + resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.40.2': - resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==} + '@rollup/rollup-win32-arm64-msvc@4.41.1': + resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.40.2': - resolution: {integrity: sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==} + '@rollup/rollup-win32-ia32-msvc@4.41.1': + resolution: {integrity: sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.40.2': - resolution: {integrity: sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==} + '@rollup/rollup-win32-x64-msvc@4.41.1': + resolution: {integrity: sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==} cpu: [x64] os: [win32] - '@shikijs/engine-oniguruma@3.4.0': - resolution: {integrity: sha512-zwcWlZ4OQuJ/+1t32ClTtyTU1AiDkK1lhtviRWoq/hFqPjCNyLj22bIg9rB7BfoZKOEOfrsGz7No33BPCf+WlQ==} + '@secretlint/config-creator@9.3.3': + resolution: {integrity: sha512-USIKXtBIDPBt+uxssxFVqYBzSommdwXNDGwRZPGErnKWeIH58XuyqIjRTi99fYB0yAQZZ+Cv4sD2JVXCxevEew==} + engines: {node: ^14.13.1 || >=16.0.0} + + '@secretlint/config-loader@9.3.3': + resolution: {integrity: sha512-t0NGpVq7fFROr/UqfxSI09UI30U7rKSGXjfKNwR0O6fMlwx2AV9RWOvLS4hDLwxxKs+ywss6DZx/wcTdtBEWxA==} + engines: {node: ^14.13.1 || >=16.0.0} + + '@secretlint/core@9.3.3': + resolution: {integrity: sha512-XPpchOJz591E6bqMWkY6VxtaIbSI0gY0bUeVz1gkfT6FUI0fOfJrAMWe9RhxXWraMuxokTQA8R/LFJefiK+bXg==} + engines: {node: ^14.13.1 || >=16.0.0} + + '@secretlint/formatter@9.3.3': + resolution: {integrity: sha512-kqfnbhtxcH1Ew7pboM+jCZl8CuBzVuEKuHHSkT92iasxaaq1NK37h5IIfUDbFdXizmNFe3MwAnnVU8lqK2Dvyg==} + engines: {node: ^14.13.1 || >=16.0.0} + + '@secretlint/node@9.3.3': + resolution: {integrity: sha512-ZD1yXlzEJmFS/lq+BmgzUBB+2mQgj6kK6A//IhBop5xqAp+lXoq1vNgu7VSJ3DR+XrKrIK7YHFZXRh9aJvIjmA==} + engines: {node: ^14.13.1 || >=16.0.0} + + '@secretlint/profiler@9.3.3': + resolution: {integrity: sha512-wcVTByh+m9O1w2WAV08Po6trGsVjhRTV1UWuzVcQTTap9EjeKQLja6Xof/SIDGORD0KWooUIMAe7VPLQFPi1cQ==} - '@shikijs/langs@3.4.0': - resolution: {integrity: sha512-bQkR+8LllaM2duU9BBRQU0GqFTx7TuF5kKlw/7uiGKoK140n1xlLAwCgXwSxAjJ7Htk9tXTFwnnsJTCU5nDPXQ==} + '@secretlint/resolver@9.3.3': + resolution: {integrity: sha512-8N0lqD7OiI/aLK/PhKyiGh5xTlO/6TjHiOt72jnrvB9BK2QF45Mp5fivCARTKBypDiTZrOrS7blvqZ7qTnOTrA==} - '@shikijs/themes@3.4.0': - resolution: {integrity: sha512-YPP4PKNFcFGLxItpbU0ZW1Osyuk8AyZ24YEFaq04CFsuCbcqydMvMUTi40V2dkc0qs1U2uZFrnU6s5zI6IH+uA==} + '@secretlint/secretlint-formatter-sarif@9.3.3': + resolution: {integrity: sha512-qH8726RFQLdD2iKXamSbBcRTSxbECDbvg0hS3aTGL0+XOmzWI7JL4tdNywMqeHzKCRLrcEJOLYWv/P/w2VdwkA==} - '@shikijs/types@3.4.0': - resolution: {integrity: sha512-EUT/0lGiE//7j5N/yTMNMT3eCWNcHJLrRKxT0NDXWIfdfSmFJKfPX7nMmRBrQnWboAzIsUziCThrYMMhjbMS1A==} + '@secretlint/secretlint-rule-no-dotenv@9.3.3': + resolution: {integrity: sha512-Fm1uSlchskbIGuVEIYr1MnhTvUSd4GHqiRXVomH0Sli9Q0JMKElBlfS8cB165OaNGrCZ+TmmdrF/Q8sjwZYWyQ==} + engines: {node: ^14.13.1 || >=16.0.0} + + '@secretlint/secretlint-rule-preset-recommend@9.3.3': + resolution: {integrity: sha512-zT8zxh1z28Vzc9S5FVMbfWOITNikTYmajLTuX4D8lhGM3bx7xDopUJnsEtj1lAGc5WcCZ3baMJ3xCFZeDv/SAg==} + engines: {node: ^14.13.1 || >=16.0.0} + + '@secretlint/source-creator@9.3.3': + resolution: {integrity: sha512-2h6t9UfWQn7Sp6PUO+hvWK3i55tqE4H4YlmUBlL5VOjubADcO21OAtp7S05LgXE+VJfLDgUcb1hflkw0cPE1rw==} + engines: {node: ^14.13.1 || >=16.0.0} + + '@secretlint/types@9.3.3': + resolution: {integrity: sha512-ehVGggPM23sHEkqQP/5HlGDK+8Xx2oRX8vF9C/fKh+TcTRWOfjCeC7QeoPxcEMXNDXfUsHK5P8DKqQEcpbiUZQ==} + engines: {node: ^14.13.1 || >=16.0.0} + + '@shikijs/engine-oniguruma@3.4.2': + resolution: {integrity: sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q==} + + '@shikijs/langs@3.4.2': + resolution: {integrity: sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA==} + + '@shikijs/themes@3.4.2': + resolution: {integrity: sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg==} + + '@shikijs/types@3.4.2': + resolution: {integrity: sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + '@stoplight/better-ajv-errors@1.0.3': resolution: {integrity: sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==} engines: {node: ^12.20 || >= 14.13} @@ -1548,68 +1602,68 @@ packages: resolution: {integrity: sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==} engines: {node: '>=10.8'} - '@swc/core-darwin-arm64@1.11.24': - resolution: {integrity: sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==} + '@swc/core-darwin-arm64@1.11.29': + resolution: {integrity: sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.11.24': - resolution: {integrity: sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==} + '@swc/core-darwin-x64@1.11.29': + resolution: {integrity: sha512-S3eTo/KYFk+76cWJRgX30hylN5XkSmjYtCBnM4jPLYn7L6zWYEPajsFLmruQEiTEDUg0gBEWLMNyUeghtswouw==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.11.24': - resolution: {integrity: sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==} + '@swc/core-linux-arm-gnueabihf@1.11.29': + resolution: {integrity: sha512-o9gdshbzkUMG6azldHdmKklcfrcMx+a23d/2qHQHPDLUPAN+Trd+sDQUYArK5Fcm7TlpG4sczz95ghN0DMkM7g==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.11.24': - resolution: {integrity: sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==} + '@swc/core-linux-arm64-gnu@1.11.29': + resolution: {integrity: sha512-sLoaciOgUKQF1KX9T6hPGzvhOQaJn+3DHy4LOHeXhQqvBgr+7QcZ+hl4uixPKTzxk6hy6Hb0QOvQEdBAAR1gXw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.11.24': - resolution: {integrity: sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==} + '@swc/core-linux-arm64-musl@1.11.29': + resolution: {integrity: sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.11.24': - resolution: {integrity: sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==} + '@swc/core-linux-x64-gnu@1.11.29': + resolution: {integrity: sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.11.24': - resolution: {integrity: sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==} + '@swc/core-linux-x64-musl@1.11.29': + resolution: {integrity: sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.11.24': - resolution: {integrity: sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==} + '@swc/core-win32-arm64-msvc@1.11.29': + resolution: {integrity: sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.11.24': - resolution: {integrity: sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==} + '@swc/core-win32-ia32-msvc@1.11.29': + resolution: {integrity: sha512-h+NjOrbqdRBYr5ItmStmQt6x3tnhqgwbj9YxdGPepbTDamFv7vFnhZR0YfB3jz3UKJ8H3uGJ65Zw1VsC+xpFkg==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.11.24': - resolution: {integrity: sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==} + '@swc/core-win32-x64-msvc@1.11.29': + resolution: {integrity: sha512-Q8cs2BDV9wqDvqobkXOYdC+pLUSEpX/KvI0Dgfun1F+LzuLotRFuDhrvkU9ETJA6OnD2+Fn/ieHgloiKA/Mn/g==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.11.24': - resolution: {integrity: sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==} + '@swc/core@1.11.29': + resolution: {integrity: sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1631,65 +1685,65 @@ packages: peerDependencies: tailwindcss: '>=3.2.0' - '@tailwindcss/node@4.1.6': - resolution: {integrity: sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==} + '@tailwindcss/node@4.1.7': + resolution: {integrity: sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==} - '@tailwindcss/oxide-android-arm64@4.1.6': - resolution: {integrity: sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==} + '@tailwindcss/oxide-android-arm64@4.1.7': + resolution: {integrity: sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.6': - resolution: {integrity: sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==} + '@tailwindcss/oxide-darwin-arm64@4.1.7': + resolution: {integrity: sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.6': - resolution: {integrity: sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==} + '@tailwindcss/oxide-darwin-x64@4.1.7': + resolution: {integrity: sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.6': - resolution: {integrity: sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==} + '@tailwindcss/oxide-freebsd-x64@4.1.7': + resolution: {integrity: sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.6': - resolution: {integrity: sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': + resolution: {integrity: sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.6': - resolution: {integrity: sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': + resolution: {integrity: sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.6': - resolution: {integrity: sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.7': + resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.6': - resolution: {integrity: sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.7': + resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.6': - resolution: {integrity: sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==} + '@tailwindcss/oxide-linux-x64-musl@4.1.7': + resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.6': - resolution: {integrity: sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==} + '@tailwindcss/oxide-wasm32-wasi@4.1.7': + resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -1700,27 +1754,27 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.6': - resolution: {integrity: sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': + resolution: {integrity: sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.6': - resolution: {integrity: sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.7': + resolution: {integrity: sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.6': - resolution: {integrity: sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==} + '@tailwindcss/oxide@4.1.7': + resolution: {integrity: sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==} engines: {node: '>= 10'} - '@tailwindcss/postcss@4.1.6': - resolution: {integrity: sha512-ELq+gDMBuRXPJlpE3PEen+1MhnHAQQrh2zF0dI1NXOlEWfr2qWf2CQdr5jl9yANv8RErQaQ2l6nIFO9OSCVq/g==} + '@tailwindcss/postcss@4.1.7': + resolution: {integrity: sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw==} - '@tailwindcss/vite@4.1.6': - resolution: {integrity: sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==} + '@tailwindcss/vite@4.1.7': + resolution: {integrity: sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==} peerDependencies: vite: ^5.2.0 || ^6 @@ -1728,24 +1782,24 @@ packages: resolution: {integrity: sha512-K7JJNrRVvyjAVnbXOH2XLRhFXDkeP54Kt2P4FR1Kl2KDGlIbkua5VqZQD2rot3qaDrpufyUa63nuLai1kOLTsQ==} engines: {node: '>=12'} - '@tanstack/query-core@5.76.0': - resolution: {integrity: sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg==} + '@tanstack/query-core@5.77.2': + resolution: {integrity: sha512-1lqJwPsR6GX6nZFw06erRt518O19tWU6Q+x0fJUygl4lxHCYF2nhzBPwLKk2NPjYOrpR0K567hxPc5K++xDe9Q==} - '@tanstack/react-query@5.76.1': - resolution: {integrity: sha512-YxdLZVGN4QkT5YT1HKZQWiIlcgauIXEIsMOTSjvyD5wLYK8YVvKZUPAysMqossFJJfDpJW3pFn7WNZuPOqq+fw==} + '@tanstack/react-query@5.77.2': + resolution: {integrity: sha512-BRHxWdy1mHmgAcYA/qy2IPLylT81oebLgkm9K85viN2Qol/Vq48t1dzDFeDIVQjTWDV96AmqsLNPlH5HjyKCxA==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.120.3': - resolution: {integrity: sha512-aeEodmbATZ81H1xJuiaWaadSW9iqG9YEvaBgmlS70bxepFNkeXONEXcw38IQMTsPNoEZqbtvqAjl2Pg08cZlxQ==} + '@tanstack/react-router-devtools@1.120.10': + resolution: {integrity: sha512-0Pc7ttT44MzcW9S0BE8V5Y4APUVnlTxP84VBTtd8z4MCMFbukiMCNqSQIR/jHJ/6zyGZNhjYIBEzB2Oftgo6QQ==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.120.3 + '@tanstack/react-router': ^1.120.10 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.120.3': - resolution: {integrity: sha512-+5Y5ORtjW/LJhIxOxxBrvhZzfMOP9B+LaJ1j1P5tM5YqpLYWHuImfzGNRpKtBgiRoSaJedPjwY7lj88EwNWVbg==} + '@tanstack/react-router@1.120.10': + resolution: {integrity: sha512-+SE3CAP/YYMNt2jhPsT49LhPyJcABaTzrowDfY/Ru6osR+byNlxbooqhXLIvtxc5WsMLY/aB8TpTcTft1W5IPA==} engines: {node: '>=12'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -1764,21 +1818,21 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.8': - resolution: {integrity: sha512-meS2AanUg50f3FBSNoAdBSRAh8uS0ue01qm7zrw65KGJtiXB9QXfybqZwkh4uFpRv2iX/eu5tjcH5wqUpwYLPg==} + '@tanstack/react-virtual@3.13.9': + resolution: {integrity: sha512-SPWC8kwG/dWBf7Py7cfheAPOxuvIv4fFQ54PdmYbg7CpXfsKxkucak43Q0qKsxVthhUJQ1A7CIMAIplq4BjVwA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.120.3': - resolution: {integrity: sha512-/16Pp7yxUUIGkc+oPVnlWqvlGtvLoQeKfJPpKc1vcPIBvHFO/o3yg/CEzP5raWDAjyq3b+BVkej3lSzkNxgBSg==} + '@tanstack/router-core@1.120.10': + resolution: {integrity: sha512-AmEJAYt+6w/790zTnfddVhnK1QJCnd96H4xg1aD65Oohc8+OTQBxgWky/wzqwhHRdkdsBgRT7iWac9x5Y8UrQA==} engines: {node: '>=12'} - '@tanstack/router-devtools-core@1.120.3': - resolution: {integrity: sha512-cUY1GFq8qNfIfivhozaG2NOt05Jran1yoZrBajVK0qLO0nIXJ673XeCPjzQkDctN5FU6xfmF6aXgO/xJd0igrA==} + '@tanstack/router-devtools-core@1.120.10': + resolution: {integrity: sha512-fysPrKH7dL/G/guHm0HN+ceFEBZnbKaU9R8KZHo/Qzue7WxQV+g4or2EWnbBJ8/aF+C/WYgxR1ATFqfZEjHSfg==} engines: {node: '>=12'} peerDependencies: - '@tanstack/router-core': ^1.120.3 + '@tanstack/router-core': ^1.120.10 csstype: ^3.0.10 solid-js: '>=1.9.5' tiny-invariant: ^1.3.3 @@ -1786,21 +1840,21 @@ packages: csstype: optional: true - '@tanstack/router-generator@1.120.3': - resolution: {integrity: sha512-Lz0nIwGNM+vlLGGiSBTQvcD2gW5WhoIeZN8IlTBssUb33m21QLpoj9ozpXFDrlzk36rTn5NcijHEStpYqrvQbA==} + '@tanstack/router-generator@1.120.10': + resolution: {integrity: sha512-oUhzCAeIDfupXGwIf3oMqqdSRw62fTtvdUhMLfnTimGMuSp1ErxIj52PeyVGFAFr/ORP85ZxNqRpAecZal247A==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.120.3 + '@tanstack/react-router': ^1.120.10 peerDependenciesMeta: '@tanstack/react-router': optional: true - '@tanstack/router-plugin@1.120.3': - resolution: {integrity: sha512-iTW402GLCxexMn42OSN8Md7A0vYm5q5+vBKDp3FcjnLgmD+31AI7H//RnGI6nxRWo/xMN8ZjESy/PVg1ouvDxA==} + '@tanstack/router-plugin@1.120.10': + resolution: {integrity: sha512-jAaL0Vh8Kuy+wFUEUiKSoCiGNljXChldFsuvcqnTo4/4qWtKgHlQminGMmx2z5eGZ0EsIfP+NSAMbCpYPFvEng==} engines: {node: '>=12'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.120.3 + '@tanstack/react-router': ^1.120.10 vite: '>=5.0.0 || >=6.0.0' vite-plugin-solid: ^2.11.2 webpack: '>=5.92.0' @@ -1827,8 +1881,8 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.8': - resolution: {integrity: sha512-BT6w89Hqy7YKaWewYzmecXQzcJh6HTBbKYJIIkMaNU49DZ06LoTV3z32DWWEdUsgW6n1xTmwTLs4GtWrZC261w==} + '@tanstack/virtual-core@3.13.9': + resolution: {integrity: sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==} '@tanstack/virtual-file-routes@1.115.0': resolution: {integrity: sha512-XLUh1Py3AftcERrxkxC5Y5m5mfllRH3YR6YVlyjFgI2Tc2Ssy2NKmQFQIafoxfW459UJ8Dn81nWKETEIJifE4g==} @@ -1863,6 +1917,21 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@textlint/ast-node-types@14.7.2': + resolution: {integrity: sha512-3rZc9vD8y/DlcFe3Y/cyKRRVgBH4ElEUzVFYdRVDwoMSwV/cIyZgYzVG6ZuOItQt+cHSREuijuucZ4VqZynbtg==} + + '@textlint/linter-formatter@14.7.2': + resolution: {integrity: sha512-QZOqft5uK+o/UN8UcEF3cHgfbG1r3+OWqlJojyjGNkEBbBNPSyDfYlVxDjHqnOAwm7jBaeqVGlwvw/7PUFmsmw==} + + '@textlint/module-interop@14.7.2': + resolution: {integrity: sha512-rDQhFERa2+xMqhyrPFvAL9d5Tb4RpQGKQExwrezvtCTREh6Zsp/nKxtK0r6o0P9xn1+zq2sZHW9NZjpe7av3xw==} + + '@textlint/resolver@14.7.2': + resolution: {integrity: sha512-FCZa9XJx5KihK/4gxXLhS/KfOnBD6vD5UxAMtgrvbifn+JFrW9Kh17uZLCcuJDDJJCnZOHq8jdT7AU+rpmJZ+w==} + + '@textlint/types@14.7.2': + resolution: {integrity: sha512-VpsmtJf9+7cnIxmKtAVVGVzI6f2k09kBZnzjdTAO8JZ+HTmV46jeoVrotpSfQbWDpuQk2UFPfrsZL/LNf/99ew==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -2025,8 +2094,14 @@ packages: '@types/node@20.11.25': resolution: {integrity: sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==} - '@types/node@20.17.46': - resolution: {integrity: sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==} + '@types/node@20.17.50': + resolution: {integrity: sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==} + + '@types/node@22.15.21': + resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} '@types/pluralize@0.0.33': resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} @@ -2039,8 +2114,11 @@ packages: peerDependencies: '@types/react': ^18.0.0 - '@types/react@18.3.21': - resolution: {integrity: sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw==} + '@types/react@18.3.22': + resolution: {integrity: sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==} + + '@types/sarif@2.1.7': + resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2137,22 +2215,22 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitejs/plugin-react-swc@3.9.0': - resolution: {integrity: sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==} + '@vitejs/plugin-react-swc@3.10.0': + resolution: {integrity: sha512-ZmkdHw3wo/o/Rk05YsXZs/DJAfY2CdQ5DUAjoWji+PEr+hYADdGMCGgEAILbiKj+CjspBTuTACBcWDrmC8AUfw==} peerDependencies: vite: ^4 || ^5 || ^6 - '@vitejs/plugin-react@4.4.1': - resolution: {integrity: sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==} + '@vitejs/plugin-react@4.5.0': + resolution: {integrity: sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - '@vitest/expect@3.1.3': - resolution: {integrity: sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==} + '@vitest/expect@3.1.4': + resolution: {integrity: sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==} - '@vitest/mocker@3.1.3': - resolution: {integrity: sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==} + '@vitest/mocker@3.1.4': + resolution: {integrity: sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==} peerDependencies: msw: ^2.4.9 vite: ^5.0.0 || ^6.0.0 @@ -2162,20 +2240,20 @@ packages: vite: optional: true - '@vitest/pretty-format@3.1.3': - resolution: {integrity: sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==} + '@vitest/pretty-format@3.1.4': + resolution: {integrity: sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==} - '@vitest/runner@3.1.3': - resolution: {integrity: sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==} + '@vitest/runner@3.1.4': + resolution: {integrity: sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==} - '@vitest/snapshot@3.1.3': - resolution: {integrity: sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==} + '@vitest/snapshot@3.1.4': + resolution: {integrity: sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==} - '@vitest/spy@3.1.3': - resolution: {integrity: sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==} + '@vitest/spy@3.1.4': + resolution: {integrity: sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==} - '@vitest/utils@3.1.3': - resolution: {integrity: sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==} + '@vitest/utils@3.1.4': + resolution: {integrity: sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==} '@vscode/python-extension@1.0.5': resolution: {integrity: sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==} @@ -2238,8 +2316,8 @@ packages: '@vscode/vsce-sign@2.0.5': resolution: {integrity: sha512-GfYWrsT/vypTMDMgWDm75iDmAOMe7F71sZECJ+Ws6/xyIfmB3ELVnVN+LwMFAvmXY+e6eWhR2EzNGF/zAhWY3Q==} - '@vscode/vsce@3.3.2': - resolution: {integrity: sha512-XQ4IhctYalSTMwLnMS8+nUaGbU7v99Qm2sOoGfIEf2QC7jpiLXZZMh7NwArEFsKX4gHTJLx0/GqAUlCdC3gKCw==} + '@vscode/vsce@3.4.2': + resolution: {integrity: sha512-U2gC7GiQc22nxRpWH4cdW16rRr5u9w+Bjsjm8g8mEjY4aeOG1U6/3XNGq+ElwdeoT8jAyhBmBAuYG7INcSe/6A==} engines: {node: '>= 20'} hasBin: true @@ -2298,10 +2376,6 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2316,6 +2390,10 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -2352,6 +2430,10 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2394,11 +2476,14 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-hidden@1.2.4: - resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} aria-query@5.3.0: @@ -2428,6 +2513,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true @@ -2469,16 +2558,19 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + binaryextensions@4.19.0: + resolution: {integrity: sha512-DRxnVbOi/1OgA5pA9EDiRT8gvVYeqfuN7TmPfLyt6cyho3KbHCi3EtDQf39TTmGDrR5dZ9CspdXhPkL/j/WGbg==} + engines: {node: '>=0.8'} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} - engines: {node: '>=18'} - boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boundary@2.0.0: + resolution: {integrity: sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -2513,10 +2605,6 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - c8@9.1.0: resolution: {integrity: sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==} engines: {node: '>=14.14.0'} @@ -2628,6 +2716,10 @@ packages: classcat@5.0.5: resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -2704,25 +2796,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} - engines: {node: '>= 0.6'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} @@ -2730,10 +2806,6 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -2817,8 +2889,8 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -2875,10 +2947,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dependency-graph@0.11.0: resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} engines: {node: '>= 0.6.0'} @@ -2908,8 +2976,8 @@ packages: resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} engines: {node: '>=0.3.1'} - diff@8.0.1: - resolution: {integrity: sha512-rEaM3KmVm78zE3dFZaop3aCQa2MTm+T4kcigUFLVU/KbOYdiY6JnL2g2puOYnct3QFw9pjZadaCbCZ1O8ArMlQ==} + diff@8.0.2: + resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} dir-glob@3.0.1: @@ -2951,11 +3019,8 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - electron-to-chromium@1.5.152: - resolution: {integrity: sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==} + electron-to-chromium@1.5.157: + resolution: {integrity: sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==} elkjs@0.8.2: resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} @@ -2969,10 +3034,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - encoding-sniffer@0.2.0: resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} @@ -2995,8 +3056,11 @@ packages: resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} engines: {node: '>=0.12'} - es-abstract@1.23.9: - resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-abstract@1.23.10: + resolution: {integrity: sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==} engines: {node: '>= 0.4'} es-aggregate-error@1.0.13: @@ -3038,9 +3102,6 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -3065,8 +3126,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.26.0: - resolution: {integrity: sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==} + eslint@9.27.0: + resolution: {integrity: sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -3079,6 +3140,11 @@ packages: resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -3105,10 +3171,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -3117,14 +3179,6 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - eventsource-parser@3.0.1: - resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==} - engines: {node: '>=18.0.0'} - - eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} - execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -3137,16 +3191,6 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} - express-rate-limit@7.5.0: - resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==} - engines: {node: '>= 16'} - peerDependencies: - express: ^4.11 || 5 || ^5.0.0-beta.1 - - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} - engines: {node: '>= 18'} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3194,10 +3238,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} - engines: {node: '>= 0.8'} - find-replace@5.0.2: resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==} engines: {node: '>=14'} @@ -3237,20 +3277,16 @@ packages: resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} engines: {node: '>= 6'} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fs-extra@11.3.0: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} @@ -3310,8 +3346,8 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.10.0: - resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -3361,6 +3397,10 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + globby@14.1.0: + resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} + engines: {node: '>=18'} + goober@2.1.16: resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==} peerDependencies: @@ -3424,6 +3464,10 @@ packages: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -3437,10 +3481,6 @@ packages: htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -3506,10 +3546,6 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -3520,6 +3556,9 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -3615,9 +3654,6 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3695,11 +3731,15 @@ packages: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} engines: {node: '>=8'} + istextorbinary@6.0.0: + resolution: {integrity: sha512-4j3UqQCa06GAf6QHlN3giz2EeFU7qc6Q5uB/aY7Gmb3xmLDLepDOtsZqkb4sCfJgFvTbLUinNw0kHgHs8XOHoQ==} + engines: {node: '>=10'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.1.0: - resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} jest-worker@27.5.1: @@ -3717,6 +3757,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -3749,6 +3793,10 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-parse-even-better-errors@3.0.2: + resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3814,68 +3862,68 @@ packages: lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} - lightningcss-darwin-arm64@1.29.2: - resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.29.2: - resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.29.2: - resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.29.2: - resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.29.2: - resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.29.2: - resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.29.2: - resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.29.2: - resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.29.2: - resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.29.2: - resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.29.2: - resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} lilconfig@3.1.3: @@ -3885,6 +3933,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lines-and-columns@2.0.4: + resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} @@ -3932,6 +3984,9 @@ packages: lodash.topath@4.5.2: resolution: {integrity: sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==} + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} @@ -4032,14 +4087,6 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -4118,18 +4165,10 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} - mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -4214,10 +4253,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -4251,6 +4286,14 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-sarif-builder@2.0.3: + resolution: {integrity: sha512-Pzr3rol8fvhG/oJjIq2NTVB0vmdNNlz22FENhhPojYRZ4/ee08CfK4YuKmuL54V9MLhI1kpzxfOJ/63LzmZzDg==} + engines: {node: '>=14'} + + normalize-package-data@6.0.2: + resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} + engines: {node: ^16.14.0 || >=18.0.0} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -4305,10 +4348,6 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -4357,6 +4396,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -4370,6 +4413,10 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@7.1.1: + resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} + engines: {node: '>=16'} + parse-semver@1.1.1: resolution: {integrity: sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==} @@ -4382,10 +4429,6 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4409,14 +4452,14 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} - path-to-regexp@8.2.0: - resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} - engines: {node: '>=16'} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + path-type@6.0.0: + resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} + engines: {node: '>=18'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -4446,10 +4489,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkce-challenge@5.0.0: - resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} - engines: {node: '>=16.20.0'} - playwright-core@1.52.0: resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==} engines: {node: '>=18'} @@ -4460,6 +4499,9 @@ packages: engines: {node: '>=18'} hasBin: true + pluralize@2.0.0: + resolution: {integrity: sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -4540,10 +4582,6 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -4565,13 +4603,8 @@ packages: randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} - engines: {node: '>= 0.8'} + rc-config-loader@4.1.3: + resolution: {integrity: sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==} rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} @@ -4626,8 +4659,8 @@ packages: '@types/react': optional: true - react-remove-scroll@2.6.3: - resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==} + react-remove-scroll@2.7.0: + resolution: {integrity: sha512-sGsQtcjMqdQyijAHytfGEELB8FufGbfXIsvUTe+NLx1GDRJCXtCFLBLUI1eyZCKXXvbEU2C6gai0PZKoIE9Vbg==} engines: {node: '>=10'} peerDependencies: '@types/react': '*' @@ -4636,8 +4669,8 @@ packages: '@types/react': optional: true - react-router@7.6.0: - resolution: {integrity: sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==} + react-router@7.6.1: + resolution: {integrity: sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -4674,6 +4707,10 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + read-pkg@8.1.0: + resolution: {integrity: sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==} + engines: {node: '>=16'} + read@1.0.7: resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==} engines: {node: '>=0.8'} @@ -4745,15 +4782,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.40.2: - resolution: {integrity: sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==} + rollup@4.41.1: + resolution: {integrity: sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -4802,6 +4835,11 @@ packages: resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} engines: {node: '>= 10.13.0'} + secretlint@9.3.3: + resolution: {integrity: sha512-JTIsI8BEon8Oo6P7YvGq3I3qCZuYgCvekU8qr4OYyvo6N/wHGg4JMruT5MVkxh3q0diX11xsqaptmeTP5/wNxQ==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -4815,27 +4853,19 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} - serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - seroval-plugins@1.3.1: - resolution: {integrity: sha512-dOlUoiI3fgZbQIcj6By+l865pzeWdP3XCSLdI3xlKnjCk5983yLWPsXytFOUI0BUZKG9qwqbj78n9yVcVwUqaQ==} + seroval-plugins@1.3.2: + resolution: {integrity: sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 - seroval@1.3.1: - resolution: {integrity: sha512-F+T9EQPdLzgdewgxnBh4mSc+vde+EOkU6dC9BDuu/bfGb+UyUlqM6t8znFCTPQSuai/ZcfFg0gu79h+bVW2O0w==} + seroval@1.3.2: + resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} - engines: {node: '>= 18'} - set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} @@ -4854,9 +4884,6 @@ packages: setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -4923,8 +4950,16 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - solid-js@1.9.6: - resolution: {integrity: sha512-PoasAJvLk60hRtOTe9ulvALOdLjjqxuxcGZRolBQqxOnXrBXHGzqMT4ijNhGsDAYdOgEa8ZYaAE94PSldrFSkA==} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + solid-js@1.9.7: + resolution: {integrity: sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==} source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} @@ -4944,16 +4979,27 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.21: + resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + split.js@1.6.5: resolution: {integrity: sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -5022,6 +5068,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + structured-source@4.0.0: + resolution: {integrity: sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==} + style-mod@4.1.2: resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} @@ -5052,6 +5101,10 @@ packages: resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} engines: {node: '>=12'} + supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -5070,20 +5123,24 @@ packages: resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} engines: {node: '>=12.17'} + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + tailwindcss@3.4.17: resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@4.1.6: - resolution: {integrity: sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==} + tailwindcss@4.1.7: + resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==} - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} engines: {node: '>=6'} - tar-fs@2.1.2: - resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} + tar-fs@2.1.3: + resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==} tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} @@ -5093,6 +5150,10 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + terminal-link@2.1.1: + resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} + engines: {node: '>=8'} + terser-webpack-plugin@5.3.14: resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} engines: {node: '>= 10.13.0'} @@ -5118,6 +5179,13 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + textextensions@5.16.0: + resolution: {integrity: sha512-7D/r3s6uPZyU//MCYrX6I14nzauDwJ5CxazouuRGNuvSCihW87ufN6VLoROLCrHg6FblLuJrT6N2BVaPVzqElw==} + engines: {node: '>=0.8'} + thememirror@2.0.1: resolution: {integrity: sha512-d5i6FVvWWPkwrm4cHLI3t9AT1OrkAt7Ig8dtdYSofgF7C/eiyNuq6zQzSTusWTde3jpW9WLvA9J/fzNKMUsd0w==} peerDependencies: @@ -5144,8 +5212,8 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.13: - resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} tinypool@1.0.2: @@ -5175,10 +5243,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -5244,9 +5308,17 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} @@ -5273,8 +5345,8 @@ packages: peerDependencies: typedoc: 0.28.x - typedoc@0.28.4: - resolution: {integrity: sha512-xKvKpIywE1rnqqLgjkoq0F3wOqYaKO9nV6YkkSat6IxOWacUCc/7Es0hR3OPmkIqkPoEn7U3x+sYdG72rstZQA==} + typedoc@0.28.5: + resolution: {integrity: sha512-5PzUddaA9FbaarUzIsEc4wNXCiO4Ot3bJNeMF2qKpYlTmM9TTaSHQ7162w756ERCkXER/+o2purRG6YOAv6EMA==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: @@ -5312,10 +5384,17 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@6.21.3: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -5338,10 +5417,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - unplugin@2.3.4: resolution: {integrity: sha512-m4PjxTurwpWfpMomp8AptjD5yj8qEZN5uQjjGM3TAs9MWWD2tXSSNNj6jGR2FoVGod4293ytyV6SwBbertfyJg==} engines: {node: '>=18.12.0'} @@ -5401,22 +5476,21 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + validator@13.15.0: resolution: {integrity: sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==} engines: {node: '>= 0.10'} - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-node@3.1.3: - resolution: {integrity: sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==} + vite-node@3.1.4: + resolution: {integrity: sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true @@ -5465,16 +5539,16 @@ packages: yaml: optional: true - vitest@3.1.3: - resolution: {integrity: sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==} + vitest@3.1.4: + resolution: {integrity: sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.1.3 - '@vitest/ui': 3.1.3 + '@vitest/browser': 3.1.4 + '@vitest/ui': 3.1.4 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -5521,8 +5595,8 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} - watchpack@2.4.3: - resolution: {integrity: sha512-adBYQLivcg1jbdKEJeqScJJFvgm4qY9+3tXw+jdG6lkVeqRJEtiQmSWjmth8GKmDZuX7sYM4YFxQsf0AzMfGGw==} + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} web-vitals@4.2.4: @@ -5535,8 +5609,8 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + webpack-sources@3.3.0: + resolution: {integrity: sha512-77R0RDmJfj9dyv5p3bM5pOHa+X8/ZkO9c7kpDstigkC4nIDobadsfSGCwB4bKhMVxqAok8tajaoR8rirM7+VFQ==} engines: {node: '>=10.13.0'} webpack-virtual-modules@0.6.2: @@ -5660,9 +5734,9 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.7.1: - resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} - engines: {node: '>= 14'} + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} hasBin: true yargs-parser@20.2.9: @@ -5695,16 +5769,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod-to-json-schema@3.24.5: - resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} - peerDependencies: - zod: ^3.24.1 - - zod@3.25.4: - resolution: {integrity: sha512-7zz8qNtVv37yCd8OeUW37PMXrR0K/zg+6vw+Z2FJ2+oozVdRbFKldkCoqxd9nJflDrx2ZkjUJrPF2DMj+L4pBQ==} + zod@3.25.28: + resolution: {integrity: sha512-/nt/67WYKnr5by3YS7LroZJbtcCBurDKKPBPWWzaxvVCGuG/NOsiKkrjoOhI8mJ+SQUXEbUzeB3S+6XDUEEj7Q==} - zustand@4.5.6: - resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} peerDependencies: '@types/react': '>=16.8' @@ -5718,8 +5787,8 @@ packages: react: optional: true - zustand@5.0.4: - resolution: {integrity: sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==} + zustand@5.0.5: + resolution: {integrity: sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -5741,7 +5810,7 @@ packages: snapshots: - '@adobe/css-tools@4.4.2': {} + '@adobe/css-tools@4.4.3': {} '@alloc/quick-lru@5.2.0': {} @@ -5771,7 +5840,7 @@ snapshots: call-me-maybe: 1.0.2 openapi-types: 12.1.3 - '@asamuzakjp/css-color@3.1.7': + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) '@csstools/css-color-parser': 3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) @@ -5783,6 +5852,12 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 + '@azu/format-text@1.0.2': {} + + '@azu/style-format@1.0.1': + dependencies: + '@azu/format-text': 1.0.2 + '@azure/abort-controller@2.1.2': dependencies: tslib: 2.8.1 @@ -5831,7 +5906,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/identity@4.9.1': + '@azure/identity@4.10.0': dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.9.0 @@ -5887,7 +5962,7 @@ snapshots: '@babel/traverse': 7.27.1 '@babel/types': 7.27.1 convert-source-map: 2.0.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -5978,7 +6053,7 @@ snapshots: '@babel/parser': 7.27.2 '@babel/template': 7.27.2 '@babel/types': 7.27.1 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6004,7 +6079,7 @@ snapshots: '@codemirror/view': 6.36.8 '@lezer/common': 1.2.3 - '@codemirror/lang-python@6.2.0': + '@codemirror/lang-python@6.2.1': dependencies: '@codemirror/autocomplete': 6.18.6 '@codemirror/language': 6.11.0 @@ -6158,9 +6233,9 @@ snapshots: '@esbuild/win32-x64@0.25.4': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.26.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.27.0(jiti@2.4.2))': dependencies: - eslint: 9.26.0(jiti@2.4.2) + eslint: 9.27.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -6168,21 +6243,21 @@ snapshots: '@eslint/config-array@0.20.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color '@eslint/config-helpers@0.2.2': {} - '@eslint/core@0.13.0': + '@eslint/core@0.14.0': dependencies: '@types/json-schema': 7.0.15 '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -6193,13 +6268,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.26.0': {} + '@eslint/js@9.27.0': {} '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.2.8': + '@eslint/plugin-kit@0.3.1': dependencies: - '@eslint/core': 0.13.0 + '@eslint/core': 0.14.0 levn: 0.4.1 '@exodus/schemasafe@1.3.0': {} @@ -6229,20 +6304,20 @@ snapshots: '@floating-ui/utils@0.2.9': {} - '@gerrit0/mini-shiki@3.4.0': + '@gerrit0/mini-shiki@3.4.2': dependencies: - '@shikijs/engine-oniguruma': 3.4.0 - '@shikijs/langs': 3.4.0 - '@shikijs/themes': 3.4.0 - '@shikijs/types': 3.4.0 + '@shikijs/engine-oniguruma': 3.4.2 + '@shikijs/langs': 3.4.2 + '@shikijs/themes': 3.4.2 + '@shikijs/types': 3.4.2 '@shikijs/vscode-textmate': 10.0.2 - '@headlessui/react@2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@headlessui/react@2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/focus': 3.20.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/interactions': 3.25.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/react-virtual': 3.13.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/focus': 3.20.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/interactions': 3.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) use-sync-external-store: 1.5.0(react@18.3.1) @@ -6349,27 +6424,12 @@ snapshots: '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 - '@lit/react@1.0.7(@types/react@18.3.21)': + '@lit/react@1.0.7(@types/react@18.3.22)': dependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 '@marijn/find-cluster-break@1.0.2': {} - '@modelcontextprotocol/sdk@1.11.2': - dependencies: - content-type: 1.0.5 - cors: 2.8.5 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - express: 5.1.0 - express-rate-limit: 7.5.0(express@5.1.0) - pkce-challenge: 5.0.0 - raw-body: 3.0.0 - zod: 3.25.4 - zod-to-json-schema: 3.24.5(zod@3.25.4) - transitivePeerDependencies: - - supports-color - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6406,7 +6466,7 @@ snapshots: ajv: 8.17.1 chalk: 4.1.2 compare-versions: 6.1.1 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) esbuild: 0.25.4 esutils: 2.0.3 fs-extra: 11.3.0 @@ -6498,303 +6558,303 @@ snapshots: '@radix-ui/primitive@1.1.2': {} - '@radix-ui/react-arrow@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-collection@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.22)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-context-menu@2.2.14(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-context-menu@2.2.15(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-context': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-menu': 2.1.14(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-context@1.1.2(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-context@1.1.2(@types/react@18.3.22)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-direction@1.1.1(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-direction@1.1.1(@types/react@18.3.22)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.22)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-focus-scope@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-id@1.1.1(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-id@1.1.1(@types/react@18.3.22)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-menu@2.1.14(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.21)(react@18.3.1) - aria-hidden: 1.2.4 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) + aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.21)(react@18.3.1) + react-remove-scroll: 2.7.0(@types/react@18.3.22)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-popper@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-arrow': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.22)(react@18.3.1) '@radix-ui/rect': 1.1.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-portal@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-primitive@2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-roving-focus@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-select@2.2.4(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-select@2.2.5(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - aria-hidden: 1.2.4 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.21)(react@18.3.1) + react-remove-scroll: 2.7.0(@types/react@18.3.22)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) - '@radix-ui/react-slot@1.2.2(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-slot@1.2.3(@types/react@18.3.22)(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.22)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.22)(react@18.3.1)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.21)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.22)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.22)(react@18.3.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.22)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.22)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.22)(react@18.3.1)': dependencies: '@radix-ui/rect': 1.1.1 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-use-size@1.1.1(@types/react@18.3.21)(react@18.3.1)': + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.22)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.21)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@radix-ui/react-visually-hidden@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) '@radix-ui/rect@1.1.1': {} - '@react-aria/focus@3.20.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/focus@3.20.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@react-aria/interactions': 3.25.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/utils': 3.28.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-types/shared': 3.29.0(react@18.3.1) + '@react-aria/interactions': 3.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.29.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.29.1(react@18.3.1) '@swc/helpers': 0.5.17 clsx: 2.1.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@react-aria/interactions@3.25.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/interactions@3.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@react-aria/ssr': 3.9.8(react@18.3.1) - '@react-aria/utils': 3.28.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.29.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-stately/flags': 3.1.1 - '@react-types/shared': 3.29.0(react@18.3.1) + '@react-types/shared': 3.29.1(react@18.3.1) '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -6804,12 +6864,12 @@ snapshots: '@swc/helpers': 0.5.17 react: 18.3.1 - '@react-aria/utils@3.28.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/utils@3.29.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@react-aria/ssr': 3.9.8(react@18.3.1) '@react-stately/flags': 3.1.1 '@react-stately/utils': 3.10.6(react@18.3.1) - '@react-types/shared': 3.29.0(react@18.3.1) + '@react-types/shared': 3.29.1(react@18.3.1) '@swc/helpers': 0.5.17 clsx: 2.1.1 react: 18.3.1 @@ -6830,33 +6890,33 @@ snapshots: '@swc/helpers': 0.5.17 react: 18.3.1 - '@react-types/shared@3.29.0(react@18.3.1)': + '@react-types/shared@3.29.1(react@18.3.1)': dependencies: react: 18.3.1 - '@reactflow/background@11.3.14(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/background@11.3.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classcat: 5.0.5 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.6(@types/react@18.3.21)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/controls@11.2.14(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/controls@11.2.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classcat: 5.0.5 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.6(@types/react@18.3.21)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/core@11.11.4(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/core@11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@types/d3': 7.4.3 '@types/d3-drag': 3.0.7 @@ -6868,14 +6928,14 @@ snapshots: d3-zoom: 3.0.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.6(@types/react@18.3.21)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/minimap@11.7.14(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/minimap@11.7.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/d3-selection': 3.0.11 '@types/d3-zoom': 3.0.8 classcat: 5.0.5 @@ -6883,115 +6943,193 @@ snapshots: d3-zoom: 3.0.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.6(@types/react@18.3.21)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-resizer@2.2.14(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/node-resizer@2.2.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classcat: 5.0.5 d3-drag: 3.0.0 d3-selection: 3.0.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.6(@types/react@18.3.21)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-toolbar@1.3.14(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/node-toolbar@1.3.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classcat: 5.0.5 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.6(@types/react@18.3.21)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer - '@rollup/rollup-android-arm-eabi@4.40.2': + '@rolldown/pluginutils@1.0.0-beta.9': {} + + '@rollup/rollup-android-arm-eabi@4.41.1': optional: true - '@rollup/rollup-android-arm64@4.40.2': + '@rollup/rollup-android-arm64@4.41.1': optional: true - '@rollup/rollup-darwin-arm64@4.40.2': + '@rollup/rollup-darwin-arm64@4.41.1': optional: true - '@rollup/rollup-darwin-x64@4.40.2': + '@rollup/rollup-darwin-x64@4.41.1': optional: true - '@rollup/rollup-freebsd-arm64@4.40.2': + '@rollup/rollup-freebsd-arm64@4.41.1': optional: true - '@rollup/rollup-freebsd-x64@4.40.2': + '@rollup/rollup-freebsd-x64@4.41.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.40.2': + '@rollup/rollup-linux-arm-gnueabihf@4.41.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.40.2': + '@rollup/rollup-linux-arm-musleabihf@4.41.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.40.2': + '@rollup/rollup-linux-arm64-gnu@4.41.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.40.2': + '@rollup/rollup-linux-arm64-musl@4.41.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.40.2': + '@rollup/rollup-linux-loongarch64-gnu@4.41.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': + '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.40.2': + '@rollup/rollup-linux-riscv64-gnu@4.41.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.40.2': + '@rollup/rollup-linux-riscv64-musl@4.41.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.40.2': + '@rollup/rollup-linux-s390x-gnu@4.41.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.40.2': + '@rollup/rollup-linux-x64-gnu@4.41.1': optional: true - '@rollup/rollup-linux-x64-musl@4.40.2': + '@rollup/rollup-linux-x64-musl@4.41.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.40.2': + '@rollup/rollup-win32-arm64-msvc@4.41.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.40.2': + '@rollup/rollup-win32-ia32-msvc@4.41.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.40.2': + '@rollup/rollup-win32-x64-msvc@4.41.1': optional: true - '@shikijs/engine-oniguruma@3.4.0': + '@secretlint/config-creator@9.3.3': + dependencies: + '@secretlint/types': 9.3.3 + + '@secretlint/config-loader@9.3.3': dependencies: - '@shikijs/types': 3.4.0 + '@secretlint/profiler': 9.3.3 + '@secretlint/resolver': 9.3.3 + '@secretlint/types': 9.3.3 + ajv: 8.17.1 + debug: 4.4.1(supports-color@8.1.1) + rc-config-loader: 4.1.3 + transitivePeerDependencies: + - supports-color + + '@secretlint/core@9.3.3': + dependencies: + '@secretlint/profiler': 9.3.3 + '@secretlint/types': 9.3.3 + debug: 4.4.1(supports-color@8.1.1) + structured-source: 4.0.0 + transitivePeerDependencies: + - supports-color + + '@secretlint/formatter@9.3.3': + dependencies: + '@secretlint/resolver': 9.3.3 + '@secretlint/types': 9.3.3 + '@textlint/linter-formatter': 14.7.2 + '@textlint/module-interop': 14.7.2 + '@textlint/types': 14.7.2 + chalk: 4.1.2 + debug: 4.4.1(supports-color@8.1.1) + pluralize: 8.0.0 + strip-ansi: 6.0.1 + table: 6.9.0 + terminal-link: 2.1.1 + transitivePeerDependencies: + - supports-color + + '@secretlint/node@9.3.3': + dependencies: + '@secretlint/config-loader': 9.3.3 + '@secretlint/core': 9.3.3 + '@secretlint/formatter': 9.3.3 + '@secretlint/profiler': 9.3.3 + '@secretlint/source-creator': 9.3.3 + '@secretlint/types': 9.3.3 + debug: 4.4.1(supports-color@8.1.1) + p-map: 4.0.0 + transitivePeerDependencies: + - supports-color + + '@secretlint/profiler@9.3.3': {} + + '@secretlint/resolver@9.3.3': {} + + '@secretlint/secretlint-formatter-sarif@9.3.3': + dependencies: + node-sarif-builder: 2.0.3 + + '@secretlint/secretlint-rule-no-dotenv@9.3.3': + dependencies: + '@secretlint/types': 9.3.3 + + '@secretlint/secretlint-rule-preset-recommend@9.3.3': {} + + '@secretlint/source-creator@9.3.3': + dependencies: + '@secretlint/types': 9.3.3 + istextorbinary: 6.0.0 + + '@secretlint/types@9.3.3': {} + + '@shikijs/engine-oniguruma@3.4.2': + dependencies: + '@shikijs/types': 3.4.2 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.4.0': + '@shikijs/langs@3.4.2': dependencies: - '@shikijs/types': 3.4.0 + '@shikijs/types': 3.4.2 - '@shikijs/themes@3.4.0': + '@shikijs/themes@3.4.2': dependencies: - '@shikijs/types': 3.4.0 + '@shikijs/types': 3.4.2 - '@shikijs/types@3.4.0': + '@shikijs/types@3.4.2': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/merge-streams@2.3.0': {} + '@stoplight/better-ajv-errors@1.0.3(ajv@8.17.1)': dependencies: ajv: 8.17.1 @@ -7155,51 +7293,51 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 - '@swc/core-darwin-arm64@1.11.24': + '@swc/core-darwin-arm64@1.11.29': optional: true - '@swc/core-darwin-x64@1.11.24': + '@swc/core-darwin-x64@1.11.29': optional: true - '@swc/core-linux-arm-gnueabihf@1.11.24': + '@swc/core-linux-arm-gnueabihf@1.11.29': optional: true - '@swc/core-linux-arm64-gnu@1.11.24': + '@swc/core-linux-arm64-gnu@1.11.29': optional: true - '@swc/core-linux-arm64-musl@1.11.24': + '@swc/core-linux-arm64-musl@1.11.29': optional: true - '@swc/core-linux-x64-gnu@1.11.24': + '@swc/core-linux-x64-gnu@1.11.29': optional: true - '@swc/core-linux-x64-musl@1.11.24': + '@swc/core-linux-x64-musl@1.11.29': optional: true - '@swc/core-win32-arm64-msvc@1.11.24': + '@swc/core-win32-arm64-msvc@1.11.29': optional: true - '@swc/core-win32-ia32-msvc@1.11.24': + '@swc/core-win32-ia32-msvc@1.11.29': optional: true - '@swc/core-win32-x64-msvc@1.11.24': + '@swc/core-win32-x64-msvc@1.11.29': optional: true - '@swc/core@1.11.24(@swc/helpers@0.5.17)': + '@swc/core@1.11.29(@swc/helpers@0.5.17)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.21 optionalDependencies: - '@swc/core-darwin-arm64': 1.11.24 - '@swc/core-darwin-x64': 1.11.24 - '@swc/core-linux-arm-gnueabihf': 1.11.24 - '@swc/core-linux-arm64-gnu': 1.11.24 - '@swc/core-linux-arm64-musl': 1.11.24 - '@swc/core-linux-x64-gnu': 1.11.24 - '@swc/core-linux-x64-musl': 1.11.24 - '@swc/core-win32-arm64-msvc': 1.11.24 - '@swc/core-win32-ia32-msvc': 1.11.24 - '@swc/core-win32-x64-msvc': 1.11.24 + '@swc/core-darwin-arm64': 1.11.29 + '@swc/core-darwin-x64': 1.11.29 + '@swc/core-linux-arm-gnueabihf': 1.11.29 + '@swc/core-linux-arm64-gnu': 1.11.29 + '@swc/core-linux-arm64-musl': 1.11.29 + '@swc/core-linux-x64-gnu': 1.11.29 + '@swc/core-linux-x64-musl': 1.11.29 + '@swc/core-win32-arm64-msvc': 1.11.29 + '@swc/core-win32-ia32-msvc': 1.11.29 + '@swc/core-win32-x64-msvc': 1.11.29 '@swc/helpers': 0.5.17 '@swc/counter@0.1.3': {} @@ -7216,111 +7354,111 @@ snapshots: dependencies: tailwindcss: 3.4.17 - '@tailwindcss/node@4.1.6': + '@tailwindcss/node@4.1.7': dependencies: '@ampproject/remapping': 2.3.0 enhanced-resolve: 5.18.1 jiti: 2.4.2 - lightningcss: 1.29.2 + lightningcss: 1.30.1 magic-string: 0.30.17 source-map-js: 1.2.1 - tailwindcss: 4.1.6 + tailwindcss: 4.1.7 - '@tailwindcss/oxide-android-arm64@4.1.6': + '@tailwindcss/oxide-android-arm64@4.1.7': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.6': + '@tailwindcss/oxide-darwin-arm64@4.1.7': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.6': + '@tailwindcss/oxide-darwin-x64@4.1.7': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.6': + '@tailwindcss/oxide-freebsd-x64@4.1.7': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.6': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.6': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.6': + '@tailwindcss/oxide-linux-arm64-musl@4.1.7': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.6': + '@tailwindcss/oxide-linux-x64-gnu@4.1.7': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.6': + '@tailwindcss/oxide-linux-x64-musl@4.1.7': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.6': + '@tailwindcss/oxide-wasm32-wasi@4.1.7': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.6': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.6': + '@tailwindcss/oxide-win32-x64-msvc@4.1.7': optional: true - '@tailwindcss/oxide@4.1.6': + '@tailwindcss/oxide@4.1.7': dependencies: detect-libc: 2.0.4 tar: 7.4.3 optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.6 - '@tailwindcss/oxide-darwin-arm64': 4.1.6 - '@tailwindcss/oxide-darwin-x64': 4.1.6 - '@tailwindcss/oxide-freebsd-x64': 4.1.6 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.6 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.6 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.6 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.6 - '@tailwindcss/oxide-linux-x64-musl': 4.1.6 - '@tailwindcss/oxide-wasm32-wasi': 4.1.6 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.6 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.6 - - '@tailwindcss/postcss@4.1.6': + '@tailwindcss/oxide-android-arm64': 4.1.7 + '@tailwindcss/oxide-darwin-arm64': 4.1.7 + '@tailwindcss/oxide-darwin-x64': 4.1.7 + '@tailwindcss/oxide-freebsd-x64': 4.1.7 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.7 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.7 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.7 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.7 + '@tailwindcss/oxide-linux-x64-musl': 4.1.7 + '@tailwindcss/oxide-wasm32-wasi': 4.1.7 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.7 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.7 + + '@tailwindcss/postcss@4.1.7': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.6 - '@tailwindcss/oxide': 4.1.6 + '@tailwindcss/node': 4.1.7 + '@tailwindcss/oxide': 4.1.7 postcss: 8.5.3 - tailwindcss: 4.1.6 + tailwindcss: 4.1.7 - '@tailwindcss/vite@4.1.6(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1))': + '@tailwindcss/vite@4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: - '@tailwindcss/node': 4.1.6 - '@tailwindcss/oxide': 4.1.6 - tailwindcss: 4.1.6 - vite: 6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + '@tailwindcss/node': 4.1.7 + '@tailwindcss/oxide': 4.1.7 + tailwindcss: 4.1.7 + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) '@tanstack/history@1.115.0': {} - '@tanstack/query-core@5.76.0': {} + '@tanstack/query-core@5.77.2': {} - '@tanstack/react-query@5.76.1(react@18.3.1)': + '@tanstack/react-query@5.77.2(react@18.3.1)': dependencies: - '@tanstack/query-core': 5.76.0 + '@tanstack/query-core': 5.77.2 react: 18.3.1 - '@tanstack/react-router-devtools@1.120.3(@tanstack/react-router@1.120.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.3)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3)': + '@tanstack/react-router-devtools@1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.10)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3)': dependencies: - '@tanstack/react-router': 1.120.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-devtools-core': 1.120.3(@tanstack/router-core@1.120.3)(csstype@3.1.3)(solid-js@1.9.6)(tiny-invariant@1.3.3) + '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/router-devtools-core': 1.120.10(@tanstack/router-core@1.120.10)(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) - solid-js: 1.9.6 + solid-js: 1.9.7 transitivePeerDependencies: - '@tanstack/router-core' - csstype - tiny-invariant - '@tanstack/react-router@1.120.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/history': 1.115.0 '@tanstack/react-store': 0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-core': 1.120.3 + '@tanstack/router-core': 1.120.10 jsesc: 3.1.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -7340,38 +7478,38 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@tanstack/react-virtual@3.13.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-virtual@3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/virtual-core': 3.13.8 + '@tanstack/virtual-core': 3.13.9 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@tanstack/router-core@1.120.3': + '@tanstack/router-core@1.120.10': dependencies: '@tanstack/history': 1.115.0 '@tanstack/store': 0.7.0 tiny-invariant: 1.3.3 - '@tanstack/router-devtools-core@1.120.3(@tanstack/router-core@1.120.3)(csstype@3.1.3)(solid-js@1.9.6)(tiny-invariant@1.3.3)': + '@tanstack/router-devtools-core@1.120.10(@tanstack/router-core@1.120.10)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': dependencies: - '@tanstack/router-core': 1.120.3 + '@tanstack/router-core': 1.120.10 clsx: 2.1.1 goober: 2.1.16(csstype@3.1.3) - solid-js: 1.9.6 + solid-js: 1.9.7 tiny-invariant: 1.3.3 optionalDependencies: csstype: 3.1.3 - '@tanstack/router-generator@1.120.3(@tanstack/react-router@1.120.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@tanstack/router-generator@1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: '@tanstack/virtual-file-routes': 1.115.0 prettier: 3.5.3 tsx: 4.19.4 - zod: 3.25.4 + zod: 3.25.28 optionalDependencies: - '@tanstack/react-router': 1.120.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-plugin@1.120.3(@tanstack/react-router@1.120.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1))(webpack@5.99.8)': + '@tanstack/router-plugin@1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8)': dependencies: '@babel/core': 7.27.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1) @@ -7379,8 +7517,8 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.27.1 '@babel/types': 7.27.1 - '@tanstack/router-core': 1.120.3 - '@tanstack/router-generator': 1.120.3(@tanstack/react-router@1.120.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@tanstack/router-core': 1.120.10 + '@tanstack/router-generator': 1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@tanstack/router-utils': 1.115.0 '@tanstack/virtual-file-routes': 1.115.0 '@types/babel__core': 7.20.5 @@ -7389,10 +7527,10 @@ snapshots: babel-dead-code-elimination: 1.0.10 chokidar: 3.6.0 unplugin: 2.3.4 - zod: 3.25.4 + zod: 3.25.28 optionalDependencies: - '@tanstack/react-router': 1.120.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite: 6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) webpack: 5.99.8 transitivePeerDependencies: - supports-color @@ -7408,7 +7546,7 @@ snapshots: '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.8': {} + '@tanstack/virtual-core@3.13.9': {} '@tanstack/virtual-file-routes@1.115.0': {} @@ -7425,7 +7563,7 @@ snapshots: '@testing-library/jest-dom@6.6.3': dependencies: - '@adobe/css-tools': 4.4.2 + '@adobe/css-tools': 4.4.3 aria-query: 5.3.2 chalk: 3.0.0 css.escape: 1.5.1 @@ -7433,20 +7571,49 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.27.1 '@testing-library/dom': 10.4.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 - '@types/react-dom': 18.3.7(@types/react@18.3.21) + '@types/react': 18.3.22 + '@types/react-dom': 18.3.7(@types/react@18.3.22) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': dependencies: '@testing-library/dom': 10.4.0 + '@textlint/ast-node-types@14.7.2': {} + + '@textlint/linter-formatter@14.7.2': + dependencies: + '@azu/format-text': 1.0.2 + '@azu/style-format': 1.0.1 + '@textlint/module-interop': 14.7.2 + '@textlint/resolver': 14.7.2 + '@textlint/types': 14.7.2 + chalk: 4.1.2 + debug: 4.4.1(supports-color@8.1.1) + js-yaml: 3.14.1 + lodash: 4.17.21 + pluralize: 2.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + table: 6.9.0 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + '@textlint/module-interop@14.7.2': {} + + '@textlint/resolver@14.7.2': {} + + '@textlint/types@14.7.2': + dependencies: + '@textlint/ast-node-types': 14.7.2 + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -7597,7 +7764,7 @@ snapshots: '@types/es-aggregate-error@1.0.6': dependencies: - '@types/node': 20.11.25 + '@types/node': 22.15.21 '@types/eslint-scope@3.7.7': dependencies: @@ -7646,23 +7813,31 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.17.46': + '@types/node@20.17.50': dependencies: undici-types: 6.19.8 + '@types/node@22.15.21': + dependencies: + undici-types: 6.21.0 + + '@types/normalize-package-data@2.4.4': {} + '@types/pluralize@0.0.33': {} '@types/prop-types@15.7.14': {} - '@types/react-dom@18.3.7(@types/react@18.3.21)': + '@types/react-dom@18.3.7(@types/react@18.3.22)': dependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - '@types/react@18.3.21': + '@types/react@18.3.22': dependencies: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/sarif@2.1.7': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -7671,15 +7846,15 @@ snapshots: '@types/vscode@1.96.0': {} - '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.32.1 - '@typescript-eslint/type-utils': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.32.1 - eslint: 9.26.0(jiti@2.4.2) + eslint: 9.27.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.4 natural-compare: 1.4.0 @@ -7688,14 +7863,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.32.1 '@typescript-eslint/types': 8.32.1 '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.32.1 - debug: 4.4.0(supports-color@8.1.1) - eslint: 9.26.0(jiti@2.4.2) + debug: 4.4.1(supports-color@8.1.1) + eslint: 9.27.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -7705,12 +7880,12 @@ snapshots: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/visitor-keys': 8.32.1 - '@typescript-eslint/type-utils@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.0(supports-color@8.1.1) - eslint: 9.26.0(jiti@2.4.2) + '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + debug: 4.4.1(supports-color@8.1.1) + eslint: 9.27.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -7722,7 +7897,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/visitor-keys': 8.32.1 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -7732,13 +7907,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.32.1 '@typescript-eslint/types': 8.32.1 '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) - eslint: 9.26.0(jiti@2.4.2) + eslint: 9.27.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -7790,61 +7965,63 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@3.9.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1))': + '@vitejs/plugin-react-swc@3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: - '@swc/core': 1.11.24(@swc/helpers@0.5.17) - vite: 6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + '@rolldown/pluginutils': 1.0.0-beta.9 + '@swc/core': 1.11.29(@swc/helpers@0.5.17) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1))': + '@vitejs/plugin-react@4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.1 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1) + '@rolldown/pluginutils': 1.0.0-beta.9 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitest/expect@3.1.3': + '@vitest/expect@3.1.4': dependencies: - '@vitest/spy': 3.1.3 - '@vitest/utils': 3.1.3 + '@vitest/spy': 3.1.4 + '@vitest/utils': 3.1.4 chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.3(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1))': + '@vitest/mocker@3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: - '@vitest/spy': 3.1.3 + '@vitest/spy': 3.1.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - '@vitest/pretty-format@3.1.3': + '@vitest/pretty-format@3.1.4': dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.1.3': + '@vitest/runner@3.1.4': dependencies: - '@vitest/utils': 3.1.3 + '@vitest/utils': 3.1.4 pathe: 2.0.3 - '@vitest/snapshot@3.1.3': + '@vitest/snapshot@3.1.4': dependencies: - '@vitest/pretty-format': 3.1.3 + '@vitest/pretty-format': 3.1.4 magic-string: 0.30.17 pathe: 2.0.3 - '@vitest/spy@3.1.3': + '@vitest/spy@3.1.4': dependencies: tinyspy: 3.0.2 - '@vitest/utils@3.1.3': + '@vitest/utils@3.1.4': dependencies: - '@vitest/pretty-format': 3.1.3 + '@vitest/pretty-format': 3.1.4 loupe: 3.1.3 tinyrainbow: 2.0.0 @@ -7911,9 +8088,13 @@ snapshots: '@vscode/vsce-sign-win32-arm64': 2.0.2 '@vscode/vsce-sign-win32-x64': 2.0.2 - '@vscode/vsce@3.3.2': + '@vscode/vsce@3.4.2': dependencies: - '@azure/identity': 4.9.1 + '@azure/identity': 4.10.0 + '@secretlint/node': 9.3.3 + '@secretlint/secretlint-formatter-sarif': 9.3.3 + '@secretlint/secretlint-rule-no-dotenv': 9.3.3 + '@secretlint/secretlint-rule-preset-recommend': 9.3.3 '@vscode/vsce-sign': 2.0.5 azure-devops-node-api: 12.5.0 chalk: 2.4.2 @@ -7930,6 +8111,7 @@ snapshots: minimatch: 3.1.2 parse-semver: 1.1.1 read: 1.0.7 + secretlint: 9.3.3 semver: 7.7.2 tmp: 0.2.3 typed-rest-client: 1.8.11 @@ -8026,11 +8208,6 @@ snapshots: dependencies: event-target-shim: 5.0.1 - accepts@2.0.0: - dependencies: - mime-types: 3.0.1 - negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 @@ -8039,6 +8216,11 @@ snapshots: agent-base@7.1.3: {} + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -8072,6 +8254,10 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -8102,7 +8288,7 @@ snapshots: '@swc/helpers': 0.5.17 '@types/command-line-args': 5.2.3 '@types/command-line-usage': 5.0.4 - '@types/node': 20.17.46 + '@types/node': 20.17.50 command-line-args: 6.0.1 command-line-usage: 7.0.3 flatbuffers: 24.12.23 @@ -8113,9 +8299,13 @@ snapshots: arg@5.0.2: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} - aria-hidden@1.2.4: + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 @@ -8139,13 +8329,15 @@ snapshots: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.23.10 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 assertion-error@2.0.1: {} + astral-regex@2.0.0: {} + astring@1.9.0: {} async-function@1.0.0: {} @@ -8189,6 +8381,8 @@ snapshots: binary-extensions@2.3.0: {} + binaryextensions@4.19.0: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -8196,22 +8390,10 @@ snapshots: readable-stream: 3.6.2 optional: true - body-parser@2.2.0: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.0(supports-color@8.1.1) - http-errors: 2.0.0 - iconv-lite: 0.6.3 - on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.0 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - boolbase@1.0.0: {} + boundary@2.0.0: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -8230,7 +8412,7 @@ snapshots: browserslist@4.24.5: dependencies: caniuse-lite: 1.0.30001718 - electron-to-chromium: 1.5.152 + electron-to-chromium: 1.5.157 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.5) @@ -8250,8 +8432,6 @@ snapshots: dependencies: run-applescript: 7.0.0 - bytes@3.1.2: {} - c8@9.1.0: dependencies: '@bcoe/v8-coverage': 0.2.3 @@ -8385,6 +8565,8 @@ snapshots: classcat@5.0.5: {} + clean-stack@2.2.0: {} + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -8459,27 +8641,12 @@ snapshots: concat-map@0.0.1: {} - content-disposition@1.0.0: - dependencies: - safe-buffer: 5.2.1 - - content-type@1.0.5: {} - convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - cookie@1.0.2: {} core-util-is@1.0.3: {} - cors@2.8.5: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - crelt@1.0.6: {} cross-spawn@7.0.6: @@ -8504,7 +8671,7 @@ snapshots: cssstyle@4.3.1: dependencies: - '@asamuzakjp/css-color': 3.1.7 + '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 csstype@3.1.3: {} @@ -8568,7 +8735,7 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - debug@4.4.0(supports-color@8.1.1): + debug@4.4.1(supports-color@8.1.1): dependencies: ms: 2.1.3 optionalDependencies: @@ -8617,8 +8784,6 @@ snapshots: delayed-stream@1.0.0: {} - depd@2.0.0: {} - dependency-graph@0.11.0: {} dequal@2.0.3: {} @@ -8637,7 +8802,7 @@ snapshots: diff@7.0.0: {} - diff@8.0.1: {} + diff@8.0.2: {} dir-glob@3.0.1: dependencies: @@ -8685,9 +8850,7 @@ snapshots: dependencies: safe-buffer: 5.2.1 - ee-first@1.1.1: {} - - electron-to-chromium@1.5.152: {} + electron-to-chromium@1.5.157: {} elkjs@0.8.2: {} @@ -8697,8 +8860,6 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} - encoding-sniffer@0.2.0: dependencies: iconv-lite: 0.6.3 @@ -8712,7 +8873,7 @@ snapshots: enhanced-resolve@5.18.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.1 + tapable: 2.2.2 enquirer@2.4.1: dependencies: @@ -8723,7 +8884,11 @@ snapshots: entities@6.0.0: {} - es-abstract@1.23.9: + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.23.10: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 @@ -8781,7 +8946,7 @@ snapshots: dependencies: define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.23.10 es-errors: 1.3.0 function-bind: 1.1.2 globalthis: 1.0.4 @@ -8843,8 +9008,6 @@ snapshots: escalade@3.2.0: {} - escape-html@1.0.3: {} - escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -8863,26 +9026,25 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.26.0(jiti@2.4.2): + eslint@9.27.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.20.0 '@eslint/config-helpers': 0.2.2 - '@eslint/core': 0.13.0 + '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.26.0 - '@eslint/plugin-kit': 0.2.8 + '@eslint/js': 9.27.0 + '@eslint/plugin-kit': 0.3.1 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@modelcontextprotocol/sdk': 1.11.2 '@types/estree': 1.0.7 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 @@ -8901,7 +9063,6 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - zod: 3.25.4 optionalDependencies: jiti: 2.4.2 transitivePeerDependencies: @@ -8913,6 +9074,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 4.2.0 + esprima@4.0.1: {} + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -8933,18 +9096,10 @@ snapshots: esutils@2.0.3: {} - etag@1.8.1: {} - event-target-shim@5.0.1: {} events@3.3.0: {} - eventsource-parser@3.0.1: {} - - eventsource@3.0.7: - dependencies: - eventsource-parser: 3.0.1 - execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -8962,42 +9117,6 @@ snapshots: expect-type@1.2.1: {} - express-rate-limit@7.5.0(express@5.1.0): - dependencies: - express: 5.1.0 - - express@5.1.0: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.0 - content-disposition: 1.0.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.0(supports-color@8.1.1) - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.0 - fresh: 2.0.0 - http-errors: 2.0.0 - merge-descriptors: 2.0.0 - mime-types: 3.0.1 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.0 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 - statuses: 2.0.1 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -9040,17 +9159,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.0: - dependencies: - debug: 4.4.0(supports-color@8.1.1) - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - find-replace@5.0.2: {} find-up@5.0.0: @@ -9085,15 +9193,17 @@ snapshots: es-set-tostringtag: 2.1.0 mime-types: 2.1.35 - forwarded@0.2.0: {} - fraction.js@4.3.7: {} - fresh@2.0.0: {} - fs-constants@1.0.0: optional: true + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs-extra@11.3.0: dependencies: graceful-fs: 4.2.11 @@ -9155,7 +9265,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.10.0: + get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -9184,7 +9294,7 @@ snapshots: glob@11.0.2: dependencies: foreground-child: 3.3.1 - jackspeak: 4.1.0 + jackspeak: 4.1.1 minimatch: 10.0.1 minipass: 7.1.2 package-json-from-dist: 1.0.1 @@ -9225,6 +9335,15 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + globby@14.1.0: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.3 + ignore: 7.0.4 + path-type: 6.0.0 + slash: 5.1.0 + unicorn-magic: 0.3.0 + goober@2.1.16(csstype@3.1.3): dependencies: csstype: 3.1.3 @@ -9293,6 +9412,10 @@ snapshots: dependencies: lru-cache: 6.0.0 + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -9308,18 +9431,10 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -9328,7 +9443,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -9376,8 +9491,6 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - ipaddr.js@1.9.1: {} - is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -9391,6 +9504,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -9475,8 +9590,6 @@ snapshots: is-potential-custom-element-name@1.0.1: {} - is-promise@4.0.0: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -9547,13 +9660,18 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + istextorbinary@6.0.0: + dependencies: + binaryextensions: 4.19.0 + textextensions: 5.16.0 + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jackspeak@4.1.0: + jackspeak@4.1.1: dependencies: '@isaacs/cliui': 8.0.2 @@ -9569,6 +9687,11 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -9610,6 +9733,8 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-parse-even-better-errors@3.0.2: {} + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -9690,55 +9815,57 @@ snapshots: dependencies: immediate: 3.0.6 - lightningcss-darwin-arm64@1.29.2: + lightningcss-darwin-arm64@1.30.1: optional: true - lightningcss-darwin-x64@1.29.2: + lightningcss-darwin-x64@1.30.1: optional: true - lightningcss-freebsd-x64@1.29.2: + lightningcss-freebsd-x64@1.30.1: optional: true - lightningcss-linux-arm-gnueabihf@1.29.2: + lightningcss-linux-arm-gnueabihf@1.30.1: optional: true - lightningcss-linux-arm64-gnu@1.29.2: + lightningcss-linux-arm64-gnu@1.30.1: optional: true - lightningcss-linux-arm64-musl@1.29.2: + lightningcss-linux-arm64-musl@1.30.1: optional: true - lightningcss-linux-x64-gnu@1.29.2: + lightningcss-linux-x64-gnu@1.30.1: optional: true - lightningcss-linux-x64-musl@1.29.2: + lightningcss-linux-x64-musl@1.30.1: optional: true - lightningcss-win32-arm64-msvc@1.29.2: + lightningcss-win32-arm64-msvc@1.30.1: optional: true - lightningcss-win32-x64-msvc@1.29.2: + lightningcss-win32-x64-msvc@1.30.1: optional: true - lightningcss@1.29.2: + lightningcss@1.30.1: dependencies: detect-libc: 2.0.4 optionalDependencies: - lightningcss-darwin-arm64: 1.29.2 - lightningcss-darwin-x64: 1.29.2 - lightningcss-freebsd-x64: 1.29.2 - lightningcss-linux-arm-gnueabihf: 1.29.2 - lightningcss-linux-arm64-gnu: 1.29.2 - lightningcss-linux-arm64-musl: 1.29.2 - lightningcss-linux-x64-gnu: 1.29.2 - lightningcss-linux-x64-musl: 1.29.2 - lightningcss-win32-arm64-msvc: 1.29.2 - lightningcss-win32-x64-msvc: 1.29.2 + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} + lines-and-columns@2.0.4: {} + linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 @@ -9773,6 +9900,8 @@ snapshots: lodash.topath@4.5.2: {} + lodash.truncate@4.4.2: {} + lodash.uniq@4.5.0: {} lodash.uniqby@4.7.0: {} @@ -9929,10 +10058,6 @@ snapshots: mdurl@2.0.0: {} - media-typer@1.1.0: {} - - merge-descriptors@2.0.0: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -10051,7 +10176,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -10077,16 +10202,10 @@ snapshots: mime-db@1.52.0: {} - mime-db@1.54.0: {} - mime-types@2.1.35: dependencies: mime-db: 1.52.0 - mime-types@3.0.1: - dependencies: - mime-db: 1.54.0 - mime@1.6.0: {} mimic-fn@2.1.0: {} @@ -10137,7 +10256,7 @@ snapshots: ansi-colors: 4.1.3 browser-stdout: 1.3.1 chokidar: 3.6.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) diff: 5.2.0 escape-string-regexp: 4.0.0 find-up: 5.0.0 @@ -10172,8 +10291,6 @@ snapshots: natural-compare@1.4.0: {} - negotiator@1.0.0: {} - neo-async@2.6.2: {} nimma@0.2.3: @@ -10208,6 +10325,17 @@ snapshots: node-releases@2.0.19: {} + node-sarif-builder@2.0.3: + dependencies: + '@types/sarif': 2.1.7 + fs-extra: 10.1.0 + + normalize-package-data@6.0.2: + dependencies: + hosted-git-info: 7.0.2 + semver: 7.7.2 + validate-npm-package-license: 3.0.4 + normalize-path@3.0.0: {} normalize-range@0.1.2: {} @@ -10270,10 +10398,6 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -10297,11 +10421,11 @@ snapshots: openapi3-ts@4.2.2: dependencies: - yaml: 2.7.1 + yaml: 2.8.0 openapi3-ts@4.4.0: dependencies: - yaml: 2.7.1 + yaml: 2.8.0 optionator@0.9.4: dependencies: @@ -10349,8 +10473,8 @@ snapshots: openapi3-ts: 4.2.2 string-argv: 0.3.2 tsconfck: 2.1.2(typescript@5.8.3) - typedoc: 0.28.4(typescript@5.8.3) - typedoc-plugin-markdown: 4.6.3(typedoc@0.28.4(typescript@5.8.3)) + typedoc: 0.28.5(typescript@5.8.3) + typedoc-plugin-markdown: 4.6.3(typedoc@0.28.5(typescript@5.8.3)) typescript: 5.8.3 transitivePeerDependencies: - encoding @@ -10371,6 +10495,10 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + package-json-from-dist@1.0.1: {} pako@1.0.11: {} @@ -10389,6 +10517,14 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-json@7.1.1: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 3.0.2 + lines-and-columns: 2.0.4 + type-fest: 3.13.1 + parse-semver@1.1.1: dependencies: semver: 5.7.2 @@ -10406,8 +10542,6 @@ snapshots: dependencies: entities: 6.0.0 - parseurl@1.3.3: {} - path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -10426,10 +10560,10 @@ snapshots: lru-cache: 11.1.0 minipass: 7.1.2 - path-to-regexp@8.2.0: {} - path-type@4.0.0: {} + path-type@6.0.0: {} + pathe@2.0.3: {} pathval@2.0.0: {} @@ -10446,8 +10580,6 @@ snapshots: pirates@4.0.7: {} - pkce-challenge@5.0.0: {} - playwright-core@1.52.0: {} playwright@1.52.0: @@ -10456,6 +10588,8 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + pluralize@2.0.0: {} + pluralize@8.0.0: {} pony-cause@1.1.1: {} @@ -10477,7 +10611,7 @@ snapshots: postcss-load-config@4.0.2(postcss@8.5.3): dependencies: lilconfig: 3.1.3 - yaml: 2.7.1 + yaml: 2.8.0 optionalDependencies: postcss: 8.5.3 @@ -10511,7 +10645,7 @@ snapshots: pump: 3.0.2 rc: 1.2.8 simple-get: 4.0.1 - tar-fs: 2.1.2 + tar-fs: 2.1.3 tunnel-agent: 0.6.0 optional: true @@ -10535,11 +10669,6 @@ snapshots: property-information@7.1.0: {} - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -10560,14 +10689,14 @@ snapshots: dependencies: safe-buffer: 5.2.1 - range-parser@1.2.1: {} - - raw-body@3.0.0: + rc-config-loader@4.1.3: dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 - unpipe: 1.0.0 + debug: 4.4.1(supports-color@8.1.1) + js-yaml: 4.1.0 + json5: 2.2.3 + require-from-string: 2.0.2 + transitivePeerDependencies: + - supports-color rc@1.2.8: dependencies: @@ -10581,7 +10710,7 @@ snapshots: dependencies: dnd-core: 16.0.1 - react-dnd@16.0.1(@types/node@20.17.46)(@types/react@18.3.21)(react@18.3.1): + react-dnd@16.0.1(@types/node@22.15.21)(@types/react@18.3.22)(react@18.3.1): dependencies: '@react-dnd/invariant': 4.0.2 '@react-dnd/shallowequal': 4.0.2 @@ -10590,8 +10719,8 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 optionalDependencies: - '@types/node': 20.17.46 - '@types/react': 18.3.21 + '@types/node': 22.15.21 + '@types/react': 18.3.22 react-dom@18.3.1(react@18.3.1): dependencies: @@ -10603,11 +10732,11 @@ snapshots: react-is@17.0.2: {} - react-markdown@10.1.0(@types/react@18.3.21)(react@18.3.1): + react-markdown@10.1.0(@types/react@18.3.22)(react@18.3.1): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 18.3.21 + '@types/react': 18.3.22 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 @@ -10623,26 +10752,26 @@ snapshots: react-refresh@0.17.0: {} - react-remove-scroll-bar@2.3.8(@types/react@18.3.21)(react@18.3.1): + react-remove-scroll-bar@2.3.8(@types/react@18.3.22)(react@18.3.1): dependencies: react: 18.3.1 - react-style-singleton: 2.2.3(@types/react@18.3.21)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.22)(react@18.3.1) tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - react-remove-scroll@2.6.3(@types/react@18.3.21)(react@18.3.1): + react-remove-scroll@2.7.0(@types/react@18.3.22)(react@18.3.1): dependencies: react: 18.3.1 - react-remove-scroll-bar: 2.3.8(@types/react@18.3.21)(react@18.3.1) - react-style-singleton: 2.2.3(@types/react@18.3.21)(react@18.3.1) + react-remove-scroll-bar: 2.3.8(@types/react@18.3.22)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.22)(react@18.3.1) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@18.3.21)(react@18.3.1) - use-sidecar: 1.1.3(@types/react@18.3.21)(react@18.3.1) + use-callback-ref: 1.3.3(@types/react@18.3.22)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.22)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - react-router@7.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router@7.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: cookie: 1.0.2 react: 18.3.1 @@ -10656,26 +10785,26 @@ snapshots: react: 18.3.1 split.js: 1.6.5 - react-style-singleton@2.2.3(@types/react@18.3.21)(react@18.3.1): + react-style-singleton@2.2.3(@types/react@18.3.22)(react@18.3.1): dependencies: get-nonce: 1.0.1 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 react@18.3.1: dependencies: loose-envify: 1.4.0 - reactflow@11.11.4(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + reactflow@11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@reactflow/background': 11.3.14(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/controls': 11.2.14(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/core': 11.11.4(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/minimap': 11.7.14(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/node-resizer': 2.2.14(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/node-toolbar': 1.3.14(@types/react@18.3.21)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/background': 11.3.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/controls': 11.2.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/minimap': 11.7.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/node-resizer': 2.2.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/node-toolbar': 1.3.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -10686,6 +10815,13 @@ snapshots: dependencies: pify: 2.3.0 + read-pkg@8.1.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 6.0.2 + parse-json: 7.1.1 + type-fest: 4.41.0 + read@1.0.7: dependencies: mute-stream: 0.0.8 @@ -10726,7 +10862,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.23.10 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 @@ -10782,42 +10918,32 @@ snapshots: reusify@1.1.0: {} - rollup@4.40.2: + rollup@4.41.1: dependencies: '@types/estree': 1.0.7 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.40.2 - '@rollup/rollup-android-arm64': 4.40.2 - '@rollup/rollup-darwin-arm64': 4.40.2 - '@rollup/rollup-darwin-x64': 4.40.2 - '@rollup/rollup-freebsd-arm64': 4.40.2 - '@rollup/rollup-freebsd-x64': 4.40.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.40.2 - '@rollup/rollup-linux-arm-musleabihf': 4.40.2 - '@rollup/rollup-linux-arm64-gnu': 4.40.2 - '@rollup/rollup-linux-arm64-musl': 4.40.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.40.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.40.2 - '@rollup/rollup-linux-riscv64-gnu': 4.40.2 - '@rollup/rollup-linux-riscv64-musl': 4.40.2 - '@rollup/rollup-linux-s390x-gnu': 4.40.2 - '@rollup/rollup-linux-x64-gnu': 4.40.2 - '@rollup/rollup-linux-x64-musl': 4.40.2 - '@rollup/rollup-win32-arm64-msvc': 4.40.2 - '@rollup/rollup-win32-ia32-msvc': 4.40.2 - '@rollup/rollup-win32-x64-msvc': 4.40.2 + '@rollup/rollup-android-arm-eabi': 4.41.1 + '@rollup/rollup-android-arm64': 4.41.1 + '@rollup/rollup-darwin-arm64': 4.41.1 + '@rollup/rollup-darwin-x64': 4.41.1 + '@rollup/rollup-freebsd-arm64': 4.41.1 + '@rollup/rollup-freebsd-x64': 4.41.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.41.1 + '@rollup/rollup-linux-arm-musleabihf': 4.41.1 + '@rollup/rollup-linux-arm64-gnu': 4.41.1 + '@rollup/rollup-linux-arm64-musl': 4.41.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.41.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.41.1 + '@rollup/rollup-linux-riscv64-gnu': 4.41.1 + '@rollup/rollup-linux-riscv64-musl': 4.41.1 + '@rollup/rollup-linux-s390x-gnu': 4.41.1 + '@rollup/rollup-linux-x64-gnu': 4.41.1 + '@rollup/rollup-linux-x64-musl': 4.41.1 + '@rollup/rollup-win32-arm64-msvc': 4.41.1 + '@rollup/rollup-win32-ia32-msvc': 4.41.1 + '@rollup/rollup-win32-x64-msvc': 4.41.1 fsevents: 2.3.3 - router@2.2.0: - dependencies: - debug: 4.4.0(supports-color@8.1.1) - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.2.0 - transitivePeerDependencies: - - supports-color - rrweb-cssom@0.8.0: {} run-applescript@7.0.0: {} @@ -10870,46 +10996,33 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) + secretlint@9.3.3: + dependencies: + '@secretlint/config-creator': 9.3.3 + '@secretlint/formatter': 9.3.3 + '@secretlint/node': 9.3.3 + '@secretlint/profiler': 9.3.3 + debug: 4.4.1(supports-color@8.1.1) + globby: 14.1.0 + read-pkg: 8.1.0 + transitivePeerDependencies: + - supports-color + semver@5.7.2: {} semver@6.3.1: {} semver@7.7.2: {} - send@1.2.0: - dependencies: - debug: 4.4.0(supports-color@8.1.1) - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.0 - mime-types: 3.0.1 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 - seroval-plugins@1.3.1(seroval@1.3.1): + seroval-plugins@1.3.2(seroval@1.3.2): dependencies: - seroval: 1.3.1 + seroval: 1.3.2 - seroval@1.3.1: {} - - serve-static@2.2.0: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.0 - transitivePeerDependencies: - - supports-color + seroval@1.3.2: {} set-cookie-parser@2.7.1: {} @@ -10937,8 +11050,6 @@ snapshots: setimmediate@1.0.5: {} - setprototypeof@1.2.0: {} - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -11021,11 +11132,19 @@ snapshots: slash@3.0.0: {} - solid-js@1.9.6: + slash@5.1.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + solid-js@1.9.7: dependencies: csstype: 3.1.3 - seroval: 1.3.1 - seroval-plugins: 1.3.1(seroval@1.3.1) + seroval: 1.3.2 + seroval-plugins: 1.3.2(seroval@1.3.2) source-map-js@1.2.1: {} @@ -11040,11 +11159,25 @@ snapshots: space-separated-tokens@2.0.2: {} + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.21 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.21 + + spdx-license-ids@3.0.21: {} + split.js@1.6.5: {} - stackback@0.0.2: {} + sprintf-js@1.0.3: {} - statuses@2.0.1: {} + stackback@0.0.2: {} std-env@3.9.0: {} @@ -11076,7 +11209,7 @@ snapshots: call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.23.10 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 @@ -11126,6 +11259,10 @@ snapshots: strip-json-comments@3.1.1: {} + structured-source@4.0.0: + dependencies: + boundary: 2.0.0 + style-mod@4.1.2: {} style-to-js@1.1.16: @@ -11160,6 +11297,11 @@ snapshots: supports-color@9.4.0: {} + supports-hyperlinks@2.3.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + supports-preserve-symlinks-flag@1.0.0: {} swagger2openapi@7.0.8: @@ -11187,6 +11329,14 @@ snapshots: array-back: 6.2.2 wordwrapjs: 5.1.0 + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + tailwindcss@3.4.17: dependencies: '@alloc/quick-lru': 5.2.0 @@ -11214,11 +11364,11 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@4.1.6: {} + tailwindcss@4.1.7: {} - tapable@2.2.1: {} + tapable@2.2.2: {} - tar-fs@2.1.2: + tar-fs@2.1.3: dependencies: chownr: 1.1.4 mkdirp-classic: 0.5.3 @@ -11244,6 +11394,11 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + terminal-link@2.1.1: + dependencies: + ansi-escapes: 4.3.2 + supports-hyperlinks: 2.3.0 + terser-webpack-plugin@5.3.14(esbuild@0.25.4)(webpack@5.99.8(esbuild@0.25.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -11278,6 +11433,10 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + text-table@0.2.0: {} + + textextensions@5.16.0: {} + thememirror@2.0.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.8): dependencies: '@codemirror/language': 6.11.0 @@ -11300,7 +11459,7 @@ snapshots: tinyexec@0.3.2: {} - tinyglobby@0.2.13: + tinyglobby@0.2.14: dependencies: fdir: 6.4.4(picomatch@4.0.2) picomatch: 4.0.2 @@ -11323,8 +11482,6 @@ snapshots: dependencies: is-number: 7.0.0 - toidentifier@1.0.1: {} - tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -11366,7 +11523,7 @@ snapshots: tsx@4.19.4: dependencies: esbuild: 0.25.4 - get-tsconfig: 4.10.0 + get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 @@ -11381,11 +11538,11 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-is@2.0.1: - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.1 + type-fest@0.21.3: {} + + type-fest@3.13.1: {} + + type-fest@4.41.0: {} typed-array-buffer@1.0.3: dependencies: @@ -11426,25 +11583,25 @@ snapshots: tunnel: 0.0.6 underscore: 1.13.7 - typedoc-plugin-markdown@4.6.3(typedoc@0.28.4(typescript@5.8.3)): + typedoc-plugin-markdown@4.6.3(typedoc@0.28.5(typescript@5.8.3)): dependencies: - typedoc: 0.28.4(typescript@5.8.3) + typedoc: 0.28.5(typescript@5.8.3) - typedoc@0.28.4(typescript@5.8.3): + typedoc@0.28.5(typescript@5.8.3): dependencies: - '@gerrit0/mini-shiki': 3.4.0 + '@gerrit0/mini-shiki': 3.4.2 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 typescript: 5.8.3 - yaml: 2.7.1 + yaml: 2.8.0 - typescript-eslint@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.26.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.27.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -11468,8 +11625,12 @@ snapshots: undici-types@6.19.8: {} + undici-types@6.21.0: {} + undici@6.21.3: {} + unicorn-magic@0.3.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -11505,8 +11666,6 @@ snapshots: universalify@2.0.1: {} - unpipe@1.0.0: {} - unplugin@2.3.4: dependencies: acorn: 8.14.1 @@ -11527,20 +11686,20 @@ snapshots: url-join@4.0.1: {} - use-callback-ref@1.3.3(@types/react@18.3.21)(react@18.3.1): + use-callback-ref@1.3.3(@types/react@18.3.22)(react@18.3.1): dependencies: react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 - use-sidecar@1.1.3(@types/react@18.3.21)(react@18.3.1): + use-sidecar@1.1.3(@types/react@18.3.22)(react@18.3.1): dependencies: detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 use-sync-external-store@1.5.0(react@18.3.1): dependencies: @@ -11558,9 +11717,12 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - validator@13.15.0: {} + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 - vary@1.1.2: {} + validator@13.15.0: {} vfile-message@4.0.2: dependencies: @@ -11572,13 +11734,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.1.3(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1): + vite-node@3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 - debug: 4.4.0(supports-color@8.1.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.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -11593,53 +11755,53 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1)): + vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)): dependencies: - vite: 6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1): + vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.4 fdir: 6.4.4(picomatch@4.0.2) picomatch: 4.0.2 postcss: 8.5.3 - rollup: 4.40.2 - tinyglobby: 0.2.13 + rollup: 4.41.1 + tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 20.17.46 + '@types/node': 22.15.21 fsevents: 2.3.3 jiti: 2.4.2 - lightningcss: 1.29.2 + lightningcss: 1.30.1 terser: 5.39.2 tsx: 4.19.4 - yaml: 2.7.1 + yaml: 2.8.0 - vitest@3.1.3(@types/debug@4.1.12)(@types/node@20.17.46)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1): + vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: - '@vitest/expect': 3.1.3 - '@vitest/mocker': 3.1.3(vite@6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1)) - '@vitest/pretty-format': 3.1.3 - '@vitest/runner': 3.1.3 - '@vitest/snapshot': 3.1.3 - '@vitest/spy': 3.1.3 - '@vitest/utils': 3.1.3 + '@vitest/expect': 3.1.4 + '@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/pretty-format': 3.1.4 + '@vitest/runner': 3.1.4 + '@vitest/snapshot': 3.1.4 + '@vitest/spy': 3.1.4 + '@vitest/utils': 3.1.4 chai: 5.2.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.1(supports-color@8.1.1) expect-type: 1.2.1 magic-string: 0.30.17 pathe: 2.0.3 std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinyglobby: 0.2.13 + tinyglobby: 0.2.14 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) - vite-node: 3.1.3(@types/node@20.17.46)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.2)(tsx@4.19.4)(yaml@2.7.1) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite-node: 3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 20.17.46 + '@types/node': 22.15.21 jsdom: 26.1.0 transitivePeerDependencies: - jiti @@ -11680,7 +11842,7 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - watchpack@2.4.3: + watchpack@2.4.4: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 @@ -11691,7 +11853,7 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-sources@3.2.3: {} + webpack-sources@3.3.0: {} webpack-virtual-modules@0.6.2: {} @@ -11717,10 +11879,10 @@ snapshots: mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 4.3.2 - tapable: 2.2.1 + tapable: 2.2.2 terser-webpack-plugin: 5.3.14(webpack@5.99.8) - watchpack: 2.4.3 - webpack-sources: 3.2.3 + watchpack: 2.4.4 + webpack-sources: 3.3.0 transitivePeerDependencies: - '@swc/core' - esbuild @@ -11749,10 +11911,10 @@ snapshots: mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 4.3.2 - tapable: 2.2.1 + tapable: 2.2.2 terser-webpack-plugin: 5.3.14(esbuild@0.25.4)(webpack@5.99.8(esbuild@0.25.4)) - watchpack: 2.4.3 - webpack-sources: 3.2.3 + watchpack: 2.4.4 + webpack-sources: 3.3.0 transitivePeerDependencies: - '@swc/core' - esbuild @@ -11867,7 +12029,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.7.1: {} + yaml@2.8.0: {} yargs-parser@20.2.9: {} @@ -11911,23 +12073,19 @@ snapshots: yocto-queue@0.1.0: {} - zod-to-json-schema@3.24.5(zod@3.25.4): - dependencies: - zod: 3.25.4 - - zod@3.25.4: {} + zod@3.25.28: {} - zustand@4.5.6(@types/react@18.3.21)(immer@9.0.21)(react@18.3.1): + zustand@4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1): dependencies: use-sync-external-store: 1.5.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 immer: 9.0.21 react: 18.3.1 - zustand@5.0.4(@types/react@18.3.21)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)): + zustand@5.0.5(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)): optionalDependencies: - '@types/react': 18.3.21 + '@types/react': 18.3.22 immer: 9.0.21 react: 18.3.1 use-sync-external-store: 1.5.0(react@18.3.1) diff --git a/vscode/bus/package.json b/vscode/bus/package.json index c7ffb6cd42..024942f6c4 100644 --- a/vscode/bus/package.json +++ b/vscode/bus/package.json @@ -14,6 +14,6 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "devDependencies": { - "typescript": "^5.5.4" + "typescript": "^5.8.3" } } diff --git a/vscode/extension/package.json b/vscode/extension/package.json index f3b44fec26..9552f0d392 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -129,22 +129,22 @@ "fs-extra": "^11.3.0", "vscode-jsonrpc": "^8.2.1", "vscode-languageclient": "^9.0.1", - "zod": "^3.25.4" + "zod": "^3.25.28" }, "devDependencies": { - "@eslint/js": "^9.25.1", - "@playwright/test": "^1.48.2", + "@eslint/js": "^9.27.0", + "@playwright/test": "^1.52.0", "@types/mocha": "^10.0.10", "@types/node": "20.11.25", "@types/vscode": "1.96.0", "@vscode/test-cli": "^0.0.10", - "@vscode/test-electron": "^2.4.1", - "@vscode/vsce": "^3.3.2", - "esbuild": "^0.25.2", - "eslint": "^9.23.0", + "@vscode/test-electron": "^2.5.2", + "@vscode/vsce": "^3.4.2", + "esbuild": "^0.25.4", + "eslint": "^9.27.0", "ts-loader": "^9.5.2", - "tsx": "^4.19.2", - "typescript": "^5.8.2", - "typescript-eslint": "^8.31.1" + "tsx": "^4.19.4", + "typescript": "^5.8.3", + "typescript-eslint": "^8.32.1" } } diff --git a/vscode/react/package.json b/vscode/react/package.json index 4522e612df..a150c0ee53 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -13,37 +13,37 @@ "lint": "tsc --noEmit" }, "dependencies": { - "@headlessui/react": "^2.0.0", - "@heroicons/react": "^2.0.18", - "@radix-ui/react-select": "^2.2.4", - "@tailwindcss/postcss": "^4.1.3", - "@tailwindcss/vite": "^4.1.3", - "@tanstack/react-query": "^5.76.1", - "@tanstack/react-router": "^1.114.3", - "@tanstack/react-router-devtools": "^1.114.3", - "@tanstack/react-virtual": "^3.13.6", - "@tanstack/router-plugin": "^1.114.3", + "@headlessui/react": "^2.2.4", + "@heroicons/react": "^2.2.0", + "@radix-ui/react-select": "^2.2.5", + "@tailwindcss/postcss": "^4.1.7", + "@tailwindcss/vite": "^4.1.7", + "@tanstack/react-query": "^5.77.2", + "@tanstack/react-router": "^1.120.10", + "@tanstack/react-router-devtools": "^1.120.10", + "@tanstack/react-virtual": "^3.13.9", + "@tanstack/router-plugin": "^1.120.10", "apache-arrow": "^19.0.1", - "clsx": "^2.0.0", + "clsx": "^2.1.1", "elkjs": "^0.8.2", - "orval": "^7.8.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router": "^7.0.0", - "reactflow": "^11.8.3", - "tailwindcss": "^4.1.3", + "orval": "^7.9.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router": "^7.6.1", + "reactflow": "^11.11.4", + "tailwindcss": "^4.1.7", "vscode-uri": "^3.1.0" }, "devDependencies": { "@testing-library/dom": "^10.4.0", - "@testing-library/react": "^16.2.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@vitejs/plugin-react": "^4.3.4", - "jsdom": "^26.0.0", - "typescript": "^5.7.2", - "vite": "^6.1.0", - "vitest": "^3.0.5", + "@testing-library/react": "^16.3.0", + "@types/react": "^18.3.22", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.0", + "jsdom": "^26.1.0", + "typescript": "^5.8.3", + "vite": "^6.3.5", + "vitest": "^3.1.4", "web-vitals": "^4.2.4" } } diff --git a/web/client/package.json b/web/client/package.json index 3b3d1033ac..845a120a9b 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -15,66 +15,66 @@ "generate:api": "orval --config ./orval.config.ts" }, "dependencies": { - "@codemirror/autocomplete": "^6.16.2", - "@codemirror/commands": "^6.6.0", - "@codemirror/lang-python": "^6.1.6", - "@codemirror/lang-sql": "^6.6.4", - "@codemirror/language": "^6.10.2", - "@codemirror/legacy-modes": "^6.4.0", - "@codemirror/state": "^6.4.1", - "@codemirror/view": "^6.28.1", - "@headlessui/react": "^2.0.0", - "@heroicons/react": "^2.0.18", - "@lit/react": "^1.0.6", - "@radix-ui/react-context-menu": "^2.1.4", - "@radix-ui/react-select": "^2.2.4", + "@codemirror/autocomplete": "^6.18.6", + "@codemirror/commands": "^6.8.1", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-sql": "^6.8.0", + "@codemirror/language": "^6.11.0", + "@codemirror/legacy-modes": "^6.5.1", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.36.8", + "@headlessui/react": "^2.2.4", + "@heroicons/react": "^2.2.0", + "@lit/react": "^1.0.7", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-select": "^2.2.5", "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^5.76.1", - "@tanstack/react-table": "^8.9.2", - "@tanstack/react-virtual": "^3.0.0-beta.56", - "@uidotdev/usehooks": "^2.2.0", - "@uiw/react-codemirror": "^4.21.12", - "apache-arrow": "^19.0.0", - "clsx": "^2.0.0", - "diff": "^8.0.0", + "@tanstack/react-query": "^5.77.2", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.9", + "@uidotdev/usehooks": "^2.4.1", + "@uiw/react-codemirror": "^4.23.12", + "apache-arrow": "^19.0.1", + "clsx": "^2.1.1", + "diff": "^8.0.2", "elkjs": "^0.8.2", "pluralize": "^8.0.0", - "react": "^18.2.0", + "react": "^18.3.1", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", - "react-dom": "^18.2.0", + "react-dom": "^18.3.1", "react-markdown": "^10.1.0", - "react-router": "^7.0.0", + "react-router": "^7.6.1", "react-split": "^2.0.14", - "reactflow": "^11.8.3", + "reactflow": "^11.11.4", "thememirror": "^2.0.1", - "zustand": "^5.0.0" + "zustand": "^5.0.5" }, "devDependencies": { - "@eslint/js": "^9.25.1", - "@playwright/test": "^1.37.1", - "@swc/core": "^1.11.24", - "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "^16.0.0", - "@testing-library/user-event": "^14.4.3", + "@eslint/js": "^9.27.0", + "@playwright/test": "^1.52.0", + "@swc/core": "^1.11.29", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/pluralize": "^0.0.33", - "@types/react": "^18.2.21", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react-swc": "^3.9.0", + "@types/react": "^18.3.22", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react-swc": "^3.10.0", "ajv": "^8.17.1", - "autoprefixer": "^10.4.15", - "eslint": "^9.23.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.27.0", "jsdom": "^26.1.0", "orval": "^7.9.0", - "postcss": "^8.4.29", - "tailwindcss": "^3.3.3", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", "typescript": "^5.8.3", - "typescript-eslint": "^8.31.1", - "vite": "^6.3.4", + "typescript-eslint": "^8.32.1", + "vite": "^6.3.5", "vite-plugin-css-injected-by-js": "^3.5.2", - "vitest": "^3.1.2" + "vitest": "^3.1.4" }, "optionalDependencies": { - "@swc/core-linux-x64-gnu": "^1.11.24" + "@swc/core-linux-x64-gnu": "^1.11.29" } } From a69f1a4d8ceb780f27c8ba898fc1d50617e889fe Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 26 May 2025 22:47:39 +0200 Subject: [PATCH 0266/1056] feat(lsp): add go to definition for ctes (#4543) --- sqlmesh/lsp/main.py | 30 ++++--- sqlmesh/lsp/reference.py | 150 ++++++++++++++++++++------------ tests/lsp/test_reference_cte.py | 64 ++++++++++++++ 3 files changed, 176 insertions(+), 68 deletions(-) create mode 100644 tests/lsp/test_reference_cte.py diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 260a640b16..c348349d1b 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -279,21 +279,31 @@ def goto_definition( raise RuntimeError(f"No context found for document: {document.path}") references = get_references(self.lsp_context, uri, params.position) - return [ - types.LocationLink( - target_uri=reference.uri, - target_selection_range=types.Range( + location_links = [] + for reference in references: + # Use target_range if available (for CTEs), otherwise default to start of file + if reference.target_range: + target_range = reference.target_range + target_selection_range = reference.target_range + else: + target_range = types.Range( start=types.Position(line=0, character=0), end=types.Position(line=0, character=0), - ), - target_range=types.Range( + ) + target_selection_range = types.Range( start=types.Position(line=0, character=0), end=types.Position(line=0, character=0), - ), - origin_selection_range=reference.range, + ) + + location_links.append( + types.LocationLink( + target_uri=reference.uri, + target_selection_range=target_selection_range, + target_range=target_range, + origin_selection_range=reference.range, + ) ) - for reference in references - ] + return location_links except Exception as e: ls.show_message(f"Error getting references: {e}", types.MessageType.Error) return [] diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 1f646dadf6..58409ccaa3 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -6,23 +6,27 @@ from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget from sqlglot import exp from sqlmesh.lsp.description import generate_markdown_description +from sqlglot.optimizer.scope import build_scope from sqlmesh.lsp.uri import URI from sqlmesh.utils.pydantic import PydanticModel +from sqlglot.optimizer.normalize_identifiers import normalize_identifiers class Reference(PydanticModel): """ - A reference to a model. + A reference to a model or CTE. Attributes: range: The range of the reference in the source file uri: The uri of the referenced model markdown_description: The markdown description of the referenced model + target_range: The range of the definition for go-to-definition (optional, used for CTEs) """ range: Range uri: str markdown_description: t.Optional[str] = None + target_range: t.Optional[Range] = None def by_position(position: Position) -> t.Callable[[Reference], bool]: @@ -88,6 +92,7 @@ def get_model_definitions_for_a_path( - Need to normalize it before matching - Try get_model before normalization - Match to models that the model refers to + - Also find CTE references within the query """ path = document_uri.to_path() if path.suffix != ".sql": @@ -126,66 +131,95 @@ def get_model_definitions_for_a_path( # Find all possible references references = [] - # Get SQL query and find all table references - tables = list(query.find_all(exp.Table)) - if len(tables) == 0: - return [] - with open(file_path, "r", encoding="utf-8") as file: read_file = file.readlines() - for table in tables: - # Normalize the table reference - unaliased = table.copy() - if unaliased.args.get("alias") is not None: - unaliased.set("alias", None) - reference_name = unaliased.sql(dialect=dialect) - try: - normalized_reference_name = normalize_model_name( - reference_name, - default_catalog=lint_context.context.default_catalog, - dialect=dialect, - ) - if normalized_reference_name not in depends_on: - continue - except Exception: - # Skip references that cannot be normalized - continue - - # Get the referenced model uri - referenced_model = lint_context.context.get_model( - model_or_snapshot=normalized_reference_name, raise_if_missing=False - ) - if referenced_model is None: - continue - referenced_model_path = referenced_model._path - # Check whether the path exists - if not referenced_model_path.is_file(): - continue - referenced_model_uri = URI.from_path(referenced_model_path) - - # Extract metadata for positioning - table_meta = TokenPositionDetails.from_meta(table.this.meta) - table_range = _range_from_token_position_details(table_meta, read_file) - start_pos = table_range.start - end_pos = table_range.end - - # If there's a catalog or database qualifier, adjust the start position - catalog_or_db = table.args.get("catalog") or table.args.get("db") - if catalog_or_db is not None: - catalog_or_db_meta = TokenPositionDetails.from_meta(catalog_or_db.meta) - catalog_or_db_range = _range_from_token_position_details(catalog_or_db_meta, read_file) - start_pos = catalog_or_db_range.start - - description = generate_markdown_description(referenced_model) - - references.append( - Reference( - uri=referenced_model_uri.value, - range=Range(start=start_pos, end=end_pos), - markdown_description=description, - ) - ) + # Build scope tree to properly handle nested CTEs + query = normalize_identifiers(query.copy(), dialect=dialect) + root_scope = build_scope(query) + + if root_scope: + # Traverse all scopes to find CTE definitions and table references + for scope in root_scope.traverse(): + for table in scope.tables: + table_name = table.name + + # Check if this table reference is a CTE in the current scope + if cte_scope := scope.cte_sources.get(table_name): + cte = cte_scope.expression.parent + alias = cte.args["alias"] + if isinstance(alias, exp.TableAlias): + identifier = alias.this + if isinstance(identifier, exp.Identifier): + target_range = _range_from_token_position_details( + TokenPositionDetails.from_meta(identifier.meta), read_file + ) + table_range = _range_from_token_position_details( + TokenPositionDetails.from_meta(table.this.meta), read_file + ) + references.append( + Reference( + uri=document_uri.value, # Same file + range=table_range, + target_range=target_range, + ) + ) + continue + + # For non-CTE tables, process as before (external model references) + # Normalize the table reference + unaliased = table.copy() + if unaliased.args.get("alias") is not None: + unaliased.set("alias", None) + reference_name = unaliased.sql(dialect=dialect) + try: + normalized_reference_name = normalize_model_name( + reference_name, + default_catalog=lint_context.context.default_catalog, + dialect=dialect, + ) + if normalized_reference_name not in depends_on: + continue + except Exception: + # Skip references that cannot be normalized + continue + + # Get the referenced model uri + referenced_model = lint_context.context.get_model( + model_or_snapshot=normalized_reference_name, raise_if_missing=False + ) + if referenced_model is None: + continue + referenced_model_path = referenced_model._path + # Check whether the path exists + if not referenced_model_path.is_file(): + continue + referenced_model_uri = URI.from_path(referenced_model_path) + + # Extract metadata for positioning + table_meta = TokenPositionDetails.from_meta(table.this.meta) + table_range = _range_from_token_position_details(table_meta, read_file) + start_pos = table_range.start + end_pos = table_range.end + + # If there's a catalog or database qualifier, adjust the start position + catalog_or_db = table.args.get("catalog") or table.args.get("db") + if catalog_or_db is not None: + catalog_or_db_meta = TokenPositionDetails.from_meta(catalog_or_db.meta) + catalog_or_db_range = _range_from_token_position_details( + catalog_or_db_meta, read_file + ) + start_pos = catalog_or_db_range.start + + description = generate_markdown_description(referenced_model) + + references.append( + Reference( + uri=referenced_model_uri.value, + range=Range(start=start_pos, end=end_pos), + markdown_description=description, + ) + ) return references diff --git a/tests/lsp/test_reference_cte.py b/tests/lsp/test_reference_cte.py new file mode 100644 index 0000000000..74e134c6f5 --- /dev/null +++ b/tests/lsp/test_reference_cte.py @@ -0,0 +1,64 @@ +import re +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.reference import get_references +from sqlmesh.lsp.uri import URI +from lsprotocol.types import Range, Position +import typing as t + + +def test_cte_parsing(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Find model URIs + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Find position of the cte reference + ranges = find_ranges_from_regex(read_file, r"current_marketing(?!_outer)") + assert len(ranges) == 2 + position = Position(line=ranges[1].start.line, character=ranges[1].start.character + 4) + references = get_references(lsp_context, URI.from_path(sushi_customers_path), position) + assert len(references) == 1 + assert references[0].uri == URI.from_path(sushi_customers_path).value + assert references[0].markdown_description is None + assert ( + references[0].range.start.line == ranges[1].start.line + ) # The reference location (where we clicked) + assert ( + references[0].target_range.start.line == ranges[0].start.line + ) # The CTE definition location + + # Find the position of the current_marketing_outer reference + ranges = find_ranges_from_regex(read_file, r"current_marketing_outer") + assert len(ranges) == 2 + position = Position(line=ranges[1].start.line, character=ranges[1].start.character + 4) + references = get_references(lsp_context, URI.from_path(sushi_customers_path), position) + assert len(references) == 1 + assert references[0].uri == URI.from_path(sushi_customers_path).value + assert references[0].markdown_description is None + assert ( + references[0].range.start.line == ranges[1].start.line + ) # The reference location (where we clicked) + assert ( + references[0].target_range.start.line == ranges[0].start.line + ) # The CTE definition location + + +def find_ranges_from_regex(read_file: t.List[str], regex: str) -> t.List[Range]: + """Find all ranges in the read file that match the regex.""" + return [ + Range( + start=Position(line=line_number, character=match.start()), + end=Position(line=line_number, character=match.end()), + ) + for line_number, line in enumerate(read_file) + for match in [m for m in [re.search(regex, line)] if m] + ] From 133f1d3ed948ea299d3e4c47e504b224dc031a17 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 26 May 2025 23:03:44 +0200 Subject: [PATCH 0267/1056] fix(lsp): overwriting the formatted file (#4547) --- sqlmesh/core/context.py | 72 ++++++++++++++++++++++++++--------------- sqlmesh/lsp/main.py | 35 +++++++++++++++----- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 80a35f597a..1f4722264c 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1129,33 +1129,15 @@ def format( with open(target._path, "r+", encoding="utf-8") as file: before = file.read() - expressions = parse(before, default_dialect=self.config_for_node(target).dialect) - if transpile and is_meta_expression(expressions[0]): - for prop in expressions[0].expressions: - if prop.name.lower() == "dialect": - prop.replace( - exp.Property( - this="dialect", - value=exp.Literal.string(transpile or target.dialect), - ) - ) - - format_config = self.config_for_node(target).format - after = format_model_expressions( - expressions, - transpile or target.dialect, - rewrite_casts=( - rewrite_casts - if rewrite_casts is not None - else not format_config.no_rewrite_casts - ), - **{**format_config.generator_options, **kwargs}, - ) - if append_newline is None: - append_newline = format_config.append_newline - if append_newline: - after += "\n" + after = self._format( + target, + before, + transpile=transpile, + rewrite_casts=rewrite_casts, + append_newline=append_newline, + **kwargs, + ) if not check: file.seek(0) @@ -1174,6 +1156,44 @@ def format( return True + def _format( + self, + target: Model | Audit, + before: str, + *, + transpile: t.Optional[str] = None, + rewrite_casts: t.Optional[bool] = None, + append_newline: t.Optional[bool] = None, + **kwargs: t.Any, + ) -> str: + expressions = parse(before, default_dialect=self.config_for_node(target).dialect) + if transpile and is_meta_expression(expressions[0]): + for prop in expressions[0].expressions: + if prop.name.lower() == "dialect": + prop.replace( + exp.Property( + this="dialect", + value=exp.Literal.string(transpile or target.dialect), + ) + ) + + format_config = self.config_for_node(target).format + after = format_model_expressions( + expressions, + transpile or target.dialect, + rewrite_casts=( + rewrite_casts if rewrite_casts is not None else not format_config.no_rewrite_casts + ), + **{**format_config.generator_options, **kwargs}, + ) + + if append_newline is None: + append_newline = format_config.append_newline + if append_newline: + after += "\n" + + return after + @python_api_analytics def plan( self, diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index c348349d1b..b866f66b5c 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -1,6 +1,7 @@ #!/usr/bin/env python """A Language Server Protocol (LSP) server for SQL with SQLMesh integration, refactored without globals.""" +from itertools import chain import logging import typing as t from pathlib import Path @@ -20,7 +21,11 @@ ApiResponseGetModels, ) from sqlmesh.lsp.completions import get_sql_completions -from sqlmesh.lsp.context import LSPContext, ModelTarget, render_model as render_model_context +from sqlmesh.lsp.context import ( + LSPContext, + ModelTarget, + render_model as render_model_context, +) from sqlmesh.lsp.custom import ( ALL_MODELS_FEATURE, RENDER_MODEL_FEATURE, @@ -213,15 +218,29 @@ def formatting( uri = URI(params.text_document.uri) self._ensure_context_for_document(uri) document = ls.workspace.get_text_document(params.text_document.uri) + before = document.source if self.lsp_context is None: raise RuntimeError(f"No context found for document: {document.path}") - # Perform formatting using the loaded context - self.lsp_context.context.format(paths=(str(uri.to_path()),)) - with open(uri.to_path(), "r+", encoding="utf-8") as file: - new_text = file.read() - - # Return a single edit that replaces the entire file. + target = next( + ( + target + for target in chain( + self.lsp_context.context._models.values(), + self.lsp_context.context._audits.values(), + ) + if target._path is not None + and target._path.suffix == ".sql" + and (target._path.samefile(uri.to_path())) + ), + None, + ) + if target is None: + return [] + after = self.lsp_context.context._format( + target=target, + before=before, + ) return [ types.TextEdit( range=types.Range( @@ -231,7 +250,7 @@ def formatting( character=len(document.lines[-1]) if document.lines else 0, ), ), - new_text=new_text, + new_text=after, ) ] except Exception as e: From 8a413685dac5b1f55d9b45309849deaa299ec194 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 27 May 2025 11:30:36 +1200 Subject: [PATCH 0268/1056] Fix: Remove bigframes version pin (#4548) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d77a1f19c5..7a54355bc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ azuresql = ["pymssql"] bigquery = [ "google-cloud-bigquery[pandas]", "google-cloud-bigquery-storage", - "bigframes>=1.32.0" + "bigframes" ] clickhouse = ["clickhouse-connect"] databricks = ["databricks-sql-connector[pyarrow]"] From 68def3041e7ae2dad72e2bb3732736157635e4f1 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 27 May 2025 11:42:09 +1200 Subject: [PATCH 0269/1056] Fix: Put bigframes back as an optional dependency separate from bigquery (#4549) --- Makefile | 1 + pyproject.toml | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a2534a3a42..b39ea166a1 100644 --- a/Makefile +++ b/Makefile @@ -157,6 +157,7 @@ snowflake-test: guard-SNOWFLAKE_ACCOUNT guard-SNOWFLAKE_WAREHOUSE guard-SNOWFLAK pytest -n auto -m "snowflake" --retries 3 --junitxml=test-results/junit-snowflake.xml bigquery-test: guard-BIGQUERY_KEYFILE engine-bigquery-install + pip install -e ".[bigframes]" pytest -n auto -m "bigquery" --retries 3 --junitxml=test-results/junit-bigquery.xml databricks-test: guard-DATABRICKS_CATALOG guard-DATABRICKS_SERVER_HOSTNAME guard-DATABRICKS_HTTP_PATH guard-DATABRICKS_ACCESS_TOKEN guard-DATABRICKS_CONNECT_VERSION engine-databricks-install diff --git a/pyproject.toml b/pyproject.toml index 7a54355bc0..c47f65d691 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,9 +41,12 @@ athena = ["PyAthena[Pandas]"] azuresql = ["pymssql"] bigquery = [ "google-cloud-bigquery[pandas]", - "google-cloud-bigquery-storage", - "bigframes" + "google-cloud-bigquery-storage" ] +# bigframes has to be separate to support environments with an older google-cloud-bigquery pin +# this is because that pin pulls in an older bigframes and the bigframes team +# pinned an older SQLGlot which is incompatible with SQLMesh +bigframes = ["bigframes>=1.32.0"] clickhouse = ["clickhouse-connect"] databricks = ["databricks-sql-connector[pyarrow]"] dev = [ From b7f3a2b0c47a7ce0518d5a28bd9e797ceb95dd6d Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 27 May 2025 12:19:51 +0300 Subject: [PATCH 0270/1056] Fix: improve error handling when SQL model query is invalid (#4551) --- sqlmesh/core/model/definition.py | 21 ++++++++------------- tests/core/test_model.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 8a282636bb..a0d52bc523 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2173,24 +2173,18 @@ def load_sql_based_model( **meta_fields, ) - if query_or_seed_insert is not None and ( - isinstance(query_or_seed_insert, (exp.Query, d.JinjaQuery)) - or ( - # Macro functions are allowed in place of model queries only when there are no - # other statements in the model definition, otherwise they would be ambiguous - isinstance(query_or_seed_insert, d.MacroFunc) - and (query_or_seed_insert.this.name.lower() == "union" or len(expressions) == 2) - ) - ): + kind = common_kwargs.pop("kind", ModelMeta.all_field_infos()["kind"].default) + + if kind.name != ModelKindName.SEED: return create_sql_model( name, query_or_seed_insert, + kind=kind, time_column_format=time_column_format, **common_kwargs, ) - seed_properties = { - p.name.lower(): p.args.get("value") for p in common_kwargs.pop("kind").expressions - } + + seed_properties = {p.name.lower(): p.args.get("value") for p in kind.expressions} return create_seed_model( name, SeedKind(**seed_properties), @@ -2200,7 +2194,7 @@ def load_sql_based_model( def create_sql_model( name: TableName, - query: exp.Expression, + query: t.Optional[exp.Expression], **kwargs: t.Any, ) -> Model: """Creates a SQL model. @@ -2215,6 +2209,7 @@ def create_sql_model( "A query is required and must be a SELECT statement, a UNION statement, or a JINJA_QUERY block", kwargs.get("path"), ) + assert isinstance(query, (exp.Query, d.JinjaQuery, d.MacroFunc)) return _create_model(SqlModel, name, query=query, **kwargs) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 3ce36b4c3b..83a67253b0 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -10150,3 +10150,22 @@ def getenv_macro(evaluator): monkeypatch.chdir(tmp_path) ctx = Context(paths=tmp_path) + + +def test_invalid_sql_model_query() -> None: + for kind in ("", ", KIND FULL"): + expressions = d.parse( + f""" + MODEL (name db.table{kind}); + + JINJA_STATEMENT_BEGIN; + SELECT 1 AS c; + JINJA_END; + """ + ) + + with pytest.raises( + ConfigError, + match=r"^A query is required and must be a SELECT statement, a UNION statement, or a JINJA_QUERY block.*", + ): + load_sql_based_model(expressions) From 6ab696328f453c666516310a0b81f002a0285f96 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 27 May 2025 13:39:07 +0200 Subject: [PATCH 0271/1056] fix(vscode): duplicate returned diagnostics (#4553) --- examples/sushi/config.py | 1 + sqlmesh/lsp/main.py | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/examples/sushi/config.py b/examples/sushi/config.py index 66adfc5531..c59675cf5b 100644 --- a/examples/sushi/config.py +++ b/examples/sushi/config.py @@ -47,6 +47,7 @@ "invalidselectstarexpansion", "noselectstar", "nomissingaudits", + "nomissingowner", ], ), ) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index b866f66b5c..262c1c3c9e 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -421,12 +421,24 @@ def _diagnostic_to_lsp_diagnostic( def _diagnostics_to_lsp_diagnostics( diagnostics: t.List[AnnotatedRuleViolation], ) -> t.List[types.Diagnostic]: - lsp_diagnostics: t.List[types.Diagnostic] = [] + """ + Converts a list of AnnotatedRuleViolations to a list of LSP diagnostics. It will remove duplicates based on the message and range. + """ + lsp_diagnostics = {} for diagnostic in diagnostics: lsp_diagnostic = SQLMeshLanguageServer._diagnostic_to_lsp_diagnostic(diagnostic) if lsp_diagnostic is not None: - lsp_diagnostics.append(lsp_diagnostic) - return lsp_diagnostics + # Create a unique key combining message and range + diagnostic_key = ( + lsp_diagnostic.message, + lsp_diagnostic.range.start.line, + lsp_diagnostic.range.start.character, + lsp_diagnostic.range.end.line, + lsp_diagnostic.range.end.character, + ) + if diagnostic_key not in lsp_diagnostics: + lsp_diagnostics[diagnostic_key] = lsp_diagnostic + return list(lsp_diagnostics.values()) @staticmethod def _uri_to_path(uri: str) -> Path: From b04d10ab4df9456990a92ea007bb1270a3ac23c6 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 27 May 2025 20:11:38 +0300 Subject: [PATCH 0272/1056] Fix: improve error handling for invalid environment connections (#4552) --- sqlmesh/core/config/connection.py | 7 ++++++- tests/core/test_config.py | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 1859794599..9cc322d901 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -1974,13 +1974,18 @@ def _connection_config_validator( ) -> ConnectionConfig | None: if v is None or isinstance(v, ConnectionConfig): return v + + check_config_and_vars_msg = "\n\nVerify your config.yaml and environment variables." + try: return parse_connection_config(v) except pydantic.ValidationError as e: raise ConfigError( validation_error_message(e, f"Invalid '{v['type']}' connection config:") - + "\n\nVerify your config.yaml and environment variables." + + check_config_and_vars_msg ) + except ConfigError as e: + raise ConfigError(str(e) + check_config_and_vars_msg) connection_config_validator: t.Callable = field_validator( diff --git a/tests/core/test_config.py b/tests/core/test_config.py index d93a5fb1e2..44c321ee66 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -301,6 +301,15 @@ def test_load_config_from_env(): ) +def test_load_config_from_env_fails(): + with mock.patch.dict(os.environ, {"SQLMESH__GATEWAYS__ABCDEF__CONNECTION__PASSWORD": "..."}): + with pytest.raises( + ConfigError, + match="Missing connection type.\n\nVerify your config.yaml and environment variables.", + ): + Config.parse_obj(load_config_from_env()) + + def test_load_config_from_env_no_config_vars(): with mock.patch.dict( os.environ, From 0872bac4f1adc2fefc470f09f167b7ae3a87c253 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 27 May 2025 10:22:29 -0700 Subject: [PATCH 0273/1056] Fix: Support of vars in on-run-start / on-run-end hooks in dbt projects (#4554) --- sqlmesh/core/environment.py | 40 ++++++++++++++++--- sqlmesh/dbt/loader.py | 25 +++++------- tests/core/test_integration.py | 15 +++++++ tests/dbt/test_adapter.py | 2 + tests/fixtures/dbt/sushi_test/dbt_project.yml | 1 + 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/sqlmesh/core/environment.py b/sqlmesh/core/environment.py index 9da2af9462..36bb901883 100644 --- a/sqlmesh/core/environment.py +++ b/sqlmesh/core/environment.py @@ -250,6 +250,39 @@ class EnvironmentStatements(PydanticModel): python_env: t.Dict[str, Executable] jinja_macros: t.Optional[JinjaMacroRegistry] = None + def render_before_all( + self, + dialect: str, + default_catalog: t.Optional[str] = None, + **render_kwargs: t.Any, + ) -> t.List[str]: + return self.render(RuntimeStage.BEFORE_ALL, dialect, default_catalog, **render_kwargs) + + def render_after_all( + self, + dialect: str, + default_catalog: t.Optional[str] = None, + **render_kwargs: t.Any, + ) -> t.List[str]: + return self.render(RuntimeStage.AFTER_ALL, dialect, default_catalog, **render_kwargs) + + def render( + self, + runtime_stage: RuntimeStage, + dialect: str, + default_catalog: t.Optional[str] = None, + **render_kwargs: t.Any, + ) -> t.List[str]: + return render_statements( + statements=getattr(self, runtime_stage.value), + dialect=dialect, + default_catalog=default_catalog, + python_env=self.python_env, + jinja_macros=self.jinja_macros, + runtime_stage=runtime_stage, + **render_kwargs, + ) + def execute_environment_statements( adapter: EngineAdapter, @@ -266,18 +299,15 @@ def execute_environment_statements( rendered_expressions = [ expr for statements in environment_statements - for expr in render_statements( - statements=getattr(statements, runtime_stage.value), + for expr in statements.render( + runtime_stage=runtime_stage, dialect=adapter.dialect, default_catalog=default_catalog, - python_env=statements.python_env, - jinja_macros=statements.jinja_macros, snapshots=snapshots, start=start, end=end, execution_time=execution_time, environment_naming_info=environment_naming_info, - runtime_stage=runtime_stage, engine_adapter=adapter, ) ] diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 230f25e6ca..8f450c6b7d 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -4,7 +4,6 @@ import sys import typing as t import sqlmesh.core.dialect as d -from sqlglot.optimizer.simplify import gen from pathlib import Path from sqlmesh.core import constants as c from sqlmesh.core.config import ( @@ -17,9 +16,9 @@ from sqlmesh.core.loader import CacheBase, LoadedProject, Loader from sqlmesh.core.macros import MacroRegistry, macro from sqlmesh.core.model import Model, ModelCache -from sqlmesh.core.model.common import make_python_env from sqlmesh.core.signal import signal from sqlmesh.dbt.basemodel import BMC, BaseModelConfig +from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.model import ModelConfig from sqlmesh.dbt.profile import Profile @@ -259,22 +258,18 @@ def _load_environment_statements(self, macros: MacroRegistry) -> t.List[Environm if statements := on_run_start + on_run_end: jinja_references, used_variables = extract_macro_references_and_variables( - *(gen(stmt) for stmt in statements) + *statements ) - jinja_registry = make_jinja_registry( - context.jinja_macros, package_name, jinja_references + statements_context = context.context_for_dependencies( + Dependencies( + variables=used_variables, + ) ) - - python_env = make_python_env( - [s for stmt in statements for s in d.parse(stmt, default_dialect=dialect)], - jinja_macro_references=jinja_references, - module_path=self.config_path, - macros=macros, - variables=context.variables, - used_variables=used_variables, - path=self.config_path, + jinja_registry = make_jinja_registry( + statements_context.jinja_macros, package_name, jinja_references ) + jinja_registry.add_globals(statements_context.jinja_globals) hooks_by_package_name[package_name] = EnvironmentStatements( before_all=[ @@ -285,7 +280,7 @@ def _load_environment_statements(self, macros: MacroRegistry) -> t.List[Environm d.jinja_statement(stmt).sql(dialect=dialect) for stmt in on_run_end or [] ], - python_env=python_env, + python_env={}, jinja_macros=jinja_registry, ) # Project hooks should be executed first and then rest of the packages diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index fb5f38a4c9..83349bf38e 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -4345,6 +4345,21 @@ def test_dbt_dialect_with_normalization_strategy(init_and_plan_context: t.Callab assert context.default_dialect == "duckdb,normalization_strategy=LOWERCASE" +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_dbt_before_all_with_var(init_and_plan_context: t.Callable): + _, plan = init_and_plan_context( + "tests/fixtures/dbt/sushi_test", config="test_config_with_normalization_strategy" + ) + environment_statements = plan.to_evaluatable().environment_statements + assert environment_statements + rendered_statements = [e.render_before_all(dialect="duckdb") for e in environment_statements] + assert rendered_statements[0] == [ + "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table TEXT, evaluation_time TEXT)", + "CREATE TABLE IF NOT EXISTS to_be_executed_last (col TEXT)", + 'SELECT 1 AS "1"', + ] + + @pytest.mark.parametrize( "context_fixture", ["sushi_context", "sushi_dbt_context", "sushi_test_dbt_context", "sushi_no_default_catalog"], diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index 4a0f1fd0c6..7c9ab0c187 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -288,6 +288,7 @@ def test_on_run_start_end(copy_to_temp_path): assert root_environment_statements.before_all == [ "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);\nJINJA_END;", "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS to_be_executed_last (col VARCHAR);\nJINJA_END;", + 'JINJA_STATEMENT_BEGIN;\nSELECT {{ var("yet_another_var") }}\nJINJA_END;', ] assert root_environment_statements.after_all == [ "JINJA_STATEMENT_BEGIN;\n{{ create_tables(schemas) }}\nJINJA_END;", @@ -317,6 +318,7 @@ def test_on_run_start_end(copy_to_temp_path): assert rendered_before_all == [ "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table TEXT, evaluation_time TEXT)", "CREATE TABLE IF NOT EXISTS to_be_executed_last (col TEXT)", + 'SELECT 1 AS "1"', ] # The jinja macro should have resolved the schemas for this environment and generated corresponding statements diff --git a/tests/fixtures/dbt/sushi_test/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_project.yml index d40fabf525..8c45f373f2 100644 --- a/tests/fixtures/dbt/sushi_test/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/dbt_project.yml @@ -62,6 +62,7 @@ vars: on-run-start: - 'CREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);' - 'CREATE TABLE IF NOT EXISTS to_be_executed_last (col VARCHAR);' + - 'SELECT {{ var("yet_another_var") }}' on-run-end: - '{{ create_tables(schemas) }}' - 'DROP TABLE to_be_executed_last;' \ No newline at end of file From 24c9b4eb314a0628ef25977c3aefa1697d963a8a Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 27 May 2025 21:04:36 +0200 Subject: [PATCH 0274/1056] fix(vscode): protect from infinite loops (#4555) --- web/server/api/endpoints/lineage.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/server/api/endpoints/lineage.py b/web/server/api/endpoints/lineage.py index 14d0237dca..5bef4601fd 100644 --- a/web/server/api/endpoints/lineage.py +++ b/web/server/api/endpoints/lineage.py @@ -109,6 +109,9 @@ def create_models_only_lineage_adjacency_list( """Create an adjacency list representation of a column's lineage graph only with models""" graph: t.Dict[str, t.Dict[str, LineageColumn]] = defaultdict(dict) nodes = [(model_name, column_name)] + # visited is a set of tuples of (model_name, column_name) to prevent infinite recursion + visited = set() + visited.add((model_name, column_name)) while nodes: model_name, column = nodes.pop(0) model = context.get_model(model_name) @@ -118,8 +121,10 @@ def create_models_only_lineage_adjacency_list( context, model_name, quote_column(column, model.dialect) ).items(): for column_name in column_names: - dependencies[table].add(column_name) - nodes.append((table, column_name)) + if (table, column_name) not in visited: + dependencies[table].add(column_name) + nodes.append((table, column_name)) + visited.add((table, column_name)) graph[model_name][column] = LineageColumn(models=dependencies) return graph From f4fa53f8af39496adb77e879bbffc9953f0a194c Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 27 May 2025 21:35:06 +0200 Subject: [PATCH 0275/1056] fix(vscode): on startup pick the right model (#4556) --- vscode/bus/src/callbacks.ts | 54 +++++++--- vscode/extension/src/webviews/lineagePanel.ts | 51 +++++++--- vscode/react/src/api/index.ts | 10 +- vscode/react/src/api/instance.ts | 53 +++------- .../src/components/graph/ModelLineage.tsx | 5 +- vscode/react/src/components/graph/context.tsx | 2 +- vscode/react/src/components/graph/help.ts | 1 - vscode/react/src/pages/lineage.tsx | 98 ++++++++++++++----- vscode/react/src/utils/rpc.ts | 50 ++++++++++ 9 files changed, 225 insertions(+), 99 deletions(-) create mode 100644 vscode/react/src/utils/rpc.ts diff --git a/vscode/bus/src/callbacks.ts b/vscode/bus/src/callbacks.ts index 0a1e474d48..8c492ace8c 100644 --- a/vscode/bus/src/callbacks.ts +++ b/vscode/bus/src/callbacks.ts @@ -1,15 +1,13 @@ +import type { Result } from './result' + +export type CallbackShape = Record + export type Callback = { openFile: { uri: string } - queryRequest: { - requestId: string - url: string - method?: string - params?: Record - body?: Record - } -} + rpcResponse: RPCResponse +} & CallbackShape /** * A tuple type representing a callback event with its associated payload. @@ -23,6 +21,8 @@ export type CallbackEvent = { [K in keyof Callback]: { key: K; payload: Callback[K] } }[keyof Callback] +export type VSCodeCallbackShape = Record + /** * A tuple type representing a VSCode event with its associated payload. */ @@ -30,15 +30,43 @@ export type VSCodeCallback = { changeFocusOnFile: { path: string } - queryResponse: { - requestId: string - response: any - } savedFile: { fileUri: string } -} + rpcRequest: RPCRequest +} & VSCodeCallbackShape export type VSCodeEvent = { [K in keyof VSCodeCallback]: { key: K; payload: VSCodeCallback[K] } }[keyof VSCodeCallback] + +type RPCMethodsShape = Record + +export type RPCMethods = { + get_active_file: { + params: {} + result: { + fileUri?: string + } + } + api_query: { + params: { + url: string + method: string + params: any + body: any + } + result: any + } +} & RPCMethodsShape + +export type RPCRequest = { + requestId: string + method: keyof RPCMethods + params: RPCMethods[keyof RPCMethods]['params'] +} + +export type RPCResponse = { + requestId: string + result: Result +} diff --git a/vscode/extension/src/webviews/lineagePanel.ts b/vscode/extension/src/webviews/lineagePanel.ts index 0c518dd0ef..f9d84dc256 100644 --- a/vscode/extension/src/webviews/lineagePanel.ts +++ b/vscode/extension/src/webviews/lineagePanel.ts @@ -1,4 +1,4 @@ -import { CallbackEvent } from '@bus/callbacks' +import { CallbackEvent, RPCRequest } from '@bus/callbacks' import { Disposable, Uri, @@ -83,18 +83,43 @@ export class LineagePanel implements WebviewViewProvider, Disposable { await window.showTextDocument(document) break } - case 'queryRequest': { - const payload = message.payload - const requestId = message.payload.requestId - const response = await this.lsp.call_custom_method( - 'sqlmesh/api', - payload as any, - ) - webviewView.webview.postMessage({ - key: 'query_response', - payload: response, - requestId, - }) + case 'rpcRequest': { + const payload: RPCRequest = message.payload + const requestId = payload.requestId + switch (payload.method) { + case 'api_query': { + const response = await this.lsp.call_custom_method( + 'sqlmesh/api', + payload.params, + ) + const responseCallback: CallbackEvent = { + key: 'rpcResponse', + payload: { + requestId, + result: response, + }, + } + webviewView.webview.postMessage(responseCallback) + break + } + case 'get_active_file': { + const active_file = window.activeTextEditor?.document.uri.fsPath + const responseCallback: CallbackEvent = { + key: 'rpcResponse', + payload: { + requestId, + result: { + fileUri: active_file, + }, + }, + } + webviewView.webview.postMessage(responseCallback) + break + } + default: { + throw new Error(`Unhandled RPC method: ${payload.method}`) + } + } break } default: diff --git a/vscode/react/src/api/index.ts b/vscode/react/src/api/index.ts index b22620f01d..0ba0314668 100644 --- a/vscode/react/src/api/index.ts +++ b/vscode/react/src/api/index.ts @@ -70,7 +70,15 @@ export function useApiModelLineage( return useQuery({ queryKey: ['/api/lineage', modelName], queryFn: async ({ signal }) => { - return await modelLineageApiLineageModelNameGet(modelName, { signal }) + try { + const response = await modelLineageApiLineageModelNameGet(modelName, { + signal, + }) + return response + } catch (error) { + console.error('error fetching lineage', error) + throw error + } }, }) } diff --git a/vscode/react/src/api/instance.ts b/vscode/react/src/api/instance.ts index eefc58b8c1..3627b273de 100644 --- a/vscode/react/src/api/instance.ts +++ b/vscode/react/src/api/instance.ts @@ -1,4 +1,4 @@ -import { sendVSCodeMessage } from '@/utils/vscodeapi' +import { callRpc } from '@/utils/rpc' import { isErr } from '@bus/result' declare global { @@ -38,43 +38,16 @@ export async function fetchAPI( config: FetchOptions, _options?: Partial, ): Promise { - // Generate a unique ID for this request - // Create a promise that will resolve when we get a response with matching ID - return new Promise((resolve, reject) => { - const requestId = `query_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` - const messageHandler = (event: MessageEvent) => { - if ( - event.data && - event.data.key === 'query_response' && - event.data.requestId === requestId - ) { - // Remove the listener once we get our response - window.removeEventListener('message', messageHandler) - - const payload = event.data.payload - if (isErr(payload)) { - reject(new Error(payload.error as string)) - } else { - resolve(payload.value.data) - } - } - } - - // Add the listener - window.addEventListener('message', messageHandler) - - sendVSCodeMessage('queryRequest', { - requestId, - url: config.url, - params: config.params as any, - body: config.data, - method: config.method, - }) - - // Set a timeout to prevent hanging promises - setTimeout(() => { - window.removeEventListener('message', messageHandler) - reject(new Error('Query request timed out')) - }, 30000) // 30 second timeout - }) + const request = { + url: config.url, + method: config.method, + params: config.params, + body: config.data, + } + const result = await callRpc('api_query', request) + if (isErr(result)) { + throw new Error(result.error) + } + const response = result.value.data + return response } diff --git a/vscode/react/src/components/graph/ModelLineage.tsx b/vscode/react/src/components/graph/ModelLineage.tsx index 7c90674bce..b936dd0285 100644 --- a/vscode/react/src/components/graph/ModelLineage.tsx +++ b/vscode/react/src/components/graph/ModelLineage.tsx @@ -78,7 +78,7 @@ export function ModelLineage({ useApiModelLineage(model.name) const { isFetching: isFetchingModels } = useApiModels() - const [isMegringModels, setIsMergingModels] = useState(false) + const [isMergingModels, setIsMergingModels] = useState(false) const [modelLineage, setModelLineage] = useState< ModelLineageApiLineageModelNameGet200 | undefined >(undefined) @@ -91,7 +91,6 @@ export function ModelLineage({ getModelLineage() .then(({ data }) => { setModelLineage(data) - if (isNil(data)) return setIsMergingModels(true) @@ -164,7 +163,7 @@ export function ModelLineage({ } const isFetching = - isFetchingModelLineage || isFetchingModels || isMegringModels + isFetchingModelLineage || isFetchingModels || isMergingModels return (
diff --git a/vscode/react/src/components/graph/context.tsx b/vscode/react/src/components/graph/context.tsx index 6785c42945..d889e5e7fe 100644 --- a/vscode/react/src/components/graph/context.tsx +++ b/vscode/react/src/components/graph/context.tsx @@ -104,7 +104,7 @@ export const LineageFlowContext = createContext({ setActiveEdges: () => {}, handleClickModel: () => {}, setManuallySelectedColumn: () => {}, - handleError: () => {}, + handleError: error => console.error(error), setLineage: () => {}, setLineageCache: () => {}, isActiveColumn: () => false, diff --git a/vscode/react/src/components/graph/help.ts b/vscode/react/src/components/graph/help.ts index 0cf0474b97..1dd7d07621 100644 --- a/vscode/react/src/components/graph/help.ts +++ b/vscode/react/src/components/graph/help.ts @@ -176,7 +176,6 @@ function getNodeMap({ ? decodeURI(modelName) : modelName const model = Object.values(models).find(m => m.fqn === decodedModelName) - console.log('model', model) const nodeType: LineageNodeModelType = isNotNil(model) ? (model.type as LineageNodeModelType) : // If model name present in lineage but not in global models diff --git a/vscode/react/src/pages/lineage.tsx b/vscode/react/src/pages/lineage.tsx index f1343f763a..0bb7e1a7aa 100644 --- a/vscode/react/src/pages/lineage.tsx +++ b/vscode/react/src/pages/lineage.tsx @@ -15,6 +15,7 @@ import { useEventBus } from '@/hooks/eventBus' import type { VSCodeEvent } from '@bus/callbacks' import { URI } from 'vscode-uri' import type { Model } from '@/api/client' +import { useRpc } from '@/utils/rpc' export function LineagePage() { const { emit } = useEventBus() @@ -66,49 +67,93 @@ export function LineagePage() { } function Lineage() { - const [selectedModelSet, setSelectedModelSet] = useState( + const [selectedModel, setSelectedModel] = useState( undefined, ) const { on } = useEventBus() const queryClient = useQueryClient() const { data: models, isLoading: isLoadingModels } = useApiModels() + const rpc = useRpc() React.useEffect(() => { - if (selectedModelSet === undefined && models && Array.isArray(models)) { - setSelectedModelSet(models[0].name) + const fetchFirstTimeModelIfNotSet = async ( + models: Model[], + ): Promise => { + if (!Array.isArray(models)) { + return undefined + } + const activeFile = await rpc('get_active_file', {}) + // @ts-ignore + if (!activeFile.fileUri) { + return models[0].name + } + // @ts-ignore + const fileUri: string = activeFile.fileUri + const filePath = URI.parse(fileUri).fsPath + const model = models.find((m: Model) => m.full_path === filePath) + if (model) { + return model.name + } + return undefined } - }, [models, selectedModelSet]) + if (selectedModel === undefined && Array.isArray(models)) { + fetchFirstTimeModelIfNotSet(models).then(modelName => { + if (modelName && selectedModel === undefined) { + setSelectedModel(modelName) + } + }) + } + }, [models, selectedModel]) + + const modelsRecord = + Array.isArray(models) && + models.reduce( + (acc, model) => { + acc[model.name] = model + return acc + }, + {} as Record, + ) + + React.useEffect(() => { + const handleChangeFocusedFile = (fileUri: { fileUri: string }) => { + const full_path = URI.parse(fileUri.fileUri).fsPath + const model = Object.values(modelsRecord).find( + m => m.full_path === full_path, + ) + if (model) { + setSelectedModel(model.name) + } + } + + const handleSavedFile = () => { + queryClient.invalidateQueries() + } + + const offChangeFocusedFile = on( + 'changeFocusedFile', + handleChangeFocusedFile, + ) + const offSavedFile = on('savedFile', handleSavedFile) + + // If your event bus returns an "off" function, call it on cleanup + return () => { + if (offChangeFocusedFile) offChangeFocusedFile() + if (offSavedFile) offSavedFile() + } + }, [on, queryClient, modelsRecord]) if ( isLoadingModels || models === undefined || - selectedModelSet === undefined + modelsRecord === false || + selectedModel === undefined ) { return
Loading models...
} if (!Array.isArray(models)) { return
Error: Models data is not in the expected format
} - const modelsRecord = models.reduce( - (acc, model) => { - acc[model.name] = model - return acc - }, - {} as Record, - ) - const selectedModel = selectedModelSet - on('changeFocusedFile', fileUri => { - const full_path = URI.parse(fileUri.fileUri).fsPath - const model = Object.values(modelsRecord).find( - m => m.full_path === full_path, - ) - if (model) { - setSelectedModelSet(model.name) - } - }) - on('savedFile', () => { - queryClient.invalidateQueries() - }) return ( m.fqn === decodedId) if (!model) { @@ -137,7 +181,7 @@ export function LineageComponentFromWeb({ } function handleError(error: any): void { - console.log(error) + console.error(error) } const model = models[selectedModel] diff --git a/vscode/react/src/utils/rpc.ts b/vscode/react/src/utils/rpc.ts new file mode 100644 index 0000000000..ccb92382e2 --- /dev/null +++ b/vscode/react/src/utils/rpc.ts @@ -0,0 +1,50 @@ +import { + type RPCRequest, + type RPCMethods, + type CallbackEvent, +} from '@bus/callbacks' +import { type Result } from '@bus/result' +import { sendVSCodeMessage } from './vscodeapi' + +export const useRpc = () => { + return callRpc +} + +export const callRpc = async ( + method: T, + params: RPCMethods[T]['params'], +): Promise> => { + return new Promise((resolve, reject) => { + const requestId = `query_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + const messageHandler = (event: MessageEvent) => { + if (event.data) { + const eventData = event.data as CallbackEvent + if (eventData.key !== 'rpcResponse') { + return + } + if (eventData.payload.requestId !== requestId) { + return + } + const payload = eventData.payload.result + window.removeEventListener('message', messageHandler) + return resolve(payload) + } + } + + // Add the listener + window.addEventListener('message', messageHandler) + + const request: RPCRequest = { + requestId, + method, + params, + } + sendVSCodeMessage('rpcRequest', request) + + // Set a timeout to prevent hanging promises + setTimeout(() => { + window.removeEventListener('message', messageHandler) + reject(new Error('Query request timed out')) + }, 30000) // 30 second timeout + }) +} From 86707967b37afc743099fd78c4ab9c34b621fffa Mon Sep 17 00:00:00 2001 From: Sung Won Chung Date: Tue, 27 May 2025 15:45:36 -0700 Subject: [PATCH 0276/1056] Update README for VS Code Extension (#4483) --- README.md | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8843a65ab1..24d8e610d4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ SQLMesh logo

-SQLMesh is a next-generation data transformation framework designed to ship data quickly, efficiently, and without error. Data teams can efficiently run and deploy data transformations written in SQL or Python with visibility and control at any size. +SQLMesh is a next-generation data transformation framework designed to ship data quickly, efficiently, and without error. Data teams can run and deploy data transformations written in SQL or Python with visibility and control at any size. It is more than just a [dbt alternative](https://tobikodata.com/reduce_costs_with_cron_and_partitions.html). @@ -11,9 +11,10 @@ It is more than just a [dbt alternative](https://tobikodata.com/reduce_costs_wit

## Core Features -SQLMesh Plan Mode -> Get instant SQL impact analysis of your changes, whether in the CLI or in [SQLMesh Plan Mode](https://sqlmesh.readthedocs.io/en/stable/guides/ui/?h=modes#working-with-an-ide) +SQLMesh Plan Mode + +> Get instant SQL impact and context of your changes, both in the CLI and in the [SQLMesh VSCode Extension](https://sqlmesh.readthedocs.io/en/latest/guides/vscode/?h=vs+cod)
Virtual Data Environments @@ -120,6 +121,7 @@ outputs: * Never build a table [more than once](https://tobikodata.com/simplicity-or-efficiency-how-dbt-makes-you-choose.html) * Track what data’s been modified and run only the necessary transformations for [incremental models](https://tobikodata.com/correctly-loading-incremental-data-at-scale.html) * Run [unit tests](https://tobikodata.com/we-need-even-greater-expectations.html) for free and configure automated audits +* Run [table diffs](https://sqlmesh.readthedocs.io/en/stable/examples/sqlmesh_cli_crash_course/?h=crash#run-data-diff-against-prod) between prod and dev based on tables/views impacted by a change
Level Up Your SQL @@ -131,7 +133,7 @@ Write SQL in any dialect and SQLMesh will transpile it to your target SQL dialec * Definitions using [simply SQL](https://sqlmesh.readthedocs.io/en/stable/concepts/models/sql_models/#sql-based-definition) (no need for redundant and confusing `Jinja` + `YAML`) * See impact of changes before you run them in your warehouse with column-level lineage -For more information, check out the [website](https://sqlmesh.com) and [documentation](https://sqlmesh.readthedocs.io/en/stable/). +For more information, check out the [website](https://www.tobikodata.com/sqlmesh) and [documentation](https://sqlmesh.readthedocs.io/en/stable/). ## Getting Started Install SQLMesh through [pypi](https://pypi.org/project/sqlmesh/) by running: @@ -141,16 +143,36 @@ mkdir sqlmesh-example cd sqlmesh-example python -m venv .venv source .venv/bin/activate -pip install sqlmesh +pip install 'sqlmesh[lsp]' # install the sqlmesh package with extensions to work with VSCode source .venv/bin/activate # reactivate the venv to ensure you're using the right installation sqlmesh init duckdb # get started right away with a local duckdb instance sqlmesh plan # see the plan for the changes you're making ``` +
+ > Note: You may need to run `python3` or `pip3` instead of `python` or `pip`, depending on your python installation. +
+Windows Installation + +```bash +mkdir sqlmesh-example +cd sqlmesh-example +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install 'sqlmesh[lsp]' # install the sqlmesh package with extensions to work with VSCode +.\.venv\Scripts\Activate.ps1 # reactivate the venv to ensure you're using the right installation +sqlmesh init duckdb # get started right away with a local duckdb instance +sqlmesh plan # see the plan for the changes you're making +``` +
+ + Follow the [quickstart guide](https://sqlmesh.readthedocs.io/en/stable/quickstart/cli/#1-create-the-sqlmesh-project) to learn how to use SQLMesh. You already have a head start! +Follow the [crash course](https://sqlmesh.readthedocs.io/en/stable/examples/sqlmesh_cli_crash_course/) to learn the core movesets and use the easy to reference cheat sheet. + Follow this [example](https://sqlmesh.readthedocs.io/en/stable/examples/incremental_time_full_walkthrough/) to learn how to use SQLMesh in a full walkthrough. ## Join Our Community From 9e077513024c4e1af6b8af27c1a873242e9fc396 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 28 May 2025 11:14:39 +1200 Subject: [PATCH 0277/1056] Fix: Catch errors caching optimized model queries so that SQLMesh can still load (#4532) --- sqlmesh/core/model/cache.py | 14 +++- tests/cli/test_integration_cli.py | 133 ++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 tests/cli/test_integration_cli.py diff --git a/sqlmesh/core/model/cache.py b/sqlmesh/core/model/cache.py index a01c1d788c..774bfa402b 100644 --- a/sqlmesh/core/model/cache.py +++ b/sqlmesh/core/model/cache.py @@ -142,6 +142,7 @@ def put(self, model: Model) -> t.Optional[str]: def _put(self, name: str, model: SqlModel) -> None: optimized_query = model.render_query() + new_entry = OptimizedQueryCacheEntry( optimized_rendered_query=optimized_query, renderer_violations=model.violated_rules_for_query, @@ -180,10 +181,17 @@ def load_optimized_query( assert _optimized_query_cache model, snapshot_id = model_snapshot_id + entry_name = None + if isinstance(model, SqlModel): - entry_name = _optimized_query_cache.put(model) - else: - entry_name = None + try: + entry_name = _optimized_query_cache.put(model) + except: + # this can happen if there is a query rendering error. + # for example, the model query references some python library or function that was available + # at the time the model was created but has since been removed locally + logger.exception(f"Failed to cache optimized query for model '{model.name}'") + return snapshot_id, entry_name diff --git a/tests/cli/test_integration_cli.py b/tests/cli/test_integration_cli.py new file mode 100644 index 0000000000..787c2c608b --- /dev/null +++ b/tests/cli/test_integration_cli.py @@ -0,0 +1,133 @@ +import typing as t +from pathlib import Path +import pytest +import subprocess +from sqlmesh.cli.example_project import init_example_project +from sqlmesh.utils import yaml +import shutil +import site + +pytestmark = pytest.mark.slow + + +class InvokeCliType(t.Protocol): + def __call__( + self, sqlmesh_args: t.List[str], **kwargs: t.Any + ) -> subprocess.CompletedProcess: ... + + +@pytest.fixture +def invoke_cli(tmp_path: Path) -> InvokeCliType: + # Fetch the full path to the SQLMesh binary so that when we use `cwd` to run in the context of a test dir, the correct SQLMesh binary is executed + # this will be the current project because `make install-dev` installs an editable version of SQLMesh into the current python environment + sqlmesh_bin = subprocess.run( + ["which", "sqlmesh"], capture_output=True, text=True + ).stdout.strip() + + def _invoke(sqlmesh_args: t.List[str], **kwargs: t.Any) -> subprocess.CompletedProcess: + return subprocess.run( + args=[sqlmesh_bin] + sqlmesh_args, + # set the working directory to the isolated temp dir for this test + cwd=tmp_path, + # return text instead of binary from the output streams + text=True, + # combine stdout/stderr into a single stream + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + **kwargs, + ) + + return _invoke + + +def test_load_snapshots_that_reference_nonexistent_python_libraries( + invoke_cli: InvokeCliType, tmp_path: Path +) -> None: + init_example_project(tmp_path, dialect="duckdb") + config_path = tmp_path / "config.yaml" + + # we need state to persist between invocations + config_dict = yaml.load(config_path) + config_dict["gateways"]["duckdb"]["state_connection"] = { + "type": "duckdb", + "database": str(tmp_path / "state.db"), + } + config_path.write_text(yaml.dump(config_dict)) + + # simulate a 3rd party library that provides a macro + site_packages = site.getsitepackages()[0] + sqlmesh_test_macros_package_path = Path(site_packages) / "sqlmesh_test_macros" + sqlmesh_test_macros_package_path.mkdir() + (sqlmesh_test_macros_package_path / "macros.py").write_text(""" +from sqlmesh import macro + +@macro() +def do_something(evaluator): + return "'value from site-packages'" +""") + + # reference the macro from site-packages + (tmp_path / "macros" / "__init__.py").write_text(""" +from sqlmesh_test_macros.macros import do_something +""") + + (tmp_path / "models" / "example.sql").write_text(""" +MODEL ( + name example.test_model, + kind FULL +); + +select @do_something() as a +""") + + result = invoke_cli(["plan", "--no-prompts", "--auto-apply", "--skip-tests"]) + + assert result.returncode == 0 + assert "Physical layer updated" in result.stdout + assert "Virtual layer updated" in result.stdout + + # render the query to ensure our macro is being invoked + result = invoke_cli(["render", "example.test_model"]) + assert result.returncode == 0 + assert """SELECT 'value from site-packages' AS "a\"""" in " ".join(result.stdout.split()) + + # clear cache to ensure we are forced to reload everything + assert invoke_cli(["clean"]).returncode == 0 + + # deleting this removes the 'do_something()' macro used by the version of the snapshot stored in state + # when loading the old snapshot from state in the local python env, this will create an ImportError + shutil.rmtree(sqlmesh_test_macros_package_path) + + # Move the macro inline so its no longer being loaded from a library but still exists with the same signature + (tmp_path / "macros" / "__init__.py").write_text(""" +from sqlmesh import macro + +@macro() +def do_something(evaluator): + return "'some value not from site-packages'" +""") + + # this should produce an error but not a fatal one. there will be an error rendering the optimized query of the old snapshot, which should be logged + result = invoke_cli( + [ + "plan", + "--no-prompts", + "--auto-apply", + "--skip-tests", + ] + ) + assert result.returncode == 0 + assert "Physical layer updated" in result.stdout + assert "Virtual layer updated" in result.stdout + + log_file = sorted(list((tmp_path / "logs").iterdir()))[-1] + log_file_contents = log_file.read_text() + assert "ModuleNotFoundError: No module named 'sqlmesh_test_macros'" in log_file_contents + assert ( + "ERROR - Failed to cache optimized query for model 'example.test_model'" + in log_file_contents + ) + assert ( + 'ERROR - Failed to cache snapshot SnapshotId<"db"."example"."test_model"' + in log_file_contents + ) From 792f5b07938be533eef2d509959f0c1962bed277 Mon Sep 17 00:00:00 2001 From: Christopher Giroir Date: Wed, 28 May 2025 09:32:48 -0700 Subject: [PATCH 0278/1056] doc: add cloud secrets docs (#4563) Co-authored-by: Trey Spiller --- docs/cloud/features/scheduler/scheduler.md | 75 +++++++++++++++++- .../features/scheduler/scheduler/secrets.png | Bin 0 -> 58623 bytes 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 docs/cloud/features/scheduler/scheduler/secrets.png diff --git a/docs/cloud/features/scheduler/scheduler.md b/docs/cloud/features/scheduler/scheduler.md index 0b85368ffe..5d28a3be50 100644 --- a/docs/cloud/features/scheduler/scheduler.md +++ b/docs/cloud/features/scheduler/scheduler.md @@ -167,4 +167,77 @@ Tobiko Cloud automatically manages Python dependencies of your Python macros and SQLMesh automatically infers which Python libraries are used by statically analyzing the code of your models and macros. -For fine-grained control, dependencies can be specified, pinned, or excluded using the `sqlmesh-requirements.lock` file. See the [Python library dependencies](../../../guides/configuration.md#python-library-dependencies) section in the SQLMesh configuration guide for more information. \ No newline at end of file +For fine-grained control, dependencies can be specified, pinned, or excluded using the `sqlmesh-requirements.lock` file. See the [Python library dependencies](../../../guides/configuration.md#python-library-dependencies) section in the SQLMesh configuration guide for more information. + +## Secret Manager + +Tobiko Cloud provides a secrets manager where you can define environment variables for your project's Python models. + +These variables are most commonly used to provide sensitive information to Python models, such as API keys or other credentials. + +Secret values are encrypted at rest and only available in the environment of your running Python models. + +!!! note "Cloud Scheduler Only" + + Secrets from the secret manager do not load into hybrid executors. They are only used for cloud scheduler executors. + +Secret names have two restrictions - they must: + +- Start with a letter or an underscore +- Only include letters, numbers, and underscores (no spaces or other symbols) + +Secret values have no limits or restrictions. We recommend base64 encoding any secrets that contain binary data. + +### Defining secrets + +Define a secret on the Secrets page, accessible via the Settings section in Tobiko Cloud's left side navigation bar. + +The Secrets page has a single panel you use to create a new secret, edit the value of an existing secret, or remove an existing secret. You cannot view the value of any existing secret. + +In this example, only one secret has been defined: `MY_SECRET`. Update its value by entering a new value in the Secret field and clicking the `Update` button, or delete it by clicking the `Remove` button. + +![secrets_panel](./scheduler/secrets.png) + + +### Python Model Example + +This Python model demonstrates how to read the `MY_SECRET` secret from an environment variable. + +!!! danger "Protecting Secrets" + + Only read environment variables from inside a Python model's `execute` function definition (not in the global scope). + + If the variable is read in the global scope, SQLMesh will load the value from *your local system* when it renders the Python model instead of loading it at runtime on our executors. + + This could expose sensitive information or embed an incorrect local value in the rendered model. + +```python linenums="1" +import os +import pandas as pd +import typing as t +from datetime import datetime + +from sqlmesh import ExecutionContext, model + +# DO NOT read environment variables here. +# Only inside the `execute` function definition! + +@model( + "my_model.name", + columns={ + "column_name": "int", + }, +) +def execute( + context: ExecutionContext, + start: datetime, + end: datetime, + execution_time: datetime, + **kwargs: t.Any, +) -> pd.DataFrame: + + # Read a secret from the MY_SECRET environment variable + my_secret = os.environ["MY_SECRET"] + + ... +``` diff --git a/docs/cloud/features/scheduler/scheduler/secrets.png b/docs/cloud/features/scheduler/scheduler/secrets.png new file mode 100644 index 0000000000000000000000000000000000000000..7873bf8b77ee754f4d2ed4d72c38dd3443e97cd8 GIT binary patch literal 58623 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 literal 0 HcmV?d00001 From 2785fc98615c21c9955c6bb34544f7af2a6a3f62 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 28 May 2025 10:00:40 -0700 Subject: [PATCH 0279/1056] Fix: Dependency handling when converting on-run-start / on-run-end hooks in dbt projects (#4567) --- sqlmesh/dbt/loader.py | 39 +++--- sqlmesh/dbt/manifest.py | 15 ++- sqlmesh/dbt/package.py | 1 + tests/core/test_integration.py | 4 +- tests/dbt/test_adapter.py | 111 ----------------- tests/dbt/test_transformation.py | 112 ++++++++++++++++++ tests/fixtures/dbt/sushi_test/dbt_project.yml | 3 +- 7 files changed, 147 insertions(+), 138 deletions(-) diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 8f450c6b7d..4f4100f092 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -28,7 +28,6 @@ from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.jinja import ( JinjaMacroRegistry, - extract_macro_references_and_variables, make_jinja_registry, ) @@ -240,11 +239,11 @@ def _load_requirements(self) -> t.Tuple[t.Dict[str, str], t.Set[str]]: def _load_environment_statements(self, macros: MacroRegistry) -> t.List[EnvironmentStatements]: """Loads dbt's on_run_start, on_run_end hooks into sqlmesh's before_all, after_all statements respectively.""" - environment_statements: t.List[EnvironmentStatements] = [] + hooks_by_package_name: t.Dict[str, EnvironmentStatements] = {} + project_names: t.Set[str] = set() dialect = self.config.dialect for project in self._load_projects(): context = project.context - hooks_by_package_name: t.Dict[str, EnvironmentStatements] = {} for package_name, package in project.packages.items(): context.set_and_render_variables(package.variables, package_name) on_run_start: t.List[str] = [ @@ -256,18 +255,14 @@ def _load_environment_statements(self, macros: MacroRegistry) -> t.List[Environm for on_run_hook in sorted(package.on_run_end.values(), key=lambda h: h.index) ] - if statements := on_run_start + on_run_end: - jinja_references, used_variables = extract_macro_references_and_variables( - *statements - ) + if on_run_start or on_run_end: + dependencies = Dependencies() + for hook in [*package.on_run_start.values(), *package.on_run_end.values()]: + dependencies = dependencies.union(hook.dependencies) - statements_context = context.context_for_dependencies( - Dependencies( - variables=used_variables, - ) - ) + statements_context = context.context_for_dependencies(dependencies) jinja_registry = make_jinja_registry( - statements_context.jinja_macros, package_name, jinja_references + statements_context.jinja_macros, package_name, set(dependencies.macros) ) jinja_registry.add_globals(statements_context.jinja_globals) @@ -283,15 +278,15 @@ def _load_environment_statements(self, macros: MacroRegistry) -> t.List[Environm python_env={}, jinja_macros=jinja_registry, ) - # Project hooks should be executed first and then rest of the packages - environment_statements = [ - statements - for _, statements in sorted( - hooks_by_package_name.items(), - key=lambda item: 0 if item[0] == context.project_name else 1, - ) - ] - return environment_statements + project_names.add(package_name) + + return [ + statements + for _, statements in sorted( + hooks_by_package_name.items(), + key=lambda item: 0 if item[0] in project_names else 1, + ) + ] def _compute_yaml_max_mtime_per_subfolder(self, root: Path) -> t.Dict[Path, float]: if not root.is_dir(): diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index fd67ef35c5..86f2f882ea 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -292,13 +292,24 @@ def _load_on_run_start_end(self) -> None: sql = node.raw_code if DBT_VERSION >= (1, 3) else node.raw_sql # type: ignore node_name = node.name node_path = Path(node.original_file_path) + + dependencies = Dependencies( + macros=_macro_references(self._manifest, node), + refs=_refs(node), + sources=_sources(node), + ) + dependencies = dependencies.union(self._extra_dependencies(sql, node.package_name)) + dependencies = dependencies.union( + self._flatten_dependencies_from_macros(dependencies.macros, node.package_name) + ) + if "on-run-start" in node.tags: self._on_run_start_per_package[node.package_name][node_name] = HookConfig( - sql=sql, index=node.index or 0, path=node_path + sql=sql, index=node.index or 0, path=node_path, dependencies=dependencies ) else: self._on_run_end_per_package[node.package_name][node_name] = HookConfig( - sql=sql, index=node.index or 0, path=node_path + sql=sql, index=node.index or 0, path=node_path, dependencies=dependencies ) @property diff --git a/sqlmesh/dbt/package.py b/sqlmesh/dbt/package.py index 8d2dc191f5..420cf3cb73 100644 --- a/sqlmesh/dbt/package.py +++ b/sqlmesh/dbt/package.py @@ -34,6 +34,7 @@ class HookConfig(PydanticModel): sql: str index: int path: Path + dependencies: Dependencies class Package(PydanticModel): diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 83349bf38e..a540eb21b3 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -4346,7 +4346,7 @@ def test_dbt_dialect_with_normalization_strategy(init_and_plan_context: t.Callab @time_machine.travel("2023-01-08 15:00:00 UTC") -def test_dbt_before_all_with_var(init_and_plan_context: t.Callable): +def test_dbt_before_all_with_var_ref_source(init_and_plan_context: t.Callable): _, plan = init_and_plan_context( "tests/fixtures/dbt/sushi_test", config="test_config_with_normalization_strategy" ) @@ -4356,7 +4356,7 @@ def test_dbt_before_all_with_var(init_and_plan_context: t.Callable): assert rendered_statements[0] == [ "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table TEXT, evaluation_time TEXT)", "CREATE TABLE IF NOT EXISTS to_be_executed_last (col TEXT)", - 'SELECT 1 AS "1"', + "SELECT 1 AS var, 'items' AS src, 'waiters' AS ref", ] diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index 7c9ab0c187..31428b953c 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -13,9 +13,6 @@ from sqlmesh import Context from sqlmesh.core.dialect import schema_ -from sqlmesh.core.environment import EnvironmentNamingInfo -from sqlmesh.core.macros import RuntimeStage -from sqlmesh.core.renderer import render_statements from sqlmesh.core.snapshot import SnapshotId from sqlmesh.dbt.adapter import ParsetimeAdapter from sqlmesh.dbt.project import Project @@ -275,114 +272,6 @@ def test_quote_as_configured(): adapter.quote_as_configured("foo", "database") == "foo" -def test_on_run_start_end(copy_to_temp_path): - project_root = "tests/fixtures/dbt/sushi_test" - sushi_context = Context(paths=copy_to_temp_path(project_root)) - assert len(sushi_context._environment_statements) == 2 - - # Root project's on run start / on run end should be first by checking the macros - root_environment_statements = sushi_context._environment_statements[0] - assert "create_tables" in root_environment_statements.jinja_macros.root_macros - - # Validate order of execution to be correct - assert root_environment_statements.before_all == [ - "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);\nJINJA_END;", - "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS to_be_executed_last (col VARCHAR);\nJINJA_END;", - 'JINJA_STATEMENT_BEGIN;\nSELECT {{ var("yet_another_var") }}\nJINJA_END;', - ] - assert root_environment_statements.after_all == [ - "JINJA_STATEMENT_BEGIN;\n{{ create_tables(schemas) }}\nJINJA_END;", - "JINJA_STATEMENT_BEGIN;\nDROP TABLE to_be_executed_last;\nJINJA_END;", - ] - - assert root_environment_statements.jinja_macros.root_package_name == "sushi" - - rendered_before_all = render_statements( - root_environment_statements.before_all, - dialect=sushi_context.default_dialect, - python_env=root_environment_statements.python_env, - jinja_macros=root_environment_statements.jinja_macros, - runtime_stage=RuntimeStage.BEFORE_ALL, - ) - - rendered_after_all = render_statements( - root_environment_statements.after_all, - dialect=sushi_context.default_dialect, - python_env=root_environment_statements.python_env, - jinja_macros=root_environment_statements.jinja_macros, - snapshots=sushi_context.snapshots, - runtime_stage=RuntimeStage.AFTER_ALL, - environment_naming_info=EnvironmentNamingInfo(name="dev"), - ) - - assert rendered_before_all == [ - "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table TEXT, evaluation_time TEXT)", - "CREATE TABLE IF NOT EXISTS to_be_executed_last (col TEXT)", - 'SELECT 1 AS "1"', - ] - - # The jinja macro should have resolved the schemas for this environment and generated corresponding statements - assert sorted(rendered_after_all) == sorted( - [ - "CREATE OR REPLACE TABLE schema_table_snapshots__dev AS SELECT 'snapshots__dev' AS schema", - "CREATE OR REPLACE TABLE schema_table_sushi__dev AS SELECT 'sushi__dev' AS schema", - "DROP TABLE to_be_executed_last", - ] - ) - - # Nested dbt_packages on run start / on run end - packaged_environment_statements = sushi_context._environment_statements[1] - - # Validate order of execution to be correct - assert packaged_environment_statements.before_all == [ - "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS to_be_executed_first (col VARCHAR);\nJINJA_END;", - "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS analytic_stats_packaged_project (physical_table VARCHAR, evaluation_time VARCHAR);\nJINJA_END;", - ] - assert packaged_environment_statements.after_all == [ - "JINJA_STATEMENT_BEGIN;\nDROP TABLE to_be_executed_first\nJINJA_END;", - "JINJA_STATEMENT_BEGIN;\n{{ packaged_tables(schemas) }}\nJINJA_END;", - ] - - assert "packaged_tables" in packaged_environment_statements.jinja_macros.root_macros - assert packaged_environment_statements.jinja_macros.root_package_name == "sushi" - - rendered_before_all = render_statements( - packaged_environment_statements.before_all, - dialect=sushi_context.default_dialect, - python_env=packaged_environment_statements.python_env, - jinja_macros=packaged_environment_statements.jinja_macros, - runtime_stage=RuntimeStage.BEFORE_ALL, - ) - - rendered_after_all = render_statements( - packaged_environment_statements.after_all, - dialect=sushi_context.default_dialect, - python_env=packaged_environment_statements.python_env, - jinja_macros=packaged_environment_statements.jinja_macros, - snapshots=sushi_context.snapshots, - runtime_stage=RuntimeStage.AFTER_ALL, - environment_naming_info=EnvironmentNamingInfo(name="dev"), - ) - - # Validate order of execution to match dbt's - assert rendered_before_all == [ - "CREATE TABLE IF NOT EXISTS to_be_executed_first (col TEXT)", - "CREATE TABLE IF NOT EXISTS analytic_stats_packaged_project (physical_table TEXT, evaluation_time TEXT)", - ] - - # This on run end statement should be executed first - assert rendered_after_all[0] == "DROP TABLE to_be_executed_first" - - # The table names is an indication of the rendering of the dbt_packages statements - assert sorted(rendered_after_all) == sorted( - [ - "DROP TABLE to_be_executed_first", - "CREATE OR REPLACE TABLE schema_table_snapshots__dev_nested_package AS SELECT 'snapshots__dev' AS schema", - "CREATE OR REPLACE TABLE schema_table_sushi__dev_nested_package AS SELECT 'sushi__dev' AS schema", - ] - ) - - def test_adapter_get_relation_normalization( sushi_test_project: Project, runtime_renderer: t.Callable ): diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index b6c1aac01a..bc970dfaaf 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -13,6 +13,9 @@ from pytest_mock.plugin import MockerFixture from sqlglot import exp, parse_one from sqlmesh.core import dialect as d +from sqlmesh.core.environment import EnvironmentNamingInfo +from sqlmesh.core.macros import RuntimeStage +from sqlmesh.core.renderer import render_statements from sqlmesh.core.audit import StandaloneAudit from sqlmesh.core.context import Context from sqlmesh.core.console import get_console @@ -1551,3 +1554,112 @@ def test_grain(): model.grain = "id_a" assert model.to_sqlmesh(context).grains == [exp.to_column("id_a")] + + +def test_on_run_start_end(copy_to_temp_path): + project_root = "tests/fixtures/dbt/sushi_test" + sushi_context = Context(paths=copy_to_temp_path(project_root)) + assert len(sushi_context._environment_statements) == 2 + + # Root project's on run start / on run end should be first by checking the macros + root_environment_statements = sushi_context._environment_statements[0] + assert "create_tables" in root_environment_statements.jinja_macros.root_macros + + # Validate order of execution to be correct + assert root_environment_statements.before_all == [ + "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);\nJINJA_END;", + "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS to_be_executed_last (col VARCHAR);\nJINJA_END;", + """JINJA_STATEMENT_BEGIN;\nSELECT {{ var("yet_another_var") }} AS var, '{{ source("raw", "items").identifier }}' AS src, '{{ ref("waiters").identifier }}' AS ref;\nJINJA_END;""", + "JINJA_STATEMENT_BEGIN;\n{{ log_value('on-run-start') }}\nJINJA_END;", + ] + assert root_environment_statements.after_all == [ + "JINJA_STATEMENT_BEGIN;\n{{ create_tables(schemas) }}\nJINJA_END;", + "JINJA_STATEMENT_BEGIN;\nDROP TABLE to_be_executed_last;\nJINJA_END;", + ] + + assert root_environment_statements.jinja_macros.root_package_name == "sushi" + + rendered_before_all = render_statements( + root_environment_statements.before_all, + dialect=sushi_context.default_dialect, + python_env=root_environment_statements.python_env, + jinja_macros=root_environment_statements.jinja_macros, + runtime_stage=RuntimeStage.BEFORE_ALL, + ) + + rendered_after_all = render_statements( + root_environment_statements.after_all, + dialect=sushi_context.default_dialect, + python_env=root_environment_statements.python_env, + jinja_macros=root_environment_statements.jinja_macros, + snapshots=sushi_context.snapshots, + runtime_stage=RuntimeStage.AFTER_ALL, + environment_naming_info=EnvironmentNamingInfo(name="dev"), + ) + + assert rendered_before_all == [ + "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table TEXT, evaluation_time TEXT)", + "CREATE TABLE IF NOT EXISTS to_be_executed_last (col TEXT)", + "SELECT 1 AS var, 'items' AS src, 'waiters' AS ref", + ] + + # The jinja macro should have resolved the schemas for this environment and generated corresponding statements + assert sorted(rendered_after_all) == sorted( + [ + "CREATE OR REPLACE TABLE schema_table_snapshots__dev AS SELECT 'snapshots__dev' AS schema", + "CREATE OR REPLACE TABLE schema_table_sushi__dev AS SELECT 'sushi__dev' AS schema", + "DROP TABLE to_be_executed_last", + ] + ) + + # Nested dbt_packages on run start / on run end + packaged_environment_statements = sushi_context._environment_statements[1] + + # Validate order of execution to be correct + assert packaged_environment_statements.before_all == [ + "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS to_be_executed_first (col VARCHAR);\nJINJA_END;", + "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS analytic_stats_packaged_project (physical_table VARCHAR, evaluation_time VARCHAR);\nJINJA_END;", + ] + assert packaged_environment_statements.after_all == [ + "JINJA_STATEMENT_BEGIN;\nDROP TABLE to_be_executed_first\nJINJA_END;", + "JINJA_STATEMENT_BEGIN;\n{{ packaged_tables(schemas) }}\nJINJA_END;", + ] + + assert "packaged_tables" in packaged_environment_statements.jinja_macros.root_macros + assert packaged_environment_statements.jinja_macros.root_package_name == "sushi" + + rendered_before_all = render_statements( + packaged_environment_statements.before_all, + dialect=sushi_context.default_dialect, + python_env=packaged_environment_statements.python_env, + jinja_macros=packaged_environment_statements.jinja_macros, + runtime_stage=RuntimeStage.BEFORE_ALL, + ) + + rendered_after_all = render_statements( + packaged_environment_statements.after_all, + dialect=sushi_context.default_dialect, + python_env=packaged_environment_statements.python_env, + jinja_macros=packaged_environment_statements.jinja_macros, + snapshots=sushi_context.snapshots, + runtime_stage=RuntimeStage.AFTER_ALL, + environment_naming_info=EnvironmentNamingInfo(name="dev"), + ) + + # Validate order of execution to match dbt's + assert rendered_before_all == [ + "CREATE TABLE IF NOT EXISTS to_be_executed_first (col TEXT)", + "CREATE TABLE IF NOT EXISTS analytic_stats_packaged_project (physical_table TEXT, evaluation_time TEXT)", + ] + + # This on run end statement should be executed first + assert rendered_after_all[0] == "DROP TABLE to_be_executed_first" + + # The table names is an indication of the rendering of the dbt_packages statements + assert sorted(rendered_after_all) == sorted( + [ + "DROP TABLE to_be_executed_first", + "CREATE OR REPLACE TABLE schema_table_snapshots__dev_nested_package AS SELECT 'snapshots__dev' AS schema", + "CREATE OR REPLACE TABLE schema_table_sushi__dev_nested_package AS SELECT 'sushi__dev' AS schema", + ] + ) diff --git a/tests/fixtures/dbt/sushi_test/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_project.yml index 8c45f373f2..d833eaf0c0 100644 --- a/tests/fixtures/dbt/sushi_test/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/dbt_project.yml @@ -62,7 +62,8 @@ vars: on-run-start: - 'CREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);' - 'CREATE TABLE IF NOT EXISTS to_be_executed_last (col VARCHAR);' - - 'SELECT {{ var("yet_another_var") }}' + - SELECT {{ var("yet_another_var") }} AS var, '{{ source("raw", "items").identifier }}' AS src, '{{ ref("waiters").identifier }}' AS ref; + - "{{ log_value('on-run-start') }}" on-run-end: - '{{ create_tables(schemas) }}' - 'DROP TABLE to_be_executed_last;' \ No newline at end of file From 1fb40ee2fb5e570952ac19d1fcbdf9b829d44893 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 28 May 2025 21:26:54 +0200 Subject: [PATCH 0280/1056] chore(vscode): introduce pull first diagnostics (#4565) --- sqlmesh/lsp/main.py | 211 +++++++++++++++++---- vscode/extension/tests/diagnostics.spec.ts | 43 +++++ 2 files changed, 221 insertions(+), 33 deletions(-) create mode 100644 vscode/extension/tests/diagnostics.spec.ts diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 262c1c3c9e..3d04f0d558 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -57,8 +57,12 @@ def __init__( self.server = LanguageServer(server_name, version) self.context_class = context_class self.lsp_context: t.Optional[LSPContext] = None - self.lint_cache: t.Dict[URI, t.List[AnnotatedRuleViolation]] = {} + # Cache stores tuples of (diagnostics, diagnostic_version) + self.lint_cache: t.Dict[URI, t.Tuple[t.List[AnnotatedRuleViolation], int]] = {} + self._diagnostic_version_counter: int = 0 + + self.client_supports_pull_diagnostics = False # Register LSP features (e.g., formatting, hover, etc.) self._register_features() @@ -69,6 +73,18 @@ def _register_features(self) -> None: def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: """Initialize the server when the client connects.""" try: + # Check if client supports pull diagnostics + if params.capabilities and params.capabilities.text_document: + diagnostics = getattr(params.capabilities.text_document, "diagnostic", None) + if diagnostics: + self.client_supports_pull_diagnostics = True + ls.log_trace("Client supports pull diagnostics") + else: + self.client_supports_pull_diagnostics = False + ls.log_trace("Client does not support pull diagnostics") + else: + self.client_supports_pull_diagnostics = False + if params.workspace_folders: # Try to find a SQLMesh config file in any workspace folder (only at the root level) for folder in params.workspace_folders: @@ -153,61 +169,71 @@ def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]: def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> None: uri = URI(params.text_document.uri) context = self._context_get_or_load(uri) - if self.lint_cache.get(uri) is not None: + models = context.map[uri.to_path()] + if models is None or not isinstance(models, ModelTarget): + return + + if self.lint_cache.get(uri) is None: + diagnostics = context.context.lint_models( + models.names, + raise_on_error=False, + ) + self._diagnostic_version_counter += 1 + self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter) + + # Only publish diagnostics if client doesn't support pull diagnostics + if not self.client_supports_pull_diagnostics: + diagnostics, _ = self.lint_cache[uri] ls.publish_diagnostics( params.text_document.uri, - SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(self.lint_cache[uri]), + SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), ) - return - models = context.map[uri.to_path()] - if models is None: - return - if not isinstance(models, ModelTarget): - return - self.lint_cache[uri] = context.context.lint_models( - models.names, - raise_on_error=False, - ) - ls.publish_diagnostics( - params.text_document.uri, - SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(self.lint_cache[uri]), - ) @self.server.feature(types.TEXT_DOCUMENT_DID_CHANGE) def did_change(ls: LanguageServer, params: types.DidChangeTextDocumentParams) -> None: uri = URI(params.text_document.uri) context = self._context_get_or_load(uri) models = context.map[uri.to_path()] - if models is None: + if models is None or not isinstance(models, ModelTarget): return - if not isinstance(models, ModelTarget): - return - self.lint_cache[uri] = context.context.lint_models( + + # Always update the cache + diagnostics = context.context.lint_models( models.names, raise_on_error=False, ) - ls.publish_diagnostics( - params.text_document.uri, - SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(self.lint_cache[uri]), - ) + self._diagnostic_version_counter += 1 + self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter) + + # Only publish diagnostics if client doesn't support pull diagnostics + if not self.client_supports_pull_diagnostics: + ls.publish_diagnostics( + params.text_document.uri, + SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), + ) @self.server.feature(types.TEXT_DOCUMENT_DID_SAVE) def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> None: uri = URI(params.text_document.uri) context = self._context_get_or_load(uri) models = context.map[uri.to_path()] - if models is None: - return - if not isinstance(models, ModelTarget): + if models is None or not isinstance(models, ModelTarget): return - self.lint_cache[uri] = context.context.lint_models( + + # Always update the cache + diagnostics = context.context.lint_models( models.names, raise_on_error=False, ) - ls.publish_diagnostics( - params.text_document.uri, - SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(self.lint_cache[uri]), - ) + self._diagnostic_version_counter += 1 + self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter) + + # Only publish diagnostics if client doesn't support pull diagnostics + if not self.client_supports_pull_diagnostics: + ls.publish_diagnostics( + params.text_document.uri, + SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), + ) @self.server.feature(types.TEXT_DOCUMENT_FORMATTING) def formatting( @@ -327,6 +353,125 @@ def goto_definition( ls.show_message(f"Error getting references: {e}", types.MessageType.Error) return [] + @self.server.feature(types.TEXT_DOCUMENT_DIAGNOSTIC) + def diagnostic( + ls: LanguageServer, params: types.DocumentDiagnosticParams + ) -> types.DocumentDiagnosticReport: + """Handle diagnostic pull requests from the client.""" + try: + uri = URI(params.text_document.uri) + diagnostics, result_id = self._get_diagnostics_for_uri(uri) + + # Check if client provided a previous result ID + if hasattr(params, "previous_result_id") and params.previous_result_id == result_id: + # Return unchanged report if diagnostics haven't changed + return types.RelatedUnchangedDocumentDiagnosticReport( + kind=types.DocumentDiagnosticReportKind.Unchanged, + result_id=str(result_id), + ) + + return types.RelatedFullDocumentDiagnosticReport( + kind=types.DocumentDiagnosticReportKind.Full, + items=diagnostics, + result_id=str(result_id), + ) + except Exception as e: + ls.show_message(f"Error getting diagnostics: {e}", types.MessageType.Error) + return types.RelatedFullDocumentDiagnosticReport( + kind=types.DocumentDiagnosticReportKind.Full, + items=[], + ) + + @self.server.feature(types.WORKSPACE_DIAGNOSTIC) + def workspace_diagnostic( + ls: LanguageServer, params: types.WorkspaceDiagnosticParams + ) -> types.WorkspaceDiagnosticReport: + """Handle workspace-wide diagnostic pull requests from the client.""" + try: + if self.lsp_context is None: + current_path = Path.cwd() + self._ensure_context_in_folder(current_path) + + if self.lsp_context is None: + return types.WorkspaceDiagnosticReport(items=[]) + + items: t.List[ + t.Union[ + types.WorkspaceFullDocumentDiagnosticReport, + types.WorkspaceUnchangedDocumentDiagnosticReport, + ] + ] = [] + + # Get all SQL and Python model files from the context + for path, target in self.lsp_context.map.items(): + if isinstance(target, ModelTarget): + uri = URI.from_path(path) + diagnostics, result_id = self._get_diagnostics_for_uri(uri) + + # Check if we have a previous result ID for this file + previous_result_id = None + if hasattr(params, "previous_result_ids") and params.previous_result_ids: + for prev in params.previous_result_ids: + if prev.uri == uri.value: + previous_result_id = prev.value + break + + if previous_result_id and previous_result_id == result_id: + # File hasn't changed + items.append( + types.WorkspaceUnchangedDocumentDiagnosticReport( + kind=types.DocumentDiagnosticReportKind.Unchanged, + result_id=str(result_id), + uri=uri.value, + ) + ) + else: + # File has changed or is new + items.append( + types.WorkspaceFullDocumentDiagnosticReport( + kind=types.DocumentDiagnosticReportKind.Full, + result_id=str(result_id), + uri=uri.value, + items=diagnostics, + ) + ) + + return types.WorkspaceDiagnosticReport(items=items) + + except Exception as e: + ls.show_message( + f"Error getting workspace diagnostics: {e}", types.MessageType.Error + ) + return types.WorkspaceDiagnosticReport(items=[]) + + def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic], int]: + """Get diagnostics for a specific URI, returning (diagnostics, result_id).""" + # Check if we have cached diagnostics + if uri in self.lint_cache: + diagnostics, result_id = self.lint_cache[uri] + return SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), result_id + + # Try to get diagnostics by loading context and linting + try: + context = self._context_get_or_load(uri) + models = context.map[uri.to_path()] + if models is None or not isinstance(models, ModelTarget): + return [], 0 + + # Lint the models and cache the results + diagnostics = context.context.lint_models( + models.names, + raise_on_error=False, + ) + self._diagnostic_version_counter += 1 + self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter) + return SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics( + diagnostics + ), self._diagnostic_version_counter + except Exception: + # If we can't get diagnostics, return empty list with no result ID + return [], 0 + def _context_get_or_load(self, document_uri: URI) -> LSPContext: if self.lsp_context is None: self._ensure_context_for_document(document_uri) diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts new file mode 100644 index 0000000000..a189b25095 --- /dev/null +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; +import fs from 'fs-extra'; +import os from 'os'; +import { startVSCode, SUSHI_SOURCE_PATH } from './utils'; + +test('Workspace diagnostics show up in the diagnostics panel', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); + await fs.copy(SUSHI_SOURCE_PATH, tempDir); + + const configPath = path.join(tempDir, 'config.py'); + const configContent = await fs.readFile(configPath, 'utf8'); + const updatedContent = configContent.replace('enabled=False', 'enabled=True'); + await fs.writeFile(configPath, updatedContent); + + try { + const { window, close } = await startVSCode(tempDir); + + // Wait for the models folder to be visible + await window.waitForSelector('text=models'); + + // Click on the models folder, excluding external_models + await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); + + // Open the customer_revenue_lifetime model + await window.getByRole('treeitem', { name: 'customers.sql', exact: true }).locator('a').click(); + + await + + // Open problems panel + await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); + await window.keyboard.type('View: Focus Problems'); + await window.keyboard.press('Enter'); + + + await window.waitForSelector('text=problems'); + await window.waitForSelector("text=All models should have an owner"); + + await close(); + } finally { + await fs.remove(tempDir); + } + }); From fa4391256d056de5e0026a2779cdc520632ab784 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 29 May 2025 16:48:49 +0200 Subject: [PATCH 0281/1056] fix(lsp): fail gently on scope building failure (#4576) --- sqlmesh/lsp/reference.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 58409ccaa3..089d775cef 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -134,9 +134,12 @@ def get_model_definitions_for_a_path( with open(file_path, "r", encoding="utf-8") as file: read_file = file.readlines() - # Build scope tree to properly handle nested CTEs - query = normalize_identifiers(query.copy(), dialect=dialect) - root_scope = build_scope(query) + # Build a scope tree to properly handle nested CTEs + try: + query = normalize_identifiers(query.copy(), dialect=dialect) + root_scope = build_scope(query) + except Exception: + root_scope = None if root_scope: # Traverse all scopes to find CTE definitions and table references From 022bf29dc5e16dd0f9249fb530f276c5763efe13 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 29 May 2025 19:57:22 +0200 Subject: [PATCH 0282/1056] feat(vscode): gtd for macros (#4566) --- examples/sushi/macros/utils.py | 1 + sqlmesh/core/context.py | 13 +- sqlmesh/lsp/reference.py | 188 ++++++++++++++++++ tests/lsp/test_reference_macro.py | 30 +++ tests/lsp/test_reference_macro_multi.py | 25 +++ .../extension/tests/go_to_definition.spec.ts | 38 ++++ vscode/extension/tests/utils.ts | 5 +- web/server/api/endpoints/files.py | 2 +- web/server/utils.py | 3 +- 9 files changed, 293 insertions(+), 12 deletions(-) create mode 100644 tests/lsp/test_reference_macro.py create mode 100644 tests/lsp/test_reference_macro_multi.py create mode 100644 vscode/extension/tests/go_to_definition.spec.ts diff --git a/examples/sushi/macros/utils.py b/examples/sushi/macros/utils.py index fbc4c04e31..a76bc3bfe0 100644 --- a/examples/sushi/macros/utils.py +++ b/examples/sushi/macros/utils.py @@ -26,6 +26,7 @@ def sql_literal( column_str: str, column_quoted: str, ): + """A macro that accepts various types of SQL literals and returns the column.""" assert isinstance(column, str) assert isinstance(str_lit, str) assert str_lit == "'x'" diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 1f4722264c..238e199558 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -932,19 +932,22 @@ def get_snapshot( return snapshot - def config_for_path(self, path: Path) -> Config: + def config_for_path(self, path: Path) -> t.Tuple[Config, Path]: + """Returns the config and path of the said project for a given file path.""" for config_path, config in self.configs.items(): try: path.relative_to(config_path) - return config + return config, config_path except ValueError: pass - return self.config + return self.config, self.path def config_for_node(self, node: str | Model | Audit) -> Config: if isinstance(node, str): - return self.config_for_path(self.get_snapshot(node, raise_if_missing=True).node._path) # type: ignore - return self.config_for_path(node._path) # type: ignore + return self.config_for_path(self.get_snapshot(node, raise_if_missing=True).node._path)[ + 0 + ] # type: ignore + return self.config_for_path(node._path)[0] # type: ignore @property def models(self) -> MappingProxyType[str, Model]: diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 089d775cef..b8e6e9c422 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -1,6 +1,8 @@ from lsprotocol.types import Range, Position import typing as t +from pathlib import Path +from sqlmesh.core.audit import StandaloneAudit from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core.model.definition import SqlModel from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget @@ -10,6 +12,10 @@ from sqlmesh.lsp.uri import URI from sqlmesh.utils.pydantic import PydanticModel from sqlglot.optimizer.normalize_identifiers import normalize_identifiers +import ast +from sqlmesh.core.model import Model +from sqlmesh import macro +import inspect class Reference(PydanticModel): @@ -72,6 +78,11 @@ def get_references( A list of references at the given position """ references = get_model_definitions_for_a_path(lint_context, document_uri) + + # Get macro references before filtering by position + macro_references = get_macro_definitions_for_a_path(lint_context, document_uri) + references.extend(macro_references) + filtered_references = list(filter(by_position(position), references)) return filtered_references @@ -290,3 +301,180 @@ def _range_from_token_position_details( start=Position(line=start_line_0, character=start_col_0), end=Position(line=end_line_0, character=end_col_0), ) + + +def get_macro_definitions_for_a_path( + lsp_context: LSPContext, document_uri: URI +) -> t.List[Reference]: + """ + Get macro references for a given path. + + This function finds all macro invocations (e.g., @ADD_ONE, @MULTIPLY) in a SQL file + and creates references to their definitions in the Python macro files. + + Args: + lsp_context: The LSP context containing macro definitions + document_uri: The URI of the document to search for macro invocations + + Returns: + A list of Reference objects for each macro invocation found + """ + path = document_uri.to_path() + if path.suffix != ".sql": + return [] + + # Get the file info from the context map + if path not in lsp_context.map: + return [] + + file_info = lsp_context.map[path] + # Process based on whether it's a model or standalone audit + if isinstance(file_info, ModelTarget): + # It's a model + target: t.Optional[t.Union[Model, StandaloneAudit]] = lsp_context.context.get_model( + model_or_snapshot=file_info.names[0], raise_if_missing=False + ) + if target is None or not isinstance(target, SqlModel): + return [] + query = target.query + file_path = target._path + elif isinstance(file_info, AuditTarget): + # It's a standalone audit + target = lsp_context.context.standalone_audits.get(file_info.name) + if target is None: + return [] + query = target.query + file_path = target._path + else: + return [] + + references = [] + config_for_model, config_path = lsp_context.context.config_for_path( + file_path, + ) + + with open(file_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + for node in query.find_all(exp.Anonymous): + macro_name = node.name.lower() + reference = get_macro_reference( + node=node, + target=target, + read_file=read_file, + config_path=config_path, + macro_name=macro_name, + ) + if reference is not None: + references.append(reference) + + return references + + +def get_macro_reference( + target: t.Union[Model, StandaloneAudit], + read_file: t.List[str], + config_path: t.Optional[Path], + node: exp.Expression, + macro_name: str, +) -> t.Optional[Reference]: + # Get the file path where the macro is defined + try: + # Get the position of the macro invocation in the source file first + if hasattr(node, "meta") and node.meta: + token_details = TokenPositionDetails.from_meta(node.meta) + macro_range = _range_from_token_position_details(token_details, read_file) + + # Check if it's a built-in method + if builtin := get_built_in_macro_reference(macro_name, macro_range): + return builtin + else: + # Skip if we can't get the position + return None + + # Find the macro definition information + macro_def = target.python_env.get(macro_name) + if macro_def is None: + return None + + function_name = macro_def.name + if not function_name: + return None + if not macro_def.path: + return None + if not config_path: + return None + path = Path(config_path).joinpath(macro_def.path) + + # Parse the Python file to find the function definition + with open(path, "r") as f: + tree = ast.parse(f.read()) + with open(path, "r") as f: + output_read_line = f.readlines() + + # Find the function definition by name + start_line = None + end_line = None + get_length_of_end_line = None + docstring = None + for ast_node in ast.walk(tree): + if isinstance(ast_node, ast.FunctionDef) and ast_node.name == function_name: + start_line = ast_node.lineno + end_line = ast_node.end_lineno + get_length_of_end_line = ( + len(output_read_line[end_line - 1]) + if end_line is not None and end_line - 1 < len(read_file) + else 0 + ) + # Extract docstring if present + docstring = ast.get_docstring(ast_node) + break + + if start_line is None or end_line is None or get_length_of_end_line is None: + return None + + # Create a reference to the macro definition + macro_uri = URI.from_path(path) + + return Reference( + uri=macro_uri.value, + range=macro_range, + target_range=Range( + start=Position(line=start_line - 1, character=0), + end=Position(line=end_line - 1, character=get_length_of_end_line), + ), + markdown_description=docstring, + ) + except Exception: + return None + + +def get_built_in_macro_reference(macro_name: str, macro_range: Range) -> t.Optional[Reference]: + """ + Get a reference to a built-in macro by its name. + + Args: + macro_name: The name of the built-in macro (e.g., 'each', 'sql_literal') + macro_range: The range of the macro invocation in the source file + """ + built_in_macros = macro.get_registry() + built_in_macro = built_in_macros.get(macro_name) + if built_in_macro is None: + return None + + func = built_in_macro.func + filename = inspect.getfile(func) + source_lines, line_number = inspect.getsourcelines(func) + + # Calculate the end line number by counting the number of source lines + end_line_number = line_number + len(source_lines) - 1 + + return Reference( + uri=URI.from_path(Path(filename)).value, + range=macro_range, + target_range=Range( + start=Position(line=line_number - 1, character=0), + end=Position(line=end_line_number - 1, character=0), + ), + markdown_description=func.__doc__ if func.__doc__ else None, + ) diff --git a/tests/lsp/test_reference_macro.py b/tests/lsp/test_reference_macro.py new file mode 100644 index 0000000000..35cf317b91 --- /dev/null +++ b/tests/lsp/test_reference_macro.py @@ -0,0 +1,30 @@ +import pytest +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.reference import get_macro_definitions_for_a_path +from sqlmesh.lsp.uri import URI + + +@pytest.mark.fast +def test_macro_references() -> None: + """Test that macro references (e.g., @ADD_ONE, @MULTIPLY) have proper go-to-definition support.""" + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Find the top_waiters model that uses macros + top_waiters_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.top_waiters" in info.names + ) + + top_waiters_uri = URI.from_path(top_waiters_path) + macro_references = get_macro_definitions_for_a_path(lsp_context, top_waiters_uri) + + # We expect 3 macro references: @ADD_ONE, @MULTIPLY, @SQL_LITERAL + assert len(macro_references) == 3 + + # Check that all references point to the utils.py file + for ref in macro_references: + assert ref.uri.endswith("sushi/macros/utils.py") + assert ref.target_range is not None diff --git a/tests/lsp/test_reference_macro_multi.py b/tests/lsp/test_reference_macro_multi.py new file mode 100644 index 0000000000..3dfa588efb --- /dev/null +++ b/tests/lsp/test_reference_macro_multi.py @@ -0,0 +1,25 @@ +import pytest +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.reference import get_macro_definitions_for_a_path +from sqlmesh.lsp.uri import URI + + +@pytest.mark.fast +def test_macro_references_multirepo() -> None: + context = Context(paths=["examples/multi/repo_1", "examples/multi/repo_2"]) + lsp_context = LSPContext(context) + + d_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "silver.d" in info.names + ) + + d = URI.from_path(d_path) + macro_references = get_macro_definitions_for_a_path(lsp_context, d) + + assert len(macro_references) == 2 + for ref in macro_references: + assert ref.uri.endswith("multi/repo_2/macros/__init__.py") + assert ref.target_range is not None diff --git a/vscode/extension/tests/go_to_definition.spec.ts b/vscode/extension/tests/go_to_definition.spec.ts new file mode 100644 index 0000000000..7047f06ffd --- /dev/null +++ b/vscode/extension/tests/go_to_definition.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; +import fs from 'fs-extra'; +import os from 'os'; +import { startVSCode, SUSHI_SOURCE_PATH } from './utils'; + +test('Go to definition for macro', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); + await fs.copy(SUSHI_SOURCE_PATH, tempDir); + + try { + const { window, close } = await startVSCode(tempDir); + + // Wait for the models folder to be visible + await window.waitForSelector('text=models'); + + // Click on the models folder, excluding external_models + await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); + + // Open the customer_revenue_lifetime model + await window.getByRole('treeitem', { name: 'top_waiters.sql', exact: true }).locator('a').click(); + + await window.waitForSelector('text=grain'); + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Render the model + window.locator("text=@MULTIPLY").click({ + modifiers: ["Meta"] + }) + + // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window + await expect(window.locator('text=def multiply(')).toBeVisible(); + + await close(); + } finally { + await fs.removeSync(tempDir); + } + }); \ No newline at end of file diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index bceceb0f70..e836d61ea7 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -40,11 +40,8 @@ export const startVSCode = async (workspaceDir: string): Promise<{ args, }); const window = await electronApp.firstWindow(); - await window.waitForLoadState('domcontentloaded'); - await window.waitForLoadState('networkidle'); - await window.waitForTimeout(2_000); return { window, close: async () => { await electronApp.close(); - await fs.remove(userDataDir); + await fs.removeSync(userDataDir); } }; } diff --git a/web/server/api/endpoints/files.py b/web/server/api/endpoints/files.py index 7e4d365227..d843e65c69 100644 --- a/web/server/api/endpoints/files.py +++ b/web/server/api/endpoints/files.py @@ -60,7 +60,7 @@ async def write_file( replace_file(settings.project_path / path, settings.project_path / path_or_new_path) else: full_path = settings.project_path / path - config = context.config_for_path(Path(path_or_new_path)) if context else None + config, _ = context.config_for_path(Path(path_or_new_path)) if context else (None, None) if ( config and config.ui.format_on_save diff --git a/web/server/utils.py b/web/server/utils.py index cda6a7d9bb..e2238d1b0a 100644 --- a/web/server/utils.py +++ b/web/server/utils.py @@ -64,11 +64,10 @@ def validate_path(path: str, settings: Settings = Depends(get_settings)) -> str: if any( full_path.match(pattern) for pattern in ( - context.config_for_path(Path(path)).ignore_patterns if context else c.IGNORE_PATTERNS + context.config_for_path(Path(path))[0].ignore_patterns if context else c.IGNORE_PATTERNS ) ): raise HTTPException(status_code=HTTP_404_NOT_FOUND) - return path From c274b321ad57bfdc260a2f9a0af685fa7e9f382e Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 30 May 2025 06:22:51 +1200 Subject: [PATCH 0283/1056] Fix: expand the 'tag:' selector based on local models and not remote ones (#4573) --- sqlmesh/core/selector.py | 2 +- tests/core/test_selector.py | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/selector.py b/sqlmesh/core/selector.py index 4519afe4ac..ffc5af3e73 100644 --- a/sqlmesh/core/selector.py +++ b/sqlmesh/core/selector.py @@ -93,7 +93,7 @@ def select_models( } all_selected_models = self.expand_model_selections( - model_selections, models={**self._models, **env_models} + model_selections, models={**env_models, **self._models} ) dag: DAG[str] = DAG() diff --git a/tests/core/test_selector.py b/tests/core/test_selector.py index 7c4e771384..9f3bc9f698 100644 --- a/tests/core/test_selector.py +++ b/tests/core/test_selector.py @@ -657,6 +657,55 @@ def test_select_models_with_external_parent(mocker: MockerFixture): assert expanded_selections == {added_model.fqn} +def test_select_models_local_tags_take_precedence_over_remote( + mocker: MockerFixture, make_snapshot: t.Callable +) -> None: + existing_model = SqlModel( + name="db.existing", + query=d.parse_one("SELECT 1 AS a"), + ) + + existing_snapshot = make_snapshot(existing_model) + existing_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + env_name = "test_env" + + state_reader_mock = mocker.Mock() + state_reader_mock.get_environment.return_value = Environment( + name=env_name, + snapshots=[existing_snapshot.table_info], + start_at="2023-01-01", + end_at="2023-02-01", + plan_id="test_plan_id", + ) + state_reader_mock.get_snapshots.return_value = { + existing_snapshot.snapshot_id: existing_snapshot + } + + local_models: UniqueKeyDict[str, Model] = UniqueKeyDict("models") + local_new = SqlModel( + name="db.new", + tags=["a"], + query=d.parse_one("SELECT 1 as a"), + ) + local_existing = existing_model.copy(update={"tags": ["a"]}) # type: ignore + local_models[local_existing.fqn] = local_existing + local_models[local_new.fqn] = local_new + + selector = Selector(state_reader_mock, local_models) + + selected = selector.select_models(["tag:a"], env_name) + + # both should get selected because they both now have the 'a' tag locally, even though one exists in remote state without the 'a' tag + _assert_models_equal( + selected, + { + local_existing.fqn: local_existing, + local_new.fqn: local_new, + }, + ) + + def _assert_models_equal(actual: t.Dict[str, Model], expected: t.Dict[str, Model]) -> None: assert set(actual) == set(expected) for name, model in actual.items(): From dbff6732d38e96b26ffaaabe6ca562ec38e887d5 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 29 May 2025 20:25:20 +0200 Subject: [PATCH 0284/1056] chore(vscode): set workers to 1 in e2e testing (#4579) --- vscode/extension/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode/extension/playwright.config.ts b/vscode/extension/playwright.config.ts index b90a42e612..6820f2c8b1 100644 --- a/vscode/extension/playwright.config.ts +++ b/vscode/extension/playwright.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ testDir: 'tests', timeout: 60_000, retries: process.env.CI ? 1 : 0, - + workers: 1, projects: [ { name: 'electron-vscode', From 6921468ffaee79ec173fb2c4122bcd87afba1fcc Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 29 May 2025 22:33:03 +0300 Subject: [PATCH 0285/1056] Refactor: Move engine adapter instantiation after loading on-demand (#4578) --- sqlmesh/core/config/scheduler.py | 18 +-- sqlmesh/core/context.py | 104 +++++++++++------- .../integrations/github/cicd/controller.py | 2 +- tests/core/test_config.py | 4 +- tests/core/test_context.py | 15 +-- tests/core/test_integration.py | 4 +- tests/core/test_model.py | 6 +- tests/core/test_test.py | 8 +- web/server/api/endpoints/commands.py | 2 +- 9 files changed, 99 insertions(+), 64 deletions(-) diff --git a/sqlmesh/core/config/scheduler.py b/sqlmesh/core/config/scheduler.py index d4e66f513e..5cbfc6a71c 100644 --- a/sqlmesh/core/config/scheduler.py +++ b/sqlmesh/core/config/scheduler.py @@ -47,8 +47,8 @@ def create_state_sync(self, context: GenericContext) -> StateSync: """ @abc.abstractmethod - def get_default_catalog(self, context: GenericContext) -> t.Optional[str]: - """Returns the default catalog for the Scheduler. + def get_default_catalog_per_gateway(self, context: GenericContext) -> t.Dict[str, str]: + """Returns the default catalog for each gateway. Args: context: The SQLMesh Context. @@ -66,7 +66,7 @@ def state_sync_fingerprint(self, context: GenericContext) -> str: class _EngineAdapterStateSyncSchedulerConfig(SchedulerConfig): def create_state_sync(self, context: GenericContext) -> StateSync: state_connection = ( - context.config.get_state_connection(context.gateway) or context._connection_config + context.config.get_state_connection(context.gateway) or context.connection_config ) warehouse_connection = context.config.get_connection(context.gateway) @@ -110,7 +110,7 @@ def create_state_sync(self, context: GenericContext) -> StateSync: def state_sync_fingerprint(self, context: GenericContext) -> str: state_connection = ( - context.config.get_state_connection(context.gateway) or context._connection_config + context.config.get_state_connection(context.gateway) or context.connection_config ) return md5( [ @@ -132,12 +132,16 @@ def create_plan_evaluator(self, context: GenericContext) -> PlanEvaluator: state_sync=context.state_sync, snapshot_evaluator=context.snapshot_evaluator, create_scheduler=context.create_scheduler, - default_catalog=self.get_default_catalog(context), + default_catalog=context.default_catalog, console=context.console, ) - def get_default_catalog(self, context: GenericContext) -> t.Optional[str]: - return context.engine_adapter.default_catalog + def get_default_catalog_per_gateway(self, context: GenericContext) -> t.Dict[str, str]: + default_catalogs_per_gateway: t.Dict[str, str] = {} + for gateway, adapter in context.engine_adapters.items(): + if catalog := adapter.default_catalog: + default_catalogs_per_gateway[gateway] = catalog + return default_catalogs_per_gateway SCHEDULER_CONFIG_TO_TYPE = { diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 238e199558..ef5a207ba3 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -62,7 +62,9 @@ Config, load_configs, ) +from sqlmesh.core.config.connection import ConnectionConfig from sqlmesh.core.config.loader import C +from sqlmesh.core.config.root import RegexKeyDict from sqlmesh.core.console import get_console from sqlmesh.core.context_diff import ContextDiff from sqlmesh.core.dialect import ( @@ -91,6 +93,7 @@ from sqlmesh.core.plan.definition import UserProvidedFlags from sqlmesh.core.reference import ReferenceGraph from sqlmesh.core.scheduler import Scheduler, CompletionStatus +from sqlmesh.core.schema_diff import SchemaDiffer from sqlmesh.core.schema_loader import create_external_models_file from sqlmesh.core.selector import Selector from sqlmesh.core.snapshot import ( @@ -367,6 +370,9 @@ def __init__( self._excluded_requirements: t.Set[str] = set() self._default_catalog: t.Optional[str] = None self._default_catalog_per_gateway: t.Optional[t.Dict[str, str]] = None + self._engine_adapter: t.Optional[EngineAdapter] = None + self._connection_config: t.Optional[ConnectionConfig] = None + self._test_connection_config: t.Optional[ConnectionConfig] = None self._linters: t.Dict[str, Linter] = {} self._loaded: bool = False @@ -407,24 +413,15 @@ def __init__( for path, config in self.configs.items() ] - self._connection_config = self.config.get_connection(self.gateway) + self._concurrent_tasks = concurrent_tasks self._state_connection_config = ( - self.config.get_state_connection(self.gateway) or self._connection_config + self.config.get_state_connection(self.gateway) or self.connection_config ) - self.concurrent_tasks = concurrent_tasks or self._connection_config.concurrent_tasks - - self._engine_adapters: t.Dict[str, EngineAdapter] = { - self.selected_gateway: self._connection_config.create_engine_adapter() - } self._snapshot_evaluator: t.Optional[SnapshotEvaluator] = None self.console = get_console() - setattr(self.console, "dialect", self.engine_adapter.dialect) - - self._test_connection_config = self.config.get_test_connection( - self.gateway, self.default_catalog, default_catalog_dialect=self.engine_adapter.DIALECT - ) + setattr(self.console, "dialect", self.config.dialect) self._provided_state_sync: t.Optional[StateSync] = state_sync self._state_sync: t.Optional[StateSync] = None @@ -435,14 +432,6 @@ def __init__( self.users = list({user.username: user for user in self.users}.values()) self._register_notification_targets() - if ( - self.config.environment_catalog_mapping - and not self.engine_adapter.catalog_support.is_multi_catalog_supported - ): - raise SQLMeshError( - "Environment catalog mapping is only supported for engine adapters that support multiple catalogs" - ) - if load: self.load() @@ -453,7 +442,9 @@ def default_dialect(self) -> t.Optional[str]: @property def engine_adapter(self) -> EngineAdapter: """Returns the default engine adapter.""" - return self._engine_adapters[self.selected_gateway] + if self._engine_adapter is None: + self._engine_adapter = self.connection_config.create_engine_adapter() + return self._engine_adapter @property def snapshot_evaluator(self) -> SnapshotEvaluator: @@ -980,8 +971,8 @@ def requirements(self) -> t.Dict[str, str]: @property def default_catalog(self) -> t.Optional[str]: - if self._default_catalog is None: - self._default_catalog = self._scheduler.get_default_catalog(self) + if self._default_catalog is None and self.default_catalog_per_gateway: + self._default_catalog = self.default_catalog_per_gateway[self.selected_gateway] return self._default_catalog @python_api_analytics @@ -1538,7 +1529,7 @@ def plan_builder( allow_destructive_models=expanded_destructive_models, environment_ttl=environment_ttl, environment_suffix_target=self.config.environment_suffix_target, - environment_catalog_mapping=self.config.environment_catalog_mapping, + environment_catalog_mapping=self.environment_catalog_mapping, categorizer_config=categorizer_config or self.auto_categorize_changes, auto_categorization_enabled=not no_auto_categorization, effective_from=effective_from, @@ -1550,7 +1541,7 @@ def plan_builder( ), end_bounded=not run, ensure_finalized_snapshots=self.config.plan.use_finalized_state, - engine_schema_differ=self.engine_adapter.SCHEMA_DIFFER, + engine_schema_differ=SchemaDiffer(), # TODO: fix to properly handle it interval_end_per_model=max_interval_end_per_model, console=self.console, user_provided_flags=user_provided_flags, @@ -1639,7 +1630,7 @@ def diff(self, environment: t.Optional[str] = None, detailed: bool = False) -> b self.console.show_model_difference_summary( context_diff, EnvironmentNamingInfo.from_environment_catalog_mapping( - self.config.environment_catalog_mapping, + self.environment_catalog_mapping, name=environment, suffix_target=self.config.environment_suffix_target, normalize_name=context_diff.normalize_environment_name, @@ -1993,7 +1984,7 @@ def create_test( try: model_to_test = self.get_model(model, raise_if_missing=True) - test_adapter = self._test_connection_config.create_engine_adapter( + test_adapter = self.test_connection_config.create_engine_adapter( register_comments_override=False ) @@ -2039,7 +2030,7 @@ def test( preserve_fixtures=preserve_fixtures, stream=stream, default_catalog=self.default_catalog, - default_catalog_dialect=self.engine_adapter.DIALECT, + default_catalog_dialect=self.config.dialect or "", ) @python_api_analytics @@ -2478,7 +2469,7 @@ def _run_plan_tests( self.console.log_test_results( result, test_output, - self._test_connection_config._engine_adapter.DIALECT, + self.test_connection_config._engine_adapter.DIALECT, ) if not result.wasSuccessful(): raise PlanError( @@ -2499,7 +2490,7 @@ def _model_tables(self) -> t.Dict[str, str]: if snapshot.version else snapshot.qualified_view_name.for_environment( EnvironmentNamingInfo.from_environment_catalog_mapping( - self.config.environment_catalog_mapping, + self.environment_catalog_mapping, name=c.PROD, suffix_target=self.config.environment_suffix_target, ) @@ -2511,24 +2502,63 @@ def _model_tables(self) -> t.Dict[str, str]: @cached_property def engine_adapters(self) -> t.Dict[str, EngineAdapter]: """Returns all the engine adapters for the gateways defined in the configuration.""" + adapters: t.Dict[str, EngineAdapter] = {self.selected_gateway: self.engine_adapter} for gateway_name in self.config.gateways: if gateway_name != self.selected_gateway: connection = self.config.get_connection(gateway_name) adapter = connection.create_engine_adapter(concurrent_tasks=self.concurrent_tasks) - self._engine_adapters[gateway_name] = adapter - return self._engine_adapters + adapters[gateway_name] = adapter + return adapters @cached_property def default_catalog_per_gateway(self) -> t.Dict[str, str]: """Returns the default catalogs for each engine adapter.""" if self._default_catalog_per_gateway is None: - self._default_catalog_per_gateway = { - name: adapter.default_catalog - for name, adapter in self.engine_adapters.items() - if adapter.default_catalog - } + self._default_catalog_per_gateway = self._scheduler.get_default_catalog_per_gateway( + self + ) return self._default_catalog_per_gateway + @cached_property + def concurrent_tasks(self) -> int: + if self._concurrent_tasks is None: + self._concurrent_tasks = self.connection_config.concurrent_tasks + return self._concurrent_tasks + + @cached_property + def connection_config(self) -> ConnectionConfig: + if self._connection_config is None: + self._connection_config = self.config.get_connection(self.selected_gateway) + return self._connection_config + + @cached_property + def test_connection_config(self) -> ConnectionConfig: + if self._test_connection_config is None: + self._test_connection_config = self.config.get_test_connection( + self.gateway, + self.default_catalog, + default_catalog_dialect=self.engine_adapter.DIALECT, + ) + return self._test_connection_config + + @cached_property + def environment_catalog_mapping(self) -> RegexKeyDict: + engine_adapter = None + try: + engine_adapter = self.engine_adapter + except Exception: + pass + + if ( + self.config.environment_catalog_mapping + and engine_adapter + and not self.engine_adapter.catalog_support.is_multi_catalog_supported + ): + raise SQLMeshError( + "Environment catalog mapping is only supported for engine adapters that support multiple catalogs" + ) + return self.config.environment_catalog_mapping + def _get_engine_adapter(self, gateway: t.Optional[str] = None) -> EngineAdapter: if gateway: if adapter := self.engine_adapters.get(gateway): diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 267e1dff77..eadb34e3a6 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -726,7 +726,7 @@ def conclusion_handler( self._console.log_test_results( result, output, - self._context._test_connection_config._engine_adapter.DIALECT, + self._context.test_connection_config._engine_adapter.DIALECT, ) test_summary = self._console.consume_captured_output() test_title = "Tests Passed" if result.wasSuccessful() else "Tests Failed" diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 44c321ee66..44ef495737 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -742,7 +742,7 @@ def test_multi_gateway_config(tmp_path, mocker: MockerFixture): ctx = Context(paths=tmp_path, config=config) - assert isinstance(ctx._connection_config, RedshiftConnectionConfig) + assert isinstance(ctx.connection_config, RedshiftConnectionConfig) assert len(ctx.engine_adapters) == 3 assert isinstance(ctx.engine_adapters["athena"], AthenaEngineAdapter) assert isinstance(ctx.engine_adapters["redshift"], RedshiftEngineAdapter) @@ -782,7 +782,7 @@ def test_multi_gateway_single_threaded_config(tmp_path): ) ctx = Context(paths=tmp_path, config=config) - assert isinstance(ctx._connection_config, DuckDBConnectionConfig) + assert isinstance(ctx.connection_config, DuckDBConnectionConfig) assert len(ctx.engine_adapters) == 2 assert ctx.engine_adapter == ctx._get_engine_adapter("duckdb") assert isinstance(ctx.engine_adapters["athena"], AthenaEngineAdapter) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 3f732911d5..29d88ac41a 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -332,18 +332,18 @@ def test_evaluate_limit(): def test_gateway_specific_adapters(copy_to_temp_path, mocker): path = copy_to_temp_path("examples/sushi") ctx = Context(paths=path, config="isolated_systems_config", gateway="prod") - assert len(ctx._engine_adapters) == 3 - assert ctx.engine_adapter == ctx._engine_adapters["prod"] - assert ctx._get_engine_adapter("dev") == ctx._engine_adapters["dev"] + assert len(ctx.engine_adapters) == 3 + assert ctx.engine_adapter == ctx.engine_adapters["prod"] + assert ctx._get_engine_adapter("dev") == ctx.engine_adapters["dev"] ctx = Context(paths=path, config="isolated_systems_config") - assert len(ctx._engine_adapters) == 3 - assert ctx.engine_adapter == ctx._engine_adapters["dev"] + assert len(ctx.engine_adapters) == 3 + assert ctx.engine_adapter == ctx.engine_adapters["dev"] ctx = Context(paths=path, config="isolated_systems_config") assert len(ctx.engine_adapters) == 3 assert ctx.engine_adapter == ctx._get_engine_adapter() - assert ctx._get_engine_adapter("test") == ctx._engine_adapters["test"] + assert ctx._get_engine_adapter("test") == ctx.engine_adapters["test"] def test_multiple_gateways(tmp_path: Path): @@ -800,7 +800,8 @@ def test_janitor(sushi_context, mocker: MockerFixture) -> None: ), ] - sushi_context._engine_adapters = {sushi_context.config.default_gateway: adapter_mock} + sushi_context._engine_adapter = adapter_mock + sushi_context.engine_adapters = {sushi_context.config.default_gateway: adapter_mock} sushi_context._state_sync = state_sync_mock state_sync_mock.get_expired_snapshots.return_value = [] diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index a540eb21b3..f891b92f00 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -4872,13 +4872,11 @@ def test_multi(mocker): assert context.fetchdf("select * from after_1").to_dict()["repo_1"][0] == "repo_1" assert context.fetchdf("select * from after_2").to_dict()["repo_2"][0] == "repo_2" - adapter = context.engine_adapter context = Context( paths=["examples/multi/repo_1"], state_sync=context.state_sync, gateway="memory", ) - context._engine_adapters["memory"] = adapter model = context.get_model("bronze.a") assert model.project == "repo_1" @@ -4935,6 +4933,8 @@ def test_multi_virtual_layer(copy_to_temp_path): ) context = Context(paths=paths, config=config) + assert context.default_catalog_per_gateway == {"first": "db_1", "second": "db_2"} + assert len(context.engine_adapters) == 2 # For the model without gateway the default should be used and the gateway variable should overide the global assert ( diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 83a67253b0..4efa45f62b 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -4518,7 +4518,7 @@ def test_model_session_properties(sushi_context): name test_schema.test_model, session_properties ( 'query_label' = ( - 'some value', + 'some value', 'another value', 'yet another value', ) @@ -8350,7 +8350,7 @@ def test_gateway_specific_render(assert_exp_eq) -> None: default_gateway="main", ) context = Context(config=config) - assert context.engine_adapter == context._engine_adapters["main"] + assert context.engine_adapter == context.engine_adapters["main"] @model( name="dummy_model", @@ -8376,7 +8376,7 @@ def dummy_model_entry(evaluator: MacroEvaluator) -> exp.Select: """, ) assert isinstance(context._get_engine_adapter("duckdb"), DuckDBEngineAdapter) - assert len(context._engine_adapters) == 2 + assert len(context.engine_adapters) == 2 def test_model_on_virtual_update(make_snapshot: t.Callable): diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 80aead8bf4..f0e9250eb1 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -54,7 +54,7 @@ def _create_test( test_name=test_name, model=model, models=context._models, - engine_adapter=context._test_connection_config.create_engine_adapter( + engine_adapter=context.test_connection_config.create_engine_adapter( register_comments_override=False ), dialect=context.config.dialect, @@ -2128,7 +2128,7 @@ def test_test_with_gateway_specific_model(tmp_path: Path, mocker: MockerFixture) return_value=pd.DataFrame({"c": [5]}), ) - assert context.engine_adapter == context._engine_adapters["main"] + assert context.engine_adapter == context.engine_adapters["main"] with pytest.raises( SQLMeshError, match=r"Gateway 'wrong' not found in the available engine adapters." ): @@ -2136,8 +2136,8 @@ def test_test_with_gateway_specific_model(tmp_path: Path, mocker: MockerFixture) # Create test should use the gateway specific engine adapter context.create_test("sqlmesh_example.gw_model", input_queries=input_queries, overwrite=True) - assert context._get_engine_adapter("second") == context._engine_adapters["second"] - assert len(context._engine_adapters) == 2 + assert context._get_engine_adapter("second") == context.engine_adapters["second"] + assert len(context.engine_adapters) == 2 test = load_yaml(context.path / c.TESTS / "test_gw_model.yaml") diff --git a/web/server/api/endpoints/commands.py b/web/server/api/endpoints/commands.py index 5631a5d33d..d8f9490c9b 100644 --- a/web/server/api/endpoints/commands.py +++ b/web/server/api/endpoints/commands.py @@ -159,7 +159,7 @@ async def test( context.console.log_test_results( result, test_output.getvalue(), - context._test_connection_config._engine_adapter.DIALECT, + context.test_connection_config._engine_adapter.DIALECT, ) def _test_path(test: ModelTest) -> t.Optional[str]: From e61131a8547d57855e4477cd9ad8692bd0599849 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 29 May 2025 23:05:23 +0300 Subject: [PATCH 0286/1056] Chore: clean up bigquery info. schema integration test (#4580) --- .../integration/test_integration_bigquery.py | 58 +++---------------- 1 file changed, 9 insertions(+), 49 deletions(-) diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index db268c8b19..208d406b9a 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -204,11 +204,11 @@ def test_information_schema_view_external_model(ctx: TestContext, tmp_path: Path # This representation is produced by BigQuery's parser, so that the mapping schema # nesting depth is consistent with other table references in a project, which will # usually look like `project.dataset.table`. - information_schema_tables_view = ctx.table("INFORMATION_SCHEMA.TABLES") - assert len(information_schema_tables_view.parts) == 3 + information_schema_tables = ctx.table("INFORMATION_SCHEMA.TABLES") + assert len(information_schema_tables.parts) == 3 model_name = ctx.table("test") - dependency = f"`{'.'.join(part.name for part in information_schema_tables_view.parts)}`" + dependency = f"`{'.'.join(part.name for part in information_schema_tables.parts)}`" init_example_project(tmp_path, dialect="bigquery", template=ProjectTemplate.EMPTY) with open(tmp_path / "models" / "test.sql", "w", encoding="utf-8") as f: @@ -231,60 +231,20 @@ def _mutate_config(_: str, config: Config) -> None: sqlmesh.create_external_models() sqlmesh.load() - assert sqlmesh.get_model(information_schema_tables_view.sql()).columns_to_types == { + actual_columns_to_types = sqlmesh.get_model(information_schema_tables.sql()).columns_to_types + expected_columns_to_types = { "table_catalog": exp.DataType.build("TEXT"), "table_schema": exp.DataType.build("TEXT"), "table_name": exp.DataType.build("TEXT"), "table_type": exp.DataType.build("TEXT"), - "is_insertable_into": exp.DataType.build("TEXT"), - "is_typed": exp.DataType.build("TEXT"), - "creation_time": exp.DataType.build("TIMESTAMPTZ"), - "base_table_catalog": exp.DataType.build("TEXT"), - "base_table_schema": exp.DataType.build("TEXT"), - "base_table_name": exp.DataType.build("TEXT"), - "snapshot_time_ms": exp.DataType.build("TIMESTAMPTZ"), - "ddl": exp.DataType.build("TEXT"), - "default_collation_name": exp.DataType.build("TEXT"), - "upsert_stream_apply_watermark": exp.DataType.build("TIMESTAMPTZ"), - "replica_source_catalog": exp.DataType.build("TEXT"), - "replica_source_schema": exp.DataType.build("TEXT"), - "replica_source_name": exp.DataType.build("TEXT"), - "replication_status": exp.DataType.build("TEXT"), - "replication_error": exp.DataType.build("TEXT"), - "is_change_history_enabled": exp.DataType.build("TEXT"), - "sync_status": exp.DataType.build( - "STRUCT>" - ), } + assert actual_columns_to_types is not None + assert actual_columns_to_types.items() >= expected_columns_to_types.items() + rendered_query = sqlmesh.get_model(model_name.sql()).render_query() assert isinstance(rendered_query, exp.Query) - - assert rendered_query.sql("bigquery", pretty=True) == ( - "SELECT\n" - " `tables`.`table_catalog` AS `table_catalog`,\n" - " `tables`.`table_schema` AS `table_schema`,\n" - " `tables`.`table_name` AS `table_name`,\n" - " `tables`.`table_type` AS `table_type`,\n" - " `tables`.`is_insertable_into` AS `is_insertable_into`,\n" - " `tables`.`is_typed` AS `is_typed`,\n" - " `tables`.`creation_time` AS `creation_time`,\n" - " `tables`.`base_table_catalog` AS `base_table_catalog`,\n" - " `tables`.`base_table_schema` AS `base_table_schema`,\n" - " `tables`.`base_table_name` AS `base_table_name`,\n" - " `tables`.`snapshot_time_ms` AS `snapshot_time_ms`,\n" - " `tables`.`ddl` AS `ddl`,\n" - " `tables`.`default_collation_name` AS `default_collation_name`,\n" - " `tables`.`upsert_stream_apply_watermark` AS `upsert_stream_apply_watermark`,\n" - " `tables`.`replica_source_catalog` AS `replica_source_catalog`,\n" - " `tables`.`replica_source_schema` AS `replica_source_schema`,\n" - " `tables`.`replica_source_name` AS `replica_source_name`,\n" - " `tables`.`replication_status` AS `replication_status`,\n" - " `tables`.`replication_error` AS `replication_error`,\n" - " `tables`.`is_change_history_enabled` AS `is_change_history_enabled`,\n" - " `tables`.`sync_status` AS `sync_status`\n" - f"FROM {dependency} AS `tables`" - ) + assert not rendered_query.selects[0].is_star def test_compare_nested_values_in_table_diff(ctx: TestContext): From 5c60eaa613a3424d89d26d39f04afa0ab6e0f42a Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 29 May 2025 15:30:24 -0500 Subject: [PATCH 0287/1056] Docs: add database permissions to ClickHouse docs (#4581) --- docs/integrations/engines/clickhouse.md | 29 +++++++++++++++++++++++++ docs/reference/model_configuration.md | 1 + 2 files changed, 30 insertions(+) diff --git a/docs/integrations/engines/clickhouse.md b/docs/integrations/engines/clickhouse.md index ba565dccba..fb48ac860a 100644 --- a/docs/integrations/engines/clickhouse.md +++ b/docs/integrations/engines/clickhouse.md @@ -59,6 +59,35 @@ ClickHouse Cloud automates ClickHouse's cluster controls, which sometimes constr Aside from those constraints, ClickHouse Cloud mode is similar to single server mode - you run standard SQL commands/queries, and ClickHouse Cloud executes them. +## Permissions + +In the default SQLMesh configuration, users must have sufficient permissions to create new ClickHouse databases. + +Alternatively, you can configure specific databases where SQLMesh should create table and view objects. + +### Environment views + +Use the [`environment_suffix_target` key in your project configuration](../../guides/configuration.md#disable-environment-specific-schemas) to specify that environment views should be created within the model's database instead of in a new database: + +``` yaml +environment_suffix_target: table +``` + +### Physical tables + +Use the [`physical_schema_mapping` key in your project configuration](../../guides/configuration.md#physical-table-schemas) to specify the databases where physical tables should be created. + +The key accepts a dictionary of regular expressions that map model database names to the corresponding databases where physical tables should be created. + +SQLMesh will compare a model's database name to each regular expression and use the first match to determine which database a physical table should be created in. + +For example, this configuration places every model's physical table in the `model_physical_tables` database because the regular expression `.*` matches any database name: + +``` yaml +physical_schema_mapping: + '.*': model_physical_tables +``` + ## Cluster specification A ClickHouse cluster allows multiple networked ClickHouse servers to operate on the same data object. Every cluster must be named in the ClickHouse configuration files, and that name is passed to a table's DDL statements in the `ON CLUSTER` clause. diff --git a/docs/reference/model_configuration.md b/docs/reference/model_configuration.md index 8cef798bd9..526e868d29 100644 --- a/docs/reference/model_configuration.md +++ b/docs/reference/model_configuration.md @@ -41,6 +41,7 @@ Configuration options for SQLMesh model properties. Supported by all model kinds | `optimize_query` | Whether the model's query should be optimized. This attribute is `true` by default. Setting it to `false` causes SQLMesh to disable query canonicalization & simplification. This should be turned off only if the optimized query leads to errors such as surpassing text limit. | bool | N | | `ignored_rules` | A list of linter rule names (or "ALL") to be ignored/excluded for this model | str \| array[str] | N | | `formatting` | Whether the model will be formatted. All models are formatted by default. Setting this to `false` causes SQLMesh to ignore this model during `sqlmesh format`. | bool | N | + ### Model defaults The SQLMesh project-level configuration must contain the `model_defaults` key and must specify a value for its `dialect` key. Other values are set automatically unless explicitly overridden in the model definition. Learn more about project-level configuration in the [configuration guide](../guides/configuration.md). From 405e218038b8cba66545fdbcabbbc78d415a7933 Mon Sep 17 00:00:00 2001 From: Eduard Safin <57670814+dnbnero@users.noreply.github.com> Date: Fri, 30 May 2025 00:15:28 +0300 Subject: [PATCH 0288/1056] Docs: Fix typo in table (#4582) --- docs/reference/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 34dcd9d27b..596f3da157 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -243,7 +243,7 @@ For example, you might have a specific connection where your tests should run re | Option | Description | Type | Required | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | :---------: | :------: | | `default_connection` | The default connection to use if one is not specified in a gateway (Default: A DuckDB connection that creates an in-memory database) | connection | N | -| `default_test_connection` | The default connection to use when running tests if one is not specified in a gateway (Default: A DuckDB connection that creates an in-memory database | connection) | N | +| `default_test_connection` | The default connection to use when running tests if one is not specified in a gateway (Default: A DuckDB connection that creates an in-memory database) | connection | N | | `default_scheduler` | The default scheduler configuration to use if one is not specified in a gateway (Default: built-in scheduler) | scheduler | N | ## Debug mode From d11fcdd397095b17a1b180c1c4a01064210cfb62 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 30 May 2025 00:44:42 +0300 Subject: [PATCH 0289/1056] Fix: ensure partial works when a query is used to produce data (#4583) --- sqlmesh/core/test/definition.py | 5 ++++- tests/core/test_test.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index d1b978b91a..70e3fbf7e2 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -559,7 +559,10 @@ def _create_df( ) -> pd.DataFrame: query = values.get("query") if query: - return self._execute(self._add_missing_columns(query, columns)) + if not partial: + query = self._add_missing_columns(query, columns) + + return self._execute(query) rows = values["rows"] if columns: diff --git a/tests/core/test_test.py b/tests/core/test_test.py index f0e9250eb1..4715cf5989 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -1474,6 +1474,33 @@ def test_generate_input_data_using_sql(mocker: MockerFixture, tmp_path: Path) -> ).run() ) + _check_successful_or_raise( + _create_test( + body=load_yaml( + """ +test_example_full_model_partial: + model: sqlmesh_example.full_model + inputs: + sqlmesh_example.incremental_model: + rows: + - id: 1 + item_id: 1 + - id: 2 + item_id: 1 + - id: 3 + item_id: 2 + outputs: + query: + partial: true + query: "SELECT 2 AS num_orders UNION ALL SELECT 1 AS num_orders" + """ + ), + test_name="test_example_full_model_partial", + model=context.get_model("sqlmesh_example.full_model"), + context=context, + ).run() + ) + mocker.patch("sqlmesh.core.test.definition.random_id", return_value="jzngz56a") test = _create_test( body=load_yaml( From 7877b064a488f168924a316f702b788f5dc1bc2d Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Fri, 30 May 2025 00:35:10 -0500 Subject: [PATCH 0290/1056] Fix: dbt-core 1.9.5 freshness error (#4586) --- sqlmesh/dbt/manifest.py | 20 +++++++++++++++++-- tests/dbt/test_manifest.py | 7 +++++++ .../fixtures/dbt/sushi_test/models/schema.yml | 13 ++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 86f2f882ea..2b38f4362f 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -21,6 +21,7 @@ from dbt.config.profile import read_profile from dbt.config.renderer import DbtProjectYamlRenderer, ProfileRenderer from dbt.parser.manifest import ManifestLoader +from dbt.parser.sources import merge_freshness from dbt.tracking import do_not_track from sqlmesh.core import constants as c @@ -152,9 +153,24 @@ def _load_all(self) -> None: def _load_sources(self) -> None: for source in self._manifest.sources.values(): + # starting in dbt-core 1.9.5, freshness can be set in both source and source config + source_dict = source.to_dict() + source_dict.pop("freshness", None) + + source_config_dict = _config(source) + source_config_dict.pop("freshness", None) + + source_config_freshness = getattr(source.config, "freshness", None) + freshness = ( + merge_freshness(source.freshness, source_config_freshness) + if source_config_freshness + else source.freshness + ) + source_config = SourceConfig( - **_config(source), - **source.to_dict(), + **source_config_dict, + **source_dict, + freshness=freshness.to_dict() if freshness else None, ) self._sources_per_package[source.package_name][source_config.config_name] = ( source_config diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index a0fa5f6a52..f53d4b629d 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -9,6 +9,7 @@ from sqlmesh.dbt.manifest import ManifestHelper from sqlmesh.dbt.profile import Profile from sqlmesh.dbt.builtin import Api, _relation_info_to_relation +from sqlmesh.dbt.util import DBT_VERSION from sqlmesh.utils.jinja import MacroReference pytestmark = pytest.mark.dbt @@ -114,6 +115,12 @@ def test_manifest_helper(caplog): assert sources["streaming.order_items"].table_name == "order_items" assert sources["streaming.order_items"].schema_ == "raw" + assert sources["streaming.order_items"].freshness == { + "warn_after": {"count": 10 if DBT_VERSION < (1, 9, 5) else 12, "period": "hour"}, + "error_after": {"count": 11 if DBT_VERSION < (1, 9, 5) else 13, "period": "hour"}, + "filter": None, + } + @pytest.mark.xdist_group("dbt_manifest") def test_tests_referencing_disabled_models(): diff --git a/tests/fixtures/dbt/sushi_test/models/schema.yml b/tests/fixtures/dbt/sushi_test/models/schema.yml index ffa7f7c7c6..a64ce3c1fc 100644 --- a/tests/fixtures/dbt/sushi_test/models/schema.yml +++ b/tests/fixtures/dbt/sushi_test/models/schema.yml @@ -9,8 +9,14 @@ models: data_type: double - name: model_columns data_type: int + freshness: + warn_after: {count: 6, period: hour} + error_after: {count: 7, period: hour} config: dialect: postgres + freshness: + warn_after: {count: 8, period: hour} + error_after: {count: 9, period: hour} - name: waiters - name: waiter_as_customer_by_day - name: waiter_revenue_by_day @@ -30,6 +36,13 @@ sources: - name: items - name: orders - name: order_items + freshness: + warn_after: {count: 10, period: hour} + error_after: {count: 11, period: hour} + config: + freshness: + warn_after: {count: 12, period: hour} + error_after: {count: 13, period: hour} - name: parquet_file meta: From 784e2b752a24d4bb7ef483db00d25910d4baecf0 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 30 May 2025 12:33:16 +0100 Subject: [PATCH 0291/1056] fix(vscode): render models without descriptions (#4593) --- sqlmesh/lsp/custom.py | 2 +- vscode/extension/src/commands/renderModel.ts | 4 +-- vscode/extension/src/lsp/custom.ts | 2 +- vscode/extension/tests/render.spec.ts | 35 +++++++++++++++++++- vscode/extension/tests/utils.ts | 2 ++ 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py index 6021d225ab..b49361e43f 100644 --- a/sqlmesh/lsp/custom.py +++ b/sqlmesh/lsp/custom.py @@ -36,7 +36,7 @@ class RenderModelEntry(PydanticModel): name: str fqn: str - description: str + description: t.Optional[str] = None rendered_query: str diff --git a/vscode/extension/src/commands/renderModel.ts b/vscode/extension/src/commands/renderModel.ts index 4c888d16c3..ce1372379b 100644 --- a/vscode/extension/src/commands/renderModel.ts +++ b/vscode/extension/src/commands/renderModel.ts @@ -42,10 +42,10 @@ export function renderModel(lspClient?: LSPClient) { // If multiple models, let user choose let selectedModel: RenderModelEntry if (result.value.models.length > 1) { - const items = result.value.models.map((model: RenderModelEntry) => ({ + const items = result.value.models.map(model => ({ label: model.name, description: model.fqn, - detail: model.description, + detail: model.description ? model.description : undefined, model: model, })) diff --git a/vscode/extension/src/lsp/custom.ts b/vscode/extension/src/lsp/custom.ts index 7d16c5c7c8..d0d4a86bce 100644 --- a/vscode/extension/src/lsp/custom.ts +++ b/vscode/extension/src/lsp/custom.ts @@ -21,7 +21,7 @@ interface RenderModelResponse { export interface RenderModelEntry { name: string fqn: string - description: string + description: string | null | undefined rendered_query: string } diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts index 9b7f929271..e6ca61a938 100644 --- a/vscode/extension/tests/render.spec.ts +++ b/vscode/extension/tests/render.spec.ts @@ -35,4 +35,37 @@ test('Render works correctly', async () => { } finally { await fs.remove(tempDir); } - }); \ No newline at end of file + }); + +test('Render works correctly with model without a description', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); + await fs.copy(SUSHI_SOURCE_PATH, tempDir); + + try { + const { window, close } = await startVSCode(tempDir); + + // Wait for the models folder to be visible + await window.waitForSelector('text=models'); + + // Click on the models folder, excluding external_models + await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); + + // Open the latest_order model + await window.getByRole('treeitem', { name: 'latest_order.sql', exact: true }).locator('a').click(); + + await window.waitForSelector('text=custom_full_with_custom_kind'); + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Render the model + await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); + await window.keyboard.type('Render Model'); + await window.keyboard.press('Enter'); + + // Check if the model is rendered correctly + await expect(window.locator('text="orders"."id" AS "id",')).toBeVisible(); + + await close(); + } finally { + await fs.remove(tempDir); + } +}); \ No newline at end of file diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index e836d61ea7..10e368873f 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -40,6 +40,8 @@ export const startVSCode = async (workspaceDir: string): Promise<{ args, }); const window = await electronApp.firstWindow(); + await window.waitForLoadState('domcontentloaded'); + await window.waitForLoadState('networkidle'); return { window, close: async () => { await electronApp.close(); await fs.removeSync(userDataDir); From c49bed27c68b110fbbd0a6cf6bb0ff480dd06b1a Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 30 May 2025 13:04:47 +0100 Subject: [PATCH 0292/1056] feat(vscode): rendered window is non-editable (#4591) --- vscode/extension/src/commands/renderModel.ts | 48 +++++++++++----- vscode/extension/src/extension.ts | 13 ++++- .../src/providers/renderedModelProvider.ts | 57 +++++++++++++++++++ vscode/extension/tests/render.spec.ts | 45 ++++++++++++++- 4 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 vscode/extension/src/providers/renderedModelProvider.ts diff --git a/vscode/extension/src/commands/renderModel.ts b/vscode/extension/src/commands/renderModel.ts index ce1372379b..92bd528d94 100644 --- a/vscode/extension/src/commands/renderModel.ts +++ b/vscode/extension/src/commands/renderModel.ts @@ -2,8 +2,12 @@ import * as vscode from 'vscode' import { LSPClient } from '../lsp/lsp' import { isErr } from '@bus/result' import { RenderModelEntry } from '../lsp/custom' +import { RenderedModelProvider } from '../providers/renderedModelProvider' -export function renderModel(lspClient?: LSPClient) { +export function renderModel( + lspClient?: LSPClient, + renderedModelProvider?: RenderedModelProvider, +) { return async () => { // Get the current active editor const activeEditor = vscode.window.activeTextEditor @@ -62,29 +66,43 @@ export function renderModel(lspClient?: LSPClient) { selectedModel = result.value.models[0] } - // Create a new untitled document with the rendered SQL - const document = await vscode.workspace.openTextDocument({ - language: 'sql', - content: selectedModel.rendered_query, - }) + if (!renderedModelProvider) { + vscode.window.showErrorMessage('Rendered model provider not available') + return + } + + // Store the rendered content and get a virtual URI + const uri = renderedModelProvider.storeRenderedModel( + selectedModel.name, + selectedModel.rendered_query, + ) + + // Open the virtual document + const document = await vscode.workspace.openTextDocument(uri) // Determine the view column for side-by-side display - let viewColumn: vscode.ViewColumn - if (activeEditor) { - // Open beside the current editor - viewColumn = activeEditor.viewColumn - ? activeEditor.viewColumn + 1 - : vscode.ViewColumn.Two - } else { - // If no active editor, open in column two - viewColumn = vscode.ViewColumn.Two + // Find the rightmost column with an editor + let maxColumn = vscode.ViewColumn.One + for (const editor of vscode.window.visibleTextEditors) { + if (editor.viewColumn && editor.viewColumn > maxColumn) { + maxColumn = editor.viewColumn + } } + // Open in the next column after the rightmost editor + const viewColumn = maxColumn + 1 + // Open the document in the editor as a preview (preview: true is default) await vscode.window.showTextDocument(document, { viewColumn: viewColumn, preview: true, preserveFocus: false, }) + + // Execute "Keep Open" command to convert preview tab to permanent tab + await vscode.commands.executeCommand('workbench.action.keepEditor') + + // Explicitly set the language mode to SQL for syntax highlighting + await vscode.languages.setTextDocumentLanguage(document, 'sql') } } diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 77c0f175cd..3931da82d3 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -22,6 +22,7 @@ import { } from './utilities/errors' import { selector, completionProvider } from './completion/completion' import { LineagePanel } from './webviews/lineagePanel' +import { RenderedModelProvider } from './providers/renderedModelProvider' let lspClient: LSPClient | undefined @@ -65,10 +66,20 @@ export async function activate(context: vscode.ExtensionContext) { lspClient = new LSPClient() + // Create and register the rendered model provider + const renderedModelProvider = new RenderedModelProvider() + context.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider( + RenderedModelProvider.getScheme(), + renderedModelProvider, + ), + renderedModelProvider, + ) + context.subscriptions.push( vscode.commands.registerCommand( 'sqlmesh.renderModel', - renderModel(lspClient), + renderModel(lspClient, renderedModelProvider), ), ) diff --git a/vscode/extension/src/providers/renderedModelProvider.ts b/vscode/extension/src/providers/renderedModelProvider.ts new file mode 100644 index 0000000000..e9d6f6de7d --- /dev/null +++ b/vscode/extension/src/providers/renderedModelProvider.ts @@ -0,0 +1,57 @@ +import * as vscode from 'vscode' + +/** + * Content provider for read-only rendered SQL models + */ +export class RenderedModelProvider + implements vscode.TextDocumentContentProvider +{ + private static readonly scheme = 'sqlmesh-rendered' + + private renderedModels = new Map() + + // Event emitter for content changes + private _onDidChange = new vscode.EventEmitter() + readonly onDidChange = this._onDidChange.event + + /** + * Provide text content for a given URI + */ + provideTextDocumentContent(uri: vscode.Uri): string { + const key = uri.toString() + return this.renderedModels.get(key) || '' + } + + /** + * Store rendered model content and create a URI for it + */ + storeRenderedModel(modelName: string, content: string): vscode.Uri { + const fileName = `${modelName} (rendered)` + // Add a timestamp to make the URI unique for each render + const timestamp = Date.now() + // Use vscode.Uri.from for proper URI construction + const uri = vscode.Uri.from({ + scheme: RenderedModelProvider.scheme, + path: fileName, + fragment: timestamp.toString(), + }) + this.renderedModels.set(uri.toString(), content) + this._onDidChange.fire(uri) + return uri + } + + /** + * Get the URI scheme for rendered models + */ + static getScheme(): string { + return this.scheme + } + + /** + * Clean up old rendered models to prevent memory leaks + */ + dispose() { + this.renderedModels.clear() + this._onDidChange.dispose() + } +} diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts index e6ca61a938..dd54ebb916 100644 --- a/vscode/extension/tests/render.spec.ts +++ b/vscode/extension/tests/render.spec.ts @@ -30,6 +30,7 @@ test('Render works correctly', async () => { // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window await expect(window.locator('text="marketing"."customer_id" AS')).toBeVisible(); + await expect(window.locator('text=sushi.customers (rendered)')).toBeVisible(); await close(); } finally { @@ -63,9 +64,51 @@ test('Render works correctly with model without a description', async () => { // Check if the model is rendered correctly await expect(window.locator('text="orders"."id" AS "id",')).toBeVisible(); + await expect(window.locator('text=sushi.latest_order (rendered)')).toBeVisible(); await close(); } finally { await fs.remove(tempDir); } -}); \ No newline at end of file +}); + +test('Render works correctly with every rendered model opening a new tab', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); + await fs.copy(SUSHI_SOURCE_PATH, tempDir); + + try { + const { window, close } = await startVSCode(tempDir); + + // Wait for the models folder to be visible + await window.waitForSelector('text=models'); + await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); + await window.getByRole('treeitem', { name: 'latest_order.sql', exact: true }).locator('a').click(); + await window.waitForSelector('text=custom_full_with_custom_kind'); + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Render the model + await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); + await window.keyboard.type('Render Model'); + await window.keyboard.press('Enter'); + + // Check if the model is rendered correctly + await expect(window.locator('text=sushi.latest_order (rendered)')).toBeVisible(); + + // Open the customers model + await window.getByRole('treeitem', { name: 'customers.sql', exact: true }).locator('a').click(); + await window.waitForSelector('text=grain'); + + // Render the customers model + await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); + await window.keyboard.type('Render Model'); + await window.keyboard.press('Enter'); + + // Assert both tabs exist + await expect(window.locator('text=sushi.latest_order (rendered)')).toBeVisible(); + await expect(window.locator('text=sushi.customers (rendered)')).toBeVisible(); + + await close(); + } finally { + await fs.remove(tempDir); + } +}) \ No newline at end of file From fbde84a1fcb0083e475b53aafa7bfe11efcc8801 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 30 May 2025 15:17:47 +0300 Subject: [PATCH 0293/1056] Fix: Only check if source snapshot is forward_only in table diff (#4592) --- sqlmesh/core/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index ef5a207ba3..aa492c48be 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1718,7 +1718,7 @@ def table_diff( if target_snapshot and source_snapshot: if (source_snapshot.fingerprint != target_snapshot.fingerprint) and ( (source_snapshot.version != target_snapshot.version) - or (source_snapshot.is_forward_only or target_snapshot.is_forward_only) + or source_snapshot.is_forward_only ): # Compare the virtual layer instead of the physical layer because the virtual layer is guaranteed to point # to the correct/active snapshot for the model in the specified environment, taking into account things like dev previews From f00d6a8613dec56c9cd40eeb6ee6a1d0f8e93daa Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 30 May 2025 15:37:17 +0300 Subject: [PATCH 0294/1056] Fix: Use memory gateway for LSP test to avoid creating actual db (#4594) --- tests/lsp/test_reference_macro_multi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lsp/test_reference_macro_multi.py b/tests/lsp/test_reference_macro_multi.py index 3dfa588efb..1133609105 100644 --- a/tests/lsp/test_reference_macro_multi.py +++ b/tests/lsp/test_reference_macro_multi.py @@ -7,7 +7,7 @@ @pytest.mark.fast def test_macro_references_multirepo() -> None: - context = Context(paths=["examples/multi/repo_1", "examples/multi/repo_2"]) + context = Context(paths=["examples/multi/repo_1", "examples/multi/repo_2"], gateway="memory") lsp_context = LSPContext(context) d_path = next( From 077ad0c06410e9fd406f0d5808ad20f95099e736 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 30 May 2025 16:26:07 +0300 Subject: [PATCH 0295/1056] Fix: Compare columns by using the schema differ based on snapshots dialect (#4595) --- sqlmesh/core/context.py | 2 - sqlmesh/core/plan/builder.py | 11 +- sqlmesh/core/schema_diff.py | 22 ++++ tests/core/test_context.py | 1 - tests/core/test_plan.py | 188 ++++++++++-------------------- tests/core/test_plan_evaluator.py | 5 +- tests/core/test_schema_diff.py | 48 ++++++++ 7 files changed, 141 insertions(+), 136 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index aa492c48be..a8ceac00ee 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -93,7 +93,6 @@ from sqlmesh.core.plan.definition import UserProvidedFlags from sqlmesh.core.reference import ReferenceGraph from sqlmesh.core.scheduler import Scheduler, CompletionStatus -from sqlmesh.core.schema_diff import SchemaDiffer from sqlmesh.core.schema_loader import create_external_models_file from sqlmesh.core.selector import Selector from sqlmesh.core.snapshot import ( @@ -1541,7 +1540,6 @@ def plan_builder( ), end_bounded=not run, ensure_finalized_snapshots=self.config.plan.use_finalized_state, - engine_schema_differ=SchemaDiffer(), # TODO: fix to properly handle it interval_end_per_model=max_interval_end_per_model, console=self.console, user_provided_flags=user_provided_flags, diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 0e8bc481f4..2c0ff96a25 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -21,7 +21,11 @@ UserProvidedFlags, earliest_interval_start, ) -from sqlmesh.core.schema_diff import SchemaDiffer, has_drop_alteration, get_dropped_column_names +from sqlmesh.core.schema_diff import ( + get_schema_differ, + has_drop_alteration, + get_dropped_column_names, +) from sqlmesh.core.snapshot import ( DeployabilityIndex, Snapshot, @@ -78,14 +82,12 @@ class PlanBuilder: ensure_finalized_snapshots: Whether to compare against snapshots from the latest finalized environment state, or to use whatever snapshots are in the current environment state even if the environment is not finalized. - engine_schema_differ: Schema differ from the context engine adapter. interval_end_per_model: The mapping from model FQNs to target end dates. """ def __init__( self, context_diff: ContextDiff, - engine_schema_differ: SchemaDiffer, start: t.Optional[TimeLike] = None, end: t.Optional[TimeLike] = None, execution_time: t.Optional[TimeLike] = None, @@ -137,7 +139,6 @@ def __init__( self._backfill_models = backfill_models self._end = end or default_end self._apply = apply - self._engine_schema_differ = engine_schema_differ self._console = console or get_console() self._choices: t.Dict[SnapshotId, SnapshotChangeCategory] = {} self._user_provided_flags = user_provided_flags @@ -493,7 +494,7 @@ def _check_destructive_changes(self, directly_modified: t.Set[SnapshotId]) -> No if columns_to_types_all_known(old_columns_to_types) and columns_to_types_all_known( new_columns_to_types ): - schema_diff = self._engine_schema_differ.compare_columns( + schema_diff = get_schema_differ(snapshot.model.dialect).compare_columns( new.name, old_columns_to_types, new_columns_to_types, diff --git a/sqlmesh/core/schema_diff.py b/sqlmesh/core/schema_diff.py index d0f45f0d7c..70b4f72163 100644 --- a/sqlmesh/core/schema_diff.py +++ b/sqlmesh/core/schema_diff.py @@ -723,5 +723,27 @@ def get_dropped_column_names(alter_expressions: t.List[exp.Alter]) -> t.List[str return dropped_columns +def get_schema_differ(dialect: str) -> SchemaDiffer: + """ + Returns the appropriate SchemaDiffer for a given dialect without initializing the engine adapter. + + Args: + dialect: The dialect for which to get the schema differ. + + Returns: + The SchemaDiffer instance configured for the given dialect. + """ + from sqlmesh.core.engine_adapter import ( + DIALECT_TO_ENGINE_ADAPTER, + DIALECT_ALIASES, + EngineAdapter, + ) + + dialect = dialect.lower() + dialect = DIALECT_ALIASES.get(dialect, dialect) + engine_adapter_class = DIALECT_TO_ENGINE_ADAPTER.get(dialect, EngineAdapter) + return getattr(engine_adapter_class, "SCHEMA_DIFFER", SchemaDiffer()) + + def _get_name_and_type(struct: exp.ColumnDef) -> t.Tuple[exp.Identifier, exp.DataType]: return struct.this, struct.args["kind"] diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 29d88ac41a..1bd0919d9c 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -279,7 +279,6 @@ def test_diff(sushi_context: Context, mocker: MockerFixture): plan = PlanBuilder( context_diff=sushi_context._context_diff("prod"), - engine_schema_differ=sushi_context.engine_adapter.SCHEMA_DIFFER, ).build() # stringify used to trigger an unhashable exception due to diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 1c0830085d..69fb508219 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -12,7 +12,6 @@ from sqlmesh.core.context import Context from sqlmesh.core.context_diff import ContextDiff -from sqlmesh.core.engine_adapter import DuckDBEngineAdapter from sqlmesh.core.environment import EnvironmentNamingInfo, EnvironmentStatements from sqlmesh.core.model import ( ExternalModel, @@ -89,7 +88,7 @@ def test_forward_only_plan_sets_version(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - plan_builder = PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, forward_only=True) + plan_builder = PlanBuilder(context_diff, forward_only=True) plan_builder.build() assert snapshot_b.version == "test_version" @@ -151,9 +150,7 @@ def test_forward_only_dev(make_snapshot, mocker: MockerFixture): mocker.patch("sqlmesh.core.plan.builder.now").return_value = expected_end mocker.patch("sqlmesh.core.plan.definition.now").return_value = expected_end - plan = PlanBuilder( - context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, forward_only=True, is_dev=True - ).build() + plan = PlanBuilder(context_diff, forward_only=True, is_dev=True).build() assert plan.restatements == { updated_snapshot.snapshot_id: (to_timestamp(expected_start), expected_interval_end) @@ -213,9 +210,7 @@ def test_forward_only_metadata_change_dev(make_snapshot, mocker: MockerFixture): mocker.patch("sqlmesh.core.plan.builder.now").return_value = expected_end mocker.patch("sqlmesh.core.plan.definition.now").return_value = expected_end - plan = PlanBuilder( - context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, forward_only=True, is_dev=True - ).build() + plan = PlanBuilder(context_diff, forward_only=True, is_dev=True).build() assert not plan.restatements @@ -255,7 +250,7 @@ def test_forward_only_plan_added_models(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, forward_only=True).build() + PlanBuilder(context_diff, forward_only=True).build() assert snapshot_a.change_category == SnapshotChangeCategory.FORWARD_ONLY assert snapshot_b.change_category == SnapshotChangeCategory.BREAKING @@ -301,7 +296,7 @@ def test_forward_only_plan_categorizes_change_model_kind_as_breaking( gateway_managed_virtual_layer=False, ) - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, forward_only=True).build() + PlanBuilder(context_diff, forward_only=True).build() assert updated_snapshot.change_category == SnapshotChangeCategory.BREAKING @@ -349,15 +344,13 @@ def test_paused_forward_only_parent(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, forward_only=False).build() + PlanBuilder(context_diff, forward_only=False).build() assert snapshot_b.change_category == SnapshotChangeCategory.BREAKING def test_forward_only_plan_allow_destructive_models( make_snapshot, make_snapshot_on_destructive_change ): - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - # forward-only model, not forward-only plan snapshot_a_old, snapshot_a = make_snapshot_on_destructive_change() @@ -383,12 +376,12 @@ def test_forward_only_plan_allow_destructive_models( with pytest.raises( PlanError, match="Plan requires a destructive change to a forward-only model" ): - PlanBuilder(context_diff_a, schema_differ, forward_only=False).build() + PlanBuilder(context_diff_a, forward_only=False).build() logger = logging.getLogger("sqlmesh.core.plan.builder") with patch.object(logger, "warning") as mock_logger: assert PlanBuilder( - context_diff_a, schema_differ, forward_only=False, allow_destructive_models=['"a"'] + context_diff_a, forward_only=False, allow_destructive_models=['"a"'] ).build() assert mock_logger.call_count == 0 @@ -460,21 +453,18 @@ def test_forward_only_plan_allow_destructive_models( PlanError, match="""Plan requires a destructive change to a forward-only model.""", ): - PlanBuilder(context_diff_b, schema_differ, forward_only=True).build() + PlanBuilder(context_diff_b, forward_only=True).build() with pytest.raises( PlanError, match="""Plan requires a destructive change to a forward-only model.""", ): - PlanBuilder( - context_diff_b, schema_differ, forward_only=True, allow_destructive_models=['"b"'] - ).build() + PlanBuilder(context_diff_b, forward_only=True, allow_destructive_models=['"b"']).build() logger = logging.getLogger("sqlmesh.core.plan.builder") with patch.object(logger, "warning") as mock_logger: PlanBuilder( context_diff_b, - schema_differ, forward_only=True, allow_destructive_models=['"b"', '"c"'], ).build() @@ -484,8 +474,6 @@ def test_forward_only_plan_allow_destructive_models( def test_forward_only_model_on_destructive_change( make_snapshot, make_snapshot_on_destructive_change ): - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - # direct change to A snapshot_a_old, snapshot_a = make_snapshot_on_destructive_change() @@ -518,7 +506,7 @@ def test_forward_only_model_on_destructive_change( PlanError, match="""Plan requires a destructive change to a forward-only model.""", ): - PlanBuilder(context_diff_1, schema_differ).build() + PlanBuilder(context_diff_1).build() # allow A, indirect change to B snapshot_a_old2, snapshot_a2 = make_snapshot_on_destructive_change( @@ -574,7 +562,7 @@ def test_forward_only_model_on_destructive_change( gateway_managed_virtual_layer=False, ) - PlanBuilder(context_diff_2, schema_differ).build() + PlanBuilder(context_diff_2).build() # allow A and B, indirect change to C snapshot_a_old3, snapshot_a3 = make_snapshot_on_destructive_change( @@ -660,7 +648,7 @@ def test_forward_only_model_on_destructive_change( gateway_managed_virtual_layer=False, ) - PlanBuilder(context_diff_3, schema_differ).build() + PlanBuilder(context_diff_3).build() def test_forward_only_model_on_destructive_change_no_column_types( @@ -698,7 +686,7 @@ def test_forward_only_model_on_destructive_change_no_column_types( logger = logging.getLogger("sqlmesh.core.plan.builder") with patch.object(logger, "warning") as mock_logger: - PlanBuilder(context_diff_1, DuckDBEngineAdapter.SCHEMA_DIFFER).build() + PlanBuilder(context_diff_1).build() assert mock_logger.call_count == 0 @@ -930,9 +918,7 @@ def test_restate_symbolic_model(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - plan = PlanBuilder( - context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, restate_models=[snapshot_a.name] - ).build() + plan = PlanBuilder(context_diff, restate_models=[snapshot_a.name]).build() assert plan.restatements @@ -966,9 +952,7 @@ def test_restate_seed_model(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - plan = PlanBuilder( - context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, restate_models=[snapshot_a.name] - ).build() + plan = PlanBuilder(context_diff, restate_models=[snapshot_a.name]).build() assert not plan.restatements @@ -996,9 +980,7 @@ def test_restate_missing_model(make_snapshot, mocker: MockerFixture): PlanError, match=r"Cannot restate model 'missing'. Model does not exist.", ): - PlanBuilder( - context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, restate_models=["missing"] - ).build() + PlanBuilder(context_diff, restate_models=["missing"]).build() def test_new_snapshots_with_restatements(make_snapshot, mocker: MockerFixture): @@ -1027,7 +1009,7 @@ def test_new_snapshots_with_restatements(make_snapshot, mocker: MockerFixture): PlanError, match=r"Model changes and restatements can't be a part of the same plan.*", ): - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, restate_models=["a"]).build() + PlanBuilder(context_diff, restate_models=["a"]).build() def test_end_validation(make_snapshot, mocker: MockerFixture): @@ -1058,8 +1040,7 @@ def test_end_validation(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - dev_plan_builder = PlanBuilder(context_diff, schema_differ, end="2022-01-03", is_dev=True) + dev_plan_builder = PlanBuilder(context_diff, end="2022-01-03", is_dev=True) assert dev_plan_builder.build().end == "2022-01-03" dev_plan_builder.set_end("2022-01-04") assert dev_plan_builder.build().end == "2022-01-04" @@ -1069,12 +1050,12 @@ def test_end_validation(make_snapshot, mocker: MockerFixture): ) with pytest.raises(PlanError, match=start_end_not_allowed_message): - PlanBuilder(context_diff, schema_differ, end="2022-01-03").build() + PlanBuilder(context_diff, end="2022-01-03").build() with pytest.raises(PlanError, match=start_end_not_allowed_message): - PlanBuilder(context_diff, schema_differ, start="2022-01-03").build() + PlanBuilder(context_diff, start="2022-01-03").build() - prod_plan_builder = PlanBuilder(context_diff, schema_differ) + prod_plan_builder = PlanBuilder(context_diff) with pytest.raises(PlanError, match=start_end_not_allowed_message): prod_plan_builder.set_end("2022-01-03").build() @@ -1085,7 +1066,6 @@ def test_end_validation(make_snapshot, mocker: MockerFixture): context_diff.new_snapshots = {} restatement_prod_plan_builder = PlanBuilder( context_diff, - schema_differ, start="2022-01-01", end="2022-01-03", restate_models=['"a"'], @@ -1125,12 +1105,11 @@ def test_forward_only_revert_not_allowed(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER with pytest.raises( PlanError, match=r"Attempted to revert to an unrevertable version of model.*", ): - PlanBuilder(context_diff, schema_differ, forward_only=True).build() + PlanBuilder(context_diff, forward_only=True).build() # Make sure the plan can be created if a new snapshot version was enforced. new_version_snapshot = make_snapshot( @@ -1139,7 +1118,7 @@ def test_forward_only_revert_not_allowed(make_snapshot, mocker: MockerFixture): snapshot.categorize_as(SnapshotChangeCategory.BREAKING) context_diff.modified_snapshots = {snapshot.name: (new_version_snapshot, forward_only_snapshot)} context_diff.new_snapshots = {new_version_snapshot.snapshot_id: new_version_snapshot} - PlanBuilder(context_diff, schema_differ, forward_only=True).build() + PlanBuilder(context_diff, forward_only=True).build() def test_forward_only_plan_seed_models(make_snapshot, mocker: MockerFixture): @@ -1185,7 +1164,7 @@ def test_forward_only_plan_seed_models(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, forward_only=True).build() + PlanBuilder(context_diff, forward_only=True).build() assert snapshot_a_updated.version == snapshot_a_updated.fingerprint.to_version() assert snapshot_a_updated.change_category == SnapshotChangeCategory.NON_BREAKING @@ -1223,15 +1202,14 @@ def test_start_inference(make_snapshot, mocker: MockerFixture): snapshot_b.add_interval("2022-01-01", now()) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - plan = PlanBuilder(context_diff, schema_differ).build() + plan = PlanBuilder(context_diff).build() assert len(plan.missing_intervals) == 1 assert plan.missing_intervals[0].snapshot_id == snapshot_a.snapshot_id assert plan.start == to_timestamp("2022-01-01") # Test inference from existing intervals context_diff.snapshots = {snapshot_b.snapshot_id: snapshot_b} - plan = PlanBuilder(context_diff, schema_differ).build() + plan = PlanBuilder(context_diff).build() assert not plan.missing_intervals assert plan.start == to_datetime("2022-01-01") @@ -1261,7 +1239,7 @@ def test_auto_categorization(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER).build() + PlanBuilder(context_diff).build() assert updated_snapshot.version == updated_snapshot.fingerprint.to_version() assert updated_snapshot.change_category == SnapshotChangeCategory.BREAKING @@ -1309,7 +1287,7 @@ def test_auto_categorization_missing_schema_downstream(make_snapshot, mocker: Mo gateway_managed_virtual_layer=False, ) - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER).build() + PlanBuilder(context_diff).build() assert updated_snapshot.version assert updated_snapshot.change_category == SnapshotChangeCategory.BREAKING @@ -1349,7 +1327,7 @@ def test_broken_references(make_snapshot, mocker: MockerFixture): PlanError, match=r"""Removed '"a"' are referenced in '"b"'.*""", ): - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER).build() + PlanBuilder(context_diff).build() def test_broken_references_external_model(make_snapshot, mocker: MockerFixture): @@ -1383,7 +1361,7 @@ def test_broken_references_external_model(make_snapshot, mocker: MockerFixture): assert not snapshot_b.parents # Shouldn't raise - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER).build() + PlanBuilder(context_diff).build() def test_effective_from(make_snapshot, mocker: MockerFixture): @@ -1421,18 +1399,16 @@ def test_effective_from(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER with pytest.raises( PlanError, match="Effective date can only be set for a forward-only plan.", ): - PlanBuilder(context_diff, schema_differ).set_effective_from("2023-02-01").build() + PlanBuilder(context_diff).set_effective_from("2023-02-01").build() # The snapshot gets categorized as breaking in previous step so we want to reset that back to None updated_snapshot.change_category = None plan_builder = PlanBuilder( context_diff, - schema_differ, forward_only=True, start="2023-01-01", end="2023-03-01", @@ -1506,10 +1482,8 @@ def test_effective_from_non_evaluatble_model(make_snapshot, mocker: MockerFixtur gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER plan_builder = PlanBuilder( context_diff, - schema_differ, forward_only=True, start="2023-01-01", end="2023-03-01", @@ -1544,17 +1518,14 @@ def test_new_environment_no_changes(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER with pytest.raises( PlanError, match="Creating a new environment requires a change, but project files match.*" ): - PlanBuilder(context_diff, schema_differ, is_dev=True).build() + PlanBuilder(context_diff, is_dev=True).build() + assert PlanBuilder(context_diff).build().environment.promoted_snapshot_ids is None assert ( - PlanBuilder(context_diff, schema_differ).build().environment.promoted_snapshot_ids is None - ) - assert ( - PlanBuilder(context_diff, schema_differ, is_dev=True, include_unmodified=True) + PlanBuilder(context_diff, is_dev=True, include_unmodified=True) .build() .environment.promoted_snapshot_ids is None @@ -1592,10 +1563,10 @@ def test_new_environment_with_changes(make_snapshot, mocker: MockerFixture): ) # Modified the existing model. - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - assert PlanBuilder( - context_diff, schema_differ, is_dev=True - ).build().environment.promoted_snapshot_ids == [updated_snapshot_a.snapshot_id] + + assert PlanBuilder(context_diff, is_dev=True).build().environment.promoted_snapshot_ids == [ + updated_snapshot_a.snapshot_id + ] # Updating the existing environment with a previously promoted snapshot. context_diff.previously_promoted_snapshot_ids = { @@ -1604,10 +1575,7 @@ def test_new_environment_with_changes(make_snapshot, mocker: MockerFixture): } context_diff.is_new_environment = False assert set( - PlanBuilder(context_diff, schema_differ, is_dev=True) - .build() - .environment.promoted_snapshot_ids - or [] + PlanBuilder(context_diff, is_dev=True).build().environment.promoted_snapshot_ids or [] ) == { updated_snapshot_a.snapshot_id, snapshot_b.snapshot_id, @@ -1626,10 +1594,7 @@ def test_new_environment_with_changes(make_snapshot, mocker: MockerFixture): context_diff.new_snapshots = {snapshot_c.snapshot_id: snapshot_c} assert set( - PlanBuilder(context_diff, schema_differ, is_dev=True) - .build() - .environment.promoted_snapshot_ids - or [] + PlanBuilder(context_diff, is_dev=True).build().environment.promoted_snapshot_ids or [] ) == { updated_snapshot_a.snapshot_id, snapshot_b.snapshot_id, @@ -1676,18 +1641,17 @@ def test_forward_only_models(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - PlanBuilder(context_diff, schema_differ, is_dev=True).build() + PlanBuilder(context_diff, is_dev=True).build() assert updated_snapshot.change_category == SnapshotChangeCategory.FORWARD_ONLY updated_snapshot.change_category = None updated_snapshot.version = None - PlanBuilder(context_diff, schema_differ, is_dev=True, forward_only=True).build() + PlanBuilder(context_diff, is_dev=True, forward_only=True).build() assert updated_snapshot.change_category == SnapshotChangeCategory.FORWARD_ONLY updated_snapshot.change_category = None updated_snapshot.version = None - PlanBuilder(context_diff, schema_differ, forward_only=True).build() + PlanBuilder(context_diff, forward_only=True).build() assert updated_snapshot.change_category == SnapshotChangeCategory.FORWARD_ONLY @@ -1722,7 +1686,7 @@ def test_forward_only_models_model_kind_changed(make_snapshot, mocker: MockerFix gateway_managed_virtual_layer=False, ) - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, is_dev=True).build() + PlanBuilder(context_diff, is_dev=True).build() assert updated_snapshot.change_category == SnapshotChangeCategory.BREAKING @@ -1801,7 +1765,7 @@ def test_indirectly_modified_forward_only_model(make_snapshot, mocker: MockerFix gateway_managed_virtual_layer=False, ) - plan = PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, is_dev=True).build() + plan = PlanBuilder(context_diff, is_dev=True).build() assert plan.indirectly_modified == { updated_snapshot_a.snapshot_id: { updated_snapshot_b.snapshot_id, @@ -1857,7 +1821,7 @@ def test_added_model_with_forward_only_parent(make_snapshot, mocker: MockerFixtu gateway_managed_virtual_layer=False, ) - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, is_dev=True).build() + PlanBuilder(context_diff, is_dev=True).build() assert snapshot_b.change_category == SnapshotChangeCategory.BREAKING @@ -1897,7 +1861,7 @@ def test_added_forward_only_model(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER).build() + PlanBuilder(context_diff).build() assert snapshot_a.change_category == SnapshotChangeCategory.BREAKING assert snapshot_b.change_category == SnapshotChangeCategory.BREAKING @@ -1931,19 +1895,16 @@ def test_disable_restatement(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - plan = PlanBuilder(context_diff, schema_differ, restate_models=['"a"']).build() + plan = PlanBuilder(context_diff, restate_models=['"a"']).build() assert not plan.restatements # Effective from doesn't apply to snapshots for which restatements are disabled. - plan = PlanBuilder( - context_diff, schema_differ, forward_only=True, effective_from="2023-01-01" - ).build() + plan = PlanBuilder(context_diff, forward_only=True, effective_from="2023-01-01").build() assert plan.effective_from == "2023-01-01" assert snapshot.effective_from is None # Restatements should still be supported when in dev. - plan = PlanBuilder(context_diff, schema_differ, is_dev=True, restate_models=['"a"']).build() + plan = PlanBuilder(context_diff, is_dev=True, restate_models=['"a"']).build() assert plan.restatements == { snapshot.snapshot_id: (to_timestamp(plan.start), to_timestamp(to_date("tomorrow"))) } @@ -1951,7 +1912,7 @@ def test_disable_restatement(make_snapshot, mocker: MockerFixture): # We don't want to restate a disable_restatement model if it is unpaused since that would be mean we are violating # the model kind property snapshot.unpaused_ts = 9999999999 - plan = PlanBuilder(context_diff, schema_differ, is_dev=True, restate_models=['"a"']).build() + plan = PlanBuilder(context_diff, is_dev=True, restate_models=['"a"']).build() assert plan.restatements == {} @@ -2000,7 +1961,7 @@ def test_revert_to_previous_value(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - plan_builder = PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER) + plan_builder = PlanBuilder(context_diff) plan_builder.set_choice(snapshot_a, SnapshotChangeCategory.BREAKING) plan_builder.build() # Make sure it does not get assigned INDIRECT_BREAKING @@ -2216,7 +2177,6 @@ def test_add_restatements( plan = PlanBuilder( context_diff, - DuckDBEngineAdapter.SCHEMA_DIFFER, start=to_date(start), end=to_date(end), execution_time=to_date(execution_time), @@ -2293,9 +2253,8 @@ def test_dev_plan_depends_past(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER dev_plan_start_aligned = PlanBuilder( - context_diff, schema_differ, start="2023-01-01", end="2023-01-10", is_dev=True + context_diff, start="2023-01-01", end="2023-01-10", is_dev=True ).build() assert len(dev_plan_start_aligned.new_snapshots) == 3 assert sorted([x.name for x in dev_plan_start_aligned.new_snapshots]) == [ @@ -2311,7 +2270,7 @@ def test_dev_plan_depends_past(make_snapshot, mocker: MockerFixture): assert dev_plan_start_aligned.indirectly_modified == {} dev_plan_start_ahead_of_model = PlanBuilder( - context_diff, schema_differ, start="2023-01-02", end="2023-01-10", is_dev=True + context_diff, start="2023-01-02", end="2023-01-10", is_dev=True ).build() assert len(dev_plan_start_ahead_of_model.new_snapshots) == 3 assert not dev_plan_start_ahead_of_model.deployability_index.is_deployable(snapshot) @@ -2398,10 +2357,8 @@ def test_dev_plan_depends_past_non_deployable(make_snapshot, mocker: MockerFixtu gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - def new_builder(start, end): - builder = PlanBuilder(context_diff, schema_differ, start=start, end=end, is_dev=True) + builder = PlanBuilder(context_diff, start=start, end=end, is_dev=True) builder.set_choice(updated_snapshot, SnapshotChangeCategory.FORWARD_ONLY) builder.set_choice(snapshot_child, SnapshotChangeCategory.BREAKING) builder.set_choice(unrelated_snapshot, SnapshotChangeCategory.BREAKING) @@ -2466,9 +2423,7 @@ def test_models_selected_for_backfill(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - - plan = PlanBuilder(context_diff, schema_differ).build() + plan = PlanBuilder(context_diff).build() assert plan.is_selected_for_backfill('"a"') assert plan.is_selected_for_backfill('"b"') assert plan.models_to_backfill is None @@ -2477,14 +2432,14 @@ def test_models_selected_for_backfill(make_snapshot, mocker: MockerFixture): snapshot_b.snapshot_id, } - plan = PlanBuilder(context_diff, schema_differ, is_dev=True, backfill_models={'"a"'}).build() + plan = PlanBuilder(context_diff, is_dev=True, backfill_models={'"a"'}).build() assert plan.is_selected_for_backfill('"a"') assert not plan.is_selected_for_backfill('"b"') assert plan.models_to_backfill == {'"a"'} assert {i.snapshot_id for i in plan.missing_intervals} == {snapshot_a.snapshot_id} assert plan.environment.promoted_snapshot_ids == [snapshot_a.snapshot_id] - plan = PlanBuilder(context_diff, schema_differ, is_dev=True, backfill_models={'"b"'}).build() + plan = PlanBuilder(context_diff, is_dev=True, backfill_models={'"b"'}).build() assert plan.is_selected_for_backfill('"a"') assert plan.is_selected_for_backfill('"b"') assert plan.models_to_backfill == {'"a"', '"b"'} @@ -2520,9 +2475,7 @@ def test_categorized_uncategorized(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - plan_builder = PlanBuilder( - context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, auto_categorization_enabled=False - ) + plan_builder = PlanBuilder(context_diff, auto_categorization_enabled=False) plan = plan_builder.build() assert plan.uncategorized == [new_snapshot] @@ -2577,8 +2530,7 @@ def test_environment_previous_finalized_snapshots(make_snapshot, mocker: MockerF gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - plan = PlanBuilder(context_diff, schema_differ).build() + plan = PlanBuilder(context_diff).build() assert set(plan.environment.previous_finalized_snapshots or []) == { snapshot_c.table_info, snapshot_d.table_info, @@ -2586,7 +2538,7 @@ def test_environment_previous_finalized_snapshots(make_snapshot, mocker: MockerF context_diff.is_unfinalized_environment = False - plan = PlanBuilder(context_diff, schema_differ).build() + plan = PlanBuilder(context_diff).build() assert set(plan.environment.previous_finalized_snapshots or []) == { snapshot_a.table_info, snapshot_c.table_info, @@ -2633,7 +2585,7 @@ def test_metadata_change(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - plan = PlanBuilder(context_diff, DuckDBEngineAdapter.SCHEMA_DIFFER, is_dev=True).build() + plan = PlanBuilder(context_diff, is_dev=True).build() assert ( plan.snapshots[updated_snapshot.snapshot_id].change_category @@ -2679,7 +2631,6 @@ def test_plan_start_when_preview_enabled(make_snapshot, mocker: MockerFixture): plan_builder = PlanBuilder( context_diff, - DuckDBEngineAdapter.SCHEMA_DIFFER, default_start=default_start_for_preview, is_dev=True, enable_preview=True, @@ -2693,7 +2644,6 @@ def test_plan_start_when_preview_enabled(make_snapshot, mocker: MockerFixture): plan_builder = PlanBuilder( context_diff, - DuckDBEngineAdapter.SCHEMA_DIFFER, default_start=default_start_for_preview, is_dev=True, enable_preview=True, @@ -2728,7 +2678,6 @@ def test_interval_end_per_model(make_snapshot): plan_builder = PlanBuilder( context_diff, - DuckDBEngineAdapter.SCHEMA_DIFFER, interval_end_per_model={snapshot.name: to_timestamp("2023-01-09")}, ) assert plan_builder.build().interval_end_per_model == { @@ -2738,7 +2687,6 @@ def test_interval_end_per_model(make_snapshot): # User-provided end should take precedence. plan_builder = PlanBuilder( context_diff, - DuckDBEngineAdapter.SCHEMA_DIFFER, interval_end_per_model={snapshot.name: to_timestamp("2023-01-09")}, end="2023-01-10", is_dev=True, @@ -2805,7 +2753,6 @@ def test_unaligned_start_model_with_forward_only_preview(make_snapshot): plan_builder = PlanBuilder( context_diff, - DuckDBEngineAdapter.SCHEMA_DIFFER, enable_preview=True, is_dev=True, ) @@ -2859,7 +2806,6 @@ def test_restate_production_model_in_dev(make_snapshot, mocker: MockerFixture): plan = PlanBuilder( context_diff, - DuckDBEngineAdapter.SCHEMA_DIFFER, is_dev=True, restate_models={snapshot.name, prod_snapshot.name}, console=mock_console, @@ -2958,11 +2904,8 @@ def test_restate_daily_to_monthly(make_snapshot, mocker: MockerFixture): gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - plan = PlanBuilder( context_diff, - schema_differ, restate_models=[snapshot_a.name, snapshot_e.name], start="2025-02-15", end="2025-02-20", @@ -3096,8 +3039,7 @@ def test_set_choice_for_forward_only_model(make_snapshot): gateway_managed_virtual_layer=False, ) - schema_differ = DuckDBEngineAdapter.SCHEMA_DIFFER - plan_builder = PlanBuilder(context_diff, schema_differ, is_dev=True) + plan_builder = PlanBuilder(context_diff, is_dev=True) with pytest.raises(PlanError, match='Forward-only model "a" cannot be categorized manually.'): plan_builder.set_choice(updated_snapshot, SnapshotChangeCategory.BREAKING) @@ -3144,13 +3086,11 @@ def test_user_provided_flags(sushi_context: Context): ) plan_builder = PlanBuilder( context_diff, - DuckDBEngineAdapter.SCHEMA_DIFFER, forward_only=True, user_provided_flags={"forward_only": True}, ).build() assert plan_builder.user_provided_flags == {"forward_only": True} plan_builder = PlanBuilder( context_diff, - DuckDBEngineAdapter.SCHEMA_DIFFER, ).build() assert plan_builder.user_provided_flags == None diff --git a/tests/core/test_plan_evaluator.py b/tests/core/test_plan_evaluator.py index 8e886c227d..ae113e87e3 100644 --- a/tests/core/test_plan_evaluator.py +++ b/tests/core/test_plan_evaluator.py @@ -22,7 +22,6 @@ def sushi_plan(sushi_context: Context, mocker: MockerFixture) -> Plan: return PlanBuilder( sushi_context._context_diff("dev"), - sushi_context.engine_adapter.SCHEMA_DIFFER, is_dev=True, include_unmodified=True, ).build() @@ -57,9 +56,7 @@ def test_builtin_evaluator_push(sushi_context: Context, make_snapshot): new_model_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) new_view_model_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - plan = PlanBuilder( - sushi_context._context_diff("prod"), sushi_context.engine_adapter.SCHEMA_DIFFER - ).build() + plan = PlanBuilder(sushi_context._context_diff("prod")).build() evaluator = BuiltInPlanEvaluator( sushi_context.state_sync, diff --git a/tests/core/test_schema_diff.py b/tests/core/test_schema_diff.py index 85f4d424a3..1e57cab57c 100644 --- a/tests/core/test_schema_diff.py +++ b/tests/core/test_schema_diff.py @@ -9,6 +9,7 @@ TableAlterColumn, TableAlterColumnPosition, TableAlterOperation, + get_schema_differ, ) @@ -1328,3 +1329,50 @@ def test_schema_diff_alter_op_column(): super_nested.column("element").sql() == '"nested_1".element.nested_2.nested_3.element.nested_4.nested_5."nested_6".nested_7.nested_8.element."col_a"' ) + + +def test_get_schema_differ(): + # Test that known dialects return SchemaDiffer instances + for dialect in ["bigquery", "snowflake", "postgres", "databricks", "spark", "duckdb"]: + schema_differ = get_schema_differ(dialect) + assert isinstance(schema_differ, SchemaDiffer) + + # Test specific configurations + # Databricks should support positional add and nested operations + databricks_differ = get_schema_differ("databricks") + assert databricks_differ.support_positional_add is True + assert databricks_differ.support_nested_operations is True + assert databricks_differ.support_nested_drop is True + + # BigQuery should have specific compatible types configured + bigquery_differ = get_schema_differ("bigquery") + assert len(bigquery_differ.compatible_types) > 0 + assert bigquery_differ.support_coercing_compatible_types is True + + # Snowflake should have parameterized type defaults + snowflake_differ = get_schema_differ("snowflake") + assert len(snowflake_differ.parameterized_type_defaults) > 0 + + # Postgres should support drop cascade + postgres_differ = get_schema_differ("postgres") + assert postgres_differ.drop_cascade is True + assert len(postgres_differ.types_with_unlimited_length) > 0 + + # Test dialect aliases work correctly + schema_differ_pg = get_schema_differ("postgresql") + schema_differ_postgres = get_schema_differ("postgres") + assert schema_differ_pg.drop_cascade == schema_differ_postgres.drop_cascade + + # Test unknown dialect returns default SchemaDiffer + schema_differ_unknown = get_schema_differ("unknown_dialect") + assert isinstance(schema_differ_unknown, SchemaDiffer) + assert schema_differ_unknown.support_positional_add is False + assert schema_differ_unknown.support_nested_operations is False + + # Test case insensitivity + schema_differ_upper = get_schema_differ("BIGQUERY") + schema_differ_lower = get_schema_differ("bigquery") + assert ( + schema_differ_upper.support_coercing_compatible_types + == schema_differ_lower.support_coercing_compatible_types + ) From 989ecb2b5a04739970c2a13ad17088ec3236e736 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 30 May 2025 17:15:51 +0300 Subject: [PATCH 0296/1056] Chore: Fix flaky dbt test by grouping it with xdist (#4596) --- tests/dbt/test_transformation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index bc970dfaaf..c5204aeb55 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1556,9 +1556,9 @@ def test_grain(): assert model.to_sqlmesh(context).grains == [exp.to_column("id_a")] -def test_on_run_start_end(copy_to_temp_path): - project_root = "tests/fixtures/dbt/sushi_test" - sushi_context = Context(paths=copy_to_temp_path(project_root)) +@pytest.mark.xdist_group("dbt_manifest") +def test_on_run_start_end(): + sushi_context = Context(paths=["tests/fixtures/dbt/sushi_test"]) assert len(sushi_context._environment_statements) == 2 # Root project's on run start / on run end should be first by checking the macros From 9326a3c9e74c85509e6d5aa49866d50424f521bc Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 30 May 2025 18:32:54 +0300 Subject: [PATCH 0297/1056] Feat: make automatic python dependency inference opt-out (#4575) --- docs/reference/configuration.md | 1 + sqlmesh/core/config/root.py | 2 ++ sqlmesh/core/context.py | 4 +-- sqlmesh/core/context_diff.py | 56 ++++++++++++++++++++++----------- tests/core/test_context.py | 13 ++++++++ tests/core/test_integration.py | 2 +- 6 files changed, 57 insertions(+), 21 deletions(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 596f3da157..79484b50cc 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -36,6 +36,7 @@ Configuration options for SQLMesh environment creation and promotion. | `physical_schema_mapping` | A mapping from regular expressions to names of schemas in which physical tables for the corresponding models [will be placed](../guides/configuration.md#physical-table-schemas). (Default physical schema name: `sqlmesh__[model schema]`) | dict[string, string] | N | | `environment_suffix_target` | Whether SQLMesh views should append their environment name to the `schema` or `table` - [additional details](../guides/configuration.md#view-schema-override). (Default: `schema`) | string | N | | `gateway_managed_virtual_layer` | Whether SQLMesh views of the virtual layer will be created by the default gateway or model specified gateways - [additional details](../guides/multi_engine.md#gateway-managed-virtual-layer). (Default: False) | boolean | N | +| `infer_python_dependencies` | Whether SQLMesh will statically analyze Python code to automatically infer Python package requirements. (Default: True) | boolean | N | | `environment_catalog_mapping` | A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment. | dict[string, string] | N | | `log_limit` | The default number of logs to keep (Default: `20`) | int | N | diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index 85e4ca77c4..0f132680a8 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -107,6 +107,7 @@ class Config(BaseConfig): physical_schema_mapping: A mapping from regular expressions to names of schemas in which physical tables for corresponding models will be placed. environment_suffix_target: Indicates whether to append the environment name to the schema or table name. gateway_managed_virtual_layer: Whether the models' views in the virtual layer are created by the model-specific gateway rather than the default gateway. + infer_python_dependencies: Whether to statically analyze Python code to automatically infer Python package requirements. environment_catalog_mapping: A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment. default_target_environment: The name of the environment that will be the default target for the `sqlmesh plan` and `sqlmesh run` commands. log_limit: The default number of logs to keep. @@ -146,6 +147,7 @@ class Config(BaseConfig): default=EnvironmentSuffixTarget.default ) gateway_managed_virtual_layer: bool = False + infer_python_dependencies: bool = True environment_catalog_mapping: RegexKeyDict = {} default_target_environment: str = c.PROD log_limit: int = c.DEFAULT_LOG_LIMIT diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index a8ceac00ee..9df16378d3 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -388,7 +388,6 @@ def __init__( self.pinned_environments = Environment.sanitize_names(self.config.pinned_environments) self.auto_categorize_changes = self.config.plan.auto_categorize_changes self.selected_gateway = gateway or self.config.default_gateway_name - self.gateway_managed_virtual_layer = self.config.gateway_managed_virtual_layer gw_model_defaults = self.config.gateways[self.selected_gateway].model_defaults if gw_model_defaults: @@ -2617,7 +2616,8 @@ def _context_diff( ensure_finalized_snapshots=ensure_finalized_snapshots, diff_rendered=diff_rendered, environment_statements=self._environment_statements, - gateway_managed_virtual_layer=self.gateway_managed_virtual_layer, + gateway_managed_virtual_layer=self.config.gateway_managed_virtual_layer, + infer_python_dependencies=self.config.infer_python_dependencies, ) def _destroy(self) -> None: diff --git a/sqlmesh/core/context_diff.py b/sqlmesh/core/context_diff.py index 19513be093..4212f328b1 100644 --- a/sqlmesh/core/context_diff.py +++ b/sqlmesh/core/context_diff.py @@ -102,6 +102,7 @@ def create( diff_rendered: bool = False, environment_statements: t.Optional[t.List[EnvironmentStatements]] = [], gateway_managed_virtual_layer: bool = False, + infer_python_dependencies: bool = True, ) -> ContextDiff: """Create a ContextDiff object. @@ -116,6 +117,12 @@ def create( the environment is not finalized. provided_requirements: Python dependencies sourced from the lock file. excluded_requirements: Python dependencies to exclude. + diff_rendered: Whether to compute the diff of the rendered version of the compared expressions. + environment_statements: A list of `before_all` or `after_all` statements associated with the environment. + gateway_managed_virtual_layer: Whether the models' views in the virtual layer are created by the + model-specific gateway rather than the default gateway. + infer_python_dependencies: Whether to statically analyze Python code to automatically infer Python + package requirements. Returns: The ContextDiff object. @@ -208,6 +215,7 @@ def create( provided_requirements or {}, excluded_requirements or set(), snapshots.values(), + infer_python_dependencies=infer_python_dependencies, ) previous_environment_statements = state_reader.get_environment_statements(environment) @@ -475,29 +483,41 @@ def _build_requirements( provided_requirements: t.Dict[str, str], excluded_requirements: t.Set[str], snapshots: t.Collection[Snapshot], + infer_python_dependencies: bool = True, ) -> t.Dict[str, str]: requirements = { k: v for k, v in provided_requirements.items() if k not in excluded_requirements } + + if not infer_python_dependencies: + return requirements + distributions = metadata.packages_distributions() for snapshot in snapshots: - if snapshot.is_model: - for executable in snapshot.model.python_env.values(): - if executable.kind == "import": - try: - start = "from " if executable.payload.startswith("from ") else "import " - lib = executable.payload.split(start)[1].split()[0].split(".")[0] - if lib in distributions: - for dist in distributions[lib]: - if ( - dist not in requirements - and dist not in IGNORED_PACKAGES - and dist not in excluded_requirements - ): - requirements[dist] = metadata.version(dist) - except metadata.PackageNotFoundError: - from sqlmesh.core.console import get_console - - get_console().log_warning(f"Failed to find package for {lib}.") + if not snapshot.is_model: + continue + + for executable in snapshot.model.python_env.values(): + if executable.kind != "import": + continue + + try: + start = "from " if executable.payload.startswith("from ") else "import " + lib = executable.payload.split(start)[1].split()[0].split(".")[0] + if lib not in distributions: + continue + + for dist in distributions[lib]: + if ( + dist not in requirements + and dist not in IGNORED_PACKAGES + and dist not in excluded_requirements + ): + requirements[dist] = metadata.version(dist) + except metadata.PackageNotFoundError: + from sqlmesh.core.console import get_console + + get_console().log_warning(f"Failed to find package for {lib}.") + return requirements diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 1bd0919d9c..3b9b0b4f61 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1240,6 +1240,19 @@ def test_requirements(copy_to_temp_path: t.Callable): assert set(diff.requirements) == {"numpy", "pandas"} +def test_deactivate_automatic_requirement_inference(copy_to_temp_path: t.Callable): + context_path = copy_to_temp_path("examples/sushi")[0] + config = next(iter(load_configs("config", Config, paths=context_path).values())) + + config.infer_python_dependencies = False + context = Context(paths=context_path, config=config) + environment = context.plan( + "dev", no_prompts=True, skip_tests=True, skip_backfill=True, auto_apply=True + ).environment + + assert environment.requirements == {"pandas": "2.2.2"} + + @pytest.mark.slow def test_rendered_diff(): ctx = Context(config=Config()) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index f891b92f00..5efbd569c4 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -5050,7 +5050,7 @@ def test_multi_virtual_layer(copy_to_temp_path): assert len(prod_environment.snapshots_) == 3 # Changing the flag should show a diff - context.gateway_managed_virtual_layer = False + context.config.gateway_managed_virtual_layer = False plan = context.plan_builder().build() assert not plan.requires_backfill assert ( From 5285753066a571109e439fdc8b699c83cd3d2bdd Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Sat, 31 May 2025 07:28:29 +0300 Subject: [PATCH 0298/1056] Chore!: bump sqlglot to v26.24.0 (#4598) --- pyproject.toml | 2 +- tests/core/engine_adapter/test_trino.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c47f65d691..532cfdb069 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.21.0", + "sqlglot[rs]~=26.24.0", "tenacity", "time-machine", "json-stream" diff --git a/tests/core/engine_adapter/test_trino.py b/tests/core/engine_adapter/test_trino.py index 9ee1730274..8e44404708 100644 --- a/tests/core/engine_adapter/test_trino.py +++ b/tests/core/engine_adapter/test_trino.py @@ -442,8 +442,8 @@ def test_table_format(trino_mocked_engine_adapter: TrinoEngineAdapter, mocker: M # rather than explicitly telling it to create an Iceberg table. So this is testing that `FORMAT='ORC'` is output # instead of `FORMAT='ICEBERG'` which would be invalid assert to_sql_calls(adapter) == [ - 'CREATE TABLE IF NOT EXISTS "iceberg"."test_table" ("cola" TIMESTAMP, "colb" VARCHAR, "colc" VARCHAR) WITH (FORMAT=\'ORC\')', - 'CREATE TABLE IF NOT EXISTS "iceberg"."test_table" WITH (FORMAT=\'ORC\') AS SELECT CAST("cola" AS TIMESTAMP) AS "cola", CAST("colb" AS VARCHAR) AS "colb", CAST("colc" AS VARCHAR) AS "colc" FROM (SELECT CAST(1 AS TIMESTAMP) AS "cola", CAST(2 AS VARCHAR) AS "colb", \'foo\' AS "colc") AS "_subquery"', + """CREATE TABLE IF NOT EXISTS "iceberg"."test_table" ("cola" TIMESTAMP, "colb" VARCHAR, "colc" VARCHAR) WITH (format='orc')""", + '''CREATE TABLE IF NOT EXISTS "iceberg"."test_table" WITH (format='orc') AS SELECT CAST("cola" AS TIMESTAMP) AS "cola", CAST("colb" AS VARCHAR) AS "colb", CAST("colc" AS VARCHAR) AS "colc" FROM (SELECT CAST(1 AS TIMESTAMP) AS "cola", CAST(2 AS VARCHAR) AS "colb", \'foo\' AS "colc") AS "_subquery"''', ] From c200c7132e0e1435ee9cc6fe52d27aa8a70f078a Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 2 Jun 2025 09:24:39 +0100 Subject: [PATCH 0299/1056] chore(vscode): add an integration test for gtd (#4608) --- .../extension/tests/go_to_definition.spec.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/vscode/extension/tests/go_to_definition.spec.ts b/vscode/extension/tests/go_to_definition.spec.ts index 7047f06ffd..f392c710bd 100644 --- a/vscode/extension/tests/go_to_definition.spec.ts +++ b/vscode/extension/tests/go_to_definition.spec.ts @@ -11,10 +11,10 @@ test('Go to definition for macro', async () => { try { const { window, close } = await startVSCode(tempDir); - // Wait for the models folder to be visible + // Wait for the models folder to be visible await window.waitForSelector('text=models'); - // Click on the models folder, excluding external_models + // Click on the models folder await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); // Open the customer_revenue_lifetime model @@ -24,7 +24,7 @@ test('Go to definition for macro', async () => { await window.waitForSelector('text=Loaded SQLMesh Context') // Render the model - window.locator("text=@MULTIPLY").click({ + await window.locator("text=@MULTIPLY").click({ modifiers: ["Meta"] }) @@ -35,4 +35,34 @@ test('Go to definition for macro', async () => { } finally { await fs.removeSync(tempDir); } - }); \ No newline at end of file + }); + +test("Go to definition for model", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); + await fs.copy(SUSHI_SOURCE_PATH, tempDir); + + try { + const { window, close } = await startVSCode(tempDir); + + // Wait for the models folder to be visible + await window.waitForSelector('text=models'); + + // Click on the models folder + await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); + + // Open the top_waiters model + await window.getByRole('treeitem', { name: 'top_waiters.sql', exact: true }).locator('a').click(); + + await window.waitForSelector('text=grain'); + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Go to definition for the model + await window.locator("text=sushi.waiter_revenue_by_day").first().click({ + modifiers: ["Meta"] + }) + await expect(window.locator('text=SUM(oi.quantity * i.price)::DOUBLE AS revenue')).toBeVisible(); + await close(); + } finally { + await fs.removeSync(tempDir); + } +}) \ No newline at end of file From d12a309aad5f23f8f508804da1450c8103a09f1b Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 2 Jun 2025 11:33:46 +0100 Subject: [PATCH 0300/1056] chore(lsp): introducing benchmark (#4601) --- .circleci/continue_config.yml | 3 + Makefile | 3 + benchmarks/lsp_render_model_bench.py | 118 +++++++++++++++++++++++++++ pyproject.toml | 1 + 4 files changed, 125 insertions(+) create mode 100644 benchmarks/lsp_render_model_bench.py diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 2546944c16..e2f4063124 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -93,6 +93,9 @@ jobs: - run: name: Run linters and code style checks command: make py-style + - run: + name: Exercise the benchmarks + command: make benchmark-ci - run: name: Run cicd tests command: make cicd-test diff --git a/Makefile b/Makefile index b39ea166a1..0a89bba437 100644 --- a/Makefile +++ b/Makefile @@ -181,3 +181,6 @@ vscode-generate-openapi: python3 web/server/openapi.py --output vscode/openapi.json pnpm run fmt cd vscode/react && pnpm run generate:api + +benchmark-ci: + python benchmarks/lsp_render_model_bench.py --debug-single-value diff --git a/benchmarks/lsp_render_model_bench.py b/benchmarks/lsp_render_model_bench.py new file mode 100644 index 0000000000..f41f5f2d22 --- /dev/null +++ b/benchmarks/lsp_render_model_bench.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +import asyncio +import pyperf +import os +import logging +from pathlib import Path +from lsprotocol import types + +from sqlmesh.lsp.custom import RenderModelRequest, RENDER_MODEL_FEATURE +from sqlmesh.lsp.uri import URI +from pygls.client import JsonRPCClient + +# Suppress debug logging during benchmark +logging.getLogger().setLevel(logging.WARNING) + + +class LSPClient(JsonRPCClient): + """A custom LSP client for benchmarking.""" + + def __init__(self): + super().__init__() + self.render_model_result = None + self.initialized = asyncio.Event() + + # Register handlers for notifications we expect from the server + @self.feature(types.WINDOW_SHOW_MESSAGE) + def handle_show_message(_): + # Silently ignore show message notifications during benchmark + pass + + @self.feature(types.WINDOW_LOG_MESSAGE) + def handle_log_message(_): + # Silently ignore log message notifications during benchmark + pass + + async def initialize_server(self): + """Send initialization request to server.""" + # Get the sushi example directory + sushi_dir = Path(__file__).parent.parent / "examples" / "sushi" + + response = await self.protocol.send_request_async( + types.INITIALIZE, + types.InitializeParams( + process_id=os.getpid(), + root_uri=URI.from_path(sushi_dir).value, + capabilities=types.ClientCapabilities(), + workspace_folders=[ + types.WorkspaceFolder( + uri=URI.from_path(sushi_dir).value, + name="sushi" + ) + ] + ) + ) + + # Send initialized notification + self.protocol.notify(types.INITIALIZED, types.InitializedParams()) + self.initialized.set() + return response + + +async def benchmark_render_model_async(client: LSPClient, model_path: Path): + """Benchmark the render_model request.""" + uri = URI.from_path(model_path).value + + # Send render_model request + result = await client.protocol.send_request_async( + RENDER_MODEL_FEATURE, + RenderModelRequest(textDocumentUri=uri) + ) + + return result + + +def benchmark_render_model(loops): + """Synchronous wrapper for the benchmark.""" + async def run(): + # Create client + client = LSPClient() + + # Start the SQLMesh LSP server as a subprocess + await client.start_io("python", "-m", "sqlmesh.lsp.main") + + # Initialize the server + await client.initialize_server() + + # Get a model file to test with + sushi_dir = Path(__file__).parent.parent / "examples" / "sushi" + model_path = sushi_dir / "models" / "customers.sql" + + # Warm up + await benchmark_render_model_async(client, model_path) + + # Run benchmark + t0 = pyperf.perf_counter() + for _ in range(loops): + await benchmark_render_model_async(client, model_path) + dt = pyperf.perf_counter() - t0 + + # Clean up + await client.stop() + + return dt + + return asyncio.run(run()) + + +def main(): + runner = pyperf.Runner() + runner.bench_time_func( + "lsp_render_model", + benchmark_render_model + ) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 532cfdb069..9129dafc5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ dev = [ "pydantic", "PyAthena[Pandas]", "PyGithub~=2.5.0", + "pyperf", "pyspark~=3.5.0", "pytest", "pytest-asyncio", From 64fc3f5fbb5acfe778c92be020e3220534f2fa02 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:01:30 +0100 Subject: [PATCH 0301/1056] chore(vscode): cleaning up setup and tests (#4610) --- vscode/extension/package.json | 2 -- vscode/extension/tests/diagnostics.spec.ts | 2 +- vscode/extension/tests/lineage.spec.ts | 2 +- vscode/extension/tsconfig.json | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 9552f0d392..dc80ac6399 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -107,8 +107,6 @@ }, "scripts": { "ci": "pnpm run lint && pnpm run compile", - "compile-tests": "tsc -p . --outDir out", - "watch-tests": "tsc -p . -w --outDir out", "lint": "eslint src", "lint:fix": "eslint src --fix", "test:e2e": "playwright test", diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts index a189b25095..39c60c3369 100644 --- a/vscode/extension/tests/diagnostics.spec.ts +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; import path from 'path'; import fs from 'fs-extra'; import os from 'os'; diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index ccb2203006..3231b61623 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -1,4 +1,4 @@ -import { test, _electron as electron, expect, ElectronApplication, Page } from '@playwright/test'; +import { test, expect, Page } from '@playwright/test'; import path from 'path'; import fs from 'fs-extra'; import os from 'os'; diff --git a/vscode/extension/tsconfig.json b/vscode/extension/tsconfig.json index 2d06f6f342..f6b6d29681 100644 --- a/vscode/extension/tsconfig.json +++ b/vscode/extension/tsconfig.json @@ -15,6 +15,6 @@ "@bus/*": ["../bus/src/*"] } }, - "include": ["src/**/*", "../bus/src/**/*"], + "include": ["tests/**/*", "src/**/*", "../bus/src/**/*"], "exclude": ["node_modules", "../node_modules", "../../node_modules"] } From 5a244417928e243c1657132be48c991f769a8e7b Mon Sep 17 00:00:00 2001 From: Afzal Jasani Date: Mon, 2 Jun 2025 08:10:05 -0700 Subject: [PATCH 0302/1056] add new security docs page (#4564) Co-authored-by: Trey Spiller <1831878+treysp@users.noreply.github.com> --- docs/cloud/features/security/security.md | 80 ++++++++++++++++++ .../security/tcloud_hybrid_deployment.png | Bin 0 -> 57806 bytes .../security/tcloud_standard_deployment.png | Bin 0 -> 91838 bytes mkdocs.yml | 2 + 4 files changed, 82 insertions(+) create mode 100644 docs/cloud/features/security/security.md create mode 100644 docs/cloud/features/security/tcloud_hybrid_deployment.png create mode 100644 docs/cloud/features/security/tcloud_standard_deployment.png diff --git a/docs/cloud/features/security/security.md b/docs/cloud/features/security/security.md new file mode 100644 index 0000000000..bbcccd1a6f --- /dev/null +++ b/docs/cloud/features/security/security.md @@ -0,0 +1,80 @@ +# Security Overview + + +At Tobiko, we treat security as a first-class citizen because we know how valuable your data assets are. Our team follows and executes security best practices across each layer of our product. + +## Tobiko Cloud Standard Deployment + +Our standard Tobiko Cloud deployment consists of several components that are each responsible for different parts of the product. + +Below is a diagram of the components along with their descriptions. + +![tobiko_cloud_standard_deployment](./tcloud_standard_deployment.png){ width=80% height=60% style="display: block; margin: 0 auto" } + +- **Scheduler**: Orchestrates schedule cadence and hosts state metadata (code versions, logs, cost) +- **Executor**: Applies code changes and runs SQL queries (actual data processing in SQL Engine) and Python models in proper DAG order. +- **Gateway**: Stores authentication credentials for SQL Engine. Secured through encryption. +- **SQL Engine**: Processes and stores data based on the above instructions within the **customer’s** environment. + +## Tobiko Cloud Hybrid Deployment + +For some customers, our hybrid deployment option is a great fit. It provides a seamless experience with Tobiko Cloud but within your own VPC and infrastructure. + +In a hybrid deployment, Tobiko Cloud does not execute tasks directly with the engine. Instead, it passes tasks to the executors hosted in your environment, which then execute the tasks with the engine. + +Executors are Docker containers that connect to both Tobiko Cloud and your SQL engine. They pull work tasks from the Tobiko Cloud scheduler and execute them with your SQL engine. This is a pull-only mechanism authenticated through an OAuth Client ID/Secret. Whitelist IPs in your network to allow reaching Tobiko Cloud IPs from the executor: 34.28.17.91, 34.136.27.153, 34.136.131.20 + +Below is a diagram of the components along with their description. + +![tobiko_cloud_hybrid_deployment](./tcloud_hybrid_deployment.png){ width=80% height=60% style="display: block; margin: 0 auto" } + +- **Scheduler**: Orchestrates schedule cadence and hosts state metadata (code versions, logs, cost). **Never pushes** instructions to executor. +- **Executor**: Appplies code changes and runs SQL queries and Python models in proper DAG order (actual data processing in SQL Engine) +- **Gateway**: Stores authentication credentials for SQL Engine. Secured through your secrets manager or Kubernetes Secrets. +- **SQL Engine**: Processes and stores data based on the above instructions +- **Executor -> Scheduler**: A pull-only mechanism for obtaining work tasks. +- **Helm Chart**: For production environements, we provide a [Helm chart](../scheduler/hybrid_executors_helm.md) that includes robust configurability, secret management, and scaling options. +- **Docker Compose**: For simpler environments or testing, we offer a [Docker Compose setup](../scheduler/hybrid_executors_docker_compose.md) to quickly deploy executors on any machine with Docker. + + + +## Internal Code Practices + +We enforce coding standards throughout Tobiko to write, maintain, and collaborate on code effectively. These practice ensure consistency, maintainability, reliability, and most importantly, trust. + +A few key components of our internal code requirements: + +- We used signed Git commits, required approvers, and signed Docker artifacts. +- Each commit to a `main` branch must be approved by someone other than the author. +- We sign commits and register the key with GitHub ([Github Docs](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)). +- Binaries are signed using cosign and OIDC for keyless ([Signing docs](https://docs.sigstore.dev/cosign/signing/overview/)). +- Attestations are created to certify an image, enforced with GCP Binary Authorization ([Attestation docs](https://cloud.google.com/binary-authorization/docs/key-concepts#attestations)). +- Encryption is a key feature of our security posture and is enforced at each stage of access. For example, the state database automatically encrypts all data. Credentials are also securely encrypted and stored. +- We back up each state database nightly and before upgrades. These backups are stored for 14 days. + +## Penetration Testing + +At least once a year, Tobiko engages a third-party security firm to perform a penetration test. This test evaluates our systems by identifying and attempting to exploit known vulnerabilities, focusing on critical external and/or internal assets. A detailed report is available upon request. + + +## Asset and Access Management + +### How do we protect PGP keys? + +If an employee loses their laptop, we don't need to get the old PGP key back because we can invalidate the key directly. + +We use GitHub to sign code commits. At the time the code was committed, the PGP key was valid. When an employee loses their laptop, we will invalidate it, and they will regenerate a new key to use in future commits. The old commits are still valid because the PGP key was valid at the time the commit was made. + +### How do we invalidate PGP keys if someone did steal it and could potentially use it? + +We would revoke access for the GitHub user account associated with the compromised key and not give it access again until the old PGP key is deprecated and a new key issued. + +### If someone steals a laptop, what's our continuity plan in protecting code? + +- All employee devices are monitored for proper encryption and password policies. +- Laptop protection is enforced through file encryption via Vanta. +- Mandatory lock screen after a timeout. +- We follow a formal IT asset disposal procedure to prevent key compromise through improper hardware disposal. +- See above for PGP key protection. +- Binaries are signed using Cosign and OIDC for keyless signing. + diff --git a/docs/cloud/features/security/tcloud_hybrid_deployment.png b/docs/cloud/features/security/tcloud_hybrid_deployment.png new file mode 100644 index 0000000000000000000000000000000000000000..6573342f60fdc1789ade7f768beb9ebfe62d31f8 GIT binary patch literal 57806 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≻+}%N*G5h+%kOZVakMpj;n81bZGEK3~`Hm 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 Q|gDUaNB&mMY+&bTr4>xN2>USE4v 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 literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index e149f26a37..092c8d72e2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -123,6 +123,8 @@ nav: - "Docker Compose example": cloud/features/scheduler/hybrid_executors_docker_compose.md - cloud/features/scheduler/airflow.md - cloud/features/scheduler/dagster.md + - Security: + - "Security Overview": cloud/features/security/security.md # - Observability: # - cloud/features/observability/overview.md # - cloud/features/observability/model_freshness.md From 2e4b4415e73c2cfb3b303dd264246e4e5ef9103c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Thal=C3=A9n?= Date: Mon, 2 Jun 2025 17:38:41 +0200 Subject: [PATCH 0303/1056] Add data path and encrypted to duckdb catalog (#4600) --- docs/integrations/engines/duckdb.md | 47 ++++++++++++++++++++++++++++ sqlmesh/core/config/connection.py | 12 +++++++ tests/core/test_connection_config.py | 24 ++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/docs/integrations/engines/duckdb.md b/docs/integrations/engines/duckdb.md index 0b593ce498..33751cda8f 100644 --- a/docs/integrations/engines/duckdb.md +++ b/docs/integrations/engines/duckdb.md @@ -64,6 +64,53 @@ SQLMesh will place models with the explicit catalog "ephemeral", such as `epheme ) ``` +#### DuckLake Catalog Example + +=== "YAML" + + ```yaml linenums="1" + gateways: + my_gateway: + connection: + type: duckdb + catalogs: + ducklake: + type: ducklake + path: 'catalog.ducklake' + data_path: data/ducklake + encrypted: True + ``` + +=== "Python" + + ```python linenums="1" + from sqlmesh.core.config import ( + Config, + ModelDefaultsConfig, + GatewayConfig, + DuckDBConnectionConfig + ) + from sqlmesh.core.config.connection import DuckDBAttachOptions + + config = Config( + model_defaults=ModelDefaultsConfig(dialect=), + gateways={ + "my_gateway": GatewayConfig( + connection=DuckDBConnectionConfig( + catalogs={ + "ducklake": DuckDBAttachOptions( + type="ducklake", + path="catalog.ducklake", + data_path="data/ducklake", + encrypted=True + ), + } + ) + ), + } + ) + ``` + #### Other Connection Catalogs Example Catalogs can also be defined to connect to anything that [DuckDB can be attached to](https://duckdb.org/docs/sql/statements/attach.html). diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 9cc322d901..b932e5d3e2 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -221,6 +221,10 @@ class DuckDBAttachOptions(BaseConfig): path: str read_only: bool = False + # DuckLake specific options + data_path: t.Optional[str] = None + encrypted: bool = False + def to_sql(self, alias: str) -> str: options = [] # 'duckdb' is actually not a supported type, but we'd like to allow it for @@ -229,6 +233,14 @@ def to_sql(self, alias: str) -> str: options.append(f"TYPE {self.type.upper()}") if self.read_only: options.append("READ_ONLY") + + # DuckLake specific options + if self.type == "ducklake": + if self.data_path: + options.append(f"DATA_PATH '{self.data_path}'") + if self.encrypted: + options.append("ENCRYPTED") + options_sql = f" ({', '.join(options)})" if options else "" alias_sql = "" # TODO: Add support for Postgres schema. Currently adding it blocks access to the information_schema diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index f30aba119b..9f91ddd55e 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -602,6 +602,30 @@ def test_duckdb_attach_catalog(make_config): assert not config.is_recommended_for_state_sync +def test_duckdb_attach_ducklake_catalog(make_config): + config = make_config( + type="duckdb", + catalogs={ + "ducklake": DuckDBAttachOptions( + type="ducklake", + path="catalog.ducklake", + data_path="/tmp/ducklake_data", + encrypted=True, + ), + }, + ) + assert isinstance(config, DuckDBConnectionConfig) + ducklake_catalog = config.catalogs.get("ducklake") + assert ducklake_catalog is not None + assert ducklake_catalog.type == "ducklake" + assert ducklake_catalog.path == "catalog.ducklake" + assert ducklake_catalog.data_path == "/tmp/ducklake_data" + assert ducklake_catalog.encrypted is True + # Check that the generated SQL includes DATA_PATH + assert "DATA_PATH '/tmp/ducklake_data'" in ducklake_catalog.to_sql("ducklake") + assert "ENCRYPTED" in ducklake_catalog.to_sql("ducklake") + + def test_duckdb_attach_options(): options = DuckDBAttachOptions( type="postgres", path="dbname=postgres user=postgres host=127.0.0.1", read_only=True From b5989c8165c0cae1fb418d360f13c5098ce6f7d3 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:56:56 +0100 Subject: [PATCH 0304/1056] chore(vscode): for tests tell it to install extension (#4617) --- vscode/extension/tests/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index 10e368873f..af4e0bb7fb 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -32,6 +32,7 @@ export const startVSCode = async (workspaceDir: string): Promise<{ `--extensionDevelopmentPath=${EXT_PATH}`, '--disable-workspace-trust', '--disable-telemetry', + '--install-extension=ms-python.python', `--user-data-dir=${userDataDir}`, workspaceDir, ]; From 1da137a0bb91a137368f0f65cb8c0dae7ae47905 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:58:10 +0100 Subject: [PATCH 0305/1056] refactor(lsp): refactor lsp calls into context (#4616) --- sqlmesh/lsp/context.py | 123 ++++++++++++++++++++++++++++++----------- sqlmesh/lsp/main.py | 64 +++++---------------- 2 files changed, 106 insertions(+), 81 deletions(-) diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 89ca236563..6fd0ddd5d3 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -4,6 +4,7 @@ import typing as t from sqlmesh.core.model.definition import SqlModel +from sqlmesh.core.linter.definition import AnnotatedRuleViolation from sqlmesh.lsp.custom import RenderModelEntry from sqlmesh.lsp.uri import URI @@ -28,8 +29,14 @@ class LSPContext: model names and standalone audit names. """ + map: t.Dict[Path, t.Union[ModelTarget, AuditTarget]] + _render_cache: t.Dict[Path, t.List[RenderModelEntry]] + _lint_cache: t.Dict[Path, t.List[AnnotatedRuleViolation]] + def __init__(self, context: Context) -> None: self.context = context + self._render_cache = {} + self._lint_cache = {} # Add models to the map model_map: t.Dict[Path, ModelTarget] = {} @@ -54,36 +61,90 @@ def __init__(self, context: Context) -> None: **audit_map, } + def render_model(self, uri: URI) -> t.List[RenderModelEntry]: + """Get rendered models for a file, using cache when available. -def render_model(context: LSPContext, uri: URI) -> t.Iterator[RenderModelEntry]: - target = context.map[uri.to_path()] - if isinstance(target, AuditTarget): - audit = context.context.standalone_audits[target.name] - definition = audit.render_definition( - include_python=False, - render_query=True, - ) - rendered_query = [render.sql(dialect=audit.dialect, pretty=True) for render in definition] - yield RenderModelEntry( - name=audit.name, - fqn=audit.fqn, - description=audit.description, - rendered_query="\n\n".join(rendered_query), - ) - if isinstance(target, ModelTarget): - for name in target.names: - model = context.context.get_model(name) - if isinstance(model, SqlModel): - rendered_query = [ - render.sql(dialect=model.dialect, pretty=True) - for render in model.render_definition( - include_python=False, - render_query=True, + Args: + uri: The URI of the file to render. + + Returns: + List of rendered model entries. + """ + path = uri.to_path() + + # Check cache first + if path in self._render_cache: + return self._render_cache[path] + + # If not cached, render and cache + entries: t.List[RenderModelEntry] = [] + target = self.map.get(path) + + if isinstance(target, AuditTarget): + audit = self.context.standalone_audits[target.name] + definition = audit.render_definition( + include_python=False, + render_query=True, + ) + rendered_query = [ + render.sql(dialect=audit.dialect, pretty=True) for render in definition + ] + entry = RenderModelEntry( + name=audit.name, + fqn=audit.fqn, + description=audit.description, + rendered_query="\n\n".join(rendered_query), + ) + entries.append(entry) + + elif isinstance(target, ModelTarget): + for name in target.names: + model = self.context.get_model(name) + if isinstance(model, SqlModel): + rendered_query = [ + render.sql(dialect=model.dialect, pretty=True) + for render in model.render_definition( + include_python=False, + render_query=True, + ) + ] + entry = RenderModelEntry( + name=model.name, + fqn=model.fqn, + description=model.description, + rendered_query="\n\n".join(rendered_query), ) - ] - yield RenderModelEntry( - name=model.name, - fqn=model.fqn, - description=model.description, - rendered_query="\n\n".join(rendered_query), - ) + entries.append(entry) + + # Store in cache + self._render_cache[path] = entries + return entries + + def lint_model(self, uri: URI) -> t.List[AnnotatedRuleViolation]: + """Get lint diagnostics for a model, using cache when available. + + Args: + uri: The URI of the file to lint. + + Returns: + List of annotated rule violations. + """ + path = uri.to_path() + + # Check cache first + if path in self._lint_cache: + return self._lint_cache[path] + + # If not cached, lint and cache + target = self.map.get(path) + if target is None or not isinstance(target, ModelTarget): + return [] + + diagnostics = self.context.lint_models( + target.names, + raise_on_error=False, + ) + + # Store in cache + self._lint_cache[path] = diagnostics + return diagnostics diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 3d04f0d558..203a208bb9 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -24,7 +24,6 @@ from sqlmesh.lsp.context import ( LSPContext, ModelTarget, - render_model as render_model_context, ) from sqlmesh.lsp.custom import ( ALL_MODELS_FEATURE, @@ -58,10 +57,6 @@ def __init__( self.context_class = context_class self.lsp_context: t.Optional[LSPContext] = None - # Cache stores tuples of (diagnostics, diagnostic_version) - self.lint_cache: t.Dict[URI, t.Tuple[t.List[AnnotatedRuleViolation], int]] = {} - self._diagnostic_version_counter: int = 0 - self.client_supports_pull_diagnostics = False # Register LSP features (e.g., formatting, hover, etc.) self._register_features() @@ -120,7 +115,7 @@ def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsRespons def render_model(ls: LanguageServer, params: RenderModelRequest) -> RenderModelResponse: uri = URI(params.textDocumentUri) context = self._context_get_or_load(uri) - return RenderModelResponse(models=list(render_model_context(context, uri))) + return RenderModelResponse(models=context.render_model(uri)) @self.server.feature(API_FEATURE) def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]: @@ -173,17 +168,11 @@ def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> Non if models is None or not isinstance(models, ModelTarget): return - if self.lint_cache.get(uri) is None: - diagnostics = context.context.lint_models( - models.names, - raise_on_error=False, - ) - self._diagnostic_version_counter += 1 - self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter) + # Get diagnostics from context (which handles caching) + diagnostics = context.lint_model(uri) # Only publish diagnostics if client doesn't support pull diagnostics if not self.client_supports_pull_diagnostics: - diagnostics, _ = self.lint_cache[uri] ls.publish_diagnostics( params.text_document.uri, SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), @@ -197,13 +186,8 @@ def did_change(ls: LanguageServer, params: types.DidChangeTextDocumentParams) -> if models is None or not isinstance(models, ModelTarget): return - # Always update the cache - diagnostics = context.context.lint_models( - models.names, - raise_on_error=False, - ) - self._diagnostic_version_counter += 1 - self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter) + # Get diagnostics from context (which handles caching) + diagnostics = context.lint_model(uri) # Only publish diagnostics if client doesn't support pull diagnostics if not self.client_supports_pull_diagnostics: @@ -220,13 +204,8 @@ def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> Non if models is None or not isinstance(models, ModelTarget): return - # Always update the cache - diagnostics = context.context.lint_models( - models.names, - raise_on_error=False, - ) - self._diagnostic_version_counter += 1 - self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter) + # Get diagnostics from context (which handles caching) + diagnostics = context.lint_model(uri) # Only publish diagnostics if client doesn't support pull diagnostics if not self.client_supports_pull_diagnostics: @@ -445,31 +424,16 @@ def workspace_diagnostic( return types.WorkspaceDiagnosticReport(items=[]) def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic], int]: - """Get diagnostics for a specific URI, returning (diagnostics, result_id).""" - # Check if we have cached diagnostics - if uri in self.lint_cache: - diagnostics, result_id = self.lint_cache[uri] - return SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), result_id + """Get diagnostics for a specific URI, returning (diagnostics, result_id). - # Try to get diagnostics by loading context and linting + Since we no longer track version numbers, we always return 0 as the result_id. + This means pull diagnostics will always fetch fresh results. + """ try: context = self._context_get_or_load(uri) - models = context.map[uri.to_path()] - if models is None or not isinstance(models, ModelTarget): - return [], 0 - - # Lint the models and cache the results - diagnostics = context.context.lint_models( - models.names, - raise_on_error=False, - ) - self._diagnostic_version_counter += 1 - self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter) - return SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics( - diagnostics - ), self._diagnostic_version_counter + diagnostics = context.lint_model(uri) + return SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), 0 except Exception: - # If we can't get diagnostics, return empty list with no result ID return [], 0 def _context_get_or_load(self, document_uri: URI) -> LSPContext: @@ -523,7 +487,7 @@ def _ensure_context_for_document( created_context = self.context_class(paths=[path]) self.lsp_context = LSPContext(created_context) loaded = True - # Re-check context for document now that it's loaded + # Re-check context for the document now that it's loaded return self._ensure_context_for_document(document_uri) except Exception as e: self.server.show_message( From 194fd9248f57e5aecbfef05e4bbbdc3d1eea207b Mon Sep 17 00:00:00 2001 From: Nick Muoh <96191720+nickmuoh@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:04:58 -0700 Subject: [PATCH 0306/1056] Support displaying snowpark dataframes in notebooks. (#4572) --- sqlmesh/magics.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 6929cc8158..d77120c72b 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -32,7 +32,7 @@ from sqlmesh.core.dialect import format_model_expressions, parse from sqlmesh.core.model import load_sql_based_model from sqlmesh.core.test import ModelTestMetadata -from sqlmesh.utils import sqlglot_dialects, yaml, Verbosity +from sqlmesh.utils import sqlglot_dialects, yaml, Verbosity, optional_import from sqlmesh.utils.errors import MagicError, MissingContextException, SQLMeshError logger = logging.getLogger(__name__) @@ -548,6 +548,8 @@ def run_dag(self, context: Context, line: str) -> None: def evaluate(self, context: Context, line: str) -> None: """Evaluate a model query and fetches a dataframe.""" context.refresh() + + snowpark = optional_import("snowflake.snowpark") args = parse_argstring(self.evaluate, line) df = context.evaluate( @@ -557,6 +559,10 @@ def evaluate(self, context: Context, line: str) -> None: execution_time=args.execution_time, limit=args.limit, ) + + if snowpark and isinstance(df, snowpark.DataFrame): + df = df.limit(args.limit or 100).to_pandas() + self.display(df) @magic_arguments() From e396c8cede35b9305b2e043704e02de1653241ba Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:05:21 -0500 Subject: [PATCH 0307/1056] Chore: improve error message when no MODEL block (#4588) --- sqlmesh/core/loader.py | 6 ++++-- sqlmesh/core/model/definition.py | 19 +++++++++++++------ tests/core/test_loader.py | 2 +- tests/core/test_model.py | 4 ++-- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index 5965e6c1b4..ee0af270cb 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -452,10 +452,12 @@ def _track_file(self, path: Path) -> None: self._path_mtimes[path] = path.stat().st_mtime def _failed_to_load_model_error(self, path: Path, error: t.Union[str, Exception]) -> str: - base_message = f"Failed to load model definition at '{path}':" + base_message = f"Failed to load model from file '{path}':" if isinstance(error, ValidationError): return validation_error_message(error, base_message) - return f"{base_message}\n {error}" + # indent all lines of error message + error_message = str(error).replace("\n", "\n ") + return f"{base_message}\n\n {error_message}" class SqlMeshLoader(Loader): diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index a0d52bc523..4ca865669a 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2036,18 +2036,25 @@ def load_sql_based_model( variables: The variables to pass to the model. kwargs: Additional kwargs to pass to the loader. """ + missing_model_msg = f"""Please add a MODEL block at the top of the file. Example: + +MODEL ( + name sqlmesh_example.full_model, --model name + kind FULL, --materialization + cron '@daily', --schedule +); + +Learn more at https://sqlmesh.readthedocs.io/en/stable/concepts/models/overview +""" + if not expressions: - raise_config_error("Incomplete model definition, missing MODEL statement", path) + raise_config_error(missing_model_msg) dialect = dialect or "" meta = expressions[0] if not isinstance(meta, d.Model): if not infer_names: - raise_config_error( - "The MODEL statement is required as the first statement in the definition, " - "unless model name inference is enabled.", - path, - ) + raise_config_error(missing_model_msg) meta = d.Model(expressions=[]) # Dummy meta node expressions.insert(0, meta) diff --git a/tests/core/test_loader.py b/tests/core/test_loader.py index 6ee9a44060..b194139d60 100644 --- a/tests/core/test_loader.py +++ b/tests/core/test_loader.py @@ -159,7 +159,7 @@ def execute( with pytest.raises( ConfigError, - match=r"Failed to load model definition at '.*'.\n Duplicate name: 'test_schema.test_model'.", + match=r"Failed to load model from file '.*'.\n\n Duplicate name: 'test_schema.test_model'.", ): Context(paths=tmp_path, config=config) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 4efa45f62b..b04aabf1dc 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -576,7 +576,7 @@ def test_no_model_statement(tmp_path: Path): expressions = d.parse("SELECT 1 AS x") with pytest.raises( ConfigError, - match="The MODEL statement is required as the first statement in the definition, unless model name inference is enabled. at '.'", + match="Please add a MODEL block at the top of the file. Example:", ): load_sql_based_model(expressions) @@ -613,7 +613,7 @@ def test_unordered_model_statements(): with pytest.raises(ConfigError) as ex: load_sql_based_model(expressions) - assert "MODEL statement is required" in str(ex.value) + assert "Please add a MODEL block at the top of the file. Example:" in str(ex.value) def test_no_query(): From 7b1b78a143308b613921778434627c8b4f18f0de Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 2 Jun 2025 21:53:34 +0300 Subject: [PATCH 0308/1056] Fix: Adapt get_dbt_version tuple to include patch in version (#4619) --- sqlmesh/dbt/builtin.py | 2 +- sqlmesh/dbt/manifest.py | 14 +++++++------- sqlmesh/dbt/relation.py | 2 +- sqlmesh/dbt/target.py | 4 ++-- sqlmesh/dbt/util.py | 10 +++++++--- tests/utils/test_metaprogramming.py | 20 ++++++++++---------- 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index b734034b2f..e07e00c961 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -30,7 +30,7 @@ class Exceptions: def raise_compiler_error(self, msg: str) -> None: - if DBT_VERSION >= (1, 4): + if DBT_VERSION >= (1, 4, 0): from dbt.exceptions import CompilationError raise CompilationError(msg) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 2b38f4362f..beb0639ed2 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -238,7 +238,7 @@ def _load_tests(self) -> None: dependencies.macros.append(MacroReference(package="dbt", name="get_where_subquery")) dependencies.macros.append(MacroReference(package="dbt", name="should_store_failures")) - sql = node.raw_code if DBT_VERSION >= (1, 3) else node.raw_sql # type: ignore + sql = node.raw_code if DBT_VERSION >= (1, 3, 0) else node.raw_sql # type: ignore dependencies = dependencies.union(self._extra_dependencies(sql, node.package_name)) dependencies = dependencies.union( self._flatten_dependencies_from_macros(dependencies.macros, node.package_name) @@ -278,7 +278,7 @@ def _load_models_and_seeds(self) -> None: node_name = f"{node_name}_v{node_version}" if node.resource_type in {"model", "snapshot"}: - sql = node.raw_code if DBT_VERSION >= (1, 3) else node.raw_sql # type: ignore + sql = node.raw_code if DBT_VERSION >= (1, 3, 0) else node.raw_sql # type: ignore dependencies = Dependencies( macros=macro_references, refs=_refs(node), sources=_sources(node) ) @@ -305,7 +305,7 @@ def _load_on_run_start_end(self) -> None: if node.resource_type == "operation" and ( set(node.tags) & {"on-run-start", "on-run-end"} ): - sql = node.raw_code if DBT_VERSION >= (1, 3) else node.raw_sql # type: ignore + sql = node.raw_code if DBT_VERSION >= (1, 3, 0) else node.raw_sql # type: ignore node_name = node.name node_path = Path(node.original_file_path) @@ -339,7 +339,7 @@ def _load_manifest(self) -> Manifest: variables = ( self.variable_overrides - if DBT_VERSION >= (1, 5) + if DBT_VERSION >= (1, 5, 0) else json.dumps(self.variable_overrides) ) @@ -354,7 +354,7 @@ def _load_manifest(self) -> Manifest: ) flags.set_from_args(args, None) - if DBT_VERSION >= (1, 8): + if DBT_VERSION >= (1, 8, 0): from dbt_common.context import set_invocation_context # type: ignore set_invocation_context(os.environ) @@ -371,7 +371,7 @@ def _load_manifest(self) -> Manifest: self._project_name = project.project_name - if DBT_VERSION >= (1, 8): + if DBT_VERSION >= (1, 8, 0): from dbt.mp_context import get_mp_context # type: ignore register_adapter(runtime_config, get_mp_context()) # type: ignore @@ -546,7 +546,7 @@ def _macro_references( def _refs(node: ManifestNode) -> t.Set[str]: - if DBT_VERSION >= (1, 5): + if DBT_VERSION >= (1, 5, 0): result = set() for r in node.refs: ref_name = f"{r.package}.{r.name}" if r.package else r.name diff --git a/sqlmesh/dbt/relation.py b/sqlmesh/dbt/relation.py index 9d07db8bc6..f68a9ff6de 100644 --- a/sqlmesh/dbt/relation.py +++ b/sqlmesh/dbt/relation.py @@ -1,7 +1,7 @@ from sqlmesh.dbt.util import DBT_VERSION -if DBT_VERSION < (1, 8): +if DBT_VERSION < (1, 8, 0): from dbt.contracts.relation import * # type: ignore # noqa: F403 else: from dbt.adapters.contracts.relation import * # type: ignore # noqa: F403 diff --git a/sqlmesh/dbt/target.py b/sqlmesh/dbt/target.py index 60490ceed8..e7603232e8 100644 --- a/sqlmesh/dbt/target.py +++ b/sqlmesh/dbt/target.py @@ -167,7 +167,7 @@ def validate_authentication(cls, data: t.Any) -> t.Any: if not isinstance(data, dict): return data - if "database" not in data and DBT_VERSION >= (1, 5): + if "database" not in data and DBT_VERSION >= (1, 5, 0): path = data.get("path") data["database"] = ( "memory" @@ -424,7 +424,7 @@ def relation_class(cls) -> t.Type[BaseRelation]: @classproperty def column_class(cls) -> t.Type[Column]: - if DBT_VERSION < (1, 6): + if DBT_VERSION < (1, 6, 0): from dbt.adapters.redshift import RedshiftColumn # type: ignore return RedshiftColumn diff --git a/sqlmesh/dbt/util.py b/sqlmesh/dbt/util.py index 8fc6c6ecd2..53f4b2010f 100644 --- a/sqlmesh/dbt/util.py +++ b/sqlmesh/dbt/util.py @@ -5,14 +5,18 @@ from dbt.version import get_installed_version -def _get_dbt_version() -> t.Tuple[int, int]: +def _get_dbt_version() -> t.Tuple[int, int, int]: dbt_version = get_installed_version() - return (int(dbt_version.major or "0"), int(dbt_version.minor or "0")) + return ( + int(dbt_version.major or "0"), + int(dbt_version.minor or "0"), + int(dbt_version.patch or "0"), + ) DBT_VERSION = _get_dbt_version() -if DBT_VERSION < (1, 8): +if DBT_VERSION < (1, 8, 0): from dbt.clients.agate_helper import table_from_data_flat, empty_table, as_matrix # type: ignore # noqa: F401 else: from dbt_common.clients.agate_helper import table_from_data_flat, empty_table, as_matrix # type: ignore # noqa: F401 diff --git a/tests/utils/test_metaprogramming.py b/tests/utils/test_metaprogramming.py index 0de3f63b81..eaea5e6fa4 100644 --- a/tests/utils/test_metaprogramming.py +++ b/tests/utils/test_metaprogramming.py @@ -108,7 +108,7 @@ def other_func(a: int) -> int: @contextmanager -def test_context_manager(): +def sample_context_manager(): yield @@ -141,7 +141,7 @@ def main_func(y: int, foo=exp.true(), *, bar=expressions.Literal.number(1) + 2) def closure(z: int) -> int: return z + Z - with test_context_manager(): + with sample_context_manager(): pass return closure(y) + other_func(Y) @@ -171,7 +171,7 @@ def test_func_globals() -> None: "exp": exp, "expressions": exp, "fetch_data": fetch_data, - "test_context_manager": test_context_manager, + "sample_context_manager": sample_context_manager, "function_with_custom_decorator": function_with_custom_decorator, "SQLGLOT_META": SQLGLOT_META, } @@ -211,7 +211,7 @@ def test_normalize_source() -> None: def closure(z: int): return z + Z - with test_context_manager(): + with sample_context_manager(): pass return closure(y) + other_func(Y)""" ) @@ -261,7 +261,7 @@ def test_serialize_env() -> None: def closure(z: int): return z + Z - with test_context_manager(): + with sample_context_manager(): pass return closure(y) + other_func(Y)""", ), @@ -318,9 +318,9 @@ def baz(self): ), "func": Executable( payload="""@contextmanager -def test_context_manager(): +def sample_context_manager(): yield""", - name="test_context_manager", + name="sample_context_manager", path="test_metaprogramming.py", alias="func", ), @@ -344,11 +344,11 @@ def test_context_manager(): my_lambda() return X + a + W""", ), - "test_context_manager": Executable( + "sample_context_manager": Executable( payload="""@contextmanager -def test_context_manager(): +def sample_context_manager(): yield""", - name="test_context_manager", + name="sample_context_manager", path="test_metaprogramming.py", ), "wraps": Executable(payload="from functools import wraps", kind=ExecutableKind.IMPORT), From f33fce300fbc9187d0e2f2ae8398819b11e4c395 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 2 Jun 2025 22:09:00 +0300 Subject: [PATCH 0309/1056] Fix: Don't pass engine adapter dialect in test connection (#4618) --- sqlmesh/core/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 9df16378d3..de0bf2fb9e 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -2534,7 +2534,7 @@ def test_connection_config(self) -> ConnectionConfig: self._test_connection_config = self.config.get_test_connection( self.gateway, self.default_catalog, - default_catalog_dialect=self.engine_adapter.DIALECT, + default_catalog_dialect=self.config.dialect, ) return self._test_connection_config From 7d1744b383c7013e77d74dbd2c648cc1644ddfe5 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 3 Jun 2025 07:33:20 +1200 Subject: [PATCH 0310/1056] Chore: Improve plan error handling when --select-model is used and the remote snapshot can't be rendered (#4585) --- sqlmesh/core/context.py | 18 +- sqlmesh/core/selector.py | 12 +- tests/cli/test_integration_cli.py | 278 ++++++++++++++++++++++++++++-- 3 files changed, 286 insertions(+), 22 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index de0bf2fb9e..e42b5d4f86 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1448,12 +1448,18 @@ def plan_builder( models_override: t.Optional[UniqueKeyDict[str, Model]] = None if select_models: - models_override = model_selector.select_models( - select_models, - environment, - fallback_env_name=create_from or c.PROD, - ensure_finalized_snapshots=self.config.plan.use_finalized_state, - ) + try: + models_override = model_selector.select_models( + select_models, + environment, + fallback_env_name=create_from or c.PROD, + ensure_finalized_snapshots=self.config.plan.use_finalized_state, + ) + except SQLMeshError as e: + logger.exception(e) # ensure the full stack trace is logged + raise PlanError( + f"{e}\nCheck the SQLMesh log file for the full stack trace.\nIf the model has been fixed locally, please ensure that the --select-model expression includes it." + ) if not backfill_models: # Only backfill selected models unless explicitly specified. backfill_models = model_selector.expand_model_selections(select_models) diff --git a/sqlmesh/core/selector.py b/sqlmesh/core/selector.py index ffc5af3e73..5d068b5d6a 100644 --- a/sqlmesh/core/selector.py +++ b/sqlmesh/core/selector.py @@ -16,6 +16,7 @@ from sqlmesh.utils import UniqueKeyDict from sqlmesh.utils.dag import DAG from sqlmesh.utils.git import GitClient +from sqlmesh.utils.errors import SQLMeshError if t.TYPE_CHECKING: @@ -111,7 +112,16 @@ def select_models( def get_model(fqn: str) -> t.Optional[Model]: if fqn not in all_selected_models and fqn in env_models: # Unselected modified or added model. - return env_models[fqn] + model_from_env = env_models[fqn] + try: + # this triggers a render_query() which can throw an exception + model_from_env.depends_on + return model_from_env + except Exception as e: + raise SQLMeshError( + f"Model '{model_from_env.name}' sourced from state cannot be rendered " + f"in the local environment due to:\n> {str(e)}" + ) from e if fqn in all_selected_models and fqn in self._models: # Selected modified or removed model. return self._models[fqn] diff --git a/tests/cli/test_integration_cli.py b/tests/cli/test_integration_cli.py index 787c2c608b..7d8802d193 100644 --- a/tests/cli/test_integration_cli.py +++ b/tests/cli/test_integration_cli.py @@ -6,6 +6,7 @@ from sqlmesh.utils import yaml import shutil import site +import uuid pytestmark = pytest.mark.slow @@ -16,6 +17,10 @@ def __call__( ) -> subprocess.CompletedProcess: ... +class CreateSitePackageType(t.Protocol): + def __call__(self, name: str) -> t.Tuple[str, Path]: ... + + @pytest.fixture def invoke_cli(tmp_path: Path) -> InvokeCliType: # Fetch the full path to the SQLMesh binary so that when we use `cwd` to run in the context of a test dir, the correct SQLMesh binary is executed @@ -40,9 +45,8 @@ def _invoke(sqlmesh_args: t.List[str], **kwargs: t.Any) -> subprocess.CompletedP return _invoke -def test_load_snapshots_that_reference_nonexistent_python_libraries( - invoke_cli: InvokeCliType, tmp_path: Path -) -> None: +@pytest.fixture +def duckdb_example_project(tmp_path: Path) -> Path: init_example_project(tmp_path, dialect="duckdb") config_path = tmp_path / "config.yaml" @@ -54,11 +58,65 @@ def test_load_snapshots_that_reference_nonexistent_python_libraries( } config_path.write_text(yaml.dump(config_dict)) + return tmp_path + + +@pytest.fixture +def last_log_file_contents(tmp_path: Path) -> t.Callable[[], str]: + def _fetch() -> str: + log_file = sorted(list((tmp_path / "logs").iterdir()))[-1] + return log_file.read_text() + + return _fetch + + +@pytest.fixture +def create_site_package() -> t.Iterator[CreateSitePackageType]: + created_package_path = None + + def _create(name: str) -> t.Tuple[str, Path]: + nonlocal created_package_path + + unique_id = str(uuid.uuid4())[0:8] + package_name = f"{name}_{unique_id}" # so that multiple tests using the same name dont clobber each other + + site_packages = site.getsitepackages()[0] + package_path = Path(site_packages) / package_name + package_path.mkdir() + + created_package_path = package_path + + return package_name, package_path + + yield _create + + if created_package_path: + # cleanup + shutil.rmtree(created_package_path, ignore_errors=True) + + +def test_load_snapshots_that_reference_nonexistent_python_libraries( + invoke_cli: InvokeCliType, + duckdb_example_project: Path, + last_log_file_contents: t.Callable[[], str], + create_site_package: CreateSitePackageType, +) -> None: + """ + Scenario: + - A model is created using a macro that is imported from an external package + - That model is applied + snapshot committed to state + - The external package is removed locally and the import macro import is changed to an inline definition + + Outcome: + - `sqlmesh plan` should not exit with an ImportError when it tries to render the query of the snapshot stored in state + - Instead, it should log a warning and proceed with applying the new model version + """ + + project_path = duckdb_example_project + # simulate a 3rd party library that provides a macro - site_packages = site.getsitepackages()[0] - sqlmesh_test_macros_package_path = Path(site_packages) / "sqlmesh_test_macros" - sqlmesh_test_macros_package_path.mkdir() - (sqlmesh_test_macros_package_path / "macros.py").write_text(""" + package_name, package_path = create_site_package("sqlmesh_test_macros") + (package_path / "macros.py").write_text(""" from sqlmesh import macro @macro() @@ -67,11 +125,11 @@ def do_something(evaluator): """) # reference the macro from site-packages - (tmp_path / "macros" / "__init__.py").write_text(""" -from sqlmesh_test_macros.macros import do_something + (project_path / "macros" / "__init__.py").write_text(f""" +from {package_name}.macros import do_something """) - (tmp_path / "models" / "example.sql").write_text(""" + (project_path / "models" / "example.sql").write_text(""" MODEL ( name example.test_model, kind FULL @@ -96,10 +154,10 @@ def do_something(evaluator): # deleting this removes the 'do_something()' macro used by the version of the snapshot stored in state # when loading the old snapshot from state in the local python env, this will create an ImportError - shutil.rmtree(sqlmesh_test_macros_package_path) + shutil.rmtree(package_path) # Move the macro inline so its no longer being loaded from a library but still exists with the same signature - (tmp_path / "macros" / "__init__.py").write_text(""" + (project_path / "macros" / "__init__.py").write_text(""" from sqlmesh import macro @macro() @@ -120,9 +178,8 @@ def do_something(evaluator): assert "Physical layer updated" in result.stdout assert "Virtual layer updated" in result.stdout - log_file = sorted(list((tmp_path / "logs").iterdir()))[-1] - log_file_contents = log_file.read_text() - assert "ModuleNotFoundError: No module named 'sqlmesh_test_macros'" in log_file_contents + log_file_contents = last_log_file_contents() + assert f"ModuleNotFoundError: No module named '{package_name}'" in log_file_contents assert ( "ERROR - Failed to cache optimized query for model 'example.test_model'" in log_file_contents @@ -131,3 +188,194 @@ def do_something(evaluator): 'ERROR - Failed to cache snapshot SnapshotId<"db"."example"."test_model"' in log_file_contents ) + + +def test_model_selector_snapshot_references_nonexistent_python_libraries( + invoke_cli: InvokeCliType, + duckdb_example_project: Path, + last_log_file_contents: t.Callable[[], str], + create_site_package: CreateSitePackageType, +) -> None: + """ + Scenario: + - A model is created using a macro that is imported from an external package + - That model is applied + snapshot committed to state + - The external package is removed locally and the import macro import is changed to an inline definition + - Thus, local version of the model can be rendered but the remote version in state cannot + + Outcome: + - `sqlmesh plan --select-model ` should work as it picks up the local version + - `sqlmesh plan --select-model should exit with an error, because the plan needs a valid DAG and the remote version is no longer valid locally + """ + project_path = duckdb_example_project + + # simulate a 3rd party library that provides a macro + package_name, package_path = create_site_package("sqlmesh_test_macros") + (package_path / "macros.py").write_text(""" +from sqlmesh import macro + +@macro() +def do_something(evaluator): + return "'value from site-packages'" +""") + + # reference the macro from site-packages + (project_path / "macros" / "__init__.py").write_text(f""" +from {package_name}.macros import do_something +""") + + (project_path / "models" / "example.sql").write_text(""" +MODEL ( + name sqlmesh_example.test_model, + kind FULL +); + +select @do_something() as a +""") + + result = invoke_cli(["plan", "--no-prompts", "--auto-apply", "--skip-tests"]) + + assert result.returncode == 0 + assert "Physical layer updated" in result.stdout + assert "Virtual layer updated" in result.stdout + + # clear cache to ensure we are forced to reload everything + assert invoke_cli(["clean"]).returncode == 0 + + # deleting this removes the 'do_something()' macro used by the version of the snapshot stored in state + # when loading the old snapshot from state in the local python env, this will create an ImportError + shutil.rmtree(package_path) + + # Move the macro inline so its no longer being loaded from a library but still exists with the same signature + (project_path / "macros" / "__init__.py").write_text(""" +from sqlmesh import macro + +@macro() +def do_something(evaluator): + return "'some value not from site-packages'" +""") + + # the invalid snapshot is in state but is not preventing a plan + result = invoke_cli( + [ + "plan", + "--no-prompts", + "--skip-tests", + ], + input="n", # for the apply backfill (y/n) prompt + ) + assert result.returncode == 0 + assert "Apply - Backfill Tables [y/n]:" in result.stdout + assert "Physical layer updated" not in result.stdout + + # the invalid snapshot in state should not prevent a plan if --select-model is used on it (since the local version can be rendered) + result = invoke_cli( + ["plan", "--select-model", "sqlmesh_example.test_model", "--no-prompts", "--skip-tests"], + input="n", # for the apply backfill (y/n) prompt + ) + assert result.returncode == 0 + assert "ModuleNotFoundError" not in result.stdout + assert "sqlmesh_example.test_model" in result.stdout + assert "Apply - Backfill Tables" in result.stdout + + # the invalid snapshot in state should prevent a plan if --select-model is used on another model + # (since this says to SQLMesh "source everything from state except this selected model" and the plan DAG must be valid to run the plan) + result = invoke_cli( + [ + "plan", + "--select-model", + "sqlmesh_example.full_model", + "--no-prompts", + "--skip-tests", + ], + input="n", # for the apply backfill (y/n) prompt + ) + assert result.returncode == 1 + assert ( + "Model 'sqlmesh_example.test_model' sourced from state cannot be rendered in the local environment" + in result.stdout + ) + assert f"No module named '{package_name}'" in result.stdout + assert ( + "If the model has been fixed locally, please ensure that the --select-model expression includes it" + in result.stdout + ) + + # verify the full stack trace was logged + log_file_contents = last_log_file_contents() + assert f"ModuleNotFoundError: No module named '{package_name}'" in log_file_contents + assert ( + "The above exception was the direct cause of the following exception:" in log_file_contents + ) + + +def test_model_selector_tags_picks_up_both_remote_and_local( + invoke_cli: InvokeCliType, duckdb_example_project: Path +) -> None: + """ + Scenario: + - A model that has already been applied to prod (so exists in state) has a tag added locally + - A new model is created locally that has the same tag + + Outcome: + - `sqlmesh plan --select-model tag:` should include both models + """ + project_path = duckdb_example_project + + # default state of full_model + (project_path / "models" / "full_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.full_model, + kind FULL, + cron '@daily', + grain item_id, + audits (assert_positive_order_ids), + ); + + SELECT + item_id, + COUNT(DISTINCT id) AS num_orders + FROM sqlmesh_example.incremental_model + GROUP BY item_id + """) + + # apply plan - starting point + result = invoke_cli(["plan", "--no-prompts", "--auto-apply", "--skip-tests"]) + + assert result.returncode == 0 + assert "Physical layer updated" in result.stdout + assert "Virtual layer updated" in result.stdout + + # add a new model locally with tag:a + (project_path / "models" / "new_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.new_model, + kind full, + tags (a) + ); + + SELECT 1; + """) + + # update full_model with tag:a + (project_path / "models" / "full_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.full_model, + kind FULL, + tags (a) + ); + + SELECT + item_id, + COUNT(DISTINCT id) AS num_orders + FROM sqlmesh_example.incremental_model + GROUP BY item_id + """) + + result = invoke_cli( + ["plan", "--select-model", "tag:a", "--no-prompts", "--skip-tests"], + input="n", # for the apply backfill (y/n) prompt + ) + assert result.returncode == 0 + assert "sqlmesh_example.full_model" in result.stdout # metadata update: tags + assert "sqlmesh_example.new_model" in result.stdout # added From 9c53116a3b0c760741345acd380fff0923db3916 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 08:23:40 +1200 Subject: [PATCH 0311/1056] Chore(deps): Bump @tanstack/react-router-devtools from 1.120.10 to 1.120.13 (#4621) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 166 +++++++++++++++++++++++++------------- vscode/react/package.json | 2 +- 2 files changed, 109 insertions(+), 59 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d36c3f873c..2a908d9b3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,7 +98,7 @@ importers: version: 4.1.7 '@tailwindcss/vite': specifier: ^4.1.7 - version: 4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)) '@tanstack/react-query': specifier: ^5.77.2 version: 5.77.2(react@18.3.1) @@ -106,14 +106,14 @@ importers: specifier: ^1.120.10 version: 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-router-devtools': - specifier: ^1.120.10 - version: 1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.10)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3) + specifier: ^1.120.13 + version: 1.120.13(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.10)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3) '@tanstack/react-virtual': specifier: ^3.13.9 version: 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-plugin': specifier: ^1.120.10 - version: 1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8) + version: 1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -159,7 +159,7 @@ importers: version: 18.3.7(@types/react@18.3.22) '@vitejs/plugin-react': specifier: ^4.5.0 - version: 4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -168,10 +168,10 @@ importers: version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -310,7 +310,7 @@ importers: version: 18.3.7(@types/react@18.3.22) '@vitejs/plugin-react-swc': specifier: ^3.10.0 - version: 3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + version: 3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)) ajv: specifier: ^8.17.1 version: 8.17.1 @@ -340,13 +340,13 @@ importers: version: 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + version: 3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)) vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) optionalDependencies: '@swc/core-linux-x64-gnu': specifier: ^1.11.29 @@ -550,6 +550,9 @@ packages: '@codemirror/language@6.11.0': resolution: {integrity: sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==} + '@codemirror/language@6.11.1': + resolution: {integrity: sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==} + '@codemirror/legacy-modes@6.5.1': resolution: {integrity: sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==} @@ -568,6 +571,9 @@ packages: '@codemirror/view@6.36.8': resolution: {integrity: sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==} + '@codemirror/view@6.37.1': + resolution: {integrity: sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==} + '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} @@ -1790,11 +1796,11 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.120.10': - resolution: {integrity: sha512-0Pc7ttT44MzcW9S0BE8V5Y4APUVnlTxP84VBTtd8z4MCMFbukiMCNqSQIR/jHJ/6zyGZNhjYIBEzB2Oftgo6QQ==} + '@tanstack/react-router-devtools@1.120.13': + resolution: {integrity: sha512-HP9Qd1SBzN/TaAw5TPeD+9xCytBTZy+HM1kGRimXqSy8aOKlOkPa4fRcPyA+OWpB9CgiLKHDu6UrVVb2ZBAd6w==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.120.10 + '@tanstack/react-router': ^1.120.13 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' @@ -1828,11 +1834,11 @@ packages: resolution: {integrity: sha512-AmEJAYt+6w/790zTnfddVhnK1QJCnd96H4xg1aD65Oohc8+OTQBxgWky/wzqwhHRdkdsBgRT7iWac9x5Y8UrQA==} engines: {node: '>=12'} - '@tanstack/router-devtools-core@1.120.10': - resolution: {integrity: sha512-fysPrKH7dL/G/guHm0HN+ceFEBZnbKaU9R8KZHo/Qzue7WxQV+g4or2EWnbBJ8/aF+C/WYgxR1ATFqfZEjHSfg==} + '@tanstack/router-devtools-core@1.120.13': + resolution: {integrity: sha512-H9Yt6BXUfdcKURo9FMBw4idZVCggWlKaxNdJjfu8Or7Wu6Zi58kRt17RJ4ys/n6D1WznyteQ3ya2JZpan5n2NA==} engines: {node: '>=12'} peerDependencies: - '@tanstack/router-core': ^1.120.10 + '@tanstack/router-core': ^1.120.13 csstype: ^3.0.10 solid-js: '>=1.9.5' tiny-invariant: ^1.3.3 @@ -2589,6 +2595,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.25.0: + resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -2644,6 +2655,9 @@ packages: caniuse-lite@1.0.30001718: resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} + caniuse-lite@1.0.30001720: + resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -3022,6 +3036,9 @@ packages: electron-to-chromium@1.5.157: resolution: {integrity: sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==} + electron-to-chromium@1.5.161: + resolution: {integrity: sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==} + elkjs@0.8.2: resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} @@ -5170,8 +5187,8 @@ packages: uglify-js: optional: true - terser@5.39.2: - resolution: {integrity: sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==} + terser@5.40.0: + resolution: {integrity: sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==} engines: {node: '>=10'} hasBin: true @@ -5609,8 +5626,8 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - webpack-sources@3.3.0: - resolution: {integrity: sha512-77R0RDmJfj9dyv5p3bM5pOHa+X8/ZkO9c7kpDstigkC4nIDobadsfSGCwB4bKhMVxqAok8tajaoR8rirM7+VFQ==} + webpack-sources@3.3.2: + resolution: {integrity: sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==} engines: {node: '>=10.13.0'} webpack-virtual-modules@0.6.2: @@ -6105,6 +6122,15 @@ snapshots: '@lezer/lr': 1.4.2 style-mod: 4.1.2 + '@codemirror/language@6.11.1': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.37.1 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + style-mod: 4.1.2 + '@codemirror/legacy-modes@6.5.1': dependencies: '@codemirror/language': 6.11.0 @@ -6112,13 +6138,13 @@ snapshots: '@codemirror/lint@6.8.5': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.8 + '@codemirror/view': 6.37.1 crelt: 1.0.6 '@codemirror/search@6.5.10': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.8 + '@codemirror/view': 6.37.1 crelt: 1.0.6 '@codemirror/state@6.5.2': @@ -6127,9 +6153,9 @@ snapshots: '@codemirror/theme-one-dark@6.1.2': dependencies: - '@codemirror/language': 6.11.0 + '@codemirror/language': 6.11.1 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.8 + '@codemirror/view': 6.37.1 '@lezer/highlight': 1.2.1 '@codemirror/view@6.36.8': @@ -6138,6 +6164,13 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 + '@codemirror/view@6.37.1': + dependencies: + '@codemirror/state': 6.5.2 + crelt: 1.0.6 + style-mod: 4.1.2 + w3c-keyname: 2.2.8 + '@csstools/color-helpers@5.0.2': {} '@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': @@ -7426,12 +7459,12 @@ snapshots: postcss: 8.5.3 tailwindcss: 4.1.7 - '@tailwindcss/vite@4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': + '@tailwindcss/vite@4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@tailwindcss/node': 4.1.7 '@tailwindcss/oxide': 4.1.7 tailwindcss: 4.1.7 - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) '@tanstack/history@1.115.0': {} @@ -7442,10 +7475,10 @@ snapshots: '@tanstack/query-core': 5.77.2 react: 18.3.1 - '@tanstack/react-router-devtools@1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.10)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3)': + '@tanstack/react-router-devtools@1.120.13(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.10)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3)': dependencies: '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-devtools-core': 1.120.10(@tanstack/router-core@1.120.10)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3) + '@tanstack/router-devtools-core': 1.120.13(@tanstack/router-core@1.120.10)(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) solid-js: 1.9.7 @@ -7490,7 +7523,7 @@ snapshots: '@tanstack/store': 0.7.0 tiny-invariant: 1.3.3 - '@tanstack/router-devtools-core@1.120.10(@tanstack/router-core@1.120.10)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': + '@tanstack/router-devtools-core@1.120.13(@tanstack/router-core@1.120.10)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': dependencies: '@tanstack/router-core': 1.120.10 clsx: 2.1.1 @@ -7509,7 +7542,7 @@ snapshots: optionalDependencies: '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-plugin@1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8)': + '@tanstack/router-plugin@1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8)': dependencies: '@babel/core': 7.27.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1) @@ -7530,7 +7563,7 @@ snapshots: zod: 3.25.28 optionalDependencies: '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) webpack: 5.99.8 transitivePeerDependencies: - supports-color @@ -7965,15 +7998,15 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react-swc@3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.9 '@swc/core': 1.11.29(@swc/helpers@0.5.17) - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react@4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.1 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) @@ -7981,7 +8014,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.9 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -7992,13 +8025,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': + '@vitest/mocker@3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.1.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) '@vitest/pretty-format@3.1.4': dependencies: @@ -8416,6 +8449,13 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.5) + browserslist@4.25.0: + dependencies: + caniuse-lite: 1.0.30001720 + electron-to-chromium: 1.5.161 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.0) + buffer-crc32@0.2.13: {} buffer-equal-constant-time@1.0.1: {} @@ -8475,6 +8515,8 @@ snapshots: caniuse-lite@1.0.30001718: {} + caniuse-lite@1.0.30001720: {} + ccount@2.0.1: {} chai@5.2.0: @@ -8593,11 +8635,11 @@ snapshots: dependencies: '@codemirror/autocomplete': 6.18.6 '@codemirror/commands': 6.8.1 - '@codemirror/language': 6.11.0 + '@codemirror/language': 6.11.1 '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.8 + '@codemirror/view': 6.37.1 color-convert@1.9.3: dependencies: @@ -8852,6 +8894,8 @@ snapshots: electron-to-chromium@1.5.157: {} + electron-to-chromium@1.5.161: {} + elkjs@0.8.2: {} emoji-regex@10.4.0: {} @@ -11405,7 +11449,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.39.2 + terser: 5.40.0 webpack: 5.99.8(esbuild@0.25.4) optionalDependencies: esbuild: 0.25.4 @@ -11416,11 +11460,11 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.39.2 + terser: 5.40.0 webpack: 5.99.8 optional: true - terser@5.39.2: + terser@5.40.0: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.14.1 @@ -11678,6 +11722,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.1.3(browserslist@4.25.0): + dependencies: + browserslist: 4.25.0 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -11734,13 +11784,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 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@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -11755,11 +11805,11 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)): + vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)): dependencies: - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) - vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.4 fdir: 6.4.4(picomatch@4.0.2) @@ -11772,14 +11822,14 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.39.2 + terser: 5.40.0 tsx: 4.19.4 yaml: 2.8.0 - vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.4 - '@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)) '@vitest/pretty-format': 3.1.4 '@vitest/runner': 3.1.4 '@vitest/snapshot': 3.1.4 @@ -11796,8 +11846,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + vite-node: 3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -11853,7 +11903,7 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-sources@3.3.0: {} + webpack-sources@3.3.2: {} webpack-virtual-modules@0.6.2: {} @@ -11866,7 +11916,7 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.14.1 - browserslist: 4.24.5 + browserslist: 4.25.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 es-module-lexer: 1.7.0 @@ -11882,7 +11932,7 @@ snapshots: tapable: 2.2.2 terser-webpack-plugin: 5.3.14(webpack@5.99.8) watchpack: 2.4.4 - webpack-sources: 3.3.0 + webpack-sources: 3.3.2 transitivePeerDependencies: - '@swc/core' - esbuild @@ -11898,7 +11948,7 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.14.1 - browserslist: 4.24.5 + browserslist: 4.25.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 es-module-lexer: 1.7.0 @@ -11914,7 +11964,7 @@ snapshots: tapable: 2.2.2 terser-webpack-plugin: 5.3.14(esbuild@0.25.4)(webpack@5.99.8(esbuild@0.25.4)) watchpack: 2.4.4 - webpack-sources: 3.3.0 + webpack-sources: 3.3.2 transitivePeerDependencies: - '@swc/core' - esbuild diff --git a/vscode/react/package.json b/vscode/react/package.json index a150c0ee53..2ebac7fe8f 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -20,7 +20,7 @@ "@tailwindcss/vite": "^4.1.7", "@tanstack/react-query": "^5.77.2", "@tanstack/react-router": "^1.120.10", - "@tanstack/react-router-devtools": "^1.120.10", + "@tanstack/react-router-devtools": "^1.120.13", "@tanstack/react-virtual": "^3.13.9", "@tanstack/router-plugin": "^1.120.10", "apache-arrow": "^19.0.1", From 0da7d7e09543ce9c752ceb9b42ebbf79c78bd49c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 13:24:43 -0700 Subject: [PATCH 0312/1056] Chore(deps): Bump @codemirror/view from 6.36.8 to 6.37.1 (#4620) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 44 +++++++++++++++++++++++++---------------- web/client/package.json | 2 +- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a908d9b3d..fc8a83ff4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,8 +200,8 @@ importers: specifier: ^6.5.2 version: 6.5.2 '@codemirror/view': - specifier: ^6.36.8 - version: 6.36.8 + specifier: ^6.37.1 + version: 6.37.1 '@headlessui/react': specifier: ^2.2.4 version: 2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -234,7 +234,7 @@ importers: version: 2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@uiw/react-codemirror': specifier: ^4.23.12 - version: 4.23.12(@babel/runtime@7.27.1)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.8)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.23.12(@babel/runtime@7.27.1)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -276,7 +276,7 @@ importers: version: 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) thememirror: specifier: ^2.0.1 - version: 2.0.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.8) + version: 2.0.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1) zustand: specifier: ^5.0.5 version: 5.0.5(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) @@ -568,8 +568,8 @@ packages: '@codemirror/theme-one-dark@6.1.2': resolution: {integrity: sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==} - '@codemirror/view@6.36.8': - resolution: {integrity: sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==} + '@codemirror/view@6.37.1': + resolution: {integrity: sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==} '@codemirror/view@6.37.1': resolution: {integrity: sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==} @@ -6086,14 +6086,14 @@ snapshots: dependencies: '@codemirror/language': 6.11.0 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.8 + '@codemirror/view': 6.37.1 '@lezer/common': 1.2.3 '@codemirror/commands@6.8.1': dependencies: '@codemirror/language': 6.11.0 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.8 + '@codemirror/view': 6.37.1 '@lezer/common': 1.2.3 '@codemirror/lang-python@6.2.1': @@ -6116,7 +6116,16 @@ snapshots: '@codemirror/language@6.11.0': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.8 + '@codemirror/view': 6.37.1 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + style-mod: 4.1.2 + + '@codemirror/language@6.11.1': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.37.1 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 @@ -6158,9 +6167,10 @@ snapshots: '@codemirror/view': 6.37.1 '@lezer/highlight': 1.2.1 - '@codemirror/view@6.36.8': + '@codemirror/view@6.37.1': dependencies: '@codemirror/state': 6.5.2 + crelt: 1.0.6 style-mod: 4.1.2 w3c-keyname: 2.2.8 @@ -7969,7 +7979,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@uiw/codemirror-extensions-basic-setup@4.23.12(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.36.8)': + '@uiw/codemirror-extensions-basic-setup@4.23.12(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1)': dependencies: '@codemirror/autocomplete': 6.18.6 '@codemirror/commands': 6.8.1 @@ -7977,16 +7987,16 @@ snapshots: '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.8 + '@codemirror/view': 6.37.1 - '@uiw/react-codemirror@4.23.12(@babel/runtime@7.27.1)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.8)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@uiw/react-codemirror@4.23.12(@babel/runtime@7.27.1)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.27.1 '@codemirror/commands': 6.8.1 '@codemirror/state': 6.5.2 '@codemirror/theme-one-dark': 6.1.2 - '@codemirror/view': 6.36.8 - '@uiw/codemirror-extensions-basic-setup': 4.23.12(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.36.8) + '@codemirror/view': 6.37.1 + '@uiw/codemirror-extensions-basic-setup': 4.23.12(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1) codemirror: 6.0.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -11481,11 +11491,11 @@ snapshots: textextensions@5.16.0: {} - thememirror@2.0.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.8): + thememirror@2.0.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1): dependencies: '@codemirror/language': 6.11.0 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.36.8 + '@codemirror/view': 6.37.1 thenify-all@1.6.0: dependencies: diff --git a/web/client/package.json b/web/client/package.json index 845a120a9b..3d886e239e 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -22,7 +22,7 @@ "@codemirror/language": "^6.11.0", "@codemirror/legacy-modes": "^6.5.1", "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.36.8", + "@codemirror/view": "^6.37.1", "@headlessui/react": "^2.2.4", "@heroicons/react": "^2.2.0", "@lit/react": "^1.0.7", From 9a1ee1fed3fc8aa9b2ff2e8c299a2ddb1fc86bb8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 21:33:49 +0100 Subject: [PATCH 0313/1056] Chore(deps): Bump zod from 3.25.28 to 3.25.48 (#4624) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 33 ++++++++++++++++++++++++++------- vscode/extension/package.json | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc8a83ff4d..c44f20404d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: ^9.0.1 version: 9.0.1 zod: - specifier: ^3.25.28 - version: 3.25.28 + specifier: ^3.25.48 + version: 3.25.48 devDependencies: '@eslint/js': specifier: ^9.27.0 @@ -574,6 +574,9 @@ packages: '@codemirror/view@6.37.1': resolution: {integrity: sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==} + '@codemirror/view@6.37.1': + resolution: {integrity: sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==} + '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} @@ -5786,8 +5789,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@3.25.28: - resolution: {integrity: sha512-/nt/67WYKnr5by3YS7LroZJbtcCBurDKKPBPWWzaxvVCGuG/NOsiKkrjoOhI8mJ+SQUXEbUzeB3S+6XDUEEj7Q==} + zod@3.25.48: + resolution: {integrity: sha512-0X1mz8FtgEIvaxGjdIImYpZEaZMrund9pGXm3M6vM7Reba0e2eI71KPjSCGXBfwKDPwPoywf6waUKc3/tFvX2Q==} zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} @@ -6140,6 +6143,15 @@ snapshots: '@lezer/lr': 1.4.2 style-mod: 4.1.2 + '@codemirror/language@6.11.1': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.37.1 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + style-mod: 4.1.2 + '@codemirror/legacy-modes@6.5.1': dependencies: '@codemirror/language': 6.11.0 @@ -6181,6 +6193,13 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 + '@codemirror/view@6.37.1': + dependencies: + '@codemirror/state': 6.5.2 + crelt: 1.0.6 + style-mod: 4.1.2 + w3c-keyname: 2.2.8 + '@csstools/color-helpers@5.0.2': {} '@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': @@ -7548,7 +7567,7 @@ snapshots: '@tanstack/virtual-file-routes': 1.115.0 prettier: 3.5.3 tsx: 4.19.4 - zod: 3.25.28 + zod: 3.25.48 optionalDependencies: '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -7570,7 +7589,7 @@ snapshots: babel-dead-code-elimination: 1.0.10 chokidar: 3.6.0 unplugin: 2.3.4 - zod: 3.25.28 + zod: 3.25.48 optionalDependencies: '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) @@ -12133,7 +12152,7 @@ snapshots: yocto-queue@0.1.0: {} - zod@3.25.28: {} + zod@3.25.48: {} zustand@4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1): dependencies: diff --git a/vscode/extension/package.json b/vscode/extension/package.json index dc80ac6399..fedc937bd4 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -127,7 +127,7 @@ "fs-extra": "^11.3.0", "vscode-jsonrpc": "^8.2.1", "vscode-languageclient": "^9.0.1", - "zod": "^3.25.28" + "zod": "^3.25.48" }, "devDependencies": { "@eslint/js": "^9.27.0", From 079f2a53adf391478ca9018f559cbc44fec5411b Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 2 Jun 2025 21:57:12 +0100 Subject: [PATCH 0314/1056] chore(vscode): reload project on save (#4602) --- sqlmesh/lsp/main.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 203a208bb9..9ed3ed89c6 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -199,6 +199,17 @@ def did_change(ls: LanguageServer, params: types.DidChangeTextDocumentParams) -> @self.server.feature(types.TEXT_DOCUMENT_DID_SAVE) def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> None: uri = URI(params.text_document.uri) + + # Reload the entire context and create a new LSPContext + if self.lsp_context is not None: + try: + new_context = Context(paths=list(self.lsp_context.context.configs)) + new_full_context = LSPContext(new_context) + self.lsp_context = new_full_context + return + except Exception as e: + pass + context = self._context_get_or_load(uri) models = context.map[uri.to_path()] if models is None or not isinstance(models, ModelTarget): From a4d22716ce98712101e997a6f64e7f284cb737e1 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 2 Jun 2025 22:26:54 +0100 Subject: [PATCH 0315/1056] chore: add typing to vscode tooling files (#4626) --- tooling/vscode/launch.json | 1 + tooling/vscode/tasks.json | 1 + 2 files changed, 2 insertions(+) diff --git a/tooling/vscode/launch.json b/tooling/vscode/launch.json index ee2f759dbf..76f55db912 100644 --- a/tooling/vscode/launch.json +++ b/tooling/vscode/launch.json @@ -3,6 +3,7 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { + "$schema": "vscode://schemas/launch", "version": "0.2.0", "configurations": [ { diff --git a/tooling/vscode/tasks.json b/tooling/vscode/tasks.json index 326fa8f3c5..18c5042471 100644 --- a/tooling/vscode/tasks.json +++ b/tooling/vscode/tasks.json @@ -1,6 +1,7 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format { + "$schema": "vscode://schemas/tasks", "version": "2.0.0", "tasks": [ { From 5d32c2947c5b9b9e5040cf8197cf4fb58c88cfd3 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 2 Jun 2025 22:54:56 +0100 Subject: [PATCH 0316/1056] chore(lsp): format test files (#4628) --- .prettierignore | 2 +- vscode/extension/tests/diagnostics.spec.ts | 87 ++++---- .../extension/tests/go_to_definition.spec.ts | 147 ++++++++------ vscode/extension/tests/lineage.spec.ts | 185 ++++++++++-------- vscode/extension/tests/render.spec.ts | 181 ++++++++++------- vscode/extension/tests/utils.ts | 96 +++++---- 6 files changed, 400 insertions(+), 298 deletions(-) diff --git a/.prettierignore b/.prettierignore index 64e19df420..67d4b9aa77 100644 --- a/.prettierignore +++ b/.prettierignore @@ -23,7 +23,7 @@ vscode/extension/.vscode-test/ sqlmesh docs -tests +/tests/** examples posts .circleci diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts index 39c60c3369..8fe02058ff 100644 --- a/vscode/extension/tests/diagnostics.spec.ts +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -1,43 +1,48 @@ -import { test } from '@playwright/test'; -import path from 'path'; -import fs from 'fs-extra'; -import os from 'os'; -import { startVSCode, SUSHI_SOURCE_PATH } from './utils'; +import { test } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { startVSCode, SUSHI_SOURCE_PATH } from './utils' test('Workspace diagnostics show up in the diagnostics panel', async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); - await fs.copy(SUSHI_SOURCE_PATH, tempDir); - - const configPath = path.join(tempDir, 'config.py'); - const configContent = await fs.readFile(configPath, 'utf8'); - const updatedContent = configContent.replace('enabled=False', 'enabled=True'); - await fs.writeFile(configPath, updatedContent); - - try { - const { window, close } = await startVSCode(tempDir); - - // Wait for the models folder to be visible - await window.waitForSelector('text=models'); - - // Click on the models folder, excluding external_models - await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); - - // Open the customer_revenue_lifetime model - await window.getByRole('treeitem', { name: 'customers.sql', exact: true }).locator('a').click(); - - await - - // Open problems panel - await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); - await window.keyboard.type('View: Focus Problems'); - await window.keyboard.press('Enter'); - - - await window.waitForSelector('text=problems'); - await window.waitForSelector("text=All models should have an owner"); - - await close(); - } finally { - await fs.remove(tempDir); - } - }); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + const configPath = path.join(tempDir, 'config.py') + const configContent = await fs.readFile(configPath, 'utf8') + const updatedContent = configContent.replace('enabled=False', 'enabled=True') + await fs.writeFile(configPath, updatedContent) + + try { + const { window, close } = await startVSCode(tempDir) + + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customer_revenue_lifetime model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await // Open problems panel + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('View: Focus Problems') + await window.keyboard.press('Enter') + + await window.waitForSelector('text=problems') + await window.waitForSelector('text=All models should have an owner') + + await close() + } finally { + await fs.remove(tempDir) + } +}) diff --git a/vscode/extension/tests/go_to_definition.spec.ts b/vscode/extension/tests/go_to_definition.spec.ts index f392c710bd..e634689843 100644 --- a/vscode/extension/tests/go_to_definition.spec.ts +++ b/vscode/extension/tests/go_to_definition.spec.ts @@ -1,68 +1,85 @@ -import { test, expect } from '@playwright/test'; -import path from 'path'; -import fs from 'fs-extra'; -import os from 'os'; -import { startVSCode, SUSHI_SOURCE_PATH } from './utils'; +import { test, expect } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { startVSCode, SUSHI_SOURCE_PATH } from './utils' test('Go to definition for macro', async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); - await fs.copy(SUSHI_SOURCE_PATH, tempDir); - - try { - const { window, close } = await startVSCode(tempDir); - - // Wait for the models folder to be visible - await window.waitForSelector('text=models'); - - // Click on the models folder - await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); - - // Open the customer_revenue_lifetime model - await window.getByRole('treeitem', { name: 'top_waiters.sql', exact: true }).locator('a').click(); - - await window.waitForSelector('text=grain'); - await window.waitForSelector('text=Loaded SQLMesh Context') - - // Render the model - await window.locator("text=@MULTIPLY").click({ - modifiers: ["Meta"] - }) - - // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window - await expect(window.locator('text=def multiply(')).toBeVisible(); - - await close(); - } finally { - await fs.removeSync(tempDir); - } - }); - -test("Go to definition for model", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); - await fs.copy(SUSHI_SOURCE_PATH, tempDir); - - try { - const { window, close } = await startVSCode(tempDir); - - // Wait for the models folder to be visible - await window.waitForSelector('text=models'); - - // Click on the models folder - await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); - - // Open the top_waiters model - await window.getByRole('treeitem', { name: 'top_waiters.sql', exact: true }).locator('a').click(); - - await window.waitForSelector('text=grain'); - await window.waitForSelector('text=Loaded SQLMesh Context') - - // Go to definition for the model - await window.locator("text=sushi.waiter_revenue_by_day").first().click({ - modifiers: ["Meta"] + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + try { + const { window, close } = await startVSCode(tempDir) + + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customer_revenue_lifetime model + await window + .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Render the model + await window.locator('text=@MULTIPLY').click({ + modifiers: ['Meta'], + }) + + // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window + await expect(window.locator('text=def multiply(')).toBeVisible() + + await close() + } finally { + await fs.removeSync(tempDir) + } +}) + +test('Go to definition for model', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + try { + const { window, close } = await startVSCode(tempDir) + + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Go to definition for the model + await window + .locator('text=sushi.waiter_revenue_by_day') + .first() + .click({ + modifiers: ['Meta'], }) - await expect(window.locator('text=SUM(oi.quantity * i.price)::DOUBLE AS revenue')).toBeVisible(); - await close(); - } finally { - await fs.removeSync(tempDir); - } -}) \ No newline at end of file + await expect( + window.locator('text=SUM(oi.quantity * i.price)::DOUBLE AS revenue'), + ).toBeVisible() + await close() + } finally { + await fs.removeSync(tempDir) + } +}) diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index 3231b61623..1c621f45b4 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -1,122 +1,143 @@ -import { test, expect, Page } from '@playwright/test'; -import path from 'path'; -import fs from 'fs-extra'; -import os from 'os'; -import { startVSCode, SUSHI_SOURCE_PATH } from './utils'; - +import { test, expect, Page } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { startVSCode, SUSHI_SOURCE_PATH } from './utils' /** * Helper function to launch VS Code and test lineage with given project path config */ -async function testLineageWithProjectPath( - window: Page, -): Promise { - // Trigger lineage command - await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); - await window.keyboard.type('Lineage: Focus On View'); - await window.keyboard.press('Enter'); - - // Wait for "Loaded SQLmesh Context" text to appear - const loadedContextText = window.locator('text=Loaded SQLMesh Context'); - await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 }); +async function testLineageWithProjectPath(window: Page): Promise { + // Trigger lineage command + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Lineage: Focus On View') + await window.keyboard.press('Enter') + + // Wait for "Loaded SQLmesh Context" text to appear + const loadedContextText = window.locator('text=Loaded SQLMesh Context') + await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 }) } - test('Lineage panel renders correctly - no project path config (default)', async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); - await fs.copy(SUSHI_SOURCE_PATH, tempDir); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) try { - const { window, close } = await startVSCode(tempDir); - await testLineageWithProjectPath(window); - await close(); - } finally { - await fs.remove(tempDir); + const { window, close } = await startVSCode(tempDir) + await testLineageWithProjectPath(window) + await close() + } finally { + await fs.remove(tempDir) } -}); +}) test('Lineage panel renders correctly - relative project path', async () => { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-')); - const projectDir = path.join(workspaceDir, 'projects', 'sushi'); - await fs.copy(SUSHI_SOURCE_PATH, projectDir); + const workspaceDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-workspace-'), + ) + const projectDir = path.join(workspaceDir, 'projects', 'sushi') + await fs.copy(SUSHI_SOURCE_PATH, projectDir) const settings = { - "sqlmesh.projectPath": "./projects/sushi", - }; - await fs.ensureDir(path.join(workspaceDir, '.vscode')); - await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 }); + 'sqlmesh.projectPath': './projects/sushi', + } + await fs.ensureDir(path.join(workspaceDir, '.vscode')) + await fs.writeJson( + path.join(workspaceDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) try { - const { window, close } = await startVSCode(workspaceDir); - await testLineageWithProjectPath(window); - await close(); + const { window, close } = await startVSCode(workspaceDir) + await testLineageWithProjectPath(window) + await close() } finally { - await fs.remove(workspaceDir); + await fs.remove(workspaceDir) } -}); +}) test('Lineage panel renders correctly - absolute project path', async () => { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-')); - const projectDir = path.join(workspaceDir, 'projects', 'sushi'); - await fs.ensureDir(path.join(workspaceDir, '.vscode')); - await fs.copy(SUSHI_SOURCE_PATH, projectDir); - await fs.ensureDir(path.join(workspaceDir, '.vscode')); + const workspaceDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-workspace-'), + ) + const projectDir = path.join(workspaceDir, 'projects', 'sushi') + await fs.ensureDir(path.join(workspaceDir, '.vscode')) + await fs.copy(SUSHI_SOURCE_PATH, projectDir) + await fs.ensureDir(path.join(workspaceDir, '.vscode')) const settings = { - "sqlmesh.projectPath": projectDir, - }; - await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 }); + 'sqlmesh.projectPath': projectDir, + } + await fs.writeJson( + path.join(workspaceDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) try { - const { window, close } = await startVSCode(workspaceDir); - await testLineageWithProjectPath(window); - await close(); + const { window, close } = await startVSCode(workspaceDir) + await testLineageWithProjectPath(window) + await close() } finally { - await fs.remove(workspaceDir); + await fs.remove(workspaceDir) } -}); - +}) -test("Lineage panel renders correctly - relative project outside of workspace", async () => { - const tempFolder = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-')); - const projectDir = path.join(tempFolder, 'projects', 'sushi'); - await fs.copy(SUSHI_SOURCE_PATH, projectDir); +test('Lineage panel renders correctly - relative project outside of workspace', async () => { + const tempFolder = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-workspace-'), + ) + const projectDir = path.join(tempFolder, 'projects', 'sushi') + await fs.copy(SUSHI_SOURCE_PATH, projectDir) - const workspaceDir = path.join(tempFolder, 'workspace'); - await fs.ensureDir(workspaceDir); + const workspaceDir = path.join(tempFolder, 'workspace') + await fs.ensureDir(workspaceDir) const settings = { - "sqlmesh.projectPath": "./../projects/sushi", - }; - await fs.ensureDir(path.join(workspaceDir, '.vscode')); - await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 }); + 'sqlmesh.projectPath': './../projects/sushi', + } + await fs.ensureDir(path.join(workspaceDir, '.vscode')) + await fs.writeJson( + path.join(workspaceDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) try { - const { window, close } = await startVSCode(workspaceDir); - await testLineageWithProjectPath(window); - await close(); + const { window, close } = await startVSCode(workspaceDir) + await testLineageWithProjectPath(window) + await close() } finally { - await fs.remove(tempFolder); + await fs.remove(tempFolder) } -}); +}) -test("Lineage panel renders correctly - absolute path project outside of workspace", async () => { - const tempFolder = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-')); - const projectDir = path.join(tempFolder, 'projects', 'sushi'); - await fs.copy(SUSHI_SOURCE_PATH, projectDir); +test('Lineage panel renders correctly - absolute path project outside of workspace', async () => { + const tempFolder = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-workspace-'), + ) + const projectDir = path.join(tempFolder, 'projects', 'sushi') + await fs.copy(SUSHI_SOURCE_PATH, projectDir) - const workspaceDir = path.join(tempFolder, 'workspace'); - await fs.ensureDir(workspaceDir); + const workspaceDir = path.join(tempFolder, 'workspace') + await fs.ensureDir(workspaceDir) const settings = { - "sqlmesh.projectPath": projectDir, - }; - await fs.ensureDir(path.join(workspaceDir, '.vscode')); - await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 }); + 'sqlmesh.projectPath': projectDir, + } + await fs.ensureDir(path.join(workspaceDir, '.vscode')) + await fs.writeJson( + path.join(workspaceDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) try { - const { window, close } = await startVSCode(workspaceDir); - await testLineageWithProjectPath(window); - await close(); + const { window, close } = await startVSCode(workspaceDir) + await testLineageWithProjectPath(window) + await close() } finally { - await fs.remove(tempFolder); + await fs.remove(tempFolder) } -}); \ No newline at end of file +}) diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts index dd54ebb916..6846b3eca8 100644 --- a/vscode/extension/tests/render.spec.ts +++ b/vscode/extension/tests/render.spec.ts @@ -1,114 +1,155 @@ -import { test, expect } from '@playwright/test'; -import path from 'path'; -import fs from 'fs-extra'; -import os from 'os'; -import { startVSCode, SUSHI_SOURCE_PATH } from './utils'; +import { test, expect } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { startVSCode, SUSHI_SOURCE_PATH } from './utils' test('Render works correctly', async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); - await fs.copy(SUSHI_SOURCE_PATH, tempDir); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) - try { - const { window, close } = await startVSCode(tempDir); - - // Wait for the models folder to be visible - await window.waitForSelector('text=models'); - - // Click on the models folder, excluding external_models - await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); - - // Open the customer_revenue_lifetime model - await window.getByRole('treeitem', { name: 'customers.sql', exact: true }).locator('a').click(); - - await window.waitForSelector('text=grain'); - await window.waitForSelector('text=Loaded SQLMesh Context') + try { + const { window, close } = await startVSCode(tempDir) - // Render the model - await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); - await window.keyboard.type('Render Model'); - await window.keyboard.press('Enter'); + // Wait for the models folder to be visible + await window.waitForSelector('text=models') - // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window - await expect(window.locator('text="marketing"."customer_id" AS')).toBeVisible(); - await expect(window.locator('text=sushi.customers (rendered)')).toBeVisible(); + // Click on the models folder, excluding external_models + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customer_revenue_lifetime model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') - await close(); - } finally { - await fs.remove(tempDir); - } - }); + // Render the model + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Render Model') + await window.keyboard.press('Enter') + + // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window + await expect( + window.locator('text="marketing"."customer_id" AS'), + ).toBeVisible() + await expect( + window.locator('text=sushi.customers (rendered)'), + ).toBeVisible() + + await close() + } finally { + await fs.remove(tempDir) + } +}) test('Render works correctly with model without a description', async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); - await fs.copy(SUSHI_SOURCE_PATH, tempDir); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) try { - const { window, close } = await startVSCode(tempDir); + const { window, close } = await startVSCode(tempDir) // Wait for the models folder to be visible - await window.waitForSelector('text=models'); + await window.waitForSelector('text=models') // Click on the models folder, excluding external_models - await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() // Open the latest_order model - await window.getByRole('treeitem', { name: 'latest_order.sql', exact: true }).locator('a').click(); + await window + .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) + .locator('a') + .click() - await window.waitForSelector('text=custom_full_with_custom_kind'); + await window.waitForSelector('text=custom_full_with_custom_kind') await window.waitForSelector('text=Loaded SQLMesh Context') // Render the model - await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); - await window.keyboard.type('Render Model'); - await window.keyboard.press('Enter'); + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Render Model') + await window.keyboard.press('Enter') // Check if the model is rendered correctly - await expect(window.locator('text="orders"."id" AS "id",')).toBeVisible(); - await expect(window.locator('text=sushi.latest_order (rendered)')).toBeVisible(); + await expect(window.locator('text="orders"."id" AS "id",')).toBeVisible() + await expect( + window.locator('text=sushi.latest_order (rendered)'), + ).toBeVisible() - await close(); + await close() } finally { - await fs.remove(tempDir); + await fs.remove(tempDir) } -}); +}) test('Render works correctly with every rendered model opening a new tab', async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')); - await fs.copy(SUSHI_SOURCE_PATH, tempDir); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) try { - const { window, close } = await startVSCode(tempDir); + const { window, close } = await startVSCode(tempDir) // Wait for the models folder to be visible - await window.waitForSelector('text=models'); - await window.getByRole('treeitem', { name: 'models', exact: true }).locator('a').click(); - await window.getByRole('treeitem', { name: 'latest_order.sql', exact: true }).locator('a').click(); - await window.waitForSelector('text=custom_full_with_custom_kind'); + await window.waitForSelector('text=models') + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await window + .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) + .locator('a') + .click() + await window.waitForSelector('text=custom_full_with_custom_kind') await window.waitForSelector('text=Loaded SQLMesh Context') // Render the model - await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); - await window.keyboard.type('Render Model'); - await window.keyboard.press('Enter'); + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Render Model') + await window.keyboard.press('Enter') // Check if the model is rendered correctly - await expect(window.locator('text=sushi.latest_order (rendered)')).toBeVisible(); + await expect( + window.locator('text=sushi.latest_order (rendered)'), + ).toBeVisible() // Open the customers model - await window.getByRole('treeitem', { name: 'customers.sql', exact: true }).locator('a').click(); - await window.waitForSelector('text=grain'); + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + await window.waitForSelector('text=grain') // Render the customers model - await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); - await window.keyboard.type('Render Model'); - await window.keyboard.press('Enter'); + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Render Model') + await window.keyboard.press('Enter') // Assert both tabs exist - await expect(window.locator('text=sushi.latest_order (rendered)')).toBeVisible(); - await expect(window.locator('text=sushi.customers (rendered)')).toBeVisible(); - - await close(); + await expect( + window.locator('text=sushi.latest_order (rendered)'), + ).toBeVisible() + await expect( + window.locator('text=sushi.customers (rendered)'), + ).toBeVisible() + + await close() } finally { - await fs.remove(tempDir); + await fs.remove(tempDir) } -}) \ No newline at end of file +}) diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index af4e0bb7fb..92fb91928c 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -1,50 +1,68 @@ -import path from 'path'; -import fs from 'fs-extra'; -import os from 'os'; -import { _electron as electron, Page } from '@playwright/test'; +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { _electron as electron, Page } from '@playwright/test' // Absolute path to the VS Code executable you downloaded in step 1. -export const VS_CODE_EXE = fs.readJsonSync('.vscode-test/paths.json').executablePath; +export const VS_CODE_EXE = fs.readJsonSync( + '.vscode-test/paths.json', +).executablePath // Where your extension lives on disk -export const EXT_PATH = path.resolve(__dirname, '..'); +export const EXT_PATH = path.resolve(__dirname, '..') // Where the sushi project lives which we copy from -export const SUSHI_SOURCE_PATH = path.join(__dirname, '..', '..', '..', 'examples', 'sushi'); +export const SUSHI_SOURCE_PATH = path.join( + __dirname, + '..', + '..', + '..', + 'examples', + 'sushi', +) /** * Launch VS Code and return the window and a function to close the app. * @param workspaceDir The workspace directory to open. * @returns The window and a function to close the app. */ -export const startVSCode = async (workspaceDir: string): Promise<{ - window: Page, - close: () => Promise, - }> => { - const userDataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-user-data-')); - const ciArgs = process.env.CI ? [ - '--disable-gpu', - '--headless', - '--no-sandbox', - '--disable-dev-shm-usage', - '--window-position=-10000,0', - ] : []; - const args = [ - ...ciArgs, - `--extensionDevelopmentPath=${EXT_PATH}`, - '--disable-workspace-trust', - '--disable-telemetry', - '--install-extension=ms-python.python', - `--user-data-dir=${userDataDir}`, - workspaceDir, - ]; - const electronApp = await electron.launch({ - executablePath: VS_CODE_EXE, - args, - }); - const window = await electronApp.firstWindow(); - await window.waitForLoadState('domcontentloaded'); - await window.waitForLoadState('networkidle'); - return { window, close: async () => { - await electronApp.close(); - await fs.removeSync(userDataDir); - } }; +export const startVSCode = async ( + workspaceDir: string, +): Promise<{ + window: Page + close: () => Promise +}> => { + const userDataDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-user-data-'), + ) + const ciArgs = process.env.CI + ? [ + '--disable-gpu', + '--headless', + '--no-sandbox', + '--disable-dev-shm-usage', + '--window-position=-10000,0', + ] + : [] + const args = [ + ...ciArgs, + `--extensionDevelopmentPath=${EXT_PATH}`, + '--disable-workspace-trust', + '--disable-telemetry', + '--install-extension=ms-python.python', + `--user-data-dir=${userDataDir}`, + workspaceDir, + ] + const electronApp = await electron.launch({ + executablePath: VS_CODE_EXE, + args, + }) + const window = await electronApp.firstWindow() + await window.waitForLoadState('domcontentloaded') + await window.waitForLoadState('networkidle') + return { + window, + close: async () => { + await electronApp.close() + await fs.removeSync(userDataDir) + }, } +} From 39e365238ef4dfe029755f76bc8a9d883b86d10e Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 2 Jun 2025 22:56:42 +0100 Subject: [PATCH 0317/1056] chore(vscode): add completions test (#4627) --- vscode/extension/tests/completions.spec.ts | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 vscode/extension/tests/completions.spec.ts diff --git a/vscode/extension/tests/completions.spec.ts b/vscode/extension/tests/completions.spec.ts new file mode 100644 index 0000000000..27d305c0eb --- /dev/null +++ b/vscode/extension/tests/completions.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { startVSCode, SUSHI_SOURCE_PATH } from './utils' + +test('Autocomplete for model names', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + try { + const { window, close } = await startVSCode(tempDir) + + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + await window.locator('text=grain').first().click() + + // Move to the end of the file + await window.keyboard.press('Control+End') + + // Add a new line + await window.keyboard.press('Enter') + + // Type the beginning of sushi.customers to trigger autocomplete + await window.keyboard.type('sushi.cus') + + // Wait a moment for autocomplete to appear + await window.waitForTimeout(500) + + // Check if the autocomplete suggestion for sushi.customers is visible + await expect(window.locator('text=sushi.customers')).toBeVisible() + + await close() + } finally { + await fs.remove(tempDir) + } +}) From 28567cd6130ee77cde4830103dac5bac3d7afbb4 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:03:13 +0100 Subject: [PATCH 0318/1056] chore(lsp): run pnpm lock (#4629) --- pnpm-lock.yaml | 164 +++++++++++++------------------------------------ 1 file changed, 43 insertions(+), 121 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c44f20404d..a7930e27ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,7 +98,7 @@ importers: version: 4.1.7 '@tailwindcss/vite': specifier: ^4.1.7 - version: 4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) '@tanstack/react-query': specifier: ^5.77.2 version: 5.77.2(react@18.3.1) @@ -113,7 +113,7 @@ importers: version: 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-plugin': specifier: ^1.120.10 - version: 1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8) + version: 1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -159,7 +159,7 @@ importers: version: 18.3.7(@types/react@18.3.22) '@vitejs/plugin-react': specifier: ^4.5.0 - version: 4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -168,10 +168,10 @@ importers: version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -310,7 +310,7 @@ importers: version: 18.3.7(@types/react@18.3.22) '@vitejs/plugin-react-swc': specifier: ^3.10.0 - version: 3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) ajv: specifier: ^8.17.1 version: 8.17.1 @@ -340,13 +340,13 @@ importers: version: 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) optionalDependencies: '@swc/core-linux-x64-gnu': specifier: ^1.11.29 @@ -550,9 +550,6 @@ packages: '@codemirror/language@6.11.0': resolution: {integrity: sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==} - '@codemirror/language@6.11.1': - resolution: {integrity: sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==} - '@codemirror/legacy-modes@6.5.1': resolution: {integrity: sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==} @@ -571,12 +568,6 @@ packages: '@codemirror/view@6.37.1': resolution: {integrity: sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==} - '@codemirror/view@6.37.1': - resolution: {integrity: sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==} - - '@codemirror/view@6.37.1': - resolution: {integrity: sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==} - '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} @@ -2598,11 +2589,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.25.0: - resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -2658,9 +2644,6 @@ packages: caniuse-lite@1.0.30001718: resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} - caniuse-lite@1.0.30001720: - resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -3039,9 +3022,6 @@ packages: electron-to-chromium@1.5.157: resolution: {integrity: sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==} - electron-to-chromium@1.5.161: - resolution: {integrity: sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==} - elkjs@0.8.2: resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} @@ -5190,8 +5170,8 @@ packages: uglify-js: optional: true - terser@5.40.0: - resolution: {integrity: sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==} + terser@5.39.2: + resolution: {integrity: sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==} engines: {node: '>=10'} hasBin: true @@ -5629,8 +5609,8 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - webpack-sources@3.3.2: - resolution: {integrity: sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==} + webpack-sources@3.3.0: + resolution: {integrity: sha512-77R0RDmJfj9dyv5p3bM5pOHa+X8/ZkO9c7kpDstigkC4nIDobadsfSGCwB4bKhMVxqAok8tajaoR8rirM7+VFQ==} engines: {node: '>=10.13.0'} webpack-virtual-modules@0.6.2: @@ -6125,33 +6105,6 @@ snapshots: '@lezer/lr': 1.4.2 style-mod: 4.1.2 - '@codemirror/language@6.11.1': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 - style-mod: 4.1.2 - - '@codemirror/language@6.11.1': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 - style-mod: 4.1.2 - - '@codemirror/language@6.11.1': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 - style-mod: 4.1.2 - '@codemirror/legacy-modes@6.5.1': dependencies: '@codemirror/language': 6.11.0 @@ -6174,7 +6127,7 @@ snapshots: '@codemirror/theme-one-dark@6.1.2': dependencies: - '@codemirror/language': 6.11.1 + '@codemirror/language': 6.11.0 '@codemirror/state': 6.5.2 '@codemirror/view': 6.37.1 '@lezer/highlight': 1.2.1 @@ -6186,20 +6139,6 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 - '@codemirror/view@6.37.1': - dependencies: - '@codemirror/state': 6.5.2 - crelt: 1.0.6 - style-mod: 4.1.2 - w3c-keyname: 2.2.8 - - '@codemirror/view@6.37.1': - dependencies: - '@codemirror/state': 6.5.2 - crelt: 1.0.6 - style-mod: 4.1.2 - w3c-keyname: 2.2.8 - '@csstools/color-helpers@5.0.2': {} '@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': @@ -7488,12 +7427,12 @@ snapshots: postcss: 8.5.3 tailwindcss: 4.1.7 - '@tailwindcss/vite@4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))': + '@tailwindcss/vite@4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@tailwindcss/node': 4.1.7 '@tailwindcss/oxide': 4.1.7 tailwindcss: 4.1.7 - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) '@tanstack/history@1.115.0': {} @@ -7571,7 +7510,7 @@ snapshots: optionalDependencies: '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-plugin@1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8)': + '@tanstack/router-plugin@1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8)': dependencies: '@babel/core': 7.27.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1) @@ -7592,7 +7531,7 @@ snapshots: zod: 3.25.48 optionalDependencies: '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) webpack: 5.99.8 transitivePeerDependencies: - supports-color @@ -8027,15 +7966,15 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react-swc@3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.9 '@swc/core': 1.11.29(@swc/helpers@0.5.17) - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react@4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.1 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) @@ -8043,7 +7982,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.9 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -8054,13 +7993,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))': + '@vitest/mocker@3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.1.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) '@vitest/pretty-format@3.1.4': dependencies: @@ -8478,13 +8417,6 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.5) - browserslist@4.25.0: - dependencies: - caniuse-lite: 1.0.30001720 - electron-to-chromium: 1.5.161 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.0) - buffer-crc32@0.2.13: {} buffer-equal-constant-time@1.0.1: {} @@ -8544,8 +8476,6 @@ snapshots: caniuse-lite@1.0.30001718: {} - caniuse-lite@1.0.30001720: {} - ccount@2.0.1: {} chai@5.2.0: @@ -8664,7 +8594,7 @@ snapshots: dependencies: '@codemirror/autocomplete': 6.18.6 '@codemirror/commands': 6.8.1 - '@codemirror/language': 6.11.1 + '@codemirror/language': 6.11.0 '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 @@ -8923,8 +8853,6 @@ snapshots: electron-to-chromium@1.5.157: {} - electron-to-chromium@1.5.161: {} - elkjs@0.8.2: {} emoji-regex@10.4.0: {} @@ -11478,7 +11406,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.40.0 + terser: 5.39.2 webpack: 5.99.8(esbuild@0.25.4) optionalDependencies: esbuild: 0.25.4 @@ -11489,11 +11417,11 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.40.0 + terser: 5.39.2 webpack: 5.99.8 optional: true - terser@5.40.0: + terser@5.39.2: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.14.1 @@ -11751,12 +11679,6 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.1.3(browserslist@4.25.0): - dependencies: - browserslist: 4.25.0 - escalade: 3.2.0 - picocolors: 1.1.1 - uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -11813,13 +11735,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 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@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -11834,11 +11756,11 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)): + vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)): dependencies: - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.4 fdir: 6.4.4(picomatch@4.0.2) @@ -11851,14 +11773,14 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.40.0 + terser: 5.39.2 tsx: 4.19.4 yaml: 2.8.0 - vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.4 - '@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) '@vitest/pretty-format': 3.1.4 '@vitest/runner': 3.1.4 '@vitest/snapshot': 3.1.4 @@ -11875,8 +11797,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite-node: 3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -11932,7 +11854,7 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-sources@3.3.2: {} + webpack-sources@3.3.0: {} webpack-virtual-modules@0.6.2: {} @@ -11945,7 +11867,7 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.14.1 - browserslist: 4.25.0 + browserslist: 4.24.5 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 es-module-lexer: 1.7.0 @@ -11961,7 +11883,7 @@ snapshots: tapable: 2.2.2 terser-webpack-plugin: 5.3.14(webpack@5.99.8) watchpack: 2.4.4 - webpack-sources: 3.3.2 + webpack-sources: 3.3.0 transitivePeerDependencies: - '@swc/core' - esbuild @@ -11977,7 +11899,7 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.14.1 - browserslist: 4.25.0 + browserslist: 4.24.5 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 es-module-lexer: 1.7.0 @@ -11993,7 +11915,7 @@ snapshots: tapable: 2.2.2 terser-webpack-plugin: 5.3.14(esbuild@0.25.4)(webpack@5.99.8(esbuild@0.25.4)) watchpack: 2.4.4 - webpack-sources: 3.3.2 + webpack-sources: 3.3.0 transitivePeerDependencies: - '@swc/core' - esbuild From 4c0885b6294550462227b5c7f0dcfba06cdd08a8 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:43:39 +0100 Subject: [PATCH 0319/1056] faet(vscode): support multiple workspaces (#4634) --- sqlmesh/lsp/main.py | 48 +++++++++++++-- vscode/extension/src/lsp/lsp.ts | 8 +-- vscode/extension/tests/lineage.spec.ts | 83 +++++++++++++++++++++++++- 3 files changed, 128 insertions(+), 11 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 9ed3ed89c6..b2f2eee78c 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -56,6 +56,7 @@ def __init__( self.server = LanguageServer(server_name, version) self.context_class = context_class self.lsp_context: t.Optional[LSPContext] = None + self.workspace_folders: t.List[Path] = [] self.client_supports_pull_diagnostics = False # Register LSP features (e.g., formatting, hover, etc.) @@ -68,7 +69,7 @@ def _register_features(self) -> None: def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: """Initialize the server when the client connects.""" try: - # Check if client supports pull diagnostics + # Check if the client supports pull diagnostics if params.capabilities and params.capabilities.text_document: diagnostics = getattr(params.capabilities.text_document, "diagnostic", None) if diagnostics: @@ -81,9 +82,13 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: self.client_supports_pull_diagnostics = False if params.workspace_folders: + # Store all workspace folders for later use + self.workspace_folders = [ + Path(self._uri_to_path(folder.uri)) for folder in params.workspace_folders + ] + # Try to find a SQLMesh config file in any workspace folder (only at the root level) - for folder in params.workspace_folders: - folder_path = Path(self._uri_to_path(folder.uri)) + for folder_path in self.workspace_folders: # Only check for config files directly in the workspace directory for ext in ("py", "yml", "yaml"): config_path = folder_path / f"config.{ext}" @@ -104,8 +109,8 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: @self.server.feature(ALL_MODELS_FEATURE) def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse: + uri = URI(params.textDocument.uri) try: - uri = URI(params.textDocument.uri) context = self._context_get_or_load(uri) return get_sql_completions(context, uri) except Exception as e: @@ -457,6 +462,8 @@ def _context_get_or_load(self, document_uri: URI) -> LSPContext: def _ensure_context_in_folder(self, folder_uri: Path) -> None: if self.lsp_context is not None: return + + # First, check the provided folder for ext in ("py", "yml", "yaml"): config_path = folder_uri / f"config.{ext}" if config_path.exists(): @@ -468,6 +475,22 @@ def _ensure_context_in_folder(self, folder_uri: Path) -> None: except Exception as e: self.server.show_message(f"Error loading context: {e}", types.MessageType.Error) + # If not found in the provided folder, search through all workspace folders + for workspace_folder in self.workspace_folders: + for ext in ("py", "yml", "yaml"): + config_path = workspace_folder / f"config.{ext}" + if config_path.exists(): + try: + created_context = self.context_class(paths=[workspace_folder]) + self.lsp_context = LSPContext(created_context) + loaded_sqlmesh_message(self.server, workspace_folder) + return + except Exception as e: + self.server.show_message( + f"Error loading context from {config_path}: {e}", + types.MessageType.Warning, + ) + def _ensure_context_for_document( self, document_uri: URI, @@ -506,6 +529,23 @@ def _ensure_context_for_document( ) path = path.parent + # If still no context found, try the workspace folders + if not loaded: + for workspace_folder in self.workspace_folders: + for ext in ("py", "yml", "yaml"): + config_path = workspace_folder / f"config.{ext}" + if config_path.exists(): + try: + created_context = self.context_class(paths=[workspace_folder]) + self.lsp_context = LSPContext(created_context) + loaded_sqlmesh_message(self.server, workspace_folder) + return + except Exception as e: + self.server.show_message( + f"Error loading context from {config_path}: {e}", + types.MessageType.Warning, + ) + return @staticmethod diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 50129406c1..989eabafd3 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -34,13 +34,11 @@ export class LSPClient implements Disposable { return sqlmesh } const workspaceFolders = getWorkspaceFolders() - if (workspaceFolders.length !== 1) { - traceError( - `Invalid number of workspace folders: ${workspaceFolders.length}`, - ) + if (workspaceFolders.length === 0) { + traceError(`No workspace folders found`) return err({ type: 'generic', - message: 'Invalid number of workspace folders', + message: 'No workspace folders found', }) } const workspacePath = sqlmesh.value.workspacePath diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index 1c621f45b4..e2a049a9f0 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -3,6 +3,7 @@ import path from 'path' import fs from 'fs-extra' import os from 'os' import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { writeFileSync } from 'fs' /** * Helper function to launch VS Code and test lineage with given project path config @@ -15,8 +16,8 @@ async function testLineageWithProjectPath(window: Page): Promise { await window.keyboard.type('Lineage: Focus On View') await window.keyboard.press('Enter') - // Wait for "Loaded SQLmesh Context" text to appear - const loadedContextText = window.locator('text=Loaded SQLMesh Context') + // Wait for "Loaded SQLMesh context" text to appear + const loadedContextText = window.locator('text=Loaded SQLMesh context') await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 }) } @@ -141,3 +142,81 @@ test('Lineage panel renders correctly - absolute path project outside of workspa await fs.remove(tempFolder) } }) + +test('Lineage panel renders correctly - multiworkspace setup', async () => { + const workspaceDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-workspace-'), + ) + const projectDir1 = path.join(workspaceDir, 'projects', 'sushi1') + const projectDir2 = path.join(workspaceDir, 'projects', 'sushi2') + await fs.copy(SUSHI_SOURCE_PATH, projectDir1) + await fs.ensureDir(projectDir2) + + // Add a .code-workspace file with multiple projects + const workspaceFilePath = path.join( + workspaceDir, + 'multi-workspace.code-workspace', + ) + writeFileSync( + workspaceFilePath, + JSON.stringify({ + folders: [ + { + name: 'sushi1', + path: 'projects/sushi1', + }, + { + name: 'sushi2', + path: 'projects/sushi2', + }, + ], + }), + ) + + try { + const { window, close } = await startVSCode(workspaceFilePath) + await testLineageWithProjectPath(window) + await close() + } finally { + await fs.remove(workspaceDir) + } +}) + +test('Lineage panel renders correctly - multiworkspace setup reversed', async () => { + const workspaceDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-workspace-'), + ) + const projectDir1 = path.join(workspaceDir, 'projects', 'sushi1') + const projectDir2 = path.join(workspaceDir, 'projects', 'sushi2') + await fs.copy(SUSHI_SOURCE_PATH, projectDir2) + await fs.ensureDir(projectDir1) + + // Add a .code-workspace file with multiple projects + const workspaceFilePath = path.join( + workspaceDir, + 'multi-workspace.code-workspace', + ) + writeFileSync( + workspaceFilePath, + JSON.stringify({ + folders: [ + { + name: 'sushi1', + path: 'projects/sushi1', + }, + { + name: 'sushi2', + path: 'projects/sushi2', + }, + ], + }), + ) + + try { + const { window, close } = await startVSCode(workspaceFilePath) + await testLineageWithProjectPath(window) + await close() + } finally { + await fs.remove(workspaceDir) + } +}) From 2f8e3b75f3af8111b741ae7a4eb39e4508dca78d Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:59:08 +0100 Subject: [PATCH 0320/1056] feat: render model without active editor (#4636) --- sqlmesh/lsp/context.py | 28 +++++++++- sqlmesh/lsp/custom.py | 26 +++++++++ sqlmesh/lsp/main.py | 16 ++++++ vscode/extension/src/commands/renderModel.ts | 57 ++++++++++++++++---- vscode/extension/src/lsp/custom.ts | 21 ++++++++ vscode/extension/tests/render.spec.ts | 41 ++++++++++++++ 6 files changed, 179 insertions(+), 10 deletions(-) diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 6fd0ddd5d3..945f2a83cf 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -5,7 +5,7 @@ from sqlmesh.core.model.definition import SqlModel from sqlmesh.core.linter.definition import AnnotatedRuleViolation -from sqlmesh.lsp.custom import RenderModelEntry +from sqlmesh.lsp.custom import RenderModelEntry, ModelForRendering from sqlmesh.lsp.uri import URI @@ -148,3 +148,29 @@ def lint_model(self, uri: URI) -> t.List[AnnotatedRuleViolation]: # Store in cache self._lint_cache[path] = diagnostics return diagnostics + + def list_of_models_for_rendering(self) -> t.List[ModelForRendering]: + """Get a list of models for rendering. + + Returns: + List of ModelForRendering objects. + """ + return [ + ModelForRendering( + name=model.name, + fqn=model.fqn, + description=model.description, + uri=URI.from_path(model._path).value, + ) + for model in self.context.models.values() + if isinstance(model, SqlModel) and model._path is not None + ] + [ + ModelForRendering( + name=audit.name, + fqn=audit.fqn, + description=audit.description, + uri=URI.from_path(audit._path).value, + ) + for audit in self.context.standalone_audits.values() + if audit._path is not None + ] diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py index b49361e43f..cc0fff67ea 100644 --- a/sqlmesh/lsp/custom.py +++ b/sqlmesh/lsp/custom.py @@ -46,3 +46,29 @@ class RenderModelResponse(PydanticModel): """ models: t.List[RenderModelEntry] + + +ALL_MODELS_FOR_RENDER_FEATURE = "sqlmesh/all_models_for_render" + + +class ModelForRendering(PydanticModel): + """ + A model that is available for rendering. + """ + + name: str + fqn: str + description: t.Optional[str] = None + uri: str + + +class AllModelsForRenderRequest(PydanticModel): + pass + + +class AllModelsForRenderResponse(PydanticModel): + """ + Response to get all the models that are in the current project for rendering purposes. + """ + + models: t.List[ModelForRendering] diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index b2f2eee78c..a43c4d254f 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -27,9 +27,12 @@ ) from sqlmesh.lsp.custom import ( ALL_MODELS_FEATURE, + ALL_MODELS_FOR_RENDER_FEATURE, RENDER_MODEL_FEATURE, AllModelsRequest, AllModelsResponse, + AllModelsForRenderRequest, + AllModelsForRenderResponse, RenderModelRequest, RenderModelResponse, ) @@ -122,6 +125,19 @@ def render_model(ls: LanguageServer, params: RenderModelRequest) -> RenderModelR context = self._context_get_or_load(uri) return RenderModelResponse(models=context.render_model(uri)) + @self.server.feature(ALL_MODELS_FOR_RENDER_FEATURE) + def all_models_for_render( + ls: LanguageServer, params: AllModelsForRenderRequest + ) -> AllModelsForRenderResponse: + if self.lsp_context is None: + current_path = Path.cwd() + self._ensure_context_in_folder(current_path) + if self.lsp_context is None: + raise RuntimeError("No context found") + return AllModelsForRenderResponse( + models=self.lsp_context.list_of_models_for_rendering() + ) + @self.server.feature(API_FEATURE) def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]: ls.log_trace(f"API request: {request}") diff --git a/vscode/extension/src/commands/renderModel.ts b/vscode/extension/src/commands/renderModel.ts index 92bd528d94..00bbe076e3 100644 --- a/vscode/extension/src/commands/renderModel.ts +++ b/vscode/extension/src/commands/renderModel.ts @@ -9,21 +9,60 @@ export function renderModel( renderedModelProvider?: RenderedModelProvider, ) { return async () => { + if (!lspClient) { + vscode.window.showErrorMessage('LSP client not available') + return + } + // Get the current active editor const activeEditor = vscode.window.activeTextEditor + let documentUri: string + if (!activeEditor) { - vscode.window.showErrorMessage('No active editor found') - return - } + // No active editor, show a list of all models + const allModelsResult = await lspClient.call_custom_method( + 'sqlmesh/all_models_for_render', + {}, + ) - if (!lspClient) { - vscode.window.showErrorMessage('LSP client not available') - return - } + if (isErr(allModelsResult)) { + vscode.window.showErrorMessage( + `Failed to get models: ${allModelsResult.error}`, + ) + return + } + + if ( + !allModelsResult.value.models || + allModelsResult.value.models.length === 0 + ) { + vscode.window.showInformationMessage('No models found in the project') + return + } - // Get the current document URI - const documentUri = activeEditor.document.uri.toString(true) + // Let user choose from all models + const items = allModelsResult.value.models.map(model => ({ + label: model.name, + description: model.fqn, + detail: model.description ? model.description : undefined, + model: model, + })) + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a model to render', + }) + + if (!selected) { + return + } + + // Use the selected model's URI + documentUri = selected.model.uri + } else { + // Get the current document URI + documentUri = activeEditor.document.uri.toString(true) + } // Call the render model API const result = await lspClient.call_custom_method('sqlmesh/render_model', { diff --git a/vscode/extension/src/lsp/custom.ts b/vscode/extension/src/lsp/custom.ts index d0d4a86bce..004ee92285 100644 --- a/vscode/extension/src/lsp/custom.ts +++ b/vscode/extension/src/lsp/custom.ts @@ -30,6 +30,7 @@ export type CustomLSPMethods = | AllModelsMethod | AbstractAPICall | RenderModelMethod + | AllModelsForRenderMethod interface AllModelsRequest { textDocument: { @@ -54,3 +55,23 @@ export interface AbstractAPICall { request: AbstractAPICallRequest response: object } + +export interface AllModelsForRenderMethod { + method: 'sqlmesh/all_models_for_render' + request: AllModelsForRenderRequest + response: AllModelsForRenderResponse +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface AllModelsForRenderRequest {} + +interface AllModelsForRenderResponse { + models: ModelForRendering[] +} + +export interface ModelForRendering { + name: string + fqn: string + description: string | null | undefined + uri: string +} diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts index 6846b3eca8..3c31cfdab5 100644 --- a/vscode/extension/tests/render.spec.ts +++ b/vscode/extension/tests/render.spec.ts @@ -153,3 +153,44 @@ test('Render works correctly with every rendered model opening a new tab', async await fs.remove(tempDir) } }) + +test('Render shows model picker when no active editor is open', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + try { + const { window, close } = await startVSCode(tempDir) + + // Load the lineage view to initialize SQLMesh context (like lineage.spec.ts does) + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Lineage: Focus On View') + await window.keyboard.press('Enter') + + // Wait for "Loaded SQLmesh Context" text to appear + const loadedContextText = window.locator('text=Loaded SQLMesh Context') + await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 }) + + // Run the render command without any active editor + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Render Model') + await window.keyboard.press('Enter') + + // Type to filter for customers model and select it + await window.keyboard.type('customers') + await window.waitForSelector('text=sushi.customers', { timeout: 5000 }) + await window.locator('text=sushi.customers').click() + + // Verify the rendered model is shown + await expect(window.locator('text=sushi.customers (rendered)')).toBeVisible( + { timeout: 15000 }, + ) + + await close() + } finally { + await fs.remove(tempDir) + } +}) From 57a32f7dbd6d63782b069b54a93d734aa91668bd Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:14:14 +0300 Subject: [PATCH 0321/1056] Chore: update docs on `@this_model` (#4599) Co-authored-by: Trey Spiller <1831878+treysp@users.noreply.github.com> --- docs/concepts/macros/macro_variables.md | 16 ++++-- docs/concepts/macros/sqlmesh_macros.md | 67 ++++++++++++++++++++++++- docs/concepts/models/external_models.md | 4 +- docs/concepts/models/python_models.md | 2 + docs/concepts/models/sql_models.md | 14 ++++-- docs/guides/isolated_systems.md | 4 +- docs/integrations/engines/trino.md | 5 +- 7 files changed, 98 insertions(+), 14 deletions(-) diff --git a/docs/concepts/macros/macro_variables.md b/docs/concepts/macros/macro_variables.md index 2b1b591f3b..79484626ff 100644 --- a/docs/concepts/macros/macro_variables.md +++ b/docs/concepts/macros/macro_variables.md @@ -130,11 +130,21 @@ SQLMesh provides additional predefined variables used to modify model behavior b * 'auditing' - The audit is being run. * 'testing' - The model query logic is being evaluated in the context of a unit test. * @gateway - A string value containing the name of the current [gateway](../../guides/connections.md). -* @this_model - A string value containing the name of the physical table the model view selects from. Typically used to create [generic audits](../audits.md#generic-audits). In the case of [on_virtual_update statements](../models/sql_models.md#optional-on-virtual-update-statements) it contains the qualified view name instead. - * Can be used in model definitions when SQLGlot cannot fully parse a statement and you need to reference the model's underlying physical table directly. - * Can be passed as an argument to macros that access or interact with the underlying physical table. +* @this_model - The physical table name that the model's view selects from. Typically used to create [generic audits](../audits.md#generic-audits). When used in [on_virtual_update statements](../models/sql_models.md#optional-on-virtual-update-statements), it contains the qualified view name instead. * @model_kind_name - A string value containing the name of the current model kind. Intended to be used in scenarios where you need to control the [physical properties in model defaults](../../reference/model_configuration.md#model-defaults). +!!! note "Embedding variables in strings" + + Macro variable references sometimes use the curly brace syntax `@{variable}`, which serves a different purpose than the regular `@variable` syntax. + + The curly brace syntax tells SQLMesh that the rendered string should be treated as an identifier, instead of simply replacing the macro variable value. + + For example, if `variable` is defined as `@DEF(`variable`, foo.bar)`, then `@variable` produces `foo.bar`, while `@{variable}` produces `"foo.bar"`. This is because SQLMesh converts `foo.bar` into an identifier, using double quotes to correctly include the `.` character in the identifier name. + + In practice, `@{variable}` is most commonly used to interpolate a value within an identifier, e.g., `@{variable}_suffix`, whereas `@variable` is used to do plain substitutions for string literals. + + Learn more [above](#embedding-variables-in-strings). + #### Before all and after all variables The following variables are also available in [`before_all` and `after_all` statements](../../guides/configuration.md#before_all-and-after_all-statements), as well as in macros invoked within them. diff --git a/docs/concepts/macros/sqlmesh_macros.md b/docs/concepts/macros/sqlmesh_macros.md index 4e5601d2e1..f1cbb6ebf0 100644 --- a/docs/concepts/macros/sqlmesh_macros.md +++ b/docs/concepts/macros/sqlmesh_macros.md @@ -38,6 +38,59 @@ It uses the following five step approach to accomplish this: 5. Modify the semantic representation of the SQL query with the substituted variable values from (3) and functions from (4). +### Embedding variables in strings + +SQLMesh always incorporates macro variable values into the semantic representation of a SQL query (step 5 above). To do that, it infers the role each macro variable value plays in the query. + +For context, two commonly used types of string in SQL are: + +- String literals, which represent text values and are surrounded by single quotes, such as `'the_string'` +- Identifiers, which reference database objects like column, table, alias, and function names + - They may be unquoted or quoted with double quotes, backticks, or brackets, depending on the SQL dialect + +In a normal query, SQLMesh can easily determine which role a given string is playing. However, it is more difficult if a macro variable is embedded directly into a string - especially if the string is in the `MODEL` block (and not the query itself). + +For example, consider a project that defines a [gateway variable](#gateway-variables) named `gateway_var`. The project includes a model that references `@gateway_var` as part of the schema in the model's `name`, which is a SQL *identifier*. + +This is how we might try to write the model: + +``` sql title="Incorrectly rendered to string literal" +MODEL ( + name the_@gateway_var_schema.table +); +``` + +From SQLMesh's perspective, the model schema is the combination of three sub-strings: `the_`, the value of `@gateway_var`, and `_schema`. + +SQLMesh will concatenate those strings, but it does not have the context to know that it is building a SQL identifier and will return a string literal. + +To provide the context SQLMesh needs, you must add curly braces to the macro variable reference: `@{gateway_var}` instead of `@gateway_var`: + +``` sql title="Correctly rendered to identifier" +MODEL ( + name the_@{gateway_var}_schema.table +); +``` + +The curly braces let SQLMesh know that it should treat the string as a SQL identifier, which it will then quote based on the SQL dialect's quoting rules. + +The most common use of the curly brace syntax is embedding macro variables into strings, it can also be used to differentiate string literals and identifiers in SQL queries. For example, consider a macro variable `my_variable` whose value is `col`. + +If we `SELECT` this value with regular macro syntax, it will render to a string literal: + +``` sql +SELECT @my_variable AS the_column; -- renders to SELECT 'col' AS the_column +``` + +`'col'` is surrounded with single quotes, and the SQL engine will use that string as the column's data value. + +If we use curly braces, SQLMesh will know that we want to use the rendered string as an identifier: + +``` sql +SELECT @{my_variable} AS the_column; -- renders to SELECT col AS the_column +``` + +`col` is not surrounded with single quotes, and the SQL engine will determine that the query is referencing a column or other object named `col`. ## User-defined variables @@ -174,6 +227,8 @@ SELECT FROM @customer.some_source ``` +Note the use of both regular `@field_a` and curly brace syntax `@{field_b}` macro variable references in the model query. Learn more [above](#embedding-variables-in-strings) + Blueprint variables can be accessed using the syntax shown above, or through the `@BLUEPRINT_VAR()` macro function, which also supports specifying default values in case the variable is undefined (similar to `@VAR()`). ### Local variables @@ -448,7 +503,13 @@ FROM table This syntax works regardless of whether the array values are quoted or not. -NOTE: SQLMesh macros support placing macro values at the end of a column name simply using `column_@x`. However if you wish to substitute the variable anywhere else in the identifier, you need to use the more explicit substitution syntax `@{}`. This avoids ambiguity. These are valid uses: `@{x}_column` or `my_@{x}_column`. +!!! note "Embedding macros in strings" + + SQLMesh macros support placing macro values at the end of a column name using `column_@x`. + + However, if you wish to substitute the variable anywhere else in the identifier, you need to use the more explicit curly brace syntax `@{}` to avoid ambiguity. For example, these are valid uses: `@{x}_column` or `my_@{x}_column`. + + Learn more about embedding macros in strings [above](#embedding-variables-in-strings) ### @IF @@ -1087,7 +1148,9 @@ The `template` can contain the following placeholders that will be substituted: - `@{schema_name}` - The name of the physical schema that SQLMesh is using for the model version table, eg `sqlmesh__landing` - `@{table_name}` - The name of the physical table that SQLMesh is using for the model version, eg `landing__customers__2517971505` -It can be used in a `MODEL` block: +Note the use of the curly brace syntax `@{}` in the template placeholders - learn more [above](#embedding-variables-in-strings). + +The `@resolve_template` macro can be used in a `MODEL` block: ```sql linenums="1" hl_lines="5" MODEL ( diff --git a/docs/concepts/models/external_models.md b/docs/concepts/models/external_models.md index a8557813bc..922daac6b0 100644 --- a/docs/concepts/models/external_models.md +++ b/docs/concepts/models/external_models.md @@ -70,7 +70,9 @@ FROM @{gateway}_db.external_table; ``` -This table will be named differently depending on which `--gateway` SQLMesh is run with. For example: +This table will be named differently depending on which `--gateway` SQLMesh is run with (learn more about the curly brace `@{gateway}` syntax [here](../../concepts/macros/sqlmesh_macros.md#embedding-variables-in-strings)). + +For example: - `sqlmesh --gateway dev plan` - SQLMesh will try to query `dev_db.external_table` - `sqlmesh --gateway prod plan` - SQLMesh will try to query `prod_db.external_table` diff --git a/docs/concepts/models/python_models.md b/docs/concepts/models/python_models.md index d07603f6b2..db25fe46f2 100644 --- a/docs/concepts/models/python_models.md +++ b/docs/concepts/models/python_models.md @@ -365,6 +365,8 @@ def entrypoint( ) ``` +Note the use of curly brace syntax `@{customer}` in the model name above. It is used to ensure SQLMesh can combine the macro variable into the model name identifier correctly - learn more [here](../../concepts/macros/sqlmesh_macros.md#embedding-variables-in-strings). + Blueprint variable mappings can also be constructed dynamically, e.g., by using a macro: `blueprints="@gen_blueprints()"`. This is useful in cases where the `blueprints` list needs to be sourced from external sources, such as CSV files. For example, the definition of the `gen_blueprints` may look like this: diff --git a/docs/concepts/models/sql_models.md b/docs/concepts/models/sql_models.md index c57a70bde6..85c2492c87 100644 --- a/docs/concepts/models/sql_models.md +++ b/docs/concepts/models/sql_models.md @@ -95,7 +95,7 @@ Note that the SQL command `UNCACHE TABLE countries` inside the `@IF()` macro doe The optional on-virtual-update statements allow you to execute SQL commands after the completion of the [Virtual Update](#virtual-update). -These can be used, for example, to grant privileges on views of the virtual layer. +These can be used, for example, to grant privileges on views of the virtual layer. These SQL statements must be enclosed within an `ON_VIRTUAL_UPDATE_BEGIN;` ...; `ON_VIRTUAL_UPDATE_END;` block like this: @@ -109,11 +109,11 @@ SELECT r.id::INT FROM raw.restaurants AS r; -ON_VIRTUAL_UPDATE_BEGIN; +ON_VIRTUAL_UPDATE_BEGIN; GRANT SELECT ON VIEW @this_model TO ROLE role_name; -JINJA_STATEMENT_BEGIN; +JINJA_STATEMENT_BEGIN; GRANT SELECT ON VIEW {{ this_model }} TO ROLE admin; -JINJA_END; +JINJA_END; ON_VIRTUAL_UPDATE_END; ``` @@ -175,6 +175,10 @@ SELECT FROM customer2.some_source ``` +Note the use of curly brace syntax `@{field_b} AS field_b` in the model query above. It is used to tell SQLMesh that the rendered variable value should be treated as a SQL identifier instead of a string literal. + +You can see the different behavior in the first rendered model. `@field_a` is resolved to the string literal `'x'` (with single quotes) and `@{field_b}` is resolved to the identifier `y` (without quotes). Learn more about the curly brace syntax [here](../../concepts/macros/sqlmesh_macros.md#embedding-variables-in-strings). + Blueprint variable mappings can also be constructed dynamically, e.g., by using a macro: `blueprints @gen_blueprints()`. This is useful in cases where the `blueprints` list needs to be sourced from external sources, such as CSV files. For example, the definition of the `gen_blueprints` may look like this: @@ -249,7 +253,7 @@ One could also define this model by simply returning a string that contained the The `@model` decorator is the Python equivalent of the `MODEL` DDL. -In addition to model metadata and configuration information, one can also set the keyword arguments `pre_statements`, `post_statements` and `on_virtual_update` to a list of SQL strings and/or SQLGlot expressions to define the pre/post-statements and on-virtual-update-statements of the model, respectively. +In addition to model metadata and configuration information, one can also set the keyword arguments `pre_statements`, `post_statements` and `on_virtual_update` to a list of SQL strings and/or SQLGlot expressions to define the pre/post-statements and on-virtual-update-statements of the model, respectively. !!! note diff --git a/docs/guides/isolated_systems.md b/docs/guides/isolated_systems.md index 462e761534..a032675653 100644 --- a/docs/guides/isolated_systems.md +++ b/docs/guides/isolated_systems.md @@ -70,7 +70,7 @@ MODEL ( ) ``` -To embed the gateway name directly in the schema name, use the `@{gateway}` syntax: +To embed the gateway name directly in the schema name, use the curly brace `@{gateway}` syntax: ```sql linenums="1" MODEL ( @@ -78,6 +78,8 @@ MODEL ( ) ``` +Learn more about the curly brace `@{}` syntax [here](../concepts/macros/sqlmesh_macros.md#embedding-variables-in-strings). + ## Workflow ### Linking systems diff --git a/docs/integrations/engines/trino.md b/docs/integrations/engines/trino.md index 031eafae22..c590ee32ba 100644 --- a/docs/integrations/engines/trino.md +++ b/docs/integrations/engines/trino.md @@ -178,12 +178,13 @@ This would perform the following mappings: !!! info "Placeholders" You may use the `@{catalog_name}` and `@{schema_name}` placeholders in the mapping value. - If there is a match on one of the patterns then the catalog / schema that SQLMesh is about to use in the `CREATE SCHEMA` statement will be subsitituted into these placeholders. + If there is a match on one of the patterns then the catalog / schema that SQLMesh is about to use in the `CREATE SCHEMA` statement will be substituted into these placeholders. + Note the use of curly brace syntax `@{}` when referencing these placeholders - learn more [here](../../concepts/macros/sqlmesh_macros.md#embedding-variables-in-strings). #### Tables -Often, you dont need to configure an explicit table location because if you have configured explicit schema locations, table locations are automatically inferred by Trino to be a subdirectory under the schema location. +Often, you don't need to configure an explicit table location because if you have configured explicit schema locations, table locations are automatically inferred by Trino to be a subdirectory under the schema location. However, if you need to, you can configure an explicit table location by adding a `location` property to the model `physical_properties`. From 9e2a5f9d5780fb2cfe4f2d31cc365c5e9d51edda Mon Sep 17 00:00:00 2001 From: mrjsj <22403319+mrjsj@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:55:24 +0200 Subject: [PATCH 0322/1056] Feat: add ducklake `DATA_INLINING_ROW_LIMIT` attach option (#4635) --- docs/integrations/engines/duckdb.md | 4 +++- sqlmesh/core/config/connection.py | 5 ++++- tests/core/test_connection_config.py | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/integrations/engines/duckdb.md b/docs/integrations/engines/duckdb.md index 33751cda8f..7aef7def25 100644 --- a/docs/integrations/engines/duckdb.md +++ b/docs/integrations/engines/duckdb.md @@ -79,6 +79,7 @@ SQLMesh will place models with the explicit catalog "ephemeral", such as `epheme path: 'catalog.ducklake' data_path: data/ducklake encrypted: True + data_inlining_row_limit: 10 ``` === "Python" @@ -102,7 +103,8 @@ SQLMesh will place models with the explicit catalog "ephemeral", such as `epheme type="ducklake", path="catalog.ducklake", data_path="data/ducklake", - encrypted=True + encrypted=True, + data_inlining_row_limit=10, ), } ) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index b932e5d3e2..1ba66a0abd 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -224,6 +224,7 @@ class DuckDBAttachOptions(BaseConfig): # DuckLake specific options data_path: t.Optional[str] = None encrypted: bool = False + data_inlining_row_limit: t.Optional[int] = None def to_sql(self, alias: str) -> str: options = [] @@ -236,10 +237,12 @@ def to_sql(self, alias: str) -> str: # DuckLake specific options if self.type == "ducklake": - if self.data_path: + if self.data_path is not None: options.append(f"DATA_PATH '{self.data_path}'") if self.encrypted: options.append("ENCRYPTED") + if self.data_inlining_row_limit is not None: + options.append(f"DATA_INLINING_ROW_LIMIT {self.data_inlining_row_limit}") options_sql = f" ({', '.join(options)})" if options else "" alias_sql = "" diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 9f91ddd55e..63a32e8a6d 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -611,6 +611,7 @@ def test_duckdb_attach_ducklake_catalog(make_config): path="catalog.ducklake", data_path="/tmp/ducklake_data", encrypted=True, + data_inlining_row_limit=10, ), }, ) @@ -621,9 +622,11 @@ def test_duckdb_attach_ducklake_catalog(make_config): assert ducklake_catalog.path == "catalog.ducklake" assert ducklake_catalog.data_path == "/tmp/ducklake_data" assert ducklake_catalog.encrypted is True + assert ducklake_catalog.data_inlining_row_limit == 10 # Check that the generated SQL includes DATA_PATH assert "DATA_PATH '/tmp/ducklake_data'" in ducklake_catalog.to_sql("ducklake") assert "ENCRYPTED" in ducklake_catalog.to_sql("ducklake") + assert "DATA_INLINING_ROW_LIMIT 10" in ducklake_catalog.to_sql("ducklake") def test_duckdb_attach_options(): From 0bbbf9a4669b87ff4083111253bad41a71445b58 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:01:36 +0100 Subject: [PATCH 0323/1056] refactor(lsp): move autocomplete functions about (#4603) --- sqlmesh/lsp/completions.py | 4 +++- sqlmesh/lsp/context.py | 14 ++++++++++++++ sqlmesh/lsp/main.py | 7 ++++--- tests/lsp/test_completions.py | 4 ++-- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/sqlmesh/lsp/completions.py b/sqlmesh/lsp/completions.py index e3bd9ab9b4..8d5d68d931 100644 --- a/sqlmesh/lsp/completions.py +++ b/sqlmesh/lsp/completions.py @@ -6,7 +6,9 @@ from sqlmesh.lsp.uri import URI -def get_sql_completions(context: t.Optional[LSPContext], file_uri: URI) -> AllModelsResponse: +def get_sql_completions( + context: t.Optional[LSPContext], file_uri: t.Optional[URI] +) -> AllModelsResponse: """ Return a list of completions for a given file. """ diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 945f2a83cf..251bac8b5b 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -6,6 +6,7 @@ from sqlmesh.core.model.definition import SqlModel from sqlmesh.core.linter.definition import AnnotatedRuleViolation from sqlmesh.lsp.custom import RenderModelEntry, ModelForRendering +from sqlmesh.lsp.custom import AllModelsResponse, RenderModelEntry from sqlmesh.lsp.uri import URI @@ -174,3 +175,16 @@ def list_of_models_for_rendering(self) -> t.List[ModelForRendering]: for audit in self.context.standalone_audits.values() if audit._path is not None ] + + def get_autocomplete(self, uri: t.Optional[URI]) -> AllModelsResponse: + """Get autocomplete suggestions for a file. + + Args: + uri: The URI of the file to get autocomplete suggestions for. + + Returns: + AllModelsResponse containing models and keywords. + """ + from sqlmesh.lsp.completions import get_sql_completions + + return get_sql_completions(self, uri) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index a43c4d254f..d2b9fcade7 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -20,7 +20,6 @@ ApiResponseGetLineage, ApiResponseGetModels, ) -from sqlmesh.lsp.completions import get_sql_completions from sqlmesh.lsp.context import ( LSPContext, ModelTarget, @@ -115,9 +114,11 @@ def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsRespons uri = URI(params.textDocument.uri) try: context = self._context_get_or_load(uri) - return get_sql_completions(context, uri) + return context.get_autocomplete(uri) except Exception as e: - return get_sql_completions(None, uri) + from sqlmesh.lsp.completions import get_sql_completions + + return get_sql_completions(None, URI(params.textDocument.uri)) @self.server.feature(RENDER_MODEL_FEATURE) def render_model(ls: LanguageServer, params: RenderModelRequest) -> RenderModelResponse: diff --git a/tests/lsp/test_completions.py b/tests/lsp/test_completions.py index 2110c5f3af..4af2d4dfc8 100644 --- a/tests/lsp/test_completions.py +++ b/tests/lsp/test_completions.py @@ -26,7 +26,7 @@ def test_get_sql_completions_with_context_no_file_uri(): context = Context(paths=["examples/sushi"]) lsp_context = LSPContext(context) - completions = get_sql_completions(lsp_context, None) + completions = lsp_context.get_autocomplete(None) assert len(completions.keywords) > len(TOKENIZER_KEYWORDS) assert "sushi.active_customers" in completions.models assert "sushi.customers" in completions.models @@ -38,6 +38,6 @@ def test_get_sql_completions_with_context_and_file_uri(): lsp_context = LSPContext(context) file_uri = next(key for key in lsp_context.map.keys() if key.name == "active_customers.sql") - completions = get_sql_completions(lsp_context, URI.from_path(file_uri)) + completions = lsp_context.get_autocomplete(URI.from_path(file_uri)) assert len(completions.keywords) > len(TOKENIZER_KEYWORDS) assert "sushi.active_customers" not in completions.models From 3e36c71a42ee6e6420104076ffd418a9895086f4 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:09:21 +0100 Subject: [PATCH 0324/1056] docs: updating vsccode with features (#4611) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/guides/vscode.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/guides/vscode.md b/docs/guides/vscode.md index fb44f6289a..f21bd2c165 100644 --- a/docs/guides/vscode.md +++ b/docs/guides/vscode.md @@ -95,6 +95,10 @@ The extension adds a lineage view to SQLMesh models. To view the lineage of a mo ![Lineage view](./vscode/lineage.png) +### Render + +The extension allows you to render a model with the macros resolved. You can invoke it either with the command palette `Render SQLMesh Model` or by clicking the preview button in the top right. + ### Editor The SQLMesh VSCode extension includes several features that make editing SQLMesh models easier and quicker: @@ -107,7 +111,13 @@ See auto-completion suggestions when writing SQL models, keywords, or model name **Go to definition and hover information** -Hovering over a model name shows a tooltip with the model description. Clicking the model name opens the file containing the model definition. +Hovering over a model name shows a tooltip with the model description. + +In addition to hover information, you can go to a definition of the following objects in a SQL file by either right-clicking and choosing "Go to definition" or by `Command/Control + Click` on the respective reference. This currently works for: + +- Model references in a SQL file like `FROM my_model` +- CTE reference in a SQL file like `WITH my_cte AS (...) ... FROM my_cte` +- Python macros in a SQL file like `SELECT @my_macro(...)` **Diagnostics** From f1da1d86a1ec4fed5907b79767066a994d24b8cf Mon Sep 17 00:00:00 2001 From: codetalker-ai Date: Tue, 3 Jun 2025 16:22:51 +0300 Subject: [PATCH 0325/1056] fix(web): incorrect encoding when viewing files containing non-ASCII characters (#4570) Co-authored-by: Jo <46752250+georgesittas@users.noreply.github.com> --- tests/web/test_main.py | 23 +++++++++++++++++++++-- web/server/api/endpoints/files.py | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/web/test_main.py b/tests/web/test_main.py index 99b268cf0d..cf2220ad6d 100644 --- a/tests/web/test_main.py +++ b/tests/web/test_main.py @@ -106,6 +106,18 @@ def test_write_file(client: TestClient, project_tmp_path: Path) -> None: } +def test_write_file_non_ascii(client: TestClient, project_tmp_path: Path) -> None: + response = client.post("/api/files/foo.txt", json={"content": "何か良いこと"}) + file = _get_file_with_content(project_tmp_path / "foo.txt", "foo.txt") + assert response.status_code == 204 + assert file.dict() == { + "name": "foo.txt", + "path": "foo.txt", + "extension": ".txt", + "content": "何か良いこと", + } + + def test_update_file(client: TestClient, project_tmp_path: Path) -> None: txt_file = project_tmp_path / "foo.txt" txt_file.write_text("bar") @@ -212,7 +224,12 @@ def test_create_directory(client: TestClient, project_tmp_path: Path) -> None: response = client.post("/api/directories/new_dir") assert response.status_code == 200 assert (project_tmp_path / "new_dir").exists() - assert response.json() == {"directories": [], "files": [], "name": "new_dir", "path": "new_dir"} + assert response.json() == { + "directories": [], + "files": [], + "name": "new_dir", + "path": "new_dir", + } def test_create_directory_already_exists(client: TestClient, project_tmp_path: Path) -> None: @@ -494,7 +511,9 @@ def test_delete_environment_failure( client: TestClient, web_sushi_context: Context, mocker: MockerFixture ): mocker.patch.object( - web_sushi_context.state_sync, "invalidate_environment", side_effect=Exception("Some error") + web_sushi_context.state_sync, + "invalidate_environment", + side_effect=Exception("Some error"), ) response = client.delete("/api/environments/test") diff --git a/web/server/api/endpoints/files.py b/web/server/api/endpoints/files.py index d843e65c69..db58fce55e 100644 --- a/web/server/api/endpoints/files.py +++ b/web/server/api/endpoints/files.py @@ -168,7 +168,7 @@ def walk_path( def _get_file_with_content(file_path: Path, relative_path: str) -> models.File: """Get a file, including its contents.""" try: - content = file_path.read_text() + content = file_path.read_text(encoding="utf-8") except FileNotFoundError as e: raise e except Exception: From fceda92053e154c7aa62552cb2bc275e3b1bc96e Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:45:26 +0100 Subject: [PATCH 0326/1056] feat(vscode): rerender on save model (#4637) --- vscode/extension/src/commands/renderModel.ts | 47 ++++++++++ vscode/extension/src/extension.ts | 22 ++++- .../src/providers/renderedModelProvider.ts | 92 ++++++++++++++++++- 3 files changed, 156 insertions(+), 5 deletions(-) diff --git a/vscode/extension/src/commands/renderModel.ts b/vscode/extension/src/commands/renderModel.ts index 00bbe076e3..50598ec55e 100644 --- a/vscode/extension/src/commands/renderModel.ts +++ b/vscode/extension/src/commands/renderModel.ts @@ -4,6 +4,51 @@ import { isErr } from '@bus/result' import { RenderModelEntry } from '../lsp/custom' import { RenderedModelProvider } from '../providers/renderedModelProvider' +export async function reRenderModelForSourceFile( + sourceUri: string, + lspClient: LSPClient, + renderedModelProvider: RenderedModelProvider, +): Promise { + const renderedUri = renderedModelProvider.getRenderedUriForSource(sourceUri) + if (!renderedUri) { + return // No rendered model exists for this source file + } + + // Call the render model API + const result = await lspClient.call_custom_method('sqlmesh/render_model', { + textDocumentUri: sourceUri, + }) + + if (isErr(result)) { + // Silently fail on auto-rerender errors to avoid spamming user + return + } + + // Check if we got any models + if (!result.value.models || result.value.models.length === 0) { + return + } + + // Get the originally rendered model information + const originalModelInfo = + renderedModelProvider.getModelInfoForRendered(renderedUri) + + // Find the specific model that was originally rendered, or fall back to the first model + const selectedModel = originalModelInfo + ? result.value.models.find( + model => + model.name === originalModelInfo.name && + model.fqn === originalModelInfo.fqn, + ) || result.value.models[0] + : result.value.models[0] + + // Update the existing rendered model content + renderedModelProvider.updateRenderedModel( + renderedUri, + selectedModel.rendered_query, + ) +} + export function renderModel( lspClient?: LSPClient, renderedModelProvider?: RenderedModelProvider, @@ -114,6 +159,8 @@ export function renderModel( const uri = renderedModelProvider.storeRenderedModel( selectedModel.name, selectedModel.rendered_query, + documentUri, + selectedModel, ) // Open the virtual document diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 3931da82d3..a9f883d67e 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -12,7 +12,7 @@ import { AuthenticationProviderTobikoCloud } from './auth/auth' import { signOut } from './commands/signout' import { signIn } from './commands/signin' import { signInSpecifyFlow } from './commands/signinSpecifyFlow' -import { renderModel } from './commands/renderModel' +import { renderModel, reRenderModelForSourceFile } from './commands/renderModel' import { isErr } from '@bus/result' import { handleNotSginedInError, @@ -23,6 +23,7 @@ import { import { selector, completionProvider } from './completion/completion' import { LineagePanel } from './webviews/lineagePanel' import { RenderedModelProvider } from './providers/renderedModelProvider' +import { sleep } from './utilities/sleep' let lspClient: LSPClient | undefined @@ -99,6 +100,25 @@ export async function activate(context: vscode.ExtensionContext) { ), ) + // Add file save listener for auto-rerendering models + context.subscriptions.push( + vscode.workspace.onDidSaveTextDocument(async document => { + if ( + lspClient && + renderedModelProvider.hasRenderedModelForSource( + document.uri.toString(true), + ) + ) { + await sleep(100) + await reRenderModelForSourceFile( + document.uri.toString(true), + lspClient, + renderedModelProvider, + ) + } + }), + ) + const restart = async () => { if (lspClient) { traceVerbose('Restarting LSP client') diff --git a/vscode/extension/src/providers/renderedModelProvider.ts b/vscode/extension/src/providers/renderedModelProvider.ts index e9d6f6de7d..017e071db6 100644 --- a/vscode/extension/src/providers/renderedModelProvider.ts +++ b/vscode/extension/src/providers/renderedModelProvider.ts @@ -1,4 +1,11 @@ import * as vscode from 'vscode' +import { RenderModelEntry } from '../lsp/custom' + +interface RenderedModelInfo { + content: string + sourceUri?: string + modelInfo?: RenderModelEntry +} /** * Content provider for read-only rendered SQL models @@ -8,7 +15,10 @@ export class RenderedModelProvider { private static readonly scheme = 'sqlmesh-rendered' - private renderedModels = new Map() + // Single map containing all rendered model information + private renderedModels = new Map() + // Track which source file URIs are associated with rendered models + private sourceToRenderedUri = new Map() // Event emitter for content changes private _onDidChange = new vscode.EventEmitter() @@ -19,13 +29,19 @@ export class RenderedModelProvider */ provideTextDocumentContent(uri: vscode.Uri): string { const key = uri.toString() - return this.renderedModels.get(key) || '' + const modelInfo = this.renderedModels.get(key) + return modelInfo?.content || '' } /** * Store rendered model content and create a URI for it */ - storeRenderedModel(modelName: string, content: string): vscode.Uri { + storeRenderedModel( + modelName: string, + content: string, + sourceUri?: string, + modelInfo?: RenderModelEntry, + ): vscode.Uri { const fileName = `${modelName} (rendered)` // Add a timestamp to make the URI unique for each render const timestamp = Date.now() @@ -35,11 +51,78 @@ export class RenderedModelProvider path: fileName, fragment: timestamp.toString(), }) - this.renderedModels.set(uri.toString(), content) + + const uriString = uri.toString() + + // Store all information in single map + this.renderedModels.set(uriString, { + content, + sourceUri, + modelInfo, + }) + + // Track the association between a source file and the rendered model + if (sourceUri) { + // Remove any existing mapping for this source file + const existingRenderedUri = this.sourceToRenderedUri.get(sourceUri) + if (existingRenderedUri) { + this.renderedModels.delete(existingRenderedUri.toString()) + } + + this.sourceToRenderedUri.set(sourceUri, uri) + } + this._onDidChange.fire(uri) return uri } + /** + * Update an existing rendered model with new content + */ + updateRenderedModel(uri: vscode.Uri, content: string): void { + const uriString = uri.toString() + const existingInfo = this.renderedModels.get(uriString) + if (existingInfo) { + this.renderedModels.set(uriString, { + ...existingInfo, + content, + }) + } + this._onDidChange.fire(uri) + } + + /** + * Get the rendered URI for a given source file URI + */ + getRenderedUriForSource(sourceUri: string): vscode.Uri | undefined { + return this.sourceToRenderedUri.get(sourceUri) + } + + /** + * Get the source URI for a given rendered model URI + */ + getSourceUriForRendered(renderedUri: string): string | undefined { + const modelInfo = this.renderedModels.get(renderedUri) + return modelInfo?.sourceUri + } + + /** + * Get the model information for a given rendered model URI + */ + getModelInfoForRendered( + renderedUri: vscode.Uri, + ): RenderModelEntry | undefined { + const modelInfo = this.renderedModels.get(renderedUri.toString()) + return modelInfo?.modelInfo + } + + /** + * Check if a source file has an associated rendered model + */ + hasRenderedModelForSource(sourceUri: string): boolean { + return this.sourceToRenderedUri.has(sourceUri) + } + /** * Get the URI scheme for rendered models */ @@ -52,6 +135,7 @@ export class RenderedModelProvider */ dispose() { this.renderedModels.clear() + this.sourceToRenderedUri.clear() this._onDidChange.dispose() } } From 0d0baf1056c5599a8c92868d8084c2123c31331a Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:06:46 +0100 Subject: [PATCH 0327/1056] chore(lsp): move autocomplete to the lsp (#4607) --- sqlmesh/lsp/main.py | 42 ++++++++++++++++++++++ vscode/extension/src/extension.ts | 16 +++++---- vscode/extension/src/lsp/lsp.ts | 10 ++++++ vscode/extension/tests/completions.spec.ts | 6 ++-- 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index d2b9fcade7..a83bd37fcb 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -20,6 +20,7 @@ ApiResponseGetLineage, ApiResponseGetModels, ) +from sqlmesh.lsp.completions import get_sql_completions from sqlmesh.lsp.context import ( LSPContext, ModelTarget, @@ -456,6 +457,47 @@ def workspace_diagnostic( ) return types.WorkspaceDiagnosticReport(items=[]) + @self.server.feature(types.TEXT_DOCUMENT_COMPLETION) + def completion( + ls: LanguageServer, params: types.CompletionParams + ) -> t.Optional[types.CompletionList]: + """Handle completion requests from the client.""" + try: + uri = URI(params.text_document.uri) + context = self._context_get_or_load(uri) + + # Get completions using the existing completions module + completion_response = context.get_autocomplete(uri) + + completion_items = [] + # Add model completions + for model in completion_response.models: + completion_items.append( + types.CompletionItem( + label=model, + kind=types.CompletionItemKind.Reference, + detail="SQLMesh Model", + ) + ) + # Add keyword completions + for keyword in completion_response.keywords: + completion_items.append( + types.CompletionItem( + label=keyword, + kind=types.CompletionItemKind.Keyword, + detail="SQL Keyword", + ) + ) + + return types.CompletionList( + is_incomplete=False, + items=completion_items, + ) + + except Exception as e: + get_sql_completions(None, URI(params.text_document.uri)) + return None + def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic], int]: """Get diagnostics for a specific URI, returning (diagnostics, result_id). diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index a9f883d67e..67e57fcbb8 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -84,13 +84,6 @@ export async function activate(context: vscode.ExtensionContext) { ), ) - context.subscriptions.push( - vscode.languages.registerCompletionItemProvider( - selector, - completionProvider(lspClient), - ), - ) - // Register the webview const lineagePanel = new LineagePanel(context.extensionUri, lspClient) context.subscriptions.push( @@ -185,6 +178,15 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(lspClient) } + if (lspClient && !lspClient.hasCompletionCapability()) { + context.subscriptions.push( + vscode.languages.registerCompletionItemProvider( + selector, + completionProvider(lspClient), + ), + ) + } + traceInfo('Extension activated') } diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 989eabafd3..d23c663c62 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -21,6 +21,16 @@ export class LSPClient implements Disposable { this.client = undefined } + public hasCompletionCapability(): boolean { + if (!this.client) { + traceError('LSP client is not initialized') + return false + } + const capabilities = this.client.initializeResult?.capabilities + const completion = capabilities?.completionProvider + return completion !== undefined + } + public async start(): Promise> { if (!outputChannel) { outputChannel = window.createOutputChannel('sqlmesh-lsp') diff --git a/vscode/extension/tests/completions.spec.ts b/vscode/extension/tests/completions.spec.ts index 27d305c0eb..f0d167b91f 100644 --- a/vscode/extension/tests/completions.spec.ts +++ b/vscode/extension/tests/completions.spec.ts @@ -38,13 +38,15 @@ test('Autocomplete for model names', async () => { await window.keyboard.press('Enter') // Type the beginning of sushi.customers to trigger autocomplete - await window.keyboard.type('sushi.cus') + await window.keyboard.type('sushi.waiter_as_customer') // Wait a moment for autocomplete to appear await window.waitForTimeout(500) // Check if the autocomplete suggestion for sushi.customers is visible - await expect(window.locator('text=sushi.customers')).toBeVisible() + expect( + await window.locator('text=sushi.waiter_as_customer_by_day').count(), + ).toBe(1) await close() } finally { From f6f441a0782b07c8ca3d870909f37689aa9475ea Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:21:24 -0500 Subject: [PATCH 0328/1056] Feat: Clickhouse HTTPS config parameters (#4571) --- docs/integrations/engines/clickhouse.md | 25 +++++++++++++++++++++ sqlmesh/core/config/connection.py | 30 +++++++++++++++++++++++-- tests/core/test_connection_config.py | 15 +++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/docs/integrations/engines/clickhouse.md b/docs/integrations/engines/clickhouse.md index fb48ac860a..14e931b046 100644 --- a/docs/integrations/engines/clickhouse.md +++ b/docs/integrations/engines/clickhouse.md @@ -421,4 +421,29 @@ If a model has many records in each partition, you may see additional performanc Choose a model's time partitioning granularity based on the characteristics of the data it will process, making sure the total number of partitions is 1000 or fewer. ## Local/Built-in Scheduler + **Engine Adapter Type**: `clickhouse` + +| Option | Description | Type | Required | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----: | :------: | +| `type` | Engine type name - must be `clickhouse` | string | Y | +| `host` | ClickHouse server hostname or IP address | string | Y | +| `username` | ClickHouse user name | string | Y | +| `password` | ClickHouse user password | string | N | +| `port` | The ClickHouse HTTP or HTTPS port (Default: `8123`) | int | N | +| `cluster` | ClickHouse cluster name | string | N | +| `connect_timeout` | Connection timeout in seconds (Default: `10`) | int | N | +| `send_receive_timeout` | Send/receive timeout in seconds (Default: `300`) | int | N | +| `query_limit` | Query result limit (Default: `0` - no limit) | int | N | +| `use_compression` | Whether to use compression (Default: `True`) | bool | N | +| `compression_method` | Compression method to use | string | N | +| `http_proxy` | HTTP proxy address (equivalent to setting the HTTP_PROXY environment variable) | string | N | +| `verify` | Verify server TLS/SSL certificate (Default: `True`) | bool | N | +| `ca_cert` | Ignored if verify is `False`. If verify is `True`, the file path to Certificate Authority root to validate ClickHouse server certificate, in .pem format. Not necessary if the ClickHouse server certificate is a globally trusted root as verified by the operating system. | string | N | +| `client_cert` | File path to a TLS Client certificate in .pem format (for mutual TLS authentication). The file should contain a full certificate chain, including any intermediate certificates. | string | N | +| `client_cert_key` | File path to the private key for the Client Certificate. Required if the private key is not included the Client Certificate key file. | string | N | +| `https_proxy` | HTTPS proxy address (equivalent to setting the HTTPS_PROXY environment variable) | string | N | +| `server_host_name` | The ClickHouse server hostname as identified by the CN or SNI of its TLS certificate. Set this to avoid SSL errors when connecting through a proxy or tunnel with a different hostname. | string | N | +| `tls_mode` | Controls advanced TLS behavior. proxy and strict do not invoke ClickHouse mutual TLS connection, but do send client cert and key. mutual assumes ClickHouse mutual TLS auth with a client certificate. | string | N | +| `connection_settings` | Additional [connection settings](https://clickhouse.com/docs/integrations/python#settings-argument) | dict | N | +| `connection_pool_options` | Additional [options](https://clickhouse.com/docs/integrations/python#customizing-the-http-connection-pool) for the HTTP connection pool | dict | N | \ No newline at end of file diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 1ba66a0abd..0c7055a59d 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -1737,11 +1737,19 @@ class ClickhouseConnectionConfig(ConnectionConfig): cluster: t.Optional[str] = None connect_timeout: int = 10 send_receive_timeout: int = 300 - verify: bool = True query_limit: int = 0 use_compression: bool = True compression_method: t.Optional[str] = None connection_settings: t.Optional[t.Dict[str, t.Any]] = None + http_proxy: t.Optional[str] = None + # HTTPS/TLS settings + verify: bool = True + ca_cert: t.Optional[str] = None + client_cert: t.Optional[str] = None + client_cert_key: t.Optional[str] = None + https_proxy: t.Optional[str] = None + server_host_name: t.Optional[str] = None + tls_mode: t.Optional[str] = None concurrent_tasks: int = 1 register_comments: bool = True @@ -1766,8 +1774,15 @@ def _connection_kwargs_keys(self) -> t.Set[str]: "password", "connect_timeout", "send_receive_timeout", - "verify", "query_limit", + "http_proxy", + "verify", + "ca_cert", + "client_cert", + "client_cert_key", + "https_proxy", + "server_host_name", + "tls_mode", } return kwargs @@ -1786,7 +1801,18 @@ def _connection_factory(self) -> t.Callable: maxsize=self.concurrent_tasks, # Block if there are no free connections block=True, + verify=self.verify, + ca_cert=self.ca_cert, + client_cert=self.client_cert, + client_cert_key=self.client_cert_key, + https_proxy=self.https_proxy, ) + # this doesn't happen automatically because we always supply our own pool manager to the connection + # https://github.com/ClickHouse/clickhouse-connect/blob/3a7f4b04cad29c7c2536661b831fb744248e2ec0/clickhouse_connect/driver/httpclient.py#L109 + if self.server_host_name: + pool_manager_options["server_hostname"] = self.server_host_name + if self.verify: + pool_manager_options["assert_hostname"] = self.server_host_name if self.connection_pool_options: pool_manager_options.update(self.connection_pool_options) pool_mgr = httputil.get_pool_manager(**pool_manager_options) diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 63a32e8a6d..d106559b67 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -902,6 +902,13 @@ def test_clickhouse(make_config): cluster="default", use_compression=True, connection_settings={"this_setting": "1"}, + server_host_name="server_host_name", + verify=True, + ca_cert="ca_cert", + client_cert="client_cert", + client_cert_key="client_cert_key", + https_proxy="https://proxy", + connection_pool_options={"pool_option": "value"}, ) assert isinstance(config, ClickhouseConnectionConfig) assert config.cluster == "default" @@ -912,6 +919,14 @@ def test_clickhouse(make_config): assert config.is_recommended_for_state_sync is False assert config.is_forbidden_for_state_sync + pool = config._connection_factory.keywords["pool_mgr"] + assert pool.connection_pool_kw["server_hostname"] == "server_host_name" + assert pool.connection_pool_kw["assert_hostname"] == "server_host_name" # because verify=True + assert pool.connection_pool_kw["ca_certs"] == "ca_cert" + assert pool.connection_pool_kw["cert_file"] == "client_cert" + assert pool.connection_pool_kw["key_file"] == "client_cert_key" + assert pool.connection_pool_kw["pool_option"] == "value" + config2 = make_config( type="clickhouse", host="localhost", From a29ad26e8494246ad047f02bde8632c783c949a9 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 3 Jun 2025 08:28:23 -0700 Subject: [PATCH 0329/1056] Chore: Fix usage of cached properties in the Context (#4630) --- sqlmesh/core/context.py | 34 ++++++++++------------------------ 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index e42b5d4f86..e3461f31d6 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -367,11 +367,7 @@ def __init__( self._requirements: t.Dict[str, str] = {} self._environment_statements: t.List[EnvironmentStatements] = [] self._excluded_requirements: t.Set[str] = set() - self._default_catalog: t.Optional[str] = None - self._default_catalog_per_gateway: t.Optional[t.Dict[str, str]] = None self._engine_adapter: t.Optional[EngineAdapter] = None - self._connection_config: t.Optional[ConnectionConfig] = None - self._test_connection_config: t.Optional[ConnectionConfig] = None self._linters: t.Dict[str, Linter] = {} self._loaded: bool = False @@ -967,11 +963,9 @@ def requirements(self) -> t.Dict[str, str]: """Returns the Python dependencies of the project loaded in this context.""" return self._requirements.copy() - @property + @cached_property def default_catalog(self) -> t.Optional[str]: - if self._default_catalog is None and self.default_catalog_per_gateway: - self._default_catalog = self.default_catalog_per_gateway[self.selected_gateway] - return self._default_catalog + return self.default_catalog_per_gateway.get(self.selected_gateway) @python_api_analytics def render( @@ -2516,13 +2510,9 @@ def engine_adapters(self) -> t.Dict[str, EngineAdapter]: @cached_property def default_catalog_per_gateway(self) -> t.Dict[str, str]: """Returns the default catalogs for each engine adapter.""" - if self._default_catalog_per_gateway is None: - self._default_catalog_per_gateway = self._scheduler.get_default_catalog_per_gateway( - self - ) - return self._default_catalog_per_gateway + return self._scheduler.get_default_catalog_per_gateway(self) - @cached_property + @property def concurrent_tasks(self) -> int: if self._concurrent_tasks is None: self._concurrent_tasks = self.connection_config.concurrent_tasks @@ -2530,19 +2520,15 @@ def concurrent_tasks(self) -> int: @cached_property def connection_config(self) -> ConnectionConfig: - if self._connection_config is None: - self._connection_config = self.config.get_connection(self.selected_gateway) - return self._connection_config + return self.config.get_connection(self.selected_gateway) @cached_property def test_connection_config(self) -> ConnectionConfig: - if self._test_connection_config is None: - self._test_connection_config = self.config.get_test_connection( - self.gateway, - self.default_catalog, - default_catalog_dialect=self.config.dialect, - ) - return self._test_connection_config + return self.config.get_test_connection( + self.gateway, + self.default_catalog, + default_catalog_dialect=self.config.dialect, + ) @cached_property def environment_catalog_mapping(self) -> RegexKeyDict: From 67428f0efedce7875e6bbd04099cc5a2b13b1e14 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:41:33 -0700 Subject: [PATCH 0330/1056] feat: remove module pandas import to improve import speed (#4631) --- .../ibis/models/ibis_full_model_python.py | 2 +- examples/sushi/models/items.py | 2 +- examples/sushi/models/order_items.py | 2 +- examples/sushi/models/orders.py | 2 +- examples/sushi/models/raw_marketing.py | 2 +- examples/wursthall/models/db/order_f.py | 2 +- .../wursthall/models/src/customer_details.py | 2 +- .../models/src/order_item_details.py | 2 +- pyproject.toml | 6 ++ sqlmesh/core/console.py | 2 +- sqlmesh/core/context.py | 6 +- sqlmesh/core/dialect.py | 3 +- sqlmesh/core/engine_adapter/_typing.py | 2 +- sqlmesh/core/engine_adapter/base.py | 17 ++++- sqlmesh/core/engine_adapter/bigquery.py | 4 +- sqlmesh/core/engine_adapter/clickhouse.py | 3 +- sqlmesh/core/engine_adapter/databricks.py | 5 +- sqlmesh/core/engine_adapter/mssql.py | 9 ++- sqlmesh/core/engine_adapter/redshift.py | 7 +- sqlmesh/core/engine_adapter/snowflake.py | 8 +- sqlmesh/core/engine_adapter/spark.py | 4 +- sqlmesh/core/engine_adapter/trino.py | 6 +- sqlmesh/core/model/definition.py | 3 +- sqlmesh/core/model/seed.py | 6 +- sqlmesh/core/snapshot/evaluator.py | 5 +- sqlmesh/core/state_sync/db/environment.py | 8 +- sqlmesh/core/state_sync/db/interval.py | 8 +- sqlmesh/core/state_sync/db/snapshot.py | 7 +- sqlmesh/core/state_sync/db/version.py | 3 +- sqlmesh/core/table_diff.py | 20 ++++- sqlmesh/core/test/definition.py | 15 +++- sqlmesh/dbt/adapter.py | 3 +- sqlmesh/dbt/util.py | 6 +- .../v0007_env_table_info_to_kind.py | 3 +- .../migrations/v0009_remove_pre_post_hooks.py | 3 +- .../migrations/v0011_add_model_kind_name.py | 3 +- .../v0012_update_jinja_expressions.py | 3 +- .../v0013_serde_using_model_dialects.py | 3 +- sqlmesh/migrations/v0016_fix_windows_path.py | 3 +- .../migrations/v0017_fix_windows_seed_path.py | 3 +- .../v0018_rename_snapshot_model_to_node.py | 3 +- ...ve_redundant_attributes_from_dbt_models.py | 3 +- .../migrations/v0021_fix_table_properties.py | 3 +- .../migrations/v0022_move_project_to_model.py | 3 +- ...replace_model_kind_name_enum_with_value.py | 3 +- ...x_intervals_and_missing_change_category.py | 3 +- .../v0026_remove_dialect_from_seed.py | 3 +- .../v0027_minute_interval_to_five.py | 3 +- ...029_generate_schema_types_using_dialect.py | 3 +- .../v0030_update_unrestorable_snapshots.py | 3 +- .../v0031_remove_dbt_target_fields.py | 3 +- .../migrations/v0034_add_default_catalog.py | 3 +- .../v0037_remove_dbt_is_incremental_macro.py | 3 +- .../v0038_add_expiration_ts_to_snapshot.py | 3 +- ...39_include_environment_in_plan_dag_spec.py | 3 +- .../v0041_remove_hash_raw_query_attribute.py | 3 +- .../v0042_trim_indirect_versions.py | 3 +- ...remove_obsolete_attributes_in_plan_dags.py | 3 +- .../migrations/v0045_move_gateway_variable.py | 3 +- .../v0048_drop_indirect_versions.py | 3 +- .../v0051_rename_column_descriptions.py | 3 +- ...used_ts_ttl_ms_unrestorable_to_snapshot.py | 3 +- .../migrations/v0060_move_audits_to_model.py | 3 +- sqlmesh/migrations/v0063_change_signals.py | 3 +- .../v0064_join_when_matched_strings.py | 3 +- .../v0069_update_dev_table_suffix.py | 3 +- .../v0071_add_dev_version_to_intervals.py | 5 +- ...073_remove_symbolic_disable_restatement.py | 3 +- .../migrations/v0075_remove_validate_query.py | 3 +- .../migrations/v0081_update_partitioned_by.py | 3 +- sqlmesh/utils/date.py | 11 ++- sqlmesh/utils/pandas.py | 76 +++++++++++-------- tests/conftest.py | 2 +- .../engine_adapter/integration/__init__.py | 2 +- .../integration/test_integration.py | 4 +- .../integration/test_integration_athena.py | 2 +- .../test_integration_clickhouse.py | 2 +- tests/core/engine_adapter/test_athena.py | 2 +- tests/core/engine_adapter/test_base.py | 2 +- tests/core/engine_adapter/test_bigquery.py | 2 +- tests/core/engine_adapter/test_databricks.py | 2 +- tests/core/engine_adapter/test_duckdb.py | 2 +- tests/core/engine_adapter/test_mssql.py | 2 +- tests/core/engine_adapter/test_redshift.py | 2 +- tests/core/engine_adapter/test_snowflake.py | 2 +- tests/core/state_sync/test_state_sync.py | 2 +- tests/core/test_context.py | 2 +- tests/core/test_integration.py | 4 +- tests/core/test_loader.py | 4 +- tests/core/test_model.py | 8 +- tests/core/test_schema_loader.py | 2 +- tests/core/test_seed.py | 2 +- tests/core/test_snapshot_evaluator.py | 4 +- tests/core/test_table_diff.py | 2 +- tests/core/test_test.py | 2 +- tests/dbt/test_integration.py | 2 +- tests/dbt/test_util.py | 2 +- tests/fixtures/migrations/snapshots.json | 2 +- tests/utils/pandas.py | 2 +- tests/utils/test_date.py | 2 +- tests/utils/test_metaprogramming.py | 2 +- web/server/api/endpoints/commands.py | 3 +- web/server/utils.py | 4 +- 103 files changed, 313 insertions(+), 148 deletions(-) diff --git a/examples/ibis/models/ibis_full_model_python.py b/examples/ibis/models/ibis_full_model_python.py index 81dcfcfd5c..1ded44775d 100644 --- a/examples/ibis/models/ibis_full_model_python.py +++ b/examples/ibis/models/ibis_full_model_python.py @@ -2,7 +2,7 @@ from datetime import datetime import ibis # type: ignore -import pandas as pd +import pandas as pd # noqa: TID253 from constants import DB_PATH # type: ignore from sqlglot import exp diff --git a/examples/sushi/models/items.py b/examples/sushi/models/items.py index f96f9184e1..9331f43fa1 100644 --- a/examples/sushi/models/items.py +++ b/examples/sushi/models/items.py @@ -3,7 +3,7 @@ from datetime import datetime import numpy as np -import pandas as pd +import pandas as pd # noqa: TID253 from helper import iter_dates # type: ignore from sqlglot.expressions import to_column diff --git a/examples/sushi/models/order_items.py b/examples/sushi/models/order_items.py index 2d6c6124ae..07ed351d9f 100644 --- a/examples/sushi/models/order_items.py +++ b/examples/sushi/models/order_items.py @@ -3,7 +3,7 @@ from datetime import datetime import numpy as np -import pandas as pd +import pandas as pd # noqa: TID253 from helper import iter_dates # type: ignore from sqlglot import exp from sqlglot.expressions import to_column diff --git a/examples/sushi/models/orders.py b/examples/sushi/models/orders.py index ef663bf50f..aa0e04559f 100644 --- a/examples/sushi/models/orders.py +++ b/examples/sushi/models/orders.py @@ -2,7 +2,7 @@ import typing as t from datetime import datetime, timedelta -import pandas as pd +import pandas as pd # noqa: TID253 from helper import iter_dates # type: ignore from sqlmesh import ExecutionContext, model diff --git a/examples/sushi/models/raw_marketing.py b/examples/sushi/models/raw_marketing.py index 3bfc410480..f02b3596fa 100644 --- a/examples/sushi/models/raw_marketing.py +++ b/examples/sushi/models/raw_marketing.py @@ -3,7 +3,7 @@ from datetime import datetime import numpy as np -import pandas as pd +import pandas as pd # noqa: TID253 from sqlglot import exp from sqlmesh import ExecutionContext, model diff --git a/examples/wursthall/models/db/order_f.py b/examples/wursthall/models/db/order_f.py index f4804b4459..fe61654148 100644 --- a/examples/wursthall/models/db/order_f.py +++ b/examples/wursthall/models/db/order_f.py @@ -3,7 +3,7 @@ from datetime import datetime import numpy as np -import pandas as pd +import pandas as pd # noqa: TID253 from models.src.shared import DATA_START_DATE_STR, set_seed # type: ignore from sqlglot import parse_one diff --git a/examples/wursthall/models/src/customer_details.py b/examples/wursthall/models/src/customer_details.py index 95914b1e53..44c56a60e5 100644 --- a/examples/wursthall/models/src/customer_details.py +++ b/examples/wursthall/models/src/customer_details.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta -import pandas as pd +import pandas as pd # noqa: TID253 from faker import Faker from models.src.shared import DATA_START_DATE_STR, iter_dates, set_seed # type: ignore diff --git a/examples/wursthall/models/src/order_item_details.py b/examples/wursthall/models/src/order_item_details.py index 32ce003cb7..a5f880c494 100644 --- a/examples/wursthall/models/src/order_item_details.py +++ b/examples/wursthall/models/src/order_item_details.py @@ -4,7 +4,7 @@ from datetime import datetime import numpy as np -import pandas as pd +import pandas as pd # noqa: TID253 from faker import Faker from models.src.shared import DATA_START_DATE_STR, iter_dates, set_seed # type: ignore from sqlglot import parse_one diff --git a/pyproject.toml b/pyproject.toml index 9129dafc5e..276f305a0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -271,3 +271,9 @@ select = [ "F401", "RET505", ] +extend-select = ["TID"] + +[tool.ruff.lint.flake8-tidy-imports] +banned-module-level-imports = [ + "pandas", +] diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index bdd92c24d2..fcb8ceb864 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -8,7 +8,6 @@ import logging import textwrap from pathlib import Path -import pandas as pd import numpy as np from hyperscript import h from rich.console import Console as RichConsole @@ -2497,6 +2496,7 @@ def show_linter_violations( def _cells_match(x: t.Any, y: t.Any) -> bool: """Helper function to compare two cells and returns true if they're equal, handling array objects.""" + import pandas as pd # Convert array-like objects to list for consistent comparison def _normalize(val: t.Any) -> t.Any: diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index e3461f31d6..1bc1276710 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -48,7 +48,6 @@ from shutil import rmtree from types import MappingProxyType -import pandas as pd from sqlglot import Dialect, exp from sqlglot.helper import first from sqlglot.lineage import GraphHTML @@ -134,6 +133,7 @@ from sqlmesh.utils.jinja import JinjaMacroRegistry if t.TYPE_CHECKING: + import pandas as pd from typing_extensions import Literal from sqlmesh.core.engine_adapter._typing import ( @@ -1007,6 +1007,8 @@ def render( expand = self.dag.upstream(model.fqn) if expand is True else expand or [] if model.is_seed: + import pandas as pd + df = next( model.render( context=self.execution_context( @@ -2013,6 +2015,8 @@ def test( ) -> ModelTextTestResult: """Discover and run model tests""" if verbosity >= Verbosity.VERBOSE: + import pandas as pd + pd.set_option("display.max_columns", None) test_meta = self.load_model_tests(tests=tests, patterns=match_patterns) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index ad5be2e884..279c9b6078 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -10,7 +10,6 @@ from enum import Enum, auto from functools import lru_cache -import pandas as pd from sqlglot import Dialect, Generator, ParseError, Parser, Tokenizer, TokenType, exp from sqlglot.dialects.dialect import DialectType from sqlglot.dialects import DuckDB, Snowflake @@ -27,6 +26,8 @@ from sqlmesh.utils.pandas import columns_to_types_from_df if t.TYPE_CHECKING: + import pandas as pd + from sqlglot._typing import E diff --git a/sqlmesh/core/engine_adapter/_typing.py b/sqlmesh/core/engine_adapter/_typing.py index ba06887bd1..143fcf6ab6 100644 --- a/sqlmesh/core/engine_adapter/_typing.py +++ b/sqlmesh/core/engine_adapter/_typing.py @@ -1,11 +1,11 @@ import typing as t -import pandas as pd from sqlglot import exp from sqlmesh.utils import optional_import if t.TYPE_CHECKING: + import pandas as pd import pyspark import pyspark.sql.connect.dataframe from bigframes.session import Session as BigframeSession # noqa diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 5586418c79..29ca98fc2b 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -16,7 +16,6 @@ import typing as t from functools import partial -import pandas as pd from sqlglot import Dialect, exp from sqlglot.errors import ErrorLevel from sqlglot.helper import ensure_list @@ -51,6 +50,8 @@ from sqlmesh.utils.pandas import columns_to_types_from_df if t.TYPE_CHECKING: + import pandas as pd + from sqlmesh.core._typing import SchemaName, SessionProperties, TableName from sqlmesh.core.engine_adapter._typing import ( BigframeSession, @@ -217,6 +218,8 @@ def _get_source_queries( *, batch_size: t.Optional[int] = None, ) -> t.List[SourceQuery]: + import pandas as pd + batch_size = self.DEFAULT_BATCH_SIZE if batch_size is None else batch_size if isinstance(query_or_df, (exp.Query, exp.DerivedTable)): return [SourceQuery(query_factory=lambda: query_or_df)] # type: ignore @@ -244,6 +247,8 @@ def _df_to_source_queries( batch_size: int, target_table: TableName, ) -> t.List[SourceQuery]: + import pandas as pd + assert isinstance(df, pd.DataFrame) num_rows = len(df.index) batch_size = sys.maxsize if batch_size == 0 else batch_size @@ -257,7 +262,7 @@ def _df_to_source_queries( SourceQuery( query_factory=partial( self._values_to_sql, - values=values, + values=values, # type: ignore columns_to_types=columns_to_types, batch_start=i, batch_end=min(i + batch_size, num_rows), @@ -295,6 +300,8 @@ def _columns_to_types( def _columns_to_types( self, query_or_df: QueryOrDF, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None ) -> t.Optional[t.Dict[str, exp.DataType]]: + import pandas as pd + if columns_to_types: return columns_to_types if isinstance(query_or_df, pd.DataFrame): @@ -1006,6 +1013,8 @@ def create_view( view_properties: Optional view properties to add to the view. create_kwargs: Additional kwargs to pass into the Create expression """ + import pandas as pd + if materialized_properties and not materialized: raise SQLMeshError("Materialized properties are only supported for materialized views") @@ -2012,6 +2021,8 @@ def _native_df_to_pandas_df( """ Take a "native" DataFrame (eg Pyspark, Bigframe, Snowpark etc) and convert it to Pandas """ + import pandas as pd + if isinstance(query_or_df, (exp.Query, exp.DerivedTable, pd.DataFrame)): return query_or_df @@ -2022,6 +2033,8 @@ def fetchdf( self, query: t.Union[exp.Expression, str], quote_identifiers: bool = False ) -> pd.DataFrame: """Fetches a Pandas DataFrame from the cursor""" + import pandas as pd + df = self._fetch_native_df(query, quote_identifiers=quote_identifiers) if not isinstance(df, pd.DataFrame): raise NotImplementedError( diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index dd0e0498e3..3bd5b1cbe3 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -4,7 +4,6 @@ import typing as t from collections import defaultdict -import pandas as pd from sqlglot import exp, parse_one from sqlglot.transforms import remove_precision_parameterized_types @@ -29,6 +28,7 @@ from sqlmesh.utils.pandas import columns_to_types_from_dtypes if t.TYPE_CHECKING: + import pandas as pd from google.api_core.retry import Retry from google.cloud import bigquery from google.cloud.bigquery import StandardSqlDataType @@ -147,6 +147,8 @@ def _df_to_source_queries( batch_size: int, target_table: TableName, ) -> t.List[SourceQuery]: + import pandas as pd + temp_bq_table = self.__get_temp_bq_table( self._get_temp_table(target_table or "pandas"), columns_to_types ) diff --git a/sqlmesh/core/engine_adapter/clickhouse.py b/sqlmesh/core/engine_adapter/clickhouse.py index 58993683dd..fb515b7291 100644 --- a/sqlmesh/core/engine_adapter/clickhouse.py +++ b/sqlmesh/core/engine_adapter/clickhouse.py @@ -2,7 +2,6 @@ import typing as t import logging -import pandas as pd import re from sqlglot import exp, maybe_parse from sqlmesh.core.dialect import to_schema @@ -19,6 +18,8 @@ from sqlmesh.core.schema_diff import SchemaDiffer if t.TYPE_CHECKING: + import pandas as pd + from sqlmesh.core._typing import SchemaName, TableName from sqlmesh.core.engine_adapter._typing import DF, Query, QueryOrDF diff --git a/sqlmesh/core/engine_adapter/databricks.py b/sqlmesh/core/engine_adapter/databricks.py index 3671f7dbdc..9d35726a32 100644 --- a/sqlmesh/core/engine_adapter/databricks.py +++ b/sqlmesh/core/engine_adapter/databricks.py @@ -4,7 +4,6 @@ import typing as t from functools import partial -import pandas as pd from sqlglot import exp from sqlmesh.core.dialect import to_schema from sqlmesh.core.engine_adapter.shared import ( @@ -21,6 +20,8 @@ from sqlmesh.utils.errors import SQLMeshError, MissingDefaultCatalogError if t.TYPE_CHECKING: + import pandas as pd + from sqlmesh.core._typing import SchemaName, TableName, SessionProperties from sqlmesh.core.engine_adapter._typing import DF, PySparkSession, Query @@ -199,6 +200,8 @@ def fetchdf( """ Returns a Pandas DataFrame from a query or expression. """ + import pandas as pd + df = self._fetch_native_df(query, quote_identifiers=quote_identifiers) if not isinstance(df, pd.DataFrame): return df.toPandas() diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index 240352b1f0..e94afcf0db 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -5,8 +5,6 @@ import typing as t import numpy as np -import pandas as pd -from pandas.api.types import is_datetime64_any_dtype # type: ignore from sqlglot import exp from sqlmesh.core.dialect import to_schema @@ -189,6 +187,9 @@ def drop_schema( super().drop_schema(schema_name, ignore_if_not_exists=ignore_if_not_exists, cascade=False) def _convert_df_datetime(self, df: DF, columns_to_types: t.Dict[str, exp.DataType]) -> None: + import pandas as pd + from pandas.api.types import is_datetime64_any_dtype # type: ignore + # pymssql doesn't convert Pandas Timestamp (datetime64) types # - this code is based on snowflake adapter implementation for column, kind in columns_to_types.items(): @@ -213,6 +214,8 @@ def _df_to_source_queries( batch_size: int, target_table: TableName, ) -> t.List[SourceQuery]: + import pandas as pd + assert isinstance(df, pd.DataFrame) temp_table = self._get_temp_table(target_table or "pandas") @@ -247,6 +250,8 @@ def _get_data_objects( """ Returns all the data objects that exist in the given schema and catalog. """ + import pandas as pd + catalog = self.get_current_catalog() query = ( exp.select( diff --git a/sqlmesh/core/engine_adapter/redshift.py b/sqlmesh/core/engine_adapter/redshift.py index f314f704b6..946d4ee318 100644 --- a/sqlmesh/core/engine_adapter/redshift.py +++ b/sqlmesh/core/engine_adapter/redshift.py @@ -3,7 +3,6 @@ import logging import typing as t -import pandas as pd from sqlglot import exp from sqlmesh.core.dialect import to_schema @@ -27,6 +26,8 @@ from sqlmesh.utils.errors import SQLMeshError if t.TYPE_CHECKING: + import pandas as pd + from sqlmesh.core._typing import SchemaName, TableName from sqlmesh.core.engine_adapter.base import QueryOrDF, Query @@ -142,6 +143,8 @@ def _fetch_native_df( self, query: t.Union[exp.Expression, str], quote_identifiers: bool = False ) -> pd.DataFrame: """Fetches a Pandas DataFrame from the cursor""" + import pandas as pd + self.execute(query, quote_identifiers=quote_identifiers) # We manually build the `DataFrame` here because the driver's `fetch_dataframe` @@ -257,6 +260,8 @@ def replace_query( If it does exist then we need to do the: `CREATE TABLE...`, `INSERT INTO...`, `RENAME TABLE...`, `RENAME TABLE...`, DROP TABLE...` dance. """ + import pandas as pd + if not isinstance(query_or_df, pd.DataFrame) or not self.table_exists(table_name): return super().replace_query( table_name, diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index b6144c668a..8b05973c19 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -4,8 +4,6 @@ import logging import typing as t -import pandas as pd -from pandas.api.types import is_datetime64_any_dtype # type: ignore from sqlglot import exp from sqlglot.helper import ensure_list from sqlglot.optimizer.normalize_identifiers import normalize_identifiers @@ -33,6 +31,8 @@ snowpark = optional_import("snowflake.snowpark") if t.TYPE_CHECKING: + import pandas as pd + from sqlmesh.core._typing import SchemaName, SessionProperties, TableName from sqlmesh.core.engine_adapter._typing import DF, Query, QueryOrDF, SnowparkSession from sqlmesh.core.node import IntervalUnit @@ -290,6 +290,9 @@ def _df_to_source_queries( batch_size: int, target_table: TableName, ) -> t.List[SourceQuery]: + import pandas as pd + from pandas.api.types import is_datetime64_any_dtype + temp_table = self._get_temp_table( target_table or "pandas", quoted=False ) # write_pandas() re-quotes everything without checking if its already quoted @@ -391,6 +394,7 @@ def cleanup() -> None: def _fetch_native_df( self, query: t.Union[exp.Expression, str], quote_identifiers: bool = False ) -> DF: + import pandas as pd from snowflake.connector.errors import NotSupportedError self.execute(query, quote_identifiers=quote_identifiers) diff --git a/sqlmesh/core/engine_adapter/spark.py b/sqlmesh/core/engine_adapter/spark.py index 43dab16e8a..799d46a9c5 100644 --- a/sqlmesh/core/engine_adapter/spark.py +++ b/sqlmesh/core/engine_adapter/spark.py @@ -4,7 +4,6 @@ import typing as t from functools import partial -import pandas as pd from sqlglot import exp from sqlmesh.core.dialect import to_schema @@ -28,6 +27,7 @@ from sqlmesh.utils.errors import SQLMeshError if t.TYPE_CHECKING: + import pandas as pd from pyspark.sql import types as spark_types from sqlmesh.core._typing import SchemaName, TableName @@ -232,6 +232,8 @@ def try_get_pyspark_df(cls, value: t.Any) -> t.Optional[PySparkDataFrame]: @classmethod def try_get_pandas_df(cls, value: t.Any) -> t.Optional[pd.DataFrame]: + import pandas as pd + if isinstance(value, pd.DataFrame): return value return None diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index 303c30258d..aed69d47f9 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -2,8 +2,7 @@ import re import typing as t from functools import lru_cache -import pandas as pd -from pandas.api.types import is_datetime64_any_dtype # type: ignore + from sqlglot import exp from sqlglot.helper import seq_get from tenacity import retry, wait_fixed, stop_after_attempt, retry_if_result @@ -194,6 +193,9 @@ def _df_to_source_queries( batch_size: int, target_table: TableName, ) -> t.List[SourceQuery]: + import pandas as pd + from pandas.api.types import is_datetime64_any_dtype # type: ignore + assert isinstance(df, pd.DataFrame) # Trino does not accept timestamps in ISOFORMAT that include the "T". `execution_time` is stored in diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 4ca865669a..a8da3577bb 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -8,7 +8,6 @@ from functools import cached_property, partial from pathlib import Path -import pandas as pd import numpy as np from pydantic import Field from sqlglot import diff, exp @@ -1596,6 +1595,8 @@ def render_seed(self) -> t.Iterator[QueryOrDF]: # convert all date/time types to native pandas timestamp for column in [*date_columns, *datetime_columns]: + import pandas as pd + df[column] = pd.to_datetime(df[column]) # extract datetime.date from pandas timestamp for DATE columns diff --git a/sqlmesh/core/model/seed.py b/sqlmesh/core/model/seed.py index f352834b89..fe1aa85204 100644 --- a/sqlmesh/core/model/seed.py +++ b/sqlmesh/core/model/seed.py @@ -6,7 +6,6 @@ from io import StringIO from pathlib import Path -import pandas as pd from sqlglot import exp from sqlglot.dialects.dialect import UNESCAPED_SEQUENCES from sqlglot.helper import seq_get @@ -16,6 +15,9 @@ from sqlmesh.utils.pandas import columns_to_types_from_df from sqlmesh.utils.pydantic import PydanticModel, field_validator +if t.TYPE_CHECKING: + import pandas as pd + logger = logging.getLogger(__name__) NaHashables = t.List[t.Union[int, str, bool, t.Literal[None]]] @@ -115,6 +117,8 @@ def read(self, batch_size: t.Optional[int] = None) -> t.Generator[pd.DataFrame, batch_start += batch_size def _get_df(self) -> pd.DataFrame: + import pandas as pd + if self._df is None: self._df = pd.read_csv( StringIO(self.content), diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 3c3f47c538..39a8f5147c 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -30,7 +30,6 @@ from contextlib import contextmanager from functools import reduce -import pandas as pd from sqlglot import exp, select from sqlglot.executor import execute @@ -736,6 +735,8 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: ) if limit is not None: + import pandas as pd + query_or_df = next(queries_or_dfs) if isinstance(query_or_df, pd.DataFrame): return query_or_df.head(limit) @@ -769,6 +770,8 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: ) and snapshot.is_incremental_by_time_range ): + import pandas as pd + query_or_df = reduce( lambda a, b: ( pd.concat([a, b], ignore_index=True) # type: ignore diff --git a/sqlmesh/core/state_sync/db/environment.py b/sqlmesh/core/state_sync/db/environment.py index cde12161d5..dcf915a91c 100644 --- a/sqlmesh/core/state_sync/db/environment.py +++ b/sqlmesh/core/state_sync/db/environment.py @@ -1,7 +1,6 @@ from __future__ import annotations import typing as t -import pandas as pd import json import logging from sqlglot import exp @@ -17,6 +16,9 @@ from sqlmesh.utils.date import now_timestamp, time_like_to_str from sqlmesh.utils.errors import SQLMeshError +if t.TYPE_CHECKING: + import pandas as pd + logger = logging.getLogger(__name__) @@ -330,6 +332,8 @@ def _create_expiration_filter_expr(self, current_ts: int) -> exp.Expression: def _environment_to_df(environment: Environment) -> pd.DataFrame: + import pandas as pd + return pd.DataFrame( [ { @@ -364,6 +368,8 @@ def _environment_to_df(environment: Environment) -> pd.DataFrame: def _environment_statements_to_df( environment_name: str, plan_id: str, environment_statements: t.List[EnvironmentStatements] ) -> pd.DataFrame: + import pandas as pd + return pd.DataFrame( [ { diff --git a/sqlmesh/core/state_sync/db/interval.py b/sqlmesh/core/state_sync/db/interval.py index 6700b7845e..a31873132c 100644 --- a/sqlmesh/core/state_sync/db/interval.py +++ b/sqlmesh/core/state_sync/db/interval.py @@ -2,7 +2,6 @@ import typing as t import logging -import pandas as pd from sqlglot import exp @@ -27,6 +26,9 @@ from sqlmesh.utils import random_id from sqlmesh.utils.date import now_timestamp +if t.TYPE_CHECKING: + import pandas as pd + logger = logging.getLogger(__name__) @@ -205,6 +207,8 @@ def _push_snapshot_intervals( snapshots: t.Iterable[t.Union[Snapshot, SnapshotIntervals]], is_compacted: bool = False, ) -> None: + import pandas as pd + new_intervals = [] for snapshot in snapshots: logger.info("Pushing intervals for snapshot %s", snapshot.snapshot_id) @@ -430,6 +434,8 @@ def _intervals_to_df( is_dev: bool, is_removed: bool, ) -> pd.DataFrame: + import pandas as pd + return pd.DataFrame( [ _interval_to_df( diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 4d9ee032d5..0cf954071d 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -1,7 +1,6 @@ from __future__ import annotations import typing as t -import pandas as pd import json import logging from pathlib import Path @@ -39,6 +38,8 @@ from sqlmesh.utils import unique if t.TYPE_CHECKING: + import pandas as pd + from sqlmesh.core.state_sync.db.interval import IntervalState @@ -674,6 +675,8 @@ def _snapshot_to_json(snapshot: Snapshot) -> str: def _snapshots_to_df(snapshots: t.Iterable[Snapshot]) -> pd.DataFrame: + import pandas as pd + return pd.DataFrame( [ { @@ -693,6 +696,8 @@ def _snapshots_to_df(snapshots: t.Iterable[Snapshot]) -> pd.DataFrame: def _auto_restatements_to_df(auto_restatements: t.Dict[SnapshotNameVersion, int]) -> pd.DataFrame: + import pandas as pd + return pd.DataFrame( [ { diff --git a/sqlmesh/core/state_sync/db/version.py b/sqlmesh/core/state_sync/db/version.py index 8ef6860b92..873e1633df 100644 --- a/sqlmesh/core/state_sync/db/version.py +++ b/sqlmesh/core/state_sync/db/version.py @@ -3,7 +3,6 @@ import logging import typing as t -import pandas as pd from sqlglot import __version__ as SQLGLOT_VERSION from sqlglot import exp from sqlglot.helper import seq_get @@ -40,6 +39,8 @@ def update_versions( sqlglot_version: str = SQLGLOT_VERSION, sqlmesh_version: str = SQLMESH_VERSION, ) -> None: + import pandas as pd + self.engine_adapter.delete_from(self.versions_table, "TRUE") self.engine_adapter.insert_append( diff --git a/sqlmesh/core/table_diff.py b/sqlmesh/core/table_diff.py index 8ea7f1e9cc..3550251a42 100644 --- a/sqlmesh/core/table_diff.py +++ b/sqlmesh/core/table_diff.py @@ -4,8 +4,6 @@ import typing as t from functools import cached_property -import pandas as pd - from sqlmesh.core.dialect import to_schema from sqlmesh.core.engine_adapter.mixins import RowDiffMixin from sqlmesh.core.engine_adapter.athena import AthenaEngineAdapter @@ -18,7 +16,10 @@ from sqlmesh.utils.pydantic import PydanticModel from sqlmesh.utils.errors import SQLMeshError + if t.TYPE_CHECKING: + import pandas as pd + from sqlmesh.core._typing import TableName from sqlmesh.core.engine_adapter import EngineAdapter @@ -75,6 +76,21 @@ class RowDiff(PydanticModel, frozen=True): model_name: t.Optional[str] = None decimals: int = 3 + _types_resolved: t.ClassVar[bool] = False + + def __new__(cls, *args: t.Any, **kwargs: t.Any) -> RowDiff: + if not cls._types_resolved: + cls._resolve_types() + return super().__new__(cls) + + @classmethod + def _resolve_types(cls) -> None: + # Pandas is imported by type checking so we need to resolve the types with the real import before instantiating + import pandas as pd # noqa + + cls.model_rebuild() + cls._types_resolved = True + @property def source_count(self) -> int: """Count of the source.""" diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index 70e3fbf7e2..b8081d0bb2 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -11,9 +11,7 @@ from unittest.mock import patch import numpy as np -import pandas as pd from io import StringIO -from pandas.api.types import is_object_dtype from sqlglot import Dialect, exp from sqlglot.optimizer.annotate_types import annotate_types from sqlglot.optimizer.normalize_identifiers import normalize_identifiers @@ -29,6 +27,8 @@ from sqlmesh.utils.yaml import load as yaml_load if t.TYPE_CHECKING: + import pandas as pd + from sqlglot.dialects.dialect import DialectType Row = t.Dict[str, t.Any] @@ -207,6 +207,9 @@ def assert_equal( partial: t.Optional[bool] = False, ) -> None: """Compare two DataFrames""" + import pandas as pd + from pandas.api.types import is_object_dtype + if partial: intersection = actual[actual.columns.intersection(expected.columns)] if len(intersection.columns) > 0: @@ -385,6 +388,8 @@ def _normalize_rows( partial: bool = False, dialect: DialectType = None, ) -> t.Dict: + import pandas as pd + if not isinstance(values, dict): values = {"rows": values} @@ -557,6 +562,8 @@ def _create_df( columns: t.Optional[t.Collection] = None, partial: t.Optional[bool] = False, ) -> pd.DataFrame: + import pandas as pd + query = values.get("query") if query: if not partial: @@ -725,6 +732,8 @@ def runTest(self) -> None: def _execute_model(self) -> pd.DataFrame: """Executes the python model and returns a DataFrame.""" + import pandas as pd + with self._concurrent_render_context(): variables = self.body.get("vars", {}).copy() time_kwargs = {key: variables.pop(key) for key in TIME_KWARG_KEYS if key in variables} @@ -893,6 +902,8 @@ def _raise_if_unexpected_columns( def _row_difference(left: pd.DataFrame, right: pd.DataFrame) -> pd.DataFrame: """Returns all rows in `left` that don't appear in `right`.""" + import pandas as pd + rows_missing_from_right = [] # `None` replaces `np.nan` because `np.nan != np.nan` and this would affect the mapping lookup diff --git a/sqlmesh/dbt/adapter.py b/sqlmesh/dbt/adapter.py index f0eb26418f..cfff977a96 100644 --- a/sqlmesh/dbt/adapter.py +++ b/sqlmesh/dbt/adapter.py @@ -4,7 +4,6 @@ import logging import typing as t -import pandas as pd from sqlglot import exp, parse_one from sqlmesh.core.dialect import normalize_and_quote, normalize_model_name, schema_ @@ -15,6 +14,7 @@ if t.TYPE_CHECKING: import agate + import pandas as pd from dbt.adapters.base import BaseRelation from dbt.adapters.base.column import Column from dbt.adapters.base.impl import AdapterResponse @@ -324,6 +324,7 @@ def drop_relation(self, relation: BaseRelation) -> None: def execute( self, sql: str, auto_begin: bool = False, fetch: bool = False ) -> t.Tuple[AdapterResponse, agate.Table]: + import pandas as pd from dbt.adapters.base.impl import AdapterResponse from sqlmesh.dbt.util import pandas_to_agate, empty_table diff --git a/sqlmesh/dbt/util.py b/sqlmesh/dbt/util.py index 53f4b2010f..9ffca39167 100644 --- a/sqlmesh/dbt/util.py +++ b/sqlmesh/dbt/util.py @@ -1,9 +1,13 @@ +from __future__ import annotations + import typing as t import agate -import pandas as pd from dbt.version import get_installed_version +if t.TYPE_CHECKING: + import pandas as pd + def _get_dbt_version() -> t.Tuple[int, int, int]: dbt_version = get_installed_version() diff --git a/sqlmesh/migrations/v0007_env_table_info_to_kind.py b/sqlmesh/migrations/v0007_env_table_info_to_kind.py index 1afffa1ca5..61335a0c51 100644 --- a/sqlmesh/migrations/v0007_env_table_info_to_kind.py +++ b/sqlmesh/migrations/v0007_env_table_info_to_kind.py @@ -3,7 +3,6 @@ import json import zlib -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type @@ -14,6 +13,8 @@ def _hash(data): # type: ignore def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema environments_table = "_environments" diff --git a/sqlmesh/migrations/v0009_remove_pre_post_hooks.py b/sqlmesh/migrations/v0009_remove_pre_post_hooks.py index 90b39bcf72..05d50c0932 100644 --- a/sqlmesh/migrations/v0009_remove_pre_post_hooks.py +++ b/sqlmesh/migrations/v0009_remove_pre_post_hooks.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0011_add_model_kind_name.py b/sqlmesh/migrations/v0011_add_model_kind_name.py index 3ae986a7cd..298d4b61ee 100644 --- a/sqlmesh/migrations/v0011_add_model_kind_name.py +++ b/sqlmesh/migrations/v0011_add_model_kind_name.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0012_update_jinja_expressions.py b/sqlmesh/migrations/v0012_update_jinja_expressions.py index aa7bcd375c..4f6f04fba5 100644 --- a/sqlmesh/migrations/v0012_update_jinja_expressions.py +++ b/sqlmesh/migrations/v0012_update_jinja_expressions.py @@ -3,7 +3,6 @@ import json import typing as t -import pandas as pd from sqlglot import exp from sqlmesh.utils.jinja import has_jinja @@ -11,6 +10,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0013_serde_using_model_dialects.py b/sqlmesh/migrations/v0013_serde_using_model_dialects.py index 284c8026dd..6f03767061 100644 --- a/sqlmesh/migrations/v0013_serde_using_model_dialects.py +++ b/sqlmesh/migrations/v0013_serde_using_model_dialects.py @@ -3,7 +3,6 @@ import json import typing as t -import pandas as pd from sqlglot import exp, parse_one from sqlmesh.utils.jinja import has_jinja @@ -11,6 +10,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0016_fix_windows_path.py b/sqlmesh/migrations/v0016_fix_windows_path.py index 46c85a0d5d..fb40d30076 100644 --- a/sqlmesh/migrations/v0016_fix_windows_path.py +++ b/sqlmesh/migrations/v0016_fix_windows_path.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0017_fix_windows_seed_path.py b/sqlmesh/migrations/v0017_fix_windows_seed_path.py index f780b216de..ca693bab72 100644 --- a/sqlmesh/migrations/v0017_fix_windows_seed_path.py +++ b/sqlmesh/migrations/v0017_fix_windows_seed_path.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py b/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py index 9b342962cc..de8f157ebb 100644 --- a/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py +++ b/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py b/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py index cf9d7a145f..c6beeb7d0a 100644 --- a/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py +++ b/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0021_fix_table_properties.py b/sqlmesh/migrations/v0021_fix_table_properties.py index 7889b59875..36bcbdcc82 100644 --- a/sqlmesh/migrations/v0021_fix_table_properties.py +++ b/sqlmesh/migrations/v0021_fix_table_properties.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.core import dialect as d @@ -10,6 +9,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0022_move_project_to_model.py b/sqlmesh/migrations/v0022_move_project_to_model.py index ec8cba5762..8da19049af 100644 --- a/sqlmesh/migrations/v0022_move_project_to_model.py +++ b/sqlmesh/migrations/v0022_move_project_to_model.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py b/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py index 1c55b93f7c..2855ecebb2 100644 --- a/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py +++ b/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py b/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py index 884b8c4067..7c794abdaa 100644 --- a/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py +++ b/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py @@ -3,7 +3,6 @@ import json import zlib -import pandas as pd from sqlglot import exp from sqlmesh.utils import random_id @@ -12,6 +11,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0026_remove_dialect_from_seed.py b/sqlmesh/migrations/v0026_remove_dialect_from_seed.py index 509c87947c..c06eeb4bca 100644 --- a/sqlmesh/migrations/v0026_remove_dialect_from_seed.py +++ b/sqlmesh/migrations/v0026_remove_dialect_from_seed.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0027_minute_interval_to_five.py b/sqlmesh/migrations/v0027_minute_interval_to_five.py index 10d58fbeb1..f92ffcb929 100644 --- a/sqlmesh/migrations/v0027_minute_interval_to_five.py +++ b/sqlmesh/migrations/v0027_minute_interval_to_five.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py b/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py index aab3ec7426..b7f58dc67f 100644 --- a/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py +++ b/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp, parse_one from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py b/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py index 95e6b36704..c2b6f545bc 100644 --- a/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py +++ b/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py @@ -4,13 +4,14 @@ import typing as t from collections import defaultdict -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync: t.Any, **kwargs: t.Any) -> None: # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0031_remove_dbt_target_fields.py b/sqlmesh/migrations/v0031_remove_dbt_target_fields.py index 7a1953707c..92137a4973 100644 --- a/sqlmesh/migrations/v0031_remove_dbt_target_fields.py +++ b/sqlmesh/migrations/v0031_remove_dbt_target_fields.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0034_add_default_catalog.py b/sqlmesh/migrations/v0034_add_default_catalog.py index d9e364022c..85a97b1134 100644 --- a/sqlmesh/migrations/v0034_add_default_catalog.py +++ b/sqlmesh/migrations/v0034_add_default_catalog.py @@ -5,7 +5,6 @@ import json import typing as t -import pandas as pd from sqlglot import exp from sqlglot.dialects.dialect import DialectType from sqlglot.helper import dict_depth, seq_get @@ -65,6 +64,8 @@ def update_dbt_relations( def migrate(state_sync, default_catalog: t.Optional[str], **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py b/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py index 5793ef9eaf..86fbc986ec 100644 --- a/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py +++ b/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type @@ -10,6 +9,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py b/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py index e67c4c61d7..9f27239f41 100644 --- a/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py +++ b/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.date import to_datetime, to_timestamp @@ -11,6 +10,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py b/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py index 8efbd4e1cb..10da4e18e5 100644 --- a/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py +++ b/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type @@ -10,6 +9,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema plan_dags_table = "_plan_dags" diff --git a/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py b/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py index f10ee66e16..ad47b63724 100644 --- a/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py +++ b/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type @@ -10,6 +9,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0042_trim_indirect_versions.py b/sqlmesh/migrations/v0042_trim_indirect_versions.py index 378ce92569..37b6bef570 100644 --- a/sqlmesh/migrations/v0042_trim_indirect_versions.py +++ b/sqlmesh/migrations/v0042_trim_indirect_versions.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type @@ -10,6 +9,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py b/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py index ec84f21525..4054f34f40 100644 --- a/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py +++ b/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type @@ -10,6 +9,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema plan_dags_table = "_plan_dags" diff --git a/sqlmesh/migrations/v0045_move_gateway_variable.py b/sqlmesh/migrations/v0045_move_gateway_variable.py index 2395d8d864..bd00e40404 100644 --- a/sqlmesh/migrations/v0045_move_gateway_variable.py +++ b/sqlmesh/migrations/v0045_move_gateway_variable.py @@ -3,7 +3,6 @@ import ast import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type @@ -11,6 +10,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0048_drop_indirect_versions.py b/sqlmesh/migrations/v0048_drop_indirect_versions.py index 4b646c5653..e5fe9a28ab 100644 --- a/sqlmesh/migrations/v0048_drop_indirect_versions.py +++ b/sqlmesh/migrations/v0048_drop_indirect_versions.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type @@ -10,6 +9,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0051_rename_column_descriptions.py b/sqlmesh/migrations/v0051_rename_column_descriptions.py index d3cb45c22b..627e58b4b9 100644 --- a/sqlmesh/migrations/v0051_rename_column_descriptions.py +++ b/sqlmesh/migrations/v0051_rename_column_descriptions.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type @@ -10,6 +9,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py b/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py index 8ef88b0a66..1c127b496b 100644 --- a/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py +++ b/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.date import to_datetime, to_timestamp @@ -11,6 +10,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0060_move_audits_to_model.py b/sqlmesh/migrations/v0060_move_audits_to_model.py index 5b2794b467..31da86999e 100644 --- a/sqlmesh/migrations/v0060_move_audits_to_model.py +++ b/sqlmesh/migrations/v0060_move_audits_to_model.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0063_change_signals.py b/sqlmesh/migrations/v0063_change_signals.py index c68cd497b8..48a5bd1998 100644 --- a/sqlmesh/migrations/v0063_change_signals.py +++ b/sqlmesh/migrations/v0063_change_signals.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp, parse_one from sqlmesh.utils.migration import index_text_type, blob_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0064_join_when_matched_strings.py b/sqlmesh/migrations/v0064_join_when_matched_strings.py index a83ffea344..6ca187be30 100644 --- a/sqlmesh/migrations/v0064_join_when_matched_strings.py +++ b/sqlmesh/migrations/v0064_join_when_matched_strings.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type, blob_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0069_update_dev_table_suffix.py b/sqlmesh/migrations/v0069_update_dev_table_suffix.py index 1eb61b5b84..57d0daaddd 100644 --- a/sqlmesh/migrations/v0069_update_dev_table_suffix.py +++ b/sqlmesh/migrations/v0069_update_dev_table_suffix.py @@ -2,13 +2,14 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type, blob_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py b/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py index 9cbb53b124..e1b7b32f37 100644 --- a/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py +++ b/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py @@ -2,7 +2,6 @@ import typing as t import json -import pandas as pd import zlib from sqlglot import exp @@ -62,6 +61,8 @@ def _migrate_intervals( used_snapshot_ids: t.Set[t.Tuple[str, str]], snapshot_ids_to_dev_versions: t.Dict[t.Tuple[str, str], str], ) -> None: + import pandas as pd + index_type = index_text_type(engine_adapter.dialect) intervals_columns_to_types = { "id": exp.DataType.build(index_type), @@ -148,6 +149,8 @@ def _migrate_snapshots( used_snapshot_ids: t.Set[t.Tuple[str, str]], snapshot_ids_to_dev_versions: t.Dict[t.Tuple[str, str], str], ) -> None: + import pandas as pd + index_type = index_text_type(engine_adapter.dialect) blob_type = blob_text_type(engine_adapter.dialect) snapshots_columns_to_types = { diff --git a/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py b/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py index 01519e0c0c..98d9582bdc 100644 --- a/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py +++ b/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py @@ -1,13 +1,14 @@ """Remove disable restatement from external and embedded models.""" import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type, blob_text_type def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0075_remove_validate_query.py b/sqlmesh/migrations/v0075_remove_validate_query.py index 3c637676d8..aa9c3fccb3 100644 --- a/sqlmesh/migrations/v0075_remove_validate_query.py +++ b/sqlmesh/migrations/v0075_remove_validate_query.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type @@ -10,6 +9,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0081_update_partitioned_by.py b/sqlmesh/migrations/v0081_update_partitioned_by.py index a88f8c3810..d6fd2dd669 100644 --- a/sqlmesh/migrations/v0081_update_partitioned_by.py +++ b/sqlmesh/migrations/v0081_update_partitioned_by.py @@ -2,7 +2,6 @@ import json -import pandas as pd from sqlglot import exp from sqlmesh.utils.migration import index_text_type @@ -10,6 +9,8 @@ def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/utils/date.py b/sqlmesh/utils/date.py index 3e23ab4e87..a241865fda 100644 --- a/sqlmesh/utils/date.py +++ b/sqlmesh/utils/date.py @@ -8,15 +8,15 @@ from datetime import date, datetime, timedelta, timezone, tzinfo import dateparser -import pandas as pd from dateparser import freshness_date_parser as freshness_date_parser_module from dateparser.freshness_date_parser import freshness_date_parser -from pandas.api.types import is_datetime64_any_dtype # type: ignore from sqlglot import exp from sqlmesh.utils import ttl_cache if t.TYPE_CHECKING: + import pandas as pd + from sqlglot.dialects.dialect import DialectType UTC = timezone.utc @@ -323,6 +323,8 @@ def make_inclusive( def make_inclusive_end(end: TimeLike, dialect: t.Optional[DialectType] = "") -> datetime: + import pandas as pd + exclusive_end = make_exclusive(end) if dialect == "tsql": return to_utc_timestamp(exclusive_end) - pd.Timedelta(1, unit="ns") @@ -337,6 +339,8 @@ def make_exclusive(time: TimeLike) -> datetime: def to_utc_timestamp(time: datetime) -> pd.Timestamp: + import pandas as pd + if time.tzinfo is not None: return pd.Timestamp(time).tz_convert("utc") return pd.Timestamp(time, tz="utc") @@ -420,6 +424,9 @@ def to_time_column( def pandas_timestamp_to_pydatetime( df: pd.DataFrame, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] ) -> pd.DataFrame: + import pandas as pd + from pandas.api.types import is_datetime64_any_dtype # type: ignore + for column in df.columns: if is_datetime64_any_dtype(df.dtypes[column]): # We must use `pd.Series` and dtype or pandas will convert it back to pd.Timestamp during assignment diff --git a/sqlmesh/utils/pandas.py b/sqlmesh/utils/pandas.py index d6bb96268d..43851e861a 100644 --- a/sqlmesh/utils/pandas.py +++ b/sqlmesh/utils/pandas.py @@ -1,42 +1,50 @@ from __future__ import annotations import typing as t +from functools import lru_cache -import numpy as np -import pandas as pd from sqlglot import exp +if t.TYPE_CHECKING: + import pandas as pd -PANDAS_TYPE_MAPPINGS = { - np.dtype("int8"): exp.DataType.build("tinyint"), - np.dtype("int16"): exp.DataType.build("smallint"), - np.dtype("int32"): exp.DataType.build("int"), - np.dtype("int64"): exp.DataType.build("bigint"), - np.dtype("float16"): exp.DataType.build("float"), - np.dtype("float32"): exp.DataType.build("float"), - np.dtype("float64"): exp.DataType.build("double"), - np.dtype("O"): exp.DataType.build("text"), - np.dtype("bool"): exp.DataType.build("boolean"), - np.dtype("datetime64"): exp.DataType.build("timestamp"), - np.dtype("datetime64[ns]"): exp.DataType.build("timestamp"), - np.dtype("datetime64[us]"): exp.DataType.build("timestamp"), - pd.Int8Dtype(): exp.DataType.build("tinyint"), - pd.Int16Dtype(): exp.DataType.build("smallint"), - pd.Int32Dtype(): exp.DataType.build("int"), - pd.Int64Dtype(): exp.DataType.build("bigint"), - pd.Float32Dtype(): exp.DataType.build("float"), - pd.Float64Dtype(): exp.DataType.build("double"), - pd.StringDtype(): exp.DataType.build("text"), # type: ignore - pd.BooleanDtype(): exp.DataType.build("boolean"), -} - -try: - import pyarrow # type: ignore # noqa - - # Only add this if pyarrow is installed - PANDAS_TYPE_MAPPINGS[pd.StringDtype("pyarrow")] = exp.DataType.build("text") -except ImportError: - pass + +@lru_cache() +def get_pandas_type_mappings() -> t.Dict[t.Any, exp.DataType]: + import pandas as pd + import numpy as np + + mappings = { + np.dtype("int8"): exp.DataType.build("tinyint"), + np.dtype("int16"): exp.DataType.build("smallint"), + np.dtype("int32"): exp.DataType.build("int"), + np.dtype("int64"): exp.DataType.build("bigint"), + np.dtype("float16"): exp.DataType.build("float"), + np.dtype("float32"): exp.DataType.build("float"), + np.dtype("float64"): exp.DataType.build("double"), + np.dtype("O"): exp.DataType.build("text"), + np.dtype("bool"): exp.DataType.build("boolean"), + np.dtype("datetime64"): exp.DataType.build("timestamp"), + np.dtype("datetime64[ns]"): exp.DataType.build("timestamp"), + np.dtype("datetime64[us]"): exp.DataType.build("timestamp"), + pd.Int8Dtype(): exp.DataType.build("tinyint"), + pd.Int16Dtype(): exp.DataType.build("smallint"), + pd.Int32Dtype(): exp.DataType.build("int"), + pd.Int64Dtype(): exp.DataType.build("bigint"), + pd.Float32Dtype(): exp.DataType.build("float"), + pd.Float64Dtype(): exp.DataType.build("double"), + pd.StringDtype(): exp.DataType.build("text"), # type: ignore + pd.BooleanDtype(): exp.DataType.build("boolean"), + } + try: + import pyarrow # type: ignore # noqa + + # Only add this if pyarrow is installed + mappings[pd.StringDtype("pyarrow")] = exp.DataType.build("text") + except ImportError: + pass + + return mappings def columns_to_types_from_df(df: pd.DataFrame) -> t.Dict[str, exp.DataType]: @@ -46,13 +54,15 @@ def columns_to_types_from_df(df: pd.DataFrame) -> t.Dict[str, exp.DataType]: def columns_to_types_from_dtypes( dtypes: t.Iterable[t.Tuple[t.Hashable, t.Any]], ) -> t.Dict[str, exp.DataType]: + import pandas as pd + result = {} for column_name, column_type in dtypes: exp_type: t.Optional[exp.DataType] = None if hasattr(pd, "DatetimeTZDtype") and isinstance(column_type, pd.DatetimeTZDtype): exp_type = exp.DataType.build("timestamptz") else: - exp_type = PANDAS_TYPE_MAPPINGS.get(column_type) + exp_type = get_pandas_type_mappings().get(column_type) if not exp_type: raise ValueError(f"Unsupported pandas type '{column_type}'") result[str(column_name)] = exp_type diff --git a/tests/conftest.py b/tests/conftest.py index b3e7af187b..e0066ec5a8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,7 @@ import shutil import duckdb -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from pytest_mock.plugin import MockerFixture from sqlglot import exp, maybe_parse, parse_one diff --git a/tests/core/engine_adapter/integration/__init__.py b/tests/core/engine_adapter/integration/__init__.py index a1a72af03b..3aa8529ef6 100644 --- a/tests/core/engine_adapter/integration/__init__.py +++ b/tests/core/engine_adapter/integration/__init__.py @@ -6,7 +6,7 @@ import typing as t import time -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from sqlglot import exp, parse_one diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 67aa8d6ad4..52c621e402 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -10,7 +10,7 @@ from datetime import datetime, timedelta import numpy as np -import pandas as pd +import pandas as pd # noqa: TID253 import pytest import pytz from sqlglot import exp, parse_one @@ -2534,7 +2534,7 @@ def test_python_model_column_order(ctx_df: TestContext, tmp_path: pathlib.Path): model_definitions = { # python model that emits a Pandas dataframe "pandas": """ -import pandas as pd +import pandas as pd # noqa: TID253 import typing as t from sqlmesh import ExecutionContext, model diff --git a/tests/core/engine_adapter/integration/test_integration_athena.py b/tests/core/engine_adapter/integration/test_integration_athena.py index 04cd080bee..33e76fc6e2 100644 --- a/tests/core/engine_adapter/integration/test_integration_athena.py +++ b/tests/core/engine_adapter/integration/test_integration_athena.py @@ -1,7 +1,7 @@ import typing as t import pytest from pytest import FixtureRequest -import pandas as pd +import pandas as pd # noqa: TID253 import datetime from sqlmesh.core.engine_adapter import AthenaEngineAdapter from sqlmesh.utils.aws import parse_s3_uri diff --git a/tests/core/engine_adapter/integration/test_integration_clickhouse.py b/tests/core/engine_adapter/integration/test_integration_clickhouse.py index f0a9f1a562..f09360c673 100644 --- a/tests/core/engine_adapter/integration/test_integration_clickhouse.py +++ b/tests/core/engine_adapter/integration/test_integration_clickhouse.py @@ -3,7 +3,7 @@ from pytest import FixtureRequest from tests.core.engine_adapter.integration import TestContext from sqlmesh.core.engine_adapter.clickhouse import ClickhouseEngineAdapter -import pandas as pd +import pandas as pd # noqa: TID253 from sqlglot import exp, parse_one from sqlmesh.core.snapshot import SnapshotChangeCategory diff --git a/tests/core/engine_adapter/test_athena.py b/tests/core/engine_adapter/test_athena.py index 2fd45ac3df..6a5f30998b 100644 --- a/tests/core/engine_adapter/test_athena.py +++ b/tests/core/engine_adapter/test_athena.py @@ -2,7 +2,7 @@ import pytest from unittest.mock import Mock from pytest_mock import MockerFixture -import pandas as pd +import pandas as pd # noqa: TID253 from sqlglot import exp, parse_one import sqlmesh.core.dialect as d diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index b242aa409b..8ab15ffdca 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -3,7 +3,7 @@ from datetime import datetime from unittest.mock import call -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from pytest_mock.plugin import MockerFixture from sqlglot import expressions as exp diff --git a/tests/core/engine_adapter/test_bigquery.py b/tests/core/engine_adapter/test_bigquery.py index 06e08d17b0..525b480650 100644 --- a/tests/core/engine_adapter/test_bigquery.py +++ b/tests/core/engine_adapter/test_bigquery.py @@ -1,7 +1,7 @@ # type: ignore import typing as t -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from google.cloud import bigquery from pytest_mock.plugin import MockerFixture diff --git a/tests/core/engine_adapter/test_databricks.py b/tests/core/engine_adapter/test_databricks.py index fa495ca247..25698875a5 100644 --- a/tests/core/engine_adapter/test_databricks.py +++ b/tests/core/engine_adapter/test_databricks.py @@ -1,7 +1,7 @@ # type: ignore import typing as t -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from pytest_mock import MockFixture from sqlglot import exp, parse_one diff --git a/tests/core/engine_adapter/test_duckdb.py b/tests/core/engine_adapter/test_duckdb.py index 42394e61cc..93ef72e874 100644 --- a/tests/core/engine_adapter/test_duckdb.py +++ b/tests/core/engine_adapter/test_duckdb.py @@ -1,6 +1,6 @@ import typing as t -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from sqlglot import expressions as exp from sqlglot import parse_one diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index a5e8aa8ecf..939d26a95d 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -3,7 +3,7 @@ from datetime import date from unittest import mock -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from pytest_mock.plugin import MockerFixture from sqlglot import expressions as exp diff --git a/tests/core/engine_adapter/test_redshift.py b/tests/core/engine_adapter/test_redshift.py index 9fdb589bb1..ef1e204ce5 100644 --- a/tests/core/engine_adapter/test_redshift.py +++ b/tests/core/engine_adapter/test_redshift.py @@ -1,7 +1,7 @@ # type: ignore import typing as t -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from pytest_mock.plugin import MockerFixture from unittest.mock import PropertyMock diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index 0f79f7aafd..c6101542a8 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -1,6 +1,6 @@ import typing as t -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from pytest_mock.plugin import MockerFixture from sqlglot import exp, parse_one diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index f3937f48c1..10ab17a0d3 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -5,7 +5,7 @@ from unittest.mock import call, patch import duckdb -import pandas as pd +import pandas as pd # noqa: TID253 import pytest import time_machine from pytest_mock.plugin import MockerFixture diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 3b9b0b4f61..11b37f8a9a 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -8,7 +8,7 @@ import time_machine import pytest -import pandas as pd +import pandas as pd # noqa: TID253 from pathlib import Path from pytest_mock.plugin import MockerFixture from sqlglot import ParseError, exp, parse_one, Dialect diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 5efbd569c4..f2998080b3 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -9,7 +9,7 @@ import os import numpy as np -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from pathlib import Path from sqlmesh.core.config.naming import NameInferenceConfig @@ -5399,7 +5399,7 @@ def test_python_model_default_kind_change(init_and_plan_context: t.Callable): # note: we deliberately dont specify a Kind here to allow the defaults to be picked up python_model_file = """import typing as t -import pandas as pd +import pandas as pd # noqa: TID253 from sqlmesh import ExecutionContext, model @model( diff --git a/tests/core/test_loader.py b/tests/core/test_loader.py index b194139d60..2c648e7718 100644 --- a/tests/core/test_loader.py +++ b/tests/core/test_loader.py @@ -22,7 +22,7 @@ def sample_models(request): }, "python": { "contents": """import typing as t -import pandas as pd +import pandas as pd # noqa: TID253 from sqlmesh import ExecutionContext, model @model( @@ -134,7 +134,7 @@ def my_model(context, **kwargs): pass""" model_payload_b = f"""import typing as t -import pandas as pd +import pandas as pd # noqa: TID253 from sqlmesh import ExecutionContext, model @model( diff --git a/tests/core/test_model.py b/tests/core/test_model.py index b04aabf1dc..efda5bc454 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -6,7 +6,7 @@ from unittest.mock import patch, PropertyMock import time_machine -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from pytest_mock.plugin import MockerFixture from sqlglot import exp, parse_one @@ -8679,7 +8679,7 @@ def identity(evaluator, value): blueprint_pydf.parent.mkdir(parents=True, exist_ok=True) blueprint_pydf.write_text( """ -import pandas as pd +import pandas as pd # noqa: TID253 from sqlmesh import model @@ -9105,7 +9105,7 @@ def test_blueprint_variable_precedence_python(tmp_path: Path, mocker: MockerFixt blueprint_variables.parent.mkdir(parents=True, exist_ok=True) blueprint_variables.write_text( """ -import pandas as pd +import pandas as pd # noqa: TID253 from sqlglot import exp from sqlmesh import model @@ -9167,7 +9167,7 @@ def test_python_model_depends_on_blueprints(tmp_path: Path) -> None: py_model.parent.mkdir(parents=True, exist_ok=True) py_model.write_text( """ -import pandas as pd +import pandas as pd # noqa: TID253 from sqlmesh import model @model( diff --git a/tests/core/test_schema_loader.py b/tests/core/test_schema_loader.py index a749335538..8b944793be 100644 --- a/tests/core/test_schema_loader.py +++ b/tests/core/test_schema_loader.py @@ -3,7 +3,7 @@ from pathlib import Path from unittest.mock import patch -import pandas as pd +import pandas as pd # noqa: TID253 from pytest_mock.plugin import MockerFixture from sqlglot import exp, parse_one diff --git a/tests/core/test_seed.py b/tests/core/test_seed.py index e3076f413c..a22805cbd2 100644 --- a/tests/core/test_seed.py +++ b/tests/core/test_seed.py @@ -1,4 +1,4 @@ -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from sqlglot import exp diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index dbe8d9c3de..d572fc7e11 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -5,7 +5,7 @@ import re import logging import pytest -import pandas as pd +import pandas as pd # noqa: TID253 import json from pydantic import model_validator from pathlib import Path @@ -1427,7 +1427,7 @@ def test_snapshot_evaluator_yield_pd(adapter_mock, make_snapshot, input_dfs, out name="python_func", alias="python_func", path="test_snapshot_evaluator.py", - payload=f"""import pandas as pd + payload=f"""import pandas as pd # noqa: TID253 def python_func(**kwargs): for df in [ {input_dfs} diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index 4d7be022df..76725d46eb 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -1,6 +1,6 @@ import pytest from pytest_mock.plugin import MockerFixture -import pandas as pd +import pandas as pd # noqa: TID253 from sqlglot import exp from sqlmesh.core import dialect as d import re diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 4715cf5989..a05e66e48f 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -8,7 +8,7 @@ from unittest.mock import call, patch from shutil import copyfile -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from pytest_mock.plugin import MockerFixture from sqlglot import exp diff --git a/tests/dbt/test_integration.py b/tests/dbt/test_integration.py index d4e5247b52..9cee4796fb 100644 --- a/tests/dbt/test_integration.py +++ b/tests/dbt/test_integration.py @@ -5,7 +5,7 @@ from functools import partial from pathlib import Path -import pandas as pd +import pandas as pd # noqa: TID253 import pytest from dbt.cli.main import dbtRunner import time_machine diff --git a/tests/dbt/test_util.py b/tests/dbt/test_util.py index 67c8ba8e89..ce98f48a82 100644 --- a/tests/dbt/test_util.py +++ b/tests/dbt/test_util.py @@ -1,6 +1,6 @@ from __future__ import annotations -import pandas as pd +import pandas as pd # noqa: TID253 from sqlmesh.dbt.util import pandas_to_agate diff --git a/tests/fixtures/migrations/snapshots.json b/tests/fixtures/migrations/snapshots.json index 3794db0f06..45cebe613b 100644 --- a/tests/fixtures/migrations/snapshots.json +++ b/tests/fixtures/migrations/snapshots.json @@ -1 +1 @@ -{"name":{"0":"sushi.waiter_as_customer_by_day","1":"sushi.waiter_revenue_by_day","2":"sushi.top_waiters","3":"sushi.waiters","4":"sushi.customers","5":"sushi.waiter_names","6":"sushi.customer_revenue_by_day","7":"sushi.items","8":"sushi.order_items","9":"sushi.orders","10":"sushi.waiter_as_customer_by_day","11":"sushi.waiter_names"},"identifier":{"0":"1281222509","1":"1609279380","2":"599861134","3":"3386889721","4":"3148897116","5":"3233103305","6":"1308408370","7":"2957171338","8":"1806777563","9":"3564161223","10":"1084858582","11":"1604207722"},"version":{"0":"1267397572","1":"2695875565","2":"3010914162","3":"2059227798","4":"2359719298","5":"2505706914","6":"1291364031","7":"312608270","8":"1015284155","9":"925846788","10":"3668757715","11":"1204702829"},"snapshot":{"0":"{\"name\": \"sushi.waiter_as_customer_by_day\", \"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_as_customer_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT w.ds AS ds, w.waiter_id AS waiter_id, wn.name AS waiter_name FROM sushi.waiters AS w JOIN sushi.customers AS c ON w.waiter_id = c.customer_id JOIN sushi.waiter_names AS wn ON w.waiter_id = wn.id\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.waiters\", \"identifier\": \"3386889721\"}, {\"name\": \"sushi.waiter_names\", \"identifier\": \"3233103305\"}, {\"name\": \"sushi.customers\", \"identifier\": \"3148897116\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376348, \"updated_ts\": 1680814376348, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"1267397572\"}","1":"{\"name\": \"sushi.waiter_revenue_by_day\", \"fingerprint\": {\"data_hash\": \"2443934302\", \"metadata_hash\": \"2904050331\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_revenue_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"Table of revenue generated by waiters by day.\", \"batch_size\": 10, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"number_of_rows\", {\"threshold\": \"0\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT CAST(o.waiter_id AS INT) AS waiter_id \/* Waiter id *\/, CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue \/* Revenue from orders taken by this waiter *\/, CAST(o.ds AS TEXT) AS ds \/* Date *\/ FROM sushi.orders AS o LEFT JOIN sushi.order_items AS oi ON o.id = oi.order_id AND o.ds = oi.ds LEFT JOIN sushi.items AS i ON oi.item_id = i.id AND oi.ds = i.ds WHERE o.ds BETWEEN @start_ds AND @end_ds GROUP BY o.waiter_id, o.ds\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.order_items\", \"identifier\": \"1806777563\"}, {\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376361, \"updated_ts\": 1680814376361, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2695875565\"}","2":"{\"name\": \"sushi.top_waiters\", \"fingerprint\": {\"data_hash\": \"2891807529\", \"metadata_hash\": \"3392493998\", \"parent_data_hash\": \"1940707936\", \"parent_metadata_hash\": \"1276363398\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.top_waiters\", \"kind\": {\"name\": \"VIEW\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"View of top waiters.\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"unique_values\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT CAST(waiter_id AS INT) AS waiter_id, CAST(revenue AS DOUBLE) AS revenue FROM sushi.waiter_revenue_by_day WHERE ds = (SELECT MAX(ds) FROM sushi.waiter_revenue_by_day) ORDER BY revenue DESC LIMIT 10\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.waiter_revenue_by_day\", \"identifier\": \"1609279380\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376384, \"updated_ts\": 1680814376384, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"3010914162\"}","3":"{\"name\": \"sushi.waiters\", \"fingerprint\": {\"data_hash\": \"3501061139\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiters\", \"kind\": {\"name\": \"EMBEDDED\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"python_env\": {\"incremental_by_ds\": {\"payload\": \"def incremental_by_ds(evaluator, column):\\n expression = evaluator.transform(exp.Between(this=column, low=MacroVar(\\n this='start_ds'), high=MacroVar(this='end_ds')))\\n if not isinstance(expression, exp.Expression):\\n raise MacroEvalError(\\n f'Return type is {type(expression)}, expected exp.Expression')\\n return expression\", \"kind\": \"definition\", \"name\": \"incremental_by_ds\", \"path\": \"macros\/macros.py\"}, \"exp\": {\"payload\": \"import sqlglot.expressions as exp\", \"kind\": \"import\"}, \"MacroVar\": {\"payload\": \"from sqlmesh.core.dialect import MacroVar\", \"kind\": \"import\"}, \"MacroEvalError\": {\"payload\": \"from sqlmesh.utils.errors import MacroEvalError\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT DISTINCT CAST(waiter_id AS INT) AS waiter_id, CAST(ds AS TEXT) AS ds FROM sushi.orders AS o WHERE @incremental_by_ds(ds)\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [], \"dev_intervals\": [], \"created_ts\": 1680814376387, \"updated_ts\": 1680814376387, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2059227798\"}","4":"{\"name\": \"sushi.customers\", \"fingerprint\": {\"data_hash\": \"3553985282\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.customers\", \"kind\": {\"name\": \"FULL\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [[\"noop\", {\"x\": \"1\"}]], \"post\": [[\"noop\", {}], [\"noop\", {\"y\": \"ARRAY('a', 2)\"}]], \"audits\": [], \"expressions\": [], \"python_env\": {\"noop\": {\"payload\": \"def noop(context, start, end, latest, **kwargs):\\n pass\", \"kind\": \"definition\", \"name\": \"noop\", \"path\": \"hooks\/hooks.py\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT DISTINCT CAST(customer_id AS INT) AS customer_id FROM sushi.orders AS o\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376388, \"updated_ts\": 1680814376388, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2359719298\"}","5":"{\"name\": \"sushi.waiter_names\", \"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_names\", \"kind\": {\"name\": \"SEED\", \"path\": \"..\/seeds\/waiter_names.csv\", \"batch_size\": 5}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"seed\": {\"content\": \"id,name\\n0,Toby\\n1,Tyson\\n2,Ryan\\n3,George\\n4,Chris\\n5,Max\\n6,Vincent\\n7,Iaroslav\\n8,Emma\\n9,Maia\\n\"}, \"source_type\": \"seed\"}, \"parents\": [], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376389, \"updated_ts\": 1680814376389, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2505706914\"}","6":"{\"name\": \"sushi.customer_revenue_by_day\", \"fingerprint\": {\"data_hash\": \"2657552867\", \"metadata_hash\": \"129771006\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.customer_revenue_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"hive\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"Table of revenue from customers by day.\", \"batch_size\": 10, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"WITH order_total AS (SELECT oi.order_id AS order_id, SUM(oi.quantity * i.price) AS total, oi.ds AS ds FROM sushi.order_items AS oi LEFT JOIN sushi.items AS i ON oi.item_id = i.id AND oi.ds = i.ds WHERE oi.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' GROUP BY oi.order_id, oi.ds) SELECT CAST(o.customer_id AS INT) AS customer_id \/* Customer id *\/, CAST(SUM(ot.total) AS DOUBLE) AS revenue \/* Revenue from orders made by this customer *\/, CAST(o.ds AS TEXT) AS ds \/* Date *\/ FROM sushi.orders AS o LEFT JOIN order_total AS ot ON o.id = ot.order_id AND o.ds = ot.ds WHERE o.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' GROUP BY o.customer_id, o.ds\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.order_items\", \"identifier\": \"1806777563\"}, {\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376391, \"updated_ts\": 1680814376391, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"1291364031\"}","7":"{\"name\": \"sushi.items\", \"fingerprint\": {\"data_hash\": \"1960378930\", \"metadata_hash\": \"2900807542\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.items\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"\", \"cron\": \"@daily\", \"start\": \"Jan 1 2022\", \"batch_size\": 30, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"depends_on\": [], \"columns\": {\"id\": \"INT\", \"name\": \"TEXT\", \"price\": \"DOUBLE\", \"ds\": \"TEXT\"}, \"audits\": [[\"accepted_values\", {\"column\": \"name\", \"values\": \"ARRAY('Ahi', 'Aji', 'Amaebi', 'Anago', 'Aoyagi', 'Bincho', 'Katsuo', 'Ebi', 'Escolar', 'Hamachi', 'Hamachi Toro', 'Hirame', 'Hokigai', 'Hotate', 'Ika', 'Ikura', 'Iwashi', 'Kani', 'Kanpachi', 'Maguro', 'Saba', 'Sake', 'Sake Toro', 'Tai', 'Tako', 'Tamago', 'Tobiko', 'Toro', 'Tsubugai', 'Umi Masu', 'Unagi', 'Uni')\"}], [\"not_null\", {\"columns\": \"ARRAY(name, price)\"}], [\"assert_items_price_exceeds_threshold\", {\"price\": \"0\"}]], \"expressions\": [], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n dfs = []\\n for dt in iter_dates(start, end):\\n num_items = random.randint(10, len(ITEMS))\\n dfs.append(pd.DataFrame({'name': random.sample(ITEMS, num_items),\\n 'price': np.random.uniform(3.0, 10.0, size=num_items).round(2),\\n 'ds': to_ds(dt)}).reset_index().rename(columns={'index': 'id'}))\\n return pd.concat(dfs)\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/items.py\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"ITEMS\": {\"payload\": \"['Ahi', 'Aji', 'Amaebi', 'Anago', 'Aoyagi', 'Bincho', 'Katsuo', 'Ebi', 'Escolar', 'Hamachi', 'Hamachi Toro', 'Hirame', 'Hokigai', 'Hotate', 'Ika', 'Ikura', 'Iwashi', 'Kani', 'Kanpachi', 'Maguro', 'Saba', 'Sake', 'Sake Toro', 'Tai', 'Tako', 'Tamago', 'Tobiko', 'Toro', 'Tsubugai', 'Umi Masu', 'Unagi', 'Uni']\", \"kind\": \"value\"}, \"pd\": {\"payload\": \"import pandas as pd\", \"kind\": \"import\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\"}, \"parents\": [], \"audits\": [{\"name\": \"assert_items_price_exceeds_threshold\", \"dialect\": \"\", \"skip\": false, \"blocking\": true, \"query\": \"SELECT * FROM @this_model WHERE price <= @price\", \"expressions\": []}], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376399, \"updated_ts\": 1680814376399, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"312608270\"}","8":"{\"name\": \"sushi.order_items\", \"fingerprint\": {\"data_hash\": \"653664599\", \"metadata_hash\": \"1960934702\", \"parent_data_hash\": \"3170724558\", \"parent_metadata_hash\": \"867324801\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.order_items\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"\", \"cron\": \"@daily\", \"batch_size\": 30, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"depends_on\": [\"sushi.items\", \"sushi.orders\"], \"columns\": {\"id\": \"INT\", \"order_id\": \"INT\", \"item_id\": \"INT\", \"quantity\": \"INT\", \"ds\": \"TEXT\"}, \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(id, order_id, item_id, quantity)\"}], [\"assert_order_items_quantity_exceeds_threshold\", {\"quantity\": \"0\"}]], \"expressions\": [], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n orders_table = context.table('sushi.orders')\\n items_table = context.table(ITEMS)\\n for dt in iter_dates(start, end):\\n orders = context.fetchdf(\\n f\\\"\\\"\\\"\\n SELECT *\\n FROM {orders_table}\\n WHERE ds = '{to_ds(dt)}'\\n \\\"\\\"\\\"\\n )\\n items = context.fetchdf(\\n f\\\"\\\"\\\"\\n SELECT *\\n FROM {items_table}\\n WHERE ds = '{to_ds(dt)}'\\n \\\"\\\"\\\"\\n )\\n for order_id in orders['id']:\\n n = random.randint(1, 5)\\n yield pd.DataFrame({'order_id': order_id, 'item_id': items.\\n sample(n=n)['id'], 'quantity': np.random.randint(1, 10, n),\\n 'ds': to_ds(dt)}).reset_index().rename(columns={'index': 'id'})\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/order_items.py\"}, \"ITEMS\": {\"payload\": \"'sushi.items'\", \"kind\": \"value\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}, \"pd\": {\"payload\": \"import pandas as pd\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\"}, \"parents\": [{\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [{\"name\": \"assert_order_items_quantity_exceeds_threshold\", \"dialect\": \"\", \"skip\": false, \"blocking\": true, \"query\": \"SELECT * FROM @this_model WHERE quantity <= @quantity\", \"expressions\": []}], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376401, \"updated_ts\": 1680814376401, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"1015284155\"}","9":"{\"name\": \"sushi.orders\", \"fingerprint\": {\"data_hash\": \"1628439771\", \"metadata_hash\": \"2745052130\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.orders\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"\", \"cron\": \"@daily\", \"description\": \"Table of sushi orders.\", \"start\": \"2022-01-01\", \"batch_size\": 30, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"depends_on\": [], \"columns\": {\"id\": \"INT\", \"customer_id\": \"INT\", \"waiter_id\": \"INT\", \"start_ts\": \"INT\", \"end_ts\": \"INT\", \"ds\": \"TEXT\"}, \"audits\": [], \"expressions\": [], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n dfs = []\\n for dt in iter_dates(start, end):\\n num_orders = random.randint(10, 30)\\n start_ts = [int((dt + timedelta(seconds=random.randint(0, 80000))).\\n timestamp()) for _ in range(num_orders)]\\n end_ts = [int(s + random.randint(0, 60 * 60)) for s in start_ts]\\n dfs.append(pd.DataFrame({'customer_id': random.choices(CUSTOMERS, k\\n =num_orders), 'waiter_id': random.choices(WAITERS, k=num_orders\\n ), 'start_ts': start_ts, 'end_ts': end_ts, 'ds': to_ds(dt)}).\\n reset_index().rename(columns={'index': 'id'}))\\n return pd.concat(dfs)\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/orders.py\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"pd\": {\"payload\": \"import pandas as pd\", \"kind\": \"import\"}, \"CUSTOMERS\": {\"payload\": \"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]\", \"kind\": \"value\"}, \"WAITERS\": {\"payload\": \"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\", \"kind\": \"value\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\"}, \"parents\": [], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376402, \"updated_ts\": 1680814376402, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"925846788\"}","10":"{\"name\": \"sushi.waiter_as_customer_by_day\", \"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2824767713\", \"parent_metadata_hash\": \"1349779748\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_as_customer_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"table_properties\": {\"key\": \"'value'\"}, \"query\": \"SELECT w.ds AS ds, w.waiter_id AS waiter_id, wn.name AS waiter_name FROM sushi.waiters AS w JOIN sushi.customers AS c ON w.waiter_id = c.customer_id JOIN sushi.waiter_names AS wn ON w.waiter_id = wn.id\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.waiters\", \"identifier\": \"3386889721\"}, {\"name\": \"sushi.waiter_names\", \"identifier\": \"1604207722\"}, {\"name\": \"sushi.customers\", \"identifier\": \"3148897116\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814464891, \"updated_ts\": 1680814464891, \"ttl\": \"in 1 week\", \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"1267397572\"}], \"indirect_versions\": {}, \"version\": \"3668757715\"}","11":"{\"name\": \"sushi.waiter_names\", \"fingerprint\": {\"data_hash\": \"4133862560\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_names\", \"kind\": {\"name\": \"SEED\", \"path\": \"..\/seeds\/waiter_names.csv\", \"batch_size\": 5}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"seed\": {\"content\": \"id,name\\n0,Toby\\n1,Tyson\\n2,Ryan\\n3,George\\n4,Chris\\n5,Max\\n6,Vincent\\n7,Iaroslav\\n8,Emma\\n9,Maia\\n10,Jim\\n\"}, \"source_type\": \"seed\"}, \"parents\": [], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814464932, \"updated_ts\": 1680814464932, \"ttl\": \"in 1 week\", \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"2505706914\"}], \"indirect_versions\": {\"sushi.waiter_as_customer_by_day\": [{\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"1267397572\"}, {\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2824767713\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"3668757715\"}]}, \"version\": \"1204702829\", \"change_category\": 1}"}} \ No newline at end of file +{"name":{"0":"sushi.waiter_as_customer_by_day","1":"sushi.waiter_revenue_by_day","2":"sushi.top_waiters","3":"sushi.waiters","4":"sushi.customers","5":"sushi.waiter_names","6":"sushi.customer_revenue_by_day","7":"sushi.items","8":"sushi.order_items","9":"sushi.orders","10":"sushi.waiter_as_customer_by_day","11":"sushi.waiter_names"},"identifier":{"0":"1281222509","1":"1609279380","2":"599861134","3":"3386889721","4":"3148897116","5":"3233103305","6":"1308408370","7":"2957171338","8":"1806777563","9":"3564161223","10":"1084858582","11":"1604207722"},"version":{"0":"1267397572","1":"2695875565","2":"3010914162","3":"2059227798","4":"2359719298","5":"2505706914","6":"1291364031","7":"312608270","8":"1015284155","9":"925846788","10":"3668757715","11":"1204702829"},"snapshot":{"0":"{\"name\": \"sushi.waiter_as_customer_by_day\", \"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_as_customer_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT w.ds AS ds, w.waiter_id AS waiter_id, wn.name AS waiter_name FROM sushi.waiters AS w JOIN sushi.customers AS c ON w.waiter_id = c.customer_id JOIN sushi.waiter_names AS wn ON w.waiter_id = wn.id\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.waiters\", \"identifier\": \"3386889721\"}, {\"name\": \"sushi.waiter_names\", \"identifier\": \"3233103305\"}, {\"name\": \"sushi.customers\", \"identifier\": \"3148897116\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376348, \"updated_ts\": 1680814376348, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"1267397572\"}","1":"{\"name\": \"sushi.waiter_revenue_by_day\", \"fingerprint\": {\"data_hash\": \"2443934302\", \"metadata_hash\": \"2904050331\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_revenue_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"Table of revenue generated by waiters by day.\", \"batch_size\": 10, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"number_of_rows\", {\"threshold\": \"0\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT CAST(o.waiter_id AS INT) AS waiter_id \/* Waiter id *\/, CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue \/* Revenue from orders taken by this waiter *\/, CAST(o.ds AS TEXT) AS ds \/* Date *\/ FROM sushi.orders AS o LEFT JOIN sushi.order_items AS oi ON o.id = oi.order_id AND o.ds = oi.ds LEFT JOIN sushi.items AS i ON oi.item_id = i.id AND oi.ds = i.ds WHERE o.ds BETWEEN @start_ds AND @end_ds GROUP BY o.waiter_id, o.ds\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.order_items\", \"identifier\": \"1806777563\"}, {\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376361, \"updated_ts\": 1680814376361, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2695875565\"}","2":"{\"name\": \"sushi.top_waiters\", \"fingerprint\": {\"data_hash\": \"2891807529\", \"metadata_hash\": \"3392493998\", \"parent_data_hash\": \"1940707936\", \"parent_metadata_hash\": \"1276363398\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.top_waiters\", \"kind\": {\"name\": \"VIEW\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"View of top waiters.\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"unique_values\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT CAST(waiter_id AS INT) AS waiter_id, CAST(revenue AS DOUBLE) AS revenue FROM sushi.waiter_revenue_by_day WHERE ds = (SELECT MAX(ds) FROM sushi.waiter_revenue_by_day) ORDER BY revenue DESC LIMIT 10\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.waiter_revenue_by_day\", \"identifier\": \"1609279380\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376384, \"updated_ts\": 1680814376384, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"3010914162\"}","3":"{\"name\": \"sushi.waiters\", \"fingerprint\": {\"data_hash\": \"3501061139\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiters\", \"kind\": {\"name\": \"EMBEDDED\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"python_env\": {\"incremental_by_ds\": {\"payload\": \"def incremental_by_ds(evaluator, column):\\n expression = evaluator.transform(exp.Between(this=column, low=MacroVar(\\n this='start_ds'), high=MacroVar(this='end_ds')))\\n if not isinstance(expression, exp.Expression):\\n raise MacroEvalError(\\n f'Return type is {type(expression)}, expected exp.Expression')\\n return expression\", \"kind\": \"definition\", \"name\": \"incremental_by_ds\", \"path\": \"macros\/macros.py\"}, \"exp\": {\"payload\": \"import sqlglot.expressions as exp\", \"kind\": \"import\"}, \"MacroVar\": {\"payload\": \"from sqlmesh.core.dialect import MacroVar\", \"kind\": \"import\"}, \"MacroEvalError\": {\"payload\": \"from sqlmesh.utils.errors import MacroEvalError\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT DISTINCT CAST(waiter_id AS INT) AS waiter_id, CAST(ds AS TEXT) AS ds FROM sushi.orders AS o WHERE @incremental_by_ds(ds)\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [], \"dev_intervals\": [], \"created_ts\": 1680814376387, \"updated_ts\": 1680814376387, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2059227798\"}","4":"{\"name\": \"sushi.customers\", \"fingerprint\": {\"data_hash\": \"3553985282\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.customers\", \"kind\": {\"name\": \"FULL\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [[\"noop\", {\"x\": \"1\"}]], \"post\": [[\"noop\", {}], [\"noop\", {\"y\": \"ARRAY('a', 2)\"}]], \"audits\": [], \"expressions\": [], \"python_env\": {\"noop\": {\"payload\": \"def noop(context, start, end, latest, **kwargs):\\n pass\", \"kind\": \"definition\", \"name\": \"noop\", \"path\": \"hooks\/hooks.py\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT DISTINCT CAST(customer_id AS INT) AS customer_id FROM sushi.orders AS o\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376388, \"updated_ts\": 1680814376388, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2359719298\"}","5":"{\"name\": \"sushi.waiter_names\", \"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_names\", \"kind\": {\"name\": \"SEED\", \"path\": \"..\/seeds\/waiter_names.csv\", \"batch_size\": 5}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"seed\": {\"content\": \"id,name\\n0,Toby\\n1,Tyson\\n2,Ryan\\n3,George\\n4,Chris\\n5,Max\\n6,Vincent\\n7,Iaroslav\\n8,Emma\\n9,Maia\\n\"}, \"source_type\": \"seed\"}, \"parents\": [], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376389, \"updated_ts\": 1680814376389, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2505706914\"}","6":"{\"name\": \"sushi.customer_revenue_by_day\", \"fingerprint\": {\"data_hash\": \"2657552867\", \"metadata_hash\": \"129771006\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.customer_revenue_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"hive\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"Table of revenue from customers by day.\", \"batch_size\": 10, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"WITH order_total AS (SELECT oi.order_id AS order_id, SUM(oi.quantity * i.price) AS total, oi.ds AS ds FROM sushi.order_items AS oi LEFT JOIN sushi.items AS i ON oi.item_id = i.id AND oi.ds = i.ds WHERE oi.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' GROUP BY oi.order_id, oi.ds) SELECT CAST(o.customer_id AS INT) AS customer_id \/* Customer id *\/, CAST(SUM(ot.total) AS DOUBLE) AS revenue \/* Revenue from orders made by this customer *\/, CAST(o.ds AS TEXT) AS ds \/* Date *\/ FROM sushi.orders AS o LEFT JOIN order_total AS ot ON o.id = ot.order_id AND o.ds = ot.ds WHERE o.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' GROUP BY o.customer_id, o.ds\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.order_items\", \"identifier\": \"1806777563\"}, {\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376391, \"updated_ts\": 1680814376391, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"1291364031\"}","7":"{\"name\": \"sushi.items\", \"fingerprint\": {\"data_hash\": \"1960378930\", \"metadata_hash\": \"2900807542\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.items\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"\", \"cron\": \"@daily\", \"start\": \"Jan 1 2022\", \"batch_size\": 30, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"depends_on\": [], \"columns\": {\"id\": \"INT\", \"name\": \"TEXT\", \"price\": \"DOUBLE\", \"ds\": \"TEXT\"}, \"audits\": [[\"accepted_values\", {\"column\": \"name\", \"values\": \"ARRAY('Ahi', 'Aji', 'Amaebi', 'Anago', 'Aoyagi', 'Bincho', 'Katsuo', 'Ebi', 'Escolar', 'Hamachi', 'Hamachi Toro', 'Hirame', 'Hokigai', 'Hotate', 'Ika', 'Ikura', 'Iwashi', 'Kani', 'Kanpachi', 'Maguro', 'Saba', 'Sake', 'Sake Toro', 'Tai', 'Tako', 'Tamago', 'Tobiko', 'Toro', 'Tsubugai', 'Umi Masu', 'Unagi', 'Uni')\"}], [\"not_null\", {\"columns\": \"ARRAY(name, price)\"}], [\"assert_items_price_exceeds_threshold\", {\"price\": \"0\"}]], \"expressions\": [], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n dfs = []\\n for dt in iter_dates(start, end):\\n num_items = random.randint(10, len(ITEMS))\\n dfs.append(pd.DataFrame({'name': random.sample(ITEMS, num_items),\\n 'price': np.random.uniform(3.0, 10.0, size=num_items).round(2),\\n 'ds': to_ds(dt)}).reset_index().rename(columns={'index': 'id'}))\\n return pd.concat(dfs)\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/items.py\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"ITEMS\": {\"payload\": \"['Ahi', 'Aji', 'Amaebi', 'Anago', 'Aoyagi', 'Bincho', 'Katsuo', 'Ebi', 'Escolar', 'Hamachi', 'Hamachi Toro', 'Hirame', 'Hokigai', 'Hotate', 'Ika', 'Ikura', 'Iwashi', 'Kani', 'Kanpachi', 'Maguro', 'Saba', 'Sake', 'Sake Toro', 'Tai', 'Tako', 'Tamago', 'Tobiko', 'Toro', 'Tsubugai', 'Umi Masu', 'Unagi', 'Uni']\", \"kind\": \"value\"}, \"pd\": {\"payload\": \"import pandas as pd\", \"kind\": \"import\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\"}, \"parents\": [], \"audits\": [{\"name\": \"assert_items_price_exceeds_threshold\", \"dialect\": \"\", \"skip\": false, \"blocking\": true, \"query\": \"SELECT * FROM @this_model WHERE price <= @price\", \"expressions\": []}], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376399, \"updated_ts\": 1680814376399, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"312608270\"}","8":"{\"name\": \"sushi.order_items\", \"fingerprint\": {\"data_hash\": \"653664599\", \"metadata_hash\": \"1960934702\", \"parent_data_hash\": \"3170724558\", \"parent_metadata_hash\": \"867324801\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.order_items\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"\", \"cron\": \"@daily\", \"batch_size\": 30, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"depends_on\": [\"sushi.items\", \"sushi.orders\"], \"columns\": {\"id\": \"INT\", \"order_id\": \"INT\", \"item_id\": \"INT\", \"quantity\": \"INT\", \"ds\": \"TEXT\"}, \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(id, order_id, item_id, quantity)\"}], [\"assert_order_items_quantity_exceeds_threshold\", {\"quantity\": \"0\"}]], \"expressions\": [], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n orders_table = context.table('sushi.orders')\\n items_table = context.table(ITEMS)\\n for dt in iter_dates(start, end):\\n orders = context.fetchdf(\\n f\\\"\\\"\\\"\\n SELECT *\\n FROM {orders_table}\\n WHERE ds = '{to_ds(dt)}'\\n \\\"\\\"\\\"\\n )\\n items = context.fetchdf(\\n f\\\"\\\"\\\"\\n SELECT *\\n FROM {items_table}\\n WHERE ds = '{to_ds(dt)}'\\n \\\"\\\"\\\"\\n )\\n for order_id in orders['id']:\\n n = random.randint(1, 5)\\n yield pd.DataFrame({'order_id': order_id, 'item_id': items.\\n sample(n=n)['id'], 'quantity': np.random.randint(1, 10, n),\\n 'ds': to_ds(dt)}).reset_index().rename(columns={'index': 'id'})\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/order_items.py\"}, \"ITEMS\": {\"payload\": \"'sushi.items'\", \"kind\": \"value\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}, \"pd\": {\"payload\": \"import pandas as pd\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\"}, \"parents\": [{\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [{\"name\": \"assert_order_items_quantity_exceeds_threshold\", \"dialect\": \"\", \"skip\": false, \"blocking\": true, \"query\": \"SELECT * FROM @this_model WHERE quantity <= @quantity\", \"expressions\": []}], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376401, \"updated_ts\": 1680814376401, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"1015284155\"}","9":"{\"name\": \"sushi.orders\", \"fingerprint\": {\"data_hash\": \"1628439771\", \"metadata_hash\": \"2745052130\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.orders\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"\", \"cron\": \"@daily\", \"description\": \"Table of sushi orders.\", \"start\": \"2022-01-01\", \"batch_size\": 30, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"depends_on\": [], \"columns\": {\"id\": \"INT\", \"customer_id\": \"INT\", \"waiter_id\": \"INT\", \"start_ts\": \"INT\", \"end_ts\": \"INT\", \"ds\": \"TEXT\"}, \"audits\": [], \"expressions\": [], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n dfs = []\\n for dt in iter_dates(start, end):\\n num_orders = random.randint(10, 30)\\n start_ts = [int((dt + timedelta(seconds=random.randint(0, 80000))).\\n timestamp()) for _ in range(num_orders)]\\n end_ts = [int(s + random.randint(0, 60 * 60)) for s in start_ts]\\n dfs.append(pd.DataFrame({'customer_id': random.choices(CUSTOMERS, k\\n =num_orders), 'waiter_id': random.choices(WAITERS, k=num_orders\\n ), 'start_ts': start_ts, 'end_ts': end_ts, 'ds': to_ds(dt)}).\\n reset_index().rename(columns={'index': 'id'}))\\n return pd.concat(dfs)\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/orders.py\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"pd\": {\"payload\": \"import pandas as pd # noqa: TID253\", \"kind\": \"import\"}, \"CUSTOMERS\": {\"payload\": \"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]\", \"kind\": \"value\"}, \"WAITERS\": {\"payload\": \"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\", \"kind\": \"value\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\"}, \"parents\": [], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376402, \"updated_ts\": 1680814376402, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"925846788\"}","10":"{\"name\": \"sushi.waiter_as_customer_by_day\", \"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2824767713\", \"parent_metadata_hash\": \"1349779748\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_as_customer_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"table_properties\": {\"key\": \"'value'\"}, \"query\": \"SELECT w.ds AS ds, w.waiter_id AS waiter_id, wn.name AS waiter_name FROM sushi.waiters AS w JOIN sushi.customers AS c ON w.waiter_id = c.customer_id JOIN sushi.waiter_names AS wn ON w.waiter_id = wn.id\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.waiters\", \"identifier\": \"3386889721\"}, {\"name\": \"sushi.waiter_names\", \"identifier\": \"1604207722\"}, {\"name\": \"sushi.customers\", \"identifier\": \"3148897116\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814464891, \"updated_ts\": 1680814464891, \"ttl\": \"in 1 week\", \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"1267397572\"}], \"indirect_versions\": {}, \"version\": \"3668757715\"}","11":"{\"name\": \"sushi.waiter_names\", \"fingerprint\": {\"data_hash\": \"4133862560\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_names\", \"kind\": {\"name\": \"SEED\", \"path\": \"..\/seeds\/waiter_names.csv\", \"batch_size\": 5}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"seed\": {\"content\": \"id,name\\n0,Toby\\n1,Tyson\\n2,Ryan\\n3,George\\n4,Chris\\n5,Max\\n6,Vincent\\n7,Iaroslav\\n8,Emma\\n9,Maia\\n10,Jim\\n\"}, \"source_type\": \"seed\"}, \"parents\": [], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814464932, \"updated_ts\": 1680814464932, \"ttl\": \"in 1 week\", \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"2505706914\"}], \"indirect_versions\": {\"sushi.waiter_as_customer_by_day\": [{\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"1267397572\"}, {\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2824767713\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"3668757715\"}]}, \"version\": \"1204702829\", \"change_category\": 1}"}} \ No newline at end of file diff --git a/tests/utils/pandas.py b/tests/utils/pandas.py index 130e515be2..422374e0fc 100644 --- a/tests/utils/pandas.py +++ b/tests/utils/pandas.py @@ -3,7 +3,7 @@ import typing as t import numpy as np -import pandas as pd +import pandas as pd # noqa: TID253 def create_df(data: t.Sequence[t.Tuple], schema: t.Dict[str, str]) -> pd.DataFrame: diff --git a/tests/utils/test_date.py b/tests/utils/test_date.py index 62a1b2a12f..03fb6e580c 100644 --- a/tests/utils/test_date.py +++ b/tests/utils/test_date.py @@ -4,7 +4,7 @@ import pytest import time_machine from sqlglot import exp -import pandas as pd +import pandas as pd # noqa: TID253 from sqlmesh.utils.date import ( UTC, diff --git a/tests/utils/test_metaprogramming.py b/tests/utils/test_metaprogramming.py index eaea5e6fa4..c7c22378ac 100644 --- a/tests/utils/test_metaprogramming.py +++ b/tests/utils/test_metaprogramming.py @@ -5,7 +5,7 @@ from tenacity import retry, stop_after_attempt import re -import pandas as pd +import pandas as pd # noqa: TID253 import pytest import sqlglot from pytest_mock.plugin import MockerFixture diff --git a/web/server/api/endpoints/commands.py b/web/server/api/endpoints/commands.py index d8f9490c9b..b879fb07cd 100644 --- a/web/server/api/endpoints/commands.py +++ b/web/server/api/endpoints/commands.py @@ -4,7 +4,6 @@ import io import typing as t -import pandas as pd from fastapi import APIRouter, Body, Depends, Request, Response from starlette.status import HTTP_204_NO_CONTENT from sqlmesh.core.console import Verbosity @@ -66,6 +65,8 @@ async def evaluate( context: Context = Depends(get_loaded_context), ) -> ArrowStreamingResponse: """Evaluate a model with a default limit of 1000""" + import pandas as pd + try: df = context.evaluate( options.model, diff --git a/web/server/utils.py b/web/server/utils.py index e2238d1b0a..868425e76e 100644 --- a/web/server/utils.py +++ b/web/server/utils.py @@ -6,7 +6,6 @@ import typing as t from pathlib import Path, PurePath -import pandas as pd import pyarrow as pa # type: ignore from fastapi import Depends, HTTPException from starlette.responses import StreamingResponse @@ -18,6 +17,9 @@ from web.server.settings import Settings, get_context, get_settings from sqlmesh.utils.windows import IS_WINDOWS +if t.TYPE_CHECKING: + import pandas as pd + R = t.TypeVar("R") From 113325cbc5c985dc31d6309c8cf7ad074a33c55c Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 4 Jun 2025 02:18:09 +0300 Subject: [PATCH 0331/1056] fix: Set `OBJC_DISABLE_INITIALIZE_FORK_SAFETY` for macOS in CLI integration tests (#4644) --- tests/cli/test_integration_cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/cli/test_integration_cli.py b/tests/cli/test_integration_cli.py index 7d8802d193..1b803aa786 100644 --- a/tests/cli/test_integration_cli.py +++ b/tests/cli/test_integration_cli.py @@ -7,6 +7,8 @@ import shutil import site import uuid +import os +import platform pytestmark = pytest.mark.slow @@ -30,6 +32,11 @@ def invoke_cli(tmp_path: Path) -> InvokeCliType: ).stdout.strip() def _invoke(sqlmesh_args: t.List[str], **kwargs: t.Any) -> subprocess.CompletedProcess: + # Set up environment to handle macOS fork safety, see https://stackoverflow.com/a/52230415 + env = os.environ.copy() + if platform.system() == "Darwin": + env["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES" + return subprocess.run( args=[sqlmesh_bin] + sqlmesh_args, # set the working directory to the isolated temp dir for this test @@ -39,6 +46,7 @@ def _invoke(sqlmesh_args: t.List[str], **kwargs: t.Any) -> subprocess.CompletedP # combine stdout/stderr into a single stream stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + env=env, **kwargs, ) From 5a1be925d8dbb777bb91d4bad7aab31be3a763ae Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 4 Jun 2025 02:24:28 +0300 Subject: [PATCH 0332/1056] Feat: add support for Trino's authorization session property (#4639) --- sqlmesh/core/engine_adapter/trino.py | 22 +++++++- sqlmesh/core/model/meta.py | 10 +++- tests/core/engine_adapter/test_trino.py | 46 +++++++++++++++ tests/core/test_model.py | 75 +++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index aed69d47f9..aab1bf3c0e 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -1,4 +1,6 @@ from __future__ import annotations + +import contextlib import re import typing as t from functools import lru_cache @@ -28,7 +30,7 @@ from sqlmesh.utils.date import TimeLike if t.TYPE_CHECKING: - from sqlmesh.core._typing import SchemaName, TableName + from sqlmesh.core._typing import SchemaName, SessionProperties, TableName from sqlmesh.core.engine_adapter._typing import DF, QueryOrDF @@ -87,6 +89,24 @@ def get_catalog_type(self, catalog: t.Optional[str]) -> str: ) return seq_get(row, 0) or self.DEFAULT_CATALOG_TYPE + @contextlib.contextmanager + def session(self, properties: SessionProperties) -> t.Iterator[None]: + authorization = properties.get("authorization") + if not authorization: + yield + return + + if not isinstance(authorization, exp.Expression): + authorization = exp.Literal.string(authorization) + + authorization_sql = authorization.sql(dialect=self.dialect) + + self.execute(f"SET SESSION AUTHORIZATION {authorization_sql}") + try: + yield + finally: + self.execute(f"RESET SESSION AUTHORIZATION") + def _insert_overwrite_by_condition( self, table_name: TableName, diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 61a657ba6b..3e67995c89 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -319,7 +319,9 @@ def session_properties_validator(cls, v: t.Any, info: ValidationInfo) -> t.Any: return parsed_session_properties for eq in parsed_session_properties: - if eq.name == "query_label": + prop_name = eq.left.name + + if prop_name == "query_label": query_label = eq.right if not ( isinstance(query_label, exp.Array) @@ -345,6 +347,12 @@ def session_properties_validator(cls, v: t.Any, info: ValidationInfo) -> t.Any: raise ConfigError( "Invalid entry in `session_properties.query_label`. Must be tuples of string literals with length 2." ) + elif prop_name == "authorization": + authorization = eq.right + if not (isinstance(authorization, exp.Literal) and authorization.is_string): + raise ConfigError( + "Invalid value for `session_properties.authorization`. Must be a string literal." + ) return parsed_session_properties diff --git a/tests/core/engine_adapter/test_trino.py b/tests/core/engine_adapter/test_trino.py index 8e44404708..2ae766baec 100644 --- a/tests/core/engine_adapter/test_trino.py +++ b/tests/core/engine_adapter/test_trino.py @@ -591,3 +591,49 @@ def test_create_schema_sets_location(make_mocked_engine_adapter: t.Callable, moc 'CREATE SCHEMA IF NOT EXISTS "landing"."transactions" WITH (LOCATION=\'s3://raw-data/landing/transactions\')', # match '^landing\..*$' ] ) + + +def test_session_authorization(trino_mocked_engine_adapter: TrinoEngineAdapter): + adapter = trino_mocked_engine_adapter + + # Test 1: No authorization property - should not execute any authorization commands + with adapter.session({}): + pass + + assert to_sql_calls(adapter) == [] + + # Test 2: String authorization + with adapter.session({"authorization": "test_user"}): + adapter.execute("SELECT 1") + + assert to_sql_calls(adapter) == [ + "SET SESSION AUTHORIZATION 'test_user'", + "SELECT 1", + "RESET SESSION AUTHORIZATION", + ] + + # Test 3: Expression authorization + adapter.cursor.execute.reset_mock() + with adapter.session({"authorization": exp.Literal.string("another_user")}): + adapter.execute("SELECT 2") + + assert to_sql_calls(adapter) == [ + "SET SESSION AUTHORIZATION 'another_user'", + "SELECT 2", + "RESET SESSION AUTHORIZATION", + ] + + # Test 4: RESET is called even if exception occurs during session + adapter.cursor.execute.reset_mock() + try: + with adapter.session({"authorization": "test_user"}): + adapter.execute("SELECT 1") + raise RuntimeError("Test exception") + except RuntimeError: + pass + + assert to_sql_calls(adapter) == [ + "SET SESSION AUTHORIZATION 'test_user'", + "SELECT 1", + "RESET SESSION AUTHORIZATION", + ] diff --git a/tests/core/test_model.py b/tests/core/test_model.py index efda5bc454..c1e0704c88 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -4531,6 +4531,81 @@ def test_model_session_properties(sushi_context): ) +def test_session_properties_authorization_validation(): + model = load_sql_based_model( + d.parse( + """ + MODEL ( + name test_schema.test_model, + session_properties ( + authorization = 'test_user' + ) + ); + SELECT a FROM tbl; + """, + default_dialect="trino", + ) + ) + assert model.session_properties == {"authorization": "test_user"} + + with pytest.raises( + ConfigError, + match=r"Invalid value for `session_properties.authorization`. Must be a string literal.", + ): + load_sql_based_model( + d.parse( + """ + MODEL ( + name test_schema.test_model, + session_properties ( + authorization = 123 + ) + ); + SELECT a FROM tbl; + """, + default_dialect="trino", + ) + ) + + with pytest.raises( + ConfigError, + match=r"Invalid value for `session_properties.authorization`. Must be a string literal.", + ): + load_sql_based_model( + d.parse( + """ + MODEL ( + name test_schema.test_model, + session_properties ( + authorization = some_column + ) + ); + SELECT a FROM tbl; + """, + default_dialect="trino", + ) + ) + + with pytest.raises( + ConfigError, + match=r"Invalid value for `session_properties.authorization`. Must be a string literal.", + ): + load_sql_based_model( + d.parse( + """ + MODEL ( + name test_schema.test_model, + session_properties ( + authorization = CONCAT('user', '_suffix') + ) + ); + SELECT a FROM tbl; + """, + default_dialect="trino", + ) + ) + + def test_model_jinja_macro_rendering(): expressions = d.parse( """ From aa0c50fae43fafe810ffe140831fc4e0ebd44b0e Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 4 Jun 2025 04:01:57 +0300 Subject: [PATCH 0333/1056] Fix: ensure session-scoped Snowflake warehouse is rolled back on failure (#4640) --- sqlmesh/core/engine_adapter/snowflake.py | 6 ++++-- tests/core/engine_adapter/test_snowflake.py | 22 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index 8b05973c19..032c1da4fe 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -93,8 +93,10 @@ def session(self, properties: SessionProperties) -> t.Iterator[None]: return self.execute(f"USE WAREHOUSE {warehouse_sql}") - yield - self.execute(f"USE WAREHOUSE {current_warehouse_sql}") + try: + yield + finally: + self.execute(f"USE WAREHOUSE {current_warehouse_sql}") @property def _current_warehouse(self) -> exp.Identifier: diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index c6101542a8..f0a47b3393 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -98,6 +98,7 @@ def test_session( adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter) adapter.cursor.fetchone.return_value = (current_warehouse,) + # Test normal execution with adapter.session({"warehouse": configured_warehouse}): pass @@ -114,6 +115,27 @@ def test_session( assert to_sql_calls(adapter) == expected_calls + # Test exception handling - warehouse should still be reset + if should_change: + adapter.cursor.execute.reset_mock() + adapter.cursor.fetchone.return_value = (current_warehouse,) + + try: + with adapter.session({"warehouse": configured_warehouse}): + adapter.execute("SELECT 1") + raise RuntimeError("Test exception") + except RuntimeError: + pass + + expected_exception_calls = [ + "SELECT CURRENT_WAREHOUSE()", + f"USE WAREHOUSE {configured_warehouse_exp}", + "SELECT 1", + f"USE WAREHOUSE {current_warehouse_exp}", + ] + + assert to_sql_calls(adapter) == expected_exception_calls + def test_comments(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter) From c29cf422a3cffd8a03fe42e4fa84f81ed5a594e3 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 4 Jun 2025 04:40:27 +0300 Subject: [PATCH 0334/1056] Fix: import pandas before loading in parallel to avoid macOS fork-related errors (#4647) --- sqlmesh/core/loader.py | 7 +++++++ tests/cli/test_integration_cli.py | 8 -------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index ee0af270cb..e7df315768 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -164,6 +164,13 @@ class Loader(abc.ABC): """Abstract base class to load macros and models for a context""" def __init__(self, context: GenericContext, path: Path) -> None: + # This ensures pandas is imported before any model loading happens in the forked process + # to avoid macOS fork() safety issues, see https://stackoverflow.com/a/52230415. Without + # it, the following error was observerd in a macOS 15.5 system: + # + # "+[NSMutableString initialize] may have been in progress in another thread when fork() was called." + import pandas as pd # noqa + from sqlmesh.core.console import get_console self._path_mtimes: t.Dict[Path, float] = {} diff --git a/tests/cli/test_integration_cli.py b/tests/cli/test_integration_cli.py index 1b803aa786..7d8802d193 100644 --- a/tests/cli/test_integration_cli.py +++ b/tests/cli/test_integration_cli.py @@ -7,8 +7,6 @@ import shutil import site import uuid -import os -import platform pytestmark = pytest.mark.slow @@ -32,11 +30,6 @@ def invoke_cli(tmp_path: Path) -> InvokeCliType: ).stdout.strip() def _invoke(sqlmesh_args: t.List[str], **kwargs: t.Any) -> subprocess.CompletedProcess: - # Set up environment to handle macOS fork safety, see https://stackoverflow.com/a/52230415 - env = os.environ.copy() - if platform.system() == "Darwin": - env["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES" - return subprocess.run( args=[sqlmesh_bin] + sqlmesh_args, # set the working directory to the isolated temp dir for this test @@ -46,7 +39,6 @@ def _invoke(sqlmesh_args: t.List[str], **kwargs: t.Any) -> subprocess.CompletedP # combine stdout/stderr into a single stream stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - env=env, **kwargs, ) From 8121aa905b17c3594db0c6a65e8ff0bca722adeb Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 3 Jun 2025 20:11:37 -0700 Subject: [PATCH 0335/1056] fix: cicd bot correctly surface plan errors (#4646) --- sqlmesh/integrations/github/cicd/command.py | 6 ++++-- sqlmesh/integrations/github/cicd/controller.py | 7 +++++++ tests/integrations/github/cicd/test_github_commands.py | 9 +++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index 9ccb8f563d..01a9236e33 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -185,9 +185,11 @@ def _deploy_production(controller: GithubController) -> bool: skip_reason=str(e), ) return False - except PlanError: + except PlanError as e: controller.update_prod_environment_check( - status=GithubCheckStatus.COMPLETED, conclusion=GithubCheckConclusion.ACTION_REQUIRED + status=GithubCheckStatus.COMPLETED, + conclusion=GithubCheckConclusion.ACTION_REQUIRED, + plan_error=e, ) return False except Exception: diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index eadb34e3a6..2afccc4879 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -977,6 +977,7 @@ def update_prod_environment_check( status: GithubCheckStatus, conclusion: t.Optional[GithubCheckConclusion] = None, skip_reason: t.Optional[str] = None, + plan_error: t.Optional[PlanError] = None, ) -> None: """ Updates the status of the merge commit for the prod environment. @@ -990,6 +991,7 @@ def conclusion_handler( GithubCheckConclusion.CANCELLED: "Cancelled deploying to prod", GithubCheckConclusion.SKIPPED: skip_reason, GithubCheckConclusion.FAILURE: "Failed to deploy to prod", + GithubCheckConclusion.ACTION_REQUIRED: "Failed due to error applying plan", } title = ( conclusion_to_title.get(conclusion) @@ -1002,6 +1004,11 @@ def conclusion_handler( summary = ( captured_errors or f"{title}\n\n**Error:**\n```\n{traceback.format_exc()}\n```" ) + elif conclusion.is_action_required: + if plan_error: + summary = f"**Plan error:**\n```\n{plan_error}\n```" + else: + summary = "Got an action required conclusion but no plan error was provided. This is unexpected." else: summary = "**Generated Prod Plan**\n" + self.get_plan_summary(self.prod_plan) return conclusion, title, summary diff --git a/tests/integrations/github/cicd/test_github_commands.py b/tests/integrations/github/cicd/test_github_commands.py index 0d2d303fb7..4d62838968 100644 --- a/tests/integrations/github/cicd/test_github_commands.py +++ b/tests/integrations/github/cicd/test_github_commands.py @@ -884,6 +884,15 @@ def raise_on_prod_plan(plan: Plan): assert GithubCheckStatus(prod_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_checks_runs[2]["conclusion"]) == expect_prod_sync_conclusion + if expect_prod_sync_conclusion.is_action_required: + assert prod_checks_runs[2]["output"]["title"] == "Failed due to error applying plan" + assert ( + prod_checks_runs[2]["output"]["summary"] + == f"""**Plan error:** +``` +{to_raise_on_prod_plan} +```""" + ) assert "SQLMesh - Run Unit Tests" in controller._check_run_mapping test_checks_runs = controller._check_run_mapping["SQLMesh - Run Unit Tests"].all_kwargs From d4dfe0ffc278f771866cf30414538d8db78796c3 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 4 Jun 2025 15:23:33 +1200 Subject: [PATCH 0336/1056] Fix(cicd): Better handle model failures in GitHub check output (#4648) --- pyproject.toml | 2 +- sqlmesh/__init__.py | 4 + sqlmesh/core/console.py | 6 ++ sqlmesh/integrations/github/cicd/command.py | 1 + .../integrations/github/cicd/controller.py | 78 ++++++++++++++----- .../github/cicd/{fixtures.py => conftest.py} | 43 ++++++++-- .../github/cicd/test_github_commands.py | 1 - .../github/cicd/test_github_controller.py | 3 +- .../github/cicd/test_github_event.py | 1 - .../github/cicd/test_integration.py | 65 ++++++++++++++-- 10 files changed, 164 insertions(+), 40 deletions(-) rename tests/integrations/github/cicd/{fixtures.py => conftest.py} (78%) diff --git a/pyproject.toml b/pyproject.toml index 276f305a0c..7299728842 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ dev = [ "psycopg2-binary", "pydantic", "PyAthena[Pandas]", - "PyGithub~=2.5.0", + "PyGithub", "pyperf", "pyspark~=3.5.0", "pytest", diff --git a/sqlmesh/__init__.py b/sqlmesh/__init__.py index 63f109d489..47e9bacce2 100644 --- a/sqlmesh/__init__.py +++ b/sqlmesh/__init__.py @@ -197,6 +197,10 @@ def configure_logging( level = logging.DEBUG if debug else logging.INFO logger.setLevel(level) + if debug: + # Remove noisy snowflake connector logs that are not useful for users + logging.getLogger("snowflake.connector").setLevel(logging.INFO) + if write_to_stdout: stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(CustomFormatter()) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index fcb8ceb864..a647f49694 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -3184,6 +3184,12 @@ def log_error(self, message: str) -> None: def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None: logger.warning(long_message or short_message) + + if not short_message.endswith("\n"): + short_message += ( + "\n" # so that the closing ``` ends up on a newline which is important for GitHub + ) + self._print(f"```\n\\[WARNING] {short_message}```\n\n") def _print(self, value: t.Any, **kwargs: t.Any) -> None: diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index 01a9236e33..b360f3366e 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -22,6 +22,7 @@ @click.option( "--token", type=str, + envvar="GITHUB_TOKEN", help="The Github Token to be used. Pass in `${{ secrets.GITHUB_TOKEN }}` if you want to use the one created by Github actions", ) @click.pass_context diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 2afccc4879..ad1b4b52b2 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -11,6 +11,7 @@ import unittest from enum import Enum from typing import List +from pathlib import Path import requests from hyperscript import Element, h @@ -28,11 +29,11 @@ SnapshotTableInfo, format_intervals, ) +from sqlglot.errors import SqlglotError from sqlmesh.core.user import User from sqlmesh.core.config import Config from sqlmesh.integrations.github.cicd.config import GithubCICDBotConfig from sqlmesh.utils import word_characters_only, Verbosity -from sqlmesh.utils.concurrency import NodeExecutionFailedError from sqlmesh.utils.date import now from sqlmesh.utils.errors import ( CICDBotError, @@ -40,6 +41,7 @@ PlanError, UncategorizedPlanError, LinterError, + SQLMeshError, ) from sqlmesh.utils.pydantic import PydanticModel @@ -283,7 +285,7 @@ class GithubController: def __init__( self, - paths: t.Union[str, t.Iterable[str]], + paths: t.Union[Path, t.Iterable[Path]], token: str, config: t.Optional[t.Union[Config, str]] = None, event: t.Optional[GithubEvent] = None, @@ -307,10 +309,13 @@ def __init__( raise CICDBotError("Console must be a markdown console.") self._console = t.cast(MarkdownConsole, get_console()) + from github.Consts import DEFAULT_BASE_URL + from github.Auth import Token + self._client: Github = client or Github( - base_url=os.environ["GITHUB_API_URL"], - login_or_token=self._token, + base_url=os.environ.get("GITHUB_API_URL", DEFAULT_BASE_URL), auth=Token(self._token) ) + self._repo: Repository = self._client.get_repo( self._event.pull_request_info.full_repo_path, lazy=True ) @@ -328,6 +333,9 @@ def __init__( logger.debug(f"Approvers: {', '.join(self._approvers)}") self._context: Context = Context(paths=self._paths, config=self.config) + # Bot config needs the context to be initialized + logger.debug(f"Bot config: {self.bot_config.json(indent=2)}") + @property def deploy_command_enabled(self) -> bool: return self.bot_config.enable_deploy_command @@ -433,7 +441,6 @@ def bot_config(self) -> GithubCICDBotConfig: bot_config = self._context.config.cicd_bot or GithubCICDBotConfig( auto_categorize_changes=self._context.auto_categorize_changes ) - logger.debug(f"Bot config: {bot_config.json(indent=2)}") return bot_config @property @@ -454,8 +461,11 @@ def _append_output(cls, key: str, value: str) -> None: Appends the given key/value to output so they can be read by following steps """ logger.debug(f"Setting output. Key: {key}, Value: {value}") - with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh: - print(f"{key}={value}", file=fh) + + # GitHub Actions sets this environment variable + if output_file := os.environ.get("GITHUB_OUTPUT"): + with open(output_file, "a", encoding="utf-8") as fh: + print(f"{key}={value}", file=fh) def get_plan_summary(self, plan: Plan) -> str: try: @@ -637,15 +647,31 @@ def _update_check( if text: kwargs["output"]["text"] = text logger.debug(f"Updating check with kwargs: {kwargs}") - if name in self._check_run_mapping: - logger.debug(f"Found check run in mapping so updating it. Name: {name}") - check_run = self._check_run_mapping[name] - check_run.edit( - **{k: v for k, v in kwargs.items() if k not in ("name", "head_sha", "started_at")} - ) + + if self.running_in_github_actions: + # Only make the API call to update the checks if we are running within GitHub Actions + # One very annoying limitation of the Pull Request Checks API is that its only available to GitHub Apps + # and not personal access tokens, which makes it unable to be utilized during local development + if name in self._check_run_mapping: + logger.debug(f"Found check run in mapping so updating it. Name: {name}") + check_run = self._check_run_mapping[name] + check_run.edit( + **{ + k: v + for k, v in kwargs.items() + if k not in ("name", "head_sha", "started_at") + } + ) + else: + logger.debug(f"Did not find check run in mapping so creating it. Name: {name}") + self._check_run_mapping[name] = self._repo.create_check_run(**kwargs) else: - logger.debug(f"Did not find check run in mapping so creating it. Name: {name}") - self._check_run_mapping[name] = self._repo.create_check_run(**kwargs) + # Output the summary using print() so the newlines are resolved and the result can easily + # be disambiguated from the rest of the console output and copy+pasted into a Markdown renderer + print( + f"---CHECK OUTPUT START: {kwargs['output']['title']} ---\n{kwargs['output']['summary']}\n---CHECK OUTPUT END---\n" + ) + if conclusion: self._append_output( word_characters_only(name.replace("SQLMesh - ", "").lower()), conclusion.value @@ -890,15 +916,21 @@ def conclusion_handler( else: skip_reason = "A prior stage failed resulting in skipping PR creation." + if not skip_reason and exception: + logger.debug( + f"Got {type(exception).__name__}. Stack trace: " + traceback.format_exc() + ) + captured_errors = self._console.consume_captured_errors() if captured_errors: logger.debug(f"Captured errors: {captured_errors}") failure_msg = f"**Errors:**\n{captured_errors}\n" - elif isinstance(exception, NodeExecutionFailedError): - logger.debug( - "Got Node Execution Failed Error. Stack trace: " + traceback.format_exc() - ) - failure_msg = f"Node `{exception.node.name}` failed to apply.\n\n**Stack Trace:**\n```\n{traceback.format_exc()}\n```" + elif isinstance(exception, PlanError): + failure_msg = f"Plan application failed.\n\n{self._console.captured_output}" + elif isinstance(exception, (SQLMeshError, SqlglotError, ValueError)): + # this logic is taken from the global error handler attached to the CLI, which uses `click.echo()` to output the message + # so cant be re-used here because it bypasses the Console + failure_msg = f"**Error:** {str(exception)}" else: logger.debug( "Got unexpected error. Error Type: " @@ -909,7 +941,7 @@ def conclusion_handler( failure_msg = f"This is an unexpected error.\n\n**Exception:**\n```\n{traceback.format_exc()}\n```" conclusion_to_summary = { GithubCheckConclusion.SKIPPED: f":next_track_button: Skipped creating or updating PR Environment `{self.pr_environment_name}`. {skip_reason}", - GithubCheckConclusion.FAILURE: f":x: Failed to create or update PR Environment `{self.pr_environment_name}`.\n{failure_msg}", + GithubCheckConclusion.FAILURE: f":x: Failed to create or update PR Environment `{self.pr_environment_name}`.\n\n{failure_msg}", GithubCheckConclusion.CANCELLED: f":stop_sign: Cancelled creating or updating PR Environment `{self.pr_environment_name}`", GithubCheckConclusion.ACTION_REQUIRED: f":warning: Action Required to create or update PR Environment `{self.pr_environment_name}`. There are likely uncateogrized changes. Run `plan` locally to apply these changes. If you want the bot to automatically categorize changes, then check documentation (https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information.", } @@ -1061,3 +1093,7 @@ def _chunk_up_api_message(self, message: str) -> t.List[str]: message_encoded[i : i + self.MAX_BYTE_LENGTH].decode("utf-8", "ignore") for i in range(0, len(message_encoded), self.MAX_BYTE_LENGTH) ] + + @property + def running_in_github_actions(self) -> bool: + return os.environ.get("GITHUB_ACTIONS", None) == "true" diff --git a/tests/integrations/github/cicd/fixtures.py b/tests/integrations/github/cicd/conftest.py similarity index 78% rename from tests/integrations/github/cicd/fixtures.py rename to tests/integrations/github/cicd/conftest.py index 1ff203b7b3..30f65aecdc 100644 --- a/tests/integrations/github/cicd/fixtures.py +++ b/tests/integrations/github/cicd/conftest.py @@ -2,6 +2,7 @@ import pytest from pytest_mock.plugin import MockerFixture +from pathlib import Path from sqlmesh.core.config import Config from sqlmesh.core.console import set_console, get_console, MarkdownConsole @@ -13,6 +14,7 @@ PullRequestInfo, ) from sqlmesh.utils import AttributeDict +from sqlglot.helper import ensure_list @pytest.fixture @@ -39,12 +41,12 @@ def github_client(mocker: MockerFixture): @pytest.fixture -def make_pull_request_review() -> t.Callable: +def make_pull_request_review(github_client) -> t.Callable: from github.PullRequestReview import PullRequestReview def _make_function(username: str, state: str, **kwargs) -> PullRequestReview: return PullRequestReview( - "test", # type: ignore + github_client.requester, {}, { # Name is whatever they provide in their GitHub profile or login as fallback. Always use login. @@ -52,24 +54,34 @@ def _make_function(username: str, state: str, **kwargs) -> PullRequestReview: "state": state, **kwargs, }, - completed=False, ) return _make_function @pytest.fixture -def make_controller(mocker: MockerFixture, copy_to_temp_path: t.Callable) -> t.Callable: +def sqlmesh_repo_root_path() -> Path: + return next(p for p in Path(__file__).parents if str(p).endswith("tests")).parent + + +@pytest.fixture +def make_controller( + mocker: MockerFixture, + copy_to_temp_path: t.Callable, + monkeypatch: pytest.MonkeyPatch, + sqlmesh_repo_root_path: Path, +) -> t.Callable: from github import Github def _make_function( - event_path: t.Union[str, t.Dict], + event_path: t.Union[str, Path, t.Dict], client: Github, *, merge_state_status: MergeStateStatus = MergeStateStatus.CLEAN, bot_config: t.Optional[GithubCICDBotConfig] = None, mock_out_context: bool = True, config: t.Optional[t.Union[Config, str]] = None, + paths: t.Optional[t.Union[Path, t.List[Path]]] = None, ) -> GithubController: if mock_out_context: mocker.patch("sqlmesh.core.context.Context.apply", mocker.MagicMock()) @@ -85,7 +97,23 @@ def _make_function( bot_config, ) - paths = copy_to_temp_path("examples/sushi") + if paths is None: + paths = copy_to_temp_path(sqlmesh_repo_root_path / "examples" / "sushi") + + paths = ensure_list(paths) + + if isinstance(event_path, str): + # resolve relative event_path references to absolute so they dont get affected by chdir() below + as_path = Path(event_path) + if not as_path.is_absolute(): + event_path = sqlmesh_repo_root_path / as_path + + # set the current working directory to the temp path so that config references to eg duckdb "db.db" + # get created in the temp path and not in the SQLMesh repo root path that the tests are triggered from + monkeypatch.chdir(paths[0]) + + # make the tests think they are running in GitHub Actions + monkeypatch.setenv("GITHUB_ACTIONS", "true") orig_console = get_console() try: @@ -96,12 +124,13 @@ def _make_function( token="abc", event=( GithubEvent.from_path(event_path) - if isinstance(event_path, str) + if isinstance(event_path, (str, Path)) else GithubEvent.from_obj(event_path) ), client=client, config=config, ) + finally: set_console(orig_console) diff --git a/tests/integrations/github/cicd/test_github_commands.py b/tests/integrations/github/cicd/test_github_commands.py index 4d62838968..17cd9fc7b7 100644 --- a/tests/integrations/github/cicd/test_github_commands.py +++ b/tests/integrations/github/cicd/test_github_commands.py @@ -18,7 +18,6 @@ ) from sqlmesh.utils.errors import ConflictingPlanError, PlanError, TestError, CICDBotError -pytest_plugins = ["tests.integrations.github.cicd.fixtures"] pytestmark = [ pytest.mark.github, pytest.mark.slow, diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index 1104ac28d2..208f59b105 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -19,9 +19,8 @@ GithubCheckStatus, MergeStateStatus, ) -from tests.integrations.github.cicd.fixtures import MockIssueComment +from tests.integrations.github.cicd.conftest import MockIssueComment -pytest_plugins = ["tests.integrations.github.cicd.fixtures"] pytestmark = pytest.mark.github github_controller_approvers_params = [ diff --git a/tests/integrations/github/cicd/test_github_event.py b/tests/integrations/github/cicd/test_github_event.py index 719b60fb3f..88979b05a8 100644 --- a/tests/integrations/github/cicd/test_github_event.py +++ b/tests/integrations/github/cicd/test_github_event.py @@ -1,6 +1,5 @@ import pytest -pytest_plugins = ["tests.integrations.github.cicd.fixtures"] pytestmark = pytest.mark.github diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index 1b26052c64..3de9d2cc9f 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -23,10 +23,9 @@ GithubCheckStatus, GithubController, ) -from sqlmesh.utils.errors import CICDBotError -from tests.integrations.github.cicd.fixtures import MockIssueComment +from sqlmesh.utils.errors import CICDBotError, SQLMeshError +from tests.integrations.github.cicd.conftest import MockIssueComment -pytest_plugins = ["tests.integrations.github.cicd.fixtures"] pytestmark = [ pytest.mark.slow, pytest.mark.github, @@ -1502,10 +1501,9 @@ def test_error_msg_when_applying_plan_with_bug( assert GithubCheckStatus(pr_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_failure assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" - assert ( - 'Binder Error: Referenced column "non_existing_col" not found in FROM clause!' - in pr_checks_runs[2]["output"]["summary"] - ) + summary = pr_checks_runs[2]["output"]["summary"].replace("\n", "") + assert 'Failed models **"memory"."sushi"."waiter_revenue_by_day"**' in summary + assert 'Binder Error: Referenced column "non_existing_col" not found in FROM clause!' in summary assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping prod_plan_preview_checks_runs = controller._check_run_mapping[ @@ -2129,3 +2127,56 @@ def test_has_required_approval_but_not_base_branch( output == "run_unit_tests=success\nhas_required_approval=success\ncreated_pr_environment=true\npr_environment_name=hello_world_2\npr_environment_synced=success\nprod_plan_preview=success\n" ) + + +def test_unexpected_error_is_handled( + github_client, + make_controller, + make_mock_check_run, + make_mock_issue_comment, + mocker: MockerFixture, +): + """ + Scenario: + - Plan throws a SQLMeshError due to a migration version mismatch + - Outcome should be a nice error like the CLI gives and not a stack trace + """ + + mock_repo = github_client.get_repo() + mock_repo.create_check_run = mocker.MagicMock( + side_effect=lambda **kwargs: make_mock_check_run(**kwargs) + ) + + created_comments: t.List[MockIssueComment] = [] + mock_issue = mock_repo.get_issue() + mock_issue.create_comment = mocker.MagicMock( + side_effect=lambda comment: make_mock_issue_comment( + comment=comment, created_comments=created_comments + ) + ) + + controller = make_controller( + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + bot_config=GithubCICDBotConfig(), + mock_out_context=True, + ) + assert isinstance(controller, GithubController) + + assert isinstance(controller._context.apply, mocker.MagicMock) + controller._context.apply.side_effect = SQLMeshError( + "SQLGlot (local) is using version 'X' which is ahead of 'Y' (remote). Please run a migration" + ) + + command._update_pr_environment(controller) + + assert "SQLMesh - PR Environment Synced" in controller._check_run_mapping + pr_checks_runs = controller._check_run_mapping["SQLMesh - PR Environment Synced"].all_kwargs # type: ignore + assert pr_checks_runs[1]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" + summary = pr_checks_runs[1]["output"]["summary"] + assert ( + "**Error:** SQLGlot (local) is using version 'X' which is ahead of 'Y' (remote). Please run a migration" + in pr_checks_runs[1]["output"]["summary"] + ) + assert "SQLMeshError" not in summary + assert "Traceback (most recent call last)" not in summary From a521311a5e8625b17a376f10959fd31939467cd3 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:45:42 +0300 Subject: [PATCH 0337/1056] Chore!: bump sqlglot to v26.25.3 (#4650) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7299728842..1def9d5fc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.24.0", + "sqlglot[rs]~=26.25.3", "tenacity", "time-machine", "json-stream" From 9ac41cc74055c2ae166eaa02433113a6d2f0de26 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 4 Jun 2025 16:52:33 +0300 Subject: [PATCH 0338/1056] Chore: Fix make style by adding correct pygithub version (#4651) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1def9d5fc7..a7640fa898 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ dev = [ "psycopg2-binary", "pydantic", "PyAthena[Pandas]", - "PyGithub", + "PyGithub>=2.6.0", "pyperf", "pyspark~=3.5.0", "pytest", From 4ada5f801f1b053eeb440b42028d74bd4bec4979 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 4 Jun 2025 08:35:52 -0700 Subject: [PATCH 0339/1056] feat: lazy import numpy like pandas (#4653) --- examples/sushi/helper.py | 2 +- examples/sushi/models/items.py | 2 +- examples/sushi/models/order_items.py | 2 +- examples/sushi/models/raw_marketing.py | 2 +- examples/wursthall/models/db/order_f.py | 2 +- examples/wursthall/models/src/order_item_details.py | 2 +- examples/wursthall/models/src/shared.py | 2 +- pyproject.toml | 3 ++- sqlmesh/core/console.py | 2 +- sqlmesh/core/engine_adapter/mssql.py | 2 +- sqlmesh/core/model/definition.py | 3 ++- sqlmesh/core/test/definition.py | 7 ++++++- tests/core/engine_adapter/integration/test_integration.py | 2 +- tests/core/test_integration.py | 2 +- tests/core/test_table_diff.py | 2 +- tests/utils/pandas.py | 2 +- web/server/api/endpoints/table_diff.py | 3 ++- 17 files changed, 25 insertions(+), 17 deletions(-) diff --git a/examples/sushi/helper.py b/examples/sushi/helper.py index 4d5c1ad960..9a853b0903 100644 --- a/examples/sushi/helper.py +++ b/examples/sushi/helper.py @@ -2,7 +2,7 @@ import typing as t from datetime import datetime, timedelta -import numpy as np +import numpy as np # noqa: TID253 def set_seed(dt: datetime) -> None: diff --git a/examples/sushi/models/items.py b/examples/sushi/models/items.py index 9331f43fa1..54c9442dc5 100644 --- a/examples/sushi/models/items.py +++ b/examples/sushi/models/items.py @@ -2,7 +2,7 @@ import typing as t from datetime import datetime -import numpy as np +import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 from helper import iter_dates # type: ignore from sqlglot.expressions import to_column diff --git a/examples/sushi/models/order_items.py b/examples/sushi/models/order_items.py index 07ed351d9f..9d4dc551e3 100644 --- a/examples/sushi/models/order_items.py +++ b/examples/sushi/models/order_items.py @@ -2,7 +2,7 @@ import typing as t from datetime import datetime -import numpy as np +import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 from helper import iter_dates # type: ignore from sqlglot import exp diff --git a/examples/sushi/models/raw_marketing.py b/examples/sushi/models/raw_marketing.py index f02b3596fa..b17c471895 100644 --- a/examples/sushi/models/raw_marketing.py +++ b/examples/sushi/models/raw_marketing.py @@ -2,7 +2,7 @@ import typing as t from datetime import datetime -import numpy as np +import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 from sqlglot import exp diff --git a/examples/wursthall/models/db/order_f.py b/examples/wursthall/models/db/order_f.py index fe61654148..d682d55f02 100644 --- a/examples/wursthall/models/db/order_f.py +++ b/examples/wursthall/models/db/order_f.py @@ -2,7 +2,7 @@ import typing as t from datetime import datetime -import numpy as np +import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 from models.src.shared import DATA_START_DATE_STR, set_seed # type: ignore from sqlglot import parse_one diff --git a/examples/wursthall/models/src/order_item_details.py b/examples/wursthall/models/src/order_item_details.py index a5f880c494..852250d2e2 100644 --- a/examples/wursthall/models/src/order_item_details.py +++ b/examples/wursthall/models/src/order_item_details.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from datetime import datetime -import numpy as np +import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 from faker import Faker from models.src.shared import DATA_START_DATE_STR, iter_dates, set_seed # type: ignore diff --git a/examples/wursthall/models/src/shared.py b/examples/wursthall/models/src/shared.py index d220825ba4..d183a1c5db 100644 --- a/examples/wursthall/models/src/shared.py +++ b/examples/wursthall/models/src/shared.py @@ -4,7 +4,7 @@ import typing as t from datetime import date, timedelta -import numpy as np +import numpy as np # noqa: TID253 from faker import Faker SEED = 99999999 diff --git a/pyproject.toml b/pyproject.toml index a7640fa898..c2464ccfb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -275,5 +275,6 @@ extend-select = ["TID"] [tool.ruff.lint.flake8-tidy-imports] banned-module-level-imports = [ - "pandas", + "pandas", + "numpy", ] diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index a647f49694..e272442e67 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -8,7 +8,6 @@ import logging import textwrap from pathlib import Path -import numpy as np from hyperscript import h from rich.console import Console as RichConsole from rich.live import Live @@ -2497,6 +2496,7 @@ def show_linter_violations( def _cells_match(x: t.Any, y: t.Any) -> bool: """Helper function to compare two cells and returns true if they're equal, handling array objects.""" import pandas as pd + import numpy as np # Convert array-like objects to list for consistent comparison def _normalize(val: t.Any) -> t.Any: diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index e94afcf0db..796bb87960 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -4,7 +4,6 @@ import typing as t -import numpy as np from sqlglot import exp from sqlmesh.core.dialect import to_schema @@ -215,6 +214,7 @@ def _df_to_source_queries( target_table: TableName, ) -> t.List[SourceQuery]: import pandas as pd + import numpy as np assert isinstance(df, pd.DataFrame) temp_table = self._get_temp_table(target_table or "pandas") diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index a8da3577bb..23346a15a2 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -8,7 +8,6 @@ from functools import cached_property, partial from pathlib import Path -import numpy as np from pydantic import Field from sqlglot import diff, exp from sqlglot.diff import Insert @@ -1565,6 +1564,8 @@ def render( yield from self.render_seed() def render_seed(self) -> t.Iterator[QueryOrDF]: + import numpy as np + self._ensure_hydrated() date_columns = [] diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index b8081d0bb2..a766706801 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -10,7 +10,6 @@ from pathlib import Path from unittest.mock import patch -import numpy as np from io import StringIO from sqlglot import Dialect, exp from sqlglot.optimizer.annotate_types import annotate_types @@ -207,6 +206,7 @@ def assert_equal( partial: t.Optional[bool] = False, ) -> None: """Compare two DataFrames""" + import numpy as np import pandas as pd from pandas.api.types import is_object_dtype @@ -775,6 +775,8 @@ def generate_test( name: The name of the test. This is inferred from the model name by default. include_ctes: When true, CTE fixtures will also be generated. """ + import numpy as np + test_name = name or f"test_{model.view_name}" path = path or f"{test_name}.yaml" @@ -902,6 +904,7 @@ def _raise_if_unexpected_columns( def _row_difference(left: pd.DataFrame, right: pd.DataFrame) -> pd.DataFrame: """Returns all rows in `left` that don't appear in `right`.""" + import numpy as np import pandas as pd rows_missing_from_right = [] @@ -928,6 +931,8 @@ def _raise_error(msg: str, path: Path | None = None) -> None: 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 + if isinstance(value, (list, np.ndarray)): return [_normalize_df_value(v) for v in value] if isinstance(value, dict): diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 52c621e402..6509eb447c 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -9,7 +9,7 @@ import shutil from datetime import datetime, timedelta -import numpy as np +import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 import pytest import pytz diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index f2998080b3..525736dd8e 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -8,7 +8,7 @@ from unittest.mock import patch import os -import numpy as np +import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 import pytest from pathlib import Path diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index 76725d46eb..19e258edc4 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -12,7 +12,7 @@ from sqlmesh.core.config import AutoCategorizationMode, CategorizerConfig, DuckDBConnectionConfig from sqlmesh.core.model import SqlModel, load_sql_based_model from sqlmesh.core.table_diff import TableDiff -import numpy as np +import numpy as np # noqa: TID253 from sqlmesh.utils.errors import SQLMeshError pytestmark = pytest.mark.slow diff --git a/tests/utils/pandas.py b/tests/utils/pandas.py index 422374e0fc..b9451f4545 100644 --- a/tests/utils/pandas.py +++ b/tests/utils/pandas.py @@ -2,7 +2,7 @@ import typing as t -import numpy as np +import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 diff --git a/web/server/api/endpoints/table_diff.py b/web/server/api/endpoints/table_diff.py index 3439327102..6d11638b84 100644 --- a/web/server/api/endpoints/table_diff.py +++ b/web/server/api/endpoints/table_diff.py @@ -2,7 +2,6 @@ import typing as t -import numpy as np from fastapi import APIRouter, Depends from sqlglot import exp @@ -25,6 +24,8 @@ def get_table_diff( context: Context = Depends(get_loaded_context), ) -> t.Optional[TableDiff]: """Calculate differences between tables, taking into account schema and row level differences.""" + import numpy as np + table_diffs = context.table_diff( source=source, target=target, From 1735a20498b86aed720e05bd3b9aa07018f91477 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 4 Jun 2025 08:49:30 -0700 Subject: [PATCH 0340/1056] Fix: Support coercion of SQL literal expressions into literal types for macro arguments (#4643) --- sqlmesh/core/macros.py | 20 ++++++++++++++++++++ tests/core/test_macros.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index db530481cd..d573fdac31 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -1452,6 +1452,26 @@ def _coerce( if base is SQL and isinstance(expr, exp.Expression): return expr.sql(dialect) + if base is t.Literal: + if not isinstance(expr, (exp.Literal, exp.Boolean)): + raise SQLMeshError( + f"{base_err_msg} Coercion to {base} requires a literal expression." + ) + literal_type_args = t.get_args(typ) + try: + for literal_type_arg in literal_type_args: + expr_is_bool = isinstance(expr.this, bool) + literal_is_bool = isinstance(literal_type_arg, bool) + if (expr_is_bool and literal_is_bool and literal_type_arg == expr.this) or ( + not expr_is_bool + and not literal_is_bool + and str(literal_type_arg) == str(expr.this) + ): + return type(literal_type_arg)(expr.this) + except Exception: + raise SQLMeshError(base_err_msg) + raise SQLMeshError(base_err_msg) + if isinstance(expr, base): return expr if issubclass(base, exp.Expression): diff --git a/tests/core/test_macros.py b/tests/core/test_macros.py index 5b79d5ff90..7f4a417c46 100644 --- a/tests/core/test_macros.py +++ b/tests/core/test_macros.py @@ -96,6 +96,12 @@ def test_default_arg_coercion( def test_select_macro(evaluator): return "SELECT 1 AS col" + @macro() + def test_literal_type(evaluator, a: t.Literal["test_literal_a", "test_literal_b", 1, True]): + if isinstance(a, exp.Expression): + raise SQLMeshError("Coercion failed") + return f"'{a}'" + return MacroEvaluator( "hive", {"test": Executable(name="test", payload="def test(_):\n return 'test'")}, @@ -1087,3 +1093,33 @@ def test_macro_with_spaces(): ("d.@z", 'd.a."b c"'), ): assert evaluator.transform(parse_one(sql)).sql() == expected + + +def test_macro_coerce_literal_type(macro_evaluator): + expression = d.parse_one("@TEST_LITERAL_TYPE('test_literal_a')") + assert macro_evaluator.transform(expression).sql() == "'test_literal_a'" + + expression = d.parse_one("@TEST_LITERAL_TYPE('test_literal_b')") + assert macro_evaluator.transform(expression).sql() == "'test_literal_b'" + + expression = d.parse_one("@TEST_LITERAL_TYPE(1)") + assert macro_evaluator.transform(expression).sql() == "'1'" + + expression = d.parse_one("@TEST_LITERAL_TYPE(True)") + assert macro_evaluator.transform(expression).sql() == "'True'" + + expression = d.parse_one("@TEST_LITERAL_TYPE('test_literal_c')") + with pytest.raises(MacroEvalError, match=".*Coercion failed"): + macro_evaluator.transform(expression) + + expression = d.parse_one("@TEST_LITERAL_TYPE(2)") + with pytest.raises(MacroEvalError, match=".*Coercion failed"): + macro_evaluator.transform(expression) + + expression = d.parse_one("@TEST_LITERAL_TYPE(False)") + with pytest.raises(MacroEvalError, match=".*Coercion failed"): + macro_evaluator.transform(expression) + + expression = d.parse_one("@TEST_LITERAL_TYPE(1.0)") + with pytest.raises(MacroEvalError, match=".*Coercion failed"): + macro_evaluator.transform(expression) From 9584fda625833cf7c6d2943126d9316e77ab045e Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 4 Jun 2025 19:00:18 +0300 Subject: [PATCH 0341/1056] Chore: detect xdb diff syntax and raise early (#4654) --- docs/guides/tablediff.md | 6 +++--- sqlmesh/core/context.py | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/guides/tablediff.md b/docs/guides/tablediff.md index b9362c11f7..d4ed61e0a3 100644 --- a/docs/guides/tablediff.md +++ b/docs/guides/tablediff.md @@ -253,12 +253,12 @@ Then, specify each table's gateway in the `table_diff` command with this syntax: For example, we could diff the `landing.table` table across `bigquery` and `snowflake` gateways like this: ```sh -$ sqlmesh table_diff 'bigquery|landing.table:snowflake|lake.table' +$ tcloud sqlmesh table_diff 'bigquery|landing.table:snowflake|lake.table' ``` This syntax tells SQLMesh to use the cross-database diffing algorithm instead of the normal within-database diffing algorithm. -After adding gateways to the table names, use `table_diff` as described above - the same options apply for specifying the join keys, decimal precision, etc. See `sqlmesh table_diff --help` for a [full list of options](../reference/cli.md#table_diff). +After adding gateways to the table names, use `table_diff` as described above - the same options apply for specifying the join keys, decimal precision, etc. See `tcloud sqlmesh table_diff --help` for a [full list of options](../reference/cli.md#table_diff). !!! warning @@ -273,7 +273,7 @@ A cross-database diff is broken up into two stages. The first stage is a schema diff. This example shows that differences in column name case across the two tables are identified as schema differences: ```bash -$ sqlmesh table_diff 'bigquery|sqlmesh_example.full_model:snowflake|sqlmesh_example.full_model' --on item_id --show-sample +$ tcloud sqlmesh table_diff 'bigquery|sqlmesh_example.full_model:snowflake|sqlmesh_example.full_model' --on item_id --show-sample Schema Diff Between 'BIGQUERY|SQLMESH_EXAMPLE.FULL_MODEL' and 'SNOWFLAKE|SQLMESH_EXAMPLE.FULL_MODEL': ├── Added Columns: diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 1bc1276710..3dc77ea2fc 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1677,6 +1677,12 @@ def table_diff( The list of TableDiff objects containing schema and summary differences. """ + if "|" in source or "|" in target: + raise ConfigError( + "Cross-database table diffing is available in Tobiko Cloud. Read more here: " + "https://sqlmesh.readthedocs.io/en/stable/guides/tablediff/#diffing-tables-or-views-across-gateways" + ) + table_diffs: t.List[TableDiff] = [] # Diffs multiple or a single model across two environments From b9c9a57eef9ddc4fcfef3c36182ac81c5adf74a6 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 4 Jun 2025 09:02:28 -0700 Subject: [PATCH 0342/1056] feat: lazy import duckdb (#4655) --- pyproject.toml | 5 +++-- sqlmesh/core/engine_adapter/duckdb.py | 11 ++--------- tests/conftest.py | 2 +- tests/core/state_sync/test_state_sync.py | 2 +- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c2464ccfb8..6054adc0d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "astor", "click", "croniter", - "duckdb!=0.10.3", + "duckdb>=0.10.0,!=0.10.3", "dateparser", "hyperscript>=0.1.0", "importlib-metadata; python_version<'3.12'", @@ -275,6 +275,7 @@ extend-select = ["TID"] [tool.ruff.lint.flake8-tidy-imports] banned-module-level-imports = [ - "pandas", + "duckdb", "numpy", + "pandas", ] diff --git a/sqlmesh/core/engine_adapter/duckdb.py b/sqlmesh/core/engine_adapter/duckdb.py index f316c2361d..f7f72f9692 100644 --- a/sqlmesh/core/engine_adapter/duckdb.py +++ b/sqlmesh/core/engine_adapter/duckdb.py @@ -1,7 +1,6 @@ from __future__ import annotations import typing as t -from duckdb import __version__ as duckdb_version from sqlglot import exp from sqlmesh.core.engine_adapter.mixins import ( @@ -18,7 +17,6 @@ SourceQuery, set_catalog, ) -from sqlmesh.utils import major_minor from sqlmesh.core.schema_diff import SchemaDiffer if t.TYPE_CHECKING: @@ -35,13 +33,8 @@ class DuckDBEngineAdapter(LogicalMergeMixin, GetCurrentCatalogFromFunctionMixin, exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(18, 3), (0,)], }, ) - - # TODO: remove once we stop supporting DuckDB 0.9 - COMMENT_CREATION_TABLE, COMMENT_CREATION_VIEW = ( - (CommentCreationTable.UNSUPPORTED, CommentCreationView.UNSUPPORTED) - if major_minor(duckdb_version) < (0, 10) - else (CommentCreationTable.COMMENT_COMMAND_ONLY, CommentCreationView.COMMENT_COMMAND_ONLY) - ) + COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY + COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY @property def catalog_support(self) -> CatalogSupport: diff --git a/tests/conftest.py b/tests/conftest.py index e0066ec5a8..54f8db0418 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ import os import shutil -import duckdb +import duckdb # noqa: TID253 import pandas as pd # noqa: TID253 import pytest from pytest_mock.plugin import MockerFixture diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index 10ab17a0d3..173264f9b5 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -4,7 +4,7 @@ import typing as t from unittest.mock import call, patch -import duckdb +import duckdb # noqa: TID253 import pandas as pd # noqa: TID253 import pytest import time_machine From 8cb48d2d86ca6af3da90a3a951f49f2d706bfb32 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 4 Jun 2025 10:18:19 -0700 Subject: [PATCH 0343/1056] Feat: Introduce plan explain mode (#4642) --- docs/reference/cli.md | 11 +- sqlmesh/cli/main.py | 8 +- sqlmesh/core/context.py | 20 +- sqlmesh/core/environment.py | 13 + sqlmesh/core/plan/__init__.py | 1 + sqlmesh/core/plan/builder.py | 4 + sqlmesh/core/plan/definition.py | 1 + sqlmesh/core/plan/evaluator.py | 96 ++-- sqlmesh/core/plan/explainer.py | 281 +++++++++ sqlmesh/core/plan/stages.py | 303 ++++++++++ sqlmesh/core/scheduler.py | 60 +- sqlmesh/core/state_sync/db/facade.py | 9 +- tests/core/test_plan.py | 1 + tests/core/test_plan_stages.py | 814 +++++++++++++++++++++++++++ 14 files changed, 1556 insertions(+), 66 deletions(-) create mode 100644 sqlmesh/core/plan/explainer.py create mode 100644 sqlmesh/core/plan/stages.py create mode 100644 tests/core/test_plan_stages.py diff --git a/docs/reference/cli.md b/docs/reference/cli.md index fe22a98360..34d543a5a0 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -343,6 +343,8 @@ Options: Default: prod. --skip-tests Skip tests prior to generating the plan if they are defined. + --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 downstream from the one specified. For production environment, all related model @@ -383,9 +385,12 @@ Options: application (prod environment only). --enable-preview Enable preview for forward-only models when targeting a development environment. - --diff-rendered Output text differences for rendered versions - of models and standalone audits - -v, --verbose Verbose output. + --diff-rendered Output text differences for the rendered + versions of the models and standalone + audits. + --explain Explain the plan instead of applying it. + -v, --verbose Verbose output. Use -vv for very verbose + output. --help Show this message and exit. ``` diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 2ef9e25426..a171928b4e 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -457,7 +457,13 @@ def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None: @click.option( "--diff-rendered", is_flag=True, - help="Output text differences for the rendered versions of the models and standalone audits", + help="Output text differences for the rendered versions of the models and standalone audits.", + default=None, +) +@click.option( + "--explain", + is_flag=True, + help="Explain the plan instead of applying it.", default=None, ) @opt.verbose diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 3dc77ea2fc..45682b0209 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -88,7 +88,7 @@ NotificationTarget, NotificationTargetManager, ) -from sqlmesh.core.plan import Plan, PlanBuilder, SnapshotIntervals +from sqlmesh.core.plan import Plan, PlanBuilder, SnapshotIntervals, PlanExplainer from sqlmesh.core.plan.definition import UserProvidedFlags from sqlmesh.core.reference import ReferenceGraph from sqlmesh.core.scheduler import Scheduler, CompletionStatus @@ -1211,6 +1211,7 @@ def plan( run: t.Optional[bool] = None, diff_rendered: t.Optional[bool] = None, skip_linter: t.Optional[bool] = None, + explain: t.Optional[bool] = None, ) -> Plan: """Interactively creates a plan. @@ -1256,6 +1257,7 @@ def plan( run: Whether to run latest intervals as part of the plan application. diff_rendered: Whether the diff should compare raw vs rendered models skip_linter: Linter runs by default so this will skip it if enabled + explain: Whether to explain the plan instead of applying it. Returns: The populated Plan object. @@ -1283,6 +1285,7 @@ def plan( run=run, diff_rendered=diff_rendered, skip_linter=skip_linter, + explain=explain, ) plan = plan_builder.build() @@ -1292,6 +1295,9 @@ def plan( # or if there are any uncategorized snapshots in the plan no_prompts = False + if explain: + auto_apply = True + self.console.plan( plan_builder, auto_apply if auto_apply is not None else self.config.plan.auto_apply, @@ -1328,6 +1334,7 @@ def plan_builder( run: t.Optional[bool] = None, diff_rendered: t.Optional[bool] = None, skip_linter: t.Optional[bool] = None, + explain: t.Optional[bool] = None, ) -> PlanBuilder: """Creates a plan builder. @@ -1544,6 +1551,7 @@ def plan_builder( interval_end_per_model=max_interval_end_per_model, console=self.console, user_provided_flags=user_provided_flags, + explain=explain or False, ) def apply( @@ -1568,6 +1576,16 @@ def apply( return if plan.uncategorized: raise UncategorizedPlanError("Can't apply a plan with uncategorized changes.") + + if plan.explain: + explainer = PlanExplainer( + state_reader=self.state_reader, + default_catalog=self.default_catalog, + console=self.console, + ) + explainer.evaluate(plan.to_evaluatable()) + return + self.notification_target_manager.notify( NotificationEvent.APPLY_START, environment=plan.environment_naming_info.name, diff --git a/sqlmesh/core/environment.py b/sqlmesh/core/environment.py index 36bb901883..891d299df8 100644 --- a/sqlmesh/core/environment.py +++ b/sqlmesh/core/environment.py @@ -229,6 +229,19 @@ def summary(self) -> EnvironmentSummary: finalized_ts=self.finalized_ts, ) + def can_partially_promote(self, existing_environment: Environment) -> bool: + """Returns True if the existing environment can be partially promoted to the current environment. + + Partial promotion means that we don't need to re-create views for snapshots that are already promoted in the + target environment. + """ + return ( + bool(existing_environment.finalized_ts) + and not existing_environment.expired + and existing_environment.gateway_managed == self.gateway_managed + and existing_environment.name == c.PROD + ) + def _convert_list_to_models_and_store( self, field: str, type_: t.Type[PydanticType] ) -> t.Optional[t.List[PydanticType]]: diff --git a/sqlmesh/core/plan/__init__.py b/sqlmesh/core/plan/__init__.py index c918c30554..903e2e43ba 100644 --- a/sqlmesh/core/plan/__init__.py +++ b/sqlmesh/core/plan/__init__.py @@ -10,3 +10,4 @@ PlanEvaluator as PlanEvaluator, update_intervals_for_new_snapshots as update_intervals_for_new_snapshots, ) +from sqlmesh.core.plan.explainer import PlanExplainer as PlanExplainer diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 2c0ff96a25..27b81f5d74 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -83,6 +83,7 @@ class PlanBuilder: environment state, or to use whatever snapshots are in the current environment state even if the environment is not finalized. interval_end_per_model: The mapping from model FQNs to target end dates. + explain: Whether to explain the plan instead of applying it. """ def __init__( @@ -112,6 +113,7 @@ def __init__( enable_preview: bool = False, end_bounded: bool = False, ensure_finalized_snapshots: bool = False, + explain: bool = False, interval_end_per_model: t.Optional[t.Dict[str, int]] = None, console: t.Optional[PlanBuilderConsole] = None, user_provided_flags: t.Optional[t.Dict[str, UserProvidedFlags]] = None, @@ -142,6 +144,7 @@ def __init__( self._console = console or get_console() self._choices: t.Dict[SnapshotId, SnapshotChangeCategory] = {} self._user_provided_flags = user_provided_flags + self._explain = explain self._start = start if not self._start and ( @@ -273,6 +276,7 @@ def build(self) -> Plan: empty_backfill=self._empty_backfill, no_gaps=self._no_gaps, forward_only=self._forward_only, + explain=self._explain, allow_destructive_models=t.cast(t.Set, self._allow_destructive_models), include_unmodified=self._include_unmodified, environment_ttl=self._environment_ttl, diff --git a/sqlmesh/core/plan/definition.py b/sqlmesh/core/plan/definition.py index 0a0e3ee739..c0ef1323f2 100644 --- a/sqlmesh/core/plan/definition.py +++ b/sqlmesh/core/plan/definition.py @@ -46,6 +46,7 @@ class Plan(PydanticModel, frozen=True): include_unmodified: bool end_bounded: bool ensure_finalized_snapshots: bool + explain: bool environment_ttl: t.Optional[str] = None environment_naming_info: EnvironmentNamingInfo diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 0d2a25b4fa..c2e8846a9c 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -36,7 +36,7 @@ SnapshotCreationFailedError, ) from sqlmesh.utils import CompletionStatus -from sqlmesh.core.state_sync import StateSync +from sqlmesh.core.state_sync import StateSync, StateReader from sqlmesh.core.state_sync.base import PromotionResult from sqlmesh.utils.concurrency import NodeExecutionFailedError from sqlmesh.utils.errors import PlanError @@ -284,23 +284,7 @@ def _push( new_snapshots=plan.new_snapshots, plan_id=plan.plan_id ) - promoted_snapshot_ids = ( - set(plan.environment.promoted_snapshot_ids) - if plan.environment.promoted_snapshot_ids is not None - else None - ) - - def _should_create(s: Snapshot) -> bool: - if not s.is_model or s.is_symbolic: - return False - # Only create tables for snapshots that we're planning to promote or that were selected for backfill - return ( - plan.is_selected_for_backfill(s.name) - or promoted_snapshot_ids is None - or s.snapshot_id in promoted_snapshot_ids - ) - - snapshots_to_create = [s for s in snapshots.values() if _should_create(s)] + snapshots_to_create = get_snapshots_to_create(plan, snapshots) completion_status = None progress_stopped = False @@ -573,32 +557,7 @@ def _run_audits_for_metadata_snapshots( plan: EvaluatablePlan, new_snapshots: t.Dict[SnapshotId, Snapshot], ) -> None: - # Filter out snapshots that are not categorized as metadata changes on models - metadata_snapshots = [] - for snapshot in new_snapshots.values(): - if not snapshot.is_metadata or not snapshot.is_model or not snapshot.evaluatable: - continue - - metadata_snapshots.append(snapshot) - - # Bulk load all the previous snapshots - previous_snapshots = self.state_sync.get_snapshots( - [ - s.previous_version.snapshot_id(s.name) - for s in metadata_snapshots - if s.previous_version - ] - ).values() - - # Check if any of the snapshots have modifications to the audits field by comparing the hashes - audit_snapshots = {} - for snapshot, previous_snapshot in zip(metadata_snapshots, previous_snapshots): - new_audits_hash = snapshot.model.audit_metadata_hash() - previous_audit_hash = previous_snapshot.model.audit_metadata_hash() - - if snapshot.model.audits and previous_audit_hash != new_audits_hash: - audit_snapshots[snapshot.snapshot_id] = snapshot - + audit_snapshots = get_audit_only_snapshots(new_snapshots, self.state_sync) if not audit_snapshots: return @@ -636,3 +595,52 @@ def update_intervals_for_new_snapshots( if snapshots_intervals: state_sync.add_snapshots_intervals(snapshots_intervals) + + +def get_audit_only_snapshots( + new_snapshots: t.Dict[SnapshotId, Snapshot], state_reader: StateReader +) -> t.Dict[SnapshotId, Snapshot]: + metadata_snapshots = [] + for snapshot in new_snapshots.values(): + if not snapshot.is_metadata or not snapshot.is_model or not snapshot.evaluatable: + continue + + metadata_snapshots.append(snapshot) + + # Bulk load all the previous snapshots + previous_snapshots = state_reader.get_snapshots( + [s.previous_version.snapshot_id(s.name) for s in metadata_snapshots if s.previous_version] + ).values() + + # Check if any of the snapshots have modifications to the audits field by comparing the hashes + audit_snapshots = {} + for snapshot, previous_snapshot in zip(metadata_snapshots, previous_snapshots): + new_audits_hash = snapshot.model.audit_metadata_hash() + previous_audit_hash = previous_snapshot.model.audit_metadata_hash() + + if snapshot.model.audits and previous_audit_hash != new_audits_hash: + audit_snapshots[snapshot.snapshot_id] = snapshot + + return audit_snapshots + + +def get_snapshots_to_create( + plan: EvaluatablePlan, snapshots: t.Dict[SnapshotId, Snapshot] +) -> t.List[Snapshot]: + promoted_snapshot_ids = ( + set(plan.environment.promoted_snapshot_ids) + if plan.environment.promoted_snapshot_ids is not None + else None + ) + + def _should_create(s: Snapshot) -> bool: + if not s.is_model or s.is_symbolic: + return False + # Only create tables for snapshots that we're planning to promote or that were selected for backfill + return ( + plan.is_selected_for_backfill(s.name) + or promoted_snapshot_ids is None + or s.snapshot_id in promoted_snapshot_ids + ) + + return [s for s in snapshots.values() if _should_create(s)] diff --git a/sqlmesh/core/plan/explainer.py b/sqlmesh/core/plan/explainer.py new file mode 100644 index 0000000000..7001c0cc0f --- /dev/null +++ b/sqlmesh/core/plan/explainer.py @@ -0,0 +1,281 @@ +import abc +import typing as t +import logging + +from rich.console import Console as RichConsole +from rich.tree import Tree +from sqlglot.dialects.dialect import DialectType +from sqlmesh.core import constants as c +from sqlmesh.core.console import Console, TerminalConsole, get_console +from sqlmesh.core.environment import EnvironmentNamingInfo +from sqlmesh.core.plan.definition import EvaluatablePlan, SnapshotIntervals +from sqlmesh.core.plan import stages +from sqlmesh.core.plan.evaluator import ( + PlanEvaluator, +) +from sqlmesh.core.state_sync import StateReader +from sqlmesh.core.snapshot.definition import ( + SnapshotInfoMixin, +) +from sqlmesh.utils import Verbosity, rich as srich +from sqlmesh.utils.date import to_ts +from sqlmesh.utils.errors import SQLMeshError + + +logger = logging.getLogger(__name__) + + +class PlanExplainer(PlanEvaluator): + def __init__( + self, + state_reader: StateReader, + default_catalog: t.Optional[str], + console: t.Optional[Console] = None, + ): + self.state_reader = state_reader + self.default_catalog = default_catalog + self.console = console or get_console() + + def evaluate( + self, plan: EvaluatablePlan, circuit_breaker: t.Optional[t.Callable[[], bool]] = None + ) -> None: + plan_stages = stages.build_plan_stages(plan, self.state_reader, self.default_catalog) + explainer_console = _get_explainer_console( + self.console, plan.environment, self.default_catalog + ) + explainer_console.explain(plan_stages) + + +class ExplainerConsole(abc.ABC): + @abc.abstractmethod + def explain(self, stages: t.List[stages.PlanStage]) -> None: + pass + + +MAX_TREE_LENGTH = 10 + + +class RichExplainerConsole(ExplainerConsole): + def __init__( + self, + environment_naming_info: EnvironmentNamingInfo, + dialect: DialectType, + default_catalog: t.Optional[str], + verbosity: Verbosity = Verbosity.DEFAULT, + console: t.Optional[RichConsole] = None, + ): + self.environment_naming_info = environment_naming_info + self.dialect = dialect + self.default_catalog = default_catalog + self.verbosity = verbosity + self.console: RichConsole = console or srich.console + + def explain(self, stages: t.List[stages.PlanStage]) -> None: + tree = Tree("[bold]Explained plan[/bold]") + for stage in stages: + handler_name = f"visit_{_to_snake_case(stage.__class__.__name__)}" + if not hasattr(self, handler_name): + logger.error("Unexpected stage: %s", stage.__class__.__name__) + continue + handler = getattr(self, handler_name) + result = handler(stage) + if result: + tree.add(self._limit_tree(result)) + self.console.print(tree) + + def visit_before_all_stage(self, stage: stages.BeforeAllStage) -> Tree: + return Tree("[bold]Execute before all statements[/bold]") + + def visit_after_all_stage(self, stage: stages.AfterAllStage) -> Tree: + return Tree("[bold]Execute after all statements[/bold]") + + def visit_physical_layer_update_stage(self, stage: stages.PhysicalLayerUpdateStage) -> Tree: + if not stage.snapshots: + return Tree("[bold]SKIP: No physical layer updates to perform[/bold]") + + tree = Tree( + "[bold]Validate SQL and create physical layer tables and views if they do not exist[/bold]" + ) + for snapshot in stage.snapshots: + is_deployable = ( + stage.deployability_index.is_deployable(snapshot) + if self.environment_naming_info.name != c.PROD + else True + ) + display_name = self._display_name(snapshot) + table_name = snapshot.table_name(is_deployable) + model_tree = Tree(f"{display_name} -> {table_name}") + + if snapshot.is_model: + if snapshot.model.pre_statements: + model_tree.add("Run pre-statements") + if snapshot.model.annotated: + model_tree.add("Dry run model query without inserting results") + + if snapshot.is_view: + create_tree = Tree("Create view if it doesn't exist") + elif snapshot.is_forward_only and snapshot.previous_versions: + prod_table = snapshot.table_name(True) + create_tree = Tree( + f"Clone {prod_table} into {table_name} and then update its schema if it doesn't exist" + ) + else: + create_tree = Tree("Create table if it doesn't exist") + + if not is_deployable: + create_tree.add("[orange1]preview[/orange1]: data will NOT be reused in production") + model_tree.add(create_tree) + + if snapshot.is_model and snapshot.model.post_statements: + model_tree.add("Run post-statements") + + tree.add(model_tree) + return tree + + def visit_audit_only_run_stage(self, stage: stages.AuditOnlyRunStage) -> Tree: + tree = Tree("[bold]Audit-only execution[/bold]") + for snapshot in stage.snapshots: + display_name = self._display_name(snapshot) + tree.add(display_name) + return tree + + def visit_restatement_stage(self, stage: stages.RestatementStage) -> Tree: + tree = Tree("[bold]Invalidate data intervals as part of restatement[/bold]") + for snapshot_table_info, interval in stage.snapshot_intervals.items(): + display_name = self._display_name(snapshot_table_info) + tree.add(f"{display_name} [{to_ts(interval[0])} - {to_ts(interval[1])}]") + return tree + + def visit_backfill_stage(self, stage: stages.BackfillStage) -> Tree: + if not stage.snapshot_to_intervals: + return Tree("[bold]SKIP: No model batches to execute[/bold]") + + tree = Tree( + "[bold]Backfill models by running their queries and run standalone audits[/bold]" + ) + for snapshot, intervals in stage.snapshot_to_intervals.items(): + display_name = self._display_name(snapshot) + if snapshot.is_model: + table_name = snapshot.table_name(stage.deployability_index.is_deployable(snapshot)) + model_tree = Tree(f"{display_name} -> {table_name}") + + for signal_name, _ in snapshot.model.signals: + model_tree.add(f"Check '{signal_name}' signal") + + if snapshot.model.pre_statements: + model_tree.add("Run pre-statements") + + if snapshot.is_incremental: + current_intervals = ( + snapshot.intervals + if stage.deployability_index.is_deployable(snapshot) + else snapshot.dev_intervals + ) + if current_intervals: + formatted_range = SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, intervals=intervals + ).format_intervals(snapshot.node.interval_unit) + model_tree.add( + f"Incrementally insert records within the range [{formatted_range}]" + ) + else: + # If there are no intervals, the table will be fully refreshed + model_tree.add("Fully refresh table") + elif snapshot.is_view: + model_tree.add("Recreate view") + else: + model_tree.add("Fully refresh table") + + if snapshot.model.post_statements: + model_tree.add("Run post-statements") + + if snapshot.model.audits: + for audit_name, _ in snapshot.model.audits: + model_tree.add(f"Run '{audit_name}' audit") + + tree.add(model_tree) + else: + tree.add(f"{display_name} \[standalone audit]") + return tree + + def visit_migrate_schemas_stage(self, stage: stages.MigrateSchemasStage) -> Tree: + tree = Tree( + "[bold]Update schemas (add, drop, alter columns) of production physical tables to reflect forward-only changes[/bold]" + ) + for snapshot in stage.snapshots: + display_name = self._display_name(snapshot) + table_name = snapshot.table_name(True) + tree.add(f"{display_name} -> {table_name}") + return tree + + def visit_virtual_layer_update_stage(self, stage: stages.VirtualLayerUpdateStage) -> Tree: + tree = Tree( + f"[bold]Update the virtual layer for environment '{self.environment_naming_info.name}'[/bold]" + ) + promote_tree = Tree( + "[bold]Create or update views in the virtual layer to point at new physical tables and views[/bold]" + ) + for snapshot in stage.promoted_snapshots: + display_name = self._display_name(snapshot) + table_name = snapshot.table_name(stage.deployability_index.is_representative(snapshot)) + promote_tree.add(f"{display_name} -> {table_name}") + + demote_tree = Tree( + "[bold]Delete views in the virtual layer for models that were removed[/bold]" + ) + for snapshot in stage.demoted_snapshots: + display_name = self._display_name(snapshot) + demote_tree.add(display_name) + + if stage.promoted_snapshots: + tree.add(self._limit_tree(promote_tree)) + if stage.demoted_snapshots: + tree.add(self._limit_tree(demote_tree)) + return tree + + def visit_environment_record_update_stage( + self, stage: stages.EnvironmentRecordUpdateStage + ) -> t.Optional[Tree]: + return None + + def _display_name(self, snapshot: SnapshotInfoMixin) -> str: + return snapshot.display_name( + self.environment_naming_info, + self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, + dialect=self.dialect, + ) + + def _limit_tree(self, tree: Tree) -> Tree: + tree_length = len(tree.children) + if tree_length <= MAX_TREE_LENGTH: + return tree + if self.verbosity < Verbosity.VERY_VERBOSE: + tree.children = [ + tree.children[0], + Tree(f".... {tree_length - 2} more ...."), + tree.children[-1], + ] + return tree + + +def _get_explainer_console( + console: t.Optional[Console], + environment_naming_info: EnvironmentNamingInfo, + default_catalog: t.Optional[str], +) -> ExplainerConsole: + console = console or get_console() + if not isinstance(console, TerminalConsole): + raise SQLMeshError("Plain explaination is only supported in the terminal.") + return RichExplainerConsole( + environment_naming_info=environment_naming_info, + dialect=console.dialect, + default_catalog=default_catalog, + verbosity=console.verbosity, + console=console.console, + ) + + +def _to_snake_case(name: str) -> str: + return "".join( + f"_{c.lower()}" if c.isupper() and idx != 0 else c.lower() for idx, c in enumerate(name) + ) diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py new file mode 100644 index 0000000000..964dadb143 --- /dev/null +++ b/sqlmesh/core/plan/stages.py @@ -0,0 +1,303 @@ +import typing as t + +from dataclasses import dataclass +from sqlmesh.core.plan.definition import EvaluatablePlan +from sqlmesh.core.plan.evaluator import ( + get_audit_only_snapshots, + get_snapshots_to_create, +) +from sqlmesh.core.state_sync import StateReader +from sqlmesh.core.scheduler import merged_missing_intervals, SnapshotToIntervals +from sqlmesh.core.snapshot.definition import ( + DeployabilityIndex, + Snapshot, + SnapshotTableInfo, + SnapshotId, + Interval, +) + + +@dataclass +class BeforeAllStage: + statements: t.List[str] + + +@dataclass +class AfterAllStage: + statements: t.List[str] + + +@dataclass +class PhysicalLayerUpdateStage: + snapshots: t.List[Snapshot] + deployability_index: DeployabilityIndex + + +@dataclass +class AuditOnlyRunStage: + snapshots: t.List[Snapshot] + + +@dataclass +class RestatementStage: + snapshot_intervals: t.Dict[SnapshotTableInfo, Interval] + + +@dataclass +class BackfillStage: + snapshot_to_intervals: SnapshotToIntervals + deployability_index: DeployabilityIndex + before_promote: bool = True + + +@dataclass +class MigrateSchemasStage: + snapshots: t.List[Snapshot] + + +@dataclass +class VirtualLayerUpdateStage: + promoted_snapshots: t.Set[SnapshotTableInfo] + demoted_snapshots: t.Set[SnapshotTableInfo] + deployability_index: DeployabilityIndex + + +@dataclass +class EnvironmentRecordUpdateStage: + pass + + +PlanStage = t.Union[ + BeforeAllStage, + AfterAllStage, + PhysicalLayerUpdateStage, + AuditOnlyRunStage, + RestatementStage, + BackfillStage, + MigrateSchemasStage, + VirtualLayerUpdateStage, + EnvironmentRecordUpdateStage, +] + + +class PlanStagesBuilder: + def __init__( + self, + state_reader: StateReader, + default_catalog: t.Optional[str], + ): + self.state_reader = state_reader + self.default_catalog = default_catalog + + def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: + new_snapshots = {s.snapshot_id: s for s in plan.new_snapshots} + stored_snapshots = self.state_reader.get_snapshots(plan.environment.snapshots) + snapshots = {**new_snapshots, **stored_snapshots} + snapshots_by_name = {s.name: s for s in snapshots.values()} + + all_selected_for_backfill_snapshots = { + s.snapshot_id for s in snapshots.values() if plan.is_selected_for_backfill(s.name) + } + + deployability_index = DeployabilityIndex.create(snapshots, start=plan.start) + deployability_index_for_creation = deployability_index + if plan.is_dev: + before_promote_snapshots = all_selected_for_backfill_snapshots + after_promote_snapshots = set() + snapshots_with_schema_migration = [] + else: + before_promote_snapshots = { + s.snapshot_id + for s in snapshots.values() + if deployability_index.is_representative(s) + and plan.is_selected_for_backfill(s.name) + } + after_promote_snapshots = all_selected_for_backfill_snapshots - before_promote_snapshots + deployability_index = DeployabilityIndex.all_deployable() + + snapshots_with_schema_migration = [ + s + for s in snapshots.values() + if s.is_paused + and s.is_materialized + and not deployability_index_for_creation.is_representative(s) + ] + + snapshots_to_intervals = self._missing_intervals( + plan, snapshots_by_name, deployability_index + ) + needs_backfill = ( + not plan.empty_backfill and not plan.skip_backfill and bool(snapshots_to_intervals) + ) + missing_intervals_before_promote: SnapshotToIntervals = {} + missing_intervals_after_promote: SnapshotToIntervals = {} + if needs_backfill: + for snapshot, intervals in snapshots_to_intervals.items(): + if snapshot.snapshot_id in before_promote_snapshots: + missing_intervals_before_promote[snapshot] = intervals + elif snapshot.snapshot_id in after_promote_snapshots: + missing_intervals_after_promote[snapshot] = intervals + + stages: t.List[PlanStage] = [] + + before_all_stage = self._get_before_all_stage(plan) + if before_all_stage: + stages.append(before_all_stage) + + stages.append( + self._get_physical_layer_update_stage( + plan, snapshots, snapshots_to_intervals, deployability_index_for_creation + ) + ) + + audit_only_snapshots = get_audit_only_snapshots(new_snapshots, self.state_reader) + if audit_only_snapshots: + stages.append(AuditOnlyRunStage(snapshots=list(audit_only_snapshots.values()))) + + restatement_stage = self._get_restatement_stage(plan, snapshots_by_name) + if restatement_stage: + stages.append(restatement_stage) + + if missing_intervals_before_promote: + stages.append( + BackfillStage( + snapshot_to_intervals=missing_intervals_before_promote, + deployability_index=deployability_index, + ) + ) + elif not needs_backfill: + # Append an empty backfill stage so that explainer can show that the stage is skipped + stages.append( + BackfillStage(snapshot_to_intervals={}, deployability_index=deployability_index) + ) + + stages.append(EnvironmentRecordUpdateStage()) + + if snapshots_with_schema_migration: + stages.append(MigrateSchemasStage(snapshots=snapshots_with_schema_migration)) + + if missing_intervals_after_promote: + stages.append( + BackfillStage( + snapshot_to_intervals=missing_intervals_after_promote, + deployability_index=deployability_index, + ) + ) + + virtual_layer_update_stage = self._get_virtual_layer_update_stage(plan, deployability_index) + if virtual_layer_update_stage: + stages.append(virtual_layer_update_stage) + + after_all_stage = self._get_after_all_stage(plan) + if after_all_stage: + stages.append(after_all_stage) + + return stages + + def _get_before_all_stage(self, plan: EvaluatablePlan) -> t.Optional[BeforeAllStage]: + before_all = [ + statement + for environment_statements in plan.environment_statements or [] + for statement in environment_statements.before_all + ] + return BeforeAllStage(statements=before_all) if before_all else None + + def _get_after_all_stage(self, plan: EvaluatablePlan) -> t.Optional[AfterAllStage]: + after_all = [ + statement + for environment_statements in plan.environment_statements or [] + for statement in environment_statements.after_all + ] + return AfterAllStage(statements=after_all) if after_all else None + + def _get_restatement_stage( + self, plan: EvaluatablePlan, snapshots_by_name: t.Dict[str, Snapshot] + ) -> t.Optional[RestatementStage]: + snapshot_intervals_to_restate = {} + for name, interval in plan.restatements.items(): + restated_snapshot = snapshots_by_name[name] + restated_snapshot.remove_interval(interval) + snapshot_intervals_to_restate[restated_snapshot.table_info] = interval + if not snapshot_intervals_to_restate or plan.is_dev: + return None + return RestatementStage(snapshot_intervals=snapshot_intervals_to_restate) + + def _get_physical_layer_update_stage( + self, + plan: EvaluatablePlan, + snapshots: t.Dict[SnapshotId, Snapshot], + snapshots_to_intervals: SnapshotToIntervals, + deployability_index: DeployabilityIndex, + ) -> PhysicalLayerUpdateStage: + snapshots_to_create = [ + s + for s in get_snapshots_to_create(plan, snapshots) + if s in snapshots_to_intervals and s.is_model and not s.is_symbolic + ] + return PhysicalLayerUpdateStage( + snapshots=snapshots_to_create, + deployability_index=deployability_index, + ) + + def _get_virtual_layer_update_stage( + self, plan: EvaluatablePlan, deployability_index: DeployabilityIndex + ) -> t.Optional[VirtualLayerUpdateStage]: + promoted_snapshots, demoted_snapshots = self._get_promoted_demoted_snapshots(plan) + if not promoted_snapshots and not demoted_snapshots: + return None + return VirtualLayerUpdateStage( + promoted_snapshots=promoted_snapshots, + demoted_snapshots=demoted_snapshots, + deployability_index=deployability_index, + ) + + def _get_promoted_demoted_snapshots( + self, plan: EvaluatablePlan + ) -> t.Tuple[t.Set[SnapshotTableInfo], t.Set[SnapshotTableInfo]]: + existing_environment = self.state_reader.get_environment(plan.environment.name) + if existing_environment: + snapshots_by_name = {s.name: s for s in existing_environment.snapshots} + demoted_snapshot_names = {s.name for s in existing_environment.promoted_snapshots} - { + s.name for s in plan.environment.promoted_snapshots + } + demoted_snapshots = {snapshots_by_name[name] for name in demoted_snapshot_names} + else: + demoted_snapshots = set() + promoted_snapshots = set(plan.environment.promoted_snapshots) + if existing_environment and plan.environment.can_partially_promote(existing_environment): + promoted_snapshots -= set(existing_environment.promoted_snapshots) + + def _snapshot_filter(snapshot: SnapshotTableInfo) -> bool: + return snapshot.is_model and not snapshot.is_symbolic + + return {s for s in promoted_snapshots if _snapshot_filter(s)}, { + s for s in demoted_snapshots if _snapshot_filter(s) + } + + def _missing_intervals( + self, + plan: EvaluatablePlan, + snapshots_by_name: t.Dict[str, Snapshot], + deployability_index: DeployabilityIndex, + ) -> SnapshotToIntervals: + return merged_missing_intervals( + snapshots=snapshots_by_name.values(), + start=plan.start, + end=plan.end, + execution_time=plan.execution_time, + restatements={ + snapshots_by_name[name].snapshot_id: interval + for name, interval in plan.restatements.items() + }, + deployability_index=deployability_index, + end_bounded=plan.end_bounded, + interval_end_per_model=plan.interval_end_per_model, + ) + + +def build_plan_stages( + plan: EvaluatablePlan, + state_reader: StateReader, + default_catalog: t.Optional[str], +) -> t.List[PlanStage]: + return PlanStagesBuilder(state_reader, default_catalog).build(plan) diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index a2bc3e3d2b..a66f7c19fe 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -118,16 +118,12 @@ def merged_missing_intervals( allow_partials, and other attributes that could cause the intervals to exceed the target end date. selected_snapshots: A set of snapshot names to run. If not provided, all snapshots will be run. """ - restatements = restatements or {} - validate_date_range(start, end) - - snapshots: t.Collection[Snapshot] = self.snapshot_per_version.values() - snapshots_to_intervals = compute_interval_params( - snapshots, - start=start or earliest_start_date(snapshots), - end=end or now_timestamp(), + snapshots_to_intervals = merged_missing_intervals( + snapshots=self.snapshot_per_version.values(), + start=start, + end=end, + execution_time=execution_time, deployability_index=deployability_index, - execution_time=execution_time or now_timestamp(), restatements=restatements, interval_end_per_model=interval_end_per_model, ignore_cron=ignore_cron, @@ -700,6 +696,52 @@ def _audit_snapshot( return audit_results +def merged_missing_intervals( + snapshots: t.Collection[Snapshot], + start: t.Optional[TimeLike] = None, + end: t.Optional[TimeLike] = None, + execution_time: t.Optional[TimeLike] = None, + deployability_index: t.Optional[DeployabilityIndex] = None, + restatements: t.Optional[t.Dict[SnapshotId, Interval]] = None, + interval_end_per_model: t.Optional[t.Dict[str, int]] = None, + ignore_cron: bool = False, + end_bounded: bool = False, +) -> SnapshotToIntervals: + """Find the largest contiguous date interval parameters based only on what is missing. + + For each node name, find all dependencies and look for a stored snapshot from the metastore. If a snapshot is found, + calculate the missing intervals that need to be processed given the passed in start and end intervals. + + This is a superset of what may actually get processed at runtime based on things like batch size, signal readiness, etc. + + Args: + snapshots: A set of target snapshots for which intervals should be computed. + start: The start of the run. Defaults to the min node start date. + end: The end of the run. Defaults to now. + execution_time: The date/time reference to use for execution time. Defaults to now. + deployability_index: Determines snapshots that are deployable in the context of this evaluation. + restatements: A set of snapshot names being restated. + interval_end_per_model: The mapping from model FQNs to target end dates. + ignore_cron: Whether to ignore the node's cron schedule. + end_bounded: If set to true, the returned intervals will be bounded by the target end date, disregarding lookback, + allow_partials, and other attributes that could cause the intervals to exceed the target end date. + """ + restatements = restatements or {} + validate_date_range(start, end) + + return compute_interval_params( + snapshots, + start=start or earliest_start_date(snapshots), + end=end or now_timestamp(), + deployability_index=deployability_index, + execution_time=execution_time or now_timestamp(), + restatements=restatements, + interval_end_per_model=interval_end_per_model, + ignore_cron=ignore_cron, + end_bounded=end_bounded, + ) + + def compute_interval_params( snapshots: t.Collection[Snapshot], *, diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index a90f9269ff..7e48418317 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -24,7 +24,6 @@ from sqlglot import exp -from sqlmesh.core import constants as c from sqlmesh.core.console import Console, get_console from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.environment import Environment, EnvironmentStatements, EnvironmentSummary @@ -224,13 +223,7 @@ def promote( } added_table_infos = set(table_infos.values()) - if ( - existing_environment - and existing_environment.finalized_ts - and not existing_environment.expired - and existing_environment.gateway_managed == environment.gateway_managed - and existing_environment.name == c.PROD - ): + if existing_environment and environment.can_partially_promote(existing_environment): # Only promote new snapshots. added_table_infos -= set(existing_environment.promoted_snapshots) diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 69fb508219..540a1384e2 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -743,6 +743,7 @@ def test_missing_intervals_lookback(make_snapshot, mocker: MockerFixture): end_bounded=False, ensure_finalized_snapshots=False, interval_end_per_model=None, + explain=False, ) assert not plan.missing_intervals diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py new file mode 100644 index 0000000000..8c9cb9acc0 --- /dev/null +++ b/tests/core/test_plan_stages.py @@ -0,0 +1,814 @@ +import pytest +import typing as t +from sqlglot import parse_one +from pytest_mock.plugin import MockerFixture + +from sqlmesh.core.model import SqlModel, ModelKindName +from sqlmesh.core.plan.definition import EvaluatablePlan +from sqlmesh.core.plan.stages import ( + build_plan_stages, + AfterAllStage, + AuditOnlyRunStage, + PhysicalLayerUpdateStage, + BeforeAllStage, + BackfillStage, + EnvironmentRecordUpdateStage, + VirtualLayerUpdateStage, + RestatementStage, + MigrateSchemasStage, +) +from sqlmesh.core.snapshot.definition import ( + SnapshotChangeCategory, + DeployabilityIndex, + Snapshot, + SnapshotId, +) +from sqlmesh.core.state_sync import StateReader +from sqlmesh.core.environment import Environment, EnvironmentStatements +from sqlmesh.utils.date import to_timestamp + + +@pytest.fixture +def snapshot_a(make_snapshot) -> Snapshot: + snapshot = make_snapshot( + SqlModel( + name="a", + query=parse_one("select 1, ds"), + kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"), + ) + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + return snapshot + + +@pytest.fixture +def snapshot_b(make_snapshot, snapshot_a: Snapshot) -> Snapshot: + snapshot = make_snapshot( + SqlModel( + name="b", + query=parse_one("select 2, ds from a"), + kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"), + ), + nodes={'"a"': snapshot_a.model}, + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + return snapshot + + +def test_build_plan_stages_basic( + snapshot_a: Snapshot, snapshot_b: Snapshot, mocker: MockerFixture +) -> None: + # Mock state reader + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = {} + state_reader.get_environment.return_value = None + + # Create environment + environment = Environment( + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[snapshot_a, snapshot_b], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + indirectly_modified_snapshots={}, + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 4 + + # Verify PhysicalLayerUpdateStage + physical_stage = stages[0] + assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert len(physical_stage.snapshots) == 2 + assert {s.snapshot_id for s in physical_stage.snapshots} == { + snapshot_a.snapshot_id, + snapshot_b.snapshot_id, + } + assert physical_stage.deployability_index == DeployabilityIndex.all_deployable() + + # Verify BackfillStage + backfill_stage = stages[1] + assert isinstance(backfill_stage, BackfillStage) + assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() + expected_interval = (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")) + assert len(backfill_stage.snapshot_to_intervals) == 2 + assert backfill_stage.snapshot_to_intervals[snapshot_a] == [expected_interval] + assert backfill_stage.snapshot_to_intervals[snapshot_b] == [expected_interval] + + # Verify EnvironmentRecordUpdateStage + assert isinstance(stages[2], EnvironmentRecordUpdateStage) + + # Verify VirtualLayerUpdateStage + virtual_stage = stages[3] + assert isinstance(virtual_stage, VirtualLayerUpdateStage) + assert len(virtual_stage.promoted_snapshots) == 2 + assert len(virtual_stage.demoted_snapshots) == 0 + assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} + + +def test_build_plan_stages_with_before_all_and_after_all( + snapshot_a: Snapshot, snapshot_b: Snapshot, mocker: MockerFixture +) -> None: + # Mock state reader + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = {} + state_reader.get_environment.return_value = None + + # Create environment + environment = Environment( + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[snapshot_a, snapshot_b], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + indirectly_modified_snapshots={}, + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=[ + EnvironmentStatements( + before_all=["BEFORE ALL A", "BEFORE ALL B"], + after_all=["AFTER ALL A", "AFTER ALL B"], + python_env={}, + jinja_macros=None, + ) + ], + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 6 + + # Verify BeforeAllStage + before_all_stage = stages[0] + assert isinstance(before_all_stage, BeforeAllStage) + assert before_all_stage.statements == ["BEFORE ALL A", "BEFORE ALL B"] + + # Verify PhysicalLayerUpdateStage + physical_stage = stages[1] + assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert len(physical_stage.snapshots) == 2 + assert {s.snapshot_id for s in physical_stage.snapshots} == { + snapshot_a.snapshot_id, + snapshot_b.snapshot_id, + } + assert physical_stage.deployability_index == DeployabilityIndex.all_deployable() + + # Verify BackfillStage + backfill_stage = stages[2] + assert isinstance(backfill_stage, BackfillStage) + assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() + expected_interval = (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")) + assert len(backfill_stage.snapshot_to_intervals) == 2 + assert backfill_stage.snapshot_to_intervals[snapshot_a] == [expected_interval] + assert backfill_stage.snapshot_to_intervals[snapshot_b] == [expected_interval] + + # Verify EnvironmentRecordUpdateStage + assert isinstance(stages[3], EnvironmentRecordUpdateStage) + + # Verify VirtualLayerUpdateStage + virtual_stage = stages[4] + assert isinstance(virtual_stage, VirtualLayerUpdateStage) + assert len(virtual_stage.promoted_snapshots) == 2 + assert len(virtual_stage.demoted_snapshots) == 0 + assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} + + # Verify AfterAllStage + after_all_stage = stages[5] + assert isinstance(after_all_stage, AfterAllStage) + assert after_all_stage.statements == ["AFTER ALL A", "AFTER ALL B"] + + +def test_build_plan_stages_select_models( + snapshot_a: Snapshot, snapshot_b: Snapshot, mocker: MockerFixture +) -> None: + # Mock state reader + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = {} + state_reader.get_environment.return_value = None + + # Create environment + environment = Environment( + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id], + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[snapshot_a, snapshot_b], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + indirectly_modified_snapshots={}, + removed_snapshots=[], + requires_backfill=True, + models_to_backfill={snapshot_a.name}, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 4 + + # Verify PhysicalLayerUpdateStage + physical_stage = stages[0] + assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert len(physical_stage.snapshots) == 1 + assert {s.snapshot_id for s in physical_stage.snapshots} == {snapshot_a.snapshot_id} + assert physical_stage.deployability_index == DeployabilityIndex.all_deployable() + + # Verify BackfillStage + backfill_stage = stages[1] + assert isinstance(backfill_stage, BackfillStage) + assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() + expected_interval = (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")) + assert len(backfill_stage.snapshot_to_intervals) == 1 + assert backfill_stage.snapshot_to_intervals[snapshot_a] == [expected_interval] + + # Verify EnvironmentRecordUpdateStage + assert isinstance(stages[2], EnvironmentRecordUpdateStage) + + # Verify VirtualLayerUpdateStage + virtual_stage = stages[3] + assert isinstance(virtual_stage, VirtualLayerUpdateStage) + assert len(virtual_stage.promoted_snapshots) == 1 + assert len(virtual_stage.demoted_snapshots) == 0 + assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"'} + + +@pytest.mark.parametrize("skip_backfill,empty_backfill", [(True, False), (False, True)]) +def test_build_plan_stages_basic_no_backfill( + snapshot_a: Snapshot, + snapshot_b: Snapshot, + mocker: MockerFixture, + skip_backfill: bool, + empty_backfill: bool, +) -> None: + # Mock state reader + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = {} + state_reader.get_environment.return_value = None + + # Create environment + environment = Environment( + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[snapshot_a, snapshot_b], + environment=environment, + no_gaps=False, + skip_backfill=skip_backfill, + empty_backfill=empty_backfill, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + indirectly_modified_snapshots={}, + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 4 + + # Verify PhysicalLayerUpdateStage + physical_stage = stages[0] + assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert len(physical_stage.snapshots) == 2 + assert {s.snapshot_id for s in physical_stage.snapshots} == { + snapshot_a.snapshot_id, + snapshot_b.snapshot_id, + } + + # Verify BackfillStage + backfill_stage = stages[1] + assert isinstance(backfill_stage, BackfillStage) + assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() + assert backfill_stage.snapshot_to_intervals == {} + + # Verify EnvironmentRecordUpdateStage + assert isinstance(stages[2], EnvironmentRecordUpdateStage) + + # Verify VirtualLayerUpdateStage + virtual_stage = stages[3] + assert isinstance(virtual_stage, VirtualLayerUpdateStage) + assert len(virtual_stage.promoted_snapshots) == 2 + assert len(virtual_stage.demoted_snapshots) == 0 + assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} + + +def test_build_plan_stages_restatement( + snapshot_a: Snapshot, snapshot_b: Snapshot, mocker: MockerFixture +) -> None: + # Mock state reader to return existing snapshots and environment + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = { + snapshot_a.snapshot_id: snapshot_a, + snapshot_b.snapshot_id: snapshot_b, + } + existing_environment = Environment( + name="prod", + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + state_reader.get_environment.return_value = existing_environment + + environment = Environment( + name="prod", + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id="previous_plan", + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + ) + + # Create evaluatable plan with restatements + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[], # No new snapshots + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={ + '"a"': (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + '"b"': (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + }, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[], # No changes + indirectly_modified_snapshots={}, # No changes + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 4 + + # Verify PhysicalLayerUpdateStage + physical_stage = stages[0] + assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert len(physical_stage.snapshots) == 2 + assert {s.snapshot_id for s in physical_stage.snapshots} == { + snapshot_a.snapshot_id, + snapshot_b.snapshot_id, + } + + # Verify RestatementStage + restatement_stage = stages[1] + assert isinstance(restatement_stage, RestatementStage) + assert len(restatement_stage.snapshot_intervals) == 2 + expected_interval = (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")) + for snapshot_info, interval in restatement_stage.snapshot_intervals.items(): + assert interval == expected_interval + assert snapshot_info.name in ('"a"', '"b"') + + # Verify BackfillStage + backfill_stage = stages[2] + assert isinstance(backfill_stage, BackfillStage) + assert len(backfill_stage.snapshot_to_intervals) == 2 + assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() + expected_backfill_interval = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] + for intervals in backfill_stage.snapshot_to_intervals.values(): + assert intervals == expected_backfill_interval + + # Verify EnvironmentRecordUpdateStage + assert isinstance(stages[3], EnvironmentRecordUpdateStage) + + +def test_build_plan_stages_forward_only( + snapshot_a: Snapshot, snapshot_b: Snapshot, make_snapshot, mocker: MockerFixture +) -> None: + # Categorize snapshot_a as forward-only + new_snapshot_a = make_snapshot( + snapshot_a.model.copy(update={"stamp": "new_version"}), + ) + new_snapshot_a.previous_versions = snapshot_a.all_versions + new_snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + + new_snapshot_b = make_snapshot( + snapshot_b.model.copy(), + nodes={'"a"': new_snapshot_a.model}, + ) + new_snapshot_b.previous_versions = snapshot_b.all_versions + new_snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = {} + existing_environment = Environment( + name="prod", + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + state_reader.get_environment.return_value = existing_environment + + # Create environment + environment = Environment( + name="prod", + snapshots=[new_snapshot_a.table_info, new_snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id="previous_plan", + promoted_snapshot_ids=[new_snapshot_a.snapshot_id, new_snapshot_b.snapshot_id], + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[new_snapshot_a, new_snapshot_b], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[new_snapshot_a.snapshot_id], + indirectly_modified_snapshots={ + new_snapshot_a.name: [new_snapshot_b.snapshot_id], + }, + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 5 + + # Verify PhysicalLayerUpdateStage + physical_stage = stages[0] + assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert len(physical_stage.snapshots) == 2 + assert {s.snapshot_id for s in physical_stage.snapshots} == { + new_snapshot_a.snapshot_id, + new_snapshot_b.snapshot_id, + } + assert physical_stage.deployability_index == DeployabilityIndex.create( + [new_snapshot_a, new_snapshot_b] + ) + + # Verify EnvironmentRecordUpdateStage + assert isinstance(stages[1], EnvironmentRecordUpdateStage) + + # Verify MigrateSchemasStage + migrate_stage = stages[2] + assert isinstance(migrate_stage, MigrateSchemasStage) + assert len(migrate_stage.snapshots) == 2 + assert {s.snapshot_id for s in migrate_stage.snapshots} == { + new_snapshot_a.snapshot_id, + new_snapshot_b.snapshot_id, + } + + # Verify BackfillStage + backfill_stage = stages[3] + assert isinstance(backfill_stage, BackfillStage) + assert len(backfill_stage.snapshot_to_intervals) == 2 + expected_interval = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] + for intervals in backfill_stage.snapshot_to_intervals.values(): + assert intervals == expected_interval + assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() + + # Verify VirtualLayerUpdateStage + virtual_stage = stages[4] + assert isinstance(virtual_stage, VirtualLayerUpdateStage) + assert len(virtual_stage.promoted_snapshots) == 2 + assert len(virtual_stage.demoted_snapshots) == 0 + assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} + + +def test_build_plan_stages_forward_only_dev( + snapshot_a: Snapshot, snapshot_b: Snapshot, make_snapshot, mocker: MockerFixture +) -> None: + # Categorize snapshot_a as forward-only + new_snapshot_a = make_snapshot( + snapshot_a.model.copy(update={"stamp": "new_version"}), + ) + new_snapshot_a.previous_versions = snapshot_a.all_versions + new_snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + + new_snapshot_b = make_snapshot( + snapshot_b.model.copy(), + nodes={'"a"': new_snapshot_a.model}, + ) + new_snapshot_b.previous_versions = snapshot_b.all_versions + new_snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = {} + state_reader.get_environment.return_value = None + + # Create environment + environment = Environment( + name="dev", + snapshots=[new_snapshot_a.table_info, new_snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id=None, + promoted_snapshot_ids=[new_snapshot_a.snapshot_id, new_snapshot_b.snapshot_id], + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[new_snapshot_a, new_snapshot_b], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=True, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[new_snapshot_a.snapshot_id], + indirectly_modified_snapshots={ + new_snapshot_a.name: [new_snapshot_b.snapshot_id], + }, + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 4 + + # Verify PhysicalLayerUpdateStage + physical_stage = stages[0] + assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert len(physical_stage.snapshots) == 2 + assert {s.snapshot_id for s in physical_stage.snapshots} == { + new_snapshot_a.snapshot_id, + new_snapshot_b.snapshot_id, + } + assert physical_stage.deployability_index == DeployabilityIndex.create( + [new_snapshot_a, new_snapshot_b] + ) + + # Verify BackfillStage + backfill_stage = stages[1] + assert isinstance(backfill_stage, BackfillStage) + assert len(backfill_stage.snapshot_to_intervals) == 2 + expected_interval = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] + for intervals in backfill_stage.snapshot_to_intervals.values(): + assert intervals == expected_interval + assert backfill_stage.deployability_index == DeployabilityIndex.create( + [new_snapshot_a, new_snapshot_b] + ) + + # Verify EnvironmentRecordUpdateStage + assert isinstance(stages[2], EnvironmentRecordUpdateStage) + + # Verify VirtualLayerUpdateStage + virtual_stage = stages[3] + assert isinstance(virtual_stage, VirtualLayerUpdateStage) + assert len(virtual_stage.promoted_snapshots) == 2 + assert len(virtual_stage.demoted_snapshots) == 0 + assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} + + +def test_build_plan_stages_audit_only( + snapshot_a: Snapshot, snapshot_b: Snapshot, make_snapshot, mocker: MockerFixture +) -> None: + # Categorize snapshot_a as forward-only + new_snapshot_a = make_snapshot( + snapshot_a.model.copy(update={"audits": [("not_null", {})]}), + ) + new_snapshot_a.previous_versions = snapshot_a.all_versions + new_snapshot_a.categorize_as(SnapshotChangeCategory.METADATA) + new_snapshot_a.add_interval("2023-01-01", "2023-01-02") + + new_snapshot_b = make_snapshot( + snapshot_b.model.copy(), + nodes={'"a"': new_snapshot_a.model}, + ) + new_snapshot_b.previous_versions = snapshot_b.all_versions + new_snapshot_b.categorize_as(SnapshotChangeCategory.METADATA) + new_snapshot_b.add_interval("2023-01-01", "2023-01-02") + + def _get_snapshots(snapshot_ids: t.List[SnapshotId]) -> t.Dict[SnapshotId, Snapshot]: + if snapshot_a.snapshot_id in snapshot_ids and snapshot_b.snapshot_id in snapshot_ids: + return { + snapshot_a.snapshot_id: snapshot_a, + snapshot_b.snapshot_id: snapshot_b, + } + return {} + + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.side_effect = _get_snapshots + state_reader.get_environment.return_value = None + + # Create environment + environment = Environment( + name="dev", + snapshots=[new_snapshot_a.table_info, new_snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id=None, + promoted_snapshot_ids=[new_snapshot_a.snapshot_id, new_snapshot_b.snapshot_id], + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[new_snapshot_a, new_snapshot_b], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=True, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[new_snapshot_a.snapshot_id], + indirectly_modified_snapshots={ + new_snapshot_a.name: [new_snapshot_b.snapshot_id], + }, + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 5 + + # Verify PhysicalLayerUpdateStage + physical_stage = stages[0] + assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert len(physical_stage.snapshots) == 0 + + # Verify AuditOnlyRunStage + audit_only_stage = stages[1] + assert isinstance(audit_only_stage, AuditOnlyRunStage) + assert len(audit_only_stage.snapshots) == 1 + assert audit_only_stage.snapshots[0].snapshot_id == new_snapshot_a.snapshot_id + + # Verify BackfillStage + backfill_stage = stages[2] + assert isinstance(backfill_stage, BackfillStage) + assert len(backfill_stage.snapshot_to_intervals) == 0 + + # Verify EnvironmentRecordUpdateStage + assert isinstance(stages[3], EnvironmentRecordUpdateStage) + + # Verify VirtualLayerUpdateStage + virtual_stage = stages[4] + assert isinstance(virtual_stage, VirtualLayerUpdateStage) + assert len(virtual_stage.promoted_snapshots) == 2 + assert len(virtual_stage.demoted_snapshots) == 0 + assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} From 2a4f5497c5938ae91c677031554496bc7d27a416 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:37:03 +0100 Subject: [PATCH 0344/1056] feat(lsp): log errors instead of notifications (#4659) --- sqlmesh/lsp/main.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index a83bd37fcb..bad27933ff 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -103,12 +103,13 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: loaded_sqlmesh_message(ls, folder_path) return # Exit after successfully loading any config except Exception as e: - ls.show_message( + ls.log_trace( f"Error loading context from {config_path}: {e}", - types.MessageType.Warning, ) except Exception as e: - ls.show_message(f"Error initializing SQLMesh context: {e}", types.MessageType.Error) + ls.log_trace( + f"Error initializing SQLMesh context: {e}", + ) @self.server.feature(ALL_MODELS_FEATURE) def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse: @@ -321,7 +322,9 @@ def hover(ls: LanguageServer, params: types.HoverParams) -> t.Optional[types.Hov ) except Exception as e: - ls.show_message(f"Error getting hover information: {e}", types.MessageType.Error) + ls.log_trace( + f"Error getting hover information: {e}", + ) return None @self.server.feature(types.TEXT_DOCUMENT_DEFINITION) @@ -389,7 +392,9 @@ def diagnostic( result_id=str(result_id), ) except Exception as e: - ls.show_message(f"Error getting diagnostics: {e}", types.MessageType.Error) + ls.log_trace( + f"Error getting diagnostics: {e}", + ) return types.RelatedFullDocumentDiagnosticReport( kind=types.DocumentDiagnosticReportKind.Full, items=[], @@ -452,8 +457,8 @@ def workspace_diagnostic( return types.WorkspaceDiagnosticReport(items=items) except Exception as e: - ls.show_message( - f"Error getting workspace diagnostics: {e}", types.MessageType.Error + ls.log_trace( + f"Error getting workspace diagnostics: {e}", ) return types.WorkspaceDiagnosticReport(items=[]) From 5da8a11bacac45cd2a80db57b396015d87755e1e Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 4 Jun 2025 14:58:21 -0700 Subject: [PATCH 0345/1056] Chore: Add integration test for the plan explantation mode (#4657) --- tests/core/test_integration.py | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 525736dd8e..bfc416596b 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -12,6 +12,7 @@ import pandas as pd # noqa: TID253 import pytest from pathlib import Path +from sqlmesh.core.console import set_console, get_console, TerminalConsole from sqlmesh.core.config.naming import NameInferenceConfig from sqlmesh.utils.concurrency import NodeExecutionFailedError import time_machine @@ -4330,6 +4331,49 @@ def test_indirect_non_breaking_view_is_updated_with_new_table_references( assert row_num > 0 +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_plan_explain(init_and_plan_context: t.Callable): + old_console = get_console() + set_console(TerminalConsole()) + + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + waiter_revenue_by_day_model = context.get_model("sushi.waiter_revenue_by_day") + waiter_revenue_by_day_model = add_projection_to_model( + t.cast(SqlModel, waiter_revenue_by_day_model) + ) + context.upsert_model(waiter_revenue_by_day_model) + + waiter_revenue_by_day_snapshot = context.get_snapshot(waiter_revenue_by_day_model.name) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters") + + common_kwargs = dict(skip_tests=True, no_prompts=True, explain=True) + + # For now just making sure the plan doesn't error + context.plan("dev", **common_kwargs) + context.plan("dev", **common_kwargs, skip_backfill=True) + context.plan("dev", **common_kwargs, empty_backfill=True) + context.plan("dev", **common_kwargs, forward_only=True, enable_preview=True) + context.plan("prod", **common_kwargs) + context.plan("prod", **common_kwargs, forward_only=True) + context.plan("prod", **common_kwargs, restate_models=[waiter_revenue_by_day_model.name]) + + set_console(old_console) + + # Make sure that the now changes were actually applied + for target_env in ("dev", "prod"): + plan = context.plan_builder(target_env, skip_tests=True).build() + assert plan.has_changes + assert plan.missing_intervals + assert plan.directly_modified == {waiter_revenue_by_day_snapshot.snapshot_id} + assert len(plan.new_snapshots) == 2 + assert {s.snapshot_id for s in plan.new_snapshots} == { + waiter_revenue_by_day_snapshot.snapshot_id, + top_waiters_snapshot.snapshot_id, + } + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_dbt_requirements(sushi_dbt_context: Context): assert set(sushi_dbt_context.requirements) == {"dbt-core", "dbt-duckdb"} From 8edfae40ec57730c88af40c4f9f91f0358e920be Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 4 Jun 2025 15:17:57 -0700 Subject: [PATCH 0346/1056] Fix: Support macros for 'authorization' and 'query_label' keys in session properties (#4660) --- sqlmesh/core/engine_adapter/bigquery.py | 4 +++ sqlmesh/core/engine_adapter/trino.py | 6 ++++ sqlmesh/core/model/meta.py | 10 +++---- tests/core/engine_adapter/test_bigquery.py | 9 ++++++ tests/core/engine_adapter/test_trino.py | 9 ++++++ tests/core/test_model.py | 35 ++++++++++++++++++++++ 6 files changed, 68 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 3bd5b1cbe3..648594d0c0 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -199,6 +199,10 @@ def _begin_session(self, properties: SessionProperties) -> None: (label_tuple.expressions[0].name, label_tuple.expressions[1].name) for label_tuple in label_tuples ) + elif query_label_property is not None: + raise SQLMeshError( + "Invalid value for `session_properties.query_label`. Must be an array or tuple." + ) if parsed_query_label: query_label_str = ",".join([":".join(label) for label in parsed_query_label]) diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index aab1bf3c0e..df8e45b520 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -27,6 +27,7 @@ set_catalog, ) from sqlmesh.core.schema_diff import SchemaDiffer +from sqlmesh.utils.errors import SQLMeshError from sqlmesh.utils.date import TimeLike if t.TYPE_CHECKING: @@ -99,6 +100,11 @@ def session(self, properties: SessionProperties) -> t.Iterator[None]: if not isinstance(authorization, exp.Expression): authorization = exp.Literal.string(authorization) + if not authorization.is_string: + raise SQLMeshError( + "Invalid value for `session_properties.authorization`. Must be a string literal." + ) + authorization_sql = authorization.sql(dialect=self.dialect) self.execute(f"SET SESSION AUTHORIZATION {authorization_sql}") diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 3e67995c89..585bb15a6c 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -323,10 +323,8 @@ def session_properties_validator(cls, v: t.Any, info: ValidationInfo) -> t.Any: if prop_name == "query_label": query_label = eq.right - if not ( - isinstance(query_label, exp.Array) - or isinstance(query_label, exp.Tuple) - or isinstance(query_label, exp.Paren) + if not isinstance( + query_label, (exp.Array, exp.Tuple, exp.Paren, d.MacroFunc, d.MacroVar) ): raise ConfigError( "Invalid value for `session_properties.query_label`. Must be an array or tuple." @@ -349,7 +347,9 @@ def session_properties_validator(cls, v: t.Any, info: ValidationInfo) -> t.Any: ) elif prop_name == "authorization": authorization = eq.right - if not (isinstance(authorization, exp.Literal) and authorization.is_string): + if not ( + isinstance(authorization, exp.Literal) and authorization.is_string + ) and not isinstance(authorization, (d.MacroFunc, d.MacroVar)): raise ConfigError( "Invalid value for `session_properties.authorization`. Must be a string literal." ) diff --git a/tests/core/engine_adapter/test_bigquery.py b/tests/core/engine_adapter/test_bigquery.py index 525b480650..e01e42049b 100644 --- a/tests/core/engine_adapter/test_bigquery.py +++ b/tests/core/engine_adapter/test_bigquery.py @@ -14,6 +14,7 @@ from sqlmesh.core.engine_adapter.bigquery import select_partitions_expr from sqlmesh.core.node import IntervalUnit from sqlmesh.utils import AttributeDict +from sqlmesh.utils.errors import SQLMeshError pytestmark = [pytest.mark.bigquery, pytest.mark.engine] @@ -564,6 +565,14 @@ def test_begin_end_session(mocker: MockerFixture): begin_new_session_call = connection_mock._client.query.call_args_list[5] assert begin_new_session_call[0][0] == 'SET @@query_label = "key1:value1";SELECT 1;' + # test invalid query_label value + with pytest.raises( + SQLMeshError, + match="Invalid value for `session_properties.query_label`. Must be an array or tuple.", + ): + with adapter.session({"query_label": parse_one("'key1:value1'")}): + adapter.execute("SELECT 6;") + def _to_sql_calls(execute_mock: t.Any, identify: bool = True) -> t.List[str]: output = [] diff --git a/tests/core/engine_adapter/test_trino.py b/tests/core/engine_adapter/test_trino.py index 2ae766baec..4895bc5a31 100644 --- a/tests/core/engine_adapter/test_trino.py +++ b/tests/core/engine_adapter/test_trino.py @@ -11,6 +11,7 @@ from sqlmesh.core.model import load_sql_based_model from sqlmesh.core.model.definition import SqlModel from sqlmesh.core.dialect import schema_ +from sqlmesh.utils.errors import SQLMeshError from tests.core.engine_adapter import to_sql_calls pytestmark = [pytest.mark.engine, pytest.mark.trino] @@ -632,6 +633,14 @@ def test_session_authorization(trino_mocked_engine_adapter: TrinoEngineAdapter): except RuntimeError: pass + # Test 5: Invalid authorization value + with pytest.raises( + SQLMeshError, + match="Invalid value for `session_properties.authorization`. Must be a string literal.", + ): + with adapter.session({"authorization": exp.Literal.number(1)}): + adapter.execute("SELECT 1") + assert to_sql_calls(adapter) == [ "SET SESSION AUTHORIZATION 'test_user'", "SELECT 1", diff --git a/tests/core/test_model.py b/tests/core/test_model.py index c1e0704c88..932775f831 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -10244,3 +10244,38 @@ def test_invalid_sql_model_query() -> None: match=r"^A query is required and must be a SELECT statement, a UNION statement, or a JINJA_QUERY block.*", ): load_sql_based_model(expressions) + + +def test_query_label_and_authorization_macro(): + @macro() + def test_query_label_macro(evaluator): + return "[('key', 'value')]" + + @macro() + def test_authorization_macro(evaluator): + return exp.Literal.string("test_authorization") + + expressions = d.parse( + """ + MODEL ( + name db.table, + session_properties ( + query_label = @test_query_label_macro(), + authorization = @test_authorization_macro() + ) + ); + + SELECT 1 AS c; + """ + ) + + model = load_sql_based_model(expressions) + assert model.session_properties == { + "query_label": d.parse_one("@test_query_label_macro()"), + "authorization": d.parse_one("@test_authorization_macro()"), + } + + assert model.render_session_properties() == { + "query_label": d.parse_one("[('key', 'value')]"), + "authorization": d.parse_one("'test_authorization'"), + } From 63fd84279427e51d9f072eb50b36de7cbea19550 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:36:32 +0300 Subject: [PATCH 0347/1056] Fix: Escape backslash to prevent syntax error in string (#4662) --- sqlmesh/core/plan/explainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh/core/plan/explainer.py b/sqlmesh/core/plan/explainer.py index 7001c0cc0f..32d80b5dc5 100644 --- a/sqlmesh/core/plan/explainer.py +++ b/sqlmesh/core/plan/explainer.py @@ -195,7 +195,7 @@ def visit_backfill_stage(self, stage: stages.BackfillStage) -> Tree: tree.add(model_tree) else: - tree.add(f"{display_name} \[standalone audit]") + tree.add(f"{display_name} \\[standalone audit]") return tree def visit_migrate_schemas_stage(self, stage: stages.MigrateSchemasStage) -> Tree: From 80b68b7248e46a18d2722f9ec671a090aaf21a91 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:30:37 +0100 Subject: [PATCH 0348/1056] chore(vscode): make the e2e tests more robust (#4664) --- vscode/extension/tests/utils.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index 92fb91928c..34301ba4f2 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -58,11 +58,31 @@ export const startVSCode = async ( const window = await electronApp.firstWindow() await window.waitForLoadState('domcontentloaded') await window.waitForLoadState('networkidle') + await clickExplorerTab(window) + return { window, close: async () => { await electronApp.close() - await fs.removeSync(userDataDir) + await fs.remove(userDataDir) }, } } + +/** + * Click on the Explorer tab in the VS Code activity bar if the Explorer tab is not already active. + * This is necessary because the Explorer tab may not be visible if the user has not opened it yet. + */ +export const clickExplorerTab = async (page: Page): Promise => { + const isExplorerActive = await page.locator("text='Explorer'").isVisible() + if (!isExplorerActive) { + // Wait for the activity bar to be loaded + await page.waitForSelector('.actions-container[role="tablist"]') + + // Click on the Explorer tab using the codicon class + await page.click('.codicon-explorer-view-icon') + + // Wait a bit for the explorer view to activate + await page.locator("text='Explorer'").waitFor({ state: 'visible' }) + } +} From aedb4725597794ea1c7ea3bf33cefc9f66679b51 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:01:52 +0100 Subject: [PATCH 0349/1056] chore: automatically mark lsp tests as fast (#4665) --- tests/lsp/conftest.py | 4 ++++ tests/lsp/test_completions.py | 5 ----- tests/lsp/test_context.py | 2 -- tests/lsp/test_reference.py | 5 ----- tests/lsp/test_reference_macro.py | 2 -- tests/lsp/test_reference_macro_multi.py | 2 -- 6 files changed, 4 insertions(+), 16 deletions(-) create mode 100644 tests/lsp/conftest.py diff --git a/tests/lsp/conftest.py b/tests/lsp/conftest.py new file mode 100644 index 0000000000..6b3d3315aa --- /dev/null +++ b/tests/lsp/conftest.py @@ -0,0 +1,4 @@ +import pytest + +# Apply the 'fast' mark to all tests in this directory and subdirectories +pytestmark = pytest.mark.fast diff --git a/tests/lsp/test_completions.py b/tests/lsp/test_completions.py index 4af2d4dfc8..aa7fde158b 100644 --- a/tests/lsp/test_completions.py +++ b/tests/lsp/test_completions.py @@ -1,4 +1,3 @@ -import pytest from sqlglot import Tokenizer from sqlmesh.core.context import Context from sqlmesh.lsp.completions import get_keywords_from_tokenizer, get_sql_completions @@ -9,19 +8,16 @@ TOKENIZER_KEYWORDS = set(Tokenizer.KEYWORDS.keys()) -@pytest.mark.fast def test_get_keywords_from_tokenizer(): assert len(get_keywords_from_tokenizer()) >= len(TOKENIZER_KEYWORDS) -@pytest.mark.fast def test_get_sql_completions_no_context(): completions = get_sql_completions(None, None) assert len(completions.keywords) >= len(TOKENIZER_KEYWORDS) assert len(completions.models) == 0 -@pytest.mark.fast def test_get_sql_completions_with_context_no_file_uri(): context = Context(paths=["examples/sushi"]) lsp_context = LSPContext(context) @@ -32,7 +28,6 @@ def test_get_sql_completions_with_context_no_file_uri(): assert "sushi.customers" in completions.models -@pytest.mark.fast def test_get_sql_completions_with_context_and_file_uri(): context = Context(paths=["examples/sushi"]) lsp_context = LSPContext(context) diff --git a/tests/lsp/test_context.py b/tests/lsp/test_context.py index 96a8c993aa..c26e8f35d5 100644 --- a/tests/lsp/test_context.py +++ b/tests/lsp/test_context.py @@ -1,9 +1,7 @@ -import pytest from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget -@pytest.mark.fast def test_lsp_context(): context = Context(paths=["examples/sushi"]) lsp_context = LSPContext(context) diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index 08e92e8fca..dc9f9ea982 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -1,4 +1,3 @@ -import pytest from lsprotocol.types import Position from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget @@ -6,7 +5,6 @@ from sqlmesh.lsp.uri import URI -@pytest.mark.fast def test_reference() -> None: context = Context(paths=["examples/sushi"]) lsp_context = LSPContext(context) @@ -39,7 +37,6 @@ def test_reference() -> None: assert referenced_text == "sushi.customers" -@pytest.mark.fast def test_reference_with_alias() -> None: context = Context(paths=["examples/sushi"]) lsp_context = LSPContext(context) @@ -79,7 +76,6 @@ def test_reference_with_alias() -> None: assert get_string_from_range(read_file, references[2].range) == "sushi.items" -@pytest.mark.fast def test_standalone_audit_reference() -> None: context = Context(paths=["examples/sushi"]) lsp_context = LSPContext(context) @@ -129,7 +125,6 @@ def get_string_from_range(file_lines, range_obj) -> str: return result -@pytest.mark.fast def test_filter_references_by_position() -> None: """Test that we can filter references correctly based on cursor position.""" context = Context(paths=["examples/sushi"]) diff --git a/tests/lsp/test_reference_macro.py b/tests/lsp/test_reference_macro.py index 35cf317b91..806fd1f819 100644 --- a/tests/lsp/test_reference_macro.py +++ b/tests/lsp/test_reference_macro.py @@ -1,11 +1,9 @@ -import pytest from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget from sqlmesh.lsp.reference import get_macro_definitions_for_a_path from sqlmesh.lsp.uri import URI -@pytest.mark.fast def test_macro_references() -> None: """Test that macro references (e.g., @ADD_ONE, @MULTIPLY) have proper go-to-definition support.""" context = Context(paths=["examples/sushi"]) diff --git a/tests/lsp/test_reference_macro_multi.py b/tests/lsp/test_reference_macro_multi.py index 1133609105..410ce69d0c 100644 --- a/tests/lsp/test_reference_macro_multi.py +++ b/tests/lsp/test_reference_macro_multi.py @@ -1,11 +1,9 @@ -import pytest from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget from sqlmesh.lsp.reference import get_macro_definitions_for_a_path from sqlmesh.lsp.uri import URI -@pytest.mark.fast def test_macro_references_multirepo() -> None: context = Context(paths=["examples/multi/repo_1", "examples/multi/repo_2"], gateway="memory") lsp_context = LSPContext(context) From 40bd72f8a9890aecd9fd06b1929cebdce8f3b036 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:54:44 +0100 Subject: [PATCH 0350/1056] feat(lsp): add keywords in query to autocompete (#4638) --- sqlmesh/lsp/completions.py | 66 +++++++++++++++++- sqlmesh/lsp/context.py | 7 +- sqlmesh/lsp/main.py | 23 +++++- tests/lsp/test_completions.py | 128 +++++++++++++++++++++++++++++++++- 4 files changed, 216 insertions(+), 8 deletions(-) diff --git a/sqlmesh/lsp/completions.py b/sqlmesh/lsp/completions.py index 8d5d68d931..6716f16e2f 100644 --- a/sqlmesh/lsp/completions.py +++ b/sqlmesh/lsp/completions.py @@ -7,14 +7,25 @@ def get_sql_completions( - context: t.Optional[LSPContext], file_uri: t.Optional[URI] + context: t.Optional[LSPContext], file_uri: t.Optional[URI], content: t.Optional[str] = None ) -> AllModelsResponse: """ Return a list of completions for a given file. """ + # Get SQL keywords for the dialect + sql_keywords = get_keywords(context, file_uri) + + # Get keywords from file content if provided + file_keywords = set() + if content: + file_keywords = extract_keywords_from_content(content, get_dialect(context, file_uri)) + + # Combine keywords - SQL keywords first, then file keywords + all_keywords = list(sql_keywords) + list(file_keywords - sql_keywords) + return AllModelsResponse( models=list(get_models(context, file_uri)), - keywords=list(get_keywords(context, file_uri)), + keywords=all_keywords, ) @@ -97,3 +108,54 @@ def get_keywords_from_tokenizer(dialect: t.Optional[str] = None) -> t.Set[str]: parts = keyword.split(" ") expanded_keywords.update(parts) return expanded_keywords + + +def get_dialect(context: t.Optional[LSPContext], file_uri: t.Optional[URI]) -> t.Optional[str]: + """ + Get the dialect for a given file. + """ + if file_uri is not None and context is not None and file_uri.to_path() in context.map: + file_info = context.map[file_uri.to_path()] + + # Handle ModelInfo objects + if isinstance(file_info, ModelTarget) and file_info.names: + model_name = file_info.names[0] + model_from_context = context.context.get_model(model_name) + return model_from_context.dialect + + # Handle AuditInfo objects + if isinstance(file_info, AuditTarget) and file_info.name: + audit = context.context.standalone_audits.get(file_info.name) + if audit is not None and audit.dialect: + return audit.dialect + + if context is not None: + return context.context.default_dialect + + return None + + +def extract_keywords_from_content(content: str, dialect: t.Optional[str] = None) -> t.Set[str]: + """ + Extract identifiers from SQL content using the tokenizer. + Only extracts identifiers (variable names, table names, column names, etc.) + that are not SQL keywords. + """ + if not content: + return set() + + tokenizer_class = Dialect.get_or_raise(dialect).tokenizer_class + keywords = set() + try: + tokenizer = tokenizer_class() + tokens = tokenizer.tokenize(content) + for token in tokens: + # Don't include keywords in the set + if token.text.upper() not in tokenizer_class.KEYWORDS: + keywords.add(token.text) + + except Exception: + # If tokenization fails, return empty set + pass + + return keywords diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 251bac8b5b..9ecbb9a2b1 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -176,15 +176,18 @@ def list_of_models_for_rendering(self) -> t.List[ModelForRendering]: if audit._path is not None ] - def get_autocomplete(self, uri: t.Optional[URI]) -> AllModelsResponse: + def get_autocomplete( + self, uri: t.Optional[URI], content: t.Optional[str] = None + ) -> AllModelsResponse: """Get autocomplete suggestions for a file. Args: uri: The URI of the file to get autocomplete suggestions for. + content: The content of the file (optional). Returns: AllModelsResponse containing models and keywords. """ from sqlmesh.lsp.completions import get_sql_completions - return get_sql_completions(self, uri) + return get_sql_completions(self, uri, content) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index bad27933ff..194b19e65d 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -114,13 +114,22 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: @self.server.feature(ALL_MODELS_FEATURE) def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse: uri = URI(params.textDocument.uri) + + # Get the document content + content = None + try: + document = ls.workspace.get_text_document(params.textDocument.uri) + content = document.source + except Exception: + pass + try: context = self._context_get_or_load(uri) - return context.get_autocomplete(uri) + return context.get_autocomplete(uri, content) except Exception as e: from sqlmesh.lsp.completions import get_sql_completions - return get_sql_completions(None, URI(params.textDocument.uri)) + return get_sql_completions(None, URI(params.textDocument.uri), content) @self.server.feature(RENDER_MODEL_FEATURE) def render_model(ls: LanguageServer, params: RenderModelRequest) -> RenderModelResponse: @@ -471,8 +480,16 @@ def completion( uri = URI(params.text_document.uri) context = self._context_get_or_load(uri) + # Get the document content + content = None + try: + document = ls.workspace.get_text_document(params.text_document.uri) + content = document.source + except Exception: + pass + # Get completions using the existing completions module - completion_response = context.get_autocomplete(uri) + completion_response = context.get_autocomplete(uri, content) completion_items = [] # Add model completions diff --git a/tests/lsp/test_completions.py b/tests/lsp/test_completions.py index aa7fde158b..e365873c19 100644 --- a/tests/lsp/test_completions.py +++ b/tests/lsp/test_completions.py @@ -1,6 +1,10 @@ from sqlglot import Tokenizer from sqlmesh.core.context import Context -from sqlmesh.lsp.completions import get_keywords_from_tokenizer, get_sql_completions +from sqlmesh.lsp.completions import ( + get_keywords_from_tokenizer, + get_sql_completions, + extract_keywords_from_content, +) from sqlmesh.lsp.context import LSPContext from sqlmesh.lsp.uri import URI @@ -36,3 +40,125 @@ def test_get_sql_completions_with_context_and_file_uri(): completions = lsp_context.get_autocomplete(URI.from_path(file_uri)) assert len(completions.keywords) > len(TOKENIZER_KEYWORDS) assert "sushi.active_customers" not in completions.models + + +def test_extract_keywords_from_content(): + # Test extracting keywords from SQL content + content = """ + SELECT customer_id, order_date, total_amount + FROM orders o + JOIN customers c ON o.customer_id = c.id + WHERE order_date > '2024-01-01' + """ + + keywords = extract_keywords_from_content(content) + + # Check that identifiers are extracted + assert "customer_id" in keywords + assert "order_date" in keywords + assert "total_amount" in keywords + assert "orders" in keywords + assert "customers" in keywords + assert "o" in keywords # alias + assert "c" in keywords # alias + assert "id" in keywords + + # Check that SQL keywords are NOT included + assert "SELECT" not in keywords + assert "FROM" not in keywords + assert "JOIN" not in keywords + assert "WHERE" not in keywords + assert "ON" not in keywords + + +def test_get_sql_completions_with_file_content(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # SQL content with custom identifiers + content = """ + SELECT my_custom_column, another_identifier + FROM my_custom_table mct + JOIN some_other_table sot ON mct.id = sot.table_id + WHERE my_custom_column > 100 + """ + + file_uri = next(key for key in lsp_context.map.keys() if key.name == "active_customers.sql") + completions = lsp_context.get_autocomplete(URI.from_path(file_uri), content) + + # Check that SQL keywords are included + assert any(k in ["SELECT", "FROM", "WHERE", "JOIN"] for k in completions.keywords) + + # Check that file-specific identifiers are included at the end + keywords_list = completions.keywords + assert "my_custom_column" in keywords_list + assert "another_identifier" in keywords_list + assert "my_custom_table" in keywords_list + assert "some_other_table" in keywords_list + assert "mct" in keywords_list # alias + assert "sot" in keywords_list # alias + assert "table_id" in keywords_list + + # Check that file keywords come after SQL keywords + # SQL keywords should appear first in the list + sql_keyword_indices = [ + i for i, k in enumerate(keywords_list) if k in ["SELECT", "FROM", "WHERE", "JOIN"] + ] + file_keyword_indices = [ + i for i, k in enumerate(keywords_list) if k in ["my_custom_column", "my_custom_table"] + ] + + if sql_keyword_indices and file_keyword_indices: + assert max(sql_keyword_indices) < min(file_keyword_indices), ( + "SQL keywords should come before file keywords" + ) + + +def test_get_sql_completions_with_partial_cte_query(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Partial SQL query with CTEs + content = """ + WITH _latest_complete_month AS ( + SELECT MAX(date_trunc('month', order_date)) as month + FROM orders + ), + _filtered AS ( + SELECT * FROM + """ + + file_uri = next(key for key in lsp_context.map.keys() if key.name == "active_customers.sql") + completions = lsp_context.get_autocomplete(URI.from_path(file_uri), content) + + # Check that CTE names are included in the keywords + keywords_list = completions.keywords + assert "_latest_complete_month" in keywords_list + assert "_filtered" in keywords_list + + # Also check other identifiers from the partial query + assert "month" in keywords_list + assert "order_date" in keywords_list + assert "orders" in keywords_list + + +def test_extract_keywords_from_partial_query(): + # Test extracting keywords from an incomplete SQL query + content = """ + WITH cte1 AS ( + SELECT col1, col2 FROM table1 + ), + cte2 AS ( + SELECT * FROM cte1 WHERE + """ + + keywords = extract_keywords_from_content(content) + + # Check that CTEs are extracted + assert "cte1" in keywords + assert "cte2" in keywords + + # Check that columns and tables are extracted + assert "col1" in keywords + assert "col2" in keywords + assert "table1" in keywords From bc8d3d7ddfe4ee12da21bb13c27e717b23030bea Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:09:51 +0300 Subject: [PATCH 0351/1056] Feat(lsp): Add support for go to and find all References for CTEs (#4652) --- sqlmesh/lsp/main.py | 26 ++- sqlmesh/lsp/reference.py | 86 +++++++- tests/lsp/test_reference_cte_find_all.py | 140 +++++++++++++ .../extension/tests/find_references.spec.ts | 186 ++++++++++++++++++ 4 files changed, 425 insertions(+), 13 deletions(-) create mode 100644 tests/lsp/test_reference_cte_find_all.py create mode 100644 vscode/extension/tests/find_references.spec.ts diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 194b19e65d..942d644689 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -36,9 +36,7 @@ RenderModelRequest, RenderModelResponse, ) -from sqlmesh.lsp.reference import ( - get_references, -) +from sqlmesh.lsp.reference import get_references, get_cte_references from sqlmesh.lsp.uri import URI from web.server.api.endpoints.lineage import column_lineage, model_lineage from web.server.api.endpoints.models import get_models @@ -378,6 +376,28 @@ def goto_definition( ls.show_message(f"Error getting references: {e}", types.MessageType.Error) return [] + @self.server.feature(types.TEXT_DOCUMENT_REFERENCES) + def find_references( + ls: LanguageServer, params: types.ReferenceParams + ) -> t.Optional[t.List[types.Location]]: + """Find all references of a symbol (currently supporting CTEs)""" + try: + uri = URI(params.text_document.uri) + self._ensure_context_for_document(uri) + document = ls.workspace.get_text_document(params.text_document.uri) + if self.lsp_context is None: + raise RuntimeError(f"No context found for document: {document.path}") + + cte_references = get_cte_references(self.lsp_context, uri, params.position) + + # Convert references to Location objects + locations = [types.Location(uri=ref.uri, range=ref.range) for ref in cte_references] + + return locations if locations else None + except Exception as e: + ls.show_message(f"Error getting locations: {e}", types.MessageType.Error) + return None + @self.server.feature(types.TEXT_DOCUMENT_DIAGNOSTIC) def diagnostic( ls: LanguageServer, params: types.DocumentDiagnosticParams diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index b8e6e9c422..b43cc56751 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -47,16 +47,7 @@ def by_position(position: Position) -> t.Callable[[Reference], bool]: """ def contains_position(r: Reference) -> bool: - return ( - r.range.start.line < position.line - or ( - r.range.start.line == position.line - and r.range.start.character <= position.character - ) - ) and ( - r.range.end.line > position.line - or (r.range.end.line == position.line and r.range.end.character >= position.character) - ) + return _position_within_range(position, r.range) return contains_position @@ -478,3 +469,78 @@ def get_built_in_macro_reference(macro_name: str, macro_range: Range) -> t.Optio ), markdown_description=func.__doc__ if func.__doc__ else None, ) + + +def get_cte_references( + lint_context: LSPContext, document_uri: URI, position: Position +) -> t.List[Reference]: + """ + Get all references to a CTE at a specific position in a document. + + This function finds both the definition and all usages of a CTE within the same file. + + Args: + lint_context: The LSP context + document_uri: The URI of the document + position: The position to check for CTE references + + Returns: + A list of references to the CTE (including its definition and all usages) + """ + references = get_model_definitions_for_a_path(lint_context, document_uri) + + # Filter for CTE references (those with target_range set and same URI) + # TODO: Consider extending Reference class to explicitly indicate reference type instead + cte_references = [ + ref for ref in references if ref.target_range is not None and ref.uri == document_uri.value + ] + + if not cte_references: + return [] + + target_cte_definition_range = None + for ref in cte_references: + # Check if cursor is on a CTE usage + if _position_within_range(position, ref.range): + target_cte_definition_range = ref.target_range + break + # Check if cursor is on the CTE definition + elif ref.target_range and _position_within_range(position, ref.target_range): + target_cte_definition_range = ref.target_range + break + + if target_cte_definition_range is None: + return [] + + # Add the CTE definition + matching_references = [ + Reference( + uri=document_uri.value, + range=target_cte_definition_range, + markdown_description="CTE definition", + ) + ] + + # Add all usages + for ref in cte_references: + if ref.target_range == target_cte_definition_range: + matching_references.append( + Reference( + uri=document_uri.value, + range=ref.range, + markdown_description="CTE usage", + ) + ) + + return matching_references + + +def _position_within_range(position: Position, range: Range) -> bool: + """Check if a position is within a given range.""" + return ( + range.start.line < position.line + or (range.start.line == position.line and range.start.character <= position.character) + ) and ( + range.end.line > position.line + or (range.end.line == position.line and range.end.character >= position.character) + ) diff --git a/tests/lsp/test_reference_cte_find_all.py b/tests/lsp/test_reference_cte_find_all.py new file mode 100644 index 0000000000..d57c996a6a --- /dev/null +++ b/tests/lsp/test_reference_cte_find_all.py @@ -0,0 +1,140 @@ +from lsprotocol.types import Position +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.reference import get_cte_references +from sqlmesh.lsp.uri import URI +from tests.lsp.test_reference_cte import find_ranges_from_regex + + +def test_cte_find_all_references(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Test finding all references of "current_marketing" + ranges = find_ranges_from_regex(read_file, r"current_marketing(?!_outer)") + assert len(ranges) == 2 + + # Click on the CTE definition + position = Position(line=ranges[0].start.line, character=ranges[0].start.character + 4) + references = get_cte_references(lsp_context, URI.from_path(sushi_customers_path), position) + + # Should find both the definition and the usage + assert len(references) == 2 + assert all(ref.uri == URI.from_path(sushi_customers_path).value for ref in references) + + reference_ranges = [ref.range for ref in references] + for expected_range in ranges: + assert any( + ref_range.start.line == expected_range.start.line + and ref_range.start.character == expected_range.start.character + for ref_range in reference_ranges + ), ( + f"Expected to find reference at line {expected_range.start.line}, char {expected_range.start.character}" + ) + + # Click on the CTE usage + position = Position(line=ranges[1].start.line, character=ranges[1].start.character + 4) + references = get_cte_references(lsp_context, URI.from_path(sushi_customers_path), position) + + # Should find the same references + assert len(references) == 2 + assert all(ref.uri == URI.from_path(sushi_customers_path).value for ref in references) + + reference_ranges = [ref.range for ref in references] + for expected_range in ranges: + assert any( + ref_range.start.line == expected_range.start.line + and ref_range.start.character == expected_range.start.character + for ref_range in reference_ranges + ), ( + f"Expected to find reference at line {expected_range.start.line}, char {expected_range.start.character}" + ) + + +def test_cte_find_all_references_outer(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Test finding all references of "current_marketing_outer" + ranges = find_ranges_from_regex(read_file, r"current_marketing_outer") + assert len(ranges) == 2 + + # Click on the CTE definition + position = Position(line=ranges[0].start.line, character=ranges[0].start.character + 4) + references = get_cte_references(lsp_context, URI.from_path(sushi_customers_path), position) + + # Should find both the definition and the usage + assert len(references) == 2 + assert all(ref.uri == URI.from_path(sushi_customers_path).value for ref in references) + + # Verify that we found both occurrences + reference_ranges = [ref.range for ref in references] + for expected_range in ranges: + assert any( + ref_range.start.line == expected_range.start.line + and ref_range.start.character == expected_range.start.character + for ref_range in reference_ranges + ), ( + f"Expected to find reference at line {expected_range.start.line}, char {expected_range.start.character}" + ) + + # Click on the CTE usage + position = Position(line=ranges[1].start.line, character=ranges[1].start.character + 4) + references = get_cte_references(lsp_context, URI.from_path(sushi_customers_path), position) + + # Should find the same references + assert len(references) == 2 + assert all(ref.uri == URI.from_path(sushi_customers_path).value for ref in references) + + reference_ranges = [ref.range for ref in references] + for expected_range in ranges: + assert any( + ref_range.start.line == expected_range.start.line + and ref_range.start.character == expected_range.start.character + for ref_range in reference_ranges + ), ( + f"Expected to find reference at line {expected_range.start.line}, char {expected_range.start.character}" + ) + + +def test_cte_no_references_on_non_cte(): + # Test that clicking on non-CTE elements returns nothing, once this is supported adapt this test accordingly + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Click on a regular table reference + ranges = find_ranges_from_regex(read_file, r"sushi\.orders") + assert len(ranges) >= 1 + + position = Position(line=ranges[0].start.line, character=ranges[0].start.character + 4) + references = get_cte_references(lsp_context, URI.from_path(sushi_customers_path), position) + + # Should find no references since this is not a CTE + assert len(references) == 0 diff --git a/vscode/extension/tests/find_references.spec.ts b/vscode/extension/tests/find_references.spec.ts new file mode 100644 index 0000000000..1733fd362a --- /dev/null +++ b/vscode/extension/tests/find_references.spec.ts @@ -0,0 +1,186 @@ +import { test, expect } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { startVSCode, SUSHI_SOURCE_PATH } from './utils' + +// Consistent keyboard shortcuts +const GO_TO_REFERENCES_KEY = 'Shift+F12' +const FIND_ALL_REFERENCES_KEY = + process.platform === 'darwin' ? 'Alt+Shift+F12' : 'Ctrl+Shift+F12' + +test.describe('CTE References', () => { + let tempDir: string + let window: any + let close: () => Promise + + test.beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const vscode = await startVSCode(tempDir) + window = vscode.window + close = vscode.close + + // Common setup: navigate to customers.sql + await window.waitForSelector('text=models') + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + }) + + test.afterEach(async () => { + await close() + fs.removeSync(tempDir) + }) + + test('Go to references from definition of CTE', async () => { + // Click on the CTE definition "current_marketing_outer" at line 20 to position cursor + await window.locator('text=current_marketing_outer').first().click() + + // Use keyboard shortcut to find all references + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Wait for the references to appear + await window.waitForSelector('text=References') + + // Wait for reference panel to populate + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) + + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() + + // Check that both CTE definition and usage are listed in references + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing_outer AS') + await window.waitForSelector('text=FROM current_marketing_outer') + }) + + test('Go to references from usage of CTE', async () => { + // Click on the CTE usage this time for "current_marketing_outer" + await window.locator('text=FROM current_marketing_outer').click({ + position: { x: 80, y: 5 }, // Clicks on the usage rather than first which was definition + }) + + // Use keyboard shortcut to go to references + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Wait for the references to appear + await window.waitForSelector('text=References') + + // Better assertions: wait for reference panel to populate + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) + + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing_outer AS') + await window.waitForSelector('text=FROM current_marketing_outer') + + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() + }) + + test('Go to references for nested CTE', async () => { + // Click on the nested CTE "current_marketing" + await window.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, // Click on the CTE name part + }) + + // Use keyboard shortcut to find all references + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Wait for the references to appear + await window.waitForSelector('text=References') + + // Wait for reference panel to populate + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) + + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() + + // Check that both CTE definition and usage are listed in references + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing AS') + await window.waitForSelector('text=FROM current_marketing') + }) + + test('Find all references for CTE', async () => { + // Click on the CTE definition "current_marketing_outer" + await window.locator('text=current_marketing_outer').first().click() + + // Use keyboard shortcut to find all references + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + + // Verify references contains expected content + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing_outer AS') + await window.waitForSelector('text=FROM current_marketing_outer') + + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() + }) + + test('Find all references from usage for CTE', async () => { + // Click on the CTE usage of "current_marketing_outer" using last + await window.locator('text=current_marketing_outer').last().click() + + // Use keyboard shortcut to find all references + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + + // Verify references contains expected content + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing_outer AS') + await window.waitForSelector('text=FROM current_marketing_outer') + + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() + }) + + test('Find all references for nested CTE', async () => { + // Click on the nested CTE "current_marketing" at line 33 + // We need to be more specific to get the inner one + await window.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, // Click on the CTE name part + }) + + // Use keyboard shortcut to find all references + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + + // Verify references contains expected content + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing AS') + await window.waitForSelector('text=FROM current_marketing') + + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() + }) +}) From 2595aca9ac866f245a7dc09da2d6850957166f36 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:07:25 +0100 Subject: [PATCH 0352/1056] feat: add position details to linter (#4663) --- sqlmesh/core/linter/definition.py | 7 +- sqlmesh/core/linter/helpers.py | 115 +++++++++++++++++++++++ sqlmesh/core/linter/rule.py | 32 ++++++- sqlmesh/core/linter/rules/builtin.py | 23 ++++- sqlmesh/lsp/context.py | 2 +- sqlmesh/lsp/main.py | 25 +++-- sqlmesh/lsp/reference.py | 132 ++++++++++----------------- tests/lsp/test_diagnostics.py | 55 +++++++++++ 8 files changed, 293 insertions(+), 98 deletions(-) create mode 100644 sqlmesh/core/linter/helpers.py create mode 100644 tests/lsp/test_diagnostics.py diff --git a/sqlmesh/core/linter/definition.py b/sqlmesh/core/linter/definition.py index a8ebdec0e8..14ae1dd2ef 100644 --- a/sqlmesh/core/linter/definition.py +++ b/sqlmesh/core/linter/definition.py @@ -8,7 +8,7 @@ from collections.abc import Iterator, Iterable, Set, Mapping, Callable from functools import reduce from sqlmesh.core.model import Model -from sqlmesh.core.linter.rule import Rule, RuleViolation +from sqlmesh.core.linter.rule import Rule, RuleViolation, Range from sqlmesh.core.console import LinterConsole, get_console if t.TYPE_CHECKING: @@ -74,6 +74,7 @@ def lint_model( violation_msg=violation.violation_msg, model=model, violation_type="error", + violation_range=violation.violation_range, ) for violation in error_violations ] + [ @@ -82,6 +83,7 @@ def lint_model( violation_msg=violation.violation_msg, model=model, violation_type="warning", + violation_range=violation.violation_range, ) for violation in warn_violations ] @@ -149,7 +151,8 @@ def __init__( violation_msg: str, model: Model, violation_type: t.Literal["error", "warning"], + violation_range: t.Optional[Range] = None, ) -> None: - super().__init__(rule, violation_msg) + super().__init__(rule, violation_msg, violation_range) self.model = model self.violation_type = violation_type diff --git a/sqlmesh/core/linter/helpers.py b/sqlmesh/core/linter/helpers.py new file mode 100644 index 0000000000..6d0d796c97 --- /dev/null +++ b/sqlmesh/core/linter/helpers.py @@ -0,0 +1,115 @@ +from pathlib import Path + +from sqlmesh.core.linter.rule import Position, Range +from sqlmesh.utils.pydantic import PydanticModel +import typing as t + + +class TokenPositionDetails(PydanticModel): + """ + Details about a token's position in the source code in the structure provided by SQLGlot. + + Attributes: + line (int): The line that the token ends on. + col (int): The column that the token ends on. + start (int): The start index of the token. + end (int): The ending index of the token. + """ + + line: int + col: int + start: int + end: int + + @staticmethod + def from_meta(meta: t.Dict[str, int]) -> "TokenPositionDetails": + return TokenPositionDetails( + line=meta["line"], + col=meta["col"], + start=meta["start"], + end=meta["end"], + ) + + def to_range(self, read_file: t.Optional[t.List[str]]) -> Range: + """ + Convert a TokenPositionDetails object to a Range object. + + In the circumstances where the token's start and end positions are the same, + there is no need for a read_file parameter, as the range can be derived from the token's + line and column. This is an optimization to avoid unnecessary file reads and should + only be used when the token represents a single character or position in the file. + + If the token's start and end positions are different, the read_file parameter is required. + + :param read_file: List of lines from the file. Optional + :return: A Range object representing the token's position + """ + if self.start == self.end: + # If the start and end positions are the same, we can create a range directly + return Range( + start=Position(line=self.line - 1, character=self.col - 1), + end=Position(line=self.line - 1, character=self.col), + ) + + if read_file is None: + raise ValueError("read_file must be provided when start and end positions differ.") + + # Convert from 1-indexed to 0-indexed for line only + end_line_0 = self.line - 1 + end_col_0 = self.col + + # Find the start line and column by counting backwards from the end position + start_pos = self.start + end_pos = self.end + + # Initialize with the end position + start_line_0 = end_line_0 + start_col_0 = end_col_0 - (end_pos - start_pos + 1) + + # If start_col_0 is negative, we need to go back to previous lines + while start_col_0 < 0 and start_line_0 > 0: + start_line_0 -= 1 + start_col_0 += len(read_file[start_line_0]) + # Account for newline character + if start_col_0 >= 0: + break + start_col_0 += 1 # For the newline character + + # Ensure we don't have negative values + start_col_0 = max(0, start_col_0) + return Range( + start=Position(line=start_line_0, character=start_col_0), + end=Position(line=end_line_0, character=end_col_0), + ) + + +def read_range_from_file(file: Path, text_range: Range) -> str: + """ + Read the file and return the content within the specified range. + + Args: + file: Path to the file to read + text_range: The range of text to extract + + Returns: + The content within the specified range + """ + with file.open("r", encoding="utf-8") as f: + lines = f.readlines() + + # Ensure the range is within bounds + start_line = max(0, text_range.start.line) + end_line = min(len(lines), text_range.end.line + 1) + + if start_line >= end_line: + return "" + + # Extract the relevant portions of each line + result = [] + for i in range(start_line, end_line): + line = lines[i] + start_char = text_range.start.character if i == text_range.start.line else 0 + end_char = text_range.end.character if i == text_range.end.line else len(line) + result.append(line[start_char:end_char]) + + return "".join(result) diff --git a/sqlmesh/core/linter/rule.py b/sqlmesh/core/linter/rule.py index 003f9b813a..84e1693bef 100644 --- a/sqlmesh/core/linter/rule.py +++ b/sqlmesh/core/linter/rule.py @@ -1,6 +1,7 @@ from __future__ import annotations import abc +from dataclasses import dataclass from sqlmesh.core.model import Model @@ -22,6 +23,22 @@ class RuleLocation(PydanticModel): start_line: t.Optional[int] = None +@dataclass(frozen=True) +class Position: + """The position of a rule violation in a file, the position follows the LSP standard.""" + + line: int + character: int + + +@dataclass(frozen=True) +class Range: + """The range of a rule violation in a file. The range follows the LSP standard.""" + + start: Position + end: Position + + class _Rule(abc.ABCMeta): def __new__(cls: Type[_Rule], clsname: str, bases: t.Tuple, attrs: t.Dict) -> _Rule: attrs["name"] = clsname.lower() @@ -45,9 +62,15 @@ def summary(self) -> str: """A summary of what this rule checks for.""" return self.__doc__ or "" - def violation(self, violation_msg: t.Optional[str] = None) -> RuleViolation: + def violation( + self, + violation_msg: t.Optional[str] = None, + violation_range: t.Optional[Range] = None, + ) -> RuleViolation: """Create a RuleViolation instance for this rule""" - return RuleViolation(rule=self, violation_msg=violation_msg or self.summary) + return RuleViolation( + rule=self, violation_msg=violation_msg or self.summary, violation_range=violation_range + ) def get_definition_location(self) -> RuleLocation: """Return the file path and position information for this rule. @@ -79,9 +102,12 @@ def __repr__(self) -> str: class RuleViolation: - def __init__(self, rule: Rule, violation_msg: str) -> None: + def __init__( + self, rule: Rule, violation_msg: str, violation_range: t.Optional[Range] = None + ) -> None: self.rule = rule self.violation_msg = violation_msg + self.violation_range = violation_range def __repr__(self) -> str: return f"{self.rule.name}: {self.violation_msg}" diff --git a/sqlmesh/core/linter/rules/builtin.py b/sqlmesh/core/linter/rules/builtin.py index 9f93a24236..0480683f6d 100644 --- a/sqlmesh/core/linter/rules/builtin.py +++ b/sqlmesh/core/linter/rules/builtin.py @@ -4,9 +4,11 @@ import typing as t +from sqlglot.expressions import Star from sqlglot.helper import subclasses -from sqlmesh.core.linter.rule import Rule, RuleViolation +from sqlmesh.core.linter.helpers import TokenPositionDetails +from sqlmesh.core.linter.rule import Rule, RuleViolation, Range from sqlmesh.core.linter.definition import RuleSet from sqlmesh.core.model import Model, SqlModel @@ -15,10 +17,25 @@ class NoSelectStar(Rule): """Query should not contain SELECT * on its outer most projections, even if it can be expanded.""" def check_model(self, model: Model) -> t.Optional[RuleViolation]: + # Only applies to SQL models, as other model types do not have a query. if not isinstance(model, SqlModel): return None - - return self.violation() if model.query.is_star else None + if model.query.is_star: + violation_range = self._get_range(model) + return self.violation(violation_range=violation_range) + return None + + def _get_range(self, model: SqlModel) -> t.Optional[Range]: + """Get the range of the violation if available.""" + try: + if len(model.query.expressions) == 1 and isinstance(model.query.expressions[0], Star): + return TokenPositionDetails.from_meta(model.query.expressions[0].meta).to_range( + None + ) + except Exception: + pass + + return None class InvalidSelectStarExpansion(Rule): diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 9ecbb9a2b1..4ac55f1a22 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -5,7 +5,7 @@ from sqlmesh.core.model.definition import SqlModel from sqlmesh.core.linter.definition import AnnotatedRuleViolation -from sqlmesh.lsp.custom import RenderModelEntry, ModelForRendering +from sqlmesh.lsp.custom import ModelForRendering from sqlmesh.lsp.custom import AllModelsResponse, RenderModelEntry from sqlmesh.lsp.uri import URI diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 942d644689..55a6280c30 100644 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -655,8 +655,24 @@ def _diagnostic_to_lsp_diagnostic( ) -> t.Optional[types.Diagnostic]: if diagnostic.model._path is None: return None - with open(diagnostic.model._path, "r", encoding="utf-8") as file: - lines = file.readlines() + if not diagnostic.violation_range: + with open(diagnostic.model._path, "r", encoding="utf-8") as file: + lines = file.readlines() + range = types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=len(lines) - 1, character=len(lines[-1])), + ) + else: + range = types.Range( + start=types.Position( + line=diagnostic.violation_range.start.line, + character=diagnostic.violation_range.start.character, + ), + end=types.Position( + line=diagnostic.violation_range.end.line, + character=diagnostic.violation_range.end.character, + ), + ) # Get rule definition location for diagnostics link rule_location = diagnostic.rule.get_definition_location() @@ -665,10 +681,7 @@ def _diagnostic_to_lsp_diagnostic( # Use URI format to create a link for "related information" return types.Diagnostic( - range=types.Range( - start=types.Position(line=0, character=0), - end=types.Position(line=len(lines), character=len(lines[-1])), - ), + range=range, message=diagnostic.violation_msg, severity=types.DiagnosticSeverity.Error if diagnostic.violation_type == "error" diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index b43cc56751..0ddef76cad 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -4,6 +4,11 @@ from sqlmesh.core.audit import StandaloneAudit from sqlmesh.core.dialect import normalize_model_name +from sqlmesh.core.linter.helpers import ( + TokenPositionDetails, + Range as SQLMeshRange, + Position as SQLMeshPosition, +) from sqlmesh.core.model.definition import SqlModel from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget from sqlglot import exp @@ -156,12 +161,17 @@ def get_model_definitions_for_a_path( if isinstance(alias, exp.TableAlias): identifier = alias.this if isinstance(identifier, exp.Identifier): - target_range = _range_from_token_position_details( - TokenPositionDetails.from_meta(identifier.meta), read_file - ) - table_range = _range_from_token_position_details( - TokenPositionDetails.from_meta(table.this.meta), read_file - ) + target_range_sqlmesh = TokenPositionDetails.from_meta( + identifier.meta + ).to_range(read_file) + table_range_sqlmesh = TokenPositionDetails.from_meta( + table.this.meta + ).to_range(read_file) + + # Convert SQLMesh Range to LSP Range + target_range = to_lsp_range(target_range_sqlmesh) + table_range = to_lsp_range(table_range_sqlmesh) + references.append( Reference( uri=document_uri.value, # Same file @@ -203,25 +213,26 @@ def get_model_definitions_for_a_path( # Extract metadata for positioning table_meta = TokenPositionDetails.from_meta(table.this.meta) - table_range = _range_from_token_position_details(table_meta, read_file) - start_pos = table_range.start - end_pos = table_range.end + table_range_sqlmesh = table_meta.to_range(read_file) + start_pos_sqlmesh = table_range_sqlmesh.start + end_pos_sqlmesh = table_range_sqlmesh.end # If there's a catalog or database qualifier, adjust the start position catalog_or_db = table.args.get("catalog") or table.args.get("db") if catalog_or_db is not None: catalog_or_db_meta = TokenPositionDetails.from_meta(catalog_or_db.meta) - catalog_or_db_range = _range_from_token_position_details( - catalog_or_db_meta, read_file - ) - start_pos = catalog_or_db_range.start + catalog_or_db_range_sqlmesh = catalog_or_db_meta.to_range(read_file) + start_pos_sqlmesh = catalog_or_db_range_sqlmesh.start description = generate_markdown_description(referenced_model) references.append( Reference( uri=referenced_model_uri.value, - range=Range(start=start_pos, end=end_pos), + range=Range( + start=to_lsp_position(start_pos_sqlmesh), + end=to_lsp_position(end_pos_sqlmesh), + ), markdown_description=description, ) ) @@ -229,71 +240,6 @@ def get_model_definitions_for_a_path( return references -class TokenPositionDetails(PydanticModel): - """ - Details about a token's position in the source code. - - Attributes: - line (int): The line that the token ends on. - col (int): The column that the token ends on. - start (int): The start index of the token. - end (int): The ending index of the token. - """ - - line: int - col: int - start: int - end: int - - @staticmethod - def from_meta(meta: t.Dict[str, int]) -> "TokenPositionDetails": - return TokenPositionDetails( - line=meta["line"], - col=meta["col"], - start=meta["start"], - end=meta["end"], - ) - - -def _range_from_token_position_details( - token_position_details: TokenPositionDetails, read_file: t.List[str] -) -> Range: - """ - Convert a TokenPositionDetails object to a Range object. - - :param token_position_details: Details about a token's position - :param read_file: List of lines from the file - :return: A Range object representing the token's position - """ - # Convert from 1-indexed to 0-indexed for line only - end_line_0 = token_position_details.line - 1 - end_col_0 = token_position_details.col - - # Find the start line and column by counting backwards from the end position - start_pos = token_position_details.start - end_pos = token_position_details.end - - # Initialize with the end position - start_line_0 = end_line_0 - start_col_0 = end_col_0 - (end_pos - start_pos + 1) - - # If start_col_0 is negative, we need to go back to previous lines - while start_col_0 < 0 and start_line_0 > 0: - start_line_0 -= 1 - start_col_0 += len(read_file[start_line_0]) - # Account for newline character - if start_col_0 >= 0: - break - start_col_0 += 1 # For the newline character - - # Ensure we don't have negative values - start_col_0 = max(0, start_col_0) - return Range( - start=Position(line=start_line_0, character=start_col_0), - end=Position(line=end_line_0, character=end_col_0), - ) - - def get_macro_definitions_for_a_path( lsp_context: LSPContext, document_uri: URI ) -> t.List[Reference]: @@ -373,11 +319,10 @@ def get_macro_reference( try: # Get the position of the macro invocation in the source file first if hasattr(node, "meta") and node.meta: - token_details = TokenPositionDetails.from_meta(node.meta) - macro_range = _range_from_token_position_details(token_details, read_file) + macro_range = TokenPositionDetails.from_meta(node.meta).to_range(read_file) # Check if it's a built-in method - if builtin := get_built_in_macro_reference(macro_name, macro_range): + if builtin := get_built_in_macro_reference(macro_name, to_lsp_range(macro_range)): return builtin else: # Skip if we can't get the position @@ -429,7 +374,7 @@ def get_macro_reference( return Reference( uri=macro_uri.value, - range=macro_range, + range=to_lsp_range(macro_range), target_range=Range( start=Position(line=start_line - 1, character=0), end=Position(line=end_line - 1, character=get_length_of_end_line), @@ -544,3 +489,24 @@ def _position_within_range(position: Position, range: Range) -> bool: range.end.line > position.line or (range.end.line == position.line and range.end.character >= position.character) ) + + +def to_lsp_range( + range: SQLMeshRange, +) -> Range: + """ + Converts a SQLMesh Range to an LSP Range. + """ + return Range( + start=Position(line=range.start.line, character=range.start.character), + end=Position(line=range.end.line, character=range.end.character), + ) + + +def to_lsp_position( + position: SQLMeshPosition, +) -> Position: + """ + Converts a SQLMesh Position to an LSP Position. + """ + return Position(line=position.line, character=position.character) diff --git a/tests/lsp/test_diagnostics.py b/tests/lsp/test_diagnostics.py new file mode 100644 index 0000000000..96167d47e5 --- /dev/null +++ b/tests/lsp/test_diagnostics.py @@ -0,0 +1,55 @@ +from sqlmesh import Context +from sqlmesh.core.linter.helpers import read_range_from_file +from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.uri import URI + + +def test_diagnostic_on_sushi(tmp_path, copy_to_temp_path) -> None: + # Copy sushi example to a temporary directory + sushi_paths = copy_to_temp_path("examples/sushi") + sushi_path = sushi_paths[0] + + # Override the active_customers.sql file to introduce a linter violation + active_customers_path = sushi_path / "models" / "active_customers.sql" + # Replace SELECT customer_id, zip with SELECT * to trigger a linter violation + with active_customers_path.open("r") as f: + lines = f.readlines() + lines = [ + line.replace("SELECT customer_id, zip", "SELECT *") + if "SELECT customer_id, zip" in line + else line + for line in lines + ] + with active_customers_path.open("w") as f: + f.writelines(lines) + + # Override the config and turn the linter on + config_path = sushi_path / "config.py" + with config_path.open("r") as f: + lines = f.readlines() + lines = [ + line.replace("enabled=False,", "enabled=True,") if "enabled=False," in line else line + for line in lines + ] + with config_path.open("w") as f: + f.writelines(lines) + + # Load the context with the temporary sushi path + context = Context(paths=[str(sushi_path)]) + lsp_context = LSPContext(context) + + # Diagnostics should be available + active_customers_uri = URI.from_path(active_customers_path) + lsp_diagnostics = lsp_context.lint_model(active_customers_uri) + + assert len(lsp_diagnostics) > 0 + + # Get the no select star diagnostic + select_star_diagnostic = [diag for diag in lsp_diagnostics if diag.rule.name == "noselectstar"] + assert len(select_star_diagnostic) == 1 + diagnostic = select_star_diagnostic[0] + + assert diagnostic.violation_range + + contents = read_range_from_file(active_customers_path, diagnostic.violation_range) + assert contents == "*" From ff87b036b2f380befc554cd17da02d18afc398ac Mon Sep 17 00:00:00 2001 From: Christopher Giroir Date: Thu, 5 Jun 2025 11:28:33 -0700 Subject: [PATCH 0353/1056] feat: add inlay type hints for sqlmesh sql models (#4641) --- sqlmesh/lsp/hints.py | 136 ++++++++++++++++++ sqlmesh/lsp/main.py | 20 +++ tests/lsp/test_hints.py | 203 +++++++++++++++++++++++++++ vscode/extension/tests/hints.spec.ts | 47 +++++++ 4 files changed, 406 insertions(+) create mode 100644 sqlmesh/lsp/hints.py mode change 100644 => 100755 sqlmesh/lsp/main.py create mode 100644 tests/lsp/test_hints.py create mode 100644 vscode/extension/tests/hints.spec.ts diff --git a/sqlmesh/lsp/hints.py b/sqlmesh/lsp/hints.py new file mode 100644 index 0000000000..a8d56e2f31 --- /dev/null +++ b/sqlmesh/lsp/hints.py @@ -0,0 +1,136 @@ +"""Type hinting on SQLMesh models""" + +import typing as t + +from lsprotocol import types + +from sqlglot import exp +from sqlglot.expressions import Expression +from sqlglot.optimizer.normalize_identifiers import normalize_identifiers +from sqlmesh.core.model.definition import SqlModel +from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.uri import URI + + +def get_hints( + lsp_context: LSPContext, + document_uri: URI, + start_line: int, + end_line: int, +) -> t.List[types.InlayHint]: + """ + Get type hints for certain lines in a document + + Args: + lint_context: The LSP context + document_uri: The URI of the document + start_line: the starting line to get hints for + end_line: the ending line to get hints for + + Returns: + A list of hints to apply to the document + """ + path = document_uri.to_path() + if path.suffix != ".sql": + return [] + + if path not in lsp_context.map: + return [] + + file_info = lsp_context.map[path] + + # Process based on whether it's a model or standalone audit + if not isinstance(file_info, ModelTarget): + return [] + + # It's a model + model = lsp_context.context.get_model( + model_or_snapshot=file_info.names[0], raise_if_missing=False + ) + if not isinstance(model, SqlModel): + return [] + + query = model.query + dialect = model.dialect + columns_to_types = model.columns_to_types or {} + + return _get_type_hints_for_model_from_query( + query, dialect, columns_to_types, start_line, end_line + ) + + +def _get_type_hints_for_select( + expression: exp.Expression, + dialect: str, + columns_to_types: t.Dict[str, exp.DataType], + start_line: int, + end_line: int, +) -> t.List[types.InlayHint]: + hints: t.List[types.InlayHint] = [] + + for select_exp in expression.expressions: + if isinstance(select_exp, exp.Alias): + if isinstance(select_exp.this, exp.Cast): + continue + + meta = select_exp.args["alias"].meta + + elif isinstance(select_exp, exp.Column): + meta = select_exp.parts[-1].meta + else: + continue + + if "line" not in meta or "col" not in meta: + continue + + line = meta["line"] + col = meta["col"] + + # Lines from sqlglot are 1 based + line -= 1 + + if line < start_line or line > end_line: + continue + + name = select_exp.alias_or_name + data_type = columns_to_types.get(name) + + if not data_type or data_type.is_type(exp.DataType.Type.UNKNOWN): + continue + + type_label = data_type.sql(dialect) + hints.append( + types.InlayHint( + label=f"::{type_label}", + kind=types.InlayHintKind.Type, + padding_left=False, + padding_right=True, + position=types.Position(line=line, character=col), + ) + ) + + return hints + + +def _get_type_hints_for_model_from_query( + query: Expression, + dialect: str, + columns_to_types: t.Dict[str, exp.DataType], + start_line: int, + end_line: int, +) -> t.List[types.InlayHint]: + hints: t.List[types.InlayHint] = [] + try: + query = normalize_identifiers(query.copy(), dialect=dialect) + + # Return the hints for top level selects (model definition columns only) + return [ + hint + for q in query.walk(prune=lambda n: not isinstance(n, exp.SetOperation)) + if isinstance(select := q.unnest(), exp.Select) + for hint in _get_type_hints_for_select( + q, dialect, columns_to_types, start_line, end_line + ) + ] + except Exception: + return [] diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py old mode 100644 new mode 100755 index 55a6280c30..4d6601292d --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -36,6 +36,7 @@ RenderModelRequest, RenderModelResponse, ) +from sqlmesh.lsp.hints import get_hints from sqlmesh.lsp.reference import get_references, get_cte_references from sqlmesh.lsp.uri import URI from web.server.api.endpoints.lineage import column_lineage, model_lineage @@ -334,6 +335,25 @@ def hover(ls: LanguageServer, params: types.HoverParams) -> t.Optional[types.Hov ) return None + @self.server.feature(types.TEXT_DOCUMENT_INLAY_HINT) + def inlay_hint( + ls: LanguageServer, params: types.InlayHintParams + ) -> t.List[types.InlayHint]: + """Implement type hints for sql columns as inlay hints""" + try: + uri = URI(params.text_document.uri) + self._ensure_context_for_document(uri) + if self.lsp_context is None: + raise RuntimeError(f"No context found for document: {uri}") + + start_line = params.range.start.line + end_line = params.range.end.line + hints = get_hints(self.lsp_context, uri, start_line, end_line) + return hints + + except Exception as e: + return [] + @self.server.feature(types.TEXT_DOCUMENT_DEFINITION) def goto_definition( ls: LanguageServer, params: types.DefinitionParams diff --git a/tests/lsp/test_hints.py b/tests/lsp/test_hints.py new file mode 100644 index 0000000000..99851a1361 --- /dev/null +++ b/tests/lsp/test_hints.py @@ -0,0 +1,203 @@ +"""Tests for type hinting SQLMesh models""" + +import pytest + +from sqlglot import exp, parse_one + +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.hints import get_hints, _get_type_hints_for_model_from_query +from sqlmesh.lsp.uri import URI + + +@pytest.mark.fast +def test_hints() -> None: + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Find model URIs + active_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.active_customers" in info.names + ) + customer_revenue_lifetime_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customer_revenue_lifetime" in info.names + ) + customer_revenue_by_day_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customer_revenue_by_day" in info.names + ) + + active_customers_uri = URI.from_path(active_customers_path) + ac_hints = get_hints(lsp_context, active_customers_uri, start_line=0, end_line=9999) + assert len(ac_hints) == 2 + assert ac_hints[0].label == "::INT" + assert ac_hints[1].label == "::TEXT" + + customer_revenue_lifetime_uri = URI.from_path(customer_revenue_lifetime_path) + crl_hints = get_hints( + lsp_context=lsp_context, + document_uri=customer_revenue_lifetime_uri, + start_line=0, + end_line=9999, + ) + assert len(crl_hints) == 3 + assert crl_hints[0].label == "::INT" + assert crl_hints[1].label == "::DOUBLE" + assert crl_hints[2].label == "::DATE" + + customer_revenue_by_day_uri = URI.from_path(customer_revenue_by_day_path) + crbd_hints = get_hints( + lsp_context=lsp_context, + document_uri=customer_revenue_by_day_uri, + start_line=0, + end_line=9999, + ) + assert len(crbd_hints) == 1 + assert crbd_hints[0].label == "::INT" + + +@pytest.mark.fast +def test_union_hints() -> None: + query_str = """SELECT a FROM table_a UNION SELECT b FROM table_b UNION SELECT c FROM table_c""" + query = parse_one(query_str, dialect="postgres") + + result = _get_type_hints_for_model_from_query( + query=query, + dialect="postgres", + columns_to_types={ + "a": exp.DataType.build("TEXT"), + "b": exp.DataType.build("INT"), + "c": exp.DataType.build("DATE"), + }, + start_line=0, + end_line=1, + ) + + assert len(result) == 3 + assert result[0].label == "::DATE" + assert result[1].label == "::TEXT" + assert result[2].label == "::INT" + + +@pytest.mark.fast +def test_complex_hints() -> None: + query = parse_one("SELECT a, b FROM c", dialect="postgres") + + result = _get_type_hints_for_model_from_query( + query=query, + dialect="postgres", + columns_to_types={ + "a": exp.DataType.build("VARCHAR(100)"), + "b": exp.DataType.build("STRUCT>>"), + }, + start_line=0, + end_line=1, + ) + + assert len(result) == 2 + assert result[0].label == "::VARCHAR(100)" + assert result[1].label == "::STRUCT>" + + +@pytest.mark.fast +def test_simple_cast_hints() -> None: + """Don't add type hints if the expression is already a cast""" + query = parse_one("SELECT a::INT, CAST(b AS DATE), c FROM d", dialect="postgres") + + result = _get_type_hints_for_model_from_query( + query=query, + dialect="postgres", + columns_to_types={ + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("DATE"), + "c": exp.DataType.build("TEXT"), + }, + start_line=0, + end_line=1, + ) + + assert len(result) == 1 + assert result[0].label == "::TEXT" + + +@pytest.mark.fast +def test_alias_cast_hints() -> None: + """Don't add type hints if the expression is already a cast""" + query = parse_one( + "SELECT raw_a::INT AS a, CAST(raw_b AS DATE) AS b, c FROM d", dialect="postgres" + ) + + result = _get_type_hints_for_model_from_query( + query=query, + dialect="postgres", + columns_to_types={ + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("DATE"), + "c": exp.DataType.build("TEXT"), + }, + start_line=0, + end_line=1, + ) + + assert len(result) == 1 + assert result[0].label == "::TEXT" + + +@pytest.mark.fast +def test_simple_cte_hints() -> None: + """Don't add type hints if the expression is already a cast""" + query = parse_one("WITH t AS (SELECT a FROM b) SELECT a AS c FROM t", dialect="postgres") + + result = _get_type_hints_for_model_from_query( + query=query, + dialect="postgres", + columns_to_types={ + "c": exp.DataType.build("INT"), + }, + start_line=0, + end_line=1, + ) + + assert len(result) == 1 + assert result[0].label == "::INT" + + +@pytest.mark.fast +def test_cte_with_union_hints() -> None: + """Don't add type hints if the expression is already a cast""" + query = parse_one( + """WITH x AS (SELECT a FROM t), + y AS (SELECT b FROM t), + z AS (SELECT c FROM t) + SELECT a AS d FROM x + UNION + SELECT b AS e FROM y + UNION + SELECT c AS f FROM z""", + dialect="postgres", + ) + + result = _get_type_hints_for_model_from_query( + query=query, + dialect="postgres", + columns_to_types={ + "a": exp.DataType.build("TEXT"), + "b": exp.DataType.build("DATE"), + "c": exp.DataType.build("INT"), + "d": exp.DataType.build("TEXT"), + "e": exp.DataType.build("DATE"), + "f": exp.DataType.build("INT"), + }, + start_line=0, + end_line=9999, + ) + + assert len(result) == 3 + assert result[0].label == "::INT" + assert result[1].label == "::TEXT" + assert result[2].label == "::DATE" diff --git a/vscode/extension/tests/hints.spec.ts b/vscode/extension/tests/hints.spec.ts new file mode 100644 index 0000000000..f0c87a20c3 --- /dev/null +++ b/vscode/extension/tests/hints.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { startVSCode, SUSHI_SOURCE_PATH } from './utils' + +test('Model type hinting', async () => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-sushi-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + try { + const { window, close } = await startVSCode(tempDir) + + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customers_revenue_by_day model + await window + .getByRole('treeitem', { + name: 'customer_revenue_by_day.sql', + exact: true, + }) + .locator('a') + .click() + + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Wait a moment for hints to appear + await window.waitForTimeout(500) + + // Check if the hint is visible + expect(await window.locator('text="country code"::INT').count()).toBe(1) + + await close() + } finally { + await fs.remove(tempDir) + } +}) From 270aed357121d3c6c08f1df401ce0e073e70ed95 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 5 Jun 2025 15:06:28 -0700 Subject: [PATCH 0354/1056] Fix: Skip unit tests when explaining the plan (#4669) --- sqlmesh/core/context.py | 2 +- tests/core/test_context.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 45682b0209..0c00839d31 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1403,7 +1403,7 @@ def plan_builder( k: v for k, v in kwargs.items() if v is not None } - skip_tests = skip_tests or False + skip_tests = explain or skip_tests or False no_gaps = no_gaps or False skip_backfill = skip_backfill or False empty_backfill = empty_backfill or False diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 11b37f8a9a..204e675c66 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -16,6 +16,7 @@ import sqlmesh.core.constants from sqlmesh.cli.example_project import init_example_project +from sqlmesh.core.console import get_console, TerminalConsole from sqlmesh.core import dialect as d, constants as c from sqlmesh.core.config import ( load_configs, @@ -2125,3 +2126,10 @@ def test_prompt_if_uncategorized_snapshot(mocker: MockerFixture, tmp_path: Path) # False instead of respecting the default plan config value, which is True assert calls[0].kwargs["no_prompts"] == False assert context.config.plan.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) From d18de8375901a9f298e9e0eb0c16193abf78adcf Mon Sep 17 00:00:00 2001 From: Martin Burch Date: Thu, 5 Jun 2025 23:06:37 +0100 Subject: [PATCH 0355/1056] Fix: add azuresql to state sync engines (#4666) Co-authored-by: Jo <46752250+georgesittas@users.noreply.github.com> --- sqlmesh/core/config/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 0c7055a59d..e7e138c908 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -43,7 +43,7 @@ logger = logging.getLogger(__name__) -RECOMMENDED_STATE_SYNC_ENGINES = {"postgres", "gcp_postgres", "mysql", "mssql"} +RECOMMENDED_STATE_SYNC_ENGINES = {"postgres", "gcp_postgres", "mysql", "mssql", "azuresql"} FORBIDDEN_STATE_SYNC_ENGINES = { # Do not support row-level operations "spark", From acc97b17f4ff478347fde3fdde0e6e689b3234aa Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 6 Jun 2025 11:41:02 +1200 Subject: [PATCH 0356/1056] Feat(table_diff): Add option for case insensitive schema comparisons (#4671) --- sqlmesh/cli/main.py | 5 ++ sqlmesh/core/context.py | 7 +++ sqlmesh/core/table_diff.py | 62 ++++++++++++++++++++++-- sqlmesh/magics.py | 6 +++ tests/cli/test_cli.py | 32 +++++++++++++ tests/core/test_table_diff.py | 90 ++++++++++++++++++++++++++++++++++- 6 files changed, 196 insertions(+), 6 deletions(-) diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index a171928b4e..dbbd8150a0 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -938,6 +938,11 @@ def create_external_models(obj: Context, **kwargs: t.Any) -> None: multiple=True, help="Specify one or more models to data diff. Use wildcards to diff multiple models. Ex: '*' (all models with applied plan diffs), 'demo.model+' (this and downstream models), 'git:feature_branch' (models with direct modifications in this branch only)", ) +@click.option( + "--schema-diff-ignore-case", + is_flag=True, + help="If set, when performing a schema diff the case of column names is ignored when matching between the two schemas. For example, 'col_a' in the source schema and 'COL_A' in the target schema will be treated as the same column.", +) @click.pass_obj @error_handler @cli_analytics diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 0c00839d31..de5ee6ede9 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1673,6 +1673,7 @@ def table_diff( skip_grain_check: bool = False, warn_grain_check: bool = False, temp_schema: t.Optional[str] = None, + schema_diff_ignore_case: bool = False, ) -> t.List[TableDiff]: """Show a diff between two tables. @@ -1796,6 +1797,7 @@ def table_diff( show=show, temp_schema=temp_schema, skip_grain_check=skip_grain_check, + schema_diff_ignore_case=schema_diff_ignore_case, ), tasks_num=tasks_num, ) @@ -1821,6 +1823,7 @@ def table_diff( on=on, skip_columns=skip_columns, where=where, + schema_diff_ignore_case=schema_diff_ignore_case, ) ] @@ -1845,6 +1848,7 @@ def _model_diff( show: bool = True, temp_schema: t.Optional[str] = None, skip_grain_check: bool = False, + schema_diff_ignore_case: bool = False, ) -> TableDiff: self.console.start_table_diff_model_progress(model.name) @@ -1860,6 +1864,7 @@ def _model_diff( target=target, source_alias=source_alias, target_alias=target_alias, + schema_diff_ignore_case=schema_diff_ignore_case, ) if show: @@ -1883,6 +1888,7 @@ def _table_diff( model: t.Optional[Model] = None, skip_columns: t.Optional[t.List[str]] = None, where: t.Optional[str | exp.Condition] = None, + schema_diff_ignore_case: bool = False, ) -> TableDiff: if not on: raise SQLMeshError( @@ -1902,6 +1908,7 @@ def _table_diff( decimals=decimals, model_name=model.name if model else None, model_dialect=model.dialect if model else None, + schema_diff_ignore_case=schema_diff_ignore_case, ) @python_api_analytics diff --git a/sqlmesh/core/table_diff.py b/sqlmesh/core/table_diff.py index 3550251a42..ac4c9c71cc 100644 --- a/sqlmesh/core/table_diff.py +++ b/sqlmesh/core/table_diff.py @@ -36,29 +36,78 @@ class SchemaDiff(PydanticModel, frozen=True): source_alias: t.Optional[str] = None target_alias: t.Optional[str] = None model_name: t.Optional[str] = None + ignore_case: bool = False + + @property + def _comparable_source_schema(self) -> t.Dict[str, exp.DataType]: + return ( + self._lowercase_schema_names(self.source_schema) + if self.ignore_case + else self.source_schema + ) + + @property + def _comparable_target_schema(self) -> t.Dict[str, exp.DataType]: + return ( + self._lowercase_schema_names(self.target_schema) + if self.ignore_case + else self.target_schema + ) + + def _lowercase_schema_names( + self, schema: t.Dict[str, exp.DataType] + ) -> t.Dict[str, exp.DataType]: + return {c.lower(): t for c, t in schema.items()} + + def _original_column_name( + self, maybe_lowercased_column_name: str, schema: t.Dict[str, exp.DataType] + ) -> str: + if not self.ignore_case: + return maybe_lowercased_column_name + + return next(c for c in schema if c.lower() == maybe_lowercased_column_name) @property def added(self) -> t.List[t.Tuple[str, exp.DataType]]: """Added columns.""" - return [(c, t) for c, t in self.target_schema.items() if c not in self.source_schema] + return [ + (self._original_column_name(c, self.target_schema), t) + for c, t in self._comparable_target_schema.items() + if c not in self._comparable_source_schema + ] @property def removed(self) -> t.List[t.Tuple[str, exp.DataType]]: """Removed columns.""" - return [(c, t) for c, t in self.source_schema.items() if c not in self.target_schema] + return [ + (self._original_column_name(c, self.source_schema), t) + for c, t in self._comparable_source_schema.items() + if c not in self._comparable_target_schema + ] @property def modified(self) -> t.Dict[str, t.Tuple[exp.DataType, exp.DataType]]: """Columns with modified types.""" modified = {} - for column in self.source_schema.keys() & self.target_schema.keys(): - source_type = self.source_schema[column] - target_type = self.target_schema[column] + for column in self._comparable_source_schema.keys() & self._comparable_target_schema.keys(): + source_type = self._comparable_source_schema[column] + target_type = self._comparable_target_schema[column] if source_type != target_type: modified[column] = (source_type, target_type) + + if self.ignore_case: + modified = { + self._original_column_name(c, self.source_schema): dt for c, dt in modified.items() + } + return modified + @property + def has_changes(self) -> bool: + """Does the schema contain any changes at all between source and target""" + return bool(self.added or self.removed or self.modified) + class RowDiff(PydanticModel, frozen=True): """Summary statistics and a sample dataframe.""" @@ -183,6 +232,7 @@ def __init__( model_name: t.Optional[str] = None, model_dialect: t.Optional[str] = None, decimals: int = 3, + schema_diff_ignore_case: bool = False, ): if not isinstance(adapter, RowDiffMixin): raise ValueError(f"Engine {adapter} doesnt support RowDiff") @@ -198,6 +248,7 @@ def __init__( self.model_name = model_name self.model_dialect = model_dialect self.decimals = decimals + self.schema_diff_ignore_case = schema_diff_ignore_case # Support environment aliases for diff output improvement in certain cases self.source_alias = source_alias @@ -282,6 +333,7 @@ def schema_diff(self) -> SchemaDiff: source_alias=self.source_alias, target_alias=self.target_alias, model_name=self.model_name, + ignore_case=self.schema_diff_ignore_case, ) def row_diff( diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index d77120c72b..e74019a743 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -719,6 +719,11 @@ def create_external_models(self, context: Context, line: str) -> None: action="store_true", help="Warn if any selected model is missing a grain, and compute diffs for the remaining models.", ) + @argument( + "--schema-diff-ignore-case", + action="store_true", + help="If set, when performing a schema diff the case of column names is ignored when matching between the two schemas. For example, 'col_a' in the source schema and 'COL_A' in the target schema will be treated as the same column.", + ) @line_magic @pass_sqlmesh_context def table_diff(self, context: Context, line: str) -> None: @@ -741,6 +746,7 @@ def table_diff(self, context: Context, line: str) -> None: decimals=args.decimals, skip_grain_check=args.skip_grain_check, warn_grain_check=args.warn_grain_check, + schema_diff_ignore_case=args.schema_diff_ignore_case, ) @magic_arguments() diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index ef0480ecd3..0efcbd4574 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1,4 +1,5 @@ import logging +import string from contextlib import contextmanager from os import getcwd, path, remove from pathlib import Path @@ -1759,3 +1760,34 @@ def test_ignore_warnings(runner: CliRunner, tmp_path: Path) -> None: ) assert result.exit_code == 0 assert audit_warning not in result.output + + +def test_table_diff_schema_diff_ignore_case(runner: CliRunner, tmp_path: Path): + from sqlmesh.core.engine_adapter import DuckDBEngineAdapter + + create_example_project(tmp_path) + + ctx = Context(paths=tmp_path) + assert isinstance(ctx.engine_adapter, DuckDBEngineAdapter) + + ctx.engine_adapter.execute('create table t1 (id int, "naME" varchar)') + ctx.engine_adapter.execute('create table t2 (id int, "name" varchar)') + + # default behavior (case sensitive) + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "table_diff", "t1:t2", "-o", "id"], + ) + assert result.exit_code == 0 + stripped_output = "".join((x for x in result.output if x in string.printable)) + assert "Added Columns:\n name (TEXT)" in stripped_output + assert "Removed Columns:\n naME (TEXT)" in stripped_output + + # ignore case + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "table_diff", "t1:t2", "-o", "id", "--schema-diff-ignore-case"], + ) + assert result.exit_code == 0 + stripped_output = "".join((x for x in result.output if x in string.printable)) + assert "Schema Diff Between 'T1' and 'T2':\n Schemas match" in stripped_output diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index 19e258edc4..bf491d77a7 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -11,7 +11,7 @@ from sqlmesh.core.context import Context from sqlmesh.core.config import AutoCategorizationMode, CategorizerConfig, DuckDBConnectionConfig from sqlmesh.core.model import SqlModel, load_sql_based_model -from sqlmesh.core.table_diff import TableDiff +from sqlmesh.core.table_diff import TableDiff, SchemaDiff import numpy as np # noqa: TID253 from sqlmesh.utils.errors import SQLMeshError @@ -944,3 +944,91 @@ def test_data_diff_multiple_models_lacking_grain(sushi_context_fixed_date, capsy assert row_diff1.t_sample.shape == (0, 2) assert row_diff1.joined_sample.shape == (2, 3) assert row_diff1.sample.shape == (2, 4) + + +def test_schema_diff_ignore_case(): + # no changes + table_a = {"COL_A": exp.DataType.build("varchar"), "cOl_b": exp.DataType.build("int")} + table_b = {"col_a": exp.DataType.build("varchar"), "COL_b": exp.DataType.build("int")} + + diff = SchemaDiff( + source="table_a", + source_schema=table_a, + target="table_b", + target_schema=table_b, + ignore_case=True, + ) + + assert not diff.has_changes + + # added in target + table_a = {"COL_A": exp.DataType.build("varchar"), "cOl_b": exp.DataType.build("int")} + table_b = { + "col_a": exp.DataType.build("varchar"), + "COL_b": exp.DataType.build("int"), + "cOL__C": exp.DataType.build("date"), + } + + diff = SchemaDiff( + source="table_a", + source_schema=table_a, + target="table_b", + target_schema=table_b, + ignore_case=True, + ) + + assert diff.has_changes + assert len(diff.added) == 1 + assert diff.added[0] == ( + "cOL__C", + exp.DataType.build("date"), + ) # notice: case preserved on output + assert not diff.removed + assert not diff.modified + + # removed from source + table_a = { + "cOL_fo0": exp.DataType.build("float"), + "COL_A": exp.DataType.build("varchar"), + "cOl_b": exp.DataType.build("int"), + } + table_b = {"col_a": exp.DataType.build("varchar"), "COL_b": exp.DataType.build("int")} + + diff = SchemaDiff( + source="table_a", + source_schema=table_a, + target="table_b", + target_schema=table_b, + ignore_case=True, + ) + + assert diff.has_changes + assert not diff.added + assert len(diff.removed) == 1 + assert diff.removed[0] == ( + "cOL_fo0", + exp.DataType.build("float"), + ) # notice: case preserved on output + assert not diff.modified + + # column type change + table_a = {"CoL_A": exp.DataType.build("varchar"), "cOl_b": exp.DataType.build("int")} + table_b = {"col_a": exp.DataType.build("date"), "COL_b": exp.DataType.build("int")} + + diff = SchemaDiff( + source="table_a", + source_schema=table_a, + target="table_b", + target_schema=table_b, + ignore_case=True, + ) + + assert diff.has_changes + assert not diff.added + assert not diff.removed + assert diff.modified == { + "CoL_A": ( + exp.DataType.build("varchar"), + exp.DataType.build("date"), + ) # notice: source casing used on output + } From 8b218a551bc7cf0bf1a6702049e6924a306a54f5 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:26:01 +0300 Subject: [PATCH 0357/1056] Chore: Run format to fix ui style (#4672) --- vscode/extension/tests/hints.spec.ts | 76 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/vscode/extension/tests/hints.spec.ts b/vscode/extension/tests/hints.spec.ts index f0c87a20c3..d08dd43d05 100644 --- a/vscode/extension/tests/hints.spec.ts +++ b/vscode/extension/tests/hints.spec.ts @@ -5,43 +5,41 @@ import os from 'os' import { startVSCode, SUSHI_SOURCE_PATH } from './utils' test('Model type hinting', async () => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-sushi-'), - ) - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - try { - const { window, close } = await startVSCode(tempDir) - - // Wait for the models folder to be visible - await window.waitForSelector('text=models') - - // Click on the models folder - await window - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the customers_revenue_by_day model - await window - .getByRole('treeitem', { - name: 'customer_revenue_by_day.sql', - exact: true, - }) - .locator('a') - .click() - - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') - - // Wait a moment for hints to appear - await window.waitForTimeout(500) - - // Check if the hint is visible - expect(await window.locator('text="country code"::INT').count()).toBe(1) - - await close() - } finally { - await fs.remove(tempDir) - } + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + try { + const { window, close } = await startVSCode(tempDir) + + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customers_revenue_by_day model + await window + .getByRole('treeitem', { + name: 'customer_revenue_by_day.sql', + exact: true, + }) + .locator('a') + .click() + + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Wait a moment for hints to appear + await window.waitForTimeout(500) + + // Check if the hint is visible + expect(await window.locator('text="country code"::INT').count()).toBe(1) + + await close() + } finally { + await fs.remove(tempDir) + } }) From eb3287aaef96d59f866b6d57a43ea021e08599d6 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:03:43 +0300 Subject: [PATCH 0358/1056] Chore: Run the UI linter on all branches, not just main (#4673) --- .circleci/continue_config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index e2f4063124..e285d798ef 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -152,7 +152,6 @@ jobs: - image: cimg/node:20.19.0 resource_class: small steps: - - halt_unless_client - checkout - restore_cache: name: Restore pnpm Package Cache From e81e56743b3c6b894a2eae5379c5b6843674f816 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 6 Jun 2025 09:20:52 +0100 Subject: [PATCH 0359/1056] chore(vscode): add testing in gha (#4670) --- .github/dependabot.yml | 4 ++++ .github/workflows/pr.yaml | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 .github/workflows/pr.yaml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index db9ba4179a..5acbdac5d3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,7 @@ updates: directory: '/' schedule: interval: 'weekly' + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 0000000000..765aea043f --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,27 @@ +on: + push: + branches: + - main + pull_request: + branches: + - main +concurrency: + group: 'pr-${{ github.event.pull_request.number }}' + cancel-in-progress: true +jobs: + test-vscode: + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - uses: pnpm/action-setup@v4 + with: + version: latest + - name: Install dependencies + run: pnpm install + - name: Run CI + run: pnpm run ci From 906ed7a71e7b7ba6437241124c0f156e989fc1ad Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 6 Jun 2025 09:43:21 +0100 Subject: [PATCH 0360/1056] chore: update node dependencies (#4677) --- pnpm-lock.yaml | 2352 +++++++++++++++++---------------- vscode/extension/package.json | 12 +- vscode/react/package.json | 22 +- web/client/package.json | 26 +- 4 files changed, 1221 insertions(+), 1191 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7930e27ba..65b12a5f3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,12 +36,12 @@ importers: specifier: ^9.0.1 version: 9.0.1 zod: - specifier: ^3.25.48 - version: 3.25.48 + specifier: ^3.25.55 + version: 3.25.55 devDependencies: '@eslint/js': - specifier: ^9.27.0 - version: 9.27.0 + specifier: ^9.28.0 + version: 9.28.0 '@playwright/test': specifier: ^1.52.0 version: 1.52.0 @@ -61,17 +61,17 @@ importers: specifier: ^2.5.2 version: 2.5.2 '@vscode/vsce': - specifier: ^3.4.2 - version: 3.4.2 + specifier: ^3.5.0 + version: 3.5.0 esbuild: - specifier: ^0.25.4 - version: 0.25.4 + specifier: ^0.25.5 + version: 0.25.5 eslint: - specifier: ^9.27.0 - version: 9.27.0(jiti@2.4.2) + specifier: ^9.28.0 + version: 9.28.0(jiti@2.4.2) ts-loader: specifier: ^9.5.2 - version: 9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.4)) + version: 9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.5)) tsx: specifier: ^4.19.4 version: 4.19.4 @@ -79,8 +79,8 @@ importers: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.32.1 - version: 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.33.1 + version: 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) vscode/react: dependencies: @@ -92,28 +92,28 @@ importers: version: 2.2.0(react@18.3.1) '@radix-ui/react-select': specifier: ^2.2.5 - version: 2.2.5(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.2.5(@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) '@tailwindcss/postcss': - specifier: ^4.1.7 - version: 4.1.7 + specifier: ^4.1.8 + version: 4.1.8 '@tailwindcss/vite': - specifier: ^4.1.7 - version: 4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + specifier: ^4.1.8 + version: 4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)) '@tanstack/react-query': - specifier: ^5.77.2 - version: 5.77.2(react@18.3.1) + specifier: ^5.80.6 + version: 5.80.6(react@18.3.1) '@tanstack/react-router': - specifier: ^1.120.10 - version: 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.120.16 + version: 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-router-devtools': - specifier: ^1.120.13 - version: 1.120.13(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.10)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3) + specifier: ^1.120.16 + version: 1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.15)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3) '@tanstack/react-virtual': specifier: ^3.13.9 version: 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-plugin': - specifier: ^1.120.10 - version: 1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8) + specifier: ^1.120.16 + version: 1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -133,14 +133,14 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-router: - specifier: ^7.6.1 - version: 7.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.6.2 + version: 7.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) reactflow: specifier: ^11.11.4 - version: 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwindcss: - specifier: ^4.1.7 - version: 4.1.7 + specifier: ^4.1.8 + version: 4.1.8 vscode-uri: specifier: ^3.1.0 version: 3.1.0 @@ -150,16 +150,16 @@ importers: version: 10.4.0 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.3.0(@testing-library/dom@10.4.0)(@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) '@types/react': - specifier: ^18.3.22 - version: 18.3.22 + specifier: ^18.3.23 + version: 18.3.23 '@types/react-dom': specifier: ^18.3.7 - version: 18.3.7(@types/react@18.3.22) + version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react': - specifier: ^4.5.0 - version: 4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + specifier: ^4.5.1 + version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -168,10 +168,10 @@ importers: version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) vitest: - specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + specifier: ^3.2.2 + version: 3.2.2(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -188,11 +188,11 @@ importers: specifier: ^6.2.1 version: 6.2.1 '@codemirror/lang-sql': - specifier: ^6.8.0 - version: 6.8.0 + specifier: ^6.9.0 + version: 6.9.0 '@codemirror/language': - specifier: ^6.11.0 - version: 6.11.0 + specifier: ^6.11.1 + version: 6.11.1 '@codemirror/legacy-modes': specifier: ^6.5.1 version: 6.5.1 @@ -210,19 +210,19 @@ importers: version: 2.2.0(react@18.3.1) '@lit/react': specifier: ^1.0.7 - version: 1.0.7(@types/react@18.3.22) + version: 1.0.7(@types/react@18.3.23) '@radix-ui/react-context-menu': specifier: ^2.2.15 - version: 2.2.15(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.2.15(@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) '@radix-ui/react-select': specifier: ^2.2.5 - version: 2.2.5(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.2.5(@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) '@tailwindcss/container-queries': specifier: ^0.1.1 version: 0.1.1(tailwindcss@3.4.17) '@tanstack/react-query': - specifier: ^5.77.2 - version: 5.77.2(react@18.3.1) + specifier: ^5.80.6 + version: 5.80.6(react@18.3.1) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -234,7 +234,7 @@ importers: version: 2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@uiw/react-codemirror': specifier: ^4.23.12 - version: 4.23.12(@babel/runtime@7.27.1)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.23.12(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -255,7 +255,7 @@ importers: version: 18.3.1 react-dnd: specifier: ^16.0.1 - version: 16.0.1(@types/node@22.15.21)(@types/react@18.3.22)(react@18.3.1) + version: 16.0.1(@types/node@22.15.30)(@types/react@18.3.23)(react@18.3.1) react-dnd-html5-backend: specifier: ^16.0.1 version: 16.0.1 @@ -264,38 +264,38 @@ importers: version: 18.3.1(react@18.3.1) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@18.3.22)(react@18.3.1) + version: 10.1.0(@types/react@18.3.23)(react@18.3.1) react-router: - specifier: ^7.6.1 - version: 7.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.6.2 + version: 7.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-split: specifier: ^2.0.14 version: 2.0.14(react@18.3.1) reactflow: specifier: ^11.11.4 - version: 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) thememirror: specifier: ^2.0.1 - version: 2.0.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1) + version: 2.0.1(@codemirror/language@6.11.1)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1) zustand: specifier: ^5.0.5 - version: 5.0.5(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) + version: 5.0.5(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) devDependencies: '@eslint/js': - specifier: ^9.27.0 - version: 9.27.0 + specifier: ^9.28.0 + version: 9.28.0 '@playwright/test': specifier: ^1.52.0 version: 1.52.0 '@swc/core': - specifier: ^1.11.29 - version: 1.11.29(@swc/helpers@0.5.17) + specifier: ^1.11.31 + version: 1.11.31(@swc/helpers@0.5.17) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.3.0(@testing-library/dom@10.4.0)(@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) '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.0) @@ -303,23 +303,23 @@ importers: specifier: ^0.0.33 version: 0.0.33 '@types/react': - specifier: ^18.3.22 - version: 18.3.22 + specifier: ^18.3.23 + version: 18.3.23 '@types/react-dom': specifier: ^18.3.7 - version: 18.3.7(@types/react@18.3.22) + version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react-swc': - specifier: ^3.10.0 - version: 3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + specifier: ^3.10.1 + version: 3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)) ajv: specifier: ^8.17.1 version: 8.17.1 autoprefixer: specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.3) + version: 10.4.21(postcss@8.5.4) eslint: - specifier: ^9.27.0 - version: 9.27.0(jiti@2.4.2) + specifier: ^9.28.0 + version: 9.28.0(jiti@2.4.2) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -327,8 +327,8 @@ importers: specifier: ^7.9.0 version: 7.9.0(openapi-types@12.1.3) postcss: - specifier: ^8.5.3 - version: 8.5.3 + specifier: ^8.5.4 + version: 8.5.4 tailwindcss: specifier: ^3.4.17 version: 3.4.17 @@ -336,21 +336,21 @@ importers: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.32.1 - version: 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.33.1 + version: 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + version: 3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)) vitest: - specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + specifier: ^3.2.2 + version: 3.2.2(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) optionalDependencies: '@swc/core-linux-x64-gnu': - specifier: ^1.11.29 - version: 1.11.29 + specifier: ^1.11.31 + version: 1.11.31 packages: @@ -425,32 +425,32 @@ packages: resolution: {integrity: sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA==} engines: {node: '>=18.0.0'} - '@azure/msal-browser@4.12.0': - resolution: {integrity: sha512-WD1lmVWchg7wn1mI7Tr4v7QPyTwK+8Nuyje3jRpOFENLRLEBsdK8VVdTw3C+TypZmYn4cOAdj3zREnuFXgvfIA==} + '@azure/msal-browser@4.13.0': + resolution: {integrity: sha512-n2ySryLd+wHmm/0Y1mwFI4J9UXVCu2DeWKtoWNWLVcpvK2k0Ez1qIigKleUm2ZfTbfAXdue+V8htmFft0qgyGQ==} engines: {node: '>=0.8.0'} - '@azure/msal-common@15.6.0': - resolution: {integrity: sha512-EotmBz42apYGjqiIV9rDUdptaMptpTn4TdGf3JfjLvFvinSe9BJ6ywU92K9ky+t/b0ghbeTSe9RfqlgLh8f2jA==} + '@azure/msal-common@15.7.0': + resolution: {integrity: sha512-m9M5hoFoxhe/HlXNVa4qBHekrX60CVPkWzsjhKQGuzw/OPOmurosKRPDIMn8fug/E1hHI5v33DvT1LVJfItjcg==} engines: {node: '>=0.8.0'} - '@azure/msal-node@3.5.3': - resolution: {integrity: sha512-c5mifzHX5mwm5JqMIlURUyp6LEEdKF1a8lmcNRLBo0lD7zpSYPHupa4jHyhJyg9ccLwszLguZJdk2h3ngnXwNw==} + '@azure/msal-node@3.6.0': + resolution: {integrity: sha512-MRZ38Ou6l9LiRkz/968mG0czfIvD1PxMZ/3Jyz5k00ZMnhNOwv+DIliEcy//laoWDobAAq+/cz97xefCcHPgjg==} engines: {node: '>=16'} '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.27.2': - resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==} + '@babel/compat-data@7.27.5': + resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==} engines: {node: '>=6.9.0'} - '@babel/core@7.27.1': - resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==} + '@babel/core@7.27.4': + resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} engines: {node: '>=6.9.0'} - '@babel/generator@7.27.1': - resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} + '@babel/generator@7.27.5': + resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.27.2': @@ -461,8 +461,8 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.27.1': - resolution: {integrity: sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==} + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -483,12 +483,12 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.1': - resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==} + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} engines: {node: '>=6.9.0'} - '@babel/parser@7.27.2': - resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} + '@babel/parser@7.27.5': + resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} engines: {node: '>=6.0.0'} hasBin: true @@ -516,20 +516,20 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.27.1': - resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.1': - resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} + '@babel/traverse@7.27.4': + resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} engines: {node: '>=6.9.0'} - '@babel/types@7.27.1': - resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} + '@babel/types@7.27.6': + resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -544,11 +544,11 @@ packages: '@codemirror/lang-python@6.2.1': resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} - '@codemirror/lang-sql@6.8.0': - resolution: {integrity: sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==} + '@codemirror/lang-sql@6.9.0': + resolution: {integrity: sha512-xmtpWqKSgum1B1J3Ro6rf7nuPqf2+kJQg5SjrofCAcyCThOe0ihSktSoXfXuhQBnwx1QbmreBbLJM5Jru6zitg==} - '@codemirror/language@6.11.0': - resolution: {integrity: sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==} + '@codemirror/language@6.11.1': + resolution: {integrity: sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==} '@codemirror/legacy-modes@6.5.1': resolution: {integrity: sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==} @@ -572,176 +572,176 @@ packages: resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} - '@csstools/css-calc@2.1.3': - resolution: {integrity: sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==} + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-color-parser@3.0.9': - resolution: {integrity: sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==} + '@csstools/css-color-parser@3.0.10': + resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-parser-algorithms@3.0.4': - resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-tokenizer@3.0.3': - resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@esbuild/aix-ppc64@0.25.4': - resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.4': - resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.4': - resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.4': - resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.4': - resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.4': - resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.4': - resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.4': - resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.4': - resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.4': - resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.4': - resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.4': - resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.4': - resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.4': - resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.4': - resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.4': - resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.4': - resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.4': - resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.4': - resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.4': - resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.4': - resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.25.4': - resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.4': - resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.4': - resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.4': - resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -772,8 +772,8 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.27.0': - resolution: {integrity: sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==} + '@eslint/js@9.28.0': + resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': @@ -787,14 +787,14 @@ packages: '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} - '@floating-ui/core@1.7.0': - resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==} + '@floating-ui/core@1.7.1': + resolution: {integrity: sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==} - '@floating-ui/dom@1.7.0': - resolution: {integrity: sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==} + '@floating-ui/dom@1.7.1': + resolution: {integrity: sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==} - '@floating-ui/react-dom@2.1.2': - resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + '@floating-ui/react-dom@2.1.3': + resolution: {integrity: sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' @@ -808,8 +808,8 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} - '@gerrit0/mini-shiki@3.4.2': - resolution: {integrity: sha512-3jXo5bNjvvimvdbIhKGfFxSnKCX+MA8wzHv55ptzk/cx8wOzT+BRcYgj8aFN3yTiTs+zvQQiaZFr7Jce1ZG3fw==} + '@gerrit0/mini-shiki@3.5.0': + resolution: {integrity: sha512-RQ1YHbN0EsRMP62QB61jFxrgpb8VIUE+PhR8CjBarIat6b2UeYHBo2s+IL7Fny6F4FuV4S63ksSNWiImsUKR+A==} '@headlessui/react@2.2.4': resolution: {integrity: sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==} @@ -1280,26 +1280,26 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@react-aria/focus@3.20.3': - resolution: {integrity: sha512-rR5uZUMSY4xLHmpK/I8bP1V6vUNHFo33gTvrvNUsAKKqvMfa7R2nu5A6v97dr5g6tVH6xzpdkPsOJCWh90H2cw==} + '@react-aria/focus@3.20.4': + resolution: {integrity: sha512-E9M/kPYvF1fBZpkRXsKqMhvBVEyTY7vmkHeXLJo6tInKQOjYyYs0VeWlnGnxBjQIAH7J7ZKAORfTFQQHyhoueQ==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-aria/interactions@3.25.1': - resolution: {integrity: sha512-ntLrlgqkmZupbbjekz3fE/n3eQH2vhncx8gUp0+N+GttKWevx7jos11JUBjnJwb1RSOPgRUFcrluOqBp0VgcfQ==} + '@react-aria/interactions@3.25.2': + resolution: {integrity: sha512-BWyZXBT4P17b9C9HfOIT2glDFMH9nUCfQF7vZ5FEeXNBudH/8OcSbzyBUG4Dg3XPtkOem5LP59ocaizkl32Tvg==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-aria/ssr@3.9.8': - resolution: {integrity: sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==} + '@react-aria/ssr@3.9.9': + resolution: {integrity: sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==} engines: {node: '>= 12'} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-aria/utils@3.29.0': - resolution: {integrity: sha512-jSOrZimCuT1iKNVlhjIxDkAhgF7HSp3pqyT6qjg/ZoA0wfqCi/okmrMPiWSAKBnkgX93N8GYTLT3CIEO6WZe9Q==} + '@react-aria/utils@3.29.1': + resolution: {integrity: sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -1313,16 +1313,16 @@ packages: '@react-dnd/shallowequal@4.0.2': resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==} - '@react-stately/flags@3.1.1': - resolution: {integrity: sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==} + '@react-stately/flags@3.1.2': + resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==} - '@react-stately/utils@3.10.6': - resolution: {integrity: sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA==} + '@react-stately/utils@3.10.7': + resolution: {integrity: sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/shared@3.29.1': - resolution: {integrity: sha512-KtM+cDf2CXoUX439rfEhbnEdAgFZX20UP2A35ypNIawR7/PFFPjQDWyA2EnClCcW/dLWJDEPX2U8+EJff8xqmQ==} + '@react-types/shared@3.30.0': + resolution: {integrity: sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -1465,62 +1465,62 @@ packages: cpu: [x64] os: [win32] - '@secretlint/config-creator@9.3.3': - resolution: {integrity: sha512-USIKXtBIDPBt+uxssxFVqYBzSommdwXNDGwRZPGErnKWeIH58XuyqIjRTi99fYB0yAQZZ+Cv4sD2JVXCxevEew==} + '@secretlint/config-creator@9.3.4': + resolution: {integrity: sha512-GRMYfHJ+rewwB26CC3USVObqSQ/mDLXzXcUMJw/wJisPr3HDZmdsYlcsNnaAcGN+EZmvqSDkgSibQm1hyZpzbg==} engines: {node: ^14.13.1 || >=16.0.0} - '@secretlint/config-loader@9.3.3': - resolution: {integrity: sha512-t0NGpVq7fFROr/UqfxSI09UI30U7rKSGXjfKNwR0O6fMlwx2AV9RWOvLS4hDLwxxKs+ywss6DZx/wcTdtBEWxA==} + '@secretlint/config-loader@9.3.4': + resolution: {integrity: sha512-sy+yWDWh4cbAbpQYLiO39DjwNGEK1EUhTqNamLLBo163BdJP10FIWhqpe8mtGQBSBXRtxr8Hg/gc3Xe4meIoww==} engines: {node: ^14.13.1 || >=16.0.0} - '@secretlint/core@9.3.3': - resolution: {integrity: sha512-XPpchOJz591E6bqMWkY6VxtaIbSI0gY0bUeVz1gkfT6FUI0fOfJrAMWe9RhxXWraMuxokTQA8R/LFJefiK+bXg==} + '@secretlint/core@9.3.4': + resolution: {integrity: sha512-ErIVHI6CJd191qdNKuMkH3bZQo9mWJsrSg++bQx64o0WFuG5nPvkYrDK0p/lebf+iQuOnzvl5HrZU6GU9a6o+Q==} engines: {node: ^14.13.1 || >=16.0.0} - '@secretlint/formatter@9.3.3': - resolution: {integrity: sha512-kqfnbhtxcH1Ew7pboM+jCZl8CuBzVuEKuHHSkT92iasxaaq1NK37h5IIfUDbFdXizmNFe3MwAnnVU8lqK2Dvyg==} + '@secretlint/formatter@9.3.4': + resolution: {integrity: sha512-ARpoBOKz6WP3ocLITCFkR1/Lj636ugpBknylhlpc45r5aLdvmyvWAJqodlw5zmUCfgD6JXeAMf3Hi60aAiuqWQ==} engines: {node: ^14.13.1 || >=16.0.0} - '@secretlint/node@9.3.3': - resolution: {integrity: sha512-ZD1yXlzEJmFS/lq+BmgzUBB+2mQgj6kK6A//IhBop5xqAp+lXoq1vNgu7VSJ3DR+XrKrIK7YHFZXRh9aJvIjmA==} + '@secretlint/node@9.3.4': + resolution: {integrity: sha512-S0u8i+CnPmyAKtuccgot9L5cmw6DqJc0F+b3hhVIALd8kkeLt3RIXOOej15tU7N0V1ISph90Gz92V72ovsprgQ==} engines: {node: ^14.13.1 || >=16.0.0} - '@secretlint/profiler@9.3.3': - resolution: {integrity: sha512-wcVTByh+m9O1w2WAV08Po6trGsVjhRTV1UWuzVcQTTap9EjeKQLja6Xof/SIDGORD0KWooUIMAe7VPLQFPi1cQ==} + '@secretlint/profiler@9.3.4': + resolution: {integrity: sha512-99WmaHd4dClNIm5BFsG++E6frNIZ3qVwg6s804Ql/M19pDmtZOoVCl4/UuzWpwNniBqLIgn9rHQZ/iGlIW3wyw==} - '@secretlint/resolver@9.3.3': - resolution: {integrity: sha512-8N0lqD7OiI/aLK/PhKyiGh5xTlO/6TjHiOt72jnrvB9BK2QF45Mp5fivCARTKBypDiTZrOrS7blvqZ7qTnOTrA==} + '@secretlint/resolver@9.3.4': + resolution: {integrity: sha512-L1lIrcjzqcspPzZttmOvMmOFDpJTYFyRBONg94TZBWrpv4x0w5G2SYR+K7EE1SbYQAiPxw1amoXT1YRP8cZF2A==} - '@secretlint/secretlint-formatter-sarif@9.3.3': - resolution: {integrity: sha512-qH8726RFQLdD2iKXamSbBcRTSxbECDbvg0hS3aTGL0+XOmzWI7JL4tdNywMqeHzKCRLrcEJOLYWv/P/w2VdwkA==} + '@secretlint/secretlint-formatter-sarif@9.3.4': + resolution: {integrity: sha512-IpAl5gzKwpTRqoivKOTJB89l6b7uvBwjSNKzJb3oIGD9Jg3vXcQunSntvLv5XGynYtdi1NhANfEpbhavlmMSyA==} - '@secretlint/secretlint-rule-no-dotenv@9.3.3': - resolution: {integrity: sha512-Fm1uSlchskbIGuVEIYr1MnhTvUSd4GHqiRXVomH0Sli9Q0JMKElBlfS8cB165OaNGrCZ+TmmdrF/Q8sjwZYWyQ==} + '@secretlint/secretlint-rule-no-dotenv@9.3.4': + resolution: {integrity: sha512-lMSVwTrJiZ/zL9VIzpT7tMcb0ClI6u4cyJo2YKGSbuJErJG1zB4gQKtjIwCSt7px5JF6U+aFtpb9M8+s40WWCQ==} engines: {node: ^14.13.1 || >=16.0.0} - '@secretlint/secretlint-rule-preset-recommend@9.3.3': - resolution: {integrity: sha512-zT8zxh1z28Vzc9S5FVMbfWOITNikTYmajLTuX4D8lhGM3bx7xDopUJnsEtj1lAGc5WcCZ3baMJ3xCFZeDv/SAg==} + '@secretlint/secretlint-rule-preset-recommend@9.3.4': + resolution: {integrity: sha512-RvzrLNN2A0B2bYQgRSRjh2dkdaIDuhXjj4SO5bElK1iBtJNiD6VBTxSSY1P3hXYaBeva7MEF+q1PZ3cCL8XYOA==} engines: {node: ^14.13.1 || >=16.0.0} - '@secretlint/source-creator@9.3.3': - resolution: {integrity: sha512-2h6t9UfWQn7Sp6PUO+hvWK3i55tqE4H4YlmUBlL5VOjubADcO21OAtp7S05LgXE+VJfLDgUcb1hflkw0cPE1rw==} + '@secretlint/source-creator@9.3.4': + resolution: {integrity: sha512-I9ZA1gm9HJNaAhZiQdInY9VM04VTAGDV4bappVbEJzMUDnK/LTbYqfQ88RPqgCGCqa6ee8c0/j5Bn7ypweouIw==} engines: {node: ^14.13.1 || >=16.0.0} - '@secretlint/types@9.3.3': - resolution: {integrity: sha512-ehVGggPM23sHEkqQP/5HlGDK+8Xx2oRX8vF9C/fKh+TcTRWOfjCeC7QeoPxcEMXNDXfUsHK5P8DKqQEcpbiUZQ==} + '@secretlint/types@9.3.4': + resolution: {integrity: sha512-z9rdKHNeL4xa48+367RQJVw1d7/Js9HIQ+gTs/angzteM9osfgs59ad3iwVRhCGYbeUoUUDe2yxJG2ylYLaH3Q==} engines: {node: ^14.13.1 || >=16.0.0} - '@shikijs/engine-oniguruma@3.4.2': - resolution: {integrity: sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q==} + '@shikijs/engine-oniguruma@3.6.0': + resolution: {integrity: sha512-nmOhIZ9yT3Grd+2plmW/d8+vZ2pcQmo/UnVwXMUXAKTXdi+LK0S08Ancrz5tQQPkxvjBalpMW2aKvwXfelauvA==} - '@shikijs/langs@3.4.2': - resolution: {integrity: sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA==} + '@shikijs/langs@3.6.0': + resolution: {integrity: sha512-IdZkQJaLBu1LCYCwkr30hNuSDfllOT8RWYVZK1tD2J03DkiagYKRxj/pDSl8Didml3xxuyzUjgtioInwEQM/TA==} - '@shikijs/themes@3.4.2': - resolution: {integrity: sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg==} + '@shikijs/themes@3.6.0': + resolution: {integrity: sha512-Fq2j4nWr1DF4drvmhqKq8x5vVQ27VncF8XZMBuHuQMZvUSS3NBgpqfwz/FoGe36+W6PvniZ1yDlg2d4kmYDU6w==} - '@shikijs/types@3.4.2': - resolution: {integrity: sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg==} + '@shikijs/types@3.6.0': + resolution: {integrity: sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1602,68 +1602,68 @@ packages: resolution: {integrity: sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==} engines: {node: '>=10.8'} - '@swc/core-darwin-arm64@1.11.29': - resolution: {integrity: sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ==} + '@swc/core-darwin-arm64@1.11.31': + resolution: {integrity: sha512-NTEaYOts0OGSbJZc0O74xsji+64JrF1stmBii6D5EevWEtrY4wlZhm8SiP/qPrOB+HqtAihxWIukWkP2aSdGSQ==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.11.29': - resolution: {integrity: sha512-S3eTo/KYFk+76cWJRgX30hylN5XkSmjYtCBnM4jPLYn7L6zWYEPajsFLmruQEiTEDUg0gBEWLMNyUeghtswouw==} + '@swc/core-darwin-x64@1.11.31': + resolution: {integrity: sha512-THSGaSwT96JwXDwuXQ6yFBbn+xDMdyw7OmBpnweAWsh5DhZmQkALEm1DgdQO3+rrE99MkmzwAfclc0UmYro/OA==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.11.29': - resolution: {integrity: sha512-o9gdshbzkUMG6azldHdmKklcfrcMx+a23d/2qHQHPDLUPAN+Trd+sDQUYArK5Fcm7TlpG4sczz95ghN0DMkM7g==} + '@swc/core-linux-arm-gnueabihf@1.11.31': + resolution: {integrity: sha512-laKtQFnW7KHgE57Hx32os2SNAogcuIDxYE+3DYIOmDMqD7/1DCfJe6Rln2N9WcOw6HuDbDpyQavIwZNfSAa8vQ==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.11.29': - resolution: {integrity: sha512-sLoaciOgUKQF1KX9T6hPGzvhOQaJn+3DHy4LOHeXhQqvBgr+7QcZ+hl4uixPKTzxk6hy6Hb0QOvQEdBAAR1gXw==} + '@swc/core-linux-arm64-gnu@1.11.31': + resolution: {integrity: sha512-T+vGw9aPE1YVyRxRr1n7NAdkbgzBzrXCCJ95xAZc/0+WUwmL77Z+js0J5v1KKTRxw4FvrslNCOXzMWrSLdwPSA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.11.29': - resolution: {integrity: sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw==} + '@swc/core-linux-arm64-musl@1.11.31': + resolution: {integrity: sha512-Mztp5NZkyd5MrOAG+kl+QSn0lL4Uawd4CK4J7wm97Hs44N9DHGIG5nOz7Qve1KZo407Y25lTxi/PqzPKHo61zQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.11.29': - resolution: {integrity: sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA==} + '@swc/core-linux-x64-gnu@1.11.31': + resolution: {integrity: sha512-DDVE0LZcXOWwOqFU1Xi7gdtiUg3FHA0vbGb3trjWCuI1ZtDZHEQYL4M3/2FjqKZtIwASrDvO96w91okZbXhvMg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.11.29': - resolution: {integrity: sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ==} + '@swc/core-linux-x64-musl@1.11.31': + resolution: {integrity: sha512-mJA1MzPPRIfaBUHZi0xJQ4vwL09MNWDeFtxXb0r4Yzpf0v5Lue9ymumcBPmw/h6TKWms+Non4+TDquAsweuKSw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.11.29': - resolution: {integrity: sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw==} + '@swc/core-win32-arm64-msvc@1.11.31': + resolution: {integrity: sha512-RdtakUkNVAb/FFIMw3LnfNdlH1/ep6KgiPDRlmyUfd0WdIQ3OACmeBegEFNFTzi7gEuzy2Yxg4LWf4IUVk8/bg==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.11.29': - resolution: {integrity: sha512-h+NjOrbqdRBYr5ItmStmQt6x3tnhqgwbj9YxdGPepbTDamFv7vFnhZR0YfB3jz3UKJ8H3uGJ65Zw1VsC+xpFkg==} + '@swc/core-win32-ia32-msvc@1.11.31': + resolution: {integrity: sha512-hErXdCGsg7swWdG1fossuL8542I59xV+all751mYlBoZ8kOghLSKObGQTkBbuNvc0sUKWfWg1X0iBuIhAYar+w==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.11.29': - resolution: {integrity: sha512-Q8cs2BDV9wqDvqobkXOYdC+pLUSEpX/KvI0Dgfun1F+LzuLotRFuDhrvkU9ETJA6OnD2+Fn/ieHgloiKA/Mn/g==} + '@swc/core-win32-x64-msvc@1.11.31': + resolution: {integrity: sha512-5t7SGjUBMMhF9b5j17ml/f/498kiBJNf4vZFNM421UGUEETdtjPN9jZIuQrowBkoFGJTCVL/ECM4YRtTH30u/A==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.11.29': - resolution: {integrity: sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA==} + '@swc/core@1.11.31': + resolution: {integrity: sha512-mAby9aUnKRjMEA7v8cVZS9Ah4duoRBnX7X6r5qrhTxErx+68MoY1TPrVwj/66/SWN3Bl+jijqAqoB8Qx0QE34A==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1677,73 +1677,73 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@swc/types@0.1.21': - resolution: {integrity: sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==} + '@swc/types@0.1.22': + resolution: {integrity: sha512-D13mY/ZA4PPEFSy6acki9eBT/3WgjMoRqNcdpIvjaYLQ44Xk5BdaL7UkDxAh6Z9UOe7tCCp67BVmZCojYp9owg==} '@tailwindcss/container-queries@0.1.1': resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==} peerDependencies: tailwindcss: '>=3.2.0' - '@tailwindcss/node@4.1.7': - resolution: {integrity: sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==} + '@tailwindcss/node@4.1.8': + resolution: {integrity: sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==} - '@tailwindcss/oxide-android-arm64@4.1.7': - resolution: {integrity: sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==} + '@tailwindcss/oxide-android-arm64@4.1.8': + resolution: {integrity: sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.7': - resolution: {integrity: sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==} + '@tailwindcss/oxide-darwin-arm64@4.1.8': + resolution: {integrity: sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.7': - resolution: {integrity: sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==} + '@tailwindcss/oxide-darwin-x64@4.1.8': + resolution: {integrity: sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.7': - resolution: {integrity: sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==} + '@tailwindcss/oxide-freebsd-x64@4.1.8': + resolution: {integrity: sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': - resolution: {integrity: sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8': + resolution: {integrity: sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': - resolution: {integrity: sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.8': + resolution: {integrity: sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.7': - resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.8': + resolution: {integrity: sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.7': - resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.8': + resolution: {integrity: sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.7': - resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==} + '@tailwindcss/oxide-linux-x64-musl@4.1.8': + resolution: {integrity: sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.7': - resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==} + '@tailwindcss/oxide-wasm32-wasi@4.1.8': + resolution: {integrity: sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -1754,27 +1754,27 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': - resolution: {integrity: sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.8': + resolution: {integrity: sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.7': - resolution: {integrity: sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.8': + resolution: {integrity: sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.7': - resolution: {integrity: sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==} + '@tailwindcss/oxide@4.1.8': + resolution: {integrity: sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==} engines: {node: '>= 10'} - '@tailwindcss/postcss@4.1.7': - resolution: {integrity: sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw==} + '@tailwindcss/postcss@4.1.8': + resolution: {integrity: sha512-vB/vlf7rIky+w94aWMw34bWW1ka6g6C3xIOdICKX2GC0VcLtL6fhlLiafF0DVIwa9V6EHz8kbWMkS2s2QvvNlw==} - '@tailwindcss/vite@4.1.7': - resolution: {integrity: sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==} + '@tailwindcss/vite@4.1.8': + resolution: {integrity: sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A==} peerDependencies: vite: ^5.2.0 || ^6 @@ -1782,31 +1782,31 @@ packages: resolution: {integrity: sha512-K7JJNrRVvyjAVnbXOH2XLRhFXDkeP54Kt2P4FR1Kl2KDGlIbkua5VqZQD2rot3qaDrpufyUa63nuLai1kOLTsQ==} engines: {node: '>=12'} - '@tanstack/query-core@5.77.2': - resolution: {integrity: sha512-1lqJwPsR6GX6nZFw06erRt518O19tWU6Q+x0fJUygl4lxHCYF2nhzBPwLKk2NPjYOrpR0K567hxPc5K++xDe9Q==} + '@tanstack/query-core@5.80.6': + resolution: {integrity: sha512-nl7YxT/TAU+VTf+e2zTkObGTyY8YZBMnbgeA1ee66lIVqzKlYursAII6z5t0e6rXgwUMJSV4dshBTNacNpZHbQ==} - '@tanstack/react-query@5.77.2': - resolution: {integrity: sha512-BRHxWdy1mHmgAcYA/qy2IPLylT81oebLgkm9K85viN2Qol/Vq48t1dzDFeDIVQjTWDV96AmqsLNPlH5HjyKCxA==} + '@tanstack/react-query@5.80.6': + resolution: {integrity: sha512-izX+5CnkpON3NQGcEm3/d7LfFQNo9ZpFtX2QsINgCYK9LT2VCIdi8D3bMaMSNhrAJCznRoAkFic76uvLroALBw==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.120.13': - resolution: {integrity: sha512-HP9Qd1SBzN/TaAw5TPeD+9xCytBTZy+HM1kGRimXqSy8aOKlOkPa4fRcPyA+OWpB9CgiLKHDu6UrVVb2ZBAd6w==} + '@tanstack/react-router-devtools@1.120.16': + resolution: {integrity: sha512-DWXmMLknVJJMGP2k5yeUWBDhJOHbV2jVfnZKxtGzA64xXhwDFgU9qpodcmYSq3+kHWsKrd7iX0wc7d27rGwGDA==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.120.13 + '@tanstack/react-router': ^1.120.16 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.120.10': - resolution: {integrity: sha512-+SE3CAP/YYMNt2jhPsT49LhPyJcABaTzrowDfY/Ru6osR+byNlxbooqhXLIvtxc5WsMLY/aB8TpTcTft1W5IPA==} + '@tanstack/react-router@1.120.16': + resolution: {integrity: sha512-bBZ+H9sBYcihsj1BkcxD/VtVxa5ZGmCEeYXlCAgWQ9fWc1kN+tA0/M2uvjLFuhsESDmv5U45TittBtHAwAgEAA==} engines: {node: '>=12'} peerDependencies: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-store@0.7.0': - resolution: {integrity: sha512-S/Rq17HaGOk+tQHV/yrePMnG1xbsKZIl/VsNWnNXt4XW+tTY8dTlvpJH2ZQ3GRALsusG5K6Q3unAGJ2pd9W/Ng==} + '@tanstack/react-store@0.7.1': + resolution: {integrity: sha512-qUTEKdId6QPWGiWyKAPf/gkN29scEsz6EUSJ0C3HgLMgaqTAyBsQ2sMCfGVcqb+kkhEXAdjleCgH6LAPD6f2sA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1824,15 +1824,15 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.120.10': - resolution: {integrity: sha512-AmEJAYt+6w/790zTnfddVhnK1QJCnd96H4xg1aD65Oohc8+OTQBxgWky/wzqwhHRdkdsBgRT7iWac9x5Y8UrQA==} + '@tanstack/router-core@1.120.15': + resolution: {integrity: sha512-soLj+mEuvSxAVFK/3b85IowkkvmSuQL6J0RSIyKJFGFgy0CmUzpcBGEO99+JNWvvvzHgIoY4F4KtLIN+rvFSFA==} engines: {node: '>=12'} - '@tanstack/router-devtools-core@1.120.13': - resolution: {integrity: sha512-H9Yt6BXUfdcKURo9FMBw4idZVCggWlKaxNdJjfu8Or7Wu6Zi58kRt17RJ4ys/n6D1WznyteQ3ya2JZpan5n2NA==} + '@tanstack/router-devtools-core@1.120.15': + resolution: {integrity: sha512-AT9obPHKpJqnHMbwshozSy6sApg5LchiAll3blpS3MMDybUCidYHrdhe9MZJLmlC99IQiEGmuZERP3VRcuPNHg==} engines: {node: '>=12'} peerDependencies: - '@tanstack/router-core': ^1.120.13 + '@tanstack/router-core': ^1.120.15 csstype: ^3.0.10 solid-js: '>=1.9.5' tiny-invariant: ^1.3.3 @@ -1840,21 +1840,21 @@ packages: csstype: optional: true - '@tanstack/router-generator@1.120.10': - resolution: {integrity: sha512-oUhzCAeIDfupXGwIf3oMqqdSRw62fTtvdUhMLfnTimGMuSp1ErxIj52PeyVGFAFr/ORP85ZxNqRpAecZal247A==} + '@tanstack/router-generator@1.120.16': + resolution: {integrity: sha512-ekCcIPk76Nj17ZOpiRmyDhZNmE06tPSQvu19TWQi6797dDChDCozed7cQbdn8qkuo84SjBhl0r087GTUkksbDg==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.120.10 + '@tanstack/react-router': ^1.120.16 peerDependenciesMeta: '@tanstack/react-router': optional: true - '@tanstack/router-plugin@1.120.10': - resolution: {integrity: sha512-jAaL0Vh8Kuy+wFUEUiKSoCiGNljXChldFsuvcqnTo4/4qWtKgHlQminGMmx2z5eGZ0EsIfP+NSAMbCpYPFvEng==} + '@tanstack/router-plugin@1.120.16': + resolution: {integrity: sha512-p++CuH8FHFToueAuxwyd+vkRm5JNhJoCl47qxZLHa91iZvUwF9i0AbGYVWKXObo0EhYdVIGn4xhisPcOq872Eg==} engines: {node: '>=12'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.120.10 + '@tanstack/react-router': ^1.120.16 vite: '>=5.0.0 || >=6.0.0' vite-plugin-solid: ^2.11.2 webpack: '>=5.92.0' @@ -1874,8 +1874,8 @@ packages: resolution: {integrity: sha512-Dng4y+uLR9b5zPGg7dHReHOTHQa6x+G6nCoZshsDtWrYsrdCcJEtLyhwZ5wG8OyYS6dVr/Cn+E5Bd2b6BhJ89w==} engines: {node: '>=12'} - '@tanstack/store@0.7.0': - resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==} + '@tanstack/store@0.7.1': + resolution: {integrity: sha512-PjUQKXEXhLYj2X5/6c1Xn/0/qKY0IVFxTJweopRfF26xfjVyb14yALydJrHupDh3/d+1WKmfEgZPBVCmDkzzwg==} '@tanstack/table-core@8.21.3': resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} @@ -1947,6 +1947,9 @@ packages: '@types/babel__traverse@7.20.7': resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/command-line-args@5.2.3': resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} @@ -2049,6 +2052,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/es-aggregate-error@1.0.6': resolution: {integrity: sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==} @@ -2064,6 +2070,9 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} @@ -2094,11 +2103,11 @@ packages: '@types/node@20.11.25': resolution: {integrity: sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==} - '@types/node@20.17.50': - resolution: {integrity: sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==} + '@types/node@20.19.0': + resolution: {integrity: sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==} - '@types/node@22.15.21': - resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==} + '@types/node@22.15.30': + resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2114,8 +2123,8 @@ packages: peerDependencies: '@types/react': ^18.0.0 - '@types/react@18.3.22': - resolution: {integrity: sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==} + '@types/react@18.3.23': + resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} '@types/sarif@2.1.7': resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} @@ -2132,51 +2141,63 @@ packages: '@types/vscode@1.96.0': resolution: {integrity: sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==} - '@typescript-eslint/eslint-plugin@8.32.1': - resolution: {integrity: sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==} + '@typescript-eslint/eslint-plugin@8.33.1': + resolution: {integrity: sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + '@typescript-eslint/parser': ^8.33.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.32.1': - resolution: {integrity: sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==} + '@typescript-eslint/parser@8.33.1': + resolution: {integrity: sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.32.1': - resolution: {integrity: sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==} + '@typescript-eslint/project-service@8.33.1': + resolution: {integrity: sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@8.33.1': + resolution: {integrity: sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.33.1': + resolution: {integrity: sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@8.32.1': - resolution: {integrity: sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==} + '@typescript-eslint/type-utils@8.33.1': + resolution: {integrity: sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.32.1': - resolution: {integrity: sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==} + '@typescript-eslint/types@8.33.1': + resolution: {integrity: sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.32.1': - resolution: {integrity: sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==} + '@typescript-eslint/typescript-estree@8.33.1': + resolution: {integrity: sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.32.1': - resolution: {integrity: sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==} + '@typescript-eslint/utils@8.33.1': + resolution: {integrity: sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.32.1': - resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==} + '@typescript-eslint/visitor-keys@8.33.1': + resolution: {integrity: sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typespec/ts-http-runtime@0.2.2': @@ -2215,45 +2236,45 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitejs/plugin-react-swc@3.10.0': - resolution: {integrity: sha512-ZmkdHw3wo/o/Rk05YsXZs/DJAfY2CdQ5DUAjoWji+PEr+hYADdGMCGgEAILbiKj+CjspBTuTACBcWDrmC8AUfw==} + '@vitejs/plugin-react-swc@3.10.1': + resolution: {integrity: sha512-FmQvN3yZGyD9XW6IyxE86Kaa/DnxSsrDQX1xCR1qojNpBLaUop+nLYFvhCkJsq8zOupNjCRA9jyhPGOJsSkutA==} peerDependencies: vite: ^4 || ^5 || ^6 - '@vitejs/plugin-react@4.5.0': - resolution: {integrity: sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==} + '@vitejs/plugin-react@4.5.1': + resolution: {integrity: sha512-uPZBqSI0YD4lpkIru6M35sIfylLGTyhGHvDZbNLuMA73lMlwJKz5xweH7FajfcCAc2HnINciejA9qTz0dr0M7A==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - '@vitest/expect@3.1.4': - resolution: {integrity: sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==} + '@vitest/expect@3.2.2': + resolution: {integrity: sha512-ipHw0z669vEMjzz3xQE8nJX1s0rQIb7oEl4jjl35qWTwm/KIHERIg/p/zORrjAaZKXfsv7IybcNGHwhOOAPMwQ==} - '@vitest/mocker@3.1.4': - resolution: {integrity: sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==} + '@vitest/mocker@3.2.2': + resolution: {integrity: sha512-jKojcaRyIYpDEf+s7/dD3LJt53c0dPfp5zCPXz9H/kcGrSlovU/t1yEaNzM9oFME3dcd4ULwRI/x0Po1Zf+LTw==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@3.1.4': - resolution: {integrity: sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==} + '@vitest/pretty-format@3.2.2': + resolution: {integrity: sha512-FY4o4U1UDhO9KMd2Wee5vumwcaHw7Vg4V7yR4Oq6uK34nhEJOmdRYrk3ClburPRUA09lXD/oXWZ8y/Sdma0aUQ==} - '@vitest/runner@3.1.4': - resolution: {integrity: sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==} + '@vitest/runner@3.2.2': + resolution: {integrity: sha512-GYcHcaS3ejGRZYed2GAkvsjBeXIEerDKdX3orQrBJqLRiea4NSS9qvn9Nxmuy1IwIB+EjFOaxXnX79l8HFaBwg==} - '@vitest/snapshot@3.1.4': - resolution: {integrity: sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==} + '@vitest/snapshot@3.2.2': + resolution: {integrity: sha512-aMEI2XFlR1aNECbBs5C5IZopfi5Lb8QJZGGpzS8ZUHML5La5wCbrbhLOVSME68qwpT05ROEEOAZPRXFpxZV2wA==} - '@vitest/spy@3.1.4': - resolution: {integrity: sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==} + '@vitest/spy@3.2.2': + resolution: {integrity: sha512-6Utxlx3o7pcTxvp0u8kUiXtRFScMrUg28KjB3R2hon7w4YqOFAEA9QwzPVVS1QNL3smo4xRNOpNZClRVfpMcYg==} - '@vitest/utils@3.1.4': - resolution: {integrity: sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==} + '@vitest/utils@3.2.2': + resolution: {integrity: sha512-qJYMllrWpF/OYfWHP32T31QCaLa3BAzT/n/8mNGhPdVcjY+JYazQFO1nsJvXU12Kp1xMpNY4AGuljPTNjQve6A==} '@vscode/python-extension@1.0.5': resolution: {integrity: sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==} @@ -2316,8 +2337,8 @@ packages: '@vscode/vsce-sign@2.0.5': resolution: {integrity: sha512-GfYWrsT/vypTMDMgWDm75iDmAOMe7F71sZECJ+Ws6/xyIfmB3ELVnVN+LwMFAvmXY+e6eWhR2EzNGF/zAhWY3Q==} - '@vscode/vsce@3.4.2': - resolution: {integrity: sha512-U2gC7GiQc22nxRpWH4cdW16rRr5u9w+Bjsjm8g8mEjY4aeOG1U6/3XNGq+ElwdeoT8jAyhBmBAuYG7INcSe/6A==} + '@vscode/vsce@3.5.0': + resolution: {integrity: sha512-2Eb6fBh8OzNhWqviCjeUPA1MW+d2GCb1QlVxrpOR8lrLHGk8x7HD4LbfELnZPyOz2X33Myz9FE9t4LwYbmeMRg==} engines: {node: '>= 20'} hasBin: true @@ -2442,10 +2463,6 @@ packages: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -2558,9 +2575,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - binaryextensions@4.19.0: - resolution: {integrity: sha512-DRxnVbOi/1OgA5pA9EDiRT8gvVYeqfuN7TmPfLyt6cyho3KbHCi3EtDQf39TTmGDrR5dZ9CspdXhPkL/j/WGbg==} - engines: {node: '>=0.8'} + binaryextensions@6.11.0: + resolution: {integrity: sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==} + engines: {node: '>=4'} bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -2584,8 +2601,8 @@ packages: browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - browserslist@4.24.5: - resolution: {integrity: sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==} + browserslist@4.25.0: + resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2641,8 +2658,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001718: - resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} + caniuse-lite@1.0.30001721: + resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2655,10 +2672,6 @@ packages: resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} engines: {node: '>=12'} - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} engines: {node: '>=8'} @@ -2746,16 +2759,10 @@ packages: codemirror@6.0.1: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -3019,8 +3026,12 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - electron-to-chromium@1.5.157: - resolution: {integrity: sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==} + editions@6.21.0: + resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==} + engines: {node: '>=4'} + + electron-to-chromium@1.5.165: + resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==} elkjs@0.8.2: resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} @@ -3059,12 +3070,12 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - es-abstract@1.23.10: - resolution: {integrity: sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==} + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} - es-aggregate-error@1.0.13: - resolution: {integrity: sha512-KkzhUUuD2CUMqEc8JEqsXEMDHzDPE8RCjZeUBitsnB1eNcAJWQPiciKsMXe3Yytj4Flw1XLl46Qcf9OxvZha7A==} + es-aggregate-error@1.0.14: + resolution: {integrity: sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA==} engines: {node: '>= 0.4'} es-define-property@1.0.1: @@ -3093,8 +3104,8 @@ packages: es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} - esbuild@0.25.4: - resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} engines: {node: '>=18'} hasBin: true @@ -3102,10 +3113,6 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -3126,8 +3133,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.27.0: - resolution: {integrity: sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==} + eslint@9.28.0: + resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -3222,8 +3229,8 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.4.4: - resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + fdir@6.4.5: + resolution: {integrity: sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -3273,8 +3280,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.2: - resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + form-data@4.0.3: + resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} engines: {node: '>= 6'} fraction.js@4.3.7: @@ -3420,10 +3427,6 @@ packages: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3507,8 +3510,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.4: - resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} immediate@3.0.6: @@ -3635,6 +3638,10 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -3731,9 +3738,9 @@ packages: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} engines: {node: '>=8'} - istextorbinary@6.0.0: - resolution: {integrity: sha512-4j3UqQCa06GAf6QHlN3giz2EeFU7qc6Q5uB/aY7Gmb3xmLDLepDOtsZqkb4sCfJgFvTbLUinNw0kHgHs8XOHoQ==} - engines: {node: '>=10'} + istextorbinary@9.5.0: + resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==} + engines: {node: '>=4'} jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -4551,8 +4558,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + postcss@8.5.4: + resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} engines: {node: ^10 || ^12 || >=14} prebuild-install@7.1.3: @@ -4659,8 +4666,8 @@ packages: '@types/react': optional: true - react-remove-scroll@2.7.0: - resolution: {integrity: sha512-sGsQtcjMqdQyijAHytfGEELB8FufGbfXIsvUTe+NLx1GDRJCXtCFLBLUI1eyZCKXXvbEU2C6gai0PZKoIE9Vbg==} + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} engines: {node: '>=10'} peerDependencies: '@types/react': '*' @@ -4669,8 +4676,8 @@ packages: '@types/react': optional: true - react-router@7.6.1: - resolution: {integrity: sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ==} + react-router@7.6.2: + resolution: {integrity: sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -4835,8 +4842,8 @@ packages: resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} engines: {node: '>= 10.13.0'} - secretlint@9.3.3: - resolution: {integrity: sha512-JTIsI8BEon8Oo6P7YvGq3I3qCZuYgCvekU8qr4OYyvo6N/wHGg4JMruT5MVkxh3q0diX11xsqaptmeTP5/wNxQ==} + secretlint@9.3.4: + resolution: {integrity: sha512-iNOzgMX/+W1SQNW/TW6eikGChyaPiazr2AEXjzjpoB0R6QJEulvlwhn0KLT1/xjPfdYrk3yiXZM40csUqET8uQ==} engines: {node: ^14.13.1 || >=16.0.0} hasBin: true @@ -5007,6 +5014,10 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -5085,10 +5096,6 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -5132,8 +5139,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@4.1.7: - resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==} + tailwindcss@4.1.8: + resolution: {integrity: sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==} tapable@2.2.2: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} @@ -5170,8 +5177,8 @@ packages: uglify-js: optional: true - terser@5.39.2: - resolution: {integrity: sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==} + terser@5.41.0: + resolution: {integrity: sha512-H406eLPXpZbAX14+B8psIuvIr8+3c+2hkuYzpMkoE0ij+NdsVATbA78vb8neA/eqrj7rywa2pIkdmWRsXW6wmw==} engines: {node: '>=10'} hasBin: true @@ -5182,9 +5189,9 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - textextensions@5.16.0: - resolution: {integrity: sha512-7D/r3s6uPZyU//MCYrX6I14nzauDwJ5CxazouuRGNuvSCihW87ufN6VLoROLCrHg6FblLuJrT6N2BVaPVzqElw==} - engines: {node: '>=0.8'} + textextensions@6.11.0: + resolution: {integrity: sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==} + engines: {node: '>=4'} thememirror@2.0.1: resolution: {integrity: sha512-d5i6FVvWWPkwrm4cHLI3t9AT1OrkAt7Ig8dtdYSofgF7C/eiyNuq6zQzSTusWTde3jpW9WLvA9J/fzNKMUsd0w==} @@ -5216,16 +5223,16 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} - tinypool@1.0.2: - resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + tinypool@1.1.0: + resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==} engines: {node: ^18.0.0 || >=20.0.0} tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} tldts-core@6.1.86: @@ -5339,8 +5346,8 @@ packages: typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} - typedoc-plugin-markdown@4.6.3: - resolution: {integrity: sha512-86oODyM2zajXwLs4Wok2mwVEfCwCnp756QyhLGX2IfsdRYr1DXLCgJgnLndaMUjJD7FBhnLk2okbNE9PdLxYRw==} + typedoc-plugin-markdown@4.6.4: + resolution: {integrity: sha512-AnbToFS1T1H+n40QbO2+i0wE6L+55rWnj7zxnM1r781+2gmhMF2dB6dzFpaylWLQYkbg4D1Y13sYnne/6qZwdw==} engines: {node: '>= 18'} peerDependencies: typedoc: 0.28.x @@ -5352,8 +5359,8 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x - typescript-eslint@8.32.1: - resolution: {integrity: sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==} + typescript-eslint@8.33.1: + resolution: {integrity: sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -5381,9 +5388,6 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -5417,8 +5421,8 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unplugin@2.3.4: - resolution: {integrity: sha512-m4PjxTurwpWfpMomp8AptjD5yj8qEZN5uQjjGM3TAs9MWWD2tXSSNNj6jGR2FoVGod4293ytyV6SwBbertfyJg==} + unplugin@2.3.5: + resolution: {integrity: sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==} engines: {node: '>=18.12.0'} update-browserslist-db@1.1.3: @@ -5479,18 +5483,22 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - validator@13.15.0: - resolution: {integrity: sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==} + validator@13.15.15: + resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} engines: {node: '>= 0.10'} + version-range@4.14.0: + resolution: {integrity: sha512-gjb0ARm9qlcBAonU4zPwkl9ecKkas+tC2CGwFfptTCWWIVTWY1YUbT2zZKsOAF1jR/tNxxyLwwG0cb42XlYcTg==} + engines: {node: '>=4'} + vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-node@3.1.4: - resolution: {integrity: sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==} + vite-node@3.2.2: + resolution: {integrity: sha512-Xj/jovjZvDXOq2FgLXu8NsY4uHUMWtzVmMC2LkCu9HWdr9Qu1Is5sanX3Z4jOFKdohfaWDnEJWp9pRP0vVpAcA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true @@ -5539,16 +5547,16 @@ packages: yaml: optional: true - vitest@3.1.4: - resolution: {integrity: sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==} + vitest@3.2.2: + resolution: {integrity: sha512-fyNn/Rp016Bt5qvY0OQvIUCwW2vnaEBLxP42PmKbNIoasSYjML+8xyeADOPvBe+Xfl/ubIw4og7Lt9jflRsCNw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.1.4 - '@vitest/ui': 3.1.4 + '@vitest/browser': 3.2.2 + '@vitest/ui': 3.2.2 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -5609,8 +5617,8 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - webpack-sources@3.3.0: - resolution: {integrity: sha512-77R0RDmJfj9dyv5p3bM5pOHa+X8/ZkO9c7kpDstigkC4nIDobadsfSGCwB4bKhMVxqAok8tajaoR8rirM7+VFQ==} + webpack-sources@3.3.2: + resolution: {integrity: sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==} engines: {node: '>=10.13.0'} webpack-virtual-modules@0.6.2: @@ -5769,8 +5777,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@3.25.48: - resolution: {integrity: sha512-0X1mz8FtgEIvaxGjdIImYpZEaZMrund9pGXm3M6vM7Reba0e2eI71KPjSCGXBfwKDPwPoywf6waUKc3/tFvX2Q==} + zod@3.25.55: + resolution: {integrity: sha512-219huNnkSLQnLsQ3uaRjXsxMrVm5C9W3OOpEVt2k5tvMKuA8nBSu38e0B//a+he9Iq2dvmk2VyYVlHqiHa4YBA==} zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} @@ -5842,10 +5850,10 @@ snapshots: '@asamuzakjp/css-color@3.2.0': dependencies: - '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-color-parser': 3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 '@asyncapi/specs@6.8.1': @@ -5915,8 +5923,8 @@ snapshots: '@azure/core-tracing': 1.2.0 '@azure/core-util': 1.12.0 '@azure/logger': 1.2.0 - '@azure/msal-browser': 4.12.0 - '@azure/msal-node': 3.5.3 + '@azure/msal-browser': 4.13.0 + '@azure/msal-node': 3.6.0 open: 10.1.2 tslib: 2.8.1 transitivePeerDependencies: @@ -5929,15 +5937,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/msal-browser@4.12.0': + '@azure/msal-browser@4.13.0': dependencies: - '@azure/msal-common': 15.6.0 + '@azure/msal-common': 15.7.0 - '@azure/msal-common@15.6.0': {} + '@azure/msal-common@15.7.0': {} - '@azure/msal-node@3.5.3': + '@azure/msal-node@3.6.0': dependencies: - '@azure/msal-common': 15.6.0 + '@azure/msal-common': 15.7.0 jsonwebtoken: 9.0.2 uuid: 8.3.2 @@ -5947,20 +5955,20 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.27.2': {} + '@babel/compat-data@7.27.5': {} - '@babel/core@7.27.1': + '@babel/core@7.27.4': dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.1 + '@babel/generator': 7.27.5 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.1(@babel/core@7.27.1) - '@babel/helpers': 7.27.1 - '@babel/parser': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.27.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 convert-source-map: 2.0.0 debug: 4.4.1(supports-color@8.1.1) gensync: 1.0.0-beta.2 @@ -5969,35 +5977,35 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.27.1': + '@babel/generator@7.27.5': dependencies: - '@babel/parser': 7.27.2 - '@babel/types': 7.27.1 + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.27.2 + '@babel/compat-data': 7.27.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.24.5 + browserslist: 4.25.0 lru-cache: 5.1.1 semver: 6.3.1 '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.1(@babel/core@7.27.1)': + '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.27.4 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.1 + '@babel/traverse': 7.27.4 transitivePeerDependencies: - supports-color @@ -6009,56 +6017,56 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.27.1': + '@babel/helpers@7.27.6': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.27.1 + '@babel/types': 7.27.6 - '@babel/parser@7.27.2': + '@babel/parser@7.27.5': dependencies: - '@babel/types': 7.27.1 + '@babel/types': 7.27.6 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.1)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.1)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.1)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/runtime@7.27.1': {} + '@babel/runtime@7.27.6': {} '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.2 - '@babel/types': 7.27.1 + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 - '@babel/traverse@7.27.1': + '@babel/traverse@7.27.4': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.1 - '@babel/parser': 7.27.2 + '@babel/generator': 7.27.5 + '@babel/parser': 7.27.5 '@babel/template': 7.27.2 - '@babel/types': 7.27.1 + '@babel/types': 7.27.6 debug: 4.4.1(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/types@7.27.1': + '@babel/types@7.27.6': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -6067,14 +6075,14 @@ snapshots: '@codemirror/autocomplete@6.18.6': dependencies: - '@codemirror/language': 6.11.0 + '@codemirror/language': 6.11.1 '@codemirror/state': 6.5.2 '@codemirror/view': 6.37.1 '@lezer/common': 1.2.3 '@codemirror/commands@6.8.1': dependencies: - '@codemirror/language': 6.11.0 + '@codemirror/language': 6.11.1 '@codemirror/state': 6.5.2 '@codemirror/view': 6.37.1 '@lezer/common': 1.2.3 @@ -6082,21 +6090,21 @@ snapshots: '@codemirror/lang-python@6.2.1': dependencies: '@codemirror/autocomplete': 6.18.6 - '@codemirror/language': 6.11.0 + '@codemirror/language': 6.11.1 '@codemirror/state': 6.5.2 '@lezer/common': 1.2.3 '@lezer/python': 1.1.18 - '@codemirror/lang-sql@6.8.0': + '@codemirror/lang-sql@6.9.0': dependencies: '@codemirror/autocomplete': 6.18.6 - '@codemirror/language': 6.11.0 + '@codemirror/language': 6.11.1 '@codemirror/state': 6.5.2 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 - '@codemirror/language@6.11.0': + '@codemirror/language@6.11.1': dependencies: '@codemirror/state': 6.5.2 '@codemirror/view': 6.37.1 @@ -6107,7 +6115,7 @@ snapshots: '@codemirror/legacy-modes@6.5.1': dependencies: - '@codemirror/language': 6.11.0 + '@codemirror/language': 6.11.1 '@codemirror/lint@6.8.5': dependencies: @@ -6127,7 +6135,7 @@ snapshots: '@codemirror/theme-one-dark@6.1.2': dependencies: - '@codemirror/language': 6.11.0 + '@codemirror/language': 6.11.1 '@codemirror/state': 6.5.2 '@codemirror/view': 6.37.1 '@lezer/highlight': 1.2.1 @@ -6141,102 +6149,102 @@ snapshots: '@csstools/color-helpers@5.0.2': {} - '@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-color-parser@3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/color-helpers': 5.0.2 - '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-tokenizer@3.0.3': {} + '@csstools/css-tokenizer@3.0.4': {} - '@esbuild/aix-ppc64@0.25.4': + '@esbuild/aix-ppc64@0.25.5': optional: true - '@esbuild/android-arm64@0.25.4': + '@esbuild/android-arm64@0.25.5': optional: true - '@esbuild/android-arm@0.25.4': + '@esbuild/android-arm@0.25.5': optional: true - '@esbuild/android-x64@0.25.4': + '@esbuild/android-x64@0.25.5': optional: true - '@esbuild/darwin-arm64@0.25.4': + '@esbuild/darwin-arm64@0.25.5': optional: true - '@esbuild/darwin-x64@0.25.4': + '@esbuild/darwin-x64@0.25.5': optional: true - '@esbuild/freebsd-arm64@0.25.4': + '@esbuild/freebsd-arm64@0.25.5': optional: true - '@esbuild/freebsd-x64@0.25.4': + '@esbuild/freebsd-x64@0.25.5': optional: true - '@esbuild/linux-arm64@0.25.4': + '@esbuild/linux-arm64@0.25.5': optional: true - '@esbuild/linux-arm@0.25.4': + '@esbuild/linux-arm@0.25.5': optional: true - '@esbuild/linux-ia32@0.25.4': + '@esbuild/linux-ia32@0.25.5': optional: true - '@esbuild/linux-loong64@0.25.4': + '@esbuild/linux-loong64@0.25.5': optional: true - '@esbuild/linux-mips64el@0.25.4': + '@esbuild/linux-mips64el@0.25.5': optional: true - '@esbuild/linux-ppc64@0.25.4': + '@esbuild/linux-ppc64@0.25.5': optional: true - '@esbuild/linux-riscv64@0.25.4': + '@esbuild/linux-riscv64@0.25.5': optional: true - '@esbuild/linux-s390x@0.25.4': + '@esbuild/linux-s390x@0.25.5': optional: true - '@esbuild/linux-x64@0.25.4': + '@esbuild/linux-x64@0.25.5': optional: true - '@esbuild/netbsd-arm64@0.25.4': + '@esbuild/netbsd-arm64@0.25.5': optional: true - '@esbuild/netbsd-x64@0.25.4': + '@esbuild/netbsd-x64@0.25.5': optional: true - '@esbuild/openbsd-arm64@0.25.4': + '@esbuild/openbsd-arm64@0.25.5': optional: true - '@esbuild/openbsd-x64@0.25.4': + '@esbuild/openbsd-x64@0.25.5': optional: true - '@esbuild/sunos-x64@0.25.4': + '@esbuild/sunos-x64@0.25.5': optional: true - '@esbuild/win32-arm64@0.25.4': + '@esbuild/win32-arm64@0.25.5': optional: true - '@esbuild/win32-ia32@0.25.4': + '@esbuild/win32-ia32@0.25.5': optional: true - '@esbuild/win32-x64@0.25.4': + '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.27.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.28.0(jiti@2.4.2))': dependencies: - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -6269,7 +6277,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.27.0': {} + '@eslint/js@9.28.0': {} '@eslint/object-schema@2.1.6': {} @@ -6280,24 +6288,24 @@ snapshots: '@exodus/schemasafe@1.3.0': {} - '@floating-ui/core@1.7.0': + '@floating-ui/core@1.7.1': dependencies: '@floating-ui/utils': 0.2.9 - '@floating-ui/dom@1.7.0': + '@floating-ui/dom@1.7.1': dependencies: - '@floating-ui/core': 1.7.0 + '@floating-ui/core': 1.7.1 '@floating-ui/utils': 0.2.9 - '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@floating-ui/react-dom@2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/dom': 1.7.0 + '@floating-ui/dom': 1.7.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@floating-ui/utils': 0.2.9 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -6305,19 +6313,19 @@ snapshots: '@floating-ui/utils@0.2.9': {} - '@gerrit0/mini-shiki@3.4.2': + '@gerrit0/mini-shiki@3.5.0': dependencies: - '@shikijs/engine-oniguruma': 3.4.2 - '@shikijs/langs': 3.4.2 - '@shikijs/themes': 3.4.2 - '@shikijs/types': 3.4.2 + '@shikijs/engine-oniguruma': 3.6.0 + '@shikijs/langs': 3.6.0 + '@shikijs/themes': 3.6.0 + '@shikijs/types': 3.6.0 '@shikijs/vscode-textmate': 10.0.2 '@headlessui/react@2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/focus': 3.20.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/interactions': 3.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/focus': 3.20.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/interactions': 3.25.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-virtual': 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -6354,7 +6362,7 @@ snapshots: loglevel: 1.9.2 loglevel-plugin-prefix: 0.8.4 minimatch: 6.2.0 - validator: 13.15.0 + validator: 13.15.15 transitivePeerDependencies: - encoding @@ -6425,9 +6433,9 @@ snapshots: '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 - '@lit/react@1.0.7(@types/react@18.3.22)': + '@lit/react@1.0.7(@types/react@18.3.23)': dependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 '@marijn/find-cluster-break@1.0.2': {} @@ -6468,7 +6476,7 @@ snapshots: chalk: 4.1.2 compare-versions: 6.1.1 debug: 4.4.1(supports-color@8.1.1) - esbuild: 0.25.4 + esbuild: 0.25.5 esutils: 2.0.3 fs-extra: 11.3.0 globby: 11.1.0 @@ -6559,318 +6567,318 @@ snapshots: '@radix-ui/primitive@1.1.2': {} - '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-arrow@1.1.7(@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)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@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) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collection@1.1.7(@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)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-context-menu@2.2.15(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-context-menu@2.2.15(@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)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-menu': 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-menu': 2.1.15(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-context@1.1.2(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-context@1.1.2(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-direction@1.1.1(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-direction@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.10(@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)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-focus-scope@1.1.7(@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)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-id@1.1.1(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-id@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-menu@2.1.15(@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)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@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) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@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) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@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) + '@radix-ui/react-portal': 1.1.9(@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) + '@radix-ui/react-presence': 1.1.4(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-roving-focus': 1.1.10(@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) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.0(@types/react@18.3.22)(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) - - '@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-popper@1.2.7(@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)': + dependencies: + '@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) '@radix-ui/rect': 1.1.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-portal@1.1.9(@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)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-presence@1.1.4(@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)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-primitive@2.1.3(@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)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-roving-focus@1.1.10(@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)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-select@2.2.5(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-select@2.2.5(@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)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@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) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@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) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@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) + '@radix-ui/react-portal': 1.1.9(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@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) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.0(@types/react@18.3.22)(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@radix-ui/react-slot@1.2.3(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.23)(react@18.3.1)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.22)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.23)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: '@radix-ui/rect': 1.1.1 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-use-size@1.1.1(@types/react@18.3.22)(react@18.3.1)': + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.22)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.2.3(@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)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@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) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) '@radix-ui/rect@1.1.1': {} - '@react-aria/focus@3.20.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/focus@3.20.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@react-aria/interactions': 3.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/utils': 3.29.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-types/shared': 3.29.1(react@18.3.1) + '@react-aria/interactions': 3.25.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.29.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.30.0(react@18.3.1) '@swc/helpers': 0.5.17 clsx: 2.1.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@react-aria/interactions@3.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/interactions@3.25.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@react-aria/ssr': 3.9.8(react@18.3.1) - '@react-aria/utils': 3.29.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-stately/flags': 3.1.1 - '@react-types/shared': 3.29.1(react@18.3.1) + '@react-aria/ssr': 3.9.9(react@18.3.1) + '@react-aria/utils': 3.29.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-stately/flags': 3.1.2 + '@react-types/shared': 3.30.0(react@18.3.1) '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@react-aria/ssr@3.9.8(react@18.3.1)': + '@react-aria/ssr@3.9.9(react@18.3.1)': dependencies: '@swc/helpers': 0.5.17 react: 18.3.1 - '@react-aria/utils@3.29.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/utils@3.29.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@react-aria/ssr': 3.9.8(react@18.3.1) - '@react-stately/flags': 3.1.1 - '@react-stately/utils': 3.10.6(react@18.3.1) - '@react-types/shared': 3.29.1(react@18.3.1) + '@react-aria/ssr': 3.9.9(react@18.3.1) + '@react-stately/flags': 3.1.2 + '@react-stately/utils': 3.10.7(react@18.3.1) + '@react-types/shared': 3.30.0(react@18.3.1) '@swc/helpers': 0.5.17 clsx: 2.1.1 react: 18.3.1 @@ -6882,42 +6890,42 @@ snapshots: '@react-dnd/shallowequal@4.0.2': {} - '@react-stately/flags@3.1.1': + '@react-stately/flags@3.1.2': dependencies: '@swc/helpers': 0.5.17 - '@react-stately/utils@3.10.6(react@18.3.1)': + '@react-stately/utils@3.10.7(react@18.3.1)': dependencies: '@swc/helpers': 0.5.17 react: 18.3.1 - '@react-types/shared@3.29.1(react@18.3.1)': + '@react-types/shared@3.30.0(react@18.3.1)': dependencies: react: 18.3.1 - '@reactflow/background@11.3.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/background@11.3.14(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classcat: 5.0.5 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/controls@11.2.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/controls@11.2.14(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classcat: 5.0.5 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/core@11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/core@11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@types/d3': 7.4.3 '@types/d3-drag': 3.0.7 @@ -6929,14 +6937,14 @@ snapshots: d3-zoom: 3.0.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/minimap@11.7.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/minimap@11.7.14(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/d3-selection': 3.0.11 '@types/d3-zoom': 3.0.8 classcat: 5.0.5 @@ -6944,31 +6952,31 @@ snapshots: d3-zoom: 3.0.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-resizer@2.2.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/node-resizer@2.2.14(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classcat: 5.0.5 d3-drag: 3.0.0 d3-selection: 3.0.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-toolbar@1.3.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/node-toolbar@1.3.14(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) classcat: 5.0.5 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer @@ -7035,34 +7043,34 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.41.1': optional: true - '@secretlint/config-creator@9.3.3': + '@secretlint/config-creator@9.3.4': dependencies: - '@secretlint/types': 9.3.3 + '@secretlint/types': 9.3.4 - '@secretlint/config-loader@9.3.3': + '@secretlint/config-loader@9.3.4': dependencies: - '@secretlint/profiler': 9.3.3 - '@secretlint/resolver': 9.3.3 - '@secretlint/types': 9.3.3 + '@secretlint/profiler': 9.3.4 + '@secretlint/resolver': 9.3.4 + '@secretlint/types': 9.3.4 ajv: 8.17.1 debug: 4.4.1(supports-color@8.1.1) rc-config-loader: 4.1.3 transitivePeerDependencies: - supports-color - '@secretlint/core@9.3.3': + '@secretlint/core@9.3.4': dependencies: - '@secretlint/profiler': 9.3.3 - '@secretlint/types': 9.3.3 + '@secretlint/profiler': 9.3.4 + '@secretlint/types': 9.3.4 debug: 4.4.1(supports-color@8.1.1) structured-source: 4.0.0 transitivePeerDependencies: - supports-color - '@secretlint/formatter@9.3.3': + '@secretlint/formatter@9.3.4': dependencies: - '@secretlint/resolver': 9.3.3 - '@secretlint/types': 9.3.3 + '@secretlint/resolver': 9.3.4 + '@secretlint/types': 9.3.4 '@textlint/linter-formatter': 14.7.2 '@textlint/module-interop': 14.7.2 '@textlint/types': 14.7.2 @@ -7075,54 +7083,54 @@ snapshots: transitivePeerDependencies: - supports-color - '@secretlint/node@9.3.3': + '@secretlint/node@9.3.4': dependencies: - '@secretlint/config-loader': 9.3.3 - '@secretlint/core': 9.3.3 - '@secretlint/formatter': 9.3.3 - '@secretlint/profiler': 9.3.3 - '@secretlint/source-creator': 9.3.3 - '@secretlint/types': 9.3.3 + '@secretlint/config-loader': 9.3.4 + '@secretlint/core': 9.3.4 + '@secretlint/formatter': 9.3.4 + '@secretlint/profiler': 9.3.4 + '@secretlint/source-creator': 9.3.4 + '@secretlint/types': 9.3.4 debug: 4.4.1(supports-color@8.1.1) p-map: 4.0.0 transitivePeerDependencies: - supports-color - '@secretlint/profiler@9.3.3': {} + '@secretlint/profiler@9.3.4': {} - '@secretlint/resolver@9.3.3': {} + '@secretlint/resolver@9.3.4': {} - '@secretlint/secretlint-formatter-sarif@9.3.3': + '@secretlint/secretlint-formatter-sarif@9.3.4': dependencies: node-sarif-builder: 2.0.3 - '@secretlint/secretlint-rule-no-dotenv@9.3.3': + '@secretlint/secretlint-rule-no-dotenv@9.3.4': dependencies: - '@secretlint/types': 9.3.3 + '@secretlint/types': 9.3.4 - '@secretlint/secretlint-rule-preset-recommend@9.3.3': {} + '@secretlint/secretlint-rule-preset-recommend@9.3.4': {} - '@secretlint/source-creator@9.3.3': + '@secretlint/source-creator@9.3.4': dependencies: - '@secretlint/types': 9.3.3 - istextorbinary: 6.0.0 + '@secretlint/types': 9.3.4 + istextorbinary: 9.5.0 - '@secretlint/types@9.3.3': {} + '@secretlint/types@9.3.4': {} - '@shikijs/engine-oniguruma@3.4.2': + '@shikijs/engine-oniguruma@3.6.0': dependencies: - '@shikijs/types': 3.4.2 + '@shikijs/types': 3.6.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.4.2': + '@shikijs/langs@3.6.0': dependencies: - '@shikijs/types': 3.4.2 + '@shikijs/types': 3.6.0 - '@shikijs/themes@3.4.2': + '@shikijs/themes@3.6.0': dependencies: - '@shikijs/types': 3.4.2 + '@shikijs/types': 3.6.0 - '@shikijs/types@3.4.2': + '@shikijs/types@3.6.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -7184,7 +7192,7 @@ snapshots: ajv: 8.17.1 ajv-errors: 3.0.0(ajv@8.17.1) ajv-formats: 2.1.1(ajv@8.17.1) - es-aggregate-error: 1.0.13 + es-aggregate-error: 1.0.14 jsonpath-plus: 10.3.0 lodash: 4.17.21 lodash.topath: 4.5.2 @@ -7294,51 +7302,51 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 - '@swc/core-darwin-arm64@1.11.29': + '@swc/core-darwin-arm64@1.11.31': optional: true - '@swc/core-darwin-x64@1.11.29': + '@swc/core-darwin-x64@1.11.31': optional: true - '@swc/core-linux-arm-gnueabihf@1.11.29': + '@swc/core-linux-arm-gnueabihf@1.11.31': optional: true - '@swc/core-linux-arm64-gnu@1.11.29': + '@swc/core-linux-arm64-gnu@1.11.31': optional: true - '@swc/core-linux-arm64-musl@1.11.29': + '@swc/core-linux-arm64-musl@1.11.31': optional: true - '@swc/core-linux-x64-gnu@1.11.29': + '@swc/core-linux-x64-gnu@1.11.31': optional: true - '@swc/core-linux-x64-musl@1.11.29': + '@swc/core-linux-x64-musl@1.11.31': optional: true - '@swc/core-win32-arm64-msvc@1.11.29': + '@swc/core-win32-arm64-msvc@1.11.31': optional: true - '@swc/core-win32-ia32-msvc@1.11.29': + '@swc/core-win32-ia32-msvc@1.11.31': optional: true - '@swc/core-win32-x64-msvc@1.11.29': + '@swc/core-win32-x64-msvc@1.11.31': optional: true - '@swc/core@1.11.29(@swc/helpers@0.5.17)': + '@swc/core@1.11.31(@swc/helpers@0.5.17)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.21 + '@swc/types': 0.1.22 optionalDependencies: - '@swc/core-darwin-arm64': 1.11.29 - '@swc/core-darwin-x64': 1.11.29 - '@swc/core-linux-arm-gnueabihf': 1.11.29 - '@swc/core-linux-arm64-gnu': 1.11.29 - '@swc/core-linux-arm64-musl': 1.11.29 - '@swc/core-linux-x64-gnu': 1.11.29 - '@swc/core-linux-x64-musl': 1.11.29 - '@swc/core-win32-arm64-msvc': 1.11.29 - '@swc/core-win32-ia32-msvc': 1.11.29 - '@swc/core-win32-x64-msvc': 1.11.29 + '@swc/core-darwin-arm64': 1.11.31 + '@swc/core-darwin-x64': 1.11.31 + '@swc/core-linux-arm-gnueabihf': 1.11.31 + '@swc/core-linux-arm64-gnu': 1.11.31 + '@swc/core-linux-arm64-musl': 1.11.31 + '@swc/core-linux-x64-gnu': 1.11.31 + '@swc/core-linux-x64-musl': 1.11.31 + '@swc/core-win32-arm64-msvc': 1.11.31 + '@swc/core-win32-ia32-msvc': 1.11.31 + '@swc/core-win32-x64-msvc': 1.11.31 '@swc/helpers': 0.5.17 '@swc/counter@0.1.3': {} @@ -7347,7 +7355,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/types@0.1.21': + '@swc/types@0.1.22': dependencies: '@swc/counter': 0.1.3 @@ -7355,7 +7363,7 @@ snapshots: dependencies: tailwindcss: 3.4.17 - '@tailwindcss/node@4.1.7': + '@tailwindcss/node@4.1.8': dependencies: '@ampproject/remapping': 2.3.0 enhanced-resolve: 5.18.1 @@ -7363,90 +7371,90 @@ snapshots: lightningcss: 1.30.1 magic-string: 0.30.17 source-map-js: 1.2.1 - tailwindcss: 4.1.7 + tailwindcss: 4.1.8 - '@tailwindcss/oxide-android-arm64@4.1.7': + '@tailwindcss/oxide-android-arm64@4.1.8': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.7': + '@tailwindcss/oxide-darwin-arm64@4.1.8': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.7': + '@tailwindcss/oxide-darwin-x64@4.1.8': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.7': + '@tailwindcss/oxide-freebsd-x64@4.1.8': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.8': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.7': + '@tailwindcss/oxide-linux-arm64-musl@4.1.8': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.7': + '@tailwindcss/oxide-linux-x64-gnu@4.1.8': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.7': + '@tailwindcss/oxide-linux-x64-musl@4.1.8': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.7': + '@tailwindcss/oxide-wasm32-wasi@4.1.8': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.8': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.7': + '@tailwindcss/oxide-win32-x64-msvc@4.1.8': optional: true - '@tailwindcss/oxide@4.1.7': + '@tailwindcss/oxide@4.1.8': dependencies: detect-libc: 2.0.4 tar: 7.4.3 optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.7 - '@tailwindcss/oxide-darwin-arm64': 4.1.7 - '@tailwindcss/oxide-darwin-x64': 4.1.7 - '@tailwindcss/oxide-freebsd-x64': 4.1.7 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.7 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.7 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.7 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.7 - '@tailwindcss/oxide-linux-x64-musl': 4.1.7 - '@tailwindcss/oxide-wasm32-wasi': 4.1.7 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.7 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.7 - - '@tailwindcss/postcss@4.1.7': + '@tailwindcss/oxide-android-arm64': 4.1.8 + '@tailwindcss/oxide-darwin-arm64': 4.1.8 + '@tailwindcss/oxide-darwin-x64': 4.1.8 + '@tailwindcss/oxide-freebsd-x64': 4.1.8 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.8 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.8 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.8 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.8 + '@tailwindcss/oxide-linux-x64-musl': 4.1.8 + '@tailwindcss/oxide-wasm32-wasi': 4.1.8 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.8 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.8 + + '@tailwindcss/postcss@4.1.8': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.7 - '@tailwindcss/oxide': 4.1.7 - postcss: 8.5.3 - tailwindcss: 4.1.7 + '@tailwindcss/node': 4.1.8 + '@tailwindcss/oxide': 4.1.8 + postcss: 8.5.4 + tailwindcss: 4.1.8 - '@tailwindcss/vite@4.1.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': + '@tailwindcss/vite@4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: - '@tailwindcss/node': 4.1.7 - '@tailwindcss/oxide': 4.1.7 - tailwindcss: 4.1.7 - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + '@tailwindcss/node': 4.1.8 + '@tailwindcss/oxide': 4.1.8 + tailwindcss: 4.1.8 + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) '@tanstack/history@1.115.0': {} - '@tanstack/query-core@5.77.2': {} + '@tanstack/query-core@5.80.6': {} - '@tanstack/react-query@5.77.2(react@18.3.1)': + '@tanstack/react-query@5.80.6(react@18.3.1)': dependencies: - '@tanstack/query-core': 5.77.2 + '@tanstack/query-core': 5.80.6 react: 18.3.1 - '@tanstack/react-router-devtools@1.120.13(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.10)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3)': + '@tanstack/react-router-devtools@1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.15)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3)': dependencies: - '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-devtools-core': 1.120.13(@tanstack/router-core@1.120.10)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3) + '@tanstack/react-router': 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/router-devtools-core': 1.120.15(@tanstack/router-core@1.120.15)(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) solid-js: 1.9.7 @@ -7455,20 +7463,20 @@ snapshots: - csstype - tiny-invariant - '@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/history': 1.115.0 - '@tanstack/react-store': 0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-core': 1.120.10 + '@tanstack/react-store': 0.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/router-core': 1.120.15 jsesc: 3.1.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-store@0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-store@0.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/store': 0.7.0 + '@tanstack/store': 0.7.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) use-sync-external-store: 1.5.0(react@18.3.1) @@ -7485,15 +7493,15 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@tanstack/router-core@1.120.10': + '@tanstack/router-core@1.120.15': dependencies: '@tanstack/history': 1.115.0 - '@tanstack/store': 0.7.0 + '@tanstack/store': 0.7.1 tiny-invariant: 1.3.3 - '@tanstack/router-devtools-core@1.120.13(@tanstack/router-core@1.120.10)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': + '@tanstack/router-devtools-core@1.120.15(@tanstack/router-core@1.120.15)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': dependencies: - '@tanstack/router-core': 1.120.10 + '@tanstack/router-core': 1.120.15 clsx: 2.1.1 goober: 2.1.16(csstype@3.1.3) solid-js: 1.9.7 @@ -7501,25 +7509,25 @@ snapshots: optionalDependencies: csstype: 3.1.3 - '@tanstack/router-generator@1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@tanstack/router-generator@1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: '@tanstack/virtual-file-routes': 1.115.0 prettier: 3.5.3 tsx: 4.19.4 - zod: 3.25.48 + zod: 3.25.55 optionalDependencies: - '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-router': 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-plugin@1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8)': + '@tanstack/router-plugin@1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8)': dependencies: - '@babel/core': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.1) + '@babel/core': 7.27.4 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4) '@babel/template': 7.27.2 - '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 - '@tanstack/router-core': 1.120.10 - '@tanstack/router-generator': 1.120.10(@tanstack/react-router@1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + '@tanstack/router-core': 1.120.15 + '@tanstack/router-generator': 1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@tanstack/router-utils': 1.115.0 '@tanstack/virtual-file-routes': 1.115.0 '@types/babel__core': 7.20.5 @@ -7527,23 +7535,23 @@ snapshots: '@types/babel__traverse': 7.20.7 babel-dead-code-elimination: 1.0.10 chokidar: 3.6.0 - unplugin: 2.3.4 - zod: 3.25.48 + unplugin: 2.3.5 + zod: 3.25.55 optionalDependencies: - '@tanstack/react-router': 1.120.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + '@tanstack/react-router': 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) webpack: 5.99.8 transitivePeerDependencies: - supports-color '@tanstack/router-utils@1.115.0': dependencies: - '@babel/generator': 7.27.1 - '@babel/parser': 7.27.2 + '@babel/generator': 7.27.5 + '@babel/parser': 7.27.5 ansis: 3.17.0 diff: 7.0.0 - '@tanstack/store@0.7.0': {} + '@tanstack/store@0.7.1': {} '@tanstack/table-core@8.21.3': {} @@ -7554,7 +7562,7 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.27.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -7572,15 +7580,15 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.22))(@types/react@18.3.22)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@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)': dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.27.6 '@testing-library/dom': 10.4.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 - '@types/react-dom': 18.3.7(@types/react@18.3.22) + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': dependencies: @@ -7619,24 +7627,28 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.27.2 - '@babel/types': 7.27.1 + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.7 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.27.1 + '@babel/types': 7.27.6 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.27.2 - '@babel/types': 7.27.1 + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 '@types/babel__traverse@7.20.7': dependencies: - '@babel/types': 7.27.1 + '@babel/types': 7.27.6 + + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 '@types/command-line-args@5.2.3': {} @@ -7763,26 +7775,30 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/es-aggregate-error@1.0.6': dependencies: - '@types/node': 22.15.21 + '@types/node': 22.15.30 '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/eslint@9.6.1': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 '@types/estree-jsx@1.0.5': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/estree@1.0.7': {} + '@types/estree@1.0.8': {} + '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 @@ -7814,11 +7830,11 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.17.50': + '@types/node@20.19.0': dependencies: - undici-types: 6.19.8 + undici-types: 6.21.0 - '@types/node@22.15.21': + '@types/node@22.15.30': dependencies: undici-types: 6.21.0 @@ -7828,11 +7844,11 @@ snapshots: '@types/prop-types@15.7.14': {} - '@types/react-dom@18.3.7(@types/react@18.3.22)': + '@types/react-dom@18.3.7(@types/react@18.3.23)': dependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - '@types/react@18.3.22': + '@types/react@18.3.23': dependencies: '@types/prop-types': 15.7.14 csstype: 3.1.3 @@ -7847,57 +7863,72 @@ snapshots: '@types/vscode@1.96.0': {} - '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.32.1 - '@typescript-eslint/type-utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.32.1 - eslint: 9.27.0(jiti@2.4.2) + '@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.33.1 + '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.33.1 + eslint: 9.28.0(jiti@2.4.2) graphemer: 1.4.0 - ignore: 7.0.4 + 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/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.32.1 - '@typescript-eslint/types': 8.32.1 - '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.32.1 + '@typescript-eslint/scope-manager': 8.33.1 + '@typescript-eslint/types': 8.33.1 + '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.33.1 debug: 4.4.1(supports-color@8.1.1) - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.32.1': + '@typescript-eslint/project-service@8.33.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) + '@typescript-eslint/types': 8.33.1 + debug: 4.4.1(supports-color@8.1.1) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.33.1': + dependencies: + '@typescript-eslint/types': 8.33.1 + '@typescript-eslint/visitor-keys': 8.33.1 + + '@typescript-eslint/tsconfig-utils@8.33.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.32.1 - '@typescript-eslint/visitor-keys': 8.32.1 + typescript: 5.8.3 - '@typescript-eslint/type-utils@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.1(supports-color@8.1.1) - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.32.1': {} + '@typescript-eslint/types@8.33.1': {} - '@typescript-eslint/typescript-estree@8.32.1(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.33.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 8.32.1 - '@typescript-eslint/visitor-keys': 8.32.1 + '@typescript-eslint/project-service': 8.33.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) + '@typescript-eslint/types': 8.33.1 + '@typescript-eslint/visitor-keys': 8.33.1 debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 @@ -7908,20 +7939,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.32.1 - '@typescript-eslint/types': 8.32.1 - '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) - eslint: 9.27.0(jiti@2.4.2) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.33.1 + '@typescript-eslint/types': 8.33.1 + '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) + eslint: 9.28.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.32.1': + '@typescript-eslint/visitor-keys@8.33.1': dependencies: - '@typescript-eslint/types': 8.32.1 + '@typescript-eslint/types': 8.33.1 eslint-visitor-keys: 4.2.0 '@typespec/ts-http-runtime@0.2.2': @@ -7937,24 +7968,24 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@uiw/codemirror-extensions-basic-setup@4.23.12(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1)': + '@uiw/codemirror-extensions-basic-setup@4.23.12(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1)': dependencies: '@codemirror/autocomplete': 6.18.6 '@codemirror/commands': 6.8.1 - '@codemirror/language': 6.11.0 + '@codemirror/language': 6.11.1 '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 '@codemirror/view': 6.37.1 - '@uiw/react-codemirror@4.23.12(@babel/runtime@7.27.1)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@uiw/react-codemirror@4.23.12(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.27.6 '@codemirror/commands': 6.8.1 '@codemirror/state': 6.5.2 '@codemirror/theme-one-dark': 6.1.2 '@codemirror/view': 6.37.1 - '@uiw/codemirror-extensions-basic-setup': 4.23.12(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1) + '@uiw/codemirror-extensions-basic-setup': 4.23.12(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1) codemirror: 6.0.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -7966,63 +7997,64 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@3.10.0(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react-swc@3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.9 - '@swc/core': 1.11.29(@swc/helpers@0.5.17) - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + '@swc/core': 1.11.31(@swc/helpers@0.5.17) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.5.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react@4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: - '@babel/core': 7.27.1 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1) + '@babel/core': 7.27.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.4) '@rolldown/pluginutils': 1.0.0-beta.9 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitest/expect@3.1.4': + '@vitest/expect@3.2.2': dependencies: - '@vitest/spy': 3.1.4 - '@vitest/utils': 3.1.4 + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.2 + '@vitest/utils': 3.2.2 chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': + '@vitest/mocker@3.2.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: - '@vitest/spy': 3.1.4 + '@vitest/spy': 3.2.2 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) - '@vitest/pretty-format@3.1.4': + '@vitest/pretty-format@3.2.2': dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.1.4': + '@vitest/runner@3.2.2': dependencies: - '@vitest/utils': 3.1.4 + '@vitest/utils': 3.2.2 pathe: 2.0.3 - '@vitest/snapshot@3.1.4': + '@vitest/snapshot@3.2.2': dependencies: - '@vitest/pretty-format': 3.1.4 + '@vitest/pretty-format': 3.2.2 magic-string: 0.30.17 pathe: 2.0.3 - '@vitest/spy@3.1.4': + '@vitest/spy@3.2.2': dependencies: - tinyspy: 3.0.2 + tinyspy: 4.0.3 - '@vitest/utils@3.1.4': + '@vitest/utils@3.2.2': dependencies: - '@vitest/pretty-format': 3.1.4 + '@vitest/pretty-format': 3.2.2 loupe: 3.1.3 tinyrainbow: 2.0.0 @@ -8089,20 +8121,20 @@ snapshots: '@vscode/vsce-sign-win32-arm64': 2.0.2 '@vscode/vsce-sign-win32-x64': 2.0.2 - '@vscode/vsce@3.4.2': + '@vscode/vsce@3.5.0': dependencies: '@azure/identity': 4.10.0 - '@secretlint/node': 9.3.3 - '@secretlint/secretlint-formatter-sarif': 9.3.3 - '@secretlint/secretlint-rule-no-dotenv': 9.3.3 - '@secretlint/secretlint-rule-preset-recommend': 9.3.3 + '@secretlint/node': 9.3.4 + '@secretlint/secretlint-formatter-sarif': 9.3.4 + '@secretlint/secretlint-rule-no-dotenv': 9.3.4 + '@secretlint/secretlint-rule-preset-recommend': 9.3.4 '@vscode/vsce-sign': 2.0.5 azure-devops-node-api: 12.5.0 - chalk: 2.4.2 + chalk: 4.1.2 cheerio: 1.0.0 cockatiel: 3.2.1 commander: 12.1.0 - form-data: 4.0.2 + form-data: 4.0.3 glob: 11.0.2 hosted-git-info: 4.1.0 jsonc-parser: 3.3.1 @@ -8112,7 +8144,7 @@ snapshots: minimatch: 3.1.2 parse-semver: 1.1.1 read: 1.0.7 - secretlint: 9.3.3 + secretlint: 9.3.4 semver: 7.7.2 tmp: 0.2.3 typed-rest-client: 1.8.11 @@ -8263,10 +8295,6 @@ snapshots: ansi-regex@6.1.0: {} - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -8289,7 +8317,7 @@ snapshots: '@swc/helpers': 0.5.17 '@types/command-line-args': 5.2.3 '@types/command-line-usage': 5.0.4 - '@types/node': 20.17.50 + '@types/node': 20.19.0 command-line-args: 6.0.1 command-line-usage: 7.0.3 flatbuffers: 24.12.23 @@ -8330,7 +8358,7 @@ snapshots: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.10 + es-abstract: 1.24.0 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 @@ -8345,14 +8373,14 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.4.21(postcss@8.5.3): + autoprefixer@10.4.21(postcss@8.5.4): dependencies: - browserslist: 4.24.5 - caniuse-lite: 1.0.30001718 + browserslist: 4.25.0 + caniuse-lite: 1.0.30001721 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.5.3 + postcss: 8.5.4 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -8366,10 +8394,10 @@ snapshots: babel-dead-code-elimination@1.0.10: dependencies: - '@babel/core': 7.27.1 - '@babel/parser': 7.27.2 - '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 + '@babel/core': 7.27.4 + '@babel/parser': 7.27.5 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 transitivePeerDependencies: - supports-color @@ -8382,7 +8410,9 @@ snapshots: binary-extensions@2.3.0: {} - binaryextensions@4.19.0: {} + binaryextensions@6.11.0: + dependencies: + editions: 6.21.0 bl@4.1.0: dependencies: @@ -8410,12 +8440,12 @@ snapshots: browser-stdout@1.3.1: {} - browserslist@4.24.5: + browserslist@4.25.0: dependencies: - caniuse-lite: 1.0.30001718 - electron-to-chromium: 1.5.157 + caniuse-lite: 1.0.30001721 + electron-to-chromium: 1.5.165 node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.24.5) + update-browserslist-db: 1.1.3(browserslist@4.25.0) buffer-crc32@0.2.13: {} @@ -8474,7 +8504,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001718: {} + caniuse-lite@1.0.30001721: {} ccount@2.0.1: {} @@ -8490,12 +8520,6 @@ snapshots: dependencies: chalk: 4.1.2 - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - chalk@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -8594,22 +8618,16 @@ snapshots: dependencies: '@codemirror/autocomplete': 6.18.6 '@codemirror/commands': 6.8.1 - '@codemirror/language': 6.11.0 + '@codemirror/language': 6.11.1 '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 '@codemirror/view': 6.37.1 - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} combined-stream@1.0.8: @@ -8851,7 +8869,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 - electron-to-chromium@1.5.157: {} + editions@6.21.0: + dependencies: + version-range: 4.14.0 + + electron-to-chromium@1.5.165: {} elkjs@0.8.2: {} @@ -8889,7 +8911,7 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-abstract@1.23.10: + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 @@ -8918,7 +8940,9 @@ snapshots: is-array-buffer: 3.0.5 is-callable: 1.2.7 is-data-view: 1.0.2 + is-negative-zero: 2.0.3 is-regex: 1.2.1 + is-set: 2.0.3 is-shared-array-buffer: 1.0.4 is-string: 1.1.1 is-typed-array: 1.1.15 @@ -8933,6 +8957,7 @@ snapshots: safe-push-apply: 1.0.0 safe-regex-test: 1.1.0 set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 string.prototype.trim: 1.2.10 string.prototype.trimend: 1.0.9 string.prototype.trimstart: 1.0.8 @@ -8943,11 +8968,11 @@ snapshots: unbox-primitive: 1.1.0 which-typed-array: 1.1.19 - es-aggregate-error@1.0.13: + es-aggregate-error@1.0.14: dependencies: define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.23.10 + es-abstract: 1.24.0 es-errors: 1.3.0 function-bind: 1.1.2 globalthis: 1.0.4 @@ -8979,38 +9004,36 @@ snapshots: es6-promise@3.3.1: {} - esbuild@0.25.4: + esbuild@0.25.5: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.4 - '@esbuild/android-arm': 0.25.4 - '@esbuild/android-arm64': 0.25.4 - '@esbuild/android-x64': 0.25.4 - '@esbuild/darwin-arm64': 0.25.4 - '@esbuild/darwin-x64': 0.25.4 - '@esbuild/freebsd-arm64': 0.25.4 - '@esbuild/freebsd-x64': 0.25.4 - '@esbuild/linux-arm': 0.25.4 - '@esbuild/linux-arm64': 0.25.4 - '@esbuild/linux-ia32': 0.25.4 - '@esbuild/linux-loong64': 0.25.4 - '@esbuild/linux-mips64el': 0.25.4 - '@esbuild/linux-ppc64': 0.25.4 - '@esbuild/linux-riscv64': 0.25.4 - '@esbuild/linux-s390x': 0.25.4 - '@esbuild/linux-x64': 0.25.4 - '@esbuild/netbsd-arm64': 0.25.4 - '@esbuild/netbsd-x64': 0.25.4 - '@esbuild/openbsd-arm64': 0.25.4 - '@esbuild/openbsd-x64': 0.25.4 - '@esbuild/sunos-x64': 0.25.4 - '@esbuild/win32-arm64': 0.25.4 - '@esbuild/win32-ia32': 0.25.4 - '@esbuild/win32-x64': 0.25.4 + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 escalade@3.2.0: {} - escape-string-regexp@1.0.5: {} - escape-string-regexp@4.0.0: {} eslint-scope@5.1.1: @@ -9027,20 +9050,20 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.27.0(jiti@2.4.2): + eslint@9.28.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.20.0 '@eslint/config-helpers': 0.2.2 '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.27.0 + '@eslint/js': 9.28.0 '@eslint/plugin-kit': 0.3.1 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 @@ -9093,7 +9116,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 esutils@2.0.3: {} @@ -9148,7 +9171,7 @@ snapshots: dependencies: pend: 1.2.0 - fdir@6.4.4(picomatch@4.0.2): + fdir@6.4.5(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -9187,11 +9210,12 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.2: + form-data@4.0.3: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 + hasown: 2.0.2 mime-types: 2.1.35 fraction.js@4.3.7: {} @@ -9340,7 +9364,7 @@ snapshots: dependencies: '@sindresorhus/merge-streams': 2.3.0 fast-glob: 3.3.3 - ignore: 7.0.4 + ignore: 7.0.5 path-type: 6.0.0 slash: 5.1.0 unicorn-magic: 0.3.0 @@ -9357,8 +9381,6 @@ snapshots: has-bigints@1.1.0: {} - has-flag@3.0.0: {} - has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -9381,7 +9403,7 @@ snapshots: hast-util-to-jsx-runtime@2.3.6: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/hast': 3.0.4 '@types/unist': 3.0.3 comma-separated-tokens: 2.0.3 @@ -9459,7 +9481,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.4: {} + ignore@7.0.5: {} immediate@3.0.6: {} @@ -9578,6 +9600,8 @@ snapshots: is-map@2.0.3: {} + is-negative-zero@2.0.3: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -9661,10 +9685,11 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - istextorbinary@6.0.0: + istextorbinary@9.5.0: dependencies: - binaryextensions: 4.19.0 - textextensions: 5.16.0 + binaryextensions: 6.11.0 + editions: 6.21.0 + textextensions: 6.11.0 jackspeak@3.4.3: dependencies: @@ -10475,7 +10500,7 @@ snapshots: string-argv: 0.3.2 tsconfck: 2.1.2(typescript@5.8.3) typedoc: 0.28.5(typescript@5.8.3) - typedoc-plugin-markdown: 4.6.3(typedoc@0.28.5(typescript@5.8.3)) + typedoc-plugin-markdown: 4.6.4(typedoc@0.28.5(typescript@5.8.3)) typescript: 5.8.3 transitivePeerDependencies: - encoding @@ -10597,28 +10622,28 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-import@15.1.0(postcss@8.5.3): + postcss-import@15.1.0(postcss@8.5.4): dependencies: - postcss: 8.5.3 + postcss: 8.5.4 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.5.3): + postcss-js@4.0.1(postcss@8.5.4): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.3 + postcss: 8.5.4 - postcss-load-config@4.0.2(postcss@8.5.3): + postcss-load-config@4.0.2(postcss@8.5.4): dependencies: lilconfig: 3.1.3 yaml: 2.8.0 optionalDependencies: - postcss: 8.5.3 + postcss: 8.5.4 - postcss-nested@6.2.0(postcss@8.5.3): + postcss-nested@6.2.0(postcss@8.5.4): dependencies: - postcss: 8.5.3 + postcss: 8.5.4 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -10628,7 +10653,7 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.3: + postcss@8.5.4: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -10711,7 +10736,7 @@ snapshots: dependencies: dnd-core: 16.0.1 - react-dnd@16.0.1(@types/node@22.15.21)(@types/react@18.3.22)(react@18.3.1): + react-dnd@16.0.1(@types/node@22.15.30)(@types/react@18.3.23)(react@18.3.1): dependencies: '@react-dnd/invariant': 4.0.2 '@react-dnd/shallowequal': 4.0.2 @@ -10720,8 +10745,8 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 optionalDependencies: - '@types/node': 22.15.21 - '@types/react': 18.3.22 + '@types/node': 22.15.30 + '@types/react': 18.3.23 react-dom@18.3.1(react@18.3.1): dependencies: @@ -10733,11 +10758,11 @@ snapshots: react-is@17.0.2: {} - react-markdown@10.1.0(@types/react@18.3.22)(react@18.3.1): + react-markdown@10.1.0(@types/react@18.3.23)(react@18.3.1): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 18.3.22 + '@types/react': 18.3.23 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 @@ -10753,26 +10778,26 @@ snapshots: react-refresh@0.17.0: {} - react-remove-scroll-bar@2.3.8(@types/react@18.3.22)(react@18.3.1): + react-remove-scroll-bar@2.3.8(@types/react@18.3.23)(react@18.3.1): dependencies: react: 18.3.1 - react-style-singleton: 2.2.3(@types/react@18.3.22)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - react-remove-scroll@2.7.0(@types/react@18.3.22)(react@18.3.1): + react-remove-scroll@2.7.1(@types/react@18.3.23)(react@18.3.1): dependencies: react: 18.3.1 - react-remove-scroll-bar: 2.3.8(@types/react@18.3.22)(react@18.3.1) - react-style-singleton: 2.2.3(@types/react@18.3.22)(react@18.3.1) + react-remove-scroll-bar: 2.3.8(@types/react@18.3.23)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@18.3.22)(react@18.3.1) - use-sidecar: 1.1.3(@types/react@18.3.22)(react@18.3.1) + use-callback-ref: 1.3.3(@types/react@18.3.23)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.23)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - react-router@7.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router@7.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: cookie: 1.0.2 react: 18.3.1 @@ -10786,26 +10811,26 @@ snapshots: react: 18.3.1 split.js: 1.6.5 - react-style-singleton@2.2.3(@types/react@18.3.22)(react@18.3.1): + react-style-singleton@2.2.3(@types/react@18.3.23)(react@18.3.1): dependencies: get-nonce: 1.0.1 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 react@18.3.1: dependencies: loose-envify: 1.4.0 - reactflow@11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + reactflow@11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@reactflow/background': 11.3.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/controls': 11.2.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/core': 11.11.4(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/minimap': 11.7.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/node-resizer': 2.2.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/node-toolbar': 1.3.14(@types/react@18.3.22)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/background': 11.3.14(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/controls': 11.2.14(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/minimap': 11.7.14(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/node-resizer': 2.2.14(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/node-toolbar': 1.3.14(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -10857,13 +10882,13 @@ snapshots: redux@4.2.1: dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.27.6 reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.10 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 @@ -10997,12 +11022,12 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) - secretlint@9.3.3: + secretlint@9.3.4: dependencies: - '@secretlint/config-creator': 9.3.3 - '@secretlint/formatter': 9.3.3 - '@secretlint/node': 9.3.3 - '@secretlint/profiler': 9.3.3 + '@secretlint/config-creator': 9.3.4 + '@secretlint/formatter': 9.3.4 + '@secretlint/node': 9.3.4 + '@secretlint/profiler': 9.3.4 debug: 4.4.1(supports-color@8.1.1) globby: 14.1.0 read-pkg: 8.1.0 @@ -11184,6 +11209,11 @@ snapshots: stdin-discarder@0.2.2: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + string-argv@0.3.2: {} string-width@4.2.3: @@ -11210,7 +11240,7 @@ snapshots: call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.23.10 + es-abstract: 1.24.0 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 @@ -11284,10 +11314,6 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -11354,18 +11380,18 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.3 - postcss-import: 15.1.0(postcss@8.5.3) - postcss-js: 4.0.1(postcss@8.5.3) - postcss-load-config: 4.0.2(postcss@8.5.3) - postcss-nested: 6.2.0(postcss@8.5.3) + postcss: 8.5.4 + postcss-import: 15.1.0(postcss@8.5.4) + postcss-js: 4.0.1(postcss@8.5.4) + postcss-load-config: 4.0.2(postcss@8.5.4) + postcss-nested: 6.2.0(postcss@8.5.4) postcss-selector-parser: 6.1.2 resolve: 1.22.10 sucrase: 3.35.0 transitivePeerDependencies: - ts-node - tailwindcss@4.1.7: {} + tailwindcss@4.1.8: {} tapable@2.2.2: {} @@ -11400,16 +11426,16 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.14(esbuild@0.25.4)(webpack@5.99.8(esbuild@0.25.4)): + terser-webpack-plugin@5.3.14(esbuild@0.25.5)(webpack@5.99.8(esbuild@0.25.5)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.39.2 - webpack: 5.99.8(esbuild@0.25.4) + terser: 5.41.0 + webpack: 5.99.8(esbuild@0.25.5) optionalDependencies: - esbuild: 0.25.4 + esbuild: 0.25.5 terser-webpack-plugin@5.3.14(webpack@5.99.8): dependencies: @@ -11417,11 +11443,11 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.39.2 + terser: 5.41.0 webpack: 5.99.8 optional: true - terser@5.39.2: + terser@5.41.0: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.14.1 @@ -11436,11 +11462,13 @@ snapshots: text-table@0.2.0: {} - textextensions@5.16.0: {} + textextensions@6.11.0: + dependencies: + editions: 6.21.0 - thememirror@2.0.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1): + thememirror@2.0.1(@codemirror/language@6.11.1)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1): dependencies: - '@codemirror/language': 6.11.0 + '@codemirror/language': 6.11.1 '@codemirror/state': 6.5.2 '@codemirror/view': 6.37.1 @@ -11462,14 +11490,14 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.4.4(picomatch@4.0.2) + fdir: 6.4.5(picomatch@4.0.2) picomatch: 4.0.2 - tinypool@1.0.2: {} + tinypool@1.1.0: {} tinyrainbow@2.0.0: {} - tinyspy@3.0.2: {} + tinyspy@4.0.3: {} tldts-core@6.1.86: {} @@ -11503,7 +11531,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-loader@9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.4)): + ts-loader@9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.5)): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.1 @@ -11511,7 +11539,7 @@ snapshots: semver: 7.7.2 source-map: 0.7.4 typescript: 5.8.3 - webpack: 5.99.8(esbuild@0.25.4) + webpack: 5.99.8(esbuild@0.25.5) tsconfck@2.1.2(typescript@5.8.3): optionalDependencies: @@ -11523,7 +11551,7 @@ snapshots: tsx@4.19.4: dependencies: - esbuild: 0.25.4 + esbuild: 0.25.5 get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 @@ -11584,25 +11612,25 @@ snapshots: tunnel: 0.0.6 underscore: 1.13.7 - typedoc-plugin-markdown@4.6.3(typedoc@0.28.5(typescript@5.8.3)): + typedoc-plugin-markdown@4.6.4(typedoc@0.28.5(typescript@5.8.3)): dependencies: typedoc: 0.28.5(typescript@5.8.3) typedoc@0.28.5(typescript@5.8.3): dependencies: - '@gerrit0/mini-shiki': 3.4.2 + '@gerrit0/mini-shiki': 3.5.0 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 typescript: 5.8.3 yaml: 2.8.0 - typescript-eslint@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.27.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.28.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -11624,8 +11652,6 @@ snapshots: undici-types@5.26.5: {} - undici-types@6.19.8: {} - undici-types@6.21.0: {} undici@6.21.3: {} @@ -11667,15 +11693,15 @@ snapshots: universalify@2.0.1: {} - unplugin@2.3.4: + unplugin@2.3.5: dependencies: acorn: 8.14.1 picomatch: 4.0.2 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.1.3(browserslist@4.24.5): + update-browserslist-db@1.1.3(browserslist@4.25.0): dependencies: - browserslist: 4.24.5 + browserslist: 4.25.0 escalade: 3.2.0 picocolors: 1.1.1 @@ -11687,20 +11713,20 @@ snapshots: url-join@4.0.1: {} - use-callback-ref@1.3.3(@types/react@18.3.22)(react@18.3.1): + use-callback-ref@1.3.3(@types/react@18.3.23)(react@18.3.1): dependencies: react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 - use-sidecar@1.1.3(@types/react@18.3.22)(react@18.3.1): + use-sidecar@1.1.3(@types/react@18.3.23)(react@18.3.1): dependencies: detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 use-sync-external-store@1.5.0(react@18.3.1): dependencies: @@ -11723,7 +11749,9 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - validator@13.15.0: {} + validator@13.15.15: {} + + version-range@4.14.0: {} vfile-message@4.0.2: dependencies: @@ -11735,13 +11763,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.2.2(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 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@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -11756,53 +11784,55 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)): + vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)): dependencies: - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) - vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: - esbuild: 0.25.4 - fdir: 6.4.4(picomatch@4.0.2) + esbuild: 0.25.5 + fdir: 6.4.5(picomatch@4.0.2) picomatch: 4.0.2 - postcss: 8.5.3 + postcss: 8.5.4 rollup: 4.41.1 tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 22.15.21 + '@types/node': 22.15.30 fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.39.2 + terser: 5.41.0 tsx: 4.19.4 yaml: 2.8.0 - vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.2.2(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: - '@vitest/expect': 3.1.4 - '@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) - '@vitest/pretty-format': 3.1.4 - '@vitest/runner': 3.1.4 - '@vitest/snapshot': 3.1.4 - '@vitest/spy': 3.1.4 - '@vitest/utils': 3.1.4 + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.2 + '@vitest/mocker': 3.2.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/pretty-format': 3.2.2 + '@vitest/runner': 3.2.2 + '@vitest/snapshot': 3.2.2 + '@vitest/spy': 3.2.2 + '@vitest/utils': 3.2.2 chai: 5.2.0 debug: 4.4.1(supports-color@8.1.1) expect-type: 1.2.1 magic-string: 0.30.17 pathe: 2.0.3 + picomatch: 4.0.2 std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.14 - tinypool: 1.0.2 + tinypool: 1.1.0 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + vite-node: 3.2.2(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.15.21 + '@types/node': 22.15.30 jsdom: 26.1.0 transitivePeerDependencies: - jiti @@ -11854,20 +11884,20 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-sources@3.3.0: {} + webpack-sources@3.3.2: {} webpack-virtual-modules@0.6.2: {} webpack@5.99.8: dependencies: '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.14.1 - browserslist: 4.24.5 + browserslist: 4.25.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 es-module-lexer: 1.7.0 @@ -11883,23 +11913,23 @@ snapshots: tapable: 2.2.2 terser-webpack-plugin: 5.3.14(webpack@5.99.8) watchpack: 2.4.4 - webpack-sources: 3.3.0 + webpack-sources: 3.3.2 transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js optional: true - webpack@5.99.8(esbuild@0.25.4): + webpack@5.99.8(esbuild@0.25.5): dependencies: '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.14.1 - browserslist: 4.24.5 + browserslist: 4.25.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 es-module-lexer: 1.7.0 @@ -11913,9 +11943,9 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(esbuild@0.25.4)(webpack@5.99.8(esbuild@0.25.4)) + terser-webpack-plugin: 5.3.14(esbuild@0.25.5)(webpack@5.99.8(esbuild@0.25.5)) watchpack: 2.4.4 - webpack-sources: 3.3.0 + webpack-sources: 3.3.2 transitivePeerDependencies: - '@swc/core' - esbuild @@ -12074,19 +12104,19 @@ snapshots: yocto-queue@0.1.0: {} - zod@3.25.48: {} + zod@3.25.55: {} - zustand@4.5.7(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1): + zustand@4.5.7(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1): dependencies: use-sync-external-store: 1.5.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 immer: 9.0.21 react: 18.3.1 - zustand@5.0.5(@types/react@18.3.22)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)): + zustand@5.0.5(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)): optionalDependencies: - '@types/react': 18.3.22 + '@types/react': 18.3.23 immer: 9.0.21 react: 18.3.1 use-sync-external-store: 1.5.0(react@18.3.1) diff --git a/vscode/extension/package.json b/vscode/extension/package.json index fedc937bd4..a777f2282f 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -127,22 +127,22 @@ "fs-extra": "^11.3.0", "vscode-jsonrpc": "^8.2.1", "vscode-languageclient": "^9.0.1", - "zod": "^3.25.48" + "zod": "^3.25.55" }, "devDependencies": { - "@eslint/js": "^9.27.0", + "@eslint/js": "^9.28.0", "@playwright/test": "^1.52.0", "@types/mocha": "^10.0.10", "@types/node": "20.11.25", "@types/vscode": "1.96.0", "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.5.2", - "@vscode/vsce": "^3.4.2", - "esbuild": "^0.25.4", - "eslint": "^9.27.0", + "@vscode/vsce": "^3.5.0", + "esbuild": "^0.25.5", + "eslint": "^9.28.0", "ts-loader": "^9.5.2", "tsx": "^4.19.4", "typescript": "^5.8.3", - "typescript-eslint": "^8.32.1" + "typescript-eslint": "^8.33.1" } } diff --git a/vscode/react/package.json b/vscode/react/package.json index 2ebac7fe8f..93ad640277 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -16,34 +16,34 @@ "@headlessui/react": "^2.2.4", "@heroicons/react": "^2.2.0", "@radix-ui/react-select": "^2.2.5", - "@tailwindcss/postcss": "^4.1.7", - "@tailwindcss/vite": "^4.1.7", - "@tanstack/react-query": "^5.77.2", - "@tanstack/react-router": "^1.120.10", - "@tanstack/react-router-devtools": "^1.120.13", + "@tailwindcss/postcss": "^4.1.8", + "@tailwindcss/vite": "^4.1.8", + "@tanstack/react-query": "^5.80.6", + "@tanstack/react-router": "^1.120.16", + "@tanstack/react-router-devtools": "^1.120.16", "@tanstack/react-virtual": "^3.13.9", - "@tanstack/router-plugin": "^1.120.10", + "@tanstack/router-plugin": "^1.120.16", "apache-arrow": "^19.0.1", "clsx": "^2.1.1", "elkjs": "^0.8.2", "orval": "^7.9.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router": "^7.6.1", + "react-router": "^7.6.2", "reactflow": "^11.11.4", - "tailwindcss": "^4.1.7", + "tailwindcss": "^4.1.8", "vscode-uri": "^3.1.0" }, "devDependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.3.0", - "@types/react": "^18.3.22", + "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react": "^4.5.0", + "@vitejs/plugin-react": "^4.5.1", "jsdom": "^26.1.0", "typescript": "^5.8.3", "vite": "^6.3.5", - "vitest": "^3.1.4", + "vitest": "^3.2.2", "web-vitals": "^4.2.4" } } diff --git a/web/client/package.json b/web/client/package.json index 3d886e239e..1aeff75d45 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -18,8 +18,8 @@ "@codemirror/autocomplete": "^6.18.6", "@codemirror/commands": "^6.8.1", "@codemirror/lang-python": "^6.2.1", - "@codemirror/lang-sql": "^6.8.0", - "@codemirror/language": "^6.11.0", + "@codemirror/lang-sql": "^6.9.0", + "@codemirror/language": "^6.11.1", "@codemirror/legacy-modes": "^6.5.1", "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.37.1", @@ -29,7 +29,7 @@ "@radix-ui/react-context-menu": "^2.2.15", "@radix-ui/react-select": "^2.2.5", "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^5.77.2", + "@tanstack/react-query": "^5.80.6", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.9", "@uidotdev/usehooks": "^2.4.1", @@ -44,37 +44,37 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", - "react-router": "^7.6.1", + "react-router": "^7.6.2", "react-split": "^2.0.14", "reactflow": "^11.11.4", "thememirror": "^2.0.1", "zustand": "^5.0.5" }, "devDependencies": { - "@eslint/js": "^9.27.0", + "@eslint/js": "^9.28.0", "@playwright/test": "^1.52.0", - "@swc/core": "^1.11.29", + "@swc/core": "^1.11.31", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/pluralize": "^0.0.33", - "@types/react": "^18.3.22", + "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react-swc": "^3.10.0", + "@vitejs/plugin-react-swc": "^3.10.1", "ajv": "^8.17.1", "autoprefixer": "^10.4.21", - "eslint": "^9.27.0", + "eslint": "^9.28.0", "jsdom": "^26.1.0", "orval": "^7.9.0", - "postcss": "^8.5.3", + "postcss": "^8.5.4", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", - "typescript-eslint": "^8.32.1", + "typescript-eslint": "^8.33.1", "vite": "^6.3.5", "vite-plugin-css-injected-by-js": "^3.5.2", - "vitest": "^3.1.4" + "vitest": "^3.2.2" }, "optionalDependencies": { - "@swc/core-linux-x64-gnu": "^1.11.29" + "@swc/core-linux-x64-gnu": "^1.11.31" } } From 53224fc1469c69970a75488cac995546826bb2b2 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 6 Jun 2025 12:27:10 +0100 Subject: [PATCH 0361/1056] refactor(vscode): centralise error handlers (#4679) --- vscode/extension/src/commands/format.ts | 28 +------------ vscode/extension/src/extension.ts | 51 ++++-------------------- vscode/extension/src/utilities/errors.ts | 38 +++++++++++++++--- 3 files changed, 42 insertions(+), 75 deletions(-) diff --git a/vscode/extension/src/commands/format.ts b/vscode/extension/src/commands/format.ts index e00a38992b..45ef3dae7d 100644 --- a/vscode/extension/src/commands/format.ts +++ b/vscode/extension/src/commands/format.ts @@ -2,13 +2,7 @@ import { traceLog } from '../utilities/common/log' import { sqlmeshExec } from '../utilities/sqlmesh/sqlmesh' import { err, isErr, ok, Result } from '@bus/result' import * as vscode from 'vscode' -import { - ErrorType, - handleNotSginedInError, - handleSqlmeshLspNotFoundError, - handleSqlmeshLspDependenciesMissingError, - handleTcloudBinNotFoundError, -} from '../utilities/errors' +import { ErrorType, handleError } from '../utilities/errors' import { AuthenticationProviderTobikoCloud } from '../auth/auth' import { execAsync } from '../utilities/exec' @@ -18,25 +12,7 @@ export const format = traceLog('Calling format') const out = await internalFormat() if (isErr(out)) { - switch (out.error.type) { - case 'not_signed_in': - await handleNotSginedInError(authProvider) - return - case 'sqlmesh_lsp_not_found': - await handleSqlmeshLspNotFoundError() - return - case 'sqlmesh_lsp_dependencies_missing': - await handleSqlmeshLspDependenciesMissingError(out.error) - return - case 'tcloud_bin_not_found': - await handleTcloudBinNotFoundError() - return - case 'generic': - await vscode.window.showErrorMessage( - `Project format failed: ${out.error.message}`, - ) - return - } + return handleError(authProvider, out.error, 'Project format failed') } vscode.window.showInformationMessage('Project formatted successfully') } diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 67e57fcbb8..a77cfe6a9e 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -14,12 +14,7 @@ import { signIn } from './commands/signin' import { signInSpecifyFlow } from './commands/signinSpecifyFlow' import { renderModel, reRenderModelForSourceFile } from './commands/renderModel' import { isErr } from '@bus/result' -import { - handleNotSginedInError, - handleSqlmeshLspNotFoundError, - handleSqlmeshLspDependenciesMissingError, - handleTcloudBinNotFoundError, -} from './utilities/errors' +import { handleError } from './utilities/errors' import { selector, completionProvider } from './completion/completion' import { LineagePanel } from './webviews/lineagePanel' import { RenderedModelProvider } from './providers/renderedModelProvider' @@ -117,25 +112,11 @@ export async function activate(context: vscode.ExtensionContext) { traceVerbose('Restarting LSP client') const restartResult = await lspClient.restart() if (isErr(restartResult)) { - switch (restartResult.error.type) { - case 'not_signed_in': - await handleNotSginedInError(authProvider) - return - case 'sqlmesh_lsp_not_found': - await handleSqlmeshLspNotFoundError() - return - case 'sqlmesh_lsp_dependencies_missing': - await handleSqlmeshLspDependenciesMissingError(restartResult.error) - return - case 'tcloud_bin_not_found': - await handleTcloudBinNotFoundError() - return - case 'generic': - await vscode.window.showErrorMessage( - `Failed to restart LSP: ${restartResult.error.message}`, - ) - return - } + return handleError( + authProvider, + restartResult.error, + 'LSP restart failed', + ) } context.subscriptions.push(lspClient) } @@ -155,25 +136,7 @@ export async function activate(context: vscode.ExtensionContext) { const result = await lspClient.start() if (isErr(result)) { - switch (result.error.type) { - case 'not_signed_in': - await handleNotSginedInError(authProvider) - break - case 'sqlmesh_lsp_not_found': - await handleSqlmeshLspNotFoundError() - break - case 'sqlmesh_lsp_dependencies_missing': - await handleSqlmeshLspDependenciesMissingError(result.error) - break - case 'tcloud_bin_not_found': - await handleTcloudBinNotFoundError() - break - case 'generic': - await vscode.window.showErrorMessage( - `Failed to start LSP: ${result.error.message}`, - ) - break - } + return handleError(authProvider, result.error, 'Failed to start LSP') } else { context.subscriptions.push(lspClient) } diff --git a/vscode/extension/src/utilities/errors.ts b/vscode/extension/src/utilities/errors.ts index 3179d26fbc..3a16c214ac 100644 --- a/vscode/extension/src/utilities/errors.ts +++ b/vscode/extension/src/utilities/errors.ts @@ -13,9 +13,12 @@ export type ErrorType = // tcloud_bin_not_found is used when the tcloud executable is not found. This is likely to happen if the user // opens a project that has a `tcloud.yaml` file but doesn't have tcloud installed. | { type: 'tcloud_bin_not_found' } - // sqlmesh_lsp_dependencies_missing is used when the sqlmesh_lsp is found but the lsp extras are missing. | SqlmeshLspDependenciesMissingError +/** + * SqlmeshLspDependenciesMissingError is used when the sqlmesh_lsp is found but + * the lsp extras are missing. + */ interface SqlmeshLspDependenciesMissingError { type: 'sqlmesh_lsp_dependencies_missing' is_missing_pygls: boolean @@ -23,11 +26,36 @@ interface SqlmeshLspDependenciesMissingError { is_tobiko_cloud: boolean } +export async function handleError( + authProvider: AuthenticationProviderTobikoCloud, + error: ErrorType, + genericErrorPrefix?: string, +): Promise { + traceInfo('handleError', error) + switch (error.type) { + case 'not_signed_in': + return handleNotSignedInError(authProvider) + case 'sqlmesh_lsp_not_found': + return handleSqlmeshLspNotFoundError() + case 'sqlmesh_lsp_dependencies_missing': + return handleSqlmeshLspDependenciesMissingError(error) + case 'tcloud_bin_not_found': + return handleTcloudBinNotFoundError() + case 'generic': + if (genericErrorPrefix) { + await window.showErrorMessage(`${genericErrorPrefix}: ${error.message}`) + } else { + await window.showErrorMessage(`An error occurred: ${error.message}`) + } + return + } +} + /** * Handles the case where the user is not signed in to Tobiko Cloud. * @param authProvider - The authentication provider to use for signing in. */ -export const handleNotSginedInError = async ( +const handleNotSignedInError = async ( authProvider: AuthenticationProviderTobikoCloud, ): Promise => { traceInfo('handleNotSginedInError') @@ -43,7 +71,7 @@ export const handleNotSginedInError = async ( /** * Handles the case where the sqlmesh_lsp is not found. */ -export const handleSqlmeshLspNotFoundError = async (): Promise => { +const handleSqlmeshLspNotFoundError = async (): Promise => { traceInfo('handleSqlmeshLspNotFoundError') await window.showErrorMessage( 'SQLMesh LSP not found, please check installation', @@ -53,7 +81,7 @@ export const handleSqlmeshLspNotFoundError = async (): Promise => { /** * Handles the case where the sqlmesh_lsp is found but the lsp extras are missing. */ -export const handleSqlmeshLspDependenciesMissingError = async ( +const handleSqlmeshLspDependenciesMissingError = async ( error: SqlmeshLspDependenciesMissingError, ): Promise => { traceInfo('handleSqlmeshLspDependenciesMissingError') @@ -80,7 +108,7 @@ export const handleSqlmeshLspDependenciesMissingError = async ( /** * Handles the case where the tcloud executable is not found. */ -export const handleTcloudBinNotFoundError = async (): Promise => { +const handleTcloudBinNotFoundError = async (): Promise => { const result = await window.showErrorMessage( 'tcloud executable not found, please check installation', 'Install', From 321b118a10d2f3d5b16878dc4ff572aa2d78e618 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:05:59 +0100 Subject: [PATCH 0362/1056] feat(vscode): gracefully handle not supported apis (#4678) --- sqlmesh/lsp/custom.py | 27 ++++ sqlmesh/lsp/main.py | 26 ++++ vscode/extension/src/commands/renderModel.ts | 6 +- vscode/extension/src/lsp/custom.ts | 18 +++ vscode/extension/src/lsp/lsp.ts | 127 ++++++++++++++++++- vscode/extension/src/utilities/errors.ts | 52 +++++++- 6 files changed, 252 insertions(+), 4 deletions(-) diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py index cc0fff67ea..a6a2de71dc 100644 --- a/sqlmesh/lsp/custom.py +++ b/sqlmesh/lsp/custom.py @@ -72,3 +72,30 @@ class AllModelsForRenderResponse(PydanticModel): """ models: t.List[ModelForRendering] + + +SUPPORTED_METHODS_FEATURE = "sqlmesh/supported_methods" + + +class SupportedMethodsRequest(PydanticModel): + """ + Request to get all supported custom LSP methods. + """ + + pass + + +class CustomMethod(PydanticModel): + """ + Information about a custom LSP method. + """ + + name: str + + +class SupportedMethodsResponse(PydanticModel): + """ + Response containing all supported custom LSP methods. + """ + + methods: t.List[CustomMethod] diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 4d6601292d..d9a34b8004 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -29,12 +29,16 @@ ALL_MODELS_FEATURE, ALL_MODELS_FOR_RENDER_FEATURE, RENDER_MODEL_FEATURE, + SUPPORTED_METHODS_FEATURE, AllModelsRequest, AllModelsResponse, AllModelsForRenderRequest, AllModelsForRenderResponse, RenderModelRequest, RenderModelResponse, + SupportedMethodsRequest, + SupportedMethodsResponse, + CustomMethod, ) from sqlmesh.lsp.hints import get_hints from sqlmesh.lsp.reference import get_references, get_cte_references @@ -42,6 +46,14 @@ from web.server.api.endpoints.lineage import column_lineage, model_lineage from web.server.api.endpoints.models import get_models +SUPPORTED_CUSTOM_METHODS = [ + ALL_MODELS_FEATURE, + RENDER_MODEL_FEATURE, + ALL_MODELS_FOR_RENDER_FEATURE, + API_FEATURE, + SUPPORTED_METHODS_FEATURE, +] + class SQLMeshLanguageServer: def __init__( @@ -149,6 +161,20 @@ def all_models_for_render( models=self.lsp_context.list_of_models_for_rendering() ) + @self.server.feature(SUPPORTED_METHODS_FEATURE) + def supported_methods( + ls: LanguageServer, params: SupportedMethodsRequest + ) -> SupportedMethodsResponse: + """Return all supported custom LSP methods.""" + return SupportedMethodsResponse( + methods=[ + CustomMethod( + name=name, + ) + for name in SUPPORTED_CUSTOM_METHODS + ] + ) + @self.server.feature(API_FEATURE) def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]: ls.log_trace(f"API request: {request}") diff --git a/vscode/extension/src/commands/renderModel.ts b/vscode/extension/src/commands/renderModel.ts index 50598ec55e..6b5db3055c 100644 --- a/vscode/extension/src/commands/renderModel.ts +++ b/vscode/extension/src/commands/renderModel.ts @@ -73,7 +73,7 @@ export function renderModel( if (isErr(allModelsResult)) { vscode.window.showErrorMessage( - `Failed to get models: ${allModelsResult.error}`, + `Failed to get models: ${allModelsResult.error.message}`, ) return } @@ -115,7 +115,9 @@ export function renderModel( }) if (isErr(result)) { - vscode.window.showErrorMessage(`Failed to render model: ${result.error}`) + vscode.window.showErrorMessage( + `Failed to render model: ${result.error.message}`, + ) return } diff --git a/vscode/extension/src/lsp/custom.ts b/vscode/extension/src/lsp/custom.ts index 004ee92285..882209de17 100644 --- a/vscode/extension/src/lsp/custom.ts +++ b/vscode/extension/src/lsp/custom.ts @@ -31,6 +31,7 @@ export type CustomLSPMethods = | AbstractAPICall | RenderModelMethod | AllModelsForRenderMethod + | SupportedMethodsMethod interface AllModelsRequest { textDocument: { @@ -75,3 +76,20 @@ export interface ModelForRendering { description: string | null | undefined uri: string } + +export interface SupportedMethodsMethod { + method: 'sqlmesh/supported_methods' + request: SupportedMethodsRequest + response: SupportedMethodsResponse +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface SupportedMethodsRequest {} + +interface SupportedMethodsResponse { + methods: CustomMethod[] +} + +interface CustomMethod { + name: string +} diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index d23c663c62..a1ad08a864 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -9,18 +9,41 @@ import { sqlmeshLspExec } from '../utilities/sqlmesh/sqlmesh' import { err, isErr, ok, Result } from '@bus/result' import { getWorkspaceFolders } from '../utilities/common/vscodeapi' import { traceError, traceInfo } from '../utilities/common/log' -import { ErrorType } from '../utilities/errors' +import { + ErrorType, + ErrorTypeGeneric, + ErrorTypeInvalidState, + ErrorTypeSQLMeshOutdated, +} from '../utilities/errors' import { CustomLSPMethods } from './custom' +type SupportedMethodsState = + | { type: 'not-fetched' } + | { type: 'fetched'; methods: Set } + // TODO: This state is used when the `sqlmesh/supported_methods` endpoint is + // not supported by the LSP server. This is in order to be backward compatible + // with older versions of SQLMesh that do not support this endpoint. At some point + // we should remove this state and always fetch the supported methods. + | { type: 'endpoint-not-supported' } + let outputChannel: OutputChannel | undefined export class LSPClient implements Disposable { private client: LanguageClient | undefined + /** + * State to track whether the supported methods have been fetched. These are used to determine if a method is supported + * by the LSP server and return an error if not. + */ + private supportedMethodsState: SupportedMethodsState = { type: 'not-fetched' } constructor() { this.client = undefined } + // TODO: This method is used to check if the LSP client has completion capability + // in order to be backward compatible with older versions of SQLMesh that do not + // support completion. At some point we should remove this method and always assume + // that the LSP client has completion capability. public hasCompletionCapability(): boolean { if (!this.client) { traceError('LSP client is not initialized') @@ -98,6 +121,8 @@ export class LSPClient implements Disposable { if (this.client) { await this.client.stop() this.client = undefined + // Reset supported methods state when the client stops + this.supportedMethodsState = { type: 'not-fetched' } } } @@ -105,7 +130,106 @@ export class LSPClient implements Disposable { await this.stop() } + private async fetchSupportedMethods(): Promise { + if (!this.client || this.supportedMethodsState.type !== 'not-fetched') { + return + } + try { + const result = await this.internal_call_custom_method( + 'sqlmesh/supported_methods', + {}, + ) + if (isErr(result)) { + traceError(`Failed to fetch supported methods: ${result.error}`) + this.supportedMethodsState = { type: 'endpoint-not-supported' } + return + } + const methodNames = new Set(result.value.methods.map(m => m.name)) + this.supportedMethodsState = { type: 'fetched', methods: methodNames } + traceInfo( + `Fetched supported methods: ${Array.from(methodNames).join(', ')}`, + ) + } catch { + // If the supported_methods endpoint doesn't exist, mark it as not supported + this.supportedMethodsState = { type: 'endpoint-not-supported' } + traceInfo( + 'Supported methods endpoint not available, proceeding without validation', + ) + } + } + public async call_custom_method< + Method extends Exclude< + CustomLSPMethods['method'], + 'sqlmesh/supported_methods' + >, + Request extends Extract['request'], + Response extends Extract['response'], + >( + method: Method, + request: Request, + ): Promise< + Result< + Response, + ErrorTypeGeneric | ErrorTypeInvalidState | ErrorTypeSQLMeshOutdated + > + > { + if (!this.client) { + return err({ + type: 'generic', + message: 'LSP client not ready.', + }) + } + await this.fetchSupportedMethods() + + const supportedState = this.supportedMethodsState + switch (supportedState.type) { + case 'not-fetched': + return err({ + type: 'invalid_state', + message: 'Supported methods not fetched yet whereas they should.', + }) + case 'fetched': { + // If we have fetched the supported methods, we can check if the method is supported + if (!supportedState.methods.has(method)) { + return err({ + type: 'sqlmesh_outdated', + message: `Method '${method}' is not supported by this LSP server.`, + }) + } + const response = await this.internal_call_custom_method( + method, + request as any, + ) + if (isErr(response)) { + return err({ + type: 'generic', + message: response.error, + }) + } + return ok(response.value as Response) + } + case 'endpoint-not-supported': { + const response = await this.internal_call_custom_method( + method, + request as any, + ) + if (isErr(response)) { + return err({ + type: 'generic', + message: response.error, + }) + } + return ok(response.value as Response) + } + } + } + + /** + * Internal method to call a custom LSP method without checking if the method is supported. It is used for + * the class whereas the `call_custom_method` checks if the method is supported. + */ + public async internal_call_custom_method< Method extends CustomLSPMethods['method'], Request extends Extract['request'], Response extends Extract['response'], @@ -113,6 +237,7 @@ export class LSPClient implements Disposable { if (!this.client) { return err('lsp client not ready') } + try { const result = await this.client.sendRequest(method, request) return ok(result) diff --git a/vscode/extension/src/utilities/errors.ts b/vscode/extension/src/utilities/errors.ts index 3a16c214ac..7a0796a449 100644 --- a/vscode/extension/src/utilities/errors.ts +++ b/vscode/extension/src/utilities/errors.ts @@ -7,13 +7,53 @@ import { traceInfo } from './common/log' * Represents different types of errors that can occur in the application. */ export type ErrorType = - | { type: 'generic'; message: string } + | ErrorTypeGeneric | { type: 'not_signed_in' } | { type: 'sqlmesh_lsp_not_found' } // tcloud_bin_not_found is used when the tcloud executable is not found. This is likely to happen if the user // opens a project that has a `tcloud.yaml` file but doesn't have tcloud installed. | { type: 'tcloud_bin_not_found' } | SqlmeshLspDependenciesMissingError + | ErrorTypeInvalidState + | ErrorTypeSQLMeshOutdated + +/** + * ErrorTypeSQLMeshOutdated is used when the SQLMesh version is outdated. The + * message should explain the problem, but the suggestion to update SQLMesh is + * handled at the place where the error is shown. + */ +export interface ErrorTypeSQLMeshOutdated { + type: 'sqlmesh_outdated' + /** + * A message that describes the outdated SQLMesh version, it should not talk about + * updating SQLMesh. This is done at the place where the error is handled. + */ + message: string +} + +/** + * ErrorTypeInvalidState is used when the state of the application is invalid state. + * They should never be thrown by the application unless there is a bug in the code. + * The shown message should be generic and not contain any sensitive information but + * asks the user to report the issue to the developers. + */ +export interface ErrorTypeInvalidState { + type: 'invalid_state' + /** + * A message that describes the invalid state, it should not talk about reporting + * the issue to the developers. This is done at the place where the error is + * handled. + */ + message: string +} + +/** + * ErrorTypeGeneric is a generic error type that can be used to represent any error with a message. + */ +export interface ErrorTypeGeneric { + type: 'generic' + message: string +} /** * SqlmeshLspDependenciesMissingError is used when the sqlmesh_lsp is found but @@ -33,6 +73,16 @@ export async function handleError( ): Promise { traceInfo('handleError', error) switch (error.type) { + case 'invalid_state': + await window.showErrorMessage( + `Invalid state: ${error.message}. Please report this issue to the developers.`, + ) + return + case 'sqlmesh_outdated': + await window.showErrorMessage( + `SQLMesh itself is outdated. Please update SQLMesh to the latest version to use this feature. ${error.message}`, + ) + return case 'not_signed_in': return handleNotSignedInError(authProvider) case 'sqlmesh_lsp_not_found': From 587e0ea209fb32af39899a3ba5e4378f0a790810 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 6 Jun 2025 16:08:39 +0200 Subject: [PATCH 0363/1056] feat(vscode): move format functionality to lsp (#4681) --- sqlmesh/lsp/custom.py | 19 ++++++++ sqlmesh/lsp/main.py | 23 +++++++++ vscode/extension/src/commands/format.ts | 32 +++++++++++-- vscode/extension/src/extension.ts | 10 ++-- vscode/extension/src/lsp/custom.ts | 13 +++++ .../src/utilities/sqlmesh/sqlmesh.ts | 2 +- vscode/extension/tests/format.spec.ts | 48 +++++++++++++++++++ 7 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 vscode/extension/tests/format.spec.ts diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py index a6a2de71dc..9e0bc07cd4 100644 --- a/sqlmesh/lsp/custom.py +++ b/sqlmesh/lsp/custom.py @@ -99,3 +99,22 @@ class SupportedMethodsResponse(PydanticModel): """ methods: t.List[CustomMethod] + + +FORMAT_PROJECT_FEATURE = "sqlmesh/format_project" + + +class FormatProjectRequest(PydanticModel): + """ + Request to format all models in the current project. + """ + + pass + + +class FormatProjectResponse(PydanticModel): + """ + Response to format project request. + """ + + pass diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index d9a34b8004..0e7d89be2a 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -30,6 +30,7 @@ ALL_MODELS_FOR_RENDER_FEATURE, RENDER_MODEL_FEATURE, SUPPORTED_METHODS_FEATURE, + FORMAT_PROJECT_FEATURE, AllModelsRequest, AllModelsResponse, AllModelsForRenderRequest, @@ -38,6 +39,8 @@ RenderModelResponse, SupportedMethodsRequest, SupportedMethodsResponse, + FormatProjectRequest, + FormatProjectResponse, CustomMethod, ) from sqlmesh.lsp.hints import get_hints @@ -52,6 +55,7 @@ ALL_MODELS_FOR_RENDER_FEATURE, API_FEATURE, SUPPORTED_METHODS_FEATURE, + FORMAT_PROJECT_FEATURE, ] @@ -175,6 +179,25 @@ def supported_methods( ] ) + @self.server.feature(FORMAT_PROJECT_FEATURE) + def format_project( + ls: LanguageServer, params: FormatProjectRequest + ) -> FormatProjectResponse: + """Format all models in the current project.""" + try: + if self.lsp_context is None: + current_path = Path.cwd() + self._ensure_context_in_folder(current_path) + if self.lsp_context is None: + raise RuntimeError("No context found") + + # Call the format method on the context + self.lsp_context.context.format() + return FormatProjectResponse() + except Exception as e: + ls.log_trace(f"Error formatting project: {e}") + return FormatProjectResponse() + @self.server.feature(API_FEATURE) def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]: ls.log_trace(f"API request: {request}") diff --git a/vscode/extension/src/commands/format.ts b/vscode/extension/src/commands/format.ts index 45ef3dae7d..5e3465921a 100644 --- a/vscode/extension/src/commands/format.ts +++ b/vscode/extension/src/commands/format.ts @@ -5,19 +5,45 @@ import * as vscode from 'vscode' import { ErrorType, handleError } from '../utilities/errors' import { AuthenticationProviderTobikoCloud } from '../auth/auth' import { execAsync } from '../utilities/exec' +import { LSPClient } from '../lsp/lsp' export const format = - (authProvider: AuthenticationProviderTobikoCloud) => + ( + authProvider: AuthenticationProviderTobikoCloud, + lsp: LSPClient | undefined, + ) => async (): Promise => { traceLog('Calling format') - const out = await internalFormat() + const out = await internalFormat(lsp) if (isErr(out)) { return handleError(authProvider, out.error, 'Project format failed') } vscode.window.showInformationMessage('Project formatted successfully') } -const internalFormat = async (): Promise> => { +const internalFormat = async ( + lsp: LSPClient | undefined, +): Promise> => { + try { + // Try LSP method first + if (lsp) { + const response = await lsp.call_custom_method( + 'sqlmesh/format_project', + {}, + ) + if (isErr(response)) { + return response + } + return ok(undefined) + } + } catch (error) { + traceLog(`LSP format failed, falling back to CLI: ${JSON.stringify(error)}`) + } + + // Fallback to CLI method if LSP is not available + // TODO This is a solution in order to be backwards compatible in the cases + // where the LSP method is not implemented yet. This should be removed at + // some point in the future. const exec = await sqlmeshExec() if (isErr(exec)) { return exec diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index a77cfe6a9e..8749c61fb2 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -56,9 +56,6 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('sqlmesh.signout', signOut(authProvider)), ) - context.subscriptions.push( - vscode.commands.registerCommand('sqlmesh.format', format(authProvider)), - ) lspClient = new LSPClient() @@ -79,6 +76,13 @@ export async function activate(context: vscode.ExtensionContext) { ), ) + context.subscriptions.push( + vscode.commands.registerCommand( + 'sqlmesh.format', + format(authProvider, lspClient), + ), + ) + // Register the webview const lineagePanel = new LineagePanel(context.extensionUri, lspClient) context.subscriptions.push( diff --git a/vscode/extension/src/lsp/custom.ts b/vscode/extension/src/lsp/custom.ts index 882209de17..be11419b79 100644 --- a/vscode/extension/src/lsp/custom.ts +++ b/vscode/extension/src/lsp/custom.ts @@ -32,6 +32,7 @@ export type CustomLSPMethods = | RenderModelMethod | AllModelsForRenderMethod | SupportedMethodsMethod + | FormatProjectMethod interface AllModelsRequest { textDocument: { @@ -93,3 +94,15 @@ interface SupportedMethodsResponse { interface CustomMethod { name: string } + +export interface FormatProjectMethod { + method: 'sqlmesh/format_project' + request: FormatProjectRequest + response: FormatProjectResponse +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface FormatProjectRequest {} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface FormatProjectResponse {} diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 707e57f76b..95cc94d38e 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -197,7 +197,7 @@ export const ensureSqlmeshEnterpriseInstalled = async (): Promise< /** * Get the sqlmesh executable for the current workspace. * - * @returns The sqlmesh executable for the current workspace. + * @deprecated Use LSP instead of direct sqlmesh execution for any new functionality. */ export const sqlmeshExec = async (): Promise< Result diff --git a/vscode/extension/tests/format.spec.ts b/vscode/extension/tests/format.spec.ts new file mode 100644 index 0000000000..ad85340aa3 --- /dev/null +++ b/vscode/extension/tests/format.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { startVSCode, SUSHI_SOURCE_PATH } from './utils' + +test('Format project works correctly', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + try { + const { window, close } = await startVSCode(tempDir) + + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customer_revenue_lifetime model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Render the model + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Format SQLMesh Project') + await window.keyboard.press('Enter') + + // Check that the notification appears saying 'Project formatted successfully' + await expect( + window.getByText('Project formatted successfully', { exact: true }), + ).toBeVisible() + + await close() + } finally { + await fs.remove(tempDir) + } +}) From 92c16f32bb700c5f7f7a56084f08a6f25b381406 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Fri, 6 Jun 2025 17:44:50 +0300 Subject: [PATCH 0364/1056] Feat: Add plan option to always compare against prod (#4615) --- docs/guides/configuration.md | 111 ++++++++++++++++++++++++++++++++ docs/reference/configuration.md | 2 +- sqlmesh/core/config/plan.py | 2 + sqlmesh/core/context.py | 3 + sqlmesh/core/context_diff.py | 17 +++-- tests/core/test_integration.py | 103 ++++++++++++++++++++++++++++- 6 files changed, 232 insertions(+), 6 deletions(-) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 561996594c..a7be1b145b 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -381,6 +381,117 @@ Example showing default values: ) ``` + +### Always comparing against production + +By default, SQLMesh compares the current state of project files to the target `` environment when `sqlmesh plan ` is run. However, a common expectation is that local changes should always be compared to the production environment. + +The `always_recreate_environment` boolean plan option can alter this behavior. When enabled, SQLMesh will always attempt to compare against the production environment by recreating the target environment; If `prod` does not exist, SQLMesh will fall back to comparing against the target environment. + +**NOTE:**: Upon succesfull plan application, changes are still promoted to the target `` environment. + +=== "YAML" + + ```yaml linenums="1" + plan: + always_recreate_environment: True + ``` + +=== "Python" + + ```python linenums="1" + from sqlmesh.core.config import ( + Config, + ModelDefaultsConfig, + PlanConfig, + ) + + config = Config( + model_defaults=ModelDefaultsConfig(dialect=), + plan=PlanConfig( + always_compare_against_prod=True, + ), + ) + ``` + +#### Change Categorization Example + +Consider this scenario with `always_recreate_environment` enabled: + +1. Initial state in `prod`: +```sql +MODEL (name sqlmesh_example.test_model, kind FULL); +SELECT 1 AS col +``` + +1. First (breaking) change in `dev`: +```sql +MODEL (name sqlmesh_example__dev.test_model, kind FULL); +SELECT 2 AS col +``` + +??? "Output plan example #1" + + ```bash + New environment `dev` will be created from `prod` + + Differences from the `prod` environment: + + Models: + └── Directly Modified: + └── sqlmesh_example__dev.test_model + + --- + +++ + + + kind FULL + ) + SELECT + - 1 AS col + + 2 AS col + ``` + +3. Second (metadata) change in `dev`: +```sql +MODEL (name sqlmesh_example__dev.test_model, kind FULL, owner 'John Doe'); +SELECT 5 AS col +``` + +??? "Output plan example #2" + + ```bash + New environment `dev` will be created from `prod` + + Differences from the `prod` environment: + + Models: + └── Directly Modified: + └── sqlmesh_example__dev.test_model + + --- + + +++ + + @@ -1,8 +1,9 @@ + + MODEL ( + name sqlmesh_example.test_model, + + owner "John Doe", + kind FULL + ) + SELECT + - 1 AS col + + 2 AS col + + Directly Modified: sqlmesh_example__dev.test_model (Breaking) + Models needing backfill: + └── sqlmesh_example__dev.test_model: [full refresh] + ``` + +Even though the second change should have been a metadata change (thus not requiring a backfill), it will still be classified as a breaking change because the comparison is against production instead of the previous development state. This is intentional and may cause additional backfills as more changes are accumulated. + + ### Gateways The `gateways` configuration defines how SQLMesh should connect to the data warehouse, state backend, and scheduler. These options are in the [gateway](../reference/configuration.md#gateway) section of the configuration reference page. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 79484b50cc..e00c9dee1f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -80,7 +80,7 @@ Configuration for the `sqlmesh plan` command. | `enable_preview` | Indicates whether to enable [data preview](../concepts/plans.md#data-preview) for forward-only models when targeting a development environment (Default: True, except for dbt projects where the target engine does not support cloning) | Boolean | N | | `no_diff` | Don't show diffs for changed models (Default: False) | boolean | N | | `no_prompts` | Disables interactive prompts in CLI (Default: True) | boolean | N | - +| `always_recreate_environment` | Always recreates the target environment from the environment specified in `create_from` (by default `prod`) (Default: False) | boolean | N | ## Run Configuration for the `sqlmesh run` command. Please note that this is only applicable when configured with the [builtin](#builtin) scheduler. diff --git a/sqlmesh/core/config/plan.py b/sqlmesh/core/config/plan.py index cac0b3fd70..df1ca44873 100644 --- a/sqlmesh/core/config/plan.py +++ b/sqlmesh/core/config/plan.py @@ -20,6 +20,7 @@ class PlanConfig(BaseConfig): auto_apply: Whether to automatically apply the new plan after creation. use_finalized_state: Whether to compare against the latest finalized environment state, or to use whatever state the target environment is currently in. + always_recreate_environment: Whether to always recreate the target environment from the `create_from` environment. """ forward_only: bool = False @@ -30,3 +31,4 @@ class PlanConfig(BaseConfig): no_prompts: bool = True auto_apply: bool = False use_finalized_state: bool = False + always_recreate_environment: bool = False diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index de5ee6ede9..0450827d6e 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1487,6 +1487,7 @@ def plan_builder( or (backfill_models is not None and not backfill_models), ensure_finalized_snapshots=self.config.plan.use_finalized_state, diff_rendered=diff_rendered, + always_recreate_environment=self.config.plan.always_recreate_environment, ) modified_model_names = { *context_diff.modified_snapshots, @@ -2628,6 +2629,7 @@ def _context_diff( force_no_diff: bool = False, ensure_finalized_snapshots: bool = False, diff_rendered: bool = False, + always_recreate_environment: bool = False, ) -> ContextDiff: environment = Environment.sanitize_name(environment) if force_no_diff: @@ -2645,6 +2647,7 @@ def _context_diff( environment_statements=self._environment_statements, gateway_managed_virtual_layer=self.config.gateway_managed_virtual_layer, infer_python_dependencies=self.config.infer_python_dependencies, + always_recreate_environment=always_recreate_environment, ) def _destroy(self) -> None: diff --git a/sqlmesh/core/context_diff.py b/sqlmesh/core/context_diff.py index 4212f328b1..354779a3e1 100644 --- a/sqlmesh/core/context_diff.py +++ b/sqlmesh/core/context_diff.py @@ -103,6 +103,7 @@ def create( environment_statements: t.Optional[t.List[EnvironmentStatements]] = [], gateway_managed_virtual_layer: bool = False, infer_python_dependencies: bool = True, + always_recreate_environment: bool = False, ) -> ContextDiff: """Create a ContextDiff object. @@ -128,10 +129,12 @@ def create( The ContextDiff object. """ environment = environment.lower() - env = state_reader.get_environment(environment) - + existing_env = state_reader.get_environment(environment) create_from_env_exists = False - if env is None or env.expired: + + recreate_environment = always_recreate_environment and not environment == create_from + + if existing_env is None or existing_env.expired or recreate_environment: env = state_reader.get_environment(create_from.lower()) if not env and create_from != c.PROD: @@ -143,6 +146,7 @@ def create( create_from_env_exists = env is not None previously_promoted_snapshot_ids = set() else: + env = existing_env is_new_environment = False previously_promoted_snapshot_ids = {s.snapshot_id for s in env.promoted_snapshots} @@ -220,6 +224,11 @@ def create( previous_environment_statements = state_reader.get_environment_statements(environment) + if existing_env and always_recreate_environment: + previous_plan_id: t.Optional[str] = existing_env.plan_id + else: + previous_plan_id = env.plan_id if env and not is_new_environment else None + return ContextDiff( environment=environment, is_new_environment=is_new_environment, @@ -232,7 +241,7 @@ def create( modified_snapshots=modified_snapshots, snapshots=merged_snapshots, new_snapshots=new_snapshots, - previous_plan_id=env.plan_id if env and not is_new_environment else None, + previous_plan_id=previous_plan_id, previously_promoted_snapshot_ids=previously_promoted_snapshot_ids, previous_finalized_snapshots=env.previous_finalized_snapshots if env else None, previous_requirements=env.requirements if env else {}, diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index bfc416596b..8725318506 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -6,7 +6,7 @@ from datetime import timedelta from unittest import mock from unittest.mock import patch - +import logging import os import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 @@ -37,6 +37,7 @@ from sqlmesh.core.console import Console, get_console from sqlmesh.core.context import Context from sqlmesh.core.config.categorizer import CategorizerConfig +from sqlmesh.core.config.plan import PlanConfig from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.environment import EnvironmentNamingInfo from sqlmesh.core.macros import macro @@ -6252,3 +6253,103 @@ def test_render_path_instead_of_model(tmp_path: Path): # Case 3: Render the model successfully assert ctx.render("test_model").sql() == 'SELECT 1 AS "col"' + + +@use_terminal_console +def test_plan_always_recreate_environment(tmp_path: Path): + def plan_with_output(ctx: Context, environment: str): + with patch.object(logger, "info") as mock_logger: + with capture_output() as output: + ctx.load() + ctx.plan(environment, no_prompts=True, auto_apply=True) + + # Facade logs info "Promoting environment {environment}" + assert mock_logger.call_args[0][1] == environment + + return output + + models_dir = tmp_path / "models" + + logger = logging.getLogger("sqlmesh.core.state_sync.db.facade") + + create_temp_file( + tmp_path, models_dir / "a.sql", "MODEL (name test.a, kind FULL); SELECT 1 AS col" + ) + + config = Config(plan=PlanConfig(always_recreate_environment=True)) + ctx = Context(paths=[tmp_path], config=config) + + # Case 1: Neither prod nor dev exists, so dev is initialized + output = plan_with_output(ctx, "dev") + + assert """`dev` environment will be initialized""" in output.stdout + + # Case 2: Prod does not exist, so dev is updated + create_temp_file( + tmp_path, models_dir / "a.sql", "MODEL (name test.a, kind FULL); SELECT 5 AS col" + ) + + output = plan_with_output(ctx, "dev") + assert "`dev` environment will be initialized" in output.stdout + + # Case 3: Prod is initialized, so plan comparisons moving forward should be against prod + output = plan_with_output(ctx, "prod") + assert "`prod` environment will be initialized" in output.stdout + + # Case 4: Dev is updated with a breaking change. Prod exists now so plan comparisons moving forward should be against prod + create_temp_file( + tmp_path, models_dir / "a.sql", "MODEL (name test.a, kind FULL); SELECT 10 AS col" + ) + ctx.load() + + plan = ctx.plan_builder("dev").build() + + assert ( + next(iter(plan.context_diff.snapshots.values())).change_category + == SnapshotChangeCategory.BREAKING + ) + + output = plan_with_output(ctx, "dev") + assert "New environment `dev` will be created from `prod`" in output.stdout + assert "Differences from the `prod` environment" in output.stdout + + # Case 5: Dev is updated with a metadata change, but comparison against prod shows both the previous and the current changes + # so it's still classified as a breaking change + create_temp_file( + tmp_path, + models_dir / "a.sql", + "MODEL (name test.a, kind FULL, owner 'test'); SELECT 10 AS col", + ) + ctx.load() + + plan = ctx.plan_builder("dev").build() + + assert ( + next(iter(plan.context_diff.snapshots.values())).change_category + == SnapshotChangeCategory.BREAKING + ) + + output = plan_with_output(ctx, "dev") + assert "New environment `dev` will be created from `prod`" in output.stdout + assert "Differences from the `prod` environment" in output.stdout + + assert ( + """MODEL ( + name test.a, ++ owner test, + kind FULL + ) + SELECT +- 5 AS col ++ 10 AS col""" + in output.stdout + ) + + # Case 6: Ensure that target environment and create_from environment are not the same + output = plan_with_output(ctx, "prod") + assert not "New environment `prod` will be created from `prod`" in output.stdout + + # Case 7: Check that we can still run Context::diff() against any environment + for environment in ["dev", "prod"]: + context_diff = ctx._context_diff(environment) + assert context_diff.environment == environment From 128ffe1583adb01deaddf53f053690d253200232 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 6 Jun 2025 19:32:37 +0300 Subject: [PATCH 0365/1056] Chore: Fix readme flag of always_recreate_environment (#4682) --- docs/guides/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index a7be1b145b..361171d937 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -409,7 +409,7 @@ The `always_recreate_environment` boolean plan option can alter this behavior. W config = Config( model_defaults=ModelDefaultsConfig(dialect=), plan=PlanConfig( - always_compare_against_prod=True, + always_recreate_environment=True, ), ) ``` From 1611644e1273bb33498b6fb0944a7bfc4998cff4 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 6 Jun 2025 13:19:42 -0700 Subject: [PATCH 0366/1056] Chore: Remove obsolete warning when converting a dbt project (#4685) --- sqlmesh/dbt/model.py | 8 -------- tests/dbt/test_transformation.py | 1 - 2 files changed, 9 deletions(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 7f36e93385..51cfd06c88 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -314,14 +314,6 @@ def model_kind(self, context: DbtContext) -> ModelKind: **incremental_by_kind_kwargs, ) - incremental_by_time_str = collection_to_str(INCREMENTAL_BY_TIME_STRATEGIES) - incremental_by_unique_key_str = collection_to_str( - INCREMENTAL_BY_UNIQUE_KEY_STRATEGIES.union(["none"]) - ) - get_console().log_warning( - f"Using unmanaged incremental materialization for model '{self.canonical_name(context)}'. " - f"Some features might not be available. Consider adding either a time_column ({incremental_by_time_str}) or a unique_key ({incremental_by_unique_key_str}) configuration to mitigate this.", - ) strategy = self.incremental_strategy or target.default_incremental_strategy( IncrementalUnmanagedKind ) diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index c5204aeb55..17b8a6f313 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1314,7 +1314,6 @@ def test_clickhouse_properties(mocker: MockerFixture): "SQLMesh does not support 'incremental_predicates' - they will not be applied.", "SQLMesh does not support the 'query_settings' model configuration parameter. Specify the query settings directly in the model query.", "SQLMesh does not support the 'sharding_key' model configuration parameter or distributed materializations.", - "Using unmanaged incremental materialization for model '`test`.`model`'. Some features might not be available. Consider adding either a time_column ('delete+insert', 'insert_overwrite') or a unique_key ('merge', 'none') configuration to mitigate this.", ] assert [e.sql("clickhouse") for e in model_to_sqlmesh.partitioned_by] == [ From 8efbe5fbeb8ab58be49cc1e26ffd8876f8d4e39c Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Sun, 8 Jun 2025 20:30:52 +0300 Subject: [PATCH 0367/1056] Feat(lsp): Add support for find and go to references for Model usages (#4680) --- sqlmesh/lsp/main.py | 19 +- sqlmesh/lsp/reference.py | 189 +++++++++-- tests/lsp/test_reference.py | 12 +- tests/lsp/test_reference_cte.py | 6 +- tests/lsp/test_reference_macro.py | 3 +- tests/lsp/test_reference_macro_multi.py | 3 +- tests/lsp/test_reference_model_find_all.py | 294 ++++++++++++++++++ .../extension/tests/find_references.spec.ts | 293 +++++++++++++++++ 8 files changed, 776 insertions(+), 43 deletions(-) create mode 100644 tests/lsp/test_reference_model_find_all.py diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 0e7d89be2a..3b42805920 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -44,7 +44,12 @@ CustomMethod, ) from sqlmesh.lsp.hints import get_hints -from sqlmesh.lsp.reference import get_references, get_cte_references +from sqlmesh.lsp.reference import ( + LSPCteReference, + LSPModelReference, + get_references, + get_all_references, +) from sqlmesh.lsp.uri import URI from web.server.api.endpoints.lineage import column_lineage, model_lineage from web.server.api.endpoints.models import get_models @@ -368,7 +373,7 @@ def hover(ls: LanguageServer, params: types.HoverParams) -> t.Optional[types.Hov if not references: return None reference = references[0] - if not reference.markdown_description: + if isinstance(reference, LSPCteReference) or not reference.markdown_description: return None return types.Hover( contents=types.MarkupContent( @@ -418,8 +423,8 @@ def goto_definition( references = get_references(self.lsp_context, uri, params.position) location_links = [] for reference in references: - # Use target_range if available (for CTEs), otherwise default to start of file - if reference.target_range: + # Use target_range if available (CTEs, Macros), otherwise default to start of file + if not isinstance(reference, LSPModelReference): target_range = reference.target_range target_selection_range = reference.target_range else: @@ -449,7 +454,7 @@ def goto_definition( def find_references( ls: LanguageServer, params: types.ReferenceParams ) -> t.Optional[t.List[types.Location]]: - """Find all references of a symbol (currently supporting CTEs)""" + """Find all references of a symbol (supporting CTEs, models for now)""" try: uri = URI(params.text_document.uri) self._ensure_context_for_document(uri) @@ -457,10 +462,10 @@ def find_references( if self.lsp_context is None: raise RuntimeError(f"No context found for document: {document.path}") - cte_references = get_cte_references(self.lsp_context, uri, params.position) + all_references = get_all_references(self.lsp_context, uri, params.position) # Convert references to Location objects - locations = [types.Location(uri=ref.uri, range=ref.range) for ref in cte_references] + locations = [types.Location(uri=ref.uri, range=ref.range) for ref in all_references] return locations if locations else None except Exception as e: diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 0ddef76cad..816449090f 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -1,6 +1,7 @@ from lsprotocol.types import Range, Position import typing as t from pathlib import Path +from pydantic import Field from sqlmesh.core.audit import StandaloneAudit from sqlmesh.core.dialect import normalize_model_name @@ -23,21 +24,37 @@ import inspect -class Reference(PydanticModel): - """ - A reference to a model or CTE. +class LSPModelReference(PydanticModel): + """A LSP reference to a model.""" + + type: t.Literal["model"] = "model" + uri: str + range: Range + markdown_description: t.Optional[str] = None - Attributes: - range: The range of the reference in the source file - uri: The uri of the referenced model - markdown_description: The markdown description of the referenced model - target_range: The range of the definition for go-to-definition (optional, used for CTEs) - """ +class LSPCteReference(PydanticModel): + """A LSP reference to a CTE.""" + + type: t.Literal["cte"] = "cte" + uri: str range: Range + target_range: Range + + +class LSPMacroReference(PydanticModel): + """A LSP reference to a macro.""" + + type: t.Literal["macro"] = "macro" uri: str + range: Range + target_range: Range markdown_description: t.Optional[str] = None - target_range: t.Optional[Range] = None + + +Reference = t.Annotated[ + t.Union[LSPModelReference, LSPCteReference, LSPMacroReference], Field(discriminator="type") +] def by_position(position: Position) -> t.Callable[[Reference], bool]: @@ -136,7 +153,7 @@ def get_model_definitions_for_a_path( return [] # Find all possible references - references = [] + references: t.List[Reference] = [] with open(file_path, "r", encoding="utf-8") as file: read_file = file.readlines() @@ -173,7 +190,7 @@ def get_model_definitions_for_a_path( table_range = to_lsp_range(table_range_sqlmesh) references.append( - Reference( + LSPCteReference( uri=document_uri.value, # Same file range=table_range, target_range=target_range, @@ -227,7 +244,7 @@ def get_model_definitions_for_a_path( description = generate_markdown_description(referenced_model) references.append( - Reference( + LSPModelReference( uri=referenced_model_uri.value, range=Range( start=to_lsp_position(start_pos_sqlmesh), @@ -286,7 +303,7 @@ def get_macro_definitions_for_a_path( return [] references = [] - config_for_model, config_path = lsp_context.context.config_for_path( + _, config_path = lsp_context.context.config_for_path( file_path, ) @@ -372,7 +389,7 @@ def get_macro_reference( # Create a reference to the macro definition macro_uri = URI.from_path(path) - return Reference( + return LSPMacroReference( uri=macro_uri.value, range=to_lsp_range(macro_range), target_range=Range( @@ -405,7 +422,7 @@ def get_built_in_macro_reference(macro_name: str, macro_range: Range) -> t.Optio # Calculate the end line number by counting the number of source lines end_line_number = line_number + len(source_lines) - 1 - return Reference( + return LSPMacroReference( uri=URI.from_path(Path(filename)).value, range=macro_range, target_range=Range( @@ -416,9 +433,99 @@ def get_built_in_macro_reference(macro_name: str, macro_range: Range) -> t.Optio ) +def get_model_find_all_references( + lint_context: LSPContext, document_uri: URI, position: Position +) -> t.List[LSPModelReference]: + """ + Get all references to a model across the entire project. + + This function finds all usages of a model in other files by searching through + all models in the project and checking their dependencies. + + Args: + lint_context: The LSP context + document_uri: The URI of the document + position: The position to check for model references + + Returns: + A list of references to the model across all files + """ + # First, get the references in the current file to determine what model we're looking for + current_file_references = [ + ref + for ref in get_model_definitions_for_a_path(lint_context, document_uri) + if isinstance(ref, LSPModelReference) + ] + + # Find the model reference at the cursor position + target_model_uri: t.Optional[str] = None + for ref in current_file_references: + if _position_within_range(position, ref.range): + # This is a model reference, get the target model URI + target_model_uri = ref.uri + break + + if target_model_uri is None: + return [] + + # Start with the model definition + all_references: t.List[LSPModelReference] = [ + LSPModelReference( + uri=ref.uri, + range=Range( + start=Position(line=0, character=0), + end=Position(line=0, character=0), + ), + markdown_description=ref.markdown_description, + ) + ] + + # Then add the original reference + for ref in current_file_references: + if ref.uri == target_model_uri and isinstance(ref, LSPModelReference): + all_references.append( + LSPModelReference( + uri=document_uri.value, + range=ref.range, + markdown_description=ref.markdown_description, + ) + ) + + # Search through the models in the project + for path, target in lint_context.map.items(): + if not isinstance(target, (ModelTarget, AuditTarget)): + continue + + file_uri = URI.from_path(path) + + # Skip current file, already processed + if file_uri.value == document_uri.value: + continue + + # Get model references for this file + file_references = [ + ref + for ref in get_model_definitions_for_a_path(lint_context, file_uri) + if isinstance(ref, LSPModelReference) + ] + + # Add references that point to the target model file + for ref in file_references: + if ref.uri == target_model_uri and isinstance(ref, LSPModelReference): + all_references.append( + LSPModelReference( + uri=file_uri.value, + range=ref.range, + markdown_description=ref.markdown_description, + ) + ) + + return all_references + + def get_cte_references( lint_context: LSPContext, document_uri: URI, position: Position -) -> t.List[Reference]: +) -> t.List[LSPCteReference]: """ Get all references to a CTE at a specific position in a document. @@ -432,12 +539,12 @@ def get_cte_references( Returns: A list of references to the CTE (including its definition and all usages) """ - references = get_model_definitions_for_a_path(lint_context, document_uri) - # Filter for CTE references (those with target_range set and same URI) - # TODO: Consider extending Reference class to explicitly indicate reference type instead - cte_references = [ - ref for ref in references if ref.target_range is not None and ref.uri == document_uri.value + # Filter to get the CTE references + cte_references: t.List[LSPCteReference] = [ + ref + for ref in get_model_definitions_for_a_path(lint_context, document_uri) + if isinstance(ref, LSPCteReference) ] if not cte_references: @@ -450,7 +557,7 @@ def get_cte_references( target_cte_definition_range = ref.target_range break # Check if cursor is on the CTE definition - elif ref.target_range and _position_within_range(position, ref.target_range): + elif _position_within_range(position, ref.target_range): target_cte_definition_range = ref.target_range break @@ -459,10 +566,10 @@ def get_cte_references( # Add the CTE definition matching_references = [ - Reference( + LSPCteReference( uri=document_uri.value, range=target_cte_definition_range, - markdown_description="CTE definition", + target_range=target_cte_definition_range, ) ] @@ -470,16 +577,44 @@ def get_cte_references( for ref in cte_references: if ref.target_range == target_cte_definition_range: matching_references.append( - Reference( + LSPCteReference( uri=document_uri.value, range=ref.range, - markdown_description="CTE usage", + target_range=ref.target_range, ) ) return matching_references +def get_all_references( + lint_context: LSPContext, document_uri: URI, position: Position +) -> t.Sequence[Reference]: + """ + Get all references of a symbol at a specific position in a document. + + This function determines the type of reference (CTE, model for now) at the cursor + position and returns all references to that symbol across the project. + + Args: + lint_context: The LSP context + document_uri: The URI of the document + position: The position to check for references + + Returns: + A list of references to the symbol at the given position + """ + # First try CTE references (within same file) + if cte_references := get_cte_references(lint_context, document_uri, position): + return cte_references + + # Then try model references (across files) + if model_references := get_model_find_all_references(lint_context, document_uri, position): + return model_references + + return [] + + def _position_within_range(position: Position, range: Range) -> bool: """Check if a position is within a given range.""" return ( diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index dc9f9ea982..f39bddc059 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -1,7 +1,7 @@ from lsprotocol.types import Position from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget -from sqlmesh.lsp.reference import get_model_definitions_for_a_path, by_position +from sqlmesh.lsp.reference import LSPModelReference, get_model_definitions_for_a_path, by_position from sqlmesh.lsp.uri import URI @@ -47,9 +47,13 @@ def test_reference_with_alias() -> None: if isinstance(info, ModelTarget) and "sushi.waiter_revenue_by_day" in info.names ) - references = get_model_definitions_for_a_path( - lsp_context, URI.from_path(waiter_revenue_by_day_path) - ) + references = [ + ref + for ref in get_model_definitions_for_a_path( + lsp_context, URI.from_path(waiter_revenue_by_day_path) + ) + if isinstance(ref, LSPModelReference) + ] assert len(references) == 3 with open(waiter_revenue_by_day_path, "r") as file: diff --git a/tests/lsp/test_reference_cte.py b/tests/lsp/test_reference_cte.py index 74e134c6f5..32cce8dc60 100644 --- a/tests/lsp/test_reference_cte.py +++ b/tests/lsp/test_reference_cte.py @@ -1,7 +1,7 @@ import re from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget -from sqlmesh.lsp.reference import get_references +from sqlmesh.lsp.reference import LSPCteReference, get_references from sqlmesh.lsp.uri import URI from lsprotocol.types import Range, Position import typing as t @@ -28,7 +28,7 @@ def test_cte_parsing(): references = get_references(lsp_context, URI.from_path(sushi_customers_path), position) assert len(references) == 1 assert references[0].uri == URI.from_path(sushi_customers_path).value - assert references[0].markdown_description is None + assert isinstance(references[0], LSPCteReference) assert ( references[0].range.start.line == ranges[1].start.line ) # The reference location (where we clicked) @@ -43,7 +43,7 @@ def test_cte_parsing(): references = get_references(lsp_context, URI.from_path(sushi_customers_path), position) assert len(references) == 1 assert references[0].uri == URI.from_path(sushi_customers_path).value - assert references[0].markdown_description is None + assert isinstance(references[0], LSPCteReference) assert ( references[0].range.start.line == ranges[1].start.line ) # The reference location (where we clicked) diff --git a/tests/lsp/test_reference_macro.py b/tests/lsp/test_reference_macro.py index 806fd1f819..705a8b48e2 100644 --- a/tests/lsp/test_reference_macro.py +++ b/tests/lsp/test_reference_macro.py @@ -1,6 +1,6 @@ from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget -from sqlmesh.lsp.reference import get_macro_definitions_for_a_path +from sqlmesh.lsp.reference import LSPMacroReference, get_macro_definitions_for_a_path from sqlmesh.lsp.uri import URI @@ -24,5 +24,6 @@ def test_macro_references() -> None: # Check that all references point to the utils.py file for ref in macro_references: + assert isinstance(ref, LSPMacroReference) assert ref.uri.endswith("sushi/macros/utils.py") assert ref.target_range is not None diff --git a/tests/lsp/test_reference_macro_multi.py b/tests/lsp/test_reference_macro_multi.py index 410ce69d0c..db4464ffac 100644 --- a/tests/lsp/test_reference_macro_multi.py +++ b/tests/lsp/test_reference_macro_multi.py @@ -1,6 +1,6 @@ from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget -from sqlmesh.lsp.reference import get_macro_definitions_for_a_path +from sqlmesh.lsp.reference import LSPMacroReference, get_macro_definitions_for_a_path from sqlmesh.lsp.uri import URI @@ -19,5 +19,6 @@ def test_macro_references_multirepo() -> None: assert len(macro_references) == 2 for ref in macro_references: + assert isinstance(ref, LSPMacroReference) assert ref.uri.endswith("multi/repo_2/macros/__init__.py") assert ref.target_range is not None diff --git a/tests/lsp/test_reference_model_find_all.py b/tests/lsp/test_reference_model_find_all.py new file mode 100644 index 0000000000..c494ef7af3 --- /dev/null +++ b/tests/lsp/test_reference_model_find_all.py @@ -0,0 +1,294 @@ +from lsprotocol.types import Position +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget +from sqlmesh.lsp.reference import ( + get_model_find_all_references, + get_model_definitions_for_a_path, +) +from sqlmesh.lsp.uri import URI +from tests.lsp.test_reference_cte import find_ranges_from_regex + + +def test_find_references_for_model_usages(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Find customers model which uses sushi.orders + customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Find sushi.orders reference + ranges = find_ranges_from_regex(read_file, r"sushi\.orders") + assert len(ranges) >= 1, "Should find at least one reference to sushi.orders" + + # Click on the model reference + position = Position(line=ranges[0].start.line, character=ranges[0].start.character + 6) + references = get_model_find_all_references(lsp_context, URI.from_path(customers_path), position) + assert len(references) >= 6, ( + f"Expected at least 6 references to sushi.orders, found {len(references)}" + ) + + # Verify expected files are present + reference_files = {ref.uri for ref in references} + expected_patterns = [ + "orders", + "customers", + "customer_revenue_by_day", + "customer_revenue_lifetime", + "latest_order", + "waiter_revenue_by_day", + ] + for pattern in expected_patterns: + assert any(pattern in uri for uri in reference_files), ( + f"Missing reference in file containing '{pattern}'" + ) + + # Verify exact ranges for each reference pattern + expected_ranges = { + "orders": (0, 0, 0, 0), # the start for the model itself + "customers": (30, 7, 30, 19), + "waiter_revenue_by_day": (19, 5, 19, 17), + "customer_revenue_lifetime": (38, 7, 38, 19), + "customer_revenue_by_day": (33, 5, 33, 17), + "latest_order": (12, 5, 12, 17), + } + + for ref in references: + matched_pattern = None + for pattern in expected_patterns: + if pattern in ref.uri: + matched_pattern = pattern + break + + assert matched_pattern is not None, ( + f"Reference URI {ref.uri} doesn't match any expected pattern" + ) + + # Get expected range for this model + expected_start_line, expected_start_char, expected_end_line, expected_end_char = ( + expected_ranges[matched_pattern] + ) + + # Assert exact range match + assert ref.range.start.line == expected_start_line, ( + f"Expected {matched_pattern} reference start line {expected_start_line}, found {ref.range.start.line}" + ) + assert ref.range.start.character == expected_start_char, ( + f"Expected {matched_pattern} reference start character {expected_start_char}, found {ref.range.start.character}" + ) + assert ref.range.end.line == expected_end_line, ( + f"Expected {matched_pattern} reference end line {expected_end_line}, found {ref.range.end.line}" + ) + assert ref.range.end.character == expected_end_char, ( + f"Expected {matched_pattern} reference end character {expected_end_char}, found {ref.range.end.character}" + ) + + +def test_find_references_for_marketing_model(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Find sushi.marketing reference + marketing_ranges = find_ranges_from_regex(read_file, r"sushi\.marketing") + assert len(marketing_ranges) >= 1, "Should find at least one reference to sushi.marketing" + + position = Position( + line=marketing_ranges[0].start.line, character=marketing_ranges[0].start.character + 8 + ) + references = get_model_find_all_references(lsp_context, URI.from_path(customers_path), position) + + # sushi.marketing should have exactly 2 references: model itself + customers usage + assert len(references) == 2, ( + f"Expected exactly 2 references to sushi.marketing, found {len(references)}" + ) + + # Verify files are present + reference_files = {ref.uri for ref in references} + expected_patterns = ["marketing", "customers"] + for pattern in expected_patterns: + assert any(pattern in uri for uri in reference_files), ( + f"Missing reference in file containing '{pattern}'" + ) + + +def test_find_references_for_python_model(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Start from customer_revenue_by_day which references sushi.items + revenue_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customer_revenue_by_day" in info.names + ) + + with open(revenue_path, "r", encoding="utf-8") as file: + revenue_file = file.readlines() + + # Find sushi.items reference + items_ranges = find_ranges_from_regex(revenue_file, r"sushi\.items") + assert len(items_ranges) >= 1, "Should find at least one reference to sushi.items" + + position = Position( + line=items_ranges[0].start.line, character=items_ranges[0].start.character + 6 + ) + references = get_model_find_all_references(lsp_context, URI.from_path(revenue_path), position) + assert len(references) == 5 + + # Verify expected files + reference_files = {ref.uri for ref in references} + + # Models and also the Audit which references it: assert_item_price_above_zero + expected_patterns = [ + "items", + "customer_revenue_by_day", + "customer_revenue_lifetime", + "waiter_revenue_by_day", + "assert_item_price_above_zero", + ] + for pattern in expected_patterns: + assert any(pattern in uri for uri in reference_files), ( + f"Missing reference in file containing '{pattern}'" + ) + + +def test_waiter_revenue_by_day_multiple_references(): + # Test sushi.waiter_revenue_by_day which is referenced 3 times in top_waiters + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + top_waiters_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.top_waiters" in info.names + ) + + with open(top_waiters_path, "r", encoding="utf-8") as file: + top_waiters_file = file.readlines() + + # Find multiple references to sushi.waiter_revenue_by_day + waiter_revenue_ranges = find_ranges_from_regex( + top_waiters_file, r"sushi\.waiter_revenue_by_day" + ) + assert len(waiter_revenue_ranges) >= 2, ( + "Should find at least 2 references to sushi.waiter_revenue_by_day in top_waiters" + ) + + # Click on the first reference + position = Position( + line=waiter_revenue_ranges[0].start.line, + character=waiter_revenue_ranges[0].start.character + 10, + ) + references = get_model_find_all_references( + lsp_context, URI.from_path(top_waiters_path), position + ) + + # Should find model definition + 3 references in top_waiters = 4 total + assert len(references) == 4, ( + f"Expected exactly 4 references to sushi.waiter_revenue_by_day, found {len(references)}" + ) + + # Count references in top_waiters file + top_waiters_refs = [ref for ref in references if "top_waiters" in ref.uri] + assert len(top_waiters_refs) == 3, ( + f"Expected exactly 3 references in top_waiters, found {len(top_waiters_refs)}" + ) + + # Verify model definition is included + assert any("waiter_revenue_by_day" in ref.uri for ref in references), ( + "Should include model definition" + ) + + +def test_precise_character_positions(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + # Test clicking on different parts of "sushi.orders" reference at line 31 + + # Click on 's' in "sushi" - should work + position = Position(line=30, character=7) + references = get_model_find_all_references(lsp_context, URI.from_path(customers_path), position) + assert len(references) > 0, "Should find references when clicking on 's' in 'sushi'" + + # Click on '.' between sushi and orders - should work + position = Position(line=30, character=12) + references = get_model_find_all_references(lsp_context, URI.from_path(customers_path), position) + assert len(references) > 0, "Should find references when clicking on '.' separator" + + # Click on 'o' in "orders" - should work + position = Position(line=30, character=13) + references = get_model_find_all_references(lsp_context, URI.from_path(customers_path), position) + assert len(references) > 0, "Should find references when clicking on 'o' in 'orders'" + + # Click just before "sushi" - should not work + position = Position(line=30, character=6) + references = get_model_find_all_references(lsp_context, URI.from_path(customers_path), position) + assert len(references) == 0, "Should not find references when clicking just before 'sushi'" + + # Click just after "orders" - should not work + position = Position(line=30, character=21) + references = get_model_find_all_references(lsp_context, URI.from_path(customers_path), position) + assert len(references) == 0, "Should not find references when clicking just after 'orders'" + + +def test_audit_model_references(): + # Tests finding model references in audits + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Find audit files + audit_paths = [path for path, info in lsp_context.map.items() if isinstance(info, AuditTarget)] + + if audit_paths: + audit_path = audit_paths[0] + refs = get_model_definitions_for_a_path(lsp_context, URI.from_path(audit_path)) + + # Audits can reference models + if refs: + # Click on the first reference which is: sushi.items + first_ref = refs[0] + position = Position( + line=first_ref.range.start.line, character=first_ref.range.start.character + 1 + ) + references = get_model_find_all_references( + lsp_context, URI.from_path(audit_path), position + ) + + assert len(references) == 5, "Should find references from audit files as well" + + reference_files = {ref.uri for ref in references} + + # Models and also the Audit which references it: assert_item_price_above_zero + expected_patterns = [ + "items", + "customer_revenue_by_day", + "customer_revenue_lifetime", + "waiter_revenue_by_day", + "assert_item_price_above_zero", + ] + for pattern in expected_patterns: + assert any(pattern in uri for uri in reference_files), ( + f"Missing reference in file containing '{pattern}'" + ) diff --git a/vscode/extension/tests/find_references.spec.ts b/vscode/extension/tests/find_references.spec.ts index 1733fd362a..cb97b696af 100644 --- a/vscode/extension/tests/find_references.spec.ts +++ b/vscode/extension/tests/find_references.spec.ts @@ -9,6 +9,299 @@ const GO_TO_REFERENCES_KEY = 'Shift+F12' const FIND_ALL_REFERENCES_KEY = process.platform === 'darwin' ? 'Alt+Shift+F12' : 'Ctrl+Shift+F12' +test.describe('Model References', () => { + let tempDir: string + let window: any + let close: () => Promise + + test.beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const vscode = await startVSCode(tempDir) + window = vscode.window + close = vscode.close + }) + + test.afterEach(async () => { + await close() + fs.removeSync(tempDir) + }) + + test('Go to References (Shift+F12) for Model usage', async () => { + // Step 1: Expand the models folder in the file explorer to access model files + await window.waitForSelector('text=models') + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Step 2: Open customers.sql which contains references to other models + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Step 3: Ensure SQLMesh extension has fully loaded by checking for model metadata + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Step 4: Position cursor on the sushi.orders model reference in the SQL query + await window.locator('text=sushi.orders').first().click() + + // Step 5: Trigger "Go to References" command using Shift+F12 keyboard shortcut + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Step 6: Wait for VSCode references panel to appear at the bottom + await window.waitForSelector('text=References') + + // Step 7: Ensure references panel has populated with all usages of sushi.orders model + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 6 + }, + { timeout: 10000 }, + ) + + // Step 8: Verify the references panel shows both SQL and Python files containing references + const hasReferences = await window.evaluate(() => { + const body = document.body.textContent || '' + return ( + body.includes('References') && + (body.includes('.sql') || body.includes('.py')) + ) + }) + + expect(hasReferences).toBe(true) + + // Step 9: Find and click on the orders.py reference to navigate to the model definition + let clickedReference = false + + const referenceItems = await window.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() + + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Search for the orders.py reference which contains the Python model definition + if (text && text.includes('orders.py')) { + await item.click() + clickedReference = true + break + } + } + + expect(clickedReference).toBe(true) + + // Step 10: Verify successful navigation to orders.py by checking for unique Python code + await expect(window.locator('text=list(range(0, 100))')).toBeVisible() + }) + + test('Find All References (Alt+Shift+F12) for Model', async () => { + // Step 1: Expand the models folder to access SQLMesh model files + await window.waitForSelector('text=models') + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Step 2: Open customers.sql which contains multiple model references + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Step 3: Wait for SQLMesh context to fully initialize + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Step 4: Click on sushi.orders model reference to position cursor + await window.locator('text=sushi.orders').first().click() + + // Step 5: Trigger "Find All References" command using Alt+Shift+F12 (or Ctrl+Shift+F12 on Windows/Linux) + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + + let clickedReference = false + const referenceItems = await window.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() + + // Step 6: Iterate through references to find and click on orders.py + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Find the orders.py reference which contains the model implementation + if (text && text.includes('orders.py')) { + await item.click() + + clickedReference = true + break + } + } + + expect(clickedReference).toBe(true) + + // Step 7: Verify navigation to orders.py by checking for Python import statement + await expect(window.locator('text=import random')).toBeVisible() + + // Step 8: Click on the import statement to ensure file is fully loaded and interactive + await window.locator('text=import random').first().click() + + // Step 9: Final verification that we're viewing the correct Python model file + await expect(window.locator('text=list(range(0, 100))')).toBeVisible() + }) + + test('Go to References for Model from Audit', async () => { + // Step 1: Expand audits folder to access SQLMesh audit files + await window.waitForSelector('text=audits') + await window + .getByRole('treeitem', { name: 'audits', exact: true }) + .locator('a') + .click() + + // Step 2: Open assert_item_price_above_zero.sql audit file which references sushi.items model + await window + .getByRole('treeitem', { + name: 'assert_item_price_above_zero.sql', + exact: true, + }) + .locator('a') + .click() + + // Step 3: Wait for audit file to load and SQLMesh context to initialize + await window.waitForSelector('text=standalone') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Step 4: Click on sushi.items model reference in the audit query + await window.locator('text=sushi.items').first().click() + + // Step 5: Trigger "Go to References" to find all places where sushi.items is used + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Step 6: Wait for VSCode references panel to appear + await window.waitForSelector('text=References') + + // Step 7: Ensure references panel shows multiple files that reference sushi.items + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 4 + }, + { timeout: 10000 }, + ) + + // Step 8: Verify references panel contains both audit and model files + const hasReferences = await window.evaluate(() => { + const body = document.body.textContent || '' + return ( + body.includes('References') && + (body.includes('.sql') || body.includes('.py')) + ) + }) + + expect(hasReferences).toBe(true) + + // 9. Click on one of the references to navigate to it + let clickedReference = false + + const referenceItems = await window.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() + + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Search for the customer_revenue_by_day.sql file which joins with sushi.items + if (text && text.includes('customer_revenue_by_day.sql')) { + await item.click() + clickedReference = true + break + } + } + + expect(clickedReference).toBe(true) + + // Step 10: Verify navigation to customer_revenue_by_day.sql by checking for SQL JOIN syntax + await expect(window.locator('text=LEFT JOIN')).toBeVisible() + + // Step 11: Click on LEFT JOIN to ensure file is interactive and verify content + await window.locator('text=LEFT JOIN').first().click() + await expect( + window.locator('text=FROM sushi.order_items AS oi'), + ).toBeVisible() + }) + + test('Find All Model References from Audit', async () => { + // Step 1: Expand audits folder in the file explorer + await window.waitForSelector('text=audits') + await window + .getByRole('treeitem', { name: 'audits', exact: true }) + .locator('a') + .click() + + // Step 2: Open the audit file that validates item prices + await window + .getByRole('treeitem', { + name: 'assert_item_price_above_zero.sql', + exact: true, + }) + .locator('a') + .click() + + // Step 3: Ensure audit file and SQLMesh context are fully loaded + await window.waitForSelector('text=standalone') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Step 4: Position cursor on sushi.items model reference + await window.locator('text=sushi.items').first().click() + + // Step 5: Use Find All References to see all occurrences across the project + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + + // Step 6: Click on a reference to navigate to customer_revenue_by_day.sql + let clickedReference = false + + const referenceItems = await window.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() + + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Look for a reference that contains customer_revenue_by_day + if (text && text.includes('customer_revenue_by_day.sql')) { + await item.click() + clickedReference = true + break + } + } + + expect(clickedReference).toBe(true) + + // Step 7: Verify successful navigation by checking for SQL JOIN statement + await expect(window.locator('text=LEFT JOIN')).toBeVisible() + + // Step 8: Interact with the file to verify it's fully loaded and check its content + await window.locator('text=LEFT JOIN').first().click() + await expect( + window.locator('text=FROM sushi.order_items AS oi'), + ).toBeVisible() + }) +}) + test.describe('CTE References', () => { let tempDir: string let window: any From 7dd52c59dc4c82e174ae9115ccb227ce85a3fa83 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 9 Jun 2025 15:05:24 +0200 Subject: [PATCH 0368/1056] feat: add macros to LSP completion (#4667) --- sqlmesh/lsp/completions.py | 20 +++- sqlmesh/lsp/context.py | 19 ++-- sqlmesh/lsp/custom.py | 1 + sqlmesh/lsp/helpers.py | 27 +++++ sqlmesh/lsp/main.py | 30 +++++- sqlmesh/lsp/reference.py | 25 +---- tests/lsp/test_completions.py | 25 ++++- vscode/extension/tests/completions.spec.ts | 109 +++++++++++++++++++++ 8 files changed, 210 insertions(+), 46 deletions(-) create mode 100644 sqlmesh/lsp/helpers.py diff --git a/sqlmesh/lsp/completions.py b/sqlmesh/lsp/completions.py index 6716f16e2f..1b1483c271 100644 --- a/sqlmesh/lsp/completions.py +++ b/sqlmesh/lsp/completions.py @@ -1,13 +1,16 @@ from functools import lru_cache from sqlglot import Dialect, Tokenizer from sqlmesh.lsp.custom import AllModelsResponse +from sqlmesh import macro import typing as t from sqlmesh.lsp.context import AuditTarget, LSPContext, ModelTarget from sqlmesh.lsp.uri import URI def get_sql_completions( - context: t.Optional[LSPContext], file_uri: t.Optional[URI], content: t.Optional[str] = None + context: t.Optional[LSPContext] = None, + file_uri: t.Optional[URI] = None, + content: t.Optional[str] = None, ) -> AllModelsResponse: """ Return a list of completions for a given file. @@ -26,6 +29,7 @@ def get_sql_completions( return AllModelsResponse( models=list(get_models(context, file_uri)), keywords=all_keywords, + macros=list(get_macros(context, file_uri)), ) @@ -56,6 +60,17 @@ def get_models(context: t.Optional[LSPContext], file_uri: t.Optional[URI]) -> t. return all_models +def get_macros(context: t.Optional[LSPContext], file_uri: t.Optional[URI]) -> t.Set[str]: + """Return a set of all macros with the ``@`` prefix.""" + names = set(macro.get_registry()) + try: + if context is not None: + names.update(context.context._macros) + except Exception: + pass + return names + + def get_keywords(context: t.Optional[LSPContext], file_uri: t.Optional[URI]) -> t.Set[str]: """ Return a list of sql keywords for a given file. @@ -138,6 +153,7 @@ def get_dialect(context: t.Optional[LSPContext], file_uri: t.Optional[URI]) -> t def extract_keywords_from_content(content: str, dialect: t.Optional[str] = None) -> t.Set[str]: """ Extract identifiers from SQL content using the tokenizer. + Only extracts identifiers (variable names, table names, column names, etc.) that are not SQL keywords. """ @@ -155,7 +171,7 @@ def extract_keywords_from_content(content: str, dialect: t.Optional[str] = None) keywords.add(token.text) except Exception: - # If tokenization fails, return empty set + # If tokenization fails, return an empty set pass return keywords diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 4ac55f1a22..f3bdcc13e3 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -176,18 +176,13 @@ def list_of_models_for_rendering(self) -> t.List[ModelForRendering]: if audit._path is not None ] - def get_autocomplete( - self, uri: t.Optional[URI], content: t.Optional[str] = None + @staticmethod + def get_completions( + self: t.Optional["LSPContext"] = None, + uri: t.Optional[URI] = None, + file_content: t.Optional[str] = None, ) -> AllModelsResponse: - """Get autocomplete suggestions for a file. - - Args: - uri: The URI of the file to get autocomplete suggestions for. - content: The content of the file (optional). - - Returns: - AllModelsResponse containing models and keywords. - """ + """Get completion suggestions for a file""" from sqlmesh.lsp.completions import get_sql_completions - return get_sql_completions(self, uri, content) + return get_sql_completions(self, uri, file_content) diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py index 9e0bc07cd4..72c1ec7917 100644 --- a/sqlmesh/lsp/custom.py +++ b/sqlmesh/lsp/custom.py @@ -20,6 +20,7 @@ class AllModelsResponse(PydanticModel): models: t.List[str] keywords: t.List[str] + macros: t.List[str] RENDER_MODEL_FEATURE = "sqlmesh/render_model" diff --git a/sqlmesh/lsp/helpers.py b/sqlmesh/lsp/helpers.py new file mode 100644 index 0000000000..7aa06ccb4c --- /dev/null +++ b/sqlmesh/lsp/helpers.py @@ -0,0 +1,27 @@ +from lsprotocol.types import Range, Position + +from sqlmesh.core.linter.helpers import ( + Range as SQLMeshRange, + Position as SQLMeshPosition, +) + + +def to_lsp_range( + range: SQLMeshRange, +) -> Range: + """ + Converts a SQLMesh Range to an LSP Range. + """ + return Range( + start=Position(line=range.start.line, character=range.start.character), + end=Position(line=range.end.line, character=range.end.character), + ) + + +def to_lsp_position( + position: SQLMeshPosition, +) -> Position: + """ + Converts a SQLMesh Position to an LSP Position. + """ + return Position(line=position.line, character=position.character) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 3b42805920..2295a4b95f 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -145,7 +145,7 @@ def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsRespons try: context = self._context_get_or_load(uri) - return context.get_autocomplete(uri, content) + return LSPContext.get_completions(context, uri, content) except Exception as e: from sqlmesh.lsp.completions import get_sql_completions @@ -565,7 +565,10 @@ def workspace_diagnostic( ) return types.WorkspaceDiagnosticReport(items=[]) - @self.server.feature(types.TEXT_DOCUMENT_COMPLETION) + @self.server.feature( + types.TEXT_DOCUMENT_COMPLETION, + types.CompletionOptions(trigger_characters=["@"]), # advertise "@" for macros + ) def completion( ls: LanguageServer, params: types.CompletionParams ) -> t.Optional[types.CompletionList]: @@ -583,7 +586,7 @@ def completion( pass # Get completions using the existing completions module - completion_response = context.get_autocomplete(uri, content) + completion_response = LSPContext.get_completions(context, uri, content) completion_items = [] # Add model completions @@ -595,7 +598,26 @@ def completion( detail="SQLMesh Model", ) ) - # Add keyword completions + # Add macro completions + triggered_by_at = ( + params.context is not None + and getattr(params.context, "trigger_character", None) == "@" + ) + + for macro_name in completion_response.macros: + insert_text = macro_name if triggered_by_at else f"@{macro_name}" + + completion_items.append( + types.CompletionItem( + label=f"@{macro_name}", + insert_text=insert_text, + insert_text_format=types.InsertTextFormat.PlainText, + filter_text=macro_name, + kind=types.CompletionItemKind.Function, + detail="SQLMesh Macro", + ) + ) + for keyword in completion_response.keywords: completion_items.append( types.CompletionItem( diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 816449090f..533ef75332 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -7,14 +7,14 @@ from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core.linter.helpers import ( TokenPositionDetails, - Range as SQLMeshRange, - Position as SQLMeshPosition, ) from sqlmesh.core.model.definition import SqlModel from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget from sqlglot import exp from sqlmesh.lsp.description import generate_markdown_description from sqlglot.optimizer.scope import build_scope + +from sqlmesh.lsp.helpers import to_lsp_range, to_lsp_position from sqlmesh.lsp.uri import URI from sqlmesh.utils.pydantic import PydanticModel from sqlglot.optimizer.normalize_identifiers import normalize_identifiers @@ -624,24 +624,3 @@ def _position_within_range(position: Position, range: Range) -> bool: range.end.line > position.line or (range.end.line == position.line and range.end.character >= position.character) ) - - -def to_lsp_range( - range: SQLMeshRange, -) -> Range: - """ - Converts a SQLMesh Range to an LSP Range. - """ - return Range( - start=Position(line=range.start.line, character=range.start.character), - end=Position(line=range.end.line, character=range.end.character), - ) - - -def to_lsp_position( - position: SQLMeshPosition, -) -> Position: - """ - Converts a SQLMesh Position to an LSP Position. - """ - return Position(line=position.line, character=position.character) diff --git a/tests/lsp/test_completions.py b/tests/lsp/test_completions.py index e365873c19..8977d178ba 100644 --- a/tests/lsp/test_completions.py +++ b/tests/lsp/test_completions.py @@ -22,12 +22,27 @@ def test_get_sql_completions_no_context(): assert len(completions.models) == 0 +def test_get_macros(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + file_path = next(key for key in lsp_context.map.keys() if key.name == "active_customers.sql") + with open(file_path, "r", encoding="utf-8") as f: + file_content = f.read() + + file_uri = URI.from_path(file_path) + completions = LSPContext.get_completions(lsp_context, file_uri, file_content) + + assert "each" in completions.macros + assert "add_one" in completions.macros + + def test_get_sql_completions_with_context_no_file_uri(): context = Context(paths=["examples/sushi"]) lsp_context = LSPContext(context) - completions = lsp_context.get_autocomplete(None) - assert len(completions.keywords) > len(TOKENIZER_KEYWORDS) + completions = LSPContext.get_completions(lsp_context, None) + assert len(completions.keywords) >= len(TOKENIZER_KEYWORDS) assert "sushi.active_customers" in completions.models assert "sushi.customers" in completions.models @@ -37,7 +52,7 @@ def test_get_sql_completions_with_context_and_file_uri(): lsp_context = LSPContext(context) file_uri = next(key for key in lsp_context.map.keys() if key.name == "active_customers.sql") - completions = lsp_context.get_autocomplete(URI.from_path(file_uri)) + completions = LSPContext.get_completions(lsp_context, URI.from_path(file_uri)) assert len(completions.keywords) > len(TOKENIZER_KEYWORDS) assert "sushi.active_customers" not in completions.models @@ -84,7 +99,7 @@ def test_get_sql_completions_with_file_content(): """ file_uri = next(key for key in lsp_context.map.keys() if key.name == "active_customers.sql") - completions = lsp_context.get_autocomplete(URI.from_path(file_uri), content) + completions = LSPContext.get_completions(lsp_context, URI.from_path(file_uri), content) # Check that SQL keywords are included assert any(k in ["SELECT", "FROM", "WHERE", "JOIN"] for k in completions.keywords) @@ -129,7 +144,7 @@ def test_get_sql_completions_with_partial_cte_query(): """ file_uri = next(key for key in lsp_context.map.keys() if key.name == "active_customers.sql") - completions = lsp_context.get_autocomplete(URI.from_path(file_uri), content) + completions = LSPContext.get_completions(lsp_context, URI.from_path(file_uri), content) # Check that CTE names are included in the keywords keywords_list = completions.keywords diff --git a/vscode/extension/tests/completions.spec.ts b/vscode/extension/tests/completions.spec.ts index f0d167b91f..3c22e388f3 100644 --- a/vscode/extension/tests/completions.spec.ts +++ b/vscode/extension/tests/completions.spec.ts @@ -47,9 +47,118 @@ test('Autocomplete for model names', async () => { expect( await window.locator('text=sushi.waiter_as_customer_by_day').count(), ).toBe(1) + expect(await window.locator('text=SQLMesh Model').count()).toBe(1) await close() } finally { await fs.remove(tempDir) } }) + +// Skip the macro completions test as regular checks because they are flaky and +// covered in other non-integration tests. +test.describe('Macro Completions', () => { + test('Completion for inbuilt macros', async () => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-sushi-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + try { + const { window, close } = await startVSCode(tempDir) + + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + await window.locator('text=grain').first().click() + + // Move to the end of the file + await window.keyboard.press('Control+End') + + // Add a new line + await window.keyboard.press('Enter') + + await window.waitForTimeout(500) + + // Hit the '@' key to trigger autocomplete for inbuilt macros + await window.keyboard.press('@') + await window.keyboard.type('eac') + + // Wait a moment for autocomplete to appear + await window.waitForTimeout(500) + + // Check if the autocomplete suggestion for inbuilt macros is visible + expect(await window.locator('text=@each').count()).toBe(1) + + await close() + } finally { + await fs.remove(tempDir) + } + }) + + test('Completion for custom macros', async () => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-sushi-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + try { + const { window, close } = await startVSCode(tempDir) + + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + await window.locator('text=grain').first().click() + + // Move to the end of the file + await window.keyboard.press('Control+End') + + // Add a new line + await window.keyboard.press('Enter') + + // Type the beginning of a macro to trigger autocomplete + await window.keyboard.press('@') + await window.keyboard.type('add_o') + + // Wait a moment for autocomplete to appear + await window.waitForTimeout(500) + + // Check if the autocomplete suggestion for custom macros is visible + expect(await window.locator('text=@add_one').count()).toBe(1) + + await close() + } finally { + await fs.remove(tempDir) + } + }) +}) From f87d3f0a6dad639584a4b70540ac19010c0ca92c Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 9 Jun 2025 18:28:56 +0300 Subject: [PATCH 0369/1056] Feat(lsp): Add find all and go to references support for Macros (#4692) --- examples/sushi/models/customers.sql | 3 +- sqlmesh/lsp/reference.py | 167 ++++++++++----- .../github/cicd/test_integration.py | 2 +- tests/lsp/test_reference_macro_find_all.py | 191 ++++++++++++++++++ tests/test_forking.py | 6 +- tests/web/test_lineage.py | 6 +- .../extension/tests/find_references.spec.ts | 141 +++++++++++++ 7 files changed, 464 insertions(+), 52 deletions(-) create mode 100644 tests/lsp/test_reference_macro_find_all.py diff --git a/examples/sushi/models/customers.sql b/examples/sushi/models/customers.sql index 6429bacbc1..24b3aaa208 100644 --- a/examples/sushi/models/customers.sql +++ b/examples/sushi/models/customers.sql @@ -33,7 +33,8 @@ LEFT JOIN ( WITH current_marketing AS ( SELECT customer_id, - status + status, + @ADD_ONE(1) AS another_column, FROM current_marketing_outer ) SELECT * FROM current_marketing diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 533ef75332..e401be898c 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -450,75 +450,76 @@ def get_model_find_all_references( Returns: A list of references to the model across all files """ - # First, get the references in the current file to determine what model we're looking for - current_file_references = [ - ref - for ref in get_model_definitions_for_a_path(lint_context, document_uri) - if isinstance(ref, LSPModelReference) - ] - # Find the model reference at the cursor position - target_model_uri: t.Optional[str] = None - for ref in current_file_references: - if _position_within_range(position, ref.range): - # This is a model reference, get the target model URI - target_model_uri = ref.uri - break + model_at_position = next( + filter( + lambda ref: isinstance(ref, LSPModelReference) + and _position_within_range(position, ref.range), + get_model_definitions_for_a_path(lint_context, document_uri), + ), + None, + ) - if target_model_uri is None: + if not model_at_position: return [] + assert isinstance(model_at_position, LSPModelReference) # for mypy + + target_model_uri = model_at_position.uri + # Start with the model definition all_references: t.List[LSPModelReference] = [ LSPModelReference( - uri=ref.uri, + uri=model_at_position.uri, range=Range( start=Position(line=0, character=0), end=Position(line=0, character=0), ), - markdown_description=ref.markdown_description, + markdown_description=model_at_position.markdown_description, ) ] - # Then add the original reference - for ref in current_file_references: - if ref.uri == target_model_uri and isinstance(ref, LSPModelReference): - all_references.append( - LSPModelReference( - uri=document_uri.value, - range=ref.range, - markdown_description=ref.markdown_description, - ) + # Then add references from the current file + current_file_refs = filter( + lambda ref: isinstance(ref, LSPModelReference) and ref.uri == target_model_uri, + get_model_definitions_for_a_path(lint_context, document_uri), + ) + + for ref in current_file_refs: + assert isinstance(ref, LSPModelReference) # for mypy + + all_references.append( + LSPModelReference( + uri=document_uri.value, + range=ref.range, + markdown_description=ref.markdown_description, ) + ) # Search through the models in the project - for path, target in lint_context.map.items(): - if not isinstance(target, (ModelTarget, AuditTarget)): - continue - + for path, _ in lint_context.map.items(): file_uri = URI.from_path(path) # Skip current file, already processed if file_uri.value == document_uri.value: continue - # Get model references for this file - file_references = [ - ref - for ref in get_model_definitions_for_a_path(lint_context, file_uri) - if isinstance(ref, LSPModelReference) - ] - - # Add references that point to the target model file - for ref in file_references: - if ref.uri == target_model_uri and isinstance(ref, LSPModelReference): - all_references.append( - LSPModelReference( - uri=file_uri.value, - range=ref.range, - markdown_description=ref.markdown_description, - ) + # Get model references that point to the target model + matching_refs = filter( + lambda ref: isinstance(ref, LSPModelReference) and ref.uri == target_model_uri, + get_model_definitions_for_a_path(lint_context, file_uri), + ) + + for ref in matching_refs: + assert isinstance(ref, LSPModelReference) # for mypy + + all_references.append( + LSPModelReference( + uri=file_uri.value, + range=ref.range, + markdown_description=ref.markdown_description, ) + ) return all_references @@ -587,13 +588,83 @@ def get_cte_references( return matching_references +def get_macro_find_all_references( + lsp_context: LSPContext, document_uri: URI, position: Position +) -> t.List[LSPMacroReference]: + """ + Get all references to a macro at a specific position in a document. + + This function finds all usages of a macro across the entire project. + + Args: + lsp_context: The LSP context + document_uri: The URI of the document + position: The position to check for macro references + + Returns: + A list of references to the macro across all files + """ + # Find the macro reference at the cursor position + macro_at_position = next( + filter( + lambda ref: isinstance(ref, LSPMacroReference) + and _position_within_range(position, ref.range), + get_macro_definitions_for_a_path(lsp_context, document_uri), + ), + None, + ) + + if not macro_at_position: + return [] + + assert isinstance(macro_at_position, LSPMacroReference) # for mypy + + target_macro_uri = macro_at_position.uri + target_macro_target_range = macro_at_position.target_range + + # Start with the macro definition + all_references: t.List[LSPMacroReference] = [ + LSPMacroReference( + uri=target_macro_uri, + range=target_macro_target_range, + target_range=target_macro_target_range, + markdown_description=None, + ) + ] + + # Search through all SQL and audit files in the project + for path, _ in lsp_context.map.items(): + file_uri = URI.from_path(path) + + # Get macro references that point to the same macro definition + matching_refs = filter( + lambda ref: isinstance(ref, LSPMacroReference) + and ref.uri == target_macro_uri + and ref.target_range == target_macro_target_range, + get_macro_definitions_for_a_path(lsp_context, file_uri), + ) + + for ref in matching_refs: + assert isinstance(ref, LSPMacroReference) # for mypy + all_references.append( + LSPMacroReference( + uri=file_uri.value, + range=ref.range, + target_range=ref.target_range, + markdown_description=ref.markdown_description, + ) + ) + + return all_references + + def get_all_references( lint_context: LSPContext, document_uri: URI, position: Position ) -> t.Sequence[Reference]: """ Get all references of a symbol at a specific position in a document. - This function determines the type of reference (CTE, model for now) at the cursor + This function determines the type of reference (CTE, model or macro) at the cursor position and returns all references to that symbol across the project. Args: @@ -612,6 +683,10 @@ def get_all_references( if model_references := get_model_find_all_references(lint_context, document_uri, position): return model_references + # Finally try macro references (across files) + if macro_references := get_macro_find_all_references(lint_context, document_uri, position): + return macro_references + return [] diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index 3de9d2cc9f..544352e893 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -1684,7 +1684,7 @@ def test_overlapping_changes_models( +++ -@@ -25,7 +25,8 @@ +@@ -29,7 +29,8 @@ SELECT DISTINCT CAST(o.customer_id AS INT) AS customer_id, diff --git a/tests/lsp/test_reference_macro_find_all.py b/tests/lsp/test_reference_macro_find_all.py new file mode 100644 index 0000000000..03f47d1dc2 --- /dev/null +++ b/tests/lsp/test_reference_macro_find_all.py @@ -0,0 +1,191 @@ +from lsprotocol.types import Position +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.reference import ( + get_macro_find_all_references, + get_macro_definitions_for_a_path, +) +from sqlmesh.lsp.uri import URI +from sqlmesh.core.linter.helpers import ( + read_range_from_file, + Range as SQLMeshRange, + Position as SQLMeshPosition, +) + + +def test_find_all_references_for_macro_add_one(): + """Test finding all references to the @ADD_ONE macro.""" + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Find the top_waiters model that uses @ADD_ONE macro + top_waiters_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.top_waiters" in info.names + ) + + top_waiters_uri = URI.from_path(top_waiters_path) + macro_references = get_macro_definitions_for_a_path(lsp_context, top_waiters_uri) + + # Find the @ADD_ONE reference + add_one_ref = next((ref for ref in macro_references if ref.range.start.line == 12), None) + assert add_one_ref is not None, "Should find @ADD_ONE reference in top_waiters" + + # Click on the @ADD_ONE macro at line 13, character 5 (the @ symbol) + position = Position(line=12, character=5) + + all_references = get_macro_find_all_references(lsp_context, top_waiters_uri, position) + + # Should find at least 2 references: the definition and the usage in top_waiters + assert len(all_references) >= 2, f"Expected at least 2 references, found {len(all_references)}" + + # Verify the macro definition is included + definition_refs = [ref for ref in all_references if "utils.py" in ref.uri] + assert len(definition_refs) >= 1, "Should include the macro definition in utils.py" + + # Verify the usage in top_waiters is included + usage_refs = [ref for ref in all_references if "top_waiters" in ref.uri] + assert len(usage_refs) >= 1, "Should include the usage in top_waiters.sql" + + expected_files = { + "utils.py": {"pattern": r"def add_one", "expected_content": "def add_one"}, + "customers.sql": {"pattern": r"@ADD_ONE\s*\(", "expected_content": "ADD_ONE"}, + "top_waiters.sql": {"pattern": r"@ADD_ONE\s*\(", "expected_content": "ADD_ONE"}, + } + + for expected_file, expectations in expected_files.items(): + file_refs = [ref for ref in all_references if expected_file in ref.uri] + assert len(file_refs) >= 1, f"Should find at least one reference in {expected_file}" + + file_ref = file_refs[0] + file_path = URI(file_ref.uri).to_path() + + sqlmesh_range = SQLMeshRange( + start=SQLMeshPosition( + line=file_ref.range.start.line, character=file_ref.range.start.character + ), + end=SQLMeshPosition( + line=file_ref.range.end.line, character=file_ref.range.end.character + ), + ) + + # Read the content at the reference location + content = read_range_from_file(file_path, sqlmesh_range) + assert content.startswith(expectations["expected_content"]), ( + f"Expected content to start with '{expectations['expected_content']}', got: {content}" + ) + + +def test_find_all_references_for_macro_multiply(): + """Test finding all references to the @MULTIPLY macro.""" + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Find the top_waiters model that uses @MULTIPLY macro + top_waiters_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.top_waiters" in info.names + ) + + top_waiters_uri = URI.from_path(top_waiters_path) + macro_references = get_macro_definitions_for_a_path(lsp_context, top_waiters_uri) + + # Find the @MULTIPLY reference + multiply_ref = next((ref for ref in macro_references if ref.range.start.line == 13), None) + assert multiply_ref is not None, "Should find @MULTIPLY reference in top_waiters" + + # Click on the @MULTIPLY macro at line 14, character 5 (the @ symbol) + position = Position(line=13, character=5) + all_references = get_macro_find_all_references(lsp_context, top_waiters_uri, position) + + # Should find at least 2 references: the definition and the usage + assert len(all_references) >= 2, f"Expected at least 2 references, found {len(all_references)}" + + # Verify both definition and usage are included + assert any("utils.py" in ref.uri for ref in all_references), "Should include macro definition" + assert any("top_waiters" in ref.uri for ref in all_references), "Should include usage" + + +def test_find_all_references_for_sql_literal_macro(): + """Test finding references to @SQL_LITERAL macro .""" + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Find the top_waiters model that uses @SQL_LITERAL macro + top_waiters_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.top_waiters" in info.names + ) + + top_waiters_uri = URI.from_path(top_waiters_path) + macro_references = get_macro_definitions_for_a_path(lsp_context, top_waiters_uri) + + # Find the @SQL_LITERAL reference + sql_literal_ref = next((ref for ref in macro_references if ref.range.start.line == 14), None) + assert sql_literal_ref is not None, "Should find @SQL_LITERAL reference in top_waiters" + + # Click on the @SQL_LITERAL macro + position = Position(line=14, character=5) + all_references = get_macro_find_all_references(lsp_context, top_waiters_uri, position) + + # For user-defined macros in utils.py, should find references + assert len(all_references) >= 2, f"Expected at least 2 references, found {len(all_references)}" + + +def test_find_references_from_outside_macro_position(): + """Test that clicking outside a macro doesn't return macro references.""" + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + top_waiters_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.top_waiters" in info.names + ) + + top_waiters_uri = URI.from_path(top_waiters_path) + + # Click on a position that is not on a macro + position = Position(line=0, character=0) # First line, which is a comment + all_references = get_macro_find_all_references(lsp_context, top_waiters_uri, position) + + # Should return empty list when not on a macro + assert len(all_references) == 0, "Should not find macro references when not on a macro" + + +def test_multi_repo_macro_references(): + """Test finding macro references across multiple repositories.""" + context = Context(paths=["examples/multi/repo_1", "examples/multi/repo_2"], gateway="memory") + lsp_context = LSPContext(context) + + # Find model 'd' which uses macros from repo_2 + d_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "silver.d" in info.names + ) + + d_uri = URI.from_path(d_path) + macro_references = get_macro_definitions_for_a_path(lsp_context, d_uri) + + if macro_references: + # Click on the second macro reference which appears under the same name in repo_1 ('dup') + first_ref = macro_references[1] + position = Position( + line=first_ref.range.start.line, character=first_ref.range.start.character + 1 + ) + all_references = get_macro_find_all_references(lsp_context, d_uri, position) + + # Should find the definition and usage + assert len(all_references) == 2, f"Expected 2 references, found {len(all_references)}" + + # Verify references from repo_2 + assert any("repo_2" in ref.uri for ref in all_references), "Should find macro in repo_2" + + # But not references in repo_1 since despite identical name they're different macros + assert not any("repo_1" in ref.uri for ref in all_references), ( + "Shouldn't find macro in repo_1" + ) diff --git a/tests/test_forking.py b/tests/test_forking.py index 5752c641c1..1cd50d9dec 100644 --- a/tests/test_forking.py +++ b/tests/test_forking.py @@ -46,12 +46,14 @@ def test_parallel_load(assert_exp_eq, mocker): WITH "current_marketing" AS ( SELECT "current_marketing_outer"."customer_id" AS "customer_id", - "current_marketing_outer"."status" AS "status" + "current_marketing_outer"."status" AS "status", + 2 AS "another_column" FROM "current_marketing_outer" AS "current_marketing_outer" ) SELECT "current_marketing"."customer_id" AS "customer_id", - "current_marketing"."status" AS "status" + "current_marketing"."status" AS "status", + "current_marketing"."another_column" AS "another_column" FROM "current_marketing" AS "current_marketing" ) AS "m" ON "m"."customer_id" = "o"."customer_id" diff --git a/tests/web/test_lineage.py b/tests/web/test_lineage.py index 345d492f34..1ed40431ef 100644 --- a/tests/web/test_lineage.py +++ b/tests/web/test_lineage.py @@ -62,12 +62,14 @@ def test_get_lineage(client: TestClient, web_sushi_context: Context) -> None: WITH "current_marketing" AS ( SELECT "current_marketing_outer"."customer_id" AS "customer_id", - "current_marketing_outer"."status" AS "status" + "current_marketing_outer"."status" AS "status", + 2 AS "another_column" FROM "current_marketing_outer" AS "current_marketing_outer" ) SELECT "current_marketing"."customer_id" AS "customer_id", - "current_marketing"."status" AS "status" + "current_marketing"."status" AS "status", + "current_marketing"."another_column" AS "another_column" FROM "current_marketing" AS "current_marketing" ) AS "m" ON "m"."customer_id" = "o"."customer_id" diff --git a/vscode/extension/tests/find_references.spec.ts b/vscode/extension/tests/find_references.spec.ts index cb97b696af..7b703257ad 100644 --- a/vscode/extension/tests/find_references.spec.ts +++ b/vscode/extension/tests/find_references.spec.ts @@ -477,3 +477,144 @@ test.describe('CTE References', () => { await expect(window.locator('text=customers.sql').first()).toBeVisible() }) }) + +test.describe('Macro References', () => { + let tempDir: string + let window: any + let close: () => Promise + + test.beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const vscode = await startVSCode(tempDir) + window = vscode.window + close = vscode.close + + // Common setup: navigate to top_waiters.sql which uses macros + await window.waitForSelector('text=models') + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await window + .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) + .locator('a') + .click() + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + }) + + test.afterEach(async () => { + await close() + fs.removeSync(tempDir) + }) + + test('Go to References for @ADD_ONE macro', async () => { + // Click on the @ADD_ONE macro usage + await window.locator('text=@ADD_ONE').first().click() + + // Use keyboard shortcut to find all references + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Wait for the references to appear + await window.waitForSelector('text=References') + + // Wait for reference panel to populate + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) + + // Verify that both the definition and two usages are shown + await expect(window.locator('text=utils.py').first()).toBeVisible() + await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() + await expect(window.locator('text=customers.sql').first()).toBeVisible() + }) + + test('Find All References for @MULTIPLY macro', async () => { + // Click on the @MULTIPLY macro usage and then navigate to it + await window.locator('text=@MULTIPLY').first().click() + + // Use keyboard shortcut to find all references + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + + // Verify references contains expected content + await window.waitForSelector('text=References') + + // Verify that both utils.py (definition) and top_waiters.sql (usage) are shown + await expect(window.locator('text=utils.py').first()).toBeVisible() + await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() + + // Click on the utils.py reference to navigate to the macro definition + let clickedReference = false + const referenceItems = await window.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() + + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Find the utils.py reference which contains the macro definition + if (text && text.includes('utils.py')) { + await item.click() + clickedReference = true + break + } + } + + expect(clickedReference).toBe(true) + + // Verify it appeared and click on it + await expect(window.locator('text=def multiply')).toBeVisible() + await window.locator('text=def multiply').first().click() + + // Verify navigation to utils.py by checking the import that appears there + await expect( + window.locator('text=from sqlmesh import SQL, macro'), + ).toBeVisible() + }) + + test('Go to References for @SQL_LITERAL macro', async () => { + // Click on the @SQL_LITERAL macro usage + await window.locator('text=@SQL_LITERAL').first().click() + + // Use keyboard shortcut to find references + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Wait for the references to appear + await window.waitForSelector('text=References') + + // Wait for reference panel to populate + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) + + // Verify that references include both definition and usage + const hasReferences = await window.evaluate(() => { + const body = document.body.textContent || '' + return ( + body.includes('References') && + body.includes('.py') && + body.includes('.sql') + ) + }) + + expect(hasReferences).toBe(true) + + await expect(window.locator('text=utils.py').first()).toBeVisible() + await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() + }) +}) From c26e1095b595eb569ee3a3a368d1f18d58d78da5 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 9 Jun 2025 12:46:29 -0700 Subject: [PATCH 0370/1056] fix: dockerfile for UI (#4697) --- web/Dockerfile.app | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/web/Dockerfile.app b/web/Dockerfile.app index 4cddd09250..b234f904b6 100644 --- a/web/Dockerfile.app +++ b/web/Dockerfile.app @@ -4,19 +4,15 @@ WORKDIR /app RUN apt-get update && apt-get -y install libnss3 libatk-bridge2.0-0 libdrm-dev libxkbcommon-dev libgbm-dev libasound-dev libatspi2.0-0 libxshmfence-dev -# Copy package files for workspaces -COPY package.json /app/package.json -COPY pnpm-lock.yaml /app/pnpm-lock.yaml -COPY pnpm-workspace.yaml /app/pnpm-workspace.yaml -COPY web/client/package.json /app/web/client/package.json -COPY vscode/extension/package.json /app/vscode/extension/package.json -COPY vscode/bus/package.json /app/vscode/bus/package.json - -RUN npm install -g pnpm@latest && \ - pnpm install --frozen-lockfile +# Install pnpm globally +RUN npm install -g pnpm@latest -# Copy the rest of the application -COPY web/client/ /app/web/client/ +# Copy package files for workspaces +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY web/client/package.json ./web/client/ # Install dependencies RUN pnpm install --frozen-lockfile + +# Copy source files (excluding node_modules which were installed above) +COPY web/client/ ./web/client/ From d92fd3801dffeab71088617cb70ec085a35e3eed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 22:19:38 +0100 Subject: [PATCH 0371/1056] Chore(deps-dev): Bump vitest from 3.2.2 to 3.2.3 (#4698) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 174 +++++++++++++++++++++----------------- vscode/react/package.json | 2 +- web/client/package.json | 2 +- 3 files changed, 99 insertions(+), 79 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65b12a5f3c..cc774a72bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,7 +98,7 @@ importers: version: 4.1.8 '@tailwindcss/vite': specifier: ^4.1.8 - version: 4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) '@tanstack/react-query': specifier: ^5.80.6 version: 5.80.6(react@18.3.1) @@ -113,7 +113,7 @@ importers: version: 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-plugin': specifier: ^1.120.16 - version: 1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8) + version: 1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -159,7 +159,7 @@ importers: version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react': specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -168,10 +168,10 @@ importers: version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) vitest: - specifier: ^3.2.2 - version: 3.2.2(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + specifier: ^3.2.3 + version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -310,7 +310,7 @@ importers: version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react-swc': specifier: ^3.10.1 - version: 3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) ajv: specifier: ^8.17.1 version: 8.17.1 @@ -340,13 +340,13 @@ importers: version: 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) vitest: - specifier: ^3.2.2 - version: 3.2.2(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + specifier: ^3.2.3 + version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) optionalDependencies: '@swc/core-linux-x64-gnu': specifier: ^1.11.31 @@ -2247,11 +2247,11 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - '@vitest/expect@3.2.2': - resolution: {integrity: sha512-ipHw0z669vEMjzz3xQE8nJX1s0rQIb7oEl4jjl35qWTwm/KIHERIg/p/zORrjAaZKXfsv7IybcNGHwhOOAPMwQ==} + '@vitest/expect@3.2.3': + resolution: {integrity: sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==} - '@vitest/mocker@3.2.2': - resolution: {integrity: sha512-jKojcaRyIYpDEf+s7/dD3LJt53c0dPfp5zCPXz9H/kcGrSlovU/t1yEaNzM9oFME3dcd4ULwRI/x0Po1Zf+LTw==} + '@vitest/mocker@3.2.3': + resolution: {integrity: sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==} peerDependencies: msw: ^2.4.9 vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 @@ -2261,20 +2261,20 @@ packages: vite: optional: true - '@vitest/pretty-format@3.2.2': - resolution: {integrity: sha512-FY4o4U1UDhO9KMd2Wee5vumwcaHw7Vg4V7yR4Oq6uK34nhEJOmdRYrk3ClburPRUA09lXD/oXWZ8y/Sdma0aUQ==} + '@vitest/pretty-format@3.2.3': + resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==} - '@vitest/runner@3.2.2': - resolution: {integrity: sha512-GYcHcaS3ejGRZYed2GAkvsjBeXIEerDKdX3orQrBJqLRiea4NSS9qvn9Nxmuy1IwIB+EjFOaxXnX79l8HFaBwg==} + '@vitest/runner@3.2.3': + resolution: {integrity: sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==} - '@vitest/snapshot@3.2.2': - resolution: {integrity: sha512-aMEI2XFlR1aNECbBs5C5IZopfi5Lb8QJZGGpzS8ZUHML5La5wCbrbhLOVSME68qwpT05ROEEOAZPRXFpxZV2wA==} + '@vitest/snapshot@3.2.3': + resolution: {integrity: sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==} - '@vitest/spy@3.2.2': - resolution: {integrity: sha512-6Utxlx3o7pcTxvp0u8kUiXtRFScMrUg28KjB3R2hon7w4YqOFAEA9QwzPVVS1QNL3smo4xRNOpNZClRVfpMcYg==} + '@vitest/spy@3.2.3': + resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==} - '@vitest/utils@3.2.2': - resolution: {integrity: sha512-qJYMllrWpF/OYfWHP32T31QCaLa3BAzT/n/8mNGhPdVcjY+JYazQFO1nsJvXU12Kp1xMpNY4AGuljPTNjQve6A==} + '@vitest/utils@3.2.3': + resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==} '@vscode/python-extension@1.0.5': resolution: {integrity: sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==} @@ -2407,6 +2407,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.3: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} @@ -3764,6 +3769,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -5079,6 +5087,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + structured-source@4.0.0: resolution: {integrity: sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==} @@ -5177,8 +5188,8 @@ packages: uglify-js: optional: true - terser@5.41.0: - resolution: {integrity: sha512-H406eLPXpZbAX14+B8psIuvIr8+3c+2hkuYzpMkoE0ij+NdsVATbA78vb8neA/eqrj7rywa2pIkdmWRsXW6wmw==} + terser@5.42.0: + resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==} engines: {node: '>=10'} hasBin: true @@ -5497,8 +5508,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-node@3.2.2: - resolution: {integrity: sha512-Xj/jovjZvDXOq2FgLXu8NsY4uHUMWtzVmMC2LkCu9HWdr9Qu1Is5sanX3Z4jOFKdohfaWDnEJWp9pRP0vVpAcA==} + vite-node@3.2.3: + resolution: {integrity: sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true @@ -5547,16 +5558,16 @@ packages: yaml: optional: true - vitest@3.2.2: - resolution: {integrity: sha512-fyNn/Rp016Bt5qvY0OQvIUCwW2vnaEBLxP42PmKbNIoasSYjML+8xyeADOPvBe+Xfl/ubIw4og7Lt9jflRsCNw==} + vitest@3.2.3: + resolution: {integrity: sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.2 - '@vitest/ui': 3.2.2 + '@vitest/browser': 3.2.3 + '@vitest/ui': 3.2.3 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -7435,12 +7446,12 @@ snapshots: postcss: 8.5.4 tailwindcss: 4.1.8 - '@tailwindcss/vite@4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))': + '@tailwindcss/vite@4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@tailwindcss/node': 4.1.8 '@tailwindcss/oxide': 4.1.8 tailwindcss: 4.1.8 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) '@tanstack/history@1.115.0': {} @@ -7518,7 +7529,7 @@ snapshots: optionalDependencies: '@tanstack/react-router': 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-plugin@1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8)': + '@tanstack/router-plugin@1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8)': dependencies: '@babel/core': 7.27.4 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) @@ -7539,7 +7550,7 @@ snapshots: zod: 3.25.55 optionalDependencies: '@tanstack/react-router': 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) webpack: 5.99.8 transitivePeerDependencies: - supports-color @@ -7997,15 +8008,15 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react-swc@3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.9 '@swc/core': 1.11.31(@swc/helpers@0.5.17) - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react@4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) @@ -8013,48 +8024,49 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.9 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitest/expect@3.2.2': + '@vitest/expect@3.2.3': dependencies: '@types/chai': 5.2.2 - '@vitest/spy': 3.2.2 - '@vitest/utils': 3.2.2 + '@vitest/spy': 3.2.3 + '@vitest/utils': 3.2.3 chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0))': + '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: - '@vitest/spy': 3.2.2 + '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) - '@vitest/pretty-format@3.2.2': + '@vitest/pretty-format@3.2.3': dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.2.2': + '@vitest/runner@3.2.3': dependencies: - '@vitest/utils': 3.2.2 + '@vitest/utils': 3.2.3 pathe: 2.0.3 + strip-literal: 3.0.0 - '@vitest/snapshot@3.2.2': + '@vitest/snapshot@3.2.3': dependencies: - '@vitest/pretty-format': 3.2.2 + '@vitest/pretty-format': 3.2.3 magic-string: 0.30.17 pathe: 2.0.3 - '@vitest/spy@3.2.2': + '@vitest/spy@3.2.3': dependencies: tinyspy: 4.0.3 - '@vitest/utils@3.2.2': + '@vitest/utils@3.2.3': dependencies: - '@vitest/pretty-format': 3.2.2 + '@vitest/pretty-format': 3.2.3 loupe: 3.1.3 tinyrainbow: 2.0.0 @@ -8247,6 +8259,8 @@ snapshots: acorn@8.14.1: {} + acorn@8.15.0: {} + agent-base@7.1.3: {} aggregate-error@3.1.0: @@ -9713,6 +9727,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -11290,6 +11306,10 @@ snapshots: strip-json-comments@3.1.1: {} + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + structured-source@4.0.0: dependencies: boundary: 2.0.0 @@ -11432,7 +11452,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.41.0 + terser: 5.42.0 webpack: 5.99.8(esbuild@0.25.5) optionalDependencies: esbuild: 0.25.5 @@ -11443,14 +11463,14 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.41.0 + terser: 5.42.0 webpack: 5.99.8 optional: true - terser@5.41.0: + terser@5.42.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -11763,13 +11783,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.2.2(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.2.3(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 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@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -11784,11 +11804,11 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)): + vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)): dependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) - vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.5 fdir: 6.4.5(picomatch@4.0.2) @@ -11801,20 +11821,20 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.41.0 + terser: 5.42.0 tsx: 4.19.4 yaml: 2.8.0 - vitest@3.2.2(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 - '@vitest/expect': 3.2.2 - '@vitest/mocker': 3.2.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0)) - '@vitest/pretty-format': 3.2.2 - '@vitest/runner': 3.2.2 - '@vitest/snapshot': 3.2.2 - '@vitest/spy': 3.2.2 - '@vitest/utils': 3.2.2 + '@vitest/expect': 3.2.3 + '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/pretty-format': 3.2.3 + '@vitest/runner': 3.2.3 + '@vitest/snapshot': 3.2.3 + '@vitest/spy': 3.2.3 + '@vitest/utils': 3.2.3 chai: 5.2.0 debug: 4.4.1(supports-color@8.1.1) expect-type: 1.2.1 @@ -11827,8 +11847,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.0 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.2.2(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.41.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite-node: 3.2.3(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -11896,7 +11916,7 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.14.1 + acorn: 8.15.0 browserslist: 4.25.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 @@ -11928,7 +11948,7 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.14.1 + acorn: 8.15.0 browserslist: 4.25.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 diff --git a/vscode/react/package.json b/vscode/react/package.json index 93ad640277..2dabf70ca8 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -43,7 +43,7 @@ "jsdom": "^26.1.0", "typescript": "^5.8.3", "vite": "^6.3.5", - "vitest": "^3.2.2", + "vitest": "^3.2.3", "web-vitals": "^4.2.4" } } diff --git a/web/client/package.json b/web/client/package.json index 1aeff75d45..536ed81088 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -72,7 +72,7 @@ "typescript-eslint": "^8.33.1", "vite": "^6.3.5", "vite-plugin-css-injected-by-js": "^3.5.2", - "vitest": "^3.2.2" + "vitest": "^3.2.3" }, "optionalDependencies": { "@swc/core-linux-x64-gnu": "^1.11.31" From e6d0e70688706b3ca953610afecca683b766b9da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 22:32:12 +0100 Subject: [PATCH 0372/1056] Chore(deps-dev): Bump typescript-eslint from 8.33.1 to 8.34.0 (#4699) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 136 ++++++++++++++++++---------------- vscode/extension/package.json | 2 +- web/client/package.json | 2 +- 3 files changed, 73 insertions(+), 67 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc774a72bc..4e97c65e44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,8 +79,8 @@ importers: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.33.1 - version: 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.34.0 + version: 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) vscode/react: dependencies: @@ -336,8 +336,8 @@ importers: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.33.1 - version: 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.34.0 + version: 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) vite: specifier: ^6.3.5 version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) @@ -2141,63 +2141,63 @@ packages: '@types/vscode@1.96.0': resolution: {integrity: sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==} - '@typescript-eslint/eslint-plugin@8.33.1': - resolution: {integrity: sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==} + '@typescript-eslint/eslint-plugin@8.34.0': + resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.33.1 + '@typescript-eslint/parser': ^8.34.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.33.1': - resolution: {integrity: sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==} + '@typescript-eslint/parser@8.34.0': + resolution: {integrity: sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/project-service@8.33.1': - resolution: {integrity: sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==} + '@typescript-eslint/project-service@8.34.0': + resolution: {integrity: sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.33.1': - resolution: {integrity: sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==} + '@typescript-eslint/scope-manager@8.34.0': + resolution: {integrity: sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.33.1': - resolution: {integrity: sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==} + '@typescript-eslint/tsconfig-utils@8.34.0': + resolution: {integrity: sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@8.33.1': - resolution: {integrity: sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==} + '@typescript-eslint/type-utils@8.34.0': + resolution: {integrity: sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.33.1': - resolution: {integrity: sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==} + '@typescript-eslint/types@8.34.0': + resolution: {integrity: sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.33.1': - resolution: {integrity: sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==} + '@typescript-eslint/typescript-estree@8.34.0': + resolution: {integrity: sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.33.1': - resolution: {integrity: sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==} + '@typescript-eslint/utils@8.34.0': + resolution: {integrity: sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.33.1': - resolution: {integrity: sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==} + '@typescript-eslint/visitor-keys@8.34.0': + resolution: {integrity: sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typespec/ts-http-runtime@0.2.2': @@ -3138,6 +3138,10 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@9.28.0: resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5370,8 +5374,8 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x - typescript-eslint@8.33.1: - resolution: {integrity: sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A==} + typescript-eslint@8.34.0: + resolution: {integrity: sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -7874,14 +7878,14 @@ snapshots: '@types/vscode@1.96.0': {} - '@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.33.1 + '@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.34.0 + '@typescript-eslint/type-utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.34.0 eslint: 9.28.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.5 @@ -7891,40 +7895,40 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.33.1 + '@typescript-eslint/scope-manager': 8.34.0 + '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.34.0 debug: 4.4.1(supports-color@8.1.1) eslint: 9.28.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.33.1(typescript@5.8.3)': + '@typescript-eslint/project-service@8.34.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 + '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) + '@typescript-eslint/types': 8.34.0 debug: 4.4.1(supports-color@8.1.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.33.1': + '@typescript-eslint/scope-manager@8.34.0': dependencies: - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/visitor-keys': 8.33.1 + '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/visitor-keys': 8.34.0 - '@typescript-eslint/tsconfig-utils@8.33.1(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.34.0(typescript@5.8.3)': dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.1(supports-color@8.1.1) eslint: 9.28.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) @@ -7932,14 +7936,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.33.1': {} + '@typescript-eslint/types@8.34.0': {} - '@typescript-eslint/typescript-estree@8.33.1(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.34.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.33.1(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/visitor-keys': 8.33.1 + '@typescript-eslint/project-service': 8.34.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) + '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/visitor-keys': 8.34.0 debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 @@ -7950,21 +7954,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.34.0 + '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) eslint: 9.28.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.33.1': + '@typescript-eslint/visitor-keys@8.34.0': dependencies: - '@typescript-eslint/types': 8.33.1 - eslint-visitor-keys: 4.2.0 + '@typescript-eslint/types': 8.34.0 + eslint-visitor-keys: 4.2.1 '@typespec/ts-http-runtime@0.2.2': dependencies: @@ -9064,6 +9068,8 @@ snapshots: eslint-visitor-keys@4.2.0: {} + eslint-visitor-keys@4.2.1: {} + eslint@9.28.0(jiti@2.4.2): dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) @@ -11645,11 +11651,11 @@ snapshots: typescript: 5.8.3 yaml: 2.8.0 - typescript-eslint@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.28.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: diff --git a/vscode/extension/package.json b/vscode/extension/package.json index a777f2282f..8e36c459c1 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -143,6 +143,6 @@ "ts-loader": "^9.5.2", "tsx": "^4.19.4", "typescript": "^5.8.3", - "typescript-eslint": "^8.33.1" + "typescript-eslint": "^8.34.0" } } diff --git a/web/client/package.json b/web/client/package.json index 536ed81088..b4c1d175b0 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -69,7 +69,7 @@ "postcss": "^8.5.4", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", - "typescript-eslint": "^8.33.1", + "typescript-eslint": "^8.34.0", "vite": "^6.3.5", "vite-plugin-css-injected-by-js": "^3.5.2", "vitest": "^3.2.3" From 5b78b35f8953ee47e17808fb2b6c1148a68f1283 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 9 Jun 2025 21:07:52 -0700 Subject: [PATCH 0373/1056] Fix: Support overriding of the quoting policy for dbt sources (#4701) --- sqlmesh/dbt/manifest.py | 8 +++++--- tests/dbt/test_manifest.py | 2 ++ tests/fixtures/dbt/sushi_test/dbt_project.yml | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index beb0639ed2..05750b43b8 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -168,9 +168,11 @@ def _load_sources(self) -> None: ) source_config = SourceConfig( - **source_config_dict, - **source_dict, - freshness=freshness.to_dict() if freshness else None, + **{ + **source_dict, + **source_config_dict, + "freshness": freshness.to_dict() if freshness else None, + } ) self._sources_per_package[source.package_name][source_config.config_name] = ( source_config diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index f53d4b629d..bf64e4b8b3 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -115,6 +115,8 @@ def test_manifest_helper(caplog): assert sources["streaming.order_items"].table_name == "order_items" assert sources["streaming.order_items"].schema_ == "raw" + assert all(s.quoting["identifier"] is False for s in sources.values()) + assert sources["streaming.order_items"].freshness == { "warn_after": {"count": 10 if DBT_VERSION < (1, 9, 5) else 12, "period": "hour"}, "error_after": {"count": 11 if DBT_VERSION < (1, 9, 5) else 13, "period": "hour"}, diff --git a/tests/fixtures/dbt/sushi_test/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_project.yml index d833eaf0c0..1afa7dd2c6 100644 --- a/tests/fixtures/dbt/sushi_test/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/dbt_project.yml @@ -35,6 +35,10 @@ seeds: +post-hook: - '{{ log("post-hook") }}' +sources: + +quoting: + identifier: false + vars: top_waiters:limit: 10 'top_waiters:revenue': "revenue" From d96fac73cee2b30f9172835c24ec3631b24401db Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:24:45 +0300 Subject: [PATCH 0374/1056] Fix: normalize datetime values to strings in unit tests (#4696) --- sqlmesh/core/test/definition.py | 11 ++++++++++- tests/core/test_test.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index a766706801..e43b8b215c 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -258,12 +258,21 @@ def assert_equal( actual = actual.replace({np.nan: None}) expected = expected.replace({np.nan: None}) + # We define this here to avoid a top-level import of numpy and pandas + DATETIME_TYPES = ( + datetime.datetime, + datetime.date, + datetime.time, + np.datetime64, + pd.Timestamp, + ) + def _to_hashable(x: t.Any) -> t.Any: if isinstance(x, (list, np.ndarray)): return tuple(_to_hashable(v) for v in x) if isinstance(x, dict): return tuple((k, _to_hashable(v)) for k, v in x.items()) - return str(x) if not isinstance(x, t.Hashable) else x + return str(x) if isinstance(x, DATETIME_TYPES) or not isinstance(x, t.Hashable) else x actual = actual.apply(lambda col: col.map(_to_hashable)) expected = expected.apply(lambda col: col.map(_to_hashable)) diff --git a/tests/core/test_test.py b/tests/core/test_test.py index a05e66e48f..9478f4aa6b 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -2609,3 +2609,34 @@ def test_model_test_text_result_reporting_no_traceback( prefix = "ERROR" if is_error else "FAIL" assert f"{prefix}: test_foo (None)" in output assert "Exception: failure" in output + + +def test_timestamp_normalization() -> None: + model = _create_model( + "SELECT id, array_agg(timestamp_col::timestamp) as agg_timestamp_col FROM temp_model_with_timestamp GROUP BY id", + meta="MODEL (name foo, kind FULL)", + ) + + _check_successful_or_raise( + _create_test( + body=load_yaml( + """ + test_foo: + model: temp_agg_model_with_timestamp + inputs: + temp_model_with_timestamp: + rows: + - id: "id1" + timestamp_col: "2024-01-02T15:00:00" + outputs: + query: + rows: + - id: id1 + agg_timestamp_col: ["2024-01-02T15:00:00.000000"] + """ + ), + test_name="test_foo", + model=model, + context=Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))), + ).run() + ) From 3d50d79f7caf7b8e677e8812cd01c512e6e8c795 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:09:50 +0300 Subject: [PATCH 0375/1056] Fix: skip project loading for clean, destroy and janitor commands (#4703) --- sqlmesh/cli/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index dbbd8150a0..51eb0c3432 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -23,12 +23,15 @@ logger = logging.getLogger(__name__) SKIP_LOAD_COMMANDS = ( + "clean", "create_external_models", + "destroy", + "environments", + "invalidate", + "janitor", "migrate", "rollback", "run", - "environments", - "invalidate", "table_name", ) SKIP_CONTEXT_COMMANDS = ("init", "ui") From d655523c1c76cbadad1e152459897be805eec157 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:34:15 +0100 Subject: [PATCH 0376/1056] refactor(lsp): creation of context (#4704) --- sqlmesh/lsp/main.py | 77 ++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 2295a4b95f..897fcd9dfd 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -85,6 +85,22 @@ def __init__( # Register LSP features (e.g., formatting, hover, etc.) self._register_features() + def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: + """Create a new LSPContext instance using the configured context class. + + Args: + paths: List of paths to pass to the context constructor + + Returns: + A new LSPContext instance wrapping the created context, or None if creation fails + """ + try: + context = self.context_class(paths=paths) + return LSPContext(context) + except Exception as e: + self.server.log_trace(f"Error creating context: {e}") + return None + def _register_features(self) -> None: """Register LSP features on the internal LanguageServer instance.""" @@ -116,16 +132,11 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: for ext in ("py", "yml", "yaml"): config_path = folder_path / f"config.{ext}" if config_path.exists(): - try: - # Use user-provided instantiator to build the context - created_context = self.context_class(paths=[folder_path]) - self.lsp_context = LSPContext(created_context) + lsp_context = self._create_lsp_context([folder_path]) + if lsp_context: + self.lsp_context = lsp_context loaded_sqlmesh_message(ls, folder_path) return # Exit after successfully loading any config - except Exception as e: - ls.log_trace( - f"Error loading context from {config_path}: {e}", - ) except Exception as e: ls.log_trace( f"Error initializing SQLMesh context: {e}", @@ -288,13 +299,10 @@ def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> Non # Reload the entire context and create a new LSPContext if self.lsp_context is not None: - try: - new_context = Context(paths=list(self.lsp_context.context.configs)) - new_full_context = LSPContext(new_context) - self.lsp_context = new_full_context + new_lsp_context = self._create_lsp_context(list(self.lsp_context.context.configs)) + if new_lsp_context: + self.lsp_context = new_lsp_context return - except Exception as e: - pass context = self._context_get_or_load(uri) models = context.map[uri.to_path()] @@ -664,29 +672,22 @@ def _ensure_context_in_folder(self, folder_uri: Path) -> None: for ext in ("py", "yml", "yaml"): config_path = folder_uri / f"config.{ext}" if config_path.exists(): - try: - created_context = self.context_class(paths=[folder_uri]) - self.lsp_context = LSPContext(created_context) + lsp_context = self._create_lsp_context([folder_uri]) + if lsp_context: + self.lsp_context = lsp_context loaded_sqlmesh_message(self.server, folder_uri) return - except Exception as e: - self.server.show_message(f"Error loading context: {e}", types.MessageType.Error) # If not found in the provided folder, search through all workspace folders for workspace_folder in self.workspace_folders: for ext in ("py", "yml", "yaml"): config_path = workspace_folder / f"config.{ext}" if config_path.exists(): - try: - created_context = self.context_class(paths=[workspace_folder]) - self.lsp_context = LSPContext(created_context) + lsp_context = self._create_lsp_context([workspace_folder]) + if lsp_context: + self.lsp_context = lsp_context loaded_sqlmesh_message(self.server, workspace_folder) return - except Exception as e: - self.server.show_message( - f"Error loading context from {config_path}: {e}", - types.MessageType.Warning, - ) def _ensure_context_for_document( self, @@ -713,17 +714,12 @@ def _ensure_context_for_document( for ext in ("py", "yml", "yaml"): config_path = path / f"config.{ext}" if config_path.exists(): - try: - # Use user-provided instantiator to build the context - created_context = self.context_class(paths=[path]) - self.lsp_context = LSPContext(created_context) + lsp_context = self._create_lsp_context([path]) + if lsp_context: + self.lsp_context = lsp_context loaded = True # Re-check context for the document now that it's loaded return self._ensure_context_for_document(document_uri) - except Exception as e: - self.server.show_message( - f"Error loading context: {e}", types.MessageType.Error - ) path = path.parent # If still no context found, try the workspace folders @@ -732,16 +728,11 @@ def _ensure_context_for_document( for ext in ("py", "yml", "yaml"): config_path = workspace_folder / f"config.{ext}" if config_path.exists(): - try: - created_context = self.context_class(paths=[workspace_folder]) - self.lsp_context = LSPContext(created_context) + lsp_context = self._create_lsp_context([workspace_folder]) + if lsp_context: + self.lsp_context = lsp_context loaded_sqlmesh_message(self.server, workspace_folder) return - except Exception as e: - self.server.show_message( - f"Error loading context from {config_path}: {e}", - types.MessageType.Warning, - ) return From 9e5bf542f3d197c32ab526cbaa5d963088d78c8f Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:47:51 +0100 Subject: [PATCH 0377/1056] refactor(lsp): further refactor of setting (#4705) --- sqlmesh/lsp/main.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 897fcd9dfd..fdfb105a63 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -88,6 +88,8 @@ def __init__( def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: """Create a new LSPContext instance using the configured context class. + On success, sets self.lsp_context and returns the created context. + Args: paths: List of paths to pass to the context constructor @@ -96,7 +98,9 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: """ try: context = self.context_class(paths=paths) - return LSPContext(context) + lsp_context = LSPContext(context) + self.lsp_context = lsp_context + return lsp_context except Exception as e: self.server.log_trace(f"Error creating context: {e}") return None @@ -132,9 +136,7 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: for ext in ("py", "yml", "yaml"): config_path = folder_path / f"config.{ext}" if config_path.exists(): - lsp_context = self._create_lsp_context([folder_path]) - if lsp_context: - self.lsp_context = lsp_context + if self._create_lsp_context([folder_path]): loaded_sqlmesh_message(ls, folder_path) return # Exit after successfully loading any config except Exception as e: @@ -299,9 +301,7 @@ def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> Non # Reload the entire context and create a new LSPContext if self.lsp_context is not None: - new_lsp_context = self._create_lsp_context(list(self.lsp_context.context.configs)) - if new_lsp_context: - self.lsp_context = new_lsp_context + if self._create_lsp_context(list(self.lsp_context.context.configs)): return context = self._context_get_or_load(uri) @@ -672,9 +672,7 @@ def _ensure_context_in_folder(self, folder_uri: Path) -> None: for ext in ("py", "yml", "yaml"): config_path = folder_uri / f"config.{ext}" if config_path.exists(): - lsp_context = self._create_lsp_context([folder_uri]) - if lsp_context: - self.lsp_context = lsp_context + if self._create_lsp_context([folder_uri]): loaded_sqlmesh_message(self.server, folder_uri) return @@ -683,9 +681,7 @@ def _ensure_context_in_folder(self, folder_uri: Path) -> None: for ext in ("py", "yml", "yaml"): config_path = workspace_folder / f"config.{ext}" if config_path.exists(): - lsp_context = self._create_lsp_context([workspace_folder]) - if lsp_context: - self.lsp_context = lsp_context + if self._create_lsp_context([workspace_folder]): loaded_sqlmesh_message(self.server, workspace_folder) return @@ -714,9 +710,7 @@ def _ensure_context_for_document( for ext in ("py", "yml", "yaml"): config_path = path / f"config.{ext}" if config_path.exists(): - lsp_context = self._create_lsp_context([path]) - if lsp_context: - self.lsp_context = lsp_context + if self._create_lsp_context([path]): loaded = True # Re-check context for the document now that it's loaded return self._ensure_context_for_document(document_uri) @@ -728,9 +722,7 @@ def _ensure_context_for_document( for ext in ("py", "yml", "yaml"): config_path = workspace_folder / f"config.{ext}" if config_path.exists(): - lsp_context = self._create_lsp_context([workspace_folder]) - if lsp_context: - self.lsp_context = lsp_context + if self._create_lsp_context([workspace_folder]): loaded_sqlmesh_message(self.server, workspace_folder) return From c9a26ecbbd374f4322e2a1a5885ee8433ae68f01 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:25:48 -0500 Subject: [PATCH 0378/1056] Feat: add typo suggestions to MODEL block field errors (#4661) --- sqlmesh/core/model/common.py | 25 ++++++- sqlmesh/core/model/definition.py | 7 +- sqlmesh/core/model/kind.py | 4 +- tests/core/test_model.py | 108 +++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index 694e6d572a..8f2d9e8ff8 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -5,6 +5,7 @@ from pathlib import Path from astor import to_source +from difflib import get_close_matches from sqlglot import exp from sqlglot.helper import ensure_list @@ -267,13 +268,33 @@ def validate_extra_and_required_fields( ) -> None: missing_required_fields = klass.missing_required_fields(provided_fields) if missing_required_fields: + field_names = "'" + "', '".join(missing_required_fields) + "'" raise_config_error( - f"Missing required fields {missing_required_fields} in the {entity_name}" + f"Please add required field{'s' if len(missing_required_fields) > 1 else ''} {field_names} to the {entity_name}." ) extra_fields = klass.extra_fields(provided_fields) if extra_fields: - raise_config_error(f"Invalid extra fields {extra_fields} in the {entity_name}") + extra_field_names = "'" + "', '".join(extra_fields) + "'" + + all_fields = klass.all_fields() + close_matches = {} + for field in extra_fields: + matches = get_close_matches(field, all_fields, n=1) + if matches: + close_matches[field] = matches[0] + + if len(close_matches) == 1: + similar_msg = ". Did you mean " + "'" + "', '".join(close_matches.values()) + "'?" + else: + similar = [ + f"- {field}: Did you mean '{match}'?" for field, match in close_matches.items() + ] + similar_msg = "\n\n " + "\n ".join(similar) if similar else "" + + raise_config_error( + f"Invalid field name{'s' if len(extra_fields) > 1 else ''} present in the {entity_name}: {extra_field_names}{similar_msg}" + ) def single_value_or_tuple(values: t.Sequence) -> exp.Identifier | exp.Tuple: diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 23346a15a2..db61b09c8e 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2156,7 +2156,10 @@ def load_sql_based_model( name = get_model_name(path) if not name: - raise_config_error("Model must have a name", path) + raise_config_error( + "Please add the required 'name' field to the MODEL block at the top of the file.\n\n" + + "Learn more at https://sqlmesh.readthedocs.io/en/stable/concepts/models/overview" + ) if "default_catalog" in meta_fields: raise_config_error( "`default_catalog` cannot be set on a per-model basis. It must be set at the connection level.", @@ -2400,7 +2403,7 @@ def _create_model( **kwargs: t.Any, ) -> Model: validate_extra_and_required_fields( - klass, {"name", *kwargs} - {"grain", "table_properties"}, "model definition" + klass, {"name", *kwargs} - {"grain", "table_properties"}, "MODEL block" ) for prop in PROPERTIES: diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index c858ac44fa..f58127dcdf 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -1015,7 +1015,9 @@ def create_model_kind(v: t.Any, dialect: str, defaults: t.Dict[str, t.Any]) -> M actual_kind_type, _ = custom_materialization return actual_kind_type(**props) - validate_extra_and_required_fields(kind_type, set(props), f"model kind '{name}'") + validate_extra_and_required_fields( + kind_type, set(props), f"MODEL block 'kind {name}' field" + ) return kind_type(**props) name = (v.name if isinstance(v, exp.Expression) else str(v)).upper() diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 932775f831..7a2f808e12 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -571,6 +571,114 @@ def test_opt_out_of_time_column_in_partitioned_by(): assert model.partitioned_by == [exp.to_column('"b"')] +def test_model_no_name(): + expressions = d.parse( + """ + MODEL ( + dialect bigquery, + ); + + SELECT 1::int AS a, 2::int AS b; + """ + ) + + with pytest.raises(ConfigError) as ex: + load_sql_based_model(expressions) + assert ( + str(ex.value) + == "Please add the required 'name' field to the MODEL block at the top of the file.\n\nLearn more at https://sqlmesh.readthedocs.io/en/stable/concepts/models/overview" + ) + + +def test_model_field_name_suggestions(): + # top-level field + expressions = d.parse( + """ + MODEL ( + name db.table, + dialects bigquery, + ); + + SELECT 1::int AS a, 2::int AS b; + """ + ) + + with pytest.raises(ConfigError) as ex: + load_sql_based_model(expressions) + assert ( + str(ex.value) + == "Invalid field name present in the MODEL block: 'dialects'. Did you mean 'dialect'?" + ) + + # kind field + expressions = d.parse( + """ + MODEL ( + name db.table, + kind INCREMENTAL_BY_TIME_RANGE( + time_column a, + batch_sizes 1 + ), + ); + + SELECT 1::int AS a, 2::int AS b; + """ + ) + + with pytest.raises(ConfigError) as ex: + load_sql_based_model(expressions) + assert ( + str(ex.value) + == "Invalid field name present in the MODEL block 'kind INCREMENTAL_BY_TIME_RANGE' field: 'batch_sizes'. Did you mean 'batch_size'?" + ) + + # multiple fields + expressions = d.parse( + """ + MODEL ( + name db.table, + dialects bigquery, + descriptions 'a', + asdfasdf true + ); + + SELECT 1::int AS a, 2::int AS b; + """ + ) + + with pytest.raises(ConfigError) as ex: + load_sql_based_model(expressions) + ex_str = str(ex.value) + # field order is non-deterministic, so we can't test the output string directly + assert "Invalid field names present in the MODEL block: " in ex_str + assert "'descriptions'" in ex_str + assert "'dialects'" in ex_str + assert "'asdfasdf'" in ex_str + assert "- descriptions: Did you mean 'description'?" in ex_str + assert "- dialects: Did you mean 'dialect'?" in ex_str + assert "- asdfasdf: Did you mean " not in ex_str + + +def test_model_required_field_missing(): + expressions = d.parse( + """ + MODEL ( + name db.table, + kind INCREMENTAL_BY_TIME_RANGE (), + ); + + SELECT 1::int AS a, 2::int AS b; + """ + ) + + with pytest.raises(ConfigError) as ex: + load_sql_based_model(expressions) + assert ( + str(ex.value) + == "Please add required field 'time_column' to the MODEL block 'kind INCREMENTAL_BY_TIME_RANGE' field." + ) + + def test_no_model_statement(tmp_path: Path): # No name inference => MODEL (...) is required expressions = d.parse("SELECT 1 AS x") From 97407d7ccbab166904168a6f4159c16b948ba752 Mon Sep 17 00:00:00 2001 From: Caitlin Bailey Date: Tue, 10 Jun 2025 09:18:29 -0700 Subject: [PATCH 0379/1056] chore(docs): add tobiko logo to tobiko cloud nav tab (#4700) --- docs/stylesheets/extra.css | 10 ++++++++++ docs/stylesheets/tobiko-logo.svg | 11 +++++++++++ mkdocs.yml | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 docs/stylesheets/tobiko-logo.svg diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 4bc469ff12..44daab1f86 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -40,4 +40,14 @@ :root { --md-primary-fg-color: #3C64E2; +} + +.md-tabs__item:last-child a::before { + content: ""; + background: transparent url("tobiko-logo.svg") center left no-repeat; + background-size: contain; + width: 18px; + height: 18px; + display: inline-block; + margin-bottom: -4px; } \ No newline at end of file diff --git a/docs/stylesheets/tobiko-logo.svg b/docs/stylesheets/tobiko-logo.svg new file mode 100644 index 0000000000..3dbc9a6194 --- /dev/null +++ b/docs/stylesheets/tobiko-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/mkdocs.yml b/mkdocs.yml index 092c8d72e2..48a5911f9e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -107,7 +107,7 @@ nav: - Configuration: - reference/configuration.md - reference/model_configuration.md - - Tobiko Cloud: + - Tobiko Cloud: # NOTE: if this item is no longer last, need to update extra.css to adjust logo positioning - "Overview": cloud/cloud_index.md - "Getting Started": cloud/tcloud_getting_started.md - Cloud Features: From 2c1ed263997abb79e9deffc355d4b8980cca97dc Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:43:42 +0100 Subject: [PATCH 0380/1056] feat(lsp): custom methods always return errors (#4708) --- sqlmesh/lsp/api.py | 17 ++- sqlmesh/lsp/custom.py | 28 ++-- sqlmesh/lsp/main.py | 261 +++++++++++++++++---------------- tooling/vscode/extensions.json | 3 +- 4 files changed, 170 insertions(+), 139 deletions(-) diff --git a/sqlmesh/lsp/api.py b/sqlmesh/lsp/api.py index 4be256c194..a034283759 100644 --- a/sqlmesh/lsp/api.py +++ b/sqlmesh/lsp/api.py @@ -9,13 +9,16 @@ import typing as t from pydantic import field_validator -from sqlmesh.utils.pydantic import PydanticModel +from sqlmesh.lsp.custom import ( + CustomMethodRequestBaseClass, + CustomMethodResponseBaseClass, +) from web.server.models import LineageColumn, Model API_FEATURE = "sqlmesh/api" -class ApiRequest(PydanticModel): +class ApiRequest(CustomMethodRequestBaseClass): """ Request to call the SQLMesh API. This is a generic request that can be used to call any API endpoint. @@ -28,7 +31,11 @@ class ApiRequest(PydanticModel): body: t.Optional[t.Dict[str, t.Any]] = None -class ApiResponseGetModels(PydanticModel): +class BaseAPIResponse(CustomMethodResponseBaseClass): + error: t.Optional[str] = None + + +class ApiResponseGetModels(BaseAPIResponse): """ Response from the SQLMesh API for the get_models endpoint. """ @@ -53,7 +60,7 @@ def sanitize_datetime_fields(cls, data: t.List[Model]) -> t.List[Model]: return data -class ApiResponseGetLineage(PydanticModel): +class ApiResponseGetLineage(BaseAPIResponse): """ Response from the SQLMesh API for the get_lineage endpoint. """ @@ -61,7 +68,7 @@ class ApiResponseGetLineage(PydanticModel): data: t.Dict[str, t.List[str]] -class ApiResponseGetColumnLineage(PydanticModel): +class ApiResponseGetColumnLineage(BaseAPIResponse): """ Response from the SQLMesh API for the get_column_lineage endpoint. """ diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py index 72c1ec7917..c432802950 100644 --- a/sqlmesh/lsp/custom.py +++ b/sqlmesh/lsp/custom.py @@ -2,10 +2,20 @@ import typing as t from sqlmesh.utils.pydantic import PydanticModel + +class CustomMethodRequestBaseClass(PydanticModel): + pass + + +class CustomMethodResponseBaseClass(PydanticModel): + # Prefixing, so guaranteed not to collide + response_error: t.Optional[str] = None + + ALL_MODELS_FEATURE = "sqlmesh/all_models" -class AllModelsRequest(PydanticModel): +class AllModelsRequest(CustomMethodRequestBaseClass): """ Request to get all the models that are in the current project. """ @@ -13,7 +23,7 @@ class AllModelsRequest(PydanticModel): textDocument: types.TextDocumentIdentifier -class AllModelsResponse(PydanticModel): +class AllModelsResponse(CustomMethodResponseBaseClass): """ Response to get all the models that are in the current project. """ @@ -26,7 +36,7 @@ class AllModelsResponse(PydanticModel): RENDER_MODEL_FEATURE = "sqlmesh/render_model" -class RenderModelRequest(PydanticModel): +class RenderModelRequest(CustomMethodRequestBaseClass): textDocumentUri: str @@ -41,7 +51,7 @@ class RenderModelEntry(PydanticModel): rendered_query: str -class RenderModelResponse(PydanticModel): +class RenderModelResponse(CustomMethodResponseBaseClass): """ Response to render a model. """ @@ -63,11 +73,11 @@ class ModelForRendering(PydanticModel): uri: str -class AllModelsForRenderRequest(PydanticModel): +class AllModelsForRenderRequest(CustomMethodRequestBaseClass): pass -class AllModelsForRenderResponse(PydanticModel): +class AllModelsForRenderResponse(CustomMethodResponseBaseClass): """ Response to get all the models that are in the current project for rendering purposes. """ @@ -94,7 +104,7 @@ class CustomMethod(PydanticModel): name: str -class SupportedMethodsResponse(PydanticModel): +class SupportedMethodsResponse(CustomMethodResponseBaseClass): """ Response containing all supported custom LSP methods. """ @@ -105,7 +115,7 @@ class SupportedMethodsResponse(PydanticModel): FORMAT_PROJECT_FEATURE = "sqlmesh/format_project" -class FormatProjectRequest(PydanticModel): +class FormatProjectRequest(CustomMethodRequestBaseClass): """ Request to format all models in the current project. """ @@ -113,7 +123,7 @@ class FormatProjectRequest(PydanticModel): pass -class FormatProjectResponse(PydanticModel): +class FormatProjectResponse(CustomMethodResponseBaseClass): """ Response to format project request. """ diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index fdfb105a63..3247bd5b50 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -35,6 +35,7 @@ AllModelsResponse, AllModelsForRenderRequest, AllModelsForRenderResponse, + CustomMethodResponseBaseClass, RenderModelRequest, RenderModelResponse, SupportedMethodsRequest, @@ -54,15 +55,6 @@ from web.server.api.endpoints.lineage import column_lineage, model_lineage from web.server.api.endpoints.models import get_models -SUPPORTED_CUSTOM_METHODS = [ - ALL_MODELS_FEATURE, - RENDER_MODEL_FEATURE, - ALL_MODELS_FOR_RENDER_FEATURE, - API_FEATURE, - SUPPORTED_METHODS_FEATURE, - FORMAT_PROJECT_FEATURE, -] - class SQLMeshLanguageServer: def __init__( @@ -82,6 +74,22 @@ def __init__( self.workspace_folders: t.List[Path] = [] self.client_supports_pull_diagnostics = False + self._supported_custom_methods: t.Dict[ + str, + t.Callable[ + # mypy unable to recognise the base class + [LanguageServer, t.Any], + t.Any, + ], + ] = { + ALL_MODELS_FEATURE: self._custom_all_models, + RENDER_MODEL_FEATURE: self._custom_render_model, + ALL_MODELS_FOR_RENDER_FEATURE: self._custom_all_models_for_render, + API_FEATURE: self._custom_api, + SUPPORTED_METHODS_FEATURE: self._custom_supported_methods, + FORMAT_PROJECT_FEATURE: self._custom_format_project, + } + # Register LSP features (e.g., formatting, hover, etc.) self._register_features() @@ -105,8 +113,128 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: self.server.log_trace(f"Error creating context: {e}") return None + # All the custom LSP methods are registered here and prefixed with _custom + def _custom_all_models(self, ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse: + uri = URI(params.textDocument.uri) + # Get the document content + content = None + try: + document = ls.workspace.get_text_document(params.textDocument.uri) + content = document.source + except Exception: + pass + try: + context = self._context_get_or_load(uri) + return LSPContext.get_completions(context, uri, content) + except Exception as e: + from sqlmesh.lsp.completions import get_sql_completions + + return get_sql_completions(None, URI(params.textDocument.uri), content) + + def _custom_render_model( + self, ls: LanguageServer, params: RenderModelRequest + ) -> RenderModelResponse: + uri = URI(params.textDocumentUri) + context = self._context_get_or_load(uri) + return RenderModelResponse(models=context.render_model(uri)) + + def _custom_all_models_for_render( + self, ls: LanguageServer, params: AllModelsForRenderRequest + ) -> AllModelsForRenderResponse: + if self.lsp_context is None: + current_path = Path.cwd() + self._ensure_context_in_folder(current_path) + if self.lsp_context is None: + raise RuntimeError("No context found") + return AllModelsForRenderResponse(models=self.lsp_context.list_of_models_for_rendering()) + + def _custom_format_project( + self, ls: LanguageServer, params: FormatProjectRequest + ) -> FormatProjectResponse: + """Format all models in the current project.""" + try: + if self.lsp_context is None: + current_path = Path.cwd() + self._ensure_context_in_folder(current_path) + if self.lsp_context is None: + raise RuntimeError("No context found") + + # Call the format method on the context + self.lsp_context.context.format() + return FormatProjectResponse() + except Exception as e: + ls.log_trace(f"Error formatting project: {e}") + return FormatProjectResponse() + + def _custom_api( + self, ls: LanguageServer, request: ApiRequest + ) -> t.Union[ApiResponseGetModels, ApiResponseGetColumnLineage, ApiResponseGetLineage]: + ls.log_trace(f"API request: {request}") + if self.lsp_context is None: + current_path = Path.cwd() + self._ensure_context_in_folder(current_path) + if self.lsp_context is None: + ls.log_trace("No context found in call") + raise RuntimeError("No context found") + + parsed_url = urllib.parse.urlparse(request.url) + path_parts = parsed_url.path.strip("/").split("/") + + if request.method == "GET": + if path_parts == ["api", "models"]: + # /api/models + return ApiResponseGetModels(data=get_models(self.lsp_context.context)) + + if path_parts[:2] == ["api", "lineage"]: + if len(path_parts) == 3: + # /api/lineage/{model} + model_name = urllib.parse.unquote(path_parts[2]) + lineage = model_lineage(model_name, self.lsp_context.context) + non_set_lineage = {k: v for k, v in lineage.items() if v is not None} + return ApiResponseGetLineage(data=non_set_lineage) + + if len(path_parts) == 4: + # /api/lineage/{model}/{column} + model_name = urllib.parse.unquote(path_parts[2]) + column = urllib.parse.unquote(path_parts[3]) + models_only = False + if hasattr(request, "params"): + models_only = bool(getattr(request.params, "models_only", False)) + column_lineage_response = column_lineage( + model_name, column, models_only, self.lsp_context.context + ) + return ApiResponseGetColumnLineage(data=column_lineage_response) + + raise NotImplementedError(f"API request not implemented: {request.url}") + + def _custom_supported_methods( + self, ls: LanguageServer, params: SupportedMethodsRequest + ) -> SupportedMethodsResponse: + """Return all supported custom LSP methods.""" + return SupportedMethodsResponse( + methods=[ + CustomMethod( + name=name, + ) + for name in self._supported_custom_methods + ] + ) + def _register_features(self) -> None: """Register LSP features on the internal LanguageServer instance.""" + for name, method in self._supported_custom_methods.items(): + + def create_function_call(method_func: t.Callable) -> t.Callable: + def function_call(ls: LanguageServer, params: t.Any) -> t.Dict[str, t.Any]: + try: + response = method_func(ls, params) + except Exception as e: + response = CustomMethodResponseBaseClass(response_error=str(e)) + return response.model_dump(mode="json") + + return function_call + + self.server.feature(name)(create_function_call(method)) @self.server.feature(types.INITIALIZE) def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: @@ -144,121 +272,6 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: f"Error initializing SQLMesh context: {e}", ) - @self.server.feature(ALL_MODELS_FEATURE) - def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse: - uri = URI(params.textDocument.uri) - - # Get the document content - content = None - try: - document = ls.workspace.get_text_document(params.textDocument.uri) - content = document.source - except Exception: - pass - - try: - context = self._context_get_or_load(uri) - return LSPContext.get_completions(context, uri, content) - except Exception as e: - from sqlmesh.lsp.completions import get_sql_completions - - return get_sql_completions(None, URI(params.textDocument.uri), content) - - @self.server.feature(RENDER_MODEL_FEATURE) - def render_model(ls: LanguageServer, params: RenderModelRequest) -> RenderModelResponse: - uri = URI(params.textDocumentUri) - context = self._context_get_or_load(uri) - return RenderModelResponse(models=context.render_model(uri)) - - @self.server.feature(ALL_MODELS_FOR_RENDER_FEATURE) - def all_models_for_render( - ls: LanguageServer, params: AllModelsForRenderRequest - ) -> AllModelsForRenderResponse: - if self.lsp_context is None: - current_path = Path.cwd() - self._ensure_context_in_folder(current_path) - if self.lsp_context is None: - raise RuntimeError("No context found") - return AllModelsForRenderResponse( - models=self.lsp_context.list_of_models_for_rendering() - ) - - @self.server.feature(SUPPORTED_METHODS_FEATURE) - def supported_methods( - ls: LanguageServer, params: SupportedMethodsRequest - ) -> SupportedMethodsResponse: - """Return all supported custom LSP methods.""" - return SupportedMethodsResponse( - methods=[ - CustomMethod( - name=name, - ) - for name in SUPPORTED_CUSTOM_METHODS - ] - ) - - @self.server.feature(FORMAT_PROJECT_FEATURE) - def format_project( - ls: LanguageServer, params: FormatProjectRequest - ) -> FormatProjectResponse: - """Format all models in the current project.""" - try: - if self.lsp_context is None: - current_path = Path.cwd() - self._ensure_context_in_folder(current_path) - if self.lsp_context is None: - raise RuntimeError("No context found") - - # Call the format method on the context - self.lsp_context.context.format() - return FormatProjectResponse() - except Exception as e: - ls.log_trace(f"Error formatting project: {e}") - return FormatProjectResponse() - - @self.server.feature(API_FEATURE) - def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]: - ls.log_trace(f"API request: {request}") - if self.lsp_context is None: - current_path = Path.cwd() - self._ensure_context_in_folder(current_path) - if self.lsp_context is None: - raise RuntimeError("No context found") - - parsed_url = urllib.parse.urlparse(request.url) - path_parts = parsed_url.path.strip("/").split("/") - - if request.method == "GET": - if path_parts == ["api", "models"]: - # /api/models - return ApiResponseGetModels( - data=get_models(self.lsp_context.context) - ).model_dump(mode="json") - - if path_parts[:2] == ["api", "lineage"]: - if len(path_parts) == 3: - # /api/lineage/{model} - model_name = urllib.parse.unquote(path_parts[2]) - lineage = model_lineage(model_name, self.lsp_context.context) - non_set_lineage = {k: v for k, v in lineage.items() if v is not None} - return ApiResponseGetLineage(data=non_set_lineage).model_dump(mode="json") - - if len(path_parts) == 4: - # /api/lineage/{model}/{column} - model_name = urllib.parse.unquote(path_parts[2]) - column = urllib.parse.unquote(path_parts[3]) - models_only = False - if hasattr(request, "params"): - models_only = bool(getattr(request.params, "models_only", False)) - column_lineage_response = column_lineage( - model_name, column, models_only, self.lsp_context.context - ) - return ApiResponseGetColumnLineage(data=column_lineage_response).model_dump( - mode="json" - ) - - raise NotImplementedError(f"API request not implemented: {request.url}") - @self.server.feature(types.TEXT_DOCUMENT_DID_OPEN) def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> None: uri = URI(params.text_document.uri) diff --git a/tooling/vscode/extensions.json b/tooling/vscode/extensions.json index 9e5e0b733f..0271570408 100644 --- a/tooling/vscode/extensions.json +++ b/tooling/vscode/extensions.json @@ -4,6 +4,7 @@ "recommendations": [ "dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher", - "ms-vscode.extension-test-runner" + "ms-vscode.extension-test-runner", + "ms-playwright.playwright" ] } From 7e252f8e798833537f72d56ac7feb752e2999cb4 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:27:43 +0100 Subject: [PATCH 0381/1056] feat: macro autocomplete has descriptions (#4712) --- examples/sushi/macros/utils.py | 4 ++-- sqlmesh/lsp/completions.py | 20 ++++++++++++++------ sqlmesh/lsp/custom.py | 9 ++++++++- sqlmesh/lsp/main.py | 4 +++- tests/lsp/test_completions.py | 8 ++++++-- 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/examples/sushi/macros/utils.py b/examples/sushi/macros/utils.py index a76bc3bfe0..fb2ccc21b0 100644 --- a/examples/sushi/macros/utils.py +++ b/examples/sushi/macros/utils.py @@ -5,14 +5,14 @@ @macro() def add_one(evaluator, column: int): - # typed column will be cast to an int and return an integer back + """typed column will be cast to an int and return an integer back""" assert isinstance(column, int) return column + 1 @macro() def multiply(evaluator, column, num): - # untyped column will be a sqlglot column and return a sqlglot exp "column > 0" + """untyped column will be a sqlglot column and return a sqlglot exp "column > 0""" assert isinstance(column, exp.Column) return column * num diff --git a/sqlmesh/lsp/completions.py b/sqlmesh/lsp/completions.py index 1b1483c271..7e3781a550 100644 --- a/sqlmesh/lsp/completions.py +++ b/sqlmesh/lsp/completions.py @@ -1,6 +1,6 @@ from functools import lru_cache from sqlglot import Dialect, Tokenizer -from sqlmesh.lsp.custom import AllModelsResponse +from sqlmesh.lsp.custom import AllModelsResponse, MacroCompletion from sqlmesh import macro import typing as t from sqlmesh.lsp.context import AuditTarget, LSPContext, ModelTarget @@ -60,15 +60,23 @@ def get_models(context: t.Optional[LSPContext], file_uri: t.Optional[URI]) -> t. return all_models -def get_macros(context: t.Optional[LSPContext], file_uri: t.Optional[URI]) -> t.Set[str]: - """Return a set of all macros with the ``@`` prefix.""" - names = set(macro.get_registry()) +def get_macros( + context: t.Optional[LSPContext], file_uri: t.Optional[URI] +) -> t.List[MacroCompletion]: + """Return a list of macros with optional descriptions.""" + macros: t.Dict[str, t.Optional[str]] = {} + + for name, m in macro.get_registry().items(): + macros[name] = getattr(m.func, "__doc__", None) + try: if context is not None: - names.update(context.context._macros) + for name, m in context.context._macros.items(): + macros[name] = getattr(m.func, "__doc__", None) except Exception: pass - return names + + return [MacroCompletion(name=name, description=doc) for name, doc in macros.items()] def get_keywords(context: t.Optional[LSPContext], file_uri: t.Optional[URI]) -> t.Set[str]: diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py index c432802950..5c8123b7a0 100644 --- a/sqlmesh/lsp/custom.py +++ b/sqlmesh/lsp/custom.py @@ -23,6 +23,13 @@ class AllModelsRequest(CustomMethodRequestBaseClass): textDocument: types.TextDocumentIdentifier +class MacroCompletion(PydanticModel): + """Information about a macro for autocompletion.""" + + name: str + description: t.Optional[str] = None + + class AllModelsResponse(CustomMethodResponseBaseClass): """ Response to get all the models that are in the current project. @@ -30,7 +37,7 @@ class AllModelsResponse(CustomMethodResponseBaseClass): models: t.List[str] keywords: t.List[str] - macros: t.List[str] + macros: t.List[MacroCompletion] RENDER_MODEL_FEATURE = "sqlmesh/render_model" diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 3247bd5b50..137ca7c07c 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -625,7 +625,8 @@ def completion( and getattr(params.context, "trigger_character", None) == "@" ) - for macro_name in completion_response.macros: + for macro in completion_response.macros: + macro_name = macro.name insert_text = macro_name if triggered_by_at else f"@{macro_name}" completion_items.append( @@ -636,6 +637,7 @@ def completion( filter_text=macro_name, kind=types.CompletionItemKind.Function, detail="SQLMesh Macro", + documentation=macro.description, ) ) diff --git a/tests/lsp/test_completions.py b/tests/lsp/test_completions.py index 8977d178ba..7e193d77d6 100644 --- a/tests/lsp/test_completions.py +++ b/tests/lsp/test_completions.py @@ -33,8 +33,12 @@ def test_get_macros(): file_uri = URI.from_path(file_path) completions = LSPContext.get_completions(lsp_context, file_uri, file_content) - assert "each" in completions.macros - assert "add_one" in completions.macros + each_macro = next((m for m in completions.macros if m.name == "each")) + assert each_macro.name == "each" + assert each_macro.description + add_one_macro = next((m for m in completions.macros if m.name == "add_one")) + assert add_one_macro.name == "add_one" + assert add_one_macro.description def test_get_sql_completions_with_context_no_file_uri(): From 28155b3a8aecd070f6cb179438a089241cb85cb0 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:31:56 +0300 Subject: [PATCH 0382/1056] Chore!: bump sqlglot to v26.26.0 (#4695) --- pyproject.toml | 2 +- tests/core/test_dialect.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6054adc0d2..b172c86375 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.25.3", + "sqlglot[rs]~=26.26.0", "tenacity", "time-machine", "json-stream" diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 76a5e66b66..c6b23efa1a 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -643,9 +643,6 @@ def test_model_normalization_quote_flexibility(): normalize_model_name('"catalog"."db"."table"', default_catalog=None, dialect="spark") == '"catalog"."db"."table"' ) - # It doesn't work the other way which is what we currently expect - with pytest.raises(ParseError): - normalize_model_name("`catalog`.`db`.`table`", default_catalog=None, dialect=None) def test_macro_parse(): From 8ff6aa211603ebdf294ff925b04873fa0bc3d1e8 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:34:01 +0100 Subject: [PATCH 0383/1056] feat: external model takes you to the model not just file (#4711) --- sqlmesh/lsp/helpers.py | 35 ++++++--- sqlmesh/lsp/main.py | 26 +++++-- sqlmesh/lsp/reference.py | 86 +++++++++++++++++++--- tests/lsp/test_reference_external_model.py | 38 ++++++++++ 4 files changed, 157 insertions(+), 28 deletions(-) create mode 100644 tests/lsp/test_reference_external_model.py diff --git a/sqlmesh/lsp/helpers.py b/sqlmesh/lsp/helpers.py index 7aa06ccb4c..920a93f5c7 100644 --- a/sqlmesh/lsp/helpers.py +++ b/sqlmesh/lsp/helpers.py @@ -6,22 +6,35 @@ ) -def to_lsp_range( - range: SQLMeshRange, -) -> Range: +def to_sqlmesh_position(position: Position) -> SQLMeshPosition: """ - Converts a SQLMesh Range to an LSP Range. + Converts an LSP Position to a SQLMesh Position. """ - return Range( - start=Position(line=range.start.line, character=range.start.character), - end=Position(line=range.end.line, character=range.end.character), - ) + return SQLMeshPosition(line=position.line, character=position.character) -def to_lsp_position( - position: SQLMeshPosition, -) -> Position: +def to_lsp_position(position: SQLMeshPosition) -> Position: """ Converts a SQLMesh Position to an LSP Position. """ return Position(line=position.line, character=position.character) + + +def to_sqlmesh_range(range: Range) -> SQLMeshRange: + """ + Converts an LSP Range to a SQLMesh Range. + """ + return SQLMeshRange( + start=to_sqlmesh_position(range.start), + end=to_sqlmesh_position(range.end), + ) + + +def to_lsp_range(range: SQLMeshRange) -> Range: + """ + Converts a SQLMesh Range to an LSP Range. + """ + return Range( + start=to_lsp_position(range.start), + end=to_lsp_position(range.end), + ) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 137ca7c07c..b028ea7766 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -48,6 +48,7 @@ from sqlmesh.lsp.reference import ( LSPCteReference, LSPModelReference, + LSPExternalModelReference, get_references, get_all_references, ) @@ -444,11 +445,19 @@ def goto_definition( references = get_references(self.lsp_context, uri, params.position) location_links = [] for reference in references: - # Use target_range if available (CTEs, Macros), otherwise default to start of file - if not isinstance(reference, LSPModelReference): - target_range = reference.target_range - target_selection_range = reference.target_range - else: + # Use target_range if available (CTEs, Macros, and external models in YAML) + if isinstance(reference, LSPModelReference): + # Regular SQL models - default to start of file + target_range = types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=0, character=0), + ) + target_selection_range = types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=0, character=0), + ) + elif isinstance(reference, LSPExternalModelReference): + # External models may have target_range set for YAML files target_range = types.Range( start=types.Position(line=0, character=0), end=types.Position(line=0, character=0), @@ -457,6 +466,13 @@ def goto_definition( start=types.Position(line=0, character=0), end=types.Position(line=0, character=0), ) + if reference.target_range is not None: + target_range = reference.target_range + target_selection_range = reference.target_range + else: + # CTEs and Macros always have target_range + target_range = reference.target_range + target_selection_range = reference.target_range location_links.append( types.LocationLink( diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index e401be898c..ac4d5374b6 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -8,7 +8,7 @@ from sqlmesh.core.linter.helpers import ( TokenPositionDetails, ) -from sqlmesh.core.model.definition import SqlModel +from sqlmesh.core.model.definition import SqlModel, ExternalModel from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget from sqlglot import exp from sqlmesh.lsp.description import generate_markdown_description @@ -22,10 +22,11 @@ from sqlmesh.core.model import Model from sqlmesh import macro import inspect +from ruamel.yaml import YAML class LSPModelReference(PydanticModel): - """A LSP reference to a model.""" + """A LSP reference to a model, excluding external models.""" type: t.Literal["model"] = "model" uri: str @@ -33,6 +34,16 @@ class LSPModelReference(PydanticModel): markdown_description: t.Optional[str] = None +class LSPExternalModelReference(PydanticModel): + """A LSP reference to an external model.""" + + type: t.Literal["external_model"] = "external_model" + uri: str + range: Range + markdown_description: t.Optional[str] = None + target_range: t.Optional[Range] = None + + class LSPCteReference(PydanticModel): """A LSP reference to a CTE.""" @@ -53,7 +64,8 @@ class LSPMacroReference(PydanticModel): Reference = t.Annotated[ - t.Union[LSPModelReference, LSPCteReference, LSPMacroReference], Field(discriminator="type") + t.Union[LSPModelReference, LSPCteReference, LSPMacroReference, LSPExternalModelReference], + Field(discriminator="type"), ] @@ -243,16 +255,38 @@ def get_model_definitions_for_a_path( description = generate_markdown_description(referenced_model) - references.append( - LSPModelReference( - uri=referenced_model_uri.value, - range=Range( - start=to_lsp_position(start_pos_sqlmesh), - end=to_lsp_position(end_pos_sqlmesh), - ), - markdown_description=description, + # For external models in YAML files, find the specific model block + if isinstance(referenced_model, ExternalModel): + yaml_target_range: t.Optional[Range] = None + if ( + referenced_model_path.suffix in (".yaml", ".yml") + and referenced_model_path.is_file() + ): + yaml_target_range = _get_yaml_model_range( + referenced_model_path, referenced_model.name + ) + references.append( + LSPExternalModelReference( + uri=referenced_model_uri.value, + range=Range( + start=to_lsp_position(start_pos_sqlmesh), + end=to_lsp_position(end_pos_sqlmesh), + ), + markdown_description=description, + target_range=yaml_target_range, + ) + ) + else: + references.append( + LSPModelReference( + uri=referenced_model_uri.value, + range=Range( + start=to_lsp_position(start_pos_sqlmesh), + end=to_lsp_position(end_pos_sqlmesh), + ), + markdown_description=description, + ) ) - ) return references @@ -699,3 +733,31 @@ def _position_within_range(position: Position, range: Range) -> bool: range.end.line > position.line or (range.end.line == position.line and range.end.character >= position.character) ) + + +def _get_yaml_model_range(path: Path, model_name: str) -> t.Optional[Range]: + """ + Find the range of a specific model block in a YAML file. + + Args: + yaml_path: Path to the YAML file + model_name: Name of the model to find + + Returns: + The Range of the model block in the YAML file, or None if not found + """ + yaml = YAML() + with path.open("r", encoding="utf-8") as f: + data = yaml.load(f) + + if not isinstance(data, list): + return None + + for item in data: + if isinstance(item, dict) and item.get("name") == model_name: + # Get size of block by taking the earliest line/col in the items block and the last line/col of the block + position_data = item.lc.data["name"] # type: ignore + start = Position(line=position_data[2], character=position_data[3]) + end = Position(line=position_data[2], character=position_data[3] + len(item["name"])) + return Range(start=start, end=end) + return None diff --git a/tests/lsp/test_reference_external_model.py b/tests/lsp/test_reference_external_model.py new file mode 100644 index 0000000000..ebf6420934 --- /dev/null +++ b/tests/lsp/test_reference_external_model.py @@ -0,0 +1,38 @@ +from lsprotocol.types import Position +from sqlmesh.core.context import Context +from sqlmesh.core.linter.helpers import read_range_from_file +from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.helpers import to_sqlmesh_range +from sqlmesh.lsp.reference import get_references, LSPExternalModelReference +from sqlmesh.lsp.uri import URI + + +def test_reference() -> None: + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Find model URIs + customers = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + # Position of reference in file sushi.customers for sushi.raw_demographics + position = Position(line=42, character=20) + references = get_references(lsp_context, URI.from_path(customers), position) + + assert len(references) == 1 + reference = references[0] + assert isinstance(reference, LSPExternalModelReference) + assert reference.uri.endswith("external_models.yaml") + + source_range = read_range_from_file(customers, to_sqlmesh_range(reference.range)) + assert source_range == "raw.demographics" + + if reference.target_range is None: + raise AssertionError("Reference target range should not be None") + target_range = read_range_from_file( + URI(reference.uri).to_path(), to_sqlmesh_range(reference.target_range) + ) + assert target_range == "raw.demographics" From c1a991e92c8fa821a92014dbf31391930a80ce3b Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:54:47 +0200 Subject: [PATCH 0384/1056] Chore: fix incorrect flags in audit command documentation (#4716) --- docs/concepts/audits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/audits.md b/docs/concepts/audits.md index 9aeda20a5c..b4a614295b 100644 --- a/docs/concepts/audits.md +++ b/docs/concepts/audits.md @@ -639,7 +639,7 @@ MODEL ( You can execute audits with the `sqlmesh audit` command as follows: ```bash -$ sqlmesh -p project audit -start 2022-01-01 -end 2022-01-02 +$ sqlmesh -p project audit --start 2022-01-01 --end 2022-01-02 Found 1 audit(s). assert_item_price_is_not_null FAIL. From e748f6f27206e37f327bf9c16dc23b6c9f165a77 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:42:56 +0100 Subject: [PATCH 0385/1056] feat(lsp): model descriptions in LSP completions (#4713) --- examples/sushi/models/latest_order.sql | 1 - sqlmesh/lsp/completions.py | 47 +++++++++++++++----------- sqlmesh/lsp/custom.py | 13 +++++-- sqlmesh/lsp/main.py | 10 ++++-- tests/lsp/test_completions.py | 14 ++++++++ 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/examples/sushi/models/latest_order.sql b/examples/sushi/models/latest_order.sql index 31293f19f9..4523537505 100644 --- a/examples/sushi/models/latest_order.sql +++ b/examples/sushi/models/latest_order.sql @@ -12,4 +12,3 @@ MODEL ( SELECT id, customer_id, start_ts, end_ts, event_date FROM sushi.orders ORDER BY event_date DESC LIMIT 1 - diff --git a/sqlmesh/lsp/completions.py b/sqlmesh/lsp/completions.py index 7e3781a550..0026260481 100644 --- a/sqlmesh/lsp/completions.py +++ b/sqlmesh/lsp/completions.py @@ -1,9 +1,14 @@ from functools import lru_cache from sqlglot import Dialect, Tokenizer -from sqlmesh.lsp.custom import AllModelsResponse, MacroCompletion +from sqlmesh.lsp.custom import ( + AllModelsResponse, + MacroCompletion, + ModelCompletion, +) from sqlmesh import macro import typing as t from sqlmesh.lsp.context import AuditTarget, LSPContext, ModelTarget +from sqlmesh.lsp.description import generate_markdown_description from sqlmesh.lsp.uri import URI @@ -26,14 +31,18 @@ def get_sql_completions( # Combine keywords - SQL keywords first, then file keywords all_keywords = list(sql_keywords) + list(file_keywords - sql_keywords) + models = list(get_models(context, file_uri)) return AllModelsResponse( - models=list(get_models(context, file_uri)), + models=[m.name for m in models], + model_completions=models, keywords=all_keywords, macros=list(get_macros(context, file_uri)), ) -def get_models(context: t.Optional[LSPContext], file_uri: t.Optional[URI]) -> t.Set[str]: +def get_models( + context: t.Optional[LSPContext], file_uri: t.Optional[URI] +) -> t.List[ModelCompletion]: """ Return a list of models for a given file. @@ -41,23 +50,23 @@ def get_models(context: t.Optional[LSPContext], file_uri: t.Optional[URI]) -> t. If there is a context, return a list of all models bar the ones the file itself defines. """ if context is None: - return set() + return [] + + current_path = file_uri.to_path() if file_uri is not None else None + + completions: t.List[ModelCompletion] = [] + for model in context.context.models.values(): + if current_path is not None and model._path == current_path: + continue + description = None + try: + description = generate_markdown_description(model) + except Exception: + description = getattr(model, "description", None) + + completions.append(ModelCompletion(name=model.name, description=description)) - all_models = set() - # Extract model names from ModelInfo objects - for file_info in context.map.values(): - if isinstance(file_info, ModelTarget): - all_models.update(file_info.names) - - # Remove models from the current file - path = file_uri.to_path() if file_uri is not None else None - if path is not None and path in context.map: - file_info = context.map[path] - if isinstance(file_info, ModelTarget): - for model in file_info.names: - all_models.discard(model) - - return all_models + return completions def get_macros( diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py index 5c8123b7a0..618b4a44bc 100644 --- a/sqlmesh/lsp/custom.py +++ b/sqlmesh/lsp/custom.py @@ -30,12 +30,19 @@ class MacroCompletion(PydanticModel): description: t.Optional[str] = None +class ModelCompletion(PydanticModel): + """Information about a model for autocompletion.""" + + name: str + description: t.Optional[str] = None + + class AllModelsResponse(CustomMethodResponseBaseClass): - """ - Response to get all the models that are in the current project. - """ + """Response to get all models that are in the current project.""" + #: Deprecated: use ``model_completions`` instead models: t.List[str] + model_completions: t.List[ModelCompletion] keywords: t.List[str] macros: t.List[MacroCompletion] diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index b028ea7766..75ac9b70a2 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -627,12 +627,18 @@ def completion( completion_items = [] # Add model completions - for model in completion_response.models: + for model in completion_response.model_completions: completion_items.append( types.CompletionItem( - label=model, + label=model.name, kind=types.CompletionItemKind.Reference, detail="SQLMesh Model", + documentation=types.MarkupContent( + kind=types.MarkupKind.Markdown, + value=model.description or "No description available", + ) + if model.description + else None, ) ) # Add macro completions diff --git a/tests/lsp/test_completions.py b/tests/lsp/test_completions.py index 7e193d77d6..e0772c1a96 100644 --- a/tests/lsp/test_completions.py +++ b/tests/lsp/test_completions.py @@ -41,6 +41,20 @@ def test_get_macros(): assert add_one_macro.description +def test_model_completions_include_descriptions(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + completions = LSPContext.get_completions(lsp_context, None) + + model_entry = next( + (m for m in completions.model_completions if m.name == "sushi.customers"), + None, + ) + assert model_entry is not None + assert model_entry.description + + def test_get_sql_completions_with_context_no_file_uri(): context = Context(paths=["examples/sushi"]) lsp_context = LSPContext(context) From af0d594a7a991ac47b197b3201086c07a358466f Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:20:19 -0700 Subject: [PATCH 0386/1056] feat: add docs for reporting incidents in cloud (#4710) Co-authored-by: Trey Spiller <1831878+treysp@users.noreply.github.com> --- docs/cloud/features/incident_reporting.md | 46 ++++++++++++++++++ .../incident_reporting/incident_reporting.png | Bin 0 -> 12628 bytes mkdocs.yml | 1 + 3 files changed, 47 insertions(+) create mode 100644 docs/cloud/features/incident_reporting.md create mode 100644 docs/cloud/features/incident_reporting/incident_reporting.png diff --git a/docs/cloud/features/incident_reporting.md b/docs/cloud/features/incident_reporting.md new file mode 100644 index 0000000000..1ff062e06b --- /dev/null +++ b/docs/cloud/features/incident_reporting.md @@ -0,0 +1,46 @@ +# Incident Reporting + +We monitor Tobiko Cloud 24/7 to ensure your projects are running smoothly. + +If you encounter any issues, however, you can report incidents directly in Tobiko Cloud itself. + +This will notify our support team, who will investigate and resolve the issue as quickly as possible. + +### Reporting an incident + +Follow these steps to report an incident in Tobiko Cloud: + +1. Visit the [Tobiko Cloud Incident Reporting Page](https://incidents.tobikodata.com/) +2. Select one of the three severity levels for your incident +3. Enter the project name the incident is related to + * The project name is displayed after your organization name in the Cloud UI +4. Write a detailed description of the incident + * Include all relevant information that will help our support team understand and resolve the issue +5. Click the `Submit` button to send your incident report +6. You will receive a confirmation message indicating that your incident has been reported successfully +7. You will hear from our support team after submitting the incident report + +![Tobiko Cloud incident reporting page](./incident_reporting/incident_reporting.png) + +### Reporting an incident when SSO is unavailable + +Single Sign-On (SSO) is the default way to log in to Tobiko Cloud. However, SSO could be down or not working when you need to report an incident. + +Tobiko Cloud provides a standalone page that doesn't require SSO so you can report an incident when SSO is not working. The page is unique to your organization. + +The standalone URL is available in the incident reporting page when you log in with SSO. Because accessing the standalone URL does not require SSO, you should only share it with staff authorized to report incidents. + +To store your standalone incident reporting URL: + +1. Visit the [Tobiko Cloud Incident Reporting Page](https://incidents.tobikodata.com/) +2. Click the `Copy Standalone URL` button below the incident reporting section +3. Save this URL in an easily accessible location in case you need to report an incident when SSO is not working + +!!! note "Don't wait!" + We recommend copying this URL *right now* so your organization is protected from difficulty reporting an incident. + +### SSO not enabled for your organization + +SSO login is required for accessing the standalone incident reporting URL. + +SSO is enabled by default in Tobiko Cloud. If it is not enabled for your organization, contact your solution architect and ask them to provide you with a standalone incident reporting URL. diff --git a/docs/cloud/features/incident_reporting/incident_reporting.png b/docs/cloud/features/incident_reporting/incident_reporting.png new file mode 100644 index 0000000000000000000000000000000000000000..542f1aa883e791ee8eef858606985d5840cf2042 GIT binary patch literal 12628 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 Date: Wed, 11 Jun 2025 18:35:29 +0100 Subject: [PATCH 0387/1056] fix(lsp): generate markdown including None (#4719) --- sqlmesh/lsp/description.py | 25 ++++++++++++------------- tests/lsp/test_description.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 13 deletions(-) create mode 100644 tests/lsp/test_description.py diff --git a/sqlmesh/lsp/description.py b/sqlmesh/lsp/description.py index 82ea93dd8d..768197742f 100644 --- a/sqlmesh/lsp/description.py +++ b/sqlmesh/lsp/description.py @@ -10,21 +10,20 @@ def generate_markdown_description( model: t.Union[SqlModel, ExternalModel, PythonModel, SeedModel], ) -> t.Optional[str]: + description = model.description columns = model.columns_to_types column_descriptions = model.column_descriptions - columns_table = ( - "\n".join( - [ - f"| {column} | {column_type} | {column_descriptions.get(column, '')} |" - for column, column_type in columns.items() - ] - ) - if columns - else "" - ) + if columns is None: + return description or None - table_header = "\n\n| Column | Type | Description |\n|--------|------|-------------|\n" - return ( - f"{model.description}{table_header}{columns_table}" if columns_table else model.description + columns_table = "\n".join( + [ + f"| {column} | {column_type} | {column_descriptions.get(column, '')} |" + for column, column_type in columns.items() + ] ) + + table_header = "| Column | Type | Description |\n|--------|------|-------------|\n" + columns_text = table_header + columns_table + return f"{description}\n\n{columns_text}" if description else columns_text diff --git a/tests/lsp/test_description.py b/tests/lsp/test_description.py new file mode 100644 index 0000000000..054d55fecc --- /dev/null +++ b/tests/lsp/test_description.py @@ -0,0 +1,32 @@ +from sqlmesh.core.context import Context +from sqlmesh.lsp.description import generate_markdown_description + + +def test_model_description() -> None: + context = Context(paths=["examples/sushi"]) + + model_no_description = context.get_model("sushi.order_items") + markdown = generate_markdown_description(model_no_description) + + assert markdown == ( + "| Column | Type | Description |\n" + "|--------|------|-------------|\n" + "| id | INT | |\n" + "| order_id | INT | |\n" + "| item_id | INT | |\n" + "| quantity | INT | |\n" + "| event_date | DATE | |" + ) + + model_with_description = context.get_model("sushi.customers") + markdown = generate_markdown_description(model_with_description) + + assert markdown == ( + "Sushi customer data\n" + "\n" + "| Column | Type | Description |\n" + "|--------|------|-------------|\n" + "| customer_id | INT | customer_id uniquely identifies customers |\n" + "| status | TEXT | |\n" + "| zip | TEXT | |" + ) From a9ef531fa4b1aa149452d276061d9b9168ec6589 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:30:03 +0100 Subject: [PATCH 0388/1056] fix(lsp): context is singleton in vscode (#4722) --- sqlmesh/lsp/main.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 75ac9b70a2..f9bfe46114 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -106,10 +106,13 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: A new LSPContext instance wrapping the created context, or None if creation fails """ try: - context = self.context_class(paths=paths) - lsp_context = LSPContext(context) - self.lsp_context = lsp_context - return lsp_context + if self.lsp_context is None: + context = self.context_class(paths=paths) + else: + self.lsp_context.context.load() + context = self.lsp_context.context + self.lsp_context = LSPContext(context) + return self.lsp_context except Exception as e: self.server.log_trace(f"Error creating context: {e}") return None @@ -293,8 +296,16 @@ def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> Non @self.server.feature(types.TEXT_DOCUMENT_DID_CHANGE) def did_change(ls: LanguageServer, params: types.DidChangeTextDocumentParams) -> None: + if self.lsp_context is None: + current_path = Path.cwd() + self._ensure_context_in_folder(current_path) + if self.lsp_context is None: + ls.log_trace("No context found after did_change") + return + uri = URI(params.text_document.uri) context = self._context_get_or_load(uri) + models = context.map[uri.to_path()] if models is None or not isinstance(models, ModelTarget): return @@ -731,9 +742,8 @@ def _ensure_context_for_document( for a config.py or config.yml file in the parent directories. """ if self.lsp_context is not None: - context = self.lsp_context - context.context.load() # Reload or refresh context - self.lsp_context = LSPContext(context.context) + self.lsp_context.context.load() + self.lsp_context = LSPContext(self.lsp_context.context) return # No context yet: try to find config and load it From 11ce8c89035c088051571211198beb5315d27158 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Thu, 12 Jun 2025 11:28:50 +1200 Subject: [PATCH 0389/1056] Fix(table_diff): Make `--limit` per-sample and not across all samples (#4727) --- sqlmesh/core/table_diff.py | 110 ++++++++++++++---- .../integration/test_integration.py | 4 +- tests/core/test_table_diff.py | 54 ++++++++- 3 files changed, 140 insertions(+), 28 deletions(-) diff --git a/sqlmesh/core/table_diff.py b/sqlmesh/core/table_diff.py index ac4c9c71cc..6a91b22dfb 100644 --- a/sqlmesh/core/table_diff.py +++ b/sqlmesh/core/table_diff.py @@ -24,6 +24,7 @@ from sqlmesh.core.engine_adapter import EngineAdapter SQLMESH_JOIN_KEY_COL = "__sqlmesh_join_key" +SQLMESH_SAMPLE_TYPE_COL = "__sqlmesh_sample_type" class SchemaDiff(PydanticModel, frozen=True): @@ -389,9 +390,6 @@ def _column_expr(name: str, table: str) -> exp.Expression: for c, t in matched_columns.items() ] - def name(e: exp.Expression) -> str: - return e.args["alias"].sql(identify=True) - source_query = ( exp.select( *(exp.column(c) for c in source_schema), @@ -581,30 +579,19 @@ def name(e: exp.Expression) -> str: .drop(index=index_cols, errors="ignore") ) - sample_filter_cols = ["s_exists", "t_exists", "row_joined", "row_full_match"] - sample_query = ( - exp.select( - *(sample_filter_cols), - *(name(c) for c in s_selects.values()), - *(name(c) for c in t_selects.values()), - ) - .from_(table) - .where(exp.or_(*(exp.column(c.alias).eq(0) for c in comparisons))) - .order_by( - *(name(s_selects[c.name]) for c in s_index), - *(name(t_selects[c.name]) for c in t_index), - ) - .limit(self.limit) + sample = self._fetch_sample( + table, s_selects, s_index, t_selects, t_index, self.limit ) - sample = self.adapter.fetchdf(sample_query, quote_identifiers=True) joined_sample_cols = [f"s__{c}" for c in s_index_names] comparison_cols = [ (f"s__{c}", f"t__{c}") for c in column_stats[column_stats["pct_match"] < 100].index ] + for cols in comparison_cols: joined_sample_cols.extend(cols) + joined_renamed_cols = { c: c.split("__")[1] if c.split("__")[1] in index_cols else c for c in joined_sample_cols @@ -638,13 +625,16 @@ def name(e: exp.Expression) -> str: ) for c, n in joined_renamed_cols.items() } - joined_sample = sample[sample["row_joined"] == 1][joined_sample_cols] + + joined_sample = sample[sample[SQLMESH_SAMPLE_TYPE_COL] == "common_rows"][ + joined_sample_cols + ] joined_sample.rename( columns=joined_renamed_cols, inplace=True, ) - s_sample = sample[(sample["s_exists"] == 1) & (sample["row_joined"] == 0)][ + s_sample = sample[sample[SQLMESH_SAMPLE_TYPE_COL] == "source_only"][ [ *[f"s__{c}" for c in s_index_names], *[f"s__{c}" for c in source_schema if c not in s_index_names], @@ -654,7 +644,7 @@ def name(e: exp.Expression) -> str: columns={c: c.replace("s__", "") for c in s_sample.columns}, inplace=True ) - t_sample = sample[(sample["t_exists"] == 1) & (sample["row_joined"] == 0)][ + t_sample = sample[sample[SQLMESH_SAMPLE_TYPE_COL] == "target_only"][ [ *[f"t__{c}" for c in t_index_names], *[f"t__{c}" for c in target_schema if c not in t_index_names], @@ -665,8 +655,11 @@ def name(e: exp.Expression) -> str: ) sample.drop( - columns=sample_filter_cols - + [f"s__{SQLMESH_JOIN_KEY_COL}", f"t__{SQLMESH_JOIN_KEY_COL}"], + columns=[ + f"s__{SQLMESH_JOIN_KEY_COL}", + f"t__{SQLMESH_JOIN_KEY_COL}", + SQLMESH_SAMPLE_TYPE_COL, + ], inplace=True, ) @@ -684,4 +677,75 @@ def name(e: exp.Expression) -> str: model_name=self.model_name, decimals=self.decimals, ) + return self._row_diff + + def _fetch_sample( + self, + sample_table: exp.Table, + s_selects: t.Dict[str, exp.Alias], + s_index: t.List[exp.Column], + t_selects: t.Dict[str, exp.Alias], + t_index: t.List[exp.Column], + limit: int, + ) -> pd.DataFrame: + rendered_data_column_names = [ + name(s) for s in list(s_selects.values()) + list(t_selects.values()) + ] + sample_type = exp.to_identifier(SQLMESH_SAMPLE_TYPE_COL) + + source_only_sample = ( + exp.select( + exp.Literal.string("source_only").as_(sample_type), *rendered_data_column_names + ) + .from_(sample_table) + .where(exp.and_(exp.column("s_exists").eq(1), exp.column("row_joined").eq(0))) + .order_by(*(name(s_selects[c.name]) for c in s_index)) + .limit(limit) + ) + + target_only_sample = ( + exp.select( + exp.Literal.string("target_only").as_(sample_type), *rendered_data_column_names + ) + .from_(sample_table) + .where(exp.and_(exp.column("t_exists").eq(1), exp.column("row_joined").eq(0))) + .order_by(*(name(t_selects[c.name]) for c in t_index)) + .limit(limit) + ) + + common_rows_sample = ( + exp.select( + exp.Literal.string("common_rows").as_(sample_type), *rendered_data_column_names + ) + .from_(sample_table) + .where(exp.and_(exp.column("row_joined").eq(1), exp.column("row_full_match").eq(0))) + .order_by( + *(name(s_selects[c.name]) for c in s_index), + *(name(t_selects[c.name]) for c in t_index), + ) + .limit(limit) + ) + + query = ( + exp.Select() + .with_("source_only", source_only_sample) + .with_("target_only", target_only_sample) + .with_("common_rows", common_rows_sample) + .select(sample_type, *rendered_data_column_names) + .from_("source_only") + .union( + exp.select(sample_type, *rendered_data_column_names).from_("target_only"), + distinct=False, + ) + .union( + exp.select(sample_type, *rendered_data_column_names).from_("common_rows"), + distinct=False, + ) + ) + + return self.adapter.fetchdf(query, quote_identifiers=True) + + +def name(e: exp.Expression) -> str: + return e.args["alias"].sql(identify=True) diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 6509eb447c..873d25547f 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -2358,8 +2358,8 @@ def test_table_diff_grain_check_multiple_keys(ctx: TestContext): assert row_diff.stats["distinct_count_s"] == 7 assert row_diff.stats["t_count"] != row_diff.stats["distinct_count_t"] assert row_diff.stats["distinct_count_t"] == 10 - assert row_diff.s_sample.shape == (0, 3) - assert row_diff.t_sample.shape == (3, 3) + assert row_diff.s_sample.shape == (row_diff.s_only_count, 3) + assert row_diff.t_sample.shape == (row_diff.t_only_count, 3) def test_table_diff_arbitrary_condition(ctx: TestContext): diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index bf491d77a7..a9b56650c7 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -306,8 +306,8 @@ def test_grain_check(sushi_context_fixed_date): assert row_diff.stats["distinct_count_s"] == 7 assert row_diff.stats["t_count"] != row_diff.stats["distinct_count_t"] assert row_diff.stats["distinct_count_t"] == 10 - assert row_diff.s_sample.shape == (0, 3) - assert row_diff.t_sample.shape == (3, 3) + assert row_diff.s_sample.shape == (row_diff.s_only_count, 3) + assert row_diff.t_sample.shape == (row_diff.t_only_count, 3) def test_generated_sql(sushi_context_fixed_date: Context, mocker: MockerFixture): @@ -338,7 +338,7 @@ def test_generated_sql(sushi_context_fixed_date: Context, mocker: MockerFixture) query_sql = 'CREATE TABLE IF NOT EXISTS "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" AS WITH "__source" AS (SELECT "s"."key", "s"."value", "s"."key" AS "__sqlmesh_join_key" FROM "table_diff_source" AS "s"), "__target" AS (SELECT "t"."key", "t"."value", "t"."key" AS "__sqlmesh_join_key" FROM "table_diff_target" AS "t"), "__stats" AS (SELECT "s"."key" AS "s__key", "s"."value" AS "s__value", "s"."__sqlmesh_join_key" AS "s____sqlmesh_join_key", "t"."key" AS "t__key", "t"."value" AS "t__value", "t"."__sqlmesh_join_key" AS "t____sqlmesh_join_key", CASE WHEN NOT "s"."key" IS NULL THEN 1 ELSE 0 END AS "s_exists", CASE WHEN NOT "t"."key" IS NULL THEN 1 ELSE 0 END AS "t_exists", CASE WHEN "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key" AND (NOT "s"."key" IS NULL AND NOT "t"."key" IS NULL) THEN 1 ELSE 0 END AS "row_joined", CASE WHEN "s"."key" IS NULL AND "t"."key" IS NULL THEN 1 ELSE 0 END AS "null_grain", CASE WHEN "s"."key" = "t"."key" THEN 1 WHEN ("s"."key" IS NULL) AND ("t"."key" IS NULL) THEN 1 WHEN ("s"."key" IS NULL) OR ("t"."key" IS NULL) THEN 0 ELSE 0 END AS "key_matches", CASE WHEN ROUND("s"."value", 3) = ROUND("t"."value", 3) THEN 1 WHEN ("s"."value" IS NULL) AND ("t"."value" IS NULL) THEN 1 WHEN ("s"."value" IS NULL) OR ("t"."value" IS NULL) THEN 0 ELSE 0 END AS "value_matches" FROM "__source" AS "s" FULL JOIN "__target" AS "t" ON "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key") SELECT *, CASE WHEN "key_matches" = 1 AND "value_matches" = 1 THEN 1 ELSE 0 END AS "row_full_match" FROM "__stats"' summary_query_sql = 'SELECT SUM("s_exists") AS "s_count", SUM("t_exists") AS "t_count", SUM("row_joined") AS "join_count", SUM("null_grain") AS "null_grain_count", SUM("row_full_match") AS "full_match_count", SUM("key_matches") AS "key_matches", SUM("value_matches") AS "value_matches", COUNT(DISTINCT ("s____sqlmesh_join_key")) AS "distinct_count_s", COUNT(DISTINCT ("t____sqlmesh_join_key")) AS "distinct_count_t" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh"' compare_sql = 'SELECT ROUND(100 * (CAST(SUM("key_matches") AS DECIMAL) / COUNT("key_matches")), 9) AS "key_matches", ROUND(100 * (CAST(SUM("value_matches") AS DECIMAL) / COUNT("value_matches")), 9) AS "value_matches" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "row_joined" = 1' - sample_query_sql = 'SELECT "s_exists", "t_exists", "row_joined", "row_full_match", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "key_matches" = 0 OR "value_matches" = 0 ORDER BY "s__key" NULLS FIRST, "t__key" NULLS FIRST LIMIT 20' + sample_query_sql = 'WITH "source_only" AS (SELECT \'source_only\' AS "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "s_exists" = 1 AND "row_joined" = 0 ORDER BY "s__key" NULLS FIRST LIMIT 20), "target_only" AS (SELECT \'target_only\' AS "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "t_exists" = 1 AND "row_joined" = 0 ORDER BY "t__key" NULLS FIRST LIMIT 20), "common_rows" AS (SELECT \'common_rows\' AS "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "row_joined" = 1 AND "row_full_match" = 0 ORDER BY "s__key" NULLS FIRST, "t__key" NULLS FIRST LIMIT 20) SELECT "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "source_only" UNION ALL SELECT "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "target_only" UNION ALL SELECT "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "common_rows"' drop_sql = 'DROP TABLE IF EXISTS "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh"' # make with_log_level() return the current instance of engine_adapter so we can still spy on _execute @@ -1032,3 +1032,51 @@ def test_schema_diff_ignore_case(): exp.DataType.build("date"), ) # notice: source casing used on output } + + +def test_data_diff_sample_limit(): + engine_adapter = DuckDBConnectionConfig().create_engine_adapter() + + columns_to_types = {"id": exp.DataType.build("int"), "name": exp.DataType.build("varchar")} + + engine_adapter.create_table("src", columns_to_types) + engine_adapter.create_table("target", columns_to_types) + + common_records = {} + src_only_records = {} + target_only_records = {} + + for i in range(0, 10): + common_records[i] = f"common_{i}" + src_only_records[i + 20] = f"src_{i}" + target_only_records[i + 40] = f"target_{i}" + + src_records = {**common_records, **src_only_records} + target_records = {**common_records, **target_only_records} + + # changes + src_records[1] = "modified_source_1" + src_records[3] = "modified_source_3" + target_records[2] = "modified_target_2" + target_records[7] = "modified_target_7" + + src_df = pd.DataFrame.from_records([{"id": k, "name": v} for k, v in src_records.items()]) + target_df = pd.DataFrame.from_records([{"id": k, "name": v} for k, v in target_records.items()]) + + engine_adapter.insert_append("src", src_df) + engine_adapter.insert_append("target", target_df) + + table_diff = TableDiff( + adapter=engine_adapter, source="src", target="target", on=["id"], limit=3 + ) + + diff = table_diff.row_diff() + + assert diff.s_only_count == 10 + assert diff.t_only_count == 10 + assert diff.join_count == 10 + + # each sample should contain :limit records + assert len(diff.s_sample) == 3 + assert len(diff.t_sample) == 3 + assert len(diff.joined_sample) == 3 From e3bf2111320a96172c331bdcdbcf330d5264fe50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Thal=C3=A9n?= Date: Thu, 12 Jun 2025 05:57:03 +0200 Subject: [PATCH 0390/1056] Enhances MSSQL connection with pyodbc support v2 (#4686) --- docs/integrations/engines/azuresql.md | 16 +- docs/integrations/engines/mssql.md | 12 +- pyproject.toml | 3 + sqlmesh/core/config/connection.py | 127 +++++++++++- sqlmesh/core/engine_adapter/mssql.py | 4 + tests/core/test_connection_config.py | 265 ++++++++++++++++++++++++++ 6 files changed, 413 insertions(+), 14 deletions(-) diff --git a/docs/integrations/engines/azuresql.md b/docs/integrations/engines/azuresql.md index e9b97abaa1..5b54ffa9c6 100644 --- a/docs/integrations/engines/azuresql.md +++ b/docs/integrations/engines/azuresql.md @@ -2,15 +2,18 @@ [Azure SQL](https://azure.microsoft.com/en-us/products/azure-sql) is "a family of managed, secure, and intelligent products that use the SQL Server database engine in the Azure cloud." -The Azure SQL adapter only supports authentication with a username and password. It does not support authentication with Microsoft Entra or Azure Active Directory. - ## Local/Built-in Scheduler **Engine Adapter Type**: `azuresql` ### Installation +#### User / Password Authentication: ``` pip install "sqlmesh[azuresql]" ``` +#### Microsoft Entra ID / Azure Active Directory Authentication: +``` +pip install "sqlmesh[azuresql-odbc]" +``` ### Connection options @@ -18,8 +21,8 @@ pip install "sqlmesh[azuresql]" | ----------------- | ---------------------------------------------------------------- | :----------: | :------: | | `type` | Engine type name - must be `azuresql` | string | Y | | `host` | The hostname of the Azure SQL server | string | Y | -| `user` | The username to use for authentication with the Azure SQL server | string | N | -| `password` | The password to use for authentication with the Azure SQL server | string | N | +| `user` | The username / client ID to use for authentication with the Azure SQL server | string | N | +| `password` | The password / client secret to use for authentication with the Azure SQL server | string | N | | `port` | The port number of the Azure SQL server | int | N | | `database` | The target database | string | N | | `charset` | The character set used for the connection | string | N | @@ -27,4 +30,7 @@ pip install "sqlmesh[azuresql]" | `login_timeout` | The timeout for connection and login in seconds. Default: 60 | int | N | | `appname` | The application name to use for the connection | string | N | | `conn_properties` | The list of connection properties | list[string] | N | -| `autocommit` | Is autocommit mode enabled. Default: false | bool | N | \ No newline at end of file +| `autocommit` | Is autocommit mode enabled. Default: false | bool | N | +| `driver` | The driver to use for the connection. Default: pymssql | string | N | +| `driver_name` | The driver name to use for the connection. E.g., *ODBC Driver 18 for SQL Server* | string | N | +| `odbc_properties` | The dict of ODBC connection properties. E.g., authentication: ActiveDirectoryServicePrincipal. See more [here](https://learn.microsoft.com/en-us/sql/connect/odbc/dsn-connection-string-attribute?view=sql-server-ver16). | dict | N | \ No newline at end of file diff --git a/docs/integrations/engines/mssql.md b/docs/integrations/engines/mssql.md index 1650319d07..f06b5f1387 100644 --- a/docs/integrations/engines/mssql.md +++ b/docs/integrations/engines/mssql.md @@ -4,9 +4,14 @@ **Engine Adapter Type**: `mssql` ### Installation +#### User / Password Authentication: ``` pip install "sqlmesh[mssql]" ``` +#### Microsoft Entra ID / Azure Active Directory Authentication: +``` +pip install "sqlmesh[mssql-odbc]" +``` ### Connection options @@ -14,8 +19,8 @@ pip install "sqlmesh[mssql]" | ----------------- | ------------------------------------------------------------ | :----------: | :------: | | `type` | Engine type name - must be `mssql` | string | Y | | `host` | The hostname of the MSSQL server | string | Y | -| `user` | The username to use for authentication with the MSSQL server | string | N | -| `password` | The password to use for authentication with the MSSQL server | string | N | +| `user` | The username / client id to use for authentication with the MSSQL server | string | N | +| `password` | The password / client secret to use for authentication with the MSSQL server | string | N | | `port` | The port number of the MSSQL server | int | N | | `database` | The target database | string | N | | `charset` | The character set used for the connection | string | N | @@ -24,3 +29,6 @@ pip install "sqlmesh[mssql]" | `appname` | The application name to use for the connection | string | N | | `conn_properties` | The list of connection properties | list[string] | N | | `autocommit` | Is autocommit mode enabled. Default: false | bool | N | +| `driver` | The driver to use for the connection. Default: pymssql | string | N | +| `driver_name` | The driver name to use for the connection. E.g., *ODBC Driver 18 for SQL Server* | string | N | +| `odbc_properties` | The dict of ODBC connection properties. E.g., authentication: ActiveDirectoryServicePrincipal. See more [here](https://learn.microsoft.com/en-us/sql/connect/odbc/dsn-connection-string-attribute?view=sql-server-ver16). | dict | N | \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b172c86375..160b1be786 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ classifiers = [ [project.optional-dependencies] athena = ["PyAthena[Pandas]"] azuresql = ["pymssql"] +azuresql-odbc = ["pyodbc"] bigquery = [ "google-cloud-bigquery[pandas]", "google-cloud-bigquery-storage" @@ -104,6 +105,7 @@ gcppostgres = ["cloud-sql-python-connector[pg8000]>=1.8.0"] github = ["PyGithub~=2.5.0"] llm = ["langchain", "openai"] mssql = ["pymssql"] +mssql-odbc = ["pyodbc"] mysql = ["pymysql"] mwaa = ["boto3"] postgres = ["psycopg2"] @@ -203,6 +205,7 @@ module = [ "databricks_cli.*", "mysql.*", "pymssql.*", + "pyodbc.*", "psycopg2.*", "langchain.*", "pytest_lazyfixture.*", diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index e7e138c908..691b3a7731 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -55,11 +55,10 @@ def _get_engine_import_validator( - import_name: str, engine_type: str, extra_name: t.Optional[str] = None + import_name: str, engine_type: str, extra_name: t.Optional[str] = None, decorate: bool = True ) -> t.Callable: extra_name = extra_name or engine_type - @model_validator(mode="before") def validate(cls: t.Any, data: t.Any) -> t.Any: check_import = ( str_to_bool(str(data.pop("check_import", True))) if isinstance(data, dict) else True @@ -83,7 +82,7 @@ def validate(cls: t.Any, data: t.Any) -> t.Any: return data - return validate + return model_validator(mode="before")(validate) if decorate else validate class ConnectionConfig(abc.ABC, BaseConfig): @@ -1422,17 +1421,50 @@ class MSSQLConnectionConfig(ConnectionConfig): autocommit: t.Optional[bool] = False tds_version: t.Optional[str] = None + # Driver options + driver: t.Literal["pymssql", "pyodbc"] = "pymssql" + # PyODBC specific options + driver_name: t.Optional[str] = None # e.g. "ODBC Driver 18 for SQL Server" + trust_server_certificate: t.Optional[bool] = None + encrypt: t.Optional[bool] = None + # Dictionary of arbitrary ODBC connection properties + # See: https://learn.microsoft.com/en-us/sql/connect/odbc/dsn-connection-string-attribute + odbc_properties: t.Optional[t.Dict[str, t.Any]] = None + concurrent_tasks: int = 4 register_comments: bool = True pre_ping: bool = True type_: t.Literal["mssql"] = Field(alias="type", default="mssql") - _engine_import_validator = _get_engine_import_validator("pymssql", "mssql") + @model_validator(mode="before") + @classmethod + def _mssql_engine_import_validator(cls, data: t.Any) -> t.Any: + if not isinstance(data, dict): + return data + + driver = data.get("driver", "pymssql") + + # Define the mapping of driver to import module and extra name + driver_configs = {"pymssql": ("pymssql", "mssql"), "pyodbc": ("pyodbc", "mssql-odbc")} + + if driver not in driver_configs: + raise ValueError(f"Unsupported driver: {driver}") + + import_module, extra_name = driver_configs[driver] + + # Use _get_engine_import_validator with decorate=False to get the raw validation function + # This avoids the __wrapped__ issue in Python 3.9 + validator_func = _get_engine_import_validator( + import_module, driver, extra_name, decorate=False + ) + + # Call the raw validation function directly + return validator_func(cls, data) @property def _connection_kwargs_keys(self) -> t.Set[str]: - return { + base_keys = { "host", "user", "password", @@ -1447,15 +1479,96 @@ def _connection_kwargs_keys(self) -> t.Set[str]: "tds_version", } + if self.driver == "pyodbc": + base_keys.update( + { + "driver_name", + "trust_server_certificate", + "encrypt", + "odbc_properties", + } + ) + # Remove pymssql-specific parameters + base_keys.discard("tds_version") + base_keys.discard("conn_properties") + + return base_keys + @property def _engine_adapter(self) -> t.Type[EngineAdapter]: return engine_adapter.MSSQLEngineAdapter @property def _connection_factory(self) -> t.Callable: - import pymssql + if self.driver == "pymssql": + import pymssql + + return pymssql.connect + + import pyodbc + + def connect(**kwargs: t.Any) -> t.Callable: + # Extract parameters for connection string + host = kwargs.pop("host") + port = kwargs.pop("port", 1433) + database = kwargs.pop("database", "") + user = kwargs.pop("user", None) + password = kwargs.pop("password", None) + driver_name = kwargs.pop("driver_name", "ODBC Driver 18 for SQL Server") + trust_server_certificate = kwargs.pop("trust_server_certificate", False) + encrypt = kwargs.pop("encrypt", True) + login_timeout = kwargs.pop("login_timeout", 60) + + # Build connection string + conn_str_parts = [ + f"DRIVER={{{driver_name}}}", + f"SERVER={host},{port}", + ] + + if database: + conn_str_parts.append(f"DATABASE={database}") + + # Add security options + conn_str_parts.append(f"Encrypt={'YES' if encrypt else 'NO'}") + if trust_server_certificate: + conn_str_parts.append("TrustServerCertificate=YES") + + conn_str_parts.append(f"Connection Timeout={login_timeout}") + + # Standard SQL Server authentication + if user: + conn_str_parts.append(f"UID={user}") + if password: + conn_str_parts.append(f"PWD={password}") + + # Add any additional ODBC properties from the odbc_properties dictionary + if self.odbc_properties: + for key, value in self.odbc_properties.items(): + # Skip properties that we've already set above + if key.lower() in ( + "driver", + "server", + "database", + "uid", + "pwd", + "encrypt", + "trustservercertificate", + "connection timeout", + ): + continue - return pymssql.connect + # Handle boolean values properly + if isinstance(value, bool): + conn_str_parts.append(f"{key}={'YES' if value else 'NO'}") + else: + conn_str_parts.append(f"{key}={value}") + + # Create the connection string + conn_str = ";".join(conn_str_parts) + + return pyodbc.connect(conn_str, autocommit=kwargs.get("autocommit", False)) + + return connect @property def _extra_engine_config(self) -> t.Dict[str, t.Any]: diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index 796bb87960..40649f3c2d 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -219,6 +219,10 @@ def _df_to_source_queries( assert isinstance(df, pd.DataFrame) temp_table = self._get_temp_table(target_table or "pandas") + # Return the superclass implementation if the connection pool doesn't support bulk_copy + if not hasattr(self._connection_pool.get(), "bulk_copy"): + return super()._df_to_source_queries(df, columns_to_types, batch_size, target_table) + def query_factory() -> Query: # It is possible for the factory to be called multiple times and if so then the temp table will already # be created so we skip creating again. This means we are assuming the first call is the same result diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index d106559b67..ba33cb010b 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -4,6 +4,7 @@ import pytest from _pytest.fixtures import FixtureRequest +from unittest.mock import patch from sqlmesh.core.config.connection import ( BigQueryConnectionConfig, @@ -19,6 +20,7 @@ SnowflakeConnectionConfig, TrinoAuthenticationMethod, AthenaConnectionConfig, + MSSQLConnectionConfig, _connection_config_validator, _get_engine_import_validator, ) @@ -1127,3 +1129,266 @@ class TestConfigC(PydanticModel): _engine_import_validator = _get_engine_import_validator("sqlmesh", "bigquery") TestConfigC() + + +def test_mssql_engine_import_validator(): + """Test that MSSQL import validator respects driver configuration.""" + + # Test PyODBC driver suggests mssql-odbc extra when import fails + with pytest.raises(ConfigError, match=r"pip install \"sqlmesh\[mssql-odbc\]\""): + with patch("importlib.import_module") as mock_import: + mock_import.side_effect = ImportError("No module named 'pyodbc'") + MSSQLConnectionConfig(host="localhost", driver="pyodbc") + + # Test PyMSSQL driver suggests mssql extra when import fails + with pytest.raises(ConfigError, match=r"pip install \"sqlmesh\[mssql\]\""): + with patch("importlib.import_module") as mock_import: + mock_import.side_effect = ImportError("No module named 'pymssql'") + MSSQLConnectionConfig(host="localhost", driver="pymssql") + + # Test default driver (pymssql) suggests mssql extra when import fails + with pytest.raises(ConfigError, match=r"pip install \"sqlmesh\[mssql\]\""): + with patch("importlib.import_module") as mock_import: + mock_import.side_effect = ImportError("No module named 'pymssql'") + MSSQLConnectionConfig(host="localhost") # No driver specified + + # Test successful import works without error + with patch("importlib.import_module") as mock_import: + mock_import.return_value = None + config = MSSQLConnectionConfig(host="localhost", driver="pyodbc") + assert config.driver == "pyodbc" + + +def test_mssql_connection_config_parameter_validation(make_config): + """Test MSSQL connection config parameter validation.""" + # Test default driver is pymssql + config = make_config(type="mssql", host="localhost", check_import=False) + assert isinstance(config, MSSQLConnectionConfig) + assert config.driver == "pymssql" + + # Test explicit pyodbc driver + config = make_config(type="mssql", host="localhost", driver="pyodbc", check_import=False) + assert isinstance(config, MSSQLConnectionConfig) + assert config.driver == "pyodbc" + + # Test explicit pymssql driver + config = make_config(type="mssql", host="localhost", driver="pymssql", check_import=False) + assert isinstance(config, MSSQLConnectionConfig) + assert config.driver == "pymssql" + + # Test pyodbc specific parameters + config = make_config( + type="mssql", + host="localhost", + driver="pyodbc", + driver_name="ODBC Driver 18 for SQL Server", + trust_server_certificate=True, + encrypt=False, + odbc_properties={"Authentication": "ActiveDirectoryServicePrincipal"}, + check_import=False, + ) + assert isinstance(config, MSSQLConnectionConfig) + assert config.driver_name == "ODBC Driver 18 for SQL Server" + assert config.trust_server_certificate is True + assert config.encrypt is False + assert config.odbc_properties == {"Authentication": "ActiveDirectoryServicePrincipal"} + + # Test pymssql specific parameters + config = make_config( + type="mssql", + host="localhost", + driver="pymssql", + tds_version="7.4", + conn_properties=["SET ANSI_NULLS ON"], + check_import=False, + ) + assert isinstance(config, MSSQLConnectionConfig) + assert config.tds_version == "7.4" + assert config.conn_properties == ["SET ANSI_NULLS ON"] + + +def test_mssql_connection_kwargs_keys(): + """Test _connection_kwargs_keys returns correct keys for each driver variant.""" + # Test pymssql driver keys + config = MSSQLConnectionConfig(host="localhost", driver="pymssql", check_import=False) + pymssql_keys = config._connection_kwargs_keys + expected_pymssql_keys = { + "password", + "user", + "database", + "host", + "timeout", + "login_timeout", + "charset", + "appname", + "port", + "tds_version", + "conn_properties", + "autocommit", + } + assert pymssql_keys == expected_pymssql_keys + + # Test pyodbc driver keys + config = MSSQLConnectionConfig(host="localhost", driver="pyodbc", check_import=False) + pyodbc_keys = config._connection_kwargs_keys + expected_pyodbc_keys = { + "password", + "user", + "database", + "host", + "timeout", + "login_timeout", + "charset", + "appname", + "port", + "autocommit", + "driver_name", + "trust_server_certificate", + "encrypt", + "odbc_properties", + } + assert pyodbc_keys == expected_pyodbc_keys + + # Verify pyodbc keys don't include pymssql-specific parameters + assert "tds_version" not in pyodbc_keys + assert "conn_properties" not in pyodbc_keys + + +def test_mssql_pyodbc_connection_string_generation(): + """Test pyodbc.connect gets invoked with the correct ODBC connection string.""" + with patch("pyodbc.connect") as mock_pyodbc_connect: + # Mock the return value to have the methods we need + mock_connection = mock_pyodbc_connect.return_value + + # Create a pyodbc config + config = MSSQLConnectionConfig( + host="testserver.database.windows.net", + port=1433, + database="testdb", + user="testuser", + password="testpass", + driver="pyodbc", + driver_name="ODBC Driver 18 for SQL Server", + trust_server_certificate=True, + encrypt=True, + login_timeout=30, + check_import=False, + ) + + # Get the connection factory with kwargs and call it + factory_with_kwargs = config._connection_factory_with_kwargs + connection = factory_with_kwargs() + + # Verify pyodbc.connect was called with the correct connection string + mock_pyodbc_connect.assert_called_once() + call_args = mock_pyodbc_connect.call_args + + # Check the connection string (first argument) + conn_str = call_args[0][0] + expected_parts = [ + "DRIVER={ODBC Driver 18 for SQL Server}", + "SERVER=testserver.database.windows.net,1433", + "DATABASE=testdb", + "Encrypt=YES", + "TrustServerCertificate=YES", + "Connection Timeout=30", + "UID=testuser", + "PWD=testpass", + ] + + for part in expected_parts: + assert part in conn_str + + # Check autocommit parameter + assert call_args[1]["autocommit"] is False + + +def test_mssql_pyodbc_connection_string_with_odbc_properties(): + """Test pyodbc connection string includes custom ODBC properties.""" + with patch("pyodbc.connect") as mock_pyodbc_connect: + # Create a pyodbc config with custom ODBC properties + config = MSSQLConnectionConfig( + host="testserver.database.windows.net", + database="testdb", + user="client-id", + password="client-secret", + driver="pyodbc", + odbc_properties={ + "Authentication": "ActiveDirectoryServicePrincipal", + "ClientCertificate": "/path/to/cert.pem", + "TrustServerCertificate": "NO", # This should be ignored since we set it explicitly + }, + trust_server_certificate=True, # This should take precedence + check_import=False, + ) + + # Get the connection factory with kwargs and call it + factory_with_kwargs = config._connection_factory_with_kwargs + connection = factory_with_kwargs() + + # Verify pyodbc.connect was called + mock_pyodbc_connect.assert_called_once() + conn_str = mock_pyodbc_connect.call_args[0][0] + + # Check that custom ODBC properties are included + assert "Authentication=ActiveDirectoryServicePrincipal" in conn_str + assert "ClientCertificate=/path/to/cert.pem" in conn_str + + # Verify that explicit trust_server_certificate takes precedence + assert "TrustServerCertificate=YES" in conn_str + + # Should not have the conflicting property from odbc_properties + assert conn_str.count("TrustServerCertificate") == 1 + + +def test_mssql_pyodbc_connection_string_minimal(): + """Test pyodbc connection string with minimal configuration.""" + with patch("pyodbc.connect") as mock_pyodbc_connect: + config = MSSQLConnectionConfig( + host="localhost", + driver="pyodbc", + autocommit=True, + check_import=False, + ) + + factory_with_kwargs = config._connection_factory_with_kwargs + connection = factory_with_kwargs() + + mock_pyodbc_connect.assert_called_once() + conn_str = mock_pyodbc_connect.call_args[0][0] + + # Check basic required parts + assert "DRIVER={ODBC Driver 18 for SQL Server}" in conn_str + assert "SERVER=localhost,1433" in conn_str + assert "Encrypt=YES" in conn_str # Default encrypt=True + assert "Connection Timeout=60" in conn_str # Default timeout + + # Check autocommit parameter + assert mock_pyodbc_connect.call_args[1]["autocommit"] is True + + +def test_mssql_pymssql_connection_factory(): + """Test pymssql connection factory returns correct function.""" + # Mock the import of pymssql at the module level + import sys + from unittest.mock import MagicMock + + # Create a mock pymssql module + mock_pymssql = MagicMock() + sys.modules["pymssql"] = mock_pymssql + + try: + config = MSSQLConnectionConfig( + host="localhost", + driver="pymssql", + check_import=False, + ) + + factory = config._connection_factory + + # Verify the factory returns pymssql.connect + assert factory is mock_pymssql.connect + finally: + # Clean up the mock module + if "pymssql" in sys.modules: + del sys.modules["pymssql"] From 2f9f32ff4fc7074b4d7b1642236162ff850ba95e Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 12 Jun 2025 23:07:50 +0300 Subject: [PATCH 0391/1056] Chore!: bump sqlglot to v26.27.0 (#4730) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 160b1be786..93e034de57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.26.0", + "sqlglot[rs]~=26.27.0", "tenacity", "time-machine", "json-stream" From 2efe428285108b3de0b6cba4f3ec8e9e62ea69b8 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 12 Jun 2025 17:08:27 -0700 Subject: [PATCH 0392/1056] Fix: Import validation for the snowflake connection config (#4723) --- sqlmesh/core/config/connection.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 691b3a7731..b3ed3bc34f 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -539,7 +539,6 @@ class SnowflakeConnectionConfig(ConnectionConfig): type_: t.Literal["snowflake"] = Field(alias="type", default="snowflake") _concurrent_tasks_validator = concurrent_tasks_validator - _engine_import_validator = _get_engine_import_validator("snowflake", "snowflake") @model_validator(mode="before") def _validate_authenticator(cls, data: t.Any) -> t.Any: @@ -566,6 +565,10 @@ def _validate_authenticator(cls, data: t.Any) -> t.Any: return data + _engine_import_validator = _get_engine_import_validator( + "snowflake.connector.network", "snowflake" + ) + @classmethod def _get_private_key(cls, values: t.Dict[str, t.Optional[str]], auth: str) -> t.Optional[bytes]: """ @@ -733,7 +736,6 @@ class DatabricksConnectionConfig(ConnectionConfig): _concurrent_tasks_validator = concurrent_tasks_validator _http_headers_validator = http_headers_validator - _engine_import_validator = _get_engine_import_validator("databricks", "databricks") @model_validator(mode="before") def _databricks_connect_validator(cls, data: t.Any) -> t.Any: @@ -811,6 +813,8 @@ def _databricks_connect_validator(cls, data: t.Any) -> t.Any: return data + _engine_import_validator = _get_engine_import_validator("databricks", "databricks") + @property def _connection_kwargs_keys(self) -> t.Set[str]: if self.use_spark_session_only: From 660be2524930070bed0ecba7f7af560ffd4c7834 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 13 Jun 2025 12:24:35 +1200 Subject: [PATCH 0393/1056] Fix: Make plan dates relative to `--execution-time` (#4702) --- sqlmesh/core/context.py | 22 +++- sqlmesh/core/plan/builder.py | 57 ++++++++- sqlmesh/core/plan/definition.py | 18 ++- sqlmesh/core/snapshot/definition.py | 7 +- sqlmesh/utils/date.py | 34 ++++-- tests/core/test_context.py | 94 +++++++++++++++ tests/core/test_plan.py | 108 ++++++++++++++++++ .../github/cicd/test_github_controller.py | 4 +- tests/utils/test_date.py | 15 +++ 9 files changed, 334 insertions(+), 25 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 0450827d6e..f919c51182 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -120,7 +120,14 @@ from sqlmesh.utils import UniqueKeyDict, Verbosity from sqlmesh.utils.concurrency import concurrent_apply_to_values from sqlmesh.utils.dag import DAG -from sqlmesh.utils.date import TimeLike, now_ds, to_timestamp, format_tz_datetime, now_timestamp +from sqlmesh.utils.date import ( + TimeLike, + now_ds, + to_timestamp, + format_tz_datetime, + now_timestamp, + now, +) from sqlmesh.utils.errors import ( CircuitBreakerError, ConfigError, @@ -1513,7 +1520,11 @@ def plan_builder( # If no end date is specified, use the max interval end from prod # to prevent unintended evaluation of the entire DAG. default_start, default_end = self._get_plan_default_start_end( - snapshots, max_interval_end_per_model, backfill_models, modified_model_names + snapshots, + max_interval_end_per_model, + backfill_models, + modified_model_names, + execution_time or now(), ) # Refresh snapshot intervals to ensure that they are up to date with values reflected in the max_interval_end_per_model. @@ -2818,6 +2829,7 @@ def _get_plan_default_start_end( max_interval_end_per_model: t.Dict[str, int], backfill_models: t.Optional[t.Set[str]], modified_model_names: t.Set[str], + execution_time: t.Optional[TimeLike] = None, ) -> t.Tuple[t.Optional[int], t.Optional[int]]: if not max_interval_end_per_model: return None, None @@ -2843,6 +2855,12 @@ def _get_plan_default_start_end( ) ), ) + + if execution_time and to_timestamp(default_end) > to_timestamp(execution_time): + # the end date can't be in the future, which can happen if a specific `execution_time` is set and prod intervals + # are newer than it + default_end = to_timestamp(execution_time) + return default_start, default_end def _get_max_interval_end_per_model( diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 27b81f5d74..b2ff0a087c 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -41,6 +41,8 @@ to_datetime, yesterday_ds, to_timestamp, + time_like_to_str, + is_relative, ) from sqlmesh.utils.errors import NoChangesPlanError, PlanError @@ -55,6 +57,7 @@ class PlanBuilder: start: The start time to backfill data. end: The end time to backfill data. execution_time: The date/time time reference to use for execution time. Defaults to now. + If :start or :end are relative time expressions, they are interpreted as relative to the :execution_time apply: The callback to apply the plan. restate_models: A list of models for which the data should be restated for the time range specified in this plan. Note: models defined outside SQLMesh (external) won't be a part @@ -137,7 +140,14 @@ def __init__( self._include_unmodified = include_unmodified self._restate_models = set(restate_models) if restate_models is not None else None self._effective_from = effective_from + + # note: this deliberately doesnt default to now() here. + # There may be an significant delay between the PlanBuilder producing a Plan and the Plan actually being run + # so if execution_time=None is passed to the PlanBuilder, then the resulting Plan should also have execution_time=None + # in order to prevent the Plan that was intended to run "as at now" from having "now" fixed to some time in the past + # ref: https://github.com/TobikoData/sqlmesh/pull/4702#discussion_r2140696156 self._execution_time = execution_time + self._backfill_models = backfill_models self._end = end or default_end self._apply = apply @@ -172,6 +182,26 @@ def is_start_and_end_allowed(self) -> bool: """Indicates whether this plan allows to set the start and end dates.""" return self._is_dev or bool(self._restate_models) + @property + def start(self) -> t.Optional[TimeLike]: + if self._start and is_relative(self._start): + # only do this for relative expressions otherwise inclusive date strings like '2020-01-01' can be turned into exclusive timestamps eg '2020-01-01 00:00:00' + return to_datetime(self._start, relative_base=to_datetime(self.execution_time)) + return self._start + + @property + def end(self) -> t.Optional[TimeLike]: + if self._end and is_relative(self._end): + # only do this for relative expressions otherwise inclusive date strings like '2020-01-01' can be turned into exclusive timestamps eg '2020-01-01 00:00:00' + return to_datetime(self._end, relative_base=to_datetime(self.execution_time)) + return self._end + + @cached_property + def execution_time(self) -> TimeLike: + # this is cached to return a stable value from now() in the places where the execution time matters for resolving relative date strings + # during the plan building process + return self._execution_time or now() + def set_start(self, new_start: TimeLike) -> PlanBuilder: self._start = new_start self.override_start = True @@ -256,7 +286,8 @@ def build(self) -> Plan: ) restatements = self._build_restatements( - dag, earliest_interval_start(self._context_diff.snapshots.values()) + dag, + earliest_interval_start(self._context_diff.snapshots.values(), self.execution_time), ) models_to_backfill = self._build_models_to_backfill(dag, restatements) @@ -266,11 +297,17 @@ def build(self) -> Plan: # model should be ignored. interval_end_per_model = None + # this deliberately uses the passed in self._execution_time and not self.execution_time cached property + # the reason is because that there can be a delay between the Plan being built and the Plan being actually run, + # so this ensures that an _execution_time of None can be propagated to the Plan and thus be re-resolved to + # the current timestamp of when the Plan is eventually run + plan_execution_time = self._execution_time + plan = Plan( context_diff=self._context_diff, plan_id=self._plan_id, - provided_start=self._start, - provided_end=self._end, + provided_start=self.start, + provided_end=self.end, is_dev=self._is_dev, skip_backfill=self._skip_backfill, empty_backfill=self._empty_backfill, @@ -289,7 +326,7 @@ def build(self) -> Plan: selected_models_to_backfill=self._backfill_models, models_to_backfill=models_to_backfill, effective_from=self._effective_from, - execution_time=self._execution_time, + execution_time=plan_execution_time, end_bounded=self._end_bounded, ensure_finalized_snapshots=self._ensure_finalized_snapshots, user_provided_flags=self._user_provided_flags, @@ -739,6 +776,18 @@ def _ensure_valid_date_range(self) -> None: "The start and end dates can't be set for a production plan without restatements." ) + if (start := self.start) and (end := self.end): + if to_datetime(start) > to_datetime(end): + raise PlanError( + f"Plan end date: '{time_like_to_str(end)}' must be after the plan start date: '{time_like_to_str(start)}'" + ) + + if end := self.end: + if to_datetime(end) > to_datetime(self.execution_time): + raise PlanError( + f"Plan end date: '{time_like_to_str(end)}' cannot be in the future (execution time: '{time_like_to_str(self.execution_time)}')" + ) + def _ensure_no_forward_only_revert(self) -> None: """Ensures that a previously superseded breaking / non-breaking snapshot is not being used again to replace an existing forward-only snapshot with the same version. diff --git a/sqlmesh/core/plan/definition.py b/sqlmesh/core/plan/definition.py index c0ef1323f2..f3fb088ebb 100644 --- a/sqlmesh/core/plan/definition.py +++ b/sqlmesh/core/plan/definition.py @@ -5,6 +5,7 @@ from datetime import datetime from enum import Enum from functools import cached_property +from pydantic import Field from sqlmesh.core.context_diff import ContextDiff from sqlmesh.core.environment import Environment, EnvironmentNamingInfo, EnvironmentStatements @@ -63,7 +64,7 @@ class Plan(PydanticModel, frozen=True): models_to_backfill: t.Optional[t.Set[str]] = None """All models that should be backfilled as part of this plan.""" effective_from: t.Optional[TimeLike] = None - execution_time: t.Optional[TimeLike] = None + execution_time_: t.Optional[TimeLike] = Field(default=None, alias="execution_time") user_provided_flags: t.Optional[t.Dict[str, UserProvidedFlags]] = None @@ -80,7 +81,12 @@ def start(self) -> TimeLike: @cached_property def end(self) -> TimeLike: - return self.provided_end or now() + return self.provided_end or self.execution_time + + @cached_property + def execution_time(self) -> TimeLike: + # note: property is cached so that it returns a consistent timestamp for now() + return self.execution_time_ or now() @property def previous_plan_id(self) -> t.Optional[str]: @@ -271,7 +277,7 @@ def to_evaluatable(self) -> EvaluatablePlan: @cached_property def _earliest_interval_start(self) -> datetime: - return earliest_interval_start(self.snapshots.values()) + return earliest_interval_start(self.snapshots.values(), self.execution_time) class EvaluatablePlan(PydanticModel): @@ -345,8 +351,10 @@ def format_intervals(self, unit: t.Optional[IntervalUnit] = None) -> str: return format_intervals(self.merged_intervals, unit) -def earliest_interval_start(snapshots: t.Collection[Snapshot]) -> datetime: - earliest_start = earliest_start_date(snapshots) +def earliest_interval_start( + snapshots: t.Collection[Snapshot], execution_time: t.Optional[TimeLike] = None +) -> datetime: + earliest_start = earliest_start_date(snapshots, relative_to=execution_time) earliest_interval_starts = [s.intervals[0][0] for s in snapshots if s.intervals] return ( min(earliest_start, to_datetime(min(earliest_interval_starts))) diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 22eed6f3c8..ba422bcdcb 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1995,7 +1995,12 @@ def earliest_start_date( start_date(snapshot, snapshots, cache=cache, relative_to=relative_to) for snapshot in snapshots.values() ) - return yesterday() + + relative_base = None + if relative_to is not None: + relative_base = to_datetime(relative_to) + + return yesterday(relative_base=relative_base) def start_date( diff --git a/sqlmesh/utils/date.py b/sqlmesh/utils/date.py index a241865fda..6c5787470e 100644 --- a/sqlmesh/utils/date.py +++ b/sqlmesh/utils/date.py @@ -87,34 +87,34 @@ def now_ds() -> str: return to_ds(now()) -def yesterday() -> datetime: +def yesterday(relative_base: t.Optional[datetime] = None) -> datetime: """ Yesterday utc datetime. Returns: A datetime object with tz utc representing yesterday's date """ - return to_datetime("yesterday") + return to_datetime("yesterday", relative_base=relative_base) -def yesterday_ds() -> str: +def yesterday_ds(relative_base: t.Optional[datetime] = None) -> str: """ Yesterday utc ds. Returns: Yesterday's ds string. """ - return to_ds("yesterday") + return to_ds("yesterday", relative_base=relative_base) -def yesterday_timestamp() -> int: +def yesterday_timestamp(relative_base: t.Optional[datetime] = None) -> int: """ Yesterday utc timestamp. Returns: UTC epoch millis timestamp of yesterday """ - return to_timestamp(yesterday()) + return to_timestamp(yesterday(relative_base=relative_base)) def to_timestamp( @@ -265,19 +265,19 @@ def date_dict( return kwargs -def to_ds(obj: TimeLike) -> str: +def to_ds(obj: TimeLike, relative_base: t.Optional[datetime] = None) -> str: """Converts a TimeLike object into YYYY-MM-DD formatted string.""" - return to_ts(obj)[0:10] + return to_ts(obj, relative_base=relative_base)[0:10] -def to_ts(obj: TimeLike) -> str: +def to_ts(obj: TimeLike, relative_base: t.Optional[datetime] = None) -> str: """Converts a TimeLike object into YYYY-MM-DD HH:MM:SS formatted string.""" - return to_datetime(obj).replace(tzinfo=None).isoformat(sep=" ") + return to_datetime(obj, relative_base=relative_base).replace(tzinfo=None).isoformat(sep=" ") -def to_tstz(obj: TimeLike) -> str: +def to_tstz(obj: TimeLike, relative_base: t.Optional[datetime] = None) -> str: """Converts a TimeLike object into YYYY-MM-DD HH:MM:SS+00:00 formatted string.""" - return to_datetime(obj).isoformat(sep=" ") + return to_datetime(obj, relative_base=relative_base).isoformat(sep=" ") def is_date(obj: TimeLike) -> bool: @@ -373,6 +373,16 @@ def is_categorical_relative_expression(expression: str) -> bool: return not any(k in TIME_UNITS for k in grain_kwargs) +def is_relative(value: TimeLike) -> bool: + """ + Tests a TimeLike object to see if it is a relative expression, eg '1 week ago' as opposed to an absolute timestamp + """ + if isinstance(value, str): + return is_categorical_relative_expression(value) + + return False + + def to_time_column( time_column: t.Union[TimeLike, exp.Null], time_column_type: exp.DataType, diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 204e675c66..e88184c2e9 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -49,6 +49,7 @@ make_inclusive_end, now, to_date, + to_datetime, to_timestamp, yesterday_ds, ) @@ -438,6 +439,99 @@ def test_plan_execution_time(): ) +def test_plan_execution_time_start_end(): + context = Context(config=Config()) + context.upsert_model( + load_sql_based_model( + parse( + """ + MODEL( + name db.x, + start '2020-01-01', + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds + ), + cron '@daily' + ); + + SELECT id, ds FROM (VALUES + ('1', '2020-01-01'), + ('2', '2021-01-01'), + ('3', '2022-01-01'), + ('4', '2023-01-01'), + ('5', '2024-01-01') + ) data(id, ds) + WHERE ds BETWEEN @start_ds AND @end_ds + """ + ) + ) + ) + + # prod plan - no fixed execution time so it defaults to now() and reads all the data + prod_plan = context.plan(auto_apply=True) + + assert len(prod_plan.new_snapshots) == 1 + + context.upsert_model( + load_sql_based_model( + parse( + """ + MODEL( + name db.x, + start '2020-01-01', + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds + ), + cron '@daily' + ); + + SELECT id, ds, 'changed' as a FROM (VALUES + ('1', '2020-01-01'), + ('2', '2021-01-01'), + ('3', '2022-01-01'), + ('4', '2023-01-01'), + ('5', '2024-01-01') + ) data(id, ds) + WHERE ds BETWEEN @start_ds AND @end_ds + """ + ) + ) + ) + + # dev plan with an execution time in the past and no explicit start/end specified + # the plan end should be bounded to it and not exceed it even though in prod the last interval (used as a default end) + # is newer than the execution time + dev_plan = context.plan("dev", execution_time="2020-01-05") + + assert to_datetime(dev_plan.start) == to_datetime( + "2020-01-01" + ) # default start is the earliest prod interval + assert to_datetime(dev_plan.execution_time) == to_datetime("2020-01-05") + assert to_datetime(dev_plan.end) == to_datetime( + "2020-01-05" + ) # end should not be greater than execution_time + + # same as above but with a relative start + dev_plan = context.plan("dev", start="1 day ago", execution_time="2020-01-05") + + assert to_datetime(dev_plan.start) == to_datetime( + "2020-01-04" + ) # start relative to execution_time + assert to_datetime(dev_plan.execution_time) == to_datetime("2020-01-05") + assert to_datetime(dev_plan.end) == to_datetime( + "2020-01-05" + ) # end should not be greater than execution_time + + # same as above but with a relative start and a relative end + dev_plan = context.plan("dev", start="2 days ago", execution_time="2020-01-05", end="1 day ago") + + assert to_datetime(dev_plan.start) == to_datetime( + "2020-01-03" + ) # start relative to execution_time + assert to_datetime(dev_plan.execution_time) == to_datetime("2020-01-05") + assert to_datetime(dev_plan.end) == to_datetime("2020-01-04") # end relative to execution_time + + def test_override_builtin_audit_blocking_mode(): context = Context(config=Config()) context.upsert_model( diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 540a1384e2..61ac2b4ba6 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -3095,3 +3095,111 @@ def test_user_provided_flags(sushi_context: Context): context_diff, ).build() assert plan_builder.user_provided_flags == None + + +@time_machine.travel(now()) +@pytest.mark.parametrize( + "input,output", + [ + # execution_time, start, end + ( + # no execution time, start or end + (None, None, None), + # execution time defaults to now() + # start defaults to 1 day before execution time + # end defaults to execution_time + (now(), yesterday_ds(), now()), + ), + ( + # fixed execution time, no start, no end + ("2020-01-05", None, None), + # execution time set to 2020-01-05 + # start defaults to 1 day before execution time + # end defaults to execution time + ("2020-01-05", "2020-01-04", "2020-01-05"), + ), + ( + # fixed execution time, relative start, no end + ("2020-01-05", "2 days ago", None), + # execution time set to 2020-01-05 + # start relative to execution time + # end defaults to execution time + ("2020-01-05", "2020-01-03", "2020-01-05"), + ), + ( + # fixed execution time, relative start, relative end + ("2020-01-05", "2 days ago", "1 day ago"), + # execution time set to 2020-01-05 + # start relative to execution time + # end relative to execution time + ("2020-01-05", "2020-01-03", "2020-01-04"), + ), + ( + # fixed execution time, fixed start, fixed end + ("2020-01-05", "2020-01-01", "2020-01-05"), + # fixed dates are all in the valid range + ("2020-01-05", "2020-01-01", "2020-01-05"), + ), + ( + # fixed execution time, fixed start, fixed end + ("2020-01-05", "2020-01-05", "2020-01-01"), + # Error because start is after end + r"Plan end date.*must be after the plan start date", + ), + ( + # fixed execution time, relative start, fixed end beyond fixed execution time + ("2020-01-05", "2 days ago", "2021-01-01"), + # Error because end is set to 2021-01-01 which is after the execution time + r"Plan end date.*cannot be in the future", + ), + ], +) +def test_plan_dates_relative_to_execution_time( + input: t.Tuple[t.Optional[str], ...], + output: t.Union[str, t.Tuple[t.Optional[str], ...]], + make_snapshot: t.Callable, +): + snapshot_a = make_snapshot( + SqlModel(name="a", query=parse_one("select 1, ds"), dialect="duckdb") + ) + + context_diff = ContextDiff( + environment="test_environment", + is_new_environment=True, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added={snapshot_a.snapshot_id}, + removed_snapshots={}, + modified_snapshots={}, + snapshots={}, + new_snapshots={snapshot_a.snapshot_id: snapshot_a}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + ) + + input_execution_time, input_start, input_end = input + + def _build_plan() -> Plan: + return PlanBuilder( + context_diff, + start=input_start, + end=input_end, + execution_time=input_execution_time, + is_dev=True, + ).build() + + if isinstance(output, str): + with pytest.raises(PlanError, match=output): + _build_plan() + else: + output_execution_time, output_start, output_end = output + + plan = _build_plan() + assert to_datetime(plan.start) == to_datetime(output_start) + assert to_datetime(plan.end) == to_datetime(output_end) + assert to_datetime(plan.execution_time) == to_datetime(output_execution_time) diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index 208f59b105..8b0a35411a 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -19,6 +19,7 @@ GithubCheckStatus, MergeStateStatus, ) +from sqlmesh.utils.date import to_datetime, now from tests.integrations.github.cicd.conftest import MockIssueComment pytestmark = pytest.mark.github @@ -236,6 +237,7 @@ def test_pr_plan(github_client, make_controller): def test_pr_plan_auto_categorization(github_client, make_controller): custom_categorizer_config = CategorizerConfig.all_semi() default_start = "1 week ago" + default_start_absolute = to_datetime(default_start, relative_base=now()) controller = make_controller( "tests/fixtures/github/pull_request_synchronized.json", github_client, @@ -249,7 +251,7 @@ def test_pr_plan_auto_categorization(github_client, make_controller): assert not controller._context.apply.called assert controller._context._run_plan_tests.call_args == call(skip_tests=True) assert controller._pr_plan_builder._categorizer_config == custom_categorizer_config - assert controller.pr_plan.start == default_start + assert controller.pr_plan.start == default_start_absolute def test_prod_plan(github_client, make_controller): diff --git a/tests/utils/test_date.py b/tests/utils/test_date.py index 03fb6e580c..d892817969 100644 --- a/tests/utils/test_date.py +++ b/tests/utils/test_date.py @@ -12,6 +12,7 @@ date_dict, format_tz_datetime, is_categorical_relative_expression, + is_relative, make_inclusive, to_datetime, to_time_column, @@ -324,3 +325,17 @@ def test_format_tz_datetime(): test_datetime = to_datetime("2020-01-01 00:00:00") assert format_tz_datetime(test_datetime) == "2020-01-01 12:00AM UTC" assert format_tz_datetime(test_datetime, format_string=None) == "2020-01-01 00:00:00+00:00" + + +def test_is_relative(): + assert is_relative("1 week ago") + assert is_relative("1 week") + assert is_relative("1 day ago") + assert is_relative("yesterday") + + assert not is_relative("2024-01-01") + assert not is_relative("2024-01-01 01:02:03") + assert not is_relative(to_datetime("2024-01-01 01:02:03")) + assert not is_relative(to_timestamp("2024-01-01 01:02:03")) + assert not is_relative(to_datetime("1 week ago")) + assert not is_relative(to_timestamp("1 day ago")) From 6afa17cce3dd1400332510a8484e35a77d2e0e21 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 13 Jun 2025 11:53:30 +0100 Subject: [PATCH 0394/1056] chore: allow debugging vscode tests (#4731) --- tooling/vscode/settings.json | 12 +++++++++++- vscode/extension/tests/utils.ts | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tooling/vscode/settings.json b/tooling/vscode/settings.json index 2c1f18f540..6cdfa704c7 100644 --- a/tooling/vscode/settings.json +++ b/tooling/vscode/settings.json @@ -13,5 +13,15 @@ "vscode/react/dist": true }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" + "typescript.tsc.autoDetect": "off", + // Playwright configuration + // Python configuration to ensure consistent environment + "python.defaultInterpreterPath": "${env:VIRTUAL_ENV}/bin/python", + "python.terminal.activateEnvironment": true, + "terminal.integrated.env.osx": { + "PATH": "${env:VIRTUAL_ENV}/bin:${env:PATH}" + }, + "terminal.integrated.env.linux": { + "PATH": "${env:VIRTUAL_ENV}/bin:${env:PATH}" + } } diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index 34301ba4f2..1a2c55e1e6 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -5,7 +5,7 @@ import { _electron as electron, Page } from '@playwright/test' // Absolute path to the VS Code executable you downloaded in step 1. export const VS_CODE_EXE = fs.readJsonSync( - '.vscode-test/paths.json', + path.join(__dirname, '..', '.vscode-test', 'paths.json'), ).executablePath // Where your extension lives on disk export const EXT_PATH = path.resolve(__dirname, '..') From 6e90e3e1ea6537bc40abca7e35c9e4843c2bcd52 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 13 Jun 2025 19:20:20 +0300 Subject: [PATCH 0395/1056] Chore: bump sqlglot to 26.27.1 (#4733) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 93e034de57..f2bb2f683e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.27.0", + "sqlglot[rs]~=26.27.1", "tenacity", "time-machine", "json-stream" From 23083f9fed2925264f94f8f647949fc2ed2a2a1a Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sat, 14 Jun 2025 00:55:39 +0100 Subject: [PATCH 0396/1056] fix(vscode): improve the tcloud stability (#4734) --- sqlmesh/lsp/main.py | 38 +-- tooling/vscode/extensions.json | 3 +- .../src/utilities/sqlmesh/sqlmesh.ts | 84 +++--- vscode/extension/tests/tcloud.spec.ts | 215 +++++++++++++++ vscode/extension/tests/tcloud/README.md | 53 ++++ .../tests/tcloud/mock_tcloud/__init__.py | 1 + .../extension/tests/tcloud/mock_tcloud/cli.py | 257 ++++++++++++++++++ vscode/extension/tests/tcloud/pyproject.toml | 18 ++ vscode/extension/tests/utils.ts | 1 + 9 files changed, 606 insertions(+), 64 deletions(-) create mode 100644 vscode/extension/tests/tcloud.spec.ts create mode 100644 vscode/extension/tests/tcloud/README.md create mode 100644 vscode/extension/tests/tcloud/mock_tcloud/__init__.py create mode 100755 vscode/extension/tests/tcloud/mock_tcloud/cli.py create mode 100644 vscode/extension/tests/tcloud/pyproject.toml diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index f9bfe46114..641b570c84 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -294,32 +294,6 @@ def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> Non SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), ) - @self.server.feature(types.TEXT_DOCUMENT_DID_CHANGE) - def did_change(ls: LanguageServer, params: types.DidChangeTextDocumentParams) -> None: - if self.lsp_context is None: - current_path = Path.cwd() - self._ensure_context_in_folder(current_path) - if self.lsp_context is None: - ls.log_trace("No context found after did_change") - return - - uri = URI(params.text_document.uri) - context = self._context_get_or_load(uri) - - models = context.map[uri.to_path()] - if models is None or not isinstance(models, ModelTarget): - return - - # Get diagnostics from context (which handles caching) - diagnostics = context.lint_model(uri) - - # Only publish diagnostics if client doesn't support pull diagnostics - if not self.client_supports_pull_diagnostics: - ls.publish_diagnostics( - params.text_document.uri, - SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), - ) - @self.server.feature(types.TEXT_DOCUMENT_DID_SAVE) def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> None: uri = URI(params.text_document.uri) @@ -753,15 +727,19 @@ def _ensure_context_for_document( loaded = False # Ascend directories to look for config - while path.parents and not loaded: + current_dir = path.parent # Start from the file's parent directory + while current_dir.parents and not loaded: for ext in ("py", "yml", "yaml"): - config_path = path / f"config.{ext}" + config_path = current_dir / f"config.{ext}" if config_path.exists(): - if self._create_lsp_context([path]): + if self._create_lsp_context([current_dir]): loaded = True # Re-check context for the document now that it's loaded return self._ensure_context_for_document(document_uri) - path = path.parent + # Check if we've reached the filesystem root to prevent infinite loops + if current_dir == current_dir.parent: + break + current_dir = current_dir.parent # If still no context found, try the workspace folders if not loaded: diff --git a/tooling/vscode/extensions.json b/tooling/vscode/extensions.json index 0271570408..b703cc6e84 100644 --- a/tooling/vscode/extensions.json +++ b/tooling/vscode/extensions.json @@ -5,6 +5,7 @@ "dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher", "ms-vscode.extension-test-runner", - "ms-playwright.playwright" + "ms-playwright.playwright", + "esbenp.prettier-vscode" ] } diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 95cc94d38e..45d9cfbd4c 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -147,51 +147,69 @@ export const installSqlmeshEnterprise = async ( return ok(true) } +let installationLock: Promise> | undefined = undefined + /** * Checks if sqlmesh enterprise is installed and updated. If not, it will install it. * This will also create a progress message in vscode in order to inform the user that sqlmesh enterprise is being installed. + * Uses a lock mechanism to prevent parallel executions. * * @returns A Result indicating whether sqlmesh enterprise was installed in the call. */ export const ensureSqlmeshEnterpriseInstalled = async (): Promise< Result > => { - traceInfo('Ensuring sqlmesh enterprise is installed') - const isInstalled = await isSqlmeshEnterpriseInstalled() - if (isErr(isInstalled)) { - return isInstalled - } - if (isInstalled.value) { - traceInfo('Sqlmesh enterprise is installed') - return ok(false) + // If there's an ongoing installation, wait for it to complete + if (installationLock) { + return installationLock } - traceInfo('Sqlmesh enterprise is not installed, installing...') - const abortController = new AbortController() - const installResult = await window.withProgress( - { - location: ProgressLocation.Notification, - title: 'Installing sqlmesh enterprise...', - cancellable: true, - }, - async (progress, token) => { - // Connect the cancellation token to our abort controller - token.onCancellationRequested(() => { - abortController.abort() - traceInfo('Sqlmesh enterprise installation cancelled') - window.showInformationMessage('Installation cancelled') - }) - progress.report({ message: 'Installing sqlmesh enterprise...' }) - const result = await installSqlmeshEnterprise(abortController) - if (isErr(result)) { - return result + + // Create a new lock + installationLock = (async () => { + try { + traceInfo('Ensuring sqlmesh enterprise is installed') + const isInstalled = await isSqlmeshEnterpriseInstalled() + if (isErr(isInstalled)) { + return isInstalled + } + if (isInstalled.value) { + traceInfo('Sqlmesh enterprise is installed') + return ok(false) + } + traceInfo('Sqlmesh enterprise is not installed, installing...') + const abortController = new AbortController() + const installResult = await window.withProgress( + { + location: ProgressLocation.Notification, + title: 'SQLMesh', + cancellable: true, + }, + async (progress, token) => { + // Connect the cancellation token to our abort controller + token.onCancellationRequested(() => { + abortController.abort() + traceInfo('Sqlmesh enterprise installation cancelled') + window.showInformationMessage('Installation cancelled') + }) + progress.report({ message: 'Installing enterprise python package...' }) + const result = await installSqlmeshEnterprise(abortController) + if (isErr(result)) { + return result + } + return ok(true) + }, + ) + if (isErr(installResult)) { + return installResult } return ok(true) - }, - ) - if (isErr(installResult)) { - return installResult - } - return ok(true) + } finally { + // Clear the lock when done + installationLock = undefined + } + })() + + return installationLock } /** diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts new file mode 100644 index 0000000000..3a99288546 --- /dev/null +++ b/vscode/extension/tests/tcloud.spec.ts @@ -0,0 +1,215 @@ +import { expect, test } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { exec } from 'child_process' +import { promisify } from 'util' +import { REPO_ROOT, startVSCode, SUSHI_SOURCE_PATH } from './utils' + +const execAsync = promisify(exec) + +/** + * Helper function to create and set up a Python virtual environment + */ +async function setupPythonEnvironment(envDir: string): Promise { + // Create virtual environment + const pythonCmd = process.platform === 'win32' ? 'python' : 'python3' + const { stderr } = await execAsync(`${pythonCmd} -m venv "${envDir}"`) + if (stderr && !stderr.includes('WARNING')) { + throw new Error(`Failed to create venv: ${stderr}`) + } + + // Get paths + const isWindows = process.platform === 'win32' + const binDir = path.join(envDir, isWindows ? 'Scripts' : 'bin') + const pythonPath = path.join(binDir, isWindows ? 'python.exe' : 'python') + const pipPath = path.join(binDir, isWindows ? 'pip.exe' : 'pip') + + // Install the mock tcloud package + const mockTcloudPath = path.join(__dirname, 'tcloud') + const { stderr: pipErr1 } = await execAsync( + `"${pipPath}" install -e "${mockTcloudPath}"`, + ) + if (pipErr1 && !pipErr1.includes('WARNING') && !pipErr1.includes('notice')) { + throw new Error(`Failed to install mock tcloud: ${pipErr1}`) + } + + // Install sqlmesh from the local repository with LSP support + const sqlmeshRepoPath = path.join(__dirname, '..', '..', '..') // Navigate to repo root from tests dir + const { stderr: pipErr2 } = await execAsync( + `"${pipPath}" install -e "${sqlmeshRepoPath}[lsp,bigquery]" "${REPO_ROOT}/examples/custom_materializations"`, + ) + if (pipErr2 && !pipErr2.includes('WARNING') && !pipErr2.includes('notice')) { + throw new Error(`Failed to install sqlmesh: ${pipErr2}`) + } + + return pythonPath +} + +/** + * Helper function to set up a pre-authenticated tcloud state + */ +async function setupAuthenticatedState(tempDir: string): Promise { + const authStateFile = path.join(tempDir, '.tcloud_auth_state.json') + const authState = { + is_logged_in: true, + id_token: { + iss: 'https://mock.tobikodata.com', + aud: 'mock-audience', + sub: 'user-123', + scope: 'openid email profile', + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 3600, // Valid for 1 hour + email: 'test@example.com', + name: 'Test User', + }, + } + await fs.writeJson(authStateFile, authState) +} + +test.describe('Tcloud', () => { + test('not signed in, shows sign in window', async ({}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Create a tcloud.yaml to mark this as a tcloud project + const tcloudConfig = { + url: 'https://mock.tobikodata.com', + org: 'test-org', + project: 'test-project', + } + await fs.writeFile( + path.join(tempDir, 'tcloud.yaml'), + `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, + ) + + // Set up Python environment with mock tcloud and sqlmesh + const pythonPath = await setupPythonEnvironment(pythonEnvDir) + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson( + path.join(tempDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) + + // Start VS Code + const { window, close } = await startVSCode(tempDir) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Wait for the file to open + await window.waitForTimeout(2000) + + await window.waitForSelector( + 'text=Please sign in to Tobiko Cloud to use SQLMesh', + ) + + // Close VS Code + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } + }) + + test('signed in and not installed shows installation window and can see output', async ({}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Create a tcloud.yaml to mark this as a tcloud project + const tcloudConfig = { + url: 'https://mock.tobikodata.com', + org: 'test-org', + project: 'test-project', + } + await fs.writeFile( + path.join(tempDir, 'tcloud.yaml'), + `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, + ) + + // Write mock ".tcloud_auth_state.json" file + await setupAuthenticatedState(tempDir) + + // Set up Python environment with mock tcloud and sqlmesh + const pythonPath = await setupPythonEnvironment(pythonEnvDir) + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson( + path.join(tempDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) + + // Start VS Code + const { window, close } = await startVSCode(tempDir) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=Installing enterprise python package') + expect( + await window.locator('text=Installing enterprise python package'), + ).toHaveCount(2) + + await window.waitForSelector('text=Loaded SQLMesh context') + + // Close VS Code + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } + }) +}) diff --git a/vscode/extension/tests/tcloud/README.md b/vscode/extension/tests/tcloud/README.md new file mode 100644 index 0000000000..c3c723be37 --- /dev/null +++ b/vscode/extension/tests/tcloud/README.md @@ -0,0 +1,53 @@ +# Mock tcloud CLI for Testing + +This directory contains a mock implementation of the tcloud CLI for testing the VSCode extension. + +## Implemented Commands + +The mock implements only the commands used by the VSCode extension: + +### Authentication Commands +- `tcloud auth vscode status` - Returns authentication status +- `tcloud auth vscode login-url` - Returns mock OAuth login URL +- `tcloud auth vscode start-server ` - Simulates OAuth callback +- `tcloud auth vscode device` - Returns mock device flow info +- `tcloud auth vscode poll_device ` - Simulates device flow success +- `tcloud auth logout` - Clears authentication state + +### SQLMesh Commands +- `tcloud is_sqlmesh_installed` - Checks installation status +- `tcloud install_sqlmesh` - Marks SQLMesh as installed +- `tcloud sqlmesh ` - Echoes sqlmesh commands + +## State Management + +The mock maintains state in two files: +- `.tcloud_auth_state.json` - Authentication state (logged in/out, ID token) +- `.sqlmesh_installed` - SQLMesh installation marker + +## Usage in Tests + +To use this mock in tests: + +1. Ensure the mock is in PATH or reference it directly +2. The mock will simulate successful authentication flows +3. State persists between calls for realistic testing + +## Example + +```bash +# Check auth status +./tcloud auth vscode status +# Output: {"is_logged_in": false, "id_token": null} + +# Simulate login +./tcloud auth vscode login-url +# Output: {"url": "https://mock-auth.example.com/auth?client_id=mock&redirect_uri=http://localhost:7890", "verifier_code": "mock_verifier_12345"} + +./tcloud auth vscode start-server mock_verifier_12345 +# Output: Mock server started successfully + +# Check status again +./tcloud auth vscode status +# Output: {"is_logged_in": true, "id_token": {"email": "test@example.com", "name": "Test User", "exp": 1736790123}} +``` \ No newline at end of file diff --git a/vscode/extension/tests/tcloud/mock_tcloud/__init__.py b/vscode/extension/tests/tcloud/mock_tcloud/__init__.py new file mode 100644 index 0000000000..98ad152e0e --- /dev/null +++ b/vscode/extension/tests/tcloud/mock_tcloud/__init__.py @@ -0,0 +1 @@ +# Mock tcloud package \ No newline at end of file diff --git a/vscode/extension/tests/tcloud/mock_tcloud/cli.py b/vscode/extension/tests/tcloud/mock_tcloud/cli.py new file mode 100755 index 0000000000..b9b80ab8f1 --- /dev/null +++ b/vscode/extension/tests/tcloud/mock_tcloud/cli.py @@ -0,0 +1,257 @@ +""" +Mock tcloud CLI for testing VSCode extension. +Implements only the commands used by the extension. +""" + +import json +import os +import subprocess +import sys +import time +from pathlib import Path + +import click + +def get_auth_state_file(): + """Get the path to the auth state file in the current working directory""" + return Path.cwd() / ".tcloud_auth_state.json" + + +def load_auth_state(): + """Load authentication state from file""" + auth_file = get_auth_state_file() + if auth_file.exists(): + with open(auth_file, "r") as f: + return json.load(f) + return {"is_logged_in": False, "id_token": None} + + +def save_auth_state(state): + """Save authentication state to file""" + auth_file = get_auth_state_file() + with open(auth_file, "w") as f: + json.dump(state, f) + + +@click.group(no_args_is_help=True) +@click.option( + "--project", + type=str, + help="The name of the project.", +) +@click.pass_context +def cli(ctx: click.Context, project: str) -> None: + """Mock Tobiko Cloud CLI""" + ctx.ensure_object(dict) + ctx.obj["project"] = project + + +@cli.command("is_sqlmesh_installed", hidden=True) +@click.pass_context +def is_sqlmesh_installed(ctx: click.Context) -> None: + """Check if SQLMesh Enterprise is installed""" + # For testing, we'll track installation state in a file in the current bin directory + # This matches where the test expects it to be + bin_dir = Path(sys.executable).parent + install_state_file = bin_dir / ".sqlmesh_installed" + is_installed = install_state_file.exists() + + print( + json.dumps( + { + "is_installed": is_installed, + } + ) + ) + + +@cli.command("install_sqlmesh") +@click.pass_context +def install_sqlmesh(ctx: click.Context) -> None: + """Install the correct version of SQLMesh Enterprise""" + + # For 3 seconds output to stdout + for i in range(3): + print(f"Installing SQLMesh Enterprise logs {i + 1}/3", flush=True) + time.sleep(1) + + # Simulate installation by creating a marker file in the bin directory + bin_dir = Path(sys.executable).parent + install_state_file = bin_dir / ".sqlmesh_installed" + install_state_file.touch() + + print("Mock SQLMesh Enterprise installed successfully") + + +@cli.command("sqlmesh") +@click.argument("args", nargs=-1) +@click.pass_context +def sqlmesh(ctx: click.Context, args) -> None: + """Run SQLMesh Enterprise commands""" + # Pass through to the real sqlmesh command + + # Get the path to sqlmesh in the same environment as this script + bin_dir = os.path.dirname(sys.executable) + sqlmesh_path = os.path.join(bin_dir, "sqlmesh") + + if not os.path.exists(sqlmesh_path): + # Try with .exe extension on Windows + sqlmesh_path = os.path.join(bin_dir, "sqlmesh.exe") + + if not os.path.exists(sqlmesh_path): + # Fall back to using sqlmesh from PATH + sqlmesh_path = "sqlmesh" + + # Execute the real sqlmesh with the provided arguments + result = subprocess.run([sqlmesh_path] + list(args), capture_output=False) + sys.exit(result.returncode) + + +@click.group() +def auth() -> None: + """ + Tobiko Cloud Authentication + """ + + +@auth.command() +def logout() -> None: + """Logout of any current session""" + save_auth_state({"is_logged_in": False, "id_token": None}) + print("Logged out successfully") + + +### Methods for VSCode +@auth.group(hidden=True) +def vscode() -> None: + """Commands for VSCode integration""" + pass + + +@vscode.command("login-url") +def login_url() -> None: + """ + Login to Tobiko Cloud. + + This returns a JSON object with the following fields: + - url: The URL to login open + """ + # Return mock OAuth URL and verifier + print( + json.dumps( + { + "url": "https://mock-auth.example.com/auth?client_id=mock&redirect_uri=http://localhost:7890", + "verifier_code": "mock_verifier_12345", + } + ) + ) + + +@vscode.command("start-server") +@click.argument("code_verifier", type=str, required=True) +def start_server(code_verifier: str) -> None: + """ + Start the server to catch the redirect from the browser. + """ + # Simulate successful authentication after a short delay + time.sleep(0.5) + + # Update auth state to logged in + save_auth_state( + { + "is_logged_in": True, + "id_token": { + "iss": "https://mock.tobikodata.com", + "aud": "mock-audience", + "sub": "user-123", + "scope": "openid email profile", + "iat": int(time.time()), + "exp": int(time.time()) + 3600, # Token expires in 1 hour + "email": "test@example.com", + "name": "Test User", + }, + } + ) + + # The real command would start a server, but for testing we just simulate success + print("Mock server started successfully") + + +@vscode.command("status") +def vscode_status() -> None: + """ + Auth status for logged in + """ + state = load_auth_state() + print( + json.dumps( + {"is_logged_in": state["is_logged_in"], "id_token": state["id_token"]} + ) + ) + + +@vscode.command("device") +def vscode_device() -> None: + """ + Initiate device flow for VSCode integration + """ + print( + json.dumps( + { + "device_code": "MOCK-DEVICE-CODE", + "user_code": "ABCD-1234", + "verification_uri": "https://mock-auth.example.com/device", + "verification_uri_complete": "https://mock-auth.example.com/device?user_code=ABCD-1234", + "expires_in": 600, + "interval": 5, + } + ) + ) + + +@vscode.command("poll_device") +@click.argument("device_code", type=str, required=True) +@click.option( + "-i", + "--interval", + type=int, + default=5, + help="The interval between polling attempts in seconds", +) +@click.option( + "-t", + "--timeout", + type=int, + default=300, + help="The timeout for the device flow in seconds", +) +def vscode_poll_device(device_code: str, interval: int, timeout: int) -> None: + """ + Poll the device flow for VSCode integration + """ + # For testing, we'll just succeed immediately + save_auth_state( + { + "is_logged_in": True, + "id_token": { + "iss": "https://mock.tobikodata.com", + "aud": "mock-audience", + "sub": "device-user-123", + "scope": "openid email profile", + "iat": int(time.time()), + "exp": int(time.time()) + 3600, + "email": "device@example.com", + "name": "Device User", + }, + } + ) + + print(json.dumps({"success": True})) + + +# Add auth group to main CLI +cli.add_command(auth) + + +if __name__ == "__main__": + cli() diff --git a/vscode/extension/tests/tcloud/pyproject.toml b/vscode/extension/tests/tcloud/pyproject.toml new file mode 100644 index 0000000000..505af5c783 --- /dev/null +++ b/vscode/extension/tests/tcloud/pyproject.toml @@ -0,0 +1,18 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "mock-tcloud" +version = "0.1.0" +description = "Mock tcloud CLI for testing VSCode extension" +requires-python = ">=3.8" +dependencies = [ + "click>=8.0", +] + +[project.scripts] +tcloud = "mock_tcloud.cli:cli" + +[tool.setuptools] +packages = ["mock_tcloud"] \ No newline at end of file diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index 1a2c55e1e6..ff68c7291f 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -18,6 +18,7 @@ export const SUSHI_SOURCE_PATH = path.join( 'examples', 'sushi', ) +export const REPO_ROOT = path.join(__dirname, '..', '..', '..') /** * Launch VS Code and return the window and a function to close the app. From b9d5e8f2140adebd6c6e30b0979922ebea63b155 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sat, 14 Jun 2025 12:17:42 +0100 Subject: [PATCH 0397/1056] chore: add lint rule that catches breakpoint (#4737) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f2bb2f683e..ea20c21e74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -273,6 +273,7 @@ retry_delay = 10 select = [ "F401", "RET505", + "T100", ] extend-select = ["TID"] From abf923941d57b332e4deb10fa2e0e3d2560917cd Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:26:59 +0100 Subject: [PATCH 0398/1056] chore(vscode): add test for non installation of deps' (#4735) --- vscode/extension/tests/no_lsp.spec.ts | 74 +++++++++++++++++++++++++++ vscode/extension/tests/tcloud.spec.ts | 45 ++++++---------- vscode/extension/tests/utils.ts | 50 ++++++++++++++++++ 3 files changed, 140 insertions(+), 29 deletions(-) create mode 100644 vscode/extension/tests/no_lsp.spec.ts diff --git a/vscode/extension/tests/no_lsp.spec.ts b/vscode/extension/tests/no_lsp.spec.ts new file mode 100644 index 0000000000..b0aa6323b3 --- /dev/null +++ b/vscode/extension/tests/no_lsp.spec.ts @@ -0,0 +1,74 @@ +import { expect, test } from '@playwright/test' +import fs from 'fs-extra' +import os from 'os' +import path from 'path' +import { + createVirtualEnvironment, + pipInstall, + REPO_ROOT, + startVSCode, + SUSHI_SOURCE_PATH, +} from './utils' + +test('missing LSP dependencies shows install prompt', async ({}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + const pythonDetails = await createVirtualEnvironment(pythonEnvDir) + const custom_materializations = path.join( + REPO_ROOT, + 'examples', + 'custom_materializations', + ) + const sqlmeshWithExtras = `${REPO_ROOT}[bigquery]` + await pipInstall(pythonDetails, [sqlmeshWithExtras, custom_materializations]) + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonDetails.pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson( + path.join(tempDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) + + // Start VS Code + const { window, close } = await startVSCode(tempDir) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Wait for the message to show that LSP extras need to be installed + await window.waitForSelector('text=LSP dependencies missing') + expect(await window.locator('text=Install').count()).toBeGreaterThanOrEqual( + 1, + ) + + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } +}) diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index 3a99288546..d047a9dcd8 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -2,48 +2,35 @@ import { expect, test } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { exec } from 'child_process' -import { promisify } from 'util' -import { REPO_ROOT, startVSCode, SUSHI_SOURCE_PATH } from './utils' - -const execAsync = promisify(exec) +import { + createVirtualEnvironment, + pipInstall, + REPO_ROOT, + startVSCode, + SUSHI_SOURCE_PATH, +} from './utils' /** * Helper function to create and set up a Python virtual environment */ async function setupPythonEnvironment(envDir: string): Promise { // Create virtual environment - const pythonCmd = process.platform === 'win32' ? 'python' : 'python3' - const { stderr } = await execAsync(`${pythonCmd} -m venv "${envDir}"`) - if (stderr && !stderr.includes('WARNING')) { - throw new Error(`Failed to create venv: ${stderr}`) - } - - // Get paths - const isWindows = process.platform === 'win32' - const binDir = path.join(envDir, isWindows ? 'Scripts' : 'bin') - const pythonPath = path.join(binDir, isWindows ? 'python.exe' : 'python') - const pipPath = path.join(binDir, isWindows ? 'pip.exe' : 'pip') + const pythonDetails = await createVirtualEnvironment(envDir) // Install the mock tcloud package const mockTcloudPath = path.join(__dirname, 'tcloud') - const { stderr: pipErr1 } = await execAsync( - `"${pipPath}" install -e "${mockTcloudPath}"`, - ) - if (pipErr1 && !pipErr1.includes('WARNING') && !pipErr1.includes('notice')) { - throw new Error(`Failed to install mock tcloud: ${pipErr1}`) - } + await pipInstall(pythonDetails, [mockTcloudPath]) // Install sqlmesh from the local repository with LSP support - const sqlmeshRepoPath = path.join(__dirname, '..', '..', '..') // Navigate to repo root from tests dir - const { stderr: pipErr2 } = await execAsync( - `"${pipPath}" install -e "${sqlmeshRepoPath}[lsp,bigquery]" "${REPO_ROOT}/examples/custom_materializations"`, + const customMaterializations = path.join( + REPO_ROOT, + 'examples', + 'custom_materializations', ) - if (pipErr2 && !pipErr2.includes('WARNING') && !pipErr2.includes('notice')) { - throw new Error(`Failed to install sqlmesh: ${pipErr2}`) - } + const sqlmeshWithExtras = `${REPO_ROOT}[lsp,bigquery]` + await pipInstall(pythonDetails, [sqlmeshWithExtras, customMaterializations]) - return pythonPath + return pythonDetails.pythonPath } /** diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index ff68c7291f..8150c43c59 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -2,6 +2,8 @@ import path from 'path' import fs from 'fs-extra' import os from 'os' import { _electron as electron, Page } from '@playwright/test' +import { exec } from 'child_process' +import { promisify } from 'util' // Absolute path to the VS Code executable you downloaded in step 1. export const VS_CODE_EXE = fs.readJsonSync( @@ -87,3 +89,51 @@ export const clickExplorerTab = async (page: Page): Promise => { await page.locator("text='Explorer'").waitFor({ state: 'visible' }) } } + +const execAsync = promisify(exec) + +export interface PythonEnvironment { + pythonPath: string + pipPath: string +} + +/** + * Create a virtual environment in the given directory. + * @param venvDir The directory to create the virtual environment in. + */ +export const createVirtualEnvironment = async ( + venvDir: string, +): Promise => { + const pythonCmd = process.platform === 'win32' ? 'python' : 'python3' + const { stderr } = await execAsync(`${pythonCmd} -m venv "${venvDir}"`) + if (stderr && !stderr.includes('WARNING')) { + throw new Error(`Failed to create venv: ${stderr}`) + } + // Get paths + const isWindows = process.platform === 'win32' + const binDir = path.join(venvDir, isWindows ? 'Scripts' : 'bin') + const pythonPath = path.join(binDir, isWindows ? 'python.exe' : 'python') + const pipPath = path.join(binDir, isWindows ? 'pip.exe' : 'pip') + + return { + pythonPath, + pipPath, + } +} + +/** + * Install packages in the given virtual environment. + * @param pythonDetails The Python environment to use. + * @param packagePaths The paths to the packages to install (string[]). + */ +export const pipInstall = async ( + pythonDetails: PythonEnvironment, + packagePaths: string[], +): Promise => { + const { pipPath } = pythonDetails + const execString = `"${pipPath}" install -e "${packagePaths.join('" -e "')}"` + const { stderr } = await execAsync(execString) + if (stderr && !stderr.includes('WARNING') && !stderr.includes('notice')) { + throw new Error(`Failed to install package: ${stderr}`) + } +} From 578d5abe92e7f5921080736c25abf8a6f08fa94e Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sat, 14 Jun 2025 16:54:59 +0100 Subject: [PATCH 0399/1056] chore: add ruff vscode extension recommendation (#4738) --- tooling/vscode/extensions.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tooling/vscode/extensions.json b/tooling/vscode/extensions.json index b703cc6e84..b9df8890ab 100644 --- a/tooling/vscode/extensions.json +++ b/tooling/vscode/extensions.json @@ -6,6 +6,7 @@ "amodio.tsl-problem-matcher", "ms-vscode.extension-test-runner", "ms-playwright.playwright", - "esbenp.prettier-vscode" + "esbenp.prettier-vscode", + "charliermarsh.ruff" ] } From a1b590399055df888a94d6507d6eb44e2301a4c7 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 15 Jun 2025 12:41:29 +0200 Subject: [PATCH 0400/1056] chore: dispose correctly in lineage panel (#4740) --- vscode/extension/src/webviews/lineagePanel.ts | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/vscode/extension/src/webviews/lineagePanel.ts b/vscode/extension/src/webviews/lineagePanel.ts index f9d84dc256..5bb3e1693f 100644 --- a/vscode/extension/src/webviews/lineagePanel.ts +++ b/vscode/extension/src/webviews/lineagePanel.ts @@ -16,37 +16,42 @@ export class LineagePanel implements WebviewViewProvider, Disposable { private panel: WebviewView | undefined private lsp: LSPClient - private _extensionUri: Uri + private readonly extensionUri: Uri + + private disposables: Disposable[] = [] public constructor(extensionUri: Uri, lsp: LSPClient) { - this._extensionUri = extensionUri + this.extensionUri = extensionUri this.lsp = lsp if (this.panel) { this.panel.webview.html = this.getHTML(this.panel.webview) } - workspace.onDidSaveTextDocument(document => { - this.panel?.webview.postMessage({ - key: 'vscode_send', - payload: { - key: 'savedFile', - payload: { fileUri: document.uri.toString() }, - }, - }) - }) - - window.onDidChangeActiveTextEditor(editor => { - if (editor) { + this.disposables.push( + workspace.onDidSaveTextDocument(document => { this.panel?.webview.postMessage({ key: 'vscode_send', payload: { - key: 'changeFocusOnFile', - payload: { path: editor.document.uri.toString() }, + key: 'savedFile', + payload: { fileUri: document.uri.toString() }, }, }) - } - }) + }), + ) + this.disposables.push( + window.onDidChangeActiveTextEditor(editor => { + if (editor) { + this.panel?.webview.postMessage({ + key: 'vscode_send', + payload: { + key: 'changeFocusOnFile', + payload: { path: editor.document.uri.toString() }, + }, + }) + } + }), + ) } public resolveWebviewView(webviewView: WebviewView) { @@ -58,12 +63,12 @@ export class LineagePanel implements WebviewViewProvider, Disposable { webviewView.webview.options = { // Allow scripts in the webview enableScripts: true, - localResourceRoots: [this._extensionUri], + localResourceRoots: [this.extensionUri], } // Set content options for external URL access // Set up message listener for events from the iframe - webviewView.webview.onDidReceiveMessage( + const disposable = webviewView.webview.onDidReceiveMessage( async request => { if (!request) { return @@ -99,7 +104,7 @@ export class LineagePanel implements WebviewViewProvider, Disposable { result: response, }, } - webviewView.webview.postMessage(responseCallback) + await webviewView.webview.postMessage(responseCallback) break } case 'get_active_file': { @@ -113,7 +118,7 @@ export class LineagePanel implements WebviewViewProvider, Disposable { }, }, } - webviewView.webview.postMessage(responseCallback) + await webviewView.webview.postMessage(responseCallback) break } default: { @@ -132,21 +137,22 @@ export class LineagePanel implements WebviewViewProvider, Disposable { undefined, [], ) + this.disposables.push(disposable) webviewView.webview.html = this.getHTML(webviewView.webview) } private getHTML(panel: Webview) { const cssUri = panel.asWebviewUri( - Uri.joinPath(this._extensionUri, 'src_react', 'assets', 'index.css'), + Uri.joinPath(this.extensionUri, 'src_react', 'assets', 'index.css'), ) const jsUri = panel.asWebviewUri( - Uri.joinPath(this._extensionUri, 'src_react', 'assets', 'index.js'), + Uri.joinPath(this.extensionUri, 'src_react', 'assets', 'index.js'), ) const faviconUri = panel.asWebviewUri( - Uri.joinPath(this._extensionUri, 'src_react', 'favicon.ico'), + Uri.joinPath(this.extensionUri, 'src_react', 'favicon.ico'), ) const logoUri = panel.asWebviewUri( - Uri.joinPath(this._extensionUri, 'src_react', 'logo192.png'), + Uri.joinPath(this.extensionUri, 'src_react', 'logo192.png'), ) // Handle query requests from the React app @@ -179,5 +185,9 @@ export class LineagePanel implements WebviewViewProvider, Disposable { // WebviewView doesn't have a dispose method // We can clear references this.panel = undefined + this.disposables.forEach(disposable => { + disposable.dispose() + }) + this.disposables = [] } } From 31f90d004a514b39b10a04c991551179c4422a17 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 15 Jun 2025 15:32:12 +0200 Subject: [PATCH 0401/1056] chore(vscode): better handling no sqlmesh (#4739) --- vscode/extension/src/utilities/errors.ts | 11 +++++ .../src/utilities/sqlmesh/sqlmesh.ts | 41 +++++++++++++++++-- .../{no_lsp.spec.ts => bad_setup.spec.ts} | 39 ++++++++++++++++++ vscode/extension/tests/lineage.spec.ts | 9 +--- vscode/extension/tests/utils.ts | 22 ++++++++++ 5 files changed, 111 insertions(+), 11 deletions(-) rename vscode/extension/tests/{no_lsp.spec.ts => bad_setup.spec.ts} (67%) diff --git a/vscode/extension/src/utilities/errors.ts b/vscode/extension/src/utilities/errors.ts index 7a0796a449..62d0d016b9 100644 --- a/vscode/extension/src/utilities/errors.ts +++ b/vscode/extension/src/utilities/errors.ts @@ -9,6 +9,7 @@ import { traceInfo } from './common/log' export type ErrorType = | ErrorTypeGeneric | { type: 'not_signed_in' } + | { type: 'sqlmesh_not_found' } | { type: 'sqlmesh_lsp_not_found' } // tcloud_bin_not_found is used when the tcloud executable is not found. This is likely to happen if the user // opens a project that has a `tcloud.yaml` file but doesn't have tcloud installed. @@ -85,6 +86,8 @@ export async function handleError( return case 'not_signed_in': return handleNotSignedInError(authProvider) + case 'sqlmesh_not_found': + return handleSqlmeshNotFoundError() case 'sqlmesh_lsp_not_found': return handleSqlmeshLspNotFoundError() case 'sqlmesh_lsp_dependencies_missing': @@ -118,6 +121,14 @@ const handleNotSignedInError = async ( } } +/** + * Handles the case where the sqlmesh executable is not found. + */ +const handleSqlmeshNotFoundError = async (): Promise => { + traceInfo('handleSqlmeshNotFoundError') + await window.showErrorMessage('SQLMesh not found, please check installation') +} + /** * Handles the case where the sqlmesh_lsp is not found. */ diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 45d9cfbd4c..be60f3be2a 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -287,6 +287,12 @@ export const sqlmeshExec = async (): Promise< args: [], }) } else { + const exists = await doesExecutableExist(sqlmesh) + if (!exists) { + return err({ + type: 'sqlmesh_not_found', + }) + } return ok({ bin: sqlmesh, workspacePath, @@ -384,15 +390,17 @@ export const sqlmeshLspExec = async (): Promise< type: 'not_signed_in', }) } + const exists = await doesExecutableExist(sqlmeshLSP) + if (!exists) { + return err({ + type: 'sqlmesh_lsp_not_found', + }) + } const ensured = await ensureSqlmeshEnterpriseInstalled() if (isErr(ensured)) { return ensured } } - const ensuredDependencies = await ensureSqlmeshLspDependenciesInstalled() - if (isErr(ensuredDependencies)) { - return ensuredDependencies - } const binPath = path.join(interpreterDetails.binPath!, sqlmeshLSP) traceLog(`Bin path: ${binPath}`) if (!fs.existsSync(binPath)) { @@ -400,6 +408,10 @@ export const sqlmeshLspExec = async (): Promise< type: 'sqlmesh_lsp_not_found', }) } + const ensuredDependencies = await ensureSqlmeshLspDependenciesInstalled() + if (isErr(ensuredDependencies)) { + return ensuredDependencies + } return ok({ bin: binPath, workspacePath, @@ -411,6 +423,12 @@ export const sqlmeshLspExec = async (): Promise< args: [], }) } else { + const exists = await doesExecutableExist(sqlmeshLSP) + if (!exists) { + return err({ + type: 'sqlmesh_lsp_not_found', + }) + } return ok({ bin: sqlmeshLSP, workspacePath, @@ -419,3 +437,18 @@ export const sqlmeshLspExec = async (): Promise< }) } } + +async function doesExecutableExist(executable: string): Promise { + const command = process.platform === 'win32' ? 'where.exe' : 'which' + traceLog(`Checking if ${executable} exists with ${command}`) + try { + const result = await execAsync(command, [executable]) + traceLog(`Checked if ${executable} exists with ${command}, with result ${result.exitCode}`) + const exists = result.exitCode === 0 + traceLog(`Checked if ${executable} exists with ${command}, with result ${exists}`) + return exists + } catch { + traceLog(`Checked if ${executable} exists with ${command}, errored, returning false`) + return false + } +} \ No newline at end of file diff --git a/vscode/extension/tests/no_lsp.spec.ts b/vscode/extension/tests/bad_setup.spec.ts similarity index 67% rename from vscode/extension/tests/no_lsp.spec.ts rename to vscode/extension/tests/bad_setup.spec.ts index b0aa6323b3..2c92e89764 100644 --- a/vscode/extension/tests/no_lsp.spec.ts +++ b/vscode/extension/tests/bad_setup.spec.ts @@ -4,6 +4,7 @@ import os from 'os' import path from 'path' import { createVirtualEnvironment, + openLineageView, pipInstall, REPO_ROOT, startVSCode, @@ -72,3 +73,41 @@ test('missing LSP dependencies shows install prompt', async ({}, testInfo) => { await fs.remove(tempDir) } }) + +test('lineage, no sqlmesh found', async ({}) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + const pythonDetails = await createVirtualEnvironment(pythonEnvDir) + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonDetails.pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson( + path.join(tempDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) + + const { window, close } = await startVSCode(tempDir) + + // Open lineage view + await openLineageView(window) + + // Assert shows that sqlmesh is not installed + await window.waitForSelector('text=SQLMesh LSP not found') + + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } +}) diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index e2a049a9f0..a75407802f 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -2,19 +2,14 @@ import { test, expect, Page } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { openLineageView, startVSCode, SUSHI_SOURCE_PATH } from './utils' import { writeFileSync } from 'fs' /** * Helper function to launch VS Code and test lineage with given project path config */ async function testLineageWithProjectPath(window: Page): Promise { - // Trigger lineage command - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('Lineage: Focus On View') - await window.keyboard.press('Enter') + await openLineageView(window) // Wait for "Loaded SQLMesh context" text to appear const loadedContextText = window.locator('text=Loaded SQLMesh context') diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index 8150c43c59..7b722c9e2f 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -137,3 +137,25 @@ export const pipInstall = async ( throw new Error(`Failed to install package: ${stderr}`) } } + +/** + * Open the lineage view in the given window. + */ +export const openLineageView = async (window: Page): Promise => { + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Lineage: Focus On View') + await window.keyboard.press('Enter') +} + +/** + * Restart the SQLMesh servers + */ +export const restartSqlmeshServers = async (window: Page): Promise => { + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Restart SQLMesh servers') + await window.keyboard.press('Enter') +} From 509d870fe7b93d91d8504f7c96142b7a17f32d12 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:08:50 +0200 Subject: [PATCH 0402/1056] fix: vscode tcloud fix order of checks (#4744) --- vscode/extension/src/utilities/sqlmesh/sqlmesh.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index be60f3be2a..f656937bab 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -390,12 +390,6 @@ export const sqlmeshLspExec = async (): Promise< type: 'not_signed_in', }) } - const exists = await doesExecutableExist(sqlmeshLSP) - if (!exists) { - return err({ - type: 'sqlmesh_lsp_not_found', - }) - } const ensured = await ensureSqlmeshEnterpriseInstalled() if (isErr(ensured)) { return ensured From 054afe596b345e94f0f1931f66c430d3d5512b73 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 16 Jun 2025 09:12:45 -0700 Subject: [PATCH 0403/1056] Chore: Update the plan evaluator to use stages (#4732) --- sqlmesh/core/plan/__init__.py | 1 - sqlmesh/core/plan/evaluator.py | 555 +++++++++--------------- sqlmesh/core/plan/explainer.py | 40 +- sqlmesh/core/plan/stages.py | 371 +++++++++++++++-- sqlmesh/core/scheduler.py | 33 +- sqlmesh/core/state_sync/base.py | 22 +- sqlmesh/utils/__init__.py | 6 + tests/conftest.py | 24 +- tests/core/test_context.py | 19 - tests/core/test_plan_evaluator.py | 47 +-- tests/core/test_plan_stages.py | 672 +++++++++++++++++++++++++++--- 11 files changed, 1231 insertions(+), 559 deletions(-) diff --git a/sqlmesh/core/plan/__init__.py b/sqlmesh/core/plan/__init__.py index 903e2e43ba..8b3ba63e55 100644 --- a/sqlmesh/core/plan/__init__.py +++ b/sqlmesh/core/plan/__init__.py @@ -8,6 +8,5 @@ from sqlmesh.core.plan.evaluator import ( BuiltInPlanEvaluator as BuiltInPlanEvaluator, PlanEvaluator as PlanEvaluator, - update_intervals_for_new_snapshots as update_intervals_for_new_snapshots, ) from sqlmesh.core.plan.explainer import PlanExplainer as PlanExplainer diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index c2e8846a9c..d1d9df8f28 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -23,6 +23,7 @@ from sqlmesh.core.environment import EnvironmentNamingInfo, execute_environment_statements from sqlmesh.core.macros import RuntimeStage from sqlmesh.core.snapshot.definition import Interval, to_view_mapping +from sqlmesh.core.plan import stages from sqlmesh.core.plan.definition import EvaluatablePlan from sqlmesh.core.scheduler import Scheduler from sqlmesh.core.snapshot import ( @@ -35,11 +36,10 @@ SnapshotTableInfo, SnapshotCreationFailedError, ) -from sqlmesh.utils import CompletionStatus -from sqlmesh.core.state_sync import StateSync, StateReader -from sqlmesh.core.state_sync.base import PromotionResult +from sqlmesh.utils import to_snake_case +from sqlmesh.core.state_sync import StateSync from sqlmesh.utils.concurrency import NodeExecutionFailedError -from sqlmesh.utils.errors import PlanError +from sqlmesh.utils.errors import PlanError, SQLMeshError from sqlmesh.utils.dag import DAG from sqlmesh.utils.date import now @@ -77,12 +77,14 @@ def __init__( self.create_scheduler = create_scheduler self.default_catalog = default_catalog self.console = console or get_console() + self._circuit_breaker: t.Optional[t.Callable[[], bool]] = None def evaluate( self, plan: EvaluatablePlan, circuit_breaker: t.Optional[t.Callable[[], bool]] = None, ) -> None: + self._circuit_breaker = circuit_breaker self.console.start_plan_evaluation(plan) analytics.collector.on_plan_apply_start( plan=plan, @@ -92,110 +94,8 @@ def evaluate( ) try: - new_snapshots = {s.snapshot_id: s for s in plan.new_snapshots} - stored_snapshots = self.state_sync.get_snapshots(plan.environment.snapshots) - snapshots = {**new_snapshots, **stored_snapshots} - snapshots_by_name = {s.name: s for s in snapshots.values()} - - all_names = {name for name in snapshots_by_name if plan.is_selected_for_backfill(name)} - - deployability_index_for_evaluation = DeployabilityIndex.create( - snapshots, start=plan.start - ) - deployability_index_for_creation = deployability_index_for_evaluation - if plan.is_dev: - before_promote_snapshots = all_names - after_promote_snapshots = set() - else: - before_promote_snapshots = { - s.name - for s in snapshots.values() - if deployability_index_for_evaluation.is_representative(s) - and plan.is_selected_for_backfill(s.name) - } - after_promote_snapshots = all_names - before_promote_snapshots - deployability_index_for_evaluation = DeployabilityIndex.all_deployable() - - # Step 1: Run before_all environment statements before doing anything else - execute_environment_statements( - adapter=self.snapshot_evaluator.adapter, - environment_statements=plan.environment_statements or [], - runtime_stage=RuntimeStage.BEFORE_ALL, - environment_naming_info=plan.environment.naming_info, - default_catalog=self.default_catalog, - snapshots=snapshots_by_name, - start=plan.start, - end=plan.end, - execution_time=plan.execution_time, - ) - - # Step 2:Store new snapshot records in the state and create physical tables for them - push_completion_status = self._push(plan, snapshots, deployability_index_for_creation) - if push_completion_status.is_nothing_to_do: - self.console.log_success( - "" if plan.restatements else "\nSKIP: No physical layer updates to perform" - ) - - # Step 3: Update the intervals for the new forward-only snapshots - update_intervals_for_new_snapshots(plan.new_snapshots, self.state_sync) - - # Step 4: Run audits without evaluations for snapshots that capture audit metadata changes - self._run_audits_for_metadata_snapshots(plan, new_snapshots) - - # Step 5: Remove intervals for snapshots that need to be restated - self._restate(plan, snapshots_by_name) - - # Step 6: Backfill missing intervals for snapshots that can be backfilled before updating - # the schema of production tables - first_bf_completion_status = self._backfill( - plan, - snapshots_by_name, - before_promote_snapshots, - deployability_index_for_evaluation, - circuit_breaker=circuit_breaker, - ) - - # Step 7: Update the target environment record in the state and migrate table schemas for forward-only - # snapshots if deploying to production - promotion_result = self._promote( - plan, snapshots, before_promote_snapshots, deployability_index_for_creation - ) - - # Step 8: Backfill missing intervals for snapshots that can be backfilled only after updating - # the schema of production tables - second_bf_completion_status = self._backfill( - plan, - snapshots_by_name, - after_promote_snapshots, - deployability_index_for_evaluation, - circuit_breaker=circuit_breaker, - ) - - if ( - first_bf_completion_status.is_nothing_to_do - and second_bf_completion_status.is_nothing_to_do - ): - self.console.log_success("SKIP: No model batches to execute") - - # Step 9: Update environment views to point at new physical tables and finalize the environment - # record in the state - self._update_views( - plan, snapshots, promotion_result, deployability_index_for_evaluation - ) - - # Step 10: Run after_all environment statements - execute_environment_statements( - adapter=self.snapshot_evaluator.adapter, - environment_statements=plan.environment_statements or [], - runtime_stage=RuntimeStage.AFTER_ALL, - environment_naming_info=plan.environment.naming_info, - default_catalog=self.default_catalog, - snapshots=snapshots_by_name, - start=plan.start, - end=plan.end, - execution_time=plan.execution_time, - ) - + plan_stages = stages.build_plan_stages(plan, self.state_sync, self.default_catalog) + self._evaluate_stages(plan_stages, plan) except Exception as e: analytics.collector.on_plan_apply_end(plan_id=plan.plan_id, error=e) raise @@ -204,101 +104,80 @@ def evaluate( finally: self.console.stop_plan_evaluation() - def _backfill( - self, - plan: EvaluatablePlan, - snapshots_by_name: t.Dict[str, Snapshot], - selected_snapshots: t.Set[str], - deployability_index: DeployabilityIndex, - circuit_breaker: t.Optional[t.Callable[[], bool]] = None, - ) -> CompletionStatus: - """Backfill missing intervals for snapshots that are part of the given plan. - - Args: - plan: The plan to source snapshots from. - selected_snapshots: The snapshots to backfill. - """ - if plan.empty_backfill: - intervals_to_add = [] - for snapshot in snapshots_by_name.values(): - if not snapshot.evaluatable or not plan.is_selected_for_backfill(snapshot.name): - # Skip snapshots that are not evaluatable or not selected for backfill. - continue - intervals = [ - snapshot.inclusive_exclusive(plan.start, plan.end, strict=False, expand=False) - ] - is_deployable = deployability_index.is_deployable(snapshot) - intervals_to_add.append( - SnapshotIntervals( - name=snapshot.name, - identifier=snapshot.identifier, - version=snapshot.version, - dev_version=snapshot.dev_version, - intervals=intervals if is_deployable else [], - dev_intervals=intervals if not is_deployable else [], - ) - ) - self.state_sync.add_snapshots_intervals(intervals_to_add) - return CompletionStatus.NOTHING_TO_DO - - if not plan.requires_backfill or not selected_snapshots: - return CompletionStatus.NOTHING_TO_DO - - scheduler = self.create_scheduler(snapshots_by_name.values()) - completion_status = scheduler.run( - plan.environment, - plan.start, - plan.end, + def _evaluate_stages( + self, plan_stages: t.List[stages.PlanStage], plan: EvaluatablePlan + ) -> None: + for stage in plan_stages: + stage_name = stage.__class__.__name__ + handler_name = f"visit_{to_snake_case(stage_name)}" + if not hasattr(self, handler_name): + raise SQLMeshError(f"Unexpected plan stage: {stage_name}") + logger.info("Evaluating plan stage %s", stage_name) + handler = getattr(self, handler_name) + handler(stage, plan) + + def visit_before_all_stage(self, stage: stages.BeforeAllStage, plan: EvaluatablePlan) -> None: + execute_environment_statements( + adapter=self.snapshot_evaluator.adapter, + environment_statements=stage.statements, + runtime_stage=RuntimeStage.BEFORE_ALL, + environment_naming_info=plan.environment.naming_info, + default_catalog=self.default_catalog, + snapshots=stage.all_snapshots, + start=plan.start, + end=plan.end, execution_time=plan.execution_time, - restatements={ - snapshots_by_name[name].snapshot_id: interval - for name, interval in plan.restatements.items() - }, - selected_snapshots=selected_snapshots, - deployability_index=deployability_index, - circuit_breaker=circuit_breaker, - end_bounded=plan.end_bounded, - interval_end_per_model=plan.interval_end_per_model, ) - if completion_status.is_failure: - raise PlanError("Plan application failed.") - return completion_status - - def _push( - self, - plan: EvaluatablePlan, - snapshots: t.Dict[SnapshotId, Snapshot], - deployability_index: t.Optional[DeployabilityIndex] = None, - ) -> CompletionStatus: - """Push the snapshots to the state sync. - - As a part of plan pushing, snapshot tables are created. + def visit_after_all_stage(self, stage: stages.AfterAllStage, plan: EvaluatablePlan) -> None: + execute_environment_statements( + adapter=self.snapshot_evaluator.adapter, + environment_statements=stage.statements, + runtime_stage=RuntimeStage.AFTER_ALL, + environment_naming_info=plan.environment.naming_info, + default_catalog=self.default_catalog, + snapshots=stage.all_snapshots, + start=plan.start, + end=plan.end, + execution_time=plan.execution_time, + ) - Args: - plan: The plan to source snapshots from. - deployability_index: Indicates which snapshots are deployable in the context of this creation. - """ - self.state_sync.push_snapshots(plan.new_snapshots) + def visit_create_snapshot_records_stage( + self, stage: stages.CreateSnapshotRecordsStage, plan: EvaluatablePlan + ) -> None: + self.state_sync.push_snapshots(stage.snapshots) analytics.collector.on_snapshots_created( - new_snapshots=plan.new_snapshots, plan_id=plan.plan_id + new_snapshots=stage.snapshots, plan_id=plan.plan_id ) + # Update the intervals for the new forward-only snapshots + self._update_intervals_for_new_snapshots(stage.snapshots) + + def visit_physical_layer_update_stage( + self, stage: stages.PhysicalLayerUpdateStage, plan: EvaluatablePlan + ) -> None: + skip_message = "" if plan.restatements else "\nSKIP: No physical layer updates to perform" - snapshots_to_create = get_snapshots_to_create(plan, snapshots) + snapshots_to_create = stage.snapshots + if not snapshots_to_create: + self.console.log_success(skip_message) + return completion_status = None progress_stopped = False try: completion_status = self.snapshot_evaluator.create( snapshots_to_create, - snapshots, + stage.all_snapshots, allow_destructive_snapshots=plan.allow_destructive_models, - deployability_index=deployability_index, + deployability_index=stage.deployability_index, on_start=lambda x: self.console.start_creation_progress( x, plan.environment, self.default_catalog ), on_complete=self.console.update_creation_progress, ) + if completion_status.is_nothing_to_do: + self.console.log_success(skip_message) + return except SnapshotCreationFailedError as ex: self.console.stop_creation_progress(success=False) progress_stopped = True @@ -316,71 +195,125 @@ def _push( success=completion_status is not None and completion_status.is_success ) - return completion_status + def visit_backfill_stage(self, stage: stages.BackfillStage, plan: EvaluatablePlan) -> None: + if plan.empty_backfill: + intervals_to_add = [] + for snapshot in stage.all_snapshots.values(): + if not snapshot.evaluatable or not plan.is_selected_for_backfill(snapshot.name): + # Skip snapshots that are not evaluatable or not selected for backfill. + continue + intervals = [ + snapshot.inclusive_exclusive(plan.start, plan.end, strict=False, expand=False) + ] + is_deployable = stage.deployability_index.is_deployable(snapshot) + intervals_to_add.append( + SnapshotIntervals( + name=snapshot.name, + identifier=snapshot.identifier, + version=snapshot.version, + dev_version=snapshot.dev_version, + intervals=intervals if is_deployable else [], + dev_intervals=intervals if not is_deployable else [], + ) + ) + self.state_sync.add_snapshots_intervals(intervals_to_add) + self.console.log_success("SKIP: No model batches to execute") + return - def _promote( - self, - plan: EvaluatablePlan, - snapshots: t.Dict[SnapshotId, Snapshot], - no_gaps_snapshot_names: t.Optional[t.Set[str]] = None, - deployability_index: t.Optional[DeployabilityIndex] = None, - ) -> PromotionResult: - """Promote a plan. + if not stage.snapshot_to_intervals: + self.console.log_success("SKIP: No model batches to execute") + return - Args: - plan: The plan to promote. - no_gaps_snapshot_names: The names of snapshots to check for gaps if the no gaps check is enabled in the plan. - If not provided, all snapshots are checked. - """ - promotion_result = self.state_sync.promote( + scheduler = self.create_scheduler(stage.all_snapshots.values()) + errors, _ = scheduler.run_merged_intervals( + merged_intervals=stage.snapshot_to_intervals, + deployability_index=stage.deployability_index, + environment_naming_info=plan.environment.naming_info, + execution_time=plan.execution_time, + circuit_breaker=self._circuit_breaker, + start=plan.start, + end=plan.end, + ) + if errors: + raise PlanError("Plan application failed.") + + def visit_audit_only_run_stage( + self, stage: stages.AuditOnlyRunStage, plan: EvaluatablePlan + ) -> None: + audit_snapshots = stage.snapshots + if not audit_snapshots: + return + + # If there are any snapshots to be audited, we'll reuse the scheduler's internals to audit them + scheduler = self.create_scheduler(audit_snapshots) + completion_status = scheduler.audit( plan.environment, - no_gaps_snapshot_names=no_gaps_snapshot_names if plan.no_gaps else set(), - environment_statements=plan.environment_statements, + plan.start, + plan.end, + execution_time=plan.execution_time, + end_bounded=plan.end_bounded, + interval_end_per_model=plan.interval_end_per_model, ) - if not plan.is_dev: - try: - self.snapshot_evaluator.migrate( - [s for s in snapshots.values() if s.is_paused], - snapshots, - allow_destructive_snapshots=plan.allow_destructive_models, - deployability_index=deployability_index, - ) - except NodeExecutionFailedError as ex: - raise PlanError(str(ex.__cause__) if ex.__cause__ else str(ex)) + if completion_status.is_failure: + raise PlanError("Plan application failed.") - if not plan.ensure_finalized_snapshots: - # Only unpause at this point if we don't have to use the finalized snapshots - # for subsequent plan applications. Otherwise, unpause right before finalizing - # the environment. - self.state_sync.unpause_snapshots(promotion_result.added, plan.end) + def visit_restatement_stage( + self, stage: stages.RestatementStage, plan: EvaluatablePlan + ) -> None: + snapshot_intervals_to_restate = {(s, i) for s, i in stage.snapshot_intervals.items()} - return promotion_result + # Restating intervals on prod plans should mean that the intervals are cleared across + # all environments, not just the version currently in prod + # This ensures that work done in dev environments can still be promoted to prod + # by forcing dev environments to re-run intervals that changed in prod + # + # Without this rule, its possible that promoting a dev table to prod will introduce old data to prod + snapshot_intervals_to_restate.update( + self._restatement_intervals_across_all_environments( + prod_restatements=plan.restatements, + disable_restatement_models=plan.disabled_restatement_models, + loaded_snapshots={s.snapshot_id: s for s in stage.all_snapshots.values()}, + ) + ) - def _update_views( - self, - plan: EvaluatablePlan, - snapshots: t.Dict[SnapshotId, Snapshot], - promotion_result: PromotionResult, - deployability_index: t.Optional[DeployabilityIndex] = None, + self.state_sync.remove_intervals( + snapshot_intervals=list(snapshot_intervals_to_restate), + remove_shared_versions=plan.is_prod, + ) + + def visit_environment_record_update_stage( + self, stage: stages.EnvironmentRecordUpdateStage, plan: EvaluatablePlan ) -> None: - """Update environment views. + self.state_sync.promote( + plan.environment, + no_gaps_snapshot_names=stage.no_gaps_snapshot_names if plan.no_gaps else set(), + environment_statements=plan.environment_statements, + ) - Args: - plan: The plan to promote. - promotion_result: The result of the promotion. - deployability_index: Indicates which snapshots are deployable in the context of this promotion. - """ - if not plan.is_dev and plan.ensure_finalized_snapshots: - # Unpause right before finalizing the environment in case when - # we need to use the finalized snapshots for subsequent plan applications. - # Otherwise, unpause right after updatig the environment record. - self.state_sync.unpause_snapshots(promotion_result.added, plan.end) + def visit_migrate_schemas_stage( + self, stage: stages.MigrateSchemasStage, plan: EvaluatablePlan + ) -> None: + try: + self.snapshot_evaluator.migrate( + stage.snapshots, + stage.all_snapshots, + allow_destructive_snapshots=plan.allow_destructive_models, + deployability_index=stage.deployability_index, + ) + except NodeExecutionFailedError as ex: + raise PlanError(str(ex.__cause__) if ex.__cause__ else str(ex)) + + def visit_unpause_stage(self, stage: stages.UnpauseStage, plan: EvaluatablePlan) -> None: + self.state_sync.unpause_snapshots(stage.promoted_snapshots, plan.end) + def visit_virtual_layer_update_stage( + self, stage: stages.VirtualLayerUpdateStage, plan: EvaluatablePlan + ) -> None: environment = plan.environment self.console.start_promotion_progress( - promotion_result.added + promotion_result.removed, + list(stage.promoted_snapshots) + list(stage.demoted_snapshots), environment.naming_info, self.default_catalog, ) @@ -389,25 +322,28 @@ def _update_views( try: self._promote_snapshots( plan, - [snapshots[s.snapshot_id] for s in promotion_result.added], + [stage.all_snapshots[s.snapshot_id] for s in stage.promoted_snapshots], environment.naming_info, - deployability_index=deployability_index, + deployability_index=stage.deployability_index, on_complete=lambda s: self.console.update_promotion_progress(s, True), - snapshots=snapshots, + snapshots=stage.all_snapshots, ) - if promotion_result.removed_environment_naming_info: + if stage.demoted_environment_naming_info: self._demote_snapshots( - plan, - promotion_result.removed, - promotion_result.removed_environment_naming_info, + stage.demoted_snapshots, + stage.demoted_environment_naming_info, on_complete=lambda s: self.console.update_promotion_progress(s, False), ) - self.state_sync.finalize(environment) completed = True finally: self.console.stop_promotion_progress(success=completed) + def visit_finalize_environment_stage( + self, stage: stages.FinalizeEnvironmentStage, plan: EvaluatablePlan + ) -> None: + self.state_sync.finalize(plan.environment) + def _promote_snapshots( self, plan: EvaluatablePlan, @@ -436,7 +372,6 @@ def _promote_snapshots( def _demote_snapshots( self, - plan: EvaluatablePlan, target_snapshots: t.Iterable[SnapshotTableInfo], environment_naming_info: EnvironmentNamingInfo, on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None, @@ -445,34 +380,6 @@ def _demote_snapshots( target_snapshots, environment_naming_info, on_complete=on_complete ) - def _restate(self, plan: EvaluatablePlan, snapshots_by_name: t.Dict[str, Snapshot]) -> None: - if not plan.restatements or plan.is_dev: - return - - snapshot_intervals_to_restate = { - (snapshots_by_name[name].table_info, intervals) - for name, intervals in plan.restatements.items() - } - - # Restating intervals on prod plans should mean that the intervals are cleared across - # all environments, not just the version currently in prod - # This ensures that work done in dev environments can still be promoted to prod - # by forcing dev environments to re-run intervals that changed in prod - # - # Without this rule, its possible that promoting a dev table to prod will introduce old data to prod - snapshot_intervals_to_restate.update( - self._restatement_intervals_across_all_environments( - prod_restatements=plan.restatements, - disable_restatement_models=plan.disabled_restatement_models, - loaded_snapshots={s.snapshot_id: s for s in snapshots_by_name.values()}, - ) - ) - - self.state_sync.remove_intervals( - snapshot_intervals=list(snapshot_intervals_to_restate), - remove_shared_versions=plan.is_prod, - ) - def _restatement_intervals_across_all_environments( self, prod_restatements: t.Dict[str, Interval], @@ -552,95 +459,19 @@ def _restatement_intervals_across_all_environments( return set(snapshots_to_restate.values()) - def _run_audits_for_metadata_snapshots( - self, - plan: EvaluatablePlan, - new_snapshots: t.Dict[SnapshotId, Snapshot], - ) -> None: - audit_snapshots = get_audit_only_snapshots(new_snapshots, self.state_sync) - if not audit_snapshots: - return - - # If there are any snapshots to be audited, we'll reuse the scheduler's internals to audit them - scheduler = self.create_scheduler(audit_snapshots.values()) - completion_status = scheduler.audit( - plan.environment, - plan.start, - plan.end, - execution_time=plan.execution_time, - end_bounded=plan.end_bounded, - interval_end_per_model=plan.interval_end_per_model, - ) - - if completion_status.is_failure: - raise PlanError("Plan application failed.") - - -def update_intervals_for_new_snapshots( - snapshots: t.Collection[Snapshot], state_sync: StateSync -) -> None: - snapshots_intervals: t.List[SnapshotIntervals] = [] - for snapshot in state_sync.refresh_snapshot_intervals(snapshots): - if snapshot.is_forward_only: - snapshot.dev_intervals = snapshot.intervals.copy() - snapshots_intervals.append( - SnapshotIntervals( - name=snapshot.name, - identifier=snapshot.identifier, - version=snapshot.version, - dev_version=snapshot.dev_version, - dev_intervals=snapshot.dev_intervals, + def _update_intervals_for_new_snapshots(self, snapshots: t.Collection[Snapshot]) -> None: + snapshots_intervals: t.List[SnapshotIntervals] = [] + for snapshot in snapshots: + if snapshot.is_forward_only: + snapshots_intervals.append( + SnapshotIntervals( + name=snapshot.name, + identifier=snapshot.identifier, + version=snapshot.version, + dev_version=snapshot.dev_version, + dev_intervals=snapshot.dev_intervals, + ) ) - ) - - if snapshots_intervals: - state_sync.add_snapshots_intervals(snapshots_intervals) - - -def get_audit_only_snapshots( - new_snapshots: t.Dict[SnapshotId, Snapshot], state_reader: StateReader -) -> t.Dict[SnapshotId, Snapshot]: - metadata_snapshots = [] - for snapshot in new_snapshots.values(): - if not snapshot.is_metadata or not snapshot.is_model or not snapshot.evaluatable: - continue - - metadata_snapshots.append(snapshot) - - # Bulk load all the previous snapshots - previous_snapshots = state_reader.get_snapshots( - [s.previous_version.snapshot_id(s.name) for s in metadata_snapshots if s.previous_version] - ).values() - - # Check if any of the snapshots have modifications to the audits field by comparing the hashes - audit_snapshots = {} - for snapshot, previous_snapshot in zip(metadata_snapshots, previous_snapshots): - new_audits_hash = snapshot.model.audit_metadata_hash() - previous_audit_hash = previous_snapshot.model.audit_metadata_hash() - - if snapshot.model.audits and previous_audit_hash != new_audits_hash: - audit_snapshots[snapshot.snapshot_id] = snapshot - - return audit_snapshots - - -def get_snapshots_to_create( - plan: EvaluatablePlan, snapshots: t.Dict[SnapshotId, Snapshot] -) -> t.List[Snapshot]: - promoted_snapshot_ids = ( - set(plan.environment.promoted_snapshot_ids) - if plan.environment.promoted_snapshot_ids is not None - else None - ) - - def _should_create(s: Snapshot) -> bool: - if not s.is_model or s.is_symbolic: - return False - # Only create tables for snapshots that we're planning to promote or that were selected for backfill - return ( - plan.is_selected_for_backfill(s.name) - or promoted_snapshot_ids is None - or s.snapshot_id in promoted_snapshot_ids - ) - return [s for s in snapshots.values() if _should_create(s)] + if snapshots_intervals: + self.state_sync.add_snapshots_intervals(snapshots_intervals) diff --git a/sqlmesh/core/plan/explainer.py b/sqlmesh/core/plan/explainer.py index 32d80b5dc5..aa1d5775b4 100644 --- a/sqlmesh/core/plan/explainer.py +++ b/sqlmesh/core/plan/explainer.py @@ -17,7 +17,7 @@ from sqlmesh.core.snapshot.definition import ( SnapshotInfoMixin, ) -from sqlmesh.utils import Verbosity, rich as srich +from sqlmesh.utils import Verbosity, rich as srich, to_snake_case from sqlmesh.utils.date import to_ts from sqlmesh.utils.errors import SQLMeshError @@ -73,7 +73,7 @@ def __init__( def explain(self, stages: t.List[stages.PlanStage]) -> None: tree = Tree("[bold]Explained plan[/bold]") for stage in stages: - handler_name = f"visit_{_to_snake_case(stage.__class__.__name__)}" + handler_name = f"visit_{to_snake_case(stage.__class__.__name__)}" if not hasattr(self, handler_name): logger.error("Unexpected stage: %s", stage.__class__.__name__) continue @@ -97,6 +97,9 @@ def visit_physical_layer_update_stage(self, stage: stages.PhysicalLayerUpdateSta "[bold]Validate SQL and create physical layer tables and views if they do not exist[/bold]" ) for snapshot in stage.snapshots: + if snapshot.snapshot_id not in stage.snapshots_with_missing_intervals: + continue + is_deployable = ( stage.deployability_index.is_deployable(snapshot) if self.environment_naming_info.name != c.PROD @@ -114,7 +117,9 @@ def visit_physical_layer_update_stage(self, stage: stages.PhysicalLayerUpdateSta if snapshot.is_view: create_tree = Tree("Create view if it doesn't exist") - elif snapshot.is_forward_only and snapshot.previous_versions: + elif ( + snapshot.is_forward_only and snapshot.previous_versions and not snapshot.is_managed + ): prod_table = snapshot.table_name(True) create_tree = Tree( f"Clone {prod_table} into {table_name} and then update its schema if it doesn't exist" @@ -224,7 +229,7 @@ def visit_virtual_layer_update_stage(self, stage: stages.VirtualLayerUpdateStage "[bold]Delete views in the virtual layer for models that were removed[/bold]" ) for snapshot in stage.demoted_snapshots: - display_name = self._display_name(snapshot) + display_name = self._display_name(snapshot, stage.demoted_environment_naming_info) demote_tree.add(display_name) if stage.promoted_snapshots: @@ -233,14 +238,31 @@ def visit_virtual_layer_update_stage(self, stage: stages.VirtualLayerUpdateStage tree.add(self._limit_tree(demote_tree)) return tree + def visit_create_snapshot_records_stage( + self, stage: stages.CreateSnapshotRecordsStage + ) -> t.Optional[Tree]: + return None + def visit_environment_record_update_stage( self, stage: stages.EnvironmentRecordUpdateStage ) -> t.Optional[Tree]: return None - def _display_name(self, snapshot: SnapshotInfoMixin) -> str: + def visit_unpause_stage(self, stage: stages.UnpauseStage) -> t.Optional[Tree]: + return None + + def visit_finalize_environment_stage( + self, stage: stages.FinalizeEnvironmentStage + ) -> t.Optional[Tree]: + return None + + def _display_name( + self, + snapshot: SnapshotInfoMixin, + environment_naming_info: t.Optional[EnvironmentNamingInfo] = None, + ) -> str: return snapshot.display_name( - self.environment_naming_info, + environment_naming_info or self.environment_naming_info, self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect, ) @@ -273,9 +295,3 @@ def _get_explainer_console( verbosity=console.verbosity, console=console.console, ) - - -def _to_snake_case(name: str) -> str: - return "".join( - f"_{c.lower()}" if c.isupper() and idx != 0 else c.lower() for idx, c in enumerate(name) - ) diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 964dadb143..d8c7cba85d 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -1,11 +1,8 @@ import typing as t from dataclasses import dataclass +from sqlmesh.core.environment import EnvironmentStatements, EnvironmentNamingInfo from sqlmesh.core.plan.definition import EvaluatablePlan -from sqlmesh.core.plan.evaluator import ( - get_audit_only_snapshots, - get_snapshots_to_create, -) from sqlmesh.core.state_sync import StateReader from sqlmesh.core.scheduler import merged_missing_intervals, SnapshotToIntervals from sqlmesh.core.snapshot.definition import ( @@ -19,68 +16,193 @@ @dataclass class BeforeAllStage: - statements: t.List[str] + """Run environment statements before every other stage. + + Args: + statements: Environment statements to run before every other stage. + all_snapshots: All snapshots in the plan by name. + """ + + statements: t.List[EnvironmentStatements] + all_snapshots: t.Dict[str, Snapshot] @dataclass class AfterAllStage: - statements: t.List[str] + """Run environment statements after all other stages. + + Args: + statements: Environment statements to run after all other stages. + all_snapshots: All snapshots in the plan by name. + """ + + statements: t.List[EnvironmentStatements] + all_snapshots: t.Dict[str, Snapshot] + + +@dataclass +class CreateSnapshotRecordsStage: + """Create new snapshot reecords in the state. + + Args: + snapshots: New snapshots to create records for. + """ + + snapshots: t.List[Snapshot] @dataclass class PhysicalLayerUpdateStage: + """Update the physical layer by creating physical tables and views for given snapshots. + + Args: + snapshots: Snapshots to create physical tables and views for. This collection can be empty in which case + no physical layer update is needed. This can be useful to report the lack of physical layer updates + back to the user. + all_snapshots: All snapshots in the plan by snapshot ID. + snapshots_with_missing_intervals: Snapshots that have missing intervals. + deployability_index: Deployability index for this stage. + """ + snapshots: t.List[Snapshot] + all_snapshots: t.Dict[SnapshotId, Snapshot] + snapshots_with_missing_intervals: t.Set[SnapshotId] deployability_index: DeployabilityIndex @dataclass class AuditOnlyRunStage: + """Run audits only for given snapshots. + + Args: + snapshots: Snapshots to run audits for. + """ + snapshots: t.List[Snapshot] @dataclass class RestatementStage: + """Restate intervals for given snapshots. + + Args: + snapshot_intervals: Intervals to restate. + all_snapshots: All snapshots in the plan by name. + """ + snapshot_intervals: t.Dict[SnapshotTableInfo, Interval] + all_snapshots: t.Dict[str, Snapshot] @dataclass class BackfillStage: + """Backfill given missing intervals. + + Args: + snapshot_to_intervals: Intervals to backfill. This collection can be empty in which case no backfill is needed. + This can be useful to report the lack of backfills back to the user. + all_snapshots: All snapshots in the plan by name. + deployability_index: Deployability index for this stage. + before_promote: Whether this stage is before the promotion stage. + """ + snapshot_to_intervals: SnapshotToIntervals + all_snapshots: t.Dict[str, Snapshot] deployability_index: DeployabilityIndex before_promote: bool = True +@dataclass +class EnvironmentRecordUpdateStage: + """Update the environment record in the state. + + Args: + no_gaps_snapshot_names: Names of snapshots for which there should be no interval gaps. + """ + + no_gaps_snapshot_names: t.Set[str] + + @dataclass class MigrateSchemasStage: + """Migrate schemas of physical tables for given snapshots. + + Args: + snapshots: Snapshots to migrate schemas for. + all_snapshots: All snapshots in the plan by snapshot ID. + deployability_index: Deployability index for this stage. + """ + snapshots: t.List[Snapshot] + all_snapshots: t.Dict[SnapshotId, Snapshot] + deployability_index: DeployabilityIndex @dataclass class VirtualLayerUpdateStage: + """Update the virtual layer by creating and deleting views for given snapshots. + + Args: + promoted_snapshots: Snapshots to create views for. + demoted_snapshots: Snapshots to delete views for. + demoted_environment_naming_info: Environment naming info of the previous environment record. + all_snapshots: All snapshots in the plan by snapshot ID. + deployability_index: Deployability index for this stage. + """ + promoted_snapshots: t.Set[SnapshotTableInfo] demoted_snapshots: t.Set[SnapshotTableInfo] + demoted_environment_naming_info: t.Optional[EnvironmentNamingInfo] + all_snapshots: t.Dict[SnapshotId, Snapshot] deployability_index: DeployabilityIndex @dataclass -class EnvironmentRecordUpdateStage: +class UnpauseStage: + """Unpause given snapshots that are being deployed to prod. + + Args: + promoted_snapshots: Snapshots to unpause. + """ + + promoted_snapshots: t.Set[SnapshotTableInfo] + + +@dataclass +class FinalizeEnvironmentStage: + """Finalize the enviornment record in the state. + + Finalization means that all stages have been applied and that the environment has been transitioned + to the new state successfully. This should be the last stage in the plan application process. + """ + pass PlanStage = t.Union[ BeforeAllStage, AfterAllStage, + CreateSnapshotRecordsStage, PhysicalLayerUpdateStage, AuditOnlyRunStage, RestatementStage, BackfillStage, + EnvironmentRecordUpdateStage, MigrateSchemasStage, VirtualLayerUpdateStage, - EnvironmentRecordUpdateStage, + UnpauseStage, + FinalizeEnvironmentStage, ] class PlanStagesBuilder: + """The builder for the plan stages. + + Args: + state_reader: The state reader to use to read the snapshots and environment. + default_catalog: The default catalog to use for the snapshots. + """ + def __init__( self, state_reader: StateReader, @@ -90,6 +212,16 @@ def __init__( self.default_catalog = default_catalog def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: + """Builds the plan stages for the given plan. + + NOTE: Building the plan stages should NOT produce any side effects in the state or the data warehouse. + + Args: + plan: The plan to build the stages for. + + Returns: + A list of plan stages. + """ new_snapshots = {s.snapshot_id: s for s in plan.new_snapshots} stored_snapshots = self.state_reader.get_snapshots(plan.environment.snapshots) snapshots = {**new_snapshots, **stored_snapshots} @@ -99,6 +231,8 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: s.snapshot_id for s in snapshots.values() if plan.is_selected_for_backfill(s.name) } + self._adjust_intervals(snapshots_by_name, plan) + deployability_index = DeployabilityIndex.create(snapshots, start=plan.start) deployability_index_for_creation = deployability_index if plan.is_dev: @@ -119,8 +253,9 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: s for s in snapshots.values() if s.is_paused - and s.is_materialized - and not deployability_index_for_creation.is_representative(s) + and s.is_model + and not s.is_symbolic + and (not deployability_index_for_creation.is_representative(s) or s.is_view) ] snapshots_to_intervals = self._missing_intervals( @@ -138,19 +273,26 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: elif snapshot.snapshot_id in after_promote_snapshots: missing_intervals_after_promote[snapshot] = intervals + promoted_snapshots, demoted_snapshots, demoted_environment_naming_info = ( + self._get_promoted_demoted_snapshots(plan) + ) + stages: t.List[PlanStage] = [] - before_all_stage = self._get_before_all_stage(plan) + before_all_stage = self._get_before_all_stage(plan, snapshots_by_name) if before_all_stage: stages.append(before_all_stage) + if plan.new_snapshots: + stages.append(CreateSnapshotRecordsStage(snapshots=plan.new_snapshots)) + stages.append( self._get_physical_layer_update_stage( plan, snapshots, snapshots_to_intervals, deployability_index_for_creation ) ) - audit_only_snapshots = get_audit_only_snapshots(new_snapshots, self.state_reader) + audit_only_snapshots = self._get_audit_only_snapshots(new_snapshots) if audit_only_snapshots: stages.append(AuditOnlyRunStage(snapshots=list(audit_only_snapshots.values()))) @@ -162,53 +304,101 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: stages.append( BackfillStage( snapshot_to_intervals=missing_intervals_before_promote, + all_snapshots=snapshots_by_name, deployability_index=deployability_index, ) ) elif not needs_backfill: # Append an empty backfill stage so that explainer can show that the stage is skipped stages.append( - BackfillStage(snapshot_to_intervals={}, deployability_index=deployability_index) + BackfillStage( + snapshot_to_intervals={}, + all_snapshots=snapshots_by_name, + deployability_index=deployability_index, + ) ) - stages.append(EnvironmentRecordUpdateStage()) + stages.append( + EnvironmentRecordUpdateStage( + no_gaps_snapshot_names={s.name for s in before_promote_snapshots} + ) + ) if snapshots_with_schema_migration: - stages.append(MigrateSchemasStage(snapshots=snapshots_with_schema_migration)) + stages.append( + MigrateSchemasStage( + snapshots=snapshots_with_schema_migration, + all_snapshots=snapshots, + deployability_index=deployability_index_for_creation, + ) + ) + + if not plan.is_dev and not plan.ensure_finalized_snapshots and promoted_snapshots: + # Only unpause at this point if we don't have to use the finalized snapshots + # for subsequent plan applications. Otherwise, unpause right before updating + # the virtual layer. + stages.append(UnpauseStage(promoted_snapshots=promoted_snapshots)) if missing_intervals_after_promote: stages.append( BackfillStage( snapshot_to_intervals=missing_intervals_after_promote, + all_snapshots=snapshots_by_name, deployability_index=deployability_index, ) ) - virtual_layer_update_stage = self._get_virtual_layer_update_stage(plan, deployability_index) + if not plan.is_dev and plan.ensure_finalized_snapshots and promoted_snapshots: + # Unpause right before updating the virtual layer and finalizing the environment in case when + # we need to use the finalized snapshots for subsequent plan applications. + # Otherwise, unpause right after updatig the environment record. + stages.append(UnpauseStage(promoted_snapshots=promoted_snapshots)) + + virtual_layer_update_stage = self._get_virtual_layer_update_stage( + promoted_snapshots, + demoted_snapshots, + demoted_environment_naming_info, + snapshots, + deployability_index, + ) if virtual_layer_update_stage: stages.append(virtual_layer_update_stage) - after_all_stage = self._get_after_all_stage(plan) + stages.append(FinalizeEnvironmentStage()) + + after_all_stage = self._get_after_all_stage(plan, snapshots_by_name) if after_all_stage: stages.append(after_all_stage) return stages - def _get_before_all_stage(self, plan: EvaluatablePlan) -> t.Optional[BeforeAllStage]: + def _get_before_all_stage( + self, plan: EvaluatablePlan, snapshots_by_name: t.Dict[str, Snapshot] + ) -> t.Optional[BeforeAllStage]: before_all = [ - statement + environment_statements for environment_statements in plan.environment_statements or [] - for statement in environment_statements.before_all + if environment_statements.before_all ] - return BeforeAllStage(statements=before_all) if before_all else None + return ( + BeforeAllStage(statements=before_all, all_snapshots=snapshots_by_name) + if before_all + else None + ) - def _get_after_all_stage(self, plan: EvaluatablePlan) -> t.Optional[AfterAllStage]: + def _get_after_all_stage( + self, plan: EvaluatablePlan, snapshots_by_name: t.Dict[str, Snapshot] + ) -> t.Optional[AfterAllStage]: after_all = [ - statement + environment_statements for environment_statements in plan.environment_statements or [] - for statement in environment_statements.after_all + if environment_statements.after_all ] - return AfterAllStage(statements=after_all) if after_all else None + return ( + AfterAllStage(statements=after_all, all_snapshots=snapshots_by_name) + if after_all + else None + ) def _get_restatement_stage( self, plan: EvaluatablePlan, snapshots_by_name: t.Dict[str, Snapshot] @@ -220,7 +410,9 @@ def _get_restatement_stage( snapshot_intervals_to_restate[restated_snapshot.table_info] = interval if not snapshot_intervals_to_restate or plan.is_dev: return None - return RestatementStage(snapshot_intervals=snapshot_intervals_to_restate) + return RestatementStage( + snapshot_intervals=snapshot_intervals_to_restate, all_snapshots=snapshots_by_name + ) def _get_physical_layer_update_stage( self, @@ -229,50 +421,80 @@ def _get_physical_layer_update_stage( snapshots_to_intervals: SnapshotToIntervals, deployability_index: DeployabilityIndex, ) -> PhysicalLayerUpdateStage: - snapshots_to_create = [ - s - for s in get_snapshots_to_create(plan, snapshots) - if s in snapshots_to_intervals and s.is_model and not s.is_symbolic - ] return PhysicalLayerUpdateStage( - snapshots=snapshots_to_create, + snapshots=self._get_snapshots_to_create(plan, snapshots), + all_snapshots=snapshots, + snapshots_with_missing_intervals={s.snapshot_id for s in snapshots_to_intervals}, deployability_index=deployability_index, ) def _get_virtual_layer_update_stage( - self, plan: EvaluatablePlan, deployability_index: DeployabilityIndex + self, + promoted_snapshots: t.Set[SnapshotTableInfo], + demoted_snapshots: t.Set[SnapshotTableInfo], + demoted_environment_naming_info: t.Optional[EnvironmentNamingInfo], + all_snapshots: t.Dict[SnapshotId, Snapshot], + deployability_index: DeployabilityIndex, ) -> t.Optional[VirtualLayerUpdateStage]: - promoted_snapshots, demoted_snapshots = self._get_promoted_demoted_snapshots(plan) + promoted_snapshots = {s for s in promoted_snapshots if s.is_model and not s.is_symbolic} + demoted_snapshots = {s for s in demoted_snapshots if s.is_model and not s.is_symbolic} if not promoted_snapshots and not demoted_snapshots: return None return VirtualLayerUpdateStage( promoted_snapshots=promoted_snapshots, demoted_snapshots=demoted_snapshots, + demoted_environment_naming_info=demoted_environment_naming_info, + all_snapshots=all_snapshots, deployability_index=deployability_index, ) def _get_promoted_demoted_snapshots( self, plan: EvaluatablePlan - ) -> t.Tuple[t.Set[SnapshotTableInfo], t.Set[SnapshotTableInfo]]: + ) -> t.Tuple[ + t.Set[SnapshotTableInfo], t.Set[SnapshotTableInfo], t.Optional[EnvironmentNamingInfo] + ]: existing_environment = self.state_reader.get_environment(plan.environment.name) if existing_environment: - snapshots_by_name = {s.name: s for s in existing_environment.snapshots} - demoted_snapshot_names = {s.name for s in existing_environment.promoted_snapshots} - { + new_table_infos = { + table_info.name: table_info for table_info in plan.environment.promoted_snapshots + } + existing_table_infos = { + table_info.name: table_info + for table_info in existing_environment.promoted_snapshots + } + views_that_changed_location = { + existing_table_info + for existing_table_info in existing_environment.promoted_snapshots + if existing_table_info.name in new_table_infos + and existing_table_info.qualified_view_name.for_environment( + existing_environment.naming_info + ) + != new_table_infos[existing_table_info.name].qualified_view_name.for_environment( + plan.environment.naming_info + ) + } + missing_model_names = set(existing_table_infos) - { s.name for s in plan.environment.promoted_snapshots } - demoted_snapshots = {snapshots_by_name[name] for name in demoted_snapshot_names} + demoted_snapshots = { + existing_table_infos[name] for name in missing_model_names + } | views_that_changed_location else: demoted_snapshots = set() + promoted_snapshots = set(plan.environment.promoted_snapshots) if existing_environment and plan.environment.can_partially_promote(existing_environment): promoted_snapshots -= set(existing_environment.promoted_snapshots) - def _snapshot_filter(snapshot: SnapshotTableInfo) -> bool: - return snapshot.is_model and not snapshot.is_symbolic + demoted_environment_naming_info = ( + existing_environment.naming_info if demoted_snapshots and existing_environment else None + ) - return {s for s in promoted_snapshots if _snapshot_filter(s)}, { - s for s in demoted_snapshots if _snapshot_filter(s) - } + return ( + promoted_snapshots, + demoted_snapshots, + demoted_environment_naming_info, + ) def _missing_intervals( self, @@ -294,6 +516,69 @@ def _missing_intervals( interval_end_per_model=plan.interval_end_per_model, ) + def _get_audit_only_snapshots( + self, new_snapshots: t.Dict[SnapshotId, Snapshot] + ) -> t.Dict[SnapshotId, Snapshot]: + metadata_snapshots = [] + for snapshot in new_snapshots.values(): + if not snapshot.is_metadata or not snapshot.is_model or not snapshot.evaluatable: + continue + + metadata_snapshots.append(snapshot) + + # Bulk load all the previous snapshots + previous_snapshots = self.state_reader.get_snapshots( + [ + s.previous_version.snapshot_id(s.name) + for s in metadata_snapshots + if s.previous_version + ] + ).values() + + # Check if any of the snapshots have modifications to the audits field by comparing the hashes + audit_snapshots = {} + for snapshot, previous_snapshot in zip(metadata_snapshots, previous_snapshots): + new_audits_hash = snapshot.model.audit_metadata_hash() + previous_audit_hash = previous_snapshot.model.audit_metadata_hash() + + if snapshot.model.audits and previous_audit_hash != new_audits_hash: + audit_snapshots[snapshot.snapshot_id] = snapshot + + return audit_snapshots + + def _get_snapshots_to_create( + self, plan: EvaluatablePlan, snapshots: t.Dict[SnapshotId, Snapshot] + ) -> t.List[Snapshot]: + promoted_snapshot_ids = ( + set(plan.environment.promoted_snapshot_ids) + if plan.environment.promoted_snapshot_ids is not None + else None + ) + + def _should_create(s: Snapshot) -> bool: + if not s.is_model or s.is_symbolic: + return False + # Only create tables for snapshots that we're planning to promote or that were selected for backfill + return ( + plan.is_selected_for_backfill(s.name) + or promoted_snapshot_ids is None + or s.snapshot_id in promoted_snapshot_ids + ) + + return [s for s in snapshots.values() if _should_create(s)] + + def _adjust_intervals( + self, snapshots_by_name: t.Dict[str, Snapshot], plan: EvaluatablePlan + ) -> None: + # Make sure the intervals are up to date and restatements are reflected + self.state_reader.refresh_snapshot_intervals(snapshots_by_name.values()) + for new_snapshot in plan.new_snapshots: + if new_snapshot.is_forward_only: + # Forward-only snapshots inherit intervals in dev because of cloning + new_snapshot.dev_intervals = new_snapshot.intervals.copy() + for s_name, interval in plan.restatements.items(): + snapshots_by_name[s_name].remove_interval(interval) + def build_plan_stages( plan: EvaluatablePlan, diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index a66f7c19fe..d0a121a40a 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -441,12 +441,27 @@ def evaluate_node(node: SchedulingUnit) -> None: try: with self.snapshot_evaluator.concurrent_context(): - return concurrent_apply_to_dag( + errors, skipped_intervals = concurrent_apply_to_dag( dag, evaluate_node, self.max_workers, raise_on_error=False, ) + self.console.stop_evaluation_progress(success=not errors) + + skipped_snapshots = {i[0] for i in skipped_intervals} + self.console.log_skipped_models(skipped_snapshots) + for skipped in skipped_snapshots: + logger.info(f"SKIPPED snapshot {skipped}\n") + + for error in errors: + if isinstance(error.__cause__, CircuitBreakerError): + raise error.__cause__ + logger.info(str(error), exc_info=error) + + self.console.log_failed_models(errors) + + return errors, skipped_intervals finally: if run_environment_statements: execute_environment_statements( @@ -604,7 +619,7 @@ def _run_or_audit( if not merged_intervals: return CompletionStatus.NOTHING_TO_DO - errors, skipped_intervals = self.run_merged_intervals( + errors, _ = self.run_merged_intervals( merged_intervals=merged_intervals, deployability_index=deployability_index, environment_naming_info=environment_naming_info, @@ -616,20 +631,6 @@ def _run_or_audit( audit_only=audit_only, ) - self.console.stop_evaluation_progress(success=not errors) - - skipped_snapshots = {i[0] for i in skipped_intervals} - self.console.log_skipped_models(skipped_snapshots) - for skipped in skipped_snapshots: - logger.info(f"SKIPPED snapshot {skipped}\n") - - for error in errors: - if isinstance(error.__cause__, CircuitBreakerError): - raise error.__cause__ - logger.info(str(error), exc_info=error) - - self.console.log_failed_models(errors) - return CompletionStatus.FAILURE if errors else CompletionStatus.SUCCESS def _audit_snapshot( diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index 05d464304f..4f46ccf9b8 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -108,6 +108,17 @@ def snapshots_exist(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> t.Set[Sna A set of all the existing snapshot ids. """ + @abc.abstractmethod + def refresh_snapshot_intervals(self, snapshots: t.Collection[Snapshot]) -> t.List[Snapshot]: + """Updates given snapshots with latest intervals from the state. + + Args: + snapshots: The snapshots to refresh. + + Returns: + The updated snapshots. + """ + @abc.abstractmethod def nodes_exist(self, names: t.Iterable[str], exclude_external: bool = False) -> t.Set[str]: """Returns the node names that exist in the state sync. @@ -373,17 +384,6 @@ def remove_intervals( remove_shared_versions: Whether to remove intervals for snapshots that share the same version with the target snapshots. """ - @abc.abstractmethod - def refresh_snapshot_intervals(self, snapshots: t.Collection[Snapshot]) -> t.List[Snapshot]: - """Updates given snapshots with latest intervals from the state. - - Args: - snapshots: The snapshots to refresh. - - Returns: - The updated snapshots. - """ - @abc.abstractmethod def promote( self, diff --git a/sqlmesh/utils/__init__.py b/sqlmesh/utils/__init__.py index 56e3cdf352..f102f23292 100644 --- a/sqlmesh/utils/__init__.py +++ b/sqlmesh/utils/__init__.py @@ -376,3 +376,9 @@ def is_failure(self) -> bool: @property def is_nothing_to_do(self) -> bool: return self == CompletionStatus.NOTHING_TO_DO + + +def to_snake_case(name: str) -> str: + return "".join( + f"_{c.lower()}" if c.isupper() and idx != 0 else c.lower() for idx, c in enumerate(name) + ) diff --git a/tests/conftest.py b/tests/conftest.py index 54f8db0418..492da9db58 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,14 +33,14 @@ from sqlmesh.core.macros import macro from sqlmesh.core.model import IncrementalByTimeRangeKind, SqlModel, model from sqlmesh.core.model.kind import OnDestructiveChange -from sqlmesh.core.plan import BuiltInPlanEvaluator, Plan +from sqlmesh.core.plan import BuiltInPlanEvaluator, Plan, stages as plan_stages from sqlmesh.core.snapshot import ( + DeployabilityIndex, Node, Snapshot, SnapshotChangeCategory, SnapshotDataVersion, SnapshotFingerprint, - DeployabilityIndex, ) from sqlmesh.utils import random_id from sqlmesh.utils.date import TimeLike, to_date @@ -258,11 +258,23 @@ def push_plan(context: Context, plan: Plan) -> None: context.default_catalog, ) deployability_index = DeployabilityIndex.create(context.snapshots.values()) - plan_evaluator._push(plan.to_evaluatable(), plan.snapshots, deployability_index) - promotion_result = plan_evaluator._promote(plan.to_evaluatable(), plan.snapshots) - plan_evaluator._update_views( - plan.to_evaluatable(), plan.snapshots, promotion_result, deployability_index + evaluatable_plan = plan.to_evaluatable() + stages = plan_stages.build_plan_stages( + evaluatable_plan, context.state_sync, context.default_catalog ) + for stage in stages: + if isinstance(stage, plan_stages.CreateSnapshotRecordsStage): + plan_evaluator.visit_create_snapshot_records_stage(stage, evaluatable_plan) + elif isinstance(stage, plan_stages.PhysicalLayerUpdateStage): + stage.deployability_index = deployability_index + plan_evaluator.visit_physical_layer_update_stage(stage, evaluatable_plan) + elif isinstance(stage, plan_stages.EnvironmentRecordUpdateStage): + plan_evaluator.visit_environment_record_update_stage(stage, evaluatable_plan) + elif isinstance(stage, plan_stages.VirtualLayerUpdateStage): + stage.deployability_index = deployability_index + plan_evaluator.visit_virtual_layer_update_stage(stage, evaluatable_plan) + elif isinstance(stage, plan_stages.FinalizeEnvironmentStage): + plan_evaluator.visit_finalize_environment_stage(stage, evaluatable_plan) @pytest.fixture() diff --git a/tests/core/test_context.py b/tests/core/test_context.py index e88184c2e9..3ce1e48893 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -41,7 +41,6 @@ from sqlmesh.core.model.cache import OptimizedQueryCache from sqlmesh.core.renderer import render_statements from sqlmesh.core.model.kind import ModelKindName -from sqlmesh.core.plan import BuiltInPlanEvaluator, PlanBuilder from sqlmesh.core.state_sync.cache import CachingStateSync from sqlmesh.core.state_sync.db import EngineAdapterStateSync from sqlmesh.utils.connection_pool import SingletonConnectionPool, ThreadLocalSharedConnectionPool @@ -272,24 +271,6 @@ def test_diff(sushi_context: Context, mocker: MockerFixture): yesterday = yesterday_ds() success = sushi_context.run(start=yesterday, end=yesterday) - plan_evaluator = BuiltInPlanEvaluator( - sushi_context.state_sync, - sushi_context.snapshot_evaluator, - sushi_context.create_scheduler, - sushi_context.default_catalog, - ) - - plan = PlanBuilder( - context_diff=sushi_context._context_diff("prod"), - ).build() - - # stringify used to trigger an unhashable exception due to - # https://github.com/pydantic/pydantic/issues/8016 - assert str(plan) != "" - - promotion_result = plan_evaluator._promote(plan.to_evaluatable(), plan.snapshots) - plan_evaluator._update_views(plan.to_evaluatable(), plan.snapshots, promotion_result) - sushi_context.upsert_model("sushi.customers", query=parse_one("select 1 as customer_id")) sushi_context.diff("test") assert mock_console.show_environment_difference_summary.called diff --git a/tests/core/test_plan_evaluator.py b/tests/core/test_plan_evaluator.py index ae113e87e3..a784644b6b 100644 --- a/tests/core/test_plan_evaluator.py +++ b/tests/core/test_plan_evaluator.py @@ -8,10 +8,9 @@ BuiltInPlanEvaluator, Plan, PlanBuilder, - update_intervals_for_new_snapshots, + stages as plan_stages, ) from sqlmesh.core.snapshot import SnapshotChangeCategory -from sqlmesh.utils.date import to_timestamp @pytest.fixture @@ -65,7 +64,14 @@ def test_builtin_evaluator_push(sushi_context: Context, make_snapshot): sushi_context.default_catalog, console=sushi_context.console, ) - evaluator._push(plan.to_evaluatable(), plan.snapshots) + evaluatable_plan = plan.to_evaluatable() + stages = plan_stages.build_plan_stages( + evaluatable_plan, sushi_context.state_sync, sushi_context.default_catalog + ) + assert isinstance(stages[0], plan_stages.CreateSnapshotRecordsStage) + evaluator.visit_create_snapshot_records_stage(stages[0], evaluatable_plan) + assert isinstance(stages[1], plan_stages.PhysicalLayerUpdateStage) + evaluator.visit_physical_layer_update_stage(stages[1], evaluatable_plan) assert ( len(sushi_context.state_sync.get_snapshots([new_model_snapshot, new_view_model_snapshot])) @@ -73,38 +79,3 @@ def test_builtin_evaluator_push(sushi_context: Context, make_snapshot): ) assert sushi_context.engine_adapter.table_exists(new_model_snapshot.table_name()) assert sushi_context.engine_adapter.table_exists(new_view_model_snapshot.table_name()) - - -@pytest.mark.parametrize( - "change_category", [SnapshotChangeCategory.BREAKING, SnapshotChangeCategory.FORWARD_ONLY] -) -def test_update_intervals_for_new_snapshots( - sushi_context: Context, - mocker: MockerFixture, - change_category: SnapshotChangeCategory, - make_snapshot, -): - model = SqlModel( - name="sushi.new_test_model", - query=parse_one("SELECT 1::INT AS one"), - ) - snapshot = make_snapshot(model) - snapshot.categorize_as(change_category) - - snapshot.add_interval("2023-01-01", "2023-01-01") - - state_sync_mock = mocker.Mock() - state_sync_mock.refresh_snapshot_intervals.return_value = [snapshot] - - update_intervals_for_new_snapshots([snapshot], state_sync_mock) - - state_sync_mock.refresh_snapshot_intervals.assert_called_once_with([snapshot]) - - if change_category == SnapshotChangeCategory.FORWARD_ONLY: - assert snapshot.dev_intervals == [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] - expected_intervals = snapshot.snapshot_intervals - expected_intervals.intervals.clear() - state_sync_mock.add_snapshots_intervals.assert_called_once_with([expected_intervals]) - else: - assert not snapshot.dev_intervals - state_sync_mock.add_snapshots_intervals.assert_not_called() diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index 8c9cb9acc0..5fa140cb6a 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -3,6 +3,7 @@ from sqlglot import parse_one from pytest_mock.plugin import MockerFixture +from sqlmesh.core.config import EnvironmentSuffixTarget from sqlmesh.core.model import SqlModel, ModelKindName from sqlmesh.core.plan.definition import EvaluatablePlan from sqlmesh.core.plan.stages import ( @@ -10,12 +11,15 @@ AfterAllStage, AuditOnlyRunStage, PhysicalLayerUpdateStage, + CreateSnapshotRecordsStage, BeforeAllStage, BackfillStage, EnvironmentRecordUpdateStage, VirtualLayerUpdateStage, RestatementStage, MigrateSchemasStage, + FinalizeEnvironmentStage, + UnpauseStage, ) from sqlmesh.core.snapshot.definition import ( SnapshotChangeCategory, @@ -55,6 +59,20 @@ def snapshot_b(make_snapshot, snapshot_a: Snapshot) -> Snapshot: return snapshot +@pytest.fixture +def snapshot_c(make_snapshot, snapshot_a: Snapshot) -> Snapshot: + snapshot = make_snapshot( + SqlModel( + name="c", + query=parse_one("select * from a"), + kind=dict(name=ModelKindName.VIEW), + ), + nodes={'"a"': snapshot_a.model}, + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + return snapshot + + def test_build_plan_stages_basic( snapshot_a: Snapshot, snapshot_b: Snapshot, mocker: MockerFixture ) -> None: @@ -104,10 +122,18 @@ def test_build_plan_stages_basic( stages = build_plan_stages(plan, state_reader, None) # Verify stages - assert len(stages) == 4 + assert len(stages) == 7 + # Verify CreateSnapshotRecordsStage + create_snapshot_records_stage = stages[0] + assert isinstance(create_snapshot_records_stage, CreateSnapshotRecordsStage) + assert len(create_snapshot_records_stage.snapshots) == 2 + assert {s.snapshot_id for s in create_snapshot_records_stage.snapshots} == { + snapshot_a.snapshot_id, + snapshot_b.snapshot_id, + } # Verify PhysicalLayerUpdateStage - physical_stage = stages[0] + physical_stage = stages[1] assert isinstance(physical_stage, PhysicalLayerUpdateStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { @@ -117,7 +143,7 @@ def test_build_plan_stages_basic( assert physical_stage.deployability_index == DeployabilityIndex.all_deployable() # Verify BackfillStage - backfill_stage = stages[1] + backfill_stage = stages[2] assert isinstance(backfill_stage, BackfillStage) assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() expected_interval = (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")) @@ -126,14 +152,23 @@ def test_build_plan_stages_basic( assert backfill_stage.snapshot_to_intervals[snapshot_b] == [expected_interval] # Verify EnvironmentRecordUpdateStage - assert isinstance(stages[2], EnvironmentRecordUpdateStage) + assert isinstance(stages[3], EnvironmentRecordUpdateStage) + assert stages[3].no_gaps_snapshot_names == {snapshot_a.name, snapshot_b.name} + + # Verify UnpauseStage + assert isinstance(stages[4], UnpauseStage) + assert {s.name for s in stages[4].promoted_snapshots} == {snapshot_a.name, snapshot_b.name} # Verify VirtualLayerUpdateStage - virtual_stage = stages[3] + virtual_stage = stages[5] assert isinstance(virtual_stage, VirtualLayerUpdateStage) assert len(virtual_stage.promoted_snapshots) == 2 assert len(virtual_stage.demoted_snapshots) == 0 - assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} + assert {s.name for s in virtual_stage.promoted_snapshots} == {snapshot_a.name, snapshot_b.name} + + state_reader.refresh_snapshot_intervals.assert_called_once() + + assert isinstance(stages[6], FinalizeEnvironmentStage) def test_build_plan_stages_with_before_all_and_after_all( @@ -154,6 +189,15 @@ def test_build_plan_stages_with_before_all_and_after_all( promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], ) + environment_statements = [ + EnvironmentStatements( + before_all=["BEFORE ALL A", "BEFORE ALL B"], + after_all=["AFTER ALL A", "AFTER ALL B"], + python_env={}, + jinja_macros=None, + ) + ] + # Create evaluatable plan plan = EvaluatablePlan( start="2023-01-01", @@ -177,14 +221,7 @@ def test_build_plan_stages_with_before_all_and_after_all( interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), - environment_statements=[ - EnvironmentStatements( - before_all=["BEFORE ALL A", "BEFORE ALL B"], - after_all=["AFTER ALL A", "AFTER ALL B"], - python_env={}, - jinja_macros=None, - ) - ], + environment_statements=environment_statements, user_provided_flags=None, ) @@ -192,15 +229,24 @@ def test_build_plan_stages_with_before_all_and_after_all( stages = build_plan_stages(plan, state_reader, None) # Verify stages - assert len(stages) == 6 + assert len(stages) == 9 # Verify BeforeAllStage before_all_stage = stages[0] assert isinstance(before_all_stage, BeforeAllStage) - assert before_all_stage.statements == ["BEFORE ALL A", "BEFORE ALL B"] + assert before_all_stage.statements == environment_statements + + # Verify CreateSnapshotRecordsStage + create_snapshot_records_stage = stages[1] + assert isinstance(create_snapshot_records_stage, CreateSnapshotRecordsStage) + assert len(create_snapshot_records_stage.snapshots) == 2 + assert {s.snapshot_id for s in create_snapshot_records_stage.snapshots} == { + snapshot_a.snapshot_id, + snapshot_b.snapshot_id, + } # Verify PhysicalLayerUpdateStage - physical_stage = stages[1] + physical_stage = stages[2] assert isinstance(physical_stage, PhysicalLayerUpdateStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { @@ -210,7 +256,7 @@ def test_build_plan_stages_with_before_all_and_after_all( assert physical_stage.deployability_index == DeployabilityIndex.all_deployable() # Verify BackfillStage - backfill_stage = stages[2] + backfill_stage = stages[3] assert isinstance(backfill_stage, BackfillStage) assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() expected_interval = (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")) @@ -219,19 +265,27 @@ def test_build_plan_stages_with_before_all_and_after_all( assert backfill_stage.snapshot_to_intervals[snapshot_b] == [expected_interval] # Verify EnvironmentRecordUpdateStage - assert isinstance(stages[3], EnvironmentRecordUpdateStage) + assert isinstance(stages[4], EnvironmentRecordUpdateStage) + assert stages[4].no_gaps_snapshot_names == {snapshot_a.name, snapshot_b.name} + + # Verify UnpauseStage + assert isinstance(stages[5], UnpauseStage) + assert {s.name for s in stages[5].promoted_snapshots} == {snapshot_a.name, snapshot_b.name} # Verify VirtualLayerUpdateStage - virtual_stage = stages[4] + virtual_stage = stages[6] assert isinstance(virtual_stage, VirtualLayerUpdateStage) assert len(virtual_stage.promoted_snapshots) == 2 assert len(virtual_stage.demoted_snapshots) == 0 assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} + # Verify FinalizeEnvironmentStage + assert isinstance(stages[7], FinalizeEnvironmentStage) + # Verify AfterAllStage - after_all_stage = stages[5] + after_all_stage = stages[8] assert isinstance(after_all_stage, AfterAllStage) - assert after_all_stage.statements == ["AFTER ALL A", "AFTER ALL B"] + assert after_all_stage.statements == environment_statements def test_build_plan_stages_select_models( @@ -283,17 +337,26 @@ def test_build_plan_stages_select_models( stages = build_plan_stages(plan, state_reader, None) # Verify stages - assert len(stages) == 4 + assert len(stages) == 7 + + # Verify CreateSnapshotRecordsStage + create_snapshot_records_stage = stages[0] + assert isinstance(create_snapshot_records_stage, CreateSnapshotRecordsStage) + assert len(create_snapshot_records_stage.snapshots) == 2 + assert {s.snapshot_id for s in create_snapshot_records_stage.snapshots} == { + snapshot_a.snapshot_id, + snapshot_b.snapshot_id, + } # Verify PhysicalLayerUpdateStage - physical_stage = stages[0] + physical_stage = stages[1] assert isinstance(physical_stage, PhysicalLayerUpdateStage) assert len(physical_stage.snapshots) == 1 assert {s.snapshot_id for s in physical_stage.snapshots} == {snapshot_a.snapshot_id} assert physical_stage.deployability_index == DeployabilityIndex.all_deployable() # Verify BackfillStage - backfill_stage = stages[1] + backfill_stage = stages[2] assert isinstance(backfill_stage, BackfillStage) assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() expected_interval = (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")) @@ -301,15 +364,23 @@ def test_build_plan_stages_select_models( assert backfill_stage.snapshot_to_intervals[snapshot_a] == [expected_interval] # Verify EnvironmentRecordUpdateStage - assert isinstance(stages[2], EnvironmentRecordUpdateStage) + assert isinstance(stages[3], EnvironmentRecordUpdateStage) + assert stages[3].no_gaps_snapshot_names == {snapshot_a.name} + + # Verify UnpauseStage + assert isinstance(stages[4], UnpauseStage) + assert {s.name for s in stages[4].promoted_snapshots} == {snapshot_a.name} # Verify VirtualLayerUpdateStage - virtual_stage = stages[3] + virtual_stage = stages[5] assert isinstance(virtual_stage, VirtualLayerUpdateStage) assert len(virtual_stage.promoted_snapshots) == 1 assert len(virtual_stage.demoted_snapshots) == 0 assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"'} + # Verify FinalizeEnvironmentStage + assert isinstance(stages[6], FinalizeEnvironmentStage) + @pytest.mark.parametrize("skip_backfill,empty_backfill", [(True, False), (False, True)]) def test_build_plan_stages_basic_no_backfill( @@ -365,10 +436,18 @@ def test_build_plan_stages_basic_no_backfill( stages = build_plan_stages(plan, state_reader, None) # Verify stages - assert len(stages) == 4 + assert len(stages) == 7 + # Verify CreateSnapshotRecordsStage + create_snapshot_records_stage = stages[0] + assert isinstance(create_snapshot_records_stage, CreateSnapshotRecordsStage) + assert len(create_snapshot_records_stage.snapshots) == 2 + assert {s.snapshot_id for s in create_snapshot_records_stage.snapshots} == { + snapshot_a.snapshot_id, + snapshot_b.snapshot_id, + } # Verify PhysicalLayerUpdateStage - physical_stage = stages[0] + physical_stage = stages[1] assert isinstance(physical_stage, PhysicalLayerUpdateStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { @@ -377,21 +456,29 @@ def test_build_plan_stages_basic_no_backfill( } # Verify BackfillStage - backfill_stage = stages[1] + backfill_stage = stages[2] assert isinstance(backfill_stage, BackfillStage) assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() assert backfill_stage.snapshot_to_intervals == {} # Verify EnvironmentRecordUpdateStage - assert isinstance(stages[2], EnvironmentRecordUpdateStage) + assert isinstance(stages[3], EnvironmentRecordUpdateStage) + assert stages[3].no_gaps_snapshot_names == {snapshot_a.name, snapshot_b.name} + + # Verify UnpauseStage + assert isinstance(stages[4], UnpauseStage) + assert {s.name for s in stages[4].promoted_snapshots} == {snapshot_a.name, snapshot_b.name} # Verify VirtualLayerUpdateStage - virtual_stage = stages[3] + virtual_stage = stages[5] assert isinstance(virtual_stage, VirtualLayerUpdateStage) assert len(virtual_stage.promoted_snapshots) == 2 assert len(virtual_stage.demoted_snapshots) == 0 assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} + # Verify FinalizeEnvironmentStage + assert isinstance(stages[6], FinalizeEnvironmentStage) + def test_build_plan_stages_restatement( snapshot_a: Snapshot, snapshot_b: Snapshot, mocker: MockerFixture @@ -458,7 +545,7 @@ def test_build_plan_stages_restatement( stages = build_plan_stages(plan, state_reader, None) # Verify stages - assert len(stages) == 4 + assert len(stages) == 5 # Verify PhysicalLayerUpdateStage physical_stage = stages[0] @@ -490,6 +577,9 @@ def test_build_plan_stages_restatement( # Verify EnvironmentRecordUpdateStage assert isinstance(stages[3], EnvironmentRecordUpdateStage) + # Verify FinalizeEnvironmentStage + assert isinstance(stages[4], FinalizeEnvironmentStage) + def test_build_plan_stages_forward_only( snapshot_a: Snapshot, snapshot_b: Snapshot, make_snapshot, mocker: MockerFixture @@ -566,10 +656,19 @@ def test_build_plan_stages_forward_only( stages = build_plan_stages(plan, state_reader, None) # Verify stages - assert len(stages) == 5 + assert len(stages) == 8 + + # Verify CreateSnapshotRecordsStage + create_snapshot_records_stage = stages[0] + assert isinstance(create_snapshot_records_stage, CreateSnapshotRecordsStage) + assert len(create_snapshot_records_stage.snapshots) == 2 + assert {s.snapshot_id for s in create_snapshot_records_stage.snapshots} == { + new_snapshot_a.snapshot_id, + new_snapshot_b.snapshot_id, + } # Verify PhysicalLayerUpdateStage - physical_stage = stages[0] + physical_stage = stages[1] assert isinstance(physical_stage, PhysicalLayerUpdateStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { @@ -581,10 +680,11 @@ def test_build_plan_stages_forward_only( ) # Verify EnvironmentRecordUpdateStage - assert isinstance(stages[1], EnvironmentRecordUpdateStage) + assert isinstance(stages[2], EnvironmentRecordUpdateStage) + assert stages[2].no_gaps_snapshot_names == set() # Verify MigrateSchemasStage - migrate_stage = stages[2] + migrate_stage = stages[3] assert isinstance(migrate_stage, MigrateSchemasStage) assert len(migrate_stage.snapshots) == 2 assert {s.snapshot_id for s in migrate_stage.snapshots} == { @@ -592,8 +692,15 @@ def test_build_plan_stages_forward_only( new_snapshot_b.snapshot_id, } + # Verify UnpauseStage + assert isinstance(stages[4], UnpauseStage) + assert {s.name for s in stages[4].promoted_snapshots} == { + new_snapshot_a.name, + new_snapshot_b.name, + } + # Verify BackfillStage - backfill_stage = stages[3] + backfill_stage = stages[5] assert isinstance(backfill_stage, BackfillStage) assert len(backfill_stage.snapshot_to_intervals) == 2 expected_interval = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] @@ -602,12 +709,15 @@ def test_build_plan_stages_forward_only( assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() # Verify VirtualLayerUpdateStage - virtual_stage = stages[4] + virtual_stage = stages[6] assert isinstance(virtual_stage, VirtualLayerUpdateStage) assert len(virtual_stage.promoted_snapshots) == 2 assert len(virtual_stage.demoted_snapshots) == 0 assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} + # Verify FinalizeEnvironmentStage + assert isinstance(stages[7], FinalizeEnvironmentStage) + def test_build_plan_stages_forward_only_dev( snapshot_a: Snapshot, snapshot_b: Snapshot, make_snapshot, mocker: MockerFixture @@ -674,10 +784,19 @@ def test_build_plan_stages_forward_only_dev( stages = build_plan_stages(plan, state_reader, None) # Verify stages - assert len(stages) == 4 + assert len(stages) == 6 + + # Verify CreateSnapshotRecordsStage + create_snapshot_records_stage = stages[0] + assert isinstance(create_snapshot_records_stage, CreateSnapshotRecordsStage) + assert len(create_snapshot_records_stage.snapshots) == 2 + assert {s.snapshot_id for s in create_snapshot_records_stage.snapshots} == { + new_snapshot_a.snapshot_id, + new_snapshot_b.snapshot_id, + } # Verify PhysicalLayerUpdateStage - physical_stage = stages[0] + physical_stage = stages[1] assert isinstance(physical_stage, PhysicalLayerUpdateStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { @@ -689,7 +808,7 @@ def test_build_plan_stages_forward_only_dev( ) # Verify BackfillStage - backfill_stage = stages[1] + backfill_stage = stages[2] assert isinstance(backfill_stage, BackfillStage) assert len(backfill_stage.snapshot_to_intervals) == 2 expected_interval = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] @@ -700,15 +819,18 @@ def test_build_plan_stages_forward_only_dev( ) # Verify EnvironmentRecordUpdateStage - assert isinstance(stages[2], EnvironmentRecordUpdateStage) + assert isinstance(stages[3], EnvironmentRecordUpdateStage) # Verify VirtualLayerUpdateStage - virtual_stage = stages[3] + virtual_stage = stages[4] assert isinstance(virtual_stage, VirtualLayerUpdateStage) assert len(virtual_stage.promoted_snapshots) == 2 assert len(virtual_stage.demoted_snapshots) == 0 assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} + # Verify FinalizeEnvironmentStage + assert isinstance(stages[5], FinalizeEnvironmentStage) + def test_build_plan_stages_audit_only( snapshot_a: Snapshot, snapshot_b: Snapshot, make_snapshot, mocker: MockerFixture @@ -785,30 +907,478 @@ def _get_snapshots(snapshot_ids: t.List[SnapshotId]) -> t.Dict[SnapshotId, Snaps stages = build_plan_stages(plan, state_reader, None) # Verify stages - assert len(stages) == 5 + assert len(stages) == 7 + + # Verify CreateSnapshotRecordsStage + create_snapshot_records_stage = stages[0] + assert isinstance(create_snapshot_records_stage, CreateSnapshotRecordsStage) + assert len(create_snapshot_records_stage.snapshots) == 2 + assert {s.snapshot_id for s in create_snapshot_records_stage.snapshots} == { + new_snapshot_a.snapshot_id, + new_snapshot_b.snapshot_id, + } # Verify PhysicalLayerUpdateStage - physical_stage = stages[0] + physical_stage = stages[1] assert isinstance(physical_stage, PhysicalLayerUpdateStage) - assert len(physical_stage.snapshots) == 0 + assert len(physical_stage.snapshots) == 2 + assert {s.snapshot_id for s in physical_stage.snapshots} == { + new_snapshot_a.snapshot_id, + new_snapshot_b.snapshot_id, + } + assert physical_stage.deployability_index == DeployabilityIndex.create( + [new_snapshot_a, new_snapshot_b] + ) # Verify AuditOnlyRunStage - audit_only_stage = stages[1] + audit_only_stage = stages[2] assert isinstance(audit_only_stage, AuditOnlyRunStage) assert len(audit_only_stage.snapshots) == 1 assert audit_only_stage.snapshots[0].snapshot_id == new_snapshot_a.snapshot_id # Verify BackfillStage - backfill_stage = stages[2] + backfill_stage = stages[3] assert isinstance(backfill_stage, BackfillStage) assert len(backfill_stage.snapshot_to_intervals) == 0 # Verify EnvironmentRecordUpdateStage - assert isinstance(stages[3], EnvironmentRecordUpdateStage) + assert isinstance(stages[4], EnvironmentRecordUpdateStage) # Verify VirtualLayerUpdateStage - virtual_stage = stages[4] + virtual_stage = stages[5] assert isinstance(virtual_stage, VirtualLayerUpdateStage) assert len(virtual_stage.promoted_snapshots) == 2 assert len(virtual_stage.demoted_snapshots) == 0 assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} + + # Verify FinalizeEnvironmentStage + assert isinstance(stages[6], FinalizeEnvironmentStage) + + +def test_build_plan_stages_forward_only_ensure_finalized_snapshots( + snapshot_a: Snapshot, snapshot_b: Snapshot, make_snapshot, mocker: MockerFixture +) -> None: + # Categorize snapshot_a as forward-only + new_snapshot_a = make_snapshot( + snapshot_a.model.copy(update={"stamp": "new_version"}), + ) + new_snapshot_a.previous_versions = snapshot_a.all_versions + new_snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + + new_snapshot_b = make_snapshot( + snapshot_b.model.copy(), + nodes={'"a"': new_snapshot_a.model}, + ) + new_snapshot_b.previous_versions = snapshot_b.all_versions + new_snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = {} + existing_environment = Environment( + name="prod", + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + state_reader.get_environment.return_value = existing_environment + + # Create environment + environment = Environment( + name="prod", + snapshots=[new_snapshot_a.table_info, new_snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id="previous_plan", + promoted_snapshot_ids=[new_snapshot_a.snapshot_id, new_snapshot_b.snapshot_id], + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[new_snapshot_a, new_snapshot_b], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=True, + directly_modified_snapshots=[new_snapshot_a.snapshot_id], + indirectly_modified_snapshots={ + new_snapshot_a.name: [new_snapshot_b.snapshot_id], + }, + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + assert len(stages) == 8 + assert isinstance(stages[0], CreateSnapshotRecordsStage) + assert isinstance(stages[1], PhysicalLayerUpdateStage) + assert isinstance(stages[2], EnvironmentRecordUpdateStage) + assert isinstance(stages[3], MigrateSchemasStage) + assert isinstance(stages[4], BackfillStage) + assert isinstance(stages[5], UnpauseStage) + assert isinstance(stages[6], VirtualLayerUpdateStage) + assert isinstance(stages[7], FinalizeEnvironmentStage) + + +def test_build_plan_stages_removed_model( + snapshot_a: Snapshot, snapshot_b: Snapshot, mocker: MockerFixture +) -> None: + # Mock state reader + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = { + snapshot_a.snapshot_id: snapshot_a, + snapshot_b.snapshot_id: snapshot_b, + } + existing_environment = Environment( + name="prod", + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + state_reader.get_environment.return_value = existing_environment + + # Create environment + environment = Environment( + snapshots=[snapshot_a.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id="previous_plan", + promoted_snapshot_ids=[snapshot_a.snapshot_id], + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[], + indirectly_modified_snapshots={}, + removed_snapshots=[snapshot_b.snapshot_id], + requires_backfill=False, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 5 + + assert isinstance(stages[0], PhysicalLayerUpdateStage) + assert isinstance(stages[1], BackfillStage) + assert isinstance(stages[2], EnvironmentRecordUpdateStage) + assert isinstance(stages[3], VirtualLayerUpdateStage) + assert isinstance(stages[4], FinalizeEnvironmentStage) + + virtual_layer_update_stage = stages[3] + assert virtual_layer_update_stage.promoted_snapshots == set() + assert virtual_layer_update_stage.demoted_snapshots == {snapshot_b.table_info} + assert ( + virtual_layer_update_stage.demoted_environment_naming_info + == existing_environment.naming_info + ) + + +def test_build_plan_stages_environment_suffix_target_changed( + snapshot_a: Snapshot, snapshot_b: Snapshot, mocker: MockerFixture +) -> None: + # Mock state reader + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = { + snapshot_a.snapshot_id: snapshot_a, + snapshot_b.snapshot_id: snapshot_b, + } + existing_environment = Environment( + name="dev", + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + state_reader.get_environment.return_value = existing_environment + + # Create environment + environment = Environment( + name="dev", + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id="previous_plan", + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + suffix_target=EnvironmentSuffixTarget.TABLE, + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=True, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[], + indirectly_modified_snapshots={}, + removed_snapshots=[], + requires_backfill=False, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 5 + + assert isinstance(stages[0], PhysicalLayerUpdateStage) + assert isinstance(stages[1], BackfillStage) + assert isinstance(stages[2], EnvironmentRecordUpdateStage) + assert isinstance(stages[3], VirtualLayerUpdateStage) + assert isinstance(stages[4], FinalizeEnvironmentStage) + + virtual_layer_update_stage = stages[3] + assert virtual_layer_update_stage.promoted_snapshots == { + snapshot_a.table_info, + snapshot_b.table_info, + } + assert virtual_layer_update_stage.demoted_snapshots == { + snapshot_a.table_info, + snapshot_b.table_info, + } + assert ( + virtual_layer_update_stage.demoted_environment_naming_info + == existing_environment.naming_info + ) + + +def test_build_plan_stages_indirect_non_breaking_no_migration( + snapshot_a: Snapshot, snapshot_b: Snapshot, make_snapshot, mocker: MockerFixture +) -> None: + # Categorize snapshot_a as forward-only + new_snapshot_a = make_snapshot( + snapshot_a.model.copy(update={"stamp": "new_version"}), + ) + new_snapshot_a.previous_versions = snapshot_a.all_versions + new_snapshot_a.categorize_as(SnapshotChangeCategory.NON_BREAKING) + + new_snapshot_b = make_snapshot( + snapshot_b.model.copy(), + nodes={'"a"': new_snapshot_a.model}, + ) + new_snapshot_b.previous_versions = snapshot_b.all_versions + new_snapshot_b.change_category = SnapshotChangeCategory.INDIRECT_NON_BREAKING + new_snapshot_b.version = new_snapshot_b.previous_version.data_version.version + + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = {} + existing_environment = Environment( + name="prod", + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + state_reader.get_environment.return_value = existing_environment + + # Create environment + environment = Environment( + name="prod", + snapshots=[new_snapshot_a.table_info, new_snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id="previous_plan", + promoted_snapshot_ids=[new_snapshot_a.snapshot_id, new_snapshot_b.snapshot_id], + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[new_snapshot_a, new_snapshot_b], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[new_snapshot_a.snapshot_id], + indirectly_modified_snapshots={ + new_snapshot_a.name: [new_snapshot_b.snapshot_id], + }, + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 7 + + assert isinstance(stages[0], CreateSnapshotRecordsStage) + assert isinstance(stages[1], PhysicalLayerUpdateStage) + assert isinstance(stages[2], BackfillStage) + assert isinstance(stages[3], EnvironmentRecordUpdateStage) + assert isinstance(stages[4], UnpauseStage) + assert isinstance(stages[5], VirtualLayerUpdateStage) + assert isinstance(stages[6], FinalizeEnvironmentStage) + + +def test_build_plan_stages_indirect_non_breaking_view_migration( + snapshot_a: Snapshot, snapshot_c: Snapshot, make_snapshot, mocker: MockerFixture +) -> None: + # Categorize snapshot_a as forward-only + new_snapshot_a = make_snapshot( + snapshot_a.model.copy(update={"stamp": "new_version"}), + ) + new_snapshot_a.previous_versions = snapshot_a.all_versions + new_snapshot_a.categorize_as(SnapshotChangeCategory.NON_BREAKING) + + new_snapshot_c = make_snapshot( + snapshot_c.model.copy(), + nodes={'"a"': new_snapshot_a.model}, + ) + new_snapshot_c.previous_versions = snapshot_c.all_versions + new_snapshot_c.change_category = SnapshotChangeCategory.INDIRECT_NON_BREAKING + new_snapshot_c.version = new_snapshot_c.previous_version.data_version.version + + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = {} + existing_environment = Environment( + name="prod", + snapshots=[snapshot_a.table_info, snapshot_c.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_c.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + state_reader.get_environment.return_value = existing_environment + + # Create environment + environment = Environment( + name="prod", + snapshots=[new_snapshot_a.table_info, new_snapshot_c.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id="previous_plan", + promoted_snapshot_ids=[new_snapshot_a.snapshot_id, new_snapshot_c.snapshot_id], + ) + + # Create evaluatable plan + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[new_snapshot_a, new_snapshot_c], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[new_snapshot_a.snapshot_id], + indirectly_modified_snapshots={ + new_snapshot_a.name: [new_snapshot_c.snapshot_id], + }, + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + interval_end_per_model=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 8 + + assert isinstance(stages[0], CreateSnapshotRecordsStage) + assert isinstance(stages[1], PhysicalLayerUpdateStage) + assert isinstance(stages[2], BackfillStage) + assert isinstance(stages[3], EnvironmentRecordUpdateStage) + assert isinstance(stages[4], MigrateSchemasStage) + assert isinstance(stages[5], UnpauseStage) + assert isinstance(stages[6], VirtualLayerUpdateStage) + assert isinstance(stages[7], FinalizeEnvironmentStage) + + migrate_schemas_stage = stages[4] + assert {s.snapshot_id for s in migrate_schemas_stage.snapshots} == {new_snapshot_c.snapshot_id} From d4cc990ac32407247a0f4663c0316c916c53b12f Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 16 Jun 2025 21:11:28 +0200 Subject: [PATCH 0404/1056] docs: duckdb vscode warning (#4746) Co-authored-by: Trey Spiller <1831878+treysp@users.noreply.github.com> --- docs/guides/vscode.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/guides/vscode.md b/docs/guides/vscode.md index f21bd2c165..fa6245ace1 100644 --- a/docs/guides/vscode.md +++ b/docs/guides/vscode.md @@ -139,6 +139,16 @@ The SQLMesh VSCode extension provides the following commands in the VSCode comma ## Troubleshooting +### DuckDB concurrent access + +If your SQLMesh project uses DuckDB to store its state, you will likely encounter problems. + +SQLMesh can create multiple connections to the state database, but DuckDB's local database file does not support concurrent access. + +Because the VSCode extension establishes a long-running process connected to the database, access conflicts are more likely than with standard SQLMesh usage from the CLI. + +Therefore, we do not recommend using DuckDB as a state store with the VSCode extension. + ### Python environment woes The most common problem is the extension not using the correct Python interpreter. From 7e5f19570c7ffb47ded283357433bee44cc93461 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 16 Jun 2025 14:35:44 -0700 Subject: [PATCH 0405/1056] Chore: Allow --run argument for non-prod environments in dev (#4747) --- sqlmesh/core/context.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index f919c51182..dc2eba42d9 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1430,9 +1430,6 @@ def plan_builder( "When targeting the production environment either the backfill should not be skipped or the lack of data gaps should be enforced (--no-gaps flag)." ) - if run and is_dev: - raise ConfigError("The '--run' flag is only supported for the production environment.") - if not skip_linter: self.lint_models() From 976ffee5329c46cbac9a5d480004db4bb440d272 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 17 Jun 2025 09:46:30 +1200 Subject: [PATCH 0406/1056] Feat(experimental): DBT project conversion (#4495) --- sqlmesh/cli/main.py | 37 ++ sqlmesh/core/config/root.py | 9 +- sqlmesh/core/constants.py | 3 + sqlmesh/core/loader.py | 109 +++- sqlmesh/core/model/definition.py | 59 +- sqlmesh/core/model/kind.py | 5 +- sqlmesh/core/renderer.py | 1 + sqlmesh/dbt/adapter.py | 3 + sqlmesh/dbt/builtin.py | 2 +- sqlmesh/dbt/converter/__init__.py | 0 sqlmesh/dbt/converter/common.py | 40 ++ sqlmesh/dbt/converter/console.py | 117 ++++ sqlmesh/dbt/converter/convert.py | 414 ++++++++++++ sqlmesh/dbt/converter/jinja.py | 604 ++++++++++++++++++ sqlmesh/dbt/converter/jinja_builtins.py | 109 ++++ sqlmesh/dbt/converter/jinja_transforms.py | 465 ++++++++++++++ sqlmesh/dbt/loader.py | 11 +- sqlmesh/dbt/model.py | 1 + sqlmesh/dbt/target.py | 93 ++- sqlmesh/utils/jinja.py | 99 ++- tests/core/test_config.py | 27 + tests/core/test_loader.py | 129 ++++ tests/core/test_model.py | 93 ++- tests/dbt/converter/conftest.py | 21 + .../fixtures/empty_dbt_project/.gitignore | 2 + .../empty_dbt_project/analyses/.gitkeep | 0 .../fixtures/empty_dbt_project/config.py | 7 + .../empty_dbt_project/dbt_project.yml | 22 + .../empty_dbt_project/macros/.gitkeep | 0 .../empty_dbt_project/models/.gitkeep | 0 .../empty_dbt_project/models/sources.yml | 6 + .../empty_dbt_project/packages/.gitkeep | 0 .../fixtures/empty_dbt_project/profiles.yml | 6 + .../fixtures/empty_dbt_project/seeds/.gitkeep | 0 .../empty_dbt_project/seeds/items.csv | 94 +++ .../empty_dbt_project/seeds/properties.yml | 13 + .../empty_dbt_project/snapshots/.gitkeep | 0 .../fixtures/empty_dbt_project/tests/.gitkeep | 0 .../converter/fixtures/jinja_nested_if.sql | 15 + .../fixtures/macro_dbt_incremental.sql | 11 + .../fixtures/macro_func_with_params.sql | 17 + .../fixtures/model_query_incremental.sql | 34 + tests/dbt/converter/test_convert.py | 105 +++ tests/dbt/converter/test_jinja.py | 439 +++++++++++++ tests/dbt/converter/test_jinja_transforms.py | 453 +++++++++++++ tests/utils/test_jinja.py | 78 +++ 46 files changed, 3714 insertions(+), 39 deletions(-) create mode 100644 sqlmesh/dbt/converter/__init__.py create mode 100644 sqlmesh/dbt/converter/common.py create mode 100644 sqlmesh/dbt/converter/console.py create mode 100644 sqlmesh/dbt/converter/convert.py create mode 100644 sqlmesh/dbt/converter/jinja.py create mode 100644 sqlmesh/dbt/converter/jinja_builtins.py create mode 100644 sqlmesh/dbt/converter/jinja_transforms.py create mode 100644 tests/dbt/converter/conftest.py create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/.gitignore create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/analyses/.gitkeep create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/config.py create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/dbt_project.yml create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/macros/.gitkeep create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/models/.gitkeep create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/models/sources.yml create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/packages/.gitkeep create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/profiles.yml create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/seeds/.gitkeep create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/seeds/items.csv create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/seeds/properties.yml create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/snapshots/.gitkeep create mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/tests/.gitkeep create mode 100644 tests/dbt/converter/fixtures/jinja_nested_if.sql create mode 100644 tests/dbt/converter/fixtures/macro_dbt_incremental.sql create mode 100644 tests/dbt/converter/fixtures/macro_func_with_params.sql create mode 100644 tests/dbt/converter/fixtures/model_query_incremental.sql create mode 100644 tests/dbt/converter/test_convert.py create mode 100644 tests/dbt/converter/test_jinja.py create mode 100644 tests/dbt/converter/test_jinja_transforms.py diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 51eb0c3432..b83daf1724 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -33,6 +33,7 @@ "rollback", "run", "table_name", + "dbt", ) SKIP_CONTEXT_COMMANDS = ("init", "ui") @@ -1219,3 +1220,39 @@ def state_import(obj: Context, input_file: Path, replace: bool, no_confirm: bool """Import a state export file back into the state database""" confirm = not no_confirm obj.import_state(input_file=input_file, clear=replace, confirm=confirm) + + +@cli.group(no_args_is_help=True, hidden=True) +def dbt() -> None: + """Commands for doing dbt-specific things""" + pass + + +@dbt.command("convert") +@click.option( + "-i", + "--input-dir", + help="Path to the DBT project", + required=True, + type=click.Path(exists=True, dir_okay=True, file_okay=False, readable=True, path_type=Path), +) +@click.option( + "-o", + "--output-dir", + required=True, + help="Path to write out the converted SQLMesh project", + type=click.Path(exists=False, dir_okay=True, file_okay=False, readable=True, path_type=Path), +) +@click.option("--no-prompts", is_flag=True, help="Disable interactive prompts", default=False) +@click.pass_obj +@error_handler +@cli_analytics +def dbt_convert(obj: Context, input_dir: Path, output_dir: Path, no_prompts: bool) -> None: + """Convert a DBT project to a SQLMesh project""" + from sqlmesh.dbt.converter.convert import convert_project_files + + convert_project_files( + input_dir.absolute(), + output_dir.absolute(), + no_prompts=no_prompts, + ) diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index 0f132680a8..1d53235f73 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -39,7 +39,7 @@ scheduler_config_validator, ) from sqlmesh.core.config.ui import UIConfig -from sqlmesh.core.loader import Loader, SqlMeshLoader +from sqlmesh.core.loader import Loader, SqlMeshLoader, MigratedDbtProjectLoader from sqlmesh.core.notification_target import NotificationTarget from sqlmesh.core.user import User from sqlmesh.utils.date import to_timestamp, now @@ -219,6 +219,13 @@ def _normalize_and_validate_fields(cls, data: t.Any) -> t.Any: f"^{k}$": v for k, v in physical_schema_override.items() } + if ( + (variables := data.get("variables", "")) + and isinstance(variables, dict) + and c.MIGRATED_DBT_PROJECT_NAME in variables + ): + data["loader"] = MigratedDbtProjectLoader + return data @model_validator(mode="after") diff --git a/sqlmesh/core/constants.py b/sqlmesh/core/constants.py index 60c6a3eedf..2ab592f368 100644 --- a/sqlmesh/core/constants.py +++ b/sqlmesh/core/constants.py @@ -31,6 +31,9 @@ MAX_MODEL_DEFINITION_SIZE = 10000 """Maximum number of characters in a model definition""" +MIGRATED_DBT_PROJECT_NAME = "__dbt_project_name__" +MIGRATED_DBT_PACKAGES = "__dbt_packages__" + # The maximum number of fork processes, used for loading projects # None means default to process pool, 1 means don't fork, :N is number of processes diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index e7df315768..7f90c0de63 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -38,7 +38,11 @@ from sqlmesh.core.test import ModelTestMetadata, filter_tests_by_patterns from sqlmesh.utils import UniqueKeyDict, sys_path from sqlmesh.utils.errors import ConfigError -from sqlmesh.utils.jinja import JinjaMacroRegistry, MacroExtractor +from sqlmesh.utils.jinja import ( + JinjaMacroRegistry, + MacroExtractor, + SQLMESH_DBT_COMPATIBILITY_PACKAGE, +) from sqlmesh.utils.metaprogramming import import_python_file from sqlmesh.utils.pydantic import validation_error_message from sqlmesh.utils.process import create_process_pool_executor @@ -548,6 +552,7 @@ def _load_sql_models( signals: UniqueKeyDict[str, signal], cache: CacheBase, gateway: t.Optional[str], + loading_default_kwargs: t.Optional[t.Dict[str, t.Any]] = None, ) -> UniqueKeyDict[str, Model]: """Loads the sql models into a Dict""" models: UniqueKeyDict[str, Model] = UniqueKeyDict("models") @@ -590,6 +595,7 @@ def _load_sql_models( infer_names=self.config.model_naming.infer_names, signal_definitions=signals, default_catalog_per_gateway=self.context.default_catalog_per_gateway, + **loading_default_kwargs or {}, ) with create_process_pool_executor( @@ -942,3 +948,104 @@ def _model_cache_entry_id(self, model_path: Path) -> str: self._loader.context.gateway or self._loader.config.default_gateway_name, ] ) + + +class MigratedDbtProjectLoader(SqlMeshLoader): + @property + def migrated_dbt_project_name(self) -> str: + return self.config.variables[c.MIGRATED_DBT_PROJECT_NAME] + + def _load_scripts(self) -> t.Tuple[MacroRegistry, JinjaMacroRegistry]: + from sqlmesh.dbt.converter.common import infer_dbt_package_from_path + from sqlmesh.dbt.target import TARGET_TYPE_TO_CONFIG_CLASS + + # Store a copy of the macro registry + standard_macros = macro.get_registry() + + jinja_macros = JinjaMacroRegistry( + create_builtins_module=SQLMESH_DBT_COMPATIBILITY_PACKAGE, + top_level_packages=["dbt", self.migrated_dbt_project_name], + ) + extractor = MacroExtractor() + + macros_max_mtime: t.Optional[float] = None + + for path in self._glob_paths( + self.config_path / c.MACROS, + ignore_patterns=self.config.ignore_patterns, + extension=".py", + ): + if import_python_file(path, self.config_path): + self._track_file(path) + macro_file_mtime = self._path_mtimes[path] + macros_max_mtime = ( + max(macros_max_mtime, macro_file_mtime) + if macros_max_mtime + else macro_file_mtime + ) + + for path in self._glob_paths( + self.config_path / c.MACROS, + ignore_patterns=self.config.ignore_patterns, + extension=".sql", + ): + self._track_file(path) + macro_file_mtime = self._path_mtimes[path] + macros_max_mtime = ( + max(macros_max_mtime, macro_file_mtime) if macros_max_mtime else macro_file_mtime + ) + + with open(path, "r", encoding="utf-8") as file: + try: + package = infer_dbt_package_from_path(path) or self.migrated_dbt_project_name + + jinja_macros.add_macros( + extractor.extract(file.read(), dialect=self.config.model_defaults.dialect), + package=package, + ) + except Exception as e: + raise ConfigError(f"Failed to load macro file: {path}", e) + + self._macros_max_mtime = macros_max_mtime + + macros = macro.get_registry() + macro.set_registry(standard_macros) + + connection_config = self.context.connection_config + # this triggers the DBT create_builtins_module to have a `target` property which is required for a bunch of DBT macros to work + if dbt_config_type := TARGET_TYPE_TO_CONFIG_CLASS.get(connection_config.type_): + try: + jinja_macros.add_globals( + { + "target": dbt_config_type.from_sqlmesh( + connection_config, + name=self.config.default_gateway_name, + ).attribute_dict() + } + ) + except NotImplementedError: + raise ConfigError(f"Unsupported dbt target type: {connection_config.type_}") + + return macros, jinja_macros + + def _load_sql_models( + self, + macros: MacroRegistry, + jinja_macros: JinjaMacroRegistry, + audits: UniqueKeyDict[str, ModelAudit], + signals: UniqueKeyDict[str, signal], + cache: CacheBase, + gateway: t.Optional[str], + loading_default_kwargs: t.Optional[t.Dict[str, t.Any]] = None, + ) -> UniqueKeyDict[str, Model]: + return super()._load_sql_models( + macros=macros, + jinja_macros=jinja_macros, + audits=audits, + signals=signals, + cache=cache, + gateway=gateway, + loading_default_kwargs=dict( + migrated_dbt_project_name=self.migrated_dbt_project_name, + ), + ) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index db61b09c8e..f42a3ebfdc 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2017,6 +2017,7 @@ def load_sql_based_model( variables: t.Optional[t.Dict[str, t.Any]] = None, infer_names: t.Optional[bool] = False, blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None, + migrated_dbt_project_name: t.Optional[str] = None, **kwargs: t.Any, ) -> Model: """Load a model from a parsed SQLMesh model SQL file. @@ -2193,6 +2194,7 @@ def load_sql_based_model( query_or_seed_insert, kind=kind, time_column_format=time_column_format, + migrated_dbt_project_name=migrated_dbt_project_name, **common_kwargs, ) @@ -2400,6 +2402,7 @@ def _create_model( signal_definitions: t.Optional[SignalRegistry] = None, variables: t.Optional[t.Dict[str, t.Any]] = None, blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None, + migrated_dbt_project_name: t.Optional[str] = None, **kwargs: t.Any, ) -> Model: validate_extra_and_required_fields( @@ -2455,13 +2458,28 @@ def _create_model( if jinja_macros: jinja_macros = ( - jinja_macros if jinja_macros.trimmed else jinja_macros.trim(jinja_macro_references) + jinja_macros + if jinja_macros.trimmed + else jinja_macros.trim(jinja_macro_references, package=migrated_dbt_project_name) ) else: jinja_macros = JinjaMacroRegistry() - for jinja_macro in jinja_macros.root_macros.values(): - used_variables.update(extract_macro_references_and_variables(jinja_macro.definition)[1]) + if migrated_dbt_project_name: + # extract {{ var() }} references used in all jinja macro dependencies to check for any variables specific + # to a migrated DBT package and resolve them accordingly + # vars are added into __sqlmesh_vars__ in the Python env so that the native SQLMesh var() function can resolve them + variables = variables or {} + + nested_macro_used_variables, flattened_package_variables = ( + _extract_migrated_dbt_variable_references(jinja_macros, variables) + ) + + used_variables.update(nested_macro_used_variables) + variables.update(flattened_package_variables) + else: + for jinja_macro in jinja_macros.root_macros.values(): + used_variables.update(extract_macro_references_and_variables(jinja_macro.definition)[1]) model = klass( name=name, @@ -2844,7 +2862,7 @@ def render_expression( "cron_tz": lambda value: exp.Literal.string(value), "partitioned_by_": _single_expr_or_tuple, "clustered_by": _single_expr_or_tuple, - "depends_on_": lambda value: exp.Tuple(expressions=sorted(value)), + "depends_on_": lambda value: exp.Tuple(expressions=sorted(value)) if value else "()", "pre": _list_of_calls_to_exp, "post": _list_of_calls_to_exp, "audits": _list_of_calls_to_exp, @@ -2915,4 +2933,37 @@ def clickhouse_partition_func( ) +def _extract_migrated_dbt_variable_references( + jinja_macros: JinjaMacroRegistry, project_variables: t.Dict[str, t.Any] +) -> t.Tuple[t.Set[str], t.Dict[str, t.Any]]: + if not jinja_macros.trimmed: + raise ValueError("Expecting a trimmed JinjaMacroRegistry") + + used_variables = set() + # note: JinjaMacroRegistry is trimmed here so "all_macros" should be just be all the macros used by this model + for _, _, jinja_macro in jinja_macros.all_macros: + _, extracted_variable_names = extract_macro_references_and_variables(jinja_macro.definition) + used_variables.update(extracted_variable_names) + + flattened = {} + if (dbt_package_variables := project_variables.get(c.MIGRATED_DBT_PACKAGES)) and isinstance( + dbt_package_variables, dict + ): + # flatten the nested dict structure from the migrated dbt package variables in the SQLmesh config into __dbt_packages.. + # to match what extract_macro_references_and_variables() returns. This allows the usage checks in create_python_env() to work + def _flatten(prefix: str, root: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + acc = {} + for k, v in root.items(): + key_with_prefix = f"{prefix}.{k}" + if isinstance(v, dict): + acc.update(_flatten(key_with_prefix, v)) + else: + acc[key_with_prefix] = v + return acc + + flattened = _flatten(c.MIGRATED_DBT_PACKAGES, dbt_package_variables) + + return used_variables, flattened + + TIME_COL_PARTITION_FUNC = {"clickhouse": clickhouse_partition_func} diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index f58127dcdf..86eb6e665c 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -4,7 +4,7 @@ from enum import Enum from typing_extensions import Self -from pydantic import Field +from pydantic import Field, BeforeValidator from sqlglot import exp from sqlglot.optimizer.normalize_identifiers import normalize_identifiers from sqlglot.optimizer.qualify_columns import quote_identifiers @@ -33,6 +33,7 @@ field_validator, get_dialect, validate_string, + positive_int_validator, ) @@ -455,7 +456,7 @@ class IncrementalByUniqueKeyKind(_IncrementalBy): unique_key: SQLGlotListOfFields when_matched: t.Optional[exp.Whens] = None merge_filter: t.Optional[exp.Expression] = None - batch_concurrency: t.Literal[1] = 1 + batch_concurrency: t.Annotated[t.Literal[1], BeforeValidator(positive_int_validator)] = 1 @field_validator("when_matched", mode="before") def _when_matched_validator( diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index c683fc5862..6622094da3 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -179,6 +179,7 @@ def _resolve_table(table: str | exp.Table) -> str: ) render_kwargs = { + "dialect": self._dialect, **date_dict( to_datetime(execution_time or c.EPOCH), start_time, diff --git a/sqlmesh/dbt/adapter.py b/sqlmesh/dbt/adapter.py index cfff977a96..92719abacc 100644 --- a/sqlmesh/dbt/adapter.py +++ b/sqlmesh/dbt/adapter.py @@ -38,6 +38,9 @@ def __init__( self.jinja_globals = jinja_globals.copy() if jinja_globals else {} self.jinja_globals["adapter"] = self self.project_dialect = project_dialect + self.jinja_globals["dialect"] = ( + project_dialect # so the dialect is available in the jinja env created by self.dispatch() + ) self.quote_policy = quote_policy or Policy() @abc.abstractmethod diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index e07e00c961..4646011d57 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -156,7 +156,7 @@ class Var: def __init__(self, variables: t.Dict[str, t.Any]) -> None: self.variables = variables - def __call__(self, name: str, default: t.Optional[t.Any] = None) -> t.Any: + def __call__(self, name: str, default: t.Optional[t.Any] = None, **kwargs: t.Any) -> t.Any: return self.variables.get(name, default) def has_var(self, name: str) -> bool: diff --git a/sqlmesh/dbt/converter/__init__.py b/sqlmesh/dbt/converter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sqlmesh/dbt/converter/common.py b/sqlmesh/dbt/converter/common.py new file mode 100644 index 0000000000..2bf4131065 --- /dev/null +++ b/sqlmesh/dbt/converter/common.py @@ -0,0 +1,40 @@ +from __future__ import annotations +import jinja2.nodes as j +from sqlglot import exp +import typing as t +import sqlmesh.core.constants as c +from pathlib import Path + + +# jinja transform is a function that takes (current node, previous node, parent node) and returns a new Node or None +# returning None means the current node is removed from the tree +# returning a different Node means the current node is replaced with the new Node +JinjaTransform = t.Callable[[j.Node, t.Optional[j.Node], t.Optional[j.Node]], t.Optional[j.Node]] +SQLGlotTransform = t.Callable[[exp.Expression], t.Optional[exp.Expression]] + + +def _sqlmesh_predefined_macro_variables() -> t.Set[str]: + def _gen() -> t.Iterable[str]: + for suffix in ("dt", "date", "ds", "ts", "tstz", "hour", "epoch", "millis"): + for prefix in ("start", "end", "execution"): + yield f"{prefix}_{suffix}" + + for item in ("runtime_stage", "gateway", "this_model", "this_env", "model_kind_name"): + yield item + + return set(_gen()) + + +SQLMESH_PREDEFINED_MACRO_VARIABLES = _sqlmesh_predefined_macro_variables() + + +def infer_dbt_package_from_path(path: Path) -> t.Optional[str]: + """ + Given a path like "sqlmesh-project/macros/__dbt_packages__/foo/bar.sql" + + Infer that 'foo' is the DBT package + """ + if c.MIGRATED_DBT_PACKAGES in path.parts: + idx = path.parts.index(c.MIGRATED_DBT_PACKAGES) + return path.parts[idx + 1] + return None diff --git a/sqlmesh/dbt/converter/console.py b/sqlmesh/dbt/converter/console.py new file mode 100644 index 0000000000..3fb12bcbc5 --- /dev/null +++ b/sqlmesh/dbt/converter/console.py @@ -0,0 +1,117 @@ +from __future__ import annotations +import typing as t +from pathlib import Path +from rich.console import Console as RichConsole +from rich.tree import Tree +from rich.progress import Progress, TextColumn, BarColumn, MofNCompleteColumn, TimeElapsedColumn +from sqlmesh.core.console import PROGRESS_BAR_WIDTH +from sqlmesh.utils import columns_to_types_all_known +from sqlmesh.utils import rich as srich +import logging +from rich.prompt import Confirm + +logger = logging.getLogger(__name__) + +if t.TYPE_CHECKING: + from sqlmesh.dbt.converter.convert import ConversionReport + + +def make_progress_bar( + console: t.Optional[RichConsole] = None, + justify: t.Literal["default", "left", "center", "right", "full"] = "right", +) -> Progress: + return Progress( + TextColumn("[bold blue]{task.description}", justify=justify), + BarColumn(bar_width=PROGRESS_BAR_WIDTH), + "[progress.percentage]{task.percentage:>3.1f}%", + "•", + MofNCompleteColumn(), + "•", + TimeElapsedColumn(), + console=console, + ) + + +class DbtConversionConsole: + """Console for displaying DBT project conversion progress""" + + def __init__(self, console: t.Optional[RichConsole] = None) -> None: + self.console: RichConsole = console or srich.console + + def log_message(self, message: str) -> None: + self.console.print(message) + + def start_project_conversion(self, input_path: Path) -> None: + self.log_message(f"DBT project loaded from {input_path}; starting conversion") + + def prompt_clear_directory(self, prefix: str, path: Path) -> bool: + return Confirm.ask( + f"{prefix}'{path}' is not empty.\nWould you like to clear it?", console=self.console + ) + + # Models + def start_models_conversion(self, model_count: int) -> None: + self.progress_bar = make_progress_bar(justify="left", console=self.console) + self.progress_bar.start() + self.models_progress_task_id = self.progress_bar.add_task( + "Converting models", total=model_count + ) + + def start_model_conversion(self, model_name: str) -> None: + logger.debug(f"Converting model {model_name}") + self.progress_bar.update(self.models_progress_task_id, description=None, refresh=True) + + def complete_model_conversion(self) -> None: + self.progress_bar.update(self.models_progress_task_id, refresh=True, advance=1) + + def complete_models_conversion(self) -> None: + self.progress_bar.update(self.models_progress_task_id, description=None, refresh=True) + + # Audits + + def start_audits_conversion(self, audit_count: int) -> None: + self.audits_progress_task_id = self.progress_bar.add_task( + "Converting audits", total=audit_count + ) + + def start_audit_conversion(self, audit_name: str) -> None: + self.progress_bar.update(self.audits_progress_task_id, description=None, refresh=True) + + def complete_audit_conversion(self) -> None: + self.progress_bar.update(self.audits_progress_task_id, refresh=True, advance=1) + + def complete_audits_conversion(self) -> None: + self.progress_bar.update(self.audits_progress_task_id, description=None, refresh=True) + + # Macros + + def start_macros_conversion(self, macro_count: int) -> None: + self.macros_progress_task_id = self.progress_bar.add_task( + "Converting macros", total=macro_count + ) + + def start_macro_conversion(self, macro_name: str) -> None: + self.progress_bar.update(self.macros_progress_task_id, description=None, refresh=True) + + def complete_macro_conversion(self) -> None: + self.progress_bar.update(self.macros_progress_task_id, refresh=True, advance=1) + + def complete_macros_conversion(self) -> None: + self.progress_bar.update(self.macros_progress_task_id, description=None, refresh=True) + self.progress_bar.stop() + + def output_report(self, report: ConversionReport) -> None: + tree = Tree( + "[blue]The following models are self-referencing and their column types could not be statically inferred:" + ) + + for output_path, model in report.self_referencing_models: + if not model.columns_to_types or not columns_to_types_all_known(model.columns_to_types): + tree_node = tree.add(f"[green]{model.name}") + tree_node.add(output_path.as_posix()) + + self.console.print(tree) + + self.log_message( + "[red]These will need to be manually fixed.[/red]\nEither specify the column types in the MODEL block or ensure the outer SELECT lists all columns" + ) diff --git a/sqlmesh/dbt/converter/convert.py b/sqlmesh/dbt/converter/convert.py new file mode 100644 index 0000000000..f097a83884 --- /dev/null +++ b/sqlmesh/dbt/converter/convert.py @@ -0,0 +1,414 @@ +import typing as t +from pathlib import Path +import shutil +import os + +from sqlmesh.dbt.loader import sqlmesh_config, DbtLoader, DbtContext, Project +from sqlmesh.core.context import Context +import sqlmesh.core.dialect as d +from sqlmesh.core import constants as c + +from sqlmesh.core.model.kind import SeedKind +from sqlmesh.core.model import SqlModel, SeedModel +from sqlmesh.dbt.converter.jinja import convert_jinja_query, convert_jinja_macro +from sqlmesh.dbt.converter.common import infer_dbt_package_from_path +import dataclasses +from dataclasses import dataclass + +from sqlmesh.dbt.converter.console import DbtConversionConsole +from sqlmesh.utils.jinja import JinjaMacroRegistry, extract_macro_references_and_variables +from sqlmesh.utils import yaml + + +@dataclass +class ConversionReport: + self_referencing_models: t.List[t.Tuple[Path, SqlModel]] = dataclasses.field( + default_factory=list + ) + + +@dataclass +class InputPaths: + # todo: read paths from DBT project yaml + + base: Path + + @property + def models(self) -> Path: + return self.base / "models" + + @property + def seeds(self) -> Path: + return self.base / "seeds" + + @property + def tests(self) -> Path: + return self.base / "tests" + + @property + def macros(self) -> Path: + return self.base / "macros" + + @property + def snapshots(self) -> Path: + return self.base / "snapshots" + + @property + def packages(self) -> Path: + return self.base / "dbt_packages" + + +@dataclass +class OutputPaths: + base: Path + + @property + def models(self) -> Path: + return self.base / "models" + + @property + def seeds(self) -> Path: + return self.base / "seeds" + + @property + def audits(self) -> Path: + return self.base / "audits" + + @property + def macros(self) -> Path: + return self.base / "macros" + + +def convert_project_files(src: Path, dest: Path, no_prompts: bool = True) -> None: + console = DbtConversionConsole() + report = ConversionReport() + + console.log_message(f"Converting project at '{src}' to '{dest}'") + + ctx, dbt_project = _load_project(src) + dbt_load_context = dbt_project.context + + console.start_project_conversion(src) + + input_paths, output_paths = _ensure_paths(src, dest, console, no_prompts) + + model_count = len(ctx.models) + + # DBT Models -> SQLMesh Models + console.start_models_conversion(model_count) + _convert_models(ctx, input_paths, output_paths, report, console) + console.complete_models_conversion() + + # DBT Tests -> Standalone Audits + console.start_audits_conversion(len(ctx.standalone_audits)) + _convert_standalone_audits(ctx, input_paths, output_paths, console) + console.complete_audits_conversion() + + # DBT Macros -> SQLMesh Jinja Macros + all_macros = list( + iterate_macros(input_paths.macros, output_paths.macros, dbt_load_context, ctx) + ) + console.start_macros_conversion(len(all_macros)) + for package, macro_text, input_id, output_file_path, should_transform in all_macros: + console.start_macro_conversion(input_id) + + output_file_path.parent.mkdir(parents=True, exist_ok=True) + converted = ( + convert_jinja_macro(ctx, macro_text, package) if should_transform else macro_text + ) + output_file_path.write_text(converted, encoding="utf8") + + console.complete_macro_conversion() + + console.complete_macros_conversion() + + # Generate SQLMesh config + # TODO: read all profiles from config and convert to gateways instead of just the current profile? + console.log_message("Writing SQLMesh config") + new_config = _generate_sqlmesh_config(ctx, dbt_project, dbt_load_context) + (dest / "config.yml").write_text(yaml.dump(new_config)) + + if report.self_referencing_models: + console.output_report(report) + + console.log_message("All done") + + +def _load_project(src: Path) -> t.Tuple[Context, Project]: + config = sqlmesh_config(project_root=src) + + ctx = Context(config=config, paths=src) + + dbt_loader = ctx._loaders[0] + assert isinstance(dbt_loader, DbtLoader) + + dbt_project = dbt_loader._projects[0] + + return ctx, dbt_project + + +def _ensure_paths( + src: Path, dest: Path, console: DbtConversionConsole, no_prompts: bool +) -> t.Tuple[InputPaths, OutputPaths]: + if not dest.exists(): + console.log_message(f"Creating output directory: {dest}") + dest.mkdir() + + if dest.is_file(): + raise ValueError(f"Output path must be a directory") + + if any(dest.iterdir()): + if not no_prompts and console.prompt_clear_directory("Output directory ", dest): + for path in dest.glob("**/*"): + if path.is_file(): + path.unlink() + elif path.is_dir(): + shutil.rmtree(path) + console.log_message(f"Output directory '{dest}' cleared") + else: + raise ValueError("Please ensure the output directory is empty") + + input_paths = InputPaths(src) + output_paths = OutputPaths(dest) + + for dir in (output_paths.models, output_paths.seeds, output_paths.audits, output_paths.macros): + dir.mkdir() + + return input_paths, output_paths + + +def _convert_models( + ctx: Context, + input_paths: InputPaths, + output_paths: OutputPaths, + report: ConversionReport, + console: DbtConversionConsole, +) -> None: + # Iterating in DAG order helps minimize re-rendering when the fingerprint cache is busted when we call upsert_model() to check if + # a self-referencing model has all its columns_to_types known or not + for fqn in ctx.dag: + model = ctx.models.get(fqn) + + if not model: + # some entries in the dag are not models + continue + + model_name = fqn + + # todo: support DBT model_paths[] being not `models` or being a list + # todo: write out column_descriptions() into model block + console.start_model_conversion(model_name) + + if model.kind.is_external: + # skip external models + # they can be created with `sqlmesh create_external_models` post-conversion + console.complete_model_conversion() # still advance the progress bar + continue + + if model.kind.is_seed: + # this will produce the original seed file, eg "items.csv" + seed_filename = model._path.relative_to(input_paths.seeds) + + # seed definition - rename "items.csv" -> "items.sql" + model_filename = seed_filename.with_suffix(".sql") + + # copy the seed data itself to the seeds dir + shutil.copyfile(model._path, output_paths.seeds / seed_filename) + + # monkeypatch the model kind to have a relative reference to the seed file + assert isinstance(model.kind, SeedKind) + model.kind.path = str(Path("../seeds", seed_filename)) + else: + if input_paths.models in model._path.parents: + model_filename = model._path.relative_to(input_paths.models) + elif input_paths.snapshots in model._path.parents: + # /base/path/snapshots/foo.sql -> /output/path/models/dbt_snapshots/foo.sql + model_filename = "dbt_snapshots" / model._path.relative_to(input_paths.snapshots) + elif input_paths.packages in model._path.parents: + model_filename = c.MIGRATED_DBT_PACKAGES / model._path.relative_to( + input_paths.packages + ) + else: + raise ValueError(f"Unhandled model path: {model._path}") + + # todo: a SQLGLot transform on `audits` in the model definition to lowercase the names? + model_output_path = output_paths.models / model_filename + model_output_path.parent.mkdir(parents=True, exist_ok=True) + model_package = infer_dbt_package_from_path(model_output_path) + + def _render(e: d.exp.Expression) -> str: + if isinstance(e, d.Jinja): + e = convert_jinja_query(ctx, model, e, model_package) + rendered = e.sql(dialect=model.dialect, pretty=True) + if not isinstance(e, d.Jinja): + rendered += ";" + return rendered + + model_to_render = model.model_copy( + update=dict(depends_on_=None if len(model.depends_on) > 0 else set()) + ) + if isinstance(model, (SqlModel, SeedModel)): + # Keep depends_on for SQL Models because sometimes the entire query is a macro call. + # If we clear it and rely on inference, the SQLMesh native loader will throw: + # - ConfigError: Dependencies must be provided explicitly for models that can be rendered only at runtime + model_to_render = model.model_copy( + update=dict(depends_on_=resolve_fqns_to_model_names(ctx, model.depends_on)) + ) + + rendered_queries = [ + _render(q) + for q in model_to_render.render_definition(render_query=False, include_python=False) + ] + + # add inline audits + # todo: handle these better + # maybe output generic audits for the 4 DBT audits (not_null, unique, accepted_values, relationships) and emit definitions for them? + for _, audit in model.audit_definitions.items(): + rendered_queries.append("\n" + _render(d.parse_one(f"AUDIT (name {audit.name})"))) + # todo: or do we want the original? + rendered_queries.append(_render(model.render_audit_query(audit))) + + model_definition = "\n".join(rendered_queries) + + model_output_path.write_text(model_definition) + + console.complete_model_conversion() + + +def _convert_standalone_audits( + ctx: Context, input_paths: InputPaths, output_paths: OutputPaths, console: DbtConversionConsole +) -> None: + for _, audit in ctx.standalone_audits.items(): + console.start_audit_conversion(audit.name) + audit_definition = audit.render_definition(include_python=False) + + stringified = [] + for expression in audit_definition: + if isinstance(expression, d.JinjaQuery): + expression = convert_jinja_query(ctx, audit, expression) + stringified.append(expression.sql(dialect=audit.dialect, pretty=True)) + + audit_definition_string = ";\n".join(stringified) + + audit_filename = audit._path.relative_to(input_paths.tests) + audit_output_path = output_paths.audits / audit_filename + audit_output_path.write_text(audit_definition_string) + console.complete_audit_conversion() + return None + + +def _generate_sqlmesh_config( + ctx: Context, dbt_project: Project, dbt_load_context: DbtContext +) -> t.Dict[str, t.Any]: + DEFAULT_ARGS: t.Dict[str, t.Any] + from sqlmesh.utils.pydantic import DEFAULT_ARGS + + base_config = ctx.config.model_dump( + mode="json", include={"gateways", "model_defaults", "variables"}, **DEFAULT_ARGS + ) + # Extend with the variables loaded from DBT + if "variables" not in base_config: + base_config["variables"] = {} + if c.MIGRATED_DBT_PACKAGES not in base_config["variables"]: + base_config["variables"][c.MIGRATED_DBT_PACKAGES] = {} + + # this is used when loading with the native loader to set the package name for top level macros + base_config["variables"][c.MIGRATED_DBT_PROJECT_NAME] = dbt_project.context.project_name + + migrated_package_names = [] + for package in dbt_project.packages.values(): + dbt_load_context.set_and_render_variables(package.variables, package.name) + + if package.name == dbt_project.context.project_name: + base_config["variables"].update(dbt_load_context.variables) + else: + base_config["variables"][c.MIGRATED_DBT_PACKAGES][package.name] = ( + dbt_load_context.variables + ) + migrated_package_names.append(package.name) + + for package_name in migrated_package_names: + # these entries are duplicates because the DBT loader already applies any project specific overrides to the + # package level variables + base_config["variables"].pop(package_name, None) + + return base_config + + +def iterate_macros( + input_macros_dir: Path, output_macros_dir: Path, dbt_load_context: DbtContext, ctx: Context +) -> t.Iterator[t.Tuple[t.Optional[str], str, str, Path, bool]]: + """ + Return an iterator over all the macros that need to be migrated + + The main project level ones are read from the source macros directory (it's assumed these are written by the user) + + The rest / library level ones are read from the DBT manifest based on merging together all the model JinjaMacroRegistry's from the SQLMesh context + """ + + all_macro_references = set() + + for dirpath, _, files in os.walk( + input_macros_dir + ): # note: pathlib doesnt have a walk function until python 3.12 + for name in files: + if name.lower().endswith(".sql"): + input_file_path = Path(dirpath) / name + + output_file_path = output_macros_dir / ( + input_file_path.relative_to(input_macros_dir) + ) + + input_file_contents = input_file_path.read_text(encoding="utf8") + + # as we migrate user-defined macros, keep track of other macros they reference from other packages/libraries + # so we can be sure theyre included + # (since there is no guarantee a model references a user-defined macro which means the dependencies may not be pulled in automatically) + macro_refs, _ = extract_macro_references_and_variables( + input_file_contents, dbt_target_name=dbt_load_context.target_name + ) + all_macro_references.update(macro_refs) + + yield ( + None, + input_file_contents, + str(input_file_path), + output_file_path, + True, + ) + + jmr = JinjaMacroRegistry() + for model in ctx.models.values(): + jmr = jmr.merge(model.jinja_macros) + + # add any macros that are referenced in user macros but not necessarily directly in models + # this can happen if a user has defined a macro that is currently unused in a model but we still want to migrate it + jmr = jmr.merge( + dbt_load_context.jinja_macros.trim( + all_macro_references, package=dbt_load_context.project_name + ) + ) + + for package, name, macro in jmr.all_macros: + if package and package != dbt_load_context.project_name: + output_file_path = output_macros_dir / c.MIGRATED_DBT_PACKAGES / package / f"{name}.sql" + + yield ( + package, + macro.definition, + f"{package}.{name}", + output_file_path, + "var(" in macro.definition, # todo: check for ref() etc as well? + ) + + +def resolve_fqns_to_model_names(ctx: Context, fqns: t.Set[str]) -> t.Set[str]: + # model.depends_on is provided by the DbtLoader as a list of fully qualified table name strings + # if we output them verbatim, when loading them back we get errors like: + # - ConfigError: Failed to load model definition: 'Dot' object has no attribute 'catalog' + # So we need to resolve them to model names instead. + # External models also need to be excluded because the "name" is still a FQN string so cause the above error + + return { + ctx.models[i].name for i in fqns if i in ctx.models and not ctx.models[i].kind.is_external + } diff --git a/sqlmesh/dbt/converter/jinja.py b/sqlmesh/dbt/converter/jinja.py new file mode 100644 index 0000000000..783ae5a74f --- /dev/null +++ b/sqlmesh/dbt/converter/jinja.py @@ -0,0 +1,604 @@ +import typing as t +import jinja2.nodes as j +import sqlmesh.core.dialect as d +from sqlmesh.core.context import Context +from sqlmesh.core.snapshot import Node +from sqlmesh.core.model import SqlModel, load_sql_based_model +from sqlglot import exp +from sqlmesh.dbt.converter.common import JinjaTransform +from inspect import signature +from more_itertools import windowed +from itertools import chain +from sqlmesh.dbt.context import DbtContext +import sqlmesh.dbt.converter.jinja_transforms as jt +from sqlmesh.utils.errors import ConfigError +from sqlmesh.utils.jinja import SQLMESH_DBT_COMPATIBILITY_PACKAGE + +# for j.Operand.op +OPERATOR_MAP = { + "eq": "==", + "ne": "!=", + "lt": "<", + "gt": ">", + "lteq": "<=", + "gteq": ">=", + "in": "in", + "notin": "not in", +} + + +def lpad_windowed(iterable: t.Iterable[j.Node]) -> t.Iterator[t.Tuple[t.Optional[j.Node], j.Node]]: + for prev, curr in windowed(chain([None], iterable), 2): + if curr is None: + raise ValueError("Current item cannot be None") + yield prev, curr + + +class JinjaGenerator: + def generate( + self, node: j.Node, prev: t.Optional[j.Node] = None, parent: t.Optional[j.Node] = None + ) -> str: + if not isinstance(node, j.Node): + raise ValueError(f"Generator only works with Jinja AST nodes, not: {type(node)}") + + acc = "" + + node_type = type(node) + generator_fn_name = f"_generate_{node_type.__name__.lower()}" + + if generator_fn := getattr(self, generator_fn_name, None): + sig = signature(generator_fn) + kwargs: t.Dict[str, t.Optional[j.Node]] = {"node": node} + if "prev" in sig.parameters: + kwargs["prev"] = prev + if "parent" in sig.parameters: + kwargs["parent"] = parent + acc += generator_fn(**kwargs) + else: + raise NotImplementedError(f"Generator for node type '{type(node)}' is not implemented") + + return acc + + def _generate_template(self, node: j.Template) -> str: + acc = [] + for prev, curr in lpad_windowed(node.body): + if curr: + acc.append(self.generate(curr, prev, node)) + + return "".join(acc) + + def _generate_output(self, node: j.Output) -> str: + acc = [] + for prev, curr in lpad_windowed(node.nodes): + acc.append(self.generate(curr, prev, node)) + + return "".join(acc) + + def _generate_templatedata(self, node: j.TemplateData) -> str: + return node.data + + def _generate_name( + self, node: j.Name, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> str: + return self._wrap_in_expression_if_necessary(node.name, prev, parent) + + def _generate_getitem( + self, node: j.Getitem, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> str: + item_name = self.generate(node.node, parent=node) + if node.arg: + if node.node.find(j.Filter): + # for when someone has {{ (foo | bar | baz)[0] }} + item_name = f"({item_name})" + item_name = f"{item_name}[{self.generate(node.arg, parent=node)}]" + + return self._wrap_in_expression_if_necessary(item_name, prev, parent) + + def _generate_getattr( + self, node: j.Getattr, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> str: + what_str = self.generate(node.node, parent=node) + + return self._wrap_in_expression_if_necessary(f"{what_str}.{node.attr}", prev, parent) + + def _generate_const( + self, node: j.Const, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> str: + quotechar = "" + node_value: str + if isinstance(node.value, str): + quotechar = "'" if "'" not in node.value else '"' + node_value = node.value + else: + node_value = str(node.value) + + const_value = quotechar + node_value + quotechar + + return self._wrap_in_expression_if_necessary(const_value, prev, parent) + + def _generate_keyword(self, node: j.Keyword) -> str: + return node.key + "=" + self.generate(node.value, parent=node) + + def _generate_test(self, node: j.Test, parent: t.Optional[j.Node]) -> str: + var_name = self.generate(node.node, parent=node) + test = "is" if not isinstance(parent, j.Not) else "is not" + if node.name: + return f"{var_name} {test} {node.name}" + return var_name + + def _generate_assign(self, node: j.Assign) -> str: + target_str = self.generate(node.target, parent=node) + what_str = self.generate(node.node, parent=node) + return "{% set " + target_str + " = " + what_str + " %}" + + def _generate_assignblock(self, node: j.AssignBlock) -> str: + target_str = self.generate(node.target, parent=node) + body_str = "".join(self.generate(c, parent=node) for c in node.body) + # todo: node.filter? + return "{% set " + target_str + " %}" + body_str + "{% endset %}" + + def _generate_call( + self, node: j.Call, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> str: + call_name = self.generate(node.node, parent=node) + call_args = ", ".join(self.generate(a, parent=node) for a in node.args) + call_kwargs = ", ".join(self.generate(a, parent=node) for a in node.kwargs) + sep = ", " if call_args and call_kwargs else "" + call_str = call_name + f"({call_args}{sep}{call_kwargs})" + + return self._wrap_in_expression_if_necessary(call_str, prev, parent) + + def _generate_if(self, node: j.If, parent: t.Optional[j.Node]) -> str: + test_str = self.generate(node.test, parent=node) + body_str = "".join(self.generate(c, parent=node) for c in node.body) + elifs_str = "".join(self.generate(c, parent=node) for c in node.elif_) + elses_str = "".join(self.generate(c, parent=node) for c in node.else_) + + end_block_name: t.Optional[str] + block_name, end_block_name = "if", "endif" + if isinstance(parent, j.If): + if node in parent.elif_: + block_name, end_block_name = "elif", None + + end_block = "{% " + end_block_name + " %}" if end_block_name else "" + + elses_str = "{% else %}" + elses_str if elses_str else "" + + return ( + "{% " + + block_name + + " " + + test_str + + " %}" + + body_str + + elifs_str + + elses_str + + end_block + ) + + def _generate_macro(self, node: j.Macro, prev: t.Optional[j.Node]) -> str: + name_str = node.name + rendered_defaults = list(reversed([self.generate(d, parent=node) for d in node.defaults])) + rendered_args = [self.generate(a, parent=node) for a in node.args] + + # the defaults, if they exist, line up with the last arguments in the list + # so we reverse the lists to match the arrays and then reverse the result to get the original order + args_with_defaults = [ + (arg, next(iter(rendered_defaults[idx : idx + 1]), None)) + for idx, arg in enumerate(reversed(rendered_args)) + ] + args_with_defaults = list(reversed(args_with_defaults)) + + args_str = ", ".join(f"{a}={d}" if d is not None else a for a, d in args_with_defaults) + body_str = "".join(self.generate(c, parent=node) for c in node.body) + + # crude sql comment detection that will cause false positives that hopefully shouldnt matter + # this is to work around a WONTFIX bug in the SQLGlot tokenizer that if the macro body contains a SQL comment + # and {% endmacro %} is on the same line, it gets included as comment instead of a proper token + # the bug also occurs if the {% macro %} tag is on a line that starts with a SQL comment + start_tag = "{% macro " + if prev: + prev_str = self.generate(prev) + if "--" in prev_str and not prev_str.rstrip(" ").endswith("\n"): + start_tag = "\n" + start_tag + + end_tag = "{% endmacro %}" + if "--" in body_str and not body_str.rstrip(" ").endswith("\n"): + end_tag = "\n" + end_tag + + return start_tag + name_str + "(" + args_str + ")" + " %}" + body_str + end_tag + + def _generate_for(self, node: j.For) -> str: + target_str = self.generate(node.target, parent=node) + iter_str = self.generate(node.iter, parent=node) + test_str = "if " + self.generate(node.test, parent=node) if node.test else None + body_str = "".join(self.generate(c, parent=node) for c in node.body) + + acc = "{% for " + target_str + " in " + iter_str + if test_str: + acc += f" {test_str}" + acc += " %}" + acc += body_str + acc += "{% endfor %}" + + return acc + + def _generate_list(self, node: j.List, parent: t.Optional[j.Node]) -> str: + items_str_array = [self.generate(i, parent=node) for i in node.items] + items_on_newline = ( + not isinstance(parent, j.Pair) + and len(items_str_array) > 1 + and any(len(i) > 50 for i in items_str_array) + ) + item_separator = "\n\t" if items_on_newline else " " + items_str = f",{item_separator}".join(items_str_array) + start_separator = "\n\t" if items_on_newline else "" + end_separator = "\n" if items_on_newline else "" + return f"[{start_separator}{items_str}{end_separator}]" + + def _generate_dict(self, node: j.Dict) -> str: + items_str = ", ".join(self.generate(c, parent=node) for c in node.items) + return "{ " + items_str + " }" + + def _generate_pair(self, node: j.Pair) -> str: + key_str = self.generate(node.key, parent=node) + value_str = self.generate(node.value, parent=node) + return f"{key_str}: {value_str}" + + def _generate_not(self, node: j.Not) -> str: + if isinstance(node.node, j.Test): + return self.generate(node.node, parent=node) + + return self.__generate_unaryexp(node) + + def _generate_neg(self, node: j.Neg) -> str: + return self.__generate_unaryexp(node) + + def _generate_pos(self, node: j.Pos) -> str: + return self.__generate_unaryexp(node) + + def _generate_compare(self, node: j.Compare) -> str: + what_str = self.generate(node.expr, parent=node) + + # todo: is this correct? need to test with multiple ops + ops_str = "".join(self.generate(o, parent=node) for o in node.ops) + + return f"{what_str} {ops_str}" + + def _generate_slice(self, node: j.Slice) -> str: + start_str = self.generate(node.start, parent=node) if node.start else "" + stop_str = self.generate(node.stop, parent=node) if node.stop else "" + # todo: need a syntax example of step + return f"{start_str}:{stop_str}" + + def _generate_operand(self, node: j.Operand) -> str: + assert isinstance(node, j.Operand) + value_str = self.generate(node.expr, parent=node) + + return f"{OPERATOR_MAP[node.op]} " + value_str + + def _generate_add(self, node: j.Add, parent: t.Optional[j.Node]) -> str: + return self.__generate_binexp(node, parent) + + def _generate_mul(self, node: j.Mul, parent: t.Optional[j.Node]) -> str: + return self.__generate_binexp(node, parent) + + def _generate_div(self, node: j.Div, parent: t.Optional[j.Node]) -> str: + return self.__generate_binexp(node, parent) + + def _generate_sub(self, node: j.Sub, parent: t.Optional[j.Node]) -> str: + return self.__generate_binexp(node, parent) + + def _generate_floordiv(self, node: j.FloorDiv, parent: t.Optional[j.Node]) -> str: + return self.__generate_binexp(node, parent) + + def _generate_mod(self, node: j.Mod, parent: t.Optional[j.Node]) -> str: + return self.__generate_binexp(node, parent) + + def _generate_pow(self, node: j.Pow, parent: t.Optional[j.Node]) -> str: + return self.__generate_binexp(node, parent) + + def _generate_or(self, node: j.Or, parent: t.Optional[j.Node]) -> str: + return self.__generate_binexp(node, parent) + + def _generate_and(self, node: j.And, parent: t.Optional[j.Node]) -> str: + return self.__generate_binexp(node, parent) + + def _generate_concat(self, node: j.Concat) -> str: + return " ~ ".join(self.generate(c, parent=node) for c in node.nodes) + + def _generate_tuple(self, node: j.Tuple, parent: t.Optional[j.Node]) -> str: + parenthesis = isinstance(parent, (j.Operand, j.Call)) + items_str = ", ".join(self.generate(i, parent=node) for i in node.items) + return items_str if not parenthesis else f"({items_str})" + + def _generate_filter( + self, node: j.Filter, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> str: + # node.node may be None if this Filter is part of a FilterBlock + what_str = self.generate(node.node, parent=node) if node.node else None + if isinstance(node.node, j.CondExpr): + what_str = f"({what_str})" + + args_str = ", ".join(self.generate(a, parent=node) for a in node.args + node.kwargs) + if args_str: + args_str = f"({args_str})" + + filter_expr = f"{node.name}{args_str}" + if what_str: + filter_expr = f"{what_str} | {filter_expr}" + + return self._wrap_in_expression_if_necessary(filter_expr, prev=prev, parent=parent) + + def _generate_filterblock(self, node: j.FilterBlock) -> str: + filter_str = self.generate(node.filter, parent=node) + body_str = "".join(self.generate(c, parent=node) for c in node.body) + return "{% filter " + filter_str + " %}" + body_str + "{% endfilter %}" + + def _generate_exprstmt(self, node: j.ExprStmt) -> str: + node_str = self.generate(node.node, parent=node) + return "{% do " + node_str + " %}" + + def _generate_condexpr( + self, node: j.CondExpr, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> str: + test_sql = self.generate(node.test, parent=node) + expr1_sql = self.generate(node.expr1, parent=node) + + if node.expr2 is None: + raise ValueError("CondExpr lacked an 'else', not sure how to handle this") + + expr2_sql = self.generate(node.expr2, parent=node) + return self._wrap_in_expression_if_necessary( + f"{expr1_sql} if {test_sql} else {expr2_sql}", prev, parent + ) + + def __generate_binexp(self, node: j.BinExpr, parent: t.Optional[j.Node]) -> str: + left_str = self.generate(node.left, parent=node) + right_str = self.generate(node.right, parent=node) + + wrap_left = isinstance(node.left, j.BinExpr) + wrap_right = isinstance(node.right, j.BinExpr) + + acc = f"({left_str})" if wrap_left else left_str + acc += f" {node.operator} " + acc += f"({right_str})" if wrap_right else right_str + + return acc + + def __generate_unaryexp(self, node: j.UnaryExpr) -> str: + body_str = self.generate(node.node, parent=node) + return f"{node.operator} {body_str}" + + def _generate_nsref(self, node: j.NSRef) -> str: + return f"{node.name}.{node.attr}" + + def _generate_callblock(self, node: j.CallBlock) -> str: + call = self.generate(node.call, parent=node) + body = "".join(self.generate(e, parent=node) for e in node.body) + args = ", ".join(self.generate(arg, parent=node) for arg in node.args) + + open_tag = "{% call" + + if args: + open_tag += "(" + args + ")" + + if len(node.defaults) > 0: + raise NotImplementedError("Not sure how to handle CallBlock.defaults") + + return open_tag + " " + call + " %}" + body + "{% endcall %}" + + def _wrap_in_expression_if_necessary( + self, string: str, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> str: + wrap = False + if isinstance(prev, j.TemplateData): + wrap = True + elif prev is None and isinstance(parent, j.Output): + wrap = True + elif parent: + # if the node is nested inside eg an {% if %} block, dont wrap it in {{ }} + wrap = not any(isinstance(parent, t) for t in (j.Operand, j.Stmt, j.Expr, j.Helper)) + + return "{{ " + string + " }}" if wrap else string + + +def _contains_jinja(query: str) -> bool: + if "{{" in query: + return True + if "{%" in query: + return True + return False + + +def transform(base: j.Node, handler: JinjaTransform) -> j.Node: + sig = signature(handler) + + def _build_handler_kwargs( + node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> t.Dict[str, t.Any]: + kwargs: t.Dict[str, t.Optional[j.Node]] = {"node": node} + if "prev" in sig.parameters: + kwargs["prev"] = prev + if "parent" in sig.parameters: + kwargs["parent"] = parent + return kwargs + + def _transform( + node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> t.Optional[j.Node]: + transformed_node: t.Optional[j.Node] = handler(**_build_handler_kwargs(node, prev, parent)) # type: ignore + + if not transformed_node: + return None + + node = transformed_node + + new_children: t.Dict[j.Node, t.Optional[j.Node]] = {} + prev = None + for child in list(node.iter_child_nodes()): + transformed_child = _transform(node=child, prev=prev, parent=node) + if transformed_child != child: + new_children[child] = transformed_child + prev = child + + if new_children: + replacement_fields: t.Dict[str, t.Union[j.Node, t.List[j.Node]]] = {} + for name, value in node.iter_fields(): + assert isinstance(name, str) + + if isinstance(value, list): + replacement_value_list = [new_children.get(i, i) for i in value] + replacement_fields[name] = [r for r in replacement_value_list if r is not None] + elif isinstance(value, j.Node): + replacement_value = new_children.get(value) or value + replacement_fields[name] = replacement_value + for name, value in replacement_fields.items(): + setattr(node, name, value) + + return node + + transformed = _transform(node=base, prev=None, parent=None) + if transformed is None: + raise ValueError( + f"Transform '{handler.__name__}' consumed the entire AST; this indicates a bug" + ) + return transformed + + +def convert_jinja_query( + context: Context, + node: Node, + query: d.Jinja, + package: t.Optional[str] = None, + exclude: t.Optional[t.List[t.Callable]] = None, +) -> t.Union[d.JinjaQuery, d.JinjaStatement, exp.Query, exp.DDL]: + jinja_env = node.jinja_macros.build_environment() + + ast: j.Node = jinja_env.parse(query.text("this")) # type: ignore + + transforms = [ + # transform {{ ref("foo") }} -> schema.foo (NOT "fully_qualified"."schema"."foo") + jt.resolve_dbt_ref_to_model_name(context.models, jinja_env, node.dialect), + # Rewrite ref() calls that cant be converted to strings (maybe theyre macro aguments) to __migrated_ref() calls + jt.rewrite_dbt_ref_to_migrated_ref(context.models, jinja_env, node.dialect), + # transform {{ source("upstream"."foo") }} -> upstream.foo (NOT "fully_qualified"."upstream"."foo") + jt.resolve_dbt_source_to_model_name(context.models, jinja_env, node.dialect), + # Rewrite source() calls that cant be converted to strings (maybe theyre macro aguments) to __migrated_source() calls + jt.rewrite_dbt_source_to_migrated_source(context.models, jinja_env, node.dialect), + # transform {{ this }} -> model.name + jt.resolve_dbt_this_to_model_name(node.name), + # deuplicate where both {% if sqlmesh_incremental %} and {% if is_incremental() %} are used + jt.deduplicate_incremental_checks(), + # unpack {% if is_incremental() %} blocks because they arent necessary when running a native project + jt.unpack_incremental_checks(), + ] + + if package: + transforms.append(jt.append_dbt_package_kwarg_to_var_calls(package)) + + transforms = [ + t for t in transforms if not any(e.__name__ in t.__name__ for e in (exclude or [])) + ] + + for handler in transforms: + ast = transform(ast, handler) + + generator = JinjaGenerator() + pre_post_processing = generator.generate(ast) + if isinstance(node, SqlModel) and isinstance(query, d.JinjaQuery) and not node.depends_on_self: + # is it self-referencing now is_incremental() has been removed? + # if so, and columns_to_types are not all known, then we can't remove is_incremental() or we will get a load error + + # try to load the converted model with the native loader + model_definition = node.copy(update=dict(audits=[])).render_definition()[0].sql() + + # we need the Jinja builtins that inclide the compatibility shims because the transforms may have created eg __migrated_ref() calls + jinja_macros = node.jinja_macros.copy( + update=dict(create_builtins_module=SQLMESH_DBT_COMPATIBILITY_PACKAGE) + ) + + converted_node = load_sql_based_model( + expressions=[d.parse_one(model_definition), d.JinjaQuery(this=pre_post_processing)], + jinja_macros=jinja_macros, + defaults=context.config.model_defaults.dict(), + default_catalog=node.default_catalog, + ) + original_model = context.models[node.fqn] + + if converted_node.depends_on_self: + try: + # we need to upsert the model into the context to trigger columns_to_types inference + # note that this can sometimes bust the optimized query cache which can lead to long pauses converting some models in large projects + context.upsert_model(converted_node) + except ConfigError as e: + if "Self-referencing models require inferrable column types" in str(e): + # we have a self-referencing model where the columns_to_types cannot be inferred + # run the conversion again without the unpack_incremental_checks transform + return convert_jinja_query( + context, node, query, exclude=[jt.unpack_incremental_checks] + ) + raise + except Exception: + # todo: perhaps swallow this so that we just continue on with the original logic + raise + finally: + context.upsert_model(original_model) # put the original model definition back + + ast = transform(ast, jt.rewrite_sqlmesh_predefined_variables_to_sqlmesh_macro_syntax()) + post_processed = generator.generate(ast) + + # post processing - have we removed all the jinja so this can effectively be a normal SQL query? + if not _contains_jinja(post_processed): + parsed = d.parse_one(post_processed, dialect=node.dialect) + + # converting DBT '{{ start_ds }}' to a SQLMesh macro results in single quoted '@start_ds' but we really need unquoted @start_ds + transformed = parsed.transform(jt.unwrap_macros_in_string_literals()) + if isinstance(transformed, (exp.Query, exp.DDL)): + return transformed + + raise ValueError( + f"Transformation resulted in a {type(transformed)} node instead of Query / DDL statement" + ) + + if isinstance(query, d.JinjaQuery): + return d.JinjaQuery(this=pre_post_processing) + if isinstance(query, d.JinjaStatement): + return d.JinjaStatement(this=pre_post_processing) + + raise ValueError(f"Not sure how to handle: {type(query)}") + + +def convert_jinja_macro(context: Context, src: str, package: t.Optional[str] = None) -> str: + jinja_macros = DbtContext().jinja_macros # ensures the correct create_builtins_module is set + jinja_macros = jinja_macros.merge(context._jinja_macros) + + jinja_env = jinja_macros.build_environment() + + dialect = context.default_dialect + if not dialect: + raise ValueError("No project dialect configured?") + + transforms = [ + # transform {{ ref("foo") }} -> schema.foo (NOT "fully_qualified"."schema"."foo") + jt.resolve_dbt_ref_to_model_name(context.models, jinja_env, dialect), + # Rewrite ref() calls that cant be converted to strings (maybe theyre macro aguments) to __migrated_ref() calls + jt.rewrite_dbt_ref_to_migrated_ref(context.models, jinja_env, dialect), + # transform {{ source("foo", "bar") }} -> `qualified`.`foo`.`bar` + jt.resolve_dbt_source_to_model_name(context.models, jinja_env, dialect), + # transform {{ var('foo') }} -> {{ var('foo', __dbt_package='') }} + jt.append_dbt_package_kwarg_to_var_calls(package), + # deduplicate where both {% if sqlmesh_incremental %} and {% if is_incremental() %} are used + jt.deduplicate_incremental_checks(), + # unpack {% if sqlmesh_incremental %} blocks because they arent necessary when running a native project + jt.unpack_incremental_checks(), + ] + + ast: j.Node = jinja_env.parse(src) + + for handler in transforms: + ast = transform(ast, handler) + + generator = JinjaGenerator() + + return generator.generate(ast) diff --git a/sqlmesh/dbt/converter/jinja_builtins.py b/sqlmesh/dbt/converter/jinja_builtins.py new file mode 100644 index 0000000000..59303ad344 --- /dev/null +++ b/sqlmesh/dbt/converter/jinja_builtins.py @@ -0,0 +1,109 @@ +import typing as t +import functools +from sqlmesh.utils.jinja import JinjaMacroRegistry +from dbt.adapters.base.relation import BaseRelation +from sqlmesh.dbt.builtin import Api +from sqlmesh.core.engine_adapter import EngineAdapter +from sqlmesh.utils.errors import ConfigError +from dbt.adapters.base import BaseRelation +from sqlglot import exp + +from dbt.adapters.base import BaseRelation + + +def migrated_ref( + dbt_api: Api, + database: t.Optional[str] = None, + schema: t.Optional[str] = None, + identifier: t.Optional[str] = None, + version: t.Optional[int] = None, + sqlmesh_model_name: t.Optional[str] = None, +) -> BaseRelation: + if version: + raise ValueError("dbt model versions are not supported in converted projects.") + + return dbt_api.Relation.create(database=database, schema=schema, identifier=identifier) + + +def migrated_source( + dbt_api: Api, + database: t.Optional[str] = None, + schema: t.Optional[str] = None, + identifier: t.Optional[str] = None, +) -> BaseRelation: + return dbt_api.Relation.create(database=database, schema=schema, identifier=identifier) + + +def create_builtin_globals( + jinja_macros: JinjaMacroRegistry, + global_vars: t.Dict[str, t.Any], + engine_adapter: t.Optional[EngineAdapter], + *args: t.Any, + **kwargs: t.Any, +) -> t.Dict[str, t.Any]: + import sqlmesh.utils.jinja as sqlmesh_native_jinja + import sqlmesh.dbt.builtin as sqlmesh_dbt_jinja + + # Capture dialect before the dbt builtins pops it + dialect = global_vars.get("dialect") + + sqlmesh_native_globals = sqlmesh_native_jinja.create_builtin_globals( + jinja_macros, global_vars, *args, **kwargs + ) + + if this_model := global_vars.get("this_model"): + # create a DBT-compatible version of @this_model for {{ this }} + if isinstance(this_model, str): + if not dialect: + raise ConfigError("No dialect?") + + # in audits, `this_model` is a SQL SELECT query that selects from the current table + # elsewhere, it's a fqn string + parsed: exp.Expression = exp.maybe_parse(this_model, dialect=dialect) + + table: t.Optional[exp.Table] = None + if isinstance(parsed, exp.Column): + table = exp.to_table(this_model, dialect=dialect) + elif isinstance(parsed, exp.Query): + table = parsed.find(exp.Table) + else: + raise ConfigError(f"Not sure how to handle this_model: {this_model}") + + if table: + # sqlmesh_dbt_jinja.create_builtin_globals() will construct a Relation for {{ this }} based on the supplied dict + global_vars["this"] = { + "database": table.catalog, + "schema": table.db, + "identifier": table.name, + } + + else: + raise ConfigError(f"Unhandled this_model type: {type(this_model)}") + + sqlmesh_dbt_globals = sqlmesh_dbt_jinja.create_builtin_globals( + jinja_macros, global_vars, engine_adapter, *args, **kwargs + ) + + def source(dbt_api: Api, source_name: str, table_name: str) -> BaseRelation: + # some source() calls cant be converted to __migrated_source() calls because they contain dynamic parameters + # this is a fallback and will be wrong in some situations because `sources` in DBT can be aliased in config + # TODO: maybe we migrate sources into the SQLMesh variables so we can look them up here? + return dbt_api.Relation.create(database=source_name, identifier=table_name) + + def ref(dbt_api: Api, ref_name: str, package: t.Optional[str] = None) -> BaseRelation: + # some ref() calls cant be converted to __migrated_ref() calls because they contain dynamic parameters + raise NotImplementedError( + f"Unable to resolve ref: {ref_name}. Please replace it with an actual model name or use a SQLMesh macro to generate dynamic model name." + ) + + dbt_compatibility_shims = { + "dialect": dialect, + "__migrated_ref": functools.partial(migrated_ref, sqlmesh_dbt_globals["api"]), + "__migrated_source": functools.partial(migrated_source, sqlmesh_dbt_globals["api"]), + "source": functools.partial(source, sqlmesh_dbt_globals["api"]), + "ref": functools.partial(ref, sqlmesh_dbt_globals["api"]), + # make {{ config(...) }} a no-op, some macros call it but its meaningless in a SQLMesh Native project + "config": lambda *_args, **_kwargs: None, + } + + return {**sqlmesh_native_globals, **sqlmesh_dbt_globals, **dbt_compatibility_shims} diff --git a/sqlmesh/dbt/converter/jinja_transforms.py b/sqlmesh/dbt/converter/jinja_transforms.py new file mode 100644 index 0000000000..4c4cf03edc --- /dev/null +++ b/sqlmesh/dbt/converter/jinja_transforms.py @@ -0,0 +1,465 @@ +import typing as t +from types import MappingProxyType +from sqlmesh.core.model import Model +from jinja2 import Environment +import jinja2.nodes as j +from sqlmesh.dbt.converter.common import ( + SQLMESH_PREDEFINED_MACRO_VARIABLES, + JinjaTransform, + SQLGlotTransform, +) +from dbt.adapters.base.relation import BaseRelation +from sqlmesh.core.dialect import normalize_model_name +from sqlglot import exp +import sqlmesh.core.dialect as d +from functools import wraps + + +def _make_standalone_call_transform(fn_name: str, handler: JinjaTransform) -> JinjaTransform: + """ + Creates a transform that identifies standalone Call nodes (that arent nested in other Call nodes) and replaces them with nodes + containing the result of the handler() function + """ + + def _handle( + node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> t.Optional[j.Node]: + if isinstance(node, j.Call): + if isinstance(parent, (j.Call, j.List, j.Keyword)): + return node + + if (name := node.find(j.Name)) and name.name == fn_name: + return handler(node, prev, parent) + + return node + + return _handle + + +def _make_single_expression_transform( + mapping: t.Union[ + t.Dict[str, str], + t.Callable[[j.Node, t.Optional[j.Node], t.Optional[j.Node], str], t.Optional[str]], + ], +) -> JinjaTransform: + """ + Creates a transform that looks for standalone {{ expression }} nodes + It then looks up 'expression' in the provided mapping and replaces it with a TemplateData node containing the value + """ + + def _handle(node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node]) -> j.Node: + # the assumption is that individual expressions are nested in between TemplateData + if prev and not isinstance(prev, j.TemplateData): + return node + + if isinstance(node, j.Name) and not isinstance(parent, j.Getattr): + if isinstance(mapping, dict): + result = mapping.get(node.name) + else: + result = mapping(node, prev, parent, node.name) + if result is not None: + return j.TemplateData(result) + + return node + + return _handle + + +def _dbt_relation_to_model_name( + models: MappingProxyType[str, t.Union[Model, str]], relation: BaseRelation, dialect: str +) -> t.Optional[str]: + model_fqn = normalize_model_name( + table=relation.render(), default_catalog=relation.database, dialect=dialect + ) + if resolved_value := models.get(model_fqn): + return resolved_value if isinstance(resolved_value, str) else resolved_value.name + return None + + +def _dbt_relation_to_kwargs(relation: BaseRelation) -> t.List[j.Keyword]: + kwargs = [] + if database := relation.database: + kwargs.append(j.Keyword("database", j.Const(database))) + if schema := relation.schema: + kwargs.append(j.Keyword("schema", j.Const(schema))) + if identifier := relation.identifier: + kwargs.append(j.Keyword("identifier", j.Const(identifier))) + return kwargs + + +ASTTransform = t.TypeVar("ASTTransform", JinjaTransform, SQLGlotTransform) + + +def ast_transform(fn: t.Callable[..., ASTTransform]) -> t.Callable[..., ASTTransform]: + """ + Decorator to mark functions as being Jinja or SQLGlot AST transforms + + The purpose is to set __name__ to be the outer function name so that the transforms have stable names for an exclude list + The function itself as well as the ASTTransform returned by the function should have the same __name__ for this to work + """ + + @wraps(fn) + def wrapper(*args: t.Any, **kwargs: t.Any) -> ASTTransform: + result = fn(*args, **kwargs) + result.__name__ = fn.__name__ + return result + + return wrapper + + +@ast_transform +def resolve_dbt_ref_to_model_name( + models: MappingProxyType[str, t.Union[Model, str]], env: Environment, dialect: str +) -> JinjaTransform: + """ + Takes an expression like "{{ ref('foo') }}" + And turns it into "sqlmesh.foo" based on the provided list of models and resolver() function + + Args: + models: A dict of models (or model names) keyed by model fqn + jinja_env: Should contain an implementation of {{ ref() }} to turn a DBT relation name into a DBT relation object + + Returns: + A string containing the **model name** (not fqn) of the model referenced by the DBT "{{ ref() }}" call + """ + + ref: t.Callable = env.globals["ref"] # type: ignore + + def _resolve( + node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> t.Optional[j.Node]: + if isinstance(node, j.Call) and node.args and isinstance(node.args[0], j.Const): + ref_name = node.args[0].value + version = None + if version_kwarg := next((k for k in node.kwargs if k.key in ("version", "v")), None): + if isinstance(version_kwarg.value, j.Const): + version = version_kwarg.value.value + else: + # the version arg is present but its some kind of dynamic runtime value + # this means we cant resolve the ref to a model + return node + + if relation := ref(ref_name, version=version): + if not isinstance(relation, BaseRelation): + raise ValueError( + f"ref() returned non-relation type for '{ref_name}': {relation}" + ) + if model_name := _dbt_relation_to_model_name(models, relation, dialect): + return j.TemplateData(model_name) + return j.TemplateData(f"__unresolved_ref__.{ref_name}") + + return node + + return _make_standalone_call_transform("ref", _resolve) + + +@ast_transform +def rewrite_dbt_ref_to_migrated_ref( + models: MappingProxyType[str, t.Union[Model, str]], env: Environment, dialect: str +) -> JinjaTransform: + """ + Takes an expression like "{{ ref('foo') }}" + And turns it into "{{ __migrated_ref(database='foo', schema='bar', identifier='baz', sqlmesh_model_name='') }}" + so that the SQLMesh Native loader can construct a Relation instance without needing the Context + + Args: + models: A dict of models (or model names) keyed by model fqn + jinja_env: Should contain an implementation of {{ ref() }} to turn a DBT relation name into a DBT relation object + + Returns: + A new Call node with enough data to reconstruct the Relation + """ + + ref: t.Callable = env.globals["ref"] # type: ignore + + def _rewrite( + node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> t.Optional[j.Node]: + if isinstance(node, j.Call) and isinstance(node.node, j.Name) and node.node.name == "ref": + if node.args and isinstance(node.args[0], j.Const): + ref_name = node.args[0].value + version_kwarg = next((k for k in node.kwargs if k.key == "version"), None) + if (relation := ref(ref_name)) and isinstance(relation, BaseRelation): + if model_name := _dbt_relation_to_model_name(models, relation, dialect): + kwargs = _dbt_relation_to_kwargs(relation) + if version_kwarg: + kwargs.append(version_kwarg) + kwargs.append(j.Keyword("sqlmesh_model_name", j.Const(model_name))) + return j.Call(j.Name("__migrated_ref", "load"), [], kwargs, None, None) + + return node + + return _rewrite + + +@ast_transform +def resolve_dbt_source_to_model_name( + models: MappingProxyType[str, t.Union[Model, str]], env: Environment, dialect: str +) -> JinjaTransform: + """ + Takes an expression like "{{ source('foo', 'bar') }}" + And turns it into "foo.bar" based on the provided list of models and resolver() function + + Args: + models: A dict of models (or model names) keyed by model fqn + jinja_env: Should contain an implementation of {{ source() }} to turn a DBT source name / table name into a DBT relation object + + Returns: + A string containing the table fqn of the external table referenced by the DBT "{{ source() }}" call + """ + source: t.Callable = env.globals["source"] # type: ignore + + def _resolve( + node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> t.Optional[j.Node]: + if isinstance(node, j.Call) and isinstance(parent, (j.TemplateData, j.Output)): + if ( + len(node.args) == 2 + and isinstance(node.args[0], j.Const) + and isinstance(node.args[1], j.Const) + ): + source_name = node.args[0].value + table_name = node.args[1].value + if relation := source(source_name, table_name): + if not isinstance(relation, BaseRelation): + raise ValueError( + f"source() returned non-relation type for '{source_name}.{table_name}': {relation}" + ) + if model_name := _dbt_relation_to_model_name(models, relation, dialect): + return j.TemplateData(model_name) + return j.TemplateData(relation.render()) + # source() didnt resolve anything, just pass through the arguments verbatim + return j.TemplateData(f"{source_name}.{table_name}") + + return node + + return _make_standalone_call_transform("source", _resolve) + + +@ast_transform +def rewrite_dbt_source_to_migrated_source( + models: MappingProxyType[str, t.Union[Model, str]], env: Environment, dialect: str +) -> JinjaTransform: + """ + Takes an expression like "{{ source('foo', 'bar') }}" + And turns it into "{{ __migrated_source(database='foo', identifier='bar') }}" + so that the SQLMesh Native loader can construct a Relation instance without needing the Context + + Args: + models: A dict of models (or model names) keyed by model fqn + jinja_env: Should contain an implementation of {{ source() }} to turn a DBT source name / table name into a DBT relation object + + Returns: + A new Call node with enough data to reconstruct the Relation + """ + + source: t.Callable = env.globals["source"] # type: ignore + + def _rewrite( + node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> t.Optional[j.Node]: + if ( + isinstance(node, j.Call) + and isinstance(node.node, j.Name) + and node.node.name == "source" + ): + if ( + len(node.args) == 2 + and isinstance(node.args[0], j.Const) + and isinstance(node.args[1], j.Const) + ): + source_name = node.args[0].value + table_name = node.args[1].value + if (relation := source(source_name, table_name)) and isinstance( + relation, BaseRelation + ): + kwargs = _dbt_relation_to_kwargs(relation) + return j.Call(j.Name("__migrated_source", "load"), [], kwargs, None, None) + + return node + + return _rewrite + + +@ast_transform +def resolve_dbt_this_to_model_name(model_name: str) -> JinjaTransform: + """ + Takes an expression like "{{ this }}" and turns it into the provided "model_name" string + """ + return _make_single_expression_transform({"this": model_name}) + + +@ast_transform +def deduplicate_incremental_checks() -> JinjaTransform: + """ + Some files may have been designed to run with both the SQLMesh DBT loader and DBT itself and contain sections like: + + --- + select * from foo + where + {% if is_incremental() %}ds > (select max(ds)) from {{ this }}{% endif %} + {% if sqlmesh_incremental is defined %}ds BETWEEN {{ start_ds }} and {{ end_ds }}{% endif %} + --- + + This is transform detects usages of {% if sqlmesh_incremental ... %} + If it finds them, it: + - removes occurances of {% if is_incremental() %} in favour of the {% if sqlmesh_incremental %} check + + If no instances of {% if sqlmesh_incremental %} are found, nothing changes + + For for example, the above will be transformed into: + --- + select * from foo + where + ds BETWEEN {{ start_ds }} and {{ end_ds }} + --- + + But if it didnt contain the {% if sqlmesh_incremental %} block, this transform would output: + --- + select * from foo + where + {% if is_incremental() %}ds > (select max(ds)) from {{ this }}){% endif %} + --- + + """ + has_sqlmesh_incremental = False + + def _handle( + node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> t.Optional[j.Node]: + nonlocal has_sqlmesh_incremental + + if isinstance(node, j.Template): + for if_node in node.find_all(j.If): + if test_name := if_node.test.find(j.Name): + if test_name.name == "sqlmesh_incremental": + has_sqlmesh_incremental = True + + # only remove the {% if is_incremental() %} checks in the present of {% sqlmesh_incremental is defined %} checks + if has_sqlmesh_incremental: + if isinstance(node, j.If) and node.test: + if test_name := node.test.find(j.Name): + if test_name.name == "is_incremental": + return None + + return node + + return _handle + + +@ast_transform +def unpack_incremental_checks() -> JinjaTransform: + """ + This takes queries like: + + > select * from foo where {% if sqlmesh_incremental is defined %}ds BETWEEN {{ start_ds }} and {{ end_ds }}{% endif %} + > select * from foo where {% if is_incremental() %}ds > (select max(ds)) from foo.table){% endif %} + + And, if possible, removes the {% if sqlmesh_incremental is defined %} / {% is_incremental %} block to achieve: + + > select * from foo where ds BETWEEN {{ start_ds }} and {{ end_ds }} + > select * from foo where ds > (select max(ds)) from foo.table) + + Note that if there is a {% else %} portion to the block, there is no SQLMesh equivalent so in that case the check is untouched. + + Also, if both may be present in a model, run the deduplicate_incremental_checks() transform first so only one gets unpacked by this transform + """ + + def _handle(node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node]) -> j.Node: + if isinstance(node, j.If) and node.test: + if test_name := node.test.find(j.Name): + if ( + test_name.name in ("is_incremental", "sqlmesh_incremental") + and not node.elif_ + and not node.else_ + ): + return j.Output(node.body) + + return node + + return _handle + + +@ast_transform +def rewrite_sqlmesh_predefined_variables_to_sqlmesh_macro_syntax() -> JinjaTransform: + """ + If there are SQLMesh predefined variables in Jinja form, eg "{{ start_dt }}" + Rewrite them to eg "@start_dt" + + Example: + + select * from foo where ds between {{ start_dt }} and {{ end_dt }} + + > select * from foo where ds between @start_dt and @end_dt + """ + + mapping = {v: f"@{v}" for v in SQLMESH_PREDEFINED_MACRO_VARIABLES} + + literal_remapping = {"dt": "ts", "date": "ds"} + + def _mapping_func( + node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node], name: str + ) -> t.Optional[str]: + wrapped_in_literal = False + if prev and isinstance(prev, j.TemplateData): + data = prev.data.strip() + if data.endswith("'"): + wrapped_in_literal = True + + if wrapped_in_literal: + for original, new in literal_remapping.items(): + if name.endswith(original): + name = name.removesuffix(original) + new + + return mapping.get(name) + + return _make_single_expression_transform(_mapping_func) + + +@ast_transform +def append_dbt_package_kwarg_to_var_calls(package_name: t.Optional[str]) -> JinjaTransform: + """ " + If there are calls like: + + > {% if 'col_name' in var('history_columns') %} + + Assuming package_name=foo, change it to: + + > {% if 'col_name' in var('history_columns', __dbt_package="foo") %} + + The point of this is to give a hint to the "var" shim in SQLMesh Native so it knows which key + under "__dbt_packages__" in the project variables to look for + """ + + def _append( + node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] + ) -> t.Optional[j.Node]: + if package_name and isinstance(node, j.Call): + node.kwargs.append(j.Keyword("__dbt_package", j.Const(package_name))) + return node + + return _make_standalone_call_transform("var", _append) + + +@ast_transform +def unwrap_macros_in_string_literals() -> SQLGlotTransform: + """ + Given a query containing string literals *that match SQLMesh predefined macro variables* like: + + > select * from foo where ds between '@start_dt' and '@end_dt' + + Unwrap them into: + + > select * from foo where ds between @start_dt and @end_dt + """ + values_to_check = {f"@{var}": var for var in SQLMESH_PREDEFINED_MACRO_VARIABLES} + + def _transform(e: exp.Expression) -> exp.Expression: + if isinstance(e, exp.Literal) and e.is_string: + if (value := e.text("this")) and value in values_to_check: + return d.MacroVar( + this=values_to_check[value] + ) # MacroVar adds in the @ so dont want to add it twice + return e + + return _transform diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 4f4100f092..672ad1ac3e 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -99,11 +99,12 @@ def _load_scripts(self) -> t.Tuple[MacroRegistry, JinjaMacroRegistry]: for file in macro_files: self._track_file(file) - # This doesn't do anything, the actual content will be loaded from the manifest - return ( - macro.get_registry(), - JinjaMacroRegistry(), - ) + jinja_macros = JinjaMacroRegistry() + for project in self._load_projects(): + jinja_macros = jinja_macros.merge(project.context.jinja_macros) + jinja_macros.add_globals(project.context.jinja_globals) + + return (macro.get_registry(), jinja_macros) def _load_models( self, diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 51cfd06c88..4cbca09aee 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -567,6 +567,7 @@ def to_sqlmesh( kind=kind, start=self.start, audit_definitions=audit_definitions, + path=model_kwargs.pop("path", self.path), # This ensures that we bypass query rendering that would otherwise be required to extract additional # dependencies from the model's SQL. # Note: any table dependencies that are not referenced using the `ref` macro will not be included. diff --git a/sqlmesh/dbt/target.py b/sqlmesh/dbt/target.py index e7603232e8..05985d8762 100644 --- a/sqlmesh/dbt/target.py +++ b/sqlmesh/dbt/target.py @@ -83,26 +83,8 @@ def load(cls, data: t.Dict[str, t.Any]) -> TargetConfig: The configuration of the provided profile target """ db_type = data["type"] - if db_type == "databricks": - return DatabricksConfig(**data) - if db_type == "duckdb": - return DuckDbConfig(**data) - if db_type == "postgres": - return PostgresConfig(**data) - if db_type == "redshift": - return RedshiftConfig(**data) - if db_type == "snowflake": - return SnowflakeConfig(**data) - if db_type == "bigquery": - return BigQueryConfig(**data) - if db_type == "sqlserver": - return MSSQLConfig(**data) - if db_type == "trino": - return TrinoConfig(**data) - if db_type == "clickhouse": - return ClickhouseConfig(**data) - if db_type == "athena": - return AthenaConfig(**data) + if config_class := TARGET_TYPE_TO_CONFIG_CLASS.get(db_type): + return config_class(**data) raise ConfigError(f"{db_type} not supported.") @@ -114,6 +96,10 @@ def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: """Converts target config to SQLMesh connection config""" raise NotImplementedError + @classmethod + def from_sqlmesh(cls, config: ConnectionConfig, **kwargs: t.Dict[str, t.Any]) -> "TargetConfig": + raise NotImplementedError + def attribute_dict(self) -> AttributeDict: fields = self.dict(include=SERIALIZABLE_FIELDS).copy() fields["target_name"] = self.name @@ -202,6 +188,18 @@ def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: **kwargs, ) + @classmethod + def from_sqlmesh(cls, config: ConnectionConfig, **kwargs: t.Dict[str, t.Any]) -> "DuckDbConfig": + if not isinstance(config, DuckDBConnectionConfig): + raise ValueError(f"Incorrect config type: {type(config)}") + + return cls( + path=config.database, + extensions=config.extensions, + settings=config.connector_config, + **kwargs, + ) + class SnowflakeConfig(TargetConfig): """ @@ -372,6 +370,28 @@ def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: **kwargs, ) + @classmethod + def from_sqlmesh( + cls, config: ConnectionConfig, **kwargs: t.Dict[str, t.Any] + ) -> "PostgresConfig": + if not isinstance(config, PostgresConnectionConfig): + raise ValueError(f"Incorrect config type: {type(config)}") + + return cls( + schema="public", + host=config.host, + user=config.user, + password=config.password, + port=config.port, + dbname=config.database, + keepalives_idle=config.keepalives_idle, + threads=config.concurrent_tasks, + connect_timeout=config.connect_timeout, + role=config.role, + sslmode=config.sslmode, + **kwargs, + ) + class RedshiftConfig(TargetConfig): """ @@ -613,6 +633,39 @@ def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: **kwargs, ) + @classmethod + def from_sqlmesh( + cls, config: ConnectionConfig, **kwargs: t.Dict[str, t.Any] + ) -> "BigQueryConfig": + if not isinstance(config, BigQueryConnectionConfig): + raise ValueError(f"Incorrect config type: {type(config)}") + + return cls( + schema="__unknown__", + method=config.method, + project=config.project, + execution_project=config.execution_project, + quota_project=config.quota_project, + location=config.location, + threads=config.concurrent_tasks, + keyfile=config.keyfile, + keyfile_json=config.keyfile_json, + token=config.token, + refresh_token=config.refresh_token, + client_id=config.client_id, + client_secret=config.client_secret, + token_uri=config.token_uri, + scopes=config.scopes, + impersonated_service_account=config.impersonated_service_account, + job_creation_timeout_seconds=config.job_creation_timeout_seconds, + job_execution_timeout_seconds=config.job_execution_timeout_seconds, + job_retries=config.job_retries, + job_retry_deadline_seconds=config.job_retry_deadline_seconds, + priority=config.priority, + maximum_bytes_billed=config.maximum_bytes_billed, + **kwargs, + ) + class MSSQLConfig(TargetConfig): """ diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index dcb09296b8..711f760b7b 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -22,6 +22,7 @@ CallNames = t.Tuple[t.Tuple[str, ...], t.Union[nodes.Call, nodes.Getattr]] SQLMESH_JINJA_PACKAGE = "sqlmesh.utils.jinja" +SQLMESH_DBT_COMPATIBILITY_PACKAGE = "sqlmesh.dbt.converter.jinja_builtins" def environment(**kwargs: t.Any) -> Environment: @@ -94,7 +95,11 @@ def extract(self, jinja: str, dialect: str = "") -> t.Dict[str, MacroInfo]: macro_str = self._find_sql(macro_start, self._next) macros[name] = MacroInfo( definition=macro_str, - depends_on=list(extract_macro_references_and_variables(macro_str)[0]), + depends_on=list( + extract_macro_references_and_variables(macro_str, dbt_target_name=dialect)[ + 0 + ] + ), ) self._advance() @@ -166,18 +171,86 @@ def parse() -> t.List[CallNames]: return parse() +def extract_dbt_adapter_dispatch_targets(jinja_str: str) -> t.List[t.Tuple[str, t.Optional[str]]]: + """ + Given a jinja string, identify {{ adapter.dispatch('foo','bar') }} calls and extract the (foo, bar) part as a tuple + """ + ast = ENVIRONMENT.parse(jinja_str) + + extracted = [] + + def _extract(node: nodes.Node, parent: t.Optional[nodes.Node] = None) -> None: + if ( + isinstance(node, nodes.Getattr) + and isinstance(parent, nodes.Call) + and (node_name := node.find(nodes.Name)) + ): + if node_name.name == "adapter" and node.attr == "dispatch": + call_args = [arg.value for arg in parent.args if isinstance(arg, nodes.Const)][0:2] + if len(call_args) == 1: + call_args.append(None) + macro_name, package = call_args + extracted.append((macro_name, package)) + + for child_node in node.iter_child_nodes(): + _extract(child_node, parent=node) + + _extract(ast) + + return extracted + + def extract_macro_references_and_variables( - *jinja_strs: str, + *jinja_strs: str, dbt_target_name: t.Optional[str] = None ) -> t.Tuple[t.Set[MacroReference], t.Set[str]]: macro_references = set() variables = set() for jinja_str in jinja_strs: + if dbt_target_name and "adapter.dispatch" in jinja_str: + for dispatch_target_name, package in extract_dbt_adapter_dispatch_targets(jinja_str): + # here we are guessing at the macro names that the {{ adapter.dispatch() }} call will invoke + # there is a defined resolution order: https://docs.getdbt.com/reference/dbt-jinja-functions/dispatch + # we rely on JinjaMacroRegistry.trim() to tune the dependencies down into just the ones that actually exist + macro_references.add( + MacroReference(package=package, name=f"default__{dispatch_target_name}") + ) + macro_references.add( + MacroReference( + package=package, name=f"{dbt_target_name}__{dispatch_target_name}" + ) + ) + if package and package.startswith("dbt"): + # handle the case where macros like `current_timestamp()` in the `dbt` package expect an implementation in eg the `dbt_bigquery` package + macro_references.add( + MacroReference( + package=f"dbt_{dbt_target_name}", + name=f"{dbt_target_name}__{dispatch_target_name}", + ) + ) + for call_name, node in extract_call_names(jinja_str): if call_name[0] == c.VAR: assert isinstance(node, nodes.Call) args = [jinja_call_arg_name(arg) for arg in node.args] if args and args[0]: - variables.add(args[0].lower()) + variable_name = args[0].lower() + + # check if this {{ var() }} reference is from a migrated DBT package + # if it is, there will be a __dbt_package= kwarg + dbt_package = next( + ( + kwarg.value + for kwarg in node.kwargs + if isinstance(kwarg, nodes.Keyword) and kwarg.key == "__dbt_package" + ), + None, + ) + if dbt_package and isinstance(dbt_package, nodes.Const): + dbt_package = dbt_package.value + # this convention is a flat way of referencing the nested values under `__dbt_packages__` in the SQLMesh project variables + variable_name = f"{c.MIGRATED_DBT_PACKAGES}.{dbt_package}.{variable_name}" + + variables.add(variable_name) elif call_name[0] == c.GATEWAY: variables.add(c.GATEWAY) elif len(call_name) == 1: @@ -255,6 +328,19 @@ def _convert( def trimmed(self) -> bool: return self._trimmed + @property + def all_macros(self) -> t.Iterable[t.Tuple[t.Optional[str], str, MacroInfo]]: + """ + Returns (package, macro_name, MacroInfo) tuples for every macro in this registry + Root macros will have package=None + """ + for name, macro in self.root_macros.items(): + yield None, name, macro + + for package, macros in self.packages.items(): + for name, macro in macros.items(): + yield (package, name, macro) + def add_macros(self, macros: t.Dict[str, MacroInfo], package: t.Optional[str] = None) -> None: """Adds macros to the target package. @@ -593,7 +679,12 @@ def jinja_call_arg_name(node: nodes.Node) -> str: def create_var(variables: t.Dict[str, t.Any]) -> t.Callable: - def _var(var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]: + def _var( + var_name: str, default: t.Optional[t.Any] = None, **kwargs: t.Any + ) -> t.Optional[t.Any]: + if dbt_package := kwargs.get("__dbt_package"): + var_name = f"{c.MIGRATED_DBT_PACKAGES}.{dbt_package}.{var_name}" + value = variables.get(var_name.lower(), default) if isinstance(value, SqlValue): return value.sql diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 44ef495737..dea9fb16da 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -28,6 +28,7 @@ from sqlmesh.core.engine_adapter.athena import AthenaEngineAdapter from sqlmesh.core.engine_adapter.duckdb import DuckDBEngineAdapter from sqlmesh.core.engine_adapter.redshift import RedshiftEngineAdapter +from sqlmesh.core.loader import MigratedDbtProjectLoader from sqlmesh.core.notification_target import ConsoleNotificationTarget from sqlmesh.core.user import User from sqlmesh.utils.errors import ConfigError @@ -1028,3 +1029,29 @@ def test_config_complex_types_supplied_as_json_strings_from_env(tmp_path: Path) assert conn.project == "unit-test" assert conn.scopes == ("a", "b", "c") assert conn.keyfile_json == {"foo": "bar"} + + +def test_loader_for_migrated_dbt_project(tmp_path: Path): + config_path = tmp_path / "config.yaml" + config_path.write_text(""" + gateways: + bigquery: + connection: + type: bigquery + project: unit-test + + default_gateway: bigquery + + model_defaults: + dialect: bigquery + + variables: + __dbt_project_name__: sushi +""") + + config = load_config_from_paths( + Config, + project_paths=[config_path], + ) + + assert config.loader == MigratedDbtProjectLoader diff --git a/tests/core/test_loader.py b/tests/core/test_loader.py index 2c648e7718..a616f520ef 100644 --- a/tests/core/test_loader.py +++ b/tests/core/test_loader.py @@ -4,6 +4,9 @@ from sqlmesh.core.config import Config, ModelDefaultsConfig from sqlmesh.core.context import Context from sqlmesh.utils.errors import ConfigError +import sqlmesh.core.constants as c +from sqlmesh.core.config import load_config_from_yaml +from sqlmesh.utils.yaml import dump @pytest.fixture @@ -201,3 +204,129 @@ def my_model(context, **kwargs): assert model.description == "model_payload_a" path_b.write_text(model_payload_b) context.load() # raise no error to duplicate key if the functions are identical (by registry class_method) + + +def test_load_migrated_dbt_adapter_dispatch_macros(tmp_path: Path): + init_example_project(tmp_path, dialect="duckdb") + + migrated_package_path = tmp_path / "macros" / c.MIGRATED_DBT_PACKAGES / "dbt_utils" + migrated_package_path.mkdir(parents=True) + + (migrated_package_path / "deduplicate.sql").write_text(""" + {%- macro deduplicate(relation) -%} + {{ return(adapter.dispatch('deduplicate', 'dbt_utils')(relation)) }} + {% endmacro %} + """) + + (migrated_package_path / "default__deduplicate.sql").write_text(""" + {%- macro default__deduplicate(relation) -%} + select 'default impl' from {{ relation }} + {% endmacro %} + """) + + (migrated_package_path / "duckdb__deduplicate.sql").write_text(""" + {%- macro duckdb__deduplicate(relation) -%} + select 'duckdb impl' from {{ relation }} + {% endmacro %} + """) + + # this should be pruned from the JinjaMacroRegistry because the target is duckdb, not bigquery + (migrated_package_path / "bigquery__deduplicate.sql").write_text(""" + {%- macro bigquery__deduplicate(relation) -%} + select 'bigquery impl' from {{ relation }} + {% endmacro %} + """) + + (tmp_path / "models" / "test_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.test, + kind FULL, + ); +JINJA_QUERY_BEGIN; +{{ dbt_utils.deduplicate(__migrated_ref(schema='sqlmesh_example', identifier='full_model')) }} +JINJA_END; + """) + + config_path = tmp_path / "config.yaml" + assert config_path.exists() + config = load_config_from_yaml(config_path) + config["variables"] = {} + config["variables"][c.MIGRATED_DBT_PROJECT_NAME] = "test" + + config_path.write_text(dump(config)) + + ctx = Context(paths=tmp_path) + + model = ctx.models['"db"."sqlmesh_example"."test"'] + assert model.dialect == "duckdb" + assert {(package, name) for package, name, _ in model.jinja_macros.all_macros} == { + ("dbt_utils", "deduplicate"), + ("dbt_utils", "default__deduplicate"), + ("dbt_utils", "duckdb__deduplicate"), + } + + assert ( + model.render_query_or_raise().sql(dialect="duckdb") + == """SELECT \'duckdb impl\' AS "duckdb impl" FROM "db"."sqlmesh_example"."full_model" AS "full_model\"""" + ) + + +def test_load_migrated_dbt_adapter_dispatch_macros_in_different_packages(tmp_path: Path): + # some things like dbt.current_timestamp() dispatch to macros in a different package + init_example_project(tmp_path, dialect="duckdb") + + migrated_package_path_dbt = tmp_path / "macros" / c.MIGRATED_DBT_PACKAGES / "dbt" + migrated_package_path_dbt_duckdb = tmp_path / "macros" / c.MIGRATED_DBT_PACKAGES / "dbt_duckdb" + migrated_package_path_dbt.mkdir(parents=True) + migrated_package_path_dbt_duckdb.mkdir(parents=True) + + (migrated_package_path_dbt / "current_timestamp.sql").write_text(""" + {%- macro current_timestamp(relation) -%} + {{ return(adapter.dispatch('current_timestamp', 'dbt')()) }} + {% endmacro %} + """) + + (migrated_package_path_dbt / "default__current_timestamp.sql").write_text(""" + {% macro default__current_timestamp() -%} + {{ exceptions.raise_not_implemented('current_timestamp macro not implemented') }} + {%- endmacro %} + """) + + (migrated_package_path_dbt_duckdb / "duckdb__current_timestamp.sql").write_text(""" + {%- macro duckdb__current_timestamp() -%} + 'duckdb current_timestamp impl' + {% endmacro %} + """) + + (tmp_path / "models" / "test_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.test, + kind FULL, + ); +JINJA_QUERY_BEGIN; +select {{ dbt.current_timestamp() }} as a +JINJA_END; + """) + + config_path = tmp_path / "config.yaml" + assert config_path.exists() + config = load_config_from_yaml(config_path) + config["variables"] = {} + config["variables"][c.MIGRATED_DBT_PROJECT_NAME] = "test" + + config_path.write_text(dump(config)) + + ctx = Context(paths=tmp_path) + + model = ctx.models['"db"."sqlmesh_example"."test"'] + assert model.dialect == "duckdb" + assert {(package, name) for package, name, _ in model.jinja_macros.all_macros} == { + ("dbt", "current_timestamp"), + ("dbt", "default__current_timestamp"), + ("dbt_duckdb", "duckdb__current_timestamp"), + } + + assert ( + model.render_query_or_raise().sql(dialect="duckdb") + == "SELECT 'duckdb current_timestamp impl' AS \"a\"" + ) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 7a2f808e12..ad083e9ae2 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -15,6 +15,7 @@ from sqlmesh.cli.example_project import init_example_project, ProjectTemplate from sqlmesh.core.environment import EnvironmentNamingInfo from sqlmesh.core.model.kind import TimeColumn, ModelKindName +from pydantic import ValidationError from sqlmesh import CustomMaterialization, CustomKind from pydantic import model_validator, ValidationError @@ -42,6 +43,7 @@ FullKind, IncrementalByTimeRangeKind, IncrementalUnmanagedKind, + IncrementalByUniqueKeyKind, ModelCache, ModelMeta, SeedKind, @@ -63,7 +65,13 @@ from sqlmesh.core.snapshot import Snapshot, SnapshotChangeCategory from sqlmesh.utils.date import TimeLike, to_datetime, to_ds, to_timestamp from sqlmesh.utils.errors import ConfigError, SQLMeshError, LinterError -from sqlmesh.utils.jinja import JinjaMacroRegistry, MacroInfo, MacroExtractor +from sqlmesh.utils.jinja import ( + JinjaMacroRegistry, + MacroInfo, + MacroExtractor, + MacroReference, + SQLMESH_DBT_COMPATIBILITY_PACKAGE, +) from sqlmesh.utils.metaprogramming import Executable, SqlValue from sqlmesh.core.macros import RuntimeStage from tests.utils.test_helpers import use_terminal_console @@ -6171,6 +6179,58 @@ def model_with_variables(context, **kwargs): assert df.to_dict(orient="records") == [{"a": "test_value", "b": "default_value", "c": None}] +def test_variables_migrated_dbt_package_macro(): + expressions = parse( + """ + MODEL( + name test_model, + kind FULL, + ); + + JINJA_QUERY_BEGIN; + SELECT '{{ var('TEST_VAR_A') }}' as a, '{{ test.test_macro_var() }}' as b + JINJA_END; + """, + default_dialect="bigquery", + ) + + jinja_macros = JinjaMacroRegistry( + create_builtins_module=SQLMESH_DBT_COMPATIBILITY_PACKAGE, + packages={ + "test": { + "test_macro_var": MacroInfo( + definition=""" + {% macro test_macro_var() %} + {{- var('test_var_b', __dbt_package='test') }} + {%- endmacro %}""", + depends_on=[MacroReference(name="var")], + ) + } + }, + ) + + model = load_sql_based_model( + expressions, + variables={ + "test_var_a": "test_var_a_value", + c.MIGRATED_DBT_PACKAGES: { + "test": {"test_var_b": "test_var_b_value", "unused": "unused_value"}, + }, + "test_var_c": "test_var_c_value", + }, + jinja_macros=jinja_macros, + migrated_dbt_project_name="test", + dialect="bigquery", + ) + assert model.python_env[c.SQLMESH_VARS] == Executable.value( + {"test_var_a": "test_var_a_value", "__dbt_packages__.test.test_var_b": "test_var_b_value"} + ) + assert ( + model.render_query().sql(dialect="bigquery") + == "SELECT 'test_var_a_value' AS `a`, 'test_var_b_value' AS `b`" + ) + + def test_load_external_model_python(sushi_context) -> None: @model( "test_load_external_model_python", @@ -7727,6 +7787,37 @@ def test_model_kind_to_expression(): ) +def test_incremental_by_unique_key_batch_concurrency(): + with pytest.raises(ValidationError, match=r"Input should be 1"): + load_sql_based_model( + d.parse(""" + MODEL ( + name db.table, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key a, + batch_concurrency 2 + ) + ); + select 1; + """) + ) + + model = load_sql_based_model( + d.parse(""" + MODEL ( + name db.table, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key a, + batch_concurrency 1 + ) + ); + select 1; + """) + ) + assert isinstance(model.kind, IncrementalByUniqueKeyKind) + assert model.kind.batch_concurrency == 1 + + def test_bad_model_kind(): with pytest.raises( SQLMeshError, diff --git a/tests/dbt/converter/conftest.py b/tests/dbt/converter/conftest.py new file mode 100644 index 0000000000..e8dffeb263 --- /dev/null +++ b/tests/dbt/converter/conftest.py @@ -0,0 +1,21 @@ +from pathlib import Path +import typing as t +import pytest +from sqlmesh.core.context import Context + + +@pytest.fixture +def sushi_dbt_context(copy_to_temp_path: t.Callable) -> Context: + return Context(paths=copy_to_temp_path("examples/sushi_dbt")) + + +@pytest.fixture +def empty_dbt_context(copy_to_temp_path: t.Callable) -> Context: + fixture_path = Path(__file__).parent / "fixtures" / "empty_dbt_project" + assert fixture_path.exists() + + actual_path = copy_to_temp_path(fixture_path)[0] + + ctx = Context(paths=actual_path) + + return ctx diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/.gitignore b/tests/dbt/converter/fixtures/empty_dbt_project/.gitignore new file mode 100644 index 0000000000..232ccd1d8c --- /dev/null +++ b/tests/dbt/converter/fixtures/empty_dbt_project/.gitignore @@ -0,0 +1,2 @@ +target/ +logs/ diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/analyses/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/analyses/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/config.py b/tests/dbt/converter/fixtures/empty_dbt_project/config.py new file mode 100644 index 0000000000..e7e28c98e4 --- /dev/null +++ b/tests/dbt/converter/fixtures/empty_dbt_project/config.py @@ -0,0 +1,7 @@ +from pathlib import Path + +from sqlmesh.dbt.loader import sqlmesh_config + +config = sqlmesh_config(Path(__file__).parent) + +test_config = config diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/dbt_project.yml b/tests/dbt/converter/fixtures/empty_dbt_project/dbt_project.yml new file mode 100644 index 0000000000..007649e553 --- /dev/null +++ b/tests/dbt/converter/fixtures/empty_dbt_project/dbt_project.yml @@ -0,0 +1,22 @@ + +name: 'test' +version: '1.0.0' +config-version: 2 +profile: 'test' + +model-paths: ["models"] +analysis-paths: ["analyses"] +test-paths: ["tests"] +seed-paths: ["seeds"] +macro-paths: ["macros"] +snapshot-paths: ["snapshots"] + +target-path: "target" + +models: + +start: Jan 1 2022 + +seeds: + +schema: raw + +vars: {} diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/macros/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/macros/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/models/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/models/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/models/sources.yml b/tests/dbt/converter/fixtures/empty_dbt_project/models/sources.yml new file mode 100644 index 0000000000..49354831f4 --- /dev/null +++ b/tests/dbt/converter/fixtures/empty_dbt_project/models/sources.yml @@ -0,0 +1,6 @@ +version: 2 + +sources: + - name: external + tables: + - name: orders diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/packages/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/packages/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/profiles.yml b/tests/dbt/converter/fixtures/empty_dbt_project/profiles.yml new file mode 100644 index 0000000000..6d91ecbe65 --- /dev/null +++ b/tests/dbt/converter/fixtures/empty_dbt_project/profiles.yml @@ -0,0 +1,6 @@ +test: + outputs: + in_memory: + type: duckdb + schema: project + target: in_memory diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/seeds/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/seeds/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/seeds/items.csv b/tests/dbt/converter/fixtures/empty_dbt_project/seeds/items.csv new file mode 100644 index 0000000000..0f87cb2507 --- /dev/null +++ b/tests/dbt/converter/fixtures/empty_dbt_project/seeds/items.csv @@ -0,0 +1,94 @@ +id,name,price,ds +0,Maguro,4.34,2022-01-01 +1,Ika,7.35,2022-01-01 +2,Aji,6.06,2022-01-01 +3,Hotate,8.5,2022-01-01 +4,Escolar,8.46,2022-01-01 +5,Sake,4.91,2022-01-01 +6,Tamago,4.94,2022-01-01 +7,Umi Masu,8.61,2022-01-01 +8,Bincho,9.71,2022-01-01 +9,Toro,9.13,2022-01-01 +10,Aoyagi,5.5,2022-01-01 +11,Hamachi,6.51,2022-01-01 +12,Tobiko,7.78,2022-01-01 +13,Unagi,7.99,2022-01-01 +14,Tako,5.59,2022-01-01 +0,Kani,8.22,2022-01-02 +1,Amaebi,9.14,2022-01-02 +2,Uni,4.55,2022-01-02 +3,Sake Toro,5.01,2022-01-02 +4,Maguro,9.95,2022-01-02 +5,Katsuo,9.03,2022-01-02 +6,Hamachi Toro,3.76,2022-01-02 +7,Iwashi,5.56,2022-01-02 +8,Tamago,6.96,2022-01-02 +9,Tai,5.84,2022-01-02 +10,Ika,3.23,2022-01-02 +0,Hirame,7.74,2022-01-03 +1,Uni,3.98,2022-01-03 +2,Tai,4.09,2022-01-03 +3,Kanpachi,7.55,2022-01-03 +4,Tobiko,9.87,2022-01-03 +5,Hotate,7.86,2022-01-03 +6,Iwashi,8.33,2022-01-03 +7,Ikura,5.98,2022-01-03 +8,Maguro,3.97,2022-01-03 +9,Tsubugai,4.51,2022-01-03 +10,Tako,8.35,2022-01-03 +11,Sake,3.38,2022-01-03 +12,Tamago,6.43,2022-01-03 +13,Ika,4.26,2022-01-03 +14,Unagi,7.42,2022-01-03 +0,Ikura,5.02,2022-01-04 +1,Tobiko,9.15,2022-01-04 +2,Hamachi,6.66,2022-01-04 +3,Bincho,8.4,2022-01-04 +4,Tsubugai,5.26,2022-01-04 +5,Hotate,8.92,2022-01-04 +6,Toro,7.52,2022-01-04 +7,Aji,7.49,2022-01-04 +8,Ebi,5.67,2022-01-04 +9,Kanpachi,7.51,2022-01-04 +10,Kani,6.97,2022-01-04 +11,Hirame,4.51,2022-01-04 +0,Saba,7.41,2022-01-05 +1,Unagi,8.45,2022-01-05 +2,Uni,3.67,2022-01-05 +3,Maguro,8.76,2022-01-05 +4,Katsuo,5.99,2022-01-05 +5,Bincho,9.15,2022-01-05 +6,Sake Toro,3.67,2022-01-05 +7,Aji,9.55,2022-01-05 +8,Umi Masu,9.88,2022-01-05 +9,Hamachi,6.53,2022-01-05 +10,Tai,6.83,2022-01-05 +11,Tsubugai,4.62,2022-01-05 +12,Ikura,4.86,2022-01-05 +13,Ahi,9.66,2022-01-05 +14,Hotate,7.85,2022-01-05 +0,Hamachi Toro,4.87,2022-01-06 +1,Ika,3.26,2022-01-06 +2,Kanpachi,8.63,2022-01-06 +3,Hirame,5.34,2022-01-06 +4,Katsuo,9.24,2022-01-06 +5,Iwashi,8.67,2022-01-06 +6,Sake Toro,9.75,2022-01-06 +7,Bincho,9.7,2022-01-06 +8,Aji,7.14,2022-01-06 +9,Hokigai,5.18,2022-01-06 +10,Umi Masu,9.43,2022-01-06 +11,Unagi,3.35,2022-01-06 +12,Sake,4.58,2022-01-06 +13,Aoyagi,5.54,2022-01-06 +0,Amaebi,6.94,2022-01-07 +1,Ebi,7.84,2022-01-07 +2,Saba,5.28,2022-01-07 +3,Anago,4.53,2022-01-07 +4,Escolar,7.28,2022-01-07 +5,Ahi,6.48,2022-01-07 +6,Katsuo,5.16,2022-01-07 +7,Umi Masu,6.09,2022-01-07 +8,Maguro,7.7,2022-01-07 +9,Hokigai,7.37,2022-01-07 +10,Sake Toro,6.99,2022-01-07 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/seeds/properties.yml b/tests/dbt/converter/fixtures/empty_dbt_project/seeds/properties.yml new file mode 100644 index 0000000000..86ce6964fe --- /dev/null +++ b/tests/dbt/converter/fixtures/empty_dbt_project/seeds/properties.yml @@ -0,0 +1,13 @@ +version: 2 + +seeds: + - name: items + columns: + - name: id + description: Item id + - name: name + description: Name of the item + - name: price + description: Price of the item + - name: ds + description: Date \ No newline at end of file diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/snapshots/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/snapshots/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/tests/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/tests/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dbt/converter/fixtures/jinja_nested_if.sql b/tests/dbt/converter/fixtures/jinja_nested_if.sql new file mode 100644 index 0000000000..e7a1bed137 --- /dev/null +++ b/tests/dbt/converter/fixtures/jinja_nested_if.sql @@ -0,0 +1,15 @@ +{% if foo == 'bar' %} + baz + {% if baz == 'bing' %} + bong + {% else %} + qux + {% endif %} +{% elif a == fn(b) %} + {% if c == 'f' and fn1(a, c, 'foo') == 'test' %} + output1 + {% elif z is defined %} + output2 + {% endif %} + output +{% endif %} \ No newline at end of file diff --git a/tests/dbt/converter/fixtures/macro_dbt_incremental.sql b/tests/dbt/converter/fixtures/macro_dbt_incremental.sql new file mode 100644 index 0000000000..a76f60713b --- /dev/null +++ b/tests/dbt/converter/fixtures/macro_dbt_incremental.sql @@ -0,0 +1,11 @@ +{% macro incremental_by_time(col, time_type) %} + {% if is_incremental() %} + WHERE + {{ col }} > (select max({{ col }}) from {{ this }}) + {% endif %} + {% if sqlmesh_incremental is defined %} + {% set dates = incremental_dates_by_time_type(time_type) %} + WHERE + {{ col }} BETWEEN '{{ dates[0] }}' AND '{{ dates[1] }}' + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/tests/dbt/converter/fixtures/macro_func_with_params.sql b/tests/dbt/converter/fixtures/macro_func_with_params.sql new file mode 100644 index 0000000000..06bb757ef9 --- /dev/null +++ b/tests/dbt/converter/fixtures/macro_func_with_params.sql @@ -0,0 +1,17 @@ +{% macro func_with_params(amount, category) %} + case + {% for row in [ + { 'category': '1', 'range': [0, 10], 'consider': True }, + { 'category': '2', 'range': [11, 20], 'consider': None } + ] %} + when {{ category }} = '{{ row.category }}' + and {{ amount }} >= {{ row.range[0] }} + {% if row.consider is not none %} + and {{ amount }} < {{ row.range[1] }} + {% endif %} + then + ({{ amount }} * {{ row.range[0] }} + {{ row.range[1] }}) * 4 + {% endfor %} + else null + end +{% endmacro %} \ No newline at end of file diff --git a/tests/dbt/converter/fixtures/model_query_incremental.sql b/tests/dbt/converter/fixtures/model_query_incremental.sql new file mode 100644 index 0000000000..a9603dbcbb --- /dev/null +++ b/tests/dbt/converter/fixtures/model_query_incremental.sql @@ -0,0 +1,34 @@ +WITH cte AS ( + SELECT + oi.order_id AS order_id, + FROM {{ ref('order_items') }} AS oi + LEFT JOIN {{ ref('items') }} AS i + ON oi.item_id = i.id AND oi.ds = i.ds +{% if is_incremental() %} +WHERE + oi.ds > (select max(ds) from {{ this }}) +{% endif %} +{% if sqlmesh_incremental is defined %} +WHERE + oi.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' +{% endif %} +GROUP BY + oi.order_id, + oi.ds +) +SELECT + o.customer_id::INT AS customer_id, /* Customer id */ + SUM(ot.total)::NUMERIC AS revenue, /* Revenue from orders made by this customer */ + o.ds::TEXT AS ds /* Date */ +FROM {{ ref('orders') }} AS o + LEFT JOIN order_total AS ot + ON o.id = ot.order_id AND o.ds = ot.ds +{% if is_incremental() %} + WHERE o.ds > (select max(ds) from {{ this }}) +{% endif %} +{% if sqlmesh_incremental is defined %} + WHERE o.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' +{% endif %} +GROUP BY + o.customer_id, + o.ds \ No newline at end of file diff --git a/tests/dbt/converter/test_convert.py b/tests/dbt/converter/test_convert.py new file mode 100644 index 0000000000..001b1f82cc --- /dev/null +++ b/tests/dbt/converter/test_convert.py @@ -0,0 +1,105 @@ +from pathlib import Path +from sqlmesh.core.context import Context +from sqlmesh.dbt.converter.convert import convert_project_files, resolve_fqns_to_model_names +import uuid +import sqlmesh.core.constants as c + + +def test_convert_project_files(sushi_dbt_context: Context, tmp_path: Path) -> None: + src_context = sushi_dbt_context + src_path = sushi_dbt_context.path + output_path = tmp_path / f"output_{uuid.uuid4().hex}" + + convert_project_files(src_path, output_path) + + target_context = Context(paths=output_path) + + assert src_context.models.keys() == target_context.models.keys() + + target_context.plan(auto_apply=True) + + +def test_convert_project_files_includes_library_macros( + sushi_dbt_context: Context, tmp_path: Path +) -> None: + src_path = sushi_dbt_context.path + output_path = tmp_path / f"output_{uuid.uuid4().hex}" + + (src_path / "macros" / "call_library.sql").write_text(""" +{% macro call_library() %} + {{ dbt.current_timestamp() }} +{% endmacro %} +""") + + convert_project_files(src_path, output_path) + + migrated_output_macros_path = output_path / "macros" / c.MIGRATED_DBT_PACKAGES + assert (migrated_output_macros_path / "dbt" / "current_timestamp.sql").exists() + # note: the DBT manifest is smart enough to prune "dbt / default__current_timestamp.sql" from the list so it is not migrated + assert (migrated_output_macros_path / "dbt_duckdb" / "duckdb__current_timestamp.sql").exists() + + +def test_resolve_fqns_to_model_names(empty_dbt_context: Context) -> None: + ctx = empty_dbt_context + + # macro that uses a property of {{ ref() }} and also creates another ref() + (ctx.path / "macros" / "foo.sql").write_text( + """ +{% macro foo(relation) %} + {{ relation.name }} r + left join {{ source('external', 'orders') }} et + on r.id = et.id +{% endmacro %} +""" + ) + + # model 1 - can be fully unwrapped + (ctx.path / "models" / "model1.sql").write_text( + """ +{{ + config( + materialized='incremental', + incremental_strategy='delete+insert', + time_column='ds' + ) +}} + +select * from {{ ref('items') }} +{% if is_incremental() %} + where ds > (select max(ds) from {{ this }}) +{% endif %} +""" + ) + + # model 2 - has ref passed to macro as parameter and also another ref nested in macro + (ctx.path / "models" / "model2.sql").write_text( + """ +select * from {{ foo(ref('model1')) }} union select * from {{ ref('items') }} +""" + ) + + ctx.load() + + assert len(ctx.models) == 3 + + model1 = ctx.models['"memory"."project"."model1"'] + model2 = ctx.models['"memory"."project"."model2"'] + + assert model1.depends_on == {'"memory"."project_raw"."items"'} + assert model2.depends_on == { + '"memory"."project"."model1"', + '"memory"."external"."orders"', + '"memory"."project_raw"."items"', + } + + # All dependencies in model 1 can be tracked by the native loader but its very difficult to cover all the edge cases at conversion time + # so we still populate depends_on() + assert resolve_fqns_to_model_names(ctx, model1.depends_on) == {"project_raw.items"} + + # For model 2, the external model "external.orders" should be removed from depends_on + # If it was output verbatim as depends_on ("memory"."external"."orders"), the native loader would throw an error like: + # - Error: Failed to load model definition, 'Dot' object is not iterable + assert resolve_fqns_to_model_names(ctx, model2.depends_on) == { + "project.model1", + "project_raw.items", + } diff --git a/tests/dbt/converter/test_jinja.py b/tests/dbt/converter/test_jinja.py new file mode 100644 index 0000000000..5d9e8f3d73 --- /dev/null +++ b/tests/dbt/converter/test_jinja.py @@ -0,0 +1,439 @@ +import pytest +from sqlmesh.utils.jinja import JinjaMacroRegistry, MacroExtractor +from sqlmesh.dbt.converter.jinja import JinjaGenerator, convert_jinja_query, convert_jinja_macro +import sqlmesh.dbt.converter.jinja_transforms as jt +from pathlib import Path +from sqlmesh.core.context import Context +import sqlmesh.core.dialect as d +from sqlglot import exp +from _pytest.mark.structures import ParameterSet +from sqlmesh.core.model import SqlModel, load_sql_based_model +from sqlmesh.utils import columns_to_types_all_known + + +def _load_fixture(name: str) -> ParameterSet: + return pytest.param( + (Path(__file__).parent / "fixtures" / name).read_text(encoding="utf8"), id=name + ) + + +@pytest.mark.parametrize( + "original_jinja", + [ + "select 1", + "select bar from {{ ref('foo') }} as f", + "select max(ds) from {{ this }}", + "{% if is_incremental() %}where ds > (select max(ds) from {{ this }}){% endif %}", + "foo {% if sqlmesh_incremental is defined %} bar {% endif %} bar", + "foo between '{{ start_ds }}' and '{{ end_ds }}'", + "{{ 42 }}", + "{{ foo.bar }}", + "{{ 'baz' }}", + "{{ col }} BETWEEN '{{ dates[0] }}' AND '{{ dates[1] }}'", + "{% set foo = bar(baz, bing='bong') %}", + "{% if a == 'ds' %}foo{% elif a == 'ts' %}bar{% elif a < 'ys' or (b != 'ds' and c >= 'ts') %}baz{% else %}bing{% endif %}", + "{% set my_string = my_string ~ stuff ~ ', ' ~ 1 %}", + "{{ context.do_some_action('param') }}", + "{% set big_ole_block %}foo{% endset %}", + "{% if not loop.last %}foo{% endif %}", + "{% for a, b in some_func(a=foo['bar'][0], b=c.d[5]).items() %}foo_{{ a }}_{{ b }}{% endfor %}", + "{{ column | replace(prefix, '') }}", + "{{ column | filter('a', foo='bar') }}", + "{% filter upper %}foo{% endfilter %}", + "{% filter foo(0, bar='baz') %}foo{% endfilter %}", + "{% if foo in ('bar', 'baz') %}bar{% endif %}", + "{% if foo not in ('bar', 'baz') %}bing{% endif %}", + "{% if (field.a if field.a else field.b) | lower not in ('c', 'd') %}foo{% endif %}", + "{% do foo.bar('baz') %}", + "{% set a = (col | lower + '_') + b %}", + "{{ foo[1:10] | lower }}", + "{{ foo[1:] }}", + "{{ foo[:1] }}", + "{% for col in all_columns if col.name in columns_to_compare and col.name in special_names %}{{ col }}{% endfor %}", + "{{ ' or ' if not loop.first else '' }}", + "{% set foo = ['a', 'b', c, d.e, f[0], g.h.i[0][1]] %}", + """{% set foo = "('%Y%m%d', partition_id)" %}""", + "{% set foo = (graph.nodes.values() | selectattr('name', 'equalto', model_name) | list)[0] %}", + "{% set foo.bar = baz.bing(database='foo') %}", + "{{ return(('some', 'tuple')) }}", + "{% call foo('bar', baz=True) %}bar{% endcall %}", + "{% call(user) dump_users(list_of_user) %}bar{% endcall %}", + "{% macro foo(a, b='default', c=None) %}{% endmacro %}", + # "{# some comment #}", #todo: comments get stripped entirely + # "foo\n{%- if bar -%} baz {% endif -%}", #todo: whitespace trim handling is a nice-to-have + _load_fixture("model_query_incremental.sql"), + _load_fixture("macro_dbt_incremental.sql"), + _load_fixture("jinja_nested_if.sql"), + ], +) +def test_generator_roundtrip(original_jinja: str) -> None: + registry = JinjaMacroRegistry() + env = registry.build_environment() + + ast = env.parse(original_jinja) + generated = JinjaGenerator().generate(ast) + + assert generated == original_jinja + + me = MacroExtractor() + # basically just test this doesnt throw an exception. + # The MacroExtractor uses SQLGLot's tokenizer and not Jinja's so these need to work when the converted project is loaded by the native loader + me.extract(generated) + + +def test_generator_sql_comment_macro(): + jinja_str = "-- before sql comment{% macro foo() %}-- inner sql comment{% endmacro %}" + + registry = JinjaMacroRegistry() + env = registry.build_environment() + + ast = env.parse(jinja_str) + generated = JinjaGenerator().generate(ast) + + assert ( + generated == "-- before sql comment\n{% macro foo() %}-- inner sql comment\n{% endmacro %}" + ) + + # check roundtripping an existing newline doesnt keep adding newlines + assert JinjaGenerator().generate(env.parse(generated)) == generated + + +@pytest.mark.parametrize("original_jinja", [_load_fixture("macro_func_with_params.sql")]) +def test_generator_roundtrip_ignore_whitespace(original_jinja: str) -> None: + """ + This makes the following assumptions: + - SQL isnt too sensitive about indentation / whitespace + - The Jinja AST doesnt capture enough information to perfectly replicate the input template with regards to whitespace handling + + So if, disregarding whitespace, the original input string is the same as the AST being run through the generator: the test passes + """ + registry = JinjaMacroRegistry() + env = registry.build_environment() + + ast = env.parse(original_jinja) + + generated = JinjaGenerator().generate(ast) + + assert " ".join(original_jinja.split()) == " ".join(generated.split()) + + +def test_convert_jinja_query(sushi_dbt_context: Context) -> None: + model = sushi_dbt_context.models['"memory"."sushi"."customer_revenue_by_day"'] + assert isinstance(model, SqlModel) + + query = model.query + assert isinstance(query, d.JinjaQuery) + + result = convert_jinja_query(sushi_dbt_context, model, query) + + assert isinstance(result, exp.Query) + + assert ( + result.sql(dialect=model.dialect, pretty=True) + == """WITH order_total AS ( + SELECT + oi.order_id AS order_id, + SUM(oi.quantity * i.price) AS total, + oi.ds AS ds + FROM sushi_raw.order_items AS oi + LEFT JOIN sushi_raw.items AS i + ON oi.item_id = i.id AND oi.ds = i.ds + WHERE + oi.ds BETWEEN @start_ds AND @end_ds + GROUP BY + oi.order_id, + oi.ds +) +SELECT + CAST(o.customer_id AS INT) AS customer_id, /* Customer id */ + CAST(SUM(ot.total) AS DOUBLE) AS revenue, /* Revenue from orders made by this customer */ + CAST(o.ds AS TEXT) AS ds /* Date */ +FROM sushi_raw.orders AS o +LEFT JOIN order_total AS ot + ON o.id = ot.order_id AND o.ds = ot.ds +WHERE + o.ds BETWEEN @start_ds AND @end_ds +GROUP BY + o.customer_id, + o.ds""" + ) + + +def test_convert_jinja_query_exclude_transform(empty_dbt_context: Context) -> None: + ctx = empty_dbt_context + + (ctx.path / "models" / "model1.sql").write_text(""" + {{ + config( + materialized='incremental', + incremental_strategy='delete+insert', + time_column='ds' + ) + }} + + select * from {{ ref('items') }} + {% if is_incremental() %} + where ds > (select max(ds) from {{ this }}) + {% endif %} + """) + + ctx.load() + + model = ctx.models['"memory"."project"."model1"'] + assert isinstance(model, SqlModel) + + query = model.query + assert isinstance(query, d.JinjaQuery) + + converted_query = convert_jinja_query( + ctx, + model, + query, + exclude=[jt.resolve_dbt_ref_to_model_name, jt.rewrite_dbt_ref_to_migrated_ref], + ) + sql = converted_query.sql() + + assert "{{ ref('items') }}" in sql + assert "{{ this }}" not in sql + assert "{% if is_incremental() %}" not in sql + assert "{% endif %}" not in sql + + +def test_convert_jinja_query_self_referencing(empty_dbt_context: Context) -> None: + ctx = empty_dbt_context + + (ctx.path / "models" / "model1.sql").write_text(""" + {{ + config( + materialized='incremental', + incremental_strategy='delete+insert', + time_column='ds' + ) + }} + + select * from {{ ref('items') }} + {% if is_incremental() %} + where ds > (select max(ds) from {{ this }}) + {% endif %} + """) + + ctx.load() + + model = ctx.models['"memory"."project"."model1"'] + assert model.columns_to_types_or_raise + assert ( + not model.depends_on_self + ) # the DBT loader doesnt detect self-references within is_incremental blocks + assert isinstance(model, SqlModel) + + query = model.query + assert isinstance(query, d.JinjaQuery) + + converted_query = convert_jinja_query(ctx, model, query) + converted_model_definition = model.copy().render_definition()[0].sql() + + # load from scratch to use the native loader and clear @cached_property's + ctx.upsert_model( + load_sql_based_model( + expressions=[d.parse_one(converted_model_definition), converted_query], + default_catalog=ctx.default_catalog, + ) + ) + converted_model = ctx.models['"memory"."project"."model1"'] + assert isinstance(converted_model, SqlModel) + + assert not "{% is_incremental" in converted_model.query.sql() + assert ( + converted_model.depends_on_self + ) # Once the is_incremental blocks are removed, the model can be detected as self referencing + assert columns_to_types_all_known( + converted_model.columns_to_types_or_raise + ) # columns to types must all be known for self-referencing models + + +def test_convert_jinja_query_self_referencing_columns_to_types_not_all_known( + empty_dbt_context: Context, +) -> None: + ctx = empty_dbt_context + + (ctx.path / "models" / "model1.sql").write_text(""" + {{ + config( + materialized='incremental', + incremental_strategy='delete+insert', + time_column='ds' + ) + }} + + select id, name, ds from external.table + {% if is_incremental() %} + where ds > (select max(ds) from {{ this }}) + {% endif %} + """) + + ctx.load() + + model = ctx.models['"memory"."project"."model1"'] + assert model.columns_to_types_or_raise + assert ( + not model.depends_on_self + ) # the DBT loader doesnt detect self-references within is_incremental blocks + assert isinstance(model, SqlModel) + + query = model.query + assert isinstance(query, d.JinjaQuery) + + converted_query = convert_jinja_query(ctx, model, query) + converted_model_definition = model.render_definition()[0].sql() + + # load from scratch to use the native loader and clear @cached_property's + ctx.upsert_model( + load_sql_based_model( + expressions=[d.parse_one(converted_model_definition), converted_query], + jinja_macros=model.jinja_macros, + default_catalog=ctx.default_catalog, + ) + ) + converted_model = ctx.models['"memory"."project"."model1"'] + assert isinstance(converted_model, SqlModel) + + # {% is_incremental() %} block should be retained because removing it would make the model self-referencing but the columns_to_types + # arent all known so this would create a load error like: Error: Self-referencing models require inferrable column types. + assert "{% if is_incremental" in converted_model.query.sql() + assert "{{ this }}" not in converted_model.query.sql() + assert not converted_model.depends_on_self + + assert not columns_to_types_all_known( + converted_model.columns_to_types_or_raise + ) # this is ok because the model is not self-referencing + + +def test_convert_jinja_query_migrated_ref(empty_dbt_context: Context) -> None: + ctx = empty_dbt_context + + (ctx.path / "models" / "model1.sql").write_text(""" + {{ + config( + materialized='incremental', + incremental_strategy='delete+insert', + time_column='ds' + ) + }} + + {% macro ref_handler(relation) %} + {{ relation.name }} + {% endmacro %} + + select * from {{ ref_handler(ref("items")) }} + """) + + ctx.load() + + model = ctx.models['"memory"."project"."model1"'] + assert isinstance(model, SqlModel) + query = model.query + assert isinstance(query, d.JinjaQuery) + + converted_query = convert_jinja_query(ctx, model, query) + + assert ( + """select * from {{ ref_handler(__migrated_ref(database='memory', schema='project_raw', identifier='items', sqlmesh_model_name='project_raw.items')) }}""" + in converted_query.sql() + ) + + +def test_convert_jinja_query_post_statement(empty_dbt_context: Context) -> None: + ctx = empty_dbt_context + + (ctx.path / "models" / "model1.sql").write_text(""" + {{ + config( + materialized='incremental', + incremental_strategy='delete+insert', + time_column='ds', + post_hook="create index foo_idx on {{ this }} (id)" + ) + }} + + select * from {{ ref("items") }} + """) + + ctx.load() + + model = ctx.models['"memory"."project"."model1"'] + assert isinstance(model, SqlModel) + + assert model.post_statements + post_statement = model.post_statements[0] + assert isinstance(post_statement, d.JinjaStatement) + + converted_post_statement = convert_jinja_query(ctx, model, post_statement) + + assert "CREATE INDEX foo_idx ON project.model1(id)" in converted_post_statement.sql( + dialect="duckdb" + ) + + +@pytest.mark.parametrize( + "input,expected", + [ + ( + """ + {% macro incremental_by_time(col, time_type) %} + {% if is_incremental() %} + WHERE + {{ col }} > (select max({{ col }}) from {{ this }}) + {% endif %} + {% if sqlmesh_incremental is defined %} + {% set dates = incremental_dates_by_time_type(time_type) %} + WHERE + {{ col }} BETWEEN '{{ dates[0] }}' AND '{{ dates[1] }}' + {% endif %} + {% endmacro %} + """, + """ + {% macro incremental_by_time(col, time_type) %} + {% set dates = incremental_dates_by_time_type(time_type) %} + WHERE + {{ col }} BETWEEN '{{ dates[0] }}' AND '{{ dates[1] }}' + {% endmacro %} + """, + ), + ( + """ + {% macro foo(iterations) %} + with base as ( + select * from {{ ref('customer_revenue_by_day') }} + ), + iter as ( + {% for i in range(0, iterations) %} + 'iter_{{ i }}' as iter_num_{{ i }} + {% if not loop.last %},{% endif %} + {% endfor %} + ) + select 1 + {% endmacro %}""", + """ + {% macro foo(iterations) %} + with base as ( + select * from sushi.customer_revenue_by_day + ), + iter as ( + {% for i in range(0, iterations) %} + 'iter_{{ i }}' as iter_num_{{ i }} + {% if not loop.last %},{% endif %} + {% endfor %} + ) + select 1 + {% endmacro %}""", + ), + ( + """{% macro expand_ref(model_name) %}{{ ref(model_name) }}{% endmacro %}""", + """{% macro expand_ref(model_name) %}{{ ref(model_name) }}{% endmacro %}""", + ), + ], +) +def test_convert_jinja_macro(input: str, expected: str, sushi_dbt_context: Context) -> None: + result = convert_jinja_macro(sushi_dbt_context, input.strip()) + + assert " ".join(result.split()) == " ".join(expected.strip().split()) diff --git a/tests/dbt/converter/test_jinja_transforms.py b/tests/dbt/converter/test_jinja_transforms.py new file mode 100644 index 0000000000..c7d060ea40 --- /dev/null +++ b/tests/dbt/converter/test_jinja_transforms.py @@ -0,0 +1,453 @@ +import pytest +import typing as t +from sqlglot import parse_one +from sqlmesh.core.model import create_sql_model, create_external_model +from sqlmesh.dbt.converter.jinja import transform, JinjaGenerator +import sqlmesh.dbt.converter.jinja_transforms as jt +from sqlmesh.dbt.converter.common import JinjaTransform +from sqlmesh.utils.jinja import environment, Environment, ENVIRONMENT +from sqlmesh.core.context import Context +from sqlmesh.core.config import Config, ModelDefaultsConfig + + +def transform_str( + input: str, handler: JinjaTransform, environment: t.Optional[Environment] = None +) -> str: + environment = environment or ENVIRONMENT + ast = environment.parse(input) + return JinjaGenerator().generate(transform(ast, handler)) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("select * from {{ ref('bar') }} as t", "select * from foo.bar as t"), + ("select * from {{ ref('bar', version=1) }} as t", "select * from foo.bar_v1 as t"), + ("select * from {{ ref('bar', v=1) }} as t", "select * from foo.bar_v1 as t"), + ( + "select * from {{ ref('unknown') }} as t", + "select * from __unresolved_ref__.unknown as t", + ), + ( + "{% macro foo() %}select * from {{ ref('bar') }}{% endmacro %}", + "{% macro foo() %}select * from foo.bar{% endmacro %}", + ), + # these shouldnt be transformed as the macro call might rely on some property of the Relation object returned by ref() + ("{{ dbt_utils.union_relations([ref('foo')]) }},", None), + ("select * from {% if some_macro(ref('bar')) %}foo{% endif %}", None), + ( + "select * from {% if some_macro(ref('bar')) %}{{ ref('bar') }}{% endif %}", + "select * from {% if some_macro(ref('bar')) %}foo.bar{% endif %}", + ), + ("{{ some_macro(ref('bar')) }}", None), + ("{{ some_macro(table=ref('bar')) }}", None), + ], +) +def test_resolve_dbt_ref_to_model_name(input: str, expected: t.Optional[str]) -> None: + expected = expected or input + + from dbt.adapters.base import BaseRelation + + # note: bigquery dialect chosen because its identifiers have backticks + # but internally SQLMesh stores model fqn with double quotes + config = Config(model_defaults=ModelDefaultsConfig(dialect="bigquery")) + ctx = Context(config=config) + ctx.default_catalog = "sqlmesh" + + assert ctx.default_catalog == "sqlmesh" + assert ctx.default_dialect == "bigquery" + + model = create_sql_model( + name="foo.bar", query=parse_one("select 1"), default_catalog=ctx.default_catalog + ) + model2 = create_sql_model( + name="foo.bar_v1", query=parse_one("select 1"), default_catalog=ctx.default_catalog + ) + ctx.upsert_model(model) + ctx.upsert_model(model2) + + assert '"sqlmesh"."foo"."bar"' in ctx.models + + def _resolve_ref(ref_name: str, version: t.Optional[int] = None) -> t.Optional[BaseRelation]: + if ref_name == "bar": + identifier = "bar" + if version: + identifier = f"bar_v{version}" + + relation = BaseRelation.create( + database="sqlmesh", schema="foo", identifier=identifier, quote_character="`" + ) + assert ( + relation.render() == "`sqlmesh`.`foo`.`bar`" + if not version + else f"`sqlmesh`.`foo`.`bar_v{version}`" + ) + return relation + return None + + jinja_env = environment() + jinja_env.globals["ref"] = _resolve_ref + + assert ( + transform_str( + input, + jt.resolve_dbt_ref_to_model_name(ctx.models, jinja_env, dialect=ctx.default_dialect), + ) + == expected + ) + + +@pytest.mark.parametrize( + "input,expected", + [ + ( + "select * from {{ ref('bar') }} as t", + "select * from {{ __migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar') }} as t", + ), + ( + "{% macro foo() %}select * from {{ ref('bar') }}{% endmacro %}", + "{% macro foo() %}select * from {{ __migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar') }}{% endmacro %}", + ), + ( + "{{ dbt_utils.union_relations([ref('bar')]) }}", + "{{ dbt_utils.union_relations([__migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar')]) }}", + ), + ( + "select * from {% if some_macro(ref('bar')) %}foo{% endif %}", + "select * from {% if some_macro(__migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar')) %}foo{% endif %}", + ), + ( + "select * from {% if some_macro(ref('bar')) %}{{ ref('bar') }}{% endif %}", + "select * from {% if some_macro(__migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar')) %}{{ __migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar') }}{% endif %}", + ), + ( + "{{ some_macro(ref('bar')) }}", + "{{ some_macro(__migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar')) }}", + ), + ( + "{{ some_macro(table=ref('bar')) }}", + "{{ some_macro(table=__migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar')) }}", + ), + ], +) +def test_rewrite_dbt_ref_to_migrated_ref(input: str, expected: t.Optional[str]) -> None: + expected = expected or input + + from dbt.adapters.base import BaseRelation + + # note: bigquery dialect chosen because its identifiers have backticks + # but internally SQLMesh stores model fqn with double quotes + config = Config(model_defaults=ModelDefaultsConfig(dialect="bigquery")) + ctx = Context(config=config) + ctx.default_catalog = "sqlmesh" + + assert ctx.default_catalog == "sqlmesh" + assert ctx.default_dialect == "bigquery" + + model = create_sql_model( + name="foo.bar", query=parse_one("select 1"), default_catalog=ctx.default_catalog + ) + ctx.upsert_model(model) + + assert '"sqlmesh"."foo"."bar"' in ctx.models + + def _resolve_ref(ref_name: str) -> t.Optional[BaseRelation]: + if ref_name == "bar": + relation = BaseRelation.create( + database="sqlmesh", schema="foo", identifier="bar", quote_character="`" + ) + assert relation.render() == "`sqlmesh`.`foo`.`bar`" + return relation + return None + + jinja_env = environment() + jinja_env.globals["ref"] = _resolve_ref + + assert ( + transform_str( + input, + jt.rewrite_dbt_ref_to_migrated_ref(ctx.models, jinja_env, dialect=ctx.default_dialect), + ) + == expected + ) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("select * from {{ source('upstream', 'foo') }} as t", "select * from upstream.foo as t"), + ("select * from {{ source('unknown', 'foo') }} as t", "select * from unknown.foo as t"), + ( + "{% macro foo() %}select * from {{ source('upstream', 'foo') }}{% endmacro %}", + "{% macro foo() %}select * from upstream.foo{% endmacro %}", + ), + # these shouldnt be transformed as the macro call might rely on some property of the Relation object returned by source() + ("select * from {% if some_macro(source('upstream', 'foo')) %}foo{% endif %}", None), + ("{{ dbt_utils.union_relations([source('upstream', 'foo')]) }},", None), + ( + "select * from {% if some_macro(source('upstream', 'foo')) %}{{ source('upstream', 'foo') }}{% endif %}", + "select * from {% if some_macro(source('upstream', 'foo')) %}upstream.foo{% endif %}", + ), + ("{{ some_macro(source('upstream', 'foo')) }}", None), + ("{% set results = run_query('select foo from ' ~ source('schema', 'table')) %}", None), + ], +) +def test_resolve_dbt_source_to_model_name(input: str, expected: t.Optional[str]) -> None: + expected = expected or input + + from dbt.adapters.base import BaseRelation + + # note: bigquery dialect chosen because its identifiers have backticks + # but internally SQLMesh stores model fqn with double quotes + config = Config(model_defaults=ModelDefaultsConfig(dialect="bigquery")) + ctx = Context(config=config) + ctx.default_catalog = "sqlmesh" + + assert ctx.default_catalog == "sqlmesh" + assert ctx.default_dialect == "bigquery" + + model = create_external_model(name="upstream.foo", default_catalog=ctx.default_catalog) + ctx.upsert_model(model) + + assert '"sqlmesh"."upstream"."foo"' in ctx.models + + def _resolve_source(schema_name: str, table_name: str) -> t.Optional[BaseRelation]: + if schema_name == "upstream" and table_name == "foo": + relation = BaseRelation.create( + database="sqlmesh", schema="upstream", identifier="foo", quote_character="`" + ) + assert relation.render() == "`sqlmesh`.`upstream`.`foo`" + return relation + return None + + jinja_env = environment() + jinja_env.globals["source"] = _resolve_source + + assert ( + transform_str( + input, + jt.resolve_dbt_source_to_model_name(ctx.models, jinja_env, dialect=ctx.default_dialect), + ) + == expected + ) + + +@pytest.mark.parametrize( + "input,expected", + [ + ( + "select * from {{ source('upstream', 'foo') }} as t", + "select * from {{ __migrated_source(database='sqlmesh', schema='upstream', identifier='foo') }} as t", + ), + ( + "select * from {{ source('unknown', 'foo') }} as t", + "select * from {{ source('unknown', 'foo') }} as t", + ), + ( + "{% macro foo() %}select * from {{ source('upstream', 'foo') }}{% endmacro %}", + "{% macro foo() %}select * from {{ __migrated_source(database='sqlmesh', schema='upstream', identifier='foo') }}{% endmacro %}", + ), + ( + "select * from {% if some_macro(source('upstream', 'foo')) %}foo{% endif %}", + "select * from {% if some_macro(__migrated_source(database='sqlmesh', schema='upstream', identifier='foo')) %}foo{% endif %}", + ), + ( + "{{ dbt_utils.union_relations([source('upstream', 'foo')]) }},", + "{{ dbt_utils.union_relations([__migrated_source(database='sqlmesh', schema='upstream', identifier='foo')]) }},", + ), + ( + "select * from {% if some_macro(source('upstream', 'foo')) %}{{ source('upstream', 'foo') }}{% endif %}", + "select * from {% if some_macro(__migrated_source(database='sqlmesh', schema='upstream', identifier='foo')) %}{{ __migrated_source(database='sqlmesh', schema='upstream', identifier='foo') }}{% endif %}", + ), + ( + "{{ some_macro(source('upstream', 'foo')) }}", + "{{ some_macro(__migrated_source(database='sqlmesh', schema='upstream', identifier='foo')) }}", + ), + ( + "{% set results = run_query('select foo from ' ~ source('upstream', 'foo')) %}", + "{% set results = run_query('select foo from ' ~ __migrated_source(database='sqlmesh', schema='upstream', identifier='foo')) %}", + ), + ], +) +def test_rewrite_dbt_source_to_migrated_source(input: str, expected: t.Optional[str]) -> None: + expected = expected or input + + from dbt.adapters.base import BaseRelation + + # note: bigquery dialect chosen because its identifiers have backticks + # but internally SQLMesh stores model fqn with double quotes + config = Config(model_defaults=ModelDefaultsConfig(dialect="bigquery")) + ctx = Context(config=config) + ctx.default_catalog = "sqlmesh" + + assert ctx.default_catalog == "sqlmesh" + assert ctx.default_dialect == "bigquery" + + model = create_external_model(name="upstream.foo", default_catalog=ctx.default_catalog) + ctx.upsert_model(model) + + assert '"sqlmesh"."upstream"."foo"' in ctx.models + + def _resolve_source(schema_name: str, table_name: str) -> t.Optional[BaseRelation]: + if schema_name == "upstream" and table_name == "foo": + relation = BaseRelation.create( + database="sqlmesh", schema="upstream", identifier="foo", quote_character="`" + ) + assert relation.render() == "`sqlmesh`.`upstream`.`foo`" + return relation + return None + + jinja_env = environment() + jinja_env.globals["source"] = _resolve_source + + assert ( + transform_str( + input, + jt.rewrite_dbt_source_to_migrated_source( + ctx.models, jinja_env, dialect=ctx.default_dialect + ), + ) + == expected + ) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("select * from {{ this }}", "select * from foo.bar"), + ("{% if foo(this) %}bar{% endif %}", None), + ("select * from {{ this.identifier }}", None), + ], +) +def test_resolve_dbt_this_to_model_name(input: str, expected: t.Optional[str]): + expected = expected or input + assert transform_str(input, jt.resolve_dbt_this_to_model_name("foo.bar")) == expected + + +@pytest.mark.parametrize( + "input,expected", + [ + # sqlmesh_incremental present, is_incremental() block removed + ( + """ + select * from foo where + {% if is_incremental() %}ds > (select max(ds)) from {{ this }}){% endif %} + {% if sqlmesh_incremental is defined %}ds BETWEEN {{ start_ds }} and {{ end_ds }}{% endif %} + """, + """ + select * from foo + where + {% if sqlmesh_incremental is defined %}ds BETWEEN {{ start_ds }} and {{ end_ds }}{% endif %} + """, + ), + # sqlmesh_incremental is NOT present; is_incremental() blocks untouched + ( + """ + select * from foo + where + {% if is_incremental() %}ds > (select max(ds)) from {{ this }}){% endif %} + """, + """ + select * from foo + where + {% if is_incremental() %}ds > (select max(ds)) from {{ this }}){% endif %} + """, + ), + ], +) +def test_deduplicate_incremental_checks(input: str, expected: str) -> None: + assert " ".join(transform_str(input, jt.deduplicate_incremental_checks()).split()) == " ".join( + expected.split() + ) + + +@pytest.mark.parametrize( + "input,expected", + [ + # is_incremental() removed + ( + "select * from foo where {% if is_incremental() %}ds >= (select max(ds) from {{ this }} ){% endif %}", + "select * from foo where ds >= (select max(ds) from {{ this }} )", + ), + # sqlmesh_incremental removed + ( + "select * from foo where {% if sqlmesh_incremental is defined %}ds BETWEEN {{ start_ds }} and {{ end_ds }}{% endif %}", + "select * from foo where ds BETWEEN {{ start_ds }} and {{ end_ds }}", + ), + # else untouched + ( + "select * from foo where {% if is_incremental() %}ds >= (select max(ds) from {{ this }} ){% else %}ds is not null{% endif %}", + "select * from foo where {% if is_incremental() %}ds >= (select max(ds) from {{ this }} ){% else %}ds is not null{% endif %}", + ), + ], +) +def test_unpack_incremental_checks(input: str, expected: str) -> None: + assert " ".join(transform_str(input, jt.unpack_incremental_checks()).split()) == " ".join( + expected.split() + ) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("{{ start_ds }}", "@start_ds"), + ( + "select id, ds from foo where ds between {{ start_ts }} and {{ end_ts }}", + "select id, ds from foo where ds between @start_ts and @end_ts", + ), + ("select {{ some_macro(start_ts) }}", None), + ("{{ start_date }}", "@start_date"), + ("'{{ start_date }}'", "'@start_ds'"), # date inside string literal should remain a string + ], +) +def test_rewrite_sqlmesh_predefined_variables_to_sqlmesh_macro_syntax( + input: str, expected: t.Optional[str] +) -> None: + expected = expected or input + assert ( + transform_str(input, jt.rewrite_sqlmesh_predefined_variables_to_sqlmesh_macro_syntax()) + == expected + ) + + +@pytest.mark.parametrize( + "input,expected,package", + [ + ("{{ var('foo') }}", "{{ var('foo') }}", None), + ("{{ var('foo') }}", "{{ var('foo', __dbt_package='test') }}", "test"), + ( + "{{ var('foo', 'default') }}", + "{{ var('foo', 'default', __dbt_package='test') }}", + "test", + ), + ( + "{% if 'col_name' in var('history_columns') %}bar{% endif %}", + "{% if 'col_name' in var('history_columns', __dbt_package='test') %}bar{% endif %}", + "test", + ), + ], +) +def test_append_dbt_package_kwarg_to_var_calls( + input: str, expected: str, package: t.Optional[str] +) -> None: + assert ( + transform_str(input, jt.append_dbt_package_kwarg_to_var_calls(package_name=package)) + == expected + ) + + +@pytest.mark.parametrize( + "input,expected", + [ + ( + "select * from foo where ds between '@start_dt' and '@end_dt'", + "SELECT * FROM foo WHERE ds BETWEEN @start_dt AND @end_dt", + ), + ( + "select * from foo where bar <> '@unrelated'", + "SELECT * FROM foo WHERE bar <> '@unrelated'", + ), + ], +) +def test_unwrap_macros_in_string_literals(input: str, expected: str) -> None: + assert parse_one(input).transform(jt.unwrap_macros_in_string_literals()).sql() == expected diff --git a/tests/utils/test_jinja.py b/tests/utils/test_jinja.py index 5eb00aeb3c..3660adaa95 100644 --- a/tests/utils/test_jinja.py +++ b/tests/utils/test_jinja.py @@ -9,6 +9,8 @@ MacroReturnVal, call_name, nodes, + extract_macro_references_and_variables, + extract_dbt_adapter_dispatch_targets, ) @@ -175,6 +177,54 @@ def test_macro_registry_trim(): assert not trimmed_registry_for_package_b.root_macros +def test_macro_registry_trim_keeps_dbt_adapter_dispatch(): + registry = JinjaMacroRegistry() + extractor = MacroExtractor() + + registry.add_macros( + extractor.extract( + """ + {% macro foo(col) %} + {{ adapter.dispatch('foo', 'test_package') }} + {% endmacro %} + + {% macro default__foo(col) %} + foo_{{ col }} + {% endmacro %} + + {% macro unrelated() %}foo{% endmacro %} + """, + dialect="duckdb", + ), + package="test_package", + ) + + assert sorted(list(registry.packages["test_package"].keys())) == [ + "default__foo", + "foo", + "unrelated", + ] + assert sorted(str(r) for r in registry.packages["test_package"]["foo"].depends_on) == [ + "adapter.dispatch", + "test_package.default__foo", + "test_package.duckdb__foo", + ] + + query_str = """ + select * from {{ test_package.foo('bar') }} + """ + + references, _ = extract_macro_references_and_variables(query_str, dbt_target_name="test") + references_list = list(references) + assert len(references_list) == 1 + assert str(references_list[0]) == "test_package.foo" + + trimmed_registry = registry.trim(references) + + # duckdb__foo is missing from this list because it's not actually defined as a macro + assert sorted(list(trimmed_registry.packages["test_package"].keys())) == ["default__foo", "foo"] + + def test_macro_return(): macros = "{% macro test_return() %}{{ macro_return([1, 2, 3]) }}{% endmacro %}" @@ -302,3 +352,31 @@ def test_dbt_adapter_macro_scope(): rendered = registry.build_environment().from_string("{{ spark__macro_a() }}").render() assert rendered.strip() == "macro_a" + + +def test_extract_dbt_adapter_dispatch_targets(): + assert extract_dbt_adapter_dispatch_targets(""" + {% macro my_macro(arg1, arg2) -%} + {{ return(adapter.dispatch('my_macro')(arg1, arg2)) }} + {% endmacro %} + """) == [("my_macro", None)] + + assert extract_dbt_adapter_dispatch_targets(""" + {% macro my_macro(arg1, arg2) -%} + {{ return(adapter.dispatch('my_macro', 'foo')(arg1, arg2)) }} + {% endmacro %} + """) == [("my_macro", "foo")] + + assert extract_dbt_adapter_dispatch_targets("""{{ adapter.dispatch('my_macro') }}""") == [ + ("my_macro", None) + ] + + assert extract_dbt_adapter_dispatch_targets(""" + {% macro foo() %} + {{ adapter.dispatch('my_macro') }} + {{ some_other_call() }} + {{ return(adapter.dispatch('other_macro', 'other_package')) }} + {% endmacro %} + """) == [("my_macro", None), ("other_macro", "other_package")] + + assert extract_dbt_adapter_dispatch_targets("no jinja") == [] From 65da025e9f9c7f5f5294525cc1bc4395131d460f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 08:30:05 +0200 Subject: [PATCH 0407/1056] Chore(deps): Bump @tanstack/react-query from 5.80.6 to 5.80.7 (#4750) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 40 ++++++++++++++++++++++++--------------- vscode/react/package.json | 2 +- web/client/package.json | 2 +- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e97c65e44..196cbafca8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,8 +100,8 @@ importers: specifier: ^4.1.8 version: 4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) '@tanstack/react-query': - specifier: ^5.80.6 - version: 5.80.6(react@18.3.1) + specifier: ^5.80.7 + version: 5.80.7(react@18.3.1) '@tanstack/react-router': specifier: ^1.120.16 version: 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -221,8 +221,8 @@ importers: specifier: ^0.1.1 version: 0.1.1(tailwindcss@3.4.17) '@tanstack/react-query': - specifier: ^5.80.6 - version: 5.80.6(react@18.3.1) + specifier: ^5.80.7 + version: 5.80.7(react@18.3.1) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -568,6 +568,9 @@ packages: '@codemirror/view@6.37.1': resolution: {integrity: sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==} + '@codemirror/view@6.37.2': + resolution: {integrity: sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==} + '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} @@ -1782,11 +1785,11 @@ packages: resolution: {integrity: sha512-K7JJNrRVvyjAVnbXOH2XLRhFXDkeP54Kt2P4FR1Kl2KDGlIbkua5VqZQD2rot3qaDrpufyUa63nuLai1kOLTsQ==} engines: {node: '>=12'} - '@tanstack/query-core@5.80.6': - resolution: {integrity: sha512-nl7YxT/TAU+VTf+e2zTkObGTyY8YZBMnbgeA1ee66lIVqzKlYursAII6z5t0e6rXgwUMJSV4dshBTNacNpZHbQ==} + '@tanstack/query-core@5.80.7': + resolution: {integrity: sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==} - '@tanstack/react-query@5.80.6': - resolution: {integrity: sha512-izX+5CnkpON3NQGcEm3/d7LfFQNo9ZpFtX2QsINgCYK9LT2VCIdi8D3bMaMSNhrAJCznRoAkFic76uvLroALBw==} + '@tanstack/react-query@5.80.7': + resolution: {integrity: sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==} peerDependencies: react: ^18 || ^19 @@ -6135,13 +6138,13 @@ snapshots: '@codemirror/lint@6.8.5': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 + '@codemirror/view': 6.37.2 crelt: 1.0.6 '@codemirror/search@6.5.10': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 + '@codemirror/view': 6.37.2 crelt: 1.0.6 '@codemirror/state@6.5.2': @@ -6152,7 +6155,7 @@ snapshots: dependencies: '@codemirror/language': 6.11.1 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 + '@codemirror/view': 6.37.2 '@lezer/highlight': 1.2.1 '@codemirror/view@6.37.1': @@ -6162,6 +6165,13 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 + '@codemirror/view@6.37.2': + dependencies: + '@codemirror/state': 6.5.2 + crelt: 1.0.6 + style-mod: 4.1.2 + w3c-keyname: 2.2.8 + '@csstools/color-helpers@5.0.2': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -7459,11 +7469,11 @@ snapshots: '@tanstack/history@1.115.0': {} - '@tanstack/query-core@5.80.6': {} + '@tanstack/query-core@5.80.7': {} - '@tanstack/react-query@5.80.6(react@18.3.1)': + '@tanstack/react-query@5.80.7(react@18.3.1)': dependencies: - '@tanstack/query-core': 5.80.6 + '@tanstack/query-core': 5.80.7 react: 18.3.1 '@tanstack/react-router-devtools@1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.15)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3)': @@ -8640,7 +8650,7 @@ snapshots: '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 + '@codemirror/view': 6.37.2 color-convert@2.0.1: dependencies: diff --git a/vscode/react/package.json b/vscode/react/package.json index 2dabf70ca8..32a429843f 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -18,7 +18,7 @@ "@radix-ui/react-select": "^2.2.5", "@tailwindcss/postcss": "^4.1.8", "@tailwindcss/vite": "^4.1.8", - "@tanstack/react-query": "^5.80.6", + "@tanstack/react-query": "^5.80.7", "@tanstack/react-router": "^1.120.16", "@tanstack/react-router-devtools": "^1.120.16", "@tanstack/react-virtual": "^3.13.9", diff --git a/web/client/package.json b/web/client/package.json index b4c1d175b0..13248ff92c 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -29,7 +29,7 @@ "@radix-ui/react-context-menu": "^2.2.15", "@radix-ui/react-select": "^2.2.5", "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^5.80.6", + "@tanstack/react-query": "^5.80.7", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.9", "@uidotdev/usehooks": "^2.4.1", From 60fa7bc3c536ca1842559f7d90c895e6a6da7861 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:26:42 +0200 Subject: [PATCH 0408/1056] feat(vscode): rename to SQLMesh prefix (#4745) --- pnpm-lock.yaml | 151 +++++++++++++++++- vscode/extension/.vscodeignore | 10 +- vscode/extension/esbuild.js | 8 + vscode/extension/eslint.config.mjs | 26 +++ vscode/extension/package.json | 23 +-- .../extension/src/commands/commands.test.ts | 19 +++ vscode/extension/tsconfig.build.json | 11 ++ vscode/extension/tsconfig.json | 8 +- vscode/extension/tsconfig.test.json | 8 + vscode/extension/vitest.config.ts | 9 ++ 10 files changed, 258 insertions(+), 15 deletions(-) create mode 100644 vscode/extension/src/commands/commands.test.ts create mode 100644 vscode/extension/tsconfig.build.json create mode 100644 vscode/extension/tsconfig.test.json create mode 100644 vscode/extension/vitest.config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 196cbafca8..b0b7342847 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: '@types/vscode': specifier: 1.96.0 version: 1.96.0 + '@vitest/ui': + specifier: ^3.2.3 + version: 3.2.3(vitest@3.2.3) '@vscode/test-cli': specifier: ^0.0.10 version: 0.0.10 @@ -81,6 +84,9 @@ importers: typescript-eslint: specifier: ^8.34.0 version: 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + vitest: + specifier: ^3.2.3 + version: 3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) vscode/react: dependencies: @@ -171,7 +177,7 @@ importers: version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) vitest: specifier: ^3.2.3 - version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -346,7 +352,7 @@ importers: version: 3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) vitest: specifier: ^3.2.3 - version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) optionalDependencies: '@swc/core-linux-x64-gnu': specifier: ^1.11.31 @@ -979,6 +985,9 @@ packages: engines: {node: '>=18'} hasBin: true + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2276,6 +2285,11 @@ packages: '@vitest/spy@3.2.3': resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==} + '@vitest/ui@3.2.3': + resolution: {integrity: sha512-9aR2tY/WT7GRHGEH/9sSIipJqeA21Eh3C6xmiOVmfyBCFmezUSUFLalpaSmRHlRzWCKQU10yz3AHhKuYcdnZGQ==} + peerDependencies: + vitest: 3.2.3 + '@vitest/utils@3.2.3': resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==} @@ -3249,6 +3263,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -4255,6 +4272,10 @@ packages: engines: {node: '>= 14.0.0'} hasBin: true + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -4968,6 +4989,10 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -5268,6 +5293,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -6588,6 +6617,8 @@ snapshots: dependencies: playwright: 1.52.0 + '@polka/url@1.0.0-next.29': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.2': {} @@ -8050,6 +8081,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))': + dependencies: + '@vitest/spy': 3.2.3 + 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.42.0)(tsx@4.19.4)(yaml@2.8.0) + '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 @@ -8078,6 +8117,17 @@ snapshots: dependencies: tinyspy: 4.0.3 + '@vitest/ui@3.2.3(vitest@3.2.3)': + dependencies: + '@vitest/utils': 3.2.3 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.1 + tinyglobby: 0.2.14 + tinyrainbow: 2.0.0 + vitest: 3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + '@vitest/utils@3.2.3': dependencies: '@vitest/pretty-format': 3.2.3 @@ -9205,6 +9255,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -10332,6 +10384,8 @@ snapshots: yargs-parser: 20.2.9 yargs-unparser: 2.0.0 + mrmime@2.0.1: {} + ms@2.1.3: {} mute-stream@0.0.8: {} @@ -11188,6 +11242,12 @@ snapshots: simple-concat: 1.0.1 optional: true + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + slash@3.0.0: {} slash@5.1.0: {} @@ -11547,6 +11607,8 @@ snapshots: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -11799,6 +11861,27 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 + vite-node@3.2.3(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + dependencies: + cac: 6.7.14 + 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.42.0)(tsx@4.19.4)(yaml@2.8.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-node@3.2.3(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 @@ -11824,6 +11907,23 @@ snapshots: dependencies: vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.5(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.4 + rollup: 4.41.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 20.11.25 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.30.1 + terser: 5.42.0 + tsx: 4.19.4 + yaml: 2.8.0 + vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.5 @@ -11841,7 +11941,51 @@ snapshots: tsx: 4.19.4 yaml: 2.8.0 - vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.3 + '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/pretty-format': 3.2.3 + '@vitest/runner': 3.2.3 + '@vitest/snapshot': 3.2.3 + '@vitest/spy': 3.2.3 + '@vitest/utils': 3.2.3 + chai: 5.2.0 + debug: 4.4.1(supports-color@8.1.1) + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.0 + tinyrainbow: 2.0.0 + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite-node: 3.2.3(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 20.11.25 + '@vitest/ui': 3.2.3(vitest@3.2.3) + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 @@ -11869,6 +12013,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.15.30 + '@vitest/ui': 3.2.3(vitest@3.2.3) jsdom: 26.1.0 transitivePeerDependencies: - jiti diff --git a/vscode/extension/.vscodeignore b/vscode/extension/.vscodeignore index 0ae5ea9583..e50f31046d 100644 --- a/vscode/extension/.vscodeignore +++ b/vscode/extension/.vscodeignore @@ -16,4 +16,12 @@ assets/logo.svg esbuild.js openapi.json test-results/** -E2E_TESTING.md \ No newline at end of file +E2E_TESTING.md +**/*.test.ts +**/*.test.js +.mocharc.json +tsconfig.test.json +tsconfig.build.json +src/test/** +tests/** +.claude \ No newline at end of file diff --git a/vscode/extension/esbuild.js b/vscode/extension/esbuild.js index 40a11421e2..f24f83a687 100644 --- a/vscode/extension/esbuild.js +++ b/vscode/extension/esbuild.js @@ -18,6 +18,14 @@ async function main() { plugins: [ /* add to the end of plugins array */ esbuildProblemMatcherPlugin, + { + name: 'exclude-tests', + setup(build) { + build.onResolve({ filter: /\.test\.ts$/ }, args => { + return { external: true } + }) + }, + }, ], }) if (watch) { diff --git a/vscode/extension/eslint.config.mjs b/vscode/extension/eslint.config.mjs index be80cd9ab1..6d939bdc51 100644 --- a/vscode/extension/eslint.config.mjs +++ b/vscode/extension/eslint.config.mjs @@ -26,4 +26,30 @@ export default tseslint.config( '@typescript-eslint/no-unsafe-member-access': 'off', }, }, + { + files: ['**/*.ts'], + ignores: ['**/*.test.ts'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: ['*.test', '*.test.ts', '**/test/**'], + }, + ], + }, + }, + { + files: ['**/*.test.ts'], + languageOptions: { + parserOptions: { + projectService: false, + project: './tsconfig.test.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + }, + }, ) diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 8e36c459c1..254ee8ef16 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -65,33 +65,33 @@ "commands": [ { "command": "sqlmesh.format", - "title": "Format SQLMesh Project", + "title": "SQLMesh: Format Project", "description": "SQLMesh" }, { "command": "sqlmesh.restart", - "title": "Restart SQLMesh Servers", + "title": "SQLMesh: Restart Servers", "description": "SQLMesh" }, { "command": "sqlmesh.signin", - "title": "Sign in to Tobiko Cloud", + "title": "SQLMesh: Sign in to Tobiko Cloud", "description": "SQLMesh" }, { "command": "sqlmesh.signinSpecifyFlow", - "title": "Sign in to Tobiko Cloud (Specify Auth Flow)", + "title": "SQLMesh: Sign in to Tobiko Cloud (Specify Auth Flow)", "description": "SQLMesh" }, { "command": "sqlmesh.signout", - "title": "Sign out from Tobiko Cloud", + "title": "SQLMesh: Sign out from Tobiko Cloud", "description": "SQLMesh" }, { "command": "sqlmesh.renderModel", - "title": "Render Model", - "description": "Render the model in the current editor", + "title": "SQLMesh: Render Model", + "description": "SQLMesh", "icon": "$(open-preview)" } ], @@ -106,15 +106,16 @@ } }, "scripts": { - "ci": "pnpm run lint && pnpm run compile", + "ci": "pnpm run lint && pnpm run compile && pnpm run test:unit", "lint": "eslint src", "lint:fix": "eslint src --fix", + "test:unit": "vitest run", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:headed": "playwright test --headed", "fetch-vscode": "tsx scripts/fetch-vscode.ts", "compile": "pnpm run check-types && node esbuild.js", - "check-types": "tsc --noEmit", + "check-types": "tsc --noEmit -p ./tsconfig.build.json", "watch": "node esbuild.js --watch", "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", "vscode:package": "vsce package --no-dependencies", @@ -135,6 +136,7 @@ "@types/mocha": "^10.0.10", "@types/node": "20.11.25", "@types/vscode": "1.96.0", + "@vitest/ui": "^3.2.3", "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.5.0", @@ -143,6 +145,7 @@ "ts-loader": "^9.5.2", "tsx": "^4.19.4", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.0" + "typescript-eslint": "^8.34.0", + "vitest": "^3.2.3" } } diff --git a/vscode/extension/src/commands/commands.test.ts b/vscode/extension/src/commands/commands.test.ts new file mode 100644 index 0000000000..7f531a2ce1 --- /dev/null +++ b/vscode/extension/src/commands/commands.test.ts @@ -0,0 +1,19 @@ +import { assert, describe, it } from 'vitest' +import * as fs from 'fs' +import * as path from 'path' + +describe('Commands', () => { + it('all commands should start with "SQLMesh: " prefix', () => { + const packageJsonPath = path.join(__dirname, '..', '..', 'package.json') + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) + + const commands = packageJson.contributes?.commands || [] + + commands.forEach((command: any) => { + assert( + command.title.startsWith('SQLMesh: '), + `Command "${command.command}" title "${command.title}" should start with "SQLMesh: "`, + ) + }) + }) +}) diff --git a/vscode/extension/tsconfig.build.json b/vscode/extension/tsconfig.build.json new file mode 100644 index 0000000000..2ea6c120ab --- /dev/null +++ b/vscode/extension/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "node_modules", + "../node_modules", + "../../node_modules", + "src/**/*.test.ts", + "tests/**/*", + "scripts/**/*" + ] +} diff --git a/vscode/extension/tsconfig.json b/vscode/extension/tsconfig.json index f6b6d29681..a20a2f08d3 100644 --- a/vscode/extension/tsconfig.json +++ b/vscode/extension/tsconfig.json @@ -16,5 +16,11 @@ } }, "include": ["tests/**/*", "src/**/*", "../bus/src/**/*"], - "exclude": ["node_modules", "../node_modules", "../../node_modules"] + "exclude": [ + "node_modules", + "../node_modules", + "../../node_modules", + "tests/**/*.test.ts", + "src/**/*.test.ts" + ] } diff --git a/vscode/extension/tsconfig.test.json b/vscode/extension/tsconfig.test.json new file mode 100644 index 0000000000..752c0f0952 --- /dev/null +++ b/vscode/extension/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src/**/*.test.ts", "tests/**/*"], + "exclude": ["node_modules", "../node_modules", "../../node_modules"] +} diff --git a/vscode/extension/vitest.config.ts b/vscode/extension/vitest.config.ts new file mode 100644 index 0000000000..49fbea3be3 --- /dev/null +++ b/vscode/extension/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + include: ['src/**/*.test.ts'], + exclude: ['**/node_modules/**', '**/dist/**', '**/out/**'], + }, +}) From 57b59ec6c7b3a3195119504a4f27d85fd9fe0f60 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Tue, 17 Jun 2025 12:47:14 +0300 Subject: [PATCH 0409/1056] Chore!: Refactor `sqlmesh test` output to use rich (#4715) --- sqlmesh/core/console.py | 125 ++++++++++---- sqlmesh/core/context.py | 28 ++- sqlmesh/core/test/definition.py | 96 ++++++++++- sqlmesh/core/test/result.py | 89 +++++----- sqlmesh/core/test/runner.py | 25 +-- sqlmesh/integrations/github/cicd/command.py | 5 +- .../integrations/github/cicd/controller.py | 19 ++- sqlmesh/magics.py | 3 + sqlmesh/utils/rich.py | 56 ++++++ tests/core/test_plan.py | 3 +- tests/core/test_table_diff.py | 8 +- tests/core/test_test.py | 159 ++++++++++++++---- .../github/cicd/test_github_commands.py | 17 +- tests/integrations/jupyter/test_magics.py | 14 -- web/server/api/endpoints/commands.py | 9 +- web/server/console.py | 8 +- 16 files changed, 459 insertions(+), 205 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index e272442e67..b73f2d576f 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -7,6 +7,7 @@ import uuid import logging import textwrap +from itertools import zip_longest from pathlib import Path from hyperscript import h from rich.console import Console as RichConsole @@ -26,6 +27,7 @@ from rich.tree import Tree from sqlglot import exp +from sqlmesh.core.test.result import ModelTextTestResult from sqlmesh.core.environment import EnvironmentNamingInfo, EnvironmentSummary from sqlmesh.core.linter.rule import RuleViolation from sqlmesh.core.model import Model @@ -46,6 +48,7 @@ NodeAuditsErrors, format_destructive_change_msg, ) +from sqlmesh.utils.rich import strip_ansi_codes if t.TYPE_CHECKING: import ipywidgets as widgets @@ -316,6 +319,17 @@ def log_destructive_change( """Display a destructive change error or warning to the user.""" +class UnitTestConsole(abc.ABC): + @abc.abstractmethod + def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None: + """Display the test result and output. + + Args: + result: The unittest test result that contains metrics like num success, fails, ect. + target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect. + """ + + class Console( PlanBuilderConsole, LinterConsole, @@ -327,6 +341,7 @@ class Console( DifferenceConsole, TableDiffConsole, BaseConsole, + UnitTestConsole, abc.ABC, ): """Abstract base class for defining classes used for displaying information to the user and also interact @@ -460,18 +475,6 @@ def plan( fail. Default: False """ - @abc.abstractmethod - def log_test_results( - self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str - ) -> None: - """Display the test result and output. - - Args: - result: The unittest test result that contains metrics like num success, fails, ect. - output: The generated output from the unittest. - target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect. - """ - @abc.abstractmethod def show_sql(self, sql: str) -> None: """Display to the user SQL.""" @@ -668,9 +671,7 @@ def plan( if auto_apply: plan_builder.apply() - def log_test_results( - self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str - ) -> None: + def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None: pass def show_sql(self, sql: str) -> None: @@ -1952,10 +1953,12 @@ def _prompt_promote(self, plan_builder: PlanBuilder) -> None: ): plan_builder.apply() - def log_test_results( - self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str - ) -> None: + def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None: divider_length = 70 + + self._log_test_details(result) + self._print("\n") + if result.wasSuccessful(): self._print("=" * divider_length) self._print( @@ -1972,9 +1975,13 @@ def log_test_results( ) for test, _ in result.failures + result.errors: if isinstance(test, ModelTest): - self._print(f"Failure Test: {test.model.name} {test.test_name}") + self._print(f"Failure Test: {test.path}::{test.test_name}") self._print("=" * divider_length) - self._print(output) + + def _captured_unit_test_results(self, result: ModelTextTestResult) -> str: + with self.console.capture() as capture: + self._log_test_details(result) + return strip_ansi_codes(capture.get()) def show_sql(self, sql: str) -> None: self._print(Syntax(sql, "sql", word_wrap=True), crop=False) @@ -2492,6 +2499,63 @@ def show_linter_violations( else: self.log_warning(msg) + def _log_test_details(self, result: ModelTextTestResult) -> None: + """ + This is a helper method that encapsulates the logic for logging the relevant unittest for the result. + The top level method (`log_test_results`) reuses `_log_test_details` differently based on the console. + + Args: + result: The unittest test result that contains metrics like num success, fails, ect. + """ + tests_run = result.testsRun + errors = result.errors + failures = result.failures + skipped = result.skipped + is_success = not (errors or failures) + + infos = [] + if failures: + infos.append(f"failures={len(failures)}") + if errors: + infos.append(f"errors={len(errors)}") + if skipped: + infos.append(f"skipped={skipped}") + + self._print("\n", end="") + + for (test_case, failure), test_failure_tables in zip_longest( # type: ignore + failures, result.failure_tables + ): + self._print(unittest.TextTestResult.separator1) + self._print(f"FAIL: {test_case}") + + if test_description := test_case.shortDescription(): + self._print(test_description) + self._print(f"{unittest.TextTestResult.separator2}") + + if not test_failure_tables: + self._print(failure) + else: + for failure_table in test_failure_tables: + self._print(failure_table) + self._print("\n", end="") + + for test_case, error in errors: + self._print(unittest.TextTestResult.separator1) + self._print(f"ERROR: {test_case}") + self._print(f"{unittest.TextTestResult.separator2}") + self._print(error) + + # Output final report + self._print(unittest.TextTestResult.separator2) + test_duration_msg = f" in {result.duration:.3f}s" if result.duration else "" + self._print( + f"\nRan {tests_run} {'tests' if tests_run > 1 else 'test'}{test_duration_msg} \n" + ) + self._print( + f"{'OK' if is_success else 'FAILED'}{' (' + ', '.join(infos) + ')' if infos else ''}" + ) + def _cells_match(x: t.Any, y: t.Any) -> bool: """Helper function to compare two cells and returns true if they're equal, handling array objects.""" @@ -2763,9 +2827,7 @@ def radio_button_selected(change: t.Dict[str, t.Any]) -> None: ) self.display(radio) - def log_test_results( - self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str - ) -> None: + def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None: import ipywidgets as widgets divider_length = 70 @@ -2781,12 +2843,14 @@ def log_test_results( h( "span", {"style": {**shared_style, **success_color}}, - f"Successfully Ran {str(result.testsRun)} Tests Against {target_dialect}", + f"Successfully Ran {str(result.testsRun)} tests against {target_dialect}", ) ) footer = str(h("span", {"style": shared_style}, "=" * divider_length)) self.display(widgets.HTML("
".join([header, message, footer]))) else: + output = self._captured_unit_test_results(result) + fail_color = {"color": "#db3737"} fail_shared_style = {**shared_style, **fail_color} header = str(h("span", {"style": fail_shared_style}, "-" * divider_length)) @@ -3137,21 +3201,22 @@ def stop_promotion_progress(self, success: bool = True) -> None: def log_success(self, message: str) -> None: self._print(message) - def log_test_results( - self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str - ) -> None: + def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None: if result.wasSuccessful(): self._print( f"**Successfully Ran `{str(result.testsRun)}` Tests Against `{target_dialect}`**\n\n" ) else: + self._print("```") + self._log_test_details(result) + self._print("```\n\n") + self._print( f"**Num Successful Tests: {result.testsRun - len(result.failures) - len(result.errors)}**\n\n" ) for test, _ in result.failures + result.errors: if isinstance(test, ModelTest): self._print(f"* Failure Test: `{test.model.name}` - `{test.test_name}`\n\n") - self._print(f"```{output}```\n\n") def log_skipped_models(self, snapshot_names: t.Set[str]) -> None: if snapshot_names: @@ -3530,9 +3595,7 @@ def show_model_difference_summary( for modified in context_diff.modified_snapshots: self._write(f" Modified: {modified}") - def log_test_results( - self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str - ) -> None: + def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None: self._write("Test Results:", result) def show_sql(self, sql: str) -> None: diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index dc2eba42d9..d1e58366eb 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -40,7 +40,6 @@ import time import traceback import typing as t -import unittest.result from functools import cached_property from io import StringIO from itertools import chain @@ -2061,7 +2060,7 @@ def test( test_meta = self.load_model_tests(tests=tests, patterns=match_patterns) - return run_tests( + result = run_tests( model_test_metadata=test_meta, models=self._models, config=self.config, @@ -2074,6 +2073,13 @@ def test( default_catalog_dialect=self.config.dialect or "", ) + self.console.log_test_results( + result, + self.test_connection_config._engine_adapter.DIALECT, + ) + + return result + @python_api_analytics def audit( self, @@ -2496,28 +2502,20 @@ def import_state(self, input_file: Path, clear: bool = False, confirm: bool = Tr def _run_tests( self, verbosity: Verbosity = Verbosity.DEFAULT - ) -> t.Tuple[unittest.result.TestResult, str]: + ) -> t.Tuple[ModelTextTestResult, str]: test_output_io = StringIO() 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.Tuple[t.Optional[unittest.result.TestResult], t.Optional[str]]: + def _run_plan_tests(self, skip_tests: bool = False) -> t.Optional[ModelTextTestResult]: if not skip_tests: - result, test_output = self._run_tests() - if result.testsRun > 0: - self.console.log_test_results( - result, - test_output, - self.test_connection_config._engine_adapter.DIALECT, - ) + 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, test_output - return None, None + return result + return None @property def _model_tables(self) -> t.Dict[str, str]: diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index e43b8b215c..8ceb8a4447 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sys + import datetime import threading import typing as t @@ -10,6 +12,7 @@ from pathlib import Path from unittest.mock import patch + from io import StringIO from sqlglot import Dialect, exp from sqlglot.optimizer.annotate_types import annotate_types @@ -24,6 +27,8 @@ from sqlmesh.utils.date import date_dict, pandas_timestamp_to_pydatetime, to_datetime from sqlmesh.utils.errors import ConfigError, TestError from sqlmesh.utils.yaml import load as yaml_load +from sqlmesh.utils import Verbosity +from sqlmesh.utils.rich import df_to_table if t.TYPE_CHECKING: import pandas as pd @@ -60,6 +65,7 @@ def __init__( preserve_fixtures: bool = False, default_catalog: str | None = None, concurrency: bool = False, + verbosity: Verbosity = Verbosity.DEFAULT, ) -> None: """ModelTest encapsulates a unit test for a model. @@ -83,6 +89,7 @@ def __init__( self.default_catalog = default_catalog self.dialect = dialect self.concurrency = concurrency + self.verbosity = verbosity self._fixture_table_cache: t.Dict[str, exp.Table] = {} self._normalized_column_name_cache: t.Dict[str, str] = {} @@ -134,6 +141,11 @@ def __init__( super().__init__() + def defaultTestResult(self) -> unittest.TestResult: + from sqlmesh.core.test.result import ModelTextTestResult + + return ModelTextTestResult(stream=sys.stdout, descriptions=True, verbosity=self.verbosity) + def shortDescription(self) -> t.Optional[str]: return self.body.get("description") @@ -290,23 +302,53 @@ def _to_hashable(x: t.Any) -> t.Any: check_like=True, # Ignore column order ) except AssertionError as e: + # There are 2 concepts at play here: + # 1. The Exception args will contain the error message plus the diff dataframe table stringified + # (backwards compatibility with existing tests, possible to serialize/send over network etc) + # 2. Each test will also transform these diff dataframes into Rich tables, which will be the ones that'll + # be surfaced to the user through Console for better UX (versus stringified dataframes) + # + # This is a bit of a hack, but it's a way to get the best of both worlds. + args: t.List[t.Any] = [] if expected.shape != actual.shape: _raise_if_unexpected_columns(expected.columns, actual.columns) - error_msg = "Data mismatch (rows are different)" + args.append("Data mismatch (rows are different)") missing_rows = _row_difference(expected, actual) if not missing_rows.empty: - error_msg += f"\n\nMissing rows:\n\n{missing_rows}" + args[0] += f"\n\nMissing rows:\n\n{missing_rows}" + args.append(df_to_table("Missing rows", missing_rows)) unexpected_rows = _row_difference(actual, expected) + if not unexpected_rows.empty: - error_msg += f"\n\nUnexpected rows:\n\n{unexpected_rows}" + args[0] += f"\n\nUnexpected rows:\n\n{unexpected_rows}" + args.append(df_to_table("Unexpected rows", unexpected_rows)) - e.args = (error_msg,) else: diff = expected.compare(actual).rename(columns={"self": "exp", "other": "act"}) - e.args = (f"Data mismatch (exp: expected, act: actual)\n\n{diff}",) + + args.append(f"Data mismatch (exp: expected, act: actual)\n\n{diff}") + + diff.rename(columns={"exp": "Expected", "act": "Actual"}, inplace=True) + if self.verbosity == Verbosity.DEFAULT: + args.extend( + df_to_table("Data mismatch", df) for df in _split_df_by_column_pairs(diff) + ) + else: + from pandas import MultiIndex + + levels = t.cast(MultiIndex, diff.columns).levels[0] + for col in levels: + col_diff = diff[col] + if not col_diff.empty: + table = df_to_table( + f"[bold red]Column '{col}' mismatch[/bold red]", col_diff + ) + args.append(table) + + e.args = (*args,) raise e @@ -328,6 +370,7 @@ def create_test( preserve_fixtures: bool = False, default_catalog: str | None = None, concurrency: bool = False, + verbosity: Verbosity = Verbosity.DEFAULT, ) -> t.Optional[ModelTest]: """Create a SqlModelTest or a PythonModelTest. @@ -373,6 +416,7 @@ def create_test( preserve_fixtures, default_catalog, concurrency, + verbosity, ) except Exception as e: raise TestError(f"Failed to create test {test_name} ({path})\n{str(e)}") @@ -692,6 +736,7 @@ def __init__( preserve_fixtures: bool = False, default_catalog: str | None = None, concurrency: bool = False, + verbosity: Verbosity = Verbosity.DEFAULT, ) -> None: """PythonModelTest encapsulates a unit test for a Python model. @@ -718,6 +763,7 @@ def __init__( preserve_fixtures, default_catalog, concurrency, + verbosity, ) self.context = TestExecutionContext( @@ -951,3 +997,43 @@ def _normalize_df_value(value: t.Any) -> t.Any: return {k: _normalize_df_value(v) for k, v in zip(value["key"], value["value"])} return {k: _normalize_df_value(v) for k, v in value.items()} return value + + +def _split_df_by_column_pairs(df: pd.DataFrame, pairs_per_chunk: int = 4) -> t.List[pd.DataFrame]: + """Split a dataframe into chunks of column pairs. + + Args: + df: The dataframe to split + pairs_per_chunk: Number of column pairs per chunk (default: 4) + + Returns: + List of dataframes, each containing an even number of columns + """ + total_columns = len(df.columns) + + # If we have fewer columns than pairs_per_chunk * 2, return the original df + if total_columns <= pairs_per_chunk * 2: + return [df] + + # Calculate number of chunks needed to split columns evenly + num_chunks = (total_columns + (pairs_per_chunk * 2 - 1)) // (pairs_per_chunk * 2) + + # Calculate columns per chunk to ensure equal distribution + # We round down to nearest even number to ensure each chunk has even columns + columns_per_chunk = (total_columns // num_chunks) & ~1 # Round down to nearest even number + remainder = total_columns - (columns_per_chunk * num_chunks) + + chunks = [] + start_idx = 0 + + # Distribute columns evenly across chunks + for i in range(num_chunks): + # Add 2 columns to early chunks if there's a remainder + # This ensures we always add pairs of columns + extra = 2 if i < remainder // 2 else 0 + end_idx = start_idx + columns_per_chunk + extra + chunk = df.iloc[:, start_idx:end_idx] + chunks.append(chunk) + start_idx = end_idx + + return chunks diff --git a/sqlmesh/core/test/result.py b/sqlmesh/core/test/result.py index cdba66b612..8621b8b10a 100644 --- a/sqlmesh/core/test/result.py +++ b/sqlmesh/core/test/result.py @@ -15,10 +15,13 @@ class ModelTextTestResult(unittest.TextTestResult): successes: t.List[unittest.TestCase] def __init__(self, *args: t.Any, **kwargs: t.Any): + self.console = kwargs.pop("console", None) super().__init__(*args, **kwargs) self.successes = [] self.original_failures: t.List[t.Tuple[unittest.TestCase, ErrorType]] = [] + 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 def addSubTest( self, @@ -41,6 +44,12 @@ def addSubTest( super().addSubTest(test, subtest, err) + def _print_char(self, char: str) -> None: + from sqlmesh.core.console import TerminalConsole + + if isinstance(self.console, TerminalConsole): + self.console._print(char, end="") + def addFailure(self, test: unittest.TestCase, err: ErrorType) -> None: """Called when the test case test signals a failure. @@ -51,7 +60,18 @@ def addFailure(self, test: unittest.TestCase, err: ErrorType) -> None: err: A tuple of the form returned by sys.exc_info(), i.e., (type, value, traceback). """ exctype, value, _ = err + + if value and value.args: + exception_msg, rich_tables = value.args[:1], value.args[1:] + value.args = exception_msg + + if rich_tables: + self.failure_tables.append(rich_tables) + + self._print_char("F") + self.original_failures.append((test, err)) + # Intentionally ignore the traceback to hide it from the user return super().addFailure(test, (exctype, value, None)) # type: ignore @@ -64,6 +84,9 @@ def addError(self, test: unittest.TestCase, err: ErrorType) -> None: """ exctype, value, _ = err self.original_errors.append((test, err)) + + self._print_char("E") + # Intentionally ignore the traceback to hide it from the user return super().addError(test, (exctype, value, None)) # type: ignore @@ -74,52 +97,24 @@ def addSuccess(self, test: unittest.TestCase) -> None: test: The test case """ super().addSuccess(test) - self.successes.append(test) - def log_test_report(self, test_duration: float) -> None: - """ - Log the test report following unittest's conventions. + self._print_char(".") - Args: - test_duration: The duration of the tests. - """ - tests_run = self.testsRun - errors = self.errors - failures = self.failures - skipped = self.skipped - - is_success = not (errors or failures) - - infos = [] - if failures: - infos.append(f"failures={len(failures)}") - if errors: - infos.append(f"errors={len(errors)}") - if skipped: - infos.append(f"skipped={skipped}") - - stream = self.stream - - stream.write("\n") - - for test_case, failure in failures: - stream.writeln(unittest.TextTestResult.separator1) - stream.writeln(f"FAIL: {test_case}") - if test_description := test_case.shortDescription(): - stream.writeln(test_description) - stream.writeln(unittest.TextTestResult.separator2) - stream.writeln(failure) - - for test_case, error in errors: - stream.writeln(unittest.TextTestResult.separator1) - stream.writeln(f"ERROR: {test_case}") - stream.writeln(error) - - # Output final report - stream.writeln(unittest.TextTestResult.separator2) - stream.writeln( - f"Ran {tests_run} {'tests' if tests_run > 1 else 'test'} in {test_duration:.3f}s \n" - ) - stream.writeln( - f"{'OK' if is_success else 'FAILED'}{' (' + ', '.join(infos) + ')' if infos else ''}" - ) + self.successes.append(test) + + def merge(self, other: ModelTextTestResult) -> None: + if other.successes: + self.addSuccess(other.successes[0]) + elif other.errors: + for error_test, error in other.original_errors: + self.addError(error_test, error) + elif other.failures: + for failure_test, failure in other.original_failures: + self.addFailure(failure_test, failure) + + self.failure_tables.extend(other.failure_tables) + elif other.skipped: + skipped_args = other.skipped[0] + self.addSkip(skipped_args[0], skipped_args[1]) + + self.testsRun += 1 diff --git a/sqlmesh/core/test/runner.py b/sqlmesh/core/test/runner.py index d2a54d68e8..c098a46d84 100644 --- a/sqlmesh/core/test/runner.py +++ b/sqlmesh/core/test/runner.py @@ -1,10 +1,10 @@ from __future__ import annotations -import sys import time import threading import typing as t import unittest +from io import StringIO import concurrent from concurrent.futures import ThreadPoolExecutor @@ -16,7 +16,6 @@ ModelTestMetadata as ModelTestMetadata, ) from sqlmesh.core.config.connection import BaseDuckDBConnectionConfig - from sqlmesh.core.test.result import ModelTextTestResult as ModelTextTestResult from sqlmesh.utils import UniqueKeyDict, Verbosity @@ -106,10 +105,13 @@ def run_tests( lock = threading.Lock() + from sqlmesh.core.console import get_console + combined_results = ModelTextTestResult( - stream=unittest.runner._WritelnDecorator(stream or sys.stderr), # type: ignore + stream=unittest.runner._WritelnDecorator(stream or StringIO()), # type: ignore verbosity=2 if verbosity >= Verbosity.VERBOSE else 1, descriptions=True, + console=get_console(), ) metadata_to_adapter = create_testing_engine_adapters( @@ -136,6 +138,7 @@ def _run_single_test( default_catalog=default_catalog, preserve_fixtures=preserve_fixtures, concurrency=num_workers > 1, + verbosity=verbosity, ) if not test: @@ -147,19 +150,7 @@ def _run_single_test( ) with lock: - if result.successes: - combined_results.addSuccess(result.successes[0]) - elif result.errors: - for error_test, error in result.original_errors: - combined_results.addError(error_test, error) - elif result.failures: - for failure_test, failure in result.original_failures: - combined_results.addFailure(failure_test, failure) - elif result.skipped: - skipped_args = result.skipped[0] - combined_results.addSkip(skipped_args[0], skipped_args[1]) - - combined_results.testsRun += 1 + combined_results.merge(result) return result @@ -183,6 +174,6 @@ def _run_single_test( end_time = time.perf_counter() - combined_results.log_test_report(test_duration=end_time - start_time) + combined_results.duration = end_time - start_time return combined_results diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index b360f3366e..cedee1fa58 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -63,20 +63,19 @@ def check_required_approvers(ctx: click.Context) -> None: def _run_tests(controller: GithubController) -> bool: controller.update_test_check(status=GithubCheckStatus.IN_PROGRESS) try: - result, output = controller.run_tests() + result, _ = controller.run_tests() controller.update_test_check( status=GithubCheckStatus.COMPLETED, # Conclusion will be updated with final status based on test results conclusion=GithubCheckConclusion.NEUTRAL, result=result, - output=output, ) return result.wasSuccessful() except Exception: controller.update_test_check( status=GithubCheckStatus.COMPLETED, conclusion=GithubCheckConclusion.FAILURE, - output=traceback.format_exc(), + traceback=traceback.format_exc(), ) return False diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index ad1b4b52b2..5a0ad36d71 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -8,7 +8,6 @@ import re import traceback import typing as t -import unittest from enum import Enum from typing import List from pathlib import Path @@ -20,6 +19,7 @@ from sqlmesh.core import constants as c from sqlmesh.core.console import SNAPSHOT_CHANGE_CATEGORY_STR, get_console, MarkdownConsole from sqlmesh.core.context import Context +from sqlmesh.core.test.result import ModelTextTestResult from sqlmesh.core.environment import Environment from sqlmesh.core.plan import Plan, PlanBuilder from sqlmesh.core.snapshot.definition import ( @@ -494,7 +494,7 @@ def get_plan_summary(self, plan: Plan) -> str: except PlanError as e: return f"Plan failed to generate. Check for pending or unresolved changes. Error: {e}" - def run_tests(self) -> t.Tuple[unittest.result.TestResult, str]: + def run_tests(self) -> t.Tuple[ModelTextTestResult, str]: """ Run tests for the PR """ @@ -734,8 +734,8 @@ def update_test_check( self, status: GithubCheckStatus, conclusion: t.Optional[GithubCheckConclusion] = None, - result: t.Optional[unittest.result.TestResult] = None, - output: t.Optional[str] = None, + result: t.Optional[ModelTextTestResult] = None, + traceback: t.Optional[str] = None, ) -> None: """ Updates the status of tests for code in the PR @@ -743,15 +743,13 @@ def update_test_check( def conclusion_handler( conclusion: GithubCheckConclusion, - result: t.Optional[unittest.result.TestResult], - output: t.Optional[str], + result: t.Optional[ModelTextTestResult], ) -> t.Tuple[GithubCheckConclusion, str, t.Optional[str]]: if result: # Clear out console self._console.consume_captured_output() self._console.log_test_results( result, - output, self._context.test_connection_config._engine_adapter.DIALECT, ) test_summary = self._console.consume_captured_output() @@ -762,8 +760,11 @@ def conclusion_handler( else GithubCheckConclusion.FAILURE ) return test_conclusion, test_title, test_summary + if traceback: + self._console._print(traceback) + test_title = "Skipped Tests" if conclusion.is_skipped else "Tests Failed" - return conclusion, test_title, output + return conclusion, test_title, traceback self._update_check_handler( check_name="SQLMesh - Run Unit Tests", @@ -776,7 +777,7 @@ def conclusion_handler( }[status], None, ), - conclusion_handler=functools.partial(conclusion_handler, result=result, output=output), + conclusion_handler=functools.partial(conclusion_handler, result=result), ) def update_required_approval_check( diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index e74019a743..95260170fe 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -1,5 +1,7 @@ from __future__ import annotations +from io import StringIO + import functools import logging import typing as t @@ -1032,6 +1034,7 @@ def run_test(self, context: Context, line: str) -> None: tests=args.tests, verbosity=Verbosity(args.verbose), preserve_fixtures=args.preserve_fixtures, + stream=StringIO(), # consume the output instead of redirecting to stdout ) @magic_arguments() diff --git a/sqlmesh/utils/rich.py b/sqlmesh/utils/rich.py index 6ebeab3114..589dd0b50f 100644 --- a/sqlmesh/utils/rich.py +++ b/sqlmesh/utils/rich.py @@ -1,8 +1,17 @@ +from __future__ import annotations + import typing as t +import re + from rich.console import Console from rich.progress import Column, ProgressColumn, Task, Text from rich.theme import Theme +from rich.table import Table +from rich.align import Align + +if t.TYPE_CHECKING: + import pandas as pd theme = Theme( { @@ -46,3 +55,50 @@ def render(self, task: Task) -> Text: f"{completed:{total_width}d}{self.separator}{total}", style="progress.download", ) + + +def strip_ansi_codes(text: str) -> str: + """Strip ANSI color codes and styling from text.""" + ansi_escape = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]") + return ansi_escape.sub("", text).strip() + + +def df_to_table( + header: str, + df: pd.DataFrame, + show_index: bool = True, + index_name: str = "Row", +) -> Table: + """Convert a pandas.DataFrame obj into a rich.Table obj. + Args: + df (DataFrame): A Pandas DataFrame to be converted to a rich Table. + rich_table (Table): A rich Table that should be populated by the DataFrame values. + show_index (bool): Add a column with a row count to the table. Defaults to True. + index_name (str, optional): The column name to give to the index column. Defaults to None, showing no value. + Returns: + Table: The rich Table instance passed, populated with the DataFrame values.""" + + rich_table = Table(title=f"[bold red]{header}[/bold red]", show_lines=True, min_width=60) + if show_index: + index_name = str(index_name) if index_name else "" + rich_table.add_column(Align.center(index_name)) + + for column in df.columns: + column_name = column if isinstance(column, str) else ": ".join(str(col) for col in column) + + # Color coding unit test columns (expected/actual), can be removed or refactored if df_to_table is used elswhere too + lower = column_name.lower() + if "expected" in lower: + column_name = f"[green]{column_name}[/green]" + elif "actual" in lower: + column_name = f"[red]{column_name}[/red]" + + rich_table.add_column(Align.center(column_name)) + + for index, value_list in enumerate(df.values.tolist()): + row = [str(index)] if show_index else [] + row += [str(x) for x in value_list] + center = [Align.center(x) for x in row] + rich_table.add_row(*center) + + return rich_table diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 61ac2b4ba6..4b02ae6c4e 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -5,7 +5,7 @@ import pytest from sqlmesh.utils.metaprogramming import Executable -from tests.core.test_table_diff import create_test_console, strip_ansi_codes +from tests.core.test_table_diff import create_test_console import time_machine from pytest_mock.plugin import MockerFixture from sqlglot import parse_one @@ -42,6 +42,7 @@ yesterday_ds, ) from sqlmesh.utils.errors import PlanError +from sqlmesh.utils.rich import strip_ansi_codes def test_forward_only_plan_sets_version(make_snapshot, mocker: MockerFixture): diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index a9b56650c7..ee4ab0ac73 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -3,7 +3,6 @@ import pandas as pd # noqa: TID253 from sqlglot import exp from sqlmesh.core import dialect as d -import re import typing as t from io import StringIO from rich.console import Console @@ -14,6 +13,7 @@ from sqlmesh.core.table_diff import TableDiff, SchemaDiff import numpy as np # noqa: TID253 from sqlmesh.utils.errors import SQLMeshError +from sqlmesh.utils.rich import strip_ansi_codes pytestmark = pytest.mark.slow @@ -45,12 +45,6 @@ def capture_console_output(method_name: str, **kwargs) -> str: console_output.close() -def strip_ansi_codes(text: str) -> str: - """Strip ANSI color codes and styling from text.""" - ansi_escape = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]") - return ansi_escape.sub("", text).strip() - - def test_data_diff(sushi_context_fixed_date, capsys, caplog): model = sushi_context_fixed_date.models['"memory"."sushi"."customer_revenue_by_day"'] diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 9478f4aa6b..7d65a818f1 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -6,7 +6,7 @@ from pathlib import Path import unittest from unittest.mock import call, patch -from shutil import copyfile +from shutil import copyfile, rmtree import pandas as pd # noqa: TID253 import pytest @@ -31,6 +31,7 @@ from sqlmesh.core.model import Model, SqlModel, load_sql_based_model, model from sqlmesh.core.test.definition import ModelTest, PythonModelTest, SqlModelTest from sqlmesh.core.test.result import ModelTextTestResult +from sqlmesh.utils import Verbosity from sqlmesh.utils.errors import ConfigError, SQLMeshError, TestError from sqlmesh.utils.yaml import dump as dump_yaml from sqlmesh.utils.yaml import load as load_yaml @@ -2218,6 +2219,7 @@ def test_test_with_resolve_template_macro(tmp_path: Path): _check_successful_or_raise(context.test()) +@use_terminal_console def test_test_output(tmp_path: Path) -> None: init_example_project(tmp_path, dialect="duckdb") @@ -2243,8 +2245,8 @@ def test_test_output(tmp_path: Path) -> None: rows: - item_id: 1 num_orders: 2 - - item_id: 2 - num_orders: 2 + - item_id: 4 + num_orders: 3 """ ) @@ -2255,40 +2257,130 @@ def test_test_output(tmp_path: Path) -> None: ) context = Context(paths=tmp_path, config=config) - # Case 1: Assert the log report is structured correctly - with capture_output() as output: + # Case 1: Ensure the log report is structured correctly + with capture_output() as captured_output: context.test() + output = captured_output.stdout + # Order may change due to concurrent execution - assert "F." in output.stderr or ".F" in output.stderr + assert "F." in output or ".F" in output assert ( - f"""====================================================================== -FAIL: test_example_full_model ({new_test_file}) -This is a test + f"""This is a test ---------------------------------------------------------------------- -AssertionError: Data mismatch (exp: expected, act: actual) - - num_orders - exp act -1 2.0 1.0 + Data mismatch +┏━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ +┃ ┃ item_id: ┃ ┃ num_orders: ┃ num_orders: ┃ +┃ Row ┃ Expected ┃ item_id: Actual ┃ Expected ┃ Actual ┃ +┡━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ +│ 0 │ 4.0 │ 2.0 │ 3.0 │ 1.0 │ +└─────┴─────────────────┴─────────────────┴─────────────────┴──────────────────┘ ----------------------------------------------------------------------""" - in output.stderr + in output ) - assert "Ran 2 tests" in output.stderr - assert "FAILED (failures=1)" in output.stderr + assert "Ran 2 tests" in output + assert "FAILED (failures=1)" in output - # Case 2: Assert that concurrent execution is working properly + # Case 2: Ensure that the verbose log report is structured correctly + with capture_output() as captured_output: + context.test(verbosity=Verbosity.VERBOSE) + + output = captured_output.stdout + + assert ( + f"""This is a test +---------------------------------------------------------------------- + Column 'item_id' mismatch +┏━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ +┃ Row ┃ Expected ┃ Actual ┃ +┡━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ +│ 0 │ 4.0 │ 2.0 │ +└─────────────┴────────────────────────┴───────────────────┘ + + Column 'num_orders' mismatch +┏━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ +┃ Row ┃ Expected ┃ Actual ┃ +┡━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ +│ 0 │ 3.0 │ 1.0 │ +└─────────────┴────────────────────────┴───────────────────┘ + +----------------------------------------------------------------------""" + in output + ) + + # Case 3: Assert that concurrent execution is working properly for i in range(50): copyfile(original_test_file, tmp_path / "tests" / f"test_success_{i}.yaml") copyfile(new_test_file, tmp_path / "tests" / f"test_failure_{i}.yaml") - with capture_output() as output: + with capture_output() as captured_output: context.test() - assert "Ran 102 tests" in output.stderr - assert "FAILED (failures=51)" in output.stderr + output = captured_output.stdout + + assert "Ran 102 tests" in output + assert "FAILED (failures=51)" in output + + # Case 4: Test that wide tables are split into even chunks for default verbosity + rmtree(tmp_path / "tests") + + wide_model_query = ( + "SELECT 1 AS col_1, 2 AS col_2, 3 AS col_3, 4 AS col_4, 5 AS col_5, 6 AS col_6, 7 AS col_7" + ) + + context.upsert_model( + _create_model( + meta="MODEL(name test.test_wide_model)", + query=wide_model_query, + default_catalog=context.default_catalog, + ) + ) + + tests_dir = tmp_path / "tests" + tests_dir.mkdir() + + wide_test_file = tmp_path / "tests" / "test_wide_model.yaml" + wide_test_file_content = """ + test_wide_model: + model: test.test_wide_model + outputs: + query: + rows: + - col_1: 6 + col_2: 5 + col_3: 4 + col_4: 3 + col_5: 2 + col_6: 1 + col_7: 0 + + """ + + wide_test_file.write_text(wide_test_file_content) + + with capture_output() as captured_output: + context.test() + + assert ( + """Data mismatch +┏━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┓ +┃ ┃ col_1: ┃ col_1: ┃ col_2: ┃ col_2: ┃ col_3: ┃ col_3: ┃ col_4: ┃ col_4: ┃ +┃ Row ┃ Expec… ┃ Actual ┃ Expec… ┃ Actual ┃ Expec… ┃ Actual ┃ Expect… ┃ Actual ┃ +┡━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━━━┩ +│ 0 │ 6 │ 1 │ 5 │ 2 │ 4 │ 3 │ 3 │ 4 │ +└─────┴────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ + + Data mismatch +┏━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━┓ +┃ ┃ col_5: ┃ col_5: ┃ col_6: ┃ col_6: ┃ col_7: ┃ col_7: ┃ +┃ Row ┃ Expected ┃ Actual ┃ Expected ┃ Actual ┃ Expected ┃ Actual ┃ +┡━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━┩ +│ 0 │ 2 │ 5 │ 1 │ 6 │ 0 │ 7 │ +└─────┴───────────┴───────────┴───────────┴───────────┴───────────┴────────────┘""" + in captured_output.stdout + ) @use_terminal_console @@ -2330,15 +2422,15 @@ def test_test_output_with_invalid_model_name(tmp_path: Path) -> None: with capture_output() as output: context.test() - assert ( - f"""Model '"invalid_model"' was not found at {wrong_test_file}""" - in mock_logger.call_args[0][0] - ) - assert ( - ".\n----------------------------------------------------------------------\nRan 1 test in" - in output.stderr - ) - assert "OK" in output.stderr + assert ( + f"""Model '"invalid_model"' was not found at {wrong_test_file}""" + in mock_logger.call_args[0][0] + ) + assert ( + ".\n----------------------------------------------------------------------\n\nRan 1 test in" + in output.stdout + ) + assert "OK" in output.stdout def test_number_of_tests_found(tmp_path: Path) -> None: @@ -2553,6 +2645,7 @@ def upstream_table_python(context, **kwargs): ) +@use_terminal_console @pytest.mark.parametrize("is_error", [True, False]) def test_model_test_text_result_reporting_no_traceback( sushi_context: Context, full_model_with_two_ctes: SqlModel, is_error: bool @@ -2596,10 +2689,10 @@ def test_model_test_text_result_reporting_no_traceback( else: result.addFailure(test, (e.__class__, e, e.__traceback__)) - result.log_test_report(0) + with capture_output() as captured_output: + get_console().log_test_results(result, "duckdb") - stream.seek(0) - output = stream.read() + output = captured_output.stdout # Make sure that the traceback is not printed assert "Traceback" not in output diff --git a/tests/integrations/github/cicd/test_github_commands.py b/tests/integrations/github/cicd/test_github_commands.py index 17cd9fc7b7..9a22a74974 100644 --- a/tests/integrations/github/cicd/test_github_commands.py +++ b/tests/integrations/github/cicd/test_github_commands.py @@ -9,6 +9,7 @@ from sqlmesh.core import constants as c from sqlmesh.core.plan import Plan +from sqlmesh.core.test.result import ModelTextTestResult from sqlmesh.core.user import User, UserRole from sqlmesh.integrations.github.cicd import command from sqlmesh.integrations.github.cicd.config import GithubCICDBotConfig, MergeMethod @@ -448,11 +449,11 @@ def test_run_all_test_failed( github_client, bot_config=GithubCICDBotConfig(merge_method=MergeMethod.MERGE), ) - test_result = TestResult() + test_result = ModelTextTestResult(stream=None, descriptions=True, verbosity=0) test_result.testsRun += 1 - test_result.addFailure(TestCase(), (None, None, None)) + test_result.addFailure(TestCase(), (TestError, TestError("some error"), None)) controller._context._run_tests = mocker.MagicMock( - side_effect=lambda **kwargs: (test_result, "some error") + side_effect=lambda **kwargs: (test_result, "") ) controller._context.users = [ User(username="test", github_username="test_github", roles=[UserRole.REQUIRED_APPROVER]) @@ -474,15 +475,9 @@ def test_run_all_test_failed( assert GithubCheckConclusion(test_checks_runs[2]["conclusion"]).is_failure assert test_checks_runs[2]["output"]["title"] == "Tests Failed" assert ( - test_checks_runs[2]["output"]["summary"] - == """**Num Successful Tests: 0** - - -```some error``` - - -""" + """sqlmesh.utils.errors.TestError: some error""" in test_checks_runs[2]["output"]["summary"] ) + assert """**Num Successful Tests: 0**""" in test_checks_runs[2]["output"]["summary"] assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping prod_plan_preview_checks_runs = controller._check_run_mapping[ diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index ac9be5cc9c..a4d98e1963 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -707,20 +707,6 @@ def test_test(notebook, sushi_context): assert test_file.read_text() == """test_customer_revenue_by_day: TESTING\n""" -def test_run_test(notebook, sushi_context): - with capture_output() as output: - notebook.run_line_magic( - magic_name="run_test", - line=f"{sushi_context.path / 'tests' / 'test_customer_revenue_by_day.yaml'}::test_customer_revenue_by_day", - ) - - assert not output.stdout - # TODO: Does it make sense for this to go to stderr? - assert "Ran 1 test" in output.stderr - assert "OK" in output.stderr - assert not output.outputs - - @pytest.mark.slow def test_audit(notebook, loaded_sushi_context, convert_all_html_output_to_text): with capture_output() as output: diff --git a/web/server/api/endpoints/commands.py b/web/server/api/endpoints/commands.py index b879fb07cd..5db3c85d66 100644 --- a/web/server/api/endpoints/commands.py +++ b/web/server/api/endpoints/commands.py @@ -1,7 +1,6 @@ from __future__ import annotations import asyncio -import io import typing as t from fastapi import APIRouter, Body, Depends, Request, Response @@ -142,12 +141,10 @@ async def test( context: Context = Depends(get_loaded_context), ) -> models.TestResult: """Run one or all model tests""" - test_output = io.StringIO() try: result = context.test( tests=[str(context.path / Path(test))] if test else None, verbosity=verbosity, - stream=test_output, ) except Exception: import traceback @@ -157,11 +154,7 @@ async def test( message="Unable to run tests", origin="API -> commands -> test", ) - context.console.log_test_results( - result, - test_output.getvalue(), - context.test_connection_config._engine_adapter.DIALECT, - ) + context.console.log_test_results(result, context.test_connection_config._engine_adapter.DIALECT) def _test_path(test: ModelTest) -> t.Optional[str]: if path := test.path_relative_to(context.path): diff --git a/web/server/console.py b/web/server/console.py index 6077c3fb9b..2cda0af697 100644 --- a/web/server/console.py +++ b/web/server/console.py @@ -3,7 +3,6 @@ import asyncio import json import typing as t -import unittest from fastapi.encoders import jsonable_encoder from sse_starlette.sse import ServerSentEvent from sqlmesh.core.snapshot.definition import Interval, Intervals @@ -12,6 +11,7 @@ from sqlmesh.core.plan.definition import EvaluatablePlan from sqlmesh.core.snapshot import Snapshot, SnapshotInfoLike, SnapshotTableInfo from sqlmesh.core.test import ModelTest +from sqlmesh.core.test.result import ModelTextTestResult from sqlmesh.utils.date import now_timestamp from web.server import models from web.server.exceptions import ApiException @@ -258,9 +258,7 @@ def log_event( ) ) - def log_test_results( - self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str - ) -> None: + def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None: if result.wasSuccessful(): self.log_event( event=models.EventName.TESTS, @@ -279,6 +277,8 @@ def log_test_results( details=details, ) ) + + output = self._captured_unit_test_results(result) self.log_event( event=models.EventName.TESTS, data=models.ReportTestsFailure( From a9478a59a3296c110af32e8a9a5c8f9996b30d76 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:28:06 +0300 Subject: [PATCH 0410/1056] Chore!: bump sqlglot to v26.29.0 (#4756) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ea20c21e74..6405d64d16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.27.1", + "sqlglot[rs]~=26.29.0", "tenacity", "time-machine", "json-stream" From 4b478c490fbd373b6fe812dbdff733bcbe9389ed Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:09:50 +0300 Subject: [PATCH 0411/1056] Fix: Don't fail when cache directories are already removed when clearing (#4758) --- sqlmesh/core/context.py | 4 +++- tests/core/test_context.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index d1e58366eb..7d17b3b863 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -2438,7 +2438,9 @@ def table_name( def clear_caches(self) -> None: for path in self.configs: - rmtree(path / c.CACHE) + cache_path = path / c.CACHE + if cache_path.exists(): + rmtree(cache_path) if isinstance(self.state_sync, CachingStateSync): self.state_sync.clear_cache() diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 3ce1e48893..3d02d32e7e 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -640,6 +640,11 @@ def test_clear_caches(tmp_path: pathlib.Path): assert not cache_dir.exists() assert models_dir.exists() + # Test clearing caches when cache directory doesn't exist + # This should not raise an exception + context.clear_caches() + assert not cache_dir.exists() + def test_ignore_files(mocker: MockerFixture, tmp_path: pathlib.Path): mocker.patch.object( From b0535ed05e7472c9d5b9d94644560a162df95e0f Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:03:39 +0200 Subject: [PATCH 0412/1056] refactor(lsp): make lsp more predictable and stable (#4759) --- sqlmesh/lsp/main.py | 247 +++++++----------- vscode/extension/src/lsp/lsp.ts | 16 ++ vscode/extension/tests/bad_setup.spec.ts | 53 ++++ vscode/extension/tests/broken_project.spec.ts | 94 +++++++ 4 files changed, 262 insertions(+), 148 deletions(-) create mode 100644 vscode/extension/tests/broken_project.spec.ts diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 641b570c84..2a3e18c6ea 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -74,6 +74,8 @@ def __init__( self.lsp_context: t.Optional[LSPContext] = None self.workspace_folders: t.List[Path] = [] + self.has_raised_loading_error: bool = False + self.client_supports_pull_diagnostics = False self._supported_custom_methods: t.Dict[ str, @@ -94,29 +96,6 @@ def __init__( # Register LSP features (e.g., formatting, hover, etc.) self._register_features() - def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: - """Create a new LSPContext instance using the configured context class. - - On success, sets self.lsp_context and returns the created context. - - Args: - paths: List of paths to pass to the context constructor - - Returns: - A new LSPContext instance wrapping the created context, or None if creation fails - """ - try: - if self.lsp_context is None: - context = self.context_class(paths=paths) - else: - self.lsp_context.context.load() - context = self.lsp_context.context - self.lsp_context = LSPContext(context) - return self.lsp_context - except Exception as e: - self.server.log_trace(f"Error creating context: {e}") - return None - # All the custom LSP methods are registered here and prefixed with _custom def _custom_all_models(self, ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse: uri = URI(params.textDocument.uri) @@ -145,26 +124,16 @@ def _custom_render_model( def _custom_all_models_for_render( self, ls: LanguageServer, params: AllModelsForRenderRequest ) -> AllModelsForRenderResponse: - if self.lsp_context is None: - current_path = Path.cwd() - self._ensure_context_in_folder(current_path) - if self.lsp_context is None: - raise RuntimeError("No context found") - return AllModelsForRenderResponse(models=self.lsp_context.list_of_models_for_rendering()) + context = self._context_get_or_load() + return AllModelsForRenderResponse(models=context.list_of_models_for_rendering()) def _custom_format_project( self, ls: LanguageServer, params: FormatProjectRequest ) -> FormatProjectResponse: """Format all models in the current project.""" try: - if self.lsp_context is None: - current_path = Path.cwd() - self._ensure_context_in_folder(current_path) - if self.lsp_context is None: - raise RuntimeError("No context found") - - # Call the format method on the context - self.lsp_context.context.format() + context = self._context_get_or_load() + context.context.format() return FormatProjectResponse() except Exception as e: ls.log_trace(f"Error formatting project: {e}") @@ -174,12 +143,7 @@ def _custom_api( self, ls: LanguageServer, request: ApiRequest ) -> t.Union[ApiResponseGetModels, ApiResponseGetColumnLineage, ApiResponseGetLineage]: ls.log_trace(f"API request: {request}") - if self.lsp_context is None: - current_path = Path.cwd() - self._ensure_context_in_folder(current_path) - if self.lsp_context is None: - ls.log_trace("No context found in call") - raise RuntimeError("No context found") + context = self._context_get_or_load() parsed_url = urllib.parse.urlparse(request.url) path_parts = parsed_url.path.strip("/").split("/") @@ -187,13 +151,13 @@ def _custom_api( if request.method == "GET": if path_parts == ["api", "models"]: # /api/models - return ApiResponseGetModels(data=get_models(self.lsp_context.context)) + return ApiResponseGetModels(data=get_models(context.context)) if path_parts[:2] == ["api", "lineage"]: if len(path_parts) == 3: # /api/lineage/{model} model_name = urllib.parse.unquote(path_parts[2]) - lineage = model_lineage(model_name, self.lsp_context.context) + lineage = model_lineage(model_name, context.context) non_set_lineage = {k: v for k, v in lineage.items() if v is not None} return ApiResponseGetLineage(data=non_set_lineage) @@ -205,7 +169,7 @@ def _custom_api( if hasattr(request, "params"): models_only = bool(getattr(request.params, "models_only", False)) column_lineage_response = column_lineage( - model_name, column, models_only, self.lsp_context.context + model_name, column, models_only, context.context ) return ApiResponseGetColumnLineage(data=column_lineage_response) @@ -280,15 +244,10 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> None: uri = URI(params.text_document.uri) context = self._context_get_or_load(uri) - models = context.map[uri.to_path()] - if models is None or not isinstance(models, ModelTarget): - return - - # Get diagnostics from context (which handles caching) - diagnostics = context.lint_model(uri) # Only publish diagnostics if client doesn't support pull diagnostics if not self.client_supports_pull_diagnostics: + diagnostics = context.lint_model(uri) ls.publish_diagnostics( params.text_document.uri, SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), @@ -297,22 +256,16 @@ def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> Non @self.server.feature(types.TEXT_DOCUMENT_DID_SAVE) def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> None: uri = URI(params.text_document.uri) - - # Reload the entire context and create a new LSPContext - if self.lsp_context is not None: - if self._create_lsp_context(list(self.lsp_context.context.configs)): - return - - context = self._context_get_or_load(uri) - models = context.map[uri.to_path()] - if models is None or not isinstance(models, ModelTarget): + if self.lsp_context is None: return - # Get diagnostics from context (which handles caching) - diagnostics = context.lint_model(uri) + context = self.lsp_context.context + context.load() + self.lsp_context = LSPContext(context) # Only publish diagnostics if client doesn't support pull diagnostics if not self.client_supports_pull_diagnostics: + diagnostics = self.lsp_context.lint_model(uri) ls.publish_diagnostics( params.text_document.uri, SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), @@ -325,18 +278,16 @@ def formatting( """Format the document using SQLMesh `format_model_expressions`.""" try: uri = URI(params.text_document.uri) - self._ensure_context_for_document(uri) + context = self._context_get_or_load(uri) document = ls.workspace.get_text_document(params.text_document.uri) before = document.source - if self.lsp_context is None: - raise RuntimeError(f"No context found for document: {document.path}") target = next( ( target for target in chain( - self.lsp_context.context._models.values(), - self.lsp_context.context._audits.values(), + context.context._models.values(), + context.context._audits.values(), ) if target._path is not None and target._path.suffix == ".sql" @@ -346,7 +297,7 @@ def formatting( ) if target is None: return [] - after = self.lsp_context.context._format( + after = context.context._format( target=target, before=before, ) @@ -371,12 +322,10 @@ def hover(ls: LanguageServer, params: types.HoverParams) -> t.Optional[types.Hov """Provide hover information for an object.""" try: uri = URI(params.text_document.uri) - self._ensure_context_for_document(uri) + context = self._context_get_or_load(uri) document = ls.workspace.get_text_document(params.text_document.uri) - if self.lsp_context is None: - raise RuntimeError(f"No context found for document: {document.path}") - references = get_references(self.lsp_context, uri, params.position) + references = get_references(context, uri, params.position) if not references: return None reference = references[0] @@ -403,13 +352,11 @@ def inlay_hint( """Implement type hints for sql columns as inlay hints""" try: uri = URI(params.text_document.uri) - self._ensure_context_for_document(uri) - if self.lsp_context is None: - raise RuntimeError(f"No context found for document: {uri}") + context = self._context_get_or_load(uri) start_line = params.range.start.line end_line = params.range.end.line - hints = get_hints(self.lsp_context, uri, start_line, end_line) + hints = get_hints(context, uri, start_line, end_line) return hints except Exception as e: @@ -422,12 +369,9 @@ def goto_definition( """Jump to an object's definition.""" try: uri = URI(params.text_document.uri) - self._ensure_context_for_document(uri) - document = ls.workspace.get_text_document(params.text_document.uri) - if self.lsp_context is None: - raise RuntimeError(f"No context found for document: {document.path}") + context = self._context_get_or_load(uri) - references = get_references(self.lsp_context, uri, params.position) + references = get_references(context, uri, params.position) location_links = [] for reference in references: # Use target_range if available (CTEs, Macros, and external models in YAML) @@ -479,12 +423,9 @@ def find_references( """Find all references of a symbol (supporting CTEs, models for now)""" try: uri = URI(params.text_document.uri) - self._ensure_context_for_document(uri) - document = ls.workspace.get_text_document(params.text_document.uri) - if self.lsp_context is None: - raise RuntimeError(f"No context found for document: {document.path}") + context = self._context_get_or_load(uri) - all_references = get_all_references(self.lsp_context, uri, params.position) + all_references = get_all_references(context, uri, params.position) # Convert references to Location objects locations = [types.Location(uri=ref.uri, range=ref.range) for ref in all_references] @@ -531,12 +472,7 @@ def workspace_diagnostic( ) -> types.WorkspaceDiagnosticReport: """Handle workspace-wide diagnostic pull requests from the client.""" try: - if self.lsp_context is None: - current_path = Path.cwd() - self._ensure_context_in_folder(current_path) - - if self.lsp_context is None: - return types.WorkspaceDiagnosticReport(items=[]) + context = self._context_get_or_load() items: t.List[ t.Union[ @@ -546,7 +482,7 @@ def workspace_diagnostic( ] = [] # Get all SQL and Python model files from the context - for path, target in self.lsp_context.map.items(): + for path, target in context.map.items(): if isinstance(target, ModelTarget): uri = URI.from_path(path) diagnostics, result_id = self._get_diagnostics_for_uri(uri) @@ -679,79 +615,94 @@ def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic] except Exception: return [], 0 - def _context_get_or_load(self, document_uri: URI) -> LSPContext: + def _context_get_or_load(self, document_uri: t.Optional[URI] = None) -> LSPContext: if self.lsp_context is None: self._ensure_context_for_document(document_uri) if self.lsp_context is None: - raise RuntimeError("No context found") + raise RuntimeError("No context found able to get or load") return self.lsp_context - def _ensure_context_in_folder(self, folder_uri: Path) -> None: - if self.lsp_context is not None: - return - - # First, check the provided folder - for ext in ("py", "yml", "yaml"): - config_path = folder_uri / f"config.{ext}" - if config_path.exists(): - if self._create_lsp_context([folder_uri]): - loaded_sqlmesh_message(self.server, folder_uri) - return - - # If not found in the provided folder, search through all workspace folders - for workspace_folder in self.workspace_folders: - for ext in ("py", "yml", "yaml"): - config_path = workspace_folder / f"config.{ext}" - if config_path.exists(): - if self._create_lsp_context([workspace_folder]): - loaded_sqlmesh_message(self.server, workspace_folder) - return - def _ensure_context_for_document( self, - document_uri: URI, + document_uri: t.Optional[URI] = None, ) -> None: """ Ensure that a context exists for the given document if applicable by searching for a config.py or config.yml file in the parent directories. """ + if document_uri is not None: + document_path = document_uri.to_path() + if document_path.is_file() and document_path.suffix in (".sql", ".py"): + document_folder = document_path.parent + if document_folder.is_dir(): + self._ensure_context_in_folder(document_folder) + return + + return self._ensure_context_in_folder() + + def _ensure_context_in_folder(self, folder_path: t.Optional[Path] = None) -> None: if self.lsp_context is not None: - self.lsp_context.context.load() - self.lsp_context = LSPContext(self.lsp_context.context) return - # No context yet: try to find config and load it - path = document_uri.to_path() - if path.suffix not in (".sql", ".py"): - return + # If not found in the provided folder, search through all workspace folders + for workspace_folder in self.workspace_folders: + for ext in ("py", "yml", "yaml"): + config_path = workspace_folder / f"config.{ext}" + if config_path.exists(): + if self._create_lsp_context([workspace_folder]): + return - loaded = False - # Ascend directories to look for config - current_dir = path.parent # Start from the file's parent directory - while current_dir.parents and not loaded: + # Then , check the provided folder recursively + path = folder_path + if path is None: + path = Path.cwd() + while path.is_dir(): for ext in ("py", "yml", "yaml"): - config_path = current_dir / f"config.{ext}" + config_path = path / f"config.{ext}" if config_path.exists(): - if self._create_lsp_context([current_dir]): - loaded = True - # Re-check context for the document now that it's loaded - return self._ensure_context_for_document(document_uri) - # Check if we've reached the filesystem root to prevent infinite loops - if current_dir == current_dir.parent: + if self._create_lsp_context([path]): + return + + path = path.parent + if path == path.parent: break - current_dir = current_dir.parent - - # If still no context found, try the workspace folders - if not loaded: - for workspace_folder in self.workspace_folders: - for ext in ("py", "yml", "yaml"): - config_path = workspace_folder / f"config.{ext}" - if config_path.exists(): - if self._create_lsp_context([workspace_folder]): - loaded_sqlmesh_message(self.server, workspace_folder) - return - - return + + raise RuntimeError( + f"No context found in workspaces folders {self.workspace_folders}" + + (f" or in {folder_path}" if folder_path else "") + ) + + def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: + """Create a new LSPContext instance using the configured context class. + + On success, sets self.lsp_context and returns the created context. + + Args: + paths: List of paths to pass to the context constructor + + Returns: + A new LSPContext instance wrapping the created context, or None if creation fails + """ + try: + if self.lsp_context is None: + context = self.context_class(paths=paths) + loaded_sqlmesh_message(self.server, paths[0]) + else: + self.lsp_context.context.load() + context = self.lsp_context.context + self.lsp_context = LSPContext(context) + return self.lsp_context + except Exception as e: + # Only show the error message once + if not self.has_raised_loading_error: + self.server.show_message( + f"Error creating context: {e}", + types.MessageType.Error, + ) + self.has_raised_loading_error = True + + self.server.log_trace(f"Error creating context: {e}") + return None @staticmethod def _diagnostic_to_lsp_diagnostic( diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index a1ad08a864..9432762aed 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -81,6 +81,14 @@ export class LSPClient implements Disposable { transport: TransportKind.stdio, options: { cwd: workspacePath, + // TODO: This is a temporary fix to avoid the issue with the LSP server + // crashing when the number of workers is too high. This is a workaround + // to avoid the issue. Once fixed, we should remove the whole env block. + env: { + MAX_FORK_WORKERS: '1', + ...process.env, + ...sqlmesh.value.env, + }, }, args: sqlmesh.value.args, }, @@ -89,6 +97,14 @@ export class LSPClient implements Disposable { transport: TransportKind.stdio, options: { cwd: workspacePath, + env: { + // TODO: This is a temporary fix to avoid the issue with the LSP server + // crashing when the number of workers is too high. This is a workaround + // to avoid the issue. Once fixed, we should remove the whole env block. + MAX_FORK_WORKERS: '1', + ...process.env, + ...sqlmesh.value.env, + }, }, args: sqlmesh.value.args, }, diff --git a/vscode/extension/tests/bad_setup.spec.ts b/vscode/extension/tests/bad_setup.spec.ts index 2c92e89764..cf32718ea4 100644 --- a/vscode/extension/tests/bad_setup.spec.ts +++ b/vscode/extension/tests/bad_setup.spec.ts @@ -111,3 +111,56 @@ test('lineage, no sqlmesh found', async ({}) => { await fs.remove(tempDir) } }) + +// Checks that if you have another file open like somewhere else, it still checks the workspace first for a successful context +// it's very flaky but runs when debugging +// - the typing in of the file name is very flaky +test('check that the LSP runs correctly by opening lineage when looking at another file before not in workspace', async ({}) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const pythonEnvDir = path.join(tempDir, '.venv') + const pythonDetails = await createVirtualEnvironment(pythonEnvDir) + const sqlmeshWithExtras = `${REPO_ROOT}[lsp, bigquery]` + const custom_materializations = path.join( + REPO_ROOT, + 'examples', + 'custom_materializations', + ) + await pipInstall(pythonDetails, [sqlmeshWithExtras, custom_materializations]) + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonDetails.pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { + spaces: 2, + }) + + // Write a sql file in another folder + const tempDir2 = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-2-'), + ) + const sqlFile = path.join(tempDir2, 'models', 'customers.sql') + await fs.ensureDir(path.dirname(sqlFile)) + await fs.writeFile(sqlFile, 'SELECT 1') + + const { window, close } = await startVSCode(tempDir) + try { + // Open the SQL file from the other directory + await window.keyboard.press('Meta+P') + await window.waitForTimeout(100) + await window.keyboard.type(sqlFile.toString(), { delay: 10 }) + await window.waitForTimeout(100) + await window.keyboard.press('Enter') + await window.waitForTimeout(100) + + await window.waitForSelector('text=Loaded SQLMesh context') + } finally { + await close() + await fs.remove(tempDir) + } +}) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts new file mode 100644 index 0000000000..eff1b7cb02 --- /dev/null +++ b/vscode/extension/tests/broken_project.spec.ts @@ -0,0 +1,94 @@ +import { test } from '@playwright/test' +import fs from 'fs-extra' +import os from 'os' +import path from 'path' +import { openLineageView, startVSCode, SUSHI_SOURCE_PATH } from './utils' + +test('bad project, double model', async ({}) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Read the customers.sql file + const customersSql = await fs.readFile( + path.join(tempDir, 'models', 'customers.sql'), + 'utf8', + ) + + // Write the customers.sql file with a double model + await fs.writeFile( + path.join(tempDir, 'models', 'customers_duplicated.sql'), + customersSql, + ) + + const { window, close } = await startVSCode(tempDir) + try { + await window.waitForSelector('text=models') + + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=Error creating context') + + await window.waitForTimeout(1000) + } finally { + await close() + await fs.remove(tempDir) + } +}) + +test('bad project, double model, then fixed', async ({}) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Read the customers.sql file + const customersSql = await fs.readFile( + path.join(tempDir, 'models', 'customers.sql'), + 'utf8', + ) + + // Write the customers.sql file with a double model + await fs.writeFile( + path.join(tempDir, 'models', 'customers_duplicated.sql'), + customersSql, + ) + + const { window, close } = await startVSCode(tempDir) + try { + await window.waitForSelector('text=models') + + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=Error creating context') + + // Remove the duplicated model + await fs.remove(path.join(tempDir, 'models', 'customers_duplicated.sql')) + + // Open the linage view + await openLineageView(window) + + // Wait for the error to go away + await window.waitForSelector('text=Loaded SQLMesh context') + } finally { + await close() + await fs.remove(tempDir) + } +}) From ea35b3c8500dee8d21ef87d1d6a0a09179c35045 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 18 Jun 2025 10:17:54 -0700 Subject: [PATCH 0413/1056] Fix: Reporting the absence of physical layer updates when explaining a plan (#4760) --- sqlmesh/core/plan/explainer.py | 10 +++++----- sqlmesh/core/plan/stages.py | 9 ++++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/sqlmesh/core/plan/explainer.py b/sqlmesh/core/plan/explainer.py index aa1d5775b4..75aa0966ff 100644 --- a/sqlmesh/core/plan/explainer.py +++ b/sqlmesh/core/plan/explainer.py @@ -90,16 +90,16 @@ def visit_after_all_stage(self, stage: stages.AfterAllStage) -> Tree: return Tree("[bold]Execute after all statements[/bold]") def visit_physical_layer_update_stage(self, stage: stages.PhysicalLayerUpdateStage) -> Tree: - if not stage.snapshots: + snapshots = [ + s for s in stage.snapshots if s.snapshot_id in stage.snapshots_with_missing_intervals + ] + if not snapshots: return Tree("[bold]SKIP: No physical layer updates to perform[/bold]") tree = Tree( "[bold]Validate SQL and create physical layer tables and views if they do not exist[/bold]" ) - for snapshot in stage.snapshots: - if snapshot.snapshot_id not in stage.snapshots_with_missing_intervals: - continue - + for snapshot in snapshots: is_deployable = ( stage.deployability_index.is_deployable(snapshot) if self.environment_naming_info.name != c.PROD diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index d8c7cba85d..b50edfbb01 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -8,6 +8,7 @@ from sqlmesh.core.snapshot.definition import ( DeployabilityIndex, Snapshot, + SnapshotChangeCategory, SnapshotTableInfo, SnapshotId, Interval, @@ -255,7 +256,13 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: if s.is_paused and s.is_model and not s.is_symbolic - and (not deployability_index_for_creation.is_representative(s) or s.is_view) + and ( + not deployability_index_for_creation.is_representative(s) + or ( + s.is_view + and s.change_category == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + ) ] snapshots_to_intervals = self._missing_intervals( From 2e5e2c838ce4e97f1a02959a9fc3875f64f2f77c Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 18 Jun 2025 23:18:29 +0200 Subject: [PATCH 0414/1056] feat: use new tcloud sqlmesh_lsp call (#4762) --- vscode/extension/src/utilities/semver.test.ts | 37 ++++ vscode/extension/src/utilities/semver.ts | 24 +++ .../src/utilities/sqlmesh/sqlmesh.ts | 45 +++++ vscode/extension/tests/tcloud.spec.ts | 175 +++++++++++++++++- .../extension/tests/tcloud/mock_tcloud/cli.py | 58 +++++- 5 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 vscode/extension/src/utilities/semver.test.ts create mode 100644 vscode/extension/src/utilities/semver.ts diff --git a/vscode/extension/src/utilities/semver.test.ts b/vscode/extension/src/utilities/semver.test.ts new file mode 100644 index 0000000000..951bf03c04 --- /dev/null +++ b/vscode/extension/src/utilities/semver.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest' +import { isSemVerGreaterThanOrEqual } from './semver' + +describe('isSemVerGreaterThanOrEqual', () => { + it('should return true when major version is greater', () => { + expect(isSemVerGreaterThanOrEqual([2, 0, 0], [1, 0, 0])).toBe(true) + expect(isSemVerGreaterThanOrEqual([3, 0, 0], [2, 5, 10])).toBe(true) + }) + + it('should return false when major version is less', () => { + expect(isSemVerGreaterThanOrEqual([1, 0, 0], [2, 0, 0])).toBe(false) + expect(isSemVerGreaterThanOrEqual([0, 10, 20], [1, 0, 0])).toBe(false) + }) + + it('should compare minor version when major versions are equal', () => { + expect(isSemVerGreaterThanOrEqual([1, 2, 0], [1, 1, 0])).toBe(true) + expect(isSemVerGreaterThanOrEqual([1, 1, 0], [1, 2, 0])).toBe(false) + expect(isSemVerGreaterThanOrEqual([2, 5, 0], [2, 3, 10])).toBe(true) + }) + + it('should compare patch version when major and minor versions are equal', () => { + expect(isSemVerGreaterThanOrEqual([1, 1, 2], [1, 1, 1])).toBe(true) + expect(isSemVerGreaterThanOrEqual([1, 1, 1], [1, 1, 2])).toBe(false) + expect(isSemVerGreaterThanOrEqual([2, 3, 10], [2, 3, 5])).toBe(true) + }) + + it('should return true when versions are equal', () => { + expect(isSemVerGreaterThanOrEqual([1, 0, 0], [1, 0, 0])).toBe(true) + expect(isSemVerGreaterThanOrEqual([2, 5, 10], [2, 5, 10])).toBe(true) + }) + + it('should handle zero versions correctly', () => { + expect(isSemVerGreaterThanOrEqual([0, 0, 1], [0, 0, 0])).toBe(true) + expect(isSemVerGreaterThanOrEqual([0, 1, 0], [0, 0, 10])).toBe(true) + expect(isSemVerGreaterThanOrEqual([0, 0, 0], [0, 0, 0])).toBe(true) + }) +}) diff --git a/vscode/extension/src/utilities/semver.ts b/vscode/extension/src/utilities/semver.ts new file mode 100644 index 0000000000..fed83af4a4 --- /dev/null +++ b/vscode/extension/src/utilities/semver.ts @@ -0,0 +1,24 @@ +type SemVer = [number, number, number] + +/** + * Check if a is greater than or equal to b. + * + * @param a - The first version. + * @param b - The second version. + * @returns True if a is greater than b, false otherwise. + */ +export function isSemVerGreaterThanOrEqual(a: SemVer, b: SemVer): boolean { + if (a[0] > b[0]) { + return true + } + if (a[0] < b[0]) { + return false + } + if (a[1] > b[1]) { + return true + } + if (a[1] < b[1]) { + return false + } + return a[2] >= b[2] +} diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index f656937bab..37eb3c0b5c 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -12,6 +12,7 @@ import z from 'zod' import { ProgressLocation, window } from 'vscode' import { IS_WINDOWS } from '../isWindows' import { resolveProjectPath } from '../config' +import { isSemVerGreaterThanOrEqual } from '../semver' export interface SqlmeshExecInfo { workspacePath: string @@ -394,6 +395,23 @@ export const sqlmeshLspExec = async (): Promise< if (isErr(ensured)) { return ensured } + const tcloudBinVersion = await getTcloudBinVersion() + if (isErr(tcloudBinVersion)) { + return tcloudBinVersion + } + // TODO: Remove this once we have a stable version of tcloud that supports sqlmesh_lsp. + if (isSemVerGreaterThanOrEqual(tcloudBinVersion.value, [2, 10, 1])) { + return ok ({ + bin: tcloudBin.value, + workspacePath, + env: { + PYTHONPATH: interpreterDetails.path?.[0], + VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), + PATH: interpreterDetails.binPath!, + }, + args: ['sqlmesh_lsp'], + }) + } } const binPath = path.join(interpreterDetails.binPath!, sqlmeshLSP) traceLog(`Bin path: ${binPath}`) @@ -445,4 +463,31 @@ async function doesExecutableExist(executable: string): Promise { traceLog(`Checked if ${executable} exists with ${command}, errored, returning false`) return false } +} + +/** + * Get the version of the tcloud bin. + * + * @returns The version of the tcloud bin. + */ +async function getTcloudBinVersion(): Promise> { + const tcloudBin = await getTcloudBin() + if (isErr(tcloudBin)) { + return tcloudBin + } + const called = await execAsync(tcloudBin.value, ['--version']) + if (called.exitCode !== 0) { + return err({ + type: 'generic', + message: `Failed to get tcloud bin version: ${called.stderr}`, + }) + } + const version = called.stdout.split('.').map(Number) + if (version.length !== 3) { + return err({ + type: 'generic', + message: `Failed to get tcloud bin version: ${called.stdout}`, + }) + } + return ok(version as [number, number, number]) } \ No newline at end of file diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index d047a9dcd8..fe3a61f4dc 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -54,6 +54,17 @@ async function setupAuthenticatedState(tempDir: string): Promise { await fs.writeJson(authStateFile, authState) } +/** + * Helper function to set the tcloud version for testing + */ +async function setTcloudVersion( + tempDir: string, + version: string, +): Promise { + const versionStateFile = path.join(tempDir, '.tcloud_version_state.json') + await fs.writeJson(versionStateFile, { version }) +} + test.describe('Tcloud', () => { test('not signed in, shows sign in window', async ({}, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation @@ -77,6 +88,9 @@ test.describe('Tcloud', () => { `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, ) + // Set tcloud version to 2.10.0 + await setTcloudVersion(tempDir, '2.10.0') + // Set up Python environment with mock tcloud and sqlmesh const pythonPath = await setupPythonEnvironment(pythonEnvDir) @@ -126,7 +140,7 @@ test.describe('Tcloud', () => { } }) - test('signed in and not installed shows installation window and can see output', async ({}, testInfo) => { + test('signed in and not installed shows installation window', async ({}, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), @@ -151,6 +165,9 @@ test.describe('Tcloud', () => { // Write mock ".tcloud_auth_state.json" file await setupAuthenticatedState(tempDir) + // Set tcloud version to 2.10.0 + await setTcloudVersion(tempDir, '2.10.0') + // Set up Python environment with mock tcloud and sqlmesh const pythonPath = await setupPythonEnvironment(pythonEnvDir) @@ -199,4 +216,160 @@ test.describe('Tcloud', () => { await fs.remove(tempDir) } }) + + test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when ready', async ({}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Create a tcloud.yaml to mark this as a tcloud project + const tcloudConfig = { + url: 'https://mock.tobikodata.com', + org: 'test-org', + project: 'test-project', + } + await fs.writeFile( + path.join(tempDir, 'tcloud.yaml'), + `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, + ) + + // Write mock ".tcloud_auth_state.json" file + await setupAuthenticatedState(tempDir) + + // Set tcloud version to 2.10.0 + await setTcloudVersion(tempDir, '2.10.0') + + // Set up Python environment with mock tcloud and sqlmesh + const pythonPath = await setupPythonEnvironment(pythonEnvDir) + + // Mark sqlmesh as installed + const binDir = path.dirname(pythonPath) + const installStateFile = path.join(binDir, '.sqlmesh_installed') + await fs.writeFile(installStateFile, '') + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson( + path.join(tempDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) + + // Start VS Code + const { window, close } = await startVSCode(tempDir) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Verify the context loads successfully + await window.waitForSelector('text=Loaded SQLMesh context') + + // Close VS Code + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } + }) + + test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when ready', async ({}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Create a tcloud.yaml to mark this as a tcloud project + const tcloudConfig = { + url: 'https://mock.tobikodata.com', + org: 'test-org', + project: 'test-project', + } + await fs.writeFile( + path.join(tempDir, 'tcloud.yaml'), + `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, + ) + + // Write mock ".tcloud_auth_state.json" file + await setupAuthenticatedState(tempDir) + + // Set tcloud version to 2.10.0 + await setTcloudVersion(tempDir, '2.10.1') + + // Set up Python environment with mock tcloud and sqlmesh + const pythonPath = await setupPythonEnvironment(pythonEnvDir) + + // Mark sqlmesh as installed + const binDir = path.dirname(pythonPath) + const installStateFile = path.join(binDir, '.sqlmesh_installed') + await fs.writeFile(installStateFile, '') + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson( + path.join(tempDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) + + // Start VS Code + const { window, close } = await startVSCode(tempDir) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Verify the context loads successfully + await window.waitForSelector('text=Loaded SQLMesh context') + + // Close VS Code + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } + }) }) diff --git a/vscode/extension/tests/tcloud/mock_tcloud/cli.py b/vscode/extension/tests/tcloud/mock_tcloud/cli.py index b9b80ab8f1..55de42ca81 100755 --- a/vscode/extension/tests/tcloud/mock_tcloud/cli.py +++ b/vscode/extension/tests/tcloud/mock_tcloud/cli.py @@ -17,6 +17,11 @@ def get_auth_state_file(): return Path.cwd() / ".tcloud_auth_state.json" +def get_version_state_file(): + """Get the path to the version state file in the current working directory""" + return Path.cwd() / ".tcloud_version_state.json" + + def load_auth_state(): """Load authentication state from file""" auth_file = get_auth_state_file() @@ -33,15 +38,39 @@ def save_auth_state(state): json.dump(state, f) -@click.group(no_args_is_help=True) +def load_version_state(): + """Load version state from file""" + version_file = get_version_state_file() + if version_file.exists(): + with open(version_file, "r") as f: + return json.load(f) + # Default to version 2.10.0 if no state file exists + return {"version": "2.10.0"} + + +@click.group(no_args_is_help=True, invoke_without_command=True) @click.option( "--project", type=str, help="The name of the project.", ) +@click.option( + "--version", + is_flag=True, + help="Show version", +) @click.pass_context -def cli(ctx: click.Context, project: str) -> None: +def cli(ctx: click.Context, project: str, version: bool) -> None: """Mock Tobiko Cloud CLI""" + if version: + version_state = load_version_state() + print(version_state["version"]) + ctx.exit(0) + + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + ctx.exit(0) + ctx.ensure_object(dict) ctx.obj["project"] = project @@ -107,6 +136,31 @@ def sqlmesh(ctx: click.Context, args) -> None: sys.exit(result.returncode) +@cli.command("sqlmesh_lsp") +@click.argument("args", nargs=-1) +@click.pass_context +def sqlmesh_lsp(ctx: click.Context, args) -> None: + """Run SQLMesh LSP server""" + # For testing purposes, we'll simulate the LSP server starting + print("Starting SQLMesh LSP server...", flush=True) + + # Get the path to sqlmesh in the same environment as this script + bin_dir = os.path.dirname(sys.executable) + sqlmesh_path = os.path.join(bin_dir, "sqlmesh") + + if not os.path.exists(sqlmesh_path): + # Try with .exe extension on Windows + sqlmesh_path = os.path.join(bin_dir, "sqlmesh.exe") + + if not os.path.exists(sqlmesh_path): + # Fall back to using sqlmesh from PATH + sqlmesh_path = "sqlmesh" + + # Execute the real sqlmesh with lsp command and provided arguments + result = subprocess.run([sqlmesh_path, "lsp"] + list(args), capture_output=False) + sys.exit(result.returncode) + + @click.group() def auth() -> None: """ From c88daec166f1b8e6eb0088a4b0d4aeea7d922aaf Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:26:04 +0200 Subject: [PATCH 0415/1056] docs: warning about environment variables (#4761) --- docs/guides/vscode.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/guides/vscode.md b/docs/guides/vscode.md index fa6245ace1..a71599c056 100644 --- a/docs/guides/vscode.md +++ b/docs/guides/vscode.md @@ -149,6 +149,16 @@ Because the VSCode extension establishes a long-running process connected to the Therefore, we do not recommend using DuckDB as a state store with the VSCode extension. +### Environment variables + +The VSCode extension is based on a [language server](https://en.wikipedia.org/wiki/Language_Server_Protocol) that runs in the background as a separate process. When the VSCode extension starts the background language server, the server inherits environment variables from the environment where you started VSCode. The server does *not* inherit environment variables from your terminal instance in VSCode, so it may not have access to variables you use when calling SQLMesh from the CLI. + +If you have environment variables that are needed by the context and the language server, you can use one of these approaches to pass variables to the language server: + +- Open VSCode from a terminal that has the variables set +- Use environment variables pulled from somewhere else dynamically (e.g. a `.env` file) in your config +- Set the environment variables in the python environment that the extension uses. You can find detailed instructions [here](https://code.visualstudio.com/docs/python/environments#_environment-variables) + ### Python environment woes The most common problem is the extension not using the correct Python interpreter. From 8018d7aeace839eece5235d670fb7919517decf5 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:10:43 +0300 Subject: [PATCH 0416/1056] Chore: use engine_adapter.execute for statements in python model docs (#4754) --- docs/concepts/models/python_models.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/concepts/models/python_models.md b/docs/concepts/models/python_models.md index db25fe46f2..53495d11aa 100644 --- a/docs/concepts/models/python_models.md +++ b/docs/concepts/models/python_models.md @@ -163,7 +163,7 @@ def execute( ) -> pd.DataFrame: # pre-statement - context.fetchdf("SET GLOBAL parameter = 'value';") + context.engine_adapter.execute("SET GLOBAL parameter = 'value';") # post-statement requires using `yield` instead of `return` yield pd.DataFrame([ @@ -171,7 +171,7 @@ def execute( ]) # post-statement - context.fetchdf("CREATE INDEX idx ON example.pre_post_statements (id);") + context.engine_adapter.execute("CREATE INDEX idx ON example.pre_post_statements (id);") ``` ## Optional on-virtual-update statements From 461fb79bac643b93c88c5506360511064f5a7513 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:47:21 +0200 Subject: [PATCH 0417/1056] feat(vscode): .env file into python env (#4764) --- .../extension/src/utilities/common/python.ts | 35 +++++ .../src/utilities/sqlmesh/sqlmesh.ts | 34 ++++- vscode/extension/tests/python_env.spec.ts | 139 ++++++++++++++++++ vscode/extension/tests/tcloud.spec.ts | 33 +---- vscode/extension/tests/tcloud_utils.ts | 34 +++++ 5 files changed, 240 insertions(+), 35 deletions(-) create mode 100644 vscode/extension/tests/python_env.spec.ts create mode 100644 vscode/extension/tests/tcloud_utils.ts diff --git a/vscode/extension/src/utilities/common/python.ts b/vscode/extension/src/utilities/common/python.ts index bffae16a01..b30e2e91f7 100644 --- a/vscode/extension/src/utilities/common/python.ts +++ b/vscode/extension/src/utilities/common/python.ts @@ -5,6 +5,8 @@ import { commands, Disposable, Event, EventEmitter, Uri } from 'vscode' import { traceError, traceLog } from './log' import { PythonExtension, ResolvedEnvironment } from '@vscode/python-extension' import path from 'path' +import { err, ok, Result } from '@bus/result' +import * as vscode from 'vscode' export interface IInterpreterDetails { path?: string[] @@ -15,10 +17,12 @@ export interface IInterpreterDetails { const onDidChangePythonInterpreterEvent = new EventEmitter() + export const onDidChangePythonInterpreter: Event = onDidChangePythonInterpreterEvent.event let _api: PythonExtension | undefined + async function getPythonExtensionAPI(): Promise { if (_api) { return _api @@ -118,3 +122,34 @@ export function checkVersion( traceError('Supported versions are 3.8 and above.') return false } + +/** + * getPythonEnvVariables returns the environment variables for the current python interpreter. + * + * @returns The environment variables for the current python interpreter. + */ +export async function getPythonEnvVariables(): Promise< + Result, string> +> { + const api = await getPythonExtensionAPI() + if (!api) { + return err('Python extension API not found') + } + + const workspaces = vscode.workspace.workspaceFolders + if (!workspaces) { + return ok({}) + } + const out: Record = {} + for (const workspace of workspaces) { + const envVariables = api.environments.getEnvironmentVariables(workspace.uri) + if (envVariables) { + for (const [key, value] of Object.entries(envVariables)) { + if (value) { + out[key] = value + } + } + } + } + return ok(out) +} diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 37eb3c0b5c..a07336be21 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -1,6 +1,6 @@ import path from 'path' import { traceInfo, traceLog, traceVerbose } from '../common/log' -import { getInterpreterDetails } from '../common/python' +import { getInterpreterDetails, getPythonEnvVariables } from '../common/python' import { Result, err, isErr, ok } from '@bus/result' import { getProjectRoot } from '../common/utilities' import { isPythonModuleInstalled } from '../python' @@ -230,6 +230,13 @@ export const sqlmeshExec = async (): Promise< message: resolvedPath.error, }) } + const envVariables = await getPythonEnvVariables() + if (isErr(envVariables)) { + return err({ + type: 'generic', + message: envVariables.error, + }) + } const workspacePath = resolvedPath.value const interpreterDetails = await getInterpreterDetails() traceLog(`Interpreter details: ${JSON.stringify(interpreterDetails)}`) @@ -268,6 +275,8 @@ export const sqlmeshExec = async (): Promise< bin: `${tcloudBin.value} sqlmesh`, workspacePath, env: { + ...process.env, + ...envVariables.value, PYTHONPATH: interpreterDetails.path?.[0], VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), PATH: interpreterDetails.binPath!, @@ -281,6 +290,8 @@ export const sqlmeshExec = async (): Promise< bin: binPath, workspacePath, env: { + ...process.env, + ...envVariables.value, PYTHONPATH: interpreterDetails.path?.[0], VIRTUAL_ENV: path.dirname(path.dirname(interpreterDetails.binPath!)), // binPath now points to bin dir PATH: interpreterDetails.binPath!, @@ -297,7 +308,10 @@ export const sqlmeshExec = async (): Promise< return ok({ bin: sqlmesh, workspacePath, - env: {}, + env: { + ...process.env, + ...envVariables.value, + }, args: [], }) } @@ -353,6 +367,13 @@ export const sqlmeshLspExec = async (): Promise< > => { const sqlmeshLSP = IS_WINDOWS ? 'sqlmesh_lsp.exe' : 'sqlmesh_lsp' const projectRoot = await getProjectRoot() + const envVariables = await getPythonEnvVariables() + if (isErr(envVariables)) { + return err({ + type: 'generic', + message: envVariables.error, + }) + } const resolvedPath = resolveProjectPath(projectRoot) if (isErr(resolvedPath)) { return err({ @@ -408,6 +429,8 @@ export const sqlmeshLspExec = async (): Promise< PYTHONPATH: interpreterDetails.path?.[0], VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), PATH: interpreterDetails.binPath!, + ...process.env, + ...envVariables.value, }, args: ['sqlmesh_lsp'], }) @@ -431,6 +454,8 @@ export const sqlmeshLspExec = async (): Promise< PYTHONPATH: interpreterDetails.path?.[0], VIRTUAL_ENV: path.dirname(path.dirname(interpreterDetails.binPath!)), // binPath now points to bin dir PATH: interpreterDetails.binPath!, // binPath already points to the bin directory + ...process.env, + ...envVariables.value, }, args: [], }) @@ -444,7 +469,10 @@ export const sqlmeshLspExec = async (): Promise< return ok({ bin: sqlmeshLSP, workspacePath, - env: {}, + env: { + ...process.env, + ...envVariables.value, + }, args: [], }) } diff --git a/vscode/extension/tests/python_env.spec.ts b/vscode/extension/tests/python_env.spec.ts new file mode 100644 index 0000000000..9fae408696 --- /dev/null +++ b/vscode/extension/tests/python_env.spec.ts @@ -0,0 +1,139 @@ +import { test } from '@playwright/test' +import fs from 'fs-extra' +import { + createVirtualEnvironment, + openLineageView, + pipInstall, + PythonEnvironment, + REPO_ROOT, + startVSCode, + SUSHI_SOURCE_PATH, +} from './utils' +import os from 'os' +import path from 'path' +import { setTcloudVersion, setupAuthenticatedState } from './tcloud_utils' + +function writeEnvironmentConfig(sushiPath: string) { + const configPath = path.join(sushiPath, 'config.py') + const originalConfig = fs.readFileSync(configPath, 'utf8') + + const newConfig = + ` +import os + +test_var = os.getenv("TEST_VAR") +if test_var is None or test_var == "": + raise Exception("TEST_VAR is not set") +` + originalConfig + + fs.writeFileSync(configPath, newConfig) +} + +async function setupEnvironment(): Promise<[string, PythonEnvironment]> { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const pythonEnvDir = path.join(tempDir, '.venv') + const pythonDetails = await createVirtualEnvironment(pythonEnvDir) + const custom_materializations = path.join( + REPO_ROOT, + 'examples', + 'custom_materializations', + ) + const sqlmeshWithExtras = `${REPO_ROOT}[bigquery,lsp]` + await pipInstall(pythonDetails, [sqlmeshWithExtras, custom_materializations]) + + const settings = { + 'python.defaultInterpreterPath': pythonDetails.pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { + spaces: 2, + }) + + return [tempDir, pythonDetails] +} + +test.describe('python environment variable injection on sqlmesh_lsp', () => { + test('normal setup - error ', async () => { + const [tempDir, _] = await setupEnvironment() + writeEnvironmentConfig(tempDir) + const { window, close } = await startVSCode(tempDir) + try { + await openLineageView(window) + await window.waitForSelector('text=Error creating context') + } finally { + await close() + } + }) + + test('normal setup - set', async () => { + const [tempDir, _] = await setupEnvironment() + writeEnvironmentConfig(tempDir) + const env_file = path.join(tempDir, '.env') + fs.writeFileSync(env_file, 'TEST_VAR=test_value') + const { window, close } = await startVSCode(tempDir) + try { + await openLineageView(window) + await window.waitForSelector('text=Loaded SQLMesh context') + } finally { + await close() + } + }) +}) + +async function setupTcloudProject( + tempDir: string, + pythonDetails: PythonEnvironment, +) { + // Install the mock tcloud package + const mockTcloudPath = path.join(__dirname, 'tcloud') + await pipInstall(pythonDetails, [mockTcloudPath]) + + // Create a tcloud.yaml to mark this as a tcloud project + const tcloudConfig = { + url: 'https://mock.tobikodata.com', + org: 'test-org', + project: 'test-project', + } + await fs.writeFile( + path.join(tempDir, 'tcloud.yaml'), + `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, + ) + // Write mock ".tcloud_auth_state.json" file + await setupAuthenticatedState(tempDir) + // Set tcloud version to 2.10.1 + await setTcloudVersion(tempDir, '2.10.1') +} + +test.describe('tcloud version', () => { + test('normal setup - error ', async () => { + const [tempDir, pythonDetails] = await setupEnvironment() + await setupTcloudProject(tempDir, pythonDetails) + writeEnvironmentConfig(tempDir) + const { window, close } = await startVSCode(tempDir) + try { + await openLineageView(window) + await window.waitForSelector('text=Error creating context') + } finally { + await close() + } + }) + + test('normal setup - set', async () => { + const [tempDir, pythonDetails] = await setupEnvironment() + await setupTcloudProject(tempDir, pythonDetails) + writeEnvironmentConfig(tempDir) + const env_file = path.join(tempDir, '.env') + fs.writeFileSync(env_file, 'TEST_VAR=test_value') + const { window, close } = await startVSCode(tempDir) + try { + await openLineageView(window) + await window.waitForSelector('text=Loaded SQLMesh context') + } finally { + await close() + } + }) +}) diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index fe3a61f4dc..f01dfc1a33 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -9,6 +9,7 @@ import { startVSCode, SUSHI_SOURCE_PATH, } from './utils' +import { setTcloudVersion, setupAuthenticatedState } from './tcloud_utils' /** * Helper function to create and set up a Python virtual environment @@ -33,38 +34,6 @@ async function setupPythonEnvironment(envDir: string): Promise { return pythonDetails.pythonPath } -/** - * Helper function to set up a pre-authenticated tcloud state - */ -async function setupAuthenticatedState(tempDir: string): Promise { - const authStateFile = path.join(tempDir, '.tcloud_auth_state.json') - const authState = { - is_logged_in: true, - id_token: { - iss: 'https://mock.tobikodata.com', - aud: 'mock-audience', - sub: 'user-123', - scope: 'openid email profile', - iat: Math.floor(Date.now() / 1000), - exp: Math.floor(Date.now() / 1000) + 3600, // Valid for 1 hour - email: 'test@example.com', - name: 'Test User', - }, - } - await fs.writeJson(authStateFile, authState) -} - -/** - * Helper function to set the tcloud version for testing - */ -async function setTcloudVersion( - tempDir: string, - version: string, -): Promise { - const versionStateFile = path.join(tempDir, '.tcloud_version_state.json') - await fs.writeJson(versionStateFile, { version }) -} - test.describe('Tcloud', () => { test('not signed in, shows sign in window', async ({}, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation diff --git a/vscode/extension/tests/tcloud_utils.ts b/vscode/extension/tests/tcloud_utils.ts new file mode 100644 index 0000000000..5334b69ef6 --- /dev/null +++ b/vscode/extension/tests/tcloud_utils.ts @@ -0,0 +1,34 @@ +import path from 'path' +import fs from 'fs-extra' + +/** + * Helper function to set up a pre-authenticated tcloud state + */ +export async function setupAuthenticatedState(tempDir: string): Promise { + const authStateFile = path.join(tempDir, '.tcloud_auth_state.json') + const authState = { + is_logged_in: true, + id_token: { + iss: 'https://mock.tobikodata.com', + aud: 'mock-audience', + sub: 'user-123', + scope: 'openid email profile', + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 3600, // Valid for 1 hour + email: 'test@example.com', + name: 'Test User', + }, + } + await fs.writeJson(authStateFile, authState) +} + +/** + * Helper function to set the tcloud version for testing + */ +export async function setTcloudVersion( + tempDir: string, + version: string, +): Promise { + const versionStateFile = path.join(tempDir, '.tcloud_version_state.json') + await fs.writeJson(versionStateFile, { version }) +} From c8ae8a8248ead4e63a764a3d4dbd595b5f5defdc Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Thu, 19 Jun 2025 18:06:20 +0300 Subject: [PATCH 0418/1056] Chore: Minor refactors in `sqlmesh test` output (#4755) --- sqlmesh/core/console.py | 96 ++++++++++--------- sqlmesh/core/test/runner.py | 2 +- tests/core/test_test.py | 10 +- .../github/cicd/test_github_commands.py | 2 +- .../github/cicd/test_integration.py | 1 + 5 files changed, 56 insertions(+), 55 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index b73f2d576f..cc112e6ebd 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -1957,26 +1957,29 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> divider_length = 70 self._log_test_details(result) - self._print("\n") + message = ( + f"Ran {result.testsRun} tests against {target_dialect} in {result.duration} seconds." + ) if result.wasSuccessful(): self._print("=" * divider_length) self._print( - f"Successfully Ran {str(result.testsRun)} tests against {target_dialect}", + f"Successfully {message}", style="green", ) self._print("-" * divider_length) else: self._print("-" * divider_length) - self._print("Test Failure Summary") + self._print("Test Failure Summary", style="red") self._print("=" * divider_length) - self._print( - f"Num Successful Tests: {result.testsRun - len(result.failures) - len(result.errors)}" - ) + failures = len(result.failures) + len(result.errors) + self._print(f"{message} \n") + + self._print(f"Failed tests ({failures}):") for test, _ in result.failures + result.errors: if isinstance(test, ModelTest): - self._print(f"Failure Test: {test.path}::{test.test_name}") - self._print("=" * divider_length) + self._print(f" • {test.path}::{test.test_name}") + self._print("=" * divider_length, end="\n\n") def _captured_unit_test_results(self, result: ModelTextTestResult) -> str: with self.console.capture() as capture: @@ -2499,7 +2502,9 @@ def show_linter_violations( else: self.log_warning(msg) - def _log_test_details(self, result: ModelTextTestResult) -> None: + def _log_test_details( + self, result: ModelTextTestResult, unittest_char_separator: bool = True + ) -> None: """ This is a helper method that encapsulates the logic for logging the relevant unittest for the result. The top level method (`log_test_results`) reuses `_log_test_details` differently based on the console. @@ -2507,11 +2512,14 @@ def _log_test_details(self, result: ModelTextTestResult) -> None: Args: result: The unittest test result that contains metrics like num success, fails, ect. """ - tests_run = result.testsRun + + if result.wasSuccessful(): + self._print("\n", end="") + return + errors = result.errors failures = result.failures skipped = result.skipped - is_success = not (errors or failures) infos = [] if failures: @@ -2521,12 +2529,13 @@ def _log_test_details(self, result: ModelTextTestResult) -> None: if skipped: infos.append(f"skipped={skipped}") - self._print("\n", end="") + if unittest_char_separator: + self._print(f"\n{unittest.TextTestResult.separator1}\n\n", end="") for (test_case, failure), test_failure_tables in zip_longest( # type: ignore failures, result.failure_tables ): - self._print(unittest.TextTestResult.separator1) + self._print(unittest.TextTestResult.separator2) self._print(f"FAIL: {test_case}") if test_description := test_case.shortDescription(): @@ -2541,21 +2550,11 @@ def _log_test_details(self, result: ModelTextTestResult) -> None: self._print("\n", end="") for test_case, error in errors: - self._print(unittest.TextTestResult.separator1) + self._print(unittest.TextTestResult.separator2) self._print(f"ERROR: {test_case}") self._print(f"{unittest.TextTestResult.separator2}") self._print(error) - # Output final report - self._print(unittest.TextTestResult.separator2) - test_duration_msg = f" in {result.duration:.3f}s" if result.duration else "" - self._print( - f"\nRan {tests_run} {'tests' if tests_run > 1 else 'test'}{test_duration_msg} \n" - ) - self._print( - f"{'OK' if is_success else 'FAILED'}{' (' + ', '.join(infos) + ')' if infos else ''}" - ) - def _cells_match(x: t.Any, y: t.Any) -> bool: """Helper function to compare two cells and returns true if they're equal, handling array objects.""" @@ -2836,6 +2835,11 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> "font-weight": "bold", "font-family": "Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace", } + + message = ( + f"Ran {result.testsRun} tests against {target_dialect} in {result.duration} seconds." + ) + if result.wasSuccessful(): success_color = {"color": "#008000"} header = str(h("span", {"style": shared_style}, "-" * divider_length)) @@ -2843,7 +2847,7 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> h( "span", {"style": {**shared_style, **success_color}}, - f"Successfully Ran {str(result.testsRun)} tests against {target_dialect}", + f"Successfully {message}", ) ) footer = str(h("span", {"style": shared_style}, "=" * divider_length)) @@ -2855,31 +2859,31 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> fail_shared_style = {**shared_style, **fail_color} header = str(h("span", {"style": fail_shared_style}, "-" * divider_length)) message = str(h("span", {"style": fail_shared_style}, "Test Failure Summary")) - num_success = str( - h( - "span", - {"style": fail_shared_style}, - f"Num Successful Tests: {result.testsRun - len(result.failures) - len(result.errors)}", + failed_tests = [ + str( + h( + "span", + {"style": fail_shared_style}, + f"Failed tests ({len(result.failures) + len(result.errors)}):", + ) ) - ) - failure_tests = [] + ] + for test, _ in result.failures + result.errors: if isinstance(test, ModelTest): - failure_tests.append( + failed_tests.append( str( h( "span", {"style": fail_shared_style}, - f"Failure Test: {test.model.name} {test.test_name}", + f" • {test.model.name}::{test.test_name}", ) ) ) - failures = "
".join(failure_tests) + failures = "
".join(failed_tests) footer = str(h("span", {"style": fail_shared_style}, "=" * divider_length)) error_output = widgets.Textarea(output, layout={"height": "300px", "width": "100%"}) - test_info = widgets.HTML( - "
".join([header, message, footer, num_success, failures, footer]) - ) + test_info = widgets.HTML("
".join([header, message, footer, failures, footer])) self.display(widgets.VBox(children=[test_info, error_output], layout={"width": "100%"})) @@ -3202,21 +3206,21 @@ def log_success(self, message: str) -> None: self._print(message) def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None: + message = f"Ran `{result.testsRun}` Tests Against `{target_dialect}`" + if result.wasSuccessful(): - self._print( - f"**Successfully Ran `{str(result.testsRun)}` Tests Against `{target_dialect}`**\n\n" - ) + self._print(f"**Successfully {message}**\n\n") else: self._print("```") - self._log_test_details(result) + self._log_test_details(result, unittest_char_separator=False) self._print("```\n\n") - self._print( - f"**Num Successful Tests: {result.testsRun - len(result.failures) - len(result.errors)}**\n\n" - ) + failures = len(result.failures) + len(result.errors) + self._print(f"**{message}**\n") + self._print(f"**Failed tests ({failures}):**") for test, _ in result.failures + result.errors: if isinstance(test, ModelTest): - self._print(f"* Failure Test: `{test.model.name}` - `{test.test_name}`\n\n") + self._print(f" • `{test.model.name}`::`{test.test_name}`\n\n") def log_skipped_models(self, snapshot_names: t.Set[str]) -> None: if snapshot_names: diff --git a/sqlmesh/core/test/runner.py b/sqlmesh/core/test/runner.py index c098a46d84..284558e1c8 100644 --- a/sqlmesh/core/test/runner.py +++ b/sqlmesh/core/test/runner.py @@ -174,6 +174,6 @@ def _run_single_test( end_time = time.perf_counter() - combined_results.duration = end_time - start_time + combined_results.duration = round(end_time - start_time, 2) return combined_results diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 7d65a818f1..d803f8bdc9 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -2281,7 +2281,7 @@ def test_test_output(tmp_path: Path) -> None: ) assert "Ran 2 tests" in output - assert "FAILED (failures=1)" in output + assert "Failed tests (1):" in output # Case 2: Ensure that the verbose log report is structured correctly with capture_output() as captured_output: @@ -2321,7 +2321,7 @@ def test_test_output(tmp_path: Path) -> None: output = captured_output.stdout assert "Ran 102 tests" in output - assert "FAILED (failures=51)" in output + assert "Failed tests (51):" in output # Case 4: Test that wide tables are split into even chunks for default verbosity rmtree(tmp_path / "tests") @@ -2426,11 +2426,7 @@ def test_test_output_with_invalid_model_name(tmp_path: Path) -> None: f"""Model '"invalid_model"' was not found at {wrong_test_file}""" in mock_logger.call_args[0][0] ) - assert ( - ".\n----------------------------------------------------------------------\n\nRan 1 test in" - in output.stdout - ) - assert "OK" in output.stdout + assert "Successfully Ran 1 test" in output.stdout def test_number_of_tests_found(tmp_path: Path) -> None: diff --git a/tests/integrations/github/cicd/test_github_commands.py b/tests/integrations/github/cicd/test_github_commands.py index 9a22a74974..f5098ac525 100644 --- a/tests/integrations/github/cicd/test_github_commands.py +++ b/tests/integrations/github/cicd/test_github_commands.py @@ -477,7 +477,7 @@ def test_run_all_test_failed( assert ( """sqlmesh.utils.errors.TestError: some error""" in test_checks_runs[2]["output"]["summary"] ) - assert """**Num Successful Tests: 0**""" in test_checks_runs[2]["output"]["summary"] + assert """Failed tests (1):""" in test_checks_runs[2]["output"]["summary"] assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping prod_plan_preview_checks_runs = controller._check_run_mapping[ diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index 544352e893..2d9a129a3f 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -1853,6 +1853,7 @@ def test_pr_delete_model( assert GithubCheckStatus(test_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(test_checks_runs[2]["conclusion"]).is_success assert test_checks_runs[2]["output"]["title"] == "Tests Passed" + print(test_checks_runs[2]["output"]["summary"]) assert ( test_checks_runs[2]["output"]["summary"].strip() == "**Successfully Ran `3` Tests Against `duckdb`**" From 9a732ae6cbdda19b6b482ce33758c1e2d039e793 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:42:55 +0200 Subject: [PATCH 0419/1056] fix(vscode): allow uv pip executable called through tcloud (#4766) --- vscode/extension/src/auth/auth.ts | 17 +++--- .../src/utilities/sqlmesh/sqlmesh.ts | 54 +++++++++++-------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/vscode/extension/src/auth/auth.ts b/vscode/extension/src/auth/auth.ts index 7ff8f9cafe..8d7908f06b 100644 --- a/vscode/extension/src/auth/auth.ts +++ b/vscode/extension/src/auth/auth.ts @@ -79,10 +79,11 @@ export class AuthenticationProviderTobikoCloud } const tcloudBinPath = tcloudBin.value const result = await execAsync( - tcloudBinPath, + tcloudBinPath.bin, ['auth', 'vscode', 'status'], { cwd: workspacePath.uri.fsPath, + env: tcloudBinPath.env, }, ) if (result.exitCode !== 0) { @@ -162,8 +163,9 @@ export class AuthenticationProviderTobikoCloud throw new Error('Failed to get tcloud bin') } const tcloudBinPath = tcloudBin.value - const result = await execAsync(tcloudBinPath, ['auth', 'logout'], { + const result = await execAsync(tcloudBinPath.bin, ['auth', 'logout'], { cwd: workspacePath.uri.fsPath, + env: tcloudBinPath.env, }) if (result.exitCode !== 0) { throw new Error('Failed to logout from tcloud') @@ -187,7 +189,7 @@ export class AuthenticationProviderTobikoCloud } const tcloudBinPath = tcloudBin.value const result = await execAsync( - tcloudBinPath, + tcloudBinPath.bin, ['auth', 'vscode', 'login-url'], { cwd: workspacePath.uri.fsPath, @@ -214,11 +216,12 @@ export class AuthenticationProviderTobikoCloud 1000 * 60 * 5, ) const backgroundServerForLogin = execAsync( - tcloudBinPath, + tcloudBinPath.bin, ['auth', 'vscode', 'start-server', urlCode.verifier_code], { cwd: workspacePath.uri.fsPath, signal: ac.signal, + env: tcloudBinPath.env, }, ) @@ -283,10 +286,11 @@ export class AuthenticationProviderTobikoCloud } const tcloudBinPath = tcloudBin.value const result = await execAsync( - tcloudBinPath, + tcloudBinPath.bin, ['auth', 'vscode', 'device'], { cwd: workspacePath.uri.fsPath, + env: tcloudBinPath.env, }, ) if (result.exitCode !== 0) { @@ -305,11 +309,12 @@ export class AuthenticationProviderTobikoCloud 1000 * 60 * 5, ) const waiting = execAsync( - tcloudBinPath, + tcloudBinPath.bin, ['auth', 'vscode', 'poll_device', deviceCodeResponse.device_code], { cwd: workspacePath.uri.fsPath, signal: ac.signal, + env: tcloudBinPath.env, }, ) diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index a07336be21..9285f20c7c 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -55,7 +55,7 @@ export const isTcloudProject = async (): Promise> => { * * @returns The tcloud executable for the current Python environment. */ -export const getTcloudBin = async (): Promise> => { +export const getTcloudBin = async (): Promise> => { const tcloud = IS_WINDOWS ? 'tcloud.exe' : 'tcloud' const interpreterDetails = await getInterpreterDetails() if (!interpreterDetails.path) { @@ -68,7 +68,25 @@ export const getTcloudBin = async (): Promise> => { if (!fs.existsSync(binPath)) { return err({type: 'tcloud_bin_not_found'}) } - return ok(binPath) + const envVariables = await getPythonEnvVariables() + if (isErr(envVariables)) { + return err({ + type: 'generic', + message: envVariables.error, + }) + } + return ok({ + bin: binPath, + workspacePath: interpreterDetails.resource?.fsPath ?? '', + env: { + ...process.env, + ...envVariables.value, + PYTHONPATH: interpreterDetails.path[0], + VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), + PATH: interpreterDetails.binPath!, + }, + args: [], + }) } const isSqlmeshInstalledSchema = z.object({ @@ -96,8 +114,9 @@ export const isSqlmeshEnterpriseInstalled = async (): Promise< message: resolvedPath.error, }) } - const called = await execAsync(tcloudBin.value, ['is_sqlmesh_installed'], { + const called = await execAsync(tcloudBin.value.bin, ['is_sqlmesh_installed'], { cwd: resolvedPath.value, + env: tcloudBin.value.env, }) if (called.exitCode !== 0) { return err({ @@ -135,9 +154,10 @@ export const installSqlmeshEnterprise = async ( message: resolvedPath.error, }) } - const called = await execAsync(tcloudBin.value, ['install_sqlmesh'], { + const called = await execAsync(tcloudBin.value.bin, ['install_sqlmesh'], { signal: abortController.signal, cwd: resolvedPath.value, + env: tcloudBin.value.env, }) if (called.exitCode !== 0) { return err({ @@ -272,16 +292,10 @@ export const sqlmeshExec = async (): Promise< return ensured } return ok({ - bin: `${tcloudBin.value} sqlmesh`, + bin: tcloudBin.value.bin, workspacePath, - env: { - ...process.env, - ...envVariables.value, - PYTHONPATH: interpreterDetails.path?.[0], - VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), - PATH: interpreterDetails.binPath!, - }, - args: [], + env: tcloudBin.value.env, + args: ["sqlmesh"], }) } const binPath = path.join(interpreterDetails.binPath!, sqlmesh) @@ -423,15 +437,9 @@ export const sqlmeshLspExec = async (): Promise< // TODO: Remove this once we have a stable version of tcloud that supports sqlmesh_lsp. if (isSemVerGreaterThanOrEqual(tcloudBinVersion.value, [2, 10, 1])) { return ok ({ - bin: tcloudBin.value, + bin: tcloudBin.value.bin, workspacePath, - env: { - PYTHONPATH: interpreterDetails.path?.[0], - VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), - PATH: interpreterDetails.binPath!, - ...process.env, - ...envVariables.value, - }, + env: tcloudBin.value.env, args: ['sqlmesh_lsp'], }) } @@ -503,7 +511,9 @@ async function getTcloudBinVersion(): Promise Date: Thu, 19 Jun 2025 09:30:13 -0700 Subject: [PATCH 0420/1056] Fix: Fetch full environment object one a time to prevent loading all of them at once during restatement (#4768) --- sqlmesh/core/plan/evaluator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index d1d9df8f28..d959fd27a4 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -400,7 +400,13 @@ def _restatement_intervals_across_all_environments( snapshots_to_restate: t.Dict[SnapshotId, t.Tuple[SnapshotTableInfo, Interval]] = {} - for env in self.state_sync.get_environments(): + for env_summary in self.state_sync.get_environments_summary(): + # Fetch the full environment object one at a time to avoid loading all environments into memory at once + env = self.state_sync.get_environment(env_summary.name) + if not env: + logger.warning("Environment %s not found", env_summary.name) + continue + keyed_snapshots = {s.name: s.table_info for s in env.snapshots} # We dont just restate matching snapshots, we also have to restate anything downstream of them From 977ead3cc4cb3181de3b7679a2df72ae2b0e31f4 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 19 Jun 2025 13:42:43 -0700 Subject: [PATCH 0421/1056] Fix: Warn into a console when the run is blocked by environment updates (#4769) --- sqlmesh/core/context.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 7d17b3b863..8d8648f918 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -733,11 +733,9 @@ def _block_until_finalized() -> str: raise SQLMeshError(f"Environment '{environment}' was not found.") if environment_state.finalized_ts: return environment_state.plan_id - logger.warning( - "Environment '%s' is being updated by plan '%s'. Retrying in %s seconds...", - environment, - environment_state.plan_id, - self.config.run.environment_check_interval, + self.console.log_warning( + f"Environment '{environment}' is being updated by plan '{environment_state.plan_id}'. " + f"Retrying in {self.config.run.environment_check_interval} seconds..." ) time.sleep(self.config.run.environment_check_interval) raise SQLMeshError( @@ -774,9 +772,8 @@ def _has_environment_changed() -> bool: ) done = True except CircuitBreakerError: - logger.warning( - "Environment '%s' modified while running. Restarting the run...", - environment, + self.console.log_warning( + f"Environment '{environment}' modified while running. Restarting the run..." ) if exit_on_env_update: interrupted = True From 17a70981100771405764b5210aa22e4d5e6814cc Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 19 Jun 2025 23:19:30 +0200 Subject: [PATCH 0422/1056] fix(vscode): include process path env variable in passed path (#4770) --- vscode/extension/src/utilities/sqlmesh/sqlmesh.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 9285f20c7c..06f05f7792 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -83,7 +83,7 @@ export const getTcloudBin = async (): Promise ...envVariables.value, PYTHONPATH: interpreterDetails.path[0], VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), - PATH: interpreterDetails.binPath!, + PATH: `${interpreterDetails.binPath!}${path.delimiter}${process.env.PATH || ''}`, }, args: [], }) @@ -308,7 +308,7 @@ export const sqlmeshExec = async (): Promise< ...envVariables.value, PYTHONPATH: interpreterDetails.path?.[0], VIRTUAL_ENV: path.dirname(path.dirname(interpreterDetails.binPath!)), // binPath now points to bin dir - PATH: interpreterDetails.binPath!, + PATH: `${interpreterDetails.binPath!}${path.delimiter}${process.env.PATH || ''}`, }, args: [], }) @@ -459,11 +459,11 @@ export const sqlmeshLspExec = async (): Promise< bin: binPath, workspacePath, env: { - PYTHONPATH: interpreterDetails.path?.[0], - VIRTUAL_ENV: path.dirname(path.dirname(interpreterDetails.binPath!)), // binPath now points to bin dir - PATH: interpreterDetails.binPath!, // binPath already points to the bin directory ...process.env, ...envVariables.value, + PYTHONPATH: interpreterDetails.path?.[0], + VIRTUAL_ENV: path.dirname(path.dirname(interpreterDetails.binPath!)), // binPath now points to bin dir + PATH: `${interpreterDetails.binPath!}${path.delimiter}${process.env.PATH || ''}`, // binPath already points to the bin directory }, args: [], }) From 05c793cba50db697077d6ad9d8f0643792e1d7d2 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 20 Jun 2025 10:12:11 +1200 Subject: [PATCH 0423/1056] Feat: Allow virtual environments to be given dedicated catalogs (#4742) --- docs/guides/configuration.md | 55 ++++++++- examples/sushi/config.py | 7 +- sqlmesh/core/config/common.py | 17 +++ sqlmesh/core/config/root.py | 9 ++ sqlmesh/core/constants.py | 1 + sqlmesh/core/engine_adapter/base.py | 17 +++ sqlmesh/core/engine_adapter/duckdb.py | 14 +++ sqlmesh/core/engine_adapter/shared.py | 7 ++ sqlmesh/core/engine_adapter/snowflake.py | 33 +++++ sqlmesh/core/environment.py | 4 + sqlmesh/core/snapshot/definition.py | 18 +-- sqlmesh/core/snapshot/evaluator.py | 14 +++ sqlmesh/core/state_sync/common.py | 28 ++++- tests/conftest.py | 15 ++- .../engine_adapter/integration/__init__.py | 8 ++ .../integration/test_integration_snowflake.py | 38 +++++- tests/core/engine_adapter/test_duckdb.py | 14 +++ tests/core/engine_adapter/test_snowflake.py | 19 +++ tests/core/test_config.py | 49 ++++++++ tests/core/test_integration.py | 116 +++++++++++++++++- tests/core/test_snapshot.py | 26 +++- 21 files changed, 489 insertions(+), 20 deletions(-) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 361171d937..901d35e6b2 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -244,7 +244,9 @@ This only applies to the _physical tables_ that SQLMesh creates - the views are SQLMesh stores `prod` environment views in the schema in a model's name - for example, the `prod` views for a model `my_schema.users` will be located in `my_schema`. -By default, for non-prod environments SQLMesh creates a new schema that appends the environment name to the model name's schema. For example, by default the view for a model `my_schema.users` in a SQLMesh environment named `dev` will be located in the schema `my_schema__dev`. +By default, for non-prod environments SQLMesh creates a new schema that appends the environment name to the model name's schema. For example, by default the view for a model `my_schema.users` in a SQLMesh environment named `dev` will be located in the schema `my_schema__dev` as `my_schema__dev.users`. + +##### Show at the table level instead This behavior can be changed to append a suffix at the end of a _table/view_ name instead. Appending the suffix to a table/view name means that non-prod environment views will be created in the same schema as the `prod` environment. The prod and non-prod views are differentiated by non-prod view names ending with `__`. @@ -260,7 +262,7 @@ Config example: === "Python" - The Python `environment_suffix_target` argument takes an `EnvironmentSuffixTarget` enumeration with a value of `EnvironmentSuffixTarget.TABLE` or `EnvironmentSuffixTarget.SCHEMA` (default). + The Python `environment_suffix_target` argument takes an `EnvironmentSuffixTarget` enumeration with a value of `EnvironmentSuffixTarget.TABLE`, `EnvironmentSuffixTarget.CATALOG` or `EnvironmentSuffixTarget.SCHEMA` (default). ```python linenums="1" from sqlmesh.core.config import Config, ModelDefaultsConfig, EnvironmentSuffixTarget @@ -271,16 +273,58 @@ Config example: ) ``` -The default behavior of appending the suffix to schemas is recommended because it leaves production with a single clean interface for accessing the views. However, if you are deploying SQLMesh in an environment with tight restrictions on schema creation then this can be a useful way of reducing the number of schemas SQLMesh uses. +!!! info "Default behavior" + The default behavior of appending the suffix to schemas is recommended because it leaves production with a single clean interface for accessing the views. However, if you are deploying SQLMesh in an environment with tight restrictions on schema creation then this can be a useful way of reducing the number of schemas SQLMesh uses. + +##### Show at the catalog level instead + +If neither the schema (default) nor the table level are sufficient for your use case, you can indicate the environment at the catalog level instead. + +This can be useful if you have downstream BI reporting tools and you would like to point them at a development environment to test something out without renaming all the table / schema references within the report query. + +In order to achieve this, you can configure [environment_suffix_target](../reference/configuration.md#environments) like so: + +=== "YAML" + + ```yaml linenums="1" + environment_suffix_target: catalog + ``` + +=== "Python" + + The Python `environment_suffix_target` argument takes an `EnvironmentSuffixTarget` enumeration with a value of `EnvironmentSuffixTarget.TABLE`, `EnvironmentSuffixTarget.CATALOG` or `EnvironmentSuffixTarget.SCHEMA` (default). + + ```python linenums="1" + from sqlmesh.core.config import Config, ModelDefaultsConfig, EnvironmentSuffixTarget + + config = Config( + model_defaults=ModelDefaultsConfig(dialect=), + environment_suffix_target=EnvironmentSuffixTarget.CATALOG, + ) + ``` + +Given the example of a model called `my_schema.users` with a default catalog of `warehouse` this will cause the following behavior: + +- For the `prod` environment, the default catalog as configured in the gateway will be used. So the view will be created at `warehouse.my_schema.users` +- For any other environment, eg `dev`, the environment name will be appended to the default catalog. So the view will be created at `warehouse__dev.my_schema.users` +- If a model is fully qualified with a catalog already, eg `finance_mart.my_schema.users`, then the environment catalog will be based off the model catalog and not the default catalog. In this example, the view will be created at `finance_mart__dev.my_schema.users` + + +!!! warning "Caveats" + - Using `environment_suffix_target: catalog` only works on engines that support querying across different catalogs. If your engine does not support cross-catalog queries then you will need to use `environment_suffix_target: schema` or `environment_suffix_target: table` instead. + - Automatic catalog creation is not supported on all engines even if they support cross-catalog queries. For engines where it is not supported, the catalogs must be managed externally from SQLMesh and exist prior to invoking SQLMesh. #### Environment view catalogs By default, SQLMesh creates an environment view in the same [catalog](../concepts/glossary.md#catalog) as the physical table the view points to. The physical table's catalog is determined by either the catalog specified in the model name or the default catalog defined in the connection. -Some companies fully segregate `prod` and non-prod environment objects by catalog. For example, they might have a "prod" catalog that contains all `prod` environment physical tables and views and a separate "dev" catalog that contains all `dev` environment physical tables and views. +It can be desirable to create `prod` and non-prod virtual layer objects in separate catalogs instead. For example, there might be a "prod" catalog that contains all `prod` environment views and a separate "dev" catalog that contains all `dev` environment views. Separate prod and non-prod catalogs can also be useful if you have a CI/CD pipeline that creates environments, like the [SQLMesh Github Actions CI/CD Bot](../integrations/github.md). You might want to store the CI/CD environment objects in a dedicated catalog since there can be many of them. +!!! info "Virtual layer only" + Note that the following setting only affects the [virtual layer](../concepts/glossary.md#virtual-layer). If you need full segregation by catalog between environments in the [physical layer](../concepts/glossary.md#physical-layer) as well, see the [Isolated Systems Guide](../guides/isolated_systems.md). + To configure separate catalogs, provide a mapping from [regex patterns](https://en.wikipedia.org/wiki/Regular_expression) to catalog names. SQLMesh will compare the name of an environment to the regex patterns; when it finds a match it will store the environment's objects in the corresponding catalog. SQLMesh evaluates the regex patterns in the order defined in the configuration; it uses the catalog for the first matching pattern. If no match is found, the catalog defined in the model or the default catalog defined on the connection will be used. @@ -317,6 +361,9 @@ With the example configuration above, SQLMesh would evaluate environment names a * If the environment name starts with `dev`, the catalog will be `dev`. * If the environment name starts with `analytics_repo`, the catalog will be `cicd`. +!!! warning + This feature is mutually exclusive with `environment_suffix_target: catalog` in order to prevent ambiguous mappings from being defined. Attempting to specify both `environment_catalog_mapping` and `environment_suffix_target: catalog` will raise an error on project load + *Note:* This feature is only available for engines that support querying across catalogs. At the time of writing, the following engines are **NOT** supported: * [MySQL](../integrations/engines/mysql.md) diff --git a/examples/sushi/config.py b/examples/sushi/config.py index c59675cf5b..bbe9ec7988 100644 --- a/examples/sushi/config.py +++ b/examples/sushi/config.py @@ -128,12 +128,17 @@ ) -environment_suffix_config = Config( +environment_suffix_table_config = Config( default_connection=DuckDBConnectionConfig(), model_defaults=model_defaults, environment_suffix_target=EnvironmentSuffixTarget.TABLE, ) +environment_suffix_catalog_config = environment_suffix_table_config.model_copy( + update={ + "environment_suffix_target": EnvironmentSuffixTarget.CATALOG, + } +) CATALOGS = { "in_memory": ":memory:", diff --git a/sqlmesh/core/config/common.py b/sqlmesh/core/config/common.py index 4efdb41647..d7be902713 100644 --- a/sqlmesh/core/config/common.py +++ b/sqlmesh/core/config/common.py @@ -10,9 +10,22 @@ class EnvironmentSuffixTarget(str, Enum): + # Intended to create virtual environments in their own schemas, with names like "__". The view name is untouched. + # For example, a model named 'sqlmesh_example.full_model' created in an environment called 'dev' + # would have its virtual layer view created as 'sqlmesh_example__dev.full_model' SCHEMA = "schema" + + # Intended to create virtual environments in the same schema as their production counterparts by adjusting the table name. + # For example, a model named 'sqlmesh_example.full_model' created in an environment called 'dev' + # would have its virtual layer view created as "sqlmesh_example.full_model__dev" TABLE = "table" + # Intended to create virtual environments in their own catalogs to preserve the schema and view name of the models + # For example, a model named 'sqlmesh_example.full_model' created in an environment called 'dev' + # with a default catalog of "warehouse" would have its virtual layer view created as "warehouse__dev.sqlmesh_example.full_model" + # note: this only works for engines that can query across catalogs + CATALOG = "catalog" + @property def is_schema(self) -> bool: return self == EnvironmentSuffixTarget.SCHEMA @@ -21,6 +34,10 @@ def is_schema(self) -> bool: def is_table(self) -> bool: return self == EnvironmentSuffixTarget.TABLE + @property + def is_catalog(self) -> bool: + return self == EnvironmentSuffixTarget.CATALOG + @classproperty def default(cls) -> EnvironmentSuffixTarget: return EnvironmentSuffixTarget.SCHEMA diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index 1d53235f73..a8b8a2a797 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -242,6 +242,15 @@ def _normalize_identifiers(key: str) -> None: }, ) + if ( + self.environment_suffix_target == EnvironmentSuffixTarget.CATALOG + and self.environment_catalog_mapping + ): + raise ConfigError( + f"'environment_suffix_target: catalog' is mutually exclusive with 'environment_catalog_mapping'.\n" + "Please specify one or the other" + ) + if self.environment_catalog_mapping: _normalize_identifiers("environment_catalog_mapping") if self.physical_schema_mapping: diff --git a/sqlmesh/core/constants.py b/sqlmesh/core/constants.py index 2ab592f368..27d6cf0d7f 100644 --- a/sqlmesh/core/constants.py +++ b/sqlmesh/core/constants.py @@ -7,6 +7,7 @@ from pathlib import Path SQLMESH = "sqlmesh" +SQLMESH_MANAGED = "sqlmesh_managed" SQLMESH_PATH = Path.home() / ".sqlmesh" PROD = "prod" diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 29ca98fc2b..a317008b1a 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -101,6 +101,7 @@ class EngineAdapter: SUPPORTS_VIEW_SCHEMA = True SUPPORTS_CLONING = False SUPPORTS_MANAGED_MODELS = False + SUPPORTS_CREATE_DROP_CATALOG = False SCHEMA_DIFFER = SchemaDiffer() SUPPORTS_TUPLE_IN = True HAS_VIEW_BINDING = False @@ -1217,6 +1218,22 @@ def drop_view( **kwargs, ) + def create_catalog(self, catalog_name: str | exp.Identifier) -> None: + return self._create_catalog(exp.parse_identifier(catalog_name, dialect=self.dialect)) + + def _create_catalog(self, catalog_name: exp.Identifier) -> None: + raise SQLMeshError( + f"Unable to create catalog '{catalog_name.sql(dialect=self.dialect)}' as automatic catalog management is not implemented in the {self.dialect} engine." + ) + + def drop_catalog(self, catalog_name: str | exp.Identifier) -> None: + return self._drop_catalog(exp.parse_identifier(catalog_name, dialect=self.dialect)) + + def _drop_catalog(self, catalog_name: exp.Identifier) -> None: + raise SQLMeshError( + f"Unable to drop catalog '{catalog_name.sql(dialect=self.dialect)}' as automatic catalog management is not implemented in the {self.dialect} engine." + ) + def columns( self, table_name: TableName, include_pseudo_columns: bool = False ) -> t.Dict[str, exp.DataType]: diff --git a/sqlmesh/core/engine_adapter/duckdb.py b/sqlmesh/core/engine_adapter/duckdb.py index f7f72f9692..169a7a7f94 100644 --- a/sqlmesh/core/engine_adapter/duckdb.py +++ b/sqlmesh/core/engine_adapter/duckdb.py @@ -2,6 +2,7 @@ import typing as t from sqlglot import exp +from pathlib import Path from sqlmesh.core.engine_adapter.mixins import ( GetCurrentCatalogFromFunctionMixin, @@ -35,6 +36,7 @@ class DuckDBEngineAdapter(LogicalMergeMixin, GetCurrentCatalogFromFunctionMixin, ) COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY + SUPPORTS_CREATE_DROP_CATALOG = True @property def catalog_support(self) -> CatalogSupport: @@ -44,6 +46,18 @@ def set_current_catalog(self, catalog: str) -> None: """Sets the catalog name of the current connection.""" self.execute(exp.Use(this=exp.to_identifier(catalog))) + def _create_catalog(self, catalog_name: exp.Identifier) -> None: + db_filename = f"{catalog_name.output_name}.db" + self.execute( + exp.Attach(this=exp.alias_(exp.Literal.string(db_filename), catalog_name), exists=True) + ) + + def _drop_catalog(self, catalog_name: exp.Identifier) -> None: + db_file_path = Path(f"{catalog_name.output_name}.db") + self.execute(exp.Detach(this=catalog_name, exists=True)) + if db_file_path.exists(): + db_file_path.unlink() + def _df_to_source_queries( self, df: DF, diff --git a/sqlmesh/core/engine_adapter/shared.py b/sqlmesh/core/engine_adapter/shared.py index e1d93b5e2f..1d882de02f 100644 --- a/sqlmesh/core/engine_adapter/shared.py +++ b/sqlmesh/core/engine_adapter/shared.py @@ -173,9 +173,16 @@ def is_clustered(self) -> bool: class CatalogSupport(Enum): + # The engine has no concept of catalogs UNSUPPORTED = 1 + + # The engine has a concept of catalogs, but they are isolated from each other and cannot reference each others tables SINGLE_CATALOG_ONLY = 2 + + # The engine supports multiple catalogs but some operations require a SET CATALOG query to set the active catalog before proceeding REQUIRES_SET_CATALOG = 3 + + # The engine supports multiple catalogs and can unambiguously target a specific catalog when performing operations (without running SET CATALOG first) FULL_SUPPORT = 4 @property diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index 032c1da4fe..71ffc10f48 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -9,6 +9,7 @@ from sqlglot.optimizer.normalize_identifiers import normalize_identifiers from sqlglot.optimizer.qualify_columns import quote_identifiers +import sqlmesh.core.constants as c from sqlmesh.core.dialect import to_schema from sqlmesh.core.engine_adapter.mixins import ( GetCurrentCatalogFromFunctionMixin, @@ -43,6 +44,7 @@ "_get_data_objects": CatalogSupport.REQUIRES_SET_CATALOG, "create_schema": CatalogSupport.REQUIRES_SET_CATALOG, "drop_schema": CatalogSupport.REQUIRES_SET_CATALOG, + "drop_catalog": CatalogSupport.REQUIRES_SET_CATALOG, # needs a catalog to issue a query to information_schema.databases even though the result is global } ) class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixin, RowDiffMixin): @@ -52,6 +54,7 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi SUPPORTS_CLONING = True SUPPORTS_MANAGED_MODELS = True CURRENT_CATALOG_EXPRESSION = exp.func("current_database") + SUPPORTS_CREATE_DROP_CATALOG = True SCHEMA_DIFFER = SchemaDiffer( parameterized_type_defaults={ exp.DataType.build("BINARY", dialect=DIALECT).this: [(8388608,)], @@ -123,6 +126,36 @@ def snowpark(self) -> t.Optional[SnowparkSession]: def catalog_support(self) -> CatalogSupport: return CatalogSupport.FULL_SUPPORT + def _create_catalog(self, catalog_name: exp.Identifier) -> None: + props = exp.Properties( + expressions=[exp.SchemaCommentProperty(this=exp.Literal.string(c.SQLMESH_MANAGED))] + ) + self.execute( + exp.Create( + this=exp.Table(this=catalog_name), kind="DATABASE", exists=True, properties=props + ) + ) + + def _drop_catalog(self, catalog_name: exp.Identifier) -> None: + # only drop the catalog if it was created by SQLMesh, which is indicated by its comment matching {c.SQLMESH_MANAGED} + exists_check = ( + exp.select(exp.Literal.number(1)) + .from_(exp.to_table("information_schema.databases")) + .where( + exp.and_( + exp.column("database_name").eq(exp.Literal.string(catalog_name)), + exp.column("comment").eq(exp.Literal.string(c.SQLMESH_MANAGED)), + ) + ) + ) + normalize_identifiers(exists_check, dialect=self.dialect) + if self.fetchone(exists_check, quote_identifiers=True) is not None: + self.execute(exp.Drop(this=exp.Table(this=catalog_name), kind="DATABASE", exists=True)) + else: + logger.warning( + f"Not dropping database {catalog_name.sql(dialect=self.dialect)} because there is no indication it is '{c.SQLMESH_MANAGED}'" + ) + def _create_table( self, table_name_or_schema: t.Union[exp.Schema, TableName], diff --git a/sqlmesh/core/environment.py b/sqlmesh/core/environment.py index 891d299df8..13ca1c5485 100644 --- a/sqlmesh/core/environment.py +++ b/sqlmesh/core/environment.py @@ -43,6 +43,10 @@ class EnvironmentNamingInfo(PydanticModel): normalize_name: bool = True gateway_managed: bool = False + @property + def is_dev(self) -> bool: + return self.name.lower() != c.PROD + @field_validator("name", mode="before") @classmethod def _sanitize_name(cls, v: str) -> str: diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index ba422bcdcb..573d3bc75d 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -274,13 +274,19 @@ def table_for_environment( def catalog_for_environment( self, environment_naming_info: EnvironmentNamingInfo, dialect: DialectType = None ) -> t.Optional[str]: - if environment_naming_info.catalog_name_override: + catalog_name: t.Optional[str] = None + if environment_naming_info.is_dev and environment_naming_info.suffix_target.is_catalog: + catalog_name = f"{self.catalog}__{environment_naming_info.name}" + elif environment_naming_info.catalog_name_override: catalog_name = environment_naming_info.catalog_name_override + + if catalog_name: return ( normalize_identifiers(catalog_name, dialect=dialect).name if environment_naming_info.normalize_name else catalog_name ) + return self.catalog def schema_for_environment( @@ -295,10 +301,7 @@ def schema_for_environment( if normalize: schema = normalize_identifiers(schema, dialect=dialect).name - if ( - environment_naming_info.name.lower() != c.PROD - and environment_naming_info.suffix_target.is_schema - ): + if environment_naming_info.is_dev and environment_naming_info.suffix_target.is_schema: env_name = environment_naming_info.name if normalize: env_name = normalize_identifiers(env_name, dialect=dialect).name @@ -311,10 +314,7 @@ def table_name_for_environment( self, environment_naming_info: EnvironmentNamingInfo, dialect: DialectType = None ) -> str: table = self.table - if ( - environment_naming_info.name.lower() != c.PROD - and environment_naming_info.suffix_target.is_table - ): + if environment_naming_info.is_dev and environment_naming_info.suffix_target.is_table: env_name = environment_naming_info.name if environment_naming_info.normalize_name: env_name = normalize_identifiers(env_name, dialect=dialect).name diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 39a8f5147c..f2042583d0 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -251,6 +251,8 @@ def promote( # A schema can be shared across multiple engines, so we need to group by gateway for gateway, tables in tables_by_gateway.items(): + if environment_naming_info.suffix_target.is_catalog: + self._create_catalogs(tables=tables, gateway=gateway) self._create_schemas(tables=tables, gateway=gateway) deployability_index = deployability_index or DeployabilityIndex.all_deployable() @@ -1114,6 +1116,18 @@ def _audit( blocking=blocking, ) + def _create_catalogs( + self, + tables: t.Iterable[t.Union[exp.Table, str]], + gateway: t.Optional[str] = None, + ) -> None: + # attempt to create catalogs for the virtual layer if possible + adapter = self.get_adapter(gateway) + if adapter.SUPPORTS_CREATE_DROP_CATALOG: + unique_catalogs = {t.catalog for t in [exp.to_table(maybe_t) for maybe_t in tables]} + for catalog_name in unique_catalogs: + adapter.create_catalog(catalog_name) + def _create_schemas( self, tables: t.Iterable[t.Union[exp.Table, str]], diff --git a/sqlmesh/core/state_sync/common.py b/sqlmesh/core/state_sync/common.py index d5e20e9e8e..12899da82e 100644 --- a/sqlmesh/core/state_sync/common.py +++ b/sqlmesh/core/state_sync/common.py @@ -30,7 +30,9 @@ def cleanup_expired_views( console: t.Optional[Console] = None, ) -> None: expired_schema_environments = [ - environment for environment in environments if environment.suffix_target.is_schema + environment + for environment in environments + if environment.suffix_target.is_schema or environment.suffix_target.is_catalog ] expired_table_environments = [ environment for environment in environments if environment.suffix_target.is_table @@ -42,8 +44,10 @@ def get_adapter(gateway_managed: bool, gateway: t.Optional[str] = None) -> Engin return engine_adapters.get(gateway, default_adapter) return default_adapter + catalogs_to_drop: t.Set[t.Tuple[EngineAdapter, str]] = set() + # Drop the schemas for the expired environments - for engine_adapter, expired_catalog, expired_schema in { + for engine_adapter, expired_catalog, expired_schema, suffix_target in { ( (engine_adapter := get_adapter(environment.gateway_managed, snapshot.model_gateway)), snapshot.qualified_view_name.catalog_for_environment( @@ -52,6 +56,7 @@ def get_adapter(gateway_managed: bool, gateway: t.Optional[str] = None) -> Engin snapshot.qualified_view_name.schema_for_environment( environment.naming_info, dialect=engine_adapter.dialect ), + environment.suffix_target, ) for environment in expired_schema_environments for snapshot in environment.snapshots @@ -64,6 +69,10 @@ def get_adapter(gateway_managed: bool, gateway: t.Optional[str] = None) -> Engin ignore_if_not_exists=True, cascade=True, ) + + if suffix_target.is_catalog and expired_catalog: + catalogs_to_drop.add((engine_adapter, expired_catalog)) + if console: console.update_cleanup_progress(schema.sql(dialect=engine_adapter.dialect)) except Exception as e: @@ -96,6 +105,21 @@ def get_adapter(gateway_managed: bool, gateway: t.Optional[str] = None) -> Engin else: raise SQLMeshError(message) from e + # Drop any catalogs that were associated with a snapshot where the engine adapter supports dropping catalogs + # catalogs_to_drop is only populated when environment_suffix_target is set to 'catalog' + for engine_adapter, catalog in catalogs_to_drop: + if engine_adapter.SUPPORTS_CREATE_DROP_CATALOG: + try: + engine_adapter.drop_catalog(catalog) + if console: + console.update_cleanup_progress(catalog) + except Exception as e: + message = f"Failed to drop the expired environment catalog '{catalog}': {e}" + if warn_on_delete_failure: + logger.warning(message) + else: + raise SQLMeshError(message) from e + def transactional() -> t.Callable[[t.Callable], t.Callable]: def decorator(func: t.Callable) -> t.Callable: diff --git a/tests/conftest.py b/tests/conftest.py index 492da9db58..574c802c0e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -105,12 +105,25 @@ def qualified_views(self) -> t.List[exp.Table]: @property def schemas(self) -> t.List[str]: + return self.schemas_in_catalog(self.engine_adapter.get_current_catalog() or "") + + def schemas_in_catalog(self, catalog_name: str) -> t.List[str]: return self._get_single_col( - f"SELECT schema_name FROM information_schema.schemata WHERE catalog_name = '{self.engine_adapter.get_current_catalog()}' and {self._system_schema_filter('schema_name')}", + f"SELECT schema_name FROM information_schema.schemata WHERE catalog_name = '{catalog_name}' and {self._system_schema_filter('schema_name')}", "schema_name", self.engine_adapter, ) + @property + def catalogs(self) -> t.Set[str]: + return set( + self._get_single_col( + f"SELECT database_name FROM duckdb_databases() WHERE internal=false", + "database_name", + self.engine_adapter, + ) + ) + def _system_schema_filter(self, col: str) -> str: return f"{col} not in ('information_schema', 'pg_catalog', 'main')" diff --git a/tests/core/engine_adapter/integration/__init__.py b/tests/core/engine_adapter/integration/__init__.py index 3aa8529ef6..7e35b832be 100644 --- a/tests/core/engine_adapter/integration/__init__.py +++ b/tests/core/engine_adapter/integration/__init__.py @@ -205,6 +205,9 @@ def __init__( self._schemas: t.List[ str ] = [] # keep track of any schemas returned from self.schema() / self.table() so we can drop them at the end + self._catalogs: t.List[ + str + ] = [] # keep track of any catalogs created via self.create_catalog() so we can drop them at the end @property def test_type(self) -> str: @@ -685,6 +688,8 @@ def create_catalog(self, catalog_name: str): except Exception: pass + self._catalogs.append(catalog_name) + def drop_catalog(self, catalog_name: str): if self.dialect == "bigquery": return # bigquery cannot create/drop catalogs @@ -707,6 +712,9 @@ def cleanup(self, ctx: t.Optional[Context] = None): schema_name=schema_name, ignore_if_not_exists=True, cascade=True ) + for catalog_name in set(self._catalogs): + self.drop_catalog(catalog_name) + self.engine_adapter.close() def upsert_sql_model(self, model_definition: str) -> t.Tuple[Context, SqlModel]: diff --git a/tests/core/engine_adapter/integration/test_integration_snowflake.py b/tests/core/engine_adapter/integration/test_integration_snowflake.py index 314a9e0f20..12e45f1f14 100644 --- a/tests/core/engine_adapter/integration/test_integration_snowflake.py +++ b/tests/core/engine_adapter/integration/test_integration_snowflake.py @@ -181,7 +181,7 @@ def _get_data_object(table: exp.Table) -> DataObject: assert not metadata.is_clustered -def test_create_iceberg_table(ctx: TestContext, engine_adapter: SnowflakeEngineAdapter) -> None: +def test_create_iceberg_table(ctx: TestContext) -> None: # Note: this test relies on a default Catalog and External Volume being configured in Snowflake # ref: https://docs.snowflake.com/en/user-guide/tables-iceberg-configure-catalog-integration#set-a-default-catalog-at-the-account-database-or-schema-level # ref: https://docs.snowflake.com/en/user-guide/tables-iceberg-configure-external-volume#set-a-default-external-volume-at-the-account-database-or-schema-level @@ -271,3 +271,39 @@ def execute(context: ExecutionContext, start: datetime, **kwargs) -> DataFrame: query = exp.select("*").from_(table) df = ctx.engine_adapter.fetchdf(query, quote_identifiers=True) assert len(df) == 10 + + +def test_create_drop_catalog(ctx: TestContext, engine_adapter: SnowflakeEngineAdapter): + non_sqlmesh_managed_catalog = ctx.add_test_suffix("external_catalog") + sqlmesh_managed_catalog = ctx.add_test_suffix("env_dev") + + initial_catalog = engine_adapter.get_current_catalog() + assert initial_catalog + + ctx.create_catalog( + non_sqlmesh_managed_catalog + ) # create via TestContext so the sqlmesh_managed comment doesnt get added + ctx._catalogs.append(sqlmesh_managed_catalog) # so it still gets cleaned up if the test fails + + engine_adapter.create_catalog( + sqlmesh_managed_catalog + ) # create via EngineAdapter so the sqlmesh_managed comment is added + + def fetch_database_names() -> t.Set[str]: + engine_adapter.set_current_catalog(initial_catalog) + return { + str(r[0]) + for r in engine_adapter.fetchall( + f"select database_name from information_schema.databases where database_name like '%{ctx.test_id}'" + ) + } + + assert fetch_database_names() == {non_sqlmesh_managed_catalog, sqlmesh_managed_catalog} + + engine_adapter.drop_catalog( + non_sqlmesh_managed_catalog + ) # no-op: catalog is not SQLMesh-managed + assert fetch_database_names() == {non_sqlmesh_managed_catalog, sqlmesh_managed_catalog} + + engine_adapter.drop_catalog(sqlmesh_managed_catalog) # works, catalog is SQLMesh-managed + assert fetch_database_names() == {non_sqlmesh_managed_catalog} diff --git a/tests/core/engine_adapter/test_duckdb.py b/tests/core/engine_adapter/test_duckdb.py index 93ef72e874..543b2e2f18 100644 --- a/tests/core/engine_adapter/test_duckdb.py +++ b/tests/core/engine_adapter/test_duckdb.py @@ -89,3 +89,17 @@ def test_temporary_table(make_mocked_engine_adapter: t.Callable, duck_conn): assert to_sql_calls(adapter) == [ 'CREATE TEMPORARY TABLE IF NOT EXISTS "test_table" ("a" INT, "b" INT)', ] + + +def test_create_catalog(make_mocked_engine_adapter: t.Callable) -> None: + adapter: DuckDBEngineAdapter = make_mocked_engine_adapter(DuckDBEngineAdapter) + adapter.create_catalog(exp.to_identifier("foo")) + + assert to_sql_calls(adapter) == ["ATTACH IF NOT EXISTS 'foo.db' AS \"foo\""] + + +def test_drop_catalog(make_mocked_engine_adapter: t.Callable) -> None: + adapter: DuckDBEngineAdapter = make_mocked_engine_adapter(DuckDBEngineAdapter) + adapter.drop_catalog(exp.to_identifier("foo")) + + assert to_sql_calls(adapter) == ['DETACH DATABASE IF EXISTS "foo"'] diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index f0a47b3393..4ca13ee8f9 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -814,3 +814,22 @@ def test_create_view_with_schema_and_grants( # materialized view - COPY GRANTS goes before the column list """CREATE OR REPLACE MATERIALIZED VIEW "target_materialized_view" COPY GRANTS ("ID", "NAME") COMMENT='materialized **view** from integration test' AS SELECT 1 AS "ID", 'foo' AS "NAME\"""", ] + + +def test_create_catalog(snowflake_mocked_engine_adapter: SnowflakeEngineAdapter) -> None: + adapter = snowflake_mocked_engine_adapter + adapter.create_catalog(exp.to_identifier("foo")) + + assert to_sql_calls(adapter) == [ + "CREATE DATABASE IF NOT EXISTS \"foo\" COMMENT='sqlmesh_managed'" + ] + + +def test_drop_catalog(snowflake_mocked_engine_adapter: SnowflakeEngineAdapter) -> None: + adapter = snowflake_mocked_engine_adapter + adapter.drop_catalog(exp.to_identifier("foo")) + + assert to_sql_calls(adapter) == [ + """SELECT 1 FROM "INFORMATION_SCHEMA"."DATABASES" WHERE "DATABASE_NAME" = 'foo' AND "COMMENT" = 'sqlmesh_managed'""", + 'DROP DATABASE IF EXISTS "foo"', + ] diff --git a/tests/core/test_config.py b/tests/core/test_config.py index dea9fb16da..e3b7a8e612 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -16,6 +16,7 @@ BigQueryConnectionConfig, MotherDuckConnectionConfig, BuiltInSchedulerConfig, + EnvironmentSuffixTarget, ) from sqlmesh.core.config.connection import DuckDBAttachOptions, RedshiftConnectionConfig from sqlmesh.core.config.feature_flag import DbtFeatureFlag, FeatureFlag @@ -1055,3 +1056,51 @@ def test_loader_for_migrated_dbt_project(tmp_path: Path): ) assert config.loader == MigratedDbtProjectLoader + + +def test_environment_suffix_target_catalog(tmp_path: Path) -> None: + config_path = tmp_path / "config.yaml" + config_path.write_text(""" + gateways: + warehouse: + connection: + type: duckdb + + default_gateway: warehouse + + model_defaults: + dialect: duckdb + + environment_suffix_target: catalog +""") + + config = load_config_from_paths( + Config, + project_paths=[config_path], + ) + + assert config.environment_suffix_target == EnvironmentSuffixTarget.CATALOG + assert not config.environment_catalog_mapping + + config_path.write_text(""" + gateways: + warehouse: + connection: + type: duckdb + + default_gateway: warehouse + + model_defaults: + dialect: duckdb + + environment_suffix_target: catalog + + environment_catalog_mapping: + '.*': "foo" +""") + + with pytest.raises(ConfigError, match=r"mutually exclusive"): + config = load_config_from_paths( + Config, + project_paths=[config_path], + ) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 8725318506..f6e696ab01 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -11,6 +11,7 @@ import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 import pytest +from pytest import MonkeyPatch from pathlib import Path from sqlmesh.core.console import set_console, get_console, TerminalConsole from sqlmesh.core.config.naming import NameInferenceConfig @@ -34,6 +35,7 @@ ModelDefaultsConfig, DuckDBConnectionConfig, ) +from sqlmesh.core.config.common import EnvironmentSuffixTarget from sqlmesh.core.console import Console, get_console from sqlmesh.core.context import Context from sqlmesh.core.config.categorizer import CategorizerConfig @@ -5259,7 +5261,9 @@ def test_invalidating_environment(sushi_context: Context): def test_environment_suffix_target_table(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi", config="environment_suffix_config") + context, plan = init_and_plan_context( + "examples/sushi", config="environment_suffix_table_config" + ) context.apply(plan) metadata = DuckDBMetadata.from_context(context) environments_schemas = {"sushi"} @@ -5295,6 +5299,116 @@ def test_environment_suffix_target_table(init_and_plan_context: t.Callable): } == set() +def test_environment_suffix_target_catalog(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + monkeypatch.chdir(tmp_path) + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(catalogs={"main_warehouse": ":memory:"}), + environment_suffix_target=EnvironmentSuffixTarget.CATALOG, + ) + + assert config.default_connection + + models_dir = tmp_path / "models" + models_dir.mkdir() + + (models_dir / "model.sql").write_text(""" + MODEL ( + name example_schema.test_model, + kind FULL + ); + + SELECT '1' as a""") + + (models_dir / "fqn_model.sql").write_text(""" + MODEL ( + name memory.example_fqn_schema.test_model_fqn, + kind FULL + ); + + SELECT '1' as a""") + + ctx = Context(config=config, paths=tmp_path) + + metadata = DuckDBMetadata.from_context(ctx) + assert ctx.default_catalog == "main_warehouse" + assert metadata.catalogs == {"main_warehouse", "memory"} + + ctx.plan(auto_apply=True) + + # prod should go to the default catalog and not be overridden to a catalog called 'prod' + assert ( + ctx.engine_adapter.fetchone("select * from main_warehouse.example_schema.test_model")[0] # type: ignore + == "1" + ) + assert ( + ctx.engine_adapter.fetchone("select * from memory.example_fqn_schema.test_model_fqn")[0] # type: ignore + == "1" + ) + assert metadata.catalogs == {"main_warehouse", "memory"} + assert metadata.schemas_in_catalog("main_warehouse") == [ + "example_schema", + "sqlmesh__example_schema", + ] + assert metadata.schemas_in_catalog("memory") == [ + "example_fqn_schema", + "sqlmesh__example_fqn_schema", + ] + + # dev should be overridden to go to a catalogs called 'main_warehouse__dev' and 'memory__dev' + ctx.plan(environment="dev", include_unmodified=True, auto_apply=True) + assert ( + ctx.engine_adapter.fetchone("select * from main_warehouse__dev.example_schema.test_model")[ + 0 + ] # type: ignore + == "1" + ) + assert ( + ctx.engine_adapter.fetchone("select * from memory__dev.example_fqn_schema.test_model_fqn")[ + 0 + ] # type: ignore + == "1" + ) + assert metadata.catalogs == {"main_warehouse", "main_warehouse__dev", "memory", "memory__dev"} + + # schemas in dev envs should match prod and not have a suffix + assert metadata.schemas_in_catalog("main_warehouse") == [ + "example_schema", + "sqlmesh__example_schema", + ] + assert metadata.schemas_in_catalog("main_warehouse__dev") == ["example_schema"] + assert metadata.schemas_in_catalog("memory") == [ + "example_fqn_schema", + "sqlmesh__example_fqn_schema", + ] + assert metadata.schemas_in_catalog("memory__dev") == ["example_fqn_schema"] + + ctx.invalidate_environment("dev", sync=True) + + # dev catalogs cleaned up + assert metadata.catalogs == {"main_warehouse", "memory"} + + # prod catalogs still contain physical layer and views still work + assert metadata.schemas_in_catalog("main_warehouse") == [ + "example_schema", + "sqlmesh__example_schema", + ] + assert metadata.schemas_in_catalog("memory") == [ + "example_fqn_schema", + "sqlmesh__example_fqn_schema", + ] + + assert ( + ctx.engine_adapter.fetchone("select * from main_warehouse.example_schema.test_model")[0] # type: ignore + == "1" + ) + assert ( + ctx.engine_adapter.fetchone("select * from memory.example_fqn_schema.test_model_fqn")[0] # type: ignore + == "1" + ) + + def test_environment_catalog_mapping(init_and_plan_context: t.Callable): environments_schemas = {"raw", "sushi"} diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index 387c799e95..e4eb12c522 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -1780,7 +1780,31 @@ def test_is_valid_start(make_snapshot): EnvironmentNamingInfo(name="dev", catalog_name_override="g-h"), '"g-h".default__dev."e-f"', ), - (QualifiedViewName(table="e-f"), EnvironmentNamingInfo(name="dev"), 'default__dev."e-f"'), + ( + QualifiedViewName(table="e-f"), + EnvironmentNamingInfo(name="dev"), + 'default__dev."e-f"', + ), + # EnvironmentSuffixTarget.CATALOG + ( + QualifiedViewName( + catalog="default-foo", schema_name="sqlmesh_example", table="full_model" + ), + EnvironmentNamingInfo( + name="dev", + suffix_target=EnvironmentSuffixTarget.CATALOG, + ), + '"default-foo__dev".sqlmesh_example.full_model', + ), + ( + QualifiedViewName(catalog="default", schema_name="sqlmesh_example", table="full_model"), + EnvironmentNamingInfo( + name=c.PROD, + catalog_name_override=None, + suffix_target=EnvironmentSuffixTarget.CATALOG, + ), + "default.sqlmesh_example.full_model", + ), ), ) def test_qualified_view_name(qualified_view_name, environment_naming_info, expected): From a830e4cfb80c2884b8a680b4b056e173d1ef5e46 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:32:47 +0300 Subject: [PATCH 0424/1056] Fix(lsp): Extend support for table references with columns (#4763) --- examples/sushi/models/customers.sql | 3 +- sqlmesh/lsp/reference.py | 134 +++++++++++ tests/lsp/test_reference_cte_find_all.py | 9 +- .../lsp/test_reference_model_column_prefix.py | 212 ++++++++++++++++++ tests/lsp/test_reference_model_find_all.py | 70 +++--- tests/test_forking.py | 4 + tests/web/test_lineage.py | 8 +- 7 files changed, 406 insertions(+), 34 deletions(-) create mode 100644 tests/lsp/test_reference_model_column_prefix.py diff --git a/examples/sushi/models/customers.sql b/examples/sushi/models/customers.sql index 24b3aaa208..f91f1166e8 100644 --- a/examples/sushi/models/customers.sql +++ b/examples/sushi/models/customers.sql @@ -37,8 +37,9 @@ LEFT JOIN ( @ADD_ONE(1) AS another_column, FROM current_marketing_outer ) - SELECT * FROM current_marketing + SELECT current_marketing.* FROM current_marketing WHERE current_marketing.customer_id != 100 ) AS m ON o.customer_id = m.customer_id LEFT JOIN raw.demographics AS d ON o.customer_id = d.customer_id +WHERE sushi.orders.customer_id > 0 \ No newline at end of file diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index ac4d5374b6..96db4dc63d 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -208,6 +208,17 @@ def get_model_definitions_for_a_path( target_range=target_range, ) ) + + column_references = _process_column_references( + scope=scope, + reference_name=table.name, + read_file=read_file, + referenced_model_uri=document_uri, + description="", + reference_type="cte", + cte_target_range=target_range, + ) + references.extend(column_references) continue # For non-CTE tables, process as before (external model references) @@ -276,6 +287,19 @@ def get_model_definitions_for_a_path( target_range=yaml_target_range, ) ) + + column_references = _process_column_references( + scope=scope, + reference_name=normalized_reference_name, + read_file=read_file, + referenced_model_uri=referenced_model_uri, + description=description, + yaml_target_range=yaml_target_range, + reference_type="external_model", + default_catalog=lint_context.context.default_catalog, + dialect=dialect, + ) + references.extend(column_references) else: references.append( LSPModelReference( @@ -288,6 +312,18 @@ def get_model_definitions_for_a_path( ) ) + column_references = _process_column_references( + scope=scope, + reference_name=normalized_reference_name, + read_file=read_file, + referenced_model_uri=referenced_model_uri, + description=description, + reference_type="model", + default_catalog=lint_context.context.default_catalog, + dialect=dialect, + ) + references.extend(column_references) + return references @@ -735,6 +771,104 @@ def _position_within_range(position: Position, range: Range) -> bool: ) +def _get_column_table_range(column: exp.Column, read_file: t.List[str]) -> Range: + """ + Get the range for a column's table reference, handling both simple and qualified table names. + + Args: + column: The column expression + read_file: The file content as list of lines + + Returns: + The Range covering the table reference in the column + """ + + table_parts = column.parts[:-1] + + start_range = TokenPositionDetails.from_meta(table_parts[0].meta).to_range(read_file) + end_range = TokenPositionDetails.from_meta(table_parts[-1].meta).to_range(read_file) + + return Range( + start=to_lsp_position(start_range.start), + end=to_lsp_position(end_range.end), + ) + + +def _process_column_references( + scope: t.Any, + reference_name: str, + read_file: t.List[str], + referenced_model_uri: URI, + description: t.Optional[str] = None, + yaml_target_range: t.Optional[Range] = None, + reference_type: t.Literal["model", "external_model", "cte"] = "model", + default_catalog: t.Optional[str] = None, + dialect: t.Optional[str] = None, + cte_target_range: t.Optional[Range] = None, +) -> t.List[Reference]: + """ + Process column references for a given table and create appropriate reference objects. + + Args: + scope: The SQL scope to search for columns + reference_name: The full reference name (may include database/catalog) + read_file: The file content as list of lines + referenced_model_uri: URI of the referenced model + description: Markdown description for the reference + yaml_target_range: Target range for external models (YAML files) + reference_type: Type of reference - "model", "external_model", or "cte" + default_catalog: Default catalog for normalization + dialect: SQL dialect for normalization + cte_target_range: Target range for CTE references + + Returns: + List of table references for column usages + """ + + references: t.List[Reference] = [] + for column in scope.find_all(exp.Column): + if column.table: + if reference_type == "cte": + if column.table == reference_name: + table_range = _get_column_table_range(column, read_file) + references.append( + LSPCteReference( + uri=referenced_model_uri.value, + range=table_range, + target_range=cte_target_range, + ) + ) + else: + table_parts = [part.sql(dialect) for part in column.parts[:-1]] + table_ref = ".".join(table_parts) + normalized_reference_name = normalize_model_name( + table_ref, + default_catalog=default_catalog, + dialect=dialect, + ) + if normalized_reference_name == reference_name: + table_range = _get_column_table_range(column, read_file) + if reference_type == "external_model": + references.append( + LSPExternalModelReference( + uri=referenced_model_uri.value, + range=table_range, + markdown_description=description, + target_range=yaml_target_range, + ) + ) + else: + references.append( + LSPModelReference( + uri=referenced_model_uri.value, + range=table_range, + markdown_description=description, + ) + ) + + return references + + def _get_yaml_model_range(path: Path, model_name: str) -> t.Optional[Range]: """ Find the range of a specific model block in a YAML file. diff --git a/tests/lsp/test_reference_cte_find_all.py b/tests/lsp/test_reference_cte_find_all.py index d57c996a6a..6a29224e75 100644 --- a/tests/lsp/test_reference_cte_find_all.py +++ b/tests/lsp/test_reference_cte_find_all.py @@ -21,14 +21,13 @@ def test_cte_find_all_references(): # Test finding all references of "current_marketing" ranges = find_ranges_from_regex(read_file, r"current_marketing(?!_outer)") - assert len(ranges) == 2 + assert len(ranges) == 2 # regex finds 2 occurrences (definition and FROM clause) # Click on the CTE definition position = Position(line=ranges[0].start.line, character=ranges[0].start.character + 4) references = get_cte_references(lsp_context, URI.from_path(sushi_customers_path), position) - - # Should find both the definition and the usage - assert len(references) == 2 + # Should find the definition, FROM clause, and column prefix usages + assert len(references) == 4 # definition + FROM + 2 column prefix uses assert all(ref.uri == URI.from_path(sushi_customers_path).value for ref in references) reference_ranges = [ref.range for ref in references] @@ -46,7 +45,7 @@ def test_cte_find_all_references(): references = get_cte_references(lsp_context, URI.from_path(sushi_customers_path), position) # Should find the same references - assert len(references) == 2 + assert len(references) == 4 # definition + FROM + 2 column prefix uses assert all(ref.uri == URI.from_path(sushi_customers_path).value for ref in references) reference_ranges = [ref.range for ref in references] diff --git a/tests/lsp/test_reference_model_column_prefix.py b/tests/lsp/test_reference_model_column_prefix.py new file mode 100644 index 0000000000..88be689810 --- /dev/null +++ b/tests/lsp/test_reference_model_column_prefix.py @@ -0,0 +1,212 @@ +from pathlib import Path + +from lsprotocol.types import Position +from sqlmesh.cli.example_project import init_example_project +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.reference import get_all_references +from sqlmesh.lsp.uri import URI +from tests.lsp.test_reference_cte import find_ranges_from_regex + + +def test_model_reference_with_column_prefix(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Test finding references for "sushi.orders" + ranges = find_ranges_from_regex(read_file, r"sushi\.orders") + + # Click on the table reference in FROM clause (should be the second occurrence) + from_clause_range = None + for r in ranges: + line_content = read_file[r.start.line].strip() + if "FROM" in line_content: + from_clause_range = r + break + + assert from_clause_range is not None, "Should find FROM clause with sushi.orders" + + position = Position( + line=from_clause_range.start.line, character=from_clause_range.start.character + 6 + ) + + model_refs = get_all_references(lsp_context, URI.from_path(sushi_customers_path), position) + + assert len(model_refs) >= 7 + + # Verify that we have the FROM clause reference + assert any(ref.range.start.line == from_clause_range.start.line for ref in model_refs), ( + "Should find FROM clause reference" + ) + + +def test_column_prefix_references_are_found(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Find all occurrences of sushi.orders in the file + ranges = find_ranges_from_regex(read_file, r"sushi\.orders") + + # Should find exactly 2: FROM clause and WHERE clause with column prefix + assert len(ranges) == 2, f"Expected 2 occurrences of 'sushi.orders', found {len(ranges)}" + + # Verify we have the expected lines + line_contents = [read_file[r.start.line].strip() for r in ranges] + + # Should find FROM clause + assert any("FROM sushi.orders" in content for content in line_contents), ( + "Should find FROM clause with sushi.orders" + ) + + # Should find customer_id in WHERE clause with column prefix + assert any("WHERE sushi.orders.customer_id" in content for content in line_contents), ( + "Should find WHERE clause with sushi.orders.customer_id" + ) + + +def test_quoted_uppercase_table_and_column_references(tmp_path: Path): + # Initialize example project in temporary directory with case sensitive normalization + init_example_project(tmp_path, dialect="duckdb,normalization_strategy=case_sensitive") + + # Create a model with quoted uppercase schema and table names + models_dir = tmp_path / "models" + + # First, create the uppercase SUSHI.orders model that will be referenced + uppercase_orders_path = models_dir / "uppercase_orders.sql" + uppercase_orders_path.write_text("""MODEL ( + name "SUSHI".orders, + kind FULL +); + +SELECT + 1 as id, + 1 as customer_id, + 1 as item_id""") + + # Second, create the lowercase sushi.orders model that will be referenced + lowercase_orders_path = models_dir / "lowercase_orders.sql" + lowercase_orders_path.write_text("""MODEL ( + name sushi.orders, + kind FULL +); + +SELECT + 1 as id, + 1 as customer_id""") + + quoted_test_path = models_dir / "quoted_test.sql" + quoted_test_path.write_text("""MODEL ( + name "SUSHI".quoted_test, + kind FULL +); + +SELECT + o.id, + o.customer_id, + o.item_id, + c.item_id as c_item_id +FROM "SUSHI".orders AS o, sushi.orders as c +WHERE "SUSHI".orders.id > 0 + AND "SUSHI".orders.customer_id IS NOT NULL + AND sushi.orders.id > 0""") + + context = Context(paths=tmp_path) + lsp_context = LSPContext(context) + + # Find the quoted test model + quoted_test_model_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and '"SUSHI".quoted_test' in info.names + ) + + with open(quoted_test_model_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Test finding references for quoted "SUSHI".orders + ranges = find_ranges_from_regex(read_file, r'"SUSHI"\.orders') + + # Should find 3 occurrences: FROM clause and 2 in WHERE clause with column prefix + assert len(ranges) == 3, f"Expected 3 occurrences of '\"SUSHI\".orders', found {len(ranges)}" + + # Click on the table reference in FROM clause + from_clause_range = None + for r in ranges: + line_content = read_file[r.start.line].strip() + if "FROM" in line_content: + from_clause_range = r + break + + assert from_clause_range is not None, 'Should find FROM clause with "SUSHI".orders' + + position = Position( + line=from_clause_range.start.line, character=from_clause_range.start.character + 5 + ) + + model_refs = get_all_references(lsp_context, URI.from_path(quoted_test_model_path), position) + + # Should find only references to "SUSHI".orders (3 total: FROM clause and 2 column prefixes in WHERE) + # The lowercase sushi.orders should NOT be included if case sensitivity is working + assert len(model_refs) == 4, ( + f'Expected exactly 3 references for "SUSHI".orders, found {len(model_refs)}' + ) + + # Verify that we have all 3 references + ref_lines = [ref.range.start.line for ref in model_refs] + + # Count how many references are on each line + from_line = from_clause_range.start.line + where_lines = [r.start.line for r in ranges if r.start.line != from_line] + + assert from_line in ref_lines, "Should find FROM clause reference" + for where_line in where_lines: + assert where_line in ref_lines, f"Should find WHERE clause reference on line {where_line}" + + # Now test that lowercase sushi.orders references are separate + lowercase_ranges = find_ranges_from_regex(read_file, r"sushi\.orders") + + # Should find 2 occurrences: FROM clause and 1 in WHERE clause + assert len(lowercase_ranges) == 2, ( + f"Expected 2 occurrences of 'sushi.orders', found {len(lowercase_ranges)}" + ) + + # Click on the lowercase table reference + lowercase_from_range = None + for r in lowercase_ranges: + line_content = read_file[r.start.line].strip() + if "FROM" in line_content: + lowercase_from_range = r + break + + assert lowercase_from_range is not None, "Should find FROM clause with sushi.orders" + + lowercase_position = Position( + line=lowercase_from_range.start.line, character=lowercase_from_range.start.character + 5 + ) + + lowercase_refs = get_all_references( + lsp_context, URI.from_path(quoted_test_model_path), lowercase_position + ) + + # Should find only references to lowercase sushi.orders, NOT the uppercase ones + assert len(lowercase_refs) == 3, ( + f"Expected exactly 2 references for sushi.orders, found {len(lowercase_refs)}" + ) diff --git a/tests/lsp/test_reference_model_find_all.py b/tests/lsp/test_reference_model_find_all.py index c494ef7af3..7bb998150f 100644 --- a/tests/lsp/test_reference_model_find_all.py +++ b/tests/lsp/test_reference_model_find_all.py @@ -30,8 +30,8 @@ def test_find_references_for_model_usages(): # Click on the model reference position = Position(line=ranges[0].start.line, character=ranges[0].start.character + 6) references = get_model_find_all_references(lsp_context, URI.from_path(customers_path), position) - assert len(references) >= 6, ( - f"Expected at least 6 references to sushi.orders, found {len(references)}" + assert len(references) >= 7, ( + f"Expected at least 7 references to sushi.orders (including column prefix), found {len(references)}" ) # Verify expected files are present @@ -50,15 +50,18 @@ def test_find_references_for_model_usages(): ) # Verify exact ranges for each reference pattern + # Note: customers file has multiple references due to column prefix support expected_ranges = { - "orders": (0, 0, 0, 0), # the start for the model itself - "customers": (30, 7, 30, 19), - "waiter_revenue_by_day": (19, 5, 19, 17), - "customer_revenue_lifetime": (38, 7, 38, 19), - "customer_revenue_by_day": (33, 5, 33, 17), - "latest_order": (12, 5, 12, 17), + "orders": [(0, 0, 0, 0)], # the start for the model itself + "customers": [(30, 7, 30, 19), (44, 6, 44, 18)], # FROM clause and WHERE clause + "waiter_revenue_by_day": [(19, 5, 19, 17)], + "customer_revenue_lifetime": [(38, 7, 38, 19)], + "customer_revenue_by_day": [(33, 5, 33, 17)], + "latest_order": [(12, 5, 12, 17)], } + # Group references by file pattern + refs_by_pattern = {} for ref in references: matched_pattern = None for pattern in expected_patterns: @@ -66,28 +69,43 @@ def test_find_references_for_model_usages(): matched_pattern = pattern break - assert matched_pattern is not None, ( - f"Reference URI {ref.uri} doesn't match any expected pattern" - ) + if matched_pattern: + if matched_pattern not in refs_by_pattern: + refs_by_pattern[matched_pattern] = [] + refs_by_pattern[matched_pattern].append(ref) - # Get expected range for this model - expected_start_line, expected_start_char, expected_end_line, expected_end_char = ( - expected_ranges[matched_pattern] - ) + # Verify each pattern has the expected references + for pattern, expected_range_list in expected_ranges.items(): + assert pattern in refs_by_pattern, f"Missing references for pattern '{pattern}'" - # Assert exact range match - assert ref.range.start.line == expected_start_line, ( - f"Expected {matched_pattern} reference start line {expected_start_line}, found {ref.range.start.line}" - ) - assert ref.range.start.character == expected_start_char, ( - f"Expected {matched_pattern} reference start character {expected_start_char}, found {ref.range.start.character}" + actual_refs = refs_by_pattern[pattern] + assert len(actual_refs) == len(expected_range_list), ( + f"Expected {len(expected_range_list)} references for {pattern}, found {len(actual_refs)}" ) - assert ref.range.end.line == expected_end_line, ( - f"Expected {matched_pattern} reference end line {expected_end_line}, found {ref.range.end.line}" - ) - assert ref.range.end.character == expected_end_char, ( - f"Expected {matched_pattern} reference end character {expected_end_char}, found {ref.range.end.character}" + + # Sort both actual and expected by line number for consistent comparison + actual_refs_sorted = sorted( + actual_refs, key=lambda r: (r.range.start.line, r.range.start.character) ) + expected_sorted = sorted(expected_range_list, key=lambda r: (r[0], r[1])) + + for i, (ref, expected_range) in enumerate(zip(actual_refs_sorted, expected_sorted)): + expected_start_line, expected_start_char, expected_end_line, expected_end_char = ( + expected_range + ) + + assert ref.range.start.line == expected_start_line, ( + f"Expected {pattern} reference #{i + 1} start line {expected_start_line}, found {ref.range.start.line}" + ) + assert ref.range.start.character == expected_start_char, ( + f"Expected {pattern} reference #{i + 1} start character {expected_start_char}, found {ref.range.start.character}" + ) + assert ref.range.end.line == expected_end_line, ( + f"Expected {pattern} reference #{i + 1} end line {expected_end_line}, found {ref.range.end.line}" + ) + assert ref.range.end.character == expected_end_char, ( + f"Expected {pattern} reference #{i + 1} end character {expected_end_char}, found {ref.range.end.character}" + ) def test_find_references_for_marketing_model(): diff --git a/tests/test_forking.py b/tests/test_forking.py index 1cd50d9dec..d11379a158 100644 --- a/tests/test_forking.py +++ b/tests/test_forking.py @@ -55,10 +55,14 @@ def test_parallel_load(assert_exp_eq, mocker): "current_marketing"."status" AS "status", "current_marketing"."another_column" AS "another_column" FROM "current_marketing" AS "current_marketing" + WHERE + "current_marketing"."customer_id" <> 100 ) AS "m" ON "m"."customer_id" = "o"."customer_id" LEFT JOIN "memory"."raw"."demographics" AS "d" ON "d"."customer_id" = "o"."customer_id" + WHERE + "o"."customer_id" > 0 """, ) diff --git a/tests/web/test_lineage.py b/tests/web/test_lineage.py index 1ed40431ef..0cffd3ecc3 100644 --- a/tests/web/test_lineage.py +++ b/tests/web/test_lineage.py @@ -47,7 +47,7 @@ def test_get_lineage(client: TestClient, web_sushi_context: Context) -> None: "customer_id": { "expression": 'CAST("o"."customer_id" AS INT) AS "customer_id" /* this comment should not be registered */', "models": {'"memory"."sushi"."orders"': ["customer_id"]}, - "source": '''WITH "current_marketing_outer" AS ( + "source": """WITH "current_marketing_outer" AS ( SELECT "marketing"."customer_id" AS "customer_id", "marketing"."status" AS "status" @@ -71,10 +71,14 @@ def test_get_lineage(client: TestClient, web_sushi_context: Context) -> None: "current_marketing"."status" AS "status", "current_marketing"."another_column" AS "another_column" FROM "current_marketing" AS "current_marketing" + WHERE + "current_marketing"."customer_id" <> 100 ) AS "m" ON "m"."customer_id" = "o"."customer_id" LEFT JOIN "memory"."raw"."demographics" AS "d" - ON "d"."customer_id" = "o"."customer_id"''', + ON "d"."customer_id" = "o"."customer_id" +WHERE + "o"."customer_id" > 0""", } }, '"memory"."sushi"."orders"': { From 347aa7cebab83286cb945247062f5b3efb6434df Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:34:26 +0200 Subject: [PATCH 0425/1056] fix(vscode): on sign in, restart lsp (#4772) --- vscode/extension/src/commands/format.ts | 8 +- vscode/extension/src/commands/signin.ts | 21 +- .../src/commands/signinSpecifyFlow.ts | 26 +- vscode/extension/src/extension.ts | 48 +- vscode/extension/src/utilities/errors.ts | 6 +- vscode/extension/tests/tcloud.spec.ts | 669 ++++++++++-------- 6 files changed, 467 insertions(+), 311 deletions(-) diff --git a/vscode/extension/src/commands/format.ts b/vscode/extension/src/commands/format.ts index 5e3465921a..bedeaa16be 100644 --- a/vscode/extension/src/commands/format.ts +++ b/vscode/extension/src/commands/format.ts @@ -11,12 +11,18 @@ export const format = ( authProvider: AuthenticationProviderTobikoCloud, lsp: LSPClient | undefined, + restartLSP: () => Promise, ) => async (): Promise => { traceLog('Calling format') const out = await internalFormat(lsp) if (isErr(out)) { - return handleError(authProvider, out.error, 'Project format failed') + return handleError( + authProvider, + restartLSP, + out.error, + 'Project format failed', + ) } vscode.window.showInformationMessage('Project formatted successfully') } diff --git a/vscode/extension/src/commands/signin.ts b/vscode/extension/src/commands/signin.ts index e59c2b2161..77131a8253 100644 --- a/vscode/extension/src/commands/signin.ts +++ b/vscode/extension/src/commands/signin.ts @@ -1,13 +1,30 @@ import { AuthenticationProviderTobikoCloud } from '../auth/auth' import * as vscode from 'vscode' import { isCodespaces } from '../utilities/isCodespaces' +import { traceInfo } from '../utilities/common/log' export const signIn = - (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { + ( + authenticationProvider: AuthenticationProviderTobikoCloud, + onSignInSuccess: () => Promise, + ) => + async () => { if (isCodespaces()) { await authenticationProvider.sign_in_device_flow() } else { await authenticationProvider.createSession() } - await vscode.window.showInformationMessage('Signed in successfully') + + // Do not await this, as this will block the thread, you just need to show the message, but not block + vscode.window.showInformationMessage('Signed in successfully') + + // Execute callback after successful sign-in + if (onSignInSuccess) { + traceInfo('Executing post sign-in callback') + try { + await onSignInSuccess() + } catch (error) { + traceInfo(`Error in post sign-in callback: ${error}`) + } + } } diff --git a/vscode/extension/src/commands/signinSpecifyFlow.ts b/vscode/extension/src/commands/signinSpecifyFlow.ts index 8e277b28b0..2e0c0cfe15 100644 --- a/vscode/extension/src/commands/signinSpecifyFlow.ts +++ b/vscode/extension/src/commands/signinSpecifyFlow.ts @@ -3,7 +3,11 @@ import { traceInfo } from '../utilities/common/log' import { window } from 'vscode' export const signInSpecifyFlow = - (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => { + ( + authenticationProvider: AuthenticationProviderTobikoCloud, + onSignInSuccess?: () => Promise, + ) => + async () => { traceInfo('Sign in specify flow') const flowOptions = [ { @@ -24,11 +28,31 @@ export const signInSpecifyFlow = await authenticationProvider.sign_in_oauth_flow() await authenticationProvider.getSessions() await window.showInformationMessage('Sign in success') + + // Execute callback after successful sign-in + if (onSignInSuccess) { + traceInfo('Executing post sign-in callback') + try { + await onSignInSuccess() + } catch (error) { + traceInfo(`Error in post sign-in callback: ${error}`) + } + } return } else if (selectedFlow.label === 'Device Flow') { await authenticationProvider.sign_in_device_flow() await authenticationProvider.getSessions() await window.showInformationMessage('Sign in success') + + // Execute callback after successful sign-in + if (onSignInSuccess) { + traceInfo('Executing post sign-in callback') + try { + await onSignInSuccess() + } catch (error) { + traceInfo(`Error in post sign-in callback: ${error}`) + } + } return } else { traceInfo('Invalid flow selected') diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 8749c61fb2..8e4b57c907 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -45,12 +45,21 @@ export async function activate(context: vscode.ExtensionContext) { traceInfo('Authentication provider registered') context.subscriptions.push( - vscode.commands.registerCommand('sqlmesh.signin', signIn(authProvider)), + vscode.commands.registerCommand( + 'sqlmesh.signin', + signIn(authProvider, async () => { + traceInfo('Restarting LSP after sign-in') + await restart() + }), + ), ) context.subscriptions.push( vscode.commands.registerCommand( 'sqlmesh.signinSpecifyFlow', - signInSpecifyFlow(authProvider), + signInSpecifyFlow(authProvider, async () => { + traceInfo('Restarting LSP after sign-in') + await restart() + }), ), ) context.subscriptions.push( @@ -76,13 +85,6 @@ export async function activate(context: vscode.ExtensionContext) { ), ) - context.subscriptions.push( - vscode.commands.registerCommand( - 'sqlmesh.format', - format(authProvider, lspClient), - ), - ) - // Register the webview const lineagePanel = new LineagePanel(context.extensionUri, lspClient) context.subscriptions.push( @@ -118,14 +120,35 @@ export async function activate(context: vscode.ExtensionContext) { if (isErr(restartResult)) { return handleError( authProvider, + restart, restartResult.error, 'LSP restart failed', ) } context.subscriptions.push(lspClient) + } else { + lspClient = new LSPClient() + const result = await lspClient.start() + if (isErr(result)) { + return handleError( + authProvider, + restart, + result.error, + 'Failed to start LSP', + ) + } else { + context.subscriptions.push(lspClient) + } } } + context.subscriptions.push( + vscode.commands.registerCommand( + 'sqlmesh.format', + format(authProvider, lspClient, restart), + ), + ) + context.subscriptions.push( onDidChangePythonInterpreter(async () => { await restart() @@ -140,7 +163,12 @@ export async function activate(context: vscode.ExtensionContext) { const result = await lspClient.start() if (isErr(result)) { - return handleError(authProvider, result.error, 'Failed to start LSP') + return handleError( + authProvider, + restart, + result.error, + 'Failed to start LSP', + ) } else { context.subscriptions.push(lspClient) } diff --git a/vscode/extension/src/utilities/errors.ts b/vscode/extension/src/utilities/errors.ts index 62d0d016b9..f062d89369 100644 --- a/vscode/extension/src/utilities/errors.ts +++ b/vscode/extension/src/utilities/errors.ts @@ -69,6 +69,7 @@ interface SqlmeshLspDependenciesMissingError { export async function handleError( authProvider: AuthenticationProviderTobikoCloud, + restartLsp: () => Promise, error: ErrorType, genericErrorPrefix?: string, ): Promise { @@ -85,7 +86,7 @@ export async function handleError( ) return case 'not_signed_in': - return handleNotSignedInError(authProvider) + return handleNotSignedInError(authProvider, restartLsp) case 'sqlmesh_not_found': return handleSqlmeshNotFoundError() case 'sqlmesh_lsp_not_found': @@ -110,6 +111,7 @@ export async function handleError( */ const handleNotSignedInError = async ( authProvider: AuthenticationProviderTobikoCloud, + restartLsp: () => Promise, ): Promise => { traceInfo('handleNotSginedInError') const result = await window.showInformationMessage( @@ -117,7 +119,7 @@ const handleNotSignedInError = async ( 'Sign In', ) if (result === 'Sign In') { - await signIn(authProvider)() + await signIn(authProvider, restartLsp)() } } diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index f01dfc1a33..20629f5b79 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -34,311 +34,390 @@ async function setupPythonEnvironment(envDir: string): Promise { return pythonDetails.pythonPath } -test.describe('Tcloud', () => { - test('not signed in, shows sign in window', async ({}, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), +test('not signed in, shows sign in window', async ({}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Create a tcloud.yaml to mark this as a tcloud project + const tcloudConfig = { + url: 'https://mock.tobikodata.com', + org: 'test-org', + project: 'test-project', + } + await fs.writeFile( + path.join(tempDir, 'tcloud.yaml'), + `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, ) - const pythonEnvDir = path.join(tempDir, '.venv') - - try { - // Copy sushi project - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - // Create a tcloud.yaml to mark this as a tcloud project - const tcloudConfig = { - url: 'https://mock.tobikodata.com', - org: 'test-org', - project: 'test-project', - } - await fs.writeFile( - path.join(tempDir, 'tcloud.yaml'), - `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, - ) - - // Set tcloud version to 2.10.0 - await setTcloudVersion(tempDir, '2.10.0') - - // Set up Python environment with mock tcloud and sqlmesh - const pythonPath = await setupPythonEnvironment(pythonEnvDir) - - // Configure VS Code settings to use our Python environment - const settings = { - 'python.defaultInterpreterPath': pythonPath, - 'sqlmesh.environmentPath': pythonEnvDir, - } - await fs.ensureDir(path.join(tempDir, '.vscode')) - await fs.writeJson( - path.join(tempDir, '.vscode', 'settings.json'), - settings, - { spaces: 2 }, - ) - - // Start VS Code - const { window, close } = await startVSCode(tempDir) - - // Open a SQL file to trigger SQLMesh activation - // Wait for the models folder to be visible - await window.waitForSelector('text=models') - - // Click on the models folder - await window - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the top_waiters model - await window - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - // Wait for the file to open - await window.waitForTimeout(2000) - - await window.waitForSelector( - 'text=Please sign in to Tobiko Cloud to use SQLMesh', - ) - - // Close VS Code - await close() - } finally { - // Clean up - await fs.remove(tempDir) + + // Set tcloud version to 2.10.0 + await setTcloudVersion(tempDir, '2.10.0') + + // Set up Python environment with mock tcloud and sqlmesh + const pythonPath = await setupPythonEnvironment(pythonEnvDir) + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, } - }) + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson( + path.join(tempDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) - test('signed in and not installed shows installation window', async ({}, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), + // Start VS Code + const { window, close } = await startVSCode(tempDir) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Wait for the file to open + await window.waitForTimeout(2000) + + await window.waitForSelector( + 'text=Please sign in to Tobiko Cloud to use SQLMesh', ) - const pythonEnvDir = path.join(tempDir, '.venv') - - try { - // Copy sushi project - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - // Create a tcloud.yaml to mark this as a tcloud project - const tcloudConfig = { - url: 'https://mock.tobikodata.com', - org: 'test-org', - project: 'test-project', - } - await fs.writeFile( - path.join(tempDir, 'tcloud.yaml'), - `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, - ) - - // Write mock ".tcloud_auth_state.json" file - await setupAuthenticatedState(tempDir) - - // Set tcloud version to 2.10.0 - await setTcloudVersion(tempDir, '2.10.0') - - // Set up Python environment with mock tcloud and sqlmesh - const pythonPath = await setupPythonEnvironment(pythonEnvDir) - - // Configure VS Code settings to use our Python environment - const settings = { - 'python.defaultInterpreterPath': pythonPath, - 'sqlmesh.environmentPath': pythonEnvDir, - } - await fs.ensureDir(path.join(tempDir, '.vscode')) - await fs.writeJson( - path.join(tempDir, '.vscode', 'settings.json'), - settings, - { spaces: 2 }, - ) - - // Start VS Code - const { window, close } = await startVSCode(tempDir) - - // Open a SQL file to trigger SQLMesh activation - // Wait for the models folder to be visible - await window.waitForSelector('text=models') - - // Click on the models folder - await window - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the top_waiters model - await window - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - await window.waitForSelector('text=Installing enterprise python package') - expect( - await window.locator('text=Installing enterprise python package'), - ).toHaveCount(2) - - await window.waitForSelector('text=Loaded SQLMesh context') - - // Close VS Code - await close() - } finally { - // Clean up - await fs.remove(tempDir) + + // Close VS Code + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } +}) + +test('signed in and not installed shows installation window', async ({}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Create a tcloud.yaml to mark this as a tcloud project + const tcloudConfig = { + url: 'https://mock.tobikodata.com', + org: 'test-org', + project: 'test-project', } - }) + await fs.writeFile( + path.join(tempDir, 'tcloud.yaml'), + `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, + ) - test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when ready', async ({}, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), + // Write mock ".tcloud_auth_state.json" file + await setupAuthenticatedState(tempDir) + + // Set tcloud version to 2.10.0 + await setTcloudVersion(tempDir, '2.10.0') + + // Set up Python environment with mock tcloud and sqlmesh + const pythonPath = await setupPythonEnvironment(pythonEnvDir) + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson( + path.join(tempDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, ) - const pythonEnvDir = path.join(tempDir, '.venv') - - try { - // Copy sushi project - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - // Create a tcloud.yaml to mark this as a tcloud project - const tcloudConfig = { - url: 'https://mock.tobikodata.com', - org: 'test-org', - project: 'test-project', - } - await fs.writeFile( - path.join(tempDir, 'tcloud.yaml'), - `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, - ) - - // Write mock ".tcloud_auth_state.json" file - await setupAuthenticatedState(tempDir) - - // Set tcloud version to 2.10.0 - await setTcloudVersion(tempDir, '2.10.0') - - // Set up Python environment with mock tcloud and sqlmesh - const pythonPath = await setupPythonEnvironment(pythonEnvDir) - - // Mark sqlmesh as installed - const binDir = path.dirname(pythonPath) - const installStateFile = path.join(binDir, '.sqlmesh_installed') - await fs.writeFile(installStateFile, '') - - // Configure VS Code settings to use our Python environment - const settings = { - 'python.defaultInterpreterPath': pythonPath, - 'sqlmesh.environmentPath': pythonEnvDir, - } - await fs.ensureDir(path.join(tempDir, '.vscode')) - await fs.writeJson( - path.join(tempDir, '.vscode', 'settings.json'), - settings, - { spaces: 2 }, - ) - - // Start VS Code - const { window, close } = await startVSCode(tempDir) - - // Open a SQL file to trigger SQLMesh activation - // Wait for the models folder to be visible - await window.waitForSelector('text=models') - - // Click on the models folder - await window - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the top_waiters model - await window - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - // Verify the context loads successfully - await window.waitForSelector('text=Loaded SQLMesh context') - - // Close VS Code - await close() - } finally { - // Clean up - await fs.remove(tempDir) + + // Start VS Code + const { window, close } = await startVSCode(tempDir) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=Installing enterprise python package') + expect( + await window.locator('text=Installing enterprise python package'), + ).toHaveCount(2) + + await window.waitForSelector('text=Loaded SQLMesh context') + + // Close VS Code + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } +}) + +test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when ready', async ({}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Create a tcloud.yaml to mark this as a tcloud project + const tcloudConfig = { + url: 'https://mock.tobikodata.com', + org: 'test-org', + project: 'test-project', } - }) + await fs.writeFile( + path.join(tempDir, 'tcloud.yaml'), + `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, + ) - test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when ready', async ({}, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), + // Write mock ".tcloud_auth_state.json" file + await setupAuthenticatedState(tempDir) + + // Set tcloud version to 2.10.0 + await setTcloudVersion(tempDir, '2.10.0') + + // Set up Python environment with mock tcloud and sqlmesh + const pythonPath = await setupPythonEnvironment(pythonEnvDir) + + // Mark sqlmesh as installed + const binDir = path.dirname(pythonPath) + const installStateFile = path.join(binDir, '.sqlmesh_installed') + await fs.writeFile(installStateFile, '') + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson( + path.join(tempDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, ) - const pythonEnvDir = path.join(tempDir, '.venv') - - try { - // Copy sushi project - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - // Create a tcloud.yaml to mark this as a tcloud project - const tcloudConfig = { - url: 'https://mock.tobikodata.com', - org: 'test-org', - project: 'test-project', - } - await fs.writeFile( - path.join(tempDir, 'tcloud.yaml'), - `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, - ) - - // Write mock ".tcloud_auth_state.json" file - await setupAuthenticatedState(tempDir) - - // Set tcloud version to 2.10.0 - await setTcloudVersion(tempDir, '2.10.1') - - // Set up Python environment with mock tcloud and sqlmesh - const pythonPath = await setupPythonEnvironment(pythonEnvDir) - - // Mark sqlmesh as installed - const binDir = path.dirname(pythonPath) - const installStateFile = path.join(binDir, '.sqlmesh_installed') - await fs.writeFile(installStateFile, '') - - // Configure VS Code settings to use our Python environment - const settings = { - 'python.defaultInterpreterPath': pythonPath, - 'sqlmesh.environmentPath': pythonEnvDir, - } - await fs.ensureDir(path.join(tempDir, '.vscode')) - await fs.writeJson( - path.join(tempDir, '.vscode', 'settings.json'), - settings, - { spaces: 2 }, - ) - - // Start VS Code - const { window, close } = await startVSCode(tempDir) - - // Open a SQL file to trigger SQLMesh activation - // Wait for the models folder to be visible - await window.waitForSelector('text=models') - - // Click on the models folder - await window - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the top_waiters model - await window - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - // Verify the context loads successfully - await window.waitForSelector('text=Loaded SQLMesh context') - - // Close VS Code - await close() - } finally { - // Clean up - await fs.remove(tempDir) + + // Start VS Code + const { window, close } = await startVSCode(tempDir) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Verify the context loads successfully + await window.waitForSelector('text=Loaded SQLMesh context') + + // Close VS Code + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } +}) + +test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when ready', async ({}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Create a tcloud.yaml to mark this as a tcloud project + const tcloudConfig = { + url: 'https://mock.tobikodata.com', + org: 'test-org', + project: 'test-project', + } + await fs.writeFile( + path.join(tempDir, 'tcloud.yaml'), + `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, + ) + + // Write mock ".tcloud_auth_state.json" file + await setupAuthenticatedState(tempDir) + + // Set tcloud version to 2.10.0 + await setTcloudVersion(tempDir, '2.10.1') + + // Set up Python environment with mock tcloud and sqlmesh + const pythonPath = await setupPythonEnvironment(pythonEnvDir) + + // Mark sqlmesh as installed + const binDir = path.dirname(pythonPath) + const installStateFile = path.join(binDir, '.sqlmesh_installed') + await fs.writeFile(installStateFile, '') + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson( + path.join(tempDir, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) + + // Start VS Code + const { window, close } = await startVSCode(tempDir) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Verify the context loads successfully + await window.waitForSelector('text=Loaded SQLMesh context') + + // Close VS Code + await close() + } finally { + // Clean up + await fs.remove(tempDir) + } +}) + +// This test is skipped becuase of the way the sign in window is shown is not useable by playwright. It's not solvable +// but the test is still useful when running it manually. +test.skip('tcloud not signed in and not installed, shows sign in window and then fact that loaded', async ({}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + const pythonEnvDir = path.join(tempDir, '.venv') + + // Create a tcloud.yaml to mark this as a tcloud project + const tcloudConfig = { + url: 'https://mock.tobikodata.com', + org: 'test-org', + project: 'test-project', + } + await fs.writeFile( + path.join(tempDir, 'tcloud.yaml'), + `url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`, + ) + + // Set up Python environment with mock tcloud and sqlmesh + const pythonPath = await setupPythonEnvironment(pythonEnvDir) + + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { + spaces: 2, }) + + // Set tcloud version to 2.10.0 + await setTcloudVersion(tempDir, '2.10.1') + + // Start VS Code + const { window, close } = await startVSCode(tempDir) + + try { + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Verify the sign in window is shown + await window.waitForSelector( + 'text=Please sign in to Tobiko Cloud to use SQLMesh', + ) + + // Click on the sign in button + await window + .getByRole('button', { name: 'Sign in' }) + .filter({ hasText: 'Sign in' }) + .click() + await window.waitForSelector('text="Signed in successfully"') + + await window.waitForSelector('text=Installing enterprise python package') + + await window.waitForSelector('text=Loaded SQLMesh context') + } finally { + // Clean up + await close() + await fs.remove(tempDir) + } }) From c3a4362d8448547ed0c58bb62c85a4aa54a70f5b Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:52:32 +0300 Subject: [PATCH 0426/1056] Fix(mssql): Properly quote table and views when dropping in mssql (#4773) --- sqlmesh/core/engine_adapter/mssql.py | 7 ++++-- tests/core/engine_adapter/test_mssql.py | 30 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index 40649f3c2d..88b3f51ed3 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -172,15 +172,18 @@ def drop_schema( if cascade: objects = self._get_data_objects(schema_name) for obj in objects: + # Build properly quoted table for MSSQL using square brackets when needed + object_table = exp.table_(obj.name, obj.schema_name) + # _get_data_objects is catalog-specific, so these can't accidentally drop view/tables in another catalog if obj.type == DataObjectType.VIEW: self.drop_view( - ".".join([obj.schema_name, obj.name]), + object_table, ignore_if_not_exists=ignore_if_not_exists, ) else: self.drop_table( - ".".join([obj.schema_name, obj.name]), + object_table, exists=ignore_if_not_exists, ) super().drop_schema(schema_name, ignore_if_not_exists=ignore_if_not_exists, cascade=False) diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index 939d26a95d..beeaa59c89 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -655,6 +655,36 @@ def test_drop_schema(make_mocked_engine_adapter: t.Callable): ] +def test_drop_schema_with_special_identifiers(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="test schema", # Schema with space + name="test view", # Object with space + type=DataObjectType.from_str("VIEW"), + ), + DataObject( + catalog="test_catalog", + schema="test schema", + name="test table", # Table with space + type=DataObjectType.from_str("TABLE"), + ), + ] + + schema_name = exp.to_table("[test schema]", dialect="tsql") + adapter.drop_schema(schema_name, cascade=True) + + # Validate that names with spaces/special chars are properly quoted with square brackets + assert to_sql_calls(adapter) == [ + """DROP VIEW IF EXISTS [test schema].[test view];""", + """DROP TABLE IF EXISTS [test schema].[test table];""", + """DROP SCHEMA IF EXISTS [test schema];""", + ] + + def test_df_dates(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(MSSQLEngineAdapter) From 8afbaf137296f1199160ccf1326f4cdfdf061646 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Fri, 20 Jun 2025 18:16:34 +0300 Subject: [PATCH 0427/1056] Chore!: Change `get_expired_environments()` to return EnvironmentSummary (#4774) --- sqlmesh/core/context.py | 22 ++++++++++++++-------- sqlmesh/core/state_sync/base.py | 6 +++--- sqlmesh/core/state_sync/db/environment.py | 21 +++++++++------------ sqlmesh/core/state_sync/db/facade.py | 4 ++-- tests/core/state_sync/test_state_sync.py | 2 +- tests/core/test_context.py | 15 ++++++++++----- 6 files changed, 39 insertions(+), 31 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 8d8648f918..d7aa873394 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -2695,16 +2695,22 @@ def _run_janitor(self, ignore_ttl: bool = False) -> None: def _cleanup_environments(self, current_ts: t.Optional[int] = None) -> None: current_ts = current_ts or now_timestamp() - expired_environments = self.state_sync.get_expired_environments(current_ts=current_ts) - - cleanup_expired_views( - default_adapter=self.engine_adapter, - engine_adapters=self.engine_adapters, - environments=expired_environments, - warn_on_delete_failure=self.config.janitor.warn_on_delete_failure, - console=self.console, + expired_environments_summaries = self.state_sync.get_expired_environments( + current_ts=current_ts ) + for expired_env_summary in expired_environments_summaries: + expired_env = self.state_reader.get_environment(expired_env_summary.name) + + if expired_env: + cleanup_expired_views( + default_adapter=self.engine_adapter, + engine_adapters=self.engine_adapters, + environments=[expired_env], + warn_on_delete_failure=self.config.janitor.warn_on_delete_failure, + console=self.console, + ) + self.state_sync.delete_expired_environments(current_ts=current_ts) def _try_connection(self, connection_name: str, validator: t.Callable[[], None]) -> None: diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index 4f46ccf9b8..4a4e31854f 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -304,12 +304,12 @@ def get_expired_snapshots( """ @abc.abstractmethod - def get_expired_environments(self, current_ts: int) -> t.List[Environment]: + def get_expired_environments(self, current_ts: int) -> t.List[EnvironmentSummary]: """Returns the expired environments. Expired environments are environments that have exceeded their time-to-live value. Returns: - The list of environments to remove, the filter to remove environments. + The list of environment summaries to remove. """ @@ -418,7 +418,7 @@ def finalize(self, environment: Environment) -> None: @abc.abstractmethod def delete_expired_environments( self, current_ts: t.Optional[int] = None - ) -> t.List[Environment]: + ) -> t.List[EnvironmentSummary]: """Removes expired environments. Expired environments are environments that have exceeded their time-to-live value. diff --git a/sqlmesh/core/state_sync/db/environment.py b/sqlmesh/core/state_sync/db/environment.py index dcf915a91c..b06d6160cc 100644 --- a/sqlmesh/core/state_sync/db/environment.py +++ b/sqlmesh/core/state_sync/db/environment.py @@ -165,27 +165,24 @@ def finalize(self, environment: Environment) -> None: where=environment_filter, ) - def get_expired_environments(self, current_ts: int) -> t.List[Environment]: + def get_expired_environments(self, current_ts: int) -> t.List[EnvironmentSummary]: """Returns the expired environments. Expired environments are environments that have exceeded their time-to-live value. Returns: - The list of environments to remove, the filter to remove environments. + The list of environment summaries to remove. """ - rows = fetchall( - self.engine_adapter, - self._environments_query( - where=self._create_expiration_filter_expr(current_ts), - lock_for_update=True, - ), - ) - expired_environments = [self._environment_from_row(r) for r in rows] - return expired_environments + environment_summaries = self.get_environments_summary() + return [ + env_summary + for env_summary in environment_summaries + if env_summary.expiration_ts is not None and env_summary.expiration_ts <= current_ts + ] def delete_expired_environments( self, current_ts: t.Optional[int] = None - ) -> t.List[Environment]: + ) -> t.List[EnvironmentSummary]: """Deletes expired environments. Returns: diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 7e48418317..2a27c5fd92 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -274,7 +274,7 @@ def get_expired_snapshots( self.environment_state.get_environments(), current_ts=current_ts, ignore_ttl=ignore_ttl ) - def get_expired_environments(self, current_ts: int) -> t.List[Environment]: + def get_expired_environments(self, current_ts: int) -> t.List[EnvironmentSummary]: return self.environment_state.get_expired_environments(current_ts=current_ts) @transactional() @@ -294,7 +294,7 @@ def delete_expired_snapshots( @transactional() def delete_expired_environments( self, current_ts: t.Optional[int] = None - ) -> t.List[Environment]: + ) -> t.List[EnvironmentSummary]: current_ts = current_ts or now_timestamp() return self.environment_state.delete_expired_environments(current_ts=current_ts) diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index 173264f9b5..dd68b5c515 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -1115,7 +1115,7 @@ def test_delete_expired_environments(state_sync: EngineAdapterStateSync, make_sn assert state_sync.get_environment_statements(env_a.name) == environment_statements deleted_environments = state_sync.delete_expired_environments() - assert deleted_environments == [env_a] + assert deleted_environments == [env_a.summary] assert state_sync.get_environment(env_a.name) is None assert state_sync.get_environment(env_b.name) == env_b diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 3d02d32e7e..276dd38afc 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -859,9 +859,9 @@ def test_janitor(sushi_context, mocker: MockerFixture) -> None: adapter_mock.dialect = "duckdb" state_sync_mock = mocker.MagicMock() - state_sync_mock.get_expired_environments.return_value = [ + environments = [ Environment( - name="test_environment", + name="test_environment1", suffix_target=EnvironmentSuffixTarget.TABLE, snapshots=[x.table_info for x in sushi_context.snapshots.values()], start_at="2022-01-01", @@ -870,7 +870,7 @@ def test_janitor(sushi_context, mocker: MockerFixture) -> None: previous_plan_id="test_plan_id", ), Environment( - name="test_environment", + name="test_environment2", suffix_target=EnvironmentSuffixTarget.SCHEMA, snapshots=[x.table_info for x in sushi_context.snapshots.values()], start_at="2022-01-01", @@ -880,6 +880,11 @@ def test_janitor(sushi_context, mocker: MockerFixture) -> None: ), ] + state_sync_mock.get_expired_environments.return_value = [env.summary for env in environments] + state_sync_mock.get_environment = lambda name: next( + env for env in environments if env.name == name + ) + sushi_context._engine_adapter = adapter_mock sushi_context.engine_adapters = {sushi_context.config.default_gateway: adapter_mock} sushi_context._state_sync = state_sync_mock @@ -891,7 +896,7 @@ def test_janitor(sushi_context, mocker: MockerFixture) -> None: adapter_mock.drop_schema.assert_has_calls( [ call( - schema_("sushi__test_environment", "memory"), + schema_("sushi__test_environment2", "memory"), cascade=True, ignore_if_not_exists=True, ), @@ -903,7 +908,7 @@ def test_janitor(sushi_context, mocker: MockerFixture) -> None: adapter_mock.drop_view.assert_has_calls( [ call( - "memory.sushi.waiter_as_customer_by_day__test_environment", + "memory.sushi.waiter_as_customer_by_day__test_environment1", ignore_if_not_exists=True, ), ] From fca8a3f67258ec92b5bd5597ce4d590391b10c0b Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sat, 21 Jun 2025 20:40:24 +0200 Subject: [PATCH 0428/1056] feat(vscode): ability to stop server (#4775) --- vscode/extension/package.json | 5 +++ vscode/extension/src/commands/stop.ts | 18 +++++++++ vscode/extension/src/extension.ts | 2 + vscode/extension/tests/stop.spec.ts | 57 +++++++++++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 vscode/extension/src/commands/stop.ts create mode 100644 vscode/extension/tests/stop.spec.ts diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 254ee8ef16..aefc1d736c 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -93,6 +93,11 @@ "title": "SQLMesh: Render Model", "description": "SQLMesh", "icon": "$(open-preview)" + }, + { + "command": "sqlmesh.stop", + "title": "SQLMesh: Stop Server", + "description": "SQLMesh" } ], "menus": { diff --git a/vscode/extension/src/commands/stop.ts b/vscode/extension/src/commands/stop.ts new file mode 100644 index 0000000000..f0276acf8b --- /dev/null +++ b/vscode/extension/src/commands/stop.ts @@ -0,0 +1,18 @@ +import { window } from 'vscode' +import { LSPClient } from '../lsp/lsp' +import { traceInfo } from '../utilities/common/log' + +export const stop = (lspClient: LSPClient | undefined) => { + return async () => { + traceInfo('Stopping LSP server') + + if (!lspClient) { + await window.showInformationMessage('LSP server is not running') + return + } + + await lspClient.stop() + await window.showInformationMessage('LSP server stopped') + traceInfo('LSP server stopped successfully') + } +} diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 8e4b57c907..0525397cff 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -13,6 +13,7 @@ import { signOut } from './commands/signout' import { signIn } from './commands/signin' import { signInSpecifyFlow } from './commands/signinSpecifyFlow' import { renderModel, reRenderModelForSourceFile } from './commands/renderModel' +import { stop } from './commands/stop' import { isErr } from '@bus/result' import { handleError } from './utilities/errors' import { selector, completionProvider } from './completion/completion' @@ -159,6 +160,7 @@ export async function activate(context: vscode.ExtensionContext) { registerCommand(`sqlmesh.restart`, async () => { await restart() }), + registerCommand(`sqlmesh.stop`, stop(lspClient)), ) const result = await lspClient.start() diff --git a/vscode/extension/tests/stop.spec.ts b/vscode/extension/tests/stop.spec.ts new file mode 100644 index 0000000000..61422991cc --- /dev/null +++ b/vscode/extension/tests/stop.spec.ts @@ -0,0 +1,57 @@ +import path from 'path' +import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import os from 'os' +import { test } from '@playwright/test' +import fs from 'fs-extra' + +test('Stop server works', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + try { + const { window, close } = await startVSCode(tempDir) + + // Wait for the models folder to be visible + await window.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customer_revenue_lifetime model + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Stop the server + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('SQLMesh: Stop Server') + await window.keyboard.press('Enter') + + // Await LSP server stopped message + await window.waitForSelector('text=LSP server stopped') + + // Render the model + await window.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await window.keyboard.type('Render Model') + await window.keyboard.press('Enter') + + // Await error message + await window.waitForSelector( + 'text="Failed to render model: LSP client not ready."', + ) + await close() + } finally { + await fs.remove(tempDir) + } +}) From 3e1c1cb11005800b976d0641cb98c9ef5bb2b23d Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Mon, 23 Jun 2025 12:00:54 +1200 Subject: [PATCH 0429/1056] Chore: Fix dbt import to prevent cicd failures (#4781) --- sqlmesh/dbt/manifest.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 05750b43b8..49827e68a7 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -21,7 +21,14 @@ from dbt.config.profile import read_profile from dbt.config.renderer import DbtProjectYamlRenderer, ProfileRenderer from dbt.parser.manifest import ManifestLoader -from dbt.parser.sources import merge_freshness + +try: + from dbt.parser.sources import merge_freshness # type: ignore[attr-defined] +except ImportError: + # merge_freshness was renamed to merge_source_freshness in dbt 1.10 + # ref: https://github.com/dbt-labs/dbt-core/commit/14fc39a76ff4830cdf2fcbe73f57ca27db500018#diff-1f09db95588f46879a83378c2a86d6b16b7cdfcaddbfe46afc5d919ee5e9a4d9R430 + from dbt.parser.sources import merge_source_freshness as merge_freshness # type: ignore[no-redef,attr-defined] + from dbt.tracking import do_not_track from sqlmesh.core import constants as c From 66ac52a17ce296d4b1a4f45b61fab940add857a1 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:32:37 +0300 Subject: [PATCH 0430/1056] Feat: add formatter options to render command (#4753) --- docs/reference/cli.md | 38 ++++++++----- docs/reference/notebook.md | 14 +++++ sqlmesh/cli/main.py | 63 +++++++------------- sqlmesh/cli/options.py | 36 ++++++++++++ sqlmesh/magics.py | 114 ++++++++++++++++++++++--------------- tests/cli/test_cli.py | 32 +++++++++++ tests/core/test_dialect.py | 2 +- 7 files changed, 196 insertions(+), 103 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 34d543a5a0..66f55de516 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -419,19 +419,31 @@ Usage: sqlmesh render [OPTIONS] MODEL Render a model's query, optionally expanding referenced models. Options: - -s, --start TEXT The start datetime of the interval for which this - command will be applied. - -e, --end TEXT The end datetime of the interval for which this - command will be applied. - --execution-time TEXT The execution time (defaults to now). - --expand TEXT Whether or not to expand materialized models - (defaults to False). If True, all referenced models - are expanded as raw queries. Multiple model names can - also be specified, in which case only they will be - expanded as raw queries. - --dialect TEXT The SQL dialect to render the query as. - --no-format Disable fancy formatting of the query. - --help Show this message and exit. + -s, --start TEXT The start datetime of the interval for which + this command will be applied. + -e, --end TEXT The end datetime of the interval for which this + command will be applied. + --execution-time TEXT The execution time (defaults to now). + --expand TEXT Whether or not to expand materialized models + (defaults to False). If True, all referenced + models are expanded as raw queries. Multiple + model names can also be specified, in which case + only they will be expanded as raw queries. + --dialect TEXT The SQL dialect to render the query as. + --no-format Disable fancy formatting of the query. + --max-text-width INTEGER The max number of characters in a segment before + creating new lines in pretty mode. + --leading-comma Determines whether or not the comma is leading + or trailing in select expressions. Default is + trailing. + --normalize-functions TEXT Whether or not to normalize all function names. + Possible values are: 'upper', 'lower' + --indent INTEGER Determines the indentation size in a formatted + string. + --pad INTEGER Determines the pad size in a formatted string. + --normalize Whether or not to normalize identifiers to + lowercase. + --help Show this message and exit. ``` ## rewrite diff --git a/docs/reference/notebook.md b/docs/reference/notebook.md index 505d79e575..df64b9bb71 100644 --- a/docs/reference/notebook.md +++ b/docs/reference/notebook.md @@ -201,6 +201,9 @@ options: ``` %render [--start START] [--end END] [--execution-time EXECUTION_TIME] [--expand EXPAND] [--dialect DIALECT] [--no-format] + [--normalize] [--pad PAD] [--indent INDENT] + [--normalize-functions NORMALIZE_FUNCTIONS] [--leading-comma] + [--max-text-width MAX_TEXT_WIDTH] model Renders a model's query, optionally expanding referenced models. @@ -220,6 +223,17 @@ options: models are expanded as raw queries. --dialect DIALECT SQL dialect to render. --no-format Disable fancy formatting of the query. + --normalize Whether or not to normalize identifiers to lowercase. + --pad PAD Determines the pad size in a formatted string. + --indent INDENT Determines the indentation size in a formatted string. + --normalize-functions NORMALIZE_FUNCTIONS + Whether or not to normalize all function names. + Possible values are: 'upper', 'lower' + --leading-comma Determines whether or not the comma is leading or + trailing in select expressions. Default is trailing. + --max-text-width MAX_TEXT_WIDTH + The max number of characters in a segment before + creating new lines in pretty mode. ``` #### dag diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index b83daf1724..b4d4537712 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -191,6 +191,7 @@ def init( help="The SQL dialect to render the query as.", ) @click.option("--no-format", is_flag=True, help="Disable fancy formatting of the query.") +@opt.format_options @click.pass_context @error_handler @cli_analytics @@ -203,6 +204,7 @@ def render( expand: t.Optional[t.Union[bool, t.Iterable[str]]] = None, dialect: t.Optional[str] = None, no_format: bool = False, + **format_kwargs: t.Any, ) -> None: """Render a model's query, optionally expanding referenced models.""" rendered = ctx.obj.render( @@ -213,7 +215,17 @@ def render( expand=expand, ) - sql = rendered.sql(pretty=True, dialect=ctx.obj.config.dialect if dialect is None else dialect) + format_config = ctx.obj.config_for_node(model).format + format_kwargs = { + **format_config.generator_options, + **{k: v for k, v in format_kwargs.items() if v is not None}, + } + + sql = rendered.sql( + pretty=True, + dialect=ctx.obj.config.dialect if dialect is None else dialect, + **format_kwargs, + ) if no_format: print(sql) else: @@ -264,55 +276,23 @@ def evaluate( help="Transpile project models to the specified dialect.", ) @click.option( - "--append-newline", - is_flag=True, - help="Include a newline at the end of each file.", - default=None, -) -@click.option( - "--no-rewrite-casts", - is_flag=True, - help="Preserve the existing casts, without rewriting them to use the :: syntax.", - default=None, -) -@click.option( - "--normalize", + "--check", is_flag=True, - help="Whether or not to normalize identifiers to lowercase.", + help="Whether or not to check formatting (but not actually format anything).", default=None, ) @click.option( - "--pad", - type=int, - help="Determines the pad size in a formatted string.", -) -@click.option( - "--indent", - type=int, - help="Determines the indentation size in a formatted string.", -) -@click.option( - "--normalize-functions", - type=str, - help="Whether or not to normalize all function names. Possible values are: 'upper', 'lower'", -) -@click.option( - "--leading-comma", + "--rewrite-casts/--no-rewrite-casts", is_flag=True, - help="Determines whether or not the comma is leading or trailing in select expressions. Default is trailing.", + help="Rewrite casts to use the :: syntax.", default=None, ) @click.option( - "--max-text-width", - type=int, - help="The max number of characters in a segment before creating new lines in pretty mode.", -) -@click.option( - "--check", + "--append-newline", is_flag=True, - help="Whether or not to check formatting (but not actually format anything).", - default=None, + help="Include a newline at the end of each file.", ) +@opt.format_options @click.pass_context @error_handler @cli_analytics @@ -320,9 +300,6 @@ def format( ctx: click.Context, paths: t.Optional[t.Tuple[str, ...]] = None, **kwargs: t.Any ) -> None: """Format all SQL models and audits.""" - if kwargs.pop("no_rewrite_casts", None): - kwargs["rewrite_casts"] = False - if not ctx.obj.format(**{k: v for k, v in kwargs.items() if v is not None}, paths=paths): ctx.exit(1) diff --git a/sqlmesh/cli/options.py b/sqlmesh/cli/options.py index 7a26b237cc..5f2ca034d8 100644 --- a/sqlmesh/cli/options.py +++ b/sqlmesh/cli/options.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import typing as t import click @@ -54,3 +55,38 @@ count=True, help="Verbose output. Use -vv for very verbose output.", ) + + +def format_options(func: t.Callable) -> t.Callable: + """Decorator to add common format options to CLI commands.""" + func = click.option( + "--normalize", + is_flag=True, + help="Whether or not to normalize identifiers to lowercase.", + )(func) + func = click.option( + "--pad", + type=int, + help="Determines the pad size in a formatted string.", + )(func) + func = click.option( + "--indent", + type=int, + help="Determines the indentation size in a formatted string.", + )(func) + func = click.option( + "--normalize-functions", + type=str, + help="Whether or not to normalize all function names. Possible values are: 'upper', 'lower'", + )(func) + func = click.option( + "--leading-comma", + is_flag=True, + help="Determines whether or not the comma is leading or trailing in select expressions. Default is trailing.", + )(func) + func = click.option( + "--max-text-width", + type=int, + help="The max number of characters in a segment before creating new lines in pretty mode.", + )(func) + return func diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 95260170fe..9110feb2fe 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -93,6 +93,43 @@ def wrapper(self: SQLMeshMagics, *args: t.Any, **kwargs: t.Any) -> None: return wrapper +def format_arguments(func: t.Callable) -> t.Callable: + """Decorator to add common format arguments to magic commands.""" + func = argument( + "--normalize", + action="store_true", + help="Whether or not to normalize identifiers to lowercase.", + default=None, + )(func) + func = argument( + "--pad", + type=int, + help="Determines the pad size in a formatted string.", + )(func) + func = argument( + "--indent", + type=int, + help="Determines the indentation size in a formatted string.", + )(func) + func = argument( + "--normalize-functions", + type=str, + help="Whether or not to normalize all function names. Possible values are: 'upper', 'lower'", + )(func) + func = argument( + "--leading-comma", + action="store_true", + help="Determines whether or not the comma is leading or trailing in select expressions. Default is trailing.", + default=None, + )(func) + func = argument( + "--max-text-width", + type=int, + help="The max number of characters in a segment before creating new lines in pretty mode.", + )(func) + return func + + @magics_class class SQLMeshMagics(Magics): @property @@ -579,23 +616,39 @@ def evaluate(self, context: Context, line: str) -> None: ) @argument("--dialect", type=str, help="SQL dialect to render.") @argument("--no-format", action="store_true", help="Disable fancy formatting of the query.") + @format_arguments @line_magic @pass_sqlmesh_context def render(self, context: Context, line: str) -> None: """Renders a model's query, optionally expanding referenced models.""" context.refresh() - args = parse_argstring(self.render, line) + render_opts = vars(parse_argstring(self.render, line)) + model = render_opts.pop("model") + dialect = render_opts.pop("dialect", None) query = context.render( - args.model, - start=args.start, - end=args.end, - execution_time=args.execution_time, - expand=args.expand, + model, + start=render_opts.pop("start", None), + end=render_opts.pop("end", None), + execution_time=render_opts.pop("execution_time", None), + expand=render_opts.pop("expand", False), ) - sql = query.sql(pretty=True, dialect=args.dialect or context.config.dialect) - if args.no_format: + no_format = render_opts.pop("no_format", False) + + format_config = context.config_for_node(model).format + format_options = { + **format_config.generator_options, + **{k: v for k, v in render_opts.items() if v is not None}, + } + + sql = query.sql( + pretty=True, + dialect=context.config.dialect if dialect is None else dialect, + **format_options, + ) + + if no_format: context.console.log_status_update(sql) else: context.console.show_sql(sql) @@ -853,55 +906,24 @@ def rewrite(self, context: Context, line: str, sql: str) -> None: help="Transpile project models to the specified dialect.", ) @argument( - "--append-newline", - action="store_true", - help="Whether or not to append a newline to the end of the file.", - default=None, - ) - @argument( - "--no-rewrite-casts", - action="store_true", - help="Whether or not to preserve the existing casts, without rewriting them to use the :: syntax.", - default=None, - ) - @argument( - "--normalize", + "--check", action="store_true", - help="Whether or not to normalize identifiers to lowercase.", + help="Whether or not to check formatting (but not actually format anything).", default=None, ) @argument( - "--pad", - type=int, - help="Determines the pad size in a formatted string.", - ) - @argument( - "--indent", - type=int, - help="Determines the indentation size in a formatted string.", - ) - @argument( - "--normalize-functions", - type=str, - help="Whether or not to normalize all function names. Possible values are: 'upper', 'lower'", - ) - @argument( - "--leading-comma", + "--append-newline", action="store_true", - help="Determines whether or not the comma is leading or trailing in select expressions. Default is trailing.", + help="Include a newline at the end of the output.", default=None, ) @argument( - "--max-text-width", - type=int, - help="The max number of characters in a segment before creating new lines in pretty mode.", - ) - @argument( - "--check", + "--no-rewrite-casts", action="store_true", - help="Whether or not to check formatting (but not actually format anything).", + help="Preserve the existing casts, without rewriting them to use the :: syntax.", default=None, ) + @format_arguments @line_magic @pass_sqlmesh_context def format(self, context: Context, line: str) -> bool: diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 0efcbd4574..c5f6438cc2 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1791,3 +1791,35 @@ def test_table_diff_schema_diff_ignore_case(runner: CliRunner, tmp_path: Path): assert result.exit_code == 0 stripped_output = "".join((x for x in result.output if x in string.printable)) assert "Schema Diff Between 'T1' and 'T2':\n Schemas match" in stripped_output + + +def test_render(runner: CliRunner, tmp_path: Path): + create_example_project(tmp_path) + + ctx = Context(paths=tmp_path) + + result = runner.invoke( + cli, + [ + "--paths", + str(tmp_path), + "render", + "sqlmesh_example.full_model", + "--max-text-width", + "10", + ], + ) + assert result.exit_code == 0 + + cleaned_output = "\n".join(l.rstrip(" ") for l in result.output.split("\n")) + expected = """SELECT + "incremental_model"."item_id" AS "item_id", + COUNT( + DISTINCT "incremental_model"."id" + ) AS "num_orders" +FROM "db"."sqlmesh_example"."incremental_model" AS "incremental_model" +GROUP BY + "incremental_model"."item_id" +""" + + assert expected in cleaned_output diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index c6b23efa1a..ebf90bebf7 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -207,7 +207,7 @@ def test_format_model_expressions(): parse( """ MODEL(name foo); - SELECT 1::INT AS bla + SELECT CAST(1 AS INT) AS bla """ ), rewrite_casts=False, From e5934928123c204f5f6341e89d5adc5dac42542e Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:48:53 +0300 Subject: [PATCH 0431/1056] Chore!: bump sqlglot to v26.30.0 (#4784) --- pyproject.toml | 2 +- sqlmesh/core/dialect.py | 2 ++ tests/core/test_macros.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6405d64d16..acee956e0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.29.0", + "sqlglot[rs]~=26.30.0", "tenacity", "time-machine", "json-stream" diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 279c9b6078..db77f0c461 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -346,12 +346,14 @@ def _parse_select( table: bool = False, parse_subquery_alias: bool = True, parse_set_operation: bool = True, + consume_pipe: bool = True, ) -> t.Optional[exp.Expression]: select = self.__parse_select( # type: ignore nested=nested, table=table, parse_subquery_alias=parse_subquery_alias, parse_set_operation=parse_set_operation, + consume_pipe=consume_pipe, ) if ( diff --git a/tests/core/test_macros.py b/tests/core/test_macros.py index 7f4a417c46..c235430a69 100644 --- a/tests/core/test_macros.py +++ b/tests/core/test_macros.py @@ -234,7 +234,9 @@ def test_macro_var(macro_evaluator): # Check Snowflake-specific StagedFilePath / MacroVar behavior e = parse_one("select @x from @path, @y", dialect="snowflake") + macro_evaluator.locals = {"x": parse_one("a"), "y": parse_one("t2")} + macro_evaluator.dialect = "snowflake" assert e.find(StagedFilePath) is not None assert macro_evaluator.transform(e).sql(dialect="snowflake") == "SELECT a FROM @path, t2" From 00d30f7a78c41c02a0d1474e26988e05e44de47f Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:02:32 +0300 Subject: [PATCH 0432/1056] Feat(lsp): Add rename functionality for CTEs in vscode (#4718) --- sqlmesh/lsp/main.py | 54 ++++++ sqlmesh/lsp/rename.py | 137 +++++++++++++ tests/lsp/test_document_highlight.py | 111 +++++++++++ tests/lsp/test_rename_cte.py | 212 +++++++++++++++++++++ vscode/extension/tests/rename_cte.spec.ts | 222 ++++++++++++++++++++++ 5 files changed, 736 insertions(+) create mode 100644 sqlmesh/lsp/rename.py create mode 100644 tests/lsp/test_document_highlight.py create mode 100644 tests/lsp/test_rename_cte.py create mode 100644 vscode/extension/tests/rename_cte.spec.ts diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 2a3e18c6ea..7462698f7d 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -52,6 +52,7 @@ get_references, get_all_references, ) +from sqlmesh.lsp.rename import prepare_rename, rename_symbol, get_document_highlights from sqlmesh.lsp.uri import URI from web.server.api.endpoints.lineage import column_lineage, model_lineage from web.server.api.endpoints.models import get_models @@ -435,6 +436,59 @@ def find_references( ls.show_message(f"Error getting locations: {e}", types.MessageType.Error) return None + @self.server.feature(types.TEXT_DOCUMENT_PREPARE_RENAME) + def prepare_rename_handler( + ls: LanguageServer, params: types.PrepareRenameParams + ) -> t.Optional[types.PrepareRenameResult]: + """Prepare for rename operation by checking if the symbol can be renamed.""" + try: + uri = URI(params.text_document.uri) + self._ensure_context_for_document(uri) + if self.lsp_context is None: + raise RuntimeError(f"No context found for document: {uri}") + + result = prepare_rename(self.lsp_context, uri, params.position) + return result + except Exception as e: + ls.log_trace(f"Error preparing rename: {e}") + return None + + @self.server.feature(types.TEXT_DOCUMENT_RENAME) + def rename_handler( + ls: LanguageServer, params: types.RenameParams + ) -> t.Optional[types.WorkspaceEdit]: + """Perform rename operation on the symbol at the given position.""" + try: + uri = URI(params.text_document.uri) + self._ensure_context_for_document(uri) + if self.lsp_context is None: + raise RuntimeError(f"No context found for document: {uri}") + + workspace_edit = rename_symbol( + self.lsp_context, uri, params.position, params.new_name + ) + return workspace_edit + except Exception as e: + ls.show_message(f"Error performing rename: {e}", types.MessageType.Error) + return None + + @self.server.feature(types.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT) + def document_highlight_handler( + ls: LanguageServer, params: types.DocumentHighlightParams + ) -> t.Optional[t.List[types.DocumentHighlight]]: + """Highlight all occurrences of the symbol at the given position.""" + try: + uri = URI(params.text_document.uri) + self._ensure_context_for_document(uri) + if self.lsp_context is None: + raise RuntimeError(f"No context found for document: {uri}") + + highlights = get_document_highlights(self.lsp_context, uri, params.position) + return highlights + except Exception as e: + ls.log_trace(f"Error getting document highlights: {e}") + return None + @self.server.feature(types.TEXT_DOCUMENT_DIAGNOSTIC) def diagnostic( ls: LanguageServer, params: types.DocumentDiagnosticParams diff --git a/sqlmesh/lsp/rename.py b/sqlmesh/lsp/rename.py new file mode 100644 index 0000000000..0dbe2594ea --- /dev/null +++ b/sqlmesh/lsp/rename.py @@ -0,0 +1,137 @@ +import typing as t +from lsprotocol.types import ( + Position, + TextEdit, + WorkspaceEdit, + PrepareRenameResult_Type1, + DocumentHighlight, + DocumentHighlightKind, +) + +from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.reference import ( + _position_within_range, + get_cte_references, + LSPCteReference, +) +from sqlmesh.lsp.uri import URI + + +def prepare_rename( + lsp_context: LSPContext, document_uri: URI, position: Position +) -> t.Optional[PrepareRenameResult_Type1]: + """ + Prepare for rename operation by checking if the symbol at the position can be renamed. + + Args: + lsp_context: The LSP context + document_uri: The URI of the document + position: The position in the document + + Returns: + PrepareRenameResult if the symbol can be renamed, None otherwise + """ + # Check if there's a CTE at this position + cte_references = get_cte_references(lsp_context, document_uri, position) + if cte_references: + # Find the target CTE definition to get its range + target_range = None + for ref in cte_references: + # Check if cursor is on a CTE usage + if _position_within_range(position, ref.range): + target_range = ref.target_range + break + # Check if cursor is on the CTE definition + elif _position_within_range(position, ref.target_range): + target_range = ref.target_range + break + if target_range: + return PrepareRenameResult_Type1(range=target_range, placeholder="cte_name") + + # For now, only CTEs are supported + return None + + +def rename_symbol( + lsp_context: LSPContext, document_uri: URI, position: Position, new_name: str +) -> t.Optional[WorkspaceEdit]: + """ + Perform rename operation on the symbol at the given position. + + Args: + lsp_context: The LSP context + document_uri: The URI of the document + position: The position in the document + new_name: The new name for the symbol + + Returns: + WorkspaceEdit with the changes, or None if no symbol to rename + """ + # Check if there's a CTE at this position + cte_references = get_cte_references(lsp_context, document_uri, position) + if cte_references: + return _rename_cte(cte_references, new_name) + + # For now, only CTEs are supported + return None + + +def _rename_cte(cte_references: t.List[LSPCteReference], new_name: str) -> WorkspaceEdit: + """ + Create a WorkspaceEdit for renaming a CTE. + + Args: + cte_references: List of CTE references (definition and usages) + new_name: The new name for the CTE + + Returns: + WorkspaceEdit with the text edits for renaming the CTE + """ + changes: t.Dict[str, t.List[TextEdit]] = {} + + for ref in cte_references: + uri = ref.uri + if uri not in changes: + changes[uri] = [] + + # Create a text edit for this reference + text_edit = TextEdit(range=ref.range, new_text=new_name) + changes[uri].append(text_edit) + + return WorkspaceEdit(changes=changes) + + +def get_document_highlights( + lsp_context: LSPContext, document_uri: URI, position: Position +) -> t.Optional[t.List[DocumentHighlight]]: + """ + Get document highlights for all occurrences of the symbol at the given position. + + This function finds all occurrences of a symbol (CTE) within the current document + and returns them as DocumentHighlight objects for "Change All Occurrences" feature. + + Args: + lsp_context: The LSP context + document_uri: The URI of the document + position: The position in the document to find highlights for + + Returns: + List of DocumentHighlight objects or None if no symbol found + """ + # Check if there's a CTE at this position + cte_references = get_cte_references(lsp_context, document_uri, position) + if cte_references: + highlights = [] + for ref in cte_references: + # Determine the highlight kind based on whether it's a definition or usage + kind = ( + DocumentHighlightKind.Write + if ref.range == ref.target_range + else DocumentHighlightKind.Read + ) + + highlights.append(DocumentHighlight(range=ref.range, kind=kind)) + return highlights + + # For now, only CTEs are supported + return None diff --git a/tests/lsp/test_document_highlight.py b/tests/lsp/test_document_highlight.py new file mode 100644 index 0000000000..e6ce0ae7ec --- /dev/null +++ b/tests/lsp/test_document_highlight.py @@ -0,0 +1,111 @@ +from lsprotocol.types import Position, DocumentHighlightKind + +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.rename import get_document_highlights +from sqlmesh.lsp.uri import URI +from tests.lsp.test_reference_cte import find_ranges_from_regex + + +def test_get_document_highlights_cte(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Use the existing customers.sql model which has CTEs + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + test_uri = URI.from_path(sushi_customers_path) + + # Find the ranges for "current_marketing" CTE (not outer one) + ranges = find_ranges_from_regex(read_file, r"current_marketing(?!_outer)") + assert len(ranges) >= 2 # Should have definition + usage + + # Test highlighting CTE definition - position on "current_marketing" definition + position = Position(line=ranges[0].start.line, character=ranges[0].start.character + 4) + highlights = get_document_highlights(lsp_context, test_uri, position) + + assert highlights is not None + assert len(highlights) >= 2 # Definition + at least 1 usage + + # Check that we have both definition (Write) and usage (Read) highlights + highlight_kinds = [h.kind for h in highlights] + assert DocumentHighlightKind.Write in highlight_kinds # CTE definition + assert DocumentHighlightKind.Read in highlight_kinds # CTE usage + + # Test highlighting CTE usage - position on "current_marketing" usage + position = Position(line=ranges[1].start.line, character=ranges[1].start.character + 4) + highlights = get_document_highlights(lsp_context, test_uri, position) + + assert highlights is not None + assert len(highlights) >= 2 # Should find the same references + + +def test_get_document_highlights_no_symbol(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Use the existing customers.sql model + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + test_uri = URI.from_path(sushi_customers_path) + + # Test position not on any CTE symbol - just on a random keyword + position = Position(line=5, character=5) + highlights = get_document_highlights(lsp_context, test_uri, position) + + assert highlights is None + + +def test_get_document_highlights_multiple_ctes(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Use the existing customers.sql model which has both outer and inner CTEs + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + test_uri = URI.from_path(sushi_customers_path) + + # Test the outer CTE - "current_marketing_outer" + outer_ranges = find_ranges_from_regex(read_file, r"current_marketing_outer") + assert len(outer_ranges) >= 2 # Should have definition + usage + + # Test highlighting outer CTE - should only highlight that CTE + position = Position( + line=outer_ranges[0].start.line, character=outer_ranges[0].start.character + 4 + ) + highlights = get_document_highlights(lsp_context, test_uri, position) + + assert highlights is not None + assert len(highlights) == len(outer_ranges) # Should match all occurrences of outer CTE + + # Test the inner CTE - "current_marketing" (not outer) + inner_ranges = find_ranges_from_regex(read_file, r"current_marketing(?!_outer)") + assert len(inner_ranges) >= 2 # Should have definition + usage + + # Test highlighting inner CTE - should only highlight that CTE, not the outer one + position = Position( + line=inner_ranges[0].start.line, character=inner_ranges[0].start.character + 4 + ) + highlights = get_document_highlights(lsp_context, test_uri, position) + + # This should return the column usages as well + assert highlights is not None + assert len(highlights) == 4 diff --git a/tests/lsp/test_rename_cte.py b/tests/lsp/test_rename_cte.py new file mode 100644 index 0000000000..4ca1002c2e --- /dev/null +++ b/tests/lsp/test_rename_cte.py @@ -0,0 +1,212 @@ +from lsprotocol.types import Position +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.rename import prepare_rename, rename_symbol +from sqlmesh.lsp.uri import URI +from tests.lsp.test_reference_cte import find_ranges_from_regex + + +def test_prepare_rename_cte(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Test clicking on CTE definition for "current_marketing" + ranges = find_ranges_from_regex(read_file, r"current_marketing(?!_outer)") + assert len(ranges) == 2 + + # Click on the CTE definition + position = Position(line=ranges[0].start.line, character=ranges[0].start.character + 4) + result = prepare_rename(lsp_context, URI.from_path(sushi_customers_path), position) + + assert result is not None + assert result.placeholder == "cte_name" + assert result.range == ranges[0] # Should return the definition range + + # Test clicking on CTE usage + position = Position(line=ranges[1].start.line, character=ranges[1].start.character + 4) + result = prepare_rename(lsp_context, URI.from_path(sushi_customers_path), position) + + assert result is not None + assert result.placeholder == "cte_name" + assert result.range == ranges[0] # Should still return the definition range + + +def test_prepare_rename_cte_outer(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Test clicking on CTE definition for "current_marketing_outer" + ranges = find_ranges_from_regex(read_file, r"current_marketing_outer") + assert len(ranges) == 2 + + # Click on the CTE definition + position = Position(line=ranges[0].start.line, character=ranges[0].start.character + 4) + result = prepare_rename(lsp_context, URI.from_path(sushi_customers_path), position) + + assert result is not None + assert result.placeholder == "cte_name" + assert result.range == ranges[0] # Should return the definition range + + +def test_prepare_rename_non_cte(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Click on a regular table reference (not a CTE) + ranges = find_ranges_from_regex(read_file, r"sushi\.orders") + assert len(ranges) >= 1 + + position = Position(line=ranges[0].start.line, character=ranges[0].start.character + 4) + result = prepare_rename(lsp_context, URI.from_path(sushi_customers_path), position) + + assert result is None + + +def test_rename_cte(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Test renaming "current_marketing" to "new_marketing" + ranges = find_ranges_from_regex(read_file, r"current_marketing(?!_outer)") + assert len(ranges) == 2 + + # Click on the CTE definition + position = Position(line=ranges[0].start.line, character=ranges[0].start.character + 4) + workspace_edit = rename_symbol( + lsp_context, URI.from_path(sushi_customers_path), position, "new_marketing" + ) + + assert workspace_edit is not None + assert workspace_edit.changes is not None + + uri = URI.from_path(sushi_customers_path).value + assert uri in workspace_edit.changes + + edits = workspace_edit.changes[uri] + + # Should have edited four occurences including column usages + assert len(edits) == 4 + + # Verify that both ranges are being edited + edit_ranges = [edit.range for edit in edits] + for expected_range in ranges: + assert any( + edit_range.start.line == expected_range.start.line + and edit_range.start.character == expected_range.start.character + for edit_range in edit_ranges + ), ( + f"Expected to find edit at line {expected_range.start.line}, char {expected_range.start.character}" + ) + + # Verify that all edits have the new name + assert all(edit.new_text == "new_marketing" for edit in edits) + + # Apply the edits to verify the result + with open(sushi_customers_path, "r", encoding="utf-8") as file: + lines = file.readlines() + + # Apply edits in reverse order to avoid offset issues + sorted_edits = sorted( + edits, key=lambda e: (e.range.start.line, e.range.start.character), reverse=True + ) + for edit in sorted_edits: + line_idx = edit.range.start.line + start_char = edit.range.start.character + end_char = edit.range.end.character + + line = lines[line_idx] + new_line = line[:start_char] + edit.new_text + line[end_char:] + lines[line_idx] = new_line + + # Verify the edited content + edited_content = "".join(lines) + assert "new_marketing" in edited_content + assert "current_marketing" not in edited_content.replace("current_marketing_outer", "") + assert edited_content.count("new_marketing") == 4 + assert ( + " SELECT new_marketing.* FROM new_marketing WHERE new_marketing.customer_id != 100\n" + in lines + ) + + +def test_rename_cte_outer(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + sushi_customers_path = next( + path + for path, info in lsp_context.map.items() + if isinstance(info, ModelTarget) and "sushi.customers" in info.names + ) + + with open(sushi_customers_path, "r", encoding="utf-8") as file: + read_file = file.readlines() + + # Test renaming "current_marketing_outer" to "new_marketing_outer" + ranges = find_ranges_from_regex(read_file, r"current_marketing_outer") + assert len(ranges) == 2 + + # Click on the CTE usage + position = Position(line=ranges[1].start.line, character=ranges[1].start.character + 4) + workspace_edit = rename_symbol( + lsp_context, URI.from_path(sushi_customers_path), position, "new_marketing_outer" + ) + + assert workspace_edit is not None + assert workspace_edit.changes is not None + + uri = URI.from_path(sushi_customers_path).value + assert uri in workspace_edit.changes + + edits = workspace_edit.changes[uri] + assert len(edits) == 2 # Should have 2 edits: definition + usage + + # Verify that both ranges are being edited + edit_ranges = [edit.range for edit in edits] + for expected_range in ranges: + assert any( + edit_range.start.line == expected_range.start.line + and edit_range.start.character == expected_range.start.character + for edit_range in edit_ranges + ), ( + f"Expected to find edit at line {expected_range.start.line}, char {expected_range.start.character}" + ) + + # Verify that all edits have the new name + assert all(edit.new_text == "new_marketing_outer" for edit in edits) diff --git a/vscode/extension/tests/rename_cte.spec.ts b/vscode/extension/tests/rename_cte.spec.ts new file mode 100644 index 0000000000..e1b5da6a7e --- /dev/null +++ b/vscode/extension/tests/rename_cte.spec.ts @@ -0,0 +1,222 @@ +import { test, expect } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { startVSCode, SUSHI_SOURCE_PATH } from './utils' + +// Keyboard shortcuts +const RENAME_KEY = 'F2' +const FIND_ALL_REFERENCES_KEY = + process.platform === 'darwin' ? 'Alt+Shift+F12' : 'Ctrl+Shift+F12' + +// Helper function to set up a test environment +async function setupTestEnvironment() { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const { window, close } = await startVSCode(tempDir) + + // Navigate to customers.sql which contains CTEs + await window.waitForSelector('text=models') + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') + + return { window, close, tempDir } +} + +test.describe('CTE Rename', () => { + test('Rename CTE from definition', async () => { + const { window, close, tempDir } = await setupTestEnvironment() + + try { + // Click on the inner CTE definition "current_marketing" (not the outer one) + await window.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, + }) + + // Press F2 to trigger rename + await window.keyboard.press(RENAME_KEY) + await expect(window.locator('text=Rename')).toBeVisible() + const renameInput = window.locator('input:focus') + await expect(renameInput).toBeVisible() + + // Type new name and confirm + await window.keyboard.type('new_marketing') + await window.keyboard.press('Enter') + await window.waitForTimeout(1000) + + // Verify the rename was applied + await expect(window.locator('text=WITH new_marketing AS')).toBeVisible() + } finally { + await close() + await fs.remove(tempDir) + } + }) + + test('Rename CTE from usage', async () => { + const { window, close, tempDir } = await setupTestEnvironment() + + try { + // Click on CTE usage in FROM clause + await window.locator('text=FROM current_marketing_outer').click({ + position: { x: 80, y: 5 }, + }) + + // Press F2 to trigger rename + await window.keyboard.press(RENAME_KEY) + + // Wait for rename input to appear + await expect(window.locator('text=Rename')).toBeVisible() + const renameInput = window.locator('input:focus') + await expect(renameInput).toBeVisible() + + // Type new name + await window.keyboard.type('updated_marketing_out') + + // Confirm rename + await window.keyboard.press('Enter') + await window.waitForTimeout(1000) + + // Verify both definition and usage were renamed + await expect( + window.locator('text=WITH updated_marketing_out AS'), + ).toBeVisible() + await expect( + window.locator('text=FROM updated_marketing_out'), + ).toBeVisible() + } finally { + await close() + await fs.remove(tempDir) + } + }) + + test('Cancel CTE rename', async () => { + const { window, close, tempDir } = await setupTestEnvironment() + + try { + // Click on the CTE to rename + await window.locator('text=current_marketing_outer').first().click() + + // Press F2 to trigger rename + await window.keyboard.press(RENAME_KEY) + + // Wait for rename input to appear + await expect(window.locator('text=Rename')).toBeVisible() + const renameInput = window.locator('input:focus') + await expect(renameInput).toBeVisible() + + // Type new name but cancel + await window.keyboard.type('cancelled_name') + await window.keyboard.press('Escape') + + // Wait for UI to update + await window.waitForTimeout(500) + + // Verify CTE name was NOT changed + await expect( + window.locator('text=current_marketing_outer').first(), + ).toBeVisible() + await expect(window.locator('text=cancelled_name')).not.toBeVisible() + } finally { + await close() + await fs.remove(tempDir) + } + }) + + test('Rename CTE updates all references', async () => { + const { window, close, tempDir } = await setupTestEnvironment() + + try { + // Click on the CTE definition + await window.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, + }) + + // Press F2 to trigger rename + await window.keyboard.press(RENAME_KEY) + // Wait for rename input to appear + await expect(window.locator('text=Rename')).toBeVisible() + const renameInput = window.locator('input:focus') + await expect(renameInput).toBeVisible() + + // Type new name and confirm + await window.keyboard.type('renamed_cte') + await window.keyboard.press('Enter') + + // Click on the renamed CTE + await window.locator('text=WITH renamed_cte AS').click({ + position: { x: 100, y: 5 }, + }) + + // Find all references using keyboard shortcut + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + + // Verify references panel shows all occurrences + await window.waitForSelector('text=References') + await expect(window.locator('text=customers.sql').first()).toBeVisible() + await window.waitForSelector('text=WITH renamed_cte AS') + await window.waitForSelector('text=renamed_cte.*') + await window.waitForSelector('text=FROM renamed_cte') + await window.waitForSelector('text=renamed_cte.customer_id != 100') + } finally { + await close() + await fs.remove(tempDir) + } + }) + + test('Rename CTE with preview', async () => { + const { window, close, tempDir } = await setupTestEnvironment() + + try { + // Click on the CTE to rename + await window.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, + }) + + // Press F2 to trigger rename + await window.keyboard.press(RENAME_KEY) + await expect(window.locator('text=Rename')).toBeVisible() + const renameInput = window.locator('input:focus') + await expect(renameInput).toBeVisible() + + // Type new name + await window.keyboard.type('preview_marketing') + + // Press Cmd+Enter (Meta+Enter) to preview changes + await window.keyboard.press('Meta+Enter') + + // Verify preview UI is showing + await expect( + window.locator('text=Refactor Preview').first(), + ).toBeVisible() + await expect(window.locator('text=Apply').first()).toBeVisible() + await expect(window.locator('text=Discard').first()).toBeVisible() + + // Verify the preview shows both old and new names + await expect( + window.locator('text=current_marketing').first(), + ).toBeVisible() + await expect( + window.locator('text=preview_marketing').first(), + ).toBeVisible() + + // Apply the changes + await window.locator('text=Apply').click() + + // Verify the rename was applied + await expect( + window.locator('text=WITH preview_marketing AS'), + ).toBeVisible() + } finally { + await close() + await fs.remove(tempDir) + } + }) +}) From 118e7e9463361ab81a45983947560544a3b1f6df Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:28:27 +0300 Subject: [PATCH 0433/1056] Chore: do not log test results if there are no tests (#4785) --- sqlmesh/core/console.py | 12 ++++++++++++ tests/core/test_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index cc112e6ebd..6b600dcd73 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -1954,6 +1954,10 @@ def _prompt_promote(self, plan_builder: PlanBuilder) -> None: plan_builder.apply() def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None: + # We don't log the test results if no tests were ran + if not result.testsRun: + return + divider_length = 70 self._log_test_details(result) @@ -2827,6 +2831,10 @@ def radio_button_selected(change: t.Dict[str, t.Any]) -> None: self.display(radio) def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None: + # We don't log the test results if no tests were ran + if not result.testsRun: + return + import ipywidgets as widgets divider_length = 70 @@ -3206,6 +3214,10 @@ def log_success(self, message: str) -> None: self._print(message) def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None: + # We don't log the test results if no tests were ran + if not result.testsRun: + return + message = f"Ran `{result.testsRun}` Tests Against `{target_dialect}`" if result.wasSuccessful(): diff --git a/tests/core/test_test.py b/tests/core/test_test.py index d803f8bdc9..f4d495801a 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -2685,6 +2685,10 @@ def test_model_test_text_result_reporting_no_traceback( else: result.addFailure(test, (e.__class__, e, e.__traceback__)) + # Since we're simulating an error/failure, this doesn't go through the + # test runner logic, so we need to manually set how many tests were ran + result.testsRun = 1 + with capture_output() as captured_output: get_console().log_test_results(result, "duckdb") @@ -2729,3 +2733,23 @@ def test_timestamp_normalization() -> None: context=Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))), ).run() ) + + +@use_terminal_console +def test_disable_test_logging_if_no_tests_found(mocker: MockerFixture, tmp_path: Path) -> None: + init_example_project(tmp_path, dialect="duckdb") + + config = Config( + default_connection=DuckDBConnectionConfig(), + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_test_connection=DuckDBConnectionConfig(concurrent_tasks=8), + ) + + rmtree(tmp_path / "tests") + + with capture_output() as captured_output: + context = Context(paths=tmp_path, config=config) + context.plan(no_prompts=True, auto_apply=True) + + output = captured_output.stdout + assert "test" not in output.lower() From 254ca388ceedfa159998e73d2a9a8ecbf9904874 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:05:40 +0200 Subject: [PATCH 0434/1056] fix(vscode): passing any lineage error through (#4786) --- vscode/extension/src/lsp/custom.ts | 19 +++++---- vscode/extension/src/lsp/lsp.ts | 3 ++ vscode/extension/src/webviews/lineagePanel.ts | 41 ++++++++++++++++--- vscode/extension/tests/broken_project.spec.ts | 36 ++++++++++++++++ vscode/react/src/pages/lineage.tsx | 10 ++++- 5 files changed, 95 insertions(+), 14 deletions(-) diff --git a/vscode/extension/src/lsp/custom.ts b/vscode/extension/src/lsp/custom.ts index be11419b79..7a9de4ca6f 100644 --- a/vscode/extension/src/lsp/custom.ts +++ b/vscode/extension/src/lsp/custom.ts @@ -14,7 +14,7 @@ interface RenderModelRequest { textDocumentUri: string } -interface RenderModelResponse { +interface RenderModelResponse extends BaseResponse { models: RenderModelEntry[] } @@ -25,7 +25,6 @@ export interface RenderModelEntry { rendered_query: string } -// @eslint-disable-next-line @typescript-eslint/consistent-type-definition export type CustomLSPMethods = | AllModelsMethod | AbstractAPICall @@ -40,7 +39,7 @@ interface AllModelsRequest { } } -interface AllModelsResponse { +interface AllModelsResponse extends BaseResponse { models: string[] keywords: string[] } @@ -55,9 +54,11 @@ export interface AbstractAPICallRequest { export interface AbstractAPICall { method: 'sqlmesh/api' request: AbstractAPICallRequest - response: object + response: AbstractAPICallResponse } +type AbstractAPICallResponse = object & BaseResponse + export interface AllModelsForRenderMethod { method: 'sqlmesh/all_models_for_render' request: AllModelsForRenderRequest @@ -67,7 +68,7 @@ export interface AllModelsForRenderMethod { // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface AllModelsForRenderRequest {} -interface AllModelsForRenderResponse { +interface AllModelsForRenderResponse extends BaseResponse { models: ModelForRendering[] } @@ -87,7 +88,7 @@ export interface SupportedMethodsMethod { // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface SupportedMethodsRequest {} -interface SupportedMethodsResponse { +interface SupportedMethodsResponse extends BaseResponse { methods: CustomMethod[] } @@ -105,4 +106,8 @@ export interface FormatProjectMethod { interface FormatProjectRequest {} // eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface FormatProjectResponse {} +interface FormatProjectResponse extends BaseResponse {} + +interface BaseResponse { + response_error?: string +} diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 9432762aed..769a0b3753 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -256,6 +256,9 @@ export class LSPClient implements Disposable { try { const result = await this.client.sendRequest(method, request) + if (result.response_error) { + return err(result.response_error) + } return ok(result) } catch (error) { traceError( diff --git a/vscode/extension/src/webviews/lineagePanel.ts b/vscode/extension/src/webviews/lineagePanel.ts index 5bb3e1693f..ee05112a64 100644 --- a/vscode/extension/src/webviews/lineagePanel.ts +++ b/vscode/extension/src/webviews/lineagePanel.ts @@ -10,6 +10,7 @@ import { } from 'vscode' import { getWorkspaceFolders } from '../utilities/common/vscodeapi' import { LSPClient } from '../lsp/lsp' +import { isErr } from '@bus/result' export class LineagePanel implements WebviewViewProvider, Disposable { public static readonly viewType = 'sqlmesh.lineage' @@ -97,12 +98,40 @@ export class LineagePanel implements WebviewViewProvider, Disposable { 'sqlmesh/api', payload.params, ) - const responseCallback: CallbackEvent = { - key: 'rpcResponse', - payload: { - requestId, - result: response, - }, + let responseCallback: CallbackEvent + if (isErr(response)) { + let errorMessage: string + switch (response.error.type) { + case 'generic': + errorMessage = response.error.message + break + case 'invalid_state': + errorMessage = `Invalid state: ${response.error.message}` + break + case 'sqlmesh_outdated': + errorMessage = `SQLMesh version issue: ${response.error.message}` + break + default: + errorMessage = 'Unknown error' + } + responseCallback = { + key: 'rpcResponse', + payload: { + requestId, + result: { + ok: false, + error: errorMessage, + }, + }, + } + } else { + responseCallback = { + key: 'rpcResponse', + payload: { + requestId, + result: response, + }, + } } await webviewView.webview.postMessage(responseCallback) break diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index eff1b7cb02..d2c7212a51 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -92,3 +92,39 @@ test('bad project, double model, then fixed', async ({}) => { await fs.remove(tempDir) } }) + +test('bad project, double model, check lineage', async ({}) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Read the customers.sql file + const customersSql = await fs.readFile( + path.join(tempDir, 'models', 'customers.sql'), + 'utf8', + ) + + // Write the customers.sql file with a double model + await fs.writeFile( + path.join(tempDir, 'models', 'customers_duplicated.sql'), + customersSql, + ) + + const { window, close } = await startVSCode(tempDir) + try { + await window.waitForSelector('text=models') + + // Open the lineage view + await openLineageView(window) + + await window.waitForSelector('text=Error creating context') + + await window.waitForSelector('text=Error:') + + await window.waitForTimeout(1000) + } finally { + await close() + await fs.remove(tempDir) + } +}) diff --git a/vscode/react/src/pages/lineage.tsx b/vscode/react/src/pages/lineage.tsx index 0bb7e1a7aa..2aef1e6525 100644 --- a/vscode/react/src/pages/lineage.tsx +++ b/vscode/react/src/pages/lineage.tsx @@ -73,7 +73,11 @@ function Lineage() { const { on } = useEventBus() const queryClient = useQueryClient() - const { data: models, isLoading: isLoadingModels } = useApiModels() + const { + data: models, + isLoading: isLoadingModels, + error: modelsError, + } = useApiModels() const rpc = useRpc() React.useEffect(() => { const fetchFirstTimeModelIfNotSet = async ( @@ -143,6 +147,10 @@ function Lineage() { } }, [on, queryClient, modelsRecord]) + if (modelsError) { + return

+ } + if ( isLoadingModels || models === undefined || From 28127d4b522d0f20bc79260c53125d46bcd4e939 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:10:33 +0200 Subject: [PATCH 0435/1056] fix(lsp): return context error in case it failed (#4787) --- sqlmesh/lsp/main.py | 141 +++++++++++++----- vscode/extension/tests/broken_project.spec.ts | 64 +++++++- 2 files changed, 168 insertions(+), 37 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 7462698f7d..bbb0f77242 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -56,6 +56,33 @@ from sqlmesh.lsp.uri import URI from web.server.api.endpoints.lineage import column_lineage, model_lineage from web.server.api.endpoints.models import get_models +from typing import Union +from dataclasses import dataclass + + +@dataclass +class NoContext: + """State when no context has been attempted to load.""" + + pass + + +@dataclass +class ContextLoaded: + """State when context has been successfully loaded.""" + + lsp_context: LSPContext + + +@dataclass +class ContextFailed: + """State when context failed to load with an error message.""" + + error_message: str + context: t.Optional[Context] = None + + +ContextState = Union[NoContext, ContextLoaded, ContextFailed] class SQLMeshLanguageServer: @@ -72,7 +99,7 @@ def __init__( """ self.server = LanguageServer(server_name, version) self.context_class = context_class - self.lsp_context: t.Optional[LSPContext] = None + self.context_state: ContextState = NoContext() self.workspace_folders: t.List[Path] = [] self.has_raised_loading_error: bool = False @@ -257,16 +284,53 @@ def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> Non @self.server.feature(types.TEXT_DOCUMENT_DID_SAVE) def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> None: uri = URI(params.text_document.uri) - if self.lsp_context is None: + if isinstance(self.context_state, NoContext): return - context = self.lsp_context.context - context.load() - self.lsp_context = LSPContext(context) + if isinstance(self.context_state, ContextFailed): + if self.context_state.context: + try: + self.context_state.context.load() + self.context_state = ContextLoaded( + lsp_context=LSPContext(self.context_state.context) + ) + except Exception as e: + ls.log_trace(f"Error loading context: {e}") + if not isinstance(self.context_state, ContextFailed): + raise Exception("Context state should be failed") + self.context_state = ContextFailed( + error_message=str(e), context=self.context_state.context + ) + return + else: + # If there's no context, try to create one from scratch + try: + self._ensure_context_for_document(uri) + # If successful, context_state will be ContextLoaded + if isinstance(self.context_state, ContextLoaded): + ls.show_message( + "Successfully loaded SQLMesh context", + types.MessageType.Info, + ) + except Exception as e: + ls.log_trace(f"Still cannot load context: {e}") + return + + # Reload the context if was successfully + try: + context = self.context_state.lsp_context.context + context.load() + self.context_state = ContextLoaded(lsp_context=LSPContext(context)) + except Exception as e: + ls.log_trace(f"Error loading context: {e}") + self.context_state = ContextFailed( + error_message=str(e), context=self.context_state.lsp_context.context + ) + return # Only publish diagnostics if client doesn't support pull diagnostics if not self.client_supports_pull_diagnostics: - diagnostics = self.lsp_context.lint_model(uri) + diagnostics = self.context_state.lsp_context.lint_model(uri) ls.publish_diagnostics( params.text_document.uri, SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), @@ -443,11 +507,8 @@ def prepare_rename_handler( """Prepare for rename operation by checking if the symbol can be renamed.""" try: uri = URI(params.text_document.uri) - self._ensure_context_for_document(uri) - if self.lsp_context is None: - raise RuntimeError(f"No context found for document: {uri}") - - result = prepare_rename(self.lsp_context, uri, params.position) + context = self._context_get_or_load(uri) + result = prepare_rename(context, uri, params.position) return result except Exception as e: ls.log_trace(f"Error preparing rename: {e}") @@ -460,13 +521,8 @@ def rename_handler( """Perform rename operation on the symbol at the given position.""" try: uri = URI(params.text_document.uri) - self._ensure_context_for_document(uri) - if self.lsp_context is None: - raise RuntimeError(f"No context found for document: {uri}") - - workspace_edit = rename_symbol( - self.lsp_context, uri, params.position, params.new_name - ) + context = self._context_get_or_load(uri) + workspace_edit = rename_symbol(context, uri, params.position, params.new_name) return workspace_edit except Exception as e: ls.show_message(f"Error performing rename: {e}", types.MessageType.Error) @@ -479,11 +535,8 @@ def document_highlight_handler( """Highlight all occurrences of the symbol at the given position.""" try: uri = URI(params.text_document.uri) - self._ensure_context_for_document(uri) - if self.lsp_context is None: - raise RuntimeError(f"No context found for document: {uri}") - - highlights = get_document_highlights(self.lsp_context, uri, params.position) + context = self._context_get_or_load(uri) + highlights = get_document_highlights(context, uri, params.position) return highlights except Exception as e: ls.log_trace(f"Error getting document highlights: {e}") @@ -670,11 +723,13 @@ def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic] return [], 0 def _context_get_or_load(self, document_uri: t.Optional[URI] = None) -> LSPContext: - if self.lsp_context is None: + if isinstance(self.context_state, ContextFailed): + raise RuntimeError(self.context_state.error_message) + if isinstance(self.context_state, NoContext): self._ensure_context_for_document(document_uri) - if self.lsp_context is None: - raise RuntimeError("No context found able to get or load") - return self.lsp_context + if not isinstance(self.context_state, ContextLoaded): + raise RuntimeError("Context is not loaded") + return self.context_state.lsp_context def _ensure_context_for_document( self, @@ -692,10 +747,10 @@ def _ensure_context_for_document( self._ensure_context_in_folder(document_folder) return - return self._ensure_context_in_folder() + self._ensure_context_in_folder() def _ensure_context_in_folder(self, folder_path: t.Optional[Path] = None) -> None: - if self.lsp_context is not None: + if not isinstance(self.context_state, NoContext): return # If not found in the provided folder, search through all workspace folders @@ -729,7 +784,7 @@ def _ensure_context_in_folder(self, folder_path: t.Optional[Path] = None) -> Non def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: """Create a new LSPContext instance using the configured context class. - On success, sets self.lsp_context and returns the created context. + On success, sets self.context_state to ContextLoaded and returns the created context. Args: paths: List of paths to pass to the context constructor @@ -738,14 +793,22 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: A new LSPContext instance wrapping the created context, or None if creation fails """ try: - if self.lsp_context is None: + if isinstance(self.context_state, NoContext): context = self.context_class(paths=paths) loaded_sqlmesh_message(self.server, paths[0]) + elif isinstance(self.context_state, ContextFailed): + if self.context_state.context: + context = self.context_state.context + context.load() + else: + # If there's no context (initial creation failed), try creating again + context = self.context_class(paths=paths) + loaded_sqlmesh_message(self.server, paths[0]) else: - self.lsp_context.context.load() - context = self.lsp_context.context - self.lsp_context = LSPContext(context) - return self.lsp_context + context = self.context_state.lsp_context.context + context.load() + self.context_state = ContextLoaded(lsp_context=LSPContext(context)) + return self.context_state.lsp_context except Exception as e: # Only show the error message once if not self.has_raised_loading_error: @@ -756,6 +819,14 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: self.has_raised_loading_error = True self.server.log_trace(f"Error creating context: {e}") + # Store the error in context state so subsequent requests show the actual error + # Try to preserve any partially loaded context if it exists + context = None + if isinstance(self.context_state, ContextLoaded): + context = self.context_state.lsp_context.context + elif isinstance(self.context_state, ContextFailed) and self.context_state.context: + context = self.context_state.context + self.context_state = ContextFailed(error_message=str(e), context=context) return None @staticmethod diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index d2c7212a51..6d49b3e80a 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -45,6 +45,66 @@ test('bad project, double model', async ({}) => { } }) +test('working project, then broken through adding double model, then refixed', async ({}) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + const { window, close } = await startVSCode(tempDir) + try { + // First, verify the project is working correctly + await window.waitForSelector('text=models') + + // Open the lineage view to confirm it loads properly + await openLineageView(window) + await window.waitForSelector('text=Loaded SQLMesh context') + + // Read the customers.sql file + const customersSql = await fs.readFile( + path.join(tempDir, 'models', 'customers.sql'), + 'utf8', + ) + + // Add a duplicate model to break the project + await fs.writeFile( + path.join(tempDir, 'models', 'customers_duplicated.sql'), + customersSql, + ) + + // Open the customers model to trigger the error + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + // Save to refresh the context + await window.keyboard.press('Control+S') + await window.keyboard.press('Meta+S') + + // Wait for the error to appear + // TODO: Selector doesn't work in the linage view + // await window.waitForSelector('text=Error') + + // Remove the duplicated model to fix the project + await fs.remove(path.join(tempDir, 'models', 'customers_duplicated.sql')) + + // Save again to refresh the context + await window.keyboard.press('Control+S') + await window.keyboard.press('Meta+S') + + // Wait for the error to go away and context to reload + // TODO: Selector doesn't work in the linage view + // await window.waitForSelector('text=raw.demographics') + } finally { + await close() + await fs.remove(tempDir) + } +}) + test('bad project, double model, then fixed', async ({}) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), @@ -86,7 +146,8 @@ test('bad project, double model, then fixed', async ({}) => { await openLineageView(window) // Wait for the error to go away - await window.waitForSelector('text=Loaded SQLMesh context') + // TODO: Selector doesn't work in the linage view + // await window.waitForSelector('text=raw.demographics') } finally { await close() await fs.remove(tempDir) @@ -119,7 +180,6 @@ test('bad project, double model, check lineage', async ({}) => { await openLineageView(window) await window.waitForSelector('text=Error creating context') - await window.waitForSelector('text=Error:') await window.waitForTimeout(1000) From bd2caa97904d5ae73660a89665083c553c890166 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:05:35 -0500 Subject: [PATCH 0436/1056] Feat: interactive sqlmesh init (#4726) Co-authored-by: Sung Won Chung --- docs/guides/configuration.md | 97 +++- docs/reference/notebook.md | 4 +- sqlmesh/cli/example_project.py | 338 ------------ sqlmesh/cli/main.py | 86 ++- sqlmesh/cli/project_init.py | 497 ++++++++++++++++++ sqlmesh/core/config/connection.py | 73 +++ sqlmesh/integrations/dlt.py | 8 +- sqlmesh/magics.py | 16 +- sqlmesh/utils/yaml.py | 2 + tests/cli/test_cli.py | 341 +++++++++--- tests/cli/test_integration_cli.py | 4 +- .../integration/test_integration.py | 4 +- .../integration/test_integration_bigquery.py | 4 +- tests/core/state_sync/test_export_import.py | 16 +- tests/core/test_config.py | 44 +- tests/core/test_connection_config.py | 14 + tests/core/test_context.py | 4 +- tests/core/test_integration.py | 4 +- tests/core/test_loader.py | 14 +- tests/core/test_model.py | 48 +- tests/core/test_test.py | 28 +- tests/integrations/jupyter/test_magics.py | 2 +- .../lsp/test_reference_model_column_prefix.py | 6 +- 23 files changed, 1137 insertions(+), 517 deletions(-) delete mode 100644 sqlmesh/cli/example_project.py create mode 100644 sqlmesh/cli/project_init.py diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 901d35e6b2..df8fd9e3f4 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -151,6 +151,55 @@ The examples specify a Snowflake connection whose password is stored in an envir ) ``` +#### Default target environment + +The SQLMesh `plan` command acts on the `prod` environment by default (i.e., `sqlmesh plan` is equivalent to `sqlmesh plan prod`). + +In some organizations, users never run plans directly against `prod` - they do all SQLMesh work in a development environment unique to them. In a standard SQLMesh configuration, this means they need to include their development environment name every time they issue the `plan` command (e.g., `sqlmesh plan dev_tony`). + +If your organization works like this, it may be convenient to change the `plan` command's default environment from `prod` to each user's development environment. That way people can issue `sqlmesh plan` without typing the environment name every time. + +The SQLMesh configuration `user()` function returns the name of the user currently logged in and running SQLMesh. It retrieves the username from system environment variables like `USER` on MacOS/Linux or `USERNAME` on Windows. + +Call `user()` inside Jinja curly braces with the syntax `{{ user() }}`, which allows you to combine the user name with a prefix or suffix. + +The example configuration below constructs the environment name by appending the username to the end of the string `dev_`. If the user running SQLMesh is `tony`, the default target environment when they run SQLMesh will be `dev_tony`. In other words, `sqlmesh plan` will be equivalent to `sqlmesh plan dev_tony`. + +=== "YAML" + + Default target environment is `dev_` combined with the username running SQLMesh. + + ```yaml + default_target_environment: dev_{{ user() }} + ``` + +=== "Python" + + Default target environment is `dev_` combined with the username running SQLMesh. + + Retrieve the username with the `getpass.getuser()` function, and combine it with `dev_` in a Python f-string. + + ```python linenums="1" hl_lines="1 17" + import getpass + import os + from sqlmesh.core.config import ( + Config, + ModelDefaultsConfig, + GatewayConfig, + SnowflakeConnectionConfig + ) + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + gateways={ + "my_gateway": GatewayConfig( + connection=DuckDBConnectionConfig(), + ), + }, + default_target_environment=f"dev_{getpass.getuser()}", + ) + ``` + ### Overrides Environment variables have the highest precedence among configuration methods, as [noted above](#configuration-files). They will automatically override configuration file specifications if they follow a specific naming structure. @@ -488,15 +537,15 @@ SELECT 2 AS col └── Directly Modified: └── sqlmesh_example__dev.test_model - --- - +++ - - - kind FULL - ) - SELECT - - 1 AS col - + 2 AS col + --- + +++ + + + kind FULL + ) + SELECT + - 1 AS col + + 2 AS col ``` 3. Second (metadata) change in `dev`: @@ -516,27 +565,27 @@ SELECT 5 AS col └── Directly Modified: └── sqlmesh_example__dev.test_model - --- - - +++ - - @@ -1,8 +1,9 @@ - - MODEL ( - name sqlmesh_example.test_model, - + owner "John Doe", - kind FULL - ) - SELECT - - 1 AS col - + 2 AS col + --- + + +++ + + @@ -1,8 +1,9 @@ + + MODEL ( + name sqlmesh_example.test_model, + + owner "John Doe", + kind FULL + ) + SELECT + - 1 AS col + + 2 AS col Directly Modified: sqlmesh_example__dev.test_model (Breaking) Models needing backfill: └── sqlmesh_example__dev.test_model: [full refresh] ``` -Even though the second change should have been a metadata change (thus not requiring a backfill), it will still be classified as a breaking change because the comparison is against production instead of the previous development state. This is intentional and may cause additional backfills as more changes are accumulated. +Even though the second change should have been a metadata change (thus not requiring a backfill), it will still be classified as a breaking change because the comparison is against production instead of the previous development state. This is intentional and may cause additional backfills as more changes are accumulated. ### Gateways diff --git a/docs/reference/notebook.md b/docs/reference/notebook.md index df64b9bb71..47c8731130 100644 --- a/docs/reference/notebook.md +++ b/docs/reference/notebook.md @@ -29,9 +29,9 @@ import sqlmesh If desired, you can create the [quickstart example project](../quick_start.md) with the Python `init_example_project` function. The function requires a default SQL dialect for the project's models; this example uses `snowflake`: ```python -from sqlmesh.cli.example_project import init_example_project +from sqlmesh.cli.project_init import init_example_project -init_example_project("path_to_project_directory", dialect="snowflake") +init_example_project("path_to_project_directory", engine_type="snowflake") ``` Alternatively, create the project with a notebook magic: diff --git a/sqlmesh/cli/example_project.py b/sqlmesh/cli/example_project.py deleted file mode 100644 index ba674ad5e5..0000000000 --- a/sqlmesh/cli/example_project.py +++ /dev/null @@ -1,338 +0,0 @@ -import typing as t -from enum import Enum -from pathlib import Path -from dataclasses import dataclass - -import click -from sqlglot import Dialect -from sqlmesh.integrations.dlt import generate_dlt_models_and_settings -from sqlmesh.utils.date import yesterday_ds - -from sqlmesh.core.config.connection import CONNECTION_CONFIG_TO_TYPE - - -PRIMITIVES = (str, int, bool, float) - - -class ProjectTemplate(Enum): - DBT = "dbt" - DLT = "dlt" - DEFAULT = "default" - EMPTY = "empty" - - -def _gen_config( - dialect: t.Optional[str], - settings: t.Optional[str], - start: t.Optional[str], - template: ProjectTemplate, -) -> str: - connection_settings = ( - settings - or """ type: duckdb - database: db.db""" - ) - - if not settings and template != ProjectTemplate.DBT: - doc_link = "https://sqlmesh.readthedocs.io/en/stable/integrations/engines{engine_link}" - engine_link = "" - - engine = "mssql" if dialect == "tsql" else dialect - - if engine in CONNECTION_CONFIG_TO_TYPE: - required_fields = [] - non_required_fields = [] - - for name, field in CONNECTION_CONFIG_TO_TYPE[engine].model_fields.items(): - field_name = field.alias or name - default_value = field.get_default() - - if isinstance(default_value, Enum): - default_value = default_value.value - elif not isinstance(default_value, PRIMITIVES): - default_value = "" - - required = field.is_required() or field_name == "type" - option_str = f" {'# ' if not required else ''}{field_name}: {default_value}\n" - - # specify the DuckDB database field so quickstart runs out of the box - if engine == "duckdb" and field_name == "database": - option_str = " database: db.db\n" - required = True - - if required: - required_fields.append(option_str) - else: - non_required_fields.append(option_str) - - connection_settings = "".join(required_fields + non_required_fields) - - engine_link = f"/{engine}/#connection-options" - - connection_settings = ( - " # For more information on configuring the connection to your execution engine, visit:\n" - " # https://sqlmesh.readthedocs.io/en/stable/reference/configuration/#connections\n" - f" # {doc_link.format(engine_link=engine_link)}\n{connection_settings}" - ) - - default_configs = { - ProjectTemplate.DEFAULT: f"""gateways: - {dialect}: - connection: -{connection_settings} - -default_gateway: {dialect} - -model_defaults: - dialect: {dialect} - start: {start or yesterday_ds()} -""", - ProjectTemplate.DBT: """from pathlib import Path - -from sqlmesh.dbt.loader import sqlmesh_config - -config = sqlmesh_config(Path(__file__).parent) -""", - } - - default_configs[ProjectTemplate.EMPTY] = default_configs[ProjectTemplate.DEFAULT] - default_configs[ProjectTemplate.DLT] = default_configs[ProjectTemplate.DEFAULT] - return default_configs[template] - - -@dataclass -class ExampleObjects: - schema_name: str - full_model_name: str - full_model_def: str - incremental_model_name: str - incremental_model_def: str - seed_model_name: str - seed_model_def: str - seed_data: str - audit_def: str - test_def: str - - def models(self) -> t.Set[t.Tuple[str, str]]: - return { - (self.full_model_name, self.full_model_def), - (self.incremental_model_name, self.incremental_model_def), - (self.seed_model_name, self.seed_model_def), - } - - -def _gen_example_objects(schema_name: str) -> ExampleObjects: - full_model_name = f"{schema_name}.full_model" - incremental_model_name = f"{schema_name}.incremental_model" - seed_model_name = f"{schema_name}.seed_model" - - full_model_def = f"""MODEL ( - name {full_model_name}, - kind FULL, - cron '@daily', - grain item_id, - audits (assert_positive_order_ids), -); - -SELECT - item_id, - COUNT(DISTINCT id) AS num_orders, -FROM - {incremental_model_name} -GROUP BY item_id - """ - - incremental_model_def = f"""MODEL ( - name {incremental_model_name}, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column event_date - ), - start '2020-01-01', - cron '@daily', - grain (id, event_date) -); - -SELECT - id, - item_id, - event_date, -FROM - {seed_model_name} -WHERE - event_date BETWEEN @start_date AND @end_date - """ - - seed_model_def = f"""MODEL ( - name {seed_model_name}, - kind SEED ( - path '../seeds/seed_data.csv' - ), - columns ( - id INTEGER, - item_id INTEGER, - event_date DATE - ), - grain (id, event_date) -); - """ - - audit_def = """AUDIT ( - name assert_positive_order_ids, -); - -SELECT * -FROM @this_model -WHERE - item_id < 0 - """ - - seed_data = """id,item_id,event_date -1,2,2020-01-01 -2,1,2020-01-01 -3,3,2020-01-03 -4,1,2020-01-04 -5,1,2020-01-05 -6,1,2020-01-06 -7,1,2020-01-07 -""" - - test_def = f"""test_example_full_model: - model: {full_model_name} - inputs: - {incremental_model_name}: - rows: - - id: 1 - item_id: 1 - - id: 2 - item_id: 1 - - id: 3 - item_id: 2 - outputs: - query: - rows: - - item_id: 1 - num_orders: 2 - - item_id: 2 - num_orders: 1 - """ - - return ExampleObjects( - schema_name=schema_name, - full_model_name=full_model_name, - full_model_def=full_model_def, - incremental_model_name=incremental_model_name, - incremental_model_def=incremental_model_def, - seed_model_name=seed_model_name, - seed_model_def=seed_model_def, - seed_data=seed_data, - audit_def=audit_def, - test_def=test_def, - ) - - -def init_example_project( - path: t.Union[str, Path], - dialect: t.Optional[str], - template: ProjectTemplate = ProjectTemplate.DEFAULT, - pipeline: t.Optional[str] = None, - dlt_path: t.Optional[str] = None, - schema_name: str = "sqlmesh_example", -) -> None: - root_path = Path(path) - config_extension = "py" if template == ProjectTemplate.DBT else "yaml" - config_path = root_path / f"config.{config_extension}" - audits_path = root_path / "audits" - macros_path = root_path / "macros" - models_path = root_path / "models" - seeds_path = root_path / "seeds" - tests_path = root_path / "tests" - - if config_path.exists(): - raise click.ClickException(f"Found an existing config in '{config_path}'") - - if not dialect and template != ProjectTemplate.DBT: - raise click.ClickException( - "Default SQL dialect is a required argument for SQLMesh projects" - ) - - models: t.Set[t.Tuple[str, str]] = set() - settings = None - start = None - if template == ProjectTemplate.DLT: - if pipeline and dialect: - models, settings, start = generate_dlt_models_and_settings( - pipeline_name=pipeline, dialect=dialect, dlt_path=dlt_path - ) - else: - raise click.ClickException( - "DLT pipeline is a required argument to generate a SQLMesh project from DLT" - ) - - _create_config(config_path, dialect, settings, start, template) - if template == ProjectTemplate.DBT: - return - - _create_folders([audits_path, macros_path, models_path, seeds_path, tests_path]) - - if template == ProjectTemplate.DLT: - _create_models(models_path, models) - return - - example_objects = _gen_example_objects(schema_name=schema_name) - - if template != ProjectTemplate.EMPTY: - _create_macros(macros_path) - _create_audits(audits_path, example_objects) - _create_models(models_path, example_objects.models()) - _create_seeds(seeds_path, example_objects) - _create_tests(tests_path, example_objects) - - -def _create_folders(target_folders: t.Sequence[Path]) -> None: - for folder_path in target_folders: - folder_path.mkdir(exist_ok=True) - (folder_path / ".gitkeep").touch() - - -def _create_config( - config_path: Path, - dialect: t.Optional[str], - settings: t.Optional[str], - start: t.Optional[str], - template: ProjectTemplate, -) -> None: - if dialect: - Dialect.get_or_raise(dialect) - - project_config = _gen_config(dialect, settings, start, template) - - _write_file( - config_path, - project_config, - ) - - -def _create_macros(macros_path: Path) -> None: - (macros_path / "__init__.py").touch() - - -def _create_audits(audits_path: Path, example_objects: ExampleObjects) -> None: - _write_file(audits_path / "assert_positive_order_ids.sql", example_objects.audit_def) - - -def _create_models(models_path: Path, models: t.Set[t.Tuple[str, str]]) -> None: - for model_name, model_def in models: - _write_file(models_path / f"{model_name.split('.')[-1]}.sql", model_def) - - -def _create_seeds(seeds_path: Path, example_objects: ExampleObjects) -> None: - _write_file(seeds_path / "seed_data.csv", example_objects.seed_data) - - -def _create_tests(tests_path: Path, example_objects: ExampleObjects) -> None: - _write_file(tests_path / "test_full_model.yaml", example_objects.test_def) - - -def _write_file(path: Path, payload: str) -> None: - with open(path, "w", encoding="utf-8") as fd: - fd.write(payload) diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index b4d4537712..4f343c731f 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -6,11 +6,15 @@ import typing as t import click - from sqlmesh import configure_logging, remove_excess_logs from sqlmesh.cli import error_handler from sqlmesh.cli import options as opt -from sqlmesh.cli.example_project import ProjectTemplate, init_example_project +from sqlmesh.cli.project_init import ( + ProjectTemplate, + init_example_project, + InitCliMode, + interactive_init, +) from sqlmesh.core.analytics import cli_analytics from sqlmesh.core.console import configure_console, get_console from sqlmesh.utils import Verbosity @@ -22,6 +26,7 @@ logger = logging.getLogger(__name__) + SKIP_LOAD_COMMANDS = ( "clean", "create_external_models", @@ -138,7 +143,7 @@ def cli( @cli.command("init") -@click.argument("sql_dialect", required=False) +@click.argument("engine", required=False) @click.option( "-t", "--template", @@ -160,24 +165,83 @@ def cli( @cli_analytics def init( ctx: click.Context, - sql_dialect: t.Optional[str] = None, + engine: t.Optional[str] = None, template: t.Optional[str] = None, dlt_pipeline: t.Optional[str] = None, dlt_path: t.Optional[str] = None, ) -> None: """Create a new SQLMesh repository.""" - try: - project_template = ProjectTemplate(template.lower() if template else "default") - except ValueError: - raise click.ClickException(f"Invalid project template '{template}'") - init_example_project( - ctx.obj, - dialect=sql_dialect, + project_template = None + if template: + try: + project_template = ProjectTemplate(template.lower()) + except ValueError: + template_strings = "', '".join([template.value for template in ProjectTemplate]) + raise click.ClickException( + f"Invalid project template '{template}'. Please specify one of '{template_strings}'." + ) + + if engine or project_template == ProjectTemplate.DBT: + init_example_project( + path=ctx.obj, + template=project_template or ProjectTemplate.DEFAULT, + engine_type=engine, + pipeline=dlt_pipeline, + dlt_path=dlt_path, + ) + return + + import sqlmesh.utils.rich as srich + + console = srich.console + + project_template, engine_type, cli_mode = interactive_init(ctx.obj, console, project_template) + + config_path = init_example_project( + path=ctx.obj, template=project_template, + engine_type=engine_type, + cli_mode=cli_mode or InitCliMode.DEFAULT, pipeline=dlt_pipeline, dlt_path=dlt_path, ) + engine_install_text = "" + if engine_type and engine_type not in ("duckdb", "motherduck"): + install_text = ( + "pyspark" if engine_type == "spark" else f"sqlmesh\\[{engine_type.replace('_', '')}]" + ) + engine_install_text = f'• Run command in CLI to install your SQL engine\'s Python dependencies: pip install "{install_text}"\n' + # interactive init does not support DLT template + next_step_text = { + ProjectTemplate.DEFAULT: f"{engine_install_text}• Update your gateway connection settings (e.g., username/password) in the project configuration file:\n {config_path}", + ProjectTemplate.DBT: "", + } + next_step_text[ProjectTemplate.EMPTY] = next_step_text[ProjectTemplate.DEFAULT] + + quickstart_text = { + ProjectTemplate.DEFAULT: "Quickstart guide:\nhttps://sqlmesh.readthedocs.io/en/stable/quickstart/cli/", + ProjectTemplate.DBT: "dbt guide:\nhttps://sqlmesh.readthedocs.io/en/stable/integrations/dbt/", + } + quickstart_text[ProjectTemplate.EMPTY] = quickstart_text[ProjectTemplate.DEFAULT] + + console.print(f"""────────────────────────────── + +Your SQLMesh project is ready! + +Next steps: +{next_step_text[project_template]} +• Run command in CLI: sqlmesh plan +• (Optional) Explain a plan: sqlmesh plan --explain + +{quickstart_text[project_template]} + +Need help? +• Docs: https://sqlmesh.readthedocs.io +• Slack: https://www.tobikodata.com/slack +• GitHub: https://github.com/TobikoData/sqlmesh/issues +""") + @cli.command("render") @click.argument("model") diff --git a/sqlmesh/cli/project_init.py b/sqlmesh/cli/project_init.py new file mode 100644 index 0000000000..66b327ef75 --- /dev/null +++ b/sqlmesh/cli/project_init.py @@ -0,0 +1,497 @@ +import typing as t +from enum import Enum +from pathlib import Path +from dataclasses import dataclass +from rich.prompt import Prompt +from rich.console import Console +from sqlmesh.integrations.dlt import generate_dlt_models_and_settings +from sqlmesh.utils.date import yesterday_ds +from sqlmesh.utils.errors import SQLMeshError + +from sqlmesh.core.config.connection import ( + CONNECTION_CONFIG_TO_TYPE, + DIALECT_TO_TYPE, + INIT_DISPLAY_INFO_TO_TYPE, +) + + +PRIMITIVES = (str, int, bool, float) + + +class ProjectTemplate(Enum): + DEFAULT = "default" + DBT = "dbt" + EMPTY = "empty" + DLT = "dlt" + + +class InitCliMode(Enum): + DEFAULT = "default" + FLOW = "flow" + + +def _gen_config( + engine_type: t.Optional[str], + settings: t.Optional[str], + start: t.Optional[str], + template: ProjectTemplate, + cli_mode: InitCliMode, + dialect: t.Optional[str] = None, +) -> str: + project_dialect = dialect or DIALECT_TO_TYPE.get(engine_type) + + connection_settings = ( + settings + or """ type: duckdb + database: db.db""" + ) + + if not settings and template != ProjectTemplate.DBT: + doc_link = "https://sqlmesh.readthedocs.io/en/stable/integrations/engines{engine_link}" + engine_link = "" + + if engine_type in CONNECTION_CONFIG_TO_TYPE: + required_fields = [] + non_required_fields = [] + + for name, field in CONNECTION_CONFIG_TO_TYPE[engine_type].model_fields.items(): + field_name = field.alias or name + + default_value = field.get_default() + + if isinstance(default_value, Enum): + default_value = default_value.value + elif not isinstance(default_value, PRIMITIVES): + default_value = "" + + required = field.is_required() or field_name == "type" + option_str = f" {'# ' if not required else ''}{field_name}: {default_value}\n" + + # specify the DuckDB database field so quickstart runs out of the box + if engine_type == "duckdb" and field_name == "database": + option_str = " database: db.db\n" + required = True + + if required: + required_fields.append(option_str) + else: + non_required_fields.append(option_str) + + connection_settings = "".join(required_fields + non_required_fields) + + engine_link = f"/{engine_type}/#connection-options" + + connection_settings = ( + " # For more information on configuring the connection to your execution engine, visit:\n" + " # https://sqlmesh.readthedocs.io/en/stable/reference/configuration/#connection\n" + f" # {doc_link.format(engine_link=engine_link)}\n{connection_settings}" + ) + + default_configs = { + ProjectTemplate.DEFAULT: f"""# --- Gateway Connection --- +gateways: + {engine_type}: + connection: +{connection_settings} +default_gateway: {engine_type} + +# --- Model Defaults --- +# https://sqlmesh.readthedocs.io/en/stable/reference/model_configuration/#model-defaults + +model_defaults: + dialect: {project_dialect} + start: {start or yesterday_ds()} # Start date for backfill history + cron: '@daily' # Run models daily at 12am UTC (can override per model) + +# --- Linting Rules --- +# Enforce standards for your team +# https://sqlmesh.readthedocs.io/en/stable/guides/linter/ + +linter: + enabled: true + rules: + - ambiguousorinvalidcolumn + - invalidselectstarexpansion +""", + ProjectTemplate.DBT: """from pathlib import Path + +from sqlmesh.dbt.loader import sqlmesh_config + +config = sqlmesh_config(Path(__file__).parent) +""", + } + + default_configs[ProjectTemplate.EMPTY] = default_configs[ProjectTemplate.DEFAULT] + default_configs[ProjectTemplate.DLT] = default_configs[ProjectTemplate.DEFAULT] + + flow_cli_mode = """ +# FLOW: Minimal prompts, automatic changes, summary output +# https://sqlmesh.readthedocs.io/en/stable/reference/configuration/#plan + +plan: + no_diff: true # Hide detailed text differences for changed models + no_prompts: true # No interactive prompts + auto_apply: true # Apply changes automatically + +# --- Optional: Set a default target environment --- +# This is intended for local development to prevent users from accidentally applying plans to the prod environment. +# It is a development only config and should NOT be committed to your git repo. +# https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#default-target-environment + +# Uncomment the following line to use a default target environment derived from the logged in user's name. +# default_target_environment: dev_{{ user() }} + +# Example usage: +# sqlmesh plan # Automatically resolves to: sqlmesh plan dev_yourname +# sqlmesh plan prod # Specify `prod` to apply changes to production +""" + + return default_configs[template] + (flow_cli_mode if cli_mode == InitCliMode.FLOW else "") + + +@dataclass +class ExampleObjects: + sql_models: t.Dict[str, str] + python_models: t.Dict[str, str] + seeds: t.Dict[str, str] + audits: t.Dict[str, str] + tests: t.Dict[str, str] + sql_macros: t.Dict[str, str] + python_macros: t.Dict[str, str] + + +def _gen_example_objects(schema_name: str) -> ExampleObjects: + sql_models: t.Dict[str, str] = {} + python_models: t.Dict[str, str] = {} + seeds: t.Dict[str, str] = {} + audits: t.Dict[str, str] = {} + tests: t.Dict[str, str] = {} + sql_macros: t.Dict[str, str] = {} + python_macros: t.Dict[str, str] = {"__init__": ""} + + full_model_name = f"{schema_name}.full_model" + incremental_model_name = f"{schema_name}.incremental_model" + seed_model_name = f"{schema_name}.seed_model" + + sql_models[full_model_name] = f"""MODEL ( + name {full_model_name}, + kind FULL, + cron '@daily', + grain item_id, + audits (assert_positive_order_ids), +); + +SELECT + item_id, + COUNT(DISTINCT id) AS num_orders, +FROM + {incremental_model_name} +GROUP BY item_id + """ + + sql_models[incremental_model_name] = f"""MODEL ( + name {incremental_model_name}, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column event_date + ), + start '2020-01-01', + cron '@daily', + grain (id, event_date) +); + +SELECT + id, + item_id, + event_date, +FROM + {seed_model_name} +WHERE + event_date BETWEEN @start_date AND @end_date + """ + + sql_models[seed_model_name] = f"""MODEL ( + name {seed_model_name}, + kind SEED ( + path '../seeds/seed_data.csv' + ), + columns ( + id INTEGER, + item_id INTEGER, + event_date DATE + ), + grain (id, event_date) +); + """ + + seeds["seed_data"] = """id,item_id,event_date +1,2,2020-01-01 +2,1,2020-01-01 +3,3,2020-01-03 +4,1,2020-01-04 +5,1,2020-01-05 +6,1,2020-01-06 +7,1,2020-01-07 +""" + + audits["assert_positive_order_ids"] = """AUDIT ( + name assert_positive_order_ids, +); + +SELECT * +FROM @this_model +WHERE + item_id < 0 + """ + + tests["test_full_model"] = f"""test_example_full_model: + model: {full_model_name} + inputs: + {incremental_model_name}: + rows: + - id: 1 + item_id: 1 + - id: 2 + item_id: 1 + - id: 3 + item_id: 2 + outputs: + query: + rows: + - item_id: 1 + num_orders: 2 + - item_id: 2 + num_orders: 1 + """ + + return ExampleObjects( + sql_models=sql_models, + python_models=python_models, + seeds=seeds, + audits=audits, + tests=tests, + python_macros=python_macros, + sql_macros=sql_macros, + ) + + +def init_example_project( + path: t.Union[str, Path], + engine_type: t.Optional[str], + dialect: t.Optional[str] = None, + template: ProjectTemplate = ProjectTemplate.DEFAULT, + pipeline: t.Optional[str] = None, + dlt_path: t.Optional[str] = None, + schema_name: str = "sqlmesh_example", + cli_mode: InitCliMode = InitCliMode.DEFAULT, +) -> Path: + root_path = Path(path) + config_extension = "py" if template == ProjectTemplate.DBT else "yaml" + config_path = root_path / f"config.{config_extension}" + audits_path = root_path / "audits" + macros_path = root_path / "macros" + models_path = root_path / "models" + seeds_path = root_path / "seeds" + tests_path = root_path / "tests" + + if config_path.exists(): + raise SQLMeshError( + f"Found an existing config file '{config_path}'.\n\nPlease change to another directory or remove the existing file." + ) + + if template == ProjectTemplate.DBT and not Path(root_path, "dbt_project.yml").exists(): + raise SQLMeshError( + "Required dbt project file 'dbt_project.yml' not found in the current directory.\n\nPlease add it or change directories before running `sqlmesh init` to set up your project." + ) + + engine_types = "', '".join(CONNECTION_CONFIG_TO_TYPE) + if engine_type is None and template != ProjectTemplate.DBT: + raise SQLMeshError( + f"Missing `engine` argument to `sqlmesh init` - please specify a SQL engine for your project. Options: '{engine_types}'." + ) + + if engine_type and engine_type not in CONNECTION_CONFIG_TO_TYPE: + raise SQLMeshError( + f"Invalid engine '{engine_type}'. Please specify one of '{engine_types}'." + ) + + models: t.Set[t.Tuple[str, str]] = set() + settings = None + start = None + if engine_type and template == ProjectTemplate.DLT: + project_dialect = dialect or DIALECT_TO_TYPE.get(engine_type) + if pipeline and project_dialect: + dlt_models, settings, start = generate_dlt_models_and_settings( + pipeline_name=pipeline, dialect=project_dialect, dlt_path=dlt_path + ) + else: + raise SQLMeshError( + "Please provide a DLT pipeline with the `--dlt-pipeline` flag to generate a SQLMesh project from DLT." + ) + + _create_config(config_path, engine_type, dialect, settings, start, template, cli_mode) + if template == ProjectTemplate.DBT: + return config_path + + _create_folders([audits_path, macros_path, models_path, seeds_path, tests_path]) + + if template == ProjectTemplate.DLT: + _create_object_files( + models_path, {model[0].split(".")[-1]: model[1] for model in dlt_models}, "sql" + ) + return config_path + + example_objects = _gen_example_objects(schema_name=schema_name) + + if template != ProjectTemplate.EMPTY: + _create_object_files(models_path, example_objects.sql_models, "sql") + _create_object_files(models_path, example_objects.python_models, "py") + _create_object_files(seeds_path, example_objects.seeds, "csv") + _create_object_files(audits_path, example_objects.audits, "sql") + _create_object_files(tests_path, example_objects.tests, "yaml") + _create_object_files(macros_path, example_objects.python_macros, "py") + _create_object_files(macros_path, example_objects.sql_macros, "sql") + + return config_path + + +def _create_folders(target_folders: t.Sequence[Path]) -> None: + for folder_path in target_folders: + folder_path.mkdir(exist_ok=True) + (folder_path / ".gitkeep").touch() + + +def _create_config( + config_path: Path, + engine_type: t.Optional[str], + dialect: t.Optional[str], + settings: t.Optional[str], + start: t.Optional[str], + template: ProjectTemplate, + cli_mode: InitCliMode, +) -> None: + project_config = _gen_config(engine_type, settings, start, template, cli_mode, dialect) + + _write_file( + config_path, + project_config, + ) + + +def _create_object_files(path: Path, object_dict: t.Dict[str, str], file_extension: str) -> None: + for object_name, object_def in object_dict.items(): + # file name is table component of catalog.schema.table + _write_file(path / f"{object_name.split('.')[-1]}.{file_extension}", object_def) + + +def _write_file(path: Path, payload: str) -> None: + with open(path, "w", encoding="utf-8") as fd: + fd.write(payload) + + +def interactive_init( + path: Path, + console: Console, + project_template: t.Optional[ProjectTemplate] = None, +) -> t.Tuple[ProjectTemplate, t.Optional[str], t.Optional[InitCliMode]]: + console.print("──────────────────────────────") + console.print("Welcome to SQLMesh!") + + project_template = _init_template_prompt(console) if not project_template else project_template + + if project_template == ProjectTemplate.DBT: + return (project_template, None, None) + + engine_type = _init_engine_prompt(console) + cli_mode = _init_cli_mode_prompt(console) + + return (project_template, engine_type, cli_mode) + + +def _init_integer_prompt( + console: Console, err_msg_entity: str, num_options: int, retry_func: t.Callable[[t.Any], t.Any] +) -> int: + err_msg = "\nERROR: '{option_str}' is not a valid {err_msg_entity} number - please enter a number between 1 and {num_options} or exit with control+c\n" + while True: + option_str = Prompt.ask("Enter a number", console=console) + + value_error = False + try: + option_num = int(option_str) + except ValueError: + value_error = True + + if value_error or option_num < 1 or option_num > num_options: + console.print( + err_msg.format( + option_str=option_str, err_msg_entity=err_msg_entity, num_options=num_options + ), + style="red", + ) + continue + console.print("") + return option_num + + +def _init_display_choices(values_dict: t.Dict[str, str], console: Console) -> t.Dict[int, str]: + display_num_to_value = {} + for i, value_str in enumerate(values_dict.keys()): + console.print(f" \[{i + 1}] {' ' if i < 9 else ''}{value_str} {values_dict[value_str]}") + display_num_to_value[i + 1] = value_str + console.print("") + return display_num_to_value + + +def _init_template_prompt(console: Console) -> ProjectTemplate: + console.print("──────────────────────────────\n") + console.print("What type of project do you want to set up?\n") + + # These are ordered for user display - do not reorder + template_descriptions = { + ProjectTemplate.DEFAULT.name: "- Create SQLMesh example project models and files", + ProjectTemplate.DBT.value: " - You have an existing dbt project and want to run it with SQLMesh", + ProjectTemplate.EMPTY.name: " - Create a SQLMesh configuration file and project directories only", + } + + display_num_to_template = _init_display_choices(template_descriptions, console) + + template_num = _init_integer_prompt( + console, "project type", len(template_descriptions), _init_template_prompt + ) + + return ProjectTemplate(display_num_to_template[template_num].lower()) + + +def _init_engine_prompt(console: Console) -> str: + console.print("──────────────────────────────\n") + console.print("Choose your SQL engine:\n") + + # INIT_DISPLAY_INFO_TO_TYPE is a dict of {engine_type: (display_order, display_name)} + DISPLAY_NAME_TO_TYPE = {v[1]: k for k, v in INIT_DISPLAY_INFO_TO_TYPE.items()} + ordered_engine_display_names = { + info[1]: "" for info in sorted(INIT_DISPLAY_INFO_TO_TYPE.values(), key=lambda x: x[0]) + } + display_num_to_display_name = _init_display_choices(ordered_engine_display_names, console) + + engine_num = _init_integer_prompt( + console, "engine", len(ordered_engine_display_names), _init_engine_prompt + ) + + return DISPLAY_NAME_TO_TYPE[display_num_to_display_name[engine_num]] + + +def _init_cli_mode_prompt(console: Console) -> InitCliMode: + console.print("──────────────────────────────\n") + console.print("Choose your SQLMesh CLI experience:\n") + + cli_mode_descriptions = { + InitCliMode.DEFAULT.name: "- See and control every detail", + InitCliMode.FLOW.name: " - Automatically run changes and show summary output", + } + + display_num_to_cli_mode = _init_display_choices(cli_mode_descriptions, console) + + cli_mode_num = _init_integer_prompt( + console, "config", len(cli_mode_descriptions), _init_cli_mode_prompt + ) + + return InitCliMode(display_num_to_cli_mode[cli_mode_num].lower()) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index b3ed3bc34f..a669471ddf 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -87,6 +87,9 @@ def validate(cls: t.Any, data: t.Any) -> t.Any: class ConnectionConfig(abc.ABC, BaseConfig): type_: str + DIALECT: t.ClassVar[str] + DISPLAY_NAME: t.ClassVar[str] + DISPLAY_ORDER: t.ClassVar[int] concurrent_tasks: int register_comments: bool pre_ping: bool @@ -463,6 +466,9 @@ class MotherDuckConnectionConfig(BaseDuckDBConnectionConfig): """Configuration for the MotherDuck connection.""" type_: t.Literal["motherduck"] = Field(alias="type", default="motherduck") + DIALECT: t.ClassVar[t.Literal["duckdb"]] = "duckdb" + DISPLAY_NAME: t.ClassVar[t.Literal["MotherDuck"]] = "MotherDuck" + DISPLAY_ORDER: t.ClassVar[t.Literal[5]] = 5 @property def _connection_kwargs_keys(self) -> t.Set[str]: @@ -487,6 +493,9 @@ class DuckDBConnectionConfig(BaseDuckDBConnectionConfig): """Configuration for the DuckDB connection.""" type_: t.Literal["duckdb"] = Field(alias="type", default="duckdb") + DIALECT: t.ClassVar[t.Literal["duckdb"]] = "duckdb" + DISPLAY_NAME: t.ClassVar[t.Literal["DuckDB"]] = "DuckDB" + DISPLAY_ORDER: t.ClassVar[t.Literal[1]] = 1 class SnowflakeConnectionConfig(ConnectionConfig): @@ -537,6 +546,9 @@ class SnowflakeConnectionConfig(ConnectionConfig): session_parameters: t.Optional[dict] = None type_: t.Literal["snowflake"] = Field(alias="type", default="snowflake") + DIALECT: t.ClassVar[t.Literal["snowflake"]] = "snowflake" + DISPLAY_NAME: t.ClassVar[t.Literal["Snowflake"]] = "Snowflake" + DISPLAY_ORDER: t.ClassVar[t.Literal[2]] = 2 _concurrent_tasks_validator = concurrent_tasks_validator @@ -733,6 +745,9 @@ class DatabricksConnectionConfig(ConnectionConfig): pre_ping: t.Literal[False] = False type_: t.Literal["databricks"] = Field(alias="type", default="databricks") + DIALECT: t.ClassVar[t.Literal["databricks"]] = "databricks" + DISPLAY_NAME: t.ClassVar[t.Literal["Databricks"]] = "Databricks" + DISPLAY_ORDER: t.ClassVar[t.Literal[3]] = 3 _concurrent_tasks_validator = concurrent_tasks_validator _http_headers_validator = http_headers_validator @@ -989,6 +1004,9 @@ class BigQueryConnectionConfig(ConnectionConfig): pre_ping: t.Literal[False] = False type_: t.Literal["bigquery"] = Field(alias="type", default="bigquery") + DIALECT: t.ClassVar[t.Literal["bigquery"]] = "bigquery" + DISPLAY_NAME: t.ClassVar[t.Literal["BigQuery"]] = "BigQuery" + DISPLAY_ORDER: t.ClassVar[t.Literal[4]] = 4 _engine_import_validator = _get_engine_import_validator("google.cloud.bigquery", "bigquery") @@ -1129,7 +1147,12 @@ class GCPPostgresConnectionConfig(ConnectionConfig): timeout: t.Optional[int] = None scopes: t.Tuple[str, ...] = ("https://www.googleapis.com/auth/sqlservice.admin",) driver: str = "pg8000" + type_: t.Literal["gcp_postgres"] = Field(alias="type", default="gcp_postgres") + DIALECT: t.ClassVar[t.Literal["postgres"]] = "postgres" + DISPLAY_NAME: t.ClassVar[t.Literal["GCP Postgres"]] = "GCP Postgres" + DISPLAY_ORDER: t.ClassVar[t.Literal[13]] = 13 + concurrent_tasks: int = 4 register_comments: bool = True pre_ping: bool = True @@ -1264,6 +1287,9 @@ class RedshiftConnectionConfig(ConnectionConfig): pre_ping: bool = False type_: t.Literal["redshift"] = Field(alias="type", default="redshift") + DIALECT: t.ClassVar[t.Literal["redshift"]] = "redshift" + DISPLAY_NAME: t.ClassVar[t.Literal["Redshift"]] = "Redshift" + DISPLAY_ORDER: t.ClassVar[t.Literal[7]] = 7 _engine_import_validator = _get_engine_import_validator("redshift_connector", "redshift") @@ -1325,6 +1351,9 @@ class PostgresConnectionConfig(ConnectionConfig): pre_ping: bool = True type_: t.Literal["postgres"] = Field(alias="type", default="postgres") + DIALECT: t.ClassVar[t.Literal["postgres"]] = "postgres" + DISPLAY_NAME: t.ClassVar[t.Literal["Postgres"]] = "Postgres" + DISPLAY_ORDER: t.ClassVar[t.Literal[12]] = 12 _engine_import_validator = _get_engine_import_validator("psycopg2", "postgres") @@ -1378,6 +1407,9 @@ class MySQLConnectionConfig(ConnectionConfig): pre_ping: bool = True type_: t.Literal["mysql"] = Field(alias="type", default="mysql") + DIALECT: t.ClassVar[t.Literal["mysql"]] = "mysql" + DISPLAY_NAME: t.ClassVar[t.Literal["MySQL"]] = "MySQL" + DISPLAY_ORDER: t.ClassVar[t.Literal[14]] = 14 _engine_import_validator = _get_engine_import_validator("pymysql", "mysql") @@ -1440,6 +1472,9 @@ class MSSQLConnectionConfig(ConnectionConfig): pre_ping: bool = True type_: t.Literal["mssql"] = Field(alias="type", default="mssql") + DIALECT: t.ClassVar[t.Literal["tsql"]] = "tsql" + DISPLAY_NAME: t.ClassVar[t.Literal["MSSQL"]] = "MSSQL" + DISPLAY_ORDER: t.ClassVar[t.Literal[11]] = 11 @model_validator(mode="before") @classmethod @@ -1581,6 +1616,8 @@ def _extra_engine_config(self) -> t.Dict[str, t.Any]: class AzureSQLConnectionConfig(MSSQLConnectionConfig): type_: t.Literal["azuresql"] = Field(alias="type", default="azuresql") # type: ignore + DISPLAY_NAME: t.ClassVar[t.Literal["Azure SQL"]] = "Azure SQL" # type: ignore + DISPLAY_ORDER: t.ClassVar[t.Literal[10]] = 10 # type: ignore @property def _extra_engine_config(self) -> t.Dict[str, t.Any]: @@ -1601,6 +1638,9 @@ class SparkConnectionConfig(ConnectionConfig): pre_ping: t.Literal[False] = False type_: t.Literal["spark"] = Field(alias="type", default="spark") + DIALECT: t.ClassVar[t.Literal["spark"]] = "spark" + DISPLAY_NAME: t.ClassVar[t.Literal["Spark"]] = "Spark" + DISPLAY_ORDER: t.ClassVar[t.Literal[8]] = 8 _engine_import_validator = _get_engine_import_validator("pyspark", "spark") @@ -1719,6 +1759,9 @@ class TrinoConnectionConfig(ConnectionConfig): pre_ping: t.Literal[False] = False type_: t.Literal["trino"] = Field(alias="type", default="trino") + DIALECT: t.ClassVar[t.Literal["trino"]] = "trino" + DISPLAY_NAME: t.ClassVar[t.Literal["Trino"]] = "Trino" + DISPLAY_ORDER: t.ClassVar[t.Literal[9]] = 9 _engine_import_validator = _get_engine_import_validator("trino", "trino") @@ -1879,6 +1922,9 @@ class ClickhouseConnectionConfig(ConnectionConfig): connection_pool_options: t.Optional[t.Dict[str, t.Any]] = None type_: t.Literal["clickhouse"] = Field(alias="type", default="clickhouse") + DIALECT: t.ClassVar[t.Literal["clickhouse"]] = "clickhouse" + DISPLAY_NAME: t.ClassVar[t.Literal["ClickHouse"]] = "ClickHouse" + DISPLAY_ORDER: t.ClassVar[t.Literal[6]] = 6 _engine_import_validator = _get_engine_import_validator("clickhouse_connect", "clickhouse") @@ -2003,6 +2049,9 @@ class AthenaConnectionConfig(ConnectionConfig): pre_ping: t.Literal[False] = False type_: t.Literal["athena"] = Field(alias="type", default="athena") + DIALECT: t.ClassVar[t.Literal["athena"]] = "athena" + DISPLAY_NAME: t.ClassVar[t.Literal["Athena"]] = "Athena" + DISPLAY_ORDER: t.ClassVar[t.Literal[15]] = 15 _engine_import_validator = _get_engine_import_validator("pyathena", "athena") @@ -2071,6 +2120,9 @@ class RisingwaveConnectionConfig(ConnectionConfig): pre_ping: bool = True type_: t.Literal["risingwave"] = Field(alias="type", default="risingwave") + DIALECT: t.ClassVar[t.Literal["risingwave"]] = "risingwave" + DISPLAY_NAME: t.ClassVar[t.Literal["RisingWave"]] = "RisingWave" + DISPLAY_ORDER: t.ClassVar[t.Literal[16]] = 16 _engine_import_validator = _get_engine_import_validator("psycopg2", "risingwave") @@ -2115,6 +2167,27 @@ def init(cursor: t.Any) -> None: ) } +DIALECT_TO_TYPE = { + tpe.all_field_infos()["type_"].default: tpe.DIALECT + for tpe in subclasses( + __name__, + ConnectionConfig, + exclude=(ConnectionConfig, BaseDuckDBConnectionConfig), + ) +} + +INIT_DISPLAY_INFO_TO_TYPE = { + tpe.all_field_infos()["type_"].default: ( + tpe.DISPLAY_ORDER, + tpe.DISPLAY_NAME, + ) + for tpe in subclasses( + __name__, + ConnectionConfig, + exclude=(ConnectionConfig, BaseDuckDBConnectionConfig), + ) +} + def parse_connection_config(v: t.Dict[str, t.Any]) -> ConnectionConfig: if "type" not in v: diff --git a/sqlmesh/integrations/dlt.py b/sqlmesh/integrations/dlt.py index 023b43f173..2d601a0e22 100644 --- a/sqlmesh/integrations/dlt.py +++ b/sqlmesh/integrations/dlt.py @@ -138,7 +138,7 @@ def generate_dlt_models( force: bool, dlt_path: t.Optional[str] = None, ) -> t.List[str]: - from sqlmesh.cli.example_project import _create_models + from sqlmesh.cli.project_init import _create_object_files sqlmesh_models, _, _ = generate_dlt_models_and_settings( pipeline_name=pipeline_name, @@ -152,7 +152,11 @@ def generate_dlt_models( sqlmesh_models = {model for model in sqlmesh_models if model[0] not in existing_models} if sqlmesh_models: - _create_models(models_path=context.path / "models", models=sqlmesh_models) + _create_object_files( + context.path / "models", + {model[0].split(".")[-1]: model[1] for model in sqlmesh_models}, + "sql", + ) return [model[0] for model in sqlmesh_models] return [] diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 9110feb2fe..2d299df668 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -26,15 +26,16 @@ from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring from IPython.utils.process import arg_split from rich.jupyter import JupyterRenderable -from sqlmesh.cli.example_project import ProjectTemplate, init_example_project +from sqlmesh.cli.project_init import ProjectTemplate, init_example_project from sqlmesh.core import analytics from sqlmesh.core.config import load_configs +from sqlmesh.core.config.connection import INIT_DISPLAY_INFO_TO_TYPE from sqlmesh.core.console import create_console, set_console, configure_console from sqlmesh.core.context import Context from sqlmesh.core.dialect import format_model_expressions, parse from sqlmesh.core.model import load_sql_based_model from sqlmesh.core.test import ModelTestMetadata -from sqlmesh.utils import sqlglot_dialects, yaml, Verbosity, optional_import +from sqlmesh.utils import yaml, Verbosity, optional_import from sqlmesh.utils.errors import MagicError, MissingContextException, SQLMeshError logger = logging.getLogger(__name__) @@ -198,9 +199,9 @@ def context(self, line: str) -> None: @magic_arguments() @argument("path", type=str, help="The path where the new SQLMesh project should be created.") @argument( - "sql_dialect", + "engine", type=str, - help=f"Default model SQL dialect. Supported values: {sqlglot_dialects()}.", + help=f"Project SQL engine. Supported values: '{', '.join([info[1] for info in sorted(INIT_DISPLAY_INFO_TO_TYPE.values(), key=lambda x: x[0])])}'.", # type: ignore ) @argument( "--template", @@ -229,7 +230,12 @@ def init(self, line: str) -> None: except ValueError: raise MagicError(f"Invalid project template '{args.template}'") init_example_project( - args.path, args.sql_dialect, project_template, args.dlt_pipeline, args.dlt_path + path=args.path, + engine_type=args.engine, + dialect=None, + template=project_template, + pipeline=args.dlt_pipeline, + dlt_path=args.dlt_path, ) html = str( h( diff --git a/sqlmesh/utils/yaml.py b/sqlmesh/utils/yaml.py index 549d849902..0eb18d8188 100644 --- a/sqlmesh/utils/yaml.py +++ b/sqlmesh/utils/yaml.py @@ -1,5 +1,6 @@ from __future__ import annotations +import getpass import io import typing as t from decimal import Decimal @@ -14,6 +15,7 @@ JINJA_METHODS = { "env_var": lambda key, default=None: getenv(key, default), + "user": lambda: getpass.getuser(), } diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index c5f6438cc2..6972db7d67 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -11,11 +11,12 @@ import json from unittest.mock import MagicMock from sqlmesh import RuntimeEnv -from sqlmesh.cli.example_project import ProjectTemplate, init_example_project +from sqlmesh.cli.project_init import ProjectTemplate, init_example_project from sqlmesh.cli.main import cli from sqlmesh.core.context import Context from sqlmesh.integrations.dlt import generate_dlt_models 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 FREEZE_TIME = "2023-01-01 00:00:00 UTC" @@ -47,7 +48,7 @@ def create_example_project(temp_dir) -> None: - Creating the SQLMesh example project in the temp_dir directory - Overwriting the config.yaml file so the duckdb database file will be created in the temp_dir directory """ - init_example_project(temp_dir, "duckdb") + init_example_project(temp_dir, engine_type="duckdb") with open(temp_dir / "config.yaml", "w", encoding="utf-8") as f: f.write( f"""gateways: @@ -885,7 +886,7 @@ 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"]) assert ( - "Error: DLT pipeline is a required argument to generate a SQLMesh project from DLT" + "Error: Please provide a DLT pipeline with the `--dlt-pipeline` flag to generate a SQLMesh project from DLT" in result.output ) @@ -913,7 +914,9 @@ def test_dlt_filesystem_pipeline(tmp_path): info = filesystem_pipeline.run([{"item_id": 1}], table_name="equipment") assert not info.has_failed_jobs - init_example_project(tmp_path, "athena", ProjectTemplate.DLT, "filesystem_pipeline") + init_example_project( + tmp_path, "athena", template=ProjectTemplate.DLT, pipeline="filesystem_pipeline" + ) # Validate generated sqlmesh config and models config_path = tmp_path / "config.yaml" @@ -948,11 +951,12 @@ def test_dlt_filesystem_pipeline(tmp_path): assert incremental_model == expected_incremental_model expected_config = ( + "# --- Gateway Connection ---\n" "gateways:\n" " athena:\n" " connection:\n" " # For more information on configuring the connection to your execution engine, visit:\n" - " # https://sqlmesh.readthedocs.io/en/stable/reference/configuration/#connections\n" + " # https://sqlmesh.readthedocs.io/en/stable/reference/configuration/#connection\n" " # https://sqlmesh.readthedocs.io/en/stable/integrations/engines/athena/#connection-options\n" " type: athena\n" " # concurrent_tasks: 4\n" @@ -968,11 +972,22 @@ def test_dlt_filesystem_pipeline(tmp_path): " # s3_staging_dir: \n" " # schema_name: \n" " # catalog_name: \n" - " # s3_warehouse_location: \n\n\n" + " # s3_warehouse_location: \n\n" "default_gateway: athena\n\n" + "# --- Model Defaults ---\n" + "# https://sqlmesh.readthedocs.io/en/stable/reference/model_configuration/#model-defaults\n\n" "model_defaults:\n" " dialect: athena\n" - f" start: {yesterday_ds()}\n" + f" start: {yesterday_ds()} # Start date for backfill history\n" + " cron: '@daily' # Run models daily at 12am UTC (can override per model)\n\n" + "# --- Linting Rules ---\n" + "# Enforce standards for your team\n" + "# https://sqlmesh.readthedocs.io/en/stable/guides/linter/\n\n" + "linter:\n" + " enabled: true\n" + " rules:\n" + " - ambiguousorinvalidcolumn\n" + " - invalidselectstarexpansion\n" ) with open(config_path) as file: @@ -985,7 +1000,7 @@ def test_dlt_filesystem_pipeline(tmp_path): @time_machine.travel(FREEZE_TIME) -def test_plan_dlt(runner, tmp_path): +def test_dlt_pipeline(runner, tmp_path): from dlt.common.pipeline import get_dlt_pipelines_dir root_dir = path.abspath(getcwd()) @@ -1001,24 +1016,44 @@ def test_plan_dlt(runner, tmp_path): # This should fail since it won't be able to locate the pipeline in this path with pytest.raises(ClickException, match=r".*Could not attach to pipeline*"): init_example_project( - tmp_path, "duckdb", ProjectTemplate.DLT, "sushi", dlt_path="./dlt2/pipelines" + tmp_path, + "duckdb", + template=ProjectTemplate.DLT, + pipeline="sushi", + dlt_path="./dlt2/pipelines", ) # By setting the pipelines path where the pipeline directory is located, it should work dlt_path = get_dlt_pipelines_dir() - init_example_project(tmp_path, "duckdb", ProjectTemplate.DLT, "sushi", dlt_path=dlt_path) + init_example_project( + tmp_path, "duckdb", template=ProjectTemplate.DLT, pipeline="sushi", dlt_path=dlt_path + ) - expected_config = f"""gateways: + expected_config = f"""# --- Gateway Connection --- +gateways: duckdb: connection: type: duckdb database: {dataset_path} - default_gateway: duckdb +# --- Model Defaults --- +# https://sqlmesh.readthedocs.io/en/stable/reference/model_configuration/#model-defaults + model_defaults: dialect: duckdb - start: {yesterday_ds()} + start: {yesterday_ds()} # Start date for backfill history + cron: '@daily' # Run models daily at 12am UTC (can override per model) + +# --- Linting Rules --- +# Enforce standards for your team +# https://sqlmesh.readthedocs.io/en/stable/guides/linter/ + +linter: + enabled: true + rules: + - ambiguousorinvalidcolumn + - invalidselectstarexpansion """ with open(tmp_path / "config.yaml") as file: @@ -1167,30 +1202,6 @@ def test_plan_dlt(runner, tmp_path): remove(dataset_path) -@time_machine.travel(FREEZE_TIME) -def test_init_project_dialects(tmp_path): - dialect_to_config = { - "redshift": "# concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # user: \n # password: \n # database: \n # host: \n # port: \n # source_address: \n # unix_sock: \n # ssl: \n # sslmode: \n # timeout: \n # tcp_keepalive: \n # application_name: \n # preferred_role: \n # principal_arn: \n # credentials_provider: \n # region: \n # cluster_identifier: \n # iam: \n # is_serverless: \n # serverless_acct_id: \n # serverless_work_group: \n # enable_merge: ", - "bigquery": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # method: oauth\n # project: \n # execution_project: \n # quota_project: \n # location: \n # keyfile: \n # keyfile_json: \n # token: \n # refresh_token: \n # client_id: \n # client_secret: \n # token_uri: \n # scopes: \n # impersonated_service_account: \n # job_creation_timeout_seconds: \n # job_execution_timeout_seconds: \n # job_retries: 1\n # job_retry_deadline_seconds: \n # priority: \n # maximum_bytes_billed: ", - "snowflake": "account: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # user: \n # password: \n # warehouse: \n # database: \n # role: \n # authenticator: \n # token: \n # host: \n # port: \n # application: Tobiko_SQLMesh\n # private_key: \n # private_key_path: \n # private_key_passphrase: \n # session_parameters: ", - "databricks": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # server_hostname: \n # http_path: \n # access_token: \n # auth_type: \n # oauth_client_id: \n # oauth_client_secret: \n # catalog: \n # http_headers: \n # session_configuration: \n # databricks_connect_server_hostname: \n # databricks_connect_access_token: \n # databricks_connect_cluster_id: \n # databricks_connect_use_serverless: False\n # force_databricks_connect: False\n # disable_databricks_connect: False\n # disable_spark_session: False", - "postgres": "host: \n user: \n password: \n port: \n database: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: True\n # pretty_sql: False\n # keepalives_idle: \n # connect_timeout: 10\n # role: \n # sslmode: \n # application_name: ", - } - - for dialect, expected_config in dialect_to_config.items(): - init_example_project(tmp_path, dialect=dialect) - - config_start = f"gateways:\n {dialect}:\n connection:\n # For more information on configuring the connection to your execution engine, visit:\n # https://sqlmesh.readthedocs.io/en/stable/reference/configuration/#connections\n # https://sqlmesh.readthedocs.io/en/stable/integrations/engines/{dialect}/#connection-options\n type: {dialect}\n " - config_end = f"\n\n\ndefault_gateway: {dialect}\n\nmodel_defaults:\n dialect: {dialect}\n start: {yesterday_ds()}\n" - - with open(tmp_path / "config.yaml") as file: - config = file.read() - - assert config == f"{config_start}{expected_config}{config_end}" - - remove(tmp_path / "config.yaml") - - @time_machine.travel(FREEZE_TIME) def test_environments(runner, tmp_path): create_example_project(tmp_path) @@ -1340,8 +1351,6 @@ def test_state_export(runner: CliRunner, tmp_path: Path) -> None: catch_exceptions=False, ) assert result.exit_code == 0 - - # verify output assert "Gateway: local" in result.output assert "Type: duckdb" in result.output assert "Exporting versions" in result.output @@ -1688,27 +1697,6 @@ def test_state_import_local(runner: CliRunner, tmp_path: Path) -> None: assert "Aborting" in result.output -def test_dbt_init(tmp_path): - # The dbt init project doesn't require a dialect - init_example_project(tmp_path, dialect=None, template=ProjectTemplate.DBT) - - config_path = tmp_path / "config.py" - assert config_path.exists() - - with open(config_path) as file: - config = file.read() - - assert ( - config - == """from pathlib import Path - -from sqlmesh.dbt.loader import sqlmesh_config - -config = sqlmesh_config(Path(__file__).parent) -""" - ) - - def test_ignore_warnings(runner: CliRunner, tmp_path: Path) -> None: create_example_project(tmp_path) @@ -1793,6 +1781,239 @@ def test_table_diff_schema_diff_ignore_case(runner: CliRunner, tmp_path: Path): assert "Schema Diff Between 'T1' and 'T2':\n Schemas match" in stripped_output +# passing an invalid engine_type errors +def test_init_bad_engine_type(runner: CliRunner, tmp_path: Path): + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init", "invalid"], + ) + assert result.exit_code == 1 + assert "Invalid engine 'invalid'. Please specify one of " in result.output + + +# passing an invalid template errors +def test_init_bad_template(runner: CliRunner, tmp_path: Path): + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init", "-t", "invalid_template"], + ) + assert result.exit_code == 1 + assert "Invalid project template 'invalid_template'. Please specify one of " in result.output + + +# empty template should not produce example project files +def test_init_empty_template(runner: CliRunner, tmp_path: Path): + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init", "duckdb", "-t", "empty"], + ) + assert result.exit_code == 0 + + # Directories should exist, but example project files should not. + assert (tmp_path / "models").exists() + assert not (tmp_path / "models" / "full_model.sql").exists() + assert not (tmp_path / "models" / "incremental_model.sql").exists() + assert not (tmp_path / "seeds" / "seed_data.csv").exists() + + +# interactive init begins when no engine_type is provided and template is not dbt +def test_init_interactive_start(runner: CliRunner, tmp_path: Path): + # Input: 1 (DEFAULT template), 1 (duckdb engine), 1 (DEFAULT CLI mode) + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init"], + input="1\n1\n1\n", + ) + assert result.exit_code == 0 + assert "Choose your SQL engine" in result.output + + # dbt template passed, so no interactive + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init", "-t", "dbt"], + ) + assert "Choose your SQL engine" not in result.output + + +# passing an invalid integer response displays error +def test_init_interactive_invalid_int(runner: CliRunner, tmp_path: Path): + # First response is invalid (0) followed by valid selections. + # Input: 0 (invalid), 1 (DEFAULT template), 1 (duckdb engine), 1 (DEFAULT CLI mode) + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init"], + input="0\n1\n1\n1\n", + ) + assert result.exit_code == 0 + assert ( + "'0' is not a valid project type number - please enter a number between 1" in result.output + ) + + +# interactive init template step should not appear if a template is passed +def test_init_interactive_template_passed(runner: CliRunner, tmp_path: Path): + # Input: 1 (duckdb engine), 1 (DEFAULT CLI mode) + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init", "-t", "empty"], + input="1\n1\n", + ) + assert result.exit_code == 0 + assert "What type of project do you want to set up?" not in result.output + + +def test_init_interactive_cli_mode_default(runner: CliRunner, tmp_path: Path): + # Input: 1 (DEFAULT template), 1 (duckdb engine), 1 (DEFAULT CLI mode) + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init"], + input="1\n1\n1\n", + ) + assert result.exit_code == 0 + + config_path = tmp_path / "config.yaml" + assert config_path.exists() + assert "no_diff: true" not in config_path.read_text() + + +def test_init_interactive_cli_mode_simple(runner: CliRunner, tmp_path: Path): + # Input: 1 (DEFAULT template), 1 (duckdb engine), 2 (SIMPLE CLI mode) + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init"], + input="1\n1\n2\n", + ) + assert result.exit_code == 0 + + config_path = tmp_path / "config.yaml" + assert config_path.exists() + assert "no_diff: true" in config_path.read_text() + + +def test_init_interactive_engine_install_msg(runner: CliRunner, tmp_path: Path): + # Engine install text should not appear for built-in engines like DuckDB + # Input: 1 (DEFAULT template), 1 (duckdb engine), 1 (DEFAULT CLI mode) + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init"], + input="1\n1\n1\n", + ) + assert result.exit_code == 0 + assert "Run command in CLI to install your SQL engine" not in result.output + + remove(tmp_path / "config.yaml") + + # Input: 1 (DEFAULT template), 13 (gcp postgres engine), 1 (DEFAULT CLI mode) + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init"], + input="1\n13\n1\n", + ) + assert result.exit_code == 0 + assert ( + 'Run command in CLI to install your SQL engine\'s Python dependencies: pip \ninstall "sqlmesh[gcppostgres]"' + in result.output + ) + + +# dbt template without dbt_project.yml in directory should error +def test_init_dbt_template_no_dbt_project(runner: CliRunner, tmp_path: Path): + # template passed to init + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init", "-t", "dbt"], + ) + assert result.exit_code == 1 + assert ( + "Required dbt project file 'dbt_project.yml' not found in the current directory." + in result.output + ) + + # interactive init + # Input: 2 (dbt template) + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init"], + input="2\n", + ) + assert result.exit_code == 1 + assert ( + "Required dbt project file 'dbt_project.yml' not found in the current directory." + in result.output + ) + + +def test_init_dbt_template(runner: CliRunner, tmp_path: Path): + Path(tmp_path / "dbt_project.yml").touch() + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "init"], + input="2\n", + ) + assert result.exit_code == 0 + + config_path = tmp_path / "config.py" + assert config_path.exists() + + with open(config_path) as file: + config = file.read() + + assert ( + config + == """from pathlib import Path + +from sqlmesh.dbt.loader import sqlmesh_config + +config = sqlmesh_config(Path(__file__).parent) +""" + ) + + +@time_machine.travel(FREEZE_TIME) +def test_init_project_engine_configs(tmp_path): + engine_type_to_config = { + "redshift": "# concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # user: \n # password: \n # database: \n # host: \n # port: \n # source_address: \n # unix_sock: \n # ssl: \n # sslmode: \n # timeout: \n # tcp_keepalive: \n # application_name: \n # preferred_role: \n # principal_arn: \n # credentials_provider: \n # region: \n # cluster_identifier: \n # iam: \n # is_serverless: \n # serverless_acct_id: \n # serverless_work_group: \n # enable_merge: ", + "bigquery": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # method: oauth\n # project: \n # execution_project: \n # quota_project: \n # location: \n # keyfile: \n # keyfile_json: \n # token: \n # refresh_token: \n # client_id: \n # client_secret: \n # token_uri: \n # scopes: \n # impersonated_service_account: \n # job_creation_timeout_seconds: \n # job_execution_timeout_seconds: \n # job_retries: 1\n # job_retry_deadline_seconds: \n # priority: \n # maximum_bytes_billed: ", + "snowflake": "account: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # user: \n # password: \n # warehouse: \n # database: \n # role: \n # authenticator: \n # token: \n # host: \n # port: \n # application: Tobiko_SQLMesh\n # private_key: \n # private_key_path: \n # private_key_passphrase: \n # session_parameters: ", + "databricks": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # server_hostname: \n # http_path: \n # access_token: \n # auth_type: \n # oauth_client_id: \n # oauth_client_secret: \n # catalog: \n # http_headers: \n # session_configuration: \n # databricks_connect_server_hostname: \n # databricks_connect_access_token: \n # databricks_connect_cluster_id: \n # databricks_connect_use_serverless: False\n # force_databricks_connect: False\n # disable_databricks_connect: False\n # disable_spark_session: False", + "postgres": "host: \n user: \n password: \n port: \n database: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: True\n # pretty_sql: False\n # keepalives_idle: \n # connect_timeout: 10\n # role: \n # sslmode: \n # application_name: ", + } + + for engine_type, expected_config in engine_type_to_config.items(): + init_example_project(tmp_path, engine_type=engine_type) + + config_start = f"# --- Gateway Connection ---\ngateways:\n {engine_type}:\n connection:\n # For more information on configuring the connection to your execution engine, visit:\n # https://sqlmesh.readthedocs.io/en/stable/reference/configuration/#connection\n # https://sqlmesh.readthedocs.io/en/stable/integrations/engines/{engine_type}/#connection-options\n type: {engine_type}\n " + config_end = f""" + +default_gateway: {engine_type} + +# --- Model Defaults --- +# https://sqlmesh.readthedocs.io/en/stable/reference/model_configuration/#model-defaults + +model_defaults: + dialect: {DIALECT_TO_TYPE.get(engine_type)} + start: {yesterday_ds()} # Start date for backfill history + cron: '@daily' # Run models daily at 12am UTC (can override per model) + +# --- Linting Rules --- +# Enforce standards for your team +# https://sqlmesh.readthedocs.io/en/stable/guides/linter/ + +linter: + enabled: true + rules: + - ambiguousorinvalidcolumn + - invalidselectstarexpansion +""" + + with open(tmp_path / "config.yaml") as file: + config = file.read() + + assert config == f"{config_start}{expected_config}{config_end}" + + remove(tmp_path / "config.yaml") + + def test_render(runner: CliRunner, tmp_path: Path): create_example_project(tmp_path) diff --git a/tests/cli/test_integration_cli.py b/tests/cli/test_integration_cli.py index 7d8802d193..9b39b948f2 100644 --- a/tests/cli/test_integration_cli.py +++ b/tests/cli/test_integration_cli.py @@ -2,7 +2,7 @@ from pathlib import Path import pytest import subprocess -from sqlmesh.cli.example_project import init_example_project +from sqlmesh.cli.project_init import init_example_project from sqlmesh.utils import yaml import shutil import site @@ -47,7 +47,7 @@ def _invoke(sqlmesh_args: t.List[str], **kwargs: t.Any) -> subprocess.CompletedP @pytest.fixture def duckdb_example_project(tmp_path: Path) -> Path: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config_path = tmp_path / "config.yaml" # we need state to persist between invocations diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 873d25547f..e48bea318f 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -17,7 +17,7 @@ from sqlglot.optimizer.normalize_identifiers import normalize_identifiers from sqlmesh import Config, Context -from sqlmesh.cli.example_project import init_example_project +from sqlmesh.cli.project_init import init_example_project from sqlmesh.core.config import load_config_from_paths from sqlmesh.core.config.connection import ConnectionConfig import sqlmesh.core.dialect as d @@ -1583,7 +1583,7 @@ def _normalize_snowflake(name: str, prefix_regex: str = "(sqlmesh__)(.*)"): k: [_normalize_snowflake(name) for name in v] for k, v in object_names.items() } - init_example_project(tmp_path, ctx.dialect, schema_name=schema_name) + init_example_project(tmp_path, ctx.mark.split("_")[0], schema_name=schema_name) config = load_config_from_paths( Config, diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index 208d406b9a..2296ed3f23 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -4,7 +4,7 @@ from sqlglot import exp from sqlglot.optimizer.qualify_columns import quote_identifiers from sqlglot.helper import seq_get -from sqlmesh.cli.example_project import ProjectTemplate, init_example_project +from sqlmesh.cli.project_init import ProjectTemplate, init_example_project from sqlmesh.core.config import Config from sqlmesh.core.engine_adapter import BigQueryEngineAdapter from sqlmesh.core.engine_adapter.bigquery import _CLUSTERING_META_KEY @@ -210,7 +210,7 @@ def test_information_schema_view_external_model(ctx: TestContext, tmp_path: Path model_name = ctx.table("test") dependency = f"`{'.'.join(part.name for part in information_schema_tables.parts)}`" - init_example_project(tmp_path, dialect="bigquery", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="bigquery", template=ProjectTemplate.EMPTY) with open(tmp_path / "models" / "test.sql", "w", encoding="utf-8") as f: f.write( f""" diff --git a/tests/core/state_sync/test_export_import.py b/tests/core/state_sync/test_export_import.py index 8989d28f6b..0b22656d1e 100644 --- a/tests/core/state_sync/test_export_import.py +++ b/tests/core/state_sync/test_export_import.py @@ -4,7 +4,7 @@ from sqlmesh.core.state_sync.export_import import export_state, import_state from sqlmesh.utils.errors import SQLMeshError from sqlmesh.core import constants as c -from sqlmesh.cli.example_project import init_example_project +from sqlmesh.cli.project_init import init_example_project from sqlmesh.core.context import Context from sqlmesh.core.environment import Environment from sqlmesh.core.config import Config, GatewayConfig, DuckDBConnectionConfig, ModelDefaultsConfig @@ -74,7 +74,7 @@ def test_export_empty_state(tmp_path: Path, state_sync: StateSync) -> None: def test_export_entire_project( tmp_path: Path, example_project_config: Config, state_sync: StateSync ) -> None: - init_example_project(path=tmp_path, dialect="duckdb") + init_example_project(path=tmp_path, engine_type="duckdb") context = Context(paths=tmp_path, config=example_project_config, state_sync=state_sync) # prod @@ -159,7 +159,7 @@ def test_export_specific_environment( tmp_path: Path, example_project_config: Config, state_sync: StateSync ) -> None: output_file = tmp_path / "state_dump.json" - init_example_project(path=tmp_path, dialect="duckdb") + init_example_project(path=tmp_path, engine_type="duckdb") context = Context(paths=tmp_path, config=example_project_config, state_sync=state_sync) # create prod @@ -231,7 +231,7 @@ def test_export_local_state( tmp_path: Path, example_project_config: Config, state_sync: StateSync ) -> None: output_file = tmp_path / "state_dump.json" - init_example_project(path=tmp_path, dialect="duckdb") + init_example_project(path=tmp_path, engine_type="duckdb") context = Context(paths=tmp_path, config=example_project_config, state_sync=state_sync) # create prod @@ -385,7 +385,7 @@ def test_import_local_state_fails( tmp_path: Path, example_project_config: Config, state_sync: StateSync ) -> None: output_file = tmp_path / "state_dump.json" - init_example_project(path=tmp_path, dialect="duckdb") + init_example_project(path=tmp_path, engine_type="duckdb") context = Context(paths=tmp_path, config=example_project_config, state_sync=state_sync) export_state(state_sync, output_file, context.snapshots) @@ -400,7 +400,7 @@ def test_import_partial( tmp_path: Path, example_project_config: Config, state_sync: StateSync ) -> None: output_file = tmp_path / "state_dump.json" - init_example_project(path=tmp_path, dialect="duckdb") + init_example_project(path=tmp_path, engine_type="duckdb") context = Context(paths=tmp_path, config=example_project_config, state_sync=state_sync) # create prod @@ -453,7 +453,7 @@ def test_import_partial( def test_roundtrip(tmp_path: Path, example_project_config: Config, state_sync: StateSync) -> None: state_file = tmp_path / "state_dump.json" - init_example_project(path=tmp_path, dialect="duckdb") + init_example_project(path=tmp_path, engine_type="duckdb") context = Context(paths=tmp_path, config=example_project_config, state_sync=state_sync) # populate initial state @@ -525,7 +525,7 @@ def test_roundtrip(tmp_path: Path, example_project_config: Config, state_sync: S def test_roundtrip_includes_auto_restatements( tmp_path: Path, example_project_config: Config, state_sync: StateSync ) -> None: - init_example_project(path=tmp_path, dialect="duckdb") + init_example_project(path=tmp_path, engine_type="duckdb") # add a model with auto restatements (tmp_path / c.MODELS / "new_model.sql").write_text(""" diff --git a/tests/core/test_config.py b/tests/core/test_config.py index e3b7a8e612..47e063c559 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -1045,9 +1045,9 @@ def test_loader_for_migrated_dbt_project(tmp_path: Path): model_defaults: dialect: bigquery - - variables: - __dbt_project_name__: sushi + + variables: + __dbt_project_name__: sushi """) config = load_config_from_paths( @@ -1058,6 +1058,32 @@ def test_loader_for_migrated_dbt_project(tmp_path: Path): assert config.loader == MigratedDbtProjectLoader +def test_config_user_macro_function(tmp_path: Path) -> None: + config_path = tmp_path / "config.yaml" + config_path.write_text(""" + gateways: + bigquery: + connection: + type: bigquery + project: unit-test + + default_gateway: bigquery + + model_defaults: + dialect: bigquery + + default_target_environment: dev_{{ user() }} +""") + + with mock.patch("getpass.getuser", return_value="test_user"): + config = load_config_from_paths( + Config, + project_paths=[config_path], + ) + + assert config.default_target_environment == "dev_test_user" + + def test_environment_suffix_target_catalog(tmp_path: Path) -> None: config_path = tmp_path / "config.yaml" config_path.write_text(""" @@ -1065,13 +1091,13 @@ def test_environment_suffix_target_catalog(tmp_path: Path) -> None: warehouse: connection: type: duckdb - + default_gateway: warehouse model_defaults: dialect: duckdb - - environment_suffix_target: catalog + + environment_suffix_target: catalog """) config = load_config_from_paths( @@ -1087,13 +1113,13 @@ def test_environment_suffix_target_catalog(tmp_path: Path) -> None: warehouse: connection: type: duckdb - + default_gateway: warehouse model_defaults: dialect: duckdb - - environment_suffix_target: catalog + + environment_suffix_target: catalog environment_catalog_mapping: '.*': "foo" diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index ba33cb010b..90aba1c3bb 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -23,6 +23,7 @@ MSSQLConnectionConfig, _connection_config_validator, _get_engine_import_validator, + INIT_DISPLAY_INFO_TO_TYPE, ) from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.pydantic import PydanticModel @@ -1131,6 +1132,19 @@ class TestConfigC(PydanticModel): TestConfigC() +def test_engine_display_order(): + """ + Each engine's ConnectionConfig contains a display_order integer class var that is used to order the + interactive `sqlmesh init` engine choices. + + This test ensures that those integers begin with 1, are unique, and are sequential. + """ + display_numbers = [ + info[0] for info in sorted(INIT_DISPLAY_INFO_TO_TYPE.values(), key=lambda x: x[0]) + ] + assert display_numbers == list(range(1, len(display_numbers) + 1)) + + def test_mssql_engine_import_validator(): """Test that MSSQL import validator respects driver configuration.""" diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 276dd38afc..64757cad6b 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -15,7 +15,7 @@ from sqlglot.errors import SchemaError import sqlmesh.core.constants -from sqlmesh.cli.example_project import init_example_project +from sqlmesh.cli.project_init import init_example_project from sqlmesh.core.console import get_console, TerminalConsole from sqlmesh.core import dialect as d, constants as c from sqlmesh.core.config import ( @@ -2177,7 +2177,7 @@ def test_audit(): def test_prompt_if_uncategorized_snapshot(mocker: MockerFixture, tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config( model_defaults=ModelDefaultsConfig(dialect="duckdb"), diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index f6e696ab01..81e8d2dcb4 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -25,7 +25,7 @@ from sqlmesh import CustomMaterialization -from sqlmesh.cli.example_project import init_example_project +from sqlmesh.cli.project_init import init_example_project from sqlmesh.core import constants as c from sqlmesh.core import dialect as d from sqlmesh.core.config import ( @@ -4005,7 +4005,7 @@ def fail_auto_restatement(evaluator, start: exp.Expression, **kwargs: t.Any) -> def test_plan_twice_with_star_macro_yields_no_diff(tmp_path: Path): - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") star_model_definition = """ MODEL ( diff --git a/tests/core/test_loader.py b/tests/core/test_loader.py index a616f520ef..b3d605e353 100644 --- a/tests/core/test_loader.py +++ b/tests/core/test_loader.py @@ -1,6 +1,6 @@ import pytest from pathlib import Path -from sqlmesh.cli.example_project import init_example_project +from sqlmesh.cli.project_init import init_example_project from sqlmesh.core.config import Config, ModelDefaultsConfig from sqlmesh.core.context import Context from sqlmesh.utils.errors import ConfigError @@ -71,7 +71,7 @@ def test_duplicate_model_names_different_kind(tmp_path: Path, sample_models): else: model_2, model_3 = models[0], None - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) path_1: Path = tmp_path / model_1["path"] @@ -101,7 +101,7 @@ def duplicate_model_path(fpath): return Path(fpath).parent / ("duplicate" + Path(fpath).suffix) model = sample_models[0] - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) path_1: Path = tmp_path / model["path"] @@ -121,7 +121,7 @@ def duplicate_model_path(fpath): @pytest.mark.registry_isolation def test_duplicate_python_model_names_raise_error(tmp_path: Path) -> None: """Test python models with duplicate model names raises ConfigError if the functions are not identical.""" - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) model_name = "test_schema.test_model" @@ -170,7 +170,7 @@ def execute( @pytest.mark.slow def test_duplicate_python_model_names_no_error(tmp_path: Path) -> None: """Test python models with duplicate model names raises no error if the functions are identical.""" - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) model_name = "test_schema.test_model" @@ -207,7 +207,7 @@ def my_model(context, **kwargs): def test_load_migrated_dbt_adapter_dispatch_macros(tmp_path: Path): - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") migrated_package_path = tmp_path / "macros" / c.MIGRATED_DBT_PACKAGES / "dbt_utils" migrated_package_path.mkdir(parents=True) @@ -273,7 +273,7 @@ def test_load_migrated_dbt_adapter_dispatch_macros(tmp_path: Path): def test_load_migrated_dbt_adapter_dispatch_macros_in_different_packages(tmp_path: Path): # some things like dbt.current_timestamp() dispatch to macros in a different package - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") migrated_package_path_dbt = tmp_path / "macros" / c.MIGRATED_DBT_PACKAGES / "dbt" migrated_package_path_dbt_duckdb = tmp_path / "macros" / c.MIGRATED_DBT_PACKAGES / "dbt_duckdb" diff --git a/tests/core/test_model.py b/tests/core/test_model.py index ad083e9ae2..01b6606074 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -12,7 +12,7 @@ from sqlglot import exp, parse_one from sqlglot.errors import ParseError from sqlglot.schema import MappingSchema -from sqlmesh.cli.example_project import init_example_project, ProjectTemplate +from sqlmesh.cli.project_init import init_example_project, ProjectTemplate from sqlmesh.core.environment import EnvironmentNamingInfo from sqlmesh.core.model.kind import TimeColumn, ModelKindName from pydantic import ValidationError @@ -481,7 +481,7 @@ def test_model_missing_audits(tmp_path: Path): def test_project_is_set_in_standalone_audit(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) db_path = str(tmp_path / "db.db") db_connection = DuckDBConnectionConfig(database=db_path) @@ -697,7 +697,7 @@ def test_no_model_statement(tmp_path: Path): load_sql_based_model(expressions) # Name inference is enabled => MODEL (...) not required - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") test_sql_file = tmp_path / "models/test_schema/test_model.sql" test_sql_file.parent.mkdir(parents=True, exist_ok=True) @@ -3088,7 +3088,7 @@ def test_model_cache(tmp_path: Path, mocker: MockerFixture): @pytest.mark.slow def test_model_cache_gateway(tmp_path: Path, mocker: MockerFixture): - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") db_path = str(tmp_path / "db.db") config = Config( @@ -3111,7 +3111,7 @@ def test_model_cache_gateway(tmp_path: Path, mocker: MockerFixture): @pytest.mark.slow def test_model_cache_default_catalog(tmp_path: Path, mocker: MockerFixture): - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") Context(paths=tmp_path) patched_cache_put = mocker.patch("sqlmesh.utils.cache.FileCache.put") @@ -7281,7 +7281,7 @@ def test_model_table_name_inference( ], ) def test_python_model_name_inference(tmp_path: Path, path: str, expected_name: str) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config( model_defaults=ModelDefaultsConfig(dialect="duckdb"), model_naming=NameInferenceConfig(infer_names=True), @@ -7301,7 +7301,7 @@ def my_model(context, **kwargs): def test_python_model_name_inference_multiple_models(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config( model_defaults=ModelDefaultsConfig(dialect="duckdb"), model_naming=NameInferenceConfig(infer_names=True), @@ -8239,7 +8239,7 @@ def test_cache(): def test_snowflake_macro_func_as_table(tmp_path: Path): - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") custom_macro_file = tmp_path / "macros/custom_macros.py" custom_macro_file.parent.mkdir(parents=True, exist_ok=True) @@ -8910,7 +8910,7 @@ def test_partition_interval_unit(): def test_model_blueprinting(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) db_path = str(tmp_path / "db.db") db_connection = DuckDBConnectionConfig(database=db_path) @@ -9071,7 +9071,7 @@ def entrypoint(evaluator): def test_dynamic_blueprinting_using_custom_macro(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) dynamic_template_sql = tmp_path / "models/dynamic_template_custom_macro.sql" dynamic_template_sql.parent.mkdir(parents=True, exist_ok=True) @@ -9134,7 +9134,7 @@ def gen_blueprints(evaluator): def test_dynamic_blueprinting_using_each(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) dynamic_template_sql = tmp_path / "models/dynamic_template_each.sql" dynamic_template_sql.parent.mkdir(parents=True, exist_ok=True) @@ -9181,7 +9181,7 @@ def entrypoint(evaluator): def test_single_blueprint(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) single_blueprint = tmp_path / "models/single_blueprint.sql" single_blueprint.parent.mkdir(parents=True, exist_ok=True) @@ -9206,7 +9206,7 @@ def test_single_blueprint(tmp_path: Path) -> None: def test_blueprinting_with_quotes(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) template_with_quoted_vars = tmp_path / "models/template_with_quoted_vars.sql" template_with_quoted_vars.parent.mkdir(parents=True, exist_ok=True) @@ -9239,7 +9239,7 @@ def test_blueprinting_with_quotes(tmp_path: Path) -> None: def test_blueprint_variable_precedence_sql(tmp_path: Path, assert_exp_eq: t.Callable) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) blueprint_variables = tmp_path / "models/blueprint_variables.sql" blueprint_variables.parent.mkdir(parents=True, exist_ok=True) @@ -9324,7 +9324,7 @@ def test_blueprint_variable_precedence_sql(tmp_path: Path, assert_exp_eq: t.Call def test_blueprint_variable_jinja(tmp_path: Path, assert_exp_eq: t.Callable) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) blueprint_variables = tmp_path / "models/blueprint_variables.sql" blueprint_variables.parent.mkdir(parents=True, exist_ok=True) @@ -9373,7 +9373,7 @@ def test_blueprint_variable_jinja(tmp_path: Path, assert_exp_eq: t.Callable) -> def test_blueprint_variable_precedence_python(tmp_path: Path, mocker: MockerFixture) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) blueprint_variables = tmp_path / "models/blueprint_variables.py" blueprint_variables.parent.mkdir(parents=True, exist_ok=True) @@ -9598,7 +9598,7 @@ def test_data_hash_unchanged_when_column_type_uses_default_dialect(): def test_transitive_dependency_of_metadata_only_object_is_metadata_only(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) test_model = tmp_path / "models/test_model.sql" test_model.parent.mkdir(parents=True, exist_ok=True) @@ -9677,7 +9677,7 @@ def metadata_macro(evaluator): def test_non_metadata_object_takes_precedence_over_metadata_only_object(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) test_model = tmp_path / "models/test_model.sql" test_model.parent.mkdir(parents=True, exist_ok=True) @@ -9733,7 +9733,7 @@ def m2(evaluator): def test_macros_referenced_in_metadata_statements_and_properties_are_metadata_only( tmp_path: Path, ) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) test_model = tmp_path / "models/test_model.sql" test_model.parent.mkdir(parents=True, exist_ok=True) @@ -10000,7 +10000,7 @@ def test_python_env_references_are_unequal_but_point_to_same_definition(tmp_path # in sqlmesh.utils.metaprogramming.import_python_file. Depending on the module loading # order, we could get a "duplicate symbol in python env" error, even though the references # essentially pointed to the same definition (e.g. function or class). - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) db_path = str(tmp_path / "db.db") db_connection = DuckDBConnectionConfig(database=db_path) @@ -10082,7 +10082,7 @@ def _patched_glob_paths(path, *args, **kwargs): def test_unequal_duplicate_python_env_references_are_prohibited(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) db_path = str(tmp_path / "db.db") db_connection = DuckDBConnectionConfig(database=db_path) @@ -10123,7 +10123,7 @@ def f(): def test_semicolon_is_not_included_in_model_state(tmp_path, assert_exp_eq): - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) db_connection = DuckDBConnectionConfig(database=str(tmp_path / "db.db")) config = Config( @@ -10363,7 +10363,7 @@ def unimportant_testing_macro(evaluator, *projections): def test_extract_schema_in_post_statement(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) @@ -10406,7 +10406,7 @@ def check_self_schema(evaluator): def test_model_relies_on_os_getenv(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY) + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) (tmp_path / "macros" / "getenv_macro.py").write_text( """ diff --git a/tests/core/test_test.py b/tests/core/test_test.py index f4d495801a..fd47ea57b2 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -14,7 +14,7 @@ from sqlglot import exp from IPython.utils.capture import capture_output -from sqlmesh.cli.example_project import init_example_project +from sqlmesh.cli.project_init import init_example_project from sqlmesh.core import constants as c from sqlmesh.core.config import ( Config, @@ -1406,7 +1406,7 @@ def test_gateway(copy_to_temp_path: t.Callable, mocker: MockerFixture) -> None: def test_generate_input_data_using_sql(mocker: MockerFixture, tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config( default_connection=DuckDBConnectionConfig(), model_defaults=ModelDefaultsConfig(dialect="duckdb"), @@ -1592,7 +1592,7 @@ def execute(context, start, end, execution_time, **kwargs): def test_variable_usage(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") variables = {"gold": "gold_db", "silver": "silver_db"} incorrect_variables = {"gold": "foo", "silver": "bar"} @@ -1873,7 +1873,7 @@ def test_unknown_model_warns(mocker: MockerFixture) -> None: def test_test_generation(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config( default_connection=DuckDBConnectionConfig(), @@ -2004,7 +2004,7 @@ def create_test(context: Context, query: str): ) return load_yaml(context.path / c.TESTS / "test_foo.yaml") - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config( default_connection=DuckDBConnectionConfig(), @@ -2023,7 +2023,7 @@ def create_test(context: Context, query: str): def test_test_generation_with_timestamp(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config( default_connection=DuckDBConnectionConfig(), @@ -2060,7 +2060,7 @@ def test_test_generation_with_timestamp(tmp_path: Path) -> None: def test_test_generation_with_decimal(tmp_path: Path, mocker: MockerFixture) -> None: from decimal import Decimal - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config( default_connection=DuckDBConnectionConfig(), @@ -2096,7 +2096,7 @@ def test_test_generation_with_decimal(tmp_path: Path, mocker: MockerFixture) -> def test_test_generation_with_recursive_ctes(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config( default_connection=DuckDBConnectionConfig(), @@ -2128,7 +2128,7 @@ def test_test_generation_with_recursive_ctes(tmp_path: Path) -> None: def test_test_with_gateway_specific_model(tmp_path: Path, mocker: MockerFixture) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config( gateways={ @@ -2221,7 +2221,7 @@ def test_test_with_resolve_template_macro(tmp_path: Path): @use_terminal_console def test_test_output(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") original_test_file = tmp_path / "tests" / "test_full_model.yaml" @@ -2385,7 +2385,7 @@ def test_test_output(tmp_path: Path) -> None: @use_terminal_console def test_test_output_with_invalid_model_name(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") wrong_test_file = tmp_path / "tests" / "test_incorrect_model_name.yaml" wrong_test_file.write_text( @@ -2430,7 +2430,7 @@ def test_test_output_with_invalid_model_name(tmp_path: Path) -> None: def test_number_of_tests_found(tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") # Example project contains 1 test and we add a new file with 2 tests test_file = tmp_path / "tests" / "test_new.yaml" @@ -2508,7 +2508,7 @@ def test_freeze_time_concurrent(tmp_path: Path) -> None: @macro() def test_datetime_now(evaluator): return exp.cast(exp.Literal.string(datetime.datetime.now(tz=datetime.timezone.utc)), exp.DataType.Type.DATE) - + @macro() def test_sqlglot_expr(evaluator): return exp.CurrentDate().sql(evaluator.dialect) @@ -2737,7 +2737,7 @@ def test_timestamp_normalization() -> None: @use_terminal_console def test_disable_test_logging_if_no_tests_found(mocker: MockerFixture, tmp_path: Path) -> None: - init_example_project(tmp_path, dialect="duckdb") + init_example_project(tmp_path, engine_type="duckdb") config = Config( default_connection=DuckDBConnectionConfig(), diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index a4d98e1963..f606e574cc 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -134,7 +134,7 @@ def test_context(notebook, convert_all_html_output_to_text, get_all_html_output, def test_init(tmp_path, notebook, convert_all_html_output_to_text, get_all_html_output): with pytest.raises(UsageError, match="the following arguments are required: path"): notebook.run_line_magic(magic_name="init", line="") - with pytest.raises(UsageError, match="the following arguments are required: sql_dialect"): + with pytest.raises(UsageError, match="the following arguments are required: engine"): notebook.run_line_magic(magic_name="init", line="foo") with capture_output() as output: notebook.run_line_magic(magic_name="init", line=f"{tmp_path} duckdb") diff --git a/tests/lsp/test_reference_model_column_prefix.py b/tests/lsp/test_reference_model_column_prefix.py index 88be689810..01b91de570 100644 --- a/tests/lsp/test_reference_model_column_prefix.py +++ b/tests/lsp/test_reference_model_column_prefix.py @@ -1,7 +1,7 @@ from pathlib import Path from lsprotocol.types import Position -from sqlmesh.cli.example_project import init_example_project +from sqlmesh.cli.project_init import init_example_project from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget from sqlmesh.lsp.reference import get_all_references @@ -84,7 +84,9 @@ def test_column_prefix_references_are_found(): def test_quoted_uppercase_table_and_column_references(tmp_path: Path): # Initialize example project in temporary directory with case sensitive normalization - init_example_project(tmp_path, dialect="duckdb,normalization_strategy=case_sensitive") + init_example_project( + tmp_path, engine_type="duckdb", dialect="duckdb,normalization_strategy=case_sensitive" + ) # Create a model with quoted uppercase schema and table names models_dir = tmp_path / "models" From c6a0be91f7f801a75ead8ca848e1dc378d415427 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:07:29 +0300 Subject: [PATCH 0437/1056] Fix: Changes to before_all/after_all alone now trigger virtual updates (#4788) --- sqlmesh/core/plan/builder.py | 1 + tests/core/test_context.py | 88 +++++++++++++++++++++++++++++++++++- tests/core/test_plan.py | 87 ++++++++++++++++++++++++++++++++++- 3 files changed, 174 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index b2ff0a087c..f3f78e1714 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -835,6 +835,7 @@ def _ensure_new_env_with_changes(self) -> None: and not self._include_unmodified and self._context_diff.is_new_environment and not self._context_diff.has_snapshot_changes + and not self._context_diff.has_environment_statements_changes and not self._backfill_models ): raise NoChangesPlanError( diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 64757cad6b..e08f5346ea 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -52,7 +52,13 @@ to_timestamp, yesterday_ds, ) -from sqlmesh.utils.errors import ConfigError, SQLMeshError, LinterError, PlanError +from sqlmesh.utils.errors import ( + ConfigError, + SQLMeshError, + LinterError, + PlanError, + NoChangesPlanError, +) from sqlmesh.utils.metaprogramming import Executable from tests.utils.test_helpers import use_terminal_console from tests.utils.test_filesystem import create_temp_file @@ -2218,3 +2224,83 @@ def test_plan_explain_skips_tests(sushi_context: Context, mocker: MockerFixture) 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) + + +def test_dev_environment_virtual_update_with_environment_statements(tmp_path: Path) -> None: + models_dir = tmp_path / "models" + models_dir.mkdir() + model_sql = """ + MODEL ( + name db.test_model, + kind FULL + ); + + SELECT 1 as id, 'test' as name + """ + + with open(models_dir / "test_model.sql", "w") as f: + f.write(model_sql) + + # Create initial context without environment statements + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + gateways={"duckdb": GatewayConfig(connection=DuckDBConnectionConfig())}, + ) + + context = Context(paths=tmp_path, config=config) + + # First, apply to production + context.plan("prod", auto_apply=True, no_prompts=True) + + # Try to create dev environment without changes (should fail) + with pytest.raises(NoChangesPlanError, match="Creating a new environment requires a change"): + context.plan("dev", auto_apply=True, no_prompts=True) + + # Now create a new context with only new environment statements + config_with_statements = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + gateways={"duckdb": GatewayConfig(connection=DuckDBConnectionConfig())}, + before_all=["CREATE TABLE IF NOT EXISTS audit_log (id INT, action VARCHAR(100))"], + after_all=["INSERT INTO audit_log VALUES (1, 'environment_created')"], + ) + + context_with_statements = Context(paths=tmp_path, config=config_with_statements) + + # This should succeed because environment statements are different + context_with_statements.plan("dev", auto_apply=True, no_prompts=True) + env = context_with_statements.state_reader.get_environment("dev") + assert env is not None + assert env.name == "dev" + + # Verify the environment statements were stored + stored_statements = context_with_statements.state_reader.get_environment_statements("dev") + assert len(stored_statements) == 1 + assert stored_statements[0].before_all == [ + "CREATE TABLE IF NOT EXISTS audit_log (id INT, action VARCHAR(100))" + ] + assert stored_statements[0].after_all == [ + "INSERT INTO audit_log VALUES (1, 'environment_created')" + ] + + # Update environment statements and plan again (should trigger another virtual update) + config_updated_statements = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + gateways={"duckdb": GatewayConfig(connection=DuckDBConnectionConfig())}, + before_all=[ + "CREATE TABLE IF NOT EXISTS audit_log (id INT, action VARCHAR(100))", + "CREATE TABLE IF NOT EXISTS metrics (metric_name VARCHAR(50), value INT)", + ], + after_all=["INSERT INTO audit_log VALUES (1, 'environment_created')"], + ) + + context_updated = Context(paths=tmp_path, config=config_updated_statements) + context_updated.plan("dev", auto_apply=True, no_prompts=True) + + # Verify the updated statements were stored + updated_statements = context_updated.state_reader.get_environment_statements("dev") + assert len(updated_statements) == 1 + assert len(updated_statements[0].before_all) == 2 + assert ( + updated_statements[0].before_all[1] + == "CREATE TABLE IF NOT EXISTS metrics (metric_name VARCHAR(50), value INT)" + ) diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 4b02ae6c4e..045f5bbada 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -41,7 +41,7 @@ to_timestamp, yesterday_ds, ) -from sqlmesh.utils.errors import PlanError +from sqlmesh.utils.errors import PlanError, NoChangesPlanError from sqlmesh.utils.rich import strip_ansi_codes @@ -3204,3 +3204,88 @@ def _build_plan() -> Plan: assert to_datetime(plan.start) == to_datetime(output_start) assert to_datetime(plan.end) == to_datetime(output_end) assert to_datetime(plan.execution_time) == to_datetime(output_execution_time) + + +def test_environment_statements_change_allows_dev_environment_creation(make_snapshot): + snapshot = make_snapshot( + SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one("select 1, ds"), + kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"), + ) + ) + + # First context diff of a new 'dev' environment without environment statements + context_diff_no_statements = ContextDiff( + environment="dev", + is_new_environment=True, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={}, + snapshots={snapshot.snapshot_id: snapshot}, + new_snapshots={}, + previous_plan_id=None, + previously_promoted_snapshot_ids={snapshot.snapshot_id}, + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + previous_environment_statements=[], + ) + + # Should fail because no changes + plan_builder = PlanBuilder( + context_diff_no_statements, + is_dev=True, + ) + + with pytest.raises(NoChangesPlanError, match="Creating a new environment requires a change"): + plan_builder.build() + + # Now create context diff with environment statements + environment_statements = [ + EnvironmentStatements( + before_all=["CREATE TABLE IF NOT EXISTS test_table (id INT)"], + after_all=[], + python_env={}, + jinja_macros=None, + ) + ] + + context_diff_with_statements = ContextDiff( + environment="dev", + is_new_environment=True, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={}, + snapshots={snapshot.snapshot_id: snapshot}, + new_snapshots={}, + previous_plan_id=None, + previously_promoted_snapshot_ids={snapshot.snapshot_id}, + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=environment_statements, + previous_environment_statements=[], + ) + + # Should succeed because there are environment statements changes + plan_builder_with_statements = PlanBuilder( + context_diff_with_statements, + is_dev=True, + ) + + # Test that allows creating a dev environment without other changes + plan = plan_builder_with_statements.build() + assert plan is not None + assert plan.context_diff.has_environment_statements_changes + assert plan.context_diff.environment_statements == environment_statements From 50a219caedb592b3d520ec711eb4639ddb07dd92 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:55:54 +0300 Subject: [PATCH 0438/1056] Fix: simplify complex expressions when validating boolean pydantic fields (#4790) --- sqlmesh/core/model/common.py | 12 ++++++++++-- tests/core/test_model.py | 18 +++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index 8f2d9e8ff8..1d8ae14442 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -336,10 +336,18 @@ def parse_expression( def parse_bool(v: t.Any) -> bool: - if isinstance(v, exp.Boolean): - return v.this if isinstance(v, exp.Expression): + if not isinstance(v, exp.Boolean): + from sqlglot.optimizer.simplify import simplify + + # Try to reduce expressions like (1 = 1) (see: T-SQL boolean generation) + v = simplify(v) + + if isinstance(v, exp.Boolean): + return v.this + return str_to_bool(v.name) + return str_to_bool(str(v or "")) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 01b6606074..7c65f25889 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -10445,7 +10445,7 @@ def test_invalid_sql_model_query() -> None: load_sql_based_model(expressions) -def test_query_label_and_authorization_macro(): +def test_query_label_and_authorization_macro() -> None: @macro() def test_query_label_macro(evaluator): return "[('key', 'value')]" @@ -10478,3 +10478,19 @@ def test_authorization_macro(evaluator): "query_label": d.parse_one("[('key', 'value')]"), "authorization": d.parse_one("'test_authorization'"), } + + +def test_boolean_property_validation() -> None: + expressions = d.parse( + """ + MODEL ( + name db.table, + enabled @IF(TRUE, TRUE, FALSE), + dialect tsql + ); + + SELECT 1 AS c; + """ + ) + model = load_sql_based_model(expressions, dialect="tsql") + assert model.enabled From f5ebfbb9f6a1b0b5cd74bbc9c21a59badd8f413d Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:02:02 +0200 Subject: [PATCH 0439/1056] feat(vscode): print env into terminal (#4789) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- vscode/extension/package.json | 5 + .../src/commands/printEnvironment.ts | 40 +++++++ vscode/extension/src/extension.ts | 2 + vscode/extension/src/lsp/lsp.ts | 18 +-- .../src/utilities/sqlmesh/sqlmesh.ts | 103 ++++++++++++------ 5 files changed, 120 insertions(+), 48 deletions(-) create mode 100644 vscode/extension/src/commands/printEnvironment.ts diff --git a/vscode/extension/package.json b/vscode/extension/package.json index aefc1d736c..997c8d50c2 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -73,6 +73,11 @@ "title": "SQLMesh: Restart Servers", "description": "SQLMesh" }, + { + "command": "sqlmesh.printEnvironment", + "title": "SQLMesh: Print Environment Variables", + "description": "SQLMesh" + }, { "command": "sqlmesh.signin", "title": "SQLMesh: Sign in to Tobiko Cloud", diff --git a/vscode/extension/src/commands/printEnvironment.ts b/vscode/extension/src/commands/printEnvironment.ts new file mode 100644 index 0000000000..41e74e40d3 --- /dev/null +++ b/vscode/extension/src/commands/printEnvironment.ts @@ -0,0 +1,40 @@ +import * as vscode from 'vscode' +import { getSqlmeshEnvironment } from '../utilities/sqlmesh/sqlmesh' +import { isErr } from '@bus/result' +import { IS_WINDOWS } from '../utilities/isWindows' + +export function printEnvironment() { + return async () => { + const envResult = await getSqlmeshEnvironment() + + if (isErr(envResult)) { + await vscode.window.showErrorMessage(envResult.error) + return + } + + const env = envResult.value + + // Create a new terminal with the SQLMesh environment + const terminal = vscode.window.createTerminal({ + name: 'SQLMesh Environment', + env: env, + }) + + // Show the terminal + terminal.show() + + // Run the appropriate command to display environment variables + if (IS_WINDOWS) { + // On Windows, use 'set' command + terminal.sendText('set') + } else { + // On Unix-like systems, use 'env' command + terminal.sendText('env | sort') + } + + // Show a notification + vscode.window.showInformationMessage( + 'SQLMesh environment variables displayed in terminal', + ) + } +} diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 0525397cff..10e5b9953c 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -14,6 +14,7 @@ import { signIn } from './commands/signin' import { signInSpecifyFlow } from './commands/signinSpecifyFlow' import { renderModel, reRenderModelForSourceFile } from './commands/renderModel' import { stop } from './commands/stop' +import { printEnvironment } from './commands/printEnvironment' import { isErr } from '@bus/result' import { handleError } from './utilities/errors' import { selector, completionProvider } from './completion/completion' @@ -161,6 +162,7 @@ export async function activate(context: vscode.ExtensionContext) { await restart() }), registerCommand(`sqlmesh.stop`, stop(lspClient)), + registerCommand(`sqlmesh.printEnvironment`, printEnvironment()), ) const result = await lspClient.start() diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 769a0b3753..ccf3981eb2 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -81,14 +81,7 @@ export class LSPClient implements Disposable { transport: TransportKind.stdio, options: { cwd: workspacePath, - // TODO: This is a temporary fix to avoid the issue with the LSP server - // crashing when the number of workers is too high. This is a workaround - // to avoid the issue. Once fixed, we should remove the whole env block. - env: { - MAX_FORK_WORKERS: '1', - ...process.env, - ...sqlmesh.value.env, - }, + env: sqlmesh.value.env, }, args: sqlmesh.value.args, }, @@ -97,14 +90,7 @@ export class LSPClient implements Disposable { transport: TransportKind.stdio, options: { cwd: workspacePath, - env: { - // TODO: This is a temporary fix to avoid the issue with the LSP server - // crashing when the number of workers is too high. This is a workaround - // to avoid the issue. Once fixed, we should remove the whole env block. - MAX_FORK_WORKERS: '1', - ...process.env, - ...sqlmesh.value.env, - }, + env: sqlmesh.value.env, }, args: sqlmesh.value.args, }, diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 06f05f7792..d95017a2ca 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -21,6 +21,41 @@ export interface SqlmeshExecInfo { args: string[] } +/** + * Gets the current SQLMesh environment variables that would be used for execution. + * This is useful for debugging and understanding the environment configuration. + * + * @returns A Result containing the environment variables or an error + */ +export async function getSqlmeshEnvironment(): Promise, string>> { + const interpreterDetails = await getInterpreterDetails() + const envVariables = await getPythonEnvVariables() + if (isErr(envVariables)) { + return err(envVariables.error) + } + + const binPath = interpreterDetails.binPath + const virtualEnvPath = binPath && interpreterDetails.isVirtualEnvironment + ? path.dirname(path.dirname(binPath)) // binPath points to bin dir in venv + : binPath ? path.dirname(binPath) : undefined + + const env: Record = { + ...process.env, + ...envVariables.value, + PYTHONPATH: interpreterDetails.path?.[0] ?? '', + } + + if (virtualEnvPath) { + env['VIRTUAL_ENV'] = virtualEnvPath + } + + if (binPath) { + env['PATH'] = `${binPath}${path.delimiter}${process.env.PATH || ''}` + } + + return ok(env) +} + /** * Returns true if the current project is a Tcloud project. To detect this we, * 1. Check if the project has a tcloud.yaml file in the project root. If it does, we assume it's a Tcloud project. @@ -68,23 +103,17 @@ export const getTcloudBin = async (): Promise if (!fs.existsSync(binPath)) { return err({type: 'tcloud_bin_not_found'}) } - const envVariables = await getPythonEnvVariables() - if (isErr(envVariables)) { + const env = await getSqlmeshEnvironment() + if (isErr(env)) { return err({ type: 'generic', - message: envVariables.error, + message: env.error, }) } return ok({ bin: binPath, workspacePath: interpreterDetails.resource?.fsPath ?? '', - env: { - ...process.env, - ...envVariables.value, - PYTHONPATH: interpreterDetails.path[0], - VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!), - PATH: `${interpreterDetails.binPath!}${path.delimiter}${process.env.PATH || ''}`, - }, + env: env.value, args: [], }) } @@ -300,16 +329,17 @@ export const sqlmeshExec = async (): Promise< } const binPath = path.join(interpreterDetails.binPath!, sqlmesh) traceLog(`Bin path: ${binPath}`) + const env = await getSqlmeshEnvironment() + if (isErr(env)) { + return err({ + type: 'generic', + message: env.error, + }) + } return ok({ bin: binPath, workspacePath, - env: { - ...process.env, - ...envVariables.value, - PYTHONPATH: interpreterDetails.path?.[0], - VIRTUAL_ENV: path.dirname(path.dirname(interpreterDetails.binPath!)), // binPath now points to bin dir - PATH: `${interpreterDetails.binPath!}${path.delimiter}${process.env.PATH || ''}`, - }, + env: env.value, args: [], }) } else { @@ -319,13 +349,17 @@ export const sqlmeshExec = async (): Promise< type: 'sqlmesh_not_found', }) } + const env = await getSqlmeshEnvironment() + if (isErr(env)) { + return err({ + type: 'generic', + message: env.error, + }) + } return ok({ bin: sqlmesh, workspacePath, - env: { - ...process.env, - ...envVariables.value, - }, + env: env.value, args: [], }) } @@ -455,19 +489,27 @@ export const sqlmeshLspExec = async (): Promise< if (isErr(ensuredDependencies)) { return ensuredDependencies } + const env = await getSqlmeshEnvironment() + if (isErr(env)) { + return err({ + type: 'generic', + message: env.error, + }) + } return ok({ bin: binPath, workspacePath, - env: { - ...process.env, - ...envVariables.value, - PYTHONPATH: interpreterDetails.path?.[0], - VIRTUAL_ENV: path.dirname(path.dirname(interpreterDetails.binPath!)), // binPath now points to bin dir - PATH: `${interpreterDetails.binPath!}${path.delimiter}${process.env.PATH || ''}`, // binPath already points to the bin directory - }, + env: env.value, args: [], }) } else { + const env = await getSqlmeshEnvironment() + if (isErr(env)) { + return err({ + type: 'generic', + message: env.error, + }) + } const exists = await doesExecutableExist(sqlmeshLSP) if (!exists) { return err({ @@ -477,10 +519,7 @@ export const sqlmeshLspExec = async (): Promise< return ok({ bin: sqlmeshLSP, workspacePath, - env: { - ...process.env, - ...envVariables.value, - }, + env: env.value, args: [], }) } From d0838887fb528fe73cd1c274197c11b2c11b090b Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:34:05 +0300 Subject: [PATCH 0440/1056] Chore: fix backslash warning (#4791) --- sqlmesh/cli/project_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh/cli/project_init.py b/sqlmesh/cli/project_init.py index 66b327ef75..613ea72c45 100644 --- a/sqlmesh/cli/project_init.py +++ b/sqlmesh/cli/project_init.py @@ -435,7 +435,7 @@ def _init_integer_prompt( def _init_display_choices(values_dict: t.Dict[str, str], console: Console) -> t.Dict[int, str]: display_num_to_value = {} for i, value_str in enumerate(values_dict.keys()): - console.print(f" \[{i + 1}] {' ' if i < 9 else ''}{value_str} {values_dict[value_str]}") + console.print(f" \\[{i + 1}] {' ' if i < 9 else ''}{value_str} {values_dict[value_str]}") display_num_to_value[i + 1] = value_str console.print("") return display_num_to_value From 166ddb1023ddae32873232a4f53c3f2ff6ed7415 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 24 Jun 2025 07:50:31 +1200 Subject: [PATCH 0441/1056] Fix(cicd_bot): When enable_deploy_command: true, dont show message about required approvers (#4782) --- sqlmesh/integrations/github/cicd/command.py | 4 +- .../integrations/github/cicd/controller.py | 4 +- .../github/cicd/test_github_commands.py | 71 +++++++++++++++++++ .../github/cicd/test_integration.py | 24 ++++--- 4 files changed, 91 insertions(+), 12 deletions(-) diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index cedee1fa58..bf2863145b 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -276,7 +276,9 @@ def _run_all(controller: GithubController) -> None: if has_required_approval and prod_plan_generated and controller.pr_targets_prod_branch: deployed_to_prod = _deploy_production(controller) elif is_auto_deploying_prod: - if not has_required_approval: + if controller.deploy_command_enabled and not has_required_approval: + skip_reason = "Skipped Deploying to Production because a `/deploy` command has not been detected yet" + elif controller.do_required_approval_check and not has_required_approval: skip_reason = ( "Skipped Deploying to Production because a required approver has not approved" ) diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 5a0ad36d71..f179834e2b 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -1022,7 +1022,7 @@ def conclusion_handler( conclusion_to_title = { GithubCheckConclusion.SUCCESS: "Deployed to Prod", GithubCheckConclusion.CANCELLED: "Cancelled deploying to prod", - GithubCheckConclusion.SKIPPED: skip_reason, + GithubCheckConclusion.SKIPPED: "Skipped deployment", GithubCheckConclusion.FAILURE: "Failed to deploy to prod", GithubCheckConclusion.ACTION_REQUIRED: "Failed due to error applying plan", } @@ -1031,7 +1031,7 @@ def conclusion_handler( or f"Got an unexpected conclusion: {conclusion.value}" ) if conclusion.is_skipped: - summary = title + summary = skip_reason elif conclusion.is_failure: captured_errors = self._console.consume_captured_errors() summary = ( diff --git a/tests/integrations/github/cicd/test_github_commands.py b/tests/integrations/github/cicd/test_github_commands.py index f5098ac525..5f1dfd0a91 100644 --- a/tests/integrations/github/cicd/test_github_commands.py +++ b/tests/integrations/github/cicd/test_github_commands.py @@ -1295,3 +1295,74 @@ def test_comment_command_deploy_prod_not_enabled( with open(github_output_file, "r", encoding="utf-8") as f: output = f.read() assert output == "" + + +def test_comment_command_deploy_prod_no_deploy_detected_yet( + github_client, + make_controller, + make_mock_check_run, + make_mock_issue_comment, + tmp_path: pathlib.Path, + mocker: MockerFixture, +): + """ + Scenario: + - PR is not merged + - No requred approvers defined + - Tests passed + - PR Merge Method defined + - Deploy command enabled but not yet triggered + + Outcome: + - "Prod Environment Synced" step should explain the reason why it was skipped is because /deploy has not yet been detected + """ + mock_repo = github_client.get_repo() + mock_repo.create_check_run = mocker.MagicMock( + side_effect=lambda **kwargs: make_mock_check_run(**kwargs) + ) + + created_comments = [] + mock_issue = mock_repo.get_issue() + mock_issue.create_comment = mocker.MagicMock( + side_effect=lambda comment: make_mock_issue_comment( + comment=comment, created_comments=created_comments + ) + ) + mock_issue.get_comments = mocker.MagicMock(side_effect=lambda: created_comments) + + mock_pull_request = mock_repo.get_pull() + mock_pull_request.get_reviews = mocker.MagicMock(lambda: []) + mock_pull_request.merged = False + mock_pull_request.merge = mocker.MagicMock() + + controller = make_controller( + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + bot_config=GithubCICDBotConfig(merge_method=MergeMethod.REBASE, enable_deploy_command=True), + ) + controller._context._run_tests = mocker.MagicMock( + side_effect=lambda **kwargs: (TestResult(), "") + ) + + github_output_file = tmp_path / "github_output.txt" + + with mock.patch.dict(os.environ, {"GITHUB_OUTPUT": str(github_output_file)}): + command._run_all(controller) + + assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping + assert "SQLMesh - PR Environment Synced" in controller._check_run_mapping + assert "SQLMesh - Prod Environment Synced" in controller._check_run_mapping + assert "SQLMesh - Run Unit Tests" in controller._check_run_mapping + prod_checks_runs = controller._check_run_mapping["SQLMesh - Prod Environment Synced"].all_kwargs + assert len(prod_checks_runs) == 2 + assert GithubCheckStatus(prod_checks_runs[0]["status"]).is_queued + assert GithubCheckStatus(prod_checks_runs[1]["status"]).is_completed + assert prod_checks_runs[1]["output"]["title"] == "Skipped deployment" + assert ( + prod_checks_runs[1]["output"]["summary"] + == "Skipped Deploying to Production because a `/deploy` command has not been detected yet" + ) + assert GithubCheckConclusion(prod_checks_runs[1]["conclusion"]).is_skipped + + # required approvers are irrelevant because /deploy command is enabled + assert "SQLMesh - Has Required Approval" not in controller._check_run_mapping diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index 2d9a129a3f..ab5b9f2245 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -691,9 +691,11 @@ def test_merge_pr_has_non_breaking_change_no_categorization( assert GithubCheckStatus(prod_checks_runs[0]["status"]).is_queued assert GithubCheckStatus(prod_checks_runs[1]["status"]).is_completed assert GithubCheckConclusion(prod_checks_runs[1]["conclusion"]).is_skipped - skip_reason = "Skipped Deploying to Production because the PR environment was not updated" - assert prod_checks_runs[1]["output"]["title"] == skip_reason - assert prod_checks_runs[1]["output"]["summary"] == skip_reason + assert prod_checks_runs[1]["output"]["title"] == "Skipped deployment" + assert ( + prod_checks_runs[1]["output"]["summary"] + == "Skipped Deploying to Production because the PR environment was not updated" + ) assert "SQLMesh - Has Required Approval" in controller._check_run_mapping approval_checks_runs = controller._check_run_mapping[ @@ -1024,9 +1026,11 @@ def test_no_merge_since_no_deploy_signal( assert GithubCheckStatus(prod_checks_runs[0]["status"]).is_queued assert GithubCheckStatus(prod_checks_runs[1]["status"]).is_completed assert GithubCheckConclusion(prod_checks_runs[1]["conclusion"]).is_skipped - skip_reason = "Skipped Deploying to Production because a required approver has not approved" - assert prod_checks_runs[1]["output"]["title"] == skip_reason - assert prod_checks_runs[1]["output"]["summary"] == skip_reason + assert prod_checks_runs[1]["output"]["title"] == "Skipped deployment" + assert ( + prod_checks_runs[1]["output"]["summary"] + == "Skipped Deploying to Production because a required approver has not approved" + ) assert "SQLMesh - Has Required Approval" in controller._check_run_mapping approval_checks_runs = controller._check_run_mapping[ @@ -1528,9 +1532,11 @@ def test_error_msg_when_applying_plan_with_bug( assert GithubCheckStatus(prod_checks_runs[0]["status"]).is_queued assert GithubCheckStatus(prod_checks_runs[1]["status"]).is_completed assert GithubCheckConclusion(prod_checks_runs[1]["conclusion"]).is_skipped - skip_reason = "Skipped Deploying to Production because the PR environment was not updated" - assert prod_checks_runs[1]["output"]["title"] == skip_reason - assert prod_checks_runs[1]["output"]["summary"] == skip_reason + assert prod_checks_runs[1]["output"]["title"] == "Skipped deployment" + assert ( + prod_checks_runs[1]["output"]["summary"] + == "Skipped Deploying to Production because the PR environment was not updated" + ) assert "SQLMesh - Has Required Approval" in controller._check_run_mapping approval_checks_runs = controller._check_run_mapping[ From 82f2447d6380f6803c29d931ccdb1d381e0f0517 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 24 Jun 2025 07:51:40 +1200 Subject: [PATCH 0442/1056] Feat(cicd_bot): Show models with uncategorized changes instead of a generic error (#4783) --- sqlmesh/integrations/github/cicd/command.py | 2 +- .../integrations/github/cicd/controller.py | 19 ++++++++++++++++++- .../github/cicd/test_integration.py | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index bf2863145b..cd5104df50 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -116,7 +116,7 @@ def _update_pr_environment(controller: GithubController) -> bool: return conclusion is not None and conclusion.is_success except Exception as e: conclusion = controller.update_pr_environment_check( - status=GithubCheckStatus.COMPLETED, exception=e + status=GithubCheckStatus.COMPLETED, exception=e, plan=controller.pr_plan_or_none ) return ( conclusion is not None diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index f179834e2b..8282f3e8d8 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -408,6 +408,13 @@ def pr_plan(self) -> Plan: assert self._pr_plan_builder return self._pr_plan_builder.build() + @property + def pr_plan_or_none(self) -> t.Optional[Plan]: + try: + return self.pr_plan + except: + return None + @property def prod_plan(self) -> Plan: if not self._prod_plan_builder: @@ -823,6 +830,7 @@ def update_pr_environment_check( self, status: GithubCheckStatus, exception: t.Optional[Exception] = None, + plan: t.Optional[Plan] = None, ) -> t.Optional[GithubCheckConclusion]: """ Updates the status of the merge commit for the PR environment. @@ -926,6 +934,14 @@ def conclusion_handler( if captured_errors: logger.debug(f"Captured errors: {captured_errors}") failure_msg = f"**Errors:**\n{captured_errors}\n" + elif isinstance(exception, UncategorizedPlanError) and plan: + failure_msg = f"The following models could not be categorized automatically:\n" + for snapshot in plan.uncategorized: + failure_msg += f"- {snapshot.name}\n" + failure_msg += ( + f"\nRun `sqlmesh plan {self.pr_environment_name}` locally to apply these changes.\n\n" + "If you would like the bot to automatically categorize changes, check the [documentation](https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information." + ) elif isinstance(exception, PlanError): failure_msg = f"Plan application failed.\n\n{self._console.captured_output}" elif isinstance(exception, (SQLMeshError, SqlglotError, ValueError)): @@ -940,11 +956,12 @@ def conclusion_handler( + traceback.format_exc() ) failure_msg = f"This is an unexpected error.\n\n**Exception:**\n```\n{traceback.format_exc()}\n```" + conclusion_to_summary = { GithubCheckConclusion.SKIPPED: f":next_track_button: Skipped creating or updating PR Environment `{self.pr_environment_name}`. {skip_reason}", GithubCheckConclusion.FAILURE: f":x: Failed to create or update PR Environment `{self.pr_environment_name}`.\n\n{failure_msg}", GithubCheckConclusion.CANCELLED: f":stop_sign: Cancelled creating or updating PR Environment `{self.pr_environment_name}`", - GithubCheckConclusion.ACTION_REQUIRED: f":warning: Action Required to create or update PR Environment `{self.pr_environment_name}`. There are likely uncateogrized changes. Run `plan` locally to apply these changes. If you want the bot to automatically categorize changes, then check documentation (https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information.", + GithubCheckConclusion.ACTION_REQUIRED: f":warning: Action Required to create or update PR Environment `{self.pr_environment_name}` :warning:\n\n{failure_msg}", } summary = conclusion_to_summary.get( conclusion, f":interrobang: Got an unexpected conclusion: {conclusion.value}" diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index ab5b9f2245..cd70ad72fe 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -665,7 +665,7 @@ def test_merge_pr_has_non_breaking_change_no_categorization( assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" assert ( pr_checks_runs[2]["output"]["summary"] - == ":warning: Action Required to create or update PR Environment `hello_world_2`. There are likely uncateogrized changes. Run `plan` locally to apply these changes. If you want the bot to automatically categorize changes, then check documentation (https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information." + == """:warning: Action Required to create or update PR Environment `hello_world_2` :warning:\n\nThe following models could not be categorized automatically:\n- "memory"."sushi"."waiter_revenue_by_day"\n\nRun `sqlmesh plan hello_world_2` locally to apply these changes.\n\nIf you would like the bot to automatically categorize changes, check the [documentation](https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information.""" ) assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping From c6d730908b90a55cf1a4848d8e0519d1ec41d234 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:21:44 -0500 Subject: [PATCH 0443/1056] Docs: add interactive init to quickstart docs (#4736) Co-authored-by: Sung Won Chung --- docs/integrations/overview.md | 32 +- docs/quickstart/cli.md | 345 ++++++++++++++---- .../cli/cli-quickstart_duckdb-tables.png | Bin 0 -> 11515 bytes .../cli/cli-quickstart_duckdb-views.png | Bin 0 -> 7245 bytes docs/reference/cli.md | 10 +- 5 files changed, 306 insertions(+), 81 deletions(-) create mode 100644 docs/quickstart/cli/cli-quickstart_duckdb-tables.png create mode 100644 docs/quickstart/cli/cli-quickstart_duckdb-views.png diff --git a/docs/integrations/overview.md b/docs/integrations/overview.md index 9f829ceab7..5e850afbf6 100644 --- a/docs/integrations/overview.md +++ b/docs/integrations/overview.md @@ -9,20 +9,20 @@ SQLMesh supports integrations with the following tools: * [Kestra](https://kestra.io/plugins/plugin-sqlmesh/tasks/cli/io.kestra.plugin.sqlmesh.cli.sqlmeshcli) ## Execution engines -SQLMesh supports the following execution engines for running SQLMesh projects: +SQLMesh supports the following execution engines for running SQLMesh projects (engine `type` in parentheses - example usage: `pip install "sqlmesh[databricks]"`): -* [Athena](./engines/athena.md) -* [Azure SQL](./engines/azuresql.md) -* [BigQuery](./engines/bigquery.md) -* [ClickHouse](./engines/clickhouse.md) -* [Databricks](./engines/databricks.md) -* [DuckDB](./engines/duckdb.md) -* [MotherDuck](./engines/motherduck.md) -* [MSSQL](./engines/mssql.md) -* [MySQL](./engines/mysql.md) -* [Postgres](./engines/postgres.md) -* [GCP Postgres](./engines/gcp-postgres.md) -* [Redshift](./engines/redshift.md) -* [Snowflake](./engines/snowflake.md) -* [Spark](./engines/spark.md) -* [Trino](./engines/trino.md) +* [Athena](./engines/athena.md) (athena) +* [Azure SQL](./engines/azuresql.md) (azuresql) +* [BigQuery](./engines/bigquery.md) (bigquery) +* [ClickHouse](./engines/clickhouse.md) (clickhouse) +* [Databricks](./engines/databricks.md) (databricks) +* [DuckDB](./engines/duckdb.md) (duckdb) +* [MotherDuck](./engines/motherduck.md) (motherduck) +* [MSSQL](./engines/mssql.md) (mssql) +* [MySQL](./engines/mysql.md) (mysql) +* [Postgres](./engines/postgres.md) (postgres) +* [GCP Postgres](./engines/gcp-postgres.md) (gcppostgres) +* [Redshift](./engines/redshift.md) (redshift) +* [Snowflake](./engines/snowflake.md) (snowflake) +* [Spark](./engines/spark.md) (spark) +* [Trino](./engines/trino.md) (trino) diff --git a/docs/quickstart/cli.md b/docs/quickstart/cli.md index cf990eb704..c89da59b75 100644 --- a/docs/quickstart/cli.md +++ b/docs/quickstart/cli.md @@ -1,6 +1,8 @@ # CLI -In this quickstart, you'll use the SQLMesh command line interface (CLI) to get up and running with SQLMesh's scaffold generator. This example project will run locally on your computer using [DuckDB](https://duckdb.org/) as an embedded SQL engine. +In this quickstart, you'll use the SQLMesh command line interface (CLI) to get up and running with SQLMesh's scaffold generator. + +It will create an example project that runs locally on your computer using [DuckDB](https://duckdb.org/) as an embedded SQL engine. Before beginning, ensure that you meet all the [prerequisites](../prerequisites.md) for using SQLMesh. @@ -39,41 +41,180 @@ mkdir sqlmesh-example cd sqlmesh-example ``` -If using a python virtual environment, ensure it's activated first by running the `source .env/bin/activate` command from the folder used during [installation](../installation.md). +If using a Python virtual environment, ensure it's activated first by running the `source .env/bin/activate` command from the folder used during [installation](../installation.md). + +### 1.1 Initialize the project + +SQLMesh includes a scaffold generator to initialize a new SQLMesh project. -Create a SQLMesh scaffold with the following command, specifying a default SQL dialect for your models. The dialect should correspond to the dialect most of your models are written in; it can be overridden for specific models in the model's `MODEL` specification. All SQL dialects [supported by the SQLGlot library](https://github.com/tobymao/sqlglot/blob/main/sqlglot/dialects/dialect.py) are allowed. +The scaffold generator will ask you some questions and create a SQLMesh configuration file based on your responses. -In this example, we specify the `duckdb` dialect: +Depending on your answers, it will also create multiple files for the SQLmesh example project used in this quickstart. + +Start the scaffold generator by executing the `sqlmesh init` command: ```bash -sqlmesh init duckdb +sqlmesh init ``` -The scaffold will include a SQLMesh configuration file for the example project. +??? info "Skip the questions" + + If you don't want to use the interactive scaffold generator, you can initialize your project with arguments to the [`sqlmesh init` command](../reference/cli.md#init). + + The only required argument is `engine`, which specifies the SQL engine your project will use. Specify one of the engine `type`s in the [list of supported engines](../integrations/overview.md#execution-engines). + + In this example, we specify the `duckdb` engine: + + ```bash + sqlmesh init duckdb + ``` + + The scaffold will include a SQLMesh configuration file and example project directories and files. You're now ready to continue the quickstart [below](#2-create-a-prod-environment). + +#### Project type + +The first question asks about the type of project you want to create. Enter the number corresponding to the type of project you want to create and press `Enter`. + +``` bash +────────────────────────────── +Welcome to SQLMesh! +────────────────────────────── + +What type of project do you want to set up? + + [1] DEFAULT - Create SQLMesh example project models and files + [2] dbt - You have an existing dbt project and want to run it with SQLMesh + [3] EMPTY - Create a SQLMesh configuration file and project directories only + +Enter a number: 1 +``` + +For this quickstart, choose the `DEFAULT` option `1` so the example project files are included in the project directories. + +#### SQL engine + +The second question asks which SQL engine your project will use. SQLMesh will include that engine's connection settings in the configuration file, which you will fill in later to connect your project to the engine. + +For this quickstart, choose the `DuckDB` option `1` so we can run the example project with the built-in DuckDB engine that doesn't need additional configuration. -??? info "Learn more about the project's configuration" +``` bash +Choose your SQL engine: + + [1] DuckDB + [2] Snowflake + [3] Databricks + [4] BigQuery + [5] MotherDuck + [6] ClickHouse + [7] Redshift + [8] Spark + [9] Trino + [10] Azure SQL + [11] MSSQL + [12] Postgres + [13] GCP Postgres + [14] MySQL + [15] Athena + [16] RisingWave + +Enter a number: 1 +``` + +#### CLI mode + +SQLMesh's core commands have multiple options that alter their behavior. Some of those options streamline the SQLMesh `plan` workflow and CLI output. + +If you prefer a streamlined workflow (no prompts, no file diff previews, auto-apply changes), choose the `FLOW` CLI mode to automatically include those options in your project configuration file. + +If you prefer to see all the output SQLMesh provides, choose `DEFAULT` mode, which we will use in this quickstart: + +``` bash +Choose your SQLMesh CLI experience: + + [1] DEFAULT - See and control every detail + [2] FLOW - Automatically run changes and show summary output + +Enter a number: 1 +``` + +#### Ready to go + +Your project is now ready to go, and SQLMesh displays a message with some good next steps. + +If you chose the DuckDB engine, you're ready to move forward and run the example project with DuckDB. + +If you chose a different engine, add your engine's connection information to the `config.yaml` file before you run any additional SQLMesh commands. + +``` bash +Your SQLMesh project is ready! + +Next steps: +- Update your gateway connection settings (e.g., username/password) in the project configuration file: + /sqlmesh-example/config.yaml +- Run command in CLI: sqlmesh plan +- (Optional) Explain a plan: sqlmesh plan --explain + +Quickstart guide: +https://sqlmesh.readthedocs.io/en/stable/quickstart/cli/ + +Need help? +- Docs: https://sqlmesh.readthedocs.io +- Slack: https://www.tobikodata.com/slack +- GitHub: https://github.com/TobikoData/sqlmesh/issues +``` + +??? info "Learn more about the project's configuration: `config.yaml`" SQLMesh project-level configuration parameters are specified in the `config.yaml` file in the project directory. - This example project uses the embedded DuckDB SQL engine, so its configuration specifies `duckdb` as the local gateway's connection and the `local` gateway as the default. + This example project uses the embedded DuckDB SQL engine, so its configuration specifies `duckdb` as the gateway's connection type. All available configuration settings are included in the file, with optional settings set to their default value and commented out. - The command to run the scaffold generator **requires** a default SQL dialect for your models, which it places in the config `model_defaults` `dialect` key. In this example, we specified the `duckdb` SQL dialect as the default: + SQLMesh requires a default model SQL dialect. SQLMesh automatically specifies the SQL dialect for your project's SQL engine, which it places in the config `model_defaults` `dialect` key. In this example, we specified the DuckDB engine, so `duckdb` is the default SQL dialect: ```yaml linenums="1" + # --- Gateway Connection --- gateways: - local: + duckdb: connection: - type: duckdb - database: ./db.db - - default_gateway: local + # For more information on configuring the connection to your execution engine, visit: + # https://sqlmesh.readthedocs.io/en/stable/reference/configuration/#connection + # https://sqlmesh.readthedocs.io/en/stable/integrations/engines/duckdb/#connection-options + # + type: duckdb # <-- DuckDB engine + database: db.db + # concurrent_tasks: 1 + # register_comments: True # <-- Optional setting `register_comments` has a default value of True + # pre_ping: False + # pretty_sql: False + # catalogs: # <-- Optional setting `catalogs` has no default value + # extensions: + # connector_config: + # secrets: + # token: + + default_gateway: duckdb + + # --- Model Defaults --- + # https://sqlmesh.readthedocs.io/en/stable/reference/model_configuration/#model-defaults model_defaults: - dialect: duckdb + dialect: duckdb # <-- Models written in DuckDB SQL dialect by default + start: 2025-06-12 # Start date for backfill history + cron: '@daily' # Run models daily at 12am UTC (can override per model) + + # --- Linting Rules --- + # Enforce standards for your team + # https://sqlmesh.readthedocs.io/en/stable/guides/linter/ + + linter: + enabled: true + rules: + - ambiguousorinvalidcolumn + - invalidselectstarexpansion ``` Learn more about SQLMesh project configuration [here](../reference/configuration.md). -The scaffold will also include multiple directories where SQLMesh project files are stored and multiple files that constitute the example project (e.g., SQL models). +The scaffold generator creates multiple directories where SQLMesh project files are stored and multiple files that constitute the example project (e.g., SQL models). ??? info "Learn more about the project directories and files" SQLMesh uses a scaffold generator to initiate a new project. The generator will create multiple sub-directories and files for organizing your SQLMesh project code. @@ -106,7 +247,7 @@ The scaffold will also include multiple directories where SQLMesh project files - ./tests - test_full_model.yaml -Finally, the scaffold will include data for the example project to use. +Finally, the scaffold generator creates data for the example project to use. ??? info "Learn more about the project's data" The data used in this example project is contained in the `seed_data.csv` file in the `/seeds` project directory. The data reflects sales of 3 items over 7 days in January 2020. @@ -133,20 +274,98 @@ SQLMesh's key actions are creating and applying *plans* to *environments*. At th SQLMesh's key actions are creating and applying *plans* to *environments*. - A [SQLMesh environment](../concepts/environments.md) is an isolated namespace containing models and the data they generated. The most important environment is `prod` ("production"), which consists of the databases behind the applications your business uses to operate each day. Environments other than `prod` provide a place where you can test and preview changes to model code before they go live and affect business operations. + A [SQLMesh environment](../concepts/environments.md) is an isolated namespace containing models and the data they generated. + + The most important environment is `prod` ("production"), which consists of the databases behind the applications your business uses to operate each day. Environments other than `prod` provide a place where you can test and preview changes to model code before they go live and affect business operations. + + A [SQLMesh plan](../concepts/plans.md) contains a comparison of one environment to another and the set of changes needed to bring them into alignment. + + For example, if a new SQL model was added, tested, and run in the `dev` environment, it would need to be added and run in the `prod` environment to bring them into alignment. SQLMesh identifies all such changes and classifies them as either breaking or non-breaking. + + Breaking changes are those that invalidate data already existing in an environment. For example, if a `WHERE` clause was added to a model in the `dev` environment, existing data created by that model in the `prod` environment are now invalid because they may contain rows that would be filtered out by the new `WHERE` clause. + + Other changes, like adding a new column to a model in `dev`, are non-breaking because all the existing data in `prod` are still valid to use - only new data must be added to align the environments. + + After SQLMesh creates a plan, it summarizes the breaking and non-breaking changes so you can understand what will happen if you apply the plan. It will prompt you to "backfill" data to apply the plan. (In this context, backfill is a generic term for updating or adding to a table's data, including an initial load or full refresh.) + +??? info "Learn more about a plan's actions: `sqlmesh plan --explain`" + + Before applying a plan, you can view a detailed description of the actions it will take by passing the explain flag in your `sqlmesh plan` command: + + ```bash + sqlmesh plan --explain + ``` + + Passing the explain flag for the quickstart example project above adds the following information to the output: + + ```bash + Explained plan + ├── Validate SQL and create physical layer tables and views if they do not exist + │ ├── sqlmesh_example.seed_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__seed_model__2185867172 + │ │ ├── Dry run model query without inserting results + │ │ └── Create table if it doesn't exist + │ ├── sqlmesh_example.full_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__full_model__2278521865 + │ │ ├── Dry run model query without inserting results + │ │ └── Create table if it doesn't exist + │ └── sqlmesh_example.incremental_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__incremental_model__1880815781 + │ ├── Dry run model query without inserting results + │ └── Create table if it doesn't exist + ├── Backfill models by running their queries and run standalone audits + │ ├── sqlmesh_example.seed_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__seed_model__2185867172 + │ │ └── Fully refresh table + │ ├── sqlmesh_example.full_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__full_model__2278521865 + │ │ ├── Fully refresh table + │ │ └── Run 'assert_positive_order_ids' audit + │ └── sqlmesh_example.incremental_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__incremental_model__1880815781 + │ └── Fully refresh table + └── Update the virtual layer for environment 'prod' + └── Create or update views in the virtual layer to point at new physical tables and views + ├── sqlmesh_example.full_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__full_model__2278521865 + ├── sqlmesh_example.seed_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__seed_model__2185867172 + └── sqlmesh_example.incremental_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__incremental_model__1880815781 + ``` + + The explanation has three top-level sections, corresponding to the three types of actions a plan takes: + + - Validate SQL and create physical layer tables and views if they do not exist + - Backfill models by running their queries and run standalone audits + - Update the virtual layer for environment 'prod' + + Each section lists the affected models and provides more information about what will occur. For example, the first model in the first section is: + + ```bash + ├── sqlmesh_example.seed_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__seed_model__2185867172 + │ ├── Dry run model query without inserting results + │ └── Create table if it doesn't exist + ``` + + The first line shows the model name `sqlmesh_example.seed_model` and the physical layer table SQLMesh will create to store its data: `db.sqlmesh__sqlmesh_example.sqlmesh_example__seed_model__2185867172`. The second and third lines tell us that in this step SQLMesh will dry-run the model query and create the physical layer table if it doesn't exist. - A [SQLMesh plan](../concepts/plans.md) contains a comparison of one environment to another and the set of changes needed to bring them into alignment. For example, if a new SQL model was added, tested, and run in the `dev` environment, it would need to be added and run in the `prod` environment to bring them into alignment. SQLMesh identifies all such changes and classifies them as either breaking or non-breaking. + The second section describes what will occur during the backfill step. The second model in this section is: - Breaking changes are those that invalidate data already existing in an environment. For example, if a `WHERE` clause was added to a model in the `dev` environment, existing data created by that model in the `prod` environment are now invalid because they may contain rows that would be filtered out by the new `WHERE` clause. Other changes, like adding a new column to a model in `dev`, are non-breaking because all the existing data in `prod` are still valid to use - only new data must be added to align the environments. + ```bash + ├── sqlmesh_example.full_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__full_model__2278521865 + │ ├── Fully refresh table + │ └── Run 'assert_positive_order_ids' audit + ``` + + The first line shows the model name `sqlmesh_example.full_model` and the physical layer table SQLMesh will insert the model's data into: `db.sqlmesh__sqlmesh_example.sqlmesh_example__full_model__2278521865`. The second and third lines tell us that the backfill action will fully refresh the model's physical table and run the `assert_positive_order_ids` audit. + + The final section describes SQLMesh's action during the virtual layer update step. The first model in this section is: + + ```bash + └── Create or update views in the virtual layer to point at new physical tables and views + ├── sqlmesh_example.full_model -> db.sqlmesh__sqlmesh_example.sqlmesh_example__full_model__2278521865 + ``` - After SQLMesh creates a plan, it summarizes the breaking and non-breaking changes so you can understand what will happen if you apply the plan. It will prompt you to "backfill" data to apply the plan - in this context, backfill is a generic term for updating or adding to a table's data (including an initial load or full refresh). + The virtual layer step will update the `sqlmesh_example.full_model` virtual layer view to `SELECT * FROM` the physical table `db.sqlmesh__sqlmesh_example.sqlmesh_example__full_model__2278521865`. The first SQLMesh plan must execute every model to populate the production environment. Running `sqlmesh plan` will generate the plan and the following output: ```bash linenums="1" $ sqlmesh plan ====================================================================== -Successfully Ran 1 tests against duckdb +Successfully Ran 1 tests against duckdb in 0.1 seconds. ---------------------------------------------------------------------- `prod` environment will be initialized @@ -158,20 +377,18 @@ Models: └── sqlmesh_example.seed_model Models needing backfill: ├── sqlmesh_example.full_model: [full refresh] -├── sqlmesh_example.incremental_model: [2020-01-01 - 2025-04-17] +├── sqlmesh_example.incremental_model: [2020-01-01 - 2025-06-22] └── sqlmesh_example.seed_model: [full refresh] Apply - Backfill Tables [y/n]: ``` Line 3 of the output notes that `sqlmesh plan` successfully executed the project's test `tests/test_full_model.yaml` with duckdb. -Line 5 describes what environments the plan will affect when applied - a new `prod` environment in this case. - -Lines 7-11 of the output show that SQLMesh detected three new models relative to the current empty environment. +Line 6 describes what environments the plan will affect when applied - a new `prod` environment in this case. -Lines 12-16 list each model that will be executed by the plan, along with the date intervals or refresh types. For both `full_model` and `seed_model`, it shows `[full refresh]`, while for `incremental_model` it shows a specific date range `[2020-01-01 - 2025-04-17]`. The incremental model date range begins from 2020-01-01 because the `full` model kind always fully rebuilds its table. +Lines 8-12 of the output show that SQLMesh detected three new models relative to the current empty environment. -The `seed_model` date range begins on the same day the plan was made because `SEED` models have no temporality associated with them other than whether they have been modified since the previous SQLMesh plan. +Lines 13-16 list each model that will be executed by the plan, along with the date intervals or refresh types. For both `full_model` and `seed_model`, it shows `[full refresh]`, while for `incremental_model` it shows a specific date range `[2020-01-01 - 2025-06-22]`. The incremental model date range begins from 2020-01-01 because its definition specifies a model start date of `2020-01-01`. ??? info "Learn more about the project's models" @@ -248,26 +465,23 @@ The `seed_model` date range begins on the same day the plan was made because `SE GROUP BY item_id ``` -Line 16 asks you whether to proceed with executing the model backfills described in lines 11-14. Enter `y` and press `Enter`, and SQLMesh will execute the models and return this output: +Line 18 asks you whether to proceed with executing the model backfills described in lines 13-16. Enter `y` and press `Enter`, and SQLMesh will execute the models and return this output: ```bash linenums="1" Apply - Backfill Tables [y/n]: y -Updating physical layer ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 +Updating physical layer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 ✔ Physical layer updated -[1/1] sqlmesh_example.seed_model [insert seed file] -0.02s -[1/1] sqlmesh_example.incremental_model [insert 2020-01-01 - -2025-04-17] 0.03s -[1/1] sqlmesh_example.full_model [full refresh, audits ✔1] -0.05s -Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 +[1/1] sqlmesh_example.seed_model [insert seed file] 0.01s +[1/1] sqlmesh_example.incremental_model [insert 2020-01-01 - 2025-06-22] 0.01s +[1/1] sqlmesh_example.full_model [full refresh, audits ✔1] 0.01s +Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 ✔ Model batches executed -Updating virtual layer ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 +Updating virtual layer ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 ✔ Virtual layer updated ``` @@ -286,7 +500,17 @@ Lines 12-14 show the progress and completion of the second step - executing mode Lines 16-18 show the progress and completion of the final step - virtually updating the plan's target environment, which makes the data available for querying. -You've now created a new production environment with all of history backfilled. +Let's take a quick look at the project's DuckDB database file to see the objects SQLMesh created. First, we open the built-in DuckDB CLI tool with the `duckdb db.db` command, then run our two queries. + +Our first query shows the three physical tables SQLMesh created in the `sqlmesh__sqlmesh_example` schema (one table for each model): + +![Example project physical layer tables in the DuckDB CLI](./cli/cli-quickstart_duckdb-tables.png) + +Our second query shows that in the `sqlmesh` schema SQLMesh created three virtual layer views that read from the three physical tables: + +![Example project virtual layer views in the DuckDB CLI](./cli/cli-quickstart_duckdb-views.png) + +You've now created a new production environment with all of history backfilled! ## 3. Update a model @@ -327,7 +551,7 @@ Run `sqlmesh plan dev` to create a development environment called `dev`: $ sqlmesh plan dev ====================================================================== Successfully Ran 1 tests against duckdb ----------------------------------------------------------------------- +---------------------------------------------------------------------- New environment `dev` will be created from `prod` @@ -341,25 +565,25 @@ Models: └── sqlmesh_example__dev.full_model --- - -+++ - + ++++ + @@ -14,6 +14,7 @@ - + SELECT - id, - item_id, + id, + item_id, + 'z' AS new_column, - event_date + event_date FROM sqlmesh_example.seed_model WHERE -Directly Modified: sqlmesh_example__dev.incremental_model +Directly Modified: sqlmesh_example__dev.incremental_model (Non-breaking) └── Indirectly Modified Children: - └── sqlmesh_example__dev.full_model (Indirect Non-breaking) + └── sqlmesh_example__dev.full_model (Indirect Non-breaking) Models needing backfill: -└── sqlmesh_example__dev.incremental_model: [2020-01-01 - 2025-04-17] +└── sqlmesh_example__dev.incremental_model: [2020-01-01 - 2025-04-17] Apply - Backfill Tables [y/n]: ``` @@ -378,8 +602,7 @@ Updating physical layer ━━━━━━━━━━━━━━━━━━ ✔ Physical layer updated -[1/1] sqlmesh_example__dev.incremental_model [insert 2020-01-01 - -2025-04-17] 0.03s +[1/1] sqlmesh_example__dev.incremental_model [insert 2020-01-01 - 2025-04-17] 0.03s Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1/1 • 0:00:00 ✔ Model batches executed @@ -447,7 +670,7 @@ Enter `y` and press `Enter` at the `Apply - Virtual Update [y/n]:` prompt to app $ sqlmesh plan ====================================================================== Successfully Ran 1 tests against duckdb ----------------------------------------------------------------------- +---------------------------------------------------------------------- Differences from the `prod` environment: @@ -458,20 +681,20 @@ Models: └── sqlmesh_example.full_model --- - -+++ - + ++++ + @@ -14,6 +14,7 @@ - + SELECT - id, - item_id, + id, + item_id, + 'z' AS new_column, - event_date + event_date FROM sqlmesh_example.seed_model WHERE -Directly Modified: sqlmesh_example.incremental_model (Non-breaking) +Directly Modified: sqlmesh_example.incremental_model (Non-breaking) └── Indirectly Modified Children: └── sqlmesh_example.full_model (Indirect Non-breaking) Apply - Virtual Update [y/n]: y diff --git a/docs/quickstart/cli/cli-quickstart_duckdb-tables.png b/docs/quickstart/cli/cli-quickstart_duckdb-tables.png new file mode 100644 index 0000000000000000000000000000000000000000..27d7180f8d920209fdd76c1e6d7a57d25aaba424 GIT binary patch literal 11515 zcmZX(1yo$k5-p4bAKWrna19U!2p&RkCpaYd;O-FIo#3v)Em(rP5AN>n5Zw8PeBXWd zzW4W<)o13cuCA)RcU7M?CqzL`937Pi6$S%UkzE|z{18r{ zi7!4x7AR0m_PQ0{c;BNl5rp*#nL>m}M6CBrq)LnE(ky9rp1wcnNc!NhR_h$x`-aE3 zolqm49+1ICH(fNtcg*6C^(puwI2_>%&Wf{H%pzmUJ5?SA0>eSqRGcCE13JFEnR1<4 z)}s8={i%|1Hry2rXF;e7e-W|ETp${kRe1)tz!0S{r7-kf6pg`g1M_b9-O^b{Bj@3i z&2|GfYS!~b67H*5)vlsiME2<(uieSn{^r0gqcZtXi~0SV+ExyfPynx-4(zYwDda>!4#|w7hy9e%cCIx zW#n<6+Kyfky%7A}m3C{&w*E|GPBcQ`EaNx8RWN`^_s8pI`l|v1C;D>n_8J4J-g^O_Vpkn=%QwBd~tDY6^8quJ8Vn_GA{$PwvnKQHcIj-4#E}MaJbs$off$Ml_6a`Mi z&sFU-0T*cD3rN|+2ErS6wcHk=3^iZ`2Ze|9+1xXP>nzyLi3yy4MvQR7R~QC@7z=S! zEtd2wXKz+5isN@mwOcnvqmLg0^G5~np;RD;guf4nrw<2~Ob0o4Mps+7AFh<8Zp81s zf?c2#-s!SQElk3*r_9lH0AzbBA#8nHBAyEjwtqOBfJZb#FeaRl!mY*; z8+(R;SIgsSw6)i9WQmai&8mgZH}Z9a_&>vQ(>G6@K}~6u`j%!(0Z_-$>w7UGiD{=+ zf~wT&XB4}+bBwJYQmL9qK0ISa!>WTy*+MDC%04TA+X@5NB7Z;~X`Sv@+!aFv{bfui z7Xx--o|aD+o32;0r%rgjrKM!`CAGNARxK$By(qhHm(P_JHOGB%GC18fqfp~^rN+~s zb4HL;)>USZIK!Ui!@l!Cna1sq{8G#DEa&4tAxU+~zvvSR%FM~W{L1SWj;WY|FZC1E zw@T8Bnf^^t0(hc12T5PSimVBlFpY!eh2tNE-V1%bZiQz?)s22wTG$d+}I;Ztgb1$mL)i{1h zk=uZ0tg(&u#sG#E78bPRB%EXtZXFx1^)qLip2>MT86~Z2u4hqD4Ypa0M7gBlo4L#8 z`3sWlPK2nIgG+>TE<_|~Sa-@7MA87{7cMuOmD)B-S&%N?j@<46*8#CUZfJfIU|pQm zpgK2$=RdJ;H(SM9={E!e{cZNJ#zFp}wz158Di>^xm&EH0UA&h+!qNRe2v-airUf$W zXUU&+pMPvuq+XHo+ue~|hh-oaiGX!IgF&~A*Kf$VT|yY)Isy^DWHh+ko?Q~-fv}-) z;s6idR)}4_emA(BHArc;wL4DnO#kW}Yl3)1E@=KQznmQ@nz)wl_^1ss%XVD;4@}BsS2CXn24BZgsn+# znFIQIO))Beta+C9Bi-xh4ZOgi6ypUfYk6=*)Opy-ASNVIQe0b~xz6Q?m+I}@ulW9$ zG2FN8e4N#)W%d{Z!H5^I3=&^|I+bFaPg~_;P2w*1|3OQ9QeMU+#v}@`Ogx3)Qct?| z#6xB~2fzMBadoJTQg*Fl${a~9O{%2tw3G7a;q{O2bH7hk*%Mi+?J~K&unpi9z57?} zqf4_-&8I#t(CmE`2!_-CF&%c*g@J^MK-FV}NfwerFP9PwZ*6D;_u6I9eLd5?u&)jE zN7QC=g(> zxKfSLr1n=;#(t$CW}+4~NbRgRXXqW#IaSqUtxf(UkRKfdaT7&sq*MOkMsLgk6`(`I z=0kkRYj*No#kM`b1Hqh3c3Z#MB}=qv6>}w0cv7`DJW()=ld(&Qfrie$$4#$Dxx(rL z@&b*;7j9vMAlXCjk^!5ZJX60$6g3BNIT9=5t}h|*43T)qQqxkzUWWo6 zjM*Y~-DqD5Y~z{9!DdgZ-ja^<7;^`)#5qJJ))F%Ejlg0@!PJd}%BiM1?zFC<d3?ix^D|ITzDYx?Pzh>H=@{J(44pY1A?3Z)i*6iuv+5uvTT|X& zQ!@UfX$S5M;KwxX_+bc^tu_JfH8fz$)b$U1Pdwd`tZ<4v(X5x#LHt`5BtXY{;b#sO zZs|iL7SPvwhfsig$CSJ(7>lSYoW$`F^tWFj_cYr`3P>RKj7!6u*rFvU#By11uxcdV z*9g%UoIP>1a>6U;}sVb;>reXXNM1ofXWWbw>iS2`zJ&$Tx!gKFd0 zv;a!EFg~(HJ1BJ~1y4vgx2|w44DS6_jnsFE?R>W~;x7$0czI~w@KkAb8vj1Tx_)(m zK-GnLGa=XDXjtIp6I`nUGf#0u+jwe=q+9-G^S9tW^;S1)Gd|Yk)r>w2ZGs3Q`P*uz zR)rpMiuLB8U;r5_W42_uZ_1a~g(Ts)3AG66La<7x*3qHUB7x67BAyxAnc=%Sh|inQ zj+UCld3)n9Dx6YcnFAFdnT^XM_DTlgc8L|ewk?d|{7Rfu6tVt` zt7Of4H%MrU#rak6H5nzS0#=uFjRqbggROU2v}*JG!+ zlG5+Z#d~zFr%f8BmKJCnNrAJDem~YG}sUCd% z-Mc6#si5F;xsDVhYrIIHCxx_;?TI*POL!W8+9*McpnFvc!n%F8WH)3rf|`gCy(W3R zp9wagGB9lKb9$$2v~`(qea>5CV9Hmu&4j^#Z!bUc09xhhcr9mqYZ7 zwm#%k*z@$OqMZK&%VWMXp62}OLoF3uO_1+L zfET&g%{jl@Ug$k-(%Y~n!&CX?ds8u}-25p$?Lya@UquCbI@*tSz19`{JrVpEClCC} zZGPu64NyOGRV^s@VCO3Hn|CRH6J-1kZ6E|^oXuDtS#(M-tWWUOuv^#c=zkj(eEEp{ zp~+Uc3cPLp+GHhkK3$`RE8^+qYNycya(v1Ec+sB4RfzFFMR_Rkl_N^w1WX|JJZu^*`EYf*Q zMf*yUs+W`99o>?khJ>~5u?gvcgr3nvDm$eo_8mGMz`(mt%dl&F<1)kmpioaodIS@} zVZWtSNDbzPRzX?y*ixQ-mJhOsL##$jdW*&2ZPjaSq;;QW$2br-<8zdu8|H8DK1@wH z6KM-Dv*FE2-Re3+Jcja4E3TaU;{9-ejA{R`V;7}{q#JJLnYllX_=(L%Oh*qJ;L{Gu z5DX=GtyjA`Y8z8b)h4Sn(i`@Tl-il^m});f_8M}T%29NNop_(7o8+ShhLihMM->ux zsCd7xLyG18!c+5VDH;fVE4cfPOrR{<=a6z(hCsr7>24m!Tt}hQIlftmJf~4>u=(c) z??}22^HqGx%eGNt@WACNrxm$YgAQY?QhRdvpfb!JYz2YL??nbTW5}cZox`B9GdC74 z&{Jxx^#JktMCtW)7g;Pd}>KU5M)LGvb>J=A)9-AIci?j%R2xpG_ zq<~$H*Y(og%#EF?@xTYA8A}SC9w8NK{e=D*M^UNVUy~oWi}A?FSZ2vH4>;=hm~?&o zC6=u2qrb4_1hW}X=l#-U?D3a1C$cO3f;2J zq8u-9=FooFK&W*IvCS{w5_Zurvb8w6CQO4|L85$+`Hh7>-Ss8xvL=#vbWNDrqrvi+ z&gTh?t*~^TD&->r8?LbW+Uy~#BH7I(eHBHHYflC^F|)8dHbWkqErHs)!d)OCM3epg z(z8i)8Mg(}8FFE}AFL81D|_CAO4GgFdxjuM>knI3I+oy1KHAZGf+1y6Iit><1V1<7 z=#L91*$uJOSlR-IK1E`i0!S?>Rt-s%g^DR}8-CJzyjGC~aTgPi%_)G*F-dD>RDX=a zNk%s;UzPVv1+uWbvzw(iC8G$GB|NcDn@XzWp+%Z-+!Y(7$c~^Rd4N$ps}zIZiNC2g zsqIv-_>R^*qseh!aM=bN6O9n1ikO^szFS1;gfQGN3?Q@DU+^>-motx!}hVwXVEM zm1o?OJ4|_z0Jh)zH;lth1Mar`cYlfkv)8emOeQa4ICKxxI1i_9l0{vvEW@ox!&y#3 z2A}jWg5;U?+FUNSPJNCTK?6a8tGhM996YAGvbfrbt|s=ccq#tuO61sVELMK_q&qB0 zGCz*>RGkgK0D;J#6$~?wSlOUPVV7!QA|i*A)phZ_0)3qDHo=LpF?g5!$M{zv3I}J< z2{{2}buAwXXx_Fef6;Z!aG3>4h0g;zO%g*eQ*82Y-#-TY!;7xo=`Ql!LMmswp_>O) zlnja(3p7$L`=XaF$@SP@HM6`WmMrgUW6i<~pCX;xMA!*@3O>|=1tsmLkp_NUOIZx& z0k`&2*OwA6BsPv+S5TiQgrc&=Nsrd&HNx)S&?`BK*ZzgMrBm}d5p@dI0yQ|I(YG5O zg&g7e7II*q8|2v4lpgVb8HADhy(L*8e*cq`t#@?+3%@M|D;GIeZ2QDT7aS=XUPN*2 zTN|EF68e3ccC&JVe8+F?XXDNq;*bA~6@IAH-?n-B@v0-zN{bj0df1(QK=ozlTov2z zdKzWrr_3&)F$o!36ZR8KQG6gKd?LKH4k*r$9BcGt}+Us5!#f;>9BpjW^n?hyegBMS%y2s|sV8gXM#gm;x=74B1yOw;@W4Wc9EpY#W!|0-tkYz_Y(={DH23@(^p= z`04?U$7RD3-4dY37tvlV!~e{r)>U_5JSgoFfKW?!W+-~KzBz}WgDrM(gxjK(jMe8Y zj>ILNal60V=Om`PF;%1|(6!uJ;Io&g49Z1CfehNNFOwzTT~>K9mjB)lVCZP`g! z_J;*Yq7+#2Kvsq*WSfjSV(SliAA4Yp$s*rDDl+SGyq>SU)b9-a>iQvXH?s0E`Dt|$ z@g<|f5eGG7D&conP@b4XY?L(`bw1tFJD2){VZJaZ_ReR(f-5lBy{c&6=WLn zH&Ek^((71Y-gD2~gY`kpEDhrEx9Zeg+WD6uwnYOwdU*b2EG;XX1d6(f8Ra=f=fB<`-{nO5o z`R&ym81ZjWdc?u%4*ufrFp-sSz6$I=w9B8n`E3{`SMfD0bo(SVY^au=cny0$BPJ6> zv3_*=O33Le-8n2T5k)Z-aHtP~qE?T~qy=C{d4I_B$s{>VxT%7V($>iMpc3&t#xY6w zha-FZU)5f5ouRiSYw!{Vw3Rjul$Hf6AJ!a`)viTbd3FZf^iU!b=Lt~wXlpSWs-wjwe$@`4+aGAs>l&E)j|f_6t8TW ze#kBA*xCQQBNUit3)2S@b#`bBgO42`(@RaYudD6E0Q;kGw6VR#v53nOb1xs09U>Pl zMw{!7fp#=Ix~qG(&rX51YcaDR4Ww98qtb4<4eE*rcmU51e1%tygV9N*!K+z8K0teo zZ+sh7sZnF-4UetU*uq|R?0|LVX_bsHqPu}UfFA)TntK5$oZQ>jGEsu3Ol&od?K~Qd zrz-hKlUian=d1MtSTxvKNL%xC&LlcB<9xvOaY6hEWU>2?lnj^Z=)fH{{C6(7n&0Ny`nHRq0&?h z;jsDJ+H1b)s0{AGo6F#a!rdht?L;8$d5S-T99gkF&ERO6YzqVQZPMW~>dwuP2}PCv zS?`3!fw=d_yGV0wz2xL%(L#;N^U_QO<(1`x` zO0ENg1qa>r7S0uV5LQ4)N9(!ZhtZT4|iq^E9#jcD_mL z!~WpdtCvllC8=?!wwbqQWLk8G?}mu4-$dH=0n)20K5zKSE(~ioX6yW^s1JJp!oI9F zslD~ZP5im&_Hglx4{Vpl1NJ>YefAIvAknkn{_ccGtNC5YB1OijDVyA*g@t*&ez&Oq z=OX^VaBIsfvsN!m_R6%@^Lu9Ns5E|jymZa6!a(V7;&ZgH$ukZ17p#!FI?_|0zd5xWWeup?UU@bPZ-NiSoV(L61nb_TYb|JL?`((6O2Nj$3A0$ zm9A;Yu{PA4OWNUNo?E!p;HwS1t|S(Xl#NN6PEFdM@UZQ%jfk7@V@T(Bxm8 zNJH{QCxhi;;G}Dhz8a{A9$h?#rxX&PH8Rzn6p3!6$k?N&@T@G5d5In$LUWd-)QMWB z>|AM{0(o41OC_((`ZzucwdXPww!}$}femUWP64>PR{3G=!?Q#61@m%b;+Y-V;WUf#RXS-#Vqn3U ze2#`)$|<@EOvuoTW?kKV;Q>&15|}@~oged`-%+gpG?WZ2NMhW-Yex5oZewsrNXcJ) zjE2>ZbB7g2`YY506nivO-!~&Vt@S4ta~NwLB?Nf*MTwl3uZt3`1l%nmK7VtHXp}ia zSmWk>X-`z7>eez>%$Zj$ha^m;)>9%!Sz-qzMZ)13S_W^~5zi3*aWR(AO02x2a+Tgs z;M#NaMFqEzJ+5Jn>iUq#Jb4qX!`9)YnTg$vCPseQp_LN8X91%ed{K!|QY|e@6|fum zS(zZ&LQe8ejJZ>_vWaP{&5(sdMxN#Ji%G{ zCe`8jHJNL)n^VN^{|S4kg;e5Xw=0iHNLqcRIFz5b9JkDPrfz^*2P^U)Hpag;wWtU~ z&03*m{{r$h*{BJMrBZPUO@nZ|jUsQi}{@=^~?c9Y$MMaV9@BWV6YT-6QkX8UxJWOcO@VpKWm!F1-0xi$#mvl z)uViFUbU^*j@NNZ)95CrYP=dr`MuzJ(Z!nXR_U)(XB*4O*W>SbJgFoy^(t05*5&+| z6f3A_65m|6_@PZr)dKoLw|*!JrP%G^kSA+x{-|-i3vAaJrdIBGG9To$GlqNnMmg@I z*pEd7pbUHrwLYv@kX$Bi>}>m>kNtWFO_b+AdyemCgR?4?HZSfh$8Bo%>Z1y_e)wa4 z7i2+JL9gS6h(OAD@nP-NFqwfprWqoqX~PY8(DN~mcT#|5sKaZ1r!~;Ln_L>D{Cc~s z<4syXV0xX?{&>8vuWu7S*a}4HHcI7ZW;UCfalJbimgtThtAOrE%BK;4?j-L3AL1DM z)LG>X=o~`yKs50Fg_+DbV!1dfxafX&rs4xw7&^o%=6rkD&p`PufcFUt8N#{DSkj`=5@5?lv@XU*-vTpiuGd4@$B& zcRmSYSPvtNkUi9Gm^bs58SF0=Z>l zU$#_0MD)IB$5J2P`Fl?PN3k~9Q}hlu`HXC(h+lJ5S!XBTh?C)ffvIysf&L|vV0J?5 zWo;5G-{hOZalFK?v`RCrYS_-85iMl$EMLO`2lCFSIk$1Z{3^2QGW#Rv)7F%l(K4s_+XYlNLZWx;SwMR~Y~12E zJ-y&hr37Co9i6yhIg4$EEWc8QA{@nu5#ojzzIDq;t#R@*(}hZ}TrvvVrg|Vr@>>cE zr%vk*4BZ>OhIDctz=Z4xZ{e8 zFfD|`6w9t~{QOTrq(B@)!&JZ~M14Q_ez^QkSfZO1(L8$bxL)kH4}5*(u>@UfbwAuO z&A5{D`~VF<>TKrir>&G(E2%rzyn{amu0ctlHB(MQSbLY>ezA{r48S7ha(^h3wl?-t z6YkJllMv2_2L{S44^JJiM@j@#acIJ=i|Hja7-g7~qWi%ply+5yoW|-!NH8D7R=F zZ7*4{lb^km#rOs)26k^iq)3*pGb;}w(@N*OtWgoUur#e!I{{9)`45SpPxPiF+>QjM zH#AXtji%{W5~wVn&_n zv^p)bb!^BhdNR}b7-#X!IuRT!#Z5J(`gzYA?!O{e$e*TjPWfKKcy*>zi=7@1-+j2r z2rI{RUW|~ImCE)VP<-~n27I4xCi3TDxfe2$$hsGEgZ7MpCycNbNWmKzp7SrXDZ;ir z?%BRdw&E|HuN1d*e(=?gKZFI=38xJAWmj+a;HPvbD(usn+L7DJ6 zOot63mBb5po_q>HKzj>QT>(QtjN^kv)oMfd7my- z%f&>WdF$^Sh=QWJtTt*1-n0cye0&s-x~gHIqX*KB$P%s|4Gjw0<`lvf33KdE8oXHr=D8N#3Xw&;-4e;fzw0GWFzj zxp>GJ7IgEq!$Qalyj&y&@QGCG8<@rT30glLVeIk@=Yw8D;MwNzk$>fU-~>uR!%=&a#>UV!CUiI=RL6gI^(JZ zE%}R7Jy|SdHEz3LBlnC!bRBaPW!m2UWsm*{w%@LnG)UzuUWHoQ5=6+5pZ0Lfbmb6* zR`$PCL$SRbn|pRsz62jPC)BD+ZqO^!+G$7K?r{SJ2+LUDqzqV&)JMwND-yFb>e)gr z1wuAxWFvEZqF$1*J4*r31gH=czNBRT`{q0@gV*)v&XuluPZfX|b_g{(i(<}kxqwz! z@-Nsbs0Z^nfW}si|MEJMsH-j;I1Z-Yx(-w9XN9E+nA&_%Q!`wKgY2d$??``oZ(yVy z*y%c@wV6#`JGSx{6u{U-D0P=+n-z{x1sWye2vWBLPR2)i{X&LD4B}1)Z@Fht;z6XXG)P?)?4=P5=sA)tks_=L;#_r|6 z;}zFGJCOJvmFx_A*imgr&WLBM*Oblqn+*%mym(@kq~**`9p5Dhfs|AJw}4bL_^u%l z!E{S#uJey<8#^o;S#Z^HRE-doYxJ;*hTCVJ30YT%$gWMonEyhr22kZmPWUHAJ_8A#yl6>fsXYIQC?s$EF@~gAoiY{cJFB$D8rRE#)cC+1Aa>fRntEe&+mL3 zON=ewze|jI;m23+K>y4ibzrdD`u=x@@qiV9p@R3geH{!V){}d%on}8=FZnnL oPqje`;~fnP^BVZ~QUS}d!EUxKOGX0yKMzJyR8HjQCtctF4>LaqO#lD@ literal 0 HcmV?d00001 diff --git a/docs/quickstart/cli/cli-quickstart_duckdb-views.png b/docs/quickstart/cli/cli-quickstart_duckdb-views.png new file mode 100644 index 0000000000000000000000000000000000000000..5d6af7fc870eca18e81c9e3105ad16b2065e2b7b GIT binary patch literal 7245 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 MRkR Date: Tue, 24 Jun 2025 00:00:38 +0200 Subject: [PATCH 0444/1056] feat(duckdb): add file systems configuration option (#4778) --- docs/integrations/engines/duckdb.md | 35 ++++++++++++++++++++++- sqlmesh/core/config/connection.py | 11 +++++++ sqlmesh/dbt/target.py | 4 +++ tests/core/test_config.py | 2 ++ tests/core/test_connection_config.py | 11 +++++++ tests/integrations/jupyter/test_magics.py | 8 +++--- 6 files changed, 66 insertions(+), 5 deletions(-) diff --git a/docs/integrations/engines/duckdb.md b/docs/integrations/engines/duckdb.md index 7aef7def25..19d8d6e1f2 100644 --- a/docs/integrations/engines/duckdb.md +++ b/docs/integrations/engines/duckdb.md @@ -18,6 +18,7 @@ | `extensions` | Extension to load into duckdb. Only autoloadable extensions are supported. | list | N | | `connector_config` | Configuration to pass into the duckdb connector. | dict | N | | `secrets` | Configuration for authenticating external sources (e.g., S3) using DuckDB secrets. | dict | N | +| `filesystems` | Configuration for registering `fsspec` filesystems to the DuckDB connection. | dict | N | #### DuckDB Catalogs Example @@ -256,4 +257,36 @@ After configuring the secrets, you can directly reference S3 paths in your catal Refer to the official DuckDB documentation for the full list of [supported S3 secret parameters](https://duckdb.org/docs/stable/extensions/httpfs/s3api.html#overview-of-s3-secret-parameters) and for more information on the [Secrets Manager configuration](https://duckdb.org/docs/configuration/secrets_manager.html). -> Note: Loading credentials at runtime using `load_aws_credentials()` or similar deprecated functions may fail when using SQLMesh. \ No newline at end of file +> Note: Loading credentials at runtime using `load_aws_credentials()` or similar deprecated functions may fail when using SQLMesh. + +##### File system configuration example for Microsoft Onelake + +The `filesystems` accepts a list of file systems to register in the DuckDB connection. This is especially useful for Azure Storage Accounts, as it adds write support for DuckDB which is not natively supported by DuckDB (yet). + + +=== "YAML" + + ```yaml linenums="1" + gateways: + ducklake: + connection: + type: duckdb + catalogs: + ducklake: + type: ducklake + path: myducklakecatalog.duckdb + data_path: abfs://MyFabricWorkspace/MyFabricLakehouse.Lakehouse/Files/DuckLake.Files + extensions: + - ducklake + filesystems: + - fs: abfs + account_name: onelake + account_host: onelake.blob.fabric.microsoft.com + client_id: {{ env_var('AZURE_CLIENT_ID') }} + client_secret: {{ env_var('AZURE_CLIENT_SECRET') }} + tenant_id: {{ env_var('AZURE_TENANT_ID') }} + # anon: False # To use azure.identity.DefaultAzureCredential authentication + ``` + + +Refer to the documentation for `fsspec` [fsspec.filesystem](https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.filesystem) and `adlfs` [adlfs.AzureBlobFileSystem](https://fsspec.github.io/adlfs/api/#api-reference) for a full list of storage options. diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index a669471ddf..2c897cd8a5 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -266,6 +266,7 @@ class BaseDuckDBConnectionConfig(ConnectionConfig): extensions: A list of autoloadable extensions to load. connector_config: A dictionary of configuration to pass into the duckdb connector. secrets: A list of dictionaries used to generate DuckDB secrets for authenticating with external services (e.g. S3). + filesystems: A list of dictionaries used to register `fsspec` filesystems to the DuckDB cursor. concurrent_tasks: The maximum number of tasks that can use this connection concurrently. register_comments: Whether or not to register model comments with the SQL engine. pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive. @@ -277,6 +278,7 @@ class BaseDuckDBConnectionConfig(ConnectionConfig): extensions: t.List[t.Union[str, t.Dict[str, t.Any]]] = [] connector_config: t.Dict[str, t.Any] = {} secrets: t.List[t.Dict[str, t.Any]] = [] + filesystems: t.List[t.Dict[str, t.Any]] = [] concurrent_tasks: int = 1 register_comments: bool = True @@ -371,6 +373,15 @@ def init(cursor: duckdb.DuckDBPyConnection) -> None: except Exception as e: raise ConfigError(f"Failed to create secret: {e}") + if self.filesystems: + from fsspec import filesystem # type: ignore + + for file_system in self.filesystems: + options = file_system.copy() + fs = options.pop("fs") + fs = filesystem(fs, **options) + cursor.register_filesystem(fs) + for i, (alias, path_options) in enumerate( (getattr(self, "catalogs", None) or {}).items() ): diff --git a/sqlmesh/dbt/target.py b/sqlmesh/dbt/target.py index 05985d8762..94f4c98894 100644 --- a/sqlmesh/dbt/target.py +++ b/sqlmesh/dbt/target.py @@ -138,6 +138,7 @@ class DuckDbConfig(TargetConfig): extensions: A list of autoloadable extensions to load. settings: A dictionary of settings to pass into the duckdb connector. secrets: A list of secrets to pass to the secret manager in the duckdb connector. + filesystems: A list of `fsspec` filesystems to register in the duckdb connection. """ type: t.Literal["duckdb"] = "duckdb" @@ -147,6 +148,7 @@ class DuckDbConfig(TargetConfig): extensions: t.Optional[t.List[str]] = None settings: t.Optional[t.Dict[str, t.Any]] = None secrets: t.Optional[t.List[t.Dict[str, t.Any]]] = None + filesystems: t.Optional[t.List[t.Dict[str, t.Any]]] = None @model_validator(mode="before") def validate_authentication(cls, data: t.Any) -> t.Any: @@ -182,6 +184,8 @@ def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: kwargs["connector_config"] = self.settings if self.secrets is not None: kwargs["secrets"] = self.secrets + if self.filesystems is not None: + kwargs["filesystems"] = self.filesystems return DuckDBConnectionConfig( database=self.path, concurrent_tasks=1, diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 47e063c559..b3457345a8 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -562,6 +562,7 @@ def test_connection_config_serialization(): "pretty_sql": False, "connector_config": {}, "secrets": [], + "filesystems": [], "database": "my_db", } assert serialized["default_test_connection"] == { @@ -573,6 +574,7 @@ def test_connection_config_serialization(): "pretty_sql": False, "connector_config": {}, "secrets": [], + "filesystems": [], "database": "my_test_db", } diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 90aba1c3bb..0d7df3d724 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -437,9 +437,20 @@ def test_duckdb(make_config): "secret": "aws_secret", } ], + filesystems=[ + { + "protocol": "abfs", + "storage_options": { + "account_name": "onelake", + "account_host": "onelake.blob.fabric.microsoft.com", + "anon": False, + }, + } + ], ) assert config.connector_config assert config.secrets + assert config.filesystems assert isinstance(config, DuckDBConnectionConfig) assert not config.is_recommended_for_state_sync diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index f606e574cc..6507f07120 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -745,16 +745,16 @@ def test_info(notebook, sushi_context, convert_all_html_output_to_text, get_all_ "Models: 20", "Macros: 8", "", - "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", - "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", + "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: []", + "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: []", "Data warehouse connection succeeded", ] assert get_all_html_output(output) == [ "
Models: 20
", "
Macros: 8
", "
",
-        '
Connection:  type: duckdb  concurrent_tasks: 1  register_comments: true  pre_ping: false  pretty_sql: false  extensions: []  connector_config: {}  secrets: None
', - '
Test Connection:  type: duckdb  concurrent_tasks: 1  register_comments: true  pre_ping: false  pretty_sql: false  extensions: []  connector_config: {}  secrets: None
', + '
Connection:  type: duckdb  concurrent_tasks: 1  register_comments: true  pre_ping: false  pretty_sql: false  extensions: []  connector_config: {}  secrets: None  filesystems: []
', + '
Test Connection:  type: duckdb  concurrent_tasks: 1  register_comments: true  pre_ping: false  pretty_sql: false  extensions: []  connector_config: {}  secrets: None  filesystems: []
', "
Data warehouse connection succeeded
", ] From 1e1ace19d0723df9f661e961730f9c8ce9634696 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 23 Jun 2025 15:15:02 -0700 Subject: [PATCH 0445/1056] Fix: Create a transaction and a session when migrating a snapshot (#4794) --- sqlmesh/core/snapshot/evaluator.py | 65 ++++++++++++++------------- tests/core/test_snapshot_evaluator.py | 19 +++++--- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index f2042583d0..d33748630d 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -911,39 +911,40 @@ def _migrate_snapshot( ): return + deployability_index = DeployabilityIndex.all_deployable() + render_kwargs: t.Dict[str, t.Any] = dict( + engine_adapter=adapter, + snapshots=parent_snapshots_by_name(snapshot, snapshots), + runtime_stage=RuntimeStage.CREATING, + deployability_index=deployability_index, + ) target_table_name = snapshot.table_name() - if adapter.table_exists(target_table_name): - evaluation_strategy = _evaluation_strategy(snapshot, adapter) - tmp_table_name = snapshot.table_name(is_deployable=False) - logger.info( - "Migrating table schema from '%s' to '%s'", - tmp_table_name, - target_table_name, - ) - evaluation_strategy.migrate( - target_table_name=target_table_name, - source_table_name=tmp_table_name, - snapshot=snapshot, - snapshots=parent_snapshots_by_name(snapshot, snapshots), - allow_destructive_snapshots=allow_destructive_snapshots, - ) - else: - logger.info( - "Creating table '%s' for the snapshot of the forward-only model %s", - target_table_name, - snapshot.snapshot_id, - ) - deployability_index = DeployabilityIndex.all_deployable() - render_kwargs: t.Dict[str, t.Any] = dict( - engine_adapter=adapter, - snapshots=parent_snapshots_by_name(snapshot, snapshots), - runtime_stage=RuntimeStage.CREATING, - deployability_index=deployability_index, - ) - with ( - adapter.transaction(), - adapter.session(snapshot.model.render_session_properties(**render_kwargs)), - ): + + with ( + adapter.transaction(), + adapter.session(snapshot.model.render_session_properties(**render_kwargs)), + ): + if adapter.table_exists(target_table_name): + evaluation_strategy = _evaluation_strategy(snapshot, adapter) + tmp_table_name = snapshot.table_name(is_deployable=False) + logger.info( + "Migrating table schema from '%s' to '%s'", + tmp_table_name, + target_table_name, + ) + evaluation_strategy.migrate( + target_table_name=target_table_name, + source_table_name=tmp_table_name, + snapshot=snapshot, + snapshots=parent_snapshots_by_name(snapshot, snapshots), + allow_destructive_snapshots=allow_destructive_snapshots, + ) + else: + logger.info( + "Creating table '%s' for the snapshot of the forward-only model %s", + target_table_name, + snapshot.snapshot_id, + ) self._execute_create( snapshot=snapshot, table_name=target_table_name, diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index d572fc7e11..d131e6aa95 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -54,7 +54,7 @@ SnapshotTableCleanupTask, ) from sqlmesh.core.snapshot.definition import to_view_mapping -from sqlmesh.core.snapshot.evaluator import CustomMaterialization +from sqlmesh.core.snapshot.evaluator import CustomMaterialization, SnapshotCreationFailedError from sqlmesh.utils.concurrency import NodeExecutionFailedError from sqlmesh.utils.date import to_timestamp from sqlmesh.utils.errors import ConfigError, SQLMeshError, DestructiveChangeError @@ -92,13 +92,16 @@ def date_kwargs() -> t.Dict[str, str]: @pytest.fixture def adapter_mock(mocker: MockerFixture): + def mock_exit(self, exc_type, exc_value, traceback): + pass + transaction_mock = mocker.Mock() transaction_mock.__enter__ = mocker.Mock() - transaction_mock.__exit__ = mocker.Mock() + transaction_mock.__exit__ = mock_exit session_mock = mocker.Mock() session_mock.__enter__ = mocker.Mock() - session_mock.__exit__ = mocker.Mock() + session_mock.__exit__ = mock_exit adapter_mock = mocker.Mock() adapter_mock.transaction.return_value = transaction_mock @@ -1160,6 +1163,7 @@ def test_migrate(mocker: MockerFixture, make_snapshot): cursor_mock = mocker.Mock() connection_mock.cursor.return_value = cursor_mock adapter = EngineAdapter(lambda: connection_mock, "") + session_spy = mocker.spy(adapter, "session") current_table = "sqlmesh__test_schema.test_schema__test_model__1" @@ -1201,6 +1205,8 @@ def columns(table_name): ] ) + session_spy.assert_called_once() + def test_migrate_missing_table(mocker: MockerFixture, make_snapshot): connection_mock = mocker.NonCallableMock() @@ -1596,7 +1602,8 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m ), ] - evaluator.create([snapshot], {}) + with pytest.raises(SnapshotCreationFailedError): + evaluator.create([snapshot], {}) adapter_mock.clone_table.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", @@ -2537,7 +2544,9 @@ def test_create_seed_on_error(mocker: MockerFixture, adapter_mock, make_snapshot snapshot.categorize_as(SnapshotChangeCategory.BREAKING) evaluator = SnapshotEvaluator(adapter_mock) - evaluator.create([snapshot], {}) + + with pytest.raises(SnapshotCreationFailedError): + evaluator.create([snapshot], {}) adapter_mock.replace_query.assert_called_once_with( f"sqlmesh__db.db__seed__{snapshot.version}", From eb174fd6c8a7384c593335e03904a13f09294dee Mon Sep 17 00:00:00 2001 From: Chris Rericha <67359577+crericha@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:12:26 -0400 Subject: [PATCH 0446/1056] Chore: Add metadata_updated_snapshots to EvaluatablePlan (#4796) --- sqlmesh/core/plan/definition.py | 2 ++ tests/core/test_plan_stages.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/sqlmesh/core/plan/definition.py b/sqlmesh/core/plan/definition.py index f3fb088ebb..7325dc3532 100644 --- a/sqlmesh/core/plan/definition.py +++ b/sqlmesh/core/plan/definition.py @@ -261,6 +261,7 @@ def to_evaluatable(self) -> EvaluatablePlan: indirectly_modified_snapshots={ s.name: sorted(snapshot_ids) for s, snapshot_ids in self.indirectly_modified.items() }, + metadata_updated_snapshots=sorted(self.metadata_updated), removed_snapshots=sorted(self.context_diff.removed_snapshots), requires_backfill=self.requires_backfill, models_to_backfill=self.models_to_backfill, @@ -298,6 +299,7 @@ class EvaluatablePlan(PydanticModel): ensure_finalized_snapshots: bool directly_modified_snapshots: t.List[SnapshotId] indirectly_modified_snapshots: t.Dict[str, t.List[SnapshotId]] + metadata_updated_snapshots: t.List[SnapshotId] removed_snapshots: t.List[SnapshotId] requires_backfill: bool models_to_backfill: t.Optional[t.Set[str]] = None diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index 5fa140cb6a..c8989f9e83 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -108,6 +108,7 @@ def test_build_plan_stages_basic( ensure_finalized_snapshots=False, directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=True, models_to_backfill=None, @@ -215,6 +216,7 @@ def test_build_plan_stages_with_before_all_and_after_all( ensure_finalized_snapshots=False, directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=True, models_to_backfill=None, @@ -323,6 +325,7 @@ def test_build_plan_stages_select_models( ensure_finalized_snapshots=False, directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=True, models_to_backfill={snapshot_a.name}, @@ -422,6 +425,7 @@ def test_build_plan_stages_basic_no_backfill( ensure_finalized_snapshots=False, directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=True, models_to_backfill=None, @@ -531,6 +535,7 @@ def test_build_plan_stages_restatement( ensure_finalized_snapshots=False, directly_modified_snapshots=[], # No changes indirectly_modified_snapshots={}, # No changes + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=True, models_to_backfill=None, @@ -642,6 +647,7 @@ def test_build_plan_stages_forward_only( indirectly_modified_snapshots={ new_snapshot_a.name: [new_snapshot_b.snapshot_id], }, + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=True, models_to_backfill=None, @@ -770,6 +776,7 @@ def test_build_plan_stages_forward_only_dev( indirectly_modified_snapshots={ new_snapshot_a.name: [new_snapshot_b.snapshot_id], }, + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=True, models_to_backfill=None, @@ -893,6 +900,7 @@ def _get_snapshots(snapshot_ids: t.List[SnapshotId]) -> t.Dict[SnapshotId, Snaps indirectly_modified_snapshots={ new_snapshot_a.name: [new_snapshot_b.snapshot_id], }, + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=True, models_to_backfill=None, @@ -1016,6 +1024,7 @@ def test_build_plan_stages_forward_only_ensure_finalized_snapshots( indirectly_modified_snapshots={ new_snapshot_a.name: [new_snapshot_b.snapshot_id], }, + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=True, models_to_backfill=None, @@ -1088,6 +1097,7 @@ def test_build_plan_stages_removed_model( ensure_finalized_snapshots=False, directly_modified_snapshots=[], indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], removed_snapshots=[snapshot_b.snapshot_id], requires_backfill=False, models_to_backfill=None, @@ -1169,6 +1179,7 @@ def test_build_plan_stages_environment_suffix_target_changed( ensure_finalized_snapshots=False, directly_modified_snapshots=[], indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=False, models_to_backfill=None, @@ -1268,6 +1279,7 @@ def test_build_plan_stages_indirect_non_breaking_no_migration( indirectly_modified_snapshots={ new_snapshot_a.name: [new_snapshot_b.snapshot_id], }, + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=True, models_to_backfill=None, @@ -1355,6 +1367,7 @@ def test_build_plan_stages_indirect_non_breaking_view_migration( indirectly_modified_snapshots={ new_snapshot_a.name: [new_snapshot_c.snapshot_id], }, + metadata_updated_snapshots=[], removed_snapshots=[], requires_backfill=True, models_to_backfill=None, From b075a5a21a23f16e2458e7c1a72ddd0ced10514a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 08:31:21 +0100 Subject: [PATCH 0447/1056] Chore(deps-dev): Bump eslint from 9.28.0 to 9.29.0 (#4798) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 281 +++++++++++++++++++--------------- vscode/extension/package.json | 2 +- web/client/package.json | 2 +- 3 files changed, 157 insertions(+), 128 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0b7342847..4c329eebf4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,8 +70,8 @@ importers: specifier: ^0.25.5 version: 0.25.5 eslint: - specifier: ^9.28.0 - version: 9.28.0(jiti@2.4.2) + specifier: ^9.29.0 + version: 9.29.0(jiti@2.4.2) ts-loader: specifier: ^9.5.2 version: 9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.5)) @@ -83,10 +83,10 @@ importers: version: 5.8.3 typescript-eslint: specifier: ^8.34.0 - version: 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) vitest: specifier: ^3.2.3 - version: 3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) vscode/react: dependencies: @@ -104,7 +104,7 @@ importers: version: 4.1.8 '@tailwindcss/vite': specifier: ^4.1.8 - version: 4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) '@tanstack/react-query': specifier: ^5.80.7 version: 5.80.7(react@18.3.1) @@ -119,7 +119,7 @@ importers: version: 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-plugin': specifier: ^1.120.16 - version: 1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8) + version: 1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -165,7 +165,7 @@ importers: version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react': specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -174,10 +174,10 @@ importers: version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) vitest: specifier: ^3.2.3 - version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -316,7 +316,7 @@ importers: version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react-swc': specifier: ^3.10.1 - version: 3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) ajv: specifier: ^8.17.1 version: 8.17.1 @@ -324,8 +324,8 @@ importers: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.4) eslint: - specifier: ^9.28.0 - version: 9.28.0(jiti@2.4.2) + specifier: ^9.29.0 + version: 9.29.0(jiti@2.4.2) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -343,16 +343,16 @@ importers: version: 5.8.3 typescript-eslint: specifier: ^8.34.0 - version: 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) vitest: specifier: ^3.2.3 - version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) optionalDependencies: '@swc/core-linux-x64-gnu': specifier: ^1.11.31 @@ -765,18 +765,22 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.20.0': - resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + '@eslint/config-array@0.20.1': + resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.2': - resolution: {integrity: sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==} + '@eslint/config-helpers@0.2.3': + resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.14.0': resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.15.0': + resolution: {integrity: sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -785,12 +789,16 @@ packages: resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.29.0': + resolution: {integrity: sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.1': - resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} + '@eslint/plugin-kit@0.3.2': + resolution: {integrity: sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@exodus/schemasafe@1.3.0': @@ -860,6 +868,14 @@ packages: resolution: {integrity: sha512-3WK2FREmDA2aadCjD71PE7tx5evyvmhg80ts1kXp2IzXIA0ZJ7guGM66tj40kxaqwpMSGchwEnnfYswntav76g==} engines: {node: '>=16.0.0'} + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2610,12 +2626,15 @@ packages: boundary@2.0.0: resolution: {integrity: sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -3143,24 +3162,20 @@ packages: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} - eslint-scope@8.3.0: - resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@4.2.1: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.28.0: - resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==} + eslint@9.29.0: + resolution: {integrity: sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -3169,8 +3184,8 @@ packages: jiti: optional: true - espree@10.3.0: - resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: @@ -4229,8 +4244,8 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@10.0.1: - resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} minimatch@3.1.2: @@ -5220,8 +5235,8 @@ packages: uglify-js: optional: true - terser@5.42.0: - resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==} + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} engines: {node: '>=10'} hasBin: true @@ -5664,8 +5679,8 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - webpack-sources@3.3.2: - resolution: {integrity: sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==} + webpack-sources@3.3.3: + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} webpack-virtual-modules@0.6.2: @@ -6296,14 +6311,14 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.28.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.29.0(jiti@2.4.2))': dependencies: - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.29.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.20.0': + '@eslint/config-array@0.20.1': dependencies: '@eslint/object-schema': 2.1.6 debug: 4.4.1(supports-color@8.1.1) @@ -6311,17 +6326,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.2': {} + '@eslint/config-helpers@0.2.3': {} '@eslint/core@0.14.0': dependencies: '@types/json-schema': 7.0.15 + '@eslint/core@0.15.0': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 debug: 4.4.1(supports-color@8.1.1) - espree: 10.3.0 + espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 @@ -6333,11 +6352,13 @@ snapshots: '@eslint/js@9.28.0': {} + '@eslint/js@9.29.0': {} + '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.3.1': + '@eslint/plugin-kit@0.3.2': dependencies: - '@eslint/core': 0.14.0 + '@eslint/core': 0.15.0 levn: 0.4.1 '@exodus/schemasafe@1.3.0': {} @@ -6420,6 +6441,12 @@ snapshots: transitivePeerDependencies: - encoding + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -7491,12 +7518,12 @@ snapshots: postcss: 8.5.4 tailwindcss: 4.1.8 - '@tailwindcss/vite@4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))': + '@tailwindcss/vite@4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@tailwindcss/node': 4.1.8 '@tailwindcss/oxide': 4.1.8 tailwindcss: 4.1.8 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) '@tanstack/history@1.115.0': {} @@ -7574,7 +7601,7 @@ snapshots: optionalDependencies: '@tanstack/react-router': 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-plugin@1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8)': + '@tanstack/router-plugin@1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8)': dependencies: '@babel/core': 7.27.4 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) @@ -7595,7 +7622,7 @@ snapshots: zod: 3.25.55 optionalDependencies: '@tanstack/react-router': 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) webpack: 5.99.8 transitivePeerDependencies: - supports-color @@ -7919,15 +7946,15 @@ snapshots: '@types/vscode@1.96.0': {} - '@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/type-utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.34.0 - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.29.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -7936,14 +7963,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.34.0 '@typescript-eslint/types': 8.34.0 '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.34.0 debug: 4.4.1(supports-color@8.1.1) - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.29.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -7966,12 +7993,12 @@ snapshots: dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.1(supports-color@8.1.1) - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.29.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -7995,13 +8022,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.34.0 '@typescript-eslint/types': 8.34.0 '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.29.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -8053,15 +8080,15 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react-swc@3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.9 '@swc/core': 1.11.31(@swc/helpers@0.5.17) - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react@4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) @@ -8069,7 +8096,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.9 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -8081,21 +8108,21 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))': + '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 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.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) - '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0))': + '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/pretty-format@3.2.3': dependencies: @@ -8126,7 +8153,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.3': dependencies: @@ -8317,9 +8344,9 @@ snapshots: dependencies: event-target-shim: 5.0.1 - acorn-jsx@5.3.2(acorn@8.14.1): + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: - acorn: 8.14.1 + acorn: 8.15.0 acorn@8.14.1: {} @@ -8503,7 +8530,7 @@ snapshots: boundary@2.0.0: {} - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 @@ -8512,6 +8539,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -9119,27 +9150,25 @@ snapshots: esrecurse: 4.3.0 estraverse: 4.3.0 - eslint-scope@8.3.0: + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.0: {} - eslint-visitor-keys@4.2.1: {} - eslint@9.28.0(jiti@2.4.2): + eslint@9.29.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.2 + '@eslint/config-array': 0.20.1 + '@eslint/config-helpers': 0.2.3 '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.28.0 - '@eslint/plugin-kit': 0.3.1 + '@eslint/js': 9.29.0 + '@eslint/plugin-kit': 0.3.2 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -9150,9 +9179,9 @@ snapshots: cross-spawn: 7.0.6 debug: 4.4.1(supports-color@8.1.1) escape-string-regexp: 4.0.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.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 @@ -9172,11 +9201,11 @@ snapshots: transitivePeerDependencies: - supports-color - espree@10.3.0: + espree@10.4.0: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 4.2.0 + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 esprima@4.0.1: {} @@ -9402,7 +9431,7 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 4.1.1 - minimatch: 10.0.1 + minimatch: 10.0.3 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 2.0.0 @@ -10327,13 +10356,13 @@ snapshots: min-indent@1.0.1: {} - minimatch@10.0.1: + minimatch@10.0.3: dependencies: - brace-expansion: 2.0.1 + '@isaacs/brace-expansion': 5.0.0 minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@5.1.6: dependencies: @@ -10341,7 +10370,7 @@ snapshots: minimatch@6.2.0: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimatch@9.0.5: dependencies: @@ -11528,7 +11557,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.42.0 + terser: 5.43.1 webpack: 5.99.8(esbuild@0.25.5) optionalDependencies: esbuild: 0.25.5 @@ -11539,11 +11568,11 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.42.0 + terser: 5.43.1 webpack: 5.99.8 optional: true - terser@5.42.0: + terser@5.43.1: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.15.0 @@ -11723,12 +11752,12 @@ snapshots: typescript: 5.8.3 yaml: 2.8.0 - typescript-eslint@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.29.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -11861,13 +11890,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.2.3(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.2.3(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 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.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -11882,13 +11911,13 @@ snapshots: - tsx - yaml - vite-node@3.2.3(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.2.3(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 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@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -11903,11 +11932,11 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)): + vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)): dependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) - vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.5 fdir: 6.4.5(picomatch@4.0.2) @@ -11920,11 +11949,11 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.42.0 + terser: 5.43.1 tsx: 4.19.4 yaml: 2.8.0 - vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.5 fdir: 6.4.5(picomatch@4.0.2) @@ -11937,15 +11966,15 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.42.0 + terser: 5.43.1 tsx: 4.19.4 yaml: 2.8.0 - vitest@3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.3 '@vitest/runner': 3.2.3 '@vitest/snapshot': 3.2.3 @@ -11963,8 +11992,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.0 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.2.3(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite-node: 3.2.3(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -11985,11 +12014,11 @@ snapshots: - tsx - yaml - vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.3 '@vitest/runner': 3.2.3 '@vitest/snapshot': 3.2.3 @@ -12007,8 +12036,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.0 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.2.3(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite-node: 3.2.3(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -12065,7 +12094,7 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-sources@3.3.2: {} + webpack-sources@3.3.3: {} webpack-virtual-modules@0.6.2: {} @@ -12094,7 +12123,7 @@ snapshots: tapable: 2.2.2 terser-webpack-plugin: 5.3.14(webpack@5.99.8) watchpack: 2.4.4 - webpack-sources: 3.3.2 + webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild @@ -12126,7 +12155,7 @@ snapshots: tapable: 2.2.2 terser-webpack-plugin: 5.3.14(esbuild@0.25.5)(webpack@5.99.8(esbuild@0.25.5)) watchpack: 2.4.4 - webpack-sources: 3.3.2 + webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 997c8d50c2..cc5722c684 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -151,7 +151,7 @@ "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.5.0", "esbuild": "^0.25.5", - "eslint": "^9.28.0", + "eslint": "^9.29.0", "ts-loader": "^9.5.2", "tsx": "^4.19.4", "typescript": "^5.8.3", diff --git a/web/client/package.json b/web/client/package.json index 13248ff92c..cfc62a8a13 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -63,7 +63,7 @@ "@vitejs/plugin-react-swc": "^3.10.1", "ajv": "^8.17.1", "autoprefixer": "^10.4.21", - "eslint": "^9.28.0", + "eslint": "^9.29.0", "jsdom": "^26.1.0", "orval": "^7.9.0", "postcss": "^8.5.4", From 2456be05ae00625f337be2efdddab5854068e845 Mon Sep 17 00:00:00 2001 From: Sung Won Chung Date: Tue, 24 Jun 2025 12:54:11 -0700 Subject: [PATCH 0448/1056] new get started (#4793) --- README.md | 8 +++----- docs/readme/architecture_diagram.png | Bin 159112 -> 153082 bytes 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 24d8e610d4..3215f7cceb 100644 --- a/README.md +++ b/README.md @@ -145,8 +145,7 @@ python -m venv .venv source .venv/bin/activate pip install 'sqlmesh[lsp]' # install the sqlmesh package with extensions to work with VSCode source .venv/bin/activate # reactivate the venv to ensure you're using the right installation -sqlmesh init duckdb # get started right away with a local duckdb instance -sqlmesh plan # see the plan for the changes you're making +sqlmesh init # follow the prompts to get started (choose DuckDB) ```
@@ -163,13 +162,12 @@ python -m venv .venv .\.venv\Scripts\Activate.ps1 pip install 'sqlmesh[lsp]' # install the sqlmesh package with extensions to work with VSCode .\.venv\Scripts\Activate.ps1 # reactivate the venv to ensure you're using the right installation -sqlmesh init duckdb # get started right away with a local duckdb instance -sqlmesh plan # see the plan for the changes you're making +sqlmesh init # follow the prompts to get started (choose DuckDB) ```
-Follow the [quickstart guide](https://sqlmesh.readthedocs.io/en/stable/quickstart/cli/#1-create-the-sqlmesh-project) to learn how to use SQLMesh. You already have a head start! +Follow the [quickstart guide](https://sqlmesh.readthedocs.io/en/stable/quickstart/cli/) to learn how to use SQLMesh. You already have a head start! Follow the [crash course](https://sqlmesh.readthedocs.io/en/stable/examples/sqlmesh_cli_crash_course/) to learn the core movesets and use the easy to reference cheat sheet. diff --git a/docs/readme/architecture_diagram.png b/docs/readme/architecture_diagram.png index f82b33096f388cd6fe5f7fbbabca7f859f1a405b..e3da16d1d68b1d1b475049a6118fa7c1745c2499 100644 GIT binary patch literal 153082 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

r^sUB278`P}#D<48Dw*<<{J;FDa_gC?I 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 @y@xHOg2q9%tR=mCcEo^)cQ(@JFCq6PiknPvn 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

Zi%7=~bjJy6MU-YV?$fg?QF zly#U9c5i!u_bdo@HGmofx;7S4da>nlVHgm-0pGtAyGr*k3OR)f+dUtEtvMj&YE&w+ zf}k3dBY6#OFB^|Fx2EiNe~r*0()W8^__;I&p*r>c&iBD-!yw$l-Pe@@YlgFt&sG!}h3_ z_tUXwL~sAzr3Y@n&g&yx-R)#4$pP1=)h7`Pu27O<^V+m;Pzsf(raXCl5#0>`RwKD& zwS6t$LC^wvv8ZM~lcRM@T!+D83kw8}7Qb$W;1+{$Ko0>bUo&JIqu#FagTnvX9ra%G z<2{){GoAVL1apdVpjeyXz_;%DV-C(?m4poIr z{G4IK2NBeLHV?zP$JT$Q?%NDft4Xl^UL@uQiVHq#?{cmKO>J>si+~dxVM;xDF{98Y z@EQ*+s2KCg<>5`sD@$8m_30m!4QD2GsR~wFmIjHNd8W3$Ud)V~oWHxOipW?er14f; zdQya9Ag^8!A8dONy9$071?N~3m*=6Zx&_pzR=~>AB;0%VaG67)Pb<=dE2o~+U$u}g zy$fw3lbHPe(jq?X^&2$vAf9&Zi;&*^eo_hSh}Y}RzX^pQ)=yDa=apP*ss#9 zqqDO9>Nx|s)a$N27!IDN^_n_GMjV6Y9%kK#p;JC~M@Rep*6=MZqMeI8rfLmQXO-Y? zUWuBkK#>pW;^qdga1&u^hY!1N$C>$Qd`Czqk$n_tY8eIQcaElx2iUqZH-<4E;l^wI z;yMd`9-tkHTMd}p!9LgSk4RpJ(HhlzABXXG##gzyx~UAZT)6BhQiSWiLEy*$Fn-WS zwWChn!Y~CR$Fb_1)t_-*gE;?^vSU(CrIdmnu}q(bS8TFCC=sbvS0l6i9F*BS%yG4= z%Ekq>RMZhV4R49xI&9rBJo-9L3m2G=uG?Og-hCC-yP5Y_-|;ZBr@Q;|h1)gc=(KOA z@7H5Qdox>BvH}cdJbOKv${%?X{XKg8BRg=1LsXxk)^zNh-C#&eWlW&;feAk~UU1iI zi=^e#<&owF<}r~y2fO$hD^H9xXT$bxKjbSuouX62Xje6#$@{vmOWt~Ix&>K3NrxC) zle6;&R&M}RuO-s>x023Bm*1);WA?r?VoZ!LxM#`6*+JG}JUG+*j@!XPU)!2|eF@*4 zskP8E7D^_G193~9?bGYs2A7y-xW*-fukDw)6P0icrqSKI-R`s^Dg9;(i-l{? zI~3Ei?l?DIixpPdeLHeTcm1Zuy^QgZ>V&} zWKaHHfg%c&d-O$6I(Fx=OK!HB*&eR_Y=*98y!$&oqL1q+y_O+v(<*ATUqckpPR}>G zaHktv;0z<`@(Sx4Zxq8evu5LUxq#4wop*PR;+ODFK%&*NztJ^h#3fhZ6y=muIV&r9TRz}`{rn|bR zE;W{qmwK&?sbk3>jQ6h#pgsh`HktH}@@~t-9wXkKvdKyDUR3NTs5ycA#wi_i?(dmY zq7`@>=aP4~<-K~;MNlm@G|`dx+bF^Ts(pp7qXX4gbvOf$Epn}XZ$SI%Xhkxu3pX}C zjs7@yDLXdgF43DB@E^gS56WWa!+(m=J3pUZEHf9!OfSc_Drj{?X(#snC0rPt>0#`^ zQ$w*-<^uJydEO9}<4QO3<(dK@XXI(M_b=L0-rb3m5BC_zAL&+fYod!fvFUr2r1X4O zOzCxcg~Du0sH2G~V*Z@P{o%Sy38*HlxEERY*;u@h3;|(pi$Z_?CE~qwXMFU+Crzq! z33RT#-W;L|mG-08q}xxcz}L!bw2!+STi_{)o-MDM@1;X}kYpfix$VV}_p=>T1;4)W zGje}gkD3nhfpQ|gcoq|hdvA?hu2pBj(L8=5(T(Ix)zp7fi?}F`!#V#!cv*v8I z5K(6z*RieV!)JeZ{f;U6yE-m~HNFuE=Zg)5w^cYDSRl>vS}d^clE|)tCrJc$ZDTrM z6+Q)E?iyUO`JR$cDQMd0iFO>@fPCq=6J;#tcEEOm8pnq5`1v(6d}jXQ<3UpV;xgqM zq(BueQ=g9jwlqfBkiRQJ40BtwywL!q&M~JypJSS`5Vx!F^ebpUz+=swrcCAFsv8CVzTIhmr^c8=HKORF-IE)+juCa#SY_ zZfCIMJMqlU0Z1(wp;Uj;=6}35L~Aj#vM*>%!hF(k5#ap5BT^~waOIT# z<>ogRm`n>#EV{WL&^{P*J-D9lcqL*( z_hD0@z51@tE!e!~U=`PdCSO64vuR~_MeiJ6P)T-v0QJ!n0r1G2n!N2c_xXmc&eb{3 zR>Yoc)bJa8Pw20f?~Hp1cPcxaqz5LZ!gs`w8XS6MK+k_ueaZQRN!_XLxcg*b517kx zLxclu(rHdT2qyo?dwqdZ-(c+IlOiD(0?IYTU?DTN?P_j+hcYNN9Pzt$csY;R zDp|+yVUOpzxd0P}+dvi-Kw?DPY_?AshFs(Bn1TBV6;&_N9TbxBHFx4;u^whV``5ic z(a&H09qtO|ZCk*miY3DFTpv| zt*y4HVb^l%>Za1VO*{r9^Jo2@JiuD6ga@@8{Ke#;YM2QDiLf+Zc4cHOuQ~7%%O2e< zLw`09nD37()|dSbjX_pzj#n+Ty6k?Bd#S$}m7E5Dqxm9pH+i#x^L0OM3D+mVO1=U| zrK+u8(N=$J%#kYx4+rbWFxpTI((hTP{z19q!#;|Crp}13~W~*8z=~CFG>Tm10>5(Z(0Ld^_t786Z(dJ|5$->p1h>er?+>;#M`& zIQl!2m8i8IqE18%Cxe&DRY-BwBBk~>@9BD~p)kc#oX^sZ_40-Z3+k$->%sL=yf*`4 zV#n&PWq2ru`*gcF@;=RPEH^(0-iepmTnCfk!JlHeDrIif9%g!<%fAE(0OMD|X>;6_ z#1fZ-CUKoN_KC!NLD=xxi(%&R%`#smLp0clpJC>$39T7sVn|q;I3S*;Y`h6;3RL_Z zk*BY}D-fXzoai&5Q`D7 zM3Bamy!T$;bI~xoIUe`ize0eLdi6(!m4QQfb-Htwrf%^Sr0>R^=zNj%ipg$t^|wn_ zHWaXk2|itYKzi+_^IA_N=v_NxVj)9IME>#m!lI5}wyhj>AmCl48Q}-5bH3R+zSDU) zncz7vR*B(DZ!}^AFrb9ucD4oxKJRo;=2<~Jb%A5T-cgcESO*R@qu7hDourd)6(&{- z$JQ?;%fF7nx{Vo|`VEIyz@Z#kZ8ghgb312>-@hogeq{{f08HJ)2wNHQ?a_PzqEILV zH{0Oj_uaP>HTiqaF8~~I)7}s`tI^##v9BuYkD3}oo22+6&)K)N{KT|Gf$8kzz{g>R zhA59ccuQON`QR^juN4MF%ous8ckeC%*Y1aYJ*OVluU0ICpVCsna(Wb}-!6taufVlq z2zl@4YDZSMBijY_W708|sYkPlg^kXg`aQfcBic%4Ty;?3e%gz4UaOpol0fQR zffYI5I%a6xDdD&3$};OXAuS{Dbt^PDvx_e=VB?Jg))UJJPWpgLmH zF2Mauq1u8$35)|)I2@oR(DQ;CmRagyBM&Wcz;u_li?%+T(p(riFh+x6Sy)FE!-_2i ze(t9!&>$=txzOfHtBOfS734vNnzE)fCo9~SzX7``$=m)SZ^xXi-oTXn;*bl>tJ9H# z&Srnv7q+BeG-}}OJ2TDvDX>AG3b6iXN3Osd1w(#Yd@-#kHcAE(pqB->crK6T_jQ5; z;j?45N7FwWjW52mJBM`_S2ooj;z9@tg%uel*pwA)Jx!M|zR269$V(sEZ@f-PT_cZ_ z;*<>?m{?v4(csm}SltDebifVI!s<)?YmV0QEij(GF_sz6V7aXEnoU{H0owy%v zi*YW7?FvAfu)+udl-#W@DRuTVotWa%`fk%pyj_ok6ptIGZachuskw4SUp4`zXHB z)V7Y1J3}=GCN~sPo7MBC^oO0RXAnN9g>Se{uqMW$R~vEUY0eGiJ}FOd!dTx}sqH*n zPgUVHBnW#e7Y`itSkwuSpN$_r-#~XgS^DWx+U>_L)i{+5IR_5P>6H>R0cX!W@|_mI zolC_m{LU#o+7Ajx5pt{Tv7vW~tiXoT&BRZ~;dq_m(J++@n~k%N5S! zi6O=BiH?bTyKDOkV$y9ELl^E$lbb)^bE>|1N369)_+3k29k->x;Cr@3E2j$n@u4~9 z0id`tA8Ox+RPW@)uaSwRps?7;A^xO8JpX6%&Cda7F#21ZZ05)D^3X4`Q<&J^ug|pQ%6Qx zx+cyyk_>T-{@d4>^Um7z!H5!dbZC(j|83^V_c$V{yExk}mMxc9H%pDspQ zQ|%JyFM&UMv3JhgXYdZqB5XkP_ys_Vq5G95l*HZyzF@QlExyuSsrNjl{-Xt?>AMEP^{23w85zpCYQ`fke{pWXge> zub_ea)YF+XjR{I2t(BoqKlGN&+dvvEuE1|bK!yasB1Q>ag6=x2+IlmD+|abRUeB7r zZ-N60so&GOH60$$T&FVF40dZiS+jm-rsAmj>qJvC`6rN6+2E_aYorXR*q;2jz2 zSPPFqYnDm{){^Q>-X%$O)3<^qY(SQvdGNrp@HBNXdxPLqy~}Gwtrtc8&2O6sC1~Gr zm(j}pA=NnZ+zi(KE5E}t`iLI(mlMI#MT!nKo2zA%C^_SL7$p`^L)pi1qk|_GM@PgOSi4>-CzsRn;+L{IX#xN>{n34I=fcy}o zF3#?J5=I?1+h#IOinVl5I!7<^&)htEd?w`9Pe&Uw@r+cgy!=zY0cICaB5*X8c6{u= zxVtnZyosoq3USDRH?mo(d>8UVNewkJS<}%4?fH<{yBCaPlgleZq3L(NmRCx^1fImO z37$5bc?(_Bg}R57EY{>d_HfDYr3qRFlt^f4Os=?q>#)7O%$Ju)+)#+Y34%RV(1+jl zAd?jUC@6^q_6D+teKu->MO{Zp-Zt}_X*+%8cNk`W@L+)5Oph0~#Vj&>p-W8?MbJ@pwP*tzuYl7H@o2ZiSp&~E+nIT2%ZAYm zxt^D3n~~dKrLPE?=H@l&u`LIv13pSuwVMkprvs&?aS)gTaqfOIcM8jA8)IGc0R-SE2Q zKla^88eUI3@Fo7QVdhnE5U#Z)1<-voR@d4apX+cfY-9=?REw0Tk*UqtBaN=Qy-D>9 z+9n0E?~9nuywEu#LDrXo@x~cX5mgQ}mg< zciM2!^eAZRSyF}w5ZM1saec1C2|25q&uLxFj`F&*}o7xEt{@=^Fm(Vd*+SS9bT+k&?OdJ;R-G=?{X42*Nr+!D~Dz1ou8O3TMu24?xM& z!6Kuam7^#V5WOS?js2LoxKVlp+vcZ3$O}Z6j2OcjDN)uXs1fy2W*hF#lEUT4YFTU^ z-;E%tR|^e~?7J(A<&!k;&rIfvuqkq95@^aUgwA<5q%+f%JzK}RTR9-%LSdI_na--r zzN*Hd3QRBN#}?GAy+GA%{Tgg7zt~5vGdK1e4u|g`O)F>f(8H|b8!0B!^V1yp%I zes5@Y_LIn>Tr6^=0xcS0`#ptHjkxOsU@??aw`pvySTAET1{$b(z*d|ZVZ1Xb1Pk$T zQA}YwVas?+Xvvul%alQRxOzS!O-qNWIPf$Groq3wY&n1npzu`; z9YO)97w}Y_!1uQ__|y7v9{Rpb0PHok<|g0{GumGSz>X^cfLOxks6Zc$yE+!bq;Xi| zQvaEy0U;;Ar99yx#y9p+#Nec7@EmHxe;=+C~!U5#at7wxDNj4a(-% z?43|0#kbYUiuOFx`)x$wH-FjgbaHZJR;EmGK3(Nqz0+7MY_Dm5AwJ9A0Km`ep&g}O zDVH}nI@c12C1`aJ{q^6g6C1loEI7=(CY6@bTIi%tNm_Hw1W*L2vVQDReBKC_`x7>* z@%xSYg=Ae%AJU~{PH(k56L>O9<`|sTp$yOjYNtm%rFAJ&O046T2U(?PKF@|$t`+*k z2wItuDsly81Yzn-${yI+H@+_2o{mHwkctMKGB8SPYc4sWJ#TvVmozk|GZFByz_vJ~ zNJ&a5mhsKvm*w+ie9_Rp?869aywUCKTerRn&e4$~#Ps(#b;%m~w? zc<5#dFe>QdEC4h7iYd0*P5tTb2gEHzwACy*H&#HemaVu{M!`icp6vv41MNGNBo+(g zJP`pb6K$2mcTxHWP4?7TkoCnppl-9<=w_k=0z3IHTz*6wkpA~?vv>Q}VkQ_n8)f6% zm5;!eKVk%i3-~*zSgO(`Qhd(E*q8t^fcMYVHs0!b6fVS`^{wAuRBMIY0I9kYfHgV# zirjzxv$pHai>jGBl{254vZP*7N(Ma5r#Lk!d=03&!Pw;(^@pPXGNx@uA^8M6?)}hv z8E>OKiiSe_ik^fTLpkU+u>gE+OR@mLDi<4Qd<3D~=)(y-&BMu}{jgb@Pr4LxLV&%D zNWRGU$9${P&96xwwYE8~Wjil{9`9CepabEOG2>M*$?cwaA~9Fe%vCvW9Y#_i8v@4t z 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%0AWY;lrewA1P*6qH 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 A9{>OV From 96cc041efb55de05a36a6adb26155a418abf09e4 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 24 Jun 2025 12:56:25 -0700 Subject: [PATCH 0449/1056] Fix: Support signals for Python models (#4797) --- examples/sushi/models/orders.py | 1 + sqlmesh/core/loader.py | 1 + sqlmesh/core/model/decorator.py | 36 ++++++++++++++++++--------------- tests/core/test_context.py | 2 +- tests/core/test_model.py | 24 ++++++++++++++++++++++ 5 files changed, 47 insertions(+), 17 deletions(-) diff --git a/examples/sushi/models/orders.py b/examples/sushi/models/orders.py index aa0e04559f..8d8718a3e3 100644 --- a/examples/sushi/models/orders.py +++ b/examples/sushi/models/orders.py @@ -36,6 +36,7 @@ "end_ts": "int", "event_date": "date", }, + signals=[("test_signal", {"arg": 1})], ) def execute( context: ExecutionContext, diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index 7f90c0de63..41bb8a0aef 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -672,6 +672,7 @@ def _load_python_models( default_catalog=self.context.default_catalog, infer_names=self.config.model_naming.infer_names, audit_definitions=audits, + signal_definitions=signals, default_catalog_per_gateway=self.context.default_catalog_per_gateway, ): if model.enabled: diff --git a/sqlmesh/core/model/decorator.py b/sqlmesh/core/model/decorator.py index 0151e9ec76..3b78efc636 100644 --- a/sqlmesh/core/model/decorator.py +++ b/sqlmesh/core/model/decorator.py @@ -9,6 +9,7 @@ from sqlglot.dialects.dialect import DialectType from sqlmesh.core.macros import MacroRegistry +from sqlmesh.core.signal import SignalRegistry from sqlmesh.utils.jinja import JinjaMacroRegistry from sqlmesh.core import constants as c from sqlmesh.core.dialect import MacroFunc, parse_one @@ -48,23 +49,24 @@ def __init__(self, name: t.Optional[str] = None, is_sql: bool = False, **kwargs: self.kwargs = kwargs # Make sure that argument values are expressions in order to pass validation in ModelMeta. - calls = self.kwargs.pop("audits", []) - self.kwargs["audits"] = [ - ( - (call, {}) - if isinstance(call, str) - else ( - call[0], - { - arg_key: exp.convert( - tuple(arg_value) if isinstance(arg_value, list) else arg_value - ) - for arg_key, arg_value in call[1].items() - }, + for function_call_attribute in ("audits", "signals"): + calls = self.kwargs.pop(function_call_attribute, []) + self.kwargs[function_call_attribute] = [ + ( + (call, {}) + if isinstance(call, str) + else ( + call[0], + { + arg_key: exp.convert( + tuple(arg_value) if isinstance(arg_value, list) else arg_value + ) + for arg_key, arg_value in call[1].items() + }, + ) ) - ) - for call in calls - ] + for call in calls + ] if "default_catalog" in kwargs: raise ConfigError("`default_catalog` cannot be set on a per-model basis.") @@ -142,6 +144,7 @@ def model( defaults: t.Optional[t.Dict[str, t.Any]] = None, macros: t.Optional[MacroRegistry] = None, jinja_macros: t.Optional[JinjaMacroRegistry] = None, + signal_definitions: t.Optional[SignalRegistry] = None, audit_definitions: t.Optional[t.Dict[str, ModelAudit]] = None, dialect: t.Optional[str] = None, time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT, @@ -223,6 +226,7 @@ def model( "macros": macros, "jinja_macros": jinja_macros, "audit_definitions": audit_definitions, + "signal_definitions": signal_definitions, "blueprint_variables": blueprint_variables, **rendered_fields, } diff --git a/tests/core/test_context.py b/tests/core/test_context.py index e08f5346ea..213c4cec2b 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -2122,7 +2122,7 @@ def test_check_intervals(sushi_context, mocker): intervals = sushi_context.check_intervals(environment=None, no_signals=False, select_models=[]) min_intervals = 19 - assert spy.call_count == 1 + assert spy.call_count == 2 assert len(intervals) >= min_intervals for i in intervals.values(): diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 7c65f25889..a1f9034481 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -5303,6 +5303,30 @@ def my_signal(batch): ) +def test_load_python_model_with_signals(): + @signal() + def always_true(batch): + return True + + @model( + name="model_with_signal", + kind="full", + columns={'"COL"': "int"}, + signals=[("always_true", {})], + ) + def model_with_signal(context, **kwargs): + return pd.DataFrame([{"COL": 1}]) + + models = model.get_registry()["model_with_signal"].models( + get_variables=lambda _: {}, + path=Path("."), + module_path=Path("."), + signal_definitions=signal.get_registry(), + ) + assert len(models) == 1 + assert models[0].signals == [("always_true", {})] + + def test_null_column_type(): expressions = d.parse( """ From 186f0edceb01f68a37a2b03266d6ab0be76bc8a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Thal=C3=A9n?= Date: Tue, 24 Jun 2025 22:45:50 +0200 Subject: [PATCH 0450/1056] fix(mssql): update SQL keywords to uppercase for consistency (#4795) --- sqlmesh/core/engine_adapter/mssql.py | 22 +++++++++++----------- tests/core/engine_adapter/test_mssql.py | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index 88b3f51ed3..a00e11e0f7 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -90,18 +90,18 @@ def columns( sql = ( exp.select( - "column_name", - "data_type", - "character_maximum_length", - "numeric_precision", - "numeric_scale", + "COLUMN_NAME", + "DATA_TYPE", + "CHARACTER_MAXIMUM_LENGTH", + "NUMERIC_PRECISION", + "NUMERIC_SCALE", ) - .from_("information_schema.columns") - .where(f"table_name = '{table.name}'") + .from_("INFORMATION_SCHEMA.COLUMNS") + .where(f"TABLE_NAME = '{table.name}'") ) database_name = table.db if database_name: - sql = sql.where(f"table_schema = '{database_name}'") + sql = sql.where(f"TABLE_SCHEMA = '{database_name}'") columns_raw = self.fetchall(sql, quote_identifiers=True) @@ -145,12 +145,12 @@ def table_exists(self, table_name: TableName) -> bool: sql = ( exp.select("1") - .from_("information_schema.tables") - .where(f"table_name = '{table.alias_or_name}'") + .from_("INFORMATION_SCHEMA.TABLES") + .where(f"TABLE_NAME = '{table.alias_or_name}'") ) database_name = table.db if database_name: - sql = sql.where(f"table_schema = '{database_name}'") + sql = sql.where(f"TABLE_SCHEMA = '{database_name}'") result = self.fetchone(sql, quote_identifiers=True) diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index beeaa59c89..22f371746c 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -79,7 +79,7 @@ def test_columns(adapter: MSSQLEngineAdapter): } adapter.cursor.execute.assert_called_once_with( - """SELECT [column_name], [data_type], [character_maximum_length], [numeric_precision], [numeric_scale] FROM [information_schema].[columns] WHERE [table_name] = 'table' AND [table_schema] = 'db';""" + """SELECT [COLUMN_NAME], [DATA_TYPE], [CHARACTER_MAXIMUM_LENGTH], [NUMERIC_PRECISION], [NUMERIC_SCALE] FROM [INFORMATION_SCHEMA].[COLUMNS] WHERE [TABLE_NAME] = 'table' AND [TABLE_SCHEMA] = 'db';""" ) @@ -149,8 +149,8 @@ def test_table_exists(make_mocked_engine_adapter: t.Callable): resp = adapter.table_exists("db.table") adapter.cursor.execute.assert_called_once_with( """SELECT 1 """ - """FROM [information_schema].[tables] """ - """WHERE [table_name] = 'table' AND [table_schema] = 'db';""" + """FROM [INFORMATION_SCHEMA].[TABLES] """ + """WHERE [TABLE_NAME] = 'table' AND [TABLE_SCHEMA] = 'db';""" ) assert resp adapter.cursor.fetchone.return_value = None @@ -506,7 +506,7 @@ def test_replace_query(make_mocked_engine_adapter: t.Callable): adapter.replace_query("test_table", parse_one("SELECT a FROM tbl"), {"a": "int"}) assert to_sql_calls(adapter) == [ - """SELECT 1 FROM [information_schema].[tables] WHERE [table_name] = 'test_table';""", + """SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE [TABLE_NAME] = 'test_table';""", "TRUNCATE TABLE [test_table];", "INSERT INTO [test_table] ([a]) SELECT [a] FROM [tbl];", ] From b256ba5d64ae125750fab0787bb7e70e46bd527c Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 24 Jun 2025 14:00:57 -0700 Subject: [PATCH 0451/1056] Fix: Propagation of snapshots with missing intervals into the physical layer update stage (#4801) --- sqlmesh/core/plan/stages.py | 6 +++++- tests/core/test_plan_stages.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index b50edfbb01..863e53387f 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -431,7 +431,11 @@ def _get_physical_layer_update_stage( return PhysicalLayerUpdateStage( snapshots=self._get_snapshots_to_create(plan, snapshots), all_snapshots=snapshots, - snapshots_with_missing_intervals={s.snapshot_id for s in snapshots_to_intervals}, + snapshots_with_missing_intervals={ + s.snapshot_id + for s in snapshots_to_intervals + if plan.is_selected_for_backfill(s.name) + }, deployability_index=deployability_index, ) diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index c8989f9e83..b806b95b75 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -141,6 +141,10 @@ def test_build_plan_stages_basic( snapshot_a.snapshot_id, snapshot_b.snapshot_id, } + assert {s.snapshot_id for s in physical_stage.snapshots_with_missing_intervals} == { + snapshot_a.snapshot_id, + snapshot_b.snapshot_id, + } assert physical_stage.deployability_index == DeployabilityIndex.all_deployable() # Verify BackfillStage @@ -357,6 +361,7 @@ def test_build_plan_stages_select_models( assert len(physical_stage.snapshots) == 1 assert {s.snapshot_id for s in physical_stage.snapshots} == {snapshot_a.snapshot_id} assert physical_stage.deployability_index == DeployabilityIndex.all_deployable() + assert physical_stage.snapshots_with_missing_intervals == {snapshot_a.snapshot_id} # Verify BackfillStage backfill_stage = stages[2] From 50572143d94ee1d3136bc66185c86873f8b7d37c Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 25 Jun 2025 10:19:31 +0100 Subject: [PATCH 0452/1056] ci: make regex for circle ci stricter (#4805) --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f8b00cf07c..259746cc70 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,14 +7,14 @@ on_main_or_tag_filter: &on_main_or_tag_filter branches: only: main tags: - only: /^v.+/ + only: /^v\d+\.\d+\.\d+$/ on_tag_filter: &on_tag_filter filters: branches: ignore: /.*/ tags: - only: /^v.+/ + only: /^v\d+\.\d+\.\d+$/ orbs: path-filtering: circleci/path-filtering@1.2.0 From 9fd2e5eaefc2c61472f8f7492af50a4dfab831dd Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 25 Jun 2025 10:41:06 +0100 Subject: [PATCH 0453/1056] ci: improve final regex requirement (#4806) --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 259746cc70..37b03fbe95 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,14 +7,14 @@ on_main_or_tag_filter: &on_main_or_tag_filter branches: only: main tags: - only: /^v\d+\.\d+\.\d+$/ + only: /^v\d+\.\d+\.\d+/ on_tag_filter: &on_tag_filter filters: branches: ignore: /.*/ tags: - only: /^v\d+\.\d+\.\d+$/ + only: /^v\d+\.\d+\.\d+/ orbs: path-filtering: circleci/path-filtering@1.2.0 From 34b05d6acdb5af4e6ef94b5d2dc98c6b4bd4dca8 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:03:35 +0100 Subject: [PATCH 0454/1056] ci: create release of vscode extension (#4765) --- .github/workflows/release_extension.yaml | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/release_extension.yaml diff --git a/.github/workflows/release_extension.yaml b/.github/workflows/release_extension.yaml new file mode 100644 index 0000000000..1807fee37d --- /dev/null +++ b/.github/workflows/release_extension.yaml @@ -0,0 +1,44 @@ +name: Release VSCode Extension +on: + push: + tags: + - 'vscode@v*' +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 10 + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Extract version from tag + id: extract_version + run: | + VERSION=${GITHUB_REF#refs/tags/vscode@v} + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + - name: Update package.json version + working-directory: vscode/extension + run: | + npm version ${{ steps.extract_version.outputs.VERSION }} --no-git-tag-version + - name: Install dependencies + working-directory: vscode/extension + run: pnpm install + - name: Run CI + run: pnpm run ci + - name: Build extension + working-directory: vscode/extension + run: pnpm run vscode:package + - name: Upload extension to Marketplace + working-directory: vscode/extension + run: | + pnpx vsce publish --packagePath sqlmesh-${{ steps.extract_version.outputs.VERSION }}.vsix + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} From fe6f3baa74fd24dd948911c3d92a7b7c590ab1ca Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:12:05 +0100 Subject: [PATCH 0455/1056] feat: add fixes to linting errors (#4800) --- sqlmesh/core/linter/definition.py | 7 +- sqlmesh/core/linter/rule.py | 29 +++- sqlmesh/core/linter/rules/builtin.py | 27 +++- sqlmesh/lsp/context.py | 158 ++++++++++++++++++++ sqlmesh/lsp/main.py | 215 ++++++++++++--------------- tests/lsp/test_code_actions.py | 111 ++++++++++++++ 6 files changed, 421 insertions(+), 126 deletions(-) create mode 100644 tests/lsp/test_code_actions.py diff --git a/sqlmesh/core/linter/definition.py b/sqlmesh/core/linter/definition.py index 14ae1dd2ef..9cfa4076cd 100644 --- a/sqlmesh/core/linter/definition.py +++ b/sqlmesh/core/linter/definition.py @@ -8,7 +8,7 @@ from collections.abc import Iterator, Iterable, Set, Mapping, Callable from functools import reduce from sqlmesh.core.model import Model -from sqlmesh.core.linter.rule import Rule, RuleViolation, Range +from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix from sqlmesh.core.console import LinterConsole, get_console if t.TYPE_CHECKING: @@ -75,6 +75,7 @@ def lint_model( model=model, violation_type="error", violation_range=violation.violation_range, + fixes=violation.fixes, ) for violation in error_violations ] + [ @@ -84,6 +85,7 @@ def lint_model( model=model, violation_type="warning", violation_range=violation.violation_range, + fixes=violation.fixes, ) for violation in warn_violations ] @@ -152,7 +154,8 @@ def __init__( model: Model, violation_type: t.Literal["error", "warning"], violation_range: t.Optional[Range] = None, + fixes: t.Optional[t.List[Fix]] = None, ) -> None: - super().__init__(rule, violation_msg, violation_range) + super().__init__(rule, violation_msg, violation_range, fixes) self.model = model self.violation_type = violation_type diff --git a/sqlmesh/core/linter/rule.py b/sqlmesh/core/linter/rule.py index 84e1693bef..da33df2124 100644 --- a/sqlmesh/core/linter/rule.py +++ b/sqlmesh/core/linter/rule.py @@ -39,6 +39,22 @@ class Range: end: Position +@dataclass(frozen=True) +class TextEdit: + """A text edit to apply to a file.""" + + range: Range + new_text: str + + +@dataclass(frozen=True) +class Fix: + """A fix that can be applied to resolve a rule violation.""" + + title: str + edits: t.List[TextEdit] + + class _Rule(abc.ABCMeta): def __new__(cls: Type[_Rule], clsname: str, bases: t.Tuple, attrs: t.Dict) -> _Rule: attrs["name"] = clsname.lower() @@ -66,10 +82,14 @@ def violation( self, violation_msg: t.Optional[str] = None, violation_range: t.Optional[Range] = None, + fixes: t.Optional[t.List[Fix]] = None, ) -> RuleViolation: """Create a RuleViolation instance for this rule""" return RuleViolation( - rule=self, violation_msg=violation_msg or self.summary, violation_range=violation_range + rule=self, + violation_msg=violation_msg or self.summary, + violation_range=violation_range, + fixes=fixes, ) def get_definition_location(self) -> RuleLocation: @@ -103,11 +123,16 @@ def __repr__(self) -> str: class RuleViolation: def __init__( - self, rule: Rule, violation_msg: str, violation_range: t.Optional[Range] = None + self, + rule: Rule, + violation_msg: str, + violation_range: t.Optional[Range] = None, + fixes: t.Optional[t.List[Fix]] = None, ) -> None: self.rule = rule self.violation_msg = violation_msg self.violation_range = violation_range + self.fixes = fixes or [] def __repr__(self) -> str: return f"{self.rule.name}: {self.violation_msg}" diff --git a/sqlmesh/core/linter/rules/builtin.py b/sqlmesh/core/linter/rules/builtin.py index 0480683f6d..02c2bb628e 100644 --- a/sqlmesh/core/linter/rules/builtin.py +++ b/sqlmesh/core/linter/rules/builtin.py @@ -8,7 +8,7 @@ from sqlglot.helper import subclasses from sqlmesh.core.linter.helpers import TokenPositionDetails -from sqlmesh.core.linter.rule import Rule, RuleViolation, Range +from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit from sqlmesh.core.linter.definition import RuleSet from sqlmesh.core.model import Model, SqlModel @@ -22,7 +22,8 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]: return None if model.query.is_star: violation_range = self._get_range(model) - return self.violation(violation_range=violation_range) + fixes = self._create_fixes(model, violation_range) + return self.violation(violation_range=violation_range, fixes=fixes) return None def _get_range(self, model: SqlModel) -> t.Optional[Range]: @@ -37,6 +38,28 @@ def _get_range(self, model: SqlModel) -> t.Optional[Range]: return None + def _create_fixes( + self, model: SqlModel, violation_range: t.Optional[Range] + ) -> t.Optional[t.List[Fix]]: + """Create fixes for the SELECT * violation.""" + if not violation_range: + return None + columns = model.columns_to_types + if not columns: + return None + new_text = ", ".join(columns.keys()) + return [ + Fix( + title="Replace SELECT * with explicit column list", + edits=[ + TextEdit( + range=violation_range, + new_text=new_text, + ) + ], + ) + ] + class InvalidSelectStarExpansion(Rule): def check_model(self, model: Model) -> t.Optional[RuleViolation]: diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index f3bdcc13e3..0d7ba16c10 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from pathlib import Path +import uuid from sqlmesh.core.context import Context import typing as t @@ -8,6 +9,7 @@ from sqlmesh.lsp.custom import ModelForRendering from sqlmesh.lsp.custom import AllModelsResponse, RenderModelEntry from sqlmesh.lsp.uri import URI +from lsprotocol import types @dataclass @@ -33,8 +35,14 @@ class LSPContext: map: t.Dict[Path, t.Union[ModelTarget, AuditTarget]] _render_cache: t.Dict[Path, t.List[RenderModelEntry]] _lint_cache: t.Dict[Path, t.List[AnnotatedRuleViolation]] + _version_id: str + """ + This is a version ID for the context. It is used to track changes to the context. It can be used to + return a version number to the LSP client. + """ def __init__(self, context: Context) -> None: + self._version_id = str(uuid.uuid4()) self.context = context self._render_cache = {} self._lint_cache = {} @@ -62,6 +70,11 @@ def __init__(self, context: Context) -> None: **audit_map, } + @property + def version_id(self) -> str: + """Get the version ID for the context.""" + return self._version_id + def render_model(self, uri: URI) -> t.List[RenderModelEntry]: """Get rendered models for a file, using cache when available. @@ -150,6 +163,86 @@ def lint_model(self, uri: URI) -> t.List[AnnotatedRuleViolation]: self._lint_cache[path] = diagnostics return diagnostics + def get_code_actions( + self, uri: URI, params: types.CodeActionParams + ) -> t.Optional[t.List[t.Union[types.Command, types.CodeAction]]]: + """Get code actions for a file.""" + + # Get the violations (which contain the fixes) + violations = self.lint_model(uri) + + # Convert violations to a map for quick lookup + # Use a hashable representation of Range as the key + violation_map: t.Dict[ + t.Tuple[str, t.Tuple[int, int, int, int]], AnnotatedRuleViolation + ] = {} + for violation in violations: + if violation.violation_range: + lsp_diagnostic = self.diagnostic_to_lsp_diagnostic(violation) + if lsp_diagnostic: + # Create a hashable key from the diagnostic message and range + key = ( + lsp_diagnostic.message, + ( + lsp_diagnostic.range.start.line, + lsp_diagnostic.range.start.character, + lsp_diagnostic.range.end.line, + lsp_diagnostic.range.end.character, + ), + ) + violation_map[key] = violation + + # Get diagnostics in the requested range + diagnostics = params.context.diagnostics if params.context else [] + + code_actions: t.List[t.Union[types.Command, types.CodeAction]] = [] + + for diagnostic in diagnostics: + # Find the corresponding violation + key = ( + diagnostic.message, + ( + diagnostic.range.start.line, + diagnostic.range.start.character, + diagnostic.range.end.line, + diagnostic.range.end.character, + ), + ) + found_violation = violation_map.get(key) + + if found_violation is not None and found_violation.fixes: + # Create code actions for each fix + for fix in found_violation.fixes: + # Convert our Fix to LSP TextEdits + text_edits = [] + for edit in fix.edits: + text_edits.append( + types.TextEdit( + range=types.Range( + start=types.Position( + line=edit.range.start.line, + character=edit.range.start.character, + ), + end=types.Position( + line=edit.range.end.line, + character=edit.range.end.character, + ), + ), + new_text=edit.new_text, + ) + ) + + # Create the code action + code_action = types.CodeAction( + title=fix.title, + kind=types.CodeActionKind.QuickFix, + diagnostics=[diagnostic], + edit=types.WorkspaceEdit(changes={params.text_document.uri: text_edits}), + ) + code_actions.append(code_action) + + return code_actions if code_actions else None + def list_of_models_for_rendering(self) -> t.List[ModelForRendering]: """Get a list of models for rendering. @@ -186,3 +279,68 @@ def get_completions( from sqlmesh.lsp.completions import get_sql_completions return get_sql_completions(self, uri, file_content) + + @staticmethod + def diagnostics_to_lsp_diagnostics( + diagnostics: t.List[AnnotatedRuleViolation], + ) -> t.List[types.Diagnostic]: + """ + Converts a list of AnnotatedRuleViolations to a list of LSP diagnostics. It will remove duplicates based on the message and range. + """ + lsp_diagnostics = {} + for diagnostic in diagnostics: + lsp_diagnostic = LSPContext.diagnostic_to_lsp_diagnostic(diagnostic) + if lsp_diagnostic is not None: + # Create a unique key combining message and range + diagnostic_key = ( + lsp_diagnostic.message, + lsp_diagnostic.range.start.line, + lsp_diagnostic.range.start.character, + lsp_diagnostic.range.end.line, + lsp_diagnostic.range.end.character, + ) + if diagnostic_key not in lsp_diagnostics: + lsp_diagnostics[diagnostic_key] = lsp_diagnostic + return list(lsp_diagnostics.values()) + + @staticmethod + def diagnostic_to_lsp_diagnostic( + diagnostic: AnnotatedRuleViolation, + ) -> t.Optional[types.Diagnostic]: + if diagnostic.model._path is None: + return None + if not diagnostic.violation_range: + with open(diagnostic.model._path, "r", encoding="utf-8") as file: + lines = file.readlines() + diagnostic_range = types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=len(lines) - 1, character=len(lines[-1])), + ) + else: + diagnostic_range = types.Range( + start=types.Position( + line=diagnostic.violation_range.start.line, + character=diagnostic.violation_range.start.character, + ), + end=types.Position( + line=diagnostic.violation_range.end.line, + character=diagnostic.violation_range.end.character, + ), + ) + + # Get rule definition location for diagnostics link + rule_location = diagnostic.rule.get_definition_location() + rule_uri_wihout_extension = URI.from_path(rule_location.file_path) + rule_uri = f"{rule_uri_wihout_extension.value}#L{rule_location.start_line}" + + # Use URI format to create a link for "related information" + return types.Diagnostic( + range=diagnostic_range, + message=diagnostic.violation_msg, + severity=types.DiagnosticSeverity.Error + if diagnostic.violation_type == "error" + else types.DiagnosticSeverity.Warning, + source="sqlmesh", + code=diagnostic.rule.name, + code_description=types.CodeDescription(href=rule_uri), + ) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index bbb0f77242..0082c4a911 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -8,11 +8,13 @@ import urllib.parse from lsprotocol import types +from lsprotocol.types import ( + WorkspaceDiagnosticRefreshRequest, + WorkspaceInlayHintRefreshRequest, +) from pygls.server import LanguageServer - from sqlmesh._version import __version__ from sqlmesh.core.context import Context -from sqlmesh.core.linter.definition import AnnotatedRuleViolation from sqlmesh.lsp.api import ( API_FEATURE, ApiRequest, @@ -216,6 +218,79 @@ def _custom_supported_methods( ] ) + def _reload_context_and_publish_diagnostics( + self, ls: LanguageServer, uri: URI, document_uri: str + ) -> None: + """Helper method to reload context and publish diagnostics.""" + if isinstance(self.context_state, NoContext): + return + + if isinstance(self.context_state, ContextFailed): + if self.context_state.context: + try: + self.context_state.context.load() + # Creating a new LSPContext will naturally create fresh caches + self.context_state = ContextLoaded( + lsp_context=LSPContext(self.context_state.context) + ) + except Exception as e: + ls.log_trace(f"Error loading context: {e}") + if not isinstance(self.context_state, ContextFailed): + raise Exception("Context state should be failed") + self.context_state = ContextFailed( + error_message=str(e), context=self.context_state.context + ) + return + else: + # If there's no context, try to create one from scratch + try: + self._ensure_context_for_document(uri) + # If successful, context_state will be ContextLoaded + if isinstance(self.context_state, ContextLoaded): + ls.show_message( + "Successfully loaded SQLMesh context", + types.MessageType.Info, + ) + except Exception as e: + ls.log_trace(f"Still cannot load context: {e}") + return + + # Reload the context if it was successfully loaded + try: + context = self.context_state.lsp_context.context + context.load() + # Create new LSPContext which will have fresh, empty caches + self.context_state = ContextLoaded(lsp_context=LSPContext(context)) + except Exception as e: + ls.log_trace(f"Error loading context: {e}") + self.context_state = ContextFailed( + error_message=str(e), context=self.context_state.lsp_context.context + ) + return + + # Send a workspace diagnostic refresh request to the client. This is used to notify the client that the diagnostics have changed. + ls.lsp.send_request( + types.WORKSPACE_DIAGNOSTIC_REFRESH, + WorkspaceDiagnosticRefreshRequest( + id=self.context_state.lsp_context.version_id, + ), + ) + + ls.lsp.send_request( + types.WORKSPACE_INLAY_HINT_REFRESH, + WorkspaceInlayHintRefreshRequest( + id=self.context_state.lsp_context.version_id, + ), + ) + + # Only publish diagnostics if client doesn't support pull diagnostics + if not self.client_supports_pull_diagnostics: + diagnostics = self.context_state.lsp_context.lint_model(uri) + ls.publish_diagnostics( + document_uri, + LSPContext.diagnostics_to_lsp_diagnostics(diagnostics), + ) + def _register_features(self) -> None: """Register LSP features on the internal LanguageServer instance.""" for name, method in self._supported_custom_methods.items(): @@ -278,63 +353,13 @@ def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> Non diagnostics = context.lint_model(uri) ls.publish_diagnostics( params.text_document.uri, - SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), + LSPContext.diagnostics_to_lsp_diagnostics(diagnostics), ) @self.server.feature(types.TEXT_DOCUMENT_DID_SAVE) def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> None: uri = URI(params.text_document.uri) - if isinstance(self.context_state, NoContext): - return - - if isinstance(self.context_state, ContextFailed): - if self.context_state.context: - try: - self.context_state.context.load() - self.context_state = ContextLoaded( - lsp_context=LSPContext(self.context_state.context) - ) - except Exception as e: - ls.log_trace(f"Error loading context: {e}") - if not isinstance(self.context_state, ContextFailed): - raise Exception("Context state should be failed") - self.context_state = ContextFailed( - error_message=str(e), context=self.context_state.context - ) - return - else: - # If there's no context, try to create one from scratch - try: - self._ensure_context_for_document(uri) - # If successful, context_state will be ContextLoaded - if isinstance(self.context_state, ContextLoaded): - ls.show_message( - "Successfully loaded SQLMesh context", - types.MessageType.Info, - ) - except Exception as e: - ls.log_trace(f"Still cannot load context: {e}") - return - - # Reload the context if was successfully - try: - context = self.context_state.lsp_context.context - context.load() - self.context_state = ContextLoaded(lsp_context=LSPContext(context)) - except Exception as e: - ls.log_trace(f"Error loading context: {e}") - self.context_state = ContextFailed( - error_message=str(e), context=self.context_state.lsp_context.context - ) - return - - # Only publish diagnostics if client doesn't support pull diagnostics - if not self.client_supports_pull_diagnostics: - diagnostics = self.context_state.lsp_context.lint_model(uri) - ls.publish_diagnostics( - params.text_document.uri, - SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), - ) + self._reload_context_and_publish_diagnostics(ls, uri, params.text_document.uri) @self.server.feature(types.TEXT_DOCUMENT_FORMATTING) def formatting( @@ -630,6 +655,21 @@ def workspace_diagnostic( ) return types.WorkspaceDiagnosticReport(items=[]) + @self.server.feature(types.TEXT_DOCUMENT_CODE_ACTION) + def code_action( + ls: LanguageServer, params: types.CodeActionParams + ) -> t.Optional[t.List[t.Union[types.Command, types.CodeAction]]]: + try: + ls.log_trace(f"Codeactionrequest: {params}") + uri = URI(params.text_document.uri) + context = self._context_get_or_load(uri) + code_actions = context.get_code_actions(uri, params) + return code_actions + + except Exception as e: + ls.log_trace(f"Error getting code actions: {e}") + return None + @self.server.feature( types.TEXT_DOCUMENT_COMPLETION, types.CompletionOptions(trigger_characters=["@"]), # advertise "@" for macros @@ -718,7 +758,7 @@ def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic] try: context = self._context_get_or_load(uri) diagnostics = context.lint_model(uri) - return SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), 0 + return LSPContext.diagnostics_to_lsp_diagnostics(diagnostics), 0 except Exception: return [], 0 @@ -829,71 +869,6 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: self.context_state = ContextFailed(error_message=str(e), context=context) return None - @staticmethod - def _diagnostic_to_lsp_diagnostic( - diagnostic: AnnotatedRuleViolation, - ) -> t.Optional[types.Diagnostic]: - if diagnostic.model._path is None: - return None - if not diagnostic.violation_range: - with open(diagnostic.model._path, "r", encoding="utf-8") as file: - lines = file.readlines() - range = types.Range( - start=types.Position(line=0, character=0), - end=types.Position(line=len(lines) - 1, character=len(lines[-1])), - ) - else: - range = types.Range( - start=types.Position( - line=diagnostic.violation_range.start.line, - character=diagnostic.violation_range.start.character, - ), - end=types.Position( - line=diagnostic.violation_range.end.line, - character=diagnostic.violation_range.end.character, - ), - ) - - # Get rule definition location for diagnostics link - rule_location = diagnostic.rule.get_definition_location() - rule_uri_wihout_extension = URI.from_path(rule_location.file_path) - rule_uri = f"{rule_uri_wihout_extension.value}#L{rule_location.start_line}" - - # Use URI format to create a link for "related information" - return types.Diagnostic( - range=range, - message=diagnostic.violation_msg, - severity=types.DiagnosticSeverity.Error - if diagnostic.violation_type == "error" - else types.DiagnosticSeverity.Warning, - source="sqlmesh", - code=diagnostic.rule.name, - code_description=types.CodeDescription(href=rule_uri), - ) - - @staticmethod - def _diagnostics_to_lsp_diagnostics( - diagnostics: t.List[AnnotatedRuleViolation], - ) -> t.List[types.Diagnostic]: - """ - Converts a list of AnnotatedRuleViolations to a list of LSP diagnostics. It will remove duplicates based on the message and range. - """ - lsp_diagnostics = {} - for diagnostic in diagnostics: - lsp_diagnostic = SQLMeshLanguageServer._diagnostic_to_lsp_diagnostic(diagnostic) - if lsp_diagnostic is not None: - # Create a unique key combining message and range - diagnostic_key = ( - lsp_diagnostic.message, - lsp_diagnostic.range.start.line, - lsp_diagnostic.range.start.character, - lsp_diagnostic.range.end.line, - lsp_diagnostic.range.end.character, - ) - if diagnostic_key not in lsp_diagnostics: - lsp_diagnostics[diagnostic_key] = lsp_diagnostic - return list(lsp_diagnostics.values()) - @staticmethod def _uri_to_path(uri: str) -> Path: """Convert a URI to a path.""" diff --git a/tests/lsp/test_code_actions.py b/tests/lsp/test_code_actions.py new file mode 100644 index 0000000000..b2f30feb47 --- /dev/null +++ b/tests/lsp/test_code_actions.py @@ -0,0 +1,111 @@ +import typing as t +from lsprotocol import types +from sqlmesh.core.context import Context +from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.uri import URI + + +def test_code_actions_with_linting(copy_to_temp_path: t.Callable): + """Test that code actions are generated for linting violations.""" + + # Copy sushi example to a temporary directory + sushi_paths = copy_to_temp_path("examples/sushi") + sushi_path = sushi_paths[0] + + # Override the config and turn the linter on + config_path = sushi_path / "config.py" + with config_path.open("r") as f: + lines = f.readlines() + lines = [ + line.replace("enabled=False,", "enabled=True,") if "enabled=False," in line else line + for line in lines + ] + with config_path.open("w") as f: + f.writelines(lines) + + # Override the latest_order.sql file to introduce a linter violation + model_content = """MODEL ( + name sushi.latest_order, + kind CUSTOM ( + materialization 'custom_full_with_custom_kind', + materialization_properties ( + custom_property = 'sushi!!!' + ) + ), + cron '@daily' +); + +SELECT * +FROM sushi.orders +ORDER BY event_date DESC LIMIT 1 +""" + latest_order_path = sushi_path / "models" / "latest_order.sql" + with latest_order_path.open("w") as f: + f.write(model_content) + + # Create context with the mocked config + context = Context(paths=[str(sushi_path)]) + + # Create LSP context + lsp_context = LSPContext(context) + + # Get diagnostics (linting violations) + violations = lsp_context.lint_model(URI.from_path(sushi_path / "models" / "latest_order.sql")) + + uri = URI.from_path(sushi_path / "models" / "latest_order.sql") + + # First, convert violations to LSP diagnostics + diagnostics = [] + for violation in violations: + if violation.violation_range: + diagnostic = types.Diagnostic( + range=types.Range( + start=types.Position( + line=violation.violation_range.start.line, + character=violation.violation_range.start.character, + ), + end=types.Position( + line=violation.violation_range.end.line, + character=violation.violation_range.end.character, + ), + ), + message=violation.violation_msg, + severity=types.DiagnosticSeverity.Warning, + ) + diagnostics.append(diagnostic) + + # Create code action params with diagnostics + params = types.CodeActionParams( + text_document=types.TextDocumentIdentifier(uri=uri.value), + range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=100, character=0), + ), + context=types.CodeActionContext(diagnostics=diagnostics), + ) + + # Get code actions + code_actions = lsp_context.get_code_actions( + URI.from_path(sushi_path / "models" / "latest_order.sql"), params + ) + + # Verify we have code actions + assert code_actions is not None + assert len(code_actions) > 0 + + # Verify the code action properties + first_action = code_actions[0] + if not isinstance(first_action, types.CodeAction): + raise AssertionError("First action is not a CodeAction instance") + assert first_action.kind == types.CodeActionKind.QuickFix + assert first_action.edit is not None + assert first_action.edit.changes is not None + assert ( + URI.from_path(sushi_path / "models" / "latest_order.sql").value in first_action.edit.changes + ) + + # The fix should replace SELECT * with specific columns + text_edits = first_action.edit.changes[ + URI.from_path(sushi_path / "models" / "latest_order.sql").value + ] + assert len(text_edits) > 0 From 2acf1a28e9e4e20e9565db13be7e78f98ac0fd43 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Wed, 25 Jun 2025 18:11:54 +0300 Subject: [PATCH 0456/1056] Fix: Push filter down to query for `get_expired_environments` (#4804) --- sqlmesh/core/state_sync/db/environment.py | 32 +++++++++++++---------- sqlmesh/core/state_sync/db/facade.py | 4 --- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/sqlmesh/core/state_sync/db/environment.py b/sqlmesh/core/state_sync/db/environment.py index b06d6160cc..b7e8128a93 100644 --- a/sqlmesh/core/state_sync/db/environment.py +++ b/sqlmesh/core/state_sync/db/environment.py @@ -172,13 +172,9 @@ def get_expired_environments(self, current_ts: int) -> t.List[EnvironmentSummary Returns: The list of environment summaries to remove. """ - - environment_summaries = self.get_environments_summary() - return [ - env_summary - for env_summary in environment_summaries - if env_summary.expiration_ts is not None and env_summary.expiration_ts <= current_ts - ] + return self._fetch_environment_summaries( + where=self._create_expiration_filter_expr(current_ts) + ) def delete_expired_environments( self, current_ts: t.Optional[int] = None @@ -225,13 +221,7 @@ def get_environments_summary(self) -> t.List[EnvironmentSummary]: Returns: A list of all environment summaries. """ - return [ - self._environment_summmary_from_row(row) - for row in fetchall( - self.engine_adapter, - self._environments_query(required_fields=list(EnvironmentSummary.all_fields())), - ) - ] + return self._fetch_environment_summaries() def get_environment( self, environment: str, lock_for_update: bool = False @@ -327,6 +317,20 @@ def _create_expiration_filter_expr(self, current_ts: int) -> exp.Expression: expression=exp.Literal.number(current_ts), ) + def _fetch_environment_summaries( + self, where: t.Optional[str | exp.Expression] = None + ) -> t.List[EnvironmentSummary]: + return [ + self._environment_summmary_from_row(row) + for row in fetchall( + self.engine_adapter, + self._environments_query( + where=where, + required_fields=list(EnvironmentSummary.all_fields()), + ), + ) + ] + def _environment_to_df(environment: Environment) -> pd.DataFrame: import pandas as pd diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 2a27c5fd92..c0d44893c4 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -69,10 +69,6 @@ T = t.TypeVar("T") -if t.TYPE_CHECKING: - pass - - class EngineAdapterStateSync(StateSync): """Manages state of nodes and snapshot with an existing engine adapter. From 3b4128a1d539289cf2139fa3c086614010ad8f42 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 25 Jun 2025 18:19:27 +0100 Subject: [PATCH 0457/1056] chore: add branded model string types (#4808) --- vscode/bus/src/brand.ts | 19 ++++++++ .../src/components/graph/ModelColumns.tsx | 3 +- vscode/react/src/components/graph/help.ts | 13 +++--- vscode/react/src/domain/lineage.ts | 5 ++- vscode/react/src/domain/models.ts | 43 +++++++++++++++++++ vscode/react/src/domain/sqlmesh-model.ts | 40 ++++++++++++----- vscode/react/src/pages/lineage.tsx | 14 +++++- vscode/react/src/workers/lineage.ts | 5 ++- 8 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 vscode/bus/src/brand.ts create mode 100644 vscode/react/src/domain/models.ts diff --git a/vscode/bus/src/brand.ts b/vscode/bus/src/brand.ts new file mode 100644 index 0000000000..2b9c3ca37a --- /dev/null +++ b/vscode/bus/src/brand.ts @@ -0,0 +1,19 @@ +declare const __brand: unique symbol +type Brand = { [__brand]: B } + +/** + * Branded is a type that adds a brand to a type. It is a type that is used to + * ensure that the type is unique and that it is not possible to mix up types + * with the same brand. + * + * @example + * + * type UserId = Branded + * type UserName = Branded + * + * const userId = '123' as UserId + * const userName = 'John Doe' as UserName + * + * userId == userName -> compile error + */ +export type Branded = T & Brand diff --git a/vscode/react/src/components/graph/ModelColumns.tsx b/vscode/react/src/components/graph/ModelColumns.tsx index 694a7dd9b4..8763beec61 100644 --- a/vscode/react/src/components/graph/ModelColumns.tsx +++ b/vscode/react/src/components/graph/ModelColumns.tsx @@ -46,6 +46,7 @@ import { Popover, Transition } from '@headlessui/react' import { useApiColumnLineage } from '@/api/index' import SourceList from '@/components/sourceList/SourceList' import type { Lineage } from '@/domain/lineage' +import type { ModelName } from '@/domain/models' export default function ModelColumns({ nodeId, @@ -732,5 +733,5 @@ function getColumnFromLineage( nodeId: string, columnName: string, ): LineageColumn | undefined { - return lineage?.[nodeId]?.columns?.[encodeURI(columnName)] + return lineage?.[nodeId]?.columns?.[encodeURI(columnName) as ModelName] } diff --git a/vscode/react/src/components/graph/help.ts b/vscode/react/src/components/graph/help.ts index 1dd7d07621..0e6b4b74d8 100644 --- a/vscode/react/src/components/graph/help.ts +++ b/vscode/react/src/components/graph/help.ts @@ -17,6 +17,7 @@ import { } from './ModelNode' import type { Lineage } from '@/domain/lineage' import type { ConnectedNode } from '@/workers/lineage' +import type { ModelEncodedFQN, ModelName } from '@/domain/models' export interface GraphNodeData { label: string @@ -108,7 +109,7 @@ function getEdges(lineage: Record = {}): Edge[] { }) for (const targetColumnName in targetModel.columns) { - const sourceModel = targetModel.columns[targetColumnName] + const sourceModel = targetModel.columns[targetColumnName as ModelName] if (isNil(sourceModel) || isNil(sourceModel.models)) continue @@ -210,7 +211,7 @@ function getNodeMap({ node.targetPosition = Position.Left } - if (sources.has(node.id)) { + if (sources.has(node.id as ModelEncodedFQN)) { node.sourcePosition = Position.Right } @@ -343,7 +344,7 @@ function mergeLineageWithColumns( } // New Column Lineage delivers fresh data, so we can just assign it - currentLineageModel.columns[targetColumnNameEncoded] = { + currentLineageModel.columns[targetColumnNameEncoded as ModelName] = { expression: newLineageModelColumn.expression, source: newLineageModelColumn.source, models: {}, @@ -353,7 +354,7 @@ function mergeLineageWithColumns( if (isObjectEmpty(newLineageModelColumn.models)) continue const currentLineageModelColumn = - currentLineageModel.columns[targetColumnNameEncoded]! + currentLineageModel.columns[targetColumnNameEncoded as ModelName]! const currentLineageModelColumnModels = currentLineageModelColumn.models for (const sourceColumnName in newLineageModelColumn.models) { @@ -371,7 +372,7 @@ function mergeLineageWithColumns( newLineageModelColumnModel, ), ), - ).map(encodeURI) + ).map((uri: string) => encodeURI(uri)) } } } @@ -481,7 +482,7 @@ function getLineageIndex(lineage: Record = {}): string { if (isNotNil(columns)) { Object.keys(columns).forEach(columnName => { - const column = columns[columnName] + const column = columns[columnName as ModelName] if (isNotNil(column) && isNotNil(column.models)) { Object.keys(column.models).forEach(m => allModels.add(m)) diff --git a/vscode/react/src/domain/lineage.ts b/vscode/react/src/domain/lineage.ts index 1b599f7b28..e9d798a361 100644 --- a/vscode/react/src/domain/lineage.ts +++ b/vscode/react/src/domain/lineage.ts @@ -1,6 +1,7 @@ import { type LineageColumn } from '@/api/client' +import type { ModelEncodedFQN, ModelName } from '@/domain/models' export interface Lineage { - models: string[] - columns?: Record + models: ModelEncodedFQN[] + columns?: Record } diff --git a/vscode/react/src/domain/models.ts b/vscode/react/src/domain/models.ts new file mode 100644 index 0000000000..9b4ab04153 --- /dev/null +++ b/vscode/react/src/domain/models.ts @@ -0,0 +1,43 @@ +import type { Branded } from '@bus/brand' + +/** + * ModelName is a type that represents the name of a model. + */ +export type ModelName = Branded + +/** + * ModelEncodedName is a type that represents the encoded name of a model. + */ +export type ModelEncodedName = Branded + +/** + * ModelFQN is a type that represents the fully qualified name of a model. + */ +export type ModelFQN = Branded + +/** + * ModelEncodedFQN is a type that represents the encoded fully qualified name of a model. + */ +export type ModelEncodedFQN = Branded + +/** + * ModelURI is a type that represents the URI of a model. + */ +export type ModelURI = Branded + +/** + * ModelEncodedURI is a type that represents the encoded URI of a model. + */ +export type ModelEncodedURI = Branded + +/** + * ModelPath is a type that represents the path of a model. + * A model path is relative to the project root. + */ +export type ModelPath = Branded + +/** + * ModelFullPath is a type that represents the full path of a model. + * A model full path is a fully qualified path to a model. + */ +export type ModelFullPath = Branded diff --git a/vscode/react/src/domain/sqlmesh-model.ts b/vscode/react/src/domain/sqlmesh-model.ts index 8712c3cee8..40b1b05aa8 100644 --- a/vscode/react/src/domain/sqlmesh-model.ts +++ b/vscode/react/src/domain/sqlmesh-model.ts @@ -8,12 +8,24 @@ import { type ModelDefaultCatalog, type ModelDefinition, } from '@/api/client' +import type { + ModelEncodedFQN, + ModelName, + ModelPath, + ModelEncodedName, + ModelFullPath, +} from '@/domain/models' import { isArrayNotEmpty } from '@/utils/index' import { ModelInitial } from './initial' import type { Lineage } from './lineage' -export interface InitialSQLMeshModel extends Model { - lineage?: Record +export interface InitialSQLMeshModel + extends Omit { + name: ModelName + fqn: ModelEncodedFQN + path: ModelPath + full_path: ModelFullPath + lineage?: Record } export class ModelSQLMeshModel< @@ -22,9 +34,10 @@ export class ModelSQLMeshModel< _details: ModelDetails = {} _detailsIndex: string = '' - name: string - fqn: string - path: string + name: ModelEncodedName + fqn: ModelEncodedFQN + path: ModelPath + full_path: ModelFullPath dialect: string type: ModelType columns: Column[] @@ -46,10 +59,11 @@ export class ModelSQLMeshModel< }, ) - this.name = encodeURI(this.initial.name) - this.fqn = encodeURI(this.initial.fqn) + this.name = encodeURI(this.initial.name) as ModelEncodedName + this.fqn = encodeURI(this.initial.fqn) as ModelEncodedFQN this.default_catalog = this.initial.default_catalog - this.path = this.initial.path + this.path = this.initial.path as ModelPath + this.full_path = this.initial.full_path as ModelFullPath this.dialect = this.initial.dialect this.description = this.initial.description this.sql = this.initial.sql @@ -127,17 +141,21 @@ export class ModelSQLMeshModel< } else if (key === 'details') { this.details = value as ModelDetails } else if (key === 'name') { - this.name = encodeURI(value as string) + this.name = encodeURI(value as string) as ModelEncodedName } else if (key === 'fqn') { - this.fqn = encodeURI(value as string) + this.fqn = encodeURI(value as string) as ModelEncodedFQN } else if (key === 'type') { this.type = value as ModelType } else if (key === 'default_catalog') { this.default_catalog = value as ModelDefaultCatalog } else if (key === 'description') { this.description = value as ModelDescription + } else if (key === 'full_path') { + this.full_path = value as ModelFullPath + } else if (key === 'path') { + this.path = value as ModelPath } else if (key in this) { - this[key as 'path' | 'dialect' | 'sql'] = value as string + this[key as 'dialect' | 'sql'] = value as string } } } diff --git a/vscode/react/src/pages/lineage.tsx b/vscode/react/src/pages/lineage.tsx index 2aef1e6525..d2d4e85858 100644 --- a/vscode/react/src/pages/lineage.tsx +++ b/vscode/react/src/pages/lineage.tsx @@ -16,6 +16,12 @@ import type { VSCodeEvent } from '@bus/callbacks' import { URI } from 'vscode-uri' import type { Model } from '@/api/client' import { useRpc } from '@/utils/rpc' +import type { + ModelEncodedFQN, + ModelName, + ModelPath, + ModelFullPath, +} from '@/domain/models' export function LineagePage() { const { emit } = useEventBus() @@ -198,7 +204,13 @@ export function LineageComponentFromWeb({ } const sqlmModel = new ModelSQLMeshModel() - sqlmModel.update(model) + sqlmModel.update({ + ...model, + name: model.name as ModelName, + fqn: model.fqn as ModelEncodedFQN, + path: model.path as ModelPath, + full_path: model.full_path as ModelFullPath, + }) return (

diff --git a/vscode/react/src/workers/lineage.ts b/vscode/react/src/workers/lineage.ts index 3a40b90708..89fc8614eb 100644 --- a/vscode/react/src/workers/lineage.ts +++ b/vscode/react/src/workers/lineage.ts @@ -1,5 +1,6 @@ import { isFalse, isNil, isStringEmptyOrNil, toID } from '@/utils/index' import { type Lineage } from '@/domain/lineage' +import type { ModelEncodedFQN } from '@/domain/models' export interface ConnectedNode { id?: string @@ -47,7 +48,7 @@ async function mergeLineageWithModels( key = encodeURI(key) acc[key] = { - models: models.map(encodeURI), + models: models.map(encodeURI) as ModelEncodedFQN[], columns: currentLineage?.[key]?.columns ?? undefined, } @@ -88,7 +89,7 @@ function getConnectedNodes( if (isDownstream) { models = Object.keys(lineage).filter(key => - lineage[key]!.models.includes(node), + lineage[key]!.models.includes(node as ModelEncodedFQN), ) } else { models = lineage[node]?.models ?? [] From af2fb95f5cc80cdba1f94773273fd77695ee2aa2 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 25 Jun 2025 13:12:12 -0700 Subject: [PATCH 0458/1056] chore: add CLAUDE.md (#4817) --- CLAUDE.md | 351 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..23a72bd371 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,351 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +SQLMesh is a next-generation data transformation framework that enables: +- Virtual data environments for isolated development without warehouse costs +- Plan/apply workflow (like Terraform) for safe deployments +- Multi-dialect SQL support with automatic transpilation +- Incremental processing to run only necessary transformations +- Built-in testing and CI/CD integration + +**Requirements**: Python >= 3.9 (Note: Python 3.13+ is not yet supported) + +## Essential Commands + +### Environment setup +```bash +# Create and activate a Python virtual environment (Python >= 3.9, < 3.13) +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install development dependencies +make install-dev + +# Setup pre-commit hooks (important for code quality) +make install-pre-commit +``` + +### Common Development Tasks +```bash +# Run linters and formatters (ALWAYS run before committing) +make style + +# Fast tests for quick feedback during development +make fast-test + +# Slow tests for comprehensive coverage +make slow-test + +# Run specific test file +pytest tests/core/test_context.py -v + +# Run tests with specific marker +pytest -m "not slow and not docker" -v + +# Build package +make package + +# Serve documentation locally +make docs-serve +``` + +### Engine-Specific Testing +```bash +# DuckDB (default, no setup required) +make duckdb-test + +# Other engines require credentials/Docker +make snowflake-test # Needs SNOWFLAKE_* env vars +make bigquery-test # Needs GOOGLE_APPLICATION_CREDENTIALS +make databricks-test # Needs DATABRICKS_* env vars +``` + +### UI Development +```bash +# In web/client directory +pnpm run dev # Start development server +pnpm run build # Production build +pnpm run test # Run tests + +# Docker-based UI +make ui-up # Start UI in Docker +make ui-down # Stop UI +``` + +## Architecture Overview + +### Core Components + +**sqlmesh/core/context.py**: The main Context class orchestrates all SQLMesh operations. This is the entry point for understanding how models are loaded, plans are created, and executions happen. + +**sqlmesh/core/model/**: Model definitions and kinds (FULL, INCREMENTAL_BY_TIME_RANGE, SCD_TYPE_2, etc.). Each model kind has specific behaviors for how data is processed. + +**sqlmesh/core/snapshot/**: The versioning system. Snapshots are immutable versions of models identified by fingerprints. Understanding snapshots is crucial for how SQLMesh tracks changes. + +**sqlmesh/core/plan/**: Plan building and evaluation logic. Plans determine what changes need to be applied and in what order. + +**sqlmesh/core/engine_adapter/**: Database engine adapters provide a unified interface across 16+ SQL engines. Each adapter handles engine-specific SQL generation and execution. + +### Key Concepts + +1. **Virtual Environments**: Lightweight branches that share unchanged data between environments, reducing storage costs and deployment time. + +2. **Fingerprinting**: Models are versioned using content-based fingerprints. Any change to a model's logic creates a new version. + +3. **State Sync**: Manages metadata across different backends (can be stored in the data warehouse or external databases). + +4. **Intervals**: Time-based partitioning system for incremental models, tracking what data has been processed. + +## Testing Philosophy + +- Tests are marked with pytest markers: + - **Type markers**: `fast`, `slow`, `docker`, `remote`, `cicdonly`, `isolated`, `registry_isolation` + - **Domain markers**: `cli`, `dbt`, `github`, `jupyter`, `web` + - **Engine markers**: `engine`, `athena`, `bigquery`, `clickhouse`, `databricks`, `duckdb`, `motherduck`, `mssql`, `mysql`, `postgres`, `redshift`, `snowflake`, `spark`, `trino`, `risingwave` +- Default to `fast` tests during development +- Engine tests use real connections when available, mocks otherwise +- The `sushi` example project is used extensively in tests +- Use `DuckDBMetadata` helper for validating table metadata in tests +- Tests run in parallel by default (`pytest -n auto`) + +## Code Style Guidelines + +- Python: Black formatting, isort for imports, mypy for type checking, Ruff for linting +- TypeScript/React: ESLint + Prettier configuration +- SQL: SQLGlot handles parsing/formatting +- All style checks run via `make style` +- Pre-commit hooks enforce all style rules automatically +- Important: Some modules (duckdb, numpy, pandas) are banned at module level to prevent import-time side effects + +## Important Files + +- `sqlmesh/core/context.py`: Main orchestration class +- `examples/sushi/`: Reference implementation used in tests +- `web/server/main.py`: Web UI backend entry point +- `web/client/src/App.tsx`: Web UI frontend entry point +- `vscode/extension/src/extension.ts`: VSCode extension entry point + +## Common Pitfalls + +1. **Engine Tests**: Many tests require specific database credentials or Docker. Check test markers before running. + +2. **Path Handling**: Be careful with Windows paths - use `pathlib.Path` for cross-platform compatibility. + +3. **State Management**: Understanding the state sync mechanism is crucial for debugging environment issues. + +4. **Snapshot Versioning**: Changes to model logic create new versions - this is by design for safe deployments. + +5. **Module Imports**: Avoid importing duckdb, numpy, or pandas at module level - these are banned by Ruff to prevent long load times in cases where the libraries aren't used. + +## GitHub CI/CD Bot Architecture + +SQLMesh includes a GitHub CI/CD bot integration that automates data transformation workflows. The implementation is located in `sqlmesh/integrations/github/` and follows a clean architectural pattern. + +### Code Organization + +**Core Integration Files:** +- `sqlmesh/cicd/bot.py`: Main CLI entry point (`sqlmesh_cicd` command) +- `sqlmesh/integrations/github/cicd/controller.py`: Core bot orchestration logic +- `sqlmesh/integrations/github/cicd/command.py`: Individual command implementations +- `sqlmesh/integrations/github/cicd/config.py`: Configuration classes and validation + +### Architecture Pattern + +The bot follows a **Command Pattern** architecture: + +1. **CLI Layer** (`bot.py`): Handles argument parsing and delegates to controllers +2. **Controller Layer** (`controller.py`): Orchestrates workflow execution and manages state +3. **Command Layer** (`command.py`): Implements individual operations (test, deploy, plan, etc.) +4. **Configuration Layer** (`config.py`): Manages bot configuration and validation + +### Key Components + +**GitHubCICDController**: Main orchestrator that: +- Manages GitHub API interactions via PyGithub +- Coordinates workflow execution across different commands +- Handles error reporting through GitHub Check Runs +- Manages PR comment interactions and status updates + +**Command Implementations**: +- `run_tests()`: Executes unit tests with detailed reporting +- `update_pr_environment()`: Creates/updates virtual PR environments +- `gen_prod_plan()`: Generates production deployment plans +- `deploy_production()`: Handles production deployments +- `check_required_approvers()`: Validates approval requirements + +**Configuration Management**: +- Uses Pydantic models for type-safe configuration +- Supports both YAML config files and environment variables +- Validates bot settings and user permissions +- Handles approval workflows and deployment triggers + +### Integration with Core SQLMesh + +The bot leverages core SQLMesh components: +- **Context**: Uses SQLMesh Context for project operations +- **Plan/Apply**: Integrates with SQLMesh's plan generation and application +- **Virtual Environments**: Creates isolated PR environments using SQLMesh's virtual data environments +- **State Sync**: Manages metadata synchronization across environments +- **Testing Framework**: Executes SQLMesh unit tests and reports results + +### Error Handling and Reporting + +- **GitHub Check Runs**: Creates detailed status reports for each workflow step +- **PR Comments**: Provides user-friendly feedback on failures and successes +- **Structured Logging**: Uses SQLMesh's logging framework for debugging +- **Exception Handling**: Graceful handling of GitHub API failures and SQLMesh errors + +## Environment Variables for Engine Testing + +When running engine-specific tests, these environment variables are required: + +- **Snowflake**: `SNOWFLAKE_ACCOUNT`, `SNOWFLAKE_WAREHOUSE`, `SNOWFLAKE_DATABASE`, `SNOWFLAKE_USER`, `SNOWFLAKE_PASSWORD` +- **BigQuery**: `BIGQUERY_KEYFILE` or `GOOGLE_APPLICATION_CREDENTIALS` +- **Databricks**: `DATABRICKS_CATALOG`, `DATABRICKS_SERVER_HOSTNAME`, `DATABRICKS_HTTP_PATH`, `DATABRICKS_ACCESS_TOKEN`, `DATABRICKS_CONNECT_VERSION` +- **Redshift**: `REDSHIFT_HOST`, `REDSHIFT_USER`, `REDSHIFT_PASSWORD`, `REDSHIFT_DATABASE` +- **Athena**: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `ATHENA_S3_WAREHOUSE_LOCATION` +- **ClickHouse Cloud**: `CLICKHOUSE_CLOUD_HOST`, `CLICKHOUSE_CLOUD_USERNAME`, `CLICKHOUSE_CLOUD_PASSWORD` + +## Migrations System + +SQLMesh uses a migration system to evolve its internal state database schema and metadata format. The migrations handle changes to SQLMesh's internal structure, not user data transformations. + +### Migration Structure + +**Location**: `sqlmesh/migrations/` - Contains 80+ migration files from v0001 to v0083+ + +**Naming Convention**: `v{XXXX}_{descriptive_name}.py` (e.g., `v0001_init.py`, `v0083_use_sql_for_scd_time_data_type_data_hash.py`) + +**Core Infrastructure**: +- `sqlmesh/core/state_sync/db/migrator.py`: Main migration orchestrator +- `sqlmesh/utils/migration.py`: Cross-database compatibility utilities +- `sqlmesh/core/state_sync/base.py`: Auto-discovery and loading logic + +### Migration Categories + +**Schema Evolution**: +- State table creation/modification (snapshots, environments, intervals) +- Column additions/removals and index management +- Database engine compatibility fixes (MySQL/MSSQL field size limits) + +**Data Format Migrations**: +- JSON metadata structure updates (snapshot serialization changes) +- Path normalization (Windows compatibility) +- Fingerprint recalculation when SQLGlot parsing changes + +**Cleanup Operations**: +- Removing obsolete tables and unused data +- Metadata optimization and attribute cleanup + +### Key Migration Patterns + +```python +# Standard migration function signature +def migrate(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + # Migration logic here + +# Common operations +engine_adapter.create_state_table(table_name, columns_dict) +engine_adapter.alter_table(alter_expression) +engine_adapter.drop_table(table_name) +``` + +### State Management Integration + +**Core State Tables**: +- `_snapshots`: Model version metadata (most frequently migrated) +- `_environments`: Environment definitions +- `_versions`: Schema/SQLGlot/SQLMesh version tracking +- `_intervals`: Incremental processing metadata + +**Migration Safety**: +- Automatic backups before migration (unless `skip_backup=True`) +- Atomic database transactions for consistency +- Snapshot count validation before/after migrations +- Automatic rollback on failures + +### Migration Execution + +**Auto-Discovery**: Migrations are automatically loaded using `pkgutil.iter_modules()` + +**Triggers**: Migrations run automatically when: +- Schema version mismatch detected +- SQLGlot version changes require fingerprint recalculation +- Manual `sqlmesh migrate` command execution + +**Execution Flow**: +1. Version comparison (local vs remote schema) +2. Backup creation of state tables +3. Sequential migration execution (numerical order) +4. Snapshot fingerprint recalculation if needed +5. Environment updates with new snapshot references + +## dbt Integration + +SQLMesh provides native support for dbt projects, allowing users to run existing dbt projects while gaining access to SQLMesh's advanced features like virtual environments and plan/apply workflows. + +### Core dbt Integration + +**Location**: `sqlmesh/dbt/` - Complete dbt integration architecture + +**Key Components**: +- `sqlmesh/dbt/loader.py`: Main dbt project loader extending SQLMesh's base loader +- `sqlmesh/dbt/manifest.py`: dbt manifest parsing and project discovery +- `sqlmesh/dbt/adapter.py`: dbt adapter system for SQL execution and schema operations +- `sqlmesh/dbt/model.py`: dbt model configurations and materialization mapping +- `sqlmesh/dbt/context.py`: dbt project context and environment management + +### Project Conversion + +**dbt Converter**: `sqlmesh/dbt/converter/` - Tools for migrating dbt projects to SQLMesh + +**Key Features**: +- `convert.py`: Main conversion orchestration +- `jinja.py` & `jinja_transforms.py`: Jinja template and macro conversion +- Full support for dbt assets (models, seeds, sources, tests, snapshots, macros) + +**CLI Commands**: +```bash +# Initialize SQLMesh in existing dbt project +sqlmesh init -t dbt + +# Convert dbt project to SQLMesh format +sqlmesh dbt convert +``` + +### Supported dbt Features + +**Project Structure**: +- Full dbt project support (models, seeds, sources, tests, snapshots, macros) +- dbt package dependencies and version management +- Profile integration using existing `profiles.yml` for connections + +**Materializations**: +- All standard dbt materializations (table, view, incremental, ephemeral) +- Incremental model strategies (delete+insert, merge, insert_overwrite) +- SCD Type 2 support and snapshot strategies + +**Advanced Features**: +- Jinja templating with full macro support +- Runtime variable passing and configuration +- dbt test integration and execution +- Cross-database compatibility with SQLMesh's multi-dialect support + +### Example Projects + +**sushi_dbt**: `examples/sushi_dbt/` - Complete dbt project running with SQLMesh +**Test Fixtures**: `tests/fixtures/dbt/sushi_test/` - Comprehensive test dbt project with all asset types + +### Integration Benefits + +When using dbt with SQLMesh, you gain: +- **Virtual Environments**: Isolated development without warehouse costs +- **Plan/Apply Workflow**: Safe deployments with change previews +- **Multi-Dialect Support**: Run the same dbt project across different SQL engines +- **Advanced Testing**: Enhanced testing capabilities beyond standard dbt tests +- **State Management**: Sophisticated metadata and versioning system \ No newline at end of file From f3e53b78e24cf4882522d7d629da7572b90a0d35 Mon Sep 17 00:00:00 2001 From: Sung Won Chung Date: Wed, 25 Jun 2025 13:22:29 -0700 Subject: [PATCH 0459/1056] vscode-env-var-docs (#4803) Co-authored-by: Trey Spiller <1831878+treysp@users.noreply.github.com> Co-authored-by: Ben <9087625+benfdking@users.noreply.github.com> --- docs/guides/vscode.md | 30 +++++++++++++++++++---- docs/guides/vscode/loaded.png | Bin 0 -> 21650 bytes docs/guides/vscode/print_env_vars.png | Bin 0 -> 16382 bytes docs/guides/vscode/restart_servers.png | Bin 0 -> 12077 bytes docs/guides/vscode/terminal_env_vars.png | Bin 0 -> 48924 bytes 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 docs/guides/vscode/loaded.png create mode 100644 docs/guides/vscode/print_env_vars.png create mode 100644 docs/guides/vscode/restart_servers.png create mode 100644 docs/guides/vscode/terminal_env_vars.png diff --git a/docs/guides/vscode.md b/docs/guides/vscode.md index a71599c056..5edebe3be9 100644 --- a/docs/guides/vscode.md +++ b/docs/guides/vscode.md @@ -55,7 +55,7 @@ If you are using Tobiko Cloud, the `tcloud` library will install SQLMesh for you First, follow the [Python setup](#python-setup) steps above to create and activate a Python environment. Next, install `tcloud`: ```bash -pip install tcloud +pip install tcloud # always make sure to install the latest version of tcloud ``` Finally, add the `lsp` extra to your `tcloud.yml` configuration file, as described [here](../cloud/tcloud_getting_started.md#connect-tobiko-cloud-to-data-warehouse). @@ -155,11 +155,31 @@ The VSCode extension is based on a [language server](https://en.wikipedia.org/wi If you have environment variables that are needed by the context and the language server, you can use one of these approaches to pass variables to the language server: -- Open VSCode from a terminal that has the variables set -- Use environment variables pulled from somewhere else dynamically (e.g. a `.env` file) in your config -- Set the environment variables in the python environment that the extension uses. You can find detailed instructions [here](https://code.visualstudio.com/docs/python/environments#_environment-variables) +- Open VSCode from a terminal that has the variables set already. + - If you have `export ENV_VAR=value` in your shell configuration file (e.g. `.zshrc` or `.bashrc`) when initializing the terminal by default, the variables will be picked up by the language server if opened from that terminal. +- Use environment variables pulled from somewhere else dynamically in your `config.py` for example by connecting to a secret store +- By default, a `.env` file in your root project directory will automatically be picked up by the language server through the python environment that the extension uses. For exact details on how to set the environment variables in the Python environment that the extension uses, see [here](https://code.visualstudio.com/docs/python/environments#_environment-variables) -### Python environment woes +You can verify that the environment variables are being passed to the language server by printing them in your terminal. + +1. `Cmd +Shift + P` (`Ctrl + Shift + P` in case of Windows) to start the VSCode command bar + ![print_env_vars](./vscode/print_env_vars.png) +2. Select the option: `SQLMesh: Print Environment Variables` +3. You should see the environment variables printed in the terminal + ![terminal_env_vars](./vscode/terminal_env_vars.png) + +If you change your setup during development (e.g., add variables to your shell config), you must restart the language server for the changes to take effect. You can do this by running the following command in the terminal: + +1. `Cmd +Shift + P` (`Ctrl + Shift + P` in case of Windows) to start the VSCode command bar +2. Select the option: `SQLMesh: Restart Servers` + ![restart_servers](./vscode/restart_servers.png) + ![loaded](./vscode/loaded.png) + + > This loaded message will appear in the lower left corner of the VSCode window. + +3. Print the environment variables based on the instructions above to verify the changes have taken effect. + +### Python environment issues The most common problem is the extension not using the correct Python interpreter. diff --git a/docs/guides/vscode/loaded.png b/docs/guides/vscode/loaded.png new file mode 100644 index 0000000000000000000000000000000000000000..efc38522be67ed05a70dd8c76de05d0ed8bc3e55 GIT binary patch literal 21650 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 F_+C{ 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 Date: Wed, 25 Jun 2025 14:19:00 -0700 Subject: [PATCH 0460/1056] Chore(doc): Fix the 'table_name' command in the table migration guide (#4815) --- docs/guides/table_migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/table_migration.md b/docs/guides/table_migration.md index 5556986316..351a704ac3 100644 --- a/docs/guides/table_migration.md +++ b/docs/guides/table_migration.md @@ -131,7 +131,7 @@ Consider an existing table named `my_schema.existing_table`. Migrating this tabl c. Create the model in the SQLMesh project without backfilling any data by running `sqlmesh plan [environment name] --empty-backfill --start 2024-01-01`, replacing "[environment name]" with an environment name other than `prod` and using the same start date from the `MODEL` DDL in step 3b. -4. Determine the name of the model's snapshot physical table by running `sqlmesh table_name --env [environment name] my_schema.existing_table`. For example, it might return `sqlmesh__my_schema.existing_table_123456`. +4. Determine the name of the model's snapshot physical table by running `sqlmesh table_name --env [environment name] --prod my_schema.existing_table`. For example, it might return `sqlmesh__my_schema.existing_table_123456`. 5. Rename the original table `my_schema.existing_table_temp` to `sqlmesh__my_schema.existing_table_123456` The model would have code similar to: From 2663dc9031edefcb7f257dfa6462633301fef40a Mon Sep 17 00:00:00 2001 From: Christopher Giroir Date: Wed, 25 Jun 2025 16:25:40 -0700 Subject: [PATCH 0461/1056] fix: set default for append-newline flag to None (#4820) --- sqlmesh/cli/main.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 4f343c731f..f3c3afa46a 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -4,25 +4,26 @@ import os import sys import typing as t +from pathlib import Path import click + from sqlmesh import configure_logging, remove_excess_logs from sqlmesh.cli import error_handler from sqlmesh.cli import options as opt from sqlmesh.cli.project_init import ( + InitCliMode, ProjectTemplate, init_example_project, - InitCliMode, interactive_init, ) from sqlmesh.core.analytics import cli_analytics -from sqlmesh.core.console import configure_console, get_console -from sqlmesh.utils import Verbosity from sqlmesh.core.config import load_configs +from sqlmesh.core.console import configure_console, get_console from sqlmesh.core.context import Context +from sqlmesh.utils import Verbosity from sqlmesh.utils.date import TimeLike from sqlmesh.utils.errors import MissingDependencyError, SQLMeshError -from pathlib import Path logger = logging.getLogger(__name__) @@ -355,6 +356,7 @@ def evaluate( "--append-newline", is_flag=True, help="Include a newline at the end of each file.", + default=None, ) @opt.format_options @click.pass_context @@ -805,7 +807,11 @@ def check_intervals( context = ctx.obj context.console.show_intervals( context.check_intervals( - environment, no_signals=no_signals, select_models=select_model, start=start, end=end + environment, + no_signals=no_signals, + select_models=select_model, + start=start, + end=end, ) ) @@ -1107,7 +1113,10 @@ def clean(obj: Context) -> None: @error_handler @cli_analytics def table_name( - obj: Context, model_name: str, environment: t.Optional[str] = None, prod: bool = False + obj: Context, + model_name: str, + environment: t.Optional[str] = None, + prod: bool = False, ) -> None: """Prints the name of the physical table for the given model.""" print(obj.table_name(model_name, environment, prod)) From 2e0f6f1af69acbbd915e2d143e90b473ca93e8a9 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Thu, 26 Jun 2025 11:40:29 +1200 Subject: [PATCH 0462/1056] Fix: Prevent formatting=false throwing an error (#4819) --- sqlmesh/core/model/definition.py | 1 + tests/core/test_model.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index f42a3ebfdc..6ccd0927cd 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2886,6 +2886,7 @@ def render_expression( for name, args in values ) ), + "formatting": str, } diff --git a/tests/core/test_model.py b/tests/core/test_model.py index a1f9034481..8d16c9422b 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9973,9 +9973,9 @@ def test_formatting_flag_serde(): ) model = load_sql_based_model(expressions) + assert model.render_definition()[0].sql() == "MODEL (\nname test_model,\nformatting False\n)" model_json = model.json() - assert "formatting" not in json.loads(model_json) deserialized_model = SqlModel.parse_raw(model_json) From 15560fa8dbc094ceacfb94556124ba130838ad74 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 26 Jun 2025 09:20:24 +0100 Subject: [PATCH 0463/1056] feat(vscode): remove unused button (#4818) --- vscode/react/src/components/graph/ModelLineage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vscode/react/src/components/graph/ModelLineage.tsx b/vscode/react/src/components/graph/ModelLineage.tsx index b936dd0285..652c0631e4 100644 --- a/vscode/react/src/components/graph/ModelLineage.tsx +++ b/vscode/react/src/components/graph/ModelLineage.tsx @@ -383,7 +383,10 @@ function ModelColumnLineage(): JSX.Element { )} - + Date: Thu, 26 Jun 2025 10:52:47 +0100 Subject: [PATCH 0464/1056] fix(vscode): lineage on non model file (#4824) --- vscode/react/src/pages/lineage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vscode/react/src/pages/lineage.tsx b/vscode/react/src/pages/lineage.tsx index d2d4e85858..546e1ad4bf 100644 --- a/vscode/react/src/pages/lineage.tsx +++ b/vscode/react/src/pages/lineage.tsx @@ -110,6 +110,8 @@ function Lineage() { fetchFirstTimeModelIfNotSet(models).then(modelName => { if (modelName && selectedModel === undefined) { setSelectedModel(modelName) + } else { + setSelectedModel(models[0].name) } }) } From ca588803d1bdb53c206c7e5e7df8e264daea32da Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:08:26 +0100 Subject: [PATCH 0465/1056] chore: pin pygls to avoid issues (#4825) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index acee956e0b..235bbb8185 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -132,7 +132,7 @@ lsp = [ "sse-starlette>=0.2.2", "pyarrow", # For lsp - "pygls", + "pygls>=1.2.0,<2.0.0", "lsprotocol", ] risingwave = ["psycopg2"] From 4861ebea9e33891bbb370c2df3a08b932d2a4724 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 26 Jun 2025 16:25:20 +0100 Subject: [PATCH 0466/1056] chore: moving everything to node 20 (#4828) --- .circleci/config.yml | 4 ++-- .nvmrc | 1 + package.json | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .nvmrc diff --git a/.circleci/config.yml b/.circleci/config.yml index 37b03fbe95..7a12d3c07d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,7 +39,7 @@ jobs: command: unset TWINE_USERNAME TWINE_PASSWORD && make publish-tests gh-release: docker: - - image: cimg/node:16.14 + - image: cimg/node:20.19.0 resource_class: small steps: - run: @@ -54,7 +54,7 @@ jobs: ui-build: docker: - - image: cimg/node:19.8 + - image: cimg/node:20.19.0 resource_class: medium steps: - checkout diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..2edeafb09d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 \ No newline at end of file diff --git a/package.json b/package.json index d6383cfc5d..82be647222 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,8 @@ { + "engines": { + "node": ">=20.0.0", + "pnpm": ">=10.0.0" + }, "scripts": { "ci": "pnpm run lint && pnpm run -r ci", "fmt": "prettier --write .", From 570274c210440967383b99ec55184e2e549637da Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:07:07 -0500 Subject: [PATCH 0467/1056] Chore: Improve MSSQL merge performance (#4811) --- sqlmesh/core/engine_adapter/mssql.py | 87 ++++++++++++++++++++++++- tests/core/engine_adapter/test_mssql.py | 4 +- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index a00e11e0f7..200640f305 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -6,11 +6,13 @@ from sqlglot import exp -from sqlmesh.core.dialect import to_schema +from sqlmesh.core.dialect import to_schema, add_table from sqlmesh.core.engine_adapter.base import ( EngineAdapterWithIndexSupport, EngineAdapter, InsertOverwriteStrategy, + MERGE_SOURCE_ALIAS, + MERGE_TARGET_ALIAS, ) from sqlmesh.core.engine_adapter.mixins import ( GetCurrentCatalogFromFunctionMixin, @@ -32,7 +34,7 @@ if t.TYPE_CHECKING: from sqlmesh.core._typing import SchemaName, TableName - from sqlmesh.core.engine_adapter._typing import DF, Query + from sqlmesh.core.engine_adapter._typing import DF, Query, QueryOrDF @set_catalog() @@ -188,6 +190,87 @@ def drop_schema( ) super().drop_schema(schema_name, ignore_if_not_exists=ignore_if_not_exists, cascade=False) + def merge( + self, + target_table: TableName, + source_table: QueryOrDF, + columns_to_types: t.Optional[t.Dict[str, exp.DataType]], + unique_key: t.Sequence[exp.Expression], + when_matched: t.Optional[exp.Whens] = None, + merge_filter: t.Optional[exp.Expression] = None, + ) -> None: + source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( + source_table, columns_to_types, target_table=target_table + ) + columns_to_types = columns_to_types or self.columns(target_table) + on = exp.and_( + *( + add_table(part, MERGE_TARGET_ALIAS).eq(add_table(part, MERGE_SOURCE_ALIAS)) + for part in unique_key + ) + ) + if merge_filter: + on = exp.and_(merge_filter, on) + + if not when_matched: + match_condition = None + unique_key_names = [y.name for y in unique_key] + columns_to_types_no_keys = [c for c in columns_to_types if c not in unique_key_names] + + target_columns_no_keys = [ + exp.column(c, MERGE_TARGET_ALIAS) for c in columns_to_types_no_keys + ] + source_columns_no_keys = [ + exp.column(c, MERGE_SOURCE_ALIAS) for c in columns_to_types_no_keys + ] + + match_condition = exp.Exists( + this=exp.select(*target_columns_no_keys).except_( + exp.select(*source_columns_no_keys) + ) + ) + + match_expressions = [ + exp.When( + matched=True, + source=False, + condition=match_condition, + then=exp.Update( + expressions=[ + exp.column(col, MERGE_TARGET_ALIAS).eq( + exp.column(col, MERGE_SOURCE_ALIAS) + ) + for col in columns_to_types_no_keys + ], + ), + ) + ] + else: + match_expressions = when_matched.copy().expressions + + match_expressions.append( + exp.When( + matched=False, + source=False, + then=exp.Insert( + this=exp.Tuple(expressions=[exp.column(col) for col in columns_to_types]), + expression=exp.Tuple( + expressions=[ + exp.column(col, MERGE_SOURCE_ALIAS) for col in columns_to_types + ] + ), + ), + ) + ) + for source_query in source_queries: + with source_query as query: + self._merge( + target_table=target_table, + query=query, + on=on, + whens=exp.Whens(expressions=match_expressions), + ) + def _convert_df_datetime(self, df: DF, columns_to_types: t.Dict[str, exp.DataType]) -> None: import pandas as pd from pandas.api.types import is_datetime64_any_dtype # type: ignore diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index 22f371746c..a420a2d82c 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -472,7 +472,7 @@ def test_merge_pandas( assert to_sql_calls(adapter) == [ f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = '__temp_target_{temp_table_id}') EXEC('CREATE TABLE [__temp_target_{temp_table_id}] ([id] INTEGER, [ts] DATETIME2, [val] INTEGER)');""", - f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] WHEN MATCHED THEN UPDATE SET [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id], [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts], [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", + f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] WHEN MATCHED AND EXISTS(SELECT [__MERGE_TARGET__].[ts], [__MERGE_TARGET__].[val] EXCEPT SELECT [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]) THEN UPDATE SET [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts], [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", f"DROP TABLE IF EXISTS [__temp_target_{temp_table_id}];", ] @@ -495,7 +495,7 @@ def test_merge_pandas( assert to_sql_calls(adapter) == [ f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = '__temp_target_{temp_table_id}') EXEC('CREATE TABLE [__temp_target_{temp_table_id}] ([id] INTEGER, [ts] DATETIME2, [val] INTEGER)');""", - f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] AND [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts] WHEN MATCHED THEN UPDATE SET [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id], [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts], [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", + f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] AND [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts] WHEN MATCHED AND EXISTS(SELECT [__MERGE_TARGET__].[val] EXCEPT SELECT [__MERGE_SOURCE__].[val]) THEN UPDATE SET [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", f"DROP TABLE IF EXISTS [__temp_target_{temp_table_id}];", ] From 5f531bd03757ec4d5c5d05bb34e141edb9b0949a Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:50:09 +0100 Subject: [PATCH 0468/1056] ci: publish to open vsx (#4807) --- .github/workflows/release_extension.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release_extension.yaml b/.github/workflows/release_extension.yaml index 1807fee37d..a4106c1d81 100644 --- a/.github/workflows/release_extension.yaml +++ b/.github/workflows/release_extension.yaml @@ -28,11 +28,6 @@ jobs: working-directory: vscode/extension run: | npm version ${{ steps.extract_version.outputs.VERSION }} --no-git-tag-version - - name: Install dependencies - working-directory: vscode/extension - run: pnpm install - - name: Run CI - run: pnpm run ci - name: Build extension working-directory: vscode/extension run: pnpm run vscode:package @@ -42,3 +37,7 @@ jobs: pnpx vsce publish --packagePath sqlmesh-${{ steps.extract_version.outputs.VERSION }}.vsix env: VSCE_PAT: ${{ secrets.VSCE_PAT }} + - name: Upload extension to OpenVSX + working-directory: vscode/extension + run: | + pnpx ovsx publish -p ${{ secrets.OPEN_VSX_TOKEN }} sqlmesh-${{ steps.extract_version.outputs.VERSION }}.vsix From 1db845c4bda092041cf752867403ba2062a797fe Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:23:12 +0100 Subject: [PATCH 0469/1056] ci: test extension through code-server and in ci (#4827) --- .github/workflows/pr.yaml | 37 ++++- .nvmrc | 2 +- vscode/extension/package.json | 7 +- vscode/extension/playwright.config.ts | 2 +- vscode/extension/tests/stop.spec.ts | 50 +++--- vscode/extension/tests/utils_code_server.ts | 165 ++++++++++++++++++++ 6 files changed, 237 insertions(+), 26 deletions(-) create mode 100644 vscode/extension/tests/utils_code_server.ts diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 765aea043f..1d7f6ec8a3 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - uses: pnpm/action-setup@v4 with: version: latest @@ -25,3 +25,38 @@ jobs: run: pnpm install - name: Run CI run: pnpm run ci + test-vscode-e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - uses: pnpm/action-setup@v4 + with: + version: latest + - name: Install dependencies + run: pnpm install + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install python dependencies + run: | + python -m venv .venv + source .venv/bin/activate + make install-dev + - name: Fetch VS Code + working-directory: ./vscode/extension + run: pnpm run fetch-vscode + + - name: Install code-server + run: curl -fsSL https://code-server.dev/install.sh | sh + - name: Install Playwright browsers + working-directory: ./vscode/extension + run: pnpm exec playwright install + - name: Run e2e tests + working-directory: ./vscode/extension + run: | + source ../../.venv/bin/activate + pnpm run test:e2e tests/stop.spec.ts diff --git a/.nvmrc b/.nvmrc index 2edeafb09d..209e3ef4b6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 \ No newline at end of file +20 diff --git a/vscode/extension/package.json b/vscode/extension/package.json index cc5722c684..e8b7d5f00b 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -120,9 +120,10 @@ "lint": "eslint src", "lint:fix": "eslint src --fix", "test:unit": "vitest run", - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui", - "test:e2e:headed": "playwright test --headed", + "code-server": "code-server", + "test:e2e": "pnpm run vscode:package && playwright test", + "test:e2e:ui": "pnpm run vscode:package && playwright test --ui", + "test:e2e:headed": "pnpm run vscode:package && playwright test --headed", "fetch-vscode": "tsx scripts/fetch-vscode.ts", "compile": "pnpm run check-types && node esbuild.js", "check-types": "tsc --noEmit -p ./tsconfig.build.json", diff --git a/vscode/extension/playwright.config.ts b/vscode/extension/playwright.config.ts index 6820f2c8b1..599e5e3738 100644 --- a/vscode/extension/playwright.config.ts +++ b/vscode/extension/playwright.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ use: { // ⭢ we'll launch Electron ourselves – no browser needed browserName: 'chromium', - headless: false, // headed makes screenshots deterministic + headless: true, // headless mode for tests launchOptions: { slowMo: process.env.CI ? 0 : 100, }, diff --git a/vscode/extension/tests/stop.spec.ts b/vscode/extension/tests/stop.spec.ts index 61422991cc..d7b2d08ccd 100644 --- a/vscode/extension/tests/stop.spec.ts +++ b/vscode/extension/tests/stop.spec.ts @@ -1,57 +1,67 @@ import path from 'path' -import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { SUSHI_SOURCE_PATH } from './utils' import os from 'os' import { test } from '@playwright/test' import fs from 'fs-extra' +import { startCodeServer, stopCodeServer } from './utils_code_server' -test('Stop server works', async () => { +test('Stop server works', async ({ page }) => { + test.setTimeout(120000) // Increase timeout to 2 minutes + + console.log('Starting test: Stop server works') const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer(tempDir, true) + try { - const { window, close } = await startVSCode(tempDir) + // Navigate to code-server instance + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + // Wait for code-server to load + await page.waitForLoadState('networkidle') + await page.waitForSelector('[role="application"]', { timeout: 10000 }) - // Wait for the models folder to be visible - await window.waitForSelector('text=models') + // Wait for the models folder to be visible in the file explorer + await page.waitForSelector('text=models') // Click on the models folder, excluding external_models - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() - // Open the customer_revenue_lifetime model - await window + // Open the customers.sql model + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') // Stop the server - await window.keyboard.press( + await page.keyboard.press( process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', ) - await window.keyboard.type('SQLMesh: Stop Server') - await window.keyboard.press('Enter') + await page.keyboard.type('SQLMesh: Stop Server') + await page.keyboard.press('Enter') // Await LSP server stopped message - await window.waitForSelector('text=LSP server stopped') + await page.waitForSelector('text=LSP server stopped') // Render the model - await window.keyboard.press( + await page.keyboard.press( process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', ) - await window.keyboard.type('Render Model') - await window.keyboard.press('Enter') + await page.keyboard.type('Render Model') + await page.keyboard.press('Enter') // Await error message - await window.waitForSelector( + await page.waitForSelector( 'text="Failed to render model: LSP client not ready."', ) - await close() } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) diff --git a/vscode/extension/tests/utils_code_server.ts b/vscode/extension/tests/utils_code_server.ts new file mode 100644 index 0000000000..c38fee615c --- /dev/null +++ b/vscode/extension/tests/utils_code_server.ts @@ -0,0 +1,165 @@ +import { spawn, ChildProcess, execSync } from 'child_process' +import path from 'path' +import fs from 'fs-extra' + +export interface CodeServerContext { + codeServerProcess: ChildProcess + codeServerPort: number + tempDir: string +} + +/** + * @param tempDir - The temporary directory to use for the code-server instance + * @param placeFileWithPythonInterpreter - Whether to place a vscode/settings.json file in the temp directory that points to the python interpreter of the environmen the test is running in. + * @returns The code-server context + */ +export async function startCodeServer( + tempDir: string, + placeFileWithPythonInterpreter: boolean = false, +): Promise { + // Find an available port + const codeServerPort = Math.floor(Math.random() * 10000) + 50000 + + // Create .vscode/settings.json with Python interpreter if requested + if (placeFileWithPythonInterpreter) { + const vscodeDir = path.join(tempDir, '.vscode') + await fs.ensureDir(vscodeDir) + + // Get the current Python interpreter path + const pythonPath = execSync('which python', { + encoding: 'utf-8', + }).trim() + + const settings = { + 'python.defaultInterpreterPath': path.join( + __dirname, + '..', + '..', + '..', + '.venv', + 'bin', + 'python', + ), + } + + await fs.writeJson(path.join(vscodeDir, 'settings.json'), settings, { + spaces: 2, + }) + console.log( + `Created .vscode/settings.json with Python interpreter: ${pythonPath}`, + ) + } + + // Get the extension version from package.json + const extensionDir = path.join(__dirname, '..') + const packageJson = JSON.parse( + fs.readFileSync(path.join(extensionDir, 'package.json'), 'utf-8'), + ) + const version = packageJson.version + const extensionName = packageJson.name || 'sqlmesh' + + // Look for the specific version .vsix file + const vsixFileName = `${extensionName}-${version}.vsix` + const vsixPath = path.join(extensionDir, vsixFileName) + + if (!fs.existsSync(vsixPath)) { + throw new Error( + `Extension file ${vsixFileName} not found. Run "pnpm run vscode:package" first.`, + ) + } + + console.log(`Using extension: ${vsixFileName}`) + + // Install the extension first + const extensionsDir = path.join(tempDir, 'extensions') + console.log('Installing extension...') + execSync( + `pnpm run code-server --user-data-dir "${tempDir}" --extensions-dir "${extensionsDir}" --install-extension "${vsixPath}"`, + { stdio: 'inherit' }, + ) + + // Start code-server instance + const codeServerProcess = spawn( + 'pnpm', + [ + 'run', + 'code-server', + '--bind-addr', + `127.0.0.1:${codeServerPort}`, + '--auth', + 'none', + '--disable-telemetry', + '--disable-update-check', + '--disable-workspace-trust', + '--user-data-dir', + tempDir, + '--extensions-dir', + extensionsDir, + tempDir, + ], + { + stdio: 'pipe', + cwd: path.join(__dirname, '..'), + }, + ) + + // Wait for code-server to be ready + await new Promise((resolve, reject) => { + let output = '' + const timeout = setTimeout(() => { + reject(new Error('Code-server failed to start within timeout')) + }, 30000) + + codeServerProcess.stdout?.on('data', data => { + output += data.toString() + if (output.includes('HTTP server listening on')) { + clearTimeout(timeout) + resolve() + } + }) + + codeServerProcess.stderr?.on('data', data => { + console.error('Code-server stderr:', data.toString()) + }) + + codeServerProcess.on('error', error => { + clearTimeout(timeout) + reject(error) + }) + + codeServerProcess.on('exit', code => { + if (code !== 0) { + clearTimeout(timeout) + reject(new Error(`Code-server exited with code ${code}`)) + } + }) + }) + + return { codeServerProcess, codeServerPort, tempDir } +} + +export async function stopCodeServer( + context: CodeServerContext, +): Promise { + const { codeServerProcess, tempDir } = context + + // Clean up code-server process + codeServerProcess.kill('SIGTERM') + + // Wait for process to exit + await new Promise(resolve => { + codeServerProcess.on('exit', () => { + resolve() + }) + // Force kill after 5 seconds + setTimeout(() => { + if (!codeServerProcess.killed) { + codeServerProcess.kill('SIGKILL') + } + resolve() + }, 5000) + }) + + // Clean up temporary directory + await fs.remove(tempDir) +} From 3d0ab458c1baaa0dd5c2f0babd13a5512ffbefcc Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 26 Jun 2025 20:08:36 +0100 Subject: [PATCH 0470/1056] ci: make release vscode manual step (#4830) --- .github/workflows/release_extension.yaml | 35 ++++++++++++++++-------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release_extension.yaml b/.github/workflows/release_extension.yaml index a4106c1d81..3807bc3440 100644 --- a/.github/workflows/release_extension.yaml +++ b/.github/workflows/release_extension.yaml @@ -1,14 +1,32 @@ name: Release VSCode Extension on: - push: - tags: - - 'vscode@v*' + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., 1.0.0)' + required: true + type: string jobs: release: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Check branch is main + run: | + if [[ "${{ github.ref }}" != "refs/heads/main" ]]; then + echo "Error: This workflow can only be run from the main branch" + exit 1 + fi + echo "Branch check passed: running from main branch" + - name: Validate version format + run: | + version="${{ github.event.inputs.version }}" + if ! [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "Error: Version must be a valid semantic version (e.g., 1.0.0, 1.0.0-beta.1, 1.0.0+build.1)" + exit 1 + fi + echo "Version format is valid: $version" - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -19,25 +37,20 @@ jobs: version: 10 - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Extract version from tag - id: extract_version - run: | - VERSION=${GITHUB_REF#refs/tags/vscode@v} - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - name: Update package.json version working-directory: vscode/extension run: | - npm version ${{ steps.extract_version.outputs.VERSION }} --no-git-tag-version + npm version ${{ github.event.inputs.version }} --no-git-tag-version - name: Build extension working-directory: vscode/extension run: pnpm run vscode:package - name: Upload extension to Marketplace working-directory: vscode/extension run: | - pnpx vsce publish --packagePath sqlmesh-${{ steps.extract_version.outputs.VERSION }}.vsix + pnpx vsce publish --packagePath sqlmesh-${{ github.event.inputs.version }}.vsix env: VSCE_PAT: ${{ secrets.VSCE_PAT }} - name: Upload extension to OpenVSX working-directory: vscode/extension run: | - pnpx ovsx publish -p ${{ secrets.OPEN_VSX_TOKEN }} sqlmesh-${{ steps.extract_version.outputs.VERSION }}.vsix + pnpx ovsx publish -p ${{ secrets.OPEN_VSX_TOKEN }} sqlmesh-${{ github.event.inputs.version }}.vsix From 03e34296142ee0c77599574577665ba67a2f0b7c Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 27 Jun 2025 07:14:49 +1200 Subject: [PATCH 0471/1056] Fix: display_name should not exclude the default catalog when environment_suffix_target is set to CATALOG (#4821) --- sqlmesh/core/snapshot/definition.py | 17 ++++++++++++----- tests/core/test_snapshot.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 573d3bc75d..6e000764c6 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -15,6 +15,7 @@ from sqlmesh.core import constants as c from sqlmesh.core.audit import StandaloneAudit +from sqlmesh.core.environment import EnvironmentSuffixTarget from sqlmesh.core.macros import call_macro from sqlmesh.core.model import Model, ModelKindMixin, ModelKindName, ViewKind, CustomKind from sqlmesh.core.model.definition import _Model @@ -1589,12 +1590,18 @@ def display_name( if snapshot_info_like.is_audit: return snapshot_info_like.name view_name = exp.to_table(snapshot_info_like.name) + + catalog = ( + None + if ( + environment_naming_info.suffix_target != EnvironmentSuffixTarget.CATALOG + and view_name.catalog == default_catalog + ) + else view_name.catalog + ) + qvn = QualifiedViewName( - catalog=( - view_name.catalog - if view_name.catalog and view_name.catalog != default_catalog - else None - ), + catalog=catalog, schema_name=view_name.db or None, table=view_name.name, ) diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index e4eb12c522..f09083f500 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -2178,6 +2178,21 @@ def test_deployability_index_missing_parent(make_snapshot): "snowflake", "CATALOG_OVERRIDE.test_db.test_model__DEV", ), + # EnvironmentSuffixTarget.CATALOG + ( + "test_db.test_model", + EnvironmentNamingInfo(name="dev", suffix_target=EnvironmentSuffixTarget.CATALOG), + "default_catalog", + "duckdb", + "default_catalog__dev.test_db.test_model", + ), + ( + "test_db.test_model", + EnvironmentNamingInfo(name="dev", suffix_target=EnvironmentSuffixTarget.CATALOG), + "default_catalog", + "snowflake", + "DEFAULT_CATALOG__DEV.test_db.test_model", + ), ), ) def test_display_name( From 2006664a0277db695cf8cff952d4bf75c3e3ea06 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:30:51 -0700 Subject: [PATCH 0472/1056] fix!: failure instead of neutral missing req approval (#4816) --- sqlmesh/integrations/github/cicd/command.py | 2 +- tests/integrations/github/cicd/test_github_commands.py | 4 ++-- tests/integrations/github/cicd/test_integration.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index cd5104df50..021c49cf31 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -44,7 +44,7 @@ def _check_required_approvers(controller: GithubController) -> bool: ) return True controller.update_required_approval_check( - status=GithubCheckStatus.COMPLETED, conclusion=GithubCheckConclusion.NEUTRAL + status=GithubCheckStatus.COMPLETED, conclusion=GithubCheckConclusion.FAILURE ) return False diff --git a/tests/integrations/github/cicd/test_github_commands.py b/tests/integrations/github/cicd/test_github_commands.py index 5f1dfd0a91..6be6a4557a 100644 --- a/tests/integrations/github/cicd/test_github_commands.py +++ b/tests/integrations/github/cicd/test_github_commands.py @@ -382,7 +382,7 @@ def test_run_all_missing_approval( assert GithubCheckStatus(approval_checks_runs[0]["status"]).is_queued assert GithubCheckStatus(approval_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(approval_checks_runs[2]["status"]).is_completed - assert GithubCheckConclusion(approval_checks_runs[2]["conclusion"]).is_neutral + assert GithubCheckConclusion(approval_checks_runs[2]["conclusion"]).is_failure assert len(controller._context.apply.call_args_list) == 1 pr_plan = controller._context.apply.call_args_list[0][0] @@ -402,7 +402,7 @@ def test_run_all_missing_approval( output = f.read() assert ( output - == "run_unit_tests=success\nhas_required_approval=neutral\ncreated_pr_environment=true\npr_environment_name=hello_world_2\npr_environment_synced=success\nprod_plan_preview=success\nprod_environment_synced=skipped\n" + == "run_unit_tests=success\nhas_required_approval=failure\ncreated_pr_environment=true\npr_environment_name=hello_world_2\npr_environment_synced=success\nprod_plan_preview=success\nprod_environment_synced=skipped\n" ) diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index cd70ad72fe..beda7a5c00 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -1040,7 +1040,7 @@ def test_no_merge_since_no_deploy_signal( assert GithubCheckStatus(approval_checks_runs[0]["status"]).is_queued assert GithubCheckStatus(approval_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(approval_checks_runs[2]["status"]).is_completed - assert GithubCheckConclusion(approval_checks_runs[2]["conclusion"]).is_neutral + assert GithubCheckConclusion(approval_checks_runs[2]["conclusion"]).is_failure assert approval_checks_runs[2]["output"]["title"] == "Need a Required Approval" assert ( approval_checks_runs[2]["output"]["summary"] @@ -1068,7 +1068,7 @@ def test_no_merge_since_no_deploy_signal( output = f.read() assert ( output - == "run_unit_tests=success\nhas_required_approval=neutral\ncreated_pr_environment=true\npr_environment_name=hello_world_2\npr_environment_synced=success\nprod_plan_preview=success\nprod_environment_synced=skipped\n" + == "run_unit_tests=success\nhas_required_approval=failure\ncreated_pr_environment=true\npr_environment_name=hello_world_2\npr_environment_synced=success\nprod_plan_preview=success\nprod_environment_synced=skipped\n" ) From 05d947e93a1bd8d4c4e95583023cc23507a3ffe1 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 26 Jun 2025 15:36:43 -0700 Subject: [PATCH 0473/1056] Chore: Warn user about interval expansion only if the model is incremental (#4834) --- sqlmesh/core/snapshot/definition.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 6e000764c6..af6641b5c0 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -784,7 +784,9 @@ def get_removal_interval( # only warn if the requested removal interval was a subset of the actual model intervals and was automatically expanded # if the requested interval was the same or wider than the actual model intervals, no need to warn - if requested_start > expanded_start or requested_end < expanded_end: + if ( + requested_start > expanded_start or requested_end < expanded_end + ) and self.is_incremental: from sqlmesh.core.console import get_console get_console().log_warning( From 610b44c078d8f46efff16b65228bd0c9421e2a84 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 27 Jun 2025 10:39:35 +0300 Subject: [PATCH 0474/1056] Chore: Fix find references lsp end to end tests (#4835) --- .../extension/tests/find_references.spec.ts | 1060 +++++++++-------- 1 file changed, 550 insertions(+), 510 deletions(-) diff --git a/vscode/extension/tests/find_references.spec.ts b/vscode/extension/tests/find_references.spec.ts index 7b703257ad..605eae8414 100644 --- a/vscode/extension/tests/find_references.spec.ts +++ b/vscode/extension/tests/find_references.spec.ts @@ -9,612 +9,652 @@ const GO_TO_REFERENCES_KEY = 'Shift+F12' const FIND_ALL_REFERENCES_KEY = process.platform === 'darwin' ? 'Alt+Shift+F12' : 'Ctrl+Shift+F12' -test.describe('Model References', () => { - let tempDir: string - let window: any - let close: () => Promise - - test.beforeEach(async () => { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const vscode = await startVSCode(tempDir) - window = vscode.window - close = vscode.close - }) - - test.afterEach(async () => { - await close() - fs.removeSync(tempDir) - }) +// Helper function to set up a test environment for model references +async function setupModelTestEnvironment() { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const { window, close } = await startVSCode(tempDir) + return { window, close, tempDir } +} + +// Helper function to navigate to models folder +async function navigateToModels(window: any) { + await window.waitForSelector('text=models') + await window + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() +} + +// Helper function to navigate to audits folder +async function navigateToAudits(window: any) { + await window.waitForSelector('text=audits') + await window + .getByRole('treeitem', { name: 'audits', exact: true }) + .locator('a') + .click() +} + +// Helper function to open customers.sql and wait for SQLMesh context +async function openCustomersFile(window: any) { + await navigateToModels(window) + await window + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') +} + +// Helper function to open top_waiters.sql and wait for SQLMesh context +async function openTopWaitersFile(window: any) { + await navigateToModels(window) + await window + .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) + .locator('a') + .click() + await window.waitForSelector('text=grain') + await window.waitForSelector('text=Loaded SQLMesh Context') +} +test.describe('Model References', () => { test('Go to References (Shift+F12) for Model usage', async () => { - // Step 1: Expand the models folder in the file explorer to access model files - await window.waitForSelector('text=models') - await window - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Step 2: Open customers.sql which contains references to other models - await window - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - // Step 3: Ensure SQLMesh extension has fully loaded by checking for model metadata - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') - - // Step 4: Position cursor on the sushi.orders model reference in the SQL query - await window.locator('text=sushi.orders').first().click() - - // Step 5: Trigger "Go to References" command using Shift+F12 keyboard shortcut - await window.keyboard.press(GO_TO_REFERENCES_KEY) - - // Step 6: Wait for VSCode references panel to appear at the bottom - await window.waitForSelector('text=References') - - // Step 7: Ensure references panel has populated with all usages of sushi.orders model - await window.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 6 - }, - { timeout: 10000 }, - ) - - // Step 8: Verify the references panel shows both SQL and Python files containing references - const hasReferences = await window.evaluate(() => { - const body = document.body.textContent || '' - return ( - body.includes('References') && - (body.includes('.sql') || body.includes('.py')) + const { window, close, tempDir } = await setupModelTestEnvironment() + + try { + // Open customers.sql which contains references to other models + await openCustomersFile(window) + + // Step 4: Position cursor on the sushi.orders model reference in the SQL query + await window.locator('text=sushi.orders').first().click() + + // Step 5: Trigger "Go to References" command using Shift+F12 keyboard shortcut + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Step 6: Wait for VSCode references panel to appear at the bottom + await window.waitForSelector('text=References') + + // Step 7: Ensure references panel has populated with all usages of sushi.orders model + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 6 + }, + { timeout: 10000 }, ) - }) - - expect(hasReferences).toBe(true) - // Step 9: Find and click on the orders.py reference to navigate to the model definition - let clickedReference = false + // Step 8: Verify the references panel shows both SQL and Python files containing references + const hasReferences = await window.evaluate(() => { + const body = document.body.textContent || '' + return ( + body.includes('References') && + (body.includes('.sql') || body.includes('.py')) + ) + }) - const referenceItems = await window.locator( - '.monaco-list-row, .reference-item, .monaco-tl-row', - ) - const count = await referenceItems.count() + expect(hasReferences).toBe(true) - for (let i = 0; i < count; i++) { - const item = referenceItems.nth(i) - const text = await item.textContent() + // Step 9: Find and click on the orders.py reference to navigate to the model definition + let clickedReference = false - // Search for the orders.py reference which contains the Python model definition - if (text && text.includes('orders.py')) { - await item.click() - clickedReference = true - break + const referenceItems = window.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() + + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Search for the orders.py reference which contains the Python model definition + if (text && text.includes('orders.py')) { + await item.click() + clickedReference = true + break + } } - } - expect(clickedReference).toBe(true) + expect(clickedReference).toBe(true) - // Step 10: Verify successful navigation to orders.py by checking for unique Python code - await expect(window.locator('text=list(range(0, 100))')).toBeVisible() + // Step 10: Verify successful navigation to orders.py by checking for unique Python code + await expect(window.locator('text=list(range(0, 100))')).toBeVisible() + } finally { + await close() + fs.removeSync(tempDir) + } }) test('Find All References (Alt+Shift+F12) for Model', async () => { - // Step 1: Expand the models folder to access SQLMesh model files - await window.waitForSelector('text=models') - await window - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Step 2: Open customers.sql which contains multiple model references - await window - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - // Step 3: Wait for SQLMesh context to fully initialize - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') - - // Step 4: Click on sushi.orders model reference to position cursor - await window.locator('text=sushi.orders').first().click() - - // Step 5: Trigger "Find All References" command using Alt+Shift+F12 (or Ctrl+Shift+F12 on Windows/Linux) - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) - - let clickedReference = false - const referenceItems = await window.locator( - '.monaco-list-row, .reference-item, .monaco-tl-row', - ) - const count = await referenceItems.count() - - // Step 6: Iterate through references to find and click on orders.py - for (let i = 0; i < count; i++) { - const item = referenceItems.nth(i) - const text = await item.textContent() - - // Find the orders.py reference which contains the model implementation - if (text && text.includes('orders.py')) { - await item.click() - - clickedReference = true - break - } - } + const { window, close, tempDir } = await setupModelTestEnvironment() - expect(clickedReference).toBe(true) + try { + // Open customers.sql which contains multiple model references + await openCustomersFile(window) - // Step 7: Verify navigation to orders.py by checking for Python import statement - await expect(window.locator('text=import random')).toBeVisible() + // Step 4: Click on sushi.orders model reference to position cursor + await window.locator('text=sushi.orders').first().click() - // Step 8: Click on the import statement to ensure file is fully loaded and interactive - await window.locator('text=import random').first().click() + // Step 5: Trigger "Find All References" command using Alt+Shift+F12 (or Ctrl+Shift+F12 on Windows/Linux) + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) - // Step 9: Final verification that we're viewing the correct Python model file - await expect(window.locator('text=list(range(0, 100))')).toBeVisible() - }) + let clickedReference = false + const referenceItems = window.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() - test('Go to References for Model from Audit', async () => { - // Step 1: Expand audits folder to access SQLMesh audit files - await window.waitForSelector('text=audits') - await window - .getByRole('treeitem', { name: 'audits', exact: true }) - .locator('a') - .click() - - // Step 2: Open assert_item_price_above_zero.sql audit file which references sushi.items model - await window - .getByRole('treeitem', { - name: 'assert_item_price_above_zero.sql', - exact: true, - }) - .locator('a') - .click() + // Step 6: Iterate through references to find and click on orders.py + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() - // Step 3: Wait for audit file to load and SQLMesh context to initialize - await window.waitForSelector('text=standalone') - await window.waitForSelector('text=Loaded SQLMesh Context') + // Find the orders.py reference which contains the model implementation + if (text && text.includes('orders.py')) { + await item.click() - // Step 4: Click on sushi.items model reference in the audit query - await window.locator('text=sushi.items').first().click() + clickedReference = true + break + } + } - // Step 5: Trigger "Go to References" to find all places where sushi.items is used - await window.keyboard.press(GO_TO_REFERENCES_KEY) + expect(clickedReference).toBe(true) - // Step 6: Wait for VSCode references panel to appear - await window.waitForSelector('text=References') + // Step 7: Verify navigation to orders.py by checking for Python import statement + await expect(window.locator('text=import random')).toBeVisible() - // Step 7: Ensure references panel shows multiple files that reference sushi.items - await window.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 4 - }, - { timeout: 10000 }, - ) - - // Step 8: Verify references panel contains both audit and model files - const hasReferences = await window.evaluate(() => { - const body = document.body.textContent || '' - return ( - body.includes('References') && - (body.includes('.sql') || body.includes('.py')) - ) - }) + // Step 8: Click on the import statement to ensure file is fully loaded and interactive + await window.locator('text=import random').first().click() - expect(hasReferences).toBe(true) + // Step 9: Final verification that we're viewing the correct Python model file + await expect(window.locator('text=list(range(0, 100))')).toBeVisible() + } finally { + await close() + fs.removeSync(tempDir) + } + }) - // 9. Click on one of the references to navigate to it - let clickedReference = false + test('Go to References for Model from Audit', async () => { + const { window, close, tempDir } = await setupModelTestEnvironment() + + try { + // Open assert_item_price_above_zero.sql audit file which references sushi.items model + await navigateToAudits(window) + await window + .getByRole('treeitem', { + name: 'assert_item_price_above_zero.sql', + exact: true, + }) + .locator('a') + .click() + + // Wait for audit file to load and SQLMesh context to initialize + await window.waitForSelector('text=standalone') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Step 4: Click on sushi.items model reference in the audit query + await window.locator('text=sushi.items').first().click() + + // Step 5: Trigger "Go to References" to find all places where sushi.items is used + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Step 6: Wait for VSCode references panel to appear + await window.waitForSelector('text=References') + + // Step 7: Ensure references panel shows multiple files that reference sushi.items + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 4 + }, + { timeout: 10000 }, + ) - const referenceItems = await window.locator( - '.monaco-list-row, .reference-item, .monaco-tl-row', - ) - const count = await referenceItems.count() + // Step 8: Verify references panel contains both audit and model files + const hasReferences = await window.evaluate(() => { + const body = document.body.textContent || '' + return ( + body.includes('References') && + (body.includes('.sql') || body.includes('.py')) + ) + }) + + expect(hasReferences).toBe(true) - for (let i = 0; i < count; i++) { - const item = referenceItems.nth(i) - const text = await item.textContent() + // 9. Click on one of the references to navigate to it + let clickedReference = false - // Search for the customer_revenue_by_day.sql file which joins with sushi.items - if (text && text.includes('customer_revenue_by_day.sql')) { - await item.click() - clickedReference = true - break + const referenceItems = window.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() + + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Search for the customer_revenue_by_day.sql file which joins with sushi.items + if (text && text.includes('customer_revenue_by_day.sql')) { + await item.click() + clickedReference = true + break + } } - } - expect(clickedReference).toBe(true) + expect(clickedReference).toBe(true) - // Step 10: Verify navigation to customer_revenue_by_day.sql by checking for SQL JOIN syntax - await expect(window.locator('text=LEFT JOIN')).toBeVisible() + // Step 10: Verify navigation to customer_revenue_by_day.sql by checking for SQL JOIN syntax + await expect(window.locator('text=LEFT JOIN')).toBeVisible() - // Step 11: Click on LEFT JOIN to ensure file is interactive and verify content - await window.locator('text=LEFT JOIN').first().click() - await expect( - window.locator('text=FROM sushi.order_items AS oi'), - ).toBeVisible() + // Step 11: Click on LEFT JOIN to ensure file is interactive and verify content + await window.locator('text=LEFT JOIN').first().click() + await expect( + window.locator('text=FROM sushi.order_items AS oi'), + ).toBeVisible() + } finally { + await close() + fs.removeSync(tempDir) + } }) test('Find All Model References from Audit', async () => { - // Step 1: Expand audits folder in the file explorer - await window.waitForSelector('text=audits') - await window - .getByRole('treeitem', { name: 'audits', exact: true }) - .locator('a') - .click() - - // Step 2: Open the audit file that validates item prices - await window - .getByRole('treeitem', { - name: 'assert_item_price_above_zero.sql', - exact: true, - }) - .locator('a') - .click() + const { window, close, tempDir } = await setupModelTestEnvironment() + + try { + // Open the audit file that validates item prices + await navigateToAudits(window) + await window + .getByRole('treeitem', { + name: 'assert_item_price_above_zero.sql', + exact: true, + }) + .locator('a') + .click() + + // Ensure audit file and SQLMesh context are fully loaded + await window.waitForSelector('text=standalone') + await window.waitForSelector('text=Loaded SQLMesh Context') + + // Step 4: Position cursor on sushi.items model reference + await window.locator('text=sushi.items').first().click() + + // Step 5: Use Find All References to see all occurrences across the project + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + + // Step 6: Click on a reference to navigate to customer_revenue_by_day.sql + let clickedReference = false + + const referenceItems = window.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() + + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Look for a reference that contains customer_revenue_by_day + if (text && text.includes('customer_revenue_by_day.sql')) { + await item.click() + clickedReference = true + break + } + } - // Step 3: Ensure audit file and SQLMesh context are fully loaded - await window.waitForSelector('text=standalone') - await window.waitForSelector('text=Loaded SQLMesh Context') + expect(clickedReference).toBe(true) - // Step 4: Position cursor on sushi.items model reference - await window.locator('text=sushi.items').first().click() + // Step 7: Verify successful navigation by checking for SQL JOIN statement + await expect(window.locator('text=LEFT JOIN')).toBeVisible() - // Step 5: Use Find All References to see all occurrences across the project - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + // Step 8: Interact with the file to verify it's fully loaded and check its content + await window.locator('text=LEFT JOIN').first().click() + await expect( + window.locator('text=FROM sushi.order_items AS oi'), + ).toBeVisible() + } finally { + await close() + fs.removeSync(tempDir) + } + }) +}) - // Step 6: Click on a reference to navigate to customer_revenue_by_day.sql - let clickedReference = false +test.describe('CTE References', () => { + test('Go to references from definition of CTE', async () => { + const { window, close, tempDir } = await setupModelTestEnvironment() - const referenceItems = await window.locator( - '.monaco-list-row, .reference-item, .monaco-tl-row', - ) - const count = await referenceItems.count() + try { + await openCustomersFile(window) - for (let i = 0; i < count; i++) { - const item = referenceItems.nth(i) - const text = await item.textContent() + // Click on the CTE definition "current_marketing_outer" at line 20 to position cursor + await window.locator('text=current_marketing_outer').first().click() - // Look for a reference that contains customer_revenue_by_day - if (text && text.includes('customer_revenue_by_day.sql')) { - await item.click() - clickedReference = true - break - } - } + // Use keyboard shortcut to find all references + await window.keyboard.press(GO_TO_REFERENCES_KEY) - expect(clickedReference).toBe(true) + // Wait for the references to appear + await window.waitForSelector('text=References') - // Step 7: Verify successful navigation by checking for SQL JOIN statement - await expect(window.locator('text=LEFT JOIN')).toBeVisible() + // Wait for reference panel to populate + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) - // Step 8: Interact with the file to verify it's fully loaded and check its content - await window.locator('text=LEFT JOIN').first().click() - await expect( - window.locator('text=FROM sushi.order_items AS oi'), - ).toBeVisible() - }) -}) + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() -test.describe('CTE References', () => { - let tempDir: string - let window: any - let close: () => Promise - - test.beforeEach(async () => { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const vscode = await startVSCode(tempDir) - window = vscode.window - close = vscode.close - - // Common setup: navigate to customers.sql - await window.waitForSelector('text=models') - await window - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await window - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + // Check that both CTE definition and usage are listed in references + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing_outer AS') + await window.waitForSelector('text=FROM current_marketing_outer') + } finally { + await close() + fs.removeSync(tempDir) + } }) - test.afterEach(async () => { - await close() - fs.removeSync(tempDir) - }) + test('Go to references from usage of CTE', async () => { + const { window, close, tempDir } = await setupModelTestEnvironment() - test('Go to references from definition of CTE', async () => { - // Click on the CTE definition "current_marketing_outer" at line 20 to position cursor - await window.locator('text=current_marketing_outer').first().click() + try { + await openCustomersFile(window) + + // Click on the CTE usage this time for "current_marketing_outer" + await window.locator('text=FROM current_marketing_outer').click({ + position: { x: 80, y: 5 }, // Clicks on the usage rather than first which was definition + }) - // Use keyboard shortcut to find all references - await window.keyboard.press(GO_TO_REFERENCES_KEY) + // Use keyboard shortcut to go to references + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Wait for the references to appear + await window.waitForSelector('text=References') + + // Better assertions: wait for reference panel to populate + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) - // Wait for the references to appear - await window.waitForSelector('text=References') + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing_outer AS') + await window.waitForSelector('text=FROM current_marketing_outer') - // Wait for reference panel to populate - await window.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 2 - }, - { timeout: 5000 }, - ) - - // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() - - // Check that both CTE definition and usage are listed in references - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing_outer AS') - await window.waitForSelector('text=FROM current_marketing_outer') + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() + } finally { + await close() + fs.removeSync(tempDir) + } }) - test('Go to references from usage of CTE', async () => { - // Click on the CTE usage this time for "current_marketing_outer" - await window.locator('text=FROM current_marketing_outer').click({ - position: { x: 80, y: 5 }, // Clicks on the usage rather than first which was definition - }) - - // Use keyboard shortcut to go to references - await window.keyboard.press(GO_TO_REFERENCES_KEY) - - // Wait for the references to appear - await window.waitForSelector('text=References') - - // Better assertions: wait for reference panel to populate - await window.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 2 - }, - { timeout: 5000 }, - ) + test('Go to references for nested CTE', async () => { + const { window, close, tempDir } = await setupModelTestEnvironment() - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing_outer AS') - await window.waitForSelector('text=FROM current_marketing_outer') + try { + await openCustomersFile(window) - // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() - }) + // Click on the nested CTE "current_marketing" + await window.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, // Click on the CTE name part + }) - test('Go to references for nested CTE', async () => { - // Click on the nested CTE "current_marketing" - await window.locator('text=WITH current_marketing AS').click({ - position: { x: 100, y: 5 }, // Click on the CTE name part - }) - - // Use keyboard shortcut to find all references - await window.keyboard.press(GO_TO_REFERENCES_KEY) - - // Wait for the references to appear - await window.waitForSelector('text=References') - - // Wait for reference panel to populate - await window.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 2 - }, - { timeout: 5000 }, - ) - - // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() - - // Check that both CTE definition and usage are listed in references - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing AS') - await window.waitForSelector('text=FROM current_marketing') + // Use keyboard shortcut to find all references + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Wait for the references to appear + await window.waitForSelector('text=References') + + // Wait for reference panel to populate + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) + + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() + + // Check that both CTE definition and usage are listed in references + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing AS') + await window.waitForSelector('text=FROM current_marketing') + } finally { + await close() + fs.removeSync(tempDir) + } }) test('Find all references for CTE', async () => { - // Click on the CTE definition "current_marketing_outer" - await window.locator('text=current_marketing_outer').first().click() + const { window, close, tempDir } = await setupModelTestEnvironment() + + try { + await openCustomersFile(window) - // Use keyboard shortcut to find all references - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + // Click on the CTE definition "current_marketing_outer" + await window.locator('text=current_marketing_outer').first().click() - // Verify references contains expected content - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing_outer AS') - await window.waitForSelector('text=FROM current_marketing_outer') + // Use keyboard shortcut to find all references + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) - // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() + // Verify references contains expected content + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing_outer AS') + await window.waitForSelector('text=FROM current_marketing_outer') + + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() + } finally { + await close() + fs.removeSync(tempDir) + } }) test('Find all references from usage for CTE', async () => { - // Click on the CTE usage of "current_marketing_outer" using last - await window.locator('text=current_marketing_outer').last().click() + const { window, close, tempDir } = await setupModelTestEnvironment() - // Use keyboard shortcut to find all references - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + try { + await openCustomersFile(window) - // Verify references contains expected content - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing_outer AS') - await window.waitForSelector('text=FROM current_marketing_outer') + // Click on the CTE usage of "current_marketing_outer" using last + await window.locator('text=current_marketing_outer').last().click() - // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() + // Use keyboard shortcut to find all references + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + + // Verify references contains expected content + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing_outer AS') + await window.waitForSelector('text=FROM current_marketing_outer') + + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() + } finally { + await close() + fs.removeSync(tempDir) + } }) test('Find all references for nested CTE', async () => { - // Click on the nested CTE "current_marketing" at line 33 - // We need to be more specific to get the inner one - await window.locator('text=WITH current_marketing AS').click({ - position: { x: 100, y: 5 }, // Click on the CTE name part - }) - - // Use keyboard shortcut to find all references - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) - - // Verify references contains expected content - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing AS') - await window.waitForSelector('text=FROM current_marketing') - - // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() + const { window, close, tempDir } = await setupModelTestEnvironment() + + try { + await openCustomersFile(window) + + // Click on the nested CTE "current_marketing" at line 33 + // We need to be more specific to get the inner one + await window.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, // Click on the CTE name part + }) + + // Use keyboard shortcut to find all references + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + + // Verify references contains expected content + await window.waitForSelector('text=References') + await window.waitForSelector('text=WITH current_marketing AS') + await window.waitForSelector('text=FROM current_marketing') + + // Verify that the customers.sql file is shown in results + await expect(window.locator('text=customers.sql').first()).toBeVisible() + } finally { + await close() + fs.removeSync(tempDir) + } }) }) test.describe('Macro References', () => { - let tempDir: string - let window: any - let close: () => Promise - - test.beforeEach(async () => { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const vscode = await startVSCode(tempDir) - window = vscode.window - close = vscode.close - - // Common setup: navigate to top_waiters.sql which uses macros - await window.waitForSelector('text=models') - await window - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await window - .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) - .locator('a') - .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') - }) + test('Go to References for @ADD_ONE macro', async () => { + const { window, close, tempDir } = await setupModelTestEnvironment() - test.afterEach(async () => { - await close() - fs.removeSync(tempDir) - }) + try { + await openTopWaitersFile(window) - test('Go to References for @ADD_ONE macro', async () => { - // Click on the @ADD_ONE macro usage - await window.locator('text=@ADD_ONE').first().click() + // Click on the @ADD_ONE macro usage + await window.locator('text=@ADD_ONE').first().click() - // Use keyboard shortcut to find all references - await window.keyboard.press(GO_TO_REFERENCES_KEY) + // Use keyboard shortcut to find all references + await window.keyboard.press(GO_TO_REFERENCES_KEY) - // Wait for the references to appear - await window.waitForSelector('text=References') + // Wait for the references to appear + await window.waitForSelector('text=References') - // Wait for reference panel to populate - await window.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 2 - }, - { timeout: 5000 }, - ) - - // Verify that both the definition and two usages are shown - await expect(window.locator('text=utils.py').first()).toBeVisible() - await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() - await expect(window.locator('text=customers.sql').first()).toBeVisible() + // Wait for reference panel to populate + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) + + // Verify that both the definition and two usages are shown + await expect(window.locator('text=utils.py').first()).toBeVisible() + await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() + await expect(window.locator('text=customers.sql').first()).toBeVisible() + } finally { + await close() + fs.removeSync(tempDir) + } }) test('Find All References for @MULTIPLY macro', async () => { - // Click on the @MULTIPLY macro usage and then navigate to it - await window.locator('text=@MULTIPLY').first().click() - - // Use keyboard shortcut to find all references - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) - - // Verify references contains expected content - await window.waitForSelector('text=References') - - // Verify that both utils.py (definition) and top_waiters.sql (usage) are shown - await expect(window.locator('text=utils.py').first()).toBeVisible() - await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() - - // Click on the utils.py reference to navigate to the macro definition - let clickedReference = false - const referenceItems = await window.locator( - '.monaco-list-row, .reference-item, .monaco-tl-row', - ) - const count = await referenceItems.count() - - for (let i = 0; i < count; i++) { - const item = referenceItems.nth(i) - const text = await item.textContent() - - // Find the utils.py reference which contains the macro definition - if (text && text.includes('utils.py')) { - await item.click() - clickedReference = true - break + const { window, close, tempDir } = await setupModelTestEnvironment() + + try { + await openTopWaitersFile(window) + + // Click on the @MULTIPLY macro usage and then navigate to it + await window.locator('text=@MULTIPLY').first().click() + + // Use keyboard shortcut to find all references + await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + + // Verify references contains expected content + await window.waitForSelector('text=References') + + // Verify that both utils.py (definition) and top_waiters.sql (usage) are shown + await expect(window.locator('text=utils.py').first()).toBeVisible() + await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() + + // Click on the utils.py reference to navigate to the macro definition + let clickedReference = false + const referenceItems = window.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() + + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Find the utils.py reference which contains the macro definition + if (text && text.includes('utils.py')) { + await item.click() + clickedReference = true + break + } } - } - expect(clickedReference).toBe(true) + expect(clickedReference).toBe(true) - // Verify it appeared and click on it - await expect(window.locator('text=def multiply')).toBeVisible() - await window.locator('text=def multiply').first().click() + // Verify it appeared and click on it + await expect(window.locator('text=def multiply')).toBeVisible() + await window.locator('text=def multiply').first().click() - // Verify navigation to utils.py by checking the import that appears there - await expect( - window.locator('text=from sqlmesh import SQL, macro'), - ).toBeVisible() + // Verify navigation to utils.py by checking the import that appears there + await expect( + window.locator('text=from sqlmesh import SQL, macro'), + ).toBeVisible() + } finally { + await close() + fs.removeSync(tempDir) + } }) test('Go to References for @SQL_LITERAL macro', async () => { - // Click on the @SQL_LITERAL macro usage - await window.locator('text=@SQL_LITERAL').first().click() + const { window, close, tempDir } = await setupModelTestEnvironment() - // Use keyboard shortcut to find references - await window.keyboard.press(GO_TO_REFERENCES_KEY) + try { + await openTopWaitersFile(window) - // Wait for the references to appear - await window.waitForSelector('text=References') + // Click on the @SQL_LITERAL macro usage + await window.locator('text=@SQL_LITERAL').first().click() - // Wait for reference panel to populate - await window.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 2 - }, - { timeout: 5000 }, - ) - - // Verify that references include both definition and usage - const hasReferences = await window.evaluate(() => { - const body = document.body.textContent || '' - return ( - body.includes('References') && - body.includes('.py') && - body.includes('.sql') + // Use keyboard shortcut to find references + await window.keyboard.press(GO_TO_REFERENCES_KEY) + + // Wait for the references to appear + await window.waitForSelector('text=References') + + // Wait for reference panel to populate + await window.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, ) - }) - expect(hasReferences).toBe(true) + // Verify that references include both definition and usage + const hasReferences = await window.evaluate(() => { + const body = document.body.textContent || '' + return ( + body.includes('References') && + body.includes('.py') && + body.includes('.sql') + ) + }) + + expect(hasReferences).toBe(true) - await expect(window.locator('text=utils.py').first()).toBeVisible() - await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() + await expect(window.locator('text=utils.py').first()).toBeVisible() + await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() + } finally { + await close() + fs.removeSync(tempDir) + } }) }) From e07c5fe70ee03e640868edc224f3acb378dd2f35 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:49:47 +0300 Subject: [PATCH 0475/1056] Chore!: bump sqlglot to v26.31.0 (#4831) --- pyproject.toml | 2 +- tests/core/engine_adapter/test_mssql.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 235bbb8185..993128874e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.30.0", + "sqlglot[rs]~=26.31.0", "tenacity", "time-machine", "json-stream" diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index a420a2d82c..d32343d593 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -296,7 +296,7 @@ def test_insert_overwrite_by_time_partition_supports_insert_overwrite_pandas_not f"__temp_test_table_{temp_table_id}", [(1, "2022-01-01"), (2, "2022-01-02")] ) assert to_sql_calls(adapter) == [ - f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = '__temp_test_table_{temp_table_id}') EXEC('CREATE TABLE [__temp_test_table_{temp_table_id}] ([a] INTEGER, [ds] VARCHAR(MAX))');""", + f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '__temp_test_table_{temp_table_id}') EXEC('CREATE TABLE [__temp_test_table_{temp_table_id}] ([a] INTEGER, [ds] VARCHAR(MAX))');""", f"""MERGE INTO [test_table] AS [__MERGE_TARGET__] USING (SELECT [a] AS [a], [ds] AS [ds] FROM (SELECT CAST([a] AS INTEGER) AS [a], CAST([ds] AS VARCHAR(MAX)) AS [ds] FROM [__temp_test_table_{temp_table_id}]) AS [_subquery] WHERE [ds] BETWEEN '2022-01-01' AND '2022-01-02') AS [__MERGE_SOURCE__] ON (1 = 0) WHEN NOT MATCHED BY SOURCE AND [ds] BETWEEN '2022-01-01' AND '2022-01-02' THEN DELETE WHEN NOT MATCHED THEN INSERT ([a], [ds]) VALUES ([a], [ds]);""", f"DROP TABLE IF EXISTS [__temp_test_table_{temp_table_id}];", ] @@ -366,7 +366,7 @@ def test_insert_overwrite_by_time_partition_replace_where_pandas( ) assert to_sql_calls(adapter) == [ - f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = '__temp_test_table_{temp_table_id}') EXEC('CREATE TABLE [__temp_test_table_{temp_table_id}] ([a] INTEGER, [ds] VARCHAR(MAX))');""", + f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '__temp_test_table_{temp_table_id}') EXEC('CREATE TABLE [__temp_test_table_{temp_table_id}] ([a] INTEGER, [ds] VARCHAR(MAX))');""", f"""MERGE INTO [test_table] AS [__MERGE_TARGET__] USING (SELECT [a] AS [a], [ds] AS [ds] FROM (SELECT CAST([a] AS INTEGER) AS [a], CAST([ds] AS VARCHAR(MAX)) AS [ds] FROM [__temp_test_table_{temp_table_id}]) AS [_subquery] WHERE [ds] BETWEEN '2022-01-01' AND '2022-01-02') AS [__MERGE_SOURCE__] ON (1 = 0) WHEN NOT MATCHED BY SOURCE AND [ds] BETWEEN '2022-01-01' AND '2022-01-02' THEN DELETE WHEN NOT MATCHED THEN INSERT ([a], [ds]) VALUES ([a], [ds]);""", f"DROP TABLE IF EXISTS [__temp_test_table_{temp_table_id}];", ] @@ -401,7 +401,7 @@ def test_insert_append_pandas( ) assert to_sql_calls(adapter) == [ - f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = '__temp_test_table_{temp_table_id}') EXEC('CREATE TABLE [__temp_test_table_{temp_table_id}] ([a] INTEGER, [b] INTEGER)');""", + f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '__temp_test_table_{temp_table_id}') EXEC('CREATE TABLE [__temp_test_table_{temp_table_id}] ([a] INTEGER, [b] INTEGER)');""", f"INSERT INTO [test_table] ([a], [b]) SELECT CAST([a] AS INTEGER) AS [a], CAST([b] AS INTEGER) AS [b] FROM [__temp_test_table_{temp_table_id}];", f"DROP TABLE IF EXISTS [__temp_test_table_{temp_table_id}];", ] @@ -417,7 +417,7 @@ def test_create_table(make_mocked_engine_adapter: t.Callable): adapter.create_table("test_table", columns_to_types) adapter.cursor.execute.assert_called_once_with( - """IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = 'test_table') EXEC('CREATE TABLE [test_table] ([cola] INTEGER, [colb] VARCHAR(MAX))');""" + """IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'test_table') EXEC('CREATE TABLE [test_table] ([cola] INTEGER, [colb] VARCHAR(MAX))');""" ) @@ -436,7 +436,7 @@ def test_create_physical_properties(make_mocked_engine_adapter: t.Callable): ) adapter.cursor.execute.assert_called_once_with( - """IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = 'test_table') EXEC('CREATE TABLE [test_table] ([cola] INTEGER, [colb] VARCHAR(MAX))');""" + """IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'test_table') EXEC('CREATE TABLE [test_table] ([cola] INTEGER, [colb] VARCHAR(MAX))');""" ) @@ -471,7 +471,7 @@ def test_merge_pandas( ) assert to_sql_calls(adapter) == [ - f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = '__temp_target_{temp_table_id}') EXEC('CREATE TABLE [__temp_target_{temp_table_id}] ([id] INTEGER, [ts] DATETIME2, [val] INTEGER)');""", + f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '__temp_target_{temp_table_id}') EXEC('CREATE TABLE [__temp_target_{temp_table_id}] ([id] INTEGER, [ts] DATETIME2, [val] INTEGER)');""", f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] WHEN MATCHED AND EXISTS(SELECT [__MERGE_TARGET__].[ts], [__MERGE_TARGET__].[val] EXCEPT SELECT [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]) THEN UPDATE SET [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts], [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", f"DROP TABLE IF EXISTS [__temp_target_{temp_table_id}];", ] @@ -494,7 +494,7 @@ def test_merge_pandas( ) assert to_sql_calls(adapter) == [ - f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = '__temp_target_{temp_table_id}') EXEC('CREATE TABLE [__temp_target_{temp_table_id}] ([id] INTEGER, [ts] DATETIME2, [val] INTEGER)');""", + f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '__temp_target_{temp_table_id}') EXEC('CREATE TABLE [__temp_target_{temp_table_id}] ([id] INTEGER, [ts] DATETIME2, [val] INTEGER)');""", f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] AND [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts] WHEN MATCHED AND EXISTS(SELECT [__MERGE_TARGET__].[val] EXCEPT SELECT [__MERGE_SOURCE__].[val]) THEN UPDATE SET [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", f"DROP TABLE IF EXISTS [__temp_target_{temp_table_id}];", ] @@ -554,7 +554,7 @@ def temp_table_exists(table: exp.Table) -> bool: ) assert to_sql_calls(adapter) == [ - f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = '{temp_table_name}') EXEC('CREATE TABLE [{temp_table_name}] ([a] INTEGER, [b] INTEGER)');""", + f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{temp_table_name}') EXEC('CREATE TABLE [{temp_table_name}] ([a] INTEGER, [b] INTEGER)');""", "TRUNCATE TABLE [test_table];", f"INSERT INTO [test_table] ([a], [b]) SELECT CAST([a] AS INTEGER) AS [a], CAST([b] AS INTEGER) AS [b] FROM [{temp_table_name}];", f"DROP TABLE IF EXISTS [{temp_table_name}];", @@ -571,7 +571,7 @@ def test_create_table_primary_key(make_mocked_engine_adapter: t.Callable): adapter.create_table("test_table", columns_to_types, primary_key=("cola", "colb")) adapter.cursor.execute.assert_called_once_with( - """IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = 'test_table') EXEC('CREATE TABLE [test_table] ([cola] INTEGER, [colb] VARCHAR(MAX), PRIMARY KEY ([cola], [colb]))');""" + """IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'test_table') EXEC('CREATE TABLE [test_table] ([cola] INTEGER, [colb] VARCHAR(MAX), PRIMARY KEY ([cola], [colb]))');""" ) @@ -846,7 +846,7 @@ def test_replace_query_strategy(adapter: MSSQLEngineAdapter, mocker: MockerFixtu assert to_sql_calls(adapter) == [ # initial - create table if not exists - "IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = 'test_table') EXEC('SELECT * INTO [test_table] FROM (SELECT [a] AS [a], [b] AS [b] FROM [db].[upstream_table] AS [upstream_table]) AS temp');", + "IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'test_table') EXEC('SELECT * INTO [test_table] FROM (SELECT [a] AS [a], [b] AS [b] FROM [db].[upstream_table] AS [upstream_table]) AS temp');", # subsequent - truncate + insert "TRUNCATE TABLE [test_table];", "INSERT INTO [test_table] ([a], [b]) SELECT [a] AS [a], [b] AS [b] FROM [db].[upstream_table] AS [upstream_table];", From f43a4c3cfc5c74542baeb8afd4142dcc07bec5b1 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Mon, 30 Jun 2025 15:41:27 +1200 Subject: [PATCH 0476/1056] Fix(table_diff): Ignore unexpected arguments instead of throwing an error (#4846) --- sqlmesh/core/context.py | 1 + tests/core/test_context.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index d7aa873394..0317aad894 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1679,6 +1679,7 @@ def table_diff( warn_grain_check: bool = False, temp_schema: t.Optional[str] = None, schema_diff_ignore_case: bool = False, + **kwargs: t.Any, # catch-all to prevent an 'unexpected keyword argument' error if an table_diff extension passes in some extra arguments ) -> t.List[TableDiff]: """Show a diff between two tables. diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 213c4cec2b..8922472aa3 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -2304,3 +2304,17 @@ def test_dev_environment_virtual_update_with_environment_statements(tmp_path: Pa updated_statements[0].before_all[1] == "CREATE TABLE IF NOT EXISTS metrics (metric_name VARCHAR(50), value INT)" ) + + +def test_table_diff_ignores_extra_args(sushi_context: Context): + sushi_context.plan(environment="dev", auto_apply=True, include_unmodified=True) + + # the test fails if this call throws an exception + sushi_context.table_diff( + source="prod", + target="dev", + select_models=["sushi.customers"], + on=["customer_id"], + show_sample=True, + some_tcloud_option=1_000, + ) From eb8a5b7443319f2051299601a62a708076740754 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 30 Jun 2025 12:51:08 +0300 Subject: [PATCH 0477/1056] Feat: Add support for dot env file to load variables from (#4840) --- docs/guides/configuration.md | 47 +++++++++- pyproject.toml | 1 + sqlmesh/cli/main.py | 9 +- sqlmesh/core/config/loader.py | 8 ++ sqlmesh/magics.py | 9 +- tests/core/test_config.py | 161 ++++++++++++++++++++++++++++++++++ 6 files changed, 232 insertions(+), 3 deletions(-) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index df8fd9e3f4..b1b06c59f6 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -98,7 +98,52 @@ All software runs within a system environment that stores information as "enviro SQLMesh can access environment variables during configuration, which enables approaches like storing passwords/secrets outside the configuration file and changing configuration parameters dynamically based on which user is running SQLMesh. -You can use environment variables in two ways: specifying them in the configuration file or creating properly named variables to override configuration file values. +You can specify environment variables in the configuration file or by storing them in a `.env` file. + +### .env files + +SQLMesh automatically loads environment variables from a `.env` file in your project directory. This provides a convenient way to manage environment variables without having to set them in your shell. + +Create a `.env` file in your project root with key-value pairs: + +```bash +# .env file +SNOWFLAKE_PW=my_secret_password +S3_BUCKET=s3://my-data-bucket/warehouse +DATABASE_URL=postgresql://user:pass@localhost/db + +# Override specific SQLMesh configuration values +SQLMESH__DEFAULT_GATEWAY=production +SQLMESH__MODEL_DEFAULTS__DIALECT=snowflake +``` + +See the [overrides](#overrides) section for a detailed explanation of how these are defined. + +The rest of the `.env` file variables can be used in your configuration files with `{{ env_var('VARIABLE_NAME') }}` syntax in YAML or accessed via `os.environ['VARIABLE_NAME']` in Python. + +#### Custom dot env file location and name + +By default, SQLMesh loads `.env` files from each project directory. However, you can specify a custom path using the `--dotenv` CLI flag directly when running a command: + +```bash +sqlmesh --dotenv /path/to/custom/.env plan +``` + +!!! note + The `--dotenv` flag is a global option and must be placed **before** the subcommand (e.g. `plan`, `run`), not after. + +Alternatively, you can export the `SQLMESH_DOTENV_PATH` environment variable once, to persist a custom path across all subsequent commands in your shell session: + +```bash +export SQLMESH_DOTENV_PATH=/path/to/custom/.custom_env +sqlmesh plan +sqlmesh run +``` + +**Important considerations:** +- Add `.env` to your `.gitignore` file to avoid committing sensitive information +- SQLMesh will only load the `.env` file if it exists in the project directory (unless a custom path is specified) +- When using a custom path, that specific file takes precedence over any `.env` file in the project directory. ### Configuration file diff --git a/pyproject.toml b/pyproject.toml index 993128874e..e6d14129d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ dev = [ "PyAthena[Pandas]", "PyGithub>=2.6.0", "pyperf", + "python-dotenv", "pyspark~=3.5.0", "pytest", "pytest-asyncio", diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index f3c3afa46a..93d3051bcd 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -84,6 +84,12 @@ def _sqlmesh_version() -> str: type=str, help="The directory to write log files to.", ) +@click.option( + "--dotenv", + type=click.Path(exists=True, path_type=Path), + help="Path to a custom .env file to load environment variables.", + envvar="SQLMESH_DOTENV_PATH", +) @click.pass_context @error_handler def cli( @@ -95,6 +101,7 @@ def cli( debug: bool = False, log_to_stdout: bool = False, log_file_dir: t.Optional[str] = None, + dotenv: t.Optional[Path] = None, ) -> None: """SQLMesh command line tool.""" if "--help" in sys.argv: @@ -118,7 +125,7 @@ def cli( ) configure_console(ignore_warnings=ignore_warnings) - configs = load_configs(config, Context.CONFIG_TYPE, paths) + configs = load_configs(config, Context.CONFIG_TYPE, paths, dotenv_path=dotenv) log_limit = list(configs.values())[0].log_limit remove_excess_logs(log_file_dir, log_limit) diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index 10acf74fa8..f1ef0ed5a7 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -6,6 +6,7 @@ from pathlib import Path from pydantic import ValidationError +from dotenv import load_dotenv from sqlglot.helper import ensure_list from sqlmesh.core import constants as c @@ -25,6 +26,7 @@ def load_configs( config_type: t.Type[C], paths: t.Union[str | Path, t.Iterable[str | Path]], sqlmesh_path: t.Optional[Path] = None, + dotenv_path: t.Optional[Path] = None, ) -> t.Dict[Path, C]: sqlmesh_path = sqlmesh_path or c.SQLMESH_PATH config = config or "config" @@ -35,6 +37,12 @@ def load_configs( for p in (glob.glob(str(path)) or [str(path)]) ] + if dotenv_path: + load_dotenv(dotenv_path=dotenv_path, override=True) + else: + for path in absolute_paths: + load_dotenv(dotenv_path=path / ".env", override=True) + if not isinstance(config, str): if type(config) != config_type: config = convert_config_type(config, config_type) diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 2d299df668..58f7135654 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -8,6 +8,7 @@ from argparse import Namespace, SUPPRESS from collections import defaultdict from copy import deepcopy +from pathlib import Path from hyperscript import h @@ -166,6 +167,9 @@ def _shell(self) -> t.Any: @argument("--ignore-warnings", action="store_true", help="Ignore warnings.") @argument("--debug", action="store_true", help="Enable debug mode.") @argument("--log-file-dir", type=str, help="The directory to write the log file to.") + @argument( + "--dotenv", type=str, help="Path to a custom .env file to load environment variables from." + ) @line_magic def context(self, line: str) -> None: """Sets the context in the user namespace.""" @@ -181,7 +185,10 @@ def context(self, line: str) -> None: ) configure_console(ignore_warnings=args.ignore_warnings) - configs = load_configs(args.config, Context.CONFIG_TYPE, args.paths) + dotenv_path = Path(args.dotenv) if args.dotenv else None + configs = load_configs( + args.config, Context.CONFIG_TYPE, args.paths, dotenv_path=dotenv_path + ) log_limit = list(configs.values())[0].log_limit remove_excess_logs(log_file_dir, log_limit) diff --git a/tests/core/test_config.py b/tests/core/test_config.py index b3457345a8..dd07c8395f 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -24,6 +24,7 @@ load_config_from_env, load_config_from_paths, load_config_from_python_module, + load_configs, ) from sqlmesh.core.context import Context from sqlmesh.core.engine_adapter.athena import AthenaEngineAdapter @@ -1132,3 +1133,163 @@ def test_environment_suffix_target_catalog(tmp_path: Path) -> None: Config, project_paths=[config_path], ) + + +def test_load_python_config_dot_env_vars(tmp_path_factory): + main_dir = tmp_path_factory.mktemp("python_config") + config_path = main_dir / "config.py" + with open(config_path, "w", encoding="utf-8") as fd: + fd.write( + """from sqlmesh.core.config import Config, DuckDBConnectionConfig, GatewayConfig, ModelDefaultsConfig +config = Config(gateways={"duckdb_gateway": GatewayConfig(connection=DuckDBConnectionConfig())}, model_defaults=ModelDefaultsConfig(dialect='')) + """ + ) + + # The environment variable value from the dot env file should be set + # SQLMESH__ variables override config fields directly if they follow the naming structure + dot_path = main_dir / ".env" + with open(dot_path, "w", encoding="utf-8") as fd: + fd.write( + """SQLMESH__GATEWAYS__DUCKDB_GATEWAY__STATE_CONNECTION__TYPE="bigquery" +SQLMESH__GATEWAYS__DUCKDB_GATEWAY__STATE_CONNECTION__CHECK_IMPORT="false" +SQLMESH__DEFAULT_GATEWAY="duckdb_gateway" + """ + ) + + # Use mock.patch.dict to isolate environment variables between the tests + with mock.patch.dict(os.environ, {}, clear=True): + configs = load_configs( + "config", + Config, + paths=[main_dir], + ) + + assert next(iter(configs.values())) == Config( + gateways={ + "duckdb_gateway": GatewayConfig( + connection=DuckDBConnectionConfig(), + state_connection=BigQueryConnectionConfig(check_import=False), + ), + }, + model_defaults=ModelDefaultsConfig(dialect=""), + default_gateway="duckdb_gateway", + ) + + +def test_load_yaml_config_dot_env_vars(tmp_path_factory): + main_dir = tmp_path_factory.mktemp("yaml_config") + config_path = main_dir / "config.yaml" + with open(config_path, "w", encoding="utf-8") as fd: + fd.write( + """gateways: + duckdb_gateway: + connection: + type: duckdb + catalogs: + local: local.db + cloud_sales: {{ env_var('S3_BUCKET') }} + extensions: + - name: httpfs + secrets: + - type: "s3" + key_id: {{ env_var('S3_KEY') }} + secret: {{ env_var('S3_SECRET') }} +model_defaults: + dialect: "" +""" + ) + + # This test checks both using SQLMESH__ prefixed environment variables with underscores + # and setting a regular environment variable for use with env_var(). + dot_path = main_dir / ".env" + with open(dot_path, "w", encoding="utf-8") as fd: + fd.write( + """S3_BUCKET="s3://metrics_bucket/sales.db" +S3_KEY="S3_KEY_ID" +S3_SECRET="XXX_S3_SECRET_XXX" +SQLMESH__DEFAULT_GATEWAY="duckdb_gateway" +SQLMESH__MODEL_DEFAULTS__DIALECT="athena" +""" + ) + + # Use mock.patch.dict to isolate environment variables between the tests + with mock.patch.dict(os.environ, {}, clear=True): + configs = load_configs( + "config", + Config, + paths=[main_dir], + ) + + assert next(iter(configs.values())) == Config( + gateways={ + "duckdb_gateway": GatewayConfig( + connection=DuckDBConnectionConfig( + catalogs={ + "local": "local.db", + "cloud_sales": "s3://metrics_bucket/sales.db", + }, + extensions=[{"name": "httpfs"}], + secrets=[{"type": "s3", "key_id": "S3_KEY_ID", "secret": "XXX_S3_SECRET_XXX"}], + ), + ), + }, + default_gateway="duckdb_gateway", + model_defaults=ModelDefaultsConfig(dialect="athena"), + ) + + +def test_load_yaml_config_custom_dotenv_path(tmp_path_factory): + main_dir = tmp_path_factory.mktemp("yaml_config_2") + config_path = main_dir / "config.yaml" + with open(config_path, "w", encoding="utf-8") as fd: + fd.write( + """gateways: + test_gateway: + connection: + type: duckdb + database: {{ env_var('DB_NAME') }} +""" + ) + + # Create a custom dot env file in a different location + custom_env_dir = tmp_path_factory.mktemp("custom_env") + custom_env_path = custom_env_dir / ".my_env" + with open(custom_env_path, "w", encoding="utf-8") as fd: + fd.write( + """DB_NAME="custom_database.db" +SQLMESH__DEFAULT_GATEWAY="test_gateway" +SQLMESH__MODEL_DEFAULTS__DIALECT="postgres" +""" + ) + + # Test that without custom dotenv path, env vars are not loaded + with mock.patch.dict(os.environ, {}, clear=True): + with pytest.raises( + ConfigError, match=r"Default model SQL dialect is a required configuratio*" + ): + load_configs( + "config", + Config, + paths=[main_dir], + ) + + # Test that with custom dotenv path, env vars are loaded correctly + with mock.patch.dict(os.environ, {}, clear=True): + configs = load_configs( + "config", + Config, + paths=[main_dir], + dotenv_path=custom_env_path, + ) + + assert next(iter(configs.values())) == Config( + gateways={ + "test_gateway": GatewayConfig( + connection=DuckDBConnectionConfig( + database="custom_database.db", + ), + ), + }, + default_gateway="test_gateway", + model_defaults=ModelDefaultsConfig(dialect="postgres"), + ) From aeeaa62edbfa7aedbf0620b9d823dfa0702d2b17 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:16:19 +0300 Subject: [PATCH 0478/1056] Chore: Move dotenv is listed in dependencies (#4849) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e6d14129d2..8fd27b98f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "packaging", "pandas", "pydantic>=2.0.0", + "python-dotenv", "requests", "rich[jupyter]", "ruamel.yaml", @@ -79,7 +80,6 @@ dev = [ "PyAthena[Pandas]", "PyGithub>=2.6.0", "pyperf", - "python-dotenv", "pyspark~=3.5.0", "pytest", "pytest-asyncio", From d3bd90365c0a8d988e8e9b0269a22c0b21cf60e0 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 30 Jun 2025 09:07:13 -0700 Subject: [PATCH 0479/1056] Fix: Include the preview disclaimer in the backfill section of the plan explanation when appropriate (#4845) --- sqlmesh/core/plan/explainer.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/sqlmesh/core/plan/explainer.py b/sqlmesh/core/plan/explainer.py index 75aa0966ff..4d1ee2256d 100644 --- a/sqlmesh/core/plan/explainer.py +++ b/sqlmesh/core/plan/explainer.py @@ -161,7 +161,8 @@ def visit_backfill_stage(self, stage: stages.BackfillStage) -> Tree: for snapshot, intervals in stage.snapshot_to_intervals.items(): display_name = self._display_name(snapshot) if snapshot.is_model: - table_name = snapshot.table_name(stage.deployability_index.is_deployable(snapshot)) + is_deployable = stage.deployability_index.is_deployable(snapshot) + table_name = snapshot.table_name(is_deployable) model_tree = Tree(f"{display_name} -> {table_name}") for signal_name, _ in snapshot.model.signals: @@ -170,26 +171,30 @@ def visit_backfill_stage(self, stage: stages.BackfillStage) -> Tree: if snapshot.model.pre_statements: model_tree.add("Run pre-statements") + backfill_tree = Tree("Fully refresh table") if snapshot.is_incremental: current_intervals = ( snapshot.intervals if stage.deployability_index.is_deployable(snapshot) else snapshot.dev_intervals ) + # If there are no intervals, the table will be fully refreshed if current_intervals: formatted_range = SnapshotIntervals( snapshot_id=snapshot.snapshot_id, intervals=intervals ).format_intervals(snapshot.node.interval_unit) - model_tree.add( + backfill_tree = Tree( f"Incrementally insert records within the range [{formatted_range}]" ) - else: - # If there are no intervals, the table will be fully refreshed - model_tree.add("Fully refresh table") elif snapshot.is_view: - model_tree.add("Recreate view") - else: - model_tree.add("Fully refresh table") + backfill_tree = Tree("Recreate view") + + if not is_deployable: + backfill_tree.add( + "[orange1]preview[/orange1]: data will NOT be reused in production" + ) + + model_tree.add(backfill_tree) if snapshot.model.post_statements: model_tree.add("Run post-statements") From ba94635547d59a7d05f22207e620e647e127228f Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:30:17 -0500 Subject: [PATCH 0480/1056] Fix: MSSQL merge should handle all columns being unique keys (#4842) --- sqlmesh/core/engine_adapter/mssql.py | 32 +++++++++++++------------ tests/core/engine_adapter/test_mssql.py | 26 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index 200640f305..60fe99ffc4 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -212,6 +212,7 @@ def merge( if merge_filter: on = exp.and_(merge_filter, on) + match_expressions = [] if not when_matched: match_condition = None unique_key_names = [y.name for y in unique_key] @@ -230,23 +231,24 @@ def merge( ) ) - match_expressions = [ - exp.When( - matched=True, - source=False, - condition=match_condition, - then=exp.Update( - expressions=[ - exp.column(col, MERGE_TARGET_ALIAS).eq( - exp.column(col, MERGE_SOURCE_ALIAS) - ) - for col in columns_to_types_no_keys - ], - ), + if target_columns_no_keys: + match_expressions.append( + exp.When( + matched=True, + source=False, + condition=match_condition, + then=exp.Update( + expressions=[ + exp.column(col, MERGE_TARGET_ALIAS).eq( + exp.column(col, MERGE_SOURCE_ALIAS) + ) + for col in columns_to_types_no_keys + ], + ), + ) ) - ] else: - match_expressions = when_matched.copy().expressions + match_expressions.extend(when_matched.copy().expressions) match_expressions.append( exp.When( diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index d32343d593..eef0a320da 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -456,6 +456,8 @@ def test_merge_pandas( temp_table_mock.return_value = make_temp_table_name(table_name, temp_table_id) df = pd.DataFrame({"id": [1, 2, 3], "ts": [1, 2, 3], "val": [4, 5, 6]}) + + # 1 key adapter.merge( target_table=table_name, source_table=df, @@ -476,6 +478,7 @@ def test_merge_pandas( f"DROP TABLE IF EXISTS [__temp_target_{temp_table_id}];", ] + # 2 keys adapter.cursor.reset_mock() adapter._connection_pool.get().reset_mock() temp_table_mock.return_value = make_temp_table_name(table_name, temp_table_id) @@ -499,6 +502,29 @@ def test_merge_pandas( f"DROP TABLE IF EXISTS [__temp_target_{temp_table_id}];", ] + # all model columns are keys + adapter.cursor.reset_mock() + adapter._connection_pool.get().reset_mock() + temp_table_mock.return_value = make_temp_table_name(table_name, temp_table_id) + adapter.merge( + target_table=table_name, + source_table=df, + columns_to_types={ + "id": exp.DataType.build("int"), + "ts": exp.DataType.build("TIMESTAMP"), + }, + unique_key=[exp.to_identifier("id"), exp.to_column("ts")], + ) + adapter._connection_pool.get().bulk_copy.assert_called_with( + f"__temp_target_{temp_table_id}", [(1, 1), (2, 2), (3, 3)] + ) + + assert to_sql_calls(adapter) == [ + f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '__temp_target_{temp_table_id}') EXEC('CREATE TABLE [__temp_target_{temp_table_id}] ([id] INTEGER, [ts] DATETIME2)');""", + f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] AND [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts] WHEN NOT MATCHED THEN INSERT ([id], [ts]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts]);", + f"DROP TABLE IF EXISTS [__temp_target_{temp_table_id}];", + ] + def test_replace_query(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(MSSQLEngineAdapter) From 1696ca24b4b358744322cf8a678c93a7dfef0776 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:04:51 +0100 Subject: [PATCH 0481/1056] ci: use bigger image for e2e (#4850) --- .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 1d7f6ec8a3..ab763cf3b8 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -26,7 +26,8 @@ jobs: - name: Run CI run: pnpm run ci test-vscode-e2e: - runs-on: ubuntu-latest + runs-on: + labels: [ubuntu-2204-8] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 From 3c155a6b43af5919ce05d62ca479980d5714afad Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 30 Jun 2025 20:16:40 +0100 Subject: [PATCH 0482/1056] ci: move all e2e vscode tests to run in ci (#4841) --- .github/workflows/pr.yaml | 31 +- .prettierignore | 3 + vscode/extension/.gitignore | 4 +- vscode/extension/.vscodeignore | 4 +- vscode/extension/E2E_TESTING.md | 11 +- vscode/extension/package.json | 1 - vscode/extension/playwright.config.ts | 12 +- vscode/extension/tests/bad_setup.spec.ts | 80 ++-- vscode/extension/tests/broken_project.spec.ts | 102 +++-- vscode/extension/tests/completions.spec.ts | 128 +++--- vscode/extension/tests/diagnostics.spec.ts | 36 +- .../extension/tests/find_references.spec.ts | 398 +++++++++--------- vscode/extension/tests/format.spec.ts | 36 +- vscode/extension/tests/global-setup.ts | 55 +++ .../extension/tests/go_to_definition.spec.ts | 67 +-- vscode/extension/tests/hints.spec.ts | 31 +- vscode/extension/tests/lineage.spec.ts | 154 +++++-- vscode/extension/tests/python_env.spec.ts | 65 ++- vscode/extension/tests/rename_cte.spec.ts | 208 ++++----- vscode/extension/tests/render.spec.ts | 163 ++++--- vscode/extension/tests/stop.spec.ts | 22 +- vscode/extension/tests/tcloud.spec.ts | 120 +++--- vscode/extension/tests/utils.ts | 179 +++++--- vscode/extension/tests/utils_code_server.ts | 115 ++--- 24 files changed, 1137 insertions(+), 888 deletions(-) create mode 100644 vscode/extension/tests/global-setup.ts diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index ab763cf3b8..e6ec9f1803 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -42,15 +42,29 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.12' + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: + ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt', + '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Cache virtual environment + uses: actions/cache@v4 + with: + path: .venv + key: + ${{ runner.os }}-venv-${{ hashFiles('**/requirements*.txt', + '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-venv- - name: Install python dependencies run: | python -m venv .venv source .venv/bin/activate make install-dev - - name: Fetch VS Code - working-directory: ./vscode/extension - run: pnpm run fetch-vscode - - name: Install code-server run: curl -fsSL https://code-server.dev/install.sh | sh - name: Install Playwright browsers @@ -58,6 +72,13 @@ jobs: run: pnpm exec playwright install - name: Run e2e tests working-directory: ./vscode/extension + timeout-minutes: 90 run: | source ../../.venv/bin/activate - pnpm run test:e2e tests/stop.spec.ts + pnpm run test:e2e + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: vscode/extension/playwright-report/ + retention-days: 30 diff --git a/.prettierignore b/.prettierignore index 67d4b9aa77..78cf56de58 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,6 +20,9 @@ vscode/extension/out vscode/extension/src_react vscode/extension/tsconfig.tsbuildinfo vscode/extension/.vscode-test/ +vscode/extension/playwright-report/ +vscode/extension/test-results/ +vscode/extension/.test_setup sqlmesh docs diff --git a/vscode/extension/.gitignore b/vscode/extension/.gitignore index e9affdeaad..f54728090f 100644 --- a/vscode/extension/.gitignore +++ b/vscode/extension/.gitignore @@ -2,7 +2,9 @@ node_modules dist out .vscode-test +.test_setup *.vsix LICENSE src_react -!src_react/.gitkeep \ No newline at end of file +!src_react/.gitkeep +playwright-report \ No newline at end of file diff --git a/vscode/extension/.vscodeignore b/vscode/extension/.vscodeignore index e50f31046d..47b7075c62 100644 --- a/vscode/extension/.vscodeignore +++ b/vscode/extension/.vscodeignore @@ -24,4 +24,6 @@ tsconfig.test.json tsconfig.build.json src/test/** tests/** -.claude \ No newline at end of file +.claude +.idea +.test_setup \ No newline at end of file diff --git a/vscode/extension/E2E_TESTING.md b/vscode/extension/E2E_TESTING.md index 3c37d05b94..9ca24bbc6d 100644 --- a/vscode/extension/E2E_TESTING.md +++ b/vscode/extension/E2E_TESTING.md @@ -9,16 +9,9 @@ This directory contains end-to-end tests for the SQLMesh VS Code extension using pnpm install ``` -2. **Download VS Code executable (one-time setup):** +2. **Install Playwright browsers:** ```bash - pnpm run fetch-vscode - ``` - - This downloads VS Code and caches it in `.vscode-test/` directory. The paths are saved to `.vscode-test/paths.json` for Playwright to use. - -3. **Install Playwright browsers:** - ```bash - npx playwright install + pnpm run playwright:install ``` ## Running Tests diff --git a/vscode/extension/package.json b/vscode/extension/package.json index e8b7d5f00b..efa9d90240 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -124,7 +124,6 @@ "test:e2e": "pnpm run vscode:package && playwright test", "test:e2e:ui": "pnpm run vscode:package && playwright test --ui", "test:e2e:headed": "pnpm run vscode:package && playwright test --headed", - "fetch-vscode": "tsx scripts/fetch-vscode.ts", "compile": "pnpm run check-types && node esbuild.js", "check-types": "tsc --noEmit -p ./tsconfig.build.json", "watch": "node esbuild.js --watch", diff --git a/vscode/extension/playwright.config.ts b/vscode/extension/playwright.config.ts index 599e5e3738..527d699668 100644 --- a/vscode/extension/playwright.config.ts +++ b/vscode/extension/playwright.config.ts @@ -3,18 +3,22 @@ import { defineConfig } from '@playwright/test' export default defineConfig({ testDir: 'tests', timeout: 60_000, - retries: process.env.CI ? 1 : 0, - workers: 1, + // TODO: When stable, allow retries in CI + retries: process.env.CI ? 2 : 0, + workers: 4, + reporter: [['html', { outputFolder: 'playwright-report' }], ['list']], + globalSetup: './tests/global-setup.ts', projects: [ { name: 'electron-vscode', use: { - // ⭢ we'll launch Electron ourselves – no browser needed browserName: 'chromium', - headless: true, // headless mode for tests + headless: true, launchOptions: { slowMo: process.env.CI ? 0 : 100, }, + viewport: { width: 1512, height: 944 }, + video: 'retain-on-failure', }, }, ], diff --git a/vscode/extension/tests/bad_setup.spec.ts b/vscode/extension/tests/bad_setup.spec.ts index cf32718ea4..6977b1dfcb 100644 --- a/vscode/extension/tests/bad_setup.spec.ts +++ b/vscode/extension/tests/bad_setup.spec.ts @@ -4,14 +4,17 @@ import os from 'os' import path from 'path' import { createVirtualEnvironment, + openFile, openLineageView, pipInstall, REPO_ROOT, - startVSCode, SUSHI_SOURCE_PATH, } from './utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' -test('missing LSP dependencies shows install prompt', async ({}, testInfo) => { +test('missing LSP dependencies shows install prompt', async ({ + page, +}, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), @@ -26,6 +29,11 @@ test('missing LSP dependencies shows install prompt', async ({}, testInfo) => { const sqlmeshWithExtras = `${REPO_ROOT}[bigquery]` await pipInstall(pythonDetails, [sqlmeshWithExtras, custom_materializations]) + // Start VS Code + const context = await startCodeServer({ + tempDir, + }) + try { // Copy sushi project await fs.copy(SUSHI_SOURCE_PATH, tempDir) @@ -42,45 +50,45 @@ test('missing LSP dependencies shows install prompt', async ({}, testInfo) => { { spaces: 2 }, ) - // Start VS Code - const { window, close } = await startVSCode(tempDir) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Open a SQL file to trigger SQLMesh activation // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the top_waiters model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() // Wait for the message to show that LSP extras need to be installed - await window.waitForSelector('text=LSP dependencies missing') - expect(await window.locator('text=Install').count()).toBeGreaterThanOrEqual( - 1, - ) - - await close() + await page.waitForSelector('text=LSP dependencies missing') + expect(await page.locator('text=Install').count()).toBeGreaterThanOrEqual(1) } finally { - // Clean up - await fs.remove(tempDir) + await stopCodeServer(context) } }) -test('lineage, no sqlmesh found', async ({}) => { +test('lineage, no sqlmesh found', async ({ page }, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) const pythonEnvDir = path.join(tempDir, '.venv') const pythonDetails = await createVirtualEnvironment(pythonEnvDir) + const context = await startCodeServer({ + tempDir, + }) + try { // Copy sushi project await fs.copy(SUSHI_SOURCE_PATH, tempDir) @@ -97,25 +105,28 @@ test('lineage, no sqlmesh found', async ({}) => { { spaces: 2 }, ) - const { window, close } = await startVSCode(tempDir) + // navigate to code-server instance + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.waitForLoadState('networkidle') // Open lineage view - await openLineageView(window) + await openLineageView(page) // Assert shows that sqlmesh is not installed - await window.waitForSelector('text=SQLMesh LSP not found') - - await close() + await page.waitForSelector('text=SQLMesh LSP not found') } finally { // Clean up - await fs.remove(tempDir) + await stopCodeServer(context) } }) // Checks that if you have another file open like somewhere else, it still checks the workspace first for a successful context // it's very flaky but runs when debugging // - the typing in of the file name is very flaky -test('check that the LSP runs correctly by opening lineage when looking at another file before not in workspace', async ({}) => { +test.skip('check that the LSP runs correctly by opening lineage when looking at another file before not in workspace', async ({ + page, +}, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -133,7 +144,7 @@ test('check that the LSP runs correctly by opening lineage when looking at anoth // Configure VS Code settings to use our Python environment const settings = { 'python.defaultInterpreterPath': pythonDetails.pythonPath, - 'sqlmesh.environmentPath': pythonEnvDir, + 'sqlmesh.environmentPath': tempDir, } await fs.ensureDir(path.join(tempDir, '.vscode')) await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { @@ -148,19 +159,18 @@ test('check that the LSP runs correctly by opening lineage when looking at anoth await fs.ensureDir(path.dirname(sqlFile)) await fs.writeFile(sqlFile, 'SELECT 1') - const { window, close } = await startVSCode(tempDir) + const context = await startCodeServer({ + tempDir, + }) try { + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.waitForLoadState('networkidle') + // Open the SQL file from the other directory - await window.keyboard.press('Meta+P') - await window.waitForTimeout(100) - await window.keyboard.type(sqlFile.toString(), { delay: 10 }) - await window.waitForTimeout(100) - await window.keyboard.press('Enter') - await window.waitForTimeout(100) - - await window.waitForSelector('text=Loaded SQLMesh context') + await openFile(page, sqlFile) + + await page.waitForSelector('text=Loaded SQLMesh context') } finally { - await close() - await fs.remove(tempDir) + await stopCodeServer(context) } }) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index 6d49b3e80a..1f9955fd0e 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -2,9 +2,10 @@ import { test } from '@playwright/test' import fs from 'fs-extra' import os from 'os' import path from 'path' -import { openLineageView, startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { openLineageView, saveFile, SUSHI_SOURCE_PATH } from './utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' -test('bad project, double model', async ({}) => { +test('bad project, double model', async ({ page }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -22,43 +23,52 @@ test('bad project, double model', async ({}) => { customersSql, ) - const { window, close } = await startVSCode(tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) try { - await window.waitForSelector('text=models') + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - await window + await page.waitForSelector('text=models') + + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=Error creating context') + await page.waitForSelector('text=Error creating context') - await window.waitForTimeout(1000) + await page.waitForTimeout(500) } finally { - await close() - await fs.remove(tempDir) + await stopCodeServer(context) } }) -test('working project, then broken through adding double model, then refixed', async ({}) => { +test('working project, then broken through adding double model, then refixed', async ({ + page, +}) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const { window, close } = await startVSCode(tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) try { - // First, verify the project is working correctly - await window.waitForSelector('text=models') + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.waitForLoadState('networkidle') // Open the lineage view to confirm it loads properly - await openLineageView(window) - await window.waitForSelector('text=Loaded SQLMesh context') + await openLineageView(page) + await page.waitForSelector('text=Loaded SQLMesh context') // Read the customers.sql file const customersSql = await fs.readFile( @@ -73,17 +83,16 @@ test('working project, then broken through adding double model, then refixed', a ) // Open the customers model to trigger the error - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() // Save to refresh the context - await window.keyboard.press('Control+S') - await window.keyboard.press('Meta+S') + await saveFile(page) // Wait for the error to appear // TODO: Selector doesn't work in the linage view @@ -93,19 +102,17 @@ test('working project, then broken through adding double model, then refixed', a await fs.remove(path.join(tempDir, 'models', 'customers_duplicated.sql')) // Save again to refresh the context - await window.keyboard.press('Control+S') - await window.keyboard.press('Meta+S') + await saveFile(page) // Wait for the error to go away and context to reload // TODO: Selector doesn't work in the linage view - // await window.waitForSelector('text=raw.demographics') + // await page.waitForSelector('text=raw.demographics') } finally { - await close() - await fs.remove(tempDir) + await stopCodeServer(context) } }) -test('bad project, double model, then fixed', async ({}) => { +test('bad project, double model, then fixed', async ({ page }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -123,38 +130,42 @@ test('bad project, double model, then fixed', async ({}) => { customersSql, ) - const { window, close } = await startVSCode(tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) try { - await window.waitForSelector('text=models') + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + await page.waitForSelector('text=models') - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=Error creating context') + await page.waitForSelector('text=Error creating context') // Remove the duplicated model await fs.remove(path.join(tempDir, 'models', 'customers_duplicated.sql')) // Open the linage view - await openLineageView(window) + await openLineageView(page) // Wait for the error to go away // TODO: Selector doesn't work in the linage view - // await window.waitForSelector('text=raw.demographics') + // await page.waitForSelector('text=raw.demographics') } finally { - await close() - await fs.remove(tempDir) + await stopCodeServer(context) } }) -test('bad project, double model, check lineage', async ({}) => { +test('bad project, double model, check lineage', async ({ page }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -172,19 +183,22 @@ test('bad project, double model, check lineage', async ({}) => { customersSql, ) - const { window, close } = await startVSCode(tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) try { - await window.waitForSelector('text=models') + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.waitForLoadState('networkidle') // Open the lineage view - await openLineageView(window) + await openLineageView(page) - await window.waitForSelector('text=Error creating context') - await window.waitForSelector('text=Error:') + await page.waitForSelector('text=Error creating context') + await page.waitForSelector('text=Error:') - await window.waitForTimeout(1000) + await page.waitForTimeout(500) } finally { - await close() - await fs.remove(tempDir) + await stopCodeServer(context) } }) diff --git a/vscode/extension/tests/completions.spec.ts b/vscode/extension/tests/completions.spec.ts index 3c22e388f3..02ad2a2b79 100644 --- a/vscode/extension/tests/completions.spec.ts +++ b/vscode/extension/tests/completions.spec.ts @@ -2,163 +2,183 @@ import { test, expect } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { SUSHI_SOURCE_PATH } from './utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' -test('Autocomplete for model names', async () => { +test('Autocomplete for model names', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + try { - const { window, close } = await startVSCode(tempDir) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the top_waiters model - await window + await page .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') - await window.locator('text=grain').first().click() + await page.locator('text=grain').first().click() // Move to the end of the file - await window.keyboard.press('Control+End') + for (let i = 0; i < 100; i++) { + await page.keyboard.press('ArrowDown') + } // Add a new line - await window.keyboard.press('Enter') + await page.keyboard.press('Enter') // Type the beginning of sushi.customers to trigger autocomplete - await window.keyboard.type('sushi.waiter_as_customer') + await page.keyboard.type('sushi.waiter_as_customer') // Wait a moment for autocomplete to appear - await window.waitForTimeout(500) + await page.waitForTimeout(500) // Check if the autocomplete suggestion for sushi.customers is visible expect( - await window.locator('text=sushi.waiter_as_customer_by_day').count(), - ).toBe(1) - expect(await window.locator('text=SQLMesh Model').count()).toBe(1) - - await close() + await page.locator('text=sushi.waiter_as_customer_by_day').count(), + ).toBeGreaterThanOrEqual(1) + expect( + await page.locator('text=SQLMesh Model').count(), + ).toBeGreaterThanOrEqual(1) } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) // Skip the macro completions test as regular checks because they are flaky and // covered in other non-integration tests. test.describe('Macro Completions', () => { - test('Completion for inbuilt macros', async () => { + test('Completion for inbuilt macros', async ({ page }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-sushi-'), ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + try { - const { window, close } = await startVSCode(tempDir) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the top_waiters model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') - await window.locator('text=grain').first().click() + await page.locator('text=grain').first().click() // Move to the end of the file - await window.keyboard.press('Control+End') + for (let i = 0; i < 100; i++) { + await page.keyboard.press('ArrowDown') + } // Add a new line - await window.keyboard.press('Enter') + await page.keyboard.press('Enter') - await window.waitForTimeout(500) + await page.waitForTimeout(500) // Hit the '@' key to trigger autocomplete for inbuilt macros - await window.keyboard.press('@') - await window.keyboard.type('eac') + await page.keyboard.press('@') + await page.keyboard.type('eac') // Wait a moment for autocomplete to appear - await window.waitForTimeout(500) + await page.waitForTimeout(500) // Check if the autocomplete suggestion for inbuilt macros is visible - expect(await window.locator('text=@each').count()).toBe(1) - - await close() + expect(await page.locator('text=@each').count()).toBeGreaterThanOrEqual(1) } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) - test('Completion for custom macros', async () => { + test('Completion for custom macros', async ({ page }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-sushi-'), ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + try { - const { window, close } = await startVSCode(tempDir) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the top_waiters model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') - await window.locator('text=grain').first().click() + await page.locator('text=grain').first().click() // Move to the end of the file - await window.keyboard.press('Control+End') + for (let i = 0; i < 100; i++) { + await page.keyboard.press('ArrowDown') + } // Add a new line - await window.keyboard.press('Enter') + await page.keyboard.press('Enter') // Type the beginning of a macro to trigger autocomplete - await window.keyboard.press('@') - await window.keyboard.type('add_o') + await page.keyboard.press('@') + await page.keyboard.type('add_o') // Wait a moment for autocomplete to appear - await window.waitForTimeout(500) + await page.waitForTimeout(500) // Check if the autocomplete suggestion for custom macros is visible - expect(await window.locator('text=@add_one').count()).toBe(1) - - await close() + expect( + await page.locator('text=@add_one').count(), + ).toBeGreaterThanOrEqual(1) } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) }) diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts index 8fe02058ff..4566102d95 100644 --- a/vscode/extension/tests/diagnostics.spec.ts +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -2,47 +2,49 @@ import { test } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { runCommand, SUSHI_SOURCE_PATH } from './utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' -test('Workspace diagnostics show up in the diagnostics panel', async () => { +test('Workspace diagnostics show up in the diagnostics panel', async ({ + page, +}) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + const configPath = path.join(tempDir, 'config.py') const configContent = await fs.readFile(configPath, 'utf8') const updatedContent = configContent.replace('enabled=False', 'enabled=True') await fs.writeFile(configPath, updatedContent) try { - const { window, close } = await startVSCode(tempDir) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder, excluding external_models - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the customer_revenue_lifetime model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await // Open problems panel - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('View: Focus Problems') - await window.keyboard.press('Enter') - - await window.waitForSelector('text=problems') - await window.waitForSelector('text=All models should have an owner') + // Open problems panel + await runCommand(page, 'View: Focus Problems') - await close() + await page.waitForSelector('text=problems') + await page.waitForSelector('text=All models should have an owner') } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) diff --git a/vscode/extension/tests/find_references.spec.ts b/vscode/extension/tests/find_references.spec.ts index 605eae8414..f7880094ca 100644 --- a/vscode/extension/tests/find_references.spec.ts +++ b/vscode/extension/tests/find_references.spec.ts @@ -1,81 +1,86 @@ -import { test, expect } from '@playwright/test' +import { test, expect, Page } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { startVSCode, SUSHI_SOURCE_PATH } from './utils' - -// Consistent keyboard shortcuts -const GO_TO_REFERENCES_KEY = 'Shift+F12' -const FIND_ALL_REFERENCES_KEY = - process.platform === 'darwin' ? 'Alt+Shift+F12' : 'Ctrl+Shift+F12' +import { findAllReferences, goToReferences, SUSHI_SOURCE_PATH } from './utils' +import { + startCodeServer, + stopCodeServer, + CodeServerContext, +} from './utils_code_server' // Helper function to set up a test environment for model references -async function setupModelTestEnvironment() { +async function setupModelTestEnvironment(): Promise { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const { window, close } = await startVSCode(tempDir) - return { window, close, tempDir } + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + return context } // Helper function to navigate to models folder -async function navigateToModels(window: any) { - await window.waitForSelector('text=models') - await window +async function navigateToModels(page: Page) { + await page.waitForSelector('text=models') + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() } // Helper function to navigate to audits folder -async function navigateToAudits(window: any) { - await window.waitForSelector('text=audits') - await window +async function navigateToAudits(page: Page) { + await page.waitForSelector('text=audits') + await page .getByRole('treeitem', { name: 'audits', exact: true }) .locator('a') .click() } // Helper function to open customers.sql and wait for SQLMesh context -async function openCustomersFile(window: any) { - await navigateToModels(window) - await window +async function openCustomersFile(page: Page) { + await navigateToModels(page) + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') } // Helper function to open top_waiters.sql and wait for SQLMesh context -async function openTopWaitersFile(window: any) { - await navigateToModels(window) - await window +async function openTopWaitersFile(page: Page) { + await navigateToModels(page) + await page .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') } test.describe('Model References', () => { - test('Go to References (Shift+F12) for Model usage', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Go to References (Shift+F12) for Model usage', async ({ page }) => { + const context = await setupModelTestEnvironment() try { + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + // Open customers.sql which contains references to other models - await openCustomersFile(window) + await openCustomersFile(page) // Step 4: Position cursor on the sushi.orders model reference in the SQL query - await window.locator('text=sushi.orders').first().click() + await page.locator('text=sushi.orders').first().click() // Step 5: Trigger "Go to References" command using Shift+F12 keyboard shortcut - await window.keyboard.press(GO_TO_REFERENCES_KEY) + await goToReferences(page) // Step 6: Wait for VSCode references panel to appear at the bottom - await window.waitForSelector('text=References') + await page.waitForSelector('text=References') // Step 7: Ensure references panel has populated with all usages of sushi.orders model - await window.waitForFunction( + await page.waitForFunction( () => { const referenceElements = document.querySelectorAll( '.reference-item, .monaco-list-row, .references-view .tree-row', @@ -86,7 +91,7 @@ test.describe('Model References', () => { ) // Step 8: Verify the references panel shows both SQL and Python files containing references - const hasReferences = await window.evaluate(() => { + const hasReferences = await page.evaluate(() => { const body = document.body.textContent || '' return ( body.includes('References') && @@ -99,7 +104,7 @@ test.describe('Model References', () => { // Step 9: Find and click on the orders.py reference to navigate to the model definition let clickedReference = false - const referenceItems = window.locator( + const referenceItems = page.locator( '.monaco-list-row, .reference-item, .monaco-tl-row', ) const count = await referenceItems.count() @@ -119,28 +124,29 @@ test.describe('Model References', () => { expect(clickedReference).toBe(true) // Step 10: Verify successful navigation to orders.py by checking for unique Python code - await expect(window.locator('text=list(range(0, 100))')).toBeVisible() + await expect(page.locator('text=list(range(0, 100))')).toBeVisible() } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) - test('Find All References (Alt+Shift+F12) for Model', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Find All References (Alt+Shift+F12) for Model', async ({ page }) => { + const context = await setupModelTestEnvironment() try { + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + // Open customers.sql which contains multiple model references - await openCustomersFile(window) + await openCustomersFile(page) // Step 4: Click on sushi.orders model reference to position cursor - await window.locator('text=sushi.orders').first().click() + await page.locator('text=sushi.orders').first().click() - // Step 5: Trigger "Find All References" command using Alt+Shift+F12 (or Ctrl+Shift+F12 on Windows/Linux) - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + // Step 5: Trigger "Find All References" command using Alt+Shift+F12 (or +Shift+F12 on Windows/Linux) + await findAllReferences(page) let clickedReference = false - const referenceItems = window.locator( + const referenceItems = page.locator( '.monaco-list-row, .reference-item, .monaco-tl-row', ) const count = await referenceItems.count() @@ -162,26 +168,27 @@ test.describe('Model References', () => { expect(clickedReference).toBe(true) // Step 7: Verify navigation to orders.py by checking for Python import statement - await expect(window.locator('text=import random')).toBeVisible() + await expect(page.locator('text=import random')).toBeVisible() // Step 8: Click on the import statement to ensure file is fully loaded and interactive - await window.locator('text=import random').first().click() + await page.locator('text=import random').first().click() // Step 9: Final verification that we're viewing the correct Python model file - await expect(window.locator('text=list(range(0, 100))')).toBeVisible() + await expect(page.locator('text=list(range(0, 100))')).toBeVisible() } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) - test('Go to References for Model from Audit', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Go to References for Model from Audit', async ({ page }) => { + const context = await setupModelTestEnvironment() try { + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + // Open assert_item_price_above_zero.sql audit file which references sushi.items model - await navigateToAudits(window) - await window + await navigateToAudits(page) + await page .getByRole('treeitem', { name: 'assert_item_price_above_zero.sql', exact: true, @@ -190,20 +197,20 @@ test.describe('Model References', () => { .click() // Wait for audit file to load and SQLMesh context to initialize - await window.waitForSelector('text=standalone') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=standalone') + await page.waitForSelector('text=Loaded SQLMesh Context') // Step 4: Click on sushi.items model reference in the audit query - await window.locator('text=sushi.items').first().click() + await page.locator('text=sushi.items').first().click() // Step 5: Trigger "Go to References" to find all places where sushi.items is used - await window.keyboard.press(GO_TO_REFERENCES_KEY) + await goToReferences(page) // Step 6: Wait for VSCode references panel to appear - await window.waitForSelector('text=References') + await page.waitForSelector('text=References') // Step 7: Ensure references panel shows multiple files that reference sushi.items - await window.waitForFunction( + await page.waitForFunction( () => { const referenceElements = document.querySelectorAll( '.reference-item, .monaco-list-row, .references-view .tree-row', @@ -214,7 +221,7 @@ test.describe('Model References', () => { ) // Step 8: Verify references panel contains both audit and model files - const hasReferences = await window.evaluate(() => { + const hasReferences = await page.evaluate(() => { const body = document.body.textContent || '' return ( body.includes('References') && @@ -227,7 +234,7 @@ test.describe('Model References', () => { // 9. Click on one of the references to navigate to it let clickedReference = false - const referenceItems = window.locator( + const referenceItems = page.locator( '.monaco-list-row, .reference-item, .monaco-tl-row', ) const count = await referenceItems.count() @@ -247,26 +254,27 @@ test.describe('Model References', () => { expect(clickedReference).toBe(true) // Step 10: Verify navigation to customer_revenue_by_day.sql by checking for SQL JOIN syntax - await expect(window.locator('text=LEFT JOIN')).toBeVisible() + await expect(page.locator('text=LEFT JOIN')).toBeVisible() // Step 11: Click on LEFT JOIN to ensure file is interactive and verify content - await window.locator('text=LEFT JOIN').first().click() + await page.locator('text=LEFT JOIN').first().click() await expect( - window.locator('text=FROM sushi.order_items AS oi'), + page.locator('text=FROM sushi.order_items AS oi'), ).toBeVisible() } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) - test('Find All Model References from Audit', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test.skip('Find All Model References from Audit', async ({ page }) => { + const context = await setupModelTestEnvironment() try { + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + // Open the audit file that validates item prices - await navigateToAudits(window) - await window + await navigateToAudits(page) + await page .getByRole('treeitem', { name: 'assert_item_price_above_zero.sql', exact: true, @@ -275,19 +283,19 @@ test.describe('Model References', () => { .click() // Ensure audit file and SQLMesh context are fully loaded - await window.waitForSelector('text=standalone') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=standalone') + await page.waitForSelector('text=Loaded SQLMesh Context') // Step 4: Position cursor on sushi.items model reference - await window.locator('text=sushi.items').first().click() + await page.locator('text=sushi.items').first().click() // Step 5: Use Find All References to see all occurrences across the project - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + await findAllReferences(page) // Step 6: Click on a reference to navigate to customer_revenue_by_day.sql let clickedReference = false - const referenceItems = window.locator( + const referenceItems = page.locator( '.monaco-list-row, .reference-item, .monaco-tl-row', ) const count = await referenceItems.count() @@ -307,38 +315,39 @@ test.describe('Model References', () => { expect(clickedReference).toBe(true) // Step 7: Verify successful navigation by checking for SQL JOIN statement - await expect(window.locator('text=LEFT JOIN')).toBeVisible() + await expect(page.locator('text=LEFT JOIN')).toBeVisible() // Step 8: Interact with the file to verify it's fully loaded and check its content - await window.locator('text=LEFT JOIN').first().click() + await page.locator('text=LEFT JOIN').first().click() await expect( - window.locator('text=FROM sushi.order_items AS oi'), + page.locator('text=FROM sushi.order_items AS oi'), ).toBeVisible() } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) }) test.describe('CTE References', () => { - test('Go to references from definition of CTE', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Go to references from definition of CTE', async ({ page }) => { + const context = await setupModelTestEnvironment() try { - await openCustomersFile(window) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + await openCustomersFile(page) // Click on the CTE definition "current_marketing_outer" at line 20 to position cursor - await window.locator('text=current_marketing_outer').first().click() + await page.locator('text=current_marketing_outer').first().click() // Use keyboard shortcut to find all references - await window.keyboard.press(GO_TO_REFERENCES_KEY) + await goToReferences(page) // Wait for the references to appear - await window.waitForSelector('text=References') + await page.waitForSelector('text=References') // Wait for reference panel to populate - await window.waitForFunction( + await page.waitForFunction( () => { const referenceElements = document.querySelectorAll( '.reference-item, .monaco-list-row, .references-view .tree-row', @@ -349,37 +358,38 @@ test.describe('CTE References', () => { ) // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() + await expect(page.locator('text=customers.sql').first()).toBeVisible() // Check that both CTE definition and usage are listed in references - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing_outer AS') - await window.waitForSelector('text=FROM current_marketing_outer') + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing_outer AS') + await page.waitForSelector('text=FROM current_marketing_outer') } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) - test('Go to references from usage of CTE', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Go to references from usage of CTE', async ({ page }) => { + const context = await setupModelTestEnvironment() try { - await openCustomersFile(window) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + await openCustomersFile(page) // Click on the CTE usage this time for "current_marketing_outer" - await window.locator('text=FROM current_marketing_outer').click({ + await page.locator('text=FROM current_marketing_outer').click({ position: { x: 80, y: 5 }, // Clicks on the usage rather than first which was definition }) // Use keyboard shortcut to go to references - await window.keyboard.press(GO_TO_REFERENCES_KEY) + await goToReferences(page) // Wait for the references to appear - await window.waitForSelector('text=References') + await page.waitForSelector('text=References') // Better assertions: wait for reference panel to populate - await window.waitForFunction( + await page.waitForFunction( () => { const referenceElements = document.querySelectorAll( '.reference-item, .monaco-list-row, .references-view .tree-row', @@ -389,37 +399,38 @@ test.describe('CTE References', () => { { timeout: 5000 }, ) - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing_outer AS') - await window.waitForSelector('text=FROM current_marketing_outer') + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing_outer AS') + await page.waitForSelector('text=FROM current_marketing_outer') // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() + await expect(page.locator('text=customers.sql').first()).toBeVisible() } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) - test('Go to references for nested CTE', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Go to references for nested CTE', async ({ page }) => { + const context = await setupModelTestEnvironment() try { - await openCustomersFile(window) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + await openCustomersFile(page) // Click on the nested CTE "current_marketing" - await window.locator('text=WITH current_marketing AS').click({ + await page.locator('text=WITH current_marketing AS').click({ position: { x: 100, y: 5 }, // Click on the CTE name part }) // Use keyboard shortcut to find all references - await window.keyboard.press(GO_TO_REFERENCES_KEY) + await goToReferences(page) // Wait for the references to appear - await window.waitForSelector('text=References') + await page.waitForSelector('text=References') // Wait for reference panel to populate - await window.waitForFunction( + await page.waitForFunction( () => { const referenceElements = document.querySelectorAll( '.reference-item, .monaco-list-row, .references-view .tree-row', @@ -430,115 +441,119 @@ test.describe('CTE References', () => { ) // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() + await expect(page.locator('text=customers.sql').first()).toBeVisible() // Check that both CTE definition and usage are listed in references - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing AS') - await window.waitForSelector('text=FROM current_marketing') + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing AS') + await page.waitForSelector('text=FROM current_marketing') } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) - test('Find all references for CTE', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Find all references for CTE', async ({ page }) => { + const context = await setupModelTestEnvironment() try { - await openCustomersFile(window) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + await openCustomersFile(page) // Click on the CTE definition "current_marketing_outer" - await window.locator('text=current_marketing_outer').first().click() + await page.locator('text=current_marketing_outer').first().click() // Use keyboard shortcut to find all references - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + await findAllReferences(page) // Verify references contains expected content - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing_outer AS') - await window.waitForSelector('text=FROM current_marketing_outer') + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing_outer AS') + await page.waitForSelector('text=FROM current_marketing_outer') // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() + await expect(page.locator('text=customers.sql').first()).toBeVisible() } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) - test('Find all references from usage for CTE', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Find all references from usage for CTE', async ({ page }) => { + const context = await setupModelTestEnvironment() try { - await openCustomersFile(window) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + await openCustomersFile(page) // Click on the CTE usage of "current_marketing_outer" using last - await window.locator('text=current_marketing_outer').last().click() + await page.locator('text=current_marketing_outer').last().click() // Use keyboard shortcut to find all references - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + await findAllReferences(page) // Verify references contains expected content - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing_outer AS') - await window.waitForSelector('text=FROM current_marketing_outer') + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing_outer AS') + await page.waitForSelector('text=FROM current_marketing_outer') // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() + await expect(page.locator('text=customers.sql').first()).toBeVisible() } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) - test('Find all references for nested CTE', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Find all references for nested CTE', async ({ page }) => { + const context = await setupModelTestEnvironment() try { - await openCustomersFile(window) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + await openCustomersFile(page) // Click on the nested CTE "current_marketing" at line 33 // We need to be more specific to get the inner one - await window.locator('text=WITH current_marketing AS').click({ + await page.locator('text=WITH current_marketing AS').click({ position: { x: 100, y: 5 }, // Click on the CTE name part }) // Use keyboard shortcut to find all references - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + await findAllReferences(page) // Verify references contains expected content - await window.waitForSelector('text=References') - await window.waitForSelector('text=WITH current_marketing AS') - await window.waitForSelector('text=FROM current_marketing') + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing AS') + await page.waitForSelector('text=FROM current_marketing') // Verify that the customers.sql file is shown in results - await expect(window.locator('text=customers.sql').first()).toBeVisible() + await expect(page.locator('text=customers.sql').first()).toBeVisible() } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) }) test.describe('Macro References', () => { - test('Go to References for @ADD_ONE macro', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Go to References for @ADD_ONE macro', async ({ page }) => { + const context = await setupModelTestEnvironment() try { - await openTopWaitersFile(window) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + await openTopWaitersFile(page) // Click on the @ADD_ONE macro usage - await window.locator('text=@ADD_ONE').first().click() + await page.locator('text=@ADD_ONE').first().click() // Use keyboard shortcut to find all references - await window.keyboard.press(GO_TO_REFERENCES_KEY) + await goToReferences(page) // Wait for the references to appear - await window.waitForSelector('text=References') + await page.waitForSelector('text=References') // Wait for reference panel to populate - await window.waitForFunction( + await page.waitForFunction( () => { const referenceElements = document.querySelectorAll( '.reference-item, .monaco-list-row, .references-view .tree-row', @@ -549,37 +564,38 @@ test.describe('Macro References', () => { ) // Verify that both the definition and two usages are shown - await expect(window.locator('text=utils.py').first()).toBeVisible() - await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() - await expect(window.locator('text=customers.sql').first()).toBeVisible() + await expect(page.locator('text=utils.py').first()).toBeVisible() + await expect(page.locator('text=top_waiters.sql').first()).toBeVisible() + await expect(page.locator('text=customers.sql').first()).toBeVisible() } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) - test('Find All References for @MULTIPLY macro', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Find All References for @MULTIPLY macro', async ({ page }) => { + const context = await setupModelTestEnvironment() try { - await openTopWaitersFile(window) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + await openTopWaitersFile(page) // Click on the @MULTIPLY macro usage and then navigate to it - await window.locator('text=@MULTIPLY').first().click() + await page.locator('text=@MULTIPLY').first().click() // Use keyboard shortcut to find all references - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + await findAllReferences(page) // Verify references contains expected content - await window.waitForSelector('text=References') + await page.waitForSelector('text=References') // Verify that both utils.py (definition) and top_waiters.sql (usage) are shown - await expect(window.locator('text=utils.py').first()).toBeVisible() - await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() + await expect(page.locator('text=utils.py').first()).toBeVisible() + await expect(page.locator('text=top_waiters.sql').first()).toBeVisible() // Click on the utils.py reference to navigate to the macro definition let clickedReference = false - const referenceItems = window.locator( + const referenceItems = page.locator( '.monaco-list-row, .reference-item, .monaco-tl-row', ) const count = await referenceItems.count() @@ -599,36 +615,37 @@ test.describe('Macro References', () => { expect(clickedReference).toBe(true) // Verify it appeared and click on it - await expect(window.locator('text=def multiply')).toBeVisible() - await window.locator('text=def multiply').first().click() + await expect(page.locator('text=def multiply')).toBeVisible() + await page.locator('text=def multiply').first().click() // Verify navigation to utils.py by checking the import that appears there await expect( - window.locator('text=from sqlmesh import SQL, macro'), + page.locator('text=from sqlmesh import SQL, macro'), ).toBeVisible() } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) - test('Go to References for @SQL_LITERAL macro', async () => { - const { window, close, tempDir } = await setupModelTestEnvironment() + test('Go to References for @SQL_LITERAL macro', async ({ page }) => { + const context = await setupModelTestEnvironment() try { - await openTopWaitersFile(window) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + await openTopWaitersFile(page) // Click on the @SQL_LITERAL macro usage - await window.locator('text=@SQL_LITERAL').first().click() + await page.locator('text=@SQL_LITERAL').first().click() // Use keyboard shortcut to find references - await window.keyboard.press(GO_TO_REFERENCES_KEY) + await goToReferences(page) // Wait for the references to appear - await window.waitForSelector('text=References') + await page.waitForSelector('text=References') // Wait for reference panel to populate - await window.waitForFunction( + await page.waitForFunction( () => { const referenceElements = document.querySelectorAll( '.reference-item, .monaco-list-row, .references-view .tree-row', @@ -639,7 +656,7 @@ test.describe('Macro References', () => { ) // Verify that references include both definition and usage - const hasReferences = await window.evaluate(() => { + const hasReferences = await page.evaluate(() => { const body = document.body.textContent || '' return ( body.includes('References') && @@ -650,11 +667,10 @@ test.describe('Macro References', () => { expect(hasReferences).toBe(true) - await expect(window.locator('text=utils.py').first()).toBeVisible() - await expect(window.locator('text=top_waiters.sql').first()).toBeVisible() + await expect(page.locator('text=utils.py').first()).toBeVisible() + await expect(page.locator('text=top_waiters.sql').first()).toBeVisible() } finally { - await close() - fs.removeSync(tempDir) + await stopCodeServer(context) } }) }) diff --git a/vscode/extension/tests/format.spec.ts b/vscode/extension/tests/format.spec.ts index ad85340aa3..3f979d8222 100644 --- a/vscode/extension/tests/format.spec.ts +++ b/vscode/extension/tests/format.spec.ts @@ -2,47 +2,47 @@ import { test, expect } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { runCommand, SUSHI_SOURCE_PATH } from './utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' -test('Format project works correctly', async () => { +test('Format project works correctly', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + try { - const { window, close } = await startVSCode(tempDir) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder, excluding external_models - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the customer_revenue_lifetime model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') - // Render the model - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('Format SQLMesh Project') - await window.keyboard.press('Enter') + // Format the project + await runCommand(page, 'SQLMesh: Format Project') // Check that the notification appears saying 'Project formatted successfully' await expect( - window.getByText('Project formatted successfully', { exact: true }), + page.getByText('Project formatted successfully', { exact: true }), ).toBeVisible() - - await close() } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) diff --git a/vscode/extension/tests/global-setup.ts b/vscode/extension/tests/global-setup.ts new file mode 100644 index 0000000000..ff0013dea1 --- /dev/null +++ b/vscode/extension/tests/global-setup.ts @@ -0,0 +1,55 @@ +import { execSync } from 'child_process' +import path from 'path' +import fs from 'fs-extra' + +async function globalSetup() { + console.log('Setting up extension for Playwright tests...') + + const extensionDir = path.join(__dirname, '..') + const testSetupDir = path.join(extensionDir, '.test_setup') + const extensionsDir = path.join(testSetupDir, 'extensions') + + // Clean up any existing test setup directory + await fs.remove(testSetupDir) + await fs.ensureDir(extensionsDir) + + // Get the extension version from package.json + const packageJson = JSON.parse( + fs.readFileSync(path.join(extensionDir, 'package.json'), 'utf-8'), + ) + const version = packageJson.version + const extensionName = packageJson.name || 'sqlmesh' + + // Look for the specific version .vsix file + const vsixFileName = `${extensionName}-${version}.vsix` + const vsixPath = path.join(extensionDir, vsixFileName) + + if (!fs.existsSync(vsixPath)) { + throw new Error( + `Extension file ${vsixFileName} not found. Run "pnpm run vscode:package" first.`, + ) + } + + console.log(`Installing extension: ${vsixFileName}`) + + // Create a temporary user data directory for the installation + const tempUserDataDir = await fs.mkdtemp( + path.join(require('os').tmpdir(), 'vscode-test-install-user-data-'), + ) + + try { + execSync( + `pnpm run code-server --user-data-dir "${tempUserDataDir}" --extensions-dir "${extensionsDir}" --install-extension "${vsixPath}"`, + { + stdio: 'inherit', + cwd: extensionDir, + }, + ) + console.log('Extension installed successfully to .test_setup/extensions') + } finally { + // Clean up temporary user data directory + await fs.remove(tempUserDataDir) + } +} + +export default globalSetup diff --git a/vscode/extension/tests/go_to_definition.spec.ts b/vscode/extension/tests/go_to_definition.spec.ts index e634689843..241b2062df 100644 --- a/vscode/extension/tests/go_to_definition.spec.ts +++ b/vscode/extension/tests/go_to_definition.spec.ts @@ -2,84 +2,89 @@ import { test, expect } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { goToDefinition, SUSHI_SOURCE_PATH } from './utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' -test('Go to definition for macro', async () => { +test('Stop server works', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + try { - const { window, close } = await startVSCode(tempDir) + // Navigate to code-server instance + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the customer_revenue_lifetime model - await window + await page .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') // Render the model - await window.locator('text=@MULTIPLY').click({ - modifiers: ['Meta'], - }) + await page.locator('text=@MULTIPLY').click() + await goToDefinition(page) // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window - await expect(window.locator('text=def multiply(')).toBeVisible() - - await close() + await expect(page.locator('text=def multiply(')).toBeVisible() } finally { - await fs.removeSync(tempDir) + await stopCodeServer(context) } }) -test('Go to definition for model', async () => { +test('Go to definition for model', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + try { - const { window, close } = await startVSCode(tempDir) + // Navigate to code-server instance + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the top_waiters model - await window + await page .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') // Go to definition for the model - await window - .locator('text=sushi.waiter_revenue_by_day') - .first() - .click({ - modifiers: ['Meta'], - }) + await page.locator('text=sushi.waiter_revenue_by_day').first().click() + await goToDefinition(page) await expect( - window.locator('text=SUM(oi.quantity * i.price)::DOUBLE AS revenue'), + page.locator('text=SUM(oi.quantity * i.price)::DOUBLE AS revenue'), ).toBeVisible() - await close() } finally { - await fs.removeSync(tempDir) + await stopCodeServer(context) } }) diff --git a/vscode/extension/tests/hints.spec.ts b/vscode/extension/tests/hints.spec.ts index d08dd43d05..6486e1bba6 100644 --- a/vscode/extension/tests/hints.spec.ts +++ b/vscode/extension/tests/hints.spec.ts @@ -2,26 +2,33 @@ import { test, expect } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { SUSHI_SOURCE_PATH } from './utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' -test('Model type hinting', async () => { +test('Model type hinting', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + try { - const { window, close } = await startVSCode(tempDir) + // Navigate to code-server instance + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the customers_revenue_by_day model - await window + await page .getByRole('treeitem', { name: 'customer_revenue_by_day.sql', exact: true, @@ -29,17 +36,15 @@ test('Model type hinting', async () => { .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') // Wait a moment for hints to appear - await window.waitForTimeout(500) + await page.waitForTimeout(500) // Check if the hint is visible - expect(await window.locator('text="country code"::INT').count()).toBe(1) - - await close() + expect(await page.locator('text="country code"::INT').count()).toBe(1) } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index a75407802f..0ff7a10140 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -2,41 +2,55 @@ import { test, expect, Page } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { openLineageView, startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { openLineageView, SUSHI_SOURCE_PATH } from './utils' import { writeFileSync } from 'fs' +import { startCodeServer, stopCodeServer } from './utils_code_server' /** * Helper function to launch VS Code and test lineage with given project path config */ -async function testLineageWithProjectPath(window: Page): Promise { - await openLineageView(window) - - // Wait for "Loaded SQLMesh context" text to appear - const loadedContextText = window.locator('text=Loaded SQLMesh context') - await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 }) +async function testLineageWithProjectPath(page: Page): Promise { + await page.waitForLoadState('networkidle') + await page.waitForLoadState('domcontentloaded') + await openLineageView(page) + await page.waitForSelector('text=Loaded SQLMesh context') } -test('Lineage panel renders correctly - no project path config (default)', async () => { +test('Lineage panel renders correctly - no project path config (default)', async ({ + page, +}) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + try { - const { window, close } = await startVSCode(tempDir) - await testLineageWithProjectPath(window) - await close() + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await testLineageWithProjectPath(page) } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) -test('Lineage panel renders correctly - relative project path', async () => { +test('Lineage panel renders correctly - relative project path', async ({ + page, +}) => { const workspaceDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), ) const projectDir = path.join(workspaceDir, 'projects', 'sushi') await fs.copy(SUSHI_SOURCE_PATH, projectDir) + const context = await startCodeServer({ + tempDir: workspaceDir, + }) + const settings = { 'sqlmesh.projectPath': './projects/sushi', + 'python.defaultInterpreterPath': context.defaultPythonInterpreter, } await fs.ensureDir(path.join(workspaceDir, '.vscode')) await fs.writeJson( @@ -46,15 +60,16 @@ test('Lineage panel renders correctly - relative project path', async () => { ) try { - const { window, close } = await startVSCode(workspaceDir) - await testLineageWithProjectPath(window) - await close() + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await testLineageWithProjectPath(page) } finally { await fs.remove(workspaceDir) } }) -test('Lineage panel renders correctly - absolute project path', async () => { +test('Lineage panel renders correctly - absolute project path', async ({ + page, +}) => { const workspaceDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), ) @@ -62,8 +77,13 @@ test('Lineage panel renders correctly - absolute project path', async () => { await fs.ensureDir(path.join(workspaceDir, '.vscode')) await fs.copy(SUSHI_SOURCE_PATH, projectDir) await fs.ensureDir(path.join(workspaceDir, '.vscode')) + const context = await startCodeServer({ + tempDir: workspaceDir, + }) + const settings = { 'sqlmesh.projectPath': projectDir, + 'python.defaultInterpreterPath': context.defaultPythonInterpreter, } await fs.writeJson( path.join(workspaceDir, '.vscode', 'settings.json'), @@ -72,15 +92,16 @@ test('Lineage panel renders correctly - absolute project path', async () => { ) try { - const { window, close } = await startVSCode(workspaceDir) - await testLineageWithProjectPath(window) - await close() + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await testLineageWithProjectPath(page) } finally { - await fs.remove(workspaceDir) + await stopCodeServer(context) } }) -test('Lineage panel renders correctly - relative project outside of workspace', async () => { +test('Lineage panel renders correctly - relative project outside of workspace', async ({ + page, +}) => { const tempFolder = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), ) @@ -89,9 +110,13 @@ test('Lineage panel renders correctly - relative project outside of workspace', const workspaceDir = path.join(tempFolder, 'workspace') await fs.ensureDir(workspaceDir) + const context = await startCodeServer({ + tempDir: workspaceDir, + }) const settings = { 'sqlmesh.projectPath': './../projects/sushi', + 'python.defaultInterpreterPath': context.defaultPythonInterpreter, } await fs.ensureDir(path.join(workspaceDir, '.vscode')) await fs.writeJson( @@ -101,15 +126,16 @@ test('Lineage panel renders correctly - relative project outside of workspace', ) try { - const { window, close } = await startVSCode(workspaceDir) - await testLineageWithProjectPath(window) - await close() + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await testLineageWithProjectPath(page) } finally { - await fs.remove(tempFolder) + await stopCodeServer(context) } }) -test('Lineage panel renders correctly - absolute path project outside of workspace', async () => { +test('Lineage panel renders correctly - absolute path project outside of workspace', async ({ + page, +}) => { const tempFolder = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), ) @@ -118,9 +144,14 @@ test('Lineage panel renders correctly - absolute path project outside of workspa const workspaceDir = path.join(tempFolder, 'workspace') await fs.ensureDir(workspaceDir) + const context = await startCodeServer({ + tempDir: workspaceDir, + placeFileWithPythonInterpreter: false, + }) const settings = { 'sqlmesh.projectPath': projectDir, + 'python.defaultInterpreterPath': context.defaultPythonInterpreter, } await fs.ensureDir(path.join(workspaceDir, '.vscode')) await fs.writeJson( @@ -130,15 +161,17 @@ test('Lineage panel renders correctly - absolute path project outside of workspa ) try { - const { window, close } = await startVSCode(workspaceDir) - await testLineageWithProjectPath(window) - await close() + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await testLineageWithProjectPath(page) } finally { - await fs.remove(tempFolder) + await stopCodeServer(context) } }) -test('Lineage panel renders correctly - multiworkspace setup', async () => { +// These work on local machine when debuggin but not on CI, so skipping for now +test.skip('Lineage panel renders correctly - multiworkspace setup', async ({ + page, +}) => { const workspaceDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), ) @@ -168,16 +201,34 @@ test('Lineage panel renders correctly - multiworkspace setup', async () => { }), ) + const context = await startCodeServer({ + tempDir: workspaceDir, + placeFileWithPythonInterpreter: true, + }) + + const settings = { + 'python.defaultInterpreterPath': context.defaultPythonInterpreter, + } + await fs.ensureDir(path.join(projectDir1, '.vscode')) + await fs.writeJson( + path.join(projectDir1, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) + try { - const { window, close } = await startVSCode(workspaceFilePath) - await testLineageWithProjectPath(window) - await close() + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.waitForSelector('text=Open workspace') + await page.click('text=Open workspace') + await testLineageWithProjectPath(page) } finally { - await fs.remove(workspaceDir) + await stopCodeServer(context) } }) -test('Lineage panel renders correctly - multiworkspace setup reversed', async () => { +test.skip('Lineage panel renders correctly - multiworkspace setup reversed', async ({ + page, +}) => { const workspaceDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), ) @@ -207,11 +258,32 @@ test('Lineage panel renders correctly - multiworkspace setup reversed', async () }), ) + const context = await startCodeServer({ + tempDir: workspaceDir, + }) + + const settings = { + 'python.defaultInterpreterPath': context.defaultPythonInterpreter, + } + await fs.ensureDir(path.join(projectDir1, '.vscode')) + await fs.writeJson( + path.join(projectDir1, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) + await fs.ensureDir(path.join(projectDir2, '.vscode')) + await fs.writeJson( + path.join(projectDir2, '.vscode', 'settings.json'), + settings, + { spaces: 2 }, + ) + try { - const { window, close } = await startVSCode(workspaceFilePath) - await testLineageWithProjectPath(window) - await close() + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.waitForSelector('text=Open workspace') + await page.click('text=Open workspace') + await testLineageWithProjectPath(page) } finally { - await fs.remove(workspaceDir) + await stopCodeServer(context) } }) diff --git a/vscode/extension/tests/python_env.spec.ts b/vscode/extension/tests/python_env.spec.ts index 9fae408696..bbf0bafb36 100644 --- a/vscode/extension/tests/python_env.spec.ts +++ b/vscode/extension/tests/python_env.spec.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test' +import { Page, test } from '@playwright/test' import fs from 'fs-extra' import { createVirtualEnvironment, @@ -6,12 +6,16 @@ import { pipInstall, PythonEnvironment, REPO_ROOT, - startVSCode, SUSHI_SOURCE_PATH, } from './utils' import os from 'os' import path from 'path' import { setTcloudVersion, setupAuthenticatedState } from './tcloud_utils' +import { + CodeServerContext, + startCodeServer, + stopCodeServer, +} from './utils_code_server' function writeEnvironmentConfig(sushiPath: string) { const configPath = path.join(sushiPath, 'config.py') @@ -29,6 +33,11 @@ if test_var is None or test_var == "": fs.writeFileSync(configPath, newConfig) } +async function runTest(page: Page, context: CodeServerContext): Promise { + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await openLineageView(page) +} + async function setupEnvironment(): Promise<[string, PythonEnvironment]> { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), @@ -57,29 +66,37 @@ async function setupEnvironment(): Promise<[string, PythonEnvironment]> { } test.describe('python environment variable injection on sqlmesh_lsp', () => { - test('normal setup - error ', async () => { + test('normal setup - error ', async ({ page }, testInfo) => { + testInfo.setTimeout(120_000) + const [tempDir, _] = await setupEnvironment() writeEnvironmentConfig(tempDir) - const { window, close } = await startVSCode(tempDir) + + const context = await startCodeServer({ + tempDir, + }) + try { - await openLineageView(window) - await window.waitForSelector('text=Error creating context') + await runTest(page, context) + await page.waitForSelector('text=Error creating context') } finally { - await close() + await stopCodeServer(context) } }) - test('normal setup - set', async () => { + test('normal setup - set', async ({ page }) => { const [tempDir, _] = await setupEnvironment() writeEnvironmentConfig(tempDir) const env_file = path.join(tempDir, '.env') fs.writeFileSync(env_file, 'TEST_VAR=test_value') - const { window, close } = await startVSCode(tempDir) + const context = await startCodeServer({ + tempDir, + }) try { - await openLineageView(window) - await window.waitForSelector('text=Loaded SQLMesh context') + await runTest(page, context) + await page.waitForSelector('text=Loaded SQLMesh context') } finally { - await close() + await stopCodeServer(context) } }) }) @@ -109,31 +126,35 @@ async function setupTcloudProject( } test.describe('tcloud version', () => { - test('normal setup - error ', async () => { + test('normal setup - error ', async ({ page }) => { const [tempDir, pythonDetails] = await setupEnvironment() await setupTcloudProject(tempDir, pythonDetails) writeEnvironmentConfig(tempDir) - const { window, close } = await startVSCode(tempDir) + const context = await startCodeServer({ + tempDir, + }) try { - await openLineageView(window) - await window.waitForSelector('text=Error creating context') + await runTest(page, context) + await page.waitForSelector('text=Error creating context') } finally { - await close() + await stopCodeServer(context) } }) - test('normal setup - set', async () => { + test('normal setup - set', async ({ page }) => { const [tempDir, pythonDetails] = await setupEnvironment() await setupTcloudProject(tempDir, pythonDetails) writeEnvironmentConfig(tempDir) const env_file = path.join(tempDir, '.env') fs.writeFileSync(env_file, 'TEST_VAR=test_value') - const { window, close } = await startVSCode(tempDir) + const context = await startCodeServer({ + tempDir, + }) try { - await openLineageView(window) - await window.waitForSelector('text=Loaded SQLMesh context') + await runTest(page, context) + await page.waitForSelector('text=Loaded SQLMesh context') } finally { - await close() + await stopCodeServer(context) } }) }) diff --git a/vscode/extension/tests/rename_cte.spec.ts b/vscode/extension/tests/rename_cte.spec.ts index e1b5da6a7e..8dc96a6e25 100644 --- a/vscode/extension/tests/rename_cte.spec.ts +++ b/vscode/extension/tests/rename_cte.spec.ts @@ -1,222 +1,196 @@ -import { test, expect } from '@playwright/test' +import { test, expect, Page } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { findAllReferences, renameSymbol, SUSHI_SOURCE_PATH } from './utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' -// Keyboard shortcuts -const RENAME_KEY = 'F2' -const FIND_ALL_REFERENCES_KEY = - process.platform === 'darwin' ? 'Alt+Shift+F12' : 'Ctrl+Shift+F12' - -// Helper function to set up a test environment -async function setupTestEnvironment() { +async function setupTestEnvironment({ page }: { page: Page }) { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const { window, close } = await startVSCode(tempDir) + + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + + // Navigate to code-server instance + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Navigate to customers.sql which contains CTEs - await window.waitForSelector('text=models') - await window + await page.waitForSelector('text=models') + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') - return { window, close, tempDir } + return { context } } test.describe('CTE Rename', () => { - test('Rename CTE from definition', async () => { - const { window, close, tempDir } = await setupTestEnvironment() + test('Rename CTE from definition', async ({ page }) => { + const { context } = await setupTestEnvironment({ page }) try { // Click on the inner CTE definition "current_marketing" (not the outer one) - await window.locator('text=WITH current_marketing AS').click({ + await page.locator('text=WITH current_marketing AS').click({ position: { x: 100, y: 5 }, }) - // Press F2 to trigger rename - await window.keyboard.press(RENAME_KEY) - await expect(window.locator('text=Rename')).toBeVisible() - const renameInput = window.locator('input:focus') - await expect(renameInput).toBeVisible() + // Open rename + await renameSymbol(page) + await page.waitForSelector('text=Rename') + await page.waitForSelector('input:focus') // Type new name and confirm - await window.keyboard.type('new_marketing') - await window.keyboard.press('Enter') - await window.waitForTimeout(1000) + await page.keyboard.type('new_marketing') + await page.keyboard.press('Enter') // Verify the rename was applied - await expect(window.locator('text=WITH new_marketing AS')).toBeVisible() + await page.waitForSelector('text=WITH new_marketing AS') } finally { - await close() - await fs.remove(tempDir) + await stopCodeServer(context) } }) - test('Rename CTE from usage', async () => { - const { window, close, tempDir } = await setupTestEnvironment() + test('Rename CTE from usage', async ({ page }) => { + const { context } = await setupTestEnvironment({ page }) try { // Click on CTE usage in FROM clause - await window.locator('text=FROM current_marketing_outer').click({ + await page.locator('text=FROM current_marketing_outer').click({ position: { x: 80, y: 5 }, }) - // Press F2 to trigger rename - await window.keyboard.press(RENAME_KEY) - - // Wait for rename input to appear - await expect(window.locator('text=Rename')).toBeVisible() - const renameInput = window.locator('input:focus') - await expect(renameInput).toBeVisible() + // Open rename + await renameSymbol(page) + await page.waitForSelector('text=Rename') + await page.waitForSelector('input:focus') // Type new name - await window.keyboard.type('updated_marketing_out') + await page.keyboard.type('updated_marketing_out') // Confirm rename - await window.keyboard.press('Enter') - await window.waitForTimeout(1000) + await page.keyboard.press('Enter') - // Verify both definition and usage were renamed - await expect( - window.locator('text=WITH updated_marketing_out AS'), - ).toBeVisible() - await expect( - window.locator('text=FROM updated_marketing_out'), - ).toBeVisible() + await page.waitForSelector('text=WITH updated_marketing_out AS') + await page.waitForSelector('text=FROM updated_marketing_out') } finally { - await close() - await fs.remove(tempDir) + await stopCodeServer(context) } }) - test('Cancel CTE rename', async () => { - const { window, close, tempDir } = await setupTestEnvironment() + test('Cancel CTE rename', async ({ page }) => { + const { context } = await setupTestEnvironment({ page }) try { // Click on the CTE to rename - await window.locator('text=current_marketing_outer').first().click() + await page.locator('text=current_marketing_outer').first().click() - // Press F2 to trigger rename - await window.keyboard.press(RENAME_KEY) - - // Wait for rename input to appear - await expect(window.locator('text=Rename')).toBeVisible() - const renameInput = window.locator('input:focus') - await expect(renameInput).toBeVisible() + // Open rename + await renameSymbol(page) + await page.waitForSelector('text=Rename') + await page.waitForSelector('input:focus') // Type new name but cancel - await window.keyboard.type('cancelled_name') - await window.keyboard.press('Escape') + await page.keyboard.type('cancelled_name') + await page.keyboard.press('Escape') // Wait for UI to update - await window.waitForTimeout(500) + await page.waitForTimeout(500) // Verify CTE name was NOT changed await expect( - window.locator('text=current_marketing_outer').first(), + page.locator('text=current_marketing_outer').first(), ).toBeVisible() - await expect(window.locator('text=cancelled_name')).not.toBeVisible() + await expect(page.locator('text=cancelled_name')).not.toBeVisible() } finally { - await close() - await fs.remove(tempDir) + await stopCodeServer(context) } }) - test('Rename CTE updates all references', async () => { - const { window, close, tempDir } = await setupTestEnvironment() + test('Rename CTE updates all references', async ({ page }) => { + const { context } = await setupTestEnvironment({ page }) try { // Click on the CTE definition - await window.locator('text=WITH current_marketing AS').click({ + await page.locator('text=WITH current_marketing AS').click({ position: { x: 100, y: 5 }, }) - // Press F2 to trigger rename - await window.keyboard.press(RENAME_KEY) - // Wait for rename input to appear - await expect(window.locator('text=Rename')).toBeVisible() - const renameInput = window.locator('input:focus') - await expect(renameInput).toBeVisible() + // Open rename + await renameSymbol(page) + await page.waitForSelector('text=Rename') + await page.waitForSelector('input:focus') // Type new name and confirm - await window.keyboard.type('renamed_cte') - await window.keyboard.press('Enter') + await page.keyboard.type('renamed_cte') + await page.keyboard.press('Enter') // Click on the renamed CTE - await window.locator('text=WITH renamed_cte AS').click({ + await page.locator('text=WITH renamed_cte AS').click({ position: { x: 100, y: 5 }, }) // Find all references using keyboard shortcut - await window.keyboard.press(FIND_ALL_REFERENCES_KEY) + await findAllReferences(page) // Verify references panel shows all occurrences - await window.waitForSelector('text=References') - await expect(window.locator('text=customers.sql').first()).toBeVisible() - await window.waitForSelector('text=WITH renamed_cte AS') - await window.waitForSelector('text=renamed_cte.*') - await window.waitForSelector('text=FROM renamed_cte') - await window.waitForSelector('text=renamed_cte.customer_id != 100') + await page.waitForSelector('text=References') + await expect(page.locator('text=customers.sql').first()).toBeVisible() + await page.waitForSelector('text=WITH renamed_cte AS') + await page.waitForSelector('text=renamed_cte.*') + await page.waitForSelector('text=FROM renamed_cte') + await page.waitForSelector('text=renamed_cte.customer_id != 100') } finally { - await close() - await fs.remove(tempDir) + await stopCodeServer(context) } }) - test('Rename CTE with preview', async () => { - const { window, close, tempDir } = await setupTestEnvironment() + test('Rename CTE with preview', async ({ page }) => { + const { context } = await setupTestEnvironment({ page }) try { // Click on the CTE to rename - await window.locator('text=WITH current_marketing AS').click({ + await page.locator('text=WITH current_marketing AS').click({ position: { x: 100, y: 5 }, }) - // Press F2 to trigger rename - await window.keyboard.press(RENAME_KEY) - await expect(window.locator('text=Rename')).toBeVisible() - const renameInput = window.locator('input:focus') - await expect(renameInput).toBeVisible() + // Open rename + await renameSymbol(page) + await page.waitForSelector('text=Rename') + await page.waitForSelector('input:focus') // Type new name - await window.keyboard.type('preview_marketing') + await page.keyboard.type('preview_marketing') // Press Cmd+Enter (Meta+Enter) to preview changes - await window.keyboard.press('Meta+Enter') + await page.keyboard.press( + process.platform === 'darwin' ? 'Meta+Enter' : 'Control+Enter', + ) // Verify preview UI is showing - await expect( - window.locator('text=Refactor Preview').first(), - ).toBeVisible() - await expect(window.locator('text=Apply').first()).toBeVisible() - await expect(window.locator('text=Discard').first()).toBeVisible() + await expect(page.locator('text=Refactor Preview').first()).toBeVisible() + await expect(page.locator('text=Apply').first()).toBeVisible() + await expect(page.locator('text=Discard').first()).toBeVisible() // Verify the preview shows both old and new names - await expect( - window.locator('text=current_marketing').first(), - ).toBeVisible() - await expect( - window.locator('text=preview_marketing').first(), - ).toBeVisible() + await expect(page.locator('text=current_marketing').first()).toBeVisible() + await expect(page.locator('text=preview_marketing').first()).toBeVisible() // Apply the changes - await window.locator('text=Apply').click() + await page.locator('text=Apply').click() // Verify the rename was applied - await expect( - window.locator('text=WITH preview_marketing AS'), - ).toBeVisible() + await expect(page.locator('text=WITH preview_marketing AS')).toBeVisible() } finally { - await close() - await fs.remove(tempDir) + await stopCodeServer(context) } }) }) diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts index 3c31cfdab5..2e9132e8ae 100644 --- a/vscode/extension/tests/render.spec.ts +++ b/vscode/extension/tests/render.spec.ts @@ -2,195 +2,184 @@ import { test, expect } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { startVSCode, SUSHI_SOURCE_PATH } from './utils' +import { openLineageView, runCommand, SUSHI_SOURCE_PATH } from './utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' -test('Render works correctly', async () => { +test('Render works correctly', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) try { - const { window, close } = await startVSCode(tempDir) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder, excluding external_models - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the customer_revenue_lifetime model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') // Render the model - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('Render Model') - await window.keyboard.press('Enter') + await runCommand(page, 'Render Model') // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window await expect( - window.locator('text="marketing"."customer_id" AS'), + page.locator('text="marketing"."customer_id" AS'), ).toBeVisible() - await expect( - window.locator('text=sushi.customers (rendered)'), - ).toBeVisible() - - await close() + await expect(page.locator('text=sushi.customers (rendered)')).toBeVisible() } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) -test('Render works correctly with model without a description', async () => { +test('Render works correctly with model without a description', async ({ + page, +}) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) try { - const { window, close } = await startVSCode(tempDir) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder, excluding external_models - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the latest_order model - await window + await page .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=custom_full_with_custom_kind') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=custom_full_with_custom_kind') + await page.waitForSelector('text=Loaded SQLMesh Context') // Render the model - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('Render Model') - await window.keyboard.press('Enter') + await runCommand(page, 'Render Model') // Check if the model is rendered correctly - await expect(window.locator('text="orders"."id" AS "id",')).toBeVisible() + await expect(page.locator('text="orders"."id" AS "id",')).toBeVisible() await expect( - window.locator('text=sushi.latest_order (rendered)'), + page.locator('text=sushi.latest_order (rendered)'), ).toBeVisible() - - await close() } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) -test('Render works correctly with every rendered model opening a new tab', async () => { +test('Render works correctly with every rendered model opening a new tab', async ({ + page, +}) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) try { - const { window, close } = await startVSCode(tempDir) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Wait for the models folder to be visible - await window.waitForSelector('text=models') - await window + await page.waitForSelector('text=models') + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() - await window + await page .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=custom_full_with_custom_kind') - await window.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=custom_full_with_custom_kind') + await page.waitForSelector('text=Loaded SQLMesh Context') // Render the model - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('Render Model') - await window.keyboard.press('Enter') + await runCommand(page, 'Render Model') // Check if the model is rendered correctly await expect( - window.locator('text=sushi.latest_order (rendered)'), + page.locator('text=sushi.latest_order (rendered)'), ).toBeVisible() // Open the customers model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=grain') + await page.waitForSelector('text=grain') // Render the customers model - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('Render Model') - await window.keyboard.press('Enter') + await runCommand(page, 'Render Model') // Assert both tabs exist await expect( - window.locator('text=sushi.latest_order (rendered)'), + page.locator('text=sushi.latest_order (rendered)'), ).toBeVisible() - await expect( - window.locator('text=sushi.customers (rendered)'), - ).toBeVisible() - - await close() + await expect(page.locator('text=sushi.customers (rendered)')).toBeVisible() } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) -test('Render shows model picker when no active editor is open', async () => { +test('Render shows model picker when no active editor is open', async ({ + page, +}) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + try { - const { window, close } = await startVSCode(tempDir) + // Navigate to code-server instance + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.waitForLoadState('networkidle') // Load the lineage view to initialize SQLMesh context (like lineage.spec.ts does) - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('Lineage: Focus On View') - await window.keyboard.press('Enter') + await openLineageView(page) // Wait for "Loaded SQLmesh Context" text to appear - const loadedContextText = window.locator('text=Loaded SQLMesh Context') - await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 }) + await page.waitForSelector('text=Loaded SQLMesh Context') // Run the render command without any active editor - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('Render Model') - await window.keyboard.press('Enter') + await runCommand(page, 'Render Model') // Type to filter for customers model and select it - await window.keyboard.type('customers') - await window.waitForSelector('text=sushi.customers', { timeout: 5000 }) - await window.locator('text=sushi.customers').click() + await page.keyboard.type('customers') + await page.waitForSelector('text=sushi.customers', { timeout: 2_000 }) + await page.locator('text=sushi.customers').click() // Verify the rendered model is shown - await expect(window.locator('text=sushi.customers (rendered)')).toBeVisible( - { timeout: 15000 }, - ) - - await close() + await expect(page.locator('text=sushi.customers (rendered)')).toBeVisible({ + timeout: 2_000, + }) } finally { - await fs.remove(tempDir) + await stopCodeServer(context) } }) diff --git a/vscode/extension/tests/stop.spec.ts b/vscode/extension/tests/stop.spec.ts index d7b2d08ccd..911c791720 100644 --- a/vscode/extension/tests/stop.spec.ts +++ b/vscode/extension/tests/stop.spec.ts @@ -1,18 +1,18 @@ import path from 'path' -import { SUSHI_SOURCE_PATH } from './utils' +import { runCommand, SUSHI_SOURCE_PATH } from './utils' import os from 'os' import { test } from '@playwright/test' import fs from 'fs-extra' import { startCodeServer, stopCodeServer } from './utils_code_server' test('Stop server works', async ({ page }) => { - test.setTimeout(120000) // Increase timeout to 2 minutes - - console.log('Starting test: Stop server works') const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const context = await startCodeServer(tempDir, true) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) try { // Navigate to code-server instance @@ -41,21 +41,13 @@ test('Stop server works', async ({ page }) => { await page.waitForSelector('text=Loaded SQLMesh Context') // Stop the server - await page.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await page.keyboard.type('SQLMesh: Stop Server') - await page.keyboard.press('Enter') + await runCommand(page, 'SQLMesh: Stop Server') // Await LSP server stopped message await page.waitForSelector('text=LSP server stopped') // Render the model - await page.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await page.keyboard.type('Render Model') - await page.keyboard.press('Enter') + await runCommand(page, 'SQLMesh: Render Model') // Await error message await page.waitForSelector( diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index 20629f5b79..b61f7900ce 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -6,10 +6,10 @@ import { createVirtualEnvironment, pipInstall, REPO_ROOT, - startVSCode, SUSHI_SOURCE_PATH, } from './utils' import { setTcloudVersion, setupAuthenticatedState } from './tcloud_utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' /** * Helper function to create and set up a Python virtual environment @@ -34,13 +34,15 @@ async function setupPythonEnvironment(envDir: string): Promise { return pythonDetails.pythonPath } -test('not signed in, shows sign in window', async ({}, testInfo) => { +test('not signed in, shows sign in window', async ({ page }, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) const pythonEnvDir = path.join(tempDir, '.venv') + const context = await startCodeServer({ tempDir }) + try { // Copy sushi project await fs.copy(SUSHI_SOURCE_PATH, tempDir) @@ -75,46 +77,46 @@ test('not signed in, shows sign in window', async ({}, testInfo) => { ) // Start VS Code - const { window, close } = await startVSCode(tempDir) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Open a SQL file to trigger SQLMesh activation // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the top_waiters model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() // Wait for the file to open - await window.waitForTimeout(2000) + await page.waitForLoadState('networkidle') - await window.waitForSelector( + await page.waitForSelector( 'text=Please sign in to Tobiko Cloud to use SQLMesh', ) - - // Close VS Code - await close() } finally { - // Clean up - await fs.remove(tempDir) + await stopCodeServer(context) } }) -test('signed in and not installed shows installation window', async ({}, testInfo) => { +test('signed in and not installed shows installation window', async ({ + page, +}, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) const pythonEnvDir = path.join(tempDir, '.venv') + const context = await startCodeServer({ tempDir }) + try { // Copy sushi project await fs.copy(SUSHI_SOURCE_PATH, tempDir) @@ -152,40 +154,38 @@ test('signed in and not installed shows installation window', async ({}, testInf ) // Start VS Code - const { window, close } = await startVSCode(tempDir) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Open a SQL file to trigger SQLMesh activation // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the top_waiters model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() - await window.waitForSelector('text=Installing enterprise python package') + await page.waitForSelector('text=Installing enterprise python package') expect( - await window.locator('text=Installing enterprise python package'), + await page.locator('text=Installing enterprise python package'), ).toHaveCount(2) - await window.waitForSelector('text=Loaded SQLMesh context') - - // Close VS Code - await close() + await page.waitForSelector('text=Loaded SQLMesh context') } finally { - // Clean up - await fs.remove(tempDir) + await stopCodeServer(context) } }) -test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when ready', async ({}, testInfo) => { +test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when ready', async ({ + page, +}, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), @@ -234,36 +234,38 @@ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when read ) // Start VS Code - const { window, close } = await startVSCode(tempDir) + const context = await startCodeServer({ + tempDir, + }) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Open a SQL file to trigger SQLMesh activation // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the top_waiters model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() // Verify the context loads successfully - await window.waitForSelector('text=Loaded SQLMesh context') - - // Close VS Code - await close() + await page.waitForSelector('text=Loaded SQLMesh context') } finally { // Clean up await fs.remove(tempDir) } }) -test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when ready', async ({}, testInfo) => { +test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when ready', async ({ + page, +}, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), @@ -312,29 +314,29 @@ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when read ) // Start VS Code - const { window, close } = await startVSCode(tempDir) + const context = await startCodeServer({ + tempDir, + }) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) // Open a SQL file to trigger SQLMesh activation // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the top_waiters model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() // Verify the context loads successfully - await window.waitForSelector('text=Loaded SQLMesh context') - - // Close VS Code - await close() + await page.waitForSelector('text=Loaded SQLMesh context') } finally { // Clean up await fs.remove(tempDir) @@ -343,7 +345,9 @@ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when read // This test is skipped becuase of the way the sign in window is shown is not useable by playwright. It's not solvable // but the test is still useful when running it manually. -test.skip('tcloud not signed in and not installed, shows sign in window and then fact that loaded', async ({}, testInfo) => { +test.skip('tcloud not signed in and not installed, shows sign in window and then fact that loaded', async ({ + page, +}, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), @@ -378,7 +382,11 @@ test.skip('tcloud not signed in and not installed, shows sign in window and then await setTcloudVersion(tempDir, '2.10.1') // Start VS Code - const { window, close } = await startVSCode(tempDir) + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) try { // Copy sushi project @@ -386,38 +394,36 @@ test.skip('tcloud not signed in and not installed, shows sign in window and then // Open a SQL file to trigger SQLMesh activation // Wait for the models folder to be visible - await window.waitForSelector('text=models') + await page.waitForSelector('text=models') // Click on the models folder - await window + await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() // Open the top_waiters model - await window + await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() // Verify the sign in window is shown - await window.waitForSelector( + await page.waitForSelector( 'text=Please sign in to Tobiko Cloud to use SQLMesh', ) // Click on the sign in button - await window + await page .getByRole('button', { name: 'Sign in' }) .filter({ hasText: 'Sign in' }) .click() - await window.waitForSelector('text="Signed in successfully"') + await page.waitForSelector('text="Signed in successfully"') - await window.waitForSelector('text=Installing enterprise python package') + await page.waitForSelector('text=Installing enterprise python package') - await window.waitForSelector('text=Loaded SQLMesh context') + await page.waitForSelector('text=Loaded SQLMesh context') } finally { - // Clean up - await close() - await fs.remove(tempDir) + await stopCodeServer(context) } }) diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index 7b722c9e2f..655568b7d0 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -1,14 +1,8 @@ import path from 'path' -import fs from 'fs-extra' -import os from 'os' -import { _electron as electron, Page } from '@playwright/test' +import { Page } from '@playwright/test' import { exec } from 'child_process' import { promisify } from 'util' -// Absolute path to the VS Code executable you downloaded in step 1. -export const VS_CODE_EXE = fs.readJsonSync( - path.join(__dirname, '..', '.vscode-test', 'paths.json'), -).executablePath // Where your extension lives on disk export const EXT_PATH = path.resolve(__dirname, '..') // Where the sushi project lives which we copy from @@ -22,56 +16,6 @@ export const SUSHI_SOURCE_PATH = path.join( ) export const REPO_ROOT = path.join(__dirname, '..', '..', '..') -/** - * Launch VS Code and return the window and a function to close the app. - * @param workspaceDir The workspace directory to open. - * @returns The window and a function to close the app. - */ -export const startVSCode = async ( - workspaceDir: string, -): Promise<{ - window: Page - close: () => Promise -}> => { - const userDataDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-user-data-'), - ) - const ciArgs = process.env.CI - ? [ - '--disable-gpu', - '--headless', - '--no-sandbox', - '--disable-dev-shm-usage', - '--window-position=-10000,0', - ] - : [] - const args = [ - ...ciArgs, - `--extensionDevelopmentPath=${EXT_PATH}`, - '--disable-workspace-trust', - '--disable-telemetry', - '--install-extension=ms-python.python', - `--user-data-dir=${userDataDir}`, - workspaceDir, - ] - const electronApp = await electron.launch({ - executablePath: VS_CODE_EXE, - args, - }) - const window = await electronApp.firstWindow() - await window.waitForLoadState('domcontentloaded') - await window.waitForLoadState('networkidle') - await clickExplorerTab(window) - - return { - window, - close: async () => { - await electronApp.close() - await fs.remove(userDataDir) - }, - } -} - /** * Click on the Explorer tab in the VS Code activity bar if the Explorer tab is not already active. * This is necessary because the Explorer tab may not be visible if the user has not opened it yet. @@ -141,21 +85,116 @@ export const pipInstall = async ( /** * Open the lineage view in the given window. */ -export const openLineageView = async (window: Page): Promise => { - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('Lineage: Focus On View') - await window.keyboard.press('Enter') -} +export const openLineageView = async (page: Page) => + await runCommand(page, 'Lineage: Focus On View') /** * Restart the SQLMesh servers */ -export const restartSqlmeshServers = async (window: Page): Promise => { - await window.keyboard.press( - process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', - ) - await window.keyboard.type('Restart SQLMesh servers') - await window.keyboard.press('Enter') +export const restartSqlmeshServers = async (page: Page) => + runCommand(page, 'SQLMesh: Restart Servers') + +/** + * Open the vscode command palette and run the given command. + * @param page The window to run the command in. + * @param command The command to run. + */ +export const runCommand = async ( + page: Page, + command: string, +): Promise => { + const maxRetries = 3 + const retryDelay = 3000 + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + await page.keyboard.press( + process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P', + ) + await page.waitForSelector( + 'input[aria-label="Type the name of a command to run."]', + { timeout: 5000 }, + ) + await page.keyboard.type(command) + const commandElement = await page.waitForSelector( + `a:has-text("${command}")`, + { timeout: 5000 }, + ) + await commandElement.click() + return // Success, exit the retry loop + } catch (error) { + if (attempt === maxRetries - 1) { + throw error // Last attempt failed, throw the error + } + + // Close any open command palette before retrying + await page.keyboard.press('Escape') + await page.waitForTimeout(retryDelay) + } + } +} + +/** + * Go to definition. Assumes the location is clicked. + */ +export const goToDefinition = async (page: Page) => + runCommand(page, 'Go to Definition') + +/** + * Save file + */ +export const saveFile = async (page: Page) => runCommand(page, 'File: Save') + +/** + * Rename Symbol opens the rename symbol dialog in VS Code. + */ +export const renameSymbol = async (page: Page) => + runCommand(page, 'Rename Symbol') + +/** + * Find all references to the symbol under the cursor. + */ +export const findAllReferences = async (page: Page): Promise => + runCommand(page, 'References: Find All References') + +/** + * Go to references. Assumes the location is clicked. + */ +export const goToReferences = async (page: Page): Promise => + runCommand(page, 'Go to References') + +/** + * Open the vscode code file picker and select the given file. + * @param window The window to run the command in. + * @param filePath The path to the file to select. + */ +export const openFile = async (page: Page, file: string): Promise => { + const maxRetries = 3 + const retryDelay = 3000 + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + await page.keyboard.press( + process.platform === 'darwin' ? 'Meta+P' : 'Control+P', + ) + await page.waitForSelector('input[aria-label="Search files by name"]', { + timeout: 5000, + }) + await page.keyboard.type(file) + const commandElement = await page.waitForSelector( + `a:has-text("${file}")`, + { timeout: 5000 }, + ) + await commandElement.click() + return // Success, exit the retry loop + } catch (error) { + if (attempt === maxRetries - 1) { + throw error // Last attempt failed, throw the error + } + + // Close any open command palette before retrying + await page.keyboard.press('Escape') + await page.waitForTimeout(retryDelay) + } + } } diff --git a/vscode/extension/tests/utils_code_server.ts b/vscode/extension/tests/utils_code_server.ts index c38fee615c..725a36a06b 100644 --- a/vscode/extension/tests/utils_code_server.ts +++ b/vscode/extension/tests/utils_code_server.ts @@ -1,11 +1,31 @@ -import { spawn, ChildProcess, execSync } from 'child_process' +import { spawn, ChildProcess } from 'child_process' import path from 'path' import fs from 'fs-extra' +import os from 'os' +import { clearTimeout } from 'node:timers' export interface CodeServerContext { codeServerProcess: ChildProcess codeServerPort: number tempDir: string + defaultPythonInterpreter: string +} + +/** + * Get the path to the extensions directory set up by global setup + * @returns The extensions directory path + */ +function getExtensionsDir(): string { + const extensionDir = path.join(__dirname, '..') + const extensionsDir = path.join(extensionDir, '.test_setup', 'extensions') + + if (!fs.existsSync(extensionsDir)) { + throw new Error( + `Extensions directory not found at ${extensionsDir}. Make sure global setup has run.`, + ) + } + + return extensionsDir } /** @@ -13,72 +33,47 @@ export interface CodeServerContext { * @param placeFileWithPythonInterpreter - Whether to place a vscode/settings.json file in the temp directory that points to the python interpreter of the environmen the test is running in. * @returns The code-server context */ -export async function startCodeServer( - tempDir: string, - placeFileWithPythonInterpreter: boolean = false, -): Promise { +export async function startCodeServer({ + tempDir, + placeFileWithPythonInterpreter = false, +}: { + tempDir: string + placeFileWithPythonInterpreter?: boolean +}): Promise { + // Get the extensions directory set up by global setup + const extensionsDir = getExtensionsDir() + // Find an available port const codeServerPort = Math.floor(Math.random() * 10000) + 50000 + const defaultPythonInterpreter = path.join( + __dirname, + '..', + '..', + '..', + '.venv', + 'bin', + 'python', + ) + + const userDataDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-sushi-user-data-dir-'), + ) // Create .vscode/settings.json with Python interpreter if requested if (placeFileWithPythonInterpreter) { const vscodeDir = path.join(tempDir, '.vscode') await fs.ensureDir(vscodeDir) - // Get the current Python interpreter path - const pythonPath = execSync('which python', { - encoding: 'utf-8', - }).trim() - const settings = { - 'python.defaultInterpreterPath': path.join( - __dirname, - '..', - '..', - '..', - '.venv', - 'bin', - 'python', - ), + 'python.defaultInterpreterPath': defaultPythonInterpreter, } await fs.writeJson(path.join(vscodeDir, 'settings.json'), settings, { spaces: 2, }) - console.log( - `Created .vscode/settings.json with Python interpreter: ${pythonPath}`, - ) } - // Get the extension version from package.json - const extensionDir = path.join(__dirname, '..') - const packageJson = JSON.parse( - fs.readFileSync(path.join(extensionDir, 'package.json'), 'utf-8'), - ) - const version = packageJson.version - const extensionName = packageJson.name || 'sqlmesh' - - // Look for the specific version .vsix file - const vsixFileName = `${extensionName}-${version}.vsix` - const vsixPath = path.join(extensionDir, vsixFileName) - - if (!fs.existsSync(vsixPath)) { - throw new Error( - `Extension file ${vsixFileName} not found. Run "pnpm run vscode:package" first.`, - ) - } - - console.log(`Using extension: ${vsixFileName}`) - - // Install the extension first - const extensionsDir = path.join(tempDir, 'extensions') - console.log('Installing extension...') - execSync( - `pnpm run code-server --user-data-dir "${tempDir}" --extensions-dir "${extensionsDir}" --install-extension "${vsixPath}"`, - { stdio: 'inherit' }, - ) - - // Start code-server instance + // Start code-server instance using the shared extensions directory const codeServerProcess = spawn( 'pnpm', [ @@ -92,7 +87,7 @@ export async function startCodeServer( '--disable-update-check', '--disable-workspace-trust', '--user-data-dir', - tempDir, + userDataDir, '--extensions-dir', extensionsDir, tempDir, @@ -130,12 +125,17 @@ export async function startCodeServer( codeServerProcess.on('exit', code => { if (code !== 0) { clearTimeout(timeout) - reject(new Error(`Code-server exited with code ${code}`)) + console.error('Code-server exited with code:', code) } }) }) - return { codeServerProcess, codeServerPort, tempDir } + return { + codeServerProcess, + codeServerPort, + tempDir, + defaultPythonInterpreter, + } } export async function stopCodeServer( @@ -161,5 +161,10 @@ export async function stopCodeServer( }) // Clean up temporary directory - await fs.remove(tempDir) + try { + await fs.remove(tempDir) + } catch (error) { + // Ignore errors when removing temp directory + console.warn(`Failed to remove temp directory ${tempDir}:`, error) + } } From 7057df7efd66f8228ffb4a1e9b4366b480b13928 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:47:36 -0500 Subject: [PATCH 0483/1056] Chore: add no-op extras for duckdb and motherduck (#4829) --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8fd27b98f8..4db73d57c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,9 +102,11 @@ dev = [ ] dbt = ["dbt-core<2"] dlt = ["dlt"] +duckdb = [] gcppostgres = ["cloud-sql-python-connector[pg8000]>=1.8.0"] github = ["PyGithub~=2.5.0"] llm = ["langchain", "openai"] +motherduck = [] mssql = ["pymssql"] mssql-odbc = ["pyodbc"] mysql = ["pymysql"] From d0f69c357c6b825dbc90a49dea1dc8352544cf5a Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 1 Jul 2025 00:22:38 +0300 Subject: [PATCH 0484/1056] Feat: Add support for Restatements on SCD Type 2 models (#4814) --- docs/concepts/models/model_kinds.md | 48 +- sqlmesh/core/engine_adapter/base.py | 40 +- sqlmesh/core/engine_adapter/trino.py | 2 + sqlmesh/core/model/kind.py | 1 - sqlmesh/core/plan/builder.py | 2 +- sqlmesh/core/snapshot/definition.py | 15 + sqlmesh/core/snapshot/evaluator.py | 2 + .../integration/test_integration.py | 4 + tests/core/engine_adapter/test_base.py | 627 +++++++++--------- tests/core/engine_adapter/test_clickhouse.py | 14 +- tests/core/engine_adapter/test_spark.py | 6 +- tests/core/test_integration.py | 326 +++++++++ tests/core/test_model.py | 6 +- tests/core/test_snapshot_evaluator.py | 2 + 14 files changed, 752 insertions(+), 343 deletions(-) diff --git a/docs/concepts/models/model_kinds.md b/docs/concepts/models/model_kinds.md index d01cc738a6..e748313c81 100644 --- a/docs/concepts/models/model_kinds.md +++ b/docs/concepts/models/model_kinds.md @@ -935,7 +935,13 @@ SQLMesh achieves this by adding a `valid_from` and `valid_to` column to your mod Therefore, you can use these models to not only tell you what the latest value is for a given record but also what the values were anytime in the past. Note that maintaining this history does come at a cost of increased storage and compute and this may not be a good fit for sources that change frequently since the history could get very large. -**Note**: Partial data [restatement](../plans.md#restatement-plans) is not supported for this model kind, which means that the entire table will be recreated from scratch if restated. This may lead to data loss, so data restatement is disabled for models of this kind by default. +**Note**: SCD Type 2 models support [restatements](../plans.md#restatement-plans) with specific limitations: + +- **Full restatements**: The entire table will be recreated from scratch when no start date is specified +- **Partial restatements**: You can specify a start date to restate data from a certain point onwards to the latest interval. The end date will always be set to the latest interval's end date, regardless of what end date you specify +- **Partial sections**: Restatements of specific sections (discontinued ranges) of the table are not supported + +Data restatement is disabled for models of this kind by default (`disable_restatement true`). To enable restatements, set `disable_restatement false` in your model configuration. There are two ways to tracking changes: By Time (Recommended) or By Column. @@ -1283,11 +1289,11 @@ This is the most accurate representation of the menu based on the source data pr ### Processing Source Table with Historical Data -The most common case for SCD Type 2 is creating history for a table that it doesn't have it already. +The most common case for SCD Type 2 is creating history for a table that it doesn't have it already. In the example of the restaurant menu, the menu just tells you what is offered right now, but you want to know what was offered over time. In this case, the default setting of `None` for `batch_size` is the best option. -Another use case though is processing a source table that already has history in it. +Another use case though is processing a source table that already has history in it. A common example of this is a "daily snapshot" table that is created by a source system that takes a snapshot of the data at the end of each day. If your source table has historical records, like a "daily snapshot" table, then set `batch_size` to `1` to process each interval (each day if a `@daily` cron) in sequential order. That way the historical records will be properly captured in the SCD Type 2 table. @@ -1433,11 +1439,14 @@ GROUP BY id ``` -### Reset SCD Type 2 Model (clearing history) +### SCD Type 2 Restatements 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. + +#### Enabling Restatements + +To enable restatements for an SCD Type 2 model, set `disable_restatement` to `false` in the model definition: ```sql linenums="1" hl_lines="5" MODEL ( @@ -1449,8 +1458,9 @@ MODEL ( ); ``` -Plan/apply this change to production. -Then you will want to [restate the model](../plans.md#restatement-plans). +#### Full Restatements (Clearing All History) + +To clear all history and recreate the entire table from scratch: ```bash sqlmesh plan --restate-model db.menu_items @@ -1458,7 +1468,29 @@ sqlmesh plan --restate-model db.menu_items !!! warning - This will remove the historical data on the model which in most situations cannot be recovered. + This will remove **all** historical data on the model which in most situations cannot be recovered. + +#### Partial Restatements (From a Specific Date) + +You can restate data from a specific start date onwards. This will: +- Delete all records with `valid_from >= start_date` +- Reprocess the data from the start date to the latest interval + +```bash +sqlmesh plan --restate-model db.menu_items --start "2023-01-15" +``` + +!!! note + + If you specify an end date for SCD Type 2 restatements, it will be ignored and automatically set to the latest interval's end date. + +```bash +# This end date will be ignored and set to the latest interval +sqlmesh plan --restate-model db.menu_items --start "2023-01-15" --end "2023-01-20" +``` + + +#### Re-enabling Protection Once complete you will want to remove `disable_restatement` on the model definition which will set it back to `true` and prevent accidental data loss. diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index a317008b1a..924aca8c99 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -1514,6 +1514,7 @@ def _scd_type_2( unique_key: t.Sequence[exp.Expression], valid_from_col: exp.Column, valid_to_col: exp.Column, + start: TimeLike, execution_time: t.Union[TimeLike, exp.Column], invalidate_hard_deletes: bool = True, updated_at_col: t.Optional[exp.Column] = None, @@ -1708,8 +1709,14 @@ def remove_managed_columns( existing_rows_query = exp.select(*table_columns, exp.true().as_("_exists")).from_( target_table ) + + cleanup_ts = None if truncate: existing_rows_query = existing_rows_query.limit(0) + else: + # If truncate is false it is not the first insert + # Determine the cleanup timestamp for restatement or a regular incremental run + cleanup_ts = to_time_column(start, time_data_type, self.dialect, nullable=True) with source_queries[0] as source_query: prefixed_columns_to_types = [] @@ -1747,12 +1754,41 @@ def remove_managed_columns( # Historical Records that Do Not Change .with_( "static", - existing_rows_query.where(valid_to_col.is_(exp.Null()).not_()), + existing_rows_query.where(valid_to_col.is_(exp.Null()).not_()) + if truncate + else existing_rows_query.where( + exp.and_( + valid_to_col.is_(exp.Null().not_()), + valid_to_col < cleanup_ts, + ), + ), ) # Latest Records that can be updated .with_( "latest", - existing_rows_query.where(valid_to_col.is_(exp.Null())), + existing_rows_query.where(valid_to_col.is_(exp.Null())) + if truncate + else exp.select( + *( + to_time_column( + exp.null(), time_data_type, self.dialect, nullable=True + ).as_(col) + if col == valid_to_col.name + else exp.column(col) + for col in columns_to_types + ), + exp.true().as_("_exists"), + ) + .from_(target_table) + .where( + exp.and_( + valid_from_col <= cleanup_ts, + exp.or_( + valid_to_col.is_(exp.null()), + valid_to_col >= cleanup_ts, + ), + ) + ), ) # Deleted records which can be used to determine `valid_from` for undeleted source records .with_( diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index df8e45b520..06d693e11c 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -256,6 +256,7 @@ def _scd_type_2( unique_key: t.Sequence[exp.Expression], valid_from_col: exp.Column, valid_to_col: exp.Column, + start: TimeLike, execution_time: t.Union[TimeLike, exp.Column], invalidate_hard_deletes: bool = True, updated_at_col: t.Optional[exp.Column] = None, @@ -277,6 +278,7 @@ def _scd_type_2( unique_key, valid_from_col, valid_to_col, + start, execution_time, invalidate_hard_deletes, updated_at_col, diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index 86eb6e665c..4a15023f2f 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -140,7 +140,6 @@ def full_history_restatement_only(self) -> bool: self.is_incremental_unmanaged or self.is_incremental_by_unique_key or self.is_incremental_by_partition - or self.is_scd_type_2 or self.is_managed or self.is_full or self.is_view diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index f3f78e1714..ff953c75a2 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -405,7 +405,7 @@ def _build_restatements( elif (not self._is_dev or not snapshot.is_paused) and snapshot.disable_restatement: self._console.log_warning( f"Cannot restate model '{snapshot.name}'. " - "Restatement is disabled for this model to prevent possible data loss." + "Restatement is disabled for this model to prevent possible data loss. " "If you want to restate this model, change the model's `disable_restatement` setting to `false`." ) continue diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index af6641b5c0..ecd547664f 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -797,6 +797,21 @@ def get_removal_interval( removal_interval = expanded_removal_interval + # SCD Type 2 validation that end date is the latest interval if it was provided + if not is_preview and self.is_scd_type_2 and self.intervals: + requested_start, requested_end = removal_interval + latest_end = self.intervals[-1][1] + if requested_end < latest_end: + from sqlmesh.core.console import get_console + + get_console().log_warning( + f"SCD Type 2 model '{self.model.name}' does not support end date in restatements.\n" + f"Requested end date [{to_ts(requested_end)}] is less than the latest interval end date.\n" + f"The requested end date will be ignored. Using the latest interval end instead: [{to_ts(latest_end)}]" + ) + + removal_interval = self.inclusive_exclusive(requested_start, latest_end, strict) + return removal_interval @property diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index d33748630d..baac96f64a 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -1788,6 +1788,7 @@ def insert( table_description=model.description, column_descriptions=model.column_descriptions, truncate=is_first_insert, + start=kwargs["start"], ) elif isinstance(model.kind, SCDType2ByColumnKind): self.adapter.scd_type_2_by_column( @@ -1805,6 +1806,7 @@ def insert( table_description=model.description, column_descriptions=model.column_descriptions, truncate=is_first_insert, + start=kwargs["start"], ) else: raise SQLMeshError( diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index e48bea318f..ee839d7593 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -744,6 +744,7 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): columns_to_types=input_schema, table_format=ctx.default_table_format, truncate=True, + start="2022-01-01 00:00:00", ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -807,6 +808,7 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): columns_to_types=input_schema, table_format=ctx.default_table_format, truncate=False, + start="2022-01-01 00:00:00", ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -899,6 +901,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): execution_time_as_valid_from=False, columns_to_types=ctx.columns_to_types, truncate=True, + start="2023-01-01", ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -970,6 +973,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): execution_time_as_valid_from=False, columns_to_types=ctx.columns_to_types, truncate=False, + start="2023-01-01", ) results = ctx.get_metadata_results() assert len(results.views) == 0 diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index 8ab15ffdca..faf1386877 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -1222,10 +1222,11 @@ def test_scd_type_2_by_time(make_mocked_engine_adapter: t.Callable): "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), + start=datetime(2020, 1, 1, 0, 0, 0), ) assert ( - adapter.cursor.execute.call_args[0][0] + parse_one(adapter.cursor.execute.call_args[0][0]).sql() == parse_one( """ CREATE OR REPLACE TABLE "target" AS @@ -1254,8 +1255,7 @@ def test_scd_type_2_by_time(make_mocked_engine_adapter: t.Callable): "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE - NOT "test_valid_to" IS NULL + WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) ), "latest" AS ( SELECT "id", @@ -1263,11 +1263,11 @@ def test_scd_type_2_by_time(make_mocked_engine_adapter: t.Callable): "price", "test_UPDATED_at", "test_valid_from", - "test_valid_to", + CAST(NULL AS TIMESTAMP) AS "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE - "test_valid_to" IS NULL + WHERE "test_valid_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) + AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) ), "deleted" AS ( SELECT "static"."id", @@ -1421,10 +1421,11 @@ def test_scd_type_2_by_time_no_invalidate_hard_deletes(make_mocked_engine_adapte "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), + start=datetime(2020, 1, 1, 0, 0, 0), ) assert ( - adapter.cursor.execute.call_args[0][0] + parse_one(adapter.cursor.execute.call_args[0][0]).sql() == parse_one( """ CREATE OR REPLACE TABLE "target" AS @@ -1453,8 +1454,7 @@ def test_scd_type_2_by_time_no_invalidate_hard_deletes(make_mocked_engine_adapte "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE - NOT "test_valid_to" IS NULL + WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) ), "latest" AS ( SELECT "id", @@ -1462,11 +1462,11 @@ def test_scd_type_2_by_time_no_invalidate_hard_deletes(make_mocked_engine_adapte "price", "test_updated_at", "test_valid_from", - "test_valid_to", + CAST(NULL AS TIMESTAMP) AS "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE - "test_valid_to" IS NULL + WHERE "test_valid_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) + AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) ), "deleted" AS ( SELECT "static"."id", @@ -1609,35 +1609,37 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "test_valid_to": exp.DataType.build("TIMESTAMPTZ"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), + start=datetime(2020, 1, 1, 0, 0, 0), ) assert ( - adapter.cursor.execute.call_args[0][0] + parse_one(adapter.cursor.execute.call_args[0][0]).sql() == parse_one( """ -CREATE OR REPLACE TABLE "target" AS -WITH "source" AS ( - SELECT DISTINCT ON ("id1", "id2") + CREATE OR REPLACE TABLE "target" AS + WITH "source" AS ( + SELECT DISTINCT ON ("id1", "id2") TRUE AS "_exists", "id1", "id2", "name", "price", CAST("test_updated_at" AS TIMESTAMPTZ) AS "test_updated_at" - FROM ( + FROM ( SELECT CAST("id1" AS INT) AS "id1", CAST("id2" AS INT) AS "id2", CAST("name" AS VARCHAR) AS "name", CAST("price" AS DOUBLE) AS "price", - CAST("test_updated_at" AS TIMESTAMPTZ) AS "test_updated_at", + CAST("test_updated_at" AS TIMESTAMPTZ) AS "test_updated_at" FROM (VALUES (1, 4, 'muffins', 4.0, '2020-01-01 10:00:00'), (2, 5, 'chips', 5.0, '2020-01-02 15:00:00'), - (3, 6, 'soda', 6.0, '2020-01-03 12:00:00')) AS "t"("id1", "id2", "name", "price", "test_updated_at") - ) AS "raw_source" -), "static" AS ( - SELECT + (3, 6, 'soda', 6.0, '2020-01-03 12:00:00') + ) AS "t"("id1", "id2", "name", "price", "test_updated_at") + ) AS "raw_source" + ), "static" AS ( + SELECT "id1", "id2", "name", @@ -1646,24 +1648,23 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "test_valid_from", "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE - NOT "test_valid_to" IS NULL -), "latest" AS ( - SELECT + FROM "target" + WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00+00:00' AS TIMESTAMPTZ) + ), "latest" AS ( + SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", - "test_valid_to", + CAST(NULL AS TIMESTAMPTZ) AS "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE - "test_valid_to" IS NULL -), "deleted" AS ( - SELECT + FROM "target" + WHERE "test_valid_from" <= CAST('2020-01-01 00:00:00+00:00' AS TIMESTAMPTZ) + AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00+00:00' AS TIMESTAMPTZ)) + ), "deleted" AS ( + SELECT "static"."id1", "static"."id2", "static"."name", @@ -1671,23 +1672,20 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "static"."test_updated_at", "static"."test_valid_from", "static"."test_valid_to" - FROM "static" - LEFT JOIN "latest" + FROM "static" + LEFT JOIN "latest" ON "static"."id1" = "latest"."id1" AND "static"."id2" = "latest"."id2" - WHERE - "latest"."test_valid_to" IS NULL -), "latest_deleted" AS ( - SELECT + WHERE "latest"."test_valid_to" IS NULL + ), "latest_deleted" AS ( + SELECT TRUE AS "_exists", "id1" AS "_key0", "id2" AS "_key1", MAX("test_valid_to") AS "test_valid_to" - FROM "deleted" - GROUP BY - "id1", - "id2" -), "joined" AS ( - SELECT + FROM "deleted" + GROUP BY "id1", "id2" + ), "joined" AS ( + SELECT "source"."_exists" AS "_exists", "latest"."id1" AS "t_id1", "latest"."id2" AS "t_id2", @@ -1701,11 +1699,11 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "source"."name" AS "name", "source"."price" AS "price", "source"."test_updated_at" AS "test_updated_at" - FROM "latest" - LEFT JOIN "source" + FROM "latest" + LEFT JOIN "source" ON "latest"."id1" = "source"."id1" AND "latest"."id2" = "source"."id2" - UNION ALL - SELECT + UNION ALL + SELECT "source"."_exists" AS "_exists", "latest"."id1" AS "t_id1", "latest"."id2" AS "t_id2", @@ -1719,13 +1717,12 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "source"."name" AS "name", "source"."price" AS "price", "source"."test_updated_at" AS "test_updated_at" - FROM "latest" - RIGHT JOIN "source" + FROM "latest" + RIGHT JOIN "source" ON "latest"."id1" = "source"."id1" AND "latest"."id2" = "source"."id2" - WHERE - "latest"."_exists" IS NULL -), "updated_rows" AS ( - SELECT + WHERE "latest"."_exists" IS NULL + ), "updated_rows" AS ( + SELECT COALESCE("joined"."t_id1", "joined"."id1") AS "id1", COALESCE("joined"."t_id2", "joined"."id2") AS "id2", COALESCE("joined"."t_name", "joined"."name") AS "name", @@ -1734,9 +1731,9 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): CASE WHEN "t_test_valid_from" IS NULL AND NOT "latest_deleted"."_exists" IS NULL THEN CASE - WHEN "latest_deleted"."test_valid_to" > "test_updated_at" - THEN "latest_deleted"."test_valid_to" - ELSE "test_updated_at" + WHEN "latest_deleted"."test_valid_to" > "test_updated_at" + THEN "latest_deleted"."test_valid_to" + ELSE "test_updated_at" END WHEN "t_test_valid_from" IS NULL THEN CAST('1970-01-01 00:00:00+00:00' AS TIMESTAMPTZ) @@ -1749,12 +1746,11 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): THEN CAST('2020-01-01 00:00:00+00:00' AS TIMESTAMPTZ) ELSE "t_test_valid_to" END AS "test_valid_to" - FROM "joined" - LEFT JOIN "latest_deleted" - ON "joined"."id1" = "latest_deleted"."_key0" - AND "joined"."id2" = "latest_deleted"."_key1" -), "inserted_rows" AS ( - SELECT + FROM "joined" + LEFT JOIN "latest_deleted" + ON "joined"."id1" = "latest_deleted"."_key0" AND "joined"."id2" = "latest_deleted"."_key1" + ), "inserted_rows" AS ( + SELECT "id1", "id2", "name", @@ -1762,12 +1758,23 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "test_updated_at", "test_updated_at" AS "test_valid_from", CAST(NULL AS TIMESTAMPTZ) AS "test_valid_to" - FROM "joined" - WHERE - "joined"."test_updated_at" > "joined"."t_test_updated_at" -) -SELECT CAST("id1" AS INT) AS "id1", CAST("id2" AS INT) AS "id2", CAST("name" AS VARCHAR) AS "name", CAST("price" AS DOUBLE) AS "price", CAST("test_updated_at" AS TIMESTAMPTZ) AS "test_updated_at", CAST("test_valid_from" AS TIMESTAMPTZ) AS "test_valid_from", CAST("test_valid_to" AS TIMESTAMPTZ) AS "test_valid_to" FROM (SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "static" UNION ALL SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "updated_rows" UNION ALL SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "inserted_rows") AS "_subquery" -""" + FROM "joined" + WHERE "joined"."test_updated_at" > "joined"."t_test_updated_at" + ) + SELECT + CAST("id1" AS INT) AS "id1", + CAST("id2" AS INT) AS "id2", + CAST("name" AS VARCHAR) AS "name", + CAST("price" AS DOUBLE) AS "price", + CAST("test_updated_at" AS TIMESTAMPTZ) AS "test_updated_at", + CAST("test_valid_from" AS TIMESTAMPTZ) AS "test_valid_from", + CAST("test_valid_to" AS TIMESTAMPTZ) AS "test_valid_to" + FROM ( + SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "static" + UNION ALL SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "updated_rows" + UNION ALL SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "inserted_rows" + ) AS "_subquery" + """ ).sql() ) @@ -1790,71 +1797,71 @@ def test_scd_type_2_by_column(make_mocked_engine_adapter: t.Callable): "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), + start=datetime(2020, 1, 1, 0, 0, 0), extra_col_ignore="testing", ) assert ( - adapter.cursor.execute.call_args[0][0] + parse_one(adapter.cursor.execute.call_args[0][0]).sql() == parse_one( """ -CREATE OR REPLACE TABLE "target" AS -WITH "source" AS ( - SELECT DISTINCT ON ("id") + CREATE OR REPLACE TABLE "target" AS + WITH "source" AS ( + SELECT DISTINCT ON ("id") TRUE AS "_exists", "id", "name", "price" - FROM ( + FROM ( SELECT "id", "name", "price" FROM "source" - ) AS "raw_source" -), "static" AS ( - SELECT + ) AS "raw_source" + ), "static" AS ( + SELECT "id", "name", "price", "test_VALID_from", "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE - NOT "test_valid_to" IS NULL -), "latest" AS ( - SELECT + FROM "target" + WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) + ), "latest" AS ( + SELECT "id", "name", "price", "test_VALID_from", - "test_valid_to", + CAST(NULL AS TIMESTAMP) AS "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE - "test_valid_to" IS NULL -), "deleted" AS ( - SELECT + FROM "target" + WHERE "test_VALID_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) + AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) + ), "deleted" AS ( + SELECT "static"."id", "static"."name", "static"."price", "static"."test_VALID_from", "static"."test_valid_to" - FROM "static" - LEFT JOIN "latest" + FROM "static" + LEFT JOIN "latest" ON "static"."id" = "latest"."id" - WHERE + WHERE "latest"."test_valid_to" IS NULL -), "latest_deleted" AS ( - SELECT + ), "latest_deleted" AS ( + SELECT TRUE AS "_exists", "id" AS "_key0", MAX("test_valid_to") AS "test_valid_to" - FROM "deleted" - GROUP BY + FROM "deleted" + GROUP BY "id" -), "joined" AS ( - SELECT + ), "joined" AS ( + SELECT "source"."_exists" AS "_exists", "latest"."id" AS "t_id", "latest"."name" AS "t_name", @@ -1864,11 +1871,11 @@ def test_scd_type_2_by_column(make_mocked_engine_adapter: t.Callable): "source"."id" AS "id", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - LEFT JOIN "source" + FROM "latest" + LEFT JOIN "source" ON "latest"."id" = "source"."id" - UNION ALL - SELECT + UNION ALL + SELECT "source"."_exists" AS "_exists", "latest"."id" AS "t_id", "latest"."name" AS "t_name", @@ -1878,13 +1885,13 @@ def test_scd_type_2_by_column(make_mocked_engine_adapter: t.Callable): "source"."id" AS "id", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - RIGHT JOIN "source" + FROM "latest" + RIGHT JOIN "source" ON "latest"."id" = "source"."id" - WHERE + WHERE "latest"."_exists" IS NULL -), "updated_rows" AS ( - SELECT + ), "updated_rows" AS ( + SELECT COALESCE("joined"."t_id", "joined"."id") AS "id", COALESCE("joined"."t_name", "joined"."name") AS "name", COALESCE("joined"."t_price", "joined"."price") AS "price", @@ -1892,63 +1899,73 @@ def test_scd_type_2_by_column(make_mocked_engine_adapter: t.Callable): CASE WHEN "joined"."_exists" IS NULL OR ( - ( - NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL + ( + NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL + ) + AND ( + "joined"."name" <> "joined"."t_name" + OR ( + "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL ) - AND ( - "joined"."name" <> "joined"."t_name" - OR ( - "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL - ) - OR ( - NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL - ) - OR "joined"."price" <> "joined"."t_price" - OR ( - "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL - ) - OR ( - NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL - ) + OR ( + NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL + ) + OR "joined"."price" <> "joined"."t_price" + OR ( + "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL + ) + OR ( + NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL ) ) + ) THEN CAST('2020-01-01 00:00:00' AS TIMESTAMP) ELSE "t_test_valid_to" END AS "test_valid_to" - FROM "joined" - LEFT JOIN "latest_deleted" + FROM "joined" + LEFT JOIN "latest_deleted" ON "joined"."id" = "latest_deleted"."_key0" -), "inserted_rows" AS ( - SELECT + ), "inserted_rows" AS ( + SELECT "id", "name", "price", CAST('2020-01-01 00:00:00' AS TIMESTAMP) AS "test_VALID_from", CAST(NULL AS TIMESTAMP) AS "test_valid_to" - FROM "joined" - WHERE + FROM "joined" + WHERE ( NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL ) AND ( "joined"."name" <> "joined"."t_name" OR ( - "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL + "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL ) OR ( - NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL + NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL ) OR "joined"."price" <> "joined"."t_price" OR ( - "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL + "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL ) OR ( - NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL + NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL ) ) -) -SELECT CAST("id" AS INT) AS "id", CAST("name" AS VARCHAR) AS "name", CAST("price" AS DOUBLE) AS "price", CAST("test_VALID_from" AS TIMESTAMP) AS "test_VALID_from", CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" FROM (SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "static" UNION ALL SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "updated_rows" UNION ALL SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "inserted_rows") AS "_subquery" - """ + ) + SELECT + CAST("id" AS INT) AS "id", + CAST("name" AS VARCHAR) AS "name", + CAST("price" AS DOUBLE) AS "price", + CAST("test_VALID_from" AS TIMESTAMP) AS "test_VALID_from", + CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" + FROM ( + SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "static" + UNION ALL SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "updated_rows" + UNION ALL SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "inserted_rows" + ) AS "_subquery" + """ ).sql() ) @@ -1972,30 +1989,30 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), + start=datetime(2020, 1, 1, 0, 0, 0), ) - assert ( - adapter.cursor.execute.call_args[0][0] + parse_one(adapter.cursor.execute.call_args[0][0]).sql() == parse_one( """ -CREATE OR REPLACE TABLE "target" AS -WITH "source" AS ( - SELECT DISTINCT ON (CONCAT("id_a", "id_b")) + CREATE OR REPLACE TABLE "target" AS + WITH "source" AS ( + SELECT DISTINCT ON (CONCAT("id_a", "id_b")) TRUE AS "_exists", "id_a", "id_b", "name", - "price", - FROM ( + "price" + FROM ( SELECT "id_a", "id_b", "name", "price" FROM "source" - ) AS "raw_source" -), "static" AS ( - SELECT + ) AS "raw_source" + ), "static" AS ( + SELECT "id_a", "id_b", "name", @@ -2003,44 +2020,41 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab "test_VALID_from", "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE - NOT "test_valid_to" IS NULL -), "latest" AS ( - SELECT + FROM "target" + WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) + ), "latest" AS ( + SELECT "id_a", "id_b", "name", "price", "test_VALID_from", - "test_valid_to", + CAST(NULL AS TIMESTAMP) AS "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE - "test_valid_to" IS NULL -), "deleted" AS ( - SELECT + FROM "target" + WHERE "test_VALID_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) + AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) + ), "deleted" AS ( + SELECT "static"."id_a", "static"."id_b", "static"."name", "static"."price", "static"."test_VALID_from", "static"."test_valid_to" - FROM "static" - LEFT JOIN "latest" + FROM "static" + LEFT JOIN "latest" ON CONCAT("static"."id_a", "static"."id_b") = CONCAT("latest"."id_a", "latest"."id_b") - WHERE - "latest"."test_valid_to" IS NULL -), "latest_deleted" AS ( - SELECT + WHERE "latest"."test_valid_to" IS NULL + ), "latest_deleted" AS ( + SELECT TRUE AS "_exists", CONCAT("id_a", "id_b") AS "_key0", MAX("test_valid_to") AS "test_valid_to" - FROM "deleted" - GROUP BY - CONCAT("id_a", "id_b") -), "joined" AS ( - SELECT + FROM "deleted" + GROUP BY CONCAT("id_a", "id_b") + ), "joined" AS ( + SELECT "source"."_exists" AS "_exists", "latest"."id_a" AS "t_id_a", "latest"."id_b" AS "t_id_b", @@ -2052,11 +2066,11 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab "source"."id_b" AS "id_b", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - LEFT JOIN "source" + FROM "latest" + LEFT JOIN "source" ON CONCAT("latest"."id_a", "latest"."id_b") = CONCAT("source"."id_a", "source"."id_b") - UNION ALL - SELECT + UNION ALL + SELECT "source"."_exists" AS "_exists", "latest"."id_a" AS "t_id_a", "latest"."id_b" AS "t_id_b", @@ -2068,13 +2082,12 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab "source"."id_b" AS "id_b", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - RIGHT JOIN "source" + FROM "latest" + RIGHT JOIN "source" ON CONCAT("latest"."id_a", "latest"."id_b") = CONCAT("source"."id_a", "source"."id_b") - WHERE - "latest"."_exists" IS NULL -), "updated_rows" AS ( - SELECT + WHERE "latest"."_exists" IS NULL + ), "updated_rows" AS ( + SELECT COALESCE("joined"."t_id_a", "joined"."id_a") AS "id_a", COALESCE("joined"."t_id_b", "joined"."id_b") AS "id_b", COALESCE("joined"."t_name", "joined"."name") AS "name", @@ -2083,64 +2096,55 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab CASE WHEN "joined"."_exists" IS NULL OR ( - ( - NOT CONCAT("t_id_a", "t_id_b") IS NULL AND NOT CONCAT("id_a", "id_b") IS NULL - ) + (NOT CONCAT("t_id_a", "t_id_b") IS NULL AND NOT CONCAT("id_a", "id_b") IS NULL) AND ( - "joined"."name" <> "joined"."t_name" - OR ( - "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL - ) - OR ( - NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL - ) - OR "joined"."price" <> "joined"."t_price" - OR ( - "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL - ) - OR ( - NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL - ) + "joined"."name" <> "joined"."t_name" + OR ("joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL) + OR (NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL) + OR "joined"."price" <> "joined"."t_price" + OR ("joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL) + OR (NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL) ) ) THEN CAST('2020-01-01 00:00:00' AS TIMESTAMP) ELSE "t_test_valid_to" END AS "test_valid_to" - FROM "joined" - LEFT JOIN "latest_deleted" + FROM "joined" + LEFT JOIN "latest_deleted" ON CONCAT("joined"."id_a", "joined"."id_b") = "latest_deleted"."_key0" -), "inserted_rows" AS ( - SELECT + ), "inserted_rows" AS ( + SELECT "id_a", "id_b", "name", "price", CAST('2020-01-01 00:00:00' AS TIMESTAMP) AS "test_VALID_from", CAST(NULL AS TIMESTAMP) AS "test_valid_to" - FROM "joined" - WHERE - ( - NOT CONCAT("t_id_a", "t_id_b") IS NULL AND NOT CONCAT("id_a", "id_b") IS NULL - ) + FROM "joined" + WHERE + (NOT CONCAT("t_id_a", "t_id_b") IS NULL AND NOT CONCAT("id_a", "id_b") IS NULL) AND ( "joined"."name" <> "joined"."t_name" - OR ( - "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL - ) - OR ( - NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL - ) + OR ("joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL) + OR (NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL) OR "joined"."price" <> "joined"."t_price" - OR ( - "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL - ) - OR ( - NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL - ) + OR ("joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL) + OR (NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL) ) -) -SELECT CAST("id_a" AS VARCHAR) AS "id_a", CAST("id_b" AS VARCHAR) AS "id_b", CAST("name" AS VARCHAR) AS "name", CAST("price" AS DOUBLE) AS "price", CAST("test_VALID_from" AS TIMESTAMP) AS "test_VALID_from", CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" FROM (SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "static" UNION ALL SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "updated_rows" UNION ALL SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "inserted_rows") AS "_subquery" - """ + ) + SELECT + CAST("id_a" AS VARCHAR) AS "id_a", + CAST("id_b" AS VARCHAR) AS "id_b", + CAST("name" AS VARCHAR) AS "name", + CAST("price" AS DOUBLE) AS "price", + CAST("test_VALID_from" AS TIMESTAMP) AS "test_VALID_from", + CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" + FROM ( + SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "static" + UNION ALL SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "updated_rows" + UNION ALL SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "inserted_rows" + ) AS "_subquery" + """ ).sql() ) @@ -2164,6 +2168,7 @@ def test_scd_type_2_truncate(make_mocked_engine_adapter: t.Callable): }, execution_time=datetime(2020, 1, 1, 0, 0, 0), truncate=True, + start=datetime(2020, 1, 1, 0, 0, 0), ) assert ( @@ -2346,70 +2351,69 @@ def test_scd_type_2_by_column_star_check(make_mocked_engine_adapter: t.Callable) "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), + start=datetime(2020, 1, 1, 0, 0, 0), ) assert ( - adapter.cursor.execute.call_args[0][0] + parse_one(adapter.cursor.execute.call_args[0][0]).sql() == parse_one( """ -CREATE OR REPLACE TABLE "target" AS -WITH "source" AS ( - SELECT DISTINCT ON ("id") + CREATE OR REPLACE TABLE "target" AS + WITH "source" AS ( + SELECT DISTINCT ON ("id") TRUE AS "_exists", "id", "name", "price" - FROM ( + FROM ( SELECT "id", "name", "price" FROM "source" - ) AS "raw_source" -), "static" AS ( - SELECT + ) AS "raw_source" + ), "static" AS ( + SELECT "id", "name", "price", "test_valid_from", "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE - NOT "test_valid_to" IS NULL -), "latest" AS ( - SELECT + FROM "target" + WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) + ), "latest" AS ( + SELECT "id", "name", "price", "test_valid_from", - "test_valid_to", + CAST(NULL AS TIMESTAMP) AS "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE - "test_valid_to" IS NULL -), "deleted" AS ( - SELECT + FROM "target" + WHERE "test_valid_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) + AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) + ), "deleted" AS ( + SELECT "static"."id", "static"."name", "static"."price", "static"."test_valid_from", "static"."test_valid_to" - FROM "static" - LEFT JOIN "latest" + FROM "static" + LEFT JOIN "latest" ON "static"."id" = "latest"."id" - WHERE - "latest"."test_valid_to" IS NULL -), "latest_deleted" AS ( - SELECT + WHERE "latest"."test_valid_to" IS NULL + ), "latest_deleted" AS ( + SELECT TRUE AS "_exists", "id" AS "_key0", MAX("test_valid_to") AS "test_valid_to" - FROM "deleted" - GROUP BY + FROM "deleted" + GROUP BY "id" -), "joined" AS ( - SELECT + ), "joined" AS ( + SELECT "source"."_exists" AS "_exists", "latest"."id" AS "t_id", "latest"."name" AS "t_name", @@ -2419,11 +2423,11 @@ def test_scd_type_2_by_column_star_check(make_mocked_engine_adapter: t.Callable) "source"."id" AS "id", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - LEFT JOIN "source" + FROM "latest" + LEFT JOIN "source" ON "latest"."id" = "source"."id" - UNION ALL - SELECT + UNION ALL + SELECT "source"."_exists" AS "_exists", "latest"."id" AS "t_id", "latest"."name" AS "t_name", @@ -2433,13 +2437,12 @@ def test_scd_type_2_by_column_star_check(make_mocked_engine_adapter: t.Callable) "source"."id" AS "id", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - RIGHT JOIN "source" + FROM "latest" + RIGHT JOIN "source" ON "latest"."id" = "source"."id" - WHERE - "latest"."_exists" IS NULL -), "updated_rows" AS ( - SELECT + WHERE "latest"."_exists" IS NULL + ), "updated_rows" AS ( + SELECT COALESCE("joined"."t_id", "joined"."id") AS "id", COALESCE("joined"."t_name", "joined"."name") AS "name", COALESCE("joined"."t_price", "joined"."price") AS "price", @@ -2447,77 +2450,59 @@ def test_scd_type_2_by_column_star_check(make_mocked_engine_adapter: t.Callable) CASE WHEN "joined"."_exists" IS NULL OR ( - ( - NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL - ) - AND ( - "joined"."id" <> "joined"."t_id" - OR ( - "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL - ) - OR ( - NOT "joined"."t_id" IS NULL AND "joined"."id" IS NULL - ) - OR "joined"."name" <> "joined"."t_name" - OR ( - "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL - ) - OR ( - NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL - ) - OR "joined"."price" <> "joined"."t_price" - OR ( - "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL - ) - OR ( - NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL - ) - ) + (NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL) + AND ( + "joined"."id" <> "joined"."t_id" + OR ("joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL) + OR (NOT "joined"."t_id" IS NULL AND "joined"."id" IS NULL) + OR "joined"."name" <> "joined"."t_name" + OR ("joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL) + OR (NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL) + OR "joined"."price" <> "joined"."t_price" + OR ("joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL) + OR (NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL) + ) ) THEN CAST('2020-01-01 00:00:00' AS TIMESTAMP) ELSE "t_test_valid_to" END AS "test_valid_to" - FROM "joined" - LEFT JOIN "latest_deleted" + FROM "joined" + LEFT JOIN "latest_deleted" ON "joined"."id" = "latest_deleted"."_key0" -), "inserted_rows" AS ( - SELECT + ), "inserted_rows" AS ( + SELECT "id", "name", "price", CAST('2020-01-01 00:00:00' AS TIMESTAMP) AS "test_valid_from", CAST(NULL AS TIMESTAMP) AS "test_valid_to" - FROM "joined" - WHERE - ( - NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL - ) + FROM "joined" + WHERE + (NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL) AND ( "joined"."id" <> "joined"."t_id" - OR ( - "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL - ) - OR ( - NOT "joined"."t_id" IS NULL AND "joined"."id" IS NULL - ) + OR ("joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL) + OR (NOT "joined"."t_id" IS NULL AND "joined"."id" IS NULL) OR "joined"."name" <> "joined"."t_name" - OR ( - "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL - ) - OR ( - NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL - ) + OR ("joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL) + OR (NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL) OR "joined"."price" <> "joined"."t_price" - OR ( - "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL - ) - OR ( - NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL - ) + OR ("joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL) + OR (NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL) ) -) -SELECT CAST("id" AS INT) AS "id", CAST("name" AS VARCHAR) AS "name", CAST("price" AS DOUBLE) AS "price", CAST("test_valid_from" AS TIMESTAMP) AS "test_valid_from", CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" FROM (SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "static" UNION ALL SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "updated_rows" UNION ALL SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "inserted_rows") AS "_subquery" - """ + ) + SELECT + CAST("id" AS INT) AS "id", + CAST("name" AS VARCHAR) AS "name", + CAST("price" AS DOUBLE) AS "price", + CAST("test_valid_from" AS TIMESTAMP) AS "test_valid_from", + CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" + FROM ( + SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "static" + UNION ALL SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "updated_rows" + UNION ALL SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "inserted_rows" + ) AS "_subquery" + """ ).sql() ) @@ -2541,10 +2526,11 @@ def test_scd_type_2_by_column_no_invalidate_hard_deletes(make_mocked_engine_adap "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), + start=datetime(2020, 1, 1, 0, 0, 0), ) assert ( - adapter.cursor.execute.call_args[0][0] + parse_one(adapter.cursor.execute.call_args[0][0]).sql() == parse_one( """ CREATE OR REPLACE TABLE "target" AS @@ -2570,19 +2556,18 @@ def test_scd_type_2_by_column_no_invalidate_hard_deletes(make_mocked_engine_adap "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE - NOT "test_valid_to" IS NULL + WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) ), "latest" AS ( SELECT "id", "name", "price", "test_valid_from", - "test_valid_to", + CAST(NULL AS TIMESTAMP) AS "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE - "test_valid_to" IS NULL + WHERE "test_valid_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) + AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) ), "deleted" AS ( SELECT "static"."id", diff --git a/tests/core/engine_adapter/test_clickhouse.py b/tests/core/engine_adapter/test_clickhouse.py index 9d63d9400e..1665239e36 100644 --- a/tests/core/engine_adapter/test_clickhouse.py +++ b/tests/core/engine_adapter/test_clickhouse.py @@ -606,6 +606,8 @@ def test_scd_type_2_by_time( "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), + start=datetime(2020, 1, 1, 0, 0, 0), + truncate=True, ) assert to_sql_calls(adapter)[4] == parse_one( @@ -637,7 +639,7 @@ def test_scd_type_2_by_time( TRUE AS "_exists" FROM ""__temp_target_efgh"" WHERE - NOT "test_valid_to" IS NULL + NOT "test_valid_to" IS NULL LIMIT 0 ), "latest" AS ( SELECT "id", @@ -649,7 +651,7 @@ def test_scd_type_2_by_time( TRUE AS "_exists" FROM ""__temp_target_efgh"" WHERE - "test_valid_to" IS NULL + "test_valid_to" IS NULL LIMIT 0 ), "deleted" AS ( SELECT "static"."id", @@ -811,6 +813,8 @@ def test_scd_type_2_by_column( "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), + start=datetime(2020, 1, 1, 0, 0, 0), + truncate=True, ) assert to_sql_calls(adapter)[4] == parse_one( @@ -840,7 +844,7 @@ def test_scd_type_2_by_column( TRUE AS "_exists" FROM "__temp_target_efgh" WHERE - NOT "test_valid_to" IS NULL + NOT ("test_valid_to" IS NULL) LIMIT 0 ), "latest" AS ( SELECT "id", @@ -851,7 +855,7 @@ def test_scd_type_2_by_column( TRUE AS "_exists" FROM "__temp_target_efgh" WHERE - "test_valid_to" IS NULL + "test_valid_to" IS NULL LIMIT 0 ), "deleted" AS ( SELECT "static"."id", @@ -907,7 +911,7 @@ def test_scd_type_2_by_column( COALESCE("joined"."t_id", "joined"."id") AS "id", COALESCE("joined"."t_name", "joined"."name") AS "name", COALESCE("joined"."t_price", "joined"."price") AS "price", - COALESCE("t_test_VALID_from", CAST('2020-01-01 00:00:00' AS Nullable(DateTime64(6)))) AS "test_VALID_from", + COALESCE("t_test_VALID_from", CAST('1970-01-01 00:00:00' AS Nullable(DateTime64(6)))) AS "test_VALID_from", CASE WHEN "joined"."_exists" IS NULL OR ( diff --git a/tests/core/engine_adapter/test_spark.py b/tests/core/engine_adapter/test_spark.py index f1c658a23a..8a455c47a3 100644 --- a/tests/core/engine_adapter/test_spark.py +++ b/tests/core/engine_adapter/test_spark.py @@ -569,6 +569,8 @@ def check_table_exists(table_name: exp.Table) -> bool: "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), + start=datetime(2020, 1, 1, 0, 0, 0), + truncate=True, ) assert to_sql_calls(adapter) == [ @@ -613,7 +615,7 @@ def check_table_exists(table_name: exp.Table) -> bool: TRUE AS `_exists` FROM `db`.`temp_target_abcdefgh` WHERE - NOT `test_valid_to` IS NULL + NOT `test_valid_to` IS NULL LIMIT 0 ), `latest` AS ( SELECT `id`, @@ -625,7 +627,7 @@ def check_table_exists(table_name: exp.Table) -> bool: TRUE AS `_exists` FROM `db`.`temp_target_abcdefgh` WHERE - `test_valid_to` IS NULL + `test_valid_to` IS NULL LIMIT 0 ), `deleted` AS ( SELECT `static`.`id`, diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 81e8d2dcb4..766a788ac8 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -6467,3 +6467,329 @@ def plan_with_output(ctx: Context, environment: str): for environment in ["dev", "prod"]: context_diff = ctx._context_diff(environment) assert context_diff.environment == environment + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_scd_type_2_restatement(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + raw_employee_status = d.parse(""" + MODEL ( + name memory.hr_system.raw_employee_status, + kind FULL + ); + + SELECT + 1001 AS employee_id, + 'engineering' AS department, + 'EMEA' AS region, + '2023-01-08 15:00:00 UTC' AS last_modified; + """) + + # Create SCD Type 2 model for employee history tracking + employee_history = d.parse(""" + MODEL ( + name memory.hr_system.employee_history, + kind SCD_TYPE_2_BY_TIME ( + unique_key employee_id, + updated_at_name last_modified, + disable_restatement false + ), + owner hr_analytics, + cron '*/5 * * * *', + grain employee_id, + description 'Historical tracking of employee status changes' + ); + + SELECT + employee_id::INT AS employee_id, + department::TEXT AS department, + region::TEXT AS region, + last_modified AS last_modified + FROM + memory.hr_system.raw_employee_status; + """) + + raw_employee_status_model = load_sql_based_model(raw_employee_status) + employee_history_model = load_sql_based_model(employee_history) + context.upsert_model(raw_employee_status_model) + context.upsert_model(employee_history_model) + + # Initial plan and apply + plan = context.plan_builder("prod", skip_tests=True).build() + context.apply(plan) + + query = "SELECT employee_id, department, region, valid_from, valid_to FROM memory.hr_system.employee_history ORDER BY employee_id, valid_from" + initial_data = context.engine_adapter.fetchdf(query) + + assert len(initial_data) == 1 + assert initial_data["valid_to"].isna().all() + assert initial_data["department"].tolist() == ["engineering"] + assert initial_data["region"].tolist() == ["EMEA"] + + # Apply a future plan with source changes + with time_machine.travel("2023-01-08 15:10:00 UTC"): + # Update source model, employee 1001 changed region + raw_employee_status_v2 = d.parse(""" + MODEL ( + name memory.hr_system.raw_employee_status, + kind FULL + ); + + SELECT + 1001 AS employee_id, + 'engineering' AS department, + 'AMER' AS region, + '2023-01-08 15:10:00 UTC' AS last_modified; + """) + raw_employee_status_v2_model = load_sql_based_model(raw_employee_status_v2) + context.upsert_model(raw_employee_status_v2_model) + context.plan( + auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() + ) + + with time_machine.travel("2023-01-08 15:20:00 UTC"): + context.run() + data_after_change = context.engine_adapter.fetchdf(query) + + # Validate the SCD2 history for employee 1001 + assert len(data_after_change) == 2 + assert data_after_change.iloc[0]["employee_id"] == 1001 + assert data_after_change.iloc[0]["department"] == "engineering" + assert data_after_change.iloc[0]["region"] == "EMEA" + assert str(data_after_change.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" + assert str(data_after_change.iloc[0]["valid_to"]) == "2023-01-08 15:10:00" + assert data_after_change.iloc[1]["employee_id"] == 1001 + assert data_after_change.iloc[1]["department"] == "engineering" + assert data_after_change.iloc[1]["region"] == "AMER" + assert str(data_after_change.iloc[1]["valid_from"]) == "2023-01-08 15:10:00" + assert pd.isna(data_after_change.iloc[1]["valid_to"]) + + # Update source model, employee 1001 changed region again and department + raw_employee_status_v2 = d.parse(""" + MODEL ( + name memory.hr_system.raw_employee_status, + kind FULL + ); + + SELECT + 1001 AS employee_id, + 'sales' AS department, + 'ANZ' AS region, + '2023-01-08 15:26:00 UTC' AS last_modified; + """) + raw_employee_status_v2_model = load_sql_based_model(raw_employee_status_v2) + context.upsert_model(raw_employee_status_v2_model) + context.plan( + auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() + ) + + with time_machine.travel("2023-01-08 15:35:00 UTC"): + context.run() + data_after_change = context.engine_adapter.fetchdf(query) + + # Validate the SCD2 history for employee 1001 after second change + assert len(data_after_change) == 3 + assert data_after_change.iloc[0]["employee_id"] == 1001 + assert data_after_change.iloc[0]["department"] == "engineering" + assert data_after_change.iloc[0]["region"] == "EMEA" + assert str(data_after_change.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" + assert str(data_after_change.iloc[0]["valid_to"]) == "2023-01-08 15:10:00" + assert data_after_change.iloc[1]["employee_id"] == 1001 + assert data_after_change.iloc[1]["department"] == "engineering" + assert data_after_change.iloc[1]["region"] == "AMER" + assert str(data_after_change.iloc[1]["valid_from"]) == "2023-01-08 15:10:00" + assert str(data_after_change.iloc[1]["valid_to"]) == "2023-01-08 15:26:00" + assert data_after_change.iloc[2]["employee_id"] == 1001 + assert data_after_change.iloc[2]["department"] == "sales" + assert data_after_change.iloc[2]["region"] == "ANZ" + assert str(data_after_change.iloc[2]["valid_from"]) == "2023-01-08 15:26:00" + assert pd.isna(data_after_change.iloc[2]["valid_to"]) + + # Now test restatement cleanup by restating from 15:10 (first change) + with time_machine.travel("2023-01-08 15:38:00 UTC"): + plan = context.plan_builder( + "prod", + skip_tests=True, + restate_models=["memory.hr_system.employee_history"], + start="2023-01-08 15:09:00", + ).build() + context.apply(plan) + restated_data = context.engine_adapter.fetchdf(query) + + # Validate the SCD2 history after restatement + assert len(restated_data) == 2 + assert restated_data.iloc[0]["employee_id"] == 1001 + assert restated_data.iloc[0]["department"] == "engineering" + assert restated_data.iloc[0]["region"] == "EMEA" + assert str(restated_data.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" + assert str(restated_data.iloc[0]["valid_to"]) == "2023-01-08 15:26:00" + assert restated_data.iloc[1]["employee_id"] == 1001 + assert restated_data.iloc[1]["department"] == "sales" + assert restated_data.iloc[1]["region"] == "ANZ" + assert str(restated_data.iloc[1]["valid_from"]) == "2023-01-08 15:26:00" + assert pd.isna(restated_data.iloc[1]["valid_to"]) + + +@time_machine.travel("2020-01-01 00:00:00 UTC") +def test_scd_type_2_full_restatement_no_start_date(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Initial product catalog of 3 products + raw_products = d.parse(""" + MODEL ( + name memory.store.raw_products, + kind FULL + ); + + SELECT * FROM VALUES + (101, 'Laptop Pro', 1299.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP), + (102, 'Wireless Mouse', 49.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP), + (103, 'Office Chair', 199.99, 'Furniture', '2020-01-01 00:00:00'::TIMESTAMP) + AS t(product_id, product_name, price, category, last_updated); + """) + + # SCD Type 2 model for product history tracking + product_history = d.parse(""" + MODEL ( + name memory.store.product_history, + kind SCD_TYPE_2_BY_TIME ( + unique_key product_id, + updated_at_name last_updated, + disable_restatement false + ), + owner catalog_team, + cron '0 */6 * * *', + grain product_id, + description 'Product catalog change history' + ); + + SELECT + product_id::INT AS product_id, + product_name::TEXT AS product_name, + price::DECIMAL(10,2) AS price, + category::TEXT AS category, + last_updated AS last_updated + FROM + memory.store.raw_products; + """) + + raw_products_model = load_sql_based_model(raw_products) + product_history_model = load_sql_based_model(product_history) + context.upsert_model(raw_products_model) + context.upsert_model(product_history_model) + + # Initial plan and apply + plan = context.plan_builder("prod", skip_tests=True).build() + context.apply(plan) + + query = "SELECT product_id, product_name, price, category, last_updated, valid_from, valid_to FROM memory.store.product_history ORDER BY product_id, valid_from" + initial_data = context.engine_adapter.fetchdf(query) + + # Validate initial state of 3 products all active + assert len(initial_data) == 3 + assert initial_data["valid_to"].isna().all() + initial_product_names = set(initial_data["product_name"].tolist()) + assert initial_product_names == {"Laptop Pro", "Wireless Mouse", "Office Chair"} + + # Price update and category change + with time_machine.travel("2020-01-15 12:00:00 UTC"): + raw_products_v2 = d.parse(""" + MODEL ( + name memory.store.raw_products, + kind FULL + ); + + SELECT * FROM VALUES + (101, 'Laptop Pro', 1199.99, 'Electronics', '2020-01-15 00:00:00'::TIMESTAMP), + (102, 'Wireless Mouse', 49.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP), + (103, 'Ergonomic Office Chair', 229.99, 'Office Furniture', '2020-01-15 00:00:00'::TIMESTAMP) + AS t(product_id, product_name, price, category, last_updated); + """) + raw_products_v2_model = load_sql_based_model(raw_products_v2) + context.upsert_model(raw_products_v2_model) + context.plan( + auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() + ) + context.run() + + data_after_first_change = context.engine_adapter.fetchdf(query) + + # Should have 5 records (3 original closed, 2 new activε, 1 unchanged) + assert len(data_after_first_change) == 5 + + # Second change + with time_machine.travel("2020-02-01 10:00:00 UTC"): + raw_products_v3 = d.parse(""" + MODEL ( + name memory.store.raw_products, + kind FULL + ); + + SELECT * FROM VALUES + (101, 'Laptop Pro Max', 1399.99, 'Electronics', '2020-02-01 00:00:00'::TIMESTAMP), + (103, 'Ergonomic Office Chair', 229.99, 'Office Furniture', '2020-01-15 00:00:00'::TIMESTAMP), + (102, 'Wireless Mouse', 49.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP) + AS t(product_id, product_name, price, category, last_updated); + """) + raw_products_v3_model = load_sql_based_model(raw_products_v3) + context.upsert_model(raw_products_v3_model) + context.plan( + auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() + ) + context.run() + data_after_second_change = context.engine_adapter.fetchdf(query) + assert len(data_after_second_change) == 6 + + # Store the current state before full restatement + data_before_full_restatement = data_after_second_change.copy() + + # Perform full restatement (no start date provided) + with time_machine.travel("2020-02-01 15:00:00 UTC"): + plan = context.plan_builder( + "prod", skip_tests=True, restate_models=["memory.store.product_history"] + ).build() + context.apply(plan) + data_after_full_restatement = context.engine_adapter.fetchdf(query) + assert len(data_after_full_restatement) == 3 + + # Check that all currently active products before restatement are still active after restatement + active_before = data_before_full_restatement[ + data_before_full_restatement["valid_to"].isna() + ] + active_after = data_after_full_restatement + assert set(active_before["product_id"]) == set(active_after["product_id"]) + + expected_products = { + 101: { + "product_name": "Laptop Pro Max", + "price": 1399.99, + "category": "Electronics", + "last_updated": "2020-02-01", + }, + 102: { + "product_name": "Wireless Mouse", + "price": 49.99, + "category": "Electronics", + "last_updated": "2020-01-01", + }, + 103: { + "product_name": "Ergonomic Office Chair", + "price": 229.99, + "category": "Office Furniture", + "last_updated": "2020-01-15", + }, + } + for _, row in data_after_full_restatement.iterrows(): + pid = row["product_id"] + assert pid in expected_products + expected = expected_products[pid] + assert row["product_name"] == expected["product_name"] + assert float(row["price"]) == expected["price"] + assert row["category"] == expected["category"] + + # valid_from should be the epoch, valid_to should be NaT + assert str(row["valid_from"]) == "1970-01-01 00:00:00" + assert pd.isna(row["valid_to"]) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 8d16c9422b..4fc82875d7 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9906,9 +9906,9 @@ def test_signal_always_true(batch, arg1, arg2): def test_scd_type_2_full_history_restatement(): - assert ModelKindName.SCD_TYPE_2.full_history_restatement_only is True - assert ModelKindName.SCD_TYPE_2_BY_TIME.full_history_restatement_only is True - assert ModelKindName.SCD_TYPE_2_BY_COLUMN.full_history_restatement_only is True + assert ModelKindName.SCD_TYPE_2.full_history_restatement_only is False + assert ModelKindName.SCD_TYPE_2_BY_TIME.full_history_restatement_only is False + assert ModelKindName.SCD_TYPE_2_BY_COLUMN.full_history_restatement_only is False assert ModelKindName.INCREMENTAL_BY_TIME_RANGE.full_history_restatement_only is False diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index d131e6aa95..3704c192bd 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -1986,6 +1986,7 @@ def test_insert_into_scd_type_2_by_time( column_descriptions={}, updated_at_as_valid_from=False, truncate=truncate, + start="2020-01-01", ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) @@ -2158,6 +2159,7 @@ def test_insert_into_scd_type_2_by_column( table_description=None, column_descriptions={}, truncate=truncate, + start="2020-01-01", ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) From 09eb408269456f3ec340c9f4df13c95f274e9a93 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 30 Jun 2025 16:41:20 -0700 Subject: [PATCH 0485/1056] Chore: Enable testing for Python 3.13 (#4851) --- .circleci/continue_config.yml | 1 + pyproject.toml | 3 ++- tests/dbt/test_config.py | 10 ++++++++-- tests/utils/test_metaprogramming.py | 4 +--- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index e285d798ef..979c748925 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -306,6 +306,7 @@ workflows: - "3.10" - "3.11" - "3.12" + - "3.13" - cicd_tests_windows - engine_tests_docker: name: engine_<< matrix.engine >> diff --git a/pyproject.toml b/pyproject.toml index 4db73d57c1..022b3fe9f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ dev = [ "dbt-clickhouse", "dbt-databricks", "dbt-redshift", - "dbt-sqlserver>=1.7.0", + "dbt-sqlserver>=1.7.0;python_version<'3.13'", "dbt-trino", "Faker", "google-auth", @@ -79,6 +79,7 @@ dev = [ "pydantic", "PyAthena[Pandas]", "PyGithub>=2.6.0", + "pyodbc", "pyperf", "pyspark~=3.5.0", "pytest", diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 9bdd7b0063..bb8806c657 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -1,5 +1,6 @@ import base64 import typing as t +import sys from pathlib import Path from shutil import copytree @@ -943,13 +944,18 @@ def test_db_type_to_column_class(): from dbt.adapters.bigquery import BigQueryColumn from dbt.adapters.databricks.column import DatabricksColumn from dbt.adapters.snowflake import SnowflakeColumn - from dbt.adapters.sqlserver.sqlserver_column import SQLServerColumn assert (TARGET_TYPE_TO_CONFIG_CLASS["bigquery"].column_class) == BigQueryColumn assert (TARGET_TYPE_TO_CONFIG_CLASS["databricks"].column_class) == DatabricksColumn assert (TARGET_TYPE_TO_CONFIG_CLASS["duckdb"].column_class) == Column assert (TARGET_TYPE_TO_CONFIG_CLASS["snowflake"].column_class) == SnowflakeColumn - assert (TARGET_TYPE_TO_CONFIG_CLASS["sqlserver"].column_class) == SQLServerColumn + + if sys.version_info < (3, 13): + # The dbt-sqlserver package is not currently compatible with Python 3.13 + + from dbt.adapters.sqlserver.sqlserver_column import SQLServerColumn + + assert (TARGET_TYPE_TO_CONFIG_CLASS["sqlserver"].column_class) == SQLServerColumn from dbt.adapters.clickhouse.column import ClickHouseColumn from dbt.adapters.trino.column import TrinoColumn diff --git a/tests/utils/test_metaprogramming.py b/tests/utils/test_metaprogramming.py index c7c22378ac..8cca48ac6e 100644 --- a/tests/utils/test_metaprogramming.py +++ b/tests/utils/test_metaprogramming.py @@ -49,9 +49,7 @@ def test_print_exception(mocker: MockerFixture): print_exception(ex, test_env, out_mock) expected_message = r""" File ".*?.tests.utils.test_metaprogramming\.py", line 47, in test_print_exception - eval\("test_fun\(\)", env\) - - File "", line 1, in + eval\("test_fun\(\)", env\).* File '/test/path.py' \(or imported file\), line 2, in test_fun def test_fun\(\): From 4a44d8952f1ed7a2192dbbceceab4927adffc2ba Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:25:18 +0300 Subject: [PATCH 0486/1056] Chore!: bump sqlglot to v26.32.0 (#4853) --- pyproject.toml | 2 +- sqlmesh/core/dialect.py | 2 +- sqlmesh/utils/jinja.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 022b3fe9f6..25d490df7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.31.0", + "sqlglot[rs]~=26.32.0", "tenacity", "time-machine", "json-stream" diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index db77f0c461..6406fa8864 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -878,7 +878,7 @@ def parse( match = match_dialect and DIALECT_PATTERN.search(sql[:MAX_MODEL_DEFINITION_SIZE]) dialect = Dialect.get_or_raise(match.group(2) if match else default_dialect) - tokens = dialect.tokenizer.tokenize(sql) + tokens = dialect.tokenize(sql) chunks: t.List[t.Tuple[t.List[Token], ChunkType]] = [([], ChunkType.SQL)] total = len(tokens) diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index 711f760b7b..d1c0ef0361 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -75,7 +75,7 @@ def extract(self, jinja: str, dialect: str = "") -> t.Dict[str, MacroInfo]: """ self.reset() self.sql = jinja - self._tokens = Dialect.get_or_raise(dialect).tokenizer.tokenize(jinja) + self._tokens = Dialect.get_or_raise(dialect).tokenize(jinja) self._index = -1 self._advance() From e91e467f9f5946655c5636a2e7c048891ae3bcee Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Tue, 1 Jul 2025 11:07:09 +0300 Subject: [PATCH 0487/1056] Feat!: Tag queries with their plan id (#4832) --- sqlmesh/core/config/scheduler.py | 1 - sqlmesh/core/context.py | 58 ++++++++++++++++++--------- sqlmesh/core/engine_adapter/athena.py | 2 +- sqlmesh/core/engine_adapter/base.py | 12 ++++-- sqlmesh/core/plan/evaluator.py | 12 ++++-- sqlmesh/core/plan/explainer.py | 6 ++- sqlmesh/utils/__init__.py | 21 ++++++++++ tests/conftest.py | 6 ++- tests/core/test_integration.py | 29 +++++++++++++- tests/core/test_plan_evaluator.py | 6 ++- tests/core/test_table_diff.py | 6 +-- 11 files changed, 123 insertions(+), 36 deletions(-) diff --git a/sqlmesh/core/config/scheduler.py b/sqlmesh/core/config/scheduler.py index 5cbfc6a71c..fc44d8f356 100644 --- a/sqlmesh/core/config/scheduler.py +++ b/sqlmesh/core/config/scheduler.py @@ -130,7 +130,6 @@ class BuiltInSchedulerConfig(_EngineAdapterStateSyncSchedulerConfig, BaseConfig) def create_plan_evaluator(self, context: GenericContext) -> PlanEvaluator: return BuiltInPlanEvaluator( state_sync=context.state_sync, - snapshot_evaluator=context.snapshot_evaluator, create_scheduler=context.create_scheduler, default_catalog=context.default_catalog, console=context.console, diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 0317aad894..402ed22fee 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -116,7 +116,7 @@ run_tests, ) from sqlmesh.core.user import User -from sqlmesh.utils import UniqueKeyDict, Verbosity +from sqlmesh.utils import UniqueKeyDict, Verbosity, CorrelationId from sqlmesh.utils.concurrency import concurrent_apply_to_values from sqlmesh.utils.dag import DAG from sqlmesh.utils.date import ( @@ -418,7 +418,7 @@ def __init__( self.config.get_state_connection(self.gateway) or self.connection_config ) - self._snapshot_evaluator: t.Optional[SnapshotEvaluator] = None + self._snapshot_evaluators: t.Dict[t.Optional[CorrelationId], SnapshotEvaluator] = {} self.console = get_console() setattr(self.console, "dialect", self.config.dialect) @@ -446,18 +446,22 @@ def engine_adapter(self) -> EngineAdapter: self._engine_adapter = self.connection_config.create_engine_adapter() return self._engine_adapter - @property - def snapshot_evaluator(self) -> SnapshotEvaluator: - if not self._snapshot_evaluator: - self._snapshot_evaluator = SnapshotEvaluator( + def snapshot_evaluator( + self, correlation_id: t.Optional[CorrelationId] = None + ) -> SnapshotEvaluator: + # Cache snapshot evaluators by correlation_id to avoid old correlation_ids being attached to future Context operations + if correlation_id not in self._snapshot_evaluators: + self._snapshot_evaluators[correlation_id] = SnapshotEvaluator( { - gateway: adapter.with_log_level(logging.INFO) + gateway: adapter.with_settings( + log_level=logging.INFO, correlation_id=correlation_id + ) for gateway, adapter in self.engine_adapters.items() }, ddl_concurrent_tasks=self.concurrent_tasks, selected_gateway=self.selected_gateway, ) - return self._snapshot_evaluator + return self._snapshot_evaluators[correlation_id] def execution_context( self, @@ -538,7 +542,9 @@ def scheduler(self, environment: t.Optional[str] = None) -> Scheduler: return self.create_scheduler(snapshots) - def create_scheduler(self, snapshots: t.Iterable[Snapshot]) -> Scheduler: + def create_scheduler( + self, snapshots: t.Iterable[Snapshot], correlation_id: t.Optional[CorrelationId] = None + ) -> Scheduler: """Creates the built-in scheduler. Args: @@ -549,7 +555,7 @@ def create_scheduler(self, snapshots: t.Iterable[Snapshot]) -> Scheduler: """ return Scheduler( snapshots, - self.snapshot_evaluator, + self.snapshot_evaluator(correlation_id), self.state_sync, default_catalog=self.default_catalog, max_workers=self.concurrent_tasks, @@ -714,7 +720,7 @@ def run( NotificationEvent.RUN_START, environment=environment ) analytics_run_id = analytics.collector.on_run_start( - engine_type=self.snapshot_evaluator.adapter.dialect, + engine_type=self.snapshot_evaluator().adapter.dialect, state_sync_type=self.state_sync.state_type(), ) self._load_materializations() @@ -1076,7 +1082,7 @@ def evaluate( and not parent_snapshot.categorized ] - df = self.snapshot_evaluator.evaluate_and_fetch( + df = self.snapshot_evaluator().evaluate_and_fetch( snapshot, start=start, end=end, @@ -1588,7 +1594,12 @@ def apply( default_catalog=self.default_catalog, console=self.console, ) - explainer.evaluate(plan.to_evaluatable()) + explainer.evaluate( + plan.to_evaluatable(), + snapshot_evaluator=self.snapshot_evaluator( + correlation_id=CorrelationId.from_plan_id(plan.plan_id) + ), + ) return self.notification_target_manager.notify( @@ -1902,7 +1913,7 @@ def _table_diff( ) return TableDiff( - adapter=adapter.with_log_level(logger.getEffectiveLevel()), + adapter=adapter.with_settings(logger.getEffectiveLevel()), source=source, target=target, on=on, @@ -2111,7 +2122,7 @@ def audit( errors = [] skipped_count = 0 for snapshot in snapshots: - for audit_result in self.snapshot_evaluator.audit( + for audit_result in self.snapshot_evaluator().audit( snapshot=snapshot, start=start, end=end, @@ -2143,7 +2154,7 @@ def audit( self.console.log_status_update(f"Got {error.count} results, expected 0.") if error.query: self.console.show_sql( - f"{error.query.sql(dialect=self.snapshot_evaluator.adapter.dialect)}" + f"{error.query.sql(dialect=self.snapshot_evaluator().adapter.dialect)}" ) self.console.log_status_update("Done.") @@ -2335,11 +2346,14 @@ def print_environment_names(self) -> None: def close(self) -> None: """Releases all resources allocated by this context.""" - if self._snapshot_evaluator: - self._snapshot_evaluator.close() + for evaluator in self._snapshot_evaluators.values(): + evaluator.close() + if self._state_sync: self._state_sync.close() + self._snapshot_evaluators.clear() + def _run( self, environment: str, @@ -2390,7 +2404,11 @@ def _run( def _apply(self, plan: Plan, circuit_breaker: t.Optional[t.Callable[[], bool]]) -> None: self._scheduler.create_plan_evaluator(self).evaluate( - plan.to_evaluatable(), circuit_breaker=circuit_breaker + plan.to_evaluatable(), + snapshot_evaluator=self.snapshot_evaluator( + correlation_id=CorrelationId.from_plan_id(plan.plan_id) + ), + circuit_breaker=circuit_breaker, ) @python_api_analytics @@ -2683,7 +2701,7 @@ def _run_janitor(self, ignore_ttl: bool = False) -> None: ) # Remove the expired snapshots tables - self.snapshot_evaluator.cleanup( + self.snapshot_evaluator().cleanup( target_snapshots=cleanup_targets, on_complete=self.console.update_cleanup_progress, ) diff --git a/sqlmesh/core/engine_adapter/athena.py b/sqlmesh/core/engine_adapter/athena.py index 8e0f3d84f7..88ab9b2c5d 100644 --- a/sqlmesh/core/engine_adapter/athena.py +++ b/sqlmesh/core/engine_adapter/athena.py @@ -46,7 +46,7 @@ def __init__( self, *args: t.Any, s3_warehouse_location: t.Optional[str] = None, **kwargs: t.Any ): # Need to pass s3_warehouse_location to the superclass so that it goes into _extra_config - # which means that EngineAdapter.with_log_level() keeps this property when it makes a clone + # which means that EngineAdapter.with_settings() keeps this property when it makes a clone super().__init__(*args, s3_warehouse_location=s3_warehouse_location, **kwargs) self.s3_warehouse_location = s3_warehouse_location diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 924aca8c99..591d81c9ae 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -39,7 +39,7 @@ ) from sqlmesh.core.model.kind import TimeColumn from sqlmesh.core.schema_diff import SchemaDiffer -from sqlmesh.utils import columns_to_types_all_known, random_id +from sqlmesh.utils import columns_to_types_all_known, random_id, CorrelationId from sqlmesh.utils.connection_pool import create_connection_pool, ConnectionPool from sqlmesh.utils.date import TimeLike, make_inclusive, to_time_column from sqlmesh.utils.errors import ( @@ -123,6 +123,7 @@ def __init__( pre_ping: bool = False, pretty_sql: bool = False, shared_connection: bool = False, + correlation_id: t.Optional[CorrelationId] = None, **kwargs: t.Any, ): self.dialect = dialect.lower() or self.DIALECT @@ -144,19 +145,21 @@ def __init__( self._pre_ping = pre_ping self._pretty_sql = pretty_sql self._multithreaded = multithreaded + self.correlation_id = correlation_id - def with_log_level(self, level: int) -> EngineAdapter: + def with_settings(self, log_level: int, **kwargs: t.Any) -> EngineAdapter: adapter = self.__class__( self._connection_pool, dialect=self.dialect, sql_gen_kwargs=self._sql_gen_kwargs, default_catalog=self._default_catalog, - execute_log_level=level, + execute_log_level=log_level, register_comments=self._register_comments, null_connection=True, multithreaded=self._multithreaded, pretty_sql=self._pretty_sql, **self._extra_config, + **kwargs, ) return adapter @@ -2211,6 +2214,9 @@ def execute( else: sql = t.cast(str, e) + if self.correlation_id: + sql = f"/* {self.correlation_id} */ {sql}" + self._log_sql( sql, expression=e if isinstance(e, exp.Expression) else None, diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index d959fd27a4..562f2ed60e 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -49,7 +49,10 @@ class PlanEvaluator(abc.ABC): @abc.abstractmethod def evaluate( - self, plan: EvaluatablePlan, circuit_breaker: t.Optional[t.Callable[[], bool]] = None + self, + plan: EvaluatablePlan, + snapshot_evaluator: SnapshotEvaluator, + circuit_breaker: t.Optional[t.Callable[[], bool]] = None, ) -> None: """Evaluates a plan by pushing snapshots and backfilling data. @@ -60,6 +63,8 @@ def evaluate( Args: plan: The plan to evaluate. + snapshot_evaluator: The snapshot evaluator to use. + circuit_breaker: The circuit breaker to use. """ @@ -67,13 +72,11 @@ class BuiltInPlanEvaluator(PlanEvaluator): def __init__( self, state_sync: StateSync, - snapshot_evaluator: SnapshotEvaluator, create_scheduler: t.Callable[[t.Iterable[Snapshot]], Scheduler], default_catalog: t.Optional[str], console: t.Optional[Console] = None, ): self.state_sync = state_sync - self.snapshot_evaluator = snapshot_evaluator self.create_scheduler = create_scheduler self.default_catalog = default_catalog self.console = console or get_console() @@ -82,9 +85,12 @@ def __init__( def evaluate( self, plan: EvaluatablePlan, + snapshot_evaluator: SnapshotEvaluator, circuit_breaker: t.Optional[t.Callable[[], bool]] = None, ) -> None: self._circuit_breaker = circuit_breaker + self.snapshot_evaluator = snapshot_evaluator + self.console.start_plan_evaluation(plan) analytics.collector.on_plan_apply_start( plan=plan, diff --git a/sqlmesh/core/plan/explainer.py b/sqlmesh/core/plan/explainer.py index 4d1ee2256d..d3c6480f74 100644 --- a/sqlmesh/core/plan/explainer.py +++ b/sqlmesh/core/plan/explainer.py @@ -20,6 +20,7 @@ from sqlmesh.utils import Verbosity, rich as srich, to_snake_case from sqlmesh.utils.date import to_ts from sqlmesh.utils.errors import SQLMeshError +from sqlmesh.core.snapshot.evaluator import SnapshotEvaluator logger = logging.getLogger(__name__) @@ -37,7 +38,10 @@ def __init__( self.console = console or get_console() def evaluate( - self, plan: EvaluatablePlan, circuit_breaker: t.Optional[t.Callable[[], bool]] = None + self, + plan: EvaluatablePlan, + snapshot_evaluator: SnapshotEvaluator, + circuit_breaker: t.Optional[t.Callable[[], bool]] = None, ) -> None: plan_stages = stages.build_plan_stages(plan, self.state_reader, self.default_catalog) explainer_console = _get_explainer_console( diff --git a/sqlmesh/utils/__init__.py b/sqlmesh/utils/__init__.py index f102f23292..80e4fa5934 100644 --- a/sqlmesh/utils/__init__.py +++ b/sqlmesh/utils/__init__.py @@ -13,6 +13,7 @@ import types import typing as t import uuid +from dataclasses import dataclass from collections import defaultdict from contextlib import contextmanager from copy import deepcopy @@ -382,3 +383,23 @@ def to_snake_case(name: str) -> str: return "".join( f"_{c.lower()}" if c.isupper() and idx != 0 else c.lower() for idx, c in enumerate(name) ) + + +class JobType(Enum): + PLAN = "SQLMESH_PLAN" + RUN = "SQLMESH_RUN" + + +@dataclass(frozen=True) +class CorrelationId: + """ID that is added to each query in order to identify the job that created it.""" + + job_type: JobType + job_id: str + + def __str__(self) -> str: + return f"{self.job_type.value}: {self.job_id}" + + @classmethod + def from_plan_id(cls, plan_id: str) -> CorrelationId: + return CorrelationId(JobType.PLAN, plan_id) diff --git a/tests/conftest.py b/tests/conftest.py index 574c802c0e..a874bd7590 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,7 +42,7 @@ SnapshotDataVersion, SnapshotFingerprint, ) -from sqlmesh.utils import random_id +from sqlmesh.utils import random_id, CorrelationId from sqlmesh.utils.date import TimeLike, to_date from sqlmesh.utils.windows import IS_WINDOWS, fix_windows_path from sqlmesh.core.engine_adapter.shared import CatalogSupport @@ -266,10 +266,12 @@ def duck_conn() -> duckdb.DuckDBPyConnection: def push_plan(context: Context, plan: Plan) -> None: plan_evaluator = BuiltInPlanEvaluator( context.state_sync, - context.snapshot_evaluator, context.create_scheduler, context.default_catalog, ) + plan_evaluator.snapshot_evaluator = context.snapshot_evaluator( + CorrelationId.from_plan_id(plan.plan_id) + ) deployability_index = DeployabilityIndex.create(context.snapshots.values()) evaluatable_plan = plan.to_evaluatable() stages = plan_stages.build_plan_stages( diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 766a788ac8..f68cb7ac47 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -67,6 +67,7 @@ SnapshotInfoLike, SnapshotTableInfo, ) +from sqlmesh.utils import CorrelationId from sqlmesh.utils.date import TimeLike, now, to_date, to_datetime, to_timestamp from sqlmesh.utils.errors import NoChangesPlanError, SQLMeshError, PlanError, ConfigError from sqlmesh.utils.pydantic import validate_string @@ -1137,7 +1138,7 @@ def test_non_breaking_change_after_forward_only_in_dev( init_and_plan_context: t.Callable, has_view_binding: bool ): context, plan = init_and_plan_context("examples/sushi") - context.snapshot_evaluator.adapter.HAS_VIEW_BINDING = has_view_binding + context.snapshot_evaluator().adapter.HAS_VIEW_BINDING = has_view_binding context.apply(plan) model = context.get_model("sushi.waiter_revenue_by_day") @@ -6793,3 +6794,29 @@ def test_scd_type_2_full_restatement_no_start_date(init_and_plan_context: t.Call # valid_from should be the epoch, valid_to should be NaT assert str(row["valid_from"]) == "1970-01-01 00:00:00" assert pd.isna(row["valid_to"]) + + +def test_plan_evaluator_correlation_id(tmp_path: Path): + def _correlation_id_in_sqls(correlation_id: CorrelationId, mock_logger): + sqls = [call[0][0] for call in mock_logger.call_args_list] + return any(f"/* {correlation_id} */" in sql for sql in sqls) + + create_temp_file( + tmp_path, Path("models") / "test.sql", "MODEL (name test.a, kind FULL); SELECT 1 AS col" + ) + + # Case 1: Ensure that the correlation id (plan_id) is included in the SQL + with mock.patch("sqlmesh.core.engine_adapter.base.EngineAdapter._log_sql") as mock_logger: + ctx = Context(paths=[tmp_path], config=Config()) + plan = ctx.plan(auto_apply=True, no_prompts=True) + + correlation_id = CorrelationId.from_plan_id(plan.plan_id) + assert str(correlation_id) == f"SQLMESH_PLAN: {plan.plan_id}" + + assert _correlation_id_in_sqls(correlation_id, mock_logger) + + # Case 2: Ensure that the previous correlation id is not included in the SQL for other operations + with mock.patch("sqlmesh.core.engine_adapter.base.EngineAdapter._log_sql") as mock_logger: + ctx.snapshot_evaluator().adapter.execute("SELECT 1") + + assert not _correlation_id_in_sqls(correlation_id, mock_logger) diff --git a/tests/core/test_plan_evaluator.py b/tests/core/test_plan_evaluator.py index a784644b6b..467c3e60bd 100644 --- a/tests/core/test_plan_evaluator.py +++ b/tests/core/test_plan_evaluator.py @@ -11,6 +11,7 @@ stages as plan_stages, ) from sqlmesh.core.snapshot import SnapshotChangeCategory +from sqlmesh.utils import CorrelationId @pytest.fixture @@ -59,11 +60,14 @@ def test_builtin_evaluator_push(sushi_context: Context, make_snapshot): evaluator = BuiltInPlanEvaluator( sushi_context.state_sync, - sushi_context.snapshot_evaluator, sushi_context.create_scheduler, sushi_context.default_catalog, console=sushi_context.console, ) + evaluator.snapshot_evaluator = sushi_context.snapshot_evaluator( + CorrelationId.from_plan_id(plan.plan_id) + ) + evaluatable_plan = plan.to_evaluatable() stages = plan_stages.build_plan_stages( evaluatable_plan, sushi_context.state_sync, sushi_context.default_catalog diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index ee4ab0ac73..1b5c39e2dd 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -335,11 +335,11 @@ def test_generated_sql(sushi_context_fixed_date: Context, mocker: MockerFixture) sample_query_sql = 'WITH "source_only" AS (SELECT \'source_only\' AS "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "s_exists" = 1 AND "row_joined" = 0 ORDER BY "s__key" NULLS FIRST LIMIT 20), "target_only" AS (SELECT \'target_only\' AS "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "t_exists" = 1 AND "row_joined" = 0 ORDER BY "t__key" NULLS FIRST LIMIT 20), "common_rows" AS (SELECT \'common_rows\' AS "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "row_joined" = 1 AND "row_full_match" = 0 ORDER BY "s__key" NULLS FIRST, "t__key" NULLS FIRST LIMIT 20) SELECT "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "source_only" UNION ALL SELECT "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "target_only" UNION ALL SELECT "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "common_rows"' drop_sql = 'DROP TABLE IF EXISTS "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh"' - # make with_log_level() return the current instance of engine_adapter so we can still spy on _execute + # make with_settings() return the current instance of engine_adapter so we can still spy on _execute mocker.patch.object( - engine_adapter, "with_log_level", new_callable=lambda: lambda _: engine_adapter + engine_adapter, "with_settings", new_callable=lambda: lambda _: engine_adapter ) - assert engine_adapter.with_log_level(1) == engine_adapter + assert engine_adapter.with_settings(1) == engine_adapter spy_execute = mocker.spy(engine_adapter, "_execute") mocker.patch("sqlmesh.core.engine_adapter.base.random_id", return_value="abcdefgh") From 84d53419d7321e826492a6d5350aac3ae8351551 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:07:30 +0300 Subject: [PATCH 0488/1056] Fix: bug in model creation related to metadata-only statements (#4836) --- sqlmesh/core/model/definition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 6ccd0927cd..16e596b43a 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2431,7 +2431,7 @@ def _create_model( if not issubclass(klass, SqlModel): defaults.pop("optimize_query", None) - statements = [] + statements: t.List[t.Union[exp.Expression, t.Tuple[exp.Expression, bool]]] = [] if "pre_statements" in kwargs: statements.extend(kwargs["pre_statements"]) @@ -2453,7 +2453,7 @@ def _create_model( statements.extend(property_values.expressions) jinja_macro_references, used_variables = extract_macro_references_and_variables( - *(gen(e) for e in statements) + *(gen(e if isinstance(e, exp.Expression) else e[0]) for e in statements) ) if jinja_macros: From 0060cab8850eee814451b5efb2cbae5c5e287508 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:23:07 +0300 Subject: [PATCH 0489/1056] Feat: add dtntz date macro variant (#4838) --- docs/concepts/macros/macro_variables.md | 6 +++++ docs/integrations/engines/redshift.md | 34 +++++++++++++++++++++++++ sqlmesh/utils/date.py | 5 ++++ tests/core/test_model.py | 25 ++++++++++++++++++ tests/utils/test_date.py | 4 +++ 5 files changed, 74 insertions(+) diff --git a/docs/concepts/macros/macro_variables.md b/docs/concepts/macros/macro_variables.md index 79484626ff..858bf9f19d 100644 --- a/docs/concepts/macros/macro_variables.md +++ b/docs/concepts/macros/macro_variables.md @@ -68,6 +68,7 @@ Prefixes: Postfixes: * dt - A python datetime object that converts into a native SQL `TIMESTAMP` (or SQL engine equivalent) +* dtntz - A python datetime object that converts into a native SQL `TIMESTAMP WITHOUT TIME ZONE` (or SQL engine equivalent) * date - A python date object that converts into a native SQL `DATE` * ds - A date string with the format: '%Y-%m-%d' * ts - An ISO 8601 datetime formatted string: '%Y-%m-%d %H:%M:%S' @@ -83,6 +84,11 @@ All predefined temporal macro variables: * @end_dt * @execution_dt +* dtntz + * @start_dtntz + * @end_dtntz + * @execution_dtntz + * date * @start_date * @end_date diff --git a/docs/integrations/engines/redshift.md b/docs/integrations/engines/redshift.md index 9341844f7d..0b853dfee1 100644 --- a/docs/integrations/engines/redshift.md +++ b/docs/integrations/engines/redshift.md @@ -33,3 +33,37 @@ pip install "sqlmesh[redshift]" | `serverless_acct_id` | The account ID of the serverless cluster | string | N | | `serverless_work_group` | The name of work group for serverless end point | string | N | | `enable_merge` | Whether the incremental_by_unique_key model kind will use the native Redshift MERGE operation or SQLMesh's logical merge. (Default: `False`) | bool | N | + +## Performance Considerations + +### Timestamp Macro Variables and Sort Keys + +When working with Redshift tables that have a `TIMESTAMP` sort key, using the standard `@start_dt` and `@end_dt` macro variables may lead to performance issues. These macros render as `TIMESTAMP WITH TIME ZONE` values in SQL queries, which prevents Redshift from performing efficient pruning when filtering against `TIMESTAMP` (without timezone) sort keys. + +This can result in full table scans instead, causing significant performance degradation. + +**Solution**: Use the `_dtntz` (datetime no timezone) variants of macro variables: + +- `@start_dtntz` instead of `@start_dt` +- `@end_dtntz` instead of `@end_dt` + +These variants render as `TIMESTAMP WITHOUT TIME ZONE`, allowing Redshift to properly utilize sort key optimizations. + +**Example**: + +```sql linenums="1" +-- Inefficient: May cause full table scan +SELECT * FROM my_table +WHERE timestamp_column >= @start_dt + AND timestamp_column < @end_dt + +-- Efficient: Uses sort key optimization +SELECT * FROM my_table +WHERE timestamp_column >= @start_dtntz + AND timestamp_column < @end_dtntz + +-- Alternative: Cast to timestamp +SELECT * FROM my_table +WHERE timestamp_column >= @start_ts::timestamp + AND timestamp_column < @end_ts::timestamp +``` diff --git a/sqlmesh/utils/date.py b/sqlmesh/utils/date.py index 6c5787470e..53a53cd62a 100644 --- a/sqlmesh/utils/date.py +++ b/sqlmesh/utils/date.py @@ -253,8 +253,12 @@ def date_dict( for prefix, time_like in prefixes: dt = to_datetime(time_like) + dtntz = dt.replace(tzinfo=None) + millis = to_timestamp(time_like) + kwargs[f"{prefix}_dt"] = dt + kwargs[f"{prefix}_dtntz"] = dtntz kwargs[f"{prefix}_date"] = to_date(dt) kwargs[f"{prefix}_ds"] = to_ds(time_like) kwargs[f"{prefix}_ts"] = to_ts(dt) @@ -262,6 +266,7 @@ def date_dict( kwargs[f"{prefix}_epoch"] = millis / 1000 kwargs[f"{prefix}_millis"] = millis kwargs[f"{prefix}_hour"] = dt.hour + return kwargs diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 4fc82875d7..0913fe56c0 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -10518,3 +10518,28 @@ def test_boolean_property_validation() -> None: ) model = load_sql_based_model(expressions, dialect="tsql") assert model.enabled + + +def test_datetime_without_timezone_variable_redshift() -> None: + expressions = d.parse( + """ + MODEL ( + name test, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column test_time_col, + batch_size 1, + batch_concurrency 1 + ), + start '2025-06-01', + dialect redshift + ); + + SELECT @start_dtntz AS test_time_col + """ + ) + model = load_sql_based_model(expressions, dialect="redshift") + + assert ( + model.render_query_or_raise().sql("redshift") + == '''SELECT CAST('1970-01-01 00:00:00' AS TIMESTAMP) AS "test_time_col"''' + ) diff --git a/tests/utils/test_date.py b/tests/utils/test_date.py index d892817969..cb35a6973c 100644 --- a/tests/utils/test_date.py +++ b/tests/utils/test_date.py @@ -258,6 +258,10 @@ def test_date_dict(): "execution_dt": datetime(2020, 1, 2, 1, 0, 0, tzinfo=UTC), "start_dt": datetime(2020, 1, 1, 0, 0, 0, tzinfo=UTC), "end_dt": datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), + "latest_dtntz": datetime(2020, 1, 2, 1, 0, 0, tzinfo=None), + "execution_dtntz": datetime(2020, 1, 2, 1, 0, 0, tzinfo=None), + "start_dtntz": datetime(2020, 1, 1, 0, 0, 0, tzinfo=None), + "end_dtntz": datetime(2020, 1, 2, 0, 0, 0, tzinfo=None), "latest_date": date(2020, 1, 2), "execution_date": date(2020, 1, 2), "start_date": date(2020, 1, 1), From bdaba6825dda1f5447a6e6d5a192cf360a198597 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:40:31 +0100 Subject: [PATCH 0490/1056] Chore(deps): Bump pnpm/action-setup from 2 to 4 (#4855) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release_extension.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_extension.yaml b/.github/workflows/release_extension.yaml index 3807bc3440..376791d434 100644 --- a/.github/workflows/release_extension.yaml +++ b/.github/workflows/release_extension.yaml @@ -32,7 +32,7 @@ jobs: with: node-version: '20' - name: Install pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v4 with: version: 10 - name: Install dependencies From 2c828b3e04dd19e3b9fb73b1cfa33e674bd17e61 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:41:49 +0100 Subject: [PATCH 0491/1056] fix(vscode): fix windows linage graph (#4857) --- vscode/react/src/pages/lineage.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/vscode/react/src/pages/lineage.tsx b/vscode/react/src/pages/lineage.tsx index 546e1ad4bf..77f66fa85b 100644 --- a/vscode/react/src/pages/lineage.tsx +++ b/vscode/react/src/pages/lineage.tsx @@ -99,8 +99,10 @@ function Lineage() { } // @ts-ignore const fileUri: string = activeFile.fileUri - const filePath = URI.parse(fileUri).fsPath - const model = models.find((m: Model) => m.full_path === filePath) + const filePath = URI.file(fileUri).path + const model = models.find( + (m: Model) => URI.file(m.full_path).path === filePath, + ) if (model) { return model.name } @@ -129,9 +131,9 @@ function Lineage() { React.useEffect(() => { const handleChangeFocusedFile = (fileUri: { fileUri: string }) => { - const full_path = URI.parse(fileUri.fileUri).fsPath + const full_path = URI.parse(fileUri.fileUri).path const model = Object.values(modelsRecord).find( - m => m.full_path === full_path, + m => URI.file(m.full_path).path === full_path, ) if (model) { setSelectedModel(model.name) From 4316e49f32f4881a0ea60555d36cb740dcde8462 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:23:40 +0100 Subject: [PATCH 0492/1056] chore(vscode): test .env virtual environmnet (#4858) --- vscode/extension/tests/venv_naming.spec.ts | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 vscode/extension/tests/venv_naming.spec.ts diff --git a/vscode/extension/tests/venv_naming.spec.ts b/vscode/extension/tests/venv_naming.spec.ts new file mode 100644 index 0000000000..f20246c9bb --- /dev/null +++ b/vscode/extension/tests/venv_naming.spec.ts @@ -0,0 +1,48 @@ +import { test } from '@playwright/test' +import fs from 'fs-extra' +import os from 'os' +import path from 'path' +import { + createVirtualEnvironment, + openLineageView, + pipInstall, + REPO_ROOT, + SUSHI_SOURCE_PATH, +} from './utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' + +test('venv being named .env', async ({ page }, testInfo) => { + testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + + const pythonEnvDir = path.join(tempDir, '.env') + const pythonDetails = await createVirtualEnvironment(pythonEnvDir) + const custom_materializations = path.join( + REPO_ROOT, + 'examples', + 'custom_materializations', + ) + const sqlmeshWithExtras = `${REPO_ROOT}[bigquery,lsp]` + await pipInstall(pythonDetails, [sqlmeshWithExtras, custom_materializations]) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + const settings = { + 'python.defaultInterpreterPath': pythonDetails.pythonPath, + } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { + spaces: 2, + }) + + const context = await startCodeServer({ tempDir }) + + try { + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await openLineageView(page) + await page.waitForSelector('text=Loaded SQLMesh Context') + } finally { + await stopCodeServer(context) + } +}) From f3615dd76dea9d076aab44f8a84328acbdc23c5e Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 1 Jul 2025 16:40:51 +0300 Subject: [PATCH 0493/1056] Chore!: bump sqlglot to v26.33.0 (#4859) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 25d490df7f..c5b99720d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.32.0", + "sqlglot[rs]~=26.33.0", "tenacity", "time-machine", "json-stream" From fa1cd8bedae33cbdf6bab8fea30f1f63dbac7c59 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Tue, 1 Jul 2025 09:55:03 -0500 Subject: [PATCH 0494/1056] Chore: pin duckdb>=1.2.0 in motherduck extra (#4854) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c5b99720d0..f6077fe20b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,7 +107,7 @@ duckdb = [] gcppostgres = ["cloud-sql-python-connector[pg8000]>=1.8.0"] github = ["PyGithub~=2.5.0"] llm = ["langchain", "openai"] -motherduck = [] +motherduck = ["duckdb>=1.2.0"] mssql = ["pymssql"] mssql-odbc = ["pyodbc"] mysql = ["pymysql"] From 68eec222f7c8b279b53ced1caf960a6248d5a7e6 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:18:17 -0500 Subject: [PATCH 0495/1056] Fix: execution_time should default to now() (#4852) --- sqlmesh/core/context.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 402ed22fee..1be6ca1dac 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -121,7 +121,6 @@ from sqlmesh.utils.dag import DAG from sqlmesh.utils.date import ( TimeLike, - now_ds, to_timestamp, format_tz_datetime, now_timestamp, @@ -1001,7 +1000,7 @@ def render( Returns: The rendered expression. """ - execution_time = execution_time or now_ds() + execution_time = execution_time or now() model = self.get_model(model_or_snapshot, raise_if_missing=True) From ee3878e1b73aad0ab4dfbb261a5ca649f61f8622 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 1 Jul 2025 09:16:57 -0700 Subject: [PATCH 0496/1056] Chore: Remove dbt-sqlserver dependencies (#4862) --- pyproject.toml | 1 - tests/dbt/test_config.py | 8 -------- 2 files changed, 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f6077fe20b..046fbee025 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,6 @@ dev = [ "dbt-clickhouse", "dbt-databricks", "dbt-redshift", - "dbt-sqlserver>=1.7.0;python_version<'3.13'", "dbt-trino", "Faker", "google-auth", diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index bb8806c657..84610d778b 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -1,6 +1,5 @@ import base64 import typing as t -import sys from pathlib import Path from shutil import copytree @@ -950,13 +949,6 @@ def test_db_type_to_column_class(): assert (TARGET_TYPE_TO_CONFIG_CLASS["duckdb"].column_class) == Column assert (TARGET_TYPE_TO_CONFIG_CLASS["snowflake"].column_class) == SnowflakeColumn - if sys.version_info < (3, 13): - # The dbt-sqlserver package is not currently compatible with Python 3.13 - - from dbt.adapters.sqlserver.sqlserver_column import SQLServerColumn - - assert (TARGET_TYPE_TO_CONFIG_CLASS["sqlserver"].column_class) == SQLServerColumn - from dbt.adapters.clickhouse.column import ClickHouseColumn from dbt.adapters.trino.column import TrinoColumn from dbt.adapters.athena.column import AthenaColumn From 98992435136c25791383669c247e1f455fc15df3 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:32:02 +0300 Subject: [PATCH 0497/1056] Feat!: improve signal CLI UX (#4812) --- sqlmesh/core/console.py | 168 ++++++++++++++++++++++++++-- sqlmesh/core/scheduler.py | 17 ++- sqlmesh/core/snapshot/definition.py | 42 ++++++- tests/cli/test_cli.py | 143 ++++++++++++++++++++++- tests/core/test_scheduler.py | 2 +- web/server/api/endpoints/plan.py | 3 +- 6 files changed, 356 insertions(+), 19 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 6b600dcd73..01dfc3d09d 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -330,7 +330,36 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> """ +class SignalConsole(abc.ABC): + @abc.abstractmethod + def start_signal_progress( + self, + snapshot: Snapshot, + default_catalog: t.Optional[str], + environment_naming_info: EnvironmentNamingInfo, + ) -> None: + """Indicates that signal checking has begun for a snapshot.""" + + @abc.abstractmethod + def update_signal_progress( + self, + snapshot: Snapshot, + signal_name: str, + signal_idx: int, + total_signals: int, + ready_intervals: Intervals, + check_intervals: Intervals, + duration: float, + ) -> None: + """Updates the signal checking progress.""" + + @abc.abstractmethod + def stop_signal_progress(self) -> None: + """Indicates that signal checking has completed for a snapshot.""" + + class Console( + SignalConsole, PlanBuilderConsole, LinterConsole, StateExporterConsole, @@ -536,6 +565,29 @@ def update_snapshot_evaluation_progress( def stop_evaluation_progress(self, success: bool = True) -> None: pass + def start_signal_progress( + self, + snapshot: Snapshot, + default_catalog: t.Optional[str], + environment_naming_info: EnvironmentNamingInfo, + ) -> None: + pass + + def update_signal_progress( + self, + snapshot: Snapshot, + signal_name: str, + signal_idx: int, + total_signals: int, + ready_intervals: Intervals, + check_intervals: Intervals, + duration: float, + ) -> None: + pass + + def stop_signal_progress(self) -> None: + pass + def start_creation_progress( self, snapshots: t.List[Snapshot], @@ -860,6 +912,8 @@ def __init__( self.table_diff_model_tasks: t.Dict[str, TaskID] = {} self.table_diff_progress_live: t.Optional[Live] = None + self.signal_status_tree: t.Optional[Tree] = None + self.verbosity = verbosity self.dialect = dialect self.ignore_warnings = ignore_warnings @@ -901,6 +955,9 @@ def start_evaluation_progress( audit_only: bool = False, ) -> None: """Indicates that a new snapshot evaluation/auditing progress has begun.""" + # Add a newline to separate signal checking from evaluation + self._print("") + if not self.evaluation_progress_live: self.evaluation_total_progress = make_progress_bar( "Executing model batches" if not audit_only else "Auditing models", self.console @@ -1050,6 +1107,88 @@ def stop_evaluation_progress(self, success: bool = True) -> None: self.environment_naming_info = EnvironmentNamingInfo() self.default_catalog = None + def start_signal_progress( + self, + snapshot: Snapshot, + default_catalog: t.Optional[str], + environment_naming_info: EnvironmentNamingInfo, + ) -> None: + """Indicates that signal checking has begun for a snapshot.""" + display_name = snapshot.display_name( + environment_naming_info, + default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, + dialect=self.dialect, + ) + self.signal_status_tree = Tree(f"Checking signals for {display_name}") + + def update_signal_progress( + self, + snapshot: Snapshot, + signal_name: str, + signal_idx: int, + total_signals: int, + ready_intervals: Intervals, + check_intervals: Intervals, + duration: float, + ) -> None: + """Updates the signal checking progress.""" + tree = Tree(f"[{signal_idx + 1}/{total_signals}] {signal_name} {duration:.2f}s") + + formatted_check_intervals = [_format_signal_interval(snapshot, i) for i in check_intervals] + formatted_ready_intervals = [_format_signal_interval(snapshot, i) for i in ready_intervals] + + if not formatted_check_intervals: + formatted_check_intervals = ["no intervals"] + if not formatted_ready_intervals: + formatted_ready_intervals = ["no intervals"] + + # Color coding to help detect partial interval ranges quickly + if ready_intervals == check_intervals: + msg = "All ready" + color = "green" + elif ready_intervals: + msg = "Some ready" + color = "yellow" + else: + msg = "None ready" + color = "red" + + if self.verbosity < Verbosity.VERY_VERBOSE: + num_check_intervals = len(formatted_check_intervals) + if num_check_intervals > 3: + formatted_check_intervals = formatted_check_intervals[:3] + formatted_check_intervals.append(f"... and {num_check_intervals - 3} more") + + num_ready_intervals = len(formatted_ready_intervals) + if num_ready_intervals > 3: + formatted_ready_intervals = formatted_ready_intervals[:3] + formatted_ready_intervals.append(f"... and {num_ready_intervals - 3} more") + + check = ", ".join(formatted_check_intervals) + tree.add(f"Check: {check}") + + ready = ", ".join(formatted_ready_intervals) + tree.add(f"[{color}]{msg}: {ready}[/{color}]") + else: + check_tree = Tree("Check") + tree.add(check_tree) + for interval in formatted_check_intervals: + check_tree.add(interval) + + ready_tree = Tree(f"[{color}]{msg}[/{color}]") + tree.add(ready_tree) + for interval in formatted_ready_intervals: + ready_tree.add(f"[{color}]{interval}[/{color}]") + + if self.signal_status_tree is not None: + self.signal_status_tree.add(tree) + + def stop_signal_progress(self) -> None: + """Indicates that signal checking has completed for a snapshot.""" + if self.signal_status_tree is not None: + self._print(self.signal_status_tree) + self.signal_status_tree = None + def start_creation_progress( self, snapshots: t.List[Snapshot], @@ -3810,19 +3949,34 @@ def _format_audits_errors(error: NodeAuditsErrors) -> str: return " " + "\n".join(error_messages) +def _format_interval(snapshot: Snapshot, interval: Interval) -> str: + """Format an interval with an optional prefix.""" + inclusive_interval = make_inclusive(interval[0], interval[1]) + if snapshot.model.interval_unit.is_date_granularity: + return f"{to_ds(inclusive_interval[0])} - {to_ds(inclusive_interval[1])}" + + if inclusive_interval[0].date() == inclusive_interval[1].date(): + # omit end date if interval start/end on same day + return f"{to_ds(inclusive_interval[0])} {inclusive_interval[0].strftime('%H:%M:%S')}-{inclusive_interval[1].strftime('%H:%M:%S')}" + + return f"{inclusive_interval[0].strftime('%Y-%m-%d %H:%M:%S')} - {inclusive_interval[1].strftime('%Y-%m-%d %H:%M:%S')}" + + +def _format_signal_interval(snapshot: Snapshot, interval: Interval) -> str: + """Format an interval for signal output (without 'insert' prefix).""" + return _format_interval(snapshot, interval) + + def _format_evaluation_model_interval(snapshot: Snapshot, interval: Interval) -> str: + """Format an interval for evaluation output (with 'insert' prefix).""" if snapshot.is_model and ( snapshot.model.kind.is_incremental or snapshot.model.kind.is_managed or snapshot.model.kind.is_custom ): - inclusive_interval = make_inclusive(interval[0], interval[1]) - if snapshot.model.interval_unit.is_date_granularity: - return f"insert {to_ds(inclusive_interval[0])} - {to_ds(inclusive_interval[1])}" - # omit end date if interval start/end on same day - if inclusive_interval[0].date() == inclusive_interval[1].date(): - return f"insert {to_ds(inclusive_interval[0])} {inclusive_interval[0].strftime('%H:%M:%S')}-{inclusive_interval[1].strftime('%H:%M:%S')}" - return f"insert {inclusive_interval[0].strftime('%Y-%m-%d %H:%M:%S')} - {inclusive_interval[1].strftime('%Y-%m-%d %H:%M:%S')}" + formatted_interval = _format_interval(snapshot, interval) + return f"insert {formatted_interval}" + return "" diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index d0a121a40a..8bac0bf081 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -269,6 +269,7 @@ def batch_intervals( self, merged_intervals: SnapshotToIntervals, deployability_index: t.Optional[DeployabilityIndex], + environment_naming_info: EnvironmentNamingInfo, ) -> t.Dict[Snapshot, Intervals]: dag = snapshots_to_dag(merged_intervals) @@ -303,7 +304,13 @@ def batch_intervals( default_catalog=self.default_catalog, ) - intervals = snapshot.check_ready_intervals(intervals, context) + intervals = snapshot.check_ready_intervals( + intervals, + context, + console=self.console, + default_catalog=self.default_catalog, + environment_naming_info=environment_naming_info, + ) unready -= set(intervals) for parent in snapshot.parents: @@ -324,10 +331,14 @@ def batch_intervals( ): batches.append((next_batch[0][0], next_batch[-1][-1])) next_batch = [] + next_batch.append(interval) + if next_batch: batches.append((next_batch[0][0], next_batch[-1][-1])) + snapshot_batches[snapshot] = batches + return snapshot_batches def run_merged_intervals( @@ -359,7 +370,9 @@ def run_merged_intervals( """ execution_time = execution_time or now_timestamp() - batched_intervals = self.batch_intervals(merged_intervals, deployability_index) + batched_intervals = self.batch_intervals( + merged_intervals, deployability_index, environment_naming_info + ) self.console.start_evaluation_progress( batched_intervals, diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index ecd547664f..c12a415093 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1,6 +1,7 @@ from __future__ import annotations import sys +import time import typing as t from collections import defaultdict from datetime import datetime, timedelta @@ -50,6 +51,7 @@ from sqlmesh.utils.pydantic import PydanticModel, field_validator if t.TYPE_CHECKING: + from sqlmesh.core.console import Console from sqlglot.dialects.dialect import DialectType from sqlmesh.core.environment import EnvironmentNamingInfo from sqlmesh.core.context import ExecutionContext @@ -965,7 +967,14 @@ def missing_intervals( model_end_ts, ) - def check_ready_intervals(self, intervals: Intervals, context: ExecutionContext) -> Intervals: + def check_ready_intervals( + self, + intervals: Intervals, + context: ExecutionContext, + console: t.Optional[Console] = None, + default_catalog: t.Optional[str] = None, + environment_naming_info: t.Optional[EnvironmentNamingInfo] = None, + ) -> Intervals: """Returns a list of intervals that are considered ready by the provided signal. Note that this will handle gaps in the provided intervals. The returned intervals @@ -979,7 +988,19 @@ def check_ready_intervals(self, intervals: Intervals, context: ExecutionContext) python_env = self.model.python_env env = prepare_env(python_env) - for signal_name, kwargs in signals.items(): + if console: + console.start_signal_progress( + self, + default_catalog, + environment_naming_info or EnvironmentNamingInfo(), + ) + + for signal_idx, (signal_name, kwargs) in enumerate(signals.items()): + # Capture intervals before signal check for display + intervals_to_check = merge_intervals(intervals) + + signal_start_ts = time.perf_counter() + try: intervals = _check_ready_intervals( env[signal_name], @@ -996,6 +1017,23 @@ def check_ready_intervals(self, intervals: Intervals, context: ExecutionContext) f"{e} '{signal_name}' for '{self.model.name}' at {self.model._path}" ) + duration = time.perf_counter() - signal_start_ts + + if console: + console.update_signal_progress( + snapshot=self, + signal_name=signal_name, + signal_idx=signal_idx, + total_signals=len(signals), + ready_intervals=merge_intervals(intervals), + check_intervals=intervals_to_check, + duration=duration, + ) + + # Stop signal progress tracking + if console: + console.stop_signal_progress() + return intervals def categorize_as(self, category: SnapshotChangeCategory) -> None: diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 6972db7d67..9078e9832d 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1,15 +1,16 @@ +import json import logging +import pytest import string +import time_machine from contextlib import contextmanager from os import getcwd, path, remove from pathlib import Path from shutil import rmtree +from unittest.mock import MagicMock + from click import ClickException -import pytest from click.testing import CliRunner -import time_machine -import json -from unittest.mock import MagicMock from sqlmesh import RuntimeEnv from sqlmesh.cli.project_init import ProjectTemplate, init_example_project from sqlmesh.cli.main import cli @@ -42,13 +43,13 @@ def disable_logging(): logging.disable(logging.NOTSET) -def create_example_project(temp_dir) -> None: +def create_example_project(temp_dir, template=ProjectTemplate.DEFAULT) -> None: """ Sets up CLI tests requiring a real SQLMesh project by: - Creating the SQLMesh example project in the temp_dir directory - Overwriting the config.yaml file so the duckdb database file will be created in the temp_dir directory """ - init_example_project(temp_dir, engine_type="duckdb") + init_example_project(temp_dir, engine_type="duckdb", template=template) with open(temp_dir / "config.yaml", "w", encoding="utf-8") as f: f.write( f"""gateways: @@ -2044,3 +2045,133 @@ def test_render(runner: CliRunner, tmp_path: Path): """ assert expected in cleaned_output + + +@time_machine.travel(FREEZE_TIME) +def test_signals(runner: CliRunner, tmp_path: Path): + create_example_project(tmp_path, template=ProjectTemplate.EMPTY) + + # Create signals module + signals_dir = tmp_path / "signals" + signals_dir.mkdir(exist_ok=True) + + # Create signal definitions + (signals_dir / "signal.py").write_text( + """from sqlmesh import signal +@signal() +def only_first_two_ready(batch): + if len(batch) > 2: + return batch[:2] + return batch + +@signal() +def none_ready(batch): + return False +""" + ) + + # Create model with signals + (tmp_path / "models" / "model_with_signals.sql").write_text( + """MODEL ( + name sqlmesh_example.model_with_signals, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds + ), + start '2022-12-28', + cron '@daily', + signals [ + only_first_two_ready() + ] +); + +SELECT + ds::DATE as ds, + 'test' as value +FROM VALUES + ('2022-12-28'), + ('2022-12-29'), + ('2022-12-30'), + ('2022-12-31'), + ('2023-01-01') +AS t(ds) +WHERE ds::DATE BETWEEN @start_ds AND @end_ds +""" + ) + + # Create model with no ready intervals + (tmp_path / "models" / "model_with_unready.sql").write_text( + """MODEL ( + name sqlmesh_example.model_with_unready, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds + ), + start '2022-12-28', + cron '@daily', + signals [ + none_ready() + ] +); + +SELECT + ds::DATE as ds, + 'unready' as value +FROM VALUES + ('2022-12-28'), + ('2022-12-29'), + ('2022-12-30'), + ('2022-12-31'), + ('2023-01-01') +AS t(ds) +WHERE ds::DATE BETWEEN @start_ds AND @end_ds +""" + ) + + # Test 1: Normal plan flow with --no-prompts --auto-apply + result = runner.invoke( + cli, + [ + "--paths", + str(tmp_path), + "plan", + "--no-prompts", + "--auto-apply", + ], + ) + assert result.exit_code == 0 + + assert "Checking signals for sqlmesh_example.model_with_signals" in result.output + assert "[1/1] only_first_two_ready" in result.output + assert "Check: 2022-12-28 - 2022-12-31" in result.output + assert "Some ready: 2022-12-28 - 2022-12-29" in result.output + + assert "Checking signals for sqlmesh_example.model_with_unready" in result.output + assert "[1/1] none_ready" in result.output + assert "None ready: no intervals" in result.output + + # Test 2: Run command with start and end dates + result = runner.invoke( + cli, + [ + "--paths", + str(tmp_path), + "run", + "--start", + "2022-12-29", + "--end", + "2022-12-31", + ], + ) + assert result.exit_code == 0 + + assert "Checking signals for sqlmesh_example.model_with_signals" in result.output + assert "[1/1] only_first_two_ready" in result.output + assert "Check: 2022-12-30 - 2022-12-31" in result.output + assert "All ready: 2022-12-30 - 2022-12-31" in result.output + + assert "Checking signals for sqlmesh_example.model_with_unready" in result.output + assert "[1/1] none_ready" in result.output + assert "Check: 2022-12-29 - 2022-12-31" in result.output + assert "None ready: no intervals" in result.output + + # Only one model was executed + assert "100.0% • 1/1 • 0:00:00" in result.output diff --git a/tests/core/test_scheduler.py b/tests/core/test_scheduler.py index aab767d85f..742642794f 100644 --- a/tests/core/test_scheduler.py +++ b/tests/core/test_scheduler.py @@ -77,7 +77,7 @@ def _get_batched_missing_intervals( execution_time: t.Optional[TimeLike] = None, ) -> SnapshotToIntervals: merged_intervals = scheduler.merged_missing_intervals(start, end, execution_time) - return scheduler.batch_intervals(merged_intervals, mocker.Mock()) + return scheduler.batch_intervals(merged_intervals, mocker.Mock(), mocker.Mock()) return _get_batched_missing_intervals diff --git a/web/server/api/endpoints/plan.py b/web/server/api/endpoints/plan.py index 47bab6626e..ba4feb34d3 100644 --- a/web/server/api/endpoints/plan.py +++ b/web/server/api/endpoints/plan.py @@ -7,6 +7,7 @@ from starlette.status import HTTP_204_NO_CONTENT from sqlmesh.core.context import Context +from sqlmesh.core.environment import EnvironmentNamingInfo from sqlmesh.core.plan import Plan, PlanBuilder from sqlmesh.core.snapshot.definition import SnapshotChangeCategory from sqlmesh.utils.date import make_inclusive, to_ds @@ -132,7 +133,7 @@ def _get_plan_changes(context: Context, plan: Plan) -> models.PlanChanges: def _get_plan_backfills(context: Context, plan: Plan) -> t.Dict[str, t.Any]: """Get plan backfills""" merged_intervals = context.scheduler().merged_missing_intervals() - batches = context.scheduler().batch_intervals(merged_intervals, None) + batches = context.scheduler().batch_intervals(merged_intervals, None, EnvironmentNamingInfo()) tasks = {snapshot.name: len(intervals) for snapshot, intervals in batches.items()} snapshots = plan.context_diff.snapshots default_catalog = context.default_catalog From c50663c54688f587eda203bb52a355a954eddeed Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:42:55 +0100 Subject: [PATCH 0498/1056] chore: speed up tests when running locally (#4863) --- vscode/extension/tests/global-setup.ts | 35 ++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/vscode/extension/tests/global-setup.ts b/vscode/extension/tests/global-setup.ts index ff0013dea1..df721b58f3 100644 --- a/vscode/extension/tests/global-setup.ts +++ b/vscode/extension/tests/global-setup.ts @@ -1,6 +1,7 @@ import { execSync } from 'child_process' import path from 'path' import fs from 'fs-extra' +import { createHash } from 'crypto' async function globalSetup() { console.log('Setting up extension for Playwright tests...') @@ -10,8 +11,6 @@ async function globalSetup() { const extensionsDir = path.join(testSetupDir, 'extensions') // Clean up any existing test setup directory - await fs.remove(testSetupDir) - await fs.ensureDir(extensionsDir) // Get the extension version from package.json const packageJson = JSON.parse( @@ -30,14 +29,32 @@ async function globalSetup() { ) } - console.log(`Installing extension: ${vsixFileName}`) - // Create a temporary user data directory for the installation const tempUserDataDir = await fs.mkdtemp( path.join(require('os').tmpdir(), 'vscode-test-install-user-data-'), ) try { + // Check if in .test_setup there is a extension hash file which contains the hash of the extension + // If it does, check if the hash is the same as the hash of the extension in the vsix file + // If it is, skip the installation + // If it is not, remove the extension hash file and install the extension + const extensionHashFile = path.join(testSetupDir, 'extension-hash.txt') + console.log('extensionHashFile', extensionHashFile) + if (fs.existsSync(extensionHashFile)) { + const extensionHash = fs.readFileSync(extensionHashFile, 'utf-8') + const vsixHash = await hashFile(vsixPath) + if (extensionHash === vsixHash) { + console.log('Extension already installed') + return + } + } + + await fs.remove(testSetupDir) + await fs.ensureDir(testSetupDir) + await fs.ensureDir(extensionsDir) + + console.log(`Installing extension: ${vsixFileName}`) execSync( `pnpm run code-server --user-data-dir "${tempUserDataDir}" --extensions-dir "${extensionsDir}" --install-extension "${vsixPath}"`, { @@ -45,11 +62,19 @@ async function globalSetup() { cwd: extensionDir, }, ) - console.log('Extension installed successfully to .test_setup/extensions') + + // Write the hash of the extension to the extension hash file + const extensionHash = await hashFile(vsixPath) + await fs.writeFile(extensionHashFile, extensionHash) } finally { // Clean up temporary user data directory await fs.remove(tempUserDataDir) } } +async function hashFile(filePath: string): Promise { + const fileBuffer = await fs.readFile(filePath) + return createHash('sha256').update(fileBuffer).digest('hex') +} + export default globalSetup From a3302e40cc7673da87bb0f9a5ff583da052e4cab Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 1 Jul 2025 18:01:51 +0100 Subject: [PATCH 0499/1056] chore(vscode): starts testing the actual contents of the lineage view (#4865) --- vscode/extension/tests/broken_project.spec.ts | 77 +++++++++++++++++-- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index 1f9955fd0e..85996692a3 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test' +import { test, expect } from '@playwright/test' import fs from 'fs-extra' import os from 'os' import path from 'path' @@ -95,8 +95,29 @@ test('working project, then broken through adding double model, then refixed', a await saveFile(page) // Wait for the error to appear - // TODO: Selector doesn't work in the linage view - // await window.waitForSelector('text=Error') + const iframes = page.locator('iframe') + const iframeCount = await iframes.count() + let errorCount = 0 + + for (let i = 0; i < iframeCount; i++) { + const iframe = iframes.nth(i) + const contentFrame = iframe.contentFrame() + if (contentFrame) { + const activeFrame = contentFrame.locator('#active-frame').contentFrame() + if (activeFrame) { + try { + await activeFrame + .getByText('Error: Failed to load model') + .waitFor({ timeout: 1000 }) + errorCount++ + } catch { + // Continue to next iframe if this one doesn't have the error + continue + } + } + } + } + expect(errorCount).toBeGreaterThan(0) // Remove the duplicated model to fix the project await fs.remove(path.join(tempDir, 'models', 'customers_duplicated.sql')) @@ -104,9 +125,29 @@ test('working project, then broken through adding double model, then refixed', a // Save again to refresh the context await saveFile(page) - // Wait for the error to go away and context to reload - // TODO: Selector doesn't work in the linage view - // await page.waitForSelector('text=raw.demographics') + const iframes2 = page.locator('iframe') + const iframeCount2 = await iframes2.count() + let raw_demographicsCount = 0 + + for (let i = 0; i < iframeCount2; i++) { + const iframe = iframes2.nth(i) + const contentFrame = iframe.contentFrame() + if (contentFrame) { + const activeFrame = contentFrame.locator('#active-frame').contentFrame() + if (activeFrame) { + try { + await activeFrame + .getByText('raw.demographics') + .waitFor({ timeout: 1000 }) + raw_demographicsCount++ + } catch { + // Continue to next iframe if this one doesn't have the error + continue + } + } + } + } + expect(raw_demographicsCount).toBeGreaterThan(0) } finally { await stopCodeServer(context) } @@ -158,8 +199,28 @@ test('bad project, double model, then fixed', async ({ page }) => { await openLineageView(page) // Wait for the error to go away - // TODO: Selector doesn't work in the linage view - // await page.waitForSelector('text=raw.demographics') + const iframes = page.locator('iframe') + const iframeCount = await iframes.count() + let raw_demographicsCount = 0 + + for (let i = 0; i < iframeCount; i++) { + const iframe = iframes.nth(i) + const contentFrame = iframe.contentFrame() + if (contentFrame) { + const activeFrame = contentFrame.locator('#active-frame').contentFrame() + if (activeFrame) { + try { + await activeFrame + .getByText('sushi.customers') + .waitFor({ timeout: 1000 }) + raw_demographicsCount++ + } catch { + continue + } + } + } + } + expect(raw_demographicsCount).toBeGreaterThan(0) } finally { await stopCodeServer(context) } From 445645b07860d4f8eb4461dba554cd88f2660ce7 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 1 Jul 2025 20:56:12 +0300 Subject: [PATCH 0500/1056] Chore: Add check for dot env to avoid python env conflicts (#4866) --- sqlmesh/core/config/loader.py | 6 ++-- tests/core/test_config.py | 60 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index f1ef0ed5a7..f40646131d 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -37,11 +37,13 @@ def load_configs( for p in (glob.glob(str(path)) or [str(path)]) ] - if dotenv_path: + if dotenv_path and dotenv_path.exists() and dotenv_path.is_file(): load_dotenv(dotenv_path=dotenv_path, override=True) else: for path in absolute_paths: - load_dotenv(dotenv_path=path / ".env", override=True) + env_file = path / ".env" + if env_file.exists() and env_file.is_file(): + load_dotenv(dotenv_path=env_file, override=True) if not isinstance(config, str): if type(config) != config_type: diff --git a/tests/core/test_config.py b/tests/core/test_config.py index dd07c8395f..a33b06eca9 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -1238,6 +1238,66 @@ def test_load_yaml_config_dot_env_vars(tmp_path_factory): ) +def test_load_config_dotenv_directory_not_loaded(tmp_path_factory): + main_dir = tmp_path_factory.mktemp("config_with_env_dir") + config_path = main_dir / "config.yaml" + with open(config_path, "w", encoding="utf-8") as fd: + fd.write( + """gateways: + test_gateway: + connection: + type: duckdb + database: test.db +model_defaults: + dialect: duckdb +""" + ) + + # Create a .env directory instead of a file to simulate a Python virtual environment + env_dir = main_dir / ".env" + env_dir.mkdir() + (env_dir / "pyvenv.cfg").touch() + + # Also create a regular .env file in another project directory + other_dir = tmp_path_factory.mktemp("config_with_env_file") + other_config_path = other_dir / "config.yaml" + with open(other_config_path, "w", encoding="utf-8") as fd: + fd.write( + """gateways: + test_gateway: + connection: + type: duckdb + database: test.db +model_defaults: + dialect: duckdb +""" + ) + + env_file = other_dir / ".env" + with open(env_file, "w", encoding="utf-8") as fd: + fd.write('TEST_ENV_VAR="from_dotenv_file"') + + # Test that the .env directory doesn't cause an error and is skipped + with mock.patch.dict(os.environ, {}, clear=True): + load_configs( + "config", + Config, + paths=[main_dir], + ) + # Should succeed without loading any env vars from the directory + assert "TEST_ENV_VAR" not in os.environ + + # Test that a real .env file is still loaded properly + with mock.patch.dict(os.environ, {}, clear=True): + load_configs( + "config", + Config, + paths=[other_dir], + ) + # The env var should be loaded from the file + assert os.environ.get("TEST_ENV_VAR") == "from_dotenv_file" + + def test_load_yaml_config_custom_dotenv_path(tmp_path_factory): main_dir = tmp_path_factory.mktemp("yaml_config_2") config_path = main_dir / "config.yaml" From 1e04ffe193f1e5cd7b000692042ee29d370accab Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:37:59 -0700 Subject: [PATCH 0501/1056] fix: put limit on lru cache (#4867) --- sqlmesh/core/dialect.py | 2 +- sqlmesh/core/macros.py | 2 +- sqlmesh/core/snapshot/definition.py | 6 +++--- sqlmesh/utils/cron.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 6406fa8864..074d758490 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -1175,7 +1175,7 @@ def set_default_catalog( return table -@lru_cache(maxsize=None) +@lru_cache(maxsize=16384) def normalize_model_name( table: str | exp.Table | exp.Column, default_catalog: t.Optional[str], diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index d573fdac31..4afb5fd334 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -1558,6 +1558,6 @@ def _convert_sql(v: t.Any, dialect: DialectType) -> t.Any: return v -@lru_cache(maxsize=1028) +@lru_cache(maxsize=16384) def _cache_convert_sql(v: t.Any, dialect: DialectType, t: type) -> t.Any: return _convert_sql(v, dialect) diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index c12a415093..e84a1fce27 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1905,7 +1905,7 @@ def missing_intervals( return missing -@lru_cache(maxsize=None) +@lru_cache(maxsize=16384) def expand_range(start_ts: int, end_ts: int, interval_unit: IntervalUnit) -> t.List[int]: croniter = interval_unit.croniter(start_ts) timestamps = [start_ts] @@ -1922,7 +1922,7 @@ def expand_range(start_ts: int, end_ts: int, interval_unit: IntervalUnit) -> t.L return timestamps -@lru_cache(maxsize=None) +@lru_cache(maxsize=16384) def compute_missing_intervals( interval_unit: IntervalUnit, intervals: t.Tuple[Interval, ...], @@ -1984,7 +1984,7 @@ def compute_missing_intervals( return sorted(missing) -@lru_cache(maxsize=None) +@lru_cache(maxsize=16384) def inclusive_exclusive( start: TimeLike, end: TimeLike, diff --git a/sqlmesh/utils/cron.py b/sqlmesh/utils/cron.py index c6080b1db7..904202db7c 100644 --- a/sqlmesh/utils/cron.py +++ b/sqlmesh/utils/cron.py @@ -10,7 +10,7 @@ from sqlmesh.utils.date import TimeLike, now, to_datetime -@lru_cache(maxsize=None) +@lru_cache(maxsize=16384) def interval_seconds(cron: str) -> int: """Computes the interval seconds of a cron statement if it is deterministic. From 649fdf003b3e9c92f784a64269374e4ae020270d Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:04:53 +0100 Subject: [PATCH 0502/1056] docs: move all vens to use .venv (#4860) --- docs/guides/projects.md | 6 +++--- docs/guides/ui.md | 2 +- docs/installation.md | 4 ++-- docs/quickstart/cli.md | 2 +- docs/quickstart/notebook.md | 2 +- docs/quickstart/ui.md | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/guides/projects.md b/docs/guides/projects.md index 6101d9eaed..e4dabd76cc 100644 --- a/docs/guides/projects.md +++ b/docs/guides/projects.md @@ -27,18 +27,18 @@ To create a project from the command line, follow these steps: 1. To scaffold a project, it is recommended that you use a python virtual environment by running the following commands: ```bash - python -m venv .env + python -m venv .venv ``` ```bash - source .env/bin/activate + source .venv/bin/activate ``` ```bash pip install sqlmesh ``` - **Note:** When using a python virtual environment, you must ensure that it is activated first. You should see `(.env)` in your command line; if you don't, run `source .env/bin/activate` from your project directory to activate your environment. + **Note:** When using a python virtual environment, you must ensure that it is activated first. You should see `(.venv)` in your command line; if you don't, run `source .venv/bin/activate` from your project directory to activate your environment. 1. Once you have activated your environment, run the following command and SQLMesh will build out your project: diff --git a/docs/guides/ui.md b/docs/guides/ui.md index a77d608e77..1da48e50b3 100644 --- a/docs/guides/ui.md +++ b/docs/guides/ui.md @@ -24,7 +24,7 @@ For development work, we recommend using the SQLMesh UI alongside an IDE. The UI Before beginning, ensure that you meet all the [prerequisites](../prerequisites.md) for using SQLMesh. The SQLMesh browser UI requires additional Python libraries not included in the base SQLMesh installation. -To use the UI, install SQLMesh with the `web` add-on. First, if using a python virtual environment, ensure it's activated by running `source .env/bin/activate` command from the folder used during [installation](../installation.md). +To use the UI, install SQLMesh with the `web` add-on. First, if using a python virtual environment, ensure it's activated by running `source .venv/bin/activate` command from the folder used during [installation](../installation.md). Next, install the UI with `pip`: diff --git a/docs/installation.md b/docs/installation.md index aed92c05b6..f12ec566e2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -8,12 +8,12 @@ It is recommended, but not required, that you use a python virtual environment w First, create the virtual environment: ```bash -python -m venv .env +python -m venv .venv ``` Then activate it: ```bash -source .env/bin/activate +source .venv/bin/activate ``` ## Install SQLMesh core diff --git a/docs/quickstart/cli.md b/docs/quickstart/cli.md index c89da59b75..7b77b2af1e 100644 --- a/docs/quickstart/cli.md +++ b/docs/quickstart/cli.md @@ -41,7 +41,7 @@ mkdir sqlmesh-example cd sqlmesh-example ``` -If using a Python virtual environment, ensure it's activated first by running the `source .env/bin/activate` command from the folder used during [installation](../installation.md). +If using a Python virtual environment, ensure it's activated first by running the `source .venv/bin/activate` command from the folder used during [installation](../installation.md). ### 1.1 Initialize the project diff --git a/docs/quickstart/notebook.md b/docs/quickstart/notebook.md index 003a2c741b..a1dae6b822 100644 --- a/docs/quickstart/notebook.md +++ b/docs/quickstart/notebook.md @@ -34,7 +34,7 @@ The notebook interface works with both Jupyter and Databricks notebooks. Learn m ## 1. Create the SQLMesh project First, create a SQLMesh project directory with your operating system's graphical or command-line tools. Next, create a Jupyter or Databricks notebook file - it does not need to be in the SQLMesh project directory. -If using a python virtual environment, ensure it's activated first by running the `source .env/bin/activate` command from the folder used during [installation](../installation.md). +If using a python virtual environment, ensure it's activated first by running the `source .venv/bin/activate` command from the folder used during [installation](../installation.md). Import the SQLMesh library to load the notebook magic commands: diff --git a/docs/quickstart/ui.md b/docs/quickstart/ui.md index af57efcf19..06a9d8c448 100644 --- a/docs/quickstart/ui.md +++ b/docs/quickstart/ui.md @@ -31,7 +31,7 @@ In this quickstart, you'll use the SQLMesh browser user interface to get up and Before beginning, ensure that you meet all the [prerequisites](../prerequisites.md) for using SQLMesh. The SQLMesh browser UI requires additional Python libraries not included in the base SQLMesh installation. -To use the UI, install SQLMesh with the `web` add-on. First, if using a python virtual environment, ensure it's activated by running `source .env/bin/activate` command from the folder used during [installation](../installation.md). +To use the UI, install SQLMesh with the `web` add-on. First, if using a python virtual environment, ensure it's activated by running `source .venv/bin/activate` command from the folder used during [installation](../installation.md). Next, install the UI with `pip`: @@ -52,7 +52,7 @@ Navigate to the directory on the command line: cd sqlmesh-example ``` -If using a python virtual environment, ensure it's activated by running `source .env/bin/activate` from the folder used during [installation](../installation.md). +If using a python virtual environment, ensure it's activated by running `source .venv/bin/activate` from the folder used during [installation](../installation.md). Create a SQLMesh scaffold with the following command, specifying a default SQL dialect for your models. The dialect should correspond to the dialect most of your models are written in; it can be overridden for specific models in the model's `MODEL` specification. All SQL dialects [supported by the SQLGlot library](https://github.com/tobymao/sqlglot/blob/main/sqlglot/dialects/dialect.py) are allowed. From 83137ffede8ebe0248758c55996bc72ac6a497cb Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 2 Jul 2025 13:07:22 +1200 Subject: [PATCH 0503/1056] Fix: Normalize when_matched and merge_filter expressions to the source dialect (#4847) --- sqlmesh/core/dialect.py | 16 +- sqlmesh/core/model/definition.py | 2 +- sqlmesh/core/model/kind.py | 19 +- ...ize_quote_when_matched_and_merge_filter.py | 9 + sqlmesh/utils/pydantic.py | 8 + tests/core/test_model.py | 197 +++++++++++++----- tests/core/test_snapshot_evaluator.py | 76 ++++--- tests/dbt/test_config.py | 6 +- 8 files changed, 247 insertions(+), 86 deletions(-) create mode 100644 sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 074d758490..88e09f4916 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -1388,17 +1388,27 @@ def is_meta_expression(v: t.Any) -> bool: return isinstance(v, (Audit, Metric, Model)) -def replace_merge_table_aliases(expression: exp.Expression) -> exp.Expression: +def replace_merge_table_aliases( + expression: exp.Expression, dialect: t.Optional[str] = None +) -> exp.Expression: """ Resolves references from the "source" and "target" tables (or their DBT equivalents) with the corresponding SQLMesh merge aliases (MERGE_SOURCE_ALIAS and MERGE_TARGET_ALIAS) """ from sqlmesh.core.engine_adapter.base import MERGE_SOURCE_ALIAS, MERGE_TARGET_ALIAS + normalized_merge_source_alias = quote_identifiers( + normalize_identifiers(exp.to_identifier(MERGE_SOURCE_ALIAS), dialect), dialect=dialect + ) + + normalized_merge_target_alias = quote_identifiers( + normalize_identifiers(exp.to_identifier(MERGE_TARGET_ALIAS), dialect), dialect=dialect + ) + if isinstance(expression, exp.Column) and (first_part := expression.parts[0]): if first_part.this.lower() in ("target", "dbt_internal_dest", "__merge_target__"): - first_part.replace(exp.to_identifier(MERGE_TARGET_ALIAS)) + first_part.replace(normalized_merge_target_alias) elif first_part.this.lower() in ("source", "dbt_internal_source", "__merge_source__"): - first_part.replace(exp.to_identifier(MERGE_SOURCE_ALIAS)) + first_part.replace(normalized_merge_source_alias) return expression diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 16e596b43a..6dce3e6843 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -653,7 +653,7 @@ def render_merge_filter( ) if len(rendered_exprs) != 1: raise SQLMeshError(f"Expected one expression but got {len(rendered_exprs)}") - return rendered_exprs[0].transform(d.replace_merge_table_aliases) + return rendered_exprs[0].transform(d.replace_merge_table_aliases, dialect=self.dialect) def _render_properties( self, properties: t.Dict[str, exp.Expression] | SessionProperties, **render_kwargs: t.Any diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index 4a15023f2f..9dc54f4b83 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -34,6 +34,7 @@ get_dialect, validate_string, positive_int_validator, + validate_expression, ) @@ -467,15 +468,20 @@ def _when_matched_validator( return v if isinstance(v, list): v = " ".join(v) + + dialect = get_dialect(info.data) + if isinstance(v, str): # Whens wrap the WHEN clauses, but the parentheses aren't parsed by sqlglot v = v.strip() if v.startswith("("): v = v[1:-1] - return t.cast(exp.Whens, d.parse_one(v, into=exp.Whens, dialect=get_dialect(info.data))) + v = t.cast(exp.Whens, d.parse_one(v, into=exp.Whens, dialect=dialect)) + else: + v = t.cast(exp.Whens, v.transform(d.replace_merge_table_aliases, dialect=dialect)) - return t.cast(exp.Whens, v.transform(d.replace_merge_table_aliases)) + return validate_expression(v, dialect=dialect) @field_validator("merge_filter", mode="before") def _merge_filter_validator( @@ -485,11 +491,16 @@ def _merge_filter_validator( ) -> t.Optional[exp.Expression]: if v is None: return v + + dialect = get_dialect(info.data) + if isinstance(v, str): v = v.strip() - return d.parse_one(v, dialect=get_dialect(info.data)) + v = d.parse_one(v, dialect=dialect) + else: + v = v.transform(d.replace_merge_table_aliases, dialect=dialect) - return v.transform(d.replace_merge_table_aliases) + return validate_expression(v, dialect=dialect) @property def data_hash_values(self) -> t.List[t.Optional[str]]: diff --git a/sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py b/sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py new file mode 100644 index 0000000000..24a6db9384 --- /dev/null +++ b/sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py @@ -0,0 +1,9 @@ +""" +Normalize and quote the when_matched and merge_filter properties of IncrementalByUniqueKeyKind +to match how other properties (such as time_column and partitioned_by) are handled and to +prevent un-normalized identifiers being quoted at the EngineAdapter level +""" + + +def migrate(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/utils/pydantic.py b/sqlmesh/utils/pydantic.py index 3de15773a3..317e873aeb 100644 --- a/sqlmesh/utils/pydantic.py +++ b/sqlmesh/utils/pydantic.py @@ -16,6 +16,8 @@ from sqlmesh.utils import str_to_bool if t.TYPE_CHECKING: + from sqlglot._typing import E + Model = t.TypeVar("Model", bound="PydanticModel") @@ -193,6 +195,12 @@ def validate_string(v: t.Any) -> str: return str(v) +def validate_expression(expression: E, dialect: str) -> E: + # this normalizes and quotes identifiers in the given expression according the specified dialect + # it also sets expression.meta["dialect"] so that when we serialize for state, the expression is serialized in the correct dialect + return _get_field(expression, {"dialect": dialect}) # type: ignore + + def bool_validator(v: t.Any) -> bool: if isinstance(v, exp.Boolean): return v.this diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 0913fe56c0..6fe0ccc87d 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -5366,13 +5366,13 @@ def test_when_matched(): """ ) - expected_when_matched = "(WHEN MATCHED THEN UPDATE SET __MERGE_TARGET__.salary = COALESCE(__MERGE_SOURCE__.salary, __MERGE_TARGET__.salary))" + expected_when_matched = "(WHEN MATCHED THEN UPDATE SET `__merge_target__`.`salary` = COALESCE(`__merge_source__`.`salary`, `__merge_target__`.`salary`))" model = load_sql_based_model(expressions, dialect="hive") - assert model.kind.when_matched.sql() == expected_when_matched + assert model.kind.when_matched.sql(dialect="hive") == expected_when_matched model = SqlModel.parse_raw(model.json()) - assert model.kind.when_matched.sql() == expected_when_matched + assert model.kind.when_matched.sql(dialect="hive") == expected_when_matched expressions = d.parse( """ @@ -5400,9 +5400,9 @@ def test_when_matched(): kind INCREMENTAL_BY_UNIQUE_KEY ( unique_key ("purchase_order_id"), when_matched ( - WHEN MATCHED AND __MERGE_SOURCE__._operation = 1 THEN DELETE - WHEN MATCHED AND __MERGE_SOURCE__._operation <> 1 THEN UPDATE SET - __MERGE_TARGET__.purchase_order_id = 1 + WHEN MATCHED AND "__merge_source__"."_operation" = 1 THEN DELETE + WHEN MATCHED AND "__merge_source__"."_operation" <> 1 THEN UPDATE SET + "__merge_target__"."purchase_order_id" = 1 ), batch_concurrency 1, forward_only FALSE, @@ -5453,7 +5453,7 @@ def fingerprint_merge( kind INCREMENTAL_BY_UNIQUE_KEY ( unique_key ("purchase_order_id"), when_matched ( - WHEN MATCHED AND __MERGE_SOURCE__.salary <> __MERGE_TARGET__.salary THEN UPDATE SET + WHEN MATCHED AND "__merge_source__"."salary" <> "__merge_target__"."salary" THEN UPDATE SET ARRAY('target.update_datetime = source.update_datetime', 'target.salary = source.salary') ), batch_concurrency 1, @@ -5487,21 +5487,21 @@ def test_when_matched_multiple(): ) expected_when_matched = [ - "WHEN MATCHED AND __MERGE_SOURCE__.x = 1 THEN UPDATE SET __MERGE_TARGET__.salary = COALESCE(__MERGE_SOURCE__.salary, __MERGE_TARGET__.salary)", - "WHEN MATCHED THEN UPDATE SET __MERGE_TARGET__.salary = COALESCE(__MERGE_SOURCE__.salary, __MERGE_TARGET__.salary)", + "WHEN MATCHED AND `__merge_source__`.`x` = 1 THEN UPDATE SET `__merge_target__`.`salary` = COALESCE(`__merge_source__`.`salary`, `__merge_target__`.`salary`)", + "WHEN MATCHED THEN UPDATE SET `__merge_target__`.`salary` = COALESCE(`__merge_source__`.`salary`, `__merge_target__`.`salary`)", ] model = load_sql_based_model(expressions, dialect="hive", variables={"schema": "db"}) whens = model.kind.when_matched assert len(whens.expressions) == 2 - assert whens.expressions[0].sql() == expected_when_matched[0] - assert whens.expressions[1].sql() == expected_when_matched[1] + assert whens.expressions[0].sql(dialect="hive") == expected_when_matched[0] + assert whens.expressions[1].sql(dialect="hive") == expected_when_matched[1] model = SqlModel.parse_raw(model.json()) whens = model.kind.when_matched assert len(whens.expressions) == 2 - assert whens.expressions[0].sql() == expected_when_matched[0] - assert whens.expressions[1].sql() == expected_when_matched[1] + assert whens.expressions[0].sql(dialect="hive") == expected_when_matched[0] + assert whens.expressions[1].sql(dialect="hive") == expected_when_matched[1] def test_when_matched_merge_filter_multi_part_columns(): @@ -5529,28 +5529,86 @@ def test_when_matched_merge_filter_multi_part_columns(): ) expected_when_matched = [ - "WHEN MATCHED AND __MERGE_SOURCE__.record.nested_record.field = 1 THEN UPDATE SET __MERGE_TARGET__.repeated_record.sub_repeated_record.sub_field = COALESCE(__MERGE_SOURCE__.repeated_record.sub_repeated_record.sub_field, __MERGE_TARGET__.repeated_record.sub_repeated_record.sub_field)", - "WHEN MATCHED THEN UPDATE SET __MERGE_TARGET__.repeated_record.sub_repeated_record.sub_field = COALESCE(__MERGE_SOURCE__.repeated_record.sub_repeated_record.sub_field, __MERGE_TARGET__.repeated_record.sub_repeated_record.sub_field)", + "WHEN MATCHED AND `__merge_source__`.`record`.`nested_record`.`field` = 1 THEN UPDATE SET `__merge_target__`.`repeated_record`.`sub_repeated_record`.`sub_field` = COALESCE(`__merge_source__`.`repeated_record`.`sub_repeated_record`.`sub_field`, `__merge_target__`.`repeated_record`.`sub_repeated_record`.`sub_field`)", + "WHEN MATCHED THEN UPDATE SET `__merge_target__`.`repeated_record`.`sub_repeated_record`.`sub_field` = COALESCE(`__merge_source__`.`repeated_record`.`sub_repeated_record`.`sub_field`, `__merge_target__`.`repeated_record`.`sub_repeated_record`.`sub_field`)", ] expected_merge_filter = ( - "__MERGE_SOURCE__.record.nested_record.field < __MERGE_TARGET__.record.nested_record.field AND " - "__MERGE_TARGET__.repeated_record.sub_repeated_record.sub_field > __MERGE_SOURCE__.repeated_record.sub_repeated_record.sub_field" + "`__merge_source__`.`record`.`nested_record`.`field` < `__merge_target__`.`record`.`nested_record`.`field` AND " + "`__merge_target__`.`repeated_record`.`sub_repeated_record`.`sub_field` > `__merge_source__`.`repeated_record`.`sub_repeated_record`.`sub_field`" ) model = load_sql_based_model(expressions, dialect="bigquery", variables={"schema": "db"}) whens = model.kind.when_matched assert len(whens.expressions) == 2 - assert whens.expressions[0].sql() == expected_when_matched[0] - assert whens.expressions[1].sql() == expected_when_matched[1] - assert model.merge_filter.sql() == expected_merge_filter + assert whens.expressions[0].sql(dialect="bigquery") == expected_when_matched[0] + assert whens.expressions[1].sql(dialect="bigquery") == expected_when_matched[1] + assert model.merge_filter.sql(dialect="bigquery") == expected_merge_filter model = SqlModel.parse_raw(model.json()) whens = model.kind.when_matched assert len(whens.expressions) == 2 - assert whens.expressions[0].sql() == expected_when_matched[0] - assert whens.expressions[1].sql() == expected_when_matched[1] - assert model.merge_filter.sql() == expected_merge_filter + assert whens.expressions[0].sql(dialect="bigquery") == expected_when_matched[0] + assert whens.expressions[1].sql(dialect="bigquery") == expected_when_matched[1] + assert model.merge_filter.sql(dialect="bigquery") == expected_merge_filter + + +def test_when_matched_normalization() -> None: + # unquoted should be normalized and quoted + expressions = d.parse( + """ + MODEL ( + name test.employees, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key name, + when_matched ( + WHEN MATCHED THEN UPDATE SET + target.key_a = source.key_a, + target.key_b = source.key_b, + ) + ) + ); + SELECT 'name' AS name, 1 AS key_a, 2 AS key_b; + """ + ) + model = load_sql_based_model(expressions, dialect="snowflake") + + assert isinstance(model.kind, IncrementalByUniqueKeyKind) + assert isinstance(model.kind.when_matched, exp.Whens) + first_expression = model.kind.when_matched.expressions[0] + assert isinstance(first_expression, exp.Expression) + assert ( + first_expression.sql(dialect="snowflake") + == 'WHEN MATCHED THEN UPDATE SET "__MERGE_TARGET__"."KEY_A" = "__MERGE_SOURCE__"."KEY_A", "__MERGE_TARGET__"."KEY_B" = "__MERGE_SOURCE__"."KEY_B"' + ) + + # quoted should be preserved + expressions = d.parse( + """ + MODEL ( + name test.employees, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key name, + when_matched ( + WHEN MATCHED THEN UPDATE SET + target."kEy_A" = source."kEy_A", + target."kEY_b" = source.key_b, + ) + ) + ); + SELECT 'name' AS name, 1 AS "kEy_A", 2 AS "kEY_b"; + """ + ) + model = load_sql_based_model(expressions, dialect="snowflake") + + assert isinstance(model.kind, IncrementalByUniqueKeyKind) + assert isinstance(model.kind.when_matched, exp.Whens) + first_expression = model.kind.when_matched.expressions[0] + assert isinstance(first_expression, exp.Expression) + assert ( + first_expression.sql(dialect="snowflake") + == 'WHEN MATCHED THEN UPDATE SET "__MERGE_TARGET__"."kEy_A" = "__MERGE_SOURCE__"."kEy_A", "__MERGE_TARGET__"."kEY_b" = "__MERGE_SOURCE__"."KEY_B"' + ) def test_default_catalog_sql(assert_exp_eq): @@ -6492,11 +6550,11 @@ def model_with_macros(evaluator, **kwargs): == "@IF(@gateway = 'dev', @'hdfs://@{catalog_name}/@{schema_name}/dev/@{table_name}', @'s3://prod/@{table_name}')" ) - # Merge_filter will stay unrendered as well + # merge_filter will stay unrendered as well assert python_sql_model.unique_key[0] == exp.column("a", quoted=True) assert ( python_sql_model.merge_filter.sql() - == "source.id > 0 AND target.updated_at < @end_ds AND source.updated_at > @start_ds" + == '"source"."id" > 0 AND "target"."updated_at" < @end_ds AND "source"."updated_at" > @start_ds' ) @@ -7583,7 +7641,7 @@ def test_model_kind_to_expression(): .sql() == """INCREMENTAL_BY_UNIQUE_KEY ( unique_key ("a"), -when_matched (WHEN MATCHED THEN UPDATE SET __MERGE_TARGET__.b = COALESCE(__MERGE_SOURCE__.b, __MERGE_TARGET__.b)), +when_matched (WHEN MATCHED THEN UPDATE SET "__merge_target__"."b" = COALESCE("__merge_source__"."b", "__merge_target__"."b")), batch_concurrency 1, forward_only FALSE, disable_restatement FALSE, @@ -7611,7 +7669,7 @@ def test_model_kind_to_expression(): .sql() == """INCREMENTAL_BY_UNIQUE_KEY ( unique_key ("a"), -when_matched (WHEN MATCHED AND __MERGE_SOURCE__.x = 1 THEN UPDATE SET __MERGE_TARGET__.b = COALESCE(__MERGE_SOURCE__.b, __MERGE_TARGET__.b) WHEN MATCHED THEN UPDATE SET __MERGE_TARGET__.b = COALESCE(__MERGE_SOURCE__.b, __MERGE_TARGET__.b)), +when_matched (WHEN MATCHED AND "__merge_source__"."x" = 1 THEN UPDATE SET "__merge_target__"."b" = COALESCE("__merge_source__"."b", "__merge_target__"."b") WHEN MATCHED THEN UPDATE SET "__merge_target__"."b" = COALESCE("__merge_source__"."b", "__merge_target__"."b")), batch_concurrency 1, forward_only FALSE, disable_restatement FALSE, @@ -7872,13 +7930,14 @@ def test_merge_filter(): """ ) - expected_incremental_predicate = f"{MERGE_SOURCE_ALIAS}.salary > 0" + expected_incremental_predicate = f"`{MERGE_SOURCE_ALIAS.lower()}`.`salary` > 0" model = load_sql_based_model(expressions, dialect="hive") - assert model.kind.merge_filter.sql() == expected_incremental_predicate + assert model.kind.merge_filter.sql(dialect="hive") == expected_incremental_predicate model = SqlModel.parse_raw(model.json()) - assert model.kind.merge_filter.sql() == expected_incremental_predicate + assert model.kind.merge_filter.sql(dialect="hive") == expected_incremental_predicate + assert model.dialect == "hive" expressions = d.parse( """ @@ -7894,7 +7953,7 @@ def test_merge_filter(): source.ds > (SELECT MAX(ds) FROM db.test) AND source.ds > @start_ds AND source._operation <> 1 AND - target.start_date > dateadd(day, -7, current_date) + target.start_date > date_add(current_date, interval 7 day) ) ) ); @@ -7906,26 +7965,27 @@ def test_merge_filter(): """ ) - model = SqlModel.parse_raw(load_sql_based_model(expressions).json()) - assert d.format_model_expressions(model.render_definition()) == ( + model = SqlModel.parse_raw(load_sql_based_model(expressions, dialect="duckdb").json()) + assert d.format_model_expressions(model.render_definition(), dialect=model.dialect) == ( f"""MODEL ( name db.test, + dialect duckdb, kind INCREMENTAL_BY_UNIQUE_KEY ( unique_key ("purchase_order_id"), when_matched ( - WHEN MATCHED AND {MERGE_SOURCE_ALIAS}._operation = 1 THEN DELETE - WHEN MATCHED AND {MERGE_SOURCE_ALIAS}._operation <> 1 THEN UPDATE SET - {MERGE_TARGET_ALIAS}.purchase_order_id = 1 + WHEN MATCHED AND "{MERGE_SOURCE_ALIAS.lower()}"."_operation" = 1 THEN DELETE + WHEN MATCHED AND "{MERGE_SOURCE_ALIAS.lower()}"."_operation" <> 1 THEN UPDATE SET + "{MERGE_TARGET_ALIAS.lower()}"."purchase_order_id" = 1 ), merge_filter ( - {MERGE_SOURCE_ALIAS}.ds > ( + "{MERGE_SOURCE_ALIAS.lower()}"."ds" > ( SELECT - MAX(ds) - FROM db.test + MAX("ds") + FROM "db"."test" ) - AND {MERGE_SOURCE_ALIAS}.ds > @start_ds - AND {MERGE_SOURCE_ALIAS}._operation <> 1 - AND {MERGE_TARGET_ALIAS}.start_date > DATEADD(day, -7, CURRENT_DATE) + AND "{MERGE_SOURCE_ALIAS.lower()}"."ds" > @start_ds + AND "{MERGE_SOURCE_ALIAS.lower()}"."_operation" <> 1 + AND "{MERGE_TARGET_ALIAS.lower()}"."start_date" > CURRENT_DATE + INTERVAL '7' DAY ), batch_concurrency 1, forward_only FALSE, @@ -7942,10 +8002,46 @@ def test_merge_filter(): rendered_merge_filters = model.render_merge_filter(start="2023-01-01", end="2023-01-02") assert ( - rendered_merge_filters.sql() - == "(__MERGE_SOURCE__.ds > (SELECT MAX(ds) FROM db.test) AND __MERGE_SOURCE__.ds > '2023-01-01' AND __MERGE_SOURCE__._operation <> 1 AND __MERGE_TARGET__.start_date > DATEADD(day, -7, CURRENT_DATE))" + rendered_merge_filters.sql(dialect="hive") + == "(`__merge_source__`.`ds` > (SELECT MAX(`ds`) FROM `db`.`test`) AND `__merge_source__`.`ds` > '2023-01-01' AND `__merge_source__`.`_operation` <> 1 AND `__merge_target__`.`start_date` > CURRENT_DATE + INTERVAL '7' DAY)" + ) + + +def test_merge_filter_normalization(): + # unquoted gets normalized and quoted + expressions = d.parse( + """ + MODEL ( + name db.employees, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key name, + merge_filter source.salary > 0 + ) + ); + SELECT 'name' AS name, 1 AS salary; + """ ) + model = load_sql_based_model(expressions, dialect="snowflake") + assert model.merge_filter.sql(dialect="snowflake") == '"__MERGE_SOURCE__"."SALARY" > 0' + + # quoted gets preserved + expressions = d.parse( + """ + MODEL ( + name db.employees, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key name, + merge_filter source."SaLArY" > 0 + ) + ); + SELECT 'name' AS name, 1 AS "SaLArY"; + """ + ) + + model = load_sql_based_model(expressions, dialect="snowflake") + assert model.merge_filter.sql(dialect="snowflake") == '"__MERGE_SOURCE__"."SaLArY" > 0' + def test_merge_filter_macro(): @macro() @@ -7969,19 +8065,20 @@ def predicate( """ ) - unrendered_merge_filter = ( - f"@predicate(update_datetime) AND {MERGE_TARGET_ALIAS}.update_datetime > @start_dt" + unrendered_merge_filter = f"""@predicate("UPDATE_DATETIME") AND "{MERGE_TARGET_ALIAS}"."UPDATE_DATETIME" > @start_dt""" + expected_merge_filter = ( + f"""\"{MERGE_SOURCE_ALIAS}"."UPDATE_DATETIME" > DATEADD(DAY, -7, "{MERGE_TARGET_ALIAS}"."UPDATE_DATETIME") """ + f"""AND "{MERGE_TARGET_ALIAS}"."UPDATE_DATETIME" > CAST('2023-01-01 15:00:00+00:00' AS TIMESTAMPTZ)""" ) - expected_merge_filter = f"{MERGE_SOURCE_ALIAS}.UPDATE_DATETIME > DATE_ADD({MERGE_TARGET_ALIAS}.UPDATE_DATETIME, -7, 'DAY') AND {MERGE_TARGET_ALIAS}.UPDATE_DATETIME > CAST('2023-01-01 15:00:00+00:00' AS TIMESTAMPTZ)" model = load_sql_based_model(expressions, dialect="snowflake") - assert model.kind.merge_filter.sql() == unrendered_merge_filter + assert model.kind.merge_filter.sql(dialect=model.dialect) == unrendered_merge_filter model = SqlModel.parse_raw(model.json()) - assert model.kind.merge_filter.sql() == unrendered_merge_filter + assert model.kind.merge_filter.sql(dialect=model.dialect) == unrendered_merge_filter rendered_merge_filters = model.render_merge_filter(start="2023-01-01 15:00:00") - assert rendered_merge_filters.sql() == expected_merge_filter + assert rendered_merge_filters.sql(dialect=model.dialect) == expected_merge_filter @pytest.mark.parametrize( diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 3704c192bd..e4de741cdb 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -2211,13 +2211,19 @@ def test_create_incremental_by_unique_key_updated_at_exp(adapter_mock, make_snap source=False, then=exp.Update( expressions=[ - exp.column("name", MERGE_TARGET_ALIAS).eq( - exp.column("name", MERGE_SOURCE_ALIAS) + exp.column("name", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( + exp.column("name", MERGE_SOURCE_ALIAS.lower(), quoted=True) ), - exp.column("updated_at", MERGE_TARGET_ALIAS).eq( + exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( exp.Coalesce( - this=exp.column("updated_at", MERGE_SOURCE_ALIAS), - expressions=[exp.column("updated_at", MERGE_TARGET_ALIAS)], + this=exp.column( + "updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True + ), + expressions=[ + exp.column( + "updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True + ) + ], ) ), ], @@ -2273,16 +2279,24 @@ def test_create_incremental_by_unique_key_multiple_updated_at_exp(adapter_mock, expressions=[ exp.When( matched=True, - condition=exp.column("id", MERGE_SOURCE_ALIAS).eq(exp.Literal.number(1)), + condition=exp.column("id", MERGE_SOURCE_ALIAS.lower(), quoted=True).eq( + exp.Literal.number(1) + ), then=exp.Update( expressions=[ - exp.column("name", MERGE_TARGET_ALIAS).eq( - exp.column("name", MERGE_SOURCE_ALIAS) + exp.column("name", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( + exp.column("name", MERGE_SOURCE_ALIAS.lower(), quoted=True) ), - exp.column("updated_at", MERGE_TARGET_ALIAS).eq( + exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( exp.Coalesce( - this=exp.column("updated_at", MERGE_SOURCE_ALIAS), - expressions=[exp.column("updated_at", MERGE_TARGET_ALIAS)], + this=exp.column( + "updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True + ), + expressions=[ + exp.column( + "updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True + ) + ], ) ), ], @@ -2293,13 +2307,19 @@ def test_create_incremental_by_unique_key_multiple_updated_at_exp(adapter_mock, source=False, then=exp.Update( expressions=[ - exp.column("name", MERGE_TARGET_ALIAS).eq( - exp.column("name", MERGE_SOURCE_ALIAS) + exp.column("name", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( + exp.column("name", MERGE_SOURCE_ALIAS.lower(), quoted=True) ), - exp.column("updated_at", MERGE_TARGET_ALIAS).eq( + exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( exp.Coalesce( - this=exp.column("updated_at", MERGE_SOURCE_ALIAS), - expressions=[exp.column("updated_at", MERGE_TARGET_ALIAS)], + this=exp.column( + "updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True + ), + expressions=[ + exp.column( + "updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True + ) + ], ) ), ], @@ -2384,16 +2404,16 @@ def test_create_incremental_by_unique_key_merge_filter(adapter_mock, make_snapsh assert model.merge_filter == exp.And( this=exp.And( this=exp.GT( - this=exp.column("id", MERGE_SOURCE_ALIAS), + this=exp.column("id", MERGE_SOURCE_ALIAS.lower(), quoted=True), expression=exp.Literal(this="0", is_string=False), ), expression=exp.LT( - this=exp.column("updated_at", MERGE_TARGET_ALIAS), + this=exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True), expression=d.MacroVar(this="end_ds"), ), ), expression=exp.GT( - this=exp.column("updated_at", MERGE_SOURCE_ALIAS), + this=exp.column("updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True), expression=d.MacroVar(this="start_ds"), ), ) @@ -2425,10 +2445,16 @@ def test_create_incremental_by_unique_key_merge_filter(adapter_mock, make_snapsh matched=True, then=exp.Update( expressions=[ - exp.column("updated_at", MERGE_TARGET_ALIAS).eq( + exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( exp.Coalesce( - this=exp.column("updated_at", MERGE_SOURCE_ALIAS), - expressions=[exp.column("updated_at", MERGE_TARGET_ALIAS)], + this=exp.column( + "updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True + ), + expressions=[ + exp.column( + "updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True + ) + ], ) ), ], @@ -2439,16 +2465,16 @@ def test_create_incremental_by_unique_key_merge_filter(adapter_mock, make_snapsh merge_filter=exp.And( this=exp.And( this=exp.GT( - this=exp.column("id", MERGE_SOURCE_ALIAS), + this=exp.column("id", MERGE_SOURCE_ALIAS.lower(), quoted=True), expression=exp.Literal(this="0", is_string=False), ), expression=exp.LT( - this=exp.column("updated_at", MERGE_TARGET_ALIAS), + this=exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True), expression=exp.Literal(this="2020-01-02", is_string=True), ), ), expression=exp.GT( - this=exp.column("updated_at", MERGE_SOURCE_ALIAS), + this=exp.column("updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True), expression=exp.Literal(this="2020-01-01", is_string=True), ), ), diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 84610d778b..cc5b05c8a5 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -96,7 +96,7 @@ def test_model_to_sqlmesh_fields(): cluster_by=["a", '"b"'], incremental_predicates=[ "55 > DBT_INTERNAL_SOURCE.b", - "DBT_INTERNAL_DEST.session_start > dateadd(day, -7, current_date)", + "DBT_INTERNAL_DEST.session_start > date_add(current_date, interval 7 day)", ], cron="@hourly", interval_unit="FIVE_MINUTE", @@ -134,8 +134,8 @@ def test_model_to_sqlmesh_fields(): assert kind.lookback == 3 assert kind.on_destructive_change == OnDestructiveChange.ALLOW assert ( - kind.merge_filter.sql() - == "55 > __MERGE_SOURCE__.b AND __MERGE_TARGET__.session_start > DATEADD(day, -7, CURRENT_DATE)" + kind.merge_filter.sql(dialect=model.dialect) + == """55 > "__merge_source__"."b" AND "__merge_target__"."session_start" > CURRENT_DATE + INTERVAL '7' DAY""" ) model = model_config.update_with({"dialect": "snowflake"}).to_sqlmesh(context) From 4137773ce5087869e7fadc4627852bb57984c90a Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:31:25 +0100 Subject: [PATCH 0504/1056] feat(vscode): add ability to turn on/off columns (#4826) --- vscode/extension/tests/broken_project.spec.ts | 2 +- .../extension/tests/lineage_settings.spec.ts | 66 +++++++++++++++++++ vscode/react/src/components/graph/CogIcon.tsx | 27 ++++++++ vscode/react/src/components/graph/Graph.css | 1 + .../src/components/graph/ModelLineage.tsx | 9 ++- .../react/src/components/graph/ModelNode.tsx | 5 +- .../src/components/graph/SettingsControl.tsx | 55 ++++++++++++++++ 7 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 vscode/extension/tests/lineage_settings.spec.ts create mode 100644 vscode/react/src/components/graph/CogIcon.tsx create mode 100644 vscode/react/src/components/graph/SettingsControl.tsx diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index 85996692a3..5638d167b2 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -137,7 +137,7 @@ test('working project, then broken through adding double model, then refixed', a if (activeFrame) { try { await activeFrame - .getByText('raw.demographics') + .getByText('sushi.customers') .waitFor({ timeout: 1000 }) raw_demographicsCount++ } catch { diff --git a/vscode/extension/tests/lineage_settings.spec.ts b/vscode/extension/tests/lineage_settings.spec.ts new file mode 100644 index 0000000000..c4b5a39dfa --- /dev/null +++ b/vscode/extension/tests/lineage_settings.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { openLineageView, SUSHI_SOURCE_PATH } from './utils' +import { startCodeServer, stopCodeServer } from './utils_code_server' + +test('Settings button is visible in the lineage view', async ({ page }) => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + const context = await startCodeServer({ + tempDir, + placeFileWithPythonInterpreter: true, + }) + + try { + await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + + await page.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + // Open the waiters.py model + await page + .getByRole('treeitem', { name: 'waiters.py', exact: true }) + .locator('a') + .click() + await page.waitForSelector('text=Loaded SQLMesh Context') + + // Open lineage + await openLineageView(page) + + const iframes = page.locator('iframe') + const iframeCount = await iframes.count() + let settingsCount = 0 + + for (let i = 0; i < iframeCount; i++) { + const iframe = iframes.nth(i) + const contentFrame = iframe.contentFrame() + if (contentFrame) { + const activeFrame = contentFrame.locator('#active-frame').contentFrame() + if (activeFrame) { + try { + await activeFrame + .getByRole('button', { + name: 'Settings', + }) + .waitFor({ timeout: 1000 }) + settingsCount++ + } catch { + // Continue to next iframe if this one doesn't have the error + continue + } + } + } + } + + expect(settingsCount).toBeGreaterThan(0) + } finally { + await stopCodeServer(context) + } +}) diff --git a/vscode/react/src/components/graph/CogIcon.tsx b/vscode/react/src/components/graph/CogIcon.tsx new file mode 100644 index 0000000000..75d3e952bd --- /dev/null +++ b/vscode/react/src/components/graph/CogIcon.tsx @@ -0,0 +1,27 @@ +import * as React from 'react' + +/** + * CogIcon as taken from https://heroicons.com/. Slightly modified to remove fill color. + * + * @param props - SVG props + * @returns SVG element + */ +export function CogIcon(props: React.SVGProps): JSX.Element { + return ( + + ) +} diff --git a/vscode/react/src/components/graph/Graph.css b/vscode/react/src/components/graph/Graph.css index 328e1c92f8..bef07d33b8 100644 --- a/vscode/react/src/components/graph/Graph.css +++ b/vscode/react/src/components/graph/Graph.css @@ -29,6 +29,7 @@ react-flow__attribution { box-shadow: none; border: var(--vscode-button-border); background: var(--vscode-button-background); + color: var(--vscode-foreground); } .react-flow__controls-button:hover { background: var(--vscode-button-hoverBackground); diff --git a/vscode/react/src/components/graph/ModelLineage.tsx b/vscode/react/src/components/graph/ModelLineage.tsx index 652c0631e4..fb3befc24e 100644 --- a/vscode/react/src/components/graph/ModelLineage.tsx +++ b/vscode/react/src/components/graph/ModelLineage.tsx @@ -36,6 +36,7 @@ import { Popover } from '@headlessui/react' import ModelLineageDetails from './ModelLineageDetails' import { Divider } from '@/components/divider/Divider' import { type ModelLineageApiLineageModelNameGet200 } from '@/api/client' +import { SettingsControl } from '@/components/graph/SettingsControl' import './Graph.css' const WITH_COLUMNS_LIMIT = 30 @@ -203,6 +204,7 @@ function ModelColumnLineage(): JSX.Element { showControls, handleError, setActiveNodes, + setWithColumns, } = useLineageFlow() const { setCenter } = useReactFlow() @@ -386,7 +388,12 @@ function ModelColumnLineage(): JSX.Element { + > + + 0 || activeNodes.size > 0 || withConnected ? isSelected || diff --git a/vscode/react/src/components/graph/SettingsControl.tsx b/vscode/react/src/components/graph/SettingsControl.tsx new file mode 100644 index 0000000000..2793528e1d --- /dev/null +++ b/vscode/react/src/components/graph/SettingsControl.tsx @@ -0,0 +1,55 @@ +import { Menu } from '@headlessui/react' +import { CheckIcon } from '@heroicons/react/24/outline' +import { CogIcon } from '@/components/graph/CogIcon' +import clsx from 'clsx' + +interface SettingsControlProps { + showColumns: boolean + onWithColumnsChange: (value: boolean) => void +} + +export function SettingsControl({ + showColumns, + onWithColumnsChange, +}: SettingsControlProps): JSX.Element { + return ( + + + + + + {({ active }) => ( + + )} + + + + ) +} From 2507f670e43fd4b057da4347583bbcd19ec20431 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:46:50 +0100 Subject: [PATCH 0505/1056] chore: make vscode tests less flaky (#4875) --- vscode/extension/tests/python_env.spec.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/vscode/extension/tests/python_env.spec.ts b/vscode/extension/tests/python_env.spec.ts index bbf0bafb36..95a726c25b 100644 --- a/vscode/extension/tests/python_env.spec.ts +++ b/vscode/extension/tests/python_env.spec.ts @@ -84,7 +84,9 @@ test.describe('python environment variable injection on sqlmesh_lsp', () => { } }) - test('normal setup - set', async ({ page }) => { + test('normal setup - set', async ({ page }, testInfo) => { + testInfo.setTimeout(120_000) + const [tempDir, _] = await setupEnvironment() writeEnvironmentConfig(tempDir) const env_file = path.join(tempDir, '.env') @@ -126,7 +128,9 @@ async function setupTcloudProject( } test.describe('tcloud version', () => { - test('normal setup - error ', async ({ page }) => { + test('normal setup - error ', async ({ page }, testInfo) => { + testInfo.setTimeout(120_000) + const [tempDir, pythonDetails] = await setupEnvironment() await setupTcloudProject(tempDir, pythonDetails) writeEnvironmentConfig(tempDir) @@ -141,7 +145,9 @@ test.describe('tcloud version', () => { } }) - test('normal setup - set', async ({ page }) => { + test('normal setup - set', async ({ page }, testInfo) => { + testInfo.setTimeout(120_000) + const [tempDir, pythonDetails] = await setupEnvironment() await setupTcloudProject(tempDir, pythonDetails) writeEnvironmentConfig(tempDir) From d2d8c2ff70a12adc7c262d4de9a5b132a13981b2 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:48:12 +0100 Subject: [PATCH 0506/1056] fix(vscode): allow undoing clicking of lineage columns (#4876) --- .../src/components/graph/ModelColumns.tsx | 4 ++-- .../react/src/components/graph/ModelNode.tsx | 8 +++++--- vscode/react/src/components/graph/context.tsx | 3 ++- vscode/react/src/domain/column.ts | 18 ++++++++++++++++++ 4 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 vscode/react/src/domain/column.ts diff --git a/vscode/react/src/components/graph/ModelColumns.tsx b/vscode/react/src/components/graph/ModelColumns.tsx index 8763beec61..7046907026 100644 --- a/vscode/react/src/components/graph/ModelColumns.tsx +++ b/vscode/react/src/components/graph/ModelColumns.tsx @@ -28,7 +28,6 @@ import { import clsx from 'clsx' import { type ColumnDescription, - type Column, type ColumnLineageApiLineageModelNameColumnNameGet200, type LineageColumn, type LineageColumnSource, @@ -47,6 +46,7 @@ import { useApiColumnLineage } from '@/api/index' import SourceList from '@/components/sourceList/SourceList' import type { Lineage } from '@/domain/lineage' import type { ModelName } from '@/domain/models' +import type { Column } from '@/domain/column' export default function ModelColumns({ nodeId, @@ -207,7 +207,7 @@ export default function ModelColumns({ id={toID(nodeId, column.name)} nodeId={nodeId} column={column} - disabled={true} + disabled={disabled} updateColumnLineage={updateColumnLineage} removeEdges={removeEdges} isActive={true} diff --git a/vscode/react/src/components/graph/ModelNode.tsx b/vscode/react/src/components/graph/ModelNode.tsx index 104b1b75c9..dda072ada0 100644 --- a/vscode/react/src/components/graph/ModelNode.tsx +++ b/vscode/react/src/components/graph/ModelNode.tsx @@ -5,9 +5,9 @@ import { ModelType, type Model } from '@/api/client' import { useLineageFlow } from './context' import { type GraphNodeData } from './help' import { Position, type NodeProps } from 'reactflow' -import { type Column } from '@/api/client' import ModelNodeHeaderHandles from './ModelNodeHeaderHandles' import ModelColumns from './ModelColumns' +import { fromAPIColumn, type Column } from '@/domain/column' export const EnumLineageNodeModelType = { ...ModelType, @@ -53,7 +53,7 @@ export default function ModelNode({ const modelsArray = Object.values(models) const decodedId = decodeURIComponent(id) const model = modelsArray.find((m: Model) => m.fqn === decodedId) - const modelColumns = model?.columns ?? [] + const modelColumns = model?.columns?.map(fromAPIColumn) ?? [] Object.keys(lineage[decodedId]?.columns ?? {}).forEach((column: string) => { const found = modelColumns.find(({ name }: any) => { @@ -65,7 +65,9 @@ export default function ModelNode({ }) if (isNil(found)) { - modelColumns.push({ name: column, type: EnumColumnType.UNKNOWN }) + modelColumns.push( + fromAPIColumn({ name: column, type: EnumColumnType.UNKNOWN }), + ) } }) diff --git a/vscode/react/src/components/graph/context.tsx b/vscode/react/src/components/graph/context.tsx index d889e5e7fe..98cf438c94 100644 --- a/vscode/react/src/components/graph/context.tsx +++ b/vscode/react/src/components/graph/context.tsx @@ -1,4 +1,4 @@ -import { type Column, type Model } from '@/api/client' +import { type Model } from '@/api/client' import { createContext, useState, @@ -11,6 +11,7 @@ import { EnumSide } from './types' import { type Node } from 'reactflow' import type { Lineage } from '@/domain/lineage' import type { ModelSQLMeshModel } from '@/domain/sqlmesh-model' +import type { Column } from '@/domain/column' export interface Connections { left: string[] diff --git a/vscode/react/src/domain/column.ts b/vscode/react/src/domain/column.ts new file mode 100644 index 0000000000..bd3f7dd9ed --- /dev/null +++ b/vscode/react/src/domain/column.ts @@ -0,0 +1,18 @@ +import { type Column as APIColumn } from '@/api/client' +import { type Branded } from '@bus/brand' + +export type ColumnName = Branded + +export type Column = { + name: ColumnName + type: string + description?: string +} + +export function fromAPIColumn(column: APIColumn): Column { + return { + name: column.name as ColumnName, + type: column.type, + description: column.description ?? undefined, + } +} From 13024bcb7bb3c930e3a84d029d8b629e764b63ae Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:21:21 -0500 Subject: [PATCH 0507/1056] Fix: make MSSQL merge exists implementation opt-in (#4870) --- docs/integrations/engines/mssql.md | 75 +++++++++++++++++-------- sqlmesh/core/engine_adapter/base.py | 1 + sqlmesh/core/engine_adapter/mixins.py | 1 + sqlmesh/core/engine_adapter/mssql.py | 14 +++-- sqlmesh/core/engine_adapter/postgres.py | 1 + sqlmesh/core/engine_adapter/redshift.py | 1 + sqlmesh/core/snapshot/evaluator.py | 2 + tests/core/engine_adapter/test_mssql.py | 68 ++++++++++++++++++++-- tests/core/test_snapshot_evaluator.py | 3 + 9 files changed, 134 insertions(+), 32 deletions(-) diff --git a/docs/integrations/engines/mssql.md b/docs/integrations/engines/mssql.md index f06b5f1387..4c68219dd2 100644 --- a/docs/integrations/engines/mssql.md +++ b/docs/integrations/engines/mssql.md @@ -1,34 +1,65 @@ # MSSQL -## Local/Built-in Scheduler -**Engine Adapter Type**: `mssql` +## Installation -### Installation -#### User / Password Authentication: +### User / Password Authentication: ``` pip install "sqlmesh[mssql]" ``` -#### Microsoft Entra ID / Azure Active Directory Authentication: +### Microsoft Entra ID / Azure Active Directory Authentication: ``` pip install "sqlmesh[mssql-odbc]" ``` +## Incremental by unique key `MERGE` + +SQLMesh executes a `MERGE` statement to insert rows for [incremental by unique key](../../concepts/models/model_kinds.md#incremental_by_unique_key) model kinds. + +By default, the `MERGE` statement updates all non-key columns of an existing row when a new row with the same key values is inserted. If all column values match between the two rows, those updates are unnecessary. + +SQLMesh provides an optional performance optimization that skips unnecessary updates by comparing column values with the `EXISTS` and `EXCEPT` operators. + +Enable the optimization by setting the `mssql_merge_exists` key to `true` in the [`physical_properties`](../../concepts/models/overview.md#physical_properties) section of the `MODEL` statement. + +For example: + +```sql linenums="1" hl_lines="7-9" +MODEL ( + name sqlmesh_example.unique_key, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key id + ), + cron '@daily', + physical_properties ( + mssql_merge_exists = true + ) +); +``` + +!!! warning "Not all column types supported" + The `mssql_merge_exists` optimization is not supported for all column types, including `GEOMETRY`, `XML`, `TEXT`, `NTEXT`, `IMAGE`, and most user-defined types. + + Learn more in the [MSSQL `EXCEPT` statement documentation](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/set-operators-except-and-intersect-transact-sql?view=sql-server-ver17#arguments). + +## Local/Built-in Scheduler +**Engine Adapter Type**: `mssql` + ### Connection options -| Option | Description | Type | Required | -| ----------------- | ------------------------------------------------------------ | :----------: | :------: | -| `type` | Engine type name - must be `mssql` | string | Y | -| `host` | The hostname of the MSSQL server | string | Y | -| `user` | The username / client id to use for authentication with the MSSQL server | string | N | -| `password` | The password / client secret to use for authentication with the MSSQL server | string | N | -| `port` | The port number of the MSSQL server | int | N | -| `database` | The target database | string | N | -| `charset` | The character set used for the connection | string | N | -| `timeout` | The query timeout in seconds. Default: no timeout | int | N | -| `login_timeout` | The timeout for connection and login in seconds. Default: 60 | int | N | -| `appname` | The application name to use for the connection | string | N | -| `conn_properties` | The list of connection properties | list[string] | N | -| `autocommit` | Is autocommit mode enabled. Default: false | bool | N | -| `driver` | The driver to use for the connection. Default: pymssql | string | N | -| `driver_name` | The driver name to use for the connection. E.g., *ODBC Driver 18 for SQL Server* | string | N | -| `odbc_properties` | The dict of ODBC connection properties. E.g., authentication: ActiveDirectoryServicePrincipal. See more [here](https://learn.microsoft.com/en-us/sql/connect/odbc/dsn-connection-string-attribute?view=sql-server-ver16). | dict | N | \ No newline at end of file +| Option | Description | Type | Required | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------: | :------: | +| `type` | Engine type name - must be `mssql` | string | Y | +| `host` | The hostname of the MSSQL server | string | Y | +| `user` | The username / client id to use for authentication with the MSSQL server | string | N | +| `password` | The password / client secret to use for authentication with the MSSQL server | string | N | +| `port` | The port number of the MSSQL server | int | N | +| `database` | The target database | string | N | +| `charset` | The character set used for the connection | string | N | +| `timeout` | The query timeout in seconds. Default: no timeout | int | N | +| `login_timeout` | The timeout for connection and login in seconds. Default: 60 | int | N | +| `appname` | The application name to use for the connection | string | N | +| `conn_properties` | The list of connection properties | list[string] | N | +| `autocommit` | Is autocommit mode enabled. Default: false | bool | N | +| `driver` | The driver to use for the connection. Default: pymssql | string | N | +| `driver_name` | The driver name to use for the connection (e.g., *ODBC Driver 18 for SQL Server*). | string | N | +| `odbc_properties` | ODBC connection properties (e.g., *authentication: ActiveDirectoryServicePrincipal*). See more [here](https://learn.microsoft.com/en-us/sql/connect/odbc/dsn-connection-string-attribute?view=sql-server-ver16). | dict | N | \ No newline at end of file diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 591d81c9ae..8740177837 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -1939,6 +1939,7 @@ def merge( unique_key: t.Sequence[exp.Expression], when_matched: t.Optional[exp.Whens] = None, merge_filter: t.Optional[exp.Expression] = None, + **kwargs: t.Any, ) -> None: source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( source_table, columns_to_types, target_table=target_table diff --git a/sqlmesh/core/engine_adapter/mixins.py b/sqlmesh/core/engine_adapter/mixins.py index 4b0e86a772..5ca1f200d9 100644 --- a/sqlmesh/core/engine_adapter/mixins.py +++ b/sqlmesh/core/engine_adapter/mixins.py @@ -32,6 +32,7 @@ def merge( unique_key: t.Sequence[exp.Expression], when_matched: t.Optional[exp.Whens] = None, merge_filter: t.Optional[exp.Expression] = None, + **kwargs: t.Any, ) -> None: logical_merge( self, diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index 60fe99ffc4..112193073d 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -198,7 +198,10 @@ def merge( unique_key: t.Sequence[exp.Expression], when_matched: t.Optional[exp.Whens] = None, merge_filter: t.Optional[exp.Expression] = None, + **kwargs: t.Any, ) -> None: + mssql_merge_exists = kwargs.get("physical_properties", {}).get("mssql_merge_exists") + source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( source_table, columns_to_types, target_table=target_table ) @@ -214,7 +217,6 @@ def merge( match_expressions = [] if not when_matched: - match_condition = None unique_key_names = [y.name for y in unique_key] columns_to_types_no_keys = [c for c in columns_to_types if c not in unique_key_names] @@ -225,10 +227,14 @@ def merge( exp.column(c, MERGE_SOURCE_ALIAS) for c in columns_to_types_no_keys ] - match_condition = exp.Exists( - this=exp.select(*target_columns_no_keys).except_( - exp.select(*source_columns_no_keys) + match_condition = ( + exp.Exists( + this=exp.select(*target_columns_no_keys).except_( + exp.select(*source_columns_no_keys) + ) ) + if mssql_merge_exists + else None ) if target_columns_no_keys: diff --git a/sqlmesh/core/engine_adapter/postgres.py b/sqlmesh/core/engine_adapter/postgres.py index bd7faef289..9962c037ac 100644 --- a/sqlmesh/core/engine_adapter/postgres.py +++ b/sqlmesh/core/engine_adapter/postgres.py @@ -109,6 +109,7 @@ def merge( unique_key: t.Sequence[exp.Expression], when_matched: t.Optional[exp.Whens] = None, merge_filter: t.Optional[exp.Expression] = None, + **kwargs: t.Any, ) -> None: # Merge isn't supported until Postgres 15 merge_impl = ( diff --git a/sqlmesh/core/engine_adapter/redshift.py b/sqlmesh/core/engine_adapter/redshift.py index 946d4ee318..906c52445f 100644 --- a/sqlmesh/core/engine_adapter/redshift.py +++ b/sqlmesh/core/engine_adapter/redshift.py @@ -353,6 +353,7 @@ def merge( unique_key: t.Sequence[exp.Expression], when_matched: t.Optional[exp.Whens] = None, merge_filter: t.Optional[exp.Expression] = None, + **kwargs: t.Any, ) -> None: if self.enable_merge: # By default we use the logical merge unless the user has opted in diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index baac96f64a..eff458dc5d 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -1614,6 +1614,7 @@ def insert( end=kwargs.get("end"), execution_time=kwargs.get("execution_time"), ), + physical_properties=kwargs.get("physical_properties", model.physical_properties), ) def append( @@ -1634,6 +1635,7 @@ def append( end=kwargs.get("end"), execution_time=kwargs.get("execution_time"), ), + physical_properties=kwargs.get("physical_properties", model.physical_properties), ) diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index eef0a320da..65f3231163 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -474,7 +474,7 @@ def test_merge_pandas( assert to_sql_calls(adapter) == [ f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '__temp_target_{temp_table_id}') EXEC('CREATE TABLE [__temp_target_{temp_table_id}] ([id] INTEGER, [ts] DATETIME2, [val] INTEGER)');""", - f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] WHEN MATCHED AND EXISTS(SELECT [__MERGE_TARGET__].[ts], [__MERGE_TARGET__].[val] EXCEPT SELECT [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]) THEN UPDATE SET [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts], [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", + f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] WHEN MATCHED THEN UPDATE SET [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts], [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", f"DROP TABLE IF EXISTS [__temp_target_{temp_table_id}];", ] @@ -498,11 +498,47 @@ def test_merge_pandas( assert to_sql_calls(adapter) == [ f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '__temp_target_{temp_table_id}') EXEC('CREATE TABLE [__temp_target_{temp_table_id}] ([id] INTEGER, [ts] DATETIME2, [val] INTEGER)');""", - f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] AND [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts] WHEN MATCHED AND EXISTS(SELECT [__MERGE_TARGET__].[val] EXCEPT SELECT [__MERGE_SOURCE__].[val]) THEN UPDATE SET [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", + f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] AND [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts] WHEN MATCHED THEN UPDATE SET [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", + f"DROP TABLE IF EXISTS [__temp_target_{temp_table_id}];", + ] + + +def test_merge_exists( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture, make_temp_table_name: t.Callable +): + mocker.patch( + "sqlmesh.core.engine_adapter.mssql.MSSQLEngineAdapter.table_exists", + return_value=False, + ) + + adapter = make_mocked_engine_adapter(MSSQLEngineAdapter) + + temp_table_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter._get_temp_table") + table_name = "target" + temp_table_id = "abcdefgh" + temp_table_mock.return_value = make_temp_table_name(table_name, temp_table_id) + + df = pd.DataFrame({"id": [1, 2, 3], "ts": [1, 2, 3], "val": [4, 5, 6]}) + + # regular implementation + adapter.merge( + target_table=table_name, + source_table=df, + columns_to_types={ + "id": exp.DataType.build("int"), + "ts": exp.DataType.build("TIMESTAMP"), + "val": exp.DataType.build("int"), + }, + unique_key=[exp.to_identifier("id")], + ) + + assert to_sql_calls(adapter) == [ + f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '__temp_target_{temp_table_id}') EXEC('CREATE TABLE [__temp_target_{temp_table_id}] ([id] INTEGER, [ts] DATETIME2, [val] INTEGER)');""", + f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] WHEN MATCHED THEN UPDATE SET [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts], [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", f"DROP TABLE IF EXISTS [__temp_target_{temp_table_id}];", ] - # all model columns are keys + # merge exists implementation adapter.cursor.reset_mock() adapter._connection_pool.get().reset_mock() temp_table_mock.return_value = make_temp_table_name(table_name, temp_table_id) @@ -512,11 +548,31 @@ def test_merge_pandas( columns_to_types={ "id": exp.DataType.build("int"), "ts": exp.DataType.build("TIMESTAMP"), + "val": exp.DataType.build("int"), }, - unique_key=[exp.to_identifier("id"), exp.to_column("ts")], + unique_key=[exp.to_identifier("id")], + physical_properties={"mssql_merge_exists": True}, ) - adapter._connection_pool.get().bulk_copy.assert_called_with( - f"__temp_target_{temp_table_id}", [(1, 1), (2, 2), (3, 3)] + + assert to_sql_calls(adapter) == [ + f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '__temp_target_{temp_table_id}') EXEC('CREATE TABLE [__temp_target_{temp_table_id}] ([id] INTEGER, [ts] DATETIME2, [val] INTEGER)');""", + f"MERGE INTO [target] AS [__MERGE_TARGET__] USING (SELECT CAST([id] AS INTEGER) AS [id], CAST([ts] AS DATETIME2) AS [ts], CAST([val] AS INTEGER) AS [val] FROM [__temp_target_{temp_table_id}]) AS [__MERGE_SOURCE__] ON [__MERGE_TARGET__].[id] = [__MERGE_SOURCE__].[id] WHEN MATCHED AND EXISTS(SELECT [__MERGE_TARGET__].[ts], [__MERGE_TARGET__].[val] EXCEPT SELECT [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]) THEN UPDATE SET [__MERGE_TARGET__].[ts] = [__MERGE_SOURCE__].[ts], [__MERGE_TARGET__].[val] = [__MERGE_SOURCE__].[val] WHEN NOT MATCHED THEN INSERT ([id], [ts], [val]) VALUES ([__MERGE_SOURCE__].[id], [__MERGE_SOURCE__].[ts], [__MERGE_SOURCE__].[val]);", + f"DROP TABLE IF EXISTS [__temp_target_{temp_table_id}];", + ] + + # merge exists and all model columns are keys + adapter.cursor.reset_mock() + adapter._connection_pool.get().reset_mock() + temp_table_mock.return_value = make_temp_table_name(table_name, temp_table_id) + adapter.merge( + target_table=table_name, + source_table=df, + columns_to_types={ + "id": exp.DataType.build("int"), + "ts": exp.DataType.build("TIMESTAMP"), + }, + unique_key=[exp.to_identifier("id"), exp.to_column("ts")], + physical_properties={"mssql_merge_exists": True}, ) assert to_sql_calls(adapter) == [ diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index e4de741cdb..dace2d93ac 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -2231,6 +2231,7 @@ def test_create_incremental_by_unique_key_updated_at_exp(adapter_mock, make_snap ) ] ), + physical_properties={}, ) @@ -2327,6 +2328,7 @@ def test_create_incremental_by_unique_key_multiple_updated_at_exp(adapter_mock, ), ], ), + physical_properties={}, ) @@ -2478,6 +2480,7 @@ def test_create_incremental_by_unique_key_merge_filter(adapter_mock, make_snapsh expression=exp.Literal(this="2020-01-01", is_string=True), ), ), + physical_properties={}, ) From 0edfea72b88474b705ef622ba93dacbbadbbae68 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Wed, 2 Jul 2025 19:28:01 +0300 Subject: [PATCH 0508/1056] Chore: Move `SnapshotEvaluator` back at plan evaluator constructor (#4877) --- sqlmesh/core/config/scheduler.py | 1 + sqlmesh/core/context.py | 55 +++++++++++-------------------- sqlmesh/core/plan/evaluator.py | 6 ++-- sqlmesh/core/plan/explainer.py | 2 -- tests/conftest.py | 6 ++-- tests/core/test_integration.py | 29 +--------------- tests/core/test_plan_evaluator.py | 5 +-- 7 files changed, 26 insertions(+), 78 deletions(-) diff --git a/sqlmesh/core/config/scheduler.py b/sqlmesh/core/config/scheduler.py index fc44d8f356..5cbfc6a71c 100644 --- a/sqlmesh/core/config/scheduler.py +++ b/sqlmesh/core/config/scheduler.py @@ -130,6 +130,7 @@ class BuiltInSchedulerConfig(_EngineAdapterStateSyncSchedulerConfig, BaseConfig) def create_plan_evaluator(self, context: GenericContext) -> PlanEvaluator: return BuiltInPlanEvaluator( state_sync=context.state_sync, + snapshot_evaluator=context.snapshot_evaluator, create_scheduler=context.create_scheduler, default_catalog=context.default_catalog, console=context.console, diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 1be6ca1dac..f7f068d6f9 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -116,7 +116,7 @@ run_tests, ) from sqlmesh.core.user import User -from sqlmesh.utils import UniqueKeyDict, Verbosity, CorrelationId +from sqlmesh.utils import UniqueKeyDict, Verbosity from sqlmesh.utils.concurrency import concurrent_apply_to_values from sqlmesh.utils.dag import DAG from sqlmesh.utils.date import ( @@ -417,7 +417,7 @@ def __init__( self.config.get_state_connection(self.gateway) or self.connection_config ) - self._snapshot_evaluators: t.Dict[t.Optional[CorrelationId], SnapshotEvaluator] = {} + self._snapshot_evaluator: t.Optional[SnapshotEvaluator] = None self.console = get_console() setattr(self.console, "dialect", self.config.dialect) @@ -445,22 +445,18 @@ def engine_adapter(self) -> EngineAdapter: self._engine_adapter = self.connection_config.create_engine_adapter() return self._engine_adapter - def snapshot_evaluator( - self, correlation_id: t.Optional[CorrelationId] = None - ) -> SnapshotEvaluator: - # Cache snapshot evaluators by correlation_id to avoid old correlation_ids being attached to future Context operations - if correlation_id not in self._snapshot_evaluators: - self._snapshot_evaluators[correlation_id] = SnapshotEvaluator( + @property + def snapshot_evaluator(self) -> SnapshotEvaluator: + if not self._snapshot_evaluator: + self._snapshot_evaluator = SnapshotEvaluator( { - gateway: adapter.with_settings( - log_level=logging.INFO, correlation_id=correlation_id - ) + gateway: adapter.with_settings(log_level=logging.INFO) for gateway, adapter in self.engine_adapters.items() }, ddl_concurrent_tasks=self.concurrent_tasks, selected_gateway=self.selected_gateway, ) - return self._snapshot_evaluators[correlation_id] + return self._snapshot_evaluator def execution_context( self, @@ -541,9 +537,7 @@ def scheduler(self, environment: t.Optional[str] = None) -> Scheduler: return self.create_scheduler(snapshots) - def create_scheduler( - self, snapshots: t.Iterable[Snapshot], correlation_id: t.Optional[CorrelationId] = None - ) -> Scheduler: + def create_scheduler(self, snapshots: t.Iterable[Snapshot]) -> Scheduler: """Creates the built-in scheduler. Args: @@ -554,7 +548,7 @@ def create_scheduler( """ return Scheduler( snapshots, - self.snapshot_evaluator(correlation_id), + self.snapshot_evaluator, self.state_sync, default_catalog=self.default_catalog, max_workers=self.concurrent_tasks, @@ -719,7 +713,7 @@ def run( NotificationEvent.RUN_START, environment=environment ) analytics_run_id = analytics.collector.on_run_start( - engine_type=self.snapshot_evaluator().adapter.dialect, + engine_type=self.snapshot_evaluator.adapter.dialect, state_sync_type=self.state_sync.state_type(), ) self._load_materializations() @@ -1081,7 +1075,7 @@ def evaluate( and not parent_snapshot.categorized ] - df = self.snapshot_evaluator().evaluate_and_fetch( + df = self.snapshot_evaluator.evaluate_and_fetch( snapshot, start=start, end=end, @@ -1593,12 +1587,7 @@ def apply( default_catalog=self.default_catalog, console=self.console, ) - explainer.evaluate( - plan.to_evaluatable(), - snapshot_evaluator=self.snapshot_evaluator( - correlation_id=CorrelationId.from_plan_id(plan.plan_id) - ), - ) + explainer.evaluate(plan.to_evaluatable()) return self.notification_target_manager.notify( @@ -2121,7 +2110,7 @@ def audit( errors = [] skipped_count = 0 for snapshot in snapshots: - for audit_result in self.snapshot_evaluator().audit( + for audit_result in self.snapshot_evaluator.audit( snapshot=snapshot, start=start, end=end, @@ -2153,7 +2142,7 @@ def audit( self.console.log_status_update(f"Got {error.count} results, expected 0.") if error.query: self.console.show_sql( - f"{error.query.sql(dialect=self.snapshot_evaluator().adapter.dialect)}" + f"{error.query.sql(dialect=self.snapshot_evaluator.adapter.dialect)}" ) self.console.log_status_update("Done.") @@ -2345,14 +2334,12 @@ def print_environment_names(self) -> None: def close(self) -> None: """Releases all resources allocated by this context.""" - for evaluator in self._snapshot_evaluators.values(): - evaluator.close() + if self._snapshot_evaluator: + self._snapshot_evaluator.close() if self._state_sync: self._state_sync.close() - self._snapshot_evaluators.clear() - def _run( self, environment: str, @@ -2403,11 +2390,7 @@ def _run( def _apply(self, plan: Plan, circuit_breaker: t.Optional[t.Callable[[], bool]]) -> None: self._scheduler.create_plan_evaluator(self).evaluate( - plan.to_evaluatable(), - snapshot_evaluator=self.snapshot_evaluator( - correlation_id=CorrelationId.from_plan_id(plan.plan_id) - ), - circuit_breaker=circuit_breaker, + plan.to_evaluatable(), circuit_breaker=circuit_breaker ) @python_api_analytics @@ -2700,7 +2683,7 @@ def _run_janitor(self, ignore_ttl: bool = False) -> None: ) # Remove the expired snapshots tables - self.snapshot_evaluator().cleanup( + self.snapshot_evaluator.cleanup( target_snapshots=cleanup_targets, on_complete=self.console.update_cleanup_progress, ) diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 562f2ed60e..a8e2aa7919 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -51,7 +51,6 @@ class PlanEvaluator(abc.ABC): def evaluate( self, plan: EvaluatablePlan, - snapshot_evaluator: SnapshotEvaluator, circuit_breaker: t.Optional[t.Callable[[], bool]] = None, ) -> None: """Evaluates a plan by pushing snapshots and backfilling data. @@ -63,7 +62,6 @@ def evaluate( Args: plan: The plan to evaluate. - snapshot_evaluator: The snapshot evaluator to use. circuit_breaker: The circuit breaker to use. """ @@ -72,11 +70,13 @@ class BuiltInPlanEvaluator(PlanEvaluator): def __init__( self, state_sync: StateSync, + snapshot_evaluator: SnapshotEvaluator, create_scheduler: t.Callable[[t.Iterable[Snapshot]], Scheduler], default_catalog: t.Optional[str], console: t.Optional[Console] = None, ): self.state_sync = state_sync + self.snapshot_evaluator = snapshot_evaluator self.create_scheduler = create_scheduler self.default_catalog = default_catalog self.console = console or get_console() @@ -85,11 +85,9 @@ def __init__( def evaluate( self, plan: EvaluatablePlan, - snapshot_evaluator: SnapshotEvaluator, circuit_breaker: t.Optional[t.Callable[[], bool]] = None, ) -> None: self._circuit_breaker = circuit_breaker - self.snapshot_evaluator = snapshot_evaluator self.console.start_plan_evaluation(plan) analytics.collector.on_plan_apply_start( diff --git a/sqlmesh/core/plan/explainer.py b/sqlmesh/core/plan/explainer.py index d3c6480f74..ee829aeac1 100644 --- a/sqlmesh/core/plan/explainer.py +++ b/sqlmesh/core/plan/explainer.py @@ -20,7 +20,6 @@ from sqlmesh.utils import Verbosity, rich as srich, to_snake_case from sqlmesh.utils.date import to_ts from sqlmesh.utils.errors import SQLMeshError -from sqlmesh.core.snapshot.evaluator import SnapshotEvaluator logger = logging.getLogger(__name__) @@ -40,7 +39,6 @@ def __init__( def evaluate( self, plan: EvaluatablePlan, - snapshot_evaluator: SnapshotEvaluator, circuit_breaker: t.Optional[t.Callable[[], bool]] = None, ) -> None: plan_stages = stages.build_plan_stages(plan, self.state_reader, self.default_catalog) diff --git a/tests/conftest.py b/tests/conftest.py index a874bd7590..574c802c0e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,7 +42,7 @@ SnapshotDataVersion, SnapshotFingerprint, ) -from sqlmesh.utils import random_id, CorrelationId +from sqlmesh.utils import random_id from sqlmesh.utils.date import TimeLike, to_date from sqlmesh.utils.windows import IS_WINDOWS, fix_windows_path from sqlmesh.core.engine_adapter.shared import CatalogSupport @@ -266,12 +266,10 @@ def duck_conn() -> duckdb.DuckDBPyConnection: def push_plan(context: Context, plan: Plan) -> None: plan_evaluator = BuiltInPlanEvaluator( context.state_sync, + context.snapshot_evaluator, context.create_scheduler, context.default_catalog, ) - plan_evaluator.snapshot_evaluator = context.snapshot_evaluator( - CorrelationId.from_plan_id(plan.plan_id) - ) deployability_index = DeployabilityIndex.create(context.snapshots.values()) evaluatable_plan = plan.to_evaluatable() stages = plan_stages.build_plan_stages( diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index f68cb7ac47..766a788ac8 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -67,7 +67,6 @@ SnapshotInfoLike, SnapshotTableInfo, ) -from sqlmesh.utils import CorrelationId from sqlmesh.utils.date import TimeLike, now, to_date, to_datetime, to_timestamp from sqlmesh.utils.errors import NoChangesPlanError, SQLMeshError, PlanError, ConfigError from sqlmesh.utils.pydantic import validate_string @@ -1138,7 +1137,7 @@ def test_non_breaking_change_after_forward_only_in_dev( init_and_plan_context: t.Callable, has_view_binding: bool ): context, plan = init_and_plan_context("examples/sushi") - context.snapshot_evaluator().adapter.HAS_VIEW_BINDING = has_view_binding + context.snapshot_evaluator.adapter.HAS_VIEW_BINDING = has_view_binding context.apply(plan) model = context.get_model("sushi.waiter_revenue_by_day") @@ -6794,29 +6793,3 @@ def test_scd_type_2_full_restatement_no_start_date(init_and_plan_context: t.Call # valid_from should be the epoch, valid_to should be NaT assert str(row["valid_from"]) == "1970-01-01 00:00:00" assert pd.isna(row["valid_to"]) - - -def test_plan_evaluator_correlation_id(tmp_path: Path): - def _correlation_id_in_sqls(correlation_id: CorrelationId, mock_logger): - sqls = [call[0][0] for call in mock_logger.call_args_list] - return any(f"/* {correlation_id} */" in sql for sql in sqls) - - create_temp_file( - tmp_path, Path("models") / "test.sql", "MODEL (name test.a, kind FULL); SELECT 1 AS col" - ) - - # Case 1: Ensure that the correlation id (plan_id) is included in the SQL - with mock.patch("sqlmesh.core.engine_adapter.base.EngineAdapter._log_sql") as mock_logger: - ctx = Context(paths=[tmp_path], config=Config()) - plan = ctx.plan(auto_apply=True, no_prompts=True) - - correlation_id = CorrelationId.from_plan_id(plan.plan_id) - assert str(correlation_id) == f"SQLMESH_PLAN: {plan.plan_id}" - - assert _correlation_id_in_sqls(correlation_id, mock_logger) - - # Case 2: Ensure that the previous correlation id is not included in the SQL for other operations - with mock.patch("sqlmesh.core.engine_adapter.base.EngineAdapter._log_sql") as mock_logger: - ctx.snapshot_evaluator().adapter.execute("SELECT 1") - - assert not _correlation_id_in_sqls(correlation_id, mock_logger) diff --git a/tests/core/test_plan_evaluator.py b/tests/core/test_plan_evaluator.py index 467c3e60bd..a3735b08ed 100644 --- a/tests/core/test_plan_evaluator.py +++ b/tests/core/test_plan_evaluator.py @@ -11,7 +11,6 @@ stages as plan_stages, ) from sqlmesh.core.snapshot import SnapshotChangeCategory -from sqlmesh.utils import CorrelationId @pytest.fixture @@ -60,13 +59,11 @@ def test_builtin_evaluator_push(sushi_context: Context, make_snapshot): evaluator = BuiltInPlanEvaluator( sushi_context.state_sync, + sushi_context.snapshot_evaluator, sushi_context.create_scheduler, sushi_context.default_catalog, console=sushi_context.console, ) - evaluator.snapshot_evaluator = sushi_context.snapshot_evaluator( - CorrelationId.from_plan_id(plan.plan_id) - ) evaluatable_plan = plan.to_evaluatable() stages = plan_stages.build_plan_stages( From 6db990da222a62830d5d11855a8ba058c665f488 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 2 Jul 2025 13:27:01 -0700 Subject: [PATCH 0509/1056] Chore: Refactor the intervals check and move progress reporting into the scheduler (#4879) --- sqlmesh/core/model/definition.py | 21 ++++++- sqlmesh/core/scheduler.py | 90 +++++++++++++++++++++++++++-- sqlmesh/core/snapshot/definition.py | 54 ++--------------- tests/core/test_context.py | 2 +- tests/core/test_snapshot.py | 14 ++--- 5 files changed, 117 insertions(+), 64 deletions(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 6dce3e6843..0808722119 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -627,8 +627,10 @@ def _render(e: exp.Expression) -> str | int | float | bool: {k: _render(v) for k, v in signal.items()} for name, signal in self.signals if not name ] - def render_signal_calls(self) -> t.Dict[str, t.Dict[str, t.Optional[exp.Expression]]]: - return { + def render_signal_calls(self) -> EvaluatableSignals: + python_env = self.python_env + env = prepare_env(python_env) + signals_to_kwargs = { name: { k: seq_get(self._create_renderer(v).render() or [], 0) for k, v in kwargs.items() } @@ -636,6 +638,12 @@ def render_signal_calls(self) -> t.Dict[str, t.Dict[str, t.Optional[exp.Expressi if name } + return EvaluatableSignals( + signals_to_kwargs=signals_to_kwargs, + python_env=python_env, + prepared_python_env=env, + ) + def render_merge_filter( self, *, @@ -1857,6 +1865,15 @@ class AuditResult(PydanticModel): blocking: bool = True +class EvaluatableSignals(PydanticModel): + signals_to_kwargs: t.Dict[str, t.Dict[str, t.Optional[exp.Expression]]] + """A mapping of signal names to the kwargs passed to the signal.""" + python_env: t.Dict[str, Executable] + """The Python environment that should be used to evaluated the rendered signal calls.""" + prepared_python_env: t.Dict[str, t.Any] + """The prepared Python environment that should be used to evaluated the rendered signal calls.""" + + def _extract_blueprints(blueprints: t.Any, path: Path) -> t.List[t.Any]: if not blueprints: return [None] diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index 8bac0bf081..2c7a2a66ac 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging import typing as t +import time from sqlglot import exp from sqlmesh.core import constants as c from sqlmesh.core.console import Console, get_console @@ -24,6 +25,7 @@ snapshots_to_dag, Intervals, ) +from sqlmesh.core.snapshot.definition import check_ready_intervals from sqlmesh.core.snapshot.definition import ( Interval, expand_range, @@ -39,7 +41,16 @@ to_timestamp, validate_date_range, ) -from sqlmesh.utils.errors import AuditError, NodeAuditsErrors, CircuitBreakerError, SQLMeshError +from sqlmesh.utils.errors import ( + AuditError, + NodeAuditsErrors, + CircuitBreakerError, + SQLMeshError, + SignalEvalError, +) + +if t.TYPE_CHECKING: + from sqlmesh.core.context import ExecutionContext logger = logging.getLogger(__name__) SnapshotToIntervals = t.Dict[Snapshot, Intervals] @@ -304,12 +315,11 @@ def batch_intervals( default_catalog=self.default_catalog, ) - intervals = snapshot.check_ready_intervals( + intervals = self._check_ready_intervals( + snapshot, intervals, context, - console=self.console, - default_catalog=self.default_catalog, - environment_naming_info=environment_naming_info, + environment_naming_info, ) unready -= set(intervals) @@ -709,6 +719,76 @@ def _audit_snapshot( return audit_results + def _check_ready_intervals( + self, + snapshot: Snapshot, + intervals: Intervals, + context: ExecutionContext, + environment_naming_info: EnvironmentNamingInfo, + ) -> Intervals: + """Checks if the intervals are ready for evaluation for the given snapshot. + + This implementation also includes the signal progress tracking. + Note that this will handle gaps in the provided intervals. The returned intervals + may introduce new gaps. + + Args: + snapshot: The snapshot to check. + intervals: The intervals to check. + context: The context to use. + environment_naming_info: The environment naming info to use. + + Returns: + The intervals that are ready for evaluation. + """ + signals = snapshot.is_model and snapshot.model.render_signal_calls() + + if not signals: + return intervals + + self.console.start_signal_progress( + snapshot, + self.default_catalog, + environment_naming_info or EnvironmentNamingInfo(), + ) + + for signal_idx, (signal_name, kwargs) in enumerate(signals.signals_to_kwargs.items()): + # Capture intervals before signal check for display + intervals_to_check = merge_intervals(intervals) + + signal_start_ts = time.perf_counter() + + try: + intervals = check_ready_intervals( + signals.prepared_python_env[signal_name], + intervals, + context, + python_env=signals.python_env, + dialect=snapshot.model.dialect, + path=snapshot.model._path, + kwargs=kwargs, + ) + except SQLMeshError as e: + raise SignalEvalError( + f"{e} '{signal_name}' for '{snapshot.model.name}' at {snapshot.model._path}" + ) + + duration = time.perf_counter() - signal_start_ts + + self.console.update_signal_progress( + snapshot=snapshot, + signal_name=signal_name, + signal_idx=signal_idx, + total_signals=len(signals.signals_to_kwargs), + ready_intervals=merge_intervals(intervals), + check_intervals=intervals_to_check, + duration=duration, + ) + + self.console.stop_signal_progress() + + return intervals + def merged_missing_intervals( snapshots: t.Collection[Snapshot], diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index e84a1fce27..9b5fa893fc 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1,7 +1,6 @@ from __future__ import annotations import sys -import time import typing as t from collections import defaultdict from datetime import datetime, timedelta @@ -42,8 +41,6 @@ ) from sqlmesh.utils.errors import SQLMeshError, SignalEvalError from sqlmesh.utils.metaprogramming import ( - prepare_env, - print_exception, format_evaluated_code_exception, Executable, ) @@ -51,7 +48,6 @@ from sqlmesh.utils.pydantic import PydanticModel, field_validator if t.TYPE_CHECKING: - from sqlmesh.core.console import Console from sqlglot.dialects.dialect import DialectType from sqlmesh.core.environment import EnvironmentNamingInfo from sqlmesh.core.context import ExecutionContext @@ -971,9 +967,6 @@ def check_ready_intervals( self, intervals: Intervals, context: ExecutionContext, - console: t.Optional[Console] = None, - default_catalog: t.Optional[str] = None, - environment_naming_info: t.Optional[EnvironmentNamingInfo] = None, ) -> Intervals: """Returns a list of intervals that are considered ready by the provided signal. @@ -981,59 +974,24 @@ def check_ready_intervals( may introduce new gaps. """ signals = self.is_model and self.model.render_signal_calls() - if not signals: return intervals - python_env = self.model.python_env - env = prepare_env(python_env) - - if console: - console.start_signal_progress( - self, - default_catalog, - environment_naming_info or EnvironmentNamingInfo(), - ) - - for signal_idx, (signal_name, kwargs) in enumerate(signals.items()): - # Capture intervals before signal check for display - intervals_to_check = merge_intervals(intervals) - - signal_start_ts = time.perf_counter() - + for signal_name, kwargs in signals.signals_to_kwargs.items(): try: - intervals = _check_ready_intervals( - env[signal_name], + intervals = check_ready_intervals( + signals.prepared_python_env[signal_name], intervals, context, - python_env=python_env, + python_env=signals.python_env, dialect=self.model.dialect, path=self.model._path, kwargs=kwargs, ) except SQLMeshError as e: - print_exception(e, python_env) - raise SQLMeshError( + raise SignalEvalError( f"{e} '{signal_name}' for '{self.model.name}' at {self.model._path}" ) - - duration = time.perf_counter() - signal_start_ts - - if console: - console.update_signal_progress( - snapshot=self, - signal_name=signal_name, - signal_idx=signal_idx, - total_signals=len(signals), - ready_intervals=merge_intervals(intervals), - check_intervals=intervals_to_check, - duration=duration, - ) - - # Stop signal progress tracking - if console: - console.stop_signal_progress() - return intervals def categorize_as(self, category: SnapshotChangeCategory) -> None: @@ -2229,7 +2187,7 @@ def _contiguous_intervals(intervals: Intervals) -> t.List[Intervals]: return contiguous_intervals -def _check_ready_intervals( +def check_ready_intervals( check: t.Callable, intervals: Intervals, context: ExecutionContext, diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 8922472aa3..7024e9f73f 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -2118,7 +2118,7 @@ def test_check_intervals(sushi_context, mocker): ): sushi_context.check_intervals(environment="dev", no_signals=False, select_models=[]) - spy = mocker.spy(sqlmesh.core.snapshot.definition, "_check_ready_intervals") + spy = mocker.spy(sqlmesh.core.snapshot.definition, "check_ready_intervals") intervals = sushi_context.check_intervals(environment=None, no_signals=False, select_models=[]) min_intervals = 19 diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index f09083f500..f2f54822f5 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -59,7 +59,7 @@ apply_auto_restatements, display_name, get_next_model_interval_start, - _check_ready_intervals, + check_ready_intervals, _contiguous_intervals, ) from sqlmesh.utils import AttributeDict @@ -2540,7 +2540,7 @@ def test_contiguous_intervals(): def test_check_ready_intervals(mocker: MockerFixture): def assert_always_signal(intervals): assert ( - _check_ready_intervals(lambda _: True, intervals, mocker.Mock(), mocker.Mock()) + check_ready_intervals(lambda _: True, intervals, mocker.Mock(), mocker.Mock()) == intervals ) @@ -2550,9 +2550,7 @@ def assert_always_signal(intervals): assert_always_signal([(0, 1), (2, 3)]) def assert_never_signal(intervals): - assert ( - _check_ready_intervals(lambda _: False, intervals, mocker.Mock(), mocker.Mock()) == [] - ) + assert check_ready_intervals(lambda _: False, intervals, mocker.Mock(), mocker.Mock()) == [] assert_never_signal([]) assert_never_signal([(0, 1)]) @@ -2560,7 +2558,7 @@ def assert_never_signal(intervals): assert_never_signal([(0, 1), (2, 3)]) def assert_empty_signal(intervals): - assert _check_ready_intervals(lambda _: [], intervals, mocker.Mock(), mocker.Mock()) == [] + assert check_ready_intervals(lambda _: [], intervals, mocker.Mock(), mocker.Mock()) == [] assert_empty_signal([]) assert_empty_signal([(0, 1)]) @@ -2577,7 +2575,7 @@ def assert_check_intervals( ): mock = mocker.Mock() mock.side_effect = [to_intervals(r) for r in ready] - _check_ready_intervals(mock, intervals, mocker.Mock(), mocker.Mock()) == expected + check_ready_intervals(mock, intervals, mocker.Mock(), mocker.Mock()) == expected assert_check_intervals([], [], []) assert_check_intervals([(0, 1)], [[]], []) @@ -2618,7 +2616,7 @@ def assert_check_intervals( ) with pytest.raises(SignalEvalError): - _check_ready_intervals( + check_ready_intervals( lambda _: (_ for _ in ()).throw(MemoryError("Some exception")), [(0, 1), (1, 2)], mocker.Mock(), From 25da4df4366dce5c456ca32066421bf7c92ae64d Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 2 Jul 2025 13:35:59 -0700 Subject: [PATCH 0510/1056] Fix: A restatement plan should not override environment statements (#4880) --- sqlmesh/core/context_diff.py | 4 ++- tests/core/test_integration.py | 22 +++++++++++++++ tests/core/test_plan.py | 51 ++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/context_diff.py b/sqlmesh/core/context_diff.py index 354779a3e1..f97edec5da 100644 --- a/sqlmesh/core/context_diff.py +++ b/sqlmesh/core/context_diff.py @@ -84,7 +84,7 @@ class ContextDiff(PydanticModel): """Python dependencies.""" previous_environment_statements: t.List[EnvironmentStatements] = [] """Previous environment statements.""" - environment_statements: t.List[EnvironmentStatements] = [] + environment_statements: t.List[EnvironmentStatements] """Environment statements.""" diff_rendered: bool = False """Whether the diff should compare raw vs rendered models""" @@ -268,6 +268,7 @@ def create_no_diff(cls, environment: str, state_reader: StateReader) -> ContextD if not env: raise SQLMeshError(f"Environment '{environment}' must exist for this operation.") + environment_statements = state_reader.get_environment_statements(environment) snapshots = state_reader.get_snapshots(env.snapshots) return ContextDiff( @@ -288,6 +289,7 @@ def create_no_diff(cls, environment: str, state_reader: StateReader) -> ContextD previous_requirements=env.requirements, requirements=env.requirements, previous_environment_statements=[], + environment_statements=environment_statements, previous_gateway_managed_virtual_layer=env.gateway_managed, gateway_managed_virtual_layer=env.gateway_managed, ) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 766a788ac8..91221d73af 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -5652,6 +5652,28 @@ def test_restatement_of_full_model_with_start(init_and_plan_context: t.Callable) assert waiter_by_day_interval == (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")) +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_restatement_should_not_override_environment_statements(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + context.config.before_all = ["SELECT 'test_before_all';"] + context.load() + + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + + prod_env_statements = context.state_reader.get_environment_statements(c.PROD) + assert prod_env_statements[0].before_all == ["SELECT 'test_before_all';"] + + context.plan( + restate_models=["sushi.waiter_revenue_by_day"], + start="2023-01-07", + auto_apply=True, + no_prompts=True, + ) + + prod_env_statements = context.state_reader.get_environment_statements(c.PROD) + assert prod_env_statements[0].before_all == ["SELECT 'test_before_all';"] + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_restatement_shouldnt_backfill_beyond_prod_intervals(init_and_plan_context: t.Callable): context, _ = init_and_plan_context("examples/sushi") diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 045f5bbada..06dbd7d2d3 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -87,6 +87,7 @@ def test_forward_only_plan_sets_version(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan_builder = PlanBuilder(context_diff, forward_only=True) @@ -140,6 +141,7 @@ def test_forward_only_dev(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) yesterday_ds_mock = mocker.patch("sqlmesh.core.plan.builder.yesterday_ds") @@ -200,6 +202,7 @@ def test_forward_only_metadata_change_dev(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) yesterday_ds_mock = mocker.patch("sqlmesh.core.plan.builder.yesterday_ds") @@ -249,6 +252,7 @@ def test_forward_only_plan_added_models(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff, forward_only=True).build() @@ -295,6 +299,7 @@ def test_forward_only_plan_categorizes_change_model_kind_as_breaking( previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff, forward_only=True).build() @@ -343,6 +348,7 @@ def test_paused_forward_only_parent(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff, forward_only=False).build() @@ -372,6 +378,7 @@ def test_forward_only_plan_allow_destructive_models( previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) with pytest.raises( @@ -448,6 +455,7 @@ def test_forward_only_plan_allow_destructive_models( previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) with pytest.raises( @@ -501,6 +509,7 @@ def test_forward_only_model_on_destructive_change( previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) with pytest.raises( @@ -561,6 +570,7 @@ def test_forward_only_model_on_destructive_change( previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff_2).build() @@ -647,6 +657,7 @@ def test_forward_only_model_on_destructive_change( previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff_3).build() @@ -683,6 +694,7 @@ def test_forward_only_model_on_destructive_change_no_column_types( previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) logger = logging.getLogger("sqlmesh.core.plan.builder") @@ -721,6 +733,7 @@ def test_missing_intervals_lookback(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan = Plan( @@ -918,6 +931,7 @@ def test_restate_symbolic_model(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan = PlanBuilder(context_diff, restate_models=[snapshot_a.name]).build() @@ -952,6 +966,7 @@ def test_restate_seed_model(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan = PlanBuilder(context_diff, restate_models=[snapshot_a.name]).build() @@ -976,6 +991,7 @@ def test_restate_missing_model(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) with pytest.raises( @@ -1005,6 +1021,7 @@ def test_new_snapshots_with_restatements(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) with pytest.raises( @@ -1040,6 +1057,7 @@ def test_end_validation(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) dev_plan_builder = PlanBuilder(context_diff, end="2022-01-03", is_dev=True) @@ -1105,6 +1123,7 @@ def test_forward_only_revert_not_allowed(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) with pytest.raises( @@ -1164,6 +1183,7 @@ def test_forward_only_plan_seed_models(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff, forward_only=True).build() @@ -1200,6 +1220,7 @@ def test_start_inference(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) snapshot_b.add_interval("2022-01-01", now()) @@ -1239,6 +1260,7 @@ def test_auto_categorization(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff).build() @@ -1287,6 +1309,7 @@ def test_auto_categorization_missing_schema_downstream(make_snapshot, mocker: Mo previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff).build() @@ -1319,6 +1342,7 @@ def test_broken_references(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) # Make sure the downstream snapshot doesn't have any parents, @@ -1356,6 +1380,7 @@ def test_broken_references_external_model(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) # Make sure the downstream snapshot doesn't have any parents, @@ -1399,6 +1424,7 @@ def test_effective_from(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) with pytest.raises( @@ -1482,6 +1508,7 @@ def test_effective_from_non_evaluatble_model(make_snapshot, mocker: MockerFixtur previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan_builder = PlanBuilder( @@ -1518,6 +1545,7 @@ def test_new_environment_no_changes(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) with pytest.raises( @@ -1562,6 +1590,7 @@ def test_new_environment_with_changes(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) # Modified the existing model. @@ -1641,6 +1670,7 @@ def test_forward_only_models(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff, is_dev=True).build() @@ -1686,6 +1716,7 @@ def test_forward_only_models_model_kind_changed(make_snapshot, mocker: MockerFix previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff, is_dev=True).build() @@ -1765,6 +1796,7 @@ def test_indirectly_modified_forward_only_model(make_snapshot, mocker: MockerFix previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan = PlanBuilder(context_diff, is_dev=True).build() @@ -1821,6 +1853,7 @@ def test_added_model_with_forward_only_parent(make_snapshot, mocker: MockerFixtu previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff, is_dev=True).build() @@ -1861,6 +1894,7 @@ def test_added_forward_only_model(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) PlanBuilder(context_diff).build() @@ -1895,6 +1929,7 @@ def test_disable_restatement(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan = PlanBuilder(context_diff, restate_models=['"a"']).build() @@ -1961,6 +1996,7 @@ def test_revert_to_previous_value(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan_builder = PlanBuilder(context_diff) @@ -2175,6 +2211,7 @@ def test_add_restatements( previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan = PlanBuilder( @@ -2253,6 +2290,7 @@ def test_dev_plan_depends_past(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) dev_plan_start_aligned = PlanBuilder( @@ -2357,6 +2395,7 @@ def test_dev_plan_depends_past_non_deployable(make_snapshot, mocker: MockerFixtu previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) def new_builder(start, end): @@ -2423,6 +2462,7 @@ def test_models_selected_for_backfill(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan = PlanBuilder(context_diff).build() @@ -2475,6 +2515,7 @@ def test_categorized_uncategorized(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan_builder = PlanBuilder(context_diff, auto_categorization_enabled=False) @@ -2530,6 +2571,7 @@ def test_environment_previous_finalized_snapshots(make_snapshot, mocker: MockerF previous_finalized_snapshots=[snapshot_c.table_info, snapshot_d.table_info], previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan = PlanBuilder(context_diff).build() @@ -2585,6 +2627,7 @@ def test_metadata_change(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan = PlanBuilder(context_diff, is_dev=True).build() @@ -2627,6 +2670,7 @@ def test_plan_start_when_preview_enabled(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) default_start_for_preview = "2024-06-09" @@ -2676,6 +2720,7 @@ def test_interval_end_per_model(make_snapshot): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan_builder = PlanBuilder( @@ -2751,6 +2796,7 @@ def test_unaligned_start_model_with_forward_only_preview(make_snapshot): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan_builder = PlanBuilder( @@ -2802,6 +2848,7 @@ def test_restate_production_model_in_dev(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) mock_console = mocker.Mock() @@ -2904,6 +2951,7 @@ def test_restate_daily_to_monthly(make_snapshot, mocker: MockerFixture): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan = PlanBuilder( @@ -3039,6 +3087,7 @@ def test_set_choice_for_forward_only_model(make_snapshot): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan_builder = PlanBuilder(context_diff, is_dev=True) @@ -3085,6 +3134,7 @@ def test_user_provided_flags(sushi_context: Context): previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) plan_builder = PlanBuilder( context_diff, @@ -3181,6 +3231,7 @@ def test_plan_dates_relative_to_execution_time( previous_finalized_snapshots=None, previous_gateway_managed_virtual_layer=False, gateway_managed_virtual_layer=False, + environment_statements=[], ) input_execution_time, input_start, input_end = input From fcfbe8fe7429da3b62867b110da5a062cf9720c4 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 3 Jul 2025 00:55:48 +0300 Subject: [PATCH 0511/1056] Feat: Add support for configurable cache directory (#4869) --- docs/guides/configuration.md | 28 +++++++ docs/reference/configuration.md | 1 + sqlmesh/core/config/root.py | 2 + sqlmesh/core/config/scheduler.py | 2 +- sqlmesh/core/context.py | 27 +++++- sqlmesh/core/loader.py | 2 +- sqlmesh/core/model/schema.py | 5 +- sqlmesh/core/selector.py | 5 +- sqlmesh/core/state_sync/db/facade.py | 8 +- sqlmesh/core/state_sync/db/snapshot.py | 5 +- sqlmesh/dbt/loader.py | 5 +- sqlmesh/dbt/manifest.py | 11 ++- sqlmesh/dbt/project.py | 1 + tests/core/state_sync/test_export_import.py | 2 +- tests/core/state_sync/test_state_sync.py | 16 +++- tests/core/test_context.py | 92 ++++++++++++++++++++- web/server/watcher.py | 5 +- 17 files changed, 190 insertions(+), 27 deletions(-) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index b1b06c59f6..52ebdf7793 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -288,6 +288,34 @@ Conceptually, we can group the root level parameters into the following types. E The rest of this page provides additional detail for some of the configuration options and provides brief examples. Comprehensive lists of configuration options are at the [configuration reference page](../reference/configuration.md). +### Cache directory + +By default, the SQLMesh cache is stored in a `.cache` directory within your project folder. You can customize the cache location using the `cache_dir` configuration option: + +=== "YAML" + + ```yaml linenums="1" + # Relative path to project directory + cache_dir: my_custom_cache + + # Absolute path + cache_dir: /tmp/sqlmesh_cache + + ``` + +=== "Python" + + ```python linenums="1" + from sqlmesh.core.config import Config, ModelDefaultsConfig + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + cache_dir="/tmp/sqlmesh_cache", + ) + ``` + +The cache directory is automatically created if it doesn't exist. You can clear the cache using the `sqlmesh clean` command. + ### 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/reference/configuration.md b/docs/reference/configuration.md index e00c9dee1f..40d0eeb26b 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -20,6 +20,7 @@ Configuration options for SQLMesh project directories. | ------------------ | ------------------------------------------------------------------------------------------------------------------ | :----------: | :------: | | `ignore_patterns` | Files that match glob patterns specified in this list are ignored when scanning the project folder (Default: `[]`) | list[string] | N | | `project` | The project name of this config. Used for [multi-repo setups](../guides/multi_repo.md). | string | N | +| `cache_dir` | The directory to store the SQLMesh cache. Can be an absolute path or relative to the project directory. (Default: `.cache`) | string | N | ### Environments diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index a8b8a2a797..315728aceb 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -120,6 +120,7 @@ class Config(BaseConfig): disable_anonymized_analytics: Whether to disable the anonymized analytics collection. before_all: SQL statements or macros to be executed at the start of the `sqlmesh plan` and `sqlmesh run` commands. after_all: SQL statements or macros to be executed at the end of the `sqlmesh plan` and `sqlmesh run` commands. + cache_dir: The directory to store the SQLMesh cache. Defaults to .cache in the project folder. """ gateways: GatewayDict = {"": GatewayConfig()} @@ -165,6 +166,7 @@ class Config(BaseConfig): after_all: t.Optional[t.List[str]] = None linter: LinterConfig = LinterConfig() janitor: JanitorConfig = JanitorConfig() + cache_dir: t.Optional[str] = None _FIELD_UPDATE_STRATEGY: t.ClassVar[t.Dict[str, UpdateStrategy]] = { "gateways": UpdateStrategy.NESTED_UPDATE, diff --git a/sqlmesh/core/config/scheduler.py b/sqlmesh/core/config/scheduler.py index 5cbfc6a71c..69adcafe70 100644 --- a/sqlmesh/core/config/scheduler.py +++ b/sqlmesh/core/config/scheduler.py @@ -105,7 +105,7 @@ def create_state_sync(self, context: GenericContext) -> StateSync: schema = context.config.get_state_schema(context.gateway) return EngineAdapterStateSync( - engine_adapter, schema=schema, context_path=context.path, console=context.console + engine_adapter, schema=schema, cache_dir=context.cache_dir, console=context.console ) def state_sync_fingerprint(self, context: GenericContext) -> str: diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index f7f068d6f9..4203e35739 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -504,7 +504,11 @@ def upsert_model(self, model: t.Union[str, Model], **kwargs: t.Any) -> Model: } ) - update_model_schemas(self.dag, models=self._models, context_path=self.path) + update_model_schemas( + self.dag, + models=self._models, + cache_dir=self.cache_dir, + ) if model.dialect: self._all_dialects.add(model.dialect) @@ -640,7 +644,11 @@ def load(self, update_schemas: bool = True) -> GenericContext[C]: self._models.update({fqn: model.copy(update={"mapping_schema": {}})}) continue - update_model_schemas(self.dag, models=self._models, context_path=self.path) + update_model_schemas( + self.dag, + models=self._models, + cache_dir=self.cache_dir, + ) models = self.models.values() for model in models: @@ -2439,6 +2447,9 @@ def clear_caches(self) -> None: cache_path = path / c.CACHE if cache_path.exists(): rmtree(cache_path) + if self.cache_dir.exists(): + rmtree(self.cache_dir) + if isinstance(self.state_sync, CachingStateSync): self.state_sync.clear_cache() @@ -2538,6 +2549,17 @@ def _model_tables(self) -> t.Dict[str, str]: for fqn, snapshot in self.snapshots.items() } + @cached_property + def cache_dir(self) -> Path: + if self.config.cache_dir: + cache_path = Path(self.config.cache_dir) + if cache_path.is_absolute(): + return cache_path + return self.path / cache_path + + # Default to .cache directory in the project path + return self.path / c.CACHE + @cached_property def engine_adapters(self) -> t.Dict[str, EngineAdapter]: """Returns all the engine adapters for the gateways defined in the configuration.""" @@ -2735,6 +2757,7 @@ def _new_selector( dag=dag, default_catalog=self.default_catalog, dialect=self.default_dialect, + cache_dir=self.cache_dir, ) def _register_notification_targets(self) -> None: diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index 41bb8a0aef..90894fd23d 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -887,7 +887,7 @@ class _Cache(CacheBase): def __init__(self, loader: SqlMeshLoader, config_path: Path): self._loader = loader self.config_path = config_path - self._model_cache = ModelCache(self.config_path / c.CACHE) + self._model_cache = ModelCache(self._loader.context.cache_dir) def get_or_load_models( self, target_path: Path, loader: t.Callable[[], t.List[Model]] diff --git a/sqlmesh/core/model/schema.py b/sqlmesh/core/model/schema.py index 9d3d38da6e..e29cacade0 100644 --- a/sqlmesh/core/model/schema.py +++ b/sqlmesh/core/model/schema.py @@ -7,7 +7,6 @@ from sqlglot.errors import SchemaError from sqlglot.schema import MappingSchema -from sqlmesh.core import constants as c from sqlmesh.core.model.cache import ( load_optimized_query_and_mapping, optimized_query_cache_pool, @@ -23,10 +22,10 @@ def update_model_schemas( dag: DAG[str], models: UniqueKeyDict[str, Model], - context_path: Path, + cache_dir: Path, ) -> None: schema = MappingSchema(normalize=False) - optimized_query_cache: OptimizedQueryCache = OptimizedQueryCache(context_path / c.CACHE) + optimized_query_cache: OptimizedQueryCache = OptimizedQueryCache(cache_dir) _update_model_schemas(dag, models, schema, optimized_query_cache) diff --git a/sqlmesh/core/selector.py b/sqlmesh/core/selector.py index 5d068b5d6a..be460d8ce3 100644 --- a/sqlmesh/core/selector.py +++ b/sqlmesh/core/selector.py @@ -10,6 +10,7 @@ from sqlglot.dialects.dialect import Dialect, DialectType from sqlglot.helper import seq_get +from sqlmesh.core import constants as c from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core.environment import Environment from sqlmesh.core.model import update_model_schemas @@ -34,10 +35,12 @@ def __init__( dag: t.Optional[DAG[str]] = None, default_catalog: t.Optional[str] = None, dialect: t.Optional[str] = None, + cache_dir: t.Optional[Path] = None, ): self._state_reader = state_reader self._models = models self._context_path = context_path + self._cache_dir = cache_dir if cache_dir else context_path / c.CACHE self._default_catalog = default_catalog self._dialect = dialect self._git_client = GitClient(context_path) @@ -157,7 +160,7 @@ def get_model(fqn: str) -> t.Optional[Model]: models[model.fqn] = model if needs_update: - update_model_schemas(dag, models=models, context_path=self._context_path) + update_model_schemas(dag, models=models, cache_dir=self._cache_dir) return models diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index c0d44893c4..779add1cca 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -79,7 +79,7 @@ class EngineAdapterStateSync(StateSync): engine_adapter: The EngineAdapter to use to store and fetch snapshots. schema: The schema to store state metadata in. If None or empty string then no schema is defined console: The console to log information to. - context_path: The context path, used for caching snapshot models. + cache_dir: The cache path, used for caching snapshot models. """ def __init__( @@ -87,14 +87,12 @@ def __init__( engine_adapter: EngineAdapter, schema: t.Optional[str], console: t.Optional[Console] = None, - context_path: Path = Path(), + cache_dir: Path = Path(), ): self.plan_dags_table = exp.table_("_plan_dags", db=schema) self.interval_state = IntervalState(engine_adapter, schema=schema) self.environment_state = EnvironmentState(engine_adapter, schema=schema) - self.snapshot_state = SnapshotState( - engine_adapter, schema=schema, context_path=context_path - ) + self.snapshot_state = SnapshotState(engine_adapter, schema=schema, cache_dir=cache_dir) self.version_state = VersionState(engine_adapter, schema=schema) self.migrator = StateMigrator( engine_adapter, diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 0cf954071d..5b6d96d970 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -8,7 +8,6 @@ from sqlglot import exp from pydantic import Field -from sqlmesh.core import constants as c from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.state_sync.db.utils import ( snapshot_name_version_filter, @@ -53,7 +52,7 @@ def __init__( self, engine_adapter: EngineAdapter, schema: t.Optional[str] = None, - context_path: Path = Path(), + cache_dir: Path = Path(), ): self.engine_adapter = engine_adapter self.snapshots_table = exp.table_("_snapshots", db=schema) @@ -79,7 +78,7 @@ def __init__( "next_auto_restatement_ts": exp.DataType.build("bigint"), } - self._snapshot_cache = SnapshotCache(context_path / c.CACHE) + self._snapshot_cache = SnapshotCache(cache_dir) def push_snapshots(self, snapshots: t.Iterable[Snapshot], overwrite: bool = False) -> None: """Pushes snapshots to the state store. diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 672ad1ac3e..0f896d5bec 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -5,7 +5,6 @@ import typing as t import sqlmesh.core.dialect as d from pathlib import Path -from sqlmesh.core import constants as c from sqlmesh.core.config import ( Config, ConnectionConfig, @@ -330,8 +329,8 @@ def __init__( self._yaml_max_mtimes = yaml_max_mtimes target = t.cast(TargetConfig, project.context.target) - cache_path = loader.config_path / c.CACHE / target.name - self._model_cache = ModelCache(cache_path) + cache_dir = loader.context.cache_dir / target.name + self._model_cache = ModelCache(cache_dir) def get_or_load_models( self, target_path: Path, loader: t.Callable[[], t.List[Model]] diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 49827e68a7..19795a0b9b 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -77,6 +77,7 @@ def __init__( profile_name: str, target: TargetConfig, variable_overrides: t.Optional[t.Dict[str, t.Any]] = None, + cache_dir: t.Optional[str] = None, ): self.project_path = project_path self.profiles_path = profiles_path @@ -99,8 +100,16 @@ def __init__( self._tests_by_owner: t.Dict[str, t.List[TestConfig]] = defaultdict(list) self._disabled_refs: t.Optional[t.Set[str]] = None self._disabled_sources: t.Optional[t.Set[str]] = None + + if cache_dir is not None: + cache_path = Path(cache_dir) + if not cache_path.is_absolute(): + cache_path = self.project_path / cache_path + else: + cache_path = self.project_path / c.CACHE + self._call_cache: FileCache[t.Dict[str, t.List[CallNames]]] = FileCache( - self.project_path / c.CACHE, "jinja_calls" + cache_path, "jinja_calls" ) self._on_run_start_per_package: t.Dict[str, HookConfigs] = defaultdict(dict) diff --git a/sqlmesh/dbt/project.py b/sqlmesh/dbt/project.py index e491736086..ac36ee4e0a 100644 --- a/sqlmesh/dbt/project.py +++ b/sqlmesh/dbt/project.py @@ -75,6 +75,7 @@ def load(cls, context: DbtContext, variables: t.Optional[t.Dict[str, t.Any]] = N profile_name, target=profile.target, variable_overrides=variable_overrides, + cache_dir=context.sqlmesh_config.cache_dir, ) extra_fields = profile.target.extra diff --git a/tests/core/state_sync/test_export_import.py b/tests/core/state_sync/test_export_import.py index 0b22656d1e..2d20199d33 100644 --- a/tests/core/state_sync/test_export_import.py +++ b/tests/core/state_sync/test_export_import.py @@ -33,7 +33,7 @@ def state_sync(tmp_path: Path, example_project_config: Config) -> StateSync: return EngineAdapterStateSync( engine_adapter=example_project_config.get_state_connection("main").create_engine_adapter(), # type: ignore schema=c.SQLMESH, - context_path=tmp_path, + cache_dir=tmp_path / c.CACHE, ) diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index dd68b5c515..cf1d35bbfc 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -55,7 +55,9 @@ @pytest.fixture def state_sync(duck_conn, tmp_path): state_sync = EngineAdapterStateSync( - create_engine_adapter(lambda: duck_conn, "duckdb"), schema=c.SQLMESH, context_path=tmp_path + create_engine_adapter(lambda: duck_conn, "duckdb"), + schema=c.SQLMESH, + cache_dir=tmp_path / c.CACHE, ) state_sync.migrate(default_catalog=None) return state_sync @@ -2082,7 +2084,9 @@ def test_version_schema(state_sync: EngineAdapterStateSync, tmp_path) -> None: # Start with a clean slate. state_sync = EngineAdapterStateSync( - create_engine_adapter(duckdb.connect, "duckdb"), schema=c.SQLMESH, context_path=tmp_path + create_engine_adapter(duckdb.connect, "duckdb"), + schema=c.SQLMESH, + cache_dir=tmp_path / c.CACHE, ) with pytest.raises( @@ -2203,7 +2207,9 @@ def test_migrate(state_sync: EngineAdapterStateSync, mocker: MockerFixture, tmp_ # Start with a clean slate. state_sync = EngineAdapterStateSync( - create_engine_adapter(duckdb.connect, "duckdb"), schema=c.SQLMESH, context_path=tmp_path + create_engine_adapter(duckdb.connect, "duckdb"), + schema=c.SQLMESH, + cache_dir=tmp_path / c.CACHE, ) state_sync.migrate(default_catalog=None) @@ -2254,7 +2260,9 @@ def test_rollback(state_sync: EngineAdapterStateSync, mocker: MockerFixture) -> def test_first_migration_failure(duck_conn, mocker: MockerFixture, tmp_path) -> None: state_sync = EngineAdapterStateSync( - create_engine_adapter(lambda: duck_conn, "duckdb"), schema=c.SQLMESH, context_path=tmp_path + create_engine_adapter(lambda: duck_conn, "duckdb"), + schema=c.SQLMESH, + cache_dir=tmp_path / c.CACHE, ) mocker.patch.object(state_sync.migrator, "_migrate_rows", side_effect=Exception("mocked error")) with pytest.raises( diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 7024e9f73f..a8d2d04da9 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -652,6 +652,96 @@ def test_clear_caches(tmp_path: pathlib.Path): assert not cache_dir.exists() +def test_cache_path_configurations(tmp_path: pathlib.Path): + project_dir = tmp_path / "project" + project_dir.mkdir(parents=True) + config_file = project_dir / "config.yaml" + + # Test relative path + config_file.write_text("model_defaults:\n dialect: duckdb\ncache_dir: .my_cache") + context = Context(paths=str(project_dir)) + assert context.cache_dir == project_dir / ".my_cache" + + # Test absolute path + abs_cache = tmp_path / "abs_cache" + config_file.write_text(f"model_defaults:\n dialect: duckdb\ncache_dir: {abs_cache}") + context = Context(paths=str(project_dir)) + assert context.cache_dir == abs_cache + + # Test default + config_file.write_text("model_defaults:\n dialect: duckdb") + context = Context(paths=str(project_dir)) + assert context.cache_dir == project_dir / ".cache" + + +def test_plan_apply_populates_cache(copy_to_temp_path, mocker): + sushi_paths = copy_to_temp_path("examples/sushi") + sushi_path = sushi_paths[0] + custom_cache_dir = sushi_path.parent / "custom_cache" + + # Modify the existing config.py to add cache_dir to test_config + config_py_path = sushi_path / "config.py" + with open(config_py_path, "r") as f: + config_content = f.read() + + # Add cache_dir to the test_config definition + config_content = config_content.replace( + 'test_config = Config(\n gateways={"in_memory": GatewayConfig(connection=DuckDBConnectionConfig())},\n default_gateway="in_memory",\n plan=PlanConfig(\n auto_categorize_changes=CategorizerConfig(\n sql=AutoCategorizationMode.SEMI, python=AutoCategorizationMode.OFF\n )\n ),\n model_defaults=model_defaults,\n)', + f"""test_config = Config( + gateways={{"in_memory": GatewayConfig(connection=DuckDBConnectionConfig())}}, + default_gateway="in_memory", + plan=PlanConfig( + auto_categorize_changes=CategorizerConfig( + sql=AutoCategorizationMode.SEMI, python=AutoCategorizationMode.OFF + ) + ), + model_defaults=model_defaults, + cache_dir="{custom_cache_dir.as_posix()}", +)""", + ) + + with open(config_py_path, "w") as f: + f.write(config_content) + + # Create context with the test config + context = Context(paths=sushi_path, config="test_config") + custom_cache_dir = context.cache_dir + assert "custom_cache" in str(custom_cache_dir) + assert (custom_cache_dir / "optimized_query").exists() + assert (custom_cache_dir / "model_definition").exists() + assert not (custom_cache_dir / "snapshot").exists() + + # Clear the cache + context.clear_caches() + assert not custom_cache_dir.exists() + + plan = context.plan("dev", create_from="prod", skip_tests=True) + context.apply(plan) + + # Cache directory should now exist again + assert custom_cache_dir.exists() + assert any(custom_cache_dir.iterdir()) + + # Since the cache has been deleted post loading here only snapshot should exist + assert (custom_cache_dir / "snapshot").exists() + assert not (custom_cache_dir / "optimized_query").exists() + assert not (custom_cache_dir / "model_definition").exists() + + # New context should load same models and create the cache for optimized_query and model_definition + initial_model_count = len(context.models) + context2 = Context(paths=context.path, config="test_config") + cached_model_count = len(context2.models) + + assert initial_model_count == cached_model_count > 0 + assert (custom_cache_dir / "optimized_query").exists() + assert (custom_cache_dir / "model_definition").exists() + assert (custom_cache_dir / "snapshot").exists() + + # Clear caches should remove the custom cache directory + context.clear_caches() + assert not custom_cache_dir.exists() + + def test_ignore_files(mocker: MockerFixture, tmp_path: pathlib.Path): mocker.patch.object( sqlmesh.core.constants, @@ -1831,7 +1921,7 @@ def assert_cached_violations_exist(cache: OptimizedQueryCache, model: Model): ctx.plan_builder("dev") # Case: Ensure error violations are cached if the model did not pass linting - cache = OptimizedQueryCache(tmp_path / c.CACHE) + cache = OptimizedQueryCache(ctx.cache_dir) assert_cached_violations_exist(cache, error_model) diff --git a/web/server/watcher.py b/web/server/watcher.py index 0474059e9f..588f6c5e22 100644 --- a/web/server/watcher.py +++ b/web/server/watcher.py @@ -30,7 +30,10 @@ async def watch_project() -> None: (settings.project_path / c.SEEDS).resolve(), ] ignore_dirs = [".env"] - ignore_paths: t.List[t.Union[str, Path]] = [(settings.project_path / c.CACHE).resolve()] + cache_path = ( + context.cache_dir.resolve() if context else (settings.project_path / c.CACHE).resolve() + ) + ignore_paths: t.List[t.Union[str, Path]] = [cache_path] ignore_entity_patterns = context.config.ignore_patterns if context else c.IGNORE_PATTERNS ignore_entity_patterns.append("^.*\\.db(\\.wal)?$") From b81b1091a6bd8023671727c4b58006a78a63ec00 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Thu, 3 Jul 2025 19:33:00 +1200 Subject: [PATCH 0512/1056] Feat(cicd_bot): Improve output of 'PR Environment Synced' and 'Prod Plan Preview' steps (#4872) --- sqlmesh/core/console.py | 38 +- sqlmesh/integrations/github/cicd/command.py | 9 +- .../integrations/github/cicd/controller.py | 338 +++++-- .../github/cicd/test_github_controller.py | 35 +- .../github/cicd/test_integration.py | 863 ++++++++++++------ 5 files changed, 909 insertions(+), 374 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 01dfc3d09d..2435e56ea8 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -3224,8 +3224,9 @@ def _print_models_with_threshold( ) else: for snapshot_table_info in models: + category_str = SNAPSHOT_CHANGE_CATEGORY_STR[snapshot_table_info.change_category] self._print( - f"- `{snapshot_table_info.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" + f"- `{snapshot_table_info.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}` ({category_str})" ) def _print_modified_models( @@ -3237,7 +3238,7 @@ def _print_modified_models( no_diff: bool = True, ) -> None: directly_modified = [] - indirectly_modified = [] + indirectly_modified: t.List[Snapshot] = [] metadata_modified = [] for snapshot in modified_snapshots: if context_diff.directly_modified(snapshot.name): @@ -3249,11 +3250,40 @@ def _print_modified_models( if directly_modified: self._print("\n**Directly Modified:**") for snapshot in sorted(directly_modified): + category_str = SNAPSHOT_CHANGE_CATEGORY_STR[snapshot.change_category] self._print( - f"- `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}`" + f"* `{snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}` ({category_str})" + ) + + indirectly_modified_children = sorted( + [s for s in indirectly_modified if snapshot.snapshot_id in s.parents] ) + if not no_diff: - self._print(f"```diff\n{context_diff.text_diff(snapshot.name)}\n```") + diff_text = context_diff.text_diff(snapshot.name) + # sometimes there is no text_diff, like on a seed model where the data has been updated + if diff_text: + diff_text = f"\n```diff\n{diff_text}\n```" + # these are part of a Markdown list, so indent them by 2 spaces to relate them to the current list item + diff_text_indented = "\n".join( + [f" {line}" for line in diff_text.splitlines()] + ) + self._print(diff_text_indented) + else: + if indirectly_modified_children: + self._print("\n") + + if indirectly_modified_children: + self._print(" Indirectly Modified Children:") + for child_snapshot in indirectly_modified_children: + child_category_str = SNAPSHOT_CHANGE_CATEGORY_STR[ + child_snapshot.change_category + ] + self._print( + f" - `{child_snapshot.display_name(environment_naming_info, default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, dialect=self.dialect)}` ({child_category_str})" + ) + self._print("\n") + if indirectly_modified: self._print("\n**Indirectly Modified:**") self._print_models_with_threshold( diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index 021c49cf31..f5d614405a 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -115,8 +115,12 @@ def _update_pr_environment(controller: GithubController) -> bool: conclusion = controller.update_pr_environment_check(status=GithubCheckStatus.COMPLETED) return conclusion is not None and conclusion.is_success except Exception as e: + logger.exception("Error occurred when updating PR environment") conclusion = controller.update_pr_environment_check( - status=GithubCheckStatus.COMPLETED, exception=e, plan=controller.pr_plan_or_none + status=GithubCheckStatus.COMPLETED, + exception=e, + plan=controller.pr_plan_or_none, + plan_flags=controller.pr_plan_flags, ) return ( conclusion is not None @@ -147,6 +151,7 @@ def _gen_prod_plan(controller: GithubController) -> bool: ) return bool(plan_summary) except Exception as e: + logger.exception("Error occurred generating prod plan") controller.update_prod_plan_preview_check( status=GithubCheckStatus.COMPLETED, conclusion=GithubCheckConclusion.FAILURE, @@ -211,6 +216,8 @@ def deploy_production(ctx: click.Context) -> None: def _run_all(controller: GithubController) -> None: + click.echo(f"SQLMesh Version: {controller.version_info}") + has_required_approval = False is_auto_deploying_prod = ( controller.deploy_command_enabled or controller.do_required_approval_check diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 8282f3e8d8..5ae2a763e7 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -9,11 +9,11 @@ import traceback import typing as t from enum import Enum -from typing import List from pathlib import Path +from dataclasses import dataclass +from functools import cached_property import requests -from hyperscript import Element, h from sqlglot.helper import seq_get from sqlmesh.core import constants as c @@ -21,13 +21,13 @@ from sqlmesh.core.context import Context from sqlmesh.core.test.result import ModelTextTestResult from sqlmesh.core.environment import Environment -from sqlmesh.core.plan import Plan, PlanBuilder +from sqlmesh.core.plan import Plan, PlanBuilder, SnapshotIntervals +from sqlmesh.core.plan.definition import UserProvidedFlags from sqlmesh.core.snapshot.definition import ( Snapshot, SnapshotChangeCategory, SnapshotId, SnapshotTableInfo, - format_intervals, ) from sqlglot.errors import SqlglotError from sqlmesh.core.user import User @@ -415,6 +415,14 @@ def pr_plan_or_none(self) -> t.Optional[Plan]: except: return None + @property + def pr_plan_flags(self) -> t.Optional[t.Dict[str, UserProvidedFlags]]: + if pr_plan := self.pr_plan_or_none: + return pr_plan.user_provided_flags + if pr_plan_builder := self._pr_plan_builder: + return pr_plan_builder._user_provided_flags + return None + @property def prod_plan(self) -> Plan: if not self._prod_plan_builder: @@ -434,7 +442,9 @@ def prod_plan_with_gaps(self) -> Plan: if not self._prod_plan_with_gaps_builder: self._prod_plan_with_gaps_builder = self._context.plan_builder( c.PROD, + # this is required to highlight any data gaps between this PR environment and prod (since PR environments may only contain a subset of data) no_gaps=False, + # this works because the snapshots were already categorized when applying self.pr_plan so there are no uncategorized local snapshots to trigger a plan error no_auto_categorization=True, skip_tests=True, skip_linter=True, @@ -495,9 +505,17 @@ def get_plan_summary(self, plan: Plan) -> str: difference_summary = self._console.consume_captured_output() self._console._show_missing_dates(plan, self._context.default_catalog) missing_dates = self._console.consume_captured_output() + + plan_flags_section = ( + f"\n\n{self._generate_plan_flags_section(plan.user_provided_flags)}" + if plan.user_provided_flags + else "" + ) + if not difference_summary and not missing_dates: - return "No changes to apply." - return f"{difference_summary}\n{missing_dates}" + return f"No changes to apply.{plan_flags_section}" + + return f"{difference_summary}\n{missing_dates}{plan_flags_section}" except PlanError as e: return f"Plan failed to generate. Check for pending or unresolved changes. Error: {e}" @@ -831,6 +849,9 @@ def update_pr_environment_check( status: GithubCheckStatus, exception: t.Optional[Exception] = None, plan: t.Optional[Plan] = None, + plan_flags: t.Optional[ + t.Dict[str, UserProvidedFlags] + ] = None, # note: the plan flags are passed separately in case the plan fails to build ) -> t.Optional[GithubCheckConclusion]: """ Updates the status of the merge commit for the PR environment. @@ -852,58 +873,16 @@ def conclusion_handler( conclusion: GithubCheckConclusion, exception: t.Optional[Exception] ) -> t.Tuple[GithubCheckConclusion, str, t.Optional[str]]: if conclusion.is_success: - if not self.modified_snapshots: + prod_plan = self.prod_plan_with_gaps + + if not prod_plan.has_changes: summary = "No models were modified in this PR.\n" else: - header_rows = [ - h("th", {"colspan": "3"}, "PR Environment Summary"), - [ - h("th", "Model"), - h("th", "Change Type"), - h("th", "Dates Loaded"), - ], - ] - body_rows: List[Element | List[Element]] = [] - for modified_snapshot in self.modified_snapshots.values(): - # We don't want to display indirect non-breaking since to users these are effectively no-op changes - if modified_snapshot.is_indirect_non_breaking: - continue - if modified_snapshot.snapshot_id in self.removed_snapshots: - # This will be an FQN since we don't have access to node name from a snapshot table info - # which is what a removed snapshot is - model_name = modified_snapshot.name - change_category = SNAPSHOT_CHANGE_CATEGORY_STR[ - SnapshotChangeCategory.BREAKING - ] - interval_output = "REMOVED" - else: - assert isinstance(modified_snapshot, Snapshot) - model_name = modified_snapshot.node.name - change_category = ( - "Uncategorized" - if not modified_snapshot.change_category - else SNAPSHOT_CHANGE_CATEGORY_STR[modified_snapshot.change_category] - ) - intervals = ( - modified_snapshot.dev_intervals - if modified_snapshot.is_forward_only - else modified_snapshot.intervals - ) - interval_output = ( - format_intervals(intervals, modified_snapshot.node.interval_unit) - if intervals - else "N/A" - ) - body_rows.append( - [ - h("td", model_name, autoescape=False), - h("td", change_category), - h("td", interval_output), - ] - ) - table_header = h("thead", [h("tr", row) for row in header_rows]) - table_body = h("tbody", [h("tr", row) for row in body_rows]) - summary = str(h("table", [table_header, table_body])) + intro = self._generate_pr_environment_summary_intro() + summary = intro + self._generate_pr_environment_summary_list(prod_plan) + if prod_plan.user_provided_flags: + summary += self._generate_plan_flags_section(prod_plan.user_provided_flags) + vde_title = ( "- :eyes: To **review** this PR's changes, use virtual data environment:" ) @@ -943,7 +922,11 @@ def conclusion_handler( "If you would like the bot to automatically categorize changes, check the [documentation](https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information." ) elif isinstance(exception, PlanError): - failure_msg = f"Plan application failed.\n\n{self._console.captured_output}" + failure_msg = f"Plan application failed.\n" + if exception.args and (msg := exception.args[0]) and isinstance(msg, str): + failure_msg += f"\n{msg}\n" + if self._console.captured_output: + failure_msg += f"\n{self._console.captured_output}" elif isinstance(exception, (SQLMeshError, SqlglotError, ValueError)): # this logic is taken from the global error handler attached to the CLI, which uses `click.echo()` to output the message # so cant be re-used here because it bypasses the Console @@ -959,13 +942,17 @@ def conclusion_handler( conclusion_to_summary = { GithubCheckConclusion.SKIPPED: f":next_track_button: Skipped creating or updating PR Environment `{self.pr_environment_name}`. {skip_reason}", - GithubCheckConclusion.FAILURE: f":x: Failed to create or update PR Environment `{self.pr_environment_name}`.\n\n{failure_msg}", + GithubCheckConclusion.FAILURE: f":x: Failed to create or update PR Environment `{self.pr_environment_name}` :x:\n\n{failure_msg}", GithubCheckConclusion.CANCELLED: f":stop_sign: Cancelled creating or updating PR Environment `{self.pr_environment_name}`", GithubCheckConclusion.ACTION_REQUIRED: f":warning: Action Required to create or update PR Environment `{self.pr_environment_name}` :warning:\n\n{failure_msg}", } summary = conclusion_to_summary.get( conclusion, f":interrobang: Got an unexpected conclusion: {conclusion.value}" ) + if plan_flags: + plan_flags_section = self._generate_plan_flags_section(plan_flags) + summary += f"\n\n{plan_flags_section}" + self._append_output("pr_environment_name", self.pr_environment_name) return conclusion, check_title, summary @@ -1006,6 +993,12 @@ def conclusion_handler( title = conclusion_to_title.get( conclusion, f"Got an unexpected conclusion: {conclusion.value}" ) + if conclusion == GithubCheckConclusion.SUCCESS and summary: + summary = ( + f"This is a preview that shows the differences between this PR environment `{self.pr_environment_name}` and `prod`.\n\n" + "These are the changes that would be deployed.\n\n" + ) + summary + return conclusion, title, summary self._update_check_handler( @@ -1115,3 +1108,232 @@ def _chunk_up_api_message(self, message: str) -> t.List[str]: @property def running_in_github_actions(self) -> bool: return os.environ.get("GITHUB_ACTIONS", None) == "true" + + @property + def version_info(self) -> str: + from sqlmesh.cli.main import _sqlmesh_version + + return _sqlmesh_version() + + def _generate_plan_flags_section( + self, user_provided_flags: t.Dict[str, UserProvidedFlags] + ) -> str: + # collapsed section syntax: + # https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections#creating-a-collapsed-section + section = "
\n\nPlan flags\n\n" + for flag_name, flag_value in user_provided_flags.items(): + section += f"- `{flag_name}` = `{flag_value}`\n" + section += "\n
" + + return section + + def _generate_pr_environment_summary_intro(self) -> str: + note = "" + subset_reasons = [] + + if self.bot_config.skip_pr_backfill: + subset_reasons.append("`skip_pr_backfill` is enabled") + + if default_pr_start := self.bot_config.default_pr_start: + subset_reasons.append(f"`default_pr_start` is set to `{default_pr_start}`") + + if subset_reasons: + note = ( + "> [!IMPORTANT]\n" + f"> This PR environment may only contain a subset of data because:\n" + + "\n".join(f"> - {r}" for r in subset_reasons) + + "\n" + "> \n" + "> This means that deploying to `prod` may not be a simple virtual update if there is still some data to load.\n" + "> See `Dates not loaded in PR` below or the `Prod Plan Preview` check for more information.\n\n" + ) + + return ( + f"Here is a summary of data that has been loaded into the PR environment `{self.pr_environment_name}` and could be deployed to `prod`.\n\n" + + note + ) + + def _generate_pr_environment_summary_list(self, plan: Plan) -> str: + added_snapshot_ids = set(plan.context_diff.added) + modified_snapshot_ids = set( + s.snapshot_id for s, _ in plan.context_diff.modified_snapshots.values() + ) + removed_snapshot_ids = set(plan.context_diff.removed_snapshots.keys()) + + # note: we sort these to get a deterministic order for the output tests + table_records = sorted( + [ + SnapshotSummaryRecord(snapshot_id=snapshot_id, plan=plan) + for snapshot_id in ( + added_snapshot_ids | modified_snapshot_ids | removed_snapshot_ids + ) + ], + key=lambda r: r.display_name, + ) + + sections = [ + ("### Added", [r for r in table_records if r.is_added]), + ("### Removed", [r for r in table_records if r.is_removed]), + ("### Directly Modified", [r for r in table_records if r.is_directly_modified]), + ("### Indirectly Modified", [r for r in table_records if r.is_indirectly_modified]), + ( + "### Metadata Updated", + [r for r in table_records if r.is_metadata_updated and not r.is_modified], + ), + ] + + summary = "" + for title, records in sections: + if records: + summary += f"\n{title}\n" + + for record in records: + summary += f"{record.as_markdown_list_item}\n" + + return summary + + +@dataclass +class SnapshotSummaryRecord: + snapshot_id: SnapshotId + plan: Plan + + @property + def snapshot(self) -> Snapshot: + if self.is_removed: + raise ValueError("Removed snapshots only have SnapshotTableInfo available") + return self.plan.snapshots[self.snapshot_id] + + @cached_property + def snapshot_table_info(self) -> SnapshotTableInfo: + if self.is_removed: + return self.plan.modified_snapshots[self.snapshot_id].table_info + return self.plan.snapshots[self.snapshot_id].table_info + + @property + def display_name(self) -> str: + dialect = None if self.is_removed else self.snapshot.node.dialect + return self.snapshot_table_info.display_name( + self.plan.environment_naming_info, default_catalog=None, dialect=dialect + ) + + @property + def change_category(self) -> str: + if self.is_removed: + return SNAPSHOT_CHANGE_CATEGORY_STR[SnapshotChangeCategory.BREAKING] + + if change_category := self.snapshot.change_category: + return SNAPSHOT_CHANGE_CATEGORY_STR[change_category] + + return "Uncategorized" + + @property + def is_added(self) -> bool: + return self.snapshot_id in self.plan.context_diff.added + + @property + def is_removed(self) -> bool: + return self.snapshot_id in self.plan.context_diff.removed_snapshots + + @property + def is_dev_preview(self) -> bool: + return not self.plan.deployability_index.is_deployable(self.snapshot_id) + + @property + def is_directly_modified(self) -> bool: + return self.plan.context_diff.directly_modified(self.snapshot_table_info.name) + + @property + def is_indirectly_modified(self) -> bool: + return self.plan.context_diff.indirectly_modified(self.snapshot_table_info.name) + + @property + def is_modified(self) -> bool: + return self.is_directly_modified or self.is_indirectly_modified + + @property + def is_metadata_updated(self) -> bool: + return self.plan.context_diff.metadata_updated(self.snapshot_table_info.name) + + @property + def is_incremental(self) -> bool: + return self.snapshot_table_info.is_incremental + + @property + def modification_type(self) -> str: + if self.is_directly_modified: + return "Directly modified" + if self.is_indirectly_modified: + return "Indirectly modified" + if self.is_metadata_updated: + return "Metadata updated" + + return "Unknown" + + @property + def loaded_intervals(self) -> SnapshotIntervals: + if self.is_removed: + raise ValueError("Removed snapshots dont have loaded intervals available") + + return SnapshotIntervals( + snapshot_id=self.snapshot_id, + intervals=( + self.snapshot.dev_intervals + if self.snapshot.is_forward_only + else self.snapshot.intervals + ), + ) + + @property + def loaded_intervals_rendered(self) -> str: + if self.is_removed: + return "REMOVED" + + return self._format_intervals(self.loaded_intervals) + + @property + def missing_intervals(self) -> t.Optional[SnapshotIntervals]: + return next( + (si for si in self.plan.missing_intervals if si.snapshot_id == self.snapshot_id), + None, + ) + + @property + def missing_intervals_formatted(self) -> str: + if not self.is_removed and (intervals := self.missing_intervals): + return self._format_intervals(intervals) + + return "N/A" + + @property + def as_markdown_list_item(self) -> str: + if self.is_removed: + return f"- `{self.display_name}` ({self.change_category})" + + how_applied = "" + + if not self.is_incremental: + from sqlmesh.core.console import _format_missing_intervals + + # note: this is to re-use the '[recreate view]' and '[full refresh]' text and keep it in sync with updates to the CLI + # it doesnt actually use the passed intervals, those are handled differently + how_applied = _format_missing_intervals(self.snapshot, self.loaded_intervals) + + how_applied_str = f" [{how_applied}]" if how_applied else "" + + item = f"- `{self.display_name}` ({self.change_category})\n" + + if self.snapshot_table_info.model_kind_name: + item += f" **Kind:** {self.snapshot_table_info.model_kind_name}{how_applied_str}\n" + + if self.is_incremental: + # in-depth interval info is only relevant for incremental models + item += f" **Dates loaded in PR:** [{self.loaded_intervals_rendered}]\n" + if self.missing_intervals: + item += f" **Dates *not* loaded in PR:** [{self.missing_intervals_formatted}]\n" + + return item + + def _format_intervals(self, intervals: SnapshotIntervals) -> str: + preview_modifier = " (**preview**)" if self.is_dev_preview else "" + return f"{intervals.format_intervals(self.snapshot.node.interval_unit)}{preview_modifier}" diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index 8b0a35411a..d7d4f5343c 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -11,14 +11,13 @@ from sqlmesh.core.config import CategorizerConfig from sqlmesh.core.dialect import parse_one from sqlmesh.core.model import SqlModel -from sqlmesh.core.snapshot import SnapshotChangeCategory from sqlmesh.core.user import User, UserRole from sqlmesh.integrations.github.cicd.config import GithubCICDBotConfig, MergeMethod from sqlmesh.integrations.github.cicd.controller import ( BotCommand, - GithubCheckStatus, MergeStateStatus, ) +from sqlmesh.integrations.github.cicd.command import _update_pr_environment from sqlmesh.utils.date import to_datetime, now from tests.integrations.github.cicd.conftest import MockIssueComment @@ -545,16 +544,11 @@ def test_uncategorized( make_mock_issue_comment, tmp_path: pathlib.Path, ): - snapshot_categrozied = make_snapshot(SqlModel(name="a", query=parse_one("select 1, ds"))) - snapshot_categrozied.categorize_as(SnapshotChangeCategory.BREAKING) snapshot_uncategorized = make_snapshot(SqlModel(name="b", query=parse_one("select 1, ds"))) mocker.patch( - "sqlmesh.core.plan.Plan.modified_snapshots", + "sqlmesh.core.plan.Plan.uncategorized", PropertyMock( - return_value={ - snapshot_categrozied.snapshot_id: snapshot_categrozied, - snapshot_uncategorized.snapshot_id: snapshot_uncategorized, - }, + return_value=[snapshot_uncategorized], ), ) mock_repo = github_client.get_repo() @@ -570,21 +564,30 @@ def test_uncategorized( ) ) mock_issue.get_comments = mocker.MagicMock(side_effect=lambda: created_comments) + + # note: context is deliberately not mocked out so that context.apply() throws UncategorizedPlanError due to the uncategorized snapshot controller = make_controller( - "tests/fixtures/github/pull_request_synchronized.json", github_client + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + mock_out_context=False, ) + assert controller.pr_plan.uncategorized github_output_file = tmp_path / "github_output.txt" with mock.patch.dict(os.environ, {"GITHUB_OUTPUT": str(github_output_file)}): - controller.update_pr_environment_check(GithubCheckStatus.COMPLETED) + _update_pr_environment(controller) assert "SQLMesh - PR Environment Synced" in controller._check_run_mapping pr_environment_check_run = controller._check_run_mapping[ "SQLMesh - PR Environment Synced" ].all_kwargs - assert len(pr_environment_check_run) == 1 - assert ( - pr_environment_check_run[0]["output"]["summary"] - == """
PR Environment Summary
ModelChange TypeDates Loaded
aBreakingN/A
bUncategorizedN/A
""" - ) + assert len(pr_environment_check_run) == 2 + assert pr_environment_check_run[0]["status"] == "in_progress" + assert pr_environment_check_run[1]["status"] == "completed" + assert pr_environment_check_run[1]["conclusion"] == "action_required" + summary = pr_environment_check_run[1]["output"]["summary"] + assert "Action Required to create or update PR Environment" in summary + assert "The following models could not be categorized automatically" in summary + assert '- "b"' in summary + assert "Run `sqlmesh plan hello_world_2` locally to apply these changes" in summary diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index beda7a5c00..bff9d4d117 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -272,9 +272,19 @@ def test_merge_pr_has_non_breaking_change( assert GithubCheckStatus(pr_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_success assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" + pr_env_summary = pr_checks_runs[2]["output"]["summary"] assert ( - pr_checks_runs[2]["output"]["summary"] - == """
PR Environment Summary
ModelChange TypeDates Loaded
sushi.waiter_revenue_by_dayNon-breaking2022-12-25 - 2022-12-31
""" + """### Directly Modified +- `memory.sushi.waiter_revenue_by_day` (Non-breaking) + **Kind:** INCREMENTAL_BY_TIME_RANGE + **Dates loaded in PR:** [2022-12-25 - 2022-12-31]""" + in pr_env_summary + ) + assert ( + """### Indirectly Modified +- `memory.sushi.top_waiters` (Indirect Non-breaking) + **Kind:** VIEW [recreate view]""" + in pr_env_summary ) assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping @@ -286,34 +296,42 @@ def test_merge_pr_has_non_breaking_change( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan_summary = """\n**Summary of differences from `prod`:** - -**Directly Modified:** -- `sushi.waiter_revenue_by_day` -```diff ---- - -+++ - -@@ -16,7 +16,8 @@ - - SELECT - CAST(o.waiter_id AS INT) AS waiter_id, - CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, -- CAST(o.event_date AS DATE) AS event_date -+ CAST(o.event_date AS DATE) AS event_date, -+ 1 AS new_col - FROM sushi.orders AS o - LEFT JOIN sushi.order_items AS oi - ON o.id = oi.order_id AND o.event_date = oi.event_date -``` - -**Indirectly Modified:** -- `sushi.top_waiters` + expected_prod_plan_directly_modified_summary = """**Directly Modified:** +* `sushi.waiter_revenue_by_day` (Non-breaking) + + ```diff + --- + + +++ + + @@ -16,7 +16,8 @@ + + SELECT + CAST(o.waiter_id AS INT) AS waiter_id, + CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, + - CAST(o.event_date AS DATE) AS event_date + + CAST(o.event_date AS DATE) AS event_date, + + 1 AS new_col + FROM sushi.orders AS o + LEFT JOIN sushi.order_items AS oi + ON o.id = oi.order_id AND o.event_date = oi.event_date + ``` + Indirectly Modified Children: + - `sushi.top_waiters` (Indirect Non-breaking) +""" + expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** +- `sushi.top_waiters` (Indirect Non-breaking) """ + assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" - assert prod_plan_preview_checks_runs[2]["output"]["summary"] == expected_prod_plan_summary + prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] + assert ( + "This is a preview that shows the differences between this PR environment `hello_world_2` and `prod`" + in prod_plan_preview_summary + ) + assert expected_prod_plan_directly_modified_summary in prod_plan_preview_summary + assert expected_prod_plan_indirectly_modified_summary in prod_plan_preview_summary assert "SQLMesh - Prod Environment Synced" in controller._check_run_mapping prod_checks_runs = controller._check_run_mapping["SQLMesh - Prod Environment Synced"].all_kwargs @@ -323,10 +341,10 @@ def test_merge_pr_has_non_breaking_change( assert GithubCheckStatus(prod_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_checks_runs[2]["conclusion"]).is_success assert prod_checks_runs[2]["output"]["title"] == "Deployed to Prod" - assert ( - prod_checks_runs[2]["output"]["summary"] - == "**Generated Prod Plan**\n" + expected_prod_plan_summary - ) + prod_environment_synced_summary = prod_checks_runs[2]["output"]["summary"] + assert "**Generated Prod Plan**" in prod_environment_synced_summary + assert expected_prod_plan_directly_modified_summary in prod_environment_synced_summary + assert expected_prod_plan_indirectly_modified_summary in prod_environment_synced_summary assert "SQLMesh - Has Required Approval" in controller._check_run_mapping approval_checks_runs = controller._check_run_mapping[ @@ -356,19 +374,15 @@ def test_merge_pr_has_non_breaking_change( assert mock_pull_request.merge.called assert len(created_comments) == 1 + comment_body = created_comments[0].body assert ( - created_comments[0].body - == f""":robot: **SQLMesh Bot Info** :robot: + """:robot: **SQLMesh Bot Info** :robot: - :eyes: To **review** this PR's changes, use virtual data environment: - - `hello_world_2` -
- :ship: Prod Plan Being Applied - -{expected_prod_plan_summary} -
- -""" + - `hello_world_2`""" + in comment_body ) + assert expected_prod_plan_directly_modified_summary in comment_body + assert expected_prod_plan_indirectly_modified_summary in comment_body with open(github_output_file, "r", encoding="utf-8") as f: output = f.read() @@ -467,9 +481,20 @@ def test_merge_pr_has_non_breaking_change_diff_start( assert GithubCheckStatus(pr_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_success assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" + pr_env_summary = pr_checks_runs[2]["output"]["summary"] assert ( - pr_checks_runs[2]["output"]["summary"] - == """
PR Environment Summary
ModelChange TypeDates Loaded
sushi.waiter_revenue_by_dayNon-breaking2022-12-29 - 2022-12-31
""" + """### Directly Modified +- `memory.sushi.waiter_revenue_by_day` (Non-breaking) + **Kind:** INCREMENTAL_BY_TIME_RANGE + **Dates loaded in PR:** [2022-12-29 - 2022-12-31] + **Dates *not* loaded in PR:** [2022-12-25 - 2022-12-28]""" + in pr_env_summary + ) + assert ( + """### Indirectly Modified +- `memory.sushi.top_waiters` (Indirect Non-breaking) + **Kind:** VIEW [recreate view]""" + in pr_env_summary ) assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping @@ -482,36 +507,41 @@ def test_merge_pr_has_non_breaking_change_diff_start( assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" - expected_prod_plan = """\n**Summary of differences from `prod`:** - -**Directly Modified:** -- `sushi.waiter_revenue_by_day` -```diff ---- - -+++ -@@ -16,7 +16,8 @@ - - SELECT - CAST(o.waiter_id AS INT) AS waiter_id, - CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, -- CAST(o.event_date AS DATE) AS event_date -+ CAST(o.event_date AS DATE) AS event_date, -+ 1 AS new_col - FROM sushi.orders AS o - LEFT JOIN sushi.order_items AS oi - ON o.id = oi.order_id AND o.event_date = oi.event_date -``` - -**Indirectly Modified:** -- `sushi.top_waiters` - - -**Models needing backfill:** -* `sushi.waiter_revenue_by_day`: [2022-12-25 - 2022-12-28] + expected_prod_plan_directly_modified_summary = """**Directly Modified:** +* `sushi.waiter_revenue_by_day` (Non-breaking) + + ```diff + --- + + +++ + + @@ -16,7 +16,8 @@ + + SELECT + CAST(o.waiter_id AS INT) AS waiter_id, + CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, + - CAST(o.event_date AS DATE) AS event_date + + CAST(o.event_date AS DATE) AS event_date, + + 1 AS new_col + FROM sushi.orders AS o + LEFT JOIN sushi.order_items AS oi + ON o.id = oi.order_id AND o.event_date = oi.event_date + ``` + Indirectly Modified Children: + - `sushi.top_waiters` (Indirect Non-breaking) +""" + expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** +- `sushi.top_waiters` (Indirect Non-breaking) """ - assert prod_plan_preview_checks_runs[2]["output"]["summary"] == expected_prod_plan + + prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] + assert ( + "This is a preview that shows the differences between this PR environment `hello_world_2` and `prod`" + in prod_plan_preview_summary + ) + assert expected_prod_plan_directly_modified_summary in prod_plan_preview_summary + assert expected_prod_plan_indirectly_modified_summary in prod_plan_preview_summary assert "SQLMesh - Prod Environment Synced" in controller._check_run_mapping prod_checks_runs = controller._check_run_mapping["SQLMesh - Prod Environment Synced"].all_kwargs @@ -521,9 +551,10 @@ def test_merge_pr_has_non_breaking_change_diff_start( assert GithubCheckStatus(prod_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_checks_runs[2]["conclusion"]).is_success assert prod_checks_runs[2]["output"]["title"] == "Deployed to Prod" - assert ( - prod_checks_runs[2]["output"]["summary"] == "**Generated Prod Plan**\n" + expected_prod_plan - ) + prod_environment_synced_summary = prod_checks_runs[2]["output"]["summary"] + assert "**Generated Prod Plan**" in prod_environment_synced_summary + assert expected_prod_plan_directly_modified_summary in prod_environment_synced_summary + assert expected_prod_plan_indirectly_modified_summary in prod_environment_synced_summary assert "SQLMesh - Has Required Approval" in controller._check_run_mapping approval_checks_runs = controller._check_run_mapping[ @@ -554,19 +585,15 @@ def test_merge_pr_has_non_breaking_change_diff_start( assert mock_pull_request.merge.called assert len(created_comments) == 1 + comment_body = created_comments[0].body assert ( - created_comments[0].body - == f""":robot: **SQLMesh Bot Info** :robot: + """:robot: **SQLMesh Bot Info** :robot: - :eyes: To **review** this PR's changes, use virtual data environment: - - `hello_world_2` -
- :ship: Prod Plan Being Applied - -{expected_prod_plan} -
- -""" + - `hello_world_2`""" + in comment_body ) + assert expected_prod_plan_directly_modified_summary in comment_body + assert expected_prod_plan_indirectly_modified_summary in comment_body with open(github_output_file, "r", encoding="utf-8") as f: output = f.read() @@ -663,9 +690,15 @@ def test_merge_pr_has_non_breaking_change_no_categorization( assert GithubCheckStatus(pr_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_action_required assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" + pr_env_summary = pr_checks_runs[2]["output"]["summary"] assert ( - pr_checks_runs[2]["output"]["summary"] - == """:warning: Action Required to create or update PR Environment `hello_world_2` :warning:\n\nThe following models could not be categorized automatically:\n- "memory"."sushi"."waiter_revenue_by_day"\n\nRun `sqlmesh plan hello_world_2` locally to apply these changes.\n\nIf you would like the bot to automatically categorize changes, check the [documentation](https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information.""" + """:warning: Action Required to create or update PR Environment `hello_world_2` :warning: + +The following models could not be categorized automatically: +- "memory"."sushi"."waiter_revenue_by_day" + +Run `sqlmesh plan hello_world_2` locally to apply these changes""" + in pr_env_summary ) assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping @@ -813,8 +846,8 @@ def test_merge_pr_has_no_changes( assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_skipped assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" assert ( - pr_checks_runs[2]["output"]["summary"] - == ":next_track_button: Skipped creating or updating PR Environment `hello_world_2`. No changes were detected compared to the prod environment." + ":next_track_button: Skipped creating or updating PR Environment `hello_world_2`. No changes were detected compared to the prod environment." + in pr_checks_runs[2]["output"]["summary"] ) assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping @@ -827,10 +860,10 @@ def test_merge_pr_has_no_changes( assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success expected_prod_plan_summary = ( - "\n**No changes to plan: project files match the `prod` environment**\n\n\n" + "**No changes to plan: project files match the `prod` environment**" ) assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" - assert prod_plan_preview_checks_runs[2]["output"]["summary"] == expected_prod_plan_summary + assert expected_prod_plan_summary in prod_plan_preview_checks_runs[2]["output"]["summary"] assert "SQLMesh - Prod Environment Synced" in controller._check_run_mapping prod_checks_runs = controller._check_run_mapping["SQLMesh - Prod Environment Synced"].all_kwargs @@ -840,10 +873,9 @@ def test_merge_pr_has_no_changes( assert GithubCheckStatus(prod_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_checks_runs[2]["conclusion"]).is_success assert prod_checks_runs[2]["output"]["title"] == "Deployed to Prod" - assert ( - prod_checks_runs[2]["output"]["summary"] - == "**Generated Prod Plan**\n" + expected_prod_plan_summary - ) + prod_environment_synced_summary = prod_checks_runs[2]["output"]["summary"] + assert "**Generated Prod Plan**" in prod_environment_synced_summary + assert expected_prod_plan_summary in prod_environment_synced_summary assert "SQLMesh - Has Required Approval" in controller._check_run_mapping approval_checks_runs = controller._check_run_mapping[ @@ -870,17 +902,14 @@ def test_merge_pr_has_no_changes( assert mock_pull_request.merge.called assert len(created_comments) == 1 + comment_body = created_comments[0].body assert ( - created_comments[0].body - == f""":robot: **SQLMesh Bot Info** :robot: + f""":robot: **SQLMesh Bot Info** :robot:
- :ship: Prod Plan Being Applied - -{expected_prod_plan_summary} -
- -""" + :ship: Prod Plan Being Applied""" + in comment_body ) + assert expected_prod_plan_summary in comment_body with open(github_output_file, "r", encoding="utf-8") as f: output = f.read() @@ -977,9 +1006,19 @@ def test_no_merge_since_no_deploy_signal( assert GithubCheckStatus(pr_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_success assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" + pr_env_summary = pr_checks_runs[2]["output"]["summary"] + assert ( + """### Directly Modified +- `memory.sushi.waiter_revenue_by_day` (Non-breaking) + **Kind:** INCREMENTAL_BY_TIME_RANGE + **Dates loaded in PR:** [2022-12-25 - 2022-12-31]""" + in pr_env_summary + ) assert ( - pr_checks_runs[2]["output"]["summary"] - == """
PR Environment Summary
ModelChange TypeDates Loaded
sushi.waiter_revenue_by_dayNon-breaking2022-12-25 - 2022-12-31
""" + """### Indirectly Modified +- `memory.sushi.top_waiters` (Indirect Non-breaking) + **Kind:** VIEW [recreate view]""" + in pr_env_summary ) assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping @@ -991,34 +1030,42 @@ def test_no_merge_since_no_deploy_signal( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan = """\n**Summary of differences from `prod`:** - -**Directly Modified:** -- `sushi.waiter_revenue_by_day` -```diff ---- - -+++ - -@@ -16,7 +16,8 @@ - - SELECT - CAST(o.waiter_id AS INT) AS waiter_id, - CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, -- CAST(o.event_date AS DATE) AS event_date -+ CAST(o.event_date AS DATE) AS event_date, -+ 1 AS new_col - FROM sushi.orders AS o - LEFT JOIN sushi.order_items AS oi - ON o.id = oi.order_id AND o.event_date = oi.event_date -``` - -**Indirectly Modified:** -- `sushi.top_waiters` + expected_prod_plan_directly_modified_summary = """**Directly Modified:** +* `sushi.waiter_revenue_by_day` (Non-breaking) + + ```diff + --- + + +++ + + @@ -16,7 +16,8 @@ + + SELECT + CAST(o.waiter_id AS INT) AS waiter_id, + CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, + - CAST(o.event_date AS DATE) AS event_date + + CAST(o.event_date AS DATE) AS event_date, + + 1 AS new_col + FROM sushi.orders AS o + LEFT JOIN sushi.order_items AS oi + ON o.id = oi.order_id AND o.event_date = oi.event_date + ``` + Indirectly Modified Children: + - `sushi.top_waiters` (Indirect Non-breaking)""" + + expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** +- `sushi.top_waiters` (Indirect Non-breaking) """ + assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" - assert prod_plan_preview_checks_runs[2]["output"]["summary"] == expected_prod_plan + prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] + assert ( + "This is a preview that shows the differences between this PR environment `hello_world_2` and `prod`" + in prod_plan_preview_summary + ) + assert expected_prod_plan_directly_modified_summary in prod_plan_preview_summary + assert expected_prod_plan_indirectly_modified_summary in prod_plan_preview_summary assert "SQLMesh - Prod Environment Synced" in controller._check_run_mapping prod_checks_runs = controller._check_run_mapping["SQLMesh - Prod Environment Synced"].all_kwargs @@ -1159,9 +1206,20 @@ def test_no_merge_since_no_deploy_signal_no_approvers_defined( assert GithubCheckStatus(pr_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_success assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" + pr_env_summary = pr_checks_runs[2]["output"]["summary"] + assert ( + """### Directly Modified +- `memory.sushi.waiter_revenue_by_day` (Non-breaking) + **Kind:** INCREMENTAL_BY_TIME_RANGE + **Dates loaded in PR:** [2022-12-30 - 2022-12-31] + **Dates *not* loaded in PR:** [2022-12-25 - 2022-12-29]""" + in pr_env_summary + ) assert ( - pr_checks_runs[2]["output"]["summary"] - == """
PR Environment Summary
ModelChange TypeDates Loaded
sushi.waiter_revenue_by_dayNon-breaking2022-12-30 - 2022-12-31
""" + """### Indirectly Modified +- `memory.sushi.top_waiters` (Indirect Non-breaking) + **Kind:** VIEW [recreate view]""" + in pr_env_summary ) assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping @@ -1173,37 +1231,40 @@ def test_no_merge_since_no_deploy_signal_no_approvers_defined( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan = """\n**Summary of differences from `prod`:** - -**Directly Modified:** -- `sushi.waiter_revenue_by_day` -```diff ---- - -+++ - -@@ -16,7 +16,8 @@ - - SELECT - CAST(o.waiter_id AS INT) AS waiter_id, - CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, -- CAST(o.event_date AS DATE) AS event_date -+ CAST(o.event_date AS DATE) AS event_date, -+ 1 AS new_col - FROM sushi.orders AS o - LEFT JOIN sushi.order_items AS oi - ON o.id = oi.order_id AND o.event_date = oi.event_date -``` - -**Indirectly Modified:** -- `sushi.top_waiters` - - -**Models needing backfill:** -* `sushi.waiter_revenue_by_day`: [2022-12-25 - 2022-12-29] + expected_prod_plan_directly_modified_summary = """**Directly Modified:** +* `sushi.waiter_revenue_by_day` (Non-breaking) + + ```diff + --- + + +++ + + @@ -16,7 +16,8 @@ + + SELECT + CAST(o.waiter_id AS INT) AS waiter_id, + CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, + - CAST(o.event_date AS DATE) AS event_date + + CAST(o.event_date AS DATE) AS event_date, + + 1 AS new_col + FROM sushi.orders AS o + LEFT JOIN sushi.order_items AS oi + ON o.id = oi.order_id AND o.event_date = oi.event_date + ``` + Indirectly Modified Children: + - `sushi.top_waiters` (Indirect Non-breaking) +""" + expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** +- `sushi.top_waiters` (Indirect Non-breaking) """ assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" - assert prod_plan_preview_checks_runs[2]["output"]["summary"] == expected_prod_plan + prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] + assert ( + "This is a preview that shows the differences between this PR environment `hello_world_2` and `prod`" + in prod_plan_preview_summary + ) + assert expected_prod_plan_directly_modified_summary in prod_plan_preview_summary + assert expected_prod_plan_indirectly_modified_summary in prod_plan_preview_summary assert "SQLMesh - Prod Environment Synced" not in controller._check_run_mapping assert "SQLMesh - Has Required Approval" not in controller._check_run_mapping @@ -1328,9 +1389,19 @@ def test_deploy_comment_pre_categorized( assert GithubCheckStatus(pr_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_success assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" + pr_env_summary = pr_checks_runs[2]["output"]["summary"] + assert ( + """### Directly Modified +- `memory.sushi.waiter_revenue_by_day` (Non-breaking) + **Kind:** INCREMENTAL_BY_TIME_RANGE + **Dates loaded in PR:** [2022-12-25 - 2022-12-31]""" + in pr_env_summary + ) assert ( - pr_checks_runs[2]["output"]["summary"] - == """
PR Environment Summary
ModelChange TypeDates Loaded
sushi.waiter_revenue_by_dayNon-breaking2022-12-25 - 2022-12-31
""" + """### Indirectly Modified +- `memory.sushi.top_waiters` (Indirect Non-breaking) + **Kind:** VIEW [recreate view]""" + in pr_env_summary ) assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping @@ -1342,34 +1413,40 @@ def test_deploy_comment_pre_categorized( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan = """\n**Summary of differences from `prod`:** - -**Directly Modified:** -- `sushi.waiter_revenue_by_day` -```diff ---- - -+++ - -@@ -16,7 +16,8 @@ - - SELECT - CAST(o.waiter_id AS INT) AS waiter_id, - CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, -- CAST(o.event_date AS DATE) AS event_date -+ CAST(o.event_date AS DATE) AS event_date, -+ 1 AS new_col - FROM sushi.orders AS o - LEFT JOIN sushi.order_items AS oi - ON o.id = oi.order_id AND o.event_date = oi.event_date -``` - -**Indirectly Modified:** -- `sushi.top_waiters` - + expected_prod_plan_directly_modified_summary = """**Directly Modified:** +* `sushi.waiter_revenue_by_day` (Non-breaking) + + ```diff + --- + + +++ + + @@ -16,7 +16,8 @@ + + SELECT + CAST(o.waiter_id AS INT) AS waiter_id, + CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, + - CAST(o.event_date AS DATE) AS event_date + + CAST(o.event_date AS DATE) AS event_date, + + 1 AS new_col + FROM sushi.orders AS o + LEFT JOIN sushi.order_items AS oi + ON o.id = oi.order_id AND o.event_date = oi.event_date + ``` + Indirectly Modified Children: + - `sushi.top_waiters` (Indirect Non-breaking) +""" + expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** +- `sushi.top_waiters` (Indirect Non-breaking) """ assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" - assert prod_plan_preview_checks_runs[2]["output"]["summary"] == expected_prod_plan + prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] + assert ( + "This is a preview that shows the differences between this PR environment `hello_world_2` and `prod`" + in prod_plan_preview_summary + ) + assert expected_prod_plan_directly_modified_summary in prod_plan_preview_summary + assert expected_prod_plan_indirectly_modified_summary in prod_plan_preview_summary assert "SQLMesh - Prod Environment Synced" in controller._check_run_mapping prod_checks_runs = controller._check_run_mapping["SQLMesh - Prod Environment Synced"].all_kwargs @@ -1379,9 +1456,10 @@ def test_deploy_comment_pre_categorized( assert GithubCheckStatus(prod_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_checks_runs[2]["conclusion"]).is_success assert prod_checks_runs[2]["output"]["title"] == "Deployed to Prod" - assert ( - prod_checks_runs[2]["output"]["summary"] == "**Generated Prod Plan**\n" + expected_prod_plan - ) + prod_environment_synced_summary = prod_checks_runs[2]["output"]["summary"] + assert "**Generated Prod Plan**" in prod_environment_synced_summary + assert expected_prod_plan_directly_modified_summary in prod_environment_synced_summary + assert expected_prod_plan_indirectly_modified_summary in prod_environment_synced_summary assert "SQLMesh - Has Required Approval" not in controller._check_run_mapping @@ -1393,21 +1471,19 @@ def test_deploy_comment_pre_categorized( assert mock_pull_request.merge.called assert len(created_comments) == 1 + comment_body = created_comments[0].body assert ( - created_comments[0].body - == f""":robot: **SQLMesh Bot Info** :robot: + """:robot: **SQLMesh Bot Info** :robot: - :eyes: To **review** this PR's changes, use virtual data environment: - `hello_world_2` - :arrow_forward: To **apply** this PR's plan to prod, comment: - `/deploy`
- :ship: Prod Plan Being Applied - -{expected_prod_plan} -
- -""" + :ship: Prod Plan Being Applied""" + in comment_body ) + assert expected_prod_plan_directly_modified_summary in comment_body + assert expected_prod_plan_indirectly_modified_summary in comment_body with open(github_output_file, "r", encoding="utf-8") as f: output = f.read() @@ -1667,9 +1743,31 @@ def test_overlapping_changes_models( assert GithubCheckStatus(pr_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_success assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" + pr_env_summary = pr_checks_runs[2]["output"]["summary"] assert ( - pr_checks_runs[2]["output"]["summary"] - == """
PR Environment Summary
ModelChange TypeDates Loaded
sushi.customersNon-breaking2022-12-25 - 2022-12-31
sushi.waiter_namesBreaking2022-12-31 - 2022-12-31
sushi.waiter_as_customer_by_dayIndirect Breaking2022-12-25 - 2022-12-31
""" + """### Directly Modified +- `memory.sushi.customers` (Non-breaking) + **Kind:** FULL [full refresh] + +- `memory.sushi.waiter_names` (Breaking) + **Kind:** SEED [full refresh]""" + in pr_env_summary + ) + assert ( + """### Indirectly Modified +- `memory.sushi.active_customers` (Indirect Non-breaking) + **Kind:** CUSTOM [full refresh] + +- `memory.sushi.count_customers_active` (Indirect Non-breaking) + **Kind:** FULL [full refresh] + +- `memory.sushi.count_customers_inactive` (Indirect Non-breaking) + **Kind:** FULL [full refresh] + +- `memory.sushi.waiter_as_customer_by_day` (Indirect Breaking) + **Kind:** INCREMENTAL_BY_TIME_RANGE + **Dates loaded in PR:** [2022-12-25 - 2022-12-31]""" + in pr_env_summary ) assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping @@ -1681,41 +1779,54 @@ def test_overlapping_changes_models( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan_summary = """\n**Summary of differences from `prod`:** - -**Directly Modified:** -- `sushi.customers` -```diff ---- - -+++ - -@@ -29,7 +29,8 @@ - - SELECT DISTINCT - CAST(o.customer_id AS INT) AS customer_id, - m.status, -- d.zip -+ d.zip, -+ 1 AS new_col - FROM sushi.orders AS o - LEFT JOIN ( - WITH current_marketing AS ( -``` -- `sushi.waiter_names` -```diff - -``` - -**Indirectly Modified:** -- `sushi.active_customers` -- `sushi.count_customers_active` -- `sushi.count_customers_inactive` -- `sushi.waiter_as_customer_by_day` -""" + expected_prod_plan_directly_modified_summary = """**Directly Modified:** +* `sushi.customers` (Non-breaking) + + ```diff + --- + + +++ + + @@ -29,7 +29,8 @@ + + SELECT DISTINCT + CAST(o.customer_id AS INT) AS customer_id, + m.status, + - d.zip + + d.zip, + + 1 AS new_col + FROM sushi.orders AS o + LEFT JOIN ( + WITH current_marketing AS ( + ``` + Indirectly Modified Children: + - `sushi.active_customers` (Indirect Non-breaking) + - `sushi.count_customers_active` (Indirect Non-breaking) + - `sushi.count_customers_inactive` (Indirect Non-breaking) + - `sushi.waiter_as_customer_by_day` (Indirect Breaking) + + +* `sushi.waiter_names` (Breaking) + + + Indirectly Modified Children: + - `sushi.waiter_as_customer_by_day` (Indirect Breaking)""" + + expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** +- `sushi.active_customers` (Indirect Non-breaking) +- `sushi.count_customers_active` (Indirect Non-breaking) +- `sushi.count_customers_inactive` (Indirect Non-breaking) +- `sushi.waiter_as_customer_by_day` (Indirect Breaking)""" + assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" - assert prod_plan_preview_checks_runs[2]["output"]["summary"] == expected_prod_plan_summary + prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] + assert ( + "This is a preview that shows the differences between this PR environment `hello_world_2` and `prod`" + in prod_plan_preview_summary + ) + assert expected_prod_plan_directly_modified_summary in prod_plan_preview_summary + assert expected_prod_plan_indirectly_modified_summary in prod_plan_preview_summary assert "SQLMesh - Prod Environment Synced" in controller._check_run_mapping prod_checks_runs = controller._check_run_mapping["SQLMesh - Prod Environment Synced"].all_kwargs @@ -1725,10 +1836,10 @@ def test_overlapping_changes_models( assert GithubCheckStatus(prod_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_checks_runs[2]["conclusion"]).is_success assert prod_checks_runs[2]["output"]["title"] == "Deployed to Prod" - assert ( - prod_checks_runs[2]["output"]["summary"] - == "**Generated Prod Plan**\n" + expected_prod_plan_summary - ) + prod_environment_synced_summary = prod_checks_runs[2]["output"]["summary"] + assert "**Generated Prod Plan**" in prod_environment_synced_summary + assert expected_prod_plan_directly_modified_summary in prod_environment_synced_summary + assert expected_prod_plan_indirectly_modified_summary in prod_environment_synced_summary assert "SQLMesh - Has Required Approval" in controller._check_run_mapping approval_checks_runs = controller._check_run_mapping[ @@ -1756,19 +1867,17 @@ def test_overlapping_changes_models( assert mock_pull_request.merge.called assert len(created_comments) == 1 + comment_body = created_comments[0].body assert ( - created_comments[0].body - == f""":robot: **SQLMesh Bot Info** :robot: + f""":robot: **SQLMesh Bot Info** :robot: - :eyes: To **review** this PR's changes, use virtual data environment: - `hello_world_2`
- :ship: Prod Plan Being Applied - -{expected_prod_plan_summary} -
- -""" + :ship: Prod Plan Being Applied""" + in comment_body ) + assert expected_prod_plan_directly_modified_summary in comment_body + assert expected_prod_plan_indirectly_modified_summary in comment_body with open(github_output_file, "r", encoding="utf-8") as f: output = f.read() @@ -1778,6 +1887,160 @@ def test_overlapping_changes_models( ) +@time_machine.travel("2023-01-01 15:00:00 UTC") +def test_pr_add_model( + github_client, + make_controller, + make_mock_check_run, + make_mock_issue_comment, + make_pull_request_review, + tmp_path: pathlib.Path, + mocker: MockerFixture, +): + """ + PR with an added model and auto-categorization will be backfilled, merged, and deployed to prod + + Scenario: + - PR is not merged + - /deploy command has been issued + - Tests passed + - PR Merge Method defined + - Changes made in PR with auto-categorization + """ + + mock_repo = github_client.get_repo() + mock_repo.create_check_run = mocker.MagicMock( + side_effect=lambda **kwargs: make_mock_check_run(**kwargs) + ) + + created_comments: t.List[MockIssueComment] = [] + mock_issue = mock_repo.get_issue() + mock_issue.create_comment = mocker.MagicMock( + side_effect=lambda comment: make_mock_issue_comment( + comment=comment, created_comments=created_comments + ) + ) + mock_issue.get_comments = mocker.MagicMock(side_effect=lambda: created_comments) + + mock_pull_request = mock_repo.get_pull() + mock_pull_request.get_reviews = mocker.MagicMock( + side_effect=lambda: [make_pull_request_review(username="test_github", state="APPROVED")] + ) + mock_pull_request.merged = False + mock_pull_request.merge = mocker.MagicMock() + + controller = make_controller( + "tests/fixtures/github/pull_request_command_deploy.json", + github_client, + bot_config=GithubCICDBotConfig( + merge_method=MergeMethod.MERGE, + auto_categorize_changes=CategorizerConfig.all_full(), + enable_deploy_command=True, + default_pr_start=None, + skip_pr_backfill=False, + ), + mock_out_context=False, + ) + controller._context.plan("prod", no_prompts=True, auto_apply=True) + + # Add a model + (controller._context.path / "models" / "cicd_test_model.sql").write_text( + """ + MODEL ( + name sushi.cicd_test_model, + kind FULL + ); + + select 1; + """ + ) + controller._context.load() + assert '"memory"."sushi"."cicd_test_model"' in controller._context.models + + github_output_file = tmp_path / "github_output.txt" + + with mock.patch.dict(os.environ, {"GITHUB_OUTPUT": str(github_output_file)}): + command._run_all(controller) + + assert "SQLMesh - Run Unit Tests" in controller._check_run_mapping + test_checks_runs = controller._check_run_mapping["SQLMesh - Run Unit Tests"].all_kwargs + assert len(test_checks_runs) == 3 + assert GithubCheckStatus(test_checks_runs[0]["status"]).is_queued + assert GithubCheckStatus(test_checks_runs[1]["status"]).is_in_progress + assert GithubCheckStatus(test_checks_runs[2]["status"]).is_completed + assert GithubCheckConclusion(test_checks_runs[2]["conclusion"]).is_success + assert test_checks_runs[2]["output"]["title"] == "Tests Passed" + print(test_checks_runs[2]["output"]["summary"]) + assert ( + test_checks_runs[2]["output"]["summary"].strip() + == "**Successfully Ran `3` Tests Against `duckdb`**" + ) + + assert "SQLMesh - PR Environment Synced" in controller._check_run_mapping + pr_checks_runs = controller._check_run_mapping["SQLMesh - PR Environment Synced"].all_kwargs + assert len(pr_checks_runs) == 3 + assert GithubCheckStatus(pr_checks_runs[0]["status"]).is_queued + assert GithubCheckStatus(pr_checks_runs[1]["status"]).is_in_progress + assert GithubCheckStatus(pr_checks_runs[2]["status"]).is_completed + assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_success + assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" + pr_env_summary = pr_checks_runs[2]["output"]["summary"] + assert ( + """### Added +- `memory.sushi.cicd_test_model` (Breaking) + **Kind:** FULL [full refresh]""" + in pr_env_summary + ) + + expected_prod_plan_summary = """**Added Models:** +- `sushi.cicd_test_model` (Breaking)""" + + assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping + prod_plan_preview_checks_runs = controller._check_run_mapping[ + "SQLMesh - Prod Plan Preview" + ].all_kwargs + assert len(prod_plan_preview_checks_runs) == 3 + assert GithubCheckStatus(prod_plan_preview_checks_runs[0]["status"]).is_queued + assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress + assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed + assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success + assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" + assert expected_prod_plan_summary in prod_plan_preview_checks_runs[2]["output"]["summary"] + + assert "SQLMesh - Prod Environment Synced" in controller._check_run_mapping + prod_checks_runs = controller._check_run_mapping["SQLMesh - Prod Environment Synced"].all_kwargs + assert len(prod_checks_runs) == 3 + assert GithubCheckStatus(prod_checks_runs[0]["status"]).is_queued + assert GithubCheckStatus(prod_checks_runs[1]["status"]).is_in_progress + assert GithubCheckStatus(prod_checks_runs[2]["status"]).is_completed + assert GithubCheckConclusion(prod_checks_runs[2]["conclusion"]).is_success + assert prod_checks_runs[2]["output"]["title"] == "Deployed to Prod" + prod_environment_synced_summary = prod_checks_runs[2]["output"]["summary"] + assert "**Generated Prod Plan**" in prod_environment_synced_summary + assert expected_prod_plan_summary in prod_environment_synced_summary + + assert mock_pull_request.merge.called + + assert len(created_comments) == 1 + comment_body = created_comments[0].body + assert ( + """:robot: **SQLMesh Bot Info** :robot: +- :eyes: To **review** this PR's changes, use virtual data environment: + - `hello_world_2` +- :arrow_forward: To **apply** this PR's plan to prod, comment: + - `/deploy` +
+ :ship: Prod Plan Being Applied""" + in comment_body + ) + assert expected_prod_plan_summary in comment_body + + assert ( + github_output_file.read_text() + == "run_unit_tests=success\ncreated_pr_environment=true\npr_environment_name=hello_world_2\npr_environment_synced=success\nprod_plan_preview=success\nprod_environment_synced=success\n" + ) + + @time_machine.travel("2023-01-01 15:00:00 UTC") def test_pr_delete_model( github_client, @@ -1873,17 +2136,15 @@ def test_pr_delete_model( assert GithubCheckStatus(pr_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_success assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" + pr_env_summary = pr_checks_runs[2]["output"]["summary"] assert ( - pr_checks_runs[2]["output"]["summary"] - == """
PR Environment Summary
ModelChange TypeDates Loaded
"memory"."sushi"."top_waiters"BreakingREMOVED
""" + """### Removed +- `memory.sushi.top_waiters` (Breaking)""" + in pr_env_summary ) - expected_prod_plan_summary = """\n**Summary of differences from `prod`:** - -**Removed Models:** -- `sushi.top_waiters` - -""" + expected_prod_plan_summary = """**Removed Models:** +- `sushi.top_waiters` (Breaking)""" assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping prod_plan_preview_checks_runs = controller._check_run_mapping[ @@ -1895,7 +2156,7 @@ def test_pr_delete_model( assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" - assert prod_plan_preview_checks_runs[2]["output"]["summary"] == expected_prod_plan_summary + assert expected_prod_plan_summary in prod_plan_preview_checks_runs[2]["output"]["summary"] assert "SQLMesh - Prod Environment Synced" in controller._check_run_mapping prod_checks_runs = controller._check_run_mapping["SQLMesh - Prod Environment Synced"].all_kwargs @@ -1905,10 +2166,9 @@ def test_pr_delete_model( assert GithubCheckStatus(prod_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_checks_runs[2]["conclusion"]).is_success assert prod_checks_runs[2]["output"]["title"] == "Deployed to Prod" - assert ( - prod_checks_runs[2]["output"]["summary"] - == "**Generated Prod Plan**\n" + expected_prod_plan_summary - ) + prod_environment_synced_summary = prod_checks_runs[2]["output"]["summary"] + assert "**Generated Prod Plan**" in prod_environment_synced_summary + assert expected_prod_plan_summary in prod_environment_synced_summary assert "SQLMesh - Has Required Approval" in controller._check_run_mapping approval_checks_runs = controller._check_run_mapping[ @@ -1935,19 +2195,16 @@ def test_pr_delete_model( assert mock_pull_request.merge.called assert len(created_comments) == 1 + comment_body = created_comments[0].body assert ( - created_comments[0].body - == f""":robot: **SQLMesh Bot Info** :robot: + """:robot: **SQLMesh Bot Info** :robot: - :eyes: To **review** this PR's changes, use virtual data environment: - `hello_world_2`
- :ship: Prod Plan Being Applied - -{expected_prod_plan_summary} -
- -""" + :ship: Prod Plan Being Applied""" + in comment_body ) + assert expected_prod_plan_summary in comment_body with open(github_output_file, "r", encoding="utf-8") as f: output = f.read() @@ -2048,9 +2305,19 @@ def test_has_required_approval_but_not_base_branch( assert GithubCheckStatus(pr_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_success assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" + pr_env_summary = pr_checks_runs[2]["output"]["summary"] + assert ( + """### Directly Modified +- `memory.sushi.waiter_revenue_by_day` (Non-breaking) + **Kind:** INCREMENTAL_BY_TIME_RANGE + **Dates loaded in PR:** [2022-12-25 - 2022-12-31]""" + in pr_env_summary + ) assert ( - pr_checks_runs[2]["output"]["summary"] - == """
PR Environment Summary
ModelChange TypeDates Loaded
sushi.waiter_revenue_by_dayNon-breaking2022-12-25 - 2022-12-31
""" + """### Indirectly Modified +- `memory.sushi.top_waiters` (Indirect Non-breaking) + **Kind:** VIEW [recreate view]""" + in pr_env_summary ) assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping @@ -2062,34 +2329,40 @@ def test_has_required_approval_but_not_base_branch( assert GithubCheckStatus(prod_plan_preview_checks_runs[1]["status"]).is_in_progress assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success - expected_prod_plan_summary = """\n**Summary of differences from `prod`:** - -**Directly Modified:** -- `sushi.waiter_revenue_by_day` -```diff ---- + expected_prod_plan_directly_modified_summary = """**Directly Modified:** +* `sushi.waiter_revenue_by_day` (Non-breaking) + + ```diff + --- + + +++ + + @@ -16,7 +16,8 @@ + + SELECT + CAST(o.waiter_id AS INT) AS waiter_id, + CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, + - CAST(o.event_date AS DATE) AS event_date + + CAST(o.event_date AS DATE) AS event_date, + + 1 AS new_col + FROM sushi.orders AS o + LEFT JOIN sushi.order_items AS oi + ON o.id = oi.order_id AND o.event_date = oi.event_date + ``` + Indirectly Modified Children: + - `sushi.top_waiters` (Indirect Non-breaking)""" + + expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** +- `sushi.top_waiters` (Indirect Non-breaking)""" -+++ - -@@ -16,7 +16,8 @@ - - SELECT - CAST(o.waiter_id AS INT) AS waiter_id, - CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue, -- CAST(o.event_date AS DATE) AS event_date -+ CAST(o.event_date AS DATE) AS event_date, -+ 1 AS new_col - FROM sushi.orders AS o - LEFT JOIN sushi.order_items AS oi - ON o.id = oi.order_id AND o.event_date = oi.event_date -``` - -**Indirectly Modified:** -- `sushi.top_waiters` - -""" assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" - assert prod_plan_preview_checks_runs[2]["output"]["summary"] == expected_prod_plan_summary + prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] + assert ( + "This is a preview that shows the differences between this PR environment `hello_world_2` and `prod`" + in prod_plan_preview_summary + ) + assert expected_prod_plan_directly_modified_summary in prod_plan_preview_summary + assert expected_prod_plan_indirectly_modified_summary in prod_plan_preview_summary assert "SQLMesh - Prod Environment Synced" not in controller._check_run_mapping From f33beb61dd8fb5d47d6babd22dd70b16f47e7838 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:40:09 +0100 Subject: [PATCH 0513/1056] chore(vscode): improving the playwright setup/teardown steps (#4883) --- vscode/extension/playwright.config.ts | 11 ++++++++++- .../{global-setup.ts => extension.setup.ts} | 7 +++---- vscode/extension/tests/extension.teardown.ts | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) rename vscode/extension/tests/{global-setup.ts => extension.setup.ts} (96%) create mode 100644 vscode/extension/tests/extension.teardown.ts diff --git a/vscode/extension/playwright.config.ts b/vscode/extension/playwright.config.ts index 527d699668..8b6beaf500 100644 --- a/vscode/extension/playwright.config.ts +++ b/vscode/extension/playwright.config.ts @@ -7,8 +7,16 @@ export default defineConfig({ retries: process.env.CI ? 2 : 0, workers: 4, reporter: [['html', { outputFolder: 'playwright-report' }], ['list']], - globalSetup: './tests/global-setup.ts', projects: [ + { + name: 'setup', + testMatch: 'tests/extension.setup.ts', + teardown: 'cleanup', + }, + { + name: 'cleanup', + testMatch: 'tests/extension.teardown.ts', + }, { name: 'electron-vscode', use: { @@ -20,6 +28,7 @@ export default defineConfig({ viewport: { width: 1512, height: 944 }, video: 'retain-on-failure', }, + dependencies: ['setup'], }, ], }) diff --git a/vscode/extension/tests/global-setup.ts b/vscode/extension/tests/extension.setup.ts similarity index 96% rename from vscode/extension/tests/global-setup.ts rename to vscode/extension/tests/extension.setup.ts index df721b58f3..7be5f5f58c 100644 --- a/vscode/extension/tests/global-setup.ts +++ b/vscode/extension/tests/extension.setup.ts @@ -1,9 +1,10 @@ +import { test as setup } from '@playwright/test' import { execSync } from 'child_process' import path from 'path' import fs from 'fs-extra' import { createHash } from 'crypto' -async function globalSetup() { +setup('prepare extension', async () => { console.log('Setting up extension for Playwright tests...') const extensionDir = path.join(__dirname, '..') @@ -70,11 +71,9 @@ async function globalSetup() { // Clean up temporary user data directory await fs.remove(tempUserDataDir) } -} +}) async function hashFile(filePath: string): Promise { const fileBuffer = await fs.readFile(filePath) return createHash('sha256').update(fileBuffer).digest('hex') } - -export default globalSetup diff --git a/vscode/extension/tests/extension.teardown.ts b/vscode/extension/tests/extension.teardown.ts new file mode 100644 index 0000000000..587ce695a6 --- /dev/null +++ b/vscode/extension/tests/extension.teardown.ts @@ -0,0 +1,16 @@ +import { test as teardown } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' + +teardown('cleanup extension', async () => { + console.log('Cleaning up extension test setup...') + + const extensionDir = path.join(__dirname, '..') + const testSetupDir = path.join(extensionDir, '.test_setup') + + // Clean up test setup directory + if (fs.existsSync(testSetupDir)) { + await fs.remove(testSetupDir) + console.log('Test setup directory cleaned up') + } +}) From 0a41cc8bc11ae16944322ea8ac74c6a7f36d3154 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:19:29 +0100 Subject: [PATCH 0514/1056] chore(vscode): move to better exec async (#4884) --- vscode/extension/tests/utils.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index 655568b7d0..e765487b0d 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -1,7 +1,6 @@ import path from 'path' import { Page } from '@playwright/test' -import { exec } from 'child_process' -import { promisify } from 'util' +import { execAsync } from '../src/utilities/exec' // Where your extension lives on disk export const EXT_PATH = path.resolve(__dirname, '..') @@ -34,8 +33,6 @@ export const clickExplorerTab = async (page: Page): Promise => { } } -const execAsync = promisify(exec) - export interface PythonEnvironment { pythonPath: string pipPath: string @@ -49,8 +46,10 @@ export const createVirtualEnvironment = async ( venvDir: string, ): Promise => { const pythonCmd = process.platform === 'win32' ? 'python' : 'python3' - const { stderr } = await execAsync(`${pythonCmd} -m venv "${venvDir}"`) - if (stderr && !stderr.includes('WARNING')) { + const { stderr, exitCode } = await execAsync( + `${pythonCmd} -m venv "${venvDir}"`, + ) + if (exitCode !== 0) { throw new Error(`Failed to create venv: ${stderr}`) } // Get paths @@ -76,8 +75,8 @@ export const pipInstall = async ( ): Promise => { const { pipPath } = pythonDetails const execString = `"${pipPath}" install -e "${packagePaths.join('" -e "')}"` - const { stderr } = await execAsync(execString) - if (stderr && !stderr.includes('WARNING') && !stderr.includes('notice')) { + const { stderr, exitCode } = await execAsync(execString) + if (exitCode !== 0) { throw new Error(`Failed to install package: ${stderr}`) } } From b043f259cb4351d10a4deef49f988160a37c8561 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 3 Jul 2025 13:13:29 +0100 Subject: [PATCH 0515/1056] chore(vscode): delete unused code (#4885) --- .../src/components/graph/ModelColumns.tsx | 149 +----------------- .../react/src/components/graph/ModelNode.tsx | 5 +- .../graph/ModelNodeHeaderHandles.tsx | 2 +- 3 files changed, 6 insertions(+), 150 deletions(-) diff --git a/vscode/react/src/components/graph/ModelColumns.tsx b/vscode/react/src/components/graph/ModelColumns.tsx index 7046907026..40b2c49d0c 100644 --- a/vscode/react/src/components/graph/ModelColumns.tsx +++ b/vscode/react/src/components/graph/ModelColumns.tsx @@ -1,11 +1,4 @@ -import React, { - useEffect, - useMemo, - useState, - useCallback, - Fragment, - useRef, -} from 'react' +import React, { useEffect, useMemo, useCallback } from 'react' import { Handle, Position, useUpdateNodeInternals } from 'reactflow' import 'reactflow/dist/base.css' import { mergeLineageWithColumns, mergeConnections } from './help' @@ -20,18 +13,12 @@ import { } from '@/utils/index' import { EnumSide, type Side } from './types' import { NoSymbolIcon } from '@heroicons/react/24/solid' -import { - InformationCircleIcon, - ClockIcon, - ExclamationCircleIcon, -} from '@heroicons/react/24/outline' +import { ClockIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline' import clsx from 'clsx' import { type ColumnDescription, type ColumnLineageApiLineageModelNameColumnNameGet200, type LineageColumn, - type LineageColumnSource, - type LineageColumnExpression, } from '@/api/client' import Loading from '@/components/loading/Loading' import Spinner from '@/components/logo/Spinner' @@ -41,21 +28,19 @@ import { type ModelSQLMeshModel, } from '@/domain/sqlmesh-model' import { useLineageFlow } from './context' -import { Popover, Transition } from '@headlessui/react' import { useApiColumnLineage } from '@/api/index' import SourceList from '@/components/sourceList/SourceList' import type { Lineage } from '@/domain/lineage' import type { ModelName } from '@/domain/models' import type { Column } from '@/domain/column' -export default function ModelColumns({ +export function ModelColumns({ nodeId, columns, disabled, className, limit = 5, withHandles = false, - withSource = false, withDescription = true, maxHeight = '50vh', }: { @@ -65,7 +50,6 @@ export default function ModelColumns({ className?: string limit?: number withHandles?: boolean - withSource?: boolean withDescription?: boolean maxHeight?: string }): JSX.Element { @@ -224,17 +208,6 @@ export default function ModelColumns({ } withHandles={withHandles} withDescription={withDescription} - expression={ - withSource - ? getColumnFromLineage(lineage, nodeId, column.name) - ?.expression - : undefined - } - source={ - withSource - ? getColumnFromLineage(lineage, nodeId, column.name)?.source - : undefined - } isEmpty={ isNotNil(getColumnFromLineage(lineage, nodeId, column.name)) && Object.keys( @@ -273,17 +246,6 @@ export default function ModelColumns({ } withHandles={withHandles} withDescription={withDescription} - expression={ - withSource - ? getColumnFromLineage(lineage, nodeId, column.name) - ?.expression - : undefined - } - source={ - withSource - ? getColumnFromLineage(lineage, nodeId, column.name)?.source - : undefined - } isEmpty={ isNotNil(getColumnFromLineage(lineage, nodeId, column.name)) && Object.keys( @@ -330,17 +292,6 @@ export default function ModelColumns({ } withHandles={withHandles} withDescription={withDescription} - expression={ - withSource - ? getColumnFromLineage(lineage, nodeId, item.name) - ?.expression - : undefined - } - source={ - withSource - ? getColumnFromLineage(lineage, nodeId, item.name)?.source - : undefined - } isEmpty={ isNotNil(getColumnFromLineage(lineage, nodeId, item.name)) && Object.keys( @@ -373,8 +324,6 @@ function ModelColumn({ selectManually, withHandles = false, withDescription = true, - source, - expression, }: { id: string nodeId: string @@ -385,8 +334,6 @@ function ModelColumn({ hasRight?: boolean isEmpty?: boolean withHandles?: boolean - source?: LineageColumnSource - expression?: LineageColumnExpression withDescription?: boolean updateColumnLineage: ( lineage: ColumnLineageApiLineageModelNameColumnNameGet200, @@ -447,12 +394,6 @@ function ModelColumn({ disabled={disabled} className="px-2" > - {isNotNil(source) && ( - - )} (null) - // const { handleClickModel } = useLineageFlow() - - // const modelExtensions = useSQLMeshModelExtensions(undefined, model => { - // handleClickModel?.(model.name) - // }) - - const [isShowing, setIsShowing] = useState(false) - - useEffect(() => { - if (isShowing) { - scrollToExpression() - } - }, [isShowing, expression]) - - function scrollToExpression(): void { - // NOTE: This is a hack to scroll to the expression - // and should be replaced with a code mirror extension - setTimeout(() => { - const lines = Array.from( - elSourceContainer.current?.querySelector('[role="textbox"].cm-content') - ?.children ?? [], - ) - - for (const node of lines) { - if (node.textContent?.trim() === expression) { - node.scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'nearest', - }) - setTimeout(() => node.classList.add('sqlmesh-expression'), 500) - return - } - } - }, 300) - } - - return ( - setIsShowing(false)} - onMouseOver={() => setIsShowing(true)} - className="flex" - > - - - e.stopPropagation()} - > - {/* */} - - - - ) -} - function getColumnFromLineage( lineage: Record, nodeId: string, diff --git a/vscode/react/src/components/graph/ModelNode.tsx b/vscode/react/src/components/graph/ModelNode.tsx index dda072ada0..5b974ca76f 100644 --- a/vscode/react/src/components/graph/ModelNode.tsx +++ b/vscode/react/src/components/graph/ModelNode.tsx @@ -5,8 +5,8 @@ import { ModelType, type Model } from '@/api/client' import { useLineageFlow } from './context' import { type GraphNodeData } from './help' import { Position, type NodeProps } from 'reactflow' -import ModelNodeHeaderHandles from './ModelNodeHeaderHandles' -import ModelColumns from './ModelColumns' +import { ModelNodeHeaderHandles } from './ModelNodeHeaderHandles' +import { ModelColumns } from './ModelColumns' import { fromAPIColumn, type Column } from '@/domain/column' export const EnumLineageNodeModelType = { @@ -229,7 +229,6 @@ export default function ModelNode({ columns={columns} disabled={shouldDisableColumns} withHandles={true} - withSource={true} withDescription={false} maxHeight="10rem" /> diff --git a/vscode/react/src/components/graph/ModelNodeHeaderHandles.tsx b/vscode/react/src/components/graph/ModelNodeHeaderHandles.tsx index 040cd1dba6..35b1428154 100644 --- a/vscode/react/src/components/graph/ModelNodeHeaderHandles.tsx +++ b/vscode/react/src/components/graph/ModelNodeHeaderHandles.tsx @@ -8,7 +8,7 @@ import { ArrowRightCircleIcon } from '@heroicons/react/24/solid' import clsx from 'clsx' import { type LineageNodeModelType } from './ModelNode' -export default function ModelNodeHeaderHandles({ +export function ModelNodeHeaderHandles({ id, className, hasLeft = false, From d0e867fb2ca7bca498ee0a2e87d257950e50d71c Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:29:08 +0100 Subject: [PATCH 0516/1056] chore(vscode): make buttons neater html (#4886) --- .../src/components/graph/SettingsControl.tsx | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/vscode/react/src/components/graph/SettingsControl.tsx b/vscode/react/src/components/graph/SettingsControl.tsx index 2793528e1d..3016a96ee7 100644 --- a/vscode/react/src/components/graph/SettingsControl.tsx +++ b/vscode/react/src/components/graph/SettingsControl.tsx @@ -1,4 +1,4 @@ -import { Menu } from '@headlessui/react' +import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react' import { CheckIcon } from '@heroicons/react/24/outline' import { CogIcon } from '@/components/graph/CogIcon' import clsx from 'clsx' @@ -17,7 +17,7 @@ export function SettingsControl({ as="div" className="relative" > - @@ -25,31 +25,26 @@ export function SettingsControl({ className="h-3 w-3" aria-hidden="true" /> - - - - {({ active }) => ( - + + + - + onClick={() => onWithColumnsChange(!showColumns)} + > + Show Columns + {showColumns && ( +
-#### Other OAuth Providers +#### Other OAuth Providers If you use Okta and other custom OpenID/OAuth2 providers you need to add us as an Application or Client (terms differ across providers). @@ -69,9 +69,9 @@ We will need the following information from you once you set us up: |---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| | Client ID | The random ID we use to communicate with their OAuth service | `` | | Client Secret | The random secret we use to authentication with their OAuth service | `` | -| Open ID Configuration URL | This is the URL we use to gather the rest of their OpenID Configuration. We can often find this on our own and don't need to request it from them, check with the onboarding engineer to make sure we know this. | +| Open ID Configuration URL | This is the URL we use to gather the rest of their OpenID Configuration. We can often find this on our own and don't need to request it from them, check with the onboarding engineer to make sure we know this. | -Once we have the above information, we can enable SSO on your account. You will then follow the login flow through your provider such as logging in through Okta. +Once we have the above information, we can enable SSO on your account. You will then follow the login flow through your provider such as logging in through Okta. ### SAML V2.0 @@ -107,34 +107,34 @@ a provider named `acme`: ### Okta Integration -The following instructions will walk you through configuring Okta as your identity provider. +The following instructions will walk you through configuring Okta as your identity provider. Log into your Okta account. Navigate to Application and create a new app. You will want to select SAML 2.0 ![okta_setup_1](./single_sign_on/okta_setup_1.png) -Next, name your app "Tobiko Cloud". You can add the app logo by downloading the image [here](https://avatars.githubusercontent.com/u/113925670?s=200&v=4). +Next, name your app "Tobiko Cloud". You can add the app logo by downloading the image [here](https://avatars.githubusercontent.com/u/113925670?s=200&v=4). ![okta_setup_2](./single_sign_on/okta_setup_2.png) #### SAML Configurations and Settings -1. We now need to fill in the SAML Settings. Please enter the following values: +1. We now need to fill in the SAML Settings. Please enter the following values: - - **Single sign-on URL**: `https://cloud.tobikodata.com/auth/saml/callback/acme` + - **Single sign-on URL**: `https://cloud.tobikodata.com/auth/saml/callback/acme` - **Audience URI (SP Entity ID)**: `https://cloud.tobikodata.com/auth/saml/metadata/acme` ![okta_setup_3](./single_sign_on/okta_setup_3.png) -2. Fill in the Attribute Statements section with email, firstName, and lastName: These are required to properly map to your users. +2. Fill in the Attribute Statements section with email, firstName, and lastName: These are required to properly map to your users. ![okta_setup_4](./single_sign_on/okta_setup_4.png) -3. Click next and now you are on the last step. Check off the box `Contact app vendor` and hit `Finish`. Now you're all set! +3. Click next and now you are on the last step. Check off the box `Contact app vendor` and hit `Finish`. Now you're all set! ![okta_setup_5](./single_sign_on/okta_setup_5.png) -Here is what you will see if you are accessing Tobiko Cloud via Okta. Click on the Tobiko Cloud icon to be redirected to the application. +Here is what you will see if you are accessing Tobiko Cloud via Okta. Click on the Tobiko Cloud icon to be redirected to the application. ![sso_okta](./single_sign_on/sso_okta.png) @@ -142,7 +142,7 @@ Here is what you will see if you are accessing Tobiko Cloud via Okta. Click on t ### Status -You can see what the status of your session is with the `status` command: +You can see what the status of your session is with the `status` command: ``` bash $ tcloud auth status @@ -192,11 +192,11 @@ Not currently authenticated ![tcloud_logout](./single_sign_on/tcloud_logout.png) -Otherwise, you will be logged out automatically when the SSO session expires (every 24 hours). +Otherwise, you will be logged out automatically when the SSO session expires (every 24 hours). ## OAuth Clients -Sometimes, you want to grant an external service access to your Tobiko Cloud project. For example, the external service could be the [CICD bot](../../integrations/github.md) or a [scheduler integration](./scheduler/airflow.md). +Sometimes, you want to grant an external service access to your Tobiko Cloud project. For example, the external service could be the [CICD bot](../../../integrations/github.md) or a [scheduler integration](../scheduler/airflow.md). These services take `Client ID` and `Client Secret` credentials. diff --git a/docs/cloud/features/single_sign_on/oauth_client_1.png b/docs/cloud/features/security/single_sign_on/oauth_client_1.png similarity index 100% rename from docs/cloud/features/single_sign_on/oauth_client_1.png rename to docs/cloud/features/security/single_sign_on/oauth_client_1.png diff --git a/docs/cloud/features/single_sign_on/oauth_client_2.png b/docs/cloud/features/security/single_sign_on/oauth_client_2.png similarity index 100% rename from docs/cloud/features/single_sign_on/oauth_client_2.png rename to docs/cloud/features/security/single_sign_on/oauth_client_2.png diff --git a/docs/cloud/features/single_sign_on/okta_setup_1.png b/docs/cloud/features/security/single_sign_on/okta_setup_1.png similarity index 100% rename from docs/cloud/features/single_sign_on/okta_setup_1.png rename to docs/cloud/features/security/single_sign_on/okta_setup_1.png diff --git a/docs/cloud/features/single_sign_on/okta_setup_2.png b/docs/cloud/features/security/single_sign_on/okta_setup_2.png similarity index 100% rename from docs/cloud/features/single_sign_on/okta_setup_2.png rename to docs/cloud/features/security/single_sign_on/okta_setup_2.png diff --git a/docs/cloud/features/single_sign_on/okta_setup_3.png b/docs/cloud/features/security/single_sign_on/okta_setup_3.png similarity index 100% rename from docs/cloud/features/single_sign_on/okta_setup_3.png rename to docs/cloud/features/security/single_sign_on/okta_setup_3.png diff --git a/docs/cloud/features/single_sign_on/okta_setup_4.png b/docs/cloud/features/security/single_sign_on/okta_setup_4.png similarity index 100% rename from docs/cloud/features/single_sign_on/okta_setup_4.png rename to docs/cloud/features/security/single_sign_on/okta_setup_4.png diff --git a/docs/cloud/features/single_sign_on/okta_setup_5.png b/docs/cloud/features/security/single_sign_on/okta_setup_5.png similarity index 100% rename from docs/cloud/features/single_sign_on/okta_setup_5.png rename to docs/cloud/features/security/single_sign_on/okta_setup_5.png diff --git a/docs/cloud/features/single_sign_on/sso_okta.png b/docs/cloud/features/security/single_sign_on/sso_okta.png similarity index 100% rename from docs/cloud/features/single_sign_on/sso_okta.png rename to docs/cloud/features/security/single_sign_on/sso_okta.png diff --git a/docs/cloud/features/single_sign_on/tcloud_auth.png b/docs/cloud/features/security/single_sign_on/tcloud_auth.png similarity index 100% rename from docs/cloud/features/single_sign_on/tcloud_auth.png rename to docs/cloud/features/security/single_sign_on/tcloud_auth.png diff --git a/docs/cloud/features/single_sign_on/tcloud_auth_browser_login.png b/docs/cloud/features/security/single_sign_on/tcloud_auth_browser_login.png similarity index 100% rename from docs/cloud/features/single_sign_on/tcloud_auth_browser_login.png rename to docs/cloud/features/security/single_sign_on/tcloud_auth_browser_login.png diff --git a/docs/cloud/features/single_sign_on/tcloud_auth_browser_success.png b/docs/cloud/features/security/single_sign_on/tcloud_auth_browser_success.png similarity index 100% rename from docs/cloud/features/single_sign_on/tcloud_auth_browser_success.png rename to docs/cloud/features/security/single_sign_on/tcloud_auth_browser_success.png diff --git a/docs/cloud/features/single_sign_on/tcloud_login.png b/docs/cloud/features/security/single_sign_on/tcloud_login.png similarity index 100% rename from docs/cloud/features/single_sign_on/tcloud_login.png rename to docs/cloud/features/security/single_sign_on/tcloud_login.png diff --git a/docs/cloud/features/single_sign_on/tcloud_logout.png b/docs/cloud/features/security/single_sign_on/tcloud_logout.png similarity index 100% rename from docs/cloud/features/single_sign_on/tcloud_logout.png rename to docs/cloud/features/security/single_sign_on/tcloud_logout.png diff --git a/mkdocs.yml b/mkdocs.yml index 6f20b9d844..9bcfeeafa3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -114,7 +114,6 @@ nav: - "Alerts & Notifications": cloud/features/alerts_notifications.md - cloud/features/debugger_view.md - cloud/features/data_catalog.md - - cloud/features/single_sign_on.md - Scheduler: - "Cloud": cloud/features/scheduler/scheduler.md - "Cloud Hybrid Deployments": @@ -124,8 +123,9 @@ nav: - cloud/features/scheduler/airflow.md - cloud/features/scheduler/dagster.md - Security: - - "Security Overview": cloud/features/security/security.md - - "Incident Reporting": cloud/features/incident_reporting.md + - cloud/features/security/security.md + - cloud/features/security/single_sign_on.md + - cloud/features/incident_reporting.md - cloud/features/xdb_diffing.md # - Observability: # - cloud/features/observability/overview.md From 73100f86c6c2087fb26306509bf1f3260e130a0a Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:27:07 +0100 Subject: [PATCH 0523/1056] chore(vscode): lint the tests folder (#4893) --- vscode/extension/package.json | 4 ++-- vscode/extension/tests/extension.setup.ts | 3 ++- vscode/extension/tests/lineage.spec.ts | 2 +- vscode/extension/tests/python_env.spec.ts | 16 +++++++++------- vscode/extension/tests/tcloud.spec.ts | 4 ++-- vscode/extension/tests/utils_code_server.ts | 4 ++-- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/vscode/extension/package.json b/vscode/extension/package.json index efa9d90240..e749c42435 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -117,8 +117,8 @@ }, "scripts": { "ci": "pnpm run lint && pnpm run compile && pnpm run test:unit", - "lint": "eslint src", - "lint:fix": "eslint src --fix", + "lint": "eslint src tests", + "lint:fix": "eslint src tests --fix", "test:unit": "vitest run", "code-server": "code-server", "test:e2e": "pnpm run vscode:package && playwright test", diff --git a/vscode/extension/tests/extension.setup.ts b/vscode/extension/tests/extension.setup.ts index 7be5f5f58c..7447e53704 100644 --- a/vscode/extension/tests/extension.setup.ts +++ b/vscode/extension/tests/extension.setup.ts @@ -3,6 +3,7 @@ import { execSync } from 'child_process' import path from 'path' import fs from 'fs-extra' import { createHash } from 'crypto' +import { tmpdir } from 'os' setup('prepare extension', async () => { console.log('Setting up extension for Playwright tests...') @@ -32,7 +33,7 @@ setup('prepare extension', async () => { // Create a temporary user data directory for the installation const tempUserDataDir = await fs.mkdtemp( - path.join(require('os').tmpdir(), 'vscode-test-install-user-data-'), + path.join(tmpdir(), 'vscode-test-install-user-data-'), ) try { diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index 0ff7a10140..6b2c3d3861 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, Page } from '@playwright/test' +import { test, Page } from '@playwright/test' import path from 'path' import fs from 'fs-extra' import os from 'os' diff --git a/vscode/extension/tests/python_env.spec.ts b/vscode/extension/tests/python_env.spec.ts index 95a726c25b..fe6b9024a1 100644 --- a/vscode/extension/tests/python_env.spec.ts +++ b/vscode/extension/tests/python_env.spec.ts @@ -38,7 +38,10 @@ async function runTest(page: Page, context: CodeServerContext): Promise { await openLineageView(page) } -async function setupEnvironment(): Promise<[string, PythonEnvironment]> { +async function setupEnvironment(): Promise<{ + tempDir: string + pythonDetails: PythonEnvironment +}> { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -61,15 +64,14 @@ async function setupEnvironment(): Promise<[string, PythonEnvironment]> { await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { spaces: 2, }) - - return [tempDir, pythonDetails] + return { tempDir, pythonDetails } } test.describe('python environment variable injection on sqlmesh_lsp', () => { test('normal setup - error ', async ({ page }, testInfo) => { testInfo.setTimeout(120_000) - const [tempDir, _] = await setupEnvironment() + const { tempDir } = await setupEnvironment() writeEnvironmentConfig(tempDir) const context = await startCodeServer({ @@ -87,7 +89,7 @@ test.describe('python environment variable injection on sqlmesh_lsp', () => { test('normal setup - set', async ({ page }, testInfo) => { testInfo.setTimeout(120_000) - const [tempDir, _] = await setupEnvironment() + const { tempDir } = await setupEnvironment() writeEnvironmentConfig(tempDir) const env_file = path.join(tempDir, '.env') fs.writeFileSync(env_file, 'TEST_VAR=test_value') @@ -131,7 +133,7 @@ test.describe('tcloud version', () => { test('normal setup - error ', async ({ page }, testInfo) => { testInfo.setTimeout(120_000) - const [tempDir, pythonDetails] = await setupEnvironment() + const { tempDir, pythonDetails } = await setupEnvironment() await setupTcloudProject(tempDir, pythonDetails) writeEnvironmentConfig(tempDir) const context = await startCodeServer({ @@ -148,7 +150,7 @@ test.describe('tcloud version', () => { test('normal setup - set', async ({ page }, testInfo) => { testInfo.setTimeout(120_000) - const [tempDir, pythonDetails] = await setupEnvironment() + const { tempDir, pythonDetails } = await setupEnvironment() await setupTcloudProject(tempDir, pythonDetails) writeEnvironmentConfig(tempDir) const env_file = path.join(tempDir, '.env') diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index b61f7900ce..5bebfee781 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -173,8 +173,8 @@ test('signed in and not installed shows installation window', async ({ .click() await page.waitForSelector('text=Installing enterprise python package') - expect( - await page.locator('text=Installing enterprise python package'), + await expect( + page.locator('text=Installing enterprise python package'), ).toHaveCount(2) await page.waitForSelector('text=Loaded SQLMesh context') diff --git a/vscode/extension/tests/utils_code_server.ts b/vscode/extension/tests/utils_code_server.ts index 725a36a06b..db6c426912 100644 --- a/vscode/extension/tests/utils_code_server.ts +++ b/vscode/extension/tests/utils_code_server.ts @@ -105,7 +105,7 @@ export async function startCodeServer({ reject(new Error('Code-server failed to start within timeout')) }, 30000) - codeServerProcess.stdout?.on('data', data => { + codeServerProcess.stdout?.on('data', (data: Buffer) => { output += data.toString() if (output.includes('HTTP server listening on')) { clearTimeout(timeout) @@ -113,7 +113,7 @@ export async function startCodeServer({ } }) - codeServerProcess.stderr?.on('data', data => { + codeServerProcess.stderr?.on('data', (data: Buffer) => { console.error('Code-server stderr:', data.toString()) }) From 4a8b836902c062dd361221d3ad547761883a5baa Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:41:39 +0100 Subject: [PATCH 0524/1056] chore(vscode): get rid of old way of testing e2e (#4891) --- pnpm-lock.yaml | 3 - vscode/extension/E2E_TESTING.md | 82 ------------------------ vscode/extension/package.json | 1 - vscode/extension/scripts/fetch-vscode.ts | 27 -------- 4 files changed, 113 deletions(-) delete mode 100644 vscode/extension/E2E_TESTING.md delete mode 100644 vscode/extension/scripts/fetch-vscode.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0918d6f40..cd0bd172c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,9 +75,6 @@ importers: ts-loader: specifier: ^9.5.2 version: 9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.5)) - tsx: - specifier: ^4.19.4 - version: 4.19.4 typescript: specifier: ^5.8.3 version: 5.8.3 diff --git a/vscode/extension/E2E_TESTING.md b/vscode/extension/E2E_TESTING.md deleted file mode 100644 index 9ca24bbc6d..0000000000 --- a/vscode/extension/E2E_TESTING.md +++ /dev/null @@ -1,82 +0,0 @@ -# E2E Testing with Playwright - -This directory contains end-to-end tests for the SQLMesh VS Code extension using Playwright. - -## Setup - -1. **Install dependencies:** - ```bash - pnpm install - ``` - -2. **Install Playwright browsers:** - ```bash - pnpm run playwright:install - ``` - -## Running Tests - -- **Run all E2E tests:** - ```bash - pnpm run test:e2e - ``` - -- **Run tests with UI (interactive):** - ```bash - pnpm run test:e2e:ui - ``` - -- **Run tests in headed mode (visible browser):** - ```bash - pnpm run test:e2e:headed - ``` - -## Test Structure - -- `scripts/fetch-vscode.ts` - Downloads and caches VS Code executable -- `playwright.config.ts` - Playwright configuration for Electron testing -- `tests/lineage.spec.ts` - E2E tests for lineage functionality - -## How It Works - -1. **VS Code as Electron app:** Playwright launches VS Code as an Electron application with the extension loaded in development mode -2. **Extension isolation:** Each test runs with a fresh user data directory (`/tmp/vscode-test`) -3. **Webview testing:** Tests can interact with webview content using frame locators -4. **Visual regression:** Screenshots are captured and compared for pixel-perfect testing - -## CI/CD - -- The `.vscode-test` directory should be cached in CI to avoid re-downloading VS Code -- Tests run in headless mode by default in CI environments -- Screenshots are stored as test artifacts for comparison - -## Adding New Tests - -Create new test files in the `tests/` directory following the pattern: - -```typescript -import { test, expect, _electron as electron } from '@playwright/test'; -import path from 'path'; -import fs from 'fs-extra'; - -const VS_CODE_EXE = fs.readJsonSync('.vscode-test/paths.json').executablePath; -const EXT_PATH = path.join(__dirname, '..'); - -test('my new test', async () => { - const electronApp = await electron.launch({ - executablePath: VS_CODE_EXE, - args: [ - `--extensionDevelopmentPath=${EXT_PATH}`, - '--disable-workspace-trust', - '--disable-telemetry', - '--user-data-dir=/tmp/vscode-test', - ], - }); - - const window = await electronApp.firstWindow(); - - // Your test logic here... - - await electronApp.close(); -}); -``` \ No newline at end of file diff --git a/vscode/extension/package.json b/vscode/extension/package.json index e749c42435..a6a4c7e806 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -153,7 +153,6 @@ "esbuild": "^0.25.5", "eslint": "^9.29.0", "ts-loader": "^9.5.2", - "tsx": "^4.19.4", "typescript": "^5.8.3", "typescript-eslint": "^8.34.0", "vitest": "^3.2.3" diff --git a/vscode/extension/scripts/fetch-vscode.ts b/vscode/extension/scripts/fetch-vscode.ts deleted file mode 100644 index cff9af6650..0000000000 --- a/vscode/extension/scripts/fetch-vscode.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - downloadAndUnzipVSCode, - resolveCliPathFromVSCodeExecutablePath, -} from '@vscode/test-electron' -import * as fs from 'fs-extra' -import * as path from 'path' -;(async () => { - const vscPath = await downloadAndUnzipVSCode('stable') // one-time-only - console.log('VS Code downloaded to:', vscPath) - - const cliPath = resolveCliPathFromVSCodeExecutablePath(vscPath) // optional - console.log('CLI path:', cliPath) - - // Save paths to a JSON file for Playwright to use - const pathsFile = path.join('.vscode-test', 'paths.json') - await fs.ensureDir(path.dirname(pathsFile)) - await fs.writeJson( - pathsFile, - { - executablePath: vscPath, - cliPath: cliPath, - }, - { spaces: 2 }, - ) - - console.log('Paths saved to:', pathsFile) -})() From 5ac6a11de681fe2ff8ac3e402239c4d08049f399 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:58:00 +0100 Subject: [PATCH 0525/1056] chore(vscode): update dependencies (#4890) --- .circleci/continue_config.yml | 2 +- package.json | 2 +- pnpm-lock.yaml | 2673 +++++++++++++++++---------------- vscode/extension/package.json | 16 +- vscode/react/package.json | 24 +- web/client/package.json | 34 +- 6 files changed, 1400 insertions(+), 1351 deletions(-) diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 979c748925..eab55143d5 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -172,7 +172,7 @@ jobs: ui_test: docker: - - image: mcr.microsoft.com/playwright:v1.52.0-jammy + - image: mcr.microsoft.com/playwright:v1.53.2-jammy resource_class: medium steps: - halt_unless_client diff --git a/package.json b/package.json index 82be647222..6d49a853ef 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,6 @@ "lint:fix": "pnpm run fmt && pnpm run -r lint:fix" }, "devDependencies": { - "prettier": "^3.5.3" + "prettier": "^3.6.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd0bd172c0..97ba5ca5b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: devDependencies: prettier: - specifier: ^3.5.3 - version: 3.5.3 + specifier: ^3.6.2 + version: 3.6.2 vscode/bus: devDependencies: @@ -36,15 +36,15 @@ importers: specifier: ^9.0.1 version: 9.0.1 zod: - specifier: ^3.25.55 - version: 3.25.55 + specifier: ^3.25.71 + version: 3.25.71 devDependencies: '@eslint/js': - specifier: ^9.28.0 - version: 9.28.0 + specifier: ^9.30.1 + version: 9.30.1 '@playwright/test': - specifier: ^1.52.0 - version: 1.52.0 + specifier: ^1.53.2 + version: 1.53.2 '@types/mocha': specifier: ^10.0.10 version: 10.0.10 @@ -55,8 +55,8 @@ importers: specifier: 1.96.0 version: 1.96.0 '@vitest/ui': - specifier: ^3.2.3 - version: 3.2.3(vitest@3.2.3) + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4) '@vscode/test-cli': specifier: ^0.0.10 version: 0.0.10 @@ -64,26 +64,32 @@ importers: specifier: ^2.5.2 version: 2.5.2 '@vscode/vsce': - specifier: ^3.5.0 - version: 3.5.0 + specifier: ^3.6.0 + version: 3.6.0 esbuild: specifier: ^0.25.5 version: 0.25.5 eslint: - specifier: ^9.29.0 - version: 9.29.0(jiti@2.4.2) + specifier: ^9.30.1 + version: 9.30.1(jiti@2.4.2) ts-loader: specifier: ^9.5.2 version: 9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.5)) +<<<<<<< HEAD +======= + tsx: + specifier: ^4.20.3 + version: 4.20.3 +>>>>>>> a6ed6cf88 (chore(vscode): update dependencies) typescript: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.34.0 - version: 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.35.1 + version: 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) vitest: - specifier: ^3.2.3 - version: 3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.3)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) vscode/react: dependencies: @@ -97,26 +103,26 @@ importers: specifier: ^2.2.5 version: 2.2.5(@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) '@tailwindcss/postcss': - specifier: ^4.1.8 - version: 4.1.8 + specifier: ^4.1.11 + version: 4.1.11 '@tailwindcss/vite': - specifier: ^4.1.8 - version: 4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + specifier: ^4.1.11 + version: 4.1.11(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@tanstack/react-query': - specifier: ^5.80.7 - version: 5.80.7(react@18.3.1) + specifier: ^5.81.5 + version: 5.81.5(react@18.3.1) '@tanstack/react-router': - specifier: ^1.120.16 - version: 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.124.0 + version: 1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-router-devtools': - specifier: ^1.120.16 - version: 1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.15)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3) + specifier: ^1.124.0 + version: 1.124.0(@tanstack/react-router@1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.124.0)(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.9 - version: 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + 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.120.16 - version: 1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.5)) + specifier: ^1.124.0 + version: 1.124.0(@tanstack/react-router@1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.5)) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -127,8 +133,8 @@ importers: specifier: ^0.8.2 version: 0.8.2 orval: - specifier: ^7.9.0 - version: 7.9.0(openapi-types@12.1.3) + specifier: ^7.10.0 + version: 7.10.0(openapi-types@12.1.3) react: specifier: ^18.3.1 version: 18.3.1 @@ -136,36 +142,36 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-router: - specifier: ^7.6.2 - version: 7.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.6.3 + version: 7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) reactflow: specifier: ^11.11.4 version: 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwindcss: - specifier: ^4.1.8 - version: 4.1.8 + specifier: ^4.1.11 + version: 4.1.11 vscode-uri: specifier: ^3.1.0 version: 3.1.0 devDependencies: '@chromatic-com/storybook': specifier: ^4.0.1 - version: 4.0.1(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3)) + version: 4.0.1(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) '@storybook/addon-a11y': specifier: ^9.0.15 - version: 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3)) + version: 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) '@storybook/addon-docs': specifier: ^9.0.15 - version: 9.0.15(@types/react@18.3.23)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3)) + version: 9.0.15(@types/react@18.3.23)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) '@storybook/addon-onboarding': specifier: ^9.0.15 - version: 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3)) + version: 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) '@storybook/addon-vitest': specifier: ^9.0.15 - version: 9.0.15(@vitest/browser@3.2.3)(@vitest/runner@3.2.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))(vitest@3.2.3) + version: 9.0.15(@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.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(vitest@3.2.4) '@storybook/react-vite': specifier: ^9.0.15 - version: 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.41.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + version: 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.44.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -179,14 +185,14 @@ importers: specifier: ^18.3.7 version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react': - specifier: ^4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + specifier: ^4.6.0 + version: 4.6.0(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/browser': specifier: 3.2.3 - version: 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.3) + version: 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) '@vitest/coverage-v8': specifier: 3.2.3 - version: 3.2.3(@vitest/browser@3.2.3)(vitest@3.2.3) + version: 3.2.3(@vitest/browser@3.2.3)(vitest@3.2.4) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -195,16 +201,16 @@ importers: version: 1.53.2 storybook: specifier: ^9.0.15 - version: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + version: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) typescript: specifier: ^5.8.3 version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) vitest: - specifier: ^3.2.3 - version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.3)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -224,8 +230,8 @@ importers: specifier: ^6.9.0 version: 6.9.0 '@codemirror/language': - specifier: ^6.11.1 - version: 6.11.1 + specifier: ^6.11.2 + version: 6.11.2 '@codemirror/legacy-modes': specifier: ^6.5.1 version: 6.5.1 @@ -233,8 +239,8 @@ importers: specifier: ^6.5.2 version: 6.5.2 '@codemirror/view': - specifier: ^6.37.1 - version: 6.37.1 + specifier: ^6.38.0 + version: 6.38.0 '@headlessui/react': specifier: ^2.2.4 version: 2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -254,20 +260,20 @@ importers: specifier: ^0.1.1 version: 0.1.1(tailwindcss@3.4.17) '@tanstack/react-query': - specifier: ^5.80.7 - version: 5.80.7(react@18.3.1) + specifier: ^5.81.5 + version: 5.81.5(react@18.3.1) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-virtual': - specifier: ^3.13.9 - version: 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^3.13.12 + version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@uiw/react-codemirror': - specifier: ^4.23.12 - version: 4.23.12(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^4.23.14 + version: 4.23.14(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.0)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -288,7 +294,7 @@ importers: version: 18.3.1 react-dnd: specifier: ^16.0.1 - version: 16.0.1(@types/node@22.15.30)(@types/react@18.3.23)(react@18.3.1) + version: 16.0.1(@types/node@24.0.10)(@types/react@18.3.23)(react@18.3.1) react-dnd-html5-backend: specifier: ^16.0.1 version: 16.0.1 @@ -299,8 +305,8 @@ importers: specifier: ^10.1.0 version: 10.1.0(@types/react@18.3.23)(react@18.3.1) react-router: - specifier: ^7.6.2 - version: 7.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.6.3 + version: 7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-split: specifier: ^2.0.14 version: 2.0.14(react@18.3.1) @@ -309,20 +315,20 @@ importers: version: 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) thememirror: specifier: ^2.0.1 - version: 2.0.1(@codemirror/language@6.11.1)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1) + version: 2.0.1(@codemirror/language@6.11.2)(@codemirror/state@6.5.2)(@codemirror/view@6.38.0) zustand: - specifier: ^5.0.5 - version: 5.0.5(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) + specifier: ^5.0.6 + version: 5.0.6(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) devDependencies: '@eslint/js': - specifier: ^9.28.0 - version: 9.28.0 + specifier: ^9.30.1 + version: 9.30.1 '@playwright/test': - specifier: ^1.52.0 - version: 1.52.0 + specifier: ^1.53.2 + version: 1.53.2 '@swc/core': - specifier: ^1.11.31 - version: 1.11.31(@swc/helpers@0.5.17) + specifier: ^1.12.9 + version: 1.12.9(@swc/helpers@0.5.17) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -342,26 +348,26 @@ importers: specifier: ^18.3.7 version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react-swc': - specifier: ^3.10.1 - version: 3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + specifier: ^3.10.2 + version: 3.10.2(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) ajv: specifier: ^8.17.1 version: 8.17.1 autoprefixer: specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.4) + version: 10.4.21(postcss@8.5.6) eslint: - specifier: ^9.29.0 - version: 9.29.0(jiti@2.4.2) + specifier: ^9.30.1 + version: 9.30.1(jiti@2.4.2) jsdom: specifier: ^26.1.0 version: 26.1.0 orval: - specifier: ^7.9.0 - version: 7.9.0(openapi-types@12.1.3) + specifier: ^7.10.0 + version: 7.10.0(openapi-types@12.1.3) postcss: - specifier: ^8.5.4 - version: 8.5.4 + specifier: ^8.5.6 + version: 8.5.6 tailwindcss: specifier: ^3.4.17 version: 3.4.17 @@ -369,21 +375,21 @@ importers: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.34.0 - version: 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.35.1 + version: 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + version: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + version: 3.5.2(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) vitest: - specifier: ^3.2.3 - version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.3)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: '@swc/core-linux-x64-gnu': - specifier: ^1.11.31 - version: 1.11.31 + specifier: ^1.12.9 + version: 1.12.9 packages: @@ -438,8 +444,8 @@ packages: resolution: {integrity: sha512-f7IxTD15Qdux30s2qFARH+JxgwxWLG2Rlr4oSkPGuLWm+1p5y1+C04XGLA0vmX6EtqfutmjvpNmAfgwVIS5hpw==} engines: {node: '>=18.0.0'} - '@azure/core-rest-pipeline@1.20.0': - resolution: {integrity: sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g==} + '@azure/core-rest-pipeline@1.21.0': + resolution: {integrity: sha512-a4MBwe/5WKbq9MIxikzgxLBbruC5qlkFYlBdI7Ev50Y7ib5Vo/Jvt5jnJo7NaWeJ908LCHL0S1Us4UMf1VoTfg==} engines: {node: '>=18.0.0'} '@azure/core-tracing@1.2.0': @@ -450,46 +456,64 @@ packages: resolution: {integrity: sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ==} engines: {node: '>=18.0.0'} - '@azure/identity@4.10.0': - resolution: {integrity: sha512-iT53Sre2NJK6wzMWnvpjNiR3md597LZ3uK/5kQD2TkrY9vqhrY5bt2KwELNjkOWQ9n8S/92knj/QEykTtjMNqQ==} - engines: {node: '>=18.0.0'} + '@azure/identity@4.10.2': + resolution: {integrity: sha512-Uth4vz0j+fkXCkbvutChUj03PDCokjbC6Wk9JT8hHEUtpy/EurNKAseb3+gO6Zi9VYBvwt61pgbzn1ovk942Qg==} + engines: {node: '>=20.0.0'} '@azure/logger@1.2.0': resolution: {integrity: sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA==} engines: {node: '>=18.0.0'} - '@azure/msal-browser@4.13.0': - resolution: {integrity: sha512-n2ySryLd+wHmm/0Y1mwFI4J9UXVCu2DeWKtoWNWLVcpvK2k0Ez1qIigKleUm2ZfTbfAXdue+V8htmFft0qgyGQ==} + '@azure/msal-browser@4.14.0': + resolution: {integrity: sha512-6VB06LypBS0Cf/dSUwRZse/eGnfAHwDof7GpCfoo3JjnruSN40jFBw+QXZd1ox5OLC6633EdWRRz+TGeHMEspg==} engines: {node: '>=0.8.0'} - '@azure/msal-common@15.7.0': - resolution: {integrity: sha512-m9M5hoFoxhe/HlXNVa4qBHekrX60CVPkWzsjhKQGuzw/OPOmurosKRPDIMn8fug/E1hHI5v33DvT1LVJfItjcg==} + '@azure/msal-common@15.8.0': + resolution: {integrity: sha512-gYqq9MsWT/KZh8iTG37DkGv+wgfllgImTMB++Z83qn75M5eZ0cMX5kSSXdJqHbFm1qxaYydv+2kiVyA9ksN9pA==} engines: {node: '>=0.8.0'} - '@azure/msal-node@3.6.0': - resolution: {integrity: sha512-MRZ38Ou6l9LiRkz/968mG0czfIvD1PxMZ/3Jyz5k00ZMnhNOwv+DIliEcy//laoWDobAAq+/cz97xefCcHPgjg==} + '@azure/msal-node@3.6.2': + resolution: {integrity: sha512-lfZtncCSmKvW31Bh3iUBkeTf+Myt85YsamMkGNZ0ayTO5MirOGBgTa3BgUth0kWFBQuhZIRfi5B95INZ+ppkjw==} engines: {node: '>=16'} '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.27.5': - resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==} + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} engines: {node: '>=6.9.0'} - '@babel/core@7.27.4': - resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.27.5': - resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} + '@babel/helper-create-class-features-plugin@7.27.1': + resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.27.1': resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} @@ -500,10 +524,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.27.1': resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -520,8 +558,8 @@ packages: resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} engines: {node: '>=6.9.0'} - '@babel/parser@7.27.5': - resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} engines: {node: '>=6.0.0'} hasBin: true @@ -537,6 +575,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-commonjs@7.27.1': + resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -549,6 +593,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.27.1': + resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.27.6': resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} @@ -557,12 +613,12 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.4': - resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} - '@babel/types@7.27.6': - resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} + '@babel/types@7.28.0': + resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -590,8 +646,8 @@ packages: '@codemirror/lang-sql@6.9.0': resolution: {integrity: sha512-xmtpWqKSgum1B1J3Ro6rf7nuPqf2+kJQg5SjrofCAcyCThOe0ihSktSoXfXuhQBnwx1QbmreBbLJM5Jru6zitg==} - '@codemirror/language@6.11.1': - resolution: {integrity: sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==} + '@codemirror/language@6.11.2': + resolution: {integrity: sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==} '@codemirror/legacy-modes@6.5.1': resolution: {integrity: sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==} @@ -608,11 +664,8 @@ packages: '@codemirror/theme-one-dark@6.1.2': resolution: {integrity: sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==} - '@codemirror/view@6.37.1': - resolution: {integrity: sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==} - - '@codemirror/view@6.37.2': - resolution: {integrity: sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==} + '@codemirror/view@6.38.0': + resolution: {integrity: sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg==} '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} @@ -802,53 +855,49 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.20.1': - resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.3': - resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.14.0': resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.15.0': - resolution: {integrity: sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==} + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.28.0': - resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.29.0': - resolution: {integrity: sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==} + '@eslint/js@9.30.1': + resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.2': - resolution: {integrity: sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==} + '@eslint/plugin-kit@0.3.3': + resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} - '@floating-ui/core@1.7.1': - resolution: {integrity: sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==} + '@floating-ui/core@1.7.2': + resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} - '@floating-ui/dom@1.7.1': - resolution: {integrity: sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==} + '@floating-ui/dom@1.7.2': + resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==} - '@floating-ui/react-dom@2.1.3': - resolution: {integrity: sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==} + '@floating-ui/react-dom@2.1.4': + resolution: {integrity: sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' @@ -859,11 +908,11 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.9': - resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@gerrit0/mini-shiki@3.5.0': - resolution: {integrity: sha512-RQ1YHbN0EsRMP62QB61jFxrgpb8VIUE+PhR8CjBarIat6b2UeYHBo2s+IL7Fny6F4FuV4S63ksSNWiImsUKR+A==} + '@gerrit0/mini-shiki@3.7.0': + resolution: {integrity: sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==} '@headlessui/react@2.2.4': resolution: {integrity: sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==} @@ -934,26 +983,21 @@ packages: typescript: optional: true - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + '@jridgewell/source-map@0.3.10': + resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} @@ -1017,42 +1061,42 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@orval/angular@7.9.0': - resolution: {integrity: sha512-GzgEdZxK/9wQMLN2bziTlPSD9bkRXwYf1PoUM+RTXj6MGw0aZVWNTMCnp3dFWp9VemThP0kK2geBFqhxC2Bgxg==} + '@orval/angular@7.10.0': + resolution: {integrity: sha512-M89GKo/PibxYXvOKp9+i6BLxhEW8YsO+evwuV2kMbDGNS3RiYDwzmMBcA9SVL7m8CumeZoxNEAXsupzq96ZAXA==} - '@orval/axios@7.9.0': - resolution: {integrity: sha512-e77WvQGfFTkkrJIH66v/DpKdZ1eQBlu4NxOt2gCxvBYFP2dxDR2ajsM7uXxdGxi0iqZIS92+opzhxLIo6TVyDQ==} + '@orval/axios@7.10.0': + resolution: {integrity: sha512-AB6BjEwyguIcH8olzOTFPvwUP8z63yP4Jfl3T2UoeFchK04KqWqxbUoxmDG9xVQ79uMs/uOrb0X+GFwdZ56gAg==} - '@orval/core@7.9.0': - resolution: {integrity: sha512-/Nn6/ARmpevAY7Vl9RRXY2WkJx/q0LIUEE2Eh15bGgzAQIYUcD9aFr9zM5hX2b3lR/fZ8721hFsq0vM9O5ZzXw==} + '@orval/core@7.10.0': + resolution: {integrity: sha512-Lm7HY4Kwzehe+2HNfi+Ov/IZ+m3nj3NskVGvOyJDAqaaHB7G/xydSCtgELG32ur4G+M/XmwChAjoP4TCNVh0VA==} - '@orval/fetch@7.9.0': - resolution: {integrity: sha512-gIw2a3jXd1If/NpewVq7C6XDfnG2RPMt4PKR/RtEBeDKasXkoJeS2DBvZp/TyC+lt9oMgketF3bmzo/st09uhA==} + '@orval/fetch@7.10.0': + resolution: {integrity: sha512-bWcXPmARcXhXRveBtUnkfPlkUcLEzfGaflAdqN4CtScS48LgNrXXtuyt2BV2wvEXAavCWIhnRyQvz2foTU4U8Q==} - '@orval/hono@7.9.0': - resolution: {integrity: sha512-80VoS5W4I0uUo7Y6sIxr0xGYNX3oIL8sKWru+mYIZdp2L4W1lVHBi4zkpk7u0u9Obv7vmAuPtozV5+QIV0zWBg==} + '@orval/hono@7.10.0': + resolution: {integrity: sha512-bOxTdZxx2BpGQf7fFuCeeUe//ZYDWc6Yz9WOhj3HrnsD06xTRKFWVBi/QZ29QcAPxqwunu/VWwbqoiHHuuX3bA==} - '@orval/mcp@7.9.0': - resolution: {integrity: sha512-zMtW4jUKXGiXyJUylVy58kCu/Jf1yF9wp3ul2Guy1vbjlhVeOO1ugCYQ1sYNYH10vN0ajTS0/2pXhTuCR7PxHw==} + '@orval/mcp@7.10.0': + resolution: {integrity: sha512-ztLXGOSxK7jFwPKAeYPR85BjKRh3KTClKEnM2MFmo2FHHojn72DPXRPCmy0Wbw5Ee+JOxK2kIpyx+HZi9XVxiA==} - '@orval/mock@7.9.0': - resolution: {integrity: sha512-Ixhb+I4VTIfUl0qxDq8LekBnXM2gpD4kS7OFVqX9rdx0ZwZl7y4xArnKSXk5qgDPjo4eOWSmVA3onuL2WCit/g==} + '@orval/mock@7.10.0': + resolution: {integrity: sha512-vkEWCaKEyMfWGJF5MtxVzl+blwc9vYzwdYxMoSdjA5yS2dNBrdNlt1aLtb4+aoI1jgBgpCg/OB7VtWaL5QYidA==} - '@orval/query@7.9.0': - resolution: {integrity: sha512-IPKP4l00dZw0AOt+7PPB82WNdmpPThsWlYvk4YmdLEZVWCuaUaJ9KyLTT2R+XqoksuuuKTJ/IK0KO7VcjMiHiA==} + '@orval/query@7.10.0': + resolution: {integrity: sha512-DBVg8RyKWSQKhr5Zfvxx5XICUdDUkG4MJKSd4BQCrRjUWgN6vwGunMEKyfnjpS5mFUSCkwWD/I3rTkjW6aysJA==} - '@orval/swr@7.9.0': - resolution: {integrity: sha512-f06MifzMPrnXYdgt2rLLnurJ0YlXexSMyVlXAHhaJENtSVM4zlJp69rA6OULLr1i1biNGTWHSonOizKOf+cNcw==} + '@orval/swr@7.10.0': + resolution: {integrity: sha512-ZdApomZQhJ5ZogjJgBK+haeCOP9gUaMaGKGjTVJr86jJaygDcKn54Ok1quiDUCbX42Eye+cgmQJeKeZvqnPohA==} - '@orval/zod@7.9.0': - resolution: {integrity: sha512-LkkofL+iSswsBVWCr3bPZ6un8065wB7x34DZVnF/gN3kBxy8A35I9X0Eld4EajI/1rbMmxzt7wHUYAf5NlJWHQ==} + '@orval/zod@7.10.0': + resolution: {integrity: sha512-AB/508IBMlVDBcGvlq+ASz7DvqU3nhoDnIeBCyjwNfQwhYzREU0qqiFBnH0XAW70c6SCMf9/bIcYbw8GAx/zxA==} '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.52.0': - resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==} + '@playwright/test@1.53.2': + resolution: {integrity: sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==} engines: {node: '>=18'} hasBin: true @@ -1363,14 +1407,14 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@react-aria/focus@3.20.4': - resolution: {integrity: sha512-E9M/kPYvF1fBZpkRXsKqMhvBVEyTY7vmkHeXLJo6tInKQOjYyYs0VeWlnGnxBjQIAH7J7ZKAORfTFQQHyhoueQ==} + '@react-aria/focus@3.20.5': + resolution: {integrity: sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-aria/interactions@3.25.2': - resolution: {integrity: sha512-BWyZXBT4P17b9C9HfOIT2glDFMH9nUCfQF7vZ5FEeXNBudH/8OcSbzyBUG4Dg3XPtkOem5LP59ocaizkl32Tvg==} + '@react-aria/interactions@3.25.3': + resolution: {integrity: sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -1445,8 +1489,11 @@ packages: react: '>=17' react-dom: '>=17' - '@rolldown/pluginutils@1.0.0-beta.9': - resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} + '@rolldown/pluginutils@1.0.0-beta.11': + resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==} + + '@rolldown/pluginutils@1.0.0-beta.19': + resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} '@rollup/pluginutils@5.2.0': resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} @@ -1457,162 +1504,162 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.41.1': - resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==} + '@rollup/rollup-android-arm-eabi@4.44.1': + resolution: {integrity: sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.41.1': - resolution: {integrity: sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==} + '@rollup/rollup-android-arm64@4.44.1': + resolution: {integrity: sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.41.1': - resolution: {integrity: sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==} + '@rollup/rollup-darwin-arm64@4.44.1': + resolution: {integrity: sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.41.1': - resolution: {integrity: sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==} + '@rollup/rollup-darwin-x64@4.44.1': + resolution: {integrity: sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.41.1': - resolution: {integrity: sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==} + '@rollup/rollup-freebsd-arm64@4.44.1': + resolution: {integrity: sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.41.1': - resolution: {integrity: sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==} + '@rollup/rollup-freebsd-x64@4.44.1': + resolution: {integrity: sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.41.1': - resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==} + '@rollup/rollup-linux-arm-gnueabihf@4.44.1': + resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.41.1': - resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==} + '@rollup/rollup-linux-arm-musleabihf@4.44.1': + resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.41.1': - resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==} + '@rollup/rollup-linux-arm64-gnu@4.44.1': + resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.41.1': - resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==} + '@rollup/rollup-linux-arm64-musl@4.44.1': + resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.41.1': - resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==} + '@rollup/rollup-linux-loongarch64-gnu@4.44.1': + resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': - resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==} + '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': + resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.41.1': - resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==} + '@rollup/rollup-linux-riscv64-gnu@4.44.1': + resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.41.1': - resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==} + '@rollup/rollup-linux-riscv64-musl@4.44.1': + resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.41.1': - resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==} + '@rollup/rollup-linux-s390x-gnu@4.44.1': + resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.41.1': - resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==} + '@rollup/rollup-linux-x64-gnu@4.44.1': + resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.41.1': - resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==} + '@rollup/rollup-linux-x64-musl@4.44.1': + resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.41.1': - resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==} + '@rollup/rollup-win32-arm64-msvc@4.44.1': + resolution: {integrity: sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.41.1': - resolution: {integrity: sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==} + '@rollup/rollup-win32-ia32-msvc@4.44.1': + resolution: {integrity: sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.41.1': - resolution: {integrity: sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==} + '@rollup/rollup-win32-x64-msvc@4.44.1': + resolution: {integrity: sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==} cpu: [x64] os: [win32] - '@secretlint/config-creator@9.3.4': - resolution: {integrity: sha512-GRMYfHJ+rewwB26CC3USVObqSQ/mDLXzXcUMJw/wJisPr3HDZmdsYlcsNnaAcGN+EZmvqSDkgSibQm1hyZpzbg==} - engines: {node: ^14.13.1 || >=16.0.0} + '@secretlint/config-creator@10.1.1': + resolution: {integrity: sha512-TJ42CHZqqnEe9ORvIXVVMqdu3KAtyZRxLspjFexo6XgrwJ6CoFHQYzIihilqRjo2sJh9HMrpnYSj/5hopofGrA==} + engines: {node: '>=20.0.0'} - '@secretlint/config-loader@9.3.4': - resolution: {integrity: sha512-sy+yWDWh4cbAbpQYLiO39DjwNGEK1EUhTqNamLLBo163BdJP10FIWhqpe8mtGQBSBXRtxr8Hg/gc3Xe4meIoww==} - engines: {node: ^14.13.1 || >=16.0.0} + '@secretlint/config-loader@10.1.1': + resolution: {integrity: sha512-jBClVFmS6Yu/zI5ejBCRF5a5ASYsE4gOjogjB+WsaHbQHtGvnyY7I26Qtdg4ihCc/VPKYQg0LdM75pLTXzwsjg==} + engines: {node: '>=20.0.0'} - '@secretlint/core@9.3.4': - resolution: {integrity: sha512-ErIVHI6CJd191qdNKuMkH3bZQo9mWJsrSg++bQx64o0WFuG5nPvkYrDK0p/lebf+iQuOnzvl5HrZU6GU9a6o+Q==} - engines: {node: ^14.13.1 || >=16.0.0} + '@secretlint/core@10.1.1': + resolution: {integrity: sha512-COLCxSoH/iVQdLeaZPVtBj0UWKOagO09SqYkCQgfFfZ+soGxKVK405dL317r4PnH9Pm8/s8xQC6OSY5rWTRObQ==} + engines: {node: '>=20.0.0'} - '@secretlint/formatter@9.3.4': - resolution: {integrity: sha512-ARpoBOKz6WP3ocLITCFkR1/Lj636ugpBknylhlpc45r5aLdvmyvWAJqodlw5zmUCfgD6JXeAMf3Hi60aAiuqWQ==} - engines: {node: ^14.13.1 || >=16.0.0} + '@secretlint/formatter@10.1.1': + resolution: {integrity: sha512-Gpd8gTPN121SJ0h/9e6nWlZU7PitfhXUiEzW7Kyswg6kNGs+bSqmgTgWFtbo1VQ4ygJYiveWPNT05RCImBexJw==} + engines: {node: '>=20.0.0'} - '@secretlint/node@9.3.4': - resolution: {integrity: sha512-S0u8i+CnPmyAKtuccgot9L5cmw6DqJc0F+b3hhVIALd8kkeLt3RIXOOej15tU7N0V1ISph90Gz92V72ovsprgQ==} - engines: {node: ^14.13.1 || >=16.0.0} + '@secretlint/node@10.1.1': + resolution: {integrity: sha512-AhN+IGqljVObm8a+B33b23FY79wihu5E61Nd3oYSoZV7SxUvMjpafqhLfpt4frNSY7Ghf/pirWu7JY7GMujFrA==} + engines: {node: '>=20.0.0'} - '@secretlint/profiler@9.3.4': - resolution: {integrity: sha512-99WmaHd4dClNIm5BFsG++E6frNIZ3qVwg6s804Ql/M19pDmtZOoVCl4/UuzWpwNniBqLIgn9rHQZ/iGlIW3wyw==} + '@secretlint/profiler@10.1.1': + resolution: {integrity: sha512-kReI+Wr7IQz0LbVwYByzlnPbx4BEF2oEWJBc4Oa45g24alCjHu+jD9h9mzkTJqYUgMnVYD3o7HfzeqxFrV+9XA==} - '@secretlint/resolver@9.3.4': - resolution: {integrity: sha512-L1lIrcjzqcspPzZttmOvMmOFDpJTYFyRBONg94TZBWrpv4x0w5G2SYR+K7EE1SbYQAiPxw1amoXT1YRP8cZF2A==} + '@secretlint/resolver@10.1.1': + resolution: {integrity: sha512-GdQzxnBtdBRjBULvZ8ERkaRqDp0njVwXrzBCav1pb0XshVk76C1cjeDqtTqM4RJ1Awo/g5U5MIWYztYv67v5Gg==} - '@secretlint/secretlint-formatter-sarif@9.3.4': - resolution: {integrity: sha512-IpAl5gzKwpTRqoivKOTJB89l6b7uvBwjSNKzJb3oIGD9Jg3vXcQunSntvLv5XGynYtdi1NhANfEpbhavlmMSyA==} + '@secretlint/secretlint-formatter-sarif@10.1.1': + resolution: {integrity: sha512-Dyq8nzy6domjSlZKX1E5PEzuWxeTqjQJWrlXBmVmOjwLBLfRZDlm5Vq+AduBmEk03KEIKIZi4cZQwsniuRPO9Q==} - '@secretlint/secretlint-rule-no-dotenv@9.3.4': - resolution: {integrity: sha512-lMSVwTrJiZ/zL9VIzpT7tMcb0ClI6u4cyJo2YKGSbuJErJG1zB4gQKtjIwCSt7px5JF6U+aFtpb9M8+s40WWCQ==} - engines: {node: ^14.13.1 || >=16.0.0} + '@secretlint/secretlint-rule-no-dotenv@10.1.1': + resolution: {integrity: sha512-a3/sOUUtEHuw1HCadtxUjViNeomiiohfJj+rwtHxJkCq4pjITS3HSYhQBXnNvkctQNljKIzFm7JUA/4QJ6I4sQ==} + engines: {node: '>=20.0.0'} - '@secretlint/secretlint-rule-preset-recommend@9.3.4': - resolution: {integrity: sha512-RvzrLNN2A0B2bYQgRSRjh2dkdaIDuhXjj4SO5bElK1iBtJNiD6VBTxSSY1P3hXYaBeva7MEF+q1PZ3cCL8XYOA==} - engines: {node: ^14.13.1 || >=16.0.0} + '@secretlint/secretlint-rule-preset-recommend@10.1.1': + resolution: {integrity: sha512-+GeISCXVgpnoeRZE4ZPsuO97+fm6z8Ge23LNq6LvR9ZJAq018maXVftkJhHj4hnvYB5URUAEerBBkPGNk5/Ong==} + engines: {node: '>=20.0.0'} - '@secretlint/source-creator@9.3.4': - resolution: {integrity: sha512-I9ZA1gm9HJNaAhZiQdInY9VM04VTAGDV4bappVbEJzMUDnK/LTbYqfQ88RPqgCGCqa6ee8c0/j5Bn7ypweouIw==} - engines: {node: ^14.13.1 || >=16.0.0} + '@secretlint/source-creator@10.1.1': + resolution: {integrity: sha512-IWjvHcE0bhC/x88a9M9jbZlFRZGUEbBzujxrs2KzI5IQ2BXTBRBRhRSjE/BEpWqDHILB22c3mfam8X+UjukphA==} + engines: {node: '>=20.0.0'} - '@secretlint/types@9.3.4': - resolution: {integrity: sha512-z9rdKHNeL4xa48+367RQJVw1d7/Js9HIQ+gTs/angzteM9osfgs59ad3iwVRhCGYbeUoUUDe2yxJG2ylYLaH3Q==} - engines: {node: ^14.13.1 || >=16.0.0} + '@secretlint/types@10.1.1': + resolution: {integrity: sha512-/JGAvVkurVHkargk3AC7UxRy+Ymc+52AVBO/fZA5pShuLW2dX4O/rKc4n8cyhQiOb/3ym5ACSlLQuQ8apPfxrQ==} + engines: {node: '>=20.0.0'} - '@shikijs/engine-oniguruma@3.6.0': - resolution: {integrity: sha512-nmOhIZ9yT3Grd+2plmW/d8+vZ2pcQmo/UnVwXMUXAKTXdi+LK0S08Ancrz5tQQPkxvjBalpMW2aKvwXfelauvA==} + '@shikijs/engine-oniguruma@3.7.0': + resolution: {integrity: sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==} - '@shikijs/langs@3.6.0': - resolution: {integrity: sha512-IdZkQJaLBu1LCYCwkr30hNuSDfllOT8RWYVZK1tD2J03DkiagYKRxj/pDSl8Didml3xxuyzUjgtioInwEQM/TA==} + '@shikijs/langs@3.7.0': + resolution: {integrity: sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==} - '@shikijs/themes@3.6.0': - resolution: {integrity: sha512-Fq2j4nWr1DF4drvmhqKq8x5vVQ27VncF8XZMBuHuQMZvUSS3NBgpqfwz/FoGe36+W6PvniZ1yDlg2d4kmYDU6w==} + '@shikijs/themes@3.7.0': + resolution: {integrity: sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==} - '@shikijs/types@3.6.0': - resolution: {integrity: sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==} + '@shikijs/types@3.7.0': + resolution: {integrity: sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1773,68 +1820,68 @@ packages: typescript: optional: true - '@swc/core-darwin-arm64@1.11.31': - resolution: {integrity: sha512-NTEaYOts0OGSbJZc0O74xsji+64JrF1stmBii6D5EevWEtrY4wlZhm8SiP/qPrOB+HqtAihxWIukWkP2aSdGSQ==} + '@swc/core-darwin-arm64@1.12.9': + resolution: {integrity: sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.11.31': - resolution: {integrity: sha512-THSGaSwT96JwXDwuXQ6yFBbn+xDMdyw7OmBpnweAWsh5DhZmQkALEm1DgdQO3+rrE99MkmzwAfclc0UmYro/OA==} + '@swc/core-darwin-x64@1.12.9': + resolution: {integrity: sha512-hv2kls7Ilkm2EpeJz+I9MCil7pGS3z55ZAgZfxklEuYsxpICycxeH+RNRv4EraggN44ms+FWCjtZFu0LGg2V3g==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.11.31': - resolution: {integrity: sha512-laKtQFnW7KHgE57Hx32os2SNAogcuIDxYE+3DYIOmDMqD7/1DCfJe6Rln2N9WcOw6HuDbDpyQavIwZNfSAa8vQ==} + '@swc/core-linux-arm-gnueabihf@1.12.9': + resolution: {integrity: sha512-od9tDPiG+wMU9wKtd6y3nYJdNqgDOyLdgRRcrj1/hrbHoUPOM8wZQZdwQYGarw63iLXGgsw7t5HAF9Yc51ilFA==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.11.31': - resolution: {integrity: sha512-T+vGw9aPE1YVyRxRr1n7NAdkbgzBzrXCCJ95xAZc/0+WUwmL77Z+js0J5v1KKTRxw4FvrslNCOXzMWrSLdwPSA==} + '@swc/core-linux-arm64-gnu@1.12.9': + resolution: {integrity: sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.11.31': - resolution: {integrity: sha512-Mztp5NZkyd5MrOAG+kl+QSn0lL4Uawd4CK4J7wm97Hs44N9DHGIG5nOz7Qve1KZo407Y25lTxi/PqzPKHo61zQ==} + '@swc/core-linux-arm64-musl@1.12.9': + resolution: {integrity: sha512-yghFZWKPVVGbUdqiD7ft23G0JX6YFGDJPz9YbLLAwGuKZ9th3/jlWoQDAw1Naci31LQhVC+oIji6ozihSuwB2A==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.11.31': - resolution: {integrity: sha512-DDVE0LZcXOWwOqFU1Xi7gdtiUg3FHA0vbGb3trjWCuI1ZtDZHEQYL4M3/2FjqKZtIwASrDvO96w91okZbXhvMg==} + '@swc/core-linux-x64-gnu@1.12.9': + resolution: {integrity: sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.11.31': - resolution: {integrity: sha512-mJA1MzPPRIfaBUHZi0xJQ4vwL09MNWDeFtxXb0r4Yzpf0v5Lue9ymumcBPmw/h6TKWms+Non4+TDquAsweuKSw==} + '@swc/core-linux-x64-musl@1.12.9': + resolution: {integrity: sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.11.31': - resolution: {integrity: sha512-RdtakUkNVAb/FFIMw3LnfNdlH1/ep6KgiPDRlmyUfd0WdIQ3OACmeBegEFNFTzi7gEuzy2Yxg4LWf4IUVk8/bg==} + '@swc/core-win32-arm64-msvc@1.12.9': + resolution: {integrity: sha512-zHOusMVbOH9ik5RtRrMiGzLpKwxrPXgXkBm3SbUCa65HAdjV33NZ0/R9Rv1uPESALtEl2tzMYLUxYA5ECFDFhA==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.11.31': - resolution: {integrity: sha512-hErXdCGsg7swWdG1fossuL8542I59xV+all751mYlBoZ8kOghLSKObGQTkBbuNvc0sUKWfWg1X0iBuIhAYar+w==} + '@swc/core-win32-ia32-msvc@1.12.9': + resolution: {integrity: sha512-aWZf0PqE0ot7tCuhAjRkDFf41AzzSQO0x2xRfTbnhpROp57BRJ/N5eee1VULO/UA2PIJRG7GKQky5bSGBYlFug==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.11.31': - resolution: {integrity: sha512-5t7SGjUBMMhF9b5j17ml/f/498kiBJNf4vZFNM421UGUEETdtjPN9jZIuQrowBkoFGJTCVL/ECM4YRtTH30u/A==} + '@swc/core-win32-x64-msvc@1.12.9': + resolution: {integrity: sha512-C25fYftXOras3P3anSUeXXIpxmEkdAcsIL9yrr0j1xepTZ/yKwpnQ6g3coj8UXdeJy4GTVlR6+Ow/QiBgZQNOg==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.11.31': - resolution: {integrity: sha512-mAby9aUnKRjMEA7v8cVZS9Ah4duoRBnX7X6r5qrhTxErx+68MoY1TPrVwj/66/SWN3Bl+jijqAqoB8Qx0QE34A==} + '@swc/core@1.12.9': + resolution: {integrity: sha512-O+LfT2JlVMsIMWG9x+rdxg8GzpzeGtCZQfXV7cKc1PjIKUkLFf1QJ7okuseA4f/9vncu37dQ2ZcRrPKy0Ndd5g==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1848,73 +1895,73 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@swc/types@0.1.22': - resolution: {integrity: sha512-D13mY/ZA4PPEFSy6acki9eBT/3WgjMoRqNcdpIvjaYLQ44Xk5BdaL7UkDxAh6Z9UOe7tCCp67BVmZCojYp9owg==} + '@swc/types@0.1.23': + resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} '@tailwindcss/container-queries@0.1.1': resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==} peerDependencies: tailwindcss: '>=3.2.0' - '@tailwindcss/node@4.1.8': - resolution: {integrity: sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==} + '@tailwindcss/node@4.1.11': + resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} - '@tailwindcss/oxide-android-arm64@4.1.8': - resolution: {integrity: sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==} + '@tailwindcss/oxide-android-arm64@4.1.11': + resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.8': - resolution: {integrity: sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==} + '@tailwindcss/oxide-darwin-arm64@4.1.11': + resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.8': - resolution: {integrity: sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==} + '@tailwindcss/oxide-darwin-x64@4.1.11': + resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.8': - resolution: {integrity: sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==} + '@tailwindcss/oxide-freebsd-x64@4.1.11': + resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8': - resolution: {integrity: sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.8': - resolution: {integrity: sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.8': - resolution: {integrity: sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.8': - resolution: {integrity: sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.8': - resolution: {integrity: sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==} + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.8': - resolution: {integrity: sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==} + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -1925,52 +1972,52 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.8': - resolution: {integrity: sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.8': - resolution: {integrity: sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.8': - resolution: {integrity: sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==} + '@tailwindcss/oxide@4.1.11': + resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} engines: {node: '>= 10'} - '@tailwindcss/postcss@4.1.8': - resolution: {integrity: sha512-vB/vlf7rIky+w94aWMw34bWW1ka6g6C3xIOdICKX2GC0VcLtL6fhlLiafF0DVIwa9V6EHz8kbWMkS2s2QvvNlw==} + '@tailwindcss/postcss@4.1.11': + resolution: {integrity: sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==} - '@tailwindcss/vite@4.1.8': - resolution: {integrity: sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A==} + '@tailwindcss/vite@4.1.11': + resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==} peerDependencies: - vite: ^5.2.0 || ^6 + vite: ^5.2.0 || ^6 || ^7 - '@tanstack/history@1.115.0': - resolution: {integrity: sha512-K7JJNrRVvyjAVnbXOH2XLRhFXDkeP54Kt2P4FR1Kl2KDGlIbkua5VqZQD2rot3qaDrpufyUa63nuLai1kOLTsQ==} + '@tanstack/history@1.121.34': + resolution: {integrity: sha512-YL8dGi5ZU+xvtav2boRlw4zrRghkY6hvdcmHhA0RGSJ/CBgzv+cbADW9eYJLx74XMZvIQ1pp6VMbrpXnnM5gHA==} engines: {node: '>=12'} - '@tanstack/query-core@5.80.7': - resolution: {integrity: sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==} + '@tanstack/query-core@5.81.5': + resolution: {integrity: sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==} - '@tanstack/react-query@5.80.7': - resolution: {integrity: sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==} + '@tanstack/react-query@5.81.5': + resolution: {integrity: sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.120.16': - resolution: {integrity: sha512-DWXmMLknVJJMGP2k5yeUWBDhJOHbV2jVfnZKxtGzA64xXhwDFgU9qpodcmYSq3+kHWsKrd7iX0wc7d27rGwGDA==} + '@tanstack/react-router-devtools@1.124.0': + resolution: {integrity: sha512-CpOUUvtOYfLQEQS/ikGL9FQgEgYzBOKq9/2LqqFDXhZZgCVW18rBvR3LZeejkYSHAWlRphG33sdXCYVRM02sZQ==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.120.16 + '@tanstack/react-router': ^1.124.0 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.120.16': - resolution: {integrity: sha512-bBZ+H9sBYcihsj1BkcxD/VtVxa5ZGmCEeYXlCAgWQ9fWc1kN+tA0/M2uvjLFuhsESDmv5U45TittBtHAwAgEAA==} + '@tanstack/react-router@1.124.0': + resolution: {integrity: sha512-jJxuLbPP/Cxirnft3CoiGWyH0aj94VTmLNcYauvjTGRNbUitK4udvGaHXVEP8bcifYvpko7ptsqqBlisaosugA==} engines: {node: '>=12'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -1989,21 +2036,21 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.9': - resolution: {integrity: sha512-SPWC8kwG/dWBf7Py7cfheAPOxuvIv4fFQ54PdmYbg7CpXfsKxkucak43Q0qKsxVthhUJQ1A7CIMAIplq4BjVwA==} + '@tanstack/react-virtual@3.13.12': + resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.120.15': - resolution: {integrity: sha512-soLj+mEuvSxAVFK/3b85IowkkvmSuQL6J0RSIyKJFGFgy0CmUzpcBGEO99+JNWvvvzHgIoY4F4KtLIN+rvFSFA==} + '@tanstack/router-core@1.124.0': + resolution: {integrity: sha512-mU2KA2v+ZFWC3NIjY2y+pPCx1sZDXPsUkzPjPPZxRgonE11nIu9MB89WuukqYuPbxoSWeodKNXsLe4KksGFCKA==} engines: {node: '>=12'} - '@tanstack/router-devtools-core@1.120.15': - resolution: {integrity: sha512-AT9obPHKpJqnHMbwshozSy6sApg5LchiAll3blpS3MMDybUCidYHrdhe9MZJLmlC99IQiEGmuZERP3VRcuPNHg==} + '@tanstack/router-devtools-core@1.124.0': + resolution: {integrity: sha512-F4xejY63XrQmQZ8q6IJmLHJGeow/5CzdCAWChYEHIFy9SWYiFMMvdGFpB8SReJuwld+eoHvvtp2qhUqNloqzRA==} engines: {node: '>=12'} peerDependencies: - '@tanstack/router-core': ^1.120.15 + '@tanstack/router-core': ^1.124.0 csstype: ^3.0.10 solid-js: '>=1.9.5' tiny-invariant: ^1.3.3 @@ -2011,21 +2058,16 @@ packages: csstype: optional: true - '@tanstack/router-generator@1.120.16': - resolution: {integrity: sha512-ekCcIPk76Nj17ZOpiRmyDhZNmE06tPSQvu19TWQi6797dDChDCozed7cQbdn8qkuo84SjBhl0r087GTUkksbDg==} + '@tanstack/router-generator@1.124.0': + resolution: {integrity: sha512-fatjfBvgLh7i2xcLKO3QaM5egHAhMy57B7DfE44sYx1D7/xxLOubSEjSnVSLE3dWBrstZ3aqyuYYhw7NuoXB7g==} engines: {node: '>=12'} - peerDependencies: - '@tanstack/react-router': ^1.120.16 - peerDependenciesMeta: - '@tanstack/react-router': - optional: true - '@tanstack/router-plugin@1.120.16': - resolution: {integrity: sha512-p++CuH8FHFToueAuxwyd+vkRm5JNhJoCl47qxZLHa91iZvUwF9i0AbGYVWKXObo0EhYdVIGn4xhisPcOq872Eg==} + '@tanstack/router-plugin@1.124.0': + resolution: {integrity: sha512-CqV3PCVoMrHw0HyTioIGHTTjaMRgfwbW4ax2Pule++smyetn+3KPLV6C3VWc0vdukZMQz13JvLORSSeM0B2cYQ==} engines: {node: '>=12'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.120.16 + '@tanstack/react-router': ^1.124.0 vite: '>=5.0.0 || >=6.0.0' vite-plugin-solid: ^2.11.2 webpack: '>=5.92.0' @@ -2041,8 +2083,8 @@ packages: webpack: optional: true - '@tanstack/router-utils@1.115.0': - resolution: {integrity: sha512-Dng4y+uLR9b5zPGg7dHReHOTHQa6x+G6nCoZshsDtWrYsrdCcJEtLyhwZ5wG8OyYS6dVr/Cn+E5Bd2b6BhJ89w==} + '@tanstack/router-utils@1.121.21': + resolution: {integrity: sha512-u7ubq1xPBtNiU7Fm+EOWlVWdgFLzuKOa1thhqdscVn8R4dNMUd1VoOjZ6AKmLw201VaUhFtlX+u0pjzI6szX7A==} engines: {node: '>=12'} '@tanstack/store@0.7.1': @@ -2052,11 +2094,11 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.9': - resolution: {integrity: sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==} + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} - '@tanstack/virtual-file-routes@1.115.0': - resolution: {integrity: sha512-XLUh1Py3AftcERrxkxC5Y5m5mfllRH3YR6YVlyjFgI2Tc2Ssy2NKmQFQIafoxfW459UJ8Dn81nWKETEIJifE4g==} + '@tanstack/virtual-file-routes@1.121.21': + resolution: {integrity: sha512-3nuYsTyaq6ZN7jRZ9z6Gj3GXZqBOqOT0yzd/WZ33ZFfv4yVNIvsa5Lw+M1j3sgyEAxKMqGu/FaNi7FCjr3yOdw==} engines: {node: '>=12'} '@testing-library/dom@10.4.0': @@ -2088,20 +2130,20 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@textlint/ast-node-types@14.7.2': - resolution: {integrity: sha512-3rZc9vD8y/DlcFe3Y/cyKRRVgBH4ElEUzVFYdRVDwoMSwV/cIyZgYzVG6ZuOItQt+cHSREuijuucZ4VqZynbtg==} + '@textlint/ast-node-types@14.8.4': + resolution: {integrity: sha512-+fI7miec/r9VeniFV9ppL4jRCmHNsTxieulTUf/4tvGII3db5hGriKHC4p/diq1SkQ9Sgs7kg6UyydxZtpTz1Q==} - '@textlint/linter-formatter@14.7.2': - resolution: {integrity: sha512-QZOqft5uK+o/UN8UcEF3cHgfbG1r3+OWqlJojyjGNkEBbBNPSyDfYlVxDjHqnOAwm7jBaeqVGlwvw/7PUFmsmw==} + '@textlint/linter-formatter@14.8.4': + resolution: {integrity: sha512-sZ0UfYRDBNHnfMVBqLqqYnqTB7Ec169ljlmo+SEHR1T+dHUPYy1/DZK4p7QREXlBSFL4cnkswETCbc9xRodm4Q==} - '@textlint/module-interop@14.7.2': - resolution: {integrity: sha512-rDQhFERa2+xMqhyrPFvAL9d5Tb4RpQGKQExwrezvtCTREh6Zsp/nKxtK0r6o0P9xn1+zq2sZHW9NZjpe7av3xw==} + '@textlint/module-interop@14.8.4': + resolution: {integrity: sha512-1LdPYLAVpa27NOt6EqvuFO99s4XLB0c19Hw9xKSG6xQ1K82nUEyuWhzTQKb3KJ5Qx7qj14JlXZLfnEuL6A16Bw==} - '@textlint/resolver@14.7.2': - resolution: {integrity: sha512-FCZa9XJx5KihK/4gxXLhS/KfOnBD6vD5UxAMtgrvbifn+JFrW9Kh17uZLCcuJDDJJCnZOHq8jdT7AU+rpmJZ+w==} + '@textlint/resolver@14.8.4': + resolution: {integrity: sha512-nMDOgDAVwNU9ommh+Db0U+MCMNDPbQ/1HBNjbnHwxZkCpcT6hsAJwBe38CW/DtWVUv8yeR4R40IYNPT84srNwA==} - '@textlint/types@14.7.2': - resolution: {integrity: sha512-VpsmtJf9+7cnIxmKtAVVGVzI6f2k09kBZnzjdTAO8JZ+HTmV46jeoVrotpSfQbWDpuQk2UFPfrsZL/LNf/99ew==} + '@textlint/types@14.8.4': + resolution: {integrity: sha512-9nyY8vVXlr8hHKxa6+37omJhXWCwovMQcgMteuldYd4dOxGm14AK2nXdkgtKEUQnzLGaXy46xwLCfhQy7V7/YA==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -2241,9 +2283,6 @@ packages: '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2280,11 +2319,11 @@ packages: '@types/node@20.11.25': resolution: {integrity: sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==} - '@types/node@20.19.0': - resolution: {integrity: sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==} + '@types/node@20.19.4': + resolution: {integrity: sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA==} - '@types/node@22.15.30': - resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==} + '@types/node@24.0.10': + resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2292,8 +2331,8 @@ packages: '@types/pluralize@0.0.33': resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} - '@types/prop-types@15.7.14': - resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} '@types/react-dom@18.3.7': resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} @@ -2321,67 +2360,67 @@ packages: '@types/vscode@1.96.0': resolution: {integrity: sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==} - '@typescript-eslint/eslint-plugin@8.34.0': - resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==} + '@typescript-eslint/eslint-plugin@8.35.1': + resolution: {integrity: sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.34.0 + '@typescript-eslint/parser': ^8.35.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.34.0': - resolution: {integrity: sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==} + '@typescript-eslint/parser@8.35.1': + resolution: {integrity: sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/project-service@8.34.0': - resolution: {integrity: sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==} + '@typescript-eslint/project-service@8.35.1': + resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.34.0': - resolution: {integrity: sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==} + '@typescript-eslint/scope-manager@8.35.1': + resolution: {integrity: sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.34.0': - resolution: {integrity: sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==} + '@typescript-eslint/tsconfig-utils@8.35.1': + resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@8.34.0': - resolution: {integrity: sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==} + '@typescript-eslint/type-utils@8.35.1': + resolution: {integrity: sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.34.0': - resolution: {integrity: sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==} + '@typescript-eslint/types@8.35.1': + resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.34.0': - resolution: {integrity: sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==} + '@typescript-eslint/typescript-estree@8.35.1': + resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.34.0': - resolution: {integrity: sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==} + '@typescript-eslint/utils@8.35.1': + resolution: {integrity: sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.34.0': - resolution: {integrity: sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==} + '@typescript-eslint/visitor-keys@8.35.1': + resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typespec/ts-http-runtime@0.2.2': - resolution: {integrity: sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w==} + '@typespec/ts-http-runtime@0.2.3': + resolution: {integrity: sha512-oRhjSzcVjX8ExyaF8hC0zzTqxlVuRlgMHL/Bh4w3xB9+wjbm0FpXylVU/lBrn+kgphwYTrOk3tp+AVShGmlYCg==} engines: {node: '>=18.0.0'} '@uidotdev/usehooks@2.4.1': @@ -2391,8 +2430,8 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' - '@uiw/codemirror-extensions-basic-setup@4.23.12': - resolution: {integrity: sha512-l9vuiXOTFDBetYrRLDmz3jDxQHDsrVAZ2Y6dVfmrqi2AsulsDu+y7csW0JsvaMqo79rYkaIZg8yeqmDgMb7VyQ==} + '@uiw/codemirror-extensions-basic-setup@4.23.14': + resolution: {integrity: sha512-lCseubZqjN9bFwHJdQlZEKEo2yO1tCiMMVL0gu3ZXwhqMdfnd6ky/fUCYbn8aJkW+cXKVwjEVhpKjOphNiHoNw==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' '@codemirror/commands': '>=6.0.0' @@ -2402,8 +2441,8 @@ packages: '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' - '@uiw/react-codemirror@4.23.12': - resolution: {integrity: sha512-yseqWdzoAAGAW7i/NiU8YrfSLVOEBjQvSx1KpDTFVV/nn0AlAZoDVTIPEBgdXrPlVUQoCrwgpEaj3uZCklk9QA==} + '@uiw/react-codemirror@4.23.14': + resolution: {integrity: sha512-/CmlSh8LGUEZCxg/f78MEkEMehKnVklqJvJlL10AXXrO/2xOyPqHb8SK10GhwOqd0kHhHgVYp4+6oK5S+UIEuQ==} peerDependencies: '@babel/runtime': '>=7.11.0' '@codemirror/state': '>=6.0.0' @@ -2416,16 +2455,16 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitejs/plugin-react-swc@3.10.1': - resolution: {integrity: sha512-FmQvN3yZGyD9XW6IyxE86Kaa/DnxSsrDQX1xCR1qojNpBLaUop+nLYFvhCkJsq8zOupNjCRA9jyhPGOJsSkutA==} + '@vitejs/plugin-react-swc@3.10.2': + resolution: {integrity: sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==} peerDependencies: - vite: ^4 || ^5 || ^6 + vite: ^4 || ^5 || ^6 || ^7.0.0-beta.0 - '@vitejs/plugin-react@4.5.1': - resolution: {integrity: sha512-uPZBqSI0YD4lpkIru6M35sIfylLGTyhGHvDZbNLuMA73lMlwJKz5xweH7FajfcCAc2HnINciejA9qTz0dr0M7A==} + '@vitejs/plugin-react@4.6.0': + resolution: {integrity: sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 '@vitest/browser@3.2.3': resolution: {integrity: sha512-5HpUb0ixGF8JWSAjb/P1x/VPuTYUkL4pL0+YO6DJiuvQgqJN3PREaUEcXwfXjU4nBc37EahfpRbAwdE9pHs9lQ==} @@ -2451,9 +2490,6 @@ packages: '@vitest/browser': optional: true - '@vitest/expect@3.2.3': - resolution: {integrity: sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==} - '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -2468,17 +2504,28 @@ packages: vite: optional: true + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@3.2.3': resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==} '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/runner@3.2.3': - resolution: {integrity: sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==} + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} - '@vitest/snapshot@3.2.3': - resolution: {integrity: sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==} + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} '@vitest/spy@3.2.3': resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==} @@ -2486,10 +2533,10 @@ packages: '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/ui@3.2.3': - resolution: {integrity: sha512-9aR2tY/WT7GRHGEH/9sSIipJqeA21Eh3C6xmiOVmfyBCFmezUSUFLalpaSmRHlRzWCKQU10yz3AHhKuYcdnZGQ==} + '@vitest/ui@3.2.4': + resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} peerDependencies: - vitest: 3.2.3 + vitest: 3.2.4 '@vitest/utils@3.2.3': resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==} @@ -2510,56 +2557,56 @@ packages: resolution: {integrity: sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==} engines: {node: '>=16'} - '@vscode/vsce-sign-alpine-arm64@2.0.2': - resolution: {integrity: sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==} + '@vscode/vsce-sign-alpine-arm64@2.0.5': + resolution: {integrity: sha512-XVmnF40APwRPXSLYA28Ye+qWxB25KhSVpF2eZVtVOs6g7fkpOxsVnpRU1Bz2xG4ySI79IRuapDJoAQFkoOgfdQ==} cpu: [arm64] os: [alpine] - '@vscode/vsce-sign-alpine-x64@2.0.2': - resolution: {integrity: sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==} + '@vscode/vsce-sign-alpine-x64@2.0.5': + resolution: {integrity: sha512-JuxY3xcquRsOezKq6PEHwCgd1rh1GnhyH6urVEWUzWn1c1PC4EOoyffMD+zLZtFuZF5qR1I0+cqDRNKyPvpK7Q==} cpu: [x64] os: [alpine] - '@vscode/vsce-sign-darwin-arm64@2.0.2': - resolution: {integrity: sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==} + '@vscode/vsce-sign-darwin-arm64@2.0.5': + resolution: {integrity: sha512-z2Q62bk0ptADFz8a0vtPvnm6vxpyP3hIEYMU+i1AWz263Pj8Mc38cm/4sjzxu+LIsAfhe9HzvYNS49lV+KsatQ==} cpu: [arm64] os: [darwin] - '@vscode/vsce-sign-darwin-x64@2.0.2': - resolution: {integrity: sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==} + '@vscode/vsce-sign-darwin-x64@2.0.5': + resolution: {integrity: sha512-ma9JDC7FJ16SuPXlLKkvOD2qLsmW/cKfqK4zzM2iJE1PbckF3BlR08lYqHV89gmuoTpYB55+z8Y5Fz4wEJBVDA==} cpu: [x64] os: [darwin] - '@vscode/vsce-sign-linux-arm64@2.0.2': - resolution: {integrity: sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==} + '@vscode/vsce-sign-linux-arm64@2.0.5': + resolution: {integrity: sha512-Hr1o0veBymg9SmkCqYnfaiUnes5YK6k/lKFA5MhNmiEN5fNqxyPUCdRZMFs3Ajtx2OFW4q3KuYVRwGA7jdLo7Q==} cpu: [arm64] os: [linux] - '@vscode/vsce-sign-linux-arm@2.0.2': - resolution: {integrity: sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==} + '@vscode/vsce-sign-linux-arm@2.0.5': + resolution: {integrity: sha512-cdCwtLGmvC1QVrkIsyzv01+o9eR+wodMJUZ9Ak3owhcGxPRB53/WvrDHAFYA6i8Oy232nuen1YqWeEohqBuSzA==} cpu: [arm] os: [linux] - '@vscode/vsce-sign-linux-x64@2.0.2': - resolution: {integrity: sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==} + '@vscode/vsce-sign-linux-x64@2.0.5': + resolution: {integrity: sha512-XLT0gfGMcxk6CMRLDkgqEPTyG8Oa0OFe1tPv2RVbphSOjFWJwZgK3TYWx39i/7gqpDHlax0AP6cgMygNJrA6zg==} cpu: [x64] os: [linux] - '@vscode/vsce-sign-win32-arm64@2.0.2': - resolution: {integrity: sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==} + '@vscode/vsce-sign-win32-arm64@2.0.5': + resolution: {integrity: sha512-hco8eaoTcvtmuPhavyCZhrk5QIcLiyAUhEso87ApAWDllG7djIrWiOCtqn48k4pHz+L8oCQlE0nwNHfcYcxOPw==} cpu: [arm64] os: [win32] - '@vscode/vsce-sign-win32-x64@2.0.2': - resolution: {integrity: sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==} + '@vscode/vsce-sign-win32-x64@2.0.5': + resolution: {integrity: sha512-1ixKFGM2FwM+6kQS2ojfY3aAelICxjiCzeg4nTHpkeU1Tfs4RC+lVLrgq5NwcBC7ZLr6UfY3Ct3D6suPeOf7BQ==} cpu: [x64] os: [win32] - '@vscode/vsce-sign@2.0.5': - resolution: {integrity: sha512-GfYWrsT/vypTMDMgWDm75iDmAOMe7F71sZECJ+Ws6/xyIfmB3ELVnVN+LwMFAvmXY+e6eWhR2EzNGF/zAhWY3Q==} + '@vscode/vsce-sign@2.0.6': + resolution: {integrity: sha512-j9Ashk+uOWCDHYDxgGsqzKq5FXW9b9MW7QqOIYZ8IYpneJclWTBeHZz2DJCSKQgo+JAqNcaRRE1hzIx0dswqAw==} - '@vscode/vsce@3.5.0': - resolution: {integrity: sha512-2Eb6fBh8OzNhWqviCjeUPA1MW+d2GCb1QlVxrpOR8lrLHGk8x7HD4LbfELnZPyOz2X33Myz9FE9t4LwYbmeMRg==} + '@vscode/vsce@3.6.0': + resolution: {integrity: sha512-u2ZoMfymRNJb14aHNawnXJtXHLXDVKc1oKZaH4VELKT/9iWKRVgtQOdwxCgtwSxJoqYvuK4hGlBWQJ05wxADhg==} engines: {node: '>= 20'} hasBin: true @@ -2623,11 +2670,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -2637,10 +2679,6 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} - aggregate-error@3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} - ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -2701,8 +2739,8 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - ansis@3.17.0: - resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} + ansis@4.1.0: + resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} any-promise@1.3.0: @@ -2832,9 +2870,6 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -2845,8 +2880,8 @@ packages: browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - browserslist@4.25.0: - resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2902,8 +2937,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001721: - resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==} + caniuse-lite@1.0.30001726: + resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2947,8 +2982,8 @@ packages: cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - cheerio@1.0.0: - resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} + cheerio@1.1.0: + resolution: {integrity: sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==} engines: {node: '>=18.17'} chokidar@3.6.0: @@ -2985,10 +3020,6 @@ packages: classcat@5.0.5: resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} - clean-stack@2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -3062,6 +3093,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} @@ -3076,11 +3110,11 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} css.escape@1.5.1: @@ -3091,8 +3125,8 @@ packages: engines: {node: '>=4'} hasBin: true - cssstyle@4.3.1: - resolution: {integrity: sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} csstype@3.1.3: @@ -3168,8 +3202,8 @@ packages: decimal.js@10.5.0: resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} - decode-named-character-reference@1.1.0: - resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} @@ -3239,10 +3273,6 @@ packages: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} - diff@7.0.0: - resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} - engines: {node: '>=0.3.1'} - diff@8.0.2: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} @@ -3294,8 +3324,8 @@ packages: resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==} engines: {node: '>=4'} - electron-to-chromium@1.5.165: - resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==} + electron-to-chromium@1.5.179: + resolution: {integrity: sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==} elkjs@0.8.2: resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} @@ -3309,14 +3339,14 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encoding-sniffer@0.2.0: - resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + enhanced-resolve@5.18.2: + resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -3327,8 +3357,8 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@6.0.0: - resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} error-ex@1.3.2: @@ -3402,8 +3432,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.29.0: - resolution: {integrity: sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==} + eslint@9.30.1: + resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -3501,8 +3531,8 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.4.5: - resolution: {integrity: sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==} + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -3657,8 +3687,8 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true - glob@11.0.2: - resolution: {integrity: sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==} + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} hasBin: true @@ -3671,10 +3701,6 @@ packages: engines: {node: '>=12'} deprecated: Glob versions prior to v9 are no longer supported - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -3764,8 +3790,8 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} - htmlparser2@9.1.0: - resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + htmlparser2@10.0.0: + resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} @@ -4015,6 +4041,10 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isbot@5.1.28: + resolution: {integrity: sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -4335,9 +4365,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.3: - resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} - loupe@3.1.4: resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} @@ -4708,8 +4735,8 @@ packages: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} - orval@7.9.0: - resolution: {integrity: sha512-kFftcVojM4wRddRktqJPI/P9uYRpgiwCFOxF82G7XqDrczX9XDu8b5ialof+Z1LIuVZL4CvLV0Y184mRgrJrUA==} + orval@7.10.0: + resolution: {integrity: sha512-R1TlDDgK82dHfTXG0IuaIXHOrk6HQ1CuGejQQpQW9mBSCQA84AInp8U4Ovxw3upjMFNhghE8OlAQqD0ES8GgHQ==} hasBin: true own-keys@1.0.1: @@ -4732,9 +4759,9 @@ packages: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} + p-map@7.0.3: + resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==} + engines: {node: '>=18'} package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -4803,8 +4830,8 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} pend@1.2.0: @@ -4829,21 +4856,11 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - playwright-core@1.52.0: - resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==} - engines: {node: '>=18'} - hasBin: true - playwright-core@1.53.2: resolution: {integrity: sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==} engines: {node: '>=18'} hasBin: true - playwright@1.52.0: - resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==} - engines: {node: '>=18'} - hasBin: true - playwright@1.53.2: resolution: {integrity: sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==} engines: {node: '>=18'} @@ -4901,8 +4918,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.4: - resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} prebuild-install@7.1.3: @@ -4914,8 +4931,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} hasBin: true @@ -4936,8 +4953,8 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - pump@3.0.2: - resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} @@ -5032,8 +5049,8 @@ packages: '@types/react': optional: true - react-router@7.6.2: - resolution: {integrity: sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==} + react-router@7.6.3: + resolution: {integrity: sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -5149,8 +5166,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.41.1: - resolution: {integrity: sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==} + rollup@4.44.1: + resolution: {integrity: sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -5202,9 +5219,9 @@ packages: resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} engines: {node: '>= 10.13.0'} - secretlint@9.3.4: - resolution: {integrity: sha512-iNOzgMX/+W1SQNW/TW6eikGChyaPiazr2AEXjzjpoB0R6QJEulvlwhn0KLT1/xjPfdYrk3yiXZM40csUqET8uQ==} - engines: {node: ^14.13.1 || >=16.0.0} + secretlint@10.1.1: + resolution: {integrity: sha512-q50i+I9w6HH8P6o34LVq6M3hm5GZn2Eq5lYGHkEByOAbVqBHn8gsMGgyxjP1xSrSv1QjDtjxs/zKPm6JtkNzGw==} + engines: {node: '>=20.0.0'} hasBin: true semver@5.7.2: @@ -5472,11 +5489,11 @@ packages: style-mod@4.1.2: resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} - style-to-js@1.1.16: - resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} + style-to-js@1.1.17: + resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} - style-to-object@1.0.8: - resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + style-to-object@1.0.9: + resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} @@ -5526,8 +5543,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@4.1.8: - resolution: {integrity: sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==} + tailwindcss@4.1.11: + resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} tapable@2.2.2: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} @@ -5614,8 +5631,8 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} - tinypool@1.1.0: - resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} tinyrainbow@2.0.0: @@ -5702,8 +5719,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.19.4: - resolution: {integrity: sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==} + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -5749,21 +5766,21 @@ packages: typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} - typedoc-plugin-markdown@4.6.4: - resolution: {integrity: sha512-AnbToFS1T1H+n40QbO2+i0wE6L+55rWnj7zxnM1r781+2gmhMF2dB6dzFpaylWLQYkbg4D1Y13sYnne/6qZwdw==} + typedoc-plugin-markdown@4.7.0: + resolution: {integrity: sha512-PitbnAps2vpcqK2gargKoiFXLWFttvwUbyns/E6zGIFG5Gz8ZQJGttHnYR9csOlcSjB/uyjd8tnoayrtsXG17w==} engines: {node: '>= 18'} peerDependencies: typedoc: 0.28.x - typedoc@0.28.5: - resolution: {integrity: sha512-5PzUddaA9FbaarUzIsEc4wNXCiO4Ot3bJNeMF2qKpYlTmM9TTaSHQ7162w756ERCkXER/+o2purRG6YOAv6EMA==} + typedoc@0.28.7: + resolution: {integrity: sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x - typescript-eslint@8.34.0: - resolution: {integrity: sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==} + typescript-eslint@8.35.1: + resolution: {integrity: sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -5794,9 +5811,12 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@6.21.3: - resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} - engines: {node: '>=18.17'} + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + + undici@7.11.0: + resolution: {integrity: sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==} + engines: {node: '>=20.18.1'} unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} @@ -5908,8 +5928,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-node@3.2.3: - resolution: {integrity: sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true @@ -5958,16 +5978,16 @@ packages: yaml: optional: true - vitest@3.2.3: - resolution: {integrity: sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==} + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.3 - '@vitest/ui': 3.2.3 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -6108,8 +6128,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.2: - resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -6192,8 +6212,8 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - zod@3.25.55: - resolution: {integrity: sha512-219huNnkSLQnLsQ3uaRjXsxMrVm5C9W3OOpEVt2k5tvMKuA8nBSu38e0B//a+he9Iq2dvmk2VyYVlHqiHa4YBA==} + zod@3.25.71: + resolution: {integrity: sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q==} zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} @@ -6210,8 +6230,8 @@ packages: react: optional: true - zustand@5.0.5: - resolution: {integrity: sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==} + zustand@5.0.6: + resolution: {integrity: sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -6239,8 +6259,8 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 '@apidevtools/json-schema-ref-parser@11.7.2': dependencies: @@ -6297,7 +6317,7 @@ snapshots: dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.9.0 - '@azure/core-rest-pipeline': 1.20.0 + '@azure/core-rest-pipeline': 1.21.0 '@azure/core-tracing': 1.2.0 '@azure/core-util': 1.12.0 '@azure/logger': 1.2.0 @@ -6305,14 +6325,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/core-rest-pipeline@1.20.0': + '@azure/core-rest-pipeline@1.21.0': dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.9.0 '@azure/core-tracing': 1.2.0 '@azure/core-util': 1.12.0 '@azure/logger': 1.2.0 - '@typespec/ts-http-runtime': 0.2.2 + '@typespec/ts-http-runtime': 0.2.3 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -6324,22 +6344,22 @@ snapshots: '@azure/core-util@1.12.0': dependencies: '@azure/abort-controller': 2.1.2 - '@typespec/ts-http-runtime': 0.2.2 + '@typespec/ts-http-runtime': 0.2.3 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/identity@4.10.0': + '@azure/identity@4.10.2': dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.9.0 '@azure/core-client': 1.9.4 - '@azure/core-rest-pipeline': 1.20.0 + '@azure/core-rest-pipeline': 1.21.0 '@azure/core-tracing': 1.2.0 '@azure/core-util': 1.12.0 '@azure/logger': 1.2.0 - '@azure/msal-browser': 4.13.0 - '@azure/msal-node': 3.6.0 + '@azure/msal-browser': 4.14.0 + '@azure/msal-node': 3.6.2 open: 10.1.2 tslib: 2.8.1 transitivePeerDependencies: @@ -6347,20 +6367,20 @@ snapshots: '@azure/logger@1.2.0': dependencies: - '@typespec/ts-http-runtime': 0.2.2 + '@typespec/ts-http-runtime': 0.2.3 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/msal-browser@4.13.0': + '@azure/msal-browser@4.14.0': dependencies: - '@azure/msal-common': 15.7.0 + '@azure/msal-common': 15.8.0 - '@azure/msal-common@15.7.0': {} + '@azure/msal-common@15.8.0': {} - '@azure/msal-node@3.6.0': + '@azure/msal-node@3.6.2': dependencies: - '@azure/msal-common': 15.7.0 + '@azure/msal-common': 15.8.0 jsonwebtoken: 9.0.2 uuid: 8.3.2 @@ -6370,20 +6390,20 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.27.5': {} + '@babel/compat-data@7.28.0': {} - '@babel/core@7.27.4': + '@babel/core@7.28.0': dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 + '@babel/generator': 7.28.0 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) '@babel/helpers': 7.27.6 - '@babel/parser': 7.27.5 + '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 convert-source-map: 2.0.0 debug: 4.4.1(supports-color@8.1.1) gensync: 1.0.0-beta.2 @@ -6392,40 +6412,86 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.27.5': + '@babel/generator@7.28.0': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.0 + '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.27.5 + '@babel/compat-data': 7.28.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.0 + browserslist: 4.25.1 lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@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 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.28.0 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.4 + '@babel/traverse': 7.28.0 transitivePeerDependencies: - supports-color + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.0 + '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.27.1': {} @@ -6435,53 +6501,83 @@ snapshots: '@babel/helpers@7.27.6': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.27.6 + '@babel/types': 7.28.0 - '@babel/parser@7.27.5': + '@babel/parser@7.28.0': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.28.0 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.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-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) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.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) + transitivePeerDependencies: + - supports-color + '@babel/runtime@7.27.6': {} '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 - '@babel/traverse@7.27.4': + '@babel/traverse@7.28.0': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 - '@babel/parser': 7.27.5 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/types': 7.27.6 + '@babel/types': 7.28.0 debug: 4.4.1(supports-color@8.1.1) - globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/types@7.27.6': + '@babel/types@7.28.0': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -6490,13 +6586,13 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@chromatic-com/storybook@4.0.1(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))': + '@chromatic-com/storybook@4.0.1(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 12.2.0 filesize: 10.1.6 jsonfile: 6.1.0 - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) strip-ansi: 7.1.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -6504,22 +6600,22 @@ snapshots: '@codemirror/autocomplete@6.18.6': dependencies: - '@codemirror/language': 6.11.1 + '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 + '@codemirror/view': 6.38.0 '@lezer/common': 1.2.3 '@codemirror/commands@6.8.1': dependencies: - '@codemirror/language': 6.11.1 + '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 + '@codemirror/view': 6.38.0 '@lezer/common': 1.2.3 '@codemirror/lang-python@6.2.1': dependencies: '@codemirror/autocomplete': 6.18.6 - '@codemirror/language': 6.11.1 + '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 '@lezer/common': 1.2.3 '@lezer/python': 1.1.18 @@ -6527,16 +6623,16 @@ snapshots: '@codemirror/lang-sql@6.9.0': dependencies: '@codemirror/autocomplete': 6.18.6 - '@codemirror/language': 6.11.1 + '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 - '@codemirror/language@6.11.1': + '@codemirror/language@6.11.2': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 + '@codemirror/view': 6.38.0 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 @@ -6544,18 +6640,18 @@ snapshots: '@codemirror/legacy-modes@6.5.1': dependencies: - '@codemirror/language': 6.11.1 + '@codemirror/language': 6.11.2 '@codemirror/lint@6.8.5': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.2 + '@codemirror/view': 6.38.0 crelt: 1.0.6 '@codemirror/search@6.5.10': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.2 + '@codemirror/view': 6.38.0 crelt: 1.0.6 '@codemirror/state@6.5.2': @@ -6564,19 +6660,12 @@ snapshots: '@codemirror/theme-one-dark@6.1.2': dependencies: - '@codemirror/language': 6.11.1 + '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.2 + '@codemirror/view': 6.38.0 '@lezer/highlight': 1.2.1 - '@codemirror/view@6.37.1': - dependencies: - '@codemirror/state': 6.5.2 - crelt: 1.0.6 - style-mod: 4.1.2 - w3c-keyname: 2.2.8 - - '@codemirror/view@6.37.2': + '@codemirror/view@6.38.0': dependencies: '@codemirror/state': 6.5.2 crelt: 1.0.6 @@ -6678,14 +6767,14 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.29.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@2.4.2))': dependencies: - eslint: 9.29.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.20.1': + '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 debug: 4.4.1(supports-color@8.1.1) @@ -6693,13 +6782,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.3': {} + '@eslint/config-helpers@0.3.0': {} '@eslint/core@0.14.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/core@0.15.0': + '@eslint/core@0.15.1': dependencies: '@types/json-schema': 7.0.15 @@ -6717,58 +6806,56 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.28.0': {} - - '@eslint/js@9.29.0': {} + '@eslint/js@9.30.1': {} '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.3.2': + '@eslint/plugin-kit@0.3.3': dependencies: - '@eslint/core': 0.15.0 + '@eslint/core': 0.15.1 levn: 0.4.1 '@exodus/schemasafe@1.3.0': {} - '@floating-ui/core@1.7.1': + '@floating-ui/core@1.7.2': dependencies: - '@floating-ui/utils': 0.2.9 + '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.7.1': + '@floating-ui/dom@1.7.2': dependencies: - '@floating-ui/core': 1.7.1 - '@floating-ui/utils': 0.2.9 + '@floating-ui/core': 1.7.2 + '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@floating-ui/react-dom@2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/dom': 1.7.1 + '@floating-ui/dom': 1.7.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@floating-ui/utils': 0.2.9 + '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.10 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tabbable: 6.2.0 - '@floating-ui/utils@0.2.9': {} + '@floating-ui/utils@0.2.10': {} - '@gerrit0/mini-shiki@3.5.0': + '@gerrit0/mini-shiki@3.7.0': dependencies: - '@shikijs/engine-oniguruma': 3.6.0 - '@shikijs/langs': 3.6.0 - '@shikijs/themes': 3.6.0 - '@shikijs/types': 3.6.0 + '@shikijs/engine-oniguruma': 3.7.0 + '@shikijs/langs': 3.7.0 + '@shikijs/themes': 3.7.0 + '@shikijs/types': 3.7.0 '@shikijs/vscode-textmate': 10.0.2 '@headlessui/react@2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/focus': 3.20.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/interactions': 3.25.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/react-virtual': 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/focus': 3.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/interactions': 3.25.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) use-sync-external-store: 1.5.0(react@18.3.1) @@ -6829,36 +6916,33 @@ 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@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.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@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: typescript: 5.8.3 - '@jridgewell/gen-mapping@0.3.8': + '@jridgewell/gen-mapping@0.3.12': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/source-map@0.3.6': + '@jridgewell/source-map@0.3.10': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.4': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.4 '@jsdevtools/ono@7.1.3': {} @@ -6916,27 +7000,27 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@orval/angular@7.9.0(openapi-types@12.1.3)': + '@orval/angular@7.10.0(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.9.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/axios@7.9.0(openapi-types@12.1.3)': + '@orval/axios@7.10.0(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.9.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/core@7.9.0(openapi-types@12.1.3)': + '@orval/core@7.10.0(openapi-types@12.1.3)': dependencies: '@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3) '@ibm-cloud/openapi-ruleset': 1.31.1 - acorn: 8.14.1 + acorn: 8.15.0 ajv: 8.17.1 chalk: 4.1.2 compare-versions: 6.1.1 @@ -6957,64 +7041,64 @@ snapshots: - openapi-types - supports-color - '@orval/fetch@7.9.0(openapi-types@12.1.3)': + '@orval/fetch@7.10.0(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.9.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/hono@7.9.0(openapi-types@12.1.3)': + '@orval/hono@7.10.0(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.9.0(openapi-types@12.1.3) - '@orval/zod': 7.9.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3) + '@orval/zod': 7.10.0(openapi-types@12.1.3) lodash.uniq: 4.5.0 transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/mcp@7.9.0(openapi-types@12.1.3)': + '@orval/mcp@7.10.0(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.9.0(openapi-types@12.1.3) - '@orval/zod': 7.9.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3) + '@orval/zod': 7.10.0(openapi-types@12.1.3) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/mock@7.9.0(openapi-types@12.1.3)': + '@orval/mock@7.10.0(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.9.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3) openapi3-ts: 4.2.2 transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/query@7.9.0(openapi-types@12.1.3)': + '@orval/query@7.10.0(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.9.0(openapi-types@12.1.3) - '@orval/fetch': 7.9.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) lodash.omitby: 4.6.0 transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/swr@7.9.0(openapi-types@12.1.3)': + '@orval/swr@7.10.0(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.9.0(openapi-types@12.1.3) - '@orval/fetch': 7.9.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) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/zod@7.9.0(openapi-types@12.1.3)': + '@orval/zod@7.10.0(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.9.0(openapi-types@12.1.3) + '@orval/core': 7.10.0(openapi-types@12.1.3) lodash.uniq: 4.5.0 transitivePeerDependencies: - encoding @@ -7024,9 +7108,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.52.0': + '@playwright/test@1.53.2': dependencies: - playwright: 1.52.0 + playwright: 1.53.2 '@polka/url@1.0.0-next.29': {} @@ -7152,7 +7236,7 @@ snapshots: '@radix-ui/react-popper@1.2.7(@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)': dependencies: - '@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-arrow': 1.1.7(@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) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) @@ -7315,9 +7399,9 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@react-aria/focus@3.20.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/focus@3.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@react-aria/interactions': 3.25.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/interactions': 3.25.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-aria/utils': 3.29.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-types/shared': 3.30.0(react@18.3.1) '@swc/helpers': 0.5.17 @@ -7325,7 +7409,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@react-aria/interactions@3.25.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/interactions@3.25.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@react-aria/ssr': 3.9.9(react@18.3.1) '@react-aria/utils': 3.29.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -7448,107 +7532,109 @@ snapshots: - '@types/react' - immer - '@rolldown/pluginutils@1.0.0-beta.9': {} + '@rolldown/pluginutils@1.0.0-beta.11': {} + + '@rolldown/pluginutils@1.0.0-beta.19': {} - '@rollup/pluginutils@5.2.0(rollup@4.41.1)': + '@rollup/pluginutils@5.2.0(rollup@4.44.1)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.41.1 + rollup: 4.44.1 - '@rollup/rollup-android-arm-eabi@4.41.1': + '@rollup/rollup-android-arm-eabi@4.44.1': optional: true - '@rollup/rollup-android-arm64@4.41.1': + '@rollup/rollup-android-arm64@4.44.1': optional: true - '@rollup/rollup-darwin-arm64@4.41.1': + '@rollup/rollup-darwin-arm64@4.44.1': optional: true - '@rollup/rollup-darwin-x64@4.41.1': + '@rollup/rollup-darwin-x64@4.44.1': optional: true - '@rollup/rollup-freebsd-arm64@4.41.1': + '@rollup/rollup-freebsd-arm64@4.44.1': optional: true - '@rollup/rollup-freebsd-x64@4.41.1': + '@rollup/rollup-freebsd-x64@4.44.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.41.1': + '@rollup/rollup-linux-arm-gnueabihf@4.44.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.41.1': + '@rollup/rollup-linux-arm-musleabihf@4.44.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.41.1': + '@rollup/rollup-linux-arm64-gnu@4.44.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.41.1': + '@rollup/rollup-linux-arm64-musl@4.44.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.41.1': + '@rollup/rollup-linux-loongarch64-gnu@4.44.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': + '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.41.1': + '@rollup/rollup-linux-riscv64-gnu@4.44.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.41.1': + '@rollup/rollup-linux-riscv64-musl@4.44.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.41.1': + '@rollup/rollup-linux-s390x-gnu@4.44.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.41.1': + '@rollup/rollup-linux-x64-gnu@4.44.1': optional: true - '@rollup/rollup-linux-x64-musl@4.41.1': + '@rollup/rollup-linux-x64-musl@4.44.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.41.1': + '@rollup/rollup-win32-arm64-msvc@4.44.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.41.1': + '@rollup/rollup-win32-ia32-msvc@4.44.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.41.1': + '@rollup/rollup-win32-x64-msvc@4.44.1': optional: true - '@secretlint/config-creator@9.3.4': + '@secretlint/config-creator@10.1.1': dependencies: - '@secretlint/types': 9.3.4 + '@secretlint/types': 10.1.1 - '@secretlint/config-loader@9.3.4': + '@secretlint/config-loader@10.1.1': dependencies: - '@secretlint/profiler': 9.3.4 - '@secretlint/resolver': 9.3.4 - '@secretlint/types': 9.3.4 + '@secretlint/profiler': 10.1.1 + '@secretlint/resolver': 10.1.1 + '@secretlint/types': 10.1.1 ajv: 8.17.1 debug: 4.4.1(supports-color@8.1.1) rc-config-loader: 4.1.3 transitivePeerDependencies: - supports-color - '@secretlint/core@9.3.4': + '@secretlint/core@10.1.1': dependencies: - '@secretlint/profiler': 9.3.4 - '@secretlint/types': 9.3.4 + '@secretlint/profiler': 10.1.1 + '@secretlint/types': 10.1.1 debug: 4.4.1(supports-color@8.1.1) structured-source: 4.0.0 transitivePeerDependencies: - supports-color - '@secretlint/formatter@9.3.4': + '@secretlint/formatter@10.1.1': dependencies: - '@secretlint/resolver': 9.3.4 - '@secretlint/types': 9.3.4 - '@textlint/linter-formatter': 14.7.2 - '@textlint/module-interop': 14.7.2 - '@textlint/types': 14.7.2 + '@secretlint/resolver': 10.1.1 + '@secretlint/types': 10.1.1 + '@textlint/linter-formatter': 14.8.4 + '@textlint/module-interop': 14.8.4 + '@textlint/types': 14.8.4 chalk: 4.1.2 debug: 4.4.1(supports-color@8.1.1) pluralize: 8.0.0 @@ -7558,54 +7644,54 @@ snapshots: transitivePeerDependencies: - supports-color - '@secretlint/node@9.3.4': + '@secretlint/node@10.1.1': dependencies: - '@secretlint/config-loader': 9.3.4 - '@secretlint/core': 9.3.4 - '@secretlint/formatter': 9.3.4 - '@secretlint/profiler': 9.3.4 - '@secretlint/source-creator': 9.3.4 - '@secretlint/types': 9.3.4 + '@secretlint/config-loader': 10.1.1 + '@secretlint/core': 10.1.1 + '@secretlint/formatter': 10.1.1 + '@secretlint/profiler': 10.1.1 + '@secretlint/source-creator': 10.1.1 + '@secretlint/types': 10.1.1 debug: 4.4.1(supports-color@8.1.1) - p-map: 4.0.0 + p-map: 7.0.3 transitivePeerDependencies: - supports-color - '@secretlint/profiler@9.3.4': {} + '@secretlint/profiler@10.1.1': {} - '@secretlint/resolver@9.3.4': {} + '@secretlint/resolver@10.1.1': {} - '@secretlint/secretlint-formatter-sarif@9.3.4': + '@secretlint/secretlint-formatter-sarif@10.1.1': dependencies: node-sarif-builder: 2.0.3 - '@secretlint/secretlint-rule-no-dotenv@9.3.4': + '@secretlint/secretlint-rule-no-dotenv@10.1.1': dependencies: - '@secretlint/types': 9.3.4 + '@secretlint/types': 10.1.1 - '@secretlint/secretlint-rule-preset-recommend@9.3.4': {} + '@secretlint/secretlint-rule-preset-recommend@10.1.1': {} - '@secretlint/source-creator@9.3.4': + '@secretlint/source-creator@10.1.1': dependencies: - '@secretlint/types': 9.3.4 + '@secretlint/types': 10.1.1 istextorbinary: 9.5.0 - '@secretlint/types@9.3.4': {} + '@secretlint/types@10.1.1': {} - '@shikijs/engine-oniguruma@3.6.0': + '@shikijs/engine-oniguruma@3.7.0': dependencies: - '@shikijs/types': 3.6.0 + '@shikijs/types': 3.7.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.6.0': + '@shikijs/langs@3.7.0': dependencies: - '@shikijs/types': 3.6.0 + '@shikijs/types': 3.7.0 - '@shikijs/themes@3.6.0': + '@shikijs/themes@3.7.0': dependencies: - '@shikijs/types': 3.6.0 + '@shikijs/types': 3.7.0 - '@shikijs/types@3.6.0': + '@shikijs/types@3.7.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -7631,7 +7717,7 @@ snapshots: dependencies: '@stoplight/json': 3.21.7 '@stoplight/path': 1.3.2 - '@stoplight/types': 13.6.0 + '@stoplight/types': 13.20.0 '@types/urijs': 1.19.25 dependency-graph: 0.11.0 fast-memoize: 2.5.2 @@ -7777,54 +7863,54 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 - '@storybook/addon-a11y@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))': + '@storybook/addon-a11y@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.10.3 - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) - '@storybook/addon-docs@9.0.15(@types/react@18.3.23)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))': + '@storybook/addon-docs@9.0.15(@types/react@18.3.23)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.23)(react@18.3.1) - '@storybook/csf-plugin': 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3)) + '@storybook/csf-plugin': 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) '@storybook/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-dom-shim': 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3)) + '@storybook/react-dom-shim': 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-onboarding@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))': + '@storybook/addon-onboarding@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))': dependencies: - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) - '@storybook/addon-vitest@9.0.15(@vitest/browser@3.2.3)(@vitest/runner@3.2.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))(vitest@3.2.3)': + '@storybook/addon-vitest@9.0.15(@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.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(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.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.3) - '@vitest/runner': 3.2.3 - vitest: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.3)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + '@vitest/browser': 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/runner': 3.2.4 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - react - react-dom - '@storybook/builder-vite@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@storybook/builder-vite@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@storybook/csf-plugin': 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3)) - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + '@storybook/csf-plugin': 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) + storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) ts-dedent: 2.2.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) - '@storybook/csf-plugin@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))': + '@storybook/csf-plugin@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))': dependencies: - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -7834,87 +7920,87 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/react-dom-shim@9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))': + '@storybook/react-dom-shim@9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) - '@storybook/react-vite@9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.41.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@storybook/react-vite@9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.44.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) - '@rollup/pluginutils': 5.2.0(rollup@4.41.1) - '@storybook/builder-vite': 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) - '@storybook/react': 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))(typescript@5.8.3) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@rollup/pluginutils': 5.2.0(rollup@4.44.1) + '@storybook/builder-vite': 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@storybook/react': 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3) find-up: 7.0.0 magic-string: 0.30.17 react: 18.3.1 react-docgen: 8.0.0 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) tsconfig-paths: 4.2.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - rollup - supports-color - typescript - '@storybook/react@9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))(typescript@5.8.3)': + '@storybook/react@9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3)) + '@storybook/react-dom-shim': 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) optionalDependencies: typescript: 5.8.3 - '@swc/core-darwin-arm64@1.11.31': + '@swc/core-darwin-arm64@1.12.9': optional: true - '@swc/core-darwin-x64@1.11.31': + '@swc/core-darwin-x64@1.12.9': optional: true - '@swc/core-linux-arm-gnueabihf@1.11.31': + '@swc/core-linux-arm-gnueabihf@1.12.9': optional: true - '@swc/core-linux-arm64-gnu@1.11.31': + '@swc/core-linux-arm64-gnu@1.12.9': optional: true - '@swc/core-linux-arm64-musl@1.11.31': + '@swc/core-linux-arm64-musl@1.12.9': optional: true - '@swc/core-linux-x64-gnu@1.11.31': + '@swc/core-linux-x64-gnu@1.12.9': optional: true - '@swc/core-linux-x64-musl@1.11.31': + '@swc/core-linux-x64-musl@1.12.9': optional: true - '@swc/core-win32-arm64-msvc@1.11.31': + '@swc/core-win32-arm64-msvc@1.12.9': optional: true - '@swc/core-win32-ia32-msvc@1.11.31': + '@swc/core-win32-ia32-msvc@1.12.9': optional: true - '@swc/core-win32-x64-msvc@1.11.31': + '@swc/core-win32-x64-msvc@1.12.9': optional: true - '@swc/core@1.11.31(@swc/helpers@0.5.17)': + '@swc/core@1.12.9(@swc/helpers@0.5.17)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.22 + '@swc/types': 0.1.23 optionalDependencies: - '@swc/core-darwin-arm64': 1.11.31 - '@swc/core-darwin-x64': 1.11.31 - '@swc/core-linux-arm-gnueabihf': 1.11.31 - '@swc/core-linux-arm64-gnu': 1.11.31 - '@swc/core-linux-arm64-musl': 1.11.31 - '@swc/core-linux-x64-gnu': 1.11.31 - '@swc/core-linux-x64-musl': 1.11.31 - '@swc/core-win32-arm64-msvc': 1.11.31 - '@swc/core-win32-ia32-msvc': 1.11.31 - '@swc/core-win32-x64-msvc': 1.11.31 + '@swc/core-darwin-arm64': 1.12.9 + '@swc/core-darwin-x64': 1.12.9 + '@swc/core-linux-arm-gnueabihf': 1.12.9 + '@swc/core-linux-arm64-gnu': 1.12.9 + '@swc/core-linux-arm64-musl': 1.12.9 + '@swc/core-linux-x64-gnu': 1.12.9 + '@swc/core-linux-x64-musl': 1.12.9 + '@swc/core-win32-arm64-msvc': 1.12.9 + '@swc/core-win32-ia32-msvc': 1.12.9 + '@swc/core-win32-x64-msvc': 1.12.9 '@swc/helpers': 0.5.17 '@swc/counter@0.1.3': {} @@ -7923,7 +8009,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/types@0.1.22': + '@swc/types@0.1.23': dependencies: '@swc/counter': 0.1.3 @@ -7931,111 +8017,112 @@ snapshots: dependencies: tailwindcss: 3.4.17 - '@tailwindcss/node@4.1.8': + '@tailwindcss/node@4.1.11': dependencies: '@ampproject/remapping': 2.3.0 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.2 jiti: 2.4.2 lightningcss: 1.30.1 magic-string: 0.30.17 source-map-js: 1.2.1 - tailwindcss: 4.1.8 + tailwindcss: 4.1.11 - '@tailwindcss/oxide-android-arm64@4.1.8': + '@tailwindcss/oxide-android-arm64@4.1.11': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.8': + '@tailwindcss/oxide-darwin-arm64@4.1.11': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.8': + '@tailwindcss/oxide-darwin-x64@4.1.11': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.8': + '@tailwindcss/oxide-freebsd-x64@4.1.11': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.8': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.8': + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.8': + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.8': + '@tailwindcss/oxide-linux-x64-musl@4.1.11': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.8': + '@tailwindcss/oxide-wasm32-wasi@4.1.11': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.8': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.8': + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': optional: true - '@tailwindcss/oxide@4.1.8': + '@tailwindcss/oxide@4.1.11': dependencies: detect-libc: 2.0.4 tar: 7.4.3 optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.8 - '@tailwindcss/oxide-darwin-arm64': 4.1.8 - '@tailwindcss/oxide-darwin-x64': 4.1.8 - '@tailwindcss/oxide-freebsd-x64': 4.1.8 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.8 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.8 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.8 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.8 - '@tailwindcss/oxide-linux-x64-musl': 4.1.8 - '@tailwindcss/oxide-wasm32-wasi': 4.1.8 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.8 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.8 - - '@tailwindcss/postcss@4.1.8': + '@tailwindcss/oxide-android-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-x64': 4.1.11 + '@tailwindcss/oxide-freebsd-x64': 4.1.11 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-x64-musl': 4.1.11 + '@tailwindcss/oxide-wasm32-wasi': 4.1.11 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 + + '@tailwindcss/postcss@4.1.11': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.8 - '@tailwindcss/oxide': 4.1.8 - postcss: 8.5.4 - tailwindcss: 4.1.8 + '@tailwindcss/node': 4.1.11 + '@tailwindcss/oxide': 4.1.11 + postcss: 8.5.6 + tailwindcss: 4.1.11 - '@tailwindcss/vite@4.1.8(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@tailwindcss/node': 4.1.8 - '@tailwindcss/oxide': 4.1.8 - tailwindcss: 4.1.8 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + '@tailwindcss/node': 4.1.11 + '@tailwindcss/oxide': 4.1.11 + tailwindcss: 4.1.11 + vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) - '@tanstack/history@1.115.0': {} + '@tanstack/history@1.121.34': {} - '@tanstack/query-core@5.80.7': {} + '@tanstack/query-core@5.81.5': {} - '@tanstack/react-query@5.80.7(react@18.3.1)': + '@tanstack/react-query@5.81.5(react@18.3.1)': dependencies: - '@tanstack/query-core': 5.80.7 + '@tanstack/query-core': 5.81.5 react: 18.3.1 - '@tanstack/react-router-devtools@1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.120.15)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tiny-invariant@1.3.3)': + '@tanstack/react-router-devtools@1.124.0(@tanstack/react-router@1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.124.0)(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.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-devtools-core': 1.120.15(@tanstack/router-core@1.120.15)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3) + '@tanstack/react-router': 1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/router-devtools-core': 1.124.0(@tanstack/router-core@1.124.0)(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) - solid-js: 1.9.7 transitivePeerDependencies: - '@tanstack/router-core' - csstype + - solid-js - tiny-invariant - '@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-router@1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/history': 1.115.0 + '@tanstack/history': 1.121.34 '@tanstack/react-store': 0.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-core': 1.120.15 + '@tanstack/router-core': 1.124.0 + isbot: 5.1.28 jsesc: 3.1.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -8055,21 +8142,24 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@tanstack/react-virtual@3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/virtual-core': 3.13.9 + '@tanstack/virtual-core': 3.13.12 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@tanstack/router-core@1.120.15': + '@tanstack/router-core@1.124.0': dependencies: - '@tanstack/history': 1.115.0 + '@tanstack/history': 1.121.34 '@tanstack/store': 0.7.1 + cookie-es: 1.2.2 + jsesc: 3.1.0 tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.120.15(@tanstack/router-core@1.120.15)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': + '@tanstack/router-devtools-core@1.124.0(@tanstack/router-core@1.124.0)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': dependencies: - '@tanstack/router-core': 1.120.15 + '@tanstack/router-core': 1.124.0 clsx: 2.1.1 goober: 2.1.16(csstype@3.1.3) solid-js: 1.9.7 @@ -8077,55 +8167,60 @@ snapshots: optionalDependencies: csstype: 3.1.3 - '@tanstack/router-generator@1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@tanstack/router-generator@1.124.0': dependencies: - '@tanstack/virtual-file-routes': 1.115.0 - prettier: 3.5.3 - tsx: 4.19.4 - zod: 3.25.55 - optionalDependencies: - '@tanstack/react-router': 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/router-core': 1.124.0 + '@tanstack/router-utils': 1.121.21 + '@tanstack/virtual-file-routes': 1.121.21 + prettier: 3.6.2 + recast: 0.23.11 + source-map: 0.7.4 + tsx: 4.20.3 + zod: 3.25.71 + transitivePeerDependencies: + - supports-color - '@tanstack/router-plugin@1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.5))': + '@tanstack/router-plugin@1.124.0(@tanstack/react-router@1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.5))': dependencies: - '@babel/core': 7.27.4 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4) + '@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/template': 7.27.2 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 - '@tanstack/router-core': 1.120.15 - '@tanstack/router-generator': 1.120.16(@tanstack/react-router@1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) - '@tanstack/router-utils': 1.115.0 - '@tanstack/virtual-file-routes': 1.115.0 - '@types/babel__core': 7.20.5 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.7 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + '@tanstack/router-core': 1.124.0 + '@tanstack/router-generator': 1.124.0 + '@tanstack/router-utils': 1.121.21 + '@tanstack/virtual-file-routes': 1.121.21 babel-dead-code-elimination: 1.0.10 chokidar: 3.6.0 unplugin: 2.3.5 - zod: 3.25.55 + zod: 3.25.71 optionalDependencies: - '@tanstack/react-router': 1.120.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + '@tanstack/react-router': 1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) webpack: 5.99.8(esbuild@0.25.5) transitivePeerDependencies: - supports-color - '@tanstack/router-utils@1.115.0': + '@tanstack/router-utils@1.121.21': dependencies: - '@babel/generator': 7.27.5 - '@babel/parser': 7.27.5 - ansis: 3.17.0 - diff: 7.0.0 + '@babel/core': 7.28.0 + '@babel/generator': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) + ansis: 4.1.0 + diff: 8.0.2 + transitivePeerDependencies: + - supports-color '@tanstack/store@0.7.1': {} '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.9': {} + '@tanstack/virtual-core@3.13.12': {} - '@tanstack/virtual-file-routes@1.115.0': {} + '@tanstack/virtual-file-routes@1.121.21': {} '@testing-library/dom@10.4.0': dependencies: @@ -8162,15 +8257,15 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 - '@textlint/ast-node-types@14.7.2': {} + '@textlint/ast-node-types@14.8.4': {} - '@textlint/linter-formatter@14.7.2': + '@textlint/linter-formatter@14.8.4': dependencies: '@azu/format-text': 1.0.2 '@azu/style-format': 1.0.1 - '@textlint/module-interop': 14.7.2 - '@textlint/resolver': 14.7.2 - '@textlint/types': 14.7.2 + '@textlint/module-interop': 14.8.4 + '@textlint/resolver': 14.8.4 + '@textlint/types': 14.8.4 chalk: 4.1.2 debug: 4.4.1(supports-color@8.1.1) js-yaml: 3.14.1 @@ -8183,36 +8278,36 @@ snapshots: transitivePeerDependencies: - supports-color - '@textlint/module-interop@14.7.2': {} + '@textlint/module-interop@14.8.4': {} - '@textlint/resolver@14.7.2': {} + '@textlint/resolver@14.8.4': {} - '@textlint/types@14.7.2': + '@textlint/types@14.8.4': dependencies: - '@textlint/ast-node-types': 14.7.2 + '@textlint/ast-node-types': 14.8.4 '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.7 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.28.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 '@types/babel__traverse@7.20.7': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.28.0 '@types/chai@5.2.2': dependencies: @@ -8349,7 +8444,7 @@ snapshots: '@types/es-aggregate-error@1.0.6': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.10 '@types/eslint-scope@3.7.7': dependencies: @@ -8365,8 +8460,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - '@types/estree@1.0.7': {} - '@types/estree@1.0.8': {} '@types/fs-extra@11.0.4': @@ -8402,19 +8495,19 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.19.0': + '@types/node@20.19.4': dependencies: undici-types: 6.21.0 - '@types/node@22.15.30': + '@types/node@24.0.10': dependencies: - undici-types: 6.21.0 + undici-types: 7.8.0 '@types/normalize-package-data@2.4.4': {} '@types/pluralize@0.0.33': {} - '@types/prop-types@15.7.14': {} + '@types/prop-types@15.7.15': {} '@types/react-dom@18.3.7(@types/react@18.3.23)': dependencies: @@ -8422,7 +8515,7 @@ snapshots: '@types/react@18.3.23': dependencies: - '@types/prop-types': 15.7.14 + '@types/prop-types': 15.7.15 csstype: 3.1.3 '@types/resolve@1.20.6': {} @@ -8437,15 +8530,15 @@ snapshots: '@types/vscode@1.96.0': {} - '@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/type-utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.34.0 - eslint: 9.29.0(jiti@2.4.2) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 + eslint: 9.30.1(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -8454,55 +8547,55 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 debug: 4.4.1(supports-color@8.1.1) - eslint: 9.29.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.34.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) - '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 debug: 4.4.1(supports-color@8.1.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.34.0': + '@typescript-eslint/scope-manager@8.35.1': dependencies: - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 - '@typescript-eslint/tsconfig-utils@8.34.0(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.8.3)': dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.1(supports-color@8.1.1) - eslint: 9.29.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.34.0': {} + '@typescript-eslint/types@8.35.1': {} - '@typescript-eslint/typescript-estree@8.34.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.35.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.34.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/project-service': 8.35.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 @@ -8513,23 +8606,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - eslint: 9.29.0(jiti@2.4.2) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.34.0': + '@typescript-eslint/visitor-keys@8.35.1': dependencies: - '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/types': 8.35.1 eslint-visitor-keys: 4.2.1 - '@typespec/ts-http-runtime@0.2.2': + '@typespec/ts-http-runtime@0.2.3': dependencies: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -8542,24 +8635,24 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@uiw/codemirror-extensions-basic-setup@4.23.12(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1)': + '@uiw/codemirror-extensions-basic-setup@4.23.14(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.38.0)': dependencies: '@codemirror/autocomplete': 6.18.6 '@codemirror/commands': 6.8.1 - '@codemirror/language': 6.11.1 + '@codemirror/language': 6.11.2 '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 + '@codemirror/view': 6.38.0 - '@uiw/react-codemirror@4.23.12(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@uiw/react-codemirror@4.23.14(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.0)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.27.6 '@codemirror/commands': 6.8.1 '@codemirror/state': 6.5.2 '@codemirror/theme-one-dark': 6.1.2 - '@codemirror/view': 6.37.1 - '@uiw/codemirror-extensions-basic-setup': 4.23.12(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1) + '@codemirror/view': 6.38.0 + '@uiw/codemirror-extensions-basic-setup': 4.23.14(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.38.0) codemirror: 6.0.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -8571,57 +8664,37 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@3.10.1(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react-swc@3.10.2(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.9 - '@swc/core': 1.11.31(@swc/helpers@0.5.17) - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + '@rolldown/pluginutils': 1.0.0-beta.11 + '@swc/core': 1.12.9(@swc/helpers@0.5.17) + vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.5.1(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@vitejs/plugin-react@4.6.0(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@babel/core': 7.27.4 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.4) - '@rolldown/pluginutils': 1.0.0-beta.9 + '@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) + '@rolldown/pluginutils': 1.0.0-beta.19 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitest/browser@3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.3)': - dependencies: - '@testing-library/dom': 10.4.0 - '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) - '@vitest/utils': 3.2.3 - magic-string: 0.30.17 - sirv: 3.0.1 - tinyrainbow: 2.0.0 - vitest: 3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.3)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) - ws: 8.18.2 - optionalDependencies: - playwright: 1.53.2 - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - optional: true - - '@vitest/browser@3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.3)': + '@vitest/browser@3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/utils': 3.2.3 magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.3)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) - ws: 8.18.2 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + ws: 8.18.3 optionalDependencies: playwright: 1.53.2 transitivePeerDependencies: @@ -8630,7 +8703,7 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-v8@3.2.3(@vitest/browser@3.2.3)(vitest@3.2.3)': + '@vitest/coverage-v8@3.2.3(@vitest/browser@3.2.3)(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -8645,20 +8718,12 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.3)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: - '@vitest/browser': 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.3) + '@vitest/browser': 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) transitivePeerDependencies: - supports-color - '@vitest/expect@3.2.3': - dependencies: - '@types/chai': 5.2.2 - '@vitest/spy': 3.2.3 - '@vitest/utils': 3.2.3 - chai: 5.2.0 - tinyrainbow: 2.0.0 - '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -8667,21 +8732,29 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))': + '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 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.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) - '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@vitest/spy': 3.2.3 + '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.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.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) '@vitest/pretty-format@3.2.3': dependencies: @@ -8691,15 +8764,15 @@ snapshots: dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.2.3': + '@vitest/runner@3.2.4': dependencies: - '@vitest/utils': 3.2.3 + '@vitest/utils': 3.2.4 pathe: 2.0.3 strip-literal: 3.0.0 - '@vitest/snapshot@3.2.3': + '@vitest/snapshot@3.2.4': dependencies: - '@vitest/pretty-format': 3.2.3 + '@vitest/pretty-format': 3.2.4 magic-string: 0.30.17 pathe: 2.0.3 @@ -8711,21 +8784,21 @@ snapshots: dependencies: tinyspy: 4.0.3 - '@vitest/ui@3.2.3(vitest@3.2.3)': + '@vitest/ui@3.2.4(vitest@3.2.4)': dependencies: - '@vitest/utils': 3.2.3 + '@vitest/utils': 3.2.4 fflate: 0.8.2 flatted: 3.3.3 pathe: 2.0.3 sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.3)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) '@vitest/utils@3.2.3': dependencies: '@vitest/pretty-format': 3.2.3 - loupe: 3.1.3 + loupe: 3.1.4 tinyrainbow: 2.0.0 '@vitest/utils@3.2.4': @@ -8741,7 +8814,7 @@ snapshots: '@types/mocha': 10.0.10 c8: 9.1.0 chokidar: 3.6.0 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.2 glob: 10.4.5 minimatch: 9.0.5 mocha: 10.8.2 @@ -8758,60 +8831,60 @@ snapshots: transitivePeerDependencies: - supports-color - '@vscode/vsce-sign-alpine-arm64@2.0.2': + '@vscode/vsce-sign-alpine-arm64@2.0.5': optional: true - '@vscode/vsce-sign-alpine-x64@2.0.2': + '@vscode/vsce-sign-alpine-x64@2.0.5': optional: true - '@vscode/vsce-sign-darwin-arm64@2.0.2': + '@vscode/vsce-sign-darwin-arm64@2.0.5': optional: true - '@vscode/vsce-sign-darwin-x64@2.0.2': + '@vscode/vsce-sign-darwin-x64@2.0.5': optional: true - '@vscode/vsce-sign-linux-arm64@2.0.2': + '@vscode/vsce-sign-linux-arm64@2.0.5': optional: true - '@vscode/vsce-sign-linux-arm@2.0.2': + '@vscode/vsce-sign-linux-arm@2.0.5': optional: true - '@vscode/vsce-sign-linux-x64@2.0.2': + '@vscode/vsce-sign-linux-x64@2.0.5': optional: true - '@vscode/vsce-sign-win32-arm64@2.0.2': + '@vscode/vsce-sign-win32-arm64@2.0.5': optional: true - '@vscode/vsce-sign-win32-x64@2.0.2': + '@vscode/vsce-sign-win32-x64@2.0.5': optional: true - '@vscode/vsce-sign@2.0.5': + '@vscode/vsce-sign@2.0.6': optionalDependencies: - '@vscode/vsce-sign-alpine-arm64': 2.0.2 - '@vscode/vsce-sign-alpine-x64': 2.0.2 - '@vscode/vsce-sign-darwin-arm64': 2.0.2 - '@vscode/vsce-sign-darwin-x64': 2.0.2 - '@vscode/vsce-sign-linux-arm': 2.0.2 - '@vscode/vsce-sign-linux-arm64': 2.0.2 - '@vscode/vsce-sign-linux-x64': 2.0.2 - '@vscode/vsce-sign-win32-arm64': 2.0.2 - '@vscode/vsce-sign-win32-x64': 2.0.2 - - '@vscode/vsce@3.5.0': - dependencies: - '@azure/identity': 4.10.0 - '@secretlint/node': 9.3.4 - '@secretlint/secretlint-formatter-sarif': 9.3.4 - '@secretlint/secretlint-rule-no-dotenv': 9.3.4 - '@secretlint/secretlint-rule-preset-recommend': 9.3.4 - '@vscode/vsce-sign': 2.0.5 + '@vscode/vsce-sign-alpine-arm64': 2.0.5 + '@vscode/vsce-sign-alpine-x64': 2.0.5 + '@vscode/vsce-sign-darwin-arm64': 2.0.5 + '@vscode/vsce-sign-darwin-x64': 2.0.5 + '@vscode/vsce-sign-linux-arm': 2.0.5 + '@vscode/vsce-sign-linux-arm64': 2.0.5 + '@vscode/vsce-sign-linux-x64': 2.0.5 + '@vscode/vsce-sign-win32-arm64': 2.0.5 + '@vscode/vsce-sign-win32-x64': 2.0.5 + + '@vscode/vsce@3.6.0': + dependencies: + '@azure/identity': 4.10.2 + '@secretlint/node': 10.1.1 + '@secretlint/secretlint-formatter-sarif': 10.1.1 + '@secretlint/secretlint-rule-no-dotenv': 10.1.1 + '@secretlint/secretlint-rule-preset-recommend': 10.1.1 + '@vscode/vsce-sign': 2.0.6 azure-devops-node-api: 12.5.0 chalk: 4.1.2 - cheerio: 1.0.0 + cheerio: 1.1.0 cockatiel: 3.2.1 commander: 12.1.0 form-data: 4.0.3 - glob: 11.0.2 + glob: 11.0.3 hosted-git-info: 4.1.0 jsonc-parser: 3.3.1 leven: 3.1.0 @@ -8820,7 +8893,7 @@ snapshots: minimatch: 3.1.2 parse-semver: 1.1.1 read: 1.0.7 - secretlint: 9.3.4 + secretlint: 10.1.1 semver: 7.7.2 tmp: 0.2.3 typed-rest-client: 1.8.11 @@ -8921,17 +8994,10 @@ snapshots: dependencies: acorn: 8.15.0 - acorn@8.14.1: {} - acorn@8.15.0: {} agent-base@7.1.3: {} - aggregate-error@3.1.0: - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -8981,7 +9047,7 @@ snapshots: ansi-styles@6.2.1: {} - ansis@3.17.0: {} + ansis@4.1.0: {} any-promise@1.3.0: {} @@ -8995,7 +9061,7 @@ snapshots: '@swc/helpers': 0.5.17 '@types/command-line-args': 5.2.3 '@types/command-line-usage': 5.0.4 - '@types/node': 20.19.0 + '@types/node': 20.19.4 command-line-args: 6.0.1 command-line-usage: 7.0.3 flatbuffers: 24.12.23 @@ -9049,7 +9115,7 @@ snapshots: ast-v8-to-istanbul@0.3.3: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.29 estree-walker: 3.0.3 js-tokens: 9.0.1 @@ -9061,14 +9127,14 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.4.21(postcss@8.5.4): + autoprefixer@10.4.21(postcss@8.5.6): dependencies: - browserslist: 4.25.0 - caniuse-lite: 1.0.30001721 + browserslist: 4.25.1 + caniuse-lite: 1.0.30001726 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -9084,10 +9150,10 @@ snapshots: babel-dead-code-elimination@1.0.10: dependencies: - '@babel/core': 7.27.4 - '@babel/parser': 7.27.5 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/core': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 transitivePeerDependencies: - supports-color @@ -9124,10 +9190,6 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 - brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -9138,12 +9200,12 @@ snapshots: browser-stdout@1.3.1: {} - browserslist@4.25.0: + browserslist@4.25.1: dependencies: - caniuse-lite: 1.0.30001721 - electron-to-chromium: 1.5.165 + caniuse-lite: 1.0.30001726 + electron-to-chromium: 1.5.179 node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.0) + update-browserslist-db: 1.1.3(browserslist@4.25.1) buffer-crc32@0.2.13: {} @@ -9202,7 +9264,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001721: {} + caniuse-lite@1.0.30001726: {} ccount@2.0.1: {} @@ -9211,8 +9273,8 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.3 - pathval: 2.0.0 + loupe: 3.1.4 + pathval: 2.0.1 chalk-template@0.4.0: dependencies: @@ -9243,24 +9305,24 @@ snapshots: cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 - css-select: 5.1.0 - css-what: 6.1.0 + css-select: 5.2.2 + css-what: 6.2.2 domelementtype: 2.3.0 domhandler: 5.0.3 domutils: 3.2.2 - cheerio@1.0.0: + cheerio@1.1.0: dependencies: cheerio-select: 2.1.0 dom-serializer: 2.0.0 domhandler: 5.0.3 domutils: 3.2.2 - encoding-sniffer: 0.2.0 - htmlparser2: 9.1.0 + encoding-sniffer: 0.2.1 + htmlparser2: 10.0.0 parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 6.21.3 + undici: 7.11.0 whatwg-mimetype: 4.0.0 chokidar@3.6.0: @@ -9290,8 +9352,6 @@ snapshots: classcat@5.0.5: {} - clean-stack@2.2.0: {} - cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -9318,11 +9378,11 @@ snapshots: dependencies: '@codemirror/autocomplete': 6.18.6 '@codemirror/commands': 6.8.1 - '@codemirror/language': 6.11.1 + '@codemirror/language': 6.11.2 '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.2 + '@codemirror/view': 6.38.0 color-convert@2.0.1: dependencies: @@ -9362,6 +9422,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@1.2.2: {} + cookie@1.0.2: {} core-util-is@1.0.3: {} @@ -9374,21 +9436,21 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-select@5.1.0: + css-select@5.2.2: dependencies: boolbase: 1.0.0 - css-what: 6.1.0 + css-what: 6.2.2 domhandler: 5.0.3 domutils: 3.2.2 nth-check: 2.1.1 - css-what@6.1.0: {} + css-what@6.2.2: {} css.escape@1.5.1: {} cssesc@3.0.0: {} - cssstyle@4.3.1: + cssstyle@4.6.0: dependencies: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 @@ -9464,7 +9526,7 @@ snapshots: decimal.js@10.5.0: {} - decode-named-character-reference@1.1.0: + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -9521,8 +9583,6 @@ snapshots: diff@5.2.0: {} - diff@7.0.0: {} - diff@8.0.2: {} dir-glob@3.0.1: @@ -9579,7 +9639,7 @@ snapshots: dependencies: version-range: 4.14.0 - electron-to-chromium@1.5.165: {} + electron-to-chromium@1.5.179: {} elkjs@0.8.2: {} @@ -9589,17 +9649,17 @@ snapshots: emoji-regex@9.2.2: {} - encoding-sniffer@0.2.0: + encoding-sniffer@0.2.1: dependencies: iconv-lite: 0.6.3 whatwg-encoding: 3.1.1 - end-of-stream@1.4.4: + end-of-stream@1.4.5: dependencies: once: 1.4.0 optional: true - enhanced-resolve@5.18.1: + enhanced-resolve@5.18.2: dependencies: graceful-fs: 4.2.11 tapable: 2.2.2 @@ -9611,7 +9671,7 @@ snapshots: entities@4.5.0: {} - entities@6.0.0: {} + entities@6.0.1: {} error-ex@1.3.2: dependencies: @@ -9763,16 +9823,16 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.29.0(jiti@2.4.2): + eslint@9.30.1(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.1 - '@eslint/config-helpers': 0.2.3 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.29.0 - '@eslint/plugin-kit': 0.3.2 + '@eslint/js': 9.30.1 + '@eslint/plugin-kit': 0.3.3 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -9886,7 +9946,7 @@ snapshots: dependencies: pend: 1.2.0 - fdir@6.4.5(picomatch@4.0.2): + fdir@6.4.6(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -10041,7 +10101,7 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@11.0.2: + glob@11.0.3: dependencies: foreground-child: 3.3.1 jackspeak: 4.1.1 @@ -10067,8 +10127,6 @@ snapshots: minimatch: 5.1.6 once: 1.4.0 - globals@11.12.0: {} - globals@14.0.0: {} globalthis@1.0.4: @@ -10140,7 +10198,7 @@ snapshots: mdast-util-mdxjs-esm: 2.0.1 property-information: 7.1.0 space-separated-tokens: 2.0.2 - style-to-js: 1.1.16 + style-to-js: 1.1.17 unist-util-position: 5.0.0 vfile-message: 4.0.2 transitivePeerDependencies: @@ -10172,12 +10230,12 @@ snapshots: html-url-attributes@3.0.1: {} - htmlparser2@9.1.0: + htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 domutils: 3.2.2 - entities: 4.5.0 + entities: 6.0.1 http-proxy-agent@7.0.2: dependencies: @@ -10401,6 +10459,8 @@ snapshots: isarray@2.0.5: {} + isbot@5.1.28: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -10413,7 +10473,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.29 debug: 4.4.1(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: @@ -10465,7 +10525,7 @@ snapshots: jsdom@26.1.0: dependencies: - cssstyle: 4.3.1 + cssstyle: 4.6.0 data-urls: 5.0.0 decimal.js: 10.5.0 html-encoding-sniffer: 4.0.0 @@ -10483,7 +10543,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.2 + ws: 8.18.3 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -10703,8 +10763,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.3: {} - loupe@3.1.4: {} lru-cache@10.4.3: {} @@ -10725,12 +10783,12 @@ snapshots: magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.4 magicast@0.3.5: dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 source-map-js: 1.2.1 make-dir@4.0.0: @@ -10752,7 +10810,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 micromark: 4.0.2 @@ -10845,7 +10903,7 @@ snapshots: micromark-core-commonmark@2.0.3: dependencies: - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-factory-destination: 2.0.1 micromark-factory-label: 2.0.1 @@ -10920,7 +10978,7 @@ snapshots: micromark-util-decode-string@2.0.1: dependencies: - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 micromark-util-character: 2.1.1 micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-symbol: 2.0.1 @@ -10958,7 +11016,7 @@ snapshots: dependencies: '@types/debug': 4.1.12 debug: 4.4.1(supports-color@8.1.1) - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 micromark-factory-space: 2.0.1 @@ -11008,7 +11066,7 @@ snapshots: minimatch@5.1.6: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimatch@6.2.0: dependencies: @@ -11016,7 +11074,7 @@ snapshots: minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -11236,19 +11294,19 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.0 - orval@7.9.0(openapi-types@12.1.3): + orval@7.10.0(openapi-types@12.1.3): dependencies: '@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3) - '@orval/angular': 7.9.0(openapi-types@12.1.3) - '@orval/axios': 7.9.0(openapi-types@12.1.3) - '@orval/core': 7.9.0(openapi-types@12.1.3) - '@orval/fetch': 7.9.0(openapi-types@12.1.3) - '@orval/hono': 7.9.0(openapi-types@12.1.3) - '@orval/mcp': 7.9.0(openapi-types@12.1.3) - '@orval/mock': 7.9.0(openapi-types@12.1.3) - '@orval/query': 7.9.0(openapi-types@12.1.3) - '@orval/swr': 7.9.0(openapi-types@12.1.3) - '@orval/zod': 7.9.0(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) ajv: 8.17.1 cac: 6.7.14 chalk: 4.1.2 @@ -11261,8 +11319,8 @@ snapshots: openapi3-ts: 4.2.2 string-argv: 0.3.2 tsconfck: 2.1.2(typescript@5.8.3) - typedoc: 0.28.5(typescript@5.8.3) - typedoc-plugin-markdown: 4.6.4(typedoc@0.28.5(typescript@5.8.3)) + typedoc: 0.28.7(typescript@5.8.3) + typedoc-plugin-markdown: 4.7.0(typedoc@0.28.7(typescript@5.8.3)) typescript: 5.8.3 transitivePeerDependencies: - encoding @@ -11291,9 +11349,7 @@ snapshots: dependencies: p-limit: 4.0.0 - p-map@4.0.0: - dependencies: - aggregate-error: 3.1.0 + p-map@7.0.3: {} package-json-from-dist@1.0.1: {} @@ -11308,7 +11364,7 @@ snapshots: '@types/unist': 2.0.11 character-entities-legacy: 3.0.0 character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 is-alphanumerical: 2.0.1 is-decimal: 2.0.1 is-hexadecimal: 2.0.1 @@ -11336,7 +11392,7 @@ snapshots: parse5@7.3.0: dependencies: - entities: 6.0.0 + entities: 6.0.1 path-exists@4.0.0: {} @@ -11364,7 +11420,7 @@ snapshots: pathe@2.0.3: {} - pathval@2.0.0: {} + pathval@2.0.1: {} pend@1.2.0: {} @@ -11378,16 +11434,8 @@ snapshots: pirates@4.0.7: {} - playwright-core@1.52.0: {} - playwright-core@1.53.2: {} - playwright@1.52.0: - dependencies: - playwright-core: 1.52.0 - optionalDependencies: - fsevents: 2.3.2 - playwright@1.53.2: dependencies: playwright-core: 1.53.2 @@ -11402,28 +11450,28 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-import@15.1.0(postcss@8.5.4): + postcss-import@15.1.0(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.5.4): + postcss-js@4.0.1(postcss@8.5.6): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.4 + postcss: 8.5.6 - postcss-load-config@4.0.2(postcss@8.5.4): + postcss-load-config@4.0.2(postcss@8.5.6): dependencies: lilconfig: 3.1.3 yaml: 2.8.0 optionalDependencies: - postcss: 8.5.4 + postcss: 8.5.6 - postcss-nested@6.2.0(postcss@8.5.4): + postcss-nested@6.2.0(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -11433,7 +11481,7 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.4: + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -11448,7 +11496,7 @@ snapshots: mkdirp-classic: 0.5.3 napi-build-utils: 2.0.0 node-abi: 3.75.0 - pump: 3.0.2 + pump: 3.0.3 rc: 1.2.8 simple-get: 4.0.1 tar-fs: 2.1.3 @@ -11457,7 +11505,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier@3.5.3: {} + prettier@3.6.2: {} pretty-format@27.5.1: dependencies: @@ -11480,9 +11528,9 @@ snapshots: property-information@7.1.0: {} - pump@3.0.2: + pump@3.0.3: dependencies: - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 once: 1.4.0 optional: true @@ -11521,7 +11569,7 @@ snapshots: dependencies: dnd-core: 16.0.1 - react-dnd@16.0.1(@types/node@22.15.30)(@types/react@18.3.23)(react@18.3.1): + react-dnd@16.0.1(@types/node@24.0.10)(@types/react@18.3.23)(react@18.3.1): dependencies: '@react-dnd/invariant': 4.0.2 '@react-dnd/shallowequal': 4.0.2 @@ -11530,7 +11578,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 optionalDependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.10 '@types/react': 18.3.23 react-docgen-typescript@2.4.0(typescript@5.8.3): @@ -11539,9 +11587,9 @@ snapshots: react-docgen@8.0.0: dependencies: - '@babel/core': 7.27.4 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/core': 7.28.0 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.7 '@types/doctrine': 0.0.9 @@ -11601,7 +11649,7 @@ snapshots: optionalDependencies: '@types/react': 18.3.23 - react-router@7.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router@7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: cookie: 1.0.2 react: 18.3.1 @@ -11756,30 +11804,30 @@ snapshots: reusify@1.1.0: {} - rollup@4.41.1: + rollup@4.44.1: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.41.1 - '@rollup/rollup-android-arm64': 4.41.1 - '@rollup/rollup-darwin-arm64': 4.41.1 - '@rollup/rollup-darwin-x64': 4.41.1 - '@rollup/rollup-freebsd-arm64': 4.41.1 - '@rollup/rollup-freebsd-x64': 4.41.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.41.1 - '@rollup/rollup-linux-arm-musleabihf': 4.41.1 - '@rollup/rollup-linux-arm64-gnu': 4.41.1 - '@rollup/rollup-linux-arm64-musl': 4.41.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.41.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.41.1 - '@rollup/rollup-linux-riscv64-gnu': 4.41.1 - '@rollup/rollup-linux-riscv64-musl': 4.41.1 - '@rollup/rollup-linux-s390x-gnu': 4.41.1 - '@rollup/rollup-linux-x64-gnu': 4.41.1 - '@rollup/rollup-linux-x64-musl': 4.41.1 - '@rollup/rollup-win32-arm64-msvc': 4.41.1 - '@rollup/rollup-win32-ia32-msvc': 4.41.1 - '@rollup/rollup-win32-x64-msvc': 4.41.1 + '@rollup/rollup-android-arm-eabi': 4.44.1 + '@rollup/rollup-android-arm64': 4.44.1 + '@rollup/rollup-darwin-arm64': 4.44.1 + '@rollup/rollup-darwin-x64': 4.44.1 + '@rollup/rollup-freebsd-arm64': 4.44.1 + '@rollup/rollup-freebsd-x64': 4.44.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.44.1 + '@rollup/rollup-linux-arm-musleabihf': 4.44.1 + '@rollup/rollup-linux-arm64-gnu': 4.44.1 + '@rollup/rollup-linux-arm64-musl': 4.44.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.44.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.44.1 + '@rollup/rollup-linux-riscv64-gnu': 4.44.1 + '@rollup/rollup-linux-riscv64-musl': 4.44.1 + '@rollup/rollup-linux-s390x-gnu': 4.44.1 + '@rollup/rollup-linux-x64-gnu': 4.44.1 + '@rollup/rollup-linux-x64-musl': 4.44.1 + '@rollup/rollup-win32-arm64-msvc': 4.44.1 + '@rollup/rollup-win32-ia32-msvc': 4.44.1 + '@rollup/rollup-win32-x64-msvc': 4.44.1 fsevents: 2.3.3 rrweb-cssom@0.8.0: {} @@ -11834,12 +11882,12 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) - secretlint@9.3.4: + secretlint@10.1.1: dependencies: - '@secretlint/config-creator': 9.3.4 - '@secretlint/formatter': 9.3.4 - '@secretlint/node': 9.3.4 - '@secretlint/profiler': 9.3.4 + '@secretlint/config-creator': 10.1.1 + '@secretlint/formatter': 10.1.1 + '@secretlint/node': 10.1.1 + '@secretlint/profiler': 10.1.1 debug: 4.4.1(supports-color@8.1.1) globby: 14.1.0 read-pkg: 8.1.0 @@ -12034,7 +12082,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3): + storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.6.3 @@ -12046,9 +12094,9 @@ snapshots: esbuild-register: 3.6.0(esbuild@0.25.5) recast: 0.23.11 semver: 7.7.2 - ws: 8.18.2 + ws: 8.18.3 optionalDependencies: - prettier: 3.5.3 + prettier: 3.6.2 transitivePeerDependencies: - '@testing-library/dom' - bufferutil @@ -12147,17 +12195,17 @@ snapshots: style-mod@4.1.2: {} - style-to-js@1.1.16: + style-to-js@1.1.17: dependencies: - style-to-object: 1.0.8 + style-to-object: 1.0.9 - style-to-object@1.0.8: + style-to-object@1.0.9: dependencies: inline-style-parser: 0.2.4 sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/gen-mapping': 0.3.12 commander: 4.1.1 glob: 10.4.5 lines-and-columns: 1.2.4 @@ -12231,18 +12279,18 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.4 - postcss-import: 15.1.0(postcss@8.5.4) - postcss-js: 4.0.1(postcss@8.5.4) - postcss-load-config: 4.0.2(postcss@8.5.4) - postcss-nested: 6.2.0(postcss@8.5.4) + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.0.1(postcss@8.5.6) + postcss-load-config: 4.0.2(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.10 sucrase: 3.35.0 transitivePeerDependencies: - ts-node - tailwindcss@4.1.8: {} + tailwindcss@4.1.11: {} tapable@2.2.2: {} @@ -12250,14 +12298,14 @@ snapshots: dependencies: chownr: 1.1.4 mkdirp-classic: 0.5.3 - pump: 3.0.2 + pump: 3.0.3 tar-stream: 2.2.0 optional: true tar-stream@2.2.0: dependencies: bl: 4.1.0 - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 @@ -12279,7 +12327,7 @@ snapshots: terser-webpack-plugin@5.3.14(esbuild@0.25.5)(webpack@5.99.8(esbuild@0.25.5)): dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.29 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 @@ -12290,7 +12338,7 @@ snapshots: terser@5.43.1: dependencies: - '@jridgewell/source-map': 0.3.6 + '@jridgewell/source-map': 0.3.10 acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -12313,11 +12361,11 @@ snapshots: dependencies: editions: 6.21.0 - thememirror@2.0.1(@codemirror/language@6.11.1)(@codemirror/state@6.5.2)(@codemirror/view@6.37.1): + thememirror@2.0.1(@codemirror/language@6.11.2)(@codemirror/state@6.5.2)(@codemirror/view@6.38.0): dependencies: - '@codemirror/language': 6.11.1 + '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.1 + '@codemirror/view': 6.38.0 thenify-all@1.6.0: dependencies: @@ -12337,10 +12385,10 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.4.5(picomatch@4.0.2) + fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 - tinypool@1.1.0: {} + tinypool@1.1.1: {} tinyrainbow@2.0.0: {} @@ -12385,7 +12433,7 @@ snapshots: ts-loader@9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.5)): dependencies: chalk: 4.1.2 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.2 micromatch: 4.0.8 semver: 7.7.2 source-map: 0.7.4 @@ -12406,7 +12454,7 @@ snapshots: tslib@2.8.1: {} - tsx@4.19.4: + tsx@4.20.3: dependencies: esbuild: 0.25.5 get-tsconfig: 4.10.1 @@ -12469,25 +12517,25 @@ snapshots: tunnel: 0.0.6 underscore: 1.13.7 - typedoc-plugin-markdown@4.6.4(typedoc@0.28.5(typescript@5.8.3)): + typedoc-plugin-markdown@4.7.0(typedoc@0.28.7(typescript@5.8.3)): dependencies: - typedoc: 0.28.5(typescript@5.8.3) + typedoc: 0.28.7(typescript@5.8.3) - typedoc@0.28.5(typescript@5.8.3): + typedoc@0.28.7(typescript@5.8.3): dependencies: - '@gerrit0/mini-shiki': 3.5.0 + '@gerrit0/mini-shiki': 3.7.0 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 typescript: 5.8.3 yaml: 2.8.0 - typescript-eslint@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.29.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -12511,7 +12559,9 @@ snapshots: undici-types@6.21.0: {} - undici@6.21.3: {} + undici-types@7.8.0: {} + + undici@7.11.0: {} unicorn-magic@0.1.0: {} @@ -12559,13 +12609,13 @@ snapshots: unplugin@2.3.5: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 picomatch: 4.0.2 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.1.3(browserslist@4.25.0): + update-browserslist-db@1.1.3(browserslist@4.25.1): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.1 escalade: 3.2.0 picocolors: 1.1.1 @@ -12604,7 +12654,7 @@ snapshots: v8-to-istanbul@9.3.0: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.29 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 @@ -12627,13 +12677,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.2.3(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.2.4(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 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.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -12648,13 +12698,13 @@ snapshots: - tsx - yaml - vite-node@3.2.3(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.2.4(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 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@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -12669,17 +12719,17 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)): + vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)): dependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.5 - fdir: 6.4.5(picomatch@4.0.2) + fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 - postcss: 8.5.4 - rollup: 4.41.1 + postcss: 8.5.6 + rollup: 4.44.1 tinyglobby: 0.2.14 optionalDependencies: '@types/node': 20.11.25 @@ -12687,36 +12737,36 @@ snapshots: jiti: 2.4.2 lightningcss: 1.30.1 terser: 5.43.1 - tsx: 4.19.4 + tsx: 4.20.3 yaml: 2.8.0 - vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): + vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.5 - fdir: 6.4.5(picomatch@4.0.2) + fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 - postcss: 8.5.4 - rollup: 4.41.1 + postcss: 8.5.6 + rollup: 4.44.1 tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.10 fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 terser: 5.43.1 - tsx: 4.19.4 + tsx: 4.20.3 yaml: 2.8.0 - vitest@3.2.3(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.3)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 - '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) - '@vitest/pretty-format': 3.2.3 - '@vitest/runner': 3.2.3 - '@vitest/snapshot': 3.2.3 - '@vitest/spy': 3.2.3 - '@vitest/utils': 3.2.3 + '@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.43.1)(tsx@4.20.3)(yaml@2.8.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.0 debug: 4.4.1(supports-color@8.1.1) expect-type: 1.2.1 @@ -12727,16 +12777,15 @@ snapshots: tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.14 - tinypool: 1.1.0 + 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.43.1)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.2.3(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 20.11.25 - '@vitest/browser': 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.3) - '@vitest/ui': 3.2.3(vitest@3.2.3) + '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: - jiti @@ -12752,16 +12801,16 @@ snapshots: - tsx - yaml - vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.3)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 - '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0)) - '@vitest/pretty-format': 3.2.3 - '@vitest/runner': 3.2.3 - '@vitest/snapshot': 3.2.3 - '@vitest/spy': 3.2.3 - '@vitest/utils': 3.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.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.0 debug: 4.4.1(supports-color@8.1.1) expect-type: 1.2.1 @@ -12772,16 +12821,16 @@ snapshots: tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.14 - tinypool: 1.1.0 + tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.2.3(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.15.30 - '@vitest/browser': 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.3) - '@vitest/ui': 3.2.3(vitest@3.2.3) + '@types/node': 24.0.10 + '@vitest/browser': 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: - jiti @@ -12846,9 +12895,9 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 - browserslist: 4.25.0 + browserslist: 4.25.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.2 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -12954,7 +13003,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.2: {} + ws@8.18.3: {} xml-name-validator@5.0.0: {} @@ -13023,7 +13072,7 @@ snapshots: yocto-queue@1.2.1: {} - zod@3.25.55: {} + zod@3.25.71: {} zustand@4.5.7(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1): dependencies: @@ -13033,7 +13082,7 @@ snapshots: immer: 9.0.21 react: 18.3.1 - zustand@5.0.5(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)): + zustand@5.0.6(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)): optionalDependencies: '@types/react': 18.3.23 immer: 9.0.21 diff --git a/vscode/extension/package.json b/vscode/extension/package.json index a6a4c7e806..4aa397cef5 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -138,23 +138,23 @@ "fs-extra": "^11.3.0", "vscode-jsonrpc": "^8.2.1", "vscode-languageclient": "^9.0.1", - "zod": "^3.25.55" + "zod": "^3.25.71" }, "devDependencies": { - "@eslint/js": "^9.28.0", - "@playwright/test": "^1.52.0", + "@eslint/js": "^9.30.1", + "@playwright/test": "^1.53.2", "@types/mocha": "^10.0.10", "@types/node": "20.11.25", "@types/vscode": "1.96.0", - "@vitest/ui": "^3.2.3", + "@vitest/ui": "^3.2.4", "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.5.2", - "@vscode/vsce": "^3.5.0", + "@vscode/vsce": "^3.6.0", "esbuild": "^0.25.5", - "eslint": "^9.29.0", + "eslint": "^9.30.1", "ts-loader": "^9.5.2", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.0", - "vitest": "^3.2.3" + "typescript-eslint": "^8.35.1", + "vitest": "^3.2.4" } } diff --git a/vscode/react/package.json b/vscode/react/package.json index 7c53d8d82f..c59ab3d35e 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -18,22 +18,22 @@ "@headlessui/react": "^2.2.4", "@heroicons/react": "^2.2.0", "@radix-ui/react-select": "^2.2.5", - "@tailwindcss/postcss": "^4.1.8", - "@tailwindcss/vite": "^4.1.8", - "@tanstack/react-query": "^5.80.7", - "@tanstack/react-router": "^1.120.16", - "@tanstack/react-router-devtools": "^1.120.16", - "@tanstack/react-virtual": "^3.13.9", - "@tanstack/router-plugin": "^1.120.16", + "@tailwindcss/postcss": "^4.1.11", + "@tailwindcss/vite": "^4.1.11", + "@tanstack/react-query": "^5.81.5", + "@tanstack/react-router": "^1.124.0", + "@tanstack/react-router-devtools": "^1.124.0", + "@tanstack/react-virtual": "^3.13.12", + "@tanstack/router-plugin": "^1.124.0", "apache-arrow": "^19.0.1", "clsx": "^2.1.1", "elkjs": "^0.8.2", - "orval": "^7.9.0", + "orval": "^7.10.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router": "^7.6.2", + "react-router": "^7.6.3", "reactflow": "^11.11.4", - "tailwindcss": "^4.1.8", + "tailwindcss": "^4.1.11", "vscode-uri": "^3.1.0" }, "devDependencies": { @@ -47,12 +47,12 @@ "@testing-library/react": "^16.3.0", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react": "^4.5.1", + "@vitejs/plugin-react": "^4.6.0", "jsdom": "^26.1.0", "storybook": "^9.0.15", "typescript": "^5.8.3", "vite": "^6.3.5", - "vitest": "^3.2.3", + "vitest": "^3.2.4", "web-vitals": "^4.2.4", "@vitest/browser": "3.2.3", "playwright": "^1.53.2", diff --git a/web/client/package.json b/web/client/package.json index cfc62a8a13..987bf6c70c 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -19,21 +19,21 @@ "@codemirror/commands": "^6.8.1", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-sql": "^6.9.0", - "@codemirror/language": "^6.11.1", + "@codemirror/language": "^6.11.2", "@codemirror/legacy-modes": "^6.5.1", "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.37.1", + "@codemirror/view": "^6.38.0", "@headlessui/react": "^2.2.4", "@heroicons/react": "^2.2.0", "@lit/react": "^1.0.7", "@radix-ui/react-context-menu": "^2.2.15", "@radix-ui/react-select": "^2.2.5", "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^5.80.7", + "@tanstack/react-query": "^5.81.5", "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.9", + "@tanstack/react-virtual": "^3.13.12", "@uidotdev/usehooks": "^2.4.1", - "@uiw/react-codemirror": "^4.23.12", + "@uiw/react-codemirror": "^4.23.14", "apache-arrow": "^19.0.1", "clsx": "^2.1.1", "diff": "^8.0.2", @@ -44,37 +44,37 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", - "react-router": "^7.6.2", + "react-router": "^7.6.3", "react-split": "^2.0.14", "reactflow": "^11.11.4", "thememirror": "^2.0.1", - "zustand": "^5.0.5" + "zustand": "^5.0.6" }, "devDependencies": { - "@eslint/js": "^9.28.0", - "@playwright/test": "^1.52.0", - "@swc/core": "^1.11.31", + "@eslint/js": "^9.30.1", + "@playwright/test": "^1.53.2", + "@swc/core": "^1.12.9", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/pluralize": "^0.0.33", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react-swc": "^3.10.1", + "@vitejs/plugin-react-swc": "^3.10.2", "ajv": "^8.17.1", "autoprefixer": "^10.4.21", - "eslint": "^9.29.0", + "eslint": "^9.30.1", "jsdom": "^26.1.0", - "orval": "^7.9.0", - "postcss": "^8.5.4", + "orval": "^7.10.0", + "postcss": "^8.5.6", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.0", + "typescript-eslint": "^8.35.1", "vite": "^6.3.5", "vite-plugin-css-injected-by-js": "^3.5.2", - "vitest": "^3.2.3" + "vitest": "^3.2.4" }, "optionalDependencies": { - "@swc/core-linux-x64-gnu": "^1.11.31" + "@swc/core-linux-x64-gnu": "^1.12.9" } } From 5b9e2f9d49601fa946047762821662bad7f4868a Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:04:38 -0500 Subject: [PATCH 0526/1056] Chore: remove observer docs (#4892) --- docs/cloud/features/alerts_notifications.md | 4 +- docs/guides/observer.md | 209 ------------------ docs/guides/observer/observer_chart-hover.png | Bin 37688 -> 0 bytes .../observer_chart-scale-selector.png | Bin 32921 -> 0 bytes .../observer/observer_chart-time-selector.png | Bin 64782 -> 0 bytes docs/guides/observer/observer_cli.png | Bin 11252 -> 0 bytes .../observer_dashboard-components.png | Bin 150382 -> 0 bytes docs/guides/observer/observer_dashboard.png | Bin 167908 -> 0 bytes .../observer/observer_environments-info-1.png | Bin 136676 -> 0 bytes .../observer/observer_environments-info-2.png | Bin 142145 -> 0 bytes .../observer/observer_environments-info-3.png | Bin 116217 -> 0 bytes .../observer_environments-info-prod-diff.png | Bin 32432 -> 0 bytes .../observer_environments-landing.png | Bin 65896 -> 0 bytes docs/guides/observer/observer_key-file.png | Bin 213400 -> 0 bytes .../observer/observer_model-information-1.png | Bin 133491 -> 0 bytes .../observer/observer_model-information-2.png | Bin 122696 -> 0 bytes .../observer/observer_model-information-3.png | Bin 142787 -> 0 bytes .../observer/observer_model-information-4.png | Bin 136042 -> 0 bytes .../observer/observer_plans-information.png | Bin 98175 -> 0 bytes docs/guides/observer/observer_plans-list.png | Bin 133025 -> 0 bytes .../observer/observer_plans-text-diff.png | Bin 31775 -> 0 bytes .../observer/observer_table-chart-toggle.png | Bin 60863 -> 0 bytes mkdocs.yml | 1 - 23 files changed, 1 insertion(+), 213 deletions(-) delete mode 100644 docs/guides/observer.md delete mode 100644 docs/guides/observer/observer_chart-hover.png delete mode 100644 docs/guides/observer/observer_chart-scale-selector.png delete mode 100644 docs/guides/observer/observer_chart-time-selector.png delete mode 100644 docs/guides/observer/observer_cli.png delete mode 100644 docs/guides/observer/observer_dashboard-components.png delete mode 100644 docs/guides/observer/observer_dashboard.png delete mode 100644 docs/guides/observer/observer_environments-info-1.png delete mode 100644 docs/guides/observer/observer_environments-info-2.png delete mode 100644 docs/guides/observer/observer_environments-info-3.png delete mode 100644 docs/guides/observer/observer_environments-info-prod-diff.png delete mode 100644 docs/guides/observer/observer_environments-landing.png delete mode 100644 docs/guides/observer/observer_key-file.png delete mode 100644 docs/guides/observer/observer_model-information-1.png delete mode 100644 docs/guides/observer/observer_model-information-2.png delete mode 100644 docs/guides/observer/observer_model-information-3.png delete mode 100644 docs/guides/observer/observer_model-information-4.png delete mode 100644 docs/guides/observer/observer_plans-information.png delete mode 100644 docs/guides/observer/observer_plans-list.png delete mode 100644 docs/guides/observer/observer_plans-text-diff.png delete mode 100644 docs/guides/observer/observer_table-chart-toggle.png diff --git a/docs/cloud/features/alerts_notifications.md b/docs/cloud/features/alerts_notifications.md index 98fe65a0ac..f8c4d0e0fc 100644 --- a/docs/cloud/features/alerts_notifications.md +++ b/docs/cloud/features/alerts_notifications.md @@ -32,8 +32,6 @@ Tobiko Cloud sends an alert based on a *trigger*. There are two types of trigger Events are tied to steps in the SQLMesh `plan` and `run` processes. For example, you could alert whenever a `plan` succeeded or a `run` failed. -Measures are [automatically calculated](../../guides/observer.md#measures) at run time. - Choose whether the alert will be triggered by a Measure or Event in the alert's Trigger Type field. ![Image showing the add Alert page trigger type field](./alerts_notifications/add_alert_trigger_type.png) @@ -63,7 +61,7 @@ Finally, choose a Notification Target where the alert should be sent (described ### Measure triggers -Tobiko Cloud Alerts can be triggered when a [measure](../../guides/observer.md#measures) exceeds a threshold or meets a condition. +Tobiko Cloud Alerts can be triggered when a measure exceeds a threshold or meets a condition. To configure a measure alert, first build the condition that triggers the measure. Choose the measure of interest, the comparison operator, and a threshold value. diff --git a/docs/guides/observer.md b/docs/guides/observer.md deleted file mode 100644 index 734e634d98..0000000000 --- a/docs/guides/observer.md +++ /dev/null @@ -1,209 +0,0 @@ -# SQLMesh Observer - -Data pipelines break. Upstream sources change without warning, buggy code gets merged, and cloud services randomly time out. These problems are ubiquitous, and someone is responsible for fixing them (probably you if you're reading this). - -SQLMesh Observer provides the information you need to rapidly detect, understand, and remedy problems with SQLMesh data transformation pipelines. - -This page describes how to install, run, and use SQLMesh Observer. - -## Context - -### The Challenge - -Remediating problems with data pipelines is challenging because there are so many potential causes. For transformation pipelines, those range from upstream source timeouts to SQL query errors to Python library conflicts (and more!). - -A useful observation tool should enable answering the following questions: - -- Did a problem occur? -- When did it occur? -- What type of problem is it? -- Where is the problem coming from? -- What is causing the problem? - -SQLMesh Observer supports answering these questions in four ways: - -1. Automatically [notifying users](./notifications.md) if a problem occurs -2. Capturing, storing, and displaying historical measures to reveal when a problem occurred -3. Enabling easy navigation from aggregated to granular information about pipeline components to identify the problem source -4. Centralizing error information from multiple sources to debug the problem - -### Measures - -SQLMesh Observer automatically captures and stores measures from all SQLMesh actions. We now briefly review the SQLMesh workflow before describing the different measures Observer captures. - -#### SQLMesh workflow - -The core of a SQLMesh project is its **models**. Roughly, each model consists of one SQL query and metadata that tells SQLMesh about how the model should be processed. - -Each model may have **audits** that validate the data returned by a model (e.g., verifying that a column contains no `NULL` values). By default, SQLMesh will stop running a project if an audit fails for any of its models. - -When you run a project on a SQL engine, you must choose an **environment** in which to run it. Environments allow people to modify projects in an isolated space that won't interfere with anyone else (or the version of the project running in production). - -SQLMesh stores a unique fingerprint of the project's content on each run so it can determine if any of that content has changed the next time you run it in that environment. - -When a project's content has changed, an environment is updated to reflect those changes with a SQLMesh **plan**. The plan identifies all the changes and determines which data will be affected by them so it only has to re-run the relevant models. - -After changes have been applied with a plan, the project is **run** on a schedule to process new data that has arrived since the previous run. - -The five entities in bold - models, audits, environments, runs, and plans - provide the information SQLMesh Observer captures to help you efficiently identify and remediate problems with your transformation pipeline. - -#### Data - -We now describe the specific measures SQLMesh captures about each entity. - -SQLMesh performs its primary actions during **plans** and **runs**, so measures are automatically generated when they occur. Both plans and runs are executed in a specific **environment**, so all of their measures are environment-specific. - -These measures are recorded and stored for each plan or run in a specific environment: - -- When it began and ended -- Total run time -- Whether it failed -- Whether and how any model audits failed -- The model versions evaluated during the plan/run -- Each model's run time - -## Installation - -SQLMesh Observer is part of the `sqlmesh-enterprise` Python library and is installed via `pip`. - -Installation requires a license key provided by Tobiko Data. You include the license key in the `pip` install command executed from the command line. It is quite long, so we recommend placing it in a file that the installation command reads. In this example, we have stored the key in a `txt` file: - -![SQLMesh Enterprise key stored in txt file](./observer/observer_key-file.png){ loading=lazy } - -Run the installation command and read the key file with the following command. The key is passed to the `--extra-index-url` argument, either directly by pasting the key into the command or by reading the key from file with an embedded `cat` command. You should replace `` with the path to your key file: - -``` bash -> pip install "sqlmesh-enterprise" --extra-index-url "$(cat )" -``` - -`sqlmesh-enterprise` works by overriding components of `sqlmesh` open source, and installing `sqlmesh-enterprise` will automatically install open-source `sqlmesh`. - -SQLMesh extras, such as SQL engine drivers, can be passed directly to the `sqlmesh-enterprise` installation command. This example installs the SQLMesh Slack notification and Snowflake engine driver extras: - -``` bash -> pip install "sqlmesh-enterprise[slack,snowflake]" --extra-index-url "$(cat )" -``` - -NOTE: `sqlmesh-enterprise` will not function properly if open-source `sqlmesh` is installed after it. - -## Startup - -As with the open-source [SQLMesh Browser UI](../quickstart/ui.md), SQLMesh Observer is initiated from the command line then opened in a web browser. - -First, navigate to your project directory in the CLI. Then start Observer by running the `sqlmesh observe` command: - -```bash -sqlmesh observe -``` - -After starting up, SQLMesh Observer is served at `http://127.0.0.1:8000` by default: - -![SQLMesh Observer startup on CLI](./observer/observer_cli.png){ loading=lazy } - -Navigate to the URL by clicking the link in your terminal (if supported) or copy-pasting it into your web browser: - -![SQLMesh Observer dashboard interface](./observer/observer_dashboard.png){ loading=lazy } - -## Interface - -We now describe the components of the SQLMesh Observer user interface. - -### Dashboard - -The "Dashboard" page is displayed when Observer starts - it consists of the following components: - -1. Links to the other two pages, "Environments" and "Plan Applications," in the top left -2. Counts and links to key information about environments, models, and plans in the top center -3. Interactive chart of historical `run` run times in the middle center -4. Interactive chart of historical audit failure counts in the bottom left -5. Interactive chart of historical `run` failures in the bottom right - -![SQLMesh Observer dashboard](./observer/observer_dashboard-components.png){ loading=lazy } - -### Charts - -Observer presents historical information via charts and tables. Most charts represent time on the x-axis and share the same appearance and user options. - -In a chart's top left corner is the `Time` selector, which sets the range of the x-axis. For example, the first chart displays 1 week of data, from November 27 through December 4. The second chart displays the same data but includes 3 months of historical data beginning on September 4: - -![SQLMesh Observer chart x-axis time selector](./observer/observer_chart-time-selector.png){ loading=lazy } - -In a chart's top right corner is the `Scale` selector, which toggles between a linear and log y-axis scale. A log scale may be helpful for comparing highly variable data series over time. This example displays the data from the second chart in the previous figure with a log y-axis scale: - -![SQLMesh Observer chart y-axis scale selector](./observer/observer_chart-scale-selector.png){ loading=lazy } - -Charts also display the data underlying a specific data point when the mouse hovers over it: - -![SQLMesh Observer chart mouse hover](./observer/observer_chart-hover.png){ loading=lazy } - -Many charts display purple `Plan` markers, which provide contextual information about when changes to the project occurred. Clicking on the marker will open a page containing [more information about the plan](#plan-applications). - -Some Observer tables include a button that toggles a chart of the measures in the table: - -![SQLMesh Observer table chart toggle](./observer/observer_table-chart-toggle.png){ loading=lazy } - - -### Environments - -Access the `Environments` landing page via the navigation links in the dashboard's top left. It displays a table listing each SQLMesh environment, the date it was created, the date it was last updated, and the date it expires (after which the SQLMesh janitor will delete it). The `prod` environment is always present and has no expiration date. - -![SQLMesh Observer environment landing page](./observer/observer_environments-landing.png){ loading=lazy } - -Clicking an environment's name in the table open's the environment's information page. The page begins with historical charts of run time, audit failures, and evaluation failures: - -![SQLMesh Observer environment information page](./observer/observer_environments-info-1.png){ loading=lazy } - -The page continues with lists of recent audit failures, evaluation failure, and model evaluations: - -![SQLMesh Observer environment information: recent occurrences](./observer/observer_environments-info-2.png){ loading=lazy } - -The page finishes with a list of models that differ from those currently in the `prod` environment, a list of the audits that have historically failed most frequently, a list of the models that have historically failed most frequently, and a list of the models with the longest run times: - -![SQLMesh Observer environment information: historical outliers](./observer/observer_environments-info-3.png){ loading=lazy } - -Each model differing from the `prod` environment may be expanded to view the text diff between the two. The models are listed separately based on whether the plan directly or indirectly modified them, and breaking changes are indicated with an orange "Breaking" label: - -![SQLMesh Observer environment information: model text diff](./observer/observer_environments-info-prod-diff.png){ loading=lazy } - -### Plan Applications - -Access the `Plan Applications` landing page via the navigation links in the dashboard's top left. It displays a table listing each SQLMesh project plan that has been applied and includes the following information about each: - -- Plan ID -- Previous plan ID (most recent plan executed prior) -- Environment to which the plan was applied (with link to environment information page) -- A count of models in the plan (with link to the plan's models) -- Whether the plan included model restatements -- Whether the plan was in forward-only mode -- The start and end dates of the time interval covered by the plan -- The start and end times of the plan application - -![SQLMesh Observer plans list](./observer/observer_plans-list.png){ loading=lazy } - -Clicking a Plan ID opens its information page, which lists the information included in the landing page table and links to models added or modified by the plan: - -![SQLMesh Observer plan information page](./observer/observer_plans-information.png){ loading=lazy } - -Modified models can be expanded to display a text diff of the change: - -![SQLMesh Observer plan text diff](./observer/observer_plans-text-diff.png){ loading=lazy } - -### Models - -A model can change over time, so its information is associated with a specific SQLMesh environment and plan. Access a model's page via links in a plan or environment page. - -The model information page begins with historical charts of model run time, audit failures, and evaluation failures: - -![SQLMesh Observer model charts](./observer/observer_model-information-1.png){ loading=lazy } - -It continues with details about the model, including its metadata (e.g., model dialect and kind), model text, and list of previous model versions and text diffs: - -![SQLMesh Observer model details](./observer/observer_model-information-2.png){ loading=lazy } - -Next, the Loaded Intervals section displays the time intervals that have been loaded and are currently present in the model's physical table, and the Recent Model Evaluations section lists the time interval each evaluation processed and the evaluation's start and end times: - -![SQLMesh Observer model time intervals](./observer/observer_model-information-3.png){ loading=lazy } - -The model information page concludes with a list of most frequent audits the model has failed, the most frequent time intervals that failed, and the largest historical model run times: - -![SQLMesh Observer historical outliers](./observer/observer_model-information-4.png){ loading=lazy } diff --git a/docs/guides/observer/observer_chart-hover.png b/docs/guides/observer/observer_chart-hover.png deleted file mode 100644 index a06bc605e098624d1ee2bd1538d92a46adf578ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37688 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 diff --git a/docs/guides/observer/observer_chart-scale-selector.png b/docs/guides/observer/observer_chart-scale-selector.png deleted file mode 100644 index fd603301e590abd5cbd6f7ec7481ce2a116aafd9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32921 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%0DpW$!{a}$RC)3Q!EvSUULdP}EhsiV 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�TPiEekS!=z= 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 diff --git a/docs/guides/observer/observer_cli.png b/docs/guides/observer/observer_cli.png deleted file mode 100644 index c1237d098bcd5b1a414480fcdf8a7caccf9c988c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11252 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

t?HI-#iOmWrQ^*z&(vlOl;)ctdZwZrk+Bx@u5V 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

Jajb@9l+EL&aLyvy>S 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 diff --git a/docs/guides/observer/observer_dashboard.png b/docs/guides/observer/observer_dashboard.png deleted file mode 100644 index 9980e73570a3913b5919077a54b6ebbd0f20730b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167908 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

R6uHW8|oD>5*_e=82C5UCRAcRBDr) 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

W%A&Fx$Z{sBEnb}nc>h6iZ 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%0D5EraevIk#nLz5K-J4} z0%$8fV0*~CVJF8`D6sv1s^bKPa*<+ceSGR%R;sr{CmK~Xwfa4Cmfh-M?W%oO(FL<* zz=am`6Si-xqW^_h-bllk7j^vu*IU+KOFn!xgv0xPPXBFP$bsyStA?nqZV%1=BlP`; z5cZEKa(XWXykMJg7i8+Dt&{qu1Ptnp?EMtjvyfMj+mr?quH&sa{2x#eUKY|PwVwwh#e&U3!3|5N(K~$toT_9#cfT4fjLG)FjVEaA>~?f zfnd7D>IryM3Ol|E7~SR?0o+K>#vpEl%hrEH0NgT_7ApK$FL+tw|6W!j4wU`(Vd3ll z!vM0h>`tJLjdkw7v1Y}&{|?yzet-eqPH$Tzf>Em=C6tQ zR9L^5Ls7NSCcxEJYbn+WPGE`IyLu;E)?3_`SQ`ujfRVFYyKFF#?;Y-D-WAAhcgD`! zaXE6|qCu2HVfpyI%Kh1%iywPJu3EKljApD0?~QWIEJv#piH$xykK;(>zU+≷Xa4w4MF4tk8u2g`Q%0a3c6W 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%0AZQHQzh*F4La(_WIdtqLz?T#5g_5CMQ zVaRZ3o9)X@02s+}j^CEAc?=S+M{HTI83R9zvGH;$+~wWs(s+)CJA)GCy{La@Ax~Pl z&o30l2D|40iAG8uU6nrV@2l z1t19!?MM~x_y75D7(0>EXv7Kt7(^CtR|lAYc*Wqf?vjS2ST^K)R*z|V%*)SfpN2bV z%M?P3VBq7uk`I6o-M0*GbGz9XgCG^MtwG*h=!e*HF%vbzO9YW93QPY1U!CkyGsyW4 zfWI#cA^oA||4z!Gb5d4hwp=G2Sy14_0R)R-#Tkm!XU9*M5XUM$B*qUyz7xxa;uyKhh3r%T{^*-M&-Xd(v zf|R_O^&-a>?X#(NRGjT77!H9T@v;_?&!(Z$9oBu=?rE3Y?92FgFF2bbYPhb+qJh#b zq*O2NuGcMklorLJ(8^Uz8J5I4fR!Z?2qMgf)%PG`2_;FPwZ112f)yXg{xEEO&eZn@ z5T8gmv|7fzypsRuE46O>{u}=G@ch&?W+}b?j(ZM})l-lf8oDUftFha7*ZHrRi*W zfLFIZUew{K+vv(Wjni7b6(JpDw#BV5hH@0V0C=q3Z@v^izmeYTq;oe1{F!^d+FRnM zu0DrQur58E9G=Suqz~S?g(!c{nW%Otm*u3K$}u==VVw)9xn}HnqPwXw8sK5{oa&cXhb`a7pR zGRY1lJr4!ZQ>zhxzF(9)IPC(}dw(LhK;{&f$@8l&rSj|BFrogam&*q(S^+-}x_I9f ziA)!tUrzq7RW$^CSV)}o>v(p#*nvL=J&o+IRat^Q2wkcQy;Lc1#o=GdR-Rq1`*^{8 zsdnU7w)nhf^wf0OG{6_XKh_Jj`HyF~{{9%QluIMc6-Vu+x}Y3Absz3u1ZuqdUoHfa^MCc@@>Hx@|Ej{BKkwpY zZV1x+TREo^NQwYR#Q$#iDcShaI6-HZ`N2Bxa$)Z=6TnA*T^Ggj^m4f;Pz3fDyL>5q ze{_jPz(p6FRQRi6DE3@$Up{m1N5$WYOtz&jJ&oql%$jvP{Hyr)Q_YjV_a7{QazR(J z;kTC_4e)&i^9ARFSm~nw%K!qzfwI;=(|Wm^mkEmpVZpj!tIkexT(+J&H8 z>LZZ;ztlyb#Rd0W`}Ws+6WL4EVVCFm?{^A7HH-2&pYnT`3$^0q0KUF;QJO2&mf-W( z%`f%Sr{Yq@{pE2MUY65eRUzk(xqLBDpLg4-56~buKk~K=dl@_^%bRjk_}b{~Rv1R9 zlp5Vtq__2|?_m7CG=3DIQ2i3JWaRf3FRbt&sQf_WWc=`fjW7D_`pb3O#P>`y-@bB( z?41b{mP?jOZ-w(aw zQ^s4weE$9~<#(mcm^AQD@cffIDp$&%CT39_VzH=m0u zh2~c5;JVW)Bt5v(WjX3HdBsra%32X;PCrOAPaT6)#0C{Fb9Kz6b(d5oovak%+pbM5 z7gGSD?P4}G*DiIY*bY70A}==4qYO`2YQsxYJ4l3%Pf#ltR z+~CDMq6iWDA3VWssyNGXcc9(9aka#ZgY#X+#m%$NyhOXdO3KX|Bza?Y$!vEKN8tvu zM?YJC{SLZQQ1gCqzctXd`W@R(Jzveq6$IMtF~+kOD29*ObSkhgsT_s^qWm!&9TyGG zmSPuf!^moyh^&Wclp3yU$vctpD&FLBYGP|doncaVD2gcNQq5Gsr$1av1E0NmyjzREwX8Sf|&G>Xp1Wi2pEevrHJ+h3ie5eT6i`~`)=^qhk7QV#n z&V6zxO{btBY=%jZmWVmI7VR2M=^dU&QbDAC@dEBdMfLj$RgiuPK^k`La>@BN1E0|v z7n~w`M6wb3(Ufeu|F)-rFcjyB8{~yM;vK&s;qi*|ZE$l73GgngH60rD(GD#B@a9o@ zzDi^9q*pI)O+Hn@F}0Xh@N-0l9(rsAyYHZQY&olMdfYN?LuQ!;z_@vX-0UCxdge*z*xn&{&9Y8f?;}tdg7}^!(Ux*q)b2; zo@CRMF7rM5BAJ>lCP zWHoT;`*AtWnl0WGI~Tne%?L7|lN71QK*!3t5WYR_r8hlmehVU zv)Ce*nVXJ?)I<|hN-z^j*Fh1G5Lv_`{jqefG|e*dF$TSq070@ciKx#q_OaOu@JemSN5$XdfJXxcGrYla6Oal5Dh>4 z8Hy}B^k<{WCnOzDPHnO29xV4uHz~vik+d6pA#pwH;e;sYl+oS|sXpF!m*Ib?U*aWB zpK0wb5thCD<79M`57BXF5q^vt9o|Uyyv#u5NBT?bq@9qTshhu zEPpKgx%=Xj#u&4+ef)SFIwkubNw#5obkHE{`;FacrnQa+H;=}%e9Rb`rR3zxXnSs59DMJiRftJXK{6gHRh zl+F#E9p#hSJH(T~phfFrtvL(-vb1gfY?eiX@9%4>Z{4}g9{0&0DgafGh3tpSweFK7 zuaN447E>r>iY|#{fORo6?m-Ff~A&rZwJO*sZ_>hM?Ug}k(gIRZ;H?vG0IEq9uC-! zf_AU(edG}j;?S!NT3lG>DQMBN?%HVhtX59xAHCTrP452oxQDSA9m6p=$foo9vP940 zkznl}XEIQYxtP`@S8&=oPvCk)myNb~$!B8rOuF9C<8I92dZ}e4Pjga++HOfOp|-T# zXAxt4l=l(ok2i>FQh8)a-ZfWoB$OL1a5OHaa@IaV%>2;6Wq$@H2nleL4>pQb2l2?} zIOB$lPt{rE%DC|KcwfCq#*3;Fmad)P<}cOTh~>Yp)FBGTrN|48Tc8&73YL|jOXCoV z4pvc2t;v3741VmPQYx6@5TL@cHdIdacrM)RIFy#1KV7zSCfkQRC7*!d#%PKN_*H3d z@YPNeuGUm7o`lM7iMtG`{CR2mJRi1M$OALfDEBg02tw3F%;*>t+x*I7AG$zMdzQ$x z@ZNm>5H#JJ@KgErEyf$VnY;w4H>J|p64hEi(@`+V8FK7pII>cEn&D{SRV}eG+=!s2tmLaB?#X6M;$DspY2073k%9JhdN{w6IcNSPxiy^gx|>oZU)a zxme1Xo-+p#EJ(w{PTXS79IJsy=Cy@n#t-Gh#ACVeb<$+Sirv z-h&S;=u619pi+4u7*dXat-1!Nwac1N-4J(QgY{9mqQ$HUA9`g-v1}tgG28E%kYln^ zix9`qd3STYKDBsrZK>!hc~L$0SgK5ywK%{=E_D7ieD?xA??R3pWNJ|H$f}FUj{;c)Pm?vuw}S;0a}J`F+i$`Z z*_p81r&cSEq$7fkQHdi8CDD&ceQ~;9*pu3G8bZH!oyeAu7(B^&zv%e^(6uFZ;>7w? zQt1>+78t-^oKCPM9CG7URPIZ1CqBnWa|qS*B7o{8Bw_-(XMN>>|9H(a=OBgKn87)_g(WLfz=bOYo2+hzkXJ4Vp z1fQrCXv{n@Y?0b|XsrHdP0;C~zV`-N+QSkendwLB**`s=)G(@MNwG`J47%u`9NBIE zM|*azQ7+Vpz$Ie`?J+=k-}U_oX!ot#!5!SIUDISzem6p>D{C0LcW?AGCYG(Rr+kCw z;fP8h*51Y3UhBV}rTK{gvvPkmc(FY*kl#GO?(CbmKjlt^hQ2h~`jIl~PEn!kR+RW3sQU7hQ! z)W^YQRaXf-p~{%zF^XIkW6Ag_C%!j!7yFh92c%JIs=nmD>0078#U0bw;E0lQ)GD;+ z>V96}q8G_#tgZg2r*dl6W%;R^PMpHY60a7uqNn)-Pq`g4a?;X_`;M=8m9KGCDkYfH z5%vhjNl3Z&`Z{;tw~#|yyqTBw4XiE?U!)}rD~qR2rTYThPlP1`ONP=ZO&o0AJ+0-I zXJ7kzW6z%HP zU7CXC%Z?JpqHwl^`R`c%P2e@ED9POq*$Je2UteN&IeW^tG%;tI=7i}QMP^eT^W)L1 zv5MdayAR{I%^!ze%jsnA)_NRu+Y`4X4Yhk_rB-H7aPM{k6}xeH){R?fPL3hd;8*b8 zi2QDrWr+pmQ0*RUK_xVEeA2vB8plLMRntn4X%-=S#!hdjpt2~TvfSNt{=HPxN(F8+ z2CZjA2~2X|2(=~0vf|m4r54Qb6d2@sFqb7NB5NF4pR^_iba+nLvl~asV$jR7XF=GE zpJULU!figee~GA6VIfq(*301cEMJH*{}Rkr2$2e7C<^uMVkVPtC7_dgcporz7l<{M znGW99^;t2MQ6{KiPH#OtLj|xAZ#7Zd8OCUCEOL&-nFd9eIN+T%4CT2{NtCmtHWo}R zvA&CwlVt<;N{l&n&&_zdN4z0_mwMsG478(r4lRb?Z(NkV8$+vM(-c;Sn({+SA1)ZMxTWTe4VVGvq!2nW-7JExZJxWT zpAGHv?}b>2P>Szr2W*ho*U*QYTAy6!Vk{tk?#hsUVge{OQIBL(0s(uL5Jx@DkW(b)|)5hiX ztEkDwC3Mls3b|}N-gR0#FOkua_=dpyggxcBp@Bgz>?F+ zH@m_N!JrroiV=G`pRRE`m?=AZG&yGc<(;m?uCBbTOv#lpf0esp2Y>^VQh;tL(cM;H z@=3i;rcBIbeJF4-wh=1hE?&|t6}QvPE#N(cn&SDH@r@TJL)w54;M7Ps%ng5^cmg0d zF{yt;(Z96Lx$gnkkxLNUb(xi#lnp&XIIdMT*JlxrSt@S3*O~nqzCPZ<9N)ylAYQBS z*Nf4^EOGWXm^e*4bbQ|S-O>}4?0GBILPUxQBdS#R5gMV1GE`{Jox+|s>;(FvXPe7D zQbsprh~nm?P4cKf5+-8i9>0sPk*gj2R!k&JQL*wv6o%tB@&xJATRy`}KktR8=k)HL?E%6>Y_K*r(VUC~}} ziG@_9nP%}^@H9cK_ihinLY(0M-P;pkCp$r@o-FRtlqG_6?%uFzu%jO@M8&caBxPrz z!0;u)7lN^_Y#E?TsgM>BbB8%?AM6;BAKv9jCg%z!g;t|-McSKTYsH07h`stu{wYPA z-+nnVHMA~U)4VELq?q1OfMrazFWoQ4$E=IB56IJv1ulB#W8^hp$yBDsX5T!9BtIGf z5+b;In|H}49Mydf*qJ=#2fVnuJG!rD*+1cx63}Z>K9NqpUqfWw<#)w1sD}uN`X#8i zzA)$(vW0O#KD(0|tD;nRE@0z{QuVR2D=1nEV~M;O7sFG~PgS9SFpT5w=K(6%lFWA&V#IkMKRYp( zWyPW4nt-|`S<6_Dl-5VF`-4hGQHgM_HaNcvV zhrinq)w+^%Mi=)&fwwewY3+q;9K_@k&&RCCT2C~&0DMjw(-`+I+CZf=Wj2u}+h@0( zU7AhV(~2~uQmPn02`(YDR5-4X@?$X>HXE6^jwu>e>dgEfVN|Et1T4X1_M5uoUEDWU z2!_Q*lp1mAqBNq{18JnPCXf&VQDWC9viyE1r_&negrYI02-b^Q$(fBxfr3GZCokG$br~bwJ+o zF#ZW3xy3W;C5qYLcOC52=sso$IRn3bVuadJMch>Pbt9B^7R!E?Aw*Eu%7&K@Ie%m* z$&y^x-AJV8)b&U`j)fa5rg*r>fwn zP##z<{IcVm<`rQg!}h)r3mUn^jZ3#iV70R}L=;z-XYd5kGHy69_*99!F( zJmW_HRMXcx98tLBKLTv+x1yH2heDU4yQZB|S%@_f{ z!d91oeP(x3>?tuNDV; zor4Jl;~L~PvrtpGVlfx<@^FZ5cPDmr9B`_YRkK8*h1VkGtH2YH z5-FbW+2NB&dvZx{3>!oa#j#8S>xzEbiTc!T5F%+nH1pB2tyPUJf8+}y$@CQbGq2b5z;skphLT*`r^ z-bWRmnXFd1{JgEZ)T5dBHNx6?LrGe77Mnk$S#gKrWTienC(E*PP|o^DLs=+~*U#Sd z+jIM@*Qs)u2@bWs#Dd%QLa)f*s-5+8-cIG2I;8L`(psII^ABgfs(AHV9Zhp*85Wdb z1}iQxG@2Ggk*mQ1gO$2uK$`5nZ=HH?ydX~jiw4S3or5+HRZa4fogp2$skD1h`QjsY zsdfE2#`?2wYu2*&$8*saOrdj}uJe;B*kjPc9ZEDn{G`Dowl%qu-!~#h@_NwHp_mPU z!e^W1Y7fYTp7GPxr-<)kU8hpZM{R(uepkZc%)?=&j@NyBQ_-!#{22BGr0My!LXG%V zvn)sTscdkK@PmqMhO4%BzQJ6Ehzh6rXC!;g$75!?#E$khc=nyV5LIFpUopbVnaXJf z0(#mvE8@rJl1diLzCN8A-rhFZN9VWa1N`z}lInzS@AG`BiT+Pq>#{gsfGmon%U`on zU!(SwWu=~qB&B%@TIJ!0dJ1}(JxS+^HB{t~+2qSas53UHs0EDaPWAQ8t8BVIb-vNp zDQcojRXHdSv1#X$av7%iW|AU&D(LJ@rxlF&;SUdMB&f6O%%a7!U2!Lu0{CXKn;eex z95QF06((#mlKOcag-yQ2-k;YiL+!3AD)5z?=hXa+S+2=_-Eqaq&ljz!+;KStvo4VQ z3kY49U|a9yhZ6jr7a{`psNQ_3GU~*P1EF@*dlphGKMW$$L@h%Ne`8MwVYx@zdh2HM zJq$`eU^txL`3qNeaOK}wJOO2gN()DmyqwA1>}eDU&QuZ_!V0907yjY(`X&987Yy$) zFl)98KSng|JP)w|6V=|8tITY1_#luV*L7?3wjz2H*7Zz@7JZZ!ugH&F)|Dne)fCx% z&5fBxk$tAV3Te*ZglyjtNL^)A&O3$yv(;eeaI?`SkIW|wn>X-#nDehl?mVY@EA3l| ziYIP5h%$A;``v`(3P5LG(5=;Lua53f(wCA4y&`E~KO)FQ^J0I6C`48K=^%^#V3XFr z2&L@%8>wTg>wB+EAGUraZhZoB2GPm*z+A*n)Irz}zrHhgs{v{qy!rg017^_c{al~F zqDXspcqG2p?b=ro>A!6XBf#cvuCIH>_Xho?*5pKou(Z*&263^IH7cG!BF--Vt}J(@ zV2Y&~hQ(9MyE*i8jPr#t*7$x-N9CibNQwHi5J;vtPusNrrf6PIqs(GjlZQ=ExP-ae z-)>9~qupmAAP$%XU<-xpmePQExdfdD@yZ$9PG&k7vvDmL?E-Xiw-3$r^n3P_U;C0Xj7zN4y7(cRvOY=cB;D1V zE5^C5@58R?UyZL8iD5TL7J!fLJYb6+;u`HXhCL8gmBbB!7qK=!I+j_qej^5=5MJ@V(;KmAA3H4$&FiSl9ioYJHsadlfXvyi<0&UOhoq!zW|J)tJYRNl+4rv?u57Iy&|-rQy-<9Po5m0Iu%Jp6He#%rTo z&f4TQ3>F68W2)U21`6a>&7yAY8H{}mk@9+9LQ9rw8*rNd| zyAnf=`s!WhHS}V2(E2-;XV-w;XgMaXd6b5zqO_nLyda{`?0sq6s(wq!WOU-?4IvcD zu+=T+)whr1rc#^DzQ(&A^|8biqBhis2#Y#20RC5l!Pknn(t3&U0K|<;)w-D(?-b(O zBe}KMs&qd|$fg^tkWPp@ejF-INSRekMf07G(3|MAFR2`RW{X}pweMi9V5;D{Fx}<= zzi4>o=BB1LI)c{lyY&wp>TL~lbLqfjB5WU$8_7Zv_wF!)Sb4qmP~e*0y@ztR!4SY0dcmrlDYo1T;2!*Anqh5~5POT>0z3sV_ z`P|#C!B<$RO>cHsqL;0PKFD=p8^W^$^oV8}?uTO9?&_&&qV|wCMT~GkdJRytb>IWG zxEEN2L<;?QpYWVIoBh?(K)`ENsE8QlxD#lqb$^0HM>v`TU! zadbzrr<_@(QjV-As^_N!!lZmw>K0^j!?<2fS@I9sHa82b*V{EMKLPRgpElPeWlVjR zMJE@q0QAGQWq!}#{j7DDUW?UUImyYB?Gpt9l;?A=js9O{S0B&x7Kdky$xC#N88)w> zBxE0C-PzEtisT}_F)wL_nq-a2yiCf?%T-y)+u~-rG09sQtw^K1)MX@OhVq)Xw6eS1 zU+?#?`}y4S&pCgb^UwL5=Q-bV&hvfFRPLPXa5BT6FP^z@2R-qMQJXz{#b)Ec2Jqu& z&%+ntk`rx*5j7grFh!ZhwHd!qY-x+xWe9$}=s=%h099;(?;MIoSZWcila1T~!u5TS z!u>B?|2Vw*m4UNzp9xJ#07}W^^rl5jh*E_b_uBH}9?oqk5boN70p_WV zo+%fQ*D`FEP=)HOD&Q@BXfAasJS7}O6{)8+j4AEhJ#baTyPr$B5!&7#jYu#ph}+sk ztFAxBf|S>P@J$<(N>(3S}U`Dx(V4CKDO%2%4Dpc&E zf(40tQPKrFMBH%G-+vw5=Ycn9>5ra5i|hJ#-@%sd61Ie8y`Sp|>pfDe7m?KHDVxMZ zBI$P-h~xm>m!0fbt4a-4C|%{AFH%8Uo>SdpdD?x^x6B3hyC{2r9H{i%$KvS8}%j8+y-cFu} z?GCfw(b3CUnhZm8%(iTY&@B=Bx+*AhdHFHzgrlS$i=j1P;gHcw1~Dq8TViICH{>Wi z*09ZGnYcsflFer9K&J6$h=%*qcA4LQ%jm60HIOgy6!1l%@4X}#E4D*4$@~@D+_g~B zdVM^CQA_gkca!et6JodQ?e;DL82l43LWjQw3`bW4mD zX_odT(EZ&yeK5y9JJ{|P!FEZfP;ih107t|FvXL_o{-O6nUf8JgrU>m1<)37p4fes8 zuGgpRD0rnrVAPFM_Ofi>zErlLM&>#2p9(>>UO5pRBmHcNNXuO=hgD5(1NF8XpLFBY_}aGCq>!4!i=k=Sg)<<- zK81RHdGjI=0cYc=>d1s1$HN35`dghO@oqEF6Ectjw}m3S;f|QCRZ!LKCI@MiR!$Q( z4IQyi$U5MN9IMNV3B$QHFaGwyl_?vR7{O(mJPYK-g`6YW&7uFS#5&1Qc3Q@JvtSh5 zLdh_EIys%{!8Dwv531o*LvhPSqn4zE8a)ODhs=5(Ra%*psZiRb?)-z4B(LWlE=1&n z@_v@^B3l|(mJb)Nq99Yz>rY;K2*x=mWH26{ckQR?Rc@D9J~ScZY&#$14(ZIRH{cJT z@34Mf=SaSFsp+oQ=6gxp$R6I6O~)E<0Pk!$7QxBteI^|0&nvIHv$@nj8nT(SbjAVM zK5_k~omWfsL0N+scl$|WQ}5OG?_SLe%4}`krSVA6-o3Uh+$5sN$0f*odCo^b*%x8_ zRRq&9+js<3P_}%EQ|4PO!HLURykTHnr%?CLla7PGm05NmNH0#8jXy8;N@zK<4OjGumF(GPg{ meJMoEl}Pn9nSyV+oi&-La^7Y6@l=)1dL1`c50`R0G4UVfAc#2t diff --git a/docs/guides/observer/observer_environments-info-2.png b/docs/guides/observer/observer_environments-info-2.png deleted file mode 100644 index 63bba899f2404ecc57c1171248194b222e54a772..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142145 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 diff --git a/docs/guides/observer/observer_environments-info-3.png b/docs/guides/observer/observer_environments-info-3.png deleted file mode 100644 index 1cd5c8918c310eb1c02d38fcc989b681f36ec5db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116217 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�SR<`F}!FO-=0mAv(_3$9~S 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%0D()d6S97OtZ+*mo%4*+` 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

eesk0*L6lUkN{TVd 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 f`PKe&ZDF5||Dr3$$8Wm{h^2@nv^K0 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

  • k#ewxN824QRW!34W!jajT=tqSW3sRF6;d%VZq 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
    Jv_C(SlE^ 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 SDX=Wh>hF=3lzuwpIZeODjH4he< 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 diff --git a/docs/guides/observer/observer_model-information-2.png b/docs/guides/observer/observer_model-information-2.png deleted file mode 100644 index 1b77b7c3239c54c2dc64134f7e293c5ec328f255..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 122696 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 z

    KoznN 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

    KM}JGCFsEZ_c|kBuwDyC&{qM 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 diff --git a/docs/guides/observer/observer_model-information-3.png b/docs/guides/observer/observer_model-information-3.png deleted file mode 100644 index 6e4a45199b3809d34c19bcac59725ca43dc40fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142787 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

    p*ZmKV@L2}!-d zK>@Ezoq3g2j{%|$|;a&AiAUtY2VoQ(6x@S#1=}Umv*yxDdm7sAoIPUA0C3BAmXXkKBhBjo^nRV&o zy5oZ-xgf7eC^%>_E=)7&?N;oABzEmVRDu${xW@u^^7G#|ZRI`TH4Ni7AkZI81s0($9m@aZPt9iuZ799^R0b}3+rIpQKuNf_WE_8QmA6LM#bg3PTQe3io)WAb=jBUCXt(@xLA7Uy?d zeFYOm(Iv^*p56&krM|MQ+MH&R*@zbHraogkvvIul<7hMbn|}EpNdQcUZaaT$Nwo#r zPYAz^2*MIOmm{dekDCGaqW?ws`PLUHR5P}5UZ^Q{pZXez1+l+1WX=pf_Au6+fQYpd1H&WRJ0x8e!Ly*R?EnHIU}8a%#Ppv{dWS38)M{mY)T-2IsJ_yye;^T zbdw82g~)NOI`n@1rHIKZW?D#?Z@-A!q*B6AdMrPGv9d|EhP-2i_)>ptRpddA9Xrq; z=E3ZXzQ=5FP+XHO0ZqC$gwGsxg4Lk`EFs2j3fMfyVI4V@^OVu512|v_w!51=V|bJR z5@v9fqFb}U>%oAT+5@60J_q}%on71@U;lQ=Al8Vr!Q7|(j#p6Q&4UD-mGOGFaBvlO zRJvCQzDBE+FJQKJmAFlE7N?A_%=>}+YbuKr=0(T|3e##XAAFjvhDWmA+n^0#^)a*4 zynz$Hntj-{CGLVlK+nn$sipzQkQLH-IL3Gt-@L~$b6V*2z$UP`E#rVI%}wl3Km`~q zPC(*{`mpmHn8P8_J@Bk}#0B(Z;a@$ua3SUj66uNC zisO9+zHi2s&`)V+fAo^?OD7DkH3k-5is#B^QRqizFbwf0g}e`7M@noiXVgLoX?Ee8 z<^A{t#|9v5MQt&QDu45^#OMy1ZFvw!N+0vJ zv>ge+5vx zG-s-awes7-#Yb!&_oTP3FP`6!58nmIhxH^7l|O^Bx(~NHuM6Q zj^>x8f32cvz-F7?b3;CySj!i5 zjX(VHR=OfKBRqSgH=MeSr}%OLARxZMSi)(Bcb;t5zV-l^#bp$%ij1!Sx@a%u5l??c z)E`Gc*yFtOB%lXs7mLQkwq?CI2i!(IHDkSsKCXHc_4HoVtsVIzHS{=)*XGyiK$jkn z*w;t)Qls)kK^(9R`k!BC6Tfu7krg4?%RXP9>#pSN9vWAtn+g;kti_XA<9+WaFnAW# z+O80k4Qm;f+r;KNx4khd52To`CUct> zRh7%YUiZa&@>W<4A_1k4=s4LKGah0Dz7&4D^-eRK9Btu5v(W0tXR9C2d?&1hMXY|L zrd}wOc9}+x*C*%;HVG)ZZF|&2cO}pzx%r!SdC0f*hjLZ>t!oc|_hGt!;@jmM7N0k3 zAm7GvH@R8=u6(Q2U4ahjyKMwqtVT4AbLgfSo1Eo#fXFdg1O~!j8BqUV5TnJf9qQcQ z2hwP_1zET62LApC1(1IQ>JNM=(w^iMJH{t=q}+!bqc3?(Uve)~%rfB{3D7Rk&wDM? zX}n~nHNqC6G!&G4MYKGRJ?}+pFnr=?P8cx${VpC?u=~jgd_MEY-Q62+mfN31*dr&~ zbK@;O8J*Q+;};wbS1b~1hO;E0#BbAul`bpA_+&VzE$jOPY%CdB0G?bonAB+$N)wAf z7IgGjV0iv|!?$PCDs18MYhtf1i@uWiKu*Kg0FRjnk_yMoq5k>sAH`;5A>mzvg4P#i zaU`dwN?W`R-vP>ph>2QVrI054>q(DI-6Nvfl9@(rcS1p<<=1j&9WfQLB|i)qY8jnK zcpyPdjQs|UFB#4(g+}{H$1sTar{^u@K7G(*210GC{GvR)r5HlP>s8&48MRC5pRGt? zu90#t=YInUx+H#m^ZE+u&XWywn(a`s&!=?r_i#)Ae*cal(+ansQ{#oPg4!CluKk6P zK&by)wl2-#b)~wvlf!j_WmY|F^Y5R1zc#t)Bc9*+sUO@{NbKlxjC1{frV>NTyX$*s z6AW-b&k5h3#)k>g^W)X~&&B(nI){1p5c;1Ih7;544xjkc5QdzAZlP4;-X%2)H0eXkB@w7+tMvY$Om zm9NED&SzQJ@0iT4855j#ac^|*78Vo|ay)dZKOCE)*7qbDO>JFs7_AI#46h>H@G5!y zl|#1Vtm$Xb{=(vVQ%z|9@j;xh{zz)8Orhu0p;uVx)v;`e{@L6}&y0)T?z#zn+OCk)mH-^Xz|V9;#C!V* zMg)IQbNUU8_O|4gG~PgMZ;$hU7DMoT`JXBR5a#{3}Y(Fei*LnQ_PA^vksl{??* zb*HNG|6JncOcoJ;zYiR$WPV1wZ9xQu!t+0^eACQ-ivRkp|Nf=_vG-T2{4F_u3&Owb z8R9EtDvwd3adLY&prFi_>MbJKG>Ki4Dmf-KNlkTT&5(?6K!H6bFf z#a&WoS>PbeeDDjlF2B;x64%d#V0lF(H1s>;A&>!)aNs5n;!SM{fm+{mX4>85sSc&h zDP#xY@oDNEV6o5CbeiPm*`ZyOKN+>dh}15gh$NYv|68*BJkM_12Nv!1>(~O!Nqg7q z7${x%O49ZA@Vyc$NTu$SXXA*+b(8k-0!rInh4CqxarXgHV5LlqY7XUDDF54^Vuct3 z_-?zE+>vGmo_Iup)`&RDyQ$}TOVo=?+%u9wXBTqDSJ1E0lnGz}NvT-~zS_91!O(6j zM`P$qe?_7)nn|#?6Wvd)vrootEykSgancYjEUxdeK>|*ziY*)&8N?b6aa9r4QHae| zIb(U?+nc^`+!Qf2GSW$B@LBw7<);$`UzEUMYfs<@7W}6kqpuf_&Plq%)husU?4hud7?^vvbJcvQalwn`bk_{v(1x&81nEila=$GMW>yH z#HTLLewjf;kiz~dwRXCH&LE+VVpFkwHzQ+a<8V^9Ij*)L_aR+Riz#Dnqne@zERng? zKPP&L8G85;I-g?jY)~sd>(ZDvUN-tAX4KLwN*IlOAMZ-?uP3JON^z_dNd6Bb&ajF_ z&M-he2V24j;}s@MFL||;E79NvzA+|D8kDHz&4o4l5&caT;SSrETy`g&%58{_!*2!* z7gMl=B_%_N0+M-LaLSt7dvzlQ`%F6HvPmM$pPEr%a9fkdws@^Y0ldgP3TS)h`j9R% z*BTlx9_hn``(=e!9vyef*B~HZuailk^Svc@xvLlG)L<$q5Kr_K&J&Mf;VT`vtHq56 z`(%8y5naTCfm0uebO$?w@*qAI!e*_nVTU^)O|y{;>1G+DgQe1v<*z*`tfVrL{{XA@MEG z9qhJ=tjl|tl%Ue4lOd(ROXz5!ZulzQ-bFzP)f=y!Cr}!`mPZRU==dy>2c2Wno&*d( zhmqk?y!!;?+?qphrqz$HCD=<}(nI+=Bj%_gG-q51PG_8^eIN6CBEpApt}jRVA9@Hn z1P;IAbJ}>PRgff&PEVPsIvn&Nnt;ogtT!EpPx1&I-smufghh3RQlwvpsq84?=6MoQ zc%|N>&ZxpGlKbtP%mWeLR6J9P&5#Al@X&3NCzfzHZ@?ly8+%E>b-;AstJ=J3B zdP5-Tc=0QmK6?WaSg*|uA3@rKMf}o zM=B%rro$H{FcRc(S9z>5?PQ~rl@gWA%t^yCKYJ-^iz3nMkMb&*R7CEvSZrpbI=l;| zoRIptO2?Q5i_F21eg346?H-Jb1bR6f@*&FPxl#kf>lSA&== zWRJf#@riqH|8OG^#rJS*ZR7ATkd;4~*Nych(d+TgBsD8@QrLoxuG>P3y~jc#?Ftw8 zPZD{}Socmov5{MjT?fLZZMVOKQ_Qg+>5{qoeRNp$TOB#*u9~(Nw#zKThPTZ-2_MjN zxJ)=KHn)fewgX8WGjPh1FDs96I|l{L+c@a!VHrB&j8@W?_*@_KLnCaUv3PDnod(zB z@8l;UB$<_T_D}f-JKyBOG9uG$9;vv!+6@NfKhMe|lnASuzJAzsX^L?xSpOn*e+lC6 z-g?tzZ$kKxAfQAm>1K(&me$)3A{mH=Bx8t28&i>(7+q2no433RmQ*1O0mrmjyW^1APaROeIu0Q9TZYC?`JUoeaCeU+32^&9cdnfFo)+q4Rj4#6Ww!$q)p35*nPE|O-CaDfl7 zM|Ou3T+|AQ>C}^E#^3yc0`;#4mwE#A=X{lfr9%5sgu_=o963fG7RIic8L5pn&_QLG z4nhJep?4}`nN5Y?f6Nw_n_x&)`;rih=RV?u?JXc*jZWp zN3Q0~RbXQgnU}K9>ba$w6rb3-r0!Rq?lnkBtHx7p6t?;!6sUc$GOpE+GTg2uTEnvv z*2I@GUsDY7VPyfe%2CWaPpzoX3)8naBSs^Y@f0#-*ClcDfRRTWw z*cOL%@l$-Rv%zQO4?YjdbgKdOqdH?Y{M$l67^rQe z#2>+G3Y{J#c5D`Y{>(BI*Et@DPh>f%j*d=D7q{J$MXsb=a8BRqp*>mWV|ouki}5;f zLDaS<4_n-u1g?2qDTk9#B^UU8j+NF zDVs^i)^LRny(|UB21#yC&-^!GjkX}c5~|D_1B0_Yzl?N5igTU_pR3#LNRl%j%J(tn zu|;|rZj&hI%k15BlDfAPiO-~QDa_qp%uH?quYLupV_TiWL{G7cYT%KXQrLQL3&w_P zN1~OtwhOS$!ZOX(ba2O|KAO9&@2@-Km9#nXGXiNkgBF4(ewWJ0KwJ-btjSR2Q4n)x}pG|Bm%N^xMEBw*DCtJ$&-z8z4~Mk*#ie^FsOc8R zNM4Jr5gdB3G^`&--II``JTPm?15T+k8Ge^Ds939r9|$93$t>XXZI4uD2+Md{k;oUV zOcgT(%FiY`z#vjrcAJn7fFWUI;a2zoxe8bgb3Zw}uUNeQ-nVeS%WgCTjln`qnP5UZ zmQH~nTTnrKkB?|`s)?*jB&uS?>>62_J8133Ts!Lv7Wlwg_%DWQ(k z-;Nf5(7>}H z&sxQ32&%RrS_*AX3$l)^AiV@WBxmRV!$T=Hmr6|%9p=39F+@#lyx(K)E=})8Ala(S zPnbWmnUONp#c6R4aYaI{Dz07z4xbYCrFB1>@Bm7A^J|bqYzrnxG84kCuDn&5slAD(YDY8g3LmtGkJHLY~F4U5x}XE z7(C08SucjPRO&gP=v0TtS{Re-Y}4-_aVd_0JJZ|>P_;EuBw4Q?Q*4L6k=)uy0=Gij zg-@Y0i7Mc)K21A{xaBrxpeJWkC7cul#S+q5!vhY~$DS$RgR`0vaSDmBq7JG>tfomn zh#39|iP~LWbzBNzQbnpW=E_6=7x^I(*WVf5h33gpQ#u*ls zn&5tfDfN#j@}ppbW!EqKE~?6@2hH@+$^^`@hEK$N)cgk^a)1dcD0vuiV+aNr-6 zja&*@a3Tfb3YU#%;8!W2OF$>OZUM&l!?0~j`7FD2&a9(#buf+Z`2I3gSeEU>u<7+D zjG2%N8p;s30-ANrhCa9h7f*J5t@s|PWQEDW&(YnLo^wYeH|aA`JyJ( zb4D5CiPWSjE2#=vbB@8$0a!(3dwm6MZhx|91;&W+K!5g!1DB>WPGq=N&1WEo+26+T zUuMc}(O-i_-#?Yn#9L)5RnFN*Y!<6IWRjrxVLJtHoawvETldIBLp4eO`#4g^5hL`y zc&Mt`H?dZq_tu2a7JFsLgH4bX`W1bW-hjUu=Rs1J>VR#EQ87PB($P<`n99k%~*)gOp>j&m- ztI=#fDCYVhhF_n98VjAGt7Bo)fz zo`D~23A;M=M5DAGqRl<)*8>N@qPo|Z2BWA`(peuxDhn3TW~%Us&(%R(NUUtDD04i23qG&Ou|W#~0mg?K&p-)cehSHrN5GQWx{%Zu zGiR@wWsp-j6?&l0Czg+>9-5hKJ(>G6(;`kNBz9G;$l!AY__Sh?3EYGRk>QchOtwaa znGpeYO|`y3xZg%5PS#T1hkVF=4~YhIGNQ1IogXZ2s}I!Scgvg65=A8iR}sG6`%D0r z2_~CaetvN>$X(nPbD)lhiC_a_??t$aerRduTQj&@Gf69V%Dd8~jO*_yehFb3=Hw4GgsOU|?a)Xf+r^R1-@wh%*KBDm2$)?R>Fz?(_#fEY{ki(h~A`Z{Orc#$* z-j=44tP!r-fwuQWW53K|Ig&E466+h0ln&=*Sd4rpL#;2L(8wF6mC6Yv*~$jBY7)(x z0VZe069;rZ35>Ak9c*t$fW(jCSRtU=2P23eJq#%cuBCu>r%+Cr21%VSjv6$S3JHz5 zAV(Ej#ursPVjl35I98oelhc~|63OaTxe$q0-5|&A3c(?V>r5r`x+66z$yx{P%J3nx zKFUN_87dRnmPLi6XKzF`3U$I@Q7_-!%uoC$@AjV^_-|L^AKozJ2e85`bop4u>>-*I1vL8%ssn#bxS*tZ#S0*p z6;>3@mz4Y%_LB%>`{4vGfAeGtToB|SHbS7(ba(X$-vyC*J~Bq#dQWD9Vu0^fLz84E zPL`Cv__7Spt9V@>DPx0?7Bxx(F;$#&6!{_a+8WsoDa->277z-@^u73!RYr9Ri#ZW zaJizmQWu4p62VvQGXj@i7wH`iz>GABSnDi}rB#iRg@tw_r9eO_S)Ru4kShY`XD(GA z!FATt8o1l4^*l7)A3S znHC3zW#4e&_mE8yc%b+C{rA8ON4&fQhTa3R>zq}@lbi3LCfprzhrB!kit!#b(*N{^ z6fRaeO^cTsfI;~A%;`z|tCYHx$gPl#HI)K?53SkPb=QtY(Ey-XWM{Gwd{eyVZ}rxe*W z6zy+_iD0ul0`2smx6#TBT+ef$l6W5C3I1=W@}c7&-zdN04a>Ii$bmC5YA9!(IJBvb zh*Sj!dnHxmu2B;xvO#P8bI?(nM1f`2CiRg_z~8bq%qHeG_c%b?g-WtYeOg|A1idzJ zycbjEuobA}kqz~yy~6;^$ekz`TO5LndutB^k=0s<)VSu9@;&10sFRZ=emrk7G+}&W z!d7>;YTAP46GTW`9=9?Rlqjv+h1jvi@iqJbaBlF=wZ~s)sv;E!5xT2jwxP8u7(`Z? zw#K}c>F`I&?s9#ZRGi_=Il@0PKf5T(sA}rS;`2zFnOl$_L;c+q(7QNbS@IHL_QL_p zl09rH8;`h(Rov0pvhj?GlCzw{?6||S{-ygI5_8^#8@O_b2C?D|Ne83&QVklE5_i3H z*qz4hZwU1JGGbdbb4*@tKW5#An4QuSVUwjq5WG>6$LMaH7))7Tt;2be-?Vfkf8$V8 z@Vtkd;h-lMg+(;&=_3YFimOR!l|BvAnqAmVjJy+~a?_iHPbuGVQa?E5{)roy^SsN6 zSxSo0ZD4&$R{dmoz*B$;V(J;FBuh5z!!pYY zKL|!8Z4Cie1ubZ3BTG3o0 zaK?nf^SLasi;_^Dv-l=_TA_4Rz(5GV4O~?vUK{ClJrG0l+6sif!WLBFuZ^Xj3v)P# z9%SNF)oO&Js&5y}eFxCEK@=5$^EbsTcMnlPHytN5@^cJniRjWBFBq6P{l_fqL6)GD z>NJGsg?KKX^qOO;>l_XBg@Y`+i8#}m)UHt{v6^TB19Z#5FSK=Ta54mxD=k+X1;;!~ z483tgRq%-Pa#J6%HpvI9j;RbwM;+OWV5{Z+n%QTNI#05mFwLS;ajqwM>PgA*dKd!k ziCJPQhyZk<8xUb!$nXr9s=L%Fq~Tj%e}uzkv1rE`vf8y`0Ok~p`cQm^zftpw&eL~M zcX*(8%RvnunZHA2d?B5k_TE!;thd}jASey)diXPZ6!OZ;@Aw76+?yNNy6^)Nm<}!rYL8M*Y*(#* zk-rAgMnQt0OAL$5osX&#rBGx(9e$sfXO*fNs-ddMCn|Xl7RvX$dSx@mb2vgY@w2if zpWNJ8VWtW&*TudrD%G`02}~4%f3EW3EnJjGwI;qIc^?yLgYjkmkOQqpJS>Z5gZ^p~ zNVomB6|^qx)jRstP11bOAc|`s>9Sn^a!TFH$U4*Vdm zNfRuaGy0{m>WNVf!VF%^ZqO!0$YQN&*Wd$|T@ue;6iIMg}uA{9$I(t`Qr&N$G^HwccBFe+-OD#1xDH+>%yv~w4w%0}1^-})dA8WtC}VuK%=t3w z@H&rGj2THU6+{+=)zQJVm2TS(+%4zm#Q?4n6-ZqsTJpnV$Ws)Bg`msRImk7FOSC)iB_VrU|2#8A0Oh7pI~ z$lSw36u6@y*I;RbwQ7mkn#K_llRu>-(1j-)H$x!<%5tv9WZr~Jy2B9Ksd<@=$C!nR z2|`GyZ|tDa!Jdn*iq#O%kfP5E!>PQQ_Ut4ChkEj=mc@&N=sou!+eOVp zQKbN>$NSLf%T-)(Pc$HSOsi%YEV%ewp_1WA-VpAQrfIvehQP*HeCg-aR$M=QkTJ=4 zgIDG78g)*hLA-AtM|ofgJD;$>ELweVm|e^n?+RCwjf6k5Q1XD>A9=ndfssk0CVL5^ zBgqBkBRgRH7}*pRfvzenm(sp#K&O4EKm~mzQ5|RTW;n7ml9~P0(c{IN$+iP$i2zSQ zO}b&~_Yz_9%7T;t;?8c95C?xEv=u7ZDE0L6tYGgQX{O3YTCKcnK<)8`t&KQ!2M+cLU5e=IU$Nqs5HeO z+Vo?;M?F7XSxwQh3sIdjICMSF=S|YX#m-21 zH(+?qE+|pVd1up-IG=QxJ-ozl8J%^N4 zE8~SvUFAqux#3Zq+R(^#*M%o%-O`7n% z1cod^pt+ssIH|lgM`*#q1V!Pp#B&8}W<~%jB3Yllrt8s?W@Oh;4%JsC3&J%Vjj$}#gY|5pbnRIsW4hVOSA9BOmcW^rNBi2=hZ zu?!b;(WmxA$HxO2-6lx|`frcL!zs{Gd-J+N*5ajm-W@V7bI)n^N6+m**y3jO~Rv09X z_+y|K&x&%`U8^SzthQ|(k7hj-;(=*2@cXI!bO)DkOe zl-YUjMpfseI?;#0+)giQMP#}g({v+8RHB%b9Pj)!Bbj`zz~{02U{LyfxUfa??#((C zUdw3#cKP)SInHlsGFA-~iL#l8V;MH%!V>N>QyAid*ijw!RgXg=wnwucGLGTgWze66peBSoa zN6n`W*s9o<xZ;sgEq+1Udkf7cWve0Z*Q_JEOU+Q=|*B|-sVi@#bIW}fQEMh zTKoU)_U8G5+gY+uwo~@lnu)81YZPgY+nQo+jCBt2nog}wtrlNeUzYuvJ6}v+j%s%F zni^l^hul}$ndY&FnQNW#A21ySMy9CNp68ok=Gv|VG@Z?<36D+9)oo1u+spQQabh&t z2&BkM4dpoqq|m1*ez8Z+m0OV|kc}IPaj0|~(*L_yg=XD}5?VX?ZS6g8SO#(A zV3*F%u#`gZUK`?Ge3gZVrTOng1hlHvq_3;0GU}}cBrZlMkYK;YQa(S(ZJrHK;V@=i zoUsgIiDKzpoMqvi@t&ZJC2-QUMs>CxIM}$b*x3#qD5WrYaeV0kt-pkso%z@@#IR@$~!Y*M~?EH{}IT@b*A|Fx?nkS|) zaYw%X-Na`T!V7@ik>Ig{?dh^dz<`w9CKW7(Co_3l9qy@(bv>d1>R}`PwHhC5d#NoU zgG|im=Wd5B4IK);r@iD4+!+32mRWfJ-oDvvP9?}jAtEiSe^o}JQ05_(4TAWcaruEK zyMjoTO1ExxmdeWqf;C{ZirI|ghM(NSfxd}fuBp~m+&0`Zec<7|Jmz>n*JX-M{oFWc z^$5#`evq=3ZKLABG$Wvlfmy;9H-3ALBcLy*C2&6+DCMD+B>YJLksQ$$!jVCVwCzV* zZ5KmN*-j2yWQja2%>s1iKIPKp_ErjAEH%p@xebBH8QQJGbaOsmbduYAN%?93yEXV& z+^Jl5*-hVAhzHB~mkO`Pu-z)(m*cG1v)$U$uT& z5PKrBx3P8tvdKi z;WFC7`tFNKxLaO;!XKazv2qP70|gRtn~N^pj;!YX?dm`hppB33kKV5{iV-s9;e@4( zHcVok&K0vcAHVM1EDYGlm7zT91`FD`PsMK4qI7~V;QpMf>51Hi{6a9r)2G9MI*&wx zpfwe@n7*3p9a>vFNV@B(0ow%!5;IT;mQ|n@3>5Hab5tU(0U;;q9)+%m!=oeCnJ~UO zV$H-mMEF3T$E`aY>8>`dKbgJPwso0pPIhzQ1fCx!idr(roe}NtJ}cO=>nBDsc@BbF z0t;wus7*R5oT~ku1JXWRM)_8r?n^3Q=ZBpmo_9^eIZ&hU7;8s#u^KD%&}(vPF@j*oA0V!{9taqjcJxj1 z>{*$-GnH03-zXB&~=p{QBBob>JklkSh-)r{AR#{Ely3_jZ$#^Yg zsru_^s>&R-aXhTpL>CRR2e06ZKD|Dfp&mNG7L>2;> z&etZiQ1Sx=tEjCEw`*TchRbKA=(1bECUwoWR~>?mh>VHYd%R_Qik!8qU(6p4%mITi zPR$Qy$aKJzDI*0Wg**3K zgOtNw5>+CgF{*g@p0dSOP)29mM2ipX<*&H{0iN$BM(1NSY5jd(HUrl~w6&$S<8>d_ zEY6w|$?T zPc*rPTH-C|Aw-FETC*3X?oh)?qh!%at@^?qThY-6(eKWRJ!C)(}>#wR}jH zrJ|32zFI7UOMUs?9xrnRvigW25^1aM zI4(u3!uY&IONuh6Cu^m-4X^1Ei>#~$`PXRN*;>5I`2s7|ug^@y5Ydf_ky4vAL<+X! zHs&zUJ<u^4x5E54u$ZaLj2wmJ+L46T0dqv1#L(LwWjnrjtOqocqnD))}M_Ns~PoJ{3 zAWm~nnCC}$13@m8Dkw|NQW_h?f-0{blepr&QqWnLiGzax*vnRZ3jAZ3Sh1lI>u(oM zZu7oA^?&*vA;xFb#2>?d)D*pFmFDSoDB5TlfDv4HzttMH8Sp9Bga(cJ z1r5Ro>?0g8#SZ=}oBU?M6Qu0rm)n|HfkqWug;^dSC$Okz4)v7B$~?c!h9LQQn&-mL znYCi8pyyQ}Vv&zpsSHoYq@2g&YaYGKyHEUJ>h^U{%n#i;K8Uz1B6${%TU-%qofaMJ z?|H%m>_*F2dvc|qmG^A3nx`>fW$WWo=&P~htaV!p*!J!~k3SL`>VqW|o2BwmYWL z2;V*%PFpVdX;0T|{Ow{8MBrp}Yg6SqkQROfZrz$SxqBjbQhr^BSf}m;oa8+AZpMT( znFhT$8rR6U31%V{UmV+V2t?mLflhW4h)(S&VZ9@%%;8tZ_0o|5r0QZP%B{8hfhAS) z6GSpDshAj|wR)pUQflk_Th^R6#MI7j;AUAW8EqwOjlmN#a~a+nBq(cvL3)Ab$I<#+ zFAMt0>fZ+W$7xH5d@-!NU2{0AnY9`OaG!9pY8Cns@hq?MQ6OM5F%sG3gnWY9)KHAb zo^%HSk-0Y))5NxeW@1#CNeXNCy;@@iy|QDRRJJ>n>5M3CxCgBbG-q0cQZMoYxdPDt zDP9;;DYK2C7wVnq{xtBK{zUN&MARA)5OTdN!`JF?NJ~>812UIWsMrIW=}|noTa4)zKiSx z2*z^aF?mjSt*&QDi5>10E(SC=pDKWi%js?6r%jzeaH+vop~}kUaR)dRQV;QwF`2qE zSx7Vue3Z=EgBV)H8V52>RTaE+tU5|B>a|;-@4tX;p9M@<`_8iVwQ(uBP&g;r;N=r) zL4&rD>MB=6!W}I9kbRXd5US?zpIiuQS**o6mo%a9fgXZ5xY>h%?~v}S zo?u-n9)ptoO+dbMN@bv~+1$CVV-u^;kmip7Y+Cgv6#Zj>&$6Wlx%Y^819B=qaeXly zrvz_nyBrRsuKJYbaUqhpjgM~crXJiol+Bf*^h(?I7pu~gTr|9x}xC3z<_ zou+Ca1w(+j9f&EeXUq!PmL-ji(hvJ1b#sQ;%dKDNz?@oHgMy@7#$H$jp4_(Zk6GNY zO|rs5NV*o;{#o&+0n~>)bjLi~sfj}N+I1q4ZW-@a)_1P|@HQw}DvLJjUoW4g2$hXM zX+()H9FpT!@i-U1Cl(xw6c=Qr{2hXREjkOz&s7$iRsPQTuz5w6O4U+qhk|A^`R7Lg zrCnV!p$EhM(ixAhTf%=*Pd%;E=CKJ%lrdrk?|oM_UE>7?KBzNf#AY-64u2p<#O<)>1_M*>ojMP=~Kot3n2Btzw0(i{Z_7x>?S!Nz z&dh(}L5gMzQbfxXZD8Bb^kem&@5^*3HrY8r{1`G_A<~+O|6tpw>^=z$93i_V?t-Gh zMl{%D-QIiU(+}2ph6}`;Nk$GPzy2p&XBm8UYWGw?n8)yNHzCw$a1T~BeF0v}*wiM^2^#at~S`_8iZOaG?y*i`dhw*E7t$L7C( za`*q(2RPqr`1hhrm$U7Q1cmBbuTOZ_2l6QYW$&-8gWk{X)6cj{Y%jM?_sitpzKGlZ zdk=wDzFP2g-6>um$#LHWm9uI6m7e0=mA>>HkMrU?0ud||f6b&1N3P(1)1@TAy6FGO zmahJY5F*xi8jRc6T>k7D%UJ95J_bs5;nuNxBUti(X1b-r<8DuXK?Mg#A>vQ#PqL2l z&qy4c|JC=0)3b4QYqRJyBiF{ly{%gtV? z5zBq6((5_RHS8i`SLXb{E{_6uA)(taXrxVqI=%?=1qOQFL&JZuwVacoUqgB zg<~>Vx-Uj8JuZp)%=;+43J<%?|3PtSAQ!U5@o?{Aob&t-!iTfpCl83}PIftFW{P_V zHqrBBGoLm22sG&oNAXyuxljdd?z(lU$n=|MyF%yJov$2$NTyq@m#^sI+iT~YnJ)9U z!-rs618oBzgBBNiNTPaVxI1OnzB;yMP&M2chscIcSbES;@qZzat9yVnirdWe5uKOh zp0+?mQrSEt{mZW4u?$)>Vi)%OKmdf%&kS4h88)Q$5Yb`nv}2~HZSGI%=8%(#n@WFA zPk}@ZR{t?i2a2u-sxV$`P01B>KmM?ix#{bWzIi}~>%Lj5Z5HKriw=?8pCc{^Ej8WP zqKohjD!fz6l6Ke3@~cnXjC|cOGCgTivV6*6}Q)u!=UZG1VlJE^aTPUUg!Qo=<5P^4Y%tIdr6kU9@Sv1?ww`TZ9e z%Q>Cb4*CUSk85p2`mEO|vI~}6#a*oC0c22L6*2qf&S#(+TR)$mg=)7s!ZY1EKhE47 z7VVOobsTAu?#8V`r5}o%rX3Hlo}$#b=fTHr0@OPlCi6{jHi4zDRSAEv+bop!MuPbVHN^lpVX z2z+P}y$yK;GX9WqEaf@2>$v!d1D|Nd2#BZ_nXbIZF3&lG`Cgom+r)ERcKWt}^a9{RwkNa_e2DEf&crVL) zav@84t|f9@JeY4uiiAbXNziSbau! ze_re!Usz3pAKbb(@!9rHXNp znfvj{N##tmG{+UqUtiHDGr^6G{a5L*(rj61bGNN?2{?X^eSlPLN;A zTzUAe&$H9O#KGiq+~j5ptWEgA;m}N!Gw3AV-$Ko zM>cFgTUPN9)n^bU<7G-O>;ZfH%{BQk+>|}Ibagl^wB2TAQyFFc6Zy{1=kZs^n9Qcx z&373wT*F_CI9h}BK92+=;)Q26-=*raOw{g@3Al~9&MY=UFghIamM@-EvVdJ12!taT z`DSgiO1>yK*7Y3=IiN2K*zPk?fD4;;r6h)3=oqb<_%b~B?Y)W*&Ac@`hD1u)>Jp+h zIp`hOGPc-NB56%C7S(Wx=y++@Qq$wOM+uFp-ROiYyD1SB!sR8`T=C)hBBgtNwyH9Y zO<1%LVjjCJ@{+$qhrau)6x}N5GHfSrNnWzHa;u9szo94XZmn`O)u|B>9i9zetxO4{ z<)c90j?{jIJ&sQHAa3Npc;D-jbH!ofx#j$Y$17H?&!15Hxl)AKzJ{T+?bfqCcv0Av z8=-z5y>vbl_VgWow#N$z0#O+o8m#&gA_YfB8t#mka(b$N_O;Rf%+H7J`%Dm>S1iae3#@(7R1%8fTQY$qxw ze%!K2f|+Iqh5-I)s0iO1RFnFBYDypY`~4qdZrR*0?&CLVG-o;$eu~tRdN&Qpiz1Wy!-U+hUV!EezP#x9DOkzdb_~Fqorl+aGo}s^7mTHf8 zsRmLFt{O~|y?=0pO$OYcR6k7BFLZUg`Qx^q4_yL1OVH(aA%HukSLnusCua_6MeNd4 zBLVC_%HNeDa#RPmpo=#@Soj4SjP-Q4QEk-a+Y4Qp>&e#UIf+S&y&bHRA*dHkw!~5r z!uGuZ$O8}f_}!@u-yd%b!Q;`=l_8`<&Y>XFOS&@ii?z`|Dz&HIp`_V>lDE-U=$ns@ z&*qU>4`)MlMh1Ua^s__mNNznnBkoaKyZfQf`2IkKm^~&N*BQ5B+svYmDlw8)1on%V zAH-f;NYKxPC;W~RByK#)bT|Ff(H$Ho7YNbftg@;1~ z-E3~IvB8Du^ATE?XWq7M*GqB$De3MQbI#h%6s3(tTkJAyg>(+)G5XEiZt+0BYlrIQHv(zrbp`NfgUOLlYAIu zVck)Rnz`+aYE)F!gIC$+v*0aR7))^{DF2pu+16y$&gs|IJu#{cBZ4d5aZ%19G25U;K} zQjw2@_LMpj`SZvXI}h2&OUB%cnZZhwE_lT{;$0^b^Ghn@M(>+{n@r-IIey;8A%G94vCC;r zRk6ot;)8)7n@pd#!qpa~2=otWrD(Oj!mPMo)5pV@!e{bv5WYiir#7R-kgyFgRdQ4A zWH6BoleCWmaI{KYI58ni_R-+z^w{m-8&OQ$k8uJh_qd9!u2G3(2Lq~8Q?F8nW32%+ z&FPz%?*I|dL5q5-8(&2p__PI_8vXk zy1=_&$wrFY;2gdRO;x9ZZR%MtVuAtRGEq{r!sG;bn_V#`Y1_51)juTeO1?l?#T|M5 z1jzuO1#u@@*DWhGDRwGim+GPyX}>BuhXzY9R0tM{D>Q3AiR`m;u#2J38l%I%A#a(; z&DJaGUeXv0K<`Z4EQ8g%WaKY-abDioxxpc-b2xjM(2qNXrT(z0arC|UjAm$^!khbZ z&6A&--_2IB80-T1)uI_v-@FYZIQlLXzG(!U51;iODfVY54p^-GcBM95z$Jm6-!Ym6 zKKBRPjr_{EA*?m9O9e;KhceZ^Tc+G7av9n7{{CAwyM@bay|)^c0Jtq~G?)j-p4m|E zJJg9&ZgngTj~oi+#Jmot?o;!rY0_Qiu&Jo=ZfA9xPKaoTfC}4wJp5Hcbro4(?Cl;J z+Q~#=FnAsih*#);eRos8z@+wmW=OQ>dLGSef}8CXieH@249`$ldpiw-`X@hv{_m=} z&>LcsPg^xKsL)SpASNL2PQQiHNQ--Mgq zrNX}PY5gO?C>k+$mCpaIkpJjvLmp;wWE>F z*%(wq?9C-Wk;^Y@S`qB8t4?o@BT&c8X*p(M+pw zqL;pSQ-w4M2VU{q?((sL49{MWRyyKC~&;~8Va+K^X zaB^Hf-MmET>Pw0k1v&*eUC#>r%{n%ibr52v2S43$xQw(-EiVGH#5`y z!Ph*71=MEb_r^Ox?8vWBhX19Ztt^1)elE_dKW)w@H}dR;xa|f(x4^!MSf`$TTj(EM+Np7K@z`VsRnGmI z=(`^LPmCG@Ko5^31mS3nd6l`U6T=L?wf_?m9ZATF^Qsz zI(ZgP7UO8~f=snc6T@QX0LSAVYnDuY$ZH?f>18?6E^DeZb`0|ekqyI*=VxAQiSxfT z;g4^NFk|{B>ipC9x8nm_ZSr`miIs^Qac3-LC8J`UJUZ7LiRFuNn>#u?J4La7*n>%4 z3~JfdA6a%(ld-&<)7d5ZG&XYoQ$ayN-oUzb;V$V8rj0@*`x!`iP;M_%YezX^E=pFO zMf&7~4o)30j@gcaP?juSU9)K3N$vq_3(IuS=o@lje>pJf>zvB7y;Fk&Eg|8GRt`%f zAT;0V-<$kY=;@cE_PF^9Bcg=Lf+ZNTG61D%;u2BFj(HDw!QU^0-RB7F4MUqwjt#Rq zvqcZGZznxc9b}n*5Kp=ou=RxKS?Bi6^-3VUC^$Q_ylh{)uYc^$t1!|BN;09gQ7^QQ zqUKrInw|tSFPkph?Uq_r5e|@|?AE*#&nZlv3yHOPzdfp4!sGH`_krVD|z^Z@6hxUKuk7Ypkl};<&^)QtxdOQxY@~6yK2NuJkCQ6|n(58Lu^(Z8 zh#y2U`VKEZIWizzFK(Ofaugz*3Qm|p2rKjPY2nqj7qelKRae(HPltN0svjw@8(qH3 zW5OzCgYhI%j)75RnYV=&h+lp(Gsx!)dPpI%iy00cMR%K=C`}*-fY;cp*{lhbGJU>1o;sxo5BL{K`VP&uj@fY}Lfyo~Uu?dC&z@wqh<5bk7_#mYT-scvFb z3VEHjc?XG)<-t5QN@KZQl$sJ%Kd#GSLOhT{5wkEOJMDTSVPu^jzt#$<*j>ZJWFv6giY#iV*s7%e3Wjx>M2sUpwF-2 zwcccY4(-ri<3bWph^Xc3VL!$f!Orp|#%LYEa02ySHs36@!C&DN{r=Q8ZOb!K<=FiV z3IP(b8#OM1Jet>Ttf;v(D~auVFGp#74g9o2R<~QoZ1{sN8{K}o+mfN)FWwc=AyLbhY(e-(IZTb(Z4t~n$4A8dluZWX_{nw z0ymAz?2+m@gJvjTNIxstoxl6OE4~>-@M%V$a<)nxG?7jOKxco+H3Al{H2HPupyRp- zZE%RfwRYt`o_xd7r{r9?e#(8M2ym;XRcT2m+UqweJ=v&V2z;0JS+64T3;q|A7B`65 zk|o;HaTu7w0CNWnY+kH3Uzpi?1+{CDVov?@Ed*>n26dv{#^4g2(`9$R`+!q!8y10R z9*$2sPrV9%?kIs)Qp`R55A`1v(kf>+14+l(IA2g4&+IIlpkrZdHoC3sW$-P7X>tO5 z5UH3=%U${R!JIZF+NoUF9Xt5(yJwxgR3ZgOXm=$h14L&b_z8>XZXv9`)x{y6#d9_a zPT^G0%kJz=*u`Sg#E?UvZEPYVN-IL{9{TEO_=60~%y^lZ#E!zz{{w^px84NO}JapmUk@8>?{840Kx+4K}>%H^iz^lB9sN-cm?Bcsh@wnQx@(ifo z1JEJgJDHcDMMJzcOtog!a_+6FtefTn=#i2HQT!R}Fp4=Fff9d1@+6e^KTYI+ z+01h{VHVE7{~vagRwO0Q_HkdUONK=lG8B)zFp5=Cm4F!~LM_L2)5h`PNtrh2?cfOE zHn<1a{=*%9;Jcl^3D!~a2=Thd4 zM^359*KRMQ`t8@IOZUs(NXiL*eMH{D#9|tKi*>%a{J7AN7pWKBp6%0&O=YiMqrX2` z%hkz@*Ccf6IEIWKd#nGyXp<$MWEMj_utdFD7Y5Z9?-Z21B&dfaIGf{ zzmm@j7u$)8oA&821R3Jau;hwkjZ?_v@_vM0Vow~pGo&DmIg+nnu@v=Qd0bNBDsOUI z^ED8q|8BAM_I-Il$&c4!?jqlPlUU;}C_!c870ySsX}_X4W=-&532ANZqdr{6331%b zTnGkySEHx#4i~<=O7nrUKY7hj3Fvh=n^?z_Bv24DZ8za&Xm&C*!fVsyP2~O-_U5e#@jPr~ErI0C16rAK5G#RlHY6X*5Y-l~Rul_7&5(>xj_{G?t&^^uXJ;V-762cKX+@qhPzP^IXh5Uu zi(mp-E?<@cU`3X&!|9tC*6!LFSEv2bT*7~>GDI3lu>DypRSB!LC(d&!^52-8-W)=s z#0;Y|Fe@t}etx7)eUiHP-(d1-t+HkIG&=^8O`BzQ6qUQz?1MMQuzPo8?&w*L>`)j* z&tdvoQrcag{gFDhU>Bp)n>Id9^BVH9k_VGIuJ;@$Koa8Uo$4#>sJ^jMKQSodK~Lz! zR?cRabk$-gU*S{Ur8gebBEitTAwHt+!?{x1tJ~0g6F%haOfB6M#J5ebCV5nLGZExC zrvjBLOWnyj(xKgv-E0OqjHozHl%;W6oY*f05|uVSv*ABjAX5{#$^}|D`-%Y>9D{+3Dm$s_?ca1PioXLNmU%#+Y4=Iziul?dKu? z-pTvvFlhJ6%PN#*jNaRwZb=g#-Px!n#XDYpEAlnLi&#UyfjWlED_(J`IE>X)idR01 zqPJ70PaX}981RHRZ`K}!Om#CaM`xFazF;K`?!{9xiP6PCG1qTclikpK9!jcatYUQQ zoAoxO{Qb~P@-UK(C0RGSY_}C{n(gG3)Wp9%iaYW{Lqz+AOG1|cDOFJr%qza~e)|{4 z4{lgmS6!;oi-nnGmlrFhUvtkMJT@Mbi)HB-9o1a>eDe9)e2{B1iJuz+HtFq__lt8l zv4m3)-|1P2LHRnlfLoW(-A7kQB@W3!*uCTHKgpEnQFX`p)Fque6XnU5o+52S53t7E zGK39;JM;Q90aEE>{LQtpHHViy`d#kPZD9;)^Uh6ZK>8!YBlA4YDyS8wQ3-bMgab+V za=Pe2?=3%KiZI|MN%Nr<5jLP(Q;dN!_#0J--_@@Yl`z%#m05>Nqe9d|q3drebP24! zpug}zl!u^uor=t?r;b)1Kl~NDG*iiUygsqVWTX>!UkL-mafEGu4h%;6{ebymRC!LH zCq730(3XGiLzWg(oCsVWyV=pCC5wIoGZ@S~xiYWjyiMVchrqeNx4RSPwyJc;*YBeo zqJ$3uBFA?P%Eg>W)|Z#0jrZSCTW{Hazmfy%lP=aG|5@TgwoL(=OEsZ`;Rr-Nt8W#j z?L(yEGjm!$P#GjJyYt>{<=&fkIS0tN*ZW`NKxQvkCV^U-&R~S#YVcDE{VS%kS<~qI zh<=XF+td9SW>0<^k_HM2l=w@6(Ny)M;;%+X<-N@##VFgquo#mY=Mb}iz)OxxWt(cE(Rw?UyR`Xd!2FOJVb6Qp$9 z7-D{9J}TSSAu2^`ejwZWu3BR3E5Z>2$@nx*xXKsF%SsMbJex3N^w&7AXheNta8MHkj?I6d5v%3H)0;;jYRu@Yy~lJcHDOb2E$X@-^;9W+!pgx zUa1Quw2~(XKCXHO%{tWc$L2q{8tWI4)mJ}!Oze5E z%>Cd>%*sQ^@FI8K@sp^g#^R8_Lf+b1!jkapyVTNcyq^{Ea>1Y+*-s87b5>Eb7FWWY zxbZ`-VL?RUjY-788rhG)?$PqT`weD|-)OQ?^Nh}f5RiA`FBg>AI5tTldMc@$ysyS+ zB=l7Iy1sl2DVv*OUtBf=w=4(4F5r>B8Xjn_&IDsfo`pklyA4xpGhon^_QXEB^UIx$ zC0dJpm5#w=KO>NO)cJ9vHni z8dr)69wL$Ercb%)#?C$}-ioIjOA}UW=UvlTt-J^^C$Gn@ zqCP8Cs~sj#$U_8xUo1rBqaanvi|bKm)n^x%ow33)`19%Y5c3I&PN!UT)46z+9@aKc1N&j$+Wp26SMnMd=tocu24S@*# zTU}gooA0~3hTGo>&S^edG-nqgt>!3@JUBr%a`PqB_DDZ?b;M82@*p9kW7mRG=-uBH zUH*XAx@N~LBNr^nW!DRpPa1=L54w;YwtR{1K%cMXpM3gWSS7?oNf$jXX&e*eN=~__ z`;jZ|9al$FX_D4|lrKdHTu~rNO1Ezx20C6)jPBAb<9F=&keJBF4Echv4xE-f{d%&q zPB#zVth+KCt==|h?kA{~{N>)0zvm?fE0Q3$ksDxoN}7>$XC3&vk|>%)vyAs`74hYZ z=K{}efn8&4o!G*nw{pov)z}xUw3z(RbxQ2KM*l5|!UE#8v^Obtaj{l%QW_Wjb>S!b zvcEECq4g8yk{|6XbwoPS!ce;2c1A^Cd}f4Ba<^kL8Mia{Lme+RNMdArXrs-`SB zouEU^Wcmbz?yMvvUmQ;Kv7Psl>f@66bK*j{nDEmtjYx0jbiCWIE#S=^xU{;{r{51V zoV;#T%nO6=qBY#qFIEHx=h;6j{jb*RKY0BfzzEgdBmTwM_~#ML@4C4Bza@aPG(R`g zN5zr-W+uj36_a7T6Pex@m_H7F5gR{Ek6MqJzJbaYl&SysQpMP;<`2K;=(y>7po~%* zGP6{#<%F9e$D99*TVhZoQ5PJ%z%mYwRdaK8E$&+oO3iKeFvQw!&CYdytrjk<-tG{7 zvd_6_g0hdBmNFv#t*$u4A~)J}b1r~@BX=sWGMrwW?Nj4b#Qam^KKL_2^qw!_U7Daj z3l<27B^D`52Snv{TxcZDv)!f;>z)1UAQ_4EJtf81N)_-pLR1G#WR$M>yHXGb%V8iu zy!CKRnk;Q|etd&344FGX76C2%J-dnaj_9rUG z?aL}xAG9vYU;=iePZ@er9-{JSxR1K^FIeYSpQC!UyBWgd#kTBx{dMfLLLlJJS~-gF z4@R5AVsb(k+LXJ++D%o_B?FT8A}aBk8Q=%_5Z6AT-`ZU9HA_W?v#YteTcs8($E}XC ze}F*?4B{w8a5J7`;dNHv4aErOXEAf(&Ix^|-jDQYTG=%vx_D+)d2PW^CclUI7yRf# zbiKHM0~)bec=R@gzvY0HHG@sOODNw<35xs)cR{aulIre*p}Lrzd7W`M+)TINd8FiW zeHvSoWZQD{V5;p}(pQU)VOSO61<^o&Rd={@P!w~R3Mo^lN)$Bs;|eQjZcn(fr!p2R zLu861e?)%PYSU$p5^39R5_%M_5>;!dziVY;IP#6j-f+~AUADH0O`epion1SRnKMfM z)15eEPaK3y&zAAl;PgzvwRH5O&)mf4bAARZve$W=w5 z_buoD z-$p6b|Mva)d-GGjmJ$)GJb=ph`QijV1n>Y?ir7w97wrmIGv=u!u; z#|P9ZuL^}({1rtJl(aD zjq*Y+UOYIqRIh7>${PY{3!K5Rt2prbsW+Qq1Q!;`e|Wv%^cnVCAOI`sUB$URINHPN zA=LUI-3^m*v@a4T-Wk>hUW$$?N(C0Ud)ZIL>5mc1dx;Hl%3vQZ;+{UV+$F=HP5Oqg zJyr~!z3REEiO#T?!gWhn3}*5zu0@qs`IieNt+Ip(Kv1F9`?DWFq#_csnUl;U#8FD~ zI^}BK03K-_JP9mz@uwXbq-8q@5VC-u9V38QC%psP7i;dxp_J*D1y4`nBA6e4(X zYNo(23OdHyn>`ri_P1}wI^VTRjzcy!uI+gb=bHda8EmjMwQz{1&d%xjAErt~*%ReHm$eyn(jv zh%^Fe-~4uzi}2QlO3JpQl(hxQHm9hDR5{AKOz7>W>V(>z)ThL~+rDwOaPDQPOu=Ce z79}r^yRC<_Z-UUSi0@ggbDn2X&A9;91y6?R#3ER8r5Lf;35axlxIJq7N?C6N*&BCP zF+ytNeQH*-Ij}_=tvAn-Q?3}{Q@()TKku4(jK9-$AUgf?glm#agc%vEs_Nh~E}4yg zdXSS1If=qoR=stCSR7IET%14=W%~pjfS1_&;8IIjecRV+>1`05Y#emr4Xxbkg(eeO zD(GwD)6-Iv?W=LVK`#RVlRCmxvh){zj(w)r_Yl#i-|BW9=R~&&Yc-1S##(4+7dC_J$ZmmG@+I zFm-P@ArivhWb@#QSTn&1Nl`5r3<7vAsh^^SteUgaGVCTRCEHiA-j6U4aPdmK{@Xs9 z!dMw>>^5Y{rFX+Lvt@newEC#vby9hRIUFt7whIMOb>M;YzLJ{;*|qGdIeA;HJ-m_V zEiB;3PDj(0zQ@FY^bYq=c>5IViln^iMQpQ6B?EKvKs&gTU)EV~#X%lUhh?dtsu9xj zxr^yt18

    - ) -} diff --git a/vscode/react/src/components/graph/ModelLineageDetails.tsx b/vscode/react/src/components/graph/ModelLineageDetails.tsx deleted file mode 100644 index 2baa435d12..0000000000 --- a/vscode/react/src/components/graph/ModelLineageDetails.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { isFalse, isNil, isNotNil, truncate } from '@/utils/index' -import { useLineageFlow } from './context' -import { useReactFlow, type Node } from 'reactflow' -import { Button, EnumButtonFormat } from '@/components/button/Button' -import { EnumSize, EnumVariant } from '@/style/variants' -import { EnumLineageNodeModelType } from './ModelNode' - -export default function ModelLineageDetails({ - nodes = [], -}: { - nodes: Node[] -}): JSX.Element { - const { setCenter } = useReactFlow() - const { - activeNodes, - models, - mainNode, - nodesMap, - selectedNodes, - setSelectedNodes, - withImpacted, - connectedNodes, - lineageCache, - setActiveEdges, - setConnections, - setLineage, - setLineageCache, - } = useLineageFlow() - - const model = isNil(mainNode) ? undefined : models[mainNode] - const countActive = - activeNodes.size > 0 ? activeNodes.size : connectedNodes.size - const countSelected = selectedNodes.size - const countUpstreamDownstream = connectedNodes.size - 1 - const countHidden = nodes.filter(n => n.hidden).length - const countSources = nodes.filter( - n => - isFalse(n.hidden) && - (n.data.type === EnumLineageNodeModelType.external || - n.data.type === EnumLineageNodeModelType.seed), - ).length - const countCTEs = nodes.filter( - n => isFalse(n.hidden) && n.data.type === EnumLineageNodeModelType.cte, - ).length - const showActive = - countActive > 0 && countActive !== countUpstreamDownstream + 1 - - function handleCenter(): void { - if (isNil(mainNode)) return - - const node = nodesMap[mainNode] - - if (isNil(node)) return - - setTimeout(() => { - setCenter(node.position.x, node.position.y, { - zoom: 0.5, - duration: 0, - }) - }, 200) - } - - return ( - <> - {isNotNil(model) && ( - - {truncate(model.name, 50, 25)} - - )} - - - All: {nodes.length} - - {countHidden > 0 && ( - - Hidden: {countHidden} - - )} - {countSelected > 0 && ( - - Selected: {countSelected} - - )} - {showActive && ( - - Active: {countActive} - - )} - {(showActive || countSelected > 0 || isNotNil(lineageCache)) && ( - - )} - - {countSources > 0 && ( - - Sources: {countSources} - - )} - {isFalse(showActive) && - withImpacted && - countSelected === 0 && - countUpstreamDownstream > 0 && ( - - Upstream/Downstream: {countUpstreamDownstream} - - )} - {countCTEs > 0 && ( - - CTEs: {countCTEs} - - )} - - ) -} diff --git a/vscode/react/src/components/graph/ModelNode.tsx b/vscode/react/src/components/graph/ModelNode.tsx index 35123a1630..0dfbdf2cbb 100644 --- a/vscode/react/src/components/graph/ModelNode.tsx +++ b/vscode/react/src/components/graph/ModelNode.tsx @@ -8,7 +8,7 @@ import { Position, type NodeProps } from 'reactflow' import { ModelNodeHeaderHandles } from './ModelNodeHeaderHandles' import { ModelColumns } from './ModelColumns' import { fromAPIColumn, type Column } from '@/domain/column' -import type { ModelEncodedFQN } from '@/domain/models' +import { decode, type ModelEncodedFQN } from '@/domain/models' export const EnumLineageNodeModelType = { ...ModelType, @@ -25,11 +25,12 @@ export type LineageNodeModelType = keyof typeof EnumLineageNodeModelType export type ColumnType = keyof typeof EnumColumnType export default function ModelNode({ - id, + id: idProp, data, sourcePosition, targetPosition, }: NodeProps): JSX.Element { + const id = idProp as ModelEncodedFQN const nodeData: GraphNodeData = data ?? { label: '', type: EnumLineageNodeModelType.unknown, @@ -52,7 +53,7 @@ export default function ModelNode({ const columns: Column[] = useMemo(() => { const modelsArray = Object.values(models) - const decodedId = decodeURIComponent(id) + const decodedId = decode(id) const model = modelsArray.find((m: Model) => m.fqn === decodedId) const modelColumns = model?.columns?.map(fromAPIColumn) ?? [] diff --git a/vscode/react/src/components/graph/context.tsx b/vscode/react/src/components/graph/context.tsx index ec365b6597..4504382205 100644 --- a/vscode/react/src/components/graph/context.tsx +++ b/vscode/react/src/components/graph/context.tsx @@ -11,7 +11,8 @@ import { type Node } from 'reactflow' import type { Lineage } from '@/domain/lineage' import type { ModelSQLMeshModel } from '@/domain/sqlmesh-model' import type { Column } from '@/domain/column' -import type { ModelEncodedFQN } from '@/domain/models' +import type { ModelEncodedFQN, ModelName } from '@/domain/models' +import type { ColumnName } from '@/domain/column' import type { Model } from '@/api/client' export interface Connections { @@ -27,13 +28,13 @@ export type HighlightedNodes = Record interface LineageFlow { lineage: Record lineageCache?: Record - mainNode?: string + mainNode?: ModelEncodedFQN connectedNodes: Set activeEdges: ActiveEdges activeNodes: ActiveNodes selectedNodes: SelectedNodes selectedEdges: any[] - models: Record + models: Record unknownModels: Set connections: Map withConnected: boolean @@ -41,14 +42,13 @@ interface LineageFlow { hasBackground: boolean withImpacted: boolean withSecondary: boolean - showControls: boolean manuallySelectedColumn?: [ModelSQLMeshModel, Column] highlightedNodes: HighlightedNodes nodesMap: Record setHighlightedNodes: React.Dispatch> setActiveNodes: React.Dispatch> setWithConnected: React.Dispatch> - setMainNode: React.Dispatch> + setMainNode: React.Dispatch> setSelectedNodes: React.Dispatch> setWithColumns: React.Dispatch> setHasBackground: React.Dispatch> @@ -70,7 +70,10 @@ interface LineageFlow { React.SetStateAction<[ModelSQLMeshModel, Column] | undefined> > setNodeConnections: React.Dispatch> - isActiveColumn: (modelName: string, columnName: string) => boolean + isActiveColumn: ( + modelName: ModelEncodedFQN, + columnName: ColumnName, + ) => boolean } export const LineageFlowContext = createContext({ @@ -93,7 +96,6 @@ export const LineageFlowContext = createContext({ connectedNodes: new Set(), highlightedNodes: {}, nodesMap: {}, - showControls: true, setHighlightedNodes: () => {}, setWithColumns: () => false, setHasBackground: () => false, @@ -144,7 +146,7 @@ export default function LineageFlowProvider({ {}, ) const [withColumns, setWithColumns] = useState(showColumns) - const [mainNode, setMainNode] = useState() + const [mainNode, setMainNode] = useState() const [manuallySelectedColumn, setManuallySelectedColumn] = useState<[ModelSQLMeshModel, Column]>() const [activeEdges, setActiveEdges] = useState(new Map()) @@ -242,7 +244,10 @@ export default function LineageFlowProvider({ ) const isActiveColumn = useCallback( - function isActive(modelName: string, columnName: string): boolean { + function isActive( + modelName: ModelEncodedFQN, + columnName: ColumnName, + ): boolean { const leftConnector = [EnumSide.Left, modelName, columnName].join('__') const rightConnector = [EnumSide.Right, modelName, columnName].join('__') diff --git a/vscode/react/src/components/graph/help.ts b/vscode/react/src/components/graph/help.ts index 4ecd93d077..a09c88fe5a 100644 --- a/vscode/react/src/components/graph/help.ts +++ b/vscode/react/src/components/graph/help.ts @@ -130,13 +130,11 @@ export function getEdges( sourceModelName, sourceColumnName, ) - console.log('sourceHandler', sourceHandler) const targetHandler = toID( EnumSide.Left, targetModelName, targetColumnName, ) - console.log('targetHandler', targetHandler) outputEdges.push( createGraphEdge( @@ -668,7 +666,7 @@ export function getUpdatedEdges( export function getUpdatedNodes( nodes: Node[] = [], activeNodes: Set, - mainNode: string, + mainNode: ModelEncodedFQN, connectedNodes: Set, selectedNodes: Set, connections: Map, diff --git a/vscode/react/src/components/listbox/ListboxShow.tsx b/vscode/react/src/components/listbox/ListboxShow.tsx deleted file mode 100644 index 42b189d9e6..0000000000 --- a/vscode/react/src/components/listbox/ListboxShow.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Fragment, useEffect, useState } from 'react' -import { Listbox, Transition } from '@headlessui/react' -import { ChevronDownIcon, CheckIcon } from '@heroicons/react/24/solid' -import { FunnelIcon } from '@heroicons/react/24/outline' -import { isFalse } from '@/utils/index' -import clsx from 'clsx' - -export default function ListboxShow({ - options, - value = [], -}: { - options: Record< - string, - | React.Dispatch> - | undefined - | ((value: boolean) => void) - > - value: string[] -}): JSX.Element { - const [selected, setSelected] = useState([]) - - useEffect(() => { - setSelected(value) - }, [value]) - - return ( - { - setSelected(value) - - for (const key in options) { - options[key]?.(value.includes(key)) - } - }} - multiple - > -
    - - - {' '} - Show - - - - - {Object.keys(options) - .filter(key => options[key]) - .map(key => ( - - clsx( - 'relative cursor-default select-none py-1 pl-10 pr-4', - disabled ? 'opacity-50 cursor-not-allowed' : '', - active - ? 'bg-primary-10 text-primary-500' - : 'text-neutral-700 dark:text-neutral-300', - ) - } - value={key} - disabled={isFalse(Boolean(options[key]))} - > - {({ selected }) => ( - <> - {key} - - - - )} - - ))} - - -
    -
    - ) -} diff --git a/vscode/react/src/components/logo/SqlMesh.tsx b/vscode/react/src/components/logo/SqlMesh.tsx deleted file mode 100644 index 67ab2b4277..0000000000 --- a/vscode/react/src/components/logo/SqlMesh.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, { memo } from 'react' - -interface PropsLogoSqlMesh extends React.SVGAttributes {} - -function LogoSqlMesh({ style, className }: PropsLogoSqlMesh): JSX.Element { - return ( - - SQLMesh logo - - - - - - - - - - ) -} - -export default memo(LogoSqlMesh) diff --git a/vscode/react/src/components/logo/Tobiko.tsx b/vscode/react/src/components/logo/Tobiko.tsx deleted file mode 100644 index ed732ecca4..0000000000 --- a/vscode/react/src/components/logo/Tobiko.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { memo } from 'react' - -interface PropsLogoTobiko extends React.SVGAttributes {} - -function LogoTobiko({ style, className }: PropsLogoTobiko): JSX.Element { - return ( - - Tobiko Data logo - - - - - - - - - - - - ) -} - -export default memo(LogoTobiko) From 1770cd95c14b465439df0d96445d7b4356d67bbb Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 4 Jul 2025 15:20:51 +0100 Subject: [PATCH 0530/1056] chore(vscode): update source tree in git (#4909) --- vscode/react/src/routeTree.gen.ts | 90 ++++++++++--------------------- 1 file changed, 28 insertions(+), 62 deletions(-) diff --git a/vscode/react/src/routeTree.gen.ts b/vscode/react/src/routeTree.gen.ts index d7727926fd..dd198661f1 100644 --- a/vscode/react/src/routeTree.gen.ts +++ b/vscode/react/src/routeTree.gen.ts @@ -8,65 +8,34 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -// Import Routes +import { Route as rootRouteImport } from './routes/__root' +import { Route as LineageRouteImport } from './routes/lineage' +import { Route as IndexRouteImport } from './routes/index' -import { Route as rootRoute } from './routes/__root' -import { Route as LineageImport } from './routes/lineage' -import { Route as IndexImport } from './routes/index' - -// Create/Update Routes - -const LineageRoute = LineageImport.update({ +const LineageRoute = LineageRouteImport.update({ id: '/lineage', path: '/lineage', - getParentRoute: () => rootRoute, + getParentRoute: () => rootRouteImport, } as any) - -const IndexRoute = IndexImport.update({ +const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', - getParentRoute: () => rootRoute, + getParentRoute: () => rootRouteImport, } as any) -// Populate the FileRoutesByPath interface - -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexImport - parentRoute: typeof rootRoute - } - '/lineage': { - id: '/lineage' - path: '/lineage' - fullPath: '/lineage' - preLoaderRoute: typeof LineageImport - parentRoute: typeof rootRoute - } - } -} - -// Create and export the route tree - export interface FileRoutesByFullPath { '/': typeof IndexRoute '/lineage': typeof LineageRoute } - export interface FileRoutesByTo { '/': typeof IndexRoute '/lineage': typeof LineageRoute } - export interface FileRoutesById { - __root__: typeof rootRoute + __root__: typeof rootRouteImport '/': typeof IndexRoute '/lineage': typeof LineageRoute } - export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: '/' | '/lineage' @@ -75,37 +44,34 @@ export interface FileRouteTypes { id: '__root__' | '/' | '/lineage' fileRoutesById: FileRoutesById } - export interface RootRouteChildren { IndexRoute: typeof IndexRoute LineageRoute: typeof LineageRoute } +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/lineage': { + id: '/lineage' + path: '/lineage' + fullPath: '/lineage' + preLoaderRoute: typeof LineageRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LineageRoute: LineageRoute, } - -export const routeTree = rootRoute +export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() - -/* ROUTE_MANIFEST_START -{ - "routes": { - "__root__": { - "filePath": "__root.tsx", - "children": [ - "/", - "/lineage" - ] - }, - "/": { - "filePath": "index.tsx" - }, - "/lineage": { - "filePath": "lineage.tsx" - } - } -} -ROUTE_MANIFEST_END */ From c6b9e187e407772be164de90ed7759d9df0ceb43 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:59:49 +0100 Subject: [PATCH 0531/1056] chore(vscode): continued typing of nodes (#4911) --- .../src/components/graph/ModelColumns.tsx | 48 ++++++++----------- .../react/src/components/graph/ModelNode.tsx | 30 +++++------- .../graph/ModelNodeHeaderHandles.tsx | 17 +++---- .../react/src/components/graph/constants.ts | 16 +++++++ vscode/react/src/components/graph/context.tsx | 30 ++++++------ vscode/react/src/components/graph/help.ts | 46 +++++------------- vscode/react/src/components/graph/types.ts | 32 +++++++++---- 7 files changed, 107 insertions(+), 112 deletions(-) create mode 100644 vscode/react/src/components/graph/constants.ts diff --git a/vscode/react/src/components/graph/ModelColumns.tsx b/vscode/react/src/components/graph/ModelColumns.tsx index 19f382467a..e0e180de51 100644 --- a/vscode/react/src/components/graph/ModelColumns.tsx +++ b/vscode/react/src/components/graph/ModelColumns.tsx @@ -10,7 +10,7 @@ import { isNotNil, truncate, } from '@/utils/index' -import { EnumSide, toID, type Side } from './types' +import { toID, type PartialColumnHandleId, type Side } from './types' import { NoSymbolIcon } from '@heroicons/react/24/solid' import { ClockIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline' import clsx from 'clsx' @@ -38,7 +38,7 @@ export function ModelColumns({ columns, disabled, className, - limit = 5, + limit, withHandles = false, withDescription = true, maxHeight = '50vh', @@ -47,7 +47,7 @@ export function ModelColumns({ columns: Column[] disabled?: boolean className?: string - limit?: number + limit: number withHandles?: boolean withDescription?: boolean maxHeight?: string @@ -125,7 +125,7 @@ export function ModelColumns({ } const isSelectManually = useCallback( - function isSelectManually(columnName: string): boolean { + function isSelectManually(columnName: ColumnName): boolean { if (isNil(manuallySelectedColumn)) return false const [selectedModel, selectedColumn] = manuallySelectedColumn @@ -138,12 +138,10 @@ export function ModelColumns({ ) const removeEdges = useCallback( - function removeEdges(columnId: string): void { + function removeEdges(columnId: PartialColumnHandleId): void { const visited = new Set() - removeActiveEdges( - walk(columnId, EnumSide.Left).concat(walk(columnId, EnumSide.Right)), - ) + removeActiveEdges(walk(columnId, 'left').concat(walk(columnId, 'right'))) if (connections.size === 0 && isNotNil(lineageCache)) { setLineage(lineageCache) @@ -164,12 +162,12 @@ export function ModelColumns({ return edges .map(edge => [ - side === EnumSide.Left - ? [toID(EnumSide.Left, id), toID(EnumSide.Right, edge)] - : [toID(EnumSide.Left, edge), toID(EnumSide.Right, id)], + side === 'left' + ? [toID('left', id), toID('right', edge)] + : [toID('left', edge), toID('right', id)], ].concat(walk(edge, side)), ) - .flat() as Array<[string, string]> + .flat() as Array<[PartialColumnHandleId, PartialColumnHandleId]> } }, [removeActiveEdges, connections], @@ -324,8 +322,8 @@ function ModelColumn({ withHandles = false, withDescription = true, }: { - id: string - nodeId: string + id: PartialColumnHandleId + nodeId: ModelEncodedFQN column: Column disabled?: boolean isActive?: boolean @@ -337,7 +335,7 @@ function ModelColumn({ updateColumnLineage: ( lineage: ColumnLineageApiLineageModelNameColumnNameGet200, ) => void - removeEdges: (columnId: string) => void + removeEdges: (columnId: PartialColumnHandleId) => void selectManually?: React.Dispatch< React.SetStateAction< [ModelSQLMeshModel, Column] | undefined @@ -454,8 +452,8 @@ function ColumnHandles({ children, className, }: { - nodeId: string - id: string + nodeId: ModelEncodedFQN + id: PartialColumnHandleId children: React.ReactNode className?: string hasLeft?: boolean @@ -482,7 +480,7 @@ function ColumnHandles({ {hasLeft && (
    {disabled && ( @@ -542,7 +534,7 @@ function ColumnDisplay({ className="w-3 h-3 mr-2" /> )} - {truncate(decodedColumnName, 50, 20)} + {truncate(columnName, 50, 20)} m.fqn === decodedId) const modelColumns = model?.columns?.map(fromAPIColumn) ?? [] - Object.keys(lineage[decodedId]?.columns ?? {}).forEach((column: string) => { - const found = modelColumns.find(({ name }: any) => { - try { - return name === decodeURI(column) - } catch { - return name === column - } - }) - + toKeys(lineage[decodedId]?.columns ?? {}).forEach(column => { + const found = modelColumns.find(({ name }) => name === column) if (isNil(found)) { modelColumns.push( fromAPIColumn({ name: column, type: EnumColumnType.UNKNOWN }), ) } }) - - modelColumns.forEach((column: any) => { + return modelColumns.map(column => { let columnType = column.type ?? EnumColumnType.UNKNOWN - if (columnType.startsWith(EnumColumnType.STRUCT)) { columnType = EnumColumnType.STRUCT } - - column.type = columnType + return { + ...column, + type: columnType, + } }) - - return modelColumns }, [id, models, lineage]) const highlightedNodeModels = useMemo( @@ -136,7 +129,7 @@ export default function ModelNode({ // Ensure nodeData.type is a valid LineageNodeModelType const nodeType: LineageNodeModelType = Object.values( EnumLineageNodeModelType, - ).includes(nodeData.type as any) + ).includes(nodeData.type) ? (nodeData.type as LineageNodeModelType) : EnumLineageNodeModelType.unknown @@ -222,12 +215,13 @@ export default function ModelNode({ ? undefined : handleSelect } - count={hasHighlightedNodes ? undefined : columns.length} + numberOfColumns={columns.length} /> {showColumns && ( {truncate(decodeURI(label), 50, 20)} - {isNotNil(count) && ( + {isNotNil(numberOfColumns) && ( - {count} + {numberOfColumns} )} @@ -97,7 +98,7 @@ export function ModelNodeHeaderHandles({ {hasRight && ( export type ActiveEdges = Map> export type ActiveNodes = Set -export type SelectedNodes = Set +export type SelectedNodes = Set export type HighlightedNodes = Record interface LineageFlow { lineage: Record lineageCache?: Record mainNode?: ModelEncodedFQN - connectedNodes: Set + connectedNodes: Set activeEdges: ActiveEdges activeNodes: ActiveNodes selectedNodes: SelectedNodes - selectedEdges: any[] + selectedEdges: ConnectedNode[] models: Record unknownModels: Set connections: Map @@ -44,7 +45,7 @@ interface LineageFlow { withSecondary: boolean manuallySelectedColumn?: [ModelSQLMeshModel, Column] highlightedNodes: HighlightedNodes - nodesMap: Record + nodesMap: Record setHighlightedNodes: React.Dispatch> setActiveNodes: React.Dispatch> setWithConnected: React.Dispatch> @@ -64,7 +65,7 @@ interface LineageFlow { setLineageCache: React.Dispatch< React.SetStateAction | undefined> > - handleClickModel?: (modelName: string) => void + handleClickModel?: (modelName: ModelEncodedFQN) => void handleError?: (error: any) => void setManuallySelectedColumn: React.Dispatch< React.SetStateAction<[ModelSQLMeshModel, Column] | undefined> @@ -130,7 +131,7 @@ export default function LineageFlowProvider({ models, }: { children: React.ReactNode - handleClickModel?: (modelName: string) => void + handleClickModel?: (modelName: ModelEncodedFQN) => void handleError?: (error: any) => void showColumns?: boolean showConnected?: boolean @@ -142,9 +143,9 @@ export default function LineageFlowProvider({ const [lineageCache, setLineageCache] = useState< Record | undefined >(undefined) - const [nodesConnections, setNodeConnections] = useState>( - {}, - ) + const [nodesConnections, setNodeConnections] = useState< + Record + >({}) const [withColumns, setWithColumns] = useState(showColumns) const [mainNode, setMainNode] = useState() const [manuallySelectedColumn, setManuallySelectedColumn] = @@ -248,9 +249,8 @@ export default function LineageFlowProvider({ modelName: ModelEncodedFQN, columnName: ColumnName, ): boolean { - const leftConnector = [EnumSide.Left, modelName, columnName].join('__') - const rightConnector = [EnumSide.Right, modelName, columnName].join('__') - + const leftConnector = toID('left', modelName, columnName) + const rightConnector = toID('right', modelName, columnName) return ( hasActiveEdgeConnector(activeEdges, leftConnector) || hasActiveEdgeConnector(activeEdges, rightConnector) @@ -259,8 +259,8 @@ export default function LineageFlowProvider({ [checkActiveEdge, activeEdges], ) - const connectedNodes: Set = useMemo( - () => new Set(Object.keys(nodesConnections)), + const connectedNodes = useMemo( + () => new Set(toKeys(nodesConnections)), [nodesConnections], ) diff --git a/vscode/react/src/components/graph/help.ts b/vscode/react/src/components/graph/help.ts index a09c88fe5a..317d449807 100644 --- a/vscode/react/src/components/graph/help.ts +++ b/vscode/react/src/components/graph/help.ts @@ -9,7 +9,7 @@ import { import { type LineageColumn } from '@/api/client' import { Position, type Edge, type Node, type XYPosition } from 'reactflow' import { type ActiveEdges, type Connections } from './context' -import { EnumSide, toID, toKeys } from './types' +import { toID, toKeys } from './types' import { EnumLineageNodeModelType, type LineageNodeModelType, @@ -19,23 +19,12 @@ import type { ConnectedNode } from '@/workers/lineage' import { encode, type ModelEncodedFQN, type ModelURI } from '@/domain/models' import type { Column, ColumnName } from '@/domain/column' import type { ModelSQLMeshModel } from '@/domain/sqlmesh-model' - -/** - * Space between nodes. - */ -const NODE_BALANCE_SPACE = 64 -/** - * Height of a column line. - */ -const COLUMN_LINE_HEIGHT = 24 -/** - * Assumed width of a character. - */ -const CHAR_WIDTH = 8 -/** - * Maximum number of columns that can be visible in a node. - */ -const MAX_VISIBLE_COLUMNS = 5 +import { + CHAR_WIDTH, + COLUMN_LINE_HEIGHT, + MAX_VISIBLE_COLUMNS, + NODE_BALANCE_SPACE, +} from './constants' export interface GraphNodeData { label: string @@ -125,17 +114,8 @@ export function getEdges( if (isNil(sourceColumns)) continue for (const sourceColumnName of sourceColumns) { - const sourceHandler = toID( - EnumSide.Right, - sourceModelName, - sourceColumnName, - ) - const targetHandler = toID( - EnumSide.Left, - targetModelName, - targetColumnName, - ) - + const sourceHandler = toID('right', sourceModelName, sourceColumnName) + const targetHandler = toID('left', targetModelName, targetColumnName) outputEdges.push( createGraphEdge( sourceModelName, @@ -460,14 +440,14 @@ export function mergeConnections( // And right bucket contains references to all targets (left handlers) connectionsModelSource.left.forEach(id => { activeEdges.push([ - toID(EnumSide.Left, modelColumnIdSource), - toID(EnumSide.Right, id), + toID('left', modelColumnIdSource), + toID('right', id), ]) }) connectionsModelSource.right.forEach(id => { activeEdges.push([ - toID(EnumSide.Left, id), - toID(EnumSide.Right, modelColumnIdSource), + toID('left', id), + toID('right', modelColumnIdSource), ]) }) }) diff --git a/vscode/react/src/components/graph/types.ts b/vscode/react/src/components/graph/types.ts index 831251e2d7..fde31e3084 100644 --- a/vscode/react/src/components/graph/types.ts +++ b/vscode/react/src/components/graph/types.ts @@ -1,17 +1,21 @@ import type { ColumnName } from '@/domain/column' import type { ModelEncodedFQN } from '@/domain/models' +import type { Branded } from '@bus/brand' -export const EnumSide = { - Left: 'left', - Right: 'right', -} as const - -export type Side = (typeof EnumSide)[keyof typeof EnumSide] +export type Side = 'left' | 'right' export type NodeId = string export type EdgeId = string +/** + * Partial column handle id that isn't complete yet as it's missing the left/right side + * definition. + */ +export type PartialColumnHandleId = Branded +export type ColumnHandleId = Branded +export type ModelHandleId = Branded + /** * Converts a list of strings to a single string with a double underscore * Outlines with types, the type of ids that can be created. @@ -19,15 +23,23 @@ export type EdgeId = string * @returns */ export function toID( - leftOrRight: 'left' | 'right', + leftOrRight: Side, modelName: ModelEncodedFQN, columnName: ColumnName, ): NodeId -export function toID(source: NodeId, target: NodeId): NodeId export function toID( - leftOrRight: 'left' | 'right', modelName: ModelEncodedFQN, -): NodeId + columnName: ColumnName, +): PartialColumnHandleId +export function toID( + leftOrRight: Side, + partialColumnHandleId: PartialColumnHandleId, +): ColumnHandleId +export function toID( + leftOrRight: Side, + modelName: ModelEncodedFQN, +): ModelHandleId +export function toID(source: NodeId, target: NodeId): NodeId export function toID( source: NodeId, target: NodeId, From 3b660467e581410ded57d15fc6a1a3cd3ef51f45 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Fri, 4 Jul 2025 10:24:26 -0700 Subject: [PATCH 0532/1056] feat: add named secret support DuckDB (#4912) --- docs/integrations/engines/duckdb.md | 106 +++++++++++++++++++++++---- sqlmesh/core/config/connection.py | 16 +++- tests/core/test_connection_config.py | 106 ++++++++++++++++++++++++++- 3 files changed, 209 insertions(+), 19 deletions(-) diff --git a/docs/integrations/engines/duckdb.md b/docs/integrations/engines/duckdb.md index 19d8d6e1f2..bc0af4f242 100644 --- a/docs/integrations/engines/duckdb.md +++ b/docs/integrations/engines/duckdb.md @@ -10,15 +10,15 @@ ### Connection options -| Option | Description | Type | Required | -|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------:|:--------:| -| `type` | Engine type name - must be `duckdb` | string | Y | -| `database` | The optional database name. If not specified, the in-memory database is used. Cannot be defined if using `catalogs`. | string | N | -| `catalogs` | Mapping to define multiple catalogs. Can [attach DuckDB catalogs](#duckdb-catalogs-example) or [catalogs for other connections](#other-connection-catalogs-example). First entry is the default catalog. Cannot be defined if using `database`. | dict | N | -| `extensions` | Extension to load into duckdb. Only autoloadable extensions are supported. | list | N | -| `connector_config` | Configuration to pass into the duckdb connector. | dict | N | -| `secrets` | Configuration for authenticating external sources (e.g., S3) using DuckDB secrets. | dict | N | -| `filesystems` | Configuration for registering `fsspec` filesystems to the DuckDB connection. | dict | N | +| Option | Description | Type | Required | +|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------:|:--------:| +| `type` | Engine type name - must be `duckdb` | string | Y | +| `database` | The optional database name. If not specified, the in-memory database is used. Cannot be defined if using `catalogs`. | string | N | +| `catalogs` | Mapping to define multiple catalogs. Can [attach DuckDB catalogs](#duckdb-catalogs-example) or [catalogs for other connections](#other-connection-catalogs-example). First entry is the default catalog. Cannot be defined if using `database`. | dict | N | +| `extensions` | Extension to load into duckdb. Only autoloadable extensions are supported. | list | N | +| `connector_config` | Configuration to pass into the duckdb connector. | dict | N | +| `secrets` | Configuration for authenticating external sources (e.g., S3) using DuckDB secrets. Can be a list of secret configurations or a dictionary with custom secret names. | list/dict | N | +| `filesystems` | Configuration for registering `fsspec` filesystems to the DuckDB connection. | dict | N | #### DuckDB Catalogs Example @@ -194,9 +194,18 @@ DuckDB can read data directly from cloud services via extensions (e.g., [httpfs] The `secrets` option allows you to configure DuckDB's [Secrets Manager](https://duckdb.org/docs/configuration/secrets_manager.html) to authenticate with external services like S3. This is the recommended approach for cloud storage authentication in DuckDB v0.10.0 and newer, replacing the [legacy authentication method](https://duckdb.org/docs/stable/extensions/httpfs/s3api_legacy_authentication.html) via variables. -##### Secrets Configuration Example for S3 +##### Secrets Configuration -The `secrets` accepts a list of secret configurations, each defining the necessary authentication parameters for the specific service: +The `secrets` option supports two formats: + +1. **List format** (default secrets): A list of secret configurations where each secret uses DuckDB's default naming +2. **Dictionary format** (named secrets): A dictionary where keys are custom secret names and values are the secret configurations + +This flexibility allows you to organize multiple secrets of the same type or reference specific secrets by name in your SQL queries. + +##### List Format Example (Default Secrets) + +Using a list creates secrets with DuckDB's default naming: === "YAML" @@ -253,6 +262,75 @@ The `secrets` accepts a list of secret configurations, each defining the necessa ) ``` +##### Dictionary Format Example (Named Secrets) + +Using a dictionary allows you to assign custom names to your secrets for better organization and reference: + +=== "YAML" + + ```yaml linenums="1" + gateways: + duckdb: + connection: + type: duckdb + catalogs: + local: local.db + remote: "s3://bucket/data/remote.duckdb" + extensions: + - name: httpfs + secrets: + my_s3_secret: + type: s3 + region: "YOUR_AWS_REGION" + key_id: "YOUR_AWS_ACCESS_KEY" + secret: "YOUR_AWS_SECRET_KEY" + my_azure_secret: + type: azure + account_name: "YOUR_AZURE_ACCOUNT" + account_key: "YOUR_AZURE_KEY" + ``` + +=== "Python" + + ```python linenums="1" + from sqlmesh.core.config import ( + Config, + ModelDefaultsConfig, + GatewayConfig, + DuckDBConnectionConfig + ) + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + gateways={ + "duckdb": GatewayConfig( + connection=DuckDBConnectionConfig( + catalogs={ + "local": "local.db", + "remote": "s3://bucket/data/remote.duckdb" + }, + extensions=[ + {"name": "httpfs"}, + ], + secrets={ + "my_s3_secret": { + "type": "s3", + "region": "YOUR_AWS_REGION", + "key_id": "YOUR_AWS_ACCESS_KEY", + "secret": "YOUR_AWS_SECRET_KEY" + }, + "my_azure_secret": { + "type": "azure", + "account_name": "YOUR_AZURE_ACCOUNT", + "account_key": "YOUR_AZURE_KEY" + } + } + ) + ), + } + ) + ``` + After configuring the secrets, you can directly reference S3 paths in your catalogs or in SQL queries without additional authentication steps. Refer to the official DuckDB documentation for the full list of [supported S3 secret parameters](https://duckdb.org/docs/stable/extensions/httpfs/s3api.html#overview-of-s3-secret-parameters) and for more information on the [Secrets Manager configuration](https://duckdb.org/docs/configuration/secrets_manager.html). @@ -273,9 +351,9 @@ The `filesystems` accepts a list of file systems to register in the DuckDB conne type: duckdb catalogs: ducklake: - type: ducklake - path: myducklakecatalog.duckdb - data_path: abfs://MyFabricWorkspace/MyFabricLakehouse.Lakehouse/Files/DuckLake.Files + type: ducklake + path: myducklakecatalog.duckdb + data_path: abfs://MyFabricWorkspace/MyFabricLakehouse.Lakehouse/Files/DuckLake.Files extensions: - ducklake filesystems: diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 2c897cd8a5..7960aa831f 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -277,7 +277,7 @@ class BaseDuckDBConnectionConfig(ConnectionConfig): catalogs: t.Optional[t.Dict[str, t.Union[str, DuckDBAttachOptions]]] = None extensions: t.List[t.Union[str, t.Dict[str, t.Any]]] = [] connector_config: t.Dict[str, t.Any] = {} - secrets: t.List[t.Dict[str, t.Any]] = [] + secrets: t.Union[t.List[t.Dict[str, t.Any]], t.Dict[str, t.Dict[str, t.Any]]] = [] filesystems: t.List[t.Dict[str, t.Any]] = [] concurrent_tasks: int = 1 @@ -362,14 +362,22 @@ def init(cursor: duckdb.DuckDBPyConnection) -> None: "More info: https://duckdb.org/docs/stable/extensions/httpfs/s3api_legacy_authentication.html" ) else: - for secrets in self.secrets: + if isinstance(self.secrets, list): + secrets_items = [(secret_dict, "") for secret_dict in self.secrets] + else: + secrets_items = [ + (secret_dict, secret_name) + for secret_name, secret_dict in self.secrets.items() + ] + + for secret_dict, secret_name in secrets_items: secret_settings: t.List[str] = [] - for field, setting in secrets.items(): + for field, setting in secret_dict.items(): secret_settings.append(f"{field} '{setting}'") if secret_settings: secret_clause = ", ".join(secret_settings) try: - cursor.execute(f"CREATE SECRET ({secret_clause});") + cursor.execute(f"CREATE SECRET {secret_name} ({secret_clause});") except Exception as e: raise ConfigError(f"Failed to create secret: {e}") diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 0d7df3d724..6d91c72f2c 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -4,7 +4,7 @@ import pytest from _pytest.fixtures import FixtureRequest -from unittest.mock import patch +from unittest.mock import patch, MagicMock from sqlmesh.core.config.connection import ( BigQueryConnectionConfig, @@ -455,6 +455,110 @@ def test_duckdb(make_config): assert not config.is_recommended_for_state_sync +@patch("duckdb.connect") +def test_duckdb_multiple_secrets(mock_connect, make_config): + """Test that multiple secrets are correctly converted to CREATE SECRET SQL statements.""" + mock_cursor = MagicMock() + mock_connection = MagicMock() + mock_connection.cursor.return_value = mock_cursor + mock_connection.execute = mock_cursor.execute + mock_connect.return_value = mock_connection + + # Create config with 2 secrets + config = make_config( + type="duckdb", + secrets=[ + { + "type": "s3", + "region": "us-east-1", + "key_id": "my_aws_key", + "secret": "my_aws_secret", + }, + { + "type": "azure", + "account_name": "myaccount", + "account_key": "myaccountkey", + }, + ], + ) + + assert isinstance(config, DuckDBConnectionConfig) + assert len(config.secrets) == 2 + + # Create cursor which triggers _cursor_init + cursor = config.create_engine_adapter().cursor + + execute_calls = [call[0][0] for call in mock_cursor.execute.call_args_list] + create_secret_calls = [call for call in execute_calls if call.startswith("CREATE SECRET")] + + # Should have exactly 2 CREATE SECRET calls + assert len(create_secret_calls) == 2 + + # Verify the SQL for the first secret (S3) + assert ( + create_secret_calls[0] + == "CREATE SECRET (type 's3', region 'us-east-1', key_id 'my_aws_key', secret 'my_aws_secret');" + ) + + # Verify the SQL for the second secret (Azure) + assert ( + create_secret_calls[1] + == "CREATE SECRET (type 'azure', account_name 'myaccount', account_key 'myaccountkey');" + ) + + +@patch("duckdb.connect") +def test_duckdb_named_secrets(mock_connect, make_config): + """Test that named secrets are correctly converted to CREATE SECRET SQL statements.""" + mock_cursor = MagicMock() + mock_connection = MagicMock() + mock_connection.cursor.return_value = mock_cursor + mock_connection.execute = mock_cursor.execute + mock_connect.return_value = mock_connection + + # Create config with named secrets using dictionary format + config = make_config( + type="duckdb", + secrets={ + "my_s3_secret": { + "type": "s3", + "region": "us-east-1", + "key_id": "my_aws_key", + "secret": "my_aws_secret", + }, + "my_azure_secret": { + "type": "azure", + "account_name": "myaccount", + "account_key": "myaccountkey", + }, + }, + ) + + assert isinstance(config, DuckDBConnectionConfig) + assert len(config.secrets) == 2 + + # Create cursor which triggers _cursor_init + cursor = config.create_engine_adapter().cursor + + execute_calls = [call[0][0] for call in mock_cursor.execute.call_args_list] + create_secret_calls = [call for call in execute_calls if call.startswith("CREATE SECRET")] + + # Should have exactly 2 CREATE SECRET calls + assert len(create_secret_calls) == 2 + + # Verify the SQL for the first secret (S3) includes the secret name + assert ( + create_secret_calls[0] + == "CREATE SECRET my_s3_secret (type 's3', region 'us-east-1', key_id 'my_aws_key', secret 'my_aws_secret');" + ) + + # Verify the SQL for the second secret (Azure) includes the secret name + assert ( + create_secret_calls[1] + == "CREATE SECRET my_azure_secret (type 'azure', account_name 'myaccount', account_key 'myaccountkey');" + ) + + @pytest.mark.parametrize( "kwargs1, kwargs2, shared_adapter", [ From 317df56bfdb9bb1389685c0abc715186ea5ab462 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Fri, 4 Jul 2025 10:31:08 -0700 Subject: [PATCH 0533/1056] fix: don't include type when using ducklake (#4913) --- sqlmesh/core/config/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 7960aa831f..f6e12a37f1 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -232,7 +232,7 @@ def to_sql(self, alias: str) -> str: options = [] # 'duckdb' is actually not a supported type, but we'd like to allow it for # fully qualified attach options or integration testing, similar to duckdb-dbt - if self.type not in ("duckdb", "motherduck"): + if self.type not in ("duckdb", "ducklake", "motherduck"): options.append(f"TYPE {self.type.upper()}") if self.read_only: options.append("READ_ONLY") From 64ecc4dc32f0d9ba1c02aef0fe33c2fa10e5f2ff Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 7 Jul 2025 12:39:44 +0100 Subject: [PATCH 0534/1056] chore(vscode): use uv to speed up e2e tests (#4882) --- .github/workflows/pr.yaml | 2 ++ vscode/extension/tests/utils.ts | 19 +++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 381bb4c58e..77ec906b13 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -42,6 +42,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.12' + - name: Install uv + uses: astral-sh/setup-uv@v4 - name: Install python dependencies run: | python -m venv .venv diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index e765487b0d..d6a2ac7aaa 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -39,19 +39,18 @@ export interface PythonEnvironment { } /** - * Create a virtual environment in the given directory. + * Create a virtual environment in the given directory using uv. * @param venvDir The directory to create the virtual environment in. */ export const createVirtualEnvironment = async ( venvDir: string, ): Promise => { - const pythonCmd = process.platform === 'win32' ? 'python' : 'python3' - const { stderr, exitCode } = await execAsync( - `${pythonCmd} -m venv "${venvDir}"`, - ) + // Try to use uv first, fallback to python -m venv + const { exitCode, stderr } = await execAsync(`uv venv "${venvDir}"`) if (exitCode !== 0) { - throw new Error(`Failed to create venv: ${stderr}`) + throw new Error(`Failed to create venv with uv: ${stderr}`) } + // Get paths const isWindows = process.platform === 'win32' const binDir = path.join(venvDir, isWindows ? 'Scripts' : 'bin') @@ -65,7 +64,7 @@ export const createVirtualEnvironment = async ( } /** - * Install packages in the given virtual environment. + * Install packages in the given virtual environment using uv. * @param pythonDetails The Python environment to use. * @param packagePaths The paths to the packages to install (string[]). */ @@ -73,11 +72,11 @@ export const pipInstall = async ( pythonDetails: PythonEnvironment, packagePaths: string[], ): Promise => { - const { pipPath } = pythonDetails - const execString = `"${pipPath}" install -e "${packagePaths.join('" -e "')}"` + const packages = packagePaths.map(pkg => `-e "${pkg}"`).join(' ') + const execString = `uv pip install --python "${pythonDetails.pythonPath}" ${packages}` const { stderr, exitCode } = await execAsync(execString) if (exitCode !== 0) { - throw new Error(`Failed to install package: ${stderr}`) + throw new Error(`Failed to install package with uv: ${stderr}`) } } From 170a1d068fe44416b7b0de0e890b3a7603dc8ab9 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:08:27 +0100 Subject: [PATCH 0535/1056] chore(vscode): adding typing to worker message bus (#4918) --- .../src/components/graph/ModelLineage.tsx | 29 ++++++---- vscode/react/src/components/graph/context.tsx | 2 +- vscode/react/src/components/graph/help.ts | 2 +- vscode/react/src/components/graph/types.ts | 44 +++++++++++++++ .../{ => components/graph}/workers/index.ts | 0 .../{ => components/graph}/workers/lineage.ts | 53 ++++++++++--------- 6 files changed, 95 insertions(+), 35 deletions(-) rename vscode/react/src/{ => components/graph}/workers/index.ts (100%) rename vscode/react/src/{ => components/graph}/workers/lineage.ts (67%) diff --git a/vscode/react/src/components/graph/ModelLineage.tsx b/vscode/react/src/components/graph/ModelLineage.tsx index e7f16debdc..3d157d3869 100644 --- a/vscode/react/src/components/graph/ModelLineage.tsx +++ b/vscode/react/src/components/graph/ModelLineage.tsx @@ -17,7 +17,7 @@ import ReactFlow, { } from 'reactflow' import Loading from '@/components/loading/Loading' import Spinner from '@/components/logo/Spinner' -import { createLineageWorker } from '@/workers/index' +import { createLineageWorker } from '@/components/graph/workers/index' import { isArrayEmpty, isFalse, isNil, isNotNil } from '@/utils/index' import clsx from 'clsx' import ModelNode from './ModelNode' @@ -35,7 +35,13 @@ import { type ModelLineage as ModelLineageType, } from '@/domain/lineage' import './Graph.css' -import { toKeys } from './types' +import { + toKeys, + type LineageWorkerMessage, + type LineageWorkerRequestMessage, + type LineageWorkerResponseMessage, + type LineageWorkerErrorMessage, +} from './types' import { encode } from '@/domain/models' const WITH_COLUMNS_LIMIT = 30 @@ -95,14 +101,15 @@ export function ModelLineage({ setIsMergingModels(true) - lineageWorker.postMessage({ + const message: LineageWorkerRequestMessage = { topic: 'lineage', payload: { currentLineage: {}, newLineage: data, mainNode: model.fqn, }, - }) + } + lineageWorker.postMessage(message) }) .catch(error => { handleError?.(error) @@ -146,21 +153,25 @@ export function ModelLineage({ setHighlightedNodes(highlightedNodes ?? {}) }, [highlightedNodes]) - function handleLineageWorkerMessage(e: MessageEvent): void { + function handleLineageWorkerMessage( + e: MessageEvent, + ): void { if (e.data.topic === 'lineage') { + const message = e.data as LineageWorkerResponseMessage setIsMergingModels(false) - setNodeConnections(e.data.payload.nodesConnections) - setLineage(e.data.payload.lineage) + setNodeConnections(message.payload.nodesConnections) + setLineage(message.payload.lineage) if ( - Object.values(e.data.payload?.lineage ?? {}).length > WITH_COLUMNS_LIMIT + Object.values(message.payload.lineage ?? {}).length > WITH_COLUMNS_LIMIT ) { setWithColumns(false) } } if (e.data.topic === 'error') { - handleError?.(e.data.error) + const message = e.data as LineageWorkerErrorMessage + handleError?.(message.error) setIsMergingModels(false) } } diff --git a/vscode/react/src/components/graph/context.tsx b/vscode/react/src/components/graph/context.tsx index d1b632279a..9ab4f0722e 100644 --- a/vscode/react/src/components/graph/context.tsx +++ b/vscode/react/src/components/graph/context.tsx @@ -14,7 +14,7 @@ import type { ModelEncodedFQN, ModelName } from '@/domain/models' import type { ColumnName } from '@/domain/column' import type { Model } from '@/api/client' import { toID, toKeys } from './types' -import type { ConnectedNode } from '@/workers/lineage' +import type { ConnectedNode } from '@/components/graph/types' export interface Connections { left: string[] diff --git a/vscode/react/src/components/graph/help.ts b/vscode/react/src/components/graph/help.ts index 317d449807..93e5c4db45 100644 --- a/vscode/react/src/components/graph/help.ts +++ b/vscode/react/src/components/graph/help.ts @@ -15,7 +15,7 @@ import { type LineageNodeModelType, } from './ModelNode' import type { Lineage } from '@/domain/lineage' -import type { ConnectedNode } from '@/workers/lineage' +import type { ConnectedNode } from '@/components/graph/types' import { encode, type ModelEncodedFQN, type ModelURI } from '@/domain/models' import type { Column, ColumnName } from '@/domain/column' import type { ModelSQLMeshModel } from '@/domain/sqlmesh-model' diff --git a/vscode/react/src/components/graph/types.ts b/vscode/react/src/components/graph/types.ts index fde31e3084..6e188b31c8 100644 --- a/vscode/react/src/components/graph/types.ts +++ b/vscode/react/src/components/graph/types.ts @@ -1,9 +1,12 @@ import type { ColumnName } from '@/domain/column' import type { ModelEncodedFQN } from '@/domain/models' import type { Branded } from '@bus/brand' +import type { Lineage } from '@/domain/lineage' export type Side = 'left' | 'right' +export type Direction = 'upstream' | 'downstream' + export type NodeId = string export type EdgeId = string @@ -55,3 +58,44 @@ export function toKeys(obj: Record): K[] { } export type ModelLineage = Record + +// Worker Message Types +export interface ConnectedNode { + id?: string + edges: ConnectedNode[] +} + +export interface LineageWorkerRequestPayload { + currentLineage: Record + newLineage: Record + mainNode: string +} + +export interface LineageWorkerResponsePayload { + lineage: Record + nodesConnections: Record +} + +export interface LineageWorkerErrorPayload { + error: Error +} + +export interface LineageWorkerRequestMessage { + topic: 'lineage' + payload: LineageWorkerRequestPayload +} + +export interface LineageWorkerResponseMessage { + topic: 'lineage' + payload: LineageWorkerResponsePayload +} + +export interface LineageWorkerErrorMessage { + topic: 'error' + error: Error +} + +export type LineageWorkerMessage = + | LineageWorkerRequestMessage + | LineageWorkerResponseMessage + | LineageWorkerErrorMessage diff --git a/vscode/react/src/workers/index.ts b/vscode/react/src/components/graph/workers/index.ts similarity index 100% rename from vscode/react/src/workers/index.ts rename to vscode/react/src/components/graph/workers/index.ts diff --git a/vscode/react/src/workers/lineage.ts b/vscode/react/src/components/graph/workers/lineage.ts similarity index 67% rename from vscode/react/src/workers/lineage.ts rename to vscode/react/src/components/graph/workers/lineage.ts index 0644cdbb63..fe8337b72d 100644 --- a/vscode/react/src/workers/lineage.ts +++ b/vscode/react/src/components/graph/workers/lineage.ts @@ -1,41 +1,46 @@ import { isFalse, isNil } from '@/utils/index' import { type Lineage } from '@/domain/lineage' import type { ModelEncodedFQN } from '@/domain/models' -import { toID, type NodeId } from '@/components/graph/types' - -export interface ConnectedNode { - id?: string - edges: ConnectedNode[] +import { + toID, + type NodeId, + type LineageWorkerMessage, + type LineageWorkerRequestMessage, + type LineageWorkerResponseMessage, + type LineageWorkerErrorMessage, + type ConnectedNode, +} from '@/components/graph/types' +import type { Direction } from '../types' + +interface WorkerScope { + onmessage: ((e: MessageEvent) => void) | null + postMessage: (message: LineageWorkerMessage) => void } -const EnumDirection = { - Upstream: 'upstream', - Downstream: 'downstream', -} as const - -type Direction = (typeof EnumDirection)[keyof typeof EnumDirection] - -const scope = self as any +const scope = self as unknown as WorkerScope -scope.onmessage = async (e: MessageEvent) => { +scope.onmessage = async (e: MessageEvent) => { if (e.data.topic === 'lineage') { try { - const { currentLineage, newLineage, mainNode } = e.data.payload + const message = e.data as LineageWorkerRequestMessage + const { currentLineage, newLineage, mainNode } = message.payload const lineage = await mergeLineageWithModels(currentLineage, newLineage) const nodesConnections = await getNodesConnections(mainNode, lineage) - scope.postMessage({ + const responseMessage: LineageWorkerResponseMessage = { topic: 'lineage', payload: { lineage, nodesConnections, }, - }) + } + scope.postMessage(responseMessage) } catch (error) { - scope.postMessage({ + const errorMessage: LineageWorkerErrorMessage = { topic: 'error', - error, - }) + error: error as Error, + } + scope.postMessage(errorMessage) } } } @@ -69,8 +74,8 @@ async function getNodesConnections( const distances: Record = {} try { - getConnectedNodes(EnumDirection.Upstream, mainNode, lineage, distances) - getConnectedNodes(EnumDirection.Downstream, mainNode, lineage, distances) + getConnectedNodes('upstream', mainNode, lineage, distances) + getConnectedNodes('downstream', mainNode, lineage, distances) } catch (error) { reject(error) } @@ -80,12 +85,12 @@ async function getNodesConnections( } function getConnectedNodes( - direction: Direction = EnumDirection.Downstream, + direction: Direction = 'downstream', node: string, lineage: Record = {}, result: Record = {}, ): void { - const isDownstream = direction === EnumDirection.Downstream + const isDownstream = direction === 'downstream' let models: string[] = [] if (isDownstream) { From bdaf6d6da4bdecbd837644bcbc7c4d4a3a21bb5f Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:13:27 +0100 Subject: [PATCH 0536/1056] chore(vscode): separate out building of settings for test (#4919) --- vscode/extension/tests/broken_project.spec.ts | 14 ++++--- vscode/extension/tests/completions.spec.ts | 12 ++++-- vscode/extension/tests/diagnostics.spec.ts | 8 +++- .../extension/tests/find_references.spec.ts | 3 +- vscode/extension/tests/format.spec.ts | 8 +++- .../extension/tests/go_to_definition.spec.ts | 10 +++-- vscode/extension/tests/hints.spec.ts | 8 +++- vscode/extension/tests/lineage.spec.ts | 12 ++++-- .../extension/tests/lineage_settings.spec.ts | 8 +++- vscode/extension/tests/python_env.spec.ts | 1 + vscode/extension/tests/rename_cte.spec.ts | 8 +++- vscode/extension/tests/render.spec.ts | 14 ++++--- vscode/extension/tests/stop.spec.ts | 8 +++- vscode/extension/tests/tcloud.spec.ts | 8 +++- vscode/extension/tests/utils_code_server.ts | 40 +++++++++++-------- vscode/extension/tests/venv_naming.spec.ts | 1 + 16 files changed, 111 insertions(+), 52 deletions(-) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index 5638d167b2..d030f9224a 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -3,7 +3,11 @@ import fs from 'fs-extra' import os from 'os' import path from 'path' import { openLineageView, saveFile, SUSHI_SOURCE_PATH } from './utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' test('bad project, double model', async ({ page }) => { const tempDir = await fs.mkdtemp( @@ -25,8 +29,8 @@ test('bad project, double model', async ({ page }) => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) @@ -60,8 +64,8 @@ test('working project, then broken through adding double model, then refixed', a const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) await page.waitForLoadState('networkidle') @@ -173,8 +177,8 @@ test('bad project, double model, then fixed', async ({ page }) => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) @@ -246,8 +250,8 @@ test('bad project, double model, check lineage', async ({ page }) => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) await page.waitForLoadState('networkidle') diff --git a/vscode/extension/tests/completions.spec.ts b/vscode/extension/tests/completions.spec.ts index 02ad2a2b79..c5921204b0 100644 --- a/vscode/extension/tests/completions.spec.ts +++ b/vscode/extension/tests/completions.spec.ts @@ -3,7 +3,11 @@ import path from 'path' import fs from 'fs-extra' import os from 'os' import { SUSHI_SOURCE_PATH } from './utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' test('Autocomplete for model names', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) @@ -11,8 +15,8 @@ test('Autocomplete for model names', async ({ page }) => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) @@ -74,8 +78,8 @@ test.describe('Macro Completions', () => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) @@ -132,8 +136,8 @@ test.describe('Macro Completions', () => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts index 4566102d95..b2b21911d5 100644 --- a/vscode/extension/tests/diagnostics.spec.ts +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -3,7 +3,11 @@ import path from 'path' import fs from 'fs-extra' import os from 'os' import { runCommand, SUSHI_SOURCE_PATH } from './utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' test('Workspace diagnostics show up in the diagnostics panel', async ({ page, @@ -13,8 +17,8 @@ test('Workspace diagnostics show up in the diagnostics panel', async ({ const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) const configPath = path.join(tempDir, 'config.py') const configContent = await fs.readFile(configPath, 'utf8') diff --git a/vscode/extension/tests/find_references.spec.ts b/vscode/extension/tests/find_references.spec.ts index f7880094ca..5ea00848db 100644 --- a/vscode/extension/tests/find_references.spec.ts +++ b/vscode/extension/tests/find_references.spec.ts @@ -7,6 +7,7 @@ import { startCodeServer, stopCodeServer, CodeServerContext, + createPythonInterpreterSettingsSpecifier, } from './utils_code_server' // Helper function to set up a test environment for model references @@ -15,8 +16,8 @@ async function setupModelTestEnvironment(): Promise { await fs.copy(SUSHI_SOURCE_PATH, tempDir) const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) return context } diff --git a/vscode/extension/tests/format.spec.ts b/vscode/extension/tests/format.spec.ts index 3f979d8222..be304c00cd 100644 --- a/vscode/extension/tests/format.spec.ts +++ b/vscode/extension/tests/format.spec.ts @@ -3,7 +3,11 @@ import path from 'path' import fs from 'fs-extra' import os from 'os' import { runCommand, SUSHI_SOURCE_PATH } from './utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' test('Format project works correctly', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) @@ -11,8 +15,8 @@ test('Format project works correctly', async ({ page }) => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) diff --git a/vscode/extension/tests/go_to_definition.spec.ts b/vscode/extension/tests/go_to_definition.spec.ts index 241b2062df..3ef20628dd 100644 --- a/vscode/extension/tests/go_to_definition.spec.ts +++ b/vscode/extension/tests/go_to_definition.spec.ts @@ -3,7 +3,11 @@ import path from 'path' import fs from 'fs-extra' import os from 'os' import { goToDefinition, SUSHI_SOURCE_PATH } from './utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' test('Stop server works', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) @@ -11,8 +15,8 @@ test('Stop server works', async ({ page }) => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { // Navigate to code-server instance @@ -53,8 +57,8 @@ test('Go to definition for model', async ({ page }) => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { // Navigate to code-server instance diff --git a/vscode/extension/tests/hints.spec.ts b/vscode/extension/tests/hints.spec.ts index 6486e1bba6..eded1a97e4 100644 --- a/vscode/extension/tests/hints.spec.ts +++ b/vscode/extension/tests/hints.spec.ts @@ -3,7 +3,11 @@ import path from 'path' import fs from 'fs-extra' import os from 'os' import { SUSHI_SOURCE_PATH } from './utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' test('Model type hinting', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) @@ -11,8 +15,8 @@ test('Model type hinting', async ({ page }) => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { // Navigate to code-server instance diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index 6b2c3d3861..110dd08f61 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -4,7 +4,11 @@ import fs from 'fs-extra' import os from 'os' import { openLineageView, SUSHI_SOURCE_PATH } from './utils' import { writeFileSync } from 'fs' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' /** * Helper function to launch VS Code and test lineage with given project path config @@ -24,8 +28,8 @@ test('Lineage panel renders correctly - no project path config (default)', async const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) @@ -146,8 +150,8 @@ test('Lineage panel renders correctly - absolute path project outside of workspa await fs.ensureDir(workspaceDir) const context = await startCodeServer({ tempDir: workspaceDir, - placeFileWithPythonInterpreter: false, }) + await createPythonInterpreterSettingsSpecifier(workspaceDir) const settings = { 'sqlmesh.projectPath': projectDir, @@ -203,8 +207,8 @@ test.skip('Lineage panel renders correctly - multiworkspace setup', async ({ const context = await startCodeServer({ tempDir: workspaceDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(workspaceDir) const settings = { 'python.defaultInterpreterPath': context.defaultPythonInterpreter, diff --git a/vscode/extension/tests/lineage_settings.spec.ts b/vscode/extension/tests/lineage_settings.spec.ts index c4b5a39dfa..ce50e7275e 100644 --- a/vscode/extension/tests/lineage_settings.spec.ts +++ b/vscode/extension/tests/lineage_settings.spec.ts @@ -3,7 +3,11 @@ import path from 'path' import fs from 'fs-extra' import os from 'os' import { openLineageView, SUSHI_SOURCE_PATH } from './utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' test('Settings button is visible in the lineage view', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) @@ -11,8 +15,8 @@ test('Settings button is visible in the lineage view', async ({ page }) => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) diff --git a/vscode/extension/tests/python_env.spec.ts b/vscode/extension/tests/python_env.spec.ts index fe6b9024a1..7c7b6f96ce 100644 --- a/vscode/extension/tests/python_env.spec.ts +++ b/vscode/extension/tests/python_env.spec.ts @@ -35,6 +35,7 @@ if test_var is None or test_var == "": async function runTest(page: Page, context: CodeServerContext): Promise { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.waitForSelector('text=models') await openLineageView(page) } diff --git a/vscode/extension/tests/rename_cte.spec.ts b/vscode/extension/tests/rename_cte.spec.ts index 8dc96a6e25..39fb62162f 100644 --- a/vscode/extension/tests/rename_cte.spec.ts +++ b/vscode/extension/tests/rename_cte.spec.ts @@ -3,7 +3,11 @@ import path from 'path' import fs from 'fs-extra' import os from 'os' import { findAllReferences, renameSymbol, SUSHI_SOURCE_PATH } from './utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' async function setupTestEnvironment({ page }: { page: Page }) { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) @@ -11,8 +15,8 @@ async function setupTestEnvironment({ page }: { page: Page }) { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) // Navigate to code-server instance await page.goto(`http://127.0.0.1:${context.codeServerPort}`) diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts index 2e9132e8ae..6593708976 100644 --- a/vscode/extension/tests/render.spec.ts +++ b/vscode/extension/tests/render.spec.ts @@ -3,7 +3,11 @@ import path from 'path' import fs from 'fs-extra' import os from 'os' import { openLineageView, runCommand, SUSHI_SOURCE_PATH } from './utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' test('Render works correctly', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) @@ -11,8 +15,8 @@ test('Render works correctly', async ({ page }) => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) @@ -55,8 +59,8 @@ test('Render works correctly with model without a description', async ({ const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) @@ -99,8 +103,8 @@ test('Render works correctly with every rendered model opening a new tab', async const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) @@ -153,8 +157,8 @@ test('Render shows model picker when no active editor is open', async ({ const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { // Navigate to code-server instance diff --git a/vscode/extension/tests/stop.spec.ts b/vscode/extension/tests/stop.spec.ts index 911c791720..2cd126f413 100644 --- a/vscode/extension/tests/stop.spec.ts +++ b/vscode/extension/tests/stop.spec.ts @@ -3,7 +3,11 @@ import { runCommand, SUSHI_SOURCE_PATH } from './utils' import os from 'os' import { test } from '@playwright/test' import fs from 'fs-extra' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' test('Stop server works', async ({ page }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) @@ -11,8 +15,8 @@ test('Stop server works', async ({ page }) => { const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) try { // Navigate to code-server instance diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index 5bebfee781..c72e3ddca3 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -9,7 +9,11 @@ import { SUSHI_SOURCE_PATH, } from './utils' import { setTcloudVersion, setupAuthenticatedState } from './tcloud_utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' +import { + createPythonInterpreterSettingsSpecifier, + startCodeServer, + stopCodeServer, +} from './utils_code_server' /** * Helper function to create and set up a Python virtual environment @@ -384,8 +388,8 @@ test.skip('tcloud not signed in and not installed, shows sign in window and then // Start VS Code const context = await startCodeServer({ tempDir, - placeFileWithPythonInterpreter: true, }) + await createPythonInterpreterSettingsSpecifier(tempDir) await page.goto(`http://127.0.0.1:${context.codeServerPort}`) try { diff --git a/vscode/extension/tests/utils_code_server.ts b/vscode/extension/tests/utils_code_server.ts index db6c426912..68bf2ed597 100644 --- a/vscode/extension/tests/utils_code_server.ts +++ b/vscode/extension/tests/utils_code_server.ts @@ -28,6 +28,30 @@ function getExtensionsDir(): string { return extensionsDir } +/** + * Creates a .vscode/settings.json specifier for the Python interpreter + */ +export const createPythonInterpreterSettingsSpecifier = async ( + directory: string, +): Promise => { + const defaultPythonInterpreter = path.join( + __dirname, + '..', + '..', + '..', + '.venv', + 'bin', + 'python', + ) + const vscodeDir = path.join(directory, '.vscode') + await fs.ensureDir(vscodeDir) + const settingsFilePath = path.join(vscodeDir, 'settings.json') + await fs.writeJson(settingsFilePath, { + 'python.defaultInterpreterPath': defaultPythonInterpreter, + }) + return settingsFilePath +} + /** * @param tempDir - The temporary directory to use for the code-server instance * @param placeFileWithPythonInterpreter - Whether to place a vscode/settings.json file in the temp directory that points to the python interpreter of the environmen the test is running in. @@ -35,10 +59,8 @@ function getExtensionsDir(): string { */ export async function startCodeServer({ tempDir, - placeFileWithPythonInterpreter = false, }: { tempDir: string - placeFileWithPythonInterpreter?: boolean }): Promise { // Get the extensions directory set up by global setup const extensionsDir = getExtensionsDir() @@ -59,20 +81,6 @@ export async function startCodeServer({ path.join(os.tmpdir(), 'vscode-test-sushi-user-data-dir-'), ) - // Create .vscode/settings.json with Python interpreter if requested - if (placeFileWithPythonInterpreter) { - const vscodeDir = path.join(tempDir, '.vscode') - await fs.ensureDir(vscodeDir) - - const settings = { - 'python.defaultInterpreterPath': defaultPythonInterpreter, - } - - await fs.writeJson(path.join(vscodeDir, 'settings.json'), settings, { - spaces: 2, - }) - } - // Start code-server instance using the shared extensions directory const codeServerProcess = spawn( 'pnpm', diff --git a/vscode/extension/tests/venv_naming.spec.ts b/vscode/extension/tests/venv_naming.spec.ts index f20246c9bb..37cffb8da3 100644 --- a/vscode/extension/tests/venv_naming.spec.ts +++ b/vscode/extension/tests/venv_naming.spec.ts @@ -40,6 +40,7 @@ test('venv being named .env', async ({ page }, testInfo) => { try { await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.waitForSelector('text=models') await openLineageView(page) await page.waitForSelector('text=Loaded SQLMesh Context') } finally { From ba4a8fb94f8fa386d7741640bb10950a05a08c47 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:56:51 +0100 Subject: [PATCH 0537/1056] chore(vscode): sharing server instance between tests (#4920) --- vscode/extension/eslint.config.mjs | 17 + vscode/extension/tests/bad_setup.spec.ts | 159 ++- vscode/extension/tests/broken_project.spec.ts | 338 +++--- vscode/extension/tests/completions.spec.ts | 229 ++--- vscode/extension/tests/diagnostics.spec.ts | 53 +- .../extension/tests/find_references.spec.ts | 972 +++++++++--------- vscode/extension/tests/fixtures.ts | 43 + vscode/extension/tests/format.spec.ts | 73 +- .../extension/tests/go_to_definition.spec.ts | 128 +-- vscode/extension/tests/hints.spec.ts | 79 +- vscode/extension/tests/lineage.spec.ts | 64 +- .../extension/tests/lineage_settings.spec.ts | 105 +- vscode/extension/tests/python_env.spec.ts | 78 +- vscode/extension/tests/rename_cte.spec.ts | 298 +++--- vscode/extension/tests/render.spec.ts | 266 +++-- vscode/extension/tests/stop.spec.ts | 77 +- vscode/extension/tests/tcloud.spec.ts | 2 +- vscode/extension/tests/venv_naming.spec.ts | 21 +- 18 files changed, 1426 insertions(+), 1576 deletions(-) create mode 100644 vscode/extension/tests/fixtures.ts diff --git a/vscode/extension/eslint.config.mjs b/vscode/extension/eslint.config.mjs index 6d939bdc51..8713558998 100644 --- a/vscode/extension/eslint.config.mjs +++ b/vscode/extension/eslint.config.mjs @@ -52,4 +52,21 @@ export default tseslint.config( '@typescript-eslint/no-unsafe-member-access': 'off', }, }, + { + files: ['tests/**/*.spec.ts'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['@playwright/test'], + message: + 'Import { test, expect, Page } from "./fixtures" instead of directly from @playwright/test', + }, + ], + }, + ], + }, + }, ) diff --git a/vscode/extension/tests/bad_setup.spec.ts b/vscode/extension/tests/bad_setup.spec.ts index 6977b1dfcb..4d716ce166 100644 --- a/vscode/extension/tests/bad_setup.spec.ts +++ b/vscode/extension/tests/bad_setup.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test' +import { expect, test } from './fixtures' import fs from 'fs-extra' import os from 'os' import path from 'path' @@ -10,10 +10,10 @@ import { REPO_ROOT, SUSHI_SOURCE_PATH, } from './utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' test('missing LSP dependencies shows install prompt', async ({ page, + sharedCodeServer, }, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( @@ -29,54 +29,48 @@ test('missing LSP dependencies shows install prompt', async ({ const sqlmeshWithExtras = `${REPO_ROOT}[bigquery]` await pipInstall(pythonDetails, [sqlmeshWithExtras, custom_materializations]) - // Start VS Code - const context = await startCodeServer({ - tempDir, - }) + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) - try { - // Copy sushi project - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - // Configure VS Code settings to use our Python environment - const settings = { - 'python.defaultInterpreterPath': pythonDetails.pythonPath, - 'sqlmesh.environmentPath': pythonEnvDir, - } - await fs.ensureDir(path.join(tempDir, '.vscode')) - await fs.writeJson( - path.join(tempDir, '.vscode', 'settings.json'), - settings, - { spaces: 2 }, - ) - - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - - // Open a SQL file to trigger SQLMesh activation - // Wait for the models folder to be visible - await page.waitForSelector('text=models') - - // Click on the models folder - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the top_waiters model - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - // Wait for the message to show that LSP extras need to be installed - await page.waitForSelector('text=LSP dependencies missing') - expect(await page.locator('text=Install').count()).toBeGreaterThanOrEqual(1) - } finally { - await stopCodeServer(context) + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonDetails.pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { + spaces: 2, + }) + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + // Open a SQL file to trigger SQLMesh activation + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Wait for the message to show that LSP extras need to be installed + await page.waitForSelector('text=LSP dependencies missing') + expect(await page.locator('text=Install').count()).toBeGreaterThanOrEqual(1) }) -test('lineage, no sqlmesh found', async ({ page }, testInfo) => { +test('lineage, no sqlmesh found', async ({ + page, + sharedCodeServer, +}, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( @@ -85,39 +79,30 @@ test('lineage, no sqlmesh found', async ({ page }, testInfo) => { const pythonEnvDir = path.join(tempDir, '.venv') const pythonDetails = await createVirtualEnvironment(pythonEnvDir) - const context = await startCodeServer({ - tempDir, - }) + // Copy sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) - try { - // Copy sushi project - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - // Configure VS Code settings to use our Python environment - const settings = { - 'python.defaultInterpreterPath': pythonDetails.pythonPath, - 'sqlmesh.environmentPath': pythonEnvDir, - } - await fs.ensureDir(path.join(tempDir, '.vscode')) - await fs.writeJson( - path.join(tempDir, '.vscode', 'settings.json'), - settings, - { spaces: 2 }, - ) - - // navigate to code-server instance - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - await page.waitForLoadState('networkidle') - - // Open lineage view - await openLineageView(page) - - // Assert shows that sqlmesh is not installed - await page.waitForSelector('text=SQLMesh LSP not found') - } finally { - // Clean up - await stopCodeServer(context) + // Configure VS Code settings to use our Python environment + const settings = { + 'python.defaultInterpreterPath': pythonDetails.pythonPath, + 'sqlmesh.environmentPath': pythonEnvDir, } + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { + spaces: 2, + }) + + // navigate to code-server instance + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open lineage view + await openLineageView(page) + + // Assert shows that sqlmesh is not installed + await page.waitForSelector('text=SQLMesh LSP not found') }) // Checks that if you have another file open like somewhere else, it still checks the workspace first for a successful context @@ -125,6 +110,7 @@ test('lineage, no sqlmesh found', async ({ page }, testInfo) => { // - the typing in of the file name is very flaky test.skip('check that the LSP runs correctly by opening lineage when looking at another file before not in workspace', async ({ page, + sharedCodeServer, }, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( @@ -159,18 +145,13 @@ test.skip('check that the LSP runs correctly by opening lineage when looking at await fs.ensureDir(path.dirname(sqlFile)) await fs.writeFile(sqlFile, 'SELECT 1') - const context = await startCodeServer({ - tempDir, - }) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - await page.waitForLoadState('networkidle') + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') - // Open the SQL file from the other directory - await openFile(page, sqlFile) + // Open the SQL file from the other directory + await openFile(page, sqlFile) - await page.waitForSelector('text=Loaded SQLMesh context') - } finally { - await stopCodeServer(context) - } + await page.waitForSelector('text=Loaded SQLMesh context') }) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index d030f9224a..bc6f4f7ed1 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -1,15 +1,11 @@ -import { test, expect } from '@playwright/test' +import { test, expect } from './fixtures' import fs from 'fs-extra' import os from 'os' import path from 'path' import { openLineageView, saveFile, SUSHI_SOURCE_PATH } from './utils' -import { - createPythonInterpreterSettingsSpecifier, - startCodeServer, - stopCodeServer, -} from './utils_code_server' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('bad project, double model', async ({ page }) => { +test('bad project, double model', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -27,137 +23,131 @@ test('bad project, double model', async ({ page }) => { customersSql, ) - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - await page.waitForSelector('text=models') + await page.waitForSelector('text=models') - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() - await page.waitForSelector('text=Error creating context') + await page.waitForSelector('text=Error creating context') - await page.waitForTimeout(500) - } finally { - await stopCodeServer(context) - } + await page.waitForTimeout(500) }) test('working project, then broken through adding double model, then refixed', async ({ page, + sharedCodeServer, }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - await page.waitForLoadState('networkidle') - - // Open the lineage view to confirm it loads properly - await openLineageView(page) - await page.waitForSelector('text=Loaded SQLMesh context') - - // Read the customers.sql file - const customersSql = await fs.readFile( - path.join(tempDir, 'models', 'customers.sql'), - 'utf8', - ) - - // Add a duplicate model to break the project - await fs.writeFile( - path.join(tempDir, 'models', 'customers_duplicated.sql'), - customersSql, - ) - - // Open the customers model to trigger the error - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - // Save to refresh the context - await saveFile(page) - - // Wait for the error to appear - const iframes = page.locator('iframe') - const iframeCount = await iframes.count() - let errorCount = 0 - - for (let i = 0; i < iframeCount; i++) { - const iframe = iframes.nth(i) - const contentFrame = iframe.contentFrame() - if (contentFrame) { - const activeFrame = contentFrame.locator('#active-frame').contentFrame() - if (activeFrame) { - try { - await activeFrame - .getByText('Error: Failed to load model') - .waitFor({ timeout: 1000 }) - errorCount++ - } catch { - // Continue to next iframe if this one doesn't have the error - continue - } + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open the lineage view to confirm it loads properly + await openLineageView(page) + await page.waitForSelector('text=Loaded SQLMesh context') + + // Read the customers.sql file + const customersSql = await fs.readFile( + path.join(tempDir, 'models', 'customers.sql'), + 'utf8', + ) + + // Add a duplicate model to break the project + await fs.writeFile( + path.join(tempDir, 'models', 'customers_duplicated.sql'), + customersSql, + ) + + // Open the customers model to trigger the error + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + // Save to refresh the context + await saveFile(page) + + // Wait for the error to appear + const iframes = page.locator('iframe') + const iframeCount = await iframes.count() + let errorCount = 0 + + for (let i = 0; i < iframeCount; i++) { + const iframe = iframes.nth(i) + const contentFrame = iframe.contentFrame() + if (contentFrame) { + const activeFrame = contentFrame.locator('#active-frame').contentFrame() + if (activeFrame) { + try { + await activeFrame + .getByText('Error: Failed to load model') + .waitFor({ timeout: 1000 }) + errorCount++ + } catch { + // Continue to next iframe if this one doesn't have the error + continue } } } - expect(errorCount).toBeGreaterThan(0) - - // Remove the duplicated model to fix the project - await fs.remove(path.join(tempDir, 'models', 'customers_duplicated.sql')) - - // Save again to refresh the context - await saveFile(page) - - const iframes2 = page.locator('iframe') - const iframeCount2 = await iframes2.count() - let raw_demographicsCount = 0 - - for (let i = 0; i < iframeCount2; i++) { - const iframe = iframes2.nth(i) - const contentFrame = iframe.contentFrame() - if (contentFrame) { - const activeFrame = contentFrame.locator('#active-frame').contentFrame() - if (activeFrame) { - try { - await activeFrame - .getByText('sushi.customers') - .waitFor({ timeout: 1000 }) - raw_demographicsCount++ - } catch { - // Continue to next iframe if this one doesn't have the error - continue - } + } + expect(errorCount).toBeGreaterThan(0) + + // Remove the duplicated model to fix the project + await fs.remove(path.join(tempDir, 'models', 'customers_duplicated.sql')) + + // Save again to refresh the context + await saveFile(page) + + const iframes2 = page.locator('iframe') + const iframeCount2 = await iframes2.count() + let raw_demographicsCount = 0 + + for (let i = 0; i < iframeCount2; i++) { + const iframe = iframes2.nth(i) + const contentFrame = iframe.contentFrame() + if (contentFrame) { + const activeFrame = contentFrame.locator('#active-frame').contentFrame() + if (activeFrame) { + try { + await activeFrame + .getByText('sushi.customers') + .waitFor({ timeout: 1000 }) + raw_demographicsCount++ + } catch { + // Continue to next iframe if this one doesn't have the error + continue } } } - expect(raw_demographicsCount).toBeGreaterThan(0) - } finally { - await stopCodeServer(context) } + expect(raw_demographicsCount).toBeGreaterThan(0) }) -test('bad project, double model, then fixed', async ({ page }) => { +test('bad project, double model, then fixed', async ({ + page, + sharedCodeServer, +}) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -175,62 +165,63 @@ test('bad project, double model, then fixed', async ({ page }) => { customersSql, ) - const context = await startCodeServer({ - tempDir, - }) - await createPythonInterpreterSettingsSpecifier(tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - - await page.waitForSelector('text=models') - - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - await page.waitForSelector('text=Error creating context') - - // Remove the duplicated model - await fs.remove(path.join(tempDir, 'models', 'customers_duplicated.sql')) - - // Open the linage view - await openLineageView(page) - - // Wait for the error to go away - const iframes = page.locator('iframe') - const iframeCount = await iframes.count() - let raw_demographicsCount = 0 - - for (let i = 0; i < iframeCount; i++) { - const iframe = iframes.nth(i) - const contentFrame = iframe.contentFrame() - if (contentFrame) { - const activeFrame = contentFrame.locator('#active-frame').contentFrame() - if (activeFrame) { - try { - await activeFrame - .getByText('sushi.customers') - .waitFor({ timeout: 1000 }) - raw_demographicsCount++ - } catch { - continue - } + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + await page.waitForSelector('text=models') + + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await page.waitForSelector('text=Error creating context') + + // Remove the duplicated model + await fs.remove(path.join(tempDir, 'models', 'customers_duplicated.sql')) + + // Open the linage view + await openLineageView(page) + + // Wait for the error to go away + const iframes = page.locator('iframe') + const iframeCount = await iframes.count() + let raw_demographicsCount = 0 + + for (let i = 0; i < iframeCount; i++) { + const iframe = iframes.nth(i) + const contentFrame = iframe.contentFrame() + if (contentFrame) { + const activeFrame = contentFrame.locator('#active-frame').contentFrame() + if (activeFrame) { + try { + await activeFrame + .getByText('sushi.customers') + .waitFor({ timeout: 1000 }) + raw_demographicsCount++ + } catch { + continue } } } - expect(raw_demographicsCount).toBeGreaterThan(0) - } finally { - await stopCodeServer(context) } + expect(raw_demographicsCount).toBeGreaterThan(0) }) -test('bad project, double model, check lineage', async ({ page }) => { +test('bad project, double model, check lineage', async ({ + page, + sharedCodeServer, +}) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -248,22 +239,17 @@ test('bad project, double model, check lineage', async ({ page }) => { customersSql, ) - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - await page.waitForLoadState('networkidle') + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') - // Open the lineage view - await openLineageView(page) + // Open the lineage view + await openLineageView(page) - await page.waitForSelector('text=Error creating context') - await page.waitForSelector('text=Error:') + await page.waitForSelector('text=Error creating context') + await page.waitForSelector('text=Error:') - await page.waitForTimeout(500) - } finally { - await stopCodeServer(context) - } + await page.waitForTimeout(500) }) diff --git a/vscode/extension/tests/completions.spec.ts b/vscode/extension/tests/completions.spec.ts index c5921204b0..693934d4e4 100644 --- a/vscode/extension/tests/completions.spec.ts +++ b/vscode/extension/tests/completions.spec.ts @@ -1,25 +1,75 @@ -import { test, expect } from '@playwright/test' +import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' import { SUSHI_SOURCE_PATH } from './utils' -import { - createPythonInterpreterSettingsSpecifier, - startCodeServer, - stopCodeServer, -} from './utils_code_server' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Autocomplete for model names', async ({ page }) => { +test('Autocomplete for model names', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await page + .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) + .locator('a') + .click() + + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') + + await page.locator('text=grain').first().click() + + // Move to the end of the file + for (let i = 0; i < 100; i++) { + await page.keyboard.press('ArrowDown') + } + + // Add a new line + await page.keyboard.press('Enter') + + // Type the beginning of sushi.customers to trigger autocomplete + await page.keyboard.type('sushi.waiter_as_customer') + + // Wait a moment for autocomplete to appear + await page.waitForTimeout(500) + + // Check if the autocomplete suggestion for sushi.customers is visible + expect( + await page.locator('text=sushi.waiter_as_customer_by_day').count(), + ).toBeGreaterThanOrEqual(1) + expect( + await page.locator('text=SQLMesh Model').count(), + ).toBeGreaterThanOrEqual(1) +}) + +// Skip the macro completions test as regular checks because they are flaky and +// covered in other non-integration tests. +test.describe('Macro Completions', () => { + test('Completion for inbuilt macros', async ({ page, sharedCodeServer }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-sushi-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await createPythonInterpreterSettingsSpecifier(tempDir) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) // Wait for the models folder to be visible await page.waitForSelector('text=models') @@ -32,7 +82,7 @@ test('Autocomplete for model names', async ({ page }) => { // Open the top_waiters model await page - .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) + .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') .click() @@ -49,140 +99,69 @@ test('Autocomplete for model names', async ({ page }) => { // Add a new line await page.keyboard.press('Enter') - // Type the beginning of sushi.customers to trigger autocomplete - await page.keyboard.type('sushi.waiter_as_customer') - - // Wait a moment for autocomplete to appear await page.waitForTimeout(500) - // Check if the autocomplete suggestion for sushi.customers is visible - expect( - await page.locator('text=sushi.waiter_as_customer_by_day').count(), - ).toBeGreaterThanOrEqual(1) - expect( - await page.locator('text=SQLMesh Model').count(), - ).toBeGreaterThanOrEqual(1) - } finally { - await stopCodeServer(context) - } -}) - -// Skip the macro completions test as regular checks because they are flaky and -// covered in other non-integration tests. -test.describe('Macro Completions', () => { - test('Completion for inbuilt macros', async ({ page }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-sushi-'), - ) - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - const context = await startCodeServer({ - tempDir, - }) - await createPythonInterpreterSettingsSpecifier(tempDir) - - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + // Hit the '@' key to trigger autocomplete for inbuilt macros + await page.keyboard.press('@') + await page.keyboard.type('eac') - // Wait for the models folder to be visible - await page.waitForSelector('text=models') - - // Click on the models folder - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the top_waiters model - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') - - await page.locator('text=grain').first().click() - - // Move to the end of the file - for (let i = 0; i < 100; i++) { - await page.keyboard.press('ArrowDown') - } - - // Add a new line - await page.keyboard.press('Enter') - - await page.waitForTimeout(500) - - // Hit the '@' key to trigger autocomplete for inbuilt macros - await page.keyboard.press('@') - await page.keyboard.type('eac') - - // Wait a moment for autocomplete to appear - await page.waitForTimeout(500) + // Wait a moment for autocomplete to appear + await page.waitForTimeout(500) - // Check if the autocomplete suggestion for inbuilt macros is visible - expect(await page.locator('text=@each').count()).toBeGreaterThanOrEqual(1) - } finally { - await stopCodeServer(context) - } + // Check if the autocomplete suggestion for inbuilt macros is visible + expect(await page.locator('text=@each').count()).toBeGreaterThanOrEqual(1) }) - test('Completion for custom macros', async ({ page }) => { + test('Completion for custom macros', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-sushi-'), ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - // Wait for the models folder to be visible - await page.waitForSelector('text=models') + // Wait for the models folder to be visible + await page.waitForSelector('text=models') - // Click on the models folder - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() + // Click on the models folder + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() - // Open the top_waiters model - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() + // Open the top_waiters model + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() - await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') - await page.locator('text=grain').first().click() + await page.locator('text=grain').first().click() - // Move to the end of the file - for (let i = 0; i < 100; i++) { - await page.keyboard.press('ArrowDown') - } + // Move to the end of the file + for (let i = 0; i < 100; i++) { + await page.keyboard.press('ArrowDown') + } - // Add a new line - await page.keyboard.press('Enter') + // Add a new line + await page.keyboard.press('Enter') - // Type the beginning of a macro to trigger autocomplete - await page.keyboard.press('@') - await page.keyboard.type('add_o') + // Type the beginning of a macro to trigger autocomplete + await page.keyboard.press('@') + await page.keyboard.type('add_o') - // Wait a moment for autocomplete to appear - await page.waitForTimeout(500) + // Wait a moment for autocomplete to appear + await page.waitForTimeout(500) - // Check if the autocomplete suggestion for custom macros is visible - expect( - await page.locator('text=@add_one').count(), - ).toBeGreaterThanOrEqual(1) - } finally { - await stopCodeServer(context) - } + // Check if the autocomplete suggestion for custom macros is visible + expect(await page.locator('text=@add_one').count()).toBeGreaterThanOrEqual( + 1, + ) }) }) diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts index b2b21911d5..3c3aa4d0e0 100644 --- a/vscode/extension/tests/diagnostics.spec.ts +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -1,23 +1,16 @@ -import { test } from '@playwright/test' +import { test } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' import { runCommand, SUSHI_SOURCE_PATH } from './utils' -import { - createPythonInterpreterSettingsSpecifier, - startCodeServer, - stopCodeServer, -} from './utils_code_server' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Workspace diagnostics show up in the diagnostics panel', async ({ page, + sharedCodeServer, }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) const configPath = path.join(tempDir, 'config.py') @@ -25,30 +18,28 @@ test('Workspace diagnostics show up in the diagnostics panel', async ({ const updatedContent = configContent.replace('enabled=False', 'enabled=True') await fs.writeFile(configPath, updatedContent) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - // Wait for the models folder to be visible - await page.waitForSelector('text=models') + // Wait for the models folder to be visible + await page.waitForSelector('text=models') - // Click on the models folder, excluding external_models - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() - // Open the customer_revenue_lifetime model - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() + // Open the customer_revenue_lifetime model + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() - // Open problems panel - await runCommand(page, 'View: Focus Problems') + // Open problems panel + await runCommand(page, 'View: Focus Problems') - await page.waitForSelector('text=problems') - await page.waitForSelector('text=All models should have an owner') - } finally { - await stopCodeServer(context) - } + await page.waitForSelector('text=problems') + await page.waitForSelector('text=All models should have an owner') }) diff --git a/vscode/extension/tests/find_references.spec.ts b/vscode/extension/tests/find_references.spec.ts index 5ea00848db..42dd0e6274 100644 --- a/vscode/extension/tests/find_references.spec.ts +++ b/vscode/extension/tests/find_references.spec.ts @@ -1,24 +1,16 @@ -import { test, expect, Page } from '@playwright/test' +import { test, expect, Page } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' import { findAllReferences, goToReferences, SUSHI_SOURCE_PATH } from './utils' -import { - startCodeServer, - stopCodeServer, - CodeServerContext, - createPythonInterpreterSettingsSpecifier, -} from './utils_code_server' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' // Helper function to set up a test environment for model references -async function setupModelTestEnvironment(): Promise { +async function setupModelTestEnvironment(): Promise { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - return context + return tempDir } // Helper function to navigate to models folder @@ -62,616 +54,626 @@ async function openTopWaitersFile(page: Page) { } test.describe('Model References', () => { - test('Go to References (Shift+F12) for Model usage', async ({ page }) => { - const context = await setupModelTestEnvironment() + test('Go to References (Shift+F12) for Model usage', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - // Open customers.sql which contains references to other models - await openCustomersFile(page) + // Open customers.sql which contains references to other models + await openCustomersFile(page) - // Step 4: Position cursor on the sushi.orders model reference in the SQL query - await page.locator('text=sushi.orders').first().click() + // Step 4: Position cursor on the sushi.orders model reference in the SQL query + await page.locator('text=sushi.orders').first().click() - // Step 5: Trigger "Go to References" command using Shift+F12 keyboard shortcut - await goToReferences(page) + // Step 5: Trigger "Go to References" command using Shift+F12 keyboard shortcut + await goToReferences(page) - // Step 6: Wait for VSCode references panel to appear at the bottom - await page.waitForSelector('text=References') + // Step 6: Wait for VSCode references panel to appear at the bottom + await page.waitForSelector('text=References') - // Step 7: Ensure references panel has populated with all usages of sushi.orders model - await page.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 6 - }, - { timeout: 10000 }, + // Step 7: Ensure references panel has populated with all usages of sushi.orders model + await page.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 6 + }, + { timeout: 10000 }, + ) + + // Step 8: Verify the references panel shows both SQL and Python files containing references + const hasReferences = await page.evaluate(() => { + const body = document.body.textContent || '' + return ( + body.includes('References') && + (body.includes('.sql') || body.includes('.py')) ) + }) - // Step 8: Verify the references panel shows both SQL and Python files containing references - const hasReferences = await page.evaluate(() => { - const body = document.body.textContent || '' - return ( - body.includes('References') && - (body.includes('.sql') || body.includes('.py')) - ) - }) + expect(hasReferences).toBe(true) - expect(hasReferences).toBe(true) + // Step 9: Find and click on the orders.py reference to navigate to the model definition + let clickedReference = false - // Step 9: Find and click on the orders.py reference to navigate to the model definition - let clickedReference = false + const referenceItems = page.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() - const referenceItems = page.locator( - '.monaco-list-row, .reference-item, .monaco-tl-row', - ) - const count = await referenceItems.count() - - for (let i = 0; i < count; i++) { - const item = referenceItems.nth(i) - const text = await item.textContent() - - // Search for the orders.py reference which contains the Python model definition - if (text && text.includes('orders.py')) { - await item.click() - clickedReference = true - break - } + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Search for the orders.py reference which contains the Python model definition + if (text && text.includes('orders.py')) { + await item.click() + clickedReference = true + break } + } - expect(clickedReference).toBe(true) + expect(clickedReference).toBe(true) - // Step 10: Verify successful navigation to orders.py by checking for unique Python code - await expect(page.locator('text=list(range(0, 100))')).toBeVisible() - } finally { - await stopCodeServer(context) - } + // Step 10: Verify successful navigation to orders.py by checking for unique Python code + await expect(page.locator('text=list(range(0, 100))')).toBeVisible() }) - test('Find All References (Alt+Shift+F12) for Model', async ({ page }) => { - const context = await setupModelTestEnvironment() + test('Find All References (Alt+Shift+F12) for Model', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - // Open customers.sql which contains multiple model references - await openCustomersFile(page) + // Open customers.sql which contains multiple model references + await openCustomersFile(page) - // Step 4: Click on sushi.orders model reference to position cursor - await page.locator('text=sushi.orders').first().click() + // Step 4: Click on sushi.orders model reference to position cursor + await page.locator('text=sushi.orders').first().click() - // Step 5: Trigger "Find All References" command using Alt+Shift+F12 (or +Shift+F12 on Windows/Linux) - await findAllReferences(page) + // Step 5: Trigger "Find All References" command using Alt+Shift+F12 (or +Shift+F12 on Windows/Linux) + await findAllReferences(page) - let clickedReference = false - const referenceItems = page.locator( - '.monaco-list-row, .reference-item, .monaco-tl-row', - ) - const count = await referenceItems.count() + let clickedReference = false + const referenceItems = page.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() - // Step 6: Iterate through references to find and click on orders.py - for (let i = 0; i < count; i++) { - const item = referenceItems.nth(i) - const text = await item.textContent() + // Step 6: Iterate through references to find and click on orders.py + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() - // Find the orders.py reference which contains the model implementation - if (text && text.includes('orders.py')) { - await item.click() + // Find the orders.py reference which contains the model implementation + if (text && text.includes('orders.py')) { + await item.click() - clickedReference = true - break - } + clickedReference = true + break } + } - expect(clickedReference).toBe(true) + expect(clickedReference).toBe(true) - // Step 7: Verify navigation to orders.py by checking for Python import statement - await expect(page.locator('text=import random')).toBeVisible() + // Step 7: Verify navigation to orders.py by checking for Python import statement + await expect(page.locator('text=import random')).toBeVisible() - // Step 8: Click on the import statement to ensure file is fully loaded and interactive - await page.locator('text=import random').first().click() + // Step 8: Click on the import statement to ensure file is fully loaded and interactive + await page.locator('text=import random').first().click() - // Step 9: Final verification that we're viewing the correct Python model file - await expect(page.locator('text=list(range(0, 100))')).toBeVisible() - } finally { - await stopCodeServer(context) - } + // Step 9: Final verification that we're viewing the correct Python model file + await expect(page.locator('text=list(range(0, 100))')).toBeVisible() }) - test('Go to References for Model from Audit', async ({ page }) => { - const context = await setupModelTestEnvironment() - - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - - // Open assert_item_price_above_zero.sql audit file which references sushi.items model - await navigateToAudits(page) - await page - .getByRole('treeitem', { - name: 'assert_item_price_above_zero.sql', - exact: true, - }) - .locator('a') - .click() - - // Wait for audit file to load and SQLMesh context to initialize - await page.waitForSelector('text=standalone') - await page.waitForSelector('text=Loaded SQLMesh Context') - - // Step 4: Click on sushi.items model reference in the audit query - await page.locator('text=sushi.items').first().click() - - // Step 5: Trigger "Go to References" to find all places where sushi.items is used - await goToReferences(page) - - // Step 6: Wait for VSCode references panel to appear - await page.waitForSelector('text=References') - - // Step 7: Ensure references panel shows multiple files that reference sushi.items - await page.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 4 - }, - { timeout: 10000 }, - ) - - // Step 8: Verify references panel contains both audit and model files - const hasReferences = await page.evaluate(() => { - const body = document.body.textContent || '' - return ( - body.includes('References') && - (body.includes('.sql') || body.includes('.py')) - ) + test('Go to References for Model from Audit', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + // Open assert_item_price_above_zero.sql audit file which references sushi.items model + await navigateToAudits(page) + await page + .getByRole('treeitem', { + name: 'assert_item_price_above_zero.sql', + exact: true, }) + .locator('a') + .click() + + // Wait for audit file to load and SQLMesh context to initialize + await page.waitForSelector('text=standalone') + await page.waitForSelector('text=Loaded SQLMesh Context') - expect(hasReferences).toBe(true) + // Step 4: Click on sushi.items model reference in the audit query + await page.locator('text=sushi.items').first().click() - // 9. Click on one of the references to navigate to it - let clickedReference = false + // Step 5: Trigger "Go to References" to find all places where sushi.items is used + await goToReferences(page) - const referenceItems = page.locator( - '.monaco-list-row, .reference-item, .monaco-tl-row', + // Step 6: Wait for VSCode references panel to appear + await page.waitForSelector('text=References') + + // Step 7: Ensure references panel shows multiple files that reference sushi.items + await page.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 4 + }, + { timeout: 10000 }, + ) + + // Step 8: Verify references panel contains both audit and model files + const hasReferences = await page.evaluate(() => { + const body = document.body.textContent || '' + return ( + body.includes('References') && + (body.includes('.sql') || body.includes('.py')) ) - const count = await referenceItems.count() - - for (let i = 0; i < count; i++) { - const item = referenceItems.nth(i) - const text = await item.textContent() - - // Search for the customer_revenue_by_day.sql file which joins with sushi.items - if (text && text.includes('customer_revenue_by_day.sql')) { - await item.click() - clickedReference = true - break - } - } + }) + + expect(hasReferences).toBe(true) - expect(clickedReference).toBe(true) + // 9. Click on one of the references to navigate to it + let clickedReference = false - // Step 10: Verify navigation to customer_revenue_by_day.sql by checking for SQL JOIN syntax - await expect(page.locator('text=LEFT JOIN')).toBeVisible() + const referenceItems = page.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() - // Step 11: Click on LEFT JOIN to ensure file is interactive and verify content - await page.locator('text=LEFT JOIN').first().click() - await expect( - page.locator('text=FROM sushi.order_items AS oi'), - ).toBeVisible() - } finally { - await stopCodeServer(context) + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Search for the customer_revenue_by_day.sql file which joins with sushi.items + if (text && text.includes('customer_revenue_by_day.sql')) { + await item.click() + clickedReference = true + break + } } + + expect(clickedReference).toBe(true) + + // Step 10: Verify navigation to customer_revenue_by_day.sql by checking for SQL JOIN syntax + await expect(page.locator('text=LEFT JOIN')).toBeVisible() + + // Step 11: Click on LEFT JOIN to ensure file is interactive and verify content + await page.locator('text=LEFT JOIN').first().click() + await expect( + page.locator('text=FROM sushi.order_items AS oi'), + ).toBeVisible() }) - test.skip('Find All Model References from Audit', async ({ page }) => { - const context = await setupModelTestEnvironment() + test.skip('Find All Model References from Audit', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + // Open the audit file that validates item prices + await navigateToAudits(page) + await page + .getByRole('treeitem', { + name: 'assert_item_price_above_zero.sql', + exact: true, + }) + .locator('a') + .click() - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + // Ensure audit file and SQLMesh context are fully loaded + await page.waitForSelector('text=standalone') + await page.waitForSelector('text=Loaded SQLMesh Context') - // Open the audit file that validates item prices - await navigateToAudits(page) - await page - .getByRole('treeitem', { - name: 'assert_item_price_above_zero.sql', - exact: true, - }) - .locator('a') - .click() + // Step 4: Position cursor on sushi.items model reference + await page.locator('text=sushi.items').first().click() - // Ensure audit file and SQLMesh context are fully loaded - await page.waitForSelector('text=standalone') - await page.waitForSelector('text=Loaded SQLMesh Context') + // Step 5: Use Find All References to see all occurrences across the project + await findAllReferences(page) - // Step 4: Position cursor on sushi.items model reference - await page.locator('text=sushi.items').first().click() + // Step 6: Click on a reference to navigate to customer_revenue_by_day.sql + let clickedReference = false - // Step 5: Use Find All References to see all occurrences across the project - await findAllReferences(page) + const referenceItems = page.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() - // Step 6: Click on a reference to navigate to customer_revenue_by_day.sql - let clickedReference = false + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() - const referenceItems = page.locator( - '.monaco-list-row, .reference-item, .monaco-tl-row', - ) - const count = await referenceItems.count() - - for (let i = 0; i < count; i++) { - const item = referenceItems.nth(i) - const text = await item.textContent() - - // Look for a reference that contains customer_revenue_by_day - if (text && text.includes('customer_revenue_by_day.sql')) { - await item.click() - clickedReference = true - break - } + // Look for a reference that contains customer_revenue_by_day + if (text && text.includes('customer_revenue_by_day.sql')) { + await item.click() + clickedReference = true + break } + } - expect(clickedReference).toBe(true) + expect(clickedReference).toBe(true) - // Step 7: Verify successful navigation by checking for SQL JOIN statement - await expect(page.locator('text=LEFT JOIN')).toBeVisible() + // Step 7: Verify successful navigation by checking for SQL JOIN statement + await expect(page.locator('text=LEFT JOIN')).toBeVisible() - // Step 8: Interact with the file to verify it's fully loaded and check its content - await page.locator('text=LEFT JOIN').first().click() - await expect( - page.locator('text=FROM sushi.order_items AS oi'), - ).toBeVisible() - } finally { - await stopCodeServer(context) - } + // Step 8: Interact with the file to verify it's fully loaded and check its content + await page.locator('text=LEFT JOIN').first().click() + await expect( + page.locator('text=FROM sushi.order_items AS oi'), + ).toBeVisible() }) }) test.describe('CTE References', () => { - test('Go to references from definition of CTE', async ({ page }) => { - const context = await setupModelTestEnvironment() + test('Go to references from definition of CTE', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - await openCustomersFile(page) + await openCustomersFile(page) - // Click on the CTE definition "current_marketing_outer" at line 20 to position cursor - await page.locator('text=current_marketing_outer').first().click() + // Click on the CTE definition "current_marketing_outer" at line 20 to position cursor + await page.locator('text=current_marketing_outer').first().click() - // Use keyboard shortcut to find all references - await goToReferences(page) + // Use keyboard shortcut to find all references + await goToReferences(page) - // Wait for the references to appear - await page.waitForSelector('text=References') + // Wait for the references to appear + await page.waitForSelector('text=References') - // Wait for reference panel to populate - await page.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 2 - }, - { timeout: 5000 }, - ) + // Wait for reference panel to populate + await page.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) + + // Verify that the customers.sql file is shown in results + await expect(page.locator('text=customers.sql').first()).toBeVisible() + + // Check that both CTE definition and usage are listed in references + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing_outer AS') + await page.waitForSelector('text=FROM current_marketing_outer') + }) - // Verify that the customers.sql file is shown in results - await expect(page.locator('text=customers.sql').first()).toBeVisible() + test('Go to references from usage of CTE', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() - // Check that both CTE definition and usage are listed in references - await page.waitForSelector('text=References') - await page.waitForSelector('text=WITH current_marketing_outer AS') - await page.waitForSelector('text=FROM current_marketing_outer') - } finally { - await stopCodeServer(context) - } - }) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - test('Go to references from usage of CTE', async ({ page }) => { - const context = await setupModelTestEnvironment() + await openCustomersFile(page) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + // Click on the CTE usage this time for "current_marketing_outer" + await page.locator('text=FROM current_marketing_outer').click({ + position: { x: 80, y: 5 }, // Clicks on the usage rather than first which was definition + }) - await openCustomersFile(page) + // Use keyboard shortcut to go to references + await goToReferences(page) - // Click on the CTE usage this time for "current_marketing_outer" - await page.locator('text=FROM current_marketing_outer').click({ - position: { x: 80, y: 5 }, // Clicks on the usage rather than first which was definition - }) + // Wait for the references to appear + await page.waitForSelector('text=References') - // Use keyboard shortcut to go to references - await goToReferences(page) - - // Wait for the references to appear - await page.waitForSelector('text=References') - - // Better assertions: wait for reference panel to populate - await page.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 2 - }, - { timeout: 5000 }, - ) + // Better assertions: wait for reference panel to populate + await page.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) - await page.waitForSelector('text=References') - await page.waitForSelector('text=WITH current_marketing_outer AS') - await page.waitForSelector('text=FROM current_marketing_outer') + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing_outer AS') + await page.waitForSelector('text=FROM current_marketing_outer') - // Verify that the customers.sql file is shown in results - await expect(page.locator('text=customers.sql').first()).toBeVisible() - } finally { - await stopCodeServer(context) - } + // Verify that the customers.sql file is shown in results + await expect(page.locator('text=customers.sql').first()).toBeVisible() }) - test('Go to references for nested CTE', async ({ page }) => { - const context = await setupModelTestEnvironment() + test('Go to references for nested CTE', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - await openCustomersFile(page) + await openCustomersFile(page) - // Click on the nested CTE "current_marketing" - await page.locator('text=WITH current_marketing AS').click({ - position: { x: 100, y: 5 }, // Click on the CTE name part - }) + // Click on the nested CTE "current_marketing" + await page.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, // Click on the CTE name part + }) - // Use keyboard shortcut to find all references - await goToReferences(page) - - // Wait for the references to appear - await page.waitForSelector('text=References') - - // Wait for reference panel to populate - await page.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 2 - }, - { timeout: 5000 }, - ) + // Use keyboard shortcut to find all references + await goToReferences(page) - // Verify that the customers.sql file is shown in results - await expect(page.locator('text=customers.sql').first()).toBeVisible() + // Wait for the references to appear + await page.waitForSelector('text=References') - // Check that both CTE definition and usage are listed in references - await page.waitForSelector('text=References') - await page.waitForSelector('text=WITH current_marketing AS') - await page.waitForSelector('text=FROM current_marketing') - } finally { - await stopCodeServer(context) - } + // Wait for reference panel to populate + await page.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) + + // Verify that the customers.sql file is shown in results + await expect(page.locator('text=customers.sql').first()).toBeVisible() + + // Check that both CTE definition and usage are listed in references + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing AS') + await page.waitForSelector('text=FROM current_marketing') }) - test('Find all references for CTE', async ({ page }) => { - const context = await setupModelTestEnvironment() + test('Find all references for CTE', async ({ page, sharedCodeServer }) => { + const tempDir = await setupModelTestEnvironment() - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - await openCustomersFile(page) + await openCustomersFile(page) - // Click on the CTE definition "current_marketing_outer" - await page.locator('text=current_marketing_outer').first().click() + // Click on the CTE definition "current_marketing_outer" + await page.locator('text=current_marketing_outer').first().click() - // Use keyboard shortcut to find all references - await findAllReferences(page) + // Use keyboard shortcut to find all references + await findAllReferences(page) - // Verify references contains expected content - await page.waitForSelector('text=References') - await page.waitForSelector('text=WITH current_marketing_outer AS') - await page.waitForSelector('text=FROM current_marketing_outer') + // Verify references contains expected content + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing_outer AS') + await page.waitForSelector('text=FROM current_marketing_outer') - // Verify that the customers.sql file is shown in results - await expect(page.locator('text=customers.sql').first()).toBeVisible() - } finally { - await stopCodeServer(context) - } + // Verify that the customers.sql file is shown in results + await expect(page.locator('text=customers.sql').first()).toBeVisible() }) - test('Find all references from usage for CTE', async ({ page }) => { - const context = await setupModelTestEnvironment() + test('Find all references from usage for CTE', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - await openCustomersFile(page) + await openCustomersFile(page) - // Click on the CTE usage of "current_marketing_outer" using last - await page.locator('text=current_marketing_outer').last().click() + // Click on the CTE usage of "current_marketing_outer" using last + await page.locator('text=current_marketing_outer').last().click() - // Use keyboard shortcut to find all references - await findAllReferences(page) + // Use keyboard shortcut to find all references + await findAllReferences(page) - // Verify references contains expected content - await page.waitForSelector('text=References') - await page.waitForSelector('text=WITH current_marketing_outer AS') - await page.waitForSelector('text=FROM current_marketing_outer') + // Verify references contains expected content + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing_outer AS') + await page.waitForSelector('text=FROM current_marketing_outer') - // Verify that the customers.sql file is shown in results - await expect(page.locator('text=customers.sql').first()).toBeVisible() - } finally { - await stopCodeServer(context) - } + // Verify that the customers.sql file is shown in results + await expect(page.locator('text=customers.sql').first()).toBeVisible() }) - test('Find all references for nested CTE', async ({ page }) => { - const context = await setupModelTestEnvironment() + test('Find all references for nested CTE', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - await openCustomersFile(page) + await openCustomersFile(page) - // Click on the nested CTE "current_marketing" at line 33 - // We need to be more specific to get the inner one - await page.locator('text=WITH current_marketing AS').click({ - position: { x: 100, y: 5 }, // Click on the CTE name part - }) + // Click on the nested CTE "current_marketing" at line 33 + // We need to be more specific to get the inner one + await page.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, // Click on the CTE name part + }) - // Use keyboard shortcut to find all references - await findAllReferences(page) + // Use keyboard shortcut to find all references + await findAllReferences(page) - // Verify references contains expected content - await page.waitForSelector('text=References') - await page.waitForSelector('text=WITH current_marketing AS') - await page.waitForSelector('text=FROM current_marketing') + // Verify references contains expected content + await page.waitForSelector('text=References') + await page.waitForSelector('text=WITH current_marketing AS') + await page.waitForSelector('text=FROM current_marketing') - // Verify that the customers.sql file is shown in results - await expect(page.locator('text=customers.sql').first()).toBeVisible() - } finally { - await stopCodeServer(context) - } + // Verify that the customers.sql file is shown in results + await expect(page.locator('text=customers.sql').first()).toBeVisible() }) }) test.describe('Macro References', () => { - test('Go to References for @ADD_ONE macro', async ({ page }) => { - const context = await setupModelTestEnvironment() + test('Go to References for @ADD_ONE macro', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - await openTopWaitersFile(page) + await openTopWaitersFile(page) - // Click on the @ADD_ONE macro usage - await page.locator('text=@ADD_ONE').first().click() + // Click on the @ADD_ONE macro usage + await page.locator('text=@ADD_ONE').first().click() - // Use keyboard shortcut to find all references - await goToReferences(page) + // Use keyboard shortcut to find all references + await goToReferences(page) - // Wait for the references to appear - await page.waitForSelector('text=References') + // Wait for the references to appear + await page.waitForSelector('text=References') - // Wait for reference panel to populate - await page.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 2 - }, - { timeout: 5000 }, - ) - - // Verify that both the definition and two usages are shown - await expect(page.locator('text=utils.py').first()).toBeVisible() - await expect(page.locator('text=top_waiters.sql').first()).toBeVisible() - await expect(page.locator('text=customers.sql').first()).toBeVisible() - } finally { - await stopCodeServer(context) - } + // Wait for reference panel to populate + await page.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', + ) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) + + // Verify that both the definition and two usages are shown + await expect(page.locator('text=utils.py').first()).toBeVisible() + await expect(page.locator('text=top_waiters.sql').first()).toBeVisible() + await expect(page.locator('text=customers.sql').first()).toBeVisible() }) - test('Find All References for @MULTIPLY macro', async ({ page }) => { - const context = await setupModelTestEnvironment() + test('Find All References for @MULTIPLY macro', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - await openTopWaitersFile(page) + await openTopWaitersFile(page) - // Click on the @MULTIPLY macro usage and then navigate to it - await page.locator('text=@MULTIPLY').first().click() + // Click on the @MULTIPLY macro usage and then navigate to it + await page.locator('text=@MULTIPLY').first().click() - // Use keyboard shortcut to find all references - await findAllReferences(page) + // Use keyboard shortcut to find all references + await findAllReferences(page) - // Verify references contains expected content - await page.waitForSelector('text=References') + // Verify references contains expected content + await page.waitForSelector('text=References') - // Verify that both utils.py (definition) and top_waiters.sql (usage) are shown - await expect(page.locator('text=utils.py').first()).toBeVisible() - await expect(page.locator('text=top_waiters.sql').first()).toBeVisible() + // Verify that both utils.py (definition) and top_waiters.sql (usage) are shown + await expect(page.locator('text=utils.py').first()).toBeVisible() + await expect(page.locator('text=top_waiters.sql').first()).toBeVisible() - // Click on the utils.py reference to navigate to the macro definition - let clickedReference = false - const referenceItems = page.locator( - '.monaco-list-row, .reference-item, .monaco-tl-row', - ) - const count = await referenceItems.count() - - for (let i = 0; i < count; i++) { - const item = referenceItems.nth(i) - const text = await item.textContent() - - // Find the utils.py reference which contains the macro definition - if (text && text.includes('utils.py')) { - await item.click() - clickedReference = true - break - } + // Click on the utils.py reference to navigate to the macro definition + let clickedReference = false + const referenceItems = page.locator( + '.monaco-list-row, .reference-item, .monaco-tl-row', + ) + const count = await referenceItems.count() + + for (let i = 0; i < count; i++) { + const item = referenceItems.nth(i) + const text = await item.textContent() + + // Find the utils.py reference which contains the macro definition + if (text && text.includes('utils.py')) { + await item.click() + clickedReference = true + break } + } - expect(clickedReference).toBe(true) + expect(clickedReference).toBe(true) - // Verify it appeared and click on it - await expect(page.locator('text=def multiply')).toBeVisible() - await page.locator('text=def multiply').first().click() + // Verify it appeared and click on it + await expect(page.locator('text=def multiply')).toBeVisible() + await page.locator('text=def multiply').first().click() - // Verify navigation to utils.py by checking the import that appears there - await expect( - page.locator('text=from sqlmesh import SQL, macro'), - ).toBeVisible() - } finally { - await stopCodeServer(context) - } + // Verify navigation to utils.py by checking the import that appears there + await expect( + page.locator('text=from sqlmesh import SQL, macro'), + ).toBeVisible() }) - test('Go to References for @SQL_LITERAL macro', async ({ page }) => { - const context = await setupModelTestEnvironment() + test('Go to References for @SQL_LITERAL macro', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await setupModelTestEnvironment() - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - await openTopWaitersFile(page) + await openTopWaitersFile(page) - // Click on the @SQL_LITERAL macro usage - await page.locator('text=@SQL_LITERAL').first().click() + // Click on the @SQL_LITERAL macro usage + await page.locator('text=@SQL_LITERAL').first().click() - // Use keyboard shortcut to find references - await goToReferences(page) + // Use keyboard shortcut to find references + await goToReferences(page) - // Wait for the references to appear - await page.waitForSelector('text=References') - - // Wait for reference panel to populate - await page.waitForFunction( - () => { - const referenceElements = document.querySelectorAll( - '.reference-item, .monaco-list-row, .references-view .tree-row', - ) - return referenceElements.length >= 2 - }, - { timeout: 5000 }, - ) + // Wait for the references to appear + await page.waitForSelector('text=References') - // Verify that references include both definition and usage - const hasReferences = await page.evaluate(() => { - const body = document.body.textContent || '' - return ( - body.includes('References') && - body.includes('.py') && - body.includes('.sql') + // Wait for reference panel to populate + await page.waitForFunction( + () => { + const referenceElements = document.querySelectorAll( + '.reference-item, .monaco-list-row, .references-view .tree-row', ) - }) + return referenceElements.length >= 2 + }, + { timeout: 5000 }, + ) + + // Verify that references include both definition and usage + const hasReferences = await page.evaluate(() => { + const body = document.body.textContent || '' + return ( + body.includes('References') && + body.includes('.py') && + body.includes('.sql') + ) + }) - expect(hasReferences).toBe(true) + expect(hasReferences).toBe(true) - await expect(page.locator('text=utils.py').first()).toBeVisible() - await expect(page.locator('text=top_waiters.sql').first()).toBeVisible() - } finally { - await stopCodeServer(context) - } + await expect(page.locator('text=utils.py').first()).toBeVisible() + await expect(page.locator('text=top_waiters.sql').first()).toBeVisible() }) }) diff --git a/vscode/extension/tests/fixtures.ts b/vscode/extension/tests/fixtures.ts new file mode 100644 index 0000000000..916305c9c2 --- /dev/null +++ b/vscode/extension/tests/fixtures.ts @@ -0,0 +1,43 @@ +import { test as base } from '@playwright/test' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { + startCodeServer, + stopCodeServer, + CodeServerContext, +} from './utils_code_server' + +// Worker-scoped fixture to start/stop VS Code server once per worker +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export const test = base.extend<{}, { sharedCodeServer: CodeServerContext }>({ + sharedCodeServer: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use) => { + // Create a temporary directory for the shared server + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-shared-server-'), + ) + + // Start the code server once per worker + const context = await startCodeServer({ + tempDir, + }) + + console.log( + `Started shared VS Code server for worker ${test.info().workerIndex} on port ${context.codeServerPort}`, + ) + + // Provide the context to all tests in this worker + await use(context) + + // Clean up after all tests in this worker are done + console.log(`Stopping shared VS Code server`) + await stopCodeServer(context) + }, + { scope: 'worker', auto: true }, + ], +}) + +// Export expect and Page from Playwright for convenience +export { expect, Page } from '@playwright/test' diff --git a/vscode/extension/tests/format.spec.ts b/vscode/extension/tests/format.spec.ts index be304c00cd..7525f88fdf 100644 --- a/vscode/extension/tests/format.spec.ts +++ b/vscode/extension/tests/format.spec.ts @@ -1,52 +1,43 @@ -import { test, expect } from '@playwright/test' +import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' import { runCommand, SUSHI_SOURCE_PATH } from './utils' -import { - createPythonInterpreterSettingsSpecifier, - startCodeServer, - stopCodeServer, -} from './utils_code_server' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Format project works correctly', async ({ page }) => { +test('Format project works correctly', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - - // Wait for the models folder to be visible - await page.waitForSelector('text=models') - - // Click on the models folder, excluding external_models - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the customer_revenue_lifetime model - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') - - // Format the project - await runCommand(page, 'SQLMesh: Format Project') - - // Check that the notification appears saying 'Project formatted successfully' - await expect( - page.getByText('Project formatted successfully', { exact: true }), - ).toBeVisible() - } finally { - await stopCodeServer(context) - } + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customer_revenue_lifetime model + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') + + // Format the project + await runCommand(page, 'SQLMesh: Format Project') + + // Check that the notification appears saying 'Project formatted successfully' + await expect( + page.getByText('Project formatted successfully', { exact: true }), + ).toBeVisible() }) diff --git a/vscode/extension/tests/go_to_definition.spec.ts b/vscode/extension/tests/go_to_definition.spec.ts index 3ef20628dd..40d941669a 100644 --- a/vscode/extension/tests/go_to_definition.spec.ts +++ b/vscode/extension/tests/go_to_definition.spec.ts @@ -1,94 +1,78 @@ -import { test, expect } from '@playwright/test' +import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' import { goToDefinition, SUSHI_SOURCE_PATH } from './utils' -import { - createPythonInterpreterSettingsSpecifier, - startCodeServer, - stopCodeServer, -} from './utils_code_server' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Stop server works', async ({ page }) => { +test('Stop server works', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - // Navigate to code-server instance - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + // Navigate to code-server instance + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - // Wait for the models folder to be visible - await page.waitForSelector('text=models') + // Wait for the models folder to be visible + await page.waitForSelector('text=models') - // Click on the models folder - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() + // Click on the models folder + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() - // Open the customer_revenue_lifetime model - await page - .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) - .locator('a') - .click() + // Open the customer_revenue_lifetime model + await page + .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) + .locator('a') + .click() - await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') - // Render the model - await page.locator('text=@MULTIPLY').click() - await goToDefinition(page) + // Render the model + await page.locator('text=@MULTIPLY').click() + await goToDefinition(page) - // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window - await expect(page.locator('text=def multiply(')).toBeVisible() - } finally { - await stopCodeServer(context) - } + // Check if the model is rendered by check if "`oi`.`order_id` AS `order_id`," is in the window + await expect(page.locator('text=def multiply(')).toBeVisible() }) -test('Go to definition for model', async ({ page }) => { +test('Go to definition for model', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - // Navigate to code-server instance - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - - // Wait for the models folder to be visible - await page.waitForSelector('text=models') - - // Click on the models folder - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the top_waiters model - await page - .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) - .locator('a') - .click() - - await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') - - // Go to definition for the model - await page.locator('text=sushi.waiter_revenue_by_day').first().click() - await goToDefinition(page) - await expect( - page.locator('text=SUM(oi.quantity * i.price)::DOUBLE AS revenue'), - ).toBeVisible() - } finally { - await stopCodeServer(context) - } + // Navigate to code-server instance + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the top_waiters model + await page + .getByRole('treeitem', { name: 'top_waiters.sql', exact: true }) + .locator('a') + .click() + + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') + + // Go to definition for the model + await page.locator('text=sushi.waiter_revenue_by_day').first().click() + await goToDefinition(page) + await expect( + page.locator('text=SUM(oi.quantity * i.price)::DOUBLE AS revenue'), + ).toBeVisible() }) diff --git a/vscode/extension/tests/hints.spec.ts b/vscode/extension/tests/hints.spec.ts index eded1a97e4..13bf37fe8a 100644 --- a/vscode/extension/tests/hints.spec.ts +++ b/vscode/extension/tests/hints.spec.ts @@ -1,54 +1,43 @@ -import { test, expect } from '@playwright/test' +import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' import { SUSHI_SOURCE_PATH } from './utils' -import { - createPythonInterpreterSettingsSpecifier, - startCodeServer, - stopCodeServer, -} from './utils_code_server' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Model type hinting', async ({ page }) => { +test('Model type hinting', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - - try { - // Navigate to code-server instance - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - - // Wait for the models folder to be visible - await page.waitForSelector('text=models') - - // Click on the models folder - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the customers_revenue_by_day model - await page - .getByRole('treeitem', { - name: 'customer_revenue_by_day.sql', - exact: true, - }) - .locator('a') - .click() - - await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') - - // Wait a moment for hints to appear - await page.waitForTimeout(500) - - // Check if the hint is visible - expect(await page.locator('text="country code"::INT').count()).toBe(1) - } finally { - await stopCodeServer(context) - } + // Navigate to code-server instance + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customers_revenue_by_day model + await page + .getByRole('treeitem', { + name: 'customer_revenue_by_day.sql', + exact: true, + }) + .locator('a') + .click() + + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') + + // Wait a moment for hints to appear + await page.waitForTimeout(500) + + // Check if the hint is visible + expect(await page.locator('text="country code"::INT').count()).toBe(1) }) diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index 110dd08f61..8f88c753f0 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -1,4 +1,4 @@ -import { test, Page } from '@playwright/test' +import { test, Page } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' @@ -22,24 +22,19 @@ async function testLineageWithProjectPath(page: Page): Promise { test('Lineage panel renders correctly - no project path config (default)', async ({ page, + sharedCodeServer, }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - await testLineageWithProjectPath(page) - } finally { - await stopCodeServer(context) - } + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await testLineageWithProjectPath(page) }) -test('Lineage panel renders correctly - relative project path', async ({ +test.skip('Lineage panel renders correctly - relative project path', async ({ page, }) => { const workspaceDir = await fs.mkdtemp( @@ -71,7 +66,7 @@ test('Lineage panel renders correctly - relative project path', async ({ } }) -test('Lineage panel renders correctly - absolute project path', async ({ +test.skip('Lineage panel renders correctly - absolute project path', async ({ page, }) => { const workspaceDir = await fs.mkdtemp( @@ -103,7 +98,7 @@ test('Lineage panel renders correctly - absolute project path', async ({ } }) -test('Lineage panel renders correctly - relative project outside of workspace', async ({ +test.skip('Lineage panel renders correctly - relative project outside of workspace', async ({ page, }) => { const tempFolder = await fs.mkdtemp( @@ -137,8 +132,9 @@ test('Lineage panel renders correctly - relative project outside of workspace', } }) -test('Lineage panel renders correctly - absolute path project outside of workspace', async ({ +test.skip('Lineage panel renders correctly - absolute path project outside of workspace', async ({ page, + sharedCodeServer, }) => { const tempFolder = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), @@ -148,14 +144,10 @@ test('Lineage panel renders correctly - absolute path project outside of workspa const workspaceDir = path.join(tempFolder, 'workspace') await fs.ensureDir(workspaceDir) - const context = await startCodeServer({ - tempDir: workspaceDir, - }) - await createPythonInterpreterSettingsSpecifier(workspaceDir) const settings = { 'sqlmesh.projectPath': projectDir, - 'python.defaultInterpreterPath': context.defaultPythonInterpreter, + 'python.defaultInterpreterPath': sharedCodeServer.defaultPythonInterpreter, } await fs.ensureDir(path.join(workspaceDir, '.vscode')) await fs.writeJson( @@ -164,17 +156,16 @@ test('Lineage panel renders correctly - absolute path project outside of workspa { spaces: 2 }, ) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - await testLineageWithProjectPath(page) - } finally { - await stopCodeServer(context) - } + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}?folder=${workspaceDir}`, + ) + await testLineageWithProjectPath(page) }) // These work on local machine when debuggin but not on CI, so skipping for now test.skip('Lineage panel renders correctly - multiworkspace setup', async ({ page, + sharedCodeServer, }) => { const workspaceDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), @@ -205,13 +196,8 @@ test.skip('Lineage panel renders correctly - multiworkspace setup', async ({ }), ) - const context = await startCodeServer({ - tempDir: workspaceDir, - }) - await createPythonInterpreterSettingsSpecifier(workspaceDir) - const settings = { - 'python.defaultInterpreterPath': context.defaultPythonInterpreter, + 'python.defaultInterpreterPath': sharedCodeServer.defaultPythonInterpreter, } await fs.ensureDir(path.join(projectDir1, '.vscode')) await fs.writeJson( @@ -220,14 +206,12 @@ test.skip('Lineage panel renders correctly - multiworkspace setup', async ({ { spaces: 2 }, ) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - await page.waitForSelector('text=Open workspace') - await page.click('text=Open workspace') - await testLineageWithProjectPath(page) - } finally { - await stopCodeServer(context) - } + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}?folder=${workspaceDir}`, + ) + await page.waitForSelector('text=Open workspace') + await page.click('text=Open workspace') + await testLineageWithProjectPath(page) }) test.skip('Lineage panel renders correctly - multiworkspace setup reversed', async ({ diff --git a/vscode/extension/tests/lineage_settings.spec.ts b/vscode/extension/tests/lineage_settings.spec.ts index ce50e7275e..f9d2e88781 100644 --- a/vscode/extension/tests/lineage_settings.spec.ts +++ b/vscode/extension/tests/lineage_settings.spec.ts @@ -1,70 +1,63 @@ -import { test, expect } from '@playwright/test' +import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' import { openLineageView, SUSHI_SOURCE_PATH } from './utils' -import { - createPythonInterpreterSettingsSpecifier, - startCodeServer, - stopCodeServer, -} from './utils_code_server' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Settings button is visible in the lineage view', async ({ page }) => { +test('Settings button is visible in the lineage view', async ({ + page, + sharedCodeServer, +}) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - - await page.waitForSelector('text=models') - - // Click on the models folder, excluding external_models - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - // Open the waiters.py model - await page - .getByRole('treeitem', { name: 'waiters.py', exact: true }) - .locator('a') - .click() - await page.waitForSelector('text=Loaded SQLMesh Context') - - // Open lineage - await openLineageView(page) - - const iframes = page.locator('iframe') - const iframeCount = await iframes.count() - let settingsCount = 0 - - for (let i = 0; i < iframeCount; i++) { - const iframe = iframes.nth(i) - const contentFrame = iframe.contentFrame() - if (contentFrame) { - const activeFrame = contentFrame.locator('#active-frame').contentFrame() - if (activeFrame) { - try { - await activeFrame - .getByRole('button', { - name: 'Settings', - }) - .waitFor({ timeout: 1000 }) - settingsCount++ - } catch { - // Continue to next iframe if this one doesn't have the error - continue - } + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + await page.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + // Open the waiters.py model + await page + .getByRole('treeitem', { name: 'waiters.py', exact: true }) + .locator('a') + .click() + await page.waitForSelector('text=Loaded SQLMesh Context') + + // Open lineage + await openLineageView(page) + + const iframes = page.locator('iframe') + const iframeCount = await iframes.count() + let settingsCount = 0 + + for (let i = 0; i < iframeCount; i++) { + const iframe = iframes.nth(i) + const contentFrame = iframe.contentFrame() + if (contentFrame) { + const activeFrame = contentFrame.locator('#active-frame').contentFrame() + if (activeFrame) { + try { + await activeFrame + .getByRole('button', { + name: 'Settings', + }) + .waitFor({ timeout: 1000 }) + settingsCount++ + } catch { + // Continue to next iframe if this one doesn't have the error + continue } } } - - expect(settingsCount).toBeGreaterThan(0) - } finally { - await stopCodeServer(context) } + + expect(settingsCount).toBeGreaterThan(0) }) diff --git a/vscode/extension/tests/python_env.spec.ts b/vscode/extension/tests/python_env.spec.ts index 7c7b6f96ce..d0d8e0134d 100644 --- a/vscode/extension/tests/python_env.spec.ts +++ b/vscode/extension/tests/python_env.spec.ts @@ -1,4 +1,4 @@ -import { Page, test } from '@playwright/test' +import { test, Page } from './fixtures' import fs from 'fs-extra' import { createVirtualEnvironment, @@ -11,11 +11,7 @@ import { import os from 'os' import path from 'path' import { setTcloudVersion, setupAuthenticatedState } from './tcloud_utils' -import { - CodeServerContext, - startCodeServer, - stopCodeServer, -} from './utils_code_server' +import { CodeServerContext } from './utils_code_server' function writeEnvironmentConfig(sushiPath: string) { const configPath = path.join(sushiPath, 'config.py') @@ -33,8 +29,14 @@ if test_var is None or test_var == "": fs.writeFileSync(configPath, newConfig) } -async function runTest(page: Page, context: CodeServerContext): Promise { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) +async function runTest( + page: Page, + context: CodeServerContext, + tempDir: string, +): Promise { + await page.goto( + `http://127.0.0.1:${context.codeServerPort}` + `/?folder=${tempDir}`, + ) await page.waitForSelector('text=models') await openLineageView(page) } @@ -69,40 +71,27 @@ async function setupEnvironment(): Promise<{ } test.describe('python environment variable injection on sqlmesh_lsp', () => { - test('normal setup - error ', async ({ page }, testInfo) => { + test('normal setup - error ', async ({ + page, + sharedCodeServer, + }, testInfo) => { testInfo.setTimeout(120_000) const { tempDir } = await setupEnvironment() writeEnvironmentConfig(tempDir) - - const context = await startCodeServer({ - tempDir, - }) - - try { - await runTest(page, context) - await page.waitForSelector('text=Error creating context') - } finally { - await stopCodeServer(context) - } + await runTest(page, sharedCodeServer, tempDir) + await page.waitForSelector('text=Error creating context') }) - test('normal setup - set', async ({ page }, testInfo) => { + test('normal setup - set', async ({ page, sharedCodeServer }, testInfo) => { testInfo.setTimeout(120_000) const { tempDir } = await setupEnvironment() writeEnvironmentConfig(tempDir) const env_file = path.join(tempDir, '.env') fs.writeFileSync(env_file, 'TEST_VAR=test_value') - const context = await startCodeServer({ - tempDir, - }) - try { - await runTest(page, context) - await page.waitForSelector('text=Loaded SQLMesh context') - } finally { - await stopCodeServer(context) - } + await runTest(page, sharedCodeServer, tempDir) + await page.waitForSelector('text=Loaded SQLMesh context') }) }) @@ -131,24 +120,20 @@ async function setupTcloudProject( } test.describe('tcloud version', () => { - test('normal setup - error ', async ({ page }, testInfo) => { + test('normal setup - error ', async ({ + page, + sharedCodeServer, + }, testInfo) => { testInfo.setTimeout(120_000) const { tempDir, pythonDetails } = await setupEnvironment() await setupTcloudProject(tempDir, pythonDetails) writeEnvironmentConfig(tempDir) - const context = await startCodeServer({ - tempDir, - }) - try { - await runTest(page, context) - await page.waitForSelector('text=Error creating context') - } finally { - await stopCodeServer(context) - } + await runTest(page, sharedCodeServer, tempDir) + await page.waitForSelector('text=Error creating context') }) - test('normal setup - set', async ({ page }, testInfo) => { + test('normal setup - set', async ({ page, sharedCodeServer }, testInfo) => { testInfo.setTimeout(120_000) const { tempDir, pythonDetails } = await setupEnvironment() @@ -156,14 +141,7 @@ test.describe('tcloud version', () => { writeEnvironmentConfig(tempDir) const env_file = path.join(tempDir, '.env') fs.writeFileSync(env_file, 'TEST_VAR=test_value') - const context = await startCodeServer({ - tempDir, - }) - try { - await runTest(page, context) - await page.waitForSelector('text=Loaded SQLMesh context') - } finally { - await stopCodeServer(context) - } + await runTest(page, sharedCodeServer, tempDir) + await page.waitForSelector('text=Loaded SQLMesh context') }) }) diff --git a/vscode/extension/tests/rename_cte.spec.ts b/vscode/extension/tests/rename_cte.spec.ts index 39fb62162f..0563f023b2 100644 --- a/vscode/extension/tests/rename_cte.spec.ts +++ b/vscode/extension/tests/rename_cte.spec.ts @@ -1,25 +1,25 @@ -import { test, expect, Page } from '@playwright/test' +import { test, expect, Page } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' import { findAllReferences, renameSymbol, SUSHI_SOURCE_PATH } from './utils' -import { - createPythonInterpreterSettingsSpecifier, - startCodeServer, - stopCodeServer, -} from './utils_code_server' - -async function setupTestEnvironment({ page }: { page: Page }) { +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' + +async function setupTestEnvironment({ + page, + sharedCodeServer, +}: { + page: Page + sharedCodeServer: any +}) { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) // Navigate to code-server instance - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) // Navigate to customers.sql which contains CTEs await page.waitForSelector('text=models') @@ -33,168 +33,144 @@ async function setupTestEnvironment({ page }: { page: Page }) { .click() await page.waitForSelector('text=grain') await page.waitForSelector('text=Loaded SQLMesh Context') - - return { context } } test.describe('CTE Rename', () => { - test('Rename CTE from definition', async ({ page }) => { - const { context } = await setupTestEnvironment({ page }) - - try { - // Click on the inner CTE definition "current_marketing" (not the outer one) - await page.locator('text=WITH current_marketing AS').click({ - position: { x: 100, y: 5 }, - }) - - // Open rename - await renameSymbol(page) - await page.waitForSelector('text=Rename') - await page.waitForSelector('input:focus') - - // Type new name and confirm - await page.keyboard.type('new_marketing') - await page.keyboard.press('Enter') - - // Verify the rename was applied - await page.waitForSelector('text=WITH new_marketing AS') - } finally { - await stopCodeServer(context) - } + test('Rename CTE from definition', async ({ page, sharedCodeServer }) => { + await setupTestEnvironment({ page, sharedCodeServer }) + // Click on the inner CTE definition "current_marketing" (not the outer one) + await page.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, + }) + + // Open rename + await renameSymbol(page) + await page.waitForSelector('text=Rename') + await page.waitForSelector('input:focus') + + // Type new name and confirm + await page.keyboard.type('new_marketing') + await page.keyboard.press('Enter') + + // Verify the rename was applied + await page.waitForSelector('text=WITH new_marketing AS') }) - test('Rename CTE from usage', async ({ page }) => { - const { context } = await setupTestEnvironment({ page }) + test('Rename CTE from usage', async ({ page, sharedCodeServer }) => { + await setupTestEnvironment({ page, sharedCodeServer }) + // Click on CTE usage in FROM clause + await page.locator('text=FROM current_marketing_outer').click({ + position: { x: 80, y: 5 }, + }) - try { - // Click on CTE usage in FROM clause - await page.locator('text=FROM current_marketing_outer').click({ - position: { x: 80, y: 5 }, - }) + // Open rename + await renameSymbol(page) + await page.waitForSelector('text=Rename') + await page.waitForSelector('input:focus') - // Open rename - await renameSymbol(page) - await page.waitForSelector('text=Rename') - await page.waitForSelector('input:focus') + // Type new name + await page.keyboard.type('updated_marketing_out') - // Type new name - await page.keyboard.type('updated_marketing_out') + // Confirm rename + await page.keyboard.press('Enter') - // Confirm rename - await page.keyboard.press('Enter') - - await page.waitForSelector('text=WITH updated_marketing_out AS') - await page.waitForSelector('text=FROM updated_marketing_out') - } finally { - await stopCodeServer(context) - } + await page.waitForSelector('text=WITH updated_marketing_out AS') + await page.waitForSelector('text=FROM updated_marketing_out') }) - test('Cancel CTE rename', async ({ page }) => { - const { context } = await setupTestEnvironment({ page }) - - try { - // Click on the CTE to rename - await page.locator('text=current_marketing_outer').first().click() - - // Open rename - await renameSymbol(page) - await page.waitForSelector('text=Rename') - await page.waitForSelector('input:focus') - - // Type new name but cancel - await page.keyboard.type('cancelled_name') - await page.keyboard.press('Escape') - - // Wait for UI to update - await page.waitForTimeout(500) - - // Verify CTE name was NOT changed - await expect( - page.locator('text=current_marketing_outer').first(), - ).toBeVisible() - await expect(page.locator('text=cancelled_name')).not.toBeVisible() - } finally { - await stopCodeServer(context) - } + test('Cancel CTE rename', async ({ page, sharedCodeServer }) => { + await setupTestEnvironment({ page, sharedCodeServer }) + // Click on the CTE to rename + await page.locator('text=current_marketing_outer').first().click() + + // Open rename + await renameSymbol(page) + await page.waitForSelector('text=Rename') + await page.waitForSelector('input:focus') + + // Type new name but cancel + await page.keyboard.type('cancelled_name') + await page.keyboard.press('Escape') + + // Wait for UI to update + await page.waitForTimeout(500) + + // Verify CTE name was NOT changed + await expect( + page.locator('text=current_marketing_outer').first(), + ).toBeVisible() + await expect(page.locator('text=cancelled_name')).not.toBeVisible() }) - test('Rename CTE updates all references', async ({ page }) => { - const { context } = await setupTestEnvironment({ page }) - - try { - // Click on the CTE definition - await page.locator('text=WITH current_marketing AS').click({ - position: { x: 100, y: 5 }, - }) - - // Open rename - await renameSymbol(page) - await page.waitForSelector('text=Rename') - await page.waitForSelector('input:focus') - - // Type new name and confirm - await page.keyboard.type('renamed_cte') - await page.keyboard.press('Enter') - - // Click on the renamed CTE - await page.locator('text=WITH renamed_cte AS').click({ - position: { x: 100, y: 5 }, - }) - - // Find all references using keyboard shortcut - await findAllReferences(page) - - // Verify references panel shows all occurrences - await page.waitForSelector('text=References') - await expect(page.locator('text=customers.sql').first()).toBeVisible() - await page.waitForSelector('text=WITH renamed_cte AS') - await page.waitForSelector('text=renamed_cte.*') - await page.waitForSelector('text=FROM renamed_cte') - await page.waitForSelector('text=renamed_cte.customer_id != 100') - } finally { - await stopCodeServer(context) - } + test('Rename CTE updates all references', async ({ + page, + sharedCodeServer, + }) => { + await setupTestEnvironment({ page, sharedCodeServer }) + // Click on the CTE definition + await page.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, + }) + + // Open rename + await renameSymbol(page) + await page.waitForSelector('text=Rename') + await page.waitForSelector('input:focus') + + // Type new name and confirm + await page.keyboard.type('renamed_cte') + await page.keyboard.press('Enter') + + // Click on the renamed CTE + await page.locator('text=WITH renamed_cte AS').click({ + position: { x: 100, y: 5 }, + }) + + // Find all references using keyboard shortcut + await findAllReferences(page) + + // Verify references panel shows all occurrences + await page.waitForSelector('text=References') + await expect(page.locator('text=customers.sql').first()).toBeVisible() + await page.waitForSelector('text=WITH renamed_cte AS') + await page.waitForSelector('text=renamed_cte.*') + await page.waitForSelector('text=FROM renamed_cte') + await page.waitForSelector('text=renamed_cte.customer_id != 100') }) - test('Rename CTE with preview', async ({ page }) => { - const { context } = await setupTestEnvironment({ page }) - - try { - // Click on the CTE to rename - await page.locator('text=WITH current_marketing AS').click({ - position: { x: 100, y: 5 }, - }) - - // Open rename - await renameSymbol(page) - await page.waitForSelector('text=Rename') - await page.waitForSelector('input:focus') - - // Type new name - await page.keyboard.type('preview_marketing') - - // Press Cmd+Enter (Meta+Enter) to preview changes - await page.keyboard.press( - process.platform === 'darwin' ? 'Meta+Enter' : 'Control+Enter', - ) - - // Verify preview UI is showing - await expect(page.locator('text=Refactor Preview').first()).toBeVisible() - await expect(page.locator('text=Apply').first()).toBeVisible() - await expect(page.locator('text=Discard').first()).toBeVisible() - - // Verify the preview shows both old and new names - await expect(page.locator('text=current_marketing').first()).toBeVisible() - await expect(page.locator('text=preview_marketing').first()).toBeVisible() - - // Apply the changes - await page.locator('text=Apply').click() - - // Verify the rename was applied - await expect(page.locator('text=WITH preview_marketing AS')).toBeVisible() - } finally { - await stopCodeServer(context) - } + test('Rename CTE with preview', async ({ page, sharedCodeServer }) => { + await setupTestEnvironment({ page, sharedCodeServer }) + // Click on the CTE to rename + await page.locator('text=WITH current_marketing AS').click({ + position: { x: 100, y: 5 }, + }) + + // Open rename + await renameSymbol(page) + await page.waitForSelector('text=Rename') + await page.waitForSelector('input:focus') + + // Type new name + await page.keyboard.type('preview_marketing') + + // Press Cmd+Enter (Meta+Enter) to preview changes + await page.keyboard.press( + process.platform === 'darwin' ? 'Meta+Enter' : 'Control+Enter', + ) + + // Verify preview UI is showing + await expect(page.locator('text=Refactor Preview').first()).toBeVisible() + await expect(page.locator('text=Apply').first()).toBeVisible() + await expect(page.locator('text=Discard').first()).toBeVisible() + + // Verify the preview shows both old and new names + await expect(page.locator('text=current_marketing').first()).toBeVisible() + await expect(page.locator('text=preview_marketing').first()).toBeVisible() + + // Apply the changes + await page.locator('text=Apply').click() + + // Verify the rename was applied + await expect(page.locator('text=WITH preview_marketing AS')).toBeVisible() }) }) diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts index 6593708976..741d37ae14 100644 --- a/vscode/extension/tests/render.spec.ts +++ b/vscode/extension/tests/render.spec.ts @@ -1,189 +1,159 @@ -import { test, expect } from '@playwright/test' +import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' import { openLineageView, runCommand, SUSHI_SOURCE_PATH } from './utils' -import { - createPythonInterpreterSettingsSpecifier, - startCodeServer, - stopCodeServer, -} from './utils_code_server' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Render works correctly', async ({ page }) => { +test('Render works correctly', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - - // Wait for the models folder to be visible - await page.waitForSelector('text=models') - - // Click on the models folder, excluding external_models - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the customer_revenue_lifetime model - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') - - // Render the model - await runCommand(page, 'Render Model') - - // 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() - } finally { - await stopCodeServer(context) - } + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customer_revenue_lifetime model + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') + + // Render the model + await runCommand(page, 'Render Model') + + // 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() }) test('Render works correctly with model without a description', async ({ page, + sharedCodeServer, }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - - // Wait for the models folder to be visible - await page.waitForSelector('text=models') - - // Click on the models folder, excluding external_models - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - - // Open the latest_order model - await page - .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) - .locator('a') - .click() - - await page.waitForSelector('text=custom_full_with_custom_kind') - await page.waitForSelector('text=Loaded SQLMesh Context') - - // Render the model - await runCommand(page, 'Render Model') - - // 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() - } finally { - await stopCodeServer(context) - } + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the latest_order model + await page + .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) + .locator('a') + .click() + + await page.waitForSelector('text=custom_full_with_custom_kind') + await page.waitForSelector('text=Loaded SQLMesh Context') + + // Render the model + await runCommand(page, 'Render Model') + + // 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() }) test('Render works correctly with every rendered model opening a new tab', async ({ page, + sharedCodeServer, }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - - // Wait for the models folder to be visible - await page.waitForSelector('text=models') - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) - .locator('a') - .click() - await page.waitForSelector('text=custom_full_with_custom_kind') - await page.waitForSelector('text=Loaded SQLMesh Context') - - // Render the model - await runCommand(page, 'Render Model') - - // Check if the model is rendered correctly - await expect( - page.locator('text=sushi.latest_order (rendered)'), - ).toBeVisible() - - // Open the customers model - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - await page.waitForSelector('text=grain') - - // Render the customers model - 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() - } finally { - await stopCodeServer(context) - } + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) + .locator('a') + .click() + await page.waitForSelector('text=custom_full_with_custom_kind') + await page.waitForSelector('text=Loaded SQLMesh Context') + + // Render the model + await runCommand(page, 'Render Model') + + // Check if the model is rendered correctly + await expect(page.locator('text=sushi.latest_order (rendered)')).toBeVisible() + + // Open the customers model + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + await page.waitForSelector('text=grain') + + // Render the customers model + 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() }) test('Render shows model picker when no active editor is open', async ({ page, + sharedCodeServer, }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - // Navigate to code-server instance - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - await page.waitForLoadState('networkidle') + // Navigate to code-server instance + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') - // Load the lineage view to initialize SQLMesh context (like lineage.spec.ts does) - await openLineageView(page) + // Load the lineage view to initialize SQLMesh context (like lineage.spec.ts does) + await openLineageView(page) - // Wait for "Loaded SQLmesh Context" text to appear - await page.waitForSelector('text=Loaded SQLMesh Context') + // Wait for "Loaded SQLmesh Context" text to appear + await page.waitForSelector('text=Loaded SQLMesh Context') - // Run the render command without any active editor - await runCommand(page, 'Render Model') + // Run the render command without any active editor + await runCommand(page, 'Render Model') - // Type to filter for customers model and select it - await page.keyboard.type('customers') - await page.waitForSelector('text=sushi.customers', { timeout: 2_000 }) - await page.locator('text=sushi.customers').click() + // Type to filter for customers model and select it + await page.keyboard.type('customers') + await page.waitForSelector('text=sushi.customers', { timeout: 2_000 }) + await page.locator('text=sushi.customers').click() - // Verify the rendered model is shown - await expect(page.locator('text=sushi.customers (rendered)')).toBeVisible({ - timeout: 2_000, - }) - } finally { - await stopCodeServer(context) - } + // Verify the rendered model is shown + await expect(page.locator('text=sushi.customers (rendered)')).toBeVisible({ + timeout: 2_000, + }) }) diff --git a/vscode/extension/tests/stop.spec.ts b/vscode/extension/tests/stop.spec.ts index 2cd126f413..e9ba98fa03 100644 --- a/vscode/extension/tests/stop.spec.ts +++ b/vscode/extension/tests/stop.spec.ts @@ -1,63 +1,54 @@ import path from 'path' import { runCommand, SUSHI_SOURCE_PATH } from './utils' import os from 'os' -import { test } from '@playwright/test' +import { test } from './fixtures' import fs from 'fs-extra' -import { - createPythonInterpreterSettingsSpecifier, - startCodeServer, - stopCodeServer, -} from './utils_code_server' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Stop server works', async ({ page }) => { +test('Stop server works', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) - const context = await startCodeServer({ - tempDir, - }) await createPythonInterpreterSettingsSpecifier(tempDir) - try { - // Navigate to code-server instance - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + // Navigate to code-server instance + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) - // Wait for code-server to load - await page.waitForLoadState('networkidle') - await page.waitForSelector('[role="application"]', { timeout: 10000 }) + // Wait for code-server to load + await page.waitForLoadState('networkidle') + await page.waitForSelector('[role="application"]', { timeout: 10000 }) - // Wait for the models folder to be visible in the file explorer - await page.waitForSelector('text=models') + // Wait for the models folder to be visible in the file explorer + await page.waitForSelector('text=models') - // Click on the models folder, excluding external_models - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() - // Open the customers.sql model - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() + // Open the customers.sql model + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() - await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await page.waitForSelector('text=grain') + await page.waitForSelector('text=Loaded SQLMesh Context') - // Stop the server - await runCommand(page, 'SQLMesh: Stop Server') + // Stop the server + await runCommand(page, 'SQLMesh: Stop Server') - // Await LSP server stopped message - await page.waitForSelector('text=LSP server stopped') + // Await LSP server stopped message + await page.waitForSelector('text=LSP server stopped') - // Render the model - await runCommand(page, 'SQLMesh: Render Model') + // Render the model + await runCommand(page, 'SQLMesh: Render Model') - // Await error message - await page.waitForSelector( - 'text="Failed to render model: LSP client not ready."', - ) - } finally { - await stopCodeServer(context) - } + // Await error message + await page.waitForSelector( + 'text="Failed to render model: LSP client not ready."', + ) }) diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index c72e3ddca3..c38e61caa8 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test' +import { expect, test } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' diff --git a/vscode/extension/tests/venv_naming.spec.ts b/vscode/extension/tests/venv_naming.spec.ts index 37cffb8da3..997317672a 100644 --- a/vscode/extension/tests/venv_naming.spec.ts +++ b/vscode/extension/tests/venv_naming.spec.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test' +import { test } from './fixtures' import fs from 'fs-extra' import os from 'os' import path from 'path' @@ -9,9 +9,8 @@ import { REPO_ROOT, SUSHI_SOURCE_PATH, } from './utils' -import { startCodeServer, stopCodeServer } from './utils_code_server' -test('venv being named .env', async ({ page }, testInfo) => { +test('venv being named .env', async ({ page, sharedCodeServer }, testInfo) => { testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), @@ -36,14 +35,10 @@ test('venv being named .env', async ({ page }, testInfo) => { spaces: 2, }) - const context = await startCodeServer({ tempDir }) - - try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) - await page.waitForSelector('text=models') - await openLineageView(page) - await page.waitForSelector('text=Loaded SQLMesh Context') - } finally { - await stopCodeServer(context) - } + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForSelector('text=models') + await openLineageView(page) + await page.waitForSelector('text=Loaded SQLMesh Context') }) From aa21086d964db22f892c83dee0c252448254d432 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:06:48 +0100 Subject: [PATCH 0538/1056] chore: removing timeouts as tests are fast enough now (#4921) --- vscode/extension/tests/bad_setup.spec.ts | 6 ++---- vscode/extension/tests/python_env.spec.ts | 18 +++--------------- vscode/extension/tests/tcloud.spec.ts | 9 +++------ vscode/extension/tests/venv_naming.spec.ts | 3 +-- 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/vscode/extension/tests/bad_setup.spec.ts b/vscode/extension/tests/bad_setup.spec.ts index 4d716ce166..db3d481341 100644 --- a/vscode/extension/tests/bad_setup.spec.ts +++ b/vscode/extension/tests/bad_setup.spec.ts @@ -14,8 +14,7 @@ import { test('missing LSP dependencies shows install prompt', async ({ page, sharedCodeServer, -}, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation +}) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -111,8 +110,7 @@ test('lineage, no sqlmesh found', async ({ test.skip('check that the LSP runs correctly by opening lineage when looking at another file before not in workspace', async ({ page, sharedCodeServer, -}, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation +}) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) diff --git a/vscode/extension/tests/python_env.spec.ts b/vscode/extension/tests/python_env.spec.ts index d0d8e0134d..bd5dfff3a3 100644 --- a/vscode/extension/tests/python_env.spec.ts +++ b/vscode/extension/tests/python_env.spec.ts @@ -71,12 +71,7 @@ async function setupEnvironment(): Promise<{ } test.describe('python environment variable injection on sqlmesh_lsp', () => { - test('normal setup - error ', async ({ - page, - sharedCodeServer, - }, testInfo) => { - testInfo.setTimeout(120_000) - + test('normal setup - error ', async ({ page, sharedCodeServer }) => { const { tempDir } = await setupEnvironment() writeEnvironmentConfig(tempDir) await runTest(page, sharedCodeServer, tempDir) @@ -120,12 +115,7 @@ async function setupTcloudProject( } test.describe('tcloud version', () => { - test('normal setup - error ', async ({ - page, - sharedCodeServer, - }, testInfo) => { - testInfo.setTimeout(120_000) - + test('normal setup - error ', async ({ page, sharedCodeServer }) => { const { tempDir, pythonDetails } = await setupEnvironment() await setupTcloudProject(tempDir, pythonDetails) writeEnvironmentConfig(tempDir) @@ -133,9 +123,7 @@ test.describe('tcloud version', () => { await page.waitForSelector('text=Error creating context') }) - test('normal setup - set', async ({ page, sharedCodeServer }, testInfo) => { - testInfo.setTimeout(120_000) - + test('normal setup - set', async ({ page, sharedCodeServer }) => { const { tempDir, pythonDetails } = await setupEnvironment() await setupTcloudProject(tempDir, pythonDetails) writeEnvironmentConfig(tempDir) diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index c38e61caa8..b677acaa27 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -38,8 +38,7 @@ async function setupPythonEnvironment(envDir: string): Promise { return pythonDetails.pythonPath } -test('not signed in, shows sign in window', async ({ page }, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation +test('not signed in, shows sign in window', async ({ page }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -189,8 +188,7 @@ test('signed in and not installed shows installation window', async ({ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when ready', async ({ page, -}, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation +}) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -351,8 +349,7 @@ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when read // but the test is still useful when running it manually. test.skip('tcloud not signed in and not installed, shows sign in window and then fact that loaded', async ({ page, -}, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation +}) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) diff --git a/vscode/extension/tests/venv_naming.spec.ts b/vscode/extension/tests/venv_naming.spec.ts index 997317672a..4aeab2dcca 100644 --- a/vscode/extension/tests/venv_naming.spec.ts +++ b/vscode/extension/tests/venv_naming.spec.ts @@ -10,8 +10,7 @@ import { SUSHI_SOURCE_PATH, } from './utils' -test('venv being named .env', async ({ page, sharedCodeServer }, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation +test('venv being named .env', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) From 7fb652eed68d530b22da7546e62bef74ab932984 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Mon, 7 Jul 2025 19:02:28 +0300 Subject: [PATCH 0539/1056] Fix!: Always recreate materialized view (#4908) --- sqlmesh/core/snapshot/evaluator.py | 18 ++----- .../integration/test_integration_bigquery.py | 47 +++++++++++++++++++ tests/core/test_snapshot_evaluator.py | 21 +-------- 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index eff458dc5d..ee434cbf12 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -1876,20 +1876,12 @@ def insert( ) snapshot = kwargs["snapshot"] snapshots = kwargs["snapshots"] + if ( - ( - isinstance(query_or_df, exp.Expression) - and snapshot.is_materialized_view - and deployability_index.is_deployable(snapshot) - and model.render_query( - snapshots=snapshots, - deployability_index=deployability_index, - engine_adapter=self.adapter, - ) - == query_or_df - ) - or self.adapter.HAS_VIEW_BINDING - ) and self.adapter.table_exists(table_name): + not snapshot.is_materialized_view + and self.adapter.HAS_VIEW_BINDING + and self.adapter.table_exists(table_name) + ): logger.info("Skipping creation of the view '%s'", table_name) return diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index 2296ed3f23..e974d79da2 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -400,3 +400,50 @@ def test_table_diff_table_name_matches_column_name(ctx: TestContext): assert row_diff.stats["join_count"] == 1 assert row_diff.full_match_count == 1 + + +def test_materialized_view_evaluation(ctx: TestContext, engine_adapter: BigQueryEngineAdapter): + model_name = ctx.table("test_tbl") + mview_name = ctx.table("test_mview") + + sqlmesh = ctx.create_context() + + sqlmesh.upsert_model( + load_sql_based_model( + d.parse( + f""" + MODEL (name {model_name}, kind FULL); + + SELECT 1 AS col + """ + ) + ) + ) + + sqlmesh.upsert_model( + load_sql_based_model( + d.parse( + f""" + MODEL (name {mview_name}, kind VIEW (materialized true)); + + SELECT * FROM {model_name} + """ + ) + ) + ) + + # Case 1: Ensure that plan is successful and we can query the materialized view + sqlmesh.plan(auto_apply=True, no_prompts=True) + + df = engine_adapter.fetchdf(f"SELECT * FROM {mview_name.sql(dialect=ctx.dialect)}") + assert df["col"][0] == 1 + + # Case 2: Ensure that we can change the underlying table and the materialized view is recreated + sqlmesh.upsert_model( + load_sql_based_model(d.parse(f"""MODEL (name {model_name}, kind FULL); SELECT 2 AS col""")) + ) + + sqlmesh.plan(auto_apply=True, no_prompts=True) + + df = engine_adapter.fetchdf(f"SELECT * FROM {mview_name.sql(dialect=ctx.dialect)}") + assert df["col"][0] == 2 diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index dace2d93ac..2888511ba1 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -516,25 +516,8 @@ def test_evaluate_materialized_view( snapshots={}, ) - adapter_mock.table_exists.assert_called_once_with(snapshot.table_name()) - - if view_exists: - # Evaluation shouldn't take place because the rendered query hasn't changed - # since the last view creation. - assert not adapter_mock.create_view.called - else: - # If the view doesn't exist, it should be created even if the rendered query - # hasn't changed since the last view creation. - adapter_mock.create_view.assert_called_once_with( - snapshot.table_name(), - model.render_query(), - model.columns_to_types, - replace=True, - materialized=True, - view_properties={}, - table_description=None, - column_descriptions={}, - ) + # Ensure that the materialized view is recreated even if it exists + assert adapter_mock.create_view.assert_called def test_evaluate_materialized_view_with_partitioned_by_cluster_by( From 7d69f1cf30a027ae0860579aef022df7c885eedc Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Mon, 7 Jul 2025 19:24:31 +0300 Subject: [PATCH 0540/1056] Chore!: Reintroduce tagging queries with correlation ID (#4895) --- sqlmesh/core/context.py | 18 ++++++++++++------ sqlmesh/core/engine_adapter/base.py | 14 +++++++++----- sqlmesh/core/plan/evaluator.py | 11 ++++++++--- sqlmesh/core/snapshot/evaluator.py | 13 ++++++++++++- tests/core/test_integration.py | 26 ++++++++++++++++++++++++++ tests/core/test_table_diff.py | 4 ++-- 6 files changed, 69 insertions(+), 17 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index c8cfbda03c..51504ed4f2 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -453,7 +453,7 @@ def snapshot_evaluator(self) -> SnapshotEvaluator: if not self._snapshot_evaluator: self._snapshot_evaluator = SnapshotEvaluator( { - gateway: adapter.with_settings(log_level=logging.INFO) + gateway: adapter.with_settings(execute_log_level=logging.INFO) for gateway, adapter in self.engine_adapters.items() }, ddl_concurrent_tasks=self.concurrent_tasks, @@ -520,7 +520,11 @@ def upsert_model(self, model: t.Union[str, Model], **kwargs: t.Any) -> Model: return model - def scheduler(self, environment: t.Optional[str] = None) -> Scheduler: + def scheduler( + self, + environment: t.Optional[str] = None, + snapshot_evaluator: t.Optional[SnapshotEvaluator] = None, + ) -> Scheduler: """Returns the built-in scheduler. Args: @@ -542,9 +546,11 @@ def scheduler(self, environment: t.Optional[str] = None) -> Scheduler: if not snapshots: raise ConfigError("No models were found") - return self.create_scheduler(snapshots) + return self.create_scheduler(snapshots, snapshot_evaluator or self.snapshot_evaluator) - def create_scheduler(self, snapshots: t.Iterable[Snapshot]) -> Scheduler: + def create_scheduler( + self, snapshots: t.Iterable[Snapshot], snapshot_evaluator: SnapshotEvaluator + ) -> Scheduler: """Creates the built-in scheduler. Args: @@ -555,7 +561,7 @@ def create_scheduler(self, snapshots: t.Iterable[Snapshot]) -> Scheduler: """ return Scheduler( snapshots, - self.snapshot_evaluator, + snapshot_evaluator, self.state_sync, default_catalog=self.default_catalog, max_workers=self.concurrent_tasks, @@ -1931,7 +1937,7 @@ def _table_diff( ) return TableDiff( - adapter=adapter.with_settings(logger.getEffectiveLevel()), + adapter=adapter.with_settings(execute_log_level=logger.getEffectiveLevel()), source=source, target=target, on=on, diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 8740177837..1d34ff1401 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -147,19 +147,23 @@ def __init__( self._multithreaded = multithreaded self.correlation_id = correlation_id - def with_settings(self, log_level: int, **kwargs: t.Any) -> EngineAdapter: + def with_settings(self, **kwargs: t.Any) -> EngineAdapter: + extra_kwargs = { + "null_connection": True, + "execute_log_level": kwargs.pop("execute_log_level", self._execute_log_level), + **self._extra_config, + **kwargs, + } + adapter = self.__class__( self._connection_pool, dialect=self.dialect, sql_gen_kwargs=self._sql_gen_kwargs, default_catalog=self._default_catalog, - execute_log_level=log_level, register_comments=self._register_comments, - null_connection=True, multithreaded=self._multithreaded, pretty_sql=self._pretty_sql, - **self._extra_config, - **kwargs, + **extra_kwargs, ) return adapter diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 03f8bdcf71..545a5e5494 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -38,6 +38,7 @@ ) from sqlmesh.utils import to_snake_case from sqlmesh.core.state_sync import StateSync +from sqlmesh.utils import CorrelationId from sqlmesh.utils.concurrency import NodeExecutionFailedError from sqlmesh.utils.errors import PlanError, SQLMeshError from sqlmesh.utils.dag import DAG @@ -71,7 +72,7 @@ def __init__( self, state_sync: StateSync, snapshot_evaluator: SnapshotEvaluator, - create_scheduler: t.Callable[[t.Iterable[Snapshot]], Scheduler], + create_scheduler: t.Callable[[t.Iterable[Snapshot], SnapshotEvaluator], Scheduler], default_catalog: t.Optional[str], console: t.Optional[Console] = None, ): @@ -88,6 +89,9 @@ def evaluate( circuit_breaker: t.Optional[t.Callable[[], bool]] = None, ) -> None: self._circuit_breaker = circuit_breaker + self.snapshot_evaluator = self.snapshot_evaluator.set_correlation_id( + CorrelationId.from_plan_id(plan.plan_id) + ) self.console.start_plan_evaluation(plan) analytics.collector.on_plan_apply_start( @@ -106,6 +110,7 @@ def evaluate( else: analytics.collector.on_plan_apply_end(plan_id=plan.plan_id) finally: + self.snapshot_evaluator.recycle() self.console.stop_plan_evaluation() def _evaluate_stages( @@ -228,7 +233,7 @@ def visit_backfill_stage(self, stage: stages.BackfillStage, plan: EvaluatablePla self.console.log_success("SKIP: No model batches to execute") return - scheduler = self.create_scheduler(stage.all_snapshots.values()) + scheduler = self.create_scheduler(stage.all_snapshots.values(), self.snapshot_evaluator) errors, _ = scheduler.run_merged_intervals( merged_intervals=stage.snapshot_to_intervals, deployability_index=stage.deployability_index, @@ -249,7 +254,7 @@ def visit_audit_only_run_stage( return # If there are any snapshots to be audited, we'll reuse the scheduler's internals to audit them - scheduler = self.create_scheduler(audit_snapshots) + scheduler = self.create_scheduler(audit_snapshots, self.snapshot_evaluator) completion_status = scheduler.audit( plan.environment, plan.start, diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index ee434cbf12..641f216699 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -61,7 +61,7 @@ SnapshotTableCleanupTask, ) from sqlmesh.core.snapshot.definition import parent_snapshots_by_name -from sqlmesh.utils import random_id +from sqlmesh.utils import random_id, CorrelationId from sqlmesh.utils.concurrency import ( concurrent_apply_to_snapshots, concurrent_apply_to_values, @@ -127,6 +127,7 @@ def __init__( if not selected_gateway else self.adapters[selected_gateway] ) + self.selected_gateway = selected_gateway self.ddl_concurrent_tasks = ddl_concurrent_tasks def evaluate( @@ -1186,6 +1187,16 @@ def _execute_create( ) adapter.execute(snapshot.model.render_post_statements(**create_render_kwargs)) + def set_correlation_id(self, correlation_id: CorrelationId) -> SnapshotEvaluator: + return SnapshotEvaluator( + { + gateway: adapter.with_settings(correlation_id=correlation_id) + for gateway, adapter in self.adapters.items() + }, + self.ddl_concurrent_tasks, + self.selected_gateway, + ) + def _evaluation_strategy(snapshot: SnapshotInfoLike, adapter: EngineAdapter) -> EvaluationStrategy: klass: t.Type diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 91221d73af..8923c4c75b 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -71,6 +71,7 @@ from sqlmesh.utils.errors import NoChangesPlanError, SQLMeshError, PlanError, ConfigError from sqlmesh.utils.pydantic import validate_string from tests.conftest import DuckDBMetadata, SushiDataValidator +from sqlmesh.utils import CorrelationId from tests.utils.test_helpers import use_terminal_console from tests.utils.test_filesystem import create_temp_file @@ -6815,3 +6816,28 @@ def test_scd_type_2_full_restatement_no_start_date(init_and_plan_context: t.Call # valid_from should be the epoch, valid_to should be NaT assert str(row["valid_from"]) == "1970-01-01 00:00:00" assert pd.isna(row["valid_to"]) + + +def test_plan_evaluator_correlation_id(tmp_path: Path): + def _correlation_id_in_sqls(correlation_id: CorrelationId, mock_logger): + sqls = [call[0][0] for call in mock_logger.call_args_list] + return any(f"/* {correlation_id} */" in sql for sql in sqls) + + ctx = Context(paths=[tmp_path], config=Config()) + + # Case: Ensure that the correlation id (plan_id) is included in the SQL for each plan + for i in range(2): + create_temp_file( + tmp_path, + Path("models", "test.sql"), + f"MODEL (name test.a, kind FULL); SELECT {i} AS col", + ) + + with mock.patch("sqlmesh.core.engine_adapter.base.EngineAdapter._log_sql") as mock_logger: + ctx.load() + plan = ctx.plan(auto_apply=True, no_prompts=True) + + correlation_id = CorrelationId.from_plan_id(plan.plan_id) + assert str(correlation_id) == f"SQLMESH_PLAN: {plan.plan_id}" + + assert _correlation_id_in_sqls(correlation_id, mock_logger) diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index 1b5c39e2dd..9ea0d64771 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -337,9 +337,9 @@ def test_generated_sql(sushi_context_fixed_date: Context, mocker: MockerFixture) # make with_settings() return the current instance of engine_adapter so we can still spy on _execute mocker.patch.object( - engine_adapter, "with_settings", new_callable=lambda: lambda _: engine_adapter + engine_adapter, "with_settings", new_callable=lambda: lambda **kwargs: engine_adapter ) - assert engine_adapter.with_settings(1) == engine_adapter + assert engine_adapter.with_settings() == engine_adapter spy_execute = mocker.spy(engine_adapter, "_execute") mocker.patch("sqlmesh.core.engine_adapter.base.random_id", return_value="abcdefgh") From 1ecfedf5b1246078150e7d561fc5f1f4a9e3aacb Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 7 Jul 2025 19:26:52 +0300 Subject: [PATCH 0541/1056] Fix!: parse runtime-rendered fields, extract python env deps from merge_filter (#4905) --- sqlmesh/core/model/common.py | 13 ++++ sqlmesh/core/model/definition.py | 20 ++++-- tests/core/test_model.py | 116 ++++++++++++++++++++++++++++--- 3 files changed, 134 insertions(+), 15 deletions(-) diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index 1d8ae14442..c843213f2a 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -440,6 +440,19 @@ def _executable_to_str(k: str, v: Executable) -> str: return [_executable_to_str(k, v) for k, v in sort_python_env(python_env)] +def parse_strings_with_macro_refs(value: t.Any, dialect: DialectType) -> t.Any: + if isinstance(value, str) and "@" in value: + return exp.maybe_parse(value, dialect=dialect) + + if isinstance(value, dict): + for k, v in dict(value).items(): + value[k] = parse_strings_with_macro_refs(v, dialect) + elif isinstance(value, list): + value = [parse_strings_with_macro_refs(v, dialect) for v in value] + + return value + + expression_validator: t.Callable = field_validator( "query", "expressions_", diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 0808722119..c05dd44498 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -27,6 +27,7 @@ expression_validator, make_python_env, parse_dependencies, + parse_strings_with_macro_refs, single_value_or_tuple, sorted_python_env_payloads, validate_extra_and_required_fields, @@ -72,13 +73,14 @@ logger = logging.getLogger(__name__) + +UNRENDERABLE_MODEL_FIELDS = {"cron", "description"} + PROPERTIES = {"physical_properties", "session_properties", "virtual_properties"} RUNTIME_RENDERED_MODEL_FIELDS = { "audits", "signals", - "description", - "cron", "merge_filter", } | PROPERTIES @@ -2469,6 +2471,9 @@ def _create_model( if isinstance(property_values, exp.Tuple): statements.extend(property_values.expressions) + if isinstance(getattr(kwargs.get("kind"), "merge_filter", None), exp.Expression): + statements.append(kwargs["kind"].merge_filter) + jinja_macro_references, used_variables = extract_macro_references_and_variables( *(gen(e if isinstance(e, exp.Expression) else e[0]) for e in statements) ) @@ -2751,21 +2756,24 @@ def render_field_value(value: t.Any) -> t.Any: for field_name, field_info in ModelMeta.all_field_infos().items(): field = field_info.alias or field_name + field_value = fields.get(field) - if field in RUNTIME_RENDERED_MODEL_FIELDS: + # We don't want to parse python model cron="@..." kwargs (e.g. @daily) into MacroVar + if field == "cron" or field_value is None: continue - field_value = fields.get(field) - if field_value is None: + if field in RUNTIME_RENDERED_MODEL_FIELDS: + fields[field] = parse_strings_with_macro_refs(field_value, dialect) continue if isinstance(field_value, dict): rendered_dict = {} for key, value in field_value.items(): if key in RUNTIME_RENDERED_MODEL_FIELDS: - rendered_dict[key] = value + rendered_dict[key] = parse_strings_with_macro_refs(value, dialect) elif (rendered := render_field_value(value)) is not None: rendered_dict[key] = rendered + if rendered_dict: fields[field] = rendered_dict else: diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 6fe0ccc87d..e025930f30 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -6436,7 +6436,7 @@ def end_date_macro(evaluator: MacroEvaluator, var: bool): owner="@IF(@gateway = 'dev', @{dev_owner}, @{prod_owner})", stamp="@{stamp}", tags=["@{tag1}", "@{tag2}"], - description="Model desc @{test_}", + description="'Model desc @{test_}'", ) def model_with_macros(evaluator, **kwargs): return exp.select( @@ -6480,6 +6480,87 @@ def model_with_macros(evaluator, **kwargs): assert query.sql() == """SELECT 'test_value' AS "a" """.strip() +def test_unrendered_macros_sql_model(mocker: MockerFixture) -> None: + model = load_sql_based_model( + parse( + """ + MODEL ( + name db.employees, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key @{key}, + merge_filter source.id > 0 and target.updated_at < @end_ds and source.updated_at > @start_ds and @merge_filter_var + ), + cron '@daily', + allow_partials @IF(@gateway = 'dev', True, False), + physical_properties ( + location1 = @'s3://bucket/prefix/@{schema_name}/@{table_name}', + location2 = @IF(@gateway = 'dev', @'hdfs://@{catalog_name}/@{schema_name}/dev/@{table_name}', @'s3://prod/@{table_name}'), + foo = @physical_var + ), + virtual_properties ( + creatable_type = @{create_type}, + bar = @virtual_var, + ), + session_properties ( + 'spark.executor.cores' = @IF(@gateway = 'dev', 1, 2), + 'spark.executor.memory' = '1G', + baz = @session_var + ), + ); + + SELECT * FROM src; + """ + ), + variables={ + "gateway": "dev", + "key": "a", # Not included in python_env because kind is rendered at load time + "create_type": "'SECURE'", + "merge_filter_var": True, + "physical_var": "bla", + "virtual_var": "blb", + "session_var": "blc", + }, + ) + + assert model.python_env[c.SQLMESH_VARS] == Executable.value( + { + "gateway": "dev", + "create_type": "'SECURE'", + "merge_filter_var": True, + "physical_var": "bla", + "virtual_var": "blb", + "session_var": "blc", + } + ) + + assert "location1" in model.physical_properties + assert "location2" in model.physical_properties + + # The properties will stay unrendered at load time + assert model.session_properties == { + "spark.executor.cores": exp.maybe_parse("@IF(@gateway = 'dev', 1, 2)"), + "spark.executor.memory": "1G", + "baz": exp.maybe_parse("@session_var"), + } + assert model.virtual_properties["creatable_type"] == exp.maybe_parse("@{create_type}") + + assert ( + model.physical_properties["location1"].sql() + == "@'s3://bucket/prefix/@{schema_name}/@{table_name}'" + ) + assert ( + model.physical_properties["location2"].sql() + == "@IF(@gateway = 'dev', @'hdfs://@{catalog_name}/@{schema_name}/dev/@{table_name}', @'s3://prod/@{table_name}')" + ) + + # merge_filter will stay unrendered as well + assert model.unique_key[0] == exp.column("a", quoted=True) + assert ( + t.cast(exp.Expression, model.merge_filter).sql() + == '"__merge_source__"."id" > 0 AND "__merge_target__"."updated_at" < @end_ds AND "__merge_source__"."updated_at" > @start_ds AND @merge_filter_var' + ) + + def test_unrendered_macros_python_model(mocker: MockerFixture) -> None: @model( "test_unrendered_macros_python_model_@{bar}", @@ -6487,7 +6568,7 @@ def test_unrendered_macros_python_model(mocker: MockerFixture) -> None: kind=dict( name=ModelKindName.INCREMENTAL_BY_UNIQUE_KEY, unique_key="@{key}", - merge_filter="source.id > 0 and target.updated_at < @end_ds and source.updated_at > @start_ds", + merge_filter="source.id > 0 and target.updated_at < @end_ds and source.updated_at > @start_ds and @merge_filter_var", ), cron="@daily", columns={"a": "string"}, @@ -6495,11 +6576,13 @@ def test_unrendered_macros_python_model(mocker: MockerFixture) -> None: physical_properties=dict( location1="@'s3://bucket/prefix/@{schema_name}/@{table_name}'", location2="@IF(@gateway = 'dev', @'hdfs://@{catalog_name}/@{schema_name}/dev/@{table_name}', @'s3://prod/@{table_name}')", + foo="@physical_var", ), - virtual_properties={"creatable_type": "@{create_type}"}, + virtual_properties={"creatable_type": "@{create_type}", "bar": "@virtual_var"}, session_properties={ "spark.executor.cores": "@IF(@gateway = 'dev', 1, 2)", "spark.executor.memory": "1G", + "baz": "@session_var", }, ) def model_with_macros(evaluator, **kwargs): @@ -6517,12 +6600,24 @@ def model_with_macros(evaluator, **kwargs): "gateway": "dev", "key": "a", "create_type": "'SECURE'", + "merge_filter_var": True, + "physical_var": "bla", + "virtual_var": "blb", + "session_var": "blc", }, ) assert python_sql_model.name == "test_unrendered_macros_python_model_suffix" assert python_sql_model.python_env[c.SQLMESH_VARS] == Executable.value( - {"test_var_a": "test_value"} + { + "test_var_a": "test_value", + "gateway": "dev", + "create_type": "'SECURE'", + "merge_filter_var": True, + "physical_var": "bla", + "virtual_var": "blb", + "session_var": "blc", + } ) assert python_sql_model.enabled @@ -6536,17 +6631,20 @@ def model_with_macros(evaluator, **kwargs): # The properties will stay unrendered at load time assert python_sql_model.session_properties == { - "spark.executor.cores": "@IF(@gateway = 'dev', 1, 2)", + "spark.executor.cores": exp.maybe_parse("@IF(@gateway = 'dev', 1, 2)"), "spark.executor.memory": "1G", + "baz": exp.maybe_parse("@session_var"), } - assert python_sql_model.virtual_properties["creatable_type"] == exp.convert("@{create_type}") + assert python_sql_model.virtual_properties["creatable_type"] == exp.maybe_parse( + "@{create_type}" + ) assert ( - python_sql_model.physical_properties["location1"].text("this") + python_sql_model.physical_properties["location1"].sql() == "@'s3://bucket/prefix/@{schema_name}/@{table_name}'" ) assert ( - python_sql_model.physical_properties["location2"].text("this") + python_sql_model.physical_properties["location2"].sql() == "@IF(@gateway = 'dev', @'hdfs://@{catalog_name}/@{schema_name}/dev/@{table_name}', @'s3://prod/@{table_name}')" ) @@ -6554,7 +6652,7 @@ def model_with_macros(evaluator, **kwargs): assert python_sql_model.unique_key[0] == exp.column("a", quoted=True) assert ( python_sql_model.merge_filter.sql() - == '"source"."id" > 0 AND "target"."updated_at" < @end_ds AND "source"."updated_at" > @start_ds' + == '"__merge_source__"."id" > 0 AND "__merge_target__"."updated_at" < @end_ds AND "__merge_source__"."updated_at" > @start_ds AND @merge_filter_var' ) From 70e60acc2c213af6914ce1fe3e1061dadbd0ebee Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 7 Jul 2025 10:04:29 -0700 Subject: [PATCH 0542/1056] Fix: Don't remove dev intervals when restating in production environment (#4922) --- sqlmesh/core/state_sync/db/interval.py | 11 +++++------ tests/core/state_sync/test_state_sync.py | 14 ++++++-------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/sqlmesh/core/state_sync/db/interval.py b/sqlmesh/core/state_sync/db/interval.py index a31873132c..bebf41d453 100644 --- a/sqlmesh/core/state_sync/db/interval.py +++ b/sqlmesh/core/state_sync/db/interval.py @@ -111,12 +111,11 @@ def remove_intervals( snapshot_ids = ", ".join(str(s.snapshot_id) for s, _ in intervals_to_remove) logger.info("Removing interval for snapshots: %s", snapshot_ids) - for is_dev in (True, False): - self.engine_adapter.insert_append( - self.intervals_table, - _intervals_to_df(intervals_to_remove, is_dev=is_dev, is_removed=True), - columns_to_types=self._interval_columns_to_types, - ) + self.engine_adapter.insert_append( + self.intervals_table, + _intervals_to_df(intervals_to_remove, is_dev=False, is_removed=True), + columns_to_types=self._interval_columns_to_types, + ) def get_snapshot_intervals( self, snapshots: t.Collection[SnapshotNameVersionLike] diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index f0b1bf00a9..b5b4a42fde 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -326,9 +326,7 @@ def test_remove_interval(state_sync: EngineAdapterStateSync, make_snapshot: t.Ca 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 == num_of_removals * 4 - ) # (1 dev record + 1 prod record) * 2 snapshots + assert remove_records_count == num_of_removals * 2 # 2 * snapshots snapshots = state_sync.get_snapshots([snapshot_a, snapshot_b]) @@ -1634,7 +1632,7 @@ def test_delete_expired_snapshots_cleanup_intervals_shared_dev_version( (to_timestamp("2023-01-01"), to_timestamp("2023-01-04")), ] assert stored_new_snapshot.dev_intervals == [ - (to_timestamp("2023-01-04"), to_timestamp("2023-01-10")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-11")), ] # Check old snapshot's intervals @@ -1643,7 +1641,7 @@ def test_delete_expired_snapshots_cleanup_intervals_shared_dev_version( (to_timestamp("2023-01-01"), to_timestamp("2023-01-04")), ] assert stored_snapshot.dev_intervals == [ - (to_timestamp("2023-01-04"), to_timestamp("2023-01-10")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-11")), ] # Check all intervals @@ -1665,7 +1663,7 @@ def test_delete_expired_snapshots_cleanup_intervals_shared_dev_version( identifier=new_snapshot.identifier, version=snapshot.version, dev_version=new_snapshot.dev_version, - dev_intervals=[(to_timestamp("2023-01-08"), to_timestamp("2023-01-10"))], + dev_intervals=[(to_timestamp("2023-01-08"), to_timestamp("2023-01-11"))], ), ], key=compare_snapshot_intervals, @@ -1681,7 +1679,7 @@ def test_delete_expired_snapshots_cleanup_intervals_shared_dev_version( (to_timestamp("2023-01-01"), to_timestamp("2023-01-04")), ] assert stored_new_snapshot.dev_intervals == [ - (to_timestamp("2023-01-04"), to_timestamp("2023-01-10")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-11")), ] # Check all intervals @@ -1709,7 +1707,7 @@ def test_delete_expired_snapshots_cleanup_intervals_shared_dev_version( identifier=new_snapshot.identifier, version=snapshot.version, dev_version=new_snapshot.dev_version, - dev_intervals=[(to_timestamp("2023-01-08"), to_timestamp("2023-01-10"))], + dev_intervals=[(to_timestamp("2023-01-08"), to_timestamp("2023-01-11"))], ), ], key=compare_snapshot_intervals, From 701efe7888319c08d619e137bb23c9c2c944ae49 Mon Sep 17 00:00:00 2001 From: Sung Won Chung Date: Mon, 7 Jul 2025 10:05:19 -0700 Subject: [PATCH 0543/1056] vscode-tutorial (#4923) --- docs/guides/vscode.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guides/vscode.md b/docs/guides/vscode.md index 5edebe3be9..151e630f27 100644 --- a/docs/guides/vscode.md +++ b/docs/guides/vscode.md @@ -1,5 +1,7 @@ # Visual Studio Code Extension +
    + !!! danger "Preview" The SQLMesh Visual Studio Code extension is in preview and undergoing active development. You may encounter bugs or API incompatibilities with the SQLMesh version you are running. From a41211001ea47c25d125ccb51ea3d5d187f6f61c Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 7 Jul 2025 18:43:27 +0100 Subject: [PATCH 0544/1056] chore: make _path optional and more representative (#4303) --- sqlmesh/core/context.py | 10 ++++---- sqlmesh/core/macros.py | 6 ++--- sqlmesh/core/model/definition.py | 25 +++++++++++++------- sqlmesh/core/node.py | 2 +- sqlmesh/core/renderer.py | 2 +- sqlmesh/core/snapshot/definition.py | 2 +- sqlmesh/dbt/converter/convert.py | 6 +++++ sqlmesh/lsp/reference.py | 8 +++++++ sqlmesh/magics.py | 10 ++++---- tests/core/test_model.py | 4 ++-- tests/integrations/github/cicd/conftest.py | 2 +- vscode/openapi.json | 27 ++++++++++------------ vscode/react/src/api/client.ts | 8 +++++-- vscode/react/src/pages/lineage.tsx | 12 +++++++--- web/server/api/endpoints/models.py | 5 ++-- web/server/models.py | 4 ++-- web/server/settings.py | 2 +- 17 files changed, 85 insertions(+), 50 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 51504ed4f2..21f629f07d 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -948,10 +948,12 @@ def config_for_path(self, path: Path) -> t.Tuple[Config, Path]: def config_for_node(self, node: str | Model | Audit) -> Config: if isinstance(node, str): - return self.config_for_path(self.get_snapshot(node, raise_if_missing=True).node._path)[ - 0 - ] # type: ignore - return self.config_for_path(node._path)[0] # type: ignore + path = self.get_snapshot(node, raise_if_missing=True).node._path + else: + path = node._path + if path is None: + return self.config + return self.config_for_path(path)[0] # type: ignore @property def models(self) -> MappingProxyType[str, Model]: diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index 4afb5fd334..2c5ef3b2e8 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -171,7 +171,7 @@ def __init__( resolve_tables: t.Optional[t.Callable[[exp.Expression], exp.Expression]] = None, snapshots: t.Optional[t.Dict[str, Snapshot]] = None, default_catalog: t.Optional[str] = None, - path: Path = Path(), + path: t.Optional[Path] = None, environment_naming_info: t.Optional[EnvironmentNamingInfo] = None, model_fqn: t.Optional[str] = None, ): @@ -1384,7 +1384,7 @@ def normalize_macro_name(name: str) -> str: def call_macro( func: t.Callable, dialect: DialectType, - path: Path, + path: t.Optional[Path], provided_args: t.Tuple[t.Any, ...], provided_kwargs: t.Dict[str, t.Any], **optional_kwargs: t.Any, @@ -1431,7 +1431,7 @@ def _coerce( expr: t.Any, typ: t.Any, dialect: DialectType, - path: Path, + path: t.Optional[Path] = None, strict: bool = False, ) -> t.Any: """Coerces the given expression to the specified type on a best-effort basis.""" diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index c05dd44498..910e6eccc5 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -1648,6 +1648,8 @@ def is_seed(self) -> bool: def seed_path(self) -> Path: seed_path = Path(self.kind.path) if not seed_path.is_absolute(): + if self._path is None: + raise SQLMeshError(f"Seed model '{self.name}' has no path") return self._path.parent / seed_path return seed_path @@ -2022,7 +2024,7 @@ def load_sql_based_model( expressions: t.List[exp.Expression], *, defaults: t.Optional[t.Dict[str, t.Any]] = None, - path: Path = Path(), + path: t.Optional[Path] = None, module_path: Path = Path(), time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT, macros: t.Optional[MacroRegistry] = None, @@ -2173,6 +2175,8 @@ def load_sql_based_model( # The name of the model will be inferred from its path relative to `models/`, if it's not explicitly specified name = meta_fields.pop("name", "") if not name and infer_names: + if path is None: + raise ValueError(f"Model {name} must have a name") name = get_model_name(path) if not name: @@ -2251,7 +2255,7 @@ def create_seed_model( name: TableName, seed_kind: SeedKind, *, - path: Path = Path(), + path: t.Optional[Path] = None, module_path: Path = Path(), **kwargs: t.Any, ) -> Model: @@ -2270,7 +2274,12 @@ def create_seed_model( seed_path = module_path.joinpath(*subdirs) seed_kind.path = str(seed_path) elif not seed_path.is_absolute(): - seed_path = path / seed_path if path.is_dir() else path.parent / seed_path + if path is None: + seed_path = seed_path + elif path.is_dir(): + seed_path = path / seed_path + else: + seed_path = path.parent / seed_path seed = create_seed(seed_path) @@ -2405,7 +2414,7 @@ def _create_model( name: TableName, *, defaults: t.Optional[t.Dict[str, t.Any]] = None, - path: Path = Path(), + path: t.Optional[Path] = None, time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT, jinja_macros: t.Optional[JinjaMacroRegistry] = None, jinja_macro_references: t.Optional[t.Set[MacroReference]] = None, @@ -2593,7 +2602,7 @@ def _create_model( def _split_sql_model_statements( expressions: t.List[exp.Expression], - path: Path, + path: t.Optional[Path], dialect: t.Optional[str] = None, ) -> t.Tuple[ t.Optional[exp.Expression], @@ -2714,7 +2723,7 @@ def _refs_to_sql(values: t.Any) -> exp.Expression: def render_meta_fields( fields: t.Dict[str, t.Any], module_path: Path, - path: Path, + path: t.Optional[Path], jinja_macros: t.Optional[JinjaMacroRegistry], macros: t.Optional[MacroRegistry], dialect: DialectType, @@ -2801,7 +2810,7 @@ def render_field_value(value: t.Any) -> t.Any: def render_model_defaults( defaults: t.Dict[str, t.Any], module_path: Path, - path: Path, + path: t.Optional[Path], jinja_macros: t.Optional[JinjaMacroRegistry], macros: t.Optional[MacroRegistry], dialect: DialectType, @@ -2851,7 +2860,7 @@ def parse_defaults_properties( def render_expression( expression: exp.Expression, module_path: Path, - path: Path, + path: t.Optional[Path], jinja_macros: t.Optional[JinjaMacroRegistry] = None, macros: t.Optional[MacroRegistry] = None, dialect: DialectType = None, diff --git a/sqlmesh/core/node.py b/sqlmesh/core/node.py index 874e74b3e9..4f0a66dc2e 100644 --- a/sqlmesh/core/node.py +++ b/sqlmesh/core/node.py @@ -199,7 +199,7 @@ class _Node(PydanticModel): interval_unit_: t.Optional[IntervalUnit] = Field(alias="interval_unit", default=None) tags: t.List[str] = [] stamp: t.Optional[str] = None - _path: Path = Path() + _path: t.Optional[Path] = None _data_hash: t.Optional[str] = None _metadata_hash: t.Optional[str] = None diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index 6622094da3..f2e9e24056 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -43,7 +43,7 @@ def __init__( expression: exp.Expression, dialect: DialectType, macro_definitions: t.List[d.MacroDef], - path: Path = Path(), + path: t.Optional[Path] = None, jinja_macro_registry: t.Optional[JinjaMacroRegistry] = None, python_env: t.Optional[t.Dict[str, Executable]] = None, only_execution_time: bool = False, diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index b6f94108a1..1a284aadfd 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -2198,7 +2198,7 @@ def check_ready_intervals( context: ExecutionContext, python_env: t.Dict[str, Executable], dialect: DialectType = None, - path: Path = Path(), + path: t.Optional[Path] = None, kwargs: t.Optional[t.Dict] = None, ) -> Intervals: checked_intervals: Intervals = [] diff --git a/sqlmesh/dbt/converter/convert.py b/sqlmesh/dbt/converter/convert.py index f097a83884..7eab536946 100644 --- a/sqlmesh/dbt/converter/convert.py +++ b/sqlmesh/dbt/converter/convert.py @@ -207,6 +207,8 @@ def _convert_models( if model.kind.is_seed: # this will produce the original seed file, eg "items.csv" + if model._path is None: + raise ValueError(f"Unhandled model path for model {model_name}") seed_filename = model._path.relative_to(input_paths.seeds) # seed definition - rename "items.csv" -> "items.sql" @@ -219,6 +221,8 @@ def _convert_models( assert isinstance(model.kind, SeedKind) model.kind.path = str(Path("../seeds", seed_filename)) else: + if model._path is None: + raise ValueError(f"Unhandled model path for model {model_name}") if input_paths.models in model._path.parents: model_filename = model._path.relative_to(input_paths.models) elif input_paths.snapshots in model._path.parents: @@ -290,6 +294,8 @@ def _convert_standalone_audits( audit_definition_string = ";\n".join(stringified) + if audit._path is None: + continue audit_filename = audit._path.relative_to(input_paths.tests) audit_output_path = output_paths.audits / audit_filename audit_output_path.write_text(audit_definition_string) diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 96db4dc63d..9f1215d9ca 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -164,6 +164,9 @@ def get_model_definitions_for_a_path( else: return [] + if file_path is None: + return [] + # Find all possible references references: t.List[Reference] = [] @@ -246,6 +249,8 @@ def get_model_definitions_for_a_path( if referenced_model is None: continue referenced_model_path = referenced_model._path + if referenced_model_path is None: + continue # Check whether the path exists if not referenced_model_path.is_file(): continue @@ -372,6 +377,9 @@ def get_macro_definitions_for_a_path( else: return [] + if file_path is None: + return [] + references = [] _, config_path = lsp_context.context.config_for_path( file_path, diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 58f7135654..ef2db145ee 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -287,8 +287,9 @@ def model(self, context: Context, line: str, sql: t.Optional[str] = None) -> Non if loaded.name == args.model: model = loaded else: - with open(model._path, "r", encoding="utf-8") as file: - expressions = parse(file.read(), default_dialect=config.dialect) + if model._path: + with open(model._path, "r", encoding="utf-8") as file: + expressions = parse(file.read(), default_dialect=config.dialect) formatted = format_model_expressions( expressions, @@ -307,8 +308,9 @@ def model(self, context: Context, line: str, sql: t.Optional[str] = None) -> Non replace=True, ) - with open(model._path, "w", encoding="utf-8") as file: - file.write(formatted) + if model._path: + with open(model._path, "w", encoding="utf-8") as file: + file.write(formatted) if sql: context.console.log_success(f"Model `{args.model}` updated") diff --git a/tests/core/test_model.py b/tests/core/test_model.py index e025930f30..909a79e143 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -3290,7 +3290,7 @@ def runtime_macro(evaluator, **kwargs) -> None: model = load_sql_based_model(expressions) with pytest.raises( ConfigError, - match=r"Dependencies must be provided explicitly for models that can be rendered only at runtime at.*", + match=r"Dependencies must be provided explicitly for models that can be rendered only at runtime", ): model.validate_definition() @@ -8324,7 +8324,7 @@ def test_physical_version(): with pytest.raises( ConfigError, - match=r"Pinning a physical version is only supported for forward only models at.*", + match=r"Pinning a physical version is only supported for forward only models( at.*)?", ): load_sql_based_model( d.parse( diff --git a/tests/integrations/github/cicd/conftest.py b/tests/integrations/github/cicd/conftest.py index 30f65aecdc..25ba3b2d60 100644 --- a/tests/integrations/github/cicd/conftest.py +++ b/tests/integrations/github/cicd/conftest.py @@ -49,7 +49,7 @@ def _make_function(username: str, state: str, **kwargs) -> PullRequestReview: github_client.requester, {}, { - # Name is whatever they provide in their GitHub profile or login as fallback. Always use login. + # Name is whatever they provide in their GitHub profile or login as a fallback. Always use login. "user": AttributeDict(name="Unrelated", login=username), "state": state, **kwargs, diff --git a/vscode/openapi.json b/vscode/openapi.json index bf1cef0809..32a7445e32 100644 --- a/vscode/openapi.json +++ b/vscode/openapi.json @@ -1382,8 +1382,14 @@ "properties": { "name": { "type": "string", "title": "Name" }, "fqn": { "type": "string", "title": "Fqn" }, - "path": { "type": "string", "title": "Path" }, - "full_path": { "type": "string", "title": "Full Path" }, + "path": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Path" + }, + "full_path": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Full Path" + }, "dialect": { "type": "string", "title": "Dialect" }, "type": { "$ref": "#/components/schemas/ModelType" }, "columns": { @@ -1417,16 +1423,7 @@ }, "additionalProperties": false, "type": "object", - "required": [ - "name", - "fqn", - "path", - "full_path", - "dialect", - "type", - "columns", - "hash" - ], + "required": ["name", "fqn", "dialect", "type", "columns", "hash"], "title": "Model" }, "ModelDetails": { @@ -2143,7 +2140,7 @@ "TestCase": { "properties": { "name": { "type": "string", "title": "Name" }, - "path": { "type": "string", "format": "path", "title": "Path" } + "path": { "type": "string", "title": "Path" } }, "additionalProperties": false, "type": "object", @@ -2153,7 +2150,7 @@ "TestErrorOrFailure": { "properties": { "name": { "type": "string", "title": "Name" }, - "path": { "type": "string", "format": "path", "title": "Path" }, + "path": { "type": "string", "title": "Path" }, "tb": { "type": "string", "title": "Tb" } }, "additionalProperties": false, @@ -2193,7 +2190,7 @@ "TestSkipped": { "properties": { "name": { "type": "string", "title": "Name" }, - "path": { "type": "string", "format": "path", "title": "Path" }, + "path": { "type": "string", "title": "Path" }, "reason": { "type": "string", "title": "Reason" } }, "additionalProperties": false, diff --git a/vscode/react/src/api/client.ts b/vscode/react/src/api/client.ts index 807f51e1d9..028b2d1912 100644 --- a/vscode/react/src/api/client.ts +++ b/vscode/react/src/api/client.ts @@ -289,6 +289,10 @@ export interface Meta { has_running_task?: boolean } +export type ModelPath = string | null + +export type ModelFullPath = string | null + export type ModelDescription = string | null export type ModelDetailsProperty = ModelDetails | null @@ -302,8 +306,8 @@ export type ModelDefaultCatalog = string | null export interface Model { name: string fqn: string - path: string - full_path: string + path?: ModelPath + full_path?: ModelFullPath dialect: string type: ModelType columns: Column[] diff --git a/vscode/react/src/pages/lineage.tsx b/vscode/react/src/pages/lineage.tsx index ddfac87fc4..18925f28da 100644 --- a/vscode/react/src/pages/lineage.tsx +++ b/vscode/react/src/pages/lineage.tsx @@ -100,9 +100,12 @@ function Lineage() { // @ts-ignore const fileUri: string = activeFile.fileUri const filePath = URI.file(fileUri).path - const model = models.find( - (m: Model) => URI.file(m.full_path).path === filePath, - ) + const model = models.find((m: Model) => { + if (!m.full_path) { + return false + } + return URI.file(m.full_path).path === filePath + }) if (model) { return model.name } @@ -195,6 +198,9 @@ export function LineageComponentFromWeb({ if (!model) { throw new Error('Model not found') } + if (!model.full_path) { + return + } vscode('openFile', { uri: URI.file(model.full_path).toString() }) } diff --git a/web/server/api/endpoints/models.py b/web/server/api/endpoints/models.py index 5d81306aea..21a7b93eb0 100644 --- a/web/server/api/endpoints/models.py +++ b/web/server/api/endpoints/models.py @@ -121,11 +121,12 @@ def serialize_model(context: Context, model: Model, render_query: bool = False) ) sql = query.sql(pretty=True, dialect=model.dialect) + path = model._path return models.Model( name=model.name, fqn=model.fqn, - path=str(model._path.absolute().relative_to(context.path).as_posix()), - full_path=str(model._path.absolute().as_posix()), + path=str(path.absolute().relative_to(context.path).as_posix()) if path else None, + full_path=str(path.absolute().as_posix()) if path else None, dialect=dialect, columns=columns, details=details, diff --git a/web/server/models.py b/web/server/models.py index cc12d036fc..d193fa5f07 100644 --- a/web/server/models.py +++ b/web/server/models.py @@ -171,8 +171,8 @@ class Column(PydanticModel): class Model(PydanticModel): name: str fqn: str - path: str - full_path: str + path: t.Optional[str] = None + full_path: t.Optional[str] = None """ As opposed to path, which is relative to the project root, full_path is the absolute path to the model file. """ diff --git a/web/server/settings.py b/web/server/settings.py index bf64504565..a96d369de8 100644 --- a/web/server/settings.py +++ b/web/server/settings.py @@ -80,7 +80,7 @@ def _get_loaded_context(path: str | Path, config: str, gateway: str) -> Context: @lru_cache() def _get_path_to_model_mapping(context: Context) -> dict[Path, Model]: - return {model._path: model for model in context._models.values()} + return {model._path: model for model in context._models.values() if model._path} def get_path_to_model_mapping( From 11dab16713b7b6e6a45f91eae5bd13422da22e3a Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 7 Jul 2025 11:10:44 -0700 Subject: [PATCH 0545/1056] Chore: Remove dead spark integration code (#4924) --- sqlmesh/engines/commands.py | 184 ----------------------------------- sqlmesh/engines/spark/app.py | 114 ---------------------- 2 files changed, 298 deletions(-) delete mode 100644 sqlmesh/engines/commands.py delete mode 100644 sqlmesh/engines/spark/app.py diff --git a/sqlmesh/engines/commands.py b/sqlmesh/engines/commands.py deleted file mode 100644 index c16e759cbf..0000000000 --- a/sqlmesh/engines/commands.py +++ /dev/null @@ -1,184 +0,0 @@ -import typing as t -from enum import Enum - -from sqlglot import exp -from sqlmesh.core.environment import Environment, EnvironmentNamingInfo -from sqlmesh.core.snapshot import ( - DeployabilityIndex, - Snapshot, - SnapshotEvaluator, - SnapshotId, - SnapshotTableCleanupTask, - SnapshotTableInfo, -) -from sqlmesh.core.state_sync import cleanup_expired_views -from sqlmesh.utils.date import TimeLike -from sqlmesh.utils.errors import AuditError -from sqlmesh.utils.pydantic import PydanticModel - -COMMAND_PAYLOAD_FILE_NAME = "payload.json" - - -class CommandType(str, Enum): - EVALUATE = "evaluate" - PROMOTE = "promote" - DEMOTE = "demote" - CLEANUP = "cleanup" - CREATE_TABLES = "create_tables" - MIGRATE_TABLES = "migrate_tables" - - # This makes it easy to integrate with argparse - def __str__(self) -> str: - return self.value - - -class EvaluateCommandPayload(PydanticModel): - snapshot: Snapshot - parent_snapshots: t.Dict[str, Snapshot] - start: TimeLike - end: TimeLike - execution_time: TimeLike - deployability_index: DeployabilityIndex - batch_index: int - - -class PromoteCommandPayload(PydanticModel): - snapshots: t.List[Snapshot] - environment_naming_info: EnvironmentNamingInfo - deployability_index: DeployabilityIndex - - -class DemoteCommandPayload(PydanticModel): - snapshots: t.List[SnapshotTableInfo] - environment_naming_info: EnvironmentNamingInfo - - -class CleanupCommandPayload(PydanticModel): - environments: t.List[Environment] - tasks: t.List[SnapshotTableCleanupTask] - - -class CreateTablesCommandPayload(PydanticModel): - target_snapshot_ids: t.List[SnapshotId] - snapshots: t.List[Snapshot] - deployability_index: DeployabilityIndex - allow_destructive_snapshots: t.Set[str] - - -class MigrateTablesCommandPayload(PydanticModel): - target_snapshot_ids: t.List[SnapshotId] - snapshots: t.List[Snapshot] - allow_destructive_snapshots: t.Set[str] - - -def evaluate( - evaluator: SnapshotEvaluator, command_payload: t.Union[str, EvaluateCommandPayload] -) -> None: - if isinstance(command_payload, str): - command_payload = EvaluateCommandPayload.parse_raw(command_payload) - - parent_snapshots = command_payload.parent_snapshots - parent_snapshots[command_payload.snapshot.name] = command_payload.snapshot - - wap_id = evaluator.evaluate( - command_payload.snapshot, - start=command_payload.start, - end=command_payload.end, - execution_time=command_payload.execution_time, - snapshots=parent_snapshots, - deployability_index=command_payload.deployability_index, - batch_index=command_payload.batch_index, - ) - audit_results = evaluator.audit( - snapshot=command_payload.snapshot, - start=command_payload.start, - end=command_payload.end, - execution_time=command_payload.execution_time, - snapshots=parent_snapshots, - deployability_index=command_payload.deployability_index, - wap_id=wap_id, - ) - - failed_audit_result = next((r for r in audit_results if r.count and r.blocking), None) - if failed_audit_result: - raise AuditError( - audit_name=failed_audit_result.audit.name, - audit_args=failed_audit_result.audit_args, - model=command_payload.snapshot.model_or_none, - count=t.cast(int, failed_audit_result.count), - query=t.cast(exp.Query, failed_audit_result.query), - adapter_dialect=evaluator.adapter.dialect, - ) - - -def promote( - evaluator: SnapshotEvaluator, command_payload: t.Union[str, PromoteCommandPayload] -) -> None: - if isinstance(command_payload, str): - command_payload = PromoteCommandPayload.parse_raw(command_payload) - evaluator.promote( - command_payload.snapshots, - command_payload.environment_naming_info, - deployability_index=command_payload.deployability_index, - ) - - -def demote( - evaluator: SnapshotEvaluator, command_payload: t.Union[str, DemoteCommandPayload] -) -> None: - if isinstance(command_payload, str): - command_payload = DemoteCommandPayload.parse_raw(command_payload) - evaluator.demote( - command_payload.snapshots, - command_payload.environment_naming_info, - ) - - -def cleanup( - evaluator: SnapshotEvaluator, command_payload: t.Union[str, CleanupCommandPayload] -) -> None: - if isinstance(command_payload, str): - command_payload = CleanupCommandPayload.parse_raw(command_payload) - - cleanup_expired_views(evaluator.adapter, evaluator.adapters, command_payload.environments) - evaluator.cleanup(command_payload.tasks) - - -def create_tables( - evaluator: SnapshotEvaluator, - command_payload: t.Union[str, CreateTablesCommandPayload], -) -> None: - if isinstance(command_payload, str): - command_payload = CreateTablesCommandPayload.parse_raw(command_payload) - - snapshots_by_id = {s.snapshot_id: s for s in command_payload.snapshots} - target_snapshots = [snapshots_by_id[sid] for sid in command_payload.target_snapshot_ids] - evaluator.create( - target_snapshots, - snapshots_by_id, - deployability_index=command_payload.deployability_index, - allow_destructive_snapshots=command_payload.allow_destructive_snapshots, - ) - - -def migrate_tables( - evaluator: SnapshotEvaluator, - command_payload: t.Union[str, MigrateTablesCommandPayload], -) -> None: - if isinstance(command_payload, str): - command_payload = MigrateTablesCommandPayload.parse_raw(command_payload) - snapshots_by_id = {s.snapshot_id: s for s in command_payload.snapshots} - target_snapshots = [snapshots_by_id[sid] for sid in command_payload.target_snapshot_ids] - evaluator.migrate( - target_snapshots, snapshots_by_id, command_payload.allow_destructive_snapshots - ) - - -COMMAND_HANDLERS: t.Dict[CommandType, t.Callable[[SnapshotEvaluator, str], None]] = { - CommandType.EVALUATE: evaluate, - CommandType.PROMOTE: promote, - CommandType.DEMOTE: demote, - CommandType.CLEANUP: cleanup, - CommandType.CREATE_TABLES: create_tables, - CommandType.MIGRATE_TABLES: migrate_tables, -} diff --git a/sqlmesh/engines/spark/app.py b/sqlmesh/engines/spark/app.py deleted file mode 100644 index a8709361fa..0000000000 --- a/sqlmesh/engines/spark/app.py +++ /dev/null @@ -1,114 +0,0 @@ -import argparse -import logging -import os -import tempfile - -from pyspark import SparkFiles -from pyspark.sql import SparkSession - -from sqlmesh.core.engine_adapter import create_engine_adapter -from sqlmesh.core.snapshot import SnapshotEvaluator -from sqlmesh.engines import commands -from sqlmesh.engines.spark.db_api import spark_session as spark_session_db -from sqlmesh.engines.spark.db_api.errors import NotSupportedError -from sqlmesh.utils.errors import SQLMeshError - -logger = logging.getLogger(__name__) - - -def get_or_create_spark_session(dialect: str) -> SparkSession: - if dialect == "databricks": - spark = SparkSession.getActiveSession() - if not spark: - raise SQLMeshError("Could not find an active SparkSession.") - return spark - return ( - SparkSession.builder.config("spark.scheduler.mode", "FAIR") - .enableHiveSupport() - .getOrCreate() - ) - - -def main( - dialect: str, - default_catalog: str, - command_type: commands.CommandType, - ddl_concurrent_tasks: int, - payload_path: str, -) -> None: - if dialect not in ("databricks", "spark"): - raise NotSupportedError( - f"Dialect '{dialect}' not supported. Must be either 'databricks' or 'spark'" - ) - logging.basicConfig( - format="%(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)", - level=logging.INFO, - ) - command_handler = commands.COMMAND_HANDLERS.get(command_type) - if not command_handler: - raise NotSupportedError(f"Command '{command_type.value}' not supported") - - spark = get_or_create_spark_session(dialect) - - evaluator = SnapshotEvaluator( - create_engine_adapter( - lambda: spark_session_db.connection(spark), - dialect, - default_catalog=default_catalog, - multithreaded=ddl_concurrent_tasks > 1, - execute_log_level=logging.INFO, - ), - ddl_concurrent_tasks=ddl_concurrent_tasks, - ) - if dialect == "spark": - with open(SparkFiles.get(payload_path), "r", encoding="utf-8") as payload_fd: - command_payload = payload_fd.read() - else: - from pyspark.dbutils import DBUtils # type: ignore - - dbutils = DBUtils(spark) - with tempfile.TemporaryDirectory() as tmp: - local_payload_path = os.path.join(tmp, commands.COMMAND_PAYLOAD_FILE_NAME) - dbutils.fs.cp(payload_path, f"file://{local_payload_path}") - with open(local_payload_path, "r", encoding="utf-8") as payload_fd: - command_payload = payload_fd.read() - logger.info("Command payload:\n %s", command_payload) - command_handler(evaluator, command_payload) - - evaluator.close() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="SQLMesh Spark Submit App") - parser.add_argument( - "--dialect", - help="The dialect to use when creating the engine adapter.", - ) - parser.add_argument( - "--default_catalog", - help="The default catalog to use when creating the engine adapter.", - ) - parser.add_argument( - "--command_type", - type=commands.CommandType, - choices=list(commands.CommandType), - help="The type of command that is being run", - ) - parser.add_argument( - "--ddl_concurrent_tasks", - type=int, - default=1, - help="The number of ddl concurrent tasks to use. Default to 1.", - ) - parser.add_argument( - "--payload_path", - help="Path to the payload object. Can be a local or remote path.", - ) - args = parser.parse_args() - main( - args.dialect, - args.default_catalog, - args.command_type, - args.ddl_concurrent_tasks, - args.payload_path, - ) From f018216cb0daec0360bef987a737695b5406e547 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 7 Jul 2025 12:46:59 -0700 Subject: [PATCH 0546/1056] Chore: Don't support string arguments for the 'config_for_node' API (#4926) --- sqlmesh/cli/main.py | 2 ++ sqlmesh/core/context.py | 7 ++----- sqlmesh/magics.py | 2 ++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index ac1b69ee62..e4b06552ad 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -279,6 +279,8 @@ def render( **format_kwargs: t.Any, ) -> None: """Render a model's query, optionally expanding referenced models.""" + model = ctx.obj.get_model(model, raise_if_missing=True) + rendered = ctx.obj.render( model, start=start, diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 21f629f07d..0c398af412 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -946,11 +946,8 @@ def config_for_path(self, path: Path) -> t.Tuple[Config, Path]: pass return self.config, self.path - def config_for_node(self, node: str | Model | Audit) -> Config: - if isinstance(node, str): - path = self.get_snapshot(node, raise_if_missing=True).node._path - else: - path = node._path + def config_for_node(self, node: Model | Audit) -> Config: + path = node._path if path is None: return self.config return self.config_for_path(path)[0] # type: ignore diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index ef2db145ee..454b6cd4ce 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -641,6 +641,8 @@ def render(self, context: Context, line: str) -> None: model = render_opts.pop("model") dialect = render_opts.pop("dialect", None) + model = context.get_model(model, raise_if_missing=True) + query = context.render( model, start=render_opts.pop("start", None), From e3daa8f2e741d0476a61af370f69ece961dd3001 Mon Sep 17 00:00:00 2001 From: Bjarke Enkelund <47357343+MisterWheatley@users.noreply.github.com> Date: Mon, 7 Jul 2025 23:05:38 +0200 Subject: [PATCH 0547/1056] Fix!: Ensure correct datatypes are fetched for RisingWave dialect (#4903) Co-authored-by: Jo <46752250+georgesittas@users.noreply.github.com> --- sqlmesh/core/engine_adapter/risingwave.py | 33 ++++++++ .../test_integration_risingwave.py | 82 +++++++++++++++++++ tests/core/engine_adapter/test_risingwave.py | 39 ++++++++- 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 tests/core/engine_adapter/integration/test_integration_risingwave.py diff --git a/sqlmesh/core/engine_adapter/risingwave.py b/sqlmesh/core/engine_adapter/risingwave.py index f32ce2f457..fdcee90f0f 100644 --- a/sqlmesh/core/engine_adapter/risingwave.py +++ b/sqlmesh/core/engine_adapter/risingwave.py @@ -14,6 +14,7 @@ CommentCreationTable, ) +from sqlmesh.utils.errors import SQLMeshError if t.TYPE_CHECKING: from sqlmesh.core._typing import TableName @@ -32,5 +33,37 @@ class RisingwaveEngineAdapter(PostgresEngineAdapter): SUPPORTS_TRANSACTIONS = False MAX_IDENTIFIER_LENGTH = None + def columns( + self, table_name: TableName, include_pseudo_columns: bool = False + ) -> t.Dict[str, exp.DataType]: + """Fetches column names and types for the target_table""" + table = exp.to_table(table_name) + + sql = ( + exp.select("rw_columns.name AS column_name", "rw_columns.data_type AS data_type") + .from_("rw_catalog.rw_columns") + .join("rw_catalog.rw_relations", on="rw_relations.id=rw_columns.relation_id") + .join("rw_catalog.rw_schemas", on="rw_schemas.id=rw_relations.schema_id") + .where( + exp.and_( + exp.column("name", table="rw_relations").eq(table.alias_or_name), + exp.column("name", table="rw_columns").neq("_row_id"), + exp.column("name", table="rw_columns").neq("_rw_timestamp"), + ) + ) + ) + + if table.db: + sql = sql.where(exp.column("name", table="rw_schemas").eq(table.db)) + + self.execute(sql) + resp = self.cursor.fetchall() + if not resp: + raise SQLMeshError(f"Could not get columns for table {table_name}. Table not found.") + return { + column_name: exp.DataType.build(data_type, dialect=self.dialect, udt=True) + for column_name, data_type in resp + } + def _truncate_table(self, table_name: TableName) -> None: return self.execute(exp.Delete(this=exp.to_table(table_name))) diff --git a/tests/core/engine_adapter/integration/test_integration_risingwave.py b/tests/core/engine_adapter/integration/test_integration_risingwave.py new file mode 100644 index 0000000000..76b3d20a7c --- /dev/null +++ b/tests/core/engine_adapter/integration/test_integration_risingwave.py @@ -0,0 +1,82 @@ +import typing as t +import pytest +from sqlglot import exp +from pytest import FixtureRequest +from sqlmesh.core.engine_adapter import RisingwaveEngineAdapter +from tests.core.engine_adapter.integration import ( + TestContext, + generate_pytest_params, + ENGINES_BY_NAME, + IntegrationTestEngine, +) + + +@pytest.fixture(params=list(generate_pytest_params(ENGINES_BY_NAME["risingwave"]))) +def ctx( + request: FixtureRequest, + create_test_context: t.Callable[[IntegrationTestEngine, str, str], t.Iterable], +) -> t.Iterable[TestContext]: + yield from create_test_context(*request.param) + + +@pytest.fixture +def engine_adapter(ctx: TestContext) -> RisingwaveEngineAdapter: + assert isinstance(ctx.engine_adapter, RisingwaveEngineAdapter) + return ctx.engine_adapter + + +@pytest.fixture +def risingwave_columns_with_datatypes(ctx: TestContext) -> t.Dict[str, exp.DataType]: + base_types = { + "smallint_col": exp.DataType.build(exp.DataType.Type.SMALLINT, nested=False), + "int_col": exp.DataType.build(exp.DataType.Type.INT, nested=False), + "bigint_col": exp.DataType.build(exp.DataType.Type.BIGINT, nested=False), + "ts_col": exp.DataType.build(exp.DataType.Type.TIMESTAMP, nested=False), + "tstz_col": exp.DataType.build(exp.DataType.Type.TIMESTAMPTZ, nested=False), + "vchar_col": exp.DataType.build(exp.DataType.Type.VARCHAR, nested=False), + } + # generate all arrays of base types + arr_types = { + f"{type_name}_arr_col": exp.DataType.build( + exp.DataType.Type.ARRAY, + expressions=[base_type], + nested=True, + ) + for type_name, base_type in base_types.items() + } + # generate struct with all base types as nested columns + struct_types = { + "struct_col": exp.DataType.build( + exp.DataType.Type.STRUCT, + expressions=[ + exp.ColumnDef( + this=exp.Identifier(this=f"nested_{type_name}_col", quoted=False), + kind=base_type, + ) + for type_name, base_type in base_types.items() + ], + nested=True, + ) + } + return {**base_types, **arr_types, **struct_types} + + +def test_engine_adapter(ctx: TestContext): + assert isinstance(ctx.engine_adapter, RisingwaveEngineAdapter) + assert ctx.engine_adapter.fetchone("select 1") == (1,) + + +def test_engine_adapter_columns( + ctx: TestContext, risingwave_columns_with_datatypes: t.Dict[str, exp.DataType] +): + table = ctx.table("TEST_COLUMNS") + query = exp.select( + *[ + exp.cast(exp.null(), dtype).as_(name) + for name, dtype in risingwave_columns_with_datatypes.items() + ] + ) + ctx.engine_adapter.ctas(table, query) + + column_result = ctx.engine_adapter.columns(table) + assert column_result == risingwave_columns_with_datatypes diff --git a/tests/core/engine_adapter/test_risingwave.py b/tests/core/engine_adapter/test_risingwave.py index 6718690283..ed3cd77a3f 100644 --- a/tests/core/engine_adapter/test_risingwave.py +++ b/tests/core/engine_adapter/test_risingwave.py @@ -3,7 +3,7 @@ from unittest.mock import call import pytest -from sqlglot import parse_one +from sqlglot import parse_one, exp from sqlmesh.core.engine_adapter.risingwave import RisingwaveEngineAdapter pytestmark = [pytest.mark.engine, pytest.mark.risingwave] @@ -15,6 +15,43 @@ def adapter(make_mocked_engine_adapter): return adapter +def test_columns(adapter: t.Callable): + adapter.cursor.fetchall.return_value = [ + ("smallint_col", "smallint"), + ("int_col", "integer"), + ("bigint_col", "bigint"), + ("ts_col", "timestamp without time zone"), + ("tstz_col", "timestamp with time zone"), + ("int_array_col", "integer[]"), + ("vchar_col", "character varying"), + ("struct_col", "struct"), + ] + resp = adapter.columns("db.table") + assert resp == { + "smallint_col": exp.DataType.build(exp.DataType.Type.SMALLINT, nested=False), + "int_col": exp.DataType.build(exp.DataType.Type.INT, nested=False), + "bigint_col": exp.DataType.build(exp.DataType.Type.BIGINT, nested=False), + "ts_col": exp.DataType.build(exp.DataType.Type.TIMESTAMP, nested=False), + "tstz_col": exp.DataType.build(exp.DataType.Type.TIMESTAMPTZ, nested=False), + "int_array_col": exp.DataType.build( + exp.DataType.Type.ARRAY, + expressions=[exp.DataType.build(exp.DataType.Type.INT, nested=False)], + nested=True, + ), + "vchar_col": exp.DataType.build(exp.DataType.Type.VARCHAR), + "struct_col": exp.DataType.build( + exp.DataType.Type.STRUCT, + expressions=[ + exp.ColumnDef( + this=exp.Identifier(this="nested_col", quoted=False), + kind=exp.DataType.build(exp.DataType.Type.INT, nested=False), + ) + ], + nested=True, + ), + } + + def test_create_view(adapter: t.Callable): adapter.create_view("db.view", parse_one("SELECT 1"), replace=True) adapter.create_view("db.view", parse_one("SELECT 1"), replace=False) From 7e983f8057a8566057f4fa0ae3046c92ad2411d4 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 8 Jul 2025 09:24:53 +1200 Subject: [PATCH 0548/1056] Fix(cicd_bot): Don't truncate backfill model list (#4915) --- sqlmesh/core/console.py | 4 +- sqlmesh/integrations/github/cicd/command.py | 4 +- .../integrations/github/cicd/controller.py | 9 +++ .../github/cicd/test_github_controller.py | 38 ++++++++++++ .../github/cicd/test_integration.py | 62 +++++++++---------- 5 files changed, 84 insertions(+), 33 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index ae48722288..78020b693f 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -3107,7 +3107,9 @@ class MarkdownConsole(CaptureTerminalConsole): AUDIT_PADDING = 7 def __init__(self, **kwargs: t.Any) -> None: - super().__init__(**{**kwargs, "console": RichConsole(no_color=True)}) + super().__init__( + **{**kwargs, "console": RichConsole(no_color=True, width=kwargs.pop("width", None))} + ) def show_environment_difference_summary( self, diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index f5d614405a..04f5757b50 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -28,7 +28,9 @@ @click.pass_context def github(ctx: click.Context, token: str) -> None: """Github Action CI/CD Bot. See https://sqlmesh.readthedocs.io/en/stable/integrations/github/ for details""" - set_console(MarkdownConsole()) + # set a larger width because if none is specified, it auto-detects 80 characters when running in GitHub Actions + # which can result in surprise newlines when outputting dates to backfill + set_console(MarkdownConsole(width=1000)) ctx.obj["github"] = GithubController( paths=ctx.obj["paths"], token=token, diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 5ae2a763e7..bc35e54520 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -485,6 +485,12 @@ def _append_output(cls, key: str, value: str) -> None: print(f"{key}={value}", file=fh) def get_plan_summary(self, plan: Plan) -> str: + # use Verbosity.VERY_VERBOSE to prevent the list of models from being truncated + # this is particularly important for the "Models needing backfill" list because + # there is no easy way to tell this otherwise + orig_verbosity = self._console.verbosity + self._console.verbosity = Verbosity.VERY_VERBOSE + try: # Clear out any output that might exist from prior steps self._console.clear_captured_outputs() @@ -517,7 +523,10 @@ def get_plan_summary(self, plan: Plan) -> str: return f"{difference_summary}\n{missing_dates}{plan_flags_section}" except PlanError as e: + logger.exception("Plan failed to generate") return f"Plan failed to generate. Check for pending or unresolved changes. Error: {e}" + finally: + self._console.verbosity = orig_verbosity def run_tests(self) -> t.Tuple[ModelTextTestResult, str]: """ diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index d7d4f5343c..62cd5a26f4 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -1,4 +1,5 @@ # type: ignore +import typing as t import os import pathlib from unittest import mock @@ -17,6 +18,7 @@ BotCommand, MergeStateStatus, ) +from sqlmesh.integrations.github.cicd.controller import GithubController from sqlmesh.integrations.github.cicd.command import _update_pr_environment from sqlmesh.utils.date import to_datetime, now from tests.integrations.github.cicd.conftest import MockIssueComment @@ -591,3 +593,39 @@ def test_uncategorized( assert "The following models could not be categorized automatically" in summary assert '- "b"' in summary assert "Run `sqlmesh plan hello_world_2` locally to apply these changes" in summary + + +def test_get_plan_summary_doesnt_truncate_backfill_list( + github_client, make_controller: t.Callable[..., GithubController] +): + controller = make_controller( + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + mock_out_context=False, + ) + + summary = controller.get_plan_summary(controller.prod_plan) + + assert "more ...." not in summary + + assert ( + """**Models needing backfill:** +* `memory.raw.demographics`: [full refresh] +* `memory.sushi.active_customers`: [full refresh] +* `memory.sushi.count_customers_active`: [full refresh] +* `memory.sushi.count_customers_inactive`: [full refresh] +* `memory.sushi.customer_revenue_by_day`: [2025-06-30 - 2025-07-06] +* `memory.sushi.customer_revenue_lifetime`: [2025-06-30 - 2025-07-06] +* `memory.sushi.customers`: [full refresh] +* `memory.sushi.items`: [2025-06-30 - 2025-07-06] +* `memory.sushi.latest_order`: [full refresh] +* `memory.sushi.marketing`: [2025-06-30 - 2025-07-06] +* `memory.sushi.order_items`: [2025-06-30 - 2025-07-06] +* `memory.sushi.orders`: [2025-06-30 - 2025-07-06] +* `memory.sushi.raw_marketing`: [full refresh] +* `memory.sushi.top_waiters`: [recreate view] +* `memory.sushi.waiter_as_customer_by_day`: [2025-06-30 - 2025-07-06] +* `memory.sushi.waiter_names`: [full refresh] +* `memory.sushi.waiter_revenue_by_day`: [2025-06-30 - 2025-07-06]""" + in summary + ) diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index bff9d4d117..ff9856993a 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -298,7 +298,7 @@ def test_merge_pr_has_non_breaking_change( assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success expected_prod_plan_directly_modified_summary = """**Directly Modified:** -* `sushi.waiter_revenue_by_day` (Non-breaking) +* `memory.sushi.waiter_revenue_by_day` (Non-breaking) ```diff --- @@ -318,10 +318,10 @@ def test_merge_pr_has_non_breaking_change( ON o.id = oi.order_id AND o.event_date = oi.event_date ``` Indirectly Modified Children: - - `sushi.top_waiters` (Indirect Non-breaking) + - `memory.sushi.top_waiters` (Indirect Non-breaking) """ expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** -- `sushi.top_waiters` (Indirect Non-breaking) +- `memory.sushi.top_waiters` (Indirect Non-breaking) """ assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" @@ -509,7 +509,7 @@ def test_merge_pr_has_non_breaking_change_diff_start( assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" expected_prod_plan_directly_modified_summary = """**Directly Modified:** -* `sushi.waiter_revenue_by_day` (Non-breaking) +* `memory.sushi.waiter_revenue_by_day` (Non-breaking) ```diff --- @@ -529,10 +529,10 @@ def test_merge_pr_has_non_breaking_change_diff_start( ON o.id = oi.order_id AND o.event_date = oi.event_date ``` Indirectly Modified Children: - - `sushi.top_waiters` (Indirect Non-breaking) + - `memory.sushi.top_waiters` (Indirect Non-breaking) """ expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** -- `sushi.top_waiters` (Indirect Non-breaking) +- `memory.sushi.top_waiters` (Indirect Non-breaking) """ prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] @@ -1032,7 +1032,7 @@ def test_no_merge_since_no_deploy_signal( assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success expected_prod_plan_directly_modified_summary = """**Directly Modified:** -* `sushi.waiter_revenue_by_day` (Non-breaking) +* `memory.sushi.waiter_revenue_by_day` (Non-breaking) ```diff --- @@ -1052,10 +1052,10 @@ def test_no_merge_since_no_deploy_signal( ON o.id = oi.order_id AND o.event_date = oi.event_date ``` Indirectly Modified Children: - - `sushi.top_waiters` (Indirect Non-breaking)""" + - `memory.sushi.top_waiters` (Indirect Non-breaking)""" expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** -- `sushi.top_waiters` (Indirect Non-breaking) +- `memory.sushi.top_waiters` (Indirect Non-breaking) """ assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" @@ -1232,7 +1232,7 @@ def test_no_merge_since_no_deploy_signal_no_approvers_defined( assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success expected_prod_plan_directly_modified_summary = """**Directly Modified:** -* `sushi.waiter_revenue_by_day` (Non-breaking) +* `memory.sushi.waiter_revenue_by_day` (Non-breaking) ```diff --- @@ -1252,10 +1252,10 @@ def test_no_merge_since_no_deploy_signal_no_approvers_defined( ON o.id = oi.order_id AND o.event_date = oi.event_date ``` Indirectly Modified Children: - - `sushi.top_waiters` (Indirect Non-breaking) + - `memory.sushi.top_waiters` (Indirect Non-breaking) """ expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** -- `sushi.top_waiters` (Indirect Non-breaking) +- `memory.sushi.top_waiters` (Indirect Non-breaking) """ assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] @@ -1414,7 +1414,7 @@ def test_deploy_comment_pre_categorized( assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success expected_prod_plan_directly_modified_summary = """**Directly Modified:** -* `sushi.waiter_revenue_by_day` (Non-breaking) +* `memory.sushi.waiter_revenue_by_day` (Non-breaking) ```diff --- @@ -1434,10 +1434,10 @@ def test_deploy_comment_pre_categorized( ON o.id = oi.order_id AND o.event_date = oi.event_date ``` Indirectly Modified Children: - - `sushi.top_waiters` (Indirect Non-breaking) + - `memory.sushi.top_waiters` (Indirect Non-breaking) """ expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** -- `sushi.top_waiters` (Indirect Non-breaking) +- `memory.sushi.top_waiters` (Indirect Non-breaking) """ assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] @@ -1781,7 +1781,7 @@ def test_overlapping_changes_models( assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success expected_prod_plan_directly_modified_summary = """**Directly Modified:** -* `sushi.customers` (Non-breaking) +* `memory.sushi.customers` (Non-breaking) ```diff --- @@ -1801,23 +1801,23 @@ def test_overlapping_changes_models( WITH current_marketing AS ( ``` Indirectly Modified Children: - - `sushi.active_customers` (Indirect Non-breaking) - - `sushi.count_customers_active` (Indirect Non-breaking) - - `sushi.count_customers_inactive` (Indirect Non-breaking) - - `sushi.waiter_as_customer_by_day` (Indirect Breaking) + - `memory.sushi.active_customers` (Indirect Non-breaking) + - `memory.sushi.count_customers_active` (Indirect Non-breaking) + - `memory.sushi.count_customers_inactive` (Indirect Non-breaking) + - `memory.sushi.waiter_as_customer_by_day` (Indirect Breaking) -* `sushi.waiter_names` (Breaking) +* `memory.sushi.waiter_names` (Breaking) Indirectly Modified Children: - - `sushi.waiter_as_customer_by_day` (Indirect Breaking)""" + - `memory.sushi.waiter_as_customer_by_day` (Indirect Breaking)""" expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** -- `sushi.active_customers` (Indirect Non-breaking) -- `sushi.count_customers_active` (Indirect Non-breaking) -- `sushi.count_customers_inactive` (Indirect Non-breaking) -- `sushi.waiter_as_customer_by_day` (Indirect Breaking)""" +- `memory.sushi.active_customers` (Indirect Non-breaking) +- `memory.sushi.count_customers_active` (Indirect Non-breaking) +- `memory.sushi.count_customers_inactive` (Indirect Non-breaking) +- `memory.sushi.waiter_as_customer_by_day` (Indirect Breaking)""" assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] @@ -1993,7 +1993,7 @@ def test_pr_add_model( ) expected_prod_plan_summary = """**Added Models:** -- `sushi.cicd_test_model` (Breaking)""" +- `memory.sushi.cicd_test_model` (Breaking)""" assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping prod_plan_preview_checks_runs = controller._check_run_mapping[ @@ -2144,7 +2144,7 @@ def test_pr_delete_model( ) expected_prod_plan_summary = """**Removed Models:** -- `sushi.top_waiters` (Breaking)""" +- `memory.sushi.top_waiters` (Breaking)""" assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping prod_plan_preview_checks_runs = controller._check_run_mapping[ @@ -2330,7 +2330,7 @@ def test_has_required_approval_but_not_base_branch( assert GithubCheckStatus(prod_plan_preview_checks_runs[2]["status"]).is_completed assert GithubCheckConclusion(prod_plan_preview_checks_runs[2]["conclusion"]).is_success expected_prod_plan_directly_modified_summary = """**Directly Modified:** -* `sushi.waiter_revenue_by_day` (Non-breaking) +* `memory.sushi.waiter_revenue_by_day` (Non-breaking) ```diff --- @@ -2350,10 +2350,10 @@ def test_has_required_approval_but_not_base_branch( ON o.id = oi.order_id AND o.event_date = oi.event_date ``` Indirectly Modified Children: - - `sushi.top_waiters` (Indirect Non-breaking)""" + - `memory.sushi.top_waiters` (Indirect Non-breaking)""" expected_prod_plan_indirectly_modified_summary = """**Indirectly Modified:** -- `sushi.top_waiters` (Indirect Non-breaking)""" +- `memory.sushi.top_waiters` (Indirect Non-breaking)""" assert prod_plan_preview_checks_runs[2]["output"]["title"] == "Prod Plan Preview" prod_plan_preview_summary = prod_plan_preview_checks_runs[2]["output"]["summary"] From b3bd132089041910a588f9cf4f2012de90e87e78 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 8 Jul 2025 09:52:52 +1200 Subject: [PATCH 0549/1056] Feat(cicd_bot): Document and enable the min_intervals plan option (#4901) --- docs/concepts/plans.md | 57 +++++++++++++++++++ docs/integrations/github.md | 1 + sqlmesh/integrations/github/cicd/config.py | 1 + .../integrations/github/cicd/controller.py | 1 + tests/integrations/github/cicd/test_config.py | 6 ++ .../github/cicd/test_github_controller.py | 13 +++++ 6 files changed, 79 insertions(+) diff --git a/docs/concepts/plans.md b/docs/concepts/plans.md index 7903fe249f..da3d3debb7 100644 --- a/docs/concepts/plans.md +++ b/docs/concepts/plans.md @@ -246,6 +246,63 @@ Models needing backfill (missing dates): Enter the backfill end date (eg. '1 month ago', '2020-01-01') or blank to backfill up until '2024-09-27 00:00:00': ``` +#### Minimum intervals + +When you run a plan with a fixed `--start` or `--end` date, you create a virtual data environment with a limited subset of data. However, if the time range specified is less than the size of an interval on one of your models, that model will be skipped by default. + +For example, if you have a model like so: + +```sql +MODEL( + name sqlmesh_example.monthly_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column month + ), + cron '@monthly' +); + +SELECT SUM(a) AS sum_a, MONTH(day) AS month +FROM sqlmesh_example.upstream_model +WHERE day BETWEEN @start_ds AND @end_ds +``` + +make a change to it and run the following: + +```bash linenums="1" hl_lines="8" +$ sqlmesh plan dev --start '1 day ago' + +Models: +└── Added: + └── sqlmesh_example__dev.monthly_model +Apply - Virtual Update [y/n]: y + +SKIP: No model batches to execute +``` + +No data will be backfilled because `1 day ago` does not contain a complete month. However, you can use the `--min-intervals` option to override this behaviour like so: + +```bash linenums="1" hl_lines="11" +$ sqlmesh plan dev --start '1 day ago' --min-intervals 1 + +Models: +└── Added: + └── sqlmesh_example__dev.monthly_model +Apply - Virtual Update [y/n]: y + +[1/1] sqlmesh_example__dev.monthly_model [insert 2025-06-01 - 2025-06-30] 0.08s +Executing model batches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1/1 • 0:00:00 + +✔ Model batches executed +``` + +This will ensure that regardless of the plan `--start` date, all added or modified models will have at least `--min-intervals` intervals considered for backfill. + +!!! info + + If you are running plans manually you can just adjust the `--start` date to be wide enough to cover the models in question. + + The `--min-intervals` option is primarily intended for [automation scenarios](../integrations/github.md) where the plan is always run with a default relative start date and you always want (for example) "2 weeks worth of data" in the target environment. + ### Data preview for forward-only changes As mentioned earlier, the data output produced by [forward-only changes](#forward-only-change) in a development environment can only be used for preview and will not be reused in production. diff --git a/docs/integrations/github.md b/docs/integrations/github.md index 450e7af8f3..323aff0565 100644 --- a/docs/integrations/github.md +++ b/docs/integrations/github.md @@ -294,6 +294,7 @@ Below is an example of how to define the default config for the bot in either YA | `command_namespace` | The namespace to use for SQLMesh commands. For example if you provide `#SQLMesh` as a value then commands will be expected in the format of `#SQLMesh/`. Default: `None` meaning no namespace is used. | string | N | | `auto_categorize_changes` | Auto categorization behavior to use for the bot. If not provided then the project-wide categorization behavior is used. See [Auto-categorize model changes](https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#auto-categorize-model-changes) for details. | dict | N | | `default_pr_start` | Default start when creating PR environment plans. If running in a mode where the bot automatically backfills models (based on `auto_categorize_changes` behavior) then this can be used to limit the amount of data backfilled. Defaults to `None` meaning the start date is set to the earliest model's start or to 1 day ago if [data previews](../concepts/plans.md#data-preview) need to be computed. | str | N | +| `pr_min_intervals` | Intended for use when `default_pr_start` is set to a relative time, eg `1 week ago`. This ensures that at least this many intervals across every model are included for backfill in the PR environment. Without this, models with an interval unit wider than `default_pr_start` (such as `@monthly` models if `default_pr_start` was set to `1 week ago`) will be excluded from backfill entirely. | int | N | | `skip_pr_backfill` | Indicates if the bot should skip backfilling models in the PR environment. Default: `True` | bool | N | | `pr_include_unmodified` | Indicates whether to include unmodified models in the PR environment. Default to the project's config value (which defaults to `False`) | bool | N | | `run_on_deploy_to_prod` | Indicates whether to run latest intervals when deploying to prod. If set to false, the deployment will backfill only the changed models up to the existing latest interval in production, ignoring any missing intervals beyond this point. Default: `False` | bool | N | diff --git a/sqlmesh/integrations/github/cicd/config.py b/sqlmesh/integrations/github/cicd/config.py index b273329380..33312c4ad7 100644 --- a/sqlmesh/integrations/github/cicd/config.py +++ b/sqlmesh/integrations/github/cicd/config.py @@ -28,6 +28,7 @@ class GithubCICDBotConfig(BaseConfig): pr_include_unmodified: t.Optional[bool] = None run_on_deploy_to_prod: bool = False pr_environment_name: t.Optional[str] = None + pr_min_intervals: t.Optional[int] = None prod_branch_names_: t.Optional[str] = Field(default=None, alias="prod_branch_name") @model_validator(mode="before") diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index bc35e54520..19c494a979 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -402,6 +402,7 @@ def pr_plan(self) -> Plan: skip_linter=True, categorizer_config=self.bot_config.auto_categorize_changes, start=self.bot_config.default_pr_start, + min_intervals=self.bot_config.pr_min_intervals, skip_backfill=self.bot_config.skip_pr_backfill, include_unmodified=self.bot_config.pr_include_unmodified, ) diff --git a/tests/integrations/github/cicd/test_config.py b/tests/integrations/github/cicd/test_config.py index d42a5bdb4f..c100a1fa98 100644 --- a/tests/integrations/github/cicd/test_config.py +++ b/tests/integrations/github/cicd/test_config.py @@ -41,6 +41,7 @@ def test_load_yaml_config_default(tmp_path): assert config.cicd_bot.pr_include_unmodified is None assert config.cicd_bot.pr_environment_name is None assert config.cicd_bot.prod_branch_names == ["main", "master"] + assert not config.cicd_bot.pr_min_intervals def test_load_yaml_config(tmp_path): @@ -64,6 +65,7 @@ def test_load_yaml_config(tmp_path): pr_include_unmodified: true pr_environment_name: "MyOverride" prod_branch_name: testing + pr_min_intervals: 1 model_defaults: dialect: duckdb """, @@ -88,6 +90,7 @@ def test_load_yaml_config(tmp_path): assert config.cicd_bot.pr_include_unmodified assert config.cicd_bot.pr_environment_name == "MyOverride" assert config.cicd_bot.prod_branch_names == ["testing"] + assert config.cicd_bot.pr_min_intervals == 1 def test_load_python_config_defaults(tmp_path): @@ -119,6 +122,7 @@ def test_load_python_config_defaults(tmp_path): assert config.cicd_bot.pr_include_unmodified is None assert config.cicd_bot.pr_environment_name is None assert config.cicd_bot.prod_branch_names == ["main", "master"] + assert not config.cicd_bot.pr_min_intervals def test_load_python_config(tmp_path): @@ -141,6 +145,7 @@ def test_load_python_config(tmp_path): seed=AutoCategorizationMode.FULL, ), default_pr_start="1 week ago", + pr_min_intervals=1, enable_deploy_command=True, skip_pr_backfill=False, pr_include_unmodified=True, @@ -172,6 +177,7 @@ def test_load_python_config(tmp_path): assert config.cicd_bot.pr_include_unmodified assert config.cicd_bot.pr_environment_name == "MyOverride" assert config.cicd_bot.prod_branch_names == ["testing"] + assert config.cicd_bot.pr_min_intervals == 1 def test_validation(tmp_path): diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index 62cd5a26f4..46c7b2d85f 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -13,6 +13,7 @@ from sqlmesh.core.dialect import parse_one from sqlmesh.core.model import SqlModel from sqlmesh.core.user import User, UserRole +from sqlmesh.core.plan.definition import Plan from sqlmesh.integrations.github.cicd.config import GithubCICDBotConfig, MergeMethod from sqlmesh.integrations.github.cicd.controller import ( BotCommand, @@ -253,6 +254,18 @@ def test_pr_plan_auto_categorization(github_client, make_controller): assert controller._context._run_plan_tests.call_args == call(skip_tests=True) 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 + + +def test_pr_plan_min_intervals(github_client, make_controller): + controller = make_controller( + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + bot_config=GithubCICDBotConfig(default_pr_start="1 day ago", pr_min_intervals=1), + ) + assert controller.pr_plan.environment.name == "hello_world_2" + assert isinstance(controller.pr_plan, Plan) + assert controller.pr_plan.start_override_per_model def test_prod_plan(github_client, make_controller): From 12bd71e80a08ecc10d82706d4785a49826dceeb0 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:18:00 -0500 Subject: [PATCH 0550/1056] Fix: establish transaction and session before snapshot promotion/demotion (#4899) --- docs/concepts/macros/macro_variables.md | 3 +- sqlmesh/core/macros.py | 1 + sqlmesh/core/plan/evaluator.py | 19 +++++- sqlmesh/core/plan/stages.py | 5 +- sqlmesh/core/snapshot/evaluator.py | 77 +++++++++++++++++-------- tests/core/test_snapshot_evaluator.py | 4 ++ 6 files changed, 81 insertions(+), 28 deletions(-) diff --git a/docs/concepts/macros/macro_variables.md b/docs/concepts/macros/macro_variables.md index 858bf9f19d..a184f7d99f 100644 --- a/docs/concepts/macros/macro_variables.md +++ b/docs/concepts/macros/macro_variables.md @@ -132,7 +132,8 @@ SQLMesh provides additional predefined variables used to modify model behavior b * 'loading' - The project is being loaded into SQLMesh's runtime context. * 'creating' - The model tables are being created. * 'evaluating' - The model query logic is being evaluated. - * 'promoting' - The model is being promoted in the target environment (virtual layer update). + * 'promoting' - The model is being promoted in the target environment (view created during virtual layer update). + * 'demoting' - The model is being demoted in the target environment (view dropped during virtual layer update). * 'auditing' - The audit is being run. * 'testing' - The model query logic is being evaluated in the context of a unit test. * @gateway - A string value containing the name of the current [gateway](../../guides/connections.md). diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index 2c5ef3b2e8..ec5b2567f4 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -67,6 +67,7 @@ class RuntimeStage(Enum): CREATING = "creating" EVALUATING = "evaluating" PROMOTING = "promoting" + DEMOTING = "demoting" AUDITING = "auditing" TESTING = "testing" BEFORE_ALL = "before_all" diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 545a5e5494..9488b9bc91 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -340,9 +340,11 @@ def visit_virtual_layer_update_stage( ) if stage.demoted_environment_naming_info: self._demote_snapshots( - stage.demoted_snapshots, + [stage.all_snapshots[s.snapshot_id] for s in stage.demoted_snapshots], stage.demoted_environment_naming_info, + deployability_index=stage.deployability_index, on_complete=lambda s: self.console.update_promotion_progress(s, False), + snapshots=stage.all_snapshots, ) completed = True @@ -382,12 +384,23 @@ def _promote_snapshots( def _demote_snapshots( self, - target_snapshots: t.Iterable[SnapshotTableInfo], + target_snapshots: t.Iterable[Snapshot], environment_naming_info: EnvironmentNamingInfo, + snapshots: t.Dict[SnapshotId, Snapshot], + deployability_index: t.Optional[DeployabilityIndex] = None, on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None, ) -> None: self.snapshot_evaluator.demote( - target_snapshots, environment_naming_info, on_complete=on_complete + target_snapshots, + environment_naming_info, + table_mapping=to_view_mapping( + snapshots.values(), + environment_naming_info, + default_catalog=self.default_catalog, + dialect=self.snapshot_evaluator.adapter.dialect, + ), + deployability_index=deployability_index, + on_complete=on_complete, ) def _restatement_intervals_across_all_environments( diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 9913a87bd0..194177b0cf 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -361,11 +361,14 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: # Otherwise, unpause right after updatig the environment record. stages.append(UnpauseStage(promoted_snapshots=promoted_snapshots)) + full_demoted_snapshots = self.state_reader.get_snapshots( + s.snapshot_id for s in demoted_snapshots if s.snapshot_id not in snapshots + ) virtual_layer_update_stage = self._get_virtual_layer_update_stage( promoted_snapshots, demoted_snapshots, demoted_environment_naming_info, - snapshots, + snapshots | full_demoted_snapshots, deployability_index, ) if virtual_layer_update_stage: diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 641f216699..993860b527 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -276,8 +276,10 @@ def promote( def demote( self, - target_snapshots: t.Iterable[SnapshotInfoLike], + target_snapshots: t.Iterable[Snapshot], environment_naming_info: EnvironmentNamingInfo, + table_mapping: t.Optional[t.Dict[str, str]] = None, + deployability_index: t.Optional[DeployabilityIndex] = None, on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None, ) -> None: """Demotes the given collection of snapshots in the target environment by removing its view. @@ -290,7 +292,13 @@ def demote( with self.concurrent_context(): concurrent_apply_to_snapshots( target_snapshots, - lambda s: self._demote_snapshot(s, environment_naming_info, on_complete), + lambda s: self._demote_snapshot( + s, + environment_naming_info, + deployability_index=deployability_index, + on_complete=on_complete, + table_mapping=table_mapping, + ), self.ddl_concurrent_tasks, ) @@ -970,25 +978,32 @@ def _promote_snapshot( snapshots: t.Optional[t.Dict[SnapshotId, Snapshot]] = None, table_mapping: t.Optional[t.Dict[str, str]] = None, ) -> None: - if snapshot.is_model: - adapter = ( - self.get_adapter(snapshot.model_gateway) - if environment_naming_info.gateway_managed - else self.adapter - ) - table_name = snapshot.table_name(deployability_index.is_representative(snapshot)) - view_name = snapshot.qualified_view_name.for_environment( - environment_naming_info, dialect=adapter.dialect - ) - render_kwargs: t.Dict[str, t.Any] = dict( - start=start, - end=end, - execution_time=execution_time, - engine_adapter=adapter, - deployability_index=deployability_index, - table_mapping=table_mapping, - runtime_stage=RuntimeStage.PROMOTING, - ) + if not snapshot.is_model: + return + + adapter = ( + self.get_adapter(snapshot.model_gateway) + if environment_naming_info.gateway_managed + else self.adapter + ) + table_name = snapshot.table_name(deployability_index.is_representative(snapshot)) + view_name = snapshot.qualified_view_name.for_environment( + environment_naming_info, dialect=adapter.dialect + ) + render_kwargs: t.Dict[str, t.Any] = dict( + start=start, + end=end, + execution_time=execution_time, + engine_adapter=adapter, + deployability_index=deployability_index, + table_mapping=table_mapping, + runtime_stage=RuntimeStage.PROMOTING, + ) + + with ( + adapter.transaction(), + adapter.session(snapshot.model.render_session_properties(**render_kwargs)), + ): _evaluation_strategy(snapshot, adapter).promote( table_name=table_name, view_name=view_name, @@ -1007,10 +1022,15 @@ def _promote_snapshot( def _demote_snapshot( self, - snapshot: SnapshotInfoLike, + snapshot: Snapshot, environment_naming_info: EnvironmentNamingInfo, + deployability_index: t.Optional[DeployabilityIndex], on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]], + table_mapping: t.Optional[t.Dict[str, str]] = None, ) -> None: + if not snapshot.is_model: + return + adapter = ( self.get_adapter(snapshot.model_gateway) if environment_naming_info.gateway_managed @@ -1019,7 +1039,18 @@ def _demote_snapshot( view_name = snapshot.qualified_view_name.for_environment( environment_naming_info, dialect=adapter.dialect ) - _evaluation_strategy(snapshot, adapter).demote(view_name) + with ( + adapter.transaction(), + adapter.session( + snapshot.model.render_session_properties( + engine_adapter=adapter, + deployability_index=deployability_index, + table_mapping=table_mapping, + runtime_stage=RuntimeStage.DEMOTING, + ) + ), + ): + _evaluation_strategy(snapshot, adapter).demote(view_name) if on_complete is not None: on_complete(snapshot) diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 2888511ba1..93cef90daf 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -294,6 +294,8 @@ def test_promote(mocker: MockerFixture, adapter_mock, make_snapshot): evaluator.promote([snapshot], EnvironmentNamingInfo(name="test_env")) + adapter_mock.transaction.assert_called() + adapter_mock.session.assert_called() adapter_mock.create_schema.assert_called_once_with(to_schema("test_schema__test_env")) adapter_mock.create_view.assert_called_once_with( "test_schema__test_env.test_model", @@ -320,6 +322,8 @@ def test_demote(mocker: MockerFixture, adapter_mock, make_snapshot): evaluator.demote([snapshot], EnvironmentNamingInfo(name="test_env")) + adapter_mock.transaction.assert_called() + adapter_mock.session.assert_called() adapter_mock.drop_view.assert_called_once_with( "test_schema__test_env.test_model", cascade=False, From c9c1429335f055d6adf10d60f9c60557d963f407 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 8 Jul 2025 01:58:52 +0300 Subject: [PATCH 0551/1056] Chore!: bump sqlglot to v27.0.0 (#4929) --- pyproject.toml | 2 +- tests/core/test_audit.py | 2 +- tests/core/test_context.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 046fbee025..d451532eb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=26.33.0", + "sqlglot[rs]~=27.0.0", "tenacity", "time-machine", "json-stream" diff --git a/tests/core/test_audit.py b/tests/core/test_audit.py index 1befb1148c..da8145ba87 100644 --- a/tests/core/test_audit.py +++ b/tests/core/test_audit.py @@ -621,7 +621,7 @@ def test_chi_square_audit(model: Model): ) assert ( rendered_query.sql() - == """WITH "samples" AS (SELECT "a" AS "x_a", "b" AS "x_b" FROM (SELECT * FROM "db"."test_model" AS "test_model" WHERE "ds" BETWEEN '1970-01-01' AND '1970-01-01') AS "_q_0" WHERE (NOT "a" IS NULL AND NOT "b" IS NULL) AND TRUE), "contingency_table" AS (SELECT "x_a", "x_b", COUNT(*) AS "observed", (SELECT COUNT(*) FROM "samples" AS "t" WHERE "r"."x_a" = "t"."x_a") AS "tot_a", (SELECT COUNT(*) FROM "samples" AS "t" WHERE "r"."x_b" = "t"."x_b") AS "tot_b", (SELECT COUNT(*) FROM "samples" AS "samples") AS "g_t" /* g_t is the grand total */ FROM "samples" AS "r" GROUP BY "x_a", "x_b") SELECT ((SELECT COUNT(DISTINCT "x_a") FROM "contingency_table" AS "contingency_table") - 1) * ((SELECT COUNT(DISTINCT "x_b") FROM "contingency_table" AS "contingency_table") - 1) AS "degrees_of_freedom", SUM(("observed" - ("tot_a" * "tot_b" / "g_t")) * ("observed" - ("tot_a" * "tot_b" / "g_t")) / ("tot_a" * "tot_b" / "g_t")) AS "chi_square" FROM "contingency_table" AS "contingency_table" HAVING NOT "chi_square" > 9.48773""" + == """WITH "samples" AS (SELECT "a" AS "x_a", "b" AS "x_b" FROM (SELECT * FROM "db"."test_model" AS "test_model" WHERE "ds" BETWEEN '1970-01-01' AND '1970-01-01') AS "_q_0" WHERE (NOT "a" IS NULL AND NOT "b" IS NULL) AND TRUE), "contingency_table" AS (SELECT "x_a", "x_b", COUNT(*) AS "observed", (SELECT COUNT(*) FROM "samples" AS "t" WHERE "r"."x_a" = "t"."x_a") AS "tot_a", (SELECT COUNT(*) FROM "samples" AS "t" WHERE "r"."x_b" = "t"."x_b") AS "tot_b", (SELECT COUNT(*) FROM "samples" AS "samples") AS "g_t" /* g_t is the grand total */ FROM "samples" AS "r" GROUP BY "x_a", "x_b") SELECT ((SELECT COUNT(DISTINCT "x_a") FROM "contingency_table" AS "contingency_table") - 1) * ((SELECT COUNT(DISTINCT "x_b") FROM "contingency_table" AS "contingency_table") - 1) AS "degrees_of_freedom", SUM(("observed" - ("tot_a" * "tot_b" / "g_t")) * ("observed" - ("tot_a" * "tot_b" / "g_t")) / ("tot_a" * "tot_b" / "g_t")) AS "chi_square" FROM "contingency_table" AS "contingency_table" /* H0: the two variables are independent */ /* H1: the two variables are dependent */ /* if chi_square > critical_value, reject H0 */ /* if chi_square <= critical_value, fail to reject H0 */ HAVING NOT "chi_square" > 9.48773""" ) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 0728168a0f..ddf9138779 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1840,7 +1840,7 @@ def access_adapter(evaluator): assert ( model.pre_statements[0].sql() - == "@IF(@runtime_stage = 'evaluating', SET VARIABLE stats_model_start = now())" + == "@IF(@runtime_stage = 'evaluating', SET VARIABLE stats_model_start = NOW())" ) assert ( model.post_statements[0].sql() From 7c31d5116ba7387cc8d7c15c48cf0df5c46ff5a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:31:38 +0100 Subject: [PATCH 0552/1056] Chore(deps): Bump astral-sh/setup-uv from 4 to 6 (#4931) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@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 77ec906b13..78253b1288 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -43,7 +43,7 @@ jobs: with: python-version: '3.12' - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v6 - name: Install python dependencies run: | python -m venv .venv From 81a9c0ec2cfedc12c7dca3255a2a8f61a9f9cfb5 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:04:13 +0300 Subject: [PATCH 0553/1056] Fix: Freeze time to make github test determinstic (#4934) --- tests/integrations/github/cicd/test_github_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index 46c7b2d85f..fafc6f6291 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -6,6 +6,7 @@ from unittest.mock import PropertyMock, call import pytest +import time_machine from pytest_mock.plugin import MockerFixture from sqlmesh.core import constants as c @@ -608,6 +609,7 @@ def test_uncategorized( assert "Run `sqlmesh plan hello_world_2` locally to apply these changes" in summary +@time_machine.travel("2025-07-07 00:00:00 UTC", tick=False) def test_get_plan_summary_doesnt_truncate_backfill_list( github_client, make_controller: t.Callable[..., GithubController] ): From d1dc55c83df11eb16051c90a9bb9f9050869c366 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:02:25 +0100 Subject: [PATCH 0554/1056] feat: ability to find a model block (#4933) --- sqlmesh/core/linter/helpers.py | 39 ++++++++++++++++++++++++++++ sqlmesh/core/linter/rules/builtin.py | 18 +++++++++++-- tests/core/linter/test_helpers.py | 36 +++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 tests/core/linter/test_helpers.py diff --git a/sqlmesh/core/linter/helpers.py b/sqlmesh/core/linter/helpers.py index 6d0d796c97..59fd478f78 100644 --- a/sqlmesh/core/linter/helpers.py +++ b/sqlmesh/core/linter/helpers.py @@ -2,6 +2,7 @@ from sqlmesh.core.linter.rule import Position, Range from sqlmesh.utils.pydantic import PydanticModel +from sqlglot import tokenize, TokenType import typing as t @@ -113,3 +114,41 @@ def read_range_from_file(file: Path, text_range: Range) -> str: result.append(line[start_char:end_char]) return "".join(result) + + +def get_range_of_model_block( + sql: str, + dialect: str, +) -> t.Optional[Range]: + """ + Get the range of the model block in an SQL file. + """ + tokens = tokenize(sql, dialect=dialect) + + # Find start of the model block + start = next( + (t for t in tokens if t.token_type is TokenType.VAR and t.text.upper() == "MODEL"), + None, + ) + end = next((t for t in tokens if t.token_type is TokenType.SEMICOLON), None) + + if start is None or end is None: + return None + + start_position = TokenPositionDetails( + line=start.line, + col=start.col, + start=start.start, + end=start.end, + ) + end_position = TokenPositionDetails( + line=end.line, + col=end.col, + start=end.start, + end=end.end, + ) + + splitlines = sql.splitlines() + return Range( + start=start_position.to_range(splitlines).start, end=end_position.to_range(splitlines).end + ) diff --git a/sqlmesh/core/linter/rules/builtin.py b/sqlmesh/core/linter/rules/builtin.py index 02c2bb628e..8bf6e33720 100644 --- a/sqlmesh/core/linter/rules/builtin.py +++ b/sqlmesh/core/linter/rules/builtin.py @@ -7,7 +7,7 @@ from sqlglot.expressions import Star from sqlglot.helper import subclasses -from sqlmesh.core.linter.helpers import TokenPositionDetails +from sqlmesh.core.linter.helpers import TokenPositionDetails, get_range_of_model_block from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit from sqlmesh.core.linter.definition import RuleSet from sqlmesh.core.model import Model, SqlModel @@ -93,7 +93,21 @@ class NoMissingAudits(Rule): """Model `audits` must be configured to test data quality.""" def check_model(self, model: Model) -> t.Optional[RuleViolation]: - return self.violation() if not model.audits and not model.kind.is_symbolic else None + if model.audits or model.kind.is_symbolic: + return None + if model._path is None or not str(model._path).endswith(".sql"): + return self.violation() + + try: + with open(model._path, "r", encoding="utf-8") as file: + content = file.read() + + range = get_range_of_model_block(content, model.dialect) + if range: + return self.violation(violation_range=range) + return self.violation() + except Exception: + return self.violation() BUILTIN_RULES = RuleSet(subclasses(__name__, Rule, (Rule,))) diff --git a/tests/core/linter/test_helpers.py b/tests/core/linter/test_helpers.py new file mode 100644 index 0000000000..be6ebf2c27 --- /dev/null +++ b/tests/core/linter/test_helpers.py @@ -0,0 +1,36 @@ +from sqlmesh import Context +from sqlmesh.core.linter.helpers import read_range_from_file, get_range_of_model_block +from sqlmesh.core.model import SqlModel + + +def test_get_position_of_model_block(): + context = Context(paths=["examples/sushi"]) + + sql_models = [ + model + for model in context.models.values() + if isinstance(model, SqlModel) + and model._path is not None + and str(model._path).endswith(".sql") + ] + assert len(sql_models) > 0 + + for model in sql_models: + dialect = model.dialect + assert dialect is not None + + path = model._path + assert path is not None + + with open(path, "r", encoding="utf-8") as file: + content = file.read() + + as_lines = content.splitlines() + + range = get_range_of_model_block(content, dialect) + assert range is not None + + # Check that the range starts with MODEL and ends with ; + read_range = read_range_from_file(path, range) + assert read_range.startswith("MODEL") + assert read_range.endswith(";") From 40bb6c86a05f637f0d170db6eb3d5757688df55a Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Tue, 8 Jul 2025 17:58:14 +0300 Subject: [PATCH 0555/1056] Fix: Do not attach `correlation_id` for Athena queries (#4935) --- sqlmesh/core/engine_adapter/athena.py | 4 ++++ sqlmesh/core/engine_adapter/base.py | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/engine_adapter/athena.py b/sqlmesh/core/engine_adapter/athena.py index 88ab9b2c5d..abaf7ba281 100644 --- a/sqlmesh/core/engine_adapter/athena.py +++ b/sqlmesh/core/engine_adapter/athena.py @@ -41,6 +41,10 @@ class AthenaEngineAdapter(PandasNativeFetchDFSupportMixin, RowDiffMixin): COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED SCHEMA_DIFFER = TrinoEngineAdapter.SCHEMA_DIFFER MAX_TIMESTAMP_PRECISION = 3 # copied from Trino + # Athena does not deal with comments well, e.g: + # >>> self._execute('/* test */ DESCRIBE foo') + # pyathena.error.OperationalError: FAILED: ParseException line 1:0 cannot recognize input near '/' '*' 'test' + ATTACH_CORRELATION_ID = False def __init__( self, *args: t.Any, s3_warehouse_location: t.Optional[str] = None, **kwargs: t.Any diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 1d34ff1401..33ad4c398a 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -109,6 +109,7 @@ class EngineAdapter: DEFAULT_CATALOG_TYPE = DIALECT QUOTE_IDENTIFIERS_IN_VIEWS = True MAX_IDENTIFIER_LENGTH: t.Optional[int] = None + ATTACH_CORRELATION_ID = True def __init__( self, @@ -2219,8 +2220,7 @@ def execute( else: sql = t.cast(str, e) - if self.correlation_id: - sql = f"/* {self.correlation_id} */ {sql}" + sql = self._attach_correlation_id(sql) self._log_sql( sql, @@ -2229,6 +2229,11 @@ def execute( ) self._execute(sql, **kwargs) + def _attach_correlation_id(self, sql: str) -> str: + if self.ATTACH_CORRELATION_ID and self.correlation_id: + return f"/* {self.correlation_id} */ {sql}" + return sql + def _log_sql( self, sql: str, From 537a31187a56eda1faae8e033b2440f75ca79805 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:01:58 -0700 Subject: [PATCH 0556/1056] fix!: make repr deterministic for fingerprinting (#4925) --- .../migrations/v0085_deterministic_repr.py | 134 +++++++++++++++ sqlmesh/utils/metaprogramming.py | 36 +++- tests/utils/test_metaprogramming.py | 162 +++++++++++++++++- 3 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 sqlmesh/migrations/v0085_deterministic_repr.py diff --git a/sqlmesh/migrations/v0085_deterministic_repr.py b/sqlmesh/migrations/v0085_deterministic_repr.py new file mode 100644 index 0000000000..9364926068 --- /dev/null +++ b/sqlmesh/migrations/v0085_deterministic_repr.py @@ -0,0 +1,134 @@ +""" +When serializing some objects, like `__sqlmesh__vars__`, the order of keys in the dictionary were not deterministic +and therefore this migration applies deterministic sorting to the keys of the dictionary. +""" + +import json +import typing as t +from dataclasses import dataclass + +from sqlglot import exp + +from sqlmesh.utils.migration import index_text_type, blob_text_type + + +# Make sure `SqlValue` is defined so it can be used by `eval` call in the migration +@dataclass +class SqlValue: + """A SQL string representing a generated SQLGlot AST.""" + + sql: str + + +def _deterministic_repr(obj: t.Any) -> str: + """ + This is a copy of the function from utils.metaprogramming + """ + + def _normalize_for_repr(o: t.Any) -> t.Any: + if isinstance(o, dict): + sorted_items = sorted(o.items(), key=lambda x: str(x[0])) + return {k: _normalize_for_repr(v) for k, v in sorted_items} + if isinstance(o, (list, tuple)): + # Recursively normalize nested structures + normalized = [_normalize_for_repr(item) for item in o] + return type(o)(normalized) + return o + + try: + return repr(_normalize_for_repr(obj)) + except Exception: + return repr(obj) + + +def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + migration_needed = False + new_snapshots = [] + + for ( + name, + identifier, + version, + snapshot, + kind_name, + updated_ts, + unpaused_ts, + ttl_ms, + unrestorable, + ) in engine_adapter.fetchall( + exp.select( + "name", + "identifier", + "version", + "snapshot", + "kind_name", + "updated_ts", + "unpaused_ts", + "ttl_ms", + "unrestorable", + ).from_(snapshots_table), + quote_identifiers=True, + ): + parsed_snapshot = json.loads(snapshot) + python_env = parsed_snapshot["node"].get("python_env") + + if python_env: + for key, executable in python_env.items(): + if isinstance(executable, dict) and executable.get("kind") == "value": + old_payload = executable["payload"] + try: + # Try to parse the old payload and re-serialize it deterministically + parsed_value = eval(old_payload) + new_payload = _deterministic_repr(parsed_value) + + # Only update if the representation changed + if old_payload != new_payload: + executable["payload"] = new_payload + migration_needed = True + except Exception: + # If we still can't eval it, leave it as-is + pass + + new_snapshots.append( + { + "name": name, + "identifier": identifier, + "version": version, + "snapshot": json.dumps(parsed_snapshot), + "kind_name": kind_name, + "updated_ts": updated_ts, + "unpaused_ts": unpaused_ts, + "ttl_ms": ttl_ms, + "unrestorable": unrestorable, + } + ) + + if migration_needed and new_snapshots: + engine_adapter.delete_from(snapshots_table, "TRUE") + + index_type = index_text_type(engine_adapter.dialect) + blob_type = blob_text_type(engine_adapter.dialect) + + engine_adapter.insert_append( + snapshots_table, + pd.DataFrame(new_snapshots), + columns_to_types={ + "name": exp.DataType.build(index_type), + "identifier": exp.DataType.build(index_type), + "version": exp.DataType.build(index_type), + "snapshot": exp.DataType.build(blob_type), + "kind_name": exp.DataType.build("text"), + "updated_ts": exp.DataType.build("bigint"), + "unpaused_ts": exp.DataType.build("bigint"), + "ttl_ms": exp.DataType.build("bigint"), + "unrestorable": exp.DataType.build("boolean"), + }, + ) diff --git a/sqlmesh/utils/metaprogramming.py b/sqlmesh/utils/metaprogramming.py index 9bfb8efc4e..c4340c0321 100644 --- a/sqlmesh/utils/metaprogramming.py +++ b/sqlmesh/utils/metaprogramming.py @@ -425,7 +425,9 @@ def is_value(self) -> bool: @classmethod def value(cls, v: t.Any, is_metadata: t.Optional[bool] = None) -> Executable: - return Executable(payload=repr(v), kind=ExecutableKind.VALUE, is_metadata=is_metadata) + return Executable( + payload=_deterministic_repr(v), kind=ExecutableKind.VALUE, is_metadata=is_metadata + ) def serialize_env(env: t.Dict[str, t.Any], path: Path) -> t.Dict[str, Executable]: @@ -633,6 +635,38 @@ def print_exception( out.write(tb) +def _deterministic_repr(obj: t.Any) -> str: + """Create a deterministic representation by ensuring consistent ordering before repr(). + + For dictionaries, ensures consistent key ordering to prevent non-deterministic + serialization that affects fingerprinting. Uses Python's native repr() logic + for all formatting to handle edge cases properly. + + Note that this function assumes list/tuple order is significant and therefore does not sort them. + + Args: + obj: The object to represent as a string. + + Returns: + A deterministic string representation of the object. + """ + + def _normalize_for_repr(o: t.Any) -> t.Any: + if isinstance(o, dict): + sorted_items = sorted(o.items(), key=lambda x: str(x[0])) + return {k: _normalize_for_repr(v) for k, v in sorted_items} + if isinstance(o, (list, tuple)): + # Recursively normalize nested structures + normalized = [_normalize_for_repr(item) for item in o] + return type(o)(normalized) + return o + + try: + return repr(_normalize_for_repr(obj)) + except Exception: + return repr(obj) + + def import_python_file(path: Path, relative_base: Path = Path()) -> types.ModuleType: relative_path = path.absolute().relative_to(relative_base.absolute()) module_name = str(relative_path.with_suffix("")).replace(os.path.sep, ".") diff --git a/tests/utils/test_metaprogramming.py b/tests/utils/test_metaprogramming.py index 8cca48ac6e..cb8421fac8 100644 --- a/tests/utils/test_metaprogramming.py +++ b/tests/utils/test_metaprogramming.py @@ -22,6 +22,7 @@ from sqlmesh.utils.metaprogramming import ( Executable, ExecutableKind, + _deterministic_repr, build_env, func_globals, normalize_source, @@ -48,7 +49,7 @@ def test_print_exception(mocker: MockerFixture): except Exception as ex: print_exception(ex, test_env, out_mock) - expected_message = r""" File ".*?.tests.utils.test_metaprogramming\.py", line 47, in test_print_exception + expected_message = r""" File ".*?.tests.utils.test_metaprogramming\.py", line 48, in test_print_exception eval\("test_fun\(\)", env\).* File '/test/path.py' \(or imported file\), line 2, in test_fun @@ -457,3 +458,162 @@ def test_serialize_env_with_enum_import_appearing_in_two_functions() -> None: } assert serialized_env == expected_env + + +def test_deterministic_repr_basic_types(): + """Test _deterministic_repr with basic Python types.""" + # Test basic types that should use standard repr + assert _deterministic_repr(42) == "42" + assert _deterministic_repr("hello") == "'hello'" + assert _deterministic_repr(True) == "True" + assert _deterministic_repr(None) == "None" + assert _deterministic_repr(3.14) == "3.14" + + +def test_deterministic_repr_dict_ordering(): + """Test that _deterministic_repr produces consistent output for dicts with different key ordering.""" + # Same dict with different key ordering + dict1 = {"c": 3, "a": 1, "b": 2} + dict2 = {"a": 1, "b": 2, "c": 3} + dict3 = {"b": 2, "c": 3, "a": 1} + + repr1 = _deterministic_repr(dict1) + repr2 = _deterministic_repr(dict2) + repr3 = _deterministic_repr(dict3) + + # All should produce the same representation + assert repr1 == repr2 == repr3 + assert repr1 == "{'a': 1, 'b': 2, 'c': 3}" + + +def test_deterministic_repr_mixed_key_types(): + """Test _deterministic_repr with mixed key types (strings and numbers).""" + dict1 = {42: "number", "string": "text", 1: "one"} + dict2 = {"string": "text", 1: "one", 42: "number"} + + repr1 = _deterministic_repr(dict1) + repr2 = _deterministic_repr(dict2) + + # Should produce consistent ordering despite mixed key types + assert repr1 == repr2 + # Numbers come before strings when sorting by string representation + assert repr1 == "{1: 'one', 42: 'number', 'string': 'text'}" + + +def test_deterministic_repr_nested_structures(): + """Test _deterministic_repr with deeply nested dictionaries.""" + nested1 = {"outer": {"z": 26, "a": 1}, "list": [3, {"y": 2, "x": 1}], "simple": "value"} + + nested2 = {"simple": "value", "list": [3, {"x": 1, "y": 2}], "outer": {"a": 1, "z": 26}} + + repr1 = _deterministic_repr(nested1) + repr2 = _deterministic_repr(nested2) + + assert repr1 == repr2 + # Verify structure is maintained with sorted keys + expected = "{'list': [3, {'x': 1, 'y': 2}], 'outer': {'a': 1, 'z': 26}, 'simple': 'value'}" + assert repr1 == expected + + +def test_deterministic_repr_lists_and_tuples(): + """Test _deterministic_repr preserves order for lists/tuples but sorts nested dicts.""" + # Lists should maintain their order + list_with_dicts = [{"b": 2, "a": 1}, {"d": 4, "c": 3}] + list_repr = _deterministic_repr(list_with_dicts) + expected_list = "[{'a': 1, 'b': 2}, {'c': 3, 'd': 4}]" + assert list_repr == expected_list + + # Tuples should maintain their order + tuple_with_dicts = ({"z": 26, "a": 1}, {"y": 25, "b": 2}) + tuple_repr = _deterministic_repr(tuple_with_dicts) + expected_tuple = "({'a': 1, 'z': 26}, {'b': 2, 'y': 25})" + assert tuple_repr == expected_tuple + + +def test_deterministic_repr_empty_containers(): + """Test _deterministic_repr with empty containers.""" + assert _deterministic_repr({}) == "{}" + assert _deterministic_repr([]) == "[]" + assert _deterministic_repr(()) == "()" + + +def test_deterministic_repr_special_characters(): + """Test _deterministic_repr handles special characters correctly.""" + special_dict = { + "quotes": "text with 'single' and \"double\" quotes", + "unicode": "unicode: ñáéíóú", + "newlines": "text\nwith\nnewlines", + "backslashes": "path\\to\\file", + } + + result = _deterministic_repr(special_dict) + + # Should be valid Python that can be evaluated + reconstructed = eval(result) + assert reconstructed == special_dict + + # Should be deterministic - same input produces same output + result2 = _deterministic_repr(special_dict) + assert result == result2 + + +def test_deterministic_repr_executable_integration(): + """Test that _deterministic_repr works correctly with Executable.value().""" + # Test the integration with Executable.value which is the main use case + variables1 = {"env": "dev", "debug": True, "timeout": 30} + variables2 = {"timeout": 30, "debug": True, "env": "dev"} + + exec1 = Executable.value(variables1) + exec2 = Executable.value(variables2) + + # Should produce identical payloads despite different input ordering + assert exec1.payload == exec2.payload + assert exec1.payload == "{'debug': True, 'env': 'dev', 'timeout': 30}" + + # Should be valid Python + reconstructed = eval(exec1.payload) + assert reconstructed == variables1 + + +def test_deterministic_repr_complex_example(): + """Test _deterministic_repr with a complex real-world-like structure.""" + complex_vars = { + "database_config": { + "host": "localhost", + "port": 5432, + "credentials": {"username": "admin", "password": "secret"}, + }, + "feature_flags": ["flag_b", "flag_a"], + "metadata": { + "version": "1.0.0", + "environment": "production", + "tags": {"team": "data", "project": "analytics"}, + }, + 42: "numeric_key", + "arrays": [{"config": {"nested": True, "level": 2}}, {"simple": "value"}], + } + + expected_structure = { + 42: "numeric_key", + "arrays": [{"config": {"level": 2, "nested": True}}, {"simple": "value"}], + "database_config": { + "credentials": {"password": "secret", "username": "admin"}, + "host": "localhost", + "port": 5432, + }, + "feature_flags": ["flag_b", "flag_a"], + "metadata": { + "environment": "production", + "tags": {"project": "analytics", "team": "data"}, + "version": "1.0.0", + }, + } + + actual_repr = _deterministic_repr(complex_vars) + expected_repr = repr(expected_structure) + assert actual_repr == expected_repr + + # Should be valid Python + reconstructed = eval(actual_repr) + assert isinstance(reconstructed, dict) + assert reconstructed == complex_vars From 4da307a868e223700f4e31b0f2c4a315387ce5f3 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:12:51 +0300 Subject: [PATCH 0557/1056] Fix: do not display signal progress if a project has no signals (#4939) --- sqlmesh/core/console.py | 5 ++++- sqlmesh/core/scheduler.py | 2 +- tests/cli/test_cli.py | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 78020b693f..2f51a20a74 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -912,6 +912,7 @@ def __init__( self.table_diff_model_tasks: t.Dict[str, TaskID] = {} self.table_diff_progress_live: t.Optional[Live] = None + self.signal_progress_logged = False self.signal_status_tree: t.Optional[Tree] = None self.verbosity = verbosity @@ -956,7 +957,8 @@ def start_evaluation_progress( ) -> None: """Indicates that a new snapshot evaluation/auditing progress has begun.""" # Add a newline to separate signal checking from evaluation - self._print("") + if self.signal_progress_logged: + self._print("") if not self.evaluation_progress_live: self.evaluation_total_progress = make_progress_bar( @@ -1188,6 +1190,7 @@ def stop_signal_progress(self) -> None: if self.signal_status_tree is not None: self._print(self.signal_status_tree) self.signal_status_tree = None + self.signal_progress_logged = True def start_creation_progress( self, diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index 2b9cb6189f..4582b24485 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -754,7 +754,7 @@ def _check_ready_intervals( """ signals = snapshot.is_model and snapshot.model.render_signal_calls() - if not signals: + if not (signals and signals.signals_to_kwargs): return intervals self.console.start_signal_progress( diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 9078e9832d..6f0d1ac089 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -2175,3 +2175,18 @@ def none_ready(batch): # Only one model was executed assert "100.0% • 1/1 • 0:00:00" in result.output + + rmtree(tmp_path) + tmp_path.mkdir(parents=True, exist_ok=True) + + create_example_project(tmp_path) + + # Example project models have start dates, so there are no date prompts + # for the `prod` environment. + # Input: `y` to apply and backfill + result = runner.invoke( + cli, ["--log-file-dir", str(tmp_path), "--paths", str(tmp_path), "plan"], input="y\n" + ) + assert_plan_success(result) + + assert "Checking signals" not in result.output From cccc5eaeb05b748461d8565294e72ffcdc538366 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:40:45 +0100 Subject: [PATCH 0558/1056] feat(lsp): return load time errors as diagnostics (#4940) --- sqlmesh/core/loader.py | 27 ++++--- sqlmesh/core/model/common.py | 9 ++- sqlmesh/core/model/definition.py | 5 +- sqlmesh/lsp/context.py | 12 ---- sqlmesh/lsp/errors.py | 51 ++++++++++++++ sqlmesh/lsp/main.py | 116 +++++++++++++++++++------------ sqlmesh/utils/errors.py | 13 ++-- 7 files changed, 157 insertions(+), 76 deletions(-) create mode 100644 sqlmesh/lsp/errors.py diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index 90894fd23d..2b40be0230 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -349,7 +349,7 @@ def _load(path: Path) -> t.List[Model]: for row in YAML().load(file.read()) ] except Exception as ex: - raise ConfigError(self._failed_to_load_model_error(path, ex)) + raise ConfigError(self._failed_to_load_model_error(path, ex), path) for path in paths_to_load: self._track_file(path) @@ -363,7 +363,8 @@ def _load(path: Path) -> t.List[Model]: raise ConfigError( self._failed_to_load_model_error( path, f"Duplicate external model name: '{model.name}'." - ) + ), + path, ) models[model.fqn] = model @@ -375,7 +376,8 @@ def _load(path: Path) -> t.List[Model]: raise ConfigError( self._failed_to_load_model_error( path, f"Duplicate external model name: '{model.name}'." - ) + ), + path, ) models.update({model.fqn: model}) @@ -402,13 +404,15 @@ def _load_requirements(self) -> t.Tuple[t.Dict[str, str], t.Set[str]]: args = [k.strip() for k in line.split("==")] if len(args) != 2: raise ConfigError( - f"Invalid lock file entry '{line.strip()}'. Only 'dep==ver' is supported" + f"Invalid lock file entry '{line.strip()}'. Only 'dep==ver' is supported", + requirements_path, ) dep, ver = args other_ver = requirements.get(dep, ver) if ver != other_ver: raise ConfigError( - f"Conflicting requirement {dep}: {ver} != {other_ver}. Fix your {c.REQUIREMENTS} file." + f"Conflicting requirement {dep}: {ver} != {other_ver}. Fix your {c.REQUIREMENTS} file.", + requirements_path, ) requirements[dep] = ver @@ -619,13 +623,14 @@ def _load_sql_models( raise ConfigError( self._failed_to_load_model_error( path, f"Duplicate SQL model name: '{model.name}'." - ) + ), + path, ) elif model.enabled: model._path = path models[model.fqn] = model except Exception as ex: - raise ConfigError(self._failed_to_load_model_error(path, ex)) + raise ConfigError(self._failed_to_load_model_error(path, ex), path) return models @@ -678,7 +683,7 @@ def _load_python_models( if model.enabled: models[model.fqn] = model except Exception as ex: - raise ConfigError(self._failed_to_load_model_error(path, ex)) + raise ConfigError(self._failed_to_load_model_error(path, ex), path) finally: model_registry._dialect = None @@ -782,7 +787,9 @@ def _load_metrics(self) -> UniqueKeyDict[str, MetricMeta]: metric = load_metric_ddl(expression, path=path, dialect=dialect) metrics[metric.name] = metric except SqlglotError as ex: - raise ConfigError(f"Failed to parse metric definitions at '{path}': {ex}.") + raise ConfigError( + f"Failed to parse metric definitions at '{path}': {ex}.", path + ) return metrics @@ -1005,7 +1012,7 @@ def _load_scripts(self) -> t.Tuple[MacroRegistry, JinjaMacroRegistry]: package=package, ) except Exception as e: - raise ConfigError(f"Failed to load macro file: {path}", e) + raise ConfigError(f"Failed to load macro file: {e}", path) self._macros_max_mtime = macros_max_mtime diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index c843213f2a..f35a08a28b 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -41,7 +41,7 @@ def make_python_env( macros: MacroRegistry, variables: t.Optional[t.Dict[str, t.Any]] = None, used_variables: t.Optional[t.Set[str]] = None, - path: t.Optional[str | Path] = None, + path: t.Optional[Path] = None, python_env: t.Optional[t.Dict[str, Executable]] = None, strict_resolution: bool = True, blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None, @@ -265,12 +265,14 @@ def validate_extra_and_required_fields( klass: t.Type[PydanticModel], provided_fields: t.Set[str], entity_name: str, + path: t.Optional[Path] = None, ) -> None: missing_required_fields = klass.missing_required_fields(provided_fields) if missing_required_fields: field_names = "'" + "', '".join(missing_required_fields) + "'" raise_config_error( - f"Please add required field{'s' if len(missing_required_fields) > 1 else ''} {field_names} to the {entity_name}." + f"Please add required field{'s' if len(missing_required_fields) > 1 else ''} {field_names} to the {entity_name}.", + path, ) extra_fields = klass.extra_fields(provided_fields) @@ -293,7 +295,8 @@ def validate_extra_and_required_fields( similar_msg = "\n\n " + "\n ".join(similar) if similar else "" raise_config_error( - f"Invalid field name{'s' if len(extra_fields) > 1 else ''} present in the {entity_name}: {extra_field_names}{similar_msg}" + f"Invalid field name{'s' if len(extra_fields) > 1 else ''} present in the {entity_name}: {extra_field_names}{similar_msg}", + path, ) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 910e6eccc5..f6c83c85f7 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2434,7 +2434,10 @@ def _create_model( **kwargs: t.Any, ) -> Model: validate_extra_and_required_fields( - klass, {"name", *kwargs} - {"grain", "table_properties"}, "MODEL block" + klass, + {"name", *kwargs} - {"grain", "table_properties"}, + "MODEL block", + path, ) for prop in PROPERTIES: diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 0d7ba16c10..30adfce5a2 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from pathlib import Path -import uuid from sqlmesh.core.context import Context import typing as t @@ -35,14 +34,8 @@ class LSPContext: map: t.Dict[Path, t.Union[ModelTarget, AuditTarget]] _render_cache: t.Dict[Path, t.List[RenderModelEntry]] _lint_cache: t.Dict[Path, t.List[AnnotatedRuleViolation]] - _version_id: str - """ - This is a version ID for the context. It is used to track changes to the context. It can be used to - return a version number to the LSP client. - """ def __init__(self, context: Context) -> None: - self._version_id = str(uuid.uuid4()) self.context = context self._render_cache = {} self._lint_cache = {} @@ -70,11 +63,6 @@ def __init__(self, context: Context) -> None: **audit_map, } - @property - def version_id(self) -> str: - """Get the version ID for the context.""" - return self._version_id - def render_model(self, uri: URI) -> t.List[RenderModelEntry]: """Get rendered models for a file, using cache when available. diff --git a/sqlmesh/lsp/errors.py b/sqlmesh/lsp/errors.py new file mode 100644 index 0000000000..a9e778a555 --- /dev/null +++ b/sqlmesh/lsp/errors.py @@ -0,0 +1,51 @@ +from lsprotocol.types import Diagnostic, DiagnosticSeverity, Range, Position + +from sqlmesh.lsp.uri import URI +from sqlmesh.utils.errors import ( + ConfigError, +) +import typing as t + +ContextFailedError = t.Union[str, ConfigError, Exception] + + +def context_error_to_diagnostic( + error: t.Union[Exception, ContextFailedError], + uri_filter: t.Optional[URI] = None, +) -> t.Tuple[t.Optional[t.Tuple[str, Diagnostic]], ContextFailedError]: + """ + Convert an error to a diagnostic message. + If the error is a ConfigError, it will be converted to a diagnostic message. + + uri_filter is used to filter diagnostics by URI. If present, only diagnostics + with a matching URI will be returned. + """ + if isinstance(error, ConfigError): + return config_error_to_diagnostic(error), error + return None, str(error) + + +def config_error_to_diagnostic( + error: ConfigError, + uri_filter: t.Optional[URI] = None, +) -> t.Optional[t.Tuple[str, Diagnostic]]: + if error.location is None: + return None + uri = URI.from_path(error.location).value + if uri_filter and uri != uri_filter.value: + return None + return uri, Diagnostic( + range=Range( + start=Position( + line=0, + character=0, + ), + end=Position( + line=0, + character=0, + ), + ), + message=str(error), + severity=DiagnosticSeverity.Error, + source="SQLMesh", + ) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 0082c4a911..771ebd19c1 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -6,6 +6,7 @@ import typing as t from pathlib import Path import urllib.parse +import uuid from lsprotocol import types from lsprotocol.types import ( @@ -46,6 +47,7 @@ FormatProjectResponse, CustomMethod, ) +from sqlmesh.lsp.errors import ContextFailedError, context_error_to_diagnostic from sqlmesh.lsp.hints import get_hints from sqlmesh.lsp.reference import ( LSPCteReference, @@ -56,17 +58,18 @@ ) from sqlmesh.lsp.rename import prepare_rename, rename_symbol, get_document_highlights from sqlmesh.lsp.uri import URI +from sqlmesh.utils.errors import ConfigError from web.server.api.endpoints.lineage import column_lineage, model_lineage from web.server.api.endpoints.models import get_models from typing import Union -from dataclasses import dataclass +from dataclasses import dataclass, field @dataclass class NoContext: """State when no context has been attempted to load.""" - pass + version_id: str = field(default_factory=lambda: str(uuid.uuid4())) @dataclass @@ -74,14 +77,16 @@ class ContextLoaded: """State when context has been successfully loaded.""" lsp_context: LSPContext + version_id: str = field(default_factory=lambda: str(uuid.uuid4())) @dataclass class ContextFailed: """State when context failed to load with an error message.""" - error_message: str + error: ContextFailedError context: t.Optional[Context] = None + version_id: str = field(default_factory=lambda: str(uuid.uuid4())) ContextState = Union[NoContext, ContextLoaded, ContextFailed] @@ -110,7 +115,7 @@ def __init__( self._supported_custom_methods: t.Dict[ str, t.Callable[ - # mypy unable to recognise the base class + # mypy unable to recognize the base class [LanguageServer, t.Any], t.Any, ], @@ -223,9 +228,8 @@ def _reload_context_and_publish_diagnostics( ) -> None: """Helper method to reload context and publish diagnostics.""" if isinstance(self.context_state, NoContext): - return - - if isinstance(self.context_state, ContextFailed): + pass + elif isinstance(self.context_state, ContextFailed): if self.context_state.context: try: self.context_state.context.load() @@ -235,14 +239,17 @@ def _reload_context_and_publish_diagnostics( ) except Exception as e: ls.log_trace(f"Error loading context: {e}") - if not isinstance(self.context_state, ContextFailed): - raise Exception("Context state should be failed") - self.context_state = ContextFailed( - error_message=str(e), context=self.context_state.context + context = ( + self.context_state.context + if hasattr(self.context_state, "context") + else None ) - return + self.context_state = ContextFailed(error=e, context=context) else: - # If there's no context, try to create one from scratch + # If there's no context, reset to NoContext and try to create one from scratch + ls.log_trace("No partial context available, attempting fresh creation") + self.context_state = NoContext() + self.has_raised_loading_error = False # Reset error flag to show new errors try: self._ensure_context_for_document(uri) # If successful, context_state will be ContextLoaded @@ -253,43 +260,42 @@ def _reload_context_and_publish_diagnostics( ) except Exception as e: ls.log_trace(f"Still cannot load context: {e}") - return - - # Reload the context if it was successfully loaded - try: - context = self.context_state.lsp_context.context - context.load() - # Create new LSPContext which will have fresh, empty caches - self.context_state = ContextLoaded(lsp_context=LSPContext(context)) - except Exception as e: - ls.log_trace(f"Error loading context: {e}") - self.context_state = ContextFailed( - error_message=str(e), context=self.context_state.lsp_context.context - ) - return + # The error will be stored in context_state by _ensure_context_for_document + else: + # Reload the context if it was successfully loaded + try: + context = self.context_state.lsp_context.context + context.load() + # Create new LSPContext which will have fresh, empty caches + self.context_state = ContextLoaded(lsp_context=LSPContext(context)) + except Exception as e: + ls.log_trace(f"Error loading context: {e}") + self.context_state = ContextFailed( + error=e, context=self.context_state.lsp_context.context + ) # Send a workspace diagnostic refresh request to the client. This is used to notify the client that the diagnostics have changed. ls.lsp.send_request( types.WORKSPACE_DIAGNOSTIC_REFRESH, WorkspaceDiagnosticRefreshRequest( - id=self.context_state.lsp_context.version_id, + id=self.context_state.version_id, ), ) - ls.lsp.send_request( types.WORKSPACE_INLAY_HINT_REFRESH, WorkspaceInlayHintRefreshRequest( - id=self.context_state.lsp_context.version_id, + id=self.context_state.version_id, ), ) # Only publish diagnostics if client doesn't support pull diagnostics if not self.client_supports_pull_diagnostics: - diagnostics = self.context_state.lsp_context.lint_model(uri) - ls.publish_diagnostics( - document_uri, - LSPContext.diagnostics_to_lsp_diagnostics(diagnostics), - ) + if hasattr(self.context_state, "lsp_context"): + diagnostics = self.context_state.lsp_context.lint_model(uri) + ls.publish_diagnostics( + document_uri, + LSPContext.diagnostics_to_lsp_diagnostics(diagnostics), + ) def _register_features(self) -> None: """Register LSP features on the internal LanguageServer instance.""" @@ -650,9 +656,21 @@ def workspace_diagnostic( return types.WorkspaceDiagnosticReport(items=items) except Exception as e: - ls.log_trace( - f"Error getting workspace diagnostics: {e}", - ) + ls.log_trace(f"Error getting workspace diagnostics: {e}") + error_diagnostic, error = context_error_to_diagnostic(e) + if error_diagnostic: + uri_value, unpacked_diagnostic = error_diagnostic + return types.WorkspaceDiagnosticReport( + items=[ + types.WorkspaceFullDocumentDiagnosticReport( + kind=types.DocumentDiagnosticReportKind.Full, + result_id=self.context_state.version_id, # No versioning, always fresh + uri=uri_value, + items=[unpacked_diagnostic], + ) + ] + ) + return types.WorkspaceDiagnosticReport(items=[]) @self.server.feature(types.TEXT_DOCUMENT_CODE_ACTION) @@ -759,17 +777,23 @@ def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic] context = self._context_get_or_load(uri) diagnostics = context.lint_model(uri) return LSPContext.diagnostics_to_lsp_diagnostics(diagnostics), 0 - except Exception: + except ConfigError as config_error: + diagnostic, error = context_error_to_diagnostic(config_error, uri_filter=uri) + if diagnostic: + return [diagnostic[1]], 0 return [], 0 def _context_get_or_load(self, document_uri: t.Optional[URI] = None) -> LSPContext: - if isinstance(self.context_state, ContextFailed): - raise RuntimeError(self.context_state.error_message) - if isinstance(self.context_state, NoContext): + state = self.context_state + if isinstance(state, ContextFailed): + if isinstance(state.error, str): + raise Exception(state.error) + raise state.error + if isinstance(state, NoContext): self._ensure_context_for_document(document_uri) - if not isinstance(self.context_state, ContextLoaded): - raise RuntimeError("Context is not loaded") - return self.context_state.lsp_context + if isinstance(state, ContextLoaded): + return state.lsp_context + raise RuntimeError("Context failed to load") def _ensure_context_for_document( self, @@ -866,7 +890,7 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: context = self.context_state.lsp_context.context elif isinstance(self.context_state, ContextFailed) and self.context_state.context: context = self.context_state.context - self.context_state = ContextFailed(error_message=str(e), context=context) + self.context_state = ContextFailed(error=e, context=context) return None @staticmethod diff --git a/sqlmesh/utils/errors.py b/sqlmesh/utils/errors.py index a2156d0438..9974fdce0a 100644 --- a/sqlmesh/utils/errors.py +++ b/sqlmesh/utils/errors.py @@ -24,7 +24,12 @@ class SQLMeshError(Exception): class ConfigError(SQLMeshError): - pass + location: t.Optional[Path] = None + + def __init__(self, message: str | Exception, location: t.Optional[Path] = None) -> None: + super().__init__(message) + if location: + self.location = Path(location) if isinstance(location, str) else location class MissingDependencyError(SQLMeshError): @@ -188,12 +193,12 @@ class SignalEvalError(SQLMeshError): def raise_config_error( msg: str, - location: t.Optional[str | Path] = None, + location: t.Optional[Path] = None, error_type: t.Type[ConfigError] = ConfigError, ) -> None: if location: - raise error_type(f"{msg} at '{location}'") - raise error_type(msg) + raise error_type(f"{msg} at '{location}'", location) + raise error_type(msg, location=location) def raise_for_status(response: Response) -> None: From dd73bb7bbcd525f53ae8e5a9709912472233ea93 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:53:25 -0700 Subject: [PATCH 0559/1056] fix: add `'ducklake` prefix if not present (#4941) --- sqlmesh/core/config/connection.py | 5 +++- tests/core/test_connection_config.py | 42 ++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index f6e12a37f1..202f4c0d71 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -238,7 +238,10 @@ def to_sql(self, alias: str) -> str: options.append("READ_ONLY") # DuckLake specific options + path = self.path if self.type == "ducklake": + if not path.startswith("ducklake:"): + path = f"ducklake:{path}" if self.data_path is not None: options.append(f"DATA_PATH '{self.data_path}'") if self.encrypted: @@ -254,7 +257,7 @@ def to_sql(self, alias: str) -> str: alias_sql = ( f" AS {alias}" if not (self.type == "motherduck" or self.path.startswith("md:")) else "" ) - return f"ATTACH IF NOT EXISTS '{self.path}'{alias_sql}{options_sql}" + return f"ATTACH IF NOT EXISTS '{path}'{alias_sql}{options_sql}" class BaseDuckDBConnectionConfig(ConnectionConfig): diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 6d91c72f2c..9532388ef1 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -742,9 +742,29 @@ def test_duckdb_attach_ducklake_catalog(make_config): assert ducklake_catalog.encrypted is True assert ducklake_catalog.data_inlining_row_limit == 10 # Check that the generated SQL includes DATA_PATH - assert "DATA_PATH '/tmp/ducklake_data'" in ducklake_catalog.to_sql("ducklake") - assert "ENCRYPTED" in ducklake_catalog.to_sql("ducklake") - assert "DATA_INLINING_ROW_LIMIT 10" in ducklake_catalog.to_sql("ducklake") + generated_sql = ducklake_catalog.to_sql("ducklake") + assert "DATA_PATH '/tmp/ducklake_data'" in generated_sql + assert "ENCRYPTED" in generated_sql + assert "DATA_INLINING_ROW_LIMIT 10" in generated_sql + # Check that the ducklake: prefix is automatically added + assert "ATTACH IF NOT EXISTS 'ducklake:catalog.ducklake'" in generated_sql + + # Test that a path with existing ducklake: prefix is preserved + config_with_prefix = make_config( + type="duckdb", + catalogs={ + "ducklake": DuckDBAttachOptions( + type="ducklake", + path="ducklake:catalog.ducklake", + data_path="/tmp/ducklake_data", + ), + }, + ) + ducklake_catalog_with_prefix = config_with_prefix.catalogs.get("ducklake") + generated_sql_with_prefix = ducklake_catalog_with_prefix.to_sql("ducklake") + assert "ATTACH IF NOT EXISTS 'ducklake:catalog.ducklake'" in generated_sql_with_prefix + # Ensure we don't have double prefixes + assert "'ducklake:catalog.ducklake" in generated_sql_with_prefix def test_duckdb_attach_options(): @@ -762,6 +782,22 @@ def test_duckdb_attach_options(): assert options.to_sql(alias="db") == "ATTACH IF NOT EXISTS 'test.db' AS db" +def test_ducklake_attach_add_ducklake_prefix(): + # Test that ducklake: prefix is automatically added when missing + options = DuckDBAttachOptions(type="ducklake", path="catalog.ducklake") + assert ( + options.to_sql(alias="my_ducklake") + == "ATTACH IF NOT EXISTS 'ducklake:catalog.ducklake' AS my_ducklake" + ) + + # Test that ducklake: prefix is preserved when already present + options = DuckDBAttachOptions(type="ducklake", path="ducklake:catalog.ducklake") + assert ( + options.to_sql(alias="my_ducklake") + == "ATTACH IF NOT EXISTS 'ducklake:catalog.ducklake' AS my_ducklake" + ) + + def test_duckdb_config_json_strings(make_config): config = make_config( type="duckdb", From 466968e713b9fef71207c008cadb66ea67894f17 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:09:34 +0300 Subject: [PATCH 0560/1056] Fix: Convert to none nan values for test generation (#4942) --- sqlmesh/core/test/definition.py | 7 ++++ tests/core/test_test.py | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index 8ceb8a4447..762585bd9f 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -151,6 +151,9 @@ def shortDescription(self) -> t.Optional[str]: def setUp(self) -> None: """Load all input tables""" + import pandas as pd + import numpy as np + self.engine_adapter.create_schema(self._qualified_fixture_schema) for name, values in self.body.get("inputs", {}).items(): @@ -201,6 +204,10 @@ def setUp(self) -> None: else: query_or_df = self._create_df(values, columns=columns_to_known_types) + # Convert NaN/NaT values to None if DataFrame + if isinstance(query_or_df, pd.DataFrame): + query_or_df = query_or_df.replace({np.nan: None}) + self.engine_adapter.create_view( self._test_fixture_table(name), query_or_df, columns_to_known_types ) diff --git a/tests/core/test_test.py b/tests/core/test_test.py index fd47ea57b2..de2fb3ea46 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -2753,3 +2753,64 @@ def test_disable_test_logging_if_no_tests_found(mocker: MockerFixture, tmp_path: output = captured_output.stdout assert "test" not in output.lower() + + +def test_test_generation_with_timestamp_nat(tmp_path: Path) -> None: + init_example_project(tmp_path, engine_type="duckdb") + + config = Config( + default_connection=DuckDBConnectionConfig(), + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + ) + foo_sql_file = tmp_path / "models" / "foo.sql" + foo_sql_file.write_text( + "MODEL (name sqlmesh_example.foo); SELECT ts_col FROM sqlmesh_example.bar;" + ) + bar_sql_file = tmp_path / "models" / "bar.sql" + bar_sql_file.write_text("MODEL (name sqlmesh_example.bar); SELECT ts_col FROM external_table;") + + context = Context(paths=tmp_path, config=config) + + # This simulates the scenario where upstream models have NULL timestamp values + input_queries = { + "sqlmesh_example.bar": """ + SELECT ts_col FROM ( + VALUES + (TIMESTAMP '2024-09-20 11:30:00.123456789'), + (CAST(NULL AS TIMESTAMP)), + (TIMESTAMP '2024-09-21 15:45:00.987654321') + ) AS t(ts_col) + """ + } + + # This should not raise an exception even with NULL timestamp values + context.create_test("sqlmesh_example.foo", input_queries=input_queries, overwrite=True) + + test = load_yaml(context.path / c.TESTS / "test_foo.yaml") + assert len(test) == 1 + assert "test_foo" in test + + # Verify that the test was created with correct input and output data + inputs = test["test_foo"]["inputs"] + outputs = test["test_foo"]["outputs"] + + # Check that we have the expected input table + assert '"memory"."sqlmesh_example"."bar"' in inputs + bar_data = inputs['"memory"."sqlmesh_example"."bar"'] + + # Verify we have 3 rows (2 with timestamps, 1 with NULL) + assert len(bar_data) == 3 + + # Verify that non-NULL timestamps are preserved + assert bar_data[0]["ts_col"] == datetime.datetime(2024, 9, 20, 11, 30, 0, 123456) + assert bar_data[2]["ts_col"] == datetime.datetime(2024, 9, 21, 15, 45, 0, 987654) + + # Verify that NULL timestamp is represented as None (not NaT) + assert bar_data[1]["ts_col"] is None + + # Verify that the output matches the input (since the model just selects from bar) + query_output = outputs["query"] + assert len(query_output) == 3 + assert query_output[0]["ts_col"] == datetime.datetime(2024, 9, 20, 11, 30, 0, 123456) + assert query_output[1]["ts_col"] is None + assert query_output[2]["ts_col"] == datetime.datetime(2024, 9, 21, 15, 45, 0, 987654) From 84214883e1844ef358cca5b67782961fbe027419 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:22:15 +0100 Subject: [PATCH 0561/1056] feat(vscode): diagnostic for bad config.yaml error (#4945) --- pnpm-lock.yaml | 3 + sqlmesh/core/config/loader.py | 7 +- sqlmesh/lsp/main.py | 12 ++-- vscode/extension/package.json | 3 +- vscode/extension/tests/broken_project.spec.ts | 66 ++++++++++++++++++- 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ab18b8d6a..d18077d1d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: vitest: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + yaml: + specifier: ^2.8.0 + version: 2.8.0 vscode/react: dependencies: diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index f40646131d..1618d29ad2 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -91,6 +91,7 @@ def load_config_from_paths( "SQLMesh project config could not be found. Point the cli to the project path with `sqlmesh -p`. If you haven't set up the SQLMesh project, run `sqlmesh init`." ) + yaml_config_path: t.Optional[Path] = None for path in [*project_paths, *personal_paths]: if not path.exists(): continue @@ -107,8 +108,9 @@ def load_config_from_paths( if extension in ("yml", "yaml"): if config_name != "config" and not python_config: raise ConfigError( - "YAML configs do not support multiple configs. Use Python instead." + "YAML configs do not support multiple configs. Use Python instead.", ) + yaml_config_path = path.resolve() non_python_configs.append(load_config_from_yaml(path)) elif extension == "py": try: @@ -149,7 +151,8 @@ def load_config_from_paths( except ValidationError as e: raise ConfigError( validation_error_message(e, "Invalid project config:") - + "\n\nVerify your config.yaml and environment variables." + + "\n\nVerify your config.yaml and environment variables.", + location=yaml_config_path, ) no_dialect_err_msg = "Default model SQL dialect is a required configuration parameter. Set it in the `model_defaults` `dialect` key in your config file." diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 771ebd19c1..c257ccfaa1 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -767,7 +767,7 @@ def completion( get_sql_completions(None, URI(params.text_document.uri)) return None - def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic], int]: + def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic], str]: """Get diagnostics for a specific URI, returning (diagnostics, result_id). Since we no longer track version numbers, we always return 0 as the result_id. @@ -776,12 +776,16 @@ def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic] try: context = self._context_get_or_load(uri) diagnostics = context.lint_model(uri) - return LSPContext.diagnostics_to_lsp_diagnostics(diagnostics), 0 + return LSPContext.diagnostics_to_lsp_diagnostics( + diagnostics + ), self.context_state.version_id except ConfigError as config_error: diagnostic, error = context_error_to_diagnostic(config_error, uri_filter=uri) if diagnostic: - return [diagnostic[1]], 0 - return [], 0 + location, diag = diagnostic + if location == uri.value: + return [diag], self.context_state.version_id + return [], self.context_state.version_id def _context_get_or_load(self, document_uri: t.Optional[URI] = None) -> LSPContext: state = self.context_state diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 4aa397cef5..9628ef662e 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -155,6 +155,7 @@ "ts-loader": "^9.5.2", "typescript": "^5.8.3", "typescript-eslint": "^8.35.1", - "vitest": "^3.2.4" + "vitest": "^3.2.4", + "yaml": "^2.8.0" } } diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index bc6f4f7ed1..744197e3bb 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -2,8 +2,15 @@ import { test, expect } from './fixtures' import fs from 'fs-extra' import os from 'os' import path from 'path' -import { openLineageView, saveFile, SUSHI_SOURCE_PATH } from './utils' +import { + openLineageView, + runCommand, + saveFile, + SUSHI_SOURCE_PATH, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' +import { execAsync } from '../src/utilities/exec' +import yaml from 'yaml' test('bad project, double model', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp( @@ -253,3 +260,60 @@ test('bad project, double model, check lineage', async ({ await page.waitForTimeout(500) }) + +const setup = async (tempDir: string) => { + // Run the sqlmesh CLI from the root of the repo using the local path + const sqlmeshCliPath = path.resolve(__dirname, '../../../.venv/bin/sqlmesh') + const result = await execAsync(sqlmeshCliPath, ['init', 'duckdb'], { + cwd: tempDir, + }) + expect(result.exitCode).toBe(0) +} + +test.describe('Bad config.py/config.yaml file issues', () => { + test('sqlmesh init, then corrupted config.yaml, bad parameters', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await setup(tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configYamlPath = path.join(tempDir, 'config.yaml') + // Write an invalid YAML to config.yaml + const config = { + gateway: 'test', + } + // Write config to the yaml file + await fs.writeFile(configYamlPath, yaml.stringify(config)) + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open full_model.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'full_model.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Asser that the error is present in the problems view + await page + .getByText('Invalid project config:', { exact: true }) + .first() + .isVisible({ timeout: 5_000 }) + }) +}) From 0d6193d05db692d0901cabb68cdd7163c19c1665 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:53:31 +0100 Subject: [PATCH 0562/1056] feat(vscode): error well on bad config.yaml (#4946) --- sqlmesh/core/config/loader.py | 9 +++- vscode/extension/tests/broken_project.spec.ts | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index 1618d29ad2..5485b0f623 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -169,7 +169,14 @@ def load_config_from_paths( def load_config_from_yaml(path: Path) -> t.Dict[str, t.Any]: - return yaml_load(path) + content = yaml_load(path) + if not isinstance(content, dict): + raise ConfigError( + f"Invalid YAML configuration: expected a dictionary but got {type(content).__name__}. " + f"Please check the YAML syntax in your config file.", + location=path, + ) + return content def load_config_from_python_module( diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index 744197e3bb..605a622787 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -271,6 +271,48 @@ const setup = async (tempDir: string) => { } test.describe('Bad config.py/config.yaml file issues', () => { + test('sqlmesh init, then corrupted config.yaml, bad yaml', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await setup(tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configYamlPath = path.join(tempDir, 'config.yaml') + // Write an invalid YAML to config.yaml + await fs.writeFile(configYamlPath, 'invalid_yaml; asdfasudfy') + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open full_model.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'full_model.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Asser that the error is present in the problems view + await page + .getByText('Invalid YAML configuration:') + .first() + .isVisible({ timeout: 5_000 }) + }) + test('sqlmesh init, then corrupted config.yaml, bad parameters', async ({ page, sharedCodeServer, From 461b2a2562198239caf106122b354368b735dc0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Thal=C3=A9n?= Date: Thu, 10 Jul 2025 00:10:17 +0200 Subject: [PATCH 0563/1056] fix!: Add DATETIMEOFFSET handling in pyodbc for MSSQL (#4930) --- pyproject.toml | 6 +- sqlmesh/core/config/connection.py | 27 +++++- tests/core/test_connection_config.py | 126 +++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d451532eb9..204a1c7f3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ classifiers = [ [project.optional-dependencies] athena = ["PyAthena[Pandas]"] azuresql = ["pymssql"] -azuresql-odbc = ["pyodbc"] +azuresql-odbc = ["pyodbc>=5.0.0"] bigquery = [ "google-cloud-bigquery[pandas]", "google-cloud-bigquery-storage" @@ -78,7 +78,7 @@ dev = [ "pydantic", "PyAthena[Pandas]", "PyGithub>=2.6.0", - "pyodbc", + "pyodbc>=5.0.0", "pyperf", "pyspark~=3.5.0", "pytest", @@ -108,7 +108,7 @@ github = ["PyGithub~=2.5.0"] llm = ["langchain", "openai"] motherduck = ["duckdb>=1.2.0"] mssql = ["pymssql"] -mssql-odbc = ["pyodbc"] +mssql-odbc = ["pyodbc>=5.0.0"] mysql = ["pymysql"] mwaa = ["boto3"] postgres = ["psycopg2"] diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 202f4c0d71..47b64b5fc4 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -1627,7 +1627,32 @@ def connect(**kwargs: t.Any) -> t.Callable: # Create the connection string conn_str = ";".join(conn_str_parts) - return pyodbc.connect(conn_str, autocommit=kwargs.get("autocommit", False)) + conn = pyodbc.connect(conn_str, autocommit=kwargs.get("autocommit", False)) + + # Set up output converters for MSSQL-specific data types + # Handle SQL type -155 (DATETIMEOFFSET) which is not yet supported by pyodbc + # ref: https://github.com/mkleehammer/pyodbc/issues/134#issuecomment-281739794 + def handle_datetimeoffset(dto_value: t.Any) -> t.Any: + from datetime import datetime, timedelta, timezone + import struct + + # Unpack the DATETIMEOFFSET binary format: + # Format: <6hI2h = (year, month, day, hour, minute, second, nanoseconds, tz_hour_offset, tz_minute_offset) + tup = struct.unpack("<6hI2h", dto_value) + return datetime( + tup[0], + tup[1], + tup[2], + tup[3], + tup[4], + tup[5], + tup[6] // 1000, + timezone(timedelta(hours=tup[7], minutes=tup[8])), + ) + + conn.add_output_converter(-155, handle_datetimeoffset) + + return conn return connect diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 9532388ef1..02ec5271a4 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -1557,3 +1557,129 @@ def test_mssql_pymssql_connection_factory(): # Clean up the mock module if "pymssql" in sys.modules: del sys.modules["pymssql"] + + +def test_mssql_pyodbc_connection_datetimeoffset_handling(): + """Test that the MSSQL pyodbc connection properly handles DATETIMEOFFSET conversion.""" + from datetime import datetime, timezone, timedelta + import struct + from unittest.mock import Mock, patch + + with patch("pyodbc.connect") as mock_pyodbc_connect: + # Track calls to add_output_converter + converter_calls = [] + + def mock_add_output_converter(sql_type, converter_func): + converter_calls.append((sql_type, converter_func)) + + # Create a mock connection that will be returned by pyodbc.connect + mock_connection = Mock() + mock_connection.add_output_converter = mock_add_output_converter + mock_pyodbc_connect.return_value = mock_connection + + config = MSSQLConnectionConfig( + host="localhost", + driver="pyodbc", # DATETIMEOFFSET handling is pyodbc-specific + check_import=False, + ) + + # Get the connection factory and call it + factory_with_kwargs = config._connection_factory_with_kwargs + connection = factory_with_kwargs() + + # Verify that add_output_converter was called for SQL type -155 (DATETIMEOFFSET) + assert len(converter_calls) == 1 + sql_type, converter_func = converter_calls[0] + assert sql_type == -155 + + # Test the converter function with actual DATETIMEOFFSET binary data + # Create a test DATETIMEOFFSET value: 2023-12-25 15:30:45.123456789 +05:30 + year, month, day = 2023, 12, 25 + hour, minute, second = 15, 30, 45 + nanoseconds = 123456789 + tz_hour_offset, tz_minute_offset = 5, 30 + + # Pack the binary data according to the DATETIMEOFFSET format + binary_data = struct.pack( + "<6hI2h", + year, + month, + day, + hour, + minute, + second, + nanoseconds, + tz_hour_offset, + tz_minute_offset, + ) + + # Convert using the registered converter + result = converter_func(binary_data) + + # Verify the result + expected_dt = datetime( + 2023, + 12, + 25, + 15, + 30, + 45, + 123456, # microseconds = nanoseconds // 1000 + timezone(timedelta(hours=5, minutes=30)), + ) + assert result == expected_dt + assert result.tzinfo == timezone(timedelta(hours=5, minutes=30)) + + +def test_mssql_pyodbc_connection_negative_timezone_offset(): + """Test DATETIMEOFFSET handling with negative timezone offset at connection level.""" + from datetime import datetime, timezone, timedelta + import struct + from unittest.mock import Mock, patch + + with patch("pyodbc.connect") as mock_pyodbc_connect: + converter_calls = [] + + def mock_add_output_converter(sql_type, converter_func): + converter_calls.append((sql_type, converter_func)) + + mock_connection = Mock() + mock_connection.add_output_converter = mock_add_output_converter + mock_pyodbc_connect.return_value = mock_connection + + config = MSSQLConnectionConfig( + host="localhost", + driver="pyodbc", # DATETIMEOFFSET handling is pyodbc-specific + check_import=False, + ) + + factory_with_kwargs = config._connection_factory_with_kwargs + connection = factory_with_kwargs() + + # Get the converter function + _, converter_func = converter_calls[0] + + # Test with negative timezone offset: 2023-01-01 12:00:00.0 -08:00 + year, month, day = 2023, 1, 1 + hour, minute, second = 12, 0, 0 + nanoseconds = 0 + tz_hour_offset, tz_minute_offset = -8, 0 + + binary_data = struct.pack( + "<6hI2h", + year, + month, + day, + hour, + minute, + second, + nanoseconds, + tz_hour_offset, + tz_minute_offset, + ) + + result = converter_func(binary_data) + + expected_dt = datetime(2023, 1, 1, 12, 0, 0, 0, timezone(timedelta(hours=-8, minutes=0))) + assert result == expected_dt + assert result.tzinfo == timezone(timedelta(hours=-8)) From 7441d636fb476caefa76b7134454a3d1c46f992e Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 10 Jul 2025 09:19:55 +0100 Subject: [PATCH 0564/1056] feat(vscode): handle correct config.py with bad config (#4948) --- sqlmesh/core/config/loader.py | 3 +- vscode/extension/tests/broken_project.spec.ts | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index 5485b0f623..7f1789a677 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -194,7 +194,8 @@ def load_config_from_python_module( if config_obj is None or not isinstance(config_obj, Config): raise ConfigError( - f"Config needs to be a valid object of type sqlmesh.core.config.Config. Found `{config_obj}` instead at '{module_path}'." + f"Config needs to be a valid object of type sqlmesh.core.config.Config. Found `{config_obj}` instead at '{module_path}'.", + module_path, ) return ( diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index 605a622787..f2af3e13e0 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -358,4 +358,46 @@ test.describe('Bad config.py/config.yaml file issues', () => { .first() .isVisible({ timeout: 5_000 }) }) + + test('sushi example, correct python, bad config', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configPyPath = path.join(tempDir, 'config.py') + // Write an invalid Python to config.py + await fs.writeFile(configPyPath, 'config = {}') + + await page.goto( + `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open customers.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Expect the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Assert that the error is present in the problems view + await page + .getByText('Config needs to be a valid object of type') + .first() + .isVisible({ timeout: 5_000 }) + }) }) From f0b87bacafc8ff4f5455cdc342c3c4f685019c78 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 10 Jul 2025 19:03:55 +0300 Subject: [PATCH 0565/1056] Chore: Update and clarify blueprints doc example macro syntax (#4951) --- docs/concepts/macros/sqlmesh_macros.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/concepts/macros/sqlmesh_macros.md b/docs/concepts/macros/sqlmesh_macros.md index f1cbb6ebf0..f28e77e203 100644 --- a/docs/concepts/macros/sqlmesh_macros.md +++ b/docs/concepts/macros/sqlmesh_macros.md @@ -216,18 +216,35 @@ MODEL ( name @customer.some_table, kind FULL, blueprints ( - (customer := customer1, field_a := x, field_b := y), - (customer := customer2, field_a := z, field_b := w) + (customer := customer1, field_a := x, field_b := y, field_c := 'foo'), + (customer := customer2, field_a := z, field_b := w, field_c := 'bar') ) ); SELECT @field_a, - @{field_b} AS field_b + @{field_b} AS field_b, + @field_c AS @{field_c} FROM @customer.some_source + +/* +When rendered for customer1.some_table: +SELECT + x, + y AS field_b, + 'foo' AS foo +FROM customer1.some_source + +When rendered for customer2.some_table: +SELECT + z, + w AS field_b, + 'bar' AS bar +FROM customer2.some_source +*/ ``` -Note the use of both regular `@field_a` and curly brace syntax `@{field_b}` macro variable references in the model query. Learn more [above](#embedding-variables-in-strings) +Note the use of both regular `@field_a` and curly brace syntax `@{field_b}` macro variable references in the model query. Both of these will be rendered as identifiers. In the case of `field_c`, which in the blueprints is a string, it would be rendered as a string literal when used with the regular macro syntax `@field_c` and if we want to use the string as an identifier then we use the curly braces `@{field_c}`. Learn more [above](#embedding-variables-in-strings) Blueprint variables can be accessed using the syntax shown above, or through the `@BLUEPRINT_VAR()` macro function, which also supports specifying default values in case the variable is undefined (similar to `@VAR()`). From 8be77dcc397483edd542b5ea00479a065936ec68 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 11 Jul 2025 08:48:00 +1200 Subject: [PATCH 0566/1056] Chore(cicd_bot): Show logged warnings/errors in the check output (#4938) --- sqlmesh/core/console.py | 89 ++++++-- sqlmesh/integrations/github/cicd/command.py | 5 +- .../integrations/github/cicd/controller.py | 215 ++++++++++-------- tests/core/test_console.py | 131 +++++++++++ .../github/cicd/test_github_controller.py | 53 +++++ .../github/cicd/test_integration.py | 2 +- 6 files changed, 373 insertions(+), 122 deletions(-) create mode 100644 tests/core/test_console.py diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 2f51a20a74..86de61c829 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -3049,34 +3049,47 @@ class CaptureTerminalConsole(TerminalConsole): def __init__(self, console: t.Optional[RichConsole] = None, **kwargs: t.Any) -> None: super().__init__(console=console, **kwargs) self._captured_outputs: t.List[str] = [] + self._warnings: t.List[str] = [] self._errors: t.List[str] = [] @property def captured_output(self) -> str: return "".join(self._captured_outputs) + @property + def captured_warnings(self) -> str: + return "".join(self._warnings) + @property def captured_errors(self) -> str: return "".join(self._errors) def consume_captured_output(self) -> str: - output = self.captured_output - self.clear_captured_outputs() - return output + try: + return self.captured_output + finally: + self._captured_outputs = [] - def consume_captured_errors(self) -> str: - errors = self.captured_errors - self.clear_captured_errors() - return errors + def consume_captured_warnings(self) -> str: + try: + return self.captured_warnings + finally: + self._warnings = [] - def clear_captured_outputs(self) -> None: - self._captured_outputs = [] + def consume_captured_errors(self) -> str: + try: + return self.captured_errors + finally: + self._errors = [] - def clear_captured_errors(self) -> None: - self._errors = [] + def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None: + if short_message not in self._warnings: + self._warnings.append(short_message) + super().log_warning(short_message, long_message) def log_error(self, message: str) -> None: - self._errors.append(message) + if message not in self._errors: + self._errors.append(message) super().log_error(message) def log_skipped_models(self, snapshot_names: t.Set[str]) -> None: @@ -3087,9 +3100,8 @@ def log_skipped_models(self, snapshot_names: t.Set[str]) -> None: super().log_skipped_models(snapshot_names) def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None: - if errors: - self._errors.append("\n".join(str(ex) for ex in errors)) - super().log_failed_models(errors) + self._errors.extend([str(ex) for ex in errors if str(ex) not in self._errors]) + super().log_failed_models(errors) def _print(self, value: t.Any, **kwargs: t.Any) -> None: with self.console.capture() as capture: @@ -3110,6 +3122,11 @@ class MarkdownConsole(CaptureTerminalConsole): AUDIT_PADDING = 7 def __init__(self, **kwargs: t.Any) -> None: + self.alert_block_max_content_length = int(kwargs.pop("alert_block_max_content_length", 500)) + self.alert_block_collapsible_threshold = int( + kwargs.pop("alert_block_collapsible_threshold", 200) + ) + super().__init__( **{**kwargs, "console": RichConsole(no_color=True, width=kwargs.pop("width", None))} ) @@ -3434,18 +3451,40 @@ def show_linter_violations( self._print(msg) self._errors.append(msg) - def log_error(self, message: str) -> None: - super().log_error(f"```\n\\[ERROR] {message}```\n\n") + @property + def captured_warnings(self) -> str: + return self._render_alert_block("WARNING", self._warnings) - def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None: - logger.warning(long_message or short_message) + @property + def captured_errors(self) -> str: + return self._render_alert_block("CAUTION", self._errors) - if not short_message.endswith("\n"): - short_message += ( - "\n" # so that the closing ``` ends up on a newline which is important for GitHub - ) + def _render_alert_block(self, block_type: str, items: t.List[str]) -> str: + # GitHub Markdown alert syntax, https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts + if items: + item_contents = "" + list_indicator = "- " if len(items) > 1 else "" + + for item in items: + item = item.replace("\n", "\n> ") + item_contents += f">\n> {list_indicator}{item}\n" + + if len(item_contents) > self.alert_block_max_content_length: + truncation_msg = ( + "...\n>\n> Truncated. Please check the console for full information.\n" + ) + item_contents = item_contents[ + 0 : self.alert_block_max_content_length - len(truncation_msg) + ] + item_contents += truncation_msg + break + + if len(item_contents) > self.alert_block_collapsible_threshold: + item_contents = f">
    \n{item_contents}>
    " + + return f"> [!{block_type}]\n{item_contents}\n" - self._print(f"```\n\\[WARNING] {short_message}```\n\n") + return "" def _print(self, value: t.Any, **kwargs: t.Any) -> None: self.console.print(value, **kwargs) @@ -3472,7 +3511,7 @@ def _print(self, value: t.Any, **kwargs: t.Any) -> None: super()._print(value, **kwargs) for captured_output in self._captured_outputs: print(captured_output) - self.clear_captured_outputs() + self.consume_captured_output() def _prompt(self, message: str, **kwargs: t.Any) -> t.Any: self._print(message) diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index 04f5757b50..1a4982e9e1 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -119,10 +119,7 @@ def _update_pr_environment(controller: GithubController) -> bool: except Exception as e: logger.exception("Error occurred when updating PR environment") conclusion = controller.update_pr_environment_check( - status=GithubCheckStatus.COMPLETED, - exception=e, - plan=controller.pr_plan_or_none, - plan_flags=controller.pr_plan_flags, + status=GithubCheckStatus.COMPLETED, exception=e ) return ( conclusion is not None diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 19c494a979..7b03868243 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -494,7 +494,7 @@ def get_plan_summary(self, plan: Plan) -> str: try: # Clear out any output that might exist from prior steps - self._console.clear_captured_outputs() + self._console.consume_captured_output() if plan.restatements: self._console._print("\n**Restating models**\n") else: @@ -522,13 +522,115 @@ def get_plan_summary(self, plan: Plan) -> str: if not difference_summary and not missing_dates: return f"No changes to apply.{plan_flags_section}" - return f"{difference_summary}\n{missing_dates}{plan_flags_section}" + warnings_block = self._console.consume_captured_warnings() + errors_block = self._console.consume_captured_errors() + + return f"{warnings_block}{errors_block}{difference_summary}\n{missing_dates}{plan_flags_section}" except PlanError as e: logger.exception("Plan failed to generate") return f"Plan failed to generate. Check for pending or unresolved changes. Error: {e}" finally: self._console.verbosity = orig_verbosity + def get_pr_environment_summary( + self, conclusion: GithubCheckConclusion, exception: t.Optional[Exception] = None + ) -> str: + heading = "" + summary = "" + + if conclusion.is_success: + summary = self._get_pr_environment_summary_success() + elif conclusion.is_action_required: + heading = f":warning: Action Required to create or update PR Environment `{self.pr_environment_name}` :warning:" + summary = self._get_pr_environment_summary_action_required(exception) + elif conclusion.is_failure: + heading = ( + f":x: Failed to create or update PR Environment `{self.pr_environment_name}` :x:" + ) + summary = self._get_pr_environment_summary_failure(exception) + elif conclusion.is_skipped: + heading = f":next_track_button: Skipped creating or updating PR Environment `{self.pr_environment_name}` :next_track_button:" + summary = self._get_pr_environment_summary_skipped(exception) + else: + heading = f":interrobang: Got an unexpected conclusion: {conclusion.value}" + + # note: we just add warnings here, errors will be covered by the "failure" conclusion + if warnings := self._console.consume_captured_warnings(): + summary = f"{warnings}\n{summary}" + + return f"{heading}\n\n{summary}".strip() + + def _get_pr_environment_summary_success(self) -> str: + prod_plan = self.prod_plan_with_gaps + + if not prod_plan.has_changes: + summary = "No models were modified in this PR.\n" + else: + intro = self._generate_pr_environment_summary_intro() + summary = intro + self._generate_pr_environment_summary_list(prod_plan) + + if prod_plan.user_provided_flags: + summary += self._generate_plan_flags_section(prod_plan.user_provided_flags) + + return summary + + def _get_pr_environment_summary_skipped(self, exception: t.Optional[Exception] = None) -> str: + if isinstance(exception, NoChangesPlanError): + skip_reason = "No changes were detected compared to the prod environment." + elif isinstance(exception, TestFailure): + skip_reason = "Unit Test(s) Failed so skipping PR creation" + else: + skip_reason = "A prior stage failed resulting in skipping PR creation." + + return skip_reason + + def _get_pr_environment_summary_action_required( + self, exception: t.Optional[Exception] = None + ) -> str: + plan = self.pr_plan_or_none + if isinstance(exception, UncategorizedPlanError) and plan: + failure_msg = f"The following models could not be categorized automatically:\n" + for snapshot in plan.uncategorized: + failure_msg += f"- {snapshot.name}\n" + failure_msg += ( + f"\nRun `sqlmesh plan {self.pr_environment_name}` locally to apply these changes.\n\n" + "If you would like the bot to automatically categorize changes, check the [documentation](https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information." + ) + else: + failure_msg = "Please check the Actions Workflow logs for more information." + + return failure_msg + + def _get_pr_environment_summary_failure(self, exception: t.Optional[Exception] = None) -> str: + console_output = self._console.consume_captured_output() + + if isinstance(exception, PlanError): + failure_msg = f"Plan application failed.\n" + if exception.args and (msg := exception.args[0]) and isinstance(msg, str): + failure_msg += f"\n{msg}\n" + if console_output: + failure_msg += f"\n{console_output}" + elif isinstance(exception, (SQLMeshError, SqlglotError, ValueError)): + # this logic is taken from the global error handler attached to the CLI, which uses `click.echo()` to output the message + # so cant be re-used here because it bypasses the Console + failure_msg = f"**Error:** {str(exception)}" + elif exception: + logger.debug( + "Got unexpected error. Error Type: " + + str(type(exception)) + + " Stack trace: " + + traceback.format_exc() + ) + failure_msg = f"This is an unexpected error.\n\n**Exception:**\n```\n{traceback.format_exc()}\n```" + + if captured_errors := self._console.consume_captured_errors(): + failure_msg = f"{captured_errors}\n{failure_msg}" + + if plan_flags := self.pr_plan_flags: + failure_msg += f"\n\n{self._generate_plan_flags_section(plan_flags)}" + + return failure_msg + def run_tests(self) -> t.Tuple[ModelTextTestResult, str]: """ Run tests for the PR @@ -539,7 +641,7 @@ def run_linter(self) -> None: """ Run linter for the PR """ - self._console.clear_captured_outputs() + self._console.consume_captured_output() self._context.lint_models() def _get_or_create_comment(self, header: str = BOT_HEADER_MSG) -> IssueComment: @@ -611,7 +713,22 @@ def update_pr_environment(self) -> None: Creates a PR environment from the logic present in the PR. If the PR contains changes that are uncategorized, then an error will be raised. """ - self._context.apply(self.pr_plan) + self._context.apply(self.pr_plan) # will raise if PR environment creation fails + + # update PR info comment + vde_title = "- :eyes: To **review** this PR's changes, use virtual data environment:" + comment_value = f"{vde_title}\n - `{self.pr_environment_name}`" + if self.bot_config.enable_deploy_command: + comment_value += ( + "\n- :arrow_forward: To **apply** this PR's plan to prod, comment:\n - `/deploy`" + ) + dedup_regex = vde_title.replace("*", r"\*") + r".*" + updated_comment, _ = self.update_sqlmesh_comment_info( + value=comment_value, + dedup_regex=dedup_regex, + ) + if updated_comment: + self._append_output("created_pr_environment", "true") def deploy_to_prod(self) -> None: """ @@ -855,13 +972,7 @@ def conclusion_handler( ) def update_pr_environment_check( - self, - status: GithubCheckStatus, - exception: t.Optional[Exception] = None, - plan: t.Optional[Plan] = None, - plan_flags: t.Optional[ - t.Dict[str, UserProvidedFlags] - ] = None, # note: the plan flags are passed separately in case the plan fails to build + self, status: GithubCheckStatus, exception: t.Optional[Exception] = None ) -> t.Optional[GithubCheckConclusion]: """ Updates the status of the merge commit for the PR environment. @@ -882,87 +993,7 @@ def update_pr_environment_check( def conclusion_handler( conclusion: GithubCheckConclusion, exception: t.Optional[Exception] ) -> t.Tuple[GithubCheckConclusion, str, t.Optional[str]]: - if conclusion.is_success: - prod_plan = self.prod_plan_with_gaps - - if not prod_plan.has_changes: - summary = "No models were modified in this PR.\n" - else: - intro = self._generate_pr_environment_summary_intro() - summary = intro + self._generate_pr_environment_summary_list(prod_plan) - if prod_plan.user_provided_flags: - summary += self._generate_plan_flags_section(prod_plan.user_provided_flags) - - vde_title = ( - "- :eyes: To **review** this PR's changes, use virtual data environment:" - ) - comment_value = f"{vde_title}\n - `{self.pr_environment_name}`" - if self.bot_config.enable_deploy_command: - comment_value += "\n- :arrow_forward: To **apply** this PR's plan to prod, comment:\n - `/deploy`" - dedup_regex = vde_title.replace("*", r"\*") + r".*" - updated_comment, _ = self.update_sqlmesh_comment_info( - value=comment_value, - dedup_regex=dedup_regex, - ) - if updated_comment: - self._append_output("created_pr_environment", "true") - else: - if isinstance(exception, NoChangesPlanError): - skip_reason = "No changes were detected compared to the prod environment." - elif isinstance(exception, TestFailure): - skip_reason = "Unit Test(s) Failed so skipping PR creation" - else: - skip_reason = "A prior stage failed resulting in skipping PR creation." - - if not skip_reason and exception: - logger.debug( - f"Got {type(exception).__name__}. Stack trace: " + traceback.format_exc() - ) - - captured_errors = self._console.consume_captured_errors() - if captured_errors: - logger.debug(f"Captured errors: {captured_errors}") - failure_msg = f"**Errors:**\n{captured_errors}\n" - elif isinstance(exception, UncategorizedPlanError) and plan: - failure_msg = f"The following models could not be categorized automatically:\n" - for snapshot in plan.uncategorized: - failure_msg += f"- {snapshot.name}\n" - failure_msg += ( - f"\nRun `sqlmesh plan {self.pr_environment_name}` locally to apply these changes.\n\n" - "If you would like the bot to automatically categorize changes, check the [documentation](https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information." - ) - elif isinstance(exception, PlanError): - failure_msg = f"Plan application failed.\n" - if exception.args and (msg := exception.args[0]) and isinstance(msg, str): - failure_msg += f"\n{msg}\n" - if self._console.captured_output: - failure_msg += f"\n{self._console.captured_output}" - elif isinstance(exception, (SQLMeshError, SqlglotError, ValueError)): - # this logic is taken from the global error handler attached to the CLI, which uses `click.echo()` to output the message - # so cant be re-used here because it bypasses the Console - failure_msg = f"**Error:** {str(exception)}" - else: - logger.debug( - "Got unexpected error. Error Type: " - + str(type(exception)) - + " Stack trace: " - + traceback.format_exc() - ) - failure_msg = f"This is an unexpected error.\n\n**Exception:**\n```\n{traceback.format_exc()}\n```" - - conclusion_to_summary = { - GithubCheckConclusion.SKIPPED: f":next_track_button: Skipped creating or updating PR Environment `{self.pr_environment_name}`. {skip_reason}", - GithubCheckConclusion.FAILURE: f":x: Failed to create or update PR Environment `{self.pr_environment_name}` :x:\n\n{failure_msg}", - GithubCheckConclusion.CANCELLED: f":stop_sign: Cancelled creating or updating PR Environment `{self.pr_environment_name}`", - GithubCheckConclusion.ACTION_REQUIRED: f":warning: Action Required to create or update PR Environment `{self.pr_environment_name}` :warning:\n\n{failure_msg}", - } - summary = conclusion_to_summary.get( - conclusion, f":interrobang: Got an unexpected conclusion: {conclusion.value}" - ) - if plan_flags: - plan_flags_section = self._generate_plan_flags_section(plan_flags) - summary += f"\n\n{plan_flags_section}" - + summary = self.get_pr_environment_summary(conclusion, exception) self._append_output("pr_environment_name", self.pr_environment_name) return conclusion, check_title, summary diff --git a/tests/core/test_console.py b/tests/core/test_console.py new file mode 100644 index 0000000000..f899713235 --- /dev/null +++ b/tests/core/test_console.py @@ -0,0 +1,131 @@ +from sqlmesh.core.console import MarkdownConsole + + +def test_markdown_console_warning_block(): + console = MarkdownConsole( + alert_block_max_content_length=100, alert_block_collapsible_threshold=45 + ) + assert console.consume_captured_warnings() == "" + + # single warning, within threshold + console.log_warning("First warning") + assert console.consume_captured_warnings() == "> [!WARNING]\n>\n> First warning\n\n" + + # multiple warnings, within threshold (list syntax) + console.log_warning("First warning") + console.log_warning("Second warning") + assert ( + console.consume_captured_warnings() + == "> [!WARNING]\n>\n> - First warning\n>\n> - Second warning\n\n" + ) + + # single warning, within max threshold but over collapsible section threshold + warning = "The snowflake engine is not recommended for storing SQLMesh state in production deployments" + assert len(warning) > console.alert_block_collapsible_threshold + assert len(warning) < console.alert_block_max_content_length + console.log_warning(warning) + assert ( + console.consume_captured_warnings() + == "> [!WARNING]\n>
    \n>\n> The snowflake engine is not recommended for storing SQLMesh state in production deployments\n>
    \n" + ) + + # single warning, over max threshold + warning = "The snowflake engine is not recommended for storing SQLMesh state in production deployments. Please see for a list of recommended engines and more information." + assert len(warning) > console.alert_block_collapsible_threshold + assert len(warning) > console.alert_block_max_content_length + console.log_warning(warning) + assert ( + console.consume_captured_warnings() + == "> [!WARNING]\n>
    \n>\n> The snowflake engine is not re...\n>\n> Truncated. Please check the console for full information.\n>
    \n" + ) + + # multiple warnings, within max threshold but over collapsible section threshold + warning_1 = "This is the first warning" + warning_2 = "This is the second warning" + assert (len(warning_1) + len(warning_2)) > console.alert_block_collapsible_threshold + assert (len(warning_1) + len(warning_2)) < console.alert_block_max_content_length + console.log_warning(warning_1) + console.log_warning(warning_2) + assert ( + console.consume_captured_warnings() + == "> [!WARNING]\n>
    \n>\n> - This is the first warning\n>\n> - This is the second warning\n>
    \n" + ) + + # multiple warnings, over max threshold + warning_1 = "This is the first warning and its really really long" + warning_2 = "This is the second warning and its also really really long" + assert (len(warning_1) + len(warning_2)) > console.alert_block_collapsible_threshold + assert (len(warning_1) + len(warning_2)) > console.alert_block_max_content_length + console.log_warning(warning_1) + console.log_warning(warning_2) + assert ( + console.consume_captured_warnings() + == "> [!WARNING]\n>
    \n>\n> - This is the first warning an...\n>\n> Truncated. Please check the console for full information.\n>
    \n" + ) + + assert console.consume_captured_warnings() == "" + + +def test_markdown_console_error_block(): + console = MarkdownConsole( + alert_block_max_content_length=100, alert_block_collapsible_threshold=40 + ) + assert console.consume_captured_errors() == "" + + # single error, within threshold + console.log_error("First error") + assert console.consume_captured_errors() == "> [!CAUTION]\n>\n> First error\n\n" + + # multiple errors, within threshold (list syntax) + console.log_error("First error") + console.log_error("Second error") + assert ( + console.consume_captured_errors() + == "> [!CAUTION]\n>\n> - First error\n>\n> - Second error\n\n" + ) + + # single error, within max threshold but over collapsible section threshold + error = "The snowflake engine is not recommended for storing SQLMesh state in production deployments" + assert len(error) > console.alert_block_collapsible_threshold + assert len(error) < console.alert_block_max_content_length + console.log_error(error) + assert ( + console.consume_captured_errors() + == "> [!CAUTION]\n>
    \n>\n> The snowflake engine is not recommended for storing SQLMesh state in production deployments\n>
    \n" + ) + + # single error, over max threshold + error = "The snowflake engine is not recommended for storing SQLMesh state in production deployments. Please see for a list of recommended engines and more information." + assert len(error) > console.alert_block_collapsible_threshold + assert len(error) > console.alert_block_max_content_length + console.log_error(error) + assert ( + console.consume_captured_errors() + == "> [!CAUTION]\n>
    \n>\n> The snowflake engine is not re...\n>\n> Truncated. Please check the console for full information.\n>
    \n" + ) + + # multiple errors, within max threshold but over collapsible section threshold + error_1 = "This is the first error" + error_2 = "This is the second error" + assert (len(error_1) + len(error_2)) > console.alert_block_collapsible_threshold + assert (len(error_1) + len(error_2)) < console.alert_block_max_content_length + console.log_error(error_1) + console.log_error(error_2) + assert ( + console.consume_captured_errors() + == "> [!CAUTION]\n>
    \n>\n> - This is the first error\n>\n> - This is the second error\n>
    \n" + ) + + # multiple errors, over max threshold + error_1 = "This is the first error and its really really long" + error_2 = "This is the second error and its also really really long" + assert (len(error_1) + len(error_2)) > console.alert_block_collapsible_threshold + assert (len(error_1) + len(error_2)) > console.alert_block_max_content_length + console.log_error(error_1) + console.log_error(error_2) + assert ( + console.consume_captured_errors() + == "> [!CAUTION]\n>
    \n>\n> - This is the first error and ...\n>\n> Truncated. Please check the console for full information.\n>
    \n" + ) + + assert console.consume_captured_errors() == "" diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index fafc6f6291..099ed6d9ef 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -19,11 +19,13 @@ from sqlmesh.integrations.github.cicd.controller import ( BotCommand, MergeStateStatus, + GithubCheckConclusion, ) from sqlmesh.integrations.github.cicd.controller import GithubController from sqlmesh.integrations.github.cicd.command import _update_pr_environment from sqlmesh.utils.date import to_datetime, now from tests.integrations.github.cicd.conftest import MockIssueComment +from sqlmesh.utils.errors import SQLMeshError pytestmark = pytest.mark.github @@ -644,3 +646,54 @@ def test_get_plan_summary_doesnt_truncate_backfill_list( * `memory.sushi.waiter_revenue_by_day`: [2025-06-30 - 2025-07-06]""" in summary ) + + +def test_get_plan_summary_includes_warnings_and_errors( + github_client, make_controller: t.Callable[..., GithubController] +): + controller = make_controller( + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + mock_out_context=False, + ) + + controller._console.log_warning("Warning 1\nWith multiline") + controller._console.log_warning("Warning 2") + controller._console.log_error("Error 1") + + summary = controller.get_plan_summary(controller.prod_plan) + + assert ("> [!WARNING]\n>\n> - Warning 1\n> With multiline\n>\n> - Warning 2\n\n") in summary + + assert ("> [!CAUTION]\n>\n> Error 1\n\n") in summary + + +def test_get_pr_environment_summary_includes_warnings_and_errors( + github_client, make_controller: t.Callable[..., GithubController] +): + controller = make_controller( + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + mock_out_context=False, + ) + + controller._console.log_warning("Warning 1") + controller._console.log_error("Error 1") + + # completed with no exception triggers a SUCCESS conclusion and only shows warnings + success_summary = controller.get_pr_environment_summary( + conclusion=GithubCheckConclusion.SUCCESS + ) + assert "> [!WARNING]\n>\n> Warning 1\n" in success_summary + assert "> [!CAUTION]\n>\n> Error 1" not in success_summary + + # since they got consumed in the previous call + controller._console.log_warning("Warning 1") + controller._console.log_error("Error 1") + + # completed with an exception triggers a FAILED conclusion and shows errors + error_summary = controller.get_pr_environment_summary( + conclusion=GithubCheckConclusion.FAILURE, exception=SQLMeshError("Something broke") + ) + assert "> [!WARNING]\n>\n> Warning 1\n" in error_summary + assert "> [!CAUTION]\n>\n> Error 1" in error_summary diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index ff9856993a..17e495fbc3 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -846,7 +846,7 @@ def test_merge_pr_has_no_changes( assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_skipped assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" assert ( - ":next_track_button: Skipped creating or updating PR Environment `hello_world_2`. No changes were detected compared to the prod environment." + ":next_track_button: Skipped creating or updating PR Environment `hello_world_2` :next_track_button:\n\nNo changes were detected compared to the prod environment." in pr_checks_runs[2]["output"]["summary"] ) From 079efc84c34572b1b04a7e76b5ce2e1979c1c333 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 11 Jul 2025 08:50:59 +1200 Subject: [PATCH 0567/1056] Chore!: Update ci/cd bot default behaviour to match CLI behaviour (#4900) --- docs/integrations/github.md | 4 +- sqlmesh/core/config/root.py | 12 ++++++ sqlmesh/integrations/github/cicd/config.py | 29 ++++++++++++-- tests/core/analytics/test_collector.py | 2 +- tests/integrations/github/cicd/test_config.py | 40 +++++++++++++++++-- 5 files changed, 77 insertions(+), 10 deletions(-) diff --git a/docs/integrations/github.md b/docs/integrations/github.md index 323aff0565..1f66ef6368 100644 --- a/docs/integrations/github.md +++ b/docs/integrations/github.md @@ -293,13 +293,13 @@ Below is an example of how to define the default config for the bot in either YA | `enable_deploy_command` | Indicates if the `/deploy` command should be enabled in order to allowed synchronized deploys to production. Default: `False` | bool | N | | `command_namespace` | The namespace to use for SQLMesh commands. For example if you provide `#SQLMesh` as a value then commands will be expected in the format of `#SQLMesh/`. Default: `None` meaning no namespace is used. | string | N | | `auto_categorize_changes` | Auto categorization behavior to use for the bot. If not provided then the project-wide categorization behavior is used. See [Auto-categorize model changes](https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#auto-categorize-model-changes) for details. | dict | N | -| `default_pr_start` | Default start when creating PR environment plans. If running in a mode where the bot automatically backfills models (based on `auto_categorize_changes` behavior) then this can be used to limit the amount of data backfilled. Defaults to `None` meaning the start date is set to the earliest model's start or to 1 day ago if [data previews](../concepts/plans.md#data-preview) need to be computed. | str | N | +| `default_pr_start` | Default start when creating PR environment plans. If running in a mode where the bot automatically backfills models (based on `auto_categorize_changes` behavior) then this can be used to limit the amount of data backfilled. Defaults to `None` meaning the start date is set to the earliest model's start or to 1 day ago if [data previews](../concepts/plans.md#data-preview) need to be computed.| str | N | | `pr_min_intervals` | Intended for use when `default_pr_start` is set to a relative time, eg `1 week ago`. This ensures that at least this many intervals across every model are included for backfill in the PR environment. Without this, models with an interval unit wider than `default_pr_start` (such as `@monthly` models if `default_pr_start` was set to `1 week ago`) will be excluded from backfill entirely. | int | N | | `skip_pr_backfill` | Indicates if the bot should skip backfilling models in the PR environment. Default: `True` | bool | N | | `pr_include_unmodified` | Indicates whether to include unmodified models in the PR environment. Default to the project's config value (which defaults to `False`) | bool | N | | `run_on_deploy_to_prod` | Indicates whether to run latest intervals when deploying to prod. If set to false, the deployment will backfill only the changed models up to the existing latest interval in production, ignoring any missing intervals beyond this point. Default: `False` | bool | N | | `pr_environment_name` | The name of the PR environment to create for which a PR number will be appended to. Defaults to the repo name if not provided. Note: The name will be normalized to alphanumeric + underscore and lowercase. | str | N | -| `prod_branch_name` | The name of the git branch associated with production. Ex: `prod`. Default: `main` or `master` is considered prod | str | N | +| `prod_branch_name` | The name of the git branch associated with production. Ex: `prod`. Default: `main` or `master` is considered prod | str | N | Example with all properties defined: diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index 315728aceb..cd92ff8467 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -260,6 +260,18 @@ def _normalize_identifiers(key: str) -> None: return self + @model_validator(mode="after") + def _inherit_project_config_in_cicd_bot(self) -> Self: + if self.cicd_bot: + # inherit the project-level settings into the CICD bot if they have not been explicitly overridden + if self.cicd_bot.auto_categorize_changes_ is None: + self.cicd_bot.auto_categorize_changes_ = self.plan.auto_categorize_changes + + if self.cicd_bot.pr_include_unmodified_ is None: + self.cicd_bot.pr_include_unmodified_ = self.plan.include_unmodified + + return self + def get_default_test_connection( self, default_catalog: t.Optional[str] = None, diff --git a/sqlmesh/integrations/github/cicd/config.py b/sqlmesh/integrations/github/cicd/config.py index 33312c4ad7..8f84db47c8 100644 --- a/sqlmesh/integrations/github/cicd/config.py +++ b/sqlmesh/integrations/github/cicd/config.py @@ -7,6 +7,7 @@ from sqlmesh.core.config.base import BaseConfig from sqlmesh.utils.date import TimeLike from sqlmesh.utils.pydantic import model_validator +from sqlmesh.core.console import get_console class MergeMethod(str, Enum): @@ -22,10 +23,12 @@ class GithubCICDBotConfig(BaseConfig): enable_deploy_command: bool = False merge_method: t.Optional[MergeMethod] = None command_namespace: t.Optional[str] = None - auto_categorize_changes: CategorizerConfig = CategorizerConfig.all_off() + auto_categorize_changes_: t.Optional[CategorizerConfig] = Field( + default=None, alias="auto_categorize_changes" + ) default_pr_start: t.Optional[TimeLike] = None - skip_pr_backfill: bool = True - pr_include_unmodified: t.Optional[bool] = None + skip_pr_backfill_: t.Optional[bool] = Field(default=None, alias="skip_pr_backfill") + pr_include_unmodified_: t.Optional[bool] = Field(default=None, alias="pr_include_unmodified") run_on_deploy_to_prod: bool = False pr_environment_name: t.Optional[str] = None pr_min_intervals: t.Optional[int] = None @@ -50,6 +53,26 @@ def prod_branch_names(self) -> t.List[str]: return [self.prod_branch_names_] return ["main", "master"] + @property + def auto_categorize_changes(self) -> CategorizerConfig: + return self.auto_categorize_changes_ or CategorizerConfig.all_off() + + @property + def pr_include_unmodified(self) -> bool: + return self.pr_include_unmodified_ or False + + @property + def skip_pr_backfill(self) -> bool: + if self.skip_pr_backfill_ is None: + get_console().log_warning( + "`skip_pr_backfill` is unset, defaulting it to `true` (no data will be backfilled).\n" + "Future versions of SQLMesh will default to `skip_pr_backfill: false` to align with the CLI default behaviour.\n" + "If you would like to preserve the current behaviour and remove this warning, please explicitly set `skip_pr_backfill: true` in the bot config.\n\n" + "For more information on configuring the bot, see: https://sqlmesh.readthedocs.io/en/stable/integrations/github/" + ) + return True + return self.skip_pr_backfill_ + FIELDS_FOR_ANALYTICS: t.ClassVar[t.Set[str]] = { "invalidate_environment_after_deploy", "enable_deploy_command", diff --git a/tests/core/analytics/test_collector.py b/tests/core/analytics/test_collector.py index 957db3a003..9eaca07ef3 100644 --- a/tests/core/analytics/test_collector.py +++ b/tests/core/analytics/test_collector.py @@ -145,7 +145,7 @@ def test_on_cicd_command(collector: AnalyticsCollector, mocker: MockerFixture): { "seq_num": 1, "event_type": "CICD_COMMAND", - "event": '{"command_name": "test_cicd", "command_args": ["arg_1", "arg_2"], "parent_command_names": ["parent_a", "parent_b"], "cicd_bot_config": {"invalidate_environment_after_deploy": true, "enable_deploy_command": false, "auto_categorize_changes": {"external": "off", "python": "off", "sql": "off", "seed": "off"}, "skip_pr_backfill": true, "run_on_deploy_to_prod": false}}', + "event": '{"command_name": "test_cicd", "command_args": ["arg_1", "arg_2"], "parent_command_names": ["parent_a", "parent_b"], "cicd_bot_config": {"invalidate_environment_after_deploy": true, "enable_deploy_command": false, "run_on_deploy_to_prod": false}}', **common_fields, } ), diff --git a/tests/integrations/github/cicd/test_config.py b/tests/integrations/github/cicd/test_config.py index c100a1fa98..e4424cf3ba 100644 --- a/tests/integrations/github/cicd/test_config.py +++ b/tests/integrations/github/cicd/test_config.py @@ -34,11 +34,11 @@ def test_load_yaml_config_default(tmp_path): assert config.cicd_bot.invalidate_environment_after_deploy assert config.cicd_bot.merge_method is None assert config.cicd_bot.command_namespace is None - assert config.cicd_bot.auto_categorize_changes == CategorizerConfig.all_off() + assert config.cicd_bot.auto_categorize_changes == config.plan.auto_categorize_changes assert config.cicd_bot.default_pr_start is None assert not config.cicd_bot.enable_deploy_command assert config.cicd_bot.skip_pr_backfill - assert config.cicd_bot.pr_include_unmodified is None + assert not config.cicd_bot.pr_include_unmodified assert config.cicd_bot.pr_environment_name is None assert config.cicd_bot.prod_branch_names == ["main", "master"] assert not config.cicd_bot.pr_min_intervals @@ -115,11 +115,11 @@ def test_load_python_config_defaults(tmp_path): assert config.cicd_bot.invalidate_environment_after_deploy assert config.cicd_bot.merge_method is None assert config.cicd_bot.command_namespace is None - assert config.cicd_bot.auto_categorize_changes == CategorizerConfig.all_off() + assert config.cicd_bot.auto_categorize_changes == config.plan.auto_categorize_changes assert config.cicd_bot.default_pr_start is None assert not config.cicd_bot.enable_deploy_command assert config.cicd_bot.skip_pr_backfill - assert config.cicd_bot.pr_include_unmodified is None + assert not config.cicd_bot.pr_include_unmodified assert config.cicd_bot.pr_environment_name is None assert config.cicd_bot.prod_branch_names == ["main", "master"] assert not config.cicd_bot.pr_min_intervals @@ -258,3 +258,35 @@ def test_ttl_in_past(tmp_path): match="TTL '1 week' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`.", ): load_config_from_paths(Config, project_paths=[tmp_path / "config.yaml"]) + + +def test_properties_inherit_from_project_config(tmp_path): + (tmp_path / "config.yaml").write_text(""" +plan: + auto_categorize_changes: + external: off + python: full + sql: off + seed: full + include_unmodified: true + +cicd_bot: + type: github + +model_defaults: + dialect: duckdb +""") + + config = load_config_from_paths(Config, [tmp_path / "config.yaml"]) + + assert ( + config.cicd_bot.auto_categorize_changes + == config.plan.auto_categorize_changes + == CategorizerConfig( + external=AutoCategorizationMode.OFF, + python=AutoCategorizationMode.FULL, + sql=AutoCategorizationMode.OFF, + seed=AutoCategorizationMode.FULL, + ) + ) + assert config.cicd_bot.pr_include_unmodified == config.plan.include_unmodified == True From 3f45d0e9a08139bee6b9b9ba7706e0ce67eb40dd Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 11 Jul 2025 10:57:30 +0100 Subject: [PATCH 0568/1056] feat(vscode): handling python errors well (#4947) Co-authored-by: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> --- sqlmesh/core/config/loader.py | 10 ++++- vscode/extension/tests/broken_project.spec.ts | 41 ++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index 7f1789a677..c381252fb9 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -184,8 +184,14 @@ def load_config_from_python_module( module_path: Path, config_name: str = "config", ) -> C: - with sys_path(module_path.parent): - config_module = import_python_file(module_path, module_path.parent) + try: + with sys_path(module_path.parent): + config_module = import_python_file(module_path, module_path.parent) + except Exception as e: + raise ConfigError( + f"Failed to load config file: {e}", + location=module_path, + ) try: config_obj = getattr(config_module, config_name) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index f2af3e13e0..c6be73e403 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -395,9 +395,46 @@ test.describe('Bad config.py/config.yaml file issues', () => { await runCommand(page, 'View: Focus Problems') // Assert that the error is present in the problems view - await page + const errorElement = page .getByText('Config needs to be a valid object of type') .first() - .isVisible({ timeout: 5_000 }) + await expect(errorElement).toBeVisible({ timeout: 5000 }) + }) + + test('sushi example, bad config.py', async ({ page, sharedCodeServer }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configPyPath = path.join(tempDir, 'config.py') + // Write an invalid Python to config.py + await fs.writeFile(configPyPath, 'invalid_python_code = [1, 2, 3') + + await page.goto( + `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open customers.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Expect the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Assert that the error is present in the problems view + const errorElement = page.getByText('Failed to load config file:').first() + await expect(errorElement).toBeVisible({ timeout: 5000 }) }) }) From 76a542035a4e442b8d0756e018fafcde677855ff Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:09:45 +0100 Subject: [PATCH 0569/1056] chore(vscode): add tests for duplicate model and bad model block (#4954) --- vscode/extension/tests/broken_project.spec.ts | 187 +----------- vscode/extension/tests/diagnostics.spec.ts | 271 +++++++++++++++++- 2 files changed, 271 insertions(+), 187 deletions(-) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index c6be73e403..bc6f4f7ed1 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -2,15 +2,8 @@ import { test, expect } from './fixtures' import fs from 'fs-extra' import os from 'os' import path from 'path' -import { - openLineageView, - runCommand, - saveFile, - SUSHI_SOURCE_PATH, -} from './utils' +import { openLineageView, saveFile, SUSHI_SOURCE_PATH } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -import { execAsync } from '../src/utilities/exec' -import yaml from 'yaml' test('bad project, double model', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp( @@ -260,181 +253,3 @@ test('bad project, double model, check lineage', async ({ await page.waitForTimeout(500) }) - -const setup = async (tempDir: string) => { - // Run the sqlmesh CLI from the root of the repo using the local path - const sqlmeshCliPath = path.resolve(__dirname, '../../../.venv/bin/sqlmesh') - const result = await execAsync(sqlmeshCliPath, ['init', 'duckdb'], { - cwd: tempDir, - }) - expect(result.exitCode).toBe(0) -} - -test.describe('Bad config.py/config.yaml file issues', () => { - test('sqlmesh init, then corrupted config.yaml, bad yaml', async ({ - page, - sharedCodeServer, - }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) - await setup(tempDir) - await createPythonInterpreterSettingsSpecifier(tempDir) - - const configYamlPath = path.join(tempDir, 'config.yaml') - // Write an invalid YAML to config.yaml - await fs.writeFile(configYamlPath, 'invalid_yaml; asdfasudfy') - - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') - - // Open full_model.sql model - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'full_model.sql', exact: true }) - .locator('a') - .click() - - // Wait for the error to appear - await page.waitForSelector('text=Error creating context') - - // Open the problems view - await runCommand(page, 'View: Focus Problems') - - // Asser that the error is present in the problems view - await page - .getByText('Invalid YAML configuration:') - .first() - .isVisible({ timeout: 5_000 }) - }) - - test('sqlmesh init, then corrupted config.yaml, bad parameters', async ({ - page, - sharedCodeServer, - }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) - await setup(tempDir) - await createPythonInterpreterSettingsSpecifier(tempDir) - - const configYamlPath = path.join(tempDir, 'config.yaml') - // Write an invalid YAML to config.yaml - const config = { - gateway: 'test', - } - // Write config to the yaml file - await fs.writeFile(configYamlPath, yaml.stringify(config)) - - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') - - // Open full_model.sql model - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'full_model.sql', exact: true }) - .locator('a') - .click() - - // Wait for the error to appear - await page.waitForSelector('text=Error creating context') - - // Open the problems view - await runCommand(page, 'View: Focus Problems') - - // Asser that the error is present in the problems view - await page - .getByText('Invalid project config:', { exact: true }) - .first() - .isVisible({ timeout: 5_000 }) - }) - - test('sushi example, correct python, bad config', async ({ - page, - sharedCodeServer, - }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - await createPythonInterpreterSettingsSpecifier(tempDir) - - const configPyPath = path.join(tempDir, 'config.py') - // Write an invalid Python to config.py - await fs.writeFile(configPyPath, 'config = {}') - - await page.goto( - `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') - - // Open customers.sql model - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - // Expect the error to appear - await page.waitForSelector('text=Error creating context') - - // Open the problems view - await runCommand(page, 'View: Focus Problems') - - // Assert that the error is present in the problems view - const errorElement = page - .getByText('Config needs to be a valid object of type') - .first() - await expect(errorElement).toBeVisible({ timeout: 5000 }) - }) - - test('sushi example, bad config.py', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - await createPythonInterpreterSettingsSpecifier(tempDir) - - const configPyPath = path.join(tempDir, 'config.py') - // Write an invalid Python to config.py - await fs.writeFile(configPyPath, 'invalid_python_code = [1, 2, 3') - - await page.goto( - `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') - - // Open customers.sql model - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'customers.sql', exact: true }) - .locator('a') - .click() - - // Expect the error to appear - await page.waitForSelector('text=Error creating context') - - // Open the problems view - await runCommand(page, 'View: Focus Problems') - - // Assert that the error is present in the problems view - const errorElement = page.getByText('Failed to load config file:').first() - await expect(errorElement).toBeVisible({ timeout: 5000 }) - }) -}) diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts index 3c3aa4d0e0..be6511a603 100644 --- a/vscode/extension/tests/diagnostics.spec.ts +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -1,9 +1,11 @@ -import { test } from './fixtures' +import { expect, test } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' import { runCommand, SUSHI_SOURCE_PATH } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' +import { execAsync } from '../src/utilities/exec' +import yaml from 'yaml' test('Workspace diagnostics show up in the diagnostics panel', async ({ page, @@ -43,3 +45,270 @@ test('Workspace diagnostics show up in the diagnostics panel', async ({ await page.waitForSelector('text=problems') await page.waitForSelector('text=All models should have an owner') }) + +test.describe('Bad config.py/config.yaml file issues', () => { + const setup = async (tempDir: string) => { + // Run the sqlmesh CLI from the root of the repo using the local path + const sqlmeshCliPath = path.resolve(__dirname, '../../../.venv/bin/sqlmesh') + const result = await execAsync(sqlmeshCliPath, ['init', 'duckdb'], { + cwd: tempDir, + }) + expect(result.exitCode).toBe(0) + } + + test('sqlmesh init, then corrupted config.yaml, bad yaml', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await setup(tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configYamlPath = path.join(tempDir, 'config.yaml') + // Write an invalid YAML to config.yaml + await fs.writeFile(configYamlPath, 'invalid_yaml; asdfasudfy') + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open full_model.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'full_model.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Asser that the error is present in the problems view + await page + .getByText('Invalid YAML configuration:') + .first() + .isVisible({ timeout: 5_000 }) + }) + + test('sqlmesh init, then corrupted config.yaml, bad parameters', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await setup(tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configYamlPath = path.join(tempDir, 'config.yaml') + // Write an invalid YAML to config.yaml + const config = { + gateway: 'test', + } + // Write config to the yaml file + await fs.writeFile(configYamlPath, yaml.stringify(config)) + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open full_model.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'full_model.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Asser that the error is present in the problems view + await page + .getByText('Invalid project config:', { exact: true }) + .first() + .isVisible({ timeout: 5_000 }) + }) + + test('sushi example, correct python, bad config', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configPyPath = path.join(tempDir, 'config.py') + // Write an invalid Python to config.py + await fs.writeFile(configPyPath, 'config = {}') + + await page.goto( + `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open customers.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Expect the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Assert that the error is present in the problems view + const errorElement = page + .getByText('Config needs to be a valid object of type') + .first() + await expect(errorElement).toBeVisible({ timeout: 5000 }) + }) + + test('sushi example, bad config.py', async ({ page, sharedCodeServer }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configPyPath = path.join(tempDir, 'config.py') + // Write an invalid Python to config.py + await fs.writeFile(configPyPath, 'invalid_python_code = [1, 2, 3') + + await page.goto( + `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open customers.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Expect the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Assert that the error is present in the problems view + const errorElement = page.getByText('Failed to load config file:').first() + await expect(errorElement).toBeVisible({ timeout: 5000 }) + }) +}) + +test.describe('Diagnostics for bad SQLMesh models', () => { + test('duplicate model names', async ({ page, sharedCodeServer }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + // Copy over the sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + // Duplicate the customers.sql model + const customersSqlPath = path.join(tempDir, 'models', 'customers.sql') + const duplicatedCustomersSqlPath = path.join( + tempDir, + 'models', + 'customers_duplicated.sql', + ) + await fs.copy(customersSqlPath, duplicatedCustomersSqlPath) + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open full_model.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Asser that the error is present in the problems view + await page + .getByText('Duplicate SQLMesh model name') + .first() + .isVisible({ timeout: 5_000 }) + }) + + test('bad model block', async ({ page, sharedCodeServer }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + // Copy over the sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + // Add a model with a bad model block + const customersSqlPath = path.join(tempDir, 'models', 'bad_model.sql') + const contents = + 'MODEL ( name sushi.bad_block, test); SELECT * FROM sushi.customers' + await fs.writeFile(customersSqlPath, contents) + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open the customers.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Assert error is present in the problems view + const errorElement = page + .getByText("Required keyword: 'value' missing for") + .first() + await expect(errorElement).toBeVisible({ timeout: 5000 }) + }) +}) From 490b3eff49994dc15757a20277c42be60664b682 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:48:49 +0100 Subject: [PATCH 0570/1056] chore(vscode): test bad audit diagnostic (#4955) --- vscode/extension/tests/diagnostics.spec.ts | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts index be6511a603..f36cf81c59 100644 --- a/vscode/extension/tests/diagnostics.spec.ts +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -312,3 +312,53 @@ test.describe('Diagnostics for bad SQLMesh models', () => { await expect(errorElement).toBeVisible({ timeout: 5000 }) }) }) + +test.describe('Diagnostics for bad audits', () => { + test('bad audit block in audit', async ({ page, sharedCodeServer }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + + // Copy over the sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + // Make an existing audit file a bad audit + const auditFilePath = path.join( + tempDir, + 'audits', + 'assert_item_price_above_zero.sql', + ) + const readFile = await fs.readFile(auditFilePath, 'utf8') + const updatedContent = readFile.replace('AUDIT (', 'AUDIT ( rubbish value,') + await fs.writeFile(auditFilePath, updatedContent) + + // Navigate to the code-server instance + await page.goto( + `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open a the customers.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Assert that the error is present in the problems view + const errorElement = page + .getByText("Invalid extra fields {'rubbish'} in the audit definition") + .first() + await expect(errorElement).toBeVisible({ timeout: 5000 }) + }) +}) From 606b740c352535293842353a40c98d805a9e3a00 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:37:54 +0100 Subject: [PATCH 0571/1056] chore(vscode): adding tests for bad projects (#4957) --- vscode/extension/tests/broken_project.spec.ts | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index bc6f4f7ed1..60c103fe7f 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -2,7 +2,12 @@ import { test, expect } from './fixtures' import fs from 'fs-extra' import os from 'os' import path from 'path' -import { openLineageView, saveFile, SUSHI_SOURCE_PATH } from './utils' +import { + openLineageView, + runCommand, + saveFile, + SUSHI_SOURCE_PATH, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('bad project, double model', async ({ page, sharedCodeServer }) => { @@ -253,3 +258,55 @@ test('bad project, double model, check lineage', async ({ await page.waitForTimeout(500) }) + +test('bad model block, then fixed', async ({ page, sharedCodeServer }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + // Copy over the sushi project + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + // Add a model with a bad model block + const badModelPath = path.join(tempDir, 'models', 'bad_model.sql') + const contents = + 'MODEL ( name sushi.bad_block, test); SELECT * FROM sushi.customers' + await fs.writeFile(badModelPath, contents) + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open the customers.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Assert error is present in the problems view + const errorElement = page + .getByText("Required keyword: 'value' missing for") + .first() + await expect(errorElement).toBeVisible({ timeout: 5000 }) + + // Remove the bad model file + await fs.remove(badModelPath) + + // Click on the grain part of the model and save + await page.getByText('grain').click() + await saveFile(page) + + // Wait for successful context load + await page.waitForSelector('text=Loaded SQLMesh context') +}) From 107fe2561129db80bbb4a8d67934d16ee51734f5 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:59:37 +0100 Subject: [PATCH 0572/1056] chore(vscode): refactoring e2e tests (#4962) --- vscode/extension/tests/bad_setup.spec.ts | 31 ++------- vscode/extension/tests/broken_project.spec.ts | 26 ++----- vscode/extension/tests/completions.spec.ts | 15 ++-- vscode/extension/tests/diagnostics.spec.ts | 37 +++------- .../extension/tests/find_references.spec.ts | 68 ++++++------------- vscode/extension/tests/format.spec.ts | 7 +- .../extension/tests/go_to_definition.spec.ts | 12 +--- vscode/extension/tests/hints.spec.ts | 7 +- vscode/extension/tests/lineage.spec.ts | 23 +++---- .../extension/tests/lineage_settings.spec.ts | 6 +- vscode/extension/tests/python_env.spec.ts | 9 +-- vscode/extension/tests/rename_cte.spec.ts | 12 ++-- vscode/extension/tests/render.spec.ts | 24 +++---- vscode/extension/tests/stop.spec.ts | 9 +-- vscode/extension/tests/tcloud.spec.ts | 32 ++++----- vscode/extension/tests/utils.ts | 18 ++++- vscode/extension/tests/venv_naming.spec.ts | 5 +- 17 files changed, 115 insertions(+), 226 deletions(-) diff --git a/vscode/extension/tests/bad_setup.spec.ts b/vscode/extension/tests/bad_setup.spec.ts index db3d481341..2284e2a95e 100644 --- a/vscode/extension/tests/bad_setup.spec.ts +++ b/vscode/extension/tests/bad_setup.spec.ts @@ -6,6 +6,7 @@ import { createVirtualEnvironment, openFile, openLineageView, + openServerPage, pipInstall, REPO_ROOT, SUSHI_SOURCE_PATH, @@ -41,21 +42,13 @@ test('missing LSP dependencies shows install prompt', async ({ spaces: 2, }) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - - // Open a SQL file to trigger SQLMesh activation - // Wait for the models folder to be visible - await page.waitForSelector('text=models') + await openServerPage(page, tempDir, sharedCodeServer) - // Click on the models folder + // Open a top_waiters model to trigger SQLMesh activation await page .getByRole('treeitem', { name: 'models', exact: true }) .locator('a') .click() - - // Open the top_waiters model await page .getByRole('treeitem', { name: 'customers.sql', exact: true }) .locator('a') @@ -66,12 +59,7 @@ test('missing LSP dependencies shows install prompt', async ({ expect(await page.locator('text=Install').count()).toBeGreaterThanOrEqual(1) }) -test('lineage, no sqlmesh found', async ({ - page, - sharedCodeServer, -}, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation - +test('lineage, no sqlmesh found', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -92,12 +80,8 @@ test('lineage, no sqlmesh found', async ({ }) // navigate to code-server instance - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + await openServerPage(page, tempDir, sharedCodeServer) - // Open lineage view await openLineageView(page) // Assert shows that sqlmesh is not installed @@ -143,10 +127,7 @@ test.skip('check that the LSP runs correctly by opening lineage when looking at await fs.ensureDir(path.dirname(sqlFile)) await fs.writeFile(sqlFile, 'SELECT 1') - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + await openServerPage(page, tempDir, sharedCodeServer) // Open the SQL file from the other directory await openFile(page, sqlFile) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index 60c103fe7f..af7b022545 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -4,6 +4,7 @@ import os from 'os' import path from 'path' import { openLineageView, + openServerPage, runCommand, saveFile, SUSHI_SOURCE_PATH, @@ -29,9 +30,7 @@ test('bad project, double model', async ({ page, sharedCodeServer }) => { ) await createPythonInterpreterSettingsSpecifier(tempDir) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await page.waitForSelector('text=models') @@ -60,9 +59,7 @@ test('working project, then broken through adding double model, then refixed', a await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await page.waitForLoadState('networkidle') // Open the lineage view to confirm it loads properly @@ -170,13 +167,7 @@ test('bad project, double model, then fixed', async ({ customersSql, ) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await page.waitForSelector('text=models') @@ -245,10 +236,7 @@ test('bad project, double model, check lineage', async ({ ) await createPythonInterpreterSettingsSpecifier(tempDir) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + await openServerPage(page, tempDir, sharedCodeServer) // Open the lineage view await openLineageView(page) @@ -273,9 +261,7 @@ test('bad model block, then fixed', async ({ page, sharedCodeServer }) => { 'MODEL ( name sushi.bad_block, test); SELECT * FROM sushi.customers' await fs.writeFile(badModelPath, contents) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await page.waitForLoadState('networkidle') // Open the customers.sql model diff --git a/vscode/extension/tests/completions.spec.ts b/vscode/extension/tests/completions.spec.ts index 693934d4e4..da4eed6efc 100644 --- a/vscode/extension/tests/completions.spec.ts +++ b/vscode/extension/tests/completions.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { SUSHI_SOURCE_PATH } from './utils' +import { openServerPage, SUSHI_SOURCE_PATH } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Autocomplete for model names', async ({ page, sharedCodeServer }) => { @@ -10,9 +10,7 @@ test('Autocomplete for model names', async ({ page, sharedCodeServer }) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Wait for the models folder to be visible await page.waitForSelector('text=models') @@ -67,9 +65,7 @@ test.describe('Macro Completions', () => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Wait for the models folder to be visible await page.waitForSelector('text=models') @@ -119,10 +115,7 @@ test.describe('Macro Completions', () => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Wait for the models folder to be visible await page.waitForSelector('text=models') diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts index f36cf81c59..a520cabcf7 100644 --- a/vscode/extension/tests/diagnostics.spec.ts +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -2,7 +2,7 @@ import { expect, test } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { runCommand, SUSHI_SOURCE_PATH } from './utils' +import { openServerPage, runCommand, SUSHI_SOURCE_PATH } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' import { execAsync } from '../src/utilities/exec' import yaml from 'yaml' @@ -20,10 +20,7 @@ test('Workspace diagnostics show up in the diagnostics panel', async ({ const updatedContent = configContent.replace('enabled=False', 'enabled=True') await fs.writeFile(configPath, updatedContent) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - + await openServerPage(page, tempDir, sharedCodeServer) // Wait for the models folder to be visible await page.waitForSelector('text=models') @@ -70,10 +67,7 @@ test.describe('Bad config.py/config.yaml file issues', () => { // Write an invalid YAML to config.yaml await fs.writeFile(configYamlPath, 'invalid_yaml; asdfasudfy') - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + await openServerPage(page, tempDir, sharedCodeServer) // Open full_model.sql model await page @@ -116,10 +110,7 @@ test.describe('Bad config.py/config.yaml file issues', () => { // Write config to the yaml file await fs.writeFile(configYamlPath, yaml.stringify(config)) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + await openServerPage(page, tempDir, sharedCodeServer) // Open full_model.sql model await page @@ -158,10 +149,7 @@ test.describe('Bad config.py/config.yaml file issues', () => { // Write an invalid Python to config.py await fs.writeFile(configPyPath, 'config = {}') - await page.goto( - `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + await openServerPage(page, tempDir, sharedCodeServer) // Open customers.sql model await page @@ -197,10 +185,7 @@ test.describe('Bad config.py/config.yaml file issues', () => { // Write an invalid Python to config.py await fs.writeFile(configPyPath, 'invalid_python_code = [1, 2, 3') - await page.goto( - `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + await openServerPage(page, tempDir, sharedCodeServer) // Open customers.sql model await page @@ -242,10 +227,7 @@ test.describe('Diagnostics for bad SQLMesh models', () => { ) await fs.copy(customersSqlPath, duplicatedCustomersSqlPath) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + await openServerPage(page, tempDir, sharedCodeServer) // Open full_model.sql model await page @@ -334,10 +316,7 @@ test.describe('Diagnostics for bad audits', () => { await fs.writeFile(auditFilePath, updatedContent) // Navigate to the code-server instance - await page.goto( - `http://127.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + await openServerPage(page, tempDir, sharedCodeServer) // Open a the customers.sql model await page diff --git a/vscode/extension/tests/find_references.spec.ts b/vscode/extension/tests/find_references.spec.ts index 42dd0e6274..b952e30ef8 100644 --- a/vscode/extension/tests/find_references.spec.ts +++ b/vscode/extension/tests/find_references.spec.ts @@ -2,7 +2,12 @@ import { test, expect, Page } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { findAllReferences, goToReferences, SUSHI_SOURCE_PATH } from './utils' +import { + findAllReferences, + goToReferences, + openServerPage, + SUSHI_SOURCE_PATH, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' // Helper function to set up a test environment for model references @@ -59,10 +64,7 @@ test.describe('Model References', () => { sharedCodeServer, }) => { const tempDir = await setupModelTestEnvironment() - - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Open customers.sql which contains references to other models await openCustomersFile(page) @@ -130,9 +132,7 @@ test.describe('Model References', () => { }) => { const tempDir = await setupModelTestEnvironment() - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Open customers.sql which contains multiple model references await openCustomersFile(page) @@ -180,10 +180,7 @@ test.describe('Model References', () => { sharedCodeServer, }) => { const tempDir = await setupModelTestEnvironment() - - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Open assert_item_price_above_zero.sql audit file which references sushi.items model await navigateToAudits(page) @@ -268,9 +265,7 @@ test.describe('Model References', () => { }) => { const tempDir = await setupModelTestEnvironment() - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Open the audit file that validates item prices await navigateToAudits(page) @@ -331,10 +326,7 @@ test.describe('CTE References', () => { sharedCodeServer, }) => { const tempDir = await setupModelTestEnvironment() - - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await openCustomersFile(page) @@ -373,9 +365,7 @@ test.describe('CTE References', () => { }) => { const tempDir = await setupModelTestEnvironment() - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await openCustomersFile(page) @@ -414,10 +404,7 @@ test.describe('CTE References', () => { sharedCodeServer, }) => { const tempDir = await setupModelTestEnvironment() - - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await openCustomersFile(page) @@ -455,10 +442,7 @@ test.describe('CTE References', () => { test('Find all references for CTE', async ({ page, sharedCodeServer }) => { const tempDir = await setupModelTestEnvironment() - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - + await openServerPage(page, tempDir, sharedCodeServer) await openCustomersFile(page) // Click on the CTE definition "current_marketing_outer" @@ -482,9 +466,7 @@ test.describe('CTE References', () => { }) => { const tempDir = await setupModelTestEnvironment() - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await openCustomersFile(page) @@ -509,10 +491,7 @@ test.describe('CTE References', () => { }) => { const tempDir = await setupModelTestEnvironment() - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - + await openServerPage(page, tempDir, sharedCodeServer) await openCustomersFile(page) // Click on the nested CTE "current_marketing" at line 33 @@ -541,10 +520,7 @@ test.describe('Macro References', () => { }) => { const tempDir = await setupModelTestEnvironment() - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - + await openServerPage(page, tempDir, sharedCodeServer) await openTopWaitersFile(page) // Click on the @ADD_ONE macro usage @@ -578,10 +554,7 @@ test.describe('Macro References', () => { sharedCodeServer, }) => { const tempDir = await setupModelTestEnvironment() - - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await openTopWaitersFile(page) @@ -635,10 +608,7 @@ test.describe('Macro References', () => { }) => { const tempDir = await setupModelTestEnvironment() - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - + await openServerPage(page, tempDir, sharedCodeServer) await openTopWaitersFile(page) // Click on the @SQL_LITERAL macro usage diff --git a/vscode/extension/tests/format.spec.ts b/vscode/extension/tests/format.spec.ts index 7525f88fdf..4e2b96bf94 100644 --- a/vscode/extension/tests/format.spec.ts +++ b/vscode/extension/tests/format.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { runCommand, SUSHI_SOURCE_PATH } from './utils' +import { openServerPage, runCommand, SUSHI_SOURCE_PATH } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Format project works correctly', async ({ page, sharedCodeServer }) => { @@ -10,10 +10,7 @@ test('Format project works correctly', async ({ page, sharedCodeServer }) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Wait for the models folder to be visible await page.waitForSelector('text=models') diff --git a/vscode/extension/tests/go_to_definition.spec.ts b/vscode/extension/tests/go_to_definition.spec.ts index 40d941669a..7d1749a1b7 100644 --- a/vscode/extension/tests/go_to_definition.spec.ts +++ b/vscode/extension/tests/go_to_definition.spec.ts @@ -2,18 +2,14 @@ import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { goToDefinition, SUSHI_SOURCE_PATH } from './utils' +import { goToDefinition, openServerPage, SUSHI_SOURCE_PATH } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Stop server works', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - - // Navigate to code-server instance - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Wait for the models folder to be visible await page.waitForSelector('text=models') @@ -47,9 +43,7 @@ test('Go to definition for model', async ({ page, sharedCodeServer }) => { await createPythonInterpreterSettingsSpecifier(tempDir) // Navigate to code-server instance - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Wait for the models folder to be visible await page.waitForSelector('text=models') diff --git a/vscode/extension/tests/hints.spec.ts b/vscode/extension/tests/hints.spec.ts index 13bf37fe8a..cb5dddb0eb 100644 --- a/vscode/extension/tests/hints.spec.ts +++ b/vscode/extension/tests/hints.spec.ts @@ -2,17 +2,14 @@ import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { SUSHI_SOURCE_PATH } from './utils' +import { openServerPage, SUSHI_SOURCE_PATH } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Model type hinting', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - // Navigate to code-server instance - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Wait for the models folder to be visible await page.waitForSelector('text=models') diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index 8f88c753f0..dfac5f5342 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -2,7 +2,7 @@ import { test, Page } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { openLineageView, SUSHI_SOURCE_PATH } from './utils' +import { openLineageView, openServerPage, SUSHI_SOURCE_PATH } from './utils' import { writeFileSync } from 'fs' import { createPythonInterpreterSettingsSpecifier, @@ -28,9 +28,7 @@ test('Lineage panel renders correctly - no project path config (default)', async await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await testLineageWithProjectPath(page) }) @@ -59,7 +57,7 @@ test.skip('Lineage panel renders correctly - relative project path', async ({ ) try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await openServerPage(page, workspaceDir, sharedCodeServer) await testLineageWithProjectPath(page) } finally { await fs.remove(workspaceDir) @@ -91,7 +89,7 @@ test.skip('Lineage panel renders correctly - absolute project path', async ({ ) try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await openServerPage(page, tempDir, sharedCodeServer) await testLineageWithProjectPath(page) } finally { await stopCodeServer(context) @@ -125,7 +123,7 @@ test.skip('Lineage panel renders correctly - relative project outside of workspa ) try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await openServerPage(page, tempDir, sharedCodeServer) await testLineageWithProjectPath(page) } finally { await stopCodeServer(context) @@ -156,9 +154,7 @@ test.skip('Lineage panel renders correctly - absolute path project outside of wo { spaces: 2 }, ) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}?folder=${workspaceDir}`, - ) + await openServerPage(page, workspaceDir, sharedCodeServer) await testLineageWithProjectPath(page) }) @@ -206,9 +202,7 @@ test.skip('Lineage panel renders correctly - multiworkspace setup', async ({ { spaces: 2 }, ) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}?folder=${workspaceDir}`, - ) + await openServerPage(page, workspaceDir, sharedCodeServer) await page.waitForSelector('text=Open workspace') await page.click('text=Open workspace') await testLineageWithProjectPath(page) @@ -216,6 +210,7 @@ test.skip('Lineage panel renders correctly - multiworkspace setup', async ({ test.skip('Lineage panel renders correctly - multiworkspace setup reversed', async ({ page, + sharedCodeServer, }) => { const workspaceDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), @@ -267,7 +262,7 @@ test.skip('Lineage panel renders correctly - multiworkspace setup reversed', asy ) try { - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await openServerPage(page, workspaceDir, sharedCodeServer) await page.waitForSelector('text=Open workspace') await page.click('text=Open workspace') await testLineageWithProjectPath(page) diff --git a/vscode/extension/tests/lineage_settings.spec.ts b/vscode/extension/tests/lineage_settings.spec.ts index f9d2e88781..21827e351f 100644 --- a/vscode/extension/tests/lineage_settings.spec.ts +++ b/vscode/extension/tests/lineage_settings.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { openLineageView, SUSHI_SOURCE_PATH } from './utils' +import { openLineageView, openServerPage, SUSHI_SOURCE_PATH } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Settings button is visible in the lineage view', async ({ @@ -13,9 +13,7 @@ test('Settings button is visible in the lineage view', async ({ await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await page.waitForSelector('text=models') diff --git a/vscode/extension/tests/python_env.spec.ts b/vscode/extension/tests/python_env.spec.ts index bd5dfff3a3..04ccd2a7a0 100644 --- a/vscode/extension/tests/python_env.spec.ts +++ b/vscode/extension/tests/python_env.spec.ts @@ -3,6 +3,7 @@ import fs from 'fs-extra' import { createVirtualEnvironment, openLineageView, + openServerPage, pipInstall, PythonEnvironment, REPO_ROOT, @@ -34,9 +35,7 @@ async function runTest( context: CodeServerContext, tempDir: string, ): Promise { - await page.goto( - `http://127.0.0.1:${context.codeServerPort}` + `/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, context) await page.waitForSelector('text=models') await openLineageView(page) } @@ -78,9 +77,7 @@ test.describe('python environment variable injection on sqlmesh_lsp', () => { await page.waitForSelector('text=Error creating context') }) - test('normal setup - set', async ({ page, sharedCodeServer }, testInfo) => { - testInfo.setTimeout(120_000) - + test('normal setup - set', async ({ page, sharedCodeServer }) => { const { tempDir } = await setupEnvironment() writeEnvironmentConfig(tempDir) const env_file = path.join(tempDir, '.env') diff --git a/vscode/extension/tests/rename_cte.spec.ts b/vscode/extension/tests/rename_cte.spec.ts index 0563f023b2..db7ab6bcb3 100644 --- a/vscode/extension/tests/rename_cte.spec.ts +++ b/vscode/extension/tests/rename_cte.spec.ts @@ -2,7 +2,12 @@ import { test, expect, Page } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { findAllReferences, renameSymbol, SUSHI_SOURCE_PATH } from './utils' +import { + findAllReferences, + openServerPage, + renameSymbol, + SUSHI_SOURCE_PATH, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' async function setupTestEnvironment({ @@ -16,10 +21,7 @@ async function setupTestEnvironment({ await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - // Navigate to code-server instance - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Navigate to customers.sql which contains CTEs await page.waitForSelector('text=models') diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts index 741d37ae14..8b5cec01ab 100644 --- a/vscode/extension/tests/render.spec.ts +++ b/vscode/extension/tests/render.spec.ts @@ -2,7 +2,12 @@ import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { openLineageView, runCommand, SUSHI_SOURCE_PATH } from './utils' +import { + openLineageView, + openServerPage, + runCommand, + SUSHI_SOURCE_PATH, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Render works correctly', async ({ page, sharedCodeServer }) => { @@ -10,9 +15,7 @@ test('Render works correctly', async ({ page, sharedCodeServer }) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Wait for the models folder to be visible await page.waitForSelector('text=models') @@ -48,9 +51,7 @@ test('Render works correctly with model without a description', async ({ await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Wait for the models folder to be visible await page.waitForSelector('text=models') @@ -86,9 +87,7 @@ test('Render works correctly with every rendered model opening a new tab', async await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) // Wait for the models folder to be visible await page.waitForSelector('text=models') @@ -133,10 +132,7 @@ test('Render shows model picker when no active editor is open', async ({ await createPythonInterpreterSettingsSpecifier(tempDir) // Navigate to code-server instance - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + await openServerPage(page, tempDir, sharedCodeServer) // Load the lineage view to initialize SQLMesh context (like lineage.spec.ts does) await openLineageView(page) diff --git a/vscode/extension/tests/stop.spec.ts b/vscode/extension/tests/stop.spec.ts index e9ba98fa03..12c3275a77 100644 --- a/vscode/extension/tests/stop.spec.ts +++ b/vscode/extension/tests/stop.spec.ts @@ -1,5 +1,5 @@ import path from 'path' -import { runCommand, SUSHI_SOURCE_PATH } from './utils' +import { openServerPage, runCommand, SUSHI_SOURCE_PATH } from './utils' import os from 'os' import { test } from './fixtures' import fs from 'fs-extra' @@ -12,12 +12,7 @@ test('Stop server works', async ({ page, sharedCodeServer }) => { await createPythonInterpreterSettingsSpecifier(tempDir) // Navigate to code-server instance - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - - // Wait for code-server to load - await page.waitForLoadState('networkidle') + await openServerPage(page, tempDir, sharedCodeServer) await page.waitForSelector('[role="application"]', { timeout: 10000 }) // Wait for the models folder to be visible in the file explorer diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index b677acaa27..ec334a3170 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -4,6 +4,7 @@ import fs from 'fs-extra' import os from 'os' import { createVirtualEnvironment, + openServerPage, pipInstall, REPO_ROOT, SUSHI_SOURCE_PATH, @@ -38,7 +39,10 @@ async function setupPythonEnvironment(envDir: string): Promise { return pythonDetails.pythonPath } -test('not signed in, shows sign in window', async ({ page }) => { +test('not signed in, shows sign in window', async ({ + page, + sharedCodeServer, +}) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -79,8 +83,7 @@ test('not signed in, shows sign in window', async ({ page }) => { { spaces: 2 }, ) - // Start VS Code - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await openServerPage(page, tempDir, sharedCodeServer) // Open a SQL file to trigger SQLMesh activation // Wait for the models folder to be visible @@ -111,8 +114,8 @@ test('not signed in, shows sign in window', async ({ page }) => { test('signed in and not installed shows installation window', async ({ page, -}, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + sharedCodeServer, +}) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -156,8 +159,7 @@ test('signed in and not installed shows installation window', async ({ { spaces: 2 }, ) - // Start VS Code - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await openServerPage(page, tempDir, sharedCodeServer) // Open a SQL file to trigger SQLMesh activation // Wait for the models folder to be visible @@ -188,6 +190,7 @@ test('signed in and not installed shows installation window', async ({ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when ready', async ({ page, + sharedCodeServer, }) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), @@ -236,10 +239,7 @@ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when read ) // Start VS Code - const context = await startCodeServer({ - tempDir, - }) - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await openServerPage(page, tempDir, sharedCodeServer) // Open a SQL file to trigger SQLMesh activation // Wait for the models folder to be visible @@ -267,8 +267,8 @@ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when read test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when ready', async ({ page, -}, testInfo) => { - testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation + sharedCodeServer, +}) => { const tempDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-tcloud-'), ) @@ -315,11 +315,7 @@ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when read { spaces: 2 }, ) - // Start VS Code - const context = await startCodeServer({ - tempDir, - }) - await page.goto(`http://127.0.0.1:${context.codeServerPort}`) + await openServerPage(page, tempDir, sharedCodeServer) // Open a SQL file to trigger SQLMesh activation // Wait for the models folder to be visible diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index d6a2ac7aaa..e783ae268e 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -1,6 +1,7 @@ import path from 'path' import { Page } from '@playwright/test' import { execAsync } from '../src/utilities/exec' +import { CodeServerContext } from './utils_code_server' // Where your extension lives on disk export const EXT_PATH = path.resolve(__dirname, '..') @@ -163,8 +164,6 @@ export const goToReferences = async (page: Page): Promise => /** * Open the vscode code file picker and select the given file. - * @param window The window to run the command in. - * @param filePath The path to the file to select. */ export const openFile = async (page: Page, file: string): Promise => { const maxRetries = 3 @@ -196,3 +195,18 @@ export const openFile = async (page: Page, file: string): Promise => { } } } + +/** + * Go to VSCode page + */ +export const openServerPage = async ( + page: Page, + tempDir: string, + context: CodeServerContext, +) => { + await page.goto( + `http://127.0.0.1:${context.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + await page.waitForSelector('[role="application"]', { timeout: 10000 }) +} diff --git a/vscode/extension/tests/venv_naming.spec.ts b/vscode/extension/tests/venv_naming.spec.ts index 4aeab2dcca..9e8006bff9 100644 --- a/vscode/extension/tests/venv_naming.spec.ts +++ b/vscode/extension/tests/venv_naming.spec.ts @@ -5,6 +5,7 @@ import path from 'path' import { createVirtualEnvironment, openLineageView, + openServerPage, pipInstall, REPO_ROOT, SUSHI_SOURCE_PATH, @@ -34,9 +35,7 @@ test('venv being named .env', async ({ page, sharedCodeServer }) => { spaces: 2, }) - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) + await openServerPage(page, tempDir, sharedCodeServer) await page.waitForSelector('text=models') await openLineageView(page) await page.waitForSelector('text=Loaded SQLMesh Context') From ff45e031a528dece0efe827a8447fab7c1c08e06 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 14 Jul 2025 22:54:42 +0300 Subject: [PATCH 0573/1056] Fix!: Add project in environment statements for consistent multi-repo plans (#4966) --- sqlmesh/core/context.py | 9 ++++ sqlmesh/core/context_diff.py | 4 +- sqlmesh/core/environment.py | 1 + sqlmesh/core/loader.py | 6 ++- sqlmesh/dbt/loader.py | 1 + tests/core/test_integration.py | 81 ++++++++++++++++++++++++++++++++-- 6 files changed, 96 insertions(+), 6 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 0c398af412..31147dec0e 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -622,6 +622,15 @@ def load(self, update_schemas: bool = True) -> GenericContext[C]: BUILTIN_RULES.union(project.user_rules), config.linter ) + # Load environment statements from state for projects not in current load + if any(self._projects): + prod = self.state_reader.get_environment(c.PROD) + if prod: + existing_statements = self.state_reader.get_environment_statements(c.PROD) + for stmt in existing_statements: + if stmt.project and stmt.project not in self._projects: + self._environment_statements.append(stmt) + uncached = set() if any(self._projects): diff --git a/sqlmesh/core/context_diff.py b/sqlmesh/core/context_diff.py index f97edec5da..ff19a3c7c6 100644 --- a/sqlmesh/core/context_diff.py +++ b/sqlmesh/core/context_diff.py @@ -311,7 +311,9 @@ def has_requirement_changes(self) -> bool: @property def has_environment_statements_changes(self) -> bool: - return self.environment_statements != self.previous_environment_statements + return sorted(self.environment_statements, key=lambda s: s.project or "") != sorted( + self.previous_environment_statements, key=lambda s: s.project or "" + ) @property def has_snapshot_changes(self) -> bool: diff --git a/sqlmesh/core/environment.py b/sqlmesh/core/environment.py index 13ca1c5485..2a0d4f115d 100644 --- a/sqlmesh/core/environment.py +++ b/sqlmesh/core/environment.py @@ -266,6 +266,7 @@ class EnvironmentStatements(PydanticModel): after_all: t.List[str] python_env: t.Dict[str, Executable] jinja_macros: t.Optional[JinjaMacroRegistry] = None + project: t.Optional[str] = None def render_before_all( self, diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index 2b40be0230..30c74884c8 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -815,7 +815,11 @@ def _load_environment_statements(self, macros: MacroRegistry) -> t.List[Environm path=self.config_path, ) - return [EnvironmentStatements(**statements, python_env=python_env)] + return [ + EnvironmentStatements( + **statements, python_env=python_env, project=self.config.project or None + ) + ] return [] def _load_linting_rules(self) -> RuleSet: diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 0f896d5bec..23d34afa31 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -277,6 +277,7 @@ def _load_environment_statements(self, macros: MacroRegistry) -> t.List[Environm ], python_env={}, jinja_macros=jinja_registry, + project=package_name, ) project_names.add(package_name) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 8923c4c75b..d03db7af91 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -4944,15 +4944,88 @@ def test_multi(mocker): context.apply(plan) validate_apply_basics(context, c.PROD, plan.snapshots.values()) - # Ensure only repo_1's environment statements have executed in this context + # Ensure that before_all and after_all statements of both repos are there despite planning with repo_1 environment_statements = context.state_reader.get_environment_statements(c.PROD) - assert len(environment_statements) == 1 - assert environment_statements[0].before_all == [ + assert len(environment_statements) == 2 + + # Ensure that environment statements have the project field set correctly + sorted_env_statements = sorted(environment_statements, key=lambda es: es.project) + assert sorted_env_statements[0].project == "repo_1" + assert sorted_env_statements[1].project == "repo_2" + + # Assert before_all and after_all for each project + assert sorted_env_statements[0].before_all == [ "CREATE TABLE IF NOT EXISTS before_1 AS select @one()" ] - assert environment_statements[0].after_all == [ + assert sorted_env_statements[0].after_all == [ "CREATE TABLE IF NOT EXISTS after_1 AS select @dup()" ] + assert sorted_env_statements[1].before_all == [ + "CREATE TABLE IF NOT EXISTS before_2 AS select @two()" + ] + assert sorted_env_statements[1].after_all == [ + "CREATE TABLE IF NOT EXISTS after_2 AS select @dup()" + ] + + +@use_terminal_console +def test_multi_repo_single_project_environment_statements_update(copy_to_temp_path): + paths = copy_to_temp_path("examples/multi") + repo_1_path = f"{paths[0]}/repo_1" + repo_2_path = f"{paths[0]}/repo_2" + + context = Context(paths=[repo_1_path, repo_2_path], gateway="memory") + context._new_state_sync().reset(default_catalog=context.default_catalog) + + initial_plan = context.plan_builder().build() + context.apply(initial_plan) + + # Get initial statements + initial_statements = context.state_reader.get_environment_statements(c.PROD) + assert len(initial_statements) == 2 + + # Modify repo_1's config to add a new before_all statement + repo_1_config_path = f"{repo_1_path}/config.yaml" + with open(repo_1_config_path, "r") as f: + config_content = f.read() + + # Add a new before_all statement to repo_1 only + modified_config = config_content.replace( + "CREATE TABLE IF NOT EXISTS before_1 AS select @one()", + "CREATE TABLE IF NOT EXISTS before_1 AS select @one()\n - CREATE TABLE IF NOT EXISTS before_1_modified AS select 999", + ) + + with open(repo_1_config_path, "w") as f: + f.write(modified_config) + + # Create new context with modified config but only for repo_1 + context_repo_1_only = Context( + paths=[repo_1_path], state_sync=context.state_sync, gateway="memory" + ) + + # Plan with only repo_1, this should preserve repo_2's statements from state + repo_1_plan = context_repo_1_only.plan_builder(environment="dev").build() + context_repo_1_only.apply(repo_1_plan) + updated_statements = context_repo_1_only.state_reader.get_environment_statements("dev") + + # Should still have statements from both projects + assert len(updated_statements) == 2 + + # Sort by project + sorted_updated = sorted(updated_statements, key=lambda es: es.project or "") + + # Verify repo_1 has the new statement + repo_1_updated = sorted_updated[0] + assert repo_1_updated.project == "repo_1" + assert len(repo_1_updated.before_all) == 2 + assert "CREATE TABLE IF NOT EXISTS before_1_modified" in repo_1_updated.before_all[1] + + # Verify repo_2 statements are preserved from state + repo_2_preserved = sorted_updated[1] + assert repo_2_preserved.project == "repo_2" + assert len(repo_2_preserved.before_all) == 1 + assert "CREATE TABLE IF NOT EXISTS before_2" in repo_2_preserved.before_all[0] + assert "CREATE TABLE IF NOT EXISTS after_2 AS select @dup()" in repo_2_preserved.after_all[0] @use_terminal_console From 3a872dc18d36bc5064612f75c2644405489b4fc7 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 15 Jul 2025 09:06:23 +1200 Subject: [PATCH 0574/1056] Fix(table_diff): Properly handle null check for array types in data sample (#4963) --- sqlmesh/core/console.py | 10 +++++- tests/core/test_table_diff.py | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 86de61c829..b78d403a87 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -2711,8 +2711,16 @@ def _cells_match(x: t.Any, y: t.Any) -> bool: def _normalize(val: t.Any) -> t.Any: # Convert Pandas null to Python null for the purposes of comparison to prevent errors like the following on boolean fields: # - TypeError: boolean value of NA is ambiguous - if pd.isnull(val): + # note pd.isnull() returns either a bool or a ndarray[bool] depending on if the input + # is scalar or an array + isnull = pd.isnull(val) + + if isinstance(isnull, bool): # scalar + if isnull: + val = None + elif all(isnull): # array val = None + return list(val) if isinstance(val, (pd.Series, np.ndarray)) else val return _normalize(x) == _normalize(y) diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index 9ea0d64771..64096a6637 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -504,6 +504,69 @@ def test_data_diff_array_dict(sushi_context_fixed_date): assert stripped_output == stripped_expected +def test_data_diff_array_struct_query(): + engine_adapter = DuckDBConnectionConfig().create_engine_adapter() + + columns_to_types = {"key": exp.DataType.build("int"), "value": exp.DataType.build("int")} + + engine_adapter.create_table("table_diff_source", columns_to_types) + engine_adapter.create_table("table_diff_target", columns_to_types) + + engine_adapter.execute( + "insert into table_diff_source (key, value) values (1, 1), (1, 2), (1, 3)" + ) + engine_adapter.execute( + "insert into table_diff_target (key, value) values (1, 1), (1, 3), (1, 2)" + ) + + engine_adapter.execute( + "create view src_view as select key, array_agg(value) as val_arr, map(['k','v'], [10,11]) as val_map from table_diff_source group by 1" + ) + engine_adapter.execute( + "create view target_view as select key, array_agg(value) as val_arr, map(['k','v'],[11,10]) as val_map from table_diff_target group by 1" + ) + + table_diff = TableDiff( + adapter=engine_adapter, + source="src_view", + target="target_view", + source_alias="dev", + target_alias="prod", + on=["key"], + ) + + diff = table_diff.row_diff() + + output = capture_console_output("show_row_diff", row_diff=diff) + + assert ( + strip_ansi_codes(output) + == """Row Counts: +└── PARTIAL MATCH: 1 rows (100.0%) + +COMMON ROWS column comparison stats: + pct_match +val_arr 0.0 +val_map 0.0 + + +COMMON ROWS sample data differences: +Column: val_arr +┏━━━━━┳━━━━━━━━━┳━━━━━━━━━┓ +┃ key ┃ DEV ┃ PROD ┃ +┡━━━━━╇━━━━━━━━━╇━━━━━━━━━┩ +│ 1 │ [1 2 3] │ [1 3 2] │ +└─────┴─────────┴─────────┘ +Column: val_map +┏━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓ +┃ key ┃ DEV ┃ PROD ┃ +┡━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩ +│ 1 │ {'k': 10, 'v': 11} │ {'k': 11, 'v': 10} │ +└─────┴────────────────────┴────────────────────┘ +""".strip() + ) + + def test_data_diff_nullable_booleans(): engine_adapter = DuckDBConnectionConfig().create_engine_adapter() From 6cf1f71ec98b9dac10d2128c2b95bd94e34c14de Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 15 Jul 2025 09:34:52 +1200 Subject: [PATCH 0575/1056] Chore(cicd_bot): Tidy up failure output (#4964) --- sqlmesh/core/console.py | 52 +++++++++++++++---- sqlmesh/integrations/github/cicd/command.py | 2 +- .../integrations/github/cicd/controller.py | 5 +- tests/integrations/github/cicd/conftest.py | 2 +- .../github/cicd/test_integration.py | 3 +- 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index b78d403a87..c14b9be2b5 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -289,11 +289,17 @@ def show_row_diff( class BaseConsole(abc.ABC): @abc.abstractmethod - def log_error(self, message: str) -> None: + def log_error(self, message: str, *args: t.Any, **kwargs: t.Any) -> None: """Display error info to the user.""" @abc.abstractmethod - def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None: + def log_warning( + self, + short_message: str, + long_message: t.Optional[str] = None, + *args: t.Any, + **kwargs: t.Any, + ) -> None: """Display warning info to the user. Args: @@ -3090,15 +3096,23 @@ def consume_captured_errors(self) -> str: finally: self._errors = [] - def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None: + def log_warning( + self, + short_message: str, + long_message: t.Optional[str] = None, + *args: t.Any, + **kwargs: t.Any, + ) -> None: if short_message not in self._warnings: self._warnings.append(short_message) - super().log_warning(short_message, long_message) + if kwargs.pop("print", True): + super().log_warning(short_message, long_message) - def log_error(self, message: str) -> None: + def log_error(self, message: str, *args: t.Any, **kwargs: t.Any) -> None: if message not in self._errors: self._errors.append(message) - super().log_error(message) + if kwargs.pop("print", True): + super().log_error(message) def log_skipped_models(self, snapshot_names: t.Set[str]) -> None: if snapshot_names: @@ -3135,6 +3149,11 @@ def __init__(self, **kwargs: t.Any) -> None: kwargs.pop("alert_block_collapsible_threshold", 200) ) + # capture_only = True: capture but dont print to console + # capture_only = False: capture and also print to console + self.warning_capture_only = kwargs.pop("warning_capture_only", False) + self.error_capture_only = kwargs.pop("error_capture_only", False) + super().__init__( **{**kwargs, "console": RichConsole(no_color=True, width=kwargs.pop("width", None))} ) @@ -3409,6 +3428,12 @@ def stop_promotion_progress(self, success: bool = True) -> None: super().stop_promotion_progress(success) self._print("\n") + def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None: + super().log_warning(short_message, long_message, print=not self.warning_capture_only) + + def log_error(self, message: str) -> None: + super().log_error(message, print=not self.error_capture_only) + def log_success(self, message: str) -> None: self._print(message) @@ -3435,19 +3460,24 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> def log_skipped_models(self, snapshot_names: t.Set[str]) -> None: if snapshot_names: - msg = " " + "\n ".join(snapshot_names) - self._print(f"**Skipped models**\n\n{msg}") + self._print(f"**Skipped models**") + for snapshot_name in snapshot_names: + self._print(f"* `{snapshot_name}`") + self._print("") def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None: if errors: - self._print("\n```\nFailed models\n") + self._print("**Failed models**") error_messages = _format_node_errors(errors) for node_name, msg in error_messages.items(): - self._print(f" **{node_name}**\n\n{msg}") + self._print(f"* `{node_name}`\n") + self._print(" ```") + self._print(msg) + self._print(" ```") - self._print("```\n") + self._print("") def show_linter_violations( self, violations: t.List[RuleViolation], model: Model, is_error: bool = False diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index 1a4982e9e1..f1b611150a 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -30,7 +30,7 @@ def github(ctx: click.Context, token: str) -> None: """Github Action CI/CD Bot. See https://sqlmesh.readthedocs.io/en/stable/integrations/github/ for details""" # set a larger width because if none is specified, it auto-detects 80 characters when running in GitHub Actions # which can result in surprise newlines when outputting dates to backfill - set_console(MarkdownConsole(width=1000)) + set_console(MarkdownConsole(width=1000, warning_capture_only=True, error_capture_only=True)) ctx.obj["github"] = GithubController( paths=ctx.obj["paths"], token=token, diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 7b03868243..56a3c1ab20 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -603,11 +603,11 @@ def _get_pr_environment_summary_action_required( def _get_pr_environment_summary_failure(self, exception: t.Optional[Exception] = None) -> str: console_output = self._console.consume_captured_output() + failure_msg = "" if isinstance(exception, PlanError): - failure_msg = f"Plan application failed.\n" if exception.args and (msg := exception.args[0]) and isinstance(msg, str): - failure_msg += f"\n{msg}\n" + failure_msg += f"*{msg}*\n" if console_output: failure_msg += f"\n{console_output}" elif isinstance(exception, (SQLMeshError, SqlglotError, ValueError)): @@ -713,6 +713,7 @@ def update_pr_environment(self) -> None: Creates a PR environment from the logic present in the PR. If the PR contains changes that are uncategorized, then an error will be raised. """ + self._console.consume_captured_output() # clear output buffer self._context.apply(self.pr_plan) # will raise if PR environment creation fails # update PR info comment diff --git a/tests/integrations/github/cicd/conftest.py b/tests/integrations/github/cicd/conftest.py index 25ba3b2d60..f869dc41ad 100644 --- a/tests/integrations/github/cicd/conftest.py +++ b/tests/integrations/github/cicd/conftest.py @@ -117,7 +117,7 @@ def _make_function( orig_console = get_console() try: - set_console(MarkdownConsole()) + set_console(MarkdownConsole(warning_capture_only=True, error_capture_only=True)) return GithubController( paths=paths, diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index 17e495fbc3..15e8be0f6b 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -1582,7 +1582,8 @@ def test_error_msg_when_applying_plan_with_bug( assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_failure assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" summary = pr_checks_runs[2]["output"]["summary"].replace("\n", "") - assert 'Failed models **"memory"."sushi"."waiter_revenue_by_day"**' in summary + assert '**Skipped models*** `"memory"."sushi"."top_waiters"`' in summary + assert '**Failed models*** `"memory"."sushi"."waiter_revenue_by_day"`' in summary assert 'Binder Error: Referenced column "non_existing_col" not found in FROM clause!' in summary assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping From 5dfdeca323ac2ade64f5453f362334c351aa851e Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 15 Jul 2025 10:08:45 +1200 Subject: [PATCH 0576/1056] Feat(cicd_bot): Allow forward-only plans based on branch suffix (#4953) --- docs/integrations/github.md | 1 + sqlmesh/integrations/github/cicd/config.py | 9 ++ .../integrations/github/cicd/controller.py | 36 ++++++++ .../github/cicd/test_github_commands.py | 84 +++++++++++++++++++ 4 files changed, 130 insertions(+) diff --git a/docs/integrations/github.md b/docs/integrations/github.md index 1f66ef6368..a11d90d044 100644 --- a/docs/integrations/github.md +++ b/docs/integrations/github.md @@ -300,6 +300,7 @@ Below is an example of how to define the default config for the bot in either YA | `run_on_deploy_to_prod` | Indicates whether to run latest intervals when deploying to prod. If set to false, the deployment will backfill only the changed models up to the existing latest interval in production, ignoring any missing intervals beyond this point. Default: `False` | bool | N | | `pr_environment_name` | The name of the PR environment to create for which a PR number will be appended to. Defaults to the repo name if not provided. Note: The name will be normalized to alphanumeric + underscore and lowercase. | str | N | | `prod_branch_name` | The name of the git branch associated with production. Ex: `prod`. Default: `main` or `master` is considered prod | str | N | +| `forward_only_branch_suffix` | If the git branch has this suffix, trigger a [forward-only](../concepts/plans.md#forward-only-plans) plan instead of a normal plan. Default: `-forward-only` | str | N | Example with all properties defined: diff --git a/sqlmesh/integrations/github/cicd/config.py b/sqlmesh/integrations/github/cicd/config.py index 8f84db47c8..a287bf1af5 100644 --- a/sqlmesh/integrations/github/cicd/config.py +++ b/sqlmesh/integrations/github/cicd/config.py @@ -33,6 +33,9 @@ class GithubCICDBotConfig(BaseConfig): pr_environment_name: t.Optional[str] = None pr_min_intervals: t.Optional[int] = None prod_branch_names_: t.Optional[str] = Field(default=None, alias="prod_branch_name") + forward_only_branch_suffix_: t.Optional[str] = Field( + default=None, alias="forward_only_branch_suffix" + ) @model_validator(mode="before") @classmethod @@ -73,6 +76,10 @@ def skip_pr_backfill(self) -> bool: return True return self.skip_pr_backfill_ + @property + def forward_only_branch_suffix(self) -> str: + return self.forward_only_branch_suffix_ or "-forward-only" + FIELDS_FOR_ANALYTICS: t.ClassVar[t.Set[str]] = { "invalidate_environment_after_deploy", "enable_deploy_command", @@ -83,4 +90,6 @@ def skip_pr_backfill(self) -> bool: "skip_pr_backfill", "pr_include_unmodified", "run_on_deploy_to_prod", + "pr_min_intervals", + "forward_only_branch_suffix", } diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 56a3c1ab20..29de4658d3 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -405,6 +405,7 @@ def pr_plan(self) -> Plan: min_intervals=self.bot_config.pr_min_intervals, skip_backfill=self.bot_config.skip_pr_backfill, include_unmodified=self.bot_config.pr_include_unmodified, + forward_only=self.forward_only_plan, ) assert self._pr_plan_builder return self._pr_plan_builder.build() @@ -434,6 +435,7 @@ def prod_plan(self) -> Plan: skip_linter=True, categorizer_config=self.bot_config.auto_categorize_changes, run=self.bot_config.run_on_deploy_to_prod, + forward_only=self.forward_only_plan, ) assert self._prod_plan_builder return self._prod_plan_builder.build() @@ -450,6 +452,7 @@ def prod_plan_with_gaps(self) -> Plan: skip_tests=True, skip_linter=True, run=self.bot_config.run_on_deploy_to_prod, + forward_only=self.forward_only_plan, ) assert self._prod_plan_with_gaps_builder return self._prod_plan_with_gaps_builder.build() @@ -473,6 +476,13 @@ def removed_snapshots(self) -> t.Set[SnapshotId]: def pr_targets_prod_branch(self) -> bool: return self._pull_request.base.ref in self.bot_config.prod_branch_names + @property + def forward_only_plan(self) -> bool: + head_ref = self._pull_request.head.ref + if isinstance(head_ref, str): + return head_ref.endswith(self.bot_config.forward_only_branch_suffix) + return False + @classmethod def _append_output(cls, key: str, value: str) -> None: """ @@ -485,6 +495,26 @@ def _append_output(cls, key: str, value: str) -> None: with open(output_file, "a", encoding="utf-8") as fh: print(f"{key}={value}", file=fh) + def get_forward_only_plan_post_deployment_tip(self, plan: Plan) -> str: + if not plan.forward_only: + return "" + + example_model_name = "" + for snapshot_id in sorted(plan.snapshots): + snapshot = plan.snapshots[snapshot_id] + if snapshot.is_incremental: + example_model_name = snapshot.node.name + break + + return ( + "> [!TIP]\n" + "> In order to see this forward-only plan retroactively apply to historical intervals on the production model, run the below for date ranges in scope:\n" + "> \n" + f"> `$ sqlmesh plan --restate-model {example_model_name} --start YYYY-MM-DD --end YYYY-MM-DD`\n" + ">\n" + "> Learn more: https://sqlmesh.readthedocs.io/en/stable/concepts/plans/?h=restate#restatement-plans" + ) + def get_plan_summary(self, plan: Plan) -> str: # use Verbosity.VERY_VERBOSE to prevent the list of models from being truncated # this is particularly important for the "Models needing backfill" list because @@ -754,6 +784,11 @@ def deploy_to_prod(self) -> None:

    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 YgSV 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 diff --git a/docs/guides/observer/observer_plans-list.png b/docs/guides/observer/observer_plans-list.png deleted file mode 100644 index cbded1fe4485d5c3dbb626602af7de34b1ee7db9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133025 zcma&O2Ut_fx-g8}y%nXYfIw&>2q*{&gd$A|MU*Pin}`C^n{*PDreG-2i^LF>2ns5_ zMnHNK2q0Y`p@_6l10nfWC~nWW_kPd6mORdy%z9_$?PZc@dO8{m$3e$wXlNKTudCjo zp`nwdp*gZi`xnsTvqKX?^Vd-aP1S3+{SN&a`N!R`pFFcXy$DzE6(84Kz8a=ic;o!T zH!`fZ-j7;gSO;%D4xmeJSu3xoNG?x4UiHw|&Eu4xa>rtGrkwZ#Mxobo;|0@_26iut z&K;|`XBTYWH@3Y25}=)H%l_Ffx(-d&7Y@kw<7$81_C21zne-!R;@R77T3a-4&07M*MWCO$a%(o54Jzq;5T2w z^g7xKa?0I;E&Y~tA&}OK{ChoRixhUORV5DMCLAx~air&|^bn5b zl6Y{pbfO`>R?!W5VqyEPzu#u-{qc7tV{|2>K?bt!6R*ED^g?g{F^>uZ@(4h*JtQL= z+&IHS(PDijR>+BM9d`DtauZwsu*k!4%^{H6Y)+#2dUHs1Q12zt3}51#@hsisu(6=; zr`G-7$UR43&i*o$Saxf-S+~D9{>&{Y!buDxDkVkFo9dWD|s@xX6G{PKJ;FT)#;h-|@=B99)@j-v4h@PbP^xL8)C!?Vc zO-so=Q%l3OiQ_eHX0-#0VocjrrJ}$(6oz{%B^1a_5Stl6eu#$4qt8RGlXWSVCshKc zm^dUvpYx8fd%l)Wy*#-YI~Ko=s27J3=avc(I}*6-zF~Tk#Odh>>?h+rHoOWf_k<&r zcfXxRsv95OMbsIrN%6K0ux#5|9{I2vixsO4C#g@(4E!o@Hs-xLPXkY=u`2U0v#~8) zR%d4IBN8 zOfPvIsS=}CfowR$EEwvf?8L}vO){GCsEtRC{hF?|19r@rxudcfBZ)Y^Gp)qC09{G{ z=-At`RR1|?X~oz$bcaop4sp?iky%J8>;}UA`$v5~+bH>kQptOFKH-yW>!uN~lS&u3 z2sxj~*mQ*F#DR`avjh+}wUgE(|~E|wo3t+-wqE14{2dD<+! ztkN6ThKf;=_Ab&xZ5BCK%rhb_`0U?ZC0i_1ZBzq$*M?f2*XaAFE!9iCDHc87=FpK* zhTIPSt++x#fS6D*@EG{pQ*>vr|9?2QWH{hvip1jF5+Wv0^jB~@=Xkv?TUhpf`RkpT9DsH`Z+!?c!iQ1qr7sFfI|7}bHfjNw2|G@1A&Xj_>$SPwdBs= z{02Q5Embau$P$`Rkd85zl4az)wx;x;gM&$d9)FDD`lhCE#{=hKA(m+tgnQJ*hEU&| z8iM>B2?^r#WA1fu8@1V{)I|*CTGRD6?2ZZ)5-E24dWXfwABnMiDnr&cJ38a~etmA` zL6&(B`;P4ZoQI2ND|zHU_n`y=?e)h$w=MR1$npxoDmDC9*|ZgM(?IO4f}GcIDR(Wit^&tYd!R6mL>hY0880bW zB)9!bIW!}lOi1=pfljXsj9%d8V6FeV{QdJwgz3PVf7rtMl&Y3$qeAphMte%CjW{wK zJ19x%YUH7lv)&g^Dut+jyF(s51Y@ase@~ZGyY)QM(?-V`qgSgR#vhG1&?+lqP-UqM ziV=J8g2;IK8Wtlk53-165(Uf3y~Aa^U_4u4#n z2BhH-2Nw$j&ZM+?Daiavw1THu>0Dl*iWt!z7guvlsq@zWhOX>!YgTuh`4?bZUQOFm9uLjs?Tisj%YQl<&|KYr}`& z3yH}<7{8m+jSjP&g^2hs1)^jKr`X7$6|s?}*E zvRifX4ThcpFfkZhIS+C}4F!4gOg%Hv7(JplS>}s5PMQ6ZtI}i5OGji|MTukRB*p#C zSZpkb@HHa*H|CZWv#odp5!-%E%l{yPd8L)+$xA~OR&=>$Mxo%qoMFaEd@@@oJ%}Jy zwx;fij*~=OEYrfv%Jb^W^kGPBO`gTbaXX*J^x#XH= z>y5@Y$yPj!N}C^JB|8;2NZ$sFK^%@OJ@CSh-owEgRY*mo`ptnZGljTuHIKPe!g3q; zorBdhXvwuoR|(VtoiTWc}H-e#LjLv6I3)$95x-Ysk6yso6VrTm6AS~(?R z0%xr{wgsl(l=b6Cj&%aCI{92aV?wiqp^QyN7zpeK}C!S@8 z7B40u8|cI<*;^<^IMcFsH^M=wghC-~iUj^qta&%?A;C@GV_At(U9@9}6Fh&J15oOF zuFuR_9O#44ftsh9{4;-G^VUd!Vyy=Qq;_Sxj{rh5EaE*d>VwF2_GTAO>++THZaZ$SCA$}UCfRk zxNNQE)Z#o387utH~Z?ArCC^*TRNoaqdHv z_K-K7Uf-@ogr~cf%O$|B*mrR7S!cHiN)$B3?QAFuzd<<(> zfpaw%o~?U2tYkY{8KD9-qX(8{Iwjvgp1b=D;W)t$F-RWbv4PN zWIe~j*dhjxg9nZ1B|Pt0?%}ry+`_zPLJ!=jgSaLXbUUvP0>7D8D%MuL)$BfB`41nZ zg*?BqSl3Xn$fPk`eFnEF4@h^w&aa=}rc)|v&xr@KGFFp|mc%!zgtaQxdfmt7OTup; zvOjqsVmHdC(wyp+#!+>p9`d2|{HK$mCr+~?5~JHd5-)_0aQW{Fu7a%W2qOExxao1h*mdV?^p`;6*J#ek#Q*Ei7I#*^BV)^|y!T}ewr#mk0nnoCi z*yf+?nToj$a;)50mmi;uQ9fqwd|41@oTlIO(sz_q!n`@(*MGL@qU9jq&t`i~XL2+| zkBH&8qs&KvmvB9!FiHsDWXiD08+I73^PC2ulNd@2CQn3(htOvgpMv)(wObsx8s1Ev z@Vi-jSQF!8d%}K%>a_1!Iw0I?fNC?><;NQ>4fCC0+jwCO&1qTqgH91`LIZ8($)e(#8H zn!nicB*ZAjKvz*QAp%t2t|W1yIj&CyzC6hZIr4Y?9LDU5#PcR$0o$mF39bfWjJzI^36e!~4L z3K8bv3k5|}eD2FG5?Uc^x`{(+zbY2L#tz*poPLjlXoS9ftI3xi+e%tYicVSLAN20I zSohHerQ+m8=*dXsFFGgVa-MfAV9tXNc6J_P77o%?T=N#T%C<$wl^8tO9@rgq5cC2u z#0XVi7pbBTslbc(97$?uK@Hx%-;0kMACKTYR#w@6>&oeT2S$5L$=P3*VMRMuR`bdE zVsy%|hyLFe-P*@D!$j)Bm=sQ_OViW&;|cOAV5IucRrE4xWyPTB04wK{m%%l-0-Q*%_dWQxGJ%Q~UWH%n^h1@Xt`=6PXt+jlnm)pAgaWK`F z{9_&8SO2Ere6{;F0RF#IA2enE%Hn^eK4P8rUi$CVTUzGF-VE5;>OV@CK{Fyg*hCqk zz{q@iga0!N0_*f-+v}&fI(*^6Fc5 z=c8{GzL;;h#=N`JJmO6dp(|^N6=5vfy2E+|$i^H@wVSS(u{eB$o{>n`Gq@Q>zx%=& zU-Sn!piFU`RDW%o0K-X%-gLeC_8pr3d7nPF#DiG8;o}Zy8qaeABbzetDgopT!DE|6LG_qM0 z!^iSH$}aIa;+hnzV*D3^<7fcvTGawlIW!1G1xr*wb|f_ng<}1 z4DBU`yD2~FYIfOv#tPZ6!_+~CNe?t?iW}_yUaBkm3osC7{!?#v1S3ct+4-0qvBI5# zre(9?)q4d%fNRYSRty{48PE9GUmbtTfzrFXwMTV~+YgQxZIUJ%TzCmY*f4|M({nkD zCB=xcqIsPo3q$-k#8L1X=^Eai6>pj2-(kuu6t=CK!YUdH=}&fZsxI3#EqnTan*X7C z*{{|iaDwWYBThPkAW8d|CkUOE${h|AWm^vW*F`%_=D|DRHZET|Qt#ss1`Qnbx;-(R zE~~RW%37HUVCSOVKBVNLm9?_0R+`kg)rApp@0YoT`PP`nHYhkqWxI3GZxV@wemjEP zv1b4k+Mrk8kJo90R|g*rU2hu$!v5OR=F!LGZoM1X_%8XJtLOoG)nXTKSsp;l_C_zN ztYNs!K0JtA6r68y$duDbBr$LMwL7qF4rB`=yBd0pt(lFACQ~lk8aZ@&DXo8>MEY$^b$b9oaOgyvPN|vN zSXD8@$UVOlpSDlqq0G8BZ2Fdk!W$Z@AVgDs_2-O@WS@X0;_OrYnB!|}oS}q*2KbOz zTgJm6ptF>tpw`G7&VaX{FYFOR`WcagxV)G6kU>y0*iJQ{eKap6K6Wy(WwCB_TZ=X^ zla9K}>rxWl*X3TMKf|`X6?(~$hC6<}G5-ULvCjqLN*&^9;=2_H{CMZjVLjVA2FOlm z=DCM>i7whYCNB1M1=k7|pUh&zy-ABrXJS5Me>~-nKF%RUTFajf9hODCdvMpp>dL4% zw;T-Dq+M6NEuBcWG3@2mlso^qbpbg)T{#}k3F%N?X)CBj>!*iRER@URHdkjQRXP?! za}8HVoQ9sK7S!r%=+ZGT)YSqiqIRJohKWPnxnhKoZ<>iqRu*Rph>X$sRM&f+amKB; z&vv%R`2+GKdkLrO9>O5a7{L|b*Q6Vjj&v>`IL+6Xtu>;I%N}wruxwg>7XLOh!#W{7 z5tpGY&2e(i7+#%@VC0Z_J>mDkIp9`dF$8XLXtqVfV%|*cl%q5FOkt7%ecHEOF{rbW z^r7#pTvzqOy*XNEJQjSin&eZ&)vPPoZvrF1fW$;!9>Uj+(vwMvLPQiWDN1=`Z>^TF zA=s)hIMPzP+H@GnOYA0w98pl45|29BPbF6#x+!ZoF2dS^^(-U=9h2fd{ zMI49{RaF6Xk}CI!+s@9!&dzRX#3r+~t<7z=Y{S*< zrK6$zXRiBGhQUzx;D=bpqKO-h+o_5*Z-tsEp22$N2pk?B*CwNQtVCRs$8jmAP3Ed; zxpx_9A}`>m`_MHYA13Zhx$H3^V51bUD$932MbSR)+YKh;l{Y>6Q(JrzW4DV5nJcdg6|9v5>>9({QV;X z)!C7$X#5@bCO5G7=AQ3!S{E1v5vfTi7QN;KrvAsh5b%eueI+^4R3&n1;=3a?0xGd9)n zILcz1+*;#HWA6raF48K;m@f_0h6`c_Nd<&aag62YPJPL7=FI!|{X$PHW|EvS{)CkG z3o|dq8Ng!smLgg=>{yO*zH*P+T7L_Fp@|;XnC`~dUeQf2tx#j5zGb5dP(x&I_Pl+UZ)O$TWCpTIQ`wM<~DXpvl@znFpxjJ08&WA zhI8Xbgj4AZLRL#Zkn`U)isV;bnC&lcIX)D1LQLS8ya{E!1OurBE0dR*2{G?9q@$CF zy-u#KtP)R0tJP|`=K9=Dya?M07qk&MJyq#Qz`d&t_$#bb;R%DbpCJSiuR0kr(;$q; z8>;kOopAs0gU?3Mr0l_pk?Vs>{8fUOA{AHr%Y@9$FR%(1SMoWe07vX^yAkhfD8z=riV@&AFp!45kNzZiwJHg<>?lZC?tBXC%%bMwKyv2Ah@d z$18ped`_(=5bF-tf*Yq}=xIAY6Krbirz7z``&_ioXuols`Y-G4F`D{jKSWTSoF7r< z#YX!{ZAx9KzX^IL&K!BfJSo=cwRMOEBN&y{YoKms9V^i2=C;TZ z4O^Y@IMyHZ(*Th~xuN*kqp&TX=j4~#Q8fvFg4D5|+TVc~ zBmzzme%Ba7Qw5grf=LsCnjzkI~*nFHKob!_>q;)Yf008ogx11 zbqn-mmqD##oyCRkRb*8(HOo{X3};yde^lHFb~2B+WL@A>wKQ-{DQoO@X;I_>JEh`v z&^3Qw$J95690yMjqHyM)K1ygwc}ik`DEHtQ2leEz>^Y>cm?q$3zYU!Iwy#^6$ZPrm zMsVp)r}nBjr_Aw{$=0M}URFgOeO&(J7PWEpz%9KH)sT<2F7_oEre(KG{GDedY|e2U z(mcf6_Q8pzuhY%p)LD8#9KZQ=8*8-I-(RNqxyG@Ptfxl4--m2C6FX*^Yq!!SJf8vi z1M4e7_qf~!iVagX#LWa|-8wN&{W{}KH_ZlLzk7cpAl)of?$}@b0s*AMRl%oxNv1_> zwKt+0xW*J7T2v?2;tY0q!;+Vjr+oK!Dy6W()#sWKA^1chl=IlE& zh1z;1?Si&^i9d}of|R&`=eRD3P7A8x5CBDN9es^L-Te6zbZ=)1uA=^B>U^amJ_lTz zJ5lUAZs@#JQ~BzZNeZ)=lyniT#%O^rBGyUkrl{p(+Zsw8Zh`_?PXMcnyH>Z z8y($EKps=R-eFB`v_PLB6&QrM>R1GIpmzJo>{;Yrd9(H73E2)brf1vM>|Yp*Q& ztZqVSd$Urjy_FQ{*z~hQY}}9zly*)GOY{Bn>)djkr>sgJePiyOa^y1oeB0!ikXT0C zB0}+;R+DNIOUkRPkGG>QnhOTyIiPC-Kb`t|baABWX3AGS=5a}hn)G7VlhJjjFN~*p zW!7;z#>yvjh@PJM(Xw4x3mX&PDVflb-f25rCAYTEWLss;Lw&uT3mAyP5LarYI_s{EA%62khv zCl@&Ysu&95dTe1U0D()|cN{CL6H;2%wBfP;^*xAKf}IS*hWL9@QUkv3@Nd7tPKw`( ziBm9}LaG}WM4kAXg@r}4G`#6i?2}{Z&rTSI&JCOnwN;myH7N>!pEw72BuVbJpy)Ec zW(k{Wz^DvV+h$*SHuPQ~pltxq1AgJ5pm8o{A)V9F>Z+&M99P#ao;Lc~O|ik zXO6nR&(uE+2c3~IEp`r5Wsc>R_dVvczdhhTr24uigCNqqBEH`dUBk`0K>mVMKSlK7 zjpGXiRmISnHXN-c==d)tkBVveNfWzv6+uKv-ZAqB^ls>DU%#|_y8iSs5Abmb`8rZQ zWheI~2DNh7KxzH^iAo0hzqaw?xHBdg;rwxbPMyd@{z5-Hy|DXC= zE^*5WfsB=%Fp#w(R5fe*ND%Ie5}zyks9uPq{a+vdXt*opJBCS z&RE*UPp=}6jOz>|qy+B-rbXPJW2QO~|CeIN)BFzyf~yUj;qXY7Ki;Xsx|x5a-^*&0 zy%~p)z0caK5?a&Q41q7v@^d&k4;HFjm#W^GU%dXpwwl=P7KcmE3?&P$ZEj=vpP!-? z2h=}RT$#XwgI7X_CV)VoO92IWn_7WqFX}5t38FKONbIcO7Qx49~aqj$ym$0&a4p%pO zn*KKIEqY;!z9u1|*#J*JqxJDswy~@dXVE2@11G8VJ|pBnEi`worwhe2|BM#n7}c933>MYC zE=Oy92k{RtJ(<@p9q1G=I^p;+>^Sq;W1~@t!-FyEOx8+1CYcyd{ggXuf!S2@KBjB5 ze=@-m-C1^D9_u%>?xM2rAX zzWk?39@pbP>ct=g`Vvdg&3&SJxUIk*i}rX?&!BfI6jXV^PdWel3+Dg=kgdN_I^RSFjL61LcXG986q|TrvH?>^PEnC1V8%(L zX9T8qN9Q5}){fKN#OYBUVP&L_d37IkCB@%8VZ*|j*feqOW|)28_G!BOzs#Xr3Rv;P z#NJvTK2=s{@7f#KewpJML!}-C2}y?60o#twiFL1Ixy2OS3{tu&WqRi@TOZ}N*=3Bz zgbD*8dT{W_0|)2RPo4#DGttgXsxY|x!*9_^h`cafe$xFOkK@O2x~F3f(l(SD&9V$bdPH3uXFp#^X8B#t?5 z|3my*qAb9>-EcJYSf`*ei0aBOx{eAY_vI!23Immv+QHomGTa{%=e0S*hE|RSY=<&% zr^NflGKM0LcnP`xPl#1ARVa* zayJ9z?h)qdxHt-?93=1aWJxz^e2^w=>51mn+WWSKw?dob4cHkQ$E8_am^xX$28Bl7 zuGQAPu9(tDEWl2V-HOpiH2{9?<+Y~`X8~Jpfq@!)_AI<}Gr=AatP>uvcAb}&_KJK( znB0vUEn)7D!DnbqK6{rbkT)G{6yNHKpZI*F&$h9{Qo^py&*t2>uC8;hkG(%?o+Twy za$KZNThoMDu>>z-bi>c+{dqC5_)FLyvQ!_^=Q6_{K=t;TQ70(Oizh*_!bJuyOtu z3Ln`O7iHcA>lPKxzdI$^9P>q<0O%4QHS>s|jn!k#KIoctW@n94u1YH3u5x|(5D@KU zhre;GM2XMf4xuJEFjTIIn>@1)9yc}WOE#h{SvNiA+N%M90}i0g=i&l$=mjjhtiWp; zt*duxQdi7W;vd&Xg@H8z3n15cKHXGQ^%&RuXi|G^fl#tIoAjY)R=V8`eXljlPVT7F z3R+bL=+}PGEzMDA4X|pJ;dDJ(Cw=+-SHKfENe->Fo%XR&ysWXR_1!LP=`20CNza67 z>Z?qU6W7t#oVi!ZLlXzCeJjxMvxauy97t0H^Iv~@cQ~Ja-9Ed$K=ltdfgj-{+jYG< zQ8Ob>S1J~vtQkJXtN8VFDBGF(I~|?ZGrbimzw4ZReu1XgV0Lzvxi6-zOFf21C>2(| z>hxXKuKV@77o?`I49>4>1&;MGtr<$%pT03{IR7*ABBeyG$>A9tI0Yz4=`_S3gtfvW z+f64rguDdAYdF$=d-f2xDD?Q}=cQohfKz#GT)tO5=arZ7qgG8EHYvzIl^nwpk|pDe}pw2N!Z* z2+Tc`&WUOf;6yypzLIi}Gd=UG<)@`I0S&7?L9`$(;@F8W9$Rci(|ya}n6fT}2mQ*`>RVFsz?s;CGDTd(rV42$*Mw+f!GpGcDB@3Z45IA#CEHbzn3lb9G} zKfPB@y>q*7ims~FTDxvi*Yjjm1UKr2*5|dRBcViEa8__HxmzR8V{Y_Rl@O>b*@kDj zpkz$n2mvep^^r522g1-0JR7g&Bxt;wbo^4eUzFM}&mIE)Z!r7j%wyEYbN5keerZmI z|J1Ibv|k{hrp!V?)Ds+E2I*dDU8KUM zpub9!G!-$IM)Q%`jGx;Aa8N8VXzC46BuPMkH>Ih@*}Nl<2 zKg(grBR~Q6vJ~j$#HGC>q-~MTGS2>|u_raWU-pX5w=()zVoK(cza?&a;)8GsR2AM5WIpJ`qR#8g!*!RIVi zWXZ;&E0Ia5%USCZ`;JW#pXSh|kaX#`nb&+rxf@LgzYLTIe+_IeWCRNKXu8a;bQ$0B z4zhF3o~7;34KPpMtHl8m9GqtTvz6R&aFl}P%R%c=^Z5@vG(mUS4>kxie!p7?e!6;cT&e^QTYUZ(vT^AF<$_meFIv1JLcNueWswX z0a)20xL*ORbwExLem+=o5|tneN>Zt}Tj zQdgyCdDjzDoEL&pTc1gOZvXIhwc|*6byU%ny|sM7B?Ry6bRB*;^Gx9AD_nLg^2otX z+y34~tLJTf<+3IY4Y^-U`Ne^N<8Rj}JLwCfFfDF~x$J)%;|@BG=|;h`XY%|pvFFyx zNrmsAWOm0#$cEKDb#Z5RHxVp&SyaY-Z%XC|j(-SEFMK4UHgBysjtWJb+1H$)TqWVP zrA<%4X1&AoV7t9SG0iRl@fj0Q^5Lj84fQNG3$Gt{t>-yQO`t`@?apCaJ@;Wv@hKMn zVND$wyD{KsAB_(jw7Uqogk|SNJndl2j5(t7@>#eTy+*;>iHJ|j&xG@@OJD)$ z2Z*D#+DY_af=N7=u7d4|-s*P|Mpf8W{Y8e>u7)@5U_wR7+jC8PYW`m}bC0&z>X|Xc z5~iCm5k|H{Z*Px{7oVRe8=3d)okO^T{xp3RCgr=_JyTAkgCXBI^{XAu*3s}-5uL%! zO6rq+vO6=QCb9rlx`e&7&n?u}H~LP!$pYVDPDR37W_`I@2j3Y@tMiMUb$V$*jXy0= z+qV3l6)v5fTa(8*r4O677#p_w&aD~_;FbS=z2)1RRs_;IzLf_TR3!x@5$E};0cljhrly#BuqXR zjE~}Su^6;Kr61PIi7CvLjH-NrS8Qi0KpRmQ-vz*An8W? zQaMLQ$}CyHdWaNEph@sVcLmMF#(bLN%_|9X8I3niqrVoQh(%Y&ARCOUm)mC+Df7xG z4mqr_9f$un&5F5XFLYe~yd63Yw$S`o!m`r5Z*Mu}cIZ#u^W_fXExr1T;6deABb?ld zmro>Gj}%d^7J9o_OTp@M9CoHY=<^^66zy{z>YT+cF$6M6Q+jcj^`*Qri{WA?#H7#l zI}6@;TiWFrft!%o^=Wyg_~KAjdh&;-r{7y}y9|`cgSg61Btqb}hlNl>C9Y9Z;?^7& zO$)>ZVKeRt8#fPjPa6*EVc$i-9kdOakHay@qtX2~?4G6x+M@NS0SxA~yM92u zguBRRWrz+}KMcgq>YtH&UJ9+rLc8@$IdncI3u|z$B#FgzFk}MAr0VvJ`kScw+qb?&F07vc;tH>;f;hE?2TilMg z9g(d*4f*wjnA7h~Sn{&b70W(6qMVYC!H_$)7Q1)!Ci!CgyjL4as`vxG z!4xlSMZjQs-mWdWP^emhub5T2Xp67kVuaGEos|$CkY!4D6jHrI)YCJL|9NFMX=_Iw z+l2T?CQtDrD@T{*sS`r8x^`-=9-wSy#w0;9EJN_#N|I<40Ubw{k5zzO8w zortWT#*GaPDD$KbPdyo5^fU3w^WY!xV*=L)F~<-pwGbEm(JIIP;@2H`j{vb1z9roV5Jp=d`*u=L(;CKgi0>!~WbXOatnAfAy$a zsaU{F<7u_*4!G)v+LGjjNFD;bsEaP|JB!Px?^(y*9N4S$htNAWdNkGm-c7*V95sM$ zR2D$5=qaIHW!BJ?bWw~O3aP$qSq^-;$n3LJ%N*Tl@n?iB?G-lFMqPSUe|#Nc^LB!- z5@A_4(rWu{iRuSz2vVKr?yZX&>@Tg{l&}u+-ojW?h2!QBn8Pb!4Bs6N!Y2}@-%++7 zkj?j0VK}H=-pln;3;%Vav0}b*kMRlNqYVC!ntpr>6JG2LQFApZmAwM9k!=}QD>9dH zd3HL_g1czDx~`62KU!!@+BKRWpUJx`&|cFKS%l3*xu_)7-Z}jHd-v{1~BBFDAt2b@?)qO{T-LOG{iKxhlPXS63E`%j{ zYeVIx8DM8zR}0gPq(F{X<2KpBk@!VZWs8|LJ=9{~n!Z)1^7NWhKIU@EoHBWURsZ3a z#EHkywFC{H(AlV1Kq3*)Ur$os}w$jiu_hgL_YI_oQ~R!B8Ic9fB=H zQSKFP7Vqd+1VR^|AtH+xluvTYE3)jYtzfu{Dky{ZhfsY9%7N=_Sw6l{-wqf39sch+ zyqwc(TN;&k+aClEw?#lwjxx7Lfc)QBeo*l*yg~Fefo>4E7XWT5uv*G;7;P_6?F?*g z&8mbp0_tWIM*0f4Zj>IFl0ZQ3+!%XN_k9jW94&vocTknR2Euzq3GboMp3U=;5lq;b zqZGFqk6`v)yf1qELUjG&CM+MuD#4BtSd*?2r7gtr*BaqzMj>4Uyv5jMph$kOJWw~m z*l4(1$!yfu8owjIu@D2`$QiJ)EykAYJkN5h6_wmCwB%2z+D!Y%#L(a~zVW@IUDJg!Ta>B>O4lWs!(Pi0Rj0F$GiL|Tlg{tFxq*YNM8n%SV< z0k07KctDqad|}<*rAZc$YceVKv81{{!b~Qq#6Mliy8TFT*H5DdclX{bR_i2wG3=_d z-WZvAX>EPNWiU{|(NJ{A{Zhb;#*{i{d-se(qgjLr9mGvXAYmc_+zi zwL+YHZMDA4JjFNvY$7lhWu&iA6ltGBD`zPyv{x=58_7*Do@{c<-tsP_jE>9G8>NPms2mfNSgYp_QM0Ug(V zMt}yoCJ%QFpKHwuny!Sk&vRz|JY!o-|9^Al3^uc?c?KW2@JuPN5kOTRU7p;DomsXL zi(k3HhxPpxwPNFjb19$1KjmxWTFY=;bcQC)j)`;-q z1!2UqyKB}hF25aOuHM*yDi!`WFgf5NVXE_SJsf13+qcCJ*w{-y1993yS!MQusJ=oR zP1x)roi))+YGcYgEAISwi{H>@Eceh3^6W@?&K9!O?SMI8csGV1`c} zef3&l^>x5%^IS5cYNv?|3%&w>;x%~ZYWqpf53Vc`kFdM})KH)bFlf7A4p9yDE>!9> zAk2sLfM|FWb68=iMNTcLU;aZ7g5bbP^JtXnO6;W@4kE-adXN`L^_B-}9ilDGYHKIr zNnS}BZ&DoEl%lWAUSQr!0X!1Z(s`#O{4%9f^5+`LRI^2w69OOj=OE4n7=yzx!3!>! z>d+?m-n`%U(@ps;0ecQpx|zL@B^Fc+Y}oZ}69IOYVhU zi`6rpJde&cBZdAm2|{mAO&c8EbX#9Ibp5k;`{dI5+PRUBpB@*v3<2rw;_QmXp5hV3 z9-y6QX*pZqHdP_}i4|P(a=^5c8Izm;nc3G)LXYy4X?LS16FbGnk*pQ z1t|_V87=4#PDWS%?!mVg{!9>k>5;tkLET>5Dw4p49;g>FMv>MWi_D@(pG~{#62{nx z^KoMW_{AhrakCo_1YWb3aHZMZ)ZXWGf83>%8lQt z5=DBM?K@N>bTgil(ug)M4kjUf=w7(+0@?6tk3KZJ6x^SJ<_>y!^Pv4Z3kD21=<}QX z(q-Gpr};wQ_dgCth3tQyG+$U^}jGKia<&ctGQTE2&w% z-)*4rdvc)hXZwEx1Hk@YK>}d@FOa{%0I+|zsbEkeANppbEaq-=%u20(#joJJrdFn% z_8du?RVez*B0g!s5s;O1wJqWv)40qD!WdJ-Gg&ZIGpEa^5V-%cJS@q`>;h>#87N_DG0&lsV zjjTRD*2({~SrxV61>a9WQ!xejDRA;Wn}&mc0Q%c?<@KesHJ#q^WfSQ7zEtjy{CBVH zK+*7@3bU?$O0eQ!P!_jh-^%L78zHf)`{Ga2+w_5{NPb!;6xf(^zm@L#oH zPxyduYim5%bu1Se|Lg32e3`eP1jk!3aI>+92_tEov?_d0Yvh?z>-D>xS z5sIO45J>YJBzNh5QAu>5{HdJOP_H*>3oSYwJnZJv5>EyWo}_g@C+`IQ?8*!KFr$pY z@a*)IV6znb7m*F{g87+tM7{=$c)v&(;eyA2QE@j;;`xQh*A>mSnBWeh2%oL={5&;D zXvN79+5TukoJ?m4Dh$^9l)NS4q^~qx-k+{-ec+z&vdf=qITD2aG^Hofa#NS=Xe0Tv z1PI{;q1((kU5Fs)#TL1GH`rb9)=S-=-cXo4$O(NJZ#>tv80-j6+c^rK!SkXuh#3`Z zMOi^O-XL}G)`;%xYKgT$6d5mn1H78jY9x25qom>-$-FMxr6oyM)yRJOu$>^m|PA2(M||FS7->Boc(O4 zJxPZjK*c9tQxU-W5sxR57X3Xm{n5)mb^A~X&E$!jPF>2uH?tK6t`u8ZwFpJFV_{(%Sk^p=Q z-zZ@^it_d(9Qf6nv5Q$_ltRZmVmz)I5n~V3Y#G2-+p_^ueR}-jkg|X*ZcC-X z2R#XPQnQUeyI>du#zi$86ug%f`fr)Cz4I8^kW5lj+?qk;sFg%Sp(zm{%DQEd!*+(< z?dlc*4Ng=9Xd{nxm+k@c*+2EIG6`FqSX3jYzN`Gs7wtD<@M@$Rm6!di7SUIb9#+8eMzMI!? zKSvHYs}V>0TnA6Fia&iocUn#I^u_2%HXy~NAy$Igpj11qdbvqwDJ)Wu4dxiV80bcP zu6|}4u}(c9hD#CK6iC?DzvVk?NVhOuHk2{6XhR1)_VF z%EKjM#XxOjPni88>A^SX!BmO;Ehj($K-BEiypsE+!D?S?Df?N1Ff2`|UebMjs$JOn z{vxujE076Hh;x#50hag``wtx|!--|}IZ~Zzu10UNW!cAj!|dcp*{S-&JjFxFgI(X+ z&$h&8_pbHKJItt`RQ!q9uUXN9d0tY>9@KkpIe=ztS>w#gdfmI$QpnNeBx$E6m)Imd zKg_EA5_2i7+TEkjb!gq;wHZ7zkF&&8O)kgJYkk8O8Cqw{BKJ6k3JC|gB2M?MZTEK?o{=kzYa~3YfaKH&L4Xk`2y0Y zt18JR4krvI8*)mk6VFHH@dN&@7m(}iuqZvzZeD%19J*rUoqy}u=?_-ScuD7>0+|*{ za)3M*Fo6=?572Hos+~~u$%w=Izn$?d1YmhUH~zarn!~c#>vF11jl`fd#cPK^FtA=~~6qnUp>(ZceEh{p1?av6oI%!vSU=@@f*Sx1ktAu&woSM5GdZI16UO$k`;__#) zl;7ZeN{GwXL2%$V`5U?DjPjxh+5R3{9omG~B@hOB@Nbh>xpjS2=`FoRUs2Y5%J-)W zRu{8s0I$t{--HG1r}L@d_a5Qy+=&2*^t(MLT=rdLP?LMqWX&p5-V*<`-lIh?Px@+CvI<`f=D?371T3>Y%1aX|3B*9Ix4ED zjT=SjPKl9jkdj7nXcUD(Qo4~2=^j$Lkxpfh7Nr~MREI{oln!Zzd$3;LFV_0Lb^p27 z#af=(r}o+V`8{=h`#Jj$%M4;c`owRPBM&-~4!XFmcy6X;Jp9}W$eyh)3T$Qpwy^4Q z>sODZ@PDQ4{!zw&?PB02^|b3QWmP%Ij|(Z$R1kUGo6{oZX`Lh!oo{mmWc;$%OIFP3 zb=|gOruA3ZWDH`}{F24yn_S%00Wq6;FT(!tfYJZl$c(Z7zQS2ntlt`?E`P&;NmA1& zBe!wiwS?36_f|&$7x9A)Bs-_|g-HLQOizr0Twg}@x_mPgKsf%iqggC3o^7?_-FuQq zeSJHrDCd#NuP*;a=}lhfH(c6*T($s{NSF4zwJg>VG9Za(~sRsu1&j*kUMumv_S zwU-B;&cXM67xP&S@Au#W6_n&xZJQU=Xv{%?ia@ zCjqSl-$<|ZF$c8la(5YE4CWhLg!&K58c$?Bvv)t>ndlPI!BWZM*Sv zt%1%c4iA6(bK;<%$o@y`^8j8_F*d1uhTBfWfppx2h+NlVbInyzJB|sACR3C$4UeD+oEvzjUJ#CQq)w^K8nKmpB|3_b#ARSutlS5wefy zZyg-&;r4ddPs%YFPcVmM8`k?VOuqxNNi3=529d-1O(d&Ic&c;Wu=qEFh6JnEe4e-Z z-{Rga@bQtv5#WKdjtFG03$UI&J8MvRR85Tww_=cdKFrQ!xjF(6SGabMl$!^U`)D5Ri-{Pox5)OxYAYd)Fehw5!ERDTEp zKOypuM-7N8L8bcz*qjy_5a=e?|VgM z{U@pdBG;SH{cp?##3y4czlWw=;@00u{ORQXnNT1O{qHOO-tljb|J}ZE;{Q)N_WLsb zF9rd)4-jY?ZhZ}jp_-@({dD3V$WxS-22lBxhX!$6i;kPtGu3K}sh)I?((E}4BzGYS zf16i;aPx=gbW`i#Z+}&h|17G%sWR}V88`%}Fp=@=CCW|xzCX*{fg}Di&)jFcU!kF&0YC6pj9P`Pt%3< z&;5Tfxqc553oTG1aW_i8*>}B6S6?^wPHWJ1R@UT@_lYUrA$C=MaQWh=N&F$ss@wlO z*iDI_KM&D{i6PLSAaB~`%~?_2HH{u>azZWn5;yY!RmtX7gnok)Uy6!^-#+j7xFq>K z!;E+JAMOksHf@-h+i|9_;O9t0t|o3H2UTsl?{*CvHdEgf_oq9~!G4-zdY0w$Wt603 zcPmERVaO1=RLRM{xNMk1xwqlj;_hIV>gZ*pVKWJ4$1Z9b#9ZgE99!0!1l@6 zB@+(+?x<&IPjX&v?`(HPL|p0|JVuPpf;K5ZF+<#3T~E&O9K<~cE?SD2j?L|SIdV7* zDH3;AdQxh%cPBlzhTtzsrT)YVdk!~6pnjH{_%&up7y^FQcClIYKC5Jnj#u&=IV0M1 zIU_G}O&x_(@}#m=Muxglw6sh;dHojEwj}R=Jk-vX^uJmB?hv)=t~>GRj?K62SPlQKrWK|UzSv$_ z29<+D_}Qt{x%Sq-yq#b582?I~qIC2LM`Fl#Rnyb0dhH*cBFw!} zxo^rCQdrI)^KhYVJUo2UU3>vvuUF|=(W_@!VX-D>J_b|?>zj8os=nLk@mBT@JUqBZ zt+nE5U=O@cR#qPQc+iwrBANo}LbQW9xEMzQF1Txxr*6|W=?0)-=*m|}!v``Nm zx@c8&9f);Cij7$MS=mRM&&G{3Ny0Zk)GzNaSyt#puO6l`lQ4OXDL(dAD5bPMiizoo zNND=bBohWaxbbt5l=W?7nX1d-Lo)||$rKg5ERnnSRvC6@&uk`4S$xBQw{88A&ks10 zxh?BGuH?5Q{Q(jAjj7+Oi~m9-K()Z5A_}LU%%@3`S6fFna`o)nqg2zgYi<^16B=Uc zqu897JH_k>kK^yIrGSN1_;8H-u|H3K9uUP7Oo8Dp>Q)bmfyBEfJ3Zq-Ue{opwpk(RiZpv zad8p%vH4^Dt(nj_lZ%UxtuNBiU8~9367!Q^?|L79*Wjq2?K`{yCZbo>)XtRQCsZWY zR|aiuFPC0x`{5^6S62bEhGW(8)+T&taHL1DrpDyV?*@0PqJ23JBs-60^-32EDgKzX z8^!exk{I*4!`xQKwhe4V>)ao){z`}cqhDnw z{wOSfQ`s5uIyrfAc*nlJIq% zk1GAE%7Ny;BmIA81pYs9ruTKv9F>(Ij+_&v-?dPdT}fSJHvhAfO(e9=;RXgnAEfsy z2)eWQ8N6Q-zIQJe#Fe^fnfi|n@>i}>*%XXMvR)l`cCQEjXS9)7f#S5k{t4^D2ru|= zd$i0J{O8PDB!qv&Ba;?hH}{6HtuvjIQ{f)%^zJFe5Y<2*C?mxf(nN3Te8d&Xflu;Rb%|8$VX0jjikK^AR z3}OiI`z2OKyb=?gtM4f1=D1Z-uNkkC8)e){(TBwtBZ+UU_^-Pg%YjnyOE@W4^>x~x&2?B*QpS$CieO7O2o!ar)3M*e*gxPJ9PGsl;n2hpdZ z6{h`$G+^;x7rwe9*3a!9zgGS!q$`jQpOgh4@BXQx8lYvRIXZY2mZz!uUBmR4VJa+GEpI4~< z#r8<|yosCJ_Fw2Mp^2V<1#Jdq4}6V-ETN7|Qkb;Ulu#HcwC3lAhKGOU(&Jx%r(rwx z@wD$y%dKpkMYb)++#w_+?Bf=}HO8!0Rdq&FZIY%Z<4%ivrGpY`99U3+Hj;%td#`oP zabGcmIksIMpRb{}@xoj{xEbH)aRv6vS8-+tbz@uysriaG} zLsVrhn!<5O;Hs+2B!7(V9UL)Z4r9qPvbyHb)JdGe7Xex3r?EOyf~i>_~5^UO~U>}aGIUfC7^@fK@GT+?d&V(u?!dNnhu_lUPYp5gTAbQxWLBD)m3Fdm?2 zi+8E8(65f#u-7$uRI7k&8E7tQBBhWC1>MLyAUUkCJU~97pc++G84@^#@9&nI{lO@x z0ynZF6FX{~n;Ux51RR)d{t@lhL1?WT!LI4e6Uch~1OctN-6`bwO`iv#x*2JE2q|of zoqePk%Eu=i1LBCk zc1WY9wnDkXblZ>~_JnW=yXKLEb5ev_@eHA!yysmEyH9FH#f|C@j;_Sv-n-Ln=M>}+ zusj1Hot>Kvjz!buF|9?P43&3q>m<%nt}BpB+f~qQ$M#eI{~YyErl(_z)E5UHEN(~ zvJ*Vw(ZA|W1Oa;b09`?Hj0w%9u`_+%OL&w%M(SUVhuGJ{h9`exP8Qd)< z!4F8Pr3CWj6e?z2pKK?Yhfkh$Dzz1~TwY2mrO5?!(?w~SN7 zxGqe!pPqi-=NPbN9;C9+neSn-+ zxD6P~da|-suM>=$J#sY^po`BUV;)`h1LoSs*_Rs{Q@1&^^~1Q$H-N9B8~J6McUP(Z zv;`@O`_Yv62yaCH{%EIUERq3(eq*(=^E0%q$!eOIPz7l}FroU61b4$sw-t%EUX3_N zn}y2hA9?=+g?2!(0#NY$XIC)NZzzCqFW7Dn0|f!v7798h{?#57h$&HnPYE{~&`nQw zbV=lWY*D|kqse?(m454amw}NjwDn>FWNxr&cgsKWM>K>QL(HN@VutUs-3r6o?5;^& zAm_C~^`Ph#=czjtvNlakJ1z@%5_ESmd&lH9GI6QDc*kvjFm5;7TMc)tGuc{^NoR@yE=Lo6Ydf+clT2P1vW&{5d#P0lFDiyqX8|UTBVA z{V8n%9d@ztJOZ|-$R@JDr1pUaHgJzO*a)qW0E2t8ZDswgVkwKLr~;Q)fIY;IGSBj% zp*R4?x86Nz;yloqnc6e^@oCR=0(C+3tn2k1B+2rWr)2ZJ`^S_l&n4btaKRu53;v=J z?-nX79|Gr{OQtsZFkq0-hNe(Ui6!f@AH{w&@0`Uo-g7ERtF(6z+lywBx#RRLWKVzn zY;S{`VR7KtCE(E+P%_L8twq|cUCGGzYYU1|R>6`S=RLqqwxwLUQ%{`u36UU1=5&zi z<6nGp`?Y${Gt9Glkblt3!Xi|VZ|3?{+LS#X-?CKVrGEsmBLnYSdUK^1&5I|QCvHN? zYN-#ETz8$A^Ny>qYq7r*aIH7f#A-8lpsTlJm5sbQR4G%4XtjQz+$3)GvhL(8HDC3F zhplOV;npnyJc#wIc}nHosmHoC3VUCbdU(+i7+=-BDC$*!W2BzmJ;^3&I^POQL|yIb zrlFcbJYcSmG~lyFXtvAmjQH})?P0#(?wMyJ9t>zAv#L5GQQPuiW`Zm2{uuPq6WKo2 z%K64w$1cIw@*~tYQ}C>#=P4?iyfv>xWwd05%#OGhc+$=yt({P6$~NUwfH#ppK4#f8 zZeo3cNXbd$G>5e#K840sLvITxw5Et_hO-X8)beyE3aw_%G6O%--(LJ74<27jNPhQ+ z40pNBxx=pA7PukL=L4HJksw>D5evA#x_{j1$V4|*&32i0e)pPh5$bANpjXqhW&Z=? z^~XCayG@fS{2TmFD~*x%=@#yD9IbJKVex7)CoOw_2 zS@moGRHqWzOIPO&I=h5UFDuu}GR_MX?Cvzl=RDf*O&%GZ$gbL|({<>Pw_1Z;LRBPg z?H8NwnC(87kCUh~D63J|aEBSomfXwkZEvVbL2ym~7vg*JeyHV{kk!v0W`HK9Ls|KI zOTB^y;$CYJM*97=v~0tczys;=wfa@tyU40R;~pH2Qxdpk_04-(?h&<9!rxAawSN~< zzWEK!mHx_tr9!H-ruP$cA)Vf{1>N2_pBoF^V*b| z$IgNm$Tf}WbiVAf>i{<~ieSLJRaY%xDH5)=~&;<6^e?)kOv zH0E4KcyCD|!!qx3=DRugC5Bw|q0kO?OtZFUu6!qV33r-P(F7T;tH5-?nf<9r{s)a%nh{DUvf32n(CY?&&heRTJJrFZ&epQ zQ*F7nl~;{1GM2+NOC6})#ojq~7r1_QnBXA-A`0YfNw&=6{)6{+qPIPO-tgT zI-)H<%BMW_R1>w%xNJ2Cy(ugeuY>0&V)zCxVAM%Iyv}Litzl7O#}JDBkokPUNKo+M zMk}MNk~yKlI$|c^$3d9_>Mj#SH*>S(iMIPKlsCJx7t*R7yVAlLHG~nM_fqA-qDlt= zZXL{RO8IDXPyDE$WQCijjf{KW6M&jQP`H3iZ;|9H4v*xys`Xyab}CxCHp++oVEu@k zudgz;rax$mctRO4l=wz>CqHIL$PG=E9AnC3`aLd0ukng$ePW@~*mdu@;zM#8VV?Mr zqRjU;K6fMixfEMjUe7RNt`O$Z2zpmnYLC(S*{Y;7XxfUh!{+mQ^uWI3MsUj+gU{au z!$rIhf?!Pam`Mi4IZ&PUq%4RtcphG^q5W;2Yx-`5S^}5#q~+(AkKC~O6B*e>t*3%s zlRI4353w50*r`RtPeoi<5PK^k`#dS8^d*wq9nr4Rid!(VcrIyOZ&d!$vqjN5Y^Hw% z<2&&^453u!vO6@!;ba1n+8ODJ`hx>eif#;>mp#VbD0kcgv^L$->6RvVx-d4L;!y#g zc^ESEe>8dEvd)e5xl{k*m@KJtC9S$kLB9dJX8D-pSrZHbyW_C+{eh?1niNgrcGI0Q zR+cdmKI|2jBTSn-0?qn;2G6=sE!owQs|$SED# zp8xogkVcH3-Ney>7)YK#=vZ{7*g@Tg3@Wu%@xGqpk}z~9ztoe%?J2Mm^y`vjE+ngD zkG#2B{Vz4aW&LsaMqO}O3v*f5-L(Dw`E&cG{qKgy^yI(VhYj!7zHWT}KIsF5WP{~5 z-il|FaBo&?RYZG~^*)~+J4yw@=nKc??mav>#y#s7uby&x?CyM_0DB(Z*MJ_xbv4?c z2td#qhAK!^@L?oLwOOi6(h$>)18cB|;|IxkF6?}HR*g27ptmR$^lBf&LDepNaZ(36X{Fnx<6FhSFY(ZATEtBD~?aL@YEruHEi)3J1Lw<$4% zXs1wIBD6Kq%Xvjjz77_Dlmo!wImXunqTEdMM}n+)$R`sHbHujXgJ_?EdA5$LlV*U3 zQ96Y)iOupYS1HW6NSRry--ZuRtG3pU{mq<|L;^A2Bi}_cpHUIq)vqkqQT8mfpDZ(_M77@_^cDbLx5RTq1n z`yYnFP?weRc8tmAn5?>LMV)rijgA-!2ZSjiKw{-j=N!I!fkh_tSRu%Mn<0Kni!VWl zjaN8Vy%G=9Vs>Oi@Z(VE(_ApU)Dn$gzIz3RJ>j65i9>d~Y?5&Chf%aicI*3P6A~K< z>;=sDBa6qeN8B?Ko*%D<*3Yq~MHq;li}cq|pL#1we>zT@rXetkeBM2iJQMp;>w=4b z)jki_Uh};v6%y{8kmSbH&Agr0Z{hz~7WY{sRrDEWOlAvxU8gNU7Nd2X1+8!gY0E{7 z0RaZC&y%l&0tr?0wBKYJ<9Xea=Tn{WkO9SDt7577?ptEL%`vtYaVws{#S z+A#Q@v7GEcVJ5PZ6*9w`Jt5-(o3Z^8D^^C_LdDaZ?GuVx&wSg@t~Hgdu2HZ+0;!?x zRZBVTusBxNtw68B6U9A-CTBCGm&6GnN^J5mh$_>%F;BvvhY)zU&W8gVUWKPb*FN!c zc#!)ZcjSW72C!uI2}3;~%l$t@p^*6T-bSQbAp$DKJ;vu1xKk_@#@3Id{AY&6>Dh8~ z?kx&ALl~~_I2?U{;AFPuLhv!Zi_JJmzT9LmDhJj1jsW1ml`1(?^+8HKoYy^b3QDzE zv?0r%ww&-rs-p&Lnm?_jxqt5Y4xk+K(3ak9JCIgUq=SDxea)m>k)!CMeMdH5dn`Y$m51+8%X{{Zj*g7nYBilX&@IT=5Fg-nx$UzfPF3ws!E zAW0&SvQ;u;0rL9%3Bjw)b~3)P@v%+h8|t72y3y zAm;$=z89Xy@CZk zkQv(Fv_@H7w>MiPPnA5+l?Dc5zELZJ5fap?dYm;Bu3z--NcS0MoVN*Hh0~2o#C*2~ z+hSQ(ty=J78cE$Qq(+`#VPVYB`%P`-qS4(rCdQB6_&E(6tp4tML=fO=hSM>mda;=H z~fUBU|pa!3a=qZ_3d%OqBU`z_zJO52~%HCH#gc*sZueS933stTu_ zk~*sknL%B()i+>;1R-U9rH*qhnv9J(;=T6Yq8t?5^l+h@ z`qLc->q!HZk{aDgH9WT-ZQGT|8KTP+*XqEB+=rVJtftmEFo?e?Nkj zWq}ogQT!+$&4sD;Qx8K5=IyIXu0ra^OFZ!*oA?n&Q=Ibf+&l4Gi(`iVHL*O;mt`Rg zhzDh`c7a1oN(VDtzz%ymI04USZ$3MdxphEFh^0USdM{z!<5)Yf>YF3`ka(&<%;j8ZYm5uo4MJtjWiDG4qb#p1SJsa=XT z)MtqvtVx}+B(U&Y8JN^r0L$TeYtX@?X!^cQ98U`95;MUgK(WrTF5#+nv)&K2U4vZx z8km!>8(42{1CAmJva~1C^q|MnWUn6Fc-IGY&l2<1BzZd|N^MtQxHmGBkexyBueJRR z%$WbJmHmrcxZF=C-#ha+Vt?;A4#0mXNHv5JGnVUfh5*IK!ki8dIV3+fNw>u$QW%4I zEB!x3fFx&2rK7ZUI1v@@uX8VdywvE303Bn^XX-JvoUK-T%<)ratIH;b1RWUqS4-!L zD$nRKQ19xp!RptHpCjdc`bJl1nb~Dh#=hvom^g+aV0rKEAJcjOC9lzu?&1;~+~VeTE_$x3~qq zg9j8mf;-5P1v z++bOiJ;RYx4ie-k3530fGx}z#1HHB@%P8sAN+Qpn_{t4od^vpi25^9DLqK8i@JLSU z!Dp3<1W-xmtzbYZzq>*Y>_puoUj-oY7IDv}; z*75TD1Io0-Bf}-6%U$e=K199k*Qd7Kc?OaK%L1o1eKJtMHB!(?;f?C%(Xdg zXq)AaNDLrg|AVId?#_-I0j2yMAUpn7L|t(D7lw7PeZ{34i#WIZ`;gyfNI5MK;P$sx zIFZJH^UU_%Sp!hK@9rI)`{Zg-gT9o{R_;UYlgO$YrAk?-a`9YllgYhgbo20b|ImOl{v z#L+O>P7cJQCspU^RmnxST8YULEYWTC3KkaD+0suH&(r~soC*%c&@G+yQ;sV%em=m! z^n=@=nbV^(*ir(B?gW(JlfI`-x&Bphl9Bq-ey1F^5HBL^o&LjiAMfog?uwVz8z?1A&F4jea3&w49Yi&qS z&G}6tO%PA+!bo{-55w#O07&oacYUhu9#0gZs>}kO4qr(HYzHj6(;94Rb5U+ZdLPU? zM3Eh#%AcN2MSI)FMhLs?hb}Z9B`sT4U_HYV&|x{me9!bkN$utSL|QEDkDuBXt(z11 zN>Jx*zWk1<_c>*|3yobTHG_%Pi=>Jd=cji!^YDjK8rO;Jm6aUHm^}i#f|SabEL^Ghc#$XM z{Z}^7HH-GE+klyZL6lUqeJb=6oaGtI^A78hp(U?BMgS0@76=`lB07Y$IRx@dq z9X;faDV)=Sxk7|TLS41<_WR!ZudrjkMfVTM*1fPy`(io#avdaahjY$DH%@FV08PTK zEB!~uj8D18`=N*~*&@i<8H?Jtf0JR+VG%#$)t?zI0H^;z^=IbmZ^)W0=yu~M?m98W z{ygE=4*Bh(OCNb!ad_w^7+(3V_e0}JR~|NnvCT8cay?iBYASImU)xg0eNa!76h73$ zyL1_SL0{TwDB{S)U?Ooii`$;Zlf>w}`h*m5UR_UTvT~o58FQ@GsJ{~ zk`ApbWh`uAy0#ExTY_lRqr%sX!Q4UARC;ivlx)Urm<$HuD({h;m!kVpLKeL~JlAJ( zYY4hWTrrJ$KLhqOal|ZAv60epKvNVEr$+B88=iRXKjTwJ>!~UR32GhI(7}xAv3-c4 zL8c3@+>?^N0#d$_VN<+UpYCVr#ct3zUZ!zSo8AM(uRw^`T`t&3lh2n6$Ovevlw_Vy03x7!Y>aR?WRz*CQ-=`tzC z?Xj!fe%YpHDb#%?X~Qo&>DT`t?ZkN<6D>ryG67ckGWvAWhX)c===~?{bKmYA97qpW zi8F(Tv-0rVlaV!TBcS_X=kIa*J;~4?A|TCyx38eeRIUiBk>Rh~Y)K!9?nH7U_0-u_+Y~D|0 z@43pj4XejQ6>*Vl)l(}jYsL>Lr(++e(oC0ZtsIlqA7BF}b?>IHQn{J*xqumTWT-nVSBA z#AiD4v2Hk+K>-_)UwftpgnS1NRA!H-;wrnhF z@LbBza=86eJ#x|0XvVTBg-LKU${xB~%D`9fa#Z@$Dlv=;3e$rTkWv$gw1tDIM?zhl z{!y0q9zCQidN1s|7)qO_?!9KdQpQkJf#Ic-C-KGplimsKM!>k510n?gzV>uzwN%>eaeS;u*~LmH?ozb%RW(} zhVNkIT9l~YOh4VfaoWI7bCe}^}2Axe^K9|$`kS`6Vz;vr2~WNm1@ z({f5U=4NNtBQ4pp#e$`SWE<##&*=ki_qKg?@@}V7U3A}gBn;m`RU}XV21)Td-q+A= ztCE^;!;%4=?S~S>)h7-53mPI*^>%K4bL-oU_~sYSwNkFj^gIz{@2@}3AilW9x(7V%kkT;@=-BG?9=qLM56YdFJ85 zb|&(ZcN9?LT&aaueC98_P@VdEU7!uYlkB4}k)l_x<(O6Jw2+Ms-0(kq9%7k$UG z(g`^X)HC1J=|C>;_KP0XrhOb%T)IARrKeQhU(&g8b1t22<_)3h7r$x`19!Um=ko}; z!JJx1@LP?^r~VSNqyWOxpCf^O=7l1ww&k(>q21juQvbuGuBEXy^Fy$gcnjm23Gw|o z<|j$|FBeZ5w(bxrFlpgD-Q<7RWH<9qZu;jQ0MW}-IqWZa4MQO6{vYZW5>OuTe{o*-rTIilF;e&~ z@`(K+9|60O6fSX@z0--`YkY3#;J+6s{@-y3e-g?6|8S<%*v0FwDxf2+7A?P@73+RI z^39j#Zn_Uh>v)(dL!~=un-jygnQNdvW9PoF&)}f@0C^}b@QWn_x05;aBk-XFG|`yt zS84aD6VAP7N6__Or>l>6STcCe8(!_W)pDu~a2CH#KVfs#X~yO@7pW}P0N-`Kr?35N zf?4-%hMq_3=;$biD8P~*ZG7zJnZb@lSTuBGLgwsEO0;zm9ozJfOl^#9k6M(Qvt3Xm zl=AGfKN>$m&qJUmFfb0vtirC>3)BQYsjT`i6d6mUZLljn#^!O!NLCK4VO19OL17OH z50w=6TqIJ5(!Nj=%!zHQox_zGYbVsCjBVQ;!EP_#4Ke<%fzD3z#qKeAxg>F5f4W>4 zL*+yRk2+NfUuuQ)oE`VqCe&oGu@c*g3g5fxya;UYs{)GGN)xO>G9bkJhXp3G1oY`! zvwGTUns^!{*)=sC3OYKKfYjVEd`J5C|(5XNZWWXPkHbG}z8p{Qb97-8C}O zc2!oKWbtis3j034jP!Kl+p$?!y{8MARXyIrle>A>i{a*C!%)im7p9D7CfqaP%MdWT9_W?7UgC=-sAzoWGv)aRhn%>YQ4O_}Z3ucWkJn z&0GASJbi+pE7Kv`QuA6YV!@8cK=$({D&le_a{`ub0i>vOhuCq_mj1+V%O*>@k$ z-HWdg^Ti?-GR@HXxSUD3am}KEX?;oqCSZiofpIad6;)-RASf;HUtDe|iU0~23X)!m zzuElv03!I$y}z3FQ2xchfXtq|Hwx|54=y`DH$4J&{E3{pJ??YV#R^wUDfx?9V@3{c< zYc*b5TkB9FxFRJHTszl^p9oGM0fV~7f@Ky5zIwOkw2s{w)^DIpx10vni!BU%_3EfS zKMs@huEw6NcaT&^%ScF|^FM!hVz4)s?MsU-xi&HXbe%JR`^7gr!_6u5V=4)vZmmM4 zhcmcSyBWS0;A6j&b{QB>OpXZl*;9K%b+y|XdXurr7Mg-v0%pQ1f(lPEq3m4!_wE@3 zt5X*@o6n_VR-!^8jkQfBd0%c!SeR{9J+Rz58BNt|@k4Hz{D96sGSVCt+onC@c2VMu zI7r?bqi&{K4acPrB4plE)KKp*2nuT#@@hq{zk2V9O?xtWeJJIlRa-#8=ZdyMsI+%^ zNV#^a#Lri$#G+$xDo$(Y(O0iF1VfJ$skBW-$DsY$Bi^X ze@Y?-@aumP$k;zZsq*%xfYJg-?Xmsfv_tgk-s&ZSZn95W%8>g#e}=)1Pr<~sa+`EM zn;2?Q<&S8`xZEWUQq}2{<3boa66`dsNv#HYccq58NGkbUIYVM?M7UPov9V)G>X1O^ zvu55LFeT%1t;`?r!)Rarn2ZlCvU3(8;^HC&)Sj7*4N_={S@s_NfhjIqL51g zCr>5`Eu%yAv_MRWg6npaQJe?-L7?C$dq=jEG;fBz6E8Gcvbw5y8A)^j!8bd>GsjyY z@!`k?r~=QBZ+UFZqSifE0L?bo;5T_JGxO6pRv#QJWEb?%T}-=d39sj8Dj*_jsbWV-rHO2-7{UP~ zjwXH4-zi?BH{ae1c}=&WU-|sF1>a-R*DuyxITknztUvf(D$zogYfjnL1Fb=YZPnmPDE@|Kk&8Htj zBKM}i(QnhA^@?ttCsFZu&fl`Km*JXkz>UomLH|Ny*$ziWU(EYrX{2L*hu<*9>!Mxe z)q8iz*%o)2Cg)AT6_w=C_8?HAK!JdkzzCl8>^KKpd9-wN#ilNrmP5B1FD6|K_n2@kzU2oT^1#df zS90mO7QJ;4aL*@hjTkjb4bo6oS6>*ob8;@y% zkC%=7UGL$GrC)wM9agyzr<61$)vIz7HyfyWYDs$%caa(HN1^rXgC9d?wHZZ}5RCA8 zZ0NM3P{$>&1VTMIEJ{L_7w(Fv%Ava&tq?2ritR`r(xsD;)1zc0REawI!bcm+R*_yA zx`;U}+2W`6!(UbAC!+sjOfV#*{s0_|dxQ0V(fjcUsE_Jw%zj@0p~LuXK&|EB~7LKu$QyyJp{JAT`iGZ}kMdmZvgj{m^! z2~OK#8eY_o$@`_9qsWqBU3d6k#`D|*Jgt|SNOS!zbdrlB7pOO0Ncws}J$e5H8AIHo z*L#C2hE7Z~fv%ib3BI+Iy`~?LB8I2|W#@EZ{6Mdo6LW5;H{cT#gri%TWlk-BZ?dBf zt+pvK`_|AzJ&lA5Y5C)aftv|WjkTXX)k2@2NkP#eec<|ycO|6}k0#{pQwEG{S7`!7 zPq?5?zJ=p<>;($#E=J~*u;sf;pVM+=k&G+IE|M6cEl?wxYpanC$DcU^F>bA5z24copCp!$#M;?~DeCvaarbXl)qU)+wxa-IY)u|L9(*jkk`y zE#hrvCJ{Q@IeqT6aqUSvULsnNi*#`Abv$q=!rbc7c$ZStmpRG-;c0Lf;M?jK6p8Jz z4l^cZZikC|=UoX;8h-6x*y?%|SMc$(^U@B9g^17b((BQ&!ksB}^e7NE@dS73Gu3pt z63x!jAW)UzSA{3pP`%8qOwQ)nIv28oFEzW$K~=+&$9#n}=gBM@#Aq+&RkH9X#2(HP zGjvj1ZhBZQ3gSfQH+kWnUn`AW11Kgy`D?!c1k;|5;2&)N zfji(_P6dI4ob`JvmMhkT_q-3K9eV&sfpPtON6b02bC;G#I8@WR{J7-W=dT=yr_GGB zSGgQ)p>o(QqzJ&lGd?g!KPUp=gtJ$R92CbrYk|FLLUDZcj<#JbkV4gHKd1`7`?mJV z0*fPly)F95Lvbx~!_(;UTXokd$Ws!IF64fI58Lgvb*k%=CZb^bd6!MFN zUtXfZaO!Na3hu;g0*&$6$+-2aSz+JewmHDSYF_MjubbXtg?1Qw9QOd>qKz61(M8Vdfr18fS%Y?LIx6{41yCeh zc|7)CegePJ{nlZLE>L%x&U<<^Nvj9=;Zq%=qMhNu7yL2__>n0r0)o~Bb;$$8PkICU z0|K9*35-9dI8a54qPg-)MY6?X1u*Gj?uCIjLmK+H42xGS?dgv6HM+nl{dm$9`eMs6 zOP&zS=^DaphFx&y;g?A`&XnK63Y!(0z@)*%DALq$9U4P|@z8Kk*y38&^EkuL`v!w@ z8k6P0PYB_#v8XSU6CObzU4Gl*QPv?`+!+-tU$=V|n~p8x(|1+CJ1*92b&um|7v#(w%8uc`Cm)e1#sz z9UWUSe=7e5R!FiV;Ye^$chAKCCT!b%X;sIHG>~#^>0PQNwD@R2r*h@2&*>=|2d!s? zf&^-pS0O!!wC$7(QcbWLX{njgLXTHo}jY+hVvv{$^A5@vPm_f?{z zto1`}i<)96qpx$+uV`+Z=|2#=K^+Ek_XeK70eG|dGZOsYLlXtOCqM*WAiL624u0hK z3LKr>!>~t#hMvcJx>icSA9)q~P_Os4$THp~d(P|9Y6?7c>blh^al&3B6kX*yoUmF) zKo9!(3Xvf;YM5~h^*~TcbW;4mcpX0uAWp2{z;E}bAPxRr-?1uP7v}Rp_?om^mE00= zww=rX&r_<#F^}NJ+&Y-rWaQ)!K{-JC8tXWXg=CF;Q|pYe#RqK!m?J3E*gLwV49nNK z@&jKqd*zh4^jdEX<;vTG>C6V26=HLFg`F1ZS5)q1WaLr6VX*ShSzZQFbi}+L_Ts1N zbz0s0G{B?QD3S-F+ix$>*@MPd7~&Rr&;;u05_paBy!y)plhL`n+r4qIkLUJikm<9y zVRSri+78!4UfW5C!ag}XC1(%1zJDJh_N#kt{U|vDt?ibLz;3GQe z4?eL$6X-4srt2(?YD-6apSjf=qTd9@<*MrOwFo#^kCfRvCnf`2gmw>;kjA8U-lr3zNlsd#~-qX&+NT+?2;F7`-RNk%2vtWX5 z$)y)O2fY<}@ABPbQbouKDZ93p(%c7hgL)_8nj)zIt(J_dmfGjoQFdlTG(cI8W6Bi> z-++-OLt=oRDK!Q5FZ0GyZn59E+kO+ z7FaI$lQU#L3OC^WZabz}kt7CJ;fikrlQy{1#%f^}9&w$|Gx5F<9G-1SFP!cNc*k0`WL)ofeB0@J6%%>2tL%~aE5h@ZSvpRSwaW1gR|~Zf+*4r<2`83W!+%Ne)NqU zvu*V|p}uiW)4|c7Bd~f?4kyZG%;VfPFjiDL0r7(9=--5@!+P~tQ)+q!cxRP1{UhpG zAi#&uJWK{(KQ``7<9nuR+82+pRnLW!40bBR7Wv{Vc(LC>z`v_%W~N5|P_>}ou=_1R zli`N6c^m4QM~t{AtX;0r1lQWpxbB2oKG*#|Z?|j*A?h{E9qE9$shJ1XF1~2Vj?eUg zYvs%qO@O?ZKWDWLjygXj=%A>Tc2R>BVueK3jY_{@p z7(fH#r9omc5BvL#N-Y}!olpIRkI(qMup*FWRQ?$Mtll2k46}chhLo(HB=UOw7LfB< zZ@Jn_-f50mvBT+0<2SN{ zlYsA09nAZx*@I$5P3Pr?)=kjnbM)Vq1^@rhvK$n$|1w2@88Ylt?Qn3zZqvD-uY`4n za8UWW+h@!k7zASLTuJ~%v)r-udbR2@y8OCtBL+@sebg2^^e3o#Q%$RtyEyBui=Qxt;3?~+O~0op`{z??v#=eX=zYe8j(hj?x8!S zI|Kx2B&9nQK{^JMjsXOQ8h#t~zVGLG-uL~E_mAK49pC%`YxY`u&EB);s`EP6wMxZ! zn7WTP*El#@obn0U1tq_kpGPs$!h$aBj}Q0jXz!2Zh5B|-+!`O^2axbl4*qb3`^w>M$yxBU{O32!q|$J!%DhF5dU2LSJ>HBX~`rykHICi&T%ZQR$pP#TLY zUVZf-7fO<>;VS2R&s;vqB>u_sjS!mk#Ay|-@7LT?F|pW()on?~o&izOPF)QmSM)r1 zQdlTl7aTKF?`a^%_O@#BdCk2;z1gOuuL-{&n;PPWNX4+vVP6Ls-5X?7O@4XBhzh_m z#E$AgQQ1e4tAgppTo$5@+bmKs$M4w&-7)|Vw%m4?#y}H?0xWWI0fk*)#_u;$i3Qi7 z2#gy6I>C9<^wB6g2e|E!TG$igy=pK>V9a_ECaS%3!~R}EL`WoE>=}QA zR=eVj^Oe@Pdz3_UK)>2zF~)eMj!+*!z^yn8@0W~b<1{mNgmVLMedFO$*mdG1@MTt4 zv>~#2SIGBIR~ru>qc>V$Ha`0Xbr%`ufGxFn<2XOlv#15)w!#usY2w+nJ3uD6t0!7M zK1A73xD%cQfOQ=;ZYSD@1pU?@ zzJqHlBq~k0IrsdJ+=K@CntzFGW?wa?PG!auE_ipsksqIYgzPYEBX?5(XZCzoNO#r0 z&QWr(GthIt+Q(KBFl+A5U9rB;4IWvI%x?2T+AP`gBJr-oEp!9BSjUm>Fq|yZC;2?e zG97QHr{i@nCSt5lNS=I5ivrC7(9TJ$xlzQxL{F z$o(69lT8ii|Nq&8yt9h{n)`^5Rfl4b@riDtRYbV)J9Z?$i*e0Ml6Rr2?i=;i#8(Dj zd%n-mg(al;J~l@w@QBSd0AyNp_>0q^kujy(ZGJ0XbyI9m-?6(Ea(o&FuMu`;9|1*jJVwVC46g#K@myS#yxSq`ZZs z58WIMn1_S_ObG8vP{`z z7YTe}kAO_sVDW&5$?@XVX1@I~ z$IVayu7!G#f$A(?s95d=sIP=WLIjAq?DV`IB3>96XOe+^V&^@$5JkUDGJoG{J^}7G z?R-mMA&J)iDcKB~EntPAN<&{$t2$2sXvV+AcS1t?QvSMNP$e+H+q68ncdXd9HdLADYA{!Ky%xl-nO!k0O5#o4ccUU3wP{7qSo)`1<9^N+|pKY6D zDBecdJ7t1N7O7W49Mpc*C$#2P@I~p{^N}wu5BFE3b$UpanDdOwYu&FmmD{`(zI$2V zZS{1tTyT81@p;H65uj60UUTd1D~|I<0-tB0DdQ||_C!)M+m;I3X0NLDY%G4F!?>A0 zu{soU%qPv8^otW!ifAE704~((ri_Gu)~Eit18H{P$d49((tcC{%@Umcl*C?10b3FYgN6wd;0y# zN9nYj!RA0f0kedk8Qaoz&MgGYQmOj19Sx^VX{@*8(*GG4O`aG{F8AlO&Qe-MvO>9Y z?>|L|12m))HG2NyZ;%Qw4E@vob>g2%X$o?uw^j~KDXLeIyW5d7dCh^!PU=02> zoylDX&tK({3($p+ieESn@?|rc6=ajkarimyQM#$8BP9sFHT|Ld)YRh;Zk1?2MY(RRJxar zPH(!jY786``YL-#`nC7y(uzT}{HZ$n-Kb6=`C)foUdLe6l82IUdY9 z0wkBB${T&ExMBu{QKdW!B5rx*WDt%DV>FjV)fYUwiJM+dU%U56{EgOJP8dym2A#Mo zaxib=8WuCl+DyLlv5jho*URiWST0V@XACK#znoW8|F)06nQa@8K%2rIxbw`^-(cdQ z_|4F$=$9(ggU@sHtIfS0IzGnl9yqPXm{@pR7+u?THZZT9i5{8bp?JMKY_shQQ|t=w zuQSH-fC4MgmQ*>pdafKLO-${PNJ|1Nk*Jp>MMB2k!+hd0XL>do8XIwK~>9kH#4i zSwBx%ZphN0{dh|BjIo7^>}9P3Q)bZeM7Ey=iuKr5x~-glSkMQ{UhKF)`fnFmJN;cT z{QV%3IY2!%BpiIM!V5$3j*k zmOP*@x)ks_ckIWB9Q*M0^SZlYRQ?0H3lg>qVulN6G#k+uD5?-q|^UGyH$Prn`97G5au75ba^KFal?Z{TsSDoo14(Jo$r<+aAo_S7R}X zyV)i;UKko=FZ-5Bb8ry#KBiEXh#tidC!lrs!Nz@dV^>vc`CJPL4C{`BnM?f*ex$iS zi;xPaLx_kVM=9u;$&fcYMqy4lh`e~Uq(IY_QJU})SOOfZ2u?j_vLQWJo7#F|h_;zl zYWkgh^+mCf^pVF8n>EQFY>@{ys2Kk5?AaEXsLG!`PcGM!zrLEoFguRc0}Jx_EP}c^ zBs7{cJZhi1*0@Xub{IL zvgblO2$c+*tUQQx-NDqRbHb^Lrec3ZfDP_e0;s`W73!^(gT7qFjVp^CKPIv}XmZLl zltzuJ71VP#S7;$uY8aOd#_z)-W6{3RAow>ptiO~RaT?DN<(@TYQk@8t?)AM6#n=+^ z;Yu$x{qC#IaCb_9EzKYkh!u}RnIM&S7%0J zm(!55uvIL5AWJiMoSIFiv2Dp*>Y3k({v;a2;fn>Hq6K|vop1x2l%(>l-oy0^YcK8k zN+TsVbgq_xR4M(ct)BkaERaC#`FHfG80wEPGSm60umZWH*Q!ufm<)9(Sww2eqlEj& zzNKyq@fuHw1<*E2Bw2eZI92m#C`427DtaA1M$3#Ssxl;v^JIwm2a_64!IZd}$zx*B zn^7yZY=a3pcJN)cz2q2jp|>efy*oE#QsTkq587`L+@A#0jVLphs^LVVt&QY=rL$GS z52sP9SY)q_Bxg~v;u@eyvLp~_(uY7ujUzBu(#uLv^2gNPc84c2M0^l z-`n6XJPA8ceV9oz+*WE@of`XsKpIPocNTq$-6rw=ivv(cltS8ZdT7Eci7yc%2Igrm zjID|5Ji-XZagZingFT}(ht4_5r`107o+cnBK&i-Z*Q3ZQ4E>~h+l=n}_c^4fOqs-p zNerU5f7S52U`+SZF#n!xZ8o&iP++04KH7|Fwo$-n1}kDUTia{yc0}6!NEnGcq7(Vz z3MYKXWi!HaL_0jGWCG)s<|`+#S`58zBmAh(2^s!2MQ)BrV4f&-qt9}w##LLrAmLW-er_W4h!*ckeP4qsK<#B8EXboAQ2 zKCChx$7D8w&l#;W9(*LkFnV`#3ELPmD3BWYW;R;HT0l5- zuj&O!Ta$5}Y2hk%*nLW_4EH^bSIw?hsv2buNK!PL%d#=?%t&o(xxiAH(q|>@O(YuT zu-T&m#f3BQ%w>scF5b@(_ERV`lGFAv(d)G->jZ-ih#C`Fmd7bnLkcT+B7Jh%be?8$ zgn%CQ(7K;P3L(OLqG#Ztk1LW>srNKW45&Qq(y3cV&rhDs=j{*o+aY^bwR8b_+?IBa z?b^X_iJ*t2oz44_I5yO1heGy*6}pUQ=(!Rus_{kdd_J_8?No@+U5f-)Ip`zuWL(=j ztY{T7tKdyE95VE)=CGx?HjfqmJkMNc6q7m)Z-5ExS-*j7)Z+ump zK@mbCAC13-&7cc0?vWaQAoiwd>ziX9G$aC=Fznv}aT$`qV%M$!RB}OR1UZw<-;G64 zKx6-T5A>P--SOWx`Xb-fK2x50Tu0M;b12&VJ9AaHw%hm zx5+30al<~7KNfyXOaBVJt^L+7Qz3p5>@{mh_|@S(0f+%jUkizazMi_a7yXGDNVqO! z1wJPfTK%vK{|RM1WE@>>5~ck!1AexZu+#{uER2%QCVnq~hfNVfVu^?8BH4{1Mrtoj zXG_L`{>fuB&Y+GEuflAY;*|nS{IX13?t?S<=cC|+(h{uWfhGY}XjxqZIjARXVico$ zqF(s{D8T=-ApX?jYz@`xM*sT)sH*+Ae&$8+=?QcO9OJ3S`gF2=?|sR-&|bEpg2?5< zXv9xHySEbrQ{D7Fj21QBhWhoKQ|DDx3B~osECAM+T-b{%bo#u;i+5BLoDCnByF`M& zRU4*CBNR|zly*)(pAM%TJ&Ym;87e;Pna0(d9W!c;$AgISmq)w^5xlnlk(v$jJx}`% zUuQSQ>K6`V(3uxiez0Oqgr0bcTOe0PTSk0IvJx821hJa*sL`s~rJK%f&%&Dy6FJ(< z;cd_9(|_rJJLT^`rXZ$V5ZcwqX-04*sHyYuY)f=+2aVvQv9GPcnLz>Gmt`j~7jf7S z0rD61`#?}CIr#iKa{U2Wep1TT8_8W#^s`{>uh}N@1wdBuA?CCDmHp?)9q(!*$bB$z z^a*<;zB*M9sA0z{c9hoGpBr`E`d{vOCJ+jEh(Gi>ekS!k87yGimuAfm;xrcbQg-qU zj@Ic{Bcx|iBRWapB&pB(WKov)W84=NbXrGD5)N$IiDvW7o|CZt;c#W?mnvg_Uej)w zFJ)R*UAC=7r8WMy_oP$?*7)+9>d%IY*UToq3H$tPqQR9Nds{vO;)vq3VbXW&{ zM;sD=uTlQdZ0%?IZ`H74dbdipMCMic53FU*U`zYtkqf ztRJQ*3OWEwJF!j~!d7Sluhhuz+y?{}>~MJ&y{XLH0VPB1@K1)02)K?lJ_64;U8D~@6#dyzhStjR0{1{g@_)4T5TKmX036_ ztTgaTdG_k5IPQ}(oEm0^pO*th*B!@=@vP6TrLY5j&geAFBaQe4WQkrMQl@&6y-fKF(;ec z;o5zcUkdtw&xmH@n#y|Qs_&E?^FRoFCzXR0cc}e-P=MACFDzuy`=`N<=8%OD;u&*4C0gdOv_!5{L&Dip{;lIJRL&)WUB zl-@P|3IXn-*9JEt zdw>s|Vt_N?6fY|E&e%2L!m>mG%9fi_q_X{J1YMF9fmYlGUs1CbrILzrt2fuHUtM}O zTCLH`Uk+w1ppGjXo)aJ>^?IIxwsS(OfQbmSNLzrity&s5fbu9|-}6$@RQNhK1B!PK zTVqqf4N;Zhvm(>S0IV=GQUT+teD^0a6YZ;@^{B;IBsxh^i|(tG%JM=?RAQo~do z0Iu~>%N*DNMZ4M!ZI)jD!X8(KNVGL@f;4v%hlNd z$>dYP?H@e5yqf&M4vx2I^g}|HS7tVrKAdk=<9N}m&tNP~U`Pu^@2r6irH%v}^{Zub zeREi3IFOwx{aO){Cr?uZ2y3%sLI0X1JE(926|E;n8Ggk%orKQdGY(odwJ|OJ=?jW9;B)-;VX7$!+*!xrIe+N$z|M!Z@Fxii6xe!8iVC%#Yjmikj*)jv zKacc)PO!*7+R!`W3LtK3!X)Bv{Z2S-@&?~*%^AQt3|<`;N21aBd<@%@wBDNWD`UzR z_l6x$+seYY`*}?1e2RdI1TaHBjUK>0S~lE?mE8QlLRB(Yf7wz%6#tUUe-l^bDFyC7 z?)k6K905NT7n>pasm=XWftK8#pyojG)ezCJ#;bd5Mn_sbQs)#AYLD*kh;U^RGJq~5 z=4hCNG?09QtMBPtJ*`UvEHpw8Pa1VMgCc4^=1!m!`RxuRP7XHWLHuJ;S_;I{Arr|{ zP&7Ibb3+6H2_K!0Ok(`c{O_L$A@-VL>PQsncY;&1%Iz+1E~hJ-13AlmIZCVqoEXX6e25LxroiDpOmcD-jmpeDh7S zG^D*2|K0uITtT$76U!G4n>=q3>mJe*}M z74d+T=85|&u_Xb!38EWMkqTNz0&W+nq-`uIzwf(nW7-IIoqNHzYeOu%K*3qt8DuuO zsI|2ya&96Ln@YoGHdy&mU&v*1Ux-%^OW3jjY6OzvR{Q-FMuz+N-?V%n8m{ixKq!Qm5)Fd&;ivK&*hjNK}PRQz^bnJ3a1b4m?WaLGY(s2b~jd%xn^J)wqp-S6o;4ZYCU1Tqd2 zv_7uJ&0Z?iRENtk!PHG3mg<}Hn6hvIk>D!s0Dc8wAmSgk(?k_Aa-2dM^#2G@;#wGU z)>K^%wcrzV0@ZMygxiGwBf8}Sn7BV7EfB?${(Efu$BZ%A%yK`!!?^&o_v!nn2=c~* zUZ?w40m!P*wVe2mReZRYvL9$zksv+X4jf;0%0Tua8k>SO{JL>>I6*rZp}KlkQgBvDE`vunfK!S)v2f!db}|kzV_e)LLTU zCLf(a3)Z9ok5Hn0`?455s4p&S3@x#ar&XbA6{vvNB%Da8(u?`51Ut6OswiF+us=L9 zrO_!jB*IcpBE_pLd)Y^_Xg=I8pT;35SjS~>c(jZ@dGk3`Pbxmne5a8*hBjcp0hi0> z7u*$37;$4Yq{{qRV%C2r#;xFc^5fARyF>zSM5!^mx>UhE>$xZI8PFfOveKjZ;8*{Y ze#?M*;W9zG;nZ*k$)PC9DI`t;ey!m?sy#gPQR`>Zja3F8aU6!;%O+|yoa1f?rubz>&koE^jI;n?*n#f)HPxyRN?C=%jhmzlmfvmOy?l|&r*l!rync~M+DYcK zp1K_szA{+a=dJH$!G~J6Gsssw0)P{6$ZagVE*j-EpG>5&4wUo+dp({K_J$Yay*ysL zSX+hAn;pHM244b@Rs^61fVKa)Q-lZe^pUYtB<4WbJ{Q2!g;{?}E4`-_HR57K+dQ;0 z4OGFTk5V#)2=eANsT)6gqdQwCp#5Qi2Ch|EKr<)xC83ap%czCp)ligOY^a3yLEEZ= z1$IWeix7UJ;|gBPLOp@N10e$Mb814vedw2UaJl_TgZyX5yMS`NtX$v~$I}PZ-<>>+ zRtPY}T@KprKh<-T{aWwPS2{qkGE0ZY`UUDN*B5P! zXSdR|8N*XHl{+1^Ac9?5jDv)53xo}BJvKD)jc-8gn$Ng5`$-;mP3fkdUP&RpM{qDae0MS)n4&xLdrSpxPEjPcL%f2 zRHEWO-x0$9nc2o?E)~z$c(WF_x^=*BHG%#Ob0Rl` zn6fpT3JC?hk{Fvk+jCH&bF*M$F(m45)!<0+jfTUp#n1vkSsUTp?Hq20qBPD|l=!v3 z!LFy>2?K1;iBPctc%+2JF=03|1(F58X*`l-g|XsSXf_HFZvnNj`xhwww$}K+b#(t6 z1T64hM)-dUae+LOS%I7|g_oK-t|HiphWqPHHAR=|lkH#~bisJPZb?w41qie$VaOcH z!)agpSXfYAgB~4HSyUSM>d7z>{B1m`Joqb0<#M#AV~her&-9$f88UM`KH8in4#?Vr zsT64n2{)2S`3ZvP9Md&6dNJ;s&Ch<7*8mg@P4<}!VL%p32N3pjgFC#d`!xMU(HV;f zi)>slw61uJsVH~f)yVY!Sfxa3@87fX-ksbj>O{W`ex$htO>m+$lYptIzy5Q$LhEcxLn}WR% zV#bZ0wN{*v$+Rm#;V~M`0zz(6!9=@}6#)Zkb2fT^Bun&zoR?iGmwCIKV;W0 zoP3X=UZ&yt*_N)PQkl}Zd zy;`LZ|8siNER@T=z^+&#P?lW>FxYv11IssSZ_$%Lw-{UGjfaaw#Y6%obMvQnm^j#< zQqtukpf|uCFzuKK@;3EdO+z582>9H)_C!2{fpv@0u#3ltg7+zIiUObIHwq~wBcmDX zZ-&q+5d95Osp6;PeVexHQ>6HwfmjkyB^9f&BJ>gLGA0w80>OR7s#6!6H{dm9fMF#4 zH56C>qi6*HR-+yAUJv00k9_0y7+6K^>YAeTIwQX2G*-loBo%*M%d($z@733a);w|d z9O;fgHIx8^`}|!vi6Il|oubN|(Mli5p75~xDEH_H-BWsV&!4l}=oNwLEZYcR(5q^z z@35%4IB)iZ`yDS@{iJaT_W-b+Wgngd_NB>N`B?)LUr(@qxNyG3h?pylU7p#Cs-{!U ziUvU1klwPdU_nSUg`|eXZsYmtrtIJy5_JyeoN!D7o*u+S&T~GinXYk#mi7+{#X@;w zU(WI% zI=G7#T`Q6GNutx|##}yndt4(|uG?hN0EUOEG|k`H#R9yM)Y@53%u}<t1gEMvNU{t|0u@o{G;vJuk7mlV!IWb6<=J-Vw`f>$4I;s+p49rsNVdZgmX}%+n$AVPx2}KN7p3dC2@U&nA^>DEaqYyNL0%b~JVlA^J&_Ta}&LaiOVO^BHV=1O!tS zjDthHP(A1#LZl7!q4mM0PtwjgT)ouvB~ac(j_>cnH+R&c-}Itj^4}Y~{TtQjJ03tZ z8Xdi38d2K7N>HT~=6{wtR!Mv3kBHQaG-{w5;jylBlsq=~#oy0LP+Io8d~P{qflC<^ok z{B@ZKg(xO}%?j{_{^10n{5_--*fI7WPSW3p|KB@P*pOP!p3Jc7V@2HkyD#JSZSvUs zx#U8WP93?JZzSG_2#f>Y+;2NR-qB31*fwN6%om!%$0C6J&$5LH%-1Y3Hal7+Fs(l5 zXS;eO!b_Ms4vlipeRE1QwMs65wBB-gJF=?LSd_UA1}tv-;&7n+*leOXt5i`GZ|3Ap z;F6UKxI9GcK+;8=yK;ql)vVUonC!m zkDA9#dL#2c{IWqq_f8(lh^M8MVcUWkjH z$zdFWq{6hkw%|)}w3DlLru#mg`O8s!Q5*`w@vlHdGvr&ZojpB>4P%n^sn6?!tX1rhrOK)SwH&nZ z30bytxYn63P1{m$6xp$jduIH!?~k45c*b!)H|X#8 zrhMrfr3tV0MRQ43ro|ZB*OthS-r|uumMI5H?xzwPpBlx$Hu{nMDqNo$HfNF;ZeD+A zgXQCBtr?Kdpj8IRYK(>&x-u{e6S&NK?(lKbw}1aYendH@xwgAA6Ye_T(*3qHc5_@LyLBAXb#X({8^to5{sZih#R-mmESQ(S+k zFs=<`-v?G_m03=_FUrTG9PKsf<*LRobl6>uH(=>vuV`nwP9;-H`EmJ1p)ekQD zi80f6FMP<}I2?iaMP0lIn^b`vV6St&p1&Mfy2k#B+0-LqQ#8}l%;`!>;vKl&K74ejUv%2~9p2!-2 z^ml{F-yI_hgvt!d0*`I`IoUFn#4AR4oge)Xw_^u0N-PEEb-EjLP~#}~lsvLA^CRx< z^|w|t_@rgGX9k~l?5go?IWTqwD-(?hb{W^7|f1%)Yx!{`r z0|BC+7P$hmv(>X|}!R#9D<>koUm@|;8#_z_Wmaj<0vKk*djf;-yV_a@RkJf>Q1K~?o z{zo$YmMGxS{t=qH{}%s6;{N@VG-al*nM6~s_*m(A_K!W8ee55l6Z8^tEstXsJdnKB zczWM3g{WN${&imyaUqTlJI(X80iyY_x4}E0*u5TJVnui)00R0k0+ z5pD-@j#V={O(^7N^y+oHQY~Usjc$5`o~R4BL)?{<+tBPM$MXOaP;O}&*Wx&)T<<6K zWRcEqedO-E8p5HvI2&4Gmpx5kBr-JbpjJ1alj>Jx;h#2-*ACVZ#|dPgr4}DoH{KcOdBTwH^~D~$l<7;y7^pJ7F4O2XN}M(Um4%3RubA6Eh_2)qhy*Z&8cT( zhv&KLU$2I-^1iXub{gNd^*r*=Ek^FDl|0BKzTTZT7YS6nRf)0b?TAs1@~GjwqcrIt zeFYcYux9GhzA+fp_C>rPbGF5{X^;0~(g;8#iY zG_RDtry3^?;n*lwrg49BT%k3hm}+vV(&sR$Q&r9qRU6f7F@$aAy+Fl@n9gjBOfr5J z8To^~cUUE}FJ{3#_?GUg^nhCUoKi}Jo>EHW67VaBPp9vAs26${#nJCa{9McH&sUJ9 zca8X|SfrF7JYYdC-~hfuW;~3v=sg%k<6g0%cBb36xRDUNo4~zLFmOM>7n7+}ECy9Q z-Eee2SYobC@WLTu^1>pgYnq2khauy9_4LV#{HamG1s>?-(pAfqAP{RBxWA4qnfXxf z&EB-U6BACxZpUqY=b-0Bo}9u2LL5xz!wSt#!?k*wvby*q z2|G^RJ}$$q@(K!iqW0m~Y%#_yY5h?dx|cOha~K-2ZoZtl9}P`jNczAOn&=7;utI2U z8gJVweQ8RUWNg+y?jtI}&X!uibOOC-X|#|V-1e3i%F#^z2Cau>WJD)@13)ojy%Q3& zmoN4=G)3n37fW(s7C=Teq2YlLrdMb?+W>qWQ>)FOBM>}cRAC2NQSViMdP9j1e~}XG zwx4R_L)fp9%@r=HZqoObrQ0)CC5r}dtPq#&8;~_2u_2u>^IHx8r zbjafU)h`v1NcYDg|IrVBsTSZr4`6ivvDp8vKYknWJLBytrF$h#@)9}jswP+j5yaqA+q%YFqaPjOX9YaX!+Z+3>X<-Oowk?{$9{U(AC z1?uRggub~QWK9N{`0gTff_N3P3`>PrOAQ98?VuX7Xw=m4G3%bnQZuNGhOg|W6Q})b z-?FNZ_e$#|@J9alrfeVo#EgPWYvbf2zX(L^%Qx;f3h3Xgou9dKbIMvFz2-gv2A*DVA+J~DHF1QvQHJBo!JW0)Q`w;7PH+|WwEFNq4F67 zW$s1KiU1YFhcE9XyH@qc+lzGQ^4pu{o%fyLYlBH8YREV@YB?hG!m*alt5Ccyyl81E zH{;6p_@pRS<2eNF)4bLj9D&H)35)>9HOjeZeO*shF!cmslgp5BF~X6_R3Cb28@t%# zsbmsm)cRVh%}1TS$ciuF&R-WseXwl_tlrc2-vJn*o|o0;RyAfzUU-0xnRnZ#_0XL# zemf877(#ENi!TJsA<_Vze2q=O^bONCOknWn6QDrvn{%{4d9jR_;1?ilY$|ak!|glg zofM!^hrU&nbdumFc;GjX^QnMhzaT1=8}r*tlyS~P>}A#A6al68IZ?xcztM~Y?vw+a z?CU@n)pU!JkVcJ11lnO3bb4RRey&FAvy$#erS@GAGAO6tYF$UeqtV*@EY__CfHA3A zx5^{?lw?QFC}l6ssO!CYxU zD4Zb+zUdEnDBQyvw4xhyf>iRILbg^1gXmO%FUto(4`v)%_r4wV1k{XYMjbmd4=oE5 zI6vygo)~3w772XX@15_{XOgcGO`nIxRFqgN?9@{pt`IMhb-K$e@Y-`5Kak$c>QV*~B>xy$+03nKUDrQy_F>R1xzOdA>{Ao+Uu zQz5F#r%}#il-~gNY$5kt4Re0&a*s&s<4akO)zgt%Dfad*#k3#+*z~Cl z$XAw?_*`Gslzc;^3;8KGUR@{ryRrlcBa(2^^L12#S=X9d)=ypSPYFA5Y$_yk2!7K3 z0BBZBTqg`ZRxdhyT`Fh4_wLmiJdtQH2j2=V`FORH&i5<;`hyR98`6lIM z2=(A&Lh<}f-i`?Iz7fA^>Qp>OP4%Adw<)mZIIXol-Fk-r<~b)p;f~ex1!01 z+OgH%SAq8>JG&M)&=OSqWP6cnK>|!N)hmzGdgEa2H0@+KHrJ+y^)%gmj{NOf;SZRA z+9^@Gn_LF8DU}N}Yf>>l+6gTd_rvO=I4Vgz3gdRuLTC^BqFzdise7R64nPPQl<<@MJqP&5-o?C!<&9$_V94G|KAY6YE3 z;-@Q647B%Y?@5@%3%}4~HL`<1O(@F)3J@ra+sxj+n)y^RTkjjTihSHwJ@0>iY^2z8 z?;t2)-`38g5oU#_T7aaQSPk0Wn0dN{%$G6occ)Sn^eY#yI{>GKw2rm2fV<4by&_i(GRDF0DZ;aT@wz0d#fWeE3{LOt?>oP{)nM#|rzFOX+dznbP#k zfVdaBTL6%^!t8!t3^$XJMIwOEKM!@gVWAZV)`qy_Ci~as$$47b#qs|k;lFT!zoHf3 z|B%JMgfxHffPX!O=PvqDX!p4B<268Aryw9f3YgKph03$OKDIG}d6k%jZ>B6Jq^XD%`kgpcAw$!myb;{DI;xF8Z89`ga%!)Vy~4_ zaV?KlxZ&CCQ8vo<^u^ozDc^xZd5)^Iai$?m>LG9aYzP zp)36DZEu@yvQ3kebGSt099(EMKtv* zW5r0oy$3;a0TgDIQx43}!ktlBZZy30xX)4StP`Vk=7cqo)eJw-}f=FPCV?Zz-(5&m5)UD;O1f%sOVDsc6+|6+Gn zMaCzSgwB4JW0(P6{Q#npb7!~)9W^VQ^l~blyLV$>ROXuZ;RZ&32~Jf=+yf$AZWm%M z;?{F&TyBD?)#+WoR`U{Ng5q;!PoP*+qa9Inh9fS@sWRC-XR{R|p@bFqfTzo>`2MX= z#jDAd534`+A$8(Dt^+?(sa`FgnM0};0pfEw%&X0F_v0P$3Wy;sZppY=wKTMV0=kgIBE8gM`jnI6vpf*KkRr$L=| z@;nZtT$C%4<0loLvbs-~=cPqA1EeZkJG(VQaE+UiaAno}hzj*#vQemoK@shhs z`a+Fq!|?cjW7gIG7iL{gJ*a@0u-hreV|9FF!8a&U{@RZLF(|8+oMO{;PI8isk>>)9 z0C@E8y*&d>FZyEIR$+qzdQxwK=x+rcaV}rB1)RLOGBr}S=!&B;AwG_{W-kQxi8jf* zJY)D+OdaI$qF@j6qfiJ6%i$A~?v9>`87pM%@)H>ku-emHsOL>*B(+!55e+wsT2F%U zRy=+*<+_qOeeYy{|C#Y*?2n#lzmL9w9_s6@86}`I=Hll*XkRH>(n$PX-+NZW%-jT4 zD)M;y=~mM#SYzNmx8Q=FC1KN15c%s1T7*6Eyp=5GrxWTMOk$;Y%UnMkg`Y(CP^+x; z+X{)&`!!nW3#+xM-r{(bgm`~2lYv|72Es;*RzoDiSQ$GLI7^&?2p%RbJsZv+K~W=j zJ*{$0x6CDKP$pV;Ta_%C$0J|!B7AF_`dkH55{l;+rig9BA)ZEXv`S;y5s54yS1}&x z5tx66j8K@&+=S&@LsGGhH+^2<*rdS^>*|Hw7`d~U&EAdHkN{NxRk;O9r9|G7iPqA= z4Pw*Ke#EyIzm8eOIMPP5abh<7+9z|wx=}|xWr71qMY6&dHFwauE(H{qaJ$967q7#H z9RyVXn?jUJ*!f`65+WH~oIKS1+&1_mh}?y(H!6s{4uHSDl5pVTP>U0bc^nVPJHHd6 zD?NIs*{DDX$bHP~1mR<0e1BIq`~e-AwWguqPrX5oR0pkT)n#W?VhqB^s}O0t#>XDw zd~O;*fh5^$*DWQT4|pG1-w=#{O=HyxxrGVKY;qgnFAjaRng9)WRz!;sIzWQi+pLE1 zrnZFag3}(lq=ZY>ZVpR{YlXT2jADe}7WSo#`BW1+n9O;W@nTT)DUG#GvAp^i;rwr7PO}=$(zMg~>>UR|4?|CKO zC7x`4OIviRdXfTuJ6Jl+Rf_j;p{HX6|1$ur7x&_bQ^Z-}u{qOX;M^@pAe;Z>eVCJ1 zb-ff3K>h^)(*OUIMW9^7D`Z)+Y8?Ms`=`)XIs7$MJo6JP!4L*ft2M#O& z`%l$-xVxamjx%U>d%Nc+h;_HrVMI_)tfvJgk#~+{y>}ZM_ca}*3WLvV|4?-kx)AR?T2xPOf|Tk6l=4DI8pYn1r<2zYSS(| zUZpV6dkPY;r#V*;{1fVR$5id(uC4KN%vdHKjL^uc*2=oBFBhr5IEn;%>Oc zk;t*LVW@g-UV+_Ge!cD)L^LmZo#W*0>#TJ;B6WFBv}@uKc?8*>2Y zHp_(lG_NRZ2Z^I)qiNp%Lki9utdT)VQGP#k9giY>g+>_H%KIgp+FB?7Fv~&lD^)17 z4)cxB+qZ~&`Q)owg?8ELCGyR7iJjN)k%a^k(Blj(LxszOS|1zota2N$cz$vf)@IdC zFPp`h=ufR)gXlFGxam68F1=RA`zqzVn7?o^mO#foUcvICXUV5jlTPG_O59+dQuHCn z6E2p6Wwz@#q*y&!{6Hqad1;jg0VkUiJXWf~`@)vdM>OQxzUa7aMOREO`RxA4n-o=q z)ROIBj~eDirTbRUQN*4r5KzB}r6RZr${sPgL5gglq zu|=hWvlE~bOUyHrB~G9>L+RJs1)89gUkm2ao)FB7p5HU5^9pAtAIs5Dh`hn4uJ<4; z&ObQ7Klx^k;Yq>0_E=Quq-I8wla{7r;LgNHw~at9Es&`p8_?(}np}7VXHMwCr7pfz zqhd<&^(J?RxaHcqgk3;IjsoWY!mU@pwEWA*|2OVESdjoy`-&BQiOTntdIB`;xrjDM z`kl+GvrrVGUW6(M>u8izps#AGEqVn zu(viiN504GZRC0~k?YZ8(Is-bl<48RAre-6zn^PopF6q($RgF?FSf!$C{q0G&!~3= zw{$S?vKwU-bk0ODWMOuxowDN;a>(O;i`=$PC4LjN9N_bcKYmdlM4gNZI(9kz>RuL% zowf8M@IT`SQwH}53ONLOy_j}hj1Rg|dT=u(D-i`gmEdBgWR^CcvFqdqYiCc}#R4)? za3DgbBg5MVs(2GitUb1XhK*nvtvi`kS~LDLD$2PpVjET(CeUHGb+TL)IybxX zs3H$$8&8c$wg*P!P6UPEnum8jR`u?OmFArz@sGWlK|#*1?Cznx2<#T35_6utx4#^j zKf|&0#Na?;meF^#&F99s6WsC{o+}@c`*c9+-VFg2oDM*lRfVYuWFM2WhIM6;1J5S# zJCY05a5mG+A>d{EVC?NEoAuy?J=*7KL-h`(F(dUD%3?$B+i1QFj!SpnA}3iJB01eC z?N;eV2L-B*2Aa$_iXB;Giee*gixBr}1uJz))WiF_RL(1JE^>o3JQ(WbkKb)GAoT{6 zP*`J<%RKHv5s>-k8Irb9@9R5muk7xEMRz&Vn&!`?1v&CYV*}>aL+!F$?!gC-VC)9< za35B(;)hL@6lzyPlQWK}Z_Ax-NE|>ObVY=D<&I_Tj=!D1v+Mqu94^7vGCPvB8;H@V zx}#PFDx+M3ambgn?Sf2L1EZfJzxPzI@Gg{NO@!m=Avw7^(V{X{7C6VIN{ximvh@o~ z-6;3)Xdf%0y7??Ae_nnpAF%XsZ`Pl+Ajt{fz*~#^}i(dE*vGYBu)J`~+ zU^#8##@PDGH?EpbWZo0%?_d5Rrx%+S*<@D#)oq(2tX3T(CnuKkZc~QxrNHeQPj2i| zIoGN zn3hqs0{t1^cyPO4M^WIW%w{elGjUD$Evx$Boy>+)TyTG15dzW^96Lu@vNAca&={j% z>Gi>m5wn|#bAt%lH z+}Q~{rJHM7hFQvY|KT!o11i|kn|xg%-<9QP^j2yG(2~HEHn~$hQcm61TMY7(+d6$& zj6{ut++W&l>s@Hiro34SV@kyg&gaeN>@+g!MK085`_eJKH%+d8My>o1IJy4Tkb(QO z8f}%U4K3y)Qk6*4oL3%TDsa-TqXlR{KxKOA$}f-SRLwitSRE;N3eS>1erxjUMJtrh zq9-85>I5n&ej7?5^nWyW=>4DH|I^dIwL#xxY=FuOJQVPO%IVS{nG=v>0UZ=_E~V*_ zbIAq4>5l~JHwp>JiO?^;cmVCxU*fGl$M|dW-w-Bn(7zB$|5tljfBf&?^q)un=bQeU z^ZLi;f8Dd|KR8$Owh&Pt8p7$$o6>WhmIH2oH+u?*qcl}GiO!H z?cL7+ZT{gw-xpy4yJz0M#n7avkTnumTRQWpQDPZkl!aS(ghQ!6hFfGET+8D@rdF?X65qxQTbw_ab5uhO9K+sRsSzvRh)%Yo_&bO1RW}aL3B%xX+1Y__5<{#IqmjQmN|wRANF_0bH9VA4BaO zE@!6eIvxLWq5&FIJ||M$l#=z^ep06#F>W`p!I0YeezsZhHPlqCIW@S6u9%)1YIGWD z!y{N<1+l!K&bd;AVe=gQ%~Z1!{q-gFBmk|?>A&xZbwwSTKe>23EaVq-NJ(`Th$!T@ zHw#XsWo2_uarUaSmD`?_hI`vY>Mors(ycWWFmrHDx*puu8+ZLhcOdldY8h0=kn?~9 z0xEm+KGU)p?K!c`P2XO%$2ZHG+K{V~dexPE7}@3DD+-m;9R3tnllB9`vu5wHIC#JW zZ7cUWO}NcDK)q0Uz_)P56&_w{5BE`W;_;mXuN-1ddzf{PVLz@z`Vua(Yg z2FH8ZZb}*Rp@Nj}k2Y+O_w)vB2d-8t(;8j}lMlM60)3zcgZW_v&%a%}xZ$xb?nnAK*vsz~6e zL+iq;W&lw*l#3)v40MVLrS3uLzHc(!a9^Ce64~ufSIia^LB)2O`Yz8ky8bngmMK*y zEzT26l3{^5!msZWrd+W6Tgsv4&iuV8h1EEYx)y z>~z*Msx!K;TPgX?5l&cO@B)z)|M>~F zH|S8D_`#Rga^r{D!Uq9(t8bcz_k9aT)NaGGgEY^gr!JMA-S?n=nl9Z7GLr(#i~j@FZ$&C zjs%ibE=95T;z_LMvswU>(}WiBg=sT6hLdCrr@rBUsxUD25B*HBfImO1&Ok!Tl}5Hh z!v!GBT3m6UtW{IpFceY#UX0``TYU$WaMp6y@Gp6Il92py0cB6vh!Aw5?V1OCfr}@b z-5Gv~GcQbbx6>%r`gUKa1=E$$-usylt1|>8%Qd*hnAO+q%OsgduQsI-3F_X*G;(IE z3T)QLEm^U5ju2rxdS$n=)w0;^HI=VtDC2y0iIi{3xC0gSyLpom<>($RSLe$X_mOm6 zof5w+kk5vn{^1SAwfD|x8g7evm(6{Tuhgem>$0fi(v?i|N%SS%igllGX5FN@Od$^S z8+uO$^@ZM0nT~@neIO~#bIrYCix?fGWO)aEQsjCYf>4e7I+ZLg4{rhdo_&?~Dc&fG zcEHvmThn9qMd8SF&Rn}M)501wyG~+n7R2WU7rr{&Rq-;?kZA7>M(jd4*P%Exh7M~> z#ymrb)y_=X#qFHG3kOpFmDh%Hv!GhI8+mY~=7f0=mc|l4A^yqA=o*fW^k-gT!=D)s zt0aP^4ibk1-W-bUq5@wei?%nfWJUT2K6rx)AKpddw0=<2pmAC=#)r`mvzyswA}VY^ zcJ|}OHst6<8{IdEhjjy=jVf0LTNqfF(G90gE;As((<2Y(I1XfT+|@?Zz2IrO*A3X) z-7oTb<=RIr&8E`a7EinTJN#dKFFU*1JtMEum2poL)4x{t!L)qjTw;NW#&L2>Z7mJIUD!FUXtifVxfHJaF+3+oqNbeD_d z02r{SmS5O(8^-mX$|HP;)ScMf2IsvbQ4d6Jkr8;m_-pDJMWQ!P0V^kdoobajz=uiz zu5k>E8>Uk8q2y^=P`LxW(X+WCd!KgBLg^3y*bC$$YemuWNHAJ^ojesSscXce;2{ba zD4(zA7_DW&z~^*2ILuB%BliY{?oHSe1_US%K;zQH2Oo##%B#pB(e`9H)k)~s?f~}s z$udQl;>kWmHEtNBy3E^!gT{lRR8t`??Yqvbu@`(%>MB0b`uV(^RGa2EKC1cP094Y- zoi_K^Y0bDAs+qha+)B)G>mJadbpEGdl#<>V0H&~MSiEy~HS(oP4lC^iiSh!r@ei_A zN)Zt-^nY0oU@rc`Z2ebzgWdwnNP#dZiIC?=bkrTfS&e(LghLwCRZUE*Y6ncQyo?>K zIs&k?pmmxIPmd)fLgF00D2-cqkBl0~?n*K8$}DS8heX##A)NjDFxaf`rd@**j$MKd zIuDD2Vm}C@lC|uwAUZ7dktZj+ltWzvC=yJz^(#U`Se!T*_xGv4PY=1m1DMD7S-%mD zT~fXS8)X9xELRz~u;5rI^}Sqs(K&zA5Om%2y@n7e`_!f&@nRXA28%W{_^T7kP0h^F%*&Yg zdT6c|zfA+0t7W0k5^naeI`vS(GnRy%HG7+|^PaH}`_0p&{euJE*d5}u%K>N?8UtTd+?^ESBzT z%3U#->8kFF!<}ZnQsK1&GYmz;WtO=;nUGPk-;k}wKaj2dqplqOpC=zOHjNe_Ow8Gt z)l)cQ^vj%G*s$1OZugF4gHIY$B)^6{fQ}5NB~ZUD+r40YYIN#yUsYV|@=eRrkCcb0 z?$sIf{<5k+!l`|((~+N^EUvr(z?>aV>wC~x2Hksog@b0Gotks;cS!myab|xjo-LnRm@Jc;y1B7QEM8 zS!)SwfTzI{EgdY^5s5U3-8~aUqHYR=A-Ss))x$V+zqVyu`}OjfaFPS^76S6nkEq4@<-KEcS|3 zPw?qeUCWvHW%<3a36o9<=Vs&o={;M_~xWc*>03 z8uuPP*-SE;ceC*dhOAMznp8HEC616XY)pa}qJQRXG^OTbd(fW1P>GGeko{D%nYP`P zc9#Qj^*ZQ%8bp1rW*z_ja8penKN0}4AaP!2=K;FvgfQk(r9}cj^VvEs3>bMPh~3pd zMEWYLf)MdAAsI`;Tzd2tF^nN*<9tDmjgqQ$`}J3a4vGiZBE%3Tt#0#fb%rL3uYA=Z znk$R5>Q3jdoRB0)T}%K>oMNqxD2T=f=+NX%v}E@TiGJn~V%bVhU>P~OJt$gWxTiM{nasgxSf{$d%? zK(6})?kdJ(48r3XS9& ziO?r{S=2tTJpJhjh(~-GFDrR>d>}b}a>U#cIA1jvubxum%!WYG`5k;pDIsBDv^n*& zP}P>X(J+8xGZIAjs>t-{Fz}l3f{DY4huQfVSZ=o3$$Y%rL$Yk@(ymzF&%% zfg7uM9PN&{N1njF*m08a@4Ho5UoXcZ9|>CdfNFQkMH$&#m~tOAFtKCCK)y!0Mf9 z^$X5vMIZX))-_CZ{6}>poBU%Wtv5@**zWK{?-YC|yiPEILl#5&{MRv1$g8*f3X40S zfs{*WNC&O1j7qv6F~+Q3%{@&vAwf=6Sf(pcT)&2c*dcann_pUC10hUiY=fUx9%@I+ z>}!T(d;Qz#8u^`P0LC>=lWOR5gkwZ)+yZ+ea_jX_28n!la@ev;cjubw0@bQ$b42E3 zJ^_sg)B?@@5!oU3#7qLKHiM3L9|bRaFT3pnD8cEy!0pS3lW5deU)O-uOgb`x+cuoH zCf~{GiI}Tn`wuK>;C%PWl-A=nWbhh3e+f@hJ^JH7_+cl--+948idX`wbwAeOG-f6L z_n+c?SiNJ~m_`RlltQSfh6{H57<^H$_`WMUw%5_2#w?)eP8~<|ak#s0_1+rteX*)9 zJf*wY%>jEQdg}lI3ML%&dG~ESN~AMS_DMD`wFF-yX`La2^<)?Q zj@h2FZDtOup$&~fP}fzM(;2~Cvs+t7J$nKHWertroAjNM%jK;_F9zCuO^gaigq~N3 z2u9v<1M^v*8CqmZGaY zk{tJ;o^2O`F}LgxQ;|EQdx}@YUg|48-|Nq0)=35$N{k}geNu7=lkmeb`X&R>Cc$wf*K+yCIf`f5E$$gm8TH)*1l3i{6fIsdM{!$ph^%hFIFQhzj}K zvbUBTu>d#ZEq4rfTWR>y9=OJ1U+T?Y=d1#_RENyh2rs;@&A@OFf2n@t0*)Szj_7s1 zhMNMoLhC#9Y*e*+V>wTpE!Xw+fByUlUKCNn-NFUvU5ni+@TB8JKo@#WcGDKZC$;MA ze=Fc_g^fEx5=zNDyR=Os;)o*~_fj#Mq!}i5O!@0QV51!Tiex&bOk2Qxp+RqimwoL% zp?9jzZDYH+*1I!NMW3o%^bkiXNct2K zzy6z9T%pY5H;(sr4&jm~(HYycLI=U@E|Ukm6tet$FRmz7_H; zMaBA**^VC3UFBD>w@V6wm+|@tiRQzTGj#4+AI{WLo^QRThJH3>WhiF=WT5nbJ3@%; zO0J9?y18*t;(Sd|@=vn%(#hpOzdd3x<1D6H#OfJ=k>z$9SYI8R734*G4{_8sN^HMTl)-SP3h!9WWybJRVG`Cg1iGDOWFv_s=VcA^-q*&DWknDB>!sc;%YKJ>)?? ztyz>VkBw7Zd~p#}ykwmQKuW1-GVcKB7qKjhqS7I}s6Ewbz*TuaWf-^~bRB1xc^cZx z9O6%l-i1()@fqlHXJy5+`D&R1u@*e7Doh7{=$mw(b?-qbX={;ySexd39-9IE8o7ZC z5KJCx4ZOAT3qURO6#*yQWmt+&Q=styx$&51Rh6d8#zZw-<3=g>7~fm&F`6rgn{>W= zPBK$L{NoduO%S2UvU$=z!VhmB594&m&tAPo-Z%M*&)}VG&GwuM@*r9xmG!iDf-xac z4|^zJ9C{YS^B4SoZd2$y873R20rePu8&hkLjoooQ0D*OQ?E0Ol2#3*N z3_Dw8s=i4{+xdG$j`3)z1(1Zjkk4Fw3*m}=L(Xes(JB~xp0pwh0`8A9<^l2E6Xx-+ zacwZMNrbAvi5z9`=THiRyd@Y-e-{xeTPIT+=2gM;cKP6`|-K3AQ%8jnRlwL4aMZ+ zU}L~WGLr{L_?eM)HDfr^{m#BxORxRRKk^koKpN>QCozOZDXIOKxj3USjuUyFCjEhQ0hxgD263p@bf%JsLp~|uaXc~nIU#g+`E_BL$Knlq=5~=> zI;2q>4ei}8LE9Z4Mwp*oj}t^h-!&JN^=wmCih6%!R$6ow-kz_=u-xaymUzkZb$qfp z_A{9Wn-HOZJ!P%-PtWj;bfWE`oREYNjoQe7wvRofPVfNI;GsguIxJ-?gc_C-!@?0k zSv_kk!SaytL$B?3Wv08A2%wzcliEHi&@-!dR}wRTdb1o{=D~X>w%gMf%RaXa5zMHa zL8sYO;SO=UQn+i;zwT%X&TbZ{O$8<>QD~o;y;d@)k0mZ;z)XS_#Z8^8nF0u@@O-*_o;#vBH@QI$_2~STc0uF$=$I#SW8hWz;8xyS!yJ7Avx3=DY`25Dq#92A=IS~a?08nJNm@tl>|ER zq(aEEpMip~>Y~?b_C!KM#*N%XNAi*VdgXXFxFFqaNygS)f=`2W0Q%PvriM}5);W*> z1(qnwM~zIP>Qz2c6%Zjlw)BWA!g#~gpNGbS^+cvagg$esJYA;~cvq!qY-Ge}XBUb8 z_+2PUQd=WtGv{=fgK*MuhP39l^vidpWZ~dzv-Ph;J?7n?{pBIoL9yhM8{*-eiv18p z=KYROhv5Cb)w#}Nv~Lu95oSDnAD7&@hs@IaR=}m#{hTOIE}O9vi^q!)5V^%FZHJ}X zmrg+79^$gb7X~IR#URDyeY^k-Yvn<%@@CGCla=ZSL4p~Ngdao^oX|p$tLbGZ(z$aTos&Bz zZDQCM=fR-9zKHVtE2A$BItIVshaPkwbw1b5-+7^wu(iadHfk^BObqf|7M_d^%1-9S|)s zhyNX_|1(VgV|9M>!~TuUL7SzEr+y>EE)`vFZp=<2RDuC)deN~`zY<|ofiw=^9^10- z(s6>)JA}}zqdg%`!u1Y1*hn(rWOk4k^jqvDr({VsYY-Jfh&2g4iKH-a+HT@vBD6b- zAj8;#BdmR$(Go8b?tDn{W>DHgB*ik2VIN2b$fpskl>R1nvD9A5NQ$4!2;;TwtFx(fjwKX03E27M ze^_F7M)99ux8yOi=uy1_darrv*>(-}goIq_Szcn9O|p2q>MhS}^T}NreZnl~3p1mG zs)uFh#f&-IkCPGYn`?#;ZGoWC;$}fEB=9#!Eo>_0j?j3B2Y)UZ&EB#)(~60L-LsnCFj7S^~2D zk(~WKi6ByXZWN>CX)68746chfo16Q-+UP;iw=?ZB3<^4M4}nJxQm)JIr3ptIe^fee zxX1pmIRw(xfatr0XtEfAumkG_Sq!iM0a=VeDu2VHE1*{nY4EXUwmURTCs$?u9KJo> zs_0m0zvd7ddB$czfDmElzkwxv#1V7|Yk5!SRJMCvha&!Aa& zYABF_&Q`d#l(XwIca+~h{J}dS^ey>}$D&vG!iE2@4*Vi~bov z92;2PNp+v*3Q6q~ur*l;8W5jwLkbXem(1Xw)BAWlZ@)u$7i0h2<2&O$0?l{9UpT+* zn3!_en&;bpe>tiue%x1+&bM?@xK&*wukb70UY-fwhcOgz>WX#u{S!<~x+HQN1BY@9 zd&k_cJa+@yyI1Nu@7giGxL4FNKB5^|cJ!inH*u*f(#5iGjdSp>*Y`82Sn^7( zeB}KcVVFS8C_XJ~^@l3e?+C{GCI-jRd9MO1xT7&TqV|Mo@V7d-ZG*Jl1PDo$ww293 zo?LtHMUZ*p>v+RP&-9%0kcAg^X0$@N-l`LCh2h<@C%%?UGE1uLMpsMnE(LF)+A=K_ z)8`YVYJzvGOII8HZaK))?4gwHnRR98AA^PvpxL1{gpu$Pl;0oK=gaMR+s#2Mql@u9 z_x}B00kll#w+8G&^VK7icjZDEb~*3Lg-R^aT_N)1Z#mefL9{vvE%R}r6N+=c&@%ni z1MhD=*hS|&7)rIjQ3D74iucohF}(i3v;If&7w-S0Lb`avOV7bM|C9U`b3f)kR7n3b z`RfnI4Bf?#;O~xn=*#N@;z&0AoZoIe<9WY!|l#rN;ZZubhF9d4*+EQ>m~oGVdP(%|KHow$&lAv zAhO+m!)GHHeHFj22b})ztGeiyX+`a0g)yV>)BY?v6+Dgw1ovb``w5>sn&Y_aT%zrw?ZM3;pmSiUrx%Z=Zp8lOw8r{LZz1pDE^pS%(G^0_gtG2|T^}~8P zmh+-_>_=i8HI4AivcB-fAh=UjS~fZ2v|!=hC_|aQhMJLNpD1GEDa$=ra!{{&z&uf$ zh;LGB$4d0~?nb7*jTs+CzWO#skHgVS_(I0a=ZUq4z{gGY&TKLy3@#_d(U{0;)*_75 z$hb54gF^0SYj!yg@vXa}k2hSRnQdvpAP3V=1Oyvim&I>57lzc8xGvCM54}~!;wwD^ zdq`MX(B(O8nf8vEE$c*>%YZHU=}b@tay^=9ckxJHz~9)Yp7c#WvNWYeh|Qov+Ay9( ziuFn0M$Okot864FtExO1F-1rkwt-F&lY$hy*jBf6^9jrjqTR%k_G-k*{aYyd6FK5J zC^kA)`WGMAXFA>7UtZU7epjd3nw0kVb`saC^g74zaJ@3@vaVVv;6O3u z8%H&Jp9O{FmO9yMB0o5>oC%A_5O4W!cPOyhyRa4m88a+!or2kY)i=_lt7H4A<&qN` z`UTKphI(uUG^BKvwQe3=k&o*F@bn}G4_6hVA(N@mE0_scZhTah`;M6MXX(bMtv1)tDwk#StjyS$aRH9YKzW@=GFw}@M(7U`Q~iTsPL!WSq^x@H3x(aN&V3>Cq43>B(+$4~F{7urnKQ$`hZxmJWl=e;fAT`>Ana(RFZwg?Kbh z@7F5NEgeHqbGrCCR%!0Y|C0!_=<2s|II?>7kuOA%S1N&m_Z3BJKd$$(O=_g+*@Lpc zlRY@HMN^NkEZ%mlq1nWj?zY6Ia~DzWkGTf16O%1BukOKJ4%6ewYN~&pHvEu(mpzDg z;wG3Egp+r*FMqZBRXL4`;hm({SsRmF}7>cS{Nn~2E7XiEir z9iN-cO)ck1$jCCOE+}-5usjuyU4w1CF21ulJTt^kGQepNw<=pG zZ=0$OM!x;g(p6l&7J&NYGp1%+< z!m|^6np7uH^OwV=T5w1!$`0G=b^v0n{)tjDM0Lx|w{zs1@@_{r&;xinw+~uKp)i7})qm7Iw6mhU66LMPe~Z^(-1w*?uM~IVj%HgiDaZ7y?N+{!G~?}2 zwdPUB9EJuErL8DOTqZ?6j{pWz0YB_zj2z0^Qaxrf-{MZK-0|fy zr*1^^g11q278$D9Q2}{{B5Gfa2jP+LmUpG^=ftq8_xah17=oC(lAY_^vMqu^cVII& z*@6?#1*h-Hsy&C+^Cg%|tVi;STv{YGSM)L#c|iIBR(stE!An82Z}3v3A@`4&{P6<( z#|n|zH)ujztE)S(LiU=)dAMN6StU(PRHRh5?P>!lB=jSw3yM5m?7E-F~a2Gw=Lc8=fTo!}pA|d{U{!DyIqo_bSo<+U3 z5V@)^KSNo=2v5*|+*D`IXWa=qf1b}`$JV1+qsTZ;KMsht~Y+Q5=fqAS9V9_ou>{FM=+gFG$29eafvM z*=9mqmSYSGFk1tb^RFsYyUg42iyvpyFB=>;B1+CL?->l?zG*`wgYjoK6h z#kQj6h3s8lTYaJz-27Eonc0D`%41uG;aO!*#M%9bbL-LtyQ|T(WX-o$Yu#8U(q9IL z=>we;MQLwzHn!1B{IwG{VZtTNpiY<_;(48JR(sLgQ-Z_-{YI z)8z6f0AbZy7|7n>67!?!l=~{)tiBe$Lc&6!jS?R6mS;iHs-t1fD_`9ZzUYEqF!+^% z`OS_W;(=BuEHNR4J3iEOv#np2n|o;-@p8W;7Vog9i}}Bzp3R4%f=7nrgKM z;>otiNVSf{;wYmd_B(e|^FK~x+q}IDJuB2s_t+%O57?e-<71E7JMAwI>juz~9cS!l zhh##J_~9Uwx-Plpm&Df8+w#6U$H}?R@)V9Y_yseJd%OTXZGT3hYjcGH_eAA*Lj(ES z3EoZ7@XwI{m!bVlxAEmf^3+qdw1A)c5%aiTfCEm-QcxIC5{%0A{p3{ri%7;8si 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 diff --git a/docs/guides/observer/observer_plans-text-diff.png b/docs/guides/observer/observer_plans-text-diff.png deleted file mode 100644 index 096c40135fd181c0c69dd6a28a482a955716a76d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31775 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 diff --git a/mkdocs.yml b/mkdocs.yml index 9bcfeeafa3..aa4db57cb4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,7 +34,6 @@ nav: - guides/tablediff.md - guides/linter.md - guides/ui.md - - guides/observer.md - Advanced usage: - guides/customizing_sqlmesh.md - guides/signals.md From 6c98222fa8d33c360659a7f508ddf115b0b7a2ea Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:34:06 -0700 Subject: [PATCH 0527/1056] fix: check sqlmesh version before sqlglot/schema (#4898) --- sqlmesh/core/state_sync/base.py | 18 +++++++++--------- tests/core/state_sync/test_state_sync.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index 4a4e31854f..6c2097d760 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -245,6 +245,15 @@ def raise_error( f"{lib} (local) is using version '{local}' which is behind '{remote}' (remote).{upgrade_suggestion}" ) + if major_minor(SQLMESH_VERSION) != major_minor(versions.sqlmesh_version): + raise_error( + "SQLMesh", + SQLMESH_VERSION, + versions.sqlmesh_version, + remote_package_version=versions.sqlmesh_version, + ahead=major_minor(SQLMESH_VERSION) > major_minor(versions.sqlmesh_version), + ) + if SCHEMA_VERSION != versions.schema_version: raise_error( "SQLMesh", @@ -263,15 +272,6 @@ def raise_error( ahead=major_minor(SQLGLOT_VERSION) > major_minor(versions.sqlglot_version), ) - if major_minor(SQLMESH_VERSION) != major_minor(versions.sqlmesh_version): - raise_error( - "SQLMesh", - SQLMESH_VERSION, - versions.sqlmesh_version, - remote_package_version=versions.sqlmesh_version, - ahead=major_minor(SQLMESH_VERSION) > major_minor(versions.sqlmesh_version), - ) - return versions @abc.abstractmethod diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index cf1d35bbfc..f0b1bf00a9 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -2091,7 +2091,7 @@ def test_version_schema(state_sync: EngineAdapterStateSync, tmp_path) -> None: with pytest.raises( SQLMeshError, - match=rf"SQLMesh \(local\) is using version '{SCHEMA_VERSION}' which is ahead of '0'", + match=rf"SQLMesh \(local\) is using version '{SQLMESH_VERSION}' which is ahead of '0.0.0' \(remote\). Please run a migration \('sqlmesh migrate' command\).", ): state_sync.get_versions() From 61455f2ac06866ead19aefb8759751dbad0f38ce Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 4 Jul 2025 14:31:43 +1200 Subject: [PATCH 0528/1056] Feat: Allow specifying a minimum number of intervals to include for each model in a plan (#4780) --- examples/multi/repo_1/linter/__init__.py | 0 sqlmesh/cli/main.py | 5 + sqlmesh/core/console.py | 4 +- sqlmesh/core/context.py | 122 ++++++++- sqlmesh/core/node.py | 3 + sqlmesh/core/plan/builder.py | 25 +- sqlmesh/core/plan/definition.py | 12 +- sqlmesh/core/plan/evaluator.py | 3 +- sqlmesh/core/plan/stages.py | 3 +- sqlmesh/core/scheduler.py | 49 ++-- sqlmesh/core/snapshot/definition.py | 27 +- tests/core/test_context.py | 325 ++++++++++++++++++++++- tests/core/test_plan.py | 15 +- tests/core/test_plan_stages.py | 13 - tests/core/test_snapshot.py | 58 +++- 15 files changed, 583 insertions(+), 81 deletions(-) create mode 100644 examples/multi/repo_1/linter/__init__.py diff --git a/examples/multi/repo_1/linter/__init__.py b/examples/multi/repo_1/linter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 93d3051bcd..ac1b69ee62 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -520,6 +520,11 @@ def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None: help="Explain the plan instead of applying it.", default=None, ) +@click.option( + "--min-intervals", + default=0, + help="For every model, ensure at least this many intervals are covered by a missing intervals check regardless of the plan start date", +) @opt.verbose @click.pass_context @error_handler diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 2435e56ea8..ae48722288 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -2073,8 +2073,8 @@ def _prompt_backfill( if not plan_builder.override_end: if plan.provided_end: blank_meaning = f"'{time_like_to_str(plan.provided_end)}'" - elif plan.interval_end_per_model: - max_end = max(plan.interval_end_per_model.values()) + elif plan.end_override_per_model: + max_end = max(plan.end_override_per_model.values()) blank_meaning = f"'{time_like_to_str(max_end)}'" else: blank_meaning = "now" diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 4203e35739..c8cfbda03c 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -46,6 +46,7 @@ from pathlib import Path from shutil import rmtree from types import MappingProxyType +from datetime import datetime from sqlglot import Dialect, exp from sqlglot.helper import first @@ -125,6 +126,8 @@ format_tz_datetime, now_timestamp, now, + to_datetime, + make_exclusive, ) from sqlmesh.utils.errors import ( CircuitBreakerError, @@ -1222,6 +1225,7 @@ def plan( diff_rendered: t.Optional[bool] = None, skip_linter: t.Optional[bool] = None, explain: t.Optional[bool] = None, + min_intervals: t.Optional[int] = None, ) -> Plan: """Interactively creates a plan. @@ -1268,6 +1272,8 @@ def plan( diff_rendered: Whether the diff should compare raw vs rendered models skip_linter: Linter runs by default so this will skip it if enabled explain: Whether to explain the plan instead of applying it. + min_intervals: Adjust the plan start date on a per-model basis in order to ensure at least this many intervals are covered + on every model when checking for missing intervals Returns: The populated Plan object. @@ -1296,6 +1302,7 @@ def plan( diff_rendered=diff_rendered, skip_linter=skip_linter, explain=explain, + min_intervals=min_intervals, ) plan = plan_builder.build() @@ -1345,6 +1352,7 @@ def plan_builder( diff_rendered: t.Optional[bool] = None, skip_linter: t.Optional[bool] = None, explain: t.Optional[bool] = None, + min_intervals: t.Optional[int] = None, ) -> PlanBuilder: """Creates a plan builder. @@ -1381,6 +1389,8 @@ def plan_builder( enable_preview: Indicates whether to enable preview for forward-only models in development environments. run: Whether to run latest intervals as part of the plan application. diff_rendered: Whether the diff should compare raw vs rendered models + min_intervals: Adjust the plan start date on a per-model basis in order to ensure at least this many intervals are covered + on every model when checking for missing intervals Returns: The plan builder. @@ -1408,6 +1418,7 @@ def plan_builder( "run": run, "diff_rendered": diff_rendered, "skip_linter": skip_linter, + "min_intervals": min_intervals, } user_provided_flags: t.Dict[str, UserProvidedFlags] = { k: v for k, v in kwargs.items() if v is not None @@ -1530,6 +1541,16 @@ def plan_builder( # Refresh snapshot intervals to ensure that they are up to date with values reflected in the max_interval_end_per_model. self.state_sync.refresh_snapshot_intervals(context_diff.snapshots.values()) + start_override_per_model = self._calculate_start_override_per_model( + min_intervals, + start or default_start, + end or default_end, + execution_time or now(), + backfill_models, + snapshots, + max_interval_end_per_model, + ) + return self.PLAN_BUILDER_TYPE( context_diff=context_diff, start=start, @@ -1560,7 +1581,8 @@ def plan_builder( ), end_bounded=not run, ensure_finalized_snapshots=self.config.plan.use_finalized_state, - interval_end_per_model=max_interval_end_per_model, + start_override_per_model=start_override_per_model, + end_override_per_model=max_interval_end_per_model, console=self.console, user_provided_flags=user_provided_flags, explain=explain or False, @@ -2850,7 +2872,7 @@ def _plan_preview_enabled(self) -> bool: def _get_plan_default_start_end( self, snapshots: t.Dict[str, Snapshot], - max_interval_end_per_model: t.Dict[str, int], + max_interval_end_per_model: t.Dict[str, datetime], backfill_models: t.Optional[t.Set[str]], modified_model_names: t.Set[str], execution_time: t.Optional[TimeLike] = None, @@ -2858,7 +2880,7 @@ def _get_plan_default_start_end( if not max_interval_end_per_model: return None, None - default_end = max(max_interval_end_per_model.values()) + default_end = to_timestamp(max(max_interval_end_per_model.values())) default_start: t.Optional[int] = None # Infer the default start by finding the smallest interval start that corresponds to the default end. for model_name in backfill_models or modified_model_names or max_interval_end_per_model: @@ -2887,19 +2909,101 @@ def _get_plan_default_start_end( return default_start, default_end + def _calculate_start_override_per_model( + self, + min_intervals: t.Optional[int], + plan_start: t.Optional[TimeLike], + plan_end: t.Optional[TimeLike], + plan_execution_time: TimeLike, + backfill_model_fqns: t.Optional[t.Set[str]], + snapshots_by_model_fqn: t.Dict[str, Snapshot], + end_override_per_model: t.Optional[t.Dict[str, datetime]], + ) -> t.Dict[str, datetime]: + if not min_intervals or not backfill_model_fqns or not plan_start: + # If there are no models to backfill, there are no intervals to consider for backfill, so we dont need to consider a minimum number + # If the plan doesnt have a start date, all intervals are considered already so we dont need to consider a minimum number + # If we dont have a minimum number of intervals to consider, then we dont need to adjust the start date on a per-model basis + return {} + + start_overrides: t.Dict[str, datetime] = {} + end_override_per_model = end_override_per_model or {} + + plan_execution_time_dt = to_datetime(plan_execution_time) + plan_start_dt = to_datetime(plan_start, relative_base=plan_execution_time_dt) + plan_end_dt = to_datetime( + plan_end or plan_execution_time_dt, relative_base=plan_execution_time_dt + ) + + # we need to take the DAG into account so that parent models can be expanded to cover at least as much as their children + # for example, A(hourly) <- B(daily) + # if min_intervals=1, A would have 1 hour and B would have 1 day + # but B depends on A so in order for B to have 1 valid day, A needs to be expanded to 24 hours + backfill_dag: DAG[str] = DAG() + for fqn in backfill_model_fqns: + backfill_dag.add( + fqn, + [ + p.name + for p in snapshots_by_model_fqn[fqn].parents + if p.name in backfill_model_fqns + ], + ) + + # start from the leaf nodes and work back towards the root because the min_start at the root node is determined by the calculated starts in the leaf nodes + reversed_dag = backfill_dag.reversed + graph = reversed_dag.graph + + for model_fqn in reversed_dag: + # Get the earliest start from all immediate children of this snapshot + # this works because topological ordering guarantees that they've already been visited + # and we always set a start override + min_child_start = min( + [start_overrides[immediate_child_fqn] for immediate_child_fqn in graph[model_fqn]], + default=plan_start_dt, + ) + + snapshot = snapshots_by_model_fqn.get(model_fqn) + + if not snapshot: + continue + + starting_point = end_override_per_model.get(model_fqn, plan_end_dt) + if node_end := snapshot.node.end: + # if we dont do this, if the node end is a *date* (as opposed to a timestamp) + # we end up incorrectly winding back an extra day + node_end_dt = make_exclusive(node_end) + + if node_end_dt < plan_end_dt: + # if the model has an end date that has already elapsed, use that as a starting point for calculating min_intervals + # instead of the plan end. If we use the plan end, we will return intervals in the future which are invalid + starting_point = node_end_dt + + snapshot_start = snapshot.node.cron_floor(starting_point) + + for _ in range(min_intervals): + # wind back the starting point by :min_intervals intervals to arrive at the minimum snapshot start date + snapshot_start = snapshot.node.cron_prev(snapshot_start) + + start_overrides[model_fqn] = min(min_child_start, snapshot_start) + + return start_overrides + def _get_max_interval_end_per_model( self, snapshots: t.Dict[str, Snapshot], backfill_models: t.Optional[t.Set[str]] - ) -> t.Dict[str, int]: + ) -> t.Dict[str, datetime]: models_for_interval_end = ( self._get_models_for_interval_end(snapshots, backfill_models) if backfill_models is not None else None ) - return self.state_sync.max_interval_end_per_model( - c.PROD, - models=models_for_interval_end, - ensure_finalized_snapshots=self.config.plan.use_finalized_state, - ) + return { + model_fqn: to_datetime(ts) + for model_fqn, ts in self.state_sync.max_interval_end_per_model( + c.PROD, + models=models_for_interval_end, + ensure_finalized_snapshots=self.config.plan.use_finalized_state, + ).items() + } @staticmethod def _get_models_for_interval_end( diff --git a/sqlmesh/core/node.py b/sqlmesh/core/node.py index 98a24884cd..874e74b3e9 100644 --- a/sqlmesh/core/node.py +++ b/sqlmesh/core/node.py @@ -31,6 +31,9 @@ class IntervalUnit(str, Enum): IntervalUnit can be one of 5 types, YEAR, MONTH, DAY, HOUR, 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. + + It's designed to align with common partitioning schemes, hence why there is no WEEK unit + because generally tables are not partitioned by week """ YEAR = "year" diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index ff953c75a2..567920997e 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -5,6 +5,7 @@ import typing as t from collections import defaultdict from functools import cached_property +from datetime import datetime from sqlmesh.core.console import PlanBuilderConsole, get_console @@ -85,7 +86,8 @@ class PlanBuilder: ensure_finalized_snapshots: Whether to compare against snapshots from the latest finalized environment state, or to use whatever snapshots are in the current environment state even if the environment is not finalized. - interval_end_per_model: The mapping from model FQNs to target end dates. + start_override_per_model: A mapping of model FQNs to target start dates. + end_override_per_model: A mapping of model FQNs to target end dates. explain: Whether to explain the plan instead of applying it. """ @@ -117,7 +119,8 @@ def __init__( end_bounded: bool = False, ensure_finalized_snapshots: bool = False, explain: bool = False, - interval_end_per_model: t.Optional[t.Dict[str, int]] = None, + start_override_per_model: t.Optional[t.Dict[str, datetime]] = None, + end_override_per_model: t.Optional[t.Dict[str, datetime]] = None, console: t.Optional[PlanBuilderConsole] = None, user_provided_flags: t.Optional[t.Dict[str, UserProvidedFlags]] = None, ): @@ -133,7 +136,8 @@ def __init__( self._enable_preview = enable_preview self._end_bounded = end_bounded self._ensure_finalized_snapshots = ensure_finalized_snapshots - self._interval_end_per_model = interval_end_per_model + self._start_override_per_model = start_override_per_model + self._end_override_per_model = end_override_per_model self._environment_ttl = environment_ttl self._categorizer_config = categorizer_config or CategorizerConfig() self._auto_categorization_enabled = auto_categorization_enabled @@ -280,7 +284,11 @@ def build(self) -> Plan: self._adjust_new_snapshot_intervals() deployability_index = ( - DeployabilityIndex.create(self._context_diff.snapshots.values(), start=self._start) + DeployabilityIndex.create( + self._context_diff.snapshots.values(), + start=self._start, + start_override_per_model=self._start_override_per_model, + ) if self._is_dev else DeployabilityIndex.all_deployable() ) @@ -291,11 +299,11 @@ def build(self) -> Plan: ) models_to_backfill = self._build_models_to_backfill(dag, restatements) - interval_end_per_model = self._interval_end_per_model - if interval_end_per_model and self.override_end: + end_override_per_model = self._end_override_per_model + if end_override_per_model and self.override_end: # If the end date was provided explicitly by a user, then interval end for each individual # model should be ignored. - interval_end_per_model = None + end_override_per_model = None # this deliberately uses the passed in self._execution_time and not self.execution_time cached property # the reason is because that there can be a delay between the Plan being built and the Plan being actually run, @@ -322,7 +330,8 @@ def build(self) -> Plan: indirectly_modified=indirectly_modified, deployability_index=deployability_index, restatements=restatements, - interval_end_per_model=interval_end_per_model, + start_override_per_model=self._start_override_per_model, + end_override_per_model=end_override_per_model, selected_models_to_backfill=self._backfill_models, models_to_backfill=models_to_backfill, effective_from=self._effective_from, diff --git a/sqlmesh/core/plan/definition.py b/sqlmesh/core/plan/definition.py index 7325dc3532..584c0d9b51 100644 --- a/sqlmesh/core/plan/definition.py +++ b/sqlmesh/core/plan/definition.py @@ -57,7 +57,8 @@ class Plan(PydanticModel, frozen=True): deployability_index: DeployabilityIndex restatements: t.Dict[SnapshotId, Interval] - interval_end_per_model: t.Optional[t.Dict[str, int]] + start_override_per_model: t.Optional[t.Dict[str, datetime]] + end_override_per_model: t.Optional[t.Dict[str, datetime]] selected_models_to_backfill: t.Optional[t.Set[str]] = None """Models that have been explicitly selected for backfill by a user.""" @@ -177,7 +178,8 @@ def missing_intervals(self) -> t.List[SnapshotIntervals]: execution_time=self.execution_time, restatements=self.restatements, deployability_index=self.deployability_index, - interval_end_per_model=self.interval_end_per_model, + start_override_per_model=self.start_override_per_model, + end_override_per_model=self.end_override_per_model, end_bounded=self.end_bounded, ).items() if snapshot.is_model and missing @@ -265,7 +267,8 @@ def to_evaluatable(self) -> EvaluatablePlan: removed_snapshots=sorted(self.context_diff.removed_snapshots), requires_backfill=self.requires_backfill, models_to_backfill=self.models_to_backfill, - interval_end_per_model=self.interval_end_per_model, + start_override_per_model=self.start_override_per_model, + end_override_per_model=self.end_override_per_model, execution_time=self.execution_time, disabled_restatement_models={ s.name @@ -303,7 +306,8 @@ class EvaluatablePlan(PydanticModel): removed_snapshots: t.List[SnapshotId] requires_backfill: bool models_to_backfill: t.Optional[t.Set[str]] = None - interval_end_per_model: t.Optional[t.Dict[str, int]] = None + start_override_per_model: t.Optional[t.Dict[str, datetime]] = None + end_override_per_model: t.Optional[t.Dict[str, datetime]] = None execution_time: t.Optional[TimeLike] = None disabled_restatement_models: t.Set[str] environment_statements: t.Optional[t.List[EnvironmentStatements]] = None diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index a8e2aa7919..03f8bdcf71 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -256,7 +256,8 @@ def visit_audit_only_run_stage( plan.end, execution_time=plan.execution_time, end_bounded=plan.end_bounded, - interval_end_per_model=plan.interval_end_per_model, + start_override_per_model=plan.start_override_per_model, + end_override_per_model=plan.end_override_per_model, ) if completion_status.is_failure: diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 863e53387f..9913a87bd0 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -524,7 +524,8 @@ def _missing_intervals( }, deployability_index=deployability_index, end_bounded=plan.end_bounded, - interval_end_per_model=plan.interval_end_per_model, + start_override_per_model=plan.start_override_per_model, + end_override_per_model=plan.end_override_per_model, ) def _get_audit_only_snapshots( diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index 2c7a2a66ac..2b9cb6189f 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -2,6 +2,7 @@ import logging import typing as t import time +from datetime import datetime from sqlglot import exp from sqlmesh.core import constants as c from sqlmesh.core.console import Console, get_console @@ -105,7 +106,8 @@ def merged_missing_intervals( execution_time: t.Optional[TimeLike] = None, deployability_index: t.Optional[DeployabilityIndex] = None, restatements: t.Optional[t.Dict[SnapshotId, Interval]] = None, - interval_end_per_model: t.Optional[t.Dict[str, int]] = None, + start_override_per_model: t.Optional[t.Dict[str, datetime]] = None, + end_override_per_model: t.Optional[t.Dict[str, datetime]] = None, ignore_cron: bool = False, end_bounded: bool = False, selected_snapshots: t.Optional[t.Set[str]] = None, @@ -123,7 +125,8 @@ def merged_missing_intervals( execution_time: The date/time reference to use for execution time. Defaults to now. deployability_index: Determines snapshots that are deployable in the context of this evaluation. restatements: A set of snapshot names being restated. - interval_end_per_model: The mapping from model FQNs to target end dates. + start_override_per_model: A mapping of model FQNs to target start dates. + end_override_per_model: A mapping of model FQNs to target end dates. ignore_cron: Whether to ignore the node's cron schedule. end_bounded: If set to true, the returned intervals will be bounded by the target end date, disregarding lookback, allow_partials, and other attributes that could cause the intervals to exceed the target end date. @@ -136,7 +139,8 @@ def merged_missing_intervals( execution_time=execution_time, deployability_index=deployability_index, restatements=restatements, - interval_end_per_model=interval_end_per_model, + start_override_per_model=start_override_per_model, + end_override_per_model=end_override_per_model, ignore_cron=ignore_cron, end_bounded=end_bounded, ) @@ -212,7 +216,8 @@ def run( end: t.Optional[TimeLike] = None, execution_time: t.Optional[TimeLike] = None, restatements: t.Optional[t.Dict[SnapshotId, Interval]] = None, - interval_end_per_model: t.Optional[t.Dict[str, int]] = None, + start_override_per_model: t.Optional[t.Dict[str, datetime]] = None, + end_override_per_model: t.Optional[t.Dict[str, datetime]] = None, ignore_cron: bool = False, end_bounded: bool = False, selected_snapshots: t.Optional[t.Set[str]] = None, @@ -227,7 +232,8 @@ def run( end=end, execution_time=execution_time, remove_intervals=restatements, - interval_end_per_model=interval_end_per_model, + start_override_per_model=start_override_per_model, + end_override_per_model=end_override_per_model, ignore_cron=ignore_cron, end_bounded=end_bounded, selected_snapshots=selected_snapshots, @@ -243,7 +249,8 @@ def audit( start: TimeLike, end: TimeLike, execution_time: t.Optional[TimeLike] = None, - interval_end_per_model: t.Optional[t.Dict[str, int]] = None, + start_override_per_model: t.Optional[t.Dict[str, datetime]] = None, + end_override_per_model: t.Optional[t.Dict[str, datetime]] = None, ignore_cron: bool = False, end_bounded: bool = False, selected_snapshots: t.Optional[t.Set[str]] = None, @@ -266,7 +273,8 @@ def audit( end=end, execution_time=execution_time, remove_intervals=remove_intervals, - interval_end_per_model=interval_end_per_model, + start_override_per_model=start_override_per_model, + end_override_per_model=end_override_per_model, ignore_cron=ignore_cron, end_bounded=end_bounded, selected_snapshots=selected_snapshots, @@ -565,7 +573,8 @@ def _run_or_audit( end: t.Optional[TimeLike] = None, execution_time: t.Optional[TimeLike] = None, remove_intervals: t.Optional[t.Dict[SnapshotId, Interval]] = None, - interval_end_per_model: t.Optional[t.Dict[str, int]] = None, + start_override_per_model: t.Optional[t.Dict[str, datetime]] = None, + end_override_per_model: t.Optional[t.Dict[str, datetime]] = None, ignore_cron: bool = False, end_bounded: bool = False, selected_snapshots: t.Optional[t.Set[str]] = None, @@ -586,7 +595,8 @@ def _run_or_audit( execution_time: The date/time time reference to use for execution time. Defaults to now. remove_intervals: A dict of snapshots to their intervals. For evaluation, these are the intervals that will be restated. For audits, these are the intervals that will be reaudited - interval_end_per_model: The mapping from model FQNs to target end dates. + start_override_per_model: A mapping of model FQNs to target start dates. + end_override_per_model: A mapping of model FQNs to target end dates. ignore_cron: Whether to ignore the node's cron schedule. end_bounded: If set to true, the evaluated intervals will be bounded by the target end date, disregarding lookback, allow_partials, and other attributes that could cause the intervals to exceed the target end date. @@ -634,7 +644,8 @@ def _run_or_audit( execution_time, deployability_index=deployability_index, restatements=remove_intervals, - interval_end_per_model=interval_end_per_model, + start_override_per_model=start_override_per_model, + end_override_per_model=end_override_per_model, ignore_cron=ignore_cron, end_bounded=end_bounded, selected_snapshots=selected_snapshots, @@ -797,7 +808,8 @@ def merged_missing_intervals( execution_time: t.Optional[TimeLike] = None, deployability_index: t.Optional[DeployabilityIndex] = None, restatements: t.Optional[t.Dict[SnapshotId, Interval]] = None, - interval_end_per_model: t.Optional[t.Dict[str, int]] = None, + start_override_per_model: t.Optional[t.Dict[str, datetime]] = None, + end_override_per_model: t.Optional[t.Dict[str, datetime]] = None, ignore_cron: bool = False, end_bounded: bool = False, ) -> SnapshotToIntervals: @@ -815,7 +827,8 @@ def merged_missing_intervals( execution_time: The date/time reference to use for execution time. Defaults to now. deployability_index: Determines snapshots that are deployable in the context of this evaluation. restatements: A set of snapshot names being restated. - interval_end_per_model: The mapping from model FQNs to target end dates. + start_override_per_model: A mapping of model FQNs to target start dates. + end_override_per_model: A mapping of model FQNs to target end dates. ignore_cron: Whether to ignore the node's cron schedule. end_bounded: If set to true, the returned intervals will be bounded by the target end date, disregarding lookback, allow_partials, and other attributes that could cause the intervals to exceed the target end date. @@ -830,7 +843,8 @@ def merged_missing_intervals( deployability_index=deployability_index, execution_time=execution_time or now_timestamp(), restatements=restatements, - interval_end_per_model=interval_end_per_model, + start_override_per_model=start_override_per_model, + end_override_per_model=end_override_per_model, ignore_cron=ignore_cron, end_bounded=end_bounded, ) @@ -844,7 +858,8 @@ def compute_interval_params( deployability_index: t.Optional[DeployabilityIndex] = None, execution_time: t.Optional[TimeLike] = None, restatements: t.Optional[t.Dict[SnapshotId, Interval]] = None, - interval_end_per_model: t.Optional[t.Dict[str, int]] = None, + start_override_per_model: t.Optional[t.Dict[str, datetime]] = None, + end_override_per_model: t.Optional[t.Dict[str, datetime]] = None, ignore_cron: bool = False, end_bounded: bool = False, ) -> SnapshotToIntervals: @@ -862,7 +877,8 @@ def compute_interval_params( deployability_index: Determines snapshots that are deployable in the context of this evaluation. execution_time: The date/time reference to use for execution time. restatements: A dict of snapshot names being restated and their intervals. - interval_end_per_model: The mapping from model FQNs to target end dates. + start_override_per_model: A mapping of model FQNs to target start dates. + end_override_per_model: A mapping of model FQNs to target end dates. ignore_cron: Whether to ignore the node's cron schedule. end_bounded: If set to true, the returned intervals will be bounded by the target end date, disregarding lookback, allow_partials, and other attributes that could cause the intervals to exceed the target end date. @@ -879,7 +895,8 @@ def compute_interval_params( execution_time=execution_time, restatements=restatements, deployability_index=deployability_index, - interval_end_per_model=interval_end_per_model, + start_override_per_model=start_override_per_model, + end_override_per_model=end_override_per_model, ignore_cron=ignore_cron, end_bounded=end_bounded, ).items(): diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 9b5fa893fc..b6f94108a1 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1469,7 +1469,8 @@ def none_deployable(cls) -> DeployabilityIndex: def create( cls, snapshots: t.Dict[SnapshotId, Snapshot] | t.Collection[Snapshot], - start: t.Optional[TimeLike] = None, + start: t.Optional[TimeLike] = None, # plan start + start_override_per_model: t.Optional[t.Dict[str, datetime]] = None, ) -> DeployabilityIndex: if not isinstance(snapshots, dict): snapshots = {s.snapshot_id: s for s in snapshots} @@ -1477,6 +1478,7 @@ def create( deployability_mapping: t.Dict[SnapshotId, bool] = {} children_deployability_mapping: t.Dict[SnapshotId, bool] = {} representative_shared_version_ids: t.Set[SnapshotId] = set() + start_override_per_model = start_override_per_model or {} start_date_cache: t.Optional[t.Dict[str, datetime]] = {} @@ -1499,12 +1501,12 @@ def create( snapshot.is_model and snapshot.model.auto_restatement_cron is not None ) + snapshot_start = start_override_per_model.get( + node.name, start_date(snapshot, snapshots.values(), cache=start_date_cache) + ) + is_valid_start = ( - snapshot.is_valid_start( - start, start_date(snapshot, snapshots.values(), start_date_cache) - ) - if start is not None - else True + snapshot.is_valid_start(start, snapshot_start) if start is not None else True ) if ( @@ -1800,7 +1802,8 @@ def missing_intervals( execution_time: t.Optional[TimeLike] = None, restatements: t.Optional[t.Dict[SnapshotId, Interval]] = None, deployability_index: t.Optional[DeployabilityIndex] = None, - interval_end_per_model: t.Optional[t.Dict[str, int]] = None, + start_override_per_model: t.Optional[t.Dict[str, datetime]] = None, + end_override_per_model: t.Optional[t.Dict[str, datetime]] = None, ignore_cron: bool = False, end_bounded: bool = False, ) -> t.Dict[Snapshot, Intervals]: @@ -1817,13 +1820,15 @@ def missing_intervals( else earliest_start_date(snapshots, cache=cache, relative_to=end_date) ) restatements = restatements or {} - interval_end_per_model = interval_end_per_model or {} + start_override_per_model = start_override_per_model or {} + end_override_per_model = end_override_per_model or {} deployability_index = deployability_index or DeployabilityIndex.all_deployable() for snapshot in snapshots.values(): if not snapshot.evaluatable: continue - snapshot_start_date = start_dt + + snapshot_start_date = start_override_per_model.get(snapshot.name, start_dt) snapshot_end_date: TimeLike = end_date restated_interval = restatements.get(snapshot.snapshot_id) @@ -1833,9 +1838,9 @@ def missing_intervals( snapshot.intervals = snapshot.intervals.copy() snapshot.remove_interval(restated_interval) - existing_interval_end = interval_end_per_model.get(snapshot.name) + existing_interval_end = end_override_per_model.get(snapshot.name) if existing_interval_end: - if to_timestamp(snapshot_start_date) >= existing_interval_end: + if snapshot_start_date >= existing_interval_end: # The start exceeds the provided interval end, so we can skip this snapshot # since it doesn't have missing intervals by definition continue diff --git a/tests/core/test_context.py b/tests/core/test_context.py index a8d2d04da9..0728168a0f 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -2,7 +2,7 @@ import pathlib import typing as t import re -from datetime import date, timedelta +from datetime import date, timedelta, datetime from tempfile import TemporaryDirectory from unittest.mock import PropertyMock, call, patch @@ -36,6 +36,7 @@ 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.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.cache import OptimizedQueryCache @@ -2184,6 +2185,7 @@ def test_plan_audit_intervals(tmp_path: pathlib.Path, caplog): plan = ctx.plan( environment="dev", auto_apply=True, no_prompts=True, start="2025-02-01", end="2025-02-01" ) + assert plan.missing_intervals date_snapshot = next(s for s in plan.new_snapshots if "date_example" in s.name) timestamp_snapshot = next(s for s in plan.new_snapshots if "timestamp_example" in s.name) @@ -2408,3 +2410,324 @@ def test_table_diff_ignores_extra_args(sushi_context: Context): show_sample=True, some_tcloud_option=1_000, ) + + +def test_plan_min_intervals(tmp_path: Path): + init_example_project(tmp_path, engine_type="duckdb", dialect="duckdb") + + context = Context( + paths=tmp_path, config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ) + + current_time = to_datetime("2020-02-01 00:00:01") + + # initial state of example project + context.plan(auto_apply=True, execution_time=current_time) + + (tmp_path / "models" / "daily_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.daily_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column start_dt + ), + start '2020-01-01', + cron '@daily' + ); + + select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt; + """) + + (tmp_path / "models" / "weekly_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.weekly_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column start_dt + ), + start '2020-01-01', + cron '@weekly' + ); + + select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt; + """) + + (tmp_path / "models" / "monthly_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.monthly_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column start_dt + ), + start '2020-01-01', + cron '@monthly' + ); + + select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt; + """) + + (tmp_path / "models" / "ended_daily_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.ended_daily_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column start_dt + ), + start '2020-01-01', + end '2020-01-18', + cron '@daily' + ); + + select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt; + """) + + context.load() + + # initial state - backfill from 2020-01-01 -> now() (2020-01-02 00:00:01) on new models + plan = context.plan(execution_time=current_time) + + assert to_datetime(plan.start) == to_datetime("2020-01-01 00:00:00") + assert to_datetime(plan.end) == to_datetime("2020-02-01 00:00:00") + assert to_datetime(plan.execution_time) == to_datetime("2020-02-01 00:00:01") + + def _get_missing_intervals(plan: Plan, name: str) -> t.List[t.Tuple[datetime, datetime]]: + snapshot_id = context.get_snapshot(name, raise_if_missing=True).snapshot_id + snapshot_intervals = next( + si for si in plan.missing_intervals if si.snapshot_id == snapshot_id + ) + return [(to_datetime(s), to_datetime(e)) for s, e in snapshot_intervals.merged_intervals] + + # check initial intervals - should be full time range between start and execution time + assert len(plan.missing_intervals) == 4 + + assert _get_missing_intervals(plan, "sqlmesh_example.daily_model") == [ + (to_datetime("2020-01-01 00:00:00"), to_datetime("2020-02-01 00:00:00")) + ] + assert _get_missing_intervals(plan, "sqlmesh_example.weekly_model") == [ + ( + to_datetime("2020-01-01 00:00:00"), + to_datetime("2020-01-26 00:00:00"), + ) # last week in 2020-01 hasnt fully elapsed yet + ] + assert _get_missing_intervals(plan, "sqlmesh_example.monthly_model") == [ + (to_datetime("2020-01-01 00:00:00"), to_datetime("2020-02-01 00:00:00")) + ] + assert _get_missing_intervals(plan, "sqlmesh_example.ended_daily_model") == [ + (to_datetime("2020-01-01 00:00:00"), to_datetime("2020-01-19 00:00:00")) + ] + + # now, create a dev env for "1 day ago" with min_intervals=1 + plan = context.plan( + environment="pr_env", + start="1 day ago", + execution_time=current_time, + min_intervals=1, + ) + + # this should pick up last day for daily model, last week for weekly model, last month for the monthly model and the last day of "ended_daily_model" before it ended + assert len(plan.missing_intervals) == 4 + + assert _get_missing_intervals(plan, "sqlmesh_example.daily_model") == [ + (to_datetime("2020-01-31 00:00:00"), to_datetime("2020-02-01 00:00:00")) + ] + assert _get_missing_intervals(plan, "sqlmesh_example.weekly_model") == [ + ( + to_datetime("2020-01-19 00:00:00"), # last completed week + to_datetime("2020-01-26 00:00:00"), + ) + ] + assert _get_missing_intervals(plan, "sqlmesh_example.monthly_model") == [ + ( + to_datetime("2020-01-01 00:00:00"), # last completed month + to_datetime("2020-02-01 00:00:00"), + ) + ] + assert _get_missing_intervals(plan, "sqlmesh_example.ended_daily_model") == [ + ( + to_datetime("2020-01-18 00:00:00"), # last day before the model end date + to_datetime("2020-01-19 00:00:00"), + ) + ] + + # run the plan for '1 day ago' but min_intervals=1 + context.apply(plan) + + # show that the data was created (which shows that when the Plan became an EvaluatablePlan and eventually evaluated, the start date overrides didnt get dropped) + assert context.engine_adapter.fetchall( + "select start_dt, end_dt from sqlmesh_example__pr_env.daily_model" + ) == [(to_datetime("2020-01-31 00:00:00"), to_datetime("2020-01-31 23:59:59.999999"))] + assert context.engine_adapter.fetchall( + "select start_dt, end_dt from sqlmesh_example__pr_env.weekly_model" + ) == [ + (to_datetime("2020-01-19 00:00:00"), to_datetime("2020-01-25 23:59:59.999999")), + ] + assert context.engine_adapter.fetchall( + "select start_dt, end_dt from sqlmesh_example__pr_env.monthly_model" + ) == [ + (to_datetime("2020-01-01 00:00:00"), to_datetime("2020-01-31 23:59:59.999999")), + ] + assert context.engine_adapter.fetchall( + "select start_dt, end_dt from sqlmesh_example__pr_env.ended_daily_model" + ) == [ + (to_datetime("2020-01-18 00:00:00"), to_datetime("2020-01-18 23:59:59.999999")), + ] + + +def test_plan_min_intervals_adjusted_for_downstream(tmp_path: Path): + """ + Scenario: + A(hourly) <- B(daily) <- C(weekly) + <- D(two-hourly) + E(monthly) + + We need to ensure that :min_intervals covers at least :min_intervals of all downstream models for the dag to be valid + In this scenario, if min_intervals=1: + - A would need to cover at least (7 days * 24 hours) because its downstream model C is weekly. It should also be unaffected by its sibling, E + - B would need to cover at least 7 days because its downstream model C is weekly + - C would need to cover at least 1 week because min_intervals: 1 + - D would need to cover at least 2 hours because min_intervals: 1 and should be unaffected by C + - E is unrelated to A, B, C and D so would need to cover 1 month satisfy min_intervals: 1. + - It also ensures that each tree branch has a unique cumulative date, because + if the dag is iterated purely in topological order with a global min date it would set A to to 1 month instead if 1 week + """ + + init_example_project(tmp_path, engine_type="duckdb", dialect="duckdb") + + context = Context( + paths=tmp_path, config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ) + + current_time = to_datetime("2020-02-01 00:00:01") + + # initial state of example project + context.plan(auto_apply=True, execution_time=current_time) + + (tmp_path / "models" / "hourly_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.hourly_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column start_dt, + batch_size 1 + ), + start '2020-01-01', + cron '@hourly' + ); + + select @start_dt as start_dt, @end_dt as end_dt; + """) + + (tmp_path / "models" / "two_hourly_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.two_hourly_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column start_dt + ), + start '2020-01-01', + cron '0 */2 * * *' + ); + + select start_dt, end_dt from sqlmesh_example.hourly_model where start_dt between @start_dt and @end_dt; + """) + + (tmp_path / "models" / "unrelated_monthly_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.unrelated_monthly_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column start_dt + ), + start '2020-01-01', + cron '@monthly' + ); + + select @start_dt as start_dt, @end_dt as end_dt; + """) + + (tmp_path / "models" / "daily_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.daily_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column start_dt + ), + start '2020-01-01', + cron '@daily' + ); + + select start_dt, end_dt from sqlmesh_example.hourly_model where start_dt between @start_dt and @end_dt; + """) + + (tmp_path / "models" / "weekly_model.sql").write_text(""" + MODEL ( + name sqlmesh_example.weekly_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column start_dt + ), + start '2020-01-01', + cron '@weekly' + ); + + select start_dt, end_dt from sqlmesh_example.daily_model where start_dt between @start_dt and @end_dt; + """) + + context.load() + + # create a dev env for "1 day ago" with min_intervals=1 + # this should force a weeks worth of intervals for every model + plan = context.plan( + environment="pr_env", + start="1 day ago", + execution_time=current_time, + min_intervals=1, + ) + + def _get_missing_intervals(name: str) -> t.List[t.Tuple[datetime, datetime]]: + snapshot_id = context.get_snapshot(name, raise_if_missing=True).snapshot_id + snapshot_intervals = next( + si for si in plan.missing_intervals if si.snapshot_id == snapshot_id + ) + return [(to_datetime(s), to_datetime(e)) for s, e in snapshot_intervals.merged_intervals] + + # We only operate on completed intervals, so given the current_time this is the range of the last completed week + _get_missing_intervals("sqlmesh_example.weekly_model") == [ + (to_datetime("2020-01-19 00:00:00"), to_datetime("2020-01-26 00:00:00")) + ] + + # The daily model needs to cover the week, so it gets its start date moved back to line up + _get_missing_intervals("sqlmesh_example.daily_model") == [ + (to_datetime("2020-01-19 00:00:00"), to_datetime("2020-02-01 00:00:00")) + ] + + # The hourly model needs to cover both the daily model and the weekly model, so it also gets its start date moved back to line up with the weekly model + assert _get_missing_intervals("sqlmesh_example.hourly_model") == [ + (to_datetime("2020-01-19 00:00:00"), to_datetime("2020-02-01 00:00:00")) + ] + + # The two-hourly model only needs to cover 2 hours and should be unaffected by the fact its sibling node has a weekly child node + # However it still gets backfilled for 24 hours because the plan start is 1 day and this satisfies min_intervals: 1 + assert _get_missing_intervals("sqlmesh_example.two_hourly_model") == [ + (to_datetime("2020-01-31 00:00:00"), to_datetime("2020-02-01 00:00:00")) + ] + + # The unrelated model has no upstream constraints, so its start date doesnt get moved to line up with the weekly model + # However it still gets backfilled for 24 hours because the plan start is 1 day and this satisfies min_intervals: 1 + _get_missing_intervals("sqlmesh_example.unrelated_monthly_model") == [ + (to_datetime("2020-01-01 00:00:00"), to_datetime("2020-02-01 00:00:00")) + ] + + # Check that actually running the plan produces the correct result, since missing intervals are re-calculated in the evaluator + context.apply(plan) + + assert context.engine_adapter.fetchall( + "select min(start_dt), max(end_dt) from sqlmesh_example__pr_env.weekly_model" + ) == [(to_datetime("2020-01-19 00:00:00"), to_datetime("2020-01-25 23:59:59.999999"))] + + assert context.engine_adapter.fetchall( + "select min(start_dt), max(end_dt) from sqlmesh_example__pr_env.daily_model" + ) == [(to_datetime("2020-01-19 00:00:00"), to_datetime("2020-01-31 23:59:59.999999"))] + + assert context.engine_adapter.fetchall( + "select min(start_dt), max(end_dt) from sqlmesh_example__pr_env.hourly_model" + ) == [(to_datetime("2020-01-19 00:00:00"), to_datetime("2020-01-31 23:59:59.999999"))] + + assert context.engine_adapter.fetchall( + "select min(start_dt), max(end_dt) from sqlmesh_example__pr_env.two_hourly_model" + ) == [(to_datetime("2020-01-31 00:00:00"), to_datetime("2020-01-31 23:59:59.999999"))] + + assert context.engine_adapter.fetchall( + "select min(start_dt), max(end_dt) from sqlmesh_example__pr_env.unrelated_monthly_model" + ) == [(to_datetime("2020-01-01 00:00:00"), to_datetime("2020-01-31 23:59:59.999999"))] diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 06dbd7d2d3..765a45f7c8 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -756,7 +756,8 @@ def test_missing_intervals_lookback(make_snapshot, mocker: MockerFixture): restatements={}, end_bounded=False, ensure_finalized_snapshots=False, - interval_end_per_model=None, + start_override_per_model=None, + end_override_per_model=None, explain=False, ) @@ -2697,7 +2698,7 @@ def test_plan_start_when_preview_enabled(make_snapshot, mocker: MockerFixture): assert plan_builder.build().start == default_start_for_preview -def test_interval_end_per_model(make_snapshot): +def test_end_override_per_model(make_snapshot): snapshot = make_snapshot(SqlModel(name="a", query=parse_one("select 1, ds"))) snapshot.categorize_as(SnapshotChangeCategory.BREAKING) @@ -2725,20 +2726,18 @@ def test_interval_end_per_model(make_snapshot): plan_builder = PlanBuilder( context_diff, - interval_end_per_model={snapshot.name: to_timestamp("2023-01-09")}, + end_override_per_model={snapshot.name: to_datetime("2023-01-09")}, ) - assert plan_builder.build().interval_end_per_model == { - snapshot.name: to_timestamp("2023-01-09") - } + assert plan_builder.build().end_override_per_model == {snapshot.name: to_datetime("2023-01-09")} # User-provided end should take precedence. plan_builder = PlanBuilder( context_diff, - interval_end_per_model={snapshot.name: to_timestamp("2023-01-09")}, + end_override_per_model={snapshot.name: to_datetime("2023-01-09")}, end="2023-01-10", is_dev=True, ) - assert plan_builder.build().interval_end_per_model is None + assert plan_builder.build().end_override_per_model is None def test_unaligned_start_model_with_forward_only_preview(make_snapshot): diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index b806b95b75..6e5a1fe43f 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -112,7 +112,6 @@ def test_build_plan_stages_basic( removed_snapshots=[], requires_backfill=True, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, @@ -224,7 +223,6 @@ def test_build_plan_stages_with_before_all_and_after_all( removed_snapshots=[], requires_backfill=True, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=environment_statements, @@ -333,7 +331,6 @@ def test_build_plan_stages_select_models( removed_snapshots=[], requires_backfill=True, models_to_backfill={snapshot_a.name}, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, @@ -434,7 +431,6 @@ def test_build_plan_stages_basic_no_backfill( removed_snapshots=[], requires_backfill=True, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, @@ -544,7 +540,6 @@ def test_build_plan_stages_restatement( removed_snapshots=[], requires_backfill=True, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, @@ -656,7 +651,6 @@ def test_build_plan_stages_forward_only( removed_snapshots=[], requires_backfill=True, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, @@ -785,7 +779,6 @@ def test_build_plan_stages_forward_only_dev( removed_snapshots=[], requires_backfill=True, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, @@ -909,7 +902,6 @@ def _get_snapshots(snapshot_ids: t.List[SnapshotId]) -> t.Dict[SnapshotId, Snaps removed_snapshots=[], requires_backfill=True, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, @@ -1033,7 +1025,6 @@ def test_build_plan_stages_forward_only_ensure_finalized_snapshots( removed_snapshots=[], requires_backfill=True, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, @@ -1106,7 +1097,6 @@ def test_build_plan_stages_removed_model( removed_snapshots=[snapshot_b.snapshot_id], requires_backfill=False, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, @@ -1188,7 +1178,6 @@ def test_build_plan_stages_environment_suffix_target_changed( removed_snapshots=[], requires_backfill=False, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, @@ -1288,7 +1277,6 @@ def test_build_plan_stages_indirect_non_breaking_no_migration( removed_snapshots=[], requires_backfill=True, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, @@ -1376,7 +1364,6 @@ def test_build_plan_stages_indirect_non_breaking_view_migration( removed_snapshots=[], requires_backfill=True, models_to_backfill=None, - interval_end_per_model=None, execution_time="2023-01-02", disabled_restatement_models=set(), environment_statements=None, diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index f2f54822f5..ffaee9be74 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -551,6 +551,50 @@ def test_missing_intervals_past_end_date_with_lookback(make_snapshot): assert snapshot.missing_intervals(start_time, end_time, execution_time=end_time) == [] +def test_missing_intervals_start_override_per_model(make_snapshot: t.Callable[..., Snapshot]): + snapshot = make_snapshot( + load_sql_based_model( + parse(""" + MODEL ( + name a, + kind FULL, + start '2023-02-01', + cron '@daily' + ); + SELECT 1; + """) + ), + version="a", + ) + + # base case - no override + assert list( + missing_intervals(execution_time="2023-02-08 00:05:07", snapshots=[snapshot]).values() + )[0] == [ + (to_timestamp("2023-02-01"), to_timestamp("2023-02-02")), + (to_timestamp("2023-02-02"), to_timestamp("2023-02-03")), + (to_timestamp("2023-02-03"), to_timestamp("2023-02-04")), + (to_timestamp("2023-02-04"), to_timestamp("2023-02-05")), + (to_timestamp("2023-02-05"), to_timestamp("2023-02-06")), + (to_timestamp("2023-02-06"), to_timestamp("2023-02-07")), + (to_timestamp("2023-02-07"), to_timestamp("2023-02-08")), + ] + + # with override, should use the overridden start date when calculating missing intervals + assert list( + missing_intervals( + start="1 day ago", + execution_time="2023-02-08 00:05:07", + snapshots=[snapshot], + start_override_per_model={snapshot.name: to_datetime("2023-02-05 00:00:00")}, + ).values() + )[0] == [ + (to_timestamp("2023-02-05"), to_timestamp("2023-02-06")), + (to_timestamp("2023-02-06"), to_timestamp("2023-02-07")), + (to_timestamp("2023-02-07"), to_timestamp("2023-02-08")), + ] + + def test_incremental_time_self_reference(make_snapshot): snapshot = make_snapshot( SqlModel( @@ -2363,7 +2407,7 @@ def test_snapshot_pickle_intervals(make_snapshot): assert len(snapshot.dev_intervals) > 0 -def test_missing_intervals_interval_end_per_model(make_snapshot): +def test_missing_intervals_end_override_per_model(make_snapshot): snapshot_a = make_snapshot( SqlModel( name="a", @@ -2386,9 +2430,9 @@ def test_missing_intervals_interval_end_per_model(make_snapshot): [snapshot_a, snapshot_b], start="2023-01-04", end="2023-01-10", - interval_end_per_model={ - snapshot_a.name: to_timestamp("2023-01-09"), - snapshot_b.name: to_timestamp("2023-01-06"), + end_override_per_model={ + snapshot_a.name: to_datetime("2023-01-09"), + snapshot_b.name: to_datetime("2023-01-06"), }, ) == { snapshot_a: [ @@ -2408,9 +2452,9 @@ def test_missing_intervals_interval_end_per_model(make_snapshot): [snapshot_a, snapshot_b], start="2023-01-08", end="2023-01-08", - interval_end_per_model={ - snapshot_a.name: to_timestamp("2023-01-09"), - snapshot_b.name: to_timestamp( + end_override_per_model={ + snapshot_a.name: to_datetime("2023-01-09"), + snapshot_b.name: to_datetime( "2023-01-06" ), # The interval end is before the start. The snapshot will be skipped }, From 9c76f8587280defccae255e3c8738e1e8b58ea5d Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:51:43 +0100 Subject: [PATCH 0529/1056] chore: cleaning up lineage (#4907) --- pnpm-lock.yaml | 6 - .../react/src/components/divider/Divider.tsx | 34 ----- .../src/components/graph/ModelColumns.tsx | 5 +- .../src/components/graph/ModelLineage.tsx | 120 +--------------- .../components/graph/ModelLineageDetails.tsx | 133 ------------------ .../react/src/components/graph/ModelNode.tsx | 7 +- vscode/react/src/components/graph/context.tsx | 23 +-- vscode/react/src/components/graph/help.ts | 4 +- .../src/components/listbox/ListboxShow.tsx | 96 ------------- vscode/react/src/components/logo/SqlMesh.tsx | 61 -------- vscode/react/src/components/logo/Tobiko.tsx | 62 -------- 11 files changed, 23 insertions(+), 528 deletions(-) delete mode 100644 vscode/react/src/components/divider/Divider.tsx delete mode 100644 vscode/react/src/components/graph/ModelLineageDetails.tsx delete mode 100644 vscode/react/src/components/listbox/ListboxShow.tsx delete mode 100644 vscode/react/src/components/logo/SqlMesh.tsx delete mode 100644 vscode/react/src/components/logo/Tobiko.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97ba5ca5b1..4ab18b8d6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,12 +75,6 @@ importers: ts-loader: specifier: ^9.5.2 version: 9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.5)) -<<<<<<< HEAD -======= - tsx: - specifier: ^4.20.3 - version: 4.20.3 ->>>>>>> a6ed6cf88 (chore(vscode): update dependencies) typescript: specifier: ^5.8.3 version: 5.8.3 diff --git a/vscode/react/src/components/divider/Divider.tsx b/vscode/react/src/components/divider/Divider.tsx deleted file mode 100644 index 866065e5dd..0000000000 --- a/vscode/react/src/components/divider/Divider.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import clsx from 'clsx' -import { EnumSize } from '@/style/variants' - -interface PropsDivider extends React.HTMLAttributes { - size?: typeof EnumSize.sm | typeof EnumSize.md | typeof EnumSize.lg - orientation?: 'horizontal' | 'vertical' -} - -export const SIZE = new Map([ - [EnumSize.sm, ''], - [EnumSize.md, '-2'], - [EnumSize.lg, '-4'], -]) - -export function Divider({ - size = EnumSize.sm, - orientation = 'horizontal', - className, -}: PropsDivider): JSX.Element { - return ( - - ) -} diff --git a/vscode/react/src/components/graph/ModelColumns.tsx b/vscode/react/src/components/graph/ModelColumns.tsx index 7a1b10f213..19f382467a 100644 --- a/vscode/react/src/components/graph/ModelColumns.tsx +++ b/vscode/react/src/components/graph/ModelColumns.tsx @@ -31,6 +31,7 @@ import { useApiColumnLineage } from '@/api/index' import SourceList from '@/components/sourceList/SourceList' import type { Lineage } from '@/domain/lineage' import type { Column, ColumnName } from '@/domain/column' +import type { ModelEncodedFQN } from '@/domain/models' export function ModelColumns({ nodeId, @@ -42,7 +43,7 @@ export function ModelColumns({ withDescription = true, maxHeight = '50vh', }: { - nodeId: string + nodeId: ModelEncodedFQN columns: Column[] disabled?: boolean className?: string @@ -131,7 +132,7 @@ export function ModelColumns({ if (isNil(selectedModel) || isNil(selectedColumn)) return false - return selectedModel.name === nodeId && selectedColumn.name === columnName + return selectedModel.fqn === nodeId && selectedColumn.name === columnName }, [nodeId, manuallySelectedColumn], ) diff --git a/vscode/react/src/components/graph/ModelLineage.tsx b/vscode/react/src/components/graph/ModelLineage.tsx index 604f54de9d..e7f16debdc 100644 --- a/vscode/react/src/components/graph/ModelLineage.tsx +++ b/vscode/react/src/components/graph/ModelLineage.tsx @@ -1,8 +1,7 @@ import { useApiModelLineage, useApiModels } from '@/api/index' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { type ModelSQLMeshModel } from '@/domain/sqlmesh-model' import { type HighlightedNodes, useLineageFlow } from './context' -import { ChevronDownIcon } from '@heroicons/react/24/solid' import ReactFlow, { Controls, Background, @@ -14,14 +13,12 @@ import ReactFlow, { useReactFlow, type Edge, type Node, - Panel, ReactFlowProvider, } from 'reactflow' import Loading from '@/components/loading/Loading' import Spinner from '@/components/logo/Spinner' import { createLineageWorker } from '@/workers/index' import { isArrayEmpty, isFalse, isNil, isNotNil } from '@/utils/index' -import ListboxShow from '@/components/listbox/ListboxShow' import clsx from 'clsx' import ModelNode from './ModelNode' import { @@ -32,9 +29,6 @@ import { getUpdatedEdges, createGraphLayout, } from './help' -import { Popover } from '@headlessui/react' -import ModelLineageDetails from './ModelLineageDetails' -import { Divider } from '@/components/divider/Divider' import { SettingsControl } from '@/components/graph/SettingsControl' import { toModelLineage, @@ -209,7 +203,6 @@ function ModelColumnLineage(): JSX.Element { connectedNodes, connections, nodesMap, - showControls, handleError, setActiveNodes, setWithColumns, @@ -337,7 +330,6 @@ function ModelColumnLineage(): JSX.Element { setEdges(newEdges) setNodes(newNodes) - console.log('newActiveNodes', newActiveNodes) setActiveNodes(newActiveNodes) }, [ connections, @@ -385,15 +377,6 @@ function ModelColumnLineage(): JSX.Element { snapGrid={[16, 16]} snapToGrid > - {showControls && ( - - - - - )} ) } - -function GraphControls({ nodes = [] }: { nodes: Node[] }): JSX.Element { - const { - withColumns, - // mainNode, - selectedNodes, - withConnected, - withImpacted, - withSecondary, - hasBackground, - activeNodes, - // highlightedNodes, - // setSelectedNodes, - setWithColumns, - setWithConnected, - setWithImpacted, - setWithSecondary, - setHasBackground, - } = useLineageFlow() - - useEffect(() => { - setWithColumns(true) - setWithSecondary(true) - setWithConnected(true) - setWithImpacted(true) - }, [setWithSecondary]) - - const lineageInfoTrigger = useRef(null) - - // const highlightedNodeModels = useMemo( - // () => Object.values(highlightedNodes ?? {}).flat(), - // [highlightedNodes], - // ) - // function handleSelect(model: { name: string; description: string }): void { - // if (highlightedNodeModels.includes(model.name) || mainNode === model.name) - // return - - // setSelectedNodes(current => { - // if (current.has(model.name)) { - // current.delete(model.name) - // } else { - // current.add(model.name) - // } - - // return new Set(current) - // }) - // } - - return ( -

  • """ + if self.forward_only_plan: + plan_summary = ( + f"{self.get_forward_only_plan_post_deployment_tip(self.prod_plan)}\n{plan_summary}" + ) + self.update_sqlmesh_comment_info( value=plan_summary, dedup_regex=None, @@ -1096,6 +1131,7 @@ def conclusion_handler( summary = "Got an action required conclusion but no plan error was provided. This is unexpected." else: summary = "**Generated Prod Plan**\n" + self.get_plan_summary(self.prod_plan) + return conclusion, title, summary self._update_check_handler( diff --git a/tests/integrations/github/cicd/test_github_commands.py b/tests/integrations/github/cicd/test_github_commands.py index 6be6a4557a..6d82755934 100644 --- a/tests/integrations/github/cicd/test_github_commands.py +++ b/tests/integrations/github/cicd/test_github_commands.py @@ -1,4 +1,5 @@ # type: ignore +import typing as t import os import pathlib from unittest import TestCase, mock @@ -14,6 +15,7 @@ from sqlmesh.integrations.github.cicd import command from sqlmesh.integrations.github.cicd.config import GithubCICDBotConfig, MergeMethod from sqlmesh.integrations.github.cicd.controller import ( + GithubController, GithubCheckConclusion, GithubCheckStatus, ) @@ -1152,6 +1154,7 @@ def test_comment_command_deploy_prod( User(username="test", github_username="test_github", roles=[UserRole.REQUIRED_APPROVER]) ] controller._context.invalidate_environment = mocker.MagicMock() + assert not controller.forward_only_plan github_output_file = tmp_path / "github_output.txt" @@ -1366,3 +1369,84 @@ def test_comment_command_deploy_prod_no_deploy_detected_yet( # required approvers are irrelevant because /deploy command is enabled assert "SQLMesh - Has Required Approval" not in controller._check_run_mapping + + +def test_deploy_prod_forward_only( + github_client, + make_controller: t.Callable[..., GithubController], + make_mock_check_run, + make_mock_issue_comment, + tmp_path: pathlib.Path, + mocker: MockerFixture, +): + """ + Scenario: + - PR is created with a branch name indicating that plans should be forward-only + - PR is not merged + - Tests passed + - PR Merge Method defined + - Deploy command has been triggered + + Outcome: + - "Prod Environment Synced" step should show a tip explaining how to retroactively apply forward-only changes to old data + - Bot Comment should show the same tip + """ + mock_repo = github_client.get_repo() + mock_repo.create_check_run = mocker.MagicMock( + side_effect=lambda **kwargs: make_mock_check_run(**kwargs) + ) + + created_comments = [] + mock_issue = mock_repo.get_issue() + mock_issue.create_comment = mocker.MagicMock( + side_effect=lambda comment: make_mock_issue_comment( + comment=comment, created_comments=created_comments + ) + ) + mock_issue.get_comments = mocker.MagicMock(side_effect=lambda: created_comments) + + mock_pull_request = mock_repo.get_pull() + mock_pull_request.get_reviews = mocker.MagicMock(lambda: []) + mock_pull_request.merged = False + mock_pull_request.merge = mocker.MagicMock() + mock_pull_request.head.ref = "unit-test-forward-only" + + controller = make_controller( + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + bot_config=GithubCICDBotConfig( + merge_method=MergeMethod.SQUASH, + enable_deploy_command=True, + forward_only_branch_suffix="-forward-only", + ), + mock_out_context=False, + ) + + # create existing prod to apply against + controller._context.plan(auto_apply=True) + + github_output_file = tmp_path / "github_output.txt" + + # then, run a deploy with forward_only set + assert controller.forward_only_plan + with mock.patch.dict(os.environ, {"GITHUB_OUTPUT": str(github_output_file)}): + command._deploy_production(controller) + + # Prod Environment Synced step should be successful + assert "SQLMesh - Prod Environment Synced" in controller._check_run_mapping + prod_checks_runs = controller._check_run_mapping["SQLMesh - Prod Environment Synced"].all_kwargs + assert len(prod_checks_runs) == 2 + assert GithubCheckStatus(prod_checks_runs[0]["status"]).is_in_progress + assert GithubCheckStatus(prod_checks_runs[1]["status"]).is_completed + assert prod_checks_runs[1]["output"]["title"] == "Deployed to Prod" + assert GithubCheckConclusion(prod_checks_runs[1]["conclusion"]).is_success + + # PR comment should be updated with forward-only tip + assert len(created_comments) == 1 + assert ( + """> [!TIP] +> In order to see this forward-only plan retroactively apply to historical intervals on the production model, run the below for date ranges in scope: +> +> `$ sqlmesh plan --restate-model sushi.customer_revenue_by_day --start YYYY-MM-DD --end YYYY-MM-DD`""" + in created_comments[0].body + ) From 4ee6cb3e408d52056431082df4c5fda0bdb6d0bd Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 15 Jul 2025 20:35:00 +1200 Subject: [PATCH 0577/1056] Fix: Allow python models to call `resolve_table()` on themselves within a unit test (#4967) --- sqlmesh/core/context.py | 9 ++ sqlmesh/core/test/context.py | 4 +- tests/core/test_test.py | 274 ++++++++++++++++++++++++++++++++++- 3 files changed, 284 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 31147dec0e..d6a305f7c0 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -214,6 +214,15 @@ def resolve_table(self, model_name: str) -> str: """ model_name = normalize_model_name(model_name, self.default_catalog, self.default_dialect) + if model_name not in self._model_tables: + model_name_list = "\n".join(list(self._model_tables)) + logger.debug( + f"'{model_name}' not found in model to table mapping. Available model names: \n{model_name_list}" + ) + raise SQLMeshError( + f"Unable to find a table mapping for model '{model_name}'. Has it been spelled correctly?" + ) + # We generate SQL for the default dialect because the table name may be used in a # fetchdf call and so the quotes need to be correct (eg. backticks for bigquery) return parse_one(self._model_tables[model_name]).sql( diff --git a/sqlmesh/core/test/context.py b/sqlmesh/core/test/context.py index 653f62bfe8..5ad9673ca8 100644 --- a/sqlmesh/core/test/context.py +++ b/sqlmesh/core/test/context.py @@ -43,8 +43,8 @@ def _model_tables(self) -> t.Dict[str, str]: # Include upstream dependencies to ensure they can be resolved during test execution return { name: self._test._test_fixture_table(name).sql() - for model in self._models.values() - for name in [model.name, *model.depends_on] + for normalized_model_name, model in self._models.items() + for name in [normalized_model_name, *model.depends_on] } def with_variables( diff --git a/tests/core/test_test.py b/tests/core/test_test.py index de2fb3ea46..c15c06fc35 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -31,6 +31,7 @@ from sqlmesh.core.model import Model, SqlModel, load_sql_based_model, model from sqlmesh.core.test.definition import ModelTest, PythonModelTest, SqlModelTest from sqlmesh.core.test.result import ModelTextTestResult +from sqlmesh.core.test.context import TestExecutionContext from sqlmesh.utils import Verbosity from sqlmesh.utils.errors import ConfigError, SQLMeshError, TestError from sqlmesh.utils.yaml import dump as dump_yaml @@ -69,11 +70,14 @@ def _create_model( meta: str = SUSHI_FOO_META, dialect: t.Optional[str] = None, default_catalog: t.Optional[str] = None, + **kwargs: t.Any, ) -> SqlModel: parsed_definition = parse(f"{meta};{query}", default_dialect=dialect) return t.cast( SqlModel, - load_sql_based_model(parsed_definition, dialect=dialect, default_catalog=default_catalog), + load_sql_based_model( + parsed_definition, dialect=dialect, default_catalog=default_catalog, **kwargs + ), ) @@ -2814,3 +2818,271 @@ def test_test_generation_with_timestamp_nat(tmp_path: Path) -> None: assert query_output[0]["ts_col"] == datetime.datetime(2024, 9, 20, 11, 30, 0, 123456) assert query_output[1]["ts_col"] is None assert query_output[2]["ts_col"] == datetime.datetime(2024, 9, 21, 15, 45, 0, 987654) + + +def test_parameterized_name_sql_model() -> None: + variables = {"table_catalog": "gold"} + model = _create_model( + "select 1 as id, 'foo' as name", + meta=""" + MODEL ( + name @{table_catalog}.sushi.foo, + kind FULL + ) + """, + dialect="snowflake", + variables=variables, + ) + assert model.fqn == '"GOLD"."SUSHI"."FOO"' + + test = _create_test( + body=load_yaml( + """ +test_foo: + model: {{ var('table_catalog' ) }}.sushi.foo + outputs: + query: + - id: 1 + name: foo + """, + variables=variables, + ), + test_name="test_foo", + model=model, + context=Context( + config=Config( + model_defaults=ModelDefaultsConfig(dialect="snowflake"), variables=variables + ) + ), + ) + + assert test.body["model"] == '"GOLD"."SUSHI"."FOO"' + + _check_successful_or_raise(test.run()) + + +def test_parameterized_name_python_model() -> None: + variables = {"table_catalog": "gold"} + + @model( + name="@{table_catalog}.sushi.foo", + columns={ + "id": "int", + "name": "varchar", + }, + dialect="snowflake", + ) + def execute( + context: ExecutionContext, + **kwargs: t.Any, + ) -> pd.DataFrame: + return pd.DataFrame([{"ID": 1, "NAME": "foo"}]) + + python_model = model.get_registry()["@{table_catalog}.sushi.foo"].model( + module_path=Path("."), path=Path("."), variables=variables + ) + + assert python_model.fqn == '"GOLD"."SUSHI"."FOO"' + + test = _create_test( + body=load_yaml( + """ +test_foo: + model: {{ var('table_catalog' ) }}.sushi.foo + outputs: + query: + - id: 1 + name: foo + """, + variables=variables, + ), + test_name="test_foo", + model=python_model, + context=Context( + config=Config( + model_defaults=ModelDefaultsConfig(dialect="snowflake"), variables=variables + ) + ), + ) + + assert test.body["model"] == '"GOLD"."SUSHI"."FOO"' + + _check_successful_or_raise(test.run()) + + +def test_parameterized_name_self_referential_model(): + variables = {"table_catalog": "gold"} + model = _create_model( + """ + with last_value as ( + select coalesce(max(v), 0) as v from @{table_catalog}.sushi.foo + ) + select v + 1 as v from last_value + """, + meta=""" + MODEL ( + name @{table_catalog}.sushi.foo, + kind FULL + ) + """, + dialect="snowflake", + variables=variables, + ) + assert model.fqn == '"GOLD"."SUSHI"."FOO"' + + test1 = _create_test( + body=load_yaml( + """ +test_foo_intial_state: + model: {{ var('table_catalog' ) }}.sushi.foo + inputs: + {{ var('table_catalog' ) }}.sushi.foo: + rows: [] + columns: + v: int + outputs: + query: + - v: 1 + """, + variables=variables, + ), + test_name="test_foo_intial_state", + model=model, + context=Context( + config=Config( + model_defaults=ModelDefaultsConfig(dialect="snowflake"), variables=variables + ) + ), + ) + assert isinstance(test1, SqlModelTest) + assert test1.body["model"] == '"GOLD"."SUSHI"."FOO"' + test1_model_query = test1._render_model_query().sql(dialect="snowflake") + assert '"GOLD"."SUSHI"."FOO"' not in test1_model_query + assert ( + test1._test_fixture_table('"GOLD"."SUSHI"."FOO"').sql(dialect="snowflake", identify=True) + in test1_model_query + ) + + test2 = _create_test( + body=load_yaml( + """ +test_foo_cumulative: + model: {{ var('table_catalog' ) }}.sushi.foo + inputs: + {{ var('table_catalog' ) }}.sushi.foo: + rows: + - v: 5 + outputs: + query: + - v: 6 + """, + variables=variables, + ), + test_name="test_foo_cumulative", + model=model, + context=Context( + config=Config( + model_defaults=ModelDefaultsConfig(dialect="snowflake"), variables=variables + ) + ), + ) + assert isinstance(test2, SqlModelTest) + assert test2.body["model"] == '"GOLD"."SUSHI"."FOO"' + test2_model_query = test2._render_model_query().sql(dialect="snowflake") + assert '"GOLD"."SUSHI"."FOO"' not in test2_model_query + assert ( + test2._test_fixture_table('"GOLD"."SUSHI"."FOO"').sql(dialect="snowflake", identify=True) + in test2_model_query + ) + + _check_successful_or_raise(test1.run()) + _check_successful_or_raise(test2.run()) + + +def test_parameterized_name_self_referential_python_model(): + variables = {"table_catalog": "gold"} + + @model( + name="@{table_catalog}.sushi.foo", + columns={ + "id": "int", + }, + depends_on=["@{table_catalog}.sushi.bar"], + dialect="snowflake", + ) + def execute( + context: ExecutionContext, + **kwargs: t.Any, + ) -> pd.DataFrame: + current_table = context.resolve_table(f"{context.var('table_catalog')}.sushi.foo") + current_df = context.fetchdf(f"select id from {current_table}") + upstream_table = context.resolve_table(f"{context.var('table_catalog')}.sushi.bar") + upstream_df = context.fetchdf(f"select id from {upstream_table}") + + return pd.DataFrame([{"ID": upstream_df["ID"].sum() + current_df["ID"].sum()}]) + + @model( + name="@{table_catalog}.sushi.bar", + columns={ + "id": "int", + }, + dialect="snowflake", + ) + def execute( + context: ExecutionContext, + **kwargs: t.Any, + ) -> pd.DataFrame: + return pd.DataFrame([{"ID": 1}]) + + model_foo = model.get_registry()["@{table_catalog}.sushi.foo"].model( + module_path=Path("."), path=Path("."), variables=variables + ) + model_bar = model.get_registry()["@{table_catalog}.sushi.bar"].model( + module_path=Path("."), path=Path("."), variables=variables + ) + + assert model_foo.fqn == '"GOLD"."SUSHI"."FOO"' + assert model_bar.fqn == '"GOLD"."SUSHI"."BAR"' + + ctx = Context( + config=Config(model_defaults=ModelDefaultsConfig(dialect="snowflake"), variables=variables) + ) + ctx.upsert_model(model_foo) + ctx.upsert_model(model_bar) + + test = _create_test( + body=load_yaml( + """ +test_foo: + model: {{ var('table_catalog') }}.sushi.foo + inputs: + {{ var('table_catalog') }}.sushi.foo: + rows: + - id: 3 + {{ var('table_catalog') }}.sushi.bar: + rows: + - id: 5 + outputs: + query: + - id: 8 + """, + variables=variables, + ), + test_name="test_foo", + model=model_foo, + context=ctx, + ) + + assert isinstance(test, PythonModelTest) + + assert test.body["model"] == '"GOLD"."SUSHI"."FOO"' + assert '"GOLD"."SUSHI"."BAR"' in test.body["inputs"] + + assert isinstance(test.context, TestExecutionContext) + assert '"GOLD"."SUSHI"."FOO"' in test.context._model_tables + assert '"GOLD"."SUSHI"."BAR"' in test.context._model_tables + + with pytest.raises(SQLMeshError, match=r"Unable to find a table mapping"): + test.context.resolve_table("silver.sushi.bar") + + _check_successful_or_raise(test.run()) From 75d161559dbec842b2772badbe252ce7642af323 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 15 Jul 2025 12:07:25 +0300 Subject: [PATCH 0578/1056] Chore: Show resources to be deleted and enhance warnings in destroy (#4965) --- docs/reference/cli.md | 2 +- docs/reference/notebook.md | 2 +- sqlmesh/core/console.py | 59 +++++++++++++++++++---- sqlmesh/core/context.py | 52 ++++++++++++++++++-- tests/core/test_integration.py | 4 +- tests/integrations/jupyter/test_magics.py | 14 +++--- 6 files changed, 109 insertions(+), 24 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d5e7b2ce07..b6877962ab 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -149,7 +149,7 @@ Options: ``` Usage: sqlmesh destroy - Removes all project resources, including warehouse objects, state tables, the SQLMesh cache and any build artifacts. + Removes all state tables, the SQLMesh cache and all project resources, including warehouse objects. This includes all tables, views and schemas managed by SQLMesh, as well as any external resources that may have been created by other tools within those schemas. Options: --help Show this message and exit. diff --git a/docs/reference/notebook.md b/docs/reference/notebook.md index 47c8731130..6cac4e1078 100644 --- a/docs/reference/notebook.md +++ b/docs/reference/notebook.md @@ -250,7 +250,7 @@ options: ``` %destroy -Removes all project resources, including warehouse objects, state tables, the SQLMesh cache and any build artifacts. +Removes all state tables, the SQLMesh cache, and other project resources, including warehouse objects. This includes all tables, views, and schemas managed by SQLMesh, as well as any external resources that may have been created by other tools within those schemas. ``` #### dlt_refresh diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index c14b9be2b5..118abcd769 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -186,9 +186,19 @@ class DestroyConsole(abc.ABC): """Console for describing a destroy operation""" @abc.abstractmethod - def start_destroy(self) -> bool: + def start_destroy( + self, + schemas_to_delete: t.Optional[t.Set[str]] = None, + views_to_delete: t.Optional[t.Set[str]] = None, + tables_to_delete: t.Optional[t.Set[str]] = None, + ) -> bool: """Start a destroy operation. + Args: + schemas_to_delete: Set of schemas that will be deleted + views_to_delete: Set of views that will be deleted + tables_to_delete: Set of tables that will be deleted + Returns: Whether or not the destroy operation should proceed """ @@ -830,7 +840,12 @@ def print_connection_config( ) -> None: pass - def start_destroy(self) -> bool: + def start_destroy( + self, + schemas_to_delete: t.Optional[t.Set[str]] = None, + views_to_delete: t.Optional[t.Set[str]] = None, + tables_to_delete: t.Optional[t.Set[str]] = None, + ) -> bool: return True def stop_destroy(self, success: bool = True) -> None: @@ -1282,16 +1297,40 @@ def stop_cleanup(self, success: bool = False) -> None: else: self.log_error("Cleanup failed!") - def start_destroy(self) -> bool: + def start_destroy( + self, + schemas_to_delete: t.Optional[t.Set[str]] = None, + views_to_delete: t.Optional[t.Set[str]] = None, + tables_to_delete: t.Optional[t.Set[str]] = None, + ) -> bool: self.log_warning( - ( - "This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n" - "The operation is irreversible and may disrupt any currently running or scheduled plans.\n" - "Use this command only when you intend to fully reset the project." - ) + "This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n" + "The operation may disrupt any currently running or scheduled plans.\n" ) - if not self._confirm("Proceed?"): - self.log_error("Destroy aborted!") + + if schemas_to_delete or views_to_delete or tables_to_delete: + if schemas_to_delete: + self.log_error("Schemas to be deleted:") + for schema in sorted(schemas_to_delete): + self.log_error(f" • {schema}") + + if views_to_delete: + self.log_error("\nEnvironment views to be deleted:") + for view in sorted(views_to_delete): + self.log_error(f" • {view}") + + if tables_to_delete: + self.log_error("\nSnapshot tables to be deleted:") + for table in sorted(tables_to_delete): + self.log_error(f" • {table}") + + self.log_error( + "\nThis action will DELETE ALL the above resources managed by SQLMesh AND\n" + "potentially external resources created by other tools in these schemas.\n" + ) + + if not self._confirm("Are you ABSOLUTELY SURE you want to proceed with deletion?"): + self.log_error("Destroy operation cancelled.") return False return True diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index d6a305f7c0..a2662ad0c2 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -858,10 +858,52 @@ def run_janitor(self, ignore_ttl: bool) -> bool: def destroy(self) -> bool: success = False - if self.console.start_destroy(): + # Collect resources to be deleted + environments = self.state_reader.get_environments() + schemas_to_delete = set() + tables_to_delete = set() + views_to_delete = set() + all_snapshot_infos = set() + + # For each environment find schemas and tables + for environment in environments: + all_snapshot_infos.update(environment.snapshots) + snapshots = self.state_reader.get_snapshots(environment.snapshots).values() + for snapshot in snapshots: + if snapshot.is_model and not snapshot.is_symbolic: + # Get the appropriate adapter + if environment.gateway_managed and snapshot.model_gateway: + adapter = self.engine_adapters.get( + snapshot.model_gateway, self.engine_adapter + ) + else: + adapter = self.engine_adapter + + if environment.suffix_target.is_schema or environment.suffix_target.is_catalog: + schema = snapshot.qualified_view_name.schema_for_environment( + environment.naming_info, dialect=adapter.dialect + ) + catalog = snapshot.qualified_view_name.catalog_for_environment( + environment.naming_info, dialect=adapter.dialect + ) + if catalog: + schemas_to_delete.add(f"{catalog}.{schema}") + else: + schemas_to_delete.add(schema) + + if environment.suffix_target.is_table: + view_name = snapshot.qualified_view_name.for_environment( + environment.naming_info, dialect=adapter.dialect + ) + views_to_delete.add(view_name) + + # Add snapshot tables + table_name = snapshot.table_name() + tables_to_delete.add(table_name) + + if self.console.start_destroy(schemas_to_delete, views_to_delete, tables_to_delete): try: - self._destroy() - success = True + success = self._destroy() finally: self.console.stop_destroy(success=success) @@ -2723,7 +2765,7 @@ def _context_diff( always_recreate_environment=always_recreate_environment, ) - def _destroy(self) -> None: + def _destroy(self) -> bool: # Invalidate all environments, including prod for environment in self.state_reader.get_environments(): self.state_sync.invalidate_environment(name=environment.name, protect_prod=False) @@ -2739,6 +2781,8 @@ def _destroy(self) -> None: # Finally clear caches self.clear_caches() + return True + def _run_janitor(self, ignore_ttl: bool = False) -> None: current_ts = now_timestamp() diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index d03db7af91..7337f8d3f4 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -6354,7 +6354,9 @@ def test_destroy(copy_to_temp_path): context.fetchdf(f"SELECT * FROM db_1.first_schema.model_two") # Use the destroy command to remove all data objects and state - context._destroy() + # Mock the console confirmation to automatically return True + with patch.object(context.console, "_confirm", return_value=True): + context._destroy() # Ensure all tables have been removed for table_name in state_tables: diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index 6507f07120..c8e38f448c 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -898,19 +898,19 @@ def test_destroy( assert not output.stderr text_output = convert_all_html_output_to_text(output) expected_messages = [ - "[WARNING] This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\n" - "The operation is irreversible and may disrupt any currently running or scheduled plans.\n" - "Use this command only when you intend to fully reset the project.", + "[WARNING] This will permanently delete all engine-managed objects, state tables and SQLMesh cache.\nThe operation may disrupt any currently running or scheduled plans.", + "Schemas to be deleted:", + "• memory.sushi", + "Snapshot tables to be deleted:", + "This action will DELETE ALL the above resources managed by SQLMesh AND\npotentially external resources created by other tools in these schemas.", + "Are you ABSOLUTELY SURE you want to proceed with deletion? [y/n]:", "Environment 'prod' invalidated.", "Deleted object memory.sushi", 'Deleted object "memory"."raw"."model1"', - 'Deleted object "memory"."raw"."model1"', - 'Deleted object "memory"."raw"."model2"', 'Deleted object "memory"."raw"."model2"', 'Deleted object "memory"."raw"."demographics"', - 'Deleted object "memory"."raw"."demographics"', "State tables removed.", "Destroy completed successfully.", ] for message in expected_messages: - assert message in text_output + assert any(message in line for line in text_output) From 4be13ff2cbfbd784735c5143d144300e5ed6d8fc Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 15 Jul 2025 09:05:44 -0700 Subject: [PATCH 0579/1056] chore: remove private tests while they are reworked (#4968) --- .circleci/continue_config.yml | 40 ----------------------------------- 1 file changed, 40 deletions(-) diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index eab55143d5..34bdf0e98b 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -201,39 +201,6 @@ jobs: name: Run tests command: npm --prefix web/client run test - trigger_private_tests: - docker: - - image: cimg/python:3.12.0 - resource_class: small - steps: - - checkout - - run: - name: Install setuptools scm - command: pip install setuptools_scm - - run: - name: Trigger private tests - command: | - export COMMIT_MESSAGE="$(git log --format=%s -n 1 $CIRCLE_SHA1)" - export FORMATTED_COMMIT_MESSAGE="${COMMIT_MESSAGE//\"/\\\"}" - # returns a version string like 0.1.0.dev11 - export PACKAGE_VERSION="$(python ./.circleci/get_scm_version.py)" - curl --request POST \ - --url $TOBIKO_PRIVATE_CIRCLECI_URL \ - --header "Circle-Token: $TOBIKO_PRIVATE_CIRCLECI_KEY" \ - --header "content-type: application/json" \ - --data '{ - "branch":"main", - "parameters":{ - "run_main_pr":false, - "run_sqlmesh_commit":true, - "sqlmesh_branch":"'$CIRCLE_BRANCH'", - "sqlmesh_commit_author":"'$CIRCLE_USERNAME'", - "sqlmesh_commit_hash":"'$CIRCLE_SHA1'", - "sqlmesh_commit_message":"'"$FORMATTED_COMMIT_MESSAGE"'", - "sqlmesh_package_version":"'$PACKAGE_VERSION'" - } - }' - engine_tests_docker: parameters: engine: @@ -340,13 +307,6 @@ workflows: branches: only: - main - - trigger_private_tests: - requires: - - style_and_cicd_tests - filters: - branches: - only: - - main - ui_style - ui_test - vscode_test From ed8b404db1ece77d2cf88fec0b139ea4e75bf02e Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 15 Jul 2025 21:38:20 +0300 Subject: [PATCH 0580/1056] Fix: Preserve actual dataframe indices in unit test output (#4969) --- sqlmesh/utils/rich.py | 2 +- tests/core/test_test.py | 147 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 145 insertions(+), 4 deletions(-) diff --git a/sqlmesh/utils/rich.py b/sqlmesh/utils/rich.py index 589dd0b50f..0b43e3d87c 100644 --- a/sqlmesh/utils/rich.py +++ b/sqlmesh/utils/rich.py @@ -95,7 +95,7 @@ def df_to_table( rich_table.add_column(Align.center(column_name)) - for index, value_list in enumerate(df.values.tolist()): + for index, value_list in zip(df.index, df.values.tolist()): row = [str(index)] if show_index else [] row += [str(x) for x in value_list] center = [Align.center(x) for x in row] diff --git a/tests/core/test_test.py b/tests/core/test_test.py index c15c06fc35..9eddf2f524 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -1026,6 +1026,97 @@ def test_row_difference_failure() -> None: ) +def test_index_preservation_with_later_rows() -> None: + # Test comparison with differences in later rows + _check_successful_or_raise( + _create_test( + body=load_yaml( + """ +test_foo: + model: sushi.foo + inputs: + raw: + - id: 1 + value: 100 + - id: 2 + value: 200 + - id: 3 + value: 300 + - id: 4 + value: 400 + outputs: + query: + - id: 1 + value: 100 + - id: 2 + value: 200 + - id: 3 + value: 999 + - id: 4 + value: 888 + """ + ), + test_name="test_foo", + model=_create_model("SELECT id, value FROM raw"), + context=Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))), + ).run(), + expected_msg=( + "AssertionError: Data mismatch (exp: expected, act: actual)\n\n" + " value \n" + " exp act\n" + "2 999.0 300.0\n" + "3 888.0 400.0\n" + ), + ) + + # Test with null values in later rows + _check_successful_or_raise( + _create_test( + body=load_yaml( + """ +test_foo: + model: sushi.foo + inputs: + raw: + - id: 1 + value: 100 + - id: 2 + value: 200 + - id: 3 + value: null + - id: 4 + value: 400 + - id: 5 + value: null + outputs: + query: + - id: 1 + value: 100 + - id: 2 + value: 200 + - id: 3 + value: 300 + - id: 4 + value: null + - id: 5 + value: 500 + """ + ), + test_name="test_foo", + model=_create_model("SELECT id, value FROM raw"), + context=Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))), + ).run(), + expected_msg=( + "AssertionError: Data mismatch (exp: expected, act: actual)\n\n" + " value \n" + " exp act\n" + "2 300.0 NaN\n" + "3 NaN 400.0\n" + "4 500.0 NaN\n" + ), + ) + + def test_unknown_column_error() -> None: _check_successful_or_raise( _create_test( @@ -2277,7 +2368,7 @@ def test_test_output(tmp_path: Path) -> None: ┃ ┃ item_id: ┃ ┃ num_orders: ┃ num_orders: ┃ ┃ Row ┃ Expected ┃ item_id: Actual ┃ Expected ┃ Actual ┃ ┡━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ -│ 0 │ 4.0 │ 2.0 │ 3.0 │ 1.0 │ +│ 1 │ 4.0 │ 2.0 │ 3.0 │ 1.0 │ └─────┴─────────────────┴─────────────────┴─────────────────┴──────────────────┘ ----------------------------------------------------------------------""" @@ -2300,14 +2391,14 @@ def test_test_output(tmp_path: Path) -> None: ┏━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ ┃ Row ┃ Expected ┃ Actual ┃ ┡━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ -│ 0 │ 4.0 │ 2.0 │ +│ 1 │ 4.0 │ 2.0 │ └─────────────┴────────────────────────┴───────────────────┘ Column 'num_orders' mismatch ┏━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ ┃ Row ┃ Expected ┃ Actual ┃ ┡━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ -│ 0 │ 3.0 │ 1.0 │ +│ 1 │ 3.0 │ 1.0 │ └─────────────┴────────────────────────┴───────────────────┘ ----------------------------------------------------------------------""" @@ -2386,6 +2477,56 @@ def test_test_output(tmp_path: Path) -> None: in captured_output.stdout ) + # Case 5: Test null value difference in the 3rd row (index 2) + rmtree(tmp_path / "tests") + tests_dir = tmp_path / "tests" + tests_dir.mkdir() + + null_test_file = tmp_path / "tests" / "test_null_in_third_row.yaml" + null_test_file.write_text( + """ +test_null_third_row: + model: sqlmesh_example.full_model + description: Test null value in third row + inputs: + sqlmesh_example.incremental_model: + rows: + - id: 1 + item_id: 1 + - id: 2 + item_id: 1 + - id: 3 + item_id: 2 + - id: 4 + item_id: 3 + outputs: + query: + rows: + - item_id: 1 + num_orders: 2 + - item_id: 2 + num_orders: 1 + - item_id: 3 + num_orders: null + """ + ) + + with capture_output() as captured_output: + context.test() + + output = captured_output.stdout + + # Check for null value difference in the 3rd row (index 2) + assert ( + """ +┏━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Row ┃ num_orders: Expected ┃ num_orders: Actual ┃ +┡━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ +│ 2 │ nan │ 1.0 │ +└──────┴───────────────────────────┴───────────────────────┘""" + in output + ) + @use_terminal_console def test_test_output_with_invalid_model_name(tmp_path: Path) -> None: From e2a406fa6f00ca55464f97b3343148a06b538b59 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 16 Jul 2025 14:49:47 +1200 Subject: [PATCH 0581/1056] Fix(duckdb): Use CREATE OR REPLACE when registering secrets on cursor init to prevent an 'already exists' error (#4974) --- sqlmesh/core/config/connection.py | 4 ++- .../integration/test_integration_duckdb.py | 36 ++++++++++++++++++- tests/core/test_connection_config.py | 16 +++++---- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 47b64b5fc4..49d49e40e7 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -380,7 +380,9 @@ def init(cursor: duckdb.DuckDBPyConnection) -> None: if secret_settings: secret_clause = ", ".join(secret_settings) try: - cursor.execute(f"CREATE SECRET {secret_name} ({secret_clause});") + cursor.execute( + f"CREATE OR REPLACE SECRET {secret_name} ({secret_clause});" + ) except Exception as e: raise ConfigError(f"Failed to create secret: {e}") diff --git a/tests/core/engine_adapter/integration/test_integration_duckdb.py b/tests/core/engine_adapter/integration/test_integration_duckdb.py index 96c31593ff..ce7fb1b80d 100644 --- a/tests/core/engine_adapter/integration/test_integration_duckdb.py +++ b/tests/core/engine_adapter/integration/test_integration_duckdb.py @@ -3,6 +3,8 @@ from threading import current_thread, Thread import random from sqlglot import exp +from pathlib import Path +from concurrent.futures import ThreadPoolExecutor, as_completed from sqlmesh.core.config.connection import DuckDBConnectionConfig from sqlmesh.utils.connection_pool import ThreadLocalSharedConnectionPool @@ -11,7 +13,7 @@ @pytest.mark.parametrize("database", [None, "db.db"]) -def test_multithread_concurrency(tmp_path, database: t.Optional[str]): +def test_multithread_concurrency(tmp_path: Path, database: t.Optional[str]): num_threads = 100 if database: @@ -72,3 +74,35 @@ def read_from_thread(): tables = adapter.fetchall("show tables") assert len(tables) == num_threads + 1 + + +def test_secret_registration_from_multiple_connections(tmp_path: Path): + database = str(tmp_path / "db.db") + + config = DuckDBConnectionConfig( + database=database, + concurrent_tasks=2, + secrets={"s3": {"type": "s3", "region": "us-east-1", "key_id": "foo", "secret": "bar"}}, + ) + + adapter = config.create_engine_adapter() + pool = adapter._connection_pool + + assert isinstance(pool, ThreadLocalSharedConnectionPool) + + def _open_connection() -> bool: + # this triggers cursor_init() to be run again for the new connection from the new thread + # if the operations in cursor_init() are not idempotent, DuckDB will throw an error and this test will fail + cur = pool.get_cursor() + cur.execute("SELECT name FROM duckdb_secrets()") + secret_names = [name for name_row in cur.fetchall() for name in name_row] + assert secret_names == ["s3"] + return True + + thread_pool = ThreadPoolExecutor(max_workers=4) + futures = [] + for _ in range(10): + futures.append(thread_pool.submit(_open_connection)) + + for future in as_completed(futures): + assert future.result() diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 02ec5271a4..7fe2487891 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -489,7 +489,9 @@ def test_duckdb_multiple_secrets(mock_connect, make_config): cursor = config.create_engine_adapter().cursor execute_calls = [call[0][0] for call in mock_cursor.execute.call_args_list] - create_secret_calls = [call for call in execute_calls if call.startswith("CREATE SECRET")] + create_secret_calls = [ + call for call in execute_calls if call.startswith("CREATE OR REPLACE SECRET") + ] # Should have exactly 2 CREATE SECRET calls assert len(create_secret_calls) == 2 @@ -497,13 +499,13 @@ def test_duckdb_multiple_secrets(mock_connect, make_config): # Verify the SQL for the first secret (S3) assert ( create_secret_calls[0] - == "CREATE SECRET (type 's3', region 'us-east-1', key_id 'my_aws_key', secret 'my_aws_secret');" + == "CREATE OR REPLACE SECRET (type 's3', region 'us-east-1', key_id 'my_aws_key', secret 'my_aws_secret');" ) # Verify the SQL for the second secret (Azure) assert ( create_secret_calls[1] - == "CREATE SECRET (type 'azure', account_name 'myaccount', account_key 'myaccountkey');" + == "CREATE OR REPLACE SECRET (type 'azure', account_name 'myaccount', account_key 'myaccountkey');" ) @@ -541,7 +543,9 @@ def test_duckdb_named_secrets(mock_connect, make_config): cursor = config.create_engine_adapter().cursor execute_calls = [call[0][0] for call in mock_cursor.execute.call_args_list] - create_secret_calls = [call for call in execute_calls if call.startswith("CREATE SECRET")] + create_secret_calls = [ + call for call in execute_calls if call.startswith("CREATE OR REPLACE SECRET") + ] # Should have exactly 2 CREATE SECRET calls assert len(create_secret_calls) == 2 @@ -549,13 +553,13 @@ def test_duckdb_named_secrets(mock_connect, make_config): # Verify the SQL for the first secret (S3) includes the secret name assert ( create_secret_calls[0] - == "CREATE SECRET my_s3_secret (type 's3', region 'us-east-1', key_id 'my_aws_key', secret 'my_aws_secret');" + == "CREATE OR REPLACE SECRET my_s3_secret (type 's3', region 'us-east-1', key_id 'my_aws_key', secret 'my_aws_secret');" ) # Verify the SQL for the second secret (Azure) includes the secret name assert ( create_secret_calls[1] - == "CREATE SECRET my_azure_secret (type 'azure', account_name 'myaccount', account_key 'myaccountkey');" + == "CREATE OR REPLACE SECRET my_azure_secret (type 'azure', account_name 'myaccount', account_key 'myaccountkey');" ) From 39caa0f16994e2591d6f53fb263d480a5f6f1e09 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Wed, 16 Jul 2025 12:53:29 +0300 Subject: [PATCH 0582/1056] Feat: Tag BigQuery queries with their correlation ID as label (#4861) --- sqlmesh/core/engine_adapter/bigquery.py | 9 +++++ .../integration/test_integration_bigquery.py | 33 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 648594d0c0..adb4aa0d19 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -134,6 +134,10 @@ def _job_params(self) -> t.Dict[str, t.Any]: } if self._extra_config.get("maximum_bytes_billed"): params["maximum_bytes_billed"] = self._extra_config.get("maximum_bytes_billed") + if self.correlation_id: + # BigQuery label keys must be lowercase + key = self.correlation_id.job_type.value.lower() + params["labels"] = {key: self.correlation_id.job_id} return params @property @@ -204,6 +208,11 @@ def _begin_session(self, properties: SessionProperties) -> None: "Invalid value for `session_properties.query_label`. Must be an array or tuple." ) + if self.correlation_id: + parsed_query_label.append( + (self.correlation_id.job_type.value.lower(), self.correlation_id.job_id) + ) + if parsed_query_label: query_label_str = ",".join([":".join(label) for label in parsed_query_label]) query = f'SET @@query_label = "{query_label_str}";SELECT 1;' diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index e974d79da2..c97c94d036 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -11,8 +11,9 @@ from sqlmesh.core.engine_adapter.shared import DataObject import sqlmesh.core.dialect as d from sqlmesh.core.model import SqlModel, load_sql_based_model -from sqlmesh.core.plan import Plan +from sqlmesh.core.plan import Plan, BuiltInPlanEvaluator from sqlmesh.core.table_diff import TableDiff +from sqlmesh.utils import CorrelationId from tests.core.engine_adapter.integration import TestContext from pytest import FixtureRequest from tests.core.engine_adapter.integration import ( @@ -447,3 +448,33 @@ def test_materialized_view_evaluation(ctx: TestContext, engine_adapter: BigQuery df = engine_adapter.fetchdf(f"SELECT * FROM {mview_name.sql(dialect=ctx.dialect)}") assert df["col"][0] == 2 + + +def test_correlation_id_in_job_labels(ctx: TestContext): + model_name = ctx.table("test") + + sqlmesh = ctx.create_context() + sqlmesh.upsert_model( + load_sql_based_model(d.parse(f"MODEL (name {model_name}, kind FULL); SELECT 1 AS col")) + ) + + # Create a plan evaluator and a plan to evaluate + plan_evaluator = BuiltInPlanEvaluator( + sqlmesh.state_sync, + sqlmesh.snapshot_evaluator, + sqlmesh.create_scheduler, + sqlmesh.default_catalog, + ) + plan: Plan = sqlmesh.plan_builder("prod", skip_tests=True).build() + + # Evaluate the plan and retrieve the plan evaluator's adapter + plan_evaluator.evaluate(plan.to_evaluatable()) + adapter = t.cast(BigQueryEngineAdapter, plan_evaluator.snapshot_evaluator.adapter) + + # Case 1: Ensure that the correlation id is set in the underlying adapter + assert adapter.correlation_id is not None + + # Case 2: Ensure that the correlation id is set in the job labels + labels = adapter._job_params.get("labels") + correlation_id = CorrelationId.from_plan_id(plan.plan_id) + assert labels == {correlation_id.job_type.value.lower(): correlation_id.job_id} From 9b26320f3425348f3fa008c3edcfed3f779af11d Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 16 Jul 2025 19:06:38 +0300 Subject: [PATCH 0583/1056] Fix: Don't use SCD type 2 restatement logic in regular runs (#4976) --- sqlmesh/core/engine_adapter/base.py | 21 ++-- sqlmesh/core/engine_adapter/trino.py | 2 + sqlmesh/core/plan/evaluator.py | 6 + sqlmesh/core/scheduler.py | 9 ++ sqlmesh/core/snapshot/evaluator.py | 7 ++ tests/core/engine_adapter/test_base.py | 7 ++ tests/core/test_integration.py | 163 +++++++++++++++++++++++++ tests/core/test_snapshot_evaluator.py | 2 + 8 files changed, 210 insertions(+), 7 deletions(-) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 33ad4c398a..c615a3029d 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -1462,6 +1462,7 @@ def scd_type_2_by_time( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, + is_restatement: bool = False, **kwargs: t.Any, ) -> None: self._scd_type_2( @@ -1478,6 +1479,7 @@ def scd_type_2_by_time( table_description=table_description, column_descriptions=column_descriptions, truncate=truncate, + is_restatement=is_restatement, **kwargs, ) @@ -1496,6 +1498,7 @@ def scd_type_2_by_column( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, + is_restatement: bool = False, **kwargs: t.Any, ) -> None: self._scd_type_2( @@ -1512,6 +1515,7 @@ def scd_type_2_by_column( table_description=table_description, column_descriptions=column_descriptions, truncate=truncate, + is_restatement=is_restatement, **kwargs, ) @@ -1533,6 +1537,7 @@ def _scd_type_2( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, + is_restatement: bool = False, **kwargs: t.Any, ) -> None: def remove_managed_columns( @@ -1718,13 +1723,15 @@ def remove_managed_columns( target_table ) - cleanup_ts = None if truncate: existing_rows_query = existing_rows_query.limit(0) - else: - # If truncate is false it is not the first insert - # Determine the cleanup timestamp for restatement or a regular incremental run - cleanup_ts = to_time_column(start, time_data_type, self.dialect, nullable=True) + + # Only set cleanup_ts if is_restatement is True and truncate is False (this to enable full restatement) + cleanup_ts = ( + to_time_column(start, time_data_type, self.dialect, nullable=True) + if is_restatement and not truncate + else None + ) with source_queries[0] as source_query: prefixed_columns_to_types = [] @@ -1763,7 +1770,7 @@ def remove_managed_columns( .with_( "static", existing_rows_query.where(valid_to_col.is_(exp.Null()).not_()) - if truncate + if cleanup_ts is None else existing_rows_query.where( exp.and_( valid_to_col.is_(exp.Null().not_()), @@ -1775,7 +1782,7 @@ def remove_managed_columns( .with_( "latest", existing_rows_query.where(valid_to_col.is_(exp.Null())) - if truncate + if cleanup_ts is None else exp.select( *( to_time_column( diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index 06d693e11c..7862bfca2d 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -267,6 +267,7 @@ def _scd_type_2( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, + is_restatement: bool = False, **kwargs: t.Any, ) -> None: if columns_to_types and self.current_catalog_type == "delta_lake": @@ -289,6 +290,7 @@ def _scd_type_2( table_description, column_descriptions, truncate, + is_restatement, **kwargs, ) diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 9488b9bc91..bb779fffe9 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -234,6 +234,11 @@ def visit_backfill_stage(self, stage: stages.BackfillStage, plan: EvaluatablePla return scheduler = self.create_scheduler(stage.all_snapshots.values(), self.snapshot_evaluator) + # Convert model name restatements to snapshot ID restatements + restatements_by_snapshot_id = { + stage.all_snapshots[name].snapshot_id: interval + for name, interval in plan.restatements.items() + } errors, _ = scheduler.run_merged_intervals( merged_intervals=stage.snapshot_to_intervals, deployability_index=stage.deployability_index, @@ -242,6 +247,7 @@ def visit_backfill_stage(self, stage: stages.BackfillStage, plan: EvaluatablePla circuit_breaker=self._circuit_breaker, start=plan.start, end=plan.end, + restatements=restatements_by_snapshot_id, ) if errors: raise PlanError("Plan application failed.") diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index 4582b24485..7177efe927 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -161,6 +161,7 @@ def evaluate( deployability_index: DeployabilityIndex, batch_index: int, environment_naming_info: t.Optional[EnvironmentNamingInfo] = None, + is_restatement: bool = False, **kwargs: t.Any, ) -> t.List[AuditResult]: """Evaluate a snapshot and add the processed interval to the state sync. @@ -192,6 +193,7 @@ def evaluate( snapshots=snapshots, deployability_index=deployability_index, batch_index=batch_index, + is_restatement=is_restatement, **kwargs, ) audit_results = self._audit_snapshot( @@ -371,6 +373,7 @@ def run_merged_intervals( end: t.Optional[TimeLike] = None, run_environment_statements: bool = False, audit_only: bool = False, + restatements: t.Optional[t.Dict[SnapshotId, Interval]] = None, ) -> t.Tuple[t.List[NodeExecutionFailedError[SchedulingUnit]], t.List[SchedulingUnit]]: """Runs precomputed batches of missing intervals. @@ -447,6 +450,10 @@ def evaluate_node(node: SchedulingUnit) -> None: execution_time=execution_time, ) else: + # Determine if this snapshot and interval is a restatement (for SCD type 2) + is_restatement = ( + restatements is not None and snapshot.snapshot_id in restatements + ) audit_results = self.evaluate( snapshot=snapshot, environment_naming_info=environment_naming_info, @@ -455,6 +462,7 @@ def evaluate_node(node: SchedulingUnit) -> None: execution_time=execution_time, deployability_index=deployability_index, batch_index=batch_idx, + is_restatement=is_restatement, ) evaluation_duration_ms = now_timestamp() - execution_start_ts @@ -663,6 +671,7 @@ def _run_or_audit( end=end, run_environment_statements=run_environment_statements, audit_only=audit_only, + restatements=remove_intervals, ) return CompletionStatus.FAILURE if errors else CompletionStatus.SUCCESS diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 993860b527..f8aa08a075 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -140,6 +140,7 @@ def evaluate( snapshots: t.Dict[str, Snapshot], deployability_index: t.Optional[DeployabilityIndex] = None, batch_index: int = 0, + is_restatement: bool = False, **kwargs: t.Any, ) -> t.Optional[str]: """Renders the snapshot's model, executes it and stores the result in the snapshot's physical table. @@ -165,6 +166,7 @@ def evaluate( snapshots, deployability_index=deployability_index, batch_index=batch_index, + is_restatement=is_restatement, **kwargs, ) if result is None or isinstance(result, str): @@ -622,6 +624,7 @@ def _evaluate_snapshot( limit: t.Optional[int] = None, deployability_index: t.Optional[DeployabilityIndex] = None, batch_index: int = 0, + is_restatement: bool = False, **kwargs: t.Any, ) -> DF | str | None: """Renders the snapshot's model and executes it. The return value depends on whether the limit was specified. @@ -694,6 +697,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: end=end, execution_time=execution_time, physical_properties=rendered_physical_properties, + is_restatement=is_restatement, ) else: logger.info( @@ -715,6 +719,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: end=end, execution_time=execution_time, physical_properties=rendered_physical_properties, + is_restatement=is_restatement, ) with ( @@ -1833,6 +1838,7 @@ def insert( column_descriptions=model.column_descriptions, truncate=is_first_insert, start=kwargs["start"], + is_restatement=kwargs.get("is_restatement", False), ) elif isinstance(model.kind, SCDType2ByColumnKind): self.adapter.scd_type_2_by_column( @@ -1851,6 +1857,7 @@ def insert( column_descriptions=model.column_descriptions, truncate=is_first_insert, start=kwargs["start"], + is_restatement=kwargs.get("is_restatement", False), ) else: raise SQLMeshError( diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index faf1386877..6c9d2ee132 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -1223,6 +1223,7 @@ def test_scd_type_2_by_time(make_mocked_engine_adapter: t.Callable): }, execution_time=datetime(2020, 1, 1, 0, 0, 0), start=datetime(2020, 1, 1, 0, 0, 0), + is_restatement=True, ) assert ( @@ -1422,6 +1423,7 @@ def test_scd_type_2_by_time_no_invalidate_hard_deletes(make_mocked_engine_adapte }, execution_time=datetime(2020, 1, 1, 0, 0, 0), start=datetime(2020, 1, 1, 0, 0, 0), + is_restatement=True, ) assert ( @@ -1610,6 +1612,7 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): }, execution_time=datetime(2020, 1, 1, 0, 0, 0), start=datetime(2020, 1, 1, 0, 0, 0), + is_restatement=True, ) assert ( @@ -1799,6 +1802,7 @@ def test_scd_type_2_by_column(make_mocked_engine_adapter: t.Callable): execution_time=datetime(2020, 1, 1, 0, 0, 0), start=datetime(2020, 1, 1, 0, 0, 0), extra_col_ignore="testing", + is_restatement=True, ) assert ( @@ -1990,6 +1994,7 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab }, execution_time=datetime(2020, 1, 1, 0, 0, 0), start=datetime(2020, 1, 1, 0, 0, 0), + is_restatement=True, ) assert ( parse_one(adapter.cursor.execute.call_args[0][0]).sql() @@ -2352,6 +2357,7 @@ def test_scd_type_2_by_column_star_check(make_mocked_engine_adapter: t.Callable) }, execution_time=datetime(2020, 1, 1, 0, 0, 0), start=datetime(2020, 1, 1, 0, 0, 0), + is_restatement=True, ) assert ( @@ -2527,6 +2533,7 @@ def test_scd_type_2_by_column_no_invalidate_hard_deletes(make_mocked_engine_adap }, execution_time=datetime(2020, 1, 1, 0, 0, 0), start=datetime(2020, 1, 1, 0, 0, 0), + is_restatement=True, ) assert ( diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 7337f8d3f4..a3a54eb7b6 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -6916,3 +6916,166 @@ def _correlation_id_in_sqls(correlation_id: CorrelationId, mock_logger): assert str(correlation_id) == f"SQLMESH_PLAN: {plan.plan_id}" assert _correlation_id_in_sqls(correlation_id, mock_logger) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_scd_type_2_regular_run_with_offset(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + raw_employee_status = d.parse(""" + MODEL ( + name memory.hr_system.raw_employee_status, + kind FULL + ); + + SELECT + 1001 AS employee_id, + 'engineering' AS department, + 'EMEA' AS region, + '2023-01-08 15:00:00 UTC' AS last_modified; + """) + + employee_history = d.parse(""" + MODEL ( + name memory.hr_system.employee_history, + kind SCD_TYPE_2_BY_TIME ( + unique_key employee_id, + updated_at_name last_modified, + disable_restatement false + ), + owner hr_analytics, + cron '0 7 * * *', + grain employee_id, + description 'Historical tracking of employee status changes' + ); + + SELECT + employee_id::INT AS employee_id, + department::TEXT AS department, + region::TEXT AS region, + last_modified AS last_modified + FROM + memory.hr_system.raw_employee_status; + """) + + raw_employee_status_model = load_sql_based_model(raw_employee_status) + employee_history_model = load_sql_based_model(employee_history) + context.upsert_model(raw_employee_status_model) + context.upsert_model(employee_history_model) + + # Initial plan and apply + plan = context.plan_builder("prod", skip_tests=True).build() + context.apply(plan) + + query = "SELECT employee_id, department, region, valid_from, valid_to FROM memory.hr_system.employee_history ORDER BY employee_id, valid_from" + initial_data = context.engine_adapter.fetchdf(query) + + assert len(initial_data) == 1 + assert initial_data["valid_to"].isna().all() + assert initial_data["department"].tolist() == ["engineering"] + assert initial_data["region"].tolist() == ["EMEA"] + + # Apply a future plan with source changes a few hours before the cron time of the SCD Type 2 model BUT on the same day + with time_machine.travel("2023-01-09 00:10:00 UTC"): + raw_employee_status_v2 = d.parse(""" + MODEL ( + name memory.hr_system.raw_employee_status, + kind FULL + ); + + SELECT + 1001 AS employee_id, + 'engineering' AS department, + 'AMER' AS region, + '2023-01-09 00:10:00 UTC' AS last_modified; + """) + raw_employee_status_v2_model = load_sql_based_model(raw_employee_status_v2) + context.upsert_model(raw_employee_status_v2_model) + context.plan( + auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() + ) + + # The 7th hour of the day the run is kicked off for the SCD Type 2 model + with time_machine.travel("2023-01-09 07:00:01 UTC"): + context.run() + data_after_change = context.engine_adapter.fetchdf(query) + + # Validate the SCD2 records for employee 1001 + assert len(data_after_change) == 2 + assert data_after_change.iloc[0]["employee_id"] == 1001 + assert data_after_change.iloc[0]["department"] == "engineering" + assert data_after_change.iloc[0]["region"] == "EMEA" + assert str(data_after_change.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" + assert str(data_after_change.iloc[0]["valid_to"]) == "2023-01-09 00:10:00" + assert data_after_change.iloc[1]["employee_id"] == 1001 + assert data_after_change.iloc[1]["department"] == "engineering" + assert data_after_change.iloc[1]["region"] == "AMER" + assert str(data_after_change.iloc[1]["valid_from"]) == "2023-01-09 00:10:00" + assert pd.isna(data_after_change.iloc[1]["valid_to"]) + + # Update source model again a bit later on the same day + raw_employee_status_v2 = d.parse(""" + MODEL ( + name memory.hr_system.raw_employee_status, + kind FULL + ); + + SELECT + 1001 AS employee_id, + 'sales' AS department, + 'ANZ' AS region, + '2023-01-09 07:26:00 UTC' AS last_modified; + """) + raw_employee_status_v2_model = load_sql_based_model(raw_employee_status_v2) + context.upsert_model(raw_employee_status_v2_model) + context.plan( + auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() + ) + + # A day later the run is kicked off for the SCD Type 2 model again + with time_machine.travel("2023-01-10 07:00:00 UTC"): + context.run() + data_after_change = context.engine_adapter.fetchdf(query) + + # Validate the SCD2 history for employee 1001 after second change with the historical records intact + assert len(data_after_change) == 3 + assert data_after_change.iloc[0]["employee_id"] == 1001 + assert data_after_change.iloc[0]["department"] == "engineering" + assert data_after_change.iloc[0]["region"] == "EMEA" + assert str(data_after_change.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" + assert str(data_after_change.iloc[0]["valid_to"]) == "2023-01-09 00:10:00" + assert data_after_change.iloc[1]["employee_id"] == 1001 + assert data_after_change.iloc[1]["department"] == "engineering" + assert data_after_change.iloc[1]["region"] == "AMER" + assert str(data_after_change.iloc[1]["valid_from"]) == "2023-01-09 00:10:00" + assert str(data_after_change.iloc[1]["valid_to"]) == "2023-01-09 07:26:00" + assert data_after_change.iloc[2]["employee_id"] == 1001 + assert data_after_change.iloc[2]["department"] == "sales" + assert data_after_change.iloc[2]["region"] == "ANZ" + assert str(data_after_change.iloc[2]["valid_from"]) == "2023-01-09 07:26:00" + assert pd.isna(data_after_change.iloc[2]["valid_to"]) + + # Now test restatement still works as expected by restating from 2023-01-09 00:10:00 (first change) + with time_machine.travel("2023-01-10 07:38:00 UTC"): + plan = context.plan_builder( + "prod", + skip_tests=True, + restate_models=["memory.hr_system.employee_history"], + start="2023-01-09 00:10:00", + ).build() + context.apply(plan) + restated_data = context.engine_adapter.fetchdf(query) + + # Validate the SCD2 history after restatement + assert len(restated_data) == 2 + assert restated_data.iloc[0]["employee_id"] == 1001 + assert restated_data.iloc[0]["department"] == "engineering" + assert restated_data.iloc[0]["region"] == "EMEA" + assert str(restated_data.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" + assert str(restated_data.iloc[0]["valid_to"]) == "2023-01-09 07:26:00" + assert restated_data.iloc[1]["employee_id"] == 1001 + assert restated_data.iloc[1]["department"] == "sales" + assert restated_data.iloc[1]["region"] == "ANZ" + assert str(restated_data.iloc[1]["valid_from"]) == "2023-01-09 07:26:00" + assert pd.isna(restated_data.iloc[1]["valid_to"]) diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 93cef90daf..b01daf9e20 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -1973,6 +1973,7 @@ def test_insert_into_scd_type_2_by_time( column_descriptions={}, updated_at_as_valid_from=False, truncate=truncate, + is_restatement=False, start="2020-01-01", ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) @@ -2146,6 +2147,7 @@ def test_insert_into_scd_type_2_by_column( table_description=None, column_descriptions={}, truncate=truncate, + is_restatement=False, start="2020-01-01", ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) From 4fa9992c5290a326ef6f1df9ef459778de4ce3a3 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 16 Jul 2025 20:54:57 +0300 Subject: [PATCH 0584/1056] Feat: correctly handle the generation of VALUES expressions using macros (#4975) --- sqlmesh/core/dialect.py | 15 +++++++++++++++ tests/core/test_macros.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 88e09f4916..1a42480c13 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -419,6 +419,20 @@ def _parse_limit( return macro +def _parse_value(self: Parser, values: bool = True) -> t.Optional[exp.Expression]: + wrapped = self._match(TokenType.L_PAREN, advance=False) + + # The base _parse_value method always constructs a Tuple instance. This is problematic when + # generating values with a macro function, because it's impossible to tell whether the user's + # intention was to construct a row or a column with the VALUES expression. To avoid this, we + # amend the AST such that the Tuple is replaced by the macro function call itself. + expr = self.__parse_value() # type: ignore + if expr and not wrapped and isinstance(seq_get(expr.expressions, 0), MacroFunc): + return expr.expressions[0] + + return expr + + def _parse_macro_or_clause(self: Parser, parser: t.Callable) -> t.Optional[exp.Expression]: return _parse_macro(self) if self._match(TokenType.PARAMETER) else parser() @@ -1063,6 +1077,7 @@ def extend_sqlglot() -> None: _override(Parser, _parse_with) _override(Parser, _parse_having) _override(Parser, _parse_limit) + _override(Parser, _parse_value) _override(Parser, _parse_lambda) _override(Parser, _parse_types) _override(Parser, _parse_if) diff --git a/tests/core/test_macros.py b/tests/core/test_macros.py index c235430a69..f1beeeb3b5 100644 --- a/tests/core/test_macros.py +++ b/tests/core/test_macros.py @@ -575,6 +575,26 @@ def test_ast_correctness(macro_evaluator): "SELECT 3", {}, ), + ( + "SELECT * FROM (VALUES @EACH([1, 2, 3], v -> (v)) ) AS v", + "SELECT * FROM (VALUES (1), (2), (3)) AS v", + {}, + ), + ( + "SELECT * FROM (VALUES (@EACH([1, 2, 3], v -> (v))) ) AS v", + "SELECT * FROM (VALUES ((1), (2), (3))) AS v", + {}, + ), + ( + "SELECT * FROM (VALUES @EACH([1, 2, 3], v -> (v, @EVAL(@v + 1))) ) AS v", + "SELECT * FROM (VALUES (1, 2), (2, 3), (3, 4)) AS v", + {}, + ), + ( + "SELECT * FROM (VALUES (@EACH([1, 2, 3], v -> (v, @EVAL(@v + 1)))) ) AS v", + "SELECT * FROM (VALUES ((1, 2), (2, 3), (3, 4))) AS v", + {}, + ), ], ) def test_macro_functions(macro_evaluator: MacroEvaluator, assert_exp_eq, sql, expected, args): From 18017c468d287e939dbb2b31a6269ad9632ce315 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 16 Jul 2025 21:16:43 +0300 Subject: [PATCH 0585/1056] Fix: respect env. var for leading comma & normalize format settings (#4978) --- sqlmesh/cli/options.py | 2 ++ tests/cli/test_cli.py | 72 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/sqlmesh/cli/options.py b/sqlmesh/cli/options.py index 5f2ca034d8..2e4642eb0e 100644 --- a/sqlmesh/cli/options.py +++ b/sqlmesh/cli/options.py @@ -63,6 +63,7 @@ def format_options(func: t.Callable) -> t.Callable: "--normalize", is_flag=True, help="Whether or not to normalize identifiers to lowercase.", + default=None, )(func) func = click.option( "--pad", @@ -82,6 +83,7 @@ def format_options(func: t.Callable) -> t.Callable: func = click.option( "--leading-comma", is_flag=True, + default=None, help="Determines whether or not the comma is leading or trailing in select expressions. Default is trailing.", )(func) func = click.option( diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 6f0d1ac089..d1f792dc28 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1,5 +1,6 @@ import json import logging +import os import pytest import string import time_machine @@ -2190,3 +2191,74 @@ def none_ready(batch): assert_plan_success(result) assert "Checking signals" not in result.output + + +@pytest.mark.isolated +@time_machine.travel(FREEZE_TIME) +def test_format_leading_comma_default(runner: CliRunner, tmp_path: Path): + """Test that format command respects leading_comma environment variable.""" + create_example_project(tmp_path, template=ProjectTemplate.EMPTY) + + # Create a SQL file with trailing comma format + test_sql = tmp_path / "models" / "test_format.sql" + test_sql.write_text("""MODEL ( + name sqlmesh_example.test_format, + kind FULL +); + +SELECT + col1, + col2, + col3 +FROM table1""") + + # Test 1: Default behavior (no env var set) - should not change the file + result = runner.invoke(cli, ["--paths", str(tmp_path), "format", "--check"]) + assert result.exit_code == 0 + + # Test 2: Set env var to true - should require reformatting to leading comma + os.environ["SQLMESH__FORMAT__LEADING_COMMA"] = "true" + try: + result = runner.invoke(cli, ["--paths", str(tmp_path), "format", "--check"]) + # Should exit with 1 because formatting is needed + assert result.exit_code == 1 + + # Actually format the file + result = runner.invoke(cli, ["--paths", str(tmp_path), "format"]) + assert result.exit_code == 0 + + # Check that the file now has leading commas + formatted_content = test_sql.read_text() + assert ", col2" in formatted_content + assert ", col3" in formatted_content + + # Now check should pass + result = runner.invoke(cli, ["--paths", str(tmp_path), "format", "--check"]) + assert result.exit_code == 0 + finally: + # Clean up env var + del os.environ["SQLMESH__FORMAT__LEADING_COMMA"] + + # Test 3: Explicit command line flag overrides env var + os.environ["SQLMESH__FORMAT__LEADING_COMMA"] = "false" + try: + # Write file with leading commas + test_sql.write_text("""MODEL ( + name sqlmesh_example.test_format, + kind FULL +); + +SELECT + col1 + , col2 + , col3 +FROM table1""") + + # Check with --leading-comma flag (should pass) + result = runner.invoke( + cli, + ["--paths", str(tmp_path), "format", "--check", "--leading-comma"], + ) + assert result.exit_code == 0 + finally: + del os.environ["SQLMESH__FORMAT__LEADING_COMMA"] From 2b702ba0baa1a92df9c729576df4f2fa003eefe2 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Thu, 17 Jul 2025 18:33:41 +1200 Subject: [PATCH 0586/1056] Fix(duckdb): Only SET connector_config values on cursor init if they are different (#4981) --- sqlmesh/core/config/connection.py | 27 ++++++++++++--- .../integration/test_integration_duckdb.py | 33 +++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 49d49e40e7..415e916365 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -348,11 +348,28 @@ def init(cursor: duckdb.DuckDBPyConnection) -> None: except Exception as e: raise ConfigError(f"Failed to load extension {extension['name']}: {e}") - for field, setting in self.connector_config.items(): - try: - cursor.execute(f"SET {field} = '{setting}'") - except Exception as e: - raise ConfigError(f"Failed to set connector config {field} to {setting}: {e}") + if self.connector_config: + option_names = list(self.connector_config) + in_part = ",".join("?" for _ in range(len(option_names))) + + cursor.execute( + f"SELECT name, value FROM duckdb_settings() WHERE name IN ({in_part})", + option_names, + ) + + existing_values = {field: setting for field, setting in cursor.fetchall()} + + # only set connector_config items if the values differ from what is already set + # trying to set options like 'temp_directory' even to the same value can throw errors like: + # Not implemented Error: Cannot switch temporary directory after the current one has been used + for field, setting in self.connector_config.items(): + if existing_values.get(field) != setting: + try: + cursor.execute(f"SET {field} = '{setting}'") + except Exception as e: + raise ConfigError( + f"Failed to set connector config {field} to {setting}: {e}" + ) if self.secrets: duckdb_version = duckdb.__version__ diff --git a/tests/core/engine_adapter/integration/test_integration_duckdb.py b/tests/core/engine_adapter/integration/test_integration_duckdb.py index ce7fb1b80d..a53c559a55 100644 --- a/tests/core/engine_adapter/integration/test_integration_duckdb.py +++ b/tests/core/engine_adapter/integration/test_integration_duckdb.py @@ -106,3 +106,36 @@ def _open_connection() -> bool: for future in as_completed(futures): assert future.result() + + +def test_connector_config_from_multiple_connections(tmp_path: Path): + config = DuckDBConnectionConfig( + concurrent_tasks=2, + extensions=["tpch"], + connector_config={"temp_directory": str(tmp_path), "memory_limit": "16mb"}, + ) + + adapter = config.create_engine_adapter() + pool = adapter._connection_pool + + assert isinstance(pool, ThreadLocalSharedConnectionPool) + + adapter.execute("CALL dbgen(sf = 0.1)") + + # check that temporary files exist so that calling "SET temp_directory = 'anything'" will throw an error + assert len(adapter.fetchall("select path from duckdb_temporary_files()")) > 0 + + def _open_connection() -> bool: + # This triggers cursor_init() which should only SET values if they have changed + pool.get_cursor() + return True + + thread_pool = ThreadPoolExecutor(max_workers=4) + futures = [] + for _ in range(4): + futures.append(thread_pool.submit(_open_connection)) + + for future in as_completed(futures): + assert future.result() + + pool.close_all() From 9a954d73e985825ae2965752ca2e3c16d5be1ea9 Mon Sep 17 00:00:00 2001 From: etonlels Date: Thu, 17 Jul 2025 04:51:36 -0600 Subject: [PATCH 0587/1056] Fix: Create engine_adapters from all projects (#4977) --- sqlmesh/core/context.py | 15 ++++++++------ tests/core/test_integration.py | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index a2662ad0c2..1ba241f69f 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -2649,13 +2649,16 @@ def cache_dir(self) -> Path: @cached_property def engine_adapters(self) -> t.Dict[str, EngineAdapter]: - """Returns all the engine adapters for the gateways defined in the configuration.""" + """Returns all the engine adapters for the gateways defined in the configurations.""" adapters: t.Dict[str, EngineAdapter] = {self.selected_gateway: self.engine_adapter} - for gateway_name in self.config.gateways: - if gateway_name != self.selected_gateway: - connection = self.config.get_connection(gateway_name) - adapter = connection.create_engine_adapter(concurrent_tasks=self.concurrent_tasks) - adapters[gateway_name] = adapter + for config in self.configs.values(): + for gateway_name in config.gateways: + if gateway_name not in adapters: + connection = config.get_connection(gateway_name) + adapter = connection.create_engine_adapter( + concurrent_tasks=self.concurrent_tasks, + ) + adapters[gateway_name] = adapter return adapters @cached_property diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index a3a54eb7b6..0717ba11aa 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -7,6 +7,7 @@ from unittest import mock from unittest.mock import patch import logging +from textwrap import dedent import os import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 @@ -7079,3 +7080,38 @@ def test_scd_type_2_regular_run_with_offset(init_and_plan_context: t.Callable): assert restated_data.iloc[1]["region"] == "ANZ" assert str(restated_data.iloc[1]["valid_from"]) == "2023-01-09 07:26:00" assert pd.isna(restated_data.iloc[1]["valid_to"]) + + +def test_engine_adapters_multi_repo_all_gateways_gathered(copy_to_temp_path): + paths = copy_to_temp_path("examples/multi") + repo_1_path = paths[0] / "repo_1" + repo_2_path = paths[0] / "repo_2" + + # Add an extra gateway to repo_2's config + repo_2_config_path = repo_2_path / "config.yaml" + config_content = repo_2_config_path.read_text() + + modified_config = config_content.replace( + "default_gateway: local", + dedent(""" + extra: + connection: + type: duckdb + database: extra.duckdb + + default_gateway: local + """), + ) + + repo_2_config_path.write_text(modified_config) + + # Create context with both repos but using the repo_1 path first + context = Context( + paths=(repo_1_path, repo_2_path), + gateway="memory", + ) + + # Verify all gateways from both repos are present + gathered_gateways = context.engine_adapters.keys() + expected_gateways = {"local", "memory", "extra"} + assert gathered_gateways == expected_gateways From a274ac7387b701635ba48861d1c648380bb6e5b7 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 17 Jul 2025 14:02:50 -0700 Subject: [PATCH 0588/1056] fix!: correctly identify keys to make deterministic (#4984) --- sqlmesh/core/model/common.py | 6 +- .../migrations/v0085_deterministic_repr.py | 35 ++--- .../v0086_check_deterministic_bug.py | 82 ++++++++++ sqlmesh/utils/metaprogramming.py | 46 ++---- tests/core/test_model.py | 25 ++- tests/utils/test_metaprogramming.py | 144 +++++++----------- 6 files changed, 186 insertions(+), 152 deletions(-) create mode 100644 sqlmesh/migrations/v0086_check_deterministic_bug.py diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index f35a08a28b..704f3e02fe 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -153,14 +153,16 @@ def _add_variables_to_python_env( variables = {k: v for k, v in (variables or {}).items() if k in used_variables} if variables: - python_env[c.SQLMESH_VARS] = Executable.value(variables) + python_env[c.SQLMESH_VARS] = Executable.value(variables, sort_root_dict=True) if blueprint_variables: blueprint_variables = { k: SqlValue(sql=v.sql(dialect=dialect)) if isinstance(v, exp.Expression) else v for k, v in blueprint_variables.items() } - python_env[c.SQLMESH_BLUEPRINT_VARS] = Executable.value(blueprint_variables) + python_env[c.SQLMESH_BLUEPRINT_VARS] = Executable.value( + blueprint_variables, sort_root_dict=True + ) return python_env diff --git a/sqlmesh/migrations/v0085_deterministic_repr.py b/sqlmesh/migrations/v0085_deterministic_repr.py index 9364926068..4c86969843 100644 --- a/sqlmesh/migrations/v0085_deterministic_repr.py +++ b/sqlmesh/migrations/v0085_deterministic_repr.py @@ -4,6 +4,7 @@ """ import json +import logging import typing as t from dataclasses import dataclass @@ -12,6 +13,12 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type +logger = logging.getLogger(__name__) + + +KEYS_TO_MAKE_DETERMINISTIC = ["__sqlmesh__vars__", "__sqlmesh__blueprint__vars__"] + + # Make sure `SqlValue` is defined so it can be used by `eval` call in the migration @dataclass class SqlValue: @@ -20,25 +27,13 @@ class SqlValue: sql: str -def _deterministic_repr(obj: t.Any) -> str: - """ - This is a copy of the function from utils.metaprogramming - """ - - def _normalize_for_repr(o: t.Any) -> t.Any: - if isinstance(o, dict): - sorted_items = sorted(o.items(), key=lambda x: str(x[0])) - return {k: _normalize_for_repr(v) for k, v in sorted_items} - if isinstance(o, (list, tuple)): - # Recursively normalize nested structures - normalized = [_normalize_for_repr(item) for item in o] - return type(o)(normalized) - return o - +def _dict_sort(obj: t.Any) -> str: try: - return repr(_normalize_for_repr(obj)) + if isinstance(obj, dict): + obj = dict(sorted(obj.items(), key=lambda x: str(x[0]))) except Exception: - return repr(obj) + logger.warning("Failed to sort non-recursive dict", exc_info=True) + return repr(obj) def migrate(state_sync, **kwargs): # type: ignore @@ -82,12 +77,14 @@ def migrate(state_sync, **kwargs): # type: ignore if python_env: for key, executable in python_env.items(): + if key not in KEYS_TO_MAKE_DETERMINISTIC: + continue if isinstance(executable, dict) and executable.get("kind") == "value": old_payload = executable["payload"] try: # Try to parse the old payload and re-serialize it deterministically parsed_value = eval(old_payload) - new_payload = _deterministic_repr(parsed_value) + new_payload = _dict_sort(parsed_value) # Only update if the representation changed if old_payload != new_payload: @@ -95,7 +92,7 @@ def migrate(state_sync, **kwargs): # type: ignore migration_needed = True except Exception: # If we still can't eval it, leave it as-is - pass + logger.warning("Exception trying to eval payload", exc_info=True) new_snapshots.append( { diff --git a/sqlmesh/migrations/v0086_check_deterministic_bug.py b/sqlmesh/migrations/v0086_check_deterministic_bug.py new file mode 100644 index 0000000000..17527e81ce --- /dev/null +++ b/sqlmesh/migrations/v0086_check_deterministic_bug.py @@ -0,0 +1,82 @@ +import json +import logging + +from sqlglot import exp + +from sqlmesh.core.console import get_console + + +logger = logging.getLogger(__name__) +KEYS_TO_MAKE_DETERMINISTIC = ["__sqlmesh__vars__", "__sqlmesh__blueprint__vars__"] + + +def migrate(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + versions_table = "_versions" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + versions_table = f"{schema}.{versions_table}" + + result = engine_adapter.fetchone( + exp.select("schema_version").from_(versions_table), quote_identifiers=True + ) + if not result: + # This must be the first migration, so we can skip the check since the project was not exposed to 85 migration bug + return + schema_version = result[0] + if schema_version < 85: + # The project was not exposed to the bugged 85 migration, so we can skip it. + return + + warning = ( + "SQLMesh detected that it may not be able to fully migrate the state database. This should not impact " + "the migration process, but may result in unexpected changes being reported by the next `sqlmesh plan` " + "command. Please run `sqlmesh diff prod` after the migration has completed, before making any new " + "changes. If any unexpected changes are reported, consider running a forward-only plan to apply these " + "changes and avoid unnecessary backfills: sqlmesh plan prod --forward-only. " + "See https://sqlmesh.readthedocs.io/en/stable/concepts/plans/#forward-only-plans for more details.\n" + ) + + for ( + name, + identifier, + version, + snapshot, + kind_name, + updated_ts, + unpaused_ts, + ttl_ms, + unrestorable, + ) in engine_adapter.fetchall( + exp.select( + "name", + "identifier", + "version", + "snapshot", + "kind_name", + "updated_ts", + "unpaused_ts", + "ttl_ms", + "unrestorable", + ).from_(snapshots_table), + quote_identifiers=True, + ): + parsed_snapshot = json.loads(snapshot) + python_env = parsed_snapshot["node"].get("python_env") + + if python_env: + for key, executable in python_env.items(): + if ( + key not in KEYS_TO_MAKE_DETERMINISTIC + and isinstance(executable, dict) + and executable.get("kind") == "value" + ): + try: + parsed_value = eval(executable["payload"]) + if isinstance(parsed_value, dict): + get_console().log_warning(warning) + return + except Exception: + logger.warning("Exception trying to eval payload", exc_info=True) diff --git a/sqlmesh/utils/metaprogramming.py b/sqlmesh/utils/metaprogramming.py index c4340c0321..9330532442 100644 --- a/sqlmesh/utils/metaprogramming.py +++ b/sqlmesh/utils/metaprogramming.py @@ -5,6 +5,7 @@ import importlib import inspect import linecache +import logging import os import re import sys @@ -23,6 +24,9 @@ from sqlmesh.utils.errors import SQLMeshError from sqlmesh.utils.pydantic import PydanticModel +logger = logging.getLogger(__name__) + + IGNORE_DECORATORS = {"macro", "model", "signal"} SERIALIZABLE_CALLABLES = (type, types.FunctionType) LITERALS = (Number, str, bytes, tuple, list, dict, set, bool) @@ -424,10 +428,11 @@ def is_value(self) -> bool: return self.kind == ExecutableKind.VALUE @classmethod - def value(cls, v: t.Any, is_metadata: t.Optional[bool] = None) -> Executable: - return Executable( - payload=_deterministic_repr(v), kind=ExecutableKind.VALUE, is_metadata=is_metadata - ) + def value( + cls, v: t.Any, is_metadata: t.Optional[bool] = None, sort_root_dict: bool = False + ) -> Executable: + payload = _dict_sort(v) if sort_root_dict else repr(v) + return Executable(payload=payload, kind=ExecutableKind.VALUE, is_metadata=is_metadata) def serialize_env(env: t.Dict[str, t.Any], path: Path) -> t.Dict[str, Executable]: @@ -635,36 +640,13 @@ def print_exception( out.write(tb) -def _deterministic_repr(obj: t.Any) -> str: - """Create a deterministic representation by ensuring consistent ordering before repr(). - - For dictionaries, ensures consistent key ordering to prevent non-deterministic - serialization that affects fingerprinting. Uses Python's native repr() logic - for all formatting to handle edge cases properly. - - Note that this function assumes list/tuple order is significant and therefore does not sort them. - - Args: - obj: The object to represent as a string. - - Returns: - A deterministic string representation of the object. - """ - - def _normalize_for_repr(o: t.Any) -> t.Any: - if isinstance(o, dict): - sorted_items = sorted(o.items(), key=lambda x: str(x[0])) - return {k: _normalize_for_repr(v) for k, v in sorted_items} - if isinstance(o, (list, tuple)): - # Recursively normalize nested structures - normalized = [_normalize_for_repr(item) for item in o] - return type(o)(normalized) - return o - +def _dict_sort(obj: t.Any) -> str: try: - return repr(_normalize_for_repr(obj)) + if isinstance(obj, dict): + obj = dict(sorted(obj.items(), key=lambda x: str(x[0]))) except Exception: - return repr(obj) + logger.warning("Failed to sort non-recursive dict", exc_info=True) + return repr(obj) def import_python_file(path: Path, relative_base: Path = Path()) -> types.ModuleType: diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 909a79e143..ce58a0f00b 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -6116,7 +6116,8 @@ def test_named_variable_macros() -> None: ) assert model.python_env[c.SQLMESH_VARS] == Executable.value( - {c.GATEWAY: "in_memory", "test_var_a": "test_value", "overridden_var": "initial_value"} + {c.GATEWAY: "in_memory", "test_var_a": "test_value", "overridden_var": "initial_value"}, + sort_root_dict=True, ) assert ( model.render_query_or_raise().sql() @@ -6142,7 +6143,8 @@ def test_variables_in_templates() -> None: ) assert model.python_env[c.SQLMESH_VARS] == Executable.value( - {c.GATEWAY: "in_memory", "test_var_a": "test_value", "overridden_var": "initial_value"} + {c.GATEWAY: "in_memory", "test_var_a": "test_value", "overridden_var": "initial_value"}, + sort_root_dict=True, ) assert ( model.render_query_or_raise().sql() @@ -6166,7 +6168,8 @@ def test_variables_in_templates() -> None: ) assert model.python_env[c.SQLMESH_VARS] == Executable.value( - {c.GATEWAY: "in_memory", "test_var_a": "test_value", "overridden_var": "initial_value"} + {c.GATEWAY: "in_memory", "test_var_a": "test_value", "overridden_var": "initial_value"}, + sort_root_dict=True, ) assert ( model.render_query_or_raise().sql() @@ -6305,7 +6308,8 @@ def test_variables_migrated_dbt_package_macro(): dialect="bigquery", ) assert model.python_env[c.SQLMESH_VARS] == Executable.value( - {"test_var_a": "test_var_a_value", "__dbt_packages__.test.test_var_b": "test_var_b_value"} + {"test_var_a": "test_var_a_value", "__dbt_packages__.test.test_var_b": "test_var_b_value"}, + sort_root_dict=True, ) assert ( model.render_query().sql(dialect="bigquery") @@ -6530,7 +6534,8 @@ def test_unrendered_macros_sql_model(mocker: MockerFixture) -> None: "physical_var": "bla", "virtual_var": "blb", "session_var": "blc", - } + }, + sort_root_dict=True, ) assert "location1" in model.physical_properties @@ -6617,7 +6622,8 @@ def model_with_macros(evaluator, **kwargs): "physical_var": "bla", "virtual_var": "blb", "session_var": "blc", - } + }, + sort_root_dict=True, ) assert python_sql_model.enabled @@ -10576,9 +10582,12 @@ def unimportant_testing_macro(evaluator, *projections): ) assert m.python_env.get(c.SQLMESH_VARS) == Executable.value( - {"selector": "bla", "bla_variable": 1, "baz_variable": 2} + {"selector": "bla", "bla_variable": 1, "baz_variable": 2}, + sort_root_dict=True, + ) + assert m.python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value( + {"selector": "baz"}, sort_root_dict=True ) - assert m.python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value({"selector": "baz"}) def test_extract_schema_in_post_statement(tmp_path: Path) -> None: diff --git a/tests/utils/test_metaprogramming.py b/tests/utils/test_metaprogramming.py index cb8421fac8..8519e1eb04 100644 --- a/tests/utils/test_metaprogramming.py +++ b/tests/utils/test_metaprogramming.py @@ -22,7 +22,7 @@ from sqlmesh.utils.metaprogramming import ( Executable, ExecutableKind, - _deterministic_repr, + _dict_sort, build_env, func_globals, normalize_source, @@ -460,39 +460,39 @@ def test_serialize_env_with_enum_import_appearing_in_two_functions() -> None: assert serialized_env == expected_env -def test_deterministic_repr_basic_types(): - """Test _deterministic_repr with basic Python types.""" +def test_dict_sort_basic_types(): + """Test dict_sort with basic Python types.""" # Test basic types that should use standard repr - assert _deterministic_repr(42) == "42" - assert _deterministic_repr("hello") == "'hello'" - assert _deterministic_repr(True) == "True" - assert _deterministic_repr(None) == "None" - assert _deterministic_repr(3.14) == "3.14" + assert _dict_sort(42) == "42" + assert _dict_sort("hello") == "'hello'" + assert _dict_sort(True) == "True" + assert _dict_sort(None) == "None" + assert _dict_sort(3.14) == "3.14" -def test_deterministic_repr_dict_ordering(): - """Test that _deterministic_repr produces consistent output for dicts with different key ordering.""" +def test_dict_sort_dict_ordering(): + """Test that dict_sort produces consistent output for dicts with different key ordering.""" # Same dict with different key ordering dict1 = {"c": 3, "a": 1, "b": 2} dict2 = {"a": 1, "b": 2, "c": 3} dict3 = {"b": 2, "c": 3, "a": 1} - repr1 = _deterministic_repr(dict1) - repr2 = _deterministic_repr(dict2) - repr3 = _deterministic_repr(dict3) + repr1 = _dict_sort(dict1) + repr2 = _dict_sort(dict2) + repr3 = _dict_sort(dict3) # All should produce the same representation assert repr1 == repr2 == repr3 assert repr1 == "{'a': 1, 'b': 2, 'c': 3}" -def test_deterministic_repr_mixed_key_types(): - """Test _deterministic_repr with mixed key types (strings and numbers).""" +def test_dict_sort_mixed_key_types(): + """Test dict_sort with mixed key types (strings and numbers).""" dict1 = {42: "number", "string": "text", 1: "one"} dict2 = {"string": "text", 1: "one", 42: "number"} - repr1 = _deterministic_repr(dict1) - repr2 = _deterministic_repr(dict2) + repr1 = _dict_sort(dict1) + repr2 = _dict_sort(dict2) # Should produce consistent ordering despite mixed key types assert repr1 == repr2 @@ -500,45 +500,47 @@ def test_deterministic_repr_mixed_key_types(): assert repr1 == "{1: 'one', 42: 'number', 'string': 'text'}" -def test_deterministic_repr_nested_structures(): - """Test _deterministic_repr with deeply nested dictionaries.""" +def test_dict_sort_nested_structures(): + """Test dict_sort with deeply nested dictionaries.""" nested1 = {"outer": {"z": 26, "a": 1}, "list": [3, {"y": 2, "x": 1}], "simple": "value"} nested2 = {"simple": "value", "list": [3, {"x": 1, "y": 2}], "outer": {"a": 1, "z": 26}} - repr1 = _deterministic_repr(nested1) - repr2 = _deterministic_repr(nested2) + repr1 = _dict_sort(nested1) + repr2 = _dict_sort(nested2) - assert repr1 == repr2 + assert repr1 != repr2 # Verify structure is maintained with sorted keys - expected = "{'list': [3, {'x': 1, 'y': 2}], 'outer': {'a': 1, 'z': 26}, 'simple': 'value'}" - assert repr1 == expected - - -def test_deterministic_repr_lists_and_tuples(): - """Test _deterministic_repr preserves order for lists/tuples but sorts nested dicts.""" - # Lists should maintain their order - list_with_dicts = [{"b": 2, "a": 1}, {"d": 4, "c": 3}] - list_repr = _deterministic_repr(list_with_dicts) - expected_list = "[{'a': 1, 'b': 2}, {'c': 3, 'd': 4}]" + expected1 = "{'list': [3, {'y': 2, 'x': 1}], 'outer': {'z': 26, 'a': 1}, 'simple': 'value'}" + expected2 = "{'list': [3, {'x': 1, 'y': 2}], 'outer': {'a': 1, 'z': 26}, 'simple': 'value'}" + assert repr1 == expected1 + assert repr2 == expected2 + + +def test_dict_sort_lists_and_tuples(): + """Test dict_sort preserves order for lists/tuples and doesn't sort nested dicts.""" + # Lists should be unchanged + list_with_dicts = [{"z": 26, "a": 1}, {"y": 25, "b": 2}] + list_repr = _dict_sort(list_with_dicts) + expected_list = "[{'z': 26, 'a': 1}, {'y': 25, 'b': 2}]" assert list_repr == expected_list - # Tuples should maintain their order + # Tuples should be unchanged tuple_with_dicts = ({"z": 26, "a": 1}, {"y": 25, "b": 2}) - tuple_repr = _deterministic_repr(tuple_with_dicts) - expected_tuple = "({'a': 1, 'z': 26}, {'b': 2, 'y': 25})" + tuple_repr = _dict_sort(tuple_with_dicts) + expected_tuple = "({'z': 26, 'a': 1}, {'y': 25, 'b': 2})" assert tuple_repr == expected_tuple -def test_deterministic_repr_empty_containers(): - """Test _deterministic_repr with empty containers.""" - assert _deterministic_repr({}) == "{}" - assert _deterministic_repr([]) == "[]" - assert _deterministic_repr(()) == "()" +def test_dict_sort_empty_containers(): + """Test dict_sort with empty containers.""" + assert _dict_sort({}) == "{}" + assert _dict_sort([]) == "[]" + assert _dict_sort(()) == "()" -def test_deterministic_repr_special_characters(): - """Test _deterministic_repr handles special characters correctly.""" +def test_dict_sort_special_characters(): + """Test dict_sort handles special characters correctly.""" special_dict = { "quotes": "text with 'single' and \"double\" quotes", "unicode": "unicode: ñáéíóú", @@ -546,25 +548,25 @@ def test_deterministic_repr_special_characters(): "backslashes": "path\\to\\file", } - result = _deterministic_repr(special_dict) + result = _dict_sort(special_dict) # Should be valid Python that can be evaluated reconstructed = eval(result) assert reconstructed == special_dict # Should be deterministic - same input produces same output - result2 = _deterministic_repr(special_dict) + result2 = _dict_sort(special_dict) assert result == result2 -def test_deterministic_repr_executable_integration(): - """Test that _deterministic_repr works correctly with Executable.value().""" +def test_dict_sort_executable_integration(): + """Test that dict_sort works correctly with Executable.value().""" # Test the integration with Executable.value which is the main use case variables1 = {"env": "dev", "debug": True, "timeout": 30} variables2 = {"timeout": 30, "debug": True, "env": "dev"} - exec1 = Executable.value(variables1) - exec2 = Executable.value(variables2) + exec1 = Executable.value(variables1, sort_root_dict=True) + exec2 = Executable.value(variables2, sort_root_dict=True) # Should produce identical payloads despite different input ordering assert exec1.payload == exec2.payload @@ -574,46 +576,6 @@ def test_deterministic_repr_executable_integration(): reconstructed = eval(exec1.payload) assert reconstructed == variables1 - -def test_deterministic_repr_complex_example(): - """Test _deterministic_repr with a complex real-world-like structure.""" - complex_vars = { - "database_config": { - "host": "localhost", - "port": 5432, - "credentials": {"username": "admin", "password": "secret"}, - }, - "feature_flags": ["flag_b", "flag_a"], - "metadata": { - "version": "1.0.0", - "environment": "production", - "tags": {"team": "data", "project": "analytics"}, - }, - 42: "numeric_key", - "arrays": [{"config": {"nested": True, "level": 2}}, {"simple": "value"}], - } - - expected_structure = { - 42: "numeric_key", - "arrays": [{"config": {"level": 2, "nested": True}}, {"simple": "value"}], - "database_config": { - "credentials": {"password": "secret", "username": "admin"}, - "host": "localhost", - "port": 5432, - }, - "feature_flags": ["flag_b", "flag_a"], - "metadata": { - "environment": "production", - "tags": {"project": "analytics", "team": "data"}, - "version": "1.0.0", - }, - } - - actual_repr = _deterministic_repr(complex_vars) - expected_repr = repr(expected_structure) - assert actual_repr == expected_repr - - # Should be valid Python - reconstructed = eval(actual_repr) - assert isinstance(reconstructed, dict) - assert reconstructed == complex_vars + # non-deterministic repr should not change the payload + exec3 = Executable.value(variables1) + assert exec3.payload == "{'env': 'dev', 'debug': True, 'timeout': 30}" From 960bacb6462bb4bb6aca1ceca86ee006c174f2b8 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:01:54 -0700 Subject: [PATCH 0589/1056] fix: add missing sort root dict (#4986) --- sqlmesh/core/model/definition.py | 2 +- tests/core/test_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index f6c83c85f7..e900e2fc25 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2361,7 +2361,7 @@ def create_python_model( used_variables = {k: v for k, v in (variables or {}).items() if k in referenced_variables} if used_variables: - python_env[c.SQLMESH_VARS] = Executable.value(used_variables) + python_env[c.SQLMESH_VARS] = Executable.value(used_variables, sort_root_dict=True) return _create_model( PythonModel, diff --git a/tests/core/test_model.py b/tests/core/test_model.py index ce58a0f00b..b13a5797cc 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -6715,7 +6715,7 @@ def model_with_named_variables( ) assert python_model.python_env[c.SQLMESH_VARS] == Executable.value( - {"test_var_a": "test_value", "start": "2024-01-01"} + {"test_var_a": "test_value", "start": "2024-01-01"}, sort_root_dict=True ) context = ExecutionContext(mocker.Mock(), {}, None, None) From d97c705af97ddd8c6eef902af2864fc45f788cef Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:20:59 +0300 Subject: [PATCH 0590/1056] Chore: use paren syntax in signals docs (#4989) --- docs/guides/signals.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guides/signals.md b/docs/guides/signals.md index acbe7031f4..36dc25496f 100644 --- a/docs/guides/signals.md +++ b/docs/guides/signals.md @@ -63,9 +63,9 @@ This example specifies that the `random_signal()` should evaluate once with a th MODEL ( name example.signal_model, kind FULL, - signals [ + signals ( random_signal(threshold := 0.5), # specify threshold value - ] + ) ); SELECT 1 @@ -108,9 +108,9 @@ MODEL ( time_column ds, ), start '2 week ago', - signals [ + signals ( one_week_ago(), - ] + ) ); From 87f533566a1f93bba713a9f22447381ce6d92a6b Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:37:07 +0300 Subject: [PATCH 0591/1056] Chore!: bump sqlglot to v27.1.0 (#4990) --- pyproject.toml | 2 +- tests/core/test_macros.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 204a1c7f3d..3ea3935c1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.0.0", + "sqlglot[rs]~=27.1.0", "tenacity", "time-machine", "json-stream" diff --git a/tests/core/test_macros.py b/tests/core/test_macros.py index f1beeeb3b5..0e3615d6c0 100644 --- a/tests/core/test_macros.py +++ b/tests/core/test_macros.py @@ -876,12 +876,7 @@ def test_date_spine(assert_exp_eq, dialect, date_part): # Generate the expected SQL based on the dialect and date_part if dialect == "duckdb": - if date_part == "week": - interval = "(7 * INTERVAL '1' DAY)" - elif date_part == "quarter": - interval = "(90 * INTERVAL '1' DAY)" - else: - interval = f"INTERVAL '1' {date_part.upper()}" + interval = f"INTERVAL '1' {date_part.upper()}" expected_sql = f""" SELECT date_{date_part} From 06fd6f1e969c3df56ce782eac21331f92af360e2 Mon Sep 17 00:00:00 2001 From: Sung Won Chung Date: Mon, 21 Jul 2025 09:33:05 -0700 Subject: [PATCH 0592/1056] Docs Top-Level Navbar (#4994) --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index aa4db57cb4..0ffbcde316 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -161,6 +161,7 @@ theme: - navigation.expand ## expands navigation bar by default - navigation.tracking - navigation.tabs + - navigation.tabs.sticky - navigation.sections - navigation.top - toc.follow From de07778cc18311c6e2a4bc521bf94651c2c2071e Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 22 Jul 2025 11:08:45 +1200 Subject: [PATCH 0593/1056] Chore: Allow supplying a Context to GithubController (#4992) --- sqlmesh/integrations/github/cicd/controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 29de4658d3..48ec7ee32e 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -290,6 +290,7 @@ def __init__( config: t.Optional[t.Union[Config, str]] = None, event: t.Optional[GithubEvent] = None, client: t.Optional[Github] = None, + context: t.Optional[Context] = None, ) -> None: from github import Github @@ -331,7 +332,7 @@ def __init__( if review.state.lower() == "approved" } logger.debug(f"Approvers: {', '.join(self._approvers)}") - self._context: Context = Context(paths=self._paths, config=self.config) + self._context: Context = context or Context(paths=self._paths, config=self.config) # Bot config needs the context to be initialized logger.debug(f"Bot config: {self.bot_config.json(indent=2)}") From 0e8fbdb905fd559a9c7b753bb3671abcba604673 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:31:21 +0300 Subject: [PATCH 0594/1056] Chore: Add cron in the fields to render for python models (#4993) --- docs/concepts/models/python_models.md | 29 +++++ sqlmesh/core/model/definition.py | 18 ++- tests/core/test_model.py | 177 +++++++++++++++++++++++++- 3 files changed, 219 insertions(+), 5 deletions(-) diff --git a/docs/concepts/models/python_models.md b/docs/concepts/models/python_models.md index 53495d11aa..63796987a8 100644 --- a/docs/concepts/models/python_models.md +++ b/docs/concepts/models/python_models.md @@ -394,6 +394,35 @@ It's also possible to use the `@EACH` macro, combined with a global list variabl ... ``` +## Using macros in model properties + +Python models support macro variables in model properties. However, special care must be taken when the macro variable appears within a string. + +For example when using macro variables inside cron expressions, you need to wrap the entire expression in quotes and prefix it with `@` to ensure proper parsing: + +```python linenums="1" +# Correct: Wrap the cron expression containing a macro variable +@model( + "my_model", + cron="@'*/@{mins} * * * *'", # Note the @'...' syntax + ... +) + +# This also works with blueprint variables +@model( + "@{customer}.scheduled_model", + cron="@'0 @{hour} * * *'", + blueprints=[ + {"customer": "customer_1", "hour": 2}, # Runs at 2 AM + {"customer": "customer_2", "hour": 8}, # Runs at 8 AM + ], + ... +) + +``` + +This is necessary because cron expressions often use `@` for aliases (like `@daily`, `@hourly`), which can conflict with SQLMesh's macro syntax. + ## Examples ### Basic The following is an example of a Python model returning a static Pandas DataFrame. diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index e900e2fc25..25dd013f4d 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -74,8 +74,6 @@ logger = logging.getLogger(__name__) -UNRENDERABLE_MODEL_FIELDS = {"cron", "description"} - PROPERTIES = {"physical_properties", "session_properties", "virtual_properties"} RUNTIME_RENDERED_MODEL_FIELDS = { @@ -84,6 +82,16 @@ "merge_filter", } | PROPERTIES +CRON_SHORTCUTS = { + "@midnight", + "@hourly", + "@daily", + "@weekly", + "@monthly", + "@yearly", + "@annually", +} + class _Model(ModelMeta, frozen=True): """Model is the core abstraction for user defined datasets. @@ -2771,7 +2779,11 @@ def render_field_value(value: t.Any) -> t.Any: field_value = fields.get(field) # We don't want to parse python model cron="@..." kwargs (e.g. @daily) into MacroVar - if field == "cron" or field_value is None: + if ( + field == "cron" + and isinstance(field_value, str) + and field_value.lower() in CRON_SHORTCUTS + ) or field_value is None: continue if field in RUNTIME_RENDERED_MODEL_FIELDS: diff --git a/tests/core/test_model.py b/tests/core/test_model.py index b13a5797cc..575d9038ae 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -4263,6 +4263,7 @@ def python_model_prop(context, **kwargs): def test_model_defaults_macros(make_snapshot): model_defaults = ModelDefaultsConfig( table_format="@IF(@gateway = 'dev', 'iceberg', NULL)", + cron="@cron_macro", storage_format="@IF(@gateway = 'local', 'parquet', NULL)", optimize_query="@IF(@gateway = 'dev', True, False)", enabled="@IF(@gateway = 'dev', True, False)", @@ -4298,7 +4299,7 @@ def test_model_defaults_macros(make_snapshot): default_dialect="snowflake", ), defaults=model_defaults.dict(), - variables={"gateway": "dev", "create_type": "SECURE"}, + variables={"gateway": "dev", "create_type": "SECURE", "cron_macro": "@daily"}, ) snapshot: Snapshot = make_snapshot(model) @@ -4311,6 +4312,7 @@ def test_model_defaults_macros(make_snapshot): assert not model.allow_partials assert model.interval_unit == IntervalUnit.DAY assert model.table_format == "iceberg" + assert model.cron == "@daily" # Validate disabling of conditional model default assert not model.storage_format @@ -4363,6 +4365,7 @@ def test_model_defaults_macros_python_model(make_snapshot): "partition_expiration_days": 13, "creatable_type": "@IF(@model_kind_name = 'FULL', 'TRANSIENT', NULL)", }, + "cron": "@cron_macro_expr", "table_format": "@IF(@gateway = 'local', 'iceberg', NULL)", "storage_format": "@IF(@gateway = 'dev', 'parquet', NULL)", "optimize_query": "@IF(@gateway = 'local', True, False)", @@ -4391,13 +4394,14 @@ def python_model_prop_macro(context, **kwargs): path=Path("."), dialect="duckdb", defaults=model_defaults, - variables={"gateway": "local", "create_type": "SECURE"}, + variables={"gateway": "local", "create_type": "SECURE", "cron_macro_expr": "0 */2 * * *"}, ) # Even if in the project wide defaults this is ignored for python models assert not m.optimize_query # Validate rendering of model defaults + assert m.cron == "0 */2 * * *" assert m.enabled assert m.start == "2024-01-01" assert m.allow_partials @@ -6379,6 +6383,7 @@ def test_macros_python_model(mocker: MockerFixture) -> None: columns={"a": "string"}, kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="@{time_col}"), stamp="@{stamp}", + cron="@some_cron_var", owner="@IF(@gateway = 'dev', @{dev_owner}, @{prod_owner})", enabled="@IF(@gateway = 'dev', True, False)", start="@IF(@gateway = 'dev', '1 month ago', '2024-01-01')", @@ -6406,6 +6411,7 @@ def model_with_macros(context, **kwargs): "prod_owner": "pr_1", "stamp": "bump", "time_col": "a", + "some_cron_var": "@daily", }, ) @@ -6417,6 +6423,7 @@ def model_with_macros(context, **kwargs): assert python_model.stamp == "bump" assert python_model.time_column.column == exp.column("a", quoted=True) assert python_model.partitioned_by[0].sql() == 'DATETIME_TRUNC("a", MONTH)' + assert python_model.cron == "@daily" context = ExecutionContext(mocker.Mock(), {}, None, None) df = list(python_model.render(context=context))[0] @@ -10747,3 +10754,169 @@ def test_datetime_without_timezone_variable_redshift() -> None: model.render_query_or_raise().sql("redshift") == '''SELECT CAST('1970-01-01 00:00:00' AS TIMESTAMP) AS "test_time_col"''' ) + + +def test_python_model_cron_with_blueprints(tmp_path: Path) -> None: + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) + + cron_blueprint_model = tmp_path / "models" / "cron_blueprint.py" + cron_blueprint_model.parent.mkdir(parents=True, exist_ok=True) + cron_blueprint_model.write_text( + """ +import typing as t +from datetime import datetime + +import pandas as pd +from sqlmesh import ExecutionContext, model + +@model( + "@{customer}.some_table", + kind="FULL", + cron="@'*/@{min} * * * *'", + blueprints=[ + {"customer": "customer1", "field_a": "x", "field_b": "y", "min": 5}, + {"customer": "customer2", "field_a": "z", "field_b": "w", "min": 10}, + ], + columns={ + "field_a": "text", + "field_b": "text", + "customer": "text", + }, + enabled=True +) +def entrypoint( + context: ExecutionContext, + start: datetime, + end: datetime, + execution_time: datetime, + **kwargs: t.Any, +) -> pd.DataFrame: + return pd.DataFrame( + { + "field_a": [context.blueprint_var("field_a")], + "field_b": [context.blueprint_var("field_b")], + "customer": [context.blueprint_var("customer")], + } + ) +""" + ) + + context = Context( + paths=tmp_path, config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ) + models = context.models + + # Test first blueprint + customer1_model = models.get('"memory"."customer1"."some_table"') + assert customer1_model is not None + assert customer1_model.cron == "*/5 * * * *" + assert customer1_model.enabled + assert "blueprints" not in customer1_model.all_fields() + assert customer1_model.python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value( + {"customer": "customer1", "field_a": "x", "field_b": "y", "min": 5} + ) + + # Test second blueprint + customer2_model = models.get('"memory"."customer2"."some_table"') + assert customer2_model is not None + assert customer2_model.cron == "*/10 * * * *" + assert customer2_model.python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value( + {"customer": "customer2", "field_a": "z", "field_b": "w", "min": 10} + ) + + # Test that the models can be planned and applied + context.plan(no_prompts=True, auto_apply=True, no_diff=True) + + # Verify the data + assert context.fetchdf('from "memory"."customer1"."some_table"').to_dict() == { + "field_a": {0: "x"}, + "field_b": {0: "y"}, + "customer": {0: "customer1"}, + } + assert context.fetchdf('from "memory"."customer2"."some_table"').to_dict() == { + "field_a": {0: "z"}, + "field_b": {0: "w"}, + "customer": {0: "customer2"}, + } + + +def test_python_model_cron_macro_rendering(tmp_path: Path) -> None: + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) + + cron_macro_model = tmp_path / "models" / "cron_macro.py" + cron_macro_model.parent.mkdir(parents=True, exist_ok=True) + cron_macro_model.write_text( + """ +import pandas as pd +from sqlmesh import model + +@model( + "msc.test_cron_model", + kind="FULL", + cron="@{cron_schedule}", + columns={"a": "int"}, +) +def entrypoint(context, **kwargs): + return pd.DataFrame([{"a": 1}]) +""" + ) + + # Test with cron alias + context_daily = Context( + paths=tmp_path, + config=Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + variables={"cron_schedule": "@daily"}, + ), + ) + model_daily = context_daily.models.get('"memory"."msc"."test_cron_model"') + + assert model_daily is not None + assert model_daily.cron == "@daily" + + # Test with cron expression + context_expr = Context( + paths=tmp_path, + config=Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + variables={"cron_schedule": "0 */2 * * *"}, + ), + ) + model_expr = context_expr.models.get('"memory"."msc"."test_cron_model"') + assert model_expr is not None + assert model_expr.cron == "0 */2 * * *" + + +def test_python_model_normal_cron(tmp_path: Path) -> None: + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) + + cron_macro_model = tmp_path / "models" / "cron_macro.py" + cron_macro_model.parent.mkdir(parents=True, exist_ok=True) + cron_macro_model.write_text( + """ +import pandas as pd +from sqlmesh import model + +@model( + "msc.normal_test_cron_model", + kind="FULL", + cron="@daily", + columns={"a": "int"}, +) +def entrypoint(context, **kwargs): + return pd.DataFrame([{"a": 1}]) +""" + ) + + # Test with cron alias + context_daily = Context( + paths=tmp_path, + config=Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + variables={"cron_schedule": "@daily"}, + ), + ) + model_daily = context_daily.models.get('"memory"."msc"."normal_test_cron_model"') + + assert model_daily is not None + assert model_daily.cron == "@daily" From 886598f4e63ae109e9c4f16b9db5141b8ece61a6 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:00:50 +0100 Subject: [PATCH 0595/1056] chore: prevent bad imports from lsp module elsewhere (#5000) --- pyproject.toml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3ea3935c1c..180b86e93b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -280,9 +280,20 @@ select = [ ] extend-select = ["TID"] + [tool.ruff.lint.flake8-tidy-imports] banned-module-level-imports = [ "duckdb", "numpy", "pandas", ] + +# Bans imports from sqlmesh.lsp in files outside of sqlmesh/lsp +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"sqlmesh.lsp".msg = "Only files within sqlmesh/lsp can import from sqlmesh.lsp" + +[tool.ruff.lint.per-file-ignores] +# TID251 is used to ignore the import of sqlmesh.lsp in files outside sqlmesh/lsp +"sqlmesh/lsp/**/*.py" = ["TID251"] +"tests/lsp/**/*.py" = ["TID251"] +"benchmarks/lsp*.py" = ["TID251"] From 5c9b3602907dd7fdd28876303cce21e9af6ef36c Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 24 Jul 2025 00:01:08 +0300 Subject: [PATCH 0596/1056] Fix: Properly pass test variables in render for python tests (#5002) --- sqlmesh/core/test/definition.py | 2 +- tests/core/test_test.py | 100 ++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index 762585bd9f..c0e2f7a08e 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -799,7 +799,7 @@ def _execute_model(self) -> pd.DataFrame: with self._concurrent_render_context(): variables = self.body.get("vars", {}).copy() time_kwargs = {key: variables.pop(key) for key in TIME_KWARG_KEYS if key in variables} - df = next(self.model.render(context=self.context, **time_kwargs, **variables)) + df = next(self.model.render(context=self.context, variables=variables, **time_kwargs)) assert not isinstance(df, exp.Expression) return df if isinstance(df, pd.DataFrame) else df.toPandas() diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 9eddf2f524..31fe0f3495 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -3227,3 +3227,103 @@ def execute( test.context.resolve_table("silver.sushi.bar") _check_successful_or_raise(test.run()) + + +def test_python_model_test_variables_override(tmp_path: Path) -> None: + py_model = tmp_path / "models" / "test_var_model.py" + py_model.parent.mkdir(parents=True, exist_ok=True) + py_model.write_text( + """ +import pandas as pd # noqa: TID253 +from sqlmesh import model, ExecutionContext +import typing as t + +@model( + name="test_var_model", + columns={"id": "int", "flag_value": "boolean", "var_value": "varchar"}, +) +def execute(context: ExecutionContext, **kwargs: t.Any) -> pd.DataFrame: + my_flag = context.var("my_flag") + other_var = context.var("other_var") + + return pd.DataFrame([{ + "id": 1 if my_flag else 2, + "flag_value": my_flag, + "var_value": other_var, + }])""" + ) + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + variables={"my_flag": False, "other_var": "default_value"}, + ) + context = Context(config=config, paths=tmp_path) + + python_model = context.models['"test_var_model"'] + + # Test when Flag is True + # Overriding the config default flag_value to True + # AND the var_value to use test one + test_flag_true = _create_test( + body=load_yaml(""" +test_flag_true: + model: test_var_model + vars: + my_flag: true + other_var: "test_value" + outputs: + query: + rows: + - id: 1 + flag_value: true + var_value: "test_value" + """), + test_name="test_flag_true", + model=python_model, + context=context, + ) + + _check_successful_or_raise(test_flag_true.run()) + + # Test when Flag is False + # Overriding the config default flag_value to False + # AND the var_value to use test one (since the above would be false for both) + test_flag_false = _create_test( + body=load_yaml(""" +test_flag_false: + model: test_var_model + vars: + my_flag: false + other_var: "another_test_value" + outputs: + query: + rows: + - id: 2 + flag_value: false + var_value: "another_test_value" + """), + test_name="test_flag_false", + model=python_model, + context=context, + ) + + _check_successful_or_raise(test_flag_false.run()) + + # Test with no vars specified + # (should use config defaults for both flag and var_value) + test_default_vars = _create_test( + body=load_yaml(""" +test_default_vars: + model: test_var_model + outputs: + query: + rows: + - id: 2 + flag_value: false + var_value: "default_value" + """), + test_name="test_default_vars", + model=python_model, + context=context, + ) + _check_successful_or_raise(test_default_vars.run()) From e53b1ee8c3bd59a8d79566ab0298cf9c1d4cd04b Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:27:35 +0100 Subject: [PATCH 0597/1056] chore: update node dependencies (#4999) --- .circleci/continue_config.yml | 2 +- pnpm-lock.yaml | 2002 ++++++++++++++++----------------- vscode/extension/package.json | 12 +- vscode/react/package.json | 34 +- web/client/package.json | 26 +- 5 files changed, 1025 insertions(+), 1051 deletions(-) diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 34bdf0e98b..b93caf482e 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -172,7 +172,7 @@ jobs: ui_test: docker: - - image: mcr.microsoft.com/playwright:v1.53.2-jammy + - image: mcr.microsoft.com/playwright:v1.54.1-jammy resource_class: medium steps: - halt_unless_client diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d18077d1d3..a94b230fc1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,15 +36,15 @@ importers: specifier: ^9.0.1 version: 9.0.1 zod: - specifier: ^3.25.71 - version: 3.25.71 + specifier: ^3.25.76 + version: 3.25.76 devDependencies: '@eslint/js': - specifier: ^9.30.1 - version: 9.30.1 + specifier: ^9.31.0 + version: 9.31.0 '@playwright/test': - specifier: ^1.53.2 - version: 1.53.2 + specifier: ^1.54.1 + version: 1.54.1 '@types/mocha': specifier: ^10.0.10 version: 10.0.10 @@ -67,20 +67,20 @@ importers: specifier: ^3.6.0 version: 3.6.0 esbuild: - specifier: ^0.25.5 - version: 0.25.5 + specifier: ^0.25.8 + version: 0.25.8 eslint: - specifier: ^9.30.1 - version: 9.30.1(jiti@2.4.2) + specifier: ^9.31.0 + version: 9.31.0(jiti@2.4.2) ts-loader: specifier: ^9.5.2 - version: 9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.5)) + version: 9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.8)) typescript: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.35.1 - version: 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.38.0 + version: 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) vitest: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) @@ -91,8 +91,8 @@ importers: vscode/react: dependencies: '@headlessui/react': - specifier: ^2.2.4 - version: 2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^2.2.5 + version: 2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': specifier: ^2.2.0 version: 2.2.0(react@18.3.1) @@ -104,22 +104,22 @@ importers: version: 4.1.11 '@tailwindcss/vite': specifier: ^4.1.11 - version: 4.1.11(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@tanstack/react-query': - specifier: ^5.81.5 - version: 5.81.5(react@18.3.1) + specifier: ^5.83.0 + version: 5.83.0(react@18.3.1) '@tanstack/react-router': - specifier: ^1.124.0 - version: 1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.129.8 + version: 1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-router-devtools': - specifier: ^1.124.0 - version: 1.124.0(@tanstack/react-router@1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.124.0)(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) + 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))(@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-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.124.0 - version: 1.124.0(@tanstack/react-router@1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.5)) + 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.43.1)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.8)) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -139,8 +139,8 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-router: - specifier: ^7.6.3 - version: 7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.7.0 + version: 7.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) reactflow: specifier: ^11.11.4 version: 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -153,22 +153,22 @@ importers: devDependencies: '@chromatic-com/storybook': specifier: ^4.0.1 - version: 4.0.1(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) + version: 4.0.1(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2)) '@storybook/addon-a11y': - specifier: ^9.0.15 - version: 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) + specifier: ^9.0.18 + version: 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2)) '@storybook/addon-docs': - specifier: ^9.0.15 - version: 9.0.15(@types/react@18.3.23)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) + specifier: ^9.0.18 + version: 9.0.18(@types/react@18.3.23)(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2)) '@storybook/addon-onboarding': - specifier: ^9.0.15 - version: 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) + specifier: ^9.0.18 + version: 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2)) '@storybook/addon-vitest': - specifier: ^9.0.15 - version: 9.0.15(@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.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(vitest@3.2.4) + 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.0)(prettier@3.6.2))(vitest@3.2.4) '@storybook/react-vite': - specifier: ^9.0.15 - version: 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.44.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + 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.0)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -182,11 +182,11 @@ importers: specifier: ^18.3.7 version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react': - specifier: ^4.6.0 - version: 4.6.0(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + 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.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/browser': specifier: 3.2.3 - version: 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.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) @@ -194,20 +194,20 @@ importers: specifier: ^26.1.0 version: 26.1.0 playwright: - specifier: ^1.53.2 - version: 1.53.2 + specifier: ^1.54.1 + version: 1.54.1 storybook: - specifier: ^9.0.15 - version: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) + specifier: ^9.0.18 + version: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) typescript: specifier: ^5.8.3 version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(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)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -236,17 +236,17 @@ importers: specifier: ^6.5.2 version: 6.5.2 '@codemirror/view': - specifier: ^6.38.0 - version: 6.38.0 + specifier: ^6.38.1 + version: 6.38.1 '@headlessui/react': - specifier: ^2.2.4 - version: 2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^2.2.5 + version: 2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': specifier: ^2.2.0 version: 2.2.0(react@18.3.1) '@lit/react': - specifier: ^1.0.7 - version: 1.0.7(@types/react@18.3.23) + specifier: ^1.0.8 + version: 1.0.8(@types/react@18.3.23) '@radix-ui/react-context-menu': specifier: ^2.2.15 version: 2.2.15(@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) @@ -257,8 +257,8 @@ importers: specifier: ^0.1.1 version: 0.1.1(tailwindcss@3.4.17) '@tanstack/react-query': - specifier: ^5.81.5 - version: 5.81.5(react@18.3.1) + specifier: ^5.83.0 + version: 5.83.0(react@18.3.1) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -269,8 +269,8 @@ importers: specifier: ^2.4.1 version: 2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@uiw/react-codemirror': - specifier: ^4.23.14 - version: 4.23.14(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.0)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^4.24.1 + version: 4.24.1(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -291,7 +291,7 @@ importers: version: 18.3.1 react-dnd: specifier: ^16.0.1 - version: 16.0.1(@types/node@24.0.10)(@types/react@18.3.23)(react@18.3.1) + version: 16.0.1(@types/node@24.1.0)(@types/react@18.3.23)(react@18.3.1) react-dnd-html5-backend: specifier: ^16.0.1 version: 16.0.1 @@ -302,8 +302,8 @@ importers: specifier: ^10.1.0 version: 10.1.0(@types/react@18.3.23)(react@18.3.1) react-router: - specifier: ^7.6.3 - version: 7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.7.0 + version: 7.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-split: specifier: ^2.0.14 version: 2.0.14(react@18.3.1) @@ -312,20 +312,20 @@ importers: version: 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) thememirror: specifier: ^2.0.1 - version: 2.0.1(@codemirror/language@6.11.2)(@codemirror/state@6.5.2)(@codemirror/view@6.38.0) + version: 2.0.1(@codemirror/language@6.11.2)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1) zustand: specifier: ^5.0.6 version: 5.0.6(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) devDependencies: '@eslint/js': - specifier: ^9.30.1 - version: 9.30.1 + specifier: ^9.31.0 + version: 9.31.0 '@playwright/test': - specifier: ^1.53.2 - version: 1.53.2 + specifier: ^1.54.1 + version: 1.54.1 '@swc/core': - specifier: ^1.12.9 - version: 1.12.9(@swc/helpers@0.5.17) + specifier: ^1.13.2 + version: 1.13.2(@swc/helpers@0.5.17) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -345,8 +345,8 @@ importers: specifier: ^18.3.7 version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react-swc': - specifier: ^3.10.2 - version: 3.10.2(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + 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.43.1)(tsx@4.20.3)(yaml@2.8.0)) ajv: specifier: ^8.17.1 version: 8.17.1 @@ -354,8 +354,8 @@ importers: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) eslint: - specifier: ^9.30.1 - version: 9.30.1(jiti@2.4.2) + specifier: ^9.31.0 + version: 9.31.0(jiti@2.4.2) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -372,21 +372,21 @@ importers: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.35.1 - version: 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.38.0 + version: 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) vite-plugin-css-injected-by-js: specifier: ^3.5.2 - version: 3.5.2(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(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)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: '@swc/core-linux-x64-gnu': - specifier: ^1.12.9 - version: 1.12.9 + specifier: ^1.13.2 + version: 1.13.2 packages: @@ -433,44 +433,44 @@ packages: resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} engines: {node: '>=18.0.0'} - '@azure/core-auth@1.9.0': - resolution: {integrity: sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==} - engines: {node: '>=18.0.0'} + '@azure/core-auth@1.10.0': + resolution: {integrity: sha512-88Djs5vBvGbHQHf5ZZcaoNHo6Y8BKZkt3cw2iuJIQzLEgH4Ox6Tm4hjFhbqOxyYsgIG/eJbFEHpxRIfEEWv5Ow==} + engines: {node: '>=20.0.0'} - '@azure/core-client@1.9.4': - resolution: {integrity: sha512-f7IxTD15Qdux30s2qFARH+JxgwxWLG2Rlr4oSkPGuLWm+1p5y1+C04XGLA0vmX6EtqfutmjvpNmAfgwVIS5hpw==} - engines: {node: '>=18.0.0'} + '@azure/core-client@1.10.0': + resolution: {integrity: sha512-O4aP3CLFNodg8eTHXECaH3B3CjicfzkxVtnrfLkOq0XNP7TIECGfHpK/C6vADZkWP75wzmdBnsIA8ksuJMk18g==} + engines: {node: '>=20.0.0'} - '@azure/core-rest-pipeline@1.21.0': - resolution: {integrity: sha512-a4MBwe/5WKbq9MIxikzgxLBbruC5qlkFYlBdI7Ev50Y7ib5Vo/Jvt5jnJo7NaWeJ908LCHL0S1Us4UMf1VoTfg==} - engines: {node: '>=18.0.0'} + '@azure/core-rest-pipeline@1.22.0': + resolution: {integrity: sha512-OKHmb3/Kpm06HypvB3g6Q3zJuvyXcpxDpCS1PnU8OV6AJgSFaee/covXBcPbWc6XDDxtEPlbi3EMQ6nUiPaQtw==} + engines: {node: '>=20.0.0'} - '@azure/core-tracing@1.2.0': - resolution: {integrity: sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==} - engines: {node: '>=18.0.0'} + '@azure/core-tracing@1.3.0': + resolution: {integrity: sha512-+XvmZLLWPe67WXNZo9Oc9CrPj/Tm8QnHR92fFAFdnbzwNdCH1h+7UdpaQgRSBsMY+oW1kHXNUZQLdZ1gHX3ROw==} + engines: {node: '>=20.0.0'} - '@azure/core-util@1.12.0': - resolution: {integrity: sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ==} - engines: {node: '>=18.0.0'} + '@azure/core-util@1.13.0': + resolution: {integrity: sha512-o0psW8QWQ58fq3i24Q1K2XfS/jYTxr7O1HRcyUE9bV9NttLU+kYOH82Ixj8DGlMTOWgxm1Sss2QAfKK5UkSPxw==} + engines: {node: '>=20.0.0'} '@azure/identity@4.10.2': resolution: {integrity: sha512-Uth4vz0j+fkXCkbvutChUj03PDCokjbC6Wk9JT8hHEUtpy/EurNKAseb3+gO6Zi9VYBvwt61pgbzn1ovk942Qg==} engines: {node: '>=20.0.0'} - '@azure/logger@1.2.0': - resolution: {integrity: sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA==} - engines: {node: '>=18.0.0'} + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} - '@azure/msal-browser@4.14.0': - resolution: {integrity: sha512-6VB06LypBS0Cf/dSUwRZse/eGnfAHwDof7GpCfoo3JjnruSN40jFBw+QXZd1ox5OLC6633EdWRRz+TGeHMEspg==} + '@azure/msal-browser@4.15.0': + resolution: {integrity: sha512-+AIGTvpVz+FIx5CsM1y+nW0r/qOb/ChRdM8/Cbp+jKWC0Wdw4ldnwPdYOBi5NaALUQnYITirD9XMZX7LdklEzQ==} engines: {node: '>=0.8.0'} - '@azure/msal-common@15.8.0': - resolution: {integrity: sha512-gYqq9MsWT/KZh8iTG37DkGv+wgfllgImTMB++Z83qn75M5eZ0cMX5kSSXdJqHbFm1qxaYydv+2kiVyA9ksN9pA==} + '@azure/msal-common@15.8.1': + resolution: {integrity: sha512-ltIlFK5VxeJ5BurE25OsJIfcx1Q3H/IZg2LjV9d4vmH+5t4c1UCyRQ/HgKLgXuCZShs7qfc/TC95GYZfsUsJUQ==} engines: {node: '>=0.8.0'} - '@azure/msal-node@3.6.2': - resolution: {integrity: sha512-lfZtncCSmKvW31Bh3iUBkeTf+Myt85YsamMkGNZ0ayTO5MirOGBgTa3BgUth0kWFBQuhZIRfi5B95INZ+ppkjw==} + '@azure/msal-node@3.6.3': + resolution: {integrity: sha512-95wjsKGyUcAd5tFmQBo5Ug/kOj+hFh/8FsXuxluEvdfbgg6xCimhSP9qnyq6+xIg78/jREkBD1/BSqd7NIDDYQ==} engines: {node: '>=16'} '@babel/code-frame@7.27.1': @@ -614,8 +614,8 @@ packages: resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.0': - resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} + '@babel/types@7.28.1': + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -661,8 +661,8 @@ packages: '@codemirror/theme-one-dark@6.1.2': resolution: {integrity: sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==} - '@codemirror/view@6.38.0': - resolution: {integrity: sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg==} + '@codemirror/view@6.38.1': + resolution: {integrity: sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==} '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} @@ -692,152 +692,158 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@esbuild/aix-ppc64@0.25.5': - resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.5': - resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.5': - resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.5': - resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.5': - resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.5': - resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.5': - resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.5': - resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.5': - resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.5': - resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.5': - resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.5': - resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.5': - resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.5': - resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.5': - resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.5': - resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.5': - resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.5': - resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.5': - resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.5': - resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.5': - resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.25.5': - resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.5': - resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.5': - resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.5': - resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -860,10 +866,6 @@ packages: resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.14.0': - resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.15.1': resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -872,16 +874,16 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.30.1': - resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==} + '@eslint/js@9.31.0': + resolution: {integrity: sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.3': - resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} + '@eslint/plugin-kit@0.3.4': + resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@exodus/schemasafe@1.3.0': @@ -908,11 +910,11 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@gerrit0/mini-shiki@3.7.0': - resolution: {integrity: sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==} + '@gerrit0/mini-shiki@3.8.1': + resolution: {integrity: sha512-HVZW+8pxoOExr5ZMPK15U79jQAZTO/S6i5byQyyZGjtNj+qaYd82cizTncwFzTQgiLo8uUBym6vh+/1tfJklTw==} - '@headlessui/react@2.2.4': - resolution: {integrity: sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==} + '@headlessui/react@2.2.5': + resolution: {integrity: sha512-h1+2Vu1yR5pp/fBcTnwVEW8Kb94Hbxp7MXZLORfDzvSrbmGgiTyaTZ4LI/tPNZnK8eDrYD9s9cMbjm5HS5otIQ==} engines: {node: '>=10'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -1029,8 +1031,8 @@ packages: '@lezer/python@1.1.18': resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} - '@lit/react@1.0.7': - resolution: {integrity: sha512-cencnwwLXQKiKxjfFzSgZRngcWJzUDZi/04E0fSaF86wZgchMdvTyu+lE36DrUfvuus3bH8+xLPrhM1cTjwpzw==} + '@lit/react@1.0.8': + resolution: {integrity: sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw==} peerDependencies: '@types/react': 17 || 18 || 19 @@ -1092,8 +1094,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.53.2': - resolution: {integrity: sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==} + '@playwright/test@1.54.1': + resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==} engines: {node: '>=18'} hasBin: true @@ -1404,26 +1406,26 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@react-aria/focus@3.20.5': - resolution: {integrity: sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==} + '@react-aria/focus@3.21.0': + resolution: {integrity: sha512-7NEGtTPsBy52EZ/ToVKCu0HSelE3kq9qeis+2eEq90XSuJOMaDHUQrA7RC2Y89tlEwQB31bud/kKRi9Qme1dkA==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-aria/interactions@3.25.3': - resolution: {integrity: sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==} + '@react-aria/interactions@3.25.4': + resolution: {integrity: sha512-HBQMxgUPHrW8V63u9uGgBymkMfj6vdWbB0GgUJY49K9mBKMsypcHeWkWM6+bF7kxRO728/IK8bWDV6whDbqjHg==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-aria/ssr@3.9.9': - resolution: {integrity: sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==} + '@react-aria/ssr@3.9.10': + resolution: {integrity: sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==} engines: {node: '>= 12'} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-aria/utils@3.29.1': - resolution: {integrity: sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==} + '@react-aria/utils@3.30.0': + resolution: {integrity: sha512-ydA6y5G1+gbem3Va2nczj/0G0W7/jUVo/cbN10WA5IizzWIwMP5qhFr7macgbKfHMkZ+YZC3oXnt2NNre5odKw==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -1440,13 +1442,13 @@ packages: '@react-stately/flags@3.1.2': resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==} - '@react-stately/utils@3.10.7': - resolution: {integrity: sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==} + '@react-stately/utils@3.10.8': + resolution: {integrity: sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-types/shared@3.30.0': - resolution: {integrity: sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==} + '@react-types/shared@3.31.0': + resolution: {integrity: sha512-ua5U6V66gDcbLZe4P2QeyNgPp4YWD1ymGA6j3n+s8CGExtrCPe64v+g4mvpT8Bnb985R96e4zFT61+m0YCwqMg==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -1486,11 +1488,8 @@ packages: react: '>=17' react-dom: '>=17' - '@rolldown/pluginutils@1.0.0-beta.11': - resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==} - - '@rolldown/pluginutils@1.0.0-beta.19': - resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} '@rollup/pluginutils@5.2.0': resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} @@ -1501,162 +1500,162 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.44.1': - resolution: {integrity: sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==} + '@rollup/rollup-android-arm-eabi@4.45.1': + resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.44.1': - resolution: {integrity: sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==} + '@rollup/rollup-android-arm64@4.45.1': + resolution: {integrity: sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.44.1': - resolution: {integrity: sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==} + '@rollup/rollup-darwin-arm64@4.45.1': + resolution: {integrity: sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.44.1': - resolution: {integrity: sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==} + '@rollup/rollup-darwin-x64@4.45.1': + resolution: {integrity: sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.44.1': - resolution: {integrity: sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==} + '@rollup/rollup-freebsd-arm64@4.45.1': + resolution: {integrity: sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.44.1': - resolution: {integrity: sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==} + '@rollup/rollup-freebsd-x64@4.45.1': + resolution: {integrity: sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.44.1': - resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.44.1': - resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==} + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.44.1': - resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==} + '@rollup/rollup-linux-arm64-gnu@4.45.1': + resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.44.1': - resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==} + '@rollup/rollup-linux-arm64-musl@4.45.1': + resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.44.1': - resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==} + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': - resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==} + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.44.1': - resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==} + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.44.1': - resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==} + '@rollup/rollup-linux-riscv64-musl@4.45.1': + resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.44.1': - resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==} + '@rollup/rollup-linux-s390x-gnu@4.45.1': + resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.44.1': - resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==} + '@rollup/rollup-linux-x64-gnu@4.45.1': + resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.44.1': - resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==} + '@rollup/rollup-linux-x64-musl@4.45.1': + resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.44.1': - resolution: {integrity: sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==} + '@rollup/rollup-win32-arm64-msvc@4.45.1': + resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.44.1': - resolution: {integrity: sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==} + '@rollup/rollup-win32-ia32-msvc@4.45.1': + resolution: {integrity: sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.44.1': - resolution: {integrity: sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==} + '@rollup/rollup-win32-x64-msvc@4.45.1': + resolution: {integrity: sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==} cpu: [x64] os: [win32] - '@secretlint/config-creator@10.1.1': - resolution: {integrity: sha512-TJ42CHZqqnEe9ORvIXVVMqdu3KAtyZRxLspjFexo6XgrwJ6CoFHQYzIihilqRjo2sJh9HMrpnYSj/5hopofGrA==} + '@secretlint/config-creator@10.2.1': + resolution: {integrity: sha512-nyuRy8uo2+mXPIRLJ93wizD1HbcdDIsVfgCT01p/zGVFrtvmiL7wqsl4KgZH0QFBM/KRLDLeog3/eaM5ASjtvw==} engines: {node: '>=20.0.0'} - '@secretlint/config-loader@10.1.1': - resolution: {integrity: sha512-jBClVFmS6Yu/zI5ejBCRF5a5ASYsE4gOjogjB+WsaHbQHtGvnyY7I26Qtdg4ihCc/VPKYQg0LdM75pLTXzwsjg==} + '@secretlint/config-loader@10.2.1': + resolution: {integrity: sha512-ob1PwhuSw/Hc6Y4TA63NWj6o++rZTRJOwPZG82o6tgEURqkrAN44fXH9GIouLsOxKa8fbCRLMeGmSBtJLdSqtw==} engines: {node: '>=20.0.0'} - '@secretlint/core@10.1.1': - resolution: {integrity: sha512-COLCxSoH/iVQdLeaZPVtBj0UWKOagO09SqYkCQgfFfZ+soGxKVK405dL317r4PnH9Pm8/s8xQC6OSY5rWTRObQ==} + '@secretlint/core@10.2.1': + resolution: {integrity: sha512-2sPp5IE7pM5Q+f1/NK6nJ49FKuqh+e3fZq5MVbtVjegiD4NMhjcoML1Cg7atCBgXPufhXRHY1DWhIhkGzOx/cw==} engines: {node: '>=20.0.0'} - '@secretlint/formatter@10.1.1': - resolution: {integrity: sha512-Gpd8gTPN121SJ0h/9e6nWlZU7PitfhXUiEzW7Kyswg6kNGs+bSqmgTgWFtbo1VQ4ygJYiveWPNT05RCImBexJw==} + '@secretlint/formatter@10.2.1': + resolution: {integrity: sha512-0A7ho3j0Y4ysK0mREB3O6FKQtScD4rQgfzuI4Slv9Cut1ynQOI7JXAoIFm4XVzhNcgtmEPeD3pQB206VFphBgQ==} engines: {node: '>=20.0.0'} - '@secretlint/node@10.1.1': - resolution: {integrity: sha512-AhN+IGqljVObm8a+B33b23FY79wihu5E61Nd3oYSoZV7SxUvMjpafqhLfpt4frNSY7Ghf/pirWu7JY7GMujFrA==} + '@secretlint/node@10.2.1': + resolution: {integrity: sha512-MQFte7C+5ZHINQGSo6+eUECcUCGvKR9PVgZcTsRj524xsbpeBqF1q1dHsUsdGb9r2jlvf40Q14MRZwMcpmLXWQ==} engines: {node: '>=20.0.0'} - '@secretlint/profiler@10.1.1': - resolution: {integrity: sha512-kReI+Wr7IQz0LbVwYByzlnPbx4BEF2oEWJBc4Oa45g24alCjHu+jD9h9mzkTJqYUgMnVYD3o7HfzeqxFrV+9XA==} + '@secretlint/profiler@10.2.1': + resolution: {integrity: sha512-gOlfPZ1ASc5mP5cqsL809uMJGp85t+AJZg1ZPscWvB/m5UFFgeNTZcOawggb1S5ExDvR388sIJxagx5hyDZ34g==} - '@secretlint/resolver@10.1.1': - resolution: {integrity: sha512-GdQzxnBtdBRjBULvZ8ERkaRqDp0njVwXrzBCav1pb0XshVk76C1cjeDqtTqM4RJ1Awo/g5U5MIWYztYv67v5Gg==} + '@secretlint/resolver@10.2.1': + resolution: {integrity: sha512-AuwehKwnE2uxKaJVv2Z5a8FzGezBmlNhtLKm70Cvsvtwd0oAtenxCSTKXkiPGYC0+S91fAw3lrX7CUkyr9cTCA==} - '@secretlint/secretlint-formatter-sarif@10.1.1': - resolution: {integrity: sha512-Dyq8nzy6domjSlZKX1E5PEzuWxeTqjQJWrlXBmVmOjwLBLfRZDlm5Vq+AduBmEk03KEIKIZi4cZQwsniuRPO9Q==} + '@secretlint/secretlint-formatter-sarif@10.2.1': + resolution: {integrity: sha512-qOZUYBesLkhCBP7YVMv0l1Pypt8e3V2rX2PT2Q5aJhJvKTcMiP9YTHG/3H9Zb7Gq3UIwZLEAGXRqJOu1XlE0Fg==} - '@secretlint/secretlint-rule-no-dotenv@10.1.1': - resolution: {integrity: sha512-a3/sOUUtEHuw1HCadtxUjViNeomiiohfJj+rwtHxJkCq4pjITS3HSYhQBXnNvkctQNljKIzFm7JUA/4QJ6I4sQ==} + '@secretlint/secretlint-rule-no-dotenv@10.2.1': + resolution: {integrity: sha512-XwPjc9Wwe2QljerfvGlBmLJAJVATLvoXXw1fnKyCDNgvY33cu1Z561Kxg93xfRB5LSep0S5hQrAfZRJw6x7MBQ==} engines: {node: '>=20.0.0'} - '@secretlint/secretlint-rule-preset-recommend@10.1.1': - resolution: {integrity: sha512-+GeISCXVgpnoeRZE4ZPsuO97+fm6z8Ge23LNq6LvR9ZJAq018maXVftkJhHj4hnvYB5URUAEerBBkPGNk5/Ong==} + '@secretlint/secretlint-rule-preset-recommend@10.2.1': + resolution: {integrity: sha512-/kj3UOpFbJt80dqoeEaUVv5nbeW1jPqPExA447FItthiybnaDse5C5HYcfNA2ywEInr399ELdcmpEMRe+ld1iQ==} engines: {node: '>=20.0.0'} - '@secretlint/source-creator@10.1.1': - resolution: {integrity: sha512-IWjvHcE0bhC/x88a9M9jbZlFRZGUEbBzujxrs2KzI5IQ2BXTBRBRhRSjE/BEpWqDHILB22c3mfam8X+UjukphA==} + '@secretlint/source-creator@10.2.1': + resolution: {integrity: sha512-1CgO+hsRx8KdA5R/LEMNTJkujjomwSQQVV0BcuKynpOefV/rRlIDVQJOU0tJOZdqUMC15oAAwQXs9tMwWLu4JQ==} engines: {node: '>=20.0.0'} - '@secretlint/types@10.1.1': - resolution: {integrity: sha512-/JGAvVkurVHkargk3AC7UxRy+Ymc+52AVBO/fZA5pShuLW2dX4O/rKc4n8cyhQiOb/3ym5ACSlLQuQ8apPfxrQ==} + '@secretlint/types@10.2.1': + resolution: {integrity: sha512-F5k1qpoMoUe7rrZossOBgJ3jWKv/FGDBZIwepqnefgPmNienBdInxhtZeXiGwjcxXHVhsdgp6I5Fi/M8PMgwcw==} engines: {node: '>=20.0.0'} - '@shikijs/engine-oniguruma@3.7.0': - resolution: {integrity: sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==} + '@shikijs/engine-oniguruma@3.8.1': + resolution: {integrity: sha512-KGQJZHlNY7c656qPFEQpIoqOuC4LrxjyNndRdzk5WKB/Ie87+NJCF1xo9KkOUxwxylk7rT6nhlZyTGTC4fCe1g==} - '@shikijs/langs@3.7.0': - resolution: {integrity: sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==} + '@shikijs/langs@3.8.1': + resolution: {integrity: sha512-TjOFg2Wp1w07oKnXjs0AUMb4kJvujML+fJ1C5cmEj45lhjbUXtziT1x2bPQb9Db6kmPhkG5NI2tgYW1/DzhUuQ==} - '@shikijs/themes@3.7.0': - resolution: {integrity: sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==} + '@shikijs/themes@3.8.1': + resolution: {integrity: sha512-Vu3t3BBLifc0GB0UPg2Pox1naTemrrvyZv2lkiSw3QayVV60me1ujFQwPZGgUTmwXl1yhCPW8Lieesm0CYruLQ==} - '@shikijs/types@3.7.0': - resolution: {integrity: sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==} + '@shikijs/types@3.8.1': + resolution: {integrity: sha512-5C39Q8/8r1I26suLh+5TPk1DTrbY/kn3IdWA5HdizR0FhlhD05zx5nKCqhzSfDHH3p4S0ZefxWd77DLV+8FhGg==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1738,27 +1737,27 @@ packages: resolution: {integrity: sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==} engines: {node: '>=10.8'} - '@storybook/addon-a11y@9.0.15': - resolution: {integrity: sha512-/oborGUeN7KT6jyTMhGRET9tXvZ080OCB/Hw6txSfsVxgZ4Z1QTJcOreejHGeYyxHN1ugEJ26K95agk4M13WZg==} + '@storybook/addon-a11y@9.0.18': + resolution: {integrity: sha512-msbsTI9TmePQ5ElVclLi7ns5WaAntouJFaj9ElNugFWME21k68RiyXnioDjDfEoi/+y8tthQNNqjsHoX/Ev0Og==} peerDependencies: - storybook: ^9.0.15 + storybook: ^9.0.18 - '@storybook/addon-docs@9.0.15': - resolution: {integrity: sha512-HOb45DkF23T1tRzakb9q33qnBRso15S/GM28ippPZWi5ZXR9RAyKVgOSMA/ViEpK4ezASxN+Tee+H7m4ksEFZw==} + '@storybook/addon-docs@9.0.18': + resolution: {integrity: sha512-1mLhaRDx8s1JAF51o56OmwMnIsg4BOQJ8cn+4wbMjh14pDFALrovlFl/BpAXnV1VaZqHjCB4ZWuP+y5CwXEpeQ==} peerDependencies: - storybook: ^9.0.15 + storybook: ^9.0.18 - '@storybook/addon-onboarding@9.0.15': - resolution: {integrity: sha512-g2FqO0aS6vvjMZdY+0xjV1C7YGcDE0GkuPAv1JqejNYGyX2Z8nuLHy2zqhLIBpfoap4S9PZO+obqEKVeo70Q0Q==} + '@storybook/addon-onboarding@9.0.18': + resolution: {integrity: sha512-A079BfJ3g3wYOtAuq9cPf2l6JHo+6UzEw1A2AbSNBBNP4hKfXpHcLadIVwuyOxuKjDUWzY5f4dJa3hCMurHXGQ==} peerDependencies: - storybook: ^9.0.15 + storybook: ^9.0.18 - '@storybook/addon-vitest@9.0.15': - resolution: {integrity: sha512-4TynzdZgJMsvneT5lZGp+WrUoFtp8+LRL3y35EepJa3GMBc+9WgsKQrso+xnDQh1gLvVNe46n3klZvunVr4AFA==} + '@storybook/addon-vitest@9.0.18': + resolution: {integrity: sha512-uPLh9H7kRho+raxyIBCm8Ymd3j0VPuWIQ1HSAkdx8itmNafNqs4HE67Z8Cfl259YzdWU/j5BhZqoiT62BCbIDw==} peerDependencies: '@vitest/browser': ^3.0.0 '@vitest/runner': ^3.0.0 - storybook: ^9.0.15 + storybook: ^9.0.18 vitest: ^3.0.0 peerDependenciesMeta: '@vitest/browser': @@ -1768,16 +1767,16 @@ packages: vitest: optional: true - '@storybook/builder-vite@9.0.15': - resolution: {integrity: sha512-ogPec1V+e3MgTY5DBlq/6hBBui0Y4TmolYQh0eL3cATHrwZlwkTTDWQfsOnMALd5w+4Jq8n0gk0cQgR5rh1FHw==} + '@storybook/builder-vite@9.0.18': + resolution: {integrity: sha512-lfbrozA6UPVizDrgbPEe04WMtxIraESwUkmwW3+Lxh8rKEUj5cXngcrJUW+meQNNaggdZZWEqeEtweuaLIR+Hg==} peerDependencies: - storybook: ^9.0.15 + storybook: ^9.0.18 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - '@storybook/csf-plugin@9.0.15': - resolution: {integrity: sha512-KszyGjrocMiNbkmpBGARF1ugLYMVaw1J8Z31kmwTHsMgMZwAKcOsofJ0fPgFno0yV59DUVkWxVBdPs9V0hhvxA==} + '@storybook/csf-plugin@9.0.18': + resolution: {integrity: sha512-MQ3WwXnMua5sX0uYyuO7dC5WOWuJCLqf8CsOn3zQ2ptNoH6hD7DFx5ZOa1uD6VxIuJ3LkA+YqfSRBncomJoRnA==} peerDependencies: - storybook: ^9.0.15 + storybook: ^9.0.18 '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} @@ -1789,96 +1788,96 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - '@storybook/react-dom-shim@9.0.15': - resolution: {integrity: sha512-X5VlYKoZSIMU9HEshIwtNzp41nPt4kiJtJ2c5HzFa5F6M8rEHM5n059CGcCZQqff3FnZtK/y6v/kCVZO+8oETA==} + '@storybook/react-dom-shim@9.0.18': + resolution: {integrity: sha512-qGR/d9x9qWRRxITaBVQkMnb73kwOm+N8fkbZRxc7U4lxupXRvkMIDh247nn71SYVBnvbh6//AL7P6ghiPWZYjA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.0.15 + storybook: ^9.0.18 - '@storybook/react-vite@9.0.15': - resolution: {integrity: sha512-OOAywn5x2Ged3LD84+TMwpjZUelFg7Wb8eHkgHE2SzM20XiZrhoKvreqxlzbfey3weBl+bKNhsiWF9BluT8YHg==} + '@storybook/react-vite@9.0.18': + resolution: {integrity: sha512-dHzUoeY0/S35TvSYxCkPuBlNQZx4Zj9QDhAZ0qdv+nSll++uPgqSe2y2vF+2p+XVYhjDn+YX5LORv00YtuQezg==} engines: {node: '>=20.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.0.15 + storybook: ^9.0.18 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - '@storybook/react@9.0.15': - resolution: {integrity: sha512-hewpSH8Ij4Bg7S9Tfw7ecfGPv7YDycRxsfpsDX7Mw3JhLuCdqjpmmTL2RgoNojg7TAW3FPdixcgQi/b4PH50ag==} + '@storybook/react@9.0.18': + resolution: {integrity: sha512-CCH6Vj/O6I07PrhCHxc1pvCWYMfZhRzK7CVHAtrBP9xxnYA7OoXhM2wymuDogml5HW1BKtyVMeQ3oWZXFNgDXQ==} engines: {node: '>=20.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.0.15 + storybook: ^9.0.18 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: optional: true - '@swc/core-darwin-arm64@1.12.9': - resolution: {integrity: sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==} + '@swc/core-darwin-arm64@1.13.2': + resolution: {integrity: sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.12.9': - resolution: {integrity: sha512-hv2kls7Ilkm2EpeJz+I9MCil7pGS3z55ZAgZfxklEuYsxpICycxeH+RNRv4EraggN44ms+FWCjtZFu0LGg2V3g==} + '@swc/core-darwin-x64@1.13.2': + resolution: {integrity: sha512-Lb9EZi7X2XDAVmuUlBm2UvVAgSCbD3qKqDCxSI4jEOddzVOpNCnyZ/xEampdngUIyDDhhJLYU9duC+Mcsv5Y+A==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.12.9': - resolution: {integrity: sha512-od9tDPiG+wMU9wKtd6y3nYJdNqgDOyLdgRRcrj1/hrbHoUPOM8wZQZdwQYGarw63iLXGgsw7t5HAF9Yc51ilFA==} + '@swc/core-linux-arm-gnueabihf@1.13.2': + resolution: {integrity: sha512-9TDe/92ee1x57x+0OqL1huG4BeljVx0nWW4QOOxp8CCK67Rpc/HHl2wciJ0Kl9Dxf2NvpNtkPvqj9+BUmM9WVA==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.12.9': - resolution: {integrity: sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==} + '@swc/core-linux-arm64-gnu@1.13.2': + resolution: {integrity: sha512-KJUSl56DBk7AWMAIEcU83zl5mg3vlQYhLELhjwRFkGFMvghQvdqQ3zFOYa4TexKA7noBZa3C8fb24rI5sw9Exg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.12.9': - resolution: {integrity: sha512-yghFZWKPVVGbUdqiD7ft23G0JX6YFGDJPz9YbLLAwGuKZ9th3/jlWoQDAw1Naci31LQhVC+oIji6ozihSuwB2A==} + '@swc/core-linux-arm64-musl@1.13.2': + resolution: {integrity: sha512-teU27iG1oyWpNh9CzcGQ48ClDRt/RCem7mYO7ehd2FY102UeTws2+OzLESS1TS1tEZipq/5xwx3FzbVgiolCiQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.12.9': - resolution: {integrity: sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==} + '@swc/core-linux-x64-gnu@1.13.2': + resolution: {integrity: sha512-dRPsyPyqpLD0HMRCRpYALIh4kdOir8pPg4AhNQZLehKowigRd30RcLXGNVZcc31Ua8CiPI4QSgjOIxK+EQe4LQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.12.9': - resolution: {integrity: sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==} + '@swc/core-linux-x64-musl@1.13.2': + resolution: {integrity: sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.12.9': - resolution: {integrity: sha512-zHOusMVbOH9ik5RtRrMiGzLpKwxrPXgXkBm3SbUCa65HAdjV33NZ0/R9Rv1uPESALtEl2tzMYLUxYA5ECFDFhA==} + '@swc/core-win32-arm64-msvc@1.13.2': + resolution: {integrity: sha512-Wv/QTA6PjyRLlmKcN6AmSI4jwSMRl0VTLGs57PHTqYRwwfwd7y4s2fIPJVBNbAlXd795dOEP6d/bGSQSyhOX3A==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.12.9': - resolution: {integrity: sha512-aWZf0PqE0ot7tCuhAjRkDFf41AzzSQO0x2xRfTbnhpROp57BRJ/N5eee1VULO/UA2PIJRG7GKQky5bSGBYlFug==} + '@swc/core-win32-ia32-msvc@1.13.2': + resolution: {integrity: sha512-PuCdtNynEkUNbUXX/wsyUC+t4mamIU5y00lT5vJcAvco3/r16Iaxl5UCzhXYaWZSNVZMzPp9qN8NlSL8M5pPxw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.12.9': - resolution: {integrity: sha512-C25fYftXOras3P3anSUeXXIpxmEkdAcsIL9yrr0j1xepTZ/yKwpnQ6g3coj8UXdeJy4GTVlR6+Ow/QiBgZQNOg==} + '@swc/core-win32-x64-msvc@1.13.2': + resolution: {integrity: sha512-qlmMkFZJus8cYuBURx1a3YAG2G7IW44i+FEYV5/32ylKkzGNAr9tDJSA53XNnNXkAB5EXSPsOz7bn5C3JlEtdQ==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.12.9': - resolution: {integrity: sha512-O+LfT2JlVMsIMWG9x+rdxg8GzpzeGtCZQfXV7cKc1PjIKUkLFf1QJ7okuseA4f/9vncu37dQ2ZcRrPKy0Ndd5g==} + '@swc/core@1.13.2': + resolution: {integrity: sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1993,35 +1992,35 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 - '@tanstack/history@1.121.34': - resolution: {integrity: sha512-YL8dGi5ZU+xvtav2boRlw4zrRghkY6hvdcmHhA0RGSJ/CBgzv+cbADW9eYJLx74XMZvIQ1pp6VMbrpXnnM5gHA==} + '@tanstack/history@1.129.7': + resolution: {integrity: sha512-I3YTkbe4RZQN54Qw4+IUhOjqG2DdbG2+EBWuQfew4MEk0eddLYAQVa50BZVww4/D2eh5I9vEk2Fd1Y0Wty7pug==} engines: {node: '>=12'} - '@tanstack/query-core@5.81.5': - resolution: {integrity: sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==} + '@tanstack/query-core@5.83.0': + resolution: {integrity: sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==} - '@tanstack/react-query@5.81.5': - resolution: {integrity: sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==} + '@tanstack/react-query@5.83.0': + resolution: {integrity: sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.124.0': - resolution: {integrity: sha512-CpOUUvtOYfLQEQS/ikGL9FQgEgYzBOKq9/2LqqFDXhZZgCVW18rBvR3LZeejkYSHAWlRphG33sdXCYVRM02sZQ==} + '@tanstack/react-router-devtools@1.129.8': + resolution: {integrity: sha512-+gVwYRLFAoQ+U4+UGX5/VgxspoJN4dm6/z4vYaZyrOUBVo+UjjH+bpvdz9ZrooBQ9EdkrkORPH8EfZp5qgi5Bg==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.124.0 + '@tanstack/react-router': ^1.129.8 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.124.0': - resolution: {integrity: sha512-jJxuLbPP/Cxirnft3CoiGWyH0aj94VTmLNcYauvjTGRNbUitK4udvGaHXVEP8bcifYvpko7ptsqqBlisaosugA==} + '@tanstack/react-router@1.129.8': + resolution: {integrity: sha512-d5mfM+67h3wq7aHkLjRKXD1ddbzx1YuxaEbNvW45jjZXMgaikZSVfJrZBiUWXE/nhV1sTdbMQ48JcPagvGPmYQ==} engines: {node: '>=12'} peerDependencies: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-store@0.7.1': - resolution: {integrity: sha512-qUTEKdId6QPWGiWyKAPf/gkN29scEsz6EUSJ0C3HgLMgaqTAyBsQ2sMCfGVcqb+kkhEXAdjleCgH6LAPD6f2sA==} + '@tanstack/react-store@0.7.3': + resolution: {integrity: sha512-3Dnqtbw9P2P0gw8uUM8WP2fFfg8XMDSZCTsywRPZe/XqqYW8PGkXKZTvP0AHkE4mpqP9Y43GpOg9vwO44azu6Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2039,15 +2038,15 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.124.0': - resolution: {integrity: sha512-mU2KA2v+ZFWC3NIjY2y+pPCx1sZDXPsUkzPjPPZxRgonE11nIu9MB89WuukqYuPbxoSWeodKNXsLe4KksGFCKA==} + '@tanstack/router-core@1.129.8': + resolution: {integrity: sha512-Izqf5q8TzJv0DJURynitJioPJT3dPAefrzHi2wlY/Q5+7nEG41SkjYMotTX2Q9i/Pjl91lW8gERCHpksszRdRw==} engines: {node: '>=12'} - '@tanstack/router-devtools-core@1.124.0': - resolution: {integrity: sha512-F4xejY63XrQmQZ8q6IJmLHJGeow/5CzdCAWChYEHIFy9SWYiFMMvdGFpB8SReJuwld+eoHvvtp2qhUqNloqzRA==} + '@tanstack/router-devtools-core@1.129.8': + resolution: {integrity: sha512-1yiAoWWYV3hWLXoHv92LMU67EjJpavoavo00EYzf7RLCy0TA/a+KyokZBS6PD38sITamHgVeY/jJBGD6hr47rQ==} engines: {node: '>=12'} peerDependencies: - '@tanstack/router-core': ^1.124.0 + '@tanstack/router-core': ^1.129.8 csstype: ^3.0.10 solid-js: '>=1.9.5' tiny-invariant: ^1.3.3 @@ -2055,16 +2054,16 @@ packages: csstype: optional: true - '@tanstack/router-generator@1.124.0': - resolution: {integrity: sha512-fatjfBvgLh7i2xcLKO3QaM5egHAhMy57B7DfE44sYx1D7/xxLOubSEjSnVSLE3dWBrstZ3aqyuYYhw7NuoXB7g==} + '@tanstack/router-generator@1.129.8': + resolution: {integrity: sha512-i4QTtJeRq3jdRTuUXHKcmPNm6STS0jLJNTKEdeUCIzuVBiiP53oujMOd84e5ARP83k2IB2XcMHekTSzDlWD2fg==} engines: {node: '>=12'} - '@tanstack/router-plugin@1.124.0': - resolution: {integrity: sha512-CqV3PCVoMrHw0HyTioIGHTTjaMRgfwbW4ax2Pule++smyetn+3KPLV6C3VWc0vdukZMQz13JvLORSSeM0B2cYQ==} + '@tanstack/router-plugin@1.129.8': + resolution: {integrity: sha512-DdO6el2slgBO2mIqIGdGyHCzsbQLsTNxsgbNz9ZY9y324iP4G+p3iEYopHWgzLKM2DKinMs9F7AxjLow4V3klQ==} engines: {node: '>=12'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.124.0 + '@tanstack/react-router': ^1.129.8 vite: '>=5.0.0 || >=6.0.0' vite-plugin-solid: ^2.11.2 webpack: '>=5.92.0' @@ -2080,12 +2079,12 @@ packages: webpack: optional: true - '@tanstack/router-utils@1.121.21': - resolution: {integrity: sha512-u7ubq1xPBtNiU7Fm+EOWlVWdgFLzuKOa1thhqdscVn8R4dNMUd1VoOjZ6AKmLw201VaUhFtlX+u0pjzI6szX7A==} + '@tanstack/router-utils@1.129.7': + resolution: {integrity: sha512-I2OyQF5U6sxHJApXKCUmCncTHKcpj4681FwyxpYg5QYOatHcn/zVMl7Rj4h36fu8/Lo2ZRLxUMd5kmXgp5Pb/A==} engines: {node: '>=12'} - '@tanstack/store@0.7.1': - resolution: {integrity: sha512-PjUQKXEXhLYj2X5/6c1Xn/0/qKY0IVFxTJweopRfF26xfjVyb14yALydJrHupDh3/d+1WKmfEgZPBVCmDkzzwg==} + '@tanstack/store@0.7.2': + resolution: {integrity: sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg==} '@tanstack/table-core@8.21.3': resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} @@ -2094,8 +2093,8 @@ packages: '@tanstack/virtual-core@3.13.12': resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} - '@tanstack/virtual-file-routes@1.121.21': - resolution: {integrity: sha512-3nuYsTyaq6ZN7jRZ9z6Gj3GXZqBOqOT0yzd/WZ33ZFfv4yVNIvsa5Lw+M1j3sgyEAxKMqGu/FaNi7FCjr3yOdw==} + '@tanstack/virtual-file-routes@1.129.7': + resolution: {integrity: sha512-a+MxoAXG+Sq94Jp67OtveKOp2vQq75AWdVI8DRt6w19B0NEqpfm784FTLbVp/qdR1wmxCOmKAvElGSIiBOx5OQ==} engines: {node: '>=12'} '@testing-library/dom@10.4.0': @@ -2127,20 +2126,20 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@textlint/ast-node-types@14.8.4': - resolution: {integrity: sha512-+fI7miec/r9VeniFV9ppL4jRCmHNsTxieulTUf/4tvGII3db5hGriKHC4p/diq1SkQ9Sgs7kg6UyydxZtpTz1Q==} + '@textlint/ast-node-types@15.2.0': + resolution: {integrity: sha512-nr9wEiZCNYafGZ++uWFZgPlDX3Bi7u4T2d5swpaoMvc1G2toXsBfe7UNVwXZq5dvYDbQN7vDeb3ltlKQ8JnPNQ==} - '@textlint/linter-formatter@14.8.4': - resolution: {integrity: sha512-sZ0UfYRDBNHnfMVBqLqqYnqTB7Ec169ljlmo+SEHR1T+dHUPYy1/DZK4p7QREXlBSFL4cnkswETCbc9xRodm4Q==} + '@textlint/linter-formatter@15.2.0': + resolution: {integrity: sha512-L+fM2OTs17hRxPCLKUdPjHce7cJp81gV9ku53FCL+cXnq5bZx0XYYkqKdtC0jnXujkQmrTYU3SYFrb4DgXqbtA==} - '@textlint/module-interop@14.8.4': - resolution: {integrity: sha512-1LdPYLAVpa27NOt6EqvuFO99s4XLB0c19Hw9xKSG6xQ1K82nUEyuWhzTQKb3KJ5Qx7qj14JlXZLfnEuL6A16Bw==} + '@textlint/module-interop@15.2.0': + resolution: {integrity: sha512-M3y1s2dZZH8PSHo4RUlnPOdK3qN90wmYGaEdy+il9/BQfrrift7S9R8lOfhHoPS0m9FEsnwyj3dQLkCUugPd9Q==} - '@textlint/resolver@14.8.4': - resolution: {integrity: sha512-nMDOgDAVwNU9ommh+Db0U+MCMNDPbQ/1HBNjbnHwxZkCpcT6hsAJwBe38CW/DtWVUv8yeR4R40IYNPT84srNwA==} + '@textlint/resolver@15.2.0': + resolution: {integrity: sha512-1UC+5bEtuoht7uu0uGofb7sX7j17Mvyst9InrRtI4XgKhh1uMZz5YFiMYpNwry1GgCZvq7Wyq1fqtEIsvYWqFw==} - '@textlint/types@14.8.4': - resolution: {integrity: sha512-9nyY8vVXlr8hHKxa6+37omJhXWCwovMQcgMteuldYd4dOxGm14AK2nXdkgtKEUQnzLGaXy46xwLCfhQy7V7/YA==} + '@textlint/types@15.2.0': + resolution: {integrity: sha512-wpF+xjGJgJK2JiwUdYjuNZrbuas3KfC9VDnHKac6aBLFyrI1iXuXtuxKXQDFi5/hebACactSJOuVVbuQbdJZ1Q==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -2316,11 +2315,11 @@ packages: '@types/node@20.11.25': resolution: {integrity: sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==} - '@types/node@20.19.4': - resolution: {integrity: sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA==} + '@types/node@20.19.9': + resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} - '@types/node@24.0.10': - resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==} + '@types/node@24.1.0': + resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2357,68 +2356,68 @@ packages: '@types/vscode@1.96.0': resolution: {integrity: sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==} - '@typescript-eslint/eslint-plugin@8.35.1': - resolution: {integrity: sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==} + '@typescript-eslint/eslint-plugin@8.38.0': + resolution: {integrity: sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.35.1 + '@typescript-eslint/parser': ^8.38.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.35.1': - resolution: {integrity: sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==} + '@typescript-eslint/parser@8.38.0': + resolution: {integrity: sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/project-service@8.35.1': - resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==} + '@typescript-eslint/project-service@8.38.0': + resolution: {integrity: sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.35.1': - resolution: {integrity: sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==} + '@typescript-eslint/scope-manager@8.38.0': + resolution: {integrity: sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.35.1': - resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==} + '@typescript-eslint/tsconfig-utils@8.38.0': + resolution: {integrity: sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@8.35.1': - resolution: {integrity: sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==} + '@typescript-eslint/type-utils@8.38.0': + resolution: {integrity: sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.35.1': - resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==} + '@typescript-eslint/types@8.38.0': + resolution: {integrity: sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.35.1': - resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==} + '@typescript-eslint/typescript-estree@8.38.0': + resolution: {integrity: sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.35.1': - resolution: {integrity: sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==} + '@typescript-eslint/utils@8.38.0': + resolution: {integrity: sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.35.1': - resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} + '@typescript-eslint/visitor-keys@8.38.0': + resolution: {integrity: sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typespec/ts-http-runtime@0.2.3': - resolution: {integrity: sha512-oRhjSzcVjX8ExyaF8hC0zzTqxlVuRlgMHL/Bh4w3xB9+wjbm0FpXylVU/lBrn+kgphwYTrOk3tp+AVShGmlYCg==} - engines: {node: '>=18.0.0'} + '@typespec/ts-http-runtime@0.3.0': + resolution: {integrity: sha512-sOx1PKSuFwnIl7z4RN0Ls7N9AQawmR9r66eI5rFCzLDIs8HTIYrIpH9QjYWoX0lkgGrkLxXhi4QnK7MizPRrIg==} + engines: {node: '>=20.0.0'} '@uidotdev/usehooks@2.4.1': resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==} @@ -2427,8 +2426,8 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' - '@uiw/codemirror-extensions-basic-setup@4.23.14': - resolution: {integrity: sha512-lCseubZqjN9bFwHJdQlZEKEo2yO1tCiMMVL0gu3ZXwhqMdfnd6ky/fUCYbn8aJkW+cXKVwjEVhpKjOphNiHoNw==} + '@uiw/codemirror-extensions-basic-setup@4.24.1': + resolution: {integrity: sha512-o1m1a8eUS3fWERMbDFvN8t8sZUFPgDKNemmlQ5Ot2vKm+Ax84lKP1dhEFgkiOaZ1bDHk4T5h6SjHuTghrJHKww==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' '@codemirror/commands': '>=6.0.0' @@ -2438,8 +2437,8 @@ packages: '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' - '@uiw/react-codemirror@4.23.14': - resolution: {integrity: sha512-/CmlSh8LGUEZCxg/f78MEkEMehKnVklqJvJlL10AXXrO/2xOyPqHb8SK10GhwOqd0kHhHgVYp4+6oK5S+UIEuQ==} + '@uiw/react-codemirror@4.24.1': + resolution: {integrity: sha512-BivF4NLqbuBQK5gPVhSkOARi9nPXw8X5r25EnInPeY+I9l1dfEX8O9V6+0xHTlGHyUo0cNfGEF9t1KHEicUfJw==} peerDependencies: '@babel/runtime': '>=7.11.0' '@codemirror/state': '>=6.0.0' @@ -2452,16 +2451,16 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitejs/plugin-react-swc@3.10.2': - resolution: {integrity: sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==} + '@vitejs/plugin-react-swc@3.11.0': + resolution: {integrity: sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==} peerDependencies: - vite: ^4 || ^5 || ^6 || ^7.0.0-beta.0 + vite: ^4 || ^5 || ^6 || ^7 - '@vitejs/plugin-react@4.6.0': - resolution: {integrity: sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==} + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 '@vitest/browser@3.2.3': resolution: {integrity: sha512-5HpUb0ixGF8JWSAjb/P1x/VPuTYUkL4pL0+YO6DJiuvQgqJN3PREaUEcXwfXjU4nBc37EahfpRbAwdE9pHs9lQ==} @@ -2672,8 +2671,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agent-base@7.1.3: - resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} ajv-draft-04@1.0.0: @@ -2712,9 +2711,9 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -2934,15 +2933,15 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001726: - resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} + caniuse-lite@1.0.30001727: + resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chai@5.2.0: - resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} - engines: {node: '>=12'} + chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} chalk-template@0.4.0: resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} @@ -2979,9 +2978,9 @@ packages: cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - cheerio@1.1.0: - resolution: {integrity: sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==} - engines: {node: '>=18.17'} + cheerio@1.1.2: + resolution: {integrity: sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==} + engines: {node: '>=20.18.1'} chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} @@ -3196,8 +3195,8 @@ packages: resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} engines: {node: '>=10'} - decimal.js@10.5.0: - resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} @@ -3321,8 +3320,8 @@ packages: resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==} engines: {node: '>=4'} - electron-to-chromium@1.5.179: - resolution: {integrity: sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==} + electron-to-chromium@1.5.190: + resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==} elkjs@0.8.2: resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} @@ -3358,8 +3357,9 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} @@ -3400,8 +3400,8 @@ packages: peerDependencies: esbuild: '>=0.12 <1' - esbuild@0.25.5: - resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} engines: {node: '>=18'} hasBin: true @@ -3429,8 +3429,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.30.1: - resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==} + eslint@9.31.0: + resolution: {integrity: sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -3493,8 +3493,8 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} - expect-type@1.2.1: - resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} extend@3.0.2: @@ -3590,8 +3590,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.3: - resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} fraction.js@4.3.7: @@ -3600,10 +3600,6 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - fs-extra@11.3.0: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} @@ -3838,6 +3834,10 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + index-to-position@1.1.0: + resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==} + engines: {node: '>=18'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -3865,9 +3865,6 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -4126,10 +4123,6 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-parse-even-better-errors@3.0.2: - resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -4270,10 +4263,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lines-and-columns@2.0.4: - resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} @@ -4634,9 +4623,9 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - node-sarif-builder@2.0.3: - resolution: {integrity: sha512-Pzr3rol8fvhG/oJjIq2NTVB0vmdNNlz22FENhhPojYRZ4/ee08CfK4YuKmuL54V9MLhI1kpzxfOJ/63LzmZzDg==} - engines: {node: '>=14'} + node-sarif-builder@3.2.0: + resolution: {integrity: sha512-kVIOdynrF2CRodHZeP/97Rh1syTUHBNiw17hUCIVhlhEsWlfJm19MuO56s4MdKbr22xWx6mzMnNAgXzVlIYM9Q==} + engines: {node: '>=18'} normalize-package-data@6.0.2: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} @@ -4707,8 +4696,8 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - open@10.1.2: - resolution: {integrity: sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} open@8.4.2: @@ -4773,9 +4762,9 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} - parse-json@7.1.1: - resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} - engines: {node: '>=16'} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} parse-semver@1.1.1: resolution: {integrity: sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==} @@ -4841,8 +4830,8 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} pify@2.3.0: @@ -4853,13 +4842,13 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - playwright-core@1.53.2: - resolution: {integrity: sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==} + playwright-core@1.54.1: + resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==} engines: {node: '>=18'} hasBin: true - playwright@1.53.2: - resolution: {integrity: sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==} + playwright@1.54.1: + resolution: {integrity: sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==} engines: {node: '>=18'} hasBin: true @@ -5046,8 +5035,8 @@ packages: '@types/react': optional: true - react-router@7.6.3: - resolution: {integrity: sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==} + react-router@7.7.0: + resolution: {integrity: sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -5084,9 +5073,9 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - read-pkg@8.1.0: - resolution: {integrity: sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==} - engines: {node: '>=16'} + read-pkg@9.0.1: + resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} + engines: {node: '>=18'} read@1.0.7: resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==} @@ -5163,8 +5152,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.44.1: - resolution: {integrity: sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==} + rollup@4.45.1: + resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -5216,8 +5205,8 @@ packages: resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} engines: {node: '>= 10.13.0'} - secretlint@10.1.1: - resolution: {integrity: sha512-q50i+I9w6HH8P6o34LVq6M3hm5GZn2Eq5lYGHkEByOAbVqBHn8gsMGgyxjP1xSrSv1QjDtjxs/zKPm6JtkNzGw==} + secretlint@10.2.1: + resolution: {integrity: sha512-3BghQkIGrDz3xJklX/COxgKbxHz2CAsGkXH4oh8MxeYVLlhA3L/TLhAxZiTyqeril+CnDGg8MUEZdX1dZNsxVA==} engines: {node: '>=20.0.0'} hasBin: true @@ -5399,8 +5388,8 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - storybook@9.0.15: - resolution: {integrity: sha512-r9hwcSMM3dq7dkMveaWFTosrmyHCL2FRrV3JOwVnVWraF6GtCgp2k+r4hsYtyp1bY3zdmK9e4KYzXsGs5q1h/Q==} + storybook@9.0.18: + resolution: {integrity: sha512-ruxpEpizwoYQTt1hBOrWyp9trPYWD9Apt1TJ37rs1rzmNQWpSNGJDMg91JV4mUhBChzRvnid/oRBFFCWJz/dfw==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -5509,9 +5498,9 @@ packages: resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} engines: {node: '>=12'} - supports-hyperlinks@2.3.0: - resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} - engines: {node: '>=8'} + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -5558,9 +5547,9 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - terminal-link@2.1.1: - resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} - engines: {node: '>=8'} + terminal-link@4.0.0: + resolution: {integrity: sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==} + engines: {node: '>=18'} terser-webpack-plugin@5.3.14: resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} @@ -5732,14 +5721,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - - type-fest@3.13.1: - resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} - engines: {node: '>=14.16'} - type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -5763,8 +5744,8 @@ packages: typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} - typedoc-plugin-markdown@4.7.0: - resolution: {integrity: sha512-PitbnAps2vpcqK2gargKoiFXLWFttvwUbyns/E6zGIFG5Gz8ZQJGttHnYR9csOlcSjB/uyjd8tnoayrtsXG17w==} + typedoc-plugin-markdown@4.7.1: + resolution: {integrity: sha512-HN/fHLm2S6MD4HX8txfB4eWvVBzX/mEYy5U5s1KTAdh3E5uX5/lilswqTzZlPTT6fNZInAboAdFGpbAuBKnE4A==} engines: {node: '>= 18'} peerDependencies: typedoc: 0.28.x @@ -5776,8 +5757,8 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x - typescript-eslint@8.35.1: - resolution: {integrity: sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==} + typescript-eslint@8.38.0: + resolution: {integrity: sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -5811,8 +5792,8 @@ packages: undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} - undici@7.11.0: - resolution: {integrity: sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==} + undici@7.12.0: + resolution: {integrity: sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==} engines: {node: '>=20.18.1'} unicorn-magic@0.1.0: @@ -6137,6 +6118,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -6209,8 +6194,8 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - zod@3.25.71: - resolution: {integrity: sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} @@ -6302,46 +6287,46 @@ snapshots: dependencies: tslib: 2.8.1 - '@azure/core-auth@1.9.0': + '@azure/core-auth@1.10.0': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-util': 1.12.0 + '@azure/core-util': 1.13.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-client@1.9.4': + '@azure/core-client@1.10.0': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.9.0 - '@azure/core-rest-pipeline': 1.21.0 - '@azure/core-tracing': 1.2.0 - '@azure/core-util': 1.12.0 - '@azure/logger': 1.2.0 + '@azure/core-auth': 1.10.0 + '@azure/core-rest-pipeline': 1.22.0 + '@azure/core-tracing': 1.3.0 + '@azure/core-util': 1.13.0 + '@azure/logger': 1.3.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-rest-pipeline@1.21.0': + '@azure/core-rest-pipeline@1.22.0': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.9.0 - '@azure/core-tracing': 1.2.0 - '@azure/core-util': 1.12.0 - '@azure/logger': 1.2.0 - '@typespec/ts-http-runtime': 0.2.3 + '@azure/core-auth': 1.10.0 + '@azure/core-tracing': 1.3.0 + '@azure/core-util': 1.13.0 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-tracing@1.2.0': + '@azure/core-tracing@1.3.0': dependencies: tslib: 2.8.1 - '@azure/core-util@1.12.0': + '@azure/core-util@1.13.0': dependencies: '@azure/abort-controller': 2.1.2 - '@typespec/ts-http-runtime': 0.2.3 + '@typespec/ts-http-runtime': 0.3.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -6349,35 +6334,35 @@ snapshots: '@azure/identity@4.10.2': dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.9.0 - '@azure/core-client': 1.9.4 - '@azure/core-rest-pipeline': 1.21.0 - '@azure/core-tracing': 1.2.0 - '@azure/core-util': 1.12.0 - '@azure/logger': 1.2.0 - '@azure/msal-browser': 4.14.0 - '@azure/msal-node': 3.6.2 - open: 10.1.2 + '@azure/core-auth': 1.10.0 + '@azure/core-client': 1.10.0 + '@azure/core-rest-pipeline': 1.22.0 + '@azure/core-tracing': 1.3.0 + '@azure/core-util': 1.13.0 + '@azure/logger': 1.3.0 + '@azure/msal-browser': 4.15.0 + '@azure/msal-node': 3.6.3 + open: 10.2.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/logger@1.2.0': + '@azure/logger@1.3.0': dependencies: - '@typespec/ts-http-runtime': 0.2.3 + '@typespec/ts-http-runtime': 0.3.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/msal-browser@4.14.0': + '@azure/msal-browser@4.15.0': dependencies: - '@azure/msal-common': 15.8.0 + '@azure/msal-common': 15.8.1 - '@azure/msal-common@15.8.0': {} + '@azure/msal-common@15.8.1': {} - '@azure/msal-node@3.6.2': + '@azure/msal-node@3.6.3': dependencies: - '@azure/msal-common': 15.8.0 + '@azure/msal-common': 15.8.1 jsonwebtoken: 9.0.2 uuid: 8.3.2 @@ -6400,7 +6385,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 convert-source-map: 2.0.0 debug: 4.4.1(supports-color@8.1.1) gensync: 1.0.0-beta.2 @@ -6412,14 +6397,14 @@ snapshots: '@babel/generator@7.28.0': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@jridgewell/gen-mapping': 0.3.12 '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -6447,14 +6432,14 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color @@ -6469,7 +6454,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@babel/helper-plugin-utils@7.27.1': {} @@ -6485,7 +6470,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color @@ -6498,11 +6483,11 @@ snapshots: '@babel/helpers@7.27.6': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@babel/parser@7.28.0': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': dependencies: @@ -6560,7 +6545,7 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@babel/traverse@7.28.0': dependencies: @@ -6569,12 +6554,12 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/types@7.28.0': + '@babel/types@7.28.1': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -6583,13 +6568,13 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@chromatic-com/storybook@4.0.1(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@chromatic-com/storybook@4.0.1(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 12.2.0 filesize: 10.1.6 jsonfile: 6.1.0 - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) strip-ansi: 7.1.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -6599,14 +6584,14 @@ snapshots: dependencies: '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.0 + '@codemirror/view': 6.38.1 '@lezer/common': 1.2.3 '@codemirror/commands@6.8.1': dependencies: '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.0 + '@codemirror/view': 6.38.1 '@lezer/common': 1.2.3 '@codemirror/lang-python@6.2.1': @@ -6629,7 +6614,7 @@ snapshots: '@codemirror/language@6.11.2': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.0 + '@codemirror/view': 6.38.1 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 @@ -6642,13 +6627,13 @@ snapshots: '@codemirror/lint@6.8.5': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.0 + '@codemirror/view': 6.38.1 crelt: 1.0.6 '@codemirror/search@6.5.10': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.0 + '@codemirror/view': 6.38.1 crelt: 1.0.6 '@codemirror/state@6.5.2': @@ -6659,10 +6644,10 @@ snapshots: dependencies: '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.0 + '@codemirror/view': 6.38.1 '@lezer/highlight': 1.2.1 - '@codemirror/view@6.38.0': + '@codemirror/view@6.38.1': dependencies: '@codemirror/state': 6.5.2 crelt: 1.0.6 @@ -6689,84 +6674,87 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@esbuild/aix-ppc64@0.25.5': + '@esbuild/aix-ppc64@0.25.8': + optional: true + + '@esbuild/android-arm64@0.25.8': optional: true - '@esbuild/android-arm64@0.25.5': + '@esbuild/android-arm@0.25.8': optional: true - '@esbuild/android-arm@0.25.5': + '@esbuild/android-x64@0.25.8': optional: true - '@esbuild/android-x64@0.25.5': + '@esbuild/darwin-arm64@0.25.8': optional: true - '@esbuild/darwin-arm64@0.25.5': + '@esbuild/darwin-x64@0.25.8': optional: true - '@esbuild/darwin-x64@0.25.5': + '@esbuild/freebsd-arm64@0.25.8': optional: true - '@esbuild/freebsd-arm64@0.25.5': + '@esbuild/freebsd-x64@0.25.8': optional: true - '@esbuild/freebsd-x64@0.25.5': + '@esbuild/linux-arm64@0.25.8': optional: true - '@esbuild/linux-arm64@0.25.5': + '@esbuild/linux-arm@0.25.8': optional: true - '@esbuild/linux-arm@0.25.5': + '@esbuild/linux-ia32@0.25.8': optional: true - '@esbuild/linux-ia32@0.25.5': + '@esbuild/linux-loong64@0.25.8': optional: true - '@esbuild/linux-loong64@0.25.5': + '@esbuild/linux-mips64el@0.25.8': optional: true - '@esbuild/linux-mips64el@0.25.5': + '@esbuild/linux-ppc64@0.25.8': optional: true - '@esbuild/linux-ppc64@0.25.5': + '@esbuild/linux-riscv64@0.25.8': optional: true - '@esbuild/linux-riscv64@0.25.5': + '@esbuild/linux-s390x@0.25.8': optional: true - '@esbuild/linux-s390x@0.25.5': + '@esbuild/linux-x64@0.25.8': optional: true - '@esbuild/linux-x64@0.25.5': + '@esbuild/netbsd-arm64@0.25.8': optional: true - '@esbuild/netbsd-arm64@0.25.5': + '@esbuild/netbsd-x64@0.25.8': optional: true - '@esbuild/netbsd-x64@0.25.5': + '@esbuild/openbsd-arm64@0.25.8': optional: true - '@esbuild/openbsd-arm64@0.25.5': + '@esbuild/openbsd-x64@0.25.8': optional: true - '@esbuild/openbsd-x64@0.25.5': + '@esbuild/openharmony-arm64@0.25.8': optional: true - '@esbuild/sunos-x64@0.25.5': + '@esbuild/sunos-x64@0.25.8': optional: true - '@esbuild/win32-arm64@0.25.5': + '@esbuild/win32-arm64@0.25.8': optional: true - '@esbuild/win32-ia32@0.25.5': + '@esbuild/win32-ia32@0.25.8': optional: true - '@esbuild/win32-x64@0.25.5': + '@esbuild/win32-x64@0.25.8': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0(jiti@2.4.2))': dependencies: - eslint: 9.30.1(jiti@2.4.2) + eslint: 9.31.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -6781,10 +6769,6 @@ snapshots: '@eslint/config-helpers@0.3.0': {} - '@eslint/core@0.14.0': - dependencies: - '@types/json-schema': 7.0.15 - '@eslint/core@0.15.1': dependencies: '@types/json-schema': 7.0.15 @@ -6803,11 +6787,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.30.1': {} + '@eslint/js@9.31.0': {} '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.3.3': + '@eslint/plugin-kit@0.3.4': dependencies: '@eslint/core': 0.15.1 levn: 0.4.1 @@ -6839,19 +6823,19 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@gerrit0/mini-shiki@3.7.0': + '@gerrit0/mini-shiki@3.8.1': dependencies: - '@shikijs/engine-oniguruma': 3.7.0 - '@shikijs/langs': 3.7.0 - '@shikijs/themes': 3.7.0 - '@shikijs/types': 3.7.0 + '@shikijs/engine-oniguruma': 3.8.1 + '@shikijs/langs': 3.8.1 + '@shikijs/themes': 3.8.1 + '@shikijs/types': 3.8.1 '@shikijs/vscode-textmate': 10.0.2 - '@headlessui/react@2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@headlessui/react@2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/focus': 3.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/interactions': 3.25.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/focus': 3.21.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/interactions': 3.25.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-virtual': 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -6913,12 +6897,12 @@ 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@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.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.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: typescript: 5.8.3 @@ -6971,7 +6955,7 @@ snapshots: '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 - '@lit/react@1.0.7(@types/react@18.3.23)': + '@lit/react@1.0.8(@types/react@18.3.23)': dependencies: '@types/react': 18.3.23 @@ -7022,7 +7006,7 @@ snapshots: chalk: 4.1.2 compare-versions: 6.1.1 debug: 4.4.1(supports-color@8.1.1) - esbuild: 0.25.5 + esbuild: 0.25.8 esutils: 2.0.3 fs-extra: 11.3.0 globby: 11.1.0 @@ -7068,7 +7052,7 @@ snapshots: '@orval/mock@7.10.0(openapi-types@12.1.3)': dependencies: '@orval/core': 7.10.0(openapi-types@12.1.3) - openapi3-ts: 4.2.2 + openapi3-ts: 4.4.0 transitivePeerDependencies: - encoding - openapi-types @@ -7105,9 +7089,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.53.2': + '@playwright/test@1.54.1': dependencies: - playwright: 1.53.2 + playwright: 1.54.1 '@polka/url@1.0.0-next.29': {} @@ -7396,37 +7380,37 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@react-aria/focus@3.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/focus@3.21.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@react-aria/interactions': 3.25.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/utils': 3.29.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-types/shared': 3.30.0(react@18.3.1) + '@react-aria/interactions': 3.25.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.31.0(react@18.3.1) '@swc/helpers': 0.5.17 clsx: 2.1.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@react-aria/interactions@3.25.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/interactions@3.25.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@react-aria/ssr': 3.9.9(react@18.3.1) - '@react-aria/utils': 3.29.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/ssr': 3.9.10(react@18.3.1) + '@react-aria/utils': 3.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-stately/flags': 3.1.2 - '@react-types/shared': 3.30.0(react@18.3.1) + '@react-types/shared': 3.31.0(react@18.3.1) '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@react-aria/ssr@3.9.9(react@18.3.1)': + '@react-aria/ssr@3.9.10(react@18.3.1)': dependencies: '@swc/helpers': 0.5.17 react: 18.3.1 - '@react-aria/utils@3.29.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/utils@3.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@react-aria/ssr': 3.9.9(react@18.3.1) + '@react-aria/ssr': 3.9.10(react@18.3.1) '@react-stately/flags': 3.1.2 - '@react-stately/utils': 3.10.7(react@18.3.1) - '@react-types/shared': 3.30.0(react@18.3.1) + '@react-stately/utils': 3.10.8(react@18.3.1) + '@react-types/shared': 3.31.0(react@18.3.1) '@swc/helpers': 0.5.17 clsx: 2.1.1 react: 18.3.1 @@ -7442,12 +7426,12 @@ snapshots: dependencies: '@swc/helpers': 0.5.17 - '@react-stately/utils@3.10.7(react@18.3.1)': + '@react-stately/utils@3.10.8(react@18.3.1)': dependencies: '@swc/helpers': 0.5.17 react: 18.3.1 - '@react-types/shared@3.30.0(react@18.3.1)': + '@react-types/shared@3.31.0(react@18.3.1)': dependencies: react: 18.3.1 @@ -7529,166 +7513,164 @@ snapshots: - '@types/react' - immer - '@rolldown/pluginutils@1.0.0-beta.11': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rolldown/pluginutils@1.0.0-beta.19': {} - - '@rollup/pluginutils@5.2.0(rollup@4.44.1)': + '@rollup/pluginutils@5.2.0(rollup@4.45.1)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.2 + picomatch: 4.0.3 optionalDependencies: - rollup: 4.44.1 + rollup: 4.45.1 - '@rollup/rollup-android-arm-eabi@4.44.1': + '@rollup/rollup-android-arm-eabi@4.45.1': optional: true - '@rollup/rollup-android-arm64@4.44.1': + '@rollup/rollup-android-arm64@4.45.1': optional: true - '@rollup/rollup-darwin-arm64@4.44.1': + '@rollup/rollup-darwin-arm64@4.45.1': optional: true - '@rollup/rollup-darwin-x64@4.44.1': + '@rollup/rollup-darwin-x64@4.45.1': optional: true - '@rollup/rollup-freebsd-arm64@4.44.1': + '@rollup/rollup-freebsd-arm64@4.45.1': optional: true - '@rollup/rollup-freebsd-x64@4.44.1': + '@rollup/rollup-freebsd-x64@4.45.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.44.1': + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.44.1': + '@rollup/rollup-linux-arm-musleabihf@4.45.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.44.1': + '@rollup/rollup-linux-arm64-gnu@4.45.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.44.1': + '@rollup/rollup-linux-arm64-musl@4.45.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.44.1': + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.44.1': + '@rollup/rollup-linux-riscv64-gnu@4.45.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.44.1': + '@rollup/rollup-linux-riscv64-musl@4.45.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.44.1': + '@rollup/rollup-linux-s390x-gnu@4.45.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.44.1': + '@rollup/rollup-linux-x64-gnu@4.45.1': optional: true - '@rollup/rollup-linux-x64-musl@4.44.1': + '@rollup/rollup-linux-x64-musl@4.45.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.44.1': + '@rollup/rollup-win32-arm64-msvc@4.45.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.44.1': + '@rollup/rollup-win32-ia32-msvc@4.45.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.44.1': + '@rollup/rollup-win32-x64-msvc@4.45.1': optional: true - '@secretlint/config-creator@10.1.1': + '@secretlint/config-creator@10.2.1': dependencies: - '@secretlint/types': 10.1.1 + '@secretlint/types': 10.2.1 - '@secretlint/config-loader@10.1.1': + '@secretlint/config-loader@10.2.1': dependencies: - '@secretlint/profiler': 10.1.1 - '@secretlint/resolver': 10.1.1 - '@secretlint/types': 10.1.1 + '@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 transitivePeerDependencies: - supports-color - '@secretlint/core@10.1.1': + '@secretlint/core@10.2.1': dependencies: - '@secretlint/profiler': 10.1.1 - '@secretlint/types': 10.1.1 + '@secretlint/profiler': 10.2.1 + '@secretlint/types': 10.2.1 debug: 4.4.1(supports-color@8.1.1) structured-source: 4.0.0 transitivePeerDependencies: - supports-color - '@secretlint/formatter@10.1.1': + '@secretlint/formatter@10.2.1': dependencies: - '@secretlint/resolver': 10.1.1 - '@secretlint/types': 10.1.1 - '@textlint/linter-formatter': 14.8.4 - '@textlint/module-interop': 14.8.4 - '@textlint/types': 14.8.4 - chalk: 4.1.2 + '@secretlint/resolver': 10.2.1 + '@secretlint/types': 10.2.1 + '@textlint/linter-formatter': 15.2.0 + '@textlint/module-interop': 15.2.0 + '@textlint/types': 15.2.0 + chalk: 5.4.1 debug: 4.4.1(supports-color@8.1.1) pluralize: 8.0.0 - strip-ansi: 6.0.1 + strip-ansi: 7.1.0 table: 6.9.0 - terminal-link: 2.1.1 + terminal-link: 4.0.0 transitivePeerDependencies: - supports-color - '@secretlint/node@10.1.1': + '@secretlint/node@10.2.1': dependencies: - '@secretlint/config-loader': 10.1.1 - '@secretlint/core': 10.1.1 - '@secretlint/formatter': 10.1.1 - '@secretlint/profiler': 10.1.1 - '@secretlint/source-creator': 10.1.1 - '@secretlint/types': 10.1.1 + '@secretlint/config-loader': 10.2.1 + '@secretlint/core': 10.2.1 + '@secretlint/formatter': 10.2.1 + '@secretlint/profiler': 10.2.1 + '@secretlint/source-creator': 10.2.1 + '@secretlint/types': 10.2.1 debug: 4.4.1(supports-color@8.1.1) p-map: 7.0.3 transitivePeerDependencies: - supports-color - '@secretlint/profiler@10.1.1': {} + '@secretlint/profiler@10.2.1': {} - '@secretlint/resolver@10.1.1': {} + '@secretlint/resolver@10.2.1': {} - '@secretlint/secretlint-formatter-sarif@10.1.1': + '@secretlint/secretlint-formatter-sarif@10.2.1': dependencies: - node-sarif-builder: 2.0.3 + node-sarif-builder: 3.2.0 - '@secretlint/secretlint-rule-no-dotenv@10.1.1': + '@secretlint/secretlint-rule-no-dotenv@10.2.1': dependencies: - '@secretlint/types': 10.1.1 + '@secretlint/types': 10.2.1 - '@secretlint/secretlint-rule-preset-recommend@10.1.1': {} + '@secretlint/secretlint-rule-preset-recommend@10.2.1': {} - '@secretlint/source-creator@10.1.1': + '@secretlint/source-creator@10.2.1': dependencies: - '@secretlint/types': 10.1.1 + '@secretlint/types': 10.2.1 istextorbinary: 9.5.0 - '@secretlint/types@10.1.1': {} + '@secretlint/types@10.2.1': {} - '@shikijs/engine-oniguruma@3.7.0': + '@shikijs/engine-oniguruma@3.8.1': dependencies: - '@shikijs/types': 3.7.0 + '@shikijs/types': 3.8.1 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.7.0': + '@shikijs/langs@3.8.1': dependencies: - '@shikijs/types': 3.7.0 + '@shikijs/types': 3.8.1 - '@shikijs/themes@3.7.0': + '@shikijs/themes@3.8.1': dependencies: - '@shikijs/types': 3.7.0 + '@shikijs/types': 3.8.1 - '@shikijs/types@3.7.0': + '@shikijs/types@3.8.1': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -7860,54 +7842,54 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 - '@storybook/addon-a11y@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/addon-a11y@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.10.3 - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) - '@storybook/addon-docs@9.0.15(@types/react@18.3.23)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/addon-docs@9.0.18(@types/react@18.3.23)(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.23)(react@18.3.1) - '@storybook/csf-plugin': 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) + '@storybook/csf-plugin': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2)) '@storybook/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-dom-shim': 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(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.0)(prettier@3.6.2)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-onboarding@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/addon-onboarding@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2))': dependencies: - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) - '@storybook/addon-vitest@9.0.15(@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.15(@testing-library/dom@10.4.0)(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.0)(prettier@3.6.2))(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.15(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) '@vitest/runner': 3.2.4 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(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)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - react - react-dom - '@storybook/builder-vite@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': + '@storybook/builder-vite@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@storybook/csf-plugin': 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2)) - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) + '@storybook/csf-plugin': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2)) + storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) ts-dedent: 2.2.0 - vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) - '@storybook/csf-plugin@9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/csf-plugin@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2))': dependencies: - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -7917,87 +7899,87 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/react-dom-shim@9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(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.0)(prettier@3.6.2))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) - '@storybook/react-vite@9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.44.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.0)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) - '@rollup/pluginutils': 5.2.0(rollup@4.44.1) - '@storybook/builder-vite': 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) - '@storybook/react': 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2))(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.43.1)(tsx@4.20.3)(yaml@2.8.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.0)(prettier@3.6.2))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.0)(prettier@3.6.2))(typescript@5.8.3) find-up: 7.0.0 magic-string: 0.30.17 react: 18.3.1 react-docgen: 8.0.0 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) tsconfig-paths: 4.2.0 - vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - rollup - supports-color - typescript - '@storybook/react@9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(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.0)(prettier@3.6.2))(typescript@5.8.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.0.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.15(@testing-library/dom@10.4.0)(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.0)(prettier@3.6.2)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) optionalDependencies: typescript: 5.8.3 - '@swc/core-darwin-arm64@1.12.9': + '@swc/core-darwin-arm64@1.13.2': optional: true - '@swc/core-darwin-x64@1.12.9': + '@swc/core-darwin-x64@1.13.2': optional: true - '@swc/core-linux-arm-gnueabihf@1.12.9': + '@swc/core-linux-arm-gnueabihf@1.13.2': optional: true - '@swc/core-linux-arm64-gnu@1.12.9': + '@swc/core-linux-arm64-gnu@1.13.2': optional: true - '@swc/core-linux-arm64-musl@1.12.9': + '@swc/core-linux-arm64-musl@1.13.2': optional: true - '@swc/core-linux-x64-gnu@1.12.9': + '@swc/core-linux-x64-gnu@1.13.2': optional: true - '@swc/core-linux-x64-musl@1.12.9': + '@swc/core-linux-x64-musl@1.13.2': optional: true - '@swc/core-win32-arm64-msvc@1.12.9': + '@swc/core-win32-arm64-msvc@1.13.2': optional: true - '@swc/core-win32-ia32-msvc@1.12.9': + '@swc/core-win32-ia32-msvc@1.13.2': optional: true - '@swc/core-win32-x64-msvc@1.12.9': + '@swc/core-win32-x64-msvc@1.13.2': optional: true - '@swc/core@1.12.9(@swc/helpers@0.5.17)': + '@swc/core@1.13.2(@swc/helpers@0.5.17)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.23 optionalDependencies: - '@swc/core-darwin-arm64': 1.12.9 - '@swc/core-darwin-x64': 1.12.9 - '@swc/core-linux-arm-gnueabihf': 1.12.9 - '@swc/core-linux-arm64-gnu': 1.12.9 - '@swc/core-linux-arm64-musl': 1.12.9 - '@swc/core-linux-x64-gnu': 1.12.9 - '@swc/core-linux-x64-musl': 1.12.9 - '@swc/core-win32-arm64-msvc': 1.12.9 - '@swc/core-win32-ia32-msvc': 1.12.9 - '@swc/core-win32-x64-msvc': 1.12.9 + '@swc/core-darwin-arm64': 1.13.2 + '@swc/core-darwin-x64': 1.13.2 + '@swc/core-linux-arm-gnueabihf': 1.13.2 + '@swc/core-linux-arm64-gnu': 1.13.2 + '@swc/core-linux-arm64-musl': 1.13.2 + '@swc/core-linux-x64-gnu': 1.13.2 + '@swc/core-linux-x64-musl': 1.13.2 + '@swc/core-win32-arm64-msvc': 1.13.2 + '@swc/core-win32-ia32-msvc': 1.13.2 + '@swc/core-win32-x64-msvc': 1.13.2 '@swc/helpers': 0.5.17 '@swc/counter@0.1.3': {} @@ -8086,26 +8068,26 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.11 - '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@tailwindcss/node': 4.1.11 '@tailwindcss/oxide': 4.1.11 tailwindcss: 4.1.11 - vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) - '@tanstack/history@1.121.34': {} + '@tanstack/history@1.129.7': {} - '@tanstack/query-core@5.81.5': {} + '@tanstack/query-core@5.83.0': {} - '@tanstack/react-query@5.81.5(react@18.3.1)': + '@tanstack/react-query@5.83.0(react@18.3.1)': dependencies: - '@tanstack/query-core': 5.81.5 + '@tanstack/query-core': 5.83.0 react: 18.3.1 - '@tanstack/react-router-devtools@1.124.0(@tanstack/react-router@1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.124.0)(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.129.8(@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)': dependencies: - '@tanstack/react-router': 1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-devtools-core': 1.124.0(@tanstack/router-core@1.124.0)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3) + '@tanstack/react-router': 1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/router-devtools-core': 1.129.8(@tanstack/router-core@1.129.8)(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: @@ -8114,21 +8096,20 @@ snapshots: - solid-js - tiny-invariant - '@tanstack/react-router@1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/history': 1.121.34 - '@tanstack/react-store': 0.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-core': 1.124.0 + '@tanstack/history': 1.129.7 + '@tanstack/react-store': 0.7.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/router-core': 1.129.8 isbot: 5.1.28 - jsesc: 3.1.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-store@0.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-store@0.7.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/store': 0.7.1 + '@tanstack/store': 0.7.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) use-sync-external-store: 1.5.0(react@18.3.1) @@ -8145,18 +8126,19 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@tanstack/router-core@1.124.0': + '@tanstack/router-core@1.129.8': dependencies: - '@tanstack/history': 1.121.34 - '@tanstack/store': 0.7.1 + '@tanstack/history': 1.129.7 + '@tanstack/store': 0.7.2 cookie-es: 1.2.2 - jsesc: 3.1.0 + seroval: 1.3.2 + seroval-plugins: 1.3.2(seroval@1.3.2) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.124.0(@tanstack/router-core@1.124.0)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': + '@tanstack/router-devtools-core@1.129.8(@tanstack/router-core@1.129.8)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': dependencies: - '@tanstack/router-core': 1.124.0 + '@tanstack/router-core': 1.129.8 clsx: 2.1.1 goober: 2.1.16(csstype@3.1.3) solid-js: 1.9.7 @@ -8164,43 +8146,43 @@ snapshots: optionalDependencies: csstype: 3.1.3 - '@tanstack/router-generator@1.124.0': + '@tanstack/router-generator@1.129.8': dependencies: - '@tanstack/router-core': 1.124.0 - '@tanstack/router-utils': 1.121.21 - '@tanstack/virtual-file-routes': 1.121.21 + '@tanstack/router-core': 1.129.8 + '@tanstack/router-utils': 1.129.7 + '@tanstack/virtual-file-routes': 1.129.7 prettier: 3.6.2 recast: 0.23.11 source-map: 0.7.4 tsx: 4.20.3 - zod: 3.25.71 + zod: 3.25.76 transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.124.0(@tanstack/react-router@1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.5))': + '@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.43.1)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(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/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 - '@tanstack/router-core': 1.124.0 - '@tanstack/router-generator': 1.124.0 - '@tanstack/router-utils': 1.121.21 - '@tanstack/virtual-file-routes': 1.121.21 + '@babel/types': 7.28.1 + '@tanstack/router-core': 1.129.8 + '@tanstack/router-generator': 1.129.8 + '@tanstack/router-utils': 1.129.7 + '@tanstack/virtual-file-routes': 1.129.7 babel-dead-code-elimination: 1.0.10 chokidar: 3.6.0 unplugin: 2.3.5 - zod: 3.25.71 + zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.124.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) - webpack: 5.99.8(esbuild@0.25.5) + '@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.43.1)(tsx@4.20.3)(yaml@2.8.0) + webpack: 5.99.8(esbuild@0.25.8) transitivePeerDependencies: - supports-color - '@tanstack/router-utils@1.121.21': + '@tanstack/router-utils@1.129.7': dependencies: '@babel/core': 7.28.0 '@babel/generator': 7.28.0 @@ -8211,13 +8193,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/store@0.7.1': {} + '@tanstack/store@0.7.2': {} '@tanstack/table-core@8.21.3': {} '@tanstack/virtual-core@3.13.12': {} - '@tanstack/virtual-file-routes@1.121.21': {} + '@tanstack/virtual-file-routes@1.129.7': {} '@testing-library/dom@10.4.0': dependencies: @@ -8254,15 +8236,15 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 - '@textlint/ast-node-types@14.8.4': {} + '@textlint/ast-node-types@15.2.0': {} - '@textlint/linter-formatter@14.8.4': + '@textlint/linter-formatter@15.2.0': dependencies: '@azu/format-text': 1.0.2 '@azu/style-format': 1.0.1 - '@textlint/module-interop': 14.8.4 - '@textlint/resolver': 14.8.4 - '@textlint/types': 14.8.4 + '@textlint/module-interop': 15.2.0 + '@textlint/resolver': 15.2.0 + '@textlint/types': 15.2.0 chalk: 4.1.2 debug: 4.4.1(supports-color@8.1.1) js-yaml: 3.14.1 @@ -8275,36 +8257,36 @@ snapshots: transitivePeerDependencies: - supports-color - '@textlint/module-interop@14.8.4': {} + '@textlint/module-interop@15.2.0': {} - '@textlint/resolver@14.8.4': {} + '@textlint/resolver@15.2.0': {} - '@textlint/types@14.8.4': + '@textlint/types@15.2.0': dependencies: - '@textlint/ast-node-types': 14.8.4 + '@textlint/ast-node-types': 15.2.0 '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.7 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@types/babel__template@7.4.4': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@types/babel__traverse@7.20.7': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@types/chai@5.2.2': dependencies: @@ -8441,7 +8423,7 @@ snapshots: '@types/es-aggregate-error@1.0.6': dependencies: - '@types/node': 24.0.10 + '@types/node': 20.11.25 '@types/eslint-scope@3.7.7': dependencies: @@ -8492,13 +8474,14 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.19.4': + '@types/node@20.19.9': dependencies: undici-types: 6.21.0 - '@types/node@24.0.10': + '@types/node@24.1.0': dependencies: undici-types: 7.8.0 + optional: true '@types/normalize-package-data@2.4.4': {} @@ -8527,15 +8510,15 @@ snapshots: '@types/vscode@1.96.0': {} - '@typescript-eslint/eslint-plugin@8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(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))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.35.1 - '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.35.1 - eslint: 9.30.1(jiti@2.4.2) + '@typescript-eslint/parser': 8.38.0(eslint@9.31.0(jiti@2.4.2))(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/visitor-keys': 8.38.0 + eslint: 9.31.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -8544,55 +8527,56 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.35.1(eslint@9.30.1(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)': dependencies: - '@typescript-eslint/scope-manager': 8.35.1 - '@typescript-eslint/types': 8.35.1 - '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.35.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) + '@typescript-eslint/visitor-keys': 8.38.0 debug: 4.4.1(supports-color@8.1.1) - eslint: 9.30.1(jiti@2.4.2) + eslint: 9.31.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)': + '@typescript-eslint/project-service@8.38.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) - '@typescript-eslint/types': 8.35.1 + '@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/scope-manager@8.35.1': + '@typescript-eslint/scope-manager@8.38.0': dependencies: - '@typescript-eslint/types': 8.35.1 - '@typescript-eslint/visitor-keys': 8.35.1 + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/visitor-keys': 8.38.0 - '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.8.3)': dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@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(supports-color@8.1.1) - eslint: 9.30.1(jiti@2.4.2) + eslint: 9.31.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.35.1': {} + '@typescript-eslint/types@8.38.0': {} - '@typescript-eslint/typescript-estree@8.35.1(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.38.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.35.1(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) - '@typescript-eslint/types': 8.35.1 - '@typescript-eslint/visitor-keys': 8.35.1 + '@typescript-eslint/project-service': 8.38.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(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 @@ -8603,23 +8587,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.35.1(eslint@9.30.1(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)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.35.1 - '@typescript-eslint/types': 8.35.1 - '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) - eslint: 9.30.1(jiti@2.4.2) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2)) + '@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: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.35.1': + '@typescript-eslint/visitor-keys@8.38.0': dependencies: - '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/types': 8.38.0 eslint-visitor-keys: 4.2.1 - '@typespec/ts-http-runtime@0.2.3': + '@typespec/ts-http-runtime@0.3.0': dependencies: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -8632,7 +8616,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@uiw/codemirror-extensions-basic-setup@4.23.14(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.38.0)': + '@uiw/codemirror-extensions-basic-setup@4.24.1(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1)': dependencies: '@codemirror/autocomplete': 6.18.6 '@codemirror/commands': 6.8.1 @@ -8640,16 +8624,16 @@ snapshots: '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.0 + '@codemirror/view': 6.38.1 - '@uiw/react-codemirror@4.23.14(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.0)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@uiw/react-codemirror@4.24.1(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.27.6 '@codemirror/commands': 6.8.1 '@codemirror/state': 6.5.2 '@codemirror/theme-one-dark': 6.1.2 - '@codemirror/view': 6.38.0 - '@uiw/codemirror-extensions-basic-setup': 4.23.14(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.38.0) + '@codemirror/view': 6.38.1 + '@uiw/codemirror-extensions-basic-setup': 4.24.1(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1) codemirror: 6.0.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -8661,39 +8645,39 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@3.10.2(@swc/helpers@0.5.17)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.11 - '@swc/core': 1.12.9(@swc/helpers@0.5.17) - vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + '@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.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.6.0(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': + '@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.43.1)(tsx@4.20.3)(yaml@2.8.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) - '@rolldown/pluginutils': 1.0.0-beta.19 + '@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.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitest/browser@3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.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.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(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)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) ws: 8.18.3 optionalDependencies: - playwright: 1.53.2 + playwright: 1.54.1 transitivePeerDependencies: - bufferutil - msw @@ -8715,9 +8699,9 @@ snapshots: 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.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(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)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: - '@vitest/browser': 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) transitivePeerDependencies: - supports-color @@ -8726,16 +8710,16 @@ snapshots: '@types/chai': 5.2.2 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 - chai: 5.2.0 + chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.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.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: @@ -8745,13 +8729,13 @@ snapshots: optionalDependencies: vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.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.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) '@vitest/pretty-format@3.2.3': dependencies: @@ -8870,17 +8854,17 @@ snapshots: '@vscode/vsce@3.6.0': dependencies: '@azure/identity': 4.10.2 - '@secretlint/node': 10.1.1 - '@secretlint/secretlint-formatter-sarif': 10.1.1 - '@secretlint/secretlint-rule-no-dotenv': 10.1.1 - '@secretlint/secretlint-rule-preset-recommend': 10.1.1 + '@secretlint/node': 10.2.1 + '@secretlint/secretlint-formatter-sarif': 10.2.1 + '@secretlint/secretlint-rule-no-dotenv': 10.2.1 + '@secretlint/secretlint-rule-preset-recommend': 10.2.1 '@vscode/vsce-sign': 2.0.6 azure-devops-node-api: 12.5.0 chalk: 4.1.2 - cheerio: 1.1.0 + cheerio: 1.1.2 cockatiel: 3.2.1 commander: 12.1.0 - form-data: 4.0.3 + form-data: 4.0.4 glob: 11.0.3 hosted-git-info: 4.1.0 jsonc-parser: 3.3.1 @@ -8890,7 +8874,7 @@ snapshots: minimatch: 3.1.2 parse-semver: 1.1.1 read: 1.0.7 - secretlint: 10.1.1 + secretlint: 10.2.1 semver: 7.7.2 tmp: 0.2.3 typed-rest-client: 1.8.11 @@ -8993,7 +8977,7 @@ snapshots: acorn@8.15.0: {} - agent-base@7.1.3: {} + agent-base@7.1.4: {} ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: @@ -9028,9 +9012,9 @@ snapshots: ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: + ansi-escapes@7.0.0: dependencies: - type-fest: 0.21.3 + environment: 1.1.0 ansi-regex@5.0.1: {} @@ -9058,7 +9042,7 @@ snapshots: '@swc/helpers': 0.5.17 '@types/command-line-args': 5.2.3 '@types/command-line-usage': 5.0.4 - '@types/node': 20.19.4 + '@types/node': 20.19.9 command-line-args: 6.0.1 command-line-usage: 7.0.3 flatbuffers: 24.12.23 @@ -9127,7 +9111,7 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.6): dependencies: browserslist: 4.25.1 - caniuse-lite: 1.0.30001726 + caniuse-lite: 1.0.30001727 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -9150,7 +9134,7 @@ snapshots: '@babel/core': 7.28.0 '@babel/parser': 7.28.0 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color @@ -9199,8 +9183,8 @@ snapshots: browserslist@4.25.1: dependencies: - caniuse-lite: 1.0.30001726 - electron-to-chromium: 1.5.179 + caniuse-lite: 1.0.30001727 + electron-to-chromium: 1.5.190 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) @@ -9261,11 +9245,11 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001726: {} + caniuse-lite@1.0.30001727: {} ccount@2.0.1: {} - chai@5.2.0: + chai@5.2.1: dependencies: assertion-error: 2.0.1 check-error: 2.1.1 @@ -9308,7 +9292,7 @@ snapshots: domhandler: 5.0.3 domutils: 3.2.2 - cheerio@1.1.0: + cheerio@1.1.2: dependencies: cheerio-select: 2.1.0 dom-serializer: 2.0.0 @@ -9319,7 +9303,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.11.0 + undici: 7.12.0 whatwg-mimetype: 4.0.0 chokidar@3.6.0: @@ -9379,7 +9363,7 @@ snapshots: '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.0 + '@codemirror/view': 6.38.1 color-convert@2.0.1: dependencies: @@ -9521,7 +9505,7 @@ snapshots: decamelize@4.0.0: {} - decimal.js@10.5.0: {} + decimal.js@10.6.0: {} decode-named-character-reference@1.2.0: dependencies: @@ -9636,7 +9620,7 @@ snapshots: dependencies: version-range: 4.14.0 - electron-to-chromium@1.5.179: {} + electron-to-chromium@1.5.190: {} elkjs@0.8.2: {} @@ -9670,9 +9654,7 @@ snapshots: entities@6.0.1: {} - error-ex@1.3.2: - dependencies: - is-arrayish: 0.2.1 + environment@1.1.0: {} es-abstract@1.24.0: dependencies: @@ -9767,40 +9749,41 @@ snapshots: es6-promise@3.3.1: {} - esbuild-register@3.6.0(esbuild@0.25.5): + esbuild-register@3.6.0(esbuild@0.25.8): dependencies: debug: 4.4.1(supports-color@8.1.1) - esbuild: 0.25.5 + esbuild: 0.25.8 transitivePeerDependencies: - supports-color - esbuild@0.25.5: + esbuild@0.25.8: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.5 - '@esbuild/android-arm': 0.25.5 - '@esbuild/android-arm64': 0.25.5 - '@esbuild/android-x64': 0.25.5 - '@esbuild/darwin-arm64': 0.25.5 - '@esbuild/darwin-x64': 0.25.5 - '@esbuild/freebsd-arm64': 0.25.5 - '@esbuild/freebsd-x64': 0.25.5 - '@esbuild/linux-arm': 0.25.5 - '@esbuild/linux-arm64': 0.25.5 - '@esbuild/linux-ia32': 0.25.5 - '@esbuild/linux-loong64': 0.25.5 - '@esbuild/linux-mips64el': 0.25.5 - '@esbuild/linux-ppc64': 0.25.5 - '@esbuild/linux-riscv64': 0.25.5 - '@esbuild/linux-s390x': 0.25.5 - '@esbuild/linux-x64': 0.25.5 - '@esbuild/netbsd-arm64': 0.25.5 - '@esbuild/netbsd-x64': 0.25.5 - '@esbuild/openbsd-arm64': 0.25.5 - '@esbuild/openbsd-x64': 0.25.5 - '@esbuild/sunos-x64': 0.25.5 - '@esbuild/win32-arm64': 0.25.5 - '@esbuild/win32-ia32': 0.25.5 - '@esbuild/win32-x64': 0.25.5 + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 escalade@3.2.0: {} @@ -9820,16 +9803,16 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.30.1(jiti@2.4.2): + eslint@9.31.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.3.0 - '@eslint/core': 0.14.0 + '@eslint/core': 0.15.1 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.30.1 - '@eslint/plugin-kit': 0.3.3 + '@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 @@ -9911,7 +9894,7 @@ snapshots: expand-template@2.0.3: optional: true - expect-type@1.2.1: {} + expect-type@1.2.2: {} extend@3.0.2: {} @@ -9943,9 +9926,9 @@ snapshots: dependencies: pend: 1.2.0 - fdir@6.4.6(picomatch@4.0.2): + fdir@6.4.6(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 fflate@0.8.2: {} @@ -9992,7 +9975,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.3: + form-data@4.0.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -10005,12 +9988,6 @@ snapshots: fs-constants@1.0.0: optional: true - fs-extra@10.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - fs-extra@11.3.0: dependencies: graceful-fs: 4.2.11 @@ -10236,7 +10213,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: - agent-base: 7.1.3 + agent-base: 7.1.4 debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -10245,7 +10222,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: - agent-base: 7.1.3 + agent-base: 7.1.4 debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -10276,6 +10253,8 @@ snapshots: indent-string@4.0.0: {} + index-to-position@1.1.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -10307,8 +10286,6 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - is-arrayish@0.2.1: {} - is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -10524,7 +10501,7 @@ snapshots: dependencies: cssstyle: 4.6.0 data-urls: 5.0.0 - decimal.js: 10.5.0 + decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -10557,8 +10534,6 @@ snapshots: json-parse-even-better-errors@2.3.1: {} - json-parse-even-better-errors@3.0.2: {} - json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -10690,8 +10665,6 @@ snapshots: lines-and-columns@1.2.4: {} - lines-and-columns@2.0.4: {} - linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 @@ -10785,7 +10758,7 @@ snapshots: magicast@0.3.5: dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 source-map-js: 1.2.1 make-dir@4.0.0: @@ -11162,10 +11135,10 @@ snapshots: node-releases@2.0.19: {} - node-sarif-builder@2.0.3: + node-sarif-builder@3.2.0: dependencies: '@types/sarif': 2.1.7 - fs-extra: 10.1.0 + fs-extra: 11.3.0 normalize-package-data@6.0.2: dependencies: @@ -11247,12 +11220,12 @@ snapshots: dependencies: mimic-function: 5.0.1 - open@10.1.2: + open@10.2.0: dependencies: default-browser: 5.2.1 define-lazy-prop: 3.0.0 is-inside-container: 1.0.0 - is-wsl: 3.1.0 + wsl-utils: 0.1.0 open@8.4.2: dependencies: @@ -11317,7 +11290,7 @@ snapshots: string-argv: 0.3.2 tsconfck: 2.1.2(typescript@5.8.3) typedoc: 0.28.7(typescript@5.8.3) - typedoc-plugin-markdown: 4.7.0(typedoc@0.28.7(typescript@5.8.3)) + typedoc-plugin-markdown: 4.7.1(typedoc@0.28.7(typescript@5.8.3)) typescript: 5.8.3 transitivePeerDependencies: - encoding @@ -11366,13 +11339,11 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 - parse-json@7.1.1: + parse-json@8.3.0: dependencies: '@babel/code-frame': 7.27.1 - error-ex: 1.3.2 - json-parse-even-better-errors: 3.0.2 - lines-and-columns: 2.0.4 - type-fest: 3.13.1 + index-to-position: 1.1.0 + type-fest: 4.41.0 parse-semver@1.1.1: dependencies: @@ -11425,17 +11396,17 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.2: {} + picomatch@4.0.3: {} pify@2.3.0: {} pirates@4.0.7: {} - playwright-core@1.53.2: {} + playwright-core@1.54.1: {} - playwright@1.53.2: + playwright@1.54.1: dependencies: - playwright-core: 1.53.2 + playwright-core: 1.54.1 optionalDependencies: fsevents: 2.3.2 @@ -11566,7 +11537,7 @@ snapshots: dependencies: dnd-core: 16.0.1 - react-dnd@16.0.1(@types/node@24.0.10)(@types/react@18.3.23)(react@18.3.1): + react-dnd@16.0.1(@types/node@24.1.0)(@types/react@18.3.23)(react@18.3.1): dependencies: '@react-dnd/invariant': 4.0.2 '@react-dnd/shallowequal': 4.0.2 @@ -11575,7 +11546,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 optionalDependencies: - '@types/node': 24.0.10 + '@types/node': 24.1.0 '@types/react': 18.3.23 react-docgen-typescript@2.4.0(typescript@5.8.3): @@ -11586,7 +11557,7 @@ snapshots: dependencies: '@babel/core': 7.28.0 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.7 '@types/doctrine': 0.0.9 @@ -11646,7 +11617,7 @@ snapshots: optionalDependencies: '@types/react': 18.3.23 - react-router@7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router@7.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: cookie: 1.0.2 react: 18.3.1 @@ -11690,12 +11661,13 @@ snapshots: dependencies: pify: 2.3.0 - read-pkg@8.1.0: + read-pkg@9.0.1: dependencies: '@types/normalize-package-data': 2.4.4 normalize-package-data: 6.0.2 - parse-json: 7.1.1 + parse-json: 8.3.0 type-fest: 4.41.0 + unicorn-magic: 0.1.0 read@1.0.7: dependencies: @@ -11801,30 +11773,30 @@ snapshots: reusify@1.1.0: {} - rollup@4.44.1: + rollup@4.45.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.44.1 - '@rollup/rollup-android-arm64': 4.44.1 - '@rollup/rollup-darwin-arm64': 4.44.1 - '@rollup/rollup-darwin-x64': 4.44.1 - '@rollup/rollup-freebsd-arm64': 4.44.1 - '@rollup/rollup-freebsd-x64': 4.44.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.44.1 - '@rollup/rollup-linux-arm-musleabihf': 4.44.1 - '@rollup/rollup-linux-arm64-gnu': 4.44.1 - '@rollup/rollup-linux-arm64-musl': 4.44.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.44.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.44.1 - '@rollup/rollup-linux-riscv64-gnu': 4.44.1 - '@rollup/rollup-linux-riscv64-musl': 4.44.1 - '@rollup/rollup-linux-s390x-gnu': 4.44.1 - '@rollup/rollup-linux-x64-gnu': 4.44.1 - '@rollup/rollup-linux-x64-musl': 4.44.1 - '@rollup/rollup-win32-arm64-msvc': 4.44.1 - '@rollup/rollup-win32-ia32-msvc': 4.44.1 - '@rollup/rollup-win32-x64-msvc': 4.44.1 + '@rollup/rollup-android-arm-eabi': 4.45.1 + '@rollup/rollup-android-arm64': 4.45.1 + '@rollup/rollup-darwin-arm64': 4.45.1 + '@rollup/rollup-darwin-x64': 4.45.1 + '@rollup/rollup-freebsd-arm64': 4.45.1 + '@rollup/rollup-freebsd-x64': 4.45.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.45.1 + '@rollup/rollup-linux-arm-musleabihf': 4.45.1 + '@rollup/rollup-linux-arm64-gnu': 4.45.1 + '@rollup/rollup-linux-arm64-musl': 4.45.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.45.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-musl': 4.45.1 + '@rollup/rollup-linux-s390x-gnu': 4.45.1 + '@rollup/rollup-linux-x64-gnu': 4.45.1 + '@rollup/rollup-linux-x64-musl': 4.45.1 + '@rollup/rollup-win32-arm64-msvc': 4.45.1 + '@rollup/rollup-win32-ia32-msvc': 4.45.1 + '@rollup/rollup-win32-x64-msvc': 4.45.1 fsevents: 2.3.3 rrweb-cssom@0.8.0: {} @@ -11879,15 +11851,15 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) - secretlint@10.1.1: + secretlint@10.2.1: dependencies: - '@secretlint/config-creator': 10.1.1 - '@secretlint/formatter': 10.1.1 - '@secretlint/node': 10.1.1 - '@secretlint/profiler': 10.1.1 + '@secretlint/config-creator': 10.2.1 + '@secretlint/formatter': 10.2.1 + '@secretlint/node': 10.2.1 + '@secretlint/profiler': 10.2.1 debug: 4.4.1(supports-color@8.1.1) globby: 14.1.0 - read-pkg: 8.1.0 + read-pkg: 9.0.1 transitivePeerDependencies: - supports-color @@ -12079,7 +12051,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.6.2): + storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.6.3 @@ -12087,8 +12059,8 @@ snapshots: '@vitest/expect': 3.2.4 '@vitest/spy': 3.2.4 better-opn: 3.0.2 - esbuild: 0.25.5 - esbuild-register: 3.6.0(esbuild@0.25.5) + esbuild: 0.25.8 + esbuild-register: 3.6.0(esbuild@0.25.8) recast: 0.23.11 semver: 7.7.2 ws: 8.18.3 @@ -12220,7 +12192,7 @@ snapshots: supports-color@9.4.0: {} - supports-hyperlinks@2.3.0: + supports-hyperlinks@3.2.0: dependencies: has-flag: 4.0.0 supports-color: 7.2.0 @@ -12317,21 +12289,21 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 - terminal-link@2.1.1: + terminal-link@4.0.0: dependencies: - ansi-escapes: 4.3.2 - supports-hyperlinks: 2.3.0 + ansi-escapes: 7.0.0 + supports-hyperlinks: 3.2.0 - terser-webpack-plugin@5.3.14(esbuild@0.25.5)(webpack@5.99.8(esbuild@0.25.5)): + terser-webpack-plugin@5.3.14(esbuild@0.25.8)(webpack@5.99.8(esbuild@0.25.8)): dependencies: '@jridgewell/trace-mapping': 0.3.29 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.43.1 - webpack: 5.99.8(esbuild@0.25.5) + webpack: 5.99.8(esbuild@0.25.8) optionalDependencies: - esbuild: 0.25.5 + esbuild: 0.25.8 terser@5.43.1: dependencies: @@ -12358,11 +12330,11 @@ snapshots: dependencies: editions: 6.21.0 - thememirror@2.0.1(@codemirror/language@6.11.2)(@codemirror/state@6.5.2)(@codemirror/view@6.38.0): + thememirror@2.0.1(@codemirror/language@6.11.2)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1): dependencies: '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.0 + '@codemirror/view': 6.38.1 thenify-all@1.6.0: dependencies: @@ -12382,8 +12354,8 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 tinypool@1.1.1: {} @@ -12427,7 +12399,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-loader@9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.5)): + ts-loader@9.5.2(typescript@5.8.3)(webpack@5.99.8(esbuild@0.25.8)): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.2 @@ -12435,7 +12407,7 @@ snapshots: semver: 7.7.2 source-map: 0.7.4 typescript: 5.8.3 - webpack: 5.99.8(esbuild@0.25.5) + webpack: 5.99.8(esbuild@0.25.8) tsconfck@2.1.2(typescript@5.8.3): optionalDependencies: @@ -12453,7 +12425,7 @@ snapshots: tsx@4.20.3: dependencies: - esbuild: 0.25.5 + esbuild: 0.25.8 get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 @@ -12469,10 +12441,6 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.21.3: {} - - type-fest@3.13.1: {} - type-fest@4.41.0: {} typed-array-buffer@1.0.3: @@ -12514,25 +12482,26 @@ snapshots: tunnel: 0.0.6 underscore: 1.13.7 - typedoc-plugin-markdown@4.7.0(typedoc@0.28.7(typescript@5.8.3)): + typedoc-plugin-markdown@4.7.1(typedoc@0.28.7(typescript@5.8.3)): dependencies: typedoc: 0.28.7(typescript@5.8.3) typedoc@0.28.7(typescript@5.8.3): dependencies: - '@gerrit0/mini-shiki': 3.7.0 + '@gerrit0/mini-shiki': 3.8.1 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 typescript: 5.8.3 yaml: 2.8.0 - typescript-eslint@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.30.1(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))(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: 5.8.3 transitivePeerDependencies: - supports-color @@ -12556,9 +12525,10 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.8.0: {} + undici-types@7.8.0: + optional: true - undici@7.11.0: {} + undici@7.12.0: {} unicorn-magic@0.1.0: {} @@ -12607,7 +12577,7 @@ snapshots: unplugin@2.3.5: dependencies: acorn: 8.15.0 - picomatch: 4.0.2 + picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 update-browserslist-db@1.1.3(browserslist@4.25.1): @@ -12695,13 +12665,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 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@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -12716,17 +12686,17 @@ snapshots: - tsx - yaml - vite-plugin-css-injected-by-js@3.5.2(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)): dependencies: - vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: - esbuild: 0.25.5 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + esbuild: 0.25.8 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.44.1 + rollup: 4.45.1 tinyglobby: 0.2.14 optionalDependencies: '@types/node': 20.11.25 @@ -12737,16 +12707,16 @@ snapshots: tsx: 4.20.3 yaml: 2.8.0 - vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: - esbuild: 0.25.5 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + esbuild: 0.25.8 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.44.1 + rollup: 4.45.1 tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 24.0.10 + '@types/node': 24.1.0 fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 @@ -12764,12 +12734,12 @@ snapshots: '@vitest/snapshot': 3.2.4 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 - chai: 5.2.0 + chai: 5.2.1 debug: 4.4.1(supports-color@8.1.1) - expect-type: 1.2.1 + expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 - picomatch: 4.0.2 + picomatch: 4.0.3 std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 @@ -12798,35 +12768,35 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(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)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.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.0 + chai: 5.2.1 debug: 4.4.1(supports-color@8.1.1) - expect-type: 1.2.1 + expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 - picomatch: 4.0.2 + 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.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 24.0.10 - '@vitest/browser': 3.2.3(playwright@1.53.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: @@ -12883,7 +12853,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.99.8(esbuild@0.25.5): + webpack@5.99.8(esbuild@0.25.8): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -12906,7 +12876,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(esbuild@0.25.5)(webpack@5.99.8(esbuild@0.25.5)) + terser-webpack-plugin: 5.3.14(esbuild@0.25.8)(webpack@5.99.8(esbuild@0.25.8)) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -13002,6 +12972,10 @@ snapshots: ws@8.18.3: {} + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + xml-name-validator@5.0.0: {} xml2js@0.5.0: @@ -13069,7 +13043,7 @@ snapshots: yocto-queue@1.2.1: {} - zod@3.25.71: {} + zod@3.25.76: {} zustand@4.5.7(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1): dependencies: diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 9628ef662e..c7285ad7c7 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -138,11 +138,11 @@ "fs-extra": "^11.3.0", "vscode-jsonrpc": "^8.2.1", "vscode-languageclient": "^9.0.1", - "zod": "^3.25.71" + "zod": "^3.25.76" }, "devDependencies": { - "@eslint/js": "^9.30.1", - "@playwright/test": "^1.53.2", + "@eslint/js": "^9.31.0", + "@playwright/test": "^1.54.1", "@types/mocha": "^10.0.10", "@types/node": "20.11.25", "@types/vscode": "1.96.0", @@ -150,11 +150,11 @@ "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.6.0", - "esbuild": "^0.25.5", - "eslint": "^9.30.1", + "esbuild": "^0.25.8", + "eslint": "^9.31.0", "ts-loader": "^9.5.2", "typescript": "^5.8.3", - "typescript-eslint": "^8.35.1", + "typescript-eslint": "^8.38.0", "vitest": "^3.2.4", "yaml": "^2.8.0" } diff --git a/vscode/react/package.json b/vscode/react/package.json index c59ab3d35e..bafcc0c556 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -15,47 +15,47 @@ "build-storybook": "storybook build" }, "dependencies": { - "@headlessui/react": "^2.2.4", + "@headlessui/react": "^2.2.5", "@heroicons/react": "^2.2.0", "@radix-ui/react-select": "^2.2.5", "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/vite": "^4.1.11", - "@tanstack/react-query": "^5.81.5", - "@tanstack/react-router": "^1.124.0", - "@tanstack/react-router-devtools": "^1.124.0", + "@tanstack/react-query": "^5.83.0", + "@tanstack/react-router": "^1.129.8", + "@tanstack/react-router-devtools": "^1.129.8", "@tanstack/react-virtual": "^3.13.12", - "@tanstack/router-plugin": "^1.124.0", + "@tanstack/router-plugin": "^1.129.8", "apache-arrow": "^19.0.1", "clsx": "^2.1.1", "elkjs": "^0.8.2", "orval": "^7.10.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router": "^7.6.3", + "react-router": "^7.7.0", "reactflow": "^11.11.4", "tailwindcss": "^4.1.11", "vscode-uri": "^3.1.0" }, "devDependencies": { "@chromatic-com/storybook": "^4.0.1", - "@storybook/addon-a11y": "^9.0.15", - "@storybook/addon-docs": "^9.0.15", - "@storybook/addon-onboarding": "^9.0.15", - "@storybook/addon-vitest": "^9.0.15", - "@storybook/react-vite": "^9.0.15", + "@storybook/addon-a11y": "^9.0.18", + "@storybook/addon-docs": "^9.0.18", + "@storybook/addon-onboarding": "^9.0.18", + "@storybook/addon-vitest": "^9.0.18", + "@storybook/react-vite": "^9.0.18", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.3.0", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react": "^4.6.0", + "@vitejs/plugin-react": "^4.7.0", + "@vitest/browser": "3.2.3", + "@vitest/coverage-v8": "3.2.3", "jsdom": "^26.1.0", - "storybook": "^9.0.15", + "playwright": "^1.54.1", + "storybook": "^9.0.18", "typescript": "^5.8.3", "vite": "^6.3.5", "vitest": "^3.2.4", - "web-vitals": "^4.2.4", - "@vitest/browser": "3.2.3", - "playwright": "^1.53.2", - "@vitest/coverage-v8": "3.2.3" + "web-vitals": "^4.2.4" } } diff --git a/web/client/package.json b/web/client/package.json index 987bf6c70c..b6c99153f9 100644 --- a/web/client/package.json +++ b/web/client/package.json @@ -22,18 +22,18 @@ "@codemirror/language": "^6.11.2", "@codemirror/legacy-modes": "^6.5.1", "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.38.0", - "@headlessui/react": "^2.2.4", + "@codemirror/view": "^6.38.1", + "@headlessui/react": "^2.2.5", "@heroicons/react": "^2.2.0", - "@lit/react": "^1.0.7", + "@lit/react": "^1.0.8", "@radix-ui/react-context-menu": "^2.2.15", "@radix-ui/react-select": "^2.2.5", "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^5.81.5", + "@tanstack/react-query": "^5.83.0", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.12", "@uidotdev/usehooks": "^2.4.1", - "@uiw/react-codemirror": "^4.23.14", + "@uiw/react-codemirror": "^4.24.1", "apache-arrow": "^19.0.1", "clsx": "^2.1.1", "diff": "^8.0.2", @@ -44,37 +44,37 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", - "react-router": "^7.6.3", + "react-router": "^7.7.0", "react-split": "^2.0.14", "reactflow": "^11.11.4", "thememirror": "^2.0.1", "zustand": "^5.0.6" }, "devDependencies": { - "@eslint/js": "^9.30.1", - "@playwright/test": "^1.53.2", - "@swc/core": "^1.12.9", + "@eslint/js": "^9.31.0", + "@playwright/test": "^1.54.1", + "@swc/core": "^1.13.2", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/pluralize": "^0.0.33", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react-swc": "^3.10.2", + "@vitejs/plugin-react-swc": "^3.11.0", "ajv": "^8.17.1", "autoprefixer": "^10.4.21", - "eslint": "^9.30.1", + "eslint": "^9.31.0", "jsdom": "^26.1.0", "orval": "^7.10.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", - "typescript-eslint": "^8.35.1", + "typescript-eslint": "^8.38.0", "vite": "^6.3.5", "vite-plugin-css-injected-by-js": "^3.5.2", "vitest": "^3.2.4" }, "optionalDependencies": { - "@swc/core-linux-x64-gnu": "^1.12.9" + "@swc/core-linux-x64-gnu": "^1.13.2" } } From cd8c153e159128746da9de63e3f3d2c7482b34d0 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:27:46 +0100 Subject: [PATCH 0598/1056] refactor(vscode): share open problems view (#5004) --- vscode/extension/tests/broken_project.spec.ts | 5 ++-- vscode/extension/tests/diagnostics.spec.ts | 26 +++++++------------ vscode/extension/tests/utils.ts | 6 +++++ 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index af7b022545..8de58c1853 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -5,7 +5,7 @@ import path from 'path' import { openLineageView, openServerPage, - runCommand, + openProblemsView, saveFile, SUSHI_SOURCE_PATH, } from './utils' @@ -277,8 +277,7 @@ test('bad model block, then fixed', async ({ page, sharedCodeServer }) => { // Wait for the error to appear await page.waitForSelector('text=Error creating context') - // Open the problems view - await runCommand(page, 'View: Focus Problems') + await openProblemsView(page) // Assert error is present in the problems view const errorElement = page diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts index a520cabcf7..fcac5cecf0 100644 --- a/vscode/extension/tests/diagnostics.spec.ts +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -2,7 +2,7 @@ import { expect, test } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { openServerPage, runCommand, SUSHI_SOURCE_PATH } from './utils' +import { openProblemsView, openServerPage, SUSHI_SOURCE_PATH } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' import { execAsync } from '../src/utilities/exec' import yaml from 'yaml' @@ -36,8 +36,7 @@ test('Workspace diagnostics show up in the diagnostics panel', async ({ .locator('a') .click() - // Open problems panel - await runCommand(page, 'View: Focus Problems') + await openProblemsView(page) await page.waitForSelector('text=problems') await page.waitForSelector('text=All models should have an owner') @@ -82,8 +81,7 @@ test.describe('Bad config.py/config.yaml file issues', () => { // Wait for the error to appear await page.waitForSelector('text=Error creating context') - // Open the problems view - await runCommand(page, 'View: Focus Problems') + await openProblemsView(page) // Asser that the error is present in the problems view await page @@ -125,8 +123,7 @@ test.describe('Bad config.py/config.yaml file issues', () => { // Wait for the error to appear await page.waitForSelector('text=Error creating context') - // Open the problems view - await runCommand(page, 'View: Focus Problems') + await openProblemsView(page) // Asser that the error is present in the problems view await page @@ -164,8 +161,7 @@ test.describe('Bad config.py/config.yaml file issues', () => { // Expect the error to appear await page.waitForSelector('text=Error creating context') - // Open the problems view - await runCommand(page, 'View: Focus Problems') + await openProblemsView(page) // Assert that the error is present in the problems view const errorElement = page @@ -200,8 +196,7 @@ test.describe('Bad config.py/config.yaml file issues', () => { // Expect the error to appear await page.waitForSelector('text=Error creating context') - // Open the problems view - await runCommand(page, 'View: Focus Problems') + await openProblemsView(page) // Assert that the error is present in the problems view const errorElement = page.getByText('Failed to load config file:').first() @@ -242,8 +237,7 @@ test.describe('Diagnostics for bad SQLMesh models', () => { // Wait for the error to appear await page.waitForSelector('text=Error creating context') - // Open the problems view - await runCommand(page, 'View: Focus Problems') + await openProblemsView(page) // Asser that the error is present in the problems view await page @@ -284,8 +278,7 @@ test.describe('Diagnostics for bad SQLMesh models', () => { // Wait for the error to appear await page.waitForSelector('text=Error creating context') - // Open the problems view - await runCommand(page, 'View: Focus Problems') + await openProblemsView(page) // Assert error is present in the problems view const errorElement = page @@ -331,8 +324,7 @@ test.describe('Diagnostics for bad audits', () => { // Wait for the error to appear await page.waitForSelector('text=Error creating context') - // Open the problems view - await runCommand(page, 'View: Focus Problems') + await openProblemsView(page) // Assert that the error is present in the problems view const errorElement = page diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index e783ae268e..951741bdd1 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -87,6 +87,12 @@ export const pipInstall = async ( export const openLineageView = async (page: Page) => await runCommand(page, 'Lineage: Focus On View') +/** + * Open the problems/diagnostics view in the given window. + */ +export const openProblemsView = async (page: Page) => + await runCommand(page, 'View: Focus Problems') + /** * Restart the SQLMesh servers */ From 969c9cccfce5ea18fbc0845bdf4ecccf2f439467 Mon Sep 17 00:00:00 2001 From: David Dai Date: Wed, 23 Jul 2025 15:42:55 -0700 Subject: [PATCH 0599/1056] chore(docs): add Tobiko Cloud upgrade documentation (#4997) Co-authored-by: Trey Spiller <1831878+treysp@users.noreply.github.com> --- .../features/upgrade/upgrade-ui-available.png | Bin 0 -> 119388 bytes .../upgrade/upgrade-ui-custom-version.png | Bin 0 -> 129804 bytes .../features/upgrade/upgrade-ui-latest.png | Bin 0 -> 236314 bytes .../features/upgrade/upgrade-ui-progress.png | Bin 0 -> 18970 bytes .../upgrade/upgrade-ui-up-to-date.png | Bin 0 -> 57450 bytes docs/cloud/features/upgrades.md | 75 ++++++++++++++++++ mkdocs.yml | 9 ++- 7 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 docs/cloud/features/upgrade/upgrade-ui-available.png create mode 100644 docs/cloud/features/upgrade/upgrade-ui-custom-version.png create mode 100644 docs/cloud/features/upgrade/upgrade-ui-latest.png create mode 100644 docs/cloud/features/upgrade/upgrade-ui-progress.png create mode 100644 docs/cloud/features/upgrade/upgrade-ui-up-to-date.png create mode 100644 docs/cloud/features/upgrades.md diff --git a/docs/cloud/features/upgrade/upgrade-ui-available.png b/docs/cloud/features/upgrade/upgrade-ui-available.png new file mode 100644 index 0000000000000000000000000000000000000000..a9ea90fa08669f8e3dedfa77bc67520196c8e416 GIT binary patch literal 119388 zcmbTe2UJr{7dEP5Z`cr(rqV$`L_|Om6(K0SOO1kbA@rIe7J5ss9C+(Z+PHfC;QVUk2G8lE{<@9YS=M1b`Xv{y?tqM@IdWcV9aMc^r~ym)n^7WOdWPvtXm zkGmhSs$ebpAW-!Xd#qdrn^)vaG1PV6-FpY<=mvgl18P(U_uNK~GJsJMxQz&iTrKSH;^I81b zA!jW9ndO=V8wl>B1qotR4vxC-Hve6j{CkmqlR(m@u_!Qq$0lDLnW0hcLZrSsMxS(D zD^CY~xhUrMeD)>*b-Zr+I;a%o7ezW{wNy!-la?z*w9D`9qBo&5bXB2kCi`6C7J50S zqM|}?cw_{@xwIrVd_?)JbYeQge7D7-nUbSdI^D89 zH=JTc_UozGTx7b;`oV8%dgQKg0o@X5e5(sc)?^~t;^v_a z;<)%rQs~vsjg{m_TbJltTUypu&dhd`(I>+!9SNh7clfkOc4ShRMVDFe>ydO+K}>DYgkskwHR1bC^NvIiU6xkX>STLLv?y$H8Uzv*ZDn{(_4BnHF6TQtHPuTT zz0ewTZs4}IK!w{==MVDF=}(yRH*^0CQ~!(}>U*^S_g&<-tL3nZN=|EPZ7z_KJDbOF zYFKfEmO5O<;}7vycK8O@zdNAwx#;oD9becf&BnrIgJBLZ@6&=BJ%Q(UWTlS77WDBm z>B5-8_WT#JmW8blQ1wc)iIC7kz3Jxp`3thSKl0`lUhg35HTB{;>HYLX~Ub)msx; zZ1l`>ls;=votW3i&zaeBwdLrmqCJk#ScO|@g4yHbn1U&;rem)x8N5X=HuHG|`H!LM z9_Q6eG%?smaB?62i47&Q*z?M8~_k?zs&owa2g z6nNOVOHchXabWhEpc_*sw8?Z6VOVgyZXF-Sl;Cx{J7>tbGj6LdFHI@>!ES~b)U#Jv z2_%$QW{;nbfuY`;%zp4b1M%2t2#hJNK+$T2mNR@}K?qag=&o*b9`5VPSdH}N(nxJy zHF8yjOlcL6P%sD>P;^st`!r2@( zB&A!)cQ(my3EaH?s$;lXa8f?QNKRY4Os#42eahF3twe_~a8{5jRd8jgfc=sHty~Y< zW7hl0VTVIfzL>ydwKwHf*l#BL5d7ZSp~aMtxR(>|nxD zxV>Ds@~@r4nSb{Vuyd4;yBTRx!%k=1h75X1P4^KYva+c*u@GiOq*>rSHz`7o$AMHI zY(D2?grMG4r}sk-;9AIod~7hIqdPCS$ExT-5SCAp<%x9gWtGa3se?Mf=o=4N4n8V> z8+f?N?SK_5mF^|_fYai|P)8f&4Gn4;F08opC-%*N_WG=6q7KMV5uukp?d~5FVbAHY zjJp>`PlTZyUodtP;;4~{_fraYt z#Do0*7c*cc$}F2U2Mz1^z*15MS@7g`|C+c8m#)svBH_kpvv|}QE7Gx9WwbEUx|#_e zgD)0x5_4ZI&Cs~}e0E-1UeyXHHA^dxfi7oDF%0A0Dxq-xYoYbfS>ZCo&OM~Mj*bqe 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­2KZB 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 literal 0 HcmV?d00001 diff --git a/docs/cloud/features/upgrade/upgrade-ui-custom-version.png b/docs/cloud/features/upgrade/upgrade-ui-custom-version.png new file mode 100644 index 0000000000000000000000000000000000000000..810861a741330cc9643a009e63a09f14715c59a1 GIT binary patch literal 129804 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 literal 0 HcmV?d00001 diff --git a/docs/cloud/features/upgrade/upgrade-ui-latest.png b/docs/cloud/features/upgrade/upgrade-ui-latest.png new file mode 100644 index 0000000000000000000000000000000000000000..c6e34cb5343d6852800da3f9ec0a45b74fd532a3 GIT binary patch literal 236314 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

    t6m8StyNADtV^FcwlH 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

    {&VG8I)wdu#_C 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 NSL>=~;YIuV{|}$;H}U`g literal 0 HcmV?d00001 diff --git a/docs/cloud/features/upgrade/upgrade-ui-progress.png b/docs/cloud/features/upgrade/upgrade-ui-progress.png new file mode 100644 index 0000000000000000000000000000000000000000..78282628eccf7bbd90cedca11ae36dd9b0cae5f8 GIT binary patch literal 18970 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 Q{Db6UHC<5g!)NdQFJN%wApigX literal 0 HcmV?d00001 diff --git a/docs/cloud/features/upgrade/upgrade-ui-up-to-date.png b/docs/cloud/features/upgrade/upgrade-ui-up-to-date.png new file mode 100644 index 0000000000000000000000000000000000000000..e044ad1d8d4b6c1b4565bb8ee0e8eea765bc1c4c GIT binary patch literal 57450 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 Upgrade` in the Tobiko Cloud UI to determine whether a new version of Tobiko Cloud is available for your project. + +If your project is already up to date, you will see a grey message: + +![Tobiko Cloud Upgrade Already Up-to-Date](./upgrade/upgrade-ui-up-to-date.png) + +If a new version is available for your project, the page will include a notification box, version, and blue Upgrade Now button: + +![Tobiko Cloud Upgrade Page](./upgrade/upgrade-ui-available.png) + +## Upgrading a project + +On the Upgrade page, you can choose to upgrade to the latest version or specify a custom version. + +!!! info "Upgrade Permissions" + Only users with Tobiko Cloud `Admin` permissions can perform upgrades. + +!!! danger "Upgrade Side Effects" + The upgrade process may take a few minutes to complete. During this time, your Tobiko Cloud project will be unavailable. + + Any in-progress plans and runs will be aborted: + + - Aborted plans will be stopped, and you must **manually** start them again. + - Aborted runs will be automatically resumed shortly after the upgrade completes. + + To avoid unexpected interruptions, please notify your team before starting the upgrade. + +### Latest Version + +Click the **Upgrade Now** button and confirm to begin upgrading your project to the latest version. + +![Tobiko Cloud Upgrade Page](./upgrade/upgrade-ui-latest.png) + +### Custom Version + +We recommend upgrading your Tobiko Cloud project to the latest version, but you may prefer to upgrade to a specific version. + +For example, consider a team that has separate staging and production Tobiko Cloud projects. They upgrade the staging project first, run tests, and only upgrade the production project after verifying that staging works as expected. + +If a new version of Tobiko Cloud is released during this testing period, the latest available version will not match the version tested in staging. The team can specify a custom Tobiko Cloud version to upgrade production to the specific version that was already tested in staging. + +To specify a custom version, select the **Custom** tab on the Upgrade page and enter your desired version in the text box. + +![Tobiko Cloud Upgrade Custom Version](./upgrade/upgrade-ui-custom-version.png) + +Make sure you are entering a valid custom version by: + + - Entering the custom version **without** the leading `v` + - Confirming that the version is valid and later than the current version of the project + +If your custom version is not valid, Tobiko Cloud will display an error message. + +After entering the custom version, click the **Upgrade Now** button and confirm to begin the upgrade process. + +## Upgrade Progress + +Tobiko Cloud will display a progress page while the upgrade is in progress: + +![Tobiko Cloud Upgrade Progress](./upgrade/upgrade-ui-progress.png) + +Once the upgrade is complete, Tobiko Cloud will automatically redirect you back to your upgraded project. + +## Upgrade Support + +If you encounter an issue during the upgrade process, please [report an incident](./incident_reporting.md). Our support team will follow up as soon as possible. + +For the quickest response, we recommend upgrading Monday through Friday between 9am and 5pm PST. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 0ffbcde316..34156b1b66 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -111,8 +111,11 @@ nav: - "Getting Started": cloud/tcloud_getting_started.md - Cloud Features: - "Alerts & Notifications": cloud/features/alerts_notifications.md - - cloud/features/debugger_view.md - cloud/features/data_catalog.md + - cloud/features/debugger_view.md + - Maintenance: + - cloud/features/incident_reporting.md + - cloud/features/upgrades.md - Scheduler: - "Cloud": cloud/features/scheduler/scheduler.md - "Cloud Hybrid Deployments": @@ -124,8 +127,8 @@ nav: - Security: - cloud/features/security/security.md - cloud/features/security/single_sign_on.md - - cloud/features/incident_reporting.md - - cloud/features/xdb_diffing.md + - Tools: + - cloud/features/xdb_diffing.md # - Observability: # - cloud/features/observability/overview.md # - cloud/features/observability/model_freshness.md From 405155db717158e6d6f1629a4143c3c7e01839af Mon Sep 17 00:00:00 2001 From: Rex Ledesma Date: Thu, 24 Jul 2025 02:07:28 -0400 Subject: [PATCH 0600/1056] chore: update pygithub version (#5008) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 180b86e93b..08926c2ab2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,7 @@ dbt = ["dbt-core<2"] dlt = ["dlt"] duckdb = [] gcppostgres = ["cloud-sql-python-connector[pg8000]>=1.8.0"] -github = ["PyGithub~=2.5.0"] +github = ["PyGithub>=2.6.0"] llm = ["langchain", "openai"] motherduck = ["duckdb>=1.2.0"] mssql = ["pymssql"] From afb7907331243a0a1b447e4506128d8979ab2171 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:13:24 +0100 Subject: [PATCH 0601/1056] chore(vscode): add test for quickfix (#4958) --- vscode/extension/tests/quickfix.spec.ts | 80 +++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 vscode/extension/tests/quickfix.spec.ts diff --git a/vscode/extension/tests/quickfix.spec.ts b/vscode/extension/tests/quickfix.spec.ts new file mode 100644 index 0000000000..7023257540 --- /dev/null +++ b/vscode/extension/tests/quickfix.spec.ts @@ -0,0 +1,80 @@ +import fs from 'fs-extra' +import path from 'path' +import os from 'os' +import { openProblemsView, SUSHI_SOURCE_PATH } from './utils' +import { test, expect } from './fixtures' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' + +test('noselectstar quickfix', async ({ page, sharedCodeServer }) => { + // Base test setup + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + // Override the settings for the linter + const configPath = path.join(tempDir, 'config.py') + const read = await fs.readFile(configPath, 'utf8') + // Replace linter to be on + const replaced = read.replace('enabled=False', 'enabled=True') + const replacedTheOtherRules = replaced.replace( + `rules=[ + "ambiguousorinvalidcolumn", + "invalidselectstarexpansion", + "noselectstar", + "nomissingaudits", + "nomissingowner", + ],`, + `rules=[ + "noselectstar", + ], +`, + ) + await fs.writeFile(configPath, replacedTheOtherRules) + // Replace the file to cause the error + const modelPath = path.join(tempDir, 'models', 'latest_order.sql') + const readModel = await fs.readFile(modelPath, 'utf8') + // Replace the specific select with the select star + const modelReplaced = readModel.replace( + 'SELECT id, customer_id, start_ts, end_ts, event_date', + 'SELECT *', + ) + await fs.writeFile(modelPath, modelReplaced) + + // Open the code server with the specified directory + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open the file with the linter issue + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) + .locator('a') + .click() + + await page.waitForSelector('text=Loaded SQLMesh context') + + await openProblemsView(page) + + await page.getByRole('button', { name: 'Show fixes' }).click() + await page + .getByRole('menuitem', { name: 'Replace SELECT * with' }) + .first() + .click() + + // Wait for the quick fix to be applied + await page.waitForTimeout(2_000) + + // Assert that the model no longer contains SELECT * but SELECT id, customer_id, waiter_id, start_ts, end_ts, event_date + const readUpdatedFile = (await fs.readFile(modelPath)).toString('utf8') + expect(readUpdatedFile).not.toContain('SELECT *') + expect(readUpdatedFile).toContain( + 'SELECT id, customer_id, waiter_id, start_ts, end_ts, event_date', + ) +}) From 95b1f6e0d7cd74dcde8a1d3aa087279aab0bc8a1 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:49:23 +0100 Subject: [PATCH 0602/1056] feat(vscode): startup extension on external models files (#5012) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- vscode/extension/package.json | 3 +- vscode/extension/src/lsp/lsp.ts | 12 +++- .../extension/tests/external_models.spec.ts | 68 +++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 vscode/extension/tests/external_models.spec.ts diff --git a/vscode/extension/package.json b/vscode/extension/package.json index c7285ad7c7..1db45abcf4 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -18,7 +18,8 @@ ], "activationEvents": [ "onLanguage:sql", - "onLanguage:python" + "onLanguage:python", + "onLanguage:yaml" ], "extensionKind": [ "workspace" diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index ccf3981eb2..8195876f48 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -96,7 +96,17 @@ export class LSPClient implements Disposable { }, } const clientOptions: LanguageClientOptions = { - documentSelector: [{ scheme: 'file', pattern: `**/*.sql` }], + documentSelector: [ + { scheme: 'file', pattern: `**/*.sql` }, + { + scheme: 'file', + pattern: '**/external_models.yaml', + }, + { + scheme: 'file', + pattern: '**/external_models.yml', + }, + ], diagnosticCollectionName: 'sqlmesh', outputChannel: outputChannel, } diff --git a/vscode/extension/tests/external_models.spec.ts b/vscode/extension/tests/external_models.spec.ts new file mode 100644 index 0000000000..dc087e836a --- /dev/null +++ b/vscode/extension/tests/external_models.spec.ts @@ -0,0 +1,68 @@ +import os from 'os' +import { openServerPage, SUSHI_SOURCE_PATH } from './utils' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' +import { test, expect } from './fixtures' +import fs from 'fs-extra' +import path from 'path' + +test.describe('External model files trigger lsp', () => { + test('external_models.yaml', async ({ page, sharedCodeServer }) => { + const file = 'external_models.yaml' + + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-sushi-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Assert external_models.yaml exists + const externalModelsYamlPath = path.join(tempDir, file) + expect(await fs.pathExists(externalModelsYamlPath)).toBe(true) + + await createPythonInterpreterSettingsSpecifier(tempDir) + await openServerPage(page, tempDir, sharedCodeServer) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the external_models file (e.g., external_models.yaml or external_models.yml) + await page + .getByRole('treeitem', { name: file, exact: true }) + .locator('a') + .click() + + await page.waitForSelector('text=raw.demographics') + await page.waitForSelector('text=Loaded SQLMesh Context') + }) + + test('external_models.yml', async ({ page, sharedCodeServer }) => { + const file = 'external_models.yml' + + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-sushi-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + // Move external_models.yaml to external_models.yml + const externalModelsYamlPath = path.join(tempDir, 'external_models.yaml') + const externalModelsYmlPath = path.join(tempDir, file) + await fs.rename(externalModelsYamlPath, externalModelsYmlPath) + + // Assert external_models.yml exists + expect(await fs.pathExists(externalModelsYmlPath)).toBe(true) + + await createPythonInterpreterSettingsSpecifier(tempDir) + await openServerPage(page, tempDir, sharedCodeServer) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the external_models.yml file + await page + .getByRole('treeitem', { name: file, exact: true }) + .locator('a') + .click() + + await page.waitForSelector('text=raw.demographics') + await page.waitForSelector('text=Loaded SQLMesh Context') + }) +}) From dc3a34c89478188223fa1bc6643000b5edc7b3f9 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 24 Jul 2025 20:00:04 +0300 Subject: [PATCH 0603/1056] Chore!: bump sqlglot to v27.3.1 (#5015) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 08926c2ab2..fc8a479617 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.1.0", + "sqlglot[rs]~=27.3.1", "tenacity", "time-machine", "json-stream" From 812bc27f9424145507a560d5a7131fba02400cd6 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 24 Jul 2025 18:04:51 +0100 Subject: [PATCH 0604/1056] feat!: linting rule no unregistered external models (#5009) --- examples/sushi/config.py | 1 + sqlmesh/core/linter/rules/builtin.py | 29 +++++++++++++- tests/core/linter/test_builtin.py | 50 +++++++++++++++++++++++++ vscode/extension/tests/quickfix.spec.ts | 16 ++++++-- 4 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 tests/core/linter/test_builtin.py diff --git a/examples/sushi/config.py b/examples/sushi/config.py index bbe9ec7988..2c124421dd 100644 --- a/examples/sushi/config.py +++ b/examples/sushi/config.py @@ -48,6 +48,7 @@ "noselectstar", "nomissingaudits", "nomissingowner", + "nomissingexternalmodels", ], ), ) diff --git a/sqlmesh/core/linter/rules/builtin.py b/sqlmesh/core/linter/rules/builtin.py index 8bf6e33720..f16bb5d111 100644 --- a/sqlmesh/core/linter/rules/builtin.py +++ b/sqlmesh/core/linter/rules/builtin.py @@ -10,7 +10,7 @@ from sqlmesh.core.linter.helpers import TokenPositionDetails, get_range_of_model_block from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit from sqlmesh.core.linter.definition import RuleSet -from sqlmesh.core.model import Model, SqlModel +from sqlmesh.core.model import Model, SqlModel, ExternalModel class NoSelectStar(Rule): @@ -110,4 +110,31 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]: return self.violation() +class NoMissingExternalModels(Rule): + """All external models must be registered in the external_models.yaml file""" + + def check_model(self, model: Model) -> t.Optional[RuleViolation]: + # Ignore external models themselves, because either they are registered, + # and if they are not, they will be caught as referenced in another model. + if isinstance(model, ExternalModel): + return None + + # Handle other models that may refer to the external models. + not_registered_external_models: t.Set[str] = set() + for depends_on_model in model.depends_on: + existing_model = self.context.get_model(depends_on_model) + if existing_model is None: + not_registered_external_models.add(depends_on_model) + + if not not_registered_external_models: + return None + + return RuleViolation( + rule=self, + violation_msg=f"Model '{model.name}' depends on unregistered external models: " + f"{', '.join(m for m in not_registered_external_models)}. " + "Please register them in the external models file. This can be done by running 'sqlmesh create_external_models'.", + ) + + BUILTIN_RULES = RuleSet(subclasses(__name__, Rule, (Rule,))) diff --git a/tests/core/linter/test_builtin.py b/tests/core/linter/test_builtin.py new file mode 100644 index 0000000000..208b591a2d --- /dev/null +++ b/tests/core/linter/test_builtin.py @@ -0,0 +1,50 @@ +import os + +from sqlmesh import Context + + +def test_no_missing_external_models(tmp_path, copy_to_temp_path) -> None: + """ + Tests that the linter correctly identifies unregistered external model dependencies. + + This test removes the `external_models.yaml` file from the sushi example project, + enables the linter, and verifies that the linter raises a violation for a model + that depends on unregistered external models. + """ + sushi_paths = copy_to_temp_path("examples/sushi") + sushi_path = sushi_paths[0] + + # Remove the external_models.yaml file + os.remove(sushi_path / "external_models.yaml") + + # Override the config.py to turn on lint + with open(sushi_path / "config.py", "r") as f: + read_file = f.read() + + before = """ linter=LinterConfig( + enabled=False, + rules=[ + "ambiguousorinvalidcolumn", + "invalidselectstarexpansion", + "noselectstar", + "nomissingaudits", + "nomissingowner", + "nomissingexternalmodels", + ], + ),""" + after = """linter=LinterConfig(enabled=True, rules=["nomissingexternalmodels"]),""" + read_file = read_file.replace(before, after) + assert after in read_file + with open(sushi_path / "config.py", "w") as f: + f.writelines(read_file) + + # Load the context with the temporary sushi path + context = Context(paths=[sushi_path]) + + # Lint the models + lints = context.lint_models(raise_on_error=False) + assert len(lints) == 1 + assert ( + "Model 'sushi.customers' depends on unregistered external models: " + in lints[0].violation_msg + ) diff --git a/vscode/extension/tests/quickfix.spec.ts b/vscode/extension/tests/quickfix.spec.ts index 7023257540..c31acaeeb3 100644 --- a/vscode/extension/tests/quickfix.spec.ts +++ b/vscode/extension/tests/quickfix.spec.ts @@ -17,7 +17,15 @@ test('noselectstar quickfix', async ({ page, sharedCodeServer }) => { const configPath = path.join(tempDir, 'config.py') const read = await fs.readFile(configPath, 'utf8') // Replace linter to be on + const target = 'enabled=True' const replaced = read.replace('enabled=False', 'enabled=True') + // Assert replaced correctly + expect(replaced).toContain(target) + + // Replace the rules to only have noselectstar + const targetRules = `rules=[ + "noselectstar", + ],` const replacedTheOtherRules = replaced.replace( `rules=[ "ambiguousorinvalidcolumn", @@ -25,12 +33,12 @@ test('noselectstar quickfix', async ({ page, sharedCodeServer }) => { "noselectstar", "nomissingaudits", "nomissingowner", + "nomissingexternalmodels", ],`, - `rules=[ - "noselectstar", - ], -`, + targetRules, ) + expect(replacedTheOtherRules).toContain(targetRules) + await fs.writeFile(configPath, replacedTheOtherRules) // Replace the file to cause the error const modelPath = path.join(tempDir, 'models', 'latest_order.sql') From a1ab5476f01ed568d548480de4b40cd271e5261b Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 24 Jul 2025 23:48:02 +0300 Subject: [PATCH 0605/1056] Feat: Add support for pre, post, on_virtual_update statements in config (#4995) --- docs/concepts/models/python_models.md | 4 + docs/concepts/models/seed_models.md | 2 + docs/concepts/models/sql_models.md | 4 + docs/reference/model_configuration.md | 39 ++++++++ sqlmesh/core/config/model.py | 7 ++ sqlmesh/core/model/definition.py | 18 ++++ tests/core/test_config.py | 59 +++++++++++ tests/core/test_context.py | 136 ++++++++++++++++++++++++++ tests/core/test_model.py | 111 ++++++++++++++++++++- 9 files changed, 379 insertions(+), 1 deletion(-) diff --git a/docs/concepts/models/python_models.md b/docs/concepts/models/python_models.md index 63796987a8..10884ecedf 100644 --- a/docs/concepts/models/python_models.md +++ b/docs/concepts/models/python_models.md @@ -102,6 +102,8 @@ For example, pre/post-statements might modify settings or create indexes. Howeve You can set the `pre_statements` and `post_statements` arguments to a list of SQL strings, SQLGlot expressions, or macro calls to define the model's pre/post-statements. +**Project-level defaults:** You can also define pre/post-statements at the project level using `model_defaults` in your configuration. These will be applied to all models in your project and merged with any model-specific statements. Default statements are executed first, followed by model-specific statements. Learn more about this in the [model configuration reference](../../reference/model_configuration.md#model-defaults). + ``` python linenums="1" hl_lines="8-12" @model( "db.test_model", @@ -182,6 +184,8 @@ These can be used, for example, to grant privileges on views of the virtual laye Similar to pre/post-statements you can set the `on_virtual_update` argument in the `@model` decorator to a list of SQL strings, SQLGlot expressions, or macro calls. +**Project-level defaults:** You can also define on-virtual-update statements at the project level using `model_defaults` in your configuration. These will be applied to all models in your project (including Python models) and merged with any model-specific statements. Default statements are executed first, followed by model-specific statements. Learn more about this in the [model configuration reference](../../reference/model_configuration.md#model-defaults). + ``` python linenums="1" hl_lines="8" @model( "db.test_model", diff --git a/docs/concepts/models/seed_models.md b/docs/concepts/models/seed_models.md index c447a0dd19..6f14960182 100644 --- a/docs/concepts/models/seed_models.md +++ b/docs/concepts/models/seed_models.md @@ -203,6 +203,8 @@ ALTER SESSION SET TIMEZONE = 'PST'; Seed models also support on-virtual-update statements, which are executed after the completion of the [Virtual Update](#virtual-update). +**Project-level defaults:** You can also define on-virtual-update statements at the project level using `model_defaults` in your configuration. These will be applied to all models in your project (including seed models) and merged with any model-specific statements. Default statements are executed first, followed by model-specific statements. Learn more about this in the [model configuration reference](../../reference/model_configuration.md#model-defaults). + These must be enclosed within an `ON_VIRTUAL_UPDATE_BEGIN;` ...; `ON_VIRTUAL_UPDATE_END;` block: ```sql linenums="1" hl_lines="8-13" diff --git a/docs/concepts/models/sql_models.md b/docs/concepts/models/sql_models.md index 85c2492c87..28bf0fbe78 100644 --- a/docs/concepts/models/sql_models.md +++ b/docs/concepts/models/sql_models.md @@ -67,6 +67,8 @@ For example, pre/post-statements might modify settings or create a table index. Pre/post-statements are just standard SQL commands located before/after the model query. They must end with a semi-colon, and the model query must end with a semi-colon if a post-statement is present. The [example above](#example) contains both pre- and post-statements. +**Project-level defaults:** You can also define pre/post-statements at the project level using `model_defaults` in your configuration. These will be applied to all models in your project and merged with any model-specific statements. Default statements are executed first, followed by model-specific statements. Learn more about this in the [model configuration reference](../../reference/model_configuration.md#model-defaults). + !!! warning Pre/post-statements are evaluated twice: when a model's table is created and when its query logic is evaluated. Executing statements more than once can have unintended side-effects, so you can [conditionally execute](../macros/sqlmesh_macros.md#prepost-statements) them based on SQLMesh's [runtime stage](../macros/macro_variables.md#runtime-variables). @@ -97,6 +99,8 @@ The optional on-virtual-update statements allow you to execute SQL commands afte These can be used, for example, to grant privileges on views of the virtual layer. +**Project-level defaults:** You can also define on-virtual-update statements at the project level using `model_defaults` in your configuration. These will be applied to all models in your project and merged with any model-specific statements. Default statements are executed first, followed by model-specific statements. Learn more about this in the [model configuration reference](../../reference/model_configuration.md#model-defaults). + These SQL statements must be enclosed within an `ON_VIRTUAL_UPDATE_BEGIN;` ...; `ON_VIRTUAL_UPDATE_END;` block like this: ```sql linenums="1" hl_lines="10-15" diff --git a/docs/reference/model_configuration.md b/docs/reference/model_configuration.md index 526e868d29..6ea3dd68b6 100644 --- a/docs/reference/model_configuration.md +++ b/docs/reference/model_configuration.md @@ -136,6 +136,42 @@ You can also use the `@model_kind_name` variable to fine-tune control over `phys ) ``` +You can aso define `pre_statements`, `post_statements` and `on_virtual_update` statements at the project level that will be applied to all models. These default statements are merged with any model-specific statements, with default statements executing first, followed by model-specific statements. + +=== "YAML" + + ```yaml linenums="1" + model_defaults: + dialect: duckdb + pre_statements: + - "SET timeout = 300000" + post_statements: + - "@IF(@runtime_stage = 'evaluating', ANALYZE @this_model)" + on_virtual_update: + - "GRANT SELECT ON @this_model TO ROLE analyst_role" + ``` + +=== "Python" + + ```python linenums="1" + from sqlmesh.core.config import Config, ModelDefaultsConfig + + config = Config( + model_defaults=ModelDefaultsConfig( + dialect="duckdb", + pre_statements=[ + "SET query_timeout = 300000", + ], + post_statements=[ + "@IF(@runtime_stage = 'evaluating', ANALYZE @this_model)", + ], + on_virtual_update=[ + "GRANT SELECT ON @this_model TO ROLE analyst_role", + ], + ), + ) + ``` + The SQLMesh project-level `model_defaults` key supports the following options, described in the [general model properties](#general-model-properties) table above: @@ -155,6 +191,9 @@ The SQLMesh project-level `model_defaults` key supports the following options, d - allow_partials - enabled - interval_unit +- 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)) ### Model Naming diff --git a/sqlmesh/core/config/model.py b/sqlmesh/core/config/model.py index fab74799d9..3a6266928a 100644 --- a/sqlmesh/core/config/model.py +++ b/sqlmesh/core/config/model.py @@ -2,6 +2,7 @@ import typing as t +from sqlglot import exp from sqlmesh.core.dialect import parse_one, extract_func_call from sqlmesh.core.config.base import BaseConfig from sqlmesh.core.model.kind import ( @@ -41,6 +42,9 @@ class ModelDefaultsConfig(BaseConfig): allow_partials: Whether the models can process partial (incomplete) data intervals. enabled: Whether the models are enabled. interval_unit: The temporal granularity of the models data intervals. By default computed from cron. + 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. """ @@ -61,6 +65,9 @@ class ModelDefaultsConfig(BaseConfig): interval_unit: t.Optional[t.Union[str, IntervalUnit]] = None enabled: t.Optional[t.Union[str, bool]] = None formatting: t.Optional[t.Union[str, bool]] = None + pre_statements: t.Optional[t.List[t.Union[str, exp.Expression]]] = None + post_statements: t.Optional[t.List[t.Union[str, exp.Expression]]] = None + on_virtual_update: t.Optional[t.List[t.Union[str, exp.Expression]]] = None _model_kind_validator = model_kind_validator _on_destructive_change_validator = on_destructive_change_validator diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 25dd013f4d..4485875df8 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2472,6 +2472,24 @@ def _create_model( statements: t.List[t.Union[exp.Expression, t.Tuple[exp.Expression, bool]]] = [] + # Merge default pre_statements with model-specific pre_statements + if "pre_statements" in defaults: + kwargs["pre_statements"] = [ + exp.maybe_parse(stmt, dialect=dialect) for stmt in defaults["pre_statements"] + ] + kwargs.get("pre_statements", []) + + # Merge default post_statements with model-specific post_statements + if "post_statements" in defaults: + kwargs["post_statements"] = [ + exp.maybe_parse(stmt, dialect=dialect) for stmt in defaults["post_statements"] + ] + kwargs.get("post_statements", []) + + # Merge default on_virtual_update with model-specific on_virtual_update + if "on_virtual_update" in defaults: + kwargs["on_virtual_update"] = [ + exp.maybe_parse(stmt, dialect=dialect) for stmt in defaults["on_virtual_update"] + ] + kwargs.get("on_virtual_update", []) + if "pre_statements" in kwargs: statements.extend(kwargs["pre_statements"]) if "query" in kwargs: diff --git a/tests/core/test_config.py b/tests/core/test_config.py index a33b06eca9..6c3eb6e361 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -676,6 +676,65 @@ def test_load_model_defaults_audits(tmp_path): assert config.model_defaults.audits[1][1]["threshold"].this == "1000" +def test_load_model_defaults_statements(tmp_path): + config_path = tmp_path / "config_model_defaults_statements.yaml" + with open(config_path, "w", encoding="utf-8") as fd: + fd.write( + """ +model_defaults: + dialect: duckdb + pre_statements: + - SET memory_limit = '10GB' + - CREATE TEMP TABLE temp_data AS SELECT 1 as id + post_statements: + - DROP TABLE IF EXISTS temp_data + - ANALYZE @this_model + - SET memory_limit = '5GB' + on_virtual_update: + - UPDATE stats_table SET last_update = CURRENT_TIMESTAMP + """ + ) + + config = load_config_from_paths( + Config, + project_paths=[config_path], + ) + + assert config.model_defaults.pre_statements is not None + assert len(config.model_defaults.pre_statements) == 2 + assert isinstance(exp.maybe_parse(config.model_defaults.pre_statements[0]), exp.Set) + assert isinstance(exp.maybe_parse(config.model_defaults.pre_statements[1]), exp.Create) + + assert config.model_defaults.post_statements is not None + assert len(config.model_defaults.post_statements) == 3 + assert isinstance(exp.maybe_parse(config.model_defaults.post_statements[0]), exp.Drop) + assert isinstance(exp.maybe_parse(config.model_defaults.post_statements[1]), exp.Analyze) + assert isinstance(exp.maybe_parse(config.model_defaults.post_statements[2]), exp.Set) + + assert config.model_defaults.on_virtual_update is not None + assert len(config.model_defaults.on_virtual_update) == 1 + assert isinstance(exp.maybe_parse(config.model_defaults.on_virtual_update[0]), exp.Update) + + +def test_load_model_defaults_validation_statements(tmp_path): + config_path = tmp_path / "config_model_defaults_statements_wrong.yaml" + with open(config_path, "w", encoding="utf-8") as fd: + fd.write( + """ +model_defaults: + dialect: duckdb + pre_statements: + - 313 + """ + ) + + with pytest.raises(TypeError, match=r"expected str instance, int found"): + config = load_config_from_paths( + Config, + project_paths=[config_path], + ) + + def test_scheduler_config(tmp_path_factory): config_path = tmp_path_factory.mktemp("yaml_config") / "config.yaml" with open(config_path, "w", encoding="utf-8") as fd: diff --git a/tests/core/test_context.py b/tests/core/test_context.py index ddf9138779..805e4d51a0 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -2731,3 +2731,139 @@ def _get_missing_intervals(name: str) -> t.List[t.Tuple[datetime, datetime]]: assert context.engine_adapter.fetchall( "select min(start_dt), max(end_dt) from sqlmesh_example__pr_env.unrelated_monthly_model" ) == [(to_datetime("2020-01-01 00:00:00"), to_datetime("2020-01-31 23:59:59.999999"))] + + +def test_defaults_pre_post_statements(tmp_path: Path): + config_path = tmp_path / "config.yaml" + models_path = tmp_path / "models" + models_path.mkdir() + + # Create config with default statements + config_path.write_text( + """ +model_defaults: + dialect: duckdb + pre_statements: + - SET memory_limit = '10GB' + - SET threads = @var1 + post_statements: + - ANALYZE @this_model +variables: + var1: 4 +""" + ) + + # Create a model + model_path = models_path / "test_model.sql" + model_path.write_text( + """ +MODEL ( + name test_model, + kind FULL +); + +SELECT 1 as id, 'test' as status; +""" + ) + + ctx = Context(paths=[tmp_path]) + + # Initial plan and apply + initial_plan = ctx.plan(auto_apply=True, no_prompts=True) + assert len(initial_plan.new_snapshots) == 1 + + snapshot = list(initial_plan.new_snapshots)[0] + model = snapshot.model + + # Verify statements are in the model and python environment has been popuplated + assert len(model.pre_statements) == 2 + assert len(model.post_statements) == 1 + assert model.python_env[c.SQLMESH_VARS].payload == "{'var1': 4}" + + # Verify the statements contain the expected SQL + assert model.pre_statements[0].sql() == "SET memory_limit = '10GB'" + assert model.render_pre_statements()[0].sql() == "SET \"memory_limit\" = '10GB'" + assert model.pre_statements[1].sql() == "SET threads = @var1" + assert model.render_pre_statements()[1].sql() == 'SET "threads" = 4' + + # Update config to change pre_statement + config_path.write_text( + """ +model_defaults: + dialect: duckdb + pre_statements: + - SET memory_limit = '5GB' # Changed value + post_statements: + - ANALYZE @this_model +""" + ) + + # Reload context and create new plan + ctx = Context(paths=[tmp_path]) + updated_plan = ctx.plan(no_prompts=True) + + # Should detect a change due to different pre_statements + assert len(updated_plan.directly_modified) == 1 + + # Apply the plan + ctx.apply(updated_plan) + + # Reload the models to get the updated version + ctx.load() + new_model = ctx.models['"test_model"'] + + # Verify updated statements + assert len(new_model.pre_statements) == 1 + assert new_model.pre_statements[0].sql() == "SET memory_limit = '5GB'" + assert new_model.render_pre_statements()[0].sql() == "SET \"memory_limit\" = '5GB'" + + # Verify the change was detected by the plan + assert len(updated_plan.directly_modified) == 1 + + +def test_model_defaults_statements_with_on_virtual_update(tmp_path: Path): + config_path = tmp_path / "config.yaml" + models_path = tmp_path / "models" + models_path.mkdir() + + # Create config with on_virtual_update + config_path.write_text( + """ +model_defaults: + dialect: duckdb + on_virtual_update: + - SELECT 'Model-defailt virtual update' AS message +""" + ) + + # Create a model with its own on_virtual_update as wel + model_path = models_path / "test_model.sql" + model_path.write_text( + """ +MODEL ( + name test_model, + kind FULL +); + +SELECT 1 as id, 'test' as name; + +ON_VIRTUAL_UPDATE_BEGIN; +SELECT 'Model-specific update' AS message; +ON_VIRTUAL_UPDATE_END; +""" + ) + + ctx = Context(paths=[tmp_path]) + + # Plan and apply + plan = ctx.plan(auto_apply=True, no_prompts=True) + + snapshot = list(plan.new_snapshots)[0] + model = snapshot.model + + # Verify both default and model-specific on_virtual_update statements + assert len(model.on_virtual_update) == 2 + + # Default statements should come first + assert model.on_virtual_update[0].sql() == "SELECT 'Model-defailt virtual update' AS message" + assert model.on_virtual_update[1].sql() == "SELECT 'Model-specific update' AS message" diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 575d9038ae..a99920b420 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -1691,6 +1691,115 @@ def test_description(sushi_context): assert sushi_context.models['"memory"."sushi"."orders"'].description == "Table of sushi orders." +def test_model_defaults_statements_merge(): + model_defaults = ModelDefaultsConfig( + dialect="duckdb", + pre_statements=[ + "SET enable_progress_bar = true", + "CREATE TEMP TABLE default_temp AS SELECT 1", + ], + post_statements=[ + "DROP TABLE IF EXISTS default_temp", + "grant select on @this_model to group reporter", + ], + on_virtual_update=["ANALYZE"], + ) + + # Create a model with its own statements as well + expressions = parse( + """ + MODEL ( + name test_model, + kind FULL + ); + + CREATE TEMP TABLE model_temp AS SELECT 2; + + SELECT * FROM test_table; + + DROP TABLE IF EXISTS model_temp; + + ON_VIRTUAL_UPDATE_BEGIN; + UPDATE stats_table SET last_update = CURRENT_TIMESTAMP; + ON_VIRTUAL_UPDATE_END; + """ + ) + + model = load_sql_based_model( + expressions, + path=Path("./test_model.sql"), + defaults=model_defaults.dict(), + ) + + # Check that pre_statements contains both default and model-specific statements + assert len(model.pre_statements) == 3 + assert model.pre_statements[0].sql() == "SET enable_progress_bar = TRUE" + assert model.pre_statements[1].sql() == "CREATE TEMPORARY TABLE default_temp AS SELECT 1" + assert model.pre_statements[2].sql() == "CREATE TEMPORARY TABLE model_temp AS SELECT 2" + + # Check that post_statements contains both default and model-specific statements + assert len(model.post_statements) == 3 + assert model.post_statements[0].sql() == "DROP TABLE IF EXISTS default_temp" + assert model.post_statements[1].sql() == "GRANT SELECT ON @this_model TO GROUP reporter" + assert model.post_statements[2].sql() == "DROP TABLE IF EXISTS model_temp" + + # Check that the query is rendered correctly with @this_model resolved to table name + assert ( + model.render_post_statements()[1].sql() + == 'GRANT SELECT ON "test_model" TO GROUP "reporter"' + ) + + # Check that on_virtual_update contains both default and model-specific statements + assert len(model.on_virtual_update) == 2 + assert model.on_virtual_update[0].sql() == "ANALYZE" + assert ( + model.on_virtual_update[1].sql() + == "UPDATE stats_table SET last_update = CURRENT_TIMESTAMP()" + ) + + +def test_model_defaults_statements_integration(): + config = Config( + model_defaults=ModelDefaultsConfig( + dialect="postgres", + pre_statements=["SET memory_limit = '10GB'"], + post_statements=["VACUUM ANALYZE"], + on_virtual_update=["GRANT SELECT ON @this_model TO GROUP public"], + ) + ) + + expressions = parse( + """ + MODEL ( + name test_model, + kind FULL + ); + + SELECT * FROM source_table; + """ + ) + + model = load_sql_based_model( + expressions, + path=Path("./test_model.sql"), + defaults=config.model_defaults.dict(), + ) + + # Verify defaults were applied + assert len(model.pre_statements) == 1 + assert model.pre_statements[0].sql() == "SET memory_limit = '10GB'" + + assert len(model.post_statements) == 1 + assert isinstance(model.post_statements[0], exp.Command) + + assert len(model.on_virtual_update) == 1 + assert model.on_virtual_update[0].sql() == "GRANT SELECT ON @this_model TO GROUP public" + assert ( + model.render_on_virtual_update()[0].sql() + == 'GRANT SELECT ON "test_model" TO GROUP "public"' + ) + + def test_render_definition(): expressions = d.parse( """ @@ -5568,7 +5677,7 @@ def test_when_matched_normalization() -> None: when_matched ( WHEN MATCHED THEN UPDATE SET target.key_a = source.key_a, - target.key_b = source.key_b, + target.key_b = source.key_b, ) ) ); From 47706cfe1520941900c6d36e2b091e5ab3f6499e Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 25 Jul 2025 09:49:26 +1200 Subject: [PATCH 0606/1056] Fix(table_diff): Correctly handle joins with composite keys where one or more of the key fields are null (#5007) --- sqlmesh/core/table_diff.py | 21 ++--- .../integration/test_integration.py | 16 ++-- tests/core/test_table_diff.py | 77 ++++++++++++++++--- 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/sqlmesh/core/table_diff.py b/sqlmesh/core/table_diff.py index 6a91b22dfb..126fa64b1e 100644 --- a/sqlmesh/core/table_diff.py +++ b/sqlmesh/core/table_diff.py @@ -421,21 +421,16 @@ def _column_expr(name: str, table: str) -> exp.Expression: exp.select( *s_selects.values(), *t_selects.values(), - exp.func("IF", exp.or_(*(c.is_(exp.Null()).not_() for c in s_index)), 1, 0).as_( - "s_exists" - ), - exp.func("IF", exp.or_(*(c.is_(exp.Null()).not_() for c in t_index)), 1, 0).as_( - "t_exists" - ), + exp.func( + "IF", exp.column(SQLMESH_JOIN_KEY_COL, "s").is_(exp.Null()).not_(), 1, 0 + ).as_("s_exists"), + exp.func( + "IF", exp.column(SQLMESH_JOIN_KEY_COL, "t").is_(exp.Null()).not_(), 1, 0 + ).as_("t_exists"), exp.func( "IF", - exp.and_( - exp.column(SQLMESH_JOIN_KEY_COL, "s").eq( - exp.column(SQLMESH_JOIN_KEY_COL, "t") - ), - exp.and_( - *(c.is_(exp.Null()).not_() for c in s_index + t_index), - ), + exp.column(SQLMESH_JOIN_KEY_COL, "s").eq( + exp.column(SQLMESH_JOIN_KEY_COL, "t") ), 1, 0, diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index ee839d7593..cb09d20537 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -2353,15 +2353,17 @@ def test_table_diff_grain_check_multiple_keys(ctx: TestContext): row_diff = table_diff.row_diff() assert row_diff.full_match_count == 7 - assert row_diff.full_match_pct == 93.33 - assert row_diff.s_only_count == 2 - assert row_diff.t_only_count == 5 - assert row_diff.stats["join_count"] == 4 - assert row_diff.stats["null_grain_count"] == 4 - assert row_diff.stats["s_count"] != row_diff.stats["distinct_count_s"] + assert row_diff.full_match_pct == 82.35 + assert row_diff.s_only_count == 0 + assert row_diff.t_only_count == 3 + assert row_diff.stats["join_count"] == 7 + assert ( + row_diff.stats["null_grain_count"] == 4 + ) # null grain currently (2025-07-24) means "any key column is null" as opposed to "all key columns are null" assert row_diff.stats["distinct_count_s"] == 7 - assert row_diff.stats["t_count"] != row_diff.stats["distinct_count_t"] + assert row_diff.stats["s_count"] == row_diff.stats["distinct_count_s"] assert row_diff.stats["distinct_count_t"] == 10 + assert row_diff.stats["t_count"] == row_diff.stats["distinct_count_t"] assert row_diff.s_sample.shape == (row_diff.s_only_count, 3) assert row_diff.t_sample.shape == (row_diff.t_only_count, 3) diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index 64096a6637..b2848676b4 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -290,16 +290,24 @@ def test_grain_check(sushi_context_fixed_date): )[0] row_diff = diff.row_diff() + assert row_diff.source_count == 7 + assert row_diff.target_count == 10 assert row_diff.full_match_count == 7 - assert row_diff.full_match_pct == 93.33 - assert row_diff.s_only_count == 2 - assert row_diff.t_only_count == 5 - assert row_diff.stats["join_count"] == 4 - assert row_diff.stats["null_grain_count"] == 4 - assert row_diff.stats["s_count"] != row_diff.stats["distinct_count_s"] + assert row_diff.partial_match_count == 0 + assert row_diff.s_only_count == 0 + assert row_diff.t_only_count == 3 + assert row_diff.full_match_pct == 82.35 + assert row_diff.partial_match_pct == 0 + assert row_diff.s_only_pct == 0 + assert row_diff.t_only_pct == 17.65 + assert row_diff.stats["join_count"] == 7 + assert ( + row_diff.stats["null_grain_count"] == 4 + ) # null grain currently (2025-07-24) means "any key column is null" as opposed to "all key columns are null" assert row_diff.stats["distinct_count_s"] == 7 - assert row_diff.stats["t_count"] != row_diff.stats["distinct_count_t"] assert row_diff.stats["distinct_count_t"] == 10 + assert row_diff.stats["s_count"] == row_diff.stats["distinct_count_s"] + assert row_diff.stats["t_count"] == row_diff.stats["distinct_count_t"] assert row_diff.s_sample.shape == (row_diff.s_only_count, 3) assert row_diff.t_sample.shape == (row_diff.t_only_count, 3) @@ -329,7 +337,7 @@ def test_generated_sql(sushi_context_fixed_date: Context, mocker: MockerFixture) ), ) - query_sql = 'CREATE TABLE IF NOT EXISTS "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" AS WITH "__source" AS (SELECT "s"."key", "s"."value", "s"."key" AS "__sqlmesh_join_key" FROM "table_diff_source" AS "s"), "__target" AS (SELECT "t"."key", "t"."value", "t"."key" AS "__sqlmesh_join_key" FROM "table_diff_target" AS "t"), "__stats" AS (SELECT "s"."key" AS "s__key", "s"."value" AS "s__value", "s"."__sqlmesh_join_key" AS "s____sqlmesh_join_key", "t"."key" AS "t__key", "t"."value" AS "t__value", "t"."__sqlmesh_join_key" AS "t____sqlmesh_join_key", CASE WHEN NOT "s"."key" IS NULL THEN 1 ELSE 0 END AS "s_exists", CASE WHEN NOT "t"."key" IS NULL THEN 1 ELSE 0 END AS "t_exists", CASE WHEN "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key" AND (NOT "s"."key" IS NULL AND NOT "t"."key" IS NULL) THEN 1 ELSE 0 END AS "row_joined", CASE WHEN "s"."key" IS NULL AND "t"."key" IS NULL THEN 1 ELSE 0 END AS "null_grain", CASE WHEN "s"."key" = "t"."key" THEN 1 WHEN ("s"."key" IS NULL) AND ("t"."key" IS NULL) THEN 1 WHEN ("s"."key" IS NULL) OR ("t"."key" IS NULL) THEN 0 ELSE 0 END AS "key_matches", CASE WHEN ROUND("s"."value", 3) = ROUND("t"."value", 3) THEN 1 WHEN ("s"."value" IS NULL) AND ("t"."value" IS NULL) THEN 1 WHEN ("s"."value" IS NULL) OR ("t"."value" IS NULL) THEN 0 ELSE 0 END AS "value_matches" FROM "__source" AS "s" FULL JOIN "__target" AS "t" ON "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key") SELECT *, CASE WHEN "key_matches" = 1 AND "value_matches" = 1 THEN 1 ELSE 0 END AS "row_full_match" FROM "__stats"' + query_sql = 'CREATE TABLE IF NOT EXISTS "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" AS WITH "__source" AS (SELECT "s"."key", "s"."value", "s"."key" AS "__sqlmesh_join_key" FROM "table_diff_source" AS "s"), "__target" AS (SELECT "t"."key", "t"."value", "t"."key" AS "__sqlmesh_join_key" FROM "table_diff_target" AS "t"), "__stats" AS (SELECT "s"."key" AS "s__key", "s"."value" AS "s__value", "s"."__sqlmesh_join_key" AS "s____sqlmesh_join_key", "t"."key" AS "t__key", "t"."value" AS "t__value", "t"."__sqlmesh_join_key" AS "t____sqlmesh_join_key", CASE WHEN NOT "s"."__sqlmesh_join_key" IS NULL THEN 1 ELSE 0 END AS "s_exists", CASE WHEN NOT "t"."__sqlmesh_join_key" IS NULL THEN 1 ELSE 0 END AS "t_exists", CASE WHEN "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key" THEN 1 ELSE 0 END AS "row_joined", CASE WHEN "s"."key" IS NULL AND "t"."key" IS NULL THEN 1 ELSE 0 END AS "null_grain", CASE WHEN "s"."key" = "t"."key" THEN 1 WHEN ("s"."key" IS NULL) AND ("t"."key" IS NULL) THEN 1 WHEN ("s"."key" IS NULL) OR ("t"."key" IS NULL) THEN 0 ELSE 0 END AS "key_matches", CASE WHEN ROUND("s"."value", 3) = ROUND("t"."value", 3) THEN 1 WHEN ("s"."value" IS NULL) AND ("t"."value" IS NULL) THEN 1 WHEN ("s"."value" IS NULL) OR ("t"."value" IS NULL) THEN 0 ELSE 0 END AS "value_matches" FROM "__source" AS "s" FULL JOIN "__target" AS "t" ON "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key") SELECT *, CASE WHEN "key_matches" = 1 AND "value_matches" = 1 THEN 1 ELSE 0 END AS "row_full_match" FROM "__stats"' summary_query_sql = 'SELECT SUM("s_exists") AS "s_count", SUM("t_exists") AS "t_count", SUM("row_joined") AS "join_count", SUM("null_grain") AS "null_grain_count", SUM("row_full_match") AS "full_match_count", SUM("key_matches") AS "key_matches", SUM("value_matches") AS "value_matches", COUNT(DISTINCT ("s____sqlmesh_join_key")) AS "distinct_count_s", COUNT(DISTINCT ("t____sqlmesh_join_key")) AS "distinct_count_t" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh"' compare_sql = 'SELECT ROUND(100 * (CAST(SUM("key_matches") AS DECIMAL) / COUNT("key_matches")), 9) AS "key_matches", ROUND(100 * (CAST(SUM("value_matches") AS DECIMAL) / COUNT("value_matches")), 9) AS "value_matches" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "row_joined" = 1' sample_query_sql = 'WITH "source_only" AS (SELECT \'source_only\' AS "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "s_exists" = 1 AND "row_joined" = 0 ORDER BY "s__key" NULLS FIRST LIMIT 20), "target_only" AS (SELECT \'target_only\' AS "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "t_exists" = 1 AND "row_joined" = 0 ORDER BY "t__key" NULLS FIRST LIMIT 20), "common_rows" AS (SELECT \'common_rows\' AS "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "memory"."sqlmesh_temp_test"."__temp_diff_abcdefgh" WHERE "row_joined" = 1 AND "row_full_match" = 0 ORDER BY "s__key" NULLS FIRST, "t__key" NULLS FIRST LIMIT 20) SELECT "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "source_only" UNION ALL SELECT "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "target_only" UNION ALL SELECT "__sqlmesh_sample_type", "s__key", "s__value", "s____sqlmesh_join_key", "t__key", "t__value", "t____sqlmesh_join_key" FROM "common_rows"' @@ -369,7 +377,7 @@ def test_generated_sql(sushi_context_fixed_date: Context, mocker: MockerFixture) where="key = 2", ) - query_sql_where = 'CREATE TABLE IF NOT EXISTS "memory"."sqlmesh_temp"."__temp_diff_abcdefgh" AS WITH "__source" AS (SELECT "s"."key", "s"."value", "s"."key" AS "__sqlmesh_join_key" FROM "table_diff_source" AS "s" WHERE "s"."key" = 2), "__target" AS (SELECT "t"."key", "t"."value", "t"."key" AS "__sqlmesh_join_key" FROM "table_diff_target" AS "t" WHERE "t"."key" = 2), "__stats" AS (SELECT "s"."key" AS "s__key", "s"."value" AS "s__value", "s"."__sqlmesh_join_key" AS "s____sqlmesh_join_key", "t"."key" AS "t__key", "t"."value" AS "t__value", "t"."__sqlmesh_join_key" AS "t____sqlmesh_join_key", CASE WHEN NOT "s"."key" IS NULL THEN 1 ELSE 0 END AS "s_exists", CASE WHEN NOT "t"."key" IS NULL THEN 1 ELSE 0 END AS "t_exists", CASE WHEN "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key" AND (NOT "s"."key" IS NULL AND NOT "t"."key" IS NULL) THEN 1 ELSE 0 END AS "row_joined", CASE WHEN "s"."key" IS NULL AND "t"."key" IS NULL THEN 1 ELSE 0 END AS "null_grain", CASE WHEN "s"."key" = "t"."key" THEN 1 WHEN ("s"."key" IS NULL) AND ("t"."key" IS NULL) THEN 1 WHEN ("s"."key" IS NULL) OR ("t"."key" IS NULL) THEN 0 ELSE 0 END AS "key_matches", CASE WHEN ROUND("s"."value", 3) = ROUND("t"."value", 3) THEN 1 WHEN ("s"."value" IS NULL) AND ("t"."value" IS NULL) THEN 1 WHEN ("s"."value" IS NULL) OR ("t"."value" IS NULL) THEN 0 ELSE 0 END AS "value_matches" FROM "__source" AS "s" FULL JOIN "__target" AS "t" ON "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key") SELECT *, CASE WHEN "key_matches" = 1 AND "value_matches" = 1 THEN 1 ELSE 0 END AS "row_full_match" FROM "__stats"' + query_sql_where = 'CREATE TABLE IF NOT EXISTS "memory"."sqlmesh_temp"."__temp_diff_abcdefgh" AS WITH "__source" AS (SELECT "s"."key", "s"."value", "s"."key" AS "__sqlmesh_join_key" FROM "table_diff_source" AS "s" WHERE "s"."key" = 2), "__target" AS (SELECT "t"."key", "t"."value", "t"."key" AS "__sqlmesh_join_key" FROM "table_diff_target" AS "t" WHERE "t"."key" = 2), "__stats" AS (SELECT "s"."key" AS "s__key", "s"."value" AS "s__value", "s"."__sqlmesh_join_key" AS "s____sqlmesh_join_key", "t"."key" AS "t__key", "t"."value" AS "t__value", "t"."__sqlmesh_join_key" AS "t____sqlmesh_join_key", CASE WHEN NOT "s"."__sqlmesh_join_key" IS NULL THEN 1 ELSE 0 END AS "s_exists", CASE WHEN NOT "t"."__sqlmesh_join_key" IS NULL THEN 1 ELSE 0 END AS "t_exists", CASE WHEN "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key" THEN 1 ELSE 0 END AS "row_joined", CASE WHEN "s"."key" IS NULL AND "t"."key" IS NULL THEN 1 ELSE 0 END AS "null_grain", CASE WHEN "s"."key" = "t"."key" THEN 1 WHEN ("s"."key" IS NULL) AND ("t"."key" IS NULL) THEN 1 WHEN ("s"."key" IS NULL) OR ("t"."key" IS NULL) THEN 0 ELSE 0 END AS "key_matches", CASE WHEN ROUND("s"."value", 3) = ROUND("t"."value", 3) THEN 1 WHEN ("s"."value" IS NULL) AND ("t"."value" IS NULL) THEN 1 WHEN ("s"."value" IS NULL) OR ("t"."value" IS NULL) THEN 0 ELSE 0 END AS "value_matches" FROM "__source" AS "s" FULL JOIN "__target" AS "t" ON "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key") SELECT *, CASE WHEN "key_matches" = 1 AND "value_matches" = 1 THEN 1 ELSE 0 END AS "row_full_match" FROM "__stats"' spy_execute.assert_any_call(query_sql_where) @@ -1137,3 +1145,54 @@ def test_data_diff_sample_limit(): assert len(diff.s_sample) == 3 assert len(diff.t_sample) == 3 assert len(diff.joined_sample) == 3 + + +def test_data_diff_nulls_in_some_grain_columns(): + engine_adapter = DuckDBConnectionConfig().create_engine_adapter() + + columns_to_types = { + "key1": exp.DataType.build("int"), + "key2": exp.DataType.build("varchar"), + "key3": exp.DataType.build("int"), + "value": exp.DataType.build("varchar"), + } + + engine_adapter.create_table("src", columns_to_types) + engine_adapter.create_table("target", columns_to_types) + + src_records = [ + (1, None, 1, "value"), # full match + (None, None, None, "null value"), # join, partial match + (2, None, None, "source only"), # source only + ] + + target_records = [ + (1, None, 1, "value"), # full match + (None, None, None, "null value modified"), # join, partial match + (None, "three", 2, "target only"), # target only + ] + + src_df = pd.DataFrame(data=src_records, columns=columns_to_types.keys()) + target_df = pd.DataFrame(data=target_records, columns=columns_to_types.keys()) + + engine_adapter.insert_append("src", src_df) + engine_adapter.insert_append("target", target_df) + + table_diff = TableDiff( + adapter=engine_adapter, source="src", target="target", on=["key1", "key2", "key3"] + ) + + diff = table_diff.row_diff() + + assert diff.join_count == 2 + assert diff.s_only_count == 1 + assert diff.t_only_count == 1 + assert diff.full_match_count == 1 + assert diff.partial_match_count == 1 + + assert diff.s_sample["value"].tolist() == ["source only"] + assert diff.t_sample["value"].tolist() == ["target only"] + assert diff.joined_sample[["s__value", "t__value"]].values.flatten().tolist() == [ + "null value", + "null value modified", + ] From 2953942b8f3032cf561d39dff10b52dd4ed86269 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 25 Jul 2025 09:50:18 +1200 Subject: [PATCH 0607/1056] Fix(cicd_bot): Include namespace in deploy command hint (#5006) --- .../integrations/github/cicd/controller.py | 5 +-- .../github/cicd/test_github_controller.py | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 48ec7ee32e..cc1131deff 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -751,9 +751,8 @@ def update_pr_environment(self) -> None: vde_title = "- :eyes: To **review** this PR's changes, use virtual data environment:" comment_value = f"{vde_title}\n - `{self.pr_environment_name}`" if self.bot_config.enable_deploy_command: - comment_value += ( - "\n- :arrow_forward: To **apply** this PR's plan to prod, comment:\n - `/deploy`" - ) + full_command = f"{self.bot_config.command_namespace or ''}/deploy" + comment_value += f"\n- :arrow_forward: To **apply** this PR's plan to prod, comment:\n - `{full_command}`" dedup_regex = vde_title.replace("*", r"\*") + r".*" updated_comment, _ = self.update_sqlmesh_comment_info( value=comment_value, diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index 099ed6d9ef..f1be97ee20 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -697,3 +697,41 @@ def test_get_pr_environment_summary_includes_warnings_and_errors( ) assert "> [!WARNING]\n>\n> Warning 1\n" in error_summary assert "> [!CAUTION]\n>\n> Error 1" in error_summary + + +def test_pr_comment_deploy_indicator_includes_command_namespace( + mocker: MockerFixture, + github_client, + make_mock_issue_comment, + make_controller: t.Callable[..., GithubController], +): + mock_repo = github_client.get_repo() + + created_comments = [] + mock_issue = mock_repo.get_issue() + mock_issue.create_comment = mocker.MagicMock( + side_effect=lambda comment: make_mock_issue_comment( + comment=comment, created_comments=created_comments + ) + ) + mock_issue.get_comments = mocker.MagicMock(side_effect=lambda: created_comments) + + controller = make_controller( + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + mock_out_context=False, + bot_config=GithubCICDBotConfig( + enable_deploy_command=True, + merge_method=MergeMethod.SQUASH, + command_namespace="#SQLMesh", + ), + ) + + _update_pr_environment(controller) + + assert len(created_comments) > 0 + + comment = created_comments[0].body + + assert "To **apply** this PR's plan to prod, comment:\n - `/deploy`" not in comment + assert "To **apply** this PR's plan to prod, comment:\n - `#SQLMesh/deploy`" in comment From 29833e74fb5e127f4bd44b472752efce23b8a3a3 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:10:20 +0100 Subject: [PATCH 0608/1056] docs: fix table formatting (#5020) --- docs/reference/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 40d0eeb26b..5ad6f079d7 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -82,6 +82,7 @@ Configuration for the `sqlmesh plan` command. | `no_diff` | Don't show diffs for changed models (Default: False) | boolean | N | | `no_prompts` | Disables interactive prompts in CLI (Default: True) | boolean | N | | `always_recreate_environment` | Always recreates the target environment from the environment specified in `create_from` (by default `prod`) (Default: False) | boolean | N | + ## Run Configuration for the `sqlmesh run` command. Please note that this is only applicable when configured with the [builtin](#builtin) scheduler. From c02022e0c2b619f5857998723016cf0b9107522e Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 25 Jul 2025 13:10:13 +0100 Subject: [PATCH 0609/1056] chore(vscode): refactor tests (#5021) --- sqlmesh/lsp/main.py | 15 ++++++--------- vscode/extension/tests/bad_setup.spec.ts | 3 ++- vscode/extension/tests/broken_project.spec.ts | 6 +++--- vscode/extension/tests/completions.spec.ts | 12 ++++++++---- vscode/extension/tests/external_models.spec.ts | 10 +++++++--- vscode/extension/tests/find_references.spec.ts | 9 +++++---- vscode/extension/tests/format.spec.ts | 9 +++++++-- vscode/extension/tests/go_to_definition.spec.ts | 11 ++++++++--- vscode/extension/tests/hints.spec.ts | 8 ++++++-- vscode/extension/tests/lineage.spec.ts | 9 +++++++-- vscode/extension/tests/lineage_settings.spec.ts | 9 +++++++-- vscode/extension/tests/python_env.spec.ts | 5 +++-- vscode/extension/tests/quickfix.spec.ts | 8 ++++++-- vscode/extension/tests/rename_cte.spec.ts | 3 ++- vscode/extension/tests/render.spec.ts | 10 +++++----- vscode/extension/tests/stop.spec.ts | 9 +++++++-- vscode/extension/tests/tcloud.spec.ts | 9 +++++---- vscode/extension/tests/utils.ts | 6 ++++++ vscode/extension/tests/venv_naming.spec.ts | 3 ++- 19 files changed, 102 insertions(+), 52 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index c257ccfaa1..b02e810997 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -254,10 +254,7 @@ def _reload_context_and_publish_diagnostics( self._ensure_context_for_document(uri) # If successful, context_state will be ContextLoaded if isinstance(self.context_state, ContextLoaded): - ls.show_message( - "Successfully loaded SQLMesh context", - types.MessageType.Info, - ) + loaded_sqlmesh_message(ls) except Exception as e: ls.log_trace(f"Still cannot load context: {e}") # The error will be stored in context_state by _ensure_context_for_document @@ -342,7 +339,7 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: config_path = folder_path / f"config.{ext}" if config_path.exists(): if self._create_lsp_context([folder_path]): - loaded_sqlmesh_message(ls, folder_path) + loaded_sqlmesh_message(ls) return # Exit after successfully loading any config except Exception as e: ls.log_trace( @@ -863,7 +860,7 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: try: if isinstance(self.context_state, NoContext): context = self.context_class(paths=paths) - loaded_sqlmesh_message(self.server, paths[0]) + loaded_sqlmesh_message(self.server) elif isinstance(self.context_state, ContextFailed): if self.context_state.context: context = self.context_state.context @@ -871,7 +868,7 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: else: # If there's no context (initial creation failed), try creating again context = self.context_class(paths=paths) - loaded_sqlmesh_message(self.server, paths[0]) + loaded_sqlmesh_message(self.server) else: context = self.context_state.lsp_context.context context.load() @@ -908,9 +905,9 @@ def start(self) -> None: self.server.start_io() -def loaded_sqlmesh_message(ls: LanguageServer, folder: Path) -> None: +def loaded_sqlmesh_message(ls: LanguageServer) -> None: ls.show_message( - f"Loaded SQLMesh context from {folder}", + f"Loaded SQLMesh Context", types.MessageType.Info, ) diff --git a/vscode/extension/tests/bad_setup.spec.ts b/vscode/extension/tests/bad_setup.spec.ts index 2284e2a95e..c4da69ca23 100644 --- a/vscode/extension/tests/bad_setup.spec.ts +++ b/vscode/extension/tests/bad_setup.spec.ts @@ -10,6 +10,7 @@ import { pipInstall, REPO_ROOT, SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, } from './utils' test('missing LSP dependencies shows install prompt', async ({ @@ -132,5 +133,5 @@ test.skip('check that the LSP runs correctly by opening lineage when looking at // Open the SQL file from the other directory await openFile(page, sqlFile) - await page.waitForSelector('text=Loaded SQLMesh context') + await waitForLoadedSQLMesh(page) }) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index 8de58c1853..8fe78fa321 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -8,6 +8,7 @@ import { openProblemsView, saveFile, SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' @@ -64,7 +65,7 @@ test('working project, then broken through adding double model, then refixed', a // Open the lineage view to confirm it loads properly await openLineageView(page) - await page.waitForSelector('text=Loaded SQLMesh context') + await waitForLoadedSQLMesh(page) // Read the customers.sql file const customersSql = await fs.readFile( @@ -292,6 +293,5 @@ test('bad model block, then fixed', async ({ page, sharedCodeServer }) => { await page.getByText('grain').click() await saveFile(page) - // Wait for successful context load - await page.waitForSelector('text=Loaded SQLMesh context') + await waitForLoadedSQLMesh(page) }) diff --git a/vscode/extension/tests/completions.spec.ts b/vscode/extension/tests/completions.spec.ts index da4eed6efc..32ec7d96e3 100644 --- a/vscode/extension/tests/completions.spec.ts +++ b/vscode/extension/tests/completions.spec.ts @@ -2,7 +2,11 @@ import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { openServerPage, SUSHI_SOURCE_PATH } from './utils' +import { + openServerPage, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Autocomplete for model names', async ({ page, sharedCodeServer }) => { @@ -28,7 +32,7 @@ test('Autocomplete for model names', async ({ page, sharedCodeServer }) => { .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) await page.locator('text=grain').first().click() @@ -83,7 +87,7 @@ test.describe('Macro Completions', () => { .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) await page.locator('text=grain').first().click() @@ -133,7 +137,7 @@ test.describe('Macro Completions', () => { .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) await page.locator('text=grain').first().click() diff --git a/vscode/extension/tests/external_models.spec.ts b/vscode/extension/tests/external_models.spec.ts index dc087e836a..8d70edae62 100644 --- a/vscode/extension/tests/external_models.spec.ts +++ b/vscode/extension/tests/external_models.spec.ts @@ -1,5 +1,9 @@ import os from 'os' -import { openServerPage, SUSHI_SOURCE_PATH } from './utils' +import { + openServerPage, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' import { test, expect } from './fixtures' import fs from 'fs-extra' @@ -31,7 +35,7 @@ test.describe('External model files trigger lsp', () => { .click() await page.waitForSelector('text=raw.demographics') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) }) test('external_models.yml', async ({ page, sharedCodeServer }) => { @@ -63,6 +67,6 @@ test.describe('External model files trigger lsp', () => { .click() await page.waitForSelector('text=raw.demographics') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) }) }) diff --git a/vscode/extension/tests/find_references.spec.ts b/vscode/extension/tests/find_references.spec.ts index b952e30ef8..e15868e234 100644 --- a/vscode/extension/tests/find_references.spec.ts +++ b/vscode/extension/tests/find_references.spec.ts @@ -7,6 +7,7 @@ import { goToReferences, openServerPage, SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' @@ -44,7 +45,7 @@ async function openCustomersFile(page: Page) { .locator('a') .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) } // Helper function to open top_waiters.sql and wait for SQLMesh context @@ -55,7 +56,7 @@ async function openTopWaitersFile(page: Page) { .locator('a') .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) } test.describe('Model References', () => { @@ -194,7 +195,7 @@ test.describe('Model References', () => { // Wait for audit file to load and SQLMesh context to initialize await page.waitForSelector('text=standalone') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Step 4: Click on sushi.items model reference in the audit query await page.locator('text=sushi.items').first().click() @@ -279,7 +280,7 @@ test.describe('Model References', () => { // Ensure audit file and SQLMesh context are fully loaded await page.waitForSelector('text=standalone') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Step 4: Position cursor on sushi.items model reference await page.locator('text=sushi.items').first().click() diff --git a/vscode/extension/tests/format.spec.ts b/vscode/extension/tests/format.spec.ts index 4e2b96bf94..fb95e66ba0 100644 --- a/vscode/extension/tests/format.spec.ts +++ b/vscode/extension/tests/format.spec.ts @@ -2,7 +2,12 @@ import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { openServerPage, runCommand, SUSHI_SOURCE_PATH } from './utils' +import { + openServerPage, + runCommand, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Format project works correctly', async ({ page, sharedCodeServer }) => { @@ -28,7 +33,7 @@ test('Format project works correctly', async ({ page, sharedCodeServer }) => { .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Format the project await runCommand(page, 'SQLMesh: Format Project') diff --git a/vscode/extension/tests/go_to_definition.spec.ts b/vscode/extension/tests/go_to_definition.spec.ts index 7d1749a1b7..36ad2177e1 100644 --- a/vscode/extension/tests/go_to_definition.spec.ts +++ b/vscode/extension/tests/go_to_definition.spec.ts @@ -2,7 +2,12 @@ import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { goToDefinition, openServerPage, SUSHI_SOURCE_PATH } from './utils' +import { + goToDefinition, + openServerPage, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Stop server works', async ({ page, sharedCodeServer }) => { @@ -27,7 +32,7 @@ test('Stop server works', async ({ page, sharedCodeServer }) => { .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Render the model await page.locator('text=@MULTIPLY').click() @@ -61,7 +66,7 @@ test('Go to definition for model', async ({ page, sharedCodeServer }) => { .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Go to definition for the model await page.locator('text=sushi.waiter_revenue_by_day').first().click() diff --git a/vscode/extension/tests/hints.spec.ts b/vscode/extension/tests/hints.spec.ts index cb5dddb0eb..8bcd2b9d09 100644 --- a/vscode/extension/tests/hints.spec.ts +++ b/vscode/extension/tests/hints.spec.ts @@ -2,7 +2,11 @@ import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { openServerPage, SUSHI_SOURCE_PATH } from './utils' +import { + openServerPage, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Model type hinting', async ({ page, sharedCodeServer }) => { @@ -30,7 +34,7 @@ test('Model type hinting', async ({ page, sharedCodeServer }) => { .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Wait a moment for hints to appear await page.waitForTimeout(500) diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index dfac5f5342..e61079e035 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -2,7 +2,12 @@ import { test, Page } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { openLineageView, openServerPage, SUSHI_SOURCE_PATH } from './utils' +import { + openLineageView, + openServerPage, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' import { writeFileSync } from 'fs' import { createPythonInterpreterSettingsSpecifier, @@ -17,7 +22,7 @@ async function testLineageWithProjectPath(page: Page): Promise { await page.waitForLoadState('networkidle') await page.waitForLoadState('domcontentloaded') await openLineageView(page) - await page.waitForSelector('text=Loaded SQLMesh context') + await waitForLoadedSQLMesh(page) } test('Lineage panel renders correctly - no project path config (default)', async ({ diff --git a/vscode/extension/tests/lineage_settings.spec.ts b/vscode/extension/tests/lineage_settings.spec.ts index 21827e351f..47ccf47f35 100644 --- a/vscode/extension/tests/lineage_settings.spec.ts +++ b/vscode/extension/tests/lineage_settings.spec.ts @@ -2,7 +2,12 @@ import { test, expect } from './fixtures' import path from 'path' import fs from 'fs-extra' import os from 'os' -import { openLineageView, openServerPage, SUSHI_SOURCE_PATH } from './utils' +import { + openLineageView, + openServerPage, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Settings button is visible in the lineage view', async ({ @@ -27,7 +32,7 @@ test('Settings button is visible in the lineage view', async ({ .getByRole('treeitem', { name: 'waiters.py', exact: true }) .locator('a') .click() - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Open lineage await openLineageView(page) diff --git a/vscode/extension/tests/python_env.spec.ts b/vscode/extension/tests/python_env.spec.ts index 04ccd2a7a0..27dd7a481f 100644 --- a/vscode/extension/tests/python_env.spec.ts +++ b/vscode/extension/tests/python_env.spec.ts @@ -8,6 +8,7 @@ import { PythonEnvironment, REPO_ROOT, SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, } from './utils' import os from 'os' import path from 'path' @@ -83,7 +84,7 @@ test.describe('python environment variable injection on sqlmesh_lsp', () => { const env_file = path.join(tempDir, '.env') fs.writeFileSync(env_file, 'TEST_VAR=test_value') await runTest(page, sharedCodeServer, tempDir) - await page.waitForSelector('text=Loaded SQLMesh context') + await waitForLoadedSQLMesh(page) }) }) @@ -127,6 +128,6 @@ test.describe('tcloud version', () => { const env_file = path.join(tempDir, '.env') fs.writeFileSync(env_file, 'TEST_VAR=test_value') await runTest(page, sharedCodeServer, tempDir) - await page.waitForSelector('text=Loaded SQLMesh context') + await waitForLoadedSQLMesh(page) }) }) diff --git a/vscode/extension/tests/quickfix.spec.ts b/vscode/extension/tests/quickfix.spec.ts index c31acaeeb3..5bbfe4020b 100644 --- a/vscode/extension/tests/quickfix.spec.ts +++ b/vscode/extension/tests/quickfix.spec.ts @@ -1,7 +1,11 @@ import fs from 'fs-extra' import path from 'path' import os from 'os' -import { openProblemsView, SUSHI_SOURCE_PATH } from './utils' +import { + openProblemsView, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' import { test, expect } from './fixtures' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' @@ -66,7 +70,7 @@ test('noselectstar quickfix', async ({ page, sharedCodeServer }) => { .locator('a') .click() - await page.waitForSelector('text=Loaded SQLMesh context') + await waitForLoadedSQLMesh(page) await openProblemsView(page) diff --git a/vscode/extension/tests/rename_cte.spec.ts b/vscode/extension/tests/rename_cte.spec.ts index db7ab6bcb3..4f566ef19c 100644 --- a/vscode/extension/tests/rename_cte.spec.ts +++ b/vscode/extension/tests/rename_cte.spec.ts @@ -7,6 +7,7 @@ import { openServerPage, renameSymbol, SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' @@ -34,7 +35,7 @@ async function setupTestEnvironment({ .locator('a') .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) } test.describe('CTE Rename', () => { diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts index 8b5cec01ab..298e3c690d 100644 --- a/vscode/extension/tests/render.spec.ts +++ b/vscode/extension/tests/render.spec.ts @@ -7,6 +7,7 @@ import { openServerPage, runCommand, SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' @@ -33,7 +34,7 @@ test('Render works correctly', async ({ page, sharedCodeServer }) => { .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Render the model await runCommand(page, 'Render Model') @@ -69,7 +70,7 @@ test('Render works correctly with model without a description', async ({ .click() await page.waitForSelector('text=custom_full_with_custom_kind') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Render the model await runCommand(page, 'Render Model') @@ -100,7 +101,7 @@ test('Render works correctly with every rendered model opening a new tab', async .locator('a') .click() await page.waitForSelector('text=custom_full_with_custom_kind') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Render the model await runCommand(page, 'Render Model') @@ -137,8 +138,7 @@ test('Render shows model picker when no active editor is open', async ({ // Load the lineage view to initialize SQLMesh context (like lineage.spec.ts does) await openLineageView(page) - // Wait for "Loaded SQLmesh Context" text to appear - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Run the render command without any active editor await runCommand(page, 'Render Model') diff --git a/vscode/extension/tests/stop.spec.ts b/vscode/extension/tests/stop.spec.ts index 12c3275a77..611d88d878 100644 --- a/vscode/extension/tests/stop.spec.ts +++ b/vscode/extension/tests/stop.spec.ts @@ -1,5 +1,10 @@ import path from 'path' -import { openServerPage, runCommand, SUSHI_SOURCE_PATH } from './utils' +import { + openServerPage, + runCommand, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' import os from 'os' import { test } from './fixtures' import fs from 'fs-extra' @@ -31,7 +36,7 @@ test('Stop server works', async ({ page, sharedCodeServer }) => { .click() await page.waitForSelector('text=grain') - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) // Stop the server await runCommand(page, 'SQLMesh: Stop Server') diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index ec334a3170..2d9010e059 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -8,6 +8,7 @@ import { pipInstall, REPO_ROOT, SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, } from './utils' import { setTcloudVersion, setupAuthenticatedState } from './tcloud_utils' import { @@ -182,7 +183,7 @@ test('signed in and not installed shows installation window', async ({ page.locator('text=Installing enterprise python package'), ).toHaveCount(2) - await page.waitForSelector('text=Loaded SQLMesh context') + await waitForLoadedSQLMesh(page) } finally { await stopCodeServer(context) } @@ -258,7 +259,7 @@ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when read .click() // Verify the context loads successfully - await page.waitForSelector('text=Loaded SQLMesh context') + await waitForLoadedSQLMesh(page) } finally { // Clean up await fs.remove(tempDir) @@ -334,7 +335,7 @@ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when read .click() // Verify the context loads successfully - await page.waitForSelector('text=Loaded SQLMesh context') + await waitForLoadedSQLMesh(page) } finally { // Clean up await fs.remove(tempDir) @@ -419,7 +420,7 @@ test.skip('tcloud not signed in and not installed, shows sign in window and then await page.waitForSelector('text=Installing enterprise python package') - await page.waitForSelector('text=Loaded SQLMesh context') + await waitForLoadedSQLMesh(page) } finally { await stopCodeServer(context) } diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index 951741bdd1..d75036b0ef 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -202,6 +202,12 @@ export const openFile = async (page: Page, file: string): Promise => { } } +/** + * Wait for SQLMesh context to be loaded. + */ +export const waitForLoadedSQLMesh = (page: Page) => + page.waitForSelector('text=Loaded SQLMesh Context') + /** * Go to VSCode page */ diff --git a/vscode/extension/tests/venv_naming.spec.ts b/vscode/extension/tests/venv_naming.spec.ts index 9e8006bff9..ee036a5eb6 100644 --- a/vscode/extension/tests/venv_naming.spec.ts +++ b/vscode/extension/tests/venv_naming.spec.ts @@ -9,6 +9,7 @@ import { pipInstall, REPO_ROOT, SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, } from './utils' test('venv being named .env', async ({ page, sharedCodeServer }) => { @@ -38,5 +39,5 @@ test('venv being named .env', async ({ page, sharedCodeServer }) => { await openServerPage(page, tempDir, sharedCodeServer) await page.waitForSelector('text=models') await openLineageView(page) - await page.waitForSelector('text=Loaded SQLMesh Context') + await waitForLoadedSQLMesh(page) }) From 51481b2e8238564b48e62490c91ab831fccbc7a7 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:16:44 +0100 Subject: [PATCH 0610/1056] feat(lsp): references function now detects non registered external models (#5023) --- sqlmesh/lsp/main.py | 21 ++++++++------ sqlmesh/lsp/reference.py | 22 +++++++++++++-- tests/lsp/test_reference.py | 4 ++- tests/lsp/test_reference_external_model.py | 32 ++++++++++++++++++++-- 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index b02e810997..dc633a3949 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -496,14 +496,15 @@ def goto_definition( target_range = reference.target_range target_selection_range = reference.target_range - location_links.append( - types.LocationLink( - target_uri=reference.uri, - target_selection_range=target_selection_range, - target_range=target_range, - origin_selection_range=reference.range, + if reference.uri is not None: + location_links.append( + types.LocationLink( + target_uri=reference.uri, + target_selection_range=target_selection_range, + target_range=target_range, + origin_selection_range=reference.range, + ) ) - ) return location_links except Exception as e: ls.show_message(f"Error getting references: {e}", types.MessageType.Error) @@ -521,7 +522,11 @@ def find_references( all_references = get_all_references(context, uri, params.position) # Convert references to Location objects - locations = [types.Location(uri=ref.uri, range=ref.range) for ref in all_references] + locations = [ + types.Location(uri=ref.uri, range=ref.range) + for ref in all_references + if ref.uri is not None + ] return locations if locations else None except Exception as e: diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 9f1215d9ca..342cd86893 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -38,10 +38,13 @@ class LSPExternalModelReference(PydanticModel): """A LSP reference to an external model.""" type: t.Literal["external_model"] = "external_model" - uri: str range: Range - markdown_description: t.Optional[str] = None target_range: t.Optional[Range] = None + uri: t.Optional[str] = None + """The URI of the external model, typically a YAML file, it is optional because + external models can be unregistered and so they URI is not available.""" + + markdown_description: t.Optional[str] = None class LSPCteReference(PydanticModel): @@ -224,7 +227,7 @@ def get_model_definitions_for_a_path( references.extend(column_references) continue - # For non-CTE tables, process as before (external model references) + # For non-CTE tables, process these as before (external model references) # Normalize the table reference unaliased = table.copy() if unaliased.args.get("alias") is not None: @@ -247,6 +250,19 @@ def get_model_definitions_for_a_path( model_or_snapshot=normalized_reference_name, raise_if_missing=False ) if referenced_model is None: + table_meta = TokenPositionDetails.from_meta(table.this.meta) + table_range_sqlmesh = table_meta.to_range(read_file) + start_pos_sqlmesh = table_range_sqlmesh.start + end_pos_sqlmesh = table_range_sqlmesh.end + references.append( + LSPExternalModelReference( + range=Range( + start=to_lsp_position(start_pos_sqlmesh), + end=to_lsp_position(end_pos_sqlmesh), + ), + markdown_description="Unregistered external model", + ) + ) continue referenced_model_path = referenced_model._path if referenced_model_path is None: diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index f39bddc059..736b995410 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -25,7 +25,9 @@ def test_reference() -> None: references = get_model_definitions_for_a_path(lsp_context, active_customers_uri) assert len(references) == 1 - assert URI(references[0].uri) == URI.from_path(sushi_customers_path) + uri = references[0].uri + assert uri is not None + assert URI(uri) == URI.from_path(sushi_customers_path) # Check that the reference in the correct range is sushi.customers path = active_customers_uri.to_path() diff --git a/tests/lsp/test_reference_external_model.py b/tests/lsp/test_reference_external_model.py index ebf6420934..43c873991e 100644 --- a/tests/lsp/test_reference_external_model.py +++ b/tests/lsp/test_reference_external_model.py @@ -1,10 +1,15 @@ +from pathlib import Path + from lsprotocol.types import Position + +from sqlmesh import Config from sqlmesh.core.context import Context from sqlmesh.core.linter.helpers import read_range_from_file from sqlmesh.lsp.context import LSPContext, ModelTarget from sqlmesh.lsp.helpers import to_sqlmesh_range from sqlmesh.lsp.reference import get_references, LSPExternalModelReference from sqlmesh.lsp.uri import URI +from tests.utils.test_filesystem import create_temp_file def test_reference() -> None: @@ -25,14 +30,37 @@ def test_reference() -> None: assert len(references) == 1 reference = references[0] assert isinstance(reference, LSPExternalModelReference) - assert reference.uri.endswith("external_models.yaml") + uri = reference.uri + assert uri is not None + assert uri.endswith("external_models.yaml") source_range = read_range_from_file(customers, to_sqlmesh_range(reference.range)) assert source_range == "raw.demographics" if reference.target_range is None: raise AssertionError("Reference target range should not be None") + uri = reference.uri + assert uri is not None target_range = read_range_from_file( - URI(reference.uri).to_path(), to_sqlmesh_range(reference.target_range) + URI(uri).to_path(), to_sqlmesh_range(reference.target_range) ) assert target_range == "raw.demographics" + + +def test_unregistered_external_model(tmp_path: Path): + model_path = tmp_path / "models" / "foo.sql" + contents = "MODEL (name test.foo, kind FULL); SELECT * FROM external_model" + create_temp_file(tmp_path, model_path, contents) + ctx = Context(paths=[tmp_path], config=Config()) + lsp_context = LSPContext(ctx) + + uri = URI.from_path(model_path) + references = get_references(lsp_context, uri, Position(line=0, character=len(contents) - 3)) + + assert len(references) == 1 + reference = references[0] + assert isinstance(reference, LSPExternalModelReference) + assert reference.uri is None + assert reference.target_range is None + assert reference.markdown_description == "Unregistered external model" + assert read_range_from_file(model_path, to_sqlmesh_range(reference.range)) == "external_model" From c747ca5767d10dd77da4727d46205c054ab377ee Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:27:55 +0100 Subject: [PATCH 0611/1056] fix(lsp): notification would now show (#5024) --- sqlmesh/lsp/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index dc633a3949..8bb161319f 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -829,6 +829,7 @@ def _ensure_context_in_folder(self, folder_path: t.Optional[Path] = None) -> Non config_path = workspace_folder / f"config.{ext}" if config_path.exists(): if self._create_lsp_context([workspace_folder]): + loaded_sqlmesh_message(self.server) return # Then , check the provided folder recursively @@ -840,6 +841,7 @@ def _ensure_context_in_folder(self, folder_path: t.Optional[Path] = None) -> Non config_path = path / f"config.{ext}" if config_path.exists(): if self._create_lsp_context([path]): + loaded_sqlmesh_message(self.server) return path = path.parent @@ -865,7 +867,6 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: try: if isinstance(self.context_state, NoContext): context = self.context_class(paths=paths) - loaded_sqlmesh_message(self.server) elif isinstance(self.context_state, ContextFailed): if self.context_state.context: context = self.context_state.context @@ -873,7 +874,6 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]: else: # If there's no context (initial creation failed), try creating again context = self.context_class(paths=paths) - loaded_sqlmesh_message(self.server) else: context = self.context_state.lsp_context.context context.load() From ddf0492808f96ffd6b2fa75b0e2963cb605d78e9 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 25 Jul 2025 18:40:56 +0200 Subject: [PATCH 0612/1056] Fix: Prevent snapshots with shared versions in dev environments from expanding restatement intervals for prod snapshots (#5025) --- sqlmesh/core/plan/evaluator.py | 7 +++++ tests/core/test_integration.py | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index bb779fffe9..39a5d69355 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -35,6 +35,7 @@ SnapshotInfoLike, SnapshotTableInfo, SnapshotCreationFailedError, + SnapshotNameVersion, ) from sqlmesh.utils import to_snake_case from sqlmesh.core.state_sync import StateSync @@ -427,6 +428,10 @@ def _restatement_intervals_across_all_environments( if not prod_restatements: return set() + prod_name_versions: t.Set[SnapshotNameVersion] = { + s.name_version for s in loaded_snapshots.values() + } + snapshots_to_restate: t.Dict[SnapshotId, t.Tuple[SnapshotTableInfo, Interval]] = {} for env_summary in self.state_sync.get_environments_summary(): @@ -454,6 +459,8 @@ def _restatement_intervals_across_all_environments( { keyed_snapshots[a].snapshot_id: (keyed_snapshots[a], intervals) for a in affected_snapshot_names + # Don't restate a snapshot if it shares the version with a snapshot in prod + if keyed_snapshots[a].name_version not in prod_name_versions } ) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 0717ba11aa..9580072d92 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -48,6 +48,7 @@ FullKind, IncrementalByTimeRangeKind, IncrementalByUniqueKeyKind, + IncrementalUnmanagedKind, Model, ModelKind, ModelKindName, @@ -2485,6 +2486,54 @@ 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_restatement_plan_across_environments_snapshot_with_shared_version( + init_and_plan_context: t.Callable, +): + context, _ = init_and_plan_context("examples/sushi") + + # Change kind to incremental unmanaged + model = context.get_model("sushi.waiter_revenue_by_day") + previous_kind = model.kind.copy(update={"forward_only": True}) + assert isinstance(previous_kind, IncrementalByTimeRangeKind) + + model = model.copy( + update={"kind": IncrementalUnmanagedKind(), "physical_version": "pinned_version_12345"} + ) + context.upsert_model(model) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Make some change and deploy it to both dev and prod environments + model = add_projection_to_model(t.cast(SqlModel, model)) + context.upsert_model(model) + context.plan("dev_a", auto_apply=True, no_prompts=True) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Change the kind back to incremental by time range and deploy to prod + model = model.copy(update={"kind": previous_kind}) + context.upsert_model(model) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Restate the model and verify that the interval hasn't been expanded because of the old snapshot + # with the same version + context.plan( + restate_models=["sushi.waiter_revenue_by_day"], + start="2023-01-06", + end="2023-01-08", + auto_apply=True, + no_prompts=True, + ) + + assert ( + context.fetchdf( + "SELECT COUNT(*) AS cnt FROM sushi.waiter_revenue_by_day WHERE one IS NOT NULL AND event_date < '2023-01-06'" + )["cnt"][0] + == 0 + ) + plan = context.plan_builder("prod").build() + assert not plan.missing_intervals + + def test_restatement_plan_hourly_with_downstream_daily_restates_correct_intervals(tmp_path: Path): model_a = """ MODEL ( From 089ba465b4764f9beb53c1836d295318e6e642d0 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 25 Jul 2025 10:08:15 -0700 Subject: [PATCH 0613/1056] Fix: Use merge when updating auto restatements (#5016) --- sqlmesh/core/state_sync/db/snapshot.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 5b6d96d970..7aaf902216 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -373,25 +373,31 @@ def update_auto_restatements( Args: next_auto_restatement_ts: A dictionary of snapshot name version to the next auto restatement timestamp. """ + next_auto_restatement_ts_deleted = [] + next_auto_restatement_ts_filtered = {} + for k, v in next_auto_restatement_ts.items(): + if v is None: + next_auto_restatement_ts_deleted.append(k) + else: + next_auto_restatement_ts_filtered[k] = v + for where in snapshot_name_version_filter( self.engine_adapter, - next_auto_restatement_ts, + next_auto_restatement_ts_deleted, column_prefix="snapshot", alias=None, batch_size=self.SNAPSHOT_BATCH_SIZE, ): self.engine_adapter.delete_from(self.auto_restatements_table, where=where) - next_auto_restatement_ts_filtered = { - k: v for k, v in next_auto_restatement_ts.items() if v is not None - } if not next_auto_restatement_ts_filtered: return - self.engine_adapter.insert_append( + self.engine_adapter.merge( self.auto_restatements_table, _auto_restatements_to_df(next_auto_restatement_ts_filtered), columns_to_types=self._auto_restatement_columns_to_types, + unique_key=(exp.column("snapshot_name"), exp.column("snapshot_version")), ) def count(self) -> int: From 431e9af2e64989c0ada3672380d39b5825da7a7c Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 25 Jul 2025 13:58:37 -0700 Subject: [PATCH 0614/1056] Chore!: Bump sqlglot to 27.4.0 (#5029) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fc8a479617..c9cf09c96d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.3.1", + "sqlglot[rs]~=27.4.0", "tenacity", "time-machine", "json-stream" From e35c9dcc66f1413ac20e74dccd017ddd67f0911e Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Sat, 26 Jul 2025 02:11:45 +0200 Subject: [PATCH 0615/1056] Fix!: Suppor forward-only changes of model kinds under certain circumstances (#5028) --- sqlmesh/core/plan/builder.py | 19 +++++++++-- sqlmesh/dbt/model.py | 18 ++++------ tests/core/test_plan.py | 58 +++++++++++++++++++++++++++++++- tests/dbt/test_transformation.py | 18 ++++++++++ 4 files changed, 98 insertions(+), 15 deletions(-) diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 567920997e..6f3c7f0805 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -601,7 +601,7 @@ def _categorize_snapshots( # If the model kind changes mark as breaking if snapshot.is_model and snapshot.name in self._context_diff.modified_snapshots: _, old = self._context_diff.modified_snapshots[snapshot.name] - if old.model.kind.name != snapshot.model.kind.name: + if _is_breaking_kind_change(old, snapshot): category = SnapshotChangeCategory.BREAKING snapshot.categorize_as(category) @@ -765,8 +765,8 @@ def _is_forward_only_change(self, s_id: SnapshotId) -> bool: snapshot = self._context_diff.snapshots[s_id] if snapshot.name in self._context_diff.modified_snapshots: _, old = self._context_diff.modified_snapshots[snapshot.name] - # If the model kind has changed, then we should not consider this to be a forward-only change. - if snapshot.is_model and old.model.kind.name != snapshot.model.kind.name: + # If the model kind has changed in a breaking way, then we can't consider this to be a forward-only change. + if snapshot.is_model and _is_breaking_kind_change(old, snapshot): return False return ( snapshot.is_model @@ -882,3 +882,16 @@ def _modified_and_added_snapshots(self) -> t.List[Snapshot]: if snapshot.name in self._context_diff.modified_snapshots or snapshot.snapshot_id in self._context_diff.added ] + + +def _is_breaking_kind_change(old: Snapshot, new: Snapshot) -> bool: + if old.model.kind.name == new.model.kind.name: + # If the kind hasn't changed, then it's not a breaking change + return False + if not old.is_incremental or not new.is_incremental: + # If either is not incremental, then it's a breaking change + return True + if old.model.partitioned_by == new.model.partitioned_by: + # If the partitioning hasn't changed, then it's not a breaking change + return False + return True diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 4cbca09aee..e34e66e119 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -257,6 +257,13 @@ def model_kind(self, context: DbtContext) -> ModelKind: if field_val is not None: incremental_by_kind_kwargs[field] = field_val + disable_restatement = self.disable_restatement + if disable_restatement is None: + disable_restatement = ( + not self.full_refresh if self.full_refresh is not None else False + ) + incremental_kind_kwargs["disable_restatement"] = disable_restatement + if self.time_column: strategy = self.incremental_strategy or target.default_incremental_strategy( IncrementalByTimeRangeKind @@ -270,20 +277,11 @@ def model_kind(self, context: DbtContext) -> ModelKind: return IncrementalByTimeRangeKind( time_column=self.time_column, - disable_restatement=( - self.disable_restatement if self.disable_restatement is not None else False - ), auto_restatement_intervals=self.auto_restatement_intervals, **incremental_kind_kwargs, **incremental_by_kind_kwargs, ) - disable_restatement = self.disable_restatement - if disable_restatement is None: - disable_restatement = ( - not self.full_refresh if self.full_refresh is not None else False - ) - if self.unique_key: strategy = self.incremental_strategy or target.default_incremental_strategy( IncrementalByUniqueKeyKind @@ -309,7 +307,6 @@ def model_kind(self, context: DbtContext) -> ModelKind: return IncrementalByUniqueKeyKind( unique_key=self.unique_key, - disable_restatement=disable_restatement, **incremental_kind_kwargs, **incremental_by_kind_kwargs, ) @@ -319,7 +316,6 @@ def model_kind(self, context: DbtContext) -> ModelKind: ) return IncrementalUnmanagedKind( insert_overwrite=strategy in INCREMENTAL_BY_TIME_STRATEGIES, - disable_restatement=disable_restatement, **incremental_kind_kwargs, ) if materialization == Materialization.EPHEMERAL: diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 765a45f7c8..efaeba8623 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -8,8 +8,9 @@ from tests.core.test_table_diff import create_test_console import time_machine from pytest_mock.plugin import MockerFixture -from sqlglot import parse_one +from sqlglot import parse_one, exp +from sqlmesh.core import dialect as d from sqlmesh.core.context import Context from sqlmesh.core.context_diff import ContextDiff from sqlmesh.core.environment import EnvironmentNamingInfo, EnvironmentStatements @@ -17,6 +18,7 @@ ExternalModel, FullKind, IncrementalByTimeRangeKind, + IncrementalUnmanagedKind, SeedKind, SeedModel, SqlModel, @@ -1724,6 +1726,60 @@ def test_forward_only_models_model_kind_changed(make_snapshot, mocker: MockerFix assert updated_snapshot.change_category == SnapshotChangeCategory.BREAKING +@pytest.mark.parametrize( + "partitioned_by, expected_change_category", + [ + ([], SnapshotChangeCategory.BREAKING), + ([d.parse_one("ds")], SnapshotChangeCategory.FORWARD_ONLY), + ], +) +def test_forward_only_models_model_kind_changed_to_incremental_by_time_range( + make_snapshot, + partitioned_by: t.List[exp.Expression], + expected_change_category: SnapshotChangeCategory, +): + snapshot = make_snapshot( + SqlModel( + name="a", + query=parse_one("select 1, ds"), + kind=IncrementalUnmanagedKind(), + partitioned_by=partitioned_by, + ) + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + updated_snapshot = make_snapshot( + SqlModel( + name="a", + query=parse_one("select 3, ds"), + kind=IncrementalByTimeRangeKind(time_column="ds", forward_only=True), + ) + ) + updated_snapshot.previous_versions = snapshot.all_versions + + context_diff = ContextDiff( + environment="test_environment", + is_new_environment=True, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={updated_snapshot.name: (updated_snapshot, snapshot)}, + snapshots={updated_snapshot.snapshot_id: updated_snapshot}, + new_snapshots={updated_snapshot.snapshot_id: updated_snapshot}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + PlanBuilder(context_diff, is_dev=True).build() + assert updated_snapshot.change_category == expected_change_category + + def test_indirectly_modified_forward_only_model(make_snapshot, mocker: MockerFixture): snapshot_a = make_snapshot(SqlModel(name="a", query=parse_one("select 1 as a, ds"))) snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING) diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 17b8a6f313..e483e45ae1 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -244,6 +244,24 @@ def test_model_kind(): time_column="foo", dialect="duckdb", forward_only=True, disable_restatement=False ) + assert ModelConfig( + materialized=Materialization.INCREMENTAL, + time_column="foo", + incremental_strategy="merge", + full_refresh=True, + ).model_kind(context) == IncrementalByTimeRangeKind( + time_column="foo", dialect="duckdb", forward_only=True, disable_restatement=False + ) + + assert ModelConfig( + materialized=Materialization.INCREMENTAL, + time_column="foo", + incremental_strategy="merge", + full_refresh=False, + ).model_kind(context) == IncrementalByTimeRangeKind( + time_column="foo", dialect="duckdb", forward_only=True, disable_restatement=True + ) + assert ModelConfig( materialized=Materialization.INCREMENTAL, time_column="foo", From 01730403e5937df07cf52becb6f2d05ac2f96ff1 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Sat, 26 Jul 2025 04:34:54 +0200 Subject: [PATCH 0616/1056] Fix: Revert dbt full_refresh handling for incremental by time range models (#5031) --- sqlmesh/dbt/model.py | 18 +++++++++++------- tests/dbt/test_transformation.py | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index e34e66e119..4cbca09aee 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -257,13 +257,6 @@ def model_kind(self, context: DbtContext) -> ModelKind: if field_val is not None: incremental_by_kind_kwargs[field] = field_val - disable_restatement = self.disable_restatement - if disable_restatement is None: - disable_restatement = ( - not self.full_refresh if self.full_refresh is not None else False - ) - incremental_kind_kwargs["disable_restatement"] = disable_restatement - if self.time_column: strategy = self.incremental_strategy or target.default_incremental_strategy( IncrementalByTimeRangeKind @@ -277,11 +270,20 @@ def model_kind(self, context: DbtContext) -> ModelKind: return IncrementalByTimeRangeKind( time_column=self.time_column, + disable_restatement=( + self.disable_restatement if self.disable_restatement is not None else False + ), auto_restatement_intervals=self.auto_restatement_intervals, **incremental_kind_kwargs, **incremental_by_kind_kwargs, ) + disable_restatement = self.disable_restatement + if disable_restatement is None: + disable_restatement = ( + not self.full_refresh if self.full_refresh is not None else False + ) + if self.unique_key: strategy = self.incremental_strategy or target.default_incremental_strategy( IncrementalByUniqueKeyKind @@ -307,6 +309,7 @@ def model_kind(self, context: DbtContext) -> ModelKind: return IncrementalByUniqueKeyKind( unique_key=self.unique_key, + disable_restatement=disable_restatement, **incremental_kind_kwargs, **incremental_by_kind_kwargs, ) @@ -316,6 +319,7 @@ def model_kind(self, context: DbtContext) -> ModelKind: ) return IncrementalUnmanagedKind( insert_overwrite=strategy in INCREMENTAL_BY_TIME_STRATEGIES, + disable_restatement=disable_restatement, **incremental_kind_kwargs, ) if materialization == Materialization.EPHEMERAL: diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index e483e45ae1..809e28fba4 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -259,7 +259,7 @@ def test_model_kind(): incremental_strategy="merge", full_refresh=False, ).model_kind(context) == IncrementalByTimeRangeKind( - time_column="foo", dialect="duckdb", forward_only=True, disable_restatement=True + time_column="foo", dialect="duckdb", forward_only=True, disable_restatement=False ) assert ModelConfig( From 2e4c21a857918c9cc694b16adef03b369838a7cb Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Sun, 27 Jul 2025 19:33:40 +0300 Subject: [PATCH 0617/1056] Chore: bump sqlglot to v27.4.1 (#5035) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c9cf09c96d..42d40a5f8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.4.0", + "sqlglot[rs]~=27.4.1", "tenacity", "time-machine", "json-stream" From 0690311e9f5ab803a52b5e521280a67f7cab1d06 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:11:13 +0100 Subject: [PATCH 0618/1056] feat: allow linter to return multiple violations (#5026) --- sqlmesh/core/console.py | 20 +++++++++++++++++++- sqlmesh/core/linter/definition.py | 5 +++-- sqlmesh/core/linter/rule.py | 4 +++- tests/core/test_context.py | 2 +- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 118abcd769..5622c21107 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -2685,7 +2685,25 @@ def show_linter_violations( self, violations: t.List[RuleViolation], model: Model, is_error: bool = False ) -> None: severity = "errors" if is_error else "warnings" - violations_msg = "\n".join(f" - {violation}" for violation in violations) + + # Sort violations by line, then alphabetically the name of the violation + # Violations with no range go first + sorted_violations = sorted( + violations, + key=lambda v: ( + v.violation_range.start.line if v.violation_range else -1, + v.rule.name.lower(), + ), + ) + violations_text = [ + ( + f" - Line {v.violation_range.start.line + 1}: {v.rule.name} - {v.violation_msg}" + if v.violation_range + else f" - {v.rule.name}: {v.violation_msg}" + ) + for v in sorted_violations + ] + violations_msg = "\n".join(violations_text) msg = f"Linter {severity} for {model._path}:\n{violations_msg}" if is_error: diff --git a/sqlmesh/core/linter/definition.py b/sqlmesh/core/linter/definition.py index 9cfa4076cd..c7cee6aaa9 100644 --- a/sqlmesh/core/linter/definition.py +++ b/sqlmesh/core/linter/definition.py @@ -108,9 +108,10 @@ def check_model(self, model: Model, context: GenericContext) -> t.List[RuleViola for rule in self._underlying.values(): violation = rule(context).check_model(model) - + if isinstance(violation, RuleViolation): + violation = [violation] if violation: - violations.append(violation) + violations.extend(violation) return violations diff --git a/sqlmesh/core/linter/rule.py b/sqlmesh/core/linter/rule.py index da33df2124..6e63dd2ee6 100644 --- a/sqlmesh/core/linter/rule.py +++ b/sqlmesh/core/linter/rule.py @@ -70,7 +70,9 @@ def __init__(self, context: GenericContext): self.context = context @abc.abstractmethod - def check_model(self, model: Model) -> t.Optional[RuleViolation]: + def check_model( + self, model: Model + ) -> t.Optional[t.Union[RuleViolation, t.List[RuleViolation]]]: """The evaluation function that'll check for a violation of this rule.""" @property diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 805e4d51a0..26fc542632 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1952,7 +1952,7 @@ def assert_cached_violations_exist(cache: OptimizedQueryCache, model: Model): ctx.plan(environment="dev", auto_apply=True, no_prompts=True) assert ( - """noselectstar: Query should not contain SELECT * on its outer most projections""" + """noselectstar - Query should not contain SELECT * on its outer most projections""" in mock_logger.call_args[0][0] ) From 9b04f45d77be5e1260fce6ae9edbe3bf891840eb Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Mon, 28 Jul 2025 09:12:59 +1200 Subject: [PATCH 0619/1056] Feat: Allow some control of table naming at the physical layer (#4982) --- docs/guides/configuration.md | 106 ++++++++++- docs/reference/configuration.md | 40 +++-- sqlmesh/core/config/__init__.py | 5 +- sqlmesh/core/config/common.py | 28 +++ sqlmesh/core/config/root.py | 6 +- sqlmesh/core/context.py | 6 +- sqlmesh/core/snapshot/definition.py | 49 ++++- sqlmesh/utils/hashing.py | 4 +- tests/core/test_config.py | 29 +++ tests/core/test_integration.py | 72 ++++++++ tests/core/test_snapshot.py | 267 +++++++++++++++++++++++++++- 11 files changed, 586 insertions(+), 26 deletions(-) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 52ebdf7793..6e14d1f605 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -320,10 +320,14 @@ The cache directory is automatically created if it doesn't exist. You can clear 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). -The default SQLMesh behavior described in the FAQ is appropriate for most deployments, but you can override where SQLMesh creates physical tables and views with the `physical_schema_mapping`, `environment_suffix_target`, and `environment_catalog_mapping` configuration options. These options are in the [environments](../reference/configuration.md#environments) section of the configuration reference page. +The default SQLMesh behavior described in the FAQ is appropriate for most deployments, but you can override *where* SQLMesh creates physical tables and views with the `physical_schema_mapping`, `environment_suffix_target`, and `environment_catalog_mapping` configuration options. + +You can also override *what* the physical tables are called by using the `physical_table_naming_convention` option. + +These options are in the [environments](../reference/configuration.md#environments) section of the configuration reference page. #### Physical table schemas -By default, SQLMesh creates physical tables for a model with a naming convention of `sqlmesh__[model schema]`. +By default, SQLMesh creates physical schemas for a model with a naming convention of `sqlmesh__[model schema]`. This can be overridden on a per-schema basis using the `physical_schema_mapping` option, which removes the `sqlmesh__` prefix and uses the [regex pattern](https://docs.python.org/3/library/re.html#regular-expression-syntax) you provide to map the schemas defined in your model to their corresponding physical schemas. @@ -436,6 +440,104 @@ Given the example of a model called `my_schema.users` with a default catalog of - Using `environment_suffix_target: catalog` only works on engines that support querying across different catalogs. If your engine does not support cross-catalog queries then you will need to use `environment_suffix_target: schema` or `environment_suffix_target: table` instead. - Automatic catalog creation is not supported on all engines even if they support cross-catalog queries. For engines where it is not supported, the catalogs must be managed externally from SQLMesh and exist prior to invoking SQLMesh. +#### Physical table naming convention + +Out of the box, SQLMesh has the following defaults set: + + - `environment_suffix_target: schema` + - `physical_table_naming_convention: schema_and_table` + - no `physical_schema_mapping` overrides, so a `sqlmesh__` physical schema will be created for each model schema + +This means that given a catalog of `warehouse` and a model named `finance_mart.transaction_events_over_threshold`, SQLMesh will create physical tables using the following convention: + +``` +# .sqlmesh__.____ + +warehouse.sqlmesh__finance_mart.finance_mart__transaction_events_over_threshold__ +``` + +This deliberately contains some redundancy with the *model* schema as it's repeated at the physical layer in both the physical schema name as well as the physical table name. + +This default exists to make the physical table names portable between different configurations. If you were to define a `physical_schema_mapping` that maps all models to the same physical schema, since the model schema is included in the table name as well, there are no naming conflicts. + +##### Table only + +Some engines have object name length limitations which cause them to [silently truncate](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS) table and view names that exceed this limit. This behaviour breaks SQLMesh, so we raise a runtime error if we detect the engine would silently truncate the name of the table we are trying to create. + +Having redundancy in the physical table names does reduce the number of characters that can be utilised in model names. To increase the number of characters available to model names, you can use `physical_table_naming_convention` like so: + +=== "YAML" + + ```yaml linenums="1" + physical_table_naming_convention: table_only + ``` + +=== "Python" + + ```python linenums="1" + from sqlmesh.core.config import Config, ModelDefaultsConfig, TableNamingConvention + + config = Config( + model_defaults=ModelDefaultsConfig(dialect=), + physical_table_naming_convention=TableNamingConvention.TABLE_ONLY, + ) + ``` + +This will cause SQLMesh to omit the model schema from the table name and generate physical names that look like (using the above example): +``` +# .sqlmesh__.
    __ + +warehouse.sqlmesh__finance_mart.transaction_events_over_threshold__ +``` + +Notice that the model schema name is no longer part of the physical table name. This allows for slightly longer model names on engines with low identifier length limits, which may be useful for your project. + +In this configuration, it is your responsibility to ensure that any schema overrides in `physical_schema_mapping` result in each model schema getting mapped to a unique physical schema. + +For example, the following configuration will cause **data corruption**: + +```yaml +physical_table_naming_convention: table_only +physical_schema_mapping: + '.*': sqlmesh +``` + +This is because every model schema is mapped to the same physical schema but the model schema name is omitted from the physical table name. + +##### MD5 hash + +If you *still* need more characters, you can set `physical_table_naming_convention: hash_md5` like so: + +=== "YAML" + + ```yaml linenums="1" + physical_table_naming_convention: hash_md5 + ``` + +=== "Python" + + ```python linenums="1" + from sqlmesh.core.config import Config, ModelDefaultsConfig, TableNamingConvention + + config = Config( + model_defaults=ModelDefaultsConfig(dialect=), + physical_table_naming_convention=TableNamingConvention.HASH_MD5, + ) + ``` + +This will cause SQLMesh generate physical names that are always 45-50 characters in length and look something like: + +``` +# sqlmesh_md5__ + +sqlmesh_md5__d3b07384d113edec49eaa6238ad5ff00 + +# or, for a dev preview +sqlmesh_md5__d3b07384d113edec49eaa6238ad5ff00__dev +``` + +This has a downside that now it's much more difficult to determine which table corresponds to which model by just looking at the database with a SQL client. However, the table names have a predictable length so there are no longer any surprises with identfiers exceeding the max length at the physical layer. + #### Environment view catalogs By default, SQLMesh creates an environment view in the same [catalog](../concepts/glossary.md#catalog) as the physical table the view points to. The physical table's catalog is determined by either the catalog specified in the model name or the default catalog defined in the connection. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 5ad6f079d7..06aed36b53 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -16,32 +16,44 @@ This section describes the other root level configuration parameters. Configuration options for SQLMesh project directories. -| Option | Description | Type | Required | -| ------------------ | ------------------------------------------------------------------------------------------------------------------ | :----------: | :------: | -| `ignore_patterns` | Files that match glob patterns specified in this list are ignored when scanning the project folder (Default: `[]`) | list[string] | N | -| `project` | The project name of this config. Used for [multi-repo setups](../guides/multi_repo.md). | string | N | +| Option | Description | Type | Required | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------- | :----------: | :------: | +| `ignore_patterns` | Files that match glob patterns specified in this list are ignored when scanning the project folder (Default: `[]`) | list[string] | N | +| `project` | The project name of this config. Used for [multi-repo setups](../guides/multi_repo.md). | string | N | | `cache_dir` | The directory to store the SQLMesh cache. Can be an absolute path or relative to the project directory. (Default: `.cache`) | string | N | +| `log_limit` | The default number of historical log files to keep (Default: `20`) | int | N | -### Environments +### Database (Physical Layer) -Configuration options for SQLMesh environment creation and promotion. +Configuration options for how SQLMesh manages database objects in the [physical layer](../concepts/glossary.md#physical-layer). | Option | Description | Type | Required | |-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------:|:--------:| | `snapshot_ttl` | The period of time that a model snapshot not a part of any environment should exist before being deleted. This is defined as a string with the default `in 1 week`. Other [relative dates](https://dateparser.readthedocs.io/en/latest/) can be used, such as `in 30 days`. (Default: `in 1 week`) | string | N | +| `physical_schema_override` | (Deprecated) Use `physical_schema_mapping` instead. A mapping from model schema names to names of schemas in which physical tables for the corresponding models will be placed. | dict[string, string] | N | +| `physical_schema_mapping` | A mapping from regular expressions to names of schemas in which physical tables for the corresponding models [will be placed](../guides/configuration.md#physical-table-schemas). (Default physical schema name: `sqlmesh__[model schema]`) | dict[string, string] | N | +| `physical_table_naming_convention`| Sets which parts of the model name are included in the physical table names. Options are `schema_and_table`, `table_only` or `hash_md5` - [additional details](../guides/configuration.md#physical-table-naming-convention). (Default: `schema_and_table`) | string | N | + +### Environments (Virtual Layer) + +Configuration options for how SQLMesh manages environment creation and promotion in the [virtual layer](../concepts/glossary.md#virtual-layer). + +| Option | Description | Type | Required | +|-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------:|:--------:| | `environment_ttl` | The period of time that a development environment should exist before being deleted. This is defined as a string with the default `in 1 week`. Other [relative dates](https://dateparser.readthedocs.io/en/latest/) can be used, such as `in 30 days`. (Default: `in 1 week`) | string | N | | `pinned_environments` | The list of development environments that are exempt from deletion due to expiration | list[string] | N | -| `time_column_format` | The default format to use for all model time columns. This time format uses [python format codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) (Default: `%Y-%m-%d`) | string | N | | `default_target_environment` | The name of the environment that will be the default target for the `sqlmesh plan` and `sqlmesh run` commands. (Default: `prod`) | string | N | -| `physical_schema_override` | (Deprecated) Use `physical_schema_mapping` instead. A mapping from model schema names to names of schemas in which physical tables for the corresponding models will be placed. | dict[string, string] | N | -| `physical_schema_mapping` | A mapping from regular expressions to names of schemas in which physical tables for the corresponding models [will be placed](../guides/configuration.md#physical-table-schemas). (Default physical schema name: `sqlmesh__[model schema]`) | dict[string, string] | N | -| `environment_suffix_target` | Whether SQLMesh views should append their environment name to the `schema` or `table` - [additional details](../guides/configuration.md#view-schema-override). (Default: `schema`) | string | N | -| `gateway_managed_virtual_layer` | Whether SQLMesh views of the virtual layer will be created by the default gateway or model specified gateways - [additional details](../guides/multi_engine.md#gateway-managed-virtual-layer). (Default: False) | boolean | N | -| `infer_python_dependencies` | Whether SQLMesh will statically analyze Python code to automatically infer Python package requirements. (Default: True) | boolean | N | +| `environment_suffix_target` | Whether SQLMesh views should append their environment name to the `schema`, `table` or `catalog` - [additional details](../guides/configuration.md#view-schema-override). (Default: `schema`) | string | N | +| `gateway_managed_virtual_layer` | Whether SQLMesh views of the virtual layer will be created by the default gateway or model specified gateways - [additional details](../guides/multi_engine.md#gateway-managed-virtual-layer). (Default: False) | boolean | N | | `environment_catalog_mapping` | A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment. | dict[string, string] | N | -| `log_limit` | The default number of logs to keep (Default: `20`) | int | N | -### Model defaults +### Models + +| Option | Description | Type | Required | +|-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------:|:--------:| +| `time_column_format` | The default format to use for all model time columns. This time format uses [python format codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) (Default: `%Y-%m-%d`) | string | N | +| `infer_python_dependencies` | Whether SQLMesh will statically analyze Python code to automatically infer Python package requirements. (Default: True) | boolean | N | +| `model_defaults` | Default [properties](./model_configuration.md#model-defaults) to set on each model. At a minimum, `dialect` must be set. | dict[string, any] | Y | The `model_defaults` key is **required** and must contain a value for the `dialect` key. diff --git a/sqlmesh/core/config/__init__.py b/sqlmesh/core/config/__init__.py index af84818858..d8c7607d51 100644 --- a/sqlmesh/core/config/__init__.py +++ b/sqlmesh/core/config/__init__.py @@ -2,7 +2,10 @@ AutoCategorizationMode as AutoCategorizationMode, CategorizerConfig as CategorizerConfig, ) -from sqlmesh.core.config.common import EnvironmentSuffixTarget as EnvironmentSuffixTarget +from sqlmesh.core.config.common import ( + EnvironmentSuffixTarget as EnvironmentSuffixTarget, + TableNamingConvention as TableNamingConvention, +) from sqlmesh.core.config.connection import ( AthenaConnectionConfig as AthenaConnectionConfig, BaseDuckDBConnectionConfig as BaseDuckDBConnectionConfig, diff --git a/sqlmesh/core/config/common.py b/sqlmesh/core/config/common.py index d7be902713..770c1f5daf 100644 --- a/sqlmesh/core/config/common.py +++ b/sqlmesh/core/config/common.py @@ -49,6 +49,34 @@ def __repr__(self) -> str: return str(self) +class TableNamingConvention(str, Enum): + # Causes table names at the physical layer to follow the convention: + # ____ + SCHEMA_AND_TABLE = "schema_and_table" + + # Causes table names at the physical layer to follow the convention: + # __ + TABLE_ONLY = "table_only" + + # Takes the table name that would be returned from SCHEMA_AND_TABLE and wraps it in md5() + # to generate a hash and prefixes the has with `sqlmesh_md5__`, for the following reasons: + # - at a glance, you can still see it's managed by sqlmesh and that md5 was used to generate the hash + # - unquoted identifiers that start with numbers can trip up DB engine parsers, so having a text prefix prevents this + # This causes table names at the physical layer to follow the convention: + # sqlmesh_md5__3b07384d113edec49eaa6238ad5ff00d + HASH_MD5 = "hash_md5" + + @classproperty + def default(cls) -> TableNamingConvention: + return TableNamingConvention.SCHEMA_AND_TABLE + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return str(self) + + def _concurrent_tasks_validator(v: t.Any) -> int: if isinstance(v, str): v = int(v) diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index cd92ff8467..4dd28f97a5 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -14,7 +14,7 @@ from sqlmesh.cicd.config import CICDBotConfig from sqlmesh.core import constants as c from sqlmesh.core.console import get_console -from sqlmesh.core.config import EnvironmentSuffixTarget +from sqlmesh.core.config import EnvironmentSuffixTarget, TableNamingConvention from sqlmesh.core.config.base import BaseConfig, UpdateStrategy from sqlmesh.core.config.common import variables_validator, compile_regex_mapping from sqlmesh.core.config.connection import ( @@ -106,6 +106,7 @@ class Config(BaseConfig): model_defaults: Default values for model definitions. physical_schema_mapping: A mapping from regular expressions to names of schemas in which physical tables for corresponding models will be placed. environment_suffix_target: Indicates whether to append the environment name to the schema or table name. + physical_table_naming_convention: Indicates how tables should be named at the physical layer gateway_managed_virtual_layer: Whether the models' views in the virtual layer are created by the model-specific gateway rather than the default gateway. infer_python_dependencies: Whether to statically analyze Python code to automatically infer Python package requirements. environment_catalog_mapping: A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment. @@ -147,6 +148,9 @@ class Config(BaseConfig): environment_suffix_target: EnvironmentSuffixTarget = Field( default=EnvironmentSuffixTarget.default ) + physical_table_naming_convention: TableNamingConvention = Field( + default=TableNamingConvention.default + ) gateway_managed_virtual_layer: bool = False infer_python_dependencies: bool = True environment_catalog_mapping: RegexKeyDict = {} diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 1ba241f69f..5a0531209a 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -2904,9 +2904,11 @@ def _nodes_to_snapshots(self, nodes: t.Dict[str, Node]) -> t.Dict[str, Snapshot] fingerprint_cache: t.Dict[str, SnapshotFingerprint] = {} for node in nodes.values(): - kwargs = {} + kwargs: t.Dict[str, t.Any] = {} if node.project in self._projects: - kwargs["ttl"] = self.config_for_node(node).snapshot_ttl + config = self.config_for_node(node) + kwargs["ttl"] = config.snapshot_ttl + kwargs["table_naming_convention"] = config.physical_table_naming_convention snapshot = Snapshot.from_node( node, diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 1a284aadfd..1331dd72f7 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -13,6 +13,7 @@ from sqlglot import exp from sqlglot.optimizer.normalize_identifiers import normalize_identifiers +from sqlmesh.core.config import TableNamingConvention from sqlmesh.core import constants as c from sqlmesh.core.audit import StandaloneAudit from sqlmesh.core.environment import EnvironmentSuffixTarget @@ -44,7 +45,7 @@ format_evaluated_code_exception, Executable, ) -from sqlmesh.utils.hashing import hash_data +from sqlmesh.utils.hashing import hash_data, md5 from sqlmesh.utils.pydantic import PydanticModel, field_validator if t.TYPE_CHECKING: @@ -227,6 +228,7 @@ class SnapshotDataVersion(PydanticModel, frozen=True): change_category: t.Optional[SnapshotChangeCategory] = None physical_schema_: t.Optional[str] = Field(default=None, alias="physical_schema") dev_table_suffix: str + table_naming_convention: TableNamingConvention = Field(default=TableNamingConvention.default) def snapshot_id(self, name: str) -> SnapshotId: return SnapshotId(name=name, identifier=self.fingerprint.to_identifier()) @@ -333,6 +335,7 @@ class SnapshotInfoMixin(ModelKindMixin): # This can be removed from this model once Pydantic 1 support is dropped (must remain in `Snapshot` though) base_table_name_override: t.Optional[str] dev_table_suffix: str + table_naming_convention: TableNamingConvention = Field(default=TableNamingConvention.default) @cached_property def identifier(self) -> str: @@ -451,6 +454,7 @@ def _table_name(self, version: str, is_deployable: bool) -> str: version, catalog=self.fully_qualified_table.catalog, suffix=self.dev_table_suffix if is_dev_table else None, + naming_convention=self.table_naming_convention, ) @property @@ -580,6 +584,7 @@ class Snapshot(PydanticModel, SnapshotInfoMixin): migrated: Whether or not this snapshot has been created as a result of migration. unrestorable: Whether or not this snapshot can be used to revert its model to a previous version. next_auto_restatement_ts: The timestamp which indicates when is the next time this snapshot should be restated. + table_naming_convention: Convention to follow when generating the physical table name """ name: str @@ -605,6 +610,9 @@ class Snapshot(PydanticModel, SnapshotInfoMixin): base_table_name_override: t.Optional[str] = None next_auto_restatement_ts: t.Optional[int] = None dev_table_suffix: str = "dev" + table_naming_convention_: TableNamingConvention = Field( + default=TableNamingConvention.default, alias="table_naming_convention" + ) @field_validator("ttl") @classmethod @@ -656,6 +664,7 @@ def from_node( ttl: str = c.DEFAULT_SNAPSHOT_TTL, version: t.Optional[str] = None, cache: t.Optional[t.Dict[str, SnapshotFingerprint]] = None, + table_naming_convention: TableNamingConvention = TableNamingConvention.default, ) -> Snapshot: """Creates a new snapshot for a node. @@ -666,6 +675,7 @@ def from_node( ttl: A TTL to determine how long orphaned (snapshots that are not promoted anywhere) should live. version: The version that a snapshot is associated with. Usually set during the planning phase. cache: Cache of node name to fingerprints. + table_naming_convention: Convention to follow when generating the physical table name Returns: The newly created snapshot. @@ -697,6 +707,7 @@ def from_node( updated_ts=created_ts, ttl=ttl, version=version, + table_naming_convention=table_naming_convention, ) def __eq__(self, other: t.Any) -> bool: @@ -1013,6 +1024,7 @@ def categorize_as(self, category: SnapshotChangeCategory) -> None: previous_version = self.previous_version self.version = previous_version.data_version.version self.physical_schema_ = previous_version.physical_schema + self.table_naming_convention = previous_version.table_naming_convention if self.is_materialized and (category.is_indirect_non_breaking or category.is_metadata): # Reuse the dev table for indirect non-breaking changes. self.dev_version_ = ( @@ -1206,6 +1218,7 @@ def table_info(self) -> SnapshotTableInfo: custom_materialization=custom_materialization, dev_table_suffix=self.dev_table_suffix, model_gateway=self.model_gateway, + table_naming_convention=self.table_naming_convention, # type: ignore ) @property @@ -1218,6 +1231,7 @@ def data_version(self) -> SnapshotDataVersion: change_category=self.change_category, physical_schema=self.physical_schema, dev_table_suffix=self.dev_table_suffix, + table_naming_convention=self.table_naming_convention, ) @property @@ -1568,14 +1582,41 @@ def table_name( version: str, catalog: t.Optional[str] = None, suffix: t.Optional[str] = None, + naming_convention: t.Optional[TableNamingConvention] = None, ) -> str: table = exp.to_table(name) - # bigquery projects usually have "-" in them which is illegal in the table name, so we aggressively prune - name = "__".join(sanitize_name(part.name) for part in table.parts) + naming_convention = naming_convention or TableNamingConvention.default + + if naming_convention == TableNamingConvention.HASH_MD5: + # just take a MD5 hash of what we would have generated anyway using SCHEMA_AND_TABLE + value_to_hash = table_name( + physical_schema=physical_schema, + name=name, + version=version, + catalog=catalog, + suffix=suffix, + naming_convention=TableNamingConvention.SCHEMA_AND_TABLE, + ) + full_name = f"{c.SQLMESH}_md5__{md5(value_to_hash)}" + else: + # note: Snapshot._table_name() already strips the catalog from the model name before calling this function + # Therefore, a model with 3-part naming like "foo.bar.baz" gets passed as (name="bar.baz", catalog="foo") to this function + # This is why there is no TableNamingConvention.CATALOG_AND_SCHEMA_AND_TABLE + table_parts = table.parts + parts_to_consider = 2 if naming_convention == TableNamingConvention.SCHEMA_AND_TABLE else 1 + + # in case the parsed table name has less parts than what the naming convention says we should be considering + parts_to_consider = min(len(table_parts), parts_to_consider) + + # bigquery projects usually have "-" in them which is illegal in the table name, so we aggressively prune + name = "__".join(sanitize_name(part.name) for part in table_parts[-parts_to_consider:]) + + full_name = f"{name}__{version}" + suffix = f"__{suffix}" if suffix else "" - table.set("this", exp.to_identifier(f"{name}__{version}{suffix}")) + table.set("this", exp.to_identifier(f"{full_name}{suffix}")) table.set("db", exp.to_identifier(physical_schema)) if not table.catalog and catalog: table.set("catalog", exp.to_identifier(catalog)) diff --git a/sqlmesh/utils/hashing.py b/sqlmesh/utils/hashing.py index 1bccd987bc..a166d36bec 100644 --- a/sqlmesh/utils/hashing.py +++ b/sqlmesh/utils/hashing.py @@ -9,7 +9,9 @@ def crc32(data: t.Iterable[t.Optional[str]]) -> str: return str(zlib.crc32(_safe_concat(data))) -def md5(data: t.Iterable[t.Optional[str]]) -> str: +def md5(data: t.Union[str, t.Iterable[t.Optional[str]]]) -> str: + if isinstance(data, str): + data = [data] return hashlib.md5(_safe_concat(data)).hexdigest() diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 6c3eb6e361..9277fc6902 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -3,6 +3,7 @@ import re from pathlib import Path from unittest import mock +import typing as t import pytest from pytest_mock import MockerFixture @@ -17,6 +18,7 @@ MotherDuckConnectionConfig, BuiltInSchedulerConfig, EnvironmentSuffixTarget, + TableNamingConvention, ) from sqlmesh.core.config.connection import DuckDBAttachOptions, RedshiftConnectionConfig from sqlmesh.core.config.feature_flag import DbtFeatureFlag, FeatureFlag @@ -1412,3 +1414,30 @@ def test_load_yaml_config_custom_dotenv_path(tmp_path_factory): default_gateway="test_gateway", model_defaults=ModelDefaultsConfig(dialect="postgres"), ) + + +@pytest.mark.parametrize( + "convention_str, expected", + [ + (None, TableNamingConvention.SCHEMA_AND_TABLE), + ("schema_and_table", TableNamingConvention.SCHEMA_AND_TABLE), + ("table_only", TableNamingConvention.TABLE_ONLY), + ("hash_md5", TableNamingConvention.HASH_MD5), + ], +) +def test_physical_table_naming_convention( + convention_str: t.Optional[str], expected: t.Optional[TableNamingConvention], tmp_path: Path +): + config_part = f"physical_table_naming_convention: {convention_str}" if convention_str else "" + (tmp_path / "config.yaml").write_text(f""" +gateways: + test_gateway: + connection: + type: duckdb +model_defaults: + dialect: duckdb +{config_part} + """) + + config = load_config_from_paths(Config, project_paths=[tmp_path / "config.yaml"]) + assert config.physical_table_naming_convention == expected diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 9580072d92..bad05d0c30 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -35,6 +35,7 @@ GatewayConfig, ModelDefaultsConfig, DuckDBConnectionConfig, + TableNamingConvention, ) from sqlmesh.core.config.common import EnvironmentSuffixTarget from sqlmesh.core.console import Console, get_console @@ -7164,3 +7165,74 @@ def test_engine_adapters_multi_repo_all_gateways_gathered(copy_to_temp_path): gathered_gateways = context.engine_adapters.keys() expected_gateways = {"local", "memory", "extra"} assert gathered_gateways == expected_gateways + + +def test_physical_table_naming_strategy_table_only(copy_to_temp_path: t.Callable): + sushi_context = Context( + paths=copy_to_temp_path("examples/sushi"), + config=Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(), + physical_table_naming_convention=TableNamingConvention.TABLE_ONLY, + ), + ) + + assert sushi_context.config.physical_table_naming_convention == TableNamingConvention.TABLE_ONLY + sushi_context.plan(auto_apply=True) + + adapter = sushi_context.engine_adapter + + snapshot_tables = [ + dict(catalog=str(r[0]), schema=str(r[1]), table=str(r[2])) + for r in adapter.fetchall( + "select table_catalog, table_schema, table_name from information_schema.tables where table_type='BASE TABLE'" + ) + ] + + assert all([not t["table"].startswith("sushi") for t in snapshot_tables]) + + prod_env = sushi_context.state_reader.get_environment("prod") + assert prod_env + + prod_env_snapshots = sushi_context.state_reader.get_snapshots(prod_env.snapshots) + + assert all( + s.table_naming_convention == TableNamingConvention.TABLE_ONLY + for s in prod_env_snapshots.values() + ) + + +def test_physical_table_naming_strategy_hash_md5(copy_to_temp_path: t.Callable): + sushi_context = Context( + paths=copy_to_temp_path("examples/sushi"), + config=Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(), + physical_table_naming_convention=TableNamingConvention.HASH_MD5, + ), + ) + + assert sushi_context.config.physical_table_naming_convention == TableNamingConvention.HASH_MD5 + sushi_context.plan(auto_apply=True) + + adapter = sushi_context.engine_adapter + + snapshot_tables = [ + dict(catalog=str(r[0]), schema=str(r[1]), table=str(r[2])) + for r in adapter.fetchall( + "select table_catalog, table_schema, table_name from information_schema.tables where table_type='BASE TABLE'" + ) + ] + + assert all([not t["table"].startswith("sushi") for t in snapshot_tables]) + assert all([t["table"].startswith("sqlmesh_md5") for t in snapshot_tables]) + + prod_env = sushi_context.state_reader.get_environment("prod") + assert prod_env + + prod_env_snapshots = sushi_context.state_reader.get_snapshots(prod_env.snapshots) + + assert all( + s.table_naming_convention == TableNamingConvention.HASH_MD5 + for s in prod_env_snapshots.values() + ) diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index ffaee9be74..cab5e17fff 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -61,11 +61,14 @@ get_next_model_interval_start, check_ready_intervals, _contiguous_intervals, + table_name, + TableNamingConvention, ) from sqlmesh.utils import AttributeDict from sqlmesh.utils.date import DatetimeRanges, to_date, to_datetime, to_timestamp from sqlmesh.utils.errors import SQLMeshError, SignalEvalError from sqlmesh.utils.jinja import JinjaMacroRegistry, MacroInfo +from sqlmesh.utils.hashing import md5 from sqlmesh.core.console import get_console @@ -163,6 +166,7 @@ def test_json(snapshot: Snapshot): "name": '"name"', "parents": [{"name": '"parent"."tbl"', "identifier": snapshot.parents[0].identifier}], "previous_versions": [], + "table_naming_convention": "schema_and_table", "updated_ts": 1663891973000, "version": snapshot.fingerprint.to_version(), "migrated": False, @@ -1131,12 +1135,15 @@ def test_stamp(model: Model): assert original_fingerprint != stamped_fingerprint -def test_table_name(snapshot: Snapshot, make_snapshot: t.Callable): +def test_snapshot_table_name(snapshot: Snapshot, make_snapshot: t.Callable): # Mimic a direct breaking change. snapshot.fingerprint = SnapshotFingerprint( data_hash="1", metadata_hash="1", parent_data_hash="1" ) snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + assert snapshot.table_naming_convention == TableNamingConvention.SCHEMA_AND_TABLE + assert snapshot.data_version.table_naming_convention == TableNamingConvention.SCHEMA_AND_TABLE + snapshot.previous_versions = () assert snapshot.table_name(is_deployable=True) == "sqlmesh__default.name__3078928823" assert snapshot.table_name(is_deployable=False) == "sqlmesh__default.name__3078928823__dev" @@ -1186,6 +1193,63 @@ def test_table_name(snapshot: Snapshot, make_snapshot: t.Callable): ) +def test_table_name_naming_convention_table_only(make_snapshot: t.Callable[..., Snapshot]): + # 3-part naming + snapshot = make_snapshot( + SqlModel(name='"foo"."bar"."baz"', query=parse_one("select 1")), + table_naming_convention=TableNamingConvention.TABLE_ONLY, + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + assert snapshot.table_naming_convention == TableNamingConvention.TABLE_ONLY + assert snapshot.data_version.table_naming_convention == TableNamingConvention.TABLE_ONLY + + assert snapshot.table_name(is_deployable=True) == f"foo.sqlmesh__bar.baz__{snapshot.version}" + assert ( + snapshot.table_name(is_deployable=False) == f"foo.sqlmesh__bar.baz__{snapshot.version}__dev" + ) + + # 2-part naming + snapshot = make_snapshot( + SqlModel(name='"foo"."bar"', query=parse_one("select 1")), + table_naming_convention=TableNamingConvention.TABLE_ONLY, + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + assert snapshot.table_name(is_deployable=True) == f"sqlmesh__foo.bar__{snapshot.version}" + assert snapshot.table_name(is_deployable=False) == f"sqlmesh__foo.bar__{snapshot.version}__dev" + + +def test_table_name_naming_convention_hash_md5(make_snapshot: t.Callable[..., Snapshot]): + # 3-part naming + snapshot = make_snapshot( + SqlModel(name='"foo"."bar"."baz"', query=parse_one("select 1")), + table_naming_convention=TableNamingConvention.HASH_MD5, + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + assert snapshot.table_naming_convention == TableNamingConvention.HASH_MD5 + assert snapshot.data_version.table_naming_convention == TableNamingConvention.HASH_MD5 + + hash = md5(f"foo.sqlmesh__bar.bar__baz__{snapshot.version}") + assert snapshot.table_name(is_deployable=True) == f"foo.sqlmesh__bar.sqlmesh_md5__{hash}" + hash_dev = md5(f"foo.sqlmesh__bar.bar__baz__{snapshot.version}__dev") + assert ( + snapshot.table_name(is_deployable=False) == f"foo.sqlmesh__bar.sqlmesh_md5__{hash_dev}__dev" + ) + + # 2-part naming + snapshot = make_snapshot( + SqlModel(name='"foo"."bar"', query=parse_one("select 1")), + table_naming_convention=TableNamingConvention.HASH_MD5, + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + hash = md5(f"sqlmesh__foo.foo__bar__{snapshot.version}") + assert snapshot.table_name(is_deployable=True) == f"sqlmesh__foo.sqlmesh_md5__{hash}" + + hash_dev = md5(f"sqlmesh__foo.foo__bar__{snapshot.version}__dev") + assert snapshot.table_name(is_deployable=False) == f"sqlmesh__foo.sqlmesh_md5__{hash_dev}__dev" + + def test_table_name_view(make_snapshot: t.Callable): # Mimic a direct breaking change. snapshot = make_snapshot(SqlModel(name="name", query=parse_one("select 1"), kind="VIEW")) @@ -1217,6 +1281,36 @@ def test_table_name_view(make_snapshot: t.Callable): assert new_snapshot.dev_version != snapshot.dev_version +def test_table_naming_convention_change_reuse_previous_version(make_snapshot): + # Ensure that snapshots that trigger "reuse previous version" inherit the naming convention of the previous snapshot + original_snapshot: Snapshot = make_snapshot( + SqlModel(name="a", query=parse_one("select 1, ds")), + table_naming_convention=TableNamingConvention.SCHEMA_AND_TABLE, + ) + original_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + assert original_snapshot.table_naming_convention == TableNamingConvention.SCHEMA_AND_TABLE + assert original_snapshot.table_name() == "sqlmesh__default.a__4145234055" + + changed_snapshot: Snapshot = make_snapshot( + SqlModel(name="a", query=parse_one("select 1, 'forward_only' as a, ds")), + table_naming_convention=TableNamingConvention.HASH_MD5, + ) + changed_snapshot.previous_versions = original_snapshot.all_versions + + assert changed_snapshot.previous_version == original_snapshot.data_version + + changed_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + + # inherited from previous version even though changed_snapshot was created with TableNamingConvention.HASH_MD5 + assert changed_snapshot.table_naming_convention == TableNamingConvention.SCHEMA_AND_TABLE + assert ( + changed_snapshot.previous_version.table_naming_convention + == TableNamingConvention.SCHEMA_AND_TABLE + ) + assert changed_snapshot.table_name() == "sqlmesh__default.a__4145234055" + + def test_categorize_change_sql(make_snapshot): old_snapshot = make_snapshot(SqlModel(name="a", query=parse_one("select 1, ds"))) @@ -2133,6 +2227,177 @@ def test_deployability_index_missing_parent(make_snapshot): assert not deplyability_index.is_deployable(snapshot_a) +@pytest.mark.parametrize( + "call_kwargs, expected", + [ + ######################################## + # TableNamingConvention.SCHEMA_AND_TABLE + ( + dict(physical_schema="sqlmesh__foo", name="bar", version="1234"), + "sqlmesh__foo.bar__1234", + ), + ( + dict(physical_schema="sqlmesh__foo", name="foo.bar", version="1234"), + "sqlmesh__foo.foo__bar__1234", + ), + ( + dict(physical_schema="sqlmesh__foo", name="bar", version="1234", catalog="foo"), + "foo.sqlmesh__foo.bar__1234", + ), + ( + dict(physical_schema="sqlmesh__foo", name="bar.baz", version="1234", catalog="foo"), + "foo.sqlmesh__foo.bar__baz__1234", + ), + ( + dict(physical_schema="sqlmesh__foo", name="bar.baz", version="1234", suffix="dev"), + "sqlmesh__foo.bar__baz__1234__dev", + ), + ( + dict( + physical_schema="sqlmesh__foo", + name="bar.baz", + version="1234", + catalog="foo", + suffix="dev", + ), + "foo.sqlmesh__foo.bar__baz__1234__dev", + ), + ################################## + # TableNamingConvention.TABLE_ONLY + ( + dict( + physical_schema="sqlmesh__foo", + name="bar", + version="1234", + naming_convention=TableNamingConvention.TABLE_ONLY, + ), + "sqlmesh__foo.bar__1234", + ), + ( + dict( + physical_schema="sqlmesh__foo", + name="foo.bar", + version="1234", + naming_convention=TableNamingConvention.TABLE_ONLY, + ), + "sqlmesh__foo.bar__1234", + ), + ( + dict( + physical_schema="sqlmesh__foo", + name="bar", + version="1234", + catalog="foo", + naming_convention=TableNamingConvention.TABLE_ONLY, + ), + "foo.sqlmesh__foo.bar__1234", + ), + ( + dict( + physical_schema="sqlmesh__bar", + name="bar.baz", + version="1234", + catalog="foo", + naming_convention=TableNamingConvention.TABLE_ONLY, + ), + "foo.sqlmesh__bar.baz__1234", + ), + ( + dict( + physical_schema="sqlmesh__bar", + name="bar.baz", + version="1234", + suffix="dev", + naming_convention=TableNamingConvention.TABLE_ONLY, + ), + "sqlmesh__bar.baz__1234__dev", + ), + ( + dict( + physical_schema="sqlmesh__bar", + name="bar.baz", + version="1234", + catalog="foo", + suffix="dev", + naming_convention=TableNamingConvention.TABLE_ONLY, + ), + "foo.sqlmesh__bar.baz__1234__dev", + ), + ################################# + # TableNamingConvention.HASH_MD5 + ( + dict( + physical_schema="sqlmesh__foo", + name="bar", + version="1234", + naming_convention=TableNamingConvention.HASH_MD5, + ), + f"sqlmesh__foo.sqlmesh_md5__{md5('sqlmesh__foo.bar__1234')}", + ), + ( + dict( + physical_schema="sqlmesh__foo", + name="foo.bar", + version="1234", + naming_convention=TableNamingConvention.HASH_MD5, + ), + f"sqlmesh__foo.sqlmesh_md5__{md5('sqlmesh__foo.foo__bar__1234')}", + ), + ( + dict( + physical_schema="sqlmesh__foo", + name="bar", + version="1234", + catalog="foo", + naming_convention=TableNamingConvention.HASH_MD5, + ), + f"foo.sqlmesh__foo.sqlmesh_md5__{md5('foo.sqlmesh__foo.bar__1234')}", + ), + ( + dict( + physical_schema="sqlmesh__foo", + name="bar.baz", + version="1234", + catalog="foo", + naming_convention=TableNamingConvention.HASH_MD5, + ), + f"foo.sqlmesh__foo.sqlmesh_md5__{md5('foo.sqlmesh__foo.bar__baz__1234')}", + ), + ( + dict( + physical_schema="sqlmesh__foo", + name="bar.baz", + version="1234", + suffix="dev", + naming_convention=TableNamingConvention.HASH_MD5, + ), + f"sqlmesh__foo.sqlmesh_md5__{md5('sqlmesh__foo.bar__baz__1234__dev')}__dev", + ), + ( + dict( + physical_schema="sqlmesh__foo", + name="bar.baz", + version="1234", + catalog="foo", + suffix="dev", + naming_convention=TableNamingConvention.HASH_MD5, + ), + f"foo.sqlmesh__foo.sqlmesh_md5__{md5('foo.sqlmesh__foo.bar__baz__1234__dev')}__dev", + ), + ], +) +def test_table_name(call_kwargs: t.Dict[str, t.Any], expected: str): + """ + physical_schema: str + name: str + version: str + catalog: t.Optional[str] + suffix: t.Optional[str] + naming_convention: t.Optional[TableNamingConvention] + """ + assert table_name(**call_kwargs) == expected + + @pytest.mark.parametrize( "model_name, environment_naming_info, default_catalog, dialect, expected", ( From fbd263272123e94fd20c4b8ede1ad109da3e06dd Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:35:44 +0100 Subject: [PATCH 0620/1056] refactor(lsp): references now use paths (#5036) --- sqlmesh/lsp/main.py | 8 +-- sqlmesh/lsp/reference.py | 64 +++++++++++----------- sqlmesh/lsp/rename.py | 2 +- tests/lsp/test_reference.py | 16 +++--- tests/lsp/test_reference_cte.py | 4 +- tests/lsp/test_reference_cte_find_all.py | 8 +-- tests/lsp/test_reference_external_model.py | 16 +++--- tests/lsp/test_reference_macro.py | 2 +- tests/lsp/test_reference_macro_find_all.py | 20 ++++--- tests/lsp/test_reference_macro_multi.py | 2 +- tests/lsp/test_reference_model_find_all.py | 14 ++--- 11 files changed, 78 insertions(+), 78 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 8bb161319f..24b287d74c 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -496,10 +496,10 @@ def goto_definition( target_range = reference.target_range target_selection_range = reference.target_range - if reference.uri is not None: + if reference.path is not None: location_links.append( types.LocationLink( - target_uri=reference.uri, + target_uri=URI.from_path(reference.path).value, target_selection_range=target_selection_range, target_range=target_range, origin_selection_range=reference.range, @@ -523,9 +523,9 @@ def find_references( # Convert references to Location objects locations = [ - types.Location(uri=ref.uri, range=ref.range) + types.Location(uri=URI.from_path(ref.path).value, range=ref.range) for ref in all_references - if ref.uri is not None + if ref.path is not None ] return locations if locations else None diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 342cd86893..6849129a7e 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -29,7 +29,7 @@ class LSPModelReference(PydanticModel): """A LSP reference to a model, excluding external models.""" type: t.Literal["model"] = "model" - uri: str + path: Path range: Range markdown_description: t.Optional[str] = None @@ -40,9 +40,9 @@ class LSPExternalModelReference(PydanticModel): type: t.Literal["external_model"] = "external_model" range: Range target_range: t.Optional[Range] = None - uri: t.Optional[str] = None - """The URI of the external model, typically a YAML file, it is optional because - external models can be unregistered and so they URI is not available.""" + path: t.Optional[Path] = None + """The path of the external model, typically a YAML file, it is optional because + external models can be unregistered and so the path is not available.""" markdown_description: t.Optional[str] = None @@ -51,7 +51,7 @@ class LSPCteReference(PydanticModel): """A LSP reference to a CTE.""" type: t.Literal["cte"] = "cte" - uri: str + path: Path range: Range target_range: Range @@ -60,7 +60,7 @@ class LSPMacroReference(PydanticModel): """A LSP reference to a macro.""" type: t.Literal["macro"] = "macro" - uri: str + path: Path range: Range target_range: Range markdown_description: t.Optional[str] = None @@ -209,7 +209,7 @@ def get_model_definitions_for_a_path( references.append( LSPCteReference( - uri=document_uri.value, # Same file + path=document_uri.to_path(), # Same file range=table_range, target_range=target_range, ) @@ -219,7 +219,7 @@ def get_model_definitions_for_a_path( scope=scope, reference_name=table.name, read_file=read_file, - referenced_model_uri=document_uri, + referenced_model_path=document_uri.to_path(), description="", reference_type="cte", cte_target_range=target_range, @@ -270,7 +270,6 @@ def get_model_definitions_for_a_path( # Check whether the path exists if not referenced_model_path.is_file(): continue - referenced_model_uri = URI.from_path(referenced_model_path) # Extract metadata for positioning table_meta = TokenPositionDetails.from_meta(table.this.meta) @@ -299,7 +298,7 @@ def get_model_definitions_for_a_path( ) references.append( LSPExternalModelReference( - uri=referenced_model_uri.value, + path=referenced_model_path, range=Range( start=to_lsp_position(start_pos_sqlmesh), end=to_lsp_position(end_pos_sqlmesh), @@ -313,7 +312,7 @@ def get_model_definitions_for_a_path( scope=scope, reference_name=normalized_reference_name, read_file=read_file, - referenced_model_uri=referenced_model_uri, + referenced_model_path=referenced_model_path, description=description, yaml_target_range=yaml_target_range, reference_type="external_model", @@ -324,7 +323,7 @@ def get_model_definitions_for_a_path( else: references.append( LSPModelReference( - uri=referenced_model_uri.value, + path=referenced_model_path, range=Range( start=to_lsp_position(start_pos_sqlmesh), end=to_lsp_position(end_pos_sqlmesh), @@ -337,7 +336,7 @@ def get_model_definitions_for_a_path( scope=scope, reference_name=normalized_reference_name, read_file=read_file, - referenced_model_uri=referenced_model_uri, + referenced_model_path=referenced_model_path, description=description, reference_type="model", default_catalog=lint_context.context.default_catalog, @@ -481,10 +480,9 @@ def get_macro_reference( return None # Create a reference to the macro definition - macro_uri = URI.from_path(path) return LSPMacroReference( - uri=macro_uri.value, + path=path, range=to_lsp_range(macro_range), target_range=Range( start=Position(line=start_line - 1, character=0), @@ -517,7 +515,7 @@ def get_built_in_macro_reference(macro_name: str, macro_range: Range) -> t.Optio end_line_number = line_number + len(source_lines) - 1 return LSPMacroReference( - uri=URI.from_path(Path(filename)).value, + path=Path(filename), range=macro_range, target_range=Range( start=Position(line=line_number - 1, character=0), @@ -559,12 +557,12 @@ def get_model_find_all_references( assert isinstance(model_at_position, LSPModelReference) # for mypy - target_model_uri = model_at_position.uri + target_model_path = model_at_position.path # Start with the model definition all_references: t.List[LSPModelReference] = [ LSPModelReference( - uri=model_at_position.uri, + path=model_at_position.path, range=Range( start=Position(line=0, character=0), end=Position(line=0, character=0), @@ -575,7 +573,7 @@ def get_model_find_all_references( # Then add references from the current file current_file_refs = filter( - lambda ref: isinstance(ref, LSPModelReference) and ref.uri == target_model_uri, + lambda ref: isinstance(ref, LSPModelReference) and ref.path == target_model_path, get_model_definitions_for_a_path(lint_context, document_uri), ) @@ -584,7 +582,7 @@ def get_model_find_all_references( all_references.append( LSPModelReference( - uri=document_uri.value, + path=document_uri.to_path(), range=ref.range, markdown_description=ref.markdown_description, ) @@ -600,7 +598,7 @@ def get_model_find_all_references( # Get model references that point to the target model matching_refs = filter( - lambda ref: isinstance(ref, LSPModelReference) and ref.uri == target_model_uri, + lambda ref: isinstance(ref, LSPModelReference) and ref.path == target_model_path, get_model_definitions_for_a_path(lint_context, file_uri), ) @@ -609,7 +607,7 @@ def get_model_find_all_references( all_references.append( LSPModelReference( - uri=file_uri.value, + path=path, range=ref.range, markdown_description=ref.markdown_description, ) @@ -662,7 +660,7 @@ def get_cte_references( # Add the CTE definition matching_references = [ LSPCteReference( - uri=document_uri.value, + path=document_uri.to_path(), range=target_cte_definition_range, target_range=target_cte_definition_range, ) @@ -673,7 +671,7 @@ def get_cte_references( if ref.target_range == target_cte_definition_range: matching_references.append( LSPCteReference( - uri=document_uri.value, + path=document_uri.to_path(), range=ref.range, target_range=ref.target_range, ) @@ -713,13 +711,13 @@ def get_macro_find_all_references( assert isinstance(macro_at_position, LSPMacroReference) # for mypy - target_macro_uri = macro_at_position.uri + target_macro_path = macro_at_position.path target_macro_target_range = macro_at_position.target_range # Start with the macro definition all_references: t.List[LSPMacroReference] = [ LSPMacroReference( - uri=target_macro_uri, + path=target_macro_path, range=target_macro_target_range, target_range=target_macro_target_range, markdown_description=None, @@ -733,7 +731,7 @@ def get_macro_find_all_references( # Get macro references that point to the same macro definition matching_refs = filter( lambda ref: isinstance(ref, LSPMacroReference) - and ref.uri == target_macro_uri + and ref.path == target_macro_path and ref.target_range == target_macro_target_range, get_macro_definitions_for_a_path(lsp_context, file_uri), ) @@ -742,7 +740,7 @@ def get_macro_find_all_references( assert isinstance(ref, LSPMacroReference) # for mypy all_references.append( LSPMacroReference( - uri=file_uri.value, + path=path, range=ref.range, target_range=ref.target_range, markdown_description=ref.markdown_description, @@ -822,7 +820,7 @@ def _process_column_references( scope: t.Any, reference_name: str, read_file: t.List[str], - referenced_model_uri: URI, + referenced_model_path: Path, description: t.Optional[str] = None, yaml_target_range: t.Optional[Range] = None, reference_type: t.Literal["model", "external_model", "cte"] = "model", @@ -837,7 +835,7 @@ def _process_column_references( scope: The SQL scope to search for columns reference_name: The full reference name (may include database/catalog) read_file: The file content as list of lines - referenced_model_uri: URI of the referenced model + referenced_model_path: Path of the referenced model description: Markdown description for the reference yaml_target_range: Target range for external models (YAML files) reference_type: Type of reference - "model", "external_model", or "cte" @@ -857,7 +855,7 @@ def _process_column_references( table_range = _get_column_table_range(column, read_file) references.append( LSPCteReference( - uri=referenced_model_uri.value, + path=referenced_model_path, range=table_range, target_range=cte_target_range, ) @@ -875,7 +873,7 @@ def _process_column_references( if reference_type == "external_model": references.append( LSPExternalModelReference( - uri=referenced_model_uri.value, + path=referenced_model_path, range=table_range, markdown_description=description, target_range=yaml_target_range, @@ -884,7 +882,7 @@ def _process_column_references( else: references.append( LSPModelReference( - uri=referenced_model_uri.value, + path=referenced_model_path, range=table_range, markdown_description=description, ) diff --git a/sqlmesh/lsp/rename.py b/sqlmesh/lsp/rename.py index 0dbe2594ea..c388b7a305 100644 --- a/sqlmesh/lsp/rename.py +++ b/sqlmesh/lsp/rename.py @@ -90,7 +90,7 @@ def _rename_cte(cte_references: t.List[LSPCteReference], new_name: str) -> Works changes: t.Dict[str, t.List[TextEdit]] = {} for ref in cte_references: - uri = ref.uri + uri = URI.from_path(ref.path).value if uri not in changes: changes[uri] = [] diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index 736b995410..b78ed8145c 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -25,9 +25,9 @@ def test_reference() -> None: references = get_model_definitions_for_a_path(lsp_context, active_customers_uri) assert len(references) == 1 - uri = references[0].uri - assert uri is not None - assert URI(uri) == URI.from_path(sushi_customers_path) + path = references[0].path + assert path is not None + assert path == sushi_customers_path # Check that the reference in the correct range is sushi.customers path = active_customers_uri.to_path() @@ -61,7 +61,7 @@ def test_reference_with_alias() -> None: with open(waiter_revenue_by_day_path, "r") as file: read_file = file.readlines() - assert references[0].uri.endswith("orders.py") + assert str(references[0].path).endswith("orders.py") assert get_string_from_range(read_file, references[0].range) == "sushi.orders" assert ( references[0].markdown_description @@ -76,9 +76,9 @@ def test_reference_with_alias() -> None: | end_ts | INT | | | event_date | DATE | |""" ) - assert references[1].uri.endswith("order_items.py") + assert str(references[1].path).endswith("order_items.py") assert get_string_from_range(read_file, references[1].range) == "sushi.order_items" - assert references[2].uri.endswith("items.py") + assert str(references[2].path).endswith("items.py") assert get_string_from_range(read_file, references[2].range) == "sushi.items" @@ -102,7 +102,7 @@ def test_standalone_audit_reference() -> None: references = get_model_definitions_for_a_path(lsp_context, URI.from_path(audit_path)) assert len(references) == 1 - assert references[0].uri == URI.from_path(items_path).value + assert references[0].path == items_path # Check that the reference in the correct range is sushi.items with open(audit_path, "r") as file: @@ -161,7 +161,7 @@ def test_filter_references_by_position() -> None: position_inside = Position(line=middle_line, character=middle_char) filtered = list(filter(by_position(position_inside), all_references)) assert len(filtered) == 1 - assert filtered[0].uri == reference.uri + assert filtered[0].path == reference.path assert filtered[0].range == reference.range # For testing outside position, use a position before the current reference diff --git a/tests/lsp/test_reference_cte.py b/tests/lsp/test_reference_cte.py index 32cce8dc60..c6c56fd8a5 100644 --- a/tests/lsp/test_reference_cte.py +++ b/tests/lsp/test_reference_cte.py @@ -27,7 +27,7 @@ def test_cte_parsing(): position = Position(line=ranges[1].start.line, character=ranges[1].start.character + 4) references = get_references(lsp_context, URI.from_path(sushi_customers_path), position) assert len(references) == 1 - assert references[0].uri == URI.from_path(sushi_customers_path).value + assert references[0].path == sushi_customers_path assert isinstance(references[0], LSPCteReference) assert ( references[0].range.start.line == ranges[1].start.line @@ -42,7 +42,7 @@ def test_cte_parsing(): position = Position(line=ranges[1].start.line, character=ranges[1].start.character + 4) references = get_references(lsp_context, URI.from_path(sushi_customers_path), position) assert len(references) == 1 - assert references[0].uri == URI.from_path(sushi_customers_path).value + assert references[0].path == sushi_customers_path assert isinstance(references[0], LSPCteReference) assert ( references[0].range.start.line == ranges[1].start.line diff --git a/tests/lsp/test_reference_cte_find_all.py b/tests/lsp/test_reference_cte_find_all.py index 6a29224e75..dabe1589e2 100644 --- a/tests/lsp/test_reference_cte_find_all.py +++ b/tests/lsp/test_reference_cte_find_all.py @@ -28,7 +28,7 @@ def test_cte_find_all_references(): references = get_cte_references(lsp_context, URI.from_path(sushi_customers_path), position) # Should find the definition, FROM clause, and column prefix usages assert len(references) == 4 # definition + FROM + 2 column prefix uses - assert all(ref.uri == URI.from_path(sushi_customers_path).value for ref in references) + assert all(ref.path == sushi_customers_path for ref in references) reference_ranges = [ref.range for ref in references] for expected_range in ranges: @@ -46,7 +46,7 @@ def test_cte_find_all_references(): # Should find the same references assert len(references) == 4 # definition + FROM + 2 column prefix uses - assert all(ref.uri == URI.from_path(sushi_customers_path).value for ref in references) + assert all(ref.path == sushi_customers_path for ref in references) reference_ranges = [ref.range for ref in references] for expected_range in ranges: @@ -82,7 +82,7 @@ def test_cte_find_all_references_outer(): # Should find both the definition and the usage assert len(references) == 2 - assert all(ref.uri == URI.from_path(sushi_customers_path).value for ref in references) + assert all(ref.path == sushi_customers_path for ref in references) # Verify that we found both occurrences reference_ranges = [ref.range for ref in references] @@ -101,7 +101,7 @@ def test_cte_find_all_references_outer(): # Should find the same references assert len(references) == 2 - assert all(ref.uri == URI.from_path(sushi_customers_path).value for ref in references) + assert all(ref.path == sushi_customers_path for ref in references) reference_ranges = [ref.range for ref in references] for expected_range in ranges: diff --git a/tests/lsp/test_reference_external_model.py b/tests/lsp/test_reference_external_model.py index 43c873991e..8f2c2f7c1b 100644 --- a/tests/lsp/test_reference_external_model.py +++ b/tests/lsp/test_reference_external_model.py @@ -30,20 +30,18 @@ def test_reference() -> None: assert len(references) == 1 reference = references[0] assert isinstance(reference, LSPExternalModelReference) - uri = reference.uri - assert uri is not None - assert uri.endswith("external_models.yaml") + path = reference.path + assert path is not None + assert str(path).endswith("external_models.yaml") source_range = read_range_from_file(customers, to_sqlmesh_range(reference.range)) assert source_range == "raw.demographics" if reference.target_range is None: raise AssertionError("Reference target range should not be None") - uri = reference.uri - assert uri is not None - target_range = read_range_from_file( - URI(uri).to_path(), to_sqlmesh_range(reference.target_range) - ) + path = reference.path + assert path is not None + target_range = read_range_from_file(path, to_sqlmesh_range(reference.target_range)) assert target_range == "raw.demographics" @@ -60,7 +58,7 @@ def test_unregistered_external_model(tmp_path: Path): assert len(references) == 1 reference = references[0] assert isinstance(reference, LSPExternalModelReference) - assert reference.uri is None + assert reference.path is None assert reference.target_range is None assert reference.markdown_description == "Unregistered external model" assert read_range_from_file(model_path, to_sqlmesh_range(reference.range)) == "external_model" diff --git a/tests/lsp/test_reference_macro.py b/tests/lsp/test_reference_macro.py index 705a8b48e2..e287212dd2 100644 --- a/tests/lsp/test_reference_macro.py +++ b/tests/lsp/test_reference_macro.py @@ -25,5 +25,5 @@ def test_macro_references() -> None: # Check that all references point to the utils.py file for ref in macro_references: assert isinstance(ref, LSPMacroReference) - assert ref.uri.endswith("sushi/macros/utils.py") + assert URI.from_path(ref.path).value.endswith("sushi/macros/utils.py") assert ref.target_range is not None diff --git a/tests/lsp/test_reference_macro_find_all.py b/tests/lsp/test_reference_macro_find_all.py index 03f47d1dc2..328924599a 100644 --- a/tests/lsp/test_reference_macro_find_all.py +++ b/tests/lsp/test_reference_macro_find_all.py @@ -41,11 +41,11 @@ def test_find_all_references_for_macro_add_one(): assert len(all_references) >= 2, f"Expected at least 2 references, found {len(all_references)}" # Verify the macro definition is included - definition_refs = [ref for ref in all_references if "utils.py" in ref.uri] + definition_refs = [ref for ref in all_references if "utils.py" in str(ref.path)] assert len(definition_refs) >= 1, "Should include the macro definition in utils.py" # Verify the usage in top_waiters is included - usage_refs = [ref for ref in all_references if "top_waiters" in ref.uri] + usage_refs = [ref for ref in all_references if "top_waiters" in str(ref.path)] assert len(usage_refs) >= 1, "Should include the usage in top_waiters.sql" expected_files = { @@ -55,11 +55,11 @@ def test_find_all_references_for_macro_add_one(): } for expected_file, expectations in expected_files.items(): - file_refs = [ref for ref in all_references if expected_file in ref.uri] + file_refs = [ref for ref in all_references if expected_file in str(ref.path)] assert len(file_refs) >= 1, f"Should find at least one reference in {expected_file}" file_ref = file_refs[0] - file_path = URI(file_ref.uri).to_path() + file_path = file_ref.path sqlmesh_range = SQLMeshRange( start=SQLMeshPosition( @@ -104,8 +104,10 @@ def test_find_all_references_for_macro_multiply(): assert len(all_references) >= 2, f"Expected at least 2 references, found {len(all_references)}" # Verify both definition and usage are included - assert any("utils.py" in ref.uri for ref in all_references), "Should include macro definition" - assert any("top_waiters" in ref.uri for ref in all_references), "Should include usage" + assert any("utils.py" in str(ref.path) for ref in all_references), ( + "Should include macro definition" + ) + assert any("top_waiters" in str(ref.path) for ref in all_references), "Should include usage" def test_find_all_references_for_sql_literal_macro(): @@ -183,9 +185,11 @@ def test_multi_repo_macro_references(): assert len(all_references) == 2, f"Expected 2 references, found {len(all_references)}" # Verify references from repo_2 - assert any("repo_2" in ref.uri for ref in all_references), "Should find macro in repo_2" + assert any("repo_2" in str(ref.path) for ref in all_references), ( + "Should find macro in repo_2" + ) # But not references in repo_1 since despite identical name they're different macros - assert not any("repo_1" in ref.uri for ref in all_references), ( + assert not any("repo_1" in str(ref.path) for ref in all_references), ( "Shouldn't find macro in repo_1" ) diff --git a/tests/lsp/test_reference_macro_multi.py b/tests/lsp/test_reference_macro_multi.py index db4464ffac..8226085a1d 100644 --- a/tests/lsp/test_reference_macro_multi.py +++ b/tests/lsp/test_reference_macro_multi.py @@ -20,5 +20,5 @@ def test_macro_references_multirepo() -> None: assert len(macro_references) == 2 for ref in macro_references: assert isinstance(ref, LSPMacroReference) - assert ref.uri.endswith("multi/repo_2/macros/__init__.py") + assert str(URI.from_path(ref.path).value).endswith("multi/repo_2/macros/__init__.py") assert ref.target_range is not None diff --git a/tests/lsp/test_reference_model_find_all.py b/tests/lsp/test_reference_model_find_all.py index 7bb998150f..7c0077d6cd 100644 --- a/tests/lsp/test_reference_model_find_all.py +++ b/tests/lsp/test_reference_model_find_all.py @@ -35,7 +35,7 @@ def test_find_references_for_model_usages(): ) # Verify expected files are present - reference_files = {ref.uri for ref in references} + reference_files = {str(ref.path) for ref in references} expected_patterns = [ "orders", "customers", @@ -65,7 +65,7 @@ def test_find_references_for_model_usages(): for ref in references: matched_pattern = None for pattern in expected_patterns: - if pattern in ref.uri: + if pattern in str(ref.path): matched_pattern = pattern break @@ -136,7 +136,7 @@ def test_find_references_for_marketing_model(): ) # Verify files are present - reference_files = {ref.uri for ref in references} + reference_files = {str(ref.path) for ref in references} expected_patterns = ["marketing", "customers"] for pattern in expected_patterns: assert any(pattern in uri for uri in reference_files), ( @@ -169,7 +169,7 @@ def test_find_references_for_python_model(): assert len(references) == 5 # Verify expected files - reference_files = {ref.uri for ref in references} + reference_files = {str(ref.path) for ref in references} # Models and also the Audit which references it: assert_item_price_above_zero expected_patterns = [ @@ -222,13 +222,13 @@ def test_waiter_revenue_by_day_multiple_references(): ) # Count references in top_waiters file - top_waiters_refs = [ref for ref in references if "top_waiters" in ref.uri] + top_waiters_refs = [ref for ref in references if "top_waiters" in str(ref.path)] assert len(top_waiters_refs) == 3, ( f"Expected exactly 3 references in top_waiters, found {len(top_waiters_refs)}" ) # Verify model definition is included - assert any("waiter_revenue_by_day" in ref.uri for ref in references), ( + assert any("waiter_revenue_by_day" in str(ref.path) for ref in references), ( "Should include model definition" ) @@ -296,7 +296,7 @@ def test_audit_model_references(): assert len(references) == 5, "Should find references from audit files as well" - reference_files = {ref.uri for ref in references} + reference_files = {str(ref.path) for ref in references} # Models and also the Audit which references it: assert_item_price_above_zero expected_patterns = [ From 00a0c5032229c85408f4a59e670dc545d76ad7f9 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:18:40 +0100 Subject: [PATCH 0621/1056] refactor(lsp): move references to internal types (#5039) --- sqlmesh/core/linter/helpers.py | 2 +- sqlmesh/lsp/main.py | 23 +++++++------ sqlmesh/lsp/reference.py | 33 ++++++++----------- sqlmesh/lsp/rename.py | 20 +++++++---- tests/lsp/test_reference.py | 2 +- tests/lsp/test_reference_external_model.py | 10 +++--- .../lsp/test_reference_model_column_prefix.py | 2 +- 7 files changed, 47 insertions(+), 45 deletions(-) diff --git a/sqlmesh/core/linter/helpers.py b/sqlmesh/core/linter/helpers.py index 59fd478f78..707b0a9159 100644 --- a/sqlmesh/core/linter/helpers.py +++ b/sqlmesh/core/linter/helpers.py @@ -1,6 +1,6 @@ from pathlib import Path -from sqlmesh.core.linter.rule import Position, Range +from sqlmesh.core.linter.rule import Range, Position from sqlmesh.utils.pydantic import PydanticModel from sqlglot import tokenize, TokenType import typing as t diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 24b287d74c..26100c1092 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -48,6 +48,7 @@ CustomMethod, ) from sqlmesh.lsp.errors import ContextFailedError, context_error_to_diagnostic +from sqlmesh.lsp.helpers import to_lsp_range, to_sqlmesh_position from sqlmesh.lsp.hints import get_hints from sqlmesh.lsp.reference import ( LSPCteReference, @@ -418,7 +419,7 @@ def hover(ls: LanguageServer, params: types.HoverParams) -> t.Optional[types.Hov context = self._context_get_or_load(uri) document = ls.workspace.get_text_document(params.text_document.uri) - references = get_references(context, uri, params.position) + references = get_references(context, uri, to_sqlmesh_position(params.position)) if not references: return None reference = references[0] @@ -429,7 +430,7 @@ def hover(ls: LanguageServer, params: types.HoverParams) -> t.Optional[types.Hov kind=types.MarkupKind.Markdown, value=reference.markdown_description, ), - range=reference.range, + range=to_lsp_range(reference.range), ) except Exception as e: @@ -464,7 +465,7 @@ def goto_definition( uri = URI(params.text_document.uri) context = self._context_get_or_load(uri) - references = get_references(context, uri, params.position) + references = get_references(context, uri, to_sqlmesh_position(params.position)) location_links = [] for reference in references: # Use target_range if available (CTEs, Macros, and external models in YAML) @@ -489,12 +490,12 @@ def goto_definition( end=types.Position(line=0, character=0), ) if reference.target_range is not None: - target_range = reference.target_range - target_selection_range = reference.target_range + target_range = to_lsp_range(reference.target_range) + target_selection_range = to_lsp_range(reference.target_range) else: # CTEs and Macros always have target_range - target_range = reference.target_range - target_selection_range = reference.target_range + target_range = to_lsp_range(reference.target_range) + target_selection_range = to_lsp_range(reference.target_range) if reference.path is not None: location_links.append( @@ -502,7 +503,7 @@ def goto_definition( target_uri=URI.from_path(reference.path).value, target_selection_range=target_selection_range, target_range=target_range, - origin_selection_range=reference.range, + origin_selection_range=to_lsp_range(reference.range), ) ) return location_links @@ -519,11 +520,13 @@ def find_references( uri = URI(params.text_document.uri) context = self._context_get_or_load(uri) - all_references = get_all_references(context, uri, params.position) + all_references = get_all_references( + context, uri, to_sqlmesh_position(params.position) + ) # Convert references to Location objects locations = [ - types.Location(uri=URI.from_path(ref.path).value, range=ref.range) + types.Location(uri=URI.from_path(ref.path).value, range=to_lsp_range(ref.range)) for ref in all_references if ref.path is not None ] diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 6849129a7e..6aee3e10da 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -1,4 +1,3 @@ -from lsprotocol.types import Range, Position import typing as t from pathlib import Path from pydantic import Field @@ -8,13 +7,13 @@ from sqlmesh.core.linter.helpers import ( TokenPositionDetails, ) +from sqlmesh.core.linter.rule import Range, Position from sqlmesh.core.model.definition import SqlModel, ExternalModel from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget from sqlglot import exp from sqlmesh.lsp.description import generate_markdown_description from sqlglot.optimizer.scope import build_scope -from sqlmesh.lsp.helpers import to_lsp_range, to_lsp_position from sqlmesh.lsp.uri import URI from sqlmesh.utils.pydantic import PydanticModel from sqlglot.optimizer.normalize_identifiers import normalize_identifiers @@ -203,15 +202,11 @@ def get_model_definitions_for_a_path( table.this.meta ).to_range(read_file) - # Convert SQLMesh Range to LSP Range - target_range = to_lsp_range(target_range_sqlmesh) - table_range = to_lsp_range(table_range_sqlmesh) - references.append( LSPCteReference( path=document_uri.to_path(), # Same file - range=table_range, - target_range=target_range, + range=table_range_sqlmesh, + target_range=target_range_sqlmesh, ) ) @@ -222,7 +217,7 @@ def get_model_definitions_for_a_path( referenced_model_path=document_uri.to_path(), description="", reference_type="cte", - cte_target_range=target_range, + cte_target_range=target_range_sqlmesh, ) references.extend(column_references) continue @@ -257,8 +252,8 @@ def get_model_definitions_for_a_path( references.append( LSPExternalModelReference( range=Range( - start=to_lsp_position(start_pos_sqlmesh), - end=to_lsp_position(end_pos_sqlmesh), + start=start_pos_sqlmesh, + end=end_pos_sqlmesh, ), markdown_description="Unregistered external model", ) @@ -300,8 +295,8 @@ def get_model_definitions_for_a_path( LSPExternalModelReference( path=referenced_model_path, range=Range( - start=to_lsp_position(start_pos_sqlmesh), - end=to_lsp_position(end_pos_sqlmesh), + start=start_pos_sqlmesh, + end=end_pos_sqlmesh, ), markdown_description=description, target_range=yaml_target_range, @@ -325,8 +320,8 @@ def get_model_definitions_for_a_path( LSPModelReference( path=referenced_model_path, range=Range( - start=to_lsp_position(start_pos_sqlmesh), - end=to_lsp_position(end_pos_sqlmesh), + start=start_pos_sqlmesh, + end=end_pos_sqlmesh, ), markdown_description=description, ) @@ -432,7 +427,7 @@ def get_macro_reference( macro_range = TokenPositionDetails.from_meta(node.meta).to_range(read_file) # Check if it's a built-in method - if builtin := get_built_in_macro_reference(macro_name, to_lsp_range(macro_range)): + if builtin := get_built_in_macro_reference(macro_name, macro_range): return builtin else: # Skip if we can't get the position @@ -483,7 +478,7 @@ def get_macro_reference( return LSPMacroReference( path=path, - range=to_lsp_range(macro_range), + range=macro_range, target_range=Range( start=Position(line=start_line - 1, character=0), end=Position(line=end_line - 1, character=get_length_of_end_line), @@ -811,8 +806,8 @@ def _get_column_table_range(column: exp.Column, read_file: t.List[str]) -> Range end_range = TokenPositionDetails.from_meta(table_parts[-1].meta).to_range(read_file) return Range( - start=to_lsp_position(start_range.start), - end=to_lsp_position(end_range.end), + start=start_range.start, + end=end_range.end, ) diff --git a/sqlmesh/lsp/rename.py b/sqlmesh/lsp/rename.py index c388b7a305..31f7eb3200 100644 --- a/sqlmesh/lsp/rename.py +++ b/sqlmesh/lsp/rename.py @@ -9,6 +9,7 @@ ) from sqlmesh.lsp.context import LSPContext +from sqlmesh.lsp.helpers import to_sqlmesh_position, to_lsp_range from sqlmesh.lsp.reference import ( _position_within_range, get_cte_references, @@ -18,7 +19,7 @@ def prepare_rename( - lsp_context: LSPContext, document_uri: URI, position: Position + lsp_context: LSPContext, document_uri: URI, lsp_position: Position ) -> t.Optional[PrepareRenameResult_Type1]: """ Prepare for rename operation by checking if the symbol at the position can be renamed. @@ -32,6 +33,7 @@ def prepare_rename( PrepareRenameResult if the symbol can be renamed, None otherwise """ # Check if there's a CTE at this position + position = to_sqlmesh_position(lsp_position) cte_references = get_cte_references(lsp_context, document_uri, position) if cte_references: # Find the target CTE definition to get its range @@ -46,14 +48,16 @@ def prepare_rename( target_range = ref.target_range break if target_range: - return PrepareRenameResult_Type1(range=target_range, placeholder="cte_name") + return PrepareRenameResult_Type1( + range=to_lsp_range(target_range), placeholder="cte_name" + ) # For now, only CTEs are supported return None def rename_symbol( - lsp_context: LSPContext, document_uri: URI, position: Position, new_name: str + lsp_context: LSPContext, document_uri: URI, lsp_position: Position, new_name: str ) -> t.Optional[WorkspaceEdit]: """ Perform rename operation on the symbol at the given position. @@ -68,7 +72,9 @@ def rename_symbol( WorkspaceEdit with the changes, or None if no symbol to rename """ # Check if there's a CTE at this position - cte_references = get_cte_references(lsp_context, document_uri, position) + cte_references = get_cte_references( + lsp_context, document_uri, to_sqlmesh_position(lsp_position) + ) if cte_references: return _rename_cte(cte_references, new_name) @@ -95,7 +101,7 @@ def _rename_cte(cte_references: t.List[LSPCteReference], new_name: str) -> Works changes[uri] = [] # Create a text edit for this reference - text_edit = TextEdit(range=ref.range, new_text=new_name) + text_edit = TextEdit(range=to_lsp_range(ref.range), new_text=new_name) changes[uri].append(text_edit) return WorkspaceEdit(changes=changes) @@ -119,7 +125,7 @@ def get_document_highlights( List of DocumentHighlight objects or None if no symbol found """ # Check if there's a CTE at this position - cte_references = get_cte_references(lsp_context, document_uri, position) + cte_references = get_cte_references(lsp_context, document_uri, to_sqlmesh_position(position)) if cte_references: highlights = [] for ref in cte_references: @@ -130,7 +136,7 @@ def get_document_highlights( else DocumentHighlightKind.Read ) - highlights.append(DocumentHighlight(range=ref.range, kind=kind)) + highlights.append(DocumentHighlight(range=to_lsp_range(ref.range), kind=kind)) return highlights # For now, only CTEs are supported diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index b78ed8145c..3d1e19f3cc 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -1,5 +1,5 @@ -from lsprotocol.types import Position from sqlmesh.core.context import Context +from sqlmesh.core.linter.rule import Position from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget from sqlmesh.lsp.reference import LSPModelReference, get_model_definitions_for_a_path, by_position from sqlmesh.lsp.uri import URI diff --git a/tests/lsp/test_reference_external_model.py b/tests/lsp/test_reference_external_model.py index 8f2c2f7c1b..36c64fe277 100644 --- a/tests/lsp/test_reference_external_model.py +++ b/tests/lsp/test_reference_external_model.py @@ -1,12 +1,10 @@ from pathlib import Path -from lsprotocol.types import Position - from sqlmesh import Config from sqlmesh.core.context import Context from sqlmesh.core.linter.helpers import read_range_from_file +from sqlmesh.core.linter.rule import Position from sqlmesh.lsp.context import LSPContext, ModelTarget -from sqlmesh.lsp.helpers import to_sqlmesh_range from sqlmesh.lsp.reference import get_references, LSPExternalModelReference from sqlmesh.lsp.uri import URI from tests.utils.test_filesystem import create_temp_file @@ -34,14 +32,14 @@ def test_reference() -> None: assert path is not None assert str(path).endswith("external_models.yaml") - source_range = read_range_from_file(customers, to_sqlmesh_range(reference.range)) + source_range = read_range_from_file(customers, reference.range) assert source_range == "raw.demographics" if reference.target_range is None: raise AssertionError("Reference target range should not be None") path = reference.path assert path is not None - target_range = read_range_from_file(path, to_sqlmesh_range(reference.target_range)) + target_range = read_range_from_file(path, reference.target_range) assert target_range == "raw.demographics" @@ -61,4 +59,4 @@ def test_unregistered_external_model(tmp_path: Path): assert reference.path is None assert reference.target_range is None assert reference.markdown_description == "Unregistered external model" - assert read_range_from_file(model_path, to_sqlmesh_range(reference.range)) == "external_model" + assert read_range_from_file(model_path, reference.range) == "external_model" diff --git a/tests/lsp/test_reference_model_column_prefix.py b/tests/lsp/test_reference_model_column_prefix.py index 01b91de570..3cd25a080e 100644 --- a/tests/lsp/test_reference_model_column_prefix.py +++ b/tests/lsp/test_reference_model_column_prefix.py @@ -1,8 +1,8 @@ from pathlib import Path -from lsprotocol.types import Position from sqlmesh.cli.project_init import init_example_project from sqlmesh.core.context import Context +from sqlmesh.core.linter.rule import Position from sqlmesh.lsp.context import LSPContext, ModelTarget from sqlmesh.lsp.reference import get_all_references from sqlmesh.lsp.uri import URI From e430f5014c72f70b135144352e9db2d65272b563 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:39:14 +0300 Subject: [PATCH 0622/1056] Chore: fail if >1 migration scripts start with the same version (#5038) --- .pre-commit-config.yaml | 5 +++ tooling/validating_migration_numbers.sh | 53 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100755 tooling/validating_migration_numbers.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 96eca9b6a4..e4b61cb0f3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,3 +23,8 @@ repos: files: *files require_serial: true exclude: ^(tests/fixtures) + - id: valid migrations + name: valid migrations + entry: tooling/validating_migration_numbers.sh + language: system + pass_filenames: false diff --git a/tooling/validating_migration_numbers.sh b/tooling/validating_migration_numbers.sh new file mode 100755 index 0000000000..6dbb597dc1 --- /dev/null +++ b/tooling/validating_migration_numbers.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Navigate to the migrations directory (modify the path if necessary) +cd "sqlmesh/migrations" || exit 1 + + +# Collect all migration files matching the pattern (e.g., v0001_initial.py) +migration_files=(v*.py) + +# Initialize an array to hold migration numbers +numbers=() + +# Extract migration numbers from filenames +for file in "${migration_files[@]}"; do + if [[ $file =~ ^v0*([0-9]+)_ ]]; then + num=${BASH_REMATCH[1]} + numbers+=("$num") + fi +done + +# Check if any migration files were found +if [[ ${#numbers[@]} -eq 0 ]]; then + echo "No migration files found matching the pattern 'v_.py'." + exit 1 +fi + +# Check for duplicate migration numbers +duplicates=$(printf "%s\n" "${numbers[@]}" | sort | uniq -d) +if [[ -n $duplicates ]]; then + echo "Error: Duplicate migration numbers found: $duplicates" + exit 1 +fi + +# Sort the migration numbers +sorted_numbers=($(printf "%s\n" "${numbers[@]}" | sort -n)) + +# Get the first and last migration numbers +first_number="${sorted_numbers[0]}" +last_index=$((${#sorted_numbers[@]} - 1)) +last_number="${sorted_numbers[$last_index]}" + +# Check for gaps in the migration sequence +expected_numbers=($(seq "$first_number" "$last_number")) + +if [[ "${sorted_numbers[*]}" != "${expected_numbers[*]}" ]]; then + echo "Error: Missing migration numbers in sequence." + echo "Expected sequence: ${expected_numbers[*]}" + echo "Found sequence: ${sorted_numbers[*]}" + exit 1 +fi + +echo "All migration numbers are sequential and without overlaps." +exit 0 From 3ee4e9fa1b9e0f08d97dc458e837e7feb13137ac Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:11:51 +0100 Subject: [PATCH 0623/1056] chore(vscode): turn on trace to help debug (#5041) --- vscode/extension/playwright.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vscode/extension/playwright.config.ts b/vscode/extension/playwright.config.ts index 8b6beaf500..f1867e15ac 100644 --- a/vscode/extension/playwright.config.ts +++ b/vscode/extension/playwright.config.ts @@ -27,6 +27,7 @@ export default defineConfig({ }, viewport: { width: 1512, height: 944 }, video: 'retain-on-failure', + trace: 'retain-on-first-failure', }, dependencies: ['setup'], }, From 57d2e9f812ac24f817d8800a7b210088a5c44c8b Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:22:18 +0300 Subject: [PATCH 0624/1056] Chore: log rendered jinja right before parsing it to improve debugging (#5043) --- sqlmesh/core/renderer.py | 3 +++ tests/core/test_model.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index f2e9e24056..dc89e15af6 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -215,6 +215,9 @@ def _resolve_table(table: str | exp.Table) -> str: try: expressions = [] rendered_expression = jinja_env.from_string(self._expression.name).render() + logger.debug( + f"Rendered Jinja expression for model '{self._model_fqn}' at '{self._path}': '{rendered_expression}'" + ) if rendered_expression.strip(): expressions = [e for e in parse(rendered_expression, read=self._dialect) if e] diff --git a/tests/core/test_model.py b/tests/core/test_model.py index a99920b420..b7c74f90a2 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -8534,6 +8534,38 @@ def test_comments_in_jinja_query(): model.render_query() +def test_jinja_render_debug_logging(caplog): + """Test that rendered Jinja expressions are logged for debugging.""" + import logging + + # Set log level to DEBUG to capture debug logs + caplog.set_level(logging.DEBUG, logger="sqlmesh.core.renderer") + + # Create a model with unparseable Jinja that will be rendered + expressions = d.parse( + """ + MODEL (name db.test_model); + + JINJA_QUERY_BEGIN; + {{ 'SELECT invalid syntax here!' }} + JINJA_END; + """ + ) + + model = load_sql_based_model(expressions) + + # Attempt to render - this should fail due to invalid SQL syntax + with pytest.raises(ConfigError, match=r"Could not render or parse jinja"): + model.render_query() + + # Check that the rendered Jinja was logged + assert any( + 'Rendered Jinja expression for model \'"db"."test_model"\'' in record.message + and "SELECT invalid syntax here!" in record.message + for record in caplog.records + ) + + def test_staged_file_path(): expressions = d.parse( """ From 82cdde02909fb9aaf16a93e1a1131f522cd5c404 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:14:37 -0500 Subject: [PATCH 0625/1056] Feat: add partitioning for ducklake databases (#5030) --- sqlmesh/core/engine_adapter/duckdb.py | 53 ++++++++++++++++++++++++ tests/core/engine_adapter/test_duckdb.py | 37 ++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/engine_adapter/duckdb.py b/sqlmesh/core/engine_adapter/duckdb.py index 169a7a7f94..00be5f426a 100644 --- a/sqlmesh/core/engine_adapter/duckdb.py +++ b/sqlmesh/core/engine_adapter/duckdb.py @@ -142,3 +142,56 @@ def _normalize_decimal_value(self, col: exp.Expression, precision: int) -> exp.E exp.cast(col, "DOUBLE"), f"DECIMAL(38, {precision})", ) + + def _create_table( + self, + table_name_or_schema: t.Union[exp.Schema, TableName], + expression: t.Optional[exp.Expression], + exists: bool = True, + replace: bool = False, + columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + table_description: t.Optional[str] = None, + column_descriptions: t.Optional[t.Dict[str, str]] = None, + table_kind: t.Optional[str] = None, + **kwargs: t.Any, + ) -> None: + catalog = self.get_current_catalog() + catalog_type_tuple = self.fetchone( + exp.select("type") + .from_("duckdb_databases()") + .where(exp.column("database_name").eq(catalog)) + ) + catalog_type = catalog_type_tuple[0] if catalog_type_tuple else None + + partitioned_by_exps = None + if catalog_type == "ducklake": + partitioned_by_exps = kwargs.pop("partitioned_by", None) + + super()._create_table( + table_name_or_schema, + expression, + exists, + replace, + columns_to_types, + table_description, + column_descriptions, + table_kind, + **kwargs, + ) + + if partitioned_by_exps: + # Schema object contains column definitions, so we extract Table + table_name = ( + table_name_or_schema.this + if isinstance(table_name_or_schema, exp.Schema) + else table_name_or_schema + ) + table_name_str = ( + table_name.sql(dialect=self.dialect) + if isinstance(table_name, exp.Table) + else table_name + ) + partitioned_by_str = ", ".join( + expr.sql(dialect=self.dialect) for expr in partitioned_by_exps + ) + self.execute(f"ALTER TABLE {table_name_str} SET PARTITIONED BY ({partitioned_by_str});") diff --git a/tests/core/engine_adapter/test_duckdb.py b/tests/core/engine_adapter/test_duckdb.py index 543b2e2f18..6442b1a0b4 100644 --- a/tests/core/engine_adapter/test_duckdb.py +++ b/tests/core/engine_adapter/test_duckdb.py @@ -2,9 +2,9 @@ import pandas as pd # noqa: TID253 import pytest +from pytest_mock.plugin import MockerFixture from sqlglot import expressions as exp from sqlglot import parse_one - from sqlmesh.core.engine_adapter import DuckDBEngineAdapter, EngineAdapter from tests.core.engine_adapter import to_sql_calls @@ -77,9 +77,12 @@ def test_set_current_catalog(make_mocked_engine_adapter: t.Callable, duck_conn): ] -def test_temporary_table(make_mocked_engine_adapter: t.Callable, duck_conn): +def test_temporary_table(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): adapter = make_mocked_engine_adapter(DuckDBEngineAdapter) + mocker.patch.object(adapter, "get_current_catalog", return_value="test_catalog") + mocker.patch.object(adapter, "fetchone", return_value=("test_catalog",)) + adapter.create_table( "test_table", {"a": exp.DataType.build("INT"), "b": exp.DataType.build("INT")}, @@ -103,3 +106,33 @@ def test_drop_catalog(make_mocked_engine_adapter: t.Callable) -> None: adapter.drop_catalog(exp.to_identifier("foo")) assert to_sql_calls(adapter) == ['DETACH DATABASE IF EXISTS "foo"'] + + +def test_ducklake_partitioning(adapter: EngineAdapter, duck_conn, tmp_path): + catalog = "a_ducklake_db" + + duck_conn.install_extension("ducklake") + duck_conn.load_extension("ducklake") + duck_conn.execute( + f"ATTACH 'ducklake:{catalog}.ducklake' AS {catalog} (DATA_PATH '{tmp_path}');" + ) + + # no partitions on catalog creation + partition_info = duck_conn.execute( + f"SELECT * FROM __ducklake_metadata_{catalog}.main.ducklake_partition_info" + ).fetchdf() + assert partition_info.empty + + adapter.set_current_catalog(catalog) + adapter.create_schema("test_schema") + adapter.create_table( + "test_schema.test_table", + {"a": exp.DataType.build("INT"), "b": exp.DataType.build("INT")}, + partitioned_by=[exp.to_column("a"), exp.to_column("b")], + ) + + # 1 partition after table creation + partition_info = duck_conn.execute( + f"SELECT * FROM __ducklake_metadata_{catalog}.main.ducklake_partition_info" + ).fetchdf() + assert partition_info.shape[0] == 1 From 683a373a085b7b1874a2d8753983757d33a4a240 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:52:51 +0100 Subject: [PATCH 0626/1056] fix: stopped server would just restart (#5051) --- vscode/extension/src/commands/stop.ts | 2 +- vscode/extension/src/extension.ts | 8 +-- vscode/extension/src/lsp/lsp.ts | 29 ++++++++-- vscode/extension/tests/stop.spec.ts | 82 ++++++++++++++++++++++++++- 4 files changed, 111 insertions(+), 10 deletions(-) diff --git a/vscode/extension/src/commands/stop.ts b/vscode/extension/src/commands/stop.ts index f0276acf8b..429d6fa7b6 100644 --- a/vscode/extension/src/commands/stop.ts +++ b/vscode/extension/src/commands/stop.ts @@ -11,7 +11,7 @@ export const stop = (lspClient: LSPClient | undefined) => { return } - await lspClient.stop() + await lspClient.stop(true) await window.showInformationMessage('LSP server stopped') traceInfo('LSP server stopped successfully') } diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 10e5b9953c..de5d35d706 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -115,10 +115,10 @@ export async function activate(context: vscode.ExtensionContext) { }), ) - const restart = async () => { + const restart = async (invokedByUser = false) => { if (lspClient) { traceVerbose('Restarting LSP client') - const restartResult = await lspClient.restart() + const restartResult = await lspClient.restart(invokedByUser) if (isErr(restartResult)) { return handleError( authProvider, @@ -130,7 +130,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(lspClient) } else { lspClient = new LSPClient() - const result = await lspClient.start() + const result = await lspClient.start(invokedByUser) if (isErr(result)) { return handleError( authProvider, @@ -159,7 +159,7 @@ export async function activate(context: vscode.ExtensionContext) { await restart() }), registerCommand(`sqlmesh.restart`, async () => { - await restart() + await restart(true) }), registerCommand(`sqlmesh.stop`, stop(lspClient)), registerCommand(`sqlmesh.printEnvironment`, printEnvironment()), diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 8195876f48..f5c7532ae4 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -36,6 +36,13 @@ export class LSPClient implements Disposable { */ private supportedMethodsState: SupportedMethodsState = { type: 'not-fetched' } + /** + * Explicitly stopped remembers whether the LSP client has been explicitly stopped + * by the user. This is used to prevent the client from being restarted unless the user + * explicitly calls the `restart` method. + */ + private explicitlyStopped = false + constructor() { this.client = undefined } @@ -54,7 +61,15 @@ export class LSPClient implements Disposable { return completion !== undefined } - public async start(): Promise> { + public async start( + overrideStoppedByUser = false, + ): Promise> { + if (this.explicitlyStopped && !overrideStoppedByUser) { + traceInfo( + 'LSP client has been explicitly stopped by user, not starting again.', + ) + return ok(undefined) + } if (!outputChannel) { outputChannel = window.createOutputChannel('sqlmesh-lsp') } @@ -124,18 +139,24 @@ export class LSPClient implements Disposable { return ok(undefined) } - public async restart(): Promise> { + public async restart( + overrideByUser = false, + ): Promise> { await this.stop() - return await this.start() + return await this.start(overrideByUser) } - public async stop() { + public async stop(stoppedByUser = false): Promise { if (this.client) { await this.client.stop() this.client = undefined // Reset supported methods state when the client stops this.supportedMethodsState = { type: 'not-fetched' } } + if (stoppedByUser) { + this.explicitlyStopped = true + traceInfo('SQLMesh LSP client stopped by user.') + } } public async dispose() { diff --git a/vscode/extension/tests/stop.spec.ts b/vscode/extension/tests/stop.spec.ts index 611d88d878..b15638be63 100644 --- a/vscode/extension/tests/stop.spec.ts +++ b/vscode/extension/tests/stop.spec.ts @@ -18,7 +18,6 @@ test('Stop server works', async ({ page, sharedCodeServer }) => { // Navigate to code-server instance await openServerPage(page, tempDir, sharedCodeServer) - await page.waitForSelector('[role="application"]', { timeout: 10000 }) // Wait for the models folder to be visible in the file explorer await page.waitForSelector('text=models') @@ -52,3 +51,84 @@ test('Stop server works', async ({ page, sharedCodeServer }) => { 'text="Failed to render model: LSP client not ready."', ) }) + +test('Stopped server only restarts when explicitly requested', async ({ + page, + sharedCodeServer, +}) => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + await createPythonInterpreterSettingsSpecifier(tempDir) + + // Navigate to code-server instance + await openServerPage(page, tempDir, sharedCodeServer) + + // Wait for the models folder to be visible in the file explorer + await page.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customers.sql model + await page + .getByRole('treeitem', { name: 'marketing.sql', exact: true }) + .locator('a') + .click() + await page.waitForSelector('text=grain') + await waitForLoadedSQLMesh(page) + + // Click on sushi.raw_marketing + await page.getByText('sushi.raw_marketing;').click() + + // Open the preview hover + await runCommand(page, 'Show Definition Preview Hover') + + // Assert that the hover is visible with text "Table of marketing status." + await page.waitForSelector('text=Table of marketing status.', { + timeout: 5_000, + state: 'visible', + }) + + // Hit Esc to close the hover + await page.keyboard.press('Escape') + + // Assert that the hover is no longer visible + await page.waitForSelector('text=Table of marketing status.', { + timeout: 5_000, + state: 'hidden', + }) + + // Stop the server + await runCommand(page, 'SQLMesh: Stop Server') + + // Await LSP server stopped message + await page.waitForSelector('text=LSP server stopped') + + // Open the preview hover again + await runCommand(page, 'Show Definition Preview Hover') + + // Assert that the hover is not visible + await page.waitForSelector('text=Table of marketing status.', { + timeout: 5_000, + state: 'hidden', + }) + + // Restart the server explicitly + await runCommand(page, 'SQLMesh: Restart Server') + + // Await LSP server started message + await waitForLoadedSQLMesh(page) + + // Open the preview hover again + await runCommand(page, 'Show Definition Preview Hover') + + // Assert that the hover is visible with text "Table of marketing status." + await page.waitForSelector('text=Table of marketing status.', { + timeout: 5_000, + state: 'visible', + }) +}) From 8f2947beb9bdc6505b45d8afb1ae5a5f2fb2499c Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 28 Jul 2025 22:35:30 +0100 Subject: [PATCH 0627/1056] feat(vscode): add test recognition to vscode (#5019) --- sqlmesh/lsp/context.py | 69 +++++++++++- sqlmesh/lsp/custom.py | 51 +++++++++ sqlmesh/lsp/main.py | 56 ++++++++++ sqlmesh/lsp/tests_ranges.py | 65 +++++++++++ tests/lsp/test_context.py | 43 ++++++++ vscode/extension/src/extension.ts | 8 ++ vscode/extension/src/lsp/custom.ts | 65 +++++++++++ vscode/extension/src/tests/tests.ts | 155 +++++++++++++++++++++++++++ vscode/extension/tests/tests.spec.ts | 42 ++++++++ 9 files changed, 553 insertions(+), 1 deletion(-) create mode 100644 sqlmesh/lsp/tests_ranges.py create mode 100644 vscode/extension/src/tests/tests.ts create mode 100644 vscode/extension/tests/tests.spec.ts diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 30adfce5a2..43eb9c8f16 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -3,10 +3,12 @@ from sqlmesh.core.context import Context import typing as t +from sqlmesh.core.linter.rule import Range from sqlmesh.core.model.definition import SqlModel from sqlmesh.core.linter.definition import AnnotatedRuleViolation -from sqlmesh.lsp.custom import ModelForRendering +from sqlmesh.lsp.custom import ModelForRendering, TestEntry, RunTestResponse from sqlmesh.lsp.custom import AllModelsResponse, RenderModelEntry +from sqlmesh.lsp.tests_ranges import get_test_ranges from sqlmesh.lsp.uri import URI from lsprotocol import types @@ -63,6 +65,71 @@ def __init__(self, context: Context) -> None: **audit_map, } + def list_workspace_tests(self) -> t.List[TestEntry]: + """List all tests in the workspace.""" + tests = self.context.load_model_tests() + + # Use a set to ensure unique URIs + unique_test_uris = {URI.from_path(test.path).value for test in tests} + test_uris: t.Dict[str, t.Dict[str, Range]] = {} + for uri in unique_test_uris: + test_ranges = get_test_ranges(URI(uri).to_path()) + if uri not in test_uris: + test_uris[uri] = {} + test_uris[uri].update(test_ranges) + return [ + TestEntry( + name=test.test_name, + uri=URI.from_path(test.path).value, + range=test_uris.get(URI.from_path(test.path).value, {}).get(test.test_name), + ) + for test in tests + ] + + def get_document_tests(self, uri: URI) -> t.List[TestEntry]: + """Get tests for a specific document. + + Args: + uri: The URI of the file to get tests for. + + Returns: + List of TestEntry objects for the specified document. + """ + tests = self.context.load_model_tests(tests=[str(uri.to_path())]) + test_ranges = get_test_ranges(uri.to_path()) + return [ + TestEntry( + name=test.test_name, + uri=URI.from_path(test.path).value, + range=test_ranges.get(test.test_name), + ) + for test in tests + ] + + def run_test(self, uri: URI, test_name: str) -> RunTestResponse: + """Run a specific test for a model. + + Args: + uri: The URI of the file containing the test. + test_name: The name of the test to run. + + Returns: + List of annotated rule violations from the test run. + """ + path = uri.to_path() + results = self.context.test( + tests=[str(path)], + match_patterns=[test_name], + ) + if results.testsRun != 1: + raise ValueError(f"Expected to run 1 test, but ran {results.testsRun} tests.") + if len(results.successes) == 1: + return RunTestResponse(success=True) + return RunTestResponse( + success=False, + error_message=str(results.failures[0][1]), + ) + def render_model(self, uri: URI) -> t.List[RenderModelEntry]: """Get rendered models for a file, using cache when available. diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py index 618b4a44bc..8ad6418401 100644 --- a/sqlmesh/lsp/custom.py +++ b/sqlmesh/lsp/custom.py @@ -1,5 +1,7 @@ from lsprotocol import types import typing as t + +from sqlmesh.core.linter.rule import Range from sqlmesh.utils.pydantic import PydanticModel @@ -143,3 +145,52 @@ class FormatProjectResponse(CustomMethodResponseBaseClass): """ pass + + +LIST_WORKSPACE_TESTS_FEATURE = "sqlmesh/list_workspace_tests" + + +class ListWorkspaceTestsRequest(CustomMethodRequestBaseClass): + """ + Request to list all tests in the current project. + """ + + pass + + +class TestEntry(PydanticModel): + """ + An entry representing a test in the workspace. + """ + + name: str + uri: str + range: Range + + +class ListWorkspaceTestsResponse(CustomMethodResponseBaseClass): + tests: t.List[TestEntry] + + +LIST_DOCUMENT_TESTS_FEATURE = "sqlmesh/list_document_tests" + + +class ListDocumentTestsRequest(CustomMethodRequestBaseClass): + textDocument: types.TextDocumentIdentifier + + +class ListDocumentTestsResponse(CustomMethodResponseBaseClass): + tests: t.List[TestEntry] + + +RUN_TEST_FEATURE = "sqlmesh/run_test" + + +class RunTestRequest(CustomMethodRequestBaseClass): + textDocument: types.TextDocumentIdentifier + testName: str + + +class RunTestResponse(CustomMethodResponseBaseClass): + success: bool + error_message: t.Optional[str] = None diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 26100c1092..3839245a08 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -46,6 +46,15 @@ FormatProjectRequest, FormatProjectResponse, CustomMethod, + LIST_WORKSPACE_TESTS_FEATURE, + ListWorkspaceTestsRequest, + ListWorkspaceTestsResponse, + LIST_DOCUMENT_TESTS_FEATURE, + ListDocumentTestsRequest, + ListDocumentTestsResponse, + RUN_TEST_FEATURE, + RunTestRequest, + RunTestResponse, ) from sqlmesh.lsp.errors import ContextFailedError, context_error_to_diagnostic from sqlmesh.lsp.helpers import to_lsp_range, to_sqlmesh_position @@ -127,11 +136,58 @@ def __init__( API_FEATURE: self._custom_api, SUPPORTED_METHODS_FEATURE: self._custom_supported_methods, FORMAT_PROJECT_FEATURE: self._custom_format_project, + LIST_WORKSPACE_TESTS_FEATURE: self._list_workspace_tests, + LIST_DOCUMENT_TESTS_FEATURE: self._list_document_tests, + RUN_TEST_FEATURE: self._run_test, } # Register LSP features (e.g., formatting, hover, etc.) self._register_features() + def _list_workspace_tests( + self, + ls: LanguageServer, + params: ListWorkspaceTestsRequest, + ) -> ListWorkspaceTestsResponse: + """List all tests in the current workspace.""" + try: + context = self._context_get_or_load() + tests = context.list_workspace_tests() + return ListWorkspaceTestsResponse(tests=tests) + except Exception as e: + ls.log_trace(f"Error listing workspace tests: {e}") + return ListWorkspaceTestsResponse(tests=[]) + + def _list_document_tests( + self, + ls: LanguageServer, + params: ListDocumentTestsRequest, + ) -> ListDocumentTestsResponse: + """List tests for a specific document.""" + try: + uri = URI(params.textDocument.uri) + context = self._context_get_or_load(uri) + tests = context.get_document_tests(uri) + return ListDocumentTestsResponse(tests=tests) + except Exception as e: + ls.log_trace(f"Error listing document tests: {e}") + return ListDocumentTestsResponse(tests=[]) + + def _run_test( + self, + ls: LanguageServer, + params: RunTestRequest, + ) -> RunTestResponse: + """Run a specific test.""" + try: + uri = URI(params.textDocument.uri) + context = self._context_get_or_load(uri) + result = context.run_test(uri, params.testName) + return result + except Exception as e: + ls.log_trace(f"Error running test: {e}") + return RunTestResponse(success=False, response_error=str(e)) + # All the custom LSP methods are registered here and prefixed with _custom def _custom_all_models(self, ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse: uri = URI(params.textDocument.uri) diff --git a/sqlmesh/lsp/tests_ranges.py b/sqlmesh/lsp/tests_ranges.py new file mode 100644 index 0000000000..cbcb33d8b6 --- /dev/null +++ b/sqlmesh/lsp/tests_ranges.py @@ -0,0 +1,65 @@ +""" +Provides helper functions to get ranges of tests in SQLMesh LSP. +""" + +from pathlib import Path + +from sqlmesh.core.linter.rule import Range, Position +from ruamel import yaml +from ruamel.yaml.comments import CommentedMap +import typing as t + + +def get_test_ranges( + path: Path, +) -> t.Dict[str, Range]: + """ + Test files are yaml files with a stucture of dict to test information. This returns a dictionary + with the test name as the key and the range of the test in the file as the value. + """ + test_ranges: t.Dict[str, Range] = {} + + with open(path, "r", encoding="utf-8") as file: + content = file.read() + + # Parse YAML to get line numbers + yaml_obj = yaml.YAML() + yaml_obj.preserve_quotes = True + data = yaml_obj.load(content) + + if not isinstance(data, dict): + raise ValueError("Invalid test file format: expected a dictionary at the top level.") + + # For each top-level key (test name), find its range + for test_name in data: + if isinstance(data, CommentedMap) and test_name in data.lc.data: + # Get line and column info from ruamel yaml + line_info = data.lc.data[test_name] + start_line = line_info[0] # 0-based line number + start_col = line_info[1] # 0-based column number + + # Find the end of this test by looking for the next test or end of file + lines = content.splitlines() + end_line = start_line + + # Find where this test ends by looking for the next top-level key + # or the end of the file + for i in range(start_line + 1, len(lines)): + line = lines[i] + # Check if this line starts a new top-level key (no leading spaces) + if line and not line[0].isspace() and ":" in line: + end_line = i - 1 + break + else: + # This test goes to the end of the file + end_line = len(lines) - 1 + + # Create the range + test_ranges[test_name] = Range( + start=Position(line=start_line, character=start_col), + end=Position( + line=end_line, character=len(lines[end_line]) if end_line < len(lines) else 0 + ), + ) + + return test_ranges diff --git a/tests/lsp/test_context.py b/tests/lsp/test_context.py index c26e8f35d5..b463a17139 100644 --- a/tests/lsp/test_context.py +++ b/tests/lsp/test_context.py @@ -1,5 +1,8 @@ +from pathlib import Path + from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget +from sqlmesh.lsp.uri import URI def test_lsp_context(): @@ -18,3 +21,43 @@ def test_lsp_context(): # Check that the value is a ModelInfo with the expected model name assert isinstance(lsp_context.map[active_customers_key], ModelTarget) assert "sushi.active_customers" in lsp_context.map[active_customers_key].names + + +def test_lsp_context_list_workspace_tests(): + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # List workspace tests + tests = lsp_context.list_workspace_tests() + + # Check that the tests are returned correctly + assert len(tests) == 3 + assert any(test.name == "test_order_items" for test in tests) + + +def test_lsp_context_get_document_tests(): + test_path = Path.cwd() / "examples/sushi/tests/test_order_items.yaml" + uri = URI.from_path(test_path) + + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + tests = lsp_context.get_document_tests(uri) + + assert len(tests) == 1 + assert tests[0].uri == uri.value + assert tests[0].name == "test_order_items" + + +def test_lsp_context_run_test(): + test_path = Path.cwd() / "examples/sushi/tests/test_order_items.yaml" + uri = URI.from_path(test_path) + + context = Context(paths=["examples/sushi"]) + lsp_context = LSPContext(context) + + # Run the test + result = lsp_context.run_test(uri, "test_order_items") + + # Check that the result is not None and has the expected properties + assert result is not None + assert result.success is True diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index de5d35d706..74454f8fdb 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -21,6 +21,10 @@ import { selector, completionProvider } from './completion/completion' import { LineagePanel } from './webviews/lineagePanel' import { RenderedModelProvider } from './providers/renderedModelProvider' import { sleep } from './utilities/sleep' +import { + controller as testController, + setupTestController, +} from './tests/tests' let lspClient: LSPClient | undefined @@ -128,6 +132,7 @@ export async function activate(context: vscode.ExtensionContext) { ) } context.subscriptions.push(lspClient) + context.subscriptions.push(setupTestController(lspClient)) } else { lspClient = new LSPClient() const result = await lspClient.start(invokedByUser) @@ -140,6 +145,7 @@ export async function activate(context: vscode.ExtensionContext) { ) } else { context.subscriptions.push(lspClient) + context.subscriptions.push(setupTestController(lspClient)) } } } @@ -175,6 +181,8 @@ export async function activate(context: vscode.ExtensionContext) { ) } else { context.subscriptions.push(lspClient) + context.subscriptions.push(setupTestController(lspClient)) + context.subscriptions.push(testController) } if (lspClient && !lspClient.hasCompletionCapability()) { diff --git a/vscode/extension/src/lsp/custom.ts b/vscode/extension/src/lsp/custom.ts index 7a9de4ca6f..8113cd86ae 100644 --- a/vscode/extension/src/lsp/custom.ts +++ b/vscode/extension/src/lsp/custom.ts @@ -32,6 +32,9 @@ export type CustomLSPMethods = | AllModelsForRenderMethod | SupportedMethodsMethod | FormatProjectMethod + | ListWorkspaceTests + | ListDocumentTests + | RunTest interface AllModelsRequest { textDocument: { @@ -111,3 +114,65 @@ interface FormatProjectResponse extends BaseResponse {} interface BaseResponse { response_error?: string } + +export interface ListWorkspaceTests { + method: 'sqlmesh/list_workspace_tests' + request: ListWorkspaceTestsRequest + response: ListWorkspaceTestsResponse +} + +type ListWorkspaceTestsRequest = object + +interface Position { + line: number + character: number +} + +interface Range { + start: Position + end: Position +} + +interface TestEntry { + name: string + uri: string + range: Range +} + +interface ListWorkspaceTestsResponse extends BaseResponse { + tests: TestEntry[] +} + +export interface ListDocumentTests { + method: 'sqlmesh/list_document_tests' + request: ListDocumentTestsRequest + response: ListDocumentTestsResponse +} + +export interface DocumentIdentifier { + uri: string +} + +export interface ListDocumentTestsRequest { + textDocument: DocumentIdentifier +} + +export interface ListDocumentTestsResponse extends BaseResponse { + tests: TestEntry[] +} + +export interface RunTest { + method: 'sqlmesh/run_test' + request: RunTestRequest + response: RunTestResponse +} + +export interface RunTestRequest { + textDocument: DocumentIdentifier + testName: string +} + +export interface RunTestResponse extends BaseResponse { + success: boolean + error_message?: string +} diff --git a/vscode/extension/src/tests/tests.ts b/vscode/extension/src/tests/tests.ts new file mode 100644 index 0000000000..dd3503165c --- /dev/null +++ b/vscode/extension/src/tests/tests.ts @@ -0,0 +1,155 @@ +import * as vscode from 'vscode' +import path from 'path' +import { LSPClient } from '../lsp/lsp' +import { isErr } from '@bus/result' +import { Disposable } from 'vscode' + +export const controller = vscode.tests.createTestController( + 'sqlmeshTests', + 'SQLMesh Tests', +) + +export const setupTestController = (lsp: LSPClient): Disposable => { + controller.resolveHandler = async test => { + console.log('Resolving test:', test?.id) + const uri = test?.uri + if (uri) { + await discoverDocumentTests(uri.toString()) + } else { + await discoverWorkspaceTests() + } + } + + // Discover tests immediately when the controller is set up + // This is useful for the initial load of tests in the workspace + // eslint-disable-next-line @typescript-eslint/no-floating-promises + discoverWorkspaceTests() + + controller.createRunProfile( + 'Run', + vscode.TestRunProfileKind.Run, + request => runTests(request), + true, + ) + + async function discoverDocumentTests(uri: string) { + const result = await lsp.call_custom_method('sqlmesh/list_document_tests', { + textDocument: { uri }, + }) + if (isErr(result)) { + vscode.window.showErrorMessage( + `Failed to list SQLMesh tests: ${result.error.message}`, + ) + return + } + const fileItem = controller.items.get(uri) + if (!fileItem) { + vscode.window.showErrorMessage(`No test item found for document: ${uri}`) + return + } + fileItem.children.replace([]) + for (const test of result.value.tests) { + const testItem = controller.createTestItem( + test.name, + test.name, + vscode.Uri.parse(test.uri), + ) + const range = test.range + testItem.range = new vscode.Range( + new vscode.Position(range.start.line, range.start.character), + new vscode.Position(range.end.line, range.end.character), + ) + fileItem.children.add(testItem) + } + } + + async function discoverWorkspaceTests() { + const result = await lsp.call_custom_method( + 'sqlmesh/list_workspace_tests', + {}, + ) + if (isErr(result)) { + vscode.window.showErrorMessage( + `Failed to list SQLMesh tests: ${result.error.message}`, + ) + return + } + controller.items.replace([]) + const files = new Map() + for (const entry of result.value.tests) { + const uri = vscode.Uri.parse(entry.uri) + let fileItem = files.get(uri.toString()) + if (!fileItem) { + fileItem = controller.createTestItem( + uri.toString(), + path.basename(uri.fsPath), + uri, + ) + // THIS IS WHERE YOU RESOLVE THE RANGE + fileItem.canResolveChildren = true + files.set(uri.toString(), fileItem) + controller.items.add(fileItem) + } + const testId = `${uri.toString()}::${entry.name}` + const testItem = controller.createTestItem(testId, entry.name, uri) + fileItem.children.add(testItem) + } + } + + async function runTests(request: vscode.TestRunRequest) { + const run = controller.createTestRun(request) + + const tests: vscode.TestItem[] = [] + const collect = (item: vscode.TestItem) => { + if (item.children.size === 0) tests.push(item) + item.children.forEach(collect) + } + + if (request.include) request.include.forEach(collect) + else controller.items.forEach(collect) + + for (const test of tests) { + run.started(test) + const startTime = Date.now() + const uri = test.uri + if (uri === undefined) { + run.failed(test, new vscode.TestMessage('Test item has no URI')) + continue + } + const response = await lsp.call_custom_method('sqlmesh/run_test', { + textDocument: { uri: uri.toString() }, + testName: test.id, + }) + if (isErr(response)) { + run.failed(test, new vscode.TestMessage(response.error.message)) + continue + } else { + const result = response.value + const duration = Date.now() - startTime + if (result.success) { + run.passed(test, duration) + } else { + run.failed( + test, + new vscode.TestMessage(result.error_message ?? 'Test failed'), + duration, + ) + } + } + } + run.end() + } + + // onChangeFile of yaml file reload the tests + return vscode.workspace.onDidChangeTextDocument(async event => { + if (event.document.languageId === 'yaml') { + const uri = event.document.uri.toString() + const testItem = controller.items.get(uri) + if (testItem) { + await discoverDocumentTests(uri) + } else { + await discoverWorkspaceTests() + } + } + }) +} diff --git a/vscode/extension/tests/tests.spec.ts b/vscode/extension/tests/tests.spec.ts new file mode 100644 index 0000000000..415eddb543 --- /dev/null +++ b/vscode/extension/tests/tests.spec.ts @@ -0,0 +1,42 @@ +import { test } from './fixtures' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { + openServerPage, + runCommand, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' + +test('Format project works correctly', async ({ page, sharedCodeServer }) => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + await createPythonInterpreterSettingsSpecifier(tempDir) + await openServerPage(page, tempDir, sharedCodeServer) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customer_revenue_lifetime model + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await page.waitForSelector('text=grain') + await waitForLoadedSQLMesh(page) + + // Format the project + await runCommand(page, 'Test: Run All Tests') + + await page.waitForSelector('text=test_order_items') +}) From 04d199bf117320e7e66d466dc186d7b94848219c Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 29 Jul 2025 09:40:27 +1200 Subject: [PATCH 0628/1056] Fix!: Inconsistent behaviour when a macro returns a list containing a single array vs multiple arrays (#5037) --- sqlmesh/core/macros.py | 33 +++++++++++++++++++++++++++++++-- tests/core/test_macros.py | 13 +++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index ec5b2567f4..a72bf4605a 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -361,8 +361,37 @@ def evaluate(self, node: MacroFunc) -> exp.Expression | t.List[exp.Expression] | return None if isinstance(result, (tuple, list)): - return [self.parse_one(item) for item in result if item is not None] - return self.parse_one(result) + result = [self.parse_one(item) for item in result if item is not None] + + if ( + len(result) == 1 + and isinstance(result[0], (exp.Array, exp.Tuple)) + and node.find_ancestor(MacroFunc) + ): + """ + if: + - the output of evaluating this node is being passed as an argument to another macro function + - and that output is something that _norm_var_arg_lambda() will unpack into varargs + > (a list containing a single item of type exp.Tuple/exp.Array) + then we will get inconsistent behaviour depending on if this node emits a list with a single item vs multiple items. + + In the first case, emitting a list containing a single array item will cause that array to get unpacked and its *members* passed to the calling macro + In the second case, emitting a list containing multiple array items will cause each item to get passed as-is to the calling macro + + To prevent this inconsistency, we wrap this node output in an exp.Array so that _norm_var_arg_lambda() can "unpack" that into the + actual argument we want to pass to the parent macro function + + Note we only do this for evaluation results that get passed as an argument to another macro, because when the final + result is given to something like SELECT, we still want that to be unpacked into a list of items like: + - SELECT ARRAY(1), ARRAY(2) + rather than a single item like: + - SELECT ARRAY(ARRAY(1), ARRAY(2)) + """ + result = [exp.Array(expressions=result)] + else: + result = self.parse_one(result) + + return result def eval_expression(self, node: t.Any) -> t.Any: """Converts a SQLGlot expression into executable Python code and evals it. diff --git a/tests/core/test_macros.py b/tests/core/test_macros.py index 0e3615d6c0..77d8fb84ae 100644 --- a/tests/core/test_macros.py +++ b/tests/core/test_macros.py @@ -363,11 +363,24 @@ def test_ast_correctness(macro_evaluator): "SELECT column LIKE a OR column LIKE b OR column LIKE c", {}, ), + ("SELECT @REDUCE([1], (x, y) -> x + y)", "SELECT 1", {}), + ("SELECT @REDUCE([1, 2], (x, y) -> x + y)", "SELECT 1 + 2", {}), + ("SELECT @REDUCE([[1]], (x, y) -> x + y)", "SELECT ARRAY(1)", {}), + ("SELECT @REDUCE([[1, 2]], (x, y) -> x + y)", "SELECT ARRAY(1, 2)", {}), ( """select @EACH([a, b, c], x -> column like x AS @SQL('@{x}_y', 'Identifier')), @x""", "SELECT column LIKE a AS a_y, column LIKE b AS b_y, column LIKE c AS c_y, '3'", {"x": "3"}, ), + ("SELECT @EACH([1], a -> [@a])", "SELECT ARRAY(1)", {}), + ("SELECT @EACH([1, 2], a -> [@a])", "SELECT ARRAY(1), ARRAY(2)", {}), + ("SELECT @REDUCE(@EACH([1], a -> [@a]), (x, y) -> x + y)", "SELECT ARRAY(1)", {}), + ( + "SELECT @REDUCE(@EACH([1, 2], a -> [@a]), (x, y) -> x + y)", + "SELECT ARRAY(1) + ARRAY(2)", + {}, + ), + ("SELECT @REDUCE([[1],[2]], (x, y) -> x + y)", "SELECT ARRAY(1) + ARRAY(2)", {}), ( """@WITH(@do_with) all_cities as (select * from city) select all_cities""", "WITH all_cities AS (SELECT * FROM city) SELECT all_cities", From b328b26e5024a689d02dc3b3026ee3915ee1f024 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 28 Jul 2025 22:09:01 +0000 Subject: [PATCH 0629/1056] Docs: fix syntax error in linter guide (#5052) --- docs/guides/linter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/linter.md b/docs/guides/linter.md index a453d4f066..22cc5077b8 100644 --- a/docs/guides/linter.md +++ b/docs/guides/linter.md @@ -221,7 +221,7 @@ This example specifies that the model `docs_example.full_model` should not run t ```sql linenums="1" MODEL( name docs_example.full_model, - ignored_rules: ["invalidselectstarexpansion"] # or "ALL" to turn off linting completely + ignored_rules ["invalidselectstarexpansion"] # or "ALL" to turn off linting completely ); ``` From a94c4f088f1a6754c1e083e0afdd39bb6daeb4a9 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:12:56 +0100 Subject: [PATCH 0630/1056] fix(vscode): force lsp to be single worker (#5050) --- sqlmesh/lsp/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 3839245a08..416b092122 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -114,7 +114,7 @@ def __init__( :param server_name: Name for the language server. :param version: Version string. """ - self.server = LanguageServer(server_name, version) + self.server = LanguageServer(server_name, version, max_workers=1) self.context_class = context_class self.context_state: ContextState = NoContext() self.workspace_folders: t.List[Path] = [] From 074df62a0351924be532405b2123c70759388ae6 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:58:39 +0100 Subject: [PATCH 0631/1056] feat: location in nomissingexternal models linting error (#5049) --- sqlmesh/core/linter/helpers.py | 32 +- sqlmesh/core/linter/rules/builtin.py | 80 +++- sqlmesh/lsp/completions.py | 2 +- sqlmesh/lsp/description.py | 29 -- sqlmesh/lsp/main.py | 16 +- sqlmesh/lsp/reference.py | 418 ++---------------- sqlmesh/lsp/rename.py | 4 +- sqlmesh/utils/lineage.py | 404 +++++++++++++++++ tests/core/linter/test_builtin.py | 5 +- tests/lsp/test_reference.py | 4 +- tests/lsp/test_reference_cte.py | 6 +- tests/lsp/test_reference_external_model.py | 66 ++- tests/lsp/test_reference_macro.py | 4 +- tests/lsp/test_reference_macro_multi.py | 4 +- .../test_lineage_description.py} | 2 +- 15 files changed, 630 insertions(+), 446 deletions(-) delete mode 100644 sqlmesh/lsp/description.py create mode 100644 sqlmesh/utils/lineage.py rename tests/{lsp/test_description.py => utils/test_lineage_description.py} (93%) diff --git a/sqlmesh/core/linter/helpers.py b/sqlmesh/core/linter/helpers.py index 707b0a9159..e62545bc02 100644 --- a/sqlmesh/core/linter/helpers.py +++ b/sqlmesh/core/linter/helpers.py @@ -84,19 +84,8 @@ def to_range(self, read_file: t.Optional[t.List[str]]) -> Range: ) -def read_range_from_file(file: Path, text_range: Range) -> str: - """ - Read the file and return the content within the specified range. - - Args: - file: Path to the file to read - text_range: The range of text to extract - - Returns: - The content within the specified range - """ - with file.open("r", encoding="utf-8") as f: - lines = f.readlines() +def read_range_from_string(content: str, text_range: Range) -> str: + lines = content.splitlines(keepends=False) # Ensure the range is within bounds start_line = max(0, text_range.start.line) @@ -116,6 +105,23 @@ def read_range_from_file(file: Path, text_range: Range) -> str: return "".join(result) +def read_range_from_file(file: Path, text_range: Range) -> str: + """ + Read the file and return the content within the specified range. + + Args: + file: Path to the file to read + text_range: The range of text to extract + + Returns: + The content within the specified range + """ + with file.open("r", encoding="utf-8") as f: + lines = f.readlines() + + return read_range_from_string("".join(lines), text_range) + + def get_range_of_model_block( sql: str, dialect: str, diff --git a/sqlmesh/core/linter/rules/builtin.py b/sqlmesh/core/linter/rules/builtin.py index f16bb5d111..c1a5f9b877 100644 --- a/sqlmesh/core/linter/rules/builtin.py +++ b/sqlmesh/core/linter/rules/builtin.py @@ -7,10 +7,16 @@ from sqlglot.expressions import Star from sqlglot.helper import subclasses -from sqlmesh.core.linter.helpers import TokenPositionDetails, get_range_of_model_block +from sqlmesh.core.dialect import normalize_model_name +from sqlmesh.core.linter.helpers import ( + TokenPositionDetails, + get_range_of_model_block, + read_range_from_string, +) from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit from sqlmesh.core.linter.definition import RuleSet from sqlmesh.core.model import Model, SqlModel, ExternalModel +from sqlmesh.utils.lineage import extract_references_from_query, ExternalModelReference class NoSelectStar(Rule): @@ -113,7 +119,9 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]: class NoMissingExternalModels(Rule): """All external models must be registered in the external_models.yaml file""" - def check_model(self, model: Model) -> t.Optional[RuleViolation]: + def check_model( + self, model: Model + ) -> t.Optional[t.Union[RuleViolation, t.List[RuleViolation]]]: # Ignore external models themselves, because either they are registered, # and if they are not, they will be caught as referenced in another model. if isinstance(model, ExternalModel): @@ -129,10 +137,74 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]: if not not_registered_external_models: return None + # If the model is anything other than a sql model that and has a path + # that ends with .sql, we cannot extract the references from the query. + path = model._path + if not isinstance(model, SqlModel) or not path or not str(path).endswith(".sql"): + return self._standard_error_message( + model_name=model.fqn, + external_models=not_registered_external_models, + ) + + with open(path, "r", encoding="utf-8") as file: + read_file = file.read() + split_read_file = read_file.splitlines() + + # If there are any unregistered external models, return a violation find + # the ranges for them. + references = extract_references_from_query( + query=model.query, + context=self.context, + document_path=path, + read_file=split_read_file, + depends_on=model.depends_on, + dialect=model.dialect, + ) + external_references = { + normalize_model_name( + table=read_range_from_string(read_file, ref.range), + default_catalog=model.default_catalog, + dialect=model.dialect, + ): ref + for ref in references + if isinstance(ref, ExternalModelReference) and ref.path is None + } + + # Ensure that depends_on and external references match. + if not_registered_external_models != set(external_references.keys()): + return self._standard_error_message( + model_name=model.fqn, + external_models=not_registered_external_models, + ) + + # Return a violation for each unregistered external model with its range. + violations = [] + for ref_name, ref in external_references.items(): + if ref_name in not_registered_external_models: + violations.append( + RuleViolation( + rule=self, + violation_msg=f"Model '{model.fqn}' depends on unregistered external model '{ref_name}'. " + "Please register it in the external models file. This can be done by running 'sqlmesh create_external_models'.", + violation_range=ref.range, + ) + ) + + if len(violations) < len(not_registered_external_models): + return self._standard_error_message( + model_name=model.fqn, + external_models=not_registered_external_models, + ) + + return violations + + def _standard_error_message( + self, model_name: str, external_models: t.Set[str] + ) -> RuleViolation: return RuleViolation( rule=self, - violation_msg=f"Model '{model.name}' depends on unregistered external models: " - f"{', '.join(m for m in not_registered_external_models)}. " + violation_msg=f"Model '{model_name}' depends on unregistered external models: " + f"{', '.join(m for m in external_models)}. " "Please register them in the external models file. This can be done by running 'sqlmesh create_external_models'.", ) diff --git a/sqlmesh/lsp/completions.py b/sqlmesh/lsp/completions.py index 0026260481..93162b15a4 100644 --- a/sqlmesh/lsp/completions.py +++ b/sqlmesh/lsp/completions.py @@ -8,8 +8,8 @@ from sqlmesh import macro import typing as t from sqlmesh.lsp.context import AuditTarget, LSPContext, ModelTarget -from sqlmesh.lsp.description import generate_markdown_description from sqlmesh.lsp.uri import URI +from sqlmesh.utils.lineage import generate_markdown_description def get_sql_completions( diff --git a/sqlmesh/lsp/description.py b/sqlmesh/lsp/description.py deleted file mode 100644 index 768197742f..0000000000 --- a/sqlmesh/lsp/description.py +++ /dev/null @@ -1,29 +0,0 @@ -from sqlmesh.core.model.definition import ( - ExternalModel, - PythonModel, - SeedModel, - SqlModel, -) -import typing as t - - -def generate_markdown_description( - model: t.Union[SqlModel, ExternalModel, PythonModel, SeedModel], -) -> t.Optional[str]: - description = model.description - columns = model.columns_to_types - column_descriptions = model.column_descriptions - - if columns is None: - return description or None - - columns_table = "\n".join( - [ - f"| {column} | {column_type} | {column_descriptions.get(column, '')} |" - for column, column_type in columns.items() - ] - ) - - table_header = "| Column | Type | Description |\n|--------|------|-------------|\n" - columns_text = table_header + columns_table - return f"{description}\n\n{columns_text}" if description else columns_text diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 416b092122..d53822fcac 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -23,6 +23,10 @@ ApiResponseGetLineage, ApiResponseGetModels, ) + +# Define the command constant +EXTERNAL_MODEL_UPDATE_COLUMNS = "sqlmesh.external_model_update_columns" + from sqlmesh.lsp.completions import get_sql_completions from sqlmesh.lsp.context import ( LSPContext, @@ -60,15 +64,15 @@ from sqlmesh.lsp.helpers import to_lsp_range, to_sqlmesh_position from sqlmesh.lsp.hints import get_hints from sqlmesh.lsp.reference import ( - LSPCteReference, - LSPModelReference, - LSPExternalModelReference, + CTEReference, + ModelReference, get_references, get_all_references, ) from sqlmesh.lsp.rename import prepare_rename, rename_symbol, get_document_highlights from sqlmesh.lsp.uri import URI from sqlmesh.utils.errors import ConfigError +from sqlmesh.utils.lineage import ExternalModelReference from web.server.api.endpoints.lineage import column_lineage, model_lineage from web.server.api.endpoints.models import get_models from typing import Union @@ -479,7 +483,7 @@ def hover(ls: LanguageServer, params: types.HoverParams) -> t.Optional[types.Hov if not references: return None reference = references[0] - if isinstance(reference, LSPCteReference) or not reference.markdown_description: + if isinstance(reference, CTEReference) or not reference.markdown_description: return None return types.Hover( contents=types.MarkupContent( @@ -525,7 +529,7 @@ def goto_definition( location_links = [] for reference in references: # Use target_range if available (CTEs, Macros, and external models in YAML) - if isinstance(reference, LSPModelReference): + if isinstance(reference, ModelReference): # Regular SQL models - default to start of file target_range = types.Range( start=types.Position(line=0, character=0), @@ -535,7 +539,7 @@ def goto_definition( start=types.Position(line=0, character=0), end=types.Position(line=0, character=0), ) - elif isinstance(reference, LSPExternalModelReference): + elif isinstance(reference, ExternalModelReference): # External models may have target_range set for YAML files target_range = types.Range( start=types.Position(line=0, character=0), diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 6aee3e10da..80d401f79c 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -1,74 +1,27 @@ import typing as t from pathlib import Path -from pydantic import Field from sqlmesh.core.audit import StandaloneAudit -from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core.linter.helpers import ( TokenPositionDetails, ) from sqlmesh.core.linter.rule import Range, Position -from sqlmesh.core.model.definition import SqlModel, ExternalModel +from sqlmesh.core.model.definition import SqlModel from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget from sqlglot import exp -from sqlmesh.lsp.description import generate_markdown_description -from sqlglot.optimizer.scope import build_scope from sqlmesh.lsp.uri import URI -from sqlmesh.utils.pydantic import PydanticModel -from sqlglot.optimizer.normalize_identifiers import normalize_identifiers +from sqlmesh.utils.lineage import ( + MacroReference, + CTEReference, + Reference, + ModelReference, + extract_references_from_query, +) import ast from sqlmesh.core.model import Model from sqlmesh import macro import inspect -from ruamel.yaml import YAML - - -class LSPModelReference(PydanticModel): - """A LSP reference to a model, excluding external models.""" - - type: t.Literal["model"] = "model" - path: Path - range: Range - markdown_description: t.Optional[str] = None - - -class LSPExternalModelReference(PydanticModel): - """A LSP reference to an external model.""" - - type: t.Literal["external_model"] = "external_model" - range: Range - target_range: t.Optional[Range] = None - path: t.Optional[Path] = None - """The path of the external model, typically a YAML file, it is optional because - external models can be unregistered and so the path is not available.""" - - markdown_description: t.Optional[str] = None - - -class LSPCteReference(PydanticModel): - """A LSP reference to a CTE.""" - - type: t.Literal["cte"] = "cte" - path: Path - range: Range - target_range: Range - - -class LSPMacroReference(PydanticModel): - """A LSP reference to a macro.""" - - type: t.Literal["macro"] = "macro" - path: Path - range: Range - target_range: Range - markdown_description: t.Optional[str] = None - - -Reference = t.Annotated[ - t.Union[LSPModelReference, LSPCteReference, LSPMacroReference, LSPExternalModelReference], - Field(discriminator="type"), -] def by_position(position: Position) -> t.Callable[[Reference], bool]: @@ -158,7 +111,6 @@ def get_model_definitions_for_a_path( audit = lint_context.context.standalone_audits.get(file_info.name) if audit is None: return [] - query = audit.query dialect = audit.dialect depends_on = audit.depends_on @@ -169,177 +121,17 @@ def get_model_definitions_for_a_path( if file_path is None: return [] - # Find all possible references - references: t.List[Reference] = [] - with open(file_path, "r", encoding="utf-8") as file: read_file = file.readlines() - # Build a scope tree to properly handle nested CTEs - try: - query = normalize_identifiers(query.copy(), dialect=dialect) - root_scope = build_scope(query) - except Exception: - root_scope = None - - if root_scope: - # Traverse all scopes to find CTE definitions and table references - for scope in root_scope.traverse(): - for table in scope.tables: - table_name = table.name - - # Check if this table reference is a CTE in the current scope - if cte_scope := scope.cte_sources.get(table_name): - cte = cte_scope.expression.parent - alias = cte.args["alias"] - if isinstance(alias, exp.TableAlias): - identifier = alias.this - if isinstance(identifier, exp.Identifier): - target_range_sqlmesh = TokenPositionDetails.from_meta( - identifier.meta - ).to_range(read_file) - table_range_sqlmesh = TokenPositionDetails.from_meta( - table.this.meta - ).to_range(read_file) - - references.append( - LSPCteReference( - path=document_uri.to_path(), # Same file - range=table_range_sqlmesh, - target_range=target_range_sqlmesh, - ) - ) - - column_references = _process_column_references( - scope=scope, - reference_name=table.name, - read_file=read_file, - referenced_model_path=document_uri.to_path(), - description="", - reference_type="cte", - cte_target_range=target_range_sqlmesh, - ) - references.extend(column_references) - continue - - # For non-CTE tables, process these as before (external model references) - # Normalize the table reference - unaliased = table.copy() - if unaliased.args.get("alias") is not None: - unaliased.set("alias", None) - reference_name = unaliased.sql(dialect=dialect) - try: - normalized_reference_name = normalize_model_name( - reference_name, - default_catalog=lint_context.context.default_catalog, - dialect=dialect, - ) - if normalized_reference_name not in depends_on: - continue - except Exception: - # Skip references that cannot be normalized - continue - - # Get the referenced model uri - referenced_model = lint_context.context.get_model( - model_or_snapshot=normalized_reference_name, raise_if_missing=False - ) - if referenced_model is None: - table_meta = TokenPositionDetails.from_meta(table.this.meta) - table_range_sqlmesh = table_meta.to_range(read_file) - start_pos_sqlmesh = table_range_sqlmesh.start - end_pos_sqlmesh = table_range_sqlmesh.end - references.append( - LSPExternalModelReference( - range=Range( - start=start_pos_sqlmesh, - end=end_pos_sqlmesh, - ), - markdown_description="Unregistered external model", - ) - ) - continue - referenced_model_path = referenced_model._path - if referenced_model_path is None: - continue - # Check whether the path exists - if not referenced_model_path.is_file(): - continue - - # Extract metadata for positioning - table_meta = TokenPositionDetails.from_meta(table.this.meta) - table_range_sqlmesh = table_meta.to_range(read_file) - start_pos_sqlmesh = table_range_sqlmesh.start - end_pos_sqlmesh = table_range_sqlmesh.end - - # If there's a catalog or database qualifier, adjust the start position - catalog_or_db = table.args.get("catalog") or table.args.get("db") - if catalog_or_db is not None: - catalog_or_db_meta = TokenPositionDetails.from_meta(catalog_or_db.meta) - catalog_or_db_range_sqlmesh = catalog_or_db_meta.to_range(read_file) - start_pos_sqlmesh = catalog_or_db_range_sqlmesh.start - - description = generate_markdown_description(referenced_model) - - # For external models in YAML files, find the specific model block - if isinstance(referenced_model, ExternalModel): - yaml_target_range: t.Optional[Range] = None - if ( - referenced_model_path.suffix in (".yaml", ".yml") - and referenced_model_path.is_file() - ): - yaml_target_range = _get_yaml_model_range( - referenced_model_path, referenced_model.name - ) - references.append( - LSPExternalModelReference( - path=referenced_model_path, - range=Range( - start=start_pos_sqlmesh, - end=end_pos_sqlmesh, - ), - markdown_description=description, - target_range=yaml_target_range, - ) - ) - - column_references = _process_column_references( - scope=scope, - reference_name=normalized_reference_name, - read_file=read_file, - referenced_model_path=referenced_model_path, - description=description, - yaml_target_range=yaml_target_range, - reference_type="external_model", - default_catalog=lint_context.context.default_catalog, - dialect=dialect, - ) - references.extend(column_references) - else: - references.append( - LSPModelReference( - path=referenced_model_path, - range=Range( - start=start_pos_sqlmesh, - end=end_pos_sqlmesh, - ), - markdown_description=description, - ) - ) - - column_references = _process_column_references( - scope=scope, - reference_name=normalized_reference_name, - read_file=read_file, - referenced_model_path=referenced_model_path, - description=description, - reference_type="model", - default_catalog=lint_context.context.default_catalog, - dialect=dialect, - ) - references.extend(column_references) - - return references + return extract_references_from_query( + query=query, + context=lint_context.context, + document_path=document_uri.to_path(), + read_file=read_file, + depends_on=depends_on, + dialect=dialect, + ) def get_macro_definitions_for_a_path( @@ -476,7 +268,7 @@ def get_macro_reference( # Create a reference to the macro definition - return LSPMacroReference( + return MacroReference( path=path, range=macro_range, target_range=Range( @@ -509,7 +301,7 @@ def get_built_in_macro_reference(macro_name: str, macro_range: Range) -> t.Optio # Calculate the end line number by counting the number of source lines end_line_number = line_number + len(source_lines) - 1 - return LSPMacroReference( + return MacroReference( path=Path(filename), range=macro_range, target_range=Range( @@ -522,7 +314,7 @@ def get_built_in_macro_reference(macro_name: str, macro_range: Range) -> t.Optio def get_model_find_all_references( lint_context: LSPContext, document_uri: URI, position: Position -) -> t.List[LSPModelReference]: +) -> t.List[ModelReference]: """ Get all references to a model across the entire project. @@ -540,7 +332,7 @@ def get_model_find_all_references( # Find the model reference at the cursor position model_at_position = next( filter( - lambda ref: isinstance(ref, LSPModelReference) + lambda ref: isinstance(ref, ModelReference) and _position_within_range(position, ref.range), get_model_definitions_for_a_path(lint_context, document_uri), ), @@ -550,13 +342,13 @@ def get_model_find_all_references( if not model_at_position: return [] - assert isinstance(model_at_position, LSPModelReference) # for mypy + assert isinstance(model_at_position, ModelReference) # for mypy target_model_path = model_at_position.path # Start with the model definition - all_references: t.List[LSPModelReference] = [ - LSPModelReference( + all_references: t.List[ModelReference] = [ + ModelReference( path=model_at_position.path, range=Range( start=Position(line=0, character=0), @@ -568,15 +360,15 @@ def get_model_find_all_references( # Then add references from the current file current_file_refs = filter( - lambda ref: isinstance(ref, LSPModelReference) and ref.path == target_model_path, + lambda ref: isinstance(ref, ModelReference) and ref.path == target_model_path, get_model_definitions_for_a_path(lint_context, document_uri), ) for ref in current_file_refs: - assert isinstance(ref, LSPModelReference) # for mypy + assert isinstance(ref, ModelReference) # for mypy all_references.append( - LSPModelReference( + ModelReference( path=document_uri.to_path(), range=ref.range, markdown_description=ref.markdown_description, @@ -593,15 +385,15 @@ def get_model_find_all_references( # Get model references that point to the target model matching_refs = filter( - lambda ref: isinstance(ref, LSPModelReference) and ref.path == target_model_path, + lambda ref: isinstance(ref, ModelReference) and ref.path == target_model_path, get_model_definitions_for_a_path(lint_context, file_uri), ) for ref in matching_refs: - assert isinstance(ref, LSPModelReference) # for mypy + assert isinstance(ref, ModelReference) # for mypy all_references.append( - LSPModelReference( + ModelReference( path=path, range=ref.range, markdown_description=ref.markdown_description, @@ -613,7 +405,7 @@ def get_model_find_all_references( def get_cte_references( lint_context: LSPContext, document_uri: URI, position: Position -) -> t.List[LSPCteReference]: +) -> t.List[CTEReference]: """ Get all references to a CTE at a specific position in a document. @@ -629,10 +421,10 @@ def get_cte_references( """ # Filter to get the CTE references - cte_references: t.List[LSPCteReference] = [ + cte_references: t.List[CTEReference] = [ ref for ref in get_model_definitions_for_a_path(lint_context, document_uri) - if isinstance(ref, LSPCteReference) + if isinstance(ref, CTEReference) ] if not cte_references: @@ -654,7 +446,7 @@ def get_cte_references( # Add the CTE definition matching_references = [ - LSPCteReference( + CTEReference( path=document_uri.to_path(), range=target_cte_definition_range, target_range=target_cte_definition_range, @@ -665,7 +457,7 @@ def get_cte_references( for ref in cte_references: if ref.target_range == target_cte_definition_range: matching_references.append( - LSPCteReference( + CTEReference( path=document_uri.to_path(), range=ref.range, target_range=ref.target_range, @@ -677,7 +469,7 @@ def get_cte_references( def get_macro_find_all_references( lsp_context: LSPContext, document_uri: URI, position: Position -) -> t.List[LSPMacroReference]: +) -> t.List[MacroReference]: """ Get all references to a macro at a specific position in a document. @@ -694,7 +486,7 @@ def get_macro_find_all_references( # Find the macro reference at the cursor position macro_at_position = next( filter( - lambda ref: isinstance(ref, LSPMacroReference) + lambda ref: isinstance(ref, MacroReference) and _position_within_range(position, ref.range), get_macro_definitions_for_a_path(lsp_context, document_uri), ), @@ -704,14 +496,14 @@ def get_macro_find_all_references( if not macro_at_position: return [] - assert isinstance(macro_at_position, LSPMacroReference) # for mypy + assert isinstance(macro_at_position, MacroReference) # for mypy target_macro_path = macro_at_position.path target_macro_target_range = macro_at_position.target_range # Start with the macro definition - all_references: t.List[LSPMacroReference] = [ - LSPMacroReference( + all_references: t.List[MacroReference] = [ + MacroReference( path=target_macro_path, range=target_macro_target_range, target_range=target_macro_target_range, @@ -725,16 +517,16 @@ def get_macro_find_all_references( # Get macro references that point to the same macro definition matching_refs = filter( - lambda ref: isinstance(ref, LSPMacroReference) + lambda ref: isinstance(ref, MacroReference) and ref.path == target_macro_path and ref.target_range == target_macro_target_range, get_macro_definitions_for_a_path(lsp_context, file_uri), ) for ref in matching_refs: - assert isinstance(ref, LSPMacroReference) # for mypy + assert isinstance(ref, MacroReference) # for mypy all_references.append( - LSPMacroReference( + MacroReference( path=path, range=ref.range, target_range=ref.target_range, @@ -786,129 +578,3 @@ def _position_within_range(position: Position, range: Range) -> bool: range.end.line > position.line or (range.end.line == position.line and range.end.character >= position.character) ) - - -def _get_column_table_range(column: exp.Column, read_file: t.List[str]) -> Range: - """ - Get the range for a column's table reference, handling both simple and qualified table names. - - Args: - column: The column expression - read_file: The file content as list of lines - - Returns: - The Range covering the table reference in the column - """ - - table_parts = column.parts[:-1] - - start_range = TokenPositionDetails.from_meta(table_parts[0].meta).to_range(read_file) - end_range = TokenPositionDetails.from_meta(table_parts[-1].meta).to_range(read_file) - - return Range( - start=start_range.start, - end=end_range.end, - ) - - -def _process_column_references( - scope: t.Any, - reference_name: str, - read_file: t.List[str], - referenced_model_path: Path, - description: t.Optional[str] = None, - yaml_target_range: t.Optional[Range] = None, - reference_type: t.Literal["model", "external_model", "cte"] = "model", - default_catalog: t.Optional[str] = None, - dialect: t.Optional[str] = None, - cte_target_range: t.Optional[Range] = None, -) -> t.List[Reference]: - """ - Process column references for a given table and create appropriate reference objects. - - Args: - scope: The SQL scope to search for columns - reference_name: The full reference name (may include database/catalog) - read_file: The file content as list of lines - referenced_model_path: Path of the referenced model - description: Markdown description for the reference - yaml_target_range: Target range for external models (YAML files) - reference_type: Type of reference - "model", "external_model", or "cte" - default_catalog: Default catalog for normalization - dialect: SQL dialect for normalization - cte_target_range: Target range for CTE references - - Returns: - List of table references for column usages - """ - - references: t.List[Reference] = [] - for column in scope.find_all(exp.Column): - if column.table: - if reference_type == "cte": - if column.table == reference_name: - table_range = _get_column_table_range(column, read_file) - references.append( - LSPCteReference( - path=referenced_model_path, - range=table_range, - target_range=cte_target_range, - ) - ) - else: - table_parts = [part.sql(dialect) for part in column.parts[:-1]] - table_ref = ".".join(table_parts) - normalized_reference_name = normalize_model_name( - table_ref, - default_catalog=default_catalog, - dialect=dialect, - ) - if normalized_reference_name == reference_name: - table_range = _get_column_table_range(column, read_file) - if reference_type == "external_model": - references.append( - LSPExternalModelReference( - path=referenced_model_path, - range=table_range, - markdown_description=description, - target_range=yaml_target_range, - ) - ) - else: - references.append( - LSPModelReference( - path=referenced_model_path, - range=table_range, - markdown_description=description, - ) - ) - - return references - - -def _get_yaml_model_range(path: Path, model_name: str) -> t.Optional[Range]: - """ - Find the range of a specific model block in a YAML file. - - Args: - yaml_path: Path to the YAML file - model_name: Name of the model to find - - Returns: - The Range of the model block in the YAML file, or None if not found - """ - yaml = YAML() - with path.open("r", encoding="utf-8") as f: - data = yaml.load(f) - - if not isinstance(data, list): - return None - - for item in data: - if isinstance(item, dict) and item.get("name") == model_name: - # Get size of block by taking the earliest line/col in the items block and the last line/col of the block - position_data = item.lc.data["name"] # type: ignore - start = Position(line=position_data[2], character=position_data[3]) - end = Position(line=position_data[2], character=position_data[3] + len(item["name"])) - return Range(start=start, end=end) - return None diff --git a/sqlmesh/lsp/rename.py b/sqlmesh/lsp/rename.py index 31f7eb3200..5675c4efca 100644 --- a/sqlmesh/lsp/rename.py +++ b/sqlmesh/lsp/rename.py @@ -13,7 +13,7 @@ from sqlmesh.lsp.reference import ( _position_within_range, get_cte_references, - LSPCteReference, + CTEReference, ) from sqlmesh.lsp.uri import URI @@ -82,7 +82,7 @@ def rename_symbol( return None -def _rename_cte(cte_references: t.List[LSPCteReference], new_name: str) -> WorkspaceEdit: +def _rename_cte(cte_references: t.List[CTEReference], new_name: str) -> WorkspaceEdit: """ Create a WorkspaceEdit for renaming a CTE. diff --git a/sqlmesh/utils/lineage.py b/sqlmesh/utils/lineage.py new file mode 100644 index 0000000000..8fcb92f56b --- /dev/null +++ b/sqlmesh/utils/lineage.py @@ -0,0 +1,404 @@ +import typing as t +from pathlib import Path + +from pydantic import Field + +from sqlmesh.core.dialect import normalize_model_name +from sqlmesh.core.linter.helpers import ( + TokenPositionDetails, +) +from sqlmesh.core.linter.rule import Range, Position +from sqlmesh.core.model.definition import SqlModel, ExternalModel, PythonModel, SeedModel +from sqlglot import exp +from sqlglot.optimizer.scope import build_scope + +from sqlglot.optimizer.normalize_identifiers import normalize_identifiers +from ruamel.yaml import YAML + +from sqlmesh.utils.pydantic import PydanticModel + +if t.TYPE_CHECKING: + from sqlmesh.core.context import Context + from sqlmesh.core.context import GenericContext + + +class ModelReference(PydanticModel): + """A reference to a model, excluding external models.""" + + type: t.Literal["model"] = "model" + path: Path + range: Range + markdown_description: t.Optional[str] = None + + +class ExternalModelReference(PydanticModel): + """A reference to an external model.""" + + type: t.Literal["external_model"] = "external_model" + range: Range + target_range: t.Optional[Range] = None + path: t.Optional[Path] = None + """The path of the external model, typically a YAML file, it is optional because + external models can be unregistered and so the path is not available.""" + + markdown_description: t.Optional[str] = None + + +class CTEReference(PydanticModel): + """A reference to a CTE.""" + + type: t.Literal["cte"] = "cte" + path: Path + range: Range + target_range: Range + + +class MacroReference(PydanticModel): + """A reference to a macro.""" + + type: t.Literal["macro"] = "macro" + path: Path + range: Range + target_range: Range + markdown_description: t.Optional[str] = None + + +Reference = t.Annotated[ + t.Union[ModelReference, CTEReference, MacroReference, ExternalModelReference], + Field(discriminator="type"), +] + + +def extract_references_from_query( + query: exp.Expression, + context: t.Union["Context", "GenericContext[t.Any]"], + document_path: Path, + read_file: t.List[str], + depends_on: t.Set[str], + dialect: t.Optional[str] = None, +) -> t.List[Reference]: + # Build a scope tree to properly handle nested CTEs + try: + query = normalize_identifiers(query.copy(), dialect=dialect) + root_scope = build_scope(query) + except Exception: + root_scope = None + + references: t.List[Reference] = [] + if not root_scope: + return references + + # Traverse all scopes to find CTE definitions and table references + for scope in root_scope.traverse(): + for table in scope.tables: + table_name = table.name + + # Check if this table reference is a CTE in the current scope + if cte_scope := scope.cte_sources.get(table_name): + cte = cte_scope.expression.parent + alias = cte.args["alias"] + if isinstance(alias, exp.TableAlias): + identifier = alias.this + if isinstance(identifier, exp.Identifier): + target_range_sqlmesh = TokenPositionDetails.from_meta( + identifier.meta + ).to_range(read_file) + table_range_sqlmesh = TokenPositionDetails.from_meta( + table.this.meta + ).to_range(read_file) + + references.append( + CTEReference( + path=document_path, # Same file + range=table_range_sqlmesh, + target_range=target_range_sqlmesh, + ) + ) + + column_references = _process_column_references( + scope=scope, + reference_name=table.name, + read_file=read_file, + referenced_model_path=document_path, + description="", + reference_type="cte", + cte_target_range=target_range_sqlmesh, + ) + references.extend(column_references) + continue + + # For non-CTE tables, process these as before (external model references) + # Normalize the table reference + unaliased = table.copy() + if unaliased.args.get("alias") is not None: + unaliased.set("alias", None) + reference_name = unaliased.sql(dialect=dialect) + try: + normalized_reference_name = normalize_model_name( + reference_name, + default_catalog=context.default_catalog, + dialect=dialect, + ) + if normalized_reference_name not in depends_on: + continue + except Exception: + # Skip references that cannot be normalized + continue + + # Get the referenced model uri + referenced_model = context.get_model( + model_or_snapshot=normalized_reference_name, raise_if_missing=False + ) + if referenced_model is None: + # Extract metadata for positioning + table_meta = TokenPositionDetails.from_meta(table.this.meta) + table_range_sqlmesh = table_meta.to_range(read_file) + start_pos_sqlmesh = table_range_sqlmesh.start + end_pos_sqlmesh = table_range_sqlmesh.end + + # If there's a catalog or database qualifier, adjust the start position + catalog_or_db = table.args.get("catalog") or table.args.get("db") + if catalog_or_db is not None: + catalog_or_db_meta = TokenPositionDetails.from_meta(catalog_or_db.meta) + catalog_or_db_range_sqlmesh = catalog_or_db_meta.to_range(read_file) + start_pos_sqlmesh = catalog_or_db_range_sqlmesh.start + + references.append( + ExternalModelReference( + range=Range( + start=start_pos_sqlmesh, + end=end_pos_sqlmesh, + ), + markdown_description="Unregistered external model", + ) + ) + continue + referenced_model_path = referenced_model._path + if referenced_model_path is None: + continue + # Check whether the path exists + if not referenced_model_path.is_file(): + continue + + # Extract metadata for positioning + table_meta = TokenPositionDetails.from_meta(table.this.meta) + table_range_sqlmesh = table_meta.to_range(read_file) + start_pos_sqlmesh = table_range_sqlmesh.start + end_pos_sqlmesh = table_range_sqlmesh.end + + # If there's a catalog or database qualifier, adjust the start position + catalog_or_db = table.args.get("catalog") or table.args.get("db") + if catalog_or_db is not None: + catalog_or_db_meta = TokenPositionDetails.from_meta(catalog_or_db.meta) + catalog_or_db_range_sqlmesh = catalog_or_db_meta.to_range(read_file) + start_pos_sqlmesh = catalog_or_db_range_sqlmesh.start + + description = generate_markdown_description(referenced_model) + + # For external models in YAML files, find the specific model block + if isinstance(referenced_model, ExternalModel): + yaml_target_range: t.Optional[Range] = None + if ( + referenced_model_path.suffix in (".yaml", ".yml") + and referenced_model_path.is_file() + ): + yaml_target_range = _get_yaml_model_range( + referenced_model_path, referenced_model.name + ) + references.append( + ExternalModelReference( + path=referenced_model_path, + range=Range( + start=start_pos_sqlmesh, + end=end_pos_sqlmesh, + ), + markdown_description=description, + target_range=yaml_target_range, + ) + ) + + column_references = _process_column_references( + scope=scope, + reference_name=normalized_reference_name, + read_file=read_file, + referenced_model_path=referenced_model_path, + description=description, + yaml_target_range=yaml_target_range, + reference_type="external_model", + default_catalog=context.default_catalog, + dialect=dialect, + ) + references.extend(column_references) + else: + references.append( + ModelReference( + path=referenced_model_path, + range=Range( + start=start_pos_sqlmesh, + end=end_pos_sqlmesh, + ), + markdown_description=description, + ) + ) + + column_references = _process_column_references( + scope=scope, + reference_name=normalized_reference_name, + read_file=read_file, + referenced_model_path=referenced_model_path, + description=description, + reference_type="model", + default_catalog=context.default_catalog, + dialect=dialect, + ) + references.extend(column_references) + + return references + + +def generate_markdown_description( + model: t.Union[SqlModel, ExternalModel, PythonModel, SeedModel], +) -> t.Optional[str]: + description = model.description + columns = model.columns_to_types + column_descriptions = model.column_descriptions + + if columns is None: + return description or None + + columns_table = "\n".join( + [ + f"| {column} | {column_type} | {column_descriptions.get(column, '')} |" + for column, column_type in columns.items() + ] + ) + + table_header = "| Column | Type | Description |\n|--------|------|-------------|\n" + columns_text = table_header + columns_table + return f"{description}\n\n{columns_text}" if description else columns_text + + +def _process_column_references( + scope: t.Any, + reference_name: str, + read_file: t.List[str], + referenced_model_path: Path, + description: t.Optional[str] = None, + yaml_target_range: t.Optional[Range] = None, + reference_type: t.Literal["model", "external_model", "cte"] = "model", + default_catalog: t.Optional[str] = None, + dialect: t.Optional[str] = None, + cte_target_range: t.Optional[Range] = None, +) -> t.List[Reference]: + """ + Process column references for a given table and create appropriate reference objects. + + Args: + scope: The SQL scope to search for columns + reference_name: The full reference name (may include database/catalog) + read_file: The file content as list of lines + referenced_model_path: Path of the referenced model + description: Markdown description for the reference + yaml_target_range: Target range for external models (YAML files) + reference_type: Type of reference - "model", "external_model", or "cte" + default_catalog: Default catalog for normalization + dialect: SQL dialect for normalization + cte_target_range: Target range for CTE references + + Returns: + List of table references for column usages + """ + + references: t.List[Reference] = [] + for column in scope.find_all(exp.Column): + if column.table: + if reference_type == "cte": + if column.table == reference_name: + table_range = _get_column_table_range(column, read_file) + references.append( + CTEReference( + path=referenced_model_path, + range=table_range, + target_range=cte_target_range, + ) + ) + else: + table_parts = [part.sql(dialect) for part in column.parts[:-1]] + table_ref = ".".join(table_parts) + normalized_reference_name = normalize_model_name( + table_ref, + default_catalog=default_catalog, + dialect=dialect, + ) + if normalized_reference_name == reference_name: + table_range = _get_column_table_range(column, read_file) + if reference_type == "external_model": + references.append( + ExternalModelReference( + path=referenced_model_path, + range=table_range, + markdown_description=description, + target_range=yaml_target_range, + ) + ) + else: + references.append( + ModelReference( + path=referenced_model_path, + range=table_range, + markdown_description=description, + ) + ) + + return references + + +def _get_column_table_range(column: exp.Column, read_file: t.List[str]) -> Range: + """ + Get the range for a column's table reference, handling both simple and qualified table names. + + Args: + column: The column expression + read_file: The file content as list of lines + + Returns: + The Range covering the table reference in the column + """ + + table_parts = column.parts[:-1] + + start_range = TokenPositionDetails.from_meta(table_parts[0].meta).to_range(read_file) + end_range = TokenPositionDetails.from_meta(table_parts[-1].meta).to_range(read_file) + + return Range( + start=start_range.start, + end=end_range.end, + ) + + +def _get_yaml_model_range(path: Path, model_name: str) -> t.Optional[Range]: + """ + Find the range of a specific model block in a YAML file. + + Args: + yaml_path: Path to the YAML file + model_name: Name of the model to find + + Returns: + The Range of the model block in the YAML file, or None if not found + """ + yaml = YAML() + with path.open("r", encoding="utf-8") as f: + data = yaml.load(f) + + if not isinstance(data, list): + return None + + for item in data: + if isinstance(item, dict) and item.get("name") == model_name: + # Get size of block by taking the earliest line/col in the items block and the last line/col of the block + position_data = item.lc.data["name"] # type: ignore + start = Position(line=position_data[2], character=position_data[3]) + end = Position(line=position_data[2], character=position_data[3] + len(item["name"])) + return Range(start=start, end=end) + return None diff --git a/tests/core/linter/test_builtin.py b/tests/core/linter/test_builtin.py index 208b591a2d..b9cf759946 100644 --- a/tests/core/linter/test_builtin.py +++ b/tests/core/linter/test_builtin.py @@ -44,7 +44,8 @@ def test_no_missing_external_models(tmp_path, copy_to_temp_path) -> None: # Lint the models lints = context.lint_models(raise_on_error=False) assert len(lints) == 1 + assert lints[0].violation_range is not None assert ( - "Model 'sushi.customers' depends on unregistered external models: " - in lints[0].violation_msg + lints[0].violation_msg + == """Model '"memory"."sushi"."customers"' depends on unregistered external model '"memory"."raw"."demographics"'. Please register it in the external models file. This can be done by running 'sqlmesh create_external_models'.""" ) diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index 3d1e19f3cc..6aae4b869e 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -1,7 +1,7 @@ from sqlmesh.core.context import Context from sqlmesh.core.linter.rule import Position from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget -from sqlmesh.lsp.reference import LSPModelReference, get_model_definitions_for_a_path, by_position +from sqlmesh.lsp.reference import ModelReference, get_model_definitions_for_a_path, by_position from sqlmesh.lsp.uri import URI @@ -54,7 +54,7 @@ def test_reference_with_alias() -> None: for ref in get_model_definitions_for_a_path( lsp_context, URI.from_path(waiter_revenue_by_day_path) ) - if isinstance(ref, LSPModelReference) + if isinstance(ref, ModelReference) ] assert len(references) == 3 diff --git a/tests/lsp/test_reference_cte.py b/tests/lsp/test_reference_cte.py index c6c56fd8a5..9bc74bc990 100644 --- a/tests/lsp/test_reference_cte.py +++ b/tests/lsp/test_reference_cte.py @@ -1,7 +1,7 @@ import re from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget -from sqlmesh.lsp.reference import LSPCteReference, get_references +from sqlmesh.lsp.reference import CTEReference, get_references from sqlmesh.lsp.uri import URI from lsprotocol.types import Range, Position import typing as t @@ -28,7 +28,7 @@ def test_cte_parsing(): references = get_references(lsp_context, URI.from_path(sushi_customers_path), position) assert len(references) == 1 assert references[0].path == sushi_customers_path - assert isinstance(references[0], LSPCteReference) + assert isinstance(references[0], CTEReference) assert ( references[0].range.start.line == ranges[1].start.line ) # The reference location (where we clicked) @@ -43,7 +43,7 @@ def test_cte_parsing(): references = get_references(lsp_context, URI.from_path(sushi_customers_path), position) assert len(references) == 1 assert references[0].path == sushi_customers_path - assert isinstance(references[0], LSPCteReference) + assert isinstance(references[0], CTEReference) assert ( references[0].range.start.line == ranges[1].start.line ) # The reference location (where we clicked) diff --git a/tests/lsp/test_reference_external_model.py b/tests/lsp/test_reference_external_model.py index 36c64fe277..25de22f10f 100644 --- a/tests/lsp/test_reference_external_model.py +++ b/tests/lsp/test_reference_external_model.py @@ -1,3 +1,4 @@ +import os from pathlib import Path from sqlmesh import Config @@ -5,9 +6,11 @@ from sqlmesh.core.linter.helpers import read_range_from_file from sqlmesh.core.linter.rule import Position from sqlmesh.lsp.context import LSPContext, ModelTarget -from sqlmesh.lsp.reference import get_references, LSPExternalModelReference +from sqlmesh.lsp.reference import get_references from sqlmesh.lsp.uri import URI +from sqlmesh.utils.lineage import ExternalModelReference from tests.utils.test_filesystem import create_temp_file +import typing as t def test_reference() -> None: @@ -27,7 +30,7 @@ def test_reference() -> None: assert len(references) == 1 reference = references[0] - assert isinstance(reference, LSPExternalModelReference) + assert isinstance(reference, ExternalModelReference) path = reference.path assert path is not None assert str(path).endswith("external_models.yaml") @@ -55,8 +58,65 @@ def test_unregistered_external_model(tmp_path: Path): assert len(references) == 1 reference = references[0] - assert isinstance(reference, LSPExternalModelReference) + assert isinstance(reference, ExternalModelReference) assert reference.path is None assert reference.target_range is None assert reference.markdown_description == "Unregistered external model" assert read_range_from_file(model_path, reference.range) == "external_model" + + +def test_unregistered_external_model_with_schema( + copy_to_temp_path: t.Callable[[str], list[Path]], +) -> None: + """ + Tests that the linter correctly identifies unregistered external model dependencies. + + This test removes the `external_models.yaml` file from the sushi example project, + enables the linter, and verifies that the linter raises a violation for a model + that depends on unregistered external models. + """ + sushi_paths = copy_to_temp_path("examples/sushi") + sushi_path = sushi_paths[0] + + # Remove the external_models.yaml file + os.remove(sushi_path / "external_models.yaml") + + # Override the config.py to turn on lint + with open(sushi_path / "config.py", "r") as f: + read_file = f.read() + + before = """ linter=LinterConfig( + enabled=False, + rules=[ + "ambiguousorinvalidcolumn", + "invalidselectstarexpansion", + "noselectstar", + "nomissingaudits", + "nomissingowner", + "nomissingexternalmodels", + ], + ),""" + after = """linter=LinterConfig(enabled=True, rules=["nomissingexternalmodels"]),""" + read_file = read_file.replace(before, after) + assert after in read_file + with open(sushi_path / "config.py", "w") as f: + f.writelines(read_file) + + # Load the context with the temporary sushi path + context = Context(paths=[sushi_path]) + + model = context.get_model("sushi.customers") + if model is None: + raise AssertionError("Model 'sushi.customers' not found in context") + + lsp_context = LSPContext(context) + path = model._path + assert path is not None + uri = URI.from_path(path) + references = get_references(lsp_context, uri, Position(line=42, character=20)) + + assert len(references) == 1 + reference = references[0] + assert isinstance(reference, ExternalModelReference) + assert reference.path is None + assert read_range_from_file(path, reference.range) == "raw.demographics" diff --git a/tests/lsp/test_reference_macro.py b/tests/lsp/test_reference_macro.py index e287212dd2..3ee7c48b3b 100644 --- a/tests/lsp/test_reference_macro.py +++ b/tests/lsp/test_reference_macro.py @@ -1,6 +1,6 @@ from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget -from sqlmesh.lsp.reference import LSPMacroReference, get_macro_definitions_for_a_path +from sqlmesh.lsp.reference import MacroReference, get_macro_definitions_for_a_path from sqlmesh.lsp.uri import URI @@ -24,6 +24,6 @@ def test_macro_references() -> None: # Check that all references point to the utils.py file for ref in macro_references: - assert isinstance(ref, LSPMacroReference) + assert isinstance(ref, MacroReference) assert URI.from_path(ref.path).value.endswith("sushi/macros/utils.py") assert ref.target_range is not None diff --git a/tests/lsp/test_reference_macro_multi.py b/tests/lsp/test_reference_macro_multi.py index 8226085a1d..3902c0b275 100644 --- a/tests/lsp/test_reference_macro_multi.py +++ b/tests/lsp/test_reference_macro_multi.py @@ -1,6 +1,6 @@ from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext, ModelTarget -from sqlmesh.lsp.reference import LSPMacroReference, get_macro_definitions_for_a_path +from sqlmesh.lsp.reference import MacroReference, get_macro_definitions_for_a_path from sqlmesh.lsp.uri import URI @@ -19,6 +19,6 @@ def test_macro_references_multirepo() -> None: assert len(macro_references) == 2 for ref in macro_references: - assert isinstance(ref, LSPMacroReference) + assert isinstance(ref, MacroReference) assert str(URI.from_path(ref.path).value).endswith("multi/repo_2/macros/__init__.py") assert ref.target_range is not None diff --git a/tests/lsp/test_description.py b/tests/utils/test_lineage_description.py similarity index 93% rename from tests/lsp/test_description.py rename to tests/utils/test_lineage_description.py index 054d55fecc..e7053e3bcc 100644 --- a/tests/lsp/test_description.py +++ b/tests/utils/test_lineage_description.py @@ -1,5 +1,5 @@ from sqlmesh.core.context import Context -from sqlmesh.lsp.description import generate_markdown_description +from sqlmesh.utils.lineage import generate_markdown_description def test_model_description() -> None: From 03c161c6cebda9d8e87154169158aa22ea79d562 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:02:15 +0100 Subject: [PATCH 0632/1056] ci(vscode): be less aggressive in CI (#5053) --- vscode/extension/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode/extension/playwright.config.ts b/vscode/extension/playwright.config.ts index f1867e15ac..95d3bda589 100644 --- a/vscode/extension/playwright.config.ts +++ b/vscode/extension/playwright.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ timeout: 60_000, // TODO: When stable, allow retries in CI retries: process.env.CI ? 2 : 0, - workers: 4, + workers: process.env.CI ? 2 : 4, reporter: [['html', { outputFolder: 'playwright-report' }], ['list']], projects: [ { From bb87f056a6b11e1ab5e796188d030c824ad83f6c Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:51:56 +0100 Subject: [PATCH 0633/1056] chore(vscode): move tests to central temp dir creation (#5054) --- vscode/extension/tests/bad_setup.spec.ts | 17 +++---- vscode/extension/tests/broken_project.spec.ts | 31 +++++------ vscode/extension/tests/diagnostics.spec.ts | 39 ++++++-------- .../extension/tests/external_models.spec.ts | 11 +--- .../extension/tests/find_references.spec.ts | 51 ++++++++++++------- vscode/extension/tests/fixtures.ts | 25 ++++++++- vscode/extension/tests/format.spec.ts | 9 ++-- .../extension/tests/go_to_definition.spec.ts | 12 ++--- vscode/extension/tests/hints.spec.ts | 5 +- vscode/extension/tests/lineage.spec.ts | 7 ++- .../extension/tests/lineage_settings.spec.ts | 4 +- vscode/extension/tests/python_env.spec.ts | 25 ++++----- vscode/extension/tests/quickfix.spec.ts | 7 +-- vscode/extension/tests/rename_cte.spec.ts | 32 +++++++----- vscode/extension/tests/render.spec.ts | 11 ++-- vscode/extension/tests/stop.spec.ts | 7 +-- vscode/extension/tests/tcloud.spec.ts | 21 ++------ vscode/extension/tests/tests.spec.ts | 9 ++-- vscode/extension/tests/venv_naming.spec.ts | 7 +-- 19 files changed, 159 insertions(+), 171 deletions(-) diff --git a/vscode/extension/tests/bad_setup.spec.ts b/vscode/extension/tests/bad_setup.spec.ts index c4da69ca23..3f715e525a 100644 --- a/vscode/extension/tests/bad_setup.spec.ts +++ b/vscode/extension/tests/bad_setup.spec.ts @@ -16,10 +16,8 @@ import { test('missing LSP dependencies shows install prompt', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) const pythonEnvDir = path.join(tempDir, '.venv') const pythonDetails = await createVirtualEnvironment(pythonEnvDir) const custom_materializations = path.join( @@ -60,10 +58,11 @@ test('missing LSP dependencies shows install prompt', async ({ expect(await page.locator('text=Install').count()).toBeGreaterThanOrEqual(1) }) -test('lineage, no sqlmesh found', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) +test('lineage, no sqlmesh found', async ({ + page, + sharedCodeServer, + tempDir, +}) => { const pythonEnvDir = path.join(tempDir, '.venv') const pythonDetails = await createVirtualEnvironment(pythonEnvDir) @@ -95,10 +94,8 @@ test('lineage, no sqlmesh found', async ({ page, sharedCodeServer }) => { test.skip('check that the LSP runs correctly by opening lineage when looking at another file before not in workspace', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) const pythonEnvDir = path.join(tempDir, '.venv') const pythonDetails = await createVirtualEnvironment(pythonEnvDir) diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index 8fe78fa321..f32a39a86d 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -1,6 +1,5 @@ import { test, expect } from './fixtures' import fs from 'fs-extra' -import os from 'os' import path from 'path' import { openLineageView, @@ -12,10 +11,11 @@ import { } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('bad project, double model', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) +test('bad project, double model', async ({ + tempDir, + page, + sharedCodeServer, +}) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) // Read the customers.sql file @@ -52,11 +52,9 @@ test('bad project, double model', async ({ page, sharedCodeServer }) => { test('working project, then broken through adding double model, then refixed', async ({ page, + tempDir, sharedCodeServer, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -149,11 +147,9 @@ test('working project, then broken through adding double model, then refixed', a test('bad project, double model, then fixed', async ({ page, + tempDir, sharedCodeServer, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) // Read the customers.sql file @@ -218,10 +214,8 @@ test('bad project, double model, then fixed', async ({ test('bad project, double model, check lineage', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) // Read the customers.sql file @@ -248,10 +242,11 @@ test('bad project, double model, check lineage', async ({ await page.waitForTimeout(500) }) -test('bad model block, then fixed', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) +test('bad model block, then fixed', async ({ + page, + sharedCodeServer, + tempDir, +}) => { // Copy over the sushi project await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) diff --git a/vscode/extension/tests/diagnostics.spec.ts b/vscode/extension/tests/diagnostics.spec.ts index fcac5cecf0..1c0e471e82 100644 --- a/vscode/extension/tests/diagnostics.spec.ts +++ b/vscode/extension/tests/diagnostics.spec.ts @@ -10,8 +10,8 @@ import yaml from 'yaml' test('Workspace diagnostics show up in the diagnostics panel', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -55,10 +55,8 @@ test.describe('Bad config.py/config.yaml file issues', () => { test('sqlmesh init, then corrupted config.yaml, bad yaml', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) await setup(tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -135,10 +133,8 @@ test.describe('Bad config.py/config.yaml file issues', () => { test('sushi example, correct python, bad config', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -170,10 +166,11 @@ test.describe('Bad config.py/config.yaml file issues', () => { await expect(errorElement).toBeVisible({ timeout: 5000 }) }) - test('sushi example, bad config.py', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) + test('sushi example, bad config.py', async ({ + page, + sharedCodeServer, + tempDir, + }) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -205,10 +202,7 @@ test.describe('Bad config.py/config.yaml file issues', () => { }) test.describe('Diagnostics for bad SQLMesh models', () => { - test('duplicate model names', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) + test('duplicate model names', async ({ page, sharedCodeServer, tempDir }) => { // Copy over the sushi project await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -246,10 +240,7 @@ test.describe('Diagnostics for bad SQLMesh models', () => { .isVisible({ timeout: 5_000 }) }) - test('bad model block', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) + test('bad model block', async ({ page, sharedCodeServer, tempDir }) => { // Copy over the sushi project await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -289,11 +280,11 @@ test.describe('Diagnostics for bad SQLMesh models', () => { }) test.describe('Diagnostics for bad audits', () => { - test('bad audit block in audit', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) - + test('bad audit block in audit', async ({ + page, + sharedCodeServer, + tempDir, + }) => { // Copy over the sushi project await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) diff --git a/vscode/extension/tests/external_models.spec.ts b/vscode/extension/tests/external_models.spec.ts index 8d70edae62..4fdd19fa61 100644 --- a/vscode/extension/tests/external_models.spec.ts +++ b/vscode/extension/tests/external_models.spec.ts @@ -1,4 +1,3 @@ -import os from 'os' import { openServerPage, SUSHI_SOURCE_PATH, @@ -10,12 +9,9 @@ import fs from 'fs-extra' import path from 'path' test.describe('External model files trigger lsp', () => { - test('external_models.yaml', async ({ page, sharedCodeServer }) => { + test('external_models.yaml', async ({ page, sharedCodeServer, tempDir }) => { const file = 'external_models.yaml' - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-sushi-'), - ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) // Assert external_models.yaml exists @@ -38,12 +34,9 @@ test.describe('External model files trigger lsp', () => { await waitForLoadedSQLMesh(page) }) - test('external_models.yml', async ({ page, sharedCodeServer }) => { + test('external_models.yml', async ({ page, sharedCodeServer, tempDir }) => { const file = 'external_models.yml' - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-sushi-'), - ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) // Move external_models.yaml to external_models.yml diff --git a/vscode/extension/tests/find_references.spec.ts b/vscode/extension/tests/find_references.spec.ts index e15868e234..e2d25269a6 100644 --- a/vscode/extension/tests/find_references.spec.ts +++ b/vscode/extension/tests/find_references.spec.ts @@ -1,7 +1,5 @@ import { test, expect, Page } from './fixtures' -import path from 'path' import fs from 'fs-extra' -import os from 'os' import { findAllReferences, goToReferences, @@ -12,11 +10,9 @@ import { import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' // Helper function to set up a test environment for model references -async function setupModelTestEnvironment(): Promise { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) +async function setupModelTestEnvironment(tempDir: string): Promise { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) - return tempDir } // Helper function to navigate to models folder @@ -63,8 +59,9 @@ test.describe('Model References', () => { test('Go to References (Shift+F12) for Model usage', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) // Open customers.sql which contains references to other models @@ -130,8 +127,9 @@ test.describe('Model References', () => { test('Find All References (Alt+Shift+F12) for Model', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) @@ -179,8 +177,9 @@ test.describe('Model References', () => { test('Go to References for Model from Audit', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) // Open assert_item_price_above_zero.sql audit file which references sushi.items model @@ -263,8 +262,9 @@ test.describe('Model References', () => { test.skip('Find All Model References from Audit', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) @@ -325,8 +325,9 @@ test.describe('CTE References', () => { test('Go to references from definition of CTE', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) await openCustomersFile(page) @@ -363,8 +364,9 @@ test.describe('CTE References', () => { test('Go to references from usage of CTE', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) @@ -403,8 +405,9 @@ test.describe('CTE References', () => { test('Go to references for nested CTE', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) await openCustomersFile(page) @@ -440,8 +443,12 @@ test.describe('CTE References', () => { await page.waitForSelector('text=FROM current_marketing') }) - test('Find all references for CTE', async ({ page, sharedCodeServer }) => { - const tempDir = await setupModelTestEnvironment() + test('Find all references for CTE', async ({ + page, + sharedCodeServer, + tempDir, + }) => { + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) await openCustomersFile(page) @@ -464,8 +471,9 @@ test.describe('CTE References', () => { test('Find all references from usage for CTE', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) @@ -489,8 +497,9 @@ test.describe('CTE References', () => { test('Find all references for nested CTE', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) await openCustomersFile(page) @@ -518,8 +527,9 @@ test.describe('Macro References', () => { test('Go to References for @ADD_ONE macro', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) await openTopWaitersFile(page) @@ -553,8 +563,10 @@ test.describe('Macro References', () => { test('Find All References for @MULTIPLY macro', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) + await openServerPage(page, tempDir, sharedCodeServer) await openTopWaitersFile(page) @@ -606,8 +618,9 @@ test.describe('Macro References', () => { test('Go to References for @SQL_LITERAL macro', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await setupModelTestEnvironment() + await setupModelTestEnvironment(tempDir) await openServerPage(page, tempDir, sharedCodeServer) await openTopWaitersFile(page) diff --git a/vscode/extension/tests/fixtures.ts b/vscode/extension/tests/fixtures.ts index 916305c9c2..7a10d811fa 100644 --- a/vscode/extension/tests/fixtures.ts +++ b/vscode/extension/tests/fixtures.ts @@ -9,8 +9,11 @@ import { } from './utils_code_server' // Worker-scoped fixture to start/stop VS Code server once per worker -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export const test = base.extend<{}, { sharedCodeServer: CodeServerContext }>({ +export const test = base.extend< + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + {}, + { sharedCodeServer: CodeServerContext; tempDir: string } +>({ sharedCodeServer: [ // eslint-disable-next-line no-empty-pattern async ({}, use) => { @@ -37,6 +40,24 @@ export const test = base.extend<{}, { sharedCodeServer: CodeServerContext }>({ }, { scope: 'worker', auto: true }, ], + tempDir: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use) => { + // Create a temporary directory for each test + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-temp-'), + ) + console.log(`Created temporary directory: ${tempDir}`) + await use(tempDir) + + // Clean up after each test + console.log(`Cleaning up temporary directory: ${tempDir}`) + await fs.remove(tempDir) + }, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + { auto: true }, + ], }) // Export expect and Page from Playwright for convenience diff --git a/vscode/extension/tests/format.spec.ts b/vscode/extension/tests/format.spec.ts index fb95e66ba0..c8a98a066c 100644 --- a/vscode/extension/tests/format.spec.ts +++ b/vscode/extension/tests/format.spec.ts @@ -1,7 +1,5 @@ import { test, expect } from './fixtures' -import path from 'path' import fs from 'fs-extra' -import os from 'os' import { openServerPage, runCommand, @@ -10,8 +8,11 @@ import { } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Format project works correctly', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) +test('Format project works correctly', async ({ + page, + sharedCodeServer, + tempDir, +}) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) diff --git a/vscode/extension/tests/go_to_definition.spec.ts b/vscode/extension/tests/go_to_definition.spec.ts index 36ad2177e1..3b85c73f27 100644 --- a/vscode/extension/tests/go_to_definition.spec.ts +++ b/vscode/extension/tests/go_to_definition.spec.ts @@ -1,7 +1,5 @@ import { test, expect } from './fixtures' -import path from 'path' import fs from 'fs-extra' -import os from 'os' import { goToDefinition, openServerPage, @@ -10,8 +8,7 @@ import { } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Stop server works', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) +test('Stop server works', async ({ page, sharedCodeServer, tempDir }) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) await openServerPage(page, tempDir, sharedCodeServer) @@ -42,8 +39,11 @@ test('Stop server works', async ({ page, sharedCodeServer }) => { await expect(page.locator('text=def multiply(')).toBeVisible() }) -test('Go to definition for model', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) +test('Go to definition for model', async ({ + page, + sharedCodeServer, + tempDir, +}) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) diff --git a/vscode/extension/tests/hints.spec.ts b/vscode/extension/tests/hints.spec.ts index 8bcd2b9d09..a74f8e184b 100644 --- a/vscode/extension/tests/hints.spec.ts +++ b/vscode/extension/tests/hints.spec.ts @@ -1,7 +1,5 @@ import { test, expect } from './fixtures' -import path from 'path' import fs from 'fs-extra' -import os from 'os' import { openServerPage, SUSHI_SOURCE_PATH, @@ -9,8 +7,7 @@ import { } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Model type hinting', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) +test('Model type hinting', async ({ page, sharedCodeServer, tempDir }) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) await openServerPage(page, tempDir, sharedCodeServer) diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index e61079e035..eb6a695e3c 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -28,8 +28,8 @@ async function testLineageWithProjectPath(page: Page): Promise { test('Lineage panel renders correctly - no project path config (default)', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -39,6 +39,7 @@ test('Lineage panel renders correctly - no project path config (default)', async test.skip('Lineage panel renders correctly - relative project path', async ({ page, + sharedCodeServer, }) => { const workspaceDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), @@ -71,6 +72,8 @@ test.skip('Lineage panel renders correctly - relative project path', async ({ test.skip('Lineage panel renders correctly - absolute project path', async ({ page, + tempDir, + sharedCodeServer, }) => { const workspaceDir = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), @@ -103,6 +106,8 @@ test.skip('Lineage panel renders correctly - absolute project path', async ({ test.skip('Lineage panel renders correctly - relative project outside of workspace', async ({ page, + sharedCodeServer, + tempDir, }) => { const tempFolder = await fs.mkdtemp( path.join(os.tmpdir(), 'vscode-test-workspace-'), diff --git a/vscode/extension/tests/lineage_settings.spec.ts b/vscode/extension/tests/lineage_settings.spec.ts index 47ccf47f35..c3237f13dc 100644 --- a/vscode/extension/tests/lineage_settings.spec.ts +++ b/vscode/extension/tests/lineage_settings.spec.ts @@ -1,7 +1,5 @@ import { test, expect } from './fixtures' -import path from 'path' import fs from 'fs-extra' -import os from 'os' import { openLineageView, openServerPage, @@ -13,8 +11,8 @@ import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' test('Settings button is visible in the lineage view', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) diff --git a/vscode/extension/tests/python_env.spec.ts b/vscode/extension/tests/python_env.spec.ts index 27dd7a481f..cfbdc7efa6 100644 --- a/vscode/extension/tests/python_env.spec.ts +++ b/vscode/extension/tests/python_env.spec.ts @@ -10,7 +10,6 @@ import { SUSHI_SOURCE_PATH, waitForLoadedSQLMesh, } from './utils' -import os from 'os' import path from 'path' import { setTcloudVersion, setupAuthenticatedState } from './tcloud_utils' import { CodeServerContext } from './utils_code_server' @@ -41,13 +40,9 @@ async function runTest( await openLineageView(page) } -async function setupEnvironment(): Promise<{ - tempDir: string +async function setupEnvironment(tempDir: string): Promise<{ pythonDetails: PythonEnvironment }> { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) await fs.copy(SUSHI_SOURCE_PATH, tempDir) const pythonEnvDir = path.join(tempDir, '.venv') const pythonDetails = await createVirtualEnvironment(pythonEnvDir) @@ -67,19 +62,19 @@ async function setupEnvironment(): Promise<{ await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { spaces: 2, }) - return { tempDir, pythonDetails } + return { pythonDetails } } test.describe('python environment variable injection on sqlmesh_lsp', () => { - test('normal setup - error ', async ({ page, sharedCodeServer }) => { - const { tempDir } = await setupEnvironment() + test('normal setup - error ', async ({ page, sharedCodeServer, tempDir }) => { + await setupEnvironment(tempDir) writeEnvironmentConfig(tempDir) await runTest(page, sharedCodeServer, tempDir) await page.waitForSelector('text=Error creating context') }) - test('normal setup - set', async ({ page, sharedCodeServer }) => { - const { tempDir } = await setupEnvironment() + test('normal setup - set', async ({ page, sharedCodeServer, tempDir }) => { + await setupEnvironment(tempDir) writeEnvironmentConfig(tempDir) const env_file = path.join(tempDir, '.env') fs.writeFileSync(env_file, 'TEST_VAR=test_value') @@ -113,16 +108,16 @@ async function setupTcloudProject( } test.describe('tcloud version', () => { - test('normal setup - error ', async ({ page, sharedCodeServer }) => { - const { tempDir, pythonDetails } = await setupEnvironment() + test('normal setup - error ', async ({ page, sharedCodeServer, tempDir }) => { + const { pythonDetails } = await setupEnvironment(tempDir) await setupTcloudProject(tempDir, pythonDetails) writeEnvironmentConfig(tempDir) await runTest(page, sharedCodeServer, tempDir) await page.waitForSelector('text=Error creating context') }) - test('normal setup - set', async ({ page, sharedCodeServer }) => { - const { tempDir, pythonDetails } = await setupEnvironment() + test('normal setup - set', async ({ page, sharedCodeServer, tempDir }) => { + const { pythonDetails } = await setupEnvironment(tempDir) await setupTcloudProject(tempDir, pythonDetails) writeEnvironmentConfig(tempDir) const env_file = path.join(tempDir, '.env') diff --git a/vscode/extension/tests/quickfix.spec.ts b/vscode/extension/tests/quickfix.spec.ts index 5bbfe4020b..60d0207f7c 100644 --- a/vscode/extension/tests/quickfix.spec.ts +++ b/vscode/extension/tests/quickfix.spec.ts @@ -1,6 +1,5 @@ import fs from 'fs-extra' import path from 'path' -import os from 'os' import { openProblemsView, SUSHI_SOURCE_PATH, @@ -9,11 +8,7 @@ import { import { test, expect } from './fixtures' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('noselectstar quickfix', async ({ page, sharedCodeServer }) => { - // Base test setup - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) +test('noselectstar quickfix', async ({ page, sharedCodeServer, tempDir }) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) diff --git a/vscode/extension/tests/rename_cte.spec.ts b/vscode/extension/tests/rename_cte.spec.ts index 4f566ef19c..579bda06dd 100644 --- a/vscode/extension/tests/rename_cte.spec.ts +++ b/vscode/extension/tests/rename_cte.spec.ts @@ -1,7 +1,5 @@ import { test, expect, Page } from './fixtures' -import path from 'path' import fs from 'fs-extra' -import os from 'os' import { findAllReferences, openServerPage, @@ -14,11 +12,12 @@ import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' async function setupTestEnvironment({ page, sharedCodeServer, + tempDir, }: { page: Page sharedCodeServer: any + tempDir: string }) { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -39,8 +38,12 @@ async function setupTestEnvironment({ } test.describe('CTE Rename', () => { - test('Rename CTE from definition', async ({ page, sharedCodeServer }) => { - await setupTestEnvironment({ page, sharedCodeServer }) + test('Rename CTE from definition', async ({ + page, + sharedCodeServer, + tempDir, + }) => { + await setupTestEnvironment({ page, sharedCodeServer, tempDir }) // Click on the inner CTE definition "current_marketing" (not the outer one) await page.locator('text=WITH current_marketing AS').click({ position: { x: 100, y: 5 }, @@ -59,8 +62,8 @@ test.describe('CTE Rename', () => { await page.waitForSelector('text=WITH new_marketing AS') }) - test('Rename CTE from usage', async ({ page, sharedCodeServer }) => { - await setupTestEnvironment({ page, sharedCodeServer }) + test('Rename CTE from usage', async ({ page, sharedCodeServer, tempDir }) => { + await setupTestEnvironment({ page, sharedCodeServer, tempDir }) // Click on CTE usage in FROM clause await page.locator('text=FROM current_marketing_outer').click({ position: { x: 80, y: 5 }, @@ -81,8 +84,8 @@ test.describe('CTE Rename', () => { await page.waitForSelector('text=FROM updated_marketing_out') }) - test('Cancel CTE rename', async ({ page, sharedCodeServer }) => { - await setupTestEnvironment({ page, sharedCodeServer }) + test('Cancel CTE rename', async ({ page, sharedCodeServer, tempDir }) => { + await setupTestEnvironment({ page, sharedCodeServer, tempDir }) // Click on the CTE to rename await page.locator('text=current_marketing_outer').first().click() @@ -107,9 +110,10 @@ test.describe('CTE Rename', () => { test('Rename CTE updates all references', async ({ page, + tempDir, sharedCodeServer, }) => { - await setupTestEnvironment({ page, sharedCodeServer }) + await setupTestEnvironment({ page, sharedCodeServer, tempDir }) // Click on the CTE definition await page.locator('text=WITH current_marketing AS').click({ position: { x: 100, y: 5 }, @@ -141,8 +145,12 @@ test.describe('CTE Rename', () => { await page.waitForSelector('text=renamed_cte.customer_id != 100') }) - test('Rename CTE with preview', async ({ page, sharedCodeServer }) => { - await setupTestEnvironment({ page, sharedCodeServer }) + test('Rename CTE with preview', async ({ + page, + sharedCodeServer, + tempDir, + }) => { + await setupTestEnvironment({ page, sharedCodeServer, tempDir }) // Click on the CTE to rename await page.locator('text=WITH current_marketing AS').click({ position: { x: 100, y: 5 }, diff --git a/vscode/extension/tests/render.spec.ts b/vscode/extension/tests/render.spec.ts index 298e3c690d..db660daae1 100644 --- a/vscode/extension/tests/render.spec.ts +++ b/vscode/extension/tests/render.spec.ts @@ -1,7 +1,5 @@ import { test, expect } from './fixtures' -import path from 'path' import fs from 'fs-extra' -import os from 'os' import { openLineageView, openServerPage, @@ -11,8 +9,7 @@ import { } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Render works correctly', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) +test('Render works correctly', async ({ page, sharedCodeServer, tempDir }) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -47,8 +44,8 @@ test('Render works correctly', async ({ page, sharedCodeServer }) => { test('Render works correctly with model without a description', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -83,8 +80,8 @@ test('Render works correctly with model without a description', async ({ test('Render works correctly with every rendered model opening a new tab', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -127,8 +124,8 @@ test('Render works correctly with every rendered model opening a new tab', async test('Render shows model picker when no active editor is open', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) diff --git a/vscode/extension/tests/stop.spec.ts b/vscode/extension/tests/stop.spec.ts index b15638be63..64a12b2e46 100644 --- a/vscode/extension/tests/stop.spec.ts +++ b/vscode/extension/tests/stop.spec.ts @@ -1,17 +1,14 @@ -import path from 'path' import { openServerPage, runCommand, SUSHI_SOURCE_PATH, waitForLoadedSQLMesh, } from './utils' -import os from 'os' import { test } from './fixtures' import fs from 'fs-extra' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Stop server works', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) +test('Stop server works', async ({ page, sharedCodeServer, tempDir }) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) @@ -55,8 +52,8 @@ test('Stop server works', async ({ page, sharedCodeServer }) => { test('Stopped server only restarts when explicitly requested', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) diff --git a/vscode/extension/tests/tcloud.spec.ts b/vscode/extension/tests/tcloud.spec.ts index 2d9010e059..1229696e02 100644 --- a/vscode/extension/tests/tcloud.spec.ts +++ b/vscode/extension/tests/tcloud.spec.ts @@ -1,7 +1,6 @@ import { expect, test } from './fixtures' import path from 'path' import fs from 'fs-extra' -import os from 'os' import { createVirtualEnvironment, openServerPage, @@ -43,10 +42,8 @@ async function setupPythonEnvironment(envDir: string): Promise { test('not signed in, shows sign in window', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) const pythonEnvDir = path.join(tempDir, '.venv') const context = await startCodeServer({ tempDir }) @@ -116,10 +113,8 @@ test('not signed in, shows sign in window', async ({ test('signed in and not installed shows installation window', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) const pythonEnvDir = path.join(tempDir, '.venv') const context = await startCodeServer({ tempDir }) @@ -192,10 +187,8 @@ test('signed in and not installed shows installation window', async ({ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when ready', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) const pythonEnvDir = path.join(tempDir, '.venv') try { @@ -269,10 +262,8 @@ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when read test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when ready', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) const pythonEnvDir = path.join(tempDir, '.venv') try { @@ -346,10 +337,8 @@ test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when read // but the test is still useful when running it manually. test.skip('tcloud not signed in and not installed, shows sign in window and then fact that loaded', async ({ page, + tempDir, }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) const pythonEnvDir = path.join(tempDir, '.venv') // Create a tcloud.yaml to mark this as a tcloud project diff --git a/vscode/extension/tests/tests.spec.ts b/vscode/extension/tests/tests.spec.ts index 415eddb543..bea8776447 100644 --- a/vscode/extension/tests/tests.spec.ts +++ b/vscode/extension/tests/tests.spec.ts @@ -1,7 +1,5 @@ import { test } from './fixtures' -import path from 'path' import fs from 'fs-extra' -import os from 'os' import { openServerPage, runCommand, @@ -10,8 +8,11 @@ import { } from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('Format project works correctly', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-')) +test('Format project works correctly', async ({ + page, + sharedCodeServer, + tempDir, +}) => { await fs.copy(SUSHI_SOURCE_PATH, tempDir) await createPythonInterpreterSettingsSpecifier(tempDir) diff --git a/vscode/extension/tests/venv_naming.spec.ts b/vscode/extension/tests/venv_naming.spec.ts index ee036a5eb6..5cb1730a18 100644 --- a/vscode/extension/tests/venv_naming.spec.ts +++ b/vscode/extension/tests/venv_naming.spec.ts @@ -1,6 +1,5 @@ import { test } from './fixtures' import fs from 'fs-extra' -import os from 'os' import path from 'path' import { createVirtualEnvironment, @@ -12,11 +11,7 @@ import { waitForLoadedSQLMesh, } from './utils' -test('venv being named .env', async ({ page, sharedCodeServer }) => { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-tcloud-'), - ) - +test('venv being named .env', async ({ page, sharedCodeServer, tempDir }) => { const pythonEnvDir = path.join(tempDir, '.env') const pythonDetails = await createVirtualEnvironment(pythonEnvDir) const custom_materializations = path.join( From a645a79004f6124260af2224512e28c4405e0b63 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:02:26 +0100 Subject: [PATCH 0634/1056] chore: remove unused variable (#5055) --- sqlmesh/lsp/main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index d53822fcac..1f53e71861 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -24,9 +24,6 @@ ApiResponseGetModels, ) -# Define the command constant -EXTERNAL_MODEL_UPDATE_COLUMNS = "sqlmesh.external_model_update_columns" - from sqlmesh.lsp.completions import get_sql_completions from sqlmesh.lsp.context import ( LSPContext, From 9238534b3273d040c878cd162853063d1ec605e5 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:52:50 +0100 Subject: [PATCH 0635/1056] fix(vscode): improve registering of lsp in extension (#5056) --- vscode/extension/src/commands/renderModel.ts | 5 +- vscode/extension/src/extension.ts | 193 ++++++++-------- vscode/extension/src/lsp/lsp.ts | 229 ++++++++----------- 3 files changed, 204 insertions(+), 223 deletions(-) diff --git a/vscode/extension/src/commands/renderModel.ts b/vscode/extension/src/commands/renderModel.ts index 6b5db3055c..24225c3e45 100644 --- a/vscode/extension/src/commands/renderModel.ts +++ b/vscode/extension/src/commands/renderModel.ts @@ -6,13 +6,16 @@ import { RenderedModelProvider } from '../providers/renderedModelProvider' export async function reRenderModelForSourceFile( sourceUri: string, - lspClient: LSPClient, + lspClient: LSPClient | undefined, renderedModelProvider: RenderedModelProvider, ): Promise { const renderedUri = renderedModelProvider.getRenderedUriForSource(sourceUri) if (!renderedUri) { return // No rendered model exists for this source file } + if (!lspClient) { + return + } // Call the render model API const result = await lspClient.call_custom_method('sqlmesh/render_model', { diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 74454f8fdb..0d0f6252cb 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -1,35 +1,51 @@ -import { format } from './commands/format' +/********************************************************************** + * Extension entry point * + *********************************************************************/ + import * as vscode from 'vscode' -import { - createOutputChannel, - onDidChangeConfiguration, - registerCommand, -} from './utilities/common/vscodeapi' -import { registerLogger, traceInfo, traceVerbose } from './utilities/common/log' -import { onDidChangePythonInterpreter } from './utilities/common/python' -import { LSPClient } from './lsp/lsp' -import { AuthenticationProviderTobikoCloud } from './auth/auth' + +import { format } from './commands/format' import { signOut } from './commands/signout' import { signIn } from './commands/signin' import { signInSpecifyFlow } from './commands/signinSpecifyFlow' import { renderModel, reRenderModelForSourceFile } from './commands/renderModel' import { stop } from './commands/stop' import { printEnvironment } from './commands/printEnvironment' -import { isErr } from '@bus/result' + +import { + createOutputChannel, + onDidChangeConfiguration, + registerCommand, +} from './utilities/common/vscodeapi' +import { + registerLogger, + traceInfo, + traceVerbose, + traceError, +} from './utilities/common/log' +import { onDidChangePythonInterpreter } from './utilities/common/python' +import { sleep } from './utilities/sleep' import { handleError } from './utilities/errors' + import { selector, completionProvider } from './completion/completion' import { LineagePanel } from './webviews/lineagePanel' import { RenderedModelProvider } from './providers/renderedModelProvider' -import { sleep } from './utilities/sleep' + import { controller as testController, setupTestController, } from './tests/tests' +import { isErr } from '@bus/result' +import { AuthenticationProviderTobikoCloud } from './auth/auth' +import { LSPClient } from './lsp/lsp' + +/** Singleton LSP client for the extension. */ let lspClient: LSPClient | undefined -// This method is called when your extension is activated -// Your extension is activated the very first time the command is executed +/** Handle to the (single) test controller disposable so we can replace it on restart. */ +let testControllerDisposable: vscode.Disposable | undefined + export async function activate(context: vscode.ExtensionContext) { const extensionOutputChannel = createOutputChannel('sqlmesh') context.subscriptions.push( @@ -38,7 +54,6 @@ export async function activate(context: vscode.ExtensionContext) { ) traceInfo('Activating SQLMesh extension') - traceInfo('Registering authentication provider') const authProvider = new AuthenticationProviderTobikoCloud() context.subscriptions.push( vscode.authentication.registerAuthenticationProvider( @@ -48,33 +63,70 @@ export async function activate(context: vscode.ExtensionContext) { { supportsMultipleAccounts: false }, ), ) - traceInfo('Authentication provider registered') + const restartLsp = async (invokedByUser = false): Promise => { + if (!lspClient) { + lspClient = new LSPClient() + } + + traceVerbose('Restarting SQLMesh LSP client') + const result = await lspClient.restart(invokedByUser) + if (isErr(result)) { + await handleError( + authProvider, + restartLsp, + result.error, + 'LSP restart failed', + ) + return + } + + // push once to avoid duplicate disposables on multiple restarts + if (!context.subscriptions.includes(lspClient)) { + context.subscriptions.push(lspClient) + } + + /* Replace the test controller each time we restart the client */ + if (testControllerDisposable) { + testControllerDisposable.dispose() + } + testControllerDisposable = setupTestController(lspClient) + context.subscriptions.push(testControllerDisposable) + } + + // commands needing the restart helper context.subscriptions.push( vscode.commands.registerCommand( 'sqlmesh.signin', - signIn(authProvider, async () => { - traceInfo('Restarting LSP after sign-in') - await restart() - }), + signIn(authProvider, () => restartLsp()), ), - ) - context.subscriptions.push( vscode.commands.registerCommand( 'sqlmesh.signinSpecifyFlow', - signInSpecifyFlow(authProvider, async () => { - traceInfo('Restarting LSP after sign-in') - await restart() - }), + signInSpecifyFlow(authProvider, () => restartLsp()), ), - ) - context.subscriptions.push( vscode.commands.registerCommand('sqlmesh.signout', signOut(authProvider)), ) + // Instantiate the LSP client (once) lspClient = new LSPClient() + const startResult = await lspClient.start() + if (isErr(startResult)) { + await handleError( + authProvider, + restartLsp, + startResult.error, + 'Failed to start LSP', + ) + return // abort activation – nothing else to do + } + + context.subscriptions.push(lspClient) - // Create and register the rendered model provider + // Initialize the test controller + testControllerDisposable = setupTestController(lspClient) + context.subscriptions.push(testControllerDisposable, testController) + + // Register the rendered model provider const renderedModelProvider = new RenderedModelProvider() context.subscriptions.push( vscode.workspace.registerTextDocumentContentProvider( @@ -91,7 +143,6 @@ export async function activate(context: vscode.ExtensionContext) { ), ) - // Register the webview const lineagePanel = new LineagePanel(context.extensionUri, lspClient) context.subscriptions.push( vscode.window.registerWebviewViewProvider( @@ -100,11 +151,10 @@ export async function activate(context: vscode.ExtensionContext) { ), ) - // Add file save listener for auto-rerendering models + // Re‑render model automatically when its source file is saved context.subscriptions.push( vscode.workspace.onDidSaveTextDocument(async document => { if ( - lspClient && renderedModelProvider.hasRenderedModelForSource( document.uri.toString(true), ) @@ -119,73 +169,23 @@ export async function activate(context: vscode.ExtensionContext) { }), ) - const restart = async (invokedByUser = false) => { - if (lspClient) { - traceVerbose('Restarting LSP client') - const restartResult = await lspClient.restart(invokedByUser) - if (isErr(restartResult)) { - return handleError( - authProvider, - restart, - restartResult.error, - 'LSP restart failed', - ) - } - context.subscriptions.push(lspClient) - context.subscriptions.push(setupTestController(lspClient)) - } else { - lspClient = new LSPClient() - const result = await lspClient.start(invokedByUser) - if (isErr(result)) { - return handleError( - authProvider, - restart, - result.error, - 'Failed to start LSP', - ) - } else { - context.subscriptions.push(lspClient) - context.subscriptions.push(setupTestController(lspClient)) - } - } - } - + // miscellaneous commands context.subscriptions.push( vscode.commands.registerCommand( 'sqlmesh.format', - format(authProvider, lspClient, restart), + format(authProvider, lspClient, restartLsp), ), + registerCommand('sqlmesh.restart', () => restartLsp(true)), + registerCommand('sqlmesh.stop', stop(lspClient)), + registerCommand('sqlmesh.printEnvironment', printEnvironment()), ) context.subscriptions.push( - onDidChangePythonInterpreter(async () => { - await restart() - }), - onDidChangeConfiguration(async () => { - await restart() - }), - registerCommand(`sqlmesh.restart`, async () => { - await restart(true) - }), - registerCommand(`sqlmesh.stop`, stop(lspClient)), - registerCommand(`sqlmesh.printEnvironment`, printEnvironment()), + onDidChangePythonInterpreter(() => restartLsp()), + onDidChangeConfiguration(() => restartLsp()), ) - const result = await lspClient.start() - if (isErr(result)) { - return handleError( - authProvider, - restart, - result.error, - 'Failed to start LSP', - ) - } else { - context.subscriptions.push(lspClient) - context.subscriptions.push(setupTestController(lspClient)) - context.subscriptions.push(testController) - } - - if (lspClient && !lspClient.hasCompletionCapability()) { + if (!lspClient.hasCompletionCapability()) { context.subscriptions.push( vscode.languages.registerCompletionItemProvider( selector, @@ -194,12 +194,19 @@ export async function activate(context: vscode.ExtensionContext) { ) } - traceInfo('Extension activated') + traceInfo('SQLMesh extension activated') } // This method is called when your extension is deactivated export async function deactivate() { - if (lspClient) { - await lspClient.dispose() + try { + if (testControllerDisposable) { + testControllerDisposable.dispose() + } + if (lspClient) { + await lspClient.dispose() + } + } catch (e) { + traceError(`Error during deactivate: ${e}`) } } diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index f5c7532ae4..0b7a5b4b62 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -20,47 +20,43 @@ import { CustomLSPMethods } from './custom' type SupportedMethodsState = | { type: 'not-fetched' } | { type: 'fetched'; methods: Set } - // TODO: This state is used when the `sqlmesh/supported_methods` endpoint is - // not supported by the LSP server. This is in order to be backward compatible - // with older versions of SQLMesh that do not support this endpoint. At some point - // we should remove this state and always fetch the supported methods. - | { type: 'endpoint-not-supported' } + | { type: 'endpoint-not-supported' } // fallback for very old servers let outputChannel: OutputChannel | undefined export class LSPClient implements Disposable { private client: LanguageClient | undefined - /** - * State to track whether the supported methods have been fetched. These are used to determine if a method is supported - * by the LSP server and return an error if not. - */ + + /** Caches which custom methods the server supports */ private supportedMethodsState: SupportedMethodsState = { type: 'not-fetched' } /** - * Explicitly stopped remembers whether the LSP client has been explicitly stopped - * by the user. This is used to prevent the client from being restarted unless the user - * explicitly calls the `restart` method. + * Remember whether the user explicitly stopped the client so that we do not + * auto‑start again until they ask for it. */ private explicitlyStopped = false - constructor() { - this.client = undefined + /** True when a LanguageClient instance is alive. */ + private get isRunning(): boolean { + return this.client !== undefined } - // TODO: This method is used to check if the LSP client has completion capability - // in order to be backward compatible with older versions of SQLMesh that do not - // support completion. At some point we should remove this method and always assume - // that the LSP client has completion capability. + /** + * Query whether the connected server advertises completion capability. + * (Transient helper kept for backwards‑compat reasons.) + */ public hasCompletionCapability(): boolean { if (!this.client) { traceError('LSP client is not initialized') return false } - const capabilities = this.client.initializeResult?.capabilities - const completion = capabilities?.completionProvider - return completion !== undefined + return ( + this.client.initializeResult?.capabilities?.completionProvider !== + undefined + ) } + /** Start the Language Client unless it is already running. */ public async start( overrideStoppedByUser = false, ): Promise> { @@ -70,10 +66,19 @@ export class LSPClient implements Disposable { ) return ok(undefined) } + + // Guard against duplicate initialisation + if (this.isRunning) { + traceInfo('LSP client already running – start() is a no‑op.') + return ok(undefined) + } + + // Ensure we have an output channel if (!outputChannel) { outputChannel = window.createOutputChannel('sqlmesh-lsp') } + // Resolve sqlmesh executable const sqlmesh = await sqlmeshLspExec() if (isErr(sqlmesh)) { traceError( @@ -81,114 +86,110 @@ export class LSPClient implements Disposable { ) return sqlmesh } - const workspaceFolders = getWorkspaceFolders() - if (workspaceFolders.length === 0) { - traceError(`No workspace folders found`) - return err({ - type: 'generic', - message: 'No workspace folders found', - }) + + // We need at least one workspace + if (getWorkspaceFolders().length === 0) { + const msg = 'No workspace folders found' + traceError(msg) + return err({ type: 'generic', message: msg }) } + const workspacePath = sqlmesh.value.workspacePath const serverOptions: ServerOptions = { run: { command: sqlmesh.value.bin, transport: TransportKind.stdio, - options: { - cwd: workspacePath, - env: sqlmesh.value.env, - }, + options: { cwd: workspacePath, env: sqlmesh.value.env }, args: sqlmesh.value.args, }, debug: { command: sqlmesh.value.bin, transport: TransportKind.stdio, - options: { - cwd: workspacePath, - env: sqlmesh.value.env, - }, + options: { cwd: workspacePath, env: sqlmesh.value.env }, args: sqlmesh.value.args, }, } const clientOptions: LanguageClientOptions = { documentSelector: [ - { scheme: 'file', pattern: `**/*.sql` }, - { - scheme: 'file', - pattern: '**/external_models.yaml', - }, - { - scheme: 'file', - pattern: '**/external_models.yml', - }, + { scheme: 'file', pattern: '**/*.sql' }, + { scheme: 'file', pattern: '**/external_models.yaml' }, + { scheme: 'file', pattern: '**/external_models.yml' }, ], diagnosticCollectionName: 'sqlmesh', - outputChannel: outputChannel, + outputChannel, } traceInfo( - `Starting SQLMesh Language Server with workspace path: ${workspacePath} with server options ${JSON.stringify(serverOptions)} and client options ${JSON.stringify(clientOptions)}`, + `Starting SQLMesh LSP (cwd=${workspacePath})\n` + + ` serverOptions=${JSON.stringify(serverOptions)}\n` + + ` clientOptions=${JSON.stringify(clientOptions)}`, ) + this.client = new LanguageClient( 'sqlmesh-lsp', 'SQLMesh Language Server', serverOptions, clientOptions, ) + this.explicitlyStopped = false // user wanted it running again await this.client.start() return ok(undefined) } + /** Restart = stop + start. */ public async restart( - overrideByUser = false, + overrideStoppedByUser = false, ): Promise> { - await this.stop() - return await this.start(overrideByUser) + await this.stop() // this also disposes + return this.start(overrideStoppedByUser) } + /** + * Stop the client (if running) and clean up all VS Code resources so that a + * future `start()` registers its commands without collisions. + */ public async stop(stoppedByUser = false): Promise { if (this.client) { - await this.client.stop() + // Shut down the JSON‑RPC connection + await this.client + .stop() + .catch(err => traceError(`Error while stopping LSP: ${err}`)) + + // Unregister commands, code lenses, etc. + await this.client.dispose() + this.client = undefined - // Reset supported methods state when the client stops this.supportedMethodsState = { type: 'not-fetched' } + traceInfo('SQLMesh LSP client disposed.') } + if (stoppedByUser) { this.explicitlyStopped = true traceInfo('SQLMesh LSP client stopped by user.') } } - public async dispose() { + public async dispose(): Promise { await this.stop() } private async fetchSupportedMethods(): Promise { - if (!this.client || this.supportedMethodsState.type !== 'not-fetched') { + if (!this.client || this.supportedMethodsState.type !== 'not-fetched') return - } - try { - const result = await this.internal_call_custom_method( - 'sqlmesh/supported_methods', - {}, - ) - if (isErr(result)) { - traceError(`Failed to fetch supported methods: ${result.error}`) - this.supportedMethodsState = { type: 'endpoint-not-supported' } - return - } - const methodNames = new Set(result.value.methods.map(m => m.name)) - this.supportedMethodsState = { type: 'fetched', methods: methodNames } - traceInfo( - `Fetched supported methods: ${Array.from(methodNames).join(', ')}`, - ) - } catch { - // If the supported_methods endpoint doesn't exist, mark it as not supported + + const result = await this.internal_call_custom_method( + 'sqlmesh/supported_methods', + {}, + ) + if (isErr(result)) { + traceError(`Failed to fetch supported methods: ${result.error}`) this.supportedMethodsState = { type: 'endpoint-not-supported' } - traceInfo( - 'Supported methods endpoint not available, proceeding without validation', - ) + return } + + const methodNames = new Set(result.value.methods.map(m => m.name)) + this.supportedMethodsState = { type: 'fetched', methods: methodNames } + traceInfo(`Fetched supported methods: ${[...methodNames].join(', ')}`) } public async call_custom_method< @@ -208,79 +209,49 @@ export class LSPClient implements Disposable { > > { if (!this.client) { - return err({ - type: 'generic', - message: 'LSP client not ready.', - }) + return err({ type: 'generic', message: 'LSP client not ready.' }) } + await this.fetchSupportedMethods() const supportedState = this.supportedMethodsState - switch (supportedState.type) { - case 'not-fetched': - return err({ - type: 'invalid_state', - message: 'Supported methods not fetched yet whereas they should.', - }) - case 'fetched': { - // If we have fetched the supported methods, we can check if the method is supported - if (!supportedState.methods.has(method)) { - return err({ - type: 'sqlmesh_outdated', - message: `Method '${method}' is not supported by this LSP server.`, - }) - } - const response = await this.internal_call_custom_method( - method, - request as any, - ) - if (isErr(response)) { - return err({ - type: 'generic', - message: response.error, - }) - } - return ok(response.value as Response) - } - case 'endpoint-not-supported': { - const response = await this.internal_call_custom_method( - method, - request as any, - ) - if (isErr(response)) { - return err({ - type: 'generic', - message: response.error, - }) - } - return ok(response.value as Response) - } + if ( + supportedState.type === 'fetched' && + !supportedState.methods.has(method) + ) { + return err({ + type: 'sqlmesh_outdated', + message: `Method '${method}' is not supported by this LSP server.`, + }) } + + const response = await this.internal_call_custom_method( + method, + request as any, + ) + if (isErr(response)) { + return err({ type: 'generic', message: response.error }) + } + return ok(response.value as Response) } /** - * Internal method to call a custom LSP method without checking if the method is supported. It is used for - * the class whereas the `call_custom_method` checks if the method is supported. + * Low‑level helper that sends a raw JSON‑RPC request without any feature checks. */ public async internal_call_custom_method< Method extends CustomLSPMethods['method'], Request extends Extract['request'], Response extends Extract['response'], >(method: Method, request: Request): Promise> { - if (!this.client) { - return err('lsp client not ready') - } + if (!this.client) return err('lsp client not ready') try { const result = await this.client.sendRequest(method, request) - if (result.response_error) { - return err(result.response_error) - } + if ((result as any).response_error) + return err((result as any).response_error) return ok(result) } catch (error) { - traceError( - `lsp '${method}' request ${JSON.stringify(request)} failed: ${JSON.stringify(error)}`, - ) + traceError(`LSP '${method}' request failed: ${JSON.stringify(error)}`) return err(JSON.stringify(error)) } } From 583552e3dbc978d45091dc6f7cb1338e3150edc2 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:35:30 +0100 Subject: [PATCH 0636/1056] feat(vscode): adding ability to update columns (#5013) --- pnpm-lock.yaml | 61 +++++++++++++ sqlmesh/core/schema_loader.py | 32 ++++--- sqlmesh/lsp/commands.py | 1 + sqlmesh/lsp/context.py | 110 +++++++++++++++++++++++- sqlmesh/lsp/main.py | 50 +++++++++++ sqlmesh/utils/lineage.py | 28 +++++- vscode/extension/package.json | 1 + vscode/extension/tests/commands.spec.ts | 96 +++++++++++++++++++++ 8 files changed, 360 insertions(+), 19 deletions(-) create mode 100644 sqlmesh/lsp/commands.py create mode 100644 vscode/extension/tests/commands.spec.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a94b230fc1..3d0a5449f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: vscode/extension: dependencies: + '@duckdb/node-api': + specifier: 1.3.2-alpha.25 + version: 1.3.2-alpha.25 '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 @@ -692,6 +695,37 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@duckdb/node-api@1.3.2-alpha.25': + resolution: {integrity: sha512-AzDyyjTtnYUxoy/MHDFRwfOggDOkS8RBgGA82OI6nla8B9NDNZeAYJ97T3PvCL8cx7y00EtGVN3g03aoW4fRmw==} + + '@duckdb/node-bindings-darwin-arm64@1.3.2-alpha.25': + resolution: {integrity: sha512-vRjzNgkz2TAYW5c2rzPwcHBctBWr0lxQ4blFASAv0DdeGPOeuCMXJUA3982X7iPNwAppH0VMII6cYzON0GA+RA==} + cpu: [arm64] + os: [darwin] + + '@duckdb/node-bindings-darwin-x64@1.3.2-alpha.25': + resolution: {integrity: sha512-BSg/DZjT25QZe87+pmdMfE1XlHdi2WxtAO+F2PEXN6VnPeLyTdl5bYlnhOGrDKquKDmUEqok5OwF7mR4QfU+Aw==} + cpu: [x64] + os: [darwin] + + '@duckdb/node-bindings-linux-arm64@1.3.2-alpha.25': + resolution: {integrity: sha512-VhjUH/AvolZWDX/URqiIh58JbAB1vYbDgSmQ0wvqhS9jzJ9Sj88urGDw+XWXw49Rr4BhIgDtX70SoARhO2i/Gg==} + cpu: [arm64] + os: [linux] + + '@duckdb/node-bindings-linux-x64@1.3.2-alpha.25': + resolution: {integrity: sha512-raav2ypBiV4TlpnKU9hocsuFDO4ipwIcQQmkMIh20/Qd9vkv35QcQYNqStiZVJh2LAaVoQffNvcKMlclblYqUQ==} + cpu: [x64] + os: [linux] + + '@duckdb/node-bindings-win32-x64@1.3.2-alpha.25': + resolution: {integrity: sha512-/fAKax+xYkdRhkUl3PkL3HfFd1ZsezG1yiOkL0StHBdD3xB80Njm1JGHxx1fO3WWE5XTbE1MTJ5I0xjEzPwsfQ==} + cpu: [x64] + os: [win32] + + '@duckdb/node-bindings@1.3.2-alpha.25': + resolution: {integrity: sha512-FkoSaoeRAi6Em0hs0qzr3SN04ykN99R+Qap5kLwhi6GNPnHzWMU1VrNpK9cE4eBj0n+RWlNK0TiO712dn44QzQ==} + '@esbuild/aix-ppc64@0.25.8': resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} engines: {node: '>=18'} @@ -6674,6 +6708,33 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@duckdb/node-api@1.3.2-alpha.25': + dependencies: + '@duckdb/node-bindings': 1.3.2-alpha.25 + + '@duckdb/node-bindings-darwin-arm64@1.3.2-alpha.25': + optional: true + + '@duckdb/node-bindings-darwin-x64@1.3.2-alpha.25': + optional: true + + '@duckdb/node-bindings-linux-arm64@1.3.2-alpha.25': + optional: true + + '@duckdb/node-bindings-linux-x64@1.3.2-alpha.25': + optional: true + + '@duckdb/node-bindings-win32-x64@1.3.2-alpha.25': + optional: true + + '@duckdb/node-bindings@1.3.2-alpha.25': + optionalDependencies: + '@duckdb/node-bindings-darwin-arm64': 1.3.2-alpha.25 + '@duckdb/node-bindings-darwin-x64': 1.3.2-alpha.25 + '@duckdb/node-bindings-linux-arm64': 1.3.2-alpha.25 + '@duckdb/node-bindings-linux-x64': 1.3.2-alpha.25 + '@duckdb/node-bindings-win32-x64': 1.3.2-alpha.25 + '@esbuild/aix-ppc64@0.25.8': optional: true diff --git a/sqlmesh/core/schema_loader.py b/sqlmesh/core/schema_loader.py index 8df5164a8a..52ab807c78 100644 --- a/sqlmesh/core/schema_loader.py +++ b/sqlmesh/core/schema_loader.py @@ -57,28 +57,17 @@ def create_external_models_file( external_model_fqns -= existing_model_fqns with ThreadPoolExecutor(max_workers=max_workers) as pool: - - def _get_columns(table: str) -> t.Optional[t.Dict[str, t.Any]]: - try: - return adapter.columns(table, include_pseudo_columns=True) - except Exception as e: - msg = f"Unable to get schema for '{table}': '{e}'." - if strict: - raise SQLMeshError(msg) from e - get_console().log_warning(msg) - return None - gateway_part = {"gateway": gateway} if gateway else {} schemas = [ { "name": exp.to_table(table).sql(dialect=dialect), - "columns": {c: dtype.sql(dialect=dialect) for c, dtype in columns.items()}, + "columns": columns, **gateway_part, } for table, columns in sorted( pool.map( - lambda table: (table, _get_columns(table)), + lambda table: (table, get_columns(adapter, dialect, table, strict)), external_model_fqns, ) ) @@ -94,3 +83,20 @@ def _get_columns(table: str) -> t.Optional[t.Dict[str, t.Any]]: with open(path, "w", encoding="utf-8") as file: yaml.dump(entries_to_keep + schemas, file) + + +def get_columns( + adapter: EngineAdapter, dialect: DialectType, table: str, strict: bool +) -> t.Optional[t.Dict[str, t.Any]]: + """ + Return the column and their types in a dictionary + """ + try: + columns = adapter.columns(table, include_pseudo_columns=True) + return {c: dtype.sql(dialect=dialect) for c, dtype in columns.items()} + except Exception as e: + msg = f"Unable to get schema for '{table}': '{e}'." + if strict: + raise SQLMeshError(msg) from e + get_console().log_warning(msg) + return None diff --git a/sqlmesh/lsp/commands.py b/sqlmesh/lsp/commands.py new file mode 100644 index 0000000000..bea81f898a --- /dev/null +++ b/sqlmesh/lsp/commands.py @@ -0,0 +1 @@ +EXTERNAL_MODEL_UPDATE_COLUMNS = "sqlmesh.external_model_update_columns" diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 43eb9c8f16..c3026da2e6 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -1,16 +1,21 @@ from dataclasses import dataclass from pathlib import Path +from pygls.server import LanguageServer from sqlmesh.core.context import Context import typing as t - from sqlmesh.core.linter.rule import Range -from sqlmesh.core.model.definition import SqlModel +from sqlmesh.core.model.definition import SqlModel, ExternalModel from sqlmesh.core.linter.definition import AnnotatedRuleViolation +from sqlmesh.core.schema_loader import get_columns +from sqlmesh.lsp.commands import EXTERNAL_MODEL_UPDATE_COLUMNS from sqlmesh.lsp.custom import ModelForRendering, TestEntry, RunTestResponse from sqlmesh.lsp.custom import AllModelsResponse, RenderModelEntry from sqlmesh.lsp.tests_ranges import get_test_ranges +from sqlmesh.lsp.helpers import to_lsp_range from sqlmesh.lsp.uri import URI from lsprotocol import types +from sqlmesh.utils import yaml +from sqlmesh.utils.lineage import get_yaml_model_name_ranges @dataclass @@ -298,6 +303,36 @@ def get_code_actions( return code_actions if code_actions else None + def get_code_lenses(self, uri: URI) -> t.Optional[t.List[types.CodeLens]]: + models_in_file = self.map.get(uri.to_path()) + if isinstance(models_in_file, ModelTarget): + models = [self.context.get_model(model) for model in models_in_file.names] + if any(isinstance(model, ExternalModel) for model in models): + code_lenses = self._get_external_model_code_lenses(uri) + if code_lenses: + return code_lenses + + return None + + def _get_external_model_code_lenses(self, uri: URI) -> t.List[types.CodeLens]: + """Get code lenses for external models YAML files.""" + ranges = get_yaml_model_name_ranges(uri.to_path()) + if ranges is None: + return [] + return [ + types.CodeLens( + range=to_lsp_range(range), + command=types.Command( + title="Update Columns", + command=EXTERNAL_MODEL_UPDATE_COLUMNS, + arguments=[ + name, + ], + ), + ) + for name, range in ranges.items() + ] + def list_of_models_for_rendering(self) -> t.List[ModelForRendering]: """Get a list of models for rendering. @@ -399,3 +434,74 @@ def diagnostic_to_lsp_diagnostic( code=diagnostic.rule.name, code_description=types.CodeDescription(href=rule_uri), ) + + def update_external_model_columns(self, ls: LanguageServer, uri: URI, model_name: str) -> bool: + """ + Update the columns for an external model in the YAML file. Returns True if changed, False if didn't because + of the columns already being up to date. + + In this case, the model name is the name of the external model as is defined in the YAML file, not any other version of it. + + Errors still throw exceptions to be handled by the caller. + """ + models = yaml.load(uri.to_path()) + if not isinstance(models, list): + raise ValueError( + f"Expected a list of models in {uri.to_path()}, but got {type(models).__name__}" + ) + + existing_model = next((model for model in models if model.get("name") == model_name), None) + if existing_model is None: + raise ValueError(f"Could not find model {model_name} in {uri.to_path()}") + + existing_model_columns = existing_model.get("columns") + + # Get the adapter and fetch columns + adapter = self.context.engine_adapter + # Get columns for the model + new_columns = get_columns( + adapter=adapter, + dialect=self.context.config.model_defaults.dialect, + table=model_name, + strict=True, + ) + # Compare existing columns and matching types and if they are the same, do not update + if existing_model_columns is not None: + if existing_model_columns == new_columns: + return False + + # Model index to update + model_index = next( + (i for i, model in enumerate(models) if model.get("name") == model_name), None + ) + if model_index is None: + raise ValueError(f"Could not find model {model_name} in {uri.to_path()}") + + # Get end of the file to set the edit range + with open(uri.to_path(), "r", encoding="utf-8") as file: + read_file = file.read() + + end_line = read_file.count("\n") + end_character = len(read_file.splitlines()[-1]) if end_line > 0 else 0 + + models[model_index]["columns"] = new_columns + edit = types.TextDocumentEdit( + text_document=types.OptionalVersionedTextDocumentIdentifier( + uri=uri.value, + version=None, + ), + edits=[ + types.TextEdit( + range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position( + line=end_line, + character=end_character, + ), + ), + new_text=yaml.dump(models), + ) + ], + ) + ls.apply_edit(types.WorkspaceEdit(document_changes=[edit])) + return True diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 1f53e71861..13e4c5d8f0 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -24,6 +24,7 @@ ApiResponseGetModels, ) +from sqlmesh.lsp.commands import EXTERNAL_MODEL_UPDATE_COLUMNS from sqlmesh.lsp.completions import get_sql_completions from sqlmesh.lsp.context import ( LSPContext, @@ -368,6 +369,44 @@ def function_call(ls: LanguageServer, params: t.Any) -> t.Dict[str, t.Any]: self.server.feature(name)(create_function_call(method)) + @self.server.command(EXTERNAL_MODEL_UPDATE_COLUMNS) + def command_external_models_update_columns(ls: LanguageServer, raw: t.Any) -> None: + try: + if not isinstance(raw, list): + raise ValueError("Invalid command parameters") + if len(raw) != 1: + raise ValueError("Command expects exactly one parameter") + model_name = raw[0] + if not isinstance(model_name, str): + raise ValueError("Command parameter must be a string") + + context = self._context_get_or_load() + if not isinstance(context, LSPContext): + raise ValueError("Context is not loaded or invalid") + model = context.context.get_model(model_name) + if model is None: + raise ValueError(f"External model '{model_name}' not found") + if model._path is None: + raise ValueError(f"External model '{model_name}' does not have a file path") + uri = URI.from_path(model._path) + updated = context.update_external_model_columns( + ls=ls, + uri=uri, + model_name=model_name, + ) + if updated: + ls.show_message( + f"Updated columns for '{model_name}'", + types.MessageType.Info, + ) + else: + ls.show_message( + f"Columns for '{model_name}' are already up to date", + ) + except Exception as e: + ls.show_message(f"Error executing command: {e}", types.MessageType.Error) + return None + @self.server.feature(types.INITIALIZE) def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: """Initialize the server when the client connects.""" @@ -750,6 +789,17 @@ def code_action( ls.log_trace(f"Error getting code actions: {e}") return None + @self.server.feature(types.TEXT_DOCUMENT_CODE_LENS) + def code_lens(ls: LanguageServer, params: types.CodeLensParams) -> t.List[types.CodeLens]: + try: + uri = URI(params.text_document.uri) + context = self._context_get_or_load(uri) + code_lenses = context.get_code_lenses(uri) + return code_lenses if code_lenses else [] + except Exception as e: + ls.log_trace(f"Error getting code lenses: {e}") + return [] + @self.server.feature( types.TEXT_DOCUMENT_COMPLETION, types.CompletionOptions(trigger_characters=["@"]), # advertise "@" for macros diff --git a/sqlmesh/utils/lineage.py b/sqlmesh/utils/lineage.py index 8fcb92f56b..f5b4506c68 100644 --- a/sqlmesh/utils/lineage.py +++ b/sqlmesh/utils/lineage.py @@ -387,6 +387,22 @@ def _get_yaml_model_range(path: Path, model_name: str) -> t.Optional[Range]: Returns: The Range of the model block in the YAML file, or None if not found """ + model_name_ranges = get_yaml_model_name_ranges(path) + if model_name_ranges is None: + return None + return model_name_ranges.get(model_name, None) + + +def get_yaml_model_name_ranges(path: Path) -> t.Optional[t.Dict[str, Range]]: + """ + Get the ranges of all model names in a YAML file. + + Args: + path: Path to the YAML file + + Returns: + A dictionary mapping model names to their ranges in the YAML file. + """ yaml = YAML() with path.open("r", encoding="utf-8") as f: data = yaml.load(f) @@ -394,11 +410,15 @@ def _get_yaml_model_range(path: Path, model_name: str) -> t.Optional[Range]: if not isinstance(data, list): return None + model_name_ranges = {} for item in data: - if isinstance(item, dict) and item.get("name") == model_name: - # Get size of block by taking the earliest line/col in the items block and the last line/col of the block + if isinstance(item, dict): position_data = item.lc.data["name"] # type: ignore start = Position(line=position_data[2], character=position_data[3]) end = Position(line=position_data[2], character=position_data[3] + len(item["name"])) - return Range(start=start, end=end) - return None + name = item.get("name") + if not name: + continue + model_name_ranges[name] = Range(start=start, end=end) + + return model_name_ranges diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 1db45abcf4..a0d853a0a9 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -134,6 +134,7 @@ "package": "rm -rf ./src_react && mkdir -p ./src_react && cd ../react && pnpm run build && cd ../extension && cp -r ../react/dist/* ./src_react && pnpm run check-types && node esbuild.js --production" }, "dependencies": { + "@duckdb/node-api": "1.3.2-alpha.25", "@types/fs-extra": "^11.0.4", "@vscode/python-extension": "^1.0.5", "fs-extra": "^11.3.0", diff --git a/vscode/extension/tests/commands.spec.ts b/vscode/extension/tests/commands.spec.ts new file mode 100644 index 0000000000..afd926310c --- /dev/null +++ b/vscode/extension/tests/commands.spec.ts @@ -0,0 +1,96 @@ +import { test, expect } from './fixtures' +import path from 'path' +import fs from 'fs-extra' +import os from 'os' +import { + openServerPage, + saveFile, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' +import { DuckDBInstance } from '@duckdb/node-api' + +test.describe('Update external models columns', () => { + test('New external model', async ({ page, sharedCodeServer }) => { + // Normal setting up + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-sushi-'), + ) + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + // Changing the config to set the default gateway to use the fixed one. + const configPath = path.join(tempDir, 'config.py') + const configContent = await fs.readFile(configPath, 'utf8') + const original = `default_gateway="duckdb",` + expect(configContent).toContain(original) + const target = `default_gateway="duckdb_persistent",` + const updatedConfigContent = configContent.replace(original, target) + expect(updatedConfigContent).toContain(target) + await fs.writeFile(configPath, updatedConfigContent) + + // Create an additional table in the database + const table = 'raw.test_table' + const databasePath = path.join(tempDir, 'data', 'duckdb.db') + const instance = await DuckDBInstance.create(databasePath) + const connection = await instance.connect() + await connection.run(`CREATE SCHEMA IF NOT EXISTS raw`) + await connection.run( + `CREATE TABLE IF NOT EXISTS ${table}( + id INTEGER, + value VARCHAR + )`, + ) + connection.closeSync() + instance.closeSync() + expect(fs.existsSync(databasePath)).toBe(true) + + // Update the external_models in the config to include the new table but + // not the columns by appending '- name: ${table}' to the external_models.yaml file + const externalModelsPath = path.join(tempDir, 'external_models.yaml') + const externalModelsContent = await fs.readFile(externalModelsPath, 'utf8') + const newExternalModel = `- name: ${table}` + const updatedExternalModelsContent = `${externalModelsContent}\n${newExternalModel}` + await fs.writeFile(externalModelsPath, updatedExternalModelsContent) + + // Open the server page + await openServerPage(page, tempDir, sharedCodeServer) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'external_models.yaml', exact: true }) + .locator('a') + .click() + + await waitForLoadedSQLMesh(page) + + // Click the update columns button + await page.waitForSelector('text=Update Columns') + const updateColumnButtons = page.getByRole('button', { + name: 'Update Columns', + exact: true, + }) + // Click each one of them + for (const button of await updateColumnButtons.all()) { + await button.click() + await page.waitForTimeout(1_000) // Wait for the action to complete + } + + await page.waitForTimeout(1_000) + await saveFile(page) + await page.waitForTimeout(1_000) + + // Check the file contains the columns + const updatedExternalModelsContentAfterUpdate = await fs.readFile( + externalModelsPath, + 'utf8', + ) + expect(updatedExternalModelsContentAfterUpdate).toContain( + `- name: ${table}\n columns:\n id: INT\n value: TEXT`, + ) + }) +}) From fa293da049a832c7def1022a5307eb89ffb78bf7 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 29 Jul 2025 18:50:25 +0200 Subject: [PATCH 0637/1056] Chore: Print a deprecation warning in the sqlmesh ui command (#5046) --- docs/guides/ui.md | 5 +++++ docs/quickstart/ui.md | 4 ++++ sqlmesh/cli/main.py | 23 +++++++++++++++-------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/guides/ui.md b/docs/guides/ui.md index 1da48e50b3..29bb204988 100644 --- a/docs/guides/ui.md +++ b/docs/guides/ui.md @@ -1,5 +1,10 @@ # Browser UI guide +!!! warning + + Browser UI is deprecated. Please use the [VSCode extension](vscode.md) instead. + + SQLMesh's free, open-source browser user interface (UI) makes it easy to understand, explore, and modify your SQLMesh project. This page describes the UI's components and how they work. diff --git a/docs/quickstart/ui.md b/docs/quickstart/ui.md index 06a9d8c448..2891536876 100644 --- a/docs/quickstart/ui.md +++ b/docs/quickstart/ui.md @@ -1,5 +1,9 @@ # Browser UI +!!! warning + + Browser UI is deprecated. Please use the [VSCode extension](../guides/vscode.md) instead. + In this quickstart, you'll use the SQLMesh browser user interface to get up and running with SQLMesh's scaffold generator. This example project will run locally on your computer using [DuckDB](https://duckdb.org/) as an embedded SQL engine. ??? info "Learn more about the quickstart project structure" diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index e4b06552ad..8982efc9f8 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -107,6 +107,14 @@ def cli( if "--help" in sys.argv: return + configure_logging( + debug, + log_to_stdout, + log_file_dir=log_file_dir, + ignore_warnings=ignore_warnings, + ) + configure_console(ignore_warnings=ignore_warnings) + load = True if len(paths) == 1: @@ -117,14 +125,6 @@ def cli( if ctx.invoked_subcommand in SKIP_LOAD_COMMANDS: load = False - configure_logging( - debug, - log_to_stdout, - log_file_dir=log_file_dir, - ignore_warnings=ignore_warnings, - ) - configure_console(ignore_warnings=ignore_warnings) - configs = load_configs(config, Context.CONFIG_TYPE, paths, dotenv_path=dotenv) log_limit = list(configs.values())[0].log_limit @@ -884,6 +884,13 @@ def info(obj: Context, skip_connection: bool, verbose: int) -> None: @cli_analytics def ui(ctx: click.Context, host: str, port: int, mode: str) -> None: """Start a browser-based SQLMesh UI.""" + from sqlmesh.core.console import get_console + + get_console().log_warning( + "The UI is deprecated and will be removed in a future version. Please use the SQLMesh VSCode extension instead. " + "Learn more at https://sqlmesh.readthedocs.io/en/stable/guides/vscode/" + ) + try: import uvicorn except ModuleNotFoundError as e: From b068c695a41822ec1ad3863bb21a714b6ad4de49 Mon Sep 17 00:00:00 2001 From: Grant Murray Date: Tue, 29 Jul 2025 12:53:08 -0400 Subject: [PATCH 0638/1056] chore(docs): fix-broken-link-to-example-file (#5048) --- docs/examples/incremental_time_full_walkthrough.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/incremental_time_full_walkthrough.md b/docs/examples/incremental_time_full_walkthrough.md index 7941647f68..6907836b0b 100644 --- a/docs/examples/incremental_time_full_walkthrough.md +++ b/docs/examples/incremental_time_full_walkthrough.md @@ -172,7 +172,7 @@ We have data like the below that gets ingested into our data warehouse on a dail I can answer some of the questions above by walking through the model's config, coupled with the business logic/code I prepared ahead of time. -You can see this code in a SQLMesh project context [here](https://github.com/sungchun12/sqlmesh-demos/blob/incremental-demo/models/examples/incrementals_demo.sql). +You can see this code in a SQLMesh project context [here](https://github.com/sungchun12/sqlmesh-demos/blob/incremental-demo/models/examples/incremental_model.sql). ```sql MODEL ( From e32fa9eb74c750237915dc03c0fa4a3183944310 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 29 Jul 2025 19:54:23 +0300 Subject: [PATCH 0639/1056] Chore: ensure ducklake testing artifact is under a tmp path (#5059) --- tests/core/engine_adapter/test_duckdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/engine_adapter/test_duckdb.py b/tests/core/engine_adapter/test_duckdb.py index 6442b1a0b4..7799cefe0c 100644 --- a/tests/core/engine_adapter/test_duckdb.py +++ b/tests/core/engine_adapter/test_duckdb.py @@ -114,7 +114,7 @@ def test_ducklake_partitioning(adapter: EngineAdapter, duck_conn, tmp_path): duck_conn.install_extension("ducklake") duck_conn.load_extension("ducklake") duck_conn.execute( - f"ATTACH 'ducklake:{catalog}.ducklake' AS {catalog} (DATA_PATH '{tmp_path}');" + f"ATTACH 'ducklake:{tmp_path}/{catalog}.ducklake' AS {catalog} (DATA_PATH '{tmp_path}');" ) # no partitions on catalog creation From 5ccd57eb91d14cb708467d6c6603ae8472bb8b68 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 29 Jul 2025 18:09:59 +0100 Subject: [PATCH 0640/1056] feat!: allow the linter to return fixes that affect other files (#5057) --- sqlmesh/core/linter/rule.py | 2 ++ sqlmesh/core/linter/rules/builtin.py | 4 ++++ sqlmesh/lsp/context.py | 10 +++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/linter/rule.py b/sqlmesh/core/linter/rule.py index 6e63dd2ee6..ec942928e7 100644 --- a/sqlmesh/core/linter/rule.py +++ b/sqlmesh/core/linter/rule.py @@ -2,6 +2,7 @@ import abc from dataclasses import dataclass +from pathlib import Path from sqlmesh.core.model import Model @@ -43,6 +44,7 @@ class Range: class TextEdit: """A text edit to apply to a file.""" + path: Path range: Range new_text: str diff --git a/sqlmesh/core/linter/rules/builtin.py b/sqlmesh/core/linter/rules/builtin.py index c1a5f9b877..a166b5e1f3 100644 --- a/sqlmesh/core/linter/rules/builtin.py +++ b/sqlmesh/core/linter/rules/builtin.py @@ -53,12 +53,16 @@ def _create_fixes( columns = model.columns_to_types if not columns: return None + path = model._path + if path is None: + return None new_text = ", ".join(columns.keys()) return [ Fix( title="Replace SELECT * with explicit column list", edits=[ TextEdit( + path=path, range=violation_range, new_text=new_text, ) diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index c3026da2e6..52b33453b2 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -274,9 +274,13 @@ def get_code_actions( # Create code actions for each fix for fix in found_violation.fixes: # Convert our Fix to LSP TextEdits - text_edits = [] + changes: t.Dict[str, t.List[types.TextEdit]] = {} for edit in fix.edits: - text_edits.append( + uri_key = URI.from_path(edit.path).value + if uri_key not in changes: + changes[uri_key] = [] + # Create a TextEdit for the LSP + changes[uri_key].append( types.TextEdit( range=types.Range( start=types.Position( @@ -297,7 +301,7 @@ def get_code_actions( title=fix.title, kind=types.CodeActionKind.QuickFix, diagnostics=[diagnostic], - edit=types.WorkspaceEdit(changes={params.text_document.uri: text_edits}), + edit=types.WorkspaceEdit(changes=changes), ) code_actions.append(code_action) From 854379dc6a2443a1e49dd6ab4058fd55aa28b032 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 30 Jul 2025 11:07:18 +1200 Subject: [PATCH 0641/1056] Chore: Pin dateparser version (#5063) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 42d40a5f8a..670c021f56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "click", "croniter", "duckdb>=0.10.0,!=0.10.3", - "dateparser", + "dateparser<=1.2.1", "hyperscript>=0.1.0", "importlib-metadata; python_version<'3.12'", "ipywidgets", From 036b75eb31d7f6e43322a68583cb9d690ec0156c Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Jul 2025 00:26:25 +0100 Subject: [PATCH 0642/1056] feat: allow empty external models file (#5061) --- sqlmesh/core/loader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index 30c74884c8..edea485d16 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -334,6 +334,10 @@ def _load_external_models( def _load(path: Path) -> t.List[Model]: try: with open(path, "r", encoding="utf-8") as file: + yaml = YAML().load(file) + # Allow empty YAML files to return an empty list + if yaml is None: + return [] return [ create_external_model( defaults=self.config.model_defaults.dict(), @@ -346,7 +350,7 @@ def _load(path: Path) -> t.List[Model]: **row, }, ) - for row in YAML().load(file.read()) + for row in yaml ] except Exception as ex: raise ConfigError(self._failed_to_load_model_error(path, ex), path) From 286305ffb07dce3aa0cd74bb558886bd98602795 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 30 Jul 2025 11:45:26 +1200 Subject: [PATCH 0643/1056] Fix: table_naming_convention not passed in all cases (#5062) --- sqlmesh/core/snapshot/definition.py | 1 + tests/core/test_snapshot.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 1331dd72f7..941ef6aae7 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -527,6 +527,7 @@ def data_version(self) -> SnapshotDataVersion: change_category=self.change_category, physical_schema=self.physical_schema, dev_table_suffix=self.dev_table_suffix, + table_naming_convention=self.table_naming_convention, ) @property diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index cab5e17fff..d71cbc4db6 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -1250,6 +1250,26 @@ def test_table_name_naming_convention_hash_md5(make_snapshot: t.Callable[..., Sn assert snapshot.table_name(is_deployable=False) == f"sqlmesh__foo.sqlmesh_md5__{hash_dev}__dev" +def test_table_naming_convention_passed_around_correctly(make_snapshot: t.Callable[..., Snapshot]): + snapshot = make_snapshot( + SqlModel(name='"foo"."bar"."baz"', query=parse_one("select 1")), + table_naming_convention=TableNamingConvention.HASH_MD5, + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + assert snapshot.table_naming_convention == TableNamingConvention.HASH_MD5 + assert snapshot.data_version.table_naming_convention == TableNamingConvention.HASH_MD5 + assert snapshot.table_info.table_naming_convention == TableNamingConvention.HASH_MD5 + assert ( + snapshot.table_info.data_version.table_naming_convention == TableNamingConvention.HASH_MD5 + ) + assert snapshot.table_info.table_info.table_naming_convention == TableNamingConvention.HASH_MD5 + assert ( + snapshot.table_info.table_info.data_version.table_naming_convention + == TableNamingConvention.HASH_MD5 + ) + + def test_table_name_view(make_snapshot: t.Callable): # Mimic a direct breaking change. snapshot = make_snapshot(SqlModel(name="name", query=parse_one("select 1"), kind="VIEW")) From cedaf8860025d580defa4104c0644d999859cd34 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Jul 2025 00:51:23 +0100 Subject: [PATCH 0644/1056] feat: no missing external model provides fix (#5058) --- sqlmesh/core/linter/rules/builtin.py | 46 ++++++++- tests/core/linter/test_builtin.py | 121 +++++++++++++++++++++++- vscode/extension/tests/quickfix.spec.ts | 119 +++++++++++------------ 3 files changed, 225 insertions(+), 61 deletions(-) diff --git a/sqlmesh/core/linter/rules/builtin.py b/sqlmesh/core/linter/rules/builtin.py index a166b5e1f3..1a96a4fcec 100644 --- a/sqlmesh/core/linter/rules/builtin.py +++ b/sqlmesh/core/linter/rules/builtin.py @@ -7,13 +7,14 @@ from sqlglot.expressions import Star from sqlglot.helper import subclasses +from sqlmesh.core.constants import EXTERNAL_MODELS_YAML from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core.linter.helpers import ( TokenPositionDetails, get_range_of_model_block, read_range_from_string, ) -from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit +from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit, Position from sqlmesh.core.linter.definition import RuleSet from sqlmesh.core.model import Model, SqlModel, ExternalModel from sqlmesh.utils.lineage import extract_references_from_query, ExternalModelReference @@ -185,12 +186,14 @@ def check_model( violations = [] for ref_name, ref in external_references.items(): if ref_name in not_registered_external_models: + fix = self.create_fix(ref_name) violations.append( RuleViolation( rule=self, violation_msg=f"Model '{model.fqn}' depends on unregistered external model '{ref_name}'. " "Please register it in the external models file. This can be done by running 'sqlmesh create_external_models'.", violation_range=ref.range, + fixes=[fix] if fix else [], ) ) @@ -212,5 +215,46 @@ def _standard_error_message( "Please register them in the external models file. This can be done by running 'sqlmesh create_external_models'.", ) + def create_fix(self, model_name: str) -> t.Optional[Fix]: + """ + Add an external model to the external models file. + - If no external models file exists, it will create one with the model. + - If the model already exists, it will not add it again. + """ + root = self.context.path + if not root: + return None + + external_models_path = root / EXTERNAL_MODELS_YAML + if not external_models_path.exists(): + return None + + # Figure out the position to insert the new external model at the end of the file, whether + # needs new line or not. + with open(external_models_path, "r", encoding="utf-8") as file: + lines = file.read() + + # If a file ends in newline, we can add the new model directly. + split_lines = lines.splitlines() + if lines.endswith("\n"): + new_text = f"- name: '{model_name}'\n" + position = Position(line=len(split_lines), character=0) + else: + new_text = f"\n- name: '{model_name}'\n" + position = Position( + line=len(split_lines) - 1, character=len(split_lines[-1]) if split_lines else 0 + ) + + return Fix( + title="Add external model", + edits=[ + TextEdit( + path=external_models_path, + range=Range(start=position, end=position), + new_text=new_text, + ) + ], + ) + BUILTIN_RULES = RuleSet(subclasses(__name__, Rule, (Rule,))) diff --git a/tests/core/linter/test_builtin.py b/tests/core/linter/test_builtin.py index b9cf759946..a5a73fcf87 100644 --- a/tests/core/linter/test_builtin.py +++ b/tests/core/linter/test_builtin.py @@ -1,6 +1,7 @@ import os from sqlmesh import Context +from sqlmesh.core.linter.rule import Position, Range def test_no_missing_external_models(tmp_path, copy_to_temp_path) -> None: @@ -44,8 +45,124 @@ def test_no_missing_external_models(tmp_path, copy_to_temp_path) -> None: # Lint the models lints = context.lint_models(raise_on_error=False) assert len(lints) == 1 - assert lints[0].violation_range is not None + lint = lints[0] + assert lint.violation_range is not None assert ( - lints[0].violation_msg + lint.violation_msg == """Model '"memory"."sushi"."customers"' depends on unregistered external model '"memory"."raw"."demographics"'. Please register it in the external models file. This can be done by running 'sqlmesh create_external_models'.""" ) + assert len(lint.fixes) == 0 + + +def test_no_missing_external_models_with_existing_file_ending_in_newline( + tmp_path, copy_to_temp_path +) -> None: + sushi_paths = copy_to_temp_path("examples/sushi") + sushi_path = sushi_paths[0] + + # Overwrite the external_models.yaml file to end with a random file and a newline + os.remove(sushi_path / "external_models.yaml") + with open(sushi_path / "external_models.yaml", "w") as f: + f.write("- name: memory.raw.test\n") + + # Override the config.py to turn on lint + with open(sushi_path / "config.py", "r") as f: + read_file = f.read() + + before = """ linter=LinterConfig( + enabled=False, + rules=[ + "ambiguousorinvalidcolumn", + "invalidselectstarexpansion", + "noselectstar", + "nomissingaudits", + "nomissingowner", + "nomissingexternalmodels", + ], + ),""" + after = """linter=LinterConfig(enabled=True, rules=["nomissingexternalmodels"]),""" + read_file = read_file.replace(before, after) + assert after in read_file + with open(sushi_path / "config.py", "w") as f: + f.writelines(read_file) + + # Load the context with the temporary sushi path + context = Context(paths=[sushi_path]) + + # Lint the models + lints = context.lint_models(raise_on_error=False) + assert len(lints) == 1 + lint = lints[0] + assert lint.violation_range is not None + assert ( + lint.violation_msg + == """Model '"memory"."sushi"."customers"' depends on unregistered external model '"memory"."raw"."demographics"'. Please register it in the external models file. This can be done by running 'sqlmesh create_external_models'.""" + ) + assert len(lint.fixes) == 1 + fix = lint.fixes[0] + assert len(fix.edits) == 1 + edit = fix.edits[0] + assert edit.new_text == """- name: '"memory"."raw"."demographics"'\n""" + assert edit.range == Range( + start=Position(line=1, character=0), + end=Position(line=1, character=0), + ) + fix_path = sushi_path / "external_models.yaml" + assert edit.path == fix_path + + +def test_no_missing_external_models_with_existing_file_not_ending_in_newline( + tmp_path, copy_to_temp_path +) -> None: + sushi_paths = copy_to_temp_path("examples/sushi") + sushi_path = sushi_paths[0] + + # Overwrite the external_models.yaml file to end with a random file and a newline + os.remove(sushi_path / "external_models.yaml") + with open(sushi_path / "external_models.yaml", "w") as f: + f.write("- name: memory.raw.test") + + # Override the config.py to turn on lint + with open(sushi_path / "config.py", "r") as f: + read_file = f.read() + + before = """ linter=LinterConfig( + enabled=False, + rules=[ + "ambiguousorinvalidcolumn", + "invalidselectstarexpansion", + "noselectstar", + "nomissingaudits", + "nomissingowner", + "nomissingexternalmodels", + ], + ),""" + after = """linter=LinterConfig(enabled=True, rules=["nomissingexternalmodels"]),""" + read_file = read_file.replace(before, after) + assert after in read_file + with open(sushi_path / "config.py", "w") as f: + f.writelines(read_file) + + # Load the context with the temporary sushi path + context = Context(paths=[sushi_path]) + + # Lint the models + lints = context.lint_models(raise_on_error=False) + assert len(lints) == 1 + lint = lints[0] + assert lint.violation_range is not None + assert ( + lint.violation_msg + == """Model '"memory"."sushi"."customers"' depends on unregistered external model '"memory"."raw"."demographics"'. Please register it in the external models file. This can be done by running 'sqlmesh create_external_models'.""" + ) + assert len(lint.fixes) == 1 + fix = lint.fixes[0] + assert len(fix.edits) == 1 + edit = fix.edits[0] + assert edit.new_text == """\n- name: '"memory"."raw"."demographics"'\n""" + assert edit.range == Range( + start=Position(line=0, character=23), + end=Position(line=0, character=23), + ) + fix_path = sushi_path / "external_models.yaml" + assert edit.path == fix_path diff --git a/vscode/extension/tests/quickfix.spec.ts b/vscode/extension/tests/quickfix.spec.ts index 60d0207f7c..84896713aa 100644 --- a/vscode/extension/tests/quickfix.spec.ts +++ b/vscode/extension/tests/quickfix.spec.ts @@ -8,25 +8,27 @@ import { import { test, expect } from './fixtures' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test('noselectstar quickfix', async ({ page, sharedCodeServer, tempDir }) => { - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - await createPythonInterpreterSettingsSpecifier(tempDir) +test.fixme( + 'noselectstar quickfix', + async ({ page, sharedCodeServer, tempDir }) => { + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) - // Override the settings for the linter - const configPath = path.join(tempDir, 'config.py') - const read = await fs.readFile(configPath, 'utf8') - // Replace linter to be on - const target = 'enabled=True' - const replaced = read.replace('enabled=False', 'enabled=True') - // Assert replaced correctly - expect(replaced).toContain(target) + // Override the settings for the linter + const configPath = path.join(tempDir, 'config.py') + const read = await fs.readFile(configPath, 'utf8') + // Replace linter to be on + const target = 'enabled=True' + const replaced = read.replace('enabled=False', 'enabled=True') + // Assert replaced correctly + expect(replaced).toContain(target) - // Replace the rules to only have noselectstar - const targetRules = `rules=[ + // Replace the rules to only have noselectstar + const targetRules = `rules=[ "noselectstar", ],` - const replacedTheOtherRules = replaced.replace( - `rules=[ + const replacedTheOtherRules = replaced.replace( + `rules=[ "ambiguousorinvalidcolumn", "invalidselectstarexpansion", "noselectstar", @@ -34,54 +36,55 @@ test('noselectstar quickfix', async ({ page, sharedCodeServer, tempDir }) => { "nomissingowner", "nomissingexternalmodels", ],`, - targetRules, - ) - expect(replacedTheOtherRules).toContain(targetRules) + targetRules, + ) + expect(replacedTheOtherRules).toContain(targetRules) - await fs.writeFile(configPath, replacedTheOtherRules) - // Replace the file to cause the error - const modelPath = path.join(tempDir, 'models', 'latest_order.sql') - const readModel = await fs.readFile(modelPath, 'utf8') - // Replace the specific select with the select star - const modelReplaced = readModel.replace( - 'SELECT id, customer_id, start_ts, end_ts, event_date', - 'SELECT *', - ) - await fs.writeFile(modelPath, modelReplaced) + await fs.writeFile(configPath, replacedTheOtherRules) + // Replace the file to cause the error + const modelPath = path.join(tempDir, 'models', 'latest_order.sql') + const readModel = await fs.readFile(modelPath, 'utf8') + // Replace the specific select with the select star + const modelReplaced = readModel.replace( + 'SELECT id, customer_id, start_ts, end_ts, event_date', + 'SELECT *', + ) + await fs.writeFile(modelPath, modelReplaced) - // Open the code server with the specified directory - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + // Open the code server with the specified directory + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') - // Open the file with the linter issue - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) - .locator('a') - .click() + // Open the file with the linter issue + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) + .locator('a') + .click() - await waitForLoadedSQLMesh(page) + await waitForLoadedSQLMesh(page) - await openProblemsView(page) + await openProblemsView(page) - await page.getByRole('button', { name: 'Show fixes' }).click() - await page - .getByRole('menuitem', { name: 'Replace SELECT * with' }) - .first() - .click() + await page.getByRole('button', { name: 'Show fixes' }).click() + await page + .getByRole('menuitem', { name: 'Replace SELECT * with' }) + .first() + .click() - // Wait for the quick fix to be applied - await page.waitForTimeout(2_000) + // Wait for the quick fix to be applied + await page.waitForTimeout(2_000) - // Assert that the model no longer contains SELECT * but SELECT id, customer_id, waiter_id, start_ts, end_ts, event_date - const readUpdatedFile = (await fs.readFile(modelPath)).toString('utf8') - expect(readUpdatedFile).not.toContain('SELECT *') - expect(readUpdatedFile).toContain( - 'SELECT id, customer_id, waiter_id, start_ts, end_ts, event_date', - ) -}) + // Assert that the model no longer contains SELECT * but SELECT id, customer_id, waiter_id, start_ts, end_ts, event_date + const readUpdatedFile = (await fs.readFile(modelPath)).toString('utf8') + expect(readUpdatedFile).not.toContain('SELECT *') + expect(readUpdatedFile).toContain( + 'SELECT id, customer_id, waiter_id, start_ts, end_ts, event_date', + ) + }, +) From d1d876663f86bd242b713c3dbc951186ee1be8a3 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 30 Jul 2025 02:06:48 +0200 Subject: [PATCH 0645/1056] Fix!: Respect the project configuration if the forward-only suffix was not set in the bot's config (#5064) --- .../integrations/github/cicd/controller.py | 5 ++- .../github/cicd/test_github_controller.py | 43 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index cc1131deff..dd5ee70e76 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -479,10 +479,11 @@ def pr_targets_prod_branch(self) -> bool: @property def forward_only_plan(self) -> bool: + default = self._context.config.plan.forward_only head_ref = self._pull_request.head.ref if isinstance(head_ref, str): - return head_ref.endswith(self.bot_config.forward_only_branch_suffix) - return False + return head_ref.endswith(self.bot_config.forward_only_branch_suffix) or default + return default @classmethod def _append_output(cls, key: str, value: str) -> None: diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index f1be97ee20..a27f75f459 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -735,3 +735,46 @@ def test_pr_comment_deploy_indicator_includes_command_namespace( assert "To **apply** this PR's plan to prod, comment:\n - `/deploy`" not in comment assert "To **apply** this PR's plan to prod, comment:\n - `#SQLMesh/deploy`" in comment + + +def test_forward_only_config_falls_back_to_plan_config( + github_client, + make_controller: t.Callable[..., GithubController], + mocker: MockerFixture, +): + mock_repo = github_client.get_repo() + mock_repo.create_check_run = mocker.MagicMock( + side_effect=lambda **kwargs: make_mock_check_run(**kwargs) + ) + + created_comments = [] + mock_issue = mock_repo.get_issue() + mock_issue.create_comment = mocker.MagicMock( + side_effect=lambda comment: make_mock_issue_comment( + comment=comment, created_comments=created_comments + ) + ) + mock_issue.get_comments = mocker.MagicMock(side_effect=lambda: created_comments) + + mock_pull_request = mock_repo.get_pull() + mock_pull_request.get_reviews = mocker.MagicMock(lambda: []) + mock_pull_request.merged = False + mock_pull_request.merge = mocker.MagicMock() + mock_pull_request.head.ref = "unit-test-test-pr" + + controller = make_controller( + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + bot_config=GithubCICDBotConfig( + merge_method=MergeMethod.SQUASH, + enable_deploy_command=True, + forward_only_branch_suffix="-forward-only", + ), + mock_out_context=False, + ) + + controller._context.config.plan.forward_only = True + assert controller.forward_only_plan + + controller._context.config.plan.forward_only = False + assert controller.forward_only_plan is False From 3555e739b49aa0b9ef6c43c3277bb23ebca4617c Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:36:39 +0300 Subject: [PATCH 0646/1056] Chore!: bump sqlglot to v27.5.1 (#5068) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 670c021f56..91a671b2d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.4.1", + "sqlglot[rs]~=27.5.1", "tenacity", "time-machine", "json-stream" From b32f3ae2aa0cbb252d9dd91d7a1f00a097744b50 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:59:51 +0100 Subject: [PATCH 0647/1056] feat(vscode): allow the lsp loaded to be specified (#5070) --- pnpm-lock.yaml | 17 ++ vscode/extension/package.json | 7 + vscode/extension/src/utilities/config.ts | 37 ++- .../src/utilities/sqlmesh/sqlmesh.ts | 30 ++- vscode/extension/tests/configuration.spec.ts | 213 ++++++++++++++++++ 5 files changed, 294 insertions(+), 10 deletions(-) create mode 100644 vscode/extension/tests/configuration.spec.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d0a5449f2..2e39e21fbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,12 +26,18 @@ importers: '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 + '@types/shell-quote': + specifier: ^1.7.5 + version: 1.7.5 '@vscode/python-extension': specifier: ^1.0.5 version: 1.0.5 fs-extra: specifier: ^11.3.0 version: 11.3.0 + shell-quote: + specifier: ^1.8.3 + version: 1.8.3 vscode-jsonrpc: specifier: ^8.2.1 version: 8.2.1 @@ -2378,6 +2384,9 @@ packages: '@types/sarif@2.1.7': resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} + '@types/shell-quote@1.7.5': + resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -5296,6 +5305,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + should-equal@2.0.0: resolution: {integrity: sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==} @@ -8563,6 +8576,8 @@ snapshots: '@types/sarif@2.1.7': {} + '@types/shell-quote@1.7.5': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -11972,6 +11987,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.3: {} + should-equal@2.0.0: dependencies: should-type: 1.4.0 diff --git a/vscode/extension/package.json b/vscode/extension/package.json index a0d853a0a9..a745645413 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -36,6 +36,11 @@ "type": "string", "default": "", "markdownDescription": "The path to the SQLMesh project. If not set, the extension will try to find the project root automatically. If set, the extension will use the project root as the workspace path, e.g. it will run `sqlmesh` and `sqlmesh_lsp` in the project root. The path can be absolute `/Users/sqlmesh_user/sqlmesh_project/sushi` or relative `./project_folder/sushi` to the workspace root." + }, + "sqlmesh.lspEntrypoint": { + "type": "string", + "default": "", + "markdownDescription": "The entry point for the SQLMesh LSP server. If not set the extension looks for the default lsp. If set, the extension will use the entry point as the LSP path, The path can be absolute `/Users/sqlmesh_user/sqlmesh_project/sushi/sqlmesh_lsp` or relative `./project_folder/sushi/sqlmesh_lsp` to the workspace root. It can also have arguments, e.g. `./project_folder/sushi/sqlmesh_lsp --port 5000`." } } }, @@ -136,8 +141,10 @@ "dependencies": { "@duckdb/node-api": "1.3.2-alpha.25", "@types/fs-extra": "^11.0.4", + "@types/shell-quote": "^1.7.5", "@vscode/python-extension": "^1.0.5", "fs-extra": "^11.3.0", + "shell-quote": "^1.8.3", "vscode-jsonrpc": "^8.2.1", "vscode-languageclient": "^9.0.1", "zod": "^3.25.76" diff --git a/vscode/extension/src/utilities/config.ts b/vscode/extension/src/utilities/config.ts index e77a39ce55..c8edcd13ce 100644 --- a/vscode/extension/src/utilities/config.ts +++ b/vscode/extension/src/utilities/config.ts @@ -3,9 +3,12 @@ import path from 'path' import fs from 'fs' import { Result, err, ok } from '@bus/result' import { traceVerbose, traceInfo } from './common/log' +import { parse } from 'shell-quote' +import { z } from 'zod' export interface SqlmeshConfiguration { projectPath: string + lspEntryPoint: string } /** @@ -13,14 +16,46 @@ export interface SqlmeshConfiguration { * * @returns The SQLMesh configuration */ -export function getSqlmeshConfiguration(): SqlmeshConfiguration { +function getSqlmeshConfiguration(): SqlmeshConfiguration { const config = workspace.getConfiguration('sqlmesh') const projectPath = config.get('projectPath', '') + const lspEntryPoint = config.get('lspEntrypoint', '') return { projectPath, + lspEntryPoint, } } +const stringsArray = z.array(z.string()) + +/** + * Get the SQLMesh LSP entry point from VS Code settings. undefined if not set + * it's expected to be a string in the format "command arg1 arg2 ...". + */ +export function getSqlmeshLspEntryPoint(): + | { + entrypoint: string + args: string[] + } + | undefined { + const config = getSqlmeshConfiguration() + if (config.lspEntryPoint === '') { + return undefined + } + // Split the entry point into command and arguments + const parts = parse(config.lspEntryPoint) + const parsed = stringsArray.safeParse(parts) + if (!parsed.success) { + throw new Error( + `Invalid lspEntrypoint configuration: ${config.lspEntryPoint}. Expected a + string in the format "command arg1 arg2 ...".`, + ) + } + const entrypoint = parsed.data[0] + const args = parsed.data.slice(1) + return { entrypoint, args } +} + /** * Validate and resolve the project path from configuration. * If no project path is configured, use the workspace folder. diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index d95017a2ca..104869192b 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -11,7 +11,7 @@ import { execAsync } from '../exec' import z from 'zod' import { ProgressLocation, window } from 'vscode' import { IS_WINDOWS } from '../isWindows' -import { resolveProjectPath } from '../config' +import { getSqlmeshLspEntryPoint, resolveProjectPath } from '../config' import { isSemVerGreaterThanOrEqual } from '../semver' export interface SqlmeshExecInfo { @@ -413,15 +413,7 @@ export const ensureSqlmeshLspDependenciesInstalled = async (): Promise< export const sqlmeshLspExec = async (): Promise< Result > => { - const sqlmeshLSP = IS_WINDOWS ? 'sqlmesh_lsp.exe' : 'sqlmesh_lsp' const projectRoot = await getProjectRoot() - const envVariables = await getPythonEnvVariables() - if (isErr(envVariables)) { - return err({ - type: 'generic', - message: envVariables.error, - }) - } const resolvedPath = resolveProjectPath(projectRoot) if (isErr(resolvedPath)) { return err({ @@ -430,6 +422,26 @@ export const sqlmeshLspExec = async (): Promise< }) } const workspacePath = resolvedPath.value + + const configuredLSPExec = getSqlmeshLspEntryPoint() + if (configuredLSPExec) { + traceLog(`Using configured SQLMesh LSP entry point: ${configuredLSPExec.entrypoint} ${configuredLSPExec.args.join(' ')}`) + return ok({ + bin: configuredLSPExec.entrypoint, + workspacePath, + env: process.env, + args: configuredLSPExec.args, + }) + } + const sqlmeshLSP = IS_WINDOWS ? 'sqlmesh_lsp.exe' : 'sqlmesh_lsp' + const envVariables = await getPythonEnvVariables() + if (isErr(envVariables)) { + return err({ + type: 'generic', + message: envVariables.error, + }) + } + const interpreterDetails = await getInterpreterDetails() traceLog(`Interpreter details: ${JSON.stringify(interpreterDetails)}`) if (interpreterDetails.path) { diff --git a/vscode/extension/tests/configuration.spec.ts b/vscode/extension/tests/configuration.spec.ts new file mode 100644 index 0000000000..6f187d5274 --- /dev/null +++ b/vscode/extension/tests/configuration.spec.ts @@ -0,0 +1,213 @@ +import { test, expect } from './fixtures' +import { + createVirtualEnvironment, + openServerPage, + pipInstall, + REPO_ROOT, + SUSHI_SOURCE_PATH, + waitForLoadedSQLMesh, +} from './utils' +import path from 'path' +import fs from 'fs-extra' + +async function setupPythonEnvironment(tempDir: string): Promise { + // Create a temporary directory for the virtual environment + const venvDir = path.join(tempDir, '.venv') + fs.mkdirSync(venvDir, { recursive: true }) + + // Create virtual environment + const pythonDetails = await createVirtualEnvironment(venvDir) + + // Install sqlmesh from the local repository with LSP support + const customMaterializations = path.join( + REPO_ROOT, + 'examples', + 'custom_materializations', + ) + const sqlmeshWithExtras = `${REPO_ROOT}[lsp,bigquery]` + await pipInstall(pythonDetails, [sqlmeshWithExtras, customMaterializations]) +} + +/** + * Creates an entrypoint file used to test the LSP configuration. + * + * The entrypoint file is a bash script that simply calls out to the + */ +const createEntrypointFile = ( + tempDir: string, + entrypointFileName: string, + bitToStripFromArgs = '', +): { + entrypointFile: string + fileWhereStoredInputs: string +} => { + const entrypointFile = path.join(tempDir, entrypointFileName) + const fileWhereStoredInputs = path.join(tempDir, 'inputs.txt') + const sqlmeshLSPFile = path.join(tempDir, '.venv/bin/sqlmesh_lsp') + + // Create the entrypoint file + fs.writeFileSync( + entrypointFile, + `#!/bin/bash +echo "$@" > ${fileWhereStoredInputs} +# Strip bitToStripFromArgs from the beginning of the args if it matches +if [[ "$1" == "${bitToStripFromArgs}" ]]; then + shift +fi +# Call the sqlmesh_lsp with the remaining arguments +${sqlmeshLSPFile} "$@"`, + { mode: 0o755 }, // Make it executable + ) + + return { + entrypointFile, + fileWhereStoredInputs, + } +} + +test.describe('Test LSP Entrypoint configuration', () => { + test('specify single entrypoint relative path', async ({ + page, + sharedCodeServer, + tempDir, + }) => { + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + await setupPythonEnvironment(tempDir) + + const { fileWhereStoredInputs } = createEntrypointFile( + tempDir, + 'entrypoint.sh', + ) + + const settings = { + 'sqlmesh.lspEntrypoint': './entrypoint.sh', + } + // Write the settings to the settings.json file + const settingsPath = path.join(tempDir, '.vscode', 'settings.json') + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }) + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)) + + await openServerPage(page, tempDir, sharedCodeServer) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customer_revenue_lifetime model + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await waitForLoadedSQLMesh(page) + + // Check that the output file exists and contains the entrypoint script arguments + expect(fs.existsSync(fileWhereStoredInputs)).toBe(true) + expect(fs.readFileSync(fileWhereStoredInputs, 'utf8')).toBe(`--stdio +`) + }) + + test('specify one entrypoint absolute path', async ({ + page, + sharedCodeServer, + tempDir, + }) => { + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + await setupPythonEnvironment(tempDir) + + const { entrypointFile, fileWhereStoredInputs } = createEntrypointFile( + tempDir, + 'entrypoint.sh', + ) + // Assert that the entrypoint file is an absolute path + expect(path.isAbsolute(entrypointFile)).toBe(true) + + const settings = { + 'sqlmesh.lspEntrypoint': `${entrypointFile}`, + } + // Write the settings to the settings.json file + const settingsPath = path.join(tempDir, '.vscode', 'settings.json') + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }) + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)) + + await openServerPage(page, tempDir, sharedCodeServer) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customer_revenue_lifetime model + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await waitForLoadedSQLMesh(page) + + // Check that the output file exists and contains the entrypoint script arguments + expect(fs.existsSync(fileWhereStoredInputs)).toBe(true) + expect(fs.readFileSync(fileWhereStoredInputs, 'utf8')).toBe(`--stdio +`) + }) + + test('specify entrypoint with arguments', async ({ + page, + sharedCodeServer, + tempDir, + }) => { + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + + await setupPythonEnvironment(tempDir) + + const { fileWhereStoredInputs } = createEntrypointFile( + tempDir, + 'entrypoint.sh', + '--argToIgnore', + ) + + const settings = { + 'sqlmesh.lspEntrypoint': './entrypoint.sh --argToIgnore', + } + // Write the settings to the settings.json file + const settingsPath = path.join(tempDir, '.vscode', 'settings.json') + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }) + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)) + + await openServerPage(page, tempDir, sharedCodeServer) + + // Wait for the models folder to be visible + await page.waitForSelector('text=models') + + // Click on the models folder, excluding external_models + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + + // Open the customer_revenue_lifetime model + await page + .getByRole('treeitem', { name: 'customers.sql', exact: true }) + .locator('a') + .click() + + await waitForLoadedSQLMesh(page) + + // Check that the output file exists and contains the entrypoint script arguments + expect(fs.existsSync(fileWhereStoredInputs)).toBe(true) + expect(fs.readFileSync(fileWhereStoredInputs, 'utf8')) + .toBe(`--argToIgnore --stdio +`) + }) +}) From 245e8c988fd4152d3fe42338af4f8f539232dc08 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:25:04 +0100 Subject: [PATCH 0648/1056] chore(vscode): reenabling tests (#5071) --- vscode/extension/tests/bad_setup.spec.ts | 2 +- vscode/extension/tests/utils.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/vscode/extension/tests/bad_setup.spec.ts b/vscode/extension/tests/bad_setup.spec.ts index 3f715e525a..b76eee2b3d 100644 --- a/vscode/extension/tests/bad_setup.spec.ts +++ b/vscode/extension/tests/bad_setup.spec.ts @@ -91,7 +91,7 @@ test('lineage, no sqlmesh found', async ({ // Checks that if you have another file open like somewhere else, it still checks the workspace first for a successful context // it's very flaky but runs when debugging // - the typing in of the file name is very flaky -test.skip('check that the LSP runs correctly by opening lineage when looking at another file before not in workspace', async ({ +test('check that the LSP runs correctly by opening lineage when looking at another file before not in workspace', async ({ page, sharedCodeServer, tempDir, diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index d75036b0ef..8aecae2f46 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -175,17 +175,19 @@ export const openFile = async (page: Page, file: string): Promise => { const maxRetries = 3 const retryDelay = 3000 + const fileName = path.basename(file) + for (let attempt = 0; attempt < maxRetries; attempt++) { try { await page.keyboard.press( process.platform === 'darwin' ? 'Meta+P' : 'Control+P', ) - await page.waitForSelector('input[aria-label="Search files by name"]', { - timeout: 5000, - }) + await page + .getByRole('textbox', { name: 'Search files by name' }) + .waitFor({ state: 'visible', timeout: 5000 }) await page.keyboard.type(file) const commandElement = await page.waitForSelector( - `a:has-text("${file}")`, + `a:has-text("${fileName}")`, { timeout: 5000 }, ) await commandElement.click() From 5ea134fc0f7e55c0dc9252c214e1c916da2e0a20 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:14:25 +0100 Subject: [PATCH 0649/1056] chore(vscode): enabling more tests (#5073) --- vscode/extension/tests/lineage.spec.ts | 95 ++++++++------------------ 1 file changed, 30 insertions(+), 65 deletions(-) diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index eb6a695e3c..bc17b27a4d 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -19,8 +19,6 @@ import { * Helper function to launch VS Code and test lineage with given project path config */ async function testLineageWithProjectPath(page: Page): Promise { - await page.waitForLoadState('networkidle') - await page.waitForLoadState('domcontentloaded') await openLineageView(page) await waitForLoadedSQLMesh(page) } @@ -37,93 +35,67 @@ test('Lineage panel renders correctly - no project path config (default)', async await testLineageWithProjectPath(page) }) -test.skip('Lineage panel renders correctly - relative project path', async ({ +test('Lineage panel renders correctly - relative project path', async ({ page, sharedCodeServer, + tempDir, }) => { - const workspaceDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-workspace-'), - ) - const projectDir = path.join(workspaceDir, 'projects', 'sushi') + const projectDir = path.join(tempDir, 'projects', 'sushi') await fs.copy(SUSHI_SOURCE_PATH, projectDir) - const context = await startCodeServer({ - tempDir: workspaceDir, - }) - const settings = { 'sqlmesh.projectPath': './projects/sushi', - 'python.defaultInterpreterPath': context.defaultPythonInterpreter, + 'python.defaultInterpreterPath': sharedCodeServer.defaultPythonInterpreter, } - await fs.ensureDir(path.join(workspaceDir, '.vscode')) - await fs.writeJson( - path.join(workspaceDir, '.vscode', 'settings.json'), - settings, - { spaces: 2 }, - ) + await fs.ensureDir(path.join(tempDir, '.vscode')) + await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { + spaces: 2, + }) try { - await openServerPage(page, workspaceDir, sharedCodeServer) + await openServerPage(page, tempDir, sharedCodeServer) await testLineageWithProjectPath(page) } finally { - await fs.remove(workspaceDir) + await fs.remove(tempDir) } }) -test.skip('Lineage panel renders correctly - absolute project path', async ({ +test('Lineage panel renders correctly - absolute project path', async ({ page, tempDir, sharedCodeServer, }) => { - const workspaceDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-workspace-'), - ) - const projectDir = path.join(workspaceDir, 'projects', 'sushi') - await fs.ensureDir(path.join(workspaceDir, '.vscode')) + // Copy the sushi project to temporary directory + const projectDir = path.join(tempDir, 'projects', 'sushi') + await fs.ensureDir(path.join(tempDir, '.vscode')) await fs.copy(SUSHI_SOURCE_PATH, projectDir) - await fs.ensureDir(path.join(workspaceDir, '.vscode')) - const context = await startCodeServer({ - tempDir: workspaceDir, - }) const settings = { 'sqlmesh.projectPath': projectDir, - 'python.defaultInterpreterPath': context.defaultPythonInterpreter, + 'python.defaultInterpreterPath': sharedCodeServer.defaultPythonInterpreter, } - await fs.writeJson( - path.join(workspaceDir, '.vscode', 'settings.json'), - settings, - { spaces: 2 }, - ) + await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { + spaces: 2, + }) - try { - await openServerPage(page, tempDir, sharedCodeServer) - await testLineageWithProjectPath(page) - } finally { - await stopCodeServer(context) - } + await openServerPage(page, tempDir, sharedCodeServer) + await testLineageWithProjectPath(page) }) -test.skip('Lineage panel renders correctly - relative project outside of workspace', async ({ +test('Lineage panel renders correctly - relative project outside of workspace', async ({ page, sharedCodeServer, tempDir, }) => { - const tempFolder = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-workspace-'), - ) - const projectDir = path.join(tempFolder, 'projects', 'sushi') + const projectDir = path.join(tempDir, 'projects', 'sushi') await fs.copy(SUSHI_SOURCE_PATH, projectDir) - const workspaceDir = path.join(tempFolder, 'workspace') + const workspaceDir = path.join(tempDir, 'workspace') await fs.ensureDir(workspaceDir) - const context = await startCodeServer({ - tempDir: workspaceDir, - }) const settings = { 'sqlmesh.projectPath': './../projects/sushi', - 'python.defaultInterpreterPath': context.defaultPythonInterpreter, + 'python.defaultInterpreterPath': sharedCodeServer.defaultPythonInterpreter, } await fs.ensureDir(path.join(workspaceDir, '.vscode')) await fs.writeJson( @@ -131,26 +103,19 @@ test.skip('Lineage panel renders correctly - relative project outside of workspa settings, { spaces: 2 }, ) - - try { - await openServerPage(page, tempDir, sharedCodeServer) - await testLineageWithProjectPath(page) - } finally { - await stopCodeServer(context) - } + await openServerPage(page, workspaceDir, sharedCodeServer) + await testLineageWithProjectPath(page) }) -test.skip('Lineage panel renders correctly - absolute path project outside of workspace', async ({ +test('Lineage panel renders correctly - absolute path project outside of workspace', async ({ page, sharedCodeServer, + tempDir, }) => { - const tempFolder = await fs.mkdtemp( - path.join(os.tmpdir(), 'vscode-test-workspace-'), - ) - const projectDir = path.join(tempFolder, 'projects', 'sushi') + const projectDir = path.join(tempDir, 'projects', 'sushi') await fs.copy(SUSHI_SOURCE_PATH, projectDir) - const workspaceDir = path.join(tempFolder, 'workspace') + const workspaceDir = path.join(tempDir, 'workspace') await fs.ensureDir(workspaceDir) const settings = { From 751c38dfdd19589a94bb81c2d701c6c995f18071 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 30 Jul 2025 23:44:03 +0200 Subject: [PATCH 0650/1056] Fix: Resolve physical tables correctly when the query optimization is disabled for a model (#5074) --- sqlmesh/core/renderer.py | 4 +--- tests/core/test_model.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index dc89e15af6..8b733d4c55 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -523,8 +523,6 @@ def render( runtime_stage, start, end, execution_time, *kwargs.values() ) - needs_optimization = needs_optimization and self._optimize_query_flag - if should_cache and self._optimized_cache: query = self._optimized_cache else: @@ -560,7 +558,7 @@ def render( ) raise - if needs_optimization: + if needs_optimization and self._optimize_query_flag: deps = d.find_tables( query, default_catalog=self._default_catalog, dialect=self._dialect ) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index b7c74f90a2..4c2f30e2f7 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -11061,3 +11061,38 @@ def entrypoint(context, **kwargs): assert model_daily is not None assert model_daily.cron == "@daily" + + +def test_render_query_optimize_query_false(assert_exp_eq, sushi_context): + snapshots = sushi_context.snapshots + + model = sushi_context.get_model("sushi.top_waiters") + model = model.copy(update={"optimize_query": False}) + + upstream_model_version = sushi_context.get_snapshot("sushi.waiter_revenue_by_day").version + + assert_exp_eq( + model.render_query(snapshots=snapshots).sql(), + f""" + WITH "test_macros" AS ( + SELECT + 2 AS "lit_two", + "revenue" * 2.0 AS "sql_exp", + CAST("revenue" AS TEXT) AS "sql_lit" + FROM "memory"."sqlmesh__sushi"."sushi__waiter_revenue_by_day__{upstream_model_version}" AS "waiter_revenue_by_day" /* memory.sushi.waiter_revenue_by_day */ + ) + SELECT + CAST("waiter_id" AS INT) AS "waiter_id", + CAST("revenue" AS DOUBLE) AS "revenue" + FROM "memory"."sqlmesh__sushi"."sushi__waiter_revenue_by_day__{upstream_model_version}" AS "waiter_revenue_by_day" /* memory.sushi.waiter_revenue_by_day */ + WHERE + "event_date" = ( + SELECT + MAX("event_date") + FROM "memory"."sqlmesh__sushi"."sushi__waiter_revenue_by_day__{upstream_model_version}" AS "waiter_revenue_by_day" /* memory.sushi.waiter_revenue_by_day */ + ) + ORDER BY + "revenue" DESC + LIMIT 10 + """, + ) From 6724d9643257653d62896e3aba7cb5643945f11c Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:36:41 +0100 Subject: [PATCH 0651/1056] feat: helper to find key in model block (#5079) --- sqlmesh/core/linter/helpers.py | 52 +++++++++++++++++++++++++++++++ tests/core/linter/test_helpers.py | 44 +++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/linter/helpers.py b/sqlmesh/core/linter/helpers.py index e62545bc02..3f6e96765f 100644 --- a/sqlmesh/core/linter/helpers.py +++ b/sqlmesh/core/linter/helpers.py @@ -158,3 +158,55 @@ def get_range_of_model_block( return Range( start=start_position.to_range(splitlines).start, end=end_position.to_range(splitlines).end ) + + +def get_range_of_a_key_in_model_block( + sql: str, + dialect: str, + key: str, +) -> t.Optional[Range]: + """ + Get the range of a specific key in the model block of an SQL file. + """ + tokens = tokenize(sql, dialect=dialect) + if tokens is None: + return None + + # Find the start of the model block + start_index = next( + ( + i + for i, t in enumerate(tokens) + if t.token_type is TokenType.VAR and t.text.upper() == "MODEL" + ), + None, + ) + end_index = next( + (i for i, t in enumerate(tokens) if t.token_type is TokenType.SEMICOLON), + None, + ) + if start_index is None or end_index is None: + return None + if start_index >= end_index: + return None + + tokens_of_interest = tokens[start_index + 1 : end_index] + # Find the key token + key_token = next( + ( + t + for t in tokens_of_interest + if t.token_type is TokenType.VAR and t.text.upper() == key.upper() + ), + None, + ) + if key_token is None: + return None + + position = TokenPositionDetails( + line=key_token.line, + col=key_token.col, + start=key_token.start, + end=key_token.end, + ) + return position.to_range(sql.splitlines()) diff --git a/tests/core/linter/test_helpers.py b/tests/core/linter/test_helpers.py index be6ebf2c27..f3ae193bb0 100644 --- a/tests/core/linter/test_helpers.py +++ b/tests/core/linter/test_helpers.py @@ -1,5 +1,9 @@ from sqlmesh import Context -from sqlmesh.core.linter.helpers import read_range_from_file, get_range_of_model_block +from sqlmesh.core.linter.helpers import ( + read_range_from_file, + get_range_of_model_block, + get_range_of_a_key_in_model_block, +) from sqlmesh.core.model import SqlModel @@ -34,3 +38,41 @@ def test_get_position_of_model_block(): read_range = read_range_from_file(path, range) assert read_range.startswith("MODEL") assert read_range.endswith(";") + + +def test_get_range_of_a_key_in_model_block_testing_on_sushi(): + context = Context(paths=["examples/sushi"]) + + sql_models = [ + model + for model in context.models.values() + if isinstance(model, SqlModel) + and model._path is not None + and str(model._path).endswith(".sql") + ] + assert len(sql_models) > 0 + + for model in sql_models: + possible_keys = ["name", "tags", "description", "columns", "owner", "cron", "dialect"] + + dialect = model.dialect + assert dialect is not None + + path = model._path + assert path is not None + + with open(path, "r", encoding="utf-8") as file: + content = file.read() + + count_properties_checked = 0 + + for key in possible_keys: + range = get_range_of_a_key_in_model_block(content, dialect, key) + + # Check that the range starts with the key and ends with ; + if range: + read_range = read_range_from_file(path, range) + assert read_range.lower() == key.lower() + count_properties_checked += 1 + + assert count_properties_checked > 0 From b448d1c02ca266768c036f234dfe4af3aa15ec77 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:09:16 +0100 Subject: [PATCH 0652/1056] feat: lint rule no missing external will create file (#5078) --- sqlmesh/core/linter/rule.py | 13 ++++- sqlmesh/core/linter/rules/builtin.py | 21 +++++++- sqlmesh/lsp/context.py | 39 +++++++++++++-- tests/core/linter/test_builtin.py | 8 +++- tests/lsp/test_code_actions.py | 71 ++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 9 deletions(-) diff --git a/sqlmesh/core/linter/rule.py b/sqlmesh/core/linter/rule.py index ec942928e7..8dd1a2ebbd 100644 --- a/sqlmesh/core/linter/rule.py +++ b/sqlmesh/core/linter/rule.py @@ -1,7 +1,7 @@ from __future__ import annotations import abc -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from sqlmesh.core.model import Model @@ -49,12 +49,21 @@ class TextEdit: new_text: str +@dataclass(frozen=True) +class CreateFile: + """Create a new file with the provided text.""" + + path: Path + text: str + + @dataclass(frozen=True) class Fix: """A fix that can be applied to resolve a rule violation.""" title: str - edits: t.List[TextEdit] + edits: t.List[TextEdit] = field(default_factory=list) + create_files: t.List[CreateFile] = field(default_factory=list) class _Rule(abc.ABCMeta): diff --git a/sqlmesh/core/linter/rules/builtin.py b/sqlmesh/core/linter/rules/builtin.py index 1a96a4fcec..a793f79434 100644 --- a/sqlmesh/core/linter/rules/builtin.py +++ b/sqlmesh/core/linter/rules/builtin.py @@ -14,7 +14,15 @@ get_range_of_model_block, read_range_from_string, ) -from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit, Position +from sqlmesh.core.linter.rule import ( + Rule, + RuleViolation, + Range, + Fix, + TextEdit, + Position, + CreateFile, +) from sqlmesh.core.linter.definition import RuleSet from sqlmesh.core.model import Model, SqlModel, ExternalModel from sqlmesh.utils.lineage import extract_references_from_query, ExternalModelReference @@ -227,7 +235,16 @@ def create_fix(self, model_name: str) -> t.Optional[Fix]: external_models_path = root / EXTERNAL_MODELS_YAML if not external_models_path.exists(): - return None + return Fix( + title="Add external model file", + edits=[], + create_files=[ + CreateFile( + path=external_models_path, + text=f"- name: '{model_name}'\n", + ) + ], + ) # Figure out the position to insert the new external model at the end of the file, whether # needs new line or not. diff --git a/sqlmesh/lsp/context.py b/sqlmesh/lsp/context.py index 52b33453b2..50265ec306 100644 --- a/sqlmesh/lsp/context.py +++ b/sqlmesh/lsp/context.py @@ -273,13 +273,41 @@ def get_code_actions( if found_violation is not None and found_violation.fixes: # Create code actions for each fix for fix in found_violation.fixes: - # Convert our Fix to LSP TextEdits changes: t.Dict[str, t.List[types.TextEdit]] = {} + document_changes: t.List[ + t.Union[ + types.TextDocumentEdit, + types.CreateFile, + types.RenameFile, + types.DeleteFile, + ] + ] = [] + + for create in fix.create_files: + create_uri = URI.from_path(create.path).value + document_changes.append(types.CreateFile(uri=create_uri)) + document_changes.append( + types.TextDocumentEdit( + text_document=types.OptionalVersionedTextDocumentIdentifier( + uri=create_uri, + version=None, + ), + edits=[ + types.TextEdit( + range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=0, character=0), + ), + new_text=create.text, + ) + ], + ) + ) + for edit in fix.edits: uri_key = URI.from_path(edit.path).value if uri_key not in changes: changes[uri_key] = [] - # Create a TextEdit for the LSP changes[uri_key].append( types.TextEdit( range=types.Range( @@ -296,12 +324,15 @@ def get_code_actions( ) ) - # Create the code action + workspace_edit = types.WorkspaceEdit( + changes=changes if changes else None, + document_changes=document_changes if document_changes else None, + ) code_action = types.CodeAction( title=fix.title, kind=types.CodeActionKind.QuickFix, diagnostics=[diagnostic], - edit=types.WorkspaceEdit(changes=changes), + edit=workspace_edit, ) code_actions.append(code_action) diff --git a/tests/core/linter/test_builtin.py b/tests/core/linter/test_builtin.py index a5a73fcf87..1a19d036b5 100644 --- a/tests/core/linter/test_builtin.py +++ b/tests/core/linter/test_builtin.py @@ -51,7 +51,13 @@ def test_no_missing_external_models(tmp_path, copy_to_temp_path) -> None: lint.violation_msg == """Model '"memory"."sushi"."customers"' depends on unregistered external model '"memory"."raw"."demographics"'. Please register it in the external models file. This can be done by running 'sqlmesh create_external_models'.""" ) - assert len(lint.fixes) == 0 + assert len(lint.fixes) == 1 + fix = lint.fixes[0] + assert len(fix.edits) == 0 + assert len(fix.create_files) == 1 + create = fix.create_files[0] + assert create.path == sushi_path / "external_models.yaml" + assert create.text == '- name: \'"memory"."raw"."demographics"\'\n' def test_no_missing_external_models_with_existing_file_ending_in_newline( diff --git a/tests/lsp/test_code_actions.py b/tests/lsp/test_code_actions.py index b2f30feb47..509f49f9b1 100644 --- a/tests/lsp/test_code_actions.py +++ b/tests/lsp/test_code_actions.py @@ -1,4 +1,5 @@ import typing as t +import os from lsprotocol import types from sqlmesh.core.context import Context from sqlmesh.lsp.context import LSPContext @@ -109,3 +110,73 @@ def test_code_actions_with_linting(copy_to_temp_path: t.Callable): URI.from_path(sushi_path / "models" / "latest_order.sql").value ] assert len(text_edits) > 0 + + +def test_code_actions_create_file(copy_to_temp_path: t.Callable) -> None: + sushi_paths = copy_to_temp_path("examples/sushi") + sushi_path = sushi_paths[0] + + # Remove external models file and enable linter + os.remove(sushi_path / "external_models.yaml") + config_path = sushi_path / "config.py" + with config_path.open("r") as f: + content = f.read() + + before = """ linter=LinterConfig( + enabled=False, + rules=[ + "ambiguousorinvalidcolumn", + "invalidselectstarexpansion", + "noselectstar", + "nomissingaudits", + "nomissingowner", + "nomissingexternalmodels", + ], + ),""" + after = """linter=LinterConfig(enabled=True, rules=["nomissingexternalmodels"]),""" + content = content.replace(before, after) + with config_path.open("w") as f: + f.write(content) + + context = Context(paths=[str(sushi_path)]) + lsp_context = LSPContext(context) + + uri = URI.from_path(sushi_path / "models" / "customers.sql") + violations = lsp_context.lint_model(uri) + + diagnostics = [] + for violation in violations: + if violation.violation_range: + diagnostics.append( + types.Diagnostic( + range=types.Range( + start=types.Position( + line=violation.violation_range.start.line, + character=violation.violation_range.start.character, + ), + end=types.Position( + line=violation.violation_range.end.line, + character=violation.violation_range.end.character, + ), + ), + message=violation.violation_msg, + severity=types.DiagnosticSeverity.Warning, + ) + ) + + params = types.CodeActionParams( + text_document=types.TextDocumentIdentifier(uri=uri.value), + range=types.Range( + start=types.Position(line=0, character=0), end=types.Position(line=1, character=0) + ), + context=types.CodeActionContext(diagnostics=diagnostics), + ) + + actions = lsp_context.get_code_actions(uri, params) + assert actions is not None and len(actions) > 0 + action = next(a for a in actions if isinstance(a, types.CodeAction)) + assert action.edit is not None + assert action.edit.document_changes is not None + create_file = [c for c in action.edit.document_changes if isinstance(c, types.CreateFile)] + assert create_file, "Expected a CreateFile operation" + assert create_file[0].uri == URI.from_path(sushi_path / "external_models.yaml").value From 3bbb819ae27ddad120a81cce50f4717ed5973f32 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:35:15 +0300 Subject: [PATCH 0653/1056] Fix!: normalize blueprint variables (#5045) --- sqlmesh/core/model/common.py | 2 +- .../v0087_normalize_blueprint_variables.py | 138 ++++++++++++++++++ tests/core/test_model.py | 6 +- 3 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 sqlmesh/migrations/v0087_normalize_blueprint_variables.py diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index 704f3e02fe..11ddc8234b 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -157,7 +157,7 @@ def _add_variables_to_python_env( if blueprint_variables: blueprint_variables = { - k: SqlValue(sql=v.sql(dialect=dialect)) if isinstance(v, exp.Expression) else v + k.lower(): SqlValue(sql=v.sql(dialect=dialect)) if isinstance(v, exp.Expression) else v for k, v in blueprint_variables.items() } python_env[c.SQLMESH_BLUEPRINT_VARS] = Executable.value( diff --git a/sqlmesh/migrations/v0087_normalize_blueprint_variables.py b/sqlmesh/migrations/v0087_normalize_blueprint_variables.py new file mode 100644 index 0000000000..8878bc8019 --- /dev/null +++ b/sqlmesh/migrations/v0087_normalize_blueprint_variables.py @@ -0,0 +1,138 @@ +""" +Normalizes blueprint variables, so Customer_Field is stored as customer_field in the `python_env`: + +MODEL ( + ... + blueprints ( + Customer_Field := 1 + ) +); + +SELECT + @customer_field AS col +""" + +import json +import logging +from dataclasses import dataclass + +from sqlglot import exp +from sqlmesh.core.console import get_console +from sqlmesh.utils.migration import index_text_type, blob_text_type + + +logger = logging.getLogger(__name__) + + +SQLMESH_BLUEPRINT_VARS = "__sqlmesh__blueprint__vars__" + + +# Make sure `SqlValue` is defined so it can be used by `eval` call in the migration +@dataclass +class SqlValue: + """A SQL string representing a generated SQLGlot AST.""" + + sql: str + + +def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + migration_needed = False + new_snapshots = [] + + for ( + name, + identifier, + version, + snapshot, + kind_name, + updated_ts, + unpaused_ts, + ttl_ms, + unrestorable, + ) in engine_adapter.fetchall( + exp.select( + "name", + "identifier", + "version", + "snapshot", + "kind_name", + "updated_ts", + "unpaused_ts", + "ttl_ms", + "unrestorable", + ).from_(snapshots_table), + quote_identifiers=True, + ): + parsed_snapshot = json.loads(snapshot) + node = parsed_snapshot["node"] + python_env = node.get("python_env") or {} + + migrate_snapshot = False + + if blueprint_vars_executable := python_env.get(SQLMESH_BLUEPRINT_VARS): + blueprint_vars = eval(blueprint_vars_executable["payload"]) + + for var, value in dict(blueprint_vars).items(): + lowercase_var = var.lower() + if var != lowercase_var: + if lowercase_var in blueprint_vars: + get_console().log_warning( + "SQLMesh is unable to fully migrate the state database, because the " + f"model '{node['name']}' contains two blueprint variables ('{var}' and " + f"'{lowercase_var}') that resolve to the same value ('{lowercase_var}'). " + "This may result in unexpected changes being reported by the next " + "`sqlmesh plan` command. If this happens, consider renaming either variable, " + "so that the lowercase version of their names are different." + ) + else: + del blueprint_vars[var] + blueprint_vars[lowercase_var] = value + migrate_snapshot = True + + if migrate_snapshot: + migration_needed = True + blueprint_vars_executable["payload"] = repr(blueprint_vars) + + new_snapshots.append( + { + "name": name, + "identifier": identifier, + "version": version, + "snapshot": json.dumps(parsed_snapshot), + "kind_name": kind_name, + "updated_ts": updated_ts, + "unpaused_ts": unpaused_ts, + "ttl_ms": ttl_ms, + "unrestorable": unrestorable, + } + ) + + if migration_needed and new_snapshots: + engine_adapter.delete_from(snapshots_table, "TRUE") + + index_type = index_text_type(engine_adapter.dialect) + blob_type = blob_text_type(engine_adapter.dialect) + + engine_adapter.insert_append( + snapshots_table, + pd.DataFrame(new_snapshots), + columns_to_types={ + "name": exp.DataType.build(index_type), + "identifier": exp.DataType.build(index_type), + "version": exp.DataType.build(index_type), + "snapshot": exp.DataType.build(blob_type), + "kind_name": exp.DataType.build("text"), + "updated_ts": exp.DataType.build("bigint"), + "unpaused_ts": exp.DataType.build("bigint"), + "ttl_ms": exp.DataType.build("bigint"), + "unrestorable": exp.DataType.build("boolean"), + }, + ) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 4c2f30e2f7..14c29165b7 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9396,14 +9396,14 @@ def entrypoint(evaluator): MODEL ( name @{customer}.my_table, blueprints ( - (customer := customer1, customer_field := 'bar'), - (customer := customer2, customer_field := qux), + (customer := customer1, Customer_Field := 'bar'), + (customer := customer2, Customer_Field := qux), ), kind FULL ); SELECT - @customer_field AS foo, + @customer_FIELD AS foo, @{customer_field} AS foo2, @BLUEPRINT_VAR('customer_field') AS foo3, FROM @{customer}.my_source From 7b88251330ba98a52ef05a4ef41540c0b4c20b3d Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 1 Aug 2025 06:45:02 +1200 Subject: [PATCH 0654/1056] Fix(athena): Properly extend Athena dialect (#5077) --- sqlmesh/core/dialect.py | 9 +++++++++ tests/core/test_dialect.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 1a42480c13..f5464e12bc 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -13,6 +13,7 @@ from sqlglot import Dialect, Generator, ParseError, Parser, Tokenizer, TokenType, exp from sqlglot.dialects.dialect import DialectType from sqlglot.dialects import DuckDB, Snowflake +import sqlglot.dialects.athena as athena from sqlglot.helper import seq_get from sqlglot.optimizer.normalize_identifiers import normalize_identifiers from sqlglot.optimizer.qualify_columns import quote_identifiers @@ -1014,6 +1015,14 @@ def extend_sqlglot() -> None: generators = {Generator} for dialect in Dialect.classes.values(): + # Athena picks a different Tokenizer / Parser / Generator depending on the query + # so this ensures that the extra ones it defines are also extended + if dialect == athena.Athena: + tokenizers.add(athena._TrinoTokenizer) + parsers.add(athena._TrinoParser) + generators.add(athena._TrinoGenerator) + generators.add(athena._HiveGenerator) + if hasattr(dialect, "Tokenizer"): tokenizers.add(dialect.Tokenizer) if hasattr(dialect, "Parser"): diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index ebf90bebf7..11ffec3720 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -12,7 +12,9 @@ select_from_values_for_batch_range, text_diff, ) +import sqlmesh.core.dialect as d from sqlmesh.core.model import SqlModel, load_sql_based_model +from sqlmesh.core.config.connection import DIALECT_TO_TYPE def test_format_model_expressions(): @@ -700,3 +702,18 @@ def test_model_name_cannot_be_string(): def test_parse_snowflake_create_schema_ddl(): assert parse_one("CREATE SCHEMA d.s", dialect="snowflake").sql() == "CREATE SCHEMA d.s" + + +@pytest.mark.parametrize("dialect", sorted(set(DIALECT_TO_TYPE.values()))) +def test_sqlglot_extended_correctly(dialect: str) -> None: + # MODEL is a SQLMesh extension and not part of SQLGlot + # If we can roundtrip an expression containing MODEL across every dialect, then the SQLMesh extensions have been registered correctly + ast = d.parse_one("MODEL (name foo)", dialect=dialect) + assert isinstance(ast, d.Model) + name_prop = ast.find(exp.Property) + assert isinstance(name_prop, exp.Property) + assert name_prop.this == "name" + value = name_prop.args["value"] + assert isinstance(value, exp.Table) + assert value.sql() == "foo" + assert ast.sql(dialect=dialect) == "MODEL (\nname foo\n)" From 055562bd57ea3c3cdf9c3663554c46d2aa7e2314 Mon Sep 17 00:00:00 2001 From: Alexander Butler <41213451+z3z1ma@users.noreply.github.com> Date: Thu, 31 Jul 2025 21:54:45 +0100 Subject: [PATCH 0655/1056] =?UTF-8?q?Feat:=20handle=20additional=20pseudoc?= =?UTF-8?q?olumn=20types=20in=20bigquery=20when=20resolving=E2=80=A6=20(#5?= =?UTF-8?q?085)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmesh/core/engine_adapter/bigquery.py | 28 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index adb4aa0d19..e953f4d1d0 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -324,14 +324,26 @@ def create_mapping_schema( bq_table = self._get_table(table) columns = create_mapping_schema(bq_table.schema) - if ( - include_pseudo_columns - and bq_table.time_partitioning - and not bq_table.time_partitioning.field - ): - columns["_PARTITIONTIME"] = exp.DataType.build("TIMESTAMP", dialect="bigquery") - if bq_table.time_partitioning.type_ == "DAY": - columns["_PARTITIONDATE"] = exp.DataType.build("DATE") + if include_pseudo_columns: + if bq_table.time_partitioning and not bq_table.time_partitioning.field: + columns["_PARTITIONTIME"] = exp.DataType.build("TIMESTAMP", dialect="bigquery") + if bq_table.time_partitioning.type_ == "DAY": + columns["_PARTITIONDATE"] = exp.DataType.build("DATE") + if bq_table.table_id.endswith("*"): + columns["_TABLE_SUFFIX"] = exp.DataType.build("STRING", dialect="bigquery") + if ( + bq_table.external_data_configuration is not None + and bq_table.external_data_configuration.source_format + in ( + "CSV", + "NEWLINE_DELIMITED_JSON", + "AVRO", + "PARQUET", + "ORC", + "DATASTORE_BACKUP", + ) + ): + columns["_FILE_NAME"] = exp.DataType.build("STRING", dialect="bigquery") return columns From 009f4b1096963098112291c285ac14d43d9fa80a Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:13:57 +0000 Subject: [PATCH 0656/1056] Docs: clarify multiple signals must all be true (#5086) --- docs/guides/signals.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/guides/signals.md b/docs/guides/signals.md index 36dc25496f..4c678d729b 100644 --- a/docs/guides/signals.md +++ b/docs/guides/signals.md @@ -24,6 +24,10 @@ It then divides those into _batches_ (configured with the model's [batch_size](. Signal checking functions examines a batch of time intervals. The function is always called with a batch of time intervals (DateTimeRanges). It can also optionally be called with key word arguments. It may return `True` if all intervals are ready for evaluation, `False` if no intervals are ready, or the time intervals themselves if only some are ready. A checking function is defined with the `@signal` decorator. +!!! note "One model, multiple signals" + + Multiple signals may be specified for a model. SQLMesh categorizes a candidate interval as ready for evaluation if **all** the signal checking functions determine it is ready. + ## Defining a signal To define a signal, create a `signals` directory in your project folder. Define your signal in a file named `__init__.py` in that directory (you can have additional python file names as well). From fd59919476b8ad896f1e6804ac62ade6ab732f9c Mon Sep 17 00:00:00 2001 From: Alexander Butler <41213451+z3z1ma@users.noreply.github.com> Date: Fri, 1 Aug 2025 04:42:36 +0100 Subject: [PATCH 0657/1056] Fix: clean signal registry before loading signals for the given project (#5088) --- sqlmesh/core/loader.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index edea485d16..59a118124c 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -710,6 +710,8 @@ def _load_materializations(self) -> None: def _load_signals(self) -> UniqueKeyDict[str, signal]: """Loads signals for the built-in scheduler.""" + base_signals = signal.get_registry() + signals_max_mtime: t.Optional[float] = None for path in self._glob_paths( @@ -729,7 +731,10 @@ def _load_signals(self) -> UniqueKeyDict[str, signal]: self._signals_max_mtime = signals_max_mtime - return signal.get_registry() + signals = signal.get_registry() + signal.set_registry(base_signals) + + return signals def _load_audits( self, macros: MacroRegistry, jinja_macros: JinjaMacroRegistry From a2f8a05e12b191d7e7d8dc18a076c3ca1c0b5a1e Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Fri, 1 Aug 2025 15:02:44 +0300 Subject: [PATCH 0658/1056] Fix: Unit test CTE failures not being captured (#5081) --- sqlmesh/core/console.py | 52 ++++----- sqlmesh/core/test/definition.py | 17 ++- sqlmesh/core/test/result.py | 19 +++- tests/core/test_test.py | 107 +++++++++++++++++- .../github/cicd/test_github_commands.py | 2 +- 5 files changed, 154 insertions(+), 43 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 5622c21107..cf87fd7443 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -2163,13 +2163,12 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> self._print("-" * divider_length) self._print("Test Failure Summary", style="red") self._print("=" * divider_length) - failures = len(result.failures) + len(result.errors) + fail_and_error_tests = result.get_fail_and_error_tests() self._print(f"{message} \n") - self._print(f"Failed tests ({failures}):") - for test, _ in result.failures + result.errors: - if isinstance(test, ModelTest): - self._print(f" • {test.path}::{test.test_name}") + self._print(f"Failed tests ({len(fail_and_error_tests)}):") + for test in fail_and_error_tests: + self._print(f" • {test.path}::{test.test_name}") self._print("=" * divider_length, end="\n\n") def _captured_unit_test_results(self, result: ModelTextTestResult) -> str: @@ -2721,28 +2720,15 @@ def _log_test_details( Args: result: The unittest test result that contains metrics like num success, fails, ect. """ - if result.wasSuccessful(): self._print("\n", end="") return - errors = result.errors - failures = result.failures - skipped = result.skipped - - infos = [] - if failures: - infos.append(f"failures={len(failures)}") - if errors: - infos.append(f"errors={len(errors)}") - if skipped: - infos.append(f"skipped={skipped}") - if unittest_char_separator: self._print(f"\n{unittest.TextTestResult.separator1}\n\n", end="") for (test_case, failure), test_failure_tables in zip_longest( # type: ignore - failures, result.failure_tables + result.failures, result.failure_tables ): self._print(unittest.TextTestResult.separator2) self._print(f"FAIL: {test_case}") @@ -2758,7 +2744,7 @@ def _log_test_details( self._print(failure_table) self._print("\n", end="") - for test_case, error in errors: + for test_case, error in result.errors: self._print(unittest.TextTestResult.separator2) self._print(f"ERROR: {test_case}") self._print(f"{unittest.TextTestResult.separator2}") @@ -3080,27 +3066,27 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> fail_shared_style = {**shared_style, **fail_color} header = str(h("span", {"style": fail_shared_style}, "-" * divider_length)) message = str(h("span", {"style": fail_shared_style}, "Test Failure Summary")) + fail_and_error_tests = result.get_fail_and_error_tests() failed_tests = [ str( h( "span", {"style": fail_shared_style}, - f"Failed tests ({len(result.failures) + len(result.errors)}):", + f"Failed tests ({len(fail_and_error_tests)}):", ) ) ] - for test, _ in result.failures + result.errors: - if isinstance(test, ModelTest): - failed_tests.append( - str( - h( - "span", - {"style": fail_shared_style}, - f" • {test.model.name}::{test.test_name}", - ) + for test in fail_and_error_tests: + failed_tests.append( + str( + h( + "span", + {"style": fail_shared_style}, + f" • {test.model.name}::{test.test_name}", ) ) + ) failures = "
    ".join(failed_tests) footer = str(h("span", {"style": fail_shared_style}, "=" * divider_length)) error_output = widgets.Textarea(output, layout={"height": "300px", "width": "100%"}) @@ -3508,10 +3494,10 @@ def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> self._log_test_details(result, unittest_char_separator=False) self._print("```\n\n") - failures = len(result.failures) + len(result.errors) + fail_and_error_tests = result.get_fail_and_error_tests() self._print(f"**{message}**\n") - self._print(f"**Failed tests ({failures}):**") - for test, _ in result.failures + result.errors: + self._print(f"**Failed tests ({len(fail_and_error_tests)}):**") + for test in fail_and_error_tests: if isinstance(test, ModelTest): self._print(f" • `{test.model.name}`::`{test.test_name}`\n\n") diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index c0e2f7a08e..8123f52d26 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -317,6 +317,13 @@ def _to_hashable(x: t.Any) -> t.Any: # # This is a bit of a hack, but it's a way to get the best of both worlds. args: t.List[t.Any] = [] + + failed_subtest = "" + + if subtest := getattr(self, "_subtest", None): + if cte := subtest.params.get("cte"): + failed_subtest = f" (CTE {cte})" + if expected.shape != actual.shape: _raise_if_unexpected_columns(expected.columns, actual.columns) @@ -325,13 +332,13 @@ def _to_hashable(x: t.Any) -> t.Any: missing_rows = _row_difference(expected, actual) if not missing_rows.empty: args[0] += f"\n\nMissing rows:\n\n{missing_rows}" - args.append(df_to_table("Missing rows", missing_rows)) + args.append(df_to_table(f"Missing rows{failed_subtest}", missing_rows)) unexpected_rows = _row_difference(actual, expected) if not unexpected_rows.empty: args[0] += f"\n\nUnexpected rows:\n\n{unexpected_rows}" - args.append(df_to_table("Unexpected rows", unexpected_rows)) + args.append(df_to_table(f"Unexpected rows{failed_subtest}", unexpected_rows)) else: diff = expected.compare(actual).rename(columns={"self": "exp", "other": "act"}) @@ -341,7 +348,8 @@ def _to_hashable(x: t.Any) -> t.Any: diff.rename(columns={"exp": "Expected", "act": "Actual"}, inplace=True) if self.verbosity == Verbosity.DEFAULT: args.extend( - df_to_table("Data mismatch", df) for df in _split_df_by_column_pairs(diff) + df_to_table(f"Data mismatch{failed_subtest}", df) + for df in _split_df_by_column_pairs(diff) ) else: from pandas import MultiIndex @@ -351,7 +359,8 @@ def _to_hashable(x: t.Any) -> t.Any: col_diff = diff[col] if not col_diff.empty: table = df_to_table( - f"[bold red]Column '{col}' mismatch[/bold red]", col_diff + f"[bold red]Column '{col}' mismatch{failed_subtest}[/bold red]", + col_diff, ) args.append(table) diff --git a/sqlmesh/core/test/result.py b/sqlmesh/core/test/result.py index 8621b8b10a..eefa0be513 100644 --- a/sqlmesh/core/test/result.py +++ b/sqlmesh/core/test/result.py @@ -4,6 +4,8 @@ import typing as t import unittest +from sqlmesh.core.test.definition import ModelTest + if t.TYPE_CHECKING: ErrorType = t.Union[ t.Tuple[type[BaseException], BaseException, types.TracebackType], @@ -42,7 +44,10 @@ def addSubTest( exctype, value, tb = err err = (exctype, value, None) # type: ignore - super().addSubTest(test, subtest, err) + if err[0] and issubclass(err[0], test.failureException): + self.addFailure(test, err) + else: + self.addError(test, err) def _print_char(self, char: str) -> None: from sqlmesh.core.console import TerminalConsole @@ -117,4 +122,14 @@ def merge(self, other: ModelTextTestResult) -> None: skipped_args = other.skipped[0] self.addSkip(skipped_args[0], skipped_args[1]) - self.testsRun += 1 + self.testsRun += other.testsRun + + def get_fail_and_error_tests(self) -> t.List[ModelTest]: + # If tests contain failed subtests (e.g testing CTE outputs) we don't want + # to report it as different test failures + test_name_to_test = { + test.test_name: test + for test, _ in self.failures + self.errors + if isinstance(test, ModelTest) + } + return list(test_name_to_test.values()) diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 31fe0f3495..521773d1ca 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -6,7 +6,7 @@ from pathlib import Path import unittest from unittest.mock import call, patch -from shutil import copyfile, rmtree +from shutil import rmtree import pandas as pd # noqa: TID253 import pytest @@ -87,6 +87,7 @@ def _check_successful_or_raise( assert result is not None if not result.wasSuccessful(): error_or_failure_traceback = (result.errors or result.failures)[0][1] + print(error_or_failure_traceback) if expected_msg: assert expected_msg in error_or_failure_traceback else: @@ -2316,6 +2317,13 @@ def test_test_with_resolve_template_macro(tmp_path: Path): @use_terminal_console def test_test_output(tmp_path: Path) -> None: + def copy_test_file(test_file: Path, new_test_file: Path, index: int) -> None: + with open(test_file, "r") as file: + filedata = file.read() + + with open(new_test_file, "w") as file: + file.write(filedata.replace("test_example_full_model", f"test_{index}")) + init_example_project(tmp_path, engine_type="duckdb") original_test_file = tmp_path / "tests" / "test_full_model.yaml" @@ -2407,8 +2415,8 @@ def test_test_output(tmp_path: Path) -> None: # Case 3: Assert that concurrent execution is working properly for i in range(50): - copyfile(original_test_file, tmp_path / "tests" / f"test_success_{i}.yaml") - copyfile(new_test_file, tmp_path / "tests" / f"test_failure_{i}.yaml") + copy_test_file(original_test_file, tmp_path / "tests" / f"test_success_{i}.yaml", i) + copy_test_file(new_test_file, tmp_path / "tests" / f"test_failure_{i}.yaml", i) with capture_output() as captured_output: context.test() @@ -3327,3 +3335,96 @@ def execute(context: ExecutionContext, **kwargs: t.Any) -> pd.DataFrame: context=context, ) _check_successful_or_raise(test_default_vars.run()) + + +@use_terminal_console +def test_cte_failure(tmp_path: Path) -> None: + models_dir = tmp_path / "models" + models_dir.mkdir() + (models_dir / "foo.sql").write_text( + """ + MODEL ( + name test.foo, + kind full + ); + + with model_cte as ( + SELECT 1 AS id + ) + SELECT id FROM model_cte + """ + ) + + config = Config( + default_connection=DuckDBConnectionConfig(), + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + ) + context = Context(paths=tmp_path, config=config) + + expected_cte_failure_output = """Data mismatch (CTE "model_cte") +┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓ +┃ Row ┃ id: Expected ┃ id: Actual ┃ +┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩ +│ 0 │ 2 │ 1 │ +└──────────┴─────────────────────────┴─────────────────────┘""" + + expected_query_failure_output = """Data mismatch +┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓ +┃ Row ┃ id: Expected ┃ id: Actual ┃ +┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩ +│ 0 │ 2 │ 1 │ +└──────────┴─────────────────────────┴─────────────────────┘""" + + # Case 1: Ensure that a single CTE failure is reported correctly + tests_dir = tmp_path / "tests" + tests_dir.mkdir() + (tests_dir / "test_foo.yaml").write_text( + """ +test_foo: + model: test.foo + outputs: + ctes: + model_cte: + rows: + - id: 2 + query: + - id: 1 + """ + ) + + with capture_output() as captured_output: + context.test() + + output = captured_output.stdout + + assert expected_cte_failure_output in output + assert expected_query_failure_output not in output + + assert "Ran 1 tests" in output + assert "Failed tests (1)" in output + + # Case 2: Ensure that both CTE and query failures are reported correctly + (tests_dir / "test_foo.yaml").write_text( + """ +test_foo: + model: test.foo + outputs: + ctes: + model_cte: + rows: + - id: 2 + query: + - id: 2 + """ + ) + + with capture_output() as captured_output: + context.test() + + output = captured_output.stdout + + assert expected_cte_failure_output in output + assert expected_query_failure_output in output + + assert "Ran 1 tests" in output + assert "Failed tests (1)" in output diff --git a/tests/integrations/github/cicd/test_github_commands.py b/tests/integrations/github/cicd/test_github_commands.py index 6d82755934..296fea5938 100644 --- a/tests/integrations/github/cicd/test_github_commands.py +++ b/tests/integrations/github/cicd/test_github_commands.py @@ -479,7 +479,7 @@ def test_run_all_test_failed( assert ( """sqlmesh.utils.errors.TestError: some error""" in test_checks_runs[2]["output"]["summary"] ) - assert """Failed tests (1):""" in test_checks_runs[2]["output"]["summary"] + assert """Failed tests""" in test_checks_runs[2]["output"]["summary"] assert "SQLMesh - Prod Plan Preview" in controller._check_run_mapping prod_plan_preview_checks_runs = controller._check_run_mapping[ From 445c6f649c977068e4b5311733dc2b9a6a66b15a Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 1 Aug 2025 18:42:50 +0200 Subject: [PATCH 0659/1056] Chore!: Bump sqlglot to 27.6.0 (#5090) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 91a671b2d4..8e1c4b879f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.5.1", + "sqlglot[rs]~=27.6.0", "tenacity", "time-machine", "json-stream" From e74995805bf1ba814789b97d227e09a434bb041c Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 4 Aug 2025 08:49:31 -0700 Subject: [PATCH 0660/1056] feat: add ui package to keep sharable components and utils (#5089) --- web/shared_ui/.gitignore | 3 +++ web/shared_ui/package.json | 41 ++++++++++++++++++++++++++++++++ web/shared_ui/src/index.ts | 2 ++ web/shared_ui/src/types/index.ts | 3 +++ web/shared_ui/src/utils/index.ts | 8 +++++++ web/shared_ui/tsconfig.json | 35 +++++++++++++++++++++++++++ web/shared_ui/vite.config.js | 36 ++++++++++++++++++++++++++++ 7 files changed, 128 insertions(+) create mode 100644 web/shared_ui/.gitignore create mode 100644 web/shared_ui/package.json create mode 100644 web/shared_ui/src/index.ts create mode 100644 web/shared_ui/src/types/index.ts create mode 100644 web/shared_ui/src/utils/index.ts create mode 100644 web/shared_ui/tsconfig.json create mode 100644 web/shared_ui/vite.config.js diff --git a/web/shared_ui/.gitignore b/web/shared_ui/.gitignore new file mode 100644 index 0000000000..0a4ebc2eae --- /dev/null +++ b/web/shared_ui/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/web/shared_ui/package.json b/web/shared_ui/package.json new file mode 100644 index 0000000000..dcc0449d0e --- /dev/null +++ b/web/shared_ui/package.json @@ -0,0 +1,41 @@ +{ + "name": "sqlmesh-shared-ui", + "version": "0.0.1", + "private": false, + "type": "module", + "files": [ + "/dist" + ], + "main": "dist/sqlmesh-shared-ui.umd.js", + "module": "dist/sqlmesh-shared-ui.es.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/sqlmesh-shared-ui.es.js", + "require": "./dist/sqlmesh-shared-ui.umd.js" + } + }, + "scripts": { + "build": "tsc -b && vite build --base './'", + "build:storybook": "storybook build", + "dev": "storybook dev -p 6006", + "lint": "eslint --max-warnings 0 --fix .", + "pretty": "prettier --write .", + "format": "pnpm run pretty && pnpm run lint", + "test": "vitest", + "test:watch": "vitest watch", + "test:ui": "vitest --ui" + }, + "devDependencies": { + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.7.0", + "typescript": "^5.8.3", + "vite": "^6.3.5", + "vite-plugin-dts": "^4.5.4" + }, + "peerDependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + } +} diff --git a/web/shared_ui/src/index.ts b/web/shared_ui/src/index.ts new file mode 100644 index 0000000000..f486364817 --- /dev/null +++ b/web/shared_ui/src/index.ts @@ -0,0 +1,2 @@ +export * from '@/utils' +export * from '@/types' diff --git a/web/shared_ui/src/types/index.ts b/web/shared_ui/src/types/index.ts new file mode 100644 index 0000000000..a2d8ca7e51 --- /dev/null +++ b/web/shared_ui/src/types/index.ts @@ -0,0 +1,3 @@ +export type Nil = undefined | null +export type Optional = T | undefined +export type Maybe = T | Nil diff --git a/web/shared_ui/src/utils/index.ts b/web/shared_ui/src/utils/index.ts new file mode 100644 index 0000000000..f967d48da4 --- /dev/null +++ b/web/shared_ui/src/utils/index.ts @@ -0,0 +1,8 @@ +import type { Nil } from '@/types' + +export function isNil(value: unknown): value is Nil { + return value == null +} +export function notNil(value: unknown): value is NonNullable { + return value != null +} diff --git a/web/shared_ui/tsconfig.json b/web/shared_ui/tsconfig.json new file mode 100644 index 0000000000..dfbf2261bd --- /dev/null +++ b/web/shared_ui/tsconfig.json @@ -0,0 +1,35 @@ +{ + "include": ["**/*.ts", "**/*.tsx", "vitest.config.js"], + "compilerOptions": { + "target": "ES2022", + "jsx": "react-jsx", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + /* Linting */ + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + /* Declaration */ + "declaration": true, + "declarationDir": "./dist", + "emitDeclarationOnly": false, + + /* Paths */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/web/shared_ui/vite.config.js b/web/shared_ui/vite.config.js new file mode 100644 index 0000000000..a5ec9df8ca --- /dev/null +++ b/web/shared_ui/vite.config.js @@ -0,0 +1,36 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' +import dts from 'vite-plugin-dts' + +export default defineConfig({ + plugins: [ + react(), + dts({ + insertTypesEntry: true, + }), + ], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + build: { + lib: { + entry: path.resolve(__dirname, 'src/index.ts'), + name: 'sqlmesh-shared-ui', + fileName: format => `sqlmesh-shared-ui.${format}.js`, + }, + rollupOptions: { + external: ['react', 'react-dom'], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + }, + }, + }, + sourcemap: true, + outDir: 'dist', + }, +}) From 1097cefeb68ba349369c88d6cbf6aea2fc43a9ad Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 4 Aug 2025 18:24:49 -0700 Subject: [PATCH 0661/1056] Fix: Make gateway names case-insesitive (#5092) --- docs/concepts/models/external_models.md | 4 +- docs/guides/configuration.md | 6 +- docs/reference/configuration.md | 4 +- sqlmesh/core/config/root.py | 15 ++- sqlmesh/core/context.py | 4 +- sqlmesh/core/loader.py | 1 + tests/core/test_context.py | 135 ++++++++++++++++++++++++ 7 files changed, 158 insertions(+), 11 deletions(-) diff --git a/docs/concepts/models/external_models.md b/docs/concepts/models/external_models.md index 922daac6b0..ef2b39a10c 100644 --- a/docs/concepts/models/external_models.md +++ b/docs/concepts/models/external_models.md @@ -56,6 +56,8 @@ If SQLMesh does not have access to an external table's metadata, the table will In some use-cases such as [isolated systems with multiple gateways](../../guides/isolated_systems.md#multiple-gateways), there are external models that only exist on a certain gateway. +**Gateway names are case-insensitive in external model configurations.** You can specify the gateway name using any case (e.g., `gateway: dev`, `gateway: DEV`, `gateway: Dev`) and SQLMesh will handle the matching correctly. + Consider the following model that queries an external table with a dynamic database based on the current gateway: ``` @@ -100,7 +102,7 @@ This example demonstrates the structure of a `external_models.yaml` file: column_d: float - name: external_db.gateway_specific_external_table description: Another external table that only exists when the gateway is set to "test" - gateway: test + gateway: test # Case-insensitive - could also be "TEST", "Test", etc. columns: column_e: int column_f: varchar diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 6e14d1f605..24371f30d0 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -322,7 +322,7 @@ SQLMesh creates schemas, physical tables, and views in the data warehouse/engine The default SQLMesh behavior described in the FAQ is appropriate for most deployments, but you can override *where* SQLMesh creates physical tables and views with the `physical_schema_mapping`, `environment_suffix_target`, and `environment_catalog_mapping` configuration options. -You can also override *what* the physical tables are called by using the `physical_table_naming_convention` option. +You can also override *what* the physical tables are called by using the `physical_table_naming_convention` option. These options are in the [environments](../reference/configuration.md#environments) section of the configuration reference page. @@ -767,7 +767,9 @@ Even though the second change should have been a metadata change (thus not requi The `gateways` configuration defines how SQLMesh should connect to the data warehouse, state backend, and scheduler. These options are in the [gateway](../reference/configuration.md#gateway) section of the configuration reference page. -Each gateway key represents a unique gateway name and configures its connections. For example, this configures the `my_gateway` gateway: +Each gateway key represents a unique gateway name and configures its connections. **Gateway names are case-insensitive** - SQLMesh automatically normalizes gateway names to lowercase during configuration validation. This means you can use any case in your configuration files (e.g., `MyGateway`, `mygateway`, `MYGATEWAY`) and they will all work correctly. + +For example, this configures the `my_gateway` gateway: === "YAML" diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 06aed36b53..df3fcf930d 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -141,7 +141,7 @@ SQLMesh UI settings. The `gateways` dictionary defines how SQLMesh should connect to the data warehouse, state backend, test backend, and scheduler. -It takes one or more named `gateway` configuration keys, each of which can define its own connections. A named gateway does not need to specify all four components and will use defaults if any are omitted - more information is provided about [gateway defaults](#gatewayconnection-defaults) below. +It takes one or more named `gateway` configuration keys, each of which can define its own connections. **Gateway names are case-insensitive** - SQLMesh normalizes all gateway names to lowercase during configuration validation, allowing you to use any case when referencing gateways. A named gateway does not need to specify all four components and will use defaults if any are omitted - more information is provided about [gateway defaults](#gatewayconnection-defaults) below. For example, a project might configure the `gate1` and `gate2` gateways: @@ -247,7 +247,7 @@ If a configuration contains multiple gateways, SQLMesh will use the first one in | Option | Description | Type | Required | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | :----: | :------: | -| `default_gateway` | The name of a gateway to use if one is not provided explicitly (Default: the gateway defined first in the `gateways` option) | string | N | +| `default_gateway` | The name of a gateway to use if one is not provided explicitly (Default: the gateway defined first in the `gateways` option). Gateway names are case-insensitive. | string | N | ### Default connections/scheduler diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index 4dd28f97a5..df8e2637da 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -62,6 +62,9 @@ def gateways_ensure_dict(value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: GatewayConfig.parse_obj(value) return {"": value} except Exception: + # Normalize all gateway keys to lowercase for case-insensitive matching + if isinstance(value, dict): + return {k.lower(): v for k, v in value.items()} return value @@ -298,19 +301,23 @@ def get_gateway(self, name: t.Optional[str] = None) -> GatewayConfig: if isinstance(self.gateways, dict): if name is None: if self.default_gateway: - if self.default_gateway not in self.gateways: + # Normalize default_gateway name to lowercase for lookup + default_key = self.default_gateway.lower() + if default_key not in self.gateways: raise ConfigError(f"Missing gateway with name '{self.default_gateway}'") - return self.gateways[self.default_gateway] + return self.gateways[default_key] if "" in self.gateways: return self.gateways[""] return first(self.gateways.values()) - if name not in self.gateways: + # Normalize lookup name to lowercase since gateway keys are already lowercase + lookup_key = name.lower() + if lookup_key not in self.gateways: raise ConfigError(f"Missing gateway with name '{name}'.") - return self.gateways[name] + return self.gateways[lookup_key] if name is not None: raise ConfigError("Gateway name is not supported when only one gateway is configured.") return self.gateways diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 5a0531209a..18df3a01fd 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -400,9 +400,9 @@ def __init__( self.environment_ttl = self.config.environment_ttl self.pinned_environments = Environment.sanitize_names(self.config.pinned_environments) self.auto_categorize_changes = self.config.plan.auto_categorize_changes - self.selected_gateway = gateway or self.config.default_gateway_name + self.selected_gateway = (gateway or self.config.default_gateway_name).lower() - gw_model_defaults = self.config.gateways[self.selected_gateway].model_defaults + gw_model_defaults = self.config.get_gateway(self.selected_gateway).model_defaults if gw_model_defaults: # Merge global model defaults with the selected gateway's, if it's overriden global_defaults = self.config.model_defaults.model_dump(exclude_unset=True) diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index 59a118124c..f4c9147d1b 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -374,6 +374,7 @@ def _load(path: Path) -> t.List[Model]: # however, if there is a gateway defined, gateway-specific models take precedence if gateway: + gateway = gateway.lower() for model in external_models: if model.gateway == gateway: if model.fqn in models and models[model.fqn].gateway == gateway: diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 26fc542632..6b98bf5d25 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1221,6 +1221,12 @@ def _get_external_model_names(gateway=None): # gateway explicitly set to prod; prod model should now show assert "prod_raw.model1" in _get_external_model_names(gateway="prod") + # test uppercase gateway name should match lowercase external model definition + assert "prod_raw.model1" in _get_external_model_names(gateway="PROD") + + # test mixed case gateway name should also work + assert "prod_raw.model1" in _get_external_model_names(gateway="Prod") + def test_disabled_model(copy_to_temp_path): path = copy_to_temp_path("examples/sushi") @@ -2867,3 +2873,132 @@ def test_model_defaults_statements_with_on_virtual_update(tmp_path: Path): # Default statements should come first assert model.on_virtual_update[0].sql() == "SELECT 'Model-defailt virtual update' AS message" assert model.on_virtual_update[1].sql() == "SELECT 'Model-specific update' AS message" + + +def test_uppercase_gateway_external_models(tmp_path): + # Create a temporary SQLMesh project with uppercase gateway name + config_py = tmp_path / "config.py" + config_py.write_text(""" +from sqlmesh.core.config import Config, DuckDBConnectionConfig, GatewayConfig, ModelDefaultsConfig + +config = Config( + gateways={ + "UPPERCASE_GATEWAY": GatewayConfig( + connection=DuckDBConnectionConfig(), + ), + }, + default_gateway="UPPERCASE_GATEWAY", + model_defaults=ModelDefaultsConfig(dialect="duckdb"), +) +""") + + # Create external models file with lowercase gateway name (this should still match uppercase) + external_models_yaml = tmp_path / "external_models.yaml" + external_models_yaml.write_text(""" +- name: test_db.uppercase_gateway_table + description: Test external model with lowercase gateway name that should match uppercase gateway + gateway: uppercase_gateway # lowercase in external model, but config has UPPERCASE_GATEWAY + columns: + id: int + name: text + +- name: test_db.no_gateway_table + description: Test external model without gateway (should be available for all gateways) + columns: + id: int + name: text +""") + + # Create a model that references the external model + models_dir = tmp_path / "models" + models_dir.mkdir() + model_sql = models_dir / "test_model.sql" + model_sql.write_text(""" +MODEL ( + name test.my_model, + kind FULL, +); + +SELECT * FROM test_db.uppercase_gateway_table; +""") + + # Test with uppercase gateway name - this should find both models + context_uppercase = Context(paths=[tmp_path], gateway="UPPERCASE_GATEWAY") + + # Verify external model with lowercase gateway name in YAML is found when using uppercase gateway + gateway_specific_models = [ + model + for model in context_uppercase.models.values() + if model.name == "test_db.uppercase_gateway_table" + ] + assert len(gateway_specific_models) == 1, ( + f"External model with lowercase gateway name should be found with uppercase gateway. Found {len(gateway_specific_models)} models" + ) + + # Verify external model without gateway is also found + no_gateway_models = [ + model + for model in context_uppercase.models.values() + if model.name == "test_db.no_gateway_table" + ] + assert len(no_gateway_models) == 1, ( + f"External model without gateway should be found. Found {len(no_gateway_models)} models" + ) + + # Check that the column types are properly loaded (not UNKNOWN) + external_model = gateway_specific_models[0] + column_types = {name: str(dtype) for name, dtype in external_model.columns_to_types.items()} + assert column_types == {"id": "INT", "name": "TEXT"}, ( + f"External model column types should not be UNKNOWN, got: {column_types}" + ) + + # Test that when using a different case for the gateway parameter, we get the same results + context_mixed_case = Context( + paths=[tmp_path], gateway="uppercase_gateway" + ) # lowercase parameter + + gateway_specific_models_mixed = [ + model + for model in context_mixed_case.models.values() + if model.name == "test_db.uppercase_gateway_table" + ] + # This should work but might fail if case sensitivity is not handled correctly + assert len(gateway_specific_models_mixed) == 1, ( + f"External model should be found regardless of gateway parameter case. Found {len(gateway_specific_models_mixed)} models" + ) + + # Test a case that should demonstrate the potential issue: + # Create another external model file with uppercase gateway name in the YAML + external_models_yaml_uppercase = tmp_path / "external_models_uppercase.yaml" + external_models_yaml_uppercase.write_text(""" +- name: test_db.uppercase_in_yaml + description: Test external model with uppercase gateway name in YAML + gateway: UPPERCASE_GATEWAY # uppercase in external model yaml + columns: + id: int + status: text +""") + + # Add the new external models file to the project + models_dir = tmp_path / "external_models" + models_dir.mkdir(exist_ok=True) + (models_dir / "uppercase_gateway_models.yaml").write_text(""" +- name: test_db.uppercase_in_yaml + description: Test external model with uppercase gateway name in YAML + gateway: UPPERCASE_GATEWAY # uppercase in external model yaml + columns: + id: int + status: text +""") + + # Reload context to pick up the new external models + context_reloaded = Context(paths=[tmp_path], gateway="UPPERCASE_GATEWAY") + + uppercase_in_yaml_models = [ + model + for model in context_reloaded.models.values() + if model.name == "test_db.uppercase_in_yaml" + ] + assert len(uppercase_in_yaml_models) == 1, ( + f"External model with uppercase gateway in YAML should be found. Found {len(uppercase_in_yaml_models)} models" + ) From a52a2189bf1d6c5fdcb8864a16efe5c42c681f4d Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 5 Aug 2025 07:57:07 -0700 Subject: [PATCH 0662/1056] Fix!: Support Paren expressions in EACH, REDUCE, etc macros (#5096) --- .../custom_materializations/pyproject.toml | 3 + sqlmesh/core/macros.py | 8 ++- tests/core/test_context.py | 4 +- tests/core/test_model.py | 59 +++++++++++++++++++ 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/examples/custom_materializations/pyproject.toml b/examples/custom_materializations/pyproject.toml index 70eb2d7f9e..fd233a9986 100644 --- a/examples/custom_materializations/pyproject.toml +++ b/examples/custom_materializations/pyproject.toml @@ -14,3 +14,6 @@ custom_full_with_custom_kind = "custom_materializations.custom_kind:CustomFullWi [tool.setuptools.packages.find] include = ["custom_materializations"] + +[tool.setuptools_scm] +fallback_version = "0.0.0" diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index a72bf4605a..af891a5460 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -653,7 +653,13 @@ def substitute( if len(items) == 1: item = items[0] - expressions = item.expressions if isinstance(item, (exp.Array, exp.Tuple)) else item + expressions = ( + item.expressions + if isinstance(item, (exp.Array, exp.Tuple)) + else [item.this] + if isinstance(item, exp.Paren) + else item + ) else: expressions = items diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 6b98bf5d25..0f14fd4a8b 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -2198,13 +2198,13 @@ def test_plan_audit_intervals(tmp_path: pathlib.Path, caplog): # Case 1: The timestamp audit should be in the inclusive range ['2025-02-01 00:00:00', '2025-02-01 23:59:59.999999'] assert ( - f"""SELECT COUNT(*) FROM (SELECT ("timestamp_id") AS "timestamp_id" FROM (SELECT * FROM "sqlmesh__sqlmesh_audit"."sqlmesh_audit__timestamp_example__{timestamp_snapshot.version}" AS "sqlmesh_audit__timestamp_example__{timestamp_snapshot.version}" WHERE "timestamp_id" BETWEEN CAST('2025-02-01 00:00:00' AS TIMESTAMP) AND CAST('2025-02-01 23:59:59.999999' AS TIMESTAMP)) AS "_q_0" WHERE TRUE GROUP BY ("timestamp_id") HAVING COUNT(*) > 1) AS "audit\"""" + f"""SELECT COUNT(*) FROM (SELECT "timestamp_id" AS "timestamp_id" FROM (SELECT * FROM "sqlmesh__sqlmesh_audit"."sqlmesh_audit__timestamp_example__{timestamp_snapshot.version}" AS "sqlmesh_audit__timestamp_example__{timestamp_snapshot.version}" WHERE "timestamp_id" BETWEEN CAST('2025-02-01 00:00:00' AS TIMESTAMP) AND CAST('2025-02-01 23:59:59.999999' AS TIMESTAMP)) AS "_q_0" WHERE TRUE GROUP BY "timestamp_id" HAVING COUNT(*) > 1) AS "audit\"""" in caplog.text ) # Case 2: The date audit should be in the inclusive range ['2025-02-01', '2025-02-01'] assert ( - f"""SELECT COUNT(*) FROM (SELECT ("date_id") AS "date_id" FROM (SELECT * FROM "sqlmesh__sqlmesh_audit"."sqlmesh_audit__date_example__{date_snapshot.version}" AS "sqlmesh_audit__date_example__{date_snapshot.version}" WHERE "date_id" BETWEEN CAST('2025-02-01' AS DATE) AND CAST('2025-02-01' AS DATE)) AS "_q_0" WHERE TRUE GROUP BY ("date_id") HAVING COUNT(*) > 1) AS "audit\"""" + f"""SELECT COUNT(*) FROM (SELECT "date_id" AS "date_id" FROM (SELECT * FROM "sqlmesh__sqlmesh_audit"."sqlmesh_audit__date_example__{date_snapshot.version}" AS "sqlmesh_audit__date_example__{date_snapshot.version}" WHERE "date_id" BETWEEN CAST('2025-02-01' AS DATE) AND CAST('2025-02-01' AS DATE)) AS "_q_0" WHERE TRUE GROUP BY "date_id" HAVING COUNT(*) > 1) AS "audit\"""" in caplog.text ) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 14c29165b7..1ba14a4aff 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -55,6 +55,7 @@ create_seed_model, create_sql_model, load_sql_based_model, + load_sql_based_models, model, ) from sqlmesh.core.model.common import parse_expression @@ -11096,3 +11097,61 @@ def test_render_query_optimize_query_false(assert_exp_eq, sushi_context): LIMIT 10 """, ) + + +def test_each_macro_with_paren_expression_arg(assert_exp_eq): + expressions = d.parse( + """ + MODEL ( + name dataset.@table_name, + kind VIEW, + blueprints ( + ( + table_name := model1, + event_columns := ( + 'value' AS property1, + 'value' AS property2 + ) + ), + ( + table_name := model2, + event_columns := ( + 'value' AS property1 + ) + ) + ), + ); + + SELECT @EACH(@event_columns, x -> x) + """ + ) + + models = load_sql_based_models(expressions, lambda _: {}) + + # Should generate 2 models from the blueprints + assert len(models) == 2 + + # Get the models sorted by name for consistent testing + model1 = next(m for m in models if "model1" in m.name) + model2 = next(m for m in models if "model2" in m.name) + + # Verify model names + assert model1.name == "dataset.model1" + assert model2.name == "dataset.model2" + + assert_exp_eq( + model1.render_query(), + """ + SELECT + 'value' AS "property1", + 'value' AS "property2" + """, + ) + + assert_exp_eq( + model2.render_query(), + """ + SELECT + 'value' AS "property1" + """, + ) From eb4c0b4d6e5f8435a082879c8862316f0e90dcdc Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 5 Aug 2025 21:45:05 -0700 Subject: [PATCH 0663/1056] Chore: Introduce Claude subagents to the repo (#5093) --- .claude/agents/code-reviewer.md | 73 +++++++++++++++++++ .claude/agents/developer.md | 110 +++++++++++++++++++++++++++++ .claude/agents/technical-writer.md | 56 +++++++++++++++ CLAUDE.md | 77 ++++++++++---------- 4 files changed, 279 insertions(+), 37 deletions(-) create mode 100644 .claude/agents/code-reviewer.md create mode 100644 .claude/agents/developer.md create mode 100644 .claude/agents/technical-writer.md diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 0000000000..85ab5be3dc --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,73 @@ +--- +name: code-reviewer +description: Use this agent PROACTIVELY when you need expert code review after writing or modifying code. This agent should be called after completing any coding task to ensure quality, architectural compliance, and catch potential issues. Examples: Context: The user has just implemented a new feature for processing SQLMesh snapshots. user: 'I just added a new method to handle snapshot fingerprinting in the Context class' assistant: 'Let me use the code-reviewer agent to analyze this implementation for potential issues and architectural compliance' Since code was just written, use the code-reviewer agent to review the implementation for quality, edge cases, and adherence to SQLMesh patterns. Context: An agent just generated a database migration script. user: 'Here's the migration I created for adding a new state table' assistant: 'Now I'll have the code-reviewer agent examine this migration for safety and best practices' Since a migration was created, use the code-reviewer agent to ensure it follows SQLMesh migration patterns and handles edge cases safely. +tools: Glob, Grep, LS, Read, NotebookRead, WebFetch, TodoWrite, WebSearch, Bash +model: sonnet +color: blue +--- + +You are an Expert Code Reviewer, a senior software engineer with deep expertise in code quality, architecture, and best practices. You NEVER write code yourself - your sole focus is providing thorough, insightful code reviews that catch issues other engineers might miss. + +Your core responsibilities: + +## Analysis Approach + +- Examine code for architectural alignment with established patterns and principles +- Identify potential edge cases, race conditions, and error scenarios +- Evaluate performance implications and scalability concerns +- Check for security vulnerabilities and data safety issues +- Assess maintainability, readability, and documentation quality +- Verify adherence to project-specific coding standards and conventions + +## Review Methodology + +- **Architectural Review**: Does the code follow established patterns? Does it fit well within the existing codebase structure? +- **Logic Analysis**: Are there logical flaws, edge cases, or scenarios that could cause failures? +- **Error Handling**: Is error handling comprehensive and appropriate? Are failure modes considered? +- **Performance Review**: Are there performance bottlenecks, inefficient algorithms, or resource leaks? +- **Security Assessment**: Are there potential security vulnerabilities or data exposure risks? +- **Maintainability Check**: Is the code readable, well-structured, and properly documented? + +### Standard Code Review Checklist + +- Code is simple and readable +- Functions, classes, and variables are well-named +- No duplicated code +- Proper error handling with specific error types +- No exposed secrets, API keys, or credentials +- Input validation and sanitization implemented +- Good test coverage including edge cases +- Performance considerations addressed +- Security best practices followed +- Documentation updated for significant changes + +## Feedback Structure + +Organize your reviews into clear categories: + +- **Critical Issues**: Problems that could cause failures, security issues, or data corruption +- **Architectural Concerns**: Deviations from established patterns or design principles +- **Edge Cases**: Scenarios that might not be handled properly +- **Performance Considerations**: Potential bottlenecks or inefficiencies +- **Maintainability Improvements**: Suggestions for better code organization or documentation +- **Documentation**: Suggestions to update documentation for significant changes + +## Communication Style + +- Be constructive and specific in your feedback +- Explain the 'why' behind your suggestions, not just the 'what' +- Prioritize issues by severity and impact +- Acknowledge good practices when you see them +- Provide context for your recommendations +- Ask clarifying questions when code intent is unclear + +## Important Constraints + +- You NEVER write, modify, or suggest specific code implementations +- You focus purely on analysis and high-level guidance +- You always consider the broader system context and existing codebase patterns +- You escalate concerns about fundamental architectural decisions +- You validate that solutions align with project requirements and constraints + +When reviewing code, assume you're looking at recently written code unless explicitly told otherwise. Focus on providing actionable insights that help improve code quality while respecting the existing architectural decisions and project constraints. + diff --git a/.claude/agents/developer.md b/.claude/agents/developer.md new file mode 100644 index 0000000000..3a9f32d6c4 --- /dev/null +++ b/.claude/agents/developer.md @@ -0,0 +1,110 @@ +--- +name: developer +description: Use this agent PROACTIVELY when you need to understand the user's task, read GitHub issues, implement new features, write comprehensive tests, refactor existing code, fix bugs, or make any code changes that require deep understanding of the project's architecture and coding standards. Examples: Context: User wants to add a new SQL dialect adapter to SQLMesh. user: 'I need to implement support for Oracle database in SQLMesh' assistant: 'I'll use the software-engineer agent to implement the Oracle adapter following SQLMesh's engine adapter patterns' Since this requires implementing a new feature with proper architecture understanding, use the software-engineer agent. Context: User discovers a bug in the migration system. user: 'The migration v0084 is failing on MySQL due to field size limits' assistant: 'Let me use the software-engineer agent to investigate and fix this migration issue' This requires debugging and fixing code while understanding SQLMesh's migration patterns, so use the software-engineer agent. Context: User needs comprehensive tests for a new feature. user: 'I just implemented a new snapshot fingerprinting algorithm and need tests' assistant: 'I'll use the software-engineer agent to write comprehensive tests following SQLMesh's testing patterns' Writing thorough tests requires understanding the codebase architecture and testing conventions, so use the software-engineer agent. +model: sonnet +color: red +--- + +You are an expert software engineer with deep expertise in Python, SQL, data engineering, and modern software development practices. You specialize in working with complex codebases like SQLMesh, understanding architectural patterns, and implementing robust, well-tested solutions. + +Your core responsibilities: + +# Project-Specific Expertise + +- Understand SQLMesh's core concepts: virtual environments, fingerprinting, snapshots, plans. You can find documentation in the ./docs folder +- Implement engine adapters following the established 16+ engine pattern +- Handle state sync and migration patterns correctly +- Support dbt integration requirements when relevant + +# Problem-Solving Approach + +1. Analyze the existing codebase to understand patterns and conventions +2. Come up with an implementation plan; identify edge cases and trade-offs; request feedback and ask clarifying questions +3. IMPORTANT: Write comprehensive tests covering normal and edge cases BEFORE you write any implementation code. It's expected for these tests to fail at first, the implementation should then ensure that the tests are passing +4. Confirm that the written tests cover the full scope of the work that has been requested +5. Identify the most appropriate location for new code based on architecture +6. Study similar existing implementations as reference +7. Implement following established patterns and best practices +8. Validate code quality with style checks +9. Consider backward compatibility and migration needs especially when the persistent state + +# Implementation Best Practices + +## Code Implementation + +- Write clean, maintainable, and performant code following established patterns +- Implement new features by studying existing similar implementations first +- Follow the project's architectural principles and design patterns +- Use appropriate abstractions and avoid code duplication +- Ensure cross-platform compatibility (Windows/Linux/macOS) + +## Testing Best Practices + +- Write comprehensive tests using pytest with appropriate markers (fast/slow/engine-specific) +- Follow the project's testing philosophy: fast tests for development, comprehensive coverage for CI +- Use existing test utilities `assert_exp_eq` and others for validation when appropriate +- Test edge cases, error conditions, and cross-engine compatibility +- Use existing tests in the same module as a reference for new tests +- Write an integration test(s) that runs against the `sushi` project when the scope of feature touches multiple decoupled components +- Only add tests within the `tests/` folder. Prefer adding tests to existing modules over creating new files +- Tests are marked with pytest markers: + - **Type markers**: `fast`, `slow`, `docker`, `remote`, `cicdonly`, `isolated`, `registry_isolation` + - **Domain markers**: `cli`, `dbt`, `github`, `jupyter`, `web` + - **Engine markers**: `engine`, `athena`, `bigquery`, `clickhouse`, `databricks`, `duckdb`, `motherduck`, `mssql`, `mysql`, `postgres`, `redshift`, `snowflake`, `spark`, `trino`, `risingwave` +- Default to `fast` tests during development +- Engine tests use real connections when available, mocks otherwise +- The `sushi` example project is used extensively in tests +- Use `DuckDBMetadata` helper for validating table metadata in tests + +## Code Quality Standards + +- Python: Black formatting, isort for imports, mypy for type checking, Ruff for linting +- TypeScript/React: ESLint + Prettier configuration +- All style checks run via `make style` +- Pre-commit hooks enforce all style rules automatically +- Important: Some modules (duckdb, numpy, pandas) are banned at module level to prevent import-time side effects +- Write clear docstrings and comments for complex logic but avoid comments that are too frequent or state overly obvious details +- Make sure there are no trailing whitespaces in edited files + +## Writing Functions / Methods Best Practices + +When evaluating whether a function you implemented is good or not, use this checklist: + +1. Can you read the function and easily follow what it's doing? If yes, then stop here +2. Does the function have very high cyclomatic complexity? (number of independent paths, or, in a lot of cases, number of nesting if if-else as a proxy). If it does, then it likely needs to be rewritten +2. Are the arguments and return values annotated with the correct types? +3. Are there any common data structures and algorithms that would make this function much easier to follow and more robust? +4. Are there any unused parameters in the function? +5. Are there any unnecessary type casts that can be moved to function arguments? +6. Is the function easily testable without mocking core features? If not, can this function be tested as part of an integration test? +7. Does it have any hidden untested dependencies or any values that can be factored out into the arguments instead? Only care about non-trivial dependencies that can actually change or affect the function +8. Brainstorm 3 better function names and see if the current name is the best, consistent with rest of codebase + +IMPORTANT: you SHOULD NOT refactor out a separate function unless there is a compelling need, such as: +- the refactored function is used in more than one place +- the refactored function is easily unit testable while the original function is not AND you can't test it any other way +- the original function is extremely hard to follow and you resort to putting comments everywhere just to explain it + +## Using Git + +- Use Conventional Commits format when writing commit messages: https://www.conventionalcommits.org/en/v1.0.0 + +# Communication + +- Be concise and to the point +- Explain your architectural decisions and reasoning +- Highlight any potential breaking changes or migration requirements +- Suggest related improvements or refactoring opportunities +- Document complex algorithms or business logic clearly + +# Common Pitfalls + +1. **Engine Tests**: Many tests require specific database credentials or Docker. Check test markers before running. +2. **Path Handling**: Be careful with Windows paths - use `pathlib.Path` for cross-platform compatibility. +3. **State Management**: Understanding the state sync mechanism is crucial for debugging environment issues. +4. **Snapshot Versioning**: Changes to model logic create new versions - this is by design for safe deployments. +5. **Module Imports**: Avoid importing duckdb, numpy, or pandas at module level - these are banned by Ruff to prevent long load times in cases where the libraries aren't used. +6. **Import And Attribute Errors**: If the code raises `ImportError` or `AttributeError` try running the `make install-dev` command first to make sure all dependencies are up to date + +When implementing features, always consider the broader impact on the system, ensure proper error handling, and maintain the high code quality standards established in the project. Your implementations should be production-ready and align with SQLMesh's philosophy of safe, reliable data transformations. + diff --git a/.claude/agents/technical-writer.md b/.claude/agents/technical-writer.md new file mode 100644 index 0000000000..7e8be9b928 --- /dev/null +++ b/.claude/agents/technical-writer.md @@ -0,0 +1,56 @@ +--- +name: technical-writer +description: Use this agent PROACTIVELY when you need to create, update, or maintain technical documentation for SQLMesh. Examples include: writing user guides for virtual environments, creating API documentation for new features, updating existing docs after code changes, writing deep-dive technical explanations of core concepts like fingerprinting or state sync, creating migration guides for users upgrading between versions, or documenting new engine adapter implementations. This agent should be used proactively when code changes affect user-facing functionality or when new features need documentation. +model: sonnet +color: white +--- + +You are a Technical Documentation Specialist with deep expertise in SQLMesh's architecture, concepts, and codebase. You possess comprehensive knowledge of data transformation frameworks, SQL engines, and developer tooling, combined with exceptional technical writing skills. + +Your core responsibilities: + +## Documentation Maintenance & Creation + +- Maintain existing documentation by identifying outdated content, broken links, and missing information +- Create new documentation pages that align with SQLMesh's documentation structure and style +- Ensure all documentation follows consistent formatting, terminology, and organizational patterns +- Update documentation proactively when code changes affect user-facing functionality + +### Editing + +- When editing files make sure to not leave any whitespaces + +## Multi-Audience Writing + +- Write clear, accessible guides for less technical users (data analysts, business users) focusing on practical workflows and concepts +- Create comprehensive deep-dives for technical users (data engineers, platform engineers) covering architecture, implementation details, and advanced configurations +- Adapt your writing style, depth, and examples based on the target audience's technical expertise + +## SQLMesh Expertise + +- Demonstrate deep understanding of SQLMesh's core concepts: virtual environments, fingerprinting, state sync, plan/apply workflows, incremental processing, and multi-dialect support +- Accurately explain complex technical concepts like model versioning, virtual data environments, state migration, and data intervals +- Reference appropriate code examples from the codebase when illustrating concepts +- Understand the relationship between SQLMesh components and how they work together + +## Quality Standards + +- Ensure technical accuracy by cross-referencing code implementation and existing documentation +- Include practical examples, code snippets, and real-world use cases +- Structure content with clear headings, bullet points, and logical flow +- Provide troubleshooting guidance and common pitfall warnings where relevant +- Include relevant CLI commands, configuration examples, and best practices + +## Documentation Types You Excel At + +- User guides and tutorials for specific workflows +- API documentation and reference materials +- Architecture explanations and system overviews +- Migration guides and upgrade instructions +- Troubleshooting guides and FAQ sections +- Integration guides for external tools and systems + +When creating documentation, always consider the user's journey and provide the right level of detail for their needs. For less technical users, focus on what they need to accomplish and provide step-by-step guidance. For technical users, include implementation details, configuration options, and architectural context. Always validate technical accuracy against the actual codebase and existing documentation patterns. + +IMPORTANT: You SHOULD NEVER edit any code. Make sure you only change files in the `docs/` folder. + diff --git a/CLAUDE.md b/CLAUDE.md index 23a72bd371..a7f86098d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,42 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Agent-Based Development Workflow + +Every time the user requests a feature or bug fix, you MUST follow the process below: + +### Development Process + +1. **Understanding The Task**: Use the `developer` agent to understand what the user is asking for and to read GitHub issues +2. **Feature Development & Bug Fixes**: Use the `developer` agent for implementing features and fixing bugs. IMPORTANT: Always begin by writing a failing test (or tests) that reflects the expected behavior +3. **Code Review**: After development work, invoke the `code-reviewer` agent to review the implementation +4. **Iteration**: Use the `developer` agent again to address feedback from the code reviewer +5. **Repeat**: Continue the developer → code-reviewer cycle until no more feedback remains +6. **Documentation**: If the feature or bug fix requires documentation updates, invoke the `technical-writer` agent + +IMPORTANT: Make sure to share the project overview, architecture overview, and other concepts outlined below with the agent when it is invoked. + +### Agent Responsibilities + +**Developer Agent**: +- Understands a feature request or a reported issue +- Implements new features following SQLMesh's architecture patterns +- Fixes bugs with proper understanding of the codebase +- Writes comprehensive tests following SQLMesh's testing conventions +- Follows established code style and conventions + +**Code-Reviewer Agent**: +- Reviews implementation for quality and architectural compliance +- Identifies potential issues, edge cases, and improvements +- Ensures adherence to SQLMesh patterns and best practices +- Validates test coverage and quality + +**Technical-Writer Agent**: +- Creates and updates user-facing documentation +- Writes API documentation for new features +- Updates existing docs after code changes +- Creates migration guides and deep-dive technical explanations + ## Project Overview SQLMesh is a next-generation data transformation framework that enables: @@ -18,8 +54,8 @@ SQLMesh is a next-generation data transformation framework that enables: ### Environment setup ```bash # Create and activate a Python virtual environment (Python >= 3.9, < 3.13) -python -m venv venv -source venv/bin/activate # On Windows: venv\Scripts\activate +python -m venv .venv +source ./.venv/bin/activate # On Windows: .venv\Scripts\activate # Install development dependencies make install-dev @@ -99,27 +135,6 @@ make ui-down # Stop UI 4. **Intervals**: Time-based partitioning system for incremental models, tracking what data has been processed. -## Testing Philosophy - -- Tests are marked with pytest markers: - - **Type markers**: `fast`, `slow`, `docker`, `remote`, `cicdonly`, `isolated`, `registry_isolation` - - **Domain markers**: `cli`, `dbt`, `github`, `jupyter`, `web` - - **Engine markers**: `engine`, `athena`, `bigquery`, `clickhouse`, `databricks`, `duckdb`, `motherduck`, `mssql`, `mysql`, `postgres`, `redshift`, `snowflake`, `spark`, `trino`, `risingwave` -- Default to `fast` tests during development -- Engine tests use real connections when available, mocks otherwise -- The `sushi` example project is used extensively in tests -- Use `DuckDBMetadata` helper for validating table metadata in tests -- Tests run in parallel by default (`pytest -n auto`) - -## Code Style Guidelines - -- Python: Black formatting, isort for imports, mypy for type checking, Ruff for linting -- TypeScript/React: ESLint + Prettier configuration -- SQL: SQLGlot handles parsing/formatting -- All style checks run via `make style` -- Pre-commit hooks enforce all style rules automatically -- Important: Some modules (duckdb, numpy, pandas) are banned at module level to prevent import-time side effects - ## Important Files - `sqlmesh/core/context.py`: Main orchestration class @@ -128,18 +143,6 @@ make ui-down # Stop UI - `web/client/src/App.tsx`: Web UI frontend entry point - `vscode/extension/src/extension.ts`: VSCode extension entry point -## Common Pitfalls - -1. **Engine Tests**: Many tests require specific database credentials or Docker. Check test markers before running. - -2. **Path Handling**: Be careful with Windows paths - use `pathlib.Path` for cross-platform compatibility. - -3. **State Management**: Understanding the state sync mechanism is crucial for debugging environment issues. - -4. **Snapshot Versioning**: Changes to model logic create new versions - this is by design for safe deployments. - -5. **Module Imports**: Avoid importing duckdb, numpy, or pandas at module level - these are banned by Ruff to prevent long load times in cases where the libraries aren't used. - ## GitHub CI/CD Bot Architecture SQLMesh includes a GitHub CI/CD bot integration that automates data transformation workflows. The implementation is located in `sqlmesh/integrations/github/` and follows a clean architectural pattern. @@ -282,7 +285,7 @@ engine_adapter.drop_table(table_name) 1. Version comparison (local vs remote schema) 2. Backup creation of state tables 3. Sequential migration execution (numerical order) -4. Snapshot fingerprint recalculation if needed +4. Snapshot fingerprint recalculation if needed 5. Environment updates with new snapshot references ## dbt Integration @@ -348,4 +351,4 @@ When using dbt with SQLMesh, you gain: - **Plan/Apply Workflow**: Safe deployments with change previews - **Multi-Dialect Support**: Run the same dbt project across different SQL engines - **Advanced Testing**: Enhanced testing capabilities beyond standard dbt tests -- **State Management**: Sophisticated metadata and versioning system \ No newline at end of file +- **State Management**: Sophisticated metadata and versioning system From adf6a6815d0184e2e7b5529a421268f9fcdf224c Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 6 Aug 2025 13:00:15 +0300 Subject: [PATCH 0664/1056] Fix!: Dont normalize aliases in merge and when matched (#5014) --- sqlmesh/core/dialect.py | 12 +- sqlmesh/core/model/kind.py | 10 +- .../integration/test_integration.py | 129 +++++++++++++++++- tests/core/test_model.py | 48 +++---- tests/core/test_snapshot_evaluator.py | 66 ++++----- tests/dbt/test_config.py | 2 +- 6 files changed, 184 insertions(+), 83 deletions(-) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index f5464e12bc..37568f2b27 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -1421,18 +1421,10 @@ def replace_merge_table_aliases( """ from sqlmesh.core.engine_adapter.base import MERGE_SOURCE_ALIAS, MERGE_TARGET_ALIAS - normalized_merge_source_alias = quote_identifiers( - normalize_identifiers(exp.to_identifier(MERGE_SOURCE_ALIAS), dialect), dialect=dialect - ) - - normalized_merge_target_alias = quote_identifiers( - normalize_identifiers(exp.to_identifier(MERGE_TARGET_ALIAS), dialect), dialect=dialect - ) - if isinstance(expression, exp.Column) and (first_part := expression.parts[0]): if first_part.this.lower() in ("target", "dbt_internal_dest", "__merge_target__"): - first_part.replace(normalized_merge_target_alias) + first_part.replace(exp.to_identifier(MERGE_TARGET_ALIAS, quoted=True)) elif first_part.this.lower() in ("source", "dbt_internal_source", "__merge_source__"): - first_part.replace(normalized_merge_source_alias) + first_part.replace(exp.to_identifier(MERGE_SOURCE_ALIAS, quoted=True)) return expression diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index 9dc54f4b83..c297d916d5 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -478,10 +478,9 @@ def _when_matched_validator( v = v[1:-1] v = t.cast(exp.Whens, d.parse_one(v, into=exp.Whens, dialect=dialect)) - else: - v = t.cast(exp.Whens, v.transform(d.replace_merge_table_aliases, dialect=dialect)) - return validate_expression(v, dialect=dialect) + v = validate_expression(v, dialect=dialect) + return t.cast(exp.Whens, v.transform(d.replace_merge_table_aliases, dialect=dialect)) @field_validator("merge_filter", mode="before") def _merge_filter_validator( @@ -497,10 +496,9 @@ def _merge_filter_validator( if isinstance(v, str): v = v.strip() v = d.parse_one(v, dialect=dialect) - else: - v = v.transform(d.replace_merge_table_aliases, dialect=dialect) - return validate_expression(v, dialect=dialect) + v = validate_expression(v, dialect=dialect) + return v.transform(d.replace_merge_table_aliases, dialect=dialect) @property def data_hash_values(self) -> t.List[t.Optional[str]]: diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index cb09d20537..80ce7ac18d 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -24,7 +24,7 @@ from sqlmesh.core.dialect import select_from_values from sqlmesh.core.model import Model, load_sql_based_model from sqlmesh.core.engine_adapter.shared import DataObject, DataObjectType -from sqlmesh.core.engine_adapter.mixins import RowDiffMixin +from sqlmesh.core.engine_adapter.mixins import RowDiffMixin, LogicalMergeMixin from sqlmesh.core.model.definition import create_sql_model from sqlmesh.core.plan import Plan from sqlmesh.core.state_sync.db import EngineAdapterStateSync @@ -1897,6 +1897,133 @@ def _mutate_config(current_gateway_name: str, config: Config): ctx.cleanup(context) +def test_incremental_by_unique_key_model_when_matched(ctx: TestContext): + if not ctx.supports_merge: + pytest.skip(f"{ctx.dialect} on {ctx.gateway} doesnt support merge") + + # DuckDB and some other engines use logical_merge which doesn't support when_matched + if isinstance(ctx.engine_adapter, LogicalMergeMixin): + pytest.skip( + f"{ctx.dialect} on {ctx.gateway} uses logical merge which doesn't support when_matched" + ) + + def _mutate_config(current_gateway_name: str, config: Config): + connection = config.gateways[current_gateway_name].connection + connection.concurrent_tasks = 1 + if current_gateway_name == "inttest_redshift": + connection.enable_merge = True + + context = ctx.create_context(_mutate_config) + schema = ctx.schema(TEST_SCHEMA) + + # Create seed data with multiple days + seed_query = ctx.input_data( + pd.DataFrame( + [ + [1, "item_a", 100, "2020-01-01"], + [2, "item_b", 200, "2020-01-01"], + [1, "item_a_changed", 150, "2020-01-02"], # Same item_id, different name and value + [2, "item_b_changed", 250, "2020-01-02"], # Same item_id, different name and value + [3, "item_c", 300, "2020-01-02"], # New item on day 2 + ], + columns=["item_id", "name", "value", "event_date"], + ), + columns_to_types={ + "item_id": exp.DataType.build("integer"), + "name": exp.DataType.build("text"), + "value": exp.DataType.build("integer"), + "event_date": exp.DataType.build("date"), + }, + ) + context.upsert_model( + create_sql_model(name=f"{schema}.seed_model", query=seed_query, kind="FULL") + ) + + table_format = "" + if ctx.dialect == "athena": + # INCREMENTAL_BY_UNIQUE_KEY uses MERGE which is only supported in Athena on Iceberg tables + table_format = "table_format iceberg," + + # Create model with when_matched clause that only updates the value column + # BUT keeps the existing name column unchanged + # batch_size=1 is so that we trigger merge on second batch and verify behaviour of when_matched + context.upsert_model( + load_sql_based_model( + d.parse( + f"""MODEL ( + name {schema}.test_model_when_matched, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key item_id, + batch_size 1, + merge_filter source.event_date > target.event_date, + when_matched WHEN MATCHED THEN UPDATE SET target.value = source.value, target.event_date = source.event_date + ), + {table_format} + start '2020-01-01', + end '2020-01-02', + cron '@daily' + ); + + select item_id, name, value, event_date + from {schema}.seed_model + where event_date between @start_date and @end_date""", + ) + ) + ) + + try: + # Initial plan to create the model and run it + context.plan(auto_apply=True, no_prompts=True) + + test_model = context.get_model(f"{schema}.test_model_when_matched") + + # Verify that the model has the when_matched clause and merge_filter + assert test_model.kind.when_matched is not None + assert ( + test_model.kind.when_matched.sql() + == '(WHEN MATCHED THEN UPDATE SET "__MERGE_TARGET__"."value" = "__MERGE_SOURCE__"."value", "__MERGE_TARGET__"."event_date" = "__MERGE_SOURCE__"."event_date")' + ) + assert test_model.merge_filter is not None + assert ( + test_model.merge_filter.sql() + == '"__MERGE_SOURCE__"."event_date" > "__MERGE_TARGET__"."event_date"' + ) + + actual_df = ( + ctx.get_current_data(test_model.fqn).sort_values(by="item_id").reset_index(drop=True) + ) + + # Expected results after batch processing: + # - Day 1: Items 1 and 2 are inserted (first insert) + # - Day 2: Items 1 and 2 are merged (when_matched clause preserves names but updates values/dates) + # Item 3 is inserted as new + expected_df = ( + pd.DataFrame( + [ + [1, "item_a", 150, "2020-01-02"], # name from day 1, value and date from day 2 + [2, "item_b", 250, "2020-01-02"], # name from day 1, value and date from day 2 + [3, "item_c", 300, "2020-01-02"], # new item from day 2 + ], + columns=["item_id", "name", "value", "event_date"], + ) + .sort_values(by="item_id") + .reset_index(drop=True) + ) + + # Convert date columns to string for comparison + actual_df["event_date"] = actual_df["event_date"].astype(str) + expected_df["event_date"] = expected_df["event_date"].astype(str) + + pd.testing.assert_frame_equal( + actual_df, + expected_df, + check_dtype=False, + ) + + finally: + ctx.cleanup(context) + + def test_managed_model_upstream_forward_only(ctx: TestContext): """ This scenario goes as follows: diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 1ba14a4aff..3cadbae9ca 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -5480,7 +5480,7 @@ def test_when_matched(): """ ) - expected_when_matched = "(WHEN MATCHED THEN UPDATE SET `__merge_target__`.`salary` = COALESCE(`__merge_source__`.`salary`, `__merge_target__`.`salary`))" + expected_when_matched = "(WHEN MATCHED THEN UPDATE SET `__MERGE_TARGET__`.`salary` = COALESCE(`__MERGE_SOURCE__`.`salary`, `__MERGE_TARGET__`.`salary`))" model = load_sql_based_model(expressions, dialect="hive") assert model.kind.when_matched.sql(dialect="hive") == expected_when_matched @@ -5514,9 +5514,9 @@ def test_when_matched(): kind INCREMENTAL_BY_UNIQUE_KEY ( unique_key ("purchase_order_id"), when_matched ( - WHEN MATCHED AND "__merge_source__"."_operation" = 1 THEN DELETE - WHEN MATCHED AND "__merge_source__"."_operation" <> 1 THEN UPDATE SET - "__merge_target__"."purchase_order_id" = 1 + WHEN MATCHED AND "__MERGE_SOURCE__"."_operation" = 1 THEN DELETE + WHEN MATCHED AND "__MERGE_SOURCE__"."_operation" <> 1 THEN UPDATE SET + "__MERGE_TARGET__"."purchase_order_id" = 1 ), batch_concurrency 1, forward_only FALSE, @@ -5567,7 +5567,7 @@ def fingerprint_merge( kind INCREMENTAL_BY_UNIQUE_KEY ( unique_key ("purchase_order_id"), when_matched ( - WHEN MATCHED AND "__merge_source__"."salary" <> "__merge_target__"."salary" THEN UPDATE SET + WHEN MATCHED AND "__MERGE_SOURCE__"."salary" <> "__MERGE_TARGET__"."salary" THEN UPDATE SET ARRAY('target.update_datetime = source.update_datetime', 'target.salary = source.salary') ), batch_concurrency 1, @@ -5601,8 +5601,8 @@ def test_when_matched_multiple(): ) expected_when_matched = [ - "WHEN MATCHED AND `__merge_source__`.`x` = 1 THEN UPDATE SET `__merge_target__`.`salary` = COALESCE(`__merge_source__`.`salary`, `__merge_target__`.`salary`)", - "WHEN MATCHED THEN UPDATE SET `__merge_target__`.`salary` = COALESCE(`__merge_source__`.`salary`, `__merge_target__`.`salary`)", + "WHEN MATCHED AND `__MERGE_SOURCE__`.`x` = 1 THEN UPDATE SET `__MERGE_TARGET__`.`salary` = COALESCE(`__MERGE_SOURCE__`.`salary`, `__MERGE_TARGET__`.`salary`)", + "WHEN MATCHED THEN UPDATE SET `__MERGE_TARGET__`.`salary` = COALESCE(`__MERGE_SOURCE__`.`salary`, `__MERGE_TARGET__`.`salary`)", ] model = load_sql_based_model(expressions, dialect="hive", variables={"schema": "db"}) @@ -5643,13 +5643,13 @@ def test_when_matched_merge_filter_multi_part_columns(): ) expected_when_matched = [ - "WHEN MATCHED AND `__merge_source__`.`record`.`nested_record`.`field` = 1 THEN UPDATE SET `__merge_target__`.`repeated_record`.`sub_repeated_record`.`sub_field` = COALESCE(`__merge_source__`.`repeated_record`.`sub_repeated_record`.`sub_field`, `__merge_target__`.`repeated_record`.`sub_repeated_record`.`sub_field`)", - "WHEN MATCHED THEN UPDATE SET `__merge_target__`.`repeated_record`.`sub_repeated_record`.`sub_field` = COALESCE(`__merge_source__`.`repeated_record`.`sub_repeated_record`.`sub_field`, `__merge_target__`.`repeated_record`.`sub_repeated_record`.`sub_field`)", + "WHEN MATCHED AND `__MERGE_SOURCE__`.`record`.`nested_record`.`field` = 1 THEN UPDATE SET `__MERGE_TARGET__`.`repeated_record`.`sub_repeated_record`.`sub_field` = COALESCE(`__MERGE_SOURCE__`.`repeated_record`.`sub_repeated_record`.`sub_field`, `__MERGE_TARGET__`.`repeated_record`.`sub_repeated_record`.`sub_field`)", + "WHEN MATCHED THEN UPDATE SET `__MERGE_TARGET__`.`repeated_record`.`sub_repeated_record`.`sub_field` = COALESCE(`__MERGE_SOURCE__`.`repeated_record`.`sub_repeated_record`.`sub_field`, `__MERGE_TARGET__`.`repeated_record`.`sub_repeated_record`.`sub_field`)", ] expected_merge_filter = ( - "`__merge_source__`.`record`.`nested_record`.`field` < `__merge_target__`.`record`.`nested_record`.`field` AND " - "`__merge_target__`.`repeated_record`.`sub_repeated_record`.`sub_field` > `__merge_source__`.`repeated_record`.`sub_repeated_record`.`sub_field`" + "`__MERGE_SOURCE__`.`record`.`nested_record`.`field` < `__MERGE_TARGET__`.`record`.`nested_record`.`field` AND " + "`__MERGE_TARGET__`.`repeated_record`.`sub_repeated_record`.`sub_field` > `__MERGE_SOURCE__`.`repeated_record`.`sub_repeated_record`.`sub_field`" ) model = load_sql_based_model(expressions, dialect="bigquery", variables={"schema": "db"}) @@ -6679,7 +6679,7 @@ def test_unrendered_macros_sql_model(mocker: MockerFixture) -> None: assert model.unique_key[0] == exp.column("a", quoted=True) assert ( t.cast(exp.Expression, model.merge_filter).sql() - == '"__merge_source__"."id" > 0 AND "__merge_target__"."updated_at" < @end_ds AND "__merge_source__"."updated_at" > @start_ds AND @merge_filter_var' + == '"__MERGE_SOURCE__"."id" > 0 AND "__MERGE_TARGET__"."updated_at" < @end_ds AND "__MERGE_SOURCE__"."updated_at" > @start_ds AND @merge_filter_var' ) @@ -6775,7 +6775,7 @@ def model_with_macros(evaluator, **kwargs): assert python_sql_model.unique_key[0] == exp.column("a", quoted=True) assert ( python_sql_model.merge_filter.sql() - == '"__merge_source__"."id" > 0 AND "__merge_target__"."updated_at" < @end_ds AND "__merge_source__"."updated_at" > @start_ds AND @merge_filter_var' + == '"__MERGE_SOURCE__"."id" > 0 AND "__MERGE_TARGET__"."updated_at" < @end_ds AND "__MERGE_SOURCE__"."updated_at" > @start_ds AND @merge_filter_var' ) @@ -7862,7 +7862,7 @@ def test_model_kind_to_expression(): .sql() == """INCREMENTAL_BY_UNIQUE_KEY ( unique_key ("a"), -when_matched (WHEN MATCHED THEN UPDATE SET "__merge_target__"."b" = COALESCE("__merge_source__"."b", "__merge_target__"."b")), +when_matched (WHEN MATCHED THEN UPDATE SET "__MERGE_TARGET__"."b" = COALESCE("__MERGE_SOURCE__"."b", "__MERGE_TARGET__"."b")), batch_concurrency 1, forward_only FALSE, disable_restatement FALSE, @@ -7890,7 +7890,7 @@ def test_model_kind_to_expression(): .sql() == """INCREMENTAL_BY_UNIQUE_KEY ( unique_key ("a"), -when_matched (WHEN MATCHED AND "__merge_source__"."x" = 1 THEN UPDATE SET "__merge_target__"."b" = COALESCE("__merge_source__"."b", "__merge_target__"."b") WHEN MATCHED THEN UPDATE SET "__merge_target__"."b" = COALESCE("__merge_source__"."b", "__merge_target__"."b")), +when_matched (WHEN MATCHED AND "__MERGE_SOURCE__"."x" = 1 THEN UPDATE SET "__MERGE_TARGET__"."b" = COALESCE("__MERGE_SOURCE__"."b", "__MERGE_TARGET__"."b") WHEN MATCHED THEN UPDATE SET "__MERGE_TARGET__"."b" = COALESCE("__MERGE_SOURCE__"."b", "__MERGE_TARGET__"."b")), batch_concurrency 1, forward_only FALSE, disable_restatement FALSE, @@ -8151,7 +8151,7 @@ def test_merge_filter(): """ ) - expected_incremental_predicate = f"`{MERGE_SOURCE_ALIAS.lower()}`.`salary` > 0" + expected_incremental_predicate = f"`{MERGE_SOURCE_ALIAS}`.`salary` > 0" model = load_sql_based_model(expressions, dialect="hive") assert model.kind.merge_filter.sql(dialect="hive") == expected_incremental_predicate @@ -8194,19 +8194,19 @@ def test_merge_filter(): kind INCREMENTAL_BY_UNIQUE_KEY ( unique_key ("purchase_order_id"), when_matched ( - WHEN MATCHED AND "{MERGE_SOURCE_ALIAS.lower()}"."_operation" = 1 THEN DELETE - WHEN MATCHED AND "{MERGE_SOURCE_ALIAS.lower()}"."_operation" <> 1 THEN UPDATE SET - "{MERGE_TARGET_ALIAS.lower()}"."purchase_order_id" = 1 + WHEN MATCHED AND "{MERGE_SOURCE_ALIAS}"."_operation" = 1 THEN DELETE + WHEN MATCHED AND "{MERGE_SOURCE_ALIAS}"."_operation" <> 1 THEN UPDATE SET + "{MERGE_TARGET_ALIAS}"."purchase_order_id" = 1 ), merge_filter ( - "{MERGE_SOURCE_ALIAS.lower()}"."ds" > ( + "{MERGE_SOURCE_ALIAS}"."ds" > ( SELECT MAX("ds") FROM "db"."test" ) - AND "{MERGE_SOURCE_ALIAS.lower()}"."ds" > @start_ds - AND "{MERGE_SOURCE_ALIAS.lower()}"."_operation" <> 1 - AND "{MERGE_TARGET_ALIAS.lower()}"."start_date" > CURRENT_DATE + INTERVAL '7' DAY + AND "{MERGE_SOURCE_ALIAS}"."ds" > @start_ds + AND "{MERGE_SOURCE_ALIAS}"."_operation" <> 1 + AND "{MERGE_TARGET_ALIAS}"."start_date" > CURRENT_DATE + INTERVAL '7' DAY ), batch_concurrency 1, forward_only FALSE, @@ -8224,7 +8224,7 @@ def test_merge_filter(): rendered_merge_filters = model.render_merge_filter(start="2023-01-01", end="2023-01-02") assert ( rendered_merge_filters.sql(dialect="hive") - == "(`__merge_source__`.`ds` > (SELECT MAX(`ds`) FROM `db`.`test`) AND `__merge_source__`.`ds` > '2023-01-01' AND `__merge_source__`.`_operation` <> 1 AND `__merge_target__`.`start_date` > CURRENT_DATE + INTERVAL '7' DAY)" + == "(`__MERGE_SOURCE__`.`ds` > (SELECT MAX(`ds`) FROM `db`.`test`) AND `__MERGE_SOURCE__`.`ds` > '2023-01-01' AND `__MERGE_SOURCE__`.`_operation` <> 1 AND `__MERGE_TARGET__`.`start_date` > CURRENT_DATE + INTERVAL '7' DAY)" ) diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index b01daf9e20..c96ddf6e56 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -2200,18 +2200,14 @@ def test_create_incremental_by_unique_key_updated_at_exp(adapter_mock, make_snap source=False, then=exp.Update( expressions=[ - exp.column("name", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( - exp.column("name", MERGE_SOURCE_ALIAS.lower(), quoted=True) + exp.column("name", MERGE_TARGET_ALIAS, quoted=True).eq( + exp.column("name", MERGE_SOURCE_ALIAS, quoted=True) ), - exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( + exp.column("updated_at", MERGE_TARGET_ALIAS, quoted=True).eq( exp.Coalesce( - this=exp.column( - "updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True - ), + this=exp.column("updated_at", MERGE_SOURCE_ALIAS, quoted=True), expressions=[ - exp.column( - "updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True - ) + exp.column("updated_at", MERGE_TARGET_ALIAS, quoted=True) ], ) ), @@ -2269,23 +2265,19 @@ def test_create_incremental_by_unique_key_multiple_updated_at_exp(adapter_mock, expressions=[ exp.When( matched=True, - condition=exp.column("id", MERGE_SOURCE_ALIAS.lower(), quoted=True).eq( + condition=exp.column("id", MERGE_SOURCE_ALIAS, quoted=True).eq( exp.Literal.number(1) ), then=exp.Update( expressions=[ - exp.column("name", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( - exp.column("name", MERGE_SOURCE_ALIAS.lower(), quoted=True) + exp.column("name", MERGE_TARGET_ALIAS, quoted=True).eq( + exp.column("name", MERGE_SOURCE_ALIAS, quoted=True) ), - exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( + exp.column("updated_at", MERGE_TARGET_ALIAS, quoted=True).eq( exp.Coalesce( - this=exp.column( - "updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True - ), + this=exp.column("updated_at", MERGE_SOURCE_ALIAS, quoted=True), expressions=[ - exp.column( - "updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True - ) + exp.column("updated_at", MERGE_TARGET_ALIAS, quoted=True) ], ) ), @@ -2297,18 +2289,14 @@ def test_create_incremental_by_unique_key_multiple_updated_at_exp(adapter_mock, source=False, then=exp.Update( expressions=[ - exp.column("name", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( - exp.column("name", MERGE_SOURCE_ALIAS.lower(), quoted=True) + exp.column("name", MERGE_TARGET_ALIAS, quoted=True).eq( + exp.column("name", MERGE_SOURCE_ALIAS, quoted=True) ), - exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( + exp.column("updated_at", MERGE_TARGET_ALIAS, quoted=True).eq( exp.Coalesce( - this=exp.column( - "updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True - ), + this=exp.column("updated_at", MERGE_SOURCE_ALIAS, quoted=True), expressions=[ - exp.column( - "updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True - ) + exp.column("updated_at", MERGE_TARGET_ALIAS, quoted=True) ], ) ), @@ -2395,16 +2383,16 @@ def test_create_incremental_by_unique_key_merge_filter(adapter_mock, make_snapsh assert model.merge_filter == exp.And( this=exp.And( this=exp.GT( - this=exp.column("id", MERGE_SOURCE_ALIAS.lower(), quoted=True), + this=exp.column("id", MERGE_SOURCE_ALIAS, quoted=True), expression=exp.Literal(this="0", is_string=False), ), expression=exp.LT( - this=exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True), + this=exp.column("updated_at", MERGE_TARGET_ALIAS, quoted=True), expression=d.MacroVar(this="end_ds"), ), ), expression=exp.GT( - this=exp.column("updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True), + this=exp.column("updated_at", MERGE_SOURCE_ALIAS, quoted=True), expression=d.MacroVar(this="start_ds"), ), ) @@ -2436,15 +2424,11 @@ def test_create_incremental_by_unique_key_merge_filter(adapter_mock, make_snapsh matched=True, then=exp.Update( expressions=[ - exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True).eq( + exp.column("updated_at", MERGE_TARGET_ALIAS, quoted=True).eq( exp.Coalesce( - this=exp.column( - "updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True - ), + this=exp.column("updated_at", MERGE_SOURCE_ALIAS, quoted=True), expressions=[ - exp.column( - "updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True - ) + exp.column("updated_at", MERGE_TARGET_ALIAS, quoted=True) ], ) ), @@ -2456,16 +2440,16 @@ def test_create_incremental_by_unique_key_merge_filter(adapter_mock, make_snapsh merge_filter=exp.And( this=exp.And( this=exp.GT( - this=exp.column("id", MERGE_SOURCE_ALIAS.lower(), quoted=True), + this=exp.column("id", MERGE_SOURCE_ALIAS, quoted=True), expression=exp.Literal(this="0", is_string=False), ), expression=exp.LT( - this=exp.column("updated_at", MERGE_TARGET_ALIAS.lower(), quoted=True), + this=exp.column("updated_at", MERGE_TARGET_ALIAS, quoted=True), expression=exp.Literal(this="2020-01-02", is_string=True), ), ), expression=exp.GT( - this=exp.column("updated_at", MERGE_SOURCE_ALIAS.lower(), quoted=True), + this=exp.column("updated_at", MERGE_SOURCE_ALIAS, quoted=True), expression=exp.Literal(this="2020-01-01", is_string=True), ), ), diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index cc5b05c8a5..eaa2fe94ad 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -135,7 +135,7 @@ def test_model_to_sqlmesh_fields(): assert kind.on_destructive_change == OnDestructiveChange.ALLOW assert ( kind.merge_filter.sql(dialect=model.dialect) - == """55 > "__merge_source__"."b" AND "__merge_target__"."session_start" > CURRENT_DATE + INTERVAL '7' DAY""" + == """55 > "__MERGE_SOURCE__"."b" AND "__MERGE_TARGET__"."session_start" > CURRENT_DATE + INTERVAL '7' DAY""" ) model = model_config.update_with({"dialect": "snowflake"}).to_sqlmesh(context) From 3f6d3e77301aca6175a568b5d644344052de667a Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Wed, 6 Aug 2025 22:25:09 +0000 Subject: [PATCH 0665/1056] Docs: clarify note about interval-scoped audits for incremental by time (#5101) --- docs/concepts/audits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/audits.md b/docs/concepts/audits.md index b4a614295b..903f8b4337 100644 --- a/docs/concepts/audits.md +++ b/docs/concepts/audits.md @@ -7,7 +7,7 @@ By default, SQLMesh will halt plan application when an audit fails so potentiall A comprehensive suite of audits can identify data issues upstream, whether they are from your vendors or other teams. Audits also empower your data engineers and analysts to work with confidence by catching problems early as they work on new features or make updates to your models. -**NOTE**: For incremental models, audits are only applied to intervals being processed - not for the entire underlying table. +**NOTE**: For incremental by time range models, audits are only applied to intervals being processed - not for the entire underlying table. ## User-Defined Audits In SQLMesh, user-defined audits are defined in `.sql` files in an `audits` directory in your SQLMesh project. Multiple audits can be defined in a single file, so you can organize them to your liking. Alternatively, audits can be defined inline within the model definition itself. From 94b13574e42bdb02a43ad3bae01abdf62923b64b Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 6 Aug 2025 22:19:07 -0700 Subject: [PATCH 0666/1056] Fix: Migrate schemas when deploying a metadata snapshot that was based on unpromoted forward-only snapshot (#5102) --- sqlmesh/core/plan/stages.py | 14 +---- sqlmesh/core/snapshot/definition.py | 15 +++++ sqlmesh/core/snapshot/evaluator.py | 6 +- tests/core/test_integration.py | 29 +++++++++ tests/core/test_plan_stages.py | 87 --------------------------- tests/core/test_snapshot_evaluator.py | 8 +++ 6 files changed, 54 insertions(+), 105 deletions(-) diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 194177b0cf..144e12c887 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -8,7 +8,6 @@ from sqlmesh.core.snapshot.definition import ( DeployabilityIndex, Snapshot, - SnapshotChangeCategory, SnapshotTableInfo, SnapshotId, Interval, @@ -251,18 +250,7 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: deployability_index = DeployabilityIndex.all_deployable() snapshots_with_schema_migration = [ - s - for s in snapshots.values() - if s.is_paused - and s.is_model - and not s.is_symbolic - and ( - not deployability_index_for_creation.is_representative(s) - or ( - s.is_view - and s.change_category == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - ) + s for s in snapshots.values() if s.requires_schema_migration_in_prod ] snapshots_to_intervals = self._missing_intervals( diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 941ef6aae7..266a974821 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1352,6 +1352,21 @@ def expiration_ts(self) -> int: check_categorical_relative_expression=False, ) + @property + def requires_schema_migration_in_prod(self) -> bool: + """Returns whether or not this snapshot requires a schema migration when deployed to production.""" + return ( + self.is_paused + and self.is_model + and not self.is_symbolic + and ( + (self.previous_version and self.previous_version.version == self.version) + or self.model.forward_only + or bool(self.model.physical_version) + or self.is_view + ) + ) + @property def ttl_ms(self) -> int: return self.expiration_ts - self.updated_ts diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index f8aa08a075..bdbf76250f 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -918,11 +918,7 @@ def _migrate_snapshot( adapter: EngineAdapter, deployability_index: DeployabilityIndex, ) -> None: - if ( - not snapshot.is_paused - or not snapshot.is_model - or (deployability_index.is_representative(snapshot) and not snapshot.is_view) - ): + if not snapshot.requires_schema_migration_in_prod: return deployability_index = DeployabilityIndex.all_deployable() diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index bad05d0c30..17f26aee2e 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -1381,6 +1381,35 @@ def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_co ) +@time_machine.travel("2023-01-08 15:00:00 UTC", tick=True) +def test_metadata_change_after_forward_only_results_in_migration(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Make a forward-only change + model = context.get_model("sushi.waiter_revenue_by_day") + model = model.copy(update={"kind": model.kind.copy(update={"forward_only": True})}) + model = add_projection_to_model(t.cast(SqlModel, model)) + context.upsert_model(model) + plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) + assert len(plan.new_snapshots) == 2 + assert all(s.change_category == SnapshotChangeCategory.FORWARD_ONLY for s in plan.new_snapshots) + + # Follow-up with a metadata change in the same environment + model = model.copy(update={"owner": "new_owner"}) + context.upsert_model(model) + plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) + assert len(plan.new_snapshots) == 2 + assert all(s.change_category == SnapshotChangeCategory.METADATA for s in plan.new_snapshots) + + # Deploy the latest change to prod + context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) + + # Check that the new column was added in prod + columns = context.engine_adapter.columns("sushi.waiter_revenue_by_day") + assert "one" in columns + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_forward_only_precedence_over_indirect_non_breaking(init_and_plan_context: t.Callable): context, plan = init_and_plan_context("examples/sushi") diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index 6e5a1fe43f..d79be24262 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -1211,93 +1211,6 @@ def test_build_plan_stages_environment_suffix_target_changed( ) -def test_build_plan_stages_indirect_non_breaking_no_migration( - snapshot_a: Snapshot, snapshot_b: Snapshot, make_snapshot, mocker: MockerFixture -) -> None: - # Categorize snapshot_a as forward-only - new_snapshot_a = make_snapshot( - snapshot_a.model.copy(update={"stamp": "new_version"}), - ) - new_snapshot_a.previous_versions = snapshot_a.all_versions - new_snapshot_a.categorize_as(SnapshotChangeCategory.NON_BREAKING) - - new_snapshot_b = make_snapshot( - snapshot_b.model.copy(), - nodes={'"a"': new_snapshot_a.model}, - ) - new_snapshot_b.previous_versions = snapshot_b.all_versions - new_snapshot_b.change_category = SnapshotChangeCategory.INDIRECT_NON_BREAKING - new_snapshot_b.version = new_snapshot_b.previous_version.data_version.version - - state_reader = mocker.Mock(spec=StateReader) - state_reader.get_snapshots.return_value = {} - existing_environment = Environment( - name="prod", - snapshots=[snapshot_a.table_info, snapshot_b.table_info], - start_at="2023-01-01", - end_at="2023-01-02", - plan_id="previous_plan", - previous_plan_id=None, - promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], - finalized_ts=to_timestamp("2023-01-02"), - ) - state_reader.get_environment.return_value = existing_environment - - # Create environment - environment = Environment( - name="prod", - snapshots=[new_snapshot_a.table_info, new_snapshot_b.table_info], - start_at="2023-01-01", - end_at="2023-01-02", - plan_id="test_plan", - previous_plan_id="previous_plan", - promoted_snapshot_ids=[new_snapshot_a.snapshot_id, new_snapshot_b.snapshot_id], - ) - - # Create evaluatable plan - plan = EvaluatablePlan( - start="2023-01-01", - end="2023-01-02", - new_snapshots=[new_snapshot_a, new_snapshot_b], - environment=environment, - no_gaps=False, - skip_backfill=False, - empty_backfill=False, - restatements={}, - is_dev=False, - allow_destructive_models=set(), - forward_only=False, - end_bounded=False, - ensure_finalized_snapshots=False, - directly_modified_snapshots=[new_snapshot_a.snapshot_id], - indirectly_modified_snapshots={ - new_snapshot_a.name: [new_snapshot_b.snapshot_id], - }, - metadata_updated_snapshots=[], - removed_snapshots=[], - requires_backfill=True, - models_to_backfill=None, - execution_time="2023-01-02", - disabled_restatement_models=set(), - environment_statements=None, - user_provided_flags=None, - ) - - # Build plan stages - stages = build_plan_stages(plan, state_reader, None) - - # Verify stages - assert len(stages) == 7 - - assert isinstance(stages[0], CreateSnapshotRecordsStage) - assert isinstance(stages[1], PhysicalLayerUpdateStage) - assert isinstance(stages[2], BackfillStage) - assert isinstance(stages[3], EnvironmentRecordUpdateStage) - assert isinstance(stages[4], UnpauseStage) - assert isinstance(stages[5], VirtualLayerUpdateStage) - assert isinstance(stages[6], FinalizeEnvironmentStage) - - def test_build_plan_stages_indirect_non_breaking_view_migration( snapshot_a: Snapshot, snapshot_c: Snapshot, make_snapshot, mocker: MockerFixture ) -> None: diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index c96ddf6e56..fc5df244b3 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -1180,6 +1180,7 @@ def columns(table_name): ) snapshot = make_snapshot(model, version="1") snapshot.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot.previous_versions = snapshot.all_versions evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) @@ -1217,6 +1218,7 @@ def test_migrate_missing_table(mocker: MockerFixture, make_snapshot): ) snapshot = make_snapshot(model, version="1") snapshot.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot.previous_versions = snapshot.all_versions evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) @@ -1714,6 +1716,7 @@ def columns(table_name): ) snapshot = make_snapshot(model, version="1") snapshot.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot.previous_versions = snapshot.all_versions with pytest.raises(NodeExecutionFailedError) as ex: evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) @@ -1735,6 +1738,7 @@ def columns(table_name): ) snapshot = make_snapshot(model, version="1") snapshot.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot.previous_versions = snapshot.all_versions logger = logging.getLogger("sqlmesh.core.snapshot.evaluator") with patch.object(logger, "warning") as mock_logger: @@ -3654,6 +3658,7 @@ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_moc new_snapshot = make_snapshot(updated_model) new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.previous_versions = snapshot.all_versions new_snapshot.version = snapshot.version assert new_snapshot.table_name() == snapshot.table_name() @@ -3724,6 +3729,7 @@ def test_migrate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): ) snapshot: Snapshot = make_snapshot(model) snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.previous_versions = snapshot.all_versions # no schema changes - no-op adapter_mock.get_alter_expressions.return_value = [] @@ -3925,6 +3931,7 @@ def columns(table_name): ) snapshot_1 = make_snapshot(model, version="1") snapshot_1.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot_1.previous_versions = snapshot_1.all_versions model_2 = SqlModel( name="test_schema.test_model_2", kind=IncrementalByTimeRangeKind( @@ -3935,6 +3942,7 @@ def columns(table_name): ) snapshot_2 = make_snapshot(model_2, version="1") snapshot_2.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot_2.previous_versions = snapshot_2.all_versions evaluator.migrate( [snapshot_1, snapshot_2], {}, deployability_index=DeployabilityIndex.none_deployable() ) From 49b5574732cb83fe2b0525e6ccbedefc85e48549 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 7 Aug 2025 08:06:23 -0700 Subject: [PATCH 0667/1056] chore: add private repo tests on pr (#4972) --- .../scripts}/get_scm_version.py | 2 +- .github/workflows/private-repo-test.yaml | 96 +++++++++++++++++++ tests/core/state_sync/test_state_sync.py | 8 +- 3 files changed, 101 insertions(+), 5 deletions(-) rename {.circleci => .github/scripts}/get_scm_version.py (53%) create mode 100644 .github/workflows/private-repo-test.yaml diff --git a/.circleci/get_scm_version.py b/.github/scripts/get_scm_version.py similarity index 53% rename from .circleci/get_scm_version.py rename to .github/scripts/get_scm_version.py index c432167958..79dfee9e5d 100644 --- a/.circleci/get_scm_version.py +++ b/.github/scripts/get_scm_version.py @@ -1,4 +1,4 @@ from setuptools_scm import get_version -version = get_version(root='../', relative_to=__file__) +version = get_version(root='../../', relative_to=__file__) print(version.split('+')[0]) diff --git a/.github/workflows/private-repo-test.yaml b/.github/workflows/private-repo-test.yaml new file mode 100644 index 0000000000..e2d0748e79 --- /dev/null +++ b/.github/workflows/private-repo-test.yaml @@ -0,0 +1,96 @@ +name: Private Repo Testing + +on: + pull_request: + branches: + - main + +concurrency: + group: 'private-test-${{ github.event.pull_request.number }}' + cancel-in-progress: true + +permissions: + contents: read + +jobs: + trigger-private-test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Set up Node.js for UI build + uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: latest + - name: Install UI dependencies + run: pnpm install + - name: Build UI + run: pnpm --prefix web/client run build + - name: Install Python dependencies + run: | + python -m venv .venv + source .venv/bin/activate + pip install build twine setuptools_scm + - name: Generate development version + id: version + run: | + source .venv/bin/activate + # Generate a PEP 440 compliant unique version including run attempt + BASE_VERSION=$(python .github/scripts/get_scm_version.py) + COMMIT_SHA=$(git rev-parse --short HEAD) + # Use PEP 440 compliant format: base.devN+pr.sha.attempt + UNIQUE_VERSION="${BASE_VERSION}+pr${{ github.event.pull_request.number }}.${COMMIT_SHA}.run${{ github.run_attempt }}" + echo "version=$UNIQUE_VERSION" >> $GITHUB_OUTPUT + echo "Generated unique version with run attempt: $UNIQUE_VERSION" + - name: Build package + env: + SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.version.outputs.version }} + run: | + source .venv/bin/activate + python -m build + - name: Configure PyPI for private repository + env: + TOBIKO_PRIVATE_PYPI_URL: ${{ secrets.TOBIKO_PRIVATE_PYPI_URL }} + TOBIKO_PRIVATE_PYPI_KEY: ${{ secrets.TOBIKO_PRIVATE_PYPI_KEY }} + run: ./.circleci/update-pypirc.sh + - name: Publish to private PyPI + run: | + source .venv/bin/activate + python -m twine upload -r tobiko-private dist/* + - name: Publish Python Tests package + env: + SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.version.outputs.version }} + run: | + source .venv/bin/activate + unset TWINE_USERNAME TWINE_PASSWORD && make publish-tests + - name: Get GitHub App token + id: get_token + uses: actions/create-github-app-token@v1 + with: + private-key: ${{ secrets.TOBIKO_RENOVATE_BOT_PRIVATE_KEY }} + app-id: ${{ secrets.TOBIKO_RENOVATE_BOT_APP_ID }} + owner: ${{ secrets.PRIVATE_REPO_OWNER }} + - name: Trigger private repository workflow + uses: convictional/trigger-workflow-and-wait@v1.6.5 + with: + owner: ${{ secrets.PRIVATE_REPO_OWNER }} + repo: ${{ secrets.PRIVATE_REPO_NAME }} + github_token: ${{ steps.get_token.outputs.token }} + workflow_file_name: ${{ secrets.PRIVATE_WORKFLOW_FILE }} + client_payload: | + { + "package_version": "${{ steps.version.outputs.version }}", + "pr_number": "${{ github.event.pull_request.number }}" + } diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index b5b4a42fde..eba948bd9a 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -2089,7 +2089,7 @@ def test_version_schema(state_sync: EngineAdapterStateSync, tmp_path) -> None: with pytest.raises( SQLMeshError, - match=rf"SQLMesh \(local\) is using version '{SQLMESH_VERSION}' which is ahead of '0.0.0' \(remote\). Please run a migration \('sqlmesh migrate' command\).", + match=rf"SQLMesh \(local\) is using version '{re.escape(SQLMESH_VERSION)}' which is ahead of '0.0.0' \(remote\). Please run a migration \('sqlmesh migrate' command\).", ): state_sync.get_versions() @@ -2099,7 +2099,7 @@ def test_version_schema(state_sync: EngineAdapterStateSync, tmp_path) -> None: state_sync.version_state.update_versions(schema_version=SCHEMA_VERSION + 1) error = ( rf"SQLMesh \(local\) is using version '{SCHEMA_VERSION}' which is behind '{SCHEMA_VERSION + 1}' \(remote\). " - rf"""Please upgrade SQLMesh \('pip install --upgrade "sqlmesh=={SQLMESH_VERSION}"' command\).""" + rf"""Please upgrade SQLMesh \('pip install --upgrade "sqlmesh=={re.escape(SQLMESH_VERSION)}"' command\).""" ) with pytest.raises(SQLMeshError, match=error): @@ -2136,7 +2136,7 @@ def test_version_sqlmesh(state_sync: EngineAdapterStateSync) -> None: # sqlmesh version is behind sqlmesh_version_minor_bump = f"{major}.{int(minor) + 1}.{patch}" error = ( - rf"SQLMesh \(local\) is using version '{SQLMESH_VERSION}' which is behind '{sqlmesh_version_minor_bump}' \(remote\). " + rf"SQLMesh \(local\) is using version '{re.escape(SQLMESH_VERSION)}' which is behind '{sqlmesh_version_minor_bump}' \(remote\). " rf"""Please upgrade SQLMesh \('pip install --upgrade "sqlmesh=={sqlmesh_version_minor_bump}"' command\).""" ) state_sync.version_state.update_versions(sqlmesh_version=sqlmesh_version_minor_bump) @@ -2146,7 +2146,7 @@ def test_version_sqlmesh(state_sync: EngineAdapterStateSync) -> None: # sqlmesh version is ahead sqlmesh_version_minor_decrease = f"{major}.{int(minor) - 1}.{patch}" - error = rf"SQLMesh \(local\) is using version '{SQLMESH_VERSION}' which is ahead of '{sqlmesh_version_minor_decrease}'" + error = rf"SQLMesh \(local\) is using version '{re.escape(SQLMESH_VERSION)}' which is ahead of '{sqlmesh_version_minor_decrease}'" state_sync.version_state.update_versions(sqlmesh_version=sqlmesh_version_minor_decrease) with pytest.raises(SQLMeshError, match=error): state_sync.get_versions() From 8fbf26c3b52c98285254e61e1457f9e65938d99b Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 7 Aug 2025 15:27:19 -0700 Subject: [PATCH 0668/1056] Fix: Revert "Fix: Use merge when updating auto restatements (#5016)" (#5108) --- sqlmesh/core/state_sync/db/snapshot.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 7aaf902216..5b6d96d970 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -373,31 +373,25 @@ def update_auto_restatements( Args: next_auto_restatement_ts: A dictionary of snapshot name version to the next auto restatement timestamp. """ - next_auto_restatement_ts_deleted = [] - next_auto_restatement_ts_filtered = {} - for k, v in next_auto_restatement_ts.items(): - if v is None: - next_auto_restatement_ts_deleted.append(k) - else: - next_auto_restatement_ts_filtered[k] = v - for where in snapshot_name_version_filter( self.engine_adapter, - next_auto_restatement_ts_deleted, + next_auto_restatement_ts, column_prefix="snapshot", alias=None, batch_size=self.SNAPSHOT_BATCH_SIZE, ): self.engine_adapter.delete_from(self.auto_restatements_table, where=where) + next_auto_restatement_ts_filtered = { + k: v for k, v in next_auto_restatement_ts.items() if v is not None + } if not next_auto_restatement_ts_filtered: return - self.engine_adapter.merge( + self.engine_adapter.insert_append( self.auto_restatements_table, _auto_restatements_to_df(next_auto_restatement_ts_filtered), columns_to_types=self._auto_restatement_columns_to_types, - unique_key=(exp.column("snapshot_name"), exp.column("snapshot_version")), ) def count(self) -> int: From 41d06a03f7d8ed2cc2af28282544828bcd3b937e Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:59:49 +0300 Subject: [PATCH 0669/1056] Chore: Fix dbt incremental predicates test (#5113) --- tests/dbt/test_transformation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 809e28fba4..aa5f9ab699 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -177,12 +177,12 @@ def test_model_kind(): expected_sqlmesh_predicate = parse_one( "__MERGE_TARGET__.session_start > DATEADD(day, -7, CURRENT_DATE)" ) - ModelConfig( + assert ModelConfig( materialized=Materialization.INCREMENTAL, unique_key=["bar"], incremental_strategy="merge", dialect="postgres", - merge_filter=[dbt_incremental_predicate], + incremental_predicates=[dbt_incremental_predicate], ).model_kind(context) == IncrementalByUniqueKeyKind( unique_key=["bar"], dialect="postgres", From a867d0bcbf3fd62371990a5edc1e71b4efad13c6 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Fri, 8 Aug 2025 18:47:19 +0300 Subject: [PATCH 0670/1056] Fix: Do not initialize a state sync for `sqlmesh clean` (#5103) --- sqlmesh/core/context.py | 4 ++-- tests/core/test_context.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 18df3a01fd..c0d9b21ff8 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -2537,8 +2537,8 @@ def clear_caches(self) -> None: if self.cache_dir.exists(): rmtree(self.cache_dir) - if isinstance(self.state_sync, CachingStateSync): - self.state_sync.clear_cache() + if isinstance(self._state_sync, CachingStateSync): + self._state_sync.clear_cache() def export_state( self, diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 0f14fd4a8b..e0567e8168 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -647,6 +647,9 @@ def test_clear_caches(tmp_path: pathlib.Path): assert not cache_dir.exists() assert models_dir.exists() + # Ensure that we don't initialize a CachingStateSync only to clear its (empty) caches + assert context._state_sync is None + # Test clearing caches when cache directory doesn't exist # This should not raise an exception context.clear_caches() From 4e80a93fbdb6376caf4c333029e0752414eef683 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 8 Aug 2025 09:30:33 -0700 Subject: [PATCH 0671/1056] Feat!: Decouple forward-only from change categorization (#5110) --- docs/concepts/plans.md | 21 ++- sqlmesh/core/plan/builder.py | 94 +++++----- sqlmesh/core/snapshot/definition.py | 35 +++- sqlmesh/core/snapshot/evaluator.py | 2 +- sqlmesh/core/state_sync/db/snapshot.py | 4 +- tests/conftest.py | 2 +- tests/core/analytics/test_collector.py | 4 +- .../integration/test_integration.py | 10 +- tests/core/state_sync/test_state_sync.py | 31 +-- tests/core/test_context.py | 2 + tests/core/test_integration.py | 177 ++++++++---------- tests/core/test_plan.py | 74 +++++--- tests/core/test_plan_stages.py | 12 +- tests/core/test_snapshot.py | 30 +-- tests/core/test_snapshot_evaluator.py | 121 ++++++++---- 15 files changed, 344 insertions(+), 275 deletions(-) diff --git a/docs/concepts/plans.md b/docs/concepts/plans.md index da3d3debb7..91616a6e7e 100644 --- a/docs/concepts/plans.md +++ b/docs/concepts/plans.md @@ -43,15 +43,6 @@ This is a common choice in scenarios such as an addition of a new column, an act If any downstream models contain a `select *` from the model, SQLMesh attempts to infer breaking status on a best-effort basis. We recommend explicitly specifying a query's columns to avoid unnecessary recomputation. -### Forward-only change -A modified (either directly or indirectly) model that is categorized as forward-only will continue to use the existing physical table once the change is deployed to production (the `prod` environment). This means that no backfill will take place. - -While iterating on forward-only changes in the development environment, the model's output will be stored in either a temporary table or a shallow clone of the production table if supported by the engine. - -In either case the data produced this way in the development environment can only be used for preview and will **not** be reused once the change is deployed to production. See [Forward-only Plans](#forward-only-plans) for more details. - -This category is assigned by SQLMesh automatically either when a user opts into using a [forward-only plan](#forward-only-plans) or when a model is explicitly configured to be forward-only. - ### Summary | Change Category | Change Type | Behaviour | @@ -59,7 +50,17 @@ This category is assigned by SQLMesh automatically either when a user opts into | [Breaking](#breaking-change) | [Direct](glossary.md#direct-modification) or [Indirect](glossary.md#indirect-modification) | [Backfill](glossary.md#backfill) | | [Non-breaking](#non-breaking-change) | [Direct](glossary.md#direct-modification) | [Backfill](glossary.md#backfill) | | [Non-breaking](#non-breaking-change) | [Indirect](glossary.md#indirect-modification) | [No Backfill](glossary.md#backfill) | -| [Forward-only](#forward-only-change) | [Direct](glossary.md#direct-modification) or [Indirect](glossary.md#indirect-modification) | [No Backfill](glossary.md#backfill), schema change | + +## Forward-only change +In addition to categorizing a change as breaking or non-breaking, it can also be classified as forward-only. + +A model change classified as forward-only will continue to use the existing physical table once the change is deployed to production (the `prod` environment). This means that no backfill will take place. + +While iterating on forward-only changes in the development environment, the model's output will be stored in either a temporary table or a shallow clone of the production table if supported by the engine. + +In either case the data produced this way in the development environment can only be used for preview and will **not** be reused once the change is deployed to production. See [Forward-only Plans](#forward-only-plans) for more details. + +This category is assigned by SQLMesh automatically either when a user opts into using a [forward-only plan](#forward-only-plans) or when a model is explicitly configured to be forward-only. ## Plan application Once a plan has been created and reviewed, it is then applied to the target [environment](environments.md) in order for its changes to take effect. diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 6f3c7f0805..178cd8d2e4 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -239,8 +239,6 @@ def set_choice(self, snapshot: Snapshot, choice: SnapshotChangeCategory) -> Plan snapshot: The target snapshot. choice: The user decision on how to version the target snapshot and its children. """ - if self._forward_only: - raise PlanError("Choice setting is not supported by a forward-only plan.") if not self._is_new_snapshot(snapshot): raise PlanError( f"A choice can't be changed for the existing version of {snapshot.name}." @@ -250,8 +248,6 @@ def set_choice(self, snapshot: Snapshot, choice: SnapshotChangeCategory) -> Plan and snapshot.snapshot_id not in self._context_diff.added ): raise PlanError(f"Only directly modified models can be categorized ({snapshot.name}).") - if snapshot.is_model and snapshot.model.forward_only: - raise PlanError(f"Forward-only model {snapshot.name} cannot be categorized manually.") self._choices[snapshot.snapshot_id] = choice self._latest_plan = None @@ -369,8 +365,10 @@ def _build_restatements( restate_models = { s.name for s in self._context_diff.new_snapshots.values() - if s.is_materialized - and (self._forward_only or s.model.forward_only) + if s.is_model + and not s.is_symbolic + and (s.is_forward_only or s.model.forward_only) + and not s.is_no_preview and ( # Metadata changes should not be previewed. self._context_diff.directly_modified(s.name) @@ -395,6 +393,9 @@ def _build_restatements( for s_id in dag: snapshot = self._context_diff.snapshots[s_id] + if is_preview and snapshot.is_no_preview: + continue + # Since we are traversing the graph in topological order and the largest interval range is pushed down # the graph we just have to check our immediate parents in the graph and not the whole upstream graph. restating_parents = [ @@ -526,6 +527,9 @@ def _adjust_new_snapshot_intervals(self) -> None: def _check_destructive_changes(self, directly_modified: t.Set[SnapshotId]) -> None: for s_id in sorted(directly_modified): + if s_id.name not in self._context_diff.modified_snapshots: + continue + snapshot = self._context_diff.snapshots[s_id] # should we raise/warn if this snapshot has/inherits a destructive change? should_raise_or_warn = ( @@ -583,38 +587,38 @@ def _categorize_snapshots( if not snapshot or not self._is_new_snapshot(snapshot): continue + forward_only = self._is_forward_only_change(s_id) or self._forward_only + if s_id in self._choices: - snapshot.categorize_as(self._choices[s_id]) + snapshot.categorize_as(self._choices[s_id], forward_only) continue if s_id in self._context_diff.added: - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - elif self._is_forward_only_change(s_id) or self._forward_only: - # In case of the forward only plan any modifications result in reuse of the - # previous version for non-seed models. - # New snapshots of seed models are considered non-breaking ones. - category = ( - SnapshotChangeCategory.NON_BREAKING - if snapshot.is_seed - else SnapshotChangeCategory.FORWARD_ONLY - ) - # If the model kind changes mark as breaking - if snapshot.is_model and snapshot.name in self._context_diff.modified_snapshots: - _, old = self._context_diff.modified_snapshots[snapshot.name] - if _is_breaking_kind_change(old, snapshot): - category = SnapshotChangeCategory.BREAKING - - snapshot.categorize_as(category) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only) elif s_id.name in self._context_diff.modified_snapshots: - self._categorize_snapshot(snapshot, dag, indirectly_modified) + self._categorize_snapshot(snapshot, forward_only, dag, indirectly_modified) def _categorize_snapshot( - self, snapshot: Snapshot, dag: DAG[SnapshotId], indirectly_modified: SnapshotMapping + self, + snapshot: Snapshot, + forward_only: bool, + dag: DAG[SnapshotId], + indirectly_modified: SnapshotMapping, ) -> None: s_id = snapshot.snapshot_id if self._context_diff.directly_modified(s_id.name): + new, old = self._context_diff.modified_snapshots[s_id.name] + is_breaking_kind_change = _is_breaking_kind_change(old, new) + if is_breaking_kind_change or snapshot.is_seed: + # Breaking kind changes and seed changes can't be forward-only. + forward_only = False + if self._auto_categorization_enabled: + if is_breaking_kind_change: + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only) + return + s_id_with_missing_columns: t.Optional[SnapshotId] = None this_sid_with_downstream = indirectly_modified.get(s_id, set()) | {s_id} for downstream_s_id in this_sid_with_downstream: @@ -626,18 +630,18 @@ def _categorize_snapshot( s_id_with_missing_columns = downstream_s_id break - new, old = self._context_diff.modified_snapshots[s_id.name] if s_id_with_missing_columns is None: change_category = categorize_change(new, old, config=self._categorizer_config) if change_category is not None: - snapshot.categorize_as(change_category) + snapshot.categorize_as(change_category, forward_only) else: mode = self._categorizer_config.dict().get( new.model.source_type, AutoCategorizationMode.OFF ) if mode == AutoCategorizationMode.FULL: - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only) elif self._context_diff.indirectly_modified(snapshot.name): + all_upstream_forward_only = set() all_upstream_categories = set() direct_parent_categories = set() @@ -646,27 +650,30 @@ def _categorize_snapshot( if parent and self._is_new_snapshot(parent): all_upstream_categories.add(parent.change_category) + all_upstream_forward_only.add(parent.is_forward_only) if p_id in snapshot.parents: direct_parent_categories.add(parent.change_category) - if snapshot.is_model and snapshot.model.forward_only: - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) - elif direct_parent_categories.intersection( + if all_upstream_forward_only == {True} or ( + snapshot.is_model and snapshot.model.forward_only + ): + forward_only = True + + if direct_parent_categories.intersection( {SnapshotChangeCategory.BREAKING, SnapshotChangeCategory.INDIRECT_BREAKING} ): - snapshot.categorize_as(SnapshotChangeCategory.INDIRECT_BREAKING) + snapshot.categorize_as(SnapshotChangeCategory.INDIRECT_BREAKING, forward_only) elif not direct_parent_categories: - snapshot.categorize_as(self._get_orphaned_indirect_change_category(snapshot)) - elif SnapshotChangeCategory.FORWARD_ONLY in all_upstream_categories: - # FORWARD_ONLY must take precedence over INDIRECT_NON_BREAKING - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as( + self._get_orphaned_indirect_change_category(snapshot), forward_only + ) elif all_upstream_categories == {SnapshotChangeCategory.METADATA}: - snapshot.categorize_as(SnapshotChangeCategory.METADATA) + snapshot.categorize_as(SnapshotChangeCategory.METADATA, forward_only) else: - snapshot.categorize_as(SnapshotChangeCategory.INDIRECT_NON_BREAKING) + snapshot.categorize_as(SnapshotChangeCategory.INDIRECT_NON_BREAKING, forward_only) else: # Metadata updated. - snapshot.categorize_as(SnapshotChangeCategory.METADATA) + snapshot.categorize_as(SnapshotChangeCategory.METADATA, forward_only) def _get_orphaned_indirect_change_category( self, indirect_snapshot: Snapshot @@ -769,10 +776,7 @@ def _is_forward_only_change(self, s_id: SnapshotId) -> bool: if snapshot.is_model and _is_breaking_kind_change(old, snapshot): return False return ( - snapshot.is_model - and snapshot.model.forward_only - and not snapshot.change_category - and bool(snapshot.previous_versions) + snapshot.is_model and snapshot.model.forward_only and bool(snapshot.previous_versions) ) def _is_new_snapshot(self, snapshot: Snapshot) -> bool: @@ -811,7 +815,7 @@ def _ensure_no_forward_only_revert(self) -> None: and not candidate.model.forward_only and promoted.is_forward_only and not promoted.is_paused - and not candidate.reuses_previous_version + and not candidate.is_no_rebuild and promoted.version == candidate.version ): raise PlanError( diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 266a974821..90cd963051 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -76,6 +76,7 @@ class SnapshotChangeCategory(IntEnum): BREAKING = 1 NON_BREAKING = 2 + # FORWARD_ONLY category is deprecated and is kept for backwards compatibility. FORWARD_ONLY = 3 INDIRECT_BREAKING = 4 INDIRECT_NON_BREAKING = 5 @@ -336,6 +337,7 @@ class SnapshotInfoMixin(ModelKindMixin): base_table_name_override: t.Optional[str] dev_table_suffix: str table_naming_convention: TableNamingConvention = Field(default=TableNamingConvention.default) + forward_only: bool @cached_property def identifier(self) -> str: @@ -383,7 +385,7 @@ def fully_qualified_table(self) -> t.Optional[exp.Table]: @property def is_forward_only(self) -> bool: - return self.change_category == SnapshotChangeCategory.FORWARD_ONLY + return self.forward_only or self.change_category == SnapshotChangeCategory.FORWARD_ONLY @property def is_metadata(self) -> bool: @@ -394,9 +396,18 @@ def is_indirect_non_breaking(self) -> bool: return self.change_category == SnapshotChangeCategory.INDIRECT_NON_BREAKING @property - def reuses_previous_version(self) -> bool: - return self.change_category in ( - SnapshotChangeCategory.FORWARD_ONLY, + def is_no_rebuild(self) -> bool: + """Returns true if this snapshot doesn't require a rebuild in production.""" + return self.forward_only or self.change_category in ( + SnapshotChangeCategory.FORWARD_ONLY, # Backwards compatibility + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.INDIRECT_NON_BREAKING, + ) + + @property + def is_no_preview(self) -> bool: + """Returns true if this snapshot doesn't require a preview in development.""" + return self.forward_only and self.change_category in ( SnapshotChangeCategory.METADATA, SnapshotChangeCategory.INDIRECT_NON_BREAKING, ) @@ -487,6 +498,7 @@ class SnapshotTableInfo(PydanticModel, SnapshotInfoMixin, frozen=True): custom_materialization: t.Optional[str] = None dev_table_suffix: str model_gateway: t.Optional[str] = None + forward_only: bool = False def __lt__(self, other: SnapshotTableInfo) -> bool: return self.name < other.name @@ -614,6 +626,7 @@ class Snapshot(PydanticModel, SnapshotInfoMixin): table_naming_convention_: TableNamingConvention = Field( default=TableNamingConvention.default, alias="table_naming_convention" ) + forward_only: bool = False @field_validator("ttl") @classmethod @@ -1006,22 +1019,26 @@ def check_ready_intervals( ) return intervals - def categorize_as(self, category: SnapshotChangeCategory) -> None: + def categorize_as(self, category: SnapshotChangeCategory, forward_only: bool = False) -> None: """Assigns the given category to this snapshot. Args: category: The change category to assign to this snapshot. + forward_only: Whether or not this snapshot is applied going forward in production. """ + assert category != SnapshotChangeCategory.FORWARD_ONLY, ( + "FORWARD_ONLY change category is deprecated" + ) + self.dev_version_ = self.fingerprint.to_version() - reuse_previous_version = category in ( - SnapshotChangeCategory.FORWARD_ONLY, + is_no_rebuild = forward_only or category in ( SnapshotChangeCategory.INDIRECT_NON_BREAKING, SnapshotChangeCategory.METADATA, ) if self.is_model and self.model.physical_version: # If the model has a pinned version then use that. self.version = self.model.physical_version - elif reuse_previous_version and self.previous_version: + elif is_no_rebuild and self.previous_version: previous_version = self.previous_version self.version = previous_version.data_version.version self.physical_schema_ = previous_version.physical_schema @@ -1040,6 +1057,7 @@ def categorize_as(self, category: SnapshotChangeCategory) -> None: self.version = self.fingerprint.to_version() self.change_category = category + self.forward_only = forward_only @property def categorized(self) -> bool: @@ -1220,6 +1238,7 @@ def table_info(self) -> SnapshotTableInfo: dev_table_suffix=self.dev_table_suffix, model_gateway=self.model_gateway, table_naming_convention=self.table_naming_convention, # type: ignore + forward_only=self.forward_only, ) @property diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index bdbf76250f..e053e1e108 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -338,7 +338,7 @@ def create( continue deployability_flags = [True] if ( - snapshot.reuses_previous_version + snapshot.is_no_rebuild or snapshot.is_managed or (snapshot.is_model and snapshot.model.forward_only) or (deployability_index and not deployability_index.is_deployable(snapshot)) diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 5b6d96d970..3be4fb1b45 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -727,6 +727,7 @@ class SharedVersionSnapshot(PydanticModel): disable_restatement: bool effective_from: t.Optional[TimeLike] raw_snapshot: t.Dict[str, t.Any] + forward_only: bool @property def snapshot_id(self) -> SnapshotId: @@ -734,7 +735,7 @@ def snapshot_id(self) -> SnapshotId: @property def is_forward_only(self) -> bool: - return self.change_category == SnapshotChangeCategory.FORWARD_ONLY + return self.forward_only or self.change_category == SnapshotChangeCategory.FORWARD_ONLY @property def normalized_effective_from_ts(self) -> t.Optional[int]: @@ -797,4 +798,5 @@ def from_snapshot_record( disable_restatement=raw_node.get("kind", {}).get("disable_restatement", False), effective_from=raw_snapshot.get("effective_from"), raw_snapshot=raw_snapshot, + forward_only=raw_snapshot.get("forward_only", False), ) diff --git a/tests/conftest.py b/tests/conftest.py index 574c802c0e..1bfa7a9f36 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -438,7 +438,7 @@ def _make_function( metadata_hash="test_metadata_hash", ), version="test_version", - change_category=SnapshotChangeCategory.FORWARD_ONLY, + change_category=SnapshotChangeCategory.NON_BREAKING, dev_table_suffix="dev", ), ) diff --git a/tests/core/analytics/test_collector.py b/tests/core/analytics/test_collector.py index 9eaca07ef3..1a4c42cbe3 100644 --- a/tests/core/analytics/test_collector.py +++ b/tests/core/analytics/test_collector.py @@ -218,7 +218,7 @@ def test_on_snapshots_created( context.get_snapshot("sushi.waiter_revenue_by_day"), context.get_snapshot("sushi.top_waiters"), ] - new_snapshots[0].categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshots[0].categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshots[0].effective_from = "2024-01-01" new_snapshots[0].version = "test_version" @@ -239,7 +239,7 @@ def test_on_snapshots_created( "node_type": "model", "model_kind": "incremental_by_time_range", "is_sql": False, - "change_category": "forward_only", + "change_category": "breaking", "dialect": "duckdb", "audits_count": 0, "effective_from_set": True, diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 80ce7ac18d..eb593c273b 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -2087,7 +2087,9 @@ def _run_plan(sqlmesh_context: Context, environment: str = None) -> PlanResults: plan_1 = _run_plan(context) assert plan_1.snapshot_for(model_a).change_category == SnapshotChangeCategory.BREAKING + assert not plan_1.snapshot_for(model_a).is_forward_only assert plan_1.snapshot_for(model_b).change_category == SnapshotChangeCategory.BREAKING + assert not plan_1.snapshot_for(model_b).is_forward_only # so far so good, model_a should exist as a normal table, model b should be a managed table and the prod views should exist assert len(plan_1.schema_metadata.views) == 2 @@ -2134,8 +2136,10 @@ def _run_plan(sqlmesh_context: Context, environment: str = None) -> PlanResults: assert plan_2.plan.has_changes assert len(plan_2.plan.modified_snapshots) == 2 - assert plan_2.snapshot_for(new_model_a).change_category == SnapshotChangeCategory.FORWARD_ONLY + assert plan_2.snapshot_for(new_model_a).change_category == SnapshotChangeCategory.NON_BREAKING + assert plan_2.snapshot_for(new_model_a).is_forward_only assert plan_2.snapshot_for(model_b).change_category == SnapshotChangeCategory.NON_BREAKING + assert not plan_2.snapshot_for(model_b).is_forward_only # verify that the new snapshots were created correctly # the forward-only change to model A should be in a new table separate from the one created in the first plan @@ -2207,8 +2211,10 @@ def _run_plan(sqlmesh_context: Context, environment: str = None) -> PlanResults: plan_4 = _run_plan(context) assert plan_4.plan.has_changes - assert plan_4.snapshot_for(model_a).change_category == SnapshotChangeCategory.FORWARD_ONLY + assert plan_4.snapshot_for(model_a).change_category == SnapshotChangeCategory.NON_BREAKING + assert plan_4.snapshot_for(model_a).is_forward_only assert plan_4.snapshot_for(model_b).change_category == SnapshotChangeCategory.NON_BREAKING + assert not plan_4.snapshot_for(model_b).is_forward_only # verify the Model B table is created as a managed table in prod assert plan_4.table_name_for(model_b) == plan_3.table_name_for( diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index eba948bd9a..a5a6969e38 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -139,7 +139,7 @@ def test_push_snapshots( state_sync.push_snapshots([snapshot_a, snapshot_b]) snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot_b.version = "2" state_sync.push_snapshots([snapshot_a, snapshot_b]) @@ -258,7 +258,8 @@ def test_add_interval( (to_timestamp("2020-01-05"), to_timestamp("2020-01-11")), ] - snapshot.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True state_sync.add_interval(snapshot, to_datetime("2020-01-16"), "2020-01-20", is_dev=True) intervals = get_snapshot_intervals(snapshot) assert intervals.intervals == [ @@ -1144,7 +1145,7 @@ def test_delete_expired_snapshots(state_sync: EngineAdapterStateSync, make_snaps ), ) new_snapshot.ttl = "in 10 seconds" - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.version = snapshot.version new_snapshot.updated_ts = now_ts - 11000 @@ -1298,7 +1299,7 @@ def test_delete_expired_snapshots_dev_table_cleanup_only( ), ) new_snapshot.ttl = "in 10 seconds" - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.version = snapshot.version new_snapshot.updated_ts = now_ts - 5000 @@ -1338,7 +1339,7 @@ def test_delete_expired_snapshots_shared_dev_table( ), ) new_snapshot.ttl = "in 10 seconds" - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.version = snapshot.version new_snapshot.dev_version_ = snapshot.dev_version new_snapshot.updated_ts = now_ts - 5000 @@ -1430,7 +1431,7 @@ def test_delete_expired_snapshots_cleanup_intervals( ), ) new_snapshot.ttl = "in 10 seconds" - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.version = snapshot.version new_snapshot.updated_ts = now_ts - 12000 @@ -1497,7 +1498,7 @@ def test_delete_expired_snapshots_cleanup_intervals_shared_version( ), ) new_snapshot.ttl = "in 10 seconds" - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.version = snapshot.version new_snapshot.updated_ts = now_ts - 5000 @@ -1612,7 +1613,7 @@ def test_delete_expired_snapshots_cleanup_intervals_shared_dev_version( ), ) new_snapshot.ttl = "in 10 seconds" - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.version = snapshot.version new_snapshot.dev_version_ = snapshot.dev_version new_snapshot.updated_ts = now_ts - 5000 @@ -1740,7 +1741,7 @@ def test_compact_intervals_after_cleanup( ) snapshot_b.previous_versions = snapshot_a.all_versions snapshot_b.ttl = "in 10 seconds" - snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot_b.updated_ts = now_ts - 12000 # An indirect non-breaking change on top of the forward-only change. Not expired. @@ -1872,7 +1873,7 @@ def test_unpause_snapshots(state_sync: EngineAdapterStateSync, make_snapshot: t. new_snapshot = make_snapshot( SqlModel(name="test_snapshot", query=parse_one("select 2, ds"), cron="@daily") ) - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.version = "a" assert not new_snapshot.unpaused_ts @@ -1917,7 +1918,7 @@ def test_unpause_snapshots_hourly(state_sync: EngineAdapterStateSync, make_snaps interval_unit="hour", ) ) - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.version = "a" assert not new_snapshot.unpaused_ts @@ -1974,7 +1975,7 @@ def test_unrestorable_snapshot(state_sync: EngineAdapterStateSync, make_snapshot new_forward_only_snapshot = make_snapshot( SqlModel(name="test_snapshot", query=parse_one("select 3, ds"), cron="@daily") ) - new_forward_only_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_forward_only_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_forward_only_snapshot.version = "a" assert not new_forward_only_snapshot.unpaused_ts @@ -2015,7 +2016,7 @@ def test_unpause_snapshots_remove_intervals( SqlModel(name="test_snapshot", query=parse_one("select 2, ds"), cron="@daily"), version="a", ) - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.version = "a" new_snapshot.effective_from = "2023-01-03" state_sync.push_snapshots([new_snapshot]) @@ -2053,7 +2054,7 @@ def test_unpause_snapshots_remove_intervals_disabled_restatement( SqlModel(name="test_snapshot", query=parse_one("select 2, ds"), cron="@daily", kind=kind), version="a", ) - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.version = "a" new_snapshot.effective_from = "2023-01-03" state_sync.push_snapshots([new_snapshot]) @@ -3019,7 +3020,7 @@ def test_seed_model_metadata_update( model = model.copy(update={"owner": "jen"}) new_snapshot = make_snapshot(model) new_snapshot.previous_versions = snapshot.all_versions - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) assert snapshot.fingerprint != new_snapshot.fingerprint assert snapshot.version == new_snapshot.version diff --git a/tests/core/test_context.py b/tests/core/test_context.py index e0567e8168..2827981ae7 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1127,6 +1127,7 @@ def test_unrestorable_snapshot(sushi_context: Context) -> None: no_prompts=True, forward_only=True, allow_destructive_models=["memory.sushi.test_unrestorable"], + categorizer_config=CategorizerConfig.all_full(), ) sushi_context.upsert_model(model_v1) @@ -1135,6 +1136,7 @@ def test_unrestorable_snapshot(sushi_context: Context) -> None: no_prompts=True, forward_only=True, allow_destructive_models=["memory.sushi.test_unrestorable"], + categorizer_config=CategorizerConfig.all_full(), ) model_v1_new_snapshot = sushi_context.get_snapshot( "memory.sushi.test_unrestorable", raise_if_missing=True diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 17f26aee2e..d7edd8d131 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -114,18 +114,17 @@ def test_forward_only_plan_with_effective_date(context_fixture: Context, request assert len(plan.new_snapshots) == 2 assert ( plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.NON_BREAKING ) assert ( plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) + assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only + assert plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].is_forward_only + assert to_timestamp(plan.start) == to_timestamp("2023-01-07") assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], - ), SnapshotIntervals( snapshot_id=snapshot.snapshot_id, intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], @@ -256,12 +255,15 @@ def test_forward_only_model_regular_plan(init_and_plan_context: t.Callable): assert len(plan.new_snapshots) == 2 assert ( plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.NON_BREAKING ) assert ( plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) + assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only + assert plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].is_forward_only + assert plan.start == to_datetime("2023-01-01") assert not plan.missing_intervals @@ -362,20 +364,17 @@ def test_forward_only_model_regular_plan_preview_enabled(init_and_plan_context: assert len(plan.new_snapshots) == 2 assert ( plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.NON_BREAKING ) assert ( plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) + assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only + assert plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].is_forward_only + assert to_timestamp(plan.start) == to_timestamp("2023-01-07") assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), SnapshotIntervals( snapshot_id=snapshot.snapshot_id, intervals=[ @@ -429,7 +428,12 @@ def test_forward_only_model_restate_full_history_in_dev(init_and_plan_context: t context.upsert_model(SqlModel.parse_obj(model_kwargs)) # Apply the model change in dev - plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() + plan = context.plan_builder( + "dev", + skip_tests=True, + enable_preview=False, + categorizer_config=CategorizerConfig.all_full(), + ).build() assert not plan.missing_intervals context.apply(plan) @@ -496,59 +500,30 @@ def test_full_history_restatement_model_regular_plan_preview_enabled( assert len(plan.new_snapshots) == 6 assert ( plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.NON_BREAKING ) assert ( plan.context_diff.snapshots[customers_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) assert ( plan.context_diff.snapshots[active_customers_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) assert ( plan.context_diff.snapshots[waiter_as_customer_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) + assert all(s.is_forward_only for s in plan.new_snapshots) assert to_timestamp(plan.start) == to_timestamp("2023-01-07") assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=active_customers_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=count_customers_active_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=count_customers_inactive_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=customers_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), SnapshotIntervals( snapshot_id=snapshot.snapshot_id, intervals=[ (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), ], ), - SnapshotIntervals( - snapshot_id=waiter_as_customer_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), ] context.apply(plan) @@ -786,15 +761,6 @@ def test_cron_not_aligned_with_day_boundary_new_model(init_and_plan_context: t.C ).snapshot_id, intervals=[(to_timestamp("2023-01-06"), to_timestamp("2023-01-07"))], ), - SnapshotIntervals( - snapshot_id=context.get_snapshot( - "sushi.top_waiters", raise_if_missing=True - ).snapshot_id, - intervals=[ - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), SnapshotIntervals( snapshot_id=context.get_snapshot( "sushi.waiter_revenue_by_day", raise_if_missing=True @@ -944,12 +910,13 @@ def test_forward_only_parent_created_in_dev_child_created_in_prod( assert len(plan.new_snapshots) == 2 assert ( plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.NON_BREAKING ) assert ( plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) + assert all(s.is_forward_only for s in plan.new_snapshots) assert plan.start == to_datetime("2023-01-01") assert not plan.missing_intervals @@ -1155,18 +1122,15 @@ def test_non_breaking_change_after_forward_only_in_dev( assert len(plan.new_snapshots) == 2 assert ( plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.NON_BREAKING ) assert ( plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) + assert all(s.is_forward_only for s in plan.new_snapshots) assert to_timestamp(plan.start) == to_timestamp("2023-01-07") assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], - ), SnapshotIntervals( snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], @@ -1267,11 +1231,17 @@ def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_co context.upsert_model(model) snapshot = context.get_snapshot(model, raise_if_missing=True) - plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() + plan = context.plan_builder( + "dev", + skip_tests=True, + enable_preview=False, + categorizer_config=CategorizerConfig.all_full(), + ).build() assert ( plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.BREAKING ) + assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only assert not plan.requires_backfill context.apply(plan) @@ -1393,7 +1363,7 @@ def test_metadata_change_after_forward_only_results_in_migration(init_and_plan_c context.upsert_model(model) plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) assert len(plan.new_snapshots) == 2 - assert all(s.change_category == SnapshotChangeCategory.FORWARD_ONLY for s in plan.new_snapshots) + assert all(s.is_forward_only for s in plan.new_snapshots) # Follow-up with a metadata change in the same environment model = model.copy(update={"owner": "new_owner"}) @@ -1411,7 +1381,7 @@ def test_metadata_change_after_forward_only_results_in_migration(init_and_plan_c @time_machine.travel("2023-01-08 15:00:00 UTC") -def test_forward_only_precedence_over_indirect_non_breaking(init_and_plan_context: t.Callable): +def test_indirect_non_breaking_downstream_of_forward_only(init_and_plan_context: t.Callable): context, plan = init_and_plan_context("examples/sushi") context.apply(plan) @@ -1425,14 +1395,20 @@ def test_forward_only_precedence_over_indirect_non_breaking(init_and_plan_contex forward_only_snapshot = context.get_snapshot(forward_only_model, raise_if_missing=True) non_breaking_model = context.get_model("sushi.waiter_revenue_by_day") + non_breaking_model = non_breaking_model.copy(update={"start": "2023-01-01"}) context.upsert_model(add_projection_to_model(t.cast(SqlModel, non_breaking_model))) non_breaking_snapshot = context.get_snapshot(non_breaking_model, raise_if_missing=True) top_waiter_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() + plan = context.plan_builder( + "dev", + skip_tests=True, + enable_preview=False, + categorizer_config=CategorizerConfig.all_full(), + ).build() assert ( plan.context_diff.snapshots[forward_only_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.BREAKING ) assert ( plan.context_diff.snapshots[non_breaking_snapshot.snapshot_id].change_category @@ -1440,10 +1416,26 @@ def test_forward_only_precedence_over_indirect_non_breaking(init_and_plan_contex ) assert ( plan.context_diff.snapshots[top_waiter_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) + assert plan.context_diff.snapshots[forward_only_snapshot.snapshot_id].is_forward_only + assert not plan.context_diff.snapshots[non_breaking_snapshot.snapshot_id].is_forward_only + assert not plan.context_diff.snapshots[top_waiter_snapshot.snapshot_id].is_forward_only + assert plan.start == to_timestamp("2023-01-01") assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiter_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), SnapshotIntervals( snapshot_id=non_breaking_snapshot.snapshot_id, intervals=[ @@ -2164,15 +2156,15 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot( dev_plan = context.plan("dev", auto_apply=True, no_prompts=True, enable_preview=False) assert ( dev_plan.snapshots[forward_only_model_snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.NON_BREAKING ) assert ( dev_plan.snapshots[full_downstream_model_snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) assert ( dev_plan.snapshots[full_downstream_model_2_snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) assert not dev_plan.missing_intervals @@ -2363,21 +2355,6 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot_migration( SnapshotChangeCategory.METADATA, SnapshotChangeCategory.METADATA, ), - ( - SnapshotChangeCategory.FORWARD_ONLY, - SnapshotChangeCategory.BREAKING, - SnapshotChangeCategory.INDIRECT_BREAKING, - ), - ( - SnapshotChangeCategory.BREAKING, - SnapshotChangeCategory.FORWARD_ONLY, - SnapshotChangeCategory.FORWARD_ONLY, - ), - ( - SnapshotChangeCategory.FORWARD_ONLY, - SnapshotChangeCategory.FORWARD_ONLY, - SnapshotChangeCategory.FORWARD_ONLY, - ), ], ) def test_rebase_two_changed_parents( @@ -4538,12 +4515,6 @@ def test_breaking_change(sushi_context: Context): validate_query_change(sushi_context, environment, SnapshotChangeCategory.BREAKING, False) -def test_forward_only(sushi_context: Context): - environment = "dev" - initial_add(sushi_context, environment) - validate_query_change(sushi_context, environment, SnapshotChangeCategory.FORWARD_ONLY, False) - - def test_logical_change(sushi_context: Context): environment = "dev" initial_add(sushi_context, environment) @@ -4795,8 +4766,11 @@ def _validate_plan(context, plan): plan.context_diff.modified_snapshots[sushi_customer_revenue_by_day_snapshot.name][ 0 ].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.NON_BREAKING ) + assert plan.context_diff.snapshots[ + sushi_customer_revenue_by_day_snapshot.snapshot_id + ].is_forward_only apply_to_environment( sushi_context, @@ -6074,6 +6048,9 @@ def apply_to_environment( plan_builder.set_start(plan_start or start(context)) if choice: + if choice == SnapshotChangeCategory.FORWARD_ONLY: + # FORWARD_ONLY is deprecated, fallback to NON_BREAKING to keep the existing tests + choice = SnapshotChangeCategory.NON_BREAKING plan_choice(plan_builder, choice) for validator in plan_validators: validator(context, plan_builder.build()) diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index efaeba8623..35c3628cff 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -63,7 +63,7 @@ def test_forward_only_plan_sets_version(make_snapshot, mocker: MockerFixture): metadata_hash="test_metadata_hash", ), version="test_version", - change_category=SnapshotChangeCategory.FORWARD_ONLY, + change_category=SnapshotChangeCategory.NON_BREAKING, dev_table_suffix="dev", ), ) @@ -97,10 +97,6 @@ def test_forward_only_plan_sets_version(make_snapshot, mocker: MockerFixture): plan_builder.build() assert snapshot_b.version == "test_version" - # Make sure that the choice can't be set manually. - with pytest.raises(PlanError, match="Choice setting is not supported by a forward-only plan."): - plan_builder.set_choice(snapshot_b, SnapshotChangeCategory.BREAKING).build() - def test_forward_only_dev(make_snapshot, mocker: MockerFixture): snapshot = make_snapshot( @@ -258,8 +254,10 @@ def test_forward_only_plan_added_models(make_snapshot, mocker: MockerFixture): ) PlanBuilder(context_diff, forward_only=True).build() - assert snapshot_a.change_category == SnapshotChangeCategory.FORWARD_ONLY + assert snapshot_a.change_category == SnapshotChangeCategory.METADATA assert snapshot_b.change_category == SnapshotChangeCategory.BREAKING + assert snapshot_a.is_forward_only + assert snapshot_b.is_forward_only def test_forward_only_plan_categorizes_change_model_kind_as_breaking( @@ -307,6 +305,7 @@ def test_forward_only_plan_categorizes_change_model_kind_as_breaking( PlanBuilder(context_diff, forward_only=True).build() assert updated_snapshot.change_category == SnapshotChangeCategory.BREAKING + assert not updated_snapshot.is_forward_only def test_paused_forward_only_parent(make_snapshot, mocker: MockerFixture): @@ -322,10 +321,10 @@ def test_paused_forward_only_parent(make_snapshot, mocker: MockerFixture): dev_table_suffix="dev", ), ) - snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot_b_old = make_snapshot(SqlModel(name="b", query=parse_one("select 2, ds from a"))) - snapshot_b_old.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot_b_old.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=False) snapshot_b = make_snapshot(SqlModel(name="b", query=parse_one("select 3, ds from a"))) assert not snapshot_b.version @@ -1104,7 +1103,7 @@ def test_forward_only_revert_not_allowed(make_snapshot, mocker: MockerFixture): assert not snapshot.is_forward_only forward_only_snapshot = make_snapshot(SqlModel(name="a", query=parse_one("select 2, ds"))) - forward_only_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + forward_only_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) forward_only_snapshot.version = snapshot.version forward_only_snapshot.unpaused_ts = now_timestamp() assert forward_only_snapshot.is_forward_only @@ -1191,7 +1190,8 @@ def test_forward_only_plan_seed_models(make_snapshot, mocker: MockerFixture): PlanBuilder(context_diff, forward_only=True).build() assert snapshot_a_updated.version == snapshot_a_updated.fingerprint.to_version() - assert snapshot_a_updated.change_category == SnapshotChangeCategory.NON_BREAKING + assert snapshot_a_updated.change_category == SnapshotChangeCategory.BREAKING + assert not snapshot_a_updated.is_forward_only def test_start_inference(make_snapshot, mocker: MockerFixture): @@ -1443,7 +1443,9 @@ def test_effective_from(make_snapshot, mocker: MockerFixture): forward_only=True, start="2023-01-01", end="2023-03-01", + execution_time="2023-03-02 00:01:00", is_dev=True, + end_bounded=True, ) updated_snapshot.add_interval("2023-01-01", "2023-03-01") @@ -1455,9 +1457,9 @@ def test_effective_from(make_snapshot, mocker: MockerFixture): assert plan_builder.set_effective_from(None).build().effective_from is None assert updated_snapshot.effective_from is None - assert not plan_builder.build().missing_intervals plan_builder.set_effective_from("2023-02-01") + plan_builder.set_start("2023-02-01") assert plan_builder.build().effective_from == "2023-02-01" assert updated_snapshot.effective_from == "2023-02-01" @@ -1677,17 +1679,20 @@ def test_forward_only_models(make_snapshot, mocker: MockerFixture): ) PlanBuilder(context_diff, is_dev=True).build() - assert updated_snapshot.change_category == SnapshotChangeCategory.FORWARD_ONLY + assert updated_snapshot.change_category == SnapshotChangeCategory.BREAKING + assert updated_snapshot.is_forward_only updated_snapshot.change_category = None updated_snapshot.version = None PlanBuilder(context_diff, is_dev=True, forward_only=True).build() - assert updated_snapshot.change_category == SnapshotChangeCategory.FORWARD_ONLY + assert updated_snapshot.change_category == SnapshotChangeCategory.BREAKING + assert updated_snapshot.is_forward_only updated_snapshot.change_category = None updated_snapshot.version = None PlanBuilder(context_diff, forward_only=True).build() - assert updated_snapshot.change_category == SnapshotChangeCategory.FORWARD_ONLY + assert updated_snapshot.change_category == SnapshotChangeCategory.BREAKING + assert updated_snapshot.is_forward_only def test_forward_only_models_model_kind_changed(make_snapshot, mocker: MockerFixture): @@ -1727,16 +1732,16 @@ def test_forward_only_models_model_kind_changed(make_snapshot, mocker: MockerFix @pytest.mark.parametrize( - "partitioned_by, expected_change_category", + "partitioned_by, expected_forward_only", [ - ([], SnapshotChangeCategory.BREAKING), - ([d.parse_one("ds")], SnapshotChangeCategory.FORWARD_ONLY), + ([], False), + ([d.parse_one("ds")], True), ], ) def test_forward_only_models_model_kind_changed_to_incremental_by_time_range( make_snapshot, partitioned_by: t.List[exp.Expression], - expected_change_category: SnapshotChangeCategory, + expected_forward_only: bool, ): snapshot = make_snapshot( SqlModel( @@ -1777,7 +1782,8 @@ def test_forward_only_models_model_kind_changed_to_incremental_by_time_range( ) PlanBuilder(context_diff, is_dev=True).build() - assert updated_snapshot.change_category == expected_change_category + assert updated_snapshot.change_category == SnapshotChangeCategory.BREAKING + assert updated_snapshot.is_forward_only == expected_forward_only def test_indirectly_modified_forward_only_model(make_snapshot, mocker: MockerFixture): @@ -1795,7 +1801,7 @@ def test_indirectly_modified_forward_only_model(make_snapshot, mocker: MockerFix ), nodes={'"a"': snapshot_a.model}, ) - snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) updated_snapshot_b = make_snapshot(snapshot_b.model, nodes={'"a"': updated_snapshot_a.model}) updated_snapshot_b.previous_versions = snapshot_b.all_versions @@ -1868,10 +1874,15 @@ def test_indirectly_modified_forward_only_model(make_snapshot, mocker: MockerFix assert plan.directly_modified == {updated_snapshot_a.snapshot_id} assert updated_snapshot_a.change_category == SnapshotChangeCategory.BREAKING - assert updated_snapshot_b.change_category == SnapshotChangeCategory.FORWARD_ONLY - assert updated_snapshot_c.change_category == SnapshotChangeCategory.FORWARD_ONLY + assert updated_snapshot_b.change_category == SnapshotChangeCategory.INDIRECT_BREAKING + assert updated_snapshot_c.change_category == SnapshotChangeCategory.INDIRECT_BREAKING assert updated_snapshot_d.change_category == SnapshotChangeCategory.INDIRECT_BREAKING + assert not updated_snapshot_a.is_forward_only + assert updated_snapshot_b.is_forward_only + assert not updated_snapshot_c.is_forward_only + assert not updated_snapshot_d.is_forward_only + deployability_index = DeployabilityIndex.create( { updated_snapshot_a.snapshot_id: updated_snapshot_a, @@ -1886,7 +1897,7 @@ def test_indirectly_modified_forward_only_model(make_snapshot, mocker: MockerFix def test_added_model_with_forward_only_parent(make_snapshot, mocker: MockerFixture): snapshot_a = make_snapshot(SqlModel(name="a", query=parse_one("select 1 as a, ds"))) - snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot_b = make_snapshot(SqlModel(name="b", query=parse_one("select a, ds from a"))) @@ -1915,6 +1926,7 @@ def test_added_model_with_forward_only_parent(make_snapshot, mocker: MockerFixtu PlanBuilder(context_diff, is_dev=True).build() assert snapshot_b.change_category == SnapshotChangeCategory.BREAKING + assert not snapshot_b.is_forward_only def test_added_forward_only_model(make_snapshot, mocker: MockerFixture): @@ -2027,7 +2039,7 @@ def test_revert_to_previous_value(make_snapshot, mocker: MockerFixture): snapshot_b = make_snapshot( SqlModel(name="b", query=parse_one("select 1, ds FROM a"), depends_on={"a"}) ) - snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot_b.add_interval("2022-01-01", now()) context_diff = ContextDiff( @@ -2060,7 +2072,8 @@ def test_revert_to_previous_value(make_snapshot, mocker: MockerFixture): plan_builder.set_choice(snapshot_a, SnapshotChangeCategory.BREAKING) plan_builder.build() # Make sure it does not get assigned INDIRECT_BREAKING - assert snapshot_b.change_category == SnapshotChangeCategory.FORWARD_ONLY + assert snapshot_b.change_category == SnapshotChangeCategory.BREAKING + assert snapshot_b.is_forward_only test_add_restatement_fixtures = [ @@ -2407,7 +2420,7 @@ def test_dev_plan_depends_past_non_deployable(make_snapshot, mocker: MockerFixtu name="a_child", query=parse_one("select 1, ds FROM a"), start="2023-01-01", - kind=IncrementalByTimeRangeKind(time_column="ds"), + kind=IncrementalByTimeRangeKind(time_column="ds", forward_only=True), ), nodes={'"a"': updated_snapshot.model}, ) @@ -2457,7 +2470,7 @@ def test_dev_plan_depends_past_non_deployable(make_snapshot, mocker: MockerFixtu def new_builder(start, end): builder = PlanBuilder(context_diff, start=start, end=end, is_dev=True) - builder.set_choice(updated_snapshot, SnapshotChangeCategory.FORWARD_ONLY) + builder.set_choice(updated_snapshot, SnapshotChangeCategory.BREAKING) builder.set_choice(snapshot_child, SnapshotChangeCategory.BREAKING) builder.set_choice(unrelated_snapshot, SnapshotChangeCategory.BREAKING) return builder @@ -3146,15 +3159,14 @@ def test_set_choice_for_forward_only_model(make_snapshot): ) plan_builder = PlanBuilder(context_diff, is_dev=True) - - with pytest.raises(PlanError, match='Forward-only model "a" cannot be categorized manually.'): - plan_builder.set_choice(updated_snapshot, SnapshotChangeCategory.BREAKING) + plan_builder.set_choice(updated_snapshot, SnapshotChangeCategory.BREAKING) plan = plan_builder.build() assert ( plan.snapshots[updated_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.FORWARD_ONLY + == SnapshotChangeCategory.BREAKING ) + assert plan.snapshots[updated_snapshot.snapshot_id].is_forward_only def test_user_provided_flags(sushi_context: Context): diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index d79be24262..f560b93251 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -594,14 +594,14 @@ def test_build_plan_stages_forward_only( snapshot_a.model.copy(update={"stamp": "new_version"}), ) new_snapshot_a.previous_versions = snapshot_a.all_versions - new_snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot_a.categorize_as(SnapshotChangeCategory.NON_BREAKING, forward_only=True) new_snapshot_b = make_snapshot( snapshot_b.model.copy(), nodes={'"a"': new_snapshot_a.model}, ) new_snapshot_b.previous_versions = snapshot_b.all_versions - new_snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot_b.categorize_as(SnapshotChangeCategory.INDIRECT_NON_BREAKING, forward_only=True) state_reader = mocker.Mock(spec=StateReader) state_reader.get_snapshots.return_value = {} @@ -732,14 +732,14 @@ def test_build_plan_stages_forward_only_dev( snapshot_a.model.copy(update={"stamp": "new_version"}), ) new_snapshot_a.previous_versions = snapshot_a.all_versions - new_snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot_a.categorize_as(SnapshotChangeCategory.NON_BREAKING, forward_only=True) new_snapshot_b = make_snapshot( snapshot_b.model.copy(), nodes={'"a"': new_snapshot_a.model}, ) new_snapshot_b.previous_versions = snapshot_b.all_versions - new_snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot_b.categorize_as(SnapshotChangeCategory.INDIRECT_NON_BREAKING, forward_only=True) state_reader = mocker.Mock(spec=StateReader) state_reader.get_snapshots.return_value = {} @@ -968,14 +968,14 @@ def test_build_plan_stages_forward_only_ensure_finalized_snapshots( snapshot_a.model.copy(update={"stamp": "new_version"}), ) new_snapshot_a.previous_versions = snapshot_a.all_versions - new_snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot_a.categorize_as(SnapshotChangeCategory.NON_BREAKING, forward_only=True) new_snapshot_b = make_snapshot( snapshot_b.model.copy(), nodes={'"a"': new_snapshot_a.model}, ) new_snapshot_b.previous_versions = snapshot_b.all_versions - new_snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot_b.categorize_as(SnapshotChangeCategory.INDIRECT_NON_BREAKING, forward_only=True) state_reader = mocker.Mock(spec=StateReader) state_reader.get_snapshots.return_value = {} diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index d71cbc4db6..0ba7180fb6 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -171,6 +171,7 @@ def test_json(snapshot: Snapshot): "version": snapshot.fingerprint.to_version(), "migrated": False, "unrestorable": False, + "forward_only": False, } @@ -264,7 +265,8 @@ def test_add_interval(snapshot: Snapshot, make_snapshot): def test_add_interval_dev(snapshot: Snapshot, make_snapshot): snapshot.version = "existing_version" snapshot.dev_version_ = "existing_dev_version" - snapshot.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True snapshot.add_interval("2020-01-01", "2020-01-01") assert snapshot.intervals == [(to_timestamp("2020-01-01"), to_timestamp("2020-01-02"))] @@ -1169,7 +1171,7 @@ def test_snapshot_table_name(snapshot: Snapshot, make_snapshot: t.Callable): data_hash="2", metadata_hash="1", parent_data_hash="1" ) snapshot.previous_versions = (previous_data_version,) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) assert snapshot.table_name(is_deployable=True) == "sqlmesh__default.name__3078928823" assert snapshot.table_name(is_deployable=False) == "sqlmesh__default.name__3049392110__dev" @@ -1320,7 +1322,7 @@ def test_table_naming_convention_change_reuse_previous_version(make_snapshot): assert changed_snapshot.previous_version == original_snapshot.data_version - changed_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + changed_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) # inherited from previous version even though changed_snapshot was created with TableNamingConvention.HASH_MD5 assert changed_snapshot.table_naming_convention == TableNamingConvention.SCHEMA_AND_TABLE @@ -1725,7 +1727,7 @@ def test_physical_schema(snapshot: Snapshot): new_snapshot.previous_versions = (snapshot.data_version,) new_snapshot.physical_schema_ = None new_snapshot.version = None - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) assert new_snapshot.physical_schema == "custom_schema" assert new_snapshot.data_version.physical_schema == "custom_schema" @@ -1735,7 +1737,7 @@ def test_physical_schema(snapshot: Snapshot): def test_has_paused_forward_only(snapshot: Snapshot): assert not has_paused_forward_only([snapshot], [snapshot]) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) assert has_paused_forward_only([snapshot], [snapshot]) snapshot.unpaused_ts = to_timestamp("2023-01-01") @@ -2029,7 +2031,7 @@ def test_deployability_index(make_snapshot): snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING) snapshot_b = make_snapshot(SqlModel(name="b", query=parse_one("SELECT 1"))) - snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot_b.parents = (snapshot_a.snapshot_id,) snapshot_c = make_snapshot(SqlModel(name="c", query=parse_one("SELECT 1"))) @@ -2093,7 +2095,7 @@ def test_deployability_index(make_snapshot): def test_deployability_index_unpaused_forward_only(make_snapshot): snapshot_a = make_snapshot(SqlModel(name="a", query=parse_one("SELECT 1"))) - snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot_a.unpaused_ts = 1 snapshot_b = make_snapshot(SqlModel(name="b", query=parse_one("SELECT 1"))) @@ -2120,7 +2122,7 @@ def test_deployability_index_unpaused_auto_restatement(make_snapshot): ), ) snapshot_a = make_snapshot(model_a) - snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot_a.unpaused_ts = 1 # Snapshot B is a child of a model with auto restatement and is not paused, @@ -2216,11 +2218,11 @@ def test_deployability_index_categorized_forward_only_model(make_snapshot): snapshot_a = make_snapshot(model_a) snapshot_a.previous_versions = snapshot_a_old.all_versions - snapshot_a.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot_b = make_snapshot(SqlModel(name="b", query=parse_one("SELECT 1"))) snapshot_b.parents = (snapshot_a.snapshot_id,) - snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) deployability_index = DeployabilityIndex.create( {s.snapshot_id: s for s in [snapshot_a, snapshot_b]} @@ -2238,7 +2240,7 @@ def test_deployability_index_missing_parent(make_snapshot): snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING) snapshot_b = make_snapshot(SqlModel(name="b", query=parse_one("SELECT 1"))) - snapshot_b.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot_b.parents = (snapshot_a.snapshot_id,) deplyability_index = DeployabilityIndex.create({snapshot_b.snapshot_id: snapshot_b}) @@ -2802,7 +2804,7 @@ def test_physical_version_pin_for_new_forward_only_models(make_snapshot): ), ) snapshot_c.previous_versions = snapshot_b.all_versions - snapshot_c.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_c.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) assert snapshot_b.fingerprint != snapshot_c.fingerprint assert snapshot_b.version == snapshot_c.version @@ -2832,7 +2834,7 @@ def test_physical_version_pin_for_new_forward_only_models(make_snapshot): ), ) snapshot_e.previous_versions = snapshot_d.all_versions - snapshot_e.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_e.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) assert snapshot_d.fingerprint != snapshot_e.fingerprint assert snapshot_d.version == snapshot_e.version @@ -2849,7 +2851,7 @@ def test_physical_version_pin_for_new_forward_only_models(make_snapshot): ), ) snapshot_f.previous_versions = snapshot_e.all_versions - snapshot_f.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot_f.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) assert snapshot_f.version == "1234" assert snapshot_f.fingerprint != snapshot_e.fingerprint diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index fc5df244b3..4b028e148b 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -370,7 +370,7 @@ def test_promote_forward_only(mocker: MockerFixture, adapter_mock, make_snapshot ) snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.version = "test_version" evaluator.promote( @@ -428,7 +428,7 @@ def create_and_cleanup(name: str, dev_table_only: bool): ) snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.version = "test_version" evaluator.promote([snapshot], EnvironmentNamingInfo(name="test_env")) @@ -810,42 +810,58 @@ def test_create_new_forward_only_model(mocker: MockerFixture, adapter_mock, make @pytest.mark.parametrize( - "deployability_index, snapshot_category, deployability_flags", + "deployability_index, snapshot_category, forward_only, deployability_flags", [ - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, [False]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.NON_BREAKING, [False]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.FORWARD_ONLY, [True]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.INDIRECT_BREAKING, [False]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.INDIRECT_NON_BREAKING, [True]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.METADATA, [True]), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, False, [False]), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.NON_BREAKING, False, [False]), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, True, [True]), + ( + DeployabilityIndex.all_deployable(), + SnapshotChangeCategory.INDIRECT_BREAKING, + False, + [False], + ), + ( + DeployabilityIndex.all_deployable(), + SnapshotChangeCategory.INDIRECT_NON_BREAKING, + False, + [True], + ), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.METADATA, False, [True]), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.BREAKING, + False, [True, False], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.NON_BREAKING, + False, [True, False], ), ( DeployabilityIndex.none_deployable(), - SnapshotChangeCategory.FORWARD_ONLY, + SnapshotChangeCategory.BREAKING, + True, [True], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.INDIRECT_BREAKING, + False, [True, False], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.INDIRECT_NON_BREAKING, + False, [True], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.METADATA, + False, [True], ), ], @@ -857,12 +873,13 @@ def test_create_tables_exist( deployability_index: DeployabilityIndex, deployability_flags: t.List[bool], snapshot_category: SnapshotChangeCategory, + forward_only: bool, ): adapter_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter") adapter_mock.dialect = "duckdb" evaluator = SnapshotEvaluator(adapter_mock) - snapshot.categorize_as(category=snapshot_category) + snapshot.categorize_as(category=snapshot_category, forward_only=forward_only) adapter_mock.get_data_objects.return_value = [ DataObject( @@ -909,7 +926,7 @@ def test_create_prod_table_exists_forward_only(mocker: MockerFixture, adapter_mo ) snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) adapter_mock.get_data_objects.return_value = [ DataObject( @@ -1179,7 +1196,8 @@ def columns(table_name): query=parse_one("SELECT c, a FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), ) snapshot = make_snapshot(model, version="1") - snapshot.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True snapshot.previous_versions = snapshot.all_versions evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) @@ -1217,7 +1235,8 @@ def test_migrate_missing_table(mocker: MockerFixture, make_snapshot): post_statements=[parse_one("DROP TABLE pre")], ) snapshot = make_snapshot(model, version="1") - snapshot.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True snapshot.previous_versions = snapshot.all_versions evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) @@ -1234,11 +1253,17 @@ def test_migrate_missing_table(mocker: MockerFixture, make_snapshot): @pytest.mark.parametrize( - "change_category", - [SnapshotChangeCategory.FORWARD_ONLY, SnapshotChangeCategory.INDIRECT_NON_BREAKING], + "change_category, forward_only", + [ + (SnapshotChangeCategory.BREAKING, True), + (SnapshotChangeCategory.INDIRECT_NON_BREAKING, False), + ], ) def test_migrate_view( - mocker: MockerFixture, make_snapshot, change_category: SnapshotChangeCategory + mocker: MockerFixture, + make_snapshot, + change_category: SnapshotChangeCategory, + forward_only: bool, ): connection_mock = mocker.NonCallableMock() cursor_mock = mocker.Mock() @@ -1255,6 +1280,7 @@ def test_migrate_view( ) snapshot = make_snapshot(model, version="1") snapshot.change_category = change_category + snapshot.forward_only = forward_only evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) @@ -1316,7 +1342,7 @@ def test_migrate_duckdb(snapshot: Snapshot, duck_conn, make_snapshot): updated_model = SqlModel.parse_obj(updated_model_dict) new_snapshot = make_snapshot(updated_model) - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.version = snapshot.version evaluator.create([new_snapshot], {}) @@ -1468,7 +1494,7 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) ) snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.previous_versions = snapshot.all_versions adapter_mock.get_data_objects.return_value = [ @@ -1535,7 +1561,7 @@ def test_create_clone_in_dev_missing_table(mocker: MockerFixture, adapter_mock, ) snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.previous_versions = snapshot.all_versions evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) @@ -1580,7 +1606,7 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m ) snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.previous_versions = snapshot.all_versions adapter_mock.get_data_objects.return_value = [ @@ -1643,7 +1669,7 @@ def test_create_clone_in_dev_self_referencing( ) snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.previous_versions = snapshot.all_versions adapter_mock.get_data_objects.return_value = [ @@ -1715,7 +1741,8 @@ def columns(table_name): query=parse_one("SELECT c, a FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), ) snapshot = make_snapshot(model, version="1") - snapshot.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True snapshot.previous_versions = snapshot.all_versions with pytest.raises(NodeExecutionFailedError) as ex: @@ -1737,7 +1764,8 @@ def columns(table_name): query=parse_one("SELECT c, a FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), ) snapshot = make_snapshot(model, version="1") - snapshot.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True snapshot.previous_versions = snapshot.all_versions logger = logging.getLogger("sqlmesh.core.snapshot.evaluator") @@ -1779,7 +1807,7 @@ def test_forward_only_snapshot_for_added_model(mocker: MockerFixture, adapter_mo ) snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) evaluator.create([snapshot], {}) @@ -3517,7 +3545,7 @@ def test_create_managed_forward_only_with_previous_version_doesnt_clone_for_dev_ ) snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.previous_versions = ( SnapshotDataVersion( fingerprint=SnapshotFingerprint( @@ -3525,7 +3553,7 @@ def test_create_managed_forward_only_with_previous_version_doesnt_clone_for_dev_ metadata_hash="test_metadata_hash", ), version="test_version", - change_category=SnapshotChangeCategory.FORWARD_ONLY, + change_category=SnapshotChangeCategory.BREAKING, dev_table_suffix="dev", ), ) @@ -3551,46 +3579,58 @@ def test_create_managed_forward_only_with_previous_version_doesnt_clone_for_dev_ @pytest.mark.parametrize( - "deployability_index, snapshot_category, deployability_flags", + "deployability_index, snapshot_category, forward_only, deployability_flags", [ - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, [True]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.NON_BREAKING, [True]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.FORWARD_ONLY, [False]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.INDIRECT_BREAKING, [True]), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, False, [True]), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.NON_BREAKING, False, [True]), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, True, [False]), + ( + DeployabilityIndex.all_deployable(), + SnapshotChangeCategory.INDIRECT_BREAKING, + False, + [True], + ), ( DeployabilityIndex.all_deployable(), SnapshotChangeCategory.INDIRECT_NON_BREAKING, + False, [False], ), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.METADATA, [False]), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.METADATA, False, [False]), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.BREAKING, + False, [False, True], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.NON_BREAKING, + False, [False, True], ), ( DeployabilityIndex.none_deployable(), - SnapshotChangeCategory.FORWARD_ONLY, + SnapshotChangeCategory.BREAKING, + True, [False], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.INDIRECT_BREAKING, + False, [False, True], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.INDIRECT_NON_BREAKING, + False, [False], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.METADATA, + False, [False], ), ], @@ -3602,12 +3642,13 @@ def test_create_snapshot( deployability_index: DeployabilityIndex, deployability_flags: t.List[bool], snapshot_category: SnapshotChangeCategory, + forward_only: bool, ): adapter_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter") adapter_mock.dialect = "duckdb" evaluator = SnapshotEvaluator(adapter_mock) - snapshot.categorize_as(category=snapshot_category) + snapshot.categorize_as(category=snapshot_category, forward_only=forward_only) evaluator._create_snapshot( snapshot=snapshot, snapshots={}, @@ -3657,7 +3698,7 @@ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_moc updated_model = SqlModel.parse_obj(updated_model_dict) new_snapshot = make_snapshot(updated_model) - new_snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) new_snapshot.previous_versions = snapshot.all_versions new_snapshot.version = snapshot.version @@ -3728,7 +3769,7 @@ def test_migrate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): ) ) snapshot: Snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.FORWARD_ONLY) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.previous_versions = snapshot.all_versions # no schema changes - no-op @@ -3930,7 +3971,8 @@ def columns(table_name): query=parse_one("SELECT c FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), ) snapshot_1 = make_snapshot(model, version="1") - snapshot_1.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot_1.change_category = SnapshotChangeCategory.BREAKING + snapshot_1.forward_only = True snapshot_1.previous_versions = snapshot_1.all_versions model_2 = SqlModel( name="test_schema.test_model_2", @@ -3941,7 +3983,8 @@ def columns(table_name): query=parse_one("SELECT c FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), ) snapshot_2 = make_snapshot(model_2, version="1") - snapshot_2.change_category = SnapshotChangeCategory.FORWARD_ONLY + snapshot_2.change_category = SnapshotChangeCategory.BREAKING + snapshot_2.forward_only = True snapshot_2.previous_versions = snapshot_2.all_versions evaluator.migrate( [snapshot_1, snapshot_2], {}, deployability_index=DeployabilityIndex.none_deployable() From 9eba5c111946382834c25744150b344d2a90a21e Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 8 Aug 2025 23:53:11 +0300 Subject: [PATCH 0672/1056] Fix!: Add model default audits in the model preserving their args (#5106) --- sqlmesh/core/dialect.py | 35 +++++ sqlmesh/core/loader.py | 1 - sqlmesh/core/model/definition.py | 16 +- sqlmesh/core/model/meta.py | 34 +--- tests/core/test_audit.py | 112 +++++++++++++ tests/core/test_integration.py | 262 +++++++++++++++++++++++++++++++ tests/core/test_model.py | 25 +-- 7 files changed, 431 insertions(+), 54 deletions(-) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 37568f2b27..568d9f5f73 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -1408,6 +1408,41 @@ def extract_func_call( return func.lower(), kwargs +def extract_function_calls(func_calls: t.Any, allow_tuples: bool = False) -> t.Any: + """Used for extracting function calls for signals or audits.""" + + if isinstance(func_calls, (exp.Tuple, exp.Array)): + return [extract_func_call(i, allow_tuples=allow_tuples) for i in func_calls.expressions] + if isinstance(func_calls, exp.Paren): + return [extract_func_call(func_calls.this, allow_tuples=allow_tuples)] + if isinstance(func_calls, exp.Expression): + return [extract_func_call(func_calls, allow_tuples=allow_tuples)] + if isinstance(func_calls, list): + function_calls = [] + for entry in func_calls: + if isinstance(entry, dict): + args = entry + name = "" if allow_tuples else entry.pop("name") + elif isinstance(entry, (tuple, list)): + name, args = entry + else: + raise ConfigError(f"Audit must be a dictionary or named tuple. Got {entry}.") + + function_calls.append( + ( + name.lower(), + { + key: parse_one(value) if isinstance(value, str) else value + for key, value in args.items() + }, + ) + ) + + return function_calls + + return func_calls or [] + + def is_meta_expression(v: t.Any) -> bool: return isinstance(v, (Audit, Metric, Model)) diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index f4c9147d1b..b593da1ad0 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -594,7 +594,6 @@ def _load_sql_models( macros=macros, jinja_macros=jinja_macros, audit_definitions=audits, - default_audits=self.config.model_defaults.audits, module_path=self.config_path, dialect=self.config.model_defaults.dialect, time_column_format=self.config.time_column_format, diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 4485875df8..954b03eff8 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -32,7 +32,7 @@ sorted_python_env_payloads, validate_extra_and_required_fields, ) -from sqlmesh.core.model.meta import ModelMeta, FunctionCall +from sqlmesh.core.model.meta import ModelMeta from sqlmesh.core.model.kind import ( ModelKindName, SeedKind, @@ -2038,7 +2038,6 @@ def load_sql_based_model( macros: t.Optional[MacroRegistry] = None, jinja_macros: t.Optional[JinjaMacroRegistry] = None, audits: t.Optional[t.Dict[str, ModelAudit]] = None, - default_audits: t.Optional[t.List[FunctionCall]] = None, python_env: t.Optional[t.Dict[str, Executable]] = None, dialect: t.Optional[str] = None, physical_schema_mapping: t.Optional[t.Dict[re.Pattern, str]] = None, @@ -2211,7 +2210,6 @@ def load_sql_based_model( physical_schema_mapping=physical_schema_mapping, default_catalog=default_catalog, variables=variables, - default_audits=default_audits, inline_audits=inline_audits, blueprint_variables=blueprint_variables, **meta_fields, @@ -2431,7 +2429,6 @@ def _create_model( physical_schema_mapping: t.Optional[t.Dict[re.Pattern, str]] = None, python_env: t.Optional[t.Dict[str, Executable]] = None, audit_definitions: t.Optional[t.Dict[str, ModelAudit]] = None, - default_audits: t.Optional[t.List[FunctionCall]] = None, inline_audits: t.Optional[t.Dict[str, ModelAudit]] = None, module_path: Path = Path(), macros: t.Optional[MacroRegistry] = None, @@ -2541,6 +2538,10 @@ def _create_model( for jinja_macro in jinja_macros.root_macros.values(): used_variables.update(extract_macro_references_and_variables(jinja_macro.definition)[1]) + # Merge model-specific audits with default audits + if default_audits := defaults.pop("audits", None): + kwargs["audits"] = default_audits + d.extract_function_calls(kwargs.pop("audits", [])) + model = klass( name=name, **{ @@ -2558,12 +2559,7 @@ def _create_model( **(inline_audits or {}), } - # TODO: default_audits needs to be merged with model.audits; the former's arguments - # are silently dropped today because we add them in audit_definitions. We also need - # to check for duplicates when we implement this merging logic. - used_audits: t.Set[str] = set() - used_audits.update(audit_name for audit_name, _ in default_audits or []) - used_audits.update(audit_name for audit_name, _ in model.audits) + used_audits: t.Set[str] = {audit_name for audit_name, _ in model.audits} audit_definitions = { audit_name: audit_definitions[audit_name] diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 585bb15a6c..b5371ab811 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -11,7 +11,7 @@ from sqlmesh.core import dialect as d from sqlmesh.core.config.linter import LinterConfig -from sqlmesh.core.dialect import normalize_model_name, extract_func_call +from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core.model.common import ( bool_validator, default_catalog_validator, @@ -94,37 +94,7 @@ class ModelMeta(_Node): def _func_call_validator(cls, v: t.Any, field: t.Any) -> t.Any: is_signal = getattr(field, "name" if hasattr(field, "name") else "field_name") == "signals" - if isinstance(v, (exp.Tuple, exp.Array)): - return [extract_func_call(i, allow_tuples=is_signal) for i in v.expressions] - if isinstance(v, exp.Paren): - return [extract_func_call(v.this, allow_tuples=is_signal)] - if isinstance(v, exp.Expression): - return [extract_func_call(v, allow_tuples=is_signal)] - if isinstance(v, list): - audits = [] - - for entry in v: - if isinstance(entry, dict): - args = entry - name = "" if is_signal else entry.pop("name") - elif isinstance(entry, (tuple, list)): - name, args = entry - else: - raise ConfigError(f"Audit must be a dictionary or named tuple. Got {entry}.") - - audits.append( - ( - name.lower(), - { - key: d.parse_one(value) if isinstance(value, str) else value - for key, value in args.items() - }, - ) - ) - - return audits - - return v or [] + return d.extract_function_calls(v, allow_tuples=is_signal) @field_validator("tags", mode="before") def _value_or_tuple_validator(cls, v: t.Any, info: ValidationInfo) -> t.Any: diff --git a/tests/core/test_audit.py b/tests/core/test_audit.py index da8145ba87..81335e5f1a 100644 --- a/tests/core/test_audit.py +++ b/tests/core/test_audit.py @@ -3,6 +3,7 @@ from sqlglot import exp, parse_one from sqlmesh.core import constants as c +from sqlmesh.core.config.model import ModelDefaultsConfig from sqlmesh.core.context import Context from sqlmesh.core.audit import ( ModelAudit, @@ -962,6 +963,117 @@ def test_multiple_audits_with_same_name(): assert model.audits[1][1] == model.audits[2][1] +def test_default_audits_included_when_no_model_audits(): + expressions = parse(""" + MODEL ( + name test.basic_model + ); + SELECT 1 as id, 'test' as name; + """) + + model_defaults = ModelDefaultsConfig( + dialect="duckdb", audits=["not_null(columns := ['id'])", "unique_values(columns := ['id'])"] + ) + model = load_sql_based_model(expressions, defaults=model_defaults.dict()) + + assert len(model.audits) == 2 + audit_names = [audit[0] for audit in model.audits] + assert "not_null" in audit_names + assert "unique_values" in audit_names + + # Verify arguments are preserved + for audit_name, audit_args in model.audits: + if audit_name == "not_null": + assert "columns" in audit_args + assert audit_args["columns"].expressions[0].this == "id" + elif audit_name == "unique_values": + assert "columns" in audit_args + assert audit_args["columns"].expressions[0].this == "id" + + for audit_name, audit_args in model.audits_with_args: + if audit_name == "not_null": + assert "columns" in audit_args + assert audit_args["columns"].expressions[0].this == "id" + elif audit_name == "unique_values": + assert "columns" in audit_args + assert audit_args["columns"].expressions[0].this == "id" + + +def test_model_defaults_audits_with_same_name(): + expressions = parse( + """ + MODEL ( + name db.table, + dialect spark, + audits( + does_not_exceed_threshold(column := id, threshold := 1000), + does_not_exceed_threshold(column := price, threshold := 100), + unique_values(columns := ['id']) + ) + ); + + SELECT id, price FROM tbl; + + AUDIT ( + name does_not_exceed_threshold, + ); + SELECT * FROM @this_model + WHERE @column >= @threshold; + """ + ) + + model_defaults = ModelDefaultsConfig( + dialect="duckdb", + audits=[ + "does_not_exceed_threshold(column := price, threshold := 33)", + "does_not_exceed_threshold(column := id, threshold := 65)", + "not_null(columns := ['id'])", + ], + ) + model = load_sql_based_model(expressions, defaults=model_defaults.dict()) + assert len(model.audits) == 6 + assert len(model.audits_with_args) == 6 + assert len(model.audit_definitions) == 1 + + expected_audits = [ + ( + "does_not_exceed_threshold", + {"column": exp.column("price"), "threshold": exp.Literal.number(33)}, + ), + ( + "does_not_exceed_threshold", + {"column": exp.column("id"), "threshold": exp.Literal.number(65)}, + ), + ("not_null", {"columns": exp.convert(["id"])}), + ( + "does_not_exceed_threshold", + {"column": exp.column("id"), "threshold": exp.Literal.number(1000)}, + ), + ( + "does_not_exceed_threshold", + {"column": exp.column("price"), "threshold": exp.Literal.number(100)}, + ), + ("unique_values", {"columns": exp.convert(["id"])}), + ] + + for (actual_name, actual_args), (expected_name, expected_args) in zip( + model.audits, expected_audits + ): + # Validate the audit names are preserved + assert actual_name == expected_name + for key in expected_args: + # comparing sql representaion is easier + assert actual_args[key].sql() == expected_args[key].sql() + + # Validate audits with args as well along with their arguments + for (actual_audit, actual_args), (expected_name, expected_args) in zip( + model.audits_with_args, expected_audits + ): + assert actual_audit.name == expected_name + for key in expected_args: + assert actual_args[key].sql() == expected_args[key].sql() + + def test_audit_formatting_flag_serde(): expressions = parse( """ diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index d7edd8d131..d15e097875 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -7242,3 +7242,265 @@ def test_physical_table_naming_strategy_hash_md5(copy_to_temp_path: t.Callable): s.table_naming_convention == TableNamingConvention.HASH_MD5 for s in prod_env_snapshots.values() ) + + +@pytest.mark.slow +def test_default_audits_applied_in_plan(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir(exist_ok=True) + + # Create a model with data that will pass the audits + create_temp_file( + tmp_path, + models_dir / "orders.sql", + dedent(""" + MODEL ( + name test.orders, + kind FULL + ); + + SELECT + 1 AS order_id, + 'customer_1' AS customer_id, + 100.50 AS amount, + '2024-01-01'::DATE AS order_date + UNION ALL + SELECT + 2 AS order_id, + 'customer_2' AS customer_id, + 200.75 AS amount, + '2024-01-02'::DATE AS order_date + """), + ) + + config = Config( + model_defaults=ModelDefaultsConfig( + dialect="duckdb", + audits=[ + "not_null(columns := [order_id, customer_id])", + "unique_values(columns := [order_id])", + ], + ) + ) + + context = Context(paths=tmp_path, config=config) + + # Create and apply plan, here audits should pass + plan = context.plan("prod", no_prompts=True) + context.apply(plan) + + # Verify model has the default audits + model = context.get_model("test.orders") + assert len(model.audits) == 2 + + audit_names = [audit[0] for audit in model.audits] + assert "not_null" in audit_names + assert "unique_values" in audit_names + + # Verify audit arguments are preserved + for audit_name, audit_args in model.audits: + if audit_name == "not_null": + assert "columns" in audit_args + columns = [col.name for col in audit_args["columns"].expressions] + assert "order_id" in columns + assert "customer_id" in columns + elif audit_name == "unique_values": + assert "columns" in audit_args + columns = [col.name for col in audit_args["columns"].expressions] + assert "order_id" in columns + + +@pytest.mark.slow +def test_default_audits_fail_on_bad_data(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir(exist_ok=True) + + # Create a model with data that violates NOT NULL constraint + create_temp_file( + tmp_path, + models_dir / "bad_orders.sql", + dedent(""" + MODEL ( + name test.bad_orders, + kind FULL + ); + + SELECT + 1 AS order_id, + NULL AS customer_id, -- This violates NOT NULL + 100.50 AS amount, + '2024-01-01'::DATE AS order_date + UNION ALL + SELECT + 2 AS order_id, + 'customer_2' AS customer_id, + 200.75 AS amount, + '2024-01-02'::DATE AS order_date + """), + ) + + config = Config( + model_defaults=ModelDefaultsConfig( + dialect="duckdb", audits=["not_null(columns := [customer_id])"] + ) + ) + + context = Context(paths=tmp_path, config=config) + + # Plan should fail due to audit failure + with pytest.raises(PlanError): + context.plan("prod", no_prompts=True, auto_apply=True) + + +@pytest.mark.slow +def test_default_audits_with_model_specific_audits(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir(exist_ok=True) + audits_dir = tmp_path / "audits" + audits_dir.mkdir(exist_ok=True) + + create_temp_file( + tmp_path, + audits_dir / "range_check.sql", + dedent(""" + AUDIT ( + name range_check + ); + + SELECT * FROM @this_model + WHERE @column < @min_value OR @column > @max_value + """), + ) + + # Create a model with its own audits in addition to defaults + create_temp_file( + tmp_path, + models_dir / "products.sql", + dedent(""" + MODEL ( + name test.products, + kind FULL, + audits ( + range_check(column := price, min_value := 0, max_value := 10000) + ) + ); + + SELECT + 1 AS product_id, + 'Widget' AS product_name, + 99.99 AS price + UNION ALL + SELECT + 2 AS product_id, + 'Gadget' AS product_name, + 149.99 AS price + """), + ) + + config = Config( + model_defaults=ModelDefaultsConfig( + dialect="duckdb", + audits=[ + "not_null(columns := [product_id, product_name])", + "unique_values(columns := [product_id])", + ], + ) + ) + + context = Context(paths=tmp_path, config=config) + + # Create and apply plan + plan = context.plan("prod", no_prompts=True) + context.apply(plan) + + # Verify model has both default and model-specific audits + model = context.get_model("test.products") + assert len(model.audits) == 3 + + audit_names = [audit[0] for audit in model.audits] + assert "not_null" in audit_names + assert "unique_values" in audit_names + assert "range_check" in audit_names + + # Verify audit execution order, default audits first then model-specific + assert model.audits[0][0] == "not_null" + assert model.audits[1][0] == "unique_values" + assert model.audits[2][0] == "range_check" + + +@pytest.mark.slow +def test_default_audits_with_custom_audit_definitions(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir(exist_ok=True) + audits_dir = tmp_path / "audits" + audits_dir.mkdir(exist_ok=True) + + # Create custom audit definition + create_temp_file( + tmp_path, + audits_dir / "positive_amount.sql", + dedent(""" + AUDIT ( + name positive_amount + ); + + SELECT * FROM @this_model + WHERE @column <= 0 + """), + ) + + # Create a model + create_temp_file( + tmp_path, + models_dir / "transactions.sql", + dedent(""" + MODEL ( + name test.transactions, + kind FULL + ); + + SELECT + 1 AS transaction_id, + 'TXN001' AS transaction_code, + 250.00 AS amount, + '2024-01-01'::DATE AS transaction_date + UNION ALL + SELECT + 2 AS transaction_id, + 'TXN002' AS transaction_code, + 150.00 AS amount, + '2024-01-02'::DATE AS transaction_date + """), + ) + + config = Config( + model_defaults=ModelDefaultsConfig( + dialect="duckdb", + audits=[ + "not_null(columns := [transaction_id, transaction_code])", + "unique_values(columns := [transaction_id])", + "positive_amount(column := amount)", + ], + ) + ) + + context = Context(paths=tmp_path, config=config) + + # Create and apply plan + plan = context.plan("prod", no_prompts=True) + context.apply(plan) + + # Verify model has all default audits including custom + model = context.get_model("test.transactions") + assert len(model.audits) == 3 + + audit_names = [audit[0] for audit in model.audits] + assert "not_null" in audit_names + assert "unique_values" in audit_names + assert "positive_amount" in audit_names + + # Verify custom audit arguments + for audit_name, audit_args in model.audits: + if audit_name == "positive_amount": + assert "column" in audit_args + assert audit_args["column"].name == "amount" diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 3cadbae9ca..0be1702fa1 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -1672,13 +1672,13 @@ def test_enable_audits_from_model_defaults(): model = load_sql_based_model( expressions, path=Path("./examples/sushi/models/test_model.sql"), - default_audits=model_defaults.audits, + defaults=model_defaults.dict(), ) - assert len(model.audits) == 0 + assert len(model.audits) == 1 config = Config(model_defaults=model_defaults) - assert config.model_defaults.audits[0] == ("assert_positive_order_ids", {}) + assert config.model_defaults.audits[0] == ("assert_positive_order_ids", {}) == model.audits[0] audits_with_args = model.audits_with_args assert len(audits_with_args) == 1 @@ -7253,23 +7253,26 @@ def max_value(evaluator: MacroEvaluator) -> int: "assert_max_value": load_audit(audit_expression, dialect="duckdb"), "assert_not_zero": load_audit(not_zero_audit, dialect="duckdb"), } - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb", audits=["assert_not_zero"]) - ) + model_defaults = ModelDefaultsConfig(dialect="duckdb", audits=["assert_not_zero"]) + model = load_sql_based_model( model_expression, - audits=audits, - default_audits=config.model_defaults.audits, + defaults=model_defaults.dict(), audit_definitions=audits, ) - assert len(model.audits) == 2 + assert len(model.audits) == 3 audits_with_args = model.audits_with_args assert len(audits_with_args) == 3 assert len(model.python_env) == 3 - assert config.model_defaults.audits == [("assert_not_zero", {})] - assert model.audits == [("assert_max_value", {}), ("assert_positive_ids", {})] + assert model.audits == [ + ("assert_not_zero", {}), + ("assert_max_value", {}), + ("assert_positive_ids", {}), + ] assert isinstance(audits_with_args[0][0], ModelAudit) + assert isinstance(audits_with_args[1][0], ModelAudit) + assert isinstance(audits_with_args[2][0], ModelAudit) assert isinstance(model.python_env["min_value"], Executable) assert isinstance(model.python_env["max_value"], Executable) assert isinstance(model.python_env["zero_value"], Executable) From dc478624bb9f1187772aedb1030653dde86f319e Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:50:30 +0100 Subject: [PATCH 0673/1056] chore(vscode): reenable quickfix test (#5120) --- vscode/extension/tests/quickfix.spec.ts | 119 ++++++++++++------------ 1 file changed, 58 insertions(+), 61 deletions(-) diff --git a/vscode/extension/tests/quickfix.spec.ts b/vscode/extension/tests/quickfix.spec.ts index 84896713aa..60d0207f7c 100644 --- a/vscode/extension/tests/quickfix.spec.ts +++ b/vscode/extension/tests/quickfix.spec.ts @@ -8,27 +8,25 @@ import { import { test, expect } from './fixtures' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' -test.fixme( - 'noselectstar quickfix', - async ({ page, sharedCodeServer, tempDir }) => { - await fs.copy(SUSHI_SOURCE_PATH, tempDir) - await createPythonInterpreterSettingsSpecifier(tempDir) +test('noselectstar quickfix', async ({ page, sharedCodeServer, tempDir }) => { + await fs.copy(SUSHI_SOURCE_PATH, tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) - // Override the settings for the linter - const configPath = path.join(tempDir, 'config.py') - const read = await fs.readFile(configPath, 'utf8') - // Replace linter to be on - const target = 'enabled=True' - const replaced = read.replace('enabled=False', 'enabled=True') - // Assert replaced correctly - expect(replaced).toContain(target) + // Override the settings for the linter + const configPath = path.join(tempDir, 'config.py') + const read = await fs.readFile(configPath, 'utf8') + // Replace linter to be on + const target = 'enabled=True' + const replaced = read.replace('enabled=False', 'enabled=True') + // Assert replaced correctly + expect(replaced).toContain(target) - // Replace the rules to only have noselectstar - const targetRules = `rules=[ + // Replace the rules to only have noselectstar + const targetRules = `rules=[ "noselectstar", ],` - const replacedTheOtherRules = replaced.replace( - `rules=[ + const replacedTheOtherRules = replaced.replace( + `rules=[ "ambiguousorinvalidcolumn", "invalidselectstarexpansion", "noselectstar", @@ -36,55 +34,54 @@ test.fixme( "nomissingowner", "nomissingexternalmodels", ],`, - targetRules, - ) - expect(replacedTheOtherRules).toContain(targetRules) + targetRules, + ) + expect(replacedTheOtherRules).toContain(targetRules) - await fs.writeFile(configPath, replacedTheOtherRules) - // Replace the file to cause the error - const modelPath = path.join(tempDir, 'models', 'latest_order.sql') - const readModel = await fs.readFile(modelPath, 'utf8') - // Replace the specific select with the select star - const modelReplaced = readModel.replace( - 'SELECT id, customer_id, start_ts, end_ts, event_date', - 'SELECT *', - ) - await fs.writeFile(modelPath, modelReplaced) + await fs.writeFile(configPath, replacedTheOtherRules) + // Replace the file to cause the error + const modelPath = path.join(tempDir, 'models', 'latest_order.sql') + const readModel = await fs.readFile(modelPath, 'utf8') + // Replace the specific select with the select star + const modelReplaced = readModel.replace( + 'SELECT id, customer_id, start_ts, end_ts, event_date', + 'SELECT *', + ) + await fs.writeFile(modelPath, modelReplaced) - // Open the code server with the specified directory - await page.goto( - `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, - ) - await page.waitForLoadState('networkidle') + // Open the code server with the specified directory + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') - // Open the file with the linter issue - await page - .getByRole('treeitem', { name: 'models', exact: true }) - .locator('a') - .click() - await page - .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) - .locator('a') - .click() + // Open the file with the linter issue + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'latest_order.sql', exact: true }) + .locator('a') + .click() - await waitForLoadedSQLMesh(page) + await waitForLoadedSQLMesh(page) - await openProblemsView(page) + await openProblemsView(page) - await page.getByRole('button', { name: 'Show fixes' }).click() - await page - .getByRole('menuitem', { name: 'Replace SELECT * with' }) - .first() - .click() + await page.getByRole('button', { name: 'Show fixes' }).click() + await page + .getByRole('menuitem', { name: 'Replace SELECT * with' }) + .first() + .click() - // Wait for the quick fix to be applied - await page.waitForTimeout(2_000) + // Wait for the quick fix to be applied + await page.waitForTimeout(2_000) - // Assert that the model no longer contains SELECT * but SELECT id, customer_id, waiter_id, start_ts, end_ts, event_date - const readUpdatedFile = (await fs.readFile(modelPath)).toString('utf8') - expect(readUpdatedFile).not.toContain('SELECT *') - expect(readUpdatedFile).toContain( - 'SELECT id, customer_id, waiter_id, start_ts, end_ts, event_date', - ) - }, -) + // Assert that the model no longer contains SELECT * but SELECT id, customer_id, waiter_id, start_ts, end_ts, event_date + const readUpdatedFile = (await fs.readFile(modelPath)).toString('utf8') + expect(readUpdatedFile).not.toContain('SELECT *') + expect(readUpdatedFile).toContain( + 'SELECT id, customer_id, waiter_id, start_ts, end_ts, event_date', + ) +}) From 02d504e03d7cd54c377004ae115ae91d4217c981 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Mon, 11 Aug 2025 14:57:39 +0300 Subject: [PATCH 0674/1056] Chore: Info instead of warn for conditional properties assignment (#5122) --- sqlmesh/core/model/definition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 954b03eff8..559d67e960 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -681,10 +681,10 @@ def _render(expression: exp.Expression) -> exp.Expression | None: # in turn makes @this_model available in the evaluation context rendered_exprs = self._statement_renderer(expression).render(**render_kwargs) - # Warn instead of raising for cases where a property is conditionally assigned + # Inform instead of raising for cases where a property is conditionally assigned if not rendered_exprs or rendered_exprs[0].sql().lower() in {"none", "null"}: - logger.warning( - f"Expected rendering '{expression.sql(dialect=self.dialect)}' to return an expression" + logger.info( + f"Rendering '{expression.sql(dialect=self.dialect)}' did not return an expression" ) return None From 677809243de0b68031f7f34db8c022ab49e1d3c7 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 11 Aug 2025 14:16:44 +0100 Subject: [PATCH 0675/1056] chore(vscode): reenable multi workspace tests (#5124) --- vscode/extension/tests/lineage.spec.ts | 32 ++++++++------------------ vscode/extension/tests/utils.ts | 6 +++-- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index bc17b27a4d..8817e7b325 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -9,11 +9,7 @@ import { waitForLoadedSQLMesh, } from './utils' import { writeFileSync } from 'fs' -import { - createPythonInterpreterSettingsSpecifier, - startCodeServer, - stopCodeServer, -} from './utils_code_server' +import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' /** * Helper function to launch VS Code and test lineage with given project path config @@ -134,7 +130,7 @@ test('Lineage panel renders correctly - absolute path project outside of workspa }) // These work on local machine when debuggin but not on CI, so skipping for now -test.skip('Lineage panel renders correctly - multiworkspace setup', async ({ +test('Lineage panel renders correctly - multiworkspace setup', async ({ page, sharedCodeServer, }) => { @@ -177,13 +173,12 @@ test.skip('Lineage panel renders correctly - multiworkspace setup', async ({ { spaces: 2 }, ) - await openServerPage(page, workspaceDir, sharedCodeServer) - await page.waitForSelector('text=Open workspace') - await page.click('text=Open workspace') + await openServerPage(page, workspaceFilePath, sharedCodeServer) + await page.reload() await testLineageWithProjectPath(page) }) -test.skip('Lineage panel renders correctly - multiworkspace setup reversed', async ({ +test('Lineage panel renders correctly - multiworkspace setup reversed', async ({ page, sharedCodeServer, }) => { @@ -216,12 +211,8 @@ test.skip('Lineage panel renders correctly - multiworkspace setup reversed', asy }), ) - const context = await startCodeServer({ - tempDir: workspaceDir, - }) - const settings = { - 'python.defaultInterpreterPath': context.defaultPythonInterpreter, + 'python.defaultInterpreterPath': sharedCodeServer.defaultPythonInterpreter, } await fs.ensureDir(path.join(projectDir1, '.vscode')) await fs.writeJson( @@ -236,12 +227,7 @@ test.skip('Lineage panel renders correctly - multiworkspace setup reversed', asy { spaces: 2 }, ) - try { - await openServerPage(page, workspaceDir, sharedCodeServer) - await page.waitForSelector('text=Open workspace') - await page.click('text=Open workspace') - await testLineageWithProjectPath(page) - } finally { - await stopCodeServer(context) - } + await openServerPage(page, workspaceFilePath, sharedCodeServer) + await page.reload() + await testLineageWithProjectPath(page) }) diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index 8aecae2f46..b91dec33e5 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -215,11 +215,13 @@ export const waitForLoadedSQLMesh = (page: Page) => */ export const openServerPage = async ( page: Page, - tempDir: string, + targetPath: string, context: CodeServerContext, ) => { + const isWorkspace = targetPath.endsWith('.code-workspace') + const param = isWorkspace ? 'workspace' : 'folder' await page.goto( - `http://127.0.0.1:${context.codeServerPort}/?folder=${tempDir}`, + `http://127.0.0.1:${context.codeServerPort}/?${param}=${targetPath}`, ) await page.waitForLoadState('networkidle') await page.waitForSelector('[role="application"]', { timeout: 10000 }) From aeee1b8eb042ad50f400c9cb7f3f2fdf53b93276 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:02:06 +0100 Subject: [PATCH 0676/1056] chore(vscode): removing legacy format methods (#5125) --- vscode/extension/src/commands/format.ts | 38 +------ .../src/utilities/sqlmesh/sqlmesh.ts | 103 ------------------ 2 files changed, 6 insertions(+), 135 deletions(-) diff --git a/vscode/extension/src/commands/format.ts b/vscode/extension/src/commands/format.ts index bedeaa16be..f01435523a 100644 --- a/vscode/extension/src/commands/format.ts +++ b/vscode/extension/src/commands/format.ts @@ -1,10 +1,8 @@ import { traceLog } from '../utilities/common/log' -import { sqlmeshExec } from '../utilities/sqlmesh/sqlmesh' import { err, isErr, ok, Result } from '@bus/result' import * as vscode from 'vscode' import { ErrorType, handleError } from '../utilities/errors' import { AuthenticationProviderTobikoCloud } from '../auth/auth' -import { execAsync } from '../utilities/exec' import { LSPClient } from '../lsp/lsp' export const format = @@ -30,39 +28,15 @@ export const format = const internalFormat = async ( lsp: LSPClient | undefined, ): Promise> => { - try { - // Try LSP method first - if (lsp) { - const response = await lsp.call_custom_method( - 'sqlmesh/format_project', - {}, - ) - if (isErr(response)) { - return response - } - return ok(undefined) - } - } catch (error) { - traceLog(`LSP format failed, falling back to CLI: ${JSON.stringify(error)}`) - } - - // Fallback to CLI method if LSP is not available - // TODO This is a solution in order to be backwards compatible in the cases - // where the LSP method is not implemented yet. This should be removed at - // some point in the future. - const exec = await sqlmeshExec() - if (isErr(exec)) { - return exec - } - const result = await execAsync(`${exec.value.bin}`, ['format'], { - cwd: exec.value.workspacePath, - env: exec.value.env, - }) - if (result.exitCode !== 0) { + if (lsp === undefined) { return err({ type: 'generic', - message: `Error executing sqlmesh format: ${result.stderr}`, + message: 'LSP is not available', }) } + const response = await lsp.call_custom_method('sqlmesh/format_project', {}) + if (isErr(response)) { + return response + } return ok(undefined) } diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 104869192b..649c57fd77 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -262,109 +262,6 @@ export const ensureSqlmeshEnterpriseInstalled = async (): Promise< return installationLock } -/** - * Get the sqlmesh executable for the current workspace. - * - * @deprecated Use LSP instead of direct sqlmesh execution for any new functionality. - */ -export const sqlmeshExec = async (): Promise< - Result -> => { - const sqlmesh = IS_WINDOWS ? 'sqlmesh.exe' : 'sqlmesh' - const projectRoot = await getProjectRoot() - const resolvedPath = resolveProjectPath(projectRoot) - if (isErr(resolvedPath)) { - return err({ - type: 'generic', - message: resolvedPath.error, - }) - } - const envVariables = await getPythonEnvVariables() - if (isErr(envVariables)) { - return err({ - type: 'generic', - message: envVariables.error, - }) - } - const workspacePath = resolvedPath.value - const interpreterDetails = await getInterpreterDetails() - traceLog(`Interpreter details: ${JSON.stringify(interpreterDetails)}`) - if (interpreterDetails.path) { - traceVerbose( - `Using interpreter from Python extension: ${interpreterDetails.path.join( - ' ', - )}`, - ) - } - if (interpreterDetails.isVirtualEnvironment) { - traceLog('Using virtual environment') - const isTcloudInstalled = await isTcloudProject() - if (isErr(isTcloudInstalled)) { - return err({ - type: 'generic', - message: isTcloudInstalled.error, - }) - } - if (isTcloudInstalled.value) { - const tcloudBin = await getTcloudBin() - if (isErr(tcloudBin)) { - return tcloudBin - } - const isSignedIn = await isSignedIntoTobikoCloud() - if (!isSignedIn) { - return err({ - type: 'not_signed_in', - }) - } - const ensured = await ensureSqlmeshEnterpriseInstalled() - if (isErr(ensured)) { - return ensured - } - return ok({ - bin: tcloudBin.value.bin, - workspacePath, - env: tcloudBin.value.env, - args: ["sqlmesh"], - }) - } - const binPath = path.join(interpreterDetails.binPath!, sqlmesh) - traceLog(`Bin path: ${binPath}`) - const env = await getSqlmeshEnvironment() - if (isErr(env)) { - return err({ - type: 'generic', - message: env.error, - }) - } - return ok({ - bin: binPath, - workspacePath, - env: env.value, - args: [], - }) - } else { - const exists = await doesExecutableExist(sqlmesh) - if (!exists) { - return err({ - type: 'sqlmesh_not_found', - }) - } - const env = await getSqlmeshEnvironment() - if (isErr(env)) { - return err({ - type: 'generic', - message: env.error, - }) - } - return ok({ - bin: sqlmesh, - workspacePath, - env: env.value, - args: [], - }) - } -} - /** * Ensure that the sqlmesh_lsp dependencies are installed. * From a6dc203ca6d8019746a25ada088d96fdf776da1e Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:07:07 +0100 Subject: [PATCH 0677/1056] chore(vscode): reeenable find audit references test (#5123) --- .../extension/tests/find_references.spec.ts | 36 +++---------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/vscode/extension/tests/find_references.spec.ts b/vscode/extension/tests/find_references.spec.ts index e2d25269a6..ccc5eaf916 100644 --- a/vscode/extension/tests/find_references.spec.ts +++ b/vscode/extension/tests/find_references.spec.ts @@ -259,7 +259,7 @@ test.describe('Model References', () => { ).toBeVisible() }) - test.skip('Find All Model References from Audit', async ({ + test('Find All Model References from Audit', async ({ page, sharedCodeServer, tempDir, @@ -288,36 +288,10 @@ test.describe('Model References', () => { // Step 5: Use Find All References to see all occurrences across the project await findAllReferences(page) - // Step 6: Click on a reference to navigate to customer_revenue_by_day.sql - let clickedReference = false - - const referenceItems = page.locator( - '.monaco-list-row, .reference-item, .monaco-tl-row', - ) - const count = await referenceItems.count() - - for (let i = 0; i < count; i++) { - const item = referenceItems.nth(i) - const text = await item.textContent() - - // Look for a reference that contains customer_revenue_by_day - if (text && text.includes('customer_revenue_by_day.sql')) { - await item.click() - clickedReference = true - break - } - } - - expect(clickedReference).toBe(true) - - // Step 7: Verify successful navigation by checking for SQL JOIN statement - await expect(page.locator('text=LEFT JOIN')).toBeVisible() - - // Step 8: Interact with the file to verify it's fully loaded and check its content - await page.locator('text=LEFT JOIN').first().click() - await expect( - page.locator('text=FROM sushi.order_items AS oi'), - ).toBeVisible() + // Assert that the references panel shows the correct files + await page.waitForSelector('text=References') + await page.waitForSelector('text=customer_revenue_by_day.sql') + await page.waitForSelector('text=items.py') }) }) From 13be24c786c19dc64a591ef07705884b3deed2da Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:42:23 +0100 Subject: [PATCH 0678/1056] chore: adding shared ui setup (#5127) --- pnpm-lock.yaml | 399 ++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 1 + web/shared_ui/eslint.config.mjs | 16 ++ web/shared_ui/package.json | 12 +- web/shared_ui/tsconfig.json | 2 +- 5 files changed, 424 insertions(+), 6 deletions(-) create mode 100644 web/shared_ui/eslint.config.mjs diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e39e21fbd..7ab6213fa4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -397,6 +397,43 @@ importers: specifier: ^1.13.2 version: 1.13.2 + web/shared_ui: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@eslint/js': + specifier: ^9.31.0 + version: 9.31.0 + '@types/react': + specifier: ^18.3.23 + version: 18.3.23 + '@types/react-dom': + specifier: ^18.3.7 + 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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + eslint: + specifier: ^9.31.0 + version: 9.31.0(jiti@2.4.2) + 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) + vite: + specifier: ^6.3.5 + version: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + vite-plugin-dts: + specifier: ^4.5.4 + version: 4.5.4(@types/node@24.1.0)(rollup@4.45.1)(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + packages: '@adobe/css-tools@4.4.3': @@ -1085,6 +1122,19 @@ packages: '@types/react': '>=16' react: '>=16' + '@microsoft/api-extractor-model@7.30.7': + resolution: {integrity: sha512-TBbmSI2/BHpfR9YhQA7nH0nqVmGgJ0xH0Ex4D99/qBDAUpnhA2oikGmdXanbw9AWWY/ExBYIpkmY8dBHdla3YQ==} + + '@microsoft/api-extractor@7.52.10': + resolution: {integrity: sha512-LhKytJM5ZJkbHQVfW/3o747rZUNs/MGg6j/wt/9qwwqEOfvUDTYXXxIBuMgrRXhJ528p41iyz4zjBVHZU74Odg==} + hasBin: true + + '@microsoft/tsdoc-config@0.17.1': + resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==} + + '@microsoft/tsdoc@0.15.1': + resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@neoconfetti/react@1.0.0': resolution: {integrity: sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==} @@ -1640,6 +1690,28 @@ packages: cpu: [x64] os: [win32] + '@rushstack/node-core-library@5.14.0': + resolution: {integrity: sha512-eRong84/rwQUlATGFW3TMTYVyqL1vfW9Lf10PH+mVGfIb9HzU3h5AASNIw+axnBLjnD0n3rT5uQBwu9fvzATrg==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rig-package@0.5.3': + resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==} + + '@rushstack/terminal@0.15.4': + resolution: {integrity: sha512-OQSThV0itlwVNHV6thoXiAYZlQh4Fgvie2CzxFABsbO2MWQsI4zOh3LRNigYSTrmS+ba2j0B3EObakPzf/x6Zg==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@5.0.2': + resolution: {integrity: sha512-+AkJDbu1GFMPIU8Sb7TLVXDv/Q7Mkvx+wAjEl8XiXVVq+p1FmWW6M3LYpJMmoHNckSofeMecgWg5lfMwNAAsEQ==} + '@secretlint/config-creator@10.2.1': resolution: {integrity: sha512-nyuRy8uo2+mXPIRLJ93wizD1HbcdDIsVfgCT01p/zGVFrtvmiL7wqsl4KgZH0QFBM/KRLDLeog3/eaM5ASjtvw==} engines: {node: '>=20.0.0'} @@ -2181,6 +2253,9 @@ packages: '@textlint/types@15.2.0': resolution: {integrity: sha512-wpF+xjGJgJK2JiwUdYjuNZrbuas3KfC9VDnHKac6aBLFyrI1iXuXtuxKXQDFi5/hebACactSJOuVVbuQbdJZ1Q==} + '@types/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -2583,6 +2658,15 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@volar/language-core@2.4.23': + resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} + + '@volar/source-map@2.4.23': + resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} + + '@volar/typescript@2.4.23': + resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} + '@vscode/python-extension@1.0.5': resolution: {integrity: sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==} engines: {node: '>=16.17.1', vscode: ^1.78.0} @@ -2649,6 +2733,26 @@ packages: engines: {node: '>= 20'} hasBin: true + '@vue/compiler-core@3.5.18': + resolution: {integrity: sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==} + + '@vue/compiler-dom@3.5.18': + resolution: {integrity: sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/shared@3.5.18': + resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -2739,6 +2843,14 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-keywords@5.1.0: resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: @@ -2747,9 +2859,18 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + + ajv@8.13.0: + resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + alien-signals@0.4.14: + resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -3129,6 +3250,12 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -3225,6 +3352,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -3540,6 +3670,9 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3869,6 +4002,10 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -4124,6 +4261,9 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4224,6 +4364,9 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -4313,6 +4456,10 @@ packages: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} + local-pkg@1.1.1: + resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} + engines: {node: '>=14'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -4604,6 +4751,9 @@ packages: engines: {node: '>=10'} hasBin: true + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mocha@10.8.2: resolution: {integrity: sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==} engines: {node: '>= 14.0.0'} @@ -4616,6 +4766,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -4821,6 +4974,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4885,6 +5041,12 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.2.0: + resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} + playwright-core@1.54.1: resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==} engines: {node: '>=18'} @@ -4997,6 +5159,9 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5261,6 +5426,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -5811,6 +5981,11 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -5823,6 +5998,9 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -5963,6 +6141,15 @@ packages: peerDependencies: vite: '>2.0.0-0' + vite-plugin-dts@4.5.4: + resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true + vite@6.3.5: resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -7041,6 +7228,41 @@ snapshots: '@types/react': 18.3.23 react: 18.3.1 + '@microsoft/api-extractor-model@7.30.7(@types/node@24.1.0)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@24.1.0) + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.52.10(@types/node@24.1.0)': + dependencies: + '@microsoft/api-extractor-model': 7.30.7(@types/node@24.1.0) + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@24.1.0) + '@rushstack/rig-package': 0.5.3 + '@rushstack/terminal': 0.15.4(@types/node@24.1.0) + '@rushstack/ts-command-line': 5.0.2(@types/node@24.1.0) + lodash: 4.17.21 + minimatch: 10.0.3 + resolve: 1.22.10 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/tsdoc-config@0.17.1': + dependencies: + '@microsoft/tsdoc': 0.15.1 + ajv: 8.12.0 + jju: 1.4.0 + resolve: 1.22.10 + + '@microsoft/tsdoc@0.15.1': {} + '@neoconfetti/react@1.0.0': {} '@nodelib/fs.scandir@2.1.5': @@ -7657,6 +7879,40 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.45.1': optional: true + '@rushstack/node-core-library@5.14.0(@types/node@24.1.0)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 11.3.0 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.10 + semver: 7.5.4 + optionalDependencies: + '@types/node': 24.1.0 + + '@rushstack/rig-package@0.5.3': + dependencies: + resolve: 1.22.10 + strip-json-comments: 3.1.1 + + '@rushstack/terminal@0.15.4(@types/node@24.1.0)': + dependencies: + '@rushstack/node-core-library': 5.14.0(@types/node@24.1.0) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 24.1.0 + + '@rushstack/ts-command-line@5.0.2(@types/node@24.1.0)': + dependencies: + '@rushstack/terminal': 0.15.4(@types/node@24.1.0) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + '@secretlint/config-creator@10.2.1': dependencies: '@secretlint/types': 10.2.1 @@ -8339,6 +8595,8 @@ snapshots: dependencies: '@textlint/ast-node-types': 15.2.0 + '@types/argparse@1.0.38': {} + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -8864,6 +9122,18 @@ snapshots: loupe: 3.1.4 tinyrainbow: 2.0.0 + '@volar/language-core@2.4.23': + dependencies: + '@volar/source-map': 2.4.23 + + '@volar/source-map@2.4.23': {} + + '@volar/typescript@2.4.23': + dependencies: + '@volar/language-core': 2.4.23 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + '@vscode/python-extension@1.0.5': {} '@vscode/test-cli@0.0.10': @@ -8963,6 +9233,39 @@ snapshots: transitivePeerDependencies: - supports-color + '@vue/compiler-core@3.5.18': + dependencies: + '@babel/parser': 7.28.0 + '@vue/shared': 3.5.18 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.18': + dependencies: + '@vue/compiler-core': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.0(typescript@5.8.3)': + dependencies: + '@volar/language-core': 2.4.23 + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.18 + alien-signals: 0.4.14 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.8.3 + + '@vue/shared@3.5.18': {} + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -9055,6 +9358,10 @@ snapshots: agent-base@7.1.4: {} + ajv-draft-04@1.0.0(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -9067,6 +9374,10 @@ snapshots: optionalDependencies: ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + ajv-keywords@5.1.0(ajv@8.17.1): dependencies: ajv: 8.17.1 @@ -9079,6 +9390,20 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.12.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ajv@8.13.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -9086,6 +9411,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + alien-signals@0.4.14: {} + ansi-colors@4.1.3: {} ansi-escapes@7.0.0: @@ -9477,6 +9804,10 @@ snapshots: concat-map@0.0.1: {} + confbox@0.1.8: {} + + confbox@0.2.2: {} + convert-source-map@2.0.0: {} cookie-es@1.2.2: {} @@ -9573,6 +9904,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + de-indent@1.0.2: {} + debug@4.4.1(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -9972,6 +10305,8 @@ snapshots: expect-type@1.2.2: {} + exsolve@1.0.7: {} + extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -10325,6 +10660,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-lazy@4.0.0: {} + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -10560,6 +10897,8 @@ snapshots: jiti@2.4.2: {} + jju@1.4.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -10681,6 +11020,8 @@ snapshots: kleur@3.0.3: {} + kolorist@1.8.0: {} + leven@3.1.0: {} levn@0.4.1: @@ -10747,6 +11088,12 @@ snapshots: loader-runner@4.3.0: {} + local-pkg@1.1.1: + dependencies: + mlly: 1.7.4 + pkg-types: 2.2.0 + quansync: 0.2.10 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -11135,6 +11482,13 @@ snapshots: mkdirp@3.0.1: {} + mlly@1.7.4: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + mocha@10.8.2: dependencies: ansi-colors: 4.1.3 @@ -11162,6 +11516,8 @@ snapshots: ms@2.1.3: {} + muggle-string@0.4.1: {} + mute-stream@0.0.8: {} mz@2.7.0: @@ -11438,6 +11794,8 @@ snapshots: dependencies: entities: 6.0.1 + path-browserify@1.0.1: {} + path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -11478,6 +11836,18 @@ snapshots: pirates@4.0.7: {} + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + pkg-types@2.2.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + playwright-core@1.54.1: {} playwright@1.54.1: @@ -11586,6 +11956,8 @@ snapshots: dependencies: side-channel: 1.1.0 + quansync@0.2.10: {} + queue-microtask@1.2.3: {} randombytes@2.1.0: @@ -11943,6 +12315,10 @@ snapshots: semver@6.3.1: {} + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + semver@7.7.2: {} serialize-javascript@6.0.2: @@ -12584,12 +12960,16 @@ snapshots: transitivePeerDependencies: - supports-color + typescript@5.8.2: {} + typescript@5.8.3: {} typical@7.3.0: {} uc.micro@2.1.0: {} + ufo@1.6.1: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -12768,6 +13148,25 @@ snapshots: dependencies: vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + vite-plugin-dts@4.5.4(@types/node@24.1.0)(rollup@4.45.1)(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)): + dependencies: + '@microsoft/api-extractor': 7.52.10(@types/node@24.1.0) + '@rollup/pluginutils': 5.2.0(rollup@4.45.1) + '@volar/typescript': 2.4.23 + '@vue/language-core': 2.2.0(typescript@5.8.3) + compare-versions: 6.1.1 + debug: 4.4.1(supports-color@8.1.1) + 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@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.8 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1cfae718c5..377ec655e9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,3 +3,4 @@ packages: - vscode/extension - vscode/react - web/client + - web/shared_ui diff --git a/web/shared_ui/eslint.config.mjs b/web/shared_ui/eslint.config.mjs new file mode 100644 index 0000000000..cd34adf5c8 --- /dev/null +++ b/web/shared_ui/eslint.config.mjs @@ -0,0 +1,16 @@ +import eslint from '@eslint/js' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.recommended, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + 'no-empty': 'off', + }, + }, +) diff --git a/web/shared_ui/package.json b/web/shared_ui/package.json index dcc0449d0e..5e1b664fe9 100644 --- a/web/shared_ui/package.json +++ b/web/shared_ui/package.json @@ -1,5 +1,5 @@ { - "name": "sqlmesh-shared-ui", + "name": "@tobikodata/sqlmesh-common", "version": "0.0.1", "private": false, "type": "module", @@ -16,23 +16,25 @@ } }, "scripts": { + "ci": "pnpm run lint && pnpm run build", "build": "tsc -b && vite build --base './'", "build:storybook": "storybook build", "dev": "storybook dev -p 6006", - "lint": "eslint --max-warnings 0 --fix .", - "pretty": "prettier --write .", - "format": "pnpm run pretty && pnpm run lint", + "lint": "eslint --max-warnings 0 --fix src", "test": "vitest", "test:watch": "vitest watch", "test:ui": "vitest --ui" }, "devDependencies": { + "@eslint/js": "^9.31.0", + "eslint": "^9.31.0", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.7.0", "typescript": "^5.8.3", "vite": "^6.3.5", - "vite-plugin-dts": "^4.5.4" + "vite-plugin-dts": "^4.5.4", + "typescript-eslint": "^8.38.0" }, "peerDependencies": { "react": "^18.3.1", diff --git a/web/shared_ui/tsconfig.json b/web/shared_ui/tsconfig.json index dfbf2261bd..d3baabf221 100644 --- a/web/shared_ui/tsconfig.json +++ b/web/shared_ui/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["**/*.ts", "**/*.tsx", "vitest.config.js"], + "include": ["src/**/*.ts", "src/**/*.tsx"], "compilerOptions": { "target": "ES2022", "jsx": "react-jsx", From 32a187f6b5176312b5b358b69835128c7f267fa5 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:35:19 +0100 Subject: [PATCH 0679/1056] chore(web,vscode): adding npm publish shared package (#5128) --- .github/workflows/release_shared_js.yaml | 54 ++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/release_shared_js.yaml diff --git a/.github/workflows/release_shared_js.yaml b/.github/workflows/release_shared_js.yaml new file mode 100644 index 0000000000..82c48e1c4a --- /dev/null +++ b/.github/workflows/release_shared_js.yaml @@ -0,0 +1,54 @@ +name: Release shared js code +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., 1.0.0)' + required: true + type: string +permissions: + id-token: write + contents: read +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Check branch is main + run: | + if [[ "${{ github.ref }}" != "refs/heads/main" ]]; then + echo "Error: This workflow can only be run from the main branch" + exit 1 + fi + echo "Branch check passed: running from main branch" + - name: Validate version format + run: | + version="${{ github.event.inputs.version }}" + if ! [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "Error: Version must be a valid semantic version (e.g., 1.0.0, 1.0.0-beta.1, 1.0.0+build.1)" + exit 1 + fi + echo "Version format is valid: $version" + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Update package.json version + working-directory: web/shared_ui + run: | + npm version ${{ github.event.inputs.version }} --no-git-tag-version + - name: Build package + working-directory: web/shared_ui + run: pnpm run build + - name: Publish to npm + working-directory: web/shared_ui + run: | + npm publish From 689728250c46ee270ba721b0205d210dbd3d7cf3 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:48:46 +0100 Subject: [PATCH 0680/1056] chore: fix publish job for shared code (#5129) --- .github/workflows/release_shared_js.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release_shared_js.yaml b/.github/workflows/release_shared_js.yaml index 82c48e1c4a..ad5a57c34d 100644 --- a/.github/workflows/release_shared_js.yaml +++ b/.github/workflows/release_shared_js.yaml @@ -35,6 +35,10 @@ jobs: with: node-version: '20' registry-url: 'https://registry.npmjs.org' + - name: Update npm + run: npm install -g npm@latest + - name: Print npm version + run: npm --version - name: Install pnpm uses: pnpm/action-setup@v4 with: From ab421d9118be7d8f7cdd2fc6da0adb9a32803e83 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:54:36 +0100 Subject: [PATCH 0681/1056] chore: add repository info to common code (#5130) --- web/shared_ui/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/shared_ui/package.json b/web/shared_ui/package.json index 5e1b664fe9..240e81fbab 100644 --- a/web/shared_ui/package.json +++ b/web/shared_ui/package.json @@ -3,6 +3,10 @@ "version": "0.0.1", "private": false, "type": "module", + "repository": { + "type": "git", + "url": "https://github.com/TobikoData/sqlmesh" + }, "files": [ "/dist" ], From cd0557b1656c3733fa340e1651064be8c96e48ee Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:11:26 -0700 Subject: [PATCH 0682/1056] chore: add qa reviewer agent (#5131) --- .claude/agents/qa-reviewer.md | 106 ++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .claude/agents/qa-reviewer.md diff --git a/.claude/agents/qa-reviewer.md b/.claude/agents/qa-reviewer.md new file mode 100644 index 0000000000..b1f6842f32 --- /dev/null +++ b/.claude/agents/qa-reviewer.md @@ -0,0 +1,106 @@ +--- +name: qa-reviewer +description: Use this agent PROACTIVELY when you need to analyze a PR or code changes to provide structured QA testing guidance for human QA testers. This agent reviews PRs and provides specific testing scenarios, example projects to use, commands to run, and validation steps. Examples: Context: A developer just implemented virtual environment isolation for SQLMesh. user: 'I just added support for isolated virtual environments in SQLMesh' assistant: 'Let me use the qa-reviewer agent to create comprehensive QA testing instructions for this feature' Since a significant feature was implemented, use the qa-reviewer agent to provide structured testing guidance for QA. Context: A PR adds a new SQL engine adapter. user: 'Here's the PR that adds BigQuery support to SQLMesh' assistant: 'I'll use the qa-reviewer agent to analyze this change and create QA test scenarios' Since a new engine adapter was added, use the qa-reviewer agent to provide testing guidance specific to engine adapters. +tools: Glob, Grep, LS, Read, NotebookRead, WebFetch, TodoWrite, WebSearch, Bash +model: sonnet +color: green +--- + +You are a QA Test Specialist with deep expertise in SQLMesh's architecture, testing methodologies, and quality assurance practices. You specialize in analyzing code changes and providing comprehensive, structured testing guidance for human QA testers. + +Your core responsibilities: + +## Analysis Approach + +- Review PRs and code changes to understand the scope and impact of modifications +- Identify all components, features, and workflows that could be affected by the changes +- Consider edge cases, integration points, and potential failure scenarios +- Map changes to existing example projects and testing workflows +- Provide specific, actionable testing instructions that non-developers can follow +- MUST write full instructions to the `plans/` folder with the filename of `_.md` so they can be reviewed and executed by QA testers + +## QA Test Plan Structure + +Organize your QA recommendations into clear, actionable sections: + +### **Change Summary** +- Brief description of what was changed and why +- Key components and files modified +- Potential impact areas and affected workflows + +### **Test Environment Setup** +- Which example project(s) to use for testing (e.g., `examples/sushi/`, `examples/sushi_dbt/`) +- Any necessary environment configuration or setup steps +- Required tools, databases, or dependencies + +### **Core Test Scenarios** +- Step-by-step testing procedures with specific commands +- Expected results and success criteria for each test +- Validation commands to confirm expected behavior +- Screenshots or output examples where helpful + +### **Edge Case Testing** +- Boundary conditions and error scenarios to test +- Negative test cases and expected failure modes +- Cross-platform considerations (Windows/Linux/macOS) +- Performance and scalability considerations + +### **Regression Testing** +- Existing functionality that should be retested +- Critical workflows that must continue working +- Backward compatibility scenarios + +### **Integration Testing** +- Cross-component testing scenarios +- Multi-engine testing when relevant +- dbt integration testing if applicable +- UI/CLI integration points + +## Example Project Guidance + +Provide specific guidance on: +- Which `examples/` project best demonstrates the feature +- How to modify example projects for comprehensive testing +- Custom test scenarios using real-world-like data +- Commands to set up test scenarios and validate results + +## Command Examples + +Always provide: +- Exact CLI commands to run tests +- Configuration file modifications needed +- Environment variable settings +- Database setup commands when applicable +- Validation queries or commands to check results + +## Testing Best Practices + +- Focus on user-facing functionality and workflows +- Include both happy path and error scenarios +- Provide clear success/failure criteria +- Consider different user personas (data analysts, engineers, platform teams) +- If the change doesn't have engine specific logic in it, prefer to test against duckdb since that is easiest +- Include performance and scalability considerations +- DO NOT have a step which is running an existing test - these tests are automatically run in CI and should not be duplicated in manual testing instructions +- Assume all example projects are already tested as is and don't suggest doing a test which is running them again +- All tests MUST just use `sqlmesh` cli commands - do not use the Python API. The goal is to run tests that mimic what an actual user would do, which is using the CLI. +- A common pattern could be using the `sqlmesh` cli command and then running a Python script to validate the database or state is in an expected state, but the Python script should not be a test itself, just a validation step. + +## Communication Style + +- Use clear, numbered steps for testing procedures +- Provide exact commands that can be copy-pasted +- Include expected outputs and how to interpret results +- Explain the "why" behind each test scenario +- Use language accessible to QA testers who may not be developers +- Organize content with clear headings and bullet points + +## Important Constraints + +- You NEVER write or modify code - you only analyze and provide testing guidance +- You focus on user-facing functionality and workflows +- You always provide specific, actionable testing steps +- You consider the full user journey and realistic usage scenarios +- You validate that your recommendations align with SQLMesh's architecture and patterns + +When analyzing changes, assume you're looking at a recent PR or set of code modifications. Focus on providing comprehensive testing guidance that ensures the changes work correctly, don't break existing functionality, and provide a good user experience across different scenarios and environments. \ No newline at end of file From 1237e9acdaa673a7ef4056769b271226bd5adb4f Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 12 Aug 2025 11:07:14 +1200 Subject: [PATCH 0683/1056] Feat: dbt cli skeleton (#5118) --- .pre-commit-config.yaml | 2 +- pyproject.toml | 3 +- sqlmesh_dbt/__init__.py | 5 + sqlmesh_dbt/cli.py | 80 +++++++++++ sqlmesh_dbt/console.py | 8 ++ sqlmesh_dbt/operations.py | 133 ++++++++++++++++++ tests/dbt/cli/conftest.py | 29 ++++ .../jaffle_shop_duckdb/dbt_project.yml | 34 +++++ .../jaffle_shop_duckdb/models/customers.sql | 69 +++++++++ .../jaffle_shop_duckdb/models/docs.md | 14 ++ .../jaffle_shop_duckdb/models/orders.sql | 56 ++++++++ .../jaffle_shop_duckdb/models/overview.md | 11 ++ .../jaffle_shop_duckdb/models/schema.yml | 82 +++++++++++ .../models/staging/schema.yml | 31 ++++ .../models/staging/stg_customers.sql | 22 +++ .../models/staging/stg_orders.sql | 23 +++ .../models/staging/stg_payments.sql | 25 ++++ .../fixtures/jaffle_shop_duckdb/profiles.yml | 8 ++ .../jaffle_shop_duckdb/seeds/.gitkeep | 0 .../seeds/raw_customers.csv | 101 +++++++++++++ .../jaffle_shop_duckdb/seeds/raw_orders.csv | 100 +++++++++++++ .../jaffle_shop_duckdb/seeds/raw_payments.csv | 114 +++++++++++++++ tests/dbt/cli/test_list.py | 17 +++ tests/dbt/cli/test_operations.py | 57 ++++++++ tests/dbt/cli/test_run.py | 15 ++ 25 files changed, 1037 insertions(+), 2 deletions(-) create mode 100644 sqlmesh_dbt/__init__.py create mode 100644 sqlmesh_dbt/cli.py create mode 100644 sqlmesh_dbt/console.py create mode 100644 sqlmesh_dbt/operations.py create mode 100644 tests/dbt/cli/conftest.py create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/dbt_project.yml create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/customers.sql create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/docs.md create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/orders.sql create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/overview.md create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/schema.yml create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/schema.yml create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_customers.sql create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_orders.sql create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_payments.sql create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/profiles.yml create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/.gitkeep create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_customers.csv create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_orders.csv create mode 100644 tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_payments.csv create mode 100644 tests/dbt/cli/test_list.py create mode 100644 tests/dbt/cli/test_operations.py create mode 100644 tests/dbt/cli/test_run.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4b61cb0f3..bb63cf1be1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: language: python types_or: [python, pyi] require_serial: true - files: &files ^(sqlmesh/|tests/|web/|examples/|setup.py) + files: &files ^(sqlmesh/|sqlmesh_dbt/|tests/|web/|examples/|setup.py) - id: ruff-format name: ruff-format entry: ruff format --force-exclude --line-length 100 diff --git a/pyproject.toml b/pyproject.toml index 8e1c4b879f..517f3be426 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -142,6 +142,7 @@ risingwave = ["psycopg2"] [project.scripts] sqlmesh = "sqlmesh.cli.main:cli" +sqlmesh_dbt = "sqlmesh_dbt.cli:dbt" sqlmesh_cicd = "sqlmesh.cicd.bot:bot" sqlmesh_lsp = "sqlmesh.lsp.main:main" @@ -164,7 +165,7 @@ fallback_version = "0.0.0" local_scheme = "no-local-version" [tool.setuptools.packages.find] -include = ["sqlmesh", "sqlmesh.*", "web*"] +include = ["sqlmesh", "sqlmesh.*", "sqlmesh_dbt", "sqlmesh_dbt.*", "web*"] [tool.setuptools.package-data] web = ["client/dist/**"] diff --git a/sqlmesh_dbt/__init__.py b/sqlmesh_dbt/__init__.py new file mode 100644 index 0000000000..984f083f5b --- /dev/null +++ b/sqlmesh_dbt/__init__.py @@ -0,0 +1,5 @@ +# Note: `sqlmesh_dbt` is deliberately in its own package from `sqlmesh` to avoid the upfront time overhead +# that comes from `import sqlmesh` +# +# Obviously we still have to `import sqlmesh` at some point but this allows us to defer it until needed, +# which means we can make the CLI feel more responsive by being able to output something immediately diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py new file mode 100644 index 0000000000..2ec59b665d --- /dev/null +++ b/sqlmesh_dbt/cli.py @@ -0,0 +1,80 @@ +import typing as t +import sys +import click +from sqlmesh_dbt.operations import DbtOperations, create + + +def _get_dbt_operations(ctx: click.Context) -> DbtOperations: + if not isinstance(ctx.obj, DbtOperations): + raise ValueError(f"Unexpected click context object: {type(ctx.obj)}") + return ctx.obj + + +@click.group() +@click.pass_context +def dbt(ctx: click.Context) -> None: + """ + An ELT tool for managing your SQL transformations and data models, powered by the SQLMesh engine. + """ + + if "--help" in sys.argv: + # we dont need to import sqlmesh/load the project for CLI help + return + + # TODO: conditionally call create() if there are times we dont want/need to import sqlmesh and load a project + ctx.obj = create() + + +@dbt.command() +@click.option("-s", "-m", "--select", "--models", "--model", help="Specify the nodes to include.") +@click.option( + "-f", + "--full-refresh", + help="If specified, dbt will drop incremental models and fully-recalculate the incremental table from the model definition.", +) +@click.pass_context +def run(ctx: click.Context, select: t.Optional[str], full_refresh: bool) -> None: + """Compile SQL and execute against the current target database.""" + _get_dbt_operations(ctx).run(select=select, full_refresh=full_refresh) + + +@dbt.command(name="list") +@click.pass_context +def list_(ctx: click.Context) -> None: + """List the resources in your project""" + _get_dbt_operations(ctx).list_() + + +@dbt.command(name="ls", hidden=True) # hidden alias for list +@click.pass_context +def ls(ctx: click.Context) -> None: + """List the resources in your project""" + ctx.forward(list_) + + +def _not_implemented(name: str) -> None: + @dbt.command(name=name) + def _not_implemented() -> None: + """Not implemented""" + click.echo(f"dbt {name} not implemented") + + +for subcommand in ( + "build", + "clean", + "clone", + "compile", + "debug", + "deps", + "docs", + "init", + "parse", + "retry", + "run-operation", + "seed", + "show", + "snapshot", + "source", + "test", +): + _not_implemented(subcommand) diff --git a/sqlmesh_dbt/console.py b/sqlmesh_dbt/console.py new file mode 100644 index 0000000000..7d804ceb71 --- /dev/null +++ b/sqlmesh_dbt/console.py @@ -0,0 +1,8 @@ +from sqlmesh.core.console import TerminalConsole + + +class DbtCliConsole(TerminalConsole): + # TODO: build this out + + def print(self, msg: str) -> None: + return self._print(msg) diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py new file mode 100644 index 0000000000..b826a00e37 --- /dev/null +++ b/sqlmesh_dbt/operations.py @@ -0,0 +1,133 @@ +from __future__ import annotations +import typing as t +from rich.progress import Progress +from pathlib import Path + +if t.TYPE_CHECKING: + # important to gate these to be able to defer importing sqlmesh until we need to + from sqlmesh.core.context import Context + from sqlmesh.dbt.project import Project + from sqlmesh_dbt.console import DbtCliConsole + + +class DbtOperations: + def __init__(self, sqlmesh_context: Context, dbt_project: Project): + self.context = sqlmesh_context + self.project = dbt_project + + def list_(self) -> None: + for _, model in self.context.models.items(): + self.console.print(model.name) + + def run(self, select: t.Optional[str] = None, full_refresh: bool = False) -> None: + # A dbt run both updates data and changes schemas and has no way of rolling back so more closely maps to a SQLMesh forward-only plan + # TODO: if --full-refresh specified, mark incrementals as breaking instead of forward_only? + + # TODO: we need to either convert DBT selector syntax to SQLMesh selector syntax + # or make the model selection engine configurable + select_models = None + if select: + if "," in select: + select_models = select.split(",") + else: + select_models = select.split(" ") + + self.context.plan( + select_models=select_models, + no_auto_categorization=True, # everything is breaking / foward-only + run=True, + no_diff=True, + no_prompts=True, + auto_apply=True, + ) + + @property + def console(self) -> DbtCliConsole: + console = self.context.console + from sqlmesh_dbt.console import DbtCliConsole + + if not isinstance(console, DbtCliConsole): + raise ValueError(f"Expecting dbt cli console, got: {console}") + + return console + + +def create( + project_dir: t.Optional[Path] = None, profiles_dir: t.Optional[Path] = None, debug: bool = False +) -> DbtOperations: + with Progress(transient=True) as progress: + # Indeterminate progress bar before SQLMesh import to provide feedback to the user that something is indeed happening + load_task_id = progress.add_task("Loading engine", total=None) + + from sqlmesh import configure_logging + from sqlmesh.core.context import Context + from sqlmesh.dbt.loader import sqlmesh_config, DbtLoader + from sqlmesh.core.console import set_console + from sqlmesh_dbt.console import DbtCliConsole + from sqlmesh.utils.errors import SQLMeshError + + configure_logging(force_debug=debug) + set_console(DbtCliConsole()) + + progress.update(load_task_id, description="Loading project", total=None) + + # inject default start date if one is not specified to prevent the user from having to do anything + _inject_default_start_date(project_dir) + + config = sqlmesh_config( + project_root=project_dir, + # do we want to use a local duckdb for state? + # warehouse state has a bunch of overhead to initialize, is slow for ongoing operations and will create tables that perhaps the user was not expecting + # on the other hand, local state is not portable + state_connection=None, + ) + + sqlmesh_context = Context( + config=config, + load=True, + ) + + # this helps things which want a default project-level start date, like the "effective from date" for forward-only plans + if not sqlmesh_context.config.model_defaults.start: + min_start_date = min( + ( + model.start + for model in sqlmesh_context.models.values() + if model.start is not None + ), + default=None, + ) + sqlmesh_context.config.model_defaults.start = min_start_date + + dbt_loader = sqlmesh_context._loaders[0] + if not isinstance(dbt_loader, DbtLoader): + raise SQLMeshError(f"Unexpected loader type: {type(dbt_loader)}") + + # so that DbtOperations can query information from the DBT project files in order to invoke SQLMesh correctly + dbt_project = dbt_loader._projects[0] + + return DbtOperations(sqlmesh_context, dbt_project) + + +def _inject_default_start_date(project_dir: t.Optional[Path] = None) -> None: + """ + SQLMesh needs a start date to as the starting point for calculating intervals on incremental models + + Rather than forcing the user to update their config manually or having a default that is not saved between runs, + we can inject it automatically to the dbt_project.yml file + """ + from sqlmesh.dbt.project import PROJECT_FILENAME, load_yaml + from sqlmesh.utils.yaml import dump + from sqlmesh.utils.date import yesterday_ds + + project_yaml_path = (project_dir or Path.cwd()) / PROJECT_FILENAME + if project_yaml_path.exists(): + loaded_project_file = load_yaml(project_yaml_path) + start_date_keys = ("start", "+start") + if "models" in loaded_project_file and all( + k not in loaded_project_file["models"] for k in start_date_keys + ): + loaded_project_file["models"]["+start"] = yesterday_ds() + # todo: this may format the file differently, is that acceptable? + with project_yaml_path.open("w") as f: + dump(loaded_project_file, f) diff --git a/tests/dbt/cli/conftest.py b/tests/dbt/cli/conftest.py new file mode 100644 index 0000000000..dfad2f0046 --- /dev/null +++ b/tests/dbt/cli/conftest.py @@ -0,0 +1,29 @@ +import typing as t +from pathlib import Path +import os +import functools +from click.testing import CliRunner, Result +import pytest + + +@pytest.fixture +def jaffle_shop_duckdb(copy_to_temp_path: t.Callable[..., t.List[Path]]) -> t.Iterable[Path]: + fixture_path = Path(__file__).parent / "fixtures" / "jaffle_shop_duckdb" + assert fixture_path.exists() + + current_path = os.getcwd() + output_path = copy_to_temp_path(paths=fixture_path)[0] + + # so that we can invoke commands from the perspective of a user that is alrady in the correct directory + os.chdir(output_path) + + yield output_path + + os.chdir(current_path) + + +@pytest.fixture +def invoke_cli() -> t.Callable[..., Result]: + from sqlmesh_dbt.cli import dbt + + return functools.partial(CliRunner().invoke, dbt) diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/dbt_project.yml b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/dbt_project.yml new file mode 100644 index 0000000000..1b71726467 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/dbt_project.yml @@ -0,0 +1,34 @@ +name: 'jaffle_shop' + +config-version: 2 +version: '0.1' + +profile: 'jaffle_shop' + +model-paths: ["models"] +seed-paths: ["seeds"] +test-paths: ["tests"] +analysis-paths: ["analysis"] +macro-paths: ["macros"] + +target-path: "target" +clean-targets: + - "target" + - "dbt_modules" + - "logs" + +require-dbt-version: [">=1.0.0", "<2.0.0"] + +seeds: + +docs: + node_color: '#cd7f32' + +models: + jaffle_shop: + +materialized: table + staging: + +materialized: view + +docs: + node_color: 'silver' + +docs: + node_color: 'gold' diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/customers.sql b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/customers.sql new file mode 100644 index 0000000000..016a004fe5 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/customers.sql @@ -0,0 +1,69 @@ +with customers as ( + + select * from {{ ref('stg_customers') }} + +), + +orders as ( + + select * from {{ ref('stg_orders') }} + +), + +payments as ( + + select * from {{ ref('stg_payments') }} + +), + +customer_orders as ( + + select + customer_id, + + min(order_date) as first_order, + max(order_date) as most_recent_order, + count(order_id) as number_of_orders + from orders + + group by customer_id + +), + +customer_payments as ( + + select + orders.customer_id, + sum(amount) as total_amount + + from payments + + left join orders on + payments.order_id = orders.order_id + + group by orders.customer_id + +), + +final as ( + + select + customers.customer_id, + customers.first_name, + customers.last_name, + customer_orders.first_order, + customer_orders.most_recent_order, + customer_orders.number_of_orders, + customer_payments.total_amount as customer_lifetime_value + + from customers + + left join customer_orders + on customers.customer_id = customer_orders.customer_id + + left join customer_payments + on customers.customer_id = customer_payments.customer_id + +) + +select * from final diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/docs.md b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/docs.md new file mode 100644 index 0000000000..c6ae93be07 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/docs.md @@ -0,0 +1,14 @@ +{% docs orders_status %} + +Orders can be one of the following statuses: + +| status | description | +|----------------|------------------------------------------------------------------------------------------------------------------------| +| placed | The order has been placed but has not yet left the warehouse | +| shipped | The order has ben shipped to the customer and is currently in transit | +| completed | The order has been received by the customer | +| return_pending | The customer has indicated that they would like to return the order, but it has not yet been received at the warehouse | +| returned | The order has been returned by the customer and received at the warehouse | + + +{% enddocs %} diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/orders.sql b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/orders.sql new file mode 100644 index 0000000000..cbb2934911 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/orders.sql @@ -0,0 +1,56 @@ +{% set payment_methods = ['credit_card', 'coupon', 'bank_transfer', 'gift_card'] %} + +with orders as ( + + select * from {{ ref('stg_orders') }} + +), + +payments as ( + + select * from {{ ref('stg_payments') }} + +), + +order_payments as ( + + select + order_id, + + {% for payment_method in payment_methods -%} + sum(case when payment_method = '{{ payment_method }}' then amount else 0 end) as {{ payment_method }}_amount, + {% endfor -%} + + sum(amount) as total_amount + + from payments + + group by order_id + +), + +final as ( + + select + orders.order_id, + orders.customer_id, + orders.order_date, + orders.status, + + {% for payment_method in payment_methods -%} + + order_payments.{{ payment_method }}_amount, + + {% endfor -%} + + order_payments.total_amount as amount + + from orders + + + left join order_payments + on orders.order_id = order_payments.order_id + +) + +select * from final diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/overview.md b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/overview.md new file mode 100644 index 0000000000..0544c42b17 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/overview.md @@ -0,0 +1,11 @@ +{% docs __overview__ %} + +## Data Documentation for Jaffle Shop + +`jaffle_shop` is a fictional ecommerce store. + +This [dbt](https://www.getdbt.com/) project is for testing out code. + +The source code can be found [here](https://github.com/clrcrl/jaffle_shop). + +{% enddocs %} diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/schema.yml b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/schema.yml new file mode 100644 index 0000000000..381349cfda --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/schema.yml @@ -0,0 +1,82 @@ +version: 2 + +models: + - name: customers + description: This table has basic information about a customer, as well as some derived facts based on a customer's orders + + columns: + - name: customer_id + description: This is a unique identifier for a customer + tests: + - unique + - not_null + + - name: first_name + description: Customer's first name. PII. + + - name: last_name + description: Customer's last name. PII. + + - name: first_order + description: Date (UTC) of a customer's first order + + - name: most_recent_order + description: Date (UTC) of a customer's most recent order + + - name: number_of_orders + description: Count of the number of orders a customer has placed + + - name: total_order_amount + description: Total value (AUD) of a customer's orders + + - name: orders + description: This table has basic information about orders, as well as some derived facts based on payments + + columns: + - name: order_id + tests: + - unique + - not_null + description: This is a unique identifier for an order + + - name: customer_id + description: Foreign key to the customers table + tests: + - not_null + - relationships: + to: ref('customers') + field: customer_id + + - name: order_date + description: Date (UTC) that the order was placed + + - name: status + description: '{{ doc("orders_status") }}' + tests: + - accepted_values: + values: ['placed', 'shipped', 'completed', 'return_pending', 'returned'] + + - name: amount + description: Total amount (AUD) of the order + tests: + - not_null + + - name: credit_card_amount + description: Amount of the order (AUD) paid for by credit card + tests: + - not_null + + - name: coupon_amount + description: Amount of the order (AUD) paid for by coupon + tests: + - not_null + + - name: bank_transfer_amount + description: Amount of the order (AUD) paid for by bank transfer + tests: + - not_null + + - name: gift_card_amount + description: Amount of the order (AUD) paid for by gift card + tests: + - not_null diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/schema.yml b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/schema.yml new file mode 100644 index 0000000000..c207e4cf52 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/schema.yml @@ -0,0 +1,31 @@ +version: 2 + +models: + - name: stg_customers + columns: + - name: customer_id + tests: + - unique + - not_null + + - name: stg_orders + columns: + - name: order_id + tests: + - unique + - not_null + - name: status + tests: + - accepted_values: + values: ['placed', 'shipped', 'completed', 'return_pending', 'returned'] + + - name: stg_payments + columns: + - name: payment_id + tests: + - unique + - not_null + - name: payment_method + tests: + - accepted_values: + values: ['credit_card', 'coupon', 'bank_transfer', 'gift_card'] diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_customers.sql b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_customers.sql new file mode 100644 index 0000000000..cad0472695 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_customers.sql @@ -0,0 +1,22 @@ +with source as ( + + {#- + Normally we would select from the table here, but we are using seeds to load + our data in this project + #} + select * from {{ ref('raw_customers') }} + +), + +renamed as ( + + select + id as customer_id, + first_name, + last_name + + from source + +) + +select * from renamed diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_orders.sql b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_orders.sql new file mode 100644 index 0000000000..a654dcb947 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_orders.sql @@ -0,0 +1,23 @@ +with source as ( + + {#- + Normally we would select from the table here, but we are using seeds to load + our data in this project + #} + select * from {{ ref('raw_orders') }} + +), + +renamed as ( + + select + id as order_id, + user_id as customer_id, + order_date, + status + + from source + +) + +select * from renamed diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_payments.sql b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_payments.sql new file mode 100644 index 0000000000..700cf7f4f6 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_payments.sql @@ -0,0 +1,25 @@ +with source as ( + + {#- + Normally we would select from the table here, but we are using seeds to load + our data in this project + #} + select * from {{ ref('raw_payments') }} + +), + +renamed as ( + + select + id as payment_id, + order_id, + payment_method, + + -- `amount` is currently stored in cents, so we convert it to dollars + amount / 100 as amount + + from source + +) + +select * from renamed diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/profiles.yml b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/profiles.yml new file mode 100644 index 0000000000..9008a2d62c --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/profiles.yml @@ -0,0 +1,8 @@ +jaffle_shop: + + target: dev + outputs: + dev: + type: duckdb + path: 'jaffle_shop.duckdb' + threads: 24 diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/.gitkeep b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_customers.csv b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_customers.csv new file mode 100644 index 0000000000..b3e6747d69 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_customers.csv @@ -0,0 +1,101 @@ +id,first_name,last_name +1,Michael,P. +2,Shawn,M. +3,Kathleen,P. +4,Jimmy,C. +5,Katherine,R. +6,Sarah,R. +7,Martin,M. +8,Frank,R. +9,Jennifer,F. +10,Henry,W. +11,Fred,S. +12,Amy,D. +13,Kathleen,M. +14,Steve,F. +15,Teresa,H. +16,Amanda,H. +17,Kimberly,R. +18,Johnny,K. +19,Virginia,F. +20,Anna,A. +21,Willie,H. +22,Sean,H. +23,Mildred,A. +24,David,G. +25,Victor,H. +26,Aaron,R. +27,Benjamin,B. +28,Lisa,W. +29,Benjamin,K. +30,Christina,W. +31,Jane,G. +32,Thomas,O. +33,Katherine,M. +34,Jennifer,S. +35,Sara,T. +36,Harold,O. +37,Shirley,J. +38,Dennis,J. +39,Louise,W. +40,Maria,A. +41,Gloria,C. +42,Diana,S. +43,Kelly,N. +44,Jane,R. +45,Scott,B. +46,Norma,C. +47,Marie,P. +48,Lillian,C. +49,Judy,N. +50,Billy,L. +51,Howard,R. +52,Laura,F. +53,Anne,B. +54,Rose,M. +55,Nicholas,R. +56,Joshua,K. +57,Paul,W. +58,Kathryn,K. +59,Adam,A. +60,Norma,W. +61,Timothy,R. +62,Elizabeth,P. +63,Edward,G. +64,David,C. +65,Brenda,W. +66,Adam,W. +67,Michael,H. +68,Jesse,E. +69,Janet,P. +70,Helen,F. +71,Gerald,C. +72,Kathryn,O. +73,Alan,B. +74,Harry,A. +75,Andrea,H. +76,Barbara,W. +77,Anne,W. +78,Harry,H. +79,Jack,R. +80,Phillip,H. +81,Shirley,H. +82,Arthur,D. +83,Virginia,R. +84,Christina,R. +85,Theresa,M. +86,Jason,C. +87,Phillip,B. +88,Adam,T. +89,Margaret,J. +90,Paul,P. +91,Todd,W. +92,Willie,O. +93,Frances,R. +94,Gregory,H. +95,Lisa,P. +96,Jacqueline,A. +97,Shirley,D. +98,Nicole,M. +99,Mary,G. +100,Jean,M. diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_orders.csv b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_orders.csv new file mode 100644 index 0000000000..7c2be07888 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_orders.csv @@ -0,0 +1,100 @@ +id,user_id,order_date,status +1,1,2018-01-01,returned +2,3,2018-01-02,completed +3,94,2018-01-04,completed +4,50,2018-01-05,completed +5,64,2018-01-05,completed +6,54,2018-01-07,completed +7,88,2018-01-09,completed +8,2,2018-01-11,returned +9,53,2018-01-12,completed +10,7,2018-01-14,completed +11,99,2018-01-14,completed +12,59,2018-01-15,completed +13,84,2018-01-17,completed +14,40,2018-01-17,returned +15,25,2018-01-17,completed +16,39,2018-01-18,completed +17,71,2018-01-18,completed +18,64,2018-01-20,returned +19,54,2018-01-22,completed +20,20,2018-01-23,completed +21,71,2018-01-23,completed +22,86,2018-01-24,completed +23,22,2018-01-26,return_pending +24,3,2018-01-27,completed +25,51,2018-01-28,completed +26,32,2018-01-28,completed +27,94,2018-01-29,completed +28,8,2018-01-29,completed +29,57,2018-01-31,completed +30,69,2018-02-02,completed +31,16,2018-02-02,completed +32,28,2018-02-04,completed +33,42,2018-02-04,completed +34,38,2018-02-06,completed +35,80,2018-02-08,completed +36,85,2018-02-10,completed +37,1,2018-02-10,completed +38,51,2018-02-10,completed +39,26,2018-02-11,completed +40,33,2018-02-13,completed +41,99,2018-02-14,completed +42,92,2018-02-16,completed +43,31,2018-02-17,completed +44,66,2018-02-17,completed +45,22,2018-02-17,completed +46,6,2018-02-19,completed +47,50,2018-02-20,completed +48,27,2018-02-21,completed +49,35,2018-02-21,completed +50,51,2018-02-23,completed +51,71,2018-02-24,completed +52,54,2018-02-25,return_pending +53,34,2018-02-26,completed +54,54,2018-02-26,completed +55,18,2018-02-27,completed +56,79,2018-02-28,completed +57,93,2018-03-01,completed +58,22,2018-03-01,completed +59,30,2018-03-02,completed +60,12,2018-03-03,completed +61,63,2018-03-03,completed +62,57,2018-03-05,completed +63,70,2018-03-06,completed +64,13,2018-03-07,completed +65,26,2018-03-08,completed +66,36,2018-03-10,completed +67,79,2018-03-11,completed +68,53,2018-03-11,completed +69,3,2018-03-11,completed +70,8,2018-03-12,completed +71,42,2018-03-12,shipped +72,30,2018-03-14,shipped +73,19,2018-03-16,completed +74,9,2018-03-17,shipped +75,69,2018-03-18,completed +76,25,2018-03-20,completed +77,35,2018-03-21,shipped +78,90,2018-03-23,shipped +79,52,2018-03-23,shipped +80,11,2018-03-23,shipped +81,76,2018-03-23,shipped +82,46,2018-03-24,shipped +83,54,2018-03-24,shipped +84,70,2018-03-26,placed +85,47,2018-03-26,shipped +86,68,2018-03-26,placed +87,46,2018-03-27,placed +88,91,2018-03-27,shipped +89,21,2018-03-28,placed +90,66,2018-03-30,shipped +91,47,2018-03-31,placed +92,84,2018-04-02,placed +93,66,2018-04-03,placed +94,63,2018-04-03,placed +95,27,2018-04-04,placed +96,90,2018-04-06,placed +97,89,2018-04-07,placed +98,41,2018-04-07,placed +99,85,2018-04-09,placed diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_payments.csv b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_payments.csv new file mode 100644 index 0000000000..a587baab59 --- /dev/null +++ b/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_payments.csv @@ -0,0 +1,114 @@ +id,order_id,payment_method,amount +1,1,credit_card,1000 +2,2,credit_card,2000 +3,3,coupon,100 +4,4,coupon,2500 +5,5,bank_transfer,1700 +6,6,credit_card,600 +7,7,credit_card,1600 +8,8,credit_card,2300 +9,9,gift_card,2300 +10,9,bank_transfer,0 +11,10,bank_transfer,2600 +12,11,credit_card,2700 +13,12,credit_card,100 +14,13,credit_card,500 +15,13,bank_transfer,1400 +16,14,bank_transfer,300 +17,15,coupon,2200 +18,16,credit_card,1000 +19,17,bank_transfer,200 +20,18,credit_card,500 +21,18,credit_card,800 +22,19,gift_card,600 +23,20,bank_transfer,1500 +24,21,credit_card,1200 +25,22,bank_transfer,800 +26,23,gift_card,2300 +27,24,coupon,2600 +28,25,bank_transfer,2000 +29,25,credit_card,2200 +30,25,coupon,1600 +31,26,credit_card,3000 +32,27,credit_card,2300 +33,28,bank_transfer,1900 +34,29,bank_transfer,1200 +35,30,credit_card,1300 +36,31,credit_card,1200 +37,32,credit_card,300 +38,33,credit_card,2200 +39,34,bank_transfer,1500 +40,35,credit_card,2900 +41,36,bank_transfer,900 +42,37,credit_card,2300 +43,38,credit_card,1500 +44,39,bank_transfer,800 +45,40,credit_card,1400 +46,41,credit_card,1700 +47,42,coupon,1700 +48,43,gift_card,1800 +49,44,gift_card,1100 +50,45,bank_transfer,500 +51,46,bank_transfer,800 +52,47,credit_card,2200 +53,48,bank_transfer,300 +54,49,credit_card,600 +55,49,credit_card,900 +56,50,credit_card,2600 +57,51,credit_card,2900 +58,51,credit_card,100 +59,52,bank_transfer,1500 +60,53,credit_card,300 +61,54,credit_card,1800 +62,54,bank_transfer,1100 +63,55,credit_card,2900 +64,56,credit_card,400 +65,57,bank_transfer,200 +66,58,coupon,1800 +67,58,gift_card,600 +68,59,gift_card,2800 +69,60,credit_card,400 +70,61,bank_transfer,1600 +71,62,gift_card,1400 +72,63,credit_card,2900 +73,64,bank_transfer,2600 +74,65,credit_card,0 +75,66,credit_card,2800 +76,67,bank_transfer,400 +77,67,credit_card,1900 +78,68,credit_card,1600 +79,69,credit_card,1900 +80,70,credit_card,2600 +81,71,credit_card,500 +82,72,credit_card,2900 +83,73,bank_transfer,300 +84,74,credit_card,3000 +85,75,credit_card,1900 +86,76,coupon,200 +87,77,credit_card,0 +88,77,bank_transfer,1900 +89,78,bank_transfer,2600 +90,79,credit_card,1800 +91,79,credit_card,900 +92,80,gift_card,300 +93,81,coupon,200 +94,82,credit_card,800 +95,83,credit_card,100 +96,84,bank_transfer,2500 +97,85,bank_transfer,1700 +98,86,coupon,2300 +99,87,gift_card,3000 +100,87,credit_card,2600 +101,88,credit_card,2900 +102,89,bank_transfer,2200 +103,90,bank_transfer,200 +104,91,credit_card,1900 +105,92,bank_transfer,1500 +106,92,coupon,200 +107,93,gift_card,2600 +108,94,coupon,700 +109,95,coupon,2400 +110,96,gift_card,1700 +111,97,bank_transfer,1400 +112,98,bank_transfer,1000 +113,99,credit_card,2400 diff --git a/tests/dbt/cli/test_list.py b/tests/dbt/cli/test_list.py new file mode 100644 index 0000000000..9312be8635 --- /dev/null +++ b/tests/dbt/cli/test_list.py @@ -0,0 +1,17 @@ +import typing as t +import pytest +from pathlib import Path +from click.testing import Result + +pytestmark = pytest.mark.slow + + +def test_list(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): + result = invoke_cli(["list"]) + + assert result.exit_code == 0 + assert not result.exception + + assert "main.orders" in result.output + assert "main.customers" in result.output + assert "main.stg_payments" in result.output diff --git a/tests/dbt/cli/test_operations.py b/tests/dbt/cli/test_operations.py new file mode 100644 index 0000000000..e384028bbc --- /dev/null +++ b/tests/dbt/cli/test_operations.py @@ -0,0 +1,57 @@ +from pathlib import Path +import pytest +from sqlmesh_dbt.operations import create +from sqlmesh.utils import yaml +import time_machine + +pytestmark = pytest.mark.slow + + +def test_create_injects_default_start_date(jaffle_shop_duckdb: Path): + with time_machine.travel("2020-01-02 00:00:00 UTC"): + from sqlmesh.utils.date import yesterday_ds + + assert yesterday_ds() == "2020-01-01" + + operations = create() + + assert operations.context.config.model_defaults.start == "2020-01-01" + assert all( + model.start == "2020-01-01" + for model in operations.context.models.values() + if not model.kind.is_seed + ) + + # check that the date set on the first invocation persists to future invocations + from sqlmesh.utils.date import yesterday_ds + + assert yesterday_ds() != "2020-01-01" + + operations = create() + + assert operations.context.config.model_defaults.start == "2020-01-01" + assert all( + model.start == "2020-01-01" + for model in operations.context.models.values() + if not model.kind.is_seed + ) + + +def test_create_uses_configured_start_date_if_supplied(jaffle_shop_duckdb: Path): + dbt_project_yaml = jaffle_shop_duckdb / "dbt_project.yml" + + contents = yaml.load(dbt_project_yaml, render_jinja=False) + + contents["models"]["+start"] = "2023-12-12" + + with dbt_project_yaml.open("w") as f: + yaml.dump(contents, f) + + operations = create() + + assert operations.context.config.model_defaults.start == "2023-12-12" + assert all( + model.start == "2023-12-12" + for model in operations.context.models.values() + if not model.kind.is_seed + ) diff --git a/tests/dbt/cli/test_run.py b/tests/dbt/cli/test_run.py new file mode 100644 index 0000000000..0e4a04bcb1 --- /dev/null +++ b/tests/dbt/cli/test_run.py @@ -0,0 +1,15 @@ +import typing as t +import pytest +from pathlib import Path +from click.testing import Result + +pytestmark = pytest.mark.slow + + +def test_run(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): + result = invoke_cli(["run"]) + + assert result.exit_code == 0 + assert not result.exception + + assert "Model batches executed" in result.output From 54320bdb1ad9d5024478919e08b0935df2e6a0e2 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:37:54 +0300 Subject: [PATCH 0684/1056] Fix!: mark vars referenced in metadata macros as metadata (#4936) --- sqlmesh/core/audit/definition.py | 8 +- sqlmesh/core/constants.py | 2 + sqlmesh/core/macros.py | 19 +- sqlmesh/core/model/common.py | 239 +++++++++++++++--- sqlmesh/core/model/definition.py | 29 ++- ...88_warn_about_variable_python_env_diffs.py | 74 ++++++ sqlmesh/utils/jinja.py | 2 +- sqlmesh/utils/metaprogramming.py | 17 +- tests/core/test_model.py | 216 +++++++++++++++- tests/utils/test_metaprogramming.py | 2 +- 10 files changed, 535 insertions(+), 73 deletions(-) create mode 100644 sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py diff --git a/sqlmesh/core/audit/definition.py b/sqlmesh/core/audit/definition.py index c136a00cc0..210ae9da1b 100644 --- a/sqlmesh/core/audit/definition.py +++ b/sqlmesh/core/audit/definition.py @@ -438,13 +438,15 @@ def load_audit( extra_kwargs: t.Dict[str, t.Any] = {} if is_standalone: - jinja_macro_refrences, used_variables = extract_macro_references_and_variables( + jinja_macro_refrences, referenced_variables = extract_macro_references_and_variables( *(gen(s) for s in statements), gen(query), ) jinja_macros = (jinja_macros or JinjaMacroRegistry()).trim(jinja_macro_refrences) for jinja_macro in jinja_macros.root_macros.values(): - used_variables.update(extract_macro_references_and_variables(jinja_macro.definition)[1]) + referenced_variables.update( + extract_macro_references_and_variables(jinja_macro.definition)[1] + ) extra_kwargs["jinja_macros"] = jinja_macros extra_kwargs["python_env"] = make_python_env( @@ -453,7 +455,7 @@ def load_audit( module_path, macros or macro.get_registry(), variables=variables, - used_variables=used_variables, + referenced_variables=referenced_variables, ) extra_kwargs["default_catalog"] = default_catalog if project is not None: diff --git a/sqlmesh/core/constants.py b/sqlmesh/core/constants.py index 27d6cf0d7f..2df7697b9d 100644 --- a/sqlmesh/core/constants.py +++ b/sqlmesh/core/constants.py @@ -80,7 +80,9 @@ DEFAULT_SCHEMA = "default" SQLMESH_VARS = "__sqlmesh__vars__" +SQLMESH_VARS_METADATA = "__sqlmesh__vars__metadata__" SQLMESH_BLUEPRINT_VARS = "__sqlmesh__blueprint__vars__" +SQLMESH_BLUEPRINT_VARS_METADATA = "__sqlmesh__blueprint__vars__metadata__" VAR = "var" BLUEPRINT_VAR = "blueprint_var" diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index af891a5460..42a4a8b8dc 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -210,7 +210,12 @@ def __init__( self.macros[normalize_macro_name(k)] = self.env[k] elif v.is_value: value = self.env[k] - if k in (c.SQLMESH_VARS, c.SQLMESH_BLUEPRINT_VARS): + if k in ( + c.SQLMESH_VARS, + c.SQLMESH_VARS_METADATA, + c.SQLMESH_BLUEPRINT_VARS, + c.SQLMESH_BLUEPRINT_VARS_METADATA, + ): value = { var_name: ( self.parse_one(var_value.sql) @@ -557,17 +562,25 @@ def views(self) -> t.List[str]: def var(self, var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]: """Returns the value of the specified variable, or the default value if it doesn't exist.""" - return (self.locals.get(c.SQLMESH_VARS) or {}).get(var_name.lower(), default) + return { + **(self.locals.get(c.SQLMESH_VARS) or {}), + **(self.locals.get(c.SQLMESH_VARS_METADATA) or {}), + }.get(var_name.lower(), default) def blueprint_var(self, var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]: """Returns the value of the specified blueprint variable, or the default value if it doesn't exist.""" - return (self.locals.get(c.SQLMESH_BLUEPRINT_VARS) or {}).get(var_name.lower(), default) + return { + **(self.locals.get(c.SQLMESH_BLUEPRINT_VARS) or {}), + **(self.locals.get(c.SQLMESH_BLUEPRINT_VARS_METADATA) or {}), + }.get(var_name.lower(), default) @property def variables(self) -> t.Dict[str, t.Any]: return { **self.locals.get(c.SQLMESH_VARS, {}), + **self.locals.get(c.SQLMESH_VARS_METADATA, {}), **self.locals.get(c.SQLMESH_BLUEPRINT_VARS, {}), + **self.locals.get(c.SQLMESH_BLUEPRINT_VARS_METADATA, {}), } def _coerce(self, expr: exp.Expression, typ: t.Any, strict: bool = False) -> t.Any: diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index 11ddc8234b..9a68ec18c0 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -28,7 +28,7 @@ from sqlmesh.utils import registry_decorator from sqlmesh.utils.jinja import MacroReference - MacroCallable = registry_decorator + MacroCallable = t.Union[Executable, registry_decorator] def make_python_env( @@ -40,7 +40,7 @@ def make_python_env( module_path: Path, macros: MacroRegistry, variables: t.Optional[t.Dict[str, t.Any]] = None, - used_variables: t.Optional[t.Set[str]] = None, + referenced_variables: t.Optional[t.Set[str]] = None, path: t.Optional[Path] = None, python_env: t.Optional[t.Dict[str, Executable]] = None, strict_resolution: bool = True, @@ -48,20 +48,64 @@ def make_python_env( dialect: DialectType = None, ) -> t.Dict[str, Executable]: python_env = {} if python_env is None else python_env - variables = variables or {} env: t.Dict[str, t.Tuple[t.Any, t.Optional[bool]]] = {} - used_macros: t.Dict[ - str, - t.Tuple[t.Union[Executable | MacroCallable], t.Optional[bool]], - ] = {} - used_variables = (used_variables or set()).copy() + + variables = variables or {} + blueprint_variables = blueprint_variables or {} + + used_macros: t.Dict[str, t.Tuple[MacroCallable, bool]] = {} + + # var -> True: var is metadata-only + # var -> False: var is not metadata-only + # var -> None: cannot determine whether var is metadata-only yet, need to walk macros first + used_variables: t.Dict[str, t.Optional[bool]] = dict.fromkeys( + referenced_variables or set(), False + ) + + # id(expr) -> true: expr appears under the AST of a metadata-only macro function + # id(expr) -> false: expr appears under the AST of a macro function whose metadata status we don't yet know + expr_under_metadata_macro_func: t.Dict[int, bool] = {} + + # For @m1(@m2(@x), @y), we'd get x -> m1 and y -> m1 + outermost_macro_func_ancestor_by_var: t.Dict[str, str] = {} + visited_macro_funcs: t.Set[int] = set() + + def _is_metadata_var( + name: str, expression: exp.Expression, appears_in_metadata_expression: bool + ) -> t.Optional[bool]: + is_metadata_so_far = used_variables.get(name, True) + if is_metadata_so_far is False: + # We've concluded this variable is definitely not metadata-only + return False + + appears_under_metadata_macro_func = expr_under_metadata_macro_func.get(id(expression)) + if is_metadata_so_far and ( + appears_in_metadata_expression or appears_under_metadata_macro_func + ): + # The variable appears in a metadata expression, e.g., audits (...), + # or in the AST of metadata-only macro call, e.g., @FOO(@x) + return True + + # The variable appears in the AST of a macro call, but we don't know if it's metadata-only + if appears_under_metadata_macro_func is False: + return None + + # The variable appears elsewhere, e.g., in the model's query: SELECT @x + return False + + def _is_metadata_macro(name: str, appears_in_metadata_expression: bool) -> bool: + if name in used_macros: + is_metadata_so_far = used_macros[name][1] + return is_metadata_so_far and appears_in_metadata_expression + + return appears_in_metadata_expression expressions = ensure_list(expressions) for expression_metadata in expressions: if isinstance(expression_metadata, tuple): expression, is_metadata = expression_metadata else: - expression, is_metadata = expression_metadata, None + expression, is_metadata = expression_metadata, False if isinstance(expression, d.Jinja): continue @@ -72,31 +116,51 @@ def make_python_env( if name not in macros: continue - # If this macro has been seen before as a non-metadata macro, prioritize that - used_macros[name] = ( - macros[name], - used_macros.get(name, (None, is_metadata))[1] and is_metadata, - ) - if name == c.VAR: + used_macros[name] = (macros[name], _is_metadata_macro(name, is_metadata)) + + if name in (c.VAR, c.BLUEPRINT_VAR): args = macro_func_or_var.this.expressions if len(args) < 1: - raise_config_error("Macro VAR requires at least one argument", path) + raise_config_error( + f"Macro {name.upper()} requires at least one argument", path + ) + if not args[0].is_string: raise_config_error( f"The variable name must be a string literal, '{args[0].sql()}' was given instead", path, ) - used_variables.add(args[0].this.lower()) + + var_name = args[0].this.lower() + used_variables[var_name] = _is_metadata_var( + var_name, macro_func_or_var, is_metadata + ) + elif id(macro_func_or_var) not in visited_macro_funcs: + # We only care about the top-level macro function calls to determine the metadata + # status of the variables referenced in their ASTs. For example, in @m1(@m2(@x)), + # if m1 is metadata-only but m2 is not, we can still determine that @x only affects + # the metadata hash, since m2's result feeds into a metadata-only macro function. + # + # Generally, if the top-level call is known to be metadata-only or appear in a + # metadata expression, then we can avoid traversing nested macro function calls. + + var_refs, _expr_under_metadata_macro_func, _visited_macro_funcs = ( + _extract_macro_func_variable_references(macro_func_or_var, is_metadata) + ) + expr_under_metadata_macro_func.update(_expr_under_metadata_macro_func) + visited_macro_funcs.update(_visited_macro_funcs) + outermost_macro_func_ancestor_by_var |= {var_ref: name for var_ref in var_refs} elif macro_func_or_var.__class__ is d.MacroVar: - name = macro_func_or_var.name.lower() - if name in macros: - # If this macro has been seen before as a non-metadata macro, prioritize that - used_macros[name] = ( - macros[name], - used_macros.get(name, (None, is_metadata))[1] and is_metadata, + var_name = macro_func_or_var.name.lower() + if var_name in macros: + used_macros[var_name] = ( + macros[var_name], + _is_metadata_macro(var_name, is_metadata), + ) + elif var_name in variables or var_name in blueprint_variables: + used_variables[var_name] = _is_metadata_var( + var_name, macro_func_or_var, is_metadata ) - elif name in variables: - used_variables.add(name) elif ( isinstance(macro_func_or_var, (exp.Identifier, d.MacroStrReplace, d.MacroSQL)) ) and "@" in macro_func_or_var.name: @@ -104,12 +168,14 @@ def make_python_env( macro_func_or_var.name ): var_name = braced_identifier or identifier - if var_name in variables: - used_variables.add(var_name) + if var_name in variables or var_name in blueprint_variables: + used_variables[var_name] = _is_metadata_var( + var_name, macro_func_or_var, is_metadata + ) for macro_ref in jinja_macro_references or set(): if macro_ref.package is None and macro_ref.name in macros: - used_macros[macro_ref.name] = (macros[macro_ref.name], None) + used_macros[macro_ref.name] = (macros[macro_ref.name], False) for name, (used_macro, is_metadata) in used_macros.items(): if isinstance(used_macro, Executable): @@ -131,16 +197,49 @@ def make_python_env( blueprint_variables=blueprint_variables, dialect=dialect, strict_resolution=strict_resolution, + outermost_macro_func_ancestor_by_var=outermost_macro_func_ancestor_by_var, ) +def _extract_macro_func_variable_references( + macro_func: exp.Expression, + is_metadata: bool, +) -> t.Tuple[t.Set[str], t.Dict[int, bool], t.Set[int]]: + var_references = set() + visited_macro_funcs = set() + expr_under_metadata_macro_func = {} + + for n in macro_func.walk(): + if type(n) is d.MacroFunc: + visited_macro_funcs.add(id(n)) + + this = n.this + args = this.expressions + + if this.name.lower() in (c.VAR, c.BLUEPRINT_VAR) and args and args[0].is_string: + var_references.add(args[0].this.lower()) + expr_under_metadata_macro_func[id(n)] = is_metadata + elif isinstance(n, d.MacroVar): + var_references.add(n.name.lower()) + expr_under_metadata_macro_func[id(n)] = is_metadata + elif isinstance(n, (exp.Identifier, d.MacroStrReplace, d.MacroSQL)) and "@" in n.name: + var_references.update( + (braced_identifier or identifier).lower() + for _, identifier, braced_identifier, _ in MacroStrTemplate.pattern.findall(n.name) + ) + expr_under_metadata_macro_func[id(n)] = is_metadata + + return (var_references, expr_under_metadata_macro_func, visited_macro_funcs) + + def _add_variables_to_python_env( python_env: t.Dict[str, Executable], - used_variables: t.Optional[t.Set[str]], + used_variables: t.Dict[str, t.Optional[bool]], variables: t.Optional[t.Dict[str, t.Any]], strict_resolution: bool = True, blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None, dialect: DialectType = None, + outermost_macro_func_ancestor_by_var: t.Optional[t.Dict[str, str]] = None, ) -> t.Dict[str, Executable]: _, python_used_variables = parse_dependencies( python_env, @@ -149,20 +248,67 @@ def _add_variables_to_python_env( variables=variables, blueprint_variables=blueprint_variables, ) - used_variables = (used_variables or set()) | python_used_variables + for var_name, is_metadata in python_used_variables.items(): + used_variables[var_name] = is_metadata and used_variables.get(var_name, True) + + # Variables are treated as metadata-only when all of their references either: + # - appear in metadata-only expressions, such as `audits (...)`, virtual statements, etc + # - appear in the ASTs or definitions of metadata-only macros + # + # See also: https://github.com/TobikoData/sqlmesh/pull/4936#issuecomment-3136339936, + # specifically the "Terminology" and "Observations" section. + metadata_used_variables = { + var_name for var_name, is_metadata in used_variables.items() if is_metadata + } + for used_var, outermost_macro_func in (outermost_macro_func_ancestor_by_var or {}).items(): + used_var_is_metadata = used_variables.get(used_var) + if used_var_is_metadata is False: + continue + + # At this point we can decide whether a variable reference in a macro call's AST is + # metadata-only, because we've annotated the corresponding macro call in the python env. + if outermost_macro_func in python_env and python_env[outermost_macro_func].is_metadata: + metadata_used_variables.add(used_var) + + non_metadata_used_variables = set(used_variables) - metadata_used_variables + + if overlapping_variables := (non_metadata_used_variables & metadata_used_variables): + raise ConfigError( + f"Variables {', '.join(overlapping_variables)} are both metadata and non-metadata, " + "which is unexpected. Please file an issue at https://github.com/TobikoData/sqlmesh/issues/new." + ) + + metadata_variables = { + k: v for k, v in (variables or {}).items() if k in metadata_used_variables + } + variables = {k: v for k, v in (variables or {}).items() if k in non_metadata_used_variables} - variables = {k: v for k, v in (variables or {}).items() if k in used_variables} if variables: python_env[c.SQLMESH_VARS] = Executable.value(variables, sort_root_dict=True) + if metadata_variables: + python_env[c.SQLMESH_VARS_METADATA] = Executable.value( + metadata_variables, sort_root_dict=True, is_metadata=True + ) if blueprint_variables: + metadata_blueprint_variables = { + k: SqlValue(sql=v.sql(dialect=dialect)) if isinstance(v, exp.Expression) else v + for k, v in blueprint_variables.items() + if k in metadata_used_variables + } blueprint_variables = { k.lower(): SqlValue(sql=v.sql(dialect=dialect)) if isinstance(v, exp.Expression) else v for k, v in blueprint_variables.items() + if k in non_metadata_used_variables } - python_env[c.SQLMESH_BLUEPRINT_VARS] = Executable.value( - blueprint_variables, sort_root_dict=True - ) + if blueprint_variables: + python_env[c.SQLMESH_BLUEPRINT_VARS] = Executable.value( + blueprint_variables, sort_root_dict=True + ) + if metadata_blueprint_variables: + python_env[c.SQLMESH_BLUEPRINT_VARS_METADATA] = Executable.value( + metadata_blueprint_variables, sort_root_dict=True, is_metadata=True + ) return python_env @@ -173,7 +319,7 @@ def parse_dependencies( strict_resolution: bool = True, variables: t.Optional[t.Dict[str, t.Any]] = None, blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None, -) -> t.Tuple[t.Set[str], t.Set[str]]: +) -> t.Tuple[t.Set[str], t.Dict[str, bool]]: """ Parses the source of a model function and finds upstream table dependencies and referenced variables based on calls to context / evaluator. @@ -187,7 +333,8 @@ def parse_dependencies( blueprint_variables: The blueprint variables available to the python environment. Returns: - A tuple containing the set of upstream table dependencies and the set of referenced variables. + A tuple containing the set of upstream table dependencies and a mapping of + the referenced variables associated with their metadata status. """ class VariableResolutionContext: @@ -205,12 +352,16 @@ def blueprint_var(var_name: str, default: t.Optional[t.Any] = None) -> t.Optiona local_env = dict.fromkeys(("context", "evaluator"), VariableResolutionContext) depends_on = set() - used_variables = set() + used_variables: t.Dict[str, bool] = {} for executable in python_env.values(): if not executable.is_definition: continue + + is_metadata = executable.is_metadata for node in ast.walk(ast.parse(executable.payload)): + next_variables = set() + if isinstance(node, ast.Call): func = node.func if not isinstance(func, ast.Attribute) or not isinstance(func.value, ast.Name): @@ -241,8 +392,11 @@ def get_first_arg(keyword_arg_name: str) -> t.Any: if func.value.id == "context" and func.attr in ("table", "resolve_table"): depends_on.add(get_first_arg("model_name")) - elif func.value.id in ("context", "evaluator") and func.attr == c.VAR: - used_variables.add(get_first_arg("var_name").lower()) + elif func.value.id in ("context", "evaluator") and func.attr in ( + c.VAR, + c.BLUEPRINT_VAR, + ): + next_variables.add(get_first_arg("var_name").lower()) elif ( isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) @@ -250,9 +404,9 @@ def get_first_arg(keyword_arg_name: str) -> t.Any: and node.attr == c.GATEWAY ): # Check whether the gateway attribute is referenced. - used_variables.add(c.GATEWAY) + next_variables.add(c.GATEWAY) elif isinstance(node, ast.FunctionDef) and node.name == entrypoint: - used_variables.update( + next_variables.update( [ arg.arg for arg in [*node.args.args, *node.args.kwonlyargs] @@ -260,6 +414,9 @@ def get_first_arg(keyword_arg_name: str) -> t.Any: ] ) + for var_name in next_variables: + used_variables[var_name] = used_variables.get(var_name, True) and bool(is_metadata) + return depends_on, used_variables diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 559d67e960..f2cfeac163 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -1782,12 +1782,17 @@ def render( start, end = make_inclusive(start or c.EPOCH, end or c.EPOCH, self.dialect) execution_time = to_datetime(execution_time or c.EPOCH) - variables = env.get(c.SQLMESH_VARS, {}) - variables.update(kwargs.pop("variables", {})) - + variables = { + **env.get(c.SQLMESH_VARS, {}), + **env.get(c.SQLMESH_VARS_METADATA, {}), + **kwargs.pop("variables", {}), + } blueprint_variables = { k: d.parse_one(v.sql, dialect=self.dialect) if isinstance(v, SqlValue) else v - for k, v in env.get(c.SQLMESH_BLUEPRINT_VARS, {}).items() + for k, v in { + **env.get(c.SQLMESH_BLUEPRINT_VARS, {}), + **env.get(c.SQLMESH_BLUEPRINT_VARS_METADATA, {}), + }.items() } try: kwargs = { @@ -1909,11 +1914,11 @@ def _extract_blueprint_variables(blueprint: t.Any, path: Path) -> t.Dict[str, t. return {} if isinstance(blueprint, (exp.Paren, exp.PropertyEQ)): blueprint = blueprint.unnest() - return {blueprint.left.name: blueprint.right} + return {blueprint.left.name.lower(): blueprint.right} if isinstance(blueprint, (exp.Tuple, exp.Array)): - return {e.left.name: e.right for e in blueprint.expressions} + return {e.left.name.lower(): e.right for e in blueprint.expressions} if isinstance(blueprint, dict): - return blueprint + return {k.lower(): v for k, v in blueprint.items()} raise_config_error( f"Expected a key-value mapping for the blueprint value, got '{blueprint}' instead", @@ -2509,7 +2514,7 @@ def _create_model( if isinstance(getattr(kwargs.get("kind"), "merge_filter", None), exp.Expression): statements.append(kwargs["kind"].merge_filter) - jinja_macro_references, used_variables = extract_macro_references_and_variables( + jinja_macro_references, referenced_variables = extract_macro_references_and_variables( *(gen(e if isinstance(e, exp.Expression) else e[0]) for e in statements) ) @@ -2532,11 +2537,13 @@ def _create_model( _extract_migrated_dbt_variable_references(jinja_macros, variables) ) - used_variables.update(nested_macro_used_variables) + referenced_variables.update(nested_macro_used_variables) variables.update(flattened_package_variables) else: for jinja_macro in jinja_macros.root_macros.values(): - used_variables.update(extract_macro_references_and_variables(jinja_macro.definition)[1]) + referenced_variables.update( + extract_macro_references_and_variables(jinja_macro.definition)[1] + ) # Merge model-specific audits with default audits if default_audits := defaults.pop("audits", None): @@ -2598,7 +2605,7 @@ def _create_model( module_path, macros or macro.get_registry(), variables=variables, - used_variables=used_variables, + referenced_variables=referenced_variables, path=path, python_env=python_env, strict_resolution=depends_on is None, diff --git a/sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py b/sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py new file mode 100644 index 0000000000..eb33a8041f --- /dev/null +++ b/sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py @@ -0,0 +1,74 @@ +""" +This script's goal is to warn users about two situations that could lead to a diff: + +- They have blueprint models and some of their variables may be trimmed from `python_env` +- Variables are used in metadata-only contexts, e.g., within metadata-only macros + +Context: + +We used to store *all* blueprint variables in `python_env`, even though some of them were +redundant. For example, if a blueprint variable is only used in the model's `name` property, +then it is rendered once, at load time, and after that point it's not needed elsewhere. + +This behavior is now different: we only store the blueprint variables that are required to render +expressions at runtime, such as model query or runtime-rendered properties, like `merge_filter`. + +Additionally, variables were previously treated as non-metadata, regardless of how they were used. +This behavior changed as well: SQLMesh now analyzes variable references and tracks the data flow, +in order to detect whether changing them will result in a metadata diff for a given model. + +Some examples where variables can be treated as metadata-only `python_env` executables are: + +- A variable is referenced in metadata-only macros +- A variable is referenced in metadata-only expressions, such as virtual update statements +- A variable is passed as argument to metadata-only macros +""" + +import json + +from sqlglot import exp + +from sqlmesh.core.console import get_console + +SQLMESH_VARS = "__sqlmesh__vars__" +SQLMESH_BLUEPRINT_VARS = "__sqlmesh__blueprint__vars__" +METADATA_HASH_EXPRESSIONS = {"on_virtual_update", "audits", "signals", "audit_definitions"} + + +def migrate(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + warning = ( + "SQLMesh detected that it may not be able to fully migrate the state database. This should not impact " + "the migration process, but may result in unexpected changes being reported by the next `sqlmesh plan` " + "command. Please run `sqlmesh diff prod` after the migration has completed, before making any new " + "changes. If any unexpected changes are reported, consider running a forward-only plan to apply these " + "changes and avoid unnecessary backfills: sqlmesh plan prod --forward-only. " + "See https://sqlmesh.readthedocs.io/en/stable/concepts/plans/#forward-only-plans for more details.\n" + ) + + for (snapshot,) in engine_adapter.fetchall( + exp.select("snapshot").from_(snapshots_table), quote_identifiers=True + ): + parsed_snapshot = json.loads(snapshot) + node = parsed_snapshot["node"] + + # Standalone audits don't have a data hash, so they're unaffected + if node.get("source_type") == "audit": + continue + + python_env = node.get("python_env") or {} + + if SQLMESH_BLUEPRINT_VARS in python_env or ( + SQLMESH_VARS in python_env + and ( + any(v.get("is_metadata") for v in python_env.values()) + or any(node.get(k) for k in METADATA_HASH_EXPRESSIONS) + ) + ): + get_console().log_warning(warning) + return diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index d1c0ef0361..6720c24581 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -229,7 +229,7 @@ def extract_macro_references_and_variables( ) for call_name, node in extract_call_names(jinja_str): - if call_name[0] == c.VAR: + if call_name[0] in (c.VAR, c.BLUEPRINT_VAR): assert isinstance(node, nodes.Call) args = [jinja_call_arg_name(arg) for arg in node.args] if args and args[0]: diff --git a/sqlmesh/utils/metaprogramming.py b/sqlmesh/utils/metaprogramming.py index 9330532442..858e8a50da 100644 --- a/sqlmesh/utils/metaprogramming.py +++ b/sqlmesh/utils/metaprogramming.py @@ -283,7 +283,7 @@ def build_env( env: t.Dict[str, t.Tuple[t.Any, t.Optional[bool]]], name: str, path: Path, - is_metadata_obj: t.Optional[bool] = None, + is_metadata_obj: bool = False, ) -> None: """Fills in env dictionary with all globals needed to execute the object. @@ -299,7 +299,7 @@ def build_env( # We don't rely on `env` to keep track of visited objects, because it's populated in post-order visited: t.Set[str] = set() - def walk(obj: t.Any, name: str, is_metadata: t.Optional[bool] = None) -> None: + def walk(obj: t.Any, name: str, is_metadata: bool = False) -> None: obj_module = inspect.getmodule(obj) if obj_module and obj_module.__name__ == "builtins": return @@ -320,7 +320,7 @@ def walk(obj: t.Any, name: str, is_metadata: t.Optional[bool] = None) -> None: # The existing object in the env is "metadata only" but we're walking it again as a # non-"metadata only" dependency, so we update this flag to ensure all transitive # dependencies are also not marked as "metadata only" - is_metadata = None + is_metadata = False if hasattr(obj, c.SQLMESH_MACRO): # We only need to add the undecorated code of @macro() functions in env, which @@ -380,7 +380,7 @@ def walk(obj: t.Any, name: str, is_metadata: t.Optional[bool] = None) -> None: ) # The "metadata only" annotation of the object is transitive - walk(obj, name, is_metadata_obj or getattr(obj, c.SQLMESH_METADATA, None)) + walk(obj, name, is_metadata_obj or getattr(obj, c.SQLMESH_METADATA, False)) @dataclass @@ -432,7 +432,11 @@ def value( cls, v: t.Any, is_metadata: t.Optional[bool] = None, sort_root_dict: bool = False ) -> Executable: payload = _dict_sort(v) if sort_root_dict else repr(v) - return Executable(payload=payload, kind=ExecutableKind.VALUE, is_metadata=is_metadata) + return Executable( + payload=payload, + kind=ExecutableKind.VALUE, + is_metadata=is_metadata or None, + ) def serialize_env(env: t.Dict[str, t.Any], path: Path) -> t.Dict[str, Executable]: @@ -447,6 +451,9 @@ def serialize_env(env: t.Dict[str, t.Any], path: Path) -> t.Dict[str, Executable serialized = {} for k, (v, is_metadata) in env.items(): + # We don't store `False` for `is_metadata` to reduce the pydantic model's payload size + is_metadata = is_metadata or None + if isinstance(v, LITERALS) or v is None: serialized[k] = Executable.value(v, is_metadata=is_metadata) elif inspect.ismodule(v): diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 0be1702fa1..f8070a98a4 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9383,13 +9383,15 @@ def entrypoint(evaluator): assert "blueprints" not in model.all_fields() python_env = model.python_env - serialized_blueprint = ( - SqlValue(sql=blueprint_value) if model_name == "test_model_sql" else blueprint_value - ) + assert python_env.get(c.SQLMESH_VARS) == Executable.value({"x": gateway_no}) - assert python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value( - {"blueprint": serialized_blueprint} - ) + + if model_name == "test_model_sql": + assert c.SQLMESH_BLUEPRINT_VARS not in python_env + else: + assert python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value( + {"blueprint": blueprint_value} + ) assert context.fetchdf(f"from {model.fqn}").to_dict() == {"x": {0: gateway_no}} @@ -10053,6 +10055,185 @@ def metadata_macro(evaluator): assert new_snapshot.change_category == SnapshotChangeCategory.METADATA +def test_vars_are_taken_into_account_when_propagating_metadata_status(tmp_path: Path) -> None: + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) + + test_model = tmp_path / "models/test_model.sql" + test_model.parent.mkdir(parents=True, exist_ok=True) + test_model.write_text( + "MODEL (name test_model, kind FULL, blueprints ((v4 := 4, v5 := 5)));" + "@m1_metadata_references_v1();" # metadata macro, references v1 internally => v1 metadata + "@m2_metadata_does_not_reference_var(@v2, @v3);" # metadata macro => v2 metadata, v3 metadata + "@m3_non_metadata_references_v4(@v3);" # non-metadata macro, references v4 => v3, v4 are not metadata + "SELECT 1 AS c;" + "@m2_metadata_does_not_reference_var(@v6);" # metadata macro => v6 is metadata + "@m4_non_metadata_references_v6();" # non-metadata macro, references v6 => v6 is not metadata + "ON_VIRTUAL_UPDATE_BEGIN;" + "@m3_non_metadata_references_v4(@v5);" # non-metadata macro, metadata expression => v5 metadata + "ON_VIRTUAL_UPDATE_END;" + ) + + macro_code = """ +from sqlmesh import macro + +@macro(metadata_only=True) +def m1_metadata_references_v1(evaluator): + evaluator.var("v1") + return None + +@macro(metadata_only=True) +def m2_metadata_does_not_reference_var(evaluator, *args): + return None + +@macro() +def m3_non_metadata_references_v4(evaluator, *args): + evaluator.var("v4") + return None + +@macro() +def m4_non_metadata_references_v6(evaluator): + evaluator.var("v6") + return None""" + + test_macros = tmp_path / "macros/test_macros.py" + test_macros.parent.mkdir(parents=True, exist_ok=True) + test_macros.write_text(macro_code) + + ctx = Context( + config=Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + variables={"v1": 1, "v2": 2, "v3": 3, "v6": 6}, + ), + paths=tmp_path, + ) + model = ctx.get_model("test_model") + + python_env = model.python_env + + assert len(python_env) == 8 + assert "m1_metadata_references_v1" in python_env + assert "m2_metadata_does_not_reference_var" in python_env + assert "m3_non_metadata_references_v4" in python_env + assert "m4_non_metadata_references_v6" in python_env + + variables = python_env.get(c.SQLMESH_VARS) + metadata_variables = python_env.get(c.SQLMESH_VARS_METADATA) + + assert variables == Executable.value({"v3": 3, "v6": 6}) + assert metadata_variables == Executable.value({"v1": 1, "v2": 2}, is_metadata=True) + + blueprint_variables = python_env.get(c.SQLMESH_BLUEPRINT_VARS) + blueprint_metadata_variables = python_env.get(c.SQLMESH_BLUEPRINT_VARS_METADATA) + + assert blueprint_variables == Executable.value({"v4": SqlValue(sql="4")}) + assert blueprint_metadata_variables == Executable.value( + {"v5": SqlValue(sql="5")}, is_metadata=True + ) + + macro_evaluator = MacroEvaluator(python_env=python_env) + + assert macro_evaluator.locals == { + "runtime_stage": "loading", + "default_catalog": None, + c.SQLMESH_VARS: {"v3": 3, "v6": 6}, + c.SQLMESH_VARS_METADATA: {"v1": 1, "v2": 2}, + c.SQLMESH_BLUEPRINT_VARS: {"v4": exp.Literal.number("4")}, + c.SQLMESH_BLUEPRINT_VARS_METADATA: {"v5": exp.Literal.number("5")}, + } + assert macro_evaluator.var("v1") == 1 + assert macro_evaluator.var("v2") == 2 + assert macro_evaluator.var("v3") == 3 + assert macro_evaluator.var("v6") == 6 + assert macro_evaluator.blueprint_var("v4") == exp.Literal.number("4") + assert macro_evaluator.blueprint_var("v5") == exp.Literal.number("5") + + query_with_vars = macro_evaluator.transform( + parse_one("SELECT " + ", ".join(f"@v{var}, @VAR('v{var}')" for var in [1, 2, 3, 6])) + ) + assert t.cast(exp.Expression, query_with_vars).sql() == "SELECT 1, 1, 2, 2, 3, 3, 6, 6" + + query_with_blueprint_vars = macro_evaluator.transform( + parse_one("SELECT " + ", ".join(f"@v{var}, @BLUEPRINT_VAR('v{var}')" for var in [4, 5])) + ) + assert t.cast(exp.Expression, query_with_blueprint_vars).sql() == "SELECT 4, 4, 5, 5" + + +def test_variable_mentioned_in_both_metadata_and_non_metadata_macro(tmp_path: Path) -> None: + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) + + test_model = tmp_path / "models/test_model.sql" + test_model.parent.mkdir(parents=True, exist_ok=True) + test_model.write_text( + "MODEL (name test_model, kind FULL); @m1_references_v_metadata(); SELECT @m2_references_v_non_metadata() AS c;" + ) + + macro_code = """ +from sqlmesh import macro + +@macro(metadata_only=True) +def m1_references_v_metadata(evaluator): + evaluator.var("v") + return None + +@macro() +def m2_references_v_non_metadata(evaluator): + evaluator.var("v") + return None""" + + test_macros = tmp_path / "macros/test_macros.py" + test_macros.parent.mkdir(parents=True, exist_ok=True) + test_macros.write_text(macro_code) + + ctx = Context( + config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"), variables={"v": 1}), + paths=tmp_path, + ) + model = ctx.get_model("test_model") + + python_env = model.python_env + + assert len(python_env) == 3 + assert set(python_env) > {"m1_references_v_metadata", "m2_references_v_non_metadata"} + assert python_env.get(c.SQLMESH_VARS) == Executable.value({"v": 1}) + + +def test_only_top_level_macro_func_impacts_var_descendant_metadata_status(tmp_path: Path) -> None: + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) + + test_model = tmp_path / "models/test_model.sql" + test_model.parent.mkdir(parents=True, exist_ok=True) + test_model.write_text( + "MODEL (name test_model, kind FULL); @m1_metadata(@m2_non_metadata(@v)); SELECT 1 AS c;" + ) + + macro_code = """ +from sqlmesh import macro + +@macro(metadata_only=True) +def m1_metadata(evaluator, *args): + return None + +@macro() +def m2_non_metadata(evaluator, *args): + return None""" + + test_macros = tmp_path / "macros/test_macros.py" + test_macros.parent.mkdir(parents=True, exist_ok=True) + test_macros.write_text(macro_code) + + ctx = Context( + config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"), variables={"v": 1}), + paths=tmp_path, + ) + model = ctx.get_model("test_model") + + python_env = model.python_env + + assert len(python_env) == 3 + assert set(python_env) > {"m1_metadata", "m2_non_metadata"} + assert python_env.get(c.SQLMESH_VARS_METADATA) == Executable.value({"v": 1}, is_metadata=True) + + def test_non_metadata_object_takes_precedence_over_metadata_only_object(tmp_path: Path) -> None: init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) @@ -10958,7 +11139,7 @@ def entrypoint( assert customer1_model.enabled assert "blueprints" not in customer1_model.all_fields() assert customer1_model.python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value( - {"customer": "customer1", "field_a": "x", "field_b": "y", "min": 5} + {"customer": "customer1", "field_a": "x", "field_b": "y"} ) # Test second blueprint @@ -10966,7 +11147,7 @@ def entrypoint( assert customer2_model is not None assert customer2_model.cron == "*/10 * * * *" assert customer2_model.python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value( - {"customer": "customer2", "field_a": "z", "field_b": "w", "min": 10} + {"customer": "customer2", "field_a": "z", "field_b": "w"} ) # Test that the models can be planned and applied @@ -11158,3 +11339,22 @@ def test_each_macro_with_paren_expression_arg(assert_exp_eq): 'value' AS "property1" """, ) + + +@pytest.mark.parametrize( + "macro_func, variables", + [ + ("@M(@v1)", {"v1"}), + ("@M(@{v1})", {"v1"}), + ("@M(@SQL('@v1'))", {"v1"}), + ("@M(@'@{v1}_foo')", {"v1"}), + ("@M1(@VAR('v1'))", {"v1"}), + ("@M1(@v1, @M2(@v2), @BLUEPRINT_VAR('v3'))", {"v1", "v2", "v3"}), + ("@M1(@BLUEPRINT_VAR(@VAR('v1')))", {"v1"}), + ], +) +def test_extract_macro_func_variable_references(macro_func: str, variables: t.Set[str]) -> None: + from sqlmesh.core.model.common import _extract_macro_func_variable_references + + macro_func_ast = parse_one(macro_func) + assert _extract_macro_func_variable_references(macro_func_ast, True)[0] == variables diff --git a/tests/utils/test_metaprogramming.py b/tests/utils/test_metaprogramming.py index 8519e1eb04..19413f68ef 100644 --- a/tests/utils/test_metaprogramming.py +++ b/tests/utils/test_metaprogramming.py @@ -406,7 +406,7 @@ def function_with_custom_decorator(): "SQLGLOT_META": Executable.value("sqlglot.meta"), } - assert all(is_metadata is None for (_, is_metadata) in env.values()) + assert all(not is_metadata for (_, is_metadata) in env.values()) assert serialized_env == expected_env # Annotate the entrypoint as "metadata only" to show how it propagates From 4379581dee8cd2fc97f74a5c1256058cf2ffbdef Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 12 Aug 2025 11:14:49 -0700 Subject: [PATCH 0685/1056] Feat!: Dev-only VDE mode (#5087) --- docs/guides/configuration.md | 38 ++ docs/reference/configuration.md | 1 + examples/sushi/config.py | 11 + sqlmesh/core/config/common.py | 29 ++ sqlmesh/core/config/root.py | 21 +- sqlmesh/core/context.py | 11 +- sqlmesh/core/engine_adapter/base.py | 78 ++- sqlmesh/core/engine_adapter/redshift.py | 7 +- sqlmesh/core/engine_adapter/shared.py | 3 + sqlmesh/core/loader.py | 2 + sqlmesh/core/model/decorator.py | 3 + sqlmesh/core/model/definition.py | 2 + sqlmesh/core/model/meta.py | 2 + sqlmesh/core/plan/builder.py | 107 ++-- sqlmesh/core/plan/common.py | 19 + sqlmesh/core/plan/stages.py | 55 +- sqlmesh/core/snapshot/definition.py | 64 ++- sqlmesh/core/snapshot/evaluator.py | 29 +- sqlmesh/core/state_sync/db/snapshot.py | 7 +- sqlmesh/dbt/basemodel.py | 6 +- sqlmesh/dbt/loader.py | 6 +- sqlmesh/dbt/model.py | 7 +- sqlmesh/dbt/seed.py | 7 +- .../v0088_add_virtual_environment_mode.py | 5 + tests/conftest.py | 3 + tests/core/engine_adapter/test_athena.py | 7 + tests/core/engine_adapter/test_base.py | 39 +- tests/core/engine_adapter/test_clickhouse.py | 18 +- tests/core/engine_adapter/test_databricks.py | 11 + tests/core/engine_adapter/test_mssql.py | 22 +- tests/core/engine_adapter/test_redshift.py | 6 + tests/core/engine_adapter/test_spark.py | 37 +- tests/core/state_sync/test_state_sync.py | 41 ++ tests/core/test_integration.py | 237 ++++++++- tests/core/test_model.py | 9 +- tests/core/test_plan.py | 48 -- tests/core/test_plan_stages.py | 483 ++++++++++++++++++ tests/core/test_snapshot.py | 51 +- tests/core/test_snapshot_evaluator.py | 126 +++-- tests/core/test_test.py | 42 -- 40 files changed, 1437 insertions(+), 263 deletions(-) create mode 100644 sqlmesh/core/plan/common.py create mode 100644 sqlmesh/migrations/v0088_add_virtual_environment_mode.py diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 24371f30d0..b137546d84 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -538,6 +538,44 @@ sqlmesh_md5__d3b07384d113edec49eaa6238ad5ff00__dev This has a downside that now it's much more difficult to determine which table corresponds to which model by just looking at the database with a SQL client. However, the table names have a predictable length so there are no longer any surprises with identfiers exceeding the max length at the physical layer. +#### Virtual Data Environment Modes + +By default, Virtual Data Environments (VDE) are applied across both development and production environments. This allows SQLMesh to reuse physical tables when appropriate, even when promoting from development to production. + +However, users may prefer their production environment to be non-virtual. The non-exhaustive list of reasons may include: + +- Integration with third-party tools and platforms, such as data catalogs, may not work well with the virtual view layer that SQLMesh imposes by default +- A desire to rely on time travel features provided by cloud data warehouses such as BigQuery, Snowflake, and Databricks + +To mitigate this, SQLMesh offers an alternative 'dev-only' mode for using VDE. It can be enabled in the project configuration like so: + +=== "YAML" + + ```yaml linenums="1" + virtual_environment_mode: dev_only + ``` + +=== "Python" + + ```python linenums="1" + from sqlmesh.core.config import Config + + config = Config( + virtual_environment_mode="dev_only", + ) + ``` + +'dev-only' mode means that VDE is applied only in development environments. While in production, model tables and views are updated directly and bypass the virtual layer. This also means that physical tables in production will be created using the original, **unversioned** model names. Users will still benefit from VDE and data reuse across development environments. + +Please note the following tradeoffs when enabling this mode: + +- All data inserted in development environments is used only for [preview](../concepts/plans.md#data-preview-for-forward-only-changes) and will **not** be reused in production +- Reverting a model to a previous version will be applied going forward and may require an explicit data restatement + +!!! warning + Switching the mode for an existing project will result in a **complete rebuild** of all models in the project. Refer to the [Table Migration Guide](./table_migration.md) to migrate existing tables without rebuilding them from scratch. + + #### Environment view catalogs By default, SQLMesh creates an environment view in the same [catalog](../concepts/glossary.md#catalog) as the physical table the view points to. The physical table's catalog is determined by either the catalog specified in the model name or the default catalog defined in the connection. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index df3fcf930d..676f9d7389 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -46,6 +46,7 @@ Configuration options for how SQLMesh manages environment creation and promotion | `environment_suffix_target` | Whether SQLMesh views should append their environment name to the `schema`, `table` or `catalog` - [additional details](../guides/configuration.md#view-schema-override). (Default: `schema`) | string | N | | `gateway_managed_virtual_layer` | Whether SQLMesh views of the virtual layer will be created by the default gateway or model specified gateways - [additional details](../guides/multi_engine.md#gateway-managed-virtual-layer). (Default: False) | boolean | N | | `environment_catalog_mapping` | A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment. | dict[string, string] | N | +| `virtual_environment_mode` | Determines the Virtual Data Environment (VDE) mode. If set to `full`, VDE is used in both production and development environments. The `dev_only` option enables VDE only in development environments, while in production, no virtual layer is used and models are materialized directly using their original names (i.e., no versioned physical tables). (Default: `full`) | string | N | ### Models diff --git a/examples/sushi/config.py b/examples/sushi/config.py index 2c124421dd..0bf15d2767 100644 --- a/examples/sushi/config.py +++ b/examples/sushi/config.py @@ -1,5 +1,6 @@ import os +from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.config import ( AutoCategorizationMode, BigQueryConnectionConfig, @@ -76,6 +77,16 @@ model_defaults=model_defaults, ) +# A configuration used for SQLMesh tests with virtual environment mode set to DEV_ONLY. +test_config_virtual_environment_mode_dev_only = test_config.copy( + update={ + "virtual_environment_mode": VirtualEnvironmentMode.DEV_ONLY, + "plan": PlanConfig( + auto_categorize_changes=CategorizerConfig.all_full(), + ), + } +) + # A DuckDB config with a physical schema map. map_config = Config( default_connection=DuckDBConnectionConfig(), diff --git a/sqlmesh/core/config/common.py b/sqlmesh/core/config/common.py index 770c1f5daf..2963632041 100644 --- a/sqlmesh/core/config/common.py +++ b/sqlmesh/core/config/common.py @@ -49,6 +49,35 @@ def __repr__(self) -> str: return str(self) +class VirtualEnvironmentMode(str, Enum): + """Mode for virtual environment behavior. + + FULL: Use full virtual environment functionality with versioned table names and virtual layer updates. + DEV_ONLY: Bypass virtual environments in production, using original unversioned model names. + """ + + FULL = "full" + DEV_ONLY = "dev_only" + + @property + def is_full(self) -> bool: + return self == VirtualEnvironmentMode.FULL + + @property + def is_dev_only(self) -> bool: + return self == VirtualEnvironmentMode.DEV_ONLY + + @classproperty + def default(cls) -> VirtualEnvironmentMode: + return VirtualEnvironmentMode.FULL + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return str(self) + + class TableNamingConvention(str, Enum): # Causes table names at the physical layer to follow the convention: # ____ diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index df8e2637da..ec8fa9988f 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -14,7 +14,11 @@ from sqlmesh.cicd.config import CICDBotConfig from sqlmesh.core import constants as c from sqlmesh.core.console import get_console -from sqlmesh.core.config import EnvironmentSuffixTarget, TableNamingConvention +from sqlmesh.core.config.common import ( + EnvironmentSuffixTarget, + TableNamingConvention, + VirtualEnvironmentMode, +) from sqlmesh.core.config.base import BaseConfig, UpdateStrategy from sqlmesh.core.config.common import variables_validator, compile_regex_mapping from sqlmesh.core.config.connection import ( @@ -110,6 +114,7 @@ class Config(BaseConfig): physical_schema_mapping: A mapping from regular expressions to names of schemas in which physical tables for corresponding models will be placed. environment_suffix_target: Indicates whether to append the environment name to the schema or table name. physical_table_naming_convention: Indicates how tables should be named at the physical layer + virtual_environment_mode: Indicates how environments should be handled. gateway_managed_virtual_layer: Whether the models' views in the virtual layer are created by the model-specific gateway rather than the default gateway. infer_python_dependencies: Whether to statically analyze Python code to automatically infer Python package requirements. environment_catalog_mapping: A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment. @@ -148,12 +153,9 @@ class Config(BaseConfig): env_vars: t.Dict[str, str] = {} username: str = "" physical_schema_mapping: RegexKeyDict = {} - environment_suffix_target: EnvironmentSuffixTarget = Field( - default=EnvironmentSuffixTarget.default - ) - physical_table_naming_convention: TableNamingConvention = Field( - default=TableNamingConvention.default - ) + environment_suffix_target: EnvironmentSuffixTarget = EnvironmentSuffixTarget.default + physical_table_naming_convention: TableNamingConvention = TableNamingConvention.default + virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default gateway_managed_virtual_layer: bool = False infer_python_dependencies: bool = True environment_catalog_mapping: RegexKeyDict = {} @@ -260,6 +262,11 @@ def _normalize_identifiers(key: str) -> None: "Please specify one or the other" ) + if self.plan.use_finalized_state and not self.virtual_environment_mode.is_full: + raise ConfigError( + "Using the finalized state is only supported when `virtual_environment_mode` is set to `full`." + ) + if self.environment_catalog_mapping: _normalize_identifiers("environment_catalog_mapping") if self.physical_schema_mapping: diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index c0d9b21ff8..7d27092f0e 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1616,6 +1616,11 @@ def plan_builder( max_interval_end_per_model, ) + if not self.config.virtual_environment_mode.is_full: + forward_only = True + elif forward_only is None: + forward_only = self.config.plan.forward_only + return self.PLAN_BUILDER_TYPE( context_diff=context_diff, start=start, @@ -1628,9 +1633,7 @@ def plan_builder( skip_backfill=skip_backfill, empty_backfill=empty_backfill, is_dev=is_dev, - forward_only=( - forward_only if forward_only is not None else self.config.plan.forward_only - ), + forward_only=forward_only, allow_destructive_models=expanded_destructive_models, environment_ttl=environment_ttl, environment_suffix_target=self.config.environment_suffix_target, @@ -2936,7 +2939,7 @@ def _node_or_snapshot_to_fqn(self, node_or_snapshot: NodeOrSnapshot) -> str: def _plan_preview_enabled(self) -> bool: if self.config.plan.enable_preview is not None: return self.config.plan.enable_preview - # It is dangerous to enable preview by default for dbt projects that rely on engines that don’t support cloning. + # It is dangerous to enable preview by default for dbt projects that rely on engines that don't support cloning. # Enabling previews in such cases can result in unintended full refreshes because dbt incremental models rely on # the maximum timestamp value in the target table. return self._project_type == c.NATIVE or self.engine_adapter.SUPPORTS_CLONING diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index c615a3029d..d401f0e705 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -32,6 +32,7 @@ CommentCreationTable, CommentCreationView, DataObject, + DataObjectType, EngineRunMode, InsertOverwriteStrategy, SourceQuery, @@ -39,13 +40,13 @@ ) from sqlmesh.core.model.kind import TimeColumn from sqlmesh.core.schema_diff import SchemaDiffer -from sqlmesh.utils import columns_to_types_all_known, random_id, CorrelationId -from sqlmesh.utils.connection_pool import create_connection_pool, ConnectionPool +from sqlmesh.utils import CorrelationId, columns_to_types_all_known, random_id +from sqlmesh.utils.connection_pool import ConnectionPool, create_connection_pool from sqlmesh.utils.date import TimeLike, make_inclusive, to_time_column from sqlmesh.utils.errors import ( + MissingDefaultCatalogError, SQLMeshError, UnsupportedCatalogOperationError, - MissingDefaultCatalogError, ) from sqlmesh.utils.pandas import columns_to_types_from_df @@ -54,8 +55,8 @@ from sqlmesh.core._typing import SchemaName, SessionProperties, TableName from sqlmesh.core.engine_adapter._typing import ( - BigframeSession, DF, + BigframeSession, PySparkDataFrame, PySparkSession, Query, @@ -369,6 +370,12 @@ def replace_query( kwargs: Optional create table properties. """ target_table = exp.to_table(table_name) + + target_data_object = self.get_data_object(target_table) + table_exists = target_data_object is not None + if self.drop_data_object_on_type_mismatch(target_data_object, DataObjectType.TABLE): + table_exists = False + source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( query_or_df, columns_to_types, target_table=target_table ) @@ -390,7 +397,7 @@ def replace_query( ) # All engines support `CREATE TABLE AS` so we use that if the table doesn't already exist and we # use `CREATE OR REPLACE TABLE AS` if the engine supports it - if self.SUPPORTS_REPLACE_TABLE or not self.table_exists(target_table): + if self.SUPPORTS_REPLACE_TABLE or not table_exists: return self._create_table_from_source_queries( target_table, source_queries, @@ -930,6 +937,28 @@ def clone_table( ) ) + def drop_data_object(self, data_object: DataObject, ignore_if_not_exists: bool = True) -> None: + """Drops a data object of arbitrary type. + + Args: + data_object: The data object to drop. + ignore_if_not_exists: If True, no error will be raised if the data object does not exist. + """ + if data_object.type.is_view: + self.drop_view(data_object.to_table(), ignore_if_not_exists=ignore_if_not_exists) + elif data_object.type.is_materialized_view: + self.drop_view( + data_object.to_table(), ignore_if_not_exists=ignore_if_not_exists, materialized=True + ) + elif data_object.type.is_table: + self.drop_table(data_object.to_table(), exists=ignore_if_not_exists) + elif data_object.type.is_managed_table: + self.drop_managed_table(data_object.to_table(), exists=ignore_if_not_exists) + else: + raise SQLMeshError( + f"Can't drop data object '{data_object.to_table().sql(dialect=self.dialect)}' of type '{data_object.type.value}'" + ) + def drop_table(self, table_name: TableName, exists: bool = True) -> None: """Drops a table. @@ -1118,6 +1147,12 @@ def create_view( if properties.expressions: create_kwargs["properties"] = properties + if replace: + self.drop_data_object_on_type_mismatch( + self.get_data_object(view_name), + DataObjectType.VIEW if not materialized else DataObjectType.MATERIALIZED_VIEW, + ) + with source_queries[0] as query: self.execute( exp.Create( @@ -2022,6 +2057,15 @@ def rename_table( ) self._rename_table(old_table_name, new_table_name) + def get_data_object(self, target_name: TableName) -> t.Optional[DataObject]: + target_table = exp.to_table(target_name) + existing_data_objects = self.get_data_objects( + schema_(target_table.db, target_table.catalog), {target_table.name} + ) + if existing_data_objects: + return existing_data_objects[0] + return None + def get_data_objects( self, schema_name: SchemaName, object_names: t.Optional[t.Set[str]] = None ) -> t.List[DataObject]: @@ -2483,6 +2527,30 @@ def _truncate_table(self, table_name: TableName) -> None: table = exp.to_table(table_name) self.execute(f"TRUNCATE TABLE {table.sql(dialect=self.dialect, identify=True)}") + def drop_data_object_on_type_mismatch( + self, data_object: t.Optional[DataObject], expected_type: DataObjectType + ) -> bool: + """Drops a data object if it exists and is not of the expected type. + + Args: + data_object: The data object to check. + expected_type: The expected type of the data object. + + Returns: + True if the data object was dropped, False otherwise. + """ + if data_object is None or data_object.type == expected_type: + return False + + logger.warning( + "Target data object '%s' is a %s and not a %s, dropping it", + data_object.to_table().sql(dialect=self.dialect), + data_object.type.value, + expected_type.value, + ) + self.drop_data_object(data_object) + return True + def _replace_by_key( self, target_table: TableName, diff --git a/sqlmesh/core/engine_adapter/redshift.py b/sqlmesh/core/engine_adapter/redshift.py index 906c52445f..829cdf3686 100644 --- a/sqlmesh/core/engine_adapter/redshift.py +++ b/sqlmesh/core/engine_adapter/redshift.py @@ -262,7 +262,12 @@ def replace_query( """ import pandas as pd - if not isinstance(query_or_df, pd.DataFrame) or not self.table_exists(table_name): + target_data_object = self.get_data_object(table_name) + table_exists = target_data_object is not None + if self.drop_data_object_on_type_mismatch(target_data_object, DataObjectType.TABLE): + table_exists = False + + if not isinstance(query_or_df, pd.DataFrame) or not table_exists: return super().replace_query( table_name, query_or_df, diff --git a/sqlmesh/core/engine_adapter/shared.py b/sqlmesh/core/engine_adapter/shared.py index 1d882de02f..55f04a995e 100644 --- a/sqlmesh/core/engine_adapter/shared.py +++ b/sqlmesh/core/engine_adapter/shared.py @@ -171,6 +171,9 @@ class DataObject(PydanticModel): def is_clustered(self) -> bool: return bool(self.clustering_key) + def to_table(self) -> exp.Table: + return exp.table_(self.name, db=self.schema_name, catalog=self.catalog, quoted=True) + class CatalogSupport(Enum): # The engine has no concept of catalogs diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index b593da1ad0..8126b39107 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -603,6 +603,7 @@ def _load_sql_models( infer_names=self.config.model_naming.infer_names, signal_definitions=signals, default_catalog_per_gateway=self.context.default_catalog_per_gateway, + virtual_environment_mode=self.config.virtual_environment_mode, **loading_default_kwargs or {}, ) @@ -683,6 +684,7 @@ def _load_python_models( audit_definitions=audits, signal_definitions=signals, default_catalog_per_gateway=self.context.default_catalog_per_gateway, + virtual_environment_mode=self.config.virtual_environment_mode, ): if model.enabled: models[model.fqn] = model diff --git a/sqlmesh/core/model/decorator.py b/sqlmesh/core/model/decorator.py index 3b78efc636..73452cc165 100644 --- a/sqlmesh/core/model/decorator.py +++ b/sqlmesh/core/model/decorator.py @@ -8,6 +8,7 @@ from sqlglot import exp from sqlglot.dialects.dialect import DialectType +from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.macros import MacroRegistry from sqlmesh.core.signal import SignalRegistry from sqlmesh.utils.jinja import JinjaMacroRegistry @@ -154,6 +155,7 @@ def model( variables: t.Optional[t.Dict[str, t.Any]] = None, infer_names: t.Optional[bool] = False, blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None, + virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default, ) -> Model: """Get the model registered by this function.""" env: t.Dict[str, t.Tuple[t.Any, t.Optional[bool]]] = {} @@ -228,6 +230,7 @@ def model( "audit_definitions": audit_definitions, "signal_definitions": signal_definitions, "blueprint_variables": blueprint_variables, + "virtual_environment_mode": virtual_environment_mode, **rendered_fields, } diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index f2cfeac163..1d0f9ad66c 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -1062,6 +1062,7 @@ def _data_hash_values(self) -> t.List[str]: self.gateway, self.interval_unit.value if self.interval_unit is not None else None, str(self.optimize_query) if self.optimize_query is not None else None, + self.virtual_environment_mode.value, ] for column_name, column_type in (self.columns_to_types_ or {}).items(): @@ -2957,6 +2958,7 @@ def render_expression( ) ), "formatting": str, + "virtual_environment_mode": lambda value: exp.Literal.string(value.value), } diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index b5371ab811..2f24349a72 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -10,6 +10,7 @@ from sqlglot.optimizer.normalize_identifiers import normalize_identifiers from sqlmesh.core import dialect as d +from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.config.linter import LinterConfig from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core.model.common import ( @@ -83,6 +84,7 @@ class ModelMeta(_Node): default=None, exclude=True, alias="ignored_rules" ) formatting: t.Optional[bool] = Field(default=None, exclude=True) + virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default _bool_validator = bool_validator _model_kind_validator = model_kind_validator diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 178cd8d2e4..3dd74755d3 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -16,6 +16,7 @@ ) from sqlmesh.core.context_diff import ContextDiff from sqlmesh.core.environment import EnvironmentNamingInfo +from sqlmesh.core.plan.common import should_force_rebuild from sqlmesh.core.plan.definition import ( Plan, SnapshotMapping, @@ -162,7 +163,7 @@ def __init__( self._start = start if not self._start and ( - self._forward_only_preview_needed or self._auto_restatement_preview_needed + self._forward_only_preview_needed or self._non_forward_only_preview_needed ): self._start = default_start or yesterday_ds() @@ -267,7 +268,6 @@ def build(self) -> Plan: self._ensure_no_new_snapshots_with_restatements() self._ensure_new_env_with_changes() self._ensure_valid_date_range() - self._ensure_no_forward_only_revert() self._ensure_no_broken_references() self._apply_effective_from() @@ -277,7 +277,7 @@ def build(self) -> Plan: self._check_destructive_changes(directly_modified) self._categorize_snapshots(dag, indirectly_modified) - self._adjust_new_snapshot_intervals() + self._adjust_snapshot_intervals() deployability_index = ( DeployabilityIndex.create( @@ -509,21 +509,22 @@ def _build_models_to_backfill( ).sorted } - def _adjust_new_snapshot_intervals(self) -> None: - old_snapshots = { - (old.name, old.version_get_or_generate()): old - for _, old in self._context_diff.modified_snapshots.values() - } - - for new in self._context_diff.new_snapshots.values(): - new.intervals = [] - new.dev_intervals = [] - old = old_snapshots.get((new.name, new.version_get_or_generate())) - if not old: + def _adjust_snapshot_intervals(self) -> None: + for new, old in self._context_diff.modified_snapshots.values(): + if not new.is_model or not old.is_model: continue - new.merge_intervals(old) - if new.is_forward_only: - new.dev_intervals = new.intervals.copy() + is_same_version = old.version_get_or_generate() == new.version_get_or_generate() + if is_same_version and should_force_rebuild(old, new): + # If the difference between 2 snapshots requires a full rebuild, + # then clear the intervals for the new snapshot. + self._context_diff.snapshots[new.snapshot_id].intervals = [] + elif new.snapshot_id in self._context_diff.new_snapshots: + new.intervals = [] + new.dev_intervals = [] + if is_same_version: + new.merge_intervals(old) + if new.is_forward_only: + new.dev_intervals = new.intervals.copy() def _check_destructive_changes(self, directly_modified: t.Set[SnapshotId]) -> None: for s_id in sorted(directly_modified): @@ -587,7 +588,12 @@ def _categorize_snapshots( if not snapshot or not self._is_new_snapshot(snapshot): continue - forward_only = self._is_forward_only_change(s_id) or self._forward_only + forward_only = self._forward_only or self._is_forward_only_change(s_id) + if forward_only and s_id.name in self._context_diff.modified_snapshots: + new, old = self._context_diff.modified_snapshots[s_id.name] + if should_force_rebuild(old, new) or snapshot.is_seed: + # Breaking kind changes and seed changes can't be forward-only. + forward_only = False if s_id in self._choices: snapshot.categorize_as(self._choices[s_id], forward_only) @@ -608,15 +614,10 @@ def _categorize_snapshot( s_id = snapshot.snapshot_id if self._context_diff.directly_modified(s_id.name): - new, old = self._context_diff.modified_snapshots[s_id.name] - is_breaking_kind_change = _is_breaking_kind_change(old, new) - if is_breaking_kind_change or snapshot.is_seed: - # Breaking kind changes and seed changes can't be forward-only. - forward_only = False - if self._auto_categorization_enabled: - if is_breaking_kind_change: - snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only) + new, old = self._context_diff.modified_snapshots[s_id.name] + if should_force_rebuild(old, new): + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, False) return s_id_with_missing_columns: t.Optional[SnapshotId] = None @@ -773,7 +774,7 @@ def _is_forward_only_change(self, s_id: SnapshotId) -> bool: if snapshot.name in self._context_diff.modified_snapshots: _, old = self._context_diff.modified_snapshots[snapshot.name] # If the model kind has changed in a breaking way, then we can't consider this to be a forward-only change. - if snapshot.is_model and _is_breaking_kind_change(old, snapshot): + if snapshot.is_model and should_force_rebuild(old, snapshot): return False return ( snapshot.is_model and snapshot.model.forward_only and bool(snapshot.previous_versions) @@ -801,27 +802,6 @@ def _ensure_valid_date_range(self) -> None: f"Plan end date: '{time_like_to_str(end)}' cannot be in the future (execution time: '{time_like_to_str(self.execution_time)}')" ) - def _ensure_no_forward_only_revert(self) -> None: - """Ensures that a previously superseded breaking / non-breaking snapshot is not being - used again to replace an existing forward-only snapshot with the same version. - - In other words there is no going back to the original non-forward-only snapshot with - the same version once a forward-only change for that version has been introduced. - """ - for name, (candidate, promoted) in self._context_diff.modified_snapshots.items(): - if ( - candidate.snapshot_id not in self._context_diff.new_snapshots - and candidate.is_model - and not candidate.model.forward_only - and promoted.is_forward_only - and not promoted.is_paused - and not candidate.is_no_rebuild - and promoted.version == candidate.version - ): - raise PlanError( - f"Attempted to revert to an unrevertable version of model '{name}'. Run `sqlmesh plan` again to mitigate the issue." - ) - def _ensure_no_broken_references(self) -> None: for snapshot in self._context_diff.snapshots.values(): broken_references = { @@ -871,12 +851,18 @@ def _forward_only_preview_needed(self) -> bool: ) @cached_property - def _auto_restatement_preview_needed(self) -> bool: - return self._is_dev and any( - snapshot.model.auto_restatement_cron is not None - for snapshot in self._modified_and_added_snapshots - if snapshot.is_model - ) + def _non_forward_only_preview_needed(self) -> bool: + if not self._is_dev: + return False + for snapshot in self._modified_and_added_snapshots: + if not snapshot.is_model: + continue + if ( + not snapshot.virtual_environment_mode.is_full + or snapshot.model.auto_restatement_cron is not None + ): + return True + return False @cached_property def _modified_and_added_snapshots(self) -> t.List[Snapshot]: @@ -886,16 +872,3 @@ def _modified_and_added_snapshots(self) -> t.List[Snapshot]: if snapshot.name in self._context_diff.modified_snapshots or snapshot.snapshot_id in self._context_diff.added ] - - -def _is_breaking_kind_change(old: Snapshot, new: Snapshot) -> bool: - if old.model.kind.name == new.model.kind.name: - # If the kind hasn't changed, then it's not a breaking change - return False - if not old.is_incremental or not new.is_incremental: - # If either is not incremental, then it's a breaking change - return True - if old.model.partitioned_by == new.model.partitioned_by: - # If the partitioning hasn't changed, then it's not a breaking change - return False - return True diff --git a/sqlmesh/core/plan/common.py b/sqlmesh/core/plan/common.py new file mode 100644 index 0000000000..e6b7a4d10c --- /dev/null +++ b/sqlmesh/core/plan/common.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from sqlmesh.core.snapshot import Snapshot + + +def should_force_rebuild(old: Snapshot, new: Snapshot) -> bool: + if old.virtual_environment_mode != new.virtual_environment_mode: + # If the virtual environment mode has changed, then we need to rebuild + return True + if old.model.kind.name == new.model.kind.name: + # If the kind hasn't changed, then we don't need to rebuild + return False + if not old.is_incremental or not new.is_incremental: + # If either is not incremental, then we need to rebuild + return True + if old.model.partitioned_by == new.model.partitioned_by: + # If the partitioning hasn't changed, then we don't need to rebuild + return False + return True diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 144e12c887..7ef9fcb7ef 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -1,7 +1,8 @@ import typing as t from dataclasses import dataclass -from sqlmesh.core.environment import EnvironmentStatements, EnvironmentNamingInfo +from sqlmesh.core.environment import EnvironmentStatements, EnvironmentNamingInfo, Environment +from sqlmesh.core.plan.common import should_force_rebuild from sqlmesh.core.plan.definition import EvaluatablePlan from sqlmesh.core.state_sync import StateReader from sqlmesh.core.scheduler import merged_missing_intervals, SnapshotToIntervals @@ -230,8 +231,9 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: all_selected_for_backfill_snapshots = { s.snapshot_id for s in snapshots.values() if plan.is_selected_for_backfill(s.name) } + existing_environment = self.state_reader.get_environment(plan.environment.name) - self._adjust_intervals(snapshots_by_name, plan) + self._adjust_intervals(snapshots_by_name, plan, existing_environment) deployability_index = DeployabilityIndex.create(snapshots, start=plan.start) deployability_index_for_creation = deployability_index @@ -269,7 +271,7 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: missing_intervals_after_promote[snapshot] = intervals promoted_snapshots, demoted_snapshots, demoted_environment_naming_info = ( - self._get_promoted_demoted_snapshots(plan) + self._get_promoted_demoted_snapshots(plan, existing_environment) ) stages: t.List[PlanStage] = [] @@ -358,6 +360,7 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: demoted_environment_naming_info, snapshots | full_demoted_snapshots, deployability_index, + plan.is_dev, ) if virtual_layer_update_stage: stages.append(virtual_layer_update_stage) @@ -437,11 +440,18 @@ def _get_virtual_layer_update_stage( demoted_environment_naming_info: t.Optional[EnvironmentNamingInfo], all_snapshots: t.Dict[SnapshotId, Snapshot], deployability_index: DeployabilityIndex, + is_dev: bool, ) -> t.Optional[VirtualLayerUpdateStage]: - promoted_snapshots = {s for s in promoted_snapshots if s.is_model and not s.is_symbolic} - demoted_snapshots = {s for s in demoted_snapshots if s.is_model and not s.is_symbolic} + def _should_update_virtual_layer(snapshot: SnapshotTableInfo) -> bool: + # Skip virtual layer update for snapshots with virtual environment support disabled + virtual_environment_enabled = is_dev or snapshot.virtual_environment_mode.is_full + return snapshot.is_model and not snapshot.is_symbolic and virtual_environment_enabled + + promoted_snapshots = {s for s in promoted_snapshots if _should_update_virtual_layer(s)} + demoted_snapshots = {s for s in demoted_snapshots if _should_update_virtual_layer(s)} if not promoted_snapshots and not demoted_snapshots: return None + return VirtualLayerUpdateStage( promoted_snapshots=promoted_snapshots, demoted_snapshots=demoted_snapshots, @@ -451,11 +461,10 @@ def _get_virtual_layer_update_stage( ) def _get_promoted_demoted_snapshots( - self, plan: EvaluatablePlan + self, plan: EvaluatablePlan, existing_environment: t.Optional[Environment] ) -> t.Tuple[ t.Set[SnapshotTableInfo], t.Set[SnapshotTableInfo], t.Optional[EnvironmentNamingInfo] ]: - existing_environment = self.state_reader.get_environment(plan.environment.name) if existing_environment: new_table_infos = { table_info.name: table_info for table_info in plan.environment.promoted_snapshots @@ -571,10 +580,40 @@ def _should_create(s: Snapshot) -> bool: return [s for s in snapshots.values() if _should_create(s)] def _adjust_intervals( - self, snapshots_by_name: t.Dict[str, Snapshot], plan: EvaluatablePlan + self, + snapshots_by_name: t.Dict[str, Snapshot], + plan: EvaluatablePlan, + existing_environment: t.Optional[Environment], ) -> None: # Make sure the intervals are up to date and restatements are reflected self.state_reader.refresh_snapshot_intervals(snapshots_by_name.values()) + + if existing_environment: + new_snapshot_ids = set() + new_snapshot_versions = set() + for s in snapshots_by_name.values(): + if s.is_model: + new_snapshot_ids.add(s.snapshot_id) + new_snapshot_versions.add(s.name_version) + # Only compare to old snapshots that share the same version as the new snapshots + old_snapshot_ids = { + s.snapshot_id + for s in existing_environment.snapshots + if s.is_model + and s.name_version in new_snapshot_versions + and s.snapshot_id not in new_snapshot_ids + } + if old_snapshot_ids: + old_snapshots = self.state_reader.get_snapshots(old_snapshot_ids) + for old in old_snapshots.values(): + new = snapshots_by_name.get(old.name) + if not new or old.version != new.version: + continue + if should_force_rebuild(old, new): + # If the difference between 2 snapshots requires a full rebuild, + # then clear the intervals for the new snapshot. + new.intervals = [] + for new_snapshot in plan.new_snapshots: if new_snapshot.is_forward_only: # Forward-only snapshots inherit intervals in dev because of cloning diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 90cd963051..076c1efa78 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -13,7 +13,7 @@ from sqlglot import exp from sqlglot.optimizer.normalize_identifiers import normalize_identifiers -from sqlmesh.core.config import TableNamingConvention +from sqlmesh.core.config.common import TableNamingConvention, VirtualEnvironmentMode from sqlmesh.core import constants as c from sqlmesh.core.audit import StandaloneAudit from sqlmesh.core.environment import EnvironmentSuffixTarget @@ -230,6 +230,7 @@ class SnapshotDataVersion(PydanticModel, frozen=True): physical_schema_: t.Optional[str] = Field(default=None, alias="physical_schema") dev_table_suffix: str table_naming_convention: TableNamingConvention = Field(default=TableNamingConvention.default) + virtual_environment_mode: VirtualEnvironmentMode = Field(default=VirtualEnvironmentMode.default) def snapshot_id(self, name: str) -> SnapshotId: return SnapshotId(name=name, identifier=self.fingerprint.to_identifier()) @@ -336,7 +337,7 @@ class SnapshotInfoMixin(ModelKindMixin): # This can be removed from this model once Pydantic 1 support is dropped (must remain in `Snapshot` though) base_table_name_override: t.Optional[str] dev_table_suffix: str - table_naming_convention: TableNamingConvention = Field(default=TableNamingConvention.default) + table_naming_convention: TableNamingConvention forward_only: bool @cached_property @@ -383,6 +384,10 @@ def is_new_version(self) -> bool: def fully_qualified_table(self) -> t.Optional[exp.Table]: raise NotImplementedError + @property + def virtual_environment_mode(self) -> VirtualEnvironmentMode: + raise NotImplementedError + @property def is_forward_only(self) -> bool: return self.forward_only or self.change_category == SnapshotChangeCategory.FORWARD_ONLY @@ -443,6 +448,10 @@ def _table_name(self, version: str, is_deployable: bool) -> str: if self.is_external: return self.name + if is_deployable and self.virtual_environment_mode.is_dev_only: + # Use the model name as is if the target is deployable and the virtual environment mode is set to dev-only + return self.name + is_dev_table = not is_deployable if is_dev_table: version = self.dev_version @@ -459,6 +468,7 @@ def _table_name(self, version: str, is_deployable: bool) -> str: fqt = self.fully_qualified_table.copy() fqt.set("catalog", None) base_table_name = fqt.sql() + return table_name( self.physical_schema, base_table_name, @@ -499,6 +509,10 @@ class SnapshotTableInfo(PydanticModel, SnapshotInfoMixin, frozen=True): dev_table_suffix: str model_gateway: t.Optional[str] = None forward_only: bool = False + table_naming_convention: TableNamingConvention = TableNamingConvention.default + virtual_environment_mode_: VirtualEnvironmentMode = Field( + default=VirtualEnvironmentMode.default, alias="virtual_environment_mode" + ) def __lt__(self, other: SnapshotTableInfo) -> bool: return self.name < other.name @@ -530,6 +544,10 @@ def table_info(self) -> SnapshotTableInfo: """Helper method to return self.""" return self + @property + def virtual_environment_mode(self) -> VirtualEnvironmentMode: + return self.virtual_environment_mode_ + @property def data_version(self) -> SnapshotDataVersion: return SnapshotDataVersion( @@ -540,6 +558,7 @@ def data_version(self) -> SnapshotDataVersion: physical_schema=self.physical_schema, dev_table_suffix=self.dev_table_suffix, table_naming_convention=self.table_naming_convention, + virtual_environment_mode=self.virtual_environment_mode, ) @property @@ -623,9 +642,7 @@ class Snapshot(PydanticModel, SnapshotInfoMixin): base_table_name_override: t.Optional[str] = None next_auto_restatement_ts: t.Optional[int] = None dev_table_suffix: str = "dev" - table_naming_convention_: TableNamingConvention = Field( - default=TableNamingConvention.default, alias="table_naming_convention" - ) + table_naming_convention: TableNamingConvention = TableNamingConvention.default forward_only: bool = False @field_validator("ttl") @@ -878,10 +895,9 @@ def merge_intervals(self, other: t.Union[Snapshot, SnapshotIntervals]) -> None: """ effective_from_ts = self.normalized_effective_from_ts or 0 apply_effective_from = effective_from_ts > 0 and self.identifier != other.identifier - for start, end in other.intervals: # If the effective_from is set, then intervals that come after it must come from - # the current snapshost. + # the current snapshots. if apply_effective_from and start < effective_from_ts: end = min(end, effective_from_ts) if not apply_effective_from or end <= effective_from_ts: @@ -1035,7 +1051,10 @@ def categorize_as(self, category: SnapshotChangeCategory, forward_only: bool = F SnapshotChangeCategory.INDIRECT_NON_BREAKING, SnapshotChangeCategory.METADATA, ) - if self.is_model and self.model.physical_version: + if self.is_model and not self.virtual_environment_mode.is_full: + # Hardcode the version if the virtual environment is not fully enabled. + self.version = "novde" + elif self.is_model and self.model.physical_version: # If the model has a pinned version then use that. self.version = self.model.physical_version elif is_no_rebuild and self.previous_version: @@ -1239,6 +1258,7 @@ def table_info(self) -> SnapshotTableInfo: model_gateway=self.model_gateway, table_naming_convention=self.table_naming_convention, # type: ignore forward_only=self.forward_only, + virtual_environment_mode=self.virtual_environment_mode, ) @property @@ -1252,6 +1272,7 @@ def data_version(self) -> SnapshotDataVersion: physical_schema=self.physical_schema, dev_table_suffix=self.dev_table_suffix, table_naming_convention=self.table_naming_convention, + virtual_environment_mode=self.virtual_environment_mode, ) @property @@ -1383,6 +1404,7 @@ def requires_schema_migration_in_prod(self) -> bool: or self.model.forward_only or bool(self.model.physical_version) or self.is_view + or not self.virtual_environment_mode.is_full ) ) @@ -1396,6 +1418,12 @@ def custom_materialization(self) -> t.Optional[str]: return t.cast(CustomKind, self.model.kind).materialization return None + @property + def virtual_environment_mode(self) -> VirtualEnvironmentMode: + return ( + self.model.virtual_environment_mode if self.is_model else VirtualEnvironmentMode.default + ) + def _ensure_categorized(self) -> None: if not self.change_category: raise SQLMeshError(f"Snapshot {self.snapshot_id} has not been categorized yet.") @@ -1535,14 +1563,20 @@ def create( for node in dag: if node not in snapshots: continue - # Make sure that the node is deployable according to all its parents - this_deployable = all( - children_deployability_mapping[p_id] - for p_id in snapshots[node].parents - if p_id in children_deployability_mapping - ) + snapshot = snapshots[node] + + if not snapshot.virtual_environment_mode.is_full: + # If the virtual environment is not fully enabled, then the snapshot can never be deployable + this_deployable = False + else: + # Make sure that the node is deployable according to all its parents + this_deployable = all( + children_deployability_mapping[p_id] + for p_id in snapshots[node].parents + if p_id in children_deployability_mapping + ) + if this_deployable: - snapshot = snapshots[node] is_forward_only_model = ( snapshot.is_model and snapshot.model.forward_only and not snapshot.is_metadata ) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index e053e1e108..a2ec242e37 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -38,7 +38,7 @@ from sqlmesh.core.audit import Audit, StandaloneAudit from sqlmesh.core.dialect import schema_ from sqlmesh.core.engine_adapter import EngineAdapter -from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy +from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, DataObjectType from sqlmesh.core.macros import RuntimeStage from sqlmesh.core.model import ( AuditResult, @@ -934,7 +934,14 @@ def _migrate_snapshot( adapter.transaction(), adapter.session(snapshot.model.render_session_properties(**render_kwargs)), ): - if adapter.table_exists(target_table_name): + target_data_object = adapter.get_data_object(target_table_name) + table_exists = target_data_object is not None + if adapter.drop_data_object_on_type_mismatch( + target_data_object, _snapshot_to_data_object_type(snapshot) + ): + table_exists = False + + if table_exists: evaluation_strategy = _evaluation_strategy(snapshot, adapter) tmp_table_name = snapshot.table_name(is_deployable=False) logger.info( @@ -2274,8 +2281,10 @@ def _check_destructive_schema_change( alter_expressions: t.List[exp.Alter], allow_destructive_snapshots: t.Set[str], ) -> None: - if snapshot.needs_destructive_check(allow_destructive_snapshots) and has_drop_alteration( - alter_expressions + if ( + snapshot.is_no_rebuild + and snapshot.needs_destructive_check(allow_destructive_snapshots) + and has_drop_alteration(alter_expressions) ): snapshot_name = snapshot.name dropped_column_names = get_dropped_column_names(alter_expressions) @@ -2305,3 +2314,15 @@ def _check_table_db_is_physical_schema(table_name: str, physical_schema: str) -> raise SQLMeshError( f"Table '{table_name}' is not a part of the physical schema '{physical_schema}' and so can't be dropped." ) + + +def _snapshot_to_data_object_type(snapshot: Snapshot) -> DataObjectType: + if snapshot.is_managed: + return DataObjectType.MANAGED_TABLE + if snapshot.is_materialized_view: + return DataObjectType.MATERIALIZED_VIEW + if snapshot.is_view: + return DataObjectType.VIEW + if snapshot.is_materialized: + return DataObjectType.TABLE + return DataObjectType.UNKNOWN diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 3be4fb1b45..6064993087 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -173,10 +173,9 @@ def unpause_snapshots( snapshot.set_unpaused_ts(None) paused_snapshots.append(snapshot.snapshot_id) - if ( - not snapshot.is_forward_only - and target_snapshot.is_forward_only - and not snapshot.unrestorable + if not snapshot.unrestorable and ( + (target_snapshot.is_forward_only and not snapshot.is_forward_only) + or (snapshot.is_forward_only and not target_snapshot.is_forward_only) ): logger.info("Marking snapshot %s as unrestorable", snapshot.snapshot_id) snapshot.unrestorable = True diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index 74b90b8441..d226325dbc 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -10,6 +10,7 @@ from sqlmesh.core import dialect as d from sqlmesh.core.config.base import UpdateStrategy +from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.model import Model from sqlmesh.dbt.column import ( ColumnConfig, @@ -345,6 +346,9 @@ def sqlmesh_model_kwargs( @abstractmethod def to_sqlmesh( - self, context: DbtContext, audit_definitions: t.Optional[t.Dict[str, ModelAudit]] = None + self, + context: DbtContext, + audit_definitions: t.Optional[t.Dict[str, ModelAudit]] = None, + virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default, ) -> Model: """Convert DBT model into sqlmesh Model""" diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 23d34afa31..3d63219004 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -117,7 +117,11 @@ def _load_models( def _to_sqlmesh(config: BMC, context: DbtContext) -> Model: logger.debug("Converting '%s' to sqlmesh format", config.canonical_name(context)) - return config.to_sqlmesh(context, audit_definitions=audits) + return config.to_sqlmesh( + context, + audit_definitions=audits, + virtual_environment_mode=self.config.virtual_environment_mode, + ) for project in self._load_projects(): context = project.context.copy() diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 4cbca09aee..8563d20d22 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -8,6 +8,7 @@ from sqlmesh.core import dialect as d from sqlmesh.core.config.base import UpdateStrategy +from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.console import get_console from sqlmesh.core.model import ( EmbeddedKind, @@ -421,7 +422,10 @@ def sqlmesh_config_fields(self) -> t.Set[str]: } def to_sqlmesh( - self, context: DbtContext, audit_definitions: t.Optional[t.Dict[str, ModelAudit]] = None + self, + context: DbtContext, + audit_definitions: t.Optional[t.Dict[str, ModelAudit]] = None, + virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default, ) -> Model: """Converts the dbt model into a SQLMesh model.""" model_dialect = self.dialect(context) @@ -573,6 +577,7 @@ def to_sqlmesh( # Note: any table dependencies that are not referenced using the `ref` macro will not be included. extract_dependencies_from_query=False, allow_partials=allow_partials, + virtual_environment_mode=virtual_environment_mode, **optional_kwargs, **model_kwargs, ) diff --git a/sqlmesh/dbt/seed.py b/sqlmesh/dbt/seed.py index 78f24255dc..fde5c7e569 100644 --- a/sqlmesh/dbt/seed.py +++ b/sqlmesh/dbt/seed.py @@ -15,6 +15,7 @@ SUPPORTS_DELIMITER = False from sqlglot import exp +from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.model import Model, SeedKind, create_seed_model from sqlmesh.dbt.basemodel import BaseModelConfig from sqlmesh.dbt.column import ColumnConfig @@ -38,7 +39,10 @@ class SeedConfig(BaseModelConfig): quote_columns: t.Optional[bool] = False def to_sqlmesh( - self, context: DbtContext, audit_definitions: t.Optional[t.Dict[str, ModelAudit]] = None + self, + context: DbtContext, + audit_definitions: t.Optional[t.Dict[str, ModelAudit]] = None, + virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default, ) -> Model: """Converts the dbt seed into a SQLMesh model.""" seed_path = self.path.absolute().as_posix() @@ -83,6 +87,7 @@ def to_sqlmesh( SeedKind(path=seed_path), dialect=self.dialect(context), audit_definitions=audit_definitions, + virtual_environment_mode=virtual_environment_mode, **kwargs, ) diff --git a/sqlmesh/migrations/v0088_add_virtual_environment_mode.py b/sqlmesh/migrations/v0088_add_virtual_environment_mode.py new file mode 100644 index 0000000000..024ff03a0e --- /dev/null +++ b/sqlmesh/migrations/v0088_add_virtual_environment_mode.py @@ -0,0 +1,5 @@ +"""Add virtual_environment_mode to the model definition.""" + + +def migrate(state_sync, **kwargs): # type: ignore + pass diff --git a/tests/conftest.py b/tests/conftest.py index 1bfa7a9f36..ad09deff6f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -470,6 +470,7 @@ def _make_function( dialect: t.Optional[str] = None, register_comments: bool = True, default_catalog: t.Optional[str] = None, + patch_get_data_objects: bool = True, **kwargs: t.Any, ) -> T: connection_mock = mocker.NonCallableMock() @@ -493,6 +494,8 @@ def _make_function( "sqlmesh.core.engine_adapter.mssql.MSSQLEngineAdapter.catalog_support", new_callable=PropertyMock(return_value=CatalogSupport.REQUIRES_SET_CATALOG), ) + if patch_get_data_objects: + mocker.patch.object(adapter, "_get_data_objects", return_value=[]) return adapter return _make_function diff --git a/tests/core/engine_adapter/test_athena.py b/tests/core/engine_adapter/test_athena.py index 6a5f30998b..5ee07f52d5 100644 --- a/tests/core/engine_adapter/test_athena.py +++ b/tests/core/engine_adapter/test_athena.py @@ -7,6 +7,7 @@ from sqlglot import exp, parse_one import sqlmesh.core.dialect as d from sqlmesh.core.engine_adapter import AthenaEngineAdapter +from sqlmesh.core.engine_adapter.shared import DataObject from sqlmesh.core.model import load_sql_based_model from sqlmesh.core.model.definition import SqlModel from sqlmesh.utils.errors import SQLMeshError @@ -288,6 +289,11 @@ def test_replace_query(adapter: AthenaEngineAdapter, mocker: MockerFixture): "sqlmesh.core.engine_adapter.athena.AthenaEngineAdapter._query_table_type", return_value="iceberg", ) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test", type="table")], + ) adapter.replace_query( table_name="test", @@ -304,6 +310,7 @@ def test_replace_query(adapter: AthenaEngineAdapter, mocker: MockerFixture): mocker.patch( "sqlmesh.core.engine_adapter.athena.AthenaEngineAdapter.table_exists", return_value=False ) + mocker.patch.object(adapter, "_get_data_objects", return_value=[]) adapter.cursor.execute.reset_mock() adapter.s3_warehouse_location = "s3://foo" diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index 6c9d2ee132..b760a4f4a1 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -14,7 +14,7 @@ from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core.engine_adapter import EngineAdapter, EngineAdapterWithIndexSupport from sqlmesh.core.engine_adapter.mixins import InsertOverwriteWithMergeMixin -from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy +from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, DataObject from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation from sqlmesh.utils import columns_to_types_to_struct from sqlmesh.utils.date import to_ds @@ -43,6 +43,23 @@ def test_create_view(make_mocked_engine_adapter: t.Callable): ] +def test_create_view_existing_data_object_type_mismatch( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test_view", type="table")], + ) + adapter.create_view("test_view", parse_one("SELECT a FROM tbl")) + + assert to_sql_calls(adapter) == [ + 'DROP TABLE IF EXISTS "test_view"', + 'CREATE OR REPLACE VIEW "test_view" AS SELECT "a" FROM "tbl"', + ] + + def test_create_view_pandas(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) adapter.create_view("test_view", pd.DataFrame({"a": [1, 2, 3]}), replace=False) @@ -2713,6 +2730,26 @@ def test_replace_query(make_mocked_engine_adapter: t.Callable, mocker: MockerFix ] +def test_replace_query_data_object_type_mismatch( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test_table", type="view")], + ) + + adapter.replace_query( + "test_table", parse_one("SELECT a FROM tbl"), {"a": exp.DataType.build("INT")} + ) + + assert to_sql_calls(adapter) == [ + 'DROP VIEW IF EXISTS "test_table"', + 'CREATE OR REPLACE TABLE "test_table" AS SELECT CAST("a" AS INT) AS "a" FROM (SELECT "a" FROM "tbl") AS "_subquery"', + ] + + def test_replace_query_pandas(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) adapter.DEFAULT_BATCH_SIZE = 1 diff --git a/tests/core/engine_adapter/test_clickhouse.py b/tests/core/engine_adapter/test_clickhouse.py index 1665239e36..a0cd33af70 100644 --- a/tests/core/engine_adapter/test_clickhouse.py +++ b/tests/core/engine_adapter/test_clickhouse.py @@ -2,7 +2,7 @@ from sqlmesh.core.engine_adapter import ClickhouseEngineAdapter from sqlmesh.core.model.definition import load_sql_based_model from sqlmesh.core.model.kind import ModelKindName -from sqlmesh.core.engine_adapter.shared import EngineRunMode +from sqlmesh.core.engine_adapter.shared import EngineRunMode, DataObject from tests.core.engine_adapter import to_sql_calls from sqlmesh.core.dialect import parse from sqlglot import exp, parse_one @@ -573,6 +573,12 @@ def test_scd_type_2_by_time( make_temp_table_name(table_name, "abcd"), ] + mocker.patch.object( + adapter, + "get_data_objects", + return_value=[DataObject(schema="", name=table_name, type="table")], + ) + fetchone_mock = mocker.patch("sqlmesh.core.engine_adapter.ClickhouseEngineAdapter.fetchone") fetchone_mock.return_value = None @@ -610,7 +616,7 @@ def test_scd_type_2_by_time( truncate=True, ) - assert to_sql_calls(adapter)[4] == parse_one( + assert to_sql_calls(adapter)[3] == parse_one( """ INSERT INTO "__temp_target_abcd" ("id", "name", "price", "test_UPDATED_at", "test_valid_from", "test_valid_to") WITH "source" AS ( @@ -787,6 +793,12 @@ def test_scd_type_2_by_column( make_temp_table_name(table_name, "abcd"), ] + mocker.patch.object( + adapter, + "get_data_objects", + return_value=[DataObject(schema="", name=table_name, type="table")], + ) + fetchone_mock = mocker.patch("sqlmesh.core.engine_adapter.ClickhouseEngineAdapter.fetchone") fetchone_mock.return_value = None @@ -817,7 +829,7 @@ def test_scd_type_2_by_column( truncate=True, ) - assert to_sql_calls(adapter)[4] == parse_one( + assert to_sql_calls(adapter)[3] == parse_one( """ INSERT INTO "__temp_target_abcd" ("id", "name", "price", "test_VALID_from", "test_valid_to") WITH "source" AS ( diff --git a/tests/core/engine_adapter/test_databricks.py b/tests/core/engine_adapter/test_databricks.py index 25698875a5..5991f5b2b9 100644 --- a/tests/core/engine_adapter/test_databricks.py +++ b/tests/core/engine_adapter/test_databricks.py @@ -8,6 +8,7 @@ from sqlmesh.core import dialect as d from sqlmesh.core.engine_adapter import DatabricksEngineAdapter +from sqlmesh.core.engine_adapter.shared import DataObject from sqlmesh.core.node import IntervalUnit from tests.core.engine_adapter import to_sql_calls @@ -41,6 +42,11 @@ def test_replace_query_exists(mocker: MockFixture, make_mocked_engine_adapter: t "sqlmesh.core.engine_adapter.databricks.DatabricksEngineAdapter.set_current_catalog" ) adapter = make_mocked_engine_adapter(DatabricksEngineAdapter, default_catalog="test_catalog") + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test_table", type="table")], + ) adapter.replace_query("test_table", parse_one("SELECT a FROM tbl"), {"a": "int"}) assert to_sql_calls(adapter) == [ @@ -78,6 +84,11 @@ def test_replace_query_pandas_exists(mocker: MockFixture, make_mocked_engine_ada "sqlmesh.core.engine_adapter.databricks.DatabricksEngineAdapter.set_current_catalog" ) adapter = make_mocked_engine_adapter(DatabricksEngineAdapter, default_catalog="test_catalog") + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test_table", type="table")], + ) df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) adapter.replace_query( "test_table", df, {"a": exp.DataType.build("int"), "b": exp.DataType.build("int")} diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index 65f3231163..d8e5214be5 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -582,13 +582,16 @@ def test_merge_exists( ] -def test_replace_query(make_mocked_engine_adapter: t.Callable): +def test_replace_query(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): adapter = make_mocked_engine_adapter(MSSQLEngineAdapter) - adapter.cursor.fetchone.return_value = (1,) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test_table", type="table")], + ) adapter.replace_query("test_table", parse_one("SELECT a FROM tbl"), {"a": "int"}) assert to_sql_calls(adapter) == [ - """SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE [TABLE_NAME] = 'test_table';""", "TRUNCATE TABLE [test_table];", "INSERT INTO [test_table] ([a]) SELECT [a] FROM [tbl];", ] @@ -605,6 +608,11 @@ def test_replace_query_pandas( ) adapter = make_mocked_engine_adapter(MSSQLEngineAdapter) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test_table", type="table")], + ) adapter.cursor.fetchone.return_value = (1,) temp_table_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter._get_temp_table") @@ -682,7 +690,7 @@ def test_drop_schema_with_catalog(make_mocked_engine_adapter: t.Callable, mocker def test_get_data_objects_catalog(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): - adapter = make_mocked_engine_adapter(MSSQLEngineAdapter) + adapter = make_mocked_engine_adapter(MSSQLEngineAdapter, patch_get_data_objects=False) original_set_current_catalog = adapter.set_current_catalog local_state = {} @@ -912,6 +920,12 @@ def test_replace_query_strategy(adapter: MSSQLEngineAdapter, mocker: MockerFixtu exists_mock.return_value = True assert adapter.table_exists("test_table") + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test_table", type="table")], + ) + adapter.replace_query( "test_table", model.render_query_or_raise(), diff --git a/tests/core/engine_adapter/test_redshift.py b/tests/core/engine_adapter/test_redshift.py index ef1e204ce5..0db8e8d055 100644 --- a/tests/core/engine_adapter/test_redshift.py +++ b/tests/core/engine_adapter/test_redshift.py @@ -9,6 +9,7 @@ from sqlglot import parse_one from sqlmesh.core.engine_adapter import RedshiftEngineAdapter +from sqlmesh.core.engine_adapter.shared import DataObject from sqlmesh.utils.errors import SQLMeshError from tests.core.engine_adapter import to_sql_calls @@ -262,6 +263,11 @@ def mock_table(*args, **kwargs): mock_temp_table = mocker.MagicMock(side_effect=mock_table) mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter._get_temp_table", mock_temp_table) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test_table", type="table")], + ) adapter.replace_query( table_name="test_table", diff --git a/tests/core/engine_adapter/test_spark.py b/tests/core/engine_adapter/test_spark.py index 8a455c47a3..2ef70a6929 100644 --- a/tests/core/engine_adapter/test_spark.py +++ b/tests/core/engine_adapter/test_spark.py @@ -10,6 +10,7 @@ from sqlglot import parse_one from sqlmesh.core.engine_adapter import SparkEngineAdapter +from sqlmesh.core.engine_adapter.shared import DataObject from sqlmesh.utils.errors import SQLMeshError from tests.core.engine_adapter import to_sql_calls import sqlmesh.core.dialect as d @@ -102,6 +103,11 @@ def test_replace_query_table_properties_exists( return_value=True, ) adapter = make_mocked_engine_adapter(SparkEngineAdapter) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test_table", type="table")], + ) columns_to_types = { "cola": exp.DataType.build("INT"), @@ -194,6 +200,11 @@ def test_replace_query_exists(mocker: MockerFixture, make_mocked_engine_adapter: return_value=True, ) adapter = make_mocked_engine_adapter(SparkEngineAdapter) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test_table", type="table")], + ) adapter.replace_query("test_table", parse_one("SELECT a FROM tbl"), {"a": "int"}) assert to_sql_calls(adapter) == [ @@ -239,6 +250,12 @@ def check_table_exists(table_name: exp.Table) -> bool: side_effect=check_table_exists, ) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="db", name="table", type="table")], + ) + adapter.replace_query(table_name, parse_one(f"SELECT col + 1 AS col FROM {table_name}")) assert to_sql_calls(adapter) == [ @@ -268,6 +285,11 @@ def test_replace_query_self_ref_exists( adapter = make_mocked_engine_adapter(SparkEngineAdapter) adapter.cursor.fetchone.return_value = (1,) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="db", name="table", type="table")], + ) table_name = "db.table" temp_table_id = "abcdefgh" @@ -525,11 +547,6 @@ def test_spark_struct_complex_to_col_to_types(type_name, spark_type): def test_scd_type_2_by_time( make_mocked_engine_adapter: t.Callable, make_temp_table_name: t.Callable, mocker: MockerFixture ): - mocker.patch( - "sqlmesh.core.engine_adapter.spark.SparkEngineAdapter.table_exists", - return_value=False, - ) - adapter = make_mocked_engine_adapter(SparkEngineAdapter) adapter._default_catalog = "spark_catalog" adapter.spark.catalog.currentCatalog.return_value = "spark_catalog" @@ -550,6 +567,11 @@ def check_table_exists(table_name: exp.Table) -> bool: "sqlmesh.core.engine_adapter.spark.SparkEngineAdapter.table_exists", side_effect=check_table_exists, ) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="db", name="target", type="table")], + ) adapter.scd_type_2_by_time( target_table="db.target", @@ -981,6 +1003,11 @@ def test_replace_query_with_wap_self_reference( ) adapter = make_mocked_engine_adapter(SparkEngineAdapter) + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="schema", name="table", type="table")], + ) adapter.replace_query( "catalog.schema.table.branch_wap_12345", diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index a5a6969e38..d8e96a1f35 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -1996,6 +1996,47 @@ def test_unrestorable_snapshot(state_sync: EngineAdapterStateSync, make_snapshot assert not actual_snapshots[new_forward_only_snapshot.snapshot_id].unrestorable +def test_unrestorable_snapshot_target_not_forward_only( + state_sync: EngineAdapterStateSync, make_snapshot: t.Callable +): + snapshot = make_snapshot( + SqlModel( + name="test_snapshot", + query=parse_one("select 1, ds"), + cron="@daily", + ), + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) + snapshot.version = "a" + + assert not snapshot.unpaused_ts + state_sync.push_snapshots([snapshot]) + + unpaused_dt = "2022-01-01" + state_sync.unpause_snapshots([snapshot], unpaused_dt) + + actual_snapshot = state_sync.get_snapshots([snapshot])[snapshot.snapshot_id] + assert actual_snapshot.unpaused_ts + assert actual_snapshot.unpaused_ts == to_timestamp(unpaused_dt) + + updated_snapshot = make_snapshot( + SqlModel(name="test_snapshot", query=parse_one("select 2, ds"), cron="@daily") + ) + updated_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=False) + updated_snapshot.version = "a" + + assert not updated_snapshot.unpaused_ts + state_sync.push_snapshots([updated_snapshot]) + state_sync.unpause_snapshots([updated_snapshot], unpaused_dt) + + actual_snapshots = state_sync.get_snapshots([snapshot, updated_snapshot]) + assert not actual_snapshots[snapshot.snapshot_id].unpaused_ts + assert actual_snapshots[updated_snapshot.snapshot_id].unpaused_ts == to_timestamp(unpaused_dt) + + assert actual_snapshots[snapshot.snapshot_id].unrestorable + assert not actual_snapshots[updated_snapshot.snapshot_id].unrestorable + + def test_unpause_snapshots_remove_intervals( state_sync: EngineAdapterStateSync, make_snapshot: t.Callable ): diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index d15e097875..5a0e7bdf48 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -2454,6 +2454,237 @@ def test_unaligned_start_snapshot_with_non_deployable_downstream(init_and_plan_c assert snapshot_interval.intervals[0][0] == to_timestamp("2023-01-07") +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + + assert all( + s.virtual_environment_mode.is_dev_only or not s.is_model or s.is_symbolic + for s in context.snapshots.values() + ) + + # Init prod + context.plan("prod", auto_apply=True, no_prompts=True) + + # Make a change in dev + original_model = context.get_model("sushi.waiter_revenue_by_day") + original_fingerprint = context.get_snapshot(original_model.name).fingerprint + model = original_model.copy(update={"query": original_model.query.order_by("waiter_id")}) + model = add_projection_to_model(t.cast(SqlModel, model)) + context.upsert_model(model) + + plan_dev = context.plan_builder("dev").build() + assert to_timestamp(plan_dev.start) == to_timestamp("2023-01-07") + assert plan_dev.requires_backfill + assert plan_dev.missing_intervals == [ + SnapshotIntervals( + snapshot_id=context.get_snapshot("sushi.top_waiters").snapshot_id, + intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], + ), + SnapshotIntervals( + snapshot_id=context.get_snapshot("sushi.waiter_revenue_by_day").snapshot_id, + intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], + ), + ] + assert plan_dev.context_diff.snapshots[context.get_snapshot(model.name).snapshot_id].intervals + assert plan_dev.context_diff.snapshots[ + context.get_snapshot("sushi.top_waiters").snapshot_id + ].intervals + assert plan_dev.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].dev_intervals + assert plan_dev.context_diff.snapshots[ + context.get_snapshot("sushi.top_waiters").snapshot_id + ].dev_intervals + context.apply(plan_dev) + + # Make sure the waiter_revenue_by_day model is a table in prod and a view in dev + table_types_df = context.engine_adapter.fetchdf( + "SELECT table_schema, table_type FROM INFORMATION_SCHEMA.TABLES WHERE table_name = 'waiter_revenue_by_day'" + ) + assert table_types_df.to_dict("records") == [ + {"table_schema": "sushi", "table_type": "BASE TABLE"}, + {"table_schema": "sushi__dev", "table_type": "VIEW"}, + ] + + # Check that the specified dates were backfilled + min_event_date = context.engine_adapter.fetchone( + "SELECT MIN(event_date) FROM sushi__dev.waiter_revenue_by_day" + )[0] + assert min_event_date == to_date("2023-01-07") + + # Make sure the changes are applied without backfill in prod + plan_prod = context.plan_builder("prod").build() + assert not plan_prod.requires_backfill + assert not plan_prod.missing_intervals + context.apply(plan_prod) + assert "one" in context.engine_adapter.columns("sushi.waiter_revenue_by_day") + + # Make sure the revert of a breaking changes results in a full rebuild + context.upsert_model(original_model) + assert context.get_snapshot(original_model.name).fingerprint == original_fingerprint + + plan_prod = context.plan_builder( + "prod", allow_destructive_models=["sushi.waiter_revenue_by_day"] + ).build() + assert not plan_prod.requires_backfill + assert not plan_prod.missing_intervals + context.apply(plan_prod) + assert "one" not in context.engine_adapter.columns("sushi.waiter_revenue_by_day") + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_kind_change(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.apply(plan) + + # Change to full kind + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": FullKind()}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.missing_intervals + assert prod_plan.requires_backfill + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "table" + + # Change back to view + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": ViewKind()}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "view" + + # Change to incremental + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": IncrementalUnmanagedKind()}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "table" + + # Change back to full + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": FullKind()}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "table" + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_kind_change_with_follow_up_changes_in_dev( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.apply(plan) + + # Make sure the initial state is a view + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "view" + + # Change to incremental unmanaged kind + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": IncrementalUnmanagedKind()}) + context.upsert_model(model) + dev_plan = context.plan_builder("dev", skip_tests=True).build() + assert dev_plan.missing_intervals + assert dev_plan.requires_backfill + context.apply(dev_plan) + + # Make a follow-up forward-only change + model = add_projection_to_model(t.cast(SqlModel, model)) + context.upsert_model(model) + dev_plan = context.plan_builder("dev", skip_tests=True, forward_only=True).build() + context.apply(dev_plan) + + # Deploy to prod + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "table" + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_kind_change_manual_categorization( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.apply(plan) + + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": FullKind()}) + context.upsert_model(model) + dev_plan_builder = context.plan_builder("dev", skip_tests=True, no_auto_categorization=True) + dev_plan_builder.set_choice( + dev_plan_builder._context_diff.snapshots[context.get_snapshot(model.name).snapshot_id], + SnapshotChangeCategory.NON_BREAKING, + ) + dev_plan = dev_plan_builder.build() + assert dev_plan.requires_backfill + assert len(dev_plan.missing_intervals) == 1 + context.apply(dev_plan) + + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=context.get_snapshot("sushi.top_waiters").snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_restatement_plan_ignores_changes(init_and_plan_context: t.Callable): context, plan = init_and_plan_context("examples/sushi") @@ -2505,7 +2736,11 @@ def test_restatement_plan_across_environments_snapshot_with_shared_version( assert isinstance(previous_kind, IncrementalByTimeRangeKind) model = model.copy( - update={"kind": IncrementalUnmanagedKind(), "physical_version": "pinned_version_12345"} + update={ + "kind": IncrementalUnmanagedKind(), + "physical_version": "pinned_version_12345", + "partitioned_by_": [exp.column("event_date")], + } ) context.upsert_model(model) context.plan("prod", auto_apply=True, no_prompts=True) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index f8070a98a4..6f3bd0bc7e 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -1908,7 +1908,8 @@ def test_render_definition_with_defaults(): dialect spark, kind VIEW ( materialized FALSE - ) + ), + virtual_environment_mode 'full' ); {query} @@ -5731,7 +5732,7 @@ def test_default_catalog_sql(assert_exp_eq): The system is not designed to actually support having an engine that doesn't support default catalog to start supporting it or the reverse of that. If that did happen then bugs would occur. """ - HASH_WITH_CATALOG = "516937963" + HASH_WITH_CATALOG = "1269513823" # Test setting default catalog doesn't change hash if it matches existing logic expressions = d.parse( @@ -5897,7 +5898,7 @@ def test_default_catalog_sql(assert_exp_eq): def test_default_catalog_python(): - HASH_WITH_CATALOG = "770057346" + HASH_WITH_CATALOG = "2728996410" @model(name="db.table", kind="full", columns={'"COL"': "int"}) def my_model(context, **kwargs): @@ -5989,7 +5990,7 @@ def test_default_catalog_external_model(): Since external models fqns are the only thing affected by default catalog, and when they change new snapshots are made, the hash will be the same across different names. """ - EXPECTED_HASH = "3614876346" + EXPECTED_HASH = "763256265" model = create_external_model("db.table", columns={"a": "int", "limit": "int"}) assert model.default_catalog is None diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 35c3628cff..66018d4be4 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -37,7 +37,6 @@ from sqlmesh.utils.dag import DAG from sqlmesh.utils.date import ( now, - now_timestamp, to_date, to_datetime, to_timestamp, @@ -1097,53 +1096,6 @@ def test_end_validation(make_snapshot, mocker: MockerFixture): assert restatement_prod_plan_builder.build().end == "2022-01-04" -def test_forward_only_revert_not_allowed(make_snapshot, mocker: MockerFixture): - snapshot = make_snapshot(SqlModel(name="a", query=parse_one("select 1, ds"))) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - assert not snapshot.is_forward_only - - forward_only_snapshot = make_snapshot(SqlModel(name="a", query=parse_one("select 2, ds"))) - forward_only_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) - forward_only_snapshot.version = snapshot.version - forward_only_snapshot.unpaused_ts = now_timestamp() - assert forward_only_snapshot.is_forward_only - - context_diff = ContextDiff( - environment="test_environment", - is_new_environment=True, - is_unfinalized_environment=False, - normalize_environment_name=True, - create_from="prod", - create_from_env_exists=True, - added=set(), - removed_snapshots={}, - modified_snapshots={snapshot.name: (snapshot, forward_only_snapshot)}, - snapshots={snapshot.snapshot_id: snapshot}, - new_snapshots={}, - previous_plan_id=None, - previously_promoted_snapshot_ids=set(), - previous_finalized_snapshots=None, - previous_gateway_managed_virtual_layer=False, - gateway_managed_virtual_layer=False, - environment_statements=[], - ) - - with pytest.raises( - PlanError, - match=r"Attempted to revert to an unrevertable version of model.*", - ): - PlanBuilder(context_diff, forward_only=True).build() - - # Make sure the plan can be created if a new snapshot version was enforced. - new_version_snapshot = make_snapshot( - SqlModel(name="a", query=parse_one("select 1, ds"), stamp="test_stamp") - ) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - context_diff.modified_snapshots = {snapshot.name: (new_version_snapshot, forward_only_snapshot)} - context_diff.new_snapshots = {new_version_snapshot.snapshot_id: new_version_snapshot} - PlanBuilder(context_diff, forward_only=True).build() - - def test_forward_only_plan_seed_models(make_snapshot, mocker: MockerFixture): snapshot_a = make_snapshot( SeedModel( diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index f560b93251..1b660e1a87 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -4,6 +4,7 @@ from pytest_mock.plugin import MockerFixture from sqlmesh.core.config import EnvironmentSuffixTarget +from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.model import SqlModel, ModelKindName from sqlmesh.core.plan.definition import EvaluatablePlan from sqlmesh.core.plan.stages import ( @@ -1300,3 +1301,485 @@ def test_build_plan_stages_indirect_non_breaking_view_migration( migrate_schemas_stage = stages[4] assert {s.snapshot_id for s in migrate_schemas_stage.snapshots} == {new_snapshot_c.snapshot_id} + + +def test_build_plan_stages_virtual_environment_mode_filtering( + make_snapshot, mocker: MockerFixture +) -> None: + # Create snapshots with different virtual environment modes + snapshot_full = make_snapshot( + SqlModel( + name="full_model", + query=parse_one("select 1, ds"), + kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"), + virtual_environment_mode=VirtualEnvironmentMode.FULL, + ) + ) + snapshot_full.categorize_as(SnapshotChangeCategory.BREAKING) + + snapshot_dev_only = make_snapshot( + SqlModel( + name="dev_only_model", + query=parse_one("select 2, ds"), + kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"), + virtual_environment_mode=VirtualEnvironmentMode.DEV_ONLY, + ) + ) + snapshot_dev_only.categorize_as(SnapshotChangeCategory.BREAKING) + + # Mock state reader + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = {} + state_reader.get_environment.return_value = None + + # Test 1: Dev environment - both snapshots should be included + environment_dev = Environment( + name="dev", + snapshots=[snapshot_full.table_info, snapshot_dev_only.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_full.snapshot_id, snapshot_dev_only.snapshot_id], + ) + + plan_dev = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[snapshot_full, snapshot_dev_only], + environment=environment_dev, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=True, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[snapshot_full.snapshot_id, snapshot_dev_only.snapshot_id], + indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + stages_dev = build_plan_stages(plan_dev, state_reader, None) + + # Find VirtualLayerUpdateStage + virtual_stage_dev = next( + stage for stage in stages_dev if isinstance(stage, VirtualLayerUpdateStage) + ) + + # In dev environment, both snapshots should be promoted regardless of virtual_environment_mode + assert {s.name for s in virtual_stage_dev.promoted_snapshots} == { + '"full_model"', + '"dev_only_model"', + } + assert len(virtual_stage_dev.demoted_snapshots) == 0 + + # Test 2: Production environment - only FULL mode snapshots should be included + environment_prod = Environment( + name="prod", + snapshots=[snapshot_full.table_info, snapshot_dev_only.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_full.snapshot_id, snapshot_dev_only.snapshot_id], + ) + + plan_prod = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[snapshot_full, snapshot_dev_only], + environment=environment_prod, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[snapshot_full.snapshot_id, snapshot_dev_only.snapshot_id], + indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + stages_prod = build_plan_stages(plan_prod, state_reader, None) + + # Find VirtualLayerUpdateStage + virtual_stage_prod = next( + stage for stage in stages_prod if isinstance(stage, VirtualLayerUpdateStage) + ) + + # In production environment, only FULL mode snapshots should be promoted + assert {s.name for s in virtual_stage_prod.promoted_snapshots} == {'"full_model"'} + assert len(virtual_stage_prod.demoted_snapshots) == 0 + + # Test 3: Production environment with demoted snapshots + existing_environment = Environment( + name="prod", + snapshots=[snapshot_full.table_info, snapshot_dev_only.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_full.snapshot_id, snapshot_dev_only.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + state_reader.get_environment.return_value = existing_environment + + # Remove both snapshots from the new environment + environment_prod_demote = Environment( + name="prod", + snapshots=[], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id="previous_plan", + promoted_snapshot_ids=[], + ) + + plan_prod_demote = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[], + environment=environment_prod_demote, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[], + indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], + removed_snapshots=[snapshot_full.snapshot_id, snapshot_dev_only.snapshot_id], + requires_backfill=False, + models_to_backfill=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + stages_prod_demote = build_plan_stages(plan_prod_demote, state_reader, None) + + # Find VirtualLayerUpdateStage + virtual_stage_prod_demote = next( + stage for stage in stages_prod_demote if isinstance(stage, VirtualLayerUpdateStage) + ) + + # In production environment, only FULL mode snapshots should be demoted + assert len(virtual_stage_prod_demote.promoted_snapshots) == 0 + assert {s.name for s in virtual_stage_prod_demote.demoted_snapshots} == {'"full_model"'} + assert ( + virtual_stage_prod_demote.demoted_environment_naming_info + == existing_environment.naming_info + ) + + +def test_build_plan_stages_virtual_environment_mode_no_updates( + snapshot_a: Snapshot, make_snapshot, mocker: MockerFixture +) -> None: + # Create snapshot with DEV_ONLY mode + snapshot_dev_only = make_snapshot( + SqlModel( + name="dev_only_model", + query=parse_one("select 1, ds"), + kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"), + virtual_environment_mode=VirtualEnvironmentMode.DEV_ONLY, + ) + ) + snapshot_dev_only.categorize_as(SnapshotChangeCategory.BREAKING) + + # Mock state reader + state_reader = mocker.Mock(spec=StateReader) + state_reader.get_snapshots.return_value = {} + state_reader.get_environment.return_value = None + + # Production environment with only DEV_ONLY snapshots + environment = Environment( + name="prod", + snapshots=[snapshot_dev_only.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_dev_only.snapshot_id], + ) + + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[snapshot_dev_only], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[snapshot_dev_only.snapshot_id], + indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + stages = build_plan_stages(plan, state_reader, None) + + # No VirtualLayerUpdateStage should be created since all snapshots are filtered out + virtual_stages = [stage for stage in stages if isinstance(stage, VirtualLayerUpdateStage)] + assert len(virtual_stages) == 0 + + +def test_adjust_intervals_new_forward_only_dev_intervals( + make_snapshot, mocker: MockerFixture +) -> None: + forward_only_snapshot = make_snapshot( + SqlModel( + name="forward_only_model", + query=parse_one("select 1, ds"), + kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"), + ) + ) + forward_only_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) + forward_only_snapshot.intervals = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] + + forward_only_snapshot.dev_intervals = [] + + state_reader = mocker.Mock(spec=StateReader) + state_reader.refresh_snapshot_intervals = mocker.Mock() + state_reader.get_snapshots.return_value = {} + state_reader.get_environment.return_value = None + + environment = Environment( + snapshots=[forward_only_snapshot.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id=None, + promoted_snapshot_ids=[forward_only_snapshot.snapshot_id], + ) + + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[forward_only_snapshot], # This snapshot should have dev_intervals set + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=True, # Dev environment + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[], + indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + assert forward_only_snapshot.dev_intervals == [] + + build_plan_stages(plan, state_reader, None) + + assert forward_only_snapshot.dev_intervals == [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")) + ] + assert forward_only_snapshot.dev_intervals is not forward_only_snapshot.intervals + + state_reader.refresh_snapshot_intervals.assert_called_once() + + +def test_adjust_intervals_restatement_removal( + snapshot_a: Snapshot, snapshot_b: Snapshot, mocker: MockerFixture +) -> None: + snapshot_a.intervals = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-04"))] + snapshot_b.intervals = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] + + original_a_intervals = snapshot_a.intervals.copy() + original_b_intervals = snapshot_b.intervals.copy() + + state_reader = mocker.Mock(spec=StateReader) + state_reader.refresh_snapshot_intervals = mocker.Mock() + state_reader.get_snapshots.return_value = {} + state_reader.get_environment.return_value = None + + environment = Environment( + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + ) + + restatements = { + snapshot_a.name: (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + snapshot_b.name: (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + } + + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[snapshot_a, snapshot_b], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements=restatements, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[], + indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + stages = build_plan_stages(plan, state_reader, None) + + assert snapshot_a.intervals != original_a_intervals + assert snapshot_b.intervals != original_b_intervals + + state_reader.refresh_snapshot_intervals.assert_called_once() + + restatement_stages = [stage for stage in stages if isinstance(stage, RestatementStage)] + assert len(restatement_stages) == 1 + restatement_stage = restatement_stages[0] + assert len(restatement_stage.snapshot_intervals) == 2 + + backfill_stages = [stage for stage in stages if isinstance(stage, BackfillStage)] + assert len(backfill_stages) == 1 + (snapshot, intervals) = next(iter(backfill_stages[0].snapshot_to_intervals.items())) + assert snapshot.intervals == [(to_timestamp("2023-01-02"), to_timestamp("2023-01-04"))] + assert intervals == [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] + + +def test_adjust_intervals_should_force_rebuild(make_snapshot, mocker: MockerFixture) -> None: + old_snapshot = make_snapshot( + SqlModel( + name="test_model", + query=parse_one("select 1, ds"), + kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"), + ) + ) + old_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + old_snapshot.intervals = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] + + new_snapshot = make_snapshot( + SqlModel( + name="test_model", + query=parse_one("select 1, ds"), + kind=dict(name=ModelKindName.FULL), + ) + ) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + new_snapshot.version = old_snapshot.version + new_snapshot.intervals = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] + + state_reader = mocker.Mock(spec=StateReader) + state_reader.refresh_snapshot_intervals = mocker.Mock() + state_reader.get_snapshots.side_effect = [{}, {old_snapshot.snapshot_id: old_snapshot}, {}, {}] + + existing_environment = Environment( + name="prod", + snapshots=[old_snapshot.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_plan", + promoted_snapshot_ids=[old_snapshot.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + state_reader.get_environment.return_value = existing_environment + + environment = Environment( + snapshots=[new_snapshot.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id="previous_plan", + promoted_snapshot_ids=[new_snapshot.snapshot_id], + ) + + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[new_snapshot], + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={}, + is_dev=False, + allow_destructive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + directly_modified_snapshots=[new_snapshot.snapshot_id], + indirectly_modified_snapshots={}, + metadata_updated_snapshots=[], + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + stages = build_plan_stages(plan, state_reader, None) + + state_reader.refresh_snapshot_intervals.assert_called_once() + state_reader.get_environment.assert_called() + + assert not new_snapshot.intervals + backfill_stages = [stage for stage in stages if isinstance(stage, BackfillStage)] + assert len(backfill_stages) == 1 + (snapshot, intervals) = next(iter(backfill_stages[0].snapshot_to_intervals.items())) + assert not snapshot.intervals + assert intervals == [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index 0ba7180fb6..bcb704ba48 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -64,6 +64,7 @@ table_name, TableNamingConvention, ) +from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.utils import AttributeDict from sqlmesh.utils.date import DatetimeRanges, to_date, to_datetime, to_timestamp from sqlmesh.utils.errors import SQLMeshError, SignalEvalError @@ -162,6 +163,7 @@ def test_json(snapshot: Snapshot): "signals": [], "enabled": True, "extract_dependencies_from_query": True, + "virtual_environment_mode": "full", }, "name": '"name"', "parents": [{"name": '"parent"."tbl"', "identifier": snapshot.parents[0].identifier}], @@ -910,7 +912,7 @@ def test_fingerprint(model: Model, parent_model: Model): fingerprint = fingerprint_from_node(model, nodes={}) original_fingerprint = SnapshotFingerprint( - data_hash="1312415267", + data_hash="3301649319", metadata_hash="1125608408", ) @@ -971,7 +973,7 @@ def test_fingerprint_seed_model(): ) expected_fingerprint = SnapshotFingerprint( - data_hash="1909791099", + data_hash="1586624913", metadata_hash="2315134974", ) @@ -1010,7 +1012,7 @@ def test_fingerprint_jinja_macros(model: Model): } ) original_fingerprint = SnapshotFingerprint( - data_hash="923305614", + data_hash="2908339239", metadata_hash="1125608408", ) @@ -1312,7 +1314,7 @@ def test_table_naming_convention_change_reuse_previous_version(make_snapshot): original_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) assert original_snapshot.table_naming_convention == TableNamingConvention.SCHEMA_AND_TABLE - assert original_snapshot.table_name() == "sqlmesh__default.a__4145234055" + assert original_snapshot.table_name() == f"sqlmesh__default.a__{original_snapshot.version}" changed_snapshot: Snapshot = make_snapshot( SqlModel(name="a", query=parse_one("select 1, 'forward_only' as a, ds")), @@ -1330,7 +1332,7 @@ def test_table_naming_convention_change_reuse_previous_version(make_snapshot): changed_snapshot.previous_version.table_naming_convention == TableNamingConvention.SCHEMA_AND_TABLE ) - assert changed_snapshot.table_name() == "sqlmesh__default.a__4145234055" + assert changed_snapshot.table_name() == f"sqlmesh__default.a__{changed_snapshot.version}" def test_categorize_change_sql(make_snapshot): @@ -3340,3 +3342,42 @@ def test_partitioned_by_roundtrip(make_snapshot: t.Callable): assert isinstance(deserialized.node, SqlModel) assert deserialized.node.partitioned_by == snapshot.node.partitioned_by + + +@pytest.mark.parametrize( + "virtual_env_mode,is_deployable,expected_uses_name_as_is", + [ + (VirtualEnvironmentMode.DEV_ONLY, True, True), + (VirtualEnvironmentMode.DEV_ONLY, False, False), + (VirtualEnvironmentMode.FULL, True, False), + (VirtualEnvironmentMode.FULL, False, False), + ], +) +def test_table_name_virtual_environment_mode( + make_snapshot, + virtual_env_mode: VirtualEnvironmentMode, + is_deployable: bool, + expected_uses_name_as_is: bool, +): + model = SqlModel( + name="my_schema.my_model", + kind=IncrementalByTimeRangeKind(time_column="ds"), + query=parse_one("SELECT 1, ds"), + virtual_environment_mode=virtual_env_mode, + ) + + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + table_name_result = snapshot.table_name(is_deployable=is_deployable) + + if expected_uses_name_as_is: + assert table_name_result == '"my_schema"."my_model"' + else: + # Should contain the versioned table name with schema prefix + assert "sqlmesh__my_schema" in table_name_result + assert "my_schema__my_model" in table_name_result + if is_deployable: + assert table_name_result.endswith(snapshot.version) + else: + assert table_name_result.endswith(f"{snapshot.dev_version}__dev") diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 4b028e148b..a3c7837711 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -1162,11 +1162,8 @@ def test_promote_deployable(mocker: MockerFixture, make_snapshot): ) -def test_migrate(mocker: MockerFixture, make_snapshot): - connection_mock = mocker.NonCallableMock() - cursor_mock = mocker.Mock() - connection_mock.cursor.return_value = cursor_mock - adapter = EngineAdapter(lambda: connection_mock, "") +def test_migrate(mocker: MockerFixture, make_snapshot, make_mocked_engine_adapter): + adapter = make_mocked_engine_adapter(EngineAdapter) session_spy = mocker.spy(adapter, "session") current_table = "sqlmesh__test_schema.test_schema__test_model__1" @@ -1184,6 +1181,11 @@ def columns(table_name): adapter.columns = columns # type: ignore adapter.table_exists = lambda _: True # type: ignore + mocker.patch.object( + adapter, + "get_data_object", + return_value=DataObject(schema="test_schema", name="test_model", type="table"), + ) evaluator = SnapshotEvaluator(adapter) @@ -1202,7 +1204,7 @@ def columns(table_name): evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) - cursor_mock.execute.assert_has_calls( + adapter.cursor.execute.assert_has_calls( [ call('ALTER TABLE "sqlmesh__test_schema"."test_schema__test_model__1" DROP COLUMN "b"'), call( @@ -1214,13 +1216,10 @@ def columns(table_name): session_spy.assert_called_once() -def test_migrate_missing_table(mocker: MockerFixture, make_snapshot): - connection_mock = mocker.NonCallableMock() - cursor_mock = mocker.Mock() - connection_mock.cursor.return_value = cursor_mock - adapter = EngineAdapter(lambda: connection_mock, "") - +def test_migrate_missing_table(mocker: MockerFixture, make_snapshot, make_mocked_engine_adapter): + adapter = make_mocked_engine_adapter(EngineAdapter) adapter.table_exists = lambda _: False # type: ignore + mocker.patch.object(adapter, "get_data_object", return_value=None) evaluator = SnapshotEvaluator(adapter) @@ -1241,7 +1240,7 @@ def test_migrate_missing_table(mocker: MockerFixture, make_snapshot): evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) - cursor_mock.execute.assert_has_calls( + adapter.cursor.execute.assert_has_calls( [ call('CREATE TABLE "pre" ("a" INT)'), call( @@ -1262,13 +1261,16 @@ def test_migrate_missing_table(mocker: MockerFixture, make_snapshot): def test_migrate_view( mocker: MockerFixture, make_snapshot, + make_mocked_engine_adapter, change_category: SnapshotChangeCategory, forward_only: bool, ): - connection_mock = mocker.NonCallableMock() - cursor_mock = mocker.Mock() - connection_mock.cursor.return_value = cursor_mock - adapter = EngineAdapter(lambda: connection_mock, "") + adapter = make_mocked_engine_adapter(EngineAdapter) + mocker.patch.object( + adapter, + "get_data_object", + return_value=DataObject(schema="test_schema", name="test_model", type="view"), + ) evaluator = SnapshotEvaluator(adapter) @@ -1284,7 +1286,7 @@ def test_migrate_view( evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) - cursor_mock.execute.assert_has_calls( + adapter.cursor.execute.assert_has_calls( [ call( 'CREATE OR REPLACE VIEW "sqlmesh__test_schema"."test_schema__test_model__1" ("c", "a") AS SELECT "c" AS "c", "a" AS "a" FROM "tbl" AS "tbl"' @@ -1293,6 +1295,45 @@ def test_migrate_view( ) +def test_migrate_snapshot_data_object_type_mismatch( + mocker: MockerFixture, + make_snapshot, + make_mocked_engine_adapter, +): + adapter = make_mocked_engine_adapter(EngineAdapter) + mocker.patch.object( + adapter, + "get_data_object", + return_value=DataObject( + schema="sqlmesh__test_schema", name="test_schema__test_model__1", type="table" + ), + ) + mocker.patch.object(adapter, "table_exists", return_value=False) + + evaluator = SnapshotEvaluator(adapter) + + model = SqlModel( + name="test_schema.test_model", + kind=ViewKind(), + storage_format="parquet", + query=parse_one("SELECT c, a FROM tbl"), + ) + snapshot = make_snapshot(model, version="1") + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True + + evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + + adapter.cursor.execute.assert_has_calls( + [ + call('DROP TABLE IF EXISTS "sqlmesh__test_schema"."test_schema__test_model__1"'), + call( + 'CREATE VIEW "sqlmesh__test_schema"."test_schema__test_model__1" AS SELECT "c" AS "c", "a" AS "a" FROM "tbl" AS "tbl"' + ), + ] + ) + + def test_evaluate_creation_duckdb( snapshot: Snapshot, duck_conn, @@ -1711,11 +1752,9 @@ def test_create_clone_in_dev_self_referencing( def test_on_destructive_change_runtime_check( mocker: MockerFixture, make_snapshot, + make_mocked_engine_adapter, ): - connection_mock = mocker.NonCallableMock() - cursor_mock = mocker.Mock() - connection_mock.cursor.return_value = cursor_mock - adapter = EngineAdapter(lambda: connection_mock, "") + adapter = make_mocked_engine_adapter(EngineAdapter) current_table = "sqlmesh__test_schema.test_schema__test_model__1" @@ -1731,6 +1770,11 @@ def columns(table_name): } adapter.columns = columns # type: ignore + mocker.patch.object( + adapter, + "get_data_object", + return_value=DataObject(schema="test_schema", name="test_model", type=DataObjectType.TABLE), + ) evaluator = SnapshotEvaluator(adapter) @@ -3704,6 +3748,11 @@ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_moc assert new_snapshot.table_name() == snapshot.table_name() + adapter_mock.get_data_object.return_value = DataObject( + schema="test_schema", name="test_model", type=DataObjectType.TABLE + ) + adapter_mock.drop_data_object_on_type_mismatch.return_value = False + evaluator.create([new_snapshot], {}) evaluator.migrate([new_snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) @@ -3772,6 +3821,11 @@ def test_migrate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.previous_versions = snapshot.all_versions + adapter_mock.get_data_object.return_value = DataObject( + schema="test_schema", name="test_model", type=DataObjectType.MANAGED_TABLE + ) + adapter_mock.drop_data_object_on_type_mismatch.return_value = False + # no schema changes - no-op adapter_mock.get_alter_expressions.return_value = [] evaluator.migrate( @@ -3938,12 +3992,12 @@ def columns(table_name): ) -def test_multiple_engine_migration(mocker: MockerFixture, adapter_mock, make_snapshot): - connection_mock = mocker.NonCallableMock() - cursor_mock = mocker.Mock() - connection_mock.cursor.return_value = cursor_mock - adapter = EngineAdapter(lambda: connection_mock, "") - engine_adapters = {"one": adapter, "two": adapter_mock} +def test_multiple_engine_migration( + mocker: MockerFixture, adapter_mock, make_snapshot, make_mocked_engine_adapter +): + adapter_one = make_mocked_engine_adapter(EngineAdapter) + adapter_two = adapter_mock + engine_adapters = {"one": adapter_one, "two": adapter_two} current_table = "sqlmesh__test_schema.test_schema__test_model__1" @@ -3958,8 +4012,18 @@ def columns(table_name): "a": exp.DataType.build("int"), } - adapter.columns = columns # type: ignore - adapter_mock.columns = columns # type: ignore + adapter_two.columns.side_effect = columns + adapter_two.get_data_object.return_value = DataObject( + schema="test_schema", name="test_model_2", type=DataObjectType.TABLE + ) + adapter_two.drop_data_object_on_type_mismatch.return_value = False + + mocker.patch.object(adapter_one, "columns", side_effect=columns) + mocker.patch.object( + adapter_one, + "get_data_object", + return_value=DataObject(schema="test_schema", name="test_model", type=DataObjectType.TABLE), + ) evaluator = SnapshotEvaluator(engine_adapters) @@ -3990,7 +4054,7 @@ def columns(table_name): [snapshot_1, snapshot_2], {}, deployability_index=DeployabilityIndex.none_deployable() ) - cursor_mock.execute.assert_has_calls( + adapter_one.cursor.execute.assert_has_calls( [ call('ALTER TABLE "sqlmesh__test_schema"."test_schema__test_model__1" DROP COLUMN "b"'), call( diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 521773d1ca..9c3c3aba4b 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -2153,44 +2153,6 @@ def test_test_generation_with_timestamp(tmp_path: Path) -> None: } -def test_test_generation_with_decimal(tmp_path: Path, mocker: MockerFixture) -> None: - from decimal import Decimal - - init_example_project(tmp_path, engine_type="duckdb") - - config = Config( - default_connection=DuckDBConnectionConfig(), - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - ) - foo_sql_file = tmp_path / "models" / "foo.sql" - foo_sql_file.write_text( - "MODEL (name sqlmesh_example.foo); SELECT dec_col FROM sqlmesh_example.bar;" - ) - bar_sql_file = tmp_path / "models" / "bar.sql" - bar_sql_file.write_text("MODEL (name sqlmesh_example.bar); SELECT dec_col FROM external_table;") - - context = Context(paths=tmp_path, config=config) - input_queries = { - '"memory"."sqlmesh_example"."bar"': "SELECT CAST(1.23 AS DECIMAL(10,2)) AS dec_col" - } - - # DuckDB actually returns a numpy.float64, even though the value is cast into a DECIMAL, - # but other engines don't behave the same. E.g. BigQuery returns a proper Decimal value. - mocker.patch( - "sqlmesh.core.engine_adapter.base.EngineAdapter.fetchdf", - return_value=pd.DataFrame({"dec_col": [Decimal("1.23")]}), - ) - - context.create_test("sqlmesh_example.foo", input_queries=input_queries, overwrite=True) - - test = load_yaml(context.path / c.TESTS / "test_foo.yaml") - - assert len(test) == 1 - assert "test_foo" in test - assert test["test_foo"]["inputs"] == {'"memory"."sqlmesh_example"."bar"': [{"dec_col": "1.23"}]} - assert test["test_foo"]["outputs"] == {"query": [{"dec_col": "1.23"}]} - - def test_test_generation_with_recursive_ctes(tmp_path: Path) -> None: init_example_project(tmp_path, engine_type="duckdb") @@ -2247,10 +2209,6 @@ def test_test_with_gateway_specific_model(tmp_path: Path, mocker: MockerFixture) context = Context(paths=tmp_path, config=config) input_queries = {'"memory"."sqlmesh_example"."input_model"': "SELECT 5 AS c"} - mocker.patch( - "sqlmesh.core.engine_adapter.base.EngineAdapter.fetchdf", - return_value=pd.DataFrame({"c": [5]}), - ) assert context.engine_adapter == context.engine_adapters["main"] with pytest.raises( From 803f7d89944d92a85100c5f9aa65d8525df3ebec Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 12 Aug 2025 11:29:49 -0700 Subject: [PATCH 0686/1056] Fix: Migration script name --- ..._environment_mode.py => v0089_add_virtual_environment_mode.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sqlmesh/migrations/{v0088_add_virtual_environment_mode.py => v0089_add_virtual_environment_mode.py} (100%) diff --git a/sqlmesh/migrations/v0088_add_virtual_environment_mode.py b/sqlmesh/migrations/v0089_add_virtual_environment_mode.py similarity index 100% rename from sqlmesh/migrations/v0088_add_virtual_environment_mode.py rename to sqlmesh/migrations/v0089_add_virtual_environment_mode.py From 9afc728ead7ec41a16f332e40180aa59b4fae2c7 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 12 Aug 2025 17:03:54 -0700 Subject: [PATCH 0687/1056] Feat: Re-introduce merge for updating auto restatements (#5112) --- sqlmesh/core/engine_adapter/postgres.py | 20 +++++++++++++------ sqlmesh/core/state_sync/db/snapshot.py | 16 ++++++++++----- .../integration/test_integration_postgres.py | 6 +++++- tests/core/engine_adapter/test_postgres.py | 20 +++++++++++++++++-- 4 files changed, 48 insertions(+), 14 deletions(-) diff --git a/sqlmesh/core/engine_adapter/postgres.py b/sqlmesh/core/engine_adapter/postgres.py index 9962c037ac..a736f5553b 100644 --- a/sqlmesh/core/engine_adapter/postgres.py +++ b/sqlmesh/core/engine_adapter/postgres.py @@ -1,8 +1,9 @@ from __future__ import annotations import logging +import re import typing as t -from functools import partial +from functools import cached_property, partial from sqlglot import exp from sqlmesh.core.engine_adapter.base_postgres import BasePostgresEngineAdapter @@ -112,11 +113,8 @@ def merge( **kwargs: t.Any, ) -> None: # Merge isn't supported until Postgres 15 - merge_impl = ( - super().merge - if self._connection_pool.get().server_version >= 150000 - else partial(logical_merge, self) - ) + major, minor = self.server_version + merge_impl = super().merge if major >= 15 else partial(logical_merge, self) merge_impl( # type: ignore target_table, source_table, @@ -125,3 +123,13 @@ def merge( when_matched=when_matched, merge_filter=merge_filter, ) + + @cached_property + def server_version(self) -> t.Tuple[int, int]: + """Lazily fetch and cache major and minor server version""" + if result := self.fetchone("SHOW server_version"): + server_version, *_ = result + match = re.search(r"(\d+)\.(\d+)", server_version) + if match: + return int(match.group(1)), int(match.group(2)) + return 0, 0 diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 6064993087..30e0de00f2 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -372,25 +372,31 @@ def update_auto_restatements( Args: next_auto_restatement_ts: A dictionary of snapshot name version to the next auto restatement timestamp. """ + next_auto_restatement_ts_deleted = [] + next_auto_restatement_ts_filtered = {} + for k, v in next_auto_restatement_ts.items(): + if v is None: + next_auto_restatement_ts_deleted.append(k) + else: + next_auto_restatement_ts_filtered[k] = v + for where in snapshot_name_version_filter( self.engine_adapter, - next_auto_restatement_ts, + next_auto_restatement_ts_deleted, column_prefix="snapshot", alias=None, batch_size=self.SNAPSHOT_BATCH_SIZE, ): self.engine_adapter.delete_from(self.auto_restatements_table, where=where) - next_auto_restatement_ts_filtered = { - k: v for k, v in next_auto_restatement_ts.items() if v is not None - } if not next_auto_restatement_ts_filtered: return - self.engine_adapter.insert_append( + self.engine_adapter.merge( self.auto_restatements_table, _auto_restatements_to_df(next_auto_restatement_ts_filtered), columns_to_types=self._auto_restatement_columns_to_types, + unique_key=(exp.column("snapshot_name"), exp.column("snapshot_version")), ) def count(self) -> int: diff --git a/tests/core/engine_adapter/integration/test_integration_postgres.py b/tests/core/engine_adapter/integration/test_integration_postgres.py index 863aae55a4..82172378ae 100644 --- a/tests/core/engine_adapter/integration/test_integration_postgres.py +++ b/tests/core/engine_adapter/integration/test_integration_postgres.py @@ -2,7 +2,6 @@ import pytest from pytest import FixtureRequest from sqlmesh.core.engine_adapter import PostgresEngineAdapter -from tests.core.engine_adapter.integration import TestContext from tests.core.engine_adapter.integration import ( TestContext, @@ -29,3 +28,8 @@ def engine_adapter(ctx: TestContext) -> PostgresEngineAdapter: def test_engine_adapter(ctx: TestContext): assert isinstance(ctx.engine_adapter, PostgresEngineAdapter) assert ctx.engine_adapter.fetchone("select 1") == (1,) + + +def test_server_version_psycopg(ctx: TestContext): + assert isinstance(ctx.engine_adapter, PostgresEngineAdapter) + assert ctx.engine_adapter.server_version != (0, 0) diff --git a/tests/core/engine_adapter/test_postgres.py b/tests/core/engine_adapter/test_postgres.py index f013914c3e..fd6ce44994 100644 --- a/tests/core/engine_adapter/test_postgres.py +++ b/tests/core/engine_adapter/test_postgres.py @@ -94,7 +94,7 @@ def test_create_table_like(make_mocked_engine_adapter: t.Callable): def test_merge_version_gte_15(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(PostgresEngineAdapter) - adapter._connection_pool.get().server_version = 150000 + adapter.server_version = (15, 0) adapter.merge( target_table="target", @@ -117,7 +117,7 @@ def test_merge_version_lt_15( make_mocked_engine_adapter: t.Callable, make_temp_table_name: t.Callable, mocker: MockerFixture ): adapter = make_mocked_engine_adapter(PostgresEngineAdapter) - adapter._connection_pool.get().server_version = 140000 + adapter.server_version = (14, 0) temp_table_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter._get_temp_table") table_name = "test" @@ -161,3 +161,19 @@ def table_columns(table_name: str) -> t.Dict[str, exp.DataType]: assert to_sql_calls(adapter) == [ 'ALTER TABLE "test_table" DROP COLUMN "test_column" CASCADE', ] + + +def test_server_version(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): + adapter = make_mocked_engine_adapter(PostgresEngineAdapter) + + fetchone_mock = mocker.patch.object(adapter, "fetchone") + fetchone_mock.return_value = ("14.0",) + assert adapter.server_version == (14, 0) + + del adapter.server_version + fetchone_mock.return_value = ("15.8",) + assert adapter.server_version == (15, 8) + + del adapter.server_version + fetchone_mock.return_value = ("15.13 (Debian 15.13-1.pgdg120+1)",) + assert adapter.server_version == (15, 13) From eed4c269071a71a22c71eec686518c9f3a899b2b Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 12 Aug 2025 17:40:47 -0700 Subject: [PATCH 0688/1056] Chore: GCP Postgres integration tests (#5143) --- .circleci/continue_config.yml | 2 ++ .circleci/manage-test-db.sh | 26 +++++++++++++++++++ Makefile | 3 +++ sqlmesh/core/config/connection.py | 6 ----- .../engine_adapter/integration/__init__.py | 1 + .../engine_adapter/integration/config.yaml | 11 ++++++++ .../integration/test_integration.py | 6 ++++- 7 files changed, 48 insertions(+), 7 deletions(-) diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index b93caf482e..04135574a9 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -246,6 +246,7 @@ jobs: echo "export SNOWFLAKE_DATABASE='$TEST_DB_NAME'" >> "$BASH_ENV" echo "export DATABRICKS_CATALOG='$TEST_DB_NAME'" >> "$BASH_ENV" echo "export REDSHIFT_DATABASE='$TEST_DB_NAME'" >> "$BASH_ENV" + echo "export GCP_POSTGRES_DATABASE='$TEST_DB_NAME'" >> "$BASH_ENV" - run: name: Create test database command: ./.circleci/manage-test-db.sh << parameters.engine >> "$TEST_DB_NAME" up @@ -303,6 +304,7 @@ workflows: - bigquery - clickhouse-cloud - athena + - gcp-postgres filters: branches: only: diff --git a/.circleci/manage-test-db.sh b/.circleci/manage-test-db.sh index de8922c988..80ca075912 100755 --- a/.circleci/manage-test-db.sh +++ b/.circleci/manage-test-db.sh @@ -109,6 +109,32 @@ clickhouse-cloud_init() { echo "Clickhouse Cloud instance $CLICKHOUSE_CLOUD_HOST is up and running" } +# GCP Postgres +gcp-postgres_init() { + # Download and start Cloud SQL Proxy + curl -fsSL -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.18.0/cloud-sql-proxy.linux.amd64 + chmod +x cloud-sql-proxy + echo "$GCP_POSTGRES_KEYFILE_JSON" > /tmp/keyfile.json + ./cloud-sql-proxy --credentials-file /tmp/keyfile.json $GCP_POSTGRES_INSTANCE_CONNECTION_STRING & + + # Wait for proxy to start + sleep 5 +} + +gcp-postgres_exec() { + PGPASSWORD=$GCP_POSTGRES_PASSWORD psql -h 127.0.0.1 -U $GCP_POSTGRES_USER -c "$1" postgres +} + +gcp-postgres_up() { + gcp-postgres_exec "create database $1" +} + +gcp-postgres_down() { + gcp-postgres_exec "drop database $1" +} + + + INIT_FUNC="${ENGINE}_init" UP_FUNC="${ENGINE}_up" DOWN_FUNC="${ENGINE}_down" diff --git a/Makefile b/Makefile index 0a89bba437..855d866c84 100644 --- a/Makefile +++ b/Makefile @@ -173,6 +173,9 @@ clickhouse-cloud-test: guard-CLICKHOUSE_CLOUD_HOST guard-CLICKHOUSE_CLOUD_USERNA athena-test: guard-AWS_ACCESS_KEY_ID guard-AWS_SECRET_ACCESS_KEY guard-ATHENA_S3_WAREHOUSE_LOCATION engine-athena-install pytest -n auto -m "athena" --retries 3 --junitxml=test-results/junit-athena.xml +gcp-postgres-test: guard-GCP_POSTGRES_INSTANCE_CONNECTION_STRING guard-GCP_POSTGRES_USER guard-GCP_POSTGRES_PASSWORD guard-GCP_POSTGRES_KEYFILE_JSON engine-gcppostgres-install + pytest -n auto -m "gcp_postgres" --retries 3 --junitxml=test-results/junit-gcp-postgres.xml + vscode_settings: mkdir -p .vscode cp -r ./tooling/vscode/*.json .vscode/ diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 415e916365..4f289262ea 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -1210,12 +1210,6 @@ def _validate_auth_method(cls, data: t.Any) -> t.Any: password = data.get("password") enable_iam_auth = data.get("enable_iam_auth") - if password and enable_iam_auth: - raise ConfigError( - "Invalid GCP Postgres connection configuration - both password and" - " enable_iam_auth set. Use password when connecting to a postgres" - " user and enable_iam_auth 'True' when connecting to an IAM user." - ) if not password and not enable_iam_auth: raise ConfigError( "GCP Postgres connection configuration requires either password set" diff --git a/tests/core/engine_adapter/integration/__init__.py b/tests/core/engine_adapter/integration/__init__.py index 7e35b832be..15339eeaa6 100644 --- a/tests/core/engine_adapter/integration/__init__.py +++ b/tests/core/engine_adapter/integration/__init__.py @@ -82,6 +82,7 @@ def pytest_marks(self) -> t.List[MarkDecorator]: IntegrationTestEngine("bigquery", native_dataframe_type="bigframe", cloud=True), IntegrationTestEngine("databricks", native_dataframe_type="pyspark", cloud=True), IntegrationTestEngine("snowflake", native_dataframe_type="snowpark", cloud=True), + IntegrationTestEngine("gcp_postgres", cloud=True), ] ENGINES_BY_NAME = {e.engine: e for e in ENGINES} diff --git a/tests/core/engine_adapter/integration/config.yaml b/tests/core/engine_adapter/integration/config.yaml index d18ea5366f..4aee4640a3 100644 --- a/tests/core/engine_adapter/integration/config.yaml +++ b/tests/core/engine_adapter/integration/config.yaml @@ -186,5 +186,16 @@ gateways: state_connection: type: duckdb + inttest_gcp_postgres: + connection: + type: gcp_postgres + instance_connection_string: {{ env_var("GCP_POSTGRES_INSTANCE_CONNECTION_STRING") }} + user: {{ env_var("GCP_POSTGRES_USER") }} + password: {{ env_var("GCP_POSTGRES_PASSWORD") }} + keyfile_json: {{ env_var("GCP_POSTGRES_KEYFILE_JSON", "") }} + db: {{ env_var("GCP_POSTGRES_DATABASE") }} + enable_iam_auth: true + check_import: false + model_defaults: dialect: duckdb diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index eb593c273b..0739256d6e 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -1587,7 +1587,11 @@ def _normalize_snowflake(name: str, prefix_regex: str = "(sqlmesh__)(.*)"): k: [_normalize_snowflake(name) for name in v] for k, v in object_names.items() } - init_example_project(tmp_path, ctx.mark.split("_")[0], schema_name=schema_name) + if ctx.mark.startswith("gcp_postgres"): + engine_type = "gcp_postgres" + else: + engine_type = ctx.mark.split("_")[0] + init_example_project(tmp_path, engine_type, schema_name=schema_name) config = load_config_from_paths( Config, From d177625b237ab960179f917a9685a40f6f4d8455 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 13 Aug 2025 10:19:07 +0300 Subject: [PATCH 0689/1056] Feat!: cancel submitted BigQuery jobs on keyboard interrupts (#4979) --- sqlmesh/core/engine_adapter/bigquery.py | 56 +++++++++++++--- sqlmesh/utils/connection_pool.py | 24 +++++++ tests/core/engine_adapter/test_bigquery.py | 78 ++++++++++++++++++++++ 3 files changed, 147 insertions(+), 11 deletions(-) diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index e953f4d1d0..1b32195f77 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -33,6 +33,7 @@ from google.cloud import bigquery from google.cloud.bigquery import StandardSqlDataType from google.cloud.bigquery.client import Client as BigQueryClient + from google.cloud.bigquery.job import QueryJob from google.cloud.bigquery.job.base import _AsyncJob as BigQueryQueryResult from google.cloud.bigquery.table import Table as BigQueryTable @@ -186,6 +187,31 @@ def query_factory() -> Query: ) ] + def close(self) -> t.Any: + # Cancel all pending query jobs across all threads + all_query_jobs = self._connection_pool.get_all_attributes("query_job") + for query_job in all_query_jobs: + if query_job: + try: + if not self._db_call(query_job.done): + self._db_call(query_job.cancel) + logger.debug( + "Cancelled BigQuery job: https://console.cloud.google.com/bigquery?project=%s&j=bq:%s:%s", + query_job.project, + query_job.location, + query_job.job_id, + ) + except Exception as ex: + logger.debug( + "Failed to cancel BigQuery job: https://console.cloud.google.com/bigquery?project=%s&j=bq:%s:%s. %s", + query_job.project, + query_job.location, + query_job.job_id, + str(ex), + ) + + return super().close() + def _begin_session(self, properties: SessionProperties) -> None: from google.cloud.bigquery import QueryJobConfig @@ -318,7 +344,10 @@ def create_mapping_schema( if len(table.parts) == 3 and "." in table.name: # The client's `get_table` method can't handle paths with >3 identifiers self.execute(exp.select("*").from_(table).limit(0)) - query_results = self._query_job._query_results + query_job = self._query_job + assert query_job is not None + + query_results = query_job._query_results columns = create_mapping_schema(query_results.schema) else: bq_table = self._get_table(table) @@ -717,7 +746,9 @@ def _fetch_native_df( self, query: t.Union[exp.Expression, str], quote_identifiers: bool = False ) -> DF: self.execute(query, quote_identifiers=quote_identifiers) - return self._query_job.to_dataframe() + query_job = self._query_job + assert query_job is not None + return query_job.to_dataframe() def _create_column_comments( self, @@ -1021,20 +1052,23 @@ def _execute( job_config=job_config, timeout=self._extra_config.get("job_creation_timeout_seconds"), ) + query_job = self._query_job + assert query_job is not None logger.debug( "BigQuery job created: https://console.cloud.google.com/bigquery?project=%s&j=bq:%s:%s", - self._query_job.project, - self._query_job.location, - self._query_job.job_id, + query_job.project, + query_job.location, + query_job.job_id, ) results = self._db_call( - self._query_job.result, + query_job.result, timeout=self._extra_config.get("job_execution_timeout_seconds"), # type: ignore ) + self._query_data = iter(results) if results.total_rows else iter([]) - query_results = self._query_job._query_results + query_results = query_job._query_results self.cursor._set_rowcount(query_results) self.cursor._set_description(query_results.schema) @@ -1198,15 +1232,15 @@ def _query_data(self) -> t.Any: @_query_data.setter def _query_data(self, value: t.Any) -> None: - return self._connection_pool.set_attribute("query_data", value) + self._connection_pool.set_attribute("query_data", value) @property - def _query_job(self) -> t.Any: + def _query_job(self) -> t.Optional[QueryJob]: return self._connection_pool.get_attribute("query_job") @_query_job.setter def _query_job(self, value: t.Any) -> None: - return self._connection_pool.set_attribute("query_job", value) + self._connection_pool.set_attribute("query_job", value) @property def _session_id(self) -> t.Any: @@ -1214,7 +1248,7 @@ def _session_id(self) -> t.Any: @_session_id.setter def _session_id(self, value: t.Any) -> None: - return self._connection_pool.set_attribute("session_id", value) + self._connection_pool.set_attribute("session_id", value) class _ErrorCounter: diff --git a/sqlmesh/utils/connection_pool.py b/sqlmesh/utils/connection_pool.py index 54d62a2f8c..a4f9486184 100644 --- a/sqlmesh/utils/connection_pool.py +++ b/sqlmesh/utils/connection_pool.py @@ -48,6 +48,17 @@ def set_attribute(self, key: str, value: t.Any) -> None: value: Attribute value. """ + @abc.abstractmethod + def get_all_attributes(self, key: str) -> t.List[t.Any]: + """Returns all attributes with the given key across all connections/threads. + + Args: + key: Attribute key. + + Returns: + List of attribute values from all connections/threads. + """ + @abc.abstractmethod def begin(self) -> None: """Starts a new transaction.""" @@ -142,6 +153,14 @@ def set_attribute(self, key: str, value: t.Any) -> None: thread_id = get_ident() self._thread_attributes[thread_id][key] = value + def get_all_attributes(self, key: str) -> t.List[t.Any]: + """Returns all attributes with the given key across all threads.""" + return [ + thread_attrs[key] + for thread_attrs in self._thread_attributes.values() + if key in thread_attrs + ] + def begin(self) -> None: self._do_begin() with self._thread_transactions_lock: @@ -282,6 +301,11 @@ def get_attribute(self, key: str) -> t.Optional[t.Any]: def set_attribute(self, key: str, value: t.Any) -> None: self._attributes[key] = value + def get_all_attributes(self, key: str) -> t.List[t.Any]: + """Returns all attributes with the given key (single-threaded pool has at most one).""" + value = self._attributes.get(key) + return [value] if value is not None else [] + def begin(self) -> None: self._do_begin() self._is_transaction_active = True diff --git a/tests/core/engine_adapter/test_bigquery.py b/tests/core/engine_adapter/test_bigquery.py index e01e42049b..32377ac1de 100644 --- a/tests/core/engine_adapter/test_bigquery.py +++ b/tests/core/engine_adapter/test_bigquery.py @@ -1072,3 +1072,81 @@ def test_get_alter_expressions_includes_catalog( assert schema.db == "bar" assert schema.sql(dialect="bigquery") == "catalog2.bar" assert tables == {"bing"} + + +def test_job_cancellation_on_keyboard_interrupt_job_still_running(mocker: MockerFixture): + # Create a mock connection + connection_mock = mocker.NonCallableMock() + cursor_mock = mocker.Mock() + cursor_mock.connection = connection_mock + connection_mock.cursor.return_value = cursor_mock + + # Mock the query job + mock_job = mocker.Mock() + mock_job.project = "test-project" + mock_job.location = "us-central1" + mock_job.job_id = "test-job-123" + mock_job.done.return_value = False # Job is still running + mock_job.result.side_effect = KeyboardInterrupt() + mock_job._query_results = mocker.Mock() + mock_job._query_results.total_rows = 0 + mock_job._query_results.schema = [] + + # Set up the client to return our mock job + connection_mock._client.query.return_value = mock_job + + # Create adapter with the mocked connection + adapter = BigQueryEngineAdapter(lambda: connection_mock, job_retries=0) + + # Execute a query and expect KeyboardInterrupt + with pytest.raises(KeyboardInterrupt): + adapter.execute("SELECT 1") + + # Ensure the adapter's closed, so that the job can be aborted + adapter.close() + + # Verify the job was created + connection_mock._client.query.assert_called_once() + + # Verify job status was checked and cancellation was called + mock_job.done.assert_called_once() + mock_job.cancel.assert_called_once() + + +def test_job_cancellation_on_keyboard_interrupt_job_already_done(mocker: MockerFixture): + # Create a mock connection + connection_mock = mocker.NonCallableMock() + cursor_mock = mocker.Mock() + cursor_mock.connection = connection_mock + connection_mock.cursor.return_value = cursor_mock + + # Mock the query job + mock_job = mocker.Mock() + mock_job.project = "test-project" + mock_job.location = "us-central1" + mock_job.job_id = "test-job-456" + mock_job.done.return_value = True # Job is already done + mock_job.result.side_effect = KeyboardInterrupt() + mock_job._query_results = mocker.Mock() + mock_job._query_results.total_rows = 0 + mock_job._query_results.schema = [] + + # Set up the client to return our mock job + connection_mock._client.query.return_value = mock_job + + # Create adapter with the mocked connection + adapter = BigQueryEngineAdapter(lambda: connection_mock, job_retries=0) + + # Execute a query and expect KeyboardInterrupt + with pytest.raises(KeyboardInterrupt): + adapter.execute("SELECT 1") + + # Ensure the adapter's closed, so that the job can be aborted + adapter.close() + + # Verify the job was created + connection_mock._client.query.assert_called_once() + + # Verify job status was checked but cancellation was NOT called + mock_job.done.assert_called_once() + mock_job.cancel.assert_not_called() From 9760ae08924719d977f5ef9142534a5364af8f08 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 13 Aug 2025 11:47:13 +0300 Subject: [PATCH 0690/1056] Chore: Validate start and end dates during plan build time (#5100) --- sqlmesh/core/plan/builder.py | 20 +++++++++++++ tests/core/test_context.py | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 3dd74755d3..7918556bad 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -155,6 +155,7 @@ def __init__( self._backfill_models = backfill_models self._end = end or default_end + self._default_start = default_start self._apply = apply self._console = console or get_console() self._choices: t.Dict[SnapshotId, SnapshotChangeCategory] = {} @@ -802,6 +803,25 @@ def _ensure_valid_date_range(self) -> None: f"Plan end date: '{time_like_to_str(end)}' cannot be in the future (execution time: '{time_like_to_str(self.execution_time)}')" ) + # Validate model-specific start/end dates + if (start := self.start or self._default_start) and (end := self.end): + start_ts = to_datetime(start) + end_ts = to_datetime(end) + if start_ts > end_ts: + models_to_check: t.Set[str] = ( + set(self._backfill_models or []) + | set(self._context_diff.modified_snapshots.keys()) + | {s.name for s in self._context_diff.added} + | set((self._end_override_per_model or {}).keys()) + ) + for model_name in models_to_check: + if snapshot := self._model_fqn_to_snapshot.get(model_name): + if snapshot.node.start is None or to_datetime(snapshot.node.start) > end_ts: + raise PlanError( + f"Model '{model_name}': Start date / time '({time_like_to_str(start_ts)})' can't be greater than end date / time '({time_like_to_str(end_ts)})'.\n" + f"Set the `start` attribute in your project config model defaults to avoid this issue." + ) + def _ensure_no_broken_references(self) -> None: for snapshot in self._context_diff.snapshots.values(): broken_references = { diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 2827981ae7..852f00e760 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -3007,3 +3007,59 @@ def test_uppercase_gateway_external_models(tmp_path): assert len(uppercase_in_yaml_models) == 1, ( f"External model with uppercase gateway in YAML should be found. Found {len(uppercase_in_yaml_models)} models" ) + + +def test_plan_no_start_configured(): + context = Context(config=Config()) + context.upsert_model( + load_sql_based_model( + parse( + """ + MODEL( + name db.xvg, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds + ), + cron '@daily' + ); + + SELECT id, ds FROM (VALUES + ('1', '2020-01-01'), + ) data(id, ds) + WHERE ds BETWEEN @start_ds AND @end_ds + """ + ) + ) + ) + + prod_plan = context.plan(auto_apply=True) + assert len(prod_plan.new_snapshots) == 1 + + context.upsert_model( + load_sql_based_model( + parse( + """ + MODEL( + name db.xvg, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds + ), + cron '@daily', + physical_properties ('some_prop' = 1), + ); + + SELECT id, ds FROM (VALUES + ('1', '2020-01-01'), + ) data(id, ds) + WHERE ds BETWEEN @start_ds AND @end_ds + """ + ) + ) + ) + + # This should raise an error because the model has no start configured and the end time is less than the start time which will be calculated from the intervals + with pytest.raises( + PlanError, + match=r"Model '.*xvg.*': Start date / time .* can't be greater than end date / time .*\.\nSet the `start` attribute in your project config model defaults to avoid this issue", + ): + context.plan("dev", execution_time="1999-01-05") From ff16da14a9a12cb6ecf522eca1ad5add78e34d1d Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:47:40 +0300 Subject: [PATCH 0691/1056] Revert: Partial restatements for SCD type 2 models (#5138) --- docs/concepts/models/model_kinds.md | 48 +- sqlmesh/core/engine_adapter/base.py | 47 +- sqlmesh/core/engine_adapter/trino.py | 4 - sqlmesh/core/model/kind.py | 1 + sqlmesh/core/plan/evaluator.py | 6 - sqlmesh/core/scheduler.py | 9 - sqlmesh/core/snapshot/definition.py | 15 - sqlmesh/core/snapshot/evaluator.py | 9 - .../integration/test_integration.py | 4 - tests/core/engine_adapter/test_base.py | 634 +++++++++--------- tests/core/engine_adapter/test_clickhouse.py | 14 +- tests/core/engine_adapter/test_spark.py | 6 +- tests/core/test_integration.py | 180 +---- tests/core/test_model.py | 6 +- tests/core/test_snapshot_evaluator.py | 4 - 15 files changed, 348 insertions(+), 639 deletions(-) diff --git a/docs/concepts/models/model_kinds.md b/docs/concepts/models/model_kinds.md index e748313c81..d01cc738a6 100644 --- a/docs/concepts/models/model_kinds.md +++ b/docs/concepts/models/model_kinds.md @@ -935,13 +935,7 @@ SQLMesh achieves this by adding a `valid_from` and `valid_to` column to your mod Therefore, you can use these models to not only tell you what the latest value is for a given record but also what the values were anytime in the past. Note that maintaining this history does come at a cost of increased storage and compute and this may not be a good fit for sources that change frequently since the history could get very large. -**Note**: SCD Type 2 models support [restatements](../plans.md#restatement-plans) with specific limitations: - -- **Full restatements**: The entire table will be recreated from scratch when no start date is specified -- **Partial restatements**: You can specify a start date to restate data from a certain point onwards to the latest interval. The end date will always be set to the latest interval's end date, regardless of what end date you specify -- **Partial sections**: Restatements of specific sections (discontinued ranges) of the table are not supported - -Data restatement is disabled for models of this kind by default (`disable_restatement true`). To enable restatements, set `disable_restatement false` in your model configuration. +**Note**: Partial data [restatement](../plans.md#restatement-plans) is not supported for this model kind, which means that the entire table will be recreated from scratch if restated. This may lead to data loss, so data restatement is disabled for models of this kind by default. There are two ways to tracking changes: By Time (Recommended) or By Column. @@ -1289,11 +1283,11 @@ This is the most accurate representation of the menu based on the source data pr ### Processing Source Table with Historical Data -The most common case for SCD Type 2 is creating history for a table that it doesn't have it already. +The most common case for SCD Type 2 is creating history for a table that it doesn't have it already. In the example of the restaurant menu, the menu just tells you what is offered right now, but you want to know what was offered over time. In this case, the default setting of `None` for `batch_size` is the best option. -Another use case though is processing a source table that already has history in it. +Another use case though is processing a source table that already has history in it. A common example of this is a "daily snapshot" table that is created by a source system that takes a snapshot of the data at the end of each day. If your source table has historical records, like a "daily snapshot" table, then set `batch_size` to `1` to process each interval (each day if a `@daily` cron) in sequential order. That way the historical records will be properly captured in the SCD Type 2 table. @@ -1439,14 +1433,11 @@ GROUP BY id ``` -### SCD Type 2 Restatements +### Reset SCD Type 2 Model (clearing history) 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. - -#### Enabling Restatements - -To enable restatements for an SCD Type 2 model, set `disable_restatement` to `false` in the model definition: +For this use use case you will want to start by setting `disable_restatement` to `false` in the model definition. ```sql linenums="1" hl_lines="5" MODEL ( @@ -1458,9 +1449,8 @@ MODEL ( ); ``` -#### Full Restatements (Clearing All History) - -To clear all history and recreate the entire table from scratch: +Plan/apply this change to production. +Then you will want to [restate the model](../plans.md#restatement-plans). ```bash sqlmesh plan --restate-model db.menu_items @@ -1468,29 +1458,7 @@ sqlmesh plan --restate-model db.menu_items !!! warning - This will remove **all** historical data on the model which in most situations cannot be recovered. - -#### Partial Restatements (From a Specific Date) - -You can restate data from a specific start date onwards. This will: -- Delete all records with `valid_from >= start_date` -- Reprocess the data from the start date to the latest interval - -```bash -sqlmesh plan --restate-model db.menu_items --start "2023-01-15" -``` - -!!! note - - If you specify an end date for SCD Type 2 restatements, it will be ignored and automatically set to the latest interval's end date. - -```bash -# This end date will be ignored and set to the latest interval -sqlmesh plan --restate-model db.menu_items --start "2023-01-15" --end "2023-01-20" -``` - - -#### Re-enabling Protection + This will remove the historical data on the model which in most situations cannot be recovered. Once complete you will want to remove `disable_restatement` on the model definition which will set it back to `true` and prevent accidental data loss. diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index d401f0e705..4651caa6ec 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -1497,7 +1497,6 @@ def scd_type_2_by_time( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, - is_restatement: bool = False, **kwargs: t.Any, ) -> None: self._scd_type_2( @@ -1514,7 +1513,6 @@ def scd_type_2_by_time( table_description=table_description, column_descriptions=column_descriptions, truncate=truncate, - is_restatement=is_restatement, **kwargs, ) @@ -1533,7 +1531,6 @@ def scd_type_2_by_column( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, - is_restatement: bool = False, **kwargs: t.Any, ) -> None: self._scd_type_2( @@ -1550,7 +1547,6 @@ def scd_type_2_by_column( table_description=table_description, column_descriptions=column_descriptions, truncate=truncate, - is_restatement=is_restatement, **kwargs, ) @@ -1561,7 +1557,6 @@ def _scd_type_2( unique_key: t.Sequence[exp.Expression], valid_from_col: exp.Column, valid_to_col: exp.Column, - start: TimeLike, execution_time: t.Union[TimeLike, exp.Column], invalidate_hard_deletes: bool = True, updated_at_col: t.Optional[exp.Column] = None, @@ -1572,7 +1567,6 @@ def _scd_type_2( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, - is_restatement: bool = False, **kwargs: t.Any, ) -> None: def remove_managed_columns( @@ -1757,17 +1751,9 @@ def remove_managed_columns( existing_rows_query = exp.select(*table_columns, exp.true().as_("_exists")).from_( target_table ) - if truncate: existing_rows_query = existing_rows_query.limit(0) - # Only set cleanup_ts if is_restatement is True and truncate is False (this to enable full restatement) - cleanup_ts = ( - to_time_column(start, time_data_type, self.dialect, nullable=True) - if is_restatement and not truncate - else None - ) - with source_queries[0] as source_query: prefixed_columns_to_types = [] for column in columns_to_types: @@ -1804,41 +1790,12 @@ def remove_managed_columns( # Historical Records that Do Not Change .with_( "static", - existing_rows_query.where(valid_to_col.is_(exp.Null()).not_()) - if cleanup_ts is None - else existing_rows_query.where( - exp.and_( - valid_to_col.is_(exp.Null().not_()), - valid_to_col < cleanup_ts, - ), - ), + existing_rows_query.where(valid_to_col.is_(exp.Null()).not_()), ) # Latest Records that can be updated .with_( "latest", - existing_rows_query.where(valid_to_col.is_(exp.Null())) - if cleanup_ts is None - else exp.select( - *( - to_time_column( - exp.null(), time_data_type, self.dialect, nullable=True - ).as_(col) - if col == valid_to_col.name - else exp.column(col) - for col in columns_to_types - ), - exp.true().as_("_exists"), - ) - .from_(target_table) - .where( - exp.and_( - valid_from_col <= cleanup_ts, - exp.or_( - valid_to_col.is_(exp.null()), - valid_to_col >= cleanup_ts, - ), - ) - ), + existing_rows_query.where(valid_to_col.is_(exp.Null())), ) # Deleted records which can be used to determine `valid_from` for undeleted source records .with_( diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index 7862bfca2d..df8e45b520 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -256,7 +256,6 @@ def _scd_type_2( unique_key: t.Sequence[exp.Expression], valid_from_col: exp.Column, valid_to_col: exp.Column, - start: TimeLike, execution_time: t.Union[TimeLike, exp.Column], invalidate_hard_deletes: bool = True, updated_at_col: t.Optional[exp.Column] = None, @@ -267,7 +266,6 @@ def _scd_type_2( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, - is_restatement: bool = False, **kwargs: t.Any, ) -> None: if columns_to_types and self.current_catalog_type == "delta_lake": @@ -279,7 +277,6 @@ def _scd_type_2( unique_key, valid_from_col, valid_to_col, - start, execution_time, invalidate_hard_deletes, updated_at_col, @@ -290,7 +287,6 @@ def _scd_type_2( table_description, column_descriptions, truncate, - is_restatement, **kwargs, ) diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index c297d916d5..185556fc8f 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -141,6 +141,7 @@ def full_history_restatement_only(self) -> bool: self.is_incremental_unmanaged or self.is_incremental_by_unique_key or self.is_incremental_by_partition + or self.is_scd_type_2 or self.is_managed or self.is_full or self.is_view diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 39a5d69355..ced4631b99 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -235,11 +235,6 @@ def visit_backfill_stage(self, stage: stages.BackfillStage, plan: EvaluatablePla return scheduler = self.create_scheduler(stage.all_snapshots.values(), self.snapshot_evaluator) - # Convert model name restatements to snapshot ID restatements - restatements_by_snapshot_id = { - stage.all_snapshots[name].snapshot_id: interval - for name, interval in plan.restatements.items() - } errors, _ = scheduler.run_merged_intervals( merged_intervals=stage.snapshot_to_intervals, deployability_index=stage.deployability_index, @@ -248,7 +243,6 @@ def visit_backfill_stage(self, stage: stages.BackfillStage, plan: EvaluatablePla circuit_breaker=self._circuit_breaker, start=plan.start, end=plan.end, - restatements=restatements_by_snapshot_id, ) if errors: raise PlanError("Plan application failed.") diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index 7177efe927..4582b24485 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -161,7 +161,6 @@ def evaluate( deployability_index: DeployabilityIndex, batch_index: int, environment_naming_info: t.Optional[EnvironmentNamingInfo] = None, - is_restatement: bool = False, **kwargs: t.Any, ) -> t.List[AuditResult]: """Evaluate a snapshot and add the processed interval to the state sync. @@ -193,7 +192,6 @@ def evaluate( snapshots=snapshots, deployability_index=deployability_index, batch_index=batch_index, - is_restatement=is_restatement, **kwargs, ) audit_results = self._audit_snapshot( @@ -373,7 +371,6 @@ def run_merged_intervals( end: t.Optional[TimeLike] = None, run_environment_statements: bool = False, audit_only: bool = False, - restatements: t.Optional[t.Dict[SnapshotId, Interval]] = None, ) -> t.Tuple[t.List[NodeExecutionFailedError[SchedulingUnit]], t.List[SchedulingUnit]]: """Runs precomputed batches of missing intervals. @@ -450,10 +447,6 @@ def evaluate_node(node: SchedulingUnit) -> None: execution_time=execution_time, ) else: - # Determine if this snapshot and interval is a restatement (for SCD type 2) - is_restatement = ( - restatements is not None and snapshot.snapshot_id in restatements - ) audit_results = self.evaluate( snapshot=snapshot, environment_naming_info=environment_naming_info, @@ -462,7 +455,6 @@ def evaluate_node(node: SchedulingUnit) -> None: execution_time=execution_time, deployability_index=deployability_index, batch_index=batch_idx, - is_restatement=is_restatement, ) evaluation_duration_ms = now_timestamp() - execution_start_ts @@ -671,7 +663,6 @@ def _run_or_audit( end=end, run_environment_statements=run_environment_statements, audit_only=audit_only, - restatements=remove_intervals, ) return CompletionStatus.FAILURE if errors else CompletionStatus.SUCCESS diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 076c1efa78..996d539e60 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -837,21 +837,6 @@ def get_removal_interval( removal_interval = expanded_removal_interval - # SCD Type 2 validation that end date is the latest interval if it was provided - if not is_preview and self.is_scd_type_2 and self.intervals: - requested_start, requested_end = removal_interval - latest_end = self.intervals[-1][1] - if requested_end < latest_end: - from sqlmesh.core.console import get_console - - get_console().log_warning( - f"SCD Type 2 model '{self.model.name}' does not support end date in restatements.\n" - f"Requested end date [{to_ts(requested_end)}] is less than the latest interval end date.\n" - f"The requested end date will be ignored. Using the latest interval end instead: [{to_ts(latest_end)}]" - ) - - removal_interval = self.inclusive_exclusive(requested_start, latest_end, strict) - return removal_interval @property diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index a2ec242e37..a6dca27d35 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -140,7 +140,6 @@ def evaluate( snapshots: t.Dict[str, Snapshot], deployability_index: t.Optional[DeployabilityIndex] = None, batch_index: int = 0, - is_restatement: bool = False, **kwargs: t.Any, ) -> t.Optional[str]: """Renders the snapshot's model, executes it and stores the result in the snapshot's physical table. @@ -166,7 +165,6 @@ def evaluate( snapshots, deployability_index=deployability_index, batch_index=batch_index, - is_restatement=is_restatement, **kwargs, ) if result is None or isinstance(result, str): @@ -624,7 +622,6 @@ def _evaluate_snapshot( limit: t.Optional[int] = None, deployability_index: t.Optional[DeployabilityIndex] = None, batch_index: int = 0, - is_restatement: bool = False, **kwargs: t.Any, ) -> DF | str | None: """Renders the snapshot's model and executes it. The return value depends on whether the limit was specified. @@ -697,7 +694,6 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: end=end, execution_time=execution_time, physical_properties=rendered_physical_properties, - is_restatement=is_restatement, ) else: logger.info( @@ -719,7 +715,6 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: end=end, execution_time=execution_time, physical_properties=rendered_physical_properties, - is_restatement=is_restatement, ) with ( @@ -1840,8 +1835,6 @@ def insert( table_description=model.description, column_descriptions=model.column_descriptions, truncate=is_first_insert, - start=kwargs["start"], - is_restatement=kwargs.get("is_restatement", False), ) elif isinstance(model.kind, SCDType2ByColumnKind): self.adapter.scd_type_2_by_column( @@ -1859,8 +1852,6 @@ def insert( table_description=model.description, column_descriptions=model.column_descriptions, truncate=is_first_insert, - start=kwargs["start"], - is_restatement=kwargs.get("is_restatement", False), ) else: raise SQLMeshError( diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 0739256d6e..039159825b 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -744,7 +744,6 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): columns_to_types=input_schema, table_format=ctx.default_table_format, truncate=True, - start="2022-01-01 00:00:00", ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -808,7 +807,6 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): columns_to_types=input_schema, table_format=ctx.default_table_format, truncate=False, - start="2022-01-01 00:00:00", ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -901,7 +899,6 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): execution_time_as_valid_from=False, columns_to_types=ctx.columns_to_types, truncate=True, - start="2023-01-01", ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -973,7 +970,6 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): execution_time_as_valid_from=False, columns_to_types=ctx.columns_to_types, truncate=False, - start="2023-01-01", ) results = ctx.get_metadata_results() assert len(results.views) == 0 diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index b760a4f4a1..618d89a445 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -1239,12 +1239,10 @@ def test_scd_type_2_by_time(make_mocked_engine_adapter: t.Callable): "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), - start=datetime(2020, 1, 1, 0, 0, 0), - is_restatement=True, ) assert ( - parse_one(adapter.cursor.execute.call_args[0][0]).sql() + adapter.cursor.execute.call_args[0][0] == parse_one( """ CREATE OR REPLACE TABLE "target" AS @@ -1273,7 +1271,8 @@ def test_scd_type_2_by_time(make_mocked_engine_adapter: t.Callable): "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) + WHERE + NOT "test_valid_to" IS NULL ), "latest" AS ( SELECT "id", @@ -1281,11 +1280,11 @@ def test_scd_type_2_by_time(make_mocked_engine_adapter: t.Callable): "price", "test_UPDATED_at", "test_valid_from", - CAST(NULL AS TIMESTAMP) AS "test_valid_to", + "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE "test_valid_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) - AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) + WHERE + "test_valid_to" IS NULL ), "deleted" AS ( SELECT "static"."id", @@ -1439,12 +1438,10 @@ def test_scd_type_2_by_time_no_invalidate_hard_deletes(make_mocked_engine_adapte "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), - start=datetime(2020, 1, 1, 0, 0, 0), - is_restatement=True, ) assert ( - parse_one(adapter.cursor.execute.call_args[0][0]).sql() + adapter.cursor.execute.call_args[0][0] == parse_one( """ CREATE OR REPLACE TABLE "target" AS @@ -1473,7 +1470,8 @@ def test_scd_type_2_by_time_no_invalidate_hard_deletes(make_mocked_engine_adapte "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) + WHERE + NOT "test_valid_to" IS NULL ), "latest" AS ( SELECT "id", @@ -1481,11 +1479,11 @@ def test_scd_type_2_by_time_no_invalidate_hard_deletes(make_mocked_engine_adapte "price", "test_updated_at", "test_valid_from", - CAST(NULL AS TIMESTAMP) AS "test_valid_to", + "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE "test_valid_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) - AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) + WHERE + "test_valid_to" IS NULL ), "deleted" AS ( SELECT "static"."id", @@ -1628,38 +1626,35 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "test_valid_to": exp.DataType.build("TIMESTAMPTZ"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), - start=datetime(2020, 1, 1, 0, 0, 0), - is_restatement=True, ) assert ( - parse_one(adapter.cursor.execute.call_args[0][0]).sql() + adapter.cursor.execute.call_args[0][0] == parse_one( """ - CREATE OR REPLACE TABLE "target" AS - WITH "source" AS ( - SELECT DISTINCT ON ("id1", "id2") +CREATE OR REPLACE TABLE "target" AS +WITH "source" AS ( + SELECT DISTINCT ON ("id1", "id2") TRUE AS "_exists", "id1", "id2", "name", "price", CAST("test_updated_at" AS TIMESTAMPTZ) AS "test_updated_at" - FROM ( + FROM ( SELECT CAST("id1" AS INT) AS "id1", CAST("id2" AS INT) AS "id2", CAST("name" AS VARCHAR) AS "name", CAST("price" AS DOUBLE) AS "price", - CAST("test_updated_at" AS TIMESTAMPTZ) AS "test_updated_at" + CAST("test_updated_at" AS TIMESTAMPTZ) AS "test_updated_at", FROM (VALUES (1, 4, 'muffins', 4.0, '2020-01-01 10:00:00'), (2, 5, 'chips', 5.0, '2020-01-02 15:00:00'), - (3, 6, 'soda', 6.0, '2020-01-03 12:00:00') - ) AS "t"("id1", "id2", "name", "price", "test_updated_at") - ) AS "raw_source" - ), "static" AS ( - SELECT + (3, 6, 'soda', 6.0, '2020-01-03 12:00:00')) AS "t"("id1", "id2", "name", "price", "test_updated_at") + ) AS "raw_source" +), "static" AS ( + SELECT "id1", "id2", "name", @@ -1668,23 +1663,24 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "test_valid_from", "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00+00:00' AS TIMESTAMPTZ) - ), "latest" AS ( - SELECT + FROM "target" + WHERE + NOT "test_valid_to" IS NULL +), "latest" AS ( + SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", - CAST(NULL AS TIMESTAMPTZ) AS "test_valid_to", + "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE "test_valid_from" <= CAST('2020-01-01 00:00:00+00:00' AS TIMESTAMPTZ) - AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00+00:00' AS TIMESTAMPTZ)) - ), "deleted" AS ( - SELECT + FROM "target" + WHERE + "test_valid_to" IS NULL +), "deleted" AS ( + SELECT "static"."id1", "static"."id2", "static"."name", @@ -1692,20 +1688,23 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "static"."test_updated_at", "static"."test_valid_from", "static"."test_valid_to" - FROM "static" - LEFT JOIN "latest" + FROM "static" + LEFT JOIN "latest" ON "static"."id1" = "latest"."id1" AND "static"."id2" = "latest"."id2" - WHERE "latest"."test_valid_to" IS NULL - ), "latest_deleted" AS ( - SELECT + WHERE + "latest"."test_valid_to" IS NULL +), "latest_deleted" AS ( + SELECT TRUE AS "_exists", "id1" AS "_key0", "id2" AS "_key1", MAX("test_valid_to") AS "test_valid_to" - FROM "deleted" - GROUP BY "id1", "id2" - ), "joined" AS ( - SELECT + FROM "deleted" + GROUP BY + "id1", + "id2" +), "joined" AS ( + SELECT "source"."_exists" AS "_exists", "latest"."id1" AS "t_id1", "latest"."id2" AS "t_id2", @@ -1719,11 +1718,11 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "source"."name" AS "name", "source"."price" AS "price", "source"."test_updated_at" AS "test_updated_at" - FROM "latest" - LEFT JOIN "source" + FROM "latest" + LEFT JOIN "source" ON "latest"."id1" = "source"."id1" AND "latest"."id2" = "source"."id2" - UNION ALL - SELECT + UNION ALL + SELECT "source"."_exists" AS "_exists", "latest"."id1" AS "t_id1", "latest"."id2" AS "t_id2", @@ -1737,12 +1736,13 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "source"."name" AS "name", "source"."price" AS "price", "source"."test_updated_at" AS "test_updated_at" - FROM "latest" - RIGHT JOIN "source" + FROM "latest" + RIGHT JOIN "source" ON "latest"."id1" = "source"."id1" AND "latest"."id2" = "source"."id2" - WHERE "latest"."_exists" IS NULL - ), "updated_rows" AS ( - SELECT + WHERE + "latest"."_exists" IS NULL +), "updated_rows" AS ( + SELECT COALESCE("joined"."t_id1", "joined"."id1") AS "id1", COALESCE("joined"."t_id2", "joined"."id2") AS "id2", COALESCE("joined"."t_name", "joined"."name") AS "name", @@ -1751,9 +1751,9 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): CASE WHEN "t_test_valid_from" IS NULL AND NOT "latest_deleted"."_exists" IS NULL THEN CASE - WHEN "latest_deleted"."test_valid_to" > "test_updated_at" - THEN "latest_deleted"."test_valid_to" - ELSE "test_updated_at" + WHEN "latest_deleted"."test_valid_to" > "test_updated_at" + THEN "latest_deleted"."test_valid_to" + ELSE "test_updated_at" END WHEN "t_test_valid_from" IS NULL THEN CAST('1970-01-01 00:00:00+00:00' AS TIMESTAMPTZ) @@ -1766,11 +1766,12 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): THEN CAST('2020-01-01 00:00:00+00:00' AS TIMESTAMPTZ) ELSE "t_test_valid_to" END AS "test_valid_to" - FROM "joined" - LEFT JOIN "latest_deleted" - ON "joined"."id1" = "latest_deleted"."_key0" AND "joined"."id2" = "latest_deleted"."_key1" - ), "inserted_rows" AS ( - SELECT + FROM "joined" + LEFT JOIN "latest_deleted" + ON "joined"."id1" = "latest_deleted"."_key0" + AND "joined"."id2" = "latest_deleted"."_key1" +), "inserted_rows" AS ( + SELECT "id1", "id2", "name", @@ -1778,23 +1779,12 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): "test_updated_at", "test_updated_at" AS "test_valid_from", CAST(NULL AS TIMESTAMPTZ) AS "test_valid_to" - FROM "joined" - WHERE "joined"."test_updated_at" > "joined"."t_test_updated_at" - ) - SELECT - CAST("id1" AS INT) AS "id1", - CAST("id2" AS INT) AS "id2", - CAST("name" AS VARCHAR) AS "name", - CAST("price" AS DOUBLE) AS "price", - CAST("test_updated_at" AS TIMESTAMPTZ) AS "test_updated_at", - CAST("test_valid_from" AS TIMESTAMPTZ) AS "test_valid_from", - CAST("test_valid_to" AS TIMESTAMPTZ) AS "test_valid_to" - FROM ( - SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "static" - UNION ALL SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "updated_rows" - UNION ALL SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "inserted_rows" - ) AS "_subquery" - """ + FROM "joined" + WHERE + "joined"."test_updated_at" > "joined"."t_test_updated_at" +) +SELECT CAST("id1" AS INT) AS "id1", CAST("id2" AS INT) AS "id2", CAST("name" AS VARCHAR) AS "name", CAST("price" AS DOUBLE) AS "price", CAST("test_updated_at" AS TIMESTAMPTZ) AS "test_updated_at", CAST("test_valid_from" AS TIMESTAMPTZ) AS "test_valid_from", CAST("test_valid_to" AS TIMESTAMPTZ) AS "test_valid_to" FROM (SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "static" UNION ALL SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "updated_rows" UNION ALL SELECT "id1", "id2", "name", "price", "test_updated_at", "test_valid_from", "test_valid_to" FROM "inserted_rows") AS "_subquery" +""" ).sql() ) @@ -1817,72 +1807,71 @@ def test_scd_type_2_by_column(make_mocked_engine_adapter: t.Callable): "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), - start=datetime(2020, 1, 1, 0, 0, 0), extra_col_ignore="testing", - is_restatement=True, ) assert ( - parse_one(adapter.cursor.execute.call_args[0][0]).sql() + adapter.cursor.execute.call_args[0][0] == parse_one( """ - CREATE OR REPLACE TABLE "target" AS - WITH "source" AS ( - SELECT DISTINCT ON ("id") +CREATE OR REPLACE TABLE "target" AS +WITH "source" AS ( + SELECT DISTINCT ON ("id") TRUE AS "_exists", "id", "name", "price" - FROM ( + FROM ( SELECT "id", "name", "price" FROM "source" - ) AS "raw_source" - ), "static" AS ( - SELECT + ) AS "raw_source" +), "static" AS ( + SELECT "id", "name", "price", "test_VALID_from", "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) - ), "latest" AS ( - SELECT + FROM "target" + WHERE + NOT "test_valid_to" IS NULL +), "latest" AS ( + SELECT "id", "name", "price", "test_VALID_from", - CAST(NULL AS TIMESTAMP) AS "test_valid_to", + "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE "test_VALID_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) - AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) - ), "deleted" AS ( - SELECT + FROM "target" + WHERE + "test_valid_to" IS NULL +), "deleted" AS ( + SELECT "static"."id", "static"."name", "static"."price", "static"."test_VALID_from", "static"."test_valid_to" - FROM "static" - LEFT JOIN "latest" + FROM "static" + LEFT JOIN "latest" ON "static"."id" = "latest"."id" - WHERE + WHERE "latest"."test_valid_to" IS NULL - ), "latest_deleted" AS ( - SELECT +), "latest_deleted" AS ( + SELECT TRUE AS "_exists", "id" AS "_key0", MAX("test_valid_to") AS "test_valid_to" - FROM "deleted" - GROUP BY + FROM "deleted" + GROUP BY "id" - ), "joined" AS ( - SELECT +), "joined" AS ( + SELECT "source"."_exists" AS "_exists", "latest"."id" AS "t_id", "latest"."name" AS "t_name", @@ -1892,11 +1881,11 @@ def test_scd_type_2_by_column(make_mocked_engine_adapter: t.Callable): "source"."id" AS "id", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - LEFT JOIN "source" + FROM "latest" + LEFT JOIN "source" ON "latest"."id" = "source"."id" - UNION ALL - SELECT + UNION ALL + SELECT "source"."_exists" AS "_exists", "latest"."id" AS "t_id", "latest"."name" AS "t_name", @@ -1906,13 +1895,13 @@ def test_scd_type_2_by_column(make_mocked_engine_adapter: t.Callable): "source"."id" AS "id", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - RIGHT JOIN "source" + FROM "latest" + RIGHT JOIN "source" ON "latest"."id" = "source"."id" - WHERE + WHERE "latest"."_exists" IS NULL - ), "updated_rows" AS ( - SELECT +), "updated_rows" AS ( + SELECT COALESCE("joined"."t_id", "joined"."id") AS "id", COALESCE("joined"."t_name", "joined"."name") AS "name", COALESCE("joined"."t_price", "joined"."price") AS "price", @@ -1920,73 +1909,63 @@ def test_scd_type_2_by_column(make_mocked_engine_adapter: t.Callable): CASE WHEN "joined"."_exists" IS NULL OR ( - ( - NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL - ) - AND ( - "joined"."name" <> "joined"."t_name" - OR ( - "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL - ) - OR ( - NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL - ) - OR "joined"."price" <> "joined"."t_price" - OR ( - "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL + ( + NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL ) - OR ( - NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL + AND ( + "joined"."name" <> "joined"."t_name" + OR ( + "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL + ) + OR ( + NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL + ) + OR "joined"."price" <> "joined"."t_price" + OR ( + "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL + ) + OR ( + NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL + ) ) ) - ) THEN CAST('2020-01-01 00:00:00' AS TIMESTAMP) ELSE "t_test_valid_to" END AS "test_valid_to" - FROM "joined" - LEFT JOIN "latest_deleted" + FROM "joined" + LEFT JOIN "latest_deleted" ON "joined"."id" = "latest_deleted"."_key0" - ), "inserted_rows" AS ( - SELECT +), "inserted_rows" AS ( + SELECT "id", "name", "price", CAST('2020-01-01 00:00:00' AS TIMESTAMP) AS "test_VALID_from", CAST(NULL AS TIMESTAMP) AS "test_valid_to" - FROM "joined" - WHERE + FROM "joined" + WHERE ( NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL ) AND ( "joined"."name" <> "joined"."t_name" OR ( - "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL + "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL ) OR ( - NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL + NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL ) OR "joined"."price" <> "joined"."t_price" OR ( - "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL + "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL ) OR ( - NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL + NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL ) ) - ) - SELECT - CAST("id" AS INT) AS "id", - CAST("name" AS VARCHAR) AS "name", - CAST("price" AS DOUBLE) AS "price", - CAST("test_VALID_from" AS TIMESTAMP) AS "test_VALID_from", - CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" - FROM ( - SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "static" - UNION ALL SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "updated_rows" - UNION ALL SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "inserted_rows" - ) AS "_subquery" - """ +) +SELECT CAST("id" AS INT) AS "id", CAST("name" AS VARCHAR) AS "name", CAST("price" AS DOUBLE) AS "price", CAST("test_VALID_from" AS TIMESTAMP) AS "test_VALID_from", CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" FROM (SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "static" UNION ALL SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "updated_rows" UNION ALL SELECT "id", "name", "price", "test_VALID_from", "test_valid_to" FROM "inserted_rows") AS "_subquery" + """ ).sql() ) @@ -2010,31 +1989,30 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), - start=datetime(2020, 1, 1, 0, 0, 0), - is_restatement=True, ) + assert ( - parse_one(adapter.cursor.execute.call_args[0][0]).sql() + adapter.cursor.execute.call_args[0][0] == parse_one( """ - CREATE OR REPLACE TABLE "target" AS - WITH "source" AS ( - SELECT DISTINCT ON (CONCAT("id_a", "id_b")) +CREATE OR REPLACE TABLE "target" AS +WITH "source" AS ( + SELECT DISTINCT ON (CONCAT("id_a", "id_b")) TRUE AS "_exists", "id_a", "id_b", "name", - "price" - FROM ( + "price", + FROM ( SELECT "id_a", "id_b", "name", "price" FROM "source" - ) AS "raw_source" - ), "static" AS ( - SELECT + ) AS "raw_source" +), "static" AS ( + SELECT "id_a", "id_b", "name", @@ -2042,41 +2020,44 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab "test_VALID_from", "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) - ), "latest" AS ( - SELECT + FROM "target" + WHERE + NOT "test_valid_to" IS NULL +), "latest" AS ( + SELECT "id_a", "id_b", "name", "price", "test_VALID_from", - CAST(NULL AS TIMESTAMP) AS "test_valid_to", + "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE "test_VALID_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) - AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) - ), "deleted" AS ( - SELECT + FROM "target" + WHERE + "test_valid_to" IS NULL +), "deleted" AS ( + SELECT "static"."id_a", "static"."id_b", "static"."name", "static"."price", "static"."test_VALID_from", "static"."test_valid_to" - FROM "static" - LEFT JOIN "latest" + FROM "static" + LEFT JOIN "latest" ON CONCAT("static"."id_a", "static"."id_b") = CONCAT("latest"."id_a", "latest"."id_b") - WHERE "latest"."test_valid_to" IS NULL - ), "latest_deleted" AS ( - SELECT + WHERE + "latest"."test_valid_to" IS NULL +), "latest_deleted" AS ( + SELECT TRUE AS "_exists", CONCAT("id_a", "id_b") AS "_key0", MAX("test_valid_to") AS "test_valid_to" - FROM "deleted" - GROUP BY CONCAT("id_a", "id_b") - ), "joined" AS ( - SELECT + FROM "deleted" + GROUP BY + CONCAT("id_a", "id_b") +), "joined" AS ( + SELECT "source"."_exists" AS "_exists", "latest"."id_a" AS "t_id_a", "latest"."id_b" AS "t_id_b", @@ -2088,11 +2069,11 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab "source"."id_b" AS "id_b", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - LEFT JOIN "source" + FROM "latest" + LEFT JOIN "source" ON CONCAT("latest"."id_a", "latest"."id_b") = CONCAT("source"."id_a", "source"."id_b") - UNION ALL - SELECT + UNION ALL + SELECT "source"."_exists" AS "_exists", "latest"."id_a" AS "t_id_a", "latest"."id_b" AS "t_id_b", @@ -2104,12 +2085,13 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab "source"."id_b" AS "id_b", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - RIGHT JOIN "source" + FROM "latest" + RIGHT JOIN "source" ON CONCAT("latest"."id_a", "latest"."id_b") = CONCAT("source"."id_a", "source"."id_b") - WHERE "latest"."_exists" IS NULL - ), "updated_rows" AS ( - SELECT + WHERE + "latest"."_exists" IS NULL +), "updated_rows" AS ( + SELECT COALESCE("joined"."t_id_a", "joined"."id_a") AS "id_a", COALESCE("joined"."t_id_b", "joined"."id_b") AS "id_b", COALESCE("joined"."t_name", "joined"."name") AS "name", @@ -2118,55 +2100,64 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab CASE WHEN "joined"."_exists" IS NULL OR ( - (NOT CONCAT("t_id_a", "t_id_b") IS NULL AND NOT CONCAT("id_a", "id_b") IS NULL) + ( + NOT CONCAT("t_id_a", "t_id_b") IS NULL AND NOT CONCAT("id_a", "id_b") IS NULL + ) AND ( - "joined"."name" <> "joined"."t_name" - OR ("joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL) - OR (NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL) - OR "joined"."price" <> "joined"."t_price" - OR ("joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL) - OR (NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL) + "joined"."name" <> "joined"."t_name" + OR ( + "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL + ) + OR ( + NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL + ) + OR "joined"."price" <> "joined"."t_price" + OR ( + "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL + ) + OR ( + NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL + ) ) ) THEN CAST('2020-01-01 00:00:00' AS TIMESTAMP) ELSE "t_test_valid_to" END AS "test_valid_to" - FROM "joined" - LEFT JOIN "latest_deleted" + FROM "joined" + LEFT JOIN "latest_deleted" ON CONCAT("joined"."id_a", "joined"."id_b") = "latest_deleted"."_key0" - ), "inserted_rows" AS ( - SELECT +), "inserted_rows" AS ( + SELECT "id_a", "id_b", "name", "price", CAST('2020-01-01 00:00:00' AS TIMESTAMP) AS "test_VALID_from", CAST(NULL AS TIMESTAMP) AS "test_valid_to" - FROM "joined" - WHERE - (NOT CONCAT("t_id_a", "t_id_b") IS NULL AND NOT CONCAT("id_a", "id_b") IS NULL) + FROM "joined" + WHERE + ( + NOT CONCAT("t_id_a", "t_id_b") IS NULL AND NOT CONCAT("id_a", "id_b") IS NULL + ) AND ( "joined"."name" <> "joined"."t_name" - OR ("joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL) - OR (NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL) + OR ( + "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL + ) + OR ( + NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL + ) OR "joined"."price" <> "joined"."t_price" - OR ("joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL) - OR (NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL) + OR ( + "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL + ) + OR ( + NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL + ) ) - ) - SELECT - CAST("id_a" AS VARCHAR) AS "id_a", - CAST("id_b" AS VARCHAR) AS "id_b", - CAST("name" AS VARCHAR) AS "name", - CAST("price" AS DOUBLE) AS "price", - CAST("test_VALID_from" AS TIMESTAMP) AS "test_VALID_from", - CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" - FROM ( - SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "static" - UNION ALL SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "updated_rows" - UNION ALL SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "inserted_rows" - ) AS "_subquery" - """ +) +SELECT CAST("id_a" AS VARCHAR) AS "id_a", CAST("id_b" AS VARCHAR) AS "id_b", CAST("name" AS VARCHAR) AS "name", CAST("price" AS DOUBLE) AS "price", CAST("test_VALID_from" AS TIMESTAMP) AS "test_VALID_from", CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" FROM (SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "static" UNION ALL SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "updated_rows" UNION ALL SELECT "id_a", "id_b", "name", "price", "test_VALID_from", "test_valid_to" FROM "inserted_rows") AS "_subquery" + """ ).sql() ) @@ -2190,7 +2181,6 @@ def test_scd_type_2_truncate(make_mocked_engine_adapter: t.Callable): }, execution_time=datetime(2020, 1, 1, 0, 0, 0), truncate=True, - start=datetime(2020, 1, 1, 0, 0, 0), ) assert ( @@ -2373,70 +2363,70 @@ def test_scd_type_2_by_column_star_check(make_mocked_engine_adapter: t.Callable) "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), - start=datetime(2020, 1, 1, 0, 0, 0), - is_restatement=True, ) assert ( - parse_one(adapter.cursor.execute.call_args[0][0]).sql() + adapter.cursor.execute.call_args[0][0] == parse_one( """ - CREATE OR REPLACE TABLE "target" AS - WITH "source" AS ( - SELECT DISTINCT ON ("id") +CREATE OR REPLACE TABLE "target" AS +WITH "source" AS ( + SELECT DISTINCT ON ("id") TRUE AS "_exists", "id", "name", "price" - FROM ( + FROM ( SELECT "id", "name", "price" FROM "source" - ) AS "raw_source" - ), "static" AS ( - SELECT + ) AS "raw_source" +), "static" AS ( + SELECT "id", "name", "price", "test_valid_from", "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) - ), "latest" AS ( - SELECT + FROM "target" + WHERE + NOT "test_valid_to" IS NULL +), "latest" AS ( + SELECT "id", "name", "price", "test_valid_from", - CAST(NULL AS TIMESTAMP) AS "test_valid_to", + "test_valid_to", TRUE AS "_exists" - FROM "target" - WHERE "test_valid_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) - AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) - ), "deleted" AS ( - SELECT + FROM "target" + WHERE + "test_valid_to" IS NULL +), "deleted" AS ( + SELECT "static"."id", "static"."name", "static"."price", "static"."test_valid_from", "static"."test_valid_to" - FROM "static" - LEFT JOIN "latest" + FROM "static" + LEFT JOIN "latest" ON "static"."id" = "latest"."id" - WHERE "latest"."test_valid_to" IS NULL - ), "latest_deleted" AS ( - SELECT + WHERE + "latest"."test_valid_to" IS NULL +), "latest_deleted" AS ( + SELECT TRUE AS "_exists", "id" AS "_key0", MAX("test_valid_to") AS "test_valid_to" - FROM "deleted" - GROUP BY + FROM "deleted" + GROUP BY "id" - ), "joined" AS ( - SELECT +), "joined" AS ( + SELECT "source"."_exists" AS "_exists", "latest"."id" AS "t_id", "latest"."name" AS "t_name", @@ -2446,11 +2436,11 @@ def test_scd_type_2_by_column_star_check(make_mocked_engine_adapter: t.Callable) "source"."id" AS "id", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - LEFT JOIN "source" + FROM "latest" + LEFT JOIN "source" ON "latest"."id" = "source"."id" - UNION ALL - SELECT + UNION ALL + SELECT "source"."_exists" AS "_exists", "latest"."id" AS "t_id", "latest"."name" AS "t_name", @@ -2460,12 +2450,13 @@ def test_scd_type_2_by_column_star_check(make_mocked_engine_adapter: t.Callable) "source"."id" AS "id", "source"."name" AS "name", "source"."price" AS "price" - FROM "latest" - RIGHT JOIN "source" + FROM "latest" + RIGHT JOIN "source" ON "latest"."id" = "source"."id" - WHERE "latest"."_exists" IS NULL - ), "updated_rows" AS ( - SELECT + WHERE + "latest"."_exists" IS NULL +), "updated_rows" AS ( + SELECT COALESCE("joined"."t_id", "joined"."id") AS "id", COALESCE("joined"."t_name", "joined"."name") AS "name", COALESCE("joined"."t_price", "joined"."price") AS "price", @@ -2473,59 +2464,77 @@ def test_scd_type_2_by_column_star_check(make_mocked_engine_adapter: t.Callable) CASE WHEN "joined"."_exists" IS NULL OR ( - (NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL) - AND ( - "joined"."id" <> "joined"."t_id" - OR ("joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL) - OR (NOT "joined"."t_id" IS NULL AND "joined"."id" IS NULL) - OR "joined"."name" <> "joined"."t_name" - OR ("joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL) - OR (NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL) - OR "joined"."price" <> "joined"."t_price" - OR ("joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL) - OR (NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL) - ) + ( + NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL + ) + AND ( + "joined"."id" <> "joined"."t_id" + OR ( + "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL + ) + OR ( + NOT "joined"."t_id" IS NULL AND "joined"."id" IS NULL + ) + OR "joined"."name" <> "joined"."t_name" + OR ( + "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL + ) + OR ( + NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL + ) + OR "joined"."price" <> "joined"."t_price" + OR ( + "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL + ) + OR ( + NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL + ) + ) ) THEN CAST('2020-01-01 00:00:00' AS TIMESTAMP) ELSE "t_test_valid_to" END AS "test_valid_to" - FROM "joined" - LEFT JOIN "latest_deleted" + FROM "joined" + LEFT JOIN "latest_deleted" ON "joined"."id" = "latest_deleted"."_key0" - ), "inserted_rows" AS ( - SELECT +), "inserted_rows" AS ( + SELECT "id", "name", "price", CAST('2020-01-01 00:00:00' AS TIMESTAMP) AS "test_valid_from", CAST(NULL AS TIMESTAMP) AS "test_valid_to" - FROM "joined" - WHERE - (NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL) + FROM "joined" + WHERE + ( + NOT "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL + ) AND ( "joined"."id" <> "joined"."t_id" - OR ("joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL) - OR (NOT "joined"."t_id" IS NULL AND "joined"."id" IS NULL) + OR ( + "joined"."t_id" IS NULL AND NOT "joined"."id" IS NULL + ) + OR ( + NOT "joined"."t_id" IS NULL AND "joined"."id" IS NULL + ) OR "joined"."name" <> "joined"."t_name" - OR ("joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL) - OR (NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL) + OR ( + "joined"."t_name" IS NULL AND NOT "joined"."name" IS NULL + ) + OR ( + NOT "joined"."t_name" IS NULL AND "joined"."name" IS NULL + ) OR "joined"."price" <> "joined"."t_price" - OR ("joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL) - OR (NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL) + OR ( + "joined"."t_price" IS NULL AND NOT "joined"."price" IS NULL + ) + OR ( + NOT "joined"."t_price" IS NULL AND "joined"."price" IS NULL + ) ) - ) - SELECT - CAST("id" AS INT) AS "id", - CAST("name" AS VARCHAR) AS "name", - CAST("price" AS DOUBLE) AS "price", - CAST("test_valid_from" AS TIMESTAMP) AS "test_valid_from", - CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" - FROM ( - SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "static" - UNION ALL SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "updated_rows" - UNION ALL SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "inserted_rows" - ) AS "_subquery" - """ +) +SELECT CAST("id" AS INT) AS "id", CAST("name" AS VARCHAR) AS "name", CAST("price" AS DOUBLE) AS "price", CAST("test_valid_from" AS TIMESTAMP) AS "test_valid_from", CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" FROM (SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "static" UNION ALL SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "updated_rows" UNION ALL SELECT "id", "name", "price", "test_valid_from", "test_valid_to" FROM "inserted_rows") AS "_subquery" + """ ).sql() ) @@ -2549,12 +2558,10 @@ def test_scd_type_2_by_column_no_invalidate_hard_deletes(make_mocked_engine_adap "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), - start=datetime(2020, 1, 1, 0, 0, 0), - is_restatement=True, ) assert ( - parse_one(adapter.cursor.execute.call_args[0][0]).sql() + adapter.cursor.execute.call_args[0][0] == parse_one( """ CREATE OR REPLACE TABLE "target" AS @@ -2580,18 +2587,19 @@ def test_scd_type_2_by_column_no_invalidate_hard_deletes(make_mocked_engine_adap "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE NOT "test_valid_to" IS NULL AND "test_valid_to" < CAST('2020-01-01 00:00:00' AS TIMESTAMP) + WHERE + NOT "test_valid_to" IS NULL ), "latest" AS ( SELECT "id", "name", "price", "test_valid_from", - CAST(NULL AS TIMESTAMP) AS "test_valid_to", + "test_valid_to", TRUE AS "_exists" FROM "target" - WHERE "test_valid_from" <= CAST('2020-01-01 00:00:00' AS TIMESTAMP) - AND ("test_valid_to" IS NULL OR "test_valid_to" >= CAST('2020-01-01 00:00:00' AS TIMESTAMP)) + WHERE + "test_valid_to" IS NULL ), "deleted" AS ( SELECT "static"."id", diff --git a/tests/core/engine_adapter/test_clickhouse.py b/tests/core/engine_adapter/test_clickhouse.py index a0cd33af70..973e178820 100644 --- a/tests/core/engine_adapter/test_clickhouse.py +++ b/tests/core/engine_adapter/test_clickhouse.py @@ -612,8 +612,6 @@ def test_scd_type_2_by_time( "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), - start=datetime(2020, 1, 1, 0, 0, 0), - truncate=True, ) assert to_sql_calls(adapter)[3] == parse_one( @@ -645,7 +643,7 @@ def test_scd_type_2_by_time( TRUE AS "_exists" FROM ""__temp_target_efgh"" WHERE - NOT "test_valid_to" IS NULL LIMIT 0 + NOT "test_valid_to" IS NULL ), "latest" AS ( SELECT "id", @@ -657,7 +655,7 @@ def test_scd_type_2_by_time( TRUE AS "_exists" FROM ""__temp_target_efgh"" WHERE - "test_valid_to" IS NULL LIMIT 0 + "test_valid_to" IS NULL ), "deleted" AS ( SELECT "static"."id", @@ -825,8 +823,6 @@ def test_scd_type_2_by_column( "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), - start=datetime(2020, 1, 1, 0, 0, 0), - truncate=True, ) assert to_sql_calls(adapter)[3] == parse_one( @@ -856,7 +852,7 @@ def test_scd_type_2_by_column( TRUE AS "_exists" FROM "__temp_target_efgh" WHERE - NOT ("test_valid_to" IS NULL) LIMIT 0 + NOT "test_valid_to" IS NULL ), "latest" AS ( SELECT "id", @@ -867,7 +863,7 @@ def test_scd_type_2_by_column( TRUE AS "_exists" FROM "__temp_target_efgh" WHERE - "test_valid_to" IS NULL LIMIT 0 + "test_valid_to" IS NULL ), "deleted" AS ( SELECT "static"."id", @@ -923,7 +919,7 @@ def test_scd_type_2_by_column( COALESCE("joined"."t_id", "joined"."id") AS "id", COALESCE("joined"."t_name", "joined"."name") AS "name", COALESCE("joined"."t_price", "joined"."price") AS "price", - COALESCE("t_test_VALID_from", CAST('1970-01-01 00:00:00' AS Nullable(DateTime64(6)))) AS "test_VALID_from", + COALESCE("t_test_VALID_from", CAST('2020-01-01 00:00:00' AS Nullable(DateTime64(6)))) AS "test_VALID_from", CASE WHEN "joined"."_exists" IS NULL OR ( diff --git a/tests/core/engine_adapter/test_spark.py b/tests/core/engine_adapter/test_spark.py index 2ef70a6929..468de9f75a 100644 --- a/tests/core/engine_adapter/test_spark.py +++ b/tests/core/engine_adapter/test_spark.py @@ -591,8 +591,6 @@ def check_table_exists(table_name: exp.Table) -> bool: "test_valid_to": exp.DataType.build("TIMESTAMP"), }, execution_time=datetime(2020, 1, 1, 0, 0, 0), - start=datetime(2020, 1, 1, 0, 0, 0), - truncate=True, ) assert to_sql_calls(adapter) == [ @@ -637,7 +635,7 @@ def check_table_exists(table_name: exp.Table) -> bool: TRUE AS `_exists` FROM `db`.`temp_target_abcdefgh` WHERE - NOT `test_valid_to` IS NULL LIMIT 0 + NOT `test_valid_to` IS NULL ), `latest` AS ( SELECT `id`, @@ -649,7 +647,7 @@ def check_table_exists(table_name: exp.Table) -> bool: TRUE AS `_exists` FROM `db`.`temp_target_abcdefgh` WHERE - `test_valid_to` IS NULL LIMIT 0 + `test_valid_to` IS NULL ), `deleted` AS ( SELECT `static`.`id`, diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 5a0e7bdf48..856406a16d 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -6859,169 +6859,6 @@ def plan_with_output(ctx: Context, environment: str): assert context_diff.environment == environment -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_scd_type_2_restatement(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - raw_employee_status = d.parse(""" - MODEL ( - name memory.hr_system.raw_employee_status, - kind FULL - ); - - SELECT - 1001 AS employee_id, - 'engineering' AS department, - 'EMEA' AS region, - '2023-01-08 15:00:00 UTC' AS last_modified; - """) - - # Create SCD Type 2 model for employee history tracking - employee_history = d.parse(""" - MODEL ( - name memory.hr_system.employee_history, - kind SCD_TYPE_2_BY_TIME ( - unique_key employee_id, - updated_at_name last_modified, - disable_restatement false - ), - owner hr_analytics, - cron '*/5 * * * *', - grain employee_id, - description 'Historical tracking of employee status changes' - ); - - SELECT - employee_id::INT AS employee_id, - department::TEXT AS department, - region::TEXT AS region, - last_modified AS last_modified - FROM - memory.hr_system.raw_employee_status; - """) - - raw_employee_status_model = load_sql_based_model(raw_employee_status) - employee_history_model = load_sql_based_model(employee_history) - context.upsert_model(raw_employee_status_model) - context.upsert_model(employee_history_model) - - # Initial plan and apply - plan = context.plan_builder("prod", skip_tests=True).build() - context.apply(plan) - - query = "SELECT employee_id, department, region, valid_from, valid_to FROM memory.hr_system.employee_history ORDER BY employee_id, valid_from" - initial_data = context.engine_adapter.fetchdf(query) - - assert len(initial_data) == 1 - assert initial_data["valid_to"].isna().all() - assert initial_data["department"].tolist() == ["engineering"] - assert initial_data["region"].tolist() == ["EMEA"] - - # Apply a future plan with source changes - with time_machine.travel("2023-01-08 15:10:00 UTC"): - # Update source model, employee 1001 changed region - raw_employee_status_v2 = d.parse(""" - MODEL ( - name memory.hr_system.raw_employee_status, - kind FULL - ); - - SELECT - 1001 AS employee_id, - 'engineering' AS department, - 'AMER' AS region, - '2023-01-08 15:10:00 UTC' AS last_modified; - """) - raw_employee_status_v2_model = load_sql_based_model(raw_employee_status_v2) - context.upsert_model(raw_employee_status_v2_model) - context.plan( - auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() - ) - - with time_machine.travel("2023-01-08 15:20:00 UTC"): - context.run() - data_after_change = context.engine_adapter.fetchdf(query) - - # Validate the SCD2 history for employee 1001 - assert len(data_after_change) == 2 - assert data_after_change.iloc[0]["employee_id"] == 1001 - assert data_after_change.iloc[0]["department"] == "engineering" - assert data_after_change.iloc[0]["region"] == "EMEA" - assert str(data_after_change.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" - assert str(data_after_change.iloc[0]["valid_to"]) == "2023-01-08 15:10:00" - assert data_after_change.iloc[1]["employee_id"] == 1001 - assert data_after_change.iloc[1]["department"] == "engineering" - assert data_after_change.iloc[1]["region"] == "AMER" - assert str(data_after_change.iloc[1]["valid_from"]) == "2023-01-08 15:10:00" - assert pd.isna(data_after_change.iloc[1]["valid_to"]) - - # Update source model, employee 1001 changed region again and department - raw_employee_status_v2 = d.parse(""" - MODEL ( - name memory.hr_system.raw_employee_status, - kind FULL - ); - - SELECT - 1001 AS employee_id, - 'sales' AS department, - 'ANZ' AS region, - '2023-01-08 15:26:00 UTC' AS last_modified; - """) - raw_employee_status_v2_model = load_sql_based_model(raw_employee_status_v2) - context.upsert_model(raw_employee_status_v2_model) - context.plan( - auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() - ) - - with time_machine.travel("2023-01-08 15:35:00 UTC"): - context.run() - data_after_change = context.engine_adapter.fetchdf(query) - - # Validate the SCD2 history for employee 1001 after second change - assert len(data_after_change) == 3 - assert data_after_change.iloc[0]["employee_id"] == 1001 - assert data_after_change.iloc[0]["department"] == "engineering" - assert data_after_change.iloc[0]["region"] == "EMEA" - assert str(data_after_change.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" - assert str(data_after_change.iloc[0]["valid_to"]) == "2023-01-08 15:10:00" - assert data_after_change.iloc[1]["employee_id"] == 1001 - assert data_after_change.iloc[1]["department"] == "engineering" - assert data_after_change.iloc[1]["region"] == "AMER" - assert str(data_after_change.iloc[1]["valid_from"]) == "2023-01-08 15:10:00" - assert str(data_after_change.iloc[1]["valid_to"]) == "2023-01-08 15:26:00" - assert data_after_change.iloc[2]["employee_id"] == 1001 - assert data_after_change.iloc[2]["department"] == "sales" - assert data_after_change.iloc[2]["region"] == "ANZ" - assert str(data_after_change.iloc[2]["valid_from"]) == "2023-01-08 15:26:00" - assert pd.isna(data_after_change.iloc[2]["valid_to"]) - - # Now test restatement cleanup by restating from 15:10 (first change) - with time_machine.travel("2023-01-08 15:38:00 UTC"): - plan = context.plan_builder( - "prod", - skip_tests=True, - restate_models=["memory.hr_system.employee_history"], - start="2023-01-08 15:09:00", - ).build() - context.apply(plan) - restated_data = context.engine_adapter.fetchdf(query) - - # Validate the SCD2 history after restatement - assert len(restated_data) == 2 - assert restated_data.iloc[0]["employee_id"] == 1001 - assert restated_data.iloc[0]["department"] == "engineering" - assert restated_data.iloc[0]["region"] == "EMEA" - assert str(restated_data.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" - assert str(restated_data.iloc[0]["valid_to"]) == "2023-01-08 15:26:00" - assert restated_data.iloc[1]["employee_id"] == 1001 - assert restated_data.iloc[1]["department"] == "sales" - assert restated_data.iloc[1]["region"] == "ANZ" - assert str(restated_data.iloc[1]["valid_from"]) == "2023-01-08 15:26:00" - assert pd.isna(restated_data.iloc[1]["valid_to"]) - - @time_machine.travel("2020-01-01 00:00:00 UTC") def test_scd_type_2_full_restatement_no_start_date(init_and_plan_context: t.Callable): context, plan = init_and_plan_context("examples/sushi") @@ -7348,7 +7185,7 @@ def test_scd_type_2_regular_run_with_offset(init_and_plan_context: t.Callable): assert str(data_after_change.iloc[2]["valid_from"]) == "2023-01-09 07:26:00" assert pd.isna(data_after_change.iloc[2]["valid_to"]) - # Now test restatement still works as expected by restating from 2023-01-09 00:10:00 (first change) + # Now test restatement works (full restatement support currently) with time_machine.travel("2023-01-10 07:38:00 UTC"): plan = context.plan_builder( "prod", @@ -7359,18 +7196,13 @@ def test_scd_type_2_regular_run_with_offset(init_and_plan_context: t.Callable): context.apply(plan) restated_data = context.engine_adapter.fetchdf(query) - # Validate the SCD2 history after restatement - assert len(restated_data) == 2 + # Validate the SCD2 history after restatement has been wiped bar one + assert len(restated_data) == 1 assert restated_data.iloc[0]["employee_id"] == 1001 - assert restated_data.iloc[0]["department"] == "engineering" - assert restated_data.iloc[0]["region"] == "EMEA" + assert restated_data.iloc[0]["department"] == "sales" + assert restated_data.iloc[0]["region"] == "ANZ" assert str(restated_data.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" - assert str(restated_data.iloc[0]["valid_to"]) == "2023-01-09 07:26:00" - assert restated_data.iloc[1]["employee_id"] == 1001 - assert restated_data.iloc[1]["department"] == "sales" - assert restated_data.iloc[1]["region"] == "ANZ" - assert str(restated_data.iloc[1]["valid_from"]) == "2023-01-09 07:26:00" - assert pd.isna(restated_data.iloc[1]["valid_to"]) + assert pd.isna(restated_data.iloc[0]["valid_to"]) def test_engine_adapters_multi_repo_all_gateways_gathered(copy_to_temp_path): diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 6f3bd0bc7e..6bc5388468 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -10441,9 +10441,9 @@ def test_signal_always_true(batch, arg1, arg2): def test_scd_type_2_full_history_restatement(): - assert ModelKindName.SCD_TYPE_2.full_history_restatement_only is False - assert ModelKindName.SCD_TYPE_2_BY_TIME.full_history_restatement_only is False - assert ModelKindName.SCD_TYPE_2_BY_COLUMN.full_history_restatement_only is False + assert ModelKindName.SCD_TYPE_2.full_history_restatement_only is True + assert ModelKindName.SCD_TYPE_2_BY_TIME.full_history_restatement_only is True + assert ModelKindName.SCD_TYPE_2_BY_COLUMN.full_history_restatement_only is True assert ModelKindName.INCREMENTAL_BY_TIME_RANGE.full_history_restatement_only is False diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index a3c7837711..6c0763892e 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -2049,8 +2049,6 @@ def test_insert_into_scd_type_2_by_time( column_descriptions={}, updated_at_as_valid_from=False, truncate=truncate, - is_restatement=False, - start="2020-01-01", ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) @@ -2223,8 +2221,6 @@ def test_insert_into_scd_type_2_by_column( table_description=None, column_descriptions={}, truncate=truncate, - is_restatement=False, - start="2020-01-01", ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) From 13ae8e3fd9637c53f47e0dec44e8d0c3a788a6b6 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:58:07 +0200 Subject: [PATCH 0692/1056] chore(vscode): fix test stability (#5145) --- vscode/extension/tests/quickfix.spec.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/vscode/extension/tests/quickfix.spec.ts b/vscode/extension/tests/quickfix.spec.ts index 60d0207f7c..c3f37a2acc 100644 --- a/vscode/extension/tests/quickfix.spec.ts +++ b/vscode/extension/tests/quickfix.spec.ts @@ -70,13 +70,22 @@ test('noselectstar quickfix', async ({ page, sharedCodeServer, tempDir }) => { await openProblemsView(page) await page.getByRole('button', { name: 'Show fixes' }).click() - await page - .getByRole('menuitem', { name: 'Replace SELECT * with' }) - .first() - .click() + // Wait for the quick fix menu to appear and click the specific action within it. + const quickFixMenu = page.getByRole('menu') + await quickFixMenu.waitFor({ state: 'visible' }) + const replaceSelectStar = quickFixMenu.getByRole('menuitem', { + name: /Replace SELECT \* with/i, + }) + await replaceSelectStar.first().waitFor({ state: 'visible' }) + await replaceSelectStar.first().click() - // Wait for the quick fix to be applied - await page.waitForTimeout(2_000) + // Wait for the quick fix to be applied by polling the file content + await expect + .poll(async () => { + const content = (await fs.readFile(modelPath)).toString('utf8') + return content.includes('SELECT *') + }) + .toBeFalsy() // Assert that the model no longer contains SELECT * but SELECT id, customer_id, waiter_id, start_ts, end_ts, event_date const readUpdatedFile = (await fs.readFile(modelPath)).toString('utf8') From d911d99ff3b919e652584efd6c7b5106cdaf538d Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:18:02 +0200 Subject: [PATCH 0693/1056] feat(vscode): adding validation to config (#5126) --- vscode/extension/src/utilities/config.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/vscode/extension/src/utilities/config.ts b/vscode/extension/src/utilities/config.ts index c8edcd13ce..9a88e24f36 100644 --- a/vscode/extension/src/utilities/config.ts +++ b/vscode/extension/src/utilities/config.ts @@ -6,10 +6,12 @@ import { traceVerbose, traceInfo } from './common/log' import { parse } from 'shell-quote' import { z } from 'zod' -export interface SqlmeshConfiguration { - projectPath: string - lspEntryPoint: string -} +const configSchema = z.object({ + projectPath: z.string(), + lspEntryPoint: z.string(), +}) + +export type SqlmeshConfiguration = z.infer /** * Get the SQLMesh configuration from VS Code settings. @@ -20,10 +22,17 @@ function getSqlmeshConfiguration(): SqlmeshConfiguration { const config = workspace.getConfiguration('sqlmesh') const projectPath = config.get('projectPath', '') const lspEntryPoint = config.get('lspEntrypoint', '') - return { + + const parsed = configSchema.safeParse({ projectPath, lspEntryPoint, + }) + if (!parsed.success) { + throw new Error( + `Invalid sqlmesh configuration: ${JSON.stringify(parsed.error)}`, + ) } + return parsed.data } const stringsArray = z.array(z.string()) From bed9f2e03ddf25d760e857b9d5f22195f9990fc6 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 13 Aug 2025 09:07:21 -0700 Subject: [PATCH 0694/1056] Chore!: Optimize snapshot unpausing --- sqlmesh/core/state_sync/db/facade.py | 2 +- sqlmesh/core/state_sync/db/migrator.py | 2 +- sqlmesh/core/state_sync/db/snapshot.py | 145 +++++++----------- sqlmesh/core/state_sync/db/utils.py | 15 ++ .../v0090_add_forward_only_column.py | 100 ++++++++++++ tests/core/state_sync/test_state_sync.py | 122 +-------------- 6 files changed, 177 insertions(+), 209 deletions(-) create mode 100644 sqlmesh/migrations/v0090_add_forward_only_column.py diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 779add1cca..858b1aa072 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -256,7 +256,7 @@ def finalize(self, environment: Environment) -> None: def unpause_snapshots( self, snapshots: t.Collection[SnapshotInfoLike], unpaused_dt: TimeLike ) -> None: - self.snapshot_state.unpause_snapshots(snapshots, unpaused_dt, self.interval_state) + self.snapshot_state.unpause_snapshots(snapshots, unpaused_dt) def invalidate_environment(self, name: str, protect_prod: bool = True) -> None: self.environment_state.invalidate_environment(name, protect_prod) diff --git a/sqlmesh/core/state_sync/db/migrator.py b/sqlmesh/core/state_sync/db/migrator.py index 405c0ea667..ca89668763 100644 --- a/sqlmesh/core/state_sync/db/migrator.py +++ b/sqlmesh/core/state_sync/db/migrator.py @@ -396,7 +396,7 @@ def _migrate_environment_rows( if updated_prod_environment: try: self.snapshot_state.unpause_snapshots( - updated_prod_environment.snapshots, now_timestamp(), self.interval_state + updated_prod_environment.snapshots, now_timestamp() ) except Exception: logger.warning("Failed to unpause migrated snapshots", exc_info=True) diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 30e0de00f2..4ea4a837fd 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -10,6 +10,7 @@ from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.state_sync.db.utils import ( + snapshot_name_filter, snapshot_name_version_filter, snapshot_id_filter, fetchone, @@ -32,15 +33,13 @@ SnapshotChangeCategory, ) from sqlmesh.utils.migration import index_text_type, blob_text_type -from sqlmesh.utils.date import now_timestamp, TimeLike, now, to_timestamp +from sqlmesh.utils.date import now_timestamp, TimeLike, to_timestamp from sqlmesh.utils.pydantic import PydanticModel from sqlmesh.utils import unique if t.TYPE_CHECKING: import pandas as pd - from sqlmesh.core.state_sync.db.interval import IntervalState - logger = logging.getLogger(__name__) @@ -70,6 +69,7 @@ def __init__( "unpaused_ts": exp.DataType.build("bigint"), "ttl_ms": exp.DataType.build("bigint"), "unrestorable": exp.DataType.build("boolean"), + "forward_only": exp.DataType.build("boolean"), } self._auto_restatement_columns_to_types = { @@ -112,84 +112,48 @@ def unpause_snapshots( self, snapshots: t.Collection[SnapshotInfoLike], unpaused_dt: TimeLike, - interval_state: IntervalState, ) -> None: - """Unpauses given snapshots while pausing all other snapshots that share the same version. - - Args: - snapshots: The snapshots to unpause. - unpaused_dt: The timestamp to unpause the snapshots at. - interval_state: The interval state to use to remove intervals when needed. - """ - current_ts = now() - - target_snapshot_ids = {s.snapshot_id for s in snapshots} - same_version_snapshots = self._get_snapshots_with_same_version( - snapshots, lock_for_update=True - ) - target_snapshots_by_version = { - (s.name, s.version): s - for s in same_version_snapshots - if s.snapshot_id in target_snapshot_ids - } + unrestorable_snapshots_by_forward_only: t.Dict[bool, t.List[str]] = defaultdict(list) - unpaused_snapshots: t.Dict[int, t.List[SnapshotId]] = defaultdict(list) - paused_snapshots: t.List[SnapshotId] = [] - unrestorable_snapshots: t.List[SnapshotId] = [] - - for snapshot in same_version_snapshots: - is_target_snapshot = snapshot.snapshot_id in target_snapshot_ids - if is_target_snapshot and not snapshot.unpaused_ts: - logger.info("Unpausing snapshot %s", snapshot.snapshot_id) - snapshot.set_unpaused_ts(unpaused_dt) - assert snapshot.unpaused_ts is not None - unpaused_snapshots[snapshot.unpaused_ts].append(snapshot.snapshot_id) - elif not is_target_snapshot: - target_snapshot = target_snapshots_by_version[(snapshot.name, snapshot.version)] - if ( - target_snapshot.normalized_effective_from_ts - and not target_snapshot.disable_restatement - ): - # Making sure that there are no overlapping intervals. - effective_from_ts = target_snapshot.normalized_effective_from_ts - logger.info( - "Removing all intervals after '%s' for snapshot %s, superseded by snapshot %s", - target_snapshot.effective_from, - snapshot.snapshot_id, - target_snapshot.snapshot_id, - ) - full_snapshot = snapshot.full_snapshot - interval_state.remove_intervals( - [ - ( - full_snapshot, - full_snapshot.get_removal_interval(effective_from_ts, current_ts), - ) - ] - ) - - if snapshot.unpaused_ts: - logger.info("Pausing snapshot %s", snapshot.snapshot_id) - snapshot.set_unpaused_ts(None) - paused_snapshots.append(snapshot.snapshot_id) + for snapshot in snapshots: + # We need to mark all other snapshots that have opposite forward only status as unrestorable + unrestorable_snapshots_by_forward_only[not snapshot.is_forward_only].append( + snapshot.name + ) - if not snapshot.unrestorable and ( - (target_snapshot.is_forward_only and not snapshot.is_forward_only) - or (snapshot.is_forward_only and not target_snapshot.is_forward_only) - ): - logger.info("Marking snapshot %s as unrestorable", snapshot.snapshot_id) - snapshot.unrestorable = True - unrestorable_snapshots.append(snapshot.snapshot_id) + updated_ts = now_timestamp() + unpaused_ts = to_timestamp(unpaused_dt) - if unpaused_snapshots: - for unpaused_ts, snapshot_ids in unpaused_snapshots.items(): - self._update_snapshots(snapshot_ids, unpaused_ts=unpaused_ts) + # Pause all snapshots with target names first + for where in snapshot_name_filter( + [s.name for s in snapshots], + batch_size=self.SNAPSHOT_BATCH_SIZE, + ): + self.engine_adapter.update_table( + self.snapshots_table, + {"unpaused_ts": None, "updated_ts": updated_ts}, + where=where, + ) - if paused_snapshots: - self._update_snapshots(paused_snapshots, unpaused_ts=None) + # Now unpause the target snapshots + self._update_snapshots( + [s.snapshot_id for s in snapshots], + unpaused_ts=unpaused_ts, + updated_ts=updated_ts, + ) - if unrestorable_snapshots: - self._update_snapshots(unrestorable_snapshots, unrestorable=True) + # Mark unrestorable snapshots + for forward_only, snapshot_names in unrestorable_snapshots_by_forward_only.items(): + forward_only_exp = exp.column("forward_only").is_(exp.convert(forward_only)) + for where in snapshot_name_filter( + snapshot_names, + batch_size=self.SNAPSHOT_BATCH_SIZE, + ): + self.engine_adapter.update_table( + self.snapshots_table, + {"unrestorable": True, "updated_ts": updated_ts}, + where=forward_only_exp.and_(where), + ) def get_expired_snapshots( self, @@ -414,7 +378,8 @@ def _update_snapshots( **kwargs: t.Any, ) -> None: properties = kwargs - properties["updated_ts"] = now_timestamp() + if "updated_ts" not in properties: + properties["updated_ts"] = now_timestamp() for where in snapshot_id_filter( self.engine_adapter, snapshots, batch_size=self.SNAPSHOT_BATCH_SIZE @@ -466,6 +431,7 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: updated_ts, unpaused_ts, unrestorable, + forward_only, next_auto_restatement_ts, ) in fetchall(self.engine_adapter, query): snapshot = parse_snapshot( @@ -473,6 +439,7 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: updated_ts=updated_ts, unpaused_ts=unpaused_ts, unrestorable=unrestorable, + forward_only=forward_only, next_auto_restatement_ts=next_auto_restatement_ts, ) snapshot_id = snapshot.snapshot_id @@ -502,6 +469,7 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: "updated_ts", "unpaused_ts", "unrestorable", + "forward_only", "next_auto_restatement_ts", ) .from_(exp.to_table(self.snapshots_table).as_("snapshots")) @@ -528,6 +496,7 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: updated_ts, unpaused_ts, unrestorable, + forward_only, next_auto_restatement_ts, ) in fetchall(self.engine_adapter, query): snapshot_id = SnapshotId(name=name, identifier=identifier) @@ -535,6 +504,7 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: snapshot.updated_ts = updated_ts snapshot.unpaused_ts = unpaused_ts snapshot.unrestorable = unrestorable + snapshot.forward_only = forward_only snapshot.next_auto_restatement_ts = next_auto_restatement_ts cached_snapshots_in_state.add(snapshot_id) @@ -568,6 +538,7 @@ def _get_snapshots_expressions( "snapshots.updated_ts", "snapshots.unpaused_ts", "snapshots.unrestorable", + "snapshots.forward_only", "auto_restatements.next_auto_restatement_ts", ) .from_(exp.to_table(self.snapshots_table).as_("snapshots")) @@ -623,6 +594,7 @@ def _get_snapshots_with_same_version( "updated_ts", "unpaused_ts", "unrestorable", + "forward_only", ) .from_(exp.to_table(self.snapshots_table).as_("snapshots")) .where(where) @@ -640,9 +612,10 @@ def _get_snapshots_with_same_version( updated_ts=updated_ts, unpaused_ts=unpaused_ts, unrestorable=unrestorable, + forward_only=forward_only, snapshot=snapshot, ) - for snapshot, name, identifier, version, updated_ts, unpaused_ts, unrestorable in snapshot_rows + for snapshot, name, identifier, version, updated_ts, unpaused_ts, unrestorable, forward_only in snapshot_rows ] @@ -651,6 +624,7 @@ def parse_snapshot( updated_ts: int, unpaused_ts: t.Optional[int], unrestorable: bool, + forward_only: bool, next_auto_restatement_ts: t.Optional[int], ) -> Snapshot: return Snapshot( @@ -659,6 +633,7 @@ def parse_snapshot( "updated_ts": updated_ts, "unpaused_ts": unpaused_ts, "unrestorable": unrestorable, + "forward_only": forward_only, "next_auto_restatement_ts": next_auto_restatement_ts, } ) @@ -673,6 +648,7 @@ def _snapshot_to_json(snapshot: Snapshot) -> str: "updated_ts", "unpaused_ts", "unrestorable", + "forward_only", "next_auto_restatement_ts", } ) @@ -693,6 +669,7 @@ def _snapshots_to_df(snapshots: t.Iterable[Snapshot]) -> pd.DataFrame: "unpaused_ts": snapshot.unpaused_ts, "ttl_ms": snapshot.ttl_ms, "unrestorable": snapshot.unrestorable, + "forward_only": snapshot.forward_only, } for snapshot in snapshots ] @@ -762,19 +739,10 @@ def full_snapshot(self) -> Snapshot: "updated_ts": self.updated_ts, "unpaused_ts": self.unpaused_ts, "unrestorable": self.unrestorable, + "forward_only": self.forward_only, } ) - def set_unpaused_ts(self, unpaused_dt: t.Optional[TimeLike]) -> None: - """Sets the timestamp for when this snapshot was unpaused. - - Args: - unpaused_dt: The datetime object of when this snapshot was unpaused. - """ - self.unpaused_ts = ( - to_timestamp(self.interval_unit.cron_floor(unpaused_dt)) if unpaused_dt else None - ) - @classmethod def from_snapshot_record( cls, @@ -785,6 +753,7 @@ def from_snapshot_record( updated_ts: int, unpaused_ts: t.Optional[int], unrestorable: bool, + forward_only: bool, snapshot: str, ) -> SharedVersionSnapshot: raw_snapshot = json.loads(snapshot) @@ -803,5 +772,5 @@ def from_snapshot_record( disable_restatement=raw_node.get("kind", {}).get("disable_restatement", False), effective_from=raw_snapshot.get("effective_from"), raw_snapshot=raw_snapshot, - forward_only=raw_snapshot.get("forward_only", False), + forward_only=forward_only, ) diff --git a/sqlmesh/core/state_sync/db/utils.py b/sqlmesh/core/state_sync/db/utils.py index e5ffda6486..87c259f5d6 100644 --- a/sqlmesh/core/state_sync/db/utils.py +++ b/sqlmesh/core/state_sync/db/utils.py @@ -22,6 +22,21 @@ T = t.TypeVar("T") +def snapshot_name_filter( + snapshot_names: t.Iterable[str], + batch_size: int, + alias: t.Optional[str] = None, +) -> t.Iterator[exp.Condition]: + names = sorted(snapshot_names) + + if not names: + yield exp.false() + else: + batches = create_batches(names, batch_size=batch_size) + for names in batches: + yield exp.column("name", table=alias).isin(*names) + + def snapshot_id_filter( engine_adapter: EngineAdapter, snapshot_ids: t.Iterable[SnapshotIdLike], diff --git a/sqlmesh/migrations/v0090_add_forward_only_column.py b/sqlmesh/migrations/v0090_add_forward_only_column.py new file mode 100644 index 0000000000..32efc14eed --- /dev/null +++ b/sqlmesh/migrations/v0090_add_forward_only_column.py @@ -0,0 +1,100 @@ +"""Add forward_only column to the snapshots table.""" + +import json + +from sqlglot import exp + +from sqlmesh.utils.migration import index_text_type, blob_text_type + + +def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + alter_table_exp = exp.Alter( + this=exp.to_table(snapshots_table), + kind="TABLE", + actions=[ + exp.ColumnDef( + this=exp.to_column("forward_only"), + kind=exp.DataType.build("boolean"), + ) + ], + ) + engine_adapter.execute(alter_table_exp) + + new_snapshots = [] + + for ( + name, + identifier, + version, + snapshot, + kind_name, + updated_ts, + unpaused_ts, + ttl_ms, + unrestorable, + forward_only, + ) in engine_adapter.fetchall( + exp.select( + "name", + "identifier", + "version", + "snapshot", + "kind_name", + "updated_ts", + "unpaused_ts", + "ttl_ms", + "unrestorable", + "forward_only", + ).from_(snapshots_table), + quote_identifiers=True, + ): + parsed_snapshot = json.loads(snapshot) + + forward_only = parsed_snapshot.get("forward_only") + if forward_only is None: + forward_only = parsed_snapshot.get("change_category") == 3 + + new_snapshots.append( + { + "name": name, + "identifier": identifier, + "version": version, + "snapshot": json.dumps(parsed_snapshot), + "kind_name": kind_name, + "updated_ts": updated_ts, + "unpaused_ts": unpaused_ts, + "ttl_ms": ttl_ms, + "unrestorable": unrestorable, + "forward_only": forward_only, + } + ) + + if new_snapshots: + engine_adapter.delete_from(snapshots_table, "TRUE") + index_type = index_text_type(engine_adapter.dialect) + blob_type = blob_text_type(engine_adapter.dialect) + + engine_adapter.insert_append( + snapshots_table, + pd.DataFrame(new_snapshots), + columns_to_types={ + "name": exp.DataType.build(index_type), + "identifier": exp.DataType.build(index_type), + "version": exp.DataType.build(index_type), + "snapshot": exp.DataType.build(blob_type), + "kind_name": exp.DataType.build(index_type), + "updated_ts": exp.DataType.build("bigint"), + "unpaused_ts": exp.DataType.build("bigint"), + "ttl_ms": exp.DataType.build("bigint"), + "unrestorable": exp.DataType.build("boolean"), + "forward_only": exp.DataType.build("boolean"), + }, + ) diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index d8e96a1f35..d61907a5aa 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -1888,50 +1888,6 @@ def test_unpause_snapshots(state_sync: EngineAdapterStateSync, make_snapshot: t. assert not actual_snapshots[new_snapshot.snapshot_id].unrestorable -def test_unpause_snapshots_hourly(state_sync: EngineAdapterStateSync, make_snapshot: t.Callable): - snapshot = make_snapshot( - SqlModel( - name="test_snapshot", - query=parse_one("select 1, ds"), - cron="@hourly", - ), - ) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot.version = "a" - - assert not snapshot.unpaused_ts - state_sync.push_snapshots([snapshot]) - - # Unpaused timestamp not aligned with cron - unpaused_dt = "2022-01-01 01:22:33" - state_sync.unpause_snapshots([snapshot], unpaused_dt) - - actual_snapshot = state_sync.get_snapshots([snapshot])[snapshot.snapshot_id] - assert actual_snapshot.unpaused_ts - assert actual_snapshot.unpaused_ts == to_timestamp("2022-01-01 01:00:00") - - new_snapshot = make_snapshot( - SqlModel( - name="test_snapshot", - query=parse_one("select 2, ds"), - cron="@daily", - interval_unit="hour", - ) - ) - new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) - new_snapshot.version = "a" - - assert not new_snapshot.unpaused_ts - state_sync.push_snapshots([new_snapshot]) - state_sync.unpause_snapshots([new_snapshot], unpaused_dt) - - actual_snapshots = state_sync.get_snapshots([snapshot, new_snapshot]) - assert not actual_snapshots[snapshot.snapshot_id].unpaused_ts - assert actual_snapshots[new_snapshot.snapshot_id].unpaused_ts == to_timestamp( - "2022-01-01 01:00:00" - ) - - def test_unrestorable_snapshot(state_sync: EngineAdapterStateSync, make_snapshot: t.Callable): snapshot = make_snapshot( SqlModel( @@ -2037,81 +1993,6 @@ def test_unrestorable_snapshot_target_not_forward_only( assert not actual_snapshots[updated_snapshot.snapshot_id].unrestorable -def test_unpause_snapshots_remove_intervals( - state_sync: EngineAdapterStateSync, make_snapshot: t.Callable -): - snapshot = make_snapshot( - SqlModel( - name="test_snapshot", - query=parse_one("select 1, ds"), - cron="@daily", - ), - version="a", - ) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot.version = "a" - state_sync.push_snapshots([snapshot]) - state_sync.add_interval(snapshot, "2023-01-01", "2023-01-05") - - new_snapshot = make_snapshot( - SqlModel(name="test_snapshot", query=parse_one("select 2, ds"), cron="@daily"), - version="a", - ) - new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) - new_snapshot.version = "a" - new_snapshot.effective_from = "2023-01-03" - state_sync.push_snapshots([new_snapshot]) - state_sync.add_interval(snapshot, "2023-01-06", "2023-01-06") - state_sync.unpause_snapshots([new_snapshot], "2023-01-06") - - actual_snapshots = state_sync.get_snapshots([snapshot, new_snapshot]) - assert actual_snapshots[new_snapshot.snapshot_id].intervals == [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), - ] - assert actual_snapshots[snapshot.snapshot_id].intervals == [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), - ] - - -def test_unpause_snapshots_remove_intervals_disabled_restatement( - state_sync: EngineAdapterStateSync, make_snapshot: t.Callable -): - kind = dict(name="INCREMENTAL_BY_TIME_RANGE", time_column="ds", disable_restatement=True) - snapshot = make_snapshot( - SqlModel( - name="test_snapshot", - query=parse_one("select 1, ds"), - cron="@daily", - kind=kind, - ), - version="a", - ) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot.version = "a" - state_sync.push_snapshots([snapshot]) - state_sync.add_interval(snapshot, "2023-01-01", "2023-01-05") - - new_snapshot = make_snapshot( - SqlModel(name="test_snapshot", query=parse_one("select 2, ds"), cron="@daily", kind=kind), - version="a", - ) - new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) - new_snapshot.version = "a" - new_snapshot.effective_from = "2023-01-03" - state_sync.push_snapshots([new_snapshot]) - state_sync.add_interval(snapshot, "2023-01-06", "2023-01-06") - state_sync.unpause_snapshots([new_snapshot], "2023-01-06") - - actual_snapshots = state_sync.get_snapshots([snapshot, new_snapshot]) - assert actual_snapshots[new_snapshot.snapshot_id].intervals == [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), - ] - # The intervals shouldn't have been removed because restatement is disabled - assert actual_snapshots[snapshot.snapshot_id].intervals == [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-07")), - ] - - def test_version_schema(state_sync: EngineAdapterStateSync, tmp_path) -> None: from sqlmesh import __version__ as SQLMESH_VERSION @@ -2999,6 +2880,7 @@ def test_snapshot_batching(state_sync, mocker, make_snapshot): 1, 1, False, + False, None, ], [ @@ -3011,6 +2893,7 @@ def test_snapshot_batching(state_sync, mocker, make_snapshot): 1, 1, False, + False, None, ], ], @@ -3025,6 +2908,7 @@ def test_snapshot_batching(state_sync, mocker, make_snapshot): 1, 1, False, + False, None, ], ], From 5aacaa89ed715b770cec43fb136c8dd89e490ea2 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 13 Aug 2025 09:08:20 -0700 Subject: [PATCH 0695/1056] Revert "Chore!: Optimize snapshot unpausing" This reverts commit bed9f2e03ddf25d760e857b9d5f22195f9990fc6. --- sqlmesh/core/state_sync/db/facade.py | 2 +- sqlmesh/core/state_sync/db/migrator.py | 2 +- sqlmesh/core/state_sync/db/snapshot.py | 145 +++++++++++------- sqlmesh/core/state_sync/db/utils.py | 15 -- .../v0090_add_forward_only_column.py | 100 ------------ tests/core/state_sync/test_state_sync.py | 122 ++++++++++++++- 6 files changed, 209 insertions(+), 177 deletions(-) delete mode 100644 sqlmesh/migrations/v0090_add_forward_only_column.py diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 858b1aa072..779add1cca 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -256,7 +256,7 @@ def finalize(self, environment: Environment) -> None: def unpause_snapshots( self, snapshots: t.Collection[SnapshotInfoLike], unpaused_dt: TimeLike ) -> None: - self.snapshot_state.unpause_snapshots(snapshots, unpaused_dt) + self.snapshot_state.unpause_snapshots(snapshots, unpaused_dt, self.interval_state) def invalidate_environment(self, name: str, protect_prod: bool = True) -> None: self.environment_state.invalidate_environment(name, protect_prod) diff --git a/sqlmesh/core/state_sync/db/migrator.py b/sqlmesh/core/state_sync/db/migrator.py index ca89668763..405c0ea667 100644 --- a/sqlmesh/core/state_sync/db/migrator.py +++ b/sqlmesh/core/state_sync/db/migrator.py @@ -396,7 +396,7 @@ def _migrate_environment_rows( if updated_prod_environment: try: self.snapshot_state.unpause_snapshots( - updated_prod_environment.snapshots, now_timestamp() + updated_prod_environment.snapshots, now_timestamp(), self.interval_state ) except Exception: logger.warning("Failed to unpause migrated snapshots", exc_info=True) diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 4ea4a837fd..30e0de00f2 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -10,7 +10,6 @@ from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.state_sync.db.utils import ( - snapshot_name_filter, snapshot_name_version_filter, snapshot_id_filter, fetchone, @@ -33,13 +32,15 @@ SnapshotChangeCategory, ) from sqlmesh.utils.migration import index_text_type, blob_text_type -from sqlmesh.utils.date import now_timestamp, TimeLike, to_timestamp +from sqlmesh.utils.date import now_timestamp, TimeLike, now, to_timestamp from sqlmesh.utils.pydantic import PydanticModel from sqlmesh.utils import unique if t.TYPE_CHECKING: import pandas as pd + from sqlmesh.core.state_sync.db.interval import IntervalState + logger = logging.getLogger(__name__) @@ -69,7 +70,6 @@ def __init__( "unpaused_ts": exp.DataType.build("bigint"), "ttl_ms": exp.DataType.build("bigint"), "unrestorable": exp.DataType.build("boolean"), - "forward_only": exp.DataType.build("boolean"), } self._auto_restatement_columns_to_types = { @@ -112,48 +112,84 @@ def unpause_snapshots( self, snapshots: t.Collection[SnapshotInfoLike], unpaused_dt: TimeLike, + interval_state: IntervalState, ) -> None: - unrestorable_snapshots_by_forward_only: t.Dict[bool, t.List[str]] = defaultdict(list) + """Unpauses given snapshots while pausing all other snapshots that share the same version. - for snapshot in snapshots: - # We need to mark all other snapshots that have opposite forward only status as unrestorable - unrestorable_snapshots_by_forward_only[not snapshot.is_forward_only].append( - snapshot.name - ) + Args: + snapshots: The snapshots to unpause. + unpaused_dt: The timestamp to unpause the snapshots at. + interval_state: The interval state to use to remove intervals when needed. + """ + current_ts = now() - updated_ts = now_timestamp() - unpaused_ts = to_timestamp(unpaused_dt) + target_snapshot_ids = {s.snapshot_id for s in snapshots} + same_version_snapshots = self._get_snapshots_with_same_version( + snapshots, lock_for_update=True + ) + target_snapshots_by_version = { + (s.name, s.version): s + for s in same_version_snapshots + if s.snapshot_id in target_snapshot_ids + } - # Pause all snapshots with target names first - for where in snapshot_name_filter( - [s.name for s in snapshots], - batch_size=self.SNAPSHOT_BATCH_SIZE, - ): - self.engine_adapter.update_table( - self.snapshots_table, - {"unpaused_ts": None, "updated_ts": updated_ts}, - where=where, - ) + unpaused_snapshots: t.Dict[int, t.List[SnapshotId]] = defaultdict(list) + paused_snapshots: t.List[SnapshotId] = [] + unrestorable_snapshots: t.List[SnapshotId] = [] + + for snapshot in same_version_snapshots: + is_target_snapshot = snapshot.snapshot_id in target_snapshot_ids + if is_target_snapshot and not snapshot.unpaused_ts: + logger.info("Unpausing snapshot %s", snapshot.snapshot_id) + snapshot.set_unpaused_ts(unpaused_dt) + assert snapshot.unpaused_ts is not None + unpaused_snapshots[snapshot.unpaused_ts].append(snapshot.snapshot_id) + elif not is_target_snapshot: + target_snapshot = target_snapshots_by_version[(snapshot.name, snapshot.version)] + if ( + target_snapshot.normalized_effective_from_ts + and not target_snapshot.disable_restatement + ): + # Making sure that there are no overlapping intervals. + effective_from_ts = target_snapshot.normalized_effective_from_ts + logger.info( + "Removing all intervals after '%s' for snapshot %s, superseded by snapshot %s", + target_snapshot.effective_from, + snapshot.snapshot_id, + target_snapshot.snapshot_id, + ) + full_snapshot = snapshot.full_snapshot + interval_state.remove_intervals( + [ + ( + full_snapshot, + full_snapshot.get_removal_interval(effective_from_ts, current_ts), + ) + ] + ) - # Now unpause the target snapshots - self._update_snapshots( - [s.snapshot_id for s in snapshots], - unpaused_ts=unpaused_ts, - updated_ts=updated_ts, - ) + if snapshot.unpaused_ts: + logger.info("Pausing snapshot %s", snapshot.snapshot_id) + snapshot.set_unpaused_ts(None) + paused_snapshots.append(snapshot.snapshot_id) - # Mark unrestorable snapshots - for forward_only, snapshot_names in unrestorable_snapshots_by_forward_only.items(): - forward_only_exp = exp.column("forward_only").is_(exp.convert(forward_only)) - for where in snapshot_name_filter( - snapshot_names, - batch_size=self.SNAPSHOT_BATCH_SIZE, - ): - self.engine_adapter.update_table( - self.snapshots_table, - {"unrestorable": True, "updated_ts": updated_ts}, - where=forward_only_exp.and_(where), - ) + if not snapshot.unrestorable and ( + (target_snapshot.is_forward_only and not snapshot.is_forward_only) + or (snapshot.is_forward_only and not target_snapshot.is_forward_only) + ): + logger.info("Marking snapshot %s as unrestorable", snapshot.snapshot_id) + snapshot.unrestorable = True + unrestorable_snapshots.append(snapshot.snapshot_id) + + if unpaused_snapshots: + for unpaused_ts, snapshot_ids in unpaused_snapshots.items(): + self._update_snapshots(snapshot_ids, unpaused_ts=unpaused_ts) + + if paused_snapshots: + self._update_snapshots(paused_snapshots, unpaused_ts=None) + + if unrestorable_snapshots: + self._update_snapshots(unrestorable_snapshots, unrestorable=True) def get_expired_snapshots( self, @@ -378,8 +414,7 @@ def _update_snapshots( **kwargs: t.Any, ) -> None: properties = kwargs - if "updated_ts" not in properties: - properties["updated_ts"] = now_timestamp() + properties["updated_ts"] = now_timestamp() for where in snapshot_id_filter( self.engine_adapter, snapshots, batch_size=self.SNAPSHOT_BATCH_SIZE @@ -431,7 +466,6 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: updated_ts, unpaused_ts, unrestorable, - forward_only, next_auto_restatement_ts, ) in fetchall(self.engine_adapter, query): snapshot = parse_snapshot( @@ -439,7 +473,6 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: updated_ts=updated_ts, unpaused_ts=unpaused_ts, unrestorable=unrestorable, - forward_only=forward_only, next_auto_restatement_ts=next_auto_restatement_ts, ) snapshot_id = snapshot.snapshot_id @@ -469,7 +502,6 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: "updated_ts", "unpaused_ts", "unrestorable", - "forward_only", "next_auto_restatement_ts", ) .from_(exp.to_table(self.snapshots_table).as_("snapshots")) @@ -496,7 +528,6 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: updated_ts, unpaused_ts, unrestorable, - forward_only, next_auto_restatement_ts, ) in fetchall(self.engine_adapter, query): snapshot_id = SnapshotId(name=name, identifier=identifier) @@ -504,7 +535,6 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: snapshot.updated_ts = updated_ts snapshot.unpaused_ts = unpaused_ts snapshot.unrestorable = unrestorable - snapshot.forward_only = forward_only snapshot.next_auto_restatement_ts = next_auto_restatement_ts cached_snapshots_in_state.add(snapshot_id) @@ -538,7 +568,6 @@ def _get_snapshots_expressions( "snapshots.updated_ts", "snapshots.unpaused_ts", "snapshots.unrestorable", - "snapshots.forward_only", "auto_restatements.next_auto_restatement_ts", ) .from_(exp.to_table(self.snapshots_table).as_("snapshots")) @@ -594,7 +623,6 @@ def _get_snapshots_with_same_version( "updated_ts", "unpaused_ts", "unrestorable", - "forward_only", ) .from_(exp.to_table(self.snapshots_table).as_("snapshots")) .where(where) @@ -612,10 +640,9 @@ def _get_snapshots_with_same_version( updated_ts=updated_ts, unpaused_ts=unpaused_ts, unrestorable=unrestorable, - forward_only=forward_only, snapshot=snapshot, ) - for snapshot, name, identifier, version, updated_ts, unpaused_ts, unrestorable, forward_only in snapshot_rows + for snapshot, name, identifier, version, updated_ts, unpaused_ts, unrestorable in snapshot_rows ] @@ -624,7 +651,6 @@ def parse_snapshot( updated_ts: int, unpaused_ts: t.Optional[int], unrestorable: bool, - forward_only: bool, next_auto_restatement_ts: t.Optional[int], ) -> Snapshot: return Snapshot( @@ -633,7 +659,6 @@ def parse_snapshot( "updated_ts": updated_ts, "unpaused_ts": unpaused_ts, "unrestorable": unrestorable, - "forward_only": forward_only, "next_auto_restatement_ts": next_auto_restatement_ts, } ) @@ -648,7 +673,6 @@ def _snapshot_to_json(snapshot: Snapshot) -> str: "updated_ts", "unpaused_ts", "unrestorable", - "forward_only", "next_auto_restatement_ts", } ) @@ -669,7 +693,6 @@ def _snapshots_to_df(snapshots: t.Iterable[Snapshot]) -> pd.DataFrame: "unpaused_ts": snapshot.unpaused_ts, "ttl_ms": snapshot.ttl_ms, "unrestorable": snapshot.unrestorable, - "forward_only": snapshot.forward_only, } for snapshot in snapshots ] @@ -739,10 +762,19 @@ def full_snapshot(self) -> Snapshot: "updated_ts": self.updated_ts, "unpaused_ts": self.unpaused_ts, "unrestorable": self.unrestorable, - "forward_only": self.forward_only, } ) + def set_unpaused_ts(self, unpaused_dt: t.Optional[TimeLike]) -> None: + """Sets the timestamp for when this snapshot was unpaused. + + Args: + unpaused_dt: The datetime object of when this snapshot was unpaused. + """ + self.unpaused_ts = ( + to_timestamp(self.interval_unit.cron_floor(unpaused_dt)) if unpaused_dt else None + ) + @classmethod def from_snapshot_record( cls, @@ -753,7 +785,6 @@ def from_snapshot_record( updated_ts: int, unpaused_ts: t.Optional[int], unrestorable: bool, - forward_only: bool, snapshot: str, ) -> SharedVersionSnapshot: raw_snapshot = json.loads(snapshot) @@ -772,5 +803,5 @@ def from_snapshot_record( disable_restatement=raw_node.get("kind", {}).get("disable_restatement", False), effective_from=raw_snapshot.get("effective_from"), raw_snapshot=raw_snapshot, - forward_only=forward_only, + forward_only=raw_snapshot.get("forward_only", False), ) diff --git a/sqlmesh/core/state_sync/db/utils.py b/sqlmesh/core/state_sync/db/utils.py index 87c259f5d6..e5ffda6486 100644 --- a/sqlmesh/core/state_sync/db/utils.py +++ b/sqlmesh/core/state_sync/db/utils.py @@ -22,21 +22,6 @@ T = t.TypeVar("T") -def snapshot_name_filter( - snapshot_names: t.Iterable[str], - batch_size: int, - alias: t.Optional[str] = None, -) -> t.Iterator[exp.Condition]: - names = sorted(snapshot_names) - - if not names: - yield exp.false() - else: - batches = create_batches(names, batch_size=batch_size) - for names in batches: - yield exp.column("name", table=alias).isin(*names) - - def snapshot_id_filter( engine_adapter: EngineAdapter, snapshot_ids: t.Iterable[SnapshotIdLike], diff --git a/sqlmesh/migrations/v0090_add_forward_only_column.py b/sqlmesh/migrations/v0090_add_forward_only_column.py deleted file mode 100644 index 32efc14eed..0000000000 --- a/sqlmesh/migrations/v0090_add_forward_only_column.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Add forward_only column to the snapshots table.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type, blob_text_type - - -def migrate(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - alter_table_exp = exp.Alter( - this=exp.to_table(snapshots_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("forward_only"), - kind=exp.DataType.build("boolean"), - ) - ], - ) - engine_adapter.execute(alter_table_exp) - - new_snapshots = [] - - for ( - name, - identifier, - version, - snapshot, - kind_name, - updated_ts, - unpaused_ts, - ttl_ms, - unrestorable, - forward_only, - ) in engine_adapter.fetchall( - exp.select( - "name", - "identifier", - "version", - "snapshot", - "kind_name", - "updated_ts", - "unpaused_ts", - "ttl_ms", - "unrestorable", - "forward_only", - ).from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - - forward_only = parsed_snapshot.get("forward_only") - if forward_only is None: - forward_only = parsed_snapshot.get("change_category") == 3 - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - "updated_ts": updated_ts, - "unpaused_ts": unpaused_ts, - "ttl_ms": ttl_ms, - "unrestorable": unrestorable, - "forward_only": forward_only, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build(blob_type), - "kind_name": exp.DataType.build(index_type), - "updated_ts": exp.DataType.build("bigint"), - "unpaused_ts": exp.DataType.build("bigint"), - "ttl_ms": exp.DataType.build("bigint"), - "unrestorable": exp.DataType.build("boolean"), - "forward_only": exp.DataType.build("boolean"), - }, - ) diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index d61907a5aa..d8e96a1f35 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -1888,6 +1888,50 @@ def test_unpause_snapshots(state_sync: EngineAdapterStateSync, make_snapshot: t. assert not actual_snapshots[new_snapshot.snapshot_id].unrestorable +def test_unpause_snapshots_hourly(state_sync: EngineAdapterStateSync, make_snapshot: t.Callable): + snapshot = make_snapshot( + SqlModel( + name="test_snapshot", + query=parse_one("select 1, ds"), + cron="@hourly", + ), + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot.version = "a" + + assert not snapshot.unpaused_ts + state_sync.push_snapshots([snapshot]) + + # Unpaused timestamp not aligned with cron + unpaused_dt = "2022-01-01 01:22:33" + state_sync.unpause_snapshots([snapshot], unpaused_dt) + + actual_snapshot = state_sync.get_snapshots([snapshot])[snapshot.snapshot_id] + assert actual_snapshot.unpaused_ts + assert actual_snapshot.unpaused_ts == to_timestamp("2022-01-01 01:00:00") + + new_snapshot = make_snapshot( + SqlModel( + name="test_snapshot", + query=parse_one("select 2, ds"), + cron="@daily", + interval_unit="hour", + ) + ) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) + new_snapshot.version = "a" + + assert not new_snapshot.unpaused_ts + state_sync.push_snapshots([new_snapshot]) + state_sync.unpause_snapshots([new_snapshot], unpaused_dt) + + actual_snapshots = state_sync.get_snapshots([snapshot, new_snapshot]) + assert not actual_snapshots[snapshot.snapshot_id].unpaused_ts + assert actual_snapshots[new_snapshot.snapshot_id].unpaused_ts == to_timestamp( + "2022-01-01 01:00:00" + ) + + def test_unrestorable_snapshot(state_sync: EngineAdapterStateSync, make_snapshot: t.Callable): snapshot = make_snapshot( SqlModel( @@ -1993,6 +2037,81 @@ def test_unrestorable_snapshot_target_not_forward_only( assert not actual_snapshots[updated_snapshot.snapshot_id].unrestorable +def test_unpause_snapshots_remove_intervals( + state_sync: EngineAdapterStateSync, make_snapshot: t.Callable +): + snapshot = make_snapshot( + SqlModel( + name="test_snapshot", + query=parse_one("select 1, ds"), + cron="@daily", + ), + version="a", + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot.version = "a" + state_sync.push_snapshots([snapshot]) + state_sync.add_interval(snapshot, "2023-01-01", "2023-01-05") + + new_snapshot = make_snapshot( + SqlModel(name="test_snapshot", query=parse_one("select 2, ds"), cron="@daily"), + version="a", + ) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) + new_snapshot.version = "a" + new_snapshot.effective_from = "2023-01-03" + state_sync.push_snapshots([new_snapshot]) + state_sync.add_interval(snapshot, "2023-01-06", "2023-01-06") + state_sync.unpause_snapshots([new_snapshot], "2023-01-06") + + actual_snapshots = state_sync.get_snapshots([snapshot, new_snapshot]) + assert actual_snapshots[new_snapshot.snapshot_id].intervals == [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), + ] + assert actual_snapshots[snapshot.snapshot_id].intervals == [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), + ] + + +def test_unpause_snapshots_remove_intervals_disabled_restatement( + state_sync: EngineAdapterStateSync, make_snapshot: t.Callable +): + kind = dict(name="INCREMENTAL_BY_TIME_RANGE", time_column="ds", disable_restatement=True) + snapshot = make_snapshot( + SqlModel( + name="test_snapshot", + query=parse_one("select 1, ds"), + cron="@daily", + kind=kind, + ), + version="a", + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot.version = "a" + state_sync.push_snapshots([snapshot]) + state_sync.add_interval(snapshot, "2023-01-01", "2023-01-05") + + new_snapshot = make_snapshot( + SqlModel(name="test_snapshot", query=parse_one("select 2, ds"), cron="@daily", kind=kind), + version="a", + ) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) + new_snapshot.version = "a" + new_snapshot.effective_from = "2023-01-03" + state_sync.push_snapshots([new_snapshot]) + state_sync.add_interval(snapshot, "2023-01-06", "2023-01-06") + state_sync.unpause_snapshots([new_snapshot], "2023-01-06") + + actual_snapshots = state_sync.get_snapshots([snapshot, new_snapshot]) + assert actual_snapshots[new_snapshot.snapshot_id].intervals == [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), + ] + # The intervals shouldn't have been removed because restatement is disabled + assert actual_snapshots[snapshot.snapshot_id].intervals == [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-07")), + ] + + def test_version_schema(state_sync: EngineAdapterStateSync, tmp_path) -> None: from sqlmesh import __version__ as SQLMESH_VERSION @@ -2880,7 +2999,6 @@ def test_snapshot_batching(state_sync, mocker, make_snapshot): 1, 1, False, - False, None, ], [ @@ -2893,7 +3011,6 @@ def test_snapshot_batching(state_sync, mocker, make_snapshot): 1, 1, False, - False, None, ], ], @@ -2908,7 +3025,6 @@ def test_snapshot_batching(state_sync, mocker, make_snapshot): 1, 1, False, - False, None, ], ], From cdfedd3b6eac3c20ab165bda2389201b656216c3 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 14 Aug 2025 02:06:21 +0300 Subject: [PATCH 0696/1056] Chore!: bump sqlglot to v27.7.0 (#5149) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 517f3be426..bbb368a7bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.6.0", + "sqlglot[rs]~=27.7.0", "tenacity", "time-machine", "json-stream" From ee2a8bdaf4b84929a2c7d30dd5aa2eed744d20d7 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 13 Aug 2025 16:29:53 -0700 Subject: [PATCH 0697/1056] Fix: Skip cleanup of missing physical tables (#5150) --- sqlmesh/core/snapshot/evaluator.py | 21 +++++++--- tests/core/test_snapshot_evaluator.py | 56 +++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index a6dca27d35..3937f37fba 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -1073,11 +1073,22 @@ def _cleanup_snapshot( evaluation_strategy = _evaluation_strategy(snapshot, adapter) for is_table_deployable, table_name in table_names: - evaluation_strategy.delete( - table_name, - is_table_deployable=is_table_deployable, - physical_schema=snapshot.physical_schema, - ) + try: + evaluation_strategy.delete( + table_name, + is_table_deployable=is_table_deployable, + physical_schema=snapshot.physical_schema, + ) + except Exception: + # Use `get_data_object` to check if the table exists instead of `table_exists` since the former + # is based on `INFORMATION_SCHEMA` and avoids touching the table directly. + # This is important when the table name is malformed for some reason and running any statement + # that touches the table would result in an error. + if adapter.get_data_object(table_name) is not None: + raise + logger.warning( + "Skipping cleanup of table '%s' because it does not exist", table_name + ) if on_complete is not None: on_complete(table_name) diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 6c0763892e..d474159b7c 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -438,12 +438,14 @@ def create_and_cleanup(name: str, dev_table_only: bool): return snapshot snapshot = create_and_cleanup("catalog.test_schema.test_model", True) + adapter_mock.get_data_object.assert_not_called() adapter_mock.drop_table.assert_called_once_with( f"catalog.sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev" ) adapter_mock.reset_mock() snapshot = create_and_cleanup("test_schema.test_model", False) + adapter_mock.get_data_object.assert_not_called() adapter_mock.drop_table.assert_has_calls( [ call( @@ -455,6 +457,7 @@ def create_and_cleanup(name: str, dev_table_only: bool): adapter_mock.reset_mock() snapshot = create_and_cleanup("test_model", False) + adapter_mock.get_data_object.assert_not_called() adapter_mock.drop_table.assert_has_calls( [ call(f"sqlmesh__default.test_model__{snapshot.fingerprint.to_version()}__dev"), @@ -463,6 +466,59 @@ def create_and_cleanup(name: str, dev_table_only: bool): ) +def test_cleanup_fails(adapter_mock, make_snapshot): + adapter_mock.drop_table.side_effect = RuntimeError("test_error") + + evaluator = SnapshotEvaluator(adapter_mock) + + model = SqlModel( + name="catalog.test_schema.test_model", + kind=IncrementalByTimeRangeKind(time_column="a"), + storage_format="parquet", + query=parse_one("SELECT a FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), + ) + + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) + snapshot.version = "test_version" + + evaluator.promote([snapshot], EnvironmentNamingInfo(name="test_env")) + with pytest.raises(NodeExecutionFailedError) as exc_info: + evaluator.cleanup( + [SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True)] + ) + + assert str(exc_info.value.__cause__) == "test_error" + + +def test_cleanup_skip_missing_table(adapter_mock, make_snapshot): + adapter_mock.get_data_object.return_value = None + adapter_mock.drop_table.side_effect = RuntimeError("fail") + + evaluator = SnapshotEvaluator(adapter_mock) + + model = SqlModel( + name="catalog.test_schema.test_model", + kind=IncrementalByTimeRangeKind(time_column="a"), + storage_format="parquet", + query=parse_one("SELECT a FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), + ) + + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) + snapshot.version = "test_version" + + evaluator.promote([snapshot], EnvironmentNamingInfo(name="test_env")) + evaluator.cleanup([SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True)]) + + adapter_mock.get_data_object.assert_called_once_with( + f"catalog.sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev" + ) + adapter_mock.drop_table.assert_called_once_with( + f"catalog.sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev" + ) + + def test_cleanup_external_model(mocker: MockerFixture, adapter_mock, make_snapshot): evaluator = SnapshotEvaluator(adapter_mock) From 0c70406f00c0bc78a71729899bbfb3e68f6312f1 Mon Sep 17 00:00:00 2001 From: David Dai Date: Wed, 13 Aug 2025 16:38:04 -0700 Subject: [PATCH 0698/1056] Fix!: sqlmesh.dbt.adapter.RuntimeAdapter.get_columns_in_relation() (#5115) --- pyproject.toml | 3 ++ sqlmesh/core/engine_adapter/bigquery.py | 7 ++++ sqlmesh/dbt/adapter.py | 26 ++++++++++-- .../integration/test_integration_bigquery.py | 33 +++++++++++++++ tests/dbt/test_adapter.py | 40 ++++++++++++++++++- 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bbb368a7bd..4372c84861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -273,6 +273,9 @@ filterwarnings = [ ] retry_delay = 10 +[tool.ruff] +line-length = 100 + [tool.ruff.lint] select = [ "F401", diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 1b32195f77..d3280db137 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -292,6 +292,13 @@ def create_schema( raise logger.warning("Failed to create schema '%s': %s", schema_name, e) + def get_bq_schema(self, table_name: TableName) -> t.List[bigquery.SchemaField]: + table = exp.to_table(table_name) + if len(table.parts) == 3 and "." in table.name: + self.execute(exp.select("*").from_(table).limit(0)) + return self._query_job._query_results.schema + return self._get_table(table).schema + def columns( self, table_name: TableName, include_pseudo_columns: bool = False ) -> t.Dict[str, exp.DataType]: diff --git a/sqlmesh/dbt/adapter.py b/sqlmesh/dbt/adapter.py index 92719abacc..4178c960a7 100644 --- a/sqlmesh/dbt/adapter.py +++ b/sqlmesh/dbt/adapter.py @@ -291,11 +291,31 @@ def list_relations_without_caching(self, schema_relation: BaseRelation) -> t.Lis return relations def get_columns_in_relation(self, relation: BaseRelation) -> t.List[Column]: - from dbt.adapters.base.column import Column - mapped_table = self._map_table_name(self._normalize(self._relation_to_table(relation))) + + if self.project_dialect == "bigquery": + # dbt.adapters.bigquery.column.BigQueryColumn has a different constructor signature + # We need to use BigQueryColumn.create_from_field() to create the column instead + if ( + hasattr(self.column_type, "create_from_field") + and callable(getattr(self.column_type, "create_from_field")) + and hasattr(self.engine_adapter, "get_bq_schema") + and callable(getattr(self.engine_adapter, "get_bq_schema")) + ): + return [ + self.column_type.create_from_field(field) # type: ignore + for field in self.engine_adapter.get_bq_schema(mapped_table) # type: ignore + ] + from dbt.adapters.base.column import Column + + return [ + Column.from_description( + name=name, raw_data_type=dtype.sql(dialect=self.project_dialect) + ) + for name, dtype in self.engine_adapter.columns(table_name=mapped_table).items() + ] return [ - Column.from_description( + self.column_type.from_description( name=name, raw_data_type=dtype.sql(dialect=self.project_dialect) ) for name, dtype in self.engine_adapter.columns(table_name=mapped_table).items() diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index c97c94d036..e1cfaded13 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -341,6 +341,39 @@ def test_compare_nested_values_in_table_diff(ctx: TestContext): ctx.engine_adapter.drop_table(target_table) +def test_get_bq_schema(ctx: TestContext, engine_adapter: BigQueryEngineAdapter): + from google.cloud.bigquery import SchemaField + + table = ctx.table("test") + + engine_adapter.execute(f""" + CREATE TABLE {table.sql(dialect=ctx.dialect)} ( + id STRING NOT NULL, + user_data STRUCT, + tags ARRAY, + score NUMERIC, + created_at DATETIME + ) + """) + + bg_schema = engine_adapter.get_bq_schema(table) + assert len(bg_schema) == 5 + assert bg_schema[0] == SchemaField(name="id", field_type="STRING", mode="REQUIRED") + assert bg_schema[1] == SchemaField( + name="user_data", + field_type="RECORD", + mode="NULLABLE", + fields=[ + SchemaField(name="id", field_type="STRING", mode="REQUIRED"), + SchemaField(name="name", field_type="STRING", mode="REQUIRED"), + SchemaField(name="address", field_type="STRING", mode="NULLABLE"), + ], + ) + assert bg_schema[2] == SchemaField(name="tags", field_type="STRING", mode="REPEATED") + assert bg_schema[3] == SchemaField(name="score", field_type="NUMERIC", mode="NULLABLE") + assert bg_schema[4] == SchemaField(name="created_at", field_type="DATETIME", mode="NULLABLE") + + def test_column_types(ctx: TestContext): model_name = ctx.table("test") sqlmesh = ctx.create_context() diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index 31428b953c..73a2e1f1f2 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -17,7 +17,7 @@ from sqlmesh.dbt.adapter import ParsetimeAdapter from sqlmesh.dbt.project import Project from sqlmesh.dbt.relation import Policy -from sqlmesh.dbt.target import SnowflakeConfig +from sqlmesh.dbt.target import BigQueryConfig, SnowflakeConfig from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.jinja import JinjaMacroRegistry @@ -68,6 +68,44 @@ def test_adapter_relation(sushi_test_project: Project, runtime_renderer: t.Calla ) +def test_bigquery_get_columns_in_relation( + sushi_test_project: Project, + runtime_renderer: t.Callable, + mocker: MockerFixture, +): + from dbt.adapters.bigquery import BigQueryColumn + from google.cloud.bigquery import SchemaField + + context = sushi_test_project.context + context.target = BigQueryConfig(name="test", schema="test", database="test") + + adapter_mock = mocker.MagicMock() + adapter_mock.default_catalog = "test" + adapter_mock.dialect = "bigquery" + table_schema = [ + SchemaField(name="id", field_type="STRING", mode="REQUIRED"), + SchemaField( + name="user_data", + field_type="RECORD", + mode="NULLABLE", + fields=[ + SchemaField(name="id", field_type="STRING", mode="REQUIRED"), + SchemaField(name="name", field_type="STRING", mode="REQUIRED"), + SchemaField(name="address", field_type="STRING", mode="NULLABLE"), + ], + ), + SchemaField(name="tags", field_type="STRING", mode="REPEATED"), + SchemaField(name="score", field_type="NUMERIC", mode="NULLABLE"), + SchemaField(name="created_at", field_type="TIMESTAMP", mode="NULLABLE"), + ] + adapter_mock.get_bq_schema.return_value = table_schema + renderer = runtime_renderer(context, engine_adapter=adapter_mock, dialect="bigquery") + assert renderer( + "{%- set relation = api.Relation.create(database='test', schema='test', identifier='test_table') -%}" + "{{ adapter.get_columns_in_relation(relation) }}" + ) == str([BigQueryColumn.create_from_field(field) for field in table_schema]) + + @pytest.mark.cicdonly def test_normalization( sushi_test_project: Project, runtime_renderer: t.Callable, mocker: MockerFixture From c9f765182b69bad2f6e8c8ca193dadbb0a6f1cf1 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:50:30 +0300 Subject: [PATCH 0699/1056] Fix: show optimized_query, column_definitions in diff (#5151) --- sqlmesh/core/model/definition.py | 13 ++--- tests/core/test_model.py | 49 +++++++++++++++++++ .../github/cicd/test_integration.py | 2 +- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 1d0f9ad66c..768fcd2c02 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -244,14 +244,7 @@ def render_definition( value=exp.to_table(field_value, dialect=self.dialect), ) ) - elif field_name not in ( - "column_descriptions_", - "default_catalog", - "enabled", - "inline_audits", - "optimize_query", - "ignored_rules_", - ): + elif field_name not in ("default_catalog", "enabled", "ignored_rules_"): expressions.append( exp.Property( this=field_info.alias or field_name, @@ -2940,6 +2933,9 @@ def render_expression( "columns_to_types_": lambda value: exp.Schema( expressions=[exp.ColumnDef(this=exp.to_column(c), kind=t) for c, t in value.items()] ), + "column_descriptions_": lambda value: exp.Schema( + expressions=[exp.to_column(c).eq(d) for c, d in value.items()] + ), "tags": single_value_or_tuple, "grains": _refs_to_sql, "references": _refs_to_sql, @@ -2958,6 +2954,7 @@ def render_expression( ) ), "formatting": str, + "optimize_query": str, "virtual_environment_mode": lambda value: exp.Literal.string(value.value), } diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 6bc5388468..df758713a6 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -11359,3 +11359,52 @@ def test_extract_macro_func_variable_references(macro_func: str, variables: t.Se macro_func_ast = parse_one(macro_func) assert _extract_macro_func_variable_references(macro_func_ast, True)[0] == variables + + +def test_text_diff_column_descriptions(): + """Test that column_descriptions changes are visible in text_diff.""" + # Create model without column descriptions + model1 = create_sql_model( + name="test.model", + query=parse("SELECT id, name FROM upstream")[0], + ) + + # Create model with column descriptions + model2 = create_sql_model( + name="test.model", + query=parse("SELECT id, name FROM upstream")[0], + column_descriptions={"id": "User identifier", "name": "User name"}, + ) + + # Verify the diff shows the column_descriptions + diff = model1.text_diff(model2) + assert diff, "Expected diff to show column_descriptions change" + assert "+ id = 'User identifier'," in diff + assert "+ name = 'User name'" in diff + + # Verify reverse diff also works + diff = model2.text_diff(model1) + assert diff, "Expected reverse diff to show column_descriptions removal" + assert "- id = 'User identifier'," in diff + assert "- name = 'User name'" in diff + + +def test_text_diff_optimize_query(): + """Test that optimize_query changes are visible in text_diff.""" + # Create model without optimize_query + model1 = create_sql_model( + name="test.model", + query=parse("SELECT id, name FROM upstream")[0], + ) + + # Create model with optimize_query enabled + model2 = create_sql_model( + name="test.model", + query=parse("SELECT id, name FROM upstream")[0], + optimize_query=True, + ) + + # Verify the diff shows the optimize_query change + diff = model1.text_diff(model2) + assert diff, "Expected diff to show optimize_query change" + assert "+ optimize_query" in diff.lower() diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index 15e8be0f6b..3fb965f310 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -1789,7 +1789,7 @@ def test_overlapping_changes_models( +++ - @@ -29,7 +29,8 @@ + @@ -32,7 +32,8 @@ SELECT DISTINCT CAST(o.customer_id AS INT) AS customer_id, From bbcbd7122051083962b51f6e6dc2d4895ebbebd8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:25:57 +0200 Subject: [PATCH 0700/1056] chore(deps-dev): bump @testing-library/dom from 10.4.0 to 10.4.1 (#5094) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 192 +++++++++++++++++++++++++------------- vscode/react/package.json | 2 +- 2 files changed, 129 insertions(+), 65 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ab6213fa4..e92c12dd30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,28 +162,28 @@ importers: devDependencies: '@chromatic-com/storybook': specifier: ^4.0.1 - version: 4.0.1(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2)) + version: 4.0.1(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) '@storybook/addon-a11y': specifier: ^9.0.18 - version: 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2)) + version: 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) '@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.0)(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)) '@storybook/addon-onboarding': specifier: ^9.0.18 - version: 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2)) + version: 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) '@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.0)(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))(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.0)(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.43.1)(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))(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@testing-library/dom': - specifier: ^10.4.0 - version: 10.4.0 + specifier: ^10.4.1 + version: 10.4.1 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@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) + version: 16.3.0(@testing-library/dom@10.4.1)(@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) '@types/react': specifier: ^18.3.23 version: 18.3.23 @@ -207,7 +207,7 @@ importers: version: 1.54.1 storybook: specifier: ^9.0.18 - version: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) + version: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) typescript: specifier: ^5.8.3 version: 5.8.3 @@ -279,7 +279,7 @@ importers: version: 2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@uiw/react-codemirror': specifier: ^4.24.1 - version: 4.24.1(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.24.1(@babel/runtime@7.28.2)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -340,10 +340,10 @@ importers: version: 6.6.3 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@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) + version: 16.3.0(@testing-library/dom@10.4.1)(@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) '@testing-library/user-event': specifier: ^14.6.1 - version: 14.6.1(@testing-library/dom@10.4.0) + version: 14.6.1(@testing-library/dom@10.4.1) '@types/pluralize': specifier: ^0.0.33 version: 0.0.33 @@ -652,6 +652,10 @@ packages: resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.2': + resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -1062,19 +1066,28 @@ packages: '@jridgewell/gen-mapping@0.3.12': resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.10': - resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} '@jridgewell/sourcemap-codec@1.5.4': resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@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==} + '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} @@ -2209,8 +2222,8 @@ packages: resolution: {integrity: sha512-a+MxoAXG+Sq94Jp67OtveKOp2vQq75AWdVI8DRt6w19B0NEqpfm784FTLbVp/qdR1wmxCOmKAvElGSIiBOx5OQ==} engines: {node: '>=12'} - '@testing-library/dom@10.4.0': - resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} '@testing-library/jest-dom@6.6.3': @@ -3045,6 +3058,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.25.2: + resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -3100,6 +3118,9 @@ packages: caniuse-lite@1.0.30001727: resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + caniuse-lite@1.0.30001735: + resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -3496,6 +3517,9 @@ packages: electron-to-chromium@1.5.190: resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==} + electron-to-chromium@1.5.200: + resolution: {integrity: sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==} + elkjs@0.8.2: resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} @@ -3518,6 +3542,10 @@ packages: resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -6775,6 +6803,8 @@ snapshots: '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.2': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -6802,13 +6832,13 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@chromatic-com/storybook@4.0.1(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@chromatic-com/storybook@4.0.1(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': 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.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) strip-ansi: 7.1.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -7172,20 +7202,32 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.4 '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/source-map@0.3.10': + '@jridgewell/source-map@0.3.11': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/sourcemap-codec@1.5.4': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.29': dependencies: '@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 + '@jsdevtools/ono@7.1.3': {} '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': @@ -8172,35 +8214,35 @@ 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.0)(prettier@3.6.2))': + '@storybook/addon-a11y@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.10.3 - storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) + 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.0)(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))': 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.0)(prettier@3.6.2)) + '@storybook/csf-plugin': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) '@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.0)(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)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-onboarding@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/addon-onboarding@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': dependencies: - storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) - '@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.0)(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))(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.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) 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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) @@ -8210,16 +8252,16 @@ snapshots: - react - react-dom - '@storybook/builder-vite@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@storybook/csf-plugin': 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2)) - storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) + '@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) ts-dedent: 2.2.0 vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) - '@storybook/csf-plugin@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/csf-plugin@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': dependencies: - storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -8229,25 +8271,25 @@ 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.0)(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))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) - '@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.0)(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.43.1)(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))(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.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.0)(prettier@3.6.2))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.0)(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))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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) find-up: 7.0.0 magic-string: 0.30.17 react: 18.3.1 react-docgen: 8.0.0 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 - storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) tsconfig-paths: 4.2.0 vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: @@ -8255,13 +8297,13 @@ snapshots: - 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.0)(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))(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.0)(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)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) optionalDependencies: typescript: 5.8.3 @@ -8531,15 +8573,15 @@ snapshots: '@tanstack/virtual-file-routes@1.129.7': {} - '@testing-library/dom@10.4.0': + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 '@types/aria-query': 5.0.4 aria-query: 5.3.0 - chalk: 4.1.2 dom-accessibility-api: 0.5.16 lz-string: 1.5.0 + picocolors: 1.1.1 pretty-format: 27.5.1 '@testing-library/jest-dom@6.6.3': @@ -8552,19 +8594,19 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@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)': + '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@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)': dependencies: '@babel/runtime': 7.27.6 - '@testing-library/dom': 10.4.0 + '@testing-library/dom': 10.4.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: - '@testing-library/dom': 10.4.0 + '@testing-library/dom': 10.4.1 '@textlint/ast-node-types@15.2.0': {} @@ -8960,9 +9002,9 @@ snapshots: '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.1 - '@uiw/react-codemirror@4.24.1(@babel/runtime@7.27.6)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@uiw/react-codemirror@4.24.1(@babel/runtime@7.28.2)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.2)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.1)(codemirror@6.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 '@codemirror/commands': 6.8.1 '@codemirror/state': 6.5.2 '@codemirror/theme-one-dark': 6.1.2 @@ -9001,8 +9043,8 @@ snapshots: '@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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': dependencies: - '@testing-library/dom': 10.4.0 - '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) + '@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/utils': 3.2.3 magic-string: 0.30.17 @@ -9591,6 +9633,13 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) + browserslist@4.25.2: + dependencies: + caniuse-lite: 1.0.30001735 + electron-to-chromium: 1.5.200 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.2) + buffer-crc32@0.2.13: {} buffer-equal-constant-time@1.0.1: {} @@ -9650,6 +9699,8 @@ snapshots: caniuse-lite@1.0.30001727: {} + caniuse-lite@1.0.30001735: {} + ccount@2.0.1: {} chai@5.2.1: @@ -10031,6 +10082,8 @@ snapshots: electron-to-chromium@1.5.190: {} + electron-to-chromium@1.5.200: {} + elkjs@0.8.2: {} emoji-regex@10.4.0: {} @@ -10054,6 +10107,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -12159,7 +12217,7 @@ snapshots: redux@4.2.1: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 reflect.getprototypeof@1.0.10: dependencies: @@ -12505,11 +12563,11 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@3.6.2): + storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2): 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.0) + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/expect': 3.2.4 '@vitest/spy': 3.2.4 better-opn: 3.0.2 @@ -12750,7 +12808,7 @@ snapshots: terser-webpack-plugin@5.3.14(esbuild@0.25.8)(webpack@5.99.8(esbuild@0.25.8)): dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.30 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 @@ -12761,7 +12819,7 @@ snapshots: terser@5.43.1: dependencies: - '@jridgewell/source-map': 0.3.10 + '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -13044,6 +13102,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.1.3(browserslist@4.25.2): + dependencies: + browserslist: 4.25.2 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -13339,9 +13403,9 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 - browserslist: 4.25.1 + browserslist: 4.25.2 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.2 + enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 diff --git a/vscode/react/package.json b/vscode/react/package.json index bafcc0c556..49b60b90dc 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -43,7 +43,7 @@ "@storybook/addon-onboarding": "^9.0.18", "@storybook/addon-vitest": "^9.0.18", "@storybook/react-vite": "^9.0.18", - "@testing-library/dom": "^10.4.0", + "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", From 9a6d7ab6f7589b64dd45cfb90c45ad59eaf21868 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:26:08 +0200 Subject: [PATCH 0701/1056] chore(deps): bump actions/checkout from 4 to 5 (#5136) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr.yaml | 4 ++-- .github/workflows/private-repo-test.yaml | 2 +- .github/workflows/release_extension.yaml | 2 +- .github/workflows/release_shared_js.yaml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 78253b1288..b63f6a3ab6 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -14,7 +14,7 @@ jobs: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: '22' @@ -29,7 +29,7 @@ jobs: runs-on: labels: [ubuntu-2204-8] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: '22' diff --git a/.github/workflows/private-repo-test.yaml b/.github/workflows/private-repo-test.yaml index e2d0748e79..838dff8235 100644 --- a/.github/workflows/private-repo-test.yaml +++ b/.github/workflows/private-repo-test.yaml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Python diff --git a/.github/workflows/release_extension.yaml b/.github/workflows/release_extension.yaml index 376791d434..bb52c32966 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@v4 + uses: actions/checkout@v5 - 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 ad5a57c34d..b6bd04ddd2 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@v4 + uses: actions/checkout@v5 - name: Check branch is main run: | if [[ "${{ github.ref }}" != "refs/heads/main" ]]; then From a2e4bbf5c96f6d7d4cedfc38cca94477f12fd7fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:36:39 +0200 Subject: [PATCH 0702/1056] chore(deps): bump actions/create-github-app-token from 1 to 2 (#5135) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/private-repo-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/private-repo-test.yaml b/.github/workflows/private-repo-test.yaml index 838dff8235..eaa71885ff 100644 --- a/.github/workflows/private-repo-test.yaml +++ b/.github/workflows/private-repo-test.yaml @@ -77,7 +77,7 @@ jobs: unset TWINE_USERNAME TWINE_PASSWORD && make publish-tests - name: Get GitHub App token id: get_token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 with: private-key: ${{ secrets.TOBIKO_RENOVATE_BOT_PRIVATE_KEY }} app-id: ${{ secrets.TOBIKO_RENOVATE_BOT_APP_ID }} From e4ea4c83a4b823370cc40e3317d1aaffe740964c Mon Sep 17 00:00:00 2001 From: David Dai Date: Thu, 14 Aug 2025 03:53:55 -0700 Subject: [PATCH 0703/1056] fix: BigQueryEngineAdapter.get_table_schema() mypy error (#5153) --- sqlmesh/core/engine_adapter/bigquery.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index d3280db137..2de3f18c99 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -296,7 +296,9 @@ def get_bq_schema(self, table_name: TableName) -> t.List[bigquery.SchemaField]: table = exp.to_table(table_name) if len(table.parts) == 3 and "." in table.name: self.execute(exp.select("*").from_(table).limit(0)) - return self._query_job._query_results.schema + query_job = self._query_job + assert query_job is not None + return query_job._query_results.schema return self._get_table(table).schema def columns( From 2897b4e5f3817908b66743e5cbac55d9f4cbd968 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 14 Aug 2025 08:21:44 -0700 Subject: [PATCH 0704/1056] fix: selector use provided models and fix mypy (#5148) --- sqlmesh/core/selector.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sqlmesh/core/selector.py b/sqlmesh/core/selector.py index be460d8ce3..c44065bdc0 100644 --- a/sqlmesh/core/selector.py +++ b/sqlmesh/core/selector.py @@ -178,10 +178,10 @@ def expand_model_selections( node = parse(" | ".join(f"({s})" for s in model_selections)) - models = models or self._models + all_models = models or self._models models_by_tags: t.Dict[str, t.Set[str]] = {} - for fqn, model in models.items(): + for fqn, model in all_models.items(): for tag in model.tags: tag = tag.lower() models_by_tags.setdefault(tag, set()) @@ -193,11 +193,11 @@ def evaluate(node: exp.Expression) -> t.Set[str]: if "*" in pattern: return { fqn - for fqn, model in models.items() + for fqn, model in all_models.items() if fnmatch.fnmatchcase(model.name, node.this) } fqn = normalize_model_name(pattern, self._default_catalog, self._dialect) - return {fqn} if fqn in models else set() + return {fqn} if fqn in all_models else set() if isinstance(node, exp.And): return evaluate(node.left) & evaluate(node.right) if isinstance(node, exp.Or): @@ -205,7 +205,7 @@ def evaluate(node: exp.Expression) -> t.Set[str]: if isinstance(node, exp.Paren): return evaluate(node.this) if isinstance(node, exp.Not): - return set(models) - evaluate(node.this) + return set(all_models) - evaluate(node.this) if isinstance(node, Git): target_branch = node.name git_modified_files = { @@ -213,7 +213,7 @@ def evaluate(node: exp.Expression) -> t.Set[str]: *self._git_client.list_uncommitted_changed_files(), *self._git_client.list_committed_changed_files(target_branch=target_branch), } - return {m.fqn for m in self._models.values() if m._path in git_modified_files} + return {m.fqn for m in all_models.values() if m._path in git_modified_files} if isinstance(node, Tag): pattern = node.name.lower() @@ -232,7 +232,7 @@ def evaluate(node: exp.Expression) -> t.Set[str]: selected.add(model_name) if node.args.get("up"): for u in self._dag.upstream(model_name): - if u in models: + if u in all_models: selected.add(u) if node.args.get("down"): selected.update(self._dag.downstream(model_name)) From ded67956cf9f8ae83e780ab4ef72aed627a4cfe2 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 14 Aug 2025 09:33:56 -0700 Subject: [PATCH 0705/1056] Chore!: Optimize snapshot unpausing (#5147) --- sqlmesh/core/state_sync/db/facade.py | 2 +- sqlmesh/core/state_sync/db/migrator.py | 2 +- sqlmesh/core/state_sync/db/snapshot.py | 147 +++++++----------- sqlmesh/core/state_sync/db/utils.py | 15 ++ .../v0090_add_forward_only_column.py | 100 ++++++++++++ tests/core/state_sync/test_state_sync.py | 122 +-------------- 6 files changed, 180 insertions(+), 208 deletions(-) create mode 100644 sqlmesh/migrations/v0090_add_forward_only_column.py diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 779add1cca..858b1aa072 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -256,7 +256,7 @@ def finalize(self, environment: Environment) -> None: def unpause_snapshots( self, snapshots: t.Collection[SnapshotInfoLike], unpaused_dt: TimeLike ) -> None: - self.snapshot_state.unpause_snapshots(snapshots, unpaused_dt, self.interval_state) + self.snapshot_state.unpause_snapshots(snapshots, unpaused_dt) def invalidate_environment(self, name: str, protect_prod: bool = True) -> None: self.environment_state.invalidate_environment(name, protect_prod) diff --git a/sqlmesh/core/state_sync/db/migrator.py b/sqlmesh/core/state_sync/db/migrator.py index 405c0ea667..ca89668763 100644 --- a/sqlmesh/core/state_sync/db/migrator.py +++ b/sqlmesh/core/state_sync/db/migrator.py @@ -396,7 +396,7 @@ def _migrate_environment_rows( if updated_prod_environment: try: self.snapshot_state.unpause_snapshots( - updated_prod_environment.snapshots, now_timestamp(), self.interval_state + updated_prod_environment.snapshots, now_timestamp() ) except Exception: logger.warning("Failed to unpause migrated snapshots", exc_info=True) diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 30e0de00f2..3745a27bb3 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -10,6 +10,7 @@ from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.state_sync.db.utils import ( + snapshot_name_filter, snapshot_name_version_filter, snapshot_id_filter, fetchone, @@ -32,15 +33,13 @@ SnapshotChangeCategory, ) from sqlmesh.utils.migration import index_text_type, blob_text_type -from sqlmesh.utils.date import now_timestamp, TimeLike, now, to_timestamp +from sqlmesh.utils.date import now_timestamp, TimeLike, to_timestamp from sqlmesh.utils.pydantic import PydanticModel from sqlmesh.utils import unique if t.TYPE_CHECKING: import pandas as pd - from sqlmesh.core.state_sync.db.interval import IntervalState - logger = logging.getLogger(__name__) @@ -70,6 +69,7 @@ def __init__( "unpaused_ts": exp.DataType.build("bigint"), "ttl_ms": exp.DataType.build("bigint"), "unrestorable": exp.DataType.build("boolean"), + "forward_only": exp.DataType.build("boolean"), } self._auto_restatement_columns_to_types = { @@ -112,84 +112,52 @@ def unpause_snapshots( self, snapshots: t.Collection[SnapshotInfoLike], unpaused_dt: TimeLike, - interval_state: IntervalState, ) -> None: - """Unpauses given snapshots while pausing all other snapshots that share the same version. - - Args: - snapshots: The snapshots to unpause. - unpaused_dt: The timestamp to unpause the snapshots at. - interval_state: The interval state to use to remove intervals when needed. - """ - current_ts = now() - - target_snapshot_ids = {s.snapshot_id for s in snapshots} - same_version_snapshots = self._get_snapshots_with_same_version( - snapshots, lock_for_update=True + unrestorable_snapshots_by_forward_only: t.Dict[bool, t.List[SnapshotNameVersion]] = ( + defaultdict(list) ) - target_snapshots_by_version = { - (s.name, s.version): s - for s in same_version_snapshots - if s.snapshot_id in target_snapshot_ids - } - - unpaused_snapshots: t.Dict[int, t.List[SnapshotId]] = defaultdict(list) - paused_snapshots: t.List[SnapshotId] = [] - unrestorable_snapshots: t.List[SnapshotId] = [] - - for snapshot in same_version_snapshots: - is_target_snapshot = snapshot.snapshot_id in target_snapshot_ids - if is_target_snapshot and not snapshot.unpaused_ts: - logger.info("Unpausing snapshot %s", snapshot.snapshot_id) - snapshot.set_unpaused_ts(unpaused_dt) - assert snapshot.unpaused_ts is not None - unpaused_snapshots[snapshot.unpaused_ts].append(snapshot.snapshot_id) - elif not is_target_snapshot: - target_snapshot = target_snapshots_by_version[(snapshot.name, snapshot.version)] - if ( - target_snapshot.normalized_effective_from_ts - and not target_snapshot.disable_restatement - ): - # Making sure that there are no overlapping intervals. - effective_from_ts = target_snapshot.normalized_effective_from_ts - logger.info( - "Removing all intervals after '%s' for snapshot %s, superseded by snapshot %s", - target_snapshot.effective_from, - snapshot.snapshot_id, - target_snapshot.snapshot_id, - ) - full_snapshot = snapshot.full_snapshot - interval_state.remove_intervals( - [ - ( - full_snapshot, - full_snapshot.get_removal_interval(effective_from_ts, current_ts), - ) - ] - ) - if snapshot.unpaused_ts: - logger.info("Pausing snapshot %s", snapshot.snapshot_id) - snapshot.set_unpaused_ts(None) - paused_snapshots.append(snapshot.snapshot_id) + for snapshot in snapshots: + # We need to mark all other snapshots that have forward-only opposite to the target snapshot as unrestorable + unrestorable_snapshots_by_forward_only[not snapshot.is_forward_only].append( + snapshot.name_version + ) - if not snapshot.unrestorable and ( - (target_snapshot.is_forward_only and not snapshot.is_forward_only) - or (snapshot.is_forward_only and not target_snapshot.is_forward_only) - ): - logger.info("Marking snapshot %s as unrestorable", snapshot.snapshot_id) - snapshot.unrestorable = True - unrestorable_snapshots.append(snapshot.snapshot_id) + updated_ts = now_timestamp() + unpaused_ts = to_timestamp(unpaused_dt) - if unpaused_snapshots: - for unpaused_ts, snapshot_ids in unpaused_snapshots.items(): - self._update_snapshots(snapshot_ids, unpaused_ts=unpaused_ts) + # Pause all snapshots with target names first + for where in snapshot_name_filter( + [s.name for s in snapshots], + batch_size=self.SNAPSHOT_BATCH_SIZE, + ): + self.engine_adapter.update_table( + self.snapshots_table, + {"unpaused_ts": None, "updated_ts": updated_ts}, + where=where, + ) - if paused_snapshots: - self._update_snapshots(paused_snapshots, unpaused_ts=None) + # Now unpause the target snapshots + self._update_snapshots( + [s.snapshot_id for s in snapshots], + unpaused_ts=unpaused_ts, + updated_ts=updated_ts, + ) - if unrestorable_snapshots: - self._update_snapshots(unrestorable_snapshots, unrestorable=True) + # Mark unrestorable snapshots + for forward_only, snapshot_name_versions in unrestorable_snapshots_by_forward_only.items(): + forward_only_exp = exp.column("forward_only").is_(exp.convert(forward_only)) + for where in snapshot_name_version_filter( + self.engine_adapter, + snapshot_name_versions, + batch_size=self.SNAPSHOT_BATCH_SIZE, + alias=None, + ): + self.engine_adapter.update_table( + self.snapshots_table, + {"unrestorable": True, "updated_ts": updated_ts}, + where=forward_only_exp.and_(where), + ) def get_expired_snapshots( self, @@ -414,7 +382,8 @@ def _update_snapshots( **kwargs: t.Any, ) -> None: properties = kwargs - properties["updated_ts"] = now_timestamp() + if "updated_ts" not in properties: + properties["updated_ts"] = now_timestamp() for where in snapshot_id_filter( self.engine_adapter, snapshots, batch_size=self.SNAPSHOT_BATCH_SIZE @@ -466,6 +435,7 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: updated_ts, unpaused_ts, unrestorable, + forward_only, next_auto_restatement_ts, ) in fetchall(self.engine_adapter, query): snapshot = parse_snapshot( @@ -473,6 +443,7 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: updated_ts=updated_ts, unpaused_ts=unpaused_ts, unrestorable=unrestorable, + forward_only=forward_only, next_auto_restatement_ts=next_auto_restatement_ts, ) snapshot_id = snapshot.snapshot_id @@ -502,6 +473,7 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: "updated_ts", "unpaused_ts", "unrestorable", + "forward_only", "next_auto_restatement_ts", ) .from_(exp.to_table(self.snapshots_table).as_("snapshots")) @@ -528,6 +500,7 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: updated_ts, unpaused_ts, unrestorable, + forward_only, next_auto_restatement_ts, ) in fetchall(self.engine_adapter, query): snapshot_id = SnapshotId(name=name, identifier=identifier) @@ -535,6 +508,7 @@ def _loader(snapshot_ids_to_load: t.Set[SnapshotId]) -> t.Collection[Snapshot]: snapshot.updated_ts = updated_ts snapshot.unpaused_ts = unpaused_ts snapshot.unrestorable = unrestorable + snapshot.forward_only = forward_only snapshot.next_auto_restatement_ts = next_auto_restatement_ts cached_snapshots_in_state.add(snapshot_id) @@ -568,6 +542,7 @@ def _get_snapshots_expressions( "snapshots.updated_ts", "snapshots.unpaused_ts", "snapshots.unrestorable", + "snapshots.forward_only", "auto_restatements.next_auto_restatement_ts", ) .from_(exp.to_table(self.snapshots_table).as_("snapshots")) @@ -623,6 +598,7 @@ def _get_snapshots_with_same_version( "updated_ts", "unpaused_ts", "unrestorable", + "forward_only", ) .from_(exp.to_table(self.snapshots_table).as_("snapshots")) .where(where) @@ -640,9 +616,10 @@ def _get_snapshots_with_same_version( updated_ts=updated_ts, unpaused_ts=unpaused_ts, unrestorable=unrestorable, + forward_only=forward_only, snapshot=snapshot, ) - for snapshot, name, identifier, version, updated_ts, unpaused_ts, unrestorable in snapshot_rows + for snapshot, name, identifier, version, updated_ts, unpaused_ts, unrestorable, forward_only in snapshot_rows ] @@ -651,6 +628,7 @@ def parse_snapshot( updated_ts: int, unpaused_ts: t.Optional[int], unrestorable: bool, + forward_only: bool, next_auto_restatement_ts: t.Optional[int], ) -> Snapshot: return Snapshot( @@ -659,6 +637,7 @@ def parse_snapshot( "updated_ts": updated_ts, "unpaused_ts": unpaused_ts, "unrestorable": unrestorable, + "forward_only": forward_only, "next_auto_restatement_ts": next_auto_restatement_ts, } ) @@ -673,6 +652,7 @@ def _snapshot_to_json(snapshot: Snapshot) -> str: "updated_ts", "unpaused_ts", "unrestorable", + "forward_only", "next_auto_restatement_ts", } ) @@ -693,6 +673,7 @@ def _snapshots_to_df(snapshots: t.Iterable[Snapshot]) -> pd.DataFrame: "unpaused_ts": snapshot.unpaused_ts, "ttl_ms": snapshot.ttl_ms, "unrestorable": snapshot.unrestorable, + "forward_only": snapshot.forward_only, } for snapshot in snapshots ] @@ -762,19 +743,10 @@ def full_snapshot(self) -> Snapshot: "updated_ts": self.updated_ts, "unpaused_ts": self.unpaused_ts, "unrestorable": self.unrestorable, + "forward_only": self.forward_only, } ) - def set_unpaused_ts(self, unpaused_dt: t.Optional[TimeLike]) -> None: - """Sets the timestamp for when this snapshot was unpaused. - - Args: - unpaused_dt: The datetime object of when this snapshot was unpaused. - """ - self.unpaused_ts = ( - to_timestamp(self.interval_unit.cron_floor(unpaused_dt)) if unpaused_dt else None - ) - @classmethod def from_snapshot_record( cls, @@ -785,6 +757,7 @@ def from_snapshot_record( updated_ts: int, unpaused_ts: t.Optional[int], unrestorable: bool, + forward_only: bool, snapshot: str, ) -> SharedVersionSnapshot: raw_snapshot = json.loads(snapshot) @@ -803,5 +776,5 @@ def from_snapshot_record( disable_restatement=raw_node.get("kind", {}).get("disable_restatement", False), effective_from=raw_snapshot.get("effective_from"), raw_snapshot=raw_snapshot, - forward_only=raw_snapshot.get("forward_only", False), + forward_only=forward_only, ) diff --git a/sqlmesh/core/state_sync/db/utils.py b/sqlmesh/core/state_sync/db/utils.py index e5ffda6486..87c259f5d6 100644 --- a/sqlmesh/core/state_sync/db/utils.py +++ b/sqlmesh/core/state_sync/db/utils.py @@ -22,6 +22,21 @@ T = t.TypeVar("T") +def snapshot_name_filter( + snapshot_names: t.Iterable[str], + batch_size: int, + alias: t.Optional[str] = None, +) -> t.Iterator[exp.Condition]: + names = sorted(snapshot_names) + + if not names: + yield exp.false() + else: + batches = create_batches(names, batch_size=batch_size) + for names in batches: + yield exp.column("name", table=alias).isin(*names) + + def snapshot_id_filter( engine_adapter: EngineAdapter, snapshot_ids: t.Iterable[SnapshotIdLike], diff --git a/sqlmesh/migrations/v0090_add_forward_only_column.py b/sqlmesh/migrations/v0090_add_forward_only_column.py new file mode 100644 index 0000000000..32efc14eed --- /dev/null +++ b/sqlmesh/migrations/v0090_add_forward_only_column.py @@ -0,0 +1,100 @@ +"""Add forward_only column to the snapshots table.""" + +import json + +from sqlglot import exp + +from sqlmesh.utils.migration import index_text_type, blob_text_type + + +def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + alter_table_exp = exp.Alter( + this=exp.to_table(snapshots_table), + kind="TABLE", + actions=[ + exp.ColumnDef( + this=exp.to_column("forward_only"), + kind=exp.DataType.build("boolean"), + ) + ], + ) + engine_adapter.execute(alter_table_exp) + + new_snapshots = [] + + for ( + name, + identifier, + version, + snapshot, + kind_name, + updated_ts, + unpaused_ts, + ttl_ms, + unrestorable, + forward_only, + ) in engine_adapter.fetchall( + exp.select( + "name", + "identifier", + "version", + "snapshot", + "kind_name", + "updated_ts", + "unpaused_ts", + "ttl_ms", + "unrestorable", + "forward_only", + ).from_(snapshots_table), + quote_identifiers=True, + ): + parsed_snapshot = json.loads(snapshot) + + forward_only = parsed_snapshot.get("forward_only") + if forward_only is None: + forward_only = parsed_snapshot.get("change_category") == 3 + + new_snapshots.append( + { + "name": name, + "identifier": identifier, + "version": version, + "snapshot": json.dumps(parsed_snapshot), + "kind_name": kind_name, + "updated_ts": updated_ts, + "unpaused_ts": unpaused_ts, + "ttl_ms": ttl_ms, + "unrestorable": unrestorable, + "forward_only": forward_only, + } + ) + + if new_snapshots: + engine_adapter.delete_from(snapshots_table, "TRUE") + index_type = index_text_type(engine_adapter.dialect) + blob_type = blob_text_type(engine_adapter.dialect) + + engine_adapter.insert_append( + snapshots_table, + pd.DataFrame(new_snapshots), + columns_to_types={ + "name": exp.DataType.build(index_type), + "identifier": exp.DataType.build(index_type), + "version": exp.DataType.build(index_type), + "snapshot": exp.DataType.build(blob_type), + "kind_name": exp.DataType.build(index_type), + "updated_ts": exp.DataType.build("bigint"), + "unpaused_ts": exp.DataType.build("bigint"), + "ttl_ms": exp.DataType.build("bigint"), + "unrestorable": exp.DataType.build("boolean"), + "forward_only": exp.DataType.build("boolean"), + }, + ) diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index d8e96a1f35..d61907a5aa 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -1888,50 +1888,6 @@ def test_unpause_snapshots(state_sync: EngineAdapterStateSync, make_snapshot: t. assert not actual_snapshots[new_snapshot.snapshot_id].unrestorable -def test_unpause_snapshots_hourly(state_sync: EngineAdapterStateSync, make_snapshot: t.Callable): - snapshot = make_snapshot( - SqlModel( - name="test_snapshot", - query=parse_one("select 1, ds"), - cron="@hourly", - ), - ) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot.version = "a" - - assert not snapshot.unpaused_ts - state_sync.push_snapshots([snapshot]) - - # Unpaused timestamp not aligned with cron - unpaused_dt = "2022-01-01 01:22:33" - state_sync.unpause_snapshots([snapshot], unpaused_dt) - - actual_snapshot = state_sync.get_snapshots([snapshot])[snapshot.snapshot_id] - assert actual_snapshot.unpaused_ts - assert actual_snapshot.unpaused_ts == to_timestamp("2022-01-01 01:00:00") - - new_snapshot = make_snapshot( - SqlModel( - name="test_snapshot", - query=parse_one("select 2, ds"), - cron="@daily", - interval_unit="hour", - ) - ) - new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) - new_snapshot.version = "a" - - assert not new_snapshot.unpaused_ts - state_sync.push_snapshots([new_snapshot]) - state_sync.unpause_snapshots([new_snapshot], unpaused_dt) - - actual_snapshots = state_sync.get_snapshots([snapshot, new_snapshot]) - assert not actual_snapshots[snapshot.snapshot_id].unpaused_ts - assert actual_snapshots[new_snapshot.snapshot_id].unpaused_ts == to_timestamp( - "2022-01-01 01:00:00" - ) - - def test_unrestorable_snapshot(state_sync: EngineAdapterStateSync, make_snapshot: t.Callable): snapshot = make_snapshot( SqlModel( @@ -2037,81 +1993,6 @@ def test_unrestorable_snapshot_target_not_forward_only( assert not actual_snapshots[updated_snapshot.snapshot_id].unrestorable -def test_unpause_snapshots_remove_intervals( - state_sync: EngineAdapterStateSync, make_snapshot: t.Callable -): - snapshot = make_snapshot( - SqlModel( - name="test_snapshot", - query=parse_one("select 1, ds"), - cron="@daily", - ), - version="a", - ) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot.version = "a" - state_sync.push_snapshots([snapshot]) - state_sync.add_interval(snapshot, "2023-01-01", "2023-01-05") - - new_snapshot = make_snapshot( - SqlModel(name="test_snapshot", query=parse_one("select 2, ds"), cron="@daily"), - version="a", - ) - new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) - new_snapshot.version = "a" - new_snapshot.effective_from = "2023-01-03" - state_sync.push_snapshots([new_snapshot]) - state_sync.add_interval(snapshot, "2023-01-06", "2023-01-06") - state_sync.unpause_snapshots([new_snapshot], "2023-01-06") - - actual_snapshots = state_sync.get_snapshots([snapshot, new_snapshot]) - assert actual_snapshots[new_snapshot.snapshot_id].intervals == [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), - ] - assert actual_snapshots[snapshot.snapshot_id].intervals == [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), - ] - - -def test_unpause_snapshots_remove_intervals_disabled_restatement( - state_sync: EngineAdapterStateSync, make_snapshot: t.Callable -): - kind = dict(name="INCREMENTAL_BY_TIME_RANGE", time_column="ds", disable_restatement=True) - snapshot = make_snapshot( - SqlModel( - name="test_snapshot", - query=parse_one("select 1, ds"), - cron="@daily", - kind=kind, - ), - version="a", - ) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot.version = "a" - state_sync.push_snapshots([snapshot]) - state_sync.add_interval(snapshot, "2023-01-01", "2023-01-05") - - new_snapshot = make_snapshot( - SqlModel(name="test_snapshot", query=parse_one("select 2, ds"), cron="@daily", kind=kind), - version="a", - ) - new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) - new_snapshot.version = "a" - new_snapshot.effective_from = "2023-01-03" - state_sync.push_snapshots([new_snapshot]) - state_sync.add_interval(snapshot, "2023-01-06", "2023-01-06") - state_sync.unpause_snapshots([new_snapshot], "2023-01-06") - - actual_snapshots = state_sync.get_snapshots([snapshot, new_snapshot]) - assert actual_snapshots[new_snapshot.snapshot_id].intervals == [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), - ] - # The intervals shouldn't have been removed because restatement is disabled - assert actual_snapshots[snapshot.snapshot_id].intervals == [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-07")), - ] - - def test_version_schema(state_sync: EngineAdapterStateSync, tmp_path) -> None: from sqlmesh import __version__ as SQLMESH_VERSION @@ -2999,6 +2880,7 @@ def test_snapshot_batching(state_sync, mocker, make_snapshot): 1, 1, False, + False, None, ], [ @@ -3011,6 +2893,7 @@ def test_snapshot_batching(state_sync, mocker, make_snapshot): 1, 1, False, + False, None, ], ], @@ -3025,6 +2908,7 @@ def test_snapshot_batching(state_sync, mocker, make_snapshot): 1, 1, False, + False, None, ], ], From 57585b399508a93966f28ae6a355baffefa1e0e7 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Thu, 14 Aug 2025 10:32:33 -0700 Subject: [PATCH 0706/1056] chore(web_common): rename folder (#5142) --- web/common/.gitignore | 3 +++ web/{shared_ui => common}/eslint.config.mjs | 0 web/{shared_ui => common}/package.json | 14 ++++++++++---- web/{shared_ui => common}/src/index.ts | 0 web/{shared_ui => common}/src/types/index.ts | 0 web/{shared_ui => common}/src/utils/index.ts | 0 web/{shared_ui => common}/tsconfig.json | 0 web/{shared_ui => common}/vite.config.js | 4 ++-- web/shared_ui/.gitignore | 3 --- 9 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 web/common/.gitignore rename web/{shared_ui => common}/eslint.config.mjs (100%) rename web/{shared_ui => common}/package.json (76%) rename web/{shared_ui => common}/src/index.ts (100%) rename web/{shared_ui => common}/src/types/index.ts (100%) rename web/{shared_ui => common}/src/utils/index.ts (100%) rename web/{shared_ui => common}/tsconfig.json (100%) rename web/{shared_ui => common}/vite.config.js (87%) delete mode 100644 web/shared_ui/.gitignore diff --git a/web/common/.gitignore b/web/common/.gitignore new file mode 100644 index 0000000000..86ff358d54 --- /dev/null +++ b/web/common/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist +tsconfig.tsbuildinfo diff --git a/web/shared_ui/eslint.config.mjs b/web/common/eslint.config.mjs similarity index 100% rename from web/shared_ui/eslint.config.mjs rename to web/common/eslint.config.mjs diff --git a/web/shared_ui/package.json b/web/common/package.json similarity index 76% rename from web/shared_ui/package.json rename to web/common/package.json index 240e81fbab..2fd8495a47 100644 --- a/web/shared_ui/package.json +++ b/web/common/package.json @@ -10,13 +10,19 @@ "files": [ "/dist" ], - "main": "dist/sqlmesh-shared-ui.umd.js", - "module": "dist/sqlmesh-shared-ui.es.js", + "main": "dist/sqlmesh-common.umd.js", + "module": "dist/sqlmesh-common.es.js", "types": "dist/index.d.ts", "exports": { ".": { - "import": "./dist/sqlmesh-shared-ui.es.js", - "require": "./dist/sqlmesh-shared-ui.umd.js" + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/sqlmesh-common.es.js" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/sqlmesh-common.umd.js" + } } }, "scripts": { diff --git a/web/shared_ui/src/index.ts b/web/common/src/index.ts similarity index 100% rename from web/shared_ui/src/index.ts rename to web/common/src/index.ts diff --git a/web/shared_ui/src/types/index.ts b/web/common/src/types/index.ts similarity index 100% rename from web/shared_ui/src/types/index.ts rename to web/common/src/types/index.ts diff --git a/web/shared_ui/src/utils/index.ts b/web/common/src/utils/index.ts similarity index 100% rename from web/shared_ui/src/utils/index.ts rename to web/common/src/utils/index.ts diff --git a/web/shared_ui/tsconfig.json b/web/common/tsconfig.json similarity index 100% rename from web/shared_ui/tsconfig.json rename to web/common/tsconfig.json diff --git a/web/shared_ui/vite.config.js b/web/common/vite.config.js similarity index 87% rename from web/shared_ui/vite.config.js rename to web/common/vite.config.js index a5ec9df8ca..3fce71c299 100644 --- a/web/shared_ui/vite.config.js +++ b/web/common/vite.config.js @@ -18,8 +18,8 @@ export default defineConfig({ build: { lib: { entry: path.resolve(__dirname, 'src/index.ts'), - name: 'sqlmesh-shared-ui', - fileName: format => `sqlmesh-shared-ui.${format}.js`, + name: 'sqlmesh-common', + fileName: format => `sqlmesh-common.${format}.js`, }, rollupOptions: { external: ['react', 'react-dom'], diff --git a/web/shared_ui/.gitignore b/web/shared_ui/.gitignore deleted file mode 100644 index 0a4ebc2eae..0000000000 --- a/web/shared_ui/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ -dist -tsconfig.tsbuildinfo \ No newline at end of file From bf811c229ae0e3043143bec021ab1c9eea5a80c4 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 14 Aug 2025 11:29:47 -0700 Subject: [PATCH 0707/1056] Feat: Propagate ignore_cron into plan (#5152) --- sqlmesh/cli/main.py | 8 +++++ sqlmesh/core/context.py | 5 +++ sqlmesh/core/plan/builder.py | 4 +++ sqlmesh/core/plan/definition.py | 4 +++ sqlmesh/core/plan/stages.py | 1 + sqlmesh/magics.py | 7 ++++ tests/core/test_integration.py | 54 +++++++++++++++++++++++++++++ tests/core/test_plan.py | 61 +++++++++++++++++++++++++++++++++ tests/core/test_plan_stages.py | 19 ++++++++++ 9 files changed, 163 insertions(+) diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 8982efc9f8..5d9a12f110 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -522,6 +522,12 @@ def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None: help="Explain the plan instead of applying it.", default=None, ) +@click.option( + "--ignore-cron", + is_flag=True, + help="Run all missing intervals, ignoring individual cron schedules. Only applies if --run is set.", + default=None, +) @click.option( "--min-intervals", default=0, @@ -543,6 +549,7 @@ def plan( select_models = kwargs.pop("select_model") or None allow_destructive_models = kwargs.pop("allow_destructive_model") or None backfill_models = kwargs.pop("backfill_model") or None + ignore_cron = kwargs.pop("ignore_cron") or None setattr(get_console(), "verbosity", Verbosity(verbose)) context.plan( @@ -551,6 +558,7 @@ def plan( select_models=select_models, allow_destructive_models=allow_destructive_models, backfill_models=backfill_models, + ignore_cron=ignore_cron, **kwargs, ) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 7d27092f0e..eca60ecea9 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1290,6 +1290,7 @@ def plan( diff_rendered: t.Optional[bool] = None, skip_linter: t.Optional[bool] = None, explain: t.Optional[bool] = None, + ignore_cron: t.Optional[bool] = None, min_intervals: t.Optional[int] = None, ) -> Plan: """Interactively creates a plan. @@ -1367,6 +1368,7 @@ def plan( diff_rendered=diff_rendered, skip_linter=skip_linter, explain=explain, + ignore_cron=ignore_cron, min_intervals=min_intervals, ) @@ -1417,6 +1419,7 @@ def plan_builder( diff_rendered: t.Optional[bool] = None, skip_linter: t.Optional[bool] = None, explain: t.Optional[bool] = None, + ignore_cron: t.Optional[bool] = None, min_intervals: t.Optional[int] = None, ) -> PlanBuilder: """Creates a plan builder. @@ -1590,6 +1593,7 @@ def plan_builder( max_interval_end_per_model = None default_start, default_end = None, None if not run: + ignore_cron = False max_interval_end_per_model = self._get_max_interval_end_per_model( snapshots, backfill_models ) @@ -1654,6 +1658,7 @@ def plan_builder( console=self.console, user_provided_flags=user_provided_flags, explain=explain or False, + ignore_cron=ignore_cron or False, ) def apply( diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 7918556bad..db2b43345a 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -89,6 +89,7 @@ class PlanBuilder: the environment is not finalized. start_override_per_model: A mapping of model FQNs to target start dates. end_override_per_model: A mapping of model FQNs to target end dates. + ignore_cron: Whether to ignore the node's cron schedule when computing missing intervals. explain: Whether to explain the plan instead of applying it. """ @@ -120,6 +121,7 @@ def __init__( end_bounded: bool = False, ensure_finalized_snapshots: bool = False, explain: bool = False, + ignore_cron: bool = False, start_override_per_model: t.Optional[t.Dict[str, datetime]] = None, end_override_per_model: t.Optional[t.Dict[str, datetime]] = None, console: t.Optional[PlanBuilderConsole] = None, @@ -137,6 +139,7 @@ def __init__( self._enable_preview = enable_preview self._end_bounded = end_bounded self._ensure_finalized_snapshots = ensure_finalized_snapshots + self._ignore_cron = ignore_cron self._start_override_per_model = start_override_per_model self._end_override_per_model = end_override_per_model self._environment_ttl = environment_ttl @@ -335,6 +338,7 @@ def build(self) -> Plan: execution_time=plan_execution_time, end_bounded=self._end_bounded, ensure_finalized_snapshots=self._ensure_finalized_snapshots, + ignore_cron=self._ignore_cron, user_provided_flags=self._user_provided_flags, ) self._latest_plan = plan diff --git a/sqlmesh/core/plan/definition.py b/sqlmesh/core/plan/definition.py index 584c0d9b51..300ac62faf 100644 --- a/sqlmesh/core/plan/definition.py +++ b/sqlmesh/core/plan/definition.py @@ -48,6 +48,7 @@ class Plan(PydanticModel, frozen=True): end_bounded: bool ensure_finalized_snapshots: bool explain: bool + ignore_cron: bool = False environment_ttl: t.Optional[str] = None environment_naming_info: EnvironmentNamingInfo @@ -181,6 +182,7 @@ def missing_intervals(self) -> t.List[SnapshotIntervals]: start_override_per_model=self.start_override_per_model, end_override_per_model=self.end_override_per_model, end_bounded=self.end_bounded, + ignore_cron=self.ignore_cron, ).items() if snapshot.is_model and missing ] @@ -259,6 +261,7 @@ def to_evaluatable(self) -> EvaluatablePlan: forward_only=self.forward_only, end_bounded=self.end_bounded, ensure_finalized_snapshots=self.ensure_finalized_snapshots, + ignore_cron=self.ignore_cron, directly_modified_snapshots=sorted(self.directly_modified), indirectly_modified_snapshots={ s.name: sorted(snapshot_ids) for s, snapshot_ids in self.indirectly_modified.items() @@ -300,6 +303,7 @@ class EvaluatablePlan(PydanticModel): forward_only: bool end_bounded: bool ensure_finalized_snapshots: bool + ignore_cron: bool = False directly_modified_snapshots: t.List[SnapshotId] indirectly_modified_snapshots: t.Dict[str, t.List[SnapshotId]] metadata_updated_snapshots: t.List[SnapshotId] diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 7ef9fcb7ef..871b540203 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -524,6 +524,7 @@ def _missing_intervals( }, deployability_index=deployability_index, end_bounded=plan.end_bounded, + ignore_cron=plan.ignore_cron, start_override_per_model=plan.start_override_per_model, end_override_per_model=plan.end_override_per_model, ) diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index 454b6cd4ce..2b5f185aa9 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -486,6 +486,12 @@ def test(self, context: Context, line: str, test_def_raw: t.Optional[str] = None action="store_true", help="Run latest intervals as part of the plan application (prod environment only).", ) + @argument( + "--ignore-cron", + action="store_true", + help="Run for all missing intervals, ignoring individual cron schedules. Only applies if --run is set.", + default=None, + ) @argument( "--enable-preview", action="store_true", @@ -533,6 +539,7 @@ def plan(self, context: Context, line: str) -> None: select_models=args.select_model, no_diff=args.no_diff, run=args.run, + ignore_cron=args.run, enable_preview=args.enable_preview, diff_rendered=args.diff_rendered, ) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 856406a16d..7248b2a724 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -1609,6 +1609,60 @@ def test_plan_with_run( } +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_plan_ignore_cron( + init_and_plan_context: t.Callable, +): + context, _ = init_and_plan_context("examples/sushi") + + expressions = d.parse( + f""" + MODEL ( + name memory.sushi.test_allow_partials, + kind INCREMENTAL_UNMANAGED, + allow_partials true, + start '2023-01-01', + ); + + SELECT @end_ts AS end_ts + """ + ) + model = load_sql_based_model(expressions) + + context.upsert_model(model) + context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) + + assert ( + context.engine_adapter.fetchone("SELECT MAX(end_ts) FROM memory.sushi.test_allow_partials")[ + 0 + ] + == "2023-01-07 23:59:59.999999" + ) + + plan_no_ignore_cron = context.plan_builder( + "prod", run=True, ignore_cron=False, skip_tests=True + ).build() + assert not plan_no_ignore_cron.missing_intervals + + plan = context.plan_builder("prod", run=True, ignore_cron=True, skip_tests=True).build() + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=context.get_snapshot(model, raise_if_missing=True).snapshot_id, + intervals=[ + (to_timestamp("2023-01-08"), to_timestamp("2023-01-08 15:00:00")), + ], + ) + ] + context.apply(plan) + + assert ( + context.engine_adapter.fetchone("SELECT MAX(end_ts) FROM memory.sushi.test_allow_partials")[ + 0 + ] + == "2023-01-08 14:59:59.999999" + ) + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_run_with_select_models_no_auto_upstream( init_and_plan_context: t.Callable, diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 66018d4be4..7254a924b1 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -3359,3 +3359,64 @@ def test_environment_statements_change_allows_dev_environment_creation(make_snap assert plan is not None assert plan.context_diff.has_environment_statements_changes assert plan.context_diff.environment_statements == environment_statements + + +def test_plan_ignore_cron_flag(make_snapshot): + snapshot_a = make_snapshot( + SqlModel( + name="test_model", + kind=IncrementalByTimeRangeKind(time_column="ds"), + cron="@daily", # Daily cron schedule + start="2023-01-01", + query=parse_one("SELECT 1 as id, ds FROM VALUES ('2023-01-01') t(ds)"), + allow_partials=True, + ) + ) + snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=False) + + context_diff = ContextDiff( + environment="dev", + is_new_environment=True, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={}, + snapshots={snapshot_a.snapshot_id: snapshot_a}, + new_snapshots={snapshot_a.snapshot_id: snapshot_a}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + plan_builder_ignore_cron = PlanBuilder( + context_diff, + start="2023-01-01", + execution_time="2023-01-05 12:00:00", + is_dev=True, + include_unmodified=True, + ignore_cron=True, + end_bounded=False, + ) + + plan = plan_builder_ignore_cron.build() + assert plan.ignore_cron is True + assert plan.to_evaluatable().ignore_cron is True + + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=snapshot_a.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-05 12:00:00")), + ], + ) + ] diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index 1b660e1a87..aedf50e26f 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -107,6 +107,7 @@ def test_build_plan_stages_basic( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -218,6 +219,7 @@ def test_build_plan_stages_with_before_all_and_after_all( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -326,6 +328,7 @@ def test_build_plan_stages_select_models( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -426,6 +429,7 @@ def test_build_plan_stages_basic_no_backfill( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -535,6 +539,7 @@ def test_build_plan_stages_restatement( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[], # No changes indirectly_modified_snapshots={}, # No changes metadata_updated_snapshots=[], @@ -644,6 +649,7 @@ def test_build_plan_stages_forward_only( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[new_snapshot_a.snapshot_id], indirectly_modified_snapshots={ new_snapshot_a.name: [new_snapshot_b.snapshot_id], @@ -772,6 +778,7 @@ def test_build_plan_stages_forward_only_dev( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[new_snapshot_a.snapshot_id], indirectly_modified_snapshots={ new_snapshot_a.name: [new_snapshot_b.snapshot_id], @@ -895,6 +902,7 @@ def _get_snapshots(snapshot_ids: t.List[SnapshotId]) -> t.Dict[SnapshotId, Snaps forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[new_snapshot_a.snapshot_id], indirectly_modified_snapshots={ new_snapshot_a.name: [new_snapshot_b.snapshot_id], @@ -1018,6 +1026,7 @@ def test_build_plan_stages_forward_only_ensure_finalized_snapshots( forward_only=False, end_bounded=False, ensure_finalized_snapshots=True, + ignore_cron=False, directly_modified_snapshots=[new_snapshot_a.snapshot_id], indirectly_modified_snapshots={ new_snapshot_a.name: [new_snapshot_b.snapshot_id], @@ -1092,6 +1101,7 @@ def test_build_plan_stages_removed_model( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -1173,6 +1183,7 @@ def test_build_plan_stages_environment_suffix_target_changed( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -1270,6 +1281,7 @@ def test_build_plan_stages_indirect_non_breaking_view_migration( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[new_snapshot_a.snapshot_id], indirectly_modified_snapshots={ new_snapshot_a.name: [new_snapshot_c.snapshot_id], @@ -1357,6 +1369,7 @@ def test_build_plan_stages_virtual_environment_mode_filtering( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[snapshot_full.snapshot_id, snapshot_dev_only.snapshot_id], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -1408,6 +1421,7 @@ def test_build_plan_stages_virtual_environment_mode_filtering( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[snapshot_full.snapshot_id, snapshot_dev_only.snapshot_id], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -1469,6 +1483,7 @@ def test_build_plan_stages_virtual_environment_mode_filtering( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -1541,6 +1556,7 @@ def test_build_plan_stages_virtual_environment_mode_no_updates( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[snapshot_dev_only.snapshot_id], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -1603,6 +1619,7 @@ def test_adjust_intervals_new_forward_only_dev_intervals( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -1669,6 +1686,7 @@ def test_adjust_intervals_restatement_removal( forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], @@ -1760,6 +1778,7 @@ def test_adjust_intervals_should_force_rebuild(make_snapshot, mocker: MockerFixt forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, + ignore_cron=False, directly_modified_snapshots=[new_snapshot.snapshot_id], indirectly_modified_snapshots={}, metadata_updated_snapshots=[], From 4d943a61c6a930986d9eea7a914121d14388a591 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Thu, 14 Aug 2025 13:06:58 -0700 Subject: [PATCH 0708/1056] chore:(web_common): update web common release file (#5161) --- .github/workflows/release_shared_js.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release_shared_js.yaml b/.github/workflows/release_shared_js.yaml index b6bd04ddd2..96992ae637 100644 --- a/.github/workflows/release_shared_js.yaml +++ b/.github/workflows/release_shared_js.yaml @@ -1,4 +1,4 @@ -name: Release shared js code +name: Release web common code on: workflow_dispatch: inputs: @@ -46,13 +46,13 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - name: Update package.json version - working-directory: web/shared_ui + working-directory: web/common run: | npm version ${{ github.event.inputs.version }} --no-git-tag-version - name: Build package - working-directory: web/shared_ui + working-directory: web/common run: pnpm run build - name: Publish to npm - working-directory: web/shared_ui + working-directory: web/common run: | npm publish From 5bb40a1c06ef1b5844415441d6c6b03c89f9b3ab Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 15 Aug 2025 08:24:01 +1200 Subject: [PATCH 0709/1056] Feat: yaml config for dbt projects (#5156) --- sqlmesh/cli/project_init.py | 21 ++++--- sqlmesh/core/config/common.py | 10 ++++ sqlmesh/core/config/loader.py | 29 ++++++++-- sqlmesh/dbt/common.py | 3 +- sqlmesh/dbt/manifest.py | 10 +++- sqlmesh/dbt/model.py | 2 +- sqlmesh/dbt/project.py | 1 + sqlmesh_dbt/operations.py | 59 ++++++------------- tests/cli/test_cli.py | 16 ++---- tests/cli/test_project_init.py | 24 ++++++++ tests/core/test_config.py | 99 ++++++++++++++++++++++++++++++++ tests/dbt/cli/test_operations.py | 26 ++++----- 12 files changed, 215 insertions(+), 85 deletions(-) create mode 100644 tests/cli/test_project_init.py diff --git a/sqlmesh/cli/project_init.py b/sqlmesh/cli/project_init.py index 613ea72c45..b6dc5050bc 100644 --- a/sqlmesh/cli/project_init.py +++ b/sqlmesh/cli/project_init.py @@ -8,6 +8,7 @@ from sqlmesh.utils.date import yesterday_ds from sqlmesh.utils.errors import SQLMeshError +from sqlmesh.core.config.common import DBT_PROJECT_FILENAME from sqlmesh.core.config.connection import ( CONNECTION_CONFIG_TO_TYPE, DIALECT_TO_TYPE, @@ -113,11 +114,10 @@ def _gen_config( - ambiguousorinvalidcolumn - invalidselectstarexpansion """, - ProjectTemplate.DBT: """from pathlib import Path - -from sqlmesh.dbt.loader import sqlmesh_config - -config = sqlmesh_config(Path(__file__).parent) + ProjectTemplate.DBT: f"""# --- Model Defaults --- +# https://sqlmesh.readthedocs.io/en/stable/reference/model_configuration/#model-defaults +model_defaults: + start: {start or yesterday_ds()} """, } @@ -285,8 +285,13 @@ def init_example_project( cli_mode: InitCliMode = InitCliMode.DEFAULT, ) -> Path: root_path = Path(path) - config_extension = "py" if template == ProjectTemplate.DBT else "yaml" - config_path = root_path / f"config.{config_extension}" + + config_path = root_path / "config.yaml" + if template == ProjectTemplate.DBT: + # name the config file `sqlmesh.yaml` to make it clear that within the context of all + # the existing yaml files DBT project, this one specifically relates to configuring the sqlmesh engine + config_path = root_path / "sqlmesh.yaml" + audits_path = root_path / "audits" macros_path = root_path / "macros" models_path = root_path / "models" @@ -298,7 +303,7 @@ def init_example_project( f"Found an existing config file '{config_path}'.\n\nPlease change to another directory or remove the existing file." ) - if template == ProjectTemplate.DBT and not Path(root_path, "dbt_project.yml").exists(): + if template == ProjectTemplate.DBT and not Path(root_path, DBT_PROJECT_FILENAME).exists(): raise SQLMeshError( "Required dbt project file 'dbt_project.yml' not found in the current directory.\n\nPlease add it or change directories before running `sqlmesh init` to set up your project." ) diff --git a/sqlmesh/core/config/common.py b/sqlmesh/core/config/common.py index 2963632041..dca472d7a9 100644 --- a/sqlmesh/core/config/common.py +++ b/sqlmesh/core/config/common.py @@ -8,6 +8,16 @@ from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.pydantic import field_validator +# Config files that can be present in the project dir +ALL_CONFIG_FILENAMES = ("config.py", "config.yml", "config.yaml", "sqlmesh.yml", "sqlmesh.yaml") + +# For personal paths (~/.sqlmesh/) where python config is not supported +YAML_CONFIG_FILENAMES = tuple(n for n in ALL_CONFIG_FILENAMES if not n.endswith(".py")) + +# Note: is here to prevent having to import from sqlmesh.dbt.loader which introduces a dependency +# on dbt-core in a native project +DBT_PROJECT_FILENAME = "dbt_project.yml" + class EnvironmentSuffixTarget(str, Enum): # Intended to create virtual environments in their own schemas, with names like "__". The view name is untouched. diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index c381252fb9..2c1554454b 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -10,6 +10,11 @@ from sqlglot.helper import ensure_list from sqlmesh.core import constants as c +from sqlmesh.core.config.common import ( + ALL_CONFIG_FILENAMES, + YAML_CONFIG_FILENAMES, + DBT_PROJECT_FILENAME, +) from sqlmesh.core.config.model import ModelDefaultsConfig from sqlmesh.core.config.root import Config from sqlmesh.utils import env_vars, merge_dicts, sys_path @@ -51,10 +56,7 @@ def load_configs( return {path: config for path in absolute_paths} config_env_vars = None - personal_paths = [ - sqlmesh_path / "config.yml", - sqlmesh_path / "config.yaml", - ] + personal_paths = [sqlmesh_path / name for name in YAML_CONFIG_FILENAMES] for path in personal_paths: if path.exists(): config_env_vars = load_config_from_yaml(path).get("env_vars") @@ -65,7 +67,7 @@ def load_configs( return { path: load_config_from_paths( config_type, - project_paths=[path / "config.py", path / "config.yml", path / "config.yaml"], + project_paths=[path / name for name in ALL_CONFIG_FILENAMES], personal_paths=personal_paths, config_name=config, ) @@ -156,6 +158,22 @@ def load_config_from_paths( ) no_dialect_err_msg = "Default model SQL dialect is a required configuration parameter. Set it in the `model_defaults` `dialect` key in your config file." + + # if "dbt_project.yml" is present *and there was no python config already defined*, + # create a basic one to ensure we are using the DBT loader. + # any config within yaml files will get overlayed on top of it. + if not python_config: + potential_project_files = [f / DBT_PROJECT_FILENAME for f in visited_folders] + dbt_project_file = next((f for f in potential_project_files if f.exists()), None) + if dbt_project_file: + from sqlmesh.dbt.loader import sqlmesh_config + + dbt_python_config = sqlmesh_config(project_root=dbt_project_file.parent) + if type(dbt_python_config) != config_type: + dbt_python_config = convert_config_type(dbt_python_config, config_type) + + python_config = dbt_python_config + if python_config: model_defaults = python_config.model_defaults if model_defaults.dialect is None: @@ -165,6 +183,7 @@ def load_config_from_paths( model_defaults = non_python_config.model_defaults if model_defaults.dialect is None: raise ConfigError(no_dialect_err_msg) + return non_python_config diff --git a/sqlmesh/dbt/common.py b/sqlmesh/dbt/common.py index 49d6c7ca18..d9db5a472c 100644 --- a/sqlmesh/dbt/common.py +++ b/sqlmesh/dbt/common.py @@ -14,10 +14,11 @@ from sqlmesh.utils.jinja import MacroReference from sqlmesh.utils.pydantic import PydanticModel, field_validator from sqlmesh.utils.yaml import load +from sqlmesh.core.config.common import DBT_PROJECT_FILENAME T = t.TypeVar("T", bound="GeneralConfig") -PROJECT_FILENAME = "dbt_project.yml" +PROJECT_FILENAME = DBT_PROJECT_FILENAME JINJA_ONLY = { "adapter", diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 19795a0b9b..4f839b9c9b 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -32,6 +32,7 @@ from dbt.tracking import do_not_track from sqlmesh.core import constants as c +from sqlmesh.core.config import ModelDefaultsConfig from sqlmesh.dbt.basemodel import Dependencies from sqlmesh.dbt.builtin import BUILTIN_FILTERS, BUILTIN_GLOBALS, OVERRIDDEN_MACROS from sqlmesh.dbt.model import ModelConfig @@ -78,12 +79,14 @@ def __init__( target: TargetConfig, variable_overrides: t.Optional[t.Dict[str, t.Any]] = None, cache_dir: t.Optional[str] = None, + model_defaults: t.Optional[ModelDefaultsConfig] = None, ): self.project_path = project_path self.profiles_path = profiles_path self.profile_name = profile_name self.target = target self.variable_overrides = variable_overrides or {} + self.model_defaults = model_defaults or ModelDefaultsConfig() self.__manifest: t.Optional[Manifest] = None self._project_name: str = "" @@ -380,9 +383,12 @@ def _load_manifest(self) -> Manifest: profile = self._load_profile() project = self._load_project(profile) - if not any(k in project.models for k in ("start", "+start")): + if ( + not any(k in project.models for k in ("start", "+start")) + and not self.model_defaults.start + ): raise ConfigError( - "SQLMesh's requires a start date in order to have a finite range of backfilling data. Add start to the 'models:' block in dbt_project.yml. https://sqlmesh.readthedocs.io/en/stable/integrations/dbt/#setting-model-backfill-start-dates" + "SQLMesh requires a start date in order to have a finite range of backfilling data. Add start to the 'models:' block in dbt_project.yml. https://sqlmesh.readthedocs.io/en/stable/integrations/dbt/#setting-model-backfill-start-dates" ) runtime_config = RuntimeConfig.from_parts(project, profile, args) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 8563d20d22..4198fabca7 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -569,7 +569,7 @@ def to_sqlmesh( query, dialect=model_dialect, kind=kind, - start=self.start, + start=self.start or context.sqlmesh_config.model_defaults.start, audit_definitions=audit_definitions, path=model_kwargs.pop("path", self.path), # This ensures that we bypass query rendering that would otherwise be required to extract additional diff --git a/sqlmesh/dbt/project.py b/sqlmesh/dbt/project.py index ac36ee4e0a..d37c9cc6c4 100644 --- a/sqlmesh/dbt/project.py +++ b/sqlmesh/dbt/project.py @@ -76,6 +76,7 @@ def load(cls, context: DbtContext, variables: t.Optional[t.Dict[str, t.Any]] = N target=profile.target, variable_overrides=variable_overrides, cache_dir=context.sqlmesh_config.cache_dir, + model_defaults=context.sqlmesh_config.model_defaults, ) extra_fields = profile.target.extra diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index b826a00e37..ec07efd37b 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -61,7 +61,7 @@ def create( from sqlmesh import configure_logging from sqlmesh.core.context import Context - from sqlmesh.dbt.loader import sqlmesh_config, DbtLoader + from sqlmesh.dbt.loader import DbtLoader from sqlmesh.core.console import set_console from sqlmesh_dbt.console import DbtCliConsole from sqlmesh.utils.errors import SQLMeshError @@ -71,34 +71,14 @@ def create( progress.update(load_task_id, description="Loading project", total=None) - # inject default start date if one is not specified to prevent the user from having to do anything - _inject_default_start_date(project_dir) - - config = sqlmesh_config( - project_root=project_dir, - # do we want to use a local duckdb for state? - # warehouse state has a bunch of overhead to initialize, is slow for ongoing operations and will create tables that perhaps the user was not expecting - # on the other hand, local state is not portable - state_connection=None, - ) + project_dir = project_dir or Path.cwd() + init_project_if_required(project_dir) sqlmesh_context = Context( - config=config, + paths=[project_dir], load=True, ) - # this helps things which want a default project-level start date, like the "effective from date" for forward-only plans - if not sqlmesh_context.config.model_defaults.start: - min_start_date = min( - ( - model.start - for model in sqlmesh_context.models.values() - if model.start is not None - ), - default=None, - ) - sqlmesh_context.config.model_defaults.start = min_start_date - dbt_loader = sqlmesh_context._loaders[0] if not isinstance(dbt_loader, DbtLoader): raise SQLMeshError(f"Unexpected loader type: {type(dbt_loader)}") @@ -109,25 +89,20 @@ def create( return DbtOperations(sqlmesh_context, dbt_project) -def _inject_default_start_date(project_dir: t.Optional[Path] = None) -> None: +def init_project_if_required(project_dir: Path) -> None: """ - SQLMesh needs a start date to as the starting point for calculating intervals on incremental models + SQLMesh needs a start date to as the starting point for calculating intervals on incremental models, amongst other things Rather than forcing the user to update their config manually or having a default that is not saved between runs, - we can inject it automatically to the dbt_project.yml file + we can generate a basic SQLMesh config if it doesnt exist. + + This is preferable to trying to inject config into `dbt_project.yml` because it means we have full control over the file + and dont need to worry about accidentally reformatting it or accidentally clobbering other config """ - from sqlmesh.dbt.project import PROJECT_FILENAME, load_yaml - from sqlmesh.utils.yaml import dump - from sqlmesh.utils.date import yesterday_ds - - project_yaml_path = (project_dir or Path.cwd()) / PROJECT_FILENAME - if project_yaml_path.exists(): - loaded_project_file = load_yaml(project_yaml_path) - start_date_keys = ("start", "+start") - if "models" in loaded_project_file and all( - k not in loaded_project_file["models"] for k in start_date_keys - ): - loaded_project_file["models"]["+start"] = yesterday_ds() - # todo: this may format the file differently, is that acceptable? - with project_yaml_path.open("w") as f: - dump(loaded_project_file, f) + from sqlmesh.cli.project_init import init_example_project, ProjectTemplate + from sqlmesh.core.config.common import ALL_CONFIG_FILENAMES + from sqlmesh.core.console import get_console + + if not any(f.exists() for f in [project_dir / file for file in ALL_CONFIG_FILENAMES]): + get_console().log_warning("No existing SQLMesh config detected; creating one") + init_example_project(path=project_dir, engine_type=None, template=ProjectTemplate.DBT) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index d1f792dc28..45accccaa8 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1954,21 +1954,13 @@ def test_init_dbt_template(runner: CliRunner, tmp_path: Path): ) assert result.exit_code == 0 - config_path = tmp_path / "config.py" + config_path = tmp_path / "sqlmesh.yaml" assert config_path.exists() - with open(config_path) as file: - config = file.read() - - assert ( - config - == """from pathlib import Path + config = config_path.read_text() -from sqlmesh.dbt.loader import sqlmesh_config - -config = sqlmesh_config(Path(__file__).parent) -""" - ) + assert "model_defaults" in config + assert "start:" in config @time_machine.travel(FREEZE_TIME) diff --git a/tests/cli/test_project_init.py b/tests/cli/test_project_init.py new file mode 100644 index 0000000000..e89e59d90c --- /dev/null +++ b/tests/cli/test_project_init.py @@ -0,0 +1,24 @@ +import pytest +from pathlib import Path +from sqlmesh.utils.errors import SQLMeshError +from sqlmesh.cli.project_init import init_example_project, ProjectTemplate +from sqlmesh.utils import yaml + + +def test_project_init_dbt(tmp_path: Path): + assert not len(list(tmp_path.glob("**/*"))) + + with pytest.raises(SQLMeshError, match=r"Required dbt project file.*not found"): + init_example_project(path=tmp_path, engine_type=None, template=ProjectTemplate.DBT) + + with (tmp_path / "dbt_project.yml").open("w") as f: + yaml.dump({"name": "jaffle_shop"}, f) + + init_example_project(path=tmp_path, engine_type=None, template=ProjectTemplate.DBT) + files = [f for f in tmp_path.glob("**/*") if f.is_file()] + + assert set([f.name for f in files]) == set(["sqlmesh.yaml", "dbt_project.yml"]) + + sqlmesh_config = next(f for f in files if f.name == "sqlmesh.yaml") + assert "model_defaults" in sqlmesh_config.read_text() + assert "start: " in sqlmesh_config.read_text() diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 9277fc6902..8e932ee30d 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -36,6 +36,8 @@ from sqlmesh.core.notification_target import ConsoleNotificationTarget from sqlmesh.core.user import User from sqlmesh.utils.errors import ConfigError +from sqlmesh.utils import yaml +from sqlmesh.dbt.loader import DbtLoader from tests.utils.test_filesystem import create_temp_file @@ -1441,3 +1443,100 @@ def test_physical_table_naming_convention( config = load_config_from_paths(Config, project_paths=[tmp_path / "config.yaml"]) assert config.physical_table_naming_convention == expected + + +def test_load_configs_includes_sqlmesh_yaml(tmp_path: Path): + for extension in ("yaml", "yml"): + config_file = tmp_path / f"sqlmesh.{extension}" + config_file.write_text(""" +model_defaults: + start: '2023-04-05' + dialect: bigquery""") + + configs = load_configs(config=None, config_type=Config, paths=[tmp_path]) + assert len(configs) == 1 + + config: Config = list(configs.values())[0] + + assert config.model_defaults.start == "2023-04-05" + assert config.model_defaults.dialect == "bigquery" + + config_file.unlink() + + +def test_load_configs_without_main_connection(tmp_path: Path): + # this is for DBT projects where the main connection is defined in profiles.yml + # but we also need to be able to specify the sqlmesh state connection without editing any DBT files + # and without also duplicating the main connection + config_file = tmp_path / "sqlmesh.yaml" + with config_file.open("w") as f: + yaml.dump( + { + "gateways": {"": {"state_connection": {"type": "duckdb", "database": "state.db"}}}, + "model_defaults": {"dialect": "duckdb", "start": "2020-01-01"}, + }, + f, + ) + + configs = list(load_configs(config=None, config_type=Config, paths=[tmp_path]).values()) + assert len(configs) == 1 + + config = configs[0] + state_connection_config = config.get_state_connection() + assert isinstance(state_connection_config, DuckDBConnectionConfig) + assert state_connection_config.database == "state.db" + + +def test_load_configs_in_dbt_project_without_config_py(tmp_path: Path): + # this is when someone either: + # - inits a dbt project for sqlmesh, which creates a sqlmesh.yaml file + # - uses the sqlmesh_dbt cli for the first time, which runs init if the config doesnt exist, which creates a config + # when in pure yaml mode, sqlmesh should be able to auto-detect the presence of DBT and select the DbtLoader instead + # of the main loader + (tmp_path / "dbt_project.yml").write_text(""" +name: jaffle_shop + """) + + (tmp_path / "profiles.yml").write_text(""" +jaffle_shop: + + target: dev + outputs: + dev: + type: duckdb + path: 'jaffle_shop.duckdb' + """) + + (tmp_path / "sqlmesh.yaml").write_text(""" +gateways: + dev: + state_connection: + type: duckdb + database: state.db +model_defaults: + start: '2020-01-01' +""") + + configs = list(load_configs(config=None, config_type=Config, paths=[tmp_path]).values()) + assert len(configs) == 1 + + config = configs[0] + assert config.loader == DbtLoader + + assert list(config.gateways) == ["dev"] + + # main connection + connection_config = config.get_connection() + assert connection_config + assert isinstance(connection_config, DuckDBConnectionConfig) + assert connection_config.database == "jaffle_shop.duckdb" # from dbt profiles.yml + + # state connection + state_connection_config = config.get_state_connection() + assert state_connection_config + assert isinstance(state_connection_config, DuckDBConnectionConfig) + assert state_connection_config.database == "state.db" # from sqlmesh.yaml + + # model_defaults + assert config.model_defaults.dialect == "duckdb" # from dbt profiles.yml + assert config.model_defaults.start == "2020-01-01" # from sqlmesh.yaml diff --git a/tests/dbt/cli/test_operations.py b/tests/dbt/cli/test_operations.py index e384028bbc..c35cab992c 100644 --- a/tests/dbt/cli/test_operations.py +++ b/tests/dbt/cli/test_operations.py @@ -7,45 +7,43 @@ pytestmark = pytest.mark.slow -def test_create_injects_default_start_date(jaffle_shop_duckdb: Path): +def test_create_sets_and_persists_default_start_date(jaffle_shop_duckdb: Path): with time_machine.travel("2020-01-02 00:00:00 UTC"): - from sqlmesh.utils.date import yesterday_ds + from sqlmesh.utils.date import yesterday_ds, to_ds assert yesterday_ds() == "2020-01-01" operations = create() - assert operations.context.config.model_defaults.start == "2020-01-01" + assert operations.context.config.model_defaults.start + assert to_ds(operations.context.config.model_defaults.start) == "2020-01-01" assert all( - model.start == "2020-01-01" + to_ds(model.start) if model.start else None == "2020-01-01" for model in operations.context.models.values() if not model.kind.is_seed ) # check that the date set on the first invocation persists to future invocations - from sqlmesh.utils.date import yesterday_ds + from sqlmesh.utils.date import yesterday_ds, to_ds assert yesterday_ds() != "2020-01-01" operations = create() - assert operations.context.config.model_defaults.start == "2020-01-01" + assert operations.context.config.model_defaults.start + assert to_ds(operations.context.config.model_defaults.start) == "2020-01-01" assert all( - model.start == "2020-01-01" + to_ds(model.start) if model.start else None == "2020-01-01" for model in operations.context.models.values() if not model.kind.is_seed ) def test_create_uses_configured_start_date_if_supplied(jaffle_shop_duckdb: Path): - dbt_project_yaml = jaffle_shop_duckdb / "dbt_project.yml" + sqlmesh_yaml = jaffle_shop_duckdb / "sqlmesh.yml" - contents = yaml.load(dbt_project_yaml, render_jinja=False) - - contents["models"]["+start"] = "2023-12-12" - - with dbt_project_yaml.open("w") as f: - yaml.dump(contents, f) + with sqlmesh_yaml.open("w") as f: + yaml.dump({"model_defaults": {"start": "2023-12-12"}}, f) operations = create() From d6ca41f58dd7e054df110c37718e76fe05e3ef2d Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 14 Aug 2025 15:17:26 -0700 Subject: [PATCH 0710/1056] Fix: Support of recursive symlinks in dbt project folders (#5164) --- sqlmesh/dbt/loader.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 3d63219004..0882c5ad99 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -293,17 +293,25 @@ def _load_environment_statements(self, macros: MacroRegistry) -> t.List[Environm ) ] - def _compute_yaml_max_mtime_per_subfolder(self, root: Path) -> t.Dict[Path, float]: - if not root.is_dir(): + def _compute_yaml_max_mtime_per_subfolder( + self, root: Path, visited: t.Optional[t.Set[Path]] = None + ) -> t.Dict[Path, float]: + root = root.resolve() + visited = visited or set() + if not root.is_dir() or root in visited: return {} + visited.add(root) + result = {} max_mtime: t.Optional[float] = None for nested in root.iterdir(): try: if nested.is_dir(): - result.update(self._compute_yaml_max_mtime_per_subfolder(nested)) + result.update( + self._compute_yaml_max_mtime_per_subfolder(nested, visited=visited) + ) elif nested.suffix.lower() in (".yaml", ".yml"): yaml_mtime = self._path_mtimes.get(nested) if yaml_mtime: From dc19226708ede5284221dd3d21d2122c75d2e5f5 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 14 Aug 2025 15:40:03 -0700 Subject: [PATCH 0711/1056] Fix: Warn instead of fail on incompatible incremental strategy for dbt models with a unique key (#5166) --- sqlmesh/dbt/model.py | 7 ++++--- tests/dbt/test_transformation.py | 29 ++++++++++------------------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 4198fabca7..e35d8c16f4 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -293,10 +293,11 @@ def model_kind(self, context: DbtContext) -> ModelKind: self.incremental_strategy and strategy not in INCREMENTAL_BY_UNIQUE_KEY_STRATEGIES ): - raise ConfigError( - f"{self.canonical_name(context)}: SQLMesh incremental by unique key strategy is not compatible with '{strategy}'" - f" incremental strategy. Supported strategies include {collection_to_str(INCREMENTAL_BY_UNIQUE_KEY_STRATEGIES)}." + get_console().log_warning( + f"Unique key is not compatible with '{strategy}' incremental strategy in model '{self.canonical_name(context)}'. " + f"Supported strategies include {collection_to_str(INCREMENTAL_BY_UNIQUE_KEY_STRATEGIES)}. Falling back to 'merge' strategy." ) + strategy = "merge" if self.incremental_predicates: dialect = self.dialect(context) diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index aa5f9ab699..c67621b206 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -238,6 +238,16 @@ def test_model_kind(): auto_restatement_cron="0 0 * * *", ) + # Test incompatibile incremental strategies + for incremental_strategy in ("delete+insert", "insert_overwrite", "append"): + assert ModelConfig( + materialized=Materialization.INCREMENTAL, + unique_key=["bar"], + incremental_strategy=incremental_strategy, + ).model_kind(context) == IncrementalByUniqueKeyKind( + unique_key=["bar"], dialect="duckdb", forward_only=True, disable_restatement=False + ) + assert ModelConfig( materialized=Materialization.INCREMENTAL, time_column="foo", incremental_strategy="merge" ).model_kind(context) == IncrementalByTimeRangeKind( @@ -372,25 +382,6 @@ def test_model_kind(): == ManagedKind() ) - with pytest.raises(ConfigError): - ModelConfig( - materialized=Materialization.INCREMENTAL, - unique_key=["bar"], - incremental_strategy="delete+insert", - ).model_kind(context) - with pytest.raises(ConfigError): - ModelConfig( - materialized=Materialization.INCREMENTAL, - unique_key=["bar"], - incremental_strategy="insert_overwrite", - ).model_kind(context) - with pytest.raises(ConfigError): - ModelConfig( - materialized=Materialization.INCREMENTAL, - unique_key=["bar"], - incremental_strategy="append", - ).model_kind(context) - def test_model_kind_snapshot_bigquery(): context = DbtContext() From 4f704a6a91ccfb1fff59c25d9f02053683ccc5b1 Mon Sep 17 00:00:00 2001 From: Toby Mao Date: Thu, 14 Aug 2025 15:44:02 -0700 Subject: [PATCH 0712/1056] refactor!: remove feature flags (#5165) --- sqlmesh/core/config/feature_flag.py | 9 ------- sqlmesh/core/config/root.py | 3 --- sqlmesh/dbt/loader.py | 11 --------- tests/core/test_config.py | 38 +++-------------------------- tests/dbt/test_adapter.py | 24 ------------------ 5 files changed, 3 insertions(+), 82 deletions(-) delete mode 100644 sqlmesh/core/config/feature_flag.py diff --git a/sqlmesh/core/config/feature_flag.py b/sqlmesh/core/config/feature_flag.py deleted file mode 100644 index b04dda2b08..0000000000 --- a/sqlmesh/core/config/feature_flag.py +++ /dev/null @@ -1,9 +0,0 @@ -from sqlmesh.utils.pydantic import PydanticModel - - -class DbtFeatureFlag(PydanticModel): - scd_type_2_support: bool = True - - -class FeatureFlag(PydanticModel): - dbt: DbtFeatureFlag = DbtFeatureFlag() diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index ec8fa9988f..65889cb7cf 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -27,7 +27,6 @@ SerializableConnectionConfig, connection_config_validator, ) -from sqlmesh.core.config.feature_flag import FeatureFlag from sqlmesh.core.config.format import FormatConfig from sqlmesh.core.config.gateway import GatewayConfig from sqlmesh.core.config.janitor import JanitorConfig @@ -122,7 +121,6 @@ class Config(BaseConfig): log_limit: The default number of logs to keep. format: The formatting options for SQL code. ui: The UI configuration for SQLMesh. - feature_flags: Feature flags to enable/disable certain features. plan: The plan configuration. migration: The migration configuration. variables: A dictionary of variables that can be used in models / macros. @@ -165,7 +163,6 @@ class Config(BaseConfig): run: RunConfig = RunConfig() format: FormatConfig = FormatConfig() ui: UIConfig = UIConfig() - feature_flags: FeatureFlag = FeatureFlag() plan: PlanConfig = PlanConfig() migration: MigrationConfig = MigrationConfig() model_naming: NameInferenceConfig = NameInferenceConfig() diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 0882c5ad99..3a22b61bf6 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -19,7 +19,6 @@ from sqlmesh.dbt.basemodel import BMC, BaseModelConfig from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext -from sqlmesh.dbt.model import ModelConfig from sqlmesh.dbt.profile import Profile from sqlmesh.dbt.project import Project from sqlmesh.dbt.target import TargetConfig @@ -139,16 +138,6 @@ def _to_sqlmesh(config: BMC, context: DbtContext) -> Model: package_models: t.Dict[str, BaseModelConfig] = {**package.models, **package.seeds} for model in package_models.values(): - if ( - not context.sqlmesh_config.feature_flags.dbt.scd_type_2_support - and isinstance(model, ModelConfig) - and model.model_kind(context).is_scd_type_2 - ): - logger.info( - "Skipping loading Snapshot (SCD Type 2) models due to the feature flag disabling this feature" - ) - continue - sqlmesh_model = cache.get_or_load_models( model.path, loader=lambda: [_to_sqlmesh(model, context)] )[0] diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 8e932ee30d..10881dc493 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -21,7 +21,6 @@ TableNamingConvention, ) from sqlmesh.core.config.connection import DuckDBAttachOptions, RedshiftConnectionConfig -from sqlmesh.core.config.feature_flag import DbtFeatureFlag, FeatureFlag from sqlmesh.core.config.loader import ( load_config_from_env, load_config_from_paths, @@ -299,12 +298,10 @@ def test_load_config_from_env(): { "SQLMESH__GATEWAY__CONNECTION__TYPE": "duckdb", "SQLMESH__GATEWAY__CONNECTION__DATABASE": "test_db", - "SQLMESH__FEATURE_FLAGS__DBT__SCD_TYPE_2_SUPPORT": "false", }, ): assert Config.parse_obj(load_config_from_env()) == Config( gateways=GatewayConfig(connection=DuckDBConnectionConfig(database="test_db")), - feature_flags=FeatureFlag(dbt=DbtFeatureFlag(scd_type_2_support=False)), ) @@ -505,35 +502,6 @@ def test_physical_schema_mapping_mutually_exclusive_with_physical_schema_overrid Config(physical_schema_override={"foo": "bar"}, physical_schema_mapping={"^foo$": "bar"}) # type: ignore -def test_load_feature_flag(tmp_path_factory): - config_path = tmp_path_factory.mktemp("yaml_config") / "config.yaml" - with open(config_path, "w", encoding="utf-8") as fd: - fd.write( - """ -gateways: - duckdb_gateway: - connection: - type: duckdb -model_defaults: - dialect: bigquery -feature_flags: - dbt: - scd_type_2_support: false - """ - ) - - assert load_config_from_paths( - Config, - project_paths=[config_path], - ) == Config( - gateways={ - "duckdb_gateway": GatewayConfig(connection=DuckDBConnectionConfig()), - }, - model_defaults=ModelDefaultsConfig(dialect="bigquery"), - feature_flags=FeatureFlag(dbt=DbtFeatureFlag(scd_type_2_support=False)), - ) - - def test_load_alternative_config_type(yaml_config_path: Path, python_config_path: Path): class DerivedConfig(Config): pass @@ -1435,7 +1403,7 @@ def test_physical_table_naming_convention( gateways: test_gateway: connection: - type: duckdb + type: duckdb model_defaults: dialect: duckdb {config_part} @@ -1504,7 +1472,7 @@ def test_load_configs_in_dbt_project_without_config_py(tmp_path: Path): outputs: dev: type: duckdb - path: 'jaffle_shop.duckdb' + path: 'jaffle_shop.duckdb' """) (tmp_path / "sqlmesh.yaml").write_text(""" @@ -1513,7 +1481,7 @@ def test_load_configs_in_dbt_project_without_config_py(tmp_path: Path): state_connection: type: duckdb database: state.db -model_defaults: +model_defaults: start: '2020-01-01' """) diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index 73a2e1f1f2..73d00d619d 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import typing as t from unittest import mock from unittest.mock import call @@ -11,7 +10,6 @@ from pytest_mock.plugin import MockerFixture from sqlglot import exp, parse_one -from sqlmesh import Context from sqlmesh.core.dialect import schema_ from sqlmesh.core.snapshot import SnapshotId from sqlmesh.dbt.adapter import ParsetimeAdapter @@ -277,28 +275,6 @@ def test_adapter_map_snapshot_tables( assert renderer("{{ adapter.resolve_identifier(foo_bar) }}") == "bar" -def test_feature_flag_scd_type_2(copy_to_temp_path, caplog): - project_root = "tests/fixtures/dbt/sushi_test" - sushi_context = Context(paths=copy_to_temp_path(project_root)) - assert '"memory"."snapshots"."items_snapshot"' in sushi_context.models - assert ( - "Skipping loading Snapshot (SCD Type 2) models due to the feature flag disabling this feature" - not in caplog.text - ) - with mock.patch.dict( - os.environ, - { - "SQLMESH__FEATURE_FLAGS__DBT__SCD_TYPE_2_SUPPORT": "false", - }, - ): - sushi_context = Context(paths=copy_to_temp_path(project_root)) - assert '"memory"."snapshots"."items_snapshot"' not in sushi_context.models - assert ( - "Skipping loading Snapshot (SCD Type 2) models due to the feature flag disabling this feature" - in caplog.text - ) - - def test_quote_as_configured(): adapter = ParsetimeAdapter( JinjaMacroRegistry(), From cce46b5938c2d497b1cfe896c5ef2169555cd105 Mon Sep 17 00:00:00 2001 From: David Dai Date: Thu, 14 Aug 2025 16:09:39 -0700 Subject: [PATCH 0713/1056] fix: properly load dbt relation type for get_relation() and related functions (#5144) --- sqlmesh/dbt/adapter.py | 40 +++++++++++++++++++++------------------ tests/dbt/test_adapter.py | 30 ++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/sqlmesh/dbt/adapter.py b/sqlmesh/dbt/adapter.py index 4178c960a7..00a1d86ba2 100644 --- a/sqlmesh/dbt/adapter.py +++ b/sqlmesh/dbt/adapter.py @@ -18,6 +18,7 @@ from dbt.adapters.base import BaseRelation from dbt.adapters.base.column import Column from dbt.adapters.base.impl import AdapterResponse + from sqlmesh.core.engine_adapter.base import DataObject from sqlmesh.dbt.relation import Policy @@ -256,10 +257,9 @@ def get_relation( def load_relation(self, relation: BaseRelation) -> t.Optional[BaseRelation]: mapped_table = self._map_table_name(self._normalize(self._relation_to_table(relation))) - if not self.engine_adapter.table_exists(mapped_table): - return None - return self._table_to_relation(mapped_table) + data_object = self.engine_adapter.get_data_object(mapped_table) + return self._data_object_to_relation(data_object) if data_object is not None else None def list_relations(self, database: t.Optional[str], schema: str) -> t.List[BaseRelation]: target_schema = schema_(schema, catalog=database) @@ -269,24 +269,10 @@ def list_relations(self, database: t.Optional[str], schema: str) -> t.List[BaseR return self.list_relations_without_caching(self._table_to_relation(target_schema)) def list_relations_without_caching(self, schema_relation: BaseRelation) -> t.List[BaseRelation]: - from sqlmesh.dbt.relation import RelationType - schema = self._normalize(self._schema(schema_relation)) relations = [ - self.relation_type.create( - database=do.catalog, - schema=do.schema_name, - identifier=do.name, - quote_policy=self.quote_policy, - # DBT relation types aren't snake case and instead just one word without spaces so we remove underscores - type=( - RelationType.External - if do.type.is_unknown - else RelationType(do.type.lower().replace("_", "")) - ), - ) - for do in self.engine_adapter.get_data_objects(schema) + self._data_object_to_relation(do) for do in self.engine_adapter.get_data_objects(schema) ] return relations @@ -401,6 +387,24 @@ def _map_table_name(self, table: exp.Table) -> exp.Table: def _relation_to_table(self, relation: BaseRelation) -> exp.Table: return exp.to_table(relation.render(), dialect=self.project_dialect) + def _data_object_to_relation(self, data_object: DataObject) -> BaseRelation: + from sqlmesh.dbt.relation import RelationType + + if data_object.type.is_unknown: + dbt_relation_type = RelationType.External + elif data_object.type.is_managed_table: + dbt_relation_type = RelationType.Table + else: + dbt_relation_type = RelationType(data_object.type.lower()) + + return self.relation_type.create( + database=data_object.catalog, + schema=data_object.schema_name, + identifier=data_object.name, + quote_policy=self.quote_policy, + type=dbt_relation_type, + ) + def _table_to_relation(self, table: exp.Table) -> BaseRelation: return self.relation_type.create( database=table.catalog or None, diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index 73d00d619d..d4dbf62e74 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -36,6 +36,9 @@ def test_adapter_relation(sushi_test_project: Project, runtime_renderer: t.Calla engine_adapter.create_table( table_name="foo.another", columns_to_types={"col": exp.DataType.build("int")} ) + engine_adapter.create_view( + view_name="foo.bar_view", query_or_df=parse_one("select * from foo.bar") + ) engine_adapter.create_table( table_name="ignored.ignore", columns_to_types={"col": exp.DataType.build("int")} ) @@ -44,11 +47,24 @@ def test_adapter_relation(sushi_test_project: Project, runtime_renderer: t.Calla renderer("{{ adapter.get_relation(database=None, schema='foo', identifier='bar') }}") == '"memory"."foo"."bar"' ) + + assert ( + renderer("{{ adapter.get_relation(database=None, schema='foo', identifier='bar').type }}") + == "table" + ) + + assert ( + renderer( + "{{ adapter.get_relation(database=None, schema='foo', identifier='bar_view').type }}" + ) + == "view" + ) + assert renderer( "{%- set relation = adapter.get_relation(database=None, schema='foo', identifier='bar') -%} {{ adapter.get_columns_in_relation(relation) }}" ) == str([Column.from_description(name="baz", raw_data_type="INT")]) - assert renderer("{{ adapter.list_relations(database=None, schema='foo')|length }}") == "2" + assert renderer("{{ adapter.list_relations(database=None, schema='foo')|length }}") == "3" assert renderer( """ @@ -108,26 +124,30 @@ def test_bigquery_get_columns_in_relation( def test_normalization( sushi_test_project: Project, runtime_renderer: t.Callable, mocker: MockerFixture ): + from sqlmesh.core.engine_adapter.base import DataObject, DataObjectType + context = sushi_test_project.context assert context.target + data_object = DataObject(catalog="test", schema="bla", name="bob", type=DataObjectType.TABLE) # bla and bob will be normalized to lowercase since the target is duckdb adapter_mock = mocker.MagicMock() adapter_mock.default_catalog = "test" adapter_mock.dialect = "duckdb" - + adapter_mock.get_data_object.return_value = data_object duckdb_renderer = runtime_renderer(context, engine_adapter=adapter_mock) schema_bla = schema_("bla", "test", quoted=True) relation_bla_bob = exp.table_("bob", db="bla", catalog="test", quoted=True) duckdb_renderer("{{ adapter.get_relation(database=None, schema='bla', identifier='bob') }}") - adapter_mock.table_exists.assert_has_calls([call(relation_bla_bob)]) + adapter_mock.get_data_object.assert_has_calls([call(relation_bla_bob)]) # bla and bob will be normalized to uppercase since the target is Snowflake, even though the default dialect is duckdb adapter_mock = mocker.MagicMock() adapter_mock.default_catalog = "test" adapter_mock.dialect = "snowflake" + adapter_mock.get_data_object.return_value = data_object context.target = SnowflakeConfig( account="test", user="test", @@ -142,10 +162,10 @@ def test_normalization( relation_bla_bob = exp.table_("bob", db="bla", catalog="test", quoted=True) renderer("{{ adapter.get_relation(database=None, schema='bla', identifier='bob') }}") - adapter_mock.table_exists.assert_has_calls([call(relation_bla_bob)]) + adapter_mock.get_data_object.assert_has_calls([call(relation_bla_bob)]) renderer("{{ adapter.get_relation(database='custom_db', schema='bla', identifier='bob') }}") - adapter_mock.table_exists.assert_has_calls( + adapter_mock.get_data_object.assert_has_calls( [call(exp.table_("bob", db="bla", catalog="custom_db", quoted=True))] ) From 0c2ba86d34dab449eb8eb1c97809f0f656546870 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 14 Aug 2025 16:12:46 -0700 Subject: [PATCH 0714/1056] Fix: Support aliases for the password field in dbt target config (#5167) --- sqlmesh/dbt/target.py | 6 +++--- tests/dbt/test_config.py | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/sqlmesh/dbt/target.py b/sqlmesh/dbt/target.py index 94f4c98894..30a8f35c50 100644 --- a/sqlmesh/dbt/target.py +++ b/sqlmesh/dbt/target.py @@ -5,7 +5,7 @@ from pathlib import Path from dbt.adapters.base import BaseRelation, Column -from pydantic import Field +from pydantic import Field, AliasChoices from sqlmesh.core.console import get_console from sqlmesh.core.config.connection import ( @@ -329,7 +329,7 @@ class PostgresConfig(TargetConfig): type: t.Literal["postgres"] = "postgres" host: str user: str - password: str + password: str = Field(validation_alias=AliasChoices("pass", "password")) port: int dbname: str keepalives_idle: t.Optional[int] = None @@ -417,7 +417,7 @@ class RedshiftConfig(TargetConfig): type: t.Literal["redshift"] = "redshift" host: str user: str - password: str + password: str = Field(validation_alias=AliasChoices("pass", "password")) port: int dbname: str connect_timeout: t.Optional[int] = None diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index eaa2fe94ad..f34a1c6c74 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -650,6 +650,28 @@ def test_postgres_config(): "outputs", "dev", ) + # 'pass' field instead of 'password' + _test_warehouse_config( + """ + dbt-postgres: + target: dev + outputs: + dev: + type: postgres + host: postgres + user: postgres + pass: postgres + port: 5432 + dbname: postgres + schema: demo + threads: 3 + keepalives_idle: 0 + """, + PostgresConfig, + "dbt-postgres", + "outputs", + "dev", + ) def test_redshift_config(): @@ -674,6 +696,28 @@ def test_redshift_config(): "outputs", "dev", ) + # 'pass' field instead of 'password' + _test_warehouse_config( + """ + dbt-redshift: + target: dev + outputs: + dev: + type: redshift + host: hostname.region.redshift.amazonaws.com + user: username + pass: password1 + port: 5439 + dbname: analytics + schema: analytics + threads: 4 + ra3_node: false + """, + RedshiftConfig, + "dbt-redshift", + "outputs", + "dev", + ) def test_databricks_config(): From 40fb9b663cf6188dd35ff106925dfa951580a566 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 15 Aug 2025 10:05:41 +0300 Subject: [PATCH 0715/1056] Fix(dbt): Use the info to control logging for dbt log builtin (#5140) --- sqlmesh/dbt/builtin.py | 9 +++++++- tests/dbt/test_transformation.py | 37 +++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 4646011d57..49f07f597c 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -14,6 +14,7 @@ from ruamel.yaml import YAMLError from sqlglot import Dialect +from sqlmesh.core.console import get_console from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.snapshot.definition import DeployabilityIndex from sqlmesh.dbt.adapter import BaseAdapter, ParsetimeAdapter, RuntimeAdapter @@ -170,7 +171,13 @@ def env_var(name: str, default: t.Optional[str] = None) -> t.Optional[str]: def log(msg: str, info: bool = False) -> str: - logger.debug(msg) + if info: + # Write to both log file and stdout + logger.info(msg) + get_console().log_status_update(msg) + else: + logger.debug(msg) + return "" diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index c67621b206..e8b355e9f5 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -854,13 +854,40 @@ def test_logging(sushi_test_project: Project, runtime_renderer: t.Callable): renderer = runtime_renderer(context, engine_adapter=engine_adapter) logger = logging.getLogger("sqlmesh.dbt.builtin") - with patch.object(logger, "debug") as mock_logger: - assert renderer('{{ log("foo") }}') == "" - assert "foo" in mock_logger.call_args[0][0] - with patch.object(logger, "debug") as mock_logger: + # Test log with info=False (default), should only log to file with debug and not to console + with ( + patch.object(logger, "debug") as mock_debug, + patch.object(logger, "info") as mock_info, + patch.object(get_console(), "log_status_update") as mock_console, + ): + assert renderer('{{ log("foo") }}') == "" + mock_debug.assert_called_once() + assert "foo" in mock_debug.call_args[0][0] + mock_info.assert_not_called() + mock_console.assert_not_called() + + # Test log with info=True, should log to info and also call log_status_update + with ( + patch.object(logger, "debug") as mock_debug, + patch.object(logger, "info") as mock_info, + patch.object(get_console(), "log_status_update") as mock_console, + ): + assert renderer('{{ log("output to be logged with info", info=true) }}') == "" + mock_info.assert_called_once() + assert "output to be logged with info" in mock_info.call_args[0][0] + mock_debug.assert_not_called() + mock_console.assert_called_once() + assert "output to be logged with info" in mock_console.call_args[0][0] + + # Test print function as well, should use debug + with ( + patch.object(logger, "debug") as mock_logger, + patch.object(get_console(), "log_status_update") as mock_console, + ): assert renderer('{{ print("bar") }}') == "" - assert "bar" in mock_logger.call_args[0][0] + assert "bar" in mock_logger.call_args[0][0] + mock_console.assert_not_called() @pytest.mark.xdist_group("dbt_manifest") From 622a36f01430d574d6ba0465f6c0d92f9603c59a Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:27:11 +0300 Subject: [PATCH 0716/1056] Feat(dbt): Add support for dbt debug macro (#5160) --- pyproject.toml | 1 + sqlmesh/dbt/builtin.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4372c84861..a7380980c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -301,3 +301,4 @@ banned-module-level-imports = [ "sqlmesh/lsp/**/*.py" = ["TID251"] "tests/lsp/**/*.py" = ["TID251"] "benchmarks/lsp*.py" = ["TID251"] +"sqlmesh/dbt/builtin.py" = ["T100"] diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 49f07f597c..07edeefa2e 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -21,7 +21,7 @@ from sqlmesh.dbt.relation import Policy from sqlmesh.dbt.target import TARGET_TYPE_TO_CONFIG_CLASS from sqlmesh.dbt.util import DBT_VERSION -from sqlmesh.utils import AttributeDict, yaml +from sqlmesh.utils import AttributeDict, debug_mode_enabled, yaml from sqlmesh.utils.date import now from sqlmesh.utils.errors import ConfigError, MacroEvalError from sqlmesh.utils.jinja import JinjaMacroRegistry, MacroReference, MacroReturnVal @@ -316,6 +316,15 @@ def _try_literal_eval(value: str) -> t.Any: return value +def debug() -> str: + import sys + import ipdb # type: ignore + + frame = sys._getframe(3) + ipdb.set_trace(frame) + return "" + + BUILTIN_GLOBALS = { "dbt_version": version.__version__, "env_var": env_var, @@ -336,6 +345,10 @@ def _try_literal_eval(value: str) -> t.Any: "zip_strict": lambda *args: list(zip(*args)), } +# Add debug function conditionally both with dbt or sqlmesh equivalent flag +if os.environ.get("DBT_MACRO_DEBUGGING") or debug_mode_enabled(): + BUILTIN_GLOBALS["debug"] = debug + BUILTIN_FILTERS = { "as_bool": as_bool, "as_native": _try_literal_eval, From 527e10714644d6ffc41b23c68fb0a323dada320d Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:19:34 +0200 Subject: [PATCH 0717/1056] feat(vscode): multi project for vscode (#5084) --- .gitignore | 2 +- examples/multi/.vscode/settings.json | 3 + sqlmesh/lsp/main.py | 26 +++++++- vscode/extension/package.json | 11 ++-- vscode/extension/src/lsp/lsp.ts | 11 ++++ vscode/extension/src/utilities/config.ts | 63 +++++++++++++------ .../src/utilities/sqlmesh/sqlmesh.ts | 18 +++--- vscode/extension/tests/lineage.spec.ts | 8 +-- vscode/extension/tests/multi_project.spec.ts | 35 +++++++++++ vscode/extension/tests/utils.ts | 8 +++ 10 files changed, 147 insertions(+), 38 deletions(-) create mode 100644 examples/multi/.vscode/settings.json create mode 100644 vscode/extension/tests/multi_project.spec.ts diff --git a/.gitignore b/.gitignore index 563f2013f2..72b41b5ce1 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,4 @@ metastore_db/ spark-warehouse/ # claude -.claude/ \ No newline at end of file +.claude/ diff --git a/examples/multi/.vscode/settings.json b/examples/multi/.vscode/settings.json new file mode 100644 index 0000000000..e08af7514c --- /dev/null +++ b/examples/multi/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "sqlmesh.projectPaths": ["./repo_1", "./repo_2"] +} \ No newline at end of file diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 13e4c5d8f0..c05fff4551 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -71,12 +71,20 @@ from sqlmesh.lsp.uri import URI from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.lineage import ExternalModelReference +from sqlmesh.utils.pydantic import PydanticModel from web.server.api.endpoints.lineage import column_lineage, model_lineage from web.server.api.endpoints.models import get_models from typing import Union from dataclasses import dataclass, field +class InitializationOptions(PydanticModel): + """Initialization options for the SQLMesh Language Server, that + are passed from the client to the server.""" + + project_paths: t.Optional[t.List[str]] = None + + @dataclass class NoContext: """State when no context has been attempted to load.""" @@ -105,6 +113,11 @@ class ContextFailed: class SQLMeshLanguageServer: + # Specified folders take precedence over workspace folders or looking + # for a config files. They are explicitly set by the user and optionally + # pass in at init + specified_paths: t.Optional[t.List[Path]] = None + def __init__( self, context_class: t.Type[Context], @@ -411,6 +424,12 @@ def command_external_models_update_columns(ls: LanguageServer, raw: t.Any) -> No def initialize(ls: LanguageServer, params: types.InitializeParams) -> None: """Initialize the server when the client connects.""" try: + # Check the custom options + if params.initialization_options: + options = InitializationOptions.model_validate(params.initialization_options) + if options.project_paths is not None: + self.specified_paths = [Path(path) for path in options.project_paths] + # Check if the client supports pull diagnostics if params.capabilities and params.capabilities.text_document: diagnostics = getattr(params.capabilities.text_document, "diagnostic", None) @@ -906,7 +925,12 @@ def _context_get_or_load(self, document_uri: t.Optional[URI] = None) -> LSPConte raise Exception(state.error) raise state.error if isinstance(state, NoContext): - self._ensure_context_for_document(document_uri) + if self.specified_paths is not None: + # If specified paths are provided, create context from them + if self._create_lsp_context(self.specified_paths): + loaded_sqlmesh_message(self.server) + else: + self._ensure_context_for_document(document_uri) if isinstance(state, ContextLoaded): return state.lsp_context raise RuntimeError("Context failed to load") diff --git a/vscode/extension/package.json b/vscode/extension/package.json index a745645413..188a91af22 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -32,10 +32,13 @@ "type": "object", "title": "SQLMesh", "properties": { - "sqlmesh.projectPath": { - "type": "string", - "default": "", - "markdownDescription": "The path to the SQLMesh project. If not set, the extension will try to find the project root automatically. If set, the extension will use the project root as the workspace path, e.g. it will run `sqlmesh` and `sqlmesh_lsp` in the project root. The path can be absolute `/Users/sqlmesh_user/sqlmesh_project/sushi` or relative `./project_folder/sushi` to the workspace root." + "sqlmesh.projectPaths": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "The path to the SQLMesh project. If not set, the extension will try to find the project root automatically. If set, the extension will use the project root as the workspace path, e.g. it will run `sqlmesh` and `sqlmesh_lsp` in the project root. The path can be absolute `/Users/sqlmesh_user/sqlmesh_project/sushi` or relative `./project_folder/sushi` to the workspace root. Multiple paths can be used for multi-project setups." }, "sqlmesh.lspEntrypoint": { "type": "string", diff --git a/vscode/extension/src/lsp/lsp.ts b/vscode/extension/src/lsp/lsp.ts index 0b7a5b4b62..1a11249853 100644 --- a/vscode/extension/src/lsp/lsp.ts +++ b/vscode/extension/src/lsp/lsp.ts @@ -16,6 +16,7 @@ import { ErrorTypeSQLMeshOutdated, } from '../utilities/errors' import { CustomLSPMethods } from './custom' +import { resolveProjectPath } from '../utilities/config' type SupportedMethodsState = | { type: 'not-fetched' } @@ -109,6 +110,11 @@ export class LSPClient implements Disposable { args: sqlmesh.value.args, }, } + const paths = resolveProjectPath(getWorkspaceFolders()[0]) + if (isErr(paths)) { + traceError(`Failed to resolve project paths: ${paths.error}`) + return err({ type: 'generic', message: paths.error }) + } const clientOptions: LanguageClientOptions = { documentSelector: [ { scheme: 'file', pattern: '**/*.sql' }, @@ -117,6 +123,11 @@ export class LSPClient implements Disposable { ], diagnosticCollectionName: 'sqlmesh', outputChannel, + initializationOptions: paths.value.projectPaths + ? { + project_paths: paths.value.projectPaths, + } + : null, } traceInfo( diff --git a/vscode/extension/src/utilities/config.ts b/vscode/extension/src/utilities/config.ts index 9a88e24f36..53c2662612 100644 --- a/vscode/extension/src/utilities/config.ts +++ b/vscode/extension/src/utilities/config.ts @@ -1,17 +1,17 @@ import { workspace, WorkspaceFolder } from 'vscode' import path from 'path' import fs from 'fs' -import { Result, err, ok } from '@bus/result' +import { Result, err, isErr, ok } from '@bus/result' import { traceVerbose, traceInfo } from './common/log' import { parse } from 'shell-quote' import { z } from 'zod' -const configSchema = z.object({ - projectPath: z.string(), +const sqlmeshConfigurationSchema = z.object({ + projectPaths: z.array(z.string()), lspEntryPoint: z.string(), }) -export type SqlmeshConfiguration = z.infer +export type SqlmeshConfiguration = z.infer /** * Get the SQLMesh configuration from VS Code settings. @@ -20,16 +20,15 @@ export type SqlmeshConfiguration = z.infer */ function getSqlmeshConfiguration(): SqlmeshConfiguration { const config = workspace.getConfiguration('sqlmesh') - const projectPath = config.get('projectPath', '') + const projectPaths = config.get('projectPaths', []) const lspEntryPoint = config.get('lspEntrypoint', '') - - const parsed = configSchema.safeParse({ - projectPath, + const parsed = sqlmeshConfigurationSchema.safeParse({ + projectPaths, lspEntryPoint, }) if (!parsed.success) { throw new Error( - `Invalid sqlmesh configuration: ${JSON.stringify(parsed.error)}`, + `Invalid SQLMesh configuration: ${JSON.stringify(parsed.error)}`, ) } return parsed.data @@ -66,31 +65,57 @@ export function getSqlmeshLspEntryPoint(): } /** - * Validate and resolve the project path from configuration. + * Validate and resolve the project paths from configuration. * If no project path is configured, use the workspace folder. * If the project path is configured, it must be a directory that contains a SQLMesh project. * * @param workspaceFolder The current workspace folder - * @returns A Result containing the resolved project path or an error + * @returns A Result containing the resolved project paths or an error */ -export function resolveProjectPath( - workspaceFolder: WorkspaceFolder, -): Result { +export function resolveProjectPath(workspaceFolder: WorkspaceFolder): Result< + { + projectPaths: string[] | undefined + workspaceFolder: string + }, + string +> { const config = getSqlmeshConfiguration() - if (!config.projectPath) { + if (config.projectPaths.length === 0) { // If no project path is configured, use the workspace folder traceVerbose('No project path configured, using workspace folder') - return ok(workspaceFolder.uri.fsPath) + return ok({ + workspaceFolder: workspaceFolder.uri.fsPath, + projectPaths: undefined, + }) + } + + const resolvedPaths: string[] = [] + for (const projectPath of config.projectPaths) { + const result = resolveSingleProjectPath(workspaceFolder, projectPath) + if (isErr(result)) { + return result + } + resolvedPaths.push(result.value) } + return ok({ + projectPaths: resolvedPaths, + workspaceFolder: workspaceFolder.uri.fsPath, + }) +} + +function resolveSingleProjectPath( + workspaceFolder: WorkspaceFolder, + projectPath: string, +): Result { let resolvedPath: string // Check if the path is absolute - if (path.isAbsolute(config.projectPath)) { - resolvedPath = config.projectPath + if (path.isAbsolute(projectPath)) { + resolvedPath = projectPath } else { // Resolve relative path from workspace root - resolvedPath = path.join(workspaceFolder.uri.fsPath, config.projectPath) + resolvedPath = path.join(workspaceFolder.uri.fsPath, projectPath) } // Normalize the path diff --git a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts index 649c57fd77..c9e181fc06 100644 --- a/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts +++ b/vscode/extension/src/utilities/sqlmesh/sqlmesh.ts @@ -69,8 +69,8 @@ export const isTcloudProject = async (): Promise> => { if (isErr(resolvedPath)) { return err(resolvedPath.error) } - const tcloudYamlPath = path.join(resolvedPath.value, 'tcloud.yaml') - const tcloudYmlPath = path.join(resolvedPath.value, 'tcloud.yml') + const tcloudYamlPath = path.join(resolvedPath.value.workspaceFolder, 'tcloud.yaml') + const tcloudYmlPath = path.join(resolvedPath.value.workspaceFolder, 'tcloud.yml') const isTcloudYamlFilePresent = fs.existsSync(tcloudYamlPath) const isTcloudYmlFilePresent = fs.existsSync(tcloudYmlPath) if (isTcloudYamlFilePresent || isTcloudYmlFilePresent) { @@ -144,7 +144,7 @@ export const isSqlmeshEnterpriseInstalled = async (): Promise< }) } const called = await execAsync(tcloudBin.value.bin, ['is_sqlmesh_installed'], { - cwd: resolvedPath.value, + cwd: resolvedPath.value.workspaceFolder, env: tcloudBin.value.env, }) if (called.exitCode !== 0) { @@ -185,7 +185,7 @@ export const installSqlmeshEnterprise = async ( } const called = await execAsync(tcloudBin.value.bin, ['install_sqlmesh'], { signal: abortController.signal, - cwd: resolvedPath.value, + cwd: resolvedPath.value.workspaceFolder, env: tcloudBin.value.env, }) if (called.exitCode !== 0) { @@ -318,14 +318,14 @@ export const sqlmeshLspExec = async (): Promise< message: resolvedPath.error, }) } - const workspacePath = resolvedPath.value + const workspacePath = resolvedPath.value.workspaceFolder const configuredLSPExec = getSqlmeshLspEntryPoint() if (configuredLSPExec) { traceLog(`Using configured SQLMesh LSP entry point: ${configuredLSPExec.entrypoint} ${configuredLSPExec.args.join(' ')}`) return ok({ bin: configuredLSPExec.entrypoint, - workspacePath, + workspacePath: workspacePath, env: process.env, args: configuredLSPExec.args, }) @@ -381,7 +381,7 @@ export const sqlmeshLspExec = async (): Promise< if (isSemVerGreaterThanOrEqual(tcloudBinVersion.value, [2, 10, 1])) { return ok ({ bin: tcloudBin.value.bin, - workspacePath, + workspacePath: workspacePath, env: tcloudBin.value.env, args: ['sqlmesh_lsp'], }) @@ -407,7 +407,7 @@ export const sqlmeshLspExec = async (): Promise< } return ok({ bin: binPath, - workspacePath, + workspacePath: workspacePath, env: env.value, args: [], }) @@ -427,7 +427,7 @@ export const sqlmeshLspExec = async (): Promise< } return ok({ bin: sqlmeshLSP, - workspacePath, + workspacePath: workspacePath, env: env.value, args: [], }) diff --git a/vscode/extension/tests/lineage.spec.ts b/vscode/extension/tests/lineage.spec.ts index 8817e7b325..66e3048246 100644 --- a/vscode/extension/tests/lineage.spec.ts +++ b/vscode/extension/tests/lineage.spec.ts @@ -40,7 +40,7 @@ test('Lineage panel renders correctly - relative project path', async ({ await fs.copy(SUSHI_SOURCE_PATH, projectDir) const settings = { - 'sqlmesh.projectPath': './projects/sushi', + 'sqlmesh.projectPaths': ['./projects/sushi'], 'python.defaultInterpreterPath': sharedCodeServer.defaultPythonInterpreter, } await fs.ensureDir(path.join(tempDir, '.vscode')) @@ -67,7 +67,7 @@ test('Lineage panel renders correctly - absolute project path', async ({ await fs.copy(SUSHI_SOURCE_PATH, projectDir) const settings = { - 'sqlmesh.projectPath': projectDir, + 'sqlmesh.projectPaths': [projectDir], 'python.defaultInterpreterPath': sharedCodeServer.defaultPythonInterpreter, } await fs.writeJson(path.join(tempDir, '.vscode', 'settings.json'), settings, { @@ -90,7 +90,7 @@ test('Lineage panel renders correctly - relative project outside of workspace', await fs.ensureDir(workspaceDir) const settings = { - 'sqlmesh.projectPath': './../projects/sushi', + 'sqlmesh.projectPaths': ['./../projects/sushi'], 'python.defaultInterpreterPath': sharedCodeServer.defaultPythonInterpreter, } await fs.ensureDir(path.join(workspaceDir, '.vscode')) @@ -115,7 +115,7 @@ test('Lineage panel renders correctly - absolute path project outside of workspa await fs.ensureDir(workspaceDir) const settings = { - 'sqlmesh.projectPath': projectDir, + 'sqlmesh.projectPaths': [projectDir], 'python.defaultInterpreterPath': sharedCodeServer.defaultPythonInterpreter, } await fs.ensureDir(path.join(workspaceDir, '.vscode')) diff --git a/vscode/extension/tests/multi_project.spec.ts b/vscode/extension/tests/multi_project.spec.ts new file mode 100644 index 0000000000..987c014537 --- /dev/null +++ b/vscode/extension/tests/multi_project.spec.ts @@ -0,0 +1,35 @@ +import { test } from './fixtures' +import { + MULTI_SOURCE_PATH, + openServerPage, + waitForLoadedSQLMesh, +} from './utils' +import fs from 'fs-extra' + +test('should work with multi-project setups', async ({ + page, + sharedCodeServer, + tempDir, +}) => { + await fs.copy(MULTI_SOURCE_PATH, tempDir) + + // Open the server + await openServerPage(page, tempDir, sharedCodeServer) + + // Open a model + await page + .getByRole('treeitem', { name: 'repo_1', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'a.sql', exact: true }) + .locator('a') + .click() + + // Wait for for the project to be loaded + await waitForLoadedSQLMesh(page) +}) diff --git a/vscode/extension/tests/utils.ts b/vscode/extension/tests/utils.ts index b91dec33e5..effdc3c062 100644 --- a/vscode/extension/tests/utils.ts +++ b/vscode/extension/tests/utils.ts @@ -14,6 +14,14 @@ export const SUSHI_SOURCE_PATH = path.join( 'examples', 'sushi', ) +export const MULTI_SOURCE_PATH = path.join( + __dirname, + '..', + '..', + '..', + 'examples', + 'multi', +) export const REPO_ROOT = path.join(__dirname, '..', '..', '..') /** From 24e3e5031844a9dbde70f084b04d4a764c449c89 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:24:02 +0200 Subject: [PATCH 0718/1056] fix: adding common to pnpm workspaces (#5170) --- pnpm-lock.yaml | 2 +- pnpm-workspace.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e92c12dd30..3f3655b1f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -397,7 +397,7 @@ importers: specifier: ^1.13.2 version: 1.13.2 - web/shared_ui: + web/common: dependencies: react: specifier: ^18.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 377ec655e9..fe1b5be597 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,4 +3,4 @@ packages: - vscode/extension - vscode/react - web/client - - web/shared_ui + - web/common From 943e496e3c6ec4e38a78e80cd9b6b1cceabcee84 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:34:47 +0200 Subject: [PATCH 0719/1056] feat: function to get range of key/value in model block (#5119) Co-authored-by: Jo <46752250+georgesittas@users.noreply.github.com> --- sqlmesh/core/linter/helpers.py | 207 ++++++++++++++++++++++-------- tests/core/linter/test_helpers.py | 66 +++++++++- 2 files changed, 214 insertions(+), 59 deletions(-) diff --git a/sqlmesh/core/linter/helpers.py b/sqlmesh/core/linter/helpers.py index 3f6e96765f..3c79f83a43 100644 --- a/sqlmesh/core/linter/helpers.py +++ b/sqlmesh/core/linter/helpers.py @@ -2,7 +2,7 @@ from sqlmesh.core.linter.rule import Range, Position from sqlmesh.utils.pydantic import PydanticModel -from sqlglot import tokenize, TokenType +from sqlglot import tokenize, TokenType, Token import typing as t @@ -122,25 +122,65 @@ def read_range_from_file(file: Path, text_range: Range) -> str: return read_range_from_string("".join(lines), text_range) +def get_start_and_end_of_model_block( + tokens: t.List[Token], +) -> t.Optional[t.Tuple[int, int]]: + """ + Returns the start and end tokens of the MODEL block in an SQL file. + The MODEL block is defined as the first occurrence of the keyword "MODEL" followed by + an opening parenthesis and a closing parenthesis that matches the opening one. + """ + # 1) Find the MODEL token + try: + model_idx = next( + i + for i, tok in enumerate(tokens) + if tok.token_type is TokenType.VAR and tok.text.upper() == "MODEL" + ) + except StopIteration: + return None + + # 2) Find the opening parenthesis for the MODEL properties list + try: + lparen_idx = next( + i + for i in range(model_idx + 1, len(tokens)) + if tokens[i].token_type is TokenType.L_PAREN + ) + except StopIteration: + return None + + # 3) Find the matching closing parenthesis by looking for the first semicolon after + # the opening parenthesis and assuming the MODEL block ends there. + try: + closing_semicolon = next( + i + for i in range(lparen_idx + 1, len(tokens)) + if tokens[i].token_type is TokenType.SEMICOLON + ) + # If we find a semicolon, we can assume the MODEL block ends there + rparen_idx = closing_semicolon - 1 + if tokens[rparen_idx].token_type is TokenType.R_PAREN: + return (lparen_idx, rparen_idx) + return None + except StopIteration: + return None + + def get_range_of_model_block( sql: str, dialect: str, ) -> t.Optional[Range]: """ - Get the range of the model block in an SQL file. + Get the range of the model block in an SQL file, """ tokens = tokenize(sql, dialect=dialect) - - # Find start of the model block - start = next( - (t for t in tokens if t.token_type is TokenType.VAR and t.text.upper() == "MODEL"), - None, - ) - end = next((t for t in tokens if t.token_type is TokenType.SEMICOLON), None) - - if start is None or end is None: + block = get_start_and_end_of_model_block(tokens) + if not block: return None - + (start_idx, end_idx) = block + start = tokens[start_idx - 1] + end = tokens[end_idx + 1] start_position = TokenPositionDetails( line=start.line, col=start.col, @@ -153,10 +193,10 @@ def get_range_of_model_block( start=end.start, end=end.end, ) - splitlines = sql.splitlines() return Range( - start=start_position.to_range(splitlines).start, end=end_position.to_range(splitlines).end + start=start_position.to_range(splitlines).start, + end=end_position.to_range(splitlines).end, ) @@ -164,49 +204,112 @@ def get_range_of_a_key_in_model_block( sql: str, dialect: str, key: str, -) -> t.Optional[Range]: +) -> t.Optional[t.Tuple[Range, Range]]: """ - Get the range of a specific key in the model block of an SQL file. + Get the ranges of a specific key and its value in the MODEL block of an SQL file. + + Returns a tuple of (key_range, value_range) if found, otherwise None. """ tokens = tokenize(sql, dialect=dialect) - if tokens is None: + if not tokens: return None - # Find the start of the model block - start_index = next( - ( - i - for i, t in enumerate(tokens) - if t.token_type is TokenType.VAR and t.text.upper() == "MODEL" - ), - None, - ) - end_index = next( - (i for i, t in enumerate(tokens) if t.token_type is TokenType.SEMICOLON), - None, - ) - if start_index is None or end_index is None: - return None - if start_index >= end_index: + block = get_start_and_end_of_model_block(tokens) + if not block: return None + (lparen_idx, rparen_idx) = block + + # 4) Scan within the MODEL property list for the key at top-level (depth == 1) + # Initialize depth to 1 since we're inside the first parentheses + depth = 1 + for i in range(lparen_idx + 1, rparen_idx): + tok = tokens[i] + tt = tok.token_type + + if tt is TokenType.L_PAREN: + depth += 1 + continue + if tt is TokenType.R_PAREN: + depth -= 1 + # If we somehow exit before rparen_idx, stop early + if depth <= 0: + break + continue + + if depth == 1 and tt is TokenType.VAR and tok.text.upper() == key.upper(): + # Validate key position: it should immediately follow '(' or ',' at top level + prev_idx = i - 1 + prev_tt = tokens[prev_idx].token_type if prev_idx >= 0 else None + if prev_tt not in (TokenType.L_PAREN, TokenType.COMMA): + continue + + # Key range + lines = sql.splitlines() + key_start = TokenPositionDetails( + line=tok.line, col=tok.col, start=tok.start, end=tok.end + ) + key_range = key_start.to_range(lines) + + value_start_idx = i + 1 + if value_start_idx >= rparen_idx: + return None + + # Walk to the end of the value expression: until top-level comma or closing paren + # Track internal nesting for (), [], {} + nested = 0 + j = value_start_idx + value_end_idx = value_start_idx + + def is_open(t: TokenType) -> bool: + return t in (TokenType.L_PAREN, TokenType.L_BRACE, TokenType.L_BRACKET) + + def is_close(t: TokenType) -> bool: + return t in (TokenType.R_PAREN, TokenType.R_BRACE, TokenType.R_BRACKET) + + while j < rparen_idx: + ttype = tokens[j].token_type + if is_open(ttype): + nested += 1 + elif is_close(ttype): + nested -= 1 + + # End of value: at top-level (nested == 0) encountering a comma or the end paren + if nested == 0 and ( + ttype is TokenType.COMMA or (ttype is TokenType.R_PAREN and depth == 1) + ): + # For comma, don't include it in the value range + # For closing paren, include it only if it's part of the value structure + if ttype is TokenType.COMMA: + # Don't include the comma in the value range + break + else: + # Include the closing parenthesis in the value range + value_end_idx = j + break + + value_end_idx = j + j += 1 + + value_start_tok = tokens[value_start_idx] + value_end_tok = tokens[value_end_idx] + + value_start_pos = TokenPositionDetails( + line=value_start_tok.line, + col=value_start_tok.col, + start=value_start_tok.start, + end=value_start_tok.end, + ) + value_end_pos = TokenPositionDetails( + line=value_end_tok.line, + col=value_end_tok.col, + start=value_end_tok.start, + end=value_end_tok.end, + ) + value_range = Range( + start=value_start_pos.to_range(lines).start, + end=value_end_pos.to_range(lines).end, + ) - tokens_of_interest = tokens[start_index + 1 : end_index] - # Find the key token - key_token = next( - ( - t - for t in tokens_of_interest - if t.token_type is TokenType.VAR and t.text.upper() == key.upper() - ), - None, - ) - if key_token is None: - return None + return (key_range, value_range) - position = TokenPositionDetails( - line=key_token.line, - col=key_token.col, - start=key_token.start, - end=key_token.end, - ) - return position.to_range(sql.splitlines()) + return None diff --git a/tests/core/linter/test_helpers.py b/tests/core/linter/test_helpers.py index f3ae193bb0..c3ba46f304 100644 --- a/tests/core/linter/test_helpers.py +++ b/tests/core/linter/test_helpers.py @@ -52,8 +52,17 @@ def test_get_range_of_a_key_in_model_block_testing_on_sushi(): ] assert len(sql_models) > 0 + # Test that the function works for all keys in the model block for model in sql_models: - possible_keys = ["name", "tags", "description", "columns", "owner", "cron", "dialect"] + possible_keys = [ + "name", + "tags", + "description", + "column_descriptions", + "owner", + "cron", + "dialect", + ] dialect = model.dialect assert dialect is not None @@ -67,12 +76,55 @@ def test_get_range_of_a_key_in_model_block_testing_on_sushi(): count_properties_checked = 0 for key in possible_keys: - range = get_range_of_a_key_in_model_block(content, dialect, key) - - # Check that the range starts with the key and ends with ; - if range: - read_range = read_range_from_file(path, range) - assert read_range.lower() == key.lower() + ranges = get_range_of_a_key_in_model_block(content, dialect, key) + + if ranges: + key_range, value_range = ranges + read_key = read_range_from_file(path, key_range) + assert read_key.lower() == key.lower() + # Value range should be non-empty + read_value = read_range_from_file(path, value_range) + assert len(read_value) > 0 count_properties_checked += 1 assert count_properties_checked > 0 + + # Test that the function works for different kind of value blocks + tests = [ + ("sushi.customers", "name", "sushi.customers"), + ( + "sushi.customers", + "tags", + "(pii, fact)", + ), + ("sushi.customers", "description", "'Sushi customer data'"), + ( + "sushi.customers", + "column_descriptions", + "( customer_id = 'customer_id uniquely identifies customers' )", + ), + ("sushi.customers", "owner", "jen"), + ("sushi.customers", "cron", "'@daily'"), + ] + for model_name, key, value in tests: + model = context.get_model(model_name) + assert model is not None + + dialect = model.dialect + assert dialect is not None + + path = model._path + assert path is not None + + with open(path, "r", encoding="utf-8") as file: + content = file.read() + + ranges = get_range_of_a_key_in_model_block(content, dialect, key) + assert ranges is not None, f"Could not find key '{key}' in model '{model_name}'" + + key_range, value_range = ranges + read_key = read_range_from_file(path, key_range) + assert read_key.lower() == key.lower() + + read_value = read_range_from_file(path, value_range) + assert read_value == value From 0109a3a5232f27ffdfc99ee3bdeea6444162f4b5 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 15 Aug 2025 12:57:39 +0300 Subject: [PATCH 0720/1056] Feat(vscode): Add the Table Diff view in the extension (#4917) --- sqlmesh/lsp/api.py | 12 +- sqlmesh/lsp/custom.py | 61 ++ sqlmesh/lsp/main.py | 149 ++++- vscode/bus/src/callbacks.ts | 45 +- vscode/extension/assets/images/diff.svg | 3 + vscode/extension/package.json | 14 +- vscode/extension/src/commands/tableDiff.ts | 589 ++++++++++++++++++ vscode/extension/src/extension.ts | 9 + vscode/extension/src/lsp/custom.ts | 43 ++ vscode/extension/src/webviews/lineagePanel.ts | 3 + vscode/react/src/api/instance.ts | 2 +- .../react/src/components/tablediff/Card.tsx | 51 ++ .../tablediff/ColumnStatsSection.tsx | 381 +++++++++++ .../components/tablediff/ContentSections.tsx | 78 +++ .../src/components/tablediff/HeaderCard.tsx | 201 ++++++ .../react/src/components/tablediff/Legend.tsx | 59 ++ .../components/tablediff/RerunController.tsx | 168 +++++ .../components/tablediff/RowStatsSection.tsx | 102 +++ .../tablediff/SampleDataSection.tsx | 440 +++++++++++++ .../tablediff/SchemaDiffSection.tsx | 122 ++++ .../src/components/tablediff/SectionCard.tsx | 63 ++ .../components/tablediff/SectionToggle.tsx | 47 ++ .../src/components/tablediff/TableDiff.tsx | 148 +++++ .../components/tablediff/TableDiffResults.tsx | 64 ++ .../react/src/components/tablediff/hooks.ts | 29 + .../react/src/components/tablediff/index.ts | 15 + .../components/tablediff/tailwind-utils.ts | 86 +++ .../react/src/components/tablediff/types.ts | 84 +++ vscode/react/src/main.tsx | 37 +- vscode/react/src/pages/tablediff.tsx | 22 + vscode/react/src/routeTree.gen.ts | 24 +- vscode/react/src/routes/__root.tsx | 5 +- vscode/react/src/routes/tablediff.tsx | 6 + web/server/api/endpoints/table_diff.py | 109 +++- web/server/models.py | 27 +- 35 files changed, 3264 insertions(+), 34 deletions(-) create mode 100644 vscode/extension/assets/images/diff.svg create mode 100644 vscode/extension/src/commands/tableDiff.ts create mode 100644 vscode/react/src/components/tablediff/Card.tsx create mode 100644 vscode/react/src/components/tablediff/ColumnStatsSection.tsx create mode 100644 vscode/react/src/components/tablediff/ContentSections.tsx create mode 100644 vscode/react/src/components/tablediff/HeaderCard.tsx create mode 100644 vscode/react/src/components/tablediff/Legend.tsx create mode 100644 vscode/react/src/components/tablediff/RerunController.tsx create mode 100644 vscode/react/src/components/tablediff/RowStatsSection.tsx create mode 100644 vscode/react/src/components/tablediff/SampleDataSection.tsx create mode 100644 vscode/react/src/components/tablediff/SchemaDiffSection.tsx create mode 100644 vscode/react/src/components/tablediff/SectionCard.tsx create mode 100644 vscode/react/src/components/tablediff/SectionToggle.tsx create mode 100644 vscode/react/src/components/tablediff/TableDiff.tsx create mode 100644 vscode/react/src/components/tablediff/TableDiffResults.tsx create mode 100644 vscode/react/src/components/tablediff/hooks.ts create mode 100644 vscode/react/src/components/tablediff/index.ts create mode 100644 vscode/react/src/components/tablediff/tailwind-utils.ts create mode 100644 vscode/react/src/components/tablediff/types.ts create mode 100644 vscode/react/src/pages/tablediff.tsx create mode 100644 vscode/react/src/routes/tablediff.tsx diff --git a/sqlmesh/lsp/api.py b/sqlmesh/lsp/api.py index a034283759..3135614d4b 100644 --- a/sqlmesh/lsp/api.py +++ b/sqlmesh/lsp/api.py @@ -13,7 +13,7 @@ CustomMethodRequestBaseClass, CustomMethodResponseBaseClass, ) -from web.server.models import LineageColumn, Model +from web.server.models import LineageColumn, Model, TableDiff API_FEATURE = "sqlmesh/api" @@ -25,7 +25,7 @@ class ApiRequest(CustomMethodRequestBaseClass): """ requestId: str - url: str + endpoint: str method: t.Optional[str] = "GET" params: t.Optional[t.Dict[str, t.Any]] = None body: t.Optional[t.Dict[str, t.Any]] = None @@ -74,3 +74,11 @@ class ApiResponseGetColumnLineage(BaseAPIResponse): """ data: t.Dict[str, t.Dict[str, LineageColumn]] + + +class ApiResponseGetTableDiff(BaseAPIResponse): + """ + Response from the SQLMesh API for the get_table_diff endpoint. + """ + + data: t.Optional[TableDiff] diff --git a/sqlmesh/lsp/custom.py b/sqlmesh/lsp/custom.py index 8ad6418401..84be43ee0e 100644 --- a/sqlmesh/lsp/custom.py +++ b/sqlmesh/lsp/custom.py @@ -158,6 +158,17 @@ class ListWorkspaceTestsRequest(CustomMethodRequestBaseClass): pass +GET_ENVIRONMENTS_FEATURE = "sqlmesh/get_environments" + + +class GetEnvironmentsRequest(CustomMethodRequestBaseClass): + """ + Request to get all environments in the current project. + """ + + pass + + class TestEntry(PydanticModel): """ An entry representing a test in the workspace. @@ -194,3 +205,53 @@ class RunTestRequest(CustomMethodRequestBaseClass): class RunTestResponse(CustomMethodResponseBaseClass): success: bool error_message: t.Optional[str] = None + + +class EnvironmentInfo(PydanticModel): + """ + Information about an environment. + """ + + name: str + snapshots: t.List[str] + start_at: str + plan_id: str + + +class GetEnvironmentsResponse(CustomMethodResponseBaseClass): + """ + Response containing all environments in the current project. + """ + + environments: t.Dict[str, EnvironmentInfo] + pinned_environments: t.Set[str] + default_target_environment: str + + +GET_MODELS_FEATURE = "sqlmesh/get_models" + + +class GetModelsRequest(CustomMethodRequestBaseClass): + """ + Request to get all models available for table diff. + """ + + pass + + +class ModelInfo(PydanticModel): + """ + Information about a model for table diff. + """ + + name: str + fqn: str + description: t.Optional[str] = None + + +class GetModelsResponse(CustomMethodResponseBaseClass): + """ + Response containing all models available for table diff. + """ + + models: t.List[ModelInfo] diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index c05fff4551..4d91dcc071 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -14,14 +14,17 @@ WorkspaceInlayHintRefreshRequest, ) from pygls.server import LanguageServer +from sqlglot import exp from sqlmesh._version import __version__ from sqlmesh.core.context import Context +from sqlmesh.utils.date import to_timestamp from sqlmesh.lsp.api import ( API_FEATURE, ApiRequest, ApiResponseGetColumnLineage, ApiResponseGetLineage, ApiResponseGetModels, + ApiResponseGetTableDiff, ) from sqlmesh.lsp.commands import EXTERNAL_MODEL_UPDATE_COLUMNS @@ -36,6 +39,8 @@ RENDER_MODEL_FEATURE, SUPPORTED_METHODS_FEATURE, FORMAT_PROJECT_FEATURE, + GET_ENVIRONMENTS_FEATURE, + GET_MODELS_FEATURE, AllModelsRequest, AllModelsResponse, AllModelsForRenderRequest, @@ -57,6 +62,12 @@ RUN_TEST_FEATURE, RunTestRequest, RunTestResponse, + GetEnvironmentsRequest, + GetEnvironmentsResponse, + EnvironmentInfo, + GetModelsRequest, + GetModelsResponse, + ModelInfo, ) from sqlmesh.lsp.errors import ContextFailedError, context_error_to_diagnostic from sqlmesh.lsp.helpers import to_lsp_range, to_sqlmesh_position @@ -74,9 +85,12 @@ from sqlmesh.utils.pydantic import PydanticModel from web.server.api.endpoints.lineage import column_lineage, model_lineage from web.server.api.endpoints.models import get_models +from web.server.api.endpoints.table_diff import _process_sample_data from typing import Union from dataclasses import dataclass, field +from web.server.models import RowDiff, SchemaDiff, TableDiff + class InitializationOptions(PydanticModel): """Initialization options for the SQLMesh Language Server, that @@ -154,6 +168,8 @@ def __init__( LIST_WORKSPACE_TESTS_FEATURE: self._list_workspace_tests, LIST_DOCUMENT_TESTS_FEATURE: self._list_document_tests, RUN_TEST_FEATURE: self._run_test, + GET_ENVIRONMENTS_FEATURE: self._custom_get_environments, + GET_MODELS_FEATURE: self._custom_get_models, } # Register LSP features (e.g., formatting, hover, etc.) @@ -246,13 +262,71 @@ def _custom_format_project( ls.log_trace(f"Error formatting project: {e}") return FormatProjectResponse() + def _custom_get_environments( + self, ls: LanguageServer, params: GetEnvironmentsRequest + ) -> GetEnvironmentsResponse: + """Get all environments in the current project.""" + try: + context = self._context_get_or_load() + environments = {} + + # Get environments from state + for env in context.context.state_reader.get_environments(): + environments[env.name] = EnvironmentInfo( + name=env.name, + snapshots=[s.fingerprint.to_identifier() for s in env.snapshots], + start_at=str(to_timestamp(env.start_at)), + plan_id=env.plan_id or "", + ) + + return GetEnvironmentsResponse( + environments=environments, + pinned_environments=context.context.config.pinned_environments, + default_target_environment=context.context.config.default_target_environment, + ) + except Exception as e: + ls.log_trace(f"Error getting environments: {e}") + return GetEnvironmentsResponse( + response_error=str(e), + environments={}, + pinned_environments=set(), + default_target_environment="", + ) + + def _custom_get_models(self, ls: LanguageServer, params: GetModelsRequest) -> GetModelsResponse: + """Get all models available for table diff.""" + try: + context = self._context_get_or_load() + models = [ + ModelInfo( + name=model.name, + fqn=model.fqn, + description=model.description, + ) + for model in context.context.models.values() + # Filter for models that are suitable for table diff + if model._path is not None # Has a file path + ] + return GetModelsResponse(models=models) + except Exception as e: + ls.log_trace(f"Error getting table diff models: {e}") + return GetModelsResponse( + response_error=str(e), + models=[], + ) + def _custom_api( self, ls: LanguageServer, request: ApiRequest - ) -> t.Union[ApiResponseGetModels, ApiResponseGetColumnLineage, ApiResponseGetLineage]: + ) -> t.Union[ + ApiResponseGetModels, + ApiResponseGetColumnLineage, + ApiResponseGetLineage, + ApiResponseGetTableDiff, + ]: ls.log_trace(f"API request: {request}") context = self._context_get_or_load() - parsed_url = urllib.parse.urlparse(request.url) + parsed_url = urllib.parse.urlparse(request.endpoint) path_parts = parsed_url.path.strip("/").split("/") if request.method == "GET": @@ -280,7 +354,76 @@ def _custom_api( ) return ApiResponseGetColumnLineage(data=column_lineage_response) - raise NotImplementedError(f"API request not implemented: {request.url}") + if path_parts[:2] == ["api", "table_diff"]: + import numpy as np + + # /api/table_diff + params = request.params + table_diff_result: t.Optional[TableDiff] = None + if params := request.params: + source = getattr(params, "source", "") if params else "" + target = getattr(params, "target", "") if params else "" + on = getattr(params, "on", None) if params else None + model_or_snapshot = ( + getattr(params, "model_or_snapshot", None) if params else None + ) + where = getattr(params, "where", None) if params else None + temp_schema = getattr(params, "temp_schema", None) if params else None + limit = getattr(params, "limit", 20) if params else 20 + + table_diffs = context.context.table_diff( + source=source, + target=target, + on=exp.condition(on) if on else None, + select_models={model_or_snapshot} if model_or_snapshot else None, + where=where, + limit=limit, + show=False, + ) + + if table_diffs: + diff = table_diffs[0] if isinstance(table_diffs, list) else table_diffs + + _schema_diff = diff.schema_diff() + _row_diff = diff.row_diff(temp_schema=temp_schema) + schema_diff = SchemaDiff( + source=_schema_diff.source, + target=_schema_diff.target, + source_schema=_schema_diff.source_schema, + target_schema=_schema_diff.target_schema, + added=_schema_diff.added, + removed=_schema_diff.removed, + modified=_schema_diff.modified, + ) + + # create a readable column-centric sample data structure + processed_sample_data = _process_sample_data(_row_diff, source, target) + + row_diff = RowDiff( + source=_row_diff.source, + target=_row_diff.target, + stats=_row_diff.stats, + sample=_row_diff.sample.replace({np.nan: None}).to_dict(), + joined_sample=_row_diff.joined_sample.replace({np.nan: None}).to_dict(), + s_sample=_row_diff.s_sample.replace({np.nan: None}).to_dict(), + t_sample=_row_diff.t_sample.replace({np.nan: None}).to_dict(), + column_stats=_row_diff.column_stats.replace({np.nan: None}).to_dict(), + source_count=_row_diff.source_count, + target_count=_row_diff.target_count, + count_pct_change=_row_diff.count_pct_change, + decimals=getattr(_row_diff, "decimals", 3), + processed_sample_data=processed_sample_data, + ) + + s_index, t_index, _ = diff.key_columns + table_diff_result = TableDiff( + schema_diff=schema_diff, + row_diff=row_diff, + on=[(s.name, t.name) for s, t in zip(s_index, t_index)], + ) + return ApiResponseGetTableDiff(data=table_diff_result) + + raise NotImplementedError(f"API request not implemented: {request.endpoint}") def _custom_supported_methods( self, ls: LanguageServer, params: SupportedMethodsRequest diff --git a/vscode/bus/src/callbacks.ts b/vscode/bus/src/callbacks.ts index 8c492ace8c..180ed0f330 100644 --- a/vscode/bus/src/callbacks.ts +++ b/vscode/bus/src/callbacks.ts @@ -51,13 +51,56 @@ export type RPCMethods = { } api_query: { params: { - url: string + endpoint: string method: string params: any body: any } result: any } + get_selected_model: { + params: {} + result: { + selectedModel?: any + } + } + get_all_models: { + params: {} + result: { + ok: boolean + models?: any[] + error?: string + } + } + set_selected_model: { + params: { + model: any + } + result: { + ok: boolean + selectedModel?: any + } + } + get_environments: { + params: {} + result: { + ok: boolean + environments?: Record + error?: string + } + } + run_table_diff: { + params: { + sourceModel: string + sourceEnvironment: string + targetEnvironment: string + } + result: { + ok: boolean + data?: any + error?: string + } + } } & RPCMethodsShape export type RPCRequest = { diff --git a/vscode/extension/assets/images/diff.svg b/vscode/extension/assets/images/diff.svg new file mode 100644 index 0000000000..fec20deaa1 --- /dev/null +++ b/vscode/extension/assets/images/diff.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 188a91af22..35499ad68f 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -61,7 +61,8 @@ { "id": "sqlmesh.lineage", "name": "", - "type": "webview" + "type": "webview", + "icon": "./assets/images/dag.svg" } ] }, @@ -112,6 +113,12 @@ "command": "sqlmesh.stop", "title": "SQLMesh: Stop Server", "description": "SQLMesh" + }, + { + "command": "sqlmesh.showTableDiff", + "title": "SQLMesh: Show Table Diff", + "description": "SQLMesh", + "icon": "$(diff)" } ], "menus": { @@ -120,6 +127,11 @@ "command": "sqlmesh.renderModel", "when": "resourceExtname == .sql", "group": "navigation" + }, + { + "command": "sqlmesh.showTableDiff", + "when": "resourceExtname == .sql", + "group": "navigation" } ] } diff --git a/vscode/extension/src/commands/tableDiff.ts b/vscode/extension/src/commands/tableDiff.ts new file mode 100644 index 0000000000..ac1a7a3069 --- /dev/null +++ b/vscode/extension/src/commands/tableDiff.ts @@ -0,0 +1,589 @@ +import * as vscode from 'vscode' +import { LSPClient } from '../lsp/lsp' +import { isErr } from '@bus/result' +import { CallbackEvent, RPCRequest } from '@bus/callbacks' +import { getWorkspaceFolders } from '../utilities/common/vscodeapi' + +interface ModelInfo { + name: string + fqn: string + description?: string | null +} + +export function showTableDiff( + lspClient?: LSPClient, + extensionUri?: vscode.Uri, +) { + return async () => { + if (!lspClient) { + vscode.window.showErrorMessage('LSP client not available') + return + } + + if (!extensionUri) { + vscode.window.showErrorMessage('Extension URI not available') + return + } + + // Get the current active editor + const activeEditor = vscode.window.activeTextEditor + let selectedModelInfo: ModelInfo | null = null + + if (!activeEditor) { + // No active editor, show a list of all models + const allModelsResult = await lspClient.call_custom_method( + 'sqlmesh/get_models', + {}, + ) + + if (isErr(allModelsResult)) { + vscode.window.showErrorMessage( + `Failed to get models: ${allModelsResult.error.message}`, + ) + return + } + + if ( + !allModelsResult.value.models || + allModelsResult.value.models.length === 0 + ) { + vscode.window.showInformationMessage('No models found in the project') + return + } + + // Let user choose from all models + const items = (allModelsResult.value.models as ModelInfo[]).map( + (model: ModelInfo) => ({ + label: model.name, + description: model.fqn, + detail: model.description ? model.description : undefined, + model: { + name: model.name, + fqn: model.fqn, + description: model.description, + }, + }), + ) + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a model for table diff', + }) + + if (!selected) { + return + } + + selectedModelInfo = selected.model + } else { + // Get the current document URI and check if it contains models + const documentUri = activeEditor.document.uri.toString(true) + + // Call the render model API to get models in the current file + const result = await lspClient.call_custom_method( + 'sqlmesh/render_model', + { + textDocumentUri: documentUri, + }, + ) + + if (isErr(result)) { + vscode.window.showErrorMessage( + `Failed to get models from current file: ${result.error.message}`, + ) + return + } + + // Check if we got any models + if (!result.value.models || result.value.models.length === 0) { + vscode.window.showInformationMessage( + 'No models found in the current file', + ) + return + } + + // If multiple models, let user choose + if (result.value.models.length > 1) { + const items = result.value.models.map(model => ({ + label: model.name, + description: model.fqn, + detail: model.description ? model.description : undefined, + model: model, + })) + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a model for table diff', + }) + + if (!selected) { + return + } + + selectedModelInfo = selected.model + } else { + selectedModelInfo = result.value.models[0] + } + } + + // Ensure we have a selected model + if (!selectedModelInfo) { + vscode.window.showErrorMessage('No model selected') + return + } + + // Get environments for selection + const environmentsResult = await lspClient.call_custom_method( + 'sqlmesh/get_environments', + {}, + ) + + if (isErr(environmentsResult)) { + vscode.window.showErrorMessage( + `Failed to get environments: ${environmentsResult.error.message}`, + ) + return + } + + const environments = environmentsResult.value.environments || {} + const environmentNames = Object.keys(environments) + + if (environmentNames.length === 0) { + vscode.window.showErrorMessage('No environments found') + return + } + + // Let user select source environment + const sourceEnvironmentItems = environmentNames.map(env => ({ + label: env, + description: `Source environment: ${env}`, + })) + + const selectedSourceEnv = await vscode.window.showQuickPick( + sourceEnvironmentItems, + { + placeHolder: 'Select source environment', + }, + ) + + if (!selectedSourceEnv) { + return + } + + // Let user select target environment (excluding source) + const targetEnvironmentItems = environmentNames + .filter(env => env !== selectedSourceEnv.label) + .map(env => ({ + label: env, + description: `Target environment: ${env}`, + })) + + if (targetEnvironmentItems.length === 0) { + vscode.window.showErrorMessage( + 'Need at least two environments for comparison', + ) + return + } + + const selectedTargetEnv = await vscode.window.showQuickPick( + targetEnvironmentItems, + { + placeHolder: 'Select target environment', + }, + ) + + if (!selectedTargetEnv) { + return + } + + // Run table diff immediately with selected parameters + const tableDiffResult = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'SQLMesh', + cancellable: false, + }, + async progress => { + progress.report({ message: 'Calculating table differences...' }) + + return await lspClient.call_custom_method('sqlmesh/api', { + method: 'GET', + endpoint: '/api/table_diff', + params: { + source: selectedSourceEnv.label, + target: selectedTargetEnv.label, + model_or_snapshot: selectedModelInfo.name, + }, + body: {}, + }) + }, + ) + + if (isErr(tableDiffResult)) { + vscode.window.showErrorMessage( + `Failed to run table diff: ${tableDiffResult.error.message}`, + ) + return + } + + // Determine the view column for side-by-side display + // Find the rightmost column with an editor + let maxColumn = vscode.ViewColumn.One + for (const editor of vscode.window.visibleTextEditors) { + if (editor.viewColumn && editor.viewColumn > maxColumn) { + maxColumn = editor.viewColumn + } + } + + // Open in the next column after the rightmost editor + const viewColumn = maxColumn + 1 + + // Create a webview panel for the table diff + const panel = vscode.window.createWebviewPanel( + 'sqlmesh.tableDiff', + `SQLMesh Table Diff - ${selectedModelInfo.name} (${selectedSourceEnv.label} → ${selectedTargetEnv.label})`, + viewColumn, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [extensionUri], + }, + ) + + // Store the initial data for the webview + // eslint-disable-next-line prefer-const + let initialData = { + selectedModel: selectedModelInfo, + sourceEnvironment: selectedSourceEnv.label, + targetEnvironment: selectedTargetEnv.label, + tableDiffData: tableDiffResult.value, + environments: environments, + } + + // Set up message listener for events from the webview + panel.webview.onDidReceiveMessage( + async request => { + if (!request || !request.key) { + return + } + const message: CallbackEvent = request + switch (message.key) { + case 'openFile': { + const workspaceFolders = getWorkspaceFolders() + if (workspaceFolders.length != 1) { + throw new Error('Only one workspace folder is supported') + } + const fullPath = vscode.Uri.parse(message.payload.uri) + const document = await vscode.workspace.openTextDocument(fullPath) + await vscode.window.showTextDocument(document) + break + } + case 'rpcRequest': { + const payload: RPCRequest = message.payload + const requestId = payload.requestId + switch (payload.method) { + case 'api_query': { + const response = await lspClient.call_custom_method( + 'sqlmesh/api', + payload.params, + ) + let responseCallback: CallbackEvent + if (isErr(response)) { + let errorMessage: string + switch (response.error.type) { + case 'generic': + errorMessage = response.error.message + break + case 'invalid_state': + errorMessage = `Invalid state: ${response.error.message}` + break + case 'sqlmesh_outdated': + errorMessage = `SQLMesh version issue: ${response.error.message}` + break + default: + errorMessage = 'Unknown error' + } + responseCallback = { + key: 'rpcResponse', + payload: { + requestId, + result: { + ok: false, + error: errorMessage, + }, + }, + } + } else { + responseCallback = { + key: 'rpcResponse', + payload: { + requestId, + result: response, + }, + } + } + await panel.webview.postMessage(responseCallback) + break + } + case 'get_active_file': { + const active_file = + vscode.window.activeTextEditor?.document.uri.fsPath + const responseCallback: CallbackEvent = { + key: 'rpcResponse', + payload: { + requestId, + result: { + fileUri: active_file, + }, + }, + } + await panel.webview.postMessage(responseCallback) + break + } + case 'get_selected_model': { + const responseCallback: CallbackEvent = { + key: 'rpcResponse', + payload: { + requestId, + result: { + ok: true, + value: { + selectedModel: initialData.selectedModel, + }, + }, + }, + } + await panel.webview.postMessage(responseCallback) + break + } + case 'get_initial_data': { + const responseCallback: CallbackEvent = { + key: 'rpcResponse', + payload: { + requestId, + result: { + ok: true, + value: { + selectedModel: initialData.selectedModel, + sourceEnvironment: initialData.sourceEnvironment, + targetEnvironment: initialData.targetEnvironment, + tableDiffData: initialData.tableDiffData, + environments: initialData.environments, + }, + }, + }, + } + await panel.webview.postMessage(responseCallback) + break + } + case 'get_all_models': { + const allModelsResult = await lspClient.call_custom_method( + 'sqlmesh/get_models', + {}, + ) + + let responseCallback: CallbackEvent + if (isErr(allModelsResult)) { + responseCallback = { + key: 'rpcResponse', + payload: { + requestId, + result: { + ok: false, + error: `Failed to get models: ${allModelsResult.error.message}`, + }, + }, + } + } else { + responseCallback = { + key: 'rpcResponse', + payload: { + requestId, + result: { + ok: true, + value: { + ok: true, + models: allModelsResult.value.models || [], + }, + }, + }, + } + } + await panel.webview.postMessage(responseCallback) + break + } + case 'set_selected_model': { + const modelInfo = payload.params?.model + if (modelInfo) { + initialData.selectedModel = modelInfo + // Update the panel title to reflect the new selection + panel.title = `SQLMesh Table Diff - ${modelInfo.name} (${initialData.sourceEnvironment} → ${initialData.targetEnvironment})` + } + + const responseCallback: CallbackEvent = { + key: 'rpcResponse', + payload: { + requestId, + result: { + ok: true, + value: { + ok: true, + selectedModel: initialData.selectedModel, + }, + }, + }, + } + await panel.webview.postMessage(responseCallback) + break + } + case 'get_environments': { + const responseCallback: CallbackEvent = { + key: 'rpcResponse', + payload: { + requestId, + result: { + ok: true, + value: { + ok: true, + environments: initialData.environments, + }, + }, + }, + } + await panel.webview.postMessage(responseCallback) + break + } + case 'run_table_diff': { + const { sourceModel, sourceEnvironment, targetEnvironment } = + payload.params || {} + + if (!sourceModel || !sourceEnvironment || !targetEnvironment) { + const responseCallback: CallbackEvent = { + key: 'rpcResponse', + payload: { + requestId, + result: { + ok: false, + error: + 'Missing required parameters: sourceModel, sourceEnvironment, or targetEnvironment', + }, + }, + } + await panel.webview.postMessage(responseCallback) + break + } + + const tableDiffResult = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'SQLMesh', + cancellable: false, + }, + async progress => { + progress.report({ + message: 'Calculating table differences...', + }) + + return await lspClient.call_custom_method('sqlmesh/api', { + method: 'GET', + endpoint: '/api/table_diff', + params: { + source: sourceEnvironment, + target: targetEnvironment, + model_or_snapshot: sourceModel, + }, + body: {}, + }) + }, + ) + + let responseCallback: CallbackEvent + if (isErr(tableDiffResult)) { + responseCallback = { + key: 'rpcResponse', + payload: { + requestId, + result: { + ok: false, + error: `Failed to run table diff: ${tableDiffResult.error.message}`, + }, + }, + } + } else { + responseCallback = { + key: 'rpcResponse', + payload: { + requestId, + result: { + ok: true, + value: { + ok: true, + data: tableDiffResult.value, + }, + }, + }, + } + } + await panel.webview.postMessage(responseCallback) + break + } + default: { + throw new Error(`Unhandled RPC method: ${payload.method}`) + } + } + break + } + default: + console.error( + 'Unhandled message type under queryRequest: ', + message, + ) + } + }, + undefined, + [], + ) + + // Set the HTML content + panel.webview.html = getHTML(panel.webview, extensionUri) + } +} + +function getHTML(webview: vscode.Webview, extensionUri: vscode.Uri): string { + const cssUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, 'src_react', 'assets', 'index.css'), + ) + const jsUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, 'src_react', 'assets', 'index.js'), + ) + const faviconUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, 'src_react', 'favicon.ico'), + ) + const logoUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, 'src_react', 'logo192.png'), + ) + + return ` + + + + + + + + + + SQLMesh Table Diff + + + + + +
    + + +` +} diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 0d0f6252cb..cfea8c2228 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -30,6 +30,7 @@ import { handleError } from './utilities/errors' import { selector, completionProvider } from './completion/completion' import { LineagePanel } from './webviews/lineagePanel' import { RenderedModelProvider } from './providers/renderedModelProvider' +import { showTableDiff } from './commands/tableDiff' import { controller as testController, @@ -151,6 +152,14 @@ export async function activate(context: vscode.ExtensionContext) { ), ) + // Register the table diff command + context.subscriptions.push( + vscode.commands.registerCommand( + 'sqlmesh.showTableDiff', + showTableDiff(lspClient, context.extensionUri), + ), + ) + // Re‑render model automatically when its source file is saved context.subscriptions.push( vscode.workspace.onDidSaveTextDocument(async document => { diff --git a/vscode/extension/src/lsp/custom.ts b/vscode/extension/src/lsp/custom.ts index 8113cd86ae..152f316cdf 100644 --- a/vscode/extension/src/lsp/custom.ts +++ b/vscode/extension/src/lsp/custom.ts @@ -35,6 +35,8 @@ export type CustomLSPMethods = | ListWorkspaceTests | ListDocumentTests | RunTest + | GetEnvironmentsMethod + | GetTableDiffModelsMethod interface AllModelsRequest { textDocument: { @@ -176,3 +178,44 @@ export interface RunTestResponse extends BaseResponse { success: boolean error_message?: string } + +export interface GetEnvironmentsMethod { + method: 'sqlmesh/get_environments' + request: GetEnvironmentsRequest + response: GetEnvironmentsResponse +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface GetEnvironmentsRequest {} + +interface GetEnvironmentsResponse extends BaseResponse { + environments: Record + pinned_environments: string[] + default_target_environment: string +} + +interface EnvironmentInfo { + name: string + snapshots: string[] + start_at: string + plan_id: string +} + +export interface GetTableDiffModelsMethod { + method: 'sqlmesh/get_models' + request: GetModelsRequest + response: GetModelsResponse +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface GetModelsRequest {} + +interface GetModelsResponse extends BaseResponse { + models: ModelInfo[] +} + +interface ModelInfo { + name: string + fqn: string + description: string | null | undefined +} diff --git a/vscode/extension/src/webviews/lineagePanel.ts b/vscode/extension/src/webviews/lineagePanel.ts index ee05112a64..0fd0be9c2a 100644 --- a/vscode/extension/src/webviews/lineagePanel.ts +++ b/vscode/extension/src/webviews/lineagePanel.ts @@ -200,6 +200,9 @@ export class LineagePanel implements WebviewViewProvider, Disposable { /> Create TanStack App - react + diff --git a/vscode/react/src/api/instance.ts b/vscode/react/src/api/instance.ts index 3627b273de..781a98ef88 100644 --- a/vscode/react/src/api/instance.ts +++ b/vscode/react/src/api/instance.ts @@ -39,7 +39,7 @@ export async function fetchAPI( _options?: Partial, ): Promise { const request = { - url: config.url, + endpoint: config.url, method: config.method, params: config.params, body: config.data, diff --git a/vscode/react/src/components/tablediff/Card.tsx b/vscode/react/src/components/tablediff/Card.tsx new file mode 100644 index 0000000000..d2f4d833c2 --- /dev/null +++ b/vscode/react/src/components/tablediff/Card.tsx @@ -0,0 +1,51 @@ +import { type ReactNode } from 'react' +import { twColors, twMerge } from './tailwind-utils' + +interface CardProps { + children: ReactNode + className?: string +} + +export function Card({ children, className }: CardProps) { + return ( +
    + {children} +
    + ) +} + +interface CardHeaderProps { + children: ReactNode + className?: string +} + +export function CardHeader({ children, className }: CardHeaderProps) { + return ( +
    + {children} +
    + ) +} + +interface CardContentProps { + children: ReactNode + className?: string +} + +export function CardContent({ children, className }: CardContentProps) { + return
    {children}
    +} diff --git a/vscode/react/src/components/tablediff/ColumnStatsSection.tsx b/vscode/react/src/components/tablediff/ColumnStatsSection.tsx new file mode 100644 index 0000000000..6b65318864 --- /dev/null +++ b/vscode/react/src/components/tablediff/ColumnStatsSection.tsx @@ -0,0 +1,381 @@ +import { useState } from 'react' +import { type TableDiffData, type SampleValue } from './types' +import { twColors, twMerge } from './tailwind-utils' +import { Card } from './Card' +import { + ArrowsUpDownIcon, + ArrowsRightLeftIcon, +} from '@heroicons/react/24/outline' + +interface ColumnStatsSectionProps { + columnStats: TableDiffData['row_diff']['column_stats'] +} + +interface StatHeaderProps { + stat: string +} + +const StatHeader = ({ stat }: StatHeaderProps) => ( +
    +) + +interface StatCellProps { + value: SampleValue +} + +const StatCell = ({ value }: StatCellProps) => ( + +) + +interface ColumnStatRowProps { + columnName: string + statsValue: TableDiffData['row_diff']['column_stats'][string] +} + +const ColumnStatRow = ({ columnName, statsValue }: ColumnStatRowProps) => ( + + + {statsValue && typeof statsValue === 'object' + ? Object.values(statsValue as Record).map( + (value, idx) => ( + + ), + ) + : [ + , + ]} + +) + +export function ColumnStatsSection({ columnStats }: ColumnStatsSectionProps) { + const [isVertical, setIsVertical] = useState(false) + + if (Object.keys(columnStats || {}).length === 0) { + return null + } + + // Get the first stats object to determine the column headers + const firstStatsValue = Object.values(columnStats)[0] + const statKeys = + firstStatsValue && typeof firstStatsValue === 'object' + ? Object.keys(firstStatsValue as Record) + : [] + + return ( +
    + {/* Statistics Table Card */} + + {/* Toggle Button */} +
    + +
    + +
    + {isVertical ? ( + // Vertical layout: Each stat as a separate row +
    + {stat} + + {typeof value === 'number' ? value.toFixed(1) : String(value)} +
    + {columnName} +
    + + + + {Object.keys(columnStats).map(col => ( + + ))} + + + + {statKeys.map(stat => ( + + + {Object.entries(columnStats).map(([col, statsValue]) => ( + )[stat] + : statsValue + } + /> + ))} + + ))} + +
    + Column + + {col} +
    + {stat} +
    + ) : ( + // Horizontal layout: Original layout + + + + + {statKeys.map(stat => ( + + ))} + + + + {Object.entries(columnStats).map(([col, statsValue]) => ( + + ))} + +
    + Column +
    + )} + + + + {/* Summary Cards */} +

    + {(() => { + let percentages: { column: string; percentage: number }[] = [] + + if (columnStats && typeof columnStats === 'object') { + if ( + 'pct_match' in columnStats && + typeof columnStats.pct_match === 'object' && + columnStats.pct_match !== null + ) { + const pctMatchData = columnStats.pct_match as Record< + string, + number + > + percentages = Object.entries(pctMatchData) + .map(([col, value]) => ({ + column: col, + percentage: Number(value) || 0, + })) + .filter(item => !isNaN(item.percentage)) + } else { + percentages = Object.entries(columnStats) + .map(([col, stats]) => { + if (!stats || typeof stats !== 'object') return null + + const statsObj = stats as Record + const pctMatch = + statsObj.pct_match || + statsObj.match_pct || + statsObj.percentage || + 0 + + return { column: col, percentage: Number(pctMatch) } + }) + .filter( + (item): item is { column: string; percentage: number } => + item !== null && + !isNaN(item.percentage) && + item.column !== 'pct_match', + ) + } + } + + const validPercentages = percentages.map(p => p.percentage) + const highest = + percentages.length > 0 + ? percentages.find( + p => p.percentage === Math.max(...validPercentages), + ) + : null + const lowest = + percentages.length > 0 + ? percentages.find( + p => p.percentage === Math.min(...validPercentages), + ) + : null + const average = + validPercentages.length > 0 + ? validPercentages.reduce((a, b) => a + b, 0) / + validPercentages.length + : 0 + + return ( + <> + +
    +
    +
    + {highest ? `${highest.percentage.toFixed(1)}%` : 'N/A'} +
    +
    + Highest Match +
    +
    + {highest ? highest.column : 'No data'} +
    +
    + + + +
    +
    +
    + {average > 0 ? `${average.toFixed(1)}%` : 'N/A'} +
    +
    + Average Match +
    +
    + Across {validPercentages.length} columns +
    +
    + + + +
    +
    +
    + {lowest ? `${lowest.percentage.toFixed(1)}%` : 'N/A'} +
    +
    + Lowest Match +
    +
    + {lowest ? lowest.column : 'No data'} +
    +
    + + + ) + })()} +
    +
    + ) +} diff --git a/vscode/react/src/components/tablediff/ContentSections.tsx b/vscode/react/src/components/tablediff/ContentSections.tsx new file mode 100644 index 0000000000..dee8019e0c --- /dev/null +++ b/vscode/react/src/components/tablediff/ContentSections.tsx @@ -0,0 +1,78 @@ +import { SectionCard } from './SectionCard' +import { SchemaDiffSection } from './SchemaDiffSection' +import { RowStatsSection } from './RowStatsSection' +import { ColumnStatsSection } from './ColumnStatsSection' +import { SampleDataSection } from './SampleDataSection' +import { usePersistedState } from './hooks' +import type { TableDiffData, ExpandedSections } from './types' + +interface ContentSectionsProps { + data: TableDiffData +} + +export function ContentSections({ data }: ContentSectionsProps) { + const [expanded, setExpanded] = usePersistedState( + 'tableDiffExpanded', + { + schema: true, + rows: true, + columnStats: false, + sampleData: false, + }, + ) + + const toggle = (section: keyof ExpandedSections) => { + setExpanded(prev => ({ + ...prev, + [section]: !prev[section], + })) + } + + const { schema_diff, row_diff } = data + + return ( +
    + {/* Schema Changes */} + toggle('schema')} + > + + + + {/* Row Statistics */} + toggle('rows')} + > + + + + {/* Column Statistics */} + toggle('columnStats')} + > + + + + {/* Sample Data */} + {row_diff.processed_sample_data && ( + toggle('sampleData')} + > + + + )} +
    + ) +} diff --git a/vscode/react/src/components/tablediff/HeaderCard.tsx b/vscode/react/src/components/tablediff/HeaderCard.tsx new file mode 100644 index 0000000000..5c3b3872a6 --- /dev/null +++ b/vscode/react/src/components/tablediff/HeaderCard.tsx @@ -0,0 +1,201 @@ +import { Card, CardContent } from './Card' +import { DiffConfig } from './Legend' +import { twColors, twMerge } from './tailwind-utils' +import type { TableDiffData } from './types' + +interface HeaderCardProps { + schemaDiff: TableDiffData['schema_diff'] + rowDiff: TableDiffData['row_diff'] + limit: number + whereClause: string + onColumns: string + on: string[][] | undefined + where: string | undefined + isRerunning: boolean + onLimitChange: (limit: number) => void + onWhereClauseChange: (where: string) => void + onOnColumnsChange: (on: string) => void + onRerun: () => void + hasChanges: boolean +} + +export function HeaderCard({ + schemaDiff, + rowDiff, + limit, + whereClause, + onColumns, + on, + where, + isRerunning, + onLimitChange, + onWhereClauseChange, + onOnColumnsChange, + onRerun, + hasChanges, +}: HeaderCardProps) { + const formatPercentage = (v: number) => `${v.toFixed(1)}%` + const formatCount = (v: number) => v.toLocaleString() + + return ( + + +
    + + Source: + + + {schemaDiff.source} + + + Target: + + + {schemaDiff.target} + +
    +
    +
    + Source rows: + + {formatCount(rowDiff.source_count)} + +
    +
    + Target rows: + + {formatCount(rowDiff.target_count)} + +
    +
    + Change: + 0 + ? twColors.textSuccess500 + : rowDiff.count_pct_change < 0 + ? twColors.textDanger500 + : twColors.textMuted, + )} + > + {formatPercentage(rowDiff.count_pct_change)} + +
    +
    +
    +
    +
    + + + onLimitChange(Math.max(1, parseInt(e.target.value) || 1)) + } + className={twMerge( + 'w-20 px-2 py-1 text-sm rounded border', + 'bg-[var(--vscode-input-background)]', + 'border-[var(--vscode-input-border)]', + 'text-[var(--vscode-input-foreground)]', + 'focus:outline-none focus:ring-1 focus:ring-[var(--vscode-focusBorder)]', + )} + min="1" + max="10000" + disabled={isRerunning} + /> +
    +
    + + onWhereClauseChange(e.target.value)} + placeholder="e.g. created_at > '2024-01-01'" + className={twMerge( + 'px-2 py-1 text-sm rounded border', + 'bg-[var(--vscode-input-background)]', + 'border-[var(--vscode-input-border)]', + 'text-[var(--vscode-input-foreground)]', + 'placeholder:text-[var(--vscode-input-placeholderForeground)]', + 'focus:outline-none focus:ring-1 focus:ring-[var(--vscode-focusBorder)]', + )} + disabled={isRerunning} + /> +
    +
    + + onOnColumnsChange(e.target.value)} + placeholder="e.g. s.id = t.id AND s.date = t.date" + className={twMerge( + 'px-2 py-1 text-sm rounded border', + 'bg-[var(--vscode-input-background)]', + 'border-[var(--vscode-input-border)]', + 'text-[var(--vscode-input-foreground)]', + 'placeholder:text-[var(--vscode-input-placeholderForeground)]', + 'focus:outline-none focus:ring-1 focus:ring-[var(--vscode-focusBorder)]', + )} + disabled={isRerunning} + /> +
    + +
    +
    + {on && ( + + )} +
    +
    +
    +
    + ) +} diff --git a/vscode/react/src/components/tablediff/Legend.tsx b/vscode/react/src/components/tablediff/Legend.tsx new file mode 100644 index 0000000000..274db60625 --- /dev/null +++ b/vscode/react/src/components/tablediff/Legend.tsx @@ -0,0 +1,59 @@ +import { twColors, twMerge } from './tailwind-utils' + +interface DiffConfigProps { + on: string[] | string[][] + limit?: number + where?: string +} + +interface ConfigItemProps { + label: string + value: string | number +} + +function ConfigItem({ label, value }: ConfigItemProps) { + return ( +
    + + {label}: + + + {value} + +
    + ) +} + +export function DiffConfig({ on, limit, where }: DiffConfigProps) { + // Handle the grain (join keys) + const grainColumns = Array.isArray(on[0]) + ? on.flat().filter((col, index, arr) => arr.indexOf(col) === index) // Remove duplicates from nested array + : (on as string[]) + + return ( +
    + + {limit && ( + + )} + {where && ( + + )} +
    + ) +} diff --git a/vscode/react/src/components/tablediff/RerunController.tsx b/vscode/react/src/components/tablediff/RerunController.tsx new file mode 100644 index 0000000000..6a609ecaaa --- /dev/null +++ b/vscode/react/src/components/tablediff/RerunController.tsx @@ -0,0 +1,168 @@ +import { useState, useEffect } from 'react' +import { callRpc } from '../../utils/rpc' +import type { TableDiffData, TableDiffParams } from './types' + +interface RerunControllerProps { + data: TableDiffData + onDataUpdate?: (data: TableDiffData) => void + children: (props: { + limit: number + whereClause: string + onColumns: string + isRerunning: boolean + hasChanges: boolean + setLimit: (limit: number) => void + setWhereClause: (where: string) => void + setOnColumns: (on: string) => void + handleRerun: () => void + }) => React.ReactNode +} + +export function RerunController({ + data, + onDataUpdate, + children, +}: RerunControllerProps) { + const [isRerunning, setIsRerunning] = useState(false) + const [limit, setLimit] = useState(data.limit || 20) + const [whereClause, setWhereClause] = useState(data.where || '') + const [onColumns, setOnColumns] = useState( + data.on?.map(([sCol, tCol]) => `s.${sCol} = t.${tCol}`).join(' AND ') || '', + ) + + // Update state when data changes + useEffect(() => { + setLimit(data.limit || 20) + setWhereClause(data.where || '') + setOnColumns( + data.on?.map(([sCol, tCol]) => `s.${sCol} = t.${tCol}`).join(' AND ') || + '', + ) + }, [data.limit, data.where, data.on]) + + // Helper function to parse on columns back to array format + const parseOnColumns = (onString: string): string[][] => { + if (!onString.trim()) return [] + + // Parse "s.id = t.id AND s.date = t.date" back to [["id", "id"], ["date", "date"]] + const conditions = onString.split(' AND ') + return conditions.map(condition => { + const match = condition.trim().match(/^s\.(\w+)\s*=\s*t\.(\w+)$/) + if (match) { + return [match[1], match[2]] + } + // Fallback for simple format + return [condition.trim(), condition.trim()] + }) + } + + const hasChanges = + limit !== (data.limit || 20) || + whereClause !== (data.where || '') || + onColumns !== + (data.on?.map(([sCol, tCol]) => `s.${sCol} = t.${tCol}`).join(' AND ') || + '') + + const handleRerun = async () => { + if (isRerunning || !hasChanges) return + + setIsRerunning(true) + try { + // Get the initial data to extract the model name and environment names + const initialDataResult = await callRpc('get_initial_data', {}) + if (!initialDataResult.ok || !initialDataResult.value?.selectedModel) { + console.error('Failed to get initial data for rerun') + return + } + + const params: TableDiffParams = { + source: initialDataResult.value.sourceEnvironment || 'prod', + target: initialDataResult.value.targetEnvironment || 'dev', + model_or_snapshot: initialDataResult.value.selectedModel.name, + limit: Math.min(Math.max(1, limit), 10000), // Ensure limit is within bounds + ...(whereClause.trim() && { where: whereClause.trim() }), + ...(onColumns.trim() && { on: onColumns.trim() }), + } + + console.log('Rerunning table diff with params:', params) + + try { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Request timeout')), 30000) // 30 second timeout + }) + + const apiPromise = callRpc('api_query', { + method: 'GET', + endpoint: '/api/table_diff', + params: params, + body: {}, + }) + + const result = (await Promise.race([apiPromise, timeoutPromise])) as any + + console.log('Table diff result:', result) + + if (result.ok && result.value) { + let newData: TableDiffData + if (result.value.data) { + newData = { + ...result.value.data, + limit, + where: whereClause, + on: parseOnColumns(onColumns), + } + } else { + newData = { + ...result.value, + limit, + where: whereClause, + on: parseOnColumns(onColumns), + } + } + + console.log('Updating table diff data:', newData) + onDataUpdate?.(newData) + } else { + console.error('API call failed:', result.error) + // Try to extract meaningful error message + let errorMessage = 'Unknown error' + if (typeof result.error === 'string') { + try { + const parsed = JSON.parse(result.error) + errorMessage = parsed.message || parsed.code || result.error + } catch { + errorMessage = result.error + } + } + console.error('Processed error message:', errorMessage) + setIsRerunning(false) + return + } + } catch (apiError) { + console.error('API call threw exception:', apiError) + setIsRerunning(false) + return + } + } catch (error) { + console.error('Error rerunning table diff:', error) + } finally { + setIsRerunning(false) + } + } + + return ( + <> + {children({ + limit, + whereClause, + onColumns, + isRerunning, + hasChanges, + setLimit, + setWhereClause, + setOnColumns, + handleRerun, + })} + + ) +} diff --git a/vscode/react/src/components/tablediff/RowStatsSection.tsx b/vscode/react/src/components/tablediff/RowStatsSection.tsx new file mode 100644 index 0000000000..076f7d95fe --- /dev/null +++ b/vscode/react/src/components/tablediff/RowStatsSection.tsx @@ -0,0 +1,102 @@ +import { type TableDiffData } from './types' +import { twColors, twMerge } from './tailwind-utils' +import { Card, CardContent } from './Card' + +interface RowStatsSectionProps { + rowDiff: TableDiffData['row_diff'] +} + +export function RowStatsSection({ rowDiff }: RowStatsSectionProps) { + const formatPercentage = (v: number) => `${(v * 100).toFixed(1)}%` + const formatCount = (v: number) => v.toLocaleString() + + const fullMatchCount = Math.round(rowDiff.stats.full_match_count || 0) + const joinCount = Math.round(rowDiff.stats.join_count || 0) + const partialMatchCount = joinCount - fullMatchCount + const sOnlyCount = Math.round(rowDiff.stats.s_only_count || 0) + const tOnlyCount = Math.round(rowDiff.stats.t_only_count || 0) + const totalRows = rowDiff.source_count + rowDiff.target_count + const fullMatchPct = totalRows > 0 ? (2 * fullMatchCount) / totalRows : 0 + + return ( +
    + {/* Full Match Card */} + +
    + +
    + {formatCount(fullMatchCount)} +
    +
    + Full Matches +
    +
    + {formatPercentage(fullMatchPct)} +
    +
    + + + {/* Partial Match Card */} + +
    + +
    + {formatCount(partialMatchCount)} +
    +
    + Partial Matches +
    +
    + {formatPercentage(partialMatchCount / totalRows)} +
    +
    + + + {/* Source Only Card */} + +
    + +
    + {formatCount(sOnlyCount)} +
    +
    + Source Only +
    +
    + {formatPercentage(sOnlyCount / totalRows)} +
    +
    + + + {/* Target Only Card */} + +
    + +
    + {formatCount(tOnlyCount)} +
    +
    + Target Only +
    +
    + {formatPercentage(tOnlyCount / totalRows)} +
    +
    + +
    + ) +} diff --git a/vscode/react/src/components/tablediff/SampleDataSection.tsx b/vscode/react/src/components/tablediff/SampleDataSection.tsx new file mode 100644 index 0000000000..00a7f07269 --- /dev/null +++ b/vscode/react/src/components/tablediff/SampleDataSection.tsx @@ -0,0 +1,440 @@ +import { useMemo } from 'react' +import { + type TableDiffData, + type SampleRow, + type SampleValue, + formatCellValue, +} from './types' +import { twColors, twMerge } from './tailwind-utils' + +interface SampleDataSectionProps { + rowDiff: TableDiffData['row_diff'] +} + +interface TableHeaderCellProps { + columnKey: string + sourceName?: SampleValue + targetName?: SampleValue +} + +const TableHeaderCell = ({ + columnKey, + sourceName, + targetName, +}: TableHeaderCellProps) => { + const isSource = columnKey === sourceName + const isTarget = columnKey === targetName + + return ( + + {columnKey} + + ) +} + +interface DiffTableCellProps { + columnKey: string + value: SampleValue + sourceName?: SampleValue + targetName?: SampleValue + decimals?: number +} + +const DiffTableCell = ({ + columnKey, + value, + sourceName, + targetName, + decimals = 3, +}: DiffTableCellProps) => { + const isSource = columnKey === sourceName + const isTarget = columnKey === targetName + + return ( + + {formatCellValue(value, decimals)} + + ) +} + +interface DiffTableRowProps { + row: SampleRow + sourceName?: SampleValue + targetName?: SampleValue + decimals?: number +} + +const DiffTableRow = ({ + row, + sourceName, + targetName, + decimals, +}: DiffTableRowProps) => ( + + {Object.entries(row) + .filter(([key]) => !key.startsWith('__')) + .map(([key, cell]) => ( + + ))} + +) + +interface SimpleTableCellProps { + value: SampleValue + colorClass: string + decimals?: number +} + +const SimpleTableCell = ({ + value, + colorClass, + decimals = 3, +}: SimpleTableCellProps) => ( + + {formatCellValue(value, decimals)} + +) + +interface SimpleTableRowProps { + row: SampleRow + colorClass: string + borderColorClass: string + decimals?: number +} + +const SimpleTableRow = ({ + row, + colorClass, + borderColorClass, + decimals, +}: SimpleTableRowProps) => ( + + {Object.values(row).map((cell, cellIdx) => ( + + ))} + +) + +interface ColumnDifferenceGroupProps { + columnName: string + rows: SampleRow[] + decimals: number +} + +const ColumnDifferenceGroup = ({ + columnName, + rows, + decimals, +}: ColumnDifferenceGroupProps) => { + if (!rows || rows.length === 0) return null + + const sourceName = rows[0].__source_name__ + const targetName = rows[0].__target_name__ + + return ( +
    +
    + Column: {columnName} + + {rows.length} difference{rows.length > 1 ? 's' : ''} + +
    +
    +
    + + + + {Object.keys(rows[0] || {}) + .filter(key => !key.startsWith('__')) + .map(key => ( + + ))} + + + + {rows.slice(0, 10).map((row, rowIdx) => ( + + ))} + +
    +
    + {rows.length > 10 && ( +

    + Showing first 10 of {rows.length} differing rows +

    + )} +
    +
    + ) +} + +export function SampleDataSection({ rowDiff }: SampleDataSectionProps) { + const { processed_sample_data, decimals = 3 } = rowDiff + + if (!processed_sample_data) { + return ( +
    +

    + No processed sample data available +

    +
    + ) + } + + const { column_differences, source_only, target_only } = processed_sample_data + + // Group column differences by column name + const groupedDifferences = useMemo(() => { + const groups: Record = {} + + column_differences.forEach((row: SampleRow) => { + const columnName = String(row.__column_name__ || 'unknown') + if (!groups[columnName]) { + groups[columnName] = [] + } + groups[columnName].push(row) + }) + + return groups + }, [column_differences]) + + return ( +
    + {/* COMMON ROWS diff */} +
    +

    + Common Rows +

    + {Object.keys(groupedDifferences).length > 0 ? ( +
    + {Object.entries(groupedDifferences).map(([columnName, rows]) => ( + + ))} +
    + ) : ( +

    + ✓ All joined rows match +

    + )} +
    + + {/* SOURCE ONLY & TARGET ONLY tables */} + {source_only && source_only.length > 0 && ( +
    +

    + Source Only Rows +

    +
    +
    + + + + {Object.keys(source_only[0] || {}).map(col => ( + + ))} + + + + {source_only.slice(0, 10).map((row, rowIdx) => ( + + ))} + +
    + {col} +
    +
    + {source_only.length > 10 && ( +
    + Showing first 10 of {source_only.length} rows +
    + )} +
    +
    + )} + + {target_only && target_only.length > 0 && ( +
    +

    + Target Only Rows +

    +
    +
    + + + + {Object.keys(target_only[0] || {}).map(col => ( + + ))} + + + + {target_only.slice(0, 10).map((row, rowIdx) => ( + + ))} + +
    + {col} +
    +
    + {target_only.length > 10 && ( +
    + Showing first 10 of {target_only.length} rows +
    + )} +
    +
    + )} +
    + ) +} diff --git a/vscode/react/src/components/tablediff/SchemaDiffSection.tsx b/vscode/react/src/components/tablediff/SchemaDiffSection.tsx new file mode 100644 index 0000000000..274ac1979c --- /dev/null +++ b/vscode/react/src/components/tablediff/SchemaDiffSection.tsx @@ -0,0 +1,122 @@ +import { useMemo } from 'react' +import { type TableDiffData } from './types' +import { twColors, twMerge } from './tailwind-utils' + +interface SchemaDiffSectionProps { + schemaDiff: TableDiffData['schema_diff'] +} + +interface SchemaChangeItemProps { + column: string + type: string + changeType: 'added' | 'removed' | 'modified' +} + +const SchemaChangeItem = ({ + column, + type, + changeType, +}: SchemaChangeItemProps) => { + const styleMap = { + added: { + bgClass: twColors.bgSuccess10, + borderClass: 'border-l-4 ' + twColors.borderSuccess500, + textClass: twColors.textSuccess500, + symbol: '+', + }, + removed: { + bgClass: twColors.bgDanger10, + borderClass: 'border-l-4 ' + twColors.borderDanger500, + textClass: twColors.textDanger500, + symbol: '-', + }, + modified: { + bgClass: twColors.bgPrimary10, + borderClass: 'border-l-4 ' + twColors.borderPrimary, + textClass: twColors.textPrimary, + symbol: '~', + }, + } + + const { bgClass, borderClass, textClass, symbol } = styleMap[changeType] + + return ( +
    + + {symbol} + + + {column} + + : + + {type} + +
    + ) +} + +export function SchemaDiffSection({ schemaDiff }: SchemaDiffSectionProps) { + const schemaHasChanges = useMemo(() => { + return ( + Object.keys(schemaDiff.added || {}).length > 0 || + Object.keys(schemaDiff.removed || {}).length > 0 || + Object.keys(schemaDiff.modified || {}).length > 0 + ) + }, [schemaDiff]) + + return ( +
    + {!schemaHasChanges ? ( +
    + ✓ Schemas are identical +
    + ) : ( + <> + {Object.entries(schemaDiff.added).map(([col, type]) => ( + + ))} + {Object.entries(schemaDiff.removed).map(([col, type]) => ( + + ))} + {Object.entries(schemaDiff.modified).map(([col, type]) => ( + + ))} + + )} +
    + ) +} diff --git a/vscode/react/src/components/tablediff/SectionCard.tsx b/vscode/react/src/components/tablediff/SectionCard.tsx new file mode 100644 index 0000000000..af9d42ecd2 --- /dev/null +++ b/vscode/react/src/components/tablediff/SectionCard.tsx @@ -0,0 +1,63 @@ +import { type ReactNode } from 'react' +import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline' +import { Card, CardHeader, CardContent } from './Card' +import { twColors, twMerge } from './tailwind-utils' + +interface Props { + id: string + title: string + children: ReactNode + expanded: boolean + onToggle: () => void + badge?: { text: string; color?: string } +} + +export function SectionCard({ + title, + children, + expanded, + onToggle, + badge, +}: Props) { + return ( + + + + +
    + {children} +
    +
    + ) +} diff --git a/vscode/react/src/components/tablediff/SectionToggle.tsx b/vscode/react/src/components/tablediff/SectionToggle.tsx new file mode 100644 index 0000000000..4066db22b7 --- /dev/null +++ b/vscode/react/src/components/tablediff/SectionToggle.tsx @@ -0,0 +1,47 @@ +import { type ReactNode } from 'react' +import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline' +import { type ExpandedSections } from './types' +import { twColors, twMerge } from './tailwind-utils' + +interface SectionToggleProps { + id: keyof ExpandedSections + title: string + expanded: boolean + onToggle(): void + children: ReactNode +} + +export function SectionToggle({ + title, + expanded, + onToggle, + children, +}: SectionToggleProps) { + return ( +
    + +
    + {children} +
    +
    + ) +} diff --git a/vscode/react/src/components/tablediff/TableDiff.tsx b/vscode/react/src/components/tablediff/TableDiff.tsx new file mode 100644 index 0000000000..8374f1bc73 --- /dev/null +++ b/vscode/react/src/components/tablediff/TableDiff.tsx @@ -0,0 +1,148 @@ +import { useState, useEffect } from 'react' +import LoadingStatus from '../loading/LoadingStatus' +import { TableDiffResults } from './TableDiffResults' +import { callRpc } from '../../utils/rpc' +import { type TableDiffData } from './types' + +interface ModelInfo { + name: string + fqn: string + description?: string +} + +export function TableDiff() { + const [selectedModel, setSelectedModel] = useState(null) + const [sourceEnvironment, setSourceEnvironment] = useState('prod') + const [targetEnvironment, setTargetEnvironment] = useState('dev') + const [tableDiffData, setTableDiffData] = useState(null) + const [isLoadingDiff] = useState(false) + const [diffError] = useState(null) + const [hasInitialData, setHasInitialData] = useState(false) + + const handleDataUpdate = (newData: TableDiffData) => { + setTableDiffData(newData) + } + + // Load initial data on mount + useEffect(() => { + const loadInitialData = async () => { + try { + // Try to get initial data first (pre-selected from VSCode) + const initialDataResult = await callRpc('get_initial_data', {}) + if (initialDataResult.ok && initialDataResult.value) { + const data = initialDataResult.value + + // Set all initial state from pre-selected data + if (data.selectedModel) { + setSelectedModel(data.selectedModel) + } + if (data.sourceEnvironment) { + setSourceEnvironment(data.sourceEnvironment) + } + if (data.targetEnvironment) { + setTargetEnvironment(data.targetEnvironment) + } + + // Always mark as having initial data if we got a response from VSCode + setHasInitialData(true) + + if (data.tableDiffData) { + // Handle different response structures + let diffData: TableDiffData | null = null + + if (data.tableDiffData.data !== undefined) { + // Response has a nested data field + diffData = data.tableDiffData.data + } else if ( + data.tableDiffData && + typeof data.tableDiffData === 'object' && + 'schema_diff' in data.tableDiffData && + 'row_diff' in data.tableDiffData + ) { + // Response is the data directly + diffData = data.tableDiffData as TableDiffData + } + + setTableDiffData(diffData) + } + } + } catch (error) { + console.error('Error loading initial data:', error) + } + } + + loadInitialData() + }, []) + + // If we're still loading, show loading state + if (isLoadingDiff) { + return ( +
    + Running table diff... +
    + ) + } + + // If we have initial data, handle all possible states + if (hasInitialData) { + // Show results if we have them + if (tableDiffData) { + return ( +
    + +
    + ) + } + + // Show error if there was one + if (diffError) { + return ( +
    +
    +
    + Error running table diff +
    +
    {diffError}
    +
    +
    + ) + } + + // If we have initial data but no results and no error, show appropriate message + return ( +
    +
    +
    No differences found
    +
    + The selected model "{selectedModel?.name}" has no differences + between {sourceEnvironment}{' '} + and {targetEnvironment}{' '} + environments. +
    +
    +
    + ) + } + + // If we don't have initial data yet, show loading + if (!hasInitialData) { + return ( +
    + Loading... +
    + ) + } + + // This should never happen with the new flow + return ( +
    +
    +
    Unexpected state
    +
    Please try running the table diff command again.
    +
    +
    + ) +} diff --git a/vscode/react/src/components/tablediff/TableDiffResults.tsx b/vscode/react/src/components/tablediff/TableDiffResults.tsx new file mode 100644 index 0000000000..45e92b65d1 --- /dev/null +++ b/vscode/react/src/components/tablediff/TableDiffResults.tsx @@ -0,0 +1,64 @@ +import { HeaderCard } from './HeaderCard' +import { ContentSections } from './ContentSections' +import { RerunController } from './RerunController' +import { type TableDiffData } from './types' +import { twColors, twMerge } from './tailwind-utils' + +interface Props { + data: TableDiffData + onDataUpdate?: (data: TableDiffData) => void +} + +export function TableDiffResults({ data, onDataUpdate }: Props) { + if (!data) + return ( +
    + No data available +
    + ) + + return ( + + {({ + limit, + whereClause, + onColumns, + isRerunning, + hasChanges, + setLimit, + setWhereClause, + setOnColumns, + handleRerun, + }) => ( +
    + + +
    + )} +
    + ) +} diff --git a/vscode/react/src/components/tablediff/hooks.ts b/vscode/react/src/components/tablediff/hooks.ts new file mode 100644 index 0000000000..803b0b8a16 --- /dev/null +++ b/vscode/react/src/components/tablediff/hooks.ts @@ -0,0 +1,29 @@ +import { useState, useEffect } from 'react' + +/** + * Persist state in localStorage so the user's expand / collapse choices + * survive reloads and navigation in VS Code's WebView. + */ +export function usePersistedState( + key: string, + initial: T, +): [T, React.Dispatch>] { + const [state, setState] = useState(() => { + try { + const stored = localStorage.getItem(key) + return stored ? (JSON.parse(stored) as T) : initial + } catch { + return initial + } + }) + + useEffect(() => { + try { + localStorage.setItem(key, JSON.stringify(state)) + } catch { + /* noop */ + } + }, [key, state]) + + return [state, setState] +} diff --git a/vscode/react/src/components/tablediff/index.ts b/vscode/react/src/components/tablediff/index.ts new file mode 100644 index 0000000000..a5b7ea2776 --- /dev/null +++ b/vscode/react/src/components/tablediff/index.ts @@ -0,0 +1,15 @@ +// Main components +export { TableDiff } from './TableDiff' +export { TableDiffResults } from './TableDiffResults' + +// Section components +export { SectionToggle } from './SectionToggle' +export { SchemaDiffSection } from './SchemaDiffSection' +export { RowStatsSection } from './RowStatsSection' +export { ColumnStatsSection } from './ColumnStatsSection' +export { SampleDataSection } from './SampleDataSection' + +// Utilities +export { usePersistedState } from './hooks' +export { twColors, twMerge } from './tailwind-utils' +export * from './types' diff --git a/vscode/react/src/components/tablediff/tailwind-utils.ts b/vscode/react/src/components/tablediff/tailwind-utils.ts new file mode 100644 index 0000000000..182dc69c28 --- /dev/null +++ b/vscode/react/src/components/tablediff/tailwind-utils.ts @@ -0,0 +1,86 @@ +// Tailwind utility classes with CSS variables +export const twColors = { + // Text colors + textForeground: 'text-[var(--vscode-editor-foreground)]', + textInfo: 'text-[var(--vscode-testing-iconUnset)]', + textSuccess: 'text-[var(--vscode-testing-iconPassed)]', + textError: 'text-[var(--vscode-testing-iconFailed)]', + textWarning: 'text-[var(--vscode-testing-iconQueued)]', + textMuted: 'text-[var(--vscode-descriptionForeground)]', + textAccent: 'text-[var(--vscode-textLink-foreground)]', + textAdded: 'text-[var(--vscode-diffEditor-insertedTextForeground)]', + textRemoved: 'text-[var(--vscode-diffEditor-removedTextForeground)]', + textModified: 'text-[var(--vscode-diffEditor-modifiedTextForeground)]', + + // Source and target environment colors + textSource: 'text-[var(--vscode-debugIcon-continueForeground)]', + textTarget: 'text-[var(--vscode-debugIcon-startForeground)]', + textClass: 'text-[var(--vscode-symbolIcon-classForeground)]', + bgSource: 'bg-[var(--vscode-debugIcon-continueForeground)]', + bgTarget: 'bg-[var(--vscode-debugIcon-startForeground)]', + bgClass: 'bg-[var(--vscode-symbolIcon-classForeground)]', + borderSource: 'border-[var(--vscode-debugIcon-continueForeground)]', + borderTarget: 'border-[var(--vscode-debugIcon-startForeground)]', + borderClass: 'border-[var(--vscode-symbolIcon-classForeground)]', + + // Background colors + bgEditor: 'bg-[var(--vscode-editor-background)]', + bgInput: 'bg-[var(--vscode-input-background)]', + bgHover: 'hover:bg-[var(--vscode-list-hoverBackground)]', + bgInactiveSelection: 'bg-[var(--vscode-editor-inactiveSelectionBackground)]', + bgAdded: 'bg-[var(--vscode-diffEditor-insertedTextBackground)]', + bgRemoved: 'bg-[var(--vscode-diffEditor-removedTextBackground)]', + bgModified: 'bg-[var(--vscode-diffEditor-modifiedTextBackground)]', + bgTestSuccess: 'bg-[var(--vscode-testing-iconPassed)]', + bgError: 'bg-[var(--vscode-testing-iconFailed)]', + bgWarning: 'bg-[var(--vscode-testing-iconQueued)]', + bgInfo: 'bg-[var(--vscode-testing-iconUnset)]', + + // Border colors + borderPanel: 'border-[var(--vscode-panel-border)]', + borderInfo: 'border-[var(--vscode-testing-iconUnset)]', + borderSuccess: 'border-[var(--vscode-testing-iconPassed)]', + borderError: 'border-[var(--vscode-diffEditor-removedTextForeground)]', + borderWarning: 'border-[var(--vscode-diffEditor-modifiedTextForeground)]', + borderAdded: 'border-[var(--vscode-diffEditor-insertedTextForeground)]', + borderRemoved: 'border-[var(--vscode-diffEditor-removedTextForeground)]', + borderModified: 'border-[var(--vscode-diffEditor-modifiedTextForeground)]', + + //These colors are similar to web UI + // Primary (blue) + textPrimary: 'text-[#3b82f6]', + bgPrimary10: 'bg-[#3b82f6]/10', + bgPrimary: 'bg-[#3b82f6]', + borderPrimary: 'border-[#3b82f6]', + + // Success (green) + textSuccess500: 'text-[#10b981]', + bgSuccess10: 'bg-[#10b981]/10', + bgSuccess: 'bg-[#10b981]', + borderSuccess500: 'border-[#10b981]', + + // Danger (red) + textDanger500: 'text-[#ef4444]', + bgDanger5: 'bg-[#ef4444]/5', + bgDanger10: 'bg-[#ef4444]/10', + bgDanger: 'bg-[#ef4444]', + borderDanger500: 'border-[#ef4444]', + + // Brand (purple) + textBrand: 'text-[#8b5cf6]', + bgBrand10: 'bg-[#8b5cf6]/10', + bgBrand: 'bg-[#8b5cf6]', + borderBrand500: 'border-[#8b5cf6]', + + // Neutral + bgNeutral5: 'bg-[var(--vscode-editor-inactiveSelectionBackground)]', + bgNeutral10: 'bg-[var(--vscode-list-hoverBackground)]', + textNeutral500: 'text-[var(--vscode-descriptionForeground)]', + textNeutral600: 'text-[var(--vscode-editor-foreground)]', + borderNeutral100: 'border-[var(--vscode-panel-border)]', +} + +// Helper function to combine conditional classes +export function twMerge(...classes: (string | false | undefined | null)[]) { + return classes.filter(Boolean).join(' ') +} diff --git a/vscode/react/src/components/tablediff/types.ts b/vscode/react/src/components/tablediff/types.ts new file mode 100644 index 0000000000..271828476b --- /dev/null +++ b/vscode/react/src/components/tablediff/types.ts @@ -0,0 +1,84 @@ +// Type for data values in samples - can be strings, numbers, booleans, or null +export type SampleValue = string | number | boolean | null + +// Type for row data in samples +export type SampleRow = Record + +// Type for column statistics +export type ColumnStats = Record + +export interface TableDiffData { + schema_diff: { + source: string + target: string + source_schema: Record + target_schema: Record + added: Record + removed: Record + modified: Record + } + row_diff: { + source: string + target: string + stats: Record + sample: Record + joined_sample: Record + s_sample: Record + t_sample: Record + column_stats: ColumnStats + source_count: number + target_count: number + count_pct_change: number + decimals: number + processed_sample_data?: { + column_differences: SampleRow[] + source_only: SampleRow[] + target_only: SampleRow[] + } + } + on: string[][] + limit?: number + where?: string +} + +export interface TableDiffParams { + source: string + target: string + model_or_snapshot: string + on?: string + where?: string + temp_schema?: string + limit?: number +} + +export interface ExpandedSections { + schema: boolean + rows: boolean + columnStats: boolean + sampleData: boolean +} + +export const themeColors = { + success: 'var(--vscode-testing-iconPassed, #22c55e)', + warning: 'var(--vscode-testing-iconQueued, #f59e0b)', + error: 'var(--vscode-testing-iconFailed, #ef4444)', + info: 'var(--vscode-testing-iconUnset, #3b82f6)', + addedText: 'var(--vscode-diffEditor-insertedTextForeground, #22c55e)', + removedText: 'var(--vscode-diffEditor-removedTextForeground, #ef4444)', + modifiedText: 'var(--vscode-diffEditor-modifiedTextForeground, #f59e0b)', + muted: 'var(--vscode-descriptionForeground)', + accent: 'var(--vscode-textLink-foreground)', + border: 'var(--vscode-panel-border)', +} + +// Helper utilities +export function cn(...classes: (string | false | undefined)[]) { + return classes.filter(Boolean).join(' ') +} + +export const formatCellValue = (cell: SampleValue, decimals = 3): string => { + if (cell == null) return 'null' + if (typeof cell === 'number') + return cell % 1 === 0 ? cell.toString() : cell.toFixed(decimals) + return String(cell) +} diff --git a/vscode/react/src/main.tsx b/vscode/react/src/main.tsx index cf3b691223..5e24fc648f 100644 --- a/vscode/react/src/main.tsx +++ b/vscode/react/src/main.tsx @@ -1,38 +1,37 @@ import { StrictMode } from 'react' import ReactDOM from 'react-dom/client' -import { RouterProvider, createRouter } from '@tanstack/react-router' - -// Import the generated route tree -import { routeTree } from './routeTree.gen' - import reportWebVitals from './reportWebVitals.ts' import { EventBusProvider } from './hooks/eventBus.tsx' +import { TableDiffPage } from './pages/tablediff.tsx' +import { LineagePage } from './pages/lineage.tsx' -// Create a new router instance -const router = createRouter({ - routeTree, - context: {}, - defaultPreload: 'intent', - scrollRestoration: true, - defaultStructuralSharing: true, - defaultPreloadStaleTime: 0, -}) +// Detect panel type +declare global { + interface Window { + __SQLMESH_PANEL_TYPE__?: string + } +} -// Register the router instance for type safety -declare module '@tanstack/react-router' { - interface Register { - router: typeof router +const panelType = window.__SQLMESH_PANEL_TYPE__ || 'lineage' + +// component selector +function App() { + if (panelType === 'tablediff') { + return } + + return } // Render the app const rootElement = document.getElementById('app') if (rootElement && !rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement) + root.render( - + , ) diff --git a/vscode/react/src/pages/tablediff.tsx b/vscode/react/src/pages/tablediff.tsx new file mode 100644 index 0000000000..47e3b4ed58 --- /dev/null +++ b/vscode/react/src/pages/tablediff.tsx @@ -0,0 +1,22 @@ +import '../App.css' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { TableDiff } from '../components/tablediff/TableDiff' + +export function TableDiffPage() { + const client = new QueryClient({ + defaultOptions: { + queries: { + networkMode: 'always', + refetchOnWindowFocus: false, + retry: false, + staleTime: Infinity, + }, + }, + }) + + return ( + + + + ) +} diff --git a/vscode/react/src/routeTree.gen.ts b/vscode/react/src/routeTree.gen.ts index dd198661f1..f18a46802a 100644 --- a/vscode/react/src/routeTree.gen.ts +++ b/vscode/react/src/routeTree.gen.ts @@ -9,9 +9,15 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as TablediffRouteImport } from './routes/tablediff' import { Route as LineageRouteImport } from './routes/lineage' import { Route as IndexRouteImport } from './routes/index' +const TablediffRoute = TablediffRouteImport.update({ + id: '/tablediff', + path: '/tablediff', + getParentRoute: () => rootRouteImport, +} as any) const LineageRoute = LineageRouteImport.update({ id: '/lineage', path: '/lineage', @@ -26,31 +32,42 @@ const IndexRoute = IndexRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/lineage': typeof LineageRoute + '/tablediff': typeof TablediffRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/lineage': typeof LineageRoute + '/tablediff': typeof TablediffRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/lineage': typeof LineageRoute + '/tablediff': typeof TablediffRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/lineage' + fullPaths: '/' | '/lineage' | '/tablediff' fileRoutesByTo: FileRoutesByTo - to: '/' | '/lineage' - id: '__root__' | '/' | '/lineage' + to: '/' | '/lineage' | '/tablediff' + id: '__root__' | '/' | '/lineage' | '/tablediff' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute LineageRoute: typeof LineageRoute + TablediffRoute: typeof TablediffRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/tablediff': { + id: '/tablediff' + path: '/tablediff' + fullPath: '/tablediff' + preLoaderRoute: typeof TablediffRouteImport + parentRoute: typeof rootRouteImport + } '/lineage': { id: '/lineage' path: '/lineage' @@ -71,6 +88,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LineageRoute: LineageRoute, + TablediffRoute: TablediffRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/vscode/react/src/routes/__root.tsx b/vscode/react/src/routes/__root.tsx index 83a1d6db65..a600d0f849 100644 --- a/vscode/react/src/routes/__root.tsx +++ b/vscode/react/src/routes/__root.tsx @@ -2,6 +2,7 @@ import { Outlet, createRootRoute } from '@tanstack/react-router' import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' import '../App.css' import { LineagePage } from '@/pages/lineage' +import { TableDiffPage } from '@/pages/tablediff' export const Route = createRootRoute({ component: () => { @@ -13,6 +14,8 @@ export const Route = createRootRoute({ ) }, notFoundComponent: () => { - return + // switch to lineage or table diff based on panel type + const panelType = (window as any).__SQLMESH_PANEL_TYPE__ || 'lineage' + return panelType === 'tablediff' ? : }, }) diff --git a/vscode/react/src/routes/tablediff.tsx b/vscode/react/src/routes/tablediff.tsx new file mode 100644 index 0000000000..c9776048cd --- /dev/null +++ b/vscode/react/src/routes/tablediff.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router' +import { TableDiffPage } from '../pages/tablediff' + +export const Route = createFileRoute('/tablediff')({ + component: TableDiffPage, +}) diff --git a/web/server/api/endpoints/table_diff.py b/web/server/api/endpoints/table_diff.py index 6d11638b84..d441d49e5a 100644 --- a/web/server/api/endpoints/table_diff.py +++ b/web/server/api/endpoints/table_diff.py @@ -6,12 +6,109 @@ from sqlglot import exp from sqlmesh.core.context import Context -from web.server.models import RowDiff, SchemaDiff, TableDiff +from web.server.models import ProcessedSampleData, RowDiff, SchemaDiff, TableDiff from web.server.settings import get_loaded_context router = APIRouter() +def _cells_match(x: t.Any, y: t.Any) -> bool: + # lazily import pandas and numpy as we do in core + import pandas as pd + import numpy as np + + def _normalize(val: t.Any) -> t.Any: + if pd.isnull(val): + val = None + return list(val) if isinstance(val, (pd.Series, np.ndarray)) else val + + return _normalize(x) == _normalize(y) + + +def _process_sample_data( + row_diff: t.Any, source_name: str, target_name: str +) -> ProcessedSampleData: + import pandas as pd + + if row_diff.joined_sample.shape[0] == 0: + return ProcessedSampleData( + column_differences=[], + source_only=row_diff.s_sample.replace({pd.NA: None}).to_dict("records") + if row_diff.s_sample.shape[0] > 0 + else [], + target_only=row_diff.t_sample.replace({pd.NA: None}).to_dict("records") + if row_diff.t_sample.shape[0] > 0 + else [], + ) + + keys: list[str] = [] + columns: dict[str, list[str]] = {} + + # todo: to be refactored to the diff module itself since it is similar to console + source_prefix, source_display = ( + (f"{source_name}__", source_name.upper()) + if source_name.lower() != row_diff.source.lower() + else ("s__", "SOURCE") + ) + target_prefix, target_display = ( + (f"{target_name}__", target_name.upper()) + if target_name.lower() != row_diff.target.lower() + else ("t__", "TARGET") + ) + + for column in row_diff.joined_sample.columns: + if column.lower().startswith(source_prefix.lower()): + column_name = column[len(source_prefix) :] + + target_column = None + for col in row_diff.joined_sample.columns: + if col.lower() == (target_prefix + column_name).lower(): + target_column = col + break + + if target_column: + columns[column_name] = [column, target_column] + elif not column.lower().startswith(target_prefix.lower()): + keys.append(column) + + column_differences = [] + for column_name, (source_column, target_column) in columns.items(): + column_table = row_diff.joined_sample[keys + [source_column, target_column]] + + # Filter to retain non identical-valued rows + column_table = column_table[ + column_table.apply( + lambda row: not _cells_match(row[source_column], row[target_column]), + axis=1, + ) + ] + + # Rename the column headers for readability + column_table = column_table.rename( + columns={ + source_column: source_display, + target_column: target_display, + } + ) + + if len(column_table) > 0: + for row in column_table.replace({pd.NA: None}).to_dict("records"): + row["__column_name__"] = column_name + row["__source_name__"] = source_display + row["__target_name__"] = target_display + column_differences.append(row) + + return ProcessedSampleData( + column_differences=column_differences, + source_only=row_diff.s_sample.replace({pd.NA: None}).to_dict("records") + if row_diff.s_sample.shape[0] > 0 + else [], + target_only=row_diff.t_sample.replace({pd.NA: None}).to_dict("records") + if row_diff.t_sample.shape[0] > 0 + else [], + ) + + @router.get("") def get_table_diff( source: str, @@ -51,14 +148,24 @@ def get_table_diff( removed=_schema_diff.removed, modified=_schema_diff.modified, ) + + # create a readable column-centric sample data structure + processed_sample_data = _process_sample_data(_row_diff, source, target) + row_diff = RowDiff( source=_row_diff.source, target=_row_diff.target, stats=_row_diff.stats, sample=_row_diff.sample.replace({np.nan: None}).to_dict(), + joined_sample=_row_diff.joined_sample.replace({np.nan: None}).to_dict(), + s_sample=_row_diff.s_sample.replace({np.nan: None}).to_dict(), + t_sample=_row_diff.t_sample.replace({np.nan: None}).to_dict(), + column_stats=_row_diff.column_stats.replace({np.nan: None}).to_dict(), source_count=_row_diff.source_count, target_count=_row_diff.target_count, count_pct_change=_row_diff.count_pct_change, + decimals=getattr(_row_diff, "decimals", 3), + processed_sample_data=processed_sample_data, ) s_index, t_index, _ = diff.key_columns diff --git a/web/server/models.py b/web/server/models.py index d193fa5f07..d26848e068 100644 --- a/web/server/models.py +++ b/web/server/models.py @@ -392,24 +392,47 @@ def validate_schema( v: t.Union[ t.Dict[str, exp.DataType], t.List[t.Tuple[str, exp.DataType]], + t.Dict[str, t.Tuple[exp.DataType, exp.DataType]], t.Dict[str, str], ], + info: ValidationInfo, ) -> t.Dict[str, str]: if isinstance(v, dict): - return {k: str(v) for k, v in v.items()} + # Handle modified field which has tuples of (source_type, target_type) + if info.field_name == "modified" and any(isinstance(val, tuple) for val in v.values()): + return { + k: f"{str(val[0])} → {str(val[1])}" + for k, val in v.items() + if isinstance(val, tuple) + and isinstance(val[0], exp.DataType) + and isinstance(val[1], exp.DataType) + } + return {k: str(val) for k, val in v.items()} if isinstance(v, list): - return {k: str(v) for k, v in v} + return {k: str(val) for k, val in v} return v +class ProcessedSampleData(PydanticModel): + column_differences: t.List[t.Dict[str, t.Any]] + source_only: t.List[t.Dict[str, t.Any]] + target_only: t.List[t.Dict[str, t.Any]] + + class RowDiff(PydanticModel): source: str target: str stats: t.Dict[str, float] sample: t.Dict[str, t.Any] + joined_sample: t.Dict[str, t.Any] + s_sample: t.Dict[str, t.Any] + t_sample: t.Dict[str, t.Any] + column_stats: t.Dict[str, t.Any] source_count: int target_count: int count_pct_change: float + decimals: int + processed_sample_data: t.Optional[ProcessedSampleData] = None class TableDiff(PydanticModel): From 5e59d18f3e61ba6bc90465625eae45fe9ba10120 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Fri, 15 Aug 2025 12:39:22 -0700 Subject: [PATCH 0721/1056] feat!: add ignore destructive support (#5117) --- docs/concepts/models/overview.md | 8 +- docs/guides/custom_materializations.md | 4 + docs/guides/incremental_time.md | 7 +- .../custom_materializations/custom_kind.py | 3 +- .../custom_materializations/full.py | 3 +- sqlmesh/core/dialect.py | 35 +- sqlmesh/core/engine_adapter/_typing.py | 2 +- sqlmesh/core/engine_adapter/athena.py | 26 +- sqlmesh/core/engine_adapter/base.py | 479 ++++++--- sqlmesh/core/engine_adapter/base_postgres.py | 6 +- sqlmesh/core/engine_adapter/bigquery.py | 77 +- sqlmesh/core/engine_adapter/clickhouse.py | 66 +- sqlmesh/core/engine_adapter/databricks.py | 21 +- sqlmesh/core/engine_adapter/duckdb.py | 13 +- sqlmesh/core/engine_adapter/mixins.py | 44 +- sqlmesh/core/engine_adapter/mssql.py | 50 +- sqlmesh/core/engine_adapter/postgres.py | 6 +- sqlmesh/core/engine_adapter/redshift.py | 38 +- sqlmesh/core/engine_adapter/snowflake.py | 72 +- sqlmesh/core/engine_adapter/spark.py | 98 +- sqlmesh/core/engine_adapter/trino.py | 39 +- sqlmesh/core/model/kind.py | 5 + sqlmesh/core/plan/builder.py | 1 + sqlmesh/core/schema_diff.py | 83 +- sqlmesh/core/snapshot/evaluator.py | 343 +++++-- sqlmesh/core/state_sync/db/environment.py | 4 +- sqlmesh/core/state_sync/db/interval.py | 4 +- sqlmesh/core/state_sync/db/snapshot.py | 6 +- sqlmesh/core/state_sync/db/version.py | 2 +- sqlmesh/core/table_diff.py | 4 +- .../v0007_env_table_info_to_kind.py | 2 +- .../migrations/v0009_remove_pre_post_hooks.py | 2 +- .../migrations/v0011_add_model_kind_name.py | 2 +- .../v0012_update_jinja_expressions.py | 2 +- .../v0013_serde_using_model_dialects.py | 2 +- sqlmesh/migrations/v0016_fix_windows_path.py | 2 +- .../migrations/v0017_fix_windows_seed_path.py | 2 +- .../v0018_rename_snapshot_model_to_node.py | 2 +- ...ve_redundant_attributes_from_dbt_models.py | 2 +- .../migrations/v0021_fix_table_properties.py | 2 +- .../migrations/v0022_move_project_to_model.py | 2 +- ...replace_model_kind_name_enum_with_value.py | 2 +- ...x_intervals_and_missing_change_category.py | 4 +- .../v0026_remove_dialect_from_seed.py | 2 +- .../v0027_minute_interval_to_five.py | 2 +- ...029_generate_schema_types_using_dialect.py | 2 +- .../v0030_update_unrestorable_snapshots.py | 2 +- .../v0031_remove_dbt_target_fields.py | 2 +- .../migrations/v0034_add_default_catalog.py | 8 +- .../v0037_remove_dbt_is_incremental_macro.py | 2 +- .../v0038_add_expiration_ts_to_snapshot.py | 2 +- ...39_include_environment_in_plan_dag_spec.py | 2 +- .../v0041_remove_hash_raw_query_attribute.py | 2 +- .../v0042_trim_indirect_versions.py | 2 +- ...remove_obsolete_attributes_in_plan_dags.py | 2 +- .../migrations/v0045_move_gateway_variable.py | 2 +- .../v0048_drop_indirect_versions.py | 2 +- .../v0051_rename_column_descriptions.py | 2 +- ...used_ts_ttl_ms_unrestorable_to_snapshot.py | 2 +- .../migrations/v0056_restore_table_indexes.py | 6 +- .../migrations/v0060_move_audits_to_model.py | 2 +- sqlmesh/migrations/v0063_change_signals.py | 2 +- .../v0064_join_when_matched_strings.py | 2 +- .../v0069_update_dev_table_suffix.py | 4 +- .../v0071_add_dev_version_to_intervals.py | 4 +- ...073_remove_symbolic_disable_restatement.py | 2 +- .../migrations/v0075_remove_validate_query.py | 2 +- .../migrations/v0081_update_partitioned_by.py | 2 +- .../migrations/v0085_deterministic_repr.py | 2 +- .../v0087_normalize_blueprint_variables.py | 2 +- .../v0090_add_forward_only_column.py | 2 +- sqlmesh/utils/__init__.py | 12 + .../engine_adapter/integration/__init__.py | 2 +- .../engine_adapter/integration/conftest.py | 2 +- .../integration/test_integration.py | 795 ++++++++++++++- .../integration/test_integration_athena.py | 22 +- tests/core/engine_adapter/test_athena.py | 24 +- tests/core/engine_adapter/test_base.py | 633 +++++++++++- tests/core/engine_adapter/test_bigquery.py | 22 +- tests/core/engine_adapter/test_clickhouse.py | 4 +- tests/core/engine_adapter/test_databricks.py | 2 +- tests/core/engine_adapter/test_mixins.py | 4 +- tests/core/engine_adapter/test_mssql.py | 31 +- tests/core/engine_adapter/test_postgres.py | 4 +- tests/core/engine_adapter/test_redshift.py | 18 +- tests/core/engine_adapter/test_snowflake.py | 27 +- tests/core/engine_adapter/test_spark.py | 16 +- tests/core/engine_adapter/test_trino.py | 10 +- tests/core/state_sync/test_state_sync.py | 8 +- tests/core/test_context.py | 2 +- tests/core/test_integration.py | 961 +++++++++++++++++- tests/core/test_schema_diff.py | 235 +++++ tests/core/test_snapshot_evaluator.py | 97 +- tests/dbt/test_adapter.py | 12 +- tests/dbt/test_integration.py | 6 +- 95 files changed, 3828 insertions(+), 768 deletions(-) diff --git a/docs/concepts/models/overview.md b/docs/concepts/models/overview.md index dd9fd0d767..cf57678607 100644 --- a/docs/concepts/models/overview.md +++ b/docs/concepts/models/overview.md @@ -507,11 +507,15 @@ Some properties are only available in specific model kinds - see the [model conf : Set this to true to indicate that all changes to this model should be [forward-only](../plans.md#forward-only-plans). ### on_destructive_change -: What should happen when a change to a [forward-only model](../../guides/incremental_time.md#forward-only-models) or incremental model in a [forward-only plan](../plans.md#forward-only-plans) causes a destructive modification to the table schema (i.e., requires dropping an existing column). +: What should happen when a change to a [forward-only model](../../guides/incremental_time.md#forward-only-models) or incremental model in a [forward-only plan](../plans.md#forward-only-plans) causes a destructive modification to the table schema (i.e., requires dropping an existing column or modifying column constraints in ways that could cause data loss). SQLMesh checks for destructive changes at plan time based on the model definition and run time based on the model's underlying physical tables. - Must be one of the following values: `allow`, `warn`, or `error` (default). + Must be one of the following values: `allow`, `warn`, `error` (default), or `ignore`. + +!!! warning "Ignore is Dangerous" + + `ignore` is dangerous since it can result in error or data loss. It likely should never be used but could be useful as an "escape-hatch" or a way to workaround unexpected behavior. ### disable_restatement : Set this to true to indicate that [data restatement](../plans.md#restatement-plans) is disabled for this model. diff --git a/docs/guides/custom_materializations.md b/docs/guides/custom_materializations.md index b11d9004a9..58eb64026d 100644 --- a/docs/guides/custom_materializations.md +++ b/docs/guides/custom_materializations.md @@ -64,6 +64,7 @@ class CustomFullMaterialization(CustomMaterialization): query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: self.adapter.replace_query(table_name, query_or_df) @@ -78,6 +79,7 @@ Let's unpack this materialization: * `query_or_df` - a query (of SQLGlot expression type) or DataFrame (Pandas, PySpark, or Snowpark) instance to be inserted * `model` - the model definition object used to access model parameters and user-specified materialization arguments * `is_first_insert` - whether this is the first insert for the current version of the model (used with batched or multi-step inserts) + * `render_kwargs` - a dictionary of arguments used to render the model query * `kwargs` - additional and future arguments * The `self.adapter` instance is used to interact with the target engine. It comes with a set of useful high-level APIs like `replace_query`, `columns`, and `table_exists`, but also supports executing arbitrary SQL expressions with its `execute` method. @@ -150,6 +152,7 @@ class CustomFullMaterialization(CustomMaterialization): query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: config_value = model.custom_materialization_properties["config_key"] @@ -232,6 +235,7 @@ class CustomFullMaterialization(CustomMaterialization[MyCustomKind]): query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: assert isinstance(model.kind, MyCustomKind) diff --git a/docs/guides/incremental_time.md b/docs/guides/incremental_time.md index 7c773f7edc..2f54516ec4 100644 --- a/docs/guides/incremental_time.md +++ b/docs/guides/incremental_time.md @@ -171,7 +171,12 @@ The check is performed at plan time based on the model definition. SQLMesh may n A model's `on_destructive_change` [configuration setting](../reference/model_configuration.md#incremental-models) determines what happens when SQLMesh detects a destructive change. -By default, SQLMesh will error so no data is lost. You can set `on_destructive_change` to `warn` or `allow` in the model's `MODEL` block to allow destructive changes. +By default, SQLMesh will error so no data is lost. You can set `on_destructive_change` to `warn` or `allow` in the model's `MODEL` block to allow destructive changes. +`ignore` can be used to not perform the schema change and allow the table's definition to diverge from the model definition. + +!!! warning "Ignore is Dangerous" + + `ignore` is dangerous since it can result in error or data loss. It likely should never be used but could be useful as an "escape-hatch" or a way to workaround unexpected behavior. This example configures a model to silently `allow` destructive changes: diff --git a/examples/custom_materializations/custom_materializations/custom_kind.py b/examples/custom_materializations/custom_materializations/custom_kind.py index a8330febad..8a0eabcfa7 100644 --- a/examples/custom_materializations/custom_materializations/custom_kind.py +++ b/examples/custom_materializations/custom_materializations/custom_kind.py @@ -24,8 +24,9 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: assert type(model.kind).__name__ == "ExtendedCustomKind" - self._replace_query_for_model(model, table_name, query_or_df) + self._replace_query_for_model(model, table_name, query_or_df, render_kwargs) diff --git a/examples/custom_materializations/custom_materializations/full.py b/examples/custom_materializations/custom_materializations/full.py index 79aa50232a..d2a7c64993 100644 --- a/examples/custom_materializations/custom_materializations/full.py +++ b/examples/custom_materializations/custom_materializations/full.py @@ -17,6 +17,7 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: - self._replace_query_for_model(model, table_name, query_or_df) + self._replace_query_for_model(model, table_name, query_or_df, render_kwargs) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 568d9f5f73..ed904cc4b3 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -23,6 +23,7 @@ from sqlglot.tokens import Token from sqlmesh.core.constants import MAX_MODEL_DEFINITION_SIZE +from sqlmesh.utils import get_source_columns_to_types from sqlmesh.utils.errors import SQLMeshError, ConfigError from sqlmesh.utils.pandas import columns_to_types_from_df @@ -1121,7 +1122,7 @@ def select_from_values( for i in range(0, num_rows, batch_size): yield select_from_values_for_batch_range( values=values, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, batch_start=i, batch_end=min(i + batch_size, num_rows), alias=alias, @@ -1130,35 +1131,49 @@ def select_from_values( def select_from_values_for_batch_range( values: t.List[t.Tuple[t.Any, ...]], - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], batch_start: int, batch_end: int, alias: str = "t", + source_columns: t.Optional[t.List[str]] = None, ) -> exp.Select: - casted_columns = [ - exp.alias_(exp.cast(exp.column(column), to=kind), column, copy=False) - for column, kind in columns_to_types.items() - ] + source_columns = source_columns or list(target_columns_to_types) + source_columns_to_types = get_source_columns_to_types(target_columns_to_types, source_columns) if not values: # Ensures we don't generate an empty VALUES clause & forces a zero-row output where = exp.false() - expressions = [tuple(exp.cast(exp.null(), to=kind) for kind in columns_to_types.values())] + expressions = [ + tuple(exp.cast(exp.null(), to=kind) for kind in source_columns_to_types.values()) + ] else: where = None expressions = [ - tuple(transform_values(v, columns_to_types)) for v in values[batch_start:batch_end] + tuple(transform_values(v, source_columns_to_types)) + for v in values[batch_start:batch_end] ] - values_exp = exp.values(expressions, alias=alias, columns=columns_to_types) + values_exp = exp.values(expressions, alias=alias, columns=source_columns_to_types) if values: # BigQuery crashes on `SELECT CAST(x AS TIMESTAMP) FROM UNNEST([NULL]) AS x`, but not # on `SELECT CAST(x AS TIMESTAMP) FROM UNNEST([CAST(NULL AS TIMESTAMP)]) AS x`. This # ensures nulls under the `Values` expression are cast to avoid similar issues. - for value, kind in zip(values_exp.expressions[0].expressions, columns_to_types.values()): + for value, kind in zip( + values_exp.expressions[0].expressions, source_columns_to_types.values() + ): if isinstance(value, exp.Null): value.replace(exp.cast(value, to=kind)) + casted_columns = [ + exp.alias_( + exp.cast( + exp.column(column) if column in source_columns_to_types else exp.Null(), to=kind + ), + column, + copy=False, + ) + for column, kind in target_columns_to_types.items() + ] return exp.select(*casted_columns).from_(values_exp, copy=False).where(where, copy=False) diff --git a/sqlmesh/core/engine_adapter/_typing.py b/sqlmesh/core/engine_adapter/_typing.py index 143fcf6ab6..98821bb2d4 100644 --- a/sqlmesh/core/engine_adapter/_typing.py +++ b/sqlmesh/core/engine_adapter/_typing.py @@ -13,7 +13,7 @@ snowpark = optional_import("snowflake.snowpark") - Query = t.Union[exp.Query, exp.DerivedTable] + Query = exp.Query PySparkSession = t.Union[pyspark.sql.SparkSession, pyspark.sql.connect.dataframe.SparkSession] PySparkDataFrame = t.Union[pyspark.sql.DataFrame, pyspark.sql.connect.dataframe.DataFrame] diff --git a/sqlmesh/core/engine_adapter/athena.py b/sqlmesh/core/engine_adapter/athena.py index abaf7ba281..d549de3f4c 100644 --- a/sqlmesh/core/engine_adapter/athena.py +++ b/sqlmesh/core/engine_adapter/athena.py @@ -84,12 +84,12 @@ def catalog_support(self) -> CatalogSupport: def create_state_table( self, table_name: str, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], primary_key: t.Optional[t.Tuple[str, ...]] = None, ) -> None: self.create_table( table_name, - columns_to_types, + target_columns_to_types, primary_key=primary_key, # it's painfully slow, but it works table_format="iceberg", @@ -178,7 +178,7 @@ def _build_create_table_exp( expression: t.Optional[exp.Expression], exists: bool = True, replace: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, table_kind: t.Optional[str] = None, partitioned_by: t.Optional[t.List[exp.Expression]] = None, @@ -198,7 +198,7 @@ def _build_create_table_exp( properties = self._build_table_properties_exp( table=table, expression=expression, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, partitioned_by=partitioned_by, table_properties=table_properties, table_description=table_description, @@ -237,7 +237,7 @@ def _build_table_properties_exp( partition_interval_unit: t.Optional[IntervalUnit] = None, clustered_by: t.Optional[t.List[exp.Expression]] = None, table_properties: t.Optional[t.Dict[str, exp.Expression]] = None, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, table_kind: t.Optional[str] = None, table: t.Optional[exp.Table] = None, @@ -265,12 +265,12 @@ def _build_table_properties_exp( if partitioned_by: schema_expressions: t.List[exp.Expression] = [] - if is_hive and columns_to_types: + if is_hive and target_columns_to_types: # For Hive-style tables, you cannot include the partitioned by columns in the main set of columns # In the PARTITIONED BY expression, you also cant just include the column names, you need to include the data type as well # ref: https://docs.aws.amazon.com/athena/latest/ug/partitions.html for match_name, match_dtype in self._find_matching_columns( - partitioned_by, columns_to_types + partitioned_by, target_columns_to_types ): column_def = exp.ColumnDef(this=exp.to_identifier(match_name), kind=match_dtype) schema_expressions.append(column_def) @@ -431,9 +431,10 @@ def replace_query( self, table_name: TableName, query_or_df: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: table = exp.to_table(table_name) @@ -444,9 +445,10 @@ def replace_query( return super().replace_query( table_name=table, query_or_df=query_or_df, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, table_description=table_description, column_descriptions=column_descriptions, + source_columns=source_columns, **kwargs, ) @@ -454,7 +456,7 @@ def _insert_overwrite_by_time_partition( self, table_name: TableName, source_queries: t.List[SourceQuery], - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], where: exp.Condition, **kwargs: t.Any, ) -> None: @@ -465,7 +467,7 @@ def _insert_overwrite_by_time_partition( if table_type == "iceberg": # Iceberg tables work as expected, we can use the default behaviour return super()._insert_overwrite_by_time_partition( - table, source_queries, columns_to_types, where, **kwargs + table, source_queries, target_columns_to_types, where, **kwargs ) # For Hive tables, we need to drop all the partitions covered by the query and delete the data from S3 @@ -475,7 +477,7 @@ def _insert_overwrite_by_time_partition( return super()._insert_overwrite_by_time_partition( table, source_queries, - columns_to_types, + target_columns_to_types, where, insert_overwrite_strategy_override=InsertOverwriteStrategy.INTO_IS_OVERWRITE, # since we already cleared the data **kwargs, diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 4651caa6ec..94ffbe81d2 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -40,7 +40,12 @@ ) from sqlmesh.core.model.kind import TimeColumn from sqlmesh.core.schema_diff import SchemaDiffer -from sqlmesh.utils import CorrelationId, columns_to_types_all_known, random_id +from sqlmesh.utils import ( + CorrelationId, + columns_to_types_all_known, + random_id, + get_source_columns_to_types, +) from sqlmesh.utils.connection_pool import ConnectionPool, create_connection_pool from sqlmesh.utils.date import TimeLike, make_inclusive, to_time_column from sqlmesh.utils.errors import ( @@ -199,10 +204,25 @@ def catalog_support(self) -> CatalogSupport: return CatalogSupport.UNSUPPORTED @classmethod - def _casted_columns(cls, columns_to_types: t.Dict[str, exp.DataType]) -> t.List[exp.Alias]: + def _casted_columns( + cls, + target_columns_to_types: t.Dict[str, exp.DataType], + source_columns: t.Optional[t.List[str]] = None, + ) -> t.List[exp.Alias]: + source_columns_lookup = set(source_columns or target_columns_to_types) return [ - exp.alias_(exp.cast(exp.column(column), to=kind), column, copy=False) - for column, kind in columns_to_types.items() + exp.alias_( + exp.cast( + exp.column(column, quoted=True) + if column in source_columns_lookup + else exp.Null(), + to=kind, + ), + column, + copy=False, + quoted=True, + ) + for column, kind in target_columns_to_types.items() ] @property @@ -223,18 +243,38 @@ def engine_run_mode(self) -> EngineRunMode: def _get_source_queries( self, query_or_df: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]], + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]], target_table: TableName, *, batch_size: t.Optional[int] = None, + source_columns: t.Optional[t.List[str]] = None, ) -> t.List[SourceQuery]: import pandas as pd batch_size = self.DEFAULT_BATCH_SIZE if batch_size is None else batch_size - if isinstance(query_or_df, (exp.Query, exp.DerivedTable)): - return [SourceQuery(query_factory=lambda: query_or_df)] # type: ignore + if isinstance(query_or_df, exp.Query): + query_factory = lambda: query_or_df + if source_columns: + source_columns_lookup = set(source_columns) + if not target_columns_to_types: + raise SQLMeshError("columns_to_types must be set if source_columns is set") + if not set(target_columns_to_types).issubset(source_columns_lookup): + select_columns = [ + exp.column(c, quoted=True) + if c in source_columns_lookup + else exp.cast(exp.Null(), target_columns_to_types[c], copy=False).as_( + c, copy=False, quoted=True + ) + for c in target_columns_to_types + ] + query_factory = ( + lambda: exp.Select() + .select(*select_columns) + .from_(query_or_df.subquery("select_source_columns")) + ) + return [SourceQuery(query_factory=query_factory)] # type: ignore - if not columns_to_types: + if not target_columns_to_types: raise SQLMeshError( "It is expected that if a DataFrame is passed in then columns_to_types is set" ) @@ -247,15 +287,20 @@ def _get_source_queries( ) return self._df_to_source_queries( - query_or_df, columns_to_types, batch_size, target_table=target_table + query_or_df, + target_columns_to_types, + batch_size, + target_table=target_table, + source_columns=source_columns, ) def _df_to_source_queries( self, df: DF, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], batch_size: int, target_table: TableName, + source_columns: t.Optional[t.List[str]] = None, ) -> t.List[SourceQuery]: import pandas as pd @@ -265,7 +310,7 @@ def _df_to_source_queries( # we need to ensure that the order of the columns in columns_to_types columns matches the order of the values # they can differ if a user specifies columns() on a python model in a different order than what's in the DataFrame's emitted by that model - df = df[list(columns_to_types)] + df = df[list(source_columns or target_columns_to_types)] values = list(df.itertuples(index=False, name=None)) return [ @@ -273,9 +318,10 @@ def _df_to_source_queries( query_factory=partial( self._values_to_sql, values=values, # type: ignore - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, batch_start=i, batch_end=min(i + batch_size, num_rows), + source_columns=source_columns, ), ) for i in range(0, num_rows, batch_size) @@ -284,39 +330,53 @@ def _df_to_source_queries( def _get_source_queries_and_columns_to_types( self, query_or_df: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]], + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]], target_table: TableName, *, batch_size: t.Optional[int] = None, + source_columns: t.Optional[t.List[str]] = None, ) -> t.Tuple[t.List[SourceQuery], t.Optional[t.Dict[str, exp.DataType]]]: - columns_to_types = self._columns_to_types(query_or_df, columns_to_types) - return ( - self._get_source_queries( - query_or_df, columns_to_types, target_table=target_table, batch_size=batch_size - ), - columns_to_types, + target_columns_to_types, source_columns = self._columns_to_types( + query_or_df, target_columns_to_types, source_columns ) + source_queries = self._get_source_queries( + query_or_df, + target_columns_to_types, + target_table=target_table, + batch_size=batch_size, + source_columns=source_columns, + ) + return source_queries, target_columns_to_types @t.overload def _columns_to_types( - self, query_or_df: DF, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Dict[str, exp.DataType]: ... + self, + query_or_df: DF, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Dict[str, exp.DataType], t.List[str]]: ... @t.overload def _columns_to_types( - self, query_or_df: Query, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Optional[t.Dict[str, exp.DataType]]: ... + self, + query_or_df: Query, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Optional[t.Dict[str, exp.DataType]], t.Optional[t.List[str]]]: ... def _columns_to_types( - self, query_or_df: QueryOrDF, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Optional[t.Dict[str, exp.DataType]]: + self, + query_or_df: QueryOrDF, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Optional[t.Dict[str, exp.DataType]], t.Optional[t.List[str]]]: import pandas as pd - if columns_to_types: - return columns_to_types - if isinstance(query_or_df, pd.DataFrame): - return columns_to_types_from_df(t.cast(pd.DataFrame, query_or_df)) - return columns_to_types + if not target_columns_to_types and isinstance(query_or_df, pd.DataFrame): + target_columns_to_types = columns_to_types_from_df(t.cast(pd.DataFrame, query_or_df)) + if not source_columns and target_columns_to_types: + source_columns = list(target_columns_to_types) + return target_columns_to_types, source_columns def recycle(self) -> None: """Closes all open connections and releases all allocated resources associated with any thread @@ -353,9 +413,10 @@ def replace_query( self, table_name: TableName, query_or_df: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: """Replaces an existing table with a query. @@ -365,7 +426,7 @@ def replace_query( Args: table_name: The name of the table (eg. prod.table) query_or_df: The SQL query to run or a dataframe. - columns_to_types: Only used if a dataframe is provided. A mapping between the column name and its data type. + target_columns_to_types: Only used if a dataframe is provided. A mapping between the column name and its data type. Expected to be ordered to match the order of values in the dataframe. kwargs: Optional create table properties. """ @@ -376,11 +437,14 @@ def replace_query( if self.drop_data_object_on_type_mismatch(target_data_object, DataObjectType.TABLE): table_exists = False - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - query_or_df, columns_to_types, target_table=target_table + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + query_or_df, + target_columns_to_types, + target_table=target_table, + source_columns=source_columns, ) - columns_to_types = columns_to_types or self.columns(target_table) query = source_queries[0].query_factory() + target_columns_to_types = target_columns_to_types or self.columns(target_table) self_referencing = any( quote_identifiers(table) == quote_identifiers(target_table) for table in query.find_all(exp.Table) @@ -389,7 +453,7 @@ def replace_query( if self_referencing: self._create_table_from_columns( target_table, - columns_to_types, + target_columns_to_types, exists=True, table_description=table_description, column_descriptions=column_descriptions, @@ -401,7 +465,7 @@ def replace_query( return self._create_table_from_source_queries( target_table, source_queries, - columns_to_types, + target_columns_to_types, replace=self.SUPPORTS_REPLACE_TABLE, table_description=table_description, column_descriptions=column_descriptions, @@ -409,9 +473,9 @@ def replace_query( ) if self_referencing: with self.temp_table( - self._select_columns(columns_to_types).from_(target_table), + self._select_columns(target_columns_to_types).from_(target_table), name=target_table, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, **kwargs, ) as temp_table: for source_query in source_queries: @@ -426,12 +490,12 @@ def replace_query( return self._insert_overwrite_by_condition( target_table, source_queries, - columns_to_types, + target_columns_to_types, ) return self._insert_overwrite_by_condition( target_table, source_queries, - columns_to_types, + target_columns_to_types, ) def create_index( @@ -493,7 +557,7 @@ def _pop_creatable_type_from_properties( def create_table( self, table_name: TableName, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], primary_key: t.Optional[t.Tuple[str, ...]] = None, exists: bool = True, table_description: t.Optional[str] = None, @@ -504,7 +568,7 @@ def create_table( Args: table_name: The name of the table to create. Can be fully qualified or just table name. - columns_to_types: A mapping between the column name and its data type. + target_columns_to_types: A mapping between the column name and its data type. primary_key: Determines the table primary key. exists: Indicates whether to include the IF NOT EXISTS check. table_description: Optional table description from MODEL DDL. @@ -513,7 +577,7 @@ def create_table( """ self._create_table_from_columns( table_name, - columns_to_types, + target_columns_to_types, primary_key, exists, table_description, @@ -525,12 +589,13 @@ def create_managed_table( self, table_name: TableName, query: Query, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, partitioned_by: t.Optional[t.List[exp.Expression]] = None, clustered_by: t.Optional[t.List[exp.Expression]] = None, table_properties: t.Optional[t.Dict[str, exp.Expression]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: """Create a managed table using a query. @@ -540,7 +605,7 @@ def create_managed_table( Args: table_name: The name of the table to create. Can be fully qualified or just table name. query: The SQL query for the engine to base the managed table on - columns_to_types: A mapping between the column name and its data type. + target_columns_to_types: A mapping between the column name and its data type. partitioned_by: The partition columns or engine specific expressions, only applicable in certain engines. (eg. (ds, hour)) clustered_by: The cluster columns or engine specific expressions, only applicable in certain engines. (eg. (ds, hour)) table_properties: Optional mapping of engine-specific properties to be set on the managed table @@ -554,10 +619,11 @@ def ctas( self, table_name: TableName, query_or_df: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, exists: bool = True, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: """Create a table using a CTAS statement @@ -565,19 +631,22 @@ def ctas( Args: table_name: The name of the table to create. Can be fully qualified or just table name. query_or_df: The SQL query to run or a dataframe for the CTAS. - columns_to_types: A mapping between the column name and its data type. Required if using a DataFrame. + target_columns_to_types: A mapping between the column name and its data type. Required if using a DataFrame. exists: Indicates whether to include the IF NOT EXISTS check. table_description: Optional table description from MODEL DDL. column_descriptions: Optional column descriptions from model query. kwargs: Optional create table properties. """ - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - query_or_df, columns_to_types, target_table=table_name + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + query_or_df, + target_columns_to_types, + target_table=table_name, + source_columns=source_columns, ) return self._create_table_from_source_queries( table_name, source_queries, - columns_to_types, + target_columns_to_types, exists, table_description=table_description, column_descriptions=column_descriptions, @@ -587,26 +656,26 @@ def ctas( def create_state_table( self, table_name: str, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], primary_key: t.Optional[t.Tuple[str, ...]] = None, ) -> None: """Create a table to store SQLMesh internal state. Args: table_name: The name of the table to create. Can be fully qualified or just table name. - columns_to_types: A mapping between the column name and its data type. + target_columns_to_types: A mapping between the column name and its data type. primary_key: Determines the table primary key. """ self.create_table( table_name, - columns_to_types, + target_columns_to_types, primary_key=primary_key, ) def _create_table_from_columns( self, table_name: TableName, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], primary_key: t.Optional[t.Tuple[str, ...]] = None, exists: bool = True, table_description: t.Optional[str] = None, @@ -618,7 +687,7 @@ def _create_table_from_columns( Args: table_name: The name of the table to create. Can be fully qualified or just table name. - columns_to_types: Mapping between the column name and its data type. + target_columns_to_types: Mapping between the column name and its data type. primary_key: Determines the table primary key. exists: Indicates whether to include the IF NOT EXISTS check. table_description: Optional table description from MODEL DDL. @@ -627,14 +696,14 @@ def _create_table_from_columns( """ table = exp.to_table(table_name) - if not columns_to_types_all_known(columns_to_types): + if not columns_to_types_all_known(target_columns_to_types): # It is ok if the columns types are not known if the table already exists and IF NOT EXISTS is set if exists and self.table_exists(table_name): return raise SQLMeshError( "Cannot create a table without knowing the column types. " "Try casting the columns to an expected type or defining the columns in the model metadata. " - f"Columns to types: {columns_to_types}" + f"Columns to types: {target_columns_to_types}" ) primary_key_expression = ( @@ -645,7 +714,7 @@ def _create_table_from_columns( schema = self._build_schema_exp( table, - columns_to_types, + target_columns_to_types, column_descriptions, primary_key_expression, ) @@ -654,7 +723,7 @@ def _create_table_from_columns( schema, None, exists=exists, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, table_description=table_description, **kwargs, ) @@ -676,7 +745,7 @@ def _create_table_from_columns( def _build_schema_exp( self, table: exp.Table, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], column_descriptions: t.Optional[t.Dict[str, str]] = None, expressions: t.Optional[t.List[exp.PrimaryKey]] = None, is_view: bool = False, @@ -689,7 +758,7 @@ def _build_schema_exp( return exp.Schema( this=table, expressions=self._build_column_defs( - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, column_descriptions=column_descriptions, is_view=is_view, ) @@ -698,7 +767,7 @@ def _build_schema_exp( def _build_column_defs( self, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], column_descriptions: t.Optional[t.Dict[str, str]] = None, is_view: bool = False, ) -> t.List[exp.ColumnDef]: @@ -714,7 +783,7 @@ def _build_column_defs( engine_supports_schema_comments=engine_supports_schema_comments, col_type=None if is_view else kind, # don't include column data type for views ) - for column, kind in columns_to_types.items() + for column, kind in target_columns_to_types.items() ] def _build_column_def( @@ -753,7 +822,7 @@ def _create_table_from_source_queries( self, table_name: TableName, source_queries: t.List[SourceQuery], - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, exists: bool = True, replace: bool = False, table_description: t.Optional[str] = None, @@ -777,27 +846,29 @@ def _create_table_from_source_queries( # types, and for evaluation methods like `LogicalReplaceQueryMixin.replace_query()` # calls and SCD Type 2 model calls. schema = None - columns_to_types_known = columns_to_types and columns_to_types_all_known(columns_to_types) + target_columns_to_types_known = target_columns_to_types and columns_to_types_all_known( + target_columns_to_types + ) if ( column_descriptions - and columns_to_types_known + and target_columns_to_types_known and self.COMMENT_CREATION_TABLE.is_in_schema_def_ctas and self.comments_enabled ): - schema = self._build_schema_exp(table, columns_to_types, column_descriptions) # type: ignore + schema = self._build_schema_exp(table, target_columns_to_types, column_descriptions) # type: ignore with self.transaction(condition=len(source_queries) > 1): for i, source_query in enumerate(source_queries): with source_query as query: - if columns_to_types and columns_to_types_known: + if target_columns_to_types and target_columns_to_types_known: query = self._order_projections_and_filter( - query, columns_to_types, coerce_types=True + query, target_columns_to_types, coerce_types=True ) if i == 0: self._create_table( schema if schema else table, query, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, exists=exists, replace=replace, table_description=table_description, @@ -806,7 +877,7 @@ def _create_table_from_source_queries( ) else: self._insert_append_query( - table_name, query, columns_to_types or self.columns(table) + table_name, query, target_columns_to_types or self.columns(table) ) # Register comments with commands if the engine supports comments and we weren't able to @@ -826,7 +897,7 @@ def _create_table( expression: t.Optional[exp.Expression], exists: bool = True, replace: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, @@ -838,7 +909,7 @@ def _create_table( expression=expression, exists=exists, replace=replace, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, table_description=( table_description if self.COMMENT_CREATION_TABLE.supports_schema_def and self.comments_enabled @@ -855,7 +926,7 @@ def _build_create_table_exp( expression: t.Optional[exp.Expression], exists: bool = True, replace: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, table_kind: t.Optional[str] = None, **kwargs: t.Any, @@ -873,7 +944,7 @@ def _build_create_table_exp( self._build_table_properties_exp( **kwargs, catalog_name=catalog_name, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, table_description=table_description, table_kind=table_kind, ) @@ -1000,6 +1071,8 @@ def get_alter_expressions( self, current_table_name: TableName, target_table_name: TableName, + *, + ignore_destructive: bool = False, ) -> t.List[exp.Alter]: """ Determines the alter statements needed to change the current table into the structure of the target table. @@ -1008,6 +1081,7 @@ def get_alter_expressions( current_table_name, self.columns(current_table_name), self.columns(target_table_name), + ignore_destructive=ignore_destructive, ) def alter_table( @@ -1025,13 +1099,14 @@ def create_view( self, view_name: TableName, query_or_df: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, replace: bool = True, materialized: bool = False, materialized_properties: t.Optional[t.Dict[str, t.Any]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, view_properties: t.Optional[t.Dict[str, exp.Expression]] = None, + source_columns: t.Optional[t.List[str]] = None, **create_kwargs: t.Any, ) -> None: """Create a view with a query or dataframe. @@ -1042,7 +1117,7 @@ def create_view( Args: view_name: The view name. query_or_df: A query or dataframe. - columns_to_types: Columns to use in the view statement. + target_columns_to_types: Columns to use in the view statement. replace: Whether or not to replace an existing view defaults to True. materialized: Whether to create a a materialized view. Only used for engines that support this feature. materialized_properties: Optional materialized view properties to add to the view. @@ -1062,26 +1137,35 @@ def create_view( values: t.List[t.Tuple[t.Any, ...]] = list( query_or_df.itertuples(index=False, name=None) ) - columns_to_types = columns_to_types or self._columns_to_types(query_or_df) - if not columns_to_types: + target_columns_to_types, source_columns = self._columns_to_types( + query_or_df, target_columns_to_types, source_columns + ) + if not target_columns_to_types: raise SQLMeshError("columns_to_types must be provided for dataframes") + source_columns_to_types = get_source_columns_to_types( + target_columns_to_types, source_columns + ) query_or_df = self._values_to_sql( values, - columns_to_types, + source_columns_to_types, batch_start=0, batch_end=len(values), ) - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - query_or_df, columns_to_types, batch_size=0, target_table=view_name + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + query_or_df, + target_columns_to_types, + batch_size=0, + target_table=view_name, + source_columns=source_columns, ) if len(source_queries) != 1: raise SQLMeshError("Only one source query is supported for creating views") schema: t.Union[exp.Table, exp.Schema] = exp.to_table(view_name) - if columns_to_types: + if target_columns_to_types: schema = self._build_schema_exp( - exp.to_table(view_name), columns_to_types, column_descriptions, is_view=True + exp.to_table(view_name), target_columns_to_types, column_descriptions, is_view=True ) properties = create_kwargs.pop("properties", None) @@ -1181,7 +1265,7 @@ def create_view( self.COMMENT_CREATION_VIEW.is_comment_command_only or ( self.COMMENT_CREATION_VIEW.is_in_schema_def_and_commands - and not columns_to_types + and not target_columns_to_types ) ) and self.comments_enabled @@ -1307,54 +1391,67 @@ def insert_append( self, table_name: TableName, query_or_df: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, ) -> None: - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - query_or_df, columns_to_types, target_table=table_name + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + query_or_df, + target_columns_to_types, + target_table=table_name, + source_columns=source_columns, ) - self._insert_append_source_queries(table_name, source_queries, columns_to_types) + self._insert_append_source_queries(table_name, source_queries, target_columns_to_types) def _insert_append_source_queries( self, table_name: TableName, source_queries: t.List[SourceQuery], - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, ) -> None: with self.transaction(condition=len(source_queries) > 0): - columns_to_types = columns_to_types or self.columns(table_name) + target_columns_to_types = target_columns_to_types or self.columns(table_name) for source_query in source_queries: with source_query as query: - self._insert_append_query(table_name, query, columns_to_types) + self._insert_append_query(table_name, query, target_columns_to_types) def _insert_append_query( self, table_name: TableName, query: Query, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], order_projections: bool = True, ) -> None: if order_projections: - query = self._order_projections_and_filter(query, columns_to_types) - self.execute(exp.insert(query, table_name, columns=list(columns_to_types))) + query = self._order_projections_and_filter(query, target_columns_to_types) + self.execute(exp.insert(query, table_name, columns=list(target_columns_to_types))) def insert_overwrite_by_partition( self, table_name: TableName, query_or_df: QueryOrDF, partitioned_by: t.List[exp.Expression], - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, ) -> None: if self.INSERT_OVERWRITE_STRATEGY.is_insert_overwrite: target_table = exp.to_table(table_name) - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - query_or_df, columns_to_types, target_table=target_table + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + query_or_df, + target_columns_to_types, + target_table=target_table, + source_columns=source_columns, ) self._insert_overwrite_by_condition( - table_name, source_queries, columns_to_types=columns_to_types + table_name, source_queries, target_columns_to_types=target_columns_to_types ) else: self._replace_by_key( - table_name, query_or_df, columns_to_types, partitioned_by, is_unique_key=False + table_name, + query_or_df, + target_columns_to_types, + partitioned_by, + is_unique_key=False, + source_columns=source_columns, ) def insert_overwrite_by_time_partition( @@ -1367,16 +1464,21 @@ def insert_overwrite_by_time_partition( [TimeLike, t.Optional[t.Dict[str, exp.DataType]]], exp.Expression ], time_column: TimeColumn | exp.Expression | str, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - query_or_df, columns_to_types, target_table=table_name + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + query_or_df, + target_columns_to_types, + target_table=table_name, + source_columns=source_columns, ) - if not columns_to_types or not columns_to_types_all_known(columns_to_types): - columns_to_types = self.columns(table_name) + if not target_columns_to_types or not columns_to_types_all_known(target_columns_to_types): + target_columns_to_types = self.columns(table_name) low, high = [ - time_formatter(dt, columns_to_types) for dt in make_inclusive(start, end, self.dialect) + time_formatter(dt, target_columns_to_types) + for dt in make_inclusive(start, end, self.dialect) ] if isinstance(time_column, TimeColumn): time_column = time_column.column @@ -1386,42 +1488,44 @@ def insert_overwrite_by_time_partition( high=high, ) return self._insert_overwrite_by_time_partition( - table_name, source_queries, columns_to_types, where, **kwargs + table_name, source_queries, target_columns_to_types, where, **kwargs ) def _insert_overwrite_by_time_partition( self, table_name: TableName, source_queries: t.List[SourceQuery], - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], where: exp.Condition, **kwargs: t.Any, ) -> None: return self._insert_overwrite_by_condition( - table_name, source_queries, columns_to_types, where + table_name, source_queries, target_columns_to_types, where ) def _values_to_sql( self, values: t.List[t.Tuple[t.Any, ...]], - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], batch_start: int, batch_end: int, alias: str = "t", + source_columns: t.Optional[t.List[str]] = None, ) -> Query: return select_from_values_for_batch_range( values=values, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, batch_start=batch_start, batch_end=batch_end, alias=alias, + source_columns=source_columns, ) def _insert_overwrite_by_condition( self, table_name: TableName, source_queries: t.List[SourceQuery], - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, where: t.Optional[exp.Condition] = None, insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None, **kwargs: t.Any, @@ -1433,17 +1537,19 @@ def _insert_overwrite_by_condition( with self.transaction( condition=len(source_queries) > 0 or insert_overwrite_strategy.is_delete_insert ): - columns_to_types = columns_to_types or self.columns(table_name) + target_columns_to_types = target_columns_to_types or self.columns(table_name) for i, source_query in enumerate(source_queries): with source_query as query: - query = self._order_projections_and_filter(query, columns_to_types, where=where) + query = self._order_projections_and_filter( + query, target_columns_to_types, where=where + ) if i > 0 or insert_overwrite_strategy.is_delete_insert: if i == 0: self.delete_from(table_name, where=where or exp.true()) self._insert_append_query( table_name, query, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, order_projections=False, ) else: @@ -1451,7 +1557,7 @@ def _insert_overwrite_by_condition( query, table, columns=( - list(columns_to_types) + list(target_columns_to_types) if not insert_overwrite_strategy.is_replace_where else None ), @@ -1493,10 +1599,11 @@ def scd_type_2_by_time( updated_at_col: exp.Column, invalidate_hard_deletes: bool = True, updated_at_as_valid_from: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: self._scd_type_2( @@ -1509,10 +1616,11 @@ def scd_type_2_by_time( updated_at_col=updated_at_col, invalidate_hard_deletes=invalidate_hard_deletes, updated_at_as_valid_from=updated_at_as_valid_from, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, table_description=table_description, column_descriptions=column_descriptions, truncate=truncate, + source_columns=source_columns, **kwargs, ) @@ -1527,10 +1635,11 @@ def scd_type_2_by_column( check_columns: t.Union[exp.Star, t.Sequence[exp.Column]], invalidate_hard_deletes: bool = True, execution_time_as_valid_from: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: self._scd_type_2( @@ -1541,12 +1650,13 @@ def scd_type_2_by_column( valid_to_col=valid_to_col, execution_time=execution_time, check_columns=check_columns, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, invalidate_hard_deletes=invalidate_hard_deletes, execution_time_as_valid_from=execution_time_as_valid_from, table_description=table_description, column_descriptions=column_descriptions, truncate=truncate, + source_columns=source_columns, **kwargs, ) @@ -1563,10 +1673,11 @@ def _scd_type_2( check_columns: t.Optional[t.Union[exp.Star, t.Sequence[exp.Column]]] = None, updated_at_as_valid_from: bool = False, execution_time_as_valid_from: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: def remove_managed_columns( @@ -1578,24 +1689,28 @@ def remove_managed_columns( valid_from_name = valid_from_col.name valid_to_name = valid_to_col.name + target_columns_to_types = target_columns_to_types or self.columns(target_table) + if ( + valid_from_name not in target_columns_to_types + or valid_to_name not in target_columns_to_types + or not columns_to_types_all_known(target_columns_to_types) + ): + target_columns_to_types = self.columns(target_table) unmanaged_columns_to_types = ( - remove_managed_columns(columns_to_types) if columns_to_types else None + remove_managed_columns(target_columns_to_types) if target_columns_to_types else None ) source_queries, unmanaged_columns_to_types = self._get_source_queries_and_columns_to_types( - source_table, unmanaged_columns_to_types, target_table=target_table, batch_size=0 + source_table, + unmanaged_columns_to_types, + target_table=target_table, + batch_size=0, + source_columns=source_columns, ) - columns_to_types = columns_to_types or self.columns(target_table) updated_at_name = updated_at_col.name if updated_at_col else None - if ( - valid_from_name not in columns_to_types - or valid_to_name not in columns_to_types - or not columns_to_types_all_known(columns_to_types) - ): - columns_to_types = self.columns(target_table) - if not columns_to_types: + if not target_columns_to_types: raise SQLMeshError(f"Could not get columns_to_types. Does {target_table} exist?") unmanaged_columns_to_types = unmanaged_columns_to_types or remove_managed_columns( - columns_to_types + target_columns_to_types ) if not unique_key: raise SQLMeshError("unique_key must be provided for SCD Type 2") @@ -1611,15 +1726,15 @@ def remove_managed_columns( raise SQLMeshError( "Cannot use `execution_time_as_valid_from` without `check_columns` for SCD Type 2" ) - if updated_at_name and updated_at_name not in columns_to_types: + if updated_at_name and updated_at_name not in target_columns_to_types: raise SQLMeshError( f"Column {updated_at_name} not found in {target_table}. Table must contain an `updated_at` timestamp for SCD Type 2" ) - time_data_type = columns_to_types[valid_from_name] + time_data_type = target_columns_to_types[valid_from_name] select_source_columns: t.List[t.Union[str, exp.Alias]] = [ col for col in unmanaged_columns_to_types if col != updated_at_name ] - table_columns = [exp.column(c, quoted=True) for c in columns_to_types] + table_columns = [exp.column(c, quoted=True) for c in target_columns_to_types] if updated_at_name: select_source_columns.append( exp.cast(updated_at_col, time_data_type).as_(updated_at_col.this) # type: ignore @@ -1756,7 +1871,7 @@ def remove_managed_columns( with source_queries[0] as source_query: prefixed_columns_to_types = [] - for column in columns_to_types: + for column in target_columns_to_types: prefixed_col = exp.column(column).copy() prefixed_col.this.set("this", f"t_{prefixed_col.name}") prefixed_columns_to_types.append(prefixed_col) @@ -1800,7 +1915,7 @@ def remove_managed_columns( # Deleted records which can be used to determine `valid_from` for undeleted source records .with_( "deleted", - exp.select(*[exp.column(col, "static") for col in columns_to_types]) + exp.select(*[exp.column(col, "static") for col in target_columns_to_types]) .from_("static") .join( "latest", @@ -1835,7 +1950,7 @@ def remove_managed_columns( exp.column("_exists", table="source").as_("_exists"), *( exp.column(col, table="latest").as_(prefixed_columns_to_types[i].this) - for i, col in enumerate(columns_to_types) + for i, col in enumerate(target_columns_to_types) ), *( exp.column(col, table="source").as_(col) @@ -1860,7 +1975,7 @@ def remove_managed_columns( exp.column(col, table="latest").as_( prefixed_columns_to_types[i].this ) - for i, col in enumerate(columns_to_types) + for i, col in enumerate(target_columns_to_types) ), *( exp.column(col, table="source").as_(col) @@ -1929,7 +2044,7 @@ def remove_managed_columns( self.replace_query( target_table, self.ensure_nulls_for_unmatched_after_join(query), - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, table_description=table_description, column_descriptions=column_descriptions, **kwargs, @@ -1939,16 +2054,20 @@ def merge( self, target_table: TableName, source_table: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]], + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]], unique_key: t.Sequence[exp.Expression], when_matched: t.Optional[exp.Whens] = None, merge_filter: t.Optional[exp.Expression] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - source_table, columns_to_types, target_table=target_table + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + source_table, + target_columns_to_types, + target_table=target_table, + source_columns=source_columns, ) - columns_to_types = columns_to_types or self.columns(target_table) + target_columns_to_types = target_columns_to_types or self.columns(target_table) on = exp.and_( *( add_table(part, MERGE_TARGET_ALIAS).eq(add_table(part, MERGE_SOURCE_ALIAS)) @@ -1968,7 +2087,7 @@ def merge( exp.column(col, MERGE_TARGET_ALIAS).eq( exp.column(col, MERGE_SOURCE_ALIAS) ) - for col in columns_to_types + for col in target_columns_to_types ], ), ) @@ -1981,10 +2100,12 @@ def merge( matched=False, source=False, then=exp.Insert( - this=exp.Tuple(expressions=[exp.column(col) for col in columns_to_types]), + this=exp.Tuple( + expressions=[exp.column(col) for col in target_columns_to_types] + ), expression=exp.Tuple( expressions=[ - exp.column(col, MERGE_SOURCE_ALIAS) for col in columns_to_types + exp.column(col, MERGE_SOURCE_ALIAS) for col in target_columns_to_types ] ), ), @@ -2093,7 +2214,7 @@ def _native_df_to_pandas_df( """ import pandas as pd - if isinstance(query_or_df, (exp.Query, exp.DerivedTable, pd.DataFrame)): + if isinstance(query_or_df, (exp.Query, pd.DataFrame)): return query_or_df # EngineAdapter subclasses that have native DataFrame types should override this @@ -2268,7 +2389,8 @@ def temp_table( self, query_or_df: QueryOrDF, name: TableName = "diff", - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> t.Iterator[exp.Table]: """A context manager for working a temp table. @@ -2278,7 +2400,7 @@ def temp_table( Args: query_or_df: The query or df to create a temp table for. name: The base name of the temp table. - columns_to_types: A mapping between the column name and its data type. + target_columns_to_types: A mapping between the column name and its data type. Yields: The table expression @@ -2288,8 +2410,11 @@ def temp_table( if isinstance(name, exp.Table) and not name.catalog and name.db and self.default_catalog: name.set("catalog", exp.parse_identifier(self.default_catalog)) - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - query_or_df, columns_to_types=columns_to_types, target_table=name + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + query_or_df, + target_columns_to_types=target_columns_to_types, + target_table=name, + source_columns=source_columns, ) with self.transaction(): @@ -2299,7 +2424,7 @@ def temp_table( self._create_table_from_source_queries( table, source_queries, - columns_to_types, + target_columns_to_types, exists=True, table_description=None, column_descriptions=None, @@ -2327,7 +2452,7 @@ def _build_partitioned_by_exp( partitioned_by: t.List[exp.Expression], *, partition_interval_unit: t.Optional[IntervalUnit] = None, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, catalog_name: t.Optional[str] = None, **kwargs: t.Any, ) -> t.Optional[t.Union[exp.PartitionedByProperty, exp.Property]]: @@ -2349,7 +2474,7 @@ def _build_table_properties_exp( partition_interval_unit: t.Optional[IntervalUnit] = None, clustered_by: t.Optional[t.List[exp.Expression]] = None, table_properties: t.Optional[t.Dict[str, exp.Expression]] = None, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, table_kind: t.Optional[str] = None, **kwargs: t.Any, @@ -2450,12 +2575,12 @@ def _get_temp_table( def _order_projections_and_filter( self, query: Query, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], where: t.Optional[exp.Expression] = None, coerce_types: bool = False, ) -> Query: if not isinstance(query, exp.Query) or ( - not where and not coerce_types and query.named_selects == list(columns_to_types) + not where and not coerce_types and query.named_selects == list(target_columns_to_types) ): return query @@ -2463,12 +2588,12 @@ def _order_projections_and_filter( with_ = query.args.pop("with", None) select_exprs: t.List[exp.Expression] = [ - exp.column(c, quoted=True) for c in columns_to_types + exp.column(c, quoted=True) for c in target_columns_to_types ] - if coerce_types and columns_to_types_all_known(columns_to_types): + if coerce_types and columns_to_types_all_known(target_columns_to_types): select_exprs = [ exp.cast(select_exprs[i], col_tpe).as_(col, quoted=True) - for i, (col, col_tpe) in enumerate(columns_to_types.items()) + for i, (col, col_tpe) in enumerate(target_columns_to_types.items()) ] query = exp.select(*select_exprs).from_(query.subquery("_subquery", copy=False), copy=False) @@ -2512,23 +2637,30 @@ def _replace_by_key( self, target_table: TableName, source_table: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]], + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]], key: t.Sequence[exp.Expression], is_unique_key: bool, + source_columns: t.Optional[t.List[str]] = None, ) -> None: - if columns_to_types is None: - columns_to_types = self.columns(target_table) + if target_columns_to_types is None: + target_columns_to_types = self.columns(target_table) temp_table = self._get_temp_table(target_table) key_exp = exp.func("CONCAT_WS", "'__SQLMESH_DELIM__'", *key) if len(key) > 1 else key[0] - column_names = list(columns_to_types or []) + column_names = list(target_columns_to_types or []) with self.transaction(): - self.ctas(temp_table, source_table, columns_to_types=columns_to_types, exists=False) + self.ctas( + temp_table, + source_table, + target_columns_to_types=target_columns_to_types, + exists=False, + source_columns=source_columns, + ) try: delete_query = exp.select(key_exp).from_(temp_table) - insert_query = self._select_columns(columns_to_types).from_(temp_table) + insert_query = self._select_columns(target_columns_to_types).from_(temp_table) if not is_unique_key: delete_query = delete_query.distinct() else: @@ -2626,8 +2758,17 @@ def ping(self) -> None: self._connection_pool.close_cursor() @classmethod - def _select_columns(cls, columns: t.Iterable[str]) -> exp.Select: - return exp.select(*(exp.column(c, quoted=True) for c in columns)) + def _select_columns( + cls, columns: t.Iterable[str], source_columns: t.Optional[t.List[str]] = None + ) -> exp.Select: + return exp.select( + *( + exp.column(c, quoted=True) + if c in (source_columns or columns) + else exp.alias_(exp.Null(), c, quoted=True) + for c in columns + ) + ) def _check_identifier_length(self, expression: exp.Expression) -> None: if self.MAX_IDENTIFIER_LENGTH is None or not isinstance(expression, exp.DDL): diff --git a/sqlmesh/core/engine_adapter/base_postgres.py b/sqlmesh/core/engine_adapter/base_postgres.py index c1026c91df..cc394efd9e 100644 --- a/sqlmesh/core/engine_adapter/base_postgres.py +++ b/sqlmesh/core/engine_adapter/base_postgres.py @@ -91,13 +91,14 @@ def create_view( self, view_name: TableName, query_or_df: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, replace: bool = True, materialized: bool = False, materialized_properties: t.Optional[t.Dict[str, t.Any]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, view_properties: t.Optional[t.Dict[str, exp.Expression]] = None, + source_columns: t.Optional[t.List[str]] = None, **create_kwargs: t.Any, ) -> None: """ @@ -113,13 +114,14 @@ def create_view( super().create_view( view_name, query_or_df, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, replace=False, materialized=materialized, materialized_properties=materialized_properties, table_description=table_description, column_descriptions=column_descriptions, view_properties=view_properties, + source_columns=source_columns, **create_kwargs, ) diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 2de3f18c99..4fe50fdeef 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -22,7 +22,7 @@ ) from sqlmesh.core.node import IntervalUnit from sqlmesh.core.schema_diff import SchemaDiffer -from sqlmesh.utils import optional_import +from sqlmesh.utils import optional_import, get_source_columns_to_types from sqlmesh.utils.date import to_datetime from sqlmesh.utils.errors import SQLMeshError from sqlmesh.utils.pandas import columns_to_types_from_dtypes @@ -148,14 +148,19 @@ def catalog_support(self) -> CatalogSupport: def _df_to_source_queries( self, df: DF, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], batch_size: int, target_table: TableName, + source_columns: t.Optional[t.List[str]] = None, ) -> t.List[SourceQuery]: import pandas as pd + source_columns_to_types = get_source_columns_to_types( + target_columns_to_types, source_columns + ) + temp_bq_table = self.__get_temp_bq_table( - self._get_temp_table(target_table or "pandas"), columns_to_types + self._get_temp_table(target_table or "pandas"), source_columns_to_types ) temp_table = exp.table_( temp_bq_table.table_id, @@ -174,11 +179,13 @@ def query_factory() -> Query: assert isinstance(df, pd.DataFrame) self._db_call(self.client.create_table, table=temp_bq_table, exists_ok=False) result = self.__load_pandas_to_table( - temp_bq_table, df, columns_to_types, replace=False + temp_bq_table, df, source_columns_to_types, replace=False ) if result.errors: raise SQLMeshError(result.errors) - return self._select_columns(columns_to_types).from_(temp_table) + return exp.select( + *self._casted_columns(target_columns_to_types, source_columns=source_columns) + ).from_(temp_table) return [ SourceQuery( @@ -673,7 +680,8 @@ def insert_overwrite_by_partition( table_name: TableName, query_or_df: QueryOrDF, partitioned_by: t.List[exp.Expression], - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, ) -> None: if len(partitioned_by) != 1: raise SQLMeshError( @@ -695,15 +703,20 @@ def insert_overwrite_by_partition( with ( self.session({}), self.temp_table( - query_or_df, name=table_name, partitioned_by=partitioned_by + query_or_df, + name=table_name, + partitioned_by=partitioned_by, + source_columns=source_columns, ) as temp_table_name, ): - if columns_to_types is None or columns_to_types[ + if target_columns_to_types is None or target_columns_to_types[ partition_column.name ] == exp.DataType.build("unknown"): - columns_to_types = self.columns(table_name) + target_columns_to_types = self.columns(table_name) - partition_type_sql = columns_to_types[partition_column.name].sql(dialect=self.dialect) + partition_type_sql = target_columns_to_types[partition_column.name].sql( + dialect=self.dialect + ) select_array_agg_partitions = select_partitions_expr( temp_table_name.db, @@ -723,7 +736,7 @@ def insert_overwrite_by_partition( self._insert_overwrite_by_condition( table_name, [SourceQuery(query_factory=lambda: exp.select("*").from_(temp_table_name))], - columns_to_types, + target_columns_to_types, where=where, ) @@ -815,7 +828,7 @@ def _build_partitioned_by_exp( partitioned_by: t.List[exp.Expression], *, partition_interval_unit: t.Optional[IntervalUnit] = None, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, **kwargs: t.Any, ) -> t.Optional[exp.PartitionedByProperty]: if len(partitioned_by) > 1: @@ -827,7 +840,7 @@ def _build_partitioned_by_exp( and partition_interval_unit is not None and not partition_interval_unit.is_minute ): - column_type: t.Optional[exp.DataType] = (columns_to_types or {}).get(this.name) + column_type: t.Optional[exp.DataType] = (target_columns_to_types or {}).get(this.name) if column_type == exp.DataType.build( "date", dialect=self.dialect @@ -862,7 +875,7 @@ def _build_table_properties_exp( partition_interval_unit: t.Optional[IntervalUnit] = None, clustered_by: t.Optional[t.List[exp.Expression]] = None, table_properties: t.Optional[t.Dict[str, exp.Expression]] = None, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, table_kind: t.Optional[str] = None, **kwargs: t.Any, @@ -873,7 +886,7 @@ def _build_table_properties_exp( partitioned_by_prop := self._build_partitioned_by_exp( partitioned_by, partition_interval_unit=partition_interval_unit, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, ) ): properties.append(partitioned_by_prop) @@ -1018,12 +1031,12 @@ def _build_create_comment_column_exp( def create_state_table( self, table_name: str, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], primary_key: t.Optional[t.Tuple[str, ...]] = None, ) -> None: self.create_table( table_name, - columns_to_types, + target_columns_to_types, ) def _db_call(self, func: t.Callable[..., t.Any], *args: t.Any, **kwargs: t.Any) -> t.Any: @@ -1204,27 +1217,39 @@ def _normalize_nested_value(self, col: exp.Expression) -> exp.Expression: @t.overload def _columns_to_types( - self, query_or_df: DF, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Dict[str, exp.DataType]: ... + self, + query_or_df: DF, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Dict[str, exp.DataType], t.List[str]]: ... @t.overload def _columns_to_types( - self, query_or_df: Query, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Optional[t.Dict[str, exp.DataType]]: ... + self, + query_or_df: Query, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Optional[t.Dict[str, exp.DataType]], t.Optional[t.List[str]]]: ... def _columns_to_types( - self, query_or_df: QueryOrDF, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Optional[t.Dict[str, exp.DataType]]: + self, + query_or_df: QueryOrDF, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Optional[t.Dict[str, exp.DataType]], t.Optional[t.List[str]]]: if ( - not columns_to_types + not target_columns_to_types and bigframes and isinstance(query_or_df, bigframes.dataframe.DataFrame) ): # using dry_run=True attempts to prevent the DataFrame from being materialized just to read the column types from it dtypes = query_or_df.to_pandas(dry_run=True).columnDtypes - return columns_to_types_from_dtypes(dtypes.items()) + target_columns_to_types = columns_to_types_from_dtypes(dtypes.items()) + return target_columns_to_types, list(source_columns or target_columns_to_types) - return super()._columns_to_types(query_or_df, columns_to_types) + return super()._columns_to_types( + query_or_df, target_columns_to_types, source_columns=source_columns + ) def _native_df_to_pandas_df( self, diff --git a/sqlmesh/core/engine_adapter/clickhouse.py b/sqlmesh/core/engine_adapter/clickhouse.py index fb515b7291..5ac4e9b152 100644 --- a/sqlmesh/core/engine_adapter/clickhouse.py +++ b/sqlmesh/core/engine_adapter/clickhouse.py @@ -16,6 +16,7 @@ InsertOverwriteStrategy, ) from sqlmesh.core.schema_diff import SchemaDiffer +from sqlmesh.utils import get_source_columns_to_types if t.TYPE_CHECKING: import pandas as pd @@ -89,12 +90,16 @@ def _fetch_native_df( def _df_to_source_queries( self, df: DF, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], batch_size: int, target_table: TableName, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> t.List[SourceQuery]: temp_table = self._get_temp_table(target_table, **kwargs) + source_columns_to_types = get_source_columns_to_types( + target_columns_to_types, source_columns + ) def query_factory() -> Query: # It is possible for the factory to be called multiple times and if so then the temp table will already @@ -102,12 +107,17 @@ def query_factory() -> Query: # as later calls. if not self.table_exists(temp_table): self.create_table( - temp_table, columns_to_types, storage_format=exp.var("MergeTree"), **kwargs + temp_table, + source_columns_to_types, + storage_format=exp.var("MergeTree"), + **kwargs, ) self.cursor.client.insert_df(temp_table.sql(dialect=self.dialect), df=df) - return exp.select(*self._casted_columns(columns_to_types)).from_(temp_table) + return exp.select(*self._casted_columns(target_columns_to_types, source_columns)).from_( + temp_table + ) return [ SourceQuery( @@ -181,7 +191,7 @@ def _insert_overwrite_by_condition( self, table_name: TableName, source_queries: t.List[SourceQuery], - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, where: t.Optional[exp.Condition] = None, insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None, **kwargs: t.Any, @@ -196,7 +206,7 @@ def _insert_overwrite_by_condition( Args: table_name: Name of target table source_queries: Source queries returning records to insert - columns_to_types: Column names and data types of target table + target_columns_to_types: Column names and data types of target table where: SQLGlot expression determining which target table rows should be overwritten insert_overwrite_strategy_override: Not used by Clickhouse kwargs: @@ -210,7 +220,7 @@ def _insert_overwrite_by_condition( Side effects only: execution of insert-overwrite operation. """ target_table = exp.to_table(table_name) - columns_to_types = columns_to_types or self.columns(target_table) + target_columns_to_types = target_columns_to_types or self.columns(target_table) temp_table = self._get_temp_table(target_table) self._create_table_like(temp_table, target_table) @@ -229,11 +239,13 @@ def _insert_overwrite_by_condition( if dynamic_key and dynamic_key_unique: query = query.distinct(*dynamic_key) # type: ignore - query = self._order_projections_and_filter(query, columns_to_types, where=where) + query = self._order_projections_and_filter( + query, target_columns_to_types, where=where + ) self._insert_append_query( temp_table, query, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, order_projections=False, ) @@ -259,7 +271,7 @@ def _insert_overwrite_by_condition( if where: # identify existing records to keep by inverting the delete `where` clause existing_records_insert_exp = exp.insert( - self._select_columns(columns_to_types) + self._select_columns(target_columns_to_types) .from_(target_table) .where(exp.paren(expression=where).not_()), temp_table, @@ -400,12 +412,16 @@ def _replace_by_key( self, target_table: TableName, source_table: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]], + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]], key: t.Sequence[exp.Expression], is_unique_key: bool, + source_columns: t.Optional[t.List[str]] = None, ) -> None: - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - source_table, columns_to_types, target_table=target_table + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + source_table, + target_columns_to_types, + target_table=target_table, + source_columns=source_columns, ) key_exp = exp.func("CONCAT_WS", "'__SQLMESH_DELIM__'", *key) if len(key) > 1 else key[0] @@ -413,7 +429,7 @@ def _replace_by_key( self._insert_overwrite_by_condition( target_table, source_queries, - columns_to_types, + target_columns_to_types, dynamic_key=key, dynamic_key_exp=key_exp, dynamic_key_unique=is_unique_key, @@ -424,14 +440,18 @@ def insert_overwrite_by_partition( table_name: TableName, query_or_df: QueryOrDF, partitioned_by: t.List[exp.Expression], - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, ) -> None: - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - query_or_df, columns_to_types, target_table=table_name + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + query_or_df, + target_columns_to_types, + target_table=table_name, + source_columns=source_columns, ) self._insert_overwrite_by_condition( - table_name, source_queries, columns_to_types, keep_existing_partition_rows=False + table_name, source_queries, target_columns_to_types, keep_existing_partition_rows=False ) def _create_table_like( @@ -465,7 +485,7 @@ def _create_table( expression: t.Optional[exp.Expression], exists: bool = True, replace: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, @@ -491,16 +511,16 @@ def _create_table( for coldef in table_name_or_schema.expressions: if coldef.name in partition_cols: coldef.kind.set("nullable", False) - if columns_to_types: + if target_columns_to_types: for col in partition_cols: - columns_to_types[col].set("nullable", False) + target_columns_to_types[col].set("nullable", False) super()._create_table( table_name_or_schema, expression, exists, replace, - columns_to_types, + target_columns_to_types, table_description, column_descriptions, table_kind, @@ -528,7 +548,7 @@ def _create_table( self._insert_append_query( table_name, expression, # type: ignore - columns_to_types or self.columns(table_name), + target_columns_to_types or self.columns(table_name), ) def _exchange_tables( @@ -708,7 +728,7 @@ def _build_table_properties_exp( partition_interval_unit: t.Optional[IntervalUnit] = None, clustered_by: t.Optional[t.List[exp.Expression]] = None, table_properties: t.Optional[t.Dict[str, exp.Expression]] = None, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, table_kind: t.Optional[str] = None, empty_ctas: bool = False, diff --git a/sqlmesh/core/engine_adapter/databricks.py b/sqlmesh/core/engine_adapter/databricks.py index 9d35726a32..4e352b27ef 100644 --- a/sqlmesh/core/engine_adapter/databricks.py +++ b/sqlmesh/core/engine_adapter/databricks.py @@ -161,25 +161,26 @@ def _end_session(self) -> None: def _df_to_source_queries( self, df: DF, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], batch_size: int, target_table: TableName, + source_columns: t.Optional[t.List[str]] = None, ) -> t.List[SourceQuery]: if not self._use_spark_session: return super(SparkEngineAdapter, self)._df_to_source_queries( - df, columns_to_types, batch_size, target_table + df, target_columns_to_types, batch_size, target_table, source_columns=source_columns ) - df = self._ensure_pyspark_df(df, columns_to_types) + pyspark_df = self._ensure_pyspark_df( + df, target_columns_to_types, source_columns=source_columns + ) def query_factory() -> Query: temp_table = self._get_temp_table(target_table or "spark", table_only=True) - df.createOrReplaceTempView(temp_table.sql(dialect=self.dialect)) + pyspark_df.createOrReplaceTempView(temp_table.sql(dialect=self.dialect)) self._connection_pool.set_attribute("use_spark_engine_adapter", True) - return exp.select(*self._casted_columns(columns_to_types)).from_(temp_table) + return exp.select(*self._select_columns(target_columns_to_types)).from_(temp_table) - if self._use_spark_session: - return [SourceQuery(query_factory=query_factory)] - return super()._df_to_source_queries(df, columns_to_types, batch_size, target_table) + return [SourceQuery(query_factory=query_factory)] def _fetch_native_df( self, query: t.Union[exp.Expression, str], quote_identifiers: bool = False @@ -337,7 +338,7 @@ def _build_table_properties_exp( partition_interval_unit: t.Optional[IntervalUnit] = None, clustered_by: t.Optional[t.List[exp.Expression]] = None, table_properties: t.Optional[t.Dict[str, exp.Expression]] = None, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, table_kind: t.Optional[str] = None, **kwargs: t.Any, @@ -350,7 +351,7 @@ def _build_table_properties_exp( partition_interval_unit=partition_interval_unit, clustered_by=clustered_by, table_properties=table_properties, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, table_description=table_description, table_kind=table_kind, ) diff --git a/sqlmesh/core/engine_adapter/duckdb.py b/sqlmesh/core/engine_adapter/duckdb.py index 00be5f426a..d90a4ed736 100644 --- a/sqlmesh/core/engine_adapter/duckdb.py +++ b/sqlmesh/core/engine_adapter/duckdb.py @@ -61,20 +61,23 @@ def _drop_catalog(self, catalog_name: exp.Identifier) -> None: def _df_to_source_queries( self, df: DF, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], batch_size: int, target_table: TableName, + source_columns: t.Optional[t.List[str]] = None, ) -> t.List[SourceQuery]: temp_table = self._get_temp_table(target_table) temp_table_sql = ( - exp.select(*self._casted_columns(columns_to_types)) + exp.select(*self._casted_columns(target_columns_to_types, source_columns)) .from_("df") .sql(dialect=self.dialect) ) self.cursor.sql(f"CREATE TABLE {temp_table} AS {temp_table_sql}") return [ SourceQuery( - query_factory=lambda: self._select_columns(columns_to_types).from_(temp_table), # type: ignore + query_factory=lambda: self._select_columns(target_columns_to_types).from_( + temp_table + ), # type: ignore cleanup_func=lambda: self.drop_table(temp_table), ) ] @@ -149,7 +152,7 @@ def _create_table( expression: t.Optional[exp.Expression], exists: bool = True, replace: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, @@ -172,7 +175,7 @@ def _create_table( expression, exists, replace, - columns_to_types, + target_columns_to_types, table_description, column_descriptions, table_kind, diff --git a/sqlmesh/core/engine_adapter/mixins.py b/sqlmesh/core/engine_adapter/mixins.py index 5ca1f200d9..12c9bfc603 100644 --- a/sqlmesh/core/engine_adapter/mixins.py +++ b/sqlmesh/core/engine_adapter/mixins.py @@ -28,20 +28,22 @@ def merge( self, target_table: TableName, source_table: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]], + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]], unique_key: t.Sequence[exp.Expression], when_matched: t.Optional[exp.Whens] = None, merge_filter: t.Optional[exp.Expression] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: logical_merge( self, target_table, source_table, - columns_to_types, + target_columns_to_types, unique_key, when_matched=when_matched, merge_filter=merge_filter, + source_columns=source_columns, ) @@ -75,7 +77,7 @@ def _insert_overwrite_by_condition( self, table_name: TableName, source_queries: t.List[SourceQuery], - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, where: t.Optional[exp.Condition] = None, insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None, **kwargs: t.Any, @@ -85,11 +87,13 @@ def _insert_overwrite_by_condition( doing an "INSERT OVERWRITE" using a Merge expression but with the predicate being `False`. """ - columns_to_types = columns_to_types or self.columns(table_name) + target_columns_to_types = target_columns_to_types or self.columns(table_name) for source_query in source_queries: with source_query as query: - query = self._order_projections_and_filter(query, columns_to_types, where=where) - columns = [exp.column(col) for col in columns_to_types] + query = self._order_projections_and_filter( + query, target_columns_to_types, where=where + ) + columns = [exp.column(col) for col in target_columns_to_types] when_not_matched_by_source = exp.When( matched=False, source=True, @@ -157,7 +161,7 @@ def _build_table_properties_exp( partition_interval_unit: t.Optional[IntervalUnit] = None, clustered_by: t.Optional[t.List[exp.Expression]] = None, table_properties: t.Optional[t.Dict[str, exp.Expression]] = None, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, table_kind: t.Optional[str] = None, **kwargs: t.Any, @@ -276,7 +280,7 @@ def _build_create_table_exp( expression: t.Optional[exp.Expression], exists: bool = True, replace: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, table_kind: t.Optional[str] = None, **kwargs: t.Any, @@ -286,7 +290,7 @@ def _build_create_table_exp( expression=expression, exists=exists, replace=replace, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, table_description=table_description, table_kind=table_kind, **kwargs, @@ -326,7 +330,7 @@ def _build_create_table_exp( None, exists=exists, replace=replace, - columns_to_types=columns_to_types_from_view, + target_columns_to_types=columns_to_types_from_view, table_description=table_description, **kwargs, ) @@ -357,9 +361,15 @@ def _parse_clustering_key(self, clustering_key: t.Optional[str]) -> t.List[exp.E return parsed_cluster_key.expressions or [parsed_cluster_key.this] def get_alter_expressions( - self, current_table_name: TableName, target_table_name: TableName + self, + current_table_name: TableName, + target_table_name: TableName, + *, + ignore_destructive: bool = False, ) -> t.List[exp.Alter]: - expressions = super().get_alter_expressions(current_table_name, target_table_name) + expressions = super().get_alter_expressions( + current_table_name, target_table_name, ignore_destructive=ignore_destructive + ) # check for a change in clustering current_table = exp.to_table(current_table_name) @@ -412,10 +422,11 @@ def logical_merge( engine_adapter: EngineAdapter, target_table: TableName, source_table: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]], + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]], unique_key: t.Sequence[exp.Expression], when_matched: t.Optional[exp.Whens] = None, merge_filter: t.Optional[exp.Expression] = None, + source_columns: t.Optional[t.List[str]] = None, ) -> None: """ Merge implementation for engine adapters that do not support merge natively. @@ -434,7 +445,12 @@ def logical_merge( ) engine_adapter._replace_by_key( - target_table, source_table, columns_to_types, unique_key, is_unique_key=True + target_table, + source_table, + target_columns_to_types, + unique_key, + is_unique_key=True, + source_columns=source_columns, ) diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index 112193073d..3a43d539a9 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -31,6 +31,7 @@ set_catalog, ) from sqlmesh.core.schema_diff import SchemaDiffer +from sqlmesh.utils import get_source_columns_to_types if t.TYPE_CHECKING: from sqlmesh.core._typing import SchemaName, TableName @@ -194,18 +195,22 @@ def merge( self, target_table: TableName, source_table: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]], + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]], unique_key: t.Sequence[exp.Expression], when_matched: t.Optional[exp.Whens] = None, merge_filter: t.Optional[exp.Expression] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: mssql_merge_exists = kwargs.get("physical_properties", {}).get("mssql_merge_exists") - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - source_table, columns_to_types, target_table=target_table + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + source_table, + target_columns_to_types, + target_table=target_table, + source_columns=source_columns, ) - columns_to_types = columns_to_types or self.columns(target_table) + target_columns_to_types = target_columns_to_types or self.columns(target_table) on = exp.and_( *( add_table(part, MERGE_TARGET_ALIAS).eq(add_table(part, MERGE_SOURCE_ALIAS)) @@ -218,7 +223,9 @@ def merge( match_expressions = [] if not when_matched: unique_key_names = [y.name for y in unique_key] - columns_to_types_no_keys = [c for c in columns_to_types if c not in unique_key_names] + columns_to_types_no_keys = [ + c for c in target_columns_to_types if c not in unique_key_names + ] target_columns_no_keys = [ exp.column(c, MERGE_TARGET_ALIAS) for c in columns_to_types_no_keys @@ -261,10 +268,12 @@ def merge( matched=False, source=False, then=exp.Insert( - this=exp.Tuple(expressions=[exp.column(col) for col in columns_to_types]), + this=exp.Tuple( + expressions=[exp.column(col) for col in target_columns_to_types] + ), expression=exp.Tuple( expressions=[ - exp.column(col, MERGE_SOURCE_ALIAS) for col in columns_to_types + exp.column(col, MERGE_SOURCE_ALIAS) for col in target_columns_to_types ] ), ), @@ -303,9 +312,10 @@ def _convert_df_datetime(self, df: DF, columns_to_types: t.Dict[str, exp.DataTyp def _df_to_source_queries( self, df: DF, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], batch_size: int, target_table: TableName, + source_columns: t.Optional[t.List[str]] = None, ) -> t.List[SourceQuery]: import pandas as pd import numpy as np @@ -315,25 +325,31 @@ def _df_to_source_queries( # Return the superclass implementation if the connection pool doesn't support bulk_copy if not hasattr(self._connection_pool.get(), "bulk_copy"): - return super()._df_to_source_queries(df, columns_to_types, batch_size, target_table) + return super()._df_to_source_queries( + df, target_columns_to_types, batch_size, target_table, source_columns=source_columns + ) def query_factory() -> Query: # It is possible for the factory to be called multiple times and if so then the temp table will already # be created so we skip creating again. This means we are assuming the first call is the same result # as later calls. if not self.table_exists(temp_table): - columns_to_types_create = columns_to_types.copy() + source_columns_to_types = get_source_columns_to_types( + target_columns_to_types, source_columns + ) ordered_df = df[ - list(columns_to_types_create) + list(source_columns_to_types) ] # reorder DataFrame so it matches columns_to_types - self._convert_df_datetime(ordered_df, columns_to_types_create) - self.create_table(temp_table, columns_to_types_create) + self._convert_df_datetime(ordered_df, source_columns_to_types) + self.create_table(temp_table, source_columns_to_types) rows: t.List[t.Tuple[t.Any, ...]] = list( ordered_df.replace({np.nan: None}).itertuples(index=False, name=None) # type: ignore ) conn = self._connection_pool.get() conn.bulk_copy(temp_table.sql(dialect=self.dialect), rows) - return exp.select(*self._casted_columns(columns_to_types)).from_(temp_table) # type: ignore + return exp.select( + *self._casted_columns(target_columns_to_types, source_columns=source_columns) + ).from_(temp_table) # type: ignore return [ SourceQuery( @@ -393,7 +409,7 @@ def _insert_overwrite_by_condition( self, table_name: TableName, source_queries: t.List[SourceQuery], - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, where: t.Optional[exp.Condition] = None, insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None, **kwargs: t.Any, @@ -405,7 +421,7 @@ def _insert_overwrite_by_condition( self, table_name=table_name, source_queries=source_queries, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, where=where, insert_overwrite_strategy_override=InsertOverwriteStrategy.DELETE_INSERT, **kwargs, @@ -415,7 +431,7 @@ def _insert_overwrite_by_condition( return super()._insert_overwrite_by_condition( table_name=table_name, source_queries=source_queries, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, where=where, insert_overwrite_strategy_override=insert_overwrite_strategy_override, **kwargs, diff --git a/sqlmesh/core/engine_adapter/postgres.py b/sqlmesh/core/engine_adapter/postgres.py index a736f5553b..a1ff46e9ad 100644 --- a/sqlmesh/core/engine_adapter/postgres.py +++ b/sqlmesh/core/engine_adapter/postgres.py @@ -106,10 +106,11 @@ def merge( self, target_table: TableName, source_table: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]], + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]], unique_key: t.Sequence[exp.Expression], when_matched: t.Optional[exp.Whens] = None, merge_filter: t.Optional[exp.Expression] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: # Merge isn't supported until Postgres 15 @@ -118,10 +119,11 @@ def merge( merge_impl( # type: ignore target_table, source_table, - columns_to_types, + target_columns_to_types, unique_key, when_matched=when_matched, merge_filter=merge_filter, + source_columns=source_columns, ) @cached_property diff --git a/sqlmesh/core/engine_adapter/redshift.py b/sqlmesh/core/engine_adapter/redshift.py index 829cdf3686..2589ef960e 100644 --- a/sqlmesh/core/engine_adapter/redshift.py +++ b/sqlmesh/core/engine_adapter/redshift.py @@ -168,7 +168,7 @@ def _create_table_from_source_queries( self, table_name: TableName, source_queries: t.List[SourceQuery], - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, exists: bool = True, replace: bool = False, table_description: t.Optional[str] = None, @@ -186,7 +186,7 @@ def _create_table_from_source_queries( return super()._create_table_from_source_queries( table_name, source_queries, - columns_to_types, + target_columns_to_types, exists, table_description=table_description, column_descriptions=column_descriptions, @@ -207,13 +207,14 @@ def create_view( self, view_name: TableName, query_or_df: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, replace: bool = True, materialized: bool = False, materialized_properties: t.Optional[t.Dict[str, t.Any]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, view_properties: t.Optional[t.Dict[str, exp.Expression]] = None, + source_columns: t.Optional[t.List[str]] = None, **create_kwargs: t.Any, ) -> None: """ @@ -232,7 +233,7 @@ def create_view( return super().create_view( view_name, query_or_df, - columns_to_types, + target_columns_to_types, replace, materialized, materialized_properties, @@ -240,6 +241,7 @@ def create_view( column_descriptions=column_descriptions, no_schema_binding=no_schema_binding, view_properties=view_properties, + source_columns=source_columns, **create_kwargs, ) @@ -247,9 +249,10 @@ def replace_query( self, table_name: TableName, query_or_df: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: """ @@ -271,28 +274,32 @@ def replace_query( return super().replace_query( table_name, query_or_df, - columns_to_types, + target_columns_to_types, table_description, column_descriptions, + source_columns=source_columns, **kwargs, ) - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - query_or_df, columns_to_types, target_table=table_name + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + query_or_df, + target_columns_to_types, + target_table=table_name, + source_columns=source_columns, ) - columns_to_types = columns_to_types or self.columns(table_name) + target_columns_to_types = target_columns_to_types or self.columns(table_name) target_table = exp.to_table(table_name) with self.transaction(): temp_table = self._get_temp_table(target_table) old_table = self._get_temp_table(target_table) self.create_table( temp_table, - columns_to_types, + target_columns_to_types, exists=False, table_description=table_description, column_descriptions=column_descriptions, **kwargs, ) - self._insert_append_source_queries(temp_table, source_queries, columns_to_types) + self._insert_append_source_queries(temp_table, source_queries, target_columns_to_types) self.rename_table(target_table, old_table) self.rename_table(temp_table, target_table) self.drop_table(old_table) @@ -354,10 +361,11 @@ def merge( self, target_table: TableName, source_table: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]], + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]], unique_key: t.Sequence[exp.Expression], when_matched: t.Optional[exp.Whens] = None, merge_filter: t.Optional[exp.Expression] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: if self.enable_merge: @@ -365,20 +373,22 @@ def merge( super().merge( target_table=target_table, source_table=source_table, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, unique_key=unique_key, when_matched=when_matched, merge_filter=merge_filter, + source_columns=source_columns, ) else: logical_merge( self, target_table, source_table, - columns_to_types, + target_columns_to_types, unique_key, when_matched=when_matched, merge_filter=merge_filter, + source_columns=source_columns, ) def _merge( diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index 71ffc10f48..f6fc32cc0a 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -24,7 +24,7 @@ set_catalog, ) from sqlmesh.core.schema_diff import SchemaDiffer -from sqlmesh.utils import optional_import +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 @@ -162,7 +162,7 @@ def _create_table( expression: t.Optional[exp.Expression], exists: bool = True, replace: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, @@ -181,7 +181,7 @@ def _create_table( expression=expression, exists=exists, replace=replace, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, table_description=table_description, column_descriptions=column_descriptions, table_kind=table_kind, @@ -192,12 +192,13 @@ def create_managed_table( self, table_name: TableName, query: Query, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, partitioned_by: t.Optional[t.List[exp.Expression]] = None, clustered_by: t.Optional[t.List[exp.Expression]] = None, table_properties: t.Optional[t.Dict[str, exp.Expression]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: target_table = exp.to_table(table_name) @@ -217,14 +218,14 @@ def create_managed_table( "`target_lag` must be specified in the model physical_properties for a Snowflake Dynamic Table" ) - source_queries, columns_to_types = self._get_source_queries_and_columns_to_types( - query, columns_to_types, target_table=target_table + source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( + query, target_columns_to_types, target_table=target_table, source_columns=source_columns ) self._create_table_from_source_queries( target_table, source_queries, - columns_to_types, + target_columns_to_types, replace=self.SUPPORTS_REPLACE_TABLE, partitioned_by=partitioned_by, clustered_by=clustered_by, @@ -239,13 +240,14 @@ def create_view( self, view_name: TableName, query_or_df: QueryOrDF, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, replace: bool = True, materialized: bool = False, materialized_properties: t.Optional[t.Dict[str, t.Any]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, view_properties: t.Optional[t.Dict[str, exp.Expression]] = None, + source_columns: t.Optional[t.List[str]] = None, **create_kwargs: t.Any, ) -> None: properties = create_kwargs.pop("properties", None) @@ -257,7 +259,7 @@ def create_view( super().create_view( view_name=view_name, query_or_df=query_or_df, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, replace=replace, materialized=materialized, materialized_properties=materialized_properties, @@ -265,6 +267,7 @@ def create_view( column_descriptions=column_descriptions, view_properties=view_properties, properties=properties, + source_columns=source_columns, **create_kwargs, ) @@ -280,7 +283,7 @@ def _build_table_properties_exp( partition_interval_unit: t.Optional[IntervalUnit] = None, clustered_by: t.Optional[t.List[exp.Expression]] = None, table_properties: t.Optional[t.Dict[str, exp.Expression]] = None, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, table_kind: t.Optional[str] = None, **kwargs: t.Any, @@ -321,13 +324,18 @@ def _build_table_properties_exp( def _df_to_source_queries( self, df: DF, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], batch_size: int, target_table: TableName, + source_columns: t.Optional[t.List[str]] = None, ) -> t.List[SourceQuery]: import pandas as pd from pandas.api.types import is_datetime64_any_dtype + source_columns_to_types = get_source_columns_to_types( + target_columns_to_types, source_columns + ) + temp_table = self._get_temp_table( target_table or "pandas", quoted=False ) # write_pandas() re-quotes everything without checking if its already quoted @@ -358,7 +366,7 @@ def query_factory() -> Query: local_df = df.rename( { col: exp.to_identifier(col).sql(dialect=self.dialect, identify=True) - for col in columns_to_types + for col in source_columns_to_types } ) # type: ignore local_df.createOrReplaceTempView( @@ -376,7 +384,7 @@ def query_factory() -> Query: self.set_current_schema(schema) # See: https://stackoverflow.com/a/75627721 - for column, kind in columns_to_types.items(): + for column, kind in source_columns_to_types.items(): if is_datetime64_any_dtype(df.dtypes[column]): if kind.is_type("date"): # type: ignore df[column] = pd.to_datetime(df[column]).dt.date # type: ignore @@ -392,7 +400,7 @@ def query_factory() -> Query: # create the table first using our usual method ensure the column datatypes match what we parsed with sqlglot # otherwise we would be trusting `write_pandas()` from the snowflake lib to do this correctly - self.create_table(temp_table, columns_to_types, table_kind="TEMPORARY TABLE") + self.create_table(temp_table, source_columns_to_types, table_kind="TEMPORARY TABLE") write_pandas( self._connection_pool.get(), @@ -409,7 +417,9 @@ def query_factory() -> Query: f"Unknown dataframe type: {type(df)} for {target_table}. Expecting pandas or snowpark." ) - return exp.select(*self._casted_columns(columns_to_types)).from_(temp_table) + return exp.select( + *self._casted_columns(target_columns_to_types, source_columns=source_columns) + ).from_(temp_table) def cleanup() -> None: if is_snowpark_dataframe: @@ -616,21 +626,35 @@ def clone_table( @t.overload def _columns_to_types( - self, query_or_df: DF, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Dict[str, exp.DataType]: ... + self, + query_or_df: DF, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Dict[str, exp.DataType], t.List[str]]: ... @t.overload def _columns_to_types( - self, query_or_df: Query, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Optional[t.Dict[str, exp.DataType]]: ... + self, + query_or_df: Query, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Optional[t.Dict[str, exp.DataType]], t.Optional[t.List[str]]]: ... def _columns_to_types( - self, query_or_df: QueryOrDF, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Optional[t.Dict[str, exp.DataType]]: - if not columns_to_types and snowpark and isinstance(query_or_df, snowpark.DataFrame): - return columns_to_types_from_dtypes(query_or_df.sample(n=1).to_pandas().dtypes.items()) + self, + query_or_df: QueryOrDF, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Optional[t.Dict[str, exp.DataType]], t.Optional[t.List[str]]]: + if not target_columns_to_types and snowpark and isinstance(query_or_df, snowpark.DataFrame): + target_columns_to_types = columns_to_types_from_dtypes( + query_or_df.sample(n=1).to_pandas().dtypes.items() + ) + return target_columns_to_types, list(source_columns or target_columns_to_types) - return super()._columns_to_types(query_or_df, columns_to_types) + return super()._columns_to_types( + query_or_df, target_columns_to_types, source_columns=source_columns + ) def close(self) -> t.Any: if snowpark_session := self._connection_pool.get_attribute(self.SNOWPARK): diff --git a/sqlmesh/core/engine_adapter/spark.py b/sqlmesh/core/engine_adapter/spark.py index 799d46a9c5..5e37ba075e 100644 --- a/sqlmesh/core/engine_adapter/spark.py +++ b/sqlmesh/core/engine_adapter/spark.py @@ -23,7 +23,7 @@ set_catalog, ) from sqlmesh.core.schema_diff import SchemaDiffer -from sqlmesh.utils import classproperty +from sqlmesh.utils import classproperty, get_source_columns_to_types from sqlmesh.utils.errors import SQLMeshError if t.TYPE_CHECKING: @@ -240,61 +240,91 @@ def try_get_pandas_df(cls, value: t.Any) -> t.Optional[pd.DataFrame]: @t.overload def _columns_to_types( - self, query_or_df: DF, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Dict[str, exp.DataType]: ... + self, + query_or_df: DF, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Dict[str, exp.DataType], t.List[str]]: ... @t.overload def _columns_to_types( - self, query_or_df: Query, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Optional[t.Dict[str, exp.DataType]]: ... + self, + query_or_df: Query, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Optional[t.Dict[str, exp.DataType]], t.Optional[t.List[str]]]: ... def _columns_to_types( - self, query_or_df: QueryOrDF, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None - ) -> t.Optional[t.Dict[str, exp.DataType]]: - if columns_to_types: - return columns_to_types + self, + query_or_df: QueryOrDF, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, + ) -> t.Tuple[t.Optional[t.Dict[str, exp.DataType]], t.Optional[t.List[str]]]: + if target_columns_to_types: + return target_columns_to_types, list(source_columns or target_columns_to_types) if self.is_pyspark_df(query_or_df): from pyspark.sql import DataFrame - return self.spark_to_sqlglot_types(t.cast(DataFrame, query_or_df).schema) - return super()._columns_to_types(query_or_df, columns_to_types) + target_columns_to_types = self.spark_to_sqlglot_types( + t.cast(DataFrame, query_or_df).schema + ) + return target_columns_to_types, list(source_columns or target_columns_to_types) + return super()._columns_to_types( + query_or_df, target_columns_to_types, source_columns=source_columns + ) def _df_to_source_queries( self, df: DF, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], batch_size: int, target_table: TableName, + source_columns: t.Optional[t.List[str]] = None, ) -> t.List[SourceQuery]: - df = self._ensure_pyspark_df(df, columns_to_types) + df = self._ensure_pyspark_df(df, target_columns_to_types, source_columns=source_columns) def query_factory() -> Query: temp_table = self._get_temp_table(target_table or "spark", table_only=True) df.createOrReplaceGlobalTempView(temp_table.sql(dialect=self.dialect)) # type: ignore temp_table.set("db", "global_temp") - return exp.select(*self._casted_columns(columns_to_types)).from_(temp_table) + return exp.select(*self._select_columns(target_columns_to_types)).from_(temp_table) return [SourceQuery(query_factory=query_factory)] def _ensure_pyspark_df( - self, generic_df: DF, columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None + self, + generic_df: DF, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + source_columns: t.Optional[t.List[str]] = None, ) -> PySparkDataFrame: pyspark_df = self.try_get_pyspark_df(generic_df) - if pyspark_df: - if columns_to_types: - # ensure Spark dataframe column order matches columns_to_types - pyspark_df = pyspark_df.select(*columns_to_types) - return pyspark_df - df = self.try_get_pandas_df(generic_df) - if df is None: - raise SQLMeshError("Ensure PySpark DF can only be run on a PySpark or Pandas DataFrame") - if columns_to_types: - # ensure Pandas dataframe column order matches columns_to_types - df = df[list(columns_to_types)] - kwargs = ( - dict(schema=self.sqlglot_to_spark_types(columns_to_types)) if columns_to_types else {} - ) - return self.spark.createDataFrame(df, **kwargs) # type: ignore + if not pyspark_df: + df = self.try_get_pandas_df(generic_df) + if df is None: + raise SQLMeshError( + "Ensure PySpark DF can only be run on a PySpark or Pandas DataFrame" + ) + + if target_columns_to_types: + source_columns_to_types = get_source_columns_to_types( + target_columns_to_types, source_columns + ) + # ensure Pandas dataframe column order matches columns_to_types + df = df[list(source_columns_to_types)] + else: + source_columns_to_types = None + kwargs = ( + dict(schema=self.sqlglot_to_spark_types(source_columns_to_types)) + if source_columns_to_types + else {} + ) + pyspark_df = self.spark.createDataFrame(df, **kwargs) # type: ignore + if target_columns_to_types: + select_columns = self._casted_columns( + target_columns_to_types, source_columns=source_columns + ) + pyspark_df = pyspark_df.selectExpr(*[x.sql(self.dialect) for x in select_columns]) # type: ignore + return pyspark_df def _get_temp_table( self, table: TableName, table_only: bool = False, quoted: bool = True @@ -375,12 +405,12 @@ def get_current_database(self) -> str: def create_state_table( self, table_name: str, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], primary_key: t.Optional[t.Tuple[str, ...]] = None, ) -> None: self.create_table( table_name, - columns_to_types, + target_columns_to_types, partitioned_by=[exp.column(x) for x in primary_key] if primary_key else None, ) @@ -399,7 +429,7 @@ def _create_table( expression: t.Optional[exp.Expression], exists: bool = True, replace: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, @@ -428,7 +458,7 @@ def _create_table( expression, exists=exists, replace=replace, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, table_description=table_description, column_descriptions=column_descriptions, **kwargs, diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index df8e45b520..e16cf2d76c 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -27,6 +27,7 @@ set_catalog, ) from sqlmesh.core.schema_diff import SchemaDiffer +from sqlmesh.utils import get_source_columns_to_types from sqlmesh.utils.errors import SQLMeshError from sqlmesh.utils.date import TimeLike @@ -117,7 +118,7 @@ def _insert_overwrite_by_condition( self, table_name: TableName, source_queries: t.List[SourceQuery], - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, where: t.Optional[exp.Condition] = None, insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None, **kwargs: t.Any, @@ -130,14 +131,14 @@ def _insert_overwrite_by_condition( # "Session property 'catalog.insert_existing_partitions_behavior' does not exist" self.execute(f"SET SESSION {catalog}.insert_existing_partitions_behavior='OVERWRITE'") super()._insert_overwrite_by_condition( - table_name, source_queries, columns_to_types, where + table_name, source_queries, target_columns_to_types, where ) self.execute(f"SET SESSION {catalog}.insert_existing_partitions_behavior='APPEND'") else: super()._insert_overwrite_by_condition( table_name, source_queries, - columns_to_types, + target_columns_to_types, where, insert_overwrite_strategy_override=InsertOverwriteStrategy.DELETE_INSERT, ) @@ -215,38 +216,44 @@ def _get_data_objects( def _df_to_source_queries( self, df: DF, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], batch_size: int, target_table: TableName, + source_columns: t.Optional[t.List[str]] = None, ) -> t.List[SourceQuery]: import pandas as pd from pandas.api.types import is_datetime64_any_dtype # type: ignore assert isinstance(df, pd.DataFrame) + source_columns_to_types = get_source_columns_to_types( + target_columns_to_types, source_columns + ) # Trino does not accept timestamps in ISOFORMAT that include the "T". `execution_time` is stored in # Pandas with that format, so we convert the column to a string with the proper format and CAST to # timestamp in Trino. - for column, kind in (columns_to_types or {}).items(): + for column, kind in source_columns_to_types.items(): dtype = df.dtypes[column] if is_datetime64_any_dtype(dtype) and getattr(dtype, "tz", None) is not None: df[column] = pd.to_datetime(df[column]).map(lambda x: x.isoformat(" ")) - return super()._df_to_source_queries(df, columns_to_types, batch_size, target_table) + return super()._df_to_source_queries( + df, target_columns_to_types, batch_size, target_table, source_columns=source_columns + ) def _build_schema_exp( self, table: exp.Table, - columns_to_types: t.Dict[str, exp.DataType], + target_columns_to_types: t.Dict[str, exp.DataType], column_descriptions: t.Optional[t.Dict[str, str]] = None, expressions: t.Optional[t.List[exp.PrimaryKey]] = None, is_view: bool = False, ) -> exp.Schema: if self.current_catalog_type == "delta_lake": - columns_to_types = self._to_delta_ts(columns_to_types) + target_columns_to_types = self._to_delta_ts(target_columns_to_types) return super()._build_schema_exp( - table, columns_to_types, column_descriptions, expressions, is_view + table, target_columns_to_types, column_descriptions, expressions, is_view ) def _scd_type_2( @@ -262,14 +269,15 @@ def _scd_type_2( check_columns: t.Optional[t.Union[exp.Star, t.Sequence[exp.Column]]] = None, updated_at_as_valid_from: bool = False, execution_time_as_valid_from: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, truncate: bool = False, + source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: - if columns_to_types and self.current_catalog_type == "delta_lake": - columns_to_types = self._to_delta_ts(columns_to_types) + if target_columns_to_types and self.current_catalog_type == "delta_lake": + target_columns_to_types = self._to_delta_ts(target_columns_to_types) return super()._scd_type_2( target_table, @@ -283,10 +291,11 @@ def _scd_type_2( check_columns, updated_at_as_valid_from, execution_time_as_valid_from, - columns_to_types, + target_columns_to_types, table_description, column_descriptions, truncate, + source_columns, **kwargs, ) @@ -344,7 +353,7 @@ def _create_table( expression: t.Optional[exp.Expression], exists: bool = True, replace: bool = False, - columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, @@ -355,7 +364,7 @@ def _create_table( expression=expression, exists=exists, replace=replace, - columns_to_types=columns_to_types, + target_columns_to_types=target_columns_to_types, table_description=table_description, column_descriptions=column_descriptions, table_kind=table_kind, diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index 185556fc8f..470ce92c20 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -189,6 +189,7 @@ class OnDestructiveChange(str, Enum): ERROR = "ERROR" WARN = "WARN" ALLOW = "ALLOW" + IGNORE = "IGNORE" @property def is_error(self) -> bool: @@ -202,6 +203,10 @@ def is_warn(self) -> bool: def is_allow(self) -> bool: return self == OnDestructiveChange.ALLOW + @property + def is_ignore(self) -> bool: + return self == OnDestructiveChange.IGNORE + def _on_destructive_change_validator( cls: t.Type, v: t.Union[OnDestructiveChange, str, exp.Identifier] diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index db2b43345a..a1adca56fb 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -558,6 +558,7 @@ def _check_destructive_changes(self, directly_modified: t.Set[SnapshotId]) -> No new.name, old_columns_to_types, new_columns_to_types, + ignore_destructive=new.model.on_destructive_change.is_ignore, ) if has_drop_alteration(schema_diff): diff --git a/sqlmesh/core/schema_diff.py b/sqlmesh/core/schema_diff.py index 70b4f72163..1bf2f76672 100644 --- a/sqlmesh/core/schema_diff.py +++ b/sqlmesh/core/schema_diff.py @@ -556,6 +556,8 @@ def _alter_operation( current_type: t.Union[str, exp.DataType], root_struct: exp.DataType, new_kwarg: exp.ColumnDef, + *, + ignore_destructive: bool = False, ) -> t.List[TableAlterOperation]: # We don't copy on purpose here because current_type may need to be mutated inside # _get_operations (struct.expressions.pop and struct.expressions.insert) @@ -570,6 +572,7 @@ def _alter_operation( current_type, new_type, root_struct, + ignore_destructive=ignore_destructive, ) if new_type.this == current_type.this == exp.DataType.Type.ARRAY: @@ -587,6 +590,7 @@ def _alter_operation( current_array_type, new_array_type, root_struct, + ignore_destructive=ignore_destructive, ) if self._is_coerceable_type(current_type, new_type): return [] @@ -607,6 +611,8 @@ def _alter_operation( col_pos, ) ] + if ignore_destructive: + return [] return self._drop_operation(columns, root_struct, pos, root_struct) + self._add_operation( columns, pos, new_kwarg, struct, root_struct ) @@ -617,11 +623,16 @@ def _resolve_alter_operations( current_struct: exp.DataType, new_struct: exp.DataType, root_struct: exp.DataType, + *, + ignore_destructive: bool = False, ) -> t.List[TableAlterOperation]: operations = [] for current_pos, current_kwarg in enumerate(current_struct.expressions.copy()): _, new_kwarg = self._get_matching_kwarg(current_kwarg, new_struct, current_pos) - assert new_kwarg + if new_kwarg is None: + if ignore_destructive: + continue + raise ValueError("Cannot alter a column that is being dropped") _, new_type = _get_name_and_type(new_kwarg) _, current_type = _get_name_and_type(current_kwarg) columns = parent_columns + [TableAlterColumn.from_struct_kwarg(current_kwarg)] @@ -636,6 +647,7 @@ def _resolve_alter_operations( current_type, root_struct, new_kwarg, + ignore_destructive=ignore_destructive, ) ) return operations @@ -646,42 +658,54 @@ def _get_operations( current_struct: exp.DataType, new_struct: exp.DataType, root_struct: exp.DataType, + *, + ignore_destructive: bool = False, ) -> t.List[TableAlterOperation]: root_struct = root_struct or current_struct parent_columns = parent_columns or [] operations = [] - operations.extend( - self._resolve_drop_operation(parent_columns, current_struct, new_struct, root_struct) - ) + if not ignore_destructive: + operations.extend( + self._resolve_drop_operation( + parent_columns, current_struct, new_struct, root_struct + ) + ) operations.extend( self._resolve_add_operations(parent_columns, current_struct, new_struct, root_struct) ) operations.extend( - self._resolve_alter_operations(parent_columns, current_struct, new_struct, root_struct) + self._resolve_alter_operations( + parent_columns, + current_struct, + new_struct, + root_struct, + ignore_destructive=ignore_destructive, + ) ) return operations def _from_structs( - self, current_struct: exp.DataType, new_struct: exp.DataType + self, + current_struct: exp.DataType, + new_struct: exp.DataType, + *, + ignore_destructive: bool = False, ) -> t.List[TableAlterOperation]: - return self._get_operations([], current_struct, new_struct, current_struct) + return self._get_operations( + [], current_struct, new_struct, current_struct, ignore_destructive=ignore_destructive + ) - def compare_structs( - self, table_name: t.Union[str, exp.Table], current: exp.DataType, new: exp.DataType + def _compare_structs( + self, + table_name: t.Union[str, exp.Table], + current: exp.DataType, + new: exp.DataType, + *, + ignore_destructive: bool = False, ) -> t.List[exp.Alter]: - """ - Compares two schemas represented as structs. - - Args: - current: The current schema. - new: The new schema. - - Returns: - The list of table alter operations. - """ return [ op.expression(table_name, self.array_element_selector) - for op in self._from_structs(current, new) + for op in self._from_structs(current, new, ignore_destructive=ignore_destructive) ] def compare_columns( @@ -689,19 +713,14 @@ def compare_columns( table_name: TableName, current: t.Dict[str, exp.DataType], new: t.Dict[str, exp.DataType], + *, + ignore_destructive: bool = False, ) -> t.List[exp.Alter]: - """ - Compares two schemas represented as dictionaries of column names and types. - - Args: - current: The current schema. - new: The new schema. - - Returns: - The list of schema deltas. - """ - return self.compare_structs( - table_name, columns_to_types_to_struct(current), columns_to_types_to_struct(new) + return self._compare_structs( + table_name, + columns_to_types_to_struct(current), + columns_to_types_to_struct(new), + ignore_destructive=ignore_destructive, ) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 3937f37fba..f2f7044ba7 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -694,6 +694,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: end=end, execution_time=execution_time, physical_properties=rendered_physical_properties, + render_kwargs=render_statements_kwargs, ) else: logger.info( @@ -715,6 +716,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: end=end, execution_time=execution_time, physical_properties=rendered_physical_properties, + render_kwargs=render_statements_kwargs, ) with ( @@ -865,7 +867,9 @@ def _create_snapshot( rendered_physical_properties=rendered_physical_properties, ) alter_expressions = adapter.get_alter_expressions( - target_table_name, tmp_table_name + target_table_name, + tmp_table_name, + ignore_destructive=snapshot.model.on_destructive_change.is_ignore, ) _check_destructive_schema_change( snapshot, alter_expressions, allow_destructive_snapshots @@ -940,9 +944,9 @@ def _migrate_snapshot( evaluation_strategy = _evaluation_strategy(snapshot, adapter) tmp_table_name = snapshot.table_name(is_deployable=False) logger.info( - "Migrating table schema from '%s' to '%s'", - tmp_table_name, + "Migrating table schema '%s' to match '%s'", target_table_name, + tmp_table_name, ) evaluation_strategy.migrate( target_table_name=target_table_name, @@ -950,6 +954,7 @@ def _migrate_snapshot( snapshot=snapshot, snapshots=parent_snapshots_by_name(snapshot, snapshots), allow_destructive_snapshots=allow_destructive_snapshots, + ignore_destructive=snapshot.model.on_destructive_change.is_ignore, ) else: logger.info( @@ -1291,6 +1296,7 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: """Inserts the given query or a DataFrame into the target table or a view. @@ -1303,6 +1309,7 @@ def insert( if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary. + render_kwargs: Additional key-value arguments to pass when rendering the model's query. """ @abc.abstractmethod @@ -1311,6 +1318,7 @@ def append( table_name: str, query_or_df: QueryOrDF, model: Model, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: """Appends the given query or a DataFrame to the existing table. @@ -1319,6 +1327,7 @@ def append( table_name: The target table name. query_or_df: A query or a DataFrame to insert. model: The target model. + render_kwargs: Additional key-value arguments to pass when rendering the model's query. """ @abc.abstractmethod @@ -1349,6 +1358,8 @@ def migrate( target_table_name: str, source_table_name: str, snapshot: Snapshot, + *, + ignore_destructive: bool, **kwargs: t.Any, ) -> None: """Migrates the target table schema so that it corresponds to the source table schema. @@ -1357,6 +1368,8 @@ def migrate( target_table_name: The target table name. source_table_name: The source table name. snapshot: The target snapshot. + ignore_destructive: If True, destructive changes are not created when migrating. + This is used for forward-only models that are being migrated to a new version. """ @abc.abstractmethod @@ -1393,36 +1406,6 @@ def demote(self, view_name: str, **kwargs: t.Any) -> None: view_name: The name of the target view in the virtual layer. """ - def _replace_query_for_model( - self, model: Model, name: str, query_or_df: QueryOrDF, **kwargs: t.Any - ) -> None: - """Replaces the table for the given model. - - Args: - model: The target model. - name: The name of the target table. - query_or_df: The query or DataFrame to replace the target table with. - """ - # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models. - columns_to_types = ( - model.columns_to_types - if (model.is_seed or model.kind.is_full) and model.annotated - else self.adapter.columns(name) - ) - self.adapter.replace_query( - name, - query_or_df, - table_format=model.table_format, - storage_format=model.storage_format, - partitioned_by=model.partitioned_by, - partition_interval_unit=model.partition_interval_unit, - clustered_by=model.clustered_by, - table_properties=kwargs.get("physical_properties", model.physical_properties), - table_description=model.description, - column_descriptions=model.column_descriptions, - columns_to_types=columns_to_types, - ) - class SymbolicStrategy(EvaluationStrategy): def insert( @@ -1431,6 +1414,7 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: pass @@ -1440,6 +1424,7 @@ def append( table_name: str, query_or_df: QueryOrDF, model: Model, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: pass @@ -1459,6 +1444,8 @@ def migrate( target_table_name: str, source_table_name: str, snapshot: Snapshot, + *, + ignore_destructive: bool, **kwarg: t.Any, ) -> None: pass @@ -1493,7 +1480,7 @@ def promote( self.adapter.drop_view(view_name, cascade=False) -class PromotableStrategy(EvaluationStrategy): +class PromotableStrategy(EvaluationStrategy, abc.ABC): def promote( self, table_name: str, @@ -1527,16 +1514,7 @@ def demote(self, view_name: str, **kwargs: t.Any) -> None: self.adapter.drop_view(view_name, cascade=False) -class MaterializableStrategy(PromotableStrategy): - def append( - self, - table_name: str, - query_or_df: QueryOrDF, - model: Model, - **kwargs: t.Any, - ) -> None: - self.adapter.insert_append(table_name, query_or_df, columns_to_types=model.columns_to_types) - +class MaterializableStrategy(PromotableStrategy, abc.ABC): def create( self, table_name: str, @@ -1552,7 +1530,7 @@ def create( if model.annotated: self.adapter.create_table( table_name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_format=model.table_format, storage_format=model.storage_format, partitioned_by=model.partitioned_by, @@ -1592,10 +1570,14 @@ def migrate( target_table_name: str, source_table_name: str, snapshot: Snapshot, + *, + ignore_destructive: bool, **kwargs: t.Any, ) -> None: logger.info(f"Altering table '{target_table_name}'") - alter_expressions = self.adapter.get_alter_expressions(target_table_name, source_table_name) + alter_expressions = self.adapter.get_alter_expressions( + target_table_name, source_table_name, ignore_destructive=ignore_destructive + ) _check_destructive_schema_change( snapshot, alter_expressions, kwargs["allow_destructive_snapshots"] ) @@ -1606,63 +1588,163 @@ def delete(self, name: str, **kwargs: t.Any) -> None: self.adapter.drop_table(name) logger.info("Dropped table '%s'", name) + def _replace_query_for_model( + self, + model: Model, + name: str, + query_or_df: QueryOrDF, + render_kwargs: t.Dict[str, t.Any], + **kwargs: t.Any, + ) -> None: + """Replaces the table for the given model. + + Args: + model: The target model. + name: The name of the target table. + query_or_df: The query or DataFrame to replace the target table with. + """ + if (model.is_seed or model.kind.is_full) and model.annotated: + columns_to_types = model.columns_to_types_or_raise + source_columns: t.Optional[t.List[str]] = list(columns_to_types) + else: + # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models. + columns_to_types, source_columns = self._get_target_and_source_columns( + model, name, render_kwargs, force_get_columns_from_target=True + ) + + self.adapter.replace_query( + name, + query_or_df, + table_format=model.table_format, + storage_format=model.storage_format, + partitioned_by=model.partitioned_by, + partition_interval_unit=model.partition_interval_unit, + clustered_by=model.clustered_by, + table_properties=kwargs.get("physical_properties", model.physical_properties), + table_description=model.description, + column_descriptions=model.column_descriptions, + target_columns_to_types=columns_to_types, + source_columns=source_columns, + ) + + def _get_target_and_source_columns( + self, + model: Model, + table_name: str, + render_kwargs: t.Dict[str, t.Any], + force_get_columns_from_target: bool = False, + ) -> t.Tuple[t.Dict[str, exp.DataType], t.Optional[t.List[str]]]: + if force_get_columns_from_target: + target_column_to_types = self.adapter.columns(table_name) + else: + target_column_to_types = ( + model.columns_to_types # type: ignore + if model.annotated and not model.on_destructive_change.is_ignore + else self.adapter.columns(table_name) + ) + assert target_column_to_types is not None + if model.on_destructive_change.is_ignore: + # We need to identify the columns that are only in the source so we create an empty table with + # the user query to determine that + with self.adapter.temp_table(model.ctas_query(**render_kwargs)) as temp_table: + source_columns = list(self.adapter.columns(temp_table)) + else: + source_columns = None + return target_column_to_types, source_columns + -class IncrementalByPartitionStrategy(MaterializableStrategy): +class IncrementalStrategy(MaterializableStrategy, abc.ABC): + def append( + self, + table_name: str, + query_or_df: QueryOrDF, + model: Model, + render_kwargs: t.Dict[str, t.Any], + **kwargs: t.Any, + ) -> None: + columns_to_types, source_columns = self._get_target_and_source_columns( + model, table_name, render_kwargs=render_kwargs + ) + self.adapter.insert_append( + table_name, + query_or_df, + target_columns_to_types=columns_to_types, + source_columns=source_columns, + ) + + +class IncrementalByPartitionStrategy(IncrementalStrategy): def insert( self, table_name: str, query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: if is_first_insert: - self._replace_query_for_model(model, table_name, query_or_df, **kwargs) + self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs) else: + columns_to_types, source_columns = self._get_target_and_source_columns( + model, table_name, render_kwargs=render_kwargs + ) self.adapter.insert_overwrite_by_partition( table_name, query_or_df, partitioned_by=model.partitioned_by, - columns_to_types=model.columns_to_types, + target_columns_to_types=columns_to_types, + source_columns=source_columns, ) -class IncrementalByTimeRangeStrategy(MaterializableStrategy): +class IncrementalByTimeRangeStrategy(IncrementalStrategy): def insert( self, table_name: str, query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: assert model.time_column + columns_to_types, source_columns = self._get_target_and_source_columns( + model, table_name, render_kwargs=render_kwargs + ) self.adapter.insert_overwrite_by_time_partition( table_name, query_or_df, time_formatter=model.convert_to_time_column, time_column=model.time_column, - columns_to_types=model.columns_to_types, + target_columns_to_types=columns_to_types, + source_columns=source_columns, **kwargs, ) -class IncrementalByUniqueKeyStrategy(MaterializableStrategy): +class IncrementalByUniqueKeyStrategy(IncrementalStrategy): def insert( self, table_name: str, query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: if is_first_insert: - self._replace_query_for_model(model, table_name, query_or_df, **kwargs) + self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs) else: + columns_to_types, source_columns = self._get_target_and_source_columns( + model, + table_name, + render_kwargs=render_kwargs, + ) self.adapter.merge( table_name, query_or_df, - columns_to_types=model.columns_to_types, + target_columns_to_types=columns_to_types, unique_key=model.unique_key, when_matched=model.when_matched, merge_filter=model.render_merge_filter( @@ -1671,6 +1753,7 @@ def insert( execution_time=kwargs.get("execution_time"), ), physical_properties=kwargs.get("physical_properties", model.physical_properties), + source_columns=source_columns, ) def append( @@ -1678,12 +1761,16 @@ def append( table_name: str, query_or_df: QueryOrDF, model: Model, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: + columns_to_types, source_columns = self._get_target_and_source_columns( + model, table_name, render_kwargs=render_kwargs + ) self.adapter.merge( table_name, query_or_df, - columns_to_types=model.columns_to_types, + target_columns_to_types=columns_to_types, unique_key=model.unique_key, when_matched=model.when_matched, merge_filter=model.render_merge_filter( @@ -1692,46 +1779,90 @@ def append( execution_time=kwargs.get("execution_time"), ), physical_properties=kwargs.get("physical_properties", model.physical_properties), + source_columns=source_columns, ) -class IncrementalUnmanagedStrategy(MaterializableStrategy): +class IncrementalUnmanagedStrategy(IncrementalStrategy): + def append( + self, + table_name: str, + query_or_df: QueryOrDF, + model: Model, + render_kwargs: t.Dict[str, t.Any], + **kwargs: t.Any, + ) -> None: + columns_to_types, source_columns = self._get_target_and_source_columns( + model, table_name, render_kwargs=render_kwargs + ) + self.adapter.insert_append( + table_name, + query_or_df, + target_columns_to_types=columns_to_types, + source_columns=source_columns, + ) + def insert( self, table_name: str, query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: if is_first_insert: - self._replace_query_for_model(model, table_name, query_or_df, **kwargs) - elif isinstance(model.kind, IncrementalUnmanagedKind) and model.kind.insert_overwrite: - self.adapter.insert_overwrite_by_partition( + return self._replace_query_for_model( + model, table_name, query_or_df, render_kwargs, **kwargs + ) + if isinstance(model.kind, IncrementalUnmanagedKind) and model.kind.insert_overwrite: + columns_to_types, source_columns = self._get_target_and_source_columns( + model, table_name, - query_or_df, - model.partitioned_by, - columns_to_types=model.columns_to_types, + render_kwargs=render_kwargs, ) - else: - self.append( + + return self.adapter.insert_overwrite_by_partition( table_name, query_or_df, - model, - **kwargs, + model.partitioned_by, + target_columns_to_types=columns_to_types, + source_columns=source_columns, ) + return self.append( + table_name, + query_or_df, + model, + render_kwargs=render_kwargs, + **kwargs, + ) class FullRefreshStrategy(MaterializableStrategy): + def append( + self, + table_name: str, + query_or_df: QueryOrDF, + model: Model, + render_kwargs: t.Dict[str, t.Any], + **kwargs: t.Any, + ) -> None: + self.adapter.insert_append( + table_name, + query_or_df, + target_columns_to_types=model.columns_to_types, + ) + def insert( self, table_name: str, query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: - self._replace_query_for_model(model, table_name, query_or_df, **kwargs) + self._replace_query_for_model(model, table_name, query_or_df, render_kwargs, **kwargs) class SeedStrategy(MaterializableStrategy): @@ -1760,10 +1891,12 @@ def create( try: for index, df in enumerate(model.render_seed()): if index == 0: - self._replace_query_for_model(model, table_name, df, **kwargs) + self._replace_query_for_model( + model, table_name, df, render_kwargs, **kwargs + ) else: self.adapter.insert_append( - table_name, df, columns_to_types=model.columns_to_types + table_name, df, target_columns_to_types=model.columns_to_types ) except Exception: self.adapter.drop_table(table_name) @@ -1775,13 +1908,25 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], + **kwargs: t.Any, + ) -> None: + # Data has already been inserted at the time of table creation. + pass + + def append( + self, + table_name: str, + query_or_df: QueryOrDF, + model: Model, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: # Data has already been inserted at the time of table creation. pass -class SCDType2Strategy(MaterializableStrategy): +class SCDType2Strategy(IncrementalStrategy): def create( self, table_name: str, @@ -1798,7 +1943,7 @@ def create( columns_to_types[model.kind.updated_at_name.name] = model.kind.time_data_type self.adapter.create_table( table_name, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, table_format=model.table_format, storage_format=model.storage_format, partitioned_by=model.partitioned_by, @@ -1826,10 +1971,16 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: - # Source columns from the underlying table to prevent unintentional table schema changes during the insert. - columns_to_types = self.adapter.columns(table_name) + # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models. + columns_to_types, source_columns = self._get_target_and_source_columns( + model, + table_name, + render_kwargs=render_kwargs, + force_get_columns_from_target=True, + ) if isinstance(model.kind, SCDType2ByTimeKind): self.adapter.scd_type_2_by_time( target_table=table_name, @@ -1841,11 +1992,12 @@ def insert( updated_at_col=model.kind.updated_at_name, invalidate_hard_deletes=model.kind.invalidate_hard_deletes, updated_at_as_valid_from=model.kind.updated_at_as_valid_from, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, table_format=model.table_format, table_description=model.description, column_descriptions=model.column_descriptions, truncate=is_first_insert, + source_columns=source_columns, ) elif isinstance(model.kind, SCDType2ByColumnKind): self.adapter.scd_type_2_by_column( @@ -1858,11 +2010,12 @@ def insert( check_columns=model.kind.columns, invalidate_hard_deletes=model.kind.invalidate_hard_deletes, execution_time_as_valid_from=model.kind.execution_time_as_valid_from, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, table_format=model.table_format, table_description=model.description, column_descriptions=model.column_descriptions, truncate=is_first_insert, + source_columns=source_columns, ) else: raise SQLMeshError( @@ -1874,10 +2027,16 @@ def append( table_name: str, query_or_df: QueryOrDF, model: Model, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: - # Source columns from the underlying table to prevent unintentional table schema changes during the insert. - columns_to_types = self.adapter.columns(table_name) + # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models. + columns_to_types, source_columns = self._get_target_and_source_columns( + model, + table_name, + render_kwargs=render_kwargs, + force_get_columns_from_target=True, + ) if isinstance(model.kind, SCDType2ByTimeKind): self.adapter.scd_type_2_by_time( target_table=table_name, @@ -1888,10 +2047,11 @@ def append( updated_at_col=model.kind.updated_at_name, invalidate_hard_deletes=model.kind.invalidate_hard_deletes, updated_at_as_valid_from=model.kind.updated_at_as_valid_from, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, table_format=model.table_format, table_description=model.description, column_descriptions=model.column_descriptions, + source_columns=source_columns, **kwargs, ) elif isinstance(model.kind, SCDType2ByColumnKind): @@ -1902,12 +2062,13 @@ def append( valid_from_col=model.kind.valid_from_name, valid_to_col=model.kind.valid_to_name, check_columns=model.kind.columns, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, table_format=model.table_format, invalidate_hard_deletes=model.kind.invalidate_hard_deletes, execution_time_as_valid_from=model.kind.execution_time_as_valid_from, table_description=model.description, column_descriptions=model.column_descriptions, + source_columns=source_columns, **kwargs, ) else: @@ -1923,6 +2084,7 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: deployability_index = ( @@ -1956,6 +2118,7 @@ def append( table_name: str, query_or_df: QueryOrDF, model: Model, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: raise ConfigError(f"Cannot append to a view '{table_name}'.") @@ -2011,6 +2174,8 @@ def migrate( target_table_name: str, source_table_name: str, snapshot: Snapshot, + *, + ignore_destructive: bool, **kwargs: t.Any, ) -> None: logger.info("Migrating view '%s'", target_table_name) @@ -2048,7 +2213,7 @@ def _is_materialized_view(self, model: Model) -> bool: C = t.TypeVar("C", bound=CustomKind) -class CustomMaterialization(MaterializableStrategy, t.Generic[C]): +class CustomMaterialization(IncrementalStrategy, t.Generic[C]): """Base class for custom materializations.""" def insert( @@ -2057,6 +2222,7 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: """Inserts the given query or a DataFrame into the target table or a view. @@ -2069,6 +2235,7 @@ def insert( if no data has been previously inserted into the target table, or when the entire history of the target model has been restated. Note that in the latter case, the table might contain data from previous executions, and it is the responsibility of a specific evaluation strategy to handle the truncation of the table if necessary. + render_kwargs: Additional key-value arguments to pass when rendering the model's query. """ raise NotImplementedError( "Custom materialization strategies must implement the 'insert' method." @@ -2177,7 +2344,7 @@ def create( self.adapter.create_managed_table( table_name=table_name, query=model.render_query_or_raise(**render_kwargs), - columns_to_types=model.columns_to_types, + target_columns_to_types=model.columns_to_types, partitioned_by=model.partitioned_by, clustered_by=model.clustered_by, table_properties=kwargs.get("physical_properties", model.physical_properties), @@ -2204,6 +2371,7 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: deployability_index: DeployabilityIndex = kwargs["deployability_index"] @@ -2214,7 +2382,7 @@ def insert( self.adapter.create_managed_table( table_name=table_name, query=query_or_df, # type: ignore - columns_to_types=model.columns_to_types, + target_columns_to_types=model.columns_to_types, partitioned_by=model.partitioned_by, clustered_by=model.clustered_by, table_properties=kwargs.get("physical_properties", model.physical_properties), @@ -2231,7 +2399,11 @@ def insert( model.name, ) self._replace_query_for_model( - model=model, name=table_name, query_or_df=query_or_df, **kwargs + model=model, + name=table_name, + query_or_df=query_or_df, + render_kwargs=render_kwargs, + **kwargs, ) def append( @@ -2239,6 +2411,7 @@ def append( table_name: str, query_or_df: QueryOrDF, model: Model, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: raise ConfigError(f"Cannot append to a managed table '{table_name}'.") @@ -2248,10 +2421,12 @@ def migrate( target_table_name: str, source_table_name: str, snapshot: Snapshot, + *, + ignore_destructive: bool, **kwargs: t.Any, ) -> None: potential_alter_expressions = self.adapter.get_alter_expressions( - target_table_name, source_table_name + target_table_name, source_table_name, ignore_destructive=ignore_destructive ) if len(potential_alter_expressions) > 0: # this can happen if a user changes a managed model and deliberately overrides a plan to be forward only, eg `sqlmesh plan --forward-only` diff --git a/sqlmesh/core/state_sync/db/environment.py b/sqlmesh/core/state_sync/db/environment.py index b7e8128a93..3196d18078 100644 --- a/sqlmesh/core/state_sync/db/environment.py +++ b/sqlmesh/core/state_sync/db/environment.py @@ -77,7 +77,7 @@ def update_environment(self, environment: Environment) -> None: self.engine_adapter.insert_append( self.environments_table, _environment_to_df(environment), - columns_to_types=self._environment_columns_to_types, + target_columns_to_types=self._environment_columns_to_types, ) def update_environment_statements( @@ -107,7 +107,7 @@ def update_environment_statements( self.engine_adapter.insert_append( self.environment_statements_table, _environment_statements_to_df(environment_name, plan_id, environment_statements), - columns_to_types=self._environment_statements_columns_to_types, + target_columns_to_types=self._environment_statements_columns_to_types, ) def invalidate_environment(self, name: str, protect_prod: bool = True) -> None: diff --git a/sqlmesh/core/state_sync/db/interval.py b/sqlmesh/core/state_sync/db/interval.py index bebf41d453..bdfedace1e 100644 --- a/sqlmesh/core/state_sync/db/interval.py +++ b/sqlmesh/core/state_sync/db/interval.py @@ -114,7 +114,7 @@ def remove_intervals( self.engine_adapter.insert_append( self.intervals_table, _intervals_to_df(intervals_to_remove, is_dev=False, is_removed=True), - columns_to_types=self._interval_columns_to_types, + target_columns_to_types=self._interval_columns_to_types, ) def get_snapshot_intervals( @@ -242,7 +242,7 @@ def _push_snapshot_intervals( self.engine_adapter.insert_append( self.intervals_table, pd.DataFrame(new_intervals), - columns_to_types=self._interval_columns_to_types, + target_columns_to_types=self._interval_columns_to_types, ) def _get_snapshot_intervals( diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 3745a27bb3..9cf4f2fbf5 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -102,7 +102,7 @@ def push_snapshots(self, snapshots: t.Iterable[Snapshot], overwrite: bool = Fals self.engine_adapter.insert_append( self.snapshots_table, _snapshots_to_df(snapshots_to_store), - columns_to_types=self._snapshot_columns_to_types, + target_columns_to_types=self._snapshot_columns_to_types, ) for snapshot in snapshots: @@ -363,7 +363,7 @@ def update_auto_restatements( self.engine_adapter.merge( self.auto_restatements_table, _auto_restatements_to_df(next_auto_restatement_ts_filtered), - columns_to_types=self._auto_restatement_columns_to_types, + target_columns_to_types=self._auto_restatement_columns_to_types, unique_key=(exp.column("snapshot_name"), exp.column("snapshot_version")), ) @@ -405,7 +405,7 @@ def _push_snapshots(self, snapshots: t.Iterable[Snapshot]) -> None: self.engine_adapter.insert_append( self.snapshots_table, _snapshots_to_df(snapshots_to_store), - columns_to_types=self._snapshot_columns_to_types, + target_columns_to_types=self._snapshot_columns_to_types, ) def _get_snapshots( diff --git a/sqlmesh/core/state_sync/db/version.py b/sqlmesh/core/state_sync/db/version.py index 873e1633df..492d74cc09 100644 --- a/sqlmesh/core/state_sync/db/version.py +++ b/sqlmesh/core/state_sync/db/version.py @@ -54,7 +54,7 @@ def update_versions( } ] ), - columns_to_types=self._version_columns_to_types, + target_columns_to_types=self._version_columns_to_types, ) def get_versions(self) -> Versions: diff --git a/sqlmesh/core/table_diff.py b/sqlmesh/core/table_diff.py index 126fa64b1e..b9dfadc075 100644 --- a/sqlmesh/core/table_diff.py +++ b/sqlmesh/core/table_diff.py @@ -494,7 +494,7 @@ def _column_expr(name: str, table: str) -> exp.Expression: schema = to_schema(temp_schema, dialect=self.dialect) temp_table = exp.table_("diff", db=schema.db, catalog=schema.catalog, quoted=True) - temp_table_kwargs = {} + temp_table_kwargs: t.Dict[str, t.Any] = {} if isinstance(self.adapter, AthenaEngineAdapter): # Athena has two table formats: Hive (the default) and Iceberg. TableDiff requires that # the formats be the same for the source, target, and temp tables. @@ -512,7 +512,7 @@ def _column_expr(name: str, table: str) -> exp.Expression: ) with self.adapter.temp_table( - query, name=temp_table, columns_to_types=None, **temp_table_kwargs + query, name=temp_table, target_columns_to_types=None, **temp_table_kwargs ) as table: summary_sums = [ exp.func("SUM", "s_exists").as_("s_count"), diff --git a/sqlmesh/migrations/v0007_env_table_info_to_kind.py b/sqlmesh/migrations/v0007_env_table_info_to_kind.py index 61335a0c51..f09f0d2b72 100644 --- a/sqlmesh/migrations/v0007_env_table_info_to_kind.py +++ b/sqlmesh/migrations/v0007_env_table_info_to_kind.py @@ -86,7 +86,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( environments_table, pd.DataFrame(new_environments), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "snapshots": exp.DataType.build("text"), "start_at": exp.DataType.build("text"), diff --git a/sqlmesh/migrations/v0009_remove_pre_post_hooks.py b/sqlmesh/migrations/v0009_remove_pre_post_hooks.py index 05d50c0932..3671f547d3 100644 --- a/sqlmesh/migrations/v0009_remove_pre_post_hooks.py +++ b/sqlmesh/migrations/v0009_remove_pre_post_hooks.py @@ -53,7 +53,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0011_add_model_kind_name.py b/sqlmesh/migrations/v0011_add_model_kind_name.py index 298d4b61ee..77aa68506a 100644 --- a/sqlmesh/migrations/v0011_add_model_kind_name.py +++ b/sqlmesh/migrations/v0011_add_model_kind_name.py @@ -53,7 +53,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0012_update_jinja_expressions.py b/sqlmesh/migrations/v0012_update_jinja_expressions.py index 4f6f04fba5..28bc4acdca 100644 --- a/sqlmesh/migrations/v0012_update_jinja_expressions.py +++ b/sqlmesh/migrations/v0012_update_jinja_expressions.py @@ -57,7 +57,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0013_serde_using_model_dialects.py b/sqlmesh/migrations/v0013_serde_using_model_dialects.py index 6f03767061..7e5e2cc217 100644 --- a/sqlmesh/migrations/v0013_serde_using_model_dialects.py +++ b/sqlmesh/migrations/v0013_serde_using_model_dialects.py @@ -55,7 +55,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0016_fix_windows_path.py b/sqlmesh/migrations/v0016_fix_windows_path.py index fb40d30076..e37c45afca 100644 --- a/sqlmesh/migrations/v0016_fix_windows_path.py +++ b/sqlmesh/migrations/v0016_fix_windows_path.py @@ -49,7 +49,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0017_fix_windows_seed_path.py b/sqlmesh/migrations/v0017_fix_windows_seed_path.py index ca693bab72..5d91443009 100644 --- a/sqlmesh/migrations/v0017_fix_windows_seed_path.py +++ b/sqlmesh/migrations/v0017_fix_windows_seed_path.py @@ -45,7 +45,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py b/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py index de8f157ebb..5229c54f81 100644 --- a/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py +++ b/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py @@ -43,7 +43,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py b/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py index c6beeb7d0a..d4c449ff34 100644 --- a/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py +++ b/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py @@ -48,7 +48,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0021_fix_table_properties.py b/sqlmesh/migrations/v0021_fix_table_properties.py index 36bcbdcc82..41429b5650 100644 --- a/sqlmesh/migrations/v0021_fix_table_properties.py +++ b/sqlmesh/migrations/v0021_fix_table_properties.py @@ -52,7 +52,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0022_move_project_to_model.py b/sqlmesh/migrations/v0022_move_project_to_model.py index 8da19049af..a5a529ef31 100644 --- a/sqlmesh/migrations/v0022_move_project_to_model.py +++ b/sqlmesh/migrations/v0022_move_project_to_model.py @@ -44,7 +44,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py b/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py index 2855ecebb2..abdbb716ea 100644 --- a/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py +++ b/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py @@ -45,7 +45,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py b/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py index 7c794abdaa..b99e208806 100644 --- a/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py +++ b/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py @@ -85,7 +85,7 @@ def _add_interval(start_ts: int, end_ts: int, is_dev: bool) -> None: engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), @@ -98,7 +98,7 @@ def _add_interval(start_ts: int, end_ts: int, is_dev: bool) -> None: engine_adapter.insert_append( intervals_table, pd.DataFrame(new_intervals), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build(index_type), "created_ts": exp.DataType.build("bigint"), "name": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0026_remove_dialect_from_seed.py b/sqlmesh/migrations/v0026_remove_dialect_from_seed.py index c06eeb4bca..73ec09aa76 100644 --- a/sqlmesh/migrations/v0026_remove_dialect_from_seed.py +++ b/sqlmesh/migrations/v0026_remove_dialect_from_seed.py @@ -45,7 +45,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0027_minute_interval_to_five.py b/sqlmesh/migrations/v0027_minute_interval_to_five.py index f92ffcb929..ce8b272734 100644 --- a/sqlmesh/migrations/v0027_minute_interval_to_five.py +++ b/sqlmesh/migrations/v0027_minute_interval_to_five.py @@ -47,7 +47,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py b/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py index b7f58dc67f..1f2dda5f5f 100644 --- a/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py +++ b/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py @@ -46,7 +46,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py b/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py index c2b6f545bc..3cd27d2ee2 100644 --- a/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py +++ b/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py @@ -55,7 +55,7 @@ def migrate(state_sync: t.Any, **kwargs: t.Any) -> None: # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0031_remove_dbt_target_fields.py b/sqlmesh/migrations/v0031_remove_dbt_target_fields.py index 92137a4973..d13ec92e0b 100644 --- a/sqlmesh/migrations/v0031_remove_dbt_target_fields.py +++ b/sqlmesh/migrations/v0031_remove_dbt_target_fields.py @@ -55,7 +55,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0034_add_default_catalog.py b/sqlmesh/migrations/v0034_add_default_catalog.py index 85a97b1134..d6469fa4b1 100644 --- a/sqlmesh/migrations/v0034_add_default_catalog.py +++ b/sqlmesh/migrations/v0034_add_default_catalog.py @@ -161,7 +161,7 @@ def migrate(state_sync, default_catalog: t.Optional[str], **kwargs): # type: ig engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), @@ -241,7 +241,7 @@ def migrate(state_sync, default_catalog: t.Optional[str], **kwargs): # type: ig engine_adapter.insert_append( environments_table, pd.DataFrame(new_environments), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "snapshots": exp.DataType.build(blob_type), "start_at": exp.DataType.build("text"), @@ -316,7 +316,7 @@ def migrate(state_sync, default_catalog: t.Optional[str], **kwargs): # type: ig engine_adapter.insert_append( intervals_table, pd.DataFrame(new_intervals), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build(index_type), "created_ts": exp.DataType.build("bigint"), "name": exp.DataType.build(index_type), @@ -359,7 +359,7 @@ def migrate(state_sync, default_catalog: t.Optional[str], **kwargs): # type: ig engine_adapter.insert_append( seeds_table, pd.DataFrame(new_seeds), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "content": exp.DataType.build("text"), diff --git a/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py b/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py index 86fbc986ec..6ca7bef406 100644 --- a/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py +++ b/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py @@ -51,7 +51,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py b/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py index 9f27239f41..54bb30a54b 100644 --- a/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py +++ b/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py @@ -62,7 +62,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py b/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py index 10da4e18e5..39fc6b6a0f 100644 --- a/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py +++ b/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py @@ -60,7 +60,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( plan_dags_table, pd.DataFrame(new_specs), - columns_to_types={ + target_columns_to_types={ "request_id": exp.DataType.build(index_type), "dag_id": exp.DataType.build(index_type), "dag_spec": exp.DataType.build(blob_type), diff --git a/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py b/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py index ad47b63724..fee9ac2955 100644 --- a/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py +++ b/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py @@ -48,7 +48,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0042_trim_indirect_versions.py b/sqlmesh/migrations/v0042_trim_indirect_versions.py index 37b6bef570..6759e8140d 100644 --- a/sqlmesh/migrations/v0042_trim_indirect_versions.py +++ b/sqlmesh/migrations/v0042_trim_indirect_versions.py @@ -55,7 +55,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py b/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py index 4054f34f40..8b27e90963 100644 --- a/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py +++ b/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py @@ -53,7 +53,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( plan_dags_table, pd.DataFrame(new_dag_specs), - columns_to_types={ + target_columns_to_types={ "request_id": exp.DataType.build(index_type), "dag_id": exp.DataType.build(index_type), "dag_spec": exp.DataType.build(blob_type), diff --git a/sqlmesh/migrations/v0045_move_gateway_variable.py b/sqlmesh/migrations/v0045_move_gateway_variable.py index bd00e40404..12115e03e0 100644 --- a/sqlmesh/migrations/v0045_move_gateway_variable.py +++ b/sqlmesh/migrations/v0045_move_gateway_variable.py @@ -59,7 +59,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0048_drop_indirect_versions.py b/sqlmesh/migrations/v0048_drop_indirect_versions.py index e5fe9a28ab..991fb43827 100644 --- a/sqlmesh/migrations/v0048_drop_indirect_versions.py +++ b/sqlmesh/migrations/v0048_drop_indirect_versions.py @@ -48,7 +48,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0051_rename_column_descriptions.py b/sqlmesh/migrations/v0051_rename_column_descriptions.py index 627e58b4b9..a6b4b72577 100644 --- a/sqlmesh/migrations/v0051_rename_column_descriptions.py +++ b/sqlmesh/migrations/v0051_rename_column_descriptions.py @@ -54,7 +54,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py b/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py index 1c127b496b..b323afa04f 100644 --- a/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py +++ b/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py @@ -118,7 +118,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0056_restore_table_indexes.py b/sqlmesh/migrations/v0056_restore_table_indexes.py index d6fab1669b..4ffec4e9cb 100644 --- a/sqlmesh/migrations/v0056_restore_table_indexes.py +++ b/sqlmesh/migrations/v0056_restore_table_indexes.py @@ -79,7 +79,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( new_snapshots_table, exp.select("*").from_(snapshots_table), - columns_to_types=snapshots_columns_to_types, + target_columns_to_types=snapshots_columns_to_types, ) # Recreate the environments table and its indexes. @@ -89,7 +89,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( new_environments_table, exp.select("*").from_(environments_table), - columns_to_types=environments_columns_to_types, + target_columns_to_types=environments_columns_to_types, ) # Recreate the intervals table and its indexes. @@ -105,7 +105,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( new_intervals_table, exp.select("*").from_(intervals_table), - columns_to_types=intervals_columns_to_types, + target_columns_to_types=intervals_columns_to_types, ) # Drop old tables. diff --git a/sqlmesh/migrations/v0060_move_audits_to_model.py b/sqlmesh/migrations/v0060_move_audits_to_model.py index 31da86999e..ca61055579 100644 --- a/sqlmesh/migrations/v0060_move_audits_to_model.py +++ b/sqlmesh/migrations/v0060_move_audits_to_model.py @@ -72,7 +72,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0063_change_signals.py b/sqlmesh/migrations/v0063_change_signals.py index 48a5bd1998..cf01bd2420 100644 --- a/sqlmesh/migrations/v0063_change_signals.py +++ b/sqlmesh/migrations/v0063_change_signals.py @@ -84,7 +84,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0064_join_when_matched_strings.py b/sqlmesh/migrations/v0064_join_when_matched_strings.py index 6ca187be30..455bf9e2c0 100644 --- a/sqlmesh/migrations/v0064_join_when_matched_strings.py +++ b/sqlmesh/migrations/v0064_join_when_matched_strings.py @@ -71,7 +71,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0069_update_dev_table_suffix.py b/sqlmesh/migrations/v0069_update_dev_table_suffix.py index 57d0daaddd..1d714a5ba2 100644 --- a/sqlmesh/migrations/v0069_update_dev_table_suffix.py +++ b/sqlmesh/migrations/v0069_update_dev_table_suffix.py @@ -85,7 +85,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types=snapshots_columns_to_types, + target_columns_to_types=snapshots_columns_to_types, ) new_environments = [] @@ -144,7 +144,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( environments_table, pd.DataFrame(new_environments), - columns_to_types=environments_columns_to_types, + target_columns_to_types=environments_columns_to_types, ) diff --git a/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py b/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py index e1b7b32f37..7e14b2d4e1 100644 --- a/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py +++ b/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py @@ -137,7 +137,7 @@ def _migrate_intervals( engine_adapter.insert_append( intervals_table, pd.DataFrame(new_intervals), - columns_to_types=intervals_columns_to_types, + target_columns_to_types=intervals_columns_to_types, ) @@ -215,7 +215,7 @@ def _migrate_snapshots( engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types=snapshots_columns_to_types, + target_columns_to_types=snapshots_columns_to_types, ) diff --git a/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py b/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py index 98d9582bdc..a460399378 100644 --- a/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py +++ b/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py @@ -69,5 +69,5 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types=snapshots_columns_to_types, + target_columns_to_types=snapshots_columns_to_types, ) diff --git a/sqlmesh/migrations/v0075_remove_validate_query.py b/sqlmesh/migrations/v0075_remove_validate_query.py index aa9c3fccb3..137430bec4 100644 --- a/sqlmesh/migrations/v0075_remove_validate_query.py +++ b/sqlmesh/migrations/v0075_remove_validate_query.py @@ -69,7 +69,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0081_update_partitioned_by.py b/sqlmesh/migrations/v0081_update_partitioned_by.py index d6fd2dd669..e5c98bd8e3 100644 --- a/sqlmesh/migrations/v0081_update_partitioned_by.py +++ b/sqlmesh/migrations/v0081_update_partitioned_by.py @@ -78,7 +78,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0085_deterministic_repr.py b/sqlmesh/migrations/v0085_deterministic_repr.py index 4c86969843..b5f0203c6d 100644 --- a/sqlmesh/migrations/v0085_deterministic_repr.py +++ b/sqlmesh/migrations/v0085_deterministic_repr.py @@ -117,7 +117,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0087_normalize_blueprint_variables.py b/sqlmesh/migrations/v0087_normalize_blueprint_variables.py index 8878bc8019..12648b5a2e 100644 --- a/sqlmesh/migrations/v0087_normalize_blueprint_variables.py +++ b/sqlmesh/migrations/v0087_normalize_blueprint_variables.py @@ -124,7 +124,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/migrations/v0090_add_forward_only_column.py b/sqlmesh/migrations/v0090_add_forward_only_column.py index 32efc14eed..cdc3fc857a 100644 --- a/sqlmesh/migrations/v0090_add_forward_only_column.py +++ b/sqlmesh/migrations/v0090_add_forward_only_column.py @@ -85,7 +85,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.insert_append( snapshots_table, pd.DataFrame(new_snapshots), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), diff --git a/sqlmesh/utils/__init__.py b/sqlmesh/utils/__init__.py index 80e4fa5934..c220de4847 100644 --- a/sqlmesh/utils/__init__.py +++ b/sqlmesh/utils/__init__.py @@ -403,3 +403,15 @@ def __str__(self) -> str: @classmethod def from_plan_id(cls, plan_id: str) -> CorrelationId: return CorrelationId(JobType.PLAN, plan_id) + + +def get_source_columns_to_types( + columns_to_types: t.Dict[str, exp.DataType], + source_columns: t.Optional[t.List[str]], +) -> t.Dict[str, exp.DataType]: + source_column_lookup = set(source_columns) if source_columns else None + return { + k: v + for k, v in columns_to_types.items() + if not source_column_lookup or k in source_column_lookup + } diff --git a/tests/core/engine_adapter/integration/__init__.py b/tests/core/engine_adapter/integration/__init__.py index 15339eeaa6..50437338ae 100644 --- a/tests/core/engine_adapter/integration/__init__.py +++ b/tests/core/engine_adapter/integration/__init__.py @@ -340,7 +340,7 @@ def input_data( list(data.itertuples(index=False, name=None)), batch_start=0, batch_end=sys.maxsize, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, ) if self.test_type == "df": formatted_df = self._format_df(data, to_datetime=self.dialect != "trino") diff --git a/tests/core/engine_adapter/integration/conftest.py b/tests/core/engine_adapter/integration/conftest.py index f072ca77f5..4d374cfdbc 100644 --- a/tests/core/engine_adapter/integration/conftest.py +++ b/tests/core/engine_adapter/integration/conftest.py @@ -145,7 +145,7 @@ def ctx_df( yield from create_test_context(*request.param) -@pytest.fixture(params=list(generate_pytest_params(ENGINES, query=True, df=True))) +@pytest.fixture(params=list(generate_pytest_params(ENGINES, query=True, df=False))) def ctx_query_and_df( request: FixtureRequest, create_test_context: t.Callable[[IntegrationTestEngine, str], t.Iterable[TestContext]], diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 039159825b..1b7d54a2d9 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -263,6 +263,52 @@ def test_ctas(ctx_query_and_df: TestContext): ctx.engine_adapter.ctas(table, exp.select("1").limit(0)) +def test_ctas_source_columns(ctx_query_and_df: TestContext): + ctx = ctx_query_and_df + table = ctx.table("test_table") + + columns_to_types = ctx.columns_to_types.copy() + columns_to_types["ignored_column"] = exp.DataType.build("int") + + input_data = pd.DataFrame( + [ + {"id": 1, "ds": "2022-01-01"}, + {"id": 2, "ds": "2022-01-02"}, + {"id": 3, "ds": "2022-01-03"}, + ] + ) + ctx.engine_adapter.ctas( + table, + ctx.input_data(input_data), + table_description="test table description", + column_descriptions={"id": "test id column description"}, + table_format=ctx.default_table_format, + target_columns_to_types=columns_to_types, + source_columns=["id", "ds"], + ) + + expected_data = input_data.copy() + expected_data["ignored_column"] = pd.Series() + + results = ctx.get_metadata_results(schema=table.db) + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + ctx.compare_with_current(table, expected_data) + + if ctx.engine_adapter.COMMENT_CREATION_TABLE.is_supported: + table_description = ctx.get_table_comment(table.db, table.name) + column_comments = ctx.get_column_comments(table.db, table.name) + + assert table_description == "test table description" + assert column_comments == {"id": "test id column description"} + + # ensure we don't hit clickhouse INSERT with LIMIT 0 bug on CTAS + if ctx.dialect == "clickhouse": + ctx.engine_adapter.ctas(table, exp.select("1").limit(0)) + + def test_create_view(ctx_query_and_df: TestContext): ctx = ctx_query_and_df input_data = pd.DataFrame( @@ -306,6 +352,47 @@ def test_create_view(ctx_query_and_df: TestContext): ) +def test_create_view_source_columns(ctx_query_and_df: TestContext): + ctx = ctx_query_and_df + + columns_to_types = ctx.columns_to_types.copy() + columns_to_types["ignored_column"] = exp.DataType.build("int") + + input_data = pd.DataFrame( + [ + {"id": 1, "ds": "2022-01-01"}, + {"id": 2, "ds": "2022-01-02"}, + {"id": 3, "ds": "2022-01-03"}, + ] + ) + view = ctx.table("test_view") + ctx.engine_adapter.create_view( + view, + ctx.input_data(input_data), + table_description="test view description", + column_descriptions={"id": "test id column description"}, + source_columns=["id", "ds"], + target_columns_to_types=columns_to_types, + ) + + expected_data = input_data.copy() + expected_data["ignored_column"] = pd.Series() + + results = ctx.get_metadata_results() + assert len(results.tables) == 0 + assert len(results.views) == 1 + assert len(results.materialized_views) == 0 + assert results.views[0] == view.name + ctx.compare_with_current(view, expected_data) + + if ctx.engine_adapter.COMMENT_CREATION_VIEW.is_supported: + table_description = ctx.get_table_comment(view.db, "test_view", table_kind="VIEW") + column_comments = ctx.get_column_comments(view.db, "test_view", table_kind="VIEW") + + assert table_description == "test view description" + assert column_comments == {"id": "test id column description"} + + def test_materialized_view(ctx_query_and_df: TestContext): ctx = ctx_query_and_df if not ctx.engine_adapter.SUPPORTS_MATERIALIZED_VIEWS: @@ -383,7 +470,7 @@ def test_nan_roundtrip(ctx_df: TestContext): ctx.engine_adapter.replace_query( table, ctx.input_data(input_data), - columns_to_types=ctx.columns_to_types, + target_columns_to_types=ctx.columns_to_types, ) results = ctx.get_metadata_results() assert not results.views @@ -415,7 +502,9 @@ def test_replace_query(ctx_query_and_df: TestContext): # provided then it checks the table itself for types. This is fine within SQLMesh since we always know the tables # exist prior to evaluation but when running these tests that isn't the case. As a result we just pass in # columns_to_types for these two engines so we can still test inference on the other ones - columns_to_types=ctx.columns_to_types if ctx.dialect in ["spark", "databricks"] else None, + target_columns_to_types=ctx.columns_to_types + if ctx.dialect in ["spark", "databricks"] + else None, table_format=ctx.default_table_format, ) results = ctx.get_metadata_results() @@ -437,7 +526,7 @@ def test_replace_query(ctx_query_and_df: TestContext): ctx.engine_adapter.replace_query( table, ctx.input_data(replace_data), - columns_to_types=( + target_columns_to_types=( ctx.columns_to_types if ctx.dialect in ["spark", "databricks"] else None ), table_format=ctx.default_table_format, @@ -450,6 +539,67 @@ def test_replace_query(ctx_query_and_df: TestContext): ctx.compare_with_current(table, replace_data) +def test_replace_query_source_columns(ctx_query_and_df: TestContext): + ctx = ctx_query_and_df + ctx.engine_adapter.DEFAULT_BATCH_SIZE = sys.maxsize + table = ctx.table("test_table") + + columns_to_types = ctx.columns_to_types.copy() + columns_to_types["ignored_column"] = exp.DataType.build("int") + + # Initial Load + input_data = pd.DataFrame( + [ + {"id": 1, "ds": "2022-01-01"}, + {"id": 2, "ds": "2022-01-02"}, + {"id": 3, "ds": "2022-01-03"}, + ] + ) + ctx.engine_adapter.create_table(table, columns_to_types, table_format=ctx.default_table_format) + ctx.engine_adapter.replace_query( + table, + ctx.input_data(input_data), + table_format=ctx.default_table_format, + source_columns=["id", "ds"], + target_columns_to_types=columns_to_types, + ) + expected_data = input_data.copy() + expected_data["ignored_column"] = pd.Series() + + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + ctx.compare_with_current(table, expected_data) + + # Replace that we only need to run once + if type == "df": + replace_data = pd.DataFrame( + [ + {"id": 4, "ds": "2022-01-04"}, + {"id": 5, "ds": "2022-01-05"}, + {"id": 6, "ds": "2022-01-06"}, + ] + ) + ctx.engine_adapter.replace_query( + table, + ctx.input_data(replace_data), + table_format=ctx.default_table_format, + source_columns=["id", "ds"], + target_columns_to_types=columns_to_types, + ) + expected_data = replace_data.copy() + expected_data["ignored_column"] = pd.Series() + + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + ctx.compare_with_current(table, expected_data) + + def test_replace_query_batched(ctx_query_and_df: TestContext): ctx = ctx_query_and_df ctx.engine_adapter.DEFAULT_BATCH_SIZE = 1 @@ -472,7 +622,9 @@ def test_replace_query_batched(ctx_query_and_df: TestContext): # provided then it checks the table itself for types. This is fine within SQLMesh since we always know the tables # exist prior to evaluation but when running these tests that isn't the case. As a result we just pass in # columns_to_types for these two engines so we can still test inference on the other ones - columns_to_types=ctx.columns_to_types if ctx.dialect in ["spark", "databricks"] else None, + target_columns_to_types=ctx.columns_to_types + if ctx.dialect in ["spark", "databricks"] + else None, table_format=ctx.default_table_format, ) results = ctx.get_metadata_results() @@ -494,7 +646,7 @@ def test_replace_query_batched(ctx_query_and_df: TestContext): ctx.engine_adapter.replace_query( table, ctx.input_data(replace_data), - columns_to_types=( + target_columns_to_types=( ctx.columns_to_types if ctx.dialect in ["spark", "databricks"] else None ), table_format=ctx.default_table_format, @@ -548,6 +700,61 @@ def test_insert_append(ctx_query_and_df: TestContext): ctx.compare_with_current(table, pd.concat([input_data, append_data])) +def test_insert_append_source_columns(ctx_query_and_df: TestContext): + ctx = ctx_query_and_df + table = ctx.table("test_table") + columns_to_types = ctx.columns_to_types.copy() + columns_to_types["ignored_column"] = exp.DataType.build("int") + ctx.engine_adapter.create_table(table, columns_to_types, table_format=ctx.default_table_format) + # Initial Load + input_data = pd.DataFrame( + [ + {"id": 1, "ds": "2022-01-01"}, + {"id": 2, "ds": "2022-01-02"}, + {"id": 3, "ds": "2022-01-03"}, + ] + ) + ctx.engine_adapter.insert_append( + table, + ctx.input_data(input_data), + source_columns=["id", "ds"], + target_columns_to_types=columns_to_types, + ) + expected_data = input_data.copy() + expected_data["ignored_column"] = pd.Series() + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + ctx.compare_with_current(table, expected_data) + + # Replace that we only need to run once + if ctx.test_type == "df": + append_data = pd.DataFrame( + [ + {"id": 4, "ds": "2022-01-04"}, + {"id": 5, "ds": "2022-01-05"}, + {"id": 6, "ds": "2022-01-06"}, + ] + ) + ctx.engine_adapter.insert_append( + table, + ctx.input_data(append_data), + source_columns=["id", "ds"], + target_columns_to_types=columns_to_types, + ) + append_expected_data = append_data.copy() + append_expected_data["ignored_column"] = pd.Series() + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) in [1, 2, 3] + assert len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + ctx.compare_with_current(table, pd.concat([expected_data, append_expected_data])) + + def test_insert_overwrite_by_time_partition(ctx_query_and_df: TestContext): ctx = ctx_query_and_df ds_type = "string" @@ -583,7 +790,7 @@ def test_insert_overwrite_by_time_partition(ctx_query_and_df: TestContext): end="2022-01-03", time_formatter=ctx.time_formatter, time_column=ctx.time_column, - columns_to_types=ctx.columns_to_types, + target_columns_to_types=ctx.columns_to_types, ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -612,7 +819,7 @@ def test_insert_overwrite_by_time_partition(ctx_query_and_df: TestContext): end="2022-01-05", time_formatter=ctx.time_formatter, time_column=ctx.time_column, - columns_to_types=ctx.columns_to_types, + target_columns_to_types=ctx.columns_to_types, ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -636,6 +843,105 @@ def test_insert_overwrite_by_time_partition(ctx_query_and_df: TestContext): ) +def test_insert_overwrite_by_time_partition_source_columns(ctx_query_and_df: TestContext): + ctx = ctx_query_and_df + ds_type = "string" + if ctx.dialect == "bigquery": + ds_type = "datetime" + if ctx.dialect == "tsql": + ds_type = "varchar(max)" + + ctx.columns_to_types = {"id": "int", "ds": ds_type} + columns_to_types = { + "id": exp.DataType.build("int"), + "ignored_column": exp.DataType.build("int"), + "ds": exp.DataType.build(ds_type), + } + table = ctx.table("test_table") + if ctx.dialect == "bigquery": + partitioned_by = ["DATE(ds)"] + else: + partitioned_by = ctx.partitioned_by # type: ignore + ctx.engine_adapter.create_table( + table, + columns_to_types, + partitioned_by=partitioned_by, + partition_interval_unit="DAY", + table_format=ctx.default_table_format, + ) + input_data = pd.DataFrame( + [ + {"id": 1, ctx.time_column: "2022-01-01"}, + {"id": 2, ctx.time_column: "2022-01-02"}, + {"id": 3, ctx.time_column: "2022-01-03"}, + ] + ) + ctx.engine_adapter.insert_overwrite_by_time_partition( + table, + ctx.input_data(input_data), + start="2022-01-02", + end="2022-01-03", + time_formatter=ctx.time_formatter, + time_column=ctx.time_column, + target_columns_to_types=columns_to_types, + source_columns=["id", "ds"], + ) + + expected_data = input_data.copy() + expected_data.insert(len(expected_data.columns) - 1, "ignored_column", pd.Series()) + + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + + if ctx.dialect == "trino": + # trino has some lag between partitions being registered and data showing up + wait_until(lambda: len(ctx.get_current_data(table)) > 0) + + ctx.compare_with_current(table, expected_data.iloc[1:]) + + if ctx.test_type == "df": + overwrite_data = pd.DataFrame( + [ + {"id": 10, ctx.time_column: "2022-01-03"}, + {"id": 4, ctx.time_column: "2022-01-04"}, + {"id": 5, ctx.time_column: "2022-01-05"}, + ] + ) + ctx.engine_adapter.insert_overwrite_by_time_partition( + table, + ctx.input_data(overwrite_data), + start="2022-01-03", + end="2022-01-05", + time_formatter=ctx.time_formatter, + time_column=ctx.time_column, + target_columns_to_types=columns_to_types, + source_columns=["id", "ds"], + ) + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + + if ctx.dialect == "trino": + wait_until(lambda: len(ctx.get_current_data(table)) > 2) + + ctx.compare_with_current( + table, + pd.DataFrame( + [ + {"id": 2, "ignored_column": None, ctx.time_column: "2022-01-02"}, + {"id": 10, "ignored_column": None, ctx.time_column: "2022-01-03"}, + {"id": 4, "ignored_column": None, ctx.time_column: "2022-01-04"}, + {"id": 5, "ignored_column": None, ctx.time_column: "2022-01-05"}, + ] + ), + ) + + def test_merge(ctx_query_and_df: TestContext): ctx = ctx_query_and_df if not ctx.supports_merge: @@ -658,7 +964,7 @@ def test_merge(ctx_query_and_df: TestContext): ctx.engine_adapter.merge( table, ctx.input_data(input_data), - columns_to_types=None, + target_columns_to_types=None, unique_key=[exp.to_identifier("id")], ) results = ctx.get_metadata_results() @@ -680,7 +986,7 @@ def test_merge(ctx_query_and_df: TestContext): ctx.engine_adapter.merge( table, ctx.input_data(merge_data), - columns_to_types=None, + target_columns_to_types=None, unique_key=[exp.to_identifier("id")], ) results = ctx.get_metadata_results() @@ -702,20 +1008,96 @@ def test_merge(ctx_query_and_df: TestContext): ) -def test_scd_type_2_by_time(ctx_query_and_df: TestContext): +def test_merge_source_columns(ctx_query_and_df: TestContext): ctx = ctx_query_and_df - # Athena only supports the operations required for SCD models on Iceberg tables - if ctx.mark == "athena_hive": - pytest.skip("SCD Type 2 is only supported on Athena / Iceberg") + if not ctx.supports_merge: + pytest.skip(f"{ctx.dialect} doesn't support merge") - time_type = exp.DataType.build("timestamp") + table = ctx.table("test_table") - ctx.columns_to_types = { - "id": "int", - "name": "string", - "updated_at": time_type, - "valid_from": time_type, - "valid_to": time_type, + # Athena only supports MERGE on Iceberg tables + # And it cant fall back to a logical merge on Hive tables because it cant delete records + table_format = "iceberg" if ctx.dialect == "athena" else None + + columns_to_types = ctx.columns_to_types.copy() + columns_to_types["ignored_column"] = exp.DataType.build("int") + + ctx.engine_adapter.create_table(table, columns_to_types, table_format=table_format) + input_data = pd.DataFrame( + [ + {"id": 1, "ds": "2022-01-01"}, + {"id": 2, "ds": "2022-01-02"}, + {"id": 3, "ds": "2022-01-03"}, + ] + ) + ctx.engine_adapter.merge( + table, + ctx.input_data(input_data), + unique_key=[exp.to_identifier("id")], + target_columns_to_types=columns_to_types, + source_columns=["id", "ds"], + ) + + expected_data = input_data.copy() + expected_data["ignored_column"] = pd.Series() + + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + ctx.compare_with_current(table, expected_data) + + if ctx.test_type == "df": + merge_data = pd.DataFrame( + [ + {"id": 2, "ds": "2022-01-10"}, + {"id": 4, "ds": "2022-01-04"}, + {"id": 5, "ds": "2022-01-05"}, + ] + ) + ctx.engine_adapter.merge( + table, + ctx.input_data(merge_data), + unique_key=[exp.to_identifier("id")], + target_columns_to_types=columns_to_types, + source_columns=["id", "ds"], + ) + + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + ctx.compare_with_current( + table, + pd.DataFrame( + [ + {"id": 1, "ds": "2022-01-01", "ignored_column": None}, + {"id": 2, "ds": "2022-01-10", "ignored_column": None}, + {"id": 3, "ds": "2022-01-03", "ignored_column": None}, + {"id": 4, "ds": "2022-01-04", "ignored_column": None}, + {"id": 5, "ds": "2022-01-05", "ignored_column": None}, + ] + ), + ) + + +def test_scd_type_2_by_time(ctx_query_and_df: TestContext): + ctx = ctx_query_and_df + # Athena only supports the operations required for SCD models on Iceberg tables + if ctx.mark == "athena_hive": + pytest.skip("SCD Type 2 is only supported on Athena / Iceberg") + + time_type = exp.DataType.build("timestamp") + + ctx.columns_to_types = { + "id": "int", + "name": "string", + "updated_at": time_type, + "valid_from": time_type, + "valid_to": time_type, } table = ctx.table("test_table") input_schema = { @@ -741,9 +1123,167 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): updated_at_col=exp.column("updated_at", quoted=True), execution_time="2023-01-01 00:00:00", updated_at_as_valid_from=False, - columns_to_types=input_schema, + target_columns_to_types=input_schema, + table_format=ctx.default_table_format, + truncate=True, + ) + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + ctx.compare_with_current( + table, + pd.DataFrame( + [ + { + "id": 1, + "name": "a", + "updated_at": "2022-01-01 00:00:00", + "valid_from": "1970-01-01 00:00:00", + "valid_to": pd.NaT, + }, + { + "id": 2, + "name": "b", + "updated_at": "2022-01-02 00:00:00", + "valid_from": "1970-01-01 00:00:00", + "valid_to": pd.NaT, + }, + { + "id": 3, + "name": "c", + "updated_at": "2022-01-03 00:00:00", + "valid_from": "1970-01-01 00:00:00", + "valid_to": pd.NaT, + }, + ] + ), + ) + + if ctx.test_type == "query": + return + + current_data = pd.DataFrame( + [ + # Change `a` to `x` + {"id": 1, "name": "x", "updated_at": "2022-01-04 00:00:00"}, + # Delete + # {"id": 2, "name": "b", "updated_at": "2022-01-02 00:00:00"}, + # No change + {"id": 3, "name": "c", "updated_at": "2022-01-03 00:00:00"}, + # Add + {"id": 4, "name": "d", "updated_at": "2022-01-04 00:00:00"}, + ] + ) + ctx.engine_adapter.scd_type_2_by_time( + table, + ctx.input_data(current_data, input_schema), + unique_key=[exp.to_column("id")], + valid_from_col=exp.column("valid_from", quoted=True), + valid_to_col=exp.column("valid_to", quoted=True), + updated_at_col=exp.column("updated_at", quoted=True), + execution_time="2023-01-05 00:00:00", + updated_at_as_valid_from=False, + target_columns_to_types=input_schema, + table_format=ctx.default_table_format, + truncate=False, + ) + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + ctx.compare_with_current( + table, + pd.DataFrame( + [ + { + "id": 1, + "name": "a", + "updated_at": "2022-01-01 00:00:00", + "valid_from": "1970-01-01 00:00:00", + "valid_to": "2022-01-04 00:00:00", + }, + { + "id": 1, + "name": "x", + "updated_at": "2022-01-04 00:00:00", + "valid_from": "2022-01-04 00:00:00", + "valid_to": pd.NaT, + }, + { + "id": 2, + "name": "b", + "updated_at": "2022-01-02 00:00:00", + "valid_from": "1970-01-01 00:00:00", + "valid_to": "2023-01-05 00:00:00", + }, + { + "id": 3, + "name": "c", + "updated_at": "2022-01-03 00:00:00", + "valid_from": "1970-01-01 00:00:00", + "valid_to": pd.NaT, + }, + { + "id": 4, + "name": "d", + "updated_at": "2022-01-04 00:00:00", + "valid_from": "1970-01-01 00:00:00", + "valid_to": pd.NaT, + }, + ] + ), + ) + + +def test_scd_type_2_by_time_source_columns(ctx_query_and_df: TestContext): + ctx = ctx_query_and_df + # Athena only supports the operations required for SCD models on Iceberg tables + if ctx.mark == "athena_hive": + pytest.skip("SCD Type 2 is only supported on Athena / Iceberg") + + time_type = exp.DataType.build("timestamp") + + ctx.columns_to_types = { + "id": "int", + "name": "string", + "updated_at": time_type, + "valid_from": time_type, + "valid_to": time_type, + } + columns_to_types = ctx.columns_to_types.copy() + columns_to_types["ignored_column"] = exp.DataType.build("int") + + table = ctx.table("test_table") + input_schema = { + k: v for k, v in ctx.columns_to_types.items() if k not in ("valid_from", "valid_to") + } + + ctx.engine_adapter.create_table(table, columns_to_types, table_format=ctx.default_table_format) + input_data = pd.DataFrame( + [ + {"id": 1, "name": "a", "updated_at": "2022-01-01 00:00:00"}, + {"id": 2, "name": "b", "updated_at": "2022-01-02 00:00:00"}, + {"id": 3, "name": "c", "updated_at": "2022-01-03 00:00:00"}, + ] + ) + ctx.engine_adapter.scd_type_2_by_time( + table, + ctx.input_data(input_data, input_schema), + unique_key=[parse_one("COALESCE(id, -1)")], + valid_from_col=exp.column("valid_from", quoted=True), + valid_to_col=exp.column("valid_to", quoted=True), + updated_at_col=exp.column("updated_at", quoted=True), + execution_time="2023-01-01 00:00:00", + updated_at_as_valid_from=False, table_format=ctx.default_table_format, truncate=True, + start="2022-01-01 00:00:00", + target_columns_to_types=columns_to_types, + source_columns=["id", "name", "updated_at"], ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -761,6 +1301,7 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): "updated_at": "2022-01-01 00:00:00", "valid_from": "1970-01-01 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, { "id": 2, @@ -768,6 +1309,7 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): "updated_at": "2022-01-02 00:00:00", "valid_from": "1970-01-01 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, { "id": 3, @@ -775,6 +1317,7 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): "updated_at": "2022-01-03 00:00:00", "valid_from": "1970-01-01 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, ] ), @@ -804,9 +1347,11 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): updated_at_col=exp.column("updated_at", quoted=True), execution_time="2023-01-05 00:00:00", updated_at_as_valid_from=False, - columns_to_types=input_schema, table_format=ctx.default_table_format, truncate=False, + start="2022-01-01 00:00:00", + target_columns_to_types=columns_to_types, + source_columns=["id", "name", "updated_at"], ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -823,6 +1368,7 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): "updated_at": "2022-01-01 00:00:00", "valid_from": "1970-01-01 00:00:00", "valid_to": "2022-01-04 00:00:00", + "ignored_column": None, }, { "id": 1, @@ -830,6 +1376,7 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): "updated_at": "2022-01-04 00:00:00", "valid_from": "2022-01-04 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, { "id": 2, @@ -837,6 +1384,7 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): "updated_at": "2022-01-02 00:00:00", "valid_from": "1970-01-01 00:00:00", "valid_to": "2023-01-05 00:00:00", + "ignored_column": None, }, { "id": 3, @@ -844,6 +1392,7 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): "updated_at": "2022-01-03 00:00:00", "valid_from": "1970-01-01 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, { "id": 4, @@ -851,6 +1400,7 @@ def test_scd_type_2_by_time(ctx_query_and_df: TestContext): "updated_at": "2022-01-04 00:00:00", "valid_from": "1970-01-01 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, ] ), @@ -897,8 +1447,188 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): valid_to_col=exp.column("valid_to", quoted=True), execution_time="2023-01-01", execution_time_as_valid_from=False, - columns_to_types=ctx.columns_to_types, + target_columns_to_types=ctx.columns_to_types, + truncate=True, + ) + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + ctx.compare_with_current( + table, + pd.DataFrame( + [ + { + "id": 1, + "name": "a", + "status": "active", + "valid_from": "1970-01-01 00:00:00", + "valid_to": pd.NaT, + }, + { + "id": 2, + "name": "b", + "status": "inactive", + "valid_from": "1970-01-01 00:00:00", + "valid_to": pd.NaT, + }, + { + "id": 3, + "name": "c", + "status": "active", + "valid_from": "1970-01-01 00:00:00", + "valid_to": pd.NaT, + }, + { + "id": 4, + "name": "d", + "status": "active", + "valid_from": "1970-01-01 00:00:00", + "valid_to": pd.NaT, + }, + ] + ), + ) + + if ctx.test_type == "query": + return + + current_data = pd.DataFrame( + [ + # Change `a` to `x` + {"id": 1, "name": "x", "status": "active"}, + # Delete + # {"id": 2, "name": "b", status: "inactive"}, + # No change + {"id": 3, "name": "c", "status": "active"}, + # Change status to inactive + {"id": 4, "name": "d", "status": "inactive"}, + # Add + {"id": 5, "name": "e", "status": "inactive"}, + ] + ) + ctx.engine_adapter.scd_type_2_by_column( + table, + ctx.input_data(current_data, input_schema), + unique_key=[exp.to_column("id")], + check_columns=[exp.to_column("name"), exp.to_column("status")], + valid_from_col=exp.column("valid_from", quoted=True), + valid_to_col=exp.column("valid_to", quoted=True), + execution_time="2023-01-05 00:00:00", + execution_time_as_valid_from=False, + target_columns_to_types=ctx.columns_to_types, + truncate=False, + ) + results = ctx.get_metadata_results() + assert len(results.views) == 0 + assert len(results.materialized_views) == 0 + assert len(results.tables) == len(results.non_temp_tables) == 1 + assert results.non_temp_tables[0] == table.name + ctx.compare_with_current( + table, + pd.DataFrame( + [ + { + "id": 1, + "name": "a", + "status": "active", + "valid_from": "1970-01-01 00:00:00", + "valid_to": "2023-01-05 00:00:00", + }, + { + "id": 1, + "name": "x", + "status": "active", + "valid_from": "2023-01-05 00:00:00", + "valid_to": pd.NaT, + }, + { + "id": 2, + "name": "b", + "status": "inactive", + "valid_from": "1970-01-01 00:00:00", + "valid_to": "2023-01-05 00:00:00", + }, + { + "id": 3, + "name": "c", + "status": "active", + "valid_from": "1970-01-01 00:00:00", + "valid_to": pd.NaT, + }, + { + "id": 4, + "name": "d", + "status": "active", + "valid_from": "1970-01-01 00:00:00", + "valid_to": "2023-01-05 00:00:00", + }, + { + "id": 4, + "name": "d", + "status": "inactive", + "valid_from": "2023-01-05 00:00:00", + "valid_to": pd.NaT, + }, + { + "id": 5, + "name": "e", + "status": "inactive", + "valid_from": "2023-01-05 00:00:00", + "valid_to": pd.NaT, + }, + ] + ), + ) + + +def test_scd_type_2_by_column_source_columns(ctx_query_and_df: TestContext): + ctx = ctx_query_and_df + # Athena only supports the operations required for SCD models on Iceberg tables + if ctx.mark == "athena_hive": + pytest.skip("SCD Type 2 is only supported on Athena / Iceberg") + + time_type = exp.DataType.build("timestamp") + + ctx.columns_to_types = { + "id": "int", + "name": "string", + "status": "string", + "valid_from": time_type, + "valid_to": time_type, + } + columns_to_types = ctx.columns_to_types.copy() + columns_to_types["ignored_column"] = exp.DataType.build("int") + + table = ctx.table("test_table") + input_schema = { + k: v for k, v in ctx.columns_to_types.items() if k not in ("valid_from", "valid_to") + } + + ctx.engine_adapter.create_table(table, columns_to_types, table_format=ctx.default_table_format) + input_data = pd.DataFrame( + [ + {"id": 1, "name": "a", "status": "active"}, + {"id": 2, "name": "b", "status": "inactive"}, + {"id": 3, "name": "c", "status": "active"}, + {"id": 4, "name": "d", "status": "active"}, + ] + ) + ctx.engine_adapter.scd_type_2_by_column( + table, + ctx.input_data(input_data, input_schema), + unique_key=[exp.to_column("id")], + check_columns=[exp.to_column("name"), exp.to_column("status")], + valid_from_col=exp.column("valid_from", quoted=True), + valid_to_col=exp.column("valid_to", quoted=True), + execution_time="2023-01-01", + execution_time_as_valid_from=False, truncate=True, + start="2023-01-01", + target_columns_to_types=columns_to_types, + source_columns=["id", "name", "status"], ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -916,6 +1646,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): "status": "active", "valid_from": "1970-01-01 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, { "id": 2, @@ -923,6 +1654,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): "status": "inactive", "valid_from": "1970-01-01 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, { "id": 3, @@ -930,6 +1662,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): "status": "active", "valid_from": "1970-01-01 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, { "id": 4, @@ -937,6 +1670,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): "status": "active", "valid_from": "1970-01-01 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, ] ), @@ -968,8 +1702,10 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): valid_to_col=exp.column("valid_to", quoted=True), execution_time="2023-01-05 00:00:00", execution_time_as_valid_from=False, - columns_to_types=ctx.columns_to_types, truncate=False, + start="2023-01-01", + target_columns_to_types=columns_to_types, + source_columns=["id", "name", "status"], ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -986,6 +1722,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): "status": "active", "valid_from": "1970-01-01 00:00:00", "valid_to": "2023-01-05 00:00:00", + "ignored_column": None, }, { "id": 1, @@ -993,6 +1730,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): "status": "active", "valid_from": "2023-01-05 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, { "id": 2, @@ -1000,6 +1738,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): "status": "inactive", "valid_from": "1970-01-01 00:00:00", "valid_to": "2023-01-05 00:00:00", + "ignored_column": None, }, { "id": 3, @@ -1007,6 +1746,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): "status": "active", "valid_from": "1970-01-01 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, { "id": 4, @@ -1014,6 +1754,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): "status": "active", "valid_from": "1970-01-01 00:00:00", "valid_to": "2023-01-05 00:00:00", + "ignored_column": None, }, { "id": 4, @@ -1021,6 +1762,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): "status": "inactive", "valid_from": "2023-01-05 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, { "id": 5, @@ -1028,6 +1770,7 @@ def test_scd_type_2_by_column(ctx_query_and_df: TestContext): "status": "inactive", "valid_from": "2023-01-05 00:00:00", "valid_to": pd.NaT, + "ignored_column": None, }, ] ), @@ -2338,13 +3081,13 @@ def test_value_normalization( } ctx.engine_adapter.create_table( - table_name=test_table, columns_to_types=columns_to_types_normalized + table_name=test_table, target_columns_to_types=columns_to_types_normalized ) data_query = next(select_from_values(input_data_with_idx, columns_to_types_normalized)) ctx.engine_adapter.insert_append( table_name=test_table, query_or_df=data_query, - columns_to_types=columns_to_types_normalized, + target_columns_to_types=columns_to_types_normalized, ) query = ( diff --git a/tests/core/engine_adapter/integration/test_integration_athena.py b/tests/core/engine_adapter/integration/test_integration_athena.py index 33e76fc6e2..1c0ece6d78 100644 --- a/tests/core/engine_adapter/integration/test_integration_athena.py +++ b/tests/core/engine_adapter/integration/test_integration_athena.py @@ -284,10 +284,10 @@ def test_hive_drop_table_removes_data(ctx: TestContext, engine_adapter: AthenaEn columns_to_types = columns_to_types_from_df(data) engine_adapter.create_table( - table_name=seed_table, columns_to_types=columns_to_types, exists=False + table_name=seed_table, target_columns_to_types=columns_to_types, exists=False ) engine_adapter.insert_append( - table_name=seed_table, query_or_df=data, columns_to_types=columns_to_types + table_name=seed_table, query_or_df=data, target_columns_to_types=columns_to_types ) assert engine_adapter.fetchone(f"select count(*) from {seed_table}")[0] == 1 # type: ignore @@ -295,7 +295,7 @@ def test_hive_drop_table_removes_data(ctx: TestContext, engine_adapter: AthenaEn # This ensures that our drop table logic to delete the data from S3 is working engine_adapter.drop_table(seed_table, exists=False) engine_adapter.create_table( - table_name=seed_table, columns_to_types=columns_to_types, exists=False + table_name=seed_table, target_columns_to_types=columns_to_types, exists=False ) assert engine_adapter.fetchone(f"select count(*) from {seed_table}")[0] == 0 # type: ignore @@ -382,12 +382,14 @@ def time_formatter(time: TimeLike, _: t.Optional[t.Dict[str, exp.DataType]]) -> return exp.cast(exp.Literal.string(to_ds(time)), "date") engine_adapter.create_table( - table_name=table, columns_to_types=columns_to_types, partitioned_by=[exp.to_column("date")] + table_name=table, + target_columns_to_types=columns_to_types, + partitioned_by=[exp.to_column("date")], ) engine_adapter.insert_overwrite_by_time_partition( table_name=table, query_or_df=data, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, time_column=exp.to_identifier("date"), start="2023-01-01", end="2023-01-03", @@ -406,7 +408,7 @@ def time_formatter(time: TimeLike, _: t.Optional[t.Dict[str, exp.DataType]]) -> engine_adapter.insert_overwrite_by_time_partition( table_name=table, query_or_df=new_data, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, time_column=exp.to_identifier("date"), start="2023-01-03", end="2023-01-04", @@ -442,12 +444,14 @@ def time_formatter(time: TimeLike, _: t.Optional[t.Dict[str, exp.DataType]]) -> return exp.cast(exp.Literal.string(to_ts(time)), "datetime") engine_adapter.create_table( - table_name=table, columns_to_types=columns_to_types, partitioned_by=[exp.to_column("ts")] + table_name=table, + target_columns_to_types=columns_to_types, + partitioned_by=[exp.to_column("ts")], ) engine_adapter.insert_overwrite_by_time_partition( table_name=table, query_or_df=data, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, time_column=exp.to_identifier("ts"), start="2023-01-01 00:00:00", end="2023-01-01 04:00:00", @@ -469,7 +473,7 @@ def time_formatter(time: TimeLike, _: t.Optional[t.Dict[str, exp.DataType]]) -> engine_adapter.insert_overwrite_by_time_partition( table_name=table, query_or_df=new_data, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, time_column=exp.to_identifier("ts"), start="2023-01-01 03:00:00", end="2023-01-01 05:00:00", diff --git a/tests/core/engine_adapter/test_athena.py b/tests/core/engine_adapter/test_athena.py index 5ee07f52d5..4fe57baf34 100644 --- a/tests/core/engine_adapter/test_athena.py +++ b/tests/core/engine_adapter/test_athena.py @@ -133,7 +133,7 @@ def test_create_table_hive(adapter: AthenaEngineAdapter) -> None: adapter.create_table( model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_properties=model.physical_properties, partitioned_by=model.partitioned_by, storage_format=model.storage_format, @@ -165,7 +165,7 @@ def test_create_table_iceberg(adapter: AthenaEngineAdapter) -> None: adapter.create_table( model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_properties=model.physical_properties, partitioned_by=model.partitioned_by, table_format=model.table_format, @@ -193,14 +193,14 @@ def test_create_table_no_location(adapter: AthenaEngineAdapter) -> None: with pytest.raises(SQLMeshError, match=r"Cannot figure out location.*"): adapter.create_table( model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_properties=model.physical_properties, ) adapter.s3_warehouse_location = "s3://bucket/prefix" adapter.create_table( model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_properties=model.physical_properties, ) @@ -214,7 +214,7 @@ def test_ctas_hive(adapter: AthenaEngineAdapter): adapter.ctas( table_name="foo.bar", - columns_to_types={"a": exp.DataType.build("int")}, + target_columns_to_types={"a": exp.DataType.build("int")}, query_or_df=parse_one("select 1", into=exp.Select), ) @@ -228,7 +228,7 @@ def test_ctas_iceberg(adapter: AthenaEngineAdapter): adapter.ctas( table_name="foo.bar", - columns_to_types={"a": exp.DataType.build("int")}, + target_columns_to_types={"a": exp.DataType.build("int")}, query_or_df=parse_one("select 1", into=exp.Select), table_format="iceberg", ) @@ -242,7 +242,7 @@ def test_ctas_iceberg_no_specific_location(adapter: AthenaEngineAdapter): with pytest.raises(SQLMeshError, match=r"Cannot figure out location.*"): adapter.ctas( table_name="foo.bar", - columns_to_types={"a": exp.DataType.build("int")}, + target_columns_to_types={"a": exp.DataType.build("int")}, query_or_df=parse_one("select 1", into=exp.Select), table_properties={"table_type": exp.Literal.string("iceberg")}, ) @@ -270,7 +270,7 @@ def test_ctas_iceberg_partitioned(adapter: AthenaEngineAdapter): adapter.s3_warehouse_location = "s3://bucket/prefix/" adapter.ctas( table_name=model.name, - columns_to_types=model.columns_to_types, + target_columns_to_types=model.columns_to_types, partitioned_by=model.partitioned_by, query_or_df=model.ctas_query(), table_format=model.table_format, @@ -298,7 +298,7 @@ def test_replace_query(adapter: AthenaEngineAdapter, mocker: MockerFixture): adapter.replace_query( table_name="test", query_or_df=parse_one("select 1 as a", into=exp.Select), - columns_to_types={"a": exp.DataType.build("int")}, + target_columns_to_types={"a": exp.DataType.build("int")}, table_properties={}, ) @@ -317,7 +317,7 @@ def test_replace_query(adapter: AthenaEngineAdapter, mocker: MockerFixture): adapter.replace_query( table_name="test", query_or_df=parse_one("select 1 as a", into=exp.Select), - columns_to_types={"a": exp.DataType.build("int")}, + target_columns_to_types={"a": exp.DataType.build("int")}, table_properties={}, ) @@ -482,14 +482,14 @@ def test_iceberg_partition_transforms(adapter: AthenaEngineAdapter): adapter.create_table( table_name=model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, partitioned_by=model.partitioned_by, table_format=model.table_format, ) adapter.ctas( table_name=model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, partitioned_by=model.partitioned_by, query_or_df=model.ctas_query(), table_format=model.table_format, diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index 618d89a445..02029ca6f8 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -76,6 +76,39 @@ def test_create_view_pandas(make_mocked_engine_adapter: t.Callable): ] +def test_create_view_pandas_source_columns(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + bigint_dtype = exp.DataType.build("BIGINT") + adapter.create_view( + "test_view", + pd.DataFrame({"a": [1, 2, 3]}), + target_columns_to_types={"a": bigint_dtype, "b": bigint_dtype}, + replace=False, + source_columns=["a"], + ) + + assert to_sql_calls(adapter) == [ + 'CREATE VIEW "test_view" ("a", "b") AS SELECT "a", CAST(NULL AS BIGINT) AS "b" FROM (SELECT CAST("a" AS BIGINT) AS "a" FROM (VALUES (1), (2), (3)) AS "t"("a")) AS "select_source_columns"', + ] + + +def test_create_view_query_source_columns(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.create_view( + "test_view", + parse_one("SELECT a FROM tbl"), + target_columns_to_types={ + "a": exp.DataType.build("BIGINT"), + "b": exp.DataType.build("BIGINT"), + }, + replace=False, + source_columns=["a"], + ) + assert to_sql_calls(adapter) == [ + 'CREATE VIEW "test_view" ("a", "b") AS SELECT "a", CAST(NULL AS BIGINT) AS "b" FROM (SELECT "a" FROM "tbl") AS "select_source_columns"', + ] + + def test_create_materialized_view(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) adapter.SUPPORTS_MATERIALIZED_VIEWS = True @@ -83,14 +116,14 @@ def test_create_materialized_view(make_mocked_engine_adapter: t.Callable): "test_view", parse_one("SELECT a FROM tbl"), materialized=True, - columns_to_types={"a": exp.DataType.build("INT")}, + target_columns_to_types={"a": exp.DataType.build("INT")}, ) adapter.create_view( "test_view", parse_one("SELECT a FROM tbl"), replace=False, materialized=True, - columns_to_types={"a": exp.DataType.build("INT")}, + target_columns_to_types={"a": exp.DataType.build("INT")}, ) adapter.cursor.execute.assert_has_calls( @@ -106,7 +139,7 @@ def test_create_materialized_view(make_mocked_engine_adapter: t.Callable): parse_one("SELECT a, b FROM tbl"), replace=False, materialized=True, - columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("INT")}, + target_columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("INT")}, ) adapter.create_view( "test_view", parse_one("SELECT a, b FROM tbl"), replace=False, materialized=True @@ -190,7 +223,7 @@ def test_insert_overwrite_by_time_partition(make_mocked_engine_adapter: t.Callab end="2022-01-02", time_column="b", time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), - columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("STRING")}, + target_columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("STRING")}, ) adapter.cursor.begin.assert_called_once() @@ -217,7 +250,10 @@ def test_insert_overwrite_by_time_partition_missing_time_column_type( end="2022-01-02", time_column="b", time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), - columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("UNKNOWN")}, + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("UNKNOWN"), + }, ) columns_mock.assert_called_once_with("test_table") @@ -244,7 +280,7 @@ def test_insert_overwrite_by_time_partition_supports_insert_overwrite( end="2022-01-02", time_column="b", time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), - columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("STRING")}, + target_columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("STRING")}, ) adapter.cursor.execute.assert_called_once_with( @@ -266,7 +302,10 @@ def test_insert_overwrite_by_time_partition_supports_insert_overwrite_pandas( end="2022-01-02", time_column="ds", time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), - columns_to_types={"a": exp.DataType.build("INT"), "ds": exp.DataType.build("STRING")}, + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "ds": exp.DataType.build("STRING"), + }, ) assert to_sql_calls(adapter) == [ @@ -274,6 +313,53 @@ def test_insert_overwrite_by_time_partition_supports_insert_overwrite_pandas( ] +def test_insert_overwrite_by_time_partition_supports_insert_overwrite_pandas_source_columns( + make_mocked_engine_adapter: t.Callable, +): + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.INSERT_OVERWRITE + df = pd.DataFrame({"a": [1, 2]}) + adapter.insert_overwrite_by_time_partition( + "test_table", + df, + start="2022-01-01", + end="2022-01-02", + time_column="ds", + time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "ds": exp.DataType.build("STRING"), + }, + source_columns=["a"], + ) + assert to_sql_calls(adapter) == [ + """INSERT OVERWRITE TABLE "test_table" ("a", "ds") SELECT "a", "ds" FROM (SELECT CAST("a" AS INT) AS "a", CAST(NULL AS TEXT) AS "ds" FROM (VALUES (1), (2)) AS "t"("a")) AS "_subquery" WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02'""" + ] + + +def test_insert_overwrite_by_time_partition_supports_insert_overwrite_query_source_columns( + make_mocked_engine_adapter: t.Callable, +): + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.INSERT_OVERWRITE + adapter.insert_overwrite_by_time_partition( + "test_table", + parse_one("SELECT a FROM tbl"), + start="2022-01-01", + end="2022-01-02", + time_column="ds", + time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "ds": exp.DataType.build("STRING"), + }, + source_columns=["a"], + ) + assert to_sql_calls(adapter) == [ + """INSERT OVERWRITE TABLE "test_table" ("a", "ds") SELECT "a", "ds" FROM (SELECT "a", CAST(NULL AS TEXT) AS "ds" FROM (SELECT "a" FROM "tbl") AS "select_source_columns") AS "_subquery" WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02'""" + ] + + def test_insert_overwrite_by_time_partition_replace_where(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) adapter.INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.REPLACE_WHERE @@ -285,7 +371,7 @@ def test_insert_overwrite_by_time_partition_replace_where(make_mocked_engine_ada end="2022-01-02", time_column="b", time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), - columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("STRING")}, + target_columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("STRING")}, ) assert to_sql_calls(adapter) == [ @@ -308,7 +394,10 @@ def test_insert_overwrite_by_time_partition_replace_where_pandas( end="2022-01-02", time_column="ds", time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), - columns_to_types={"a": exp.DataType.build("INT"), "ds": exp.DataType.build("STRING")}, + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "ds": exp.DataType.build("STRING"), + }, ) assert to_sql_calls(adapter) == [ @@ -316,6 +405,53 @@ def test_insert_overwrite_by_time_partition_replace_where_pandas( ] +def test_insert_overwrite_by_time_partition_replace_where_pandas_source_columns( + make_mocked_engine_adapter: t.Callable, +): + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.REPLACE_WHERE + df = pd.DataFrame({"a": [1, 2]}) + adapter.insert_overwrite_by_time_partition( + "test_table", + df, + start="2022-01-01", + end="2022-01-02", + time_column="ds", + time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "ds": exp.DataType.build("STRING"), + }, + source_columns=["a"], + ) + assert to_sql_calls(adapter) == [ + """INSERT INTO "test_table" REPLACE WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02' SELECT "a", "ds" FROM (SELECT CAST("a" AS INT) AS "a", CAST(NULL AS TEXT) AS "ds" FROM (VALUES (1), (2)) AS "t"("a")) AS "_subquery" WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02'""" + ] + + +def test_insert_overwrite_by_time_partition_replace_where_query_source_columns( + make_mocked_engine_adapter: t.Callable, +): + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.REPLACE_WHERE + adapter.insert_overwrite_by_time_partition( + "test_table", + parse_one("SELECT a FROM tbl"), + start="2022-01-01", + end="2022-01-02", + time_column="ds", + time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "ds": exp.DataType.build("STRING"), + }, + source_columns=["a"], + ) + assert to_sql_calls(adapter) == [ + """INSERT INTO "test_table" REPLACE WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02' SELECT "a", "ds" FROM (SELECT "a", CAST(NULL AS TEXT) AS "ds" FROM (SELECT "a" FROM "tbl") AS "select_source_columns") AS "_subquery" WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02'""" + ] + + def test_insert_overwrite_no_where(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) @@ -328,7 +464,7 @@ def test_insert_overwrite_no_where(make_mocked_engine_adapter: t.Callable): adapter._insert_overwrite_by_condition( "test_table", source_queries, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, ) adapter.cursor.begin.assert_called_once() @@ -355,7 +491,7 @@ def test_insert_overwrite_by_condition_column_contains_unsafe_characters( adapter._insert_overwrite_by_condition( "test_table", source_queries, - columns_to_types=None, + target_columns_to_types=None, ) # The goal here is to assert that we don't parse `foo.bar.baz` into a qualified column @@ -370,7 +506,7 @@ def test_insert_append_query(make_mocked_engine_adapter: t.Callable): adapter.insert_append( "test_table", parse_one("SELECT a FROM tbl"), - columns_to_types={"a": exp.DataType.build("INT")}, + target_columns_to_types={"a": exp.DataType.build("INT")}, ) assert to_sql_calls(adapter) == [ @@ -384,7 +520,7 @@ def test_insert_append_query_select_star(make_mocked_engine_adapter: t.Callable) adapter.insert_append( "test_table", parse_one("SELECT 1 AS a, * FROM tbl"), - columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("INT")}, + target_columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("INT")}, ) assert to_sql_calls(adapter) == [ @@ -399,7 +535,7 @@ def test_insert_append_pandas(make_mocked_engine_adapter: t.Callable): adapter.insert_append( "test_table", df, - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("INT"), "b": exp.DataType.build("INT"), }, @@ -418,7 +554,7 @@ def test_insert_append_pandas_batches(make_mocked_engine_adapter: t.Callable): adapter.insert_append( "test_table", df, - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("INT"), "b": exp.DataType.build("INT"), }, @@ -434,6 +570,39 @@ def test_insert_append_pandas_batches(make_mocked_engine_adapter: t.Callable): ] +def test_insert_append_pandas_source_columns(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + df = pd.DataFrame({"a": [1, 2, 3]}) + adapter.insert_append( + "test_table", + df, + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("INT"), + }, + source_columns=["a"], + ) + assert to_sql_calls(adapter) == [ + 'INSERT INTO "test_table" ("a", "b") SELECT CAST("a" AS INT) AS "a", CAST(NULL AS INT) AS "b" FROM (VALUES (1), (2), (3)) AS "t"("a")', + ] + + +def test_insert_append_query_source_columns(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.insert_append( + "test_table", + parse_one("SELECT a FROM tbl"), + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("INT"), + }, + source_columns=["a"], + ) + assert to_sql_calls(adapter) == [ + 'INSERT INTO "test_table" ("a", "b") SELECT "a", CAST(NULL AS INT) AS "b" FROM (SELECT "a" FROM "tbl") AS "select_source_columns"', + ] + + def test_create_table(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) @@ -899,9 +1068,11 @@ def test_alter_table( original_from_structs = adapter.SCHEMA_DIFFER._from_structs def _from_structs( - current_struct: exp.DataType, new_struct: exp.DataType + current_struct: exp.DataType, new_struct: exp.DataType, *, ignore_destructive: bool = False ) -> t.List[TableAlterOperation]: - operations = original_from_structs(current_struct, new_struct) + operations = original_from_structs( + current_struct, new_struct, ignore_destructive=ignore_destructive + ) if not operations: return operations assert ( @@ -935,7 +1106,7 @@ def test_merge_upsert(make_mocked_engine_adapter: t.Callable, assert_exp_eq): adapter.merge( target_table="target", source_table=t.cast(exp.Select, parse_one('SELECT "ID", ts, val FROM source')), - columns_to_types={ + target_columns_to_types={ "ID": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), @@ -966,7 +1137,7 @@ def test_merge_upsert(make_mocked_engine_adapter: t.Callable, assert_exp_eq): adapter.merge( target_table="target", source_table=parse_one("SELECT id, ts, val FROM source"), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), @@ -987,7 +1158,7 @@ def test_merge_upsert_pandas(make_mocked_engine_adapter: t.Callable): adapter.merge( target_table="target", source_table=df, - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), @@ -1004,7 +1175,7 @@ def test_merge_upsert_pandas(make_mocked_engine_adapter: t.Callable): adapter.merge( target_table="target", source_table=df, - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), @@ -1018,13 +1189,54 @@ def test_merge_upsert_pandas(make_mocked_engine_adapter: t.Callable): ) +def test_merge_upsert_pandas_source_columns(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + df = pd.DataFrame({"id": [1, 2, 3], "ts": [4, 5, 6]}) + adapter.merge( + target_table="target", + source_table=df, + target_columns_to_types={ + "id": exp.DataType.build("int"), + "ts": exp.DataType.build("timestamp"), + "val": exp.DataType.build("int"), + }, + unique_key=[exp.to_identifier("id")], + source_columns=["id", "ts"], + ) + adapter.cursor.execute.assert_called_once_with( + 'MERGE INTO "target" AS "__MERGE_TARGET__" USING (SELECT CAST("id" AS INT) AS "id", CAST("ts" AS TIMESTAMP) AS "ts", CAST(NULL AS INT) AS "val" FROM (VALUES (1, 4), (2, 5), (3, 6)) AS "t"("id", "ts")) AS "__MERGE_SOURCE__" ON "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id" ' + 'WHEN MATCHED THEN UPDATE SET "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id", "__MERGE_TARGET__"."ts" = "__MERGE_SOURCE__"."ts", "__MERGE_TARGET__"."val" = "__MERGE_SOURCE__"."val" ' + 'WHEN NOT MATCHED THEN INSERT ("id", "ts", "val") VALUES ("__MERGE_SOURCE__"."id", "__MERGE_SOURCE__"."ts", "__MERGE_SOURCE__"."val")' + ) + + +def test_merge_upsert_query_source_columns(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.merge( + target_table="target", + source_table=parse_one("SELECT id, ts FROM source"), + target_columns_to_types={ + "id": exp.DataType.build("int"), + "ts": exp.DataType.build("timestamp"), + "val": exp.DataType.build("int"), + }, + unique_key=[exp.to_identifier("id")], + source_columns=["id", "ts"], + ) + adapter.cursor.execute.assert_called_once_with( + 'MERGE INTO "target" AS "__MERGE_TARGET__" USING (SELECT "id", "ts", CAST(NULL AS INT) AS "val" FROM (SELECT "id", "ts" FROM "source") AS "select_source_columns") AS "__MERGE_SOURCE__" ON "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id" ' + 'WHEN MATCHED THEN UPDATE SET "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id", "__MERGE_TARGET__"."ts" = "__MERGE_SOURCE__"."ts", "__MERGE_TARGET__"."val" = "__MERGE_SOURCE__"."val" ' + 'WHEN NOT MATCHED THEN INSERT ("id", "ts", "val") VALUES ("__MERGE_SOURCE__"."id", "__MERGE_SOURCE__"."ts", "__MERGE_SOURCE__"."val")' + ) + + def test_merge_when_matched(make_mocked_engine_adapter: t.Callable, assert_exp_eq): adapter = make_mocked_engine_adapter(EngineAdapter) adapter.merge( target_table="target", source_table=t.cast(exp.Select, parse_one('SELECT "ID", ts, val FROM source')), - columns_to_types={ + target_columns_to_types={ "ID": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), @@ -1077,7 +1289,7 @@ def test_merge_when_matched_multiple(make_mocked_engine_adapter: t.Callable, ass adapter.merge( target_table="target", source_table=t.cast(exp.Select, parse_one('SELECT "ID", ts, val FROM source')), - columns_to_types={ + target_columns_to_types={ "ID": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), @@ -1148,7 +1360,7 @@ def test_merge_filter(make_mocked_engine_adapter: t.Callable, assert_exp_eq): adapter.merge( target_table="target", source_table=t.cast(exp.Select, parse_one('SELECT "ID", ts, val FROM source')), - columns_to_types={ + target_columns_to_types={ "ID": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), @@ -1230,7 +1442,7 @@ def test_scd_type_2_by_time(make_mocked_engine_adapter: t.Callable): valid_from_col=exp.column("test_valid_from", quoted=True), valid_to_col=exp.column("test_valid_to", quoted=True), updated_at_col=exp.column("test_UPDATED_at", quoted=True), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("VARCHAR"), "price": exp.DataType.build("DOUBLE"), @@ -1416,6 +1628,219 @@ def test_scd_type_2_by_time(make_mocked_engine_adapter: t.Callable): ) +def test_scd_type_2_by_time_source_columns(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + df = pd.DataFrame( + { + "id": [1, 2, 3], + "name": ["a", "b", "c"], + "test_UPDATED_at": [ + "2020-01-01 10:00:00", + "2020-01-02 15:00:00", + "2020-01-03 12:00:00", + ], + } + ) + adapter.scd_type_2_by_time( + target_table="target", + source_table=df, + unique_key=[exp.column("id")], + valid_from_col=exp.column("test_valid_from", quoted=True), + valid_to_col=exp.column("test_valid_to", quoted=True), + updated_at_col=exp.column("test_UPDATED_at", quoted=True), + target_columns_to_types={ + "id": exp.DataType.build("INT"), + "name": exp.DataType.build("VARCHAR"), + "price": exp.DataType.build("DOUBLE"), + "test_UPDATED_at": exp.DataType.build("TIMESTAMP"), + "test_valid_from": exp.DataType.build("TIMESTAMP"), + "test_valid_to": exp.DataType.build("TIMESTAMP"), + }, + source_columns=["id", "name", "test_UPDATED_at"], + execution_time=datetime(2020, 1, 1, 0, 0, 0), + start=datetime(2020, 1, 1, 0, 0, 0), + is_restatement=True, + ) + sql_calls = to_sql_calls(adapter) + assert ( + parse_one(sql_calls[1]).sql() + == parse_one(""" +CREATE OR REPLACE TABLE "target" AS +WITH "source" AS ( + SELECT DISTINCT ON ("id") + TRUE AS "_exists", + "id", + "name", + "price", + CAST("test_UPDATED_at" AS TIMESTAMP) AS "test_UPDATED_at" + FROM ( + SELECT + CAST("id" AS INT) AS "id", + CAST("name" AS VARCHAR) AS "name", + CAST(NULL AS DOUBLE) AS "price", + CAST("test_UPDATED_at" AS TIMESTAMP) AS "test_UPDATED_at" + FROM (VALUES + (1, 'a', '2020-01-01 10:00:00'), + (2, 'b', '2020-01-02 15:00:00'), + (3, 'c', '2020-01-03 12:00:00')) AS "t"("id", "name", "test_UPDATED_at") + ) AS "raw_source" +), "static" AS ( + SELECT + "id", + "name", + "price", + "test_UPDATED_at", + "test_valid_from", + "test_valid_to", + TRUE AS "_exists" + FROM "target" + WHERE + NOT "test_valid_to" IS NULL +), "latest" AS ( + SELECT + "id", + "name", + "price", + "test_UPDATED_at", + "test_valid_from", + "test_valid_to", + TRUE AS "_exists" + FROM "target" + WHERE + "test_valid_to" IS NULL +), "deleted" AS ( + SELECT + "static"."id", + "static"."name", + "static"."price", + "static"."test_UPDATED_at", + "static"."test_valid_from", + "static"."test_valid_to" + FROM "static" + LEFT JOIN "latest" + ON "static"."id" = "latest"."id" + WHERE + "latest"."test_valid_to" IS NULL +), "latest_deleted" AS ( + SELECT + TRUE AS "_exists", + "id" AS "_key0", + MAX("test_valid_to") AS "test_valid_to" + FROM "deleted" + GROUP BY + "id" +), "joined" AS ( + SELECT + "source"."_exists" AS "_exists", + "latest"."id" AS "t_id", + "latest"."name" AS "t_name", + "latest"."price" AS "t_price", + "latest"."test_UPDATED_at" AS "t_test_UPDATED_at", + "latest"."test_valid_from" AS "t_test_valid_from", + "latest"."test_valid_to" AS "t_test_valid_to", + "source"."id" AS "id", + "source"."name" AS "name", + "source"."price" AS "price", + "source"."test_UPDATED_at" AS "test_UPDATED_at" + FROM "latest" + LEFT JOIN "source" + ON "latest"."id" = "source"."id" + UNION ALL + SELECT + "source"."_exists" AS "_exists", + "latest"."id" AS "t_id", + "latest"."name" AS "t_name", + "latest"."price" AS "t_price", + "latest"."test_UPDATED_at" AS "t_test_UPDATED_at", + "latest"."test_valid_from" AS "t_test_valid_from", + "latest"."test_valid_to" AS "t_test_valid_to", + "source"."id" AS "id", + "source"."name" AS "name", + "source"."price" AS "price", + "source"."test_UPDATED_at" AS "test_UPDATED_at" + FROM "latest" + RIGHT JOIN "source" + ON "latest"."id" = "source"."id" + WHERE + "latest"."_exists" IS NULL +), "updated_rows" AS ( + SELECT + COALESCE("joined"."t_id", "joined"."id") AS "id", + COALESCE("joined"."t_name", "joined"."name") AS "name", + COALESCE("joined"."t_price", "joined"."price") AS "price", + COALESCE("joined"."t_test_UPDATED_at", "joined"."test_UPDATED_at") AS "test_UPDATED_at", + CASE + WHEN "t_test_valid_from" IS NULL AND NOT "latest_deleted"."_exists" IS NULL + THEN CASE + WHEN "latest_deleted"."test_valid_to" > "test_UPDATED_at" + THEN "latest_deleted"."test_valid_to" + ELSE "test_UPDATED_at" + END + WHEN "t_test_valid_from" IS NULL + THEN CAST('1970-01-01 00:00:00' AS TIMESTAMP) + ELSE "t_test_valid_from" + END AS "test_valid_from", + CASE + WHEN "joined"."test_UPDATED_at" > "joined"."t_test_UPDATED_at" + THEN "joined"."test_UPDATED_at" + WHEN "joined"."_exists" IS NULL + THEN CAST('2020-01-01 00:00:00' AS TIMESTAMP) + ELSE "t_test_valid_to" + END AS "test_valid_to" + FROM "joined" + LEFT JOIN "latest_deleted" + ON "joined"."id" = "latest_deleted"."_key0" +), "inserted_rows" AS ( + SELECT + "id", + "name", + "price", + "test_UPDATED_at", + "test_UPDATED_at" AS "test_valid_from", + CAST(NULL AS TIMESTAMP) AS "test_valid_to" + FROM "joined" + WHERE + "joined"."test_UPDATED_at" > "joined"."t_test_UPDATED_at" +) +SELECT + CAST("id" AS INT) AS "id", + CAST("name" AS VARCHAR) AS "name", + CAST("price" AS DOUBLE) AS "price", + CAST("test_UPDATED_at" AS TIMESTAMP) AS "test_UPDATED_at", + CAST("test_valid_from" AS TIMESTAMP) AS "test_valid_from", + CAST("test_valid_to" AS TIMESTAMP) AS "test_valid_to" +FROM ( + SELECT + "id", + "name", + "price", + "test_UPDATED_at", + "test_valid_from", + "test_valid_to" + FROM "static" + UNION ALL + SELECT + "id", + "name", + "price", + "test_UPDATED_at", + "test_valid_from", + "test_valid_to" + FROM "updated_rows" + UNION ALL + SELECT + "id", + "name", + "price", + "test_UPDATED_at", + "test_valid_from", + "test_valid_to" + FROM "inserted_rows" +) AS "_subquery" + """).sql() + ) + + def test_scd_type_2_by_time_no_invalidate_hard_deletes(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) @@ -1429,7 +1854,7 @@ def test_scd_type_2_by_time_no_invalidate_hard_deletes(make_mocked_engine_adapte valid_to_col=exp.column("test_valid_to", quoted=True), updated_at_col=exp.column("test_updated_at", quoted=True), invalidate_hard_deletes=False, - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("VARCHAR"), "price": exp.DataType.build("DOUBLE"), @@ -1616,7 +2041,7 @@ def test_merge_scd_type_2_pandas(make_mocked_engine_adapter: t.Callable): valid_from_col=exp.column("test_valid_from", quoted=True), valid_to_col=exp.column("test_valid_to", quoted=True), updated_at_col=exp.column("test_updated_at", quoted=True), - columns_to_types={ + target_columns_to_types={ "id1": exp.DataType.build("INT"), "id2": exp.DataType.build("INT"), "name": exp.DataType.build("VARCHAR"), @@ -1799,7 +2224,7 @@ def test_scd_type_2_by_column(make_mocked_engine_adapter: t.Callable): valid_from_col=exp.column("test_VALID_from", quoted=True), valid_to_col=exp.column("test_valid_to", quoted=True), check_columns=[exp.column("name"), exp.column("price")], - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("VARCHAR"), "price": exp.DataType.build("DOUBLE"), @@ -1980,7 +2405,7 @@ def test_scd_type_2_by_column_composite_key(make_mocked_engine_adapter: t.Callab valid_from_col=exp.column("test_VALID_from", quoted=True), valid_to_col=exp.column("test_valid_to", quoted=True), check_columns=[exp.column("name"), exp.column("price")], - columns_to_types={ + target_columns_to_types={ "id_a": exp.DataType.build("VARCHAR"), "id_b": exp.DataType.build("VARCHAR"), "name": exp.DataType.build("VARCHAR"), @@ -2172,7 +2597,7 @@ def test_scd_type_2_truncate(make_mocked_engine_adapter: t.Callable): valid_from_col=exp.column("test_valid_from", quoted=True), valid_to_col=exp.column("test_valid_to", quoted=True), check_columns=[exp.column("name"), exp.column("price")], - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("VARCHAR"), "price": exp.DataType.build("DOUBLE"), @@ -2355,7 +2780,7 @@ def test_scd_type_2_by_column_star_check(make_mocked_engine_adapter: t.Callable) valid_from_col=exp.column("test_valid_from", quoted=True), valid_to_col=exp.column("test_valid_to", quoted=True), check_columns=exp.Star(), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("VARCHAR"), "price": exp.DataType.build("DOUBLE"), @@ -2550,7 +2975,7 @@ def test_scd_type_2_by_column_no_invalidate_hard_deletes(make_mocked_engine_adap valid_to_col=exp.column("test_valid_to", quoted=True), invalidate_hard_deletes=False, check_columns=[exp.column("name"), exp.column("price")], - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("VARCHAR"), "price": exp.DataType.build("DOUBLE"), @@ -2774,6 +3199,39 @@ def test_replace_query_pandas(make_mocked_engine_adapter: t.Callable): ] +def test_replace_query_pandas_source_columns(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + df = pd.DataFrame({"a": [1, 2, 3]}) + adapter.replace_query( + "test_table", + df, + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("INT"), + }, + source_columns=["a"], + ) + assert to_sql_calls(adapter) == [ + 'CREATE OR REPLACE TABLE "test_table" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT CAST("a" AS INT) AS "a", CAST(NULL AS INT) AS "b" FROM (VALUES (1), (2), (3)) AS "t"("a")) AS "_subquery"', + ] + + +def test_replace_query_query_source_columns(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.replace_query( + "test_table", + parse_one("SELECT a FROM tbl"), + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("INT"), + }, + source_columns=["a"], + ) + assert to_sql_calls(adapter) == [ + 'CREATE OR REPLACE TABLE "test_table" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT "a", CAST(NULL AS INT) AS "b" FROM (SELECT "a" FROM "tbl") AS "select_source_columns") AS "_subquery"', + ] + + def test_replace_query_self_referencing_not_exists_unknown( make_mocked_engine_adapter: t.Callable, mocker: MockerFixture ): @@ -2791,7 +3249,7 @@ def test_replace_query_self_referencing_not_exists_unknown( adapter.replace_query( "test", parse_one("SELECT a FROM test"), - columns_to_types={"a": exp.DataType.build("UNKNOWN")}, + target_columns_to_types={"a": exp.DataType.build("UNKNOWN")}, ) @@ -2808,7 +3266,7 @@ def test_replace_query_self_referencing_exists( adapter.replace_query( "test", parse_one("SELECT a FROM test"), - columns_to_types={"a": exp.DataType.build("UNKNOWN")}, + target_columns_to_types={"a": exp.DataType.build("UNKNOWN")}, ) assert to_sql_calls(adapter) == [ @@ -2829,7 +3287,7 @@ def test_replace_query_self_referencing_not_exists_known( adapter.replace_query( "test", parse_one("SELECT a FROM test"), - columns_to_types={"a": exp.DataType.build("INT")}, + target_columns_to_types={"a": exp.DataType.build("INT")}, ) assert to_sql_calls(adapter) == [ @@ -2922,6 +3380,39 @@ def test_ctas_pandas(make_mocked_engine_adapter: t.Callable): ] +def test_ctas_pandas_source_columns(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + df = pd.DataFrame({"a": [1, 2, 3]}) + adapter.ctas( + "test_table", + df, + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("INT"), + }, + source_columns=["a"], + ) + assert to_sql_calls(adapter) == [ + 'CREATE TABLE IF NOT EXISTS "test_table" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT CAST("a" AS INT) AS "a", CAST(NULL AS INT) AS "b" FROM (VALUES (1), (2), (3)) AS "t"("a")) AS "_subquery"', + ] + + +def test_ctas_query_source_columns(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.ctas( + "test_table", + parse_one("SELECT a FROM tbl"), + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("INT"), + }, + source_columns=["a"], + ) + assert to_sql_calls(adapter) == [ + 'CREATE TABLE IF NOT EXISTS "test_table" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT "a", CAST(NULL AS INT) AS "b" FROM (SELECT "a" FROM "tbl") AS "select_source_columns") AS "_subquery"', + ] + + def test_drop_view(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) @@ -3061,7 +3552,7 @@ def test_insert_overwrite_by_partition_query( table_name, parse_one("SELECT a, ds, b FROM tbl"), partitioned_by=[d.parse_one(k) for k in partitioned_by], - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("int"), "ds": exp.DataType.build("DATETIME"), "b": exp.DataType.build("boolean"), @@ -3101,7 +3592,7 @@ def test_insert_overwrite_by_partition_query_insert_overwrite_strategy( d.parse_one("DATETIME_TRUNC(ds, MONTH)"), d.parse_one("b"), ], - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("int"), "ds": exp.DataType.build("DATETIME"), "b": exp.DataType.build("boolean"), @@ -3142,3 +3633,71 @@ def test_log_sql(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): mock_logger.log.call_args_list[4][0][2] == 'CREATE OR REPLACE TABLE "test" AS SELECT CAST("id" AS BIGINT) AS "id", CAST("value" AS TEXT) AS "value" FROM (SELECT CAST("id" AS BIGINT) AS "id", CAST("value" AS TEXT) AS "value" FROM (VALUES "") AS "t"("id", "value")) AS "_subquery"' ) + + +@pytest.mark.parametrize( + "columns, source_columns, expected", + [ + (["a", "b"], None, 'SELECT "a", "b"'), + (["a", "b"], ["a"], 'SELECT "a", NULL AS "b"'), + (["a", "b"], ["a", "b"], 'SELECT "a", "b"'), + (["a", "b"], ["c", "d"], 'SELECT NULL AS "a", NULL AS "b"'), + (["a", "b"], [], 'SELECT "a", "b"'), + ], +) +def test_select_columns( + columns: t.List[str], source_columns: t.Optional[t.List[str]], expected: str +) -> None: + assert ( + EngineAdapter._select_columns( + columns, + source_columns, + ).sql() + == expected + ) + + +@pytest.mark.parametrize( + "columns_to_types, source_columns, expected", + [ + ( + { + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("TEXT"), + }, + None, + [ + 'CAST("a" AS INT) AS "a"', + 'CAST("b" AS TEXT) AS "b"', + ], + ), + ( + { + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("TEXT"), + }, + ["a"], + [ + 'CAST("a" AS INT) AS "a"', + 'CAST(NULL AS TEXT) AS "b"', + ], + ), + ( + { + "a": exp.DataType.build("INT"), + "b": exp.DataType.build("TEXT"), + }, + ["b", "c"], + [ + 'CAST(NULL AS INT) AS "a"', + 'CAST("b" AS TEXT) AS "b"', + ], + ), + ], +) +def test_casted_columns( + columns_to_types: t.Dict[str, exp.DataType], source_columns: t.List[str], expected: t.List[str] +) -> None: + assert [ + x.sql() for x in EngineAdapter._casted_columns(columns_to_types, source_columns) + ] == expected diff --git a/tests/core/engine_adapter/test_bigquery.py b/tests/core/engine_adapter/test_bigquery.py index 32377ac1de..f5a287defb 100644 --- a/tests/core/engine_adapter/test_bigquery.py +++ b/tests/core/engine_adapter/test_bigquery.py @@ -38,7 +38,7 @@ def test_insert_overwrite_by_time_partition_query( end="2022-01-05", time_formatter=lambda x, _: exp.Literal.string(x.strftime("%Y-%m-%d")), time_column="ds", - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("int"), "ds": exp.DataType.build("string"), }, @@ -68,7 +68,7 @@ def test_insert_overwrite_by_partition_query( partitioned_by=[ d.parse_one("DATETIME_TRUNC(ds, MONTH)"), ], - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("int"), "ds": exp.DataType.build("DATETIME"), }, @@ -111,7 +111,7 @@ def test_insert_overwrite_by_partition_query_unknown_column_types( partitioned_by=[ d.parse_one("DATETIME_TRUNC(ds, MONTH)"), ], - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("unknown"), "ds": exp.DataType.build("UNKNOWN"), }, @@ -176,7 +176,7 @@ def temp_table_exists(table: exp.Table) -> bool: end="2022-01-05", time_formatter=lambda x, _: exp.Literal.string(x.strftime("%Y-%m-%d")), time_column="ds", - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("int"), "ds": exp.DataType.build("string"), }, @@ -205,7 +205,7 @@ def temp_table_exists(table: exp.Table) -> bool: assert load_temp_table.kwargs["job_config"].write_disposition is None assert ( merge_sql.sql(dialect="bigquery") - == "MERGE INTO test_table AS __MERGE_TARGET__ USING (SELECT `a`, `ds` FROM (SELECT `a`, `ds` FROM project.dataset.temp_table) AS _subquery WHERE ds BETWEEN '2022-01-01' AND '2022-01-05') AS __MERGE_SOURCE__ ON FALSE WHEN NOT MATCHED BY SOURCE AND ds BETWEEN '2022-01-01' AND '2022-01-05' THEN DELETE WHEN NOT MATCHED THEN INSERT (a, ds) VALUES (a, ds)" + == "MERGE INTO test_table AS __MERGE_TARGET__ USING (SELECT `a`, `ds` FROM (SELECT CAST(`a` AS INT64) AS `a`, CAST(`ds` AS STRING) AS `ds` FROM project.dataset.temp_table) AS _subquery WHERE ds BETWEEN '2022-01-01' AND '2022-01-05') AS __MERGE_SOURCE__ ON FALSE WHEN NOT MATCHED BY SOURCE AND ds BETWEEN '2022-01-01' AND '2022-01-05' THEN DELETE WHEN NOT MATCHED THEN INSERT (a, ds) VALUES (a, ds)" ) assert ( drop_temp_table_sql.sql(dialect="bigquery") @@ -295,7 +295,7 @@ def temp_table_exists(table: exp.Table) -> bool: ] sql_calls = _to_sql_calls(execute_mock) assert sql_calls == [ - "CREATE OR REPLACE TABLE `test_table` AS SELECT CAST(`a` AS INT64) AS `a`, CAST(`b` AS INT64) AS `b` FROM (SELECT `a`, `b` FROM `project`.`dataset`.`temp_table`) AS `_subquery`", + "CREATE OR REPLACE TABLE `test_table` AS SELECT CAST(`a` AS INT64) AS `a`, CAST(`b` AS INT64) AS `b` FROM (SELECT CAST(`a` AS INT64) AS `a`, CAST(`b` AS INT64) AS `b` FROM `project`.`dataset`.`temp_table`) AS `_subquery`", "DROP TABLE IF EXISTS `project`.`dataset`.`temp_table`", ] @@ -431,7 +431,7 @@ def test_merge(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): adapter.merge( target_table="target", source_table=parse_one("SELECT id, ts, val FROM source"), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.Type.INT, "ts": exp.DataType.Type.TIMESTAMP, "val": exp.DataType.Type.INT, @@ -488,7 +488,7 @@ def temp_table_exists(table: exp.Table) -> bool: adapter.merge( target_table="target", source_table=df, - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "ts": exp.DataType.build("TIMESTAMP"), "val": exp.DataType.build("INT"), @@ -498,7 +498,7 @@ def temp_table_exists(table: exp.Table) -> bool: sql_calls = _to_sql_calls(execute_mock, identify=False) assert sql_calls == [ - "MERGE INTO target AS __MERGE_TARGET__ USING (SELECT `id`, `ts`, `val` FROM project.dataset.temp_table) AS __MERGE_SOURCE__ ON __MERGE_TARGET__.id = __MERGE_SOURCE__.id " + "MERGE INTO target AS __MERGE_TARGET__ USING (SELECT CAST(`id` AS INT64) AS `id`, CAST(`ts` AS DATETIME) AS `ts`, CAST(`val` AS INT64) AS `val` FROM project.dataset.temp_table) AS __MERGE_SOURCE__ ON __MERGE_TARGET__.id = __MERGE_SOURCE__.id " "WHEN MATCHED THEN UPDATE SET __MERGE_TARGET__.id = __MERGE_SOURCE__.id, __MERGE_TARGET__.ts = __MERGE_SOURCE__.ts, __MERGE_TARGET__.val = __MERGE_SOURCE__.val " "WHEN NOT MATCHED THEN INSERT (id, ts, val) VALUES (__MERGE_SOURCE__.id, __MERGE_SOURCE__.ts, __MERGE_SOURCE__.val)", "DROP TABLE IF EXISTS project.dataset.temp_table", @@ -733,14 +733,14 @@ def test_nested_comments(make_mocked_engine_adapter: t.Callable, mocker: MockerF adapter.create_table( "test_table", - columns_to_types=nested_columns_to_types, + target_columns_to_types=nested_columns_to_types, column_descriptions=long_column_descriptions, ) adapter.ctas( "test_table", parse_one("SELECT * FROM source_table"), - columns_to_types=nested_columns_to_types, + target_columns_to_types=nested_columns_to_types, column_descriptions=long_column_descriptions, ) diff --git a/tests/core/engine_adapter/test_clickhouse.py b/tests/core/engine_adapter/test_clickhouse.py index 973e178820..3e92a8fe9b 100644 --- a/tests/core/engine_adapter/test_clickhouse.py +++ b/tests/core/engine_adapter/test_clickhouse.py @@ -603,7 +603,7 @@ def test_scd_type_2_by_time( valid_from_col=exp.column("test_valid_from", quoted=True), valid_to_col=exp.column("test_valid_to", quoted=True), updated_at_col=exp.column("test_UPDATED_at", quoted=True), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("VARCHAR"), "price": exp.DataType.build("DOUBLE"), @@ -815,7 +815,7 @@ def test_scd_type_2_by_column( valid_from_col=exp.column("test_VALID_from", quoted=True), valid_to_col=exp.column("test_valid_to", quoted=True), check_columns=[exp.column("name"), exp.column("price")], - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("VARCHAR"), "price": exp.DataType.build("DOUBLE"), diff --git a/tests/core/engine_adapter/test_databricks.py b/tests/core/engine_adapter/test_databricks.py index 5991f5b2b9..cd4c8c4074 100644 --- a/tests/core/engine_adapter/test_databricks.py +++ b/tests/core/engine_adapter/test_databricks.py @@ -159,7 +159,7 @@ def test_insert_overwrite_by_partition_query( d.parse_one("DATETIME_TRUNC(ds, MONTH)"), d.parse_one("b"), ], - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("int"), "ds": exp.DataType.build("DATETIME"), "b": exp.DataType.build("boolean"), diff --git a/tests/core/engine_adapter/test_mixins.py b/tests/core/engine_adapter/test_mixins.py index 57803427d4..50bef59d6e 100644 --- a/tests/core/engine_adapter/test_mixins.py +++ b/tests/core/engine_adapter/test_mixins.py @@ -23,7 +23,7 @@ def test_logical_merge(make_mocked_engine_adapter: t.Callable, mocker: MockerFix adapter.merge( target_table="target", source_table=t.cast(exp.Select, parse_one("SELECT id, ts, val FROM source")), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType(this=exp.DataType.Type.INT), "ts": exp.DataType(this=exp.DataType.Type.TIMESTAMP), "val": exp.DataType(this=exp.DataType.Type.INT), @@ -48,7 +48,7 @@ def test_logical_merge(make_mocked_engine_adapter: t.Callable, mocker: MockerFix adapter.merge( target_table="target", source_table=t.cast(exp.Select, parse_one("SELECT id, ts, val FROM source")), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType(this=exp.DataType.Type.INT), "ts": exp.DataType(this=exp.DataType.Type.TIMESTAMP), "val": exp.DataType(this=exp.DataType.Type.INT), diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index d8e5214be5..caa7843726 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -290,7 +290,10 @@ def test_insert_overwrite_by_time_partition_supports_insert_overwrite_pandas_not end="2022-01-02", time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), time_column="ds", - columns_to_types={"a": exp.DataType.build("INT"), "ds": exp.DataType.build("STRING")}, + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "ds": exp.DataType.build("STRING"), + }, ) adapter._connection_pool.get().bulk_copy.assert_called_with( f"__temp_test_table_{temp_table_id}", [(1, "2022-01-01"), (2, "2022-01-02")] @@ -327,7 +330,10 @@ def test_insert_overwrite_by_time_partition_supports_insert_overwrite_pandas_exi end="2022-01-02", time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), time_column="ds", - columns_to_types={"a": exp.DataType.build("INT"), "ds": exp.DataType.build("STRING")}, + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "ds": exp.DataType.build("STRING"), + }, ) assert to_sql_calls(adapter) == [ f"""MERGE INTO [test_table] AS [__MERGE_TARGET__] USING (SELECT [a] AS [a], [ds] AS [ds] FROM (SELECT CAST([a] AS INTEGER) AS [a], CAST([ds] AS VARCHAR(MAX)) AS [ds] FROM [__temp_test_table_{temp_table_id}]) AS [_subquery] WHERE [ds] BETWEEN '2022-01-01' AND '2022-01-02') AS [__MERGE_SOURCE__] ON (1 = 0) WHEN NOT MATCHED BY SOURCE AND [ds] BETWEEN '2022-01-01' AND '2022-01-02' THEN DELETE WHEN NOT MATCHED THEN INSERT ([a], [ds]) VALUES ([a], [ds]);""", @@ -359,7 +365,10 @@ def test_insert_overwrite_by_time_partition_replace_where_pandas( end="2022-01-02", time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), time_column="ds", - columns_to_types={"a": exp.DataType.build("INT"), "ds": exp.DataType.build("STRING")}, + target_columns_to_types={ + "a": exp.DataType.build("INT"), + "ds": exp.DataType.build("STRING"), + }, ) adapter._connection_pool.get().bulk_copy.assert_called_with( f"__temp_test_table_{temp_table_id}", [(1, "2022-01-01"), (2, "2022-01-02")] @@ -391,7 +400,7 @@ def test_insert_append_pandas( adapter.insert_append( table_name, df, - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("INT"), "b": exp.DataType.build("INT"), }, @@ -461,7 +470,7 @@ def test_merge_pandas( adapter.merge( target_table=table_name, source_table=df, - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("int"), "ts": exp.DataType.build("TIMESTAMP"), "val": exp.DataType.build("int"), @@ -485,7 +494,7 @@ def test_merge_pandas( adapter.merge( target_table=table_name, source_table=df, - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("int"), "ts": exp.DataType.build("TIMESTAMP"), "val": exp.DataType.build("int"), @@ -524,7 +533,7 @@ def test_merge_exists( adapter.merge( target_table=table_name, source_table=df, - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("int"), "ts": exp.DataType.build("TIMESTAMP"), "val": exp.DataType.build("int"), @@ -545,7 +554,7 @@ def test_merge_exists( adapter.merge( target_table=table_name, source_table=df, - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("int"), "ts": exp.DataType.build("TIMESTAMP"), "val": exp.DataType.build("int"), @@ -567,7 +576,7 @@ def test_merge_exists( adapter.merge( target_table=table_name, source_table=df, - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("int"), "ts": exp.DataType.build("TIMESTAMP"), }, @@ -913,7 +922,7 @@ def test_replace_query_strategy(adapter: MSSQLEngineAdapter, mocker: MockerFixtu table_properties=model.physical_properties, table_description=model.description, column_descriptions=model.column_descriptions, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, ) # subsequent - table exists @@ -937,7 +946,7 @@ def test_replace_query_strategy(adapter: MSSQLEngineAdapter, mocker: MockerFixtu table_properties=model.physical_properties, table_description=model.description, column_descriptions=model.column_descriptions, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, ) assert to_sql_calls(adapter) == [ diff --git a/tests/core/engine_adapter/test_postgres.py b/tests/core/engine_adapter/test_postgres.py index fd6ce44994..5d05dd653c 100644 --- a/tests/core/engine_adapter/test_postgres.py +++ b/tests/core/engine_adapter/test_postgres.py @@ -99,7 +99,7 @@ def test_merge_version_gte_15(make_mocked_engine_adapter: t.Callable): adapter.merge( target_table="target", source_table=t.cast(exp.Select, parse_one('SELECT "ID", ts, val FROM source')), - columns_to_types={ + target_columns_to_types={ "ID": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), @@ -127,7 +127,7 @@ def test_merge_version_lt_15( adapter.merge( target_table="target", source_table=t.cast(exp.Select, parse_one('SELECT "ID", ts, val FROM source')), - columns_to_types={ + target_columns_to_types={ "ID": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), diff --git a/tests/core/engine_adapter/test_redshift.py b/tests/core/engine_adapter/test_redshift.py index 0db8e8d055..17c3dd1866 100644 --- a/tests/core/engine_adapter/test_redshift.py +++ b/tests/core/engine_adapter/test_redshift.py @@ -220,7 +220,7 @@ def test_values_to_sql(adapter: t.Callable, mocker: MockerFixture): df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) result = adapter._values_to_sql( values=list(df.itertuples(index=False, name=None)), - columns_to_types={"a": exp.DataType.build("int"), "b": exp.DataType.build("int")}, + target_columns_to_types={"a": exp.DataType.build("int"), "b": exp.DataType.build("int")}, batch_start=0, batch_end=2, ) @@ -272,7 +272,7 @@ def mock_table(*args, **kwargs): adapter.replace_query( table_name="test_table", query_or_df=df, - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("int"), "b": exp.DataType.build("int"), }, @@ -299,7 +299,7 @@ def test_replace_query_with_df_table_not_exists(adapter: t.Callable, mocker: Moc adapter.replace_query( table_name="test_table", query_or_df=df, - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("int"), "b": exp.DataType.build("int"), }, @@ -342,7 +342,7 @@ def test_create_view(adapter: t.Callable): adapter.create_view( view_name="test_view", query_or_df=parse_one("SELECT cola FROM table"), - columns_to_types={ + target_columns_to_types={ "a": exp.DataType.build("int"), "b": exp.DataType.build("int"), }, @@ -428,7 +428,7 @@ def test_merge(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): adapter.merge( target_table=exp.to_table("target_table_name"), source_table=t.cast(exp.Select, parse_one('SELECT "ID", ts, val FROM source')), - columns_to_types={ + target_columns_to_types={ "ID": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), @@ -440,7 +440,7 @@ def test_merge(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): adapter.merge( target_table=exp.to_table("target_table_name"), source_table=t.cast(exp.Select, parse_one('SELECT "ID", ts, val FROM source')), - columns_to_types={ + target_columns_to_types={ "ID": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), @@ -473,7 +473,7 @@ def test_merge_when_matched_error(make_mocked_engine_adapter: t.Callable, mocker adapter.merge( target_table=exp.to_table("target_table_name"), source_table=t.cast(exp.Select, parse_one('SELECT "ID", val FROM source')), - columns_to_types={ + target_columns_to_types={ "ID": exp.DataType.build("int"), "val": exp.DataType.build("int"), }, @@ -521,7 +521,7 @@ def test_merge_logical_filter_error(make_mocked_engine_adapter: t.Callable, mock adapter.merge( target_table=exp.to_table("target_table_name_2"), source_table=t.cast(exp.Select, parse_one('SELECT "ID", ts FROM source')), - columns_to_types={ + target_columns_to_types={ "ID": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), }, @@ -546,7 +546,7 @@ def test_merge_logical( adapter.merge( target_table=exp.to_table("target"), source_table=t.cast(exp.Select, parse_one('SELECT "ID", ts FROM source')), - columns_to_types={ + target_columns_to_types={ "ID": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), }, diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index 4ca13ee8f9..9a1e068aa6 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -254,14 +254,14 @@ def test_create_managed_table(make_mocked_engine_adapter: t.Callable, mocker: Mo adapter.create_managed_table( table_name="test_table", query=query, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, ) # warehouse not specified, should default to current_warehouse() adapter.create_managed_table( table_name="test_table", query=query, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, table_properties={"target_lag": exp.Literal.string("20 minutes")}, ) @@ -269,7 +269,7 @@ def test_create_managed_table(make_mocked_engine_adapter: t.Callable, mocker: Mo adapter.create_managed_table( table_name="test_table", query=query, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, table_properties={ "target_lag": exp.Literal.string("20 minutes"), "warehouse": exp.to_identifier("foo"), @@ -280,7 +280,7 @@ def test_create_managed_table(make_mocked_engine_adapter: t.Callable, mocker: Mo adapter.create_managed_table( table_name="test_table", query=query, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, table_properties={ "target_lag": exp.Literal.string("20 minutes"), }, @@ -292,7 +292,7 @@ def test_create_managed_table(make_mocked_engine_adapter: t.Callable, mocker: Mo adapter.create_managed_table( table_name="test_table", query=query, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, table_properties={ "target_lag": exp.Literal.string("20 minutes"), "refresh_mode": exp.Literal.string("auto"), @@ -304,7 +304,7 @@ def test_create_managed_table(make_mocked_engine_adapter: t.Callable, mocker: Mo adapter.create_managed_table( table_name="test_table", query=query, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, table_properties={ "target_lag": exp.Literal.string("20 minutes"), "catalog": exp.Literal.string("snowflake"), @@ -343,7 +343,7 @@ def test_ctas_skips_dynamic_table_properties(make_mocked_engine_adapter: t.Calla adapter.ctas( table_name="test_table", query_or_df=query, - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, table_properties={ "warehouse": exp.to_identifier("foo"), "target_lag": exp.Literal.string("20 minutes"), @@ -463,7 +463,10 @@ def test_replace_query_snowpark_dataframe( adapter.replace_query( table_name="foo", query_or_df=df, - columns_to_types={"ID": exp.DataType.build("INT"), "NAME": exp.DataType.build("VARCHAR")}, + target_columns_to_types={ + "ID": exp.DataType.build("INT"), + "NAME": exp.DataType.build("VARCHAR"), + }, ) # verify that DROP VIEW is called instead of DROP TABLE @@ -622,7 +625,7 @@ def test_creatable_type_transient_type_from_model_definition( ) adapter.create_table( model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_properties=model.physical_properties, ) @@ -657,7 +660,7 @@ def test_creatable_type_transient_type_from_model_definition_with_other_property ) adapter.create_table( model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_properties=model.physical_properties, ) @@ -733,7 +736,7 @@ def test_table_format_iceberg(snowflake_mocked_engine_adapter: SnowflakeEngineAd adapter.create_table( table_name=model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_format=model.table_format, table_properties=model.physical_properties, ) @@ -741,7 +744,7 @@ def test_table_format_iceberg(snowflake_mocked_engine_adapter: SnowflakeEngineAd adapter.ctas( table_name=model.name, query_or_df=model.render_query_or_raise(), - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_format=model.table_format, table_properties=model.physical_properties, ) diff --git a/tests/core/engine_adapter/test_spark.py b/tests/core/engine_adapter/test_spark.py index 468de9f75a..55a925b995 100644 --- a/tests/core/engine_adapter/test_spark.py +++ b/tests/core/engine_adapter/test_spark.py @@ -83,7 +83,7 @@ def test_replace_query_table_properties_not_exists( adapter.replace_query( "test_table", parse_one("SELECT 1 AS cola, '2' AS colb, '3' AS colc"), - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, partitioned_by=[exp.to_column("colb")], storage_format="ICEBERG", table_properties={"a": exp.convert(1)}, @@ -117,7 +117,7 @@ def test_replace_query_table_properties_exists( adapter.replace_query( "test_table", parse_one("SELECT 1 AS cola, '2' AS colb, '3' AS colc"), - columns_to_types=columns_to_types, + target_columns_to_types=columns_to_types, partitioned_by=[exp.to_column("colb")], storage_format="ICEBERG", table_properties={"a": exp.convert(1)}, @@ -582,7 +582,7 @@ def check_table_exists(table_name: exp.Table) -> bool: valid_from_col=exp.column("test_valid_from", quoted=True), valid_to_col=exp.column("test_valid_to", quoted=True), updated_at_col=exp.column("test_updated_at", quoted=True), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("VARCHAR"), "price": exp.DataType.build("DOUBLE"), @@ -1010,7 +1010,7 @@ def test_replace_query_with_wap_self_reference( adapter.replace_query( "catalog.schema.table.branch_wap_12345", parse_one("SELECT 1 as a FROM catalog.schema.table.branch_wap_12345"), - columns_to_types={"a": exp.DataType.build("INT")}, + target_columns_to_types={"a": exp.DataType.build("INT")}, storage_format="ICEBERG", ) @@ -1047,7 +1047,7 @@ def test_table_format(adapter: SparkEngineAdapter, mocker: MockerFixture): # both table_format and storage_format adapter.create_table( table_name=model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_format=model.table_format, storage_format=model.storage_format, ) @@ -1055,21 +1055,21 @@ def test_table_format(adapter: SparkEngineAdapter, mocker: MockerFixture): # just table_format adapter.create_table( table_name=model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_format=model.table_format, ) # just storage_format set to a table format (test for backwards compatibility) adapter.create_table( table_name=model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, storage_format=model.table_format, ) adapter.ctas( table_name=model.name, query_or_df=model.query, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_format=model.table_format, storage_format=model.storage_format, ) diff --git a/tests/core/engine_adapter/test_trino.py b/tests/core/engine_adapter/test_trino.py index 4895bc5a31..745c2bbdfb 100644 --- a/tests/core/engine_adapter/test_trino.py +++ b/tests/core/engine_adapter/test_trino.py @@ -183,7 +183,7 @@ def test_partitioned_by_iceberg_transforms( adapter.create_table( table_name=model.view_name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, partitioned_by=model.partitioned_by, ) @@ -426,7 +426,7 @@ def test_table_format(trino_mocked_engine_adapter: TrinoEngineAdapter, mocker: M adapter.create_table( table_name=model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_format=model.table_format, storage_format=model.storage_format, ) @@ -434,7 +434,7 @@ def test_table_format(trino_mocked_engine_adapter: TrinoEngineAdapter, mocker: M adapter.ctas( table_name=model.name, query_or_df=t.cast(exp.Query, model.query), - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_format=model.table_format, storage_format=model.storage_format, ) @@ -472,14 +472,14 @@ def test_table_location(trino_mocked_engine_adapter: TrinoEngineAdapter, mocker: adapter.create_table( table_name=model.name, - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_properties=model.physical_properties, ) adapter.ctas( table_name=model.name, query_or_df=t.cast(exp.Query, model.query), - columns_to_types=model.columns_to_types_or_raise, + target_columns_to_types=model.columns_to_types_or_raise, table_properties=model.physical_properties, ) diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index d61907a5aa..e7046be13d 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -2205,7 +2205,7 @@ def test_migrate_rows(state_sync: EngineAdapterStateSync, mocker: MockerFixture) state_sync.engine_adapter.replace_query( "sqlmesh._snapshots", pd.read_json("tests/fixtures/migrations/snapshots.json"), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build("text"), "identifier": exp.DataType.build("text"), "version": exp.DataType.build("text"), @@ -2216,7 +2216,7 @@ def test_migrate_rows(state_sync: EngineAdapterStateSync, mocker: MockerFixture) state_sync.engine_adapter.replace_query( "sqlmesh._environments", pd.read_json("tests/fixtures/migrations/environments.json"), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build("text"), "snapshots": exp.DataType.build("text"), "start_at": exp.DataType.build("text"), @@ -2285,7 +2285,7 @@ def test_backup_state(state_sync: EngineAdapterStateSync, mocker: MockerFixture) state_sync.engine_adapter.replace_query( "sqlmesh._snapshots", pd.read_json("tests/fixtures/migrations/snapshots.json"), - columns_to_types={ + target_columns_to_types={ "name": exp.DataType.build("text"), "identifier": exp.DataType.build("text"), "version": exp.DataType.build("text"), @@ -2310,7 +2310,7 @@ def test_restore_snapshots_table(state_sync: EngineAdapterStateSync) -> None: state_sync.engine_adapter.replace_query( "sqlmesh._snapshots", pd.read_json("tests/fixtures/migrations/snapshots.json"), - columns_to_types=snapshot_columns_to_types, + target_columns_to_types=snapshot_columns_to_types, ) old_snapshots = state_sync.engine_adapter.fetchdf("select * from sqlmesh._snapshots") diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 852f00e760..a94ba74a20 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -16,7 +16,7 @@ import sqlmesh.core.constants from sqlmesh.cli.project_init import init_example_project -from sqlmesh.core.console import get_console, TerminalConsole +from sqlmesh.core.console import TerminalConsole from sqlmesh.core import dialect as d, constants as c from sqlmesh.core.config import ( load_configs, diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 7248b2a724..72d8964a71 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -2059,12 +2059,13 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: nonlocal custom_insert_called custom_insert_called = True - self._replace_query_for_model(model, table_name, query_or_df) + self._replace_query_for_model(model, table_name, query_or_df, render_kwargs) model = context.get_model("sushi.top_waiters") kwargs = { @@ -2104,6 +2105,7 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: assert isinstance(model.kind, TestCustomKind) @@ -2111,7 +2113,7 @@ def insert( nonlocal custom_insert_calls custom_insert_calls.append(model.kind.custom_property) - self._replace_query_for_model(model, table_name, query_or_df) + self._replace_query_for_model(model, table_name, query_or_df, render_kwargs) model = context.get_model("sushi.top_waiters") kwargs = { @@ -2884,9 +2886,9 @@ def test_restatement_plan_hourly_with_downstream_daily_restates_correct_interval "ts": exp.DataType.build("timestamp"), } external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, columns_to_types=columns_to_types) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) engine_adapter.insert_append( - table_name=external_table, query_or_df=df, columns_to_types=columns_to_types + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types ) # plan + apply @@ -2937,7 +2939,7 @@ def _dates_in_table(table_name: str) -> t.List[str]: } ) engine_adapter.replace_query( - table_name=external_table, query_or_df=df, columns_to_types=columns_to_types + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types ) # Restate A across a day boundary with the expectation that two day intervals in B are affected @@ -3016,9 +3018,9 @@ def test_restatement_plan_respects_disable_restatements(tmp_path: Path): "ts": exp.DataType.build("timestamp"), } external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, columns_to_types=columns_to_types) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) engine_adapter.insert_append( - table_name=external_table, query_or_df=df, columns_to_types=columns_to_types + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types ) # plan + apply @@ -3122,9 +3124,9 @@ def test_restatement_plan_clears_correct_intervals_across_environments(tmp_path: "date": exp.DataType.build("date"), } external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, columns_to_types=columns_to_types) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) engine_adapter.insert_append( - table_name=external_table, query_or_df=df, columns_to_types=columns_to_types + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types ) # first, create the prod models @@ -3317,9 +3319,9 @@ def _derived_incremental_model_def(name: str, upstream: str) -> str: "ts": exp.DataType.build("timestamp"), } external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, columns_to_types=columns_to_types) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) engine_adapter.insert_append( - table_name=external_table, query_or_df=df, columns_to_types=columns_to_types + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types ) # plan + apply A, B, C in prod @@ -3467,9 +3469,9 @@ def test_prod_restatement_plan_clears_unaligned_intervals_in_derived_dev_tables( "ts": exp.DataType.build("timestamp"), } external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, columns_to_types=columns_to_types) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) engine_adapter.insert_append( - table_name=external_table, query_or_df=df, columns_to_types=columns_to_types + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types ) # plan + apply A[hourly] in prod @@ -3609,9 +3611,9 @@ def test_prod_restatement_plan_causes_dev_intervals_to_be_processed_in_next_dev_ "ts": exp.DataType.build("timestamp"), } external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, columns_to_types=columns_to_types) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) engine_adapter.insert_append( - table_name=external_table, query_or_df=df, columns_to_types=columns_to_types + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types ) # plan + apply A[hourly] in prod @@ -3744,9 +3746,9 @@ def test_prod_restatement_plan_causes_dev_intervals_to_be_widened_on_full_restat "ts": exp.DataType.build("timestamp"), } external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, columns_to_types=columns_to_types) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) engine_adapter.insert_append( - table_name=external_table, query_or_df=df, columns_to_types=columns_to_types + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types ) # plan + apply A[daily] in prod @@ -3879,9 +3881,9 @@ def test_prod_restatement_plan_missing_model_in_dev( "ts": exp.DataType.build("timestamp"), } external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, columns_to_types=columns_to_types) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) engine_adapter.insert_append( - table_name=external_table, query_or_df=df, columns_to_types=columns_to_types + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types ) # plan + apply A[hourly] in dev @@ -7625,3 +7627,924 @@ def test_default_audits_with_custom_audit_definitions(tmp_path: Path): if audit_name == "positive_amount": assert "column" in audit_args assert audit_args["column"].name == "amount" + + +def test_incremental_by_time_model_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + assert updated_df["new_column"].dropna().tolist() == [3] + + with time_machine.travel("2023-01-11 00:00:00 UTC"): + updated_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + CAST(4 AS STRING) as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(updated_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True, run=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 3 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + # The destructive change was ignored but this change is coercable and therefore we still return ints + assert updated_df["new_column"].dropna().tolist() == [3, 4] + + with time_machine.travel("2023-01-12 00:00:00 UTC"): + updated_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + CAST(5 AS STRING) as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(updated_model) + + context = Context(paths=[tmp_path], config=config) + # Make the change compatible since that means we will attempt and alter now that is considered additive + context.engine_adapter.SCHEMA_DIFFER.compatible_types = { + exp.DataType.build("INT"): {exp.DataType.build("STRING")} + } + context.plan("prod", auto_apply=True, no_prompts=True, run=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 4 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + # The change is now reflected since an additive alter could be performed + assert updated_df["new_column"].dropna().tolist() == ["3", "4", "5"] + + context.close() + + +def test_incremental_by_unique_key_model_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key id, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key id, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_incremental_unmanaged_model_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_UNMANAGED( + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_UNMANAGED( + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_scd_type_2_by_time_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + updated_at_name ds, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_dt as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + updated_at_name ds, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 3 as new_column, + @start_dt as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_scd_type_2_by_column_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key id, + columns [name], + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key id, + columns [new_column], + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_incremental_partition_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_PARTITION ( + on_destructive_change ignore + ), + partitioned_by [ds], + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_PARTITION ( + on_destructive_change ignore + ), + partitioned_by [ds], + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_incremental_by_time_model_ignore_destructive_change_unit_test(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + test_dir = tmp_path / "tests" + test_dir.mkdir() + test_filepath = test_dir / "test_test_model.yaml" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + id, + name, + ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + initial_test = f""" + +test_test_model: + model: test_model + inputs: + source_table: + - id: 1 + name: 'test_name' + ds: '2025-01-01' + outputs: + query: + - id: 1 + name: 'test_name' + ds: '2025-01-01' +""" + + # Write initial test + test_filepath.write_text(initial_test) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute( + "CREATE TABLE source_table (id INT, name STRING, new_column INT, ds STRING)" + ) + context.engine_adapter.execute( + "INSERT INTO source_table VALUES (1, 'test_name', NULL, '2023-01-01')" + ) + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + test_result = context.test() + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + id, + new_column, + ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + updated_test = f""" + + test_test_model: + model: test_model + inputs: + source_table: + - id: 1 + new_column: 3 + ds: '2025-01-01' + outputs: + query: + - id: 1 + new_column: 3 + ds: '2025-01-01' + """ + + # Write initial test + test_filepath.write_text(updated_test) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + test_result = context.test() + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 1 + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("INSERT INTO source_table VALUES (2, NULL, 3, '2023-01-09')") + context.run() + test_result = context.test() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() diff --git a/tests/core/test_schema_diff.py b/tests/core/test_schema_diff.py index 1e57cab57c..fd14b0b9b3 100644 --- a/tests/core/test_schema_diff.py +++ b/tests/core/test_schema_diff.py @@ -1331,6 +1331,201 @@ def test_schema_diff_alter_op_column(): ) +@pytest.mark.parametrize( + "current_struct, new_struct, expected_diff_with_destructive, expected_diff_ignore_destructive, config", + [ + # Simple DROP operation - should be ignored when ignore_destructive=True + ( + "STRUCT", + "STRUCT", + [ + TableAlterOperation.drop( + TableAlterColumn.primitive("name"), + "STRUCT", + "STRING", + ) + ], + [], # No operations when ignoring destructive + {}, + ), + # DROP + ADD operation (incompatible type change) - should be ignored when ignore_destructive=True + ( + "STRUCT", + "STRUCT", + [ + TableAlterOperation.drop( + TableAlterColumn.primitive("name"), + "STRUCT", + "STRING", + ), + TableAlterOperation.add( + TableAlterColumn.primitive("name"), + "BIGINT", + "STRUCT", + ), + ], + [], # No operations when ignoring destructive + {}, + ), + # Pure ADD operation - should work same way regardless of ignore_destructive + ( + "STRUCT", + "STRUCT", + [ + TableAlterOperation.add( + TableAlterColumn.primitive("new_col"), + "STRING", + "STRUCT", + ), + ], + [ + # Same operation when ignoring destructive + TableAlterOperation.add( + TableAlterColumn.primitive("new_col"), + "STRING", + "STRUCT", + ), + ], + {}, + ), + # Mix of destructive and non-destructive operations + ( + "STRUCT", + "STRUCT", + [ + TableAlterOperation.drop( + TableAlterColumn.primitive("name"), + "STRUCT", + "STRING", + ), + TableAlterOperation.add( + TableAlterColumn.primitive("address"), + "STRING", + "STRUCT", + ), + TableAlterOperation.alter_type( + TableAlterColumn.primitive("id"), + "STRING", + current_type="INT", + expected_table_struct="STRUCT", + ), + ], + [ + # Only non-destructive operations remain + TableAlterOperation.add( + TableAlterColumn.primitive("address"), + "STRING", + "STRUCT", + ), + TableAlterOperation.alter_type( + TableAlterColumn.primitive("id"), + "STRING", + current_type="INT", + expected_table_struct="STRUCT", + ), + ], + dict( + compatible_types={ + exp.DataType.build("INT"): {exp.DataType.build("STRING")}, + } + ), + ), + ], +) +def test_ignore_destructive_operations( + current_struct, + new_struct, + expected_diff_with_destructive: t.List[TableAlterOperation], + expected_diff_ignore_destructive: t.List[TableAlterOperation], + config: t.Dict[str, t.Any], +): + resolver = SchemaDiffer(**config) + + # Test with destructive operations allowed (default behavior) + operations_with_destructive = resolver._from_structs( + exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=False + ) + assert operations_with_destructive == expected_diff_with_destructive + + # Test with destructive operations ignored + operations_ignore_destructive = resolver._from_structs( + exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=True + ) + assert operations_ignore_destructive == expected_diff_ignore_destructive + + +def test_ignore_destructive_compare_columns(): + """Test ignore_destructive behavior in compare_columns method.""" + schema_differ = SchemaDiffer( + support_positional_add=True, + support_nested_operations=False, + compatible_types={ + exp.DataType.build("INT"): {exp.DataType.build("STRING")}, + }, + ) + + current = { + "id": exp.DataType.build("INT"), + "name": exp.DataType.build("STRING"), + "to_drop": exp.DataType.build("DOUBLE"), + "age": exp.DataType.build("INT"), + } + + new = { + "id": exp.DataType.build("STRING"), # Compatible type change + "name": exp.DataType.build("STRING"), + "age": exp.DataType.build("INT"), + "new_col": exp.DataType.build("DOUBLE"), # New column + } + + # With destructive operations allowed + alter_expressions_with_destructive = schema_differ.compare_columns( + "test_table", current, new, ignore_destructive=False + ) + assert len(alter_expressions_with_destructive) == 3 # DROP + ADD + ALTER + + # With destructive operations ignored + alter_expressions_ignore_destructive = schema_differ.compare_columns( + "test_table", current, new, ignore_destructive=True + ) + assert len(alter_expressions_ignore_destructive) == 2 # Only ADD + ALTER + + # Verify the operations are correct + operations_sql = [expr.sql() for expr in alter_expressions_ignore_destructive] + add_column_found = any("ADD COLUMN new_col DOUBLE" in op for op in operations_sql) + alter_column_found = any("ALTER COLUMN id SET DATA TYPE" in op for op in operations_sql) + drop_column_found = any("DROP COLUMN to_drop" in op for op in operations_sql) + + assert add_column_found, f"ADD COLUMN not found in: {operations_sql}" + assert alter_column_found, f"ALTER COLUMN not found in: {operations_sql}" + assert not drop_column_found, f"DROP COLUMN should not be present in: {operations_sql}" + + +def test_ignore_destructive_nested_struct_without_support(): + """Test ignore_destructive with nested structs when nested_drop is not supported.""" + schema_differ = SchemaDiffer( + support_nested_operations=True, + support_nested_drop=False, # This forces DROP+ADD for nested changes + ) + + current_struct = "STRUCT>" + new_struct = "STRUCT>" # Removes col_b + + # With destructive operations allowed - should do DROP+ADD of entire struct + operations_with_destructive = schema_differ._from_structs( + exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=False + ) + assert len(operations_with_destructive) == 2 # DROP struct + ADD struct + assert operations_with_destructive[0].is_drop + assert operations_with_destructive[1].is_add + + # With destructive operations ignored - should do nothing + operations_ignore_destructive = schema_differ._from_structs( + exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=True + ) + assert len(operations_ignore_destructive) == 0 + + def test_get_schema_differ(): # Test that known dialects return SchemaDiffer instances for dialect in ["bigquery", "snowflake", "postgres", "databricks", "spark", "duckdb"]: @@ -1376,3 +1571,43 @@ def test_get_schema_differ(): schema_differ_upper.support_coercing_compatible_types == schema_differ_lower.support_coercing_compatible_types ) + + +def test_ignore_destructive_edge_cases(): + """Test edge cases for ignore_destructive behavior.""" + schema_differ = SchemaDiffer(support_positional_add=True) + + # Test when all operations are destructive - should result in empty list + current_struct = "STRUCT" + new_struct = "STRUCT<>" # Remove all columns + + operations_ignore_destructive = schema_differ._from_structs( + exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=True + ) + assert len(operations_ignore_destructive) == 0 + + # Test when no operations are needed - should result in empty list regardless of ignore_destructive + same_struct = "STRUCT" + + operations_same_with_destructive = schema_differ._from_structs( + exp.DataType.build(same_struct), exp.DataType.build(same_struct), ignore_destructive=False + ) + operations_same_ignore_destructive = schema_differ._from_structs( + exp.DataType.build(same_struct), exp.DataType.build(same_struct), ignore_destructive=True + ) + assert len(operations_same_with_destructive) == 0 + assert len(operations_same_ignore_destructive) == 0 + + # Test when only ADD operations are needed - should be same regardless of ignore_destructive + current_struct = "STRUCT" + new_struct = "STRUCT" + + operations_add_with_destructive = schema_differ._from_structs( + exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=False + ) + operations_add_ignore_destructive = schema_differ._from_structs( + exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=True + ) + assert len(operations_add_with_destructive) == 2 # ADD name, ADD age + assert len(operations_add_ignore_destructive) == 2 # Same operations + assert operations_add_with_destructive == operations_add_ignore_destructive diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index d474159b7c..b05d567cd2 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -204,7 +204,7 @@ def x(evaluator, y=None) -> None: ) common_kwargs = dict( - columns_to_types={"a": exp.DataType.build("int")}, + target_columns_to_types={"a": exp.DataType.build("int")}, table_format=None, storage_format="parquet", partitioned_by=[exp.to_column("a", quoted=True)], @@ -677,6 +677,8 @@ def test_evaluate_incremental_unmanaged_with_intervals( snapshot.categorize_as(SnapshotChangeCategory.BREAKING) snapshot.intervals = [(to_timestamp("2020-01-01"), to_timestamp("2020-01-02"))] + adapter_mock.columns.return_value = model.columns_to_types + evaluator = SnapshotEvaluator(adapter_mock) evaluator.evaluate( snapshot, @@ -691,13 +693,15 @@ def test_evaluate_incremental_unmanaged_with_intervals( snapshot.table_name(), model.render_query(), [exp.to_column("ds", quoted=True)], - columns_to_types=model.columns_to_types, + target_columns_to_types=model.columns_to_types, + source_columns=None, ) else: adapter_mock.insert_append.assert_called_once_with( snapshot.table_name(), model.render_query(), - columns_to_types=model.columns_to_types, + target_columns_to_types=model.columns_to_types, + source_columns=None, ) @@ -731,13 +735,14 @@ def test_evaluate_incremental_unmanaged_no_intervals( model.render_query(), clustered_by=[], column_descriptions={}, - columns_to_types=table_columns, + target_columns_to_types=table_columns, partition_interval_unit=model.partition_interval_unit, partitioned_by=model.partitioned_by, table_format=None, storage_format=None, table_description=None, table_properties={}, + source_columns=None, ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) @@ -846,7 +851,10 @@ def test_create_new_forward_only_model(mocker: MockerFixture, adapter_mock, make # Only non-deployable table should be created adapter_mock.create_table.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.dev_version}__dev", - columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("varchar")}, + target_columns_to_types={ + "a": exp.DataType.build("int"), + "ds": exp.DataType.build("varchar"), + }, table_format=None, storage_format=None, partitioned_by=model.partitioned_by, @@ -1005,7 +1013,7 @@ def test_create_prod_table_exists_forward_only(mocker: MockerFixture, adapter_mo adapter_mock.create_schema.assert_called_once_with(to_schema("sqlmesh__test_schema")) adapter_mock.create_table.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", - columns_to_types={"a": exp.DataType.build("int")}, + target_columns_to_types={"a": exp.DataType.build("int")}, table_format=None, storage_format=None, partitioned_by=[], @@ -1606,7 +1614,7 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) adapter_mock.create_table.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev__schema_migration_source", - columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, + target_columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, table_format=None, storage_format=None, partitioned_by=[exp.to_column("ds", quoted=True)], @@ -1627,6 +1635,7 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) adapter_mock.get_alter_expressions.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev__schema_migration_source", + ignore_destructive=False, ) adapter_mock.alter_table.assert_called_once_with([]) @@ -1665,7 +1674,7 @@ def test_create_clone_in_dev_missing_table(mocker: MockerFixture, adapter_mock, adapter_mock.create_table.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.dev_version}__dev", - columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, + target_columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, table_format=None, storage_format=None, partitioned_by=[exp.to_column("ds", quoted=True)], @@ -1727,6 +1736,7 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m adapter_mock.get_alter_expressions.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev__schema_migration_source", + ignore_destructive=False, ) adapter_mock.alter_table.assert_called_once_with([]) @@ -1781,7 +1791,7 @@ def test_create_clone_in_dev_self_referencing( adapter_mock.create_table.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev__schema_migration_source", - columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, + target_columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, table_format=None, storage_format=None, partitioned_by=[exp.to_column("ds", quoted=True)], @@ -1912,7 +1922,7 @@ def test_forward_only_snapshot_for_added_model(mocker: MockerFixture, adapter_mo evaluator.create([snapshot], {}) common_create_args = dict( - columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, + target_columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, table_format=None, storage_format=None, partitioned_by=[exp.to_column("ds", quoted=True)], @@ -1956,7 +1966,7 @@ def test_create_scd_type_2_by_time(adapter_mock, make_snapshot): evaluator.create([snapshot], {}) common_kwargs = dict( - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("STRING"), "updated_at": exp.DataType.build("TIMESTAMPTZ"), @@ -2093,7 +2103,7 @@ def test_insert_into_scd_type_2_by_time( adapter_mock.scd_type_2_by_time.assert_called_once_with( target_table=snapshot.table_name(), source_table=model.render_query(), - columns_to_types=table_columns, + target_columns_to_types=table_columns, table_format=None, unique_key=[exp.to_column("id", quoted=True)], valid_from_col=exp.column("valid_from", quoted=True), @@ -2105,6 +2115,7 @@ def test_insert_into_scd_type_2_by_time( column_descriptions={}, updated_at_as_valid_from=False, truncate=truncate, + source_columns=None, ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) @@ -2134,7 +2145,7 @@ def test_create_scd_type_2_by_column(adapter_mock, make_snapshot): evaluator.create([snapshot], {}) common_kwargs = dict( - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("STRING"), # Make sure that the call includes these extra columns @@ -2265,7 +2276,7 @@ def test_insert_into_scd_type_2_by_column( adapter_mock.scd_type_2_by_column.assert_called_once_with( target_table=snapshot.table_name(), source_table=model.render_query(), - columns_to_types=table_columns, + target_columns_to_types=table_columns, table_format=None, unique_key=[exp.to_column("id", quoted=True)], check_columns=exp.Star(), @@ -2277,6 +2288,7 @@ def test_insert_into_scd_type_2_by_column( table_description=None, column_descriptions={}, truncate=truncate, + source_columns=None, ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) @@ -2314,7 +2326,7 @@ def test_create_incremental_by_unique_key_updated_at_exp(adapter_mock, make_snap adapter_mock.merge.assert_called_once_with( snapshot.table_name(), model.render_query(), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("STRING"), "updated_at": exp.DataType.build("TIMESTAMP"), @@ -2345,6 +2357,7 @@ def test_create_incremental_by_unique_key_updated_at_exp(adapter_mock, make_snap ] ), physical_properties={}, + source_columns=None, ) @@ -2382,7 +2395,7 @@ def test_create_incremental_by_unique_key_multiple_updated_at_exp(adapter_mock, adapter_mock.merge.assert_called_once_with( snapshot.table_name(), model.render_query(), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "name": exp.DataType.build("STRING"), "updated_at": exp.DataType.build("TIMESTAMP"), @@ -2434,6 +2447,7 @@ def test_create_incremental_by_unique_key_multiple_updated_at_exp(adapter_mock, ], ), physical_properties={}, + source_columns=None, ) @@ -2477,13 +2491,14 @@ def test_create_incremental_by_unique_no_intervals(adapter_mock, make_snapshot): model.render_query(), clustered_by=[], column_descriptions={}, - columns_to_types=table_columns, + target_columns_to_types=table_columns, partition_interval_unit=model.partition_interval_unit, partitioned_by=model.partitioned_by, table_format=None, storage_format=None, table_description=None, table_properties={}, + source_columns=None, ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) @@ -2541,7 +2556,7 @@ def test_create_incremental_by_unique_key_merge_filter(adapter_mock, make_snapsh adapter_mock.merge.assert_called_once_with( snapshot.table_name(), model.render_query(), - columns_to_types={ + target_columns_to_types={ "id": exp.DataType.build("INT"), "updated_at": exp.DataType.build("TIMESTAMP"), }, @@ -2582,6 +2597,7 @@ def test_create_incremental_by_unique_key_merge_filter(adapter_mock, make_snapsh ), ), physical_properties={}, + source_columns=None, ) @@ -2607,7 +2623,10 @@ def test_create_seed(mocker: MockerFixture, adapter_mock, make_snapshot): evaluator.create([snapshot], {}) common_create_kwargs: t.Dict[str, t.Any] = dict( - columns_to_types={"id": exp.DataType.build("bigint"), "name": exp.DataType.build("text")}, + target_columns_to_types={ + "id": exp.DataType.build("bigint"), + "name": exp.DataType.build("text"), + }, table_format=None, storage_format=None, partitioned_by=[], @@ -2621,6 +2640,7 @@ def test_create_seed(mocker: MockerFixture, adapter_mock, make_snapshot): f"sqlmesh__db.db__seed__{snapshot.version}", mocker.ANY, column_descriptions={}, + source_columns=["id", "name"], **common_create_kwargs, ) @@ -2684,7 +2704,10 @@ def test_create_seed_on_error(mocker: MockerFixture, adapter_mock, make_snapshot f"sqlmesh__db.db__seed__{snapshot.version}", mocker.ANY, column_descriptions={}, - columns_to_types={"id": exp.DataType.build("bigint"), "name": exp.DataType.build("text")}, + target_columns_to_types={ + "id": exp.DataType.build("bigint"), + "name": exp.DataType.build("text"), + }, table_format=None, storage_format=None, partitioned_by=[], @@ -2692,6 +2715,7 @@ def test_create_seed_on_error(mocker: MockerFixture, adapter_mock, make_snapshot clustered_by=[], table_properties={}, table_description=None, + source_columns=["id", "name"], ) adapter_mock.drop_table.assert_called_once_with(f"sqlmesh__db.db__seed__{snapshot.version}") @@ -2740,7 +2764,10 @@ def test_create_seed_no_intervals(mocker: MockerFixture, adapter_mock, make_snap f"sqlmesh__db.db__seed__{snapshot.version}", mocker.ANY, column_descriptions={}, - columns_to_types={"id": exp.DataType.build("bigint"), "name": exp.DataType.build("text")}, + target_columns_to_types={ + "id": exp.DataType.build("bigint"), + "name": exp.DataType.build("text"), + }, table_format=None, storage_format=None, partitioned_by=[], @@ -2748,6 +2775,7 @@ def test_create_seed_no_intervals(mocker: MockerFixture, adapter_mock, make_snap clustered_by=[], table_properties={}, table_description=None, + source_columns=["id", "name"], ) @@ -3245,7 +3273,7 @@ def test_evaluate_incremental_by_partition(mocker: MockerFixture, make_snapshot, exp.to_column("ds", quoted=True), exp.to_column("b", quoted=True), ], - columns_to_types=model.columns_to_types, + target_columns_to_types=model.columns_to_types, clustered_by=[], table_properties={}, column_descriptions={}, @@ -3253,6 +3281,7 @@ def test_evaluate_incremental_by_partition(mocker: MockerFixture, make_snapshot, storage_format=None, table_description=None, table_format=None, + source_columns=None, ) adapter_mock.reset_mock() @@ -3274,7 +3303,8 @@ def test_evaluate_incremental_by_partition(mocker: MockerFixture, make_snapshot, exp.to_column("ds", quoted=True), exp.to_column("b", quoted=True), ], - columns_to_types=model.columns_to_types, + target_columns_to_types=model.columns_to_types, + source_columns=None, ) @@ -3291,6 +3321,7 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: nonlocal custom_insert_kind @@ -3365,6 +3396,7 @@ def insert( query_or_df: QueryOrDF, model: Model, is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: nonlocal custom_insert_kind @@ -3502,7 +3534,7 @@ def test_create_managed(adapter_mock, make_snapshot, mocker: MockerFixture): adapter_mock.create_managed_table.assert_called_with( table_name=snapshot.table_name(), query=mocker.ANY, - columns_to_types=model.columns_to_types, + target_columns_to_types=model.columns_to_types, partitioned_by=model.partitioned_by, clustered_by=model.clustered_by, table_properties=model.physical_properties, @@ -3574,7 +3606,7 @@ def test_evaluate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): adapter_mock.replace_query.assert_called_with( snapshot.table_name(is_deployable=False), mocker.ANY, - columns_to_types=table_colmns, + target_columns_to_types=table_colmns, table_format=model.table_format, storage_format=model.storage_format, partitioned_by=model.partitioned_by, @@ -3583,6 +3615,7 @@ def test_evaluate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): table_properties=model.physical_properties, table_description=model.description, column_descriptions=model.column_descriptions, + source_columns=None, ) adapter_mock.columns.assert_called_once_with(snapshot.table_name(is_deployable=False)) @@ -3755,7 +3788,7 @@ def test_create_snapshot( ) common_kwargs: t.Dict[str, t.Any] = dict( - columns_to_types={"a": exp.DataType.build("int")}, + target_columns_to_types={"a": exp.DataType.build("int")}, table_format=None, storage_format=None, partitioned_by=[], @@ -3822,13 +3855,16 @@ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_moc [ call( new_snapshot.table_name(), - columns_to_types={"a": exp.DataType.build("int")}, + target_columns_to_types={"a": exp.DataType.build("int")}, column_descriptions={}, **common_kwargs, ), call( new_snapshot.table_name(is_deployable=False), - columns_to_types={"a": exp.DataType.build("int"), "b": exp.DataType.build("int")}, + target_columns_to_types={ + "a": exp.DataType.build("int"), + "b": exp.DataType.build("int"), + }, column_descriptions=None, **common_kwargs, ), @@ -3851,6 +3887,7 @@ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_moc adapter_mock.get_alter_expressions.assert_called_once_with( snapshot.table_name(), new_snapshot.table_name(is_deployable=False), + ignore_destructive=False, ) @@ -4117,7 +4154,9 @@ def columns(table_name): # The second mock adapter has to be called only for the gateway-specific model adapter_mock.get_alter_expressions.assert_called_once_with( - snapshot_2.table_name(True), snapshot_2.table_name(False) + snapshot_2.table_name(True), + snapshot_2.table_name(False), + ignore_destructive=False, ) diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index d4dbf62e74..944c4ce78d 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -31,16 +31,16 @@ def test_adapter_relation(sushi_test_project: Project, runtime_renderer: t.Calla engine_adapter.create_schema("foo") engine_adapter.create_schema("ignored") engine_adapter.create_table( - table_name="foo.bar", columns_to_types={"baz": exp.DataType.build("int")} + table_name="foo.bar", target_columns_to_types={"baz": exp.DataType.build("int")} ) engine_adapter.create_table( - table_name="foo.another", columns_to_types={"col": exp.DataType.build("int")} + table_name="foo.another", target_columns_to_types={"col": exp.DataType.build("int")} ) engine_adapter.create_view( view_name="foo.bar_view", query_or_df=parse_one("select * from foo.bar") ) engine_adapter.create_table( - table_name="ignored.ignore", columns_to_types={"col": exp.DataType.build("int")} + table_name="ignored.ignore", target_columns_to_types={"col": exp.DataType.build("int")} ) assert ( @@ -262,10 +262,10 @@ def test_adapter_map_snapshot_tables( engine_adapter.create_schema("sqlmesh") engine_adapter.create_table( table_name='"memory"."sqlmesh"."test_db__test_model"', - columns_to_types={"baz": exp.DataType.build("int")}, + target_columns_to_types={"baz": exp.DataType.build("int")}, ) engine_adapter.create_table( - table_name="foo.bar", columns_to_types={"col": exp.DataType.build("int")} + table_name="foo.bar", target_columns_to_types={"col": exp.DataType.build("int")} ) expected_test_model_table_name = parse_one('"memory"."sqlmesh"."test_db__test_model"').sql( @@ -324,7 +324,7 @@ def test_adapter_get_relation_normalization( engine_adapter.create_schema('"FOO"') engine_adapter.create_table( - table_name='"FOO"."BAR"', columns_to_types={"baz": exp.DataType.build("int")} + table_name='"FOO"."BAR"', target_columns_to_types={"baz": exp.DataType.build("int")} ) assert ( diff --git a/tests/dbt/test_integration.py b/tests/dbt/test_integration.py index 9cee4796fb..45c1422395 100644 --- a/tests/dbt/test_integration.py +++ b/tests/dbt/test_integration.py @@ -194,9 +194,11 @@ def _replace_source_table( columns_to_types = columns_to_types_from_df(df) if values: - adapter.replace_query("sushi.raw_marketing", df, columns_to_types=columns_to_types) + adapter.replace_query( + "sushi.raw_marketing", df, target_columns_to_types=columns_to_types + ) else: - adapter.create_table("sushi.raw_marketing", columns_to_types=columns_to_types) + adapter.create_table("sushi.raw_marketing", target_columns_to_types=columns_to_types) def _normalize_dbt_dataframe( self, From 19270e1cba455b517eea675cf37f5372492b6a8b Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Sat, 16 Aug 2025 12:52:25 -0700 Subject: [PATCH 0722/1056] Fix: Use csv header as the primary source for column names when converting dbt seeds (#5173) --- sqlmesh/dbt/seed.py | 44 +++++++++++++++----------------- tests/dbt/test_transformation.py | 28 +++++++++----------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/sqlmesh/dbt/seed.py b/sqlmesh/dbt/seed.py index fde5c7e569..10e98cf93c 100644 --- a/sqlmesh/dbt/seed.py +++ b/sqlmesh/dbt/seed.py @@ -58,29 +58,27 @@ def to_sqlmesh( kwargs = self.sqlmesh_model_kwargs(context) columns = kwargs.get("columns") or {} - descriptions = kwargs.get("column_descriptions") or {} - missing_types = (set(descriptions) | set(self.columns)) - set(columns) - if not columns or missing_types: - agate_table = ( - agate_helper.from_csv(seed_path, [], delimiter=self.delimiter) - if SUPPORTS_DELIMITER - else agate_helper.from_csv(seed_path, []) - ) - inferred_types = { - name: AGATE_TYPE_MAPPING[tpe.__class__] - for name, tpe in zip(agate_table.column_names, agate_table.column_types) - } - - # The columns list built from the mixture of supplied and inferred types needs to - # be in the same order as the data for assumptions elsewhere in the codebase to hold true - new_columns = {} - for column_name in agate_table.column_names: - if (column_name in missing_types) or (column_name not in columns): - new_columns[column_name] = inferred_types[column_name] - else: - new_columns[column_name] = columns[column_name] - - kwargs["columns"] = new_columns + + agate_table = ( + agate_helper.from_csv(seed_path, [], delimiter=self.delimiter) + if SUPPORTS_DELIMITER + else agate_helper.from_csv(seed_path, []) + ) + inferred_types = { + name: AGATE_TYPE_MAPPING[tpe.__class__] + for name, tpe in zip(agate_table.column_names, agate_table.column_types) + } + + # The columns list built from the mixture of supplied and inferred types needs to + # be in the same order as the data for assumptions elsewhere in the codebase to hold true + new_columns = {} + for column_name in agate_table.column_names: + if column_name not in columns: + new_columns[column_name] = inferred_types[column_name] + else: + new_columns[column_name] = columns[column_name] + + kwargs["columns"] = new_columns return create_seed_model( self.canonical_name(context), diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index e8b355e9f5..a16cc16f43 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -471,22 +471,18 @@ def test_seed_columns(): package="package", path=Path("examples/sushi_dbt/seeds/waiter_names.csv"), columns={ - "address": ColumnConfig( - name="address", data_type="text", description="Business address" - ), - "zipcode": ColumnConfig( - name="zipcode", data_type="text", description="Business zipcode" - ), + "id": ColumnConfig(name="id", data_type="text", description="The ID"), + "name": ColumnConfig(name="name", data_type="text", description="The name"), }, ) expected_column_types = { - "address": exp.DataType.build("text"), - "zipcode": exp.DataType.build("text"), + "id": exp.DataType.build("text"), + "name": exp.DataType.build("text"), } expected_column_descriptions = { - "address": "Business address", - "zipcode": "Business zipcode", + "id": "The ID", + "name": "The name", } context = DbtContext() @@ -503,21 +499,21 @@ def test_seed_column_types(): package="package", path=Path("examples/sushi_dbt/seeds/waiter_names.csv"), column_types={ - "address": "text", - "zipcode": "text", + "id": "text", + "name": "text", }, columns={ - "zipcode": ColumnConfig(name="zipcode", description="Business zipcode"), + "name": ColumnConfig(name="name", description="The name"), }, quote_columns=True, ) expected_column_types = { - "address": exp.DataType.build("text"), - "zipcode": exp.DataType.build("text"), + "id": exp.DataType.build("text"), + "name": exp.DataType.build("text"), } expected_column_descriptions = { - "zipcode": "Business zipcode", + "name": "The name", } context = DbtContext() From 21f06ddffc204ac70ad171c4a049dae4fa3e84a7 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Mon, 18 Aug 2025 10:39:39 +1200 Subject: [PATCH 0723/1056] Docs(dbt): update for yaml config (#5169) --- docs/integrations/dbt.md | 114 ++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 26 deletions(-) diff --git a/docs/integrations/dbt.md b/docs/integrations/dbt.md index e46e2fef39..a07e02bb23 100644 --- a/docs/integrations/dbt.md +++ b/docs/integrations/dbt.md @@ -44,16 +44,36 @@ Prepare an existing dbt project to be run by SQLMesh by executing the `sqlmesh i $ sqlmesh init -t dbt ``` -SQLMesh will use the data warehouse connection target in your dbt project `profiles.yml` file. The target can be changed at any time. +This will create a file called `sqlmesh.yaml` containing the [default model start date](../reference/model_configuration.md#model-defaults). This configuration file is a minimum starting point for enabling SQLMesh to work with your DBT project. + +As you become more comfortable with running your project under SQLMesh, you may specify additional SQLMesh [configuration](../reference/configuration.md) as required to unlock more features. + +!!! note "profiles.yml" + + SQLMesh will use the existing data warehouse connection target from your dbt project's `profiles.yml` file so the connection configuration does not need to be duplicated in `sqlmesh.yaml`. You may change the target at any time in the dbt config and SQLMesh will pick up the new target. ### Setting model backfill start dates -Models **require** a start date for backfilling data through use of the `start` configuration parameter. `start` can be defined individually for each model in its `config` block or globally in the `dbt_project.yml` file as follows: +Models **require** a start date for backfilling data through use of the `start` configuration parameter. `start` can be defined individually for each model in its `config` block or globally in the `sqlmesh.yaml` file as follows: -``` -> models: -> +start: Jan 1 2000 -``` +=== "sqlmesh.yaml" + + ```yaml + model_defaults: + start: '2000-01-01' + ``` + +=== "dbt Model" + + ```jinja + {{ + config( + materialized='incremental', + start='2000-01-01', + ... + ) + }} + ``` ### Configuration @@ -63,47 +83,89 @@ SQLMesh derives a project's configuration from its dbt configuration files. This [Certain engines](https://sqlmesh.readthedocs.io/en/stable/guides/configuration/?h=unsupported#state-connection), like Trino, cannot be used to store SQLMesh's state. -As a workaround, we recommend specifying a supported state engine using the `state_connection` argument instead. +In addition, even if your warehouse is supported for state, you may find that you get better performance by using a [traditional database](../concepts/state.md) to store state as these are a better fit for the state workload than a warehouse optimized for analytics workloads. -Learn more about how to configure state connections in Python [here](https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#state-connection). +In these cases, we recommend specifying a [supported production state engine](../concepts/state.md#state) using the `state_connection` configuration. -#### Runtime vars +This involves updating `sqlmesh.yaml` to add a gateway configuration for the state connection: -dbt supports passing variable values at runtime with its [CLI `vars` option](https://docs.getdbt.com/docs/build/project-variables#defining-variables-on-the-command-line). +```yaml +gateways: + "": # "" (empty string) is the default gateway + state_connection: + type: postgres + ... -In SQLMesh, these variables are passed via configurations. When you initialize a dbt project with `sqlmesh init`, a file `config.py` is created in your project directory. +model_defaults: + start: '2000-01-01' +``` -The file creates a SQLMesh `config` object pointing to the project directory: +Or, for a specific dbt profile defined in `profiles.yml`, eg `dev`: -```python -config = sqlmesh_config(Path(__file__).parent) +```yaml +gateways: + dev: # must match the target dbt profile name + state_connection: + type: postgres + ... + +model_defaults: + start: '2000-01-01' ``` -Specify runtime variables by adding a Python dictionary to the `sqlmesh_config()` `variables` argument. +Learn more about how to configure state connections [here](https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#state-connection). + +#### Runtime vars + +dbt supports passing variable values at runtime with its [CLI `vars` option](https://docs.getdbt.com/docs/build/project-variables#defining-variables-on-the-command-line). + +In SQLMesh, these variables are passed via configurations. When you initialize a dbt project with `sqlmesh init`, a file `sqlmesh.yaml` is created in your project directory. + +You may define global variables in the same way as a native project by adding a `variables` section to the config. For example, we could specify the runtime variable `is_marketing` and its value `no` as: -```python -config = sqlmesh_config( - Path(__file__).parent, - variables={"is_marketing": "no"} - ) +```yaml +variables: + is_marketing: no + +model_defaults: + start: '2000-01-01' ``` +Variables can also be set at the gateway/profile level which override variables set at the project level. See the [variables documentation](../concepts/macros/sqlmesh_macros.md#gateway-variables) to learn more about how to specify them at different levels. + +#### Combinations + Some projects use combinations of runtime variables to control project behavior. Different combinations can be specified in different `sqlmesh_config` objects, with the relevant configuration passed to the SQLMesh CLI command. +!!! info "Python config" + + Switching between different config objects requires the use of [Python config](../guides/configuration.md#python) instead of the default YAML config. + + You will need to create a file called `config.py` in the root of your project with the following contents: + + ```py + from pathlib import Path + from sqlmesh.dbt.loader import sqlmesh_config + + config = sqlmesh_config(Path(__file__).parent) + ``` + + Note that any config from `sqlmesh.yaml` will be overlayed on top of the active Python config so you dont need to remove the `sqlmesh.yaml` file + For example, consider a project with a special configuration for the `marketing` department. We could create separate configurations to pass at runtime like this: ```python config = sqlmesh_config( - Path(__file__).parent, - variables={"is_marketing": "no", "include_pii": "no"} - ) + Path(__file__).parent, + variables={"is_marketing": "no", "include_pii": "no"} +) marketing_config = sqlmesh_config( - Path(__file__).parent, - variables={"is_marketing": "yes", "include_pii": "yes"} - ) + Path(__file__).parent, + variables={"is_marketing": "yes", "include_pii": "yes"} +) ``` By default, SQLMesh will use the configuration object named `config`. Use a different configuration by passing the object name to SQLMesh CLI commands with the `--config` option. For example, we could run a `plan` with the marketing configuration like this: From d12d5b383137c398952ec92c2bc2749cc5037e81 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 18 Aug 2025 09:29:21 -0700 Subject: [PATCH 0724/1056] chore(web_common): add base styling + storybook (#5163) --- pnpm-lock.yaml | 358 +- web/common/.gitignore | 3 + web/common/.storybook/main.ts | 15 + web/common/.storybook/preview.ts | 15 + web/common/eslint.config.mjs | 4 + web/common/package-lock.json | 4884 +++++++++++++++++ web/common/package.json | 42 +- web/common/postcss.config.js | 6 + .../src/components/Badge/Badge.stories.tsx | 117 + web/common/src/components/Badge/Badge.tsx | 29 + web/common/src/components/Badge/help.ts | 29 + web/common/src/index.ts | 39 +- web/common/src/styles/design/fonts.css | 113 + .../Inter/Inter-VariableFont_slnt,wght.ttf | Bin 0 -> 803384 bytes .../src/styles/design/fonts/Inter/OFL.txt | 93 + .../src/styles/design/fonts/Inter/README.txt | 72 + .../design/fonts/Inter/static/Inter-Black.ttf | Bin 0 -> 316372 bytes .../design/fonts/Inter/static/Inter-Bold.ttf | Bin 0 -> 316100 bytes .../fonts/Inter/static/Inter-ExtraBold.ttf | Bin 0 -> 316716 bytes .../fonts/Inter/static/Inter-ExtraLight.ttf | Bin 0 -> 310808 bytes .../design/fonts/Inter/static/Inter-Light.ttf | Bin 0 -> 310420 bytes .../fonts/Inter/static/Inter-Medium.ttf | Bin 0 -> 314712 bytes .../fonts/Inter/static/Inter-Regular.ttf | Bin 0 -> 309828 bytes .../fonts/Inter/static/Inter-SemiBold.ttf | Bin 0 -> 315756 bytes .../design/fonts/Inter/static/Inter-Thin.ttf | Bin 0 -> 310516 bytes ...JetBrainsMono-Italic-VariableFont_wght.ttf | Bin 0 -> 191988 bytes .../JetBrainsMono-VariableFont_wght.ttf | Bin 0 -> 187860 bytes .../design/fonts/JetBrains_Mono/OFL.txt | 93 + .../design/fonts/JetBrains_Mono/README.txt | 79 + .../static/JetBrainsMono-Bold.ttf | Bin 0 -> 114832 bytes .../static/JetBrainsMono-BoldItalic.ttf | Bin 0 -> 117928 bytes .../static/JetBrainsMono-ExtraBold.ttf | Bin 0 -> 114804 bytes .../static/JetBrainsMono-ExtraBoldItalic.ttf | Bin 0 -> 117960 bytes .../static/JetBrainsMono-ExtraLight.ttf | Bin 0 -> 115076 bytes .../static/JetBrainsMono-ExtraLightItalic.ttf | Bin 0 -> 118236 bytes .../static/JetBrainsMono-Italic.ttf | Bin 0 -> 117948 bytes .../static/JetBrainsMono-Light.ttf | Bin 0 -> 115024 bytes .../static/JetBrainsMono-LightItalic.ttf | Bin 0 -> 118132 bytes .../static/JetBrainsMono-Medium.ttf | Bin 0 -> 114924 bytes .../static/JetBrainsMono-MediumItalic.ttf | Bin 0 -> 118052 bytes .../static/JetBrainsMono-Regular.ttf | Bin 0 -> 114908 bytes .../static/JetBrainsMono-SemiBold.ttf | Bin 0 -> 114904 bytes .../static/JetBrainsMono-SemiBoldItalic.ttf | Bin 0 -> 118040 bytes .../static/JetBrainsMono-Thin.ttf | Bin 0 -> 115000 bytes .../static/JetBrainsMono-ThinItalic.ttf | Bin 0 -> 118164 bytes web/common/src/styles/design/index.css | 22 + web/common/src/styles/design/palette.css | 199 + .../src/styles/design/semantic-colors.css | 12 + web/common/src/styles/design/space.css | 23 + web/common/src/styles/design/typography.css | 40 + web/common/src/styles/index.css | 5 + web/common/src/styles/tokens.ts | 223 + web/common/src/types/enums.ts | 43 + web/common/src/utils/index.ts | 6 + web/common/tailwind.config.js | 45 + web/common/tsconfig.build.json | 15 + web/common/vite.config.js | 27 +- 57 files changed, 6633 insertions(+), 18 deletions(-) create mode 100644 web/common/.storybook/main.ts create mode 100644 web/common/.storybook/preview.ts create mode 100644 web/common/package-lock.json create mode 100644 web/common/postcss.config.js create mode 100644 web/common/src/components/Badge/Badge.stories.tsx create mode 100644 web/common/src/components/Badge/Badge.tsx create mode 100644 web/common/src/components/Badge/help.ts create mode 100644 web/common/src/styles/design/fonts.css create mode 100644 web/common/src/styles/design/fonts/Inter/Inter-VariableFont_slnt,wght.ttf create mode 100644 web/common/src/styles/design/fonts/Inter/OFL.txt create mode 100644 web/common/src/styles/design/fonts/Inter/README.txt create mode 100644 web/common/src/styles/design/fonts/Inter/static/Inter-Black.ttf create mode 100644 web/common/src/styles/design/fonts/Inter/static/Inter-Bold.ttf create mode 100644 web/common/src/styles/design/fonts/Inter/static/Inter-ExtraBold.ttf create mode 100644 web/common/src/styles/design/fonts/Inter/static/Inter-ExtraLight.ttf create mode 100644 web/common/src/styles/design/fonts/Inter/static/Inter-Light.ttf create mode 100644 web/common/src/styles/design/fonts/Inter/static/Inter-Medium.ttf create mode 100644 web/common/src/styles/design/fonts/Inter/static/Inter-Regular.ttf create mode 100644 web/common/src/styles/design/fonts/Inter/static/Inter-SemiBold.ttf create mode 100644 web/common/src/styles/design/fonts/Inter/static/Inter-Thin.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/OFL.txt create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/README.txt create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Bold.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-BoldItalic.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-ExtraBold.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-ExtraBoldItalic.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-ExtraLight.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-ExtraLightItalic.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Italic.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Light.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-LightItalic.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Medium.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-MediumItalic.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Regular.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-SemiBold.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-SemiBoldItalic.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Thin.ttf create mode 100644 web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-ThinItalic.ttf create mode 100644 web/common/src/styles/design/index.css create mode 100644 web/common/src/styles/design/palette.css create mode 100644 web/common/src/styles/design/semantic-colors.css create mode 100644 web/common/src/styles/design/space.css create mode 100644 web/common/src/styles/design/typography.css create mode 100644 web/common/src/styles/index.css create mode 100644 web/common/src/styles/tokens.ts create mode 100644 web/common/src/types/enums.ts create mode 100644 web/common/tailwind.config.js create mode 100644 web/common/tsconfig.build.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f3655b1f8..bedd297a5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -398,17 +398,28 @@ importers: version: 1.13.2 web/common: - dependencies: - react: - specifier: ^18.3.1 - version: 18.3.1 - react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) devDependencies: '@eslint/js': specifier: ^9.31.0 version: 9.31.0 + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@storybook/addon-docs': + specifier: ^9.1.2 + version: 9.1.2(@types/react@18.3.23)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/addon-essentials': + specifier: ^9.0.0-alpha.12 + version: 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/addon-onboarding': + specifier: ^9.1.2 + version: 9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/react-vite': + specifier: ^9.1.2 + version: 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@tailwindcss/typography': + specifier: ^0.5.16 + version: 0.5.16(tailwindcss@3.4.17) '@types/react': specifier: ^18.3.23 version: 18.3.23 @@ -418,9 +429,42 @@ importers: '@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + autoprefixer: + specifier: ^10.4.21 + version: 10.4.21(postcss@8.5.6) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 eslint: specifier: ^9.31.0 version: 9.31.0(jiti@2.4.2) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.31.0(jiti@2.4.2)) + eslint-plugin-storybook: + specifier: ^9.1.2 + version: 9.1.2(eslint@9.31.0(jiti@2.4.2))(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3) + postcss: + specifier: ^8.5.6 + version: 8.5.6 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + storybook: + specifier: ^9.1.2 + version: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 typescript: specifier: ^5.8.3 version: 5.8.3 @@ -433,6 +477,9 @@ importers: vite-plugin-dts: specifier: ^4.5.4 version: 4.5.4(@types/node@24.1.0)(rollup@4.45.1)(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + vite-plugin-static-copy: + specifier: ^3.1.1 + version: 3.1.1(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) packages: @@ -1867,16 +1914,51 @@ packages: peerDependencies: storybook: ^9.0.18 + '@storybook/addon-backgrounds@9.0.0-alpha.12': + resolution: {integrity: sha512-oiQL8GIs2jNhN1cfbWa6iJIdey/WC+TFlmIeoWzYsJ79EQCxpL5JgmzCMGIkZ+p7L4MUR/5S5b5fh6ApyWcUKw==} + peerDependencies: + storybook: ^9.0.0-alpha.12 + '@storybook/addon-docs@9.0.18': resolution: {integrity: sha512-1mLhaRDx8s1JAF51o56OmwMnIsg4BOQJ8cn+4wbMjh14pDFALrovlFl/BpAXnV1VaZqHjCB4ZWuP+y5CwXEpeQ==} peerDependencies: storybook: ^9.0.18 + '@storybook/addon-docs@9.1.2': + resolution: {integrity: sha512-U3eHJ8lQFfEZ/OcgdKkUBbW2Y2tpAsHfy8lQOBgs5Pgj9biHEJcUmq+drOS/sJhle673eoBcUFmspXulI4KP1w==} + peerDependencies: + storybook: ^9.1.2 + + '@storybook/addon-essentials@9.0.0-alpha.12': + resolution: {integrity: sha512-wmUT9Q4rl6SvVgrIYDj97uHHkMSGba1A+/rMHypIw7OtrdUp+w1OKZRDNVrU0AfqfbaptT5dRrBsDr/eFZ9v8Q==} + peerDependencies: + storybook: ^9.0.0-alpha.12 + + '@storybook/addon-highlight@9.0.0-alpha.12': + resolution: {integrity: sha512-b8E1AjBaWFvBoWUfXXlAYfAIanuaHLZwJhmOcqJGtbx9RIC5uHfyGC8KHJgeyKMzvHhZD86vWBo5KUAFLFVUrg==} + peerDependencies: + storybook: ^9.0.0-alpha.12 + + '@storybook/addon-measure@9.0.0-alpha.12': + resolution: {integrity: sha512-ZtAKi/mlvVYaBMlPokvrHF94YFsyYAlz3IpKu+uz5QymN3VweSIgGsDJmAqV49lVzyVk40KWCVypi4O3L7nvdQ==} + peerDependencies: + storybook: ^9.0.0-alpha.12 + '@storybook/addon-onboarding@9.0.18': resolution: {integrity: sha512-A079BfJ3g3wYOtAuq9cPf2l6JHo+6UzEw1A2AbSNBBNP4hKfXpHcLadIVwuyOxuKjDUWzY5f4dJa3hCMurHXGQ==} peerDependencies: storybook: ^9.0.18 + '@storybook/addon-onboarding@9.1.2': + resolution: {integrity: sha512-WfYIBmRtwUF13Hcu6BdsqATsAuBK0dwsz7O4tL0FGrIwY/vdzZ5jNzYvzzgilzlu9QiPvzEIBvs6X4BVulN3LQ==} + peerDependencies: + storybook: ^9.1.2 + + '@storybook/addon-outline@9.0.0-alpha.12': + resolution: {integrity: sha512-I7opVIK8bNUYSC+P+b8AwP6sE2pFyXH5F0gz8WA0pdkRcxerQmYnhlsXrI5T0QMu79tZnjVNrQTUrqpy/Z5oqQ==} + peerDependencies: + storybook: ^9.0.0-alpha.12 + '@storybook/addon-vitest@9.0.18': resolution: {integrity: sha512-uPLh9H7kRho+raxyIBCm8Ymd3j0VPuWIQ1HSAkdx8itmNafNqs4HE67Z8Cfl259YzdWU/j5BhZqoiT62BCbIDw==} peerDependencies: @@ -1898,11 +1980,22 @@ packages: storybook: ^9.0.18 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + '@storybook/builder-vite@9.1.2': + resolution: {integrity: sha512-5Y7e5wnSzFxCGP63UNRRZVoxHe1znU4dYXazJBobAlEcUPBk7A0sH2716tA6bS4oz92oG9tgvn1g996hRrw4ow==} + peerDependencies: + storybook: ^9.1.2 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + '@storybook/csf-plugin@9.0.18': resolution: {integrity: sha512-MQ3WwXnMua5sX0uYyuO7dC5WOWuJCLqf8CsOn3zQ2ptNoH6hD7DFx5ZOa1uD6VxIuJ3LkA+YqfSRBncomJoRnA==} peerDependencies: storybook: ^9.0.18 + '@storybook/csf-plugin@9.1.2': + resolution: {integrity: sha512-bfMh6r+RieBLPWtqqYN70le2uTE4JzOYPMYSCagHykUti3uM/1vRFaZNkZtUsRy5GwEzE5jLdDXioG1lOEeT2Q==} + peerDependencies: + storybook: ^9.1.2 + '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} @@ -1920,6 +2013,13 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta storybook: ^9.0.18 + '@storybook/react-dom-shim@9.1.2': + resolution: {integrity: sha512-nw7BLAHCJswPZGsuL0Gs2AvFUWriusCTgPBmcHppSw/AqvT4XRFRDE+5q3j04/XKuZBrAA2sC4L+HuC0uzEChQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.1.2 + '@storybook/react-vite@9.0.18': resolution: {integrity: sha512-dHzUoeY0/S35TvSYxCkPuBlNQZx4Zj9QDhAZ0qdv+nSll++uPgqSe2y2vF+2p+XVYhjDn+YX5LORv00YtuQezg==} engines: {node: '>=20.0.0'} @@ -1929,6 +2029,15 @@ packages: storybook: ^9.0.18 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + '@storybook/react-vite@9.1.2': + resolution: {integrity: sha512-dv3CBjOzmMoSyIotMtdmsBRjB25i19OjFP0IZqauLeUoVm6QddILW7JRcZVLrzhATyBEn+sEAdWQ4j79Z11HAg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.1.2 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + '@storybook/react@9.0.18': resolution: {integrity: sha512-CCH6Vj/O6I07PrhCHxc1pvCWYMfZhRzK7CVHAtrBP9xxnYA7OoXhM2wymuDogml5HW1BKtyVMeQ3oWZXFNgDXQ==} engines: {node: '>=20.0.0'} @@ -1941,6 +2050,18 @@ packages: typescript: optional: true + '@storybook/react@9.1.2': + resolution: {integrity: sha512-VVXu1HrhDExj/yj+heFYc8cgIzBruXy1UYT3LW0WiJyadgzYz3J41l/Lf/j2FCppyxwlXb19Uv51plb1F1C77w==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.1.2 + typescript: '>= 4.9.x' + peerDependenciesMeta: + typescript: + optional: true + '@swc/core-darwin-arm64@1.13.2': resolution: {integrity: sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==} engines: {node: '>=10'} @@ -2112,6 +2233,11 @@ packages: '@tailwindcss/postcss@4.1.11': resolution: {integrity: sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==} + '@tailwindcss/typography@0.5.16': + resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.1.11': resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==} peerDependencies: @@ -3198,6 +3324,9 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + classcat@5.0.5: resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} @@ -3614,6 +3743,19 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-storybook@9.1.2: + resolution: {integrity: sha512-EQa/kChrYrekxv36q3pvW57anqxMlAP4EdPXEDyA/EDrCQJaaTbWEdsMnVZtD744RjPP0M5wzaUjHbMhNooAwQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + eslint: '>=8' + storybook: ^9.1.2 + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -4499,6 +4641,9 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -4603,6 +4748,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + map-or-similar@1.5.0: + resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} + markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true @@ -4638,6 +4786,9 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + memoizerific@1.11.3: + resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -5130,6 +5281,10 @@ packages: peerDependencies: postcss: ^8.2.14 + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} @@ -5642,6 +5797,15 @@ packages: prettier: optional: true + storybook@9.1.2: + resolution: {integrity: sha512-TYcq7WmgfVCAQge/KueGkVlM/+g33sQcmbATlC3X6y/g2FEeSSLGrb6E6d3iemht8oio+aY6ld3YOdAnMwx45Q==} + hasBin: true + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -5769,6 +5933,9 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwindcss@3.4.17: resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} @@ -6178,6 +6345,12 @@ packages: vite: optional: true + vite-plugin-static-copy@3.1.1: + resolution: {integrity: sha512-oR53SkL5cX4KT1t18E/xU50vJDo0N8oaHza4EMk0Fm+2/u6nQivxavOfrDk3udWj+dizRizB/QnBvJOOQrTTAQ==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vite@6.3.5: resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -8220,6 +8393,13 @@ snapshots: axe-core: 4.10.3 storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + '@storybook/addon-backgrounds@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + '@storybook/global': 5.0.0 + memoizerific: 1.11.3 + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + ts-dedent: 2.2.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))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.23)(react@18.3.1) @@ -8233,10 +8413,53 @@ snapshots: transitivePeerDependencies: - '@types/react' + '@storybook/addon-docs@9.1.2(@types/react@18.3.23)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + '@mdx-js/react': 3.1.0(@types/react@18.3.23)(react@18.3.1) + '@storybook/csf-plugin': 9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + + '@storybook/addon-essentials@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + '@storybook/addon-backgrounds': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/addon-highlight': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/addon-measure': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/addon-outline': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + ts-dedent: 2.2.0 + + '@storybook/addon-highlight@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + + '@storybook/addon-measure@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + tiny-invariant: 1.3.3 + '@storybook/addon-onboarding@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': dependencies: storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) + '@storybook/addon-onboarding@9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + + '@storybook/addon-outline@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + ts-dedent: 2.2.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)': dependencies: '@storybook/global': 5.0.0 @@ -8259,11 +8482,23 @@ snapshots: ts-dedent: 2.2.0 vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + '@storybook/builder-vite@9.1.2(storybook@9.1.2(@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))': + dependencies: + '@storybook/csf-plugin': 9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0) + '@storybook/csf-plugin@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': dependencies: storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) unplugin: 1.16.1 + '@storybook/csf-plugin@9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + unplugin: 1.16.1 + '@storybook/global@5.0.0': {} '@storybook/icons@1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -8277,6 +8512,12 @@ snapshots: react-dom: 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.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 9.1.2(@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.43.1)(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))(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0)) @@ -8297,6 +8538,26 @@ snapshots: - supports-color - typescript + '@storybook/react-vite@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@rollup/pluginutils': 5.2.0(rollup@4.45.1) + '@storybook/builder-vite': 9.1.2(storybook@9.1.2(@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@storybook/react': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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-dom: 18.3.1(react@18.3.1) + resolve: 1.22.10 + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.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)': dependencies: '@storybook/global': 5.0.0 @@ -8307,6 +8568,16 @@ snapshots: optionalDependencies: typescript: 5.8.3 + '@storybook/react@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + optionalDependencies: + typescript: 5.8.3 + '@swc/core-darwin-arm64@1.13.2': optional: true @@ -8440,6 +8711,14 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.11 + '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17)': + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + 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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@tailwindcss/node': 4.1.11 @@ -9785,6 +10064,10 @@ snapshots: chrome-trace-event@1.0.4: {} + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + classcat@5.0.5: {} cli-cursor@5.0.0: @@ -10256,6 +10539,19 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-plugin-react-hooks@5.2.0(eslint@9.31.0(jiti@2.4.2)): + dependencies: + eslint: 9.31.0(jiti@2.4.2) + + eslint-plugin-storybook@9.1.2(eslint@9.31.0(jiti@2.4.2))(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + transitivePeerDependencies: + - supports-color + - typescript + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -11162,6 +11458,8 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.castarray@4.4.0: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} @@ -11246,6 +11544,8 @@ snapshots: dependencies: semver: 7.7.2 + map-or-similar@1.5.0: {} + markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -11348,6 +11648,10 @@ snapshots: mdurl@2.0.0: {} + memoizerific@1.11.3: + dependencies: + map-or-similar: 1.5.0 + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -11946,6 +12250,11 @@ snapshots: postcss: 8.5.6 postcss-selector-parser: 6.1.2 + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 @@ -12584,6 +12893,30 @@ snapshots: - supports-color - utf-8-validate + storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/spy': 3.2.4 + better-opn: 3.0.2 + esbuild: 0.25.8 + esbuild-register: 3.6.0(esbuild@0.25.8) + recast: 0.23.11 + semver: 7.7.2 + ws: 8.18.3 + optionalDependencies: + prettier: 3.6.2 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - msw + - supports-color + - utf-8-validate + - vite + string-argv@0.3.2: {} string-width@4.2.3: @@ -12744,6 +13077,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + tailwind-merge@3.3.1: {} + tailwindcss@3.4.17: dependencies: '@alloc/quick-lru': 5.2.0 @@ -13231,6 +13566,15 @@ snapshots: - rollup - supports-color + vite-plugin-static-copy@3.1.1(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.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@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.8 diff --git a/web/common/.gitignore b/web/common/.gitignore index 86ff358d54..392d3f0ae6 100644 --- a/web/common/.gitignore +++ b/web/common/.gitignore @@ -1,3 +1,6 @@ node_modules/ dist tsconfig.tsbuildinfo + +*storybook.log +storybook-static diff --git a/web/common/.storybook/main.ts b/web/common/.storybook/main.ts new file mode 100644 index 0000000000..8fe508f79a --- /dev/null +++ b/web/common/.storybook/main.ts @@ -0,0 +1,15 @@ +import type { StorybookConfig } from '@storybook/react-vite' + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + '@storybook/addon-essentials', + '@storybook/addon-docs', + '@storybook/addon-onboarding', + ], + framework: { + name: '@storybook/react-vite', + options: {}, + }, +} +export default config diff --git a/web/common/.storybook/preview.ts b/web/common/.storybook/preview.ts new file mode 100644 index 0000000000..bfb259c320 --- /dev/null +++ b/web/common/.storybook/preview.ts @@ -0,0 +1,15 @@ +import type { Preview } from '@storybook/react-vite' +import '../src/styles/index.css' + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +} + +export default preview diff --git a/web/common/eslint.config.mjs b/web/common/eslint.config.mjs index cd34adf5c8..ce96c1ba4c 100644 --- a/web/common/eslint.config.mjs +++ b/web/common/eslint.config.mjs @@ -1,3 +1,6 @@ +// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format +import storybook from 'eslint-plugin-storybook' + import eslint from '@eslint/js' import tseslint from 'typescript-eslint' @@ -13,4 +16,5 @@ export default tseslint.config( 'no-empty': 'off', }, }, + storybook.configs['flat/recommended'], ) diff --git a/web/common/package-lock.json b/web/common/package-lock.json new file mode 100644 index 0000000000..abfcad57d6 --- /dev/null +++ b/web/common/package-lock.json @@ -0,0 +1,4884 @@ +{ + "name": "@tobikodata/sqlmesh-common", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@tobikodata/sqlmesh-common", + "version": "0.0.1", + "devDependencies": { + "@eslint/js": "^9.31.0", + "@radix-ui/react-slot": "^1.2.3", + "@storybook/addon-docs": "^9.1.2", + "@storybook/addon-essentials": "^9.0.0-alpha.12", + "@storybook/addon-onboarding": "^9.1.2", + "@storybook/react-vite": "^9.1.2", + "@tailwindcss/typography": "^0.5.16", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.4.21", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "eslint": "^9.31.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-storybook": "^9.1.2", + "postcss": "^8.5.6", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "storybook": "^9.1.2", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", + "vite": "^6.3.5", + "vite-plugin-dts": "^4.5.4", + "vite-plugin-static-copy": "^3.1.1" + }, + "peerDependencies": { + "@radix-ui/react-slot": "^1.0.0", + "@tailwindcss/typography": "^0.5.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "react": "^18.3.1 || ^19.0.0", + "react-dom": "^18.3.1 || ^19.0.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^3.4.0" + } + }, + "../../node_modules/.pnpm/@eslint+js@9.31.0/node_modules/@eslint/js": { + "version": "9.31.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "../../node_modules/.pnpm/@types+react-dom@18.3.7_@types+react@18.3.23/node_modules/@types/react-dom": { + "version": "18.3.7", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "../../node_modules/.pnpm/@types+react@18.3.23/node_modules/@types/react": { + "version": "18.3.23", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "../../node_modules/.pnpm/@vitejs+plugin-react@4.7.0_vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terse_p5zuafkpgv2vlm3nhxz3zj4hsu/node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "devDependencies": { + "@vitejs/react-common": "workspace:*", + "babel-plugin-react-compiler": "19.1.0-rc.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "rolldown": "1.0.0-beta.27", + "tsdown": "^0.12.9", + "vitest": "^3.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "../../node_modules/.pnpm/eslint@9.31.0_jiti@2.4.2/node_modules/eslint": { + "version": "9.31.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "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.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.18.0", + "@babel/core": "^7.4.3", + "@babel/preset-env": "^7.4.3", + "@cypress/webpack-preprocessor": "^6.0.2", + "@eslint/json": "^0.13.0", + "@trunkio/launcher": "^1.3.4", + "@types/esquery": "^1.5.4", + "@types/node": "^22.13.14", + "@typescript-eslint/parser": "^8.4.0", + "babel-loader": "^8.0.5", + "c8": "^7.12.0", + "chai": "^4.0.1", + "cheerio": "^0.22.0", + "common-tags": "^1.8.0", + "core-js": "^3.1.3", + "cypress": "^14.1.0", + "ejs": "^3.0.2", + "eslint": "file:.", + "eslint-config-eslint": "file:packages/eslint-config-eslint", + "eslint-plugin-eslint-plugin": "^6.0.0", + "eslint-plugin-expect-type": "^0.6.0", + "eslint-plugin-yml": "^1.14.0", + "eslint-release": "^3.3.0", + "eslint-rule-composer": "^0.3.0", + "eslump": "^3.0.0", + "esprima": "^4.0.1", + "fast-glob": "^3.2.11", + "fs-teardown": "^0.1.3", + "glob": "^10.0.0", + "globals": "^16.2.0", + "got": "^11.8.3", + "gray-matter": "^4.0.3", + "jiti": "^2.2.0", + "jiti-v2.0": "npm:jiti@2.0.x", + "jiti-v2.1": "npm:jiti@2.1.x", + "knip": "^5.60.2", + "lint-staged": "^11.0.0", + "load-perf": "^0.2.0", + "markdown-it": "^12.2.0", + "markdown-it-container": "^3.0.0", + "marked": "^4.0.8", + "metascraper": "^5.25.7", + "metascraper-description": "^5.25.7", + "metascraper-image": "^5.29.3", + "metascraper-logo": "^5.25.7", + "metascraper-logo-favicon": "^5.25.7", + "metascraper-title": "^5.25.7", + "mocha": "^11.7.1", + "node-polyfill-webpack-plugin": "^1.0.3", + "npm-license": "^0.3.3", + "pirates": "^4.0.5", + "progress": "^2.0.3", + "proxyquire": "^2.0.1", + "recast": "^0.23.0", + "regenerator-runtime": "^0.14.0", + "semver": "^7.5.3", + "shelljs": "^0.10.0", + "sinon": "^11.0.0", + "typescript": "^5.3.3", + "webpack": "^5.23.0", + "webpack-cli": "^4.5.0", + "yorkie": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "../../node_modules/.pnpm/typescript-eslint@8.38.0_eslint@9.31.0_jiti@2.4.2__typescript@5.8.3/node_modules/typescript-eslint": { + "version": "8.38.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0" + }, + "devDependencies": { + "@vitest/coverage-v8": "^3.1.3", + "eslint": "*", + "rimraf": "*", + "typescript": "*", + "vitest": "^3.1.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript": { + "version": "5.8.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "devDependencies": { + "@dprint/formatter": "^0.4.1", + "@dprint/typescript": "0.93.3", + "@esfx/canceltoken": "^1.0.0", + "@eslint/js": "^9.17.0", + "@octokit/rest": "^21.0.2", + "@types/chai": "^4.3.20", + "@types/diff": "^5.2.3", + "@types/minimist": "^1.2.5", + "@types/mocha": "^10.0.10", + "@types/ms": "^0.7.34", + "@types/node": "latest", + "@types/source-map-support": "^0.5.10", + "@types/which": "^3.0.4", + "@typescript-eslint/rule-tester": "^8.18.1", + "@typescript-eslint/type-utils": "^8.18.1", + "@typescript-eslint/utils": "^8.18.1", + "azure-devops-node-api": "^14.1.0", + "c8": "^10.1.3", + "chai": "^4.5.0", + "chalk": "^4.1.2", + "chokidar": "^3.6.0", + "diff": "^5.2.0", + "dprint": "^0.47.6", + "esbuild": "^0.24.0", + "eslint": "^9.17.0", + "eslint-formatter-autolinkable-stylish": "^1.4.0", + "eslint-plugin-regexp": "^2.7.0", + "fast-xml-parser": "^4.5.1", + "glob": "^10.4.5", + "globals": "^15.13.0", + "hereby": "^1.10.0", + "jsonc-parser": "^3.3.1", + "knip": "^5.41.0", + "minimist": "^1.2.8", + "mocha": "^10.8.2", + "mocha-fivemat-progress-reporter": "^0.1.0", + "monocart-coverage-reports": "^2.11.4", + "ms": "^2.1.3", + "playwright": "^1.49.1", + "source-map-support": "^0.5.21", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.1", + "which": "^3.0.1" + }, + "engines": { + "node": ">=14.17" + } + }, + "../../node_modules/.pnpm/vite-plugin-dts@4.5.4_@types+node@24.1.0_rollup@4.45.1_typescript@5.8.3_vite@6.3.5_@types+nod_ddgp24sr5pf6ze3b5hs7mrzr5e/node_modules/vite-plugin-dts": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor": "^7.50.1", + "@rollup/pluginutils": "^5.1.4", + "@volar/typescript": "^2.4.11", + "@vue/language-core": "2.2.0", + "compare-versions": "^6.1.1", + "debug": "^4.4.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17" + }, + "devDependencies": { + "@commitlint/cli": "^19.7.1", + "@types/debug": "^4.1.12", + "@types/minimist": "^1.2.5", + "@types/node": "^22.13.5", + "@types/prompts": "^2.4.9", + "@types/semver": "^7.5.8", + "@vexip-ui/commitlint-config": "^0.5.0", + "@vexip-ui/eslint-config": "^0.12.1", + "@vexip-ui/prettier-config": "^1.0.0", + "@vexip-ui/scripts": "^1.2.0", + "@vue/eslint-config-standard": "^8.0.1", + "@vue/eslint-config-typescript": "^13.0.0", + "conventional-changelog-cli": "^5.0.0", + "eslint": "^8.57.0", + "execa": "^9.5.2", + "husky": "^9.1.7", + "is-ci": "^4.1.0", + "lint-staged": "^15.4.3", + "minimist": "^1.2.8", + "pinst": "^3.0.0", + "prettier": "^3.5.2", + "pretty-quick": "^4.0.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.7.1", + "tsx": "^4.19.3", + "typescript": "5.7.3", + "unbuild": "^3.3.1", + "vite": "^6.2.0", + "vitest": "^3.0.7" + }, + "peerDependencies": { + "typescript": "*", + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "../../node_modules/.pnpm/vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terser@5.43.1_tsx@4.20.3_yaml@2.8.0/node_modules/vite": { + "version": "6.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "devDependencies": { + "@ampproject/remapping": "^2.3.0", + "@babel/parser": "^7.27.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@polka/compression": "^1.0.0-next.25", + "@rollup/plugin-alias": "^5.1.1", + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-dynamic-import-vars": "2.1.4", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "16.0.1", + "@rollup/pluginutils": "^5.1.4", + "@types/escape-html": "^1.0.4", + "@types/pnpapi": "^0.0.5", + "artichokie": "^0.3.1", + "cac": "^6.7.14", + "chokidar": "^3.6.0", + "connect": "^3.7.0", + "convert-source-map": "^2.0.0", + "cors": "^2.8.5", + "cross-spawn": "^7.0.6", + "debug": "^4.4.0", + "dep-types": "link:./src/types", + "dotenv": "^16.5.0", + "dotenv-expand": "^12.0.2", + "es-module-lexer": "^1.6.0", + "escape-html": "^1.0.3", + "estree-walker": "^3.0.3", + "etag": "^1.8.1", + "http-proxy": "^1.18.1", + "launch-editor-middleware": "^2.10.0", + "lightningcss": "^1.29.3", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "mrmime": "^2.0.1", + "nanoid": "^5.1.5", + "open": "^10.1.1", + "parse5": "^7.2.1", + "pathe": "^2.0.3", + "periscopic": "^4.0.2", + "picocolors": "^1.1.1", + "postcss-import": "^16.1.0", + "postcss-load-config": "^6.0.1", + "postcss-modules": "^6.0.1", + "resolve.exports": "^2.0.3", + "rollup-plugin-dts": "^6.2.1", + "rollup-plugin-esbuild": "^6.2.1", + "rollup-plugin-license": "^3.6.0", + "sass": "^1.86.3", + "sass-embedded": "^1.86.3", + "sirv": "^3.0.1", + "source-map-support": "^0.5.21", + "strip-literal": "^3.0.0", + "terser": "^5.39.0", + "tsconfck": "^3.1.5", + "tslib": "^2.8.1", + "types": "link:./types", + "ufo": "^1.6.1", + "ws": "^8.18.1" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint/js": { + "resolved": "../../node_modules/.pnpm/@eslint+js@9.31.0/node_modules/@eslint/js", + "link": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.1.tgz", + "integrity": "sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "magic-string": "^0.30.0", + "react-docgen-typescript": "^2.2.2" + }, + "peerDependencies": { + "typescript": ">= 4.3.x", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", + "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@storybook/addon-backgrounds": { + "version": "9.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-9.0.0-alpha.12.tgz", + "integrity": "sha512-oiQL8GIs2jNhN1cfbWa6iJIdey/WC+TFlmIeoWzYsJ79EQCxpL5JgmzCMGIkZ+p7L4MUR/5S5b5fh6ApyWcUKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "memoizerific": "^1.11.3", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.0.0-alpha.12" + } + }, + "node_modules/@storybook/addon-docs": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.2.tgz", + "integrity": "sha512-U3eHJ8lQFfEZ/OcgdKkUBbW2Y2tpAsHfy8lQOBgs5Pgj9biHEJcUmq+drOS/sJhle673eoBcUFmspXulI4KP1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdx-js/react": "^3.0.0", + "@storybook/csf-plugin": "9.1.2", + "@storybook/icons": "^1.4.0", + "@storybook/react-dom-shim": "9.1.2", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.2" + } + }, + "node_modules/@storybook/addon-essentials": { + "version": "9.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-9.0.0-alpha.12.tgz", + "integrity": "sha512-wmUT9Q4rl6SvVgrIYDj97uHHkMSGba1A+/rMHypIw7OtrdUp+w1OKZRDNVrU0AfqfbaptT5dRrBsDr/eFZ9v8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/addon-backgrounds": "9.0.0-alpha.12", + "@storybook/addon-highlight": "9.0.0-alpha.12", + "@storybook/addon-measure": "9.0.0-alpha.12", + "@storybook/addon-outline": "9.0.0-alpha.12", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.0.0-alpha.12" + } + }, + "node_modules/@storybook/addon-highlight": { + "version": "9.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-9.0.0-alpha.12.tgz", + "integrity": "sha512-b8E1AjBaWFvBoWUfXXlAYfAIanuaHLZwJhmOcqJGtbx9RIC5uHfyGC8KHJgeyKMzvHhZD86vWBo5KUAFLFVUrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.0.0-alpha.12" + } + }, + "node_modules/@storybook/addon-measure": { + "version": "9.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-9.0.0-alpha.12.tgz", + "integrity": "sha512-ZtAKi/mlvVYaBMlPokvrHF94YFsyYAlz3IpKu+uz5QymN3VweSIgGsDJmAqV49lVzyVk40KWCVypi4O3L7nvdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.0.0-alpha.12" + } + }, + "node_modules/@storybook/addon-onboarding": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-9.1.2.tgz", + "integrity": "sha512-WfYIBmRtwUF13Hcu6BdsqATsAuBK0dwsz7O4tL0FGrIwY/vdzZ5jNzYvzzgilzlu9QiPvzEIBvs6X4BVulN3LQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.2" + } + }, + "node_modules/@storybook/addon-outline": { + "version": "9.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-9.0.0-alpha.12.tgz", + "integrity": "sha512-I7opVIK8bNUYSC+P+b8AwP6sE2pFyXH5F0gz8WA0pdkRcxerQmYnhlsXrI5T0QMu79tZnjVNrQTUrqpy/Z5oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.0.0-alpha.12" + } + }, + "node_modules/@storybook/builder-vite": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.2.tgz", + "integrity": "sha512-5Y7e5wnSzFxCGP63UNRRZVoxHe1znU4dYXazJBobAlEcUPBk7A0sH2716tA6bS4oz92oG9tgvn1g996hRrw4ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf-plugin": "9.1.2", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.2", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@storybook/csf-plugin": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.2.tgz", + "integrity": "sha512-bfMh6r+RieBLPWtqqYN70le2uTE4JzOYPMYSCagHykUti3uM/1vRFaZNkZtUsRy5GwEzE5jLdDXioG1lOEeT2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.2" + } + }, + "node_modules/@storybook/global": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/icons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.4.0.tgz", + "integrity": "sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" + } + }, + "node_modules/@storybook/react": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.2.tgz", + "integrity": "sha512-VVXu1HrhDExj/yj+heFYc8cgIzBruXy1UYT3LW0WiJyadgzYz3J41l/Lf/j2FCppyxwlXb19Uv51plb1F1C77w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/react-dom-shim": "9.1.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.1.2", + "typescript": ">= 4.9.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react-dom-shim": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.2.tgz", + "integrity": "sha512-nw7BLAHCJswPZGsuL0Gs2AvFUWriusCTgPBmcHppSw/AqvT4XRFRDE+5q3j04/XKuZBrAA2sC4L+HuC0uzEChQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.1.2" + } + }, + "node_modules/@storybook/react-vite": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.2.tgz", + "integrity": "sha512-dv3CBjOzmMoSyIotMtdmsBRjB25i19OjFP0IZqauLeUoVm6QddILW7JRcZVLrzhATyBEn+sEAdWQ4j79Z11HAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", + "@rollup/pluginutils": "^5.0.2", + "@storybook/builder-vite": "9.1.2", + "@storybook/react": "9.1.2", + "find-up": "^7.0.0", + "magic-string": "^0.30.0", + "react-docgen": "^8.0.0", + "resolve": "^1.22.8", + "tsconfig-paths": "^4.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.1.2", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz", + "integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "resolved": "../../node_modules/.pnpm/@types+react@18.3.23/node_modules/@types/react", + "link": true + }, + "node_modules/@types/react-dom": { + "resolved": "../../node_modules/.pnpm/@types+react-dom@18.3.7_@types+react@18.3.23/node_modules/@types/react-dom", + "link": true + }, + "node_modules/@types/resolve": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", + "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.39.1", + "@typescript-eslint/types": "^8.39.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", + "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", + "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", + "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", + "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.39.1", + "@typescript-eslint/tsconfig-utils": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", + "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", + "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "resolved": "../../node_modules/.pnpm/@vitejs+plugin-react@4.7.0_vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terse_p5zuafkpgv2vlm3nhxz3zj4hsu/node_modules/@vitejs/plugin-react", + "link": true + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/better-opn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", + "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.201", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.201.tgz", + "integrity": "sha512-ZG65vsrLClodGqywuigc+7m0gr4ISoTQttfVh7nfpLv0M7SIwF4WbFNEOywcqTiujs12AUeeXbFyQieDICAIxg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint": { + "resolved": "../../node_modules/.pnpm/eslint@9.31.0_jiti@2.4.2/node_modules/eslint", + "link": true + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-storybook": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.2.tgz", + "integrity": "sha512-EQa/kChrYrekxv36q3pvW57anqxMlAP4EdPXEDyA/EDrCQJaaTbWEdsMnVZtD744RjPP0M5wzaUjHbMhNooAwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "eslint": ">=8", + "storybook": "^9.1.2" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/map-or-similar": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", + "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", + "dev": true, + "license": "MIT" + }, + "node_modules/memoizerific": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", + "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", + "dev": true, + "license": "MIT", + "dependencies": { + "map-or-similar": "^1.5.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-docgen": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.0.tgz", + "integrity": "sha512-kmob/FOTwep7DUWf9KjuenKX0vyvChr3oTdvvPt09V60Iz75FJp+T/0ZeHMbAfJj2WaVWqAPP5Hmm3PYzSPPKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.18.9", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9", + "@types/babel__core": "^7.18.0", + "@types/babel__traverse": "^7.18.0", + "@types/doctrine": "^0.0.9", + "@types/resolve": "^1.20.2", + "doctrine": "^3.0.0", + "resolve": "^1.22.1", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": "^20.9.0 || >=22" + } + }, + "node_modules/react-docgen-typescript": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", + "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 4.3.x" + } + }, + "node_modules/react-docgen/node_modules/strip-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/storybook": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.2.tgz", + "integrity": "sha512-TYcq7WmgfVCAQge/KueGkVlM/+g33sQcmbATlC3X6y/g2FEeSSLGrb6E6d3iemht8oio+aY6ld3YOdAnMwx45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/user-event": "^14.6.1", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/spy": "3.2.4", + "better-opn": "^3.0.2", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", + "esbuild-register": "^3.5.0", + "recast": "^0.23.5", + "semver": "^7.6.2", + "ws": "^8.18.0" + }, + "bin": { + "storybook": "bin/index.cjs" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "resolved": "../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript", + "link": true + }, + "node_modules/typescript-eslint": { + "resolved": "../../node_modules/.pnpm/typescript-eslint@8.38.0_eslint@9.31.0_jiti@2.4.2__typescript@5.8.3/node_modules/typescript-eslint", + "link": true + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "resolved": "../../node_modules/.pnpm/vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terser@5.43.1_tsx@4.20.3_yaml@2.8.0/node_modules/vite", + "link": true + }, + "node_modules/vite-plugin-dts": { + "resolved": "../../node_modules/.pnpm/vite-plugin-dts@4.5.4_@types+node@24.1.0_rollup@4.45.1_typescript@5.8.3_vite@6.3.5_@types+nod_ddgp24sr5pf6ze3b5hs7mrzr5e/node_modules/vite-plugin-dts", + "link": true + }, + "node_modules/vite-plugin-static-copy": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.1.tgz", + "integrity": "sha512-oR53SkL5cX4KT1t18E/xU50vJDo0N8oaHza4EMk0Fm+2/u6nQivxavOfrDk3udWj+dizRizB/QnBvJOOQrTTAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "fs-extra": "^11.3.0", + "p-map": "^7.0.3", + "picocolors": "^1.1.1", + "tinyglobby": "^0.2.14" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/web/common/package.json b/web/common/package.json index 2fd8495a47..47636d5525 100644 --- a/web/common/package.json +++ b/web/common/package.json @@ -23,31 +23,59 @@ "types": "./dist/index.d.ts", "default": "./dist/sqlmesh-common.umd.js" } - } + }, + "./design": "./dist/styles/design/index.css", + "./design/*": "./dist/styles/design/*" }, "scripts": { "ci": "pnpm run lint && pnpm run build", - "build": "tsc -b && vite build --base './'", + "build": "tsc -p tsconfig.build.json && vite build --base './'", "build:storybook": "storybook build", "dev": "storybook dev -p 6006", "lint": "eslint --max-warnings 0 --fix src", "test": "vitest", "test:watch": "vitest watch", - "test:ui": "vitest --ui" + "test:ui": "vitest --ui", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "devDependencies": { "@eslint/js": "^9.31.0", - "eslint": "^9.31.0", + "@radix-ui/react-slot": "^1.2.3", + "@storybook/addon-docs": "^9.1.2", + "@storybook/addon-essentials": "^9.0.0-alpha.12", + "@storybook/addon-onboarding": "^9.1.2", + "@storybook/react-vite": "^9.1.2", + "@tailwindcss/typography": "^0.5.16", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.4.21", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "eslint": "^9.31.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-storybook": "^9.1.2", + "postcss": "^8.5.6", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "storybook": "^9.1.2", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^3.4.17", "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", "vite": "^6.3.5", "vite-plugin-dts": "^4.5.4", - "typescript-eslint": "^8.38.0" + "vite-plugin-static-copy": "^3.1.1" }, "peerDependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" + "@radix-ui/react-slot": "^1.0.0", + "@tailwindcss/typography": "^0.5.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "react": "^18.3.1 || ^19.0.0", + "react-dom": "^18.3.1 || ^19.0.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^3.4.0" } } diff --git a/web/common/postcss.config.js b/web/common/postcss.config.js new file mode 100644 index 0000000000..2e7af2b7f1 --- /dev/null +++ b/web/common/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/web/common/src/components/Badge/Badge.stories.tsx b/web/common/src/components/Badge/Badge.stories.tsx new file mode 100644 index 0000000000..aec5bd0bca --- /dev/null +++ b/web/common/src/components/Badge/Badge.stories.tsx @@ -0,0 +1,117 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { EnumShape, EnumSize } from '@/types/enums' +import { Badge } from './Badge' + +const meta: Meta = { + title: 'Components/Badge', + component: Badge, + tags: ['autodocs'], + argTypes: { + size: { + control: { type: 'select' }, + options: Object.values(EnumSize), + }, + shape: { + control: { type: 'select' }, + options: Object.values(EnumShape), + }, + children: { control: 'text' }, + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + children: 'Default Badge', + }, +} + +export const Sizes: Story = { + render: args => ( +
    + {Object.values(EnumSize).map(size => ( + + {size} Badge + + ))} +
    + ), + args: { + children: undefined, + }, +} + +export const Shapes: Story = { + render: args => ( +
    + {Object.values(EnumShape).map(shape => ( + + {shape} Badge + + ))} +
    + ), + args: { + children: undefined, + }, +} + +export const Colors: Story = { + render: args => ( +
    + + Primary Badge + + + Secondary Badge + + + Failed Badge + + + Success Badge + +
    + ), + args: { + children: undefined, + }, +} diff --git a/web/common/src/components/Badge/Badge.tsx b/web/common/src/components/Badge/Badge.tsx new file mode 100644 index 0000000000..9ba338e245 --- /dev/null +++ b/web/common/src/components/Badge/Badge.tsx @@ -0,0 +1,29 @@ +import { Slot } from '@radix-ui/react-slot' +import { type VariantProps } from 'class-variance-authority' +import React from 'react' + +import { type Size, type Shape } from '@/types/enums' +import { cn } from '@/utils' +import { badgeVariants } from './help' + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps { + asChild?: boolean + size?: Size + shape?: Shape +} + +export const Badge = React.forwardRef( + ({ className, size, shape, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'span' + return ( + + ) + }, +) +Badge.displayName = 'Badge' diff --git a/web/common/src/components/Badge/help.ts b/web/common/src/components/Badge/help.ts new file mode 100644 index 0000000000..df489eb8d2 --- /dev/null +++ b/web/common/src/components/Badge/help.ts @@ -0,0 +1,29 @@ +import { cva } from 'class-variance-authority' + +import { EnumShape, EnumSize } from '@/types/enums' + +export const badgeVariants = cva( + 'bg-badge-background text-badge-foreground font-mono inline-flex align-middle items-center justify-center gap-2 leading-none whitespace-nowrap font-semibold', + { + variants: { + size: { + [EnumSize.XXS]: 'h-5 px-2 text-2xs leading-none rounded-2xs', + [EnumSize.XS]: 'h-6 px-2 text-2xs rounded-xs', + [EnumSize.S]: 'h-7 px-3 text-xs rounded-sm', + [EnumSize.M]: 'h-8 px-4 rounded-md', + [EnumSize.L]: 'h-9 px-4 rounded-lg', + [EnumSize.XL]: 'h-10 px-4 rounded-xl', + [EnumSize.XXL]: 'h-11 px-6 rounded-2xl', + }, + shape: { + [EnumShape.Square]: 'rounded-none', + [EnumShape.Round]: 'rounded-inherit', + [EnumShape.Pill]: 'rounded-full', + }, + }, + defaultVariants: { + size: EnumSize.S, + shape: EnumShape.Round, + }, + }, +) diff --git a/web/common/src/index.ts b/web/common/src/index.ts index f486364817..309e993504 100644 --- a/web/common/src/index.ts +++ b/web/common/src/index.ts @@ -1,2 +1,37 @@ -export * from '@/utils' -export * from '@/types' +// Components +export { Badge, type BadgeProps } from '@/components/Badge/Badge' + +// Utils +export { cn, isNil, notNil } from '@/utils' + +// Types +export type { Nil, Optional, Maybe } from '@/types' +export { + EnumSize, + EnumHeadlineLevel, + EnumSide, + EnumLayoutDirection, + EnumShape, + type Size, + type HeadlineLevel, + type Side, + type LayoutDirection, + type Shape, +} from '@/types/enums' + +// Design Tokens +export { + colorToken, + spacingToken, + textSizeToken, + type ColorTokens, + type SpacingTokens, + type TypographyTokens, + type DesignTokens, + type ColorScale, + type ColorVariant, + type StepScale, + type TextSize, + type TextRole, + type CSSCustomProperty, +} from '@/styles/tokens' diff --git a/web/common/src/styles/design/fonts.css b/web/common/src/styles/design/fonts.css new file mode 100644 index 0000000000..604cfc0ecb --- /dev/null +++ b/web/common/src/styles/design/fonts.css @@ -0,0 +1,113 @@ +/* Inter */ +@font-face { + font-family: 'Inter', sans-serif; + src: url('./fonts/Inter/static/Inter-Thin.ttf') format('TrueType'); + font-weight: 100; + font-display: swap; +} +@font-face { + font-family: 'Inter', sans-serif; + src: url('./fonts/Inter/static/Inter-ExtraLight.ttf') format('TrueType'); + font-weight: 200; + font-display: swap; +} +@font-face { + font-family: 'Inter', sans-serif; + src: url('./fonts/Inter/static/Inter-Light.ttf') format('TrueType'); + font-weight: 300; + font-display: swap; +} +@font-face { + font-family: 'Inter', sans-serif; + src: url('./fonts/Inter/static/Inter-Regular.ttf') format('TrueType'); + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: 'Inter', sans-serif; + src: url('./fonts/Inter/static/Inter-Medium.ttf') format('truetype'); + font-weight: 500; + display: swap; +} +@font-face { + font-family: 'Inter', sans-serif; + src: url('./fonts/Inter/static/Inter-SemiBold.ttf') format('TrueType'); + font-weight: 600; + font-display: swap; +} +@font-face { + font-family: 'Inter', sans-serif; + src: url('./fonts/Inter/static/Inter-Bold.ttf') format('TrueType'); + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: 'Inter', sans-serif; + src: url('./fonts/Inter/static/Inter-ExtraBold.ttf') format('TrueType'); + font-weight: 800; + font-display: swap; +} +@font-face { + font-family: 'Inter', sans-serif; + src: url('./fonts/Inter/static/Inter-Black.ttf') format('truetype'); + font-weight: 900; + font-display: swap; +} + +/* JetBrains Mono */ +@font-face { + font-family: 'JetBrains Mono'; + src: url('./fonts/JetBrains_Mono/static/JetBrainsMono-Thin.ttf') + format('truetype'); + font-weight: 100; + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + src: url('./fonts/JetBrains_Mono/static/JetBrainsMono-ExtraLight.ttf') + format('truetype'); + font-weight: 200; + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + src: url('./fonts/JetBrains_Mono/static/JetBrainsMono-Light.ttf') + format('truetype'); + font-weight: 300; + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + src: url('./fonts/JetBrains_Mono/static/JetBrainsMono-Regular.ttf') + format('truetype'); + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + src: url('./fonts/JetBrains_Mono/static/JetBrainsMono-Medium.ttf') + format('truetype'); + font-weight: 500; + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + src: url('./fonts/JetBrains_Mono/static/JetBrainsMono-SemiBold.ttf') + format('truetype'); + font-weight: 600; + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + src: url('./fonts/JetBrains_Mono/static/JetBrainsMono-Bold.ttf') + format('truetype'); + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + src: url('./fonts/JetBrains_Mono/static/JetBrainsMono-ExtraBold.ttf') + format('truetype'); + font-weight: 800; + font-display: swap; +} diff --git a/web/common/src/styles/design/fonts/Inter/Inter-VariableFont_slnt,wght.ttf b/web/common/src/styles/design/fonts/Inter/Inter-VariableFont_slnt,wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ec3164efa8fe938310f74b6156d3ad33ccb2fef6 GIT binary patch literal 803384 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

    k`dD%Oq-FGvtZa7vl=NRMebJriyObJqizc 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

    S6KrIX_&}SieWqPQ?{H}=^!9pt?REVP{Y`DH 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%0Dq6U{pGSTfs98Msn(h=U&e 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 z zMK|2GYz}+|pRy*n1q1A5eg4RQY}p$8Z_#g!B03k|JqH|IZA-bCZ58sSyL8$2FZcgP z)Vp=vwV>G_#oWvbzzb%ex~$qIeJYX>7Nu-CKgOvG1WtB7P8>5d91qR*@7vS7x9PyY z-mW`z!skEmbwe9F2d+8_8YVSi9i3!KHJMCFsJ3^OnxT8JzBCW%y?EzxJPf*bg&-1wF;sYIv7Oe1d~J<p}>`dsgR6Y(UEdNf^yONXnaX=!5Ma?hY*+ySs7sGz2?tM}Z~sRB_#oRk`B34%WT> zVrA9e?bpThIN>*WP9`T6;Kq`t=ihu&e*TpyP3RKNH9+C_Y8lfxcCilp=sJb0WO@tMLdGfE;hD({YOP!0JqoO*)BRLw?yf&FBS0Y1e)$K^eA9cvac`E`v@fU7XpmgoZDshNj5ESo=G zFX^sY!6TyLR^&v!Y1)%sFdMk#K4#e|Fx-K+wZoMlooF~_uvy3H*nXdDb}am0OTyi` z`ttJf|A)P|kBhR*{>QKTVIF`11{`(75l0=BjEss}YsnpOP&6_!YpvLpZQWfzo7=jr z+sAFKz3-U;smQ3*wIU;ziY>NSm(0q_id@#EA|pd1LnIn;#1TgrhGFLWKKDH%m=Epu z_51$$TG>Ijek`YI3Sbtd{ehrsS@;i z*xQAX9A%q_Fwru!DJ;QvKcDnO+ovrpRWE(hnwVrZ=NA=~+?Jbl82zZ;Zl@wb zgI^--G{&BbdKwJ`dS+cR-{l>2Cbz%c*cH);`k4!EzWvUXcU|e^TG2kmjmxMgh4l%m z?=H&Jc|GmjCl2qL2UCoKMmF|@OIR&Qs}~>|^+7cHkB+6R+;HztZ^)fh>J5A($Xd*x?G$_bQ(~$ zfD85yCyg=cBB6CGh$?roUYCNV17bvgArO);Lu>+992!9LR(NT&iOI=EV`@Bo6)>mS zY*Wo>MWJKEp&()y0nb85# z6wo1Dvsn=PPwr~__@lO++xPnVO<7rJgN0~5WT2C)*pv_RR>h{@94XfM`>@yybT?z< zq2Kt{*7`;2kfW8vW*l-KfE5t_>{;E)qW*urQcoRTb7}qJM3(k|DN5@#|w<9xoJ&aTcb)EGju zHWa^pm`uXRgAxavMRNSuN$Kg6tW$Z$@hE^2AN{M$1Dxnq>97)XAk9WK0e=WOP$gAk z2_pN++1K2#Y}rqK@$28+RZ?;R$}|JHg|>mC>pk74+W*3%uk2*sM>SS351wq=9!jv! z8XE{`CuCYKDJb}Fskufb>|l#_^<2o4L}Px`fG0ElVgA0=7}E=rnyap*ihy|vl)#D( zGB!nbsNv;>@ZT=~FUA6_^&qCeQ)4_`K_0{wS?>wI??~@p5KySI#|Xj_mI!pV6XpI8 zlcE6|W3`!6Q1?Qwhb#gdWP+KUa|E5(7hJ3!r4FPb6^Wk%cd7imJjTzgcs{F! zcMMm zpp`v{3i~pdY{$_*E|txVY15HiF+IbeMU7i+lF0?-N6|Cl?fXHKe9(mIU0w~Eus|LZ z1Gbd%h9fxYJ>JGgui3^BzwX!@;VCOl%t zzWLU-det9LH?X8&+59O^{@`cGwvbfm0(b{VJz+=PmZ~jvJKlrN>pHT_UA6NtJZYcp zczJ_6Docd&XI^i_QII!yb(IkW!^xXxO(`dtK3#;WKGb#SM1UV-GM)OOoq2nqr+p{T zNe1XHkqw>lGqc%U#=~{t0E#V+{+AtC0`wzsDgZ3XF74gc zFYBOO?C5r?GJuoIiur2C{C@cZ53F3d@}A$8+>wQ+iiAb2=;S^}^+TEpw@1BctN9jY zabN|cqFrAw-e5cosLY2R<0N=IH7QeUXDTLRvUVo|PH*G>fd$BC`V~5l$LICFX;Jr-Fmi z!NFu-j9<2_s;UM}y?;11q}7GY>3rAMyJ~6_3J;)jikMZo()p!b?eI}k=uvj*4 z9<7khGO)z^c>X?Ba$dm7LN~#AQ3CyU#+F!7S62byabYt+FlJ?;GnMqcX4f-X6B6al zsn$_STZVSlqSLGA+{#kSl{Bh~vr6^L5MRZ-B%0_E zN@U6&nJ>{Xif@Ixg-e&CeF0+y7F2ymSJUBYXB-Qx23?KMqiexKLQW&ap+oK?x*{DF z^Hz;`jxu7`<62gu>!rkIsctny{!Slt_O;s8YjvvEQXEu@tBTxfv1^@uRP+w-{O%pb zDk;RMc;<8MRIX)b?<-W4s7Se_7A_;I#Ata|oEr%aBeVqeN_{$nGD~rI?CB`d?Eocb zVid-M!{HTW=Axn+OzvgN@|P}}tDnL&ZEM(i z7uV8~OT`i6QP$OWp#48@zSakL_$%zSVYo2pzet#q|_|vy@GOg+e$WYyG`wCfL)9uk|kI za?Xhrxl?PLfaSQ=MU@fjhc@XSM zKPpGw1UfDU9jVXcrJ&;s(2-ToQ&7~_$j>eH_2uiT9Vn3Oa1<6^T`>1@cxxm{`XH29 z;9&Mhf*kMj#b2?o0P&KZ3($BWnnSxe7k3A|WMrKV!%s7h>*&ZOyDJ_fVLje2_k7sc z=zs)=3xFu)D6Vt?iiMLSv9iQdGZk(baR7-u%{^rh>ADELz!asIA}8jGPRm5wEtry~FcK{bR^f z6(qMCG%c2y7YsXJsh+H|>8~R95eFNMDNX^|lU~=DR4gE=(3N73D<5FRDQtTJ;pPp( zwd_y_P_nqo@i#p%4+}IhDOw~h^G;gr5#zqf{VY?~`LMP&`K%1fFJfT5r*%7c^|jX1 z{fWv^}u*ikwSmqqt>pkY{J$5n}g4YB;retG6f>!HxK`RqG z!;OXhFcym&z6rhFIBB}gHp!@Wp^LVFYRb4fJx&)!6P*Y;!xI0J<|+a|UIeOH!H-r@ zEtfn#a#BuCWo1r|!*QAYs#%yK(UpMaEx*m41G3#Ii+l3&_FRAomCl|rdO7E!O?g&4 z-ENp2Ke)fGEnjI>F_!%Y`u!39ho``HdRfeDY(yvG^G|xM!K=?`U%fh))+~H=HLNcJ z`>#qYt#4Vqy7dj<_+My5&dC-_cfc6zA}<9N4R{^w_XPrkI&1IjW@h}|YV?UFxMmDS zBYa|b#fxP+7C4z!W|6ljlp$aFcW1U!o!86Hp4X!#@`xO0QTMm2`$r=&qa;U6%ZzA3 z%V|l24wt*2)naj&=ut6k7}KAsHe^lG$ZI&{=i#f})EgF!taGj_y?JRJQzr}wwBvXSQji<}{GwHOyI zI5vg{64aVHoTXjC0}<6Qv+yJ&+2C)F^|7Sm&V}b$+|E(Otq!(}^D-Q%*6G?zp)2Ny zQZII_U_G;1*M@e^hGW}sY-6+*4w|Lmz%F`?(fvaDgcHwvx|r=j<9nJOeC6rU1-|7B zYQ*-ZaUI!z`RV<+bnLH?V}$0hry&YbRiYT8YHvH&fv0ieX$a#=_5)tk3S{O{76d&{ zEcz6+#)?PAi`z+8v_}eX1hR|q60;#yZx`q?=nIE~^<;vZXnPgYWM%lv|-=$=X zgOw}uBbpgD89gaoOyy%h!bZm6Y@R&7jvh@PVHABT+u_L0r4isfL?0N9YW*-wvNNxA z<~`P_+`mHQ{?QiaLEJ2H3B~lz!JE|JiNgQ6h~@6$T4UE#j5=JO$V5jIUFrE^S5>jP zjgZj>vO*9eg;I}Eg)Cj3l26V&mbiyzxO%KVcI;~PSWIrkL{<+}pY^-zJ6T1|ak#j+4#x%zUpf8+3%SK@$HGYh zk4nP5FTHfH>tUCm?dZ_bdw*IQ7Kq9dPQbNLzKSJ}x)z^h0o-I2pV^Ea@v9}}Vgzz_ zvwE1Z;IB4*gTrBGew>`HMm;{=Op02&58ZOhLv)d#)f%Mt`0j3UaVuAgX&|EKh$Lvm z6~F@y<>pBQ;OET!V_c)or%H8y3MkAhO5F=F#;fjsSqOeE=rQaA-SG_#n{pkqW?}T*xeNX_PU*1 z2C{B4GF)6^qx*@|6gpcLOSsXfZQqWot0YqPrzgDtj{Av`aXs6v zFrI$ONC_@ITGgMX&Dk>2Pe(~yv2wdAWzNApBiz=g?mtJ}|FqgU;|j!YBS&PZN2ml* zWfWcKtRqM^sT^9Z9^s8$<7^HcJrhQ(7E%v*mipMUjJ8^)V5DbYq{rdTsIL}e8KlB$ zt_i)7mk|(;vow)-m~+h>Rte(0;%4`(v$GDas2K_H=+5oux{C5xW27#!hEwb6N$7p< zW*gQ~w3}*UN|HvBx+Dpgmt2p=1$)B?GV>-eRO$kt3G#`FZY(dfoY|}9jK`iLHa~eL zGKn|i`gn>~oQ5X0s_C%2kIm|sj*UrG*w+S1W#F#tY)kx|Gtx7!z41=`+>?i-@)uYwH{Xl&NW+Rd zufGDT2iw@8Zw}_!S(VTuvU_AnaJ#G5%>!aV2`)SN!Q&n~drIOn?_4#HIhBY@g$922 zU}BoGLd$g7Bex{%o4T|p?;=xSp{es33c{5b!VmB-)bISHtFEqV+?@{O&OH?gVez&V ztFy(jv_OgAK7}0SZOW$xC!cbu-5&0d=7)PBiI%`!sD#YvQincjv~#>{5BK6)Q#p_4 zdW`%{Xp}Qw*``Kl%ZU6CuEx5^RPFA(8*WDu3FVYv)86;XC4mPX*zI~=(!s59)YIMF z?My{Fa~jhdQ^$@sn<>|f7rSJbRnO~rz|1VjW=IG6QP9Wl<@a_U{`zb5u@4Oo4fM~F z*eYEA zS7YIMb;$#HYDAcO0CVYlBX$uU4)l5P8Ogbk?~b|a933E}S?6E_?QoFSi(!!k z2Zpu7A$Ww;&);Q_(YJWI(Sstpeg=-Y0~|xWpD6NA`C8wZN4nPL!W(}O-197es*$TA z_iEN{srlRAx7j22^6uB662_f-X6cGo%z$?xE`ty(P9BjbnlklVQ_pv&Taw+rWBYrb zwEX9z)?FWZwTm4^8krRz$CZ_lLgHTe@3F|h9NVUT5B2X4#ah;To+D^7<(RphM<9l9+OX`5H z=ZkI@ujitJ^dJQcI2jJDb;^ibhY_J%TdL3)tGT6kpOA37@92qr+ul9s?=xEgNah=N zS}1b)OW>+2wgb7wj7mh4880B}6bBSnQv*E|_p>ow)}*0tn(Lb2YeI>NItTLsowS&{ zj2(c`tC3li(oYwyyv{HE1iRI0ztB`0Edn3>jtCqtjvJ>x*Hm$pe@6^X036;!t$Ak5 zn4Xpt7Ew(kD5j<+VR0ru_#I)W&@pB!jMm>0-vCioDsfgtsGMQFk-m>v!Z8cUNqZ|6 zLZpCf|ClkR>Y$ii5wk`VTg=YtNL)sb%ED2KRS~sH&RP|pd3RRz?i$a&yV%pjuBBQ& zbJYDetNTYUglohspV3E+*q2@EQI1hZ5hWC=eRl<`RXs^mC8wJE7%P1zE62(S#v5u5 z<5hVe1_#fDk1kYeJEInRxw`&UR=&|{?6Ecbx$jmn%_Po#uDkvJt$=e`0mR9%b;}u? zEOC7?&Wf%>lu`Tt!pC&2v#n&<$FP#IRv%?0pL=wDDlZz&HM%IfuH;Np^n&bavhnn9 zKL6~S2S45R%oeIR81oruvu0Vv{Wuju_I?NS&2wxcdjRZdf}I9Wg3QmEqIYwa9~H3m zY$v{doIQ{CRxGF6_{{|fBl#os&~8g2zvVtuZbjw>y7Q2vkri3QZ|cZ_u~-r*k@@Nu z_qi7WuyGKUnZq;!=Zx*zdq8hWfaq5eSHIP+6o%oX|pjBjjn5>bf=vD zQCa!9tOfLC6YhH425!$G|bwG`cK z7kfNm-hW~LYbWH~Ub1+bH4n*|pRm1LTDI0fP9#4EVI~cL=j2~&&&XK58jxvwWUkgBQct!o}|Sj%9BuGUDBR2?ORP=a7+9?+S~BZaO_aR#PARW#-uXO^`lABs{3 z4X#ZluOICR5ddy)4~4uI%VcAK>P1EC)ZBw7T!SYh|HO1W;dDG9Aup|wOYvHW#MEDL z&du|2iQBP)ErfqE99bf6u6gOXM`#(iSaAo;jm*_Y>47AcLe)p1ysG&X^m;S)oUPr| zK#^)5F5Tt8D#r-rx{XyM;BMo_azJSzjYWfa=OiwNyBT$SYPhv<=ld|Ps#z)f6O4?f z0Pm>=@rr>(Qpu^9wvwIOB^RnB1GM@h^zhrSM3URt0KAO`u3+Yas5mW;(AOn`e4FGV z1vVciht*7)mfC z@y8E+)vJy7B`HIIT&?G)0I8dfI$$>2^o)x#^c0RCM!8_b<1MifaXqhEjJIMW{s$v* zB}RfWh%UlN2-(1LEdb~gxLe$$nl^b~{Nr0b%+AJ?DlROXr{LqVBiSgA2mxCnr=&1w@@l}OzFCZ<%EnaOl16xWp zvp``&_G3~i(-Qm+0?OR#2l7!{PQ@8M46_Z`5FvLpaXhAMWR8sLoiu4#lbfT4+$C@e zIq;|0Mx<3UQ+^aFIeT7L%VYXZc#ryrPonE_iXnc`$3{(a5oIhWUzSp=om^b-(R3i9 z)2CRilP6pCI-OpJTm$_8pc)gox$U%lKUE=HLI$MyexAJ2AmdB@~nZA z^xhV)*Ke`-y)I*NOumwg#Trq77|kCoM){>k9p!ASD&oh0oF!8QTZxT){_LzFC}5rN z|8-*PAjZLKr)zUm*cwRKqOhZng|*tS`(Zs#<%#LXHEuNF<4wp1r3TL+;btjD!O@=f z@5o_v-UjWXiy_eeywoSVRXx_NQa=!*K2{u16m+Qev<#1pcWgDK9@(fKxkf!wS>o_$ z`xBM>r5Lk^F&D{$n2W1XHlOb9yNj*BWV}nIr73~oPr8S5o!e468T5c=GrUOSw>H@^*BdvyC5osN6@ zb1JtmN=o84*H7sbyqVj|je&?914qbmu8ez=`vO}dx0E%r05^^M89>4n+>=}b_b;vq z|95fkar?P|0wU??ut)U3TCQD-T49RFl%i@TuF!G>t>nMaCWpX;hI)U_-K$ou`spc2{U2@Lgi72?BEL*<{_PN$v2XvLcWY45=YF(L z*h(!Fv|Dbw?STjG*y?)uqV1bux&}3q(qu7bkiHVrtqr6Z)PRfbG|cHc>Qn zb%~sxz~fQ-NwVtLlZMK0RW zCYWlc?23n>uyD4qZ$FB^#_~1Y+Qr7!Ex4vjIS-^`v@3Gz?+Y?hyHUd&j`grP(|-Gk z6)RlNxW@Ve3^nm0DQ3Os^LxDmAyK0<=v`&WCwf_&juJ7#;ou-ruFG6w!JLWk_#ulj z05|K{Br$LtXLCKHQh`=-22jChRqMj&tPr4-Eg`g279dZ)%E?f`V;5Ipu}3UUmWSs= z(hcqqaYiIcNt`9EjNaPX=#A!cI?iat8B;;mSZtUxS@NLi9TAI=^TIZ0q-8Qp7cecI z3_WY>@}ba|aqdVn)d&Iq72)p$xg&qFivqVGFOOuzT&VkMirMz`q^Hw+2Aa^sVz;;B z%D=&-m*C0{jA%aQXWA{dIGqg*O`YCTs4HwU_Io><8kR4=VWRgPpvaZ14c)r8yc3Il zffCNzZHpIgV+nCiG2XyV^*hCZQ_O;jKT72i@i*zL`!7+aHMg{Cve0+KD38#3{N6k_A z;ow=$865Y=zFQWqWtCl%sU-$MShHV{W1F+e@Esr2hu|T z7{ULIx}_PRw>NLt{4PPtsHb(bx%80VXb{y#*NE?G46bQ@q+&8**pKiKy4mvahB$$R z_%Z30Nn`bFsOM1EH^&BDfbYfnV~3nblJN!5(F7$_&}ley$~BEz>4bs|{i{WW1Oc=- zMoBq8>a28j_J;Hmr&_F&j5vqa7j%le-k_&UPakHaQw;Qadj|lvi&rb5OvKm`qF^le z&}bF#A7FXnL;SD}`EVwczy?FYSa>HS9=vizm`+vFe6|C!&OoLhXW=ar{N|SeV-T@P zOb33EEOxtvu!967tf#es4%OcceSHSA%*^;bAv|?zxW{1VX$yzjT#qUo3=IlogSu*= zJbTOu4Pg8D0mraR#4(L!;QuP8gLxu5Ib)=YHA9x$oVcY{OpV-8Q{-Xh9 zrXm`CU`6cT$}m$@6z~}EsUZQ$b)>F|g?LRxwZ4Am&U^%dJnV&t2M_N70)H7~OeR3$ z6#nmJO-K+jB+z}u4x0~~tpeX?aCic4DW4t6&V~4I#2vLnR_JrA=-rc%lf#5sNvfR( z_94b@Mgdnzs<9E9Be&GG!Pv#~UHYk0_2H0#?`Af=h$x!2j9F!^)zH;t5cN($4^O&l zgF-DQuC)%-nv16?#H>?dnUOjA`uD#C7WKdf4U15_Vis!X6H8OE;~8+A%!&L&-0;ta z*PhHp?d~jYN7(`_8ek)0yD9QuBrTH0@7co;Vw90tk(!#o`i~v$>FGIoG8C7Rnx|CB zyjy(saA@r9RQYHL)p7^4N;@42VBT5Nv< zqULu{u}irG%uvdVdW^lvB>-qp&i=zq#-0V-LT(B^ujSUl$Ia|qi=2Yq3j5o8w3AS4 zbm}B+Psf)B=OL+vwnunt&+dHO)9mPWH@~~bn#&$x`_^fj-L##(*nea_5wlo z#Bi`5pAj`p3HSRBeX(!f7l)4bM^e*iXV=Thm=eCa1Y>$J#*`uoWSuhaJPhRJ*zQRE zYty#lZB3tlv@= zIdG}0j;vb8vhtOj_hnoWW^h2`duJmup!3u`4gOaXjk&0De2D*JR#qRJe%&;!% zIZ?;fYkppcpP%Pq?Ca!(_~vtj{ndOp3p{xLV0G&hwGp>;mJexcpZaDkjsAXR0s^4} z30E#1A@#Fzoagu;t-yjOJoxN6)Q$l^?D4E4uZRoI3IKk?gdr}M<51ZUNyN!1W(z(~ zyX3O0tjjVcS=K$P>OTpztBlSEvJ$CZ7S{H;5YM|3Z@VZX!Yz6Y==e) z*mlB@S1JvFi8>yPra1~_JsHdLQEB%umT}nFzrAjs92`P+ z6~2*EI=YV>5AZ2S5Fe8e4)~6Cceq`eVVL$B-l;)FX?9BU5ai{034RINloY)VD^xv? zN?*uP1I{TWb5^>6`8&Y12RsAJFnLy{TkX|Es|6j#lzf-8&Y-xBV6wtZTq<-lu?)P0 zlWt;Re6v0rq$?ZAFI%m!=8put7J1KPc5BYxd^SJhOw~W)f%W| zx&0kSzk;Jz;3@CGQ(i`#tJf7QBCr$to3?MMEH7V|7rB{0X-F!A|H$@(Iag0U z*7UNI8sprGS^i_-B{rfGBVxZ-@ntW(U=Qyxpse%z-}GiKL+2Z|{r>?^OUC{{0M&N` z*|TP)^NoRV*#Vcq=c}n{f^{32Z@P7Y$!Hpj9NM-2PdHvO;!a2Xz|)VV(sEsSdBt-cbIuwE ztHC^^twy&d$Lx8oqP$!Q;c7Qjy&o{=+&eECbuQKZ>{i+Q&!g#JwoGmSiaYMCT zwxj9HLvYMch*MdxvvF&ys!+^h?dvH>-CX{UIvi=TvO7-sPVUsR$8qnge7N@=xDAUQcS~*uOl6FM z@<}-dML<;=(AsOXbfEL_HF`*g|0U^A#nHLCa4w@>S@q&XCt`P^oKJb4bJ0TRuwjyw z3fa5BzYr99Mi^!uP*D6PnoiRq7P}oSUBrM$;-yS9gh;-WsXyu+)Bf>zay_0rCeOsC z77{RJLP?P-Vai10$lwxXvLa&eMk1zcWZYxakDWo{JLqmD-bQyT@z5jm=2M0nql&>- zo^G9|7qpgL}%5y+iCX>K=%S3Al#@@J|B9 zUEJA33{)npDp5J8OtdI0ge$w0)4=YbkLsDAF4T1sadTpHMCf_wTtZEq%0y+k5Cj!b zHYyX13LljzXFGBFrs%Bd*l2Jr8k^DF;G`bAG!{i?Y8n~)Qvb;X*!xGgF6jzB<03?2 zVCHBtGFn?3P#8wBW0D^Gz$4CH1oI|;VklHwo2P)p?lqwKL;bsLo?z;f3|xe>=FNlX zQi}JZAJR^UGPiKM?dT)a0f5Rkt`S}h*lx{3Gn6gxqs&wHWU}b6T2iz~ksLG0#qZp? zL9ufXdMPMalsjN zy)xT=i4mr5#NV@?w5oD<@6nGoUk5952~7Jt5YD*` zOVwh;N^+=irNo?s9`&(%VUtq#tsPLvJL4>I5m--iQ|Dd=PrZ2o=>KN)uPWt<_QKW5-gk0-wg45BSX#0Qf zZ673ksT>D=Q1qyx`E@Nd=V!#cQKjVeuygmEbKW zsiP{`yyZ{6(egzvM5Y=V`Hg0Cix#8c5-KW|EmJgzzWfy)m09>#$``U-z(SP3You4U z!bmyWMW&iFpC|{3N!upj?xok5pcT~Q_P=ZHUOEdwe<(J!3La$Z7yy1R;k$M}31=6I z+O>9ttX8Y5$~o3-?nMcZzWxBhL-1I6j`eeByXaac4#7_{=oTVD|8SWT%`7ce>qHaW zp>hIlHr-@YGQdT-O67YaXp#z=5Y{2iD&W5u^%}AQJCFrUm0nc7hgm1H3N)!^%b_VV zB<6r$U1m(mhahX^tejQR0}_1NPJul3Zu(#_w6)=bBA1hJog`c*hU=rXsKl(m_e+?~ z$TW0Iti{fWl45_>;x~i43`Qrt`0zq2y(KuuEjULeQ{(&8TS8|tC7DfnF{mU43zC$X zNq)u^k*heXl?pR21ApEgxlRH~i-nZMiSXd-jSYkyLJW$9REIuBm~Ys)&K|knr3(Z^ zJ>0(5d1(KvCm~JcND$Dsam(NhbznATbC+{ha`Pd)>tKh#D~|0Q{!bYp_;~Qy#l(5^ zP9x2StoPX2n^-nwX=-X-{d}xLoh1kgPSM1a8-98JH?!G)6kTeZ5SZM0ejo&eDcVrEK;z_WHn+}ZLti>Eb+YSemD+^$xCWFWPNyz$ zMo=m@+#T)~w`|#N4KIqMV8MbRpItXZP*KWCCP5qDbu_ucM! zTnpDi_r~kH+7=-js0+SG3tz?8^J_qvT>vmwK)TRzCYf8gl8o0}%=e}4({3tG%2J;> zCr5eLWJ#z4D7iMKknaw%HPGt|;0?8(w62YA*sQ4ahExfe>*{i=RS;v6oyYT#CqqGn zauv`GQdF{Y4Y-HWQVG@c5CB!F@BpZ=8B|z>{mCnd`tGIq7TUX4Om=$=Y>=Ko8DFJEXz8j;M28kyVwSQHSG zyuR5J|3eh1+_7WFUfyQEH(Ec5TRB7KhQrPT<0Nwml8VrBtZ&#YTK(G|+nfin{2}TF zEY3LbbRL!KjQmeh#Q}@YXF2uOlbfG>t8eo6soFT z?{X$Yye*#|LF4ub)-hVQxx8%CTWzf$Y<;Bik(xd2Uc(ft#Xe`wb-!MF102G2xY#cC z4s4C}q#6LLcnjJ4P58eZrTf?X1a&HA2HJK${V)pLD+PjFVpwC07fMBhToNFFQJ)|o zc+%$&hgjse@5o8_WdFc$1l9UaBAnt6gwQ_TV1%VGjLuCc#xr@mUPeXKBx6z{*7G_M z{v5^!`;_we0IR!1VaPWyTq=f6`A&t(;N5g4XcLVF0sEyG#~IvL7~>Phm`xKhF12ON zo@`Ey^oI~eN{Z)0CyyQT9_#7x`3HFjAn2F@T_|)Ih17U)GH#o#S7cq9=bn&3)jyA|`aE4%s_j9QHP0xrPE(NXHy(TBqRKoZPZy5PLwn zxpj!$*(Z1H+(1~XYw=Y{5n1 z*+*)zdh@?ap%eWFKKrV@Etdhq2X%#nR&3p@R=6db-0S(EQw!?=Pz`nkP^yqNd|SiE zrCPL0towdDk@A(`Y+BpR!_zqMG~+HDB~MVJ~}$E#ba5O3r_LIg9&mg5-u+kep#HoN?hm`8sLp_XNoiC`Box z3&hFSP5ZtWxy9qLTsTa=PL-A^7`buaLyHyC(h6xgDo(B~s4T8|Y6+~5NHI!Jt^iaF zHP0fepwDVJ$p#~<$;qw3!O{6sTdU3^samn10IYbFIbkX|1y4BaQGT&nov=oLSyX4i z4{`MzoPZ+N)4?}nH^PHZ@#_A5eUjN8DdHMnD&7W9!0o_@-o>qFgzg5!i5kh@61gt& zGj78R$PI0k`Nn^?mg-S^`I?-|vuvIP@Xl96T7V))#c1H+C2l3N!;2@)$8@0WWd+|^ zCEK|jGQawkDWy7^58HM%wIb~jn@F$Popp~%66~FgV*rXjy#I@?o=~vSAMEQH5JSL| zBjtaPX=P2Ag;{@~bW8|H$Z;l0DlG_wjPaN+*h!rs_u@%icv8x#BJamcT!DECAcJni zI(9nBy-tJ2u z{TTdE-1zC~4Vh+SD8g2d4umrD5jarSPwngpKEvOd2aFaZF|x1KRn~(Eg|_hf)%bZY zWJBBQ2k|}hK;m!nZ2GTId%E!}y)EyzREn^iwGp@moxWa<4+aMYhheZFKJ7Fkq0vwA z{Lnzp;qGo!rw;Rprc~+xI!3$U@5WO{ekK0B{4%Y~g;Ki~b zqa+^y9kBCTo6vbi&?R_nBxmt7Pc`~Zo*Dq0ZkQMSGQ4$OIf|tN2eEjJGp)=iwxO)d`KR#gQJsXP&eXZ zsgqt*%3hCSZv|zkg4pFab_yt)3qRI9d{WZ)6PUAsqaTok9cJ^6^UzsDx!;oiJ6gBN z!rHdBwckT_Me~bFyX#S^)yo;U;?=8oRmjdljl_0T?yLV z1lmml?P6RxaqDfj-FDq1ukV|W{=Q|)+b0r4&sRIQZY?V)C{uXRr^^G3b>+`*DXf31 z>64b0PZ~e*t+TU$7LM}BG|SBkm<;K;4J?D$6vSLDe~;zI^RQeSWC8dGMbXY>V8#(U zwpEiKQgt}~I`XH4C#sh(b6kmvgNClQb>lZw8)xNV^_ZXg!#Twzzxp+@ntwb?EZ;yZ z-7YD-c%b>?&klBbj(R=cc7OcYXP@kT|ARfBALu@Cpqu!G(`uaBFrgPXlp^GX@`>J# z_PrnBp7(x!pglA+a0gb=>(zB*tje|ist!g#1&Z^_u8SpRJ(ng zK@V5^TH*DHx1vKuIeCUyxy0t+d}PU@(#nO1MZP*ka$T5euID7nxHPt}Y0oDII=jC0 zbRF*8^T{XwdH8_C*u2Ga5FG)4$yme?j1k*P%k1NSD`u?$0+NdbJ z(@zoA74t#MrJ!XNX5}S#3M-xh{AV`T<|K7s`PZ2O2d~?mQv~-oEFe}Y-k4Jh4<>m? z@t@qpPrcdl#WD9;Zelpq6iiA&Vclmle^&CdnV;3w*XJ^Xjk&r~2dvCXz-yO8ekarw zvP@cS%8p1*)PzwT#qP5l#iuc%S~PkY-rKOL0PZA3t+5(pNGd>KeOXNdA+Zsy++s;% z^?Ua0`JnORy{!j3;qg8|_Rqi3}ah*KX7v` zcG$0hkigDrrNo$Sr7ox!gI>MPw}Cm}4WiavOjTb#M-p=mamczP)@ zU!`kH+Ih$$;>QIX@Cbn(8h0MztlOeW_^ZK7^TA7GK~a|iiZ`5>HauwKIzLrh-$xZ; zLpEFWeY9|0fEK6uq*;E-d>$gOZ21fll%Wjpx%neoR%`119yC}78c_C0K4_2y8pOA^ zw;cHP*zsOpD7XMt?py?vu0;up-$rU8zm5;LY<>Lk$7?==YdDvR@94s*m(QQSU`*If zp;-llrTQrT$Upg91$;V?_(Jt7pLj38Q!d3trCq!7r#~qvDPE{dx|PTS`we#`JC220 z1M_l)zo*DMYV^6#IYYO~b%4kGNvO#b~%RP`$sVTY4 zAf1tWh36q6m3l+EmM+n3p72cz+4zXPI1T0HHXFNuc&tDkE&R+}9jbHSJrY3Z=Fj41 ziH5|_u1Ct|GFtCosaJ_lmxTwy`@?PF{gFwLWqbgh9VdZn>-dfiIEF4L4O~$-LbAhQ zwVp-xxDm3uwmK*F_G-20igBV3%Rqw>DPVdeNcIh|c+>@UJ#+?fod&BF)IUwoxRGF+ zYpX)ZUxJeNf|5T1B^QE{gs>G03TkVy+E^aB*RZ!`ID0XEa>sevUnC=Xd8%#pWhQTX z{d)Yo($cD*oyoSz+U6B2nmaoF8iUvVQGB(-`lI{ivFDVPMY(uWM#eOwQxi_Kb@U@o z{Mc8n1;p=)LTH1d;tRC1Eu4J-v5l&)^Hw3)`3n4a0TH<#b5_k~PWIO*s$mDK)!bJ& zH`ELvTOn&#RaLFZ&PI5Hf?X}*{j;;1-=wZ%YFuHlxPIr(-QPMjfrE{6(zP!5UXtdR zOmn>6CR%p`3y9UvCd@P1w!%@e0v#^PVVIH^1R3h$O27xVM&Tpelg+M|X7nc|0V~=2 z^+5z((JC!2-aOu9(29C((C0a+(f0SB7|4GV}ZhXQ)7hz~plLw-q>+k{0(wyKCC zLdq%8CPDiS+k`?@UbjF?V2Tm1hT#XOR+%Kk!#5){z7$?YWvmzA3}o9=q>_39(7FhK zfUy}Fji0{acDrBwtf{T7CH03Woc=>&GaM)^18iNyXTXCEAp(hpO-fpvnb3+-Pf)w_ zugIL4g2ytHV!h0}-K*zA_zX~h5+TX}dn;j$&u1L_ErL*;ngF=~nt_O6d?WnU2v}2& zsx?v+_vFrw={Ol~03LK<~#jC6SW0s6Y8&|1``aG2 zNB*Q;zvP-peY+pchf6HBt=86b>Sp9DSA_r&g8c+Bq=l9Cv}9h%Vbj-o45k*_o^ zBjd*oWOXQ6SFA$3v(WPavc*VM{Uma4+^ZcOC&H{`=gtyWe8IRbEc;vA1}uvyW3C=? zzS{b)Q!F>{p@+WcYx2Ejn`G8D+fl2oTAWRSO zD^@(ds*{oDW$xsu@|gex@p}It zs=p0~2K;{S;e#Cqza11q8q;L!L3vKgGEH4&#=Cam!Qc z@-5eIejnV{_V$wx&4<^o7>0zp-Ja}zbKloPr!^hj++2%ElM_b1f^vnDA=U%72XK!=6VbWMZ z%u^)U2H|>R`(GUJ9IGwT?i4n)_^x&!W~w5s9v80lZr_-TboV0HJOwx)vhA7&O7QdB z>oD}yY!7f@3X~hK>8uxHP?2y;_#KkYpJbGjJg@*TGI)`ctr2d$3hl}(Fzbx-7UClH z$R3#rRD!zA2N;;_VJzkVWt=@{K~br;v#z{o?vw|ILyx?Gs6$`#sZ%E=i^1N`LyF*LiI!gry2r%-B1=s0?cThVspKM^F5-<_dkOp*MlQ(2S<`FUkHw57G;hlKi$-H zbYl5GdXw-o+}`xkJmgp@RU~zrHo>MnvHOu&VBwa??}bgzO6ap#j6Dje`D>XhCm!Cr z_l}=rh@xSF4e3!%{gmsA@3>>%zWD!%rgRER#mskU+82ZGehE=z&w z($Z$+XOEeGnv9+t$C_-YAPV$ zJ>H|<-uO734kx092Tl|n!pP$+IF}QMMPh@Cwc0pClCLks178*f5dkU_IM#!vspE$B z@7sUU5H?*hJ9DNj6hR_Y>_ptzCsKKWv`fssD*{gt^3-Vl_ATVnZTlo_o=k-^=H}#~ z@(4C@Lx0-EKc5F@owCLKE`LVGmDd&J-SU7P-H8g>KB$(lhK?jIwe{Le$#*2R^OmdkiUVbaB6hVjL9BcrfnXJ8rQZ{;;tTJw?c< zX73@xLNeurUVW^x^0CLOc69|0zvz}~PL2bDT-IdXvb1i`almiKWKc1~3Zb^PE=v}D z{I8%TYtfoTh=u(F!(9&?Ney_e5rLnN+2<7as+(nr2VN}{=pV$YMUxcp>5b!r?)?Y% z9q@+y!k8(US6!K5HuVq1Pe7k)j63V>FJlA{wX~&o60y;TV47CD;aTnIj<3GpH{^7JnDku@B@J z>*uSM3^O7A2SX6r&4%!2Q^OmF3>+-LjAB=EYwHJpht{ue?&wPapBCgRe5Inlcj&P? z&7N?oqY-)J%A;|Q?QoSL5Y8g&)k#@CEfg9!^jYhthXCdoJJDza1ki-wWGFBYL=bJ5 z(7@o(*cV6?Q_})7QJ_dk z?Q|`Ke0?oC464SjCPa+7hUd0#cRj()FgcMEF>(4VAl2JiU2Yi4Vkw7`C!A-{DI%lk zV5f_lJ=+zn;7#?y?!>cE<(wbj*=UuhX>4q3-2GPvn$!?j>{pT1hR5FP>g-FNVx8R6 z_;H6KV|Cft6%{tDu%Z7Y&Kq>1la82}nj9aW3kCFB;i-;z#6H!P#bX4n(}TnDt6NyU zeDMthnC_JbSX0`5h)V)*RmW##Po$1`CbZKV@vQ<4&o?%L*P7(SN-G+}iBm5zPPask z_^1k2jd1@JY+KCrdA~cCAdv3y=A69i3k$ChyP98kM3QR$x~-*)pP4Z;9f}6Ta=@Pw z^`J-P{(8netW6EZo#^ck%j0;B$c4MQyg@!`Qu-tl8ZNT1*&N2c0vg^9_4f}By9BhS z6P;}MVZn>Cq;iTzFY55Oka>a$5YBHsM|#{S_TW~9_&QP}EF49qN}zZpDDD8osh04y zpm>sL&dLJQtV=N!F3l^t;g?kMq7LiU^+~r~e^2`Sg)?AM4D+UpyQ3;uTy8OOuSdJ& zP*T+lS(y7vqenLDljC_koU;?c&CTg6{;-01-g&i!PXO_0QVLHVNke?50ty+MxaR8k z&nnOf&BZXLmPkpG&6aow4%#=~ZQJ&+t73hA{Eb)n zU%;|%>+!3QKk^ndK5b3>pV=KU>%d&#YBJ@7!2t7o)!EsZTC{w*ojDVVK6iJg-FV01 zoU11SEkK=l9~ExAW@clw>!a{$+$MXIM^Sby=2LfIZ6E;Z)|mK)5fQw`UiBeaX8F| zBa!}o(U@us51u>`kW(U&uER$MS)$oI&YY@a{hmXp)PSfurai47!ov?^X^&zINH{<- zhDex&hTXFRUADQS!$&=ceml7SoK!_-?{tMS8gc7kc+OcvKBoQ<^q zLyU_`l6x@rWXDkz0P2}K3uCWC+o(FXJ8L(p7$udz_jTirCqdcG4Z9A!Q--fncV}gd zIo9z}4P04&ZSLqbWwEV*)?>JQTn2oB7}dB&MB2ntC#W(8$In$dp!1Oe+VlRyZ)AogriF&W|T4iE4U^zsR9%YWY;2XbSlJ#wD_lw6ww z8QiL!|6{&^Eg{29<(MbX*iM=2yhE(dC1oKjTXx6q)li39{CNgzdnT#_#yqmMww9&M z$5Lj2g>~(K_IRVq^>^rD(I|^PPd|{LG*CR}N{u(0n0~@^pdL^Xtp{O;BS9A}QzTT_ za$`s^AD@wVvmYH8(I?7)iXB=EuyafwnhUbv5ghQ1pBW}$HcrsH{;ueN`!RBq=R<44 zA7kWh#mE^mrcSn6t?3iZm;LOXVxzvXgsd1zQ+$=h?Aa2no1%Tzmoe$mTafPXP+Uo) z-n8@=^QW*wD7zmH22iEl$$eR-Li$({C6c!RBBgX)z_ZadWFzwXQsphWJxWrw%_78Bbq>A#t zwu>3X|GRq4UDwaa0kVBPCi_Io*hwBj+kJG6M?cp(mdU7 zl3u_s{r&;mFOQT>-0xtBQ?m-S;0Zl=;yblMxs{!q5AwE%WpfsWoCc*24c?JWDVN56 zSGbC>5V1^j!kMdB2viRP_<`t%@hX+jLo!4t2psIBPp6O6;o3Uzw`l@%5vw zOv(0VJnMO`{m{s@{byVovFIqN)N2!!&$>2mP@maw#+A{O<9t*;eb>P0;F%@9>5LD?k+DWbhDJt4#=5MTk&%(1B94eS;)nwb^M0T6-k@l@ z-~D}mKdu8f%zgLXbI(2Z{6Ezp?7;7;0D-)O3qlEDB@~9{F$_-#voXAW_5?x^>Qzlh z^7HQJW^jrAger1>LY7PQABwI6#^Z!2HBKBXD}*mlU{^0iZ#g_SG<}CE2o6gvADWiCI9XtoKo zBZNlu5W=J36Cq6c@18*DL{A`m8vX>rs0*HOaZI-10l>#S=#L-ZRg@p+;Uk?PqBKKZ zR}}9E64RudKf7d-X^lc(@Cfv!V>m=@hQn~kUl41*6$2w;Tg2qsWEh(9Yk-g%>j>_^ zQ=F)g=1^JbNvM1YV;EuQg{zR~0wsjJiV!7G#vYmpVR$I!^3s?cM{p#}-ba9>gF&D; zdocdf*eITj=MOXn@}AJv;W#Fo0mF%A(G+e*}NpKk82Fr~de9r3YX-wDJ<{ z!n@do+knsa;9ShY6Z9VMCzT)8d{Ece_<~*f!<7!l$`$Ls>d@Km%3FNBzRTlp{d7}x zb@hhNx;&mhRO)XR-ZsUz^HqcgrKDVwpoK3}BsfmZC}h7z@yTf^DOD(dyHplTbG60I z`kD7Hdt|9I(tPg#PbYO~fBFBR+m^^>eR$*0nI@viW4WBgN|b(AFSDj(*|7>7%|Vb{7w(ES?26 z=6mX10Utab4ee#+lzyuc&m|7j%W75mDe*f(k_^JMuNduZejmIY zKI`4&m?Pa7N{AH?=inSC@7U^fYoQ*P+<)*3h~Khyz_hBYthK2coj&;b;Hjty@$rhY z^FNW7_1W;{52Q_+GzqR{w=G?sh3w^uN$@RbqKgm(9nXpod5+vV;wR2)25>q!O}G)+ zXbrtR$2vSmyZeO*#CgW{2Y6k$UTZ*-tw5k}K&3a2v5g&RfPA3Q@Bz`KhGO^B04iIk zU8Dd;pih60bE|{kdIlt1k1Bo@r&77Eg563DU4sfLeyOJ?ZtNtO3vAH_t^Zh87oHWS zWe8jHdhs#jZ*AYVZ{LrHI-IKRGf3zU_2iq3drrY4$^!@Tj#xV$)+r^Q0i)VHy6%Fi zS6-Pj2a%toc7abixuvseiknP89@A2?7ZFdMN_;y)gxc^A23rSnpi~-tUkRlRqF#c`8V{YBL+YsWO2Ls1aD2f-s0qnPM z*EI)s$gM*fhj*q>(lWUvNNLidaK~%Nc=J}}7hdlN>*fUaP?J<}%~&KAYCi7n?mm9_ zqgjvvZ;@2NJ+QbnI5iOAg26LBMX|r0p{Z}z|MuW49~f#Qrug#;5vw zqobPI-pzo9Oz{`^b68c|&XZE?{3~Nmi+=_v-eyPO@HGhACB@1cU@@_yN(b`wqTR(! zV6EI(Bt%;#w_9QQFhELbfG$yKe}nmL4Y2uH_6|~pQhN-Aj}1^v{#{mwl1j9K?Z&<8OoNn_4Rrp&McUP}1VO2Y8gRr?tx;z4tPI&n^gk zk5dbKWp|H1=2y9ScRv6zB+Bn=t9@0N{D$OtSvOu~5TDgT*Ceri(rE@D+oB3E zKiJ<7dL>??(+omC6SR(sx5h_+7e3kD0XGo%_43G#6!ZrM0!YxI^Y@?V= z@>*lKju$zIR#fhVcWKpT142o)0sP-(MAjVW)Nu%cMgiItq&L(!H5?8G&W_0Y2PG#e zAa`{g@f zWU+!|f#p2KV+=(F6S{d;0&iai-u?!7yBK&&b)3}tx8B-8b(Tod`op;LUE#l3wEnfq ztzYkJQ$qrrn>XhzeQM4uq{uUS_tij)_n`EMFlB7dzKsOkLX^RJXUAJmpzGq|=Fgu$ zoohipF*#ye{V$k()VwQFI_-%{N<;>VQJQ{D0%B-|FgSGNrz?Vum{H*@7p6d%9Ne8*E zXH`PkM^Z-NY~)~Q!RsG*QkjDlIdwW6jQU3=Oov=8Bd+>ZzyDaL&lL%c-K6AkI+*v= zg9z%(;#eL8S$r_)YR;IkUBJVD)OkpSprH_PE>PbR>uK>g-lk7tQ^VnI-Nn& z-+R2HeNVfeoq>=S;GwTY&4xi|xLPHlW?fIm!L}o(onZq(4dOccPWGTS3Un$eI2-i? zQ3btG%S$dbr;Ug-YD8{efN+iQti*Y>*L$Lu6O6HzSgkJr+v@<5)(tY1z)8>z_d~iP z9WhLQDAwH8_EU$?k6I9P)03`764f{iXr;hNAP}&2rA+sRBtDG|h-Q=dTA8cAd#^0< z6|f>%;sZ-m3}V_#-xcXk?!68$cm>#Z7qIU>U|$ZfFKko&A+9h`^D67$B0$d~;EkJ#WX@0z)1?u4+q4<}r0d5cqSlgGFzZ)~GvuoT zamH9sTQ`L47?dz|2CZr_({>dvrXZCbYS%!Z_CN-~atFb`~QwG4Uhe zJ5v?HNeysv`t<2097EjwmS;9Le08{Ajg&+_pQpKT^K0eh&4O@v6FkL=5NWF!H8mw0 zS}T#cncMWpE%Ab2%UqndXnx9NF=lf>bcXN}#uzsU6}A#&4|oc)x9Xa%ei-KjXq+_~ zgTWXU?C&|!+}aOiFgI{Y2n$p3I#u9ghZpt+&~vj{KhmTdgmM^xy=pare>GgODkuRT z)d_(>c$6L#PHTuX>ID~n^2jkKcevBdgcYu6n2S)bL|(z5MrR&lwQ8LFz(ByIk%Dli z#(@D9Q@g@^hub^4{6Q*o#}Z&WQxFt0g+fcVLq7$?m#~smGOmG>VQSzSgeo*ev)}}k za-+6_6Z%CrehE00Tqt;goTX?sK1nu!>*zLbKvwk!!&hwBu;HJ_eS_vn$#TuV$Hsro zuDt8+d(vWc6cebMv~WSrc;652SL80fAReD9>peQC^Kr z`qvQpy^Ruppp|iPpnJZg#UZQ{@=d0wFuE%eKqp7ViK_hc*-O$9#IlCHpuH!~*WdQ> zTQzC8>RvT2OzL^6^r`1Q@d176e87&sN)G9-`~mZC({KgWX{uiOu)`a7Upkn(wd^JJ z{V6l@;a-m;J}!BZFc@&DeVxbr{#!!?(fG&d6OMpJ>P(<_>K3>c$R3}<7LVuTfJ+Mk z$qlOEf``!XGwr(?HkO9&)o~@Se6(|C&AN5#YIf{7eA4R`{L#@dlV;3awCK*v88dEt zD4os$%B((k(~P880rzUqxdji=0(FAOVIyI8ElyYiViaLoIaoGxf0e5Nl~sE_(|Co-PC z5ZZLhn=J!Z%9|cHaZ-fmL_fP=`!$bU>GAkg0&Wy}|3$ShZ3ZF%(>VrG{0^6NBpvaR z*cjf`#N-f+u%($Zvtir3dHhoO{|*L3_%&Q(e$9PvA)EA3z>XO`p0LX%vgF?{Su%IZ zbwqsB$G+e1_m2&c9Lpc4k3R}5!ZoQN2Z#?y%sDjBRNU3J@L)(Vk6*1z|6w zj?InDM^5$hfT!>4*uA3~URy;`{V1b0A~9v&;=H_h<6*NRD&(T3y(49+Mbi&N*y@}T zL~72!afFmXeW?vM7)U%&bNvBHB6@IYP!l;aHZ}%?EXavkjD(c@lyzC$mK7Rs|R8 z0hcD=!UEvJLg2zpcrY@--GH>Umm_`c4CIj+jnK_RZ00y@Y_{6DenU+?SnLPFMI7-j zj(Yn{@kt1ZTq6Bx{HKA-?_9L_m(c;2)Q_3PIo&B>$u z)Ah)nu>(SfE9_wHwnLz7Y8|{ti@`mwX3y)N_}wG_kJN%IEW8;xQ>JinxS-5n={1~* zOXh5>2ZrhG5W6ebTGf*IH!e#DgH^`i> zP_ts?ilf%M(%?JLps9K7lWu=JY;UiDe|k-L$bS72c>Gm4!&V0pHAfKo*U~Z@5ujIv zwCMA~9F;@3^uHuDqY|?gW=w(^Z-gb#hr5lpCjBo-k9LUkh@U<+%Cmd#*E^ebf3>IC zbNFcY@y;Wj{}nBALc1%w$DqOh5*}?G*Tr;TzL0|1-}Yl`>kkKydH>hM>eG;wbVxH! z`l@sK?G3oWNHe~c_}XFzvx@lEEnY+~ety5}<$ip{|6Q*FrB~CXdXY}eV7nm4kezLG zP(R`eDgQ$eX~mU^dP5Uc&ft(n{JfbW!;jpXw9DsK*g1TKL)!2Q`Z>;y${B`@2~Jd2 z>M3Iq|Ej^cC;pYZV28BQnR`hq?JttG(uh+kCc;_642n`(>3c8UdKi7Pup@IoGiDPp z(p6NDiHa(`-38Hqv8uGCyga0r8TrxPl#JOKvnKk2r(-RnBDLXGtKV;B+nr8re$K4e zh-n93Nr!B{TzbU(M&(Yuss77<@9rKj$6aAf8RRH0@~IZ8GhK*FdS$$;x0w72avm=ffND1rVEB zsI5w3a<9^OlO{!*M(G1x-p+uk_ds(i%&9Ozis?|6G_%^U>}yB&wQPT{vizm_P;4y0 zC3r1JVk*)#FNTEo2gub9_cNPXdPdH;HGh`e2O2TatqKe<{yBk%wP|qG#9+_}0d_#g zbp*QnUN6$t^&ac>561CkF3PCtJ=uqVZwN|aIBt1n80-xOPWAdk!yuLI=pT%Q-x=#) z=^W?n>Nz!FinCatcQR?ARo9VS0FnbDzWdG$Kx}Gl6(P0=Y6!k?Qp5oTo50|o4nC5B z8Q>HNhU$6Th+UkFU8HO+6hCDlUE-QGurIH99wAU`perRRx05Xq`2b}->@%h~ zxy-y-ip&a(ULq>d(ls^n~<#ru=QsgZTha5D6RPqmm?7#d&842=^Xl&O*bD5nizQW9_c!}3JH3q6v z5f?4K5*q-wBfxk9r+O5I#l=bX+e0TsB_8NPiA`f8o|@1L(5}Qtz+i+%N z9-3&-P^2>P(6Y}sUj?v)PIoONf%NAh?X=ZzOjoz6j#=+qF6Qy_1pUaoT>L_*24;|$+t zqHNmGKF{9`%El}-J2L~?hG^Rk&^EvVX*6WGkY=B}{As))Y007T&{mf?sCTV{hAlyE zWf7Tv9ZYbCH2Tz2|MQ--;Ux~O7xo;EN%G279zqIC&;rhKXoH-a!g@@8cuz1$0q@Y`UNMBn%doF2u&>u)b(HIfJd;paEt6smC5zmqK@Go9TC8?K<|&7B;$0BNa=`Uk z1XRA-S~?g1H3fZ2gXp+Wwc?{j@cgw-ZDmGA<)*kXCf5kln7B>v?*M_`@&25WA7>%C zKv>C~AqOz-xf9}VDAE?^GQgp%2=@ecJ9im0^rvv~zX1lm0^dJpr@`1yxbh-EoF2t`}~70>!9CfhCvEnMxVuS3Z_1l#izpE7ef}(cjSFf z!QPR7>S*lUXzZOCd#AFmS%ZQ=OTo7M72orvrJ5)IQGeQQXRs1EopTUFxl;D+UWi@I z;<|h;)`q``%zFb;u9>)KI0XB-Fm{T_B*2M!8LEsPFXSEpy;&u;pShI@BL z4mbh_yo>8MBKqRZN;jO;6z|=#dhrEkgy-0y_Ctr-0)u#Bzej{$9R4|hWX?#dZLAGR ze_>7&c&z~{ix=c|4c%7foj6?Aw1Q#>rRv3GUsywK2Uk&v-@BZMbhxK9}^a}9`=Vm*!!t?uq2r4V| zDJvY_?c8`nwD2rt;T|072N2O5f8KBSElK+85I2MxTo^D7X^;fm;n+mTRrn6Rft{PH z-gZ9kKvYQv2-}3tG6_68sLVVJgU=<$EkRe7OM5bGT&iJyZrtH8I(uWln>YlcWy99$ zVBjJCr=GT;*zKrj0+Thjh{5;rT1sMyg#^?FS$P# z&R6!xpi2!siyhqA`JTOV;1x0a#0=r$x&6r->YIAru*jBrZmoG(D@l|Tp*$IMD*+f4 zS^L9;xZsR~9fy8wWu`N&@1^6OSs*=NSW{C~wPDlRH)^ci+qT)IJO^Tv)oS%g7XMZT zbi6O(^(tGZ(}yEbcybm){Y1rclB{*eUc{g{?qD5m)pX+z6tWcN={hs&S6QZc_b#~s zT~9)8DMRSnPH2kiVLGdVobw`oOLKcVs9ymmIG{IF^&zW`ebC3PMY4^%5Cnm61tcD6KX$s$f2MQS=E_%_ckgblor9!}nTQ&j4Xy8!(xW4sZg+9{ zm;17=xiRYoUQlmQhdUABSFPQ;r}*Lc3F zT>Xsr%=%AG#o3y-BK;h1oqFx${zFGaXm%zy>K9?_R-Rm4NvkT+CmwH*||r+de{0fG$`okZ+iWw+k4<3e*CWA20Un$r@IZ0ESRVfGQ!6sM8p`DL!Y3U zW7c$ub1_;KHj>%~;9iODp#=*T%$#}M&5z6hh}Yu*vIh+NCL$|q+O%Jc=RCXKtyuSx z;|5rd??F7%G;mcjk%Q$<+{_Q607Mb$+<)9YGE4mUnn3JG+t?U29GA%vM1>&2!2#4N zgEd~SkANBKbXRBR*B{o_?fCbu?H_F4vBSp;;c7UVK=FX6!v11`hlzk+N!sE;&S_Ae z^r6^2z6Jz8=Trr8MBOp{`+r0NhwfemqXSazpYq}(FuEhA7S-dtgSsf#*2j&AHfo$u zN~=*9n>UXPhjmu`SfMowurrj0i{i~E0E;MBTUxL^*p9TbX~Y5Yi0uiv?jY~HN2020 zy@-D?z`NrLaE%2#JooIuI;TU{@sx0DoU;K6C8`zq+ciBWpxgMjLtMzUa4pQu+{D~5 z;sD)TCE7dO)0`6}>dLs)_`a5V1PovXc$Q-3cG!6N)!L7FgIeeD+;)^B24QuGO2r z^BgqHLiMLbvq$voSw~^>4mU%fd35^^U3g|h+~g#y0s9mfG}tHvc^(+>d5XV*uwt(@ z>2A_-mCNk~E~Cw@25RA?3Z3ws^(4zAgVrg%rSI63Tt zS#miNq?=W}%T53DEF3COJ!o)Pp3}i68U|e&Uk}V=W7RHpOB&nDCGa`7{WSv*R|0!$ zRgb2o9?U@OnoLWirQFUo*bNd4!j<_*wpcJL#_!t=wV%uxj##~Ux3|5KT(>s2`utJ9 zM44E7oPdvu>kyJN8NuEH47D*P)I#P}I%A}k*K{5{(9zj(sIy<-W2U6cyk>&NhdL34 z))Qohl?G3F;Gb1O=;jLN`QZMO_$}CftkvxZ7bFDGL2U-d13?^NOh`7FMvlcc*pRIu za15OFaeu(=WLz=hUYGan;&Rv;Y)7#})(y~ZFi_&aFflyScL*v~*ce*b%TCz5&DZ7T zqUPf3G9s<8{>pFr;k}CI*RESzx>Y08tovt6yLn3T$S|8=?eP?N$T@lMe$LjQ=AEFOgdznd*EcXOj@pWJz4pkpHI5a&fMaY(2n9pd={^jlY>DX27XxD zMw)~GKXB@J=dt5mohPNB_jpflKOJ*~h$Dr7xJu(c!>9&KDdZMV+PDGu_okgxlOXEk-M@1 zm6t8SgI)@1PrUtsWxtz0BP$Np#lV>}2ZIgdLnp&s?PxOeT!|zsGKt_Hn<%8ozY3xr z#-@au3-7)0wq=XSx${H$a{CZ8<5^YVEt5uM$jLv#*a7xhSm7-1XZ7`;d1n=7Kp=02 zn25dS3k$K8b%;I%)4_R=1@v*0=~z?K>g3vwBBTllOQvS95|A5eC2Xpb?aBaG0Z$dQ zKXa5O9KX3ueipy5u<+3b^Ya#5WAu6V49Val*X?EgHVXWyZ9|^wR9xYyv4uG~h2ndc z3rwRz)O46BIwlhCn_8IIwFt5n`j7m$|6o@@!?dyE5bdEyG7(->39)80;!3AITQ`Nfz^H!D}VhRN(}&GNM;baZuf zb{#$932OB^L+?SjU?DWJmxuaY!}ft%1JUqnv0Xa#F)G2&fMJGU)ycEeIL|N zh#J?Dl!3cX$y4%%>LzA1h-#CW9Xosy8H-xP*rKAg#&=&Wd$sm3i@E-RqN0ap$Fh#i ztJ4tzi##T(sx~NFUK=9_tdGh{9Up+E0s)}7X5hc5QVYC>hde}xLi(FkJVKdV@F^M% z!$erzgoFuWBH(!jTEMH}GC4@G#!^5GWu~E9hHeWjpri$q6m8!OfT7sI*JJkh5%Ng) z%R(1EJIi>!0^rr1YeKLUg%!Pm)r9))TTEu3zd@`9Jj~z0P^Cva?-s zUs7Xm`6jYo3=Iv!k7p{5d_xlbMaFIlja_lx*or#o>^{-FFEV!dxv@RxjSb%Nys>GY z)jz|Kcv`jVym5_ZJD$alpUs({sX(jlyq<)<%5vqNL%fKL9l*&UYf6D0+7GVTB#3G zjC4--h3=c*t17(7@Bo!#hPkCP+&2@01#v3|E0{Ds$9b=})=qkZgQakIf4-jZdPEMOCKMlCRv=U5;}+?fhub6-#j zm@Otrc3(f*)yo@3#>I^ug$rUOQhY$%K|C7#&%p6Pyaw@pE?jf)I9LlL{Q=o}p!RzPN*ley`nielvzo~I;gq)&>_)ZI0TvnK zN`p=fU2b5++)+#JyUxH)e*VguHLrYrk{PbMZ;1%UIE~|tR+f zjtp!erQ>grY*FRww|ST)eNkTCqI3(A7pyNAA(FY|^gObgD?G|^wJP7;dY-apf{(*q zxJD5fr!UV@FG6`DM_Mc+BN5em@~8*TpkBz#@avDT=K(shGOsd z>O}-x7#uzwU=pz&#ft(f?+Cisn}KGZz59=nYH4Ly*s}3dQYLs ze1E2#ZWP|!5Pi}S^D;w9(>R;DtSKq6X%zp##WK5epVK)Xczzd{bn^VBIGJSpev|YN zSA7!P3mh(IOV{C@do)kgf&Kgz4$kn$5Cm{Sv!RQkwgEi#N?lciN-SSU5S!D7p56f?9|FkeAX4o<0U;a)vi}scrJH)q~hPJ~8e>&LK z`jh9d#~W})_>Z+6Kv>6_0a(ROw5^aiCcv1f!wnjY2?tjOH+{;WwbE$-?=;>vKGt9| zhG|qz$o^qQQv|YRVNX^m*+R>(jQ6nvbFl+ght3sSI~QR@CvcWdLlbohV7pKCEX3W} zA<$;SE4@hb>>E{8Rd4LO74t-$O}T~P;3Z?1ausz-u!|b$jrQY&5^h%^Jj{W zu&vxP+`HU+T)Et`I3wbB6pEYAQCrAe!2;|MR`nbh*1zE=^>~TfgyZ=Ue%=c?z>2F@ z&bPx=%k5#*x-cOn2GQF}`1q~^mCeLFEfM{if^>!J<(8+ApJM~Rd`YrPII{QPQH@j3 z96h-Ah>K5Nf`lvh8<(_xG%C&s$`78{ z35RA_k(?$tI_tW-11MYO=^oT*BlPHG6!5obTm1M~eSCauj2`a#NbnA&CVmDGh3|~i z#lXi^A3HuiKF&ao6E`ao*${V2x|CtwAgK5ktO9q-asVKiyS1}*q(iYwdCHHQhBPWe z9MA<%K(1M!KpN);fYFWZP`WKte%wCPdx+!H053JbOW6|7Rmu2MOk^sJt;Q3S2Vivc z$9Hr;%f}GEcz*A_!+Y2M=icO%cz$o|u-@Dl+2|g&nXtykkUJeS zC##>G?UY?4?b29AenfU@!4U6DtT1^JosE3Qod9WkiDYpQ@hE}5yPI>%2ebralD2@} za{vXDpSKTrWL)$~NY)cVBl$^;7^+M>^?&yy1*McH)&93nBJZQ~R!4JE&`W+2w@e{F za2z^sT3DF8Q2I#{)g{-bpqetPnv2XT=iGyo(@syi=&Tg9Q=a6%_^cM4d)TFCMf@^esP`-iQqGPI_j zVfn==r9)m=h*IuVPV6T6tllbr&&K!vh(pQ)6d3OvHjhoAdE6TMnLTvcC=1^bVD>GH zXLeBRYZ+9EE)zV(BVhLAs3PQ3dqldI`_v1=lrUda)ticT{r8aG6 zH#D)YnG)jrA**03RnVaSZXQ_qR4$LB%FB17Msp#;iUo(AOM%ABl6fx$ zDD3B$lP$Hg7j(Z{a3|cWZdvfS9j56#XF4d?Nu(?<$QbEfEJt9cz$S1z#J@+RLYZS* z3*$2-mc22Q)AmOEz6eU(wenvJ;YD;0B##{Msc|AWG;Rgs-3;~fIKFsX5IGJH_459p z;8%g&Yi{}RM<_{qbb>kHIn>rIAt+97ikmUn=TybcjuW7B6(VEfP-83tO7{Q?c}pt2 zapZ(4fq>B*32i)haYKyN7ziXy9u4ac`Xl3-iYrEtJR&^ttr)&l8s05qK@s+jY})g% zca)onBAb$$U4Pqj*xJ6~<#lB*Z`fJC30Af(-*lWZk>M@Obh4$nL%0Su+v#C}A1YCN z>`|()$Zul1AWbcLcot0c>(E{jvcGe?-*pKmkM>A{EA03uYf8U4LvjF-geAhWO+7ZG zNmUXyEgtzp^BNpW@z>z!Dq**1Md(Qj%#RgNv6taP#mKm|5(@M8@Khe-f|&2tSi`^{N;9?8vTI2Oq3x1hkmhYYql)nXWm|BqyLEc03dSQ>FVX z6*V;*@r*CN@j{-2uLJYfN?~9k1L?9Adlf&X<(1jkQ^nbOs&p5>q2+pH!-5sN4WHN+ zQuS|QFNgmjD=Yu;q9+z1@78OuJ!%||^8JRy zFxQ2lP^(^_bh#~3@E<+U)^@miKrl~X3C?FoIA9!bmgvK;ff*0k6t70AvQDRZVoHjy z>+9WL9ck;LWapc|Y+c)GhB+nKvUaWc%Dlf6Ju%a_7I_gw{eV7A`U_+yXH2ZQODsVVdC$juc+iV20(yMhOog1F`qmo1>ygz2Hj z-~^RgjY{5!phSx@7D!6ahR4EX)n7$M;AKHT08d=P0gFGW++7zDLU?T@fi0i7| zr71o{w7XcI9T^vDY_I~kK!hEt++2qPKgCtB)mtnjtC&`3Z^tAsh7#pUHzOD_7K>+w z+f;bApiwJI0QK%7dM@&uNDX*4Ie=Fw&tC#7BQq#t)XP2q^~1(`nrVE<-^wr^k}vDp z+L$t?__VwV@_!D;h-BL(ZN2O%rP68GaVK<>ZimWeU^e{P?sl+NcZn0=V(S(BZMG6rhBK@WfboCtjzM-yiJ}mkHsf|ly zUa1{~sSerxeqgP0WZ|!peZ%QF#;O7O1|B(DW;3}$9tc_``vS8_(NtExxBUmsDq|ss zqD`ctwx;jvDIv^Nz$UZ7;#uP-PlxMN{KZf;%< zev9>dM8u?URPwaR;l2YqP|2Zb&z{er=BWLu%g4t2g0+K84ib zb0{lvBW%O6o1gG@b2D7=q9P1_=uzPu6B+Q6?NqRdaoe|7BSQQA9p4;5F%@{DO__Pa z;zf&Zm^t%ixIeh486LWM=0v@Q=T7e3@g6dPJV8lQxTpUiesCqv_d(w_z-z+6ll1lm zf`Tz&QUZdp;ouF@r2-M;1#A*|dy_U>{9u&9U>;=|6{S;AY z_@;Spe^N$G&0P6yWo`NNJAm3qtBM4A9cpR#;m{VhWsK(g`nS?y!hy4n3-QdL@A%EF zMRAI#GmVw2XG3SxZ@-{3Taa-%=<`u*B5i~yobvV$z!L(qK7Hm{pqmvoeT1R+d3Ac6r!&U^O9ie3 zR2`l_q)unjYMJ&JY>IdI5WS*%krZ951zDTmN$lX3F)|f^SSbNA@{OoSaaUq*W&(Gt z*c%h}CKi3=4RFWOCCH=#A@Rx5(x6f$6{(`nVvq*4-CA2SGes&PFJqB%;jx+<0OLlf z6>bp?oE;^TYw-U%*2ozg1lJHk@%B=70aAqtGE&*?b#>wzAeL7_EUI}fu8BSmxeNv$ z84;BV@>8*ftFQ(e)?f~;A*`k*F)=9#Oms^+psACPpMIUgfisZd#J0gpRvdN`4B=S9 z*|Wgg>ziaTQ0n5QL-(7E>l#*1RJU@SI}mn&H8&UfcF93UeFzDL4#pajc}g6z8JL8i z9^Yf*ZCHaN3M@@~d%NG?-rm;U-q;9Da%c_Hu!dBe5sD)j89Kuwu*%UhuDUEKwW@02 z!nG?));At#WnEOcU2#)+lz;MtQc+%ws_pQJ{$htV;A{QjlaFiRaSeY>1QE>wGid?28F8 z(;)iz;L=DmtyuPM0A|uHN?Jg&L91-=2eO^h0yf*Fkz9`K`wv4TxC^>=c@x+>`sty^ z$oR?WQ&*SCF3)vn_V^zzKEY=+`cNK)+`@wRe}u*F z2iaD>FtSofopzD-VmMWCaQ)cDM9hs#0!b-QND9IsLQ_`ue~gV|lWmGm7F7ow!Qc_8 zleuByh;?o4%EWsicsOYCs06Ce4*XR}S-NcaT1+yEI%q|-mRxvHIWE33&^G4_xx{=-6EIg{`}XY#C}C29GgJ==@J+5@iJh%5 zJ^0{*IXN3Q5(;hK{vh)2!T7pnFO|r`Bnzd#;LJFjYgkQocFj|voLphQ=xVN9y?UdI zBJ+u1UW%N{85wdaX|4vIRLI*!?>bNh)WH>8?w*YiKf#NdOI^SUDk}?+mf46 zQzK7AvQy&ZqFssK7G4}6;%knWuHG;Wyz4xkPETi)Iq@81u=7qfP&P(Dx(@nE7!T#l z^{Gi0fT@&8p}dC|o@!b8;@$#2+&*-wt5BV9_^Cb*D@b}X0n#R5GTDWUHt=sUQV90i z{55_Yv@$0-m#u+LtP!M&xF8m~A_O8w4)H0tL|82Drx*pj3AC%&^HA0bl1F1g&o|)t zka35epSZR(^CCui4tw1zM@=bu|{u>WG9cG7Uf*zimr<}g*y`1 z*GP5qd$;y9B)Ukm9zJ7W#nq0teq_mygz^MH8OAMHl13i;rX}-@KQ%No{A8TJq_B|w z<$?MihF2+MEy!O%_7Of-hE~b#go5vCK3i^A^BQg^-g}t45*BT&8f_$(r+n67=aJx2 zAlXrXccKu4!v+&(LwVX1avNE!dbM4+G7UkZs8A=kw1WddL8sRW>GT=)w(55ignd-# zLUuA?O;}_S1ea#`)gWv~v-*QWZ0t=~jhmWO6kc8qJ?t$r%*Tgjr2Iwm@Xce{ zp$}k{{|i1+{jcLlRDV}qUb(oXSc~MJlxt3Mg?l_mi5C+gurr;9-7%OeSYIaLSRfga zCNkDy@rW-I7lU#XrEpKhN)h`ZhTCjl5f69wGwq16q8Y9ym+s=$kAVjs zdjul&9Pp)$P$?z?Dyx}~%SWY&Vua>Yu{X4TO7b7vT3-GRl#1NyXVc)XxkO$1>bvDD z6QN{4%{-@Q=X9=goQYbtN;Msll$)vqY{m;sv)tOU=U<0ScF6{rTgU*=QEUaTl`5h7 zjc0C#_O?L!4YG42!GJ#ns?j76BwTOU)l|4rJ*aaOzwkztcO^L26&kn@=ZoCl35^qzaN8;NevD;Li zMyrv@8nTAh+NC(v4*QyXyJSESAxuEl8aYCr;CZSS(ZH3}AZG#RDBy|&P{De<9Ne1~ zadK6%h09cFyEJl`>XJvJFH+{UfcMzlw4m!`*d7BTA zjyGy!M(TBTdwMwJ8_&*vfctz~luNs6)~pExl4s+lU!xiaHSug*2h?6-g?MM48d;6S zsJ!y3%}|(ZXbc1z{h5V@h4+agiX4t2q3wrncJKZwm{e4hG)kgOUgFERsiS$HL95p4 zj4F8P2mPmxb$F4`Qg%N=@i<9Z0ZJAvn7pS>pZ4{h@}n#PGmbFFMx(O~(;@rx1$-Pt zEGZ85dajxdHGQ1NbBYK2%?K^xytAZQ3Paq-x5^X}Tz& zRCc?~_TNH_EhPS=kS)9@*hWpSshNHW(7h`3M@RQx0(^@1mI6M3n9WOo@KqUt!z@QxQY#du0%rrMB65Y73zw|}XjxHl{!d6M zRH@$Mp%*IELs5jBX!1)SKH*tqCnsSSCSw;av+-;M=3%i3;n2ij8p>SKg55!{P}h8J zO2J;t2Adq3mpr%mkZtR6iv{T149$_ljZdey{@heq*&I4W1b(UPV$)VRNKg7AQ==}zMD_NQ zRjXNrQ-e^J=7!CC5k0Gjj!y`?N8_&|Z%sv@**Ca%;WMhbC$$EW4n!bbs`}IVPt~dL zEdb4<%B5sEc_?LMamYhd{g=!r7`{ZReSEsP4rQ$sI;E+ocyrtC!@*)i+ravuMJnNb z->HBg>O3BlbyA~}pXh>i&Of*Uv66gAtkoJf(x3yYhA%wJVnWJ2imZNy#}%w-?Qg(d#J9J4rQ1X&YQP{8^UkFRm&9s9FV`|qA?H!F9{Vp&@+!Dl}m!> zc^NzoeOw&77>G)SfykkU)&RwlVsrxZN;;bjWz--=_{HaC5Gh!F9bhz9)hp+5ZD}thm(j zogua!H=2wmPhJd9Rt&?FC*v-PCqNY@E4vMAwNGeGk-2~vroCv zK$6kWaJ>sQA0<~Q^p3fO8xaI`qs;jOWjv6I8XDLzgb_Kn`+}1+3)F-x(@n#HMvUm$ zBcILUBBIOEhCkz=#{m~ybr#~O$bz?x`8U2;woXSUhK#~3(N&k{>y+aQo9sGP6= zPcW;_%BueV2{Tn3zOd)mUq^IUy&9{4pWd$CbYS&xZ24GQefcTXmV4Wo307lHhQ4<*eocJsz|{ z&`QdByi1--L<*X8-tYJ9ZfYzmBZQ&A`?uAFcjOBRsQvIURDB<_)v6m3NPh*xjxOQ& z+m&x0PY{5FD=*Ofxz6eS{=*R#6}8|Fuc8)=f>Q7xDFrV@_RD3uHcEr~0p6ZdJ({s8 zW8pHQ(Q1aEKl~zq67kvLgih8c#u;3sPxPG`aB43`^~#4)Jv(+`QRuW^hFyrqE{vtC z&uA{6Kh96dzn2o_=lu1d5H+FR-vVjf#U&=n24vg}vgmZ)eYrf- z1e25)B78ivgp{9VFS1kJU0j>MG#tb$}GbyYSvGo+m?OQFKDW=!kyr zzdr&@YCrs!HzGa(<`?|I^ztq*mqG!rDa>YTY=rJU4GuOhM?O9;Ir;KYu%-r3B#dM^ zp{KQ}>AegjJh}&$@;wM)-zEri(x5|oSyh#pS%oBn36G|+dR89|MZ`C!Evfm-@O9ty zmi})W8st9+%coj`>xb&zF4V5`Ll=|FQsDoQD*aNr*ZBA9>hDqQK&Oie_nG@o!&PP6 z!i9e%y@}$5zg9KN?E9J~{!5jqwbdl*^79v@O`o1|*W+aNgsN?9{uCYCU*U8(U-`VX zb=QYTs{h$BrW>1?kE*dSaXYJ2=9oyeGl63R2v0#;)j)qAB6<5xBR2T(f%b##NR6N| znY(m4(p|fl^c2(qK;spV{=<>_u(PWRmL1e7bdJKsMEwK2!C)GNOFI@Ogx-!;Y$)=x zIjiJd`5ktJV#g`-*WA!;l}5e?WKJMX&1y33J8eW26yxcApS}rG>RX@g`zaQwoJah$ zZ|`fNG8U?J-@C&vM zG=!5RKs%Br?of=YmQ+?Q z5fQL%G#eOVcmvE}HbS}6hV~*q&Crnnr#7Xy-E!VzNG4ggO&q4GOo#CVNCsbamUvG( zE>koipXv_6-69ehy*ijB13I4!3+wo}TzprhvHA8n#|M~3t%hPp8tg~J3y@rW@bK|I z-Y{xhylr$O@9#P+t`ZPQtcH6N3jLv40sT*dT5|IL+$xjTYbu7lnCf4QfY%cU3RN3q zfnyM>JMs9~1o2&Ye`aET=3#%xo-hgfBTKX?DTb{`@YR%PwI+^DvfJ$w!wqZKTBeY( z|857VKq(?0sm~++nsNe3P~Zu8+jS{d$2|pOX3nD&SdS{$?5Xe~v$^P+uU=aF(hh5m zEXi@LO=%EwTO1H@VdF`aTdtw(J75t6=dyoPZ)8G6T@>Ej~|1{h}g$4t-q z!(SCy$l(BMCBEv2vRFpP=z_id5Z_{>^!hM9=%!TWB8A5m3o5AL_@^8iD7WC@6?B6A zI(DMRcRJvPLyKFH*XCgDH(~8Fu=Xif`{ke*TyO6j#82l*C9<9VF==_2+gqYc@Ut=XIrrs@U#q(jQeL}sqa@$W{uN}sMAx|c-JqRX}MSkw7SQut|{JbvAIMNz8 z)5q(>O)*AtJQCH&_HqUR4m`zFp>phK5Bvxs41(UIXC`P6Q5pk=Sg4+5>I#!Qh1??= z4YWYUnJmDKIM6Eqx61)H;>5Ym&P-Ul3#HW{kd$a>3DVg9UbnKoB@9+?b2Y5qVJRu( zasYexdZ*crn_wqrJ=GnzU}*><~Ih8|3I5O?$yKO~^8((GCDbC3l1A*kD? zL8*9+l&5+DE{U^AQ;g9_kKPDvozvjX$jJE3O^IW!NW0p!47`BmffJEc~K3c z?EtDj)9LCx^R%_gU>GsNpws9Kdciz0N~hy|$0(PIth*OsDp$OX$a}^Gm`wxB(g8EN z$J1n=Jii}a3#x$s-IWwxc{><)YIjI~6jpBdX)r*Eld_Sc>OtMgf3)G4w7&Bs)d$Fv zvcbnEIW_gUx%D2;%#hcN@Vg1VUC)OMG0`t0OzaH9RNxhf0+@k-w?oWe+fPN_mgr*#z_Ha zwERf^#e`xdNFMkqfhgjS0(^!B2gO(ecnB2F1^MgfWdKG+7&_78~?XicW37b{!22$y=4#aq9* z#iiZ#T1CqVsO1&z`Qf-0oCiV_Vngng?oqAS)`h!;vd*vLZ)t(=*~2(`%aH4`4lR$n z3SJ6I%Y_1OHMfyF1fL?gaeJ{N zG0uih+%Fqb$e^;8tAL>-(Wz@|X=zwrYSjhd*l-Y9!+dV_cP?#P)oMBnkV8|TJilAm zDXeb4d#KR#J?*6o1&!n@lv~>l!IUricbllIa5&cQ7i`!6K7A6ZLo?))ic!SgY&XiGb@&RmqY_&S^5`J_U8SGHQLyxPMNkNKjt+FO zp4QeMy#XWu@neo~b>UVj8ji8RzDdBoDK{Kz^Lh_k{rsX})%2AMx5=iy zA)Or%C*KLSWfml&I;lY_;+B*G#PC^M3YQL?Ras@HXua)h$g87*%u>9=Q9*Bolv_|a z;(rte|03oT8^=&j%!$Nyq;rHzAl_89wK1Ehw}F}j0#++>9EY@Z6ovd8WYq+~{ zUZ_oEpE0*8FEQwy#R-U_>}BuEP&8&{rc9r{dp8Ig+${AdoYTIuscZM{>C=61?2{?C zdcAew+Q4J86ak67#yt%4XepnUAc)$GjN}n!-|4ZmyvKxBz70%Rl+M7;FvsX+3lbQZ zHq+#Jcjo5bFwWrJQ~9^Db#H&vQ2QoI?QwKPc=ev%lr}})yPxTsf$WSGy5#I>z zFwn?jmDRd-?OgfX&qa*L zqYB~ImVMcB5I#=Th#KCH`irt2SNBGzfAViZo!ZlhVJD9ka-8-YIT?`rYORYi7!jtN z&OAuZRvr0(-L%EwaJ(uN=-@0|TRR``d&ua_#d0=Sfr%e$VH?!Emx90ik@8KDq1$%gZlplfMqtl4tFz#)D(F;~QanFFrmw;{xZ<}Ivr z-6sg!@hx1aSlYGu2=#9J;`P!w0K_arQ&9V*lrNS5;!Jvm$O1WW<{8PtA4Yy(aixQ!A}oz>V#f_XHgnDLVm6QLwUI(l$wZEdYiV+)>h zs&`P36Xa`6;wli1rv)A(g?5Sbr^M+`z%JRaOXL~Ol5ypjo0}HSHGJP(|JJ(Nw<@aF zR#v*LkhUO{S|kfp!Z*gauy1n!{de%9_BXbsm{r@EPo0rbwytAg764c-O9WN;%tx#q z$LSeX$Qh&}2izPWQA2#?*M8}9ap9&p#P!Jcqg~Pk>x&i6tEMJ%QK6E1LDVlV%FVqq z_s(14uMY2g?rBF`6C(FagNW44M?BGT$Ps9Y?0BD)}l?YvZfHm~`N3j`WbRqV8mOE&ji1 zV@qHBj1<|bWOlyO(mx(JoIcg3)8c2$(dKQ4#d(5vfPHtfnX`zeLUW9qmykHB@0uWw8qO>PBhm0M+}FR4gX!cD*LW-w#G z4q)moK)DbT&jYzS0zH^q9frBpxfvt=`DQg~RzGGXs;pEkoRn{aKz&>IGu!LBn%=E? z_w&Idkd-yqr*e|BppeM`mMZZ%vtnLq>bvR|q+h%1>JcN74GhK?>-}@_w0cD09EVka z+UJ@Fl1|sXSq@LM$5qc(e|gNsAKB;-=R;}-AaSh}VS8l-5POrAMif1id-L3k)N7`U z@U_+>zDVZcVxF%9KkzPy{d+rGy9JZkmULU;94zXy&>`H0RDwmK$ax1h^1(A+QJgAJ zeX`hanCA!2_`KavGz13KFiT3fPkXyPo}7_(sCX+fqF<0a9NBD6Vy5ci;v{gQ8wEIR2;i3 zK1L_`yk5wPsL4)gFOY;0BTyI-mQDj8B8@|p2$`tTn0UG#sT>H)azk{GJgq1mh%`{K zQ>N_L;WbVG!#O6**ZS#)wRyRE@3+`AvX>PaUisP^IkzCcZtKT;aKDPsiK*0lA2L9p zE}LGzARPch6Yd+^--ZZyxu&P)xo?BP)4f440(#ceM9n^^o4)0fmZT#R?{iiylvR0? z;oJ2s99q!Wcj{%yQQudUk^94EYy)>U$lgk)i2Q|Me<;aR@Eg}0I9tpIOF2VskMO&K z)8=A(_Q>sDVEMPPZTu6Hd2yEX01-;*0d-~NPE8VAdY;B(sgM5e{#y@5z z84p1|-i!KQZ68;Hg-7x^U2q@(pOYYRhf2t&Xo!zTCemR4(S7?`JxC&ox5Q#{66U!Q zriJ8GO#28{%bkEQw*8dF3%Uk;HGNW3^U()2o9ddH_apS{bT8D7M_Zf4=VYp?coj~^ zUR;M0PqxD`*bDN|gFVPxn`~xFD^@xjDJkXU77M*_h8-$T3Tzl$ircAnuQeFfLX|^= zE?Km57S+;5rh^L_(vws$Br*tJySAvPs)}B0sS|ub4c7>mHaNLP(ypgAHKo!^q!26% z@>MhgHQ@AP&`ozZrY9Na^i`q-O7MoPw5)G{1BeTKE6g6hF3 zO0|2TO^bHGYga&=(vsFM-z=-B_-Ie-l5E^B^&v9S2(#1*JP7ePh;USh(p#)VeA^l0 zfnEbPdrupf`y&SrcuX2pwhLG3B2Bt*3ZjTH!ki8agMO1RjqXZtIc{fkVxoW>#bAmU zX-=Fn)(BRKbA_WSqu}!%h6H)IUwm7+S18LOX*cI$FRsU4kW^2>km>SynlJq}W?jXH z+qQlF{JKwZqB}eB?a=1e=Seq10kH&`LYB&`51qaFkmd!%TFbW~=l&NK!4{zTC5LKDI{oX(GhhcyLM;vjq zQPCzNmy9(k(g8=MBBP>WTesL^TiRt?ch_1g*XGWQibiF|8Wj~46%`ehTU2b3kxNEK zE*a`%s1u@&IN}Hc3^U(z?wv6$cYp2vzVGA84B(x6-}}DjJ@0wXdCqe}CUC-P`AHM7 zbxJdZs9aFUjq0tae+_t4D6|qiik<-(fY94(jCWJ)*kVaD5=0^xc4|4X7g>Yeqd|9c z#ELOu*J8xZ#fXv5k17<2RaFHA0lVys{29h&G5`2fP&yU!uN?^}*=H2?pv{ui3MH~w zlGFj#+S(I_>JqmMoChcSZV1U7ZbxF0E%{ADoQOzGD`%8-iUSj>lh6TA z&Qq4!$X`Uvk+*wu(V6R1`Af>}!E{#*NJLpMh(#u$0I*Q{p}hQwPk6Mm@6IDN z!)n}WZ19-I2o*tMVZp;X;xO+Qrkn_zNVy8Wbn@h523&@j@EE*M=R*&j598=z0x|Fz zz-t3^CC8_zvar4tMWB`es3%heBZp>N{d{Dj{~Tbxc5>+ej;Yb>Va?^C8XL_VVosJ)>dOtqM&-cSO!L`>iHy% zIDSq%M4Vpd6cY^0f85DNTnR<_21}O7U|DX+zI{1tu?7?~ER~Nm)?i$=?3OIUvOnIt z?55e6LbGpLcJE!RRKg;pvM>x>#sV0TXRY#rrxfu9bbRj3%gEIJb$YnB||I%#J^<1|A!d7Q2&I9us4A6 zykyWpni0A~(jHR5krVgIcM|<`i~y~nl&?i=C}m^aWAHWK)!9|EMZgVrb@`gR)-A-O zR*pqbdk==&DCGox_;ujw@jz)`4);hd%;5*)de}7>?~(KJ@5od315WLPIr($JHoFX{ zNDNV}J8s#M^bBuSj$tpHoj2pUH^;Ra(sK+`um020Kh0-PadB8yi^(-1E??sHLqcjL zv8%F@=FKuEuj*->hkc5?I6Ae7+TQKm9cvw;#5KTF2RMUL2izKm5kftuxIF))Z59*f zwVLv4&W3s6c92!l0D#a~;ULMLAtXr3#{}$D^{yo2H)vEijyI&FB09Wsr@NW2NUkk4eUoRhj2tN{j zD!d|mPuLM&6J9SLEAjJR!%g9XFm4CHt@xYp?csIdcf!r#e}I!-F{_iq+rrO=cZCl_ z`wYuRH~#-MTx&fF99~0Np^?|%N~XgUnL|~%uY^oqkFZJ>bio~wrNSexy$0@*prl>8 z6dRe{V$PlO6x|8!sR8L*&d-(_ZKTXzQFKD$&2WWwZQI)V>8`z=u6_MZv3Fmn`LA}A z7gS_=Me;k|0m~I7i*L@eW=x|DwDC;#Gdl#Uwer#Vples4XV*s-YXaoM3-+|T96Bi5 z)@p*Rr_0>`q#t8rehIqH(@s=p*$48>{1sitdW~AGLp& zrB`>q)lmNwh3)R)oA$vy+y;%ZG_usmI%||PG8$;;_7}8FD>VkY17721mE-nWz z+LN*{H=efmtQ}uoI8~dUlZWm4;wy4yO*I+ynxO;#YHGCVXY#OXBQ5W|`Vy9%u25p|*=F(pB( zk3;Zz6kyZTaI#w6ceJ;A-#2@EhEdWS4EY1D)!blkH5VMDJ)Kh=MbL(GsSwn_g5rdK zTlT1vN(W=7DkA8OI;iU~c`Vmav`DXy<3s)XAoK1C(B%V1dcFREV~7m*2SX}=N{wj~ zQ}qJkJY)616BsEGLMQUW{qVpY21iK5DGY#&!WpMeJqv5wS0T^t+xOBGDVdE`QQVqV4{@zwuiLlySe}f(RA}eb8s19sy ze0Dvta+B`FtG*;Brz8|YS6on32I>2u>GL*xOJ$dj54oOmjKK~$U@+hxCVvPlEBJ%S ztr9_zl4x)}H`#|T^fMUEQAJFL5Tj>T7o3q;PeKE5;v51LHs)Hue zsvB;@k{W;&y(qE>@%#BuqV3`B*caUuxrVE6&cpxzCftMYShplRJ7>W@M0&?MO$VoDD*sR@>(Wf5|LS>{lBH+#3Xw-*0k@0Z&&t z)+XP|4YFNVi03Q9^HJRr6QqbTjdRKI21_~i4!fDJbaY;ZKbOItXoLLdBb%^$S{BLT zJy_?%a0d{ah_Hs}o!)S-B)__HiD+{bs5hcgsR$+>nd7hGDwtjVh6N0dtsOq^N)))O zW~KP47m|`yXye-DDzdai#8$6bvT;?e+^{KZa%o5$lvK)2uBAopgOl&nC!k|M-=ITW zPv`=&GeRF=?d=l45KI(VR@SN~bTF9G$sfh+c8RmwUEsu6^@K9+e?-5mU<}#6N>%{b zyOf||wNk*ZwFPume~#D_GZy|DK8w4GrWr>Le$~7r7u))W5`UFB^hrx=Yi1^Wvrb96 zf;OKl9kcEtY_n-!T8N*>caffeaV3X<#zTsr$#m|uE3GU0`I)V0vp6lehlbw zUM!qrXD+xM+7K~!h*?%KJn9sWZfj~<2TxHH*s?^mwZvp9$(LBZE79SouU|?ojQW?~ z+wNI<6@)V83Ed3WxdX>~G&<&0tYibUrXOBx%3Q|+SkGpWK{ZKk9xlYe_e^z7-L0>qa820!)gkRx8 zCJ4p!@TcKsp<;dA+I;+$4Fd}Zal{`6VY2n9oK>LwfH806s;iZmT3t<)R)vwJB1#rN zQn3)W;I{DB%6=An*@gVWe{Y|M|#}YSJ%@^i10ma+Uz>t%H;1?ccwP!cs9Mw*17~+X+4D8nfhjuHjR;aXyAcDwB8)Pc| zI8<0L!UedpNs+MG7LJ{Zd6Nza8v_ZW4EzK=rTHMPM>7HD6M%+BxMQkOqNKvMj?fM@ z!0YR20dBTK^V*!?t&otEBdTJb;k&yt7Dfs%?(JY+abS-Sh_twDqdupNH%pr0?CfF# z0>rUpir~4iiPP3bs`*h3QIn8*c5+!+F0Q@Z{is?M&p`}0Rb5@ExHz;@d4h7x5m$7M zT!tseLKldwtyb%D%4A(F-a0k#0W3_&YGmnI?j~Q#)k~My+S)KhKHcLB0!o{n7C#bD za|gCGBeBPggmy&kpd;af_}wJdgUGGygNUmP*mH7fYjY-RovOHaHZ&M=^51^8(+yZA z0I^^`zEOPOv$TA~D!w8sp0ltH;g&1`pijctECIkG3javBBR_Mj55|+vFe|4JD+8=s zei;CoF3!_1ALmYui1KkU-yq&-hlB!}isNjU3MsD|4*^PXz4L zM_g7QVL{b3!K@Aq96cI1c61mPJ`_0lEJ&CcV1f5d14LbD z81Sj%j(&zgU5U<@_>uDv1ds<-xG#&fQJo)oWcdfa5G4M5+f*mAo!LOp$?w_R;nKk0 zB7Y(Hi1W(CXT4LhZ?m)Q(ZTH$f1Nd9Cp=W8h^17q7vSUDsJp{zJc5DkPqV_o=Z2Q3 zMN~jq4cT4*AJ)0huFA+}^Yv`e0`@_4WDjH6trSa3?!66s(a)0eW7`m&-ca4>(^9PyfTn+3;EEu!O;E;F|K+hxgHWn zkS-f~KvYo*?V5#Pji(GzXf@@1S|}I{$LX~RNm_=wetw^S0E4O$M~;GI5VX+|Y@EUa z1Go-dKzu;H+{pK?gBSO_@ACC2@94O=<11Tg{6+Xi8PyIx%z~=(bv^!wg2IW|A(WkxMHwO{;vmH z!BYGegMD%m^Z(^&-+%gOyMFBG-hcY&x_<0vuKb@z6WRF?TE%{$h|(O4=ENqycZX+t z+x+pGlM`AK@85FnI~8Azu-|4*m6I6ry~HbxgtR33{Fd_mQs_ASA=&9F~byuBr;UxA#UH zaAt-0Qury~P~Qh6@O2^7AGsSiV573Lj$Fxa4rR?}uSOlvz3d&;vcka8{-Kx?nnx(z zH7>`PI>9_a*xONEZL=ATXZ>`ZeID5A?w*gzn)L{oZ^HK@i8o|KHa}PQ+$-y41;zhPB+5p8v$trVnLqOR#7JLyw9>gZ`vPbNKb2iMeeHU8^d{60Q%KE_&!A=<%9 z_H*o$>Cgh5e}%^V0oK&dYaUu4m#4WA{`e*M+&#kjCm&xu9}8S@6mNC2Sn+yyclXa> zY|xPn_J$dtC)R{V;HFiMP$U6Pz>6TWXGgNbkHba#X?PgL8m0iOwOjzP@5>7TX`hGB zCBgiE7RTeT_kEJa>~Opg&hME`kU}|t1>@katd0W!Rv(`t`tmQBY-U<-*CFTF-EEC< zmabdtb|52a($sTiW?m3%do&6P;&v2EV_RB#L{a1zug$*@_^*(!eN(0vRdesaW8qLe z$>nQv?u!A9++2FITPad@kUb&Hx7+8>hhzHy3N`Gb{`SNS9ca4Y3U?E(l^HISK z&I7z&fC$549KS%>oQnX1tQBYLw7PSPV4!b=qd>FaLEk9cd4$_Y|UlL=(3WKw#ylMeUaJS}X}P+#c5}#*b>o$=RFpk=Bl@8R)?a&c zkJG{)5wFj*_+P+Wkaz$d<9eVYu|8xNE5G%*9`Xb0UE81 zlVF&vRq{;;VkOt4Q$5V9kc`9xXPm5AW6ZpcxIRX6!u^0R9?)%|$7I`UHbS#WJ11aYVqRHtHOkFlDAzXQc38 zS^_LGibowh5pbiQsDcu{@QMaJpu^0HeYq7KdK+{o;g#)>z%1yc8BAn9seSO`{DubN zQ}%DJ2f1i*aYIAh%da#vZtHAo`RIe@mR+BNt^+RCC576q2Ee3U4DK#75P;;ctz(aH z%a^(Yi{+egPH~{CE2weu$A--kZ%hY~w{psFU8HT-ap@38_%l>MNgc5)nulFGeq%oP z&90SaU>8uwA@J_|*a^hA0h_>55RbA*KA>=z5(S~p#upUk!gJ(vFe@74ml(=%-mOd> zew{8KH={-RuAe%^dBw%opX=nc{jCz$gqlBBDQB^bF0>v-c`2*YTO1DSJUo8EOcU)c zF;xNHxM^v0A12;*$}jQ1Utq=AIoDEexi+5H-MwoUDwTQ(|x2*Rgzg$4fxJK0N^t4K-XF+A(LPnPK{?)rSrp=nsjgaHrvc z?H%tpHTc6sVdv)Oh=7Xz01T2aX%bs20k?5E`G4esLd?lsW|Xw1abTG0jf^v` zwjqt*>)p>?Qi@gTJ$8T`&uY-rjLa?liygZ)STf`<b9@DW1oV zLO3`uFwF6*o!CHA*(c=na7g?KRY=-kAEd6^+lQ!WFi2eQ!@=O{ClwuVDgz*oKEd`1 zw`f7XTuppP<;=#V6CVpz-5s*uz3k8PRegV75KTb75qKLME}<|xyO119%a_wDA}ORr;>yPCJJ@?r zGq?0UO4UZWcvZ(2sw+y7OZ+|*H%v0NnY*i$vX)9AoB)4eOyD-6ZzrHeXi;i@{>((t zG7p(w1#_~pQoq33_05c&YZgG;;QwQXO9lpFF^#>)0gL3QtbB98Qea25dN?R?REP^q zra5rSLm9CO-I6(`;75@2`uaN8tLO9r)ZF`G1fM_njO9v@4%Xnpm@|X?+DWg3-68HXCq7F|zqD%5 z{_H$LlIQ1#wr|9W(ei0a%NHNxhc|ny>#V7$Ii7d68PI~#U*ksuK>S{5bwX;oDP6s{ z+jns1PG4_7koGhTr3rPuluID#SYdYQOvrtb)sjm)313J$EkLlJh_TnPwZiWU3z285 z{I-t&-OtJwu$D*{GQWt4ClijX=Q?v?tF&-k8!4ATRzMKSF4p3}K5KQt*y)xFrqPP3 z8;k zFdmSq`!Q$bB79Y#)e*?Bami6e)JK38gel2InAJLcyg|<)SzWJ>7l#9fcX>Rnwb&Xt zv9(5yI-uDQ?p4ZI5jyihjMWVoD?7&OB8=5Vd3k&Gq@}&}R_y&h7&n$LaryZO7lK&< zOK6k(KO09Qg~!N06?^}`I;M~Ohhv%#j~C!-c#XbCuIJW+Ea7_jeF5J8o3VZmq)x;- zFA%MR1a0L0aby4Jsbl}hj~n(4KNxm-hF*tJ{}o1^ceY57DonLBxk&j)%r-0R7huC5fXFk8VzrBX4TnJ5;Ms9B#E+d~nW_y9 z?Azgzt8h?#N}LUhL_Z4-IfZyUwR>V{WN0W9!DNb{C|+n}Bz-D~1j)QU5>kzu>T-Y( z0;V-`Y>)~OkzW+ty&1hvh_5l|eJy&Q7~gYAji{l%&E_^NttFCEv4eV9wp&gO(g3{B z(a{FSFDitSzt_$zZY9!T$JVYM$om>ZIPP}}7K>Hu#(P(nS9B}c=v48d6nC71J7(gJ zKfxU*WM-P@UJX^6JvQV;YG^^h`I^A4x1gh5e!oLug#mXE2x&Erx-PxNnioUzsFp3V z*(R%<>ZB=F>-iVwP$&Z9?~dQJNxyd!KHnxzhogE8wMyl7*Y2NGayk4*yX4~&tc=AR z5S28ZJgWmxBCTftN#h?whF5KDsd&Q<9Dh@_O`tD(guWwJfwWxYDcZd~oCKcVvHfWcZ%^Md4md8Q)Xt$ws}s?pcqUpW&qxNgP`FXcKImP{0lz^h(<)w&e9SbsD9h%r!v-xD;5{pVgp>c%hvksQ{Ua61NjhMS1;9da%7$cM0qjaJ+2i(`~Uq<;tf^+-TSt zZHDMb=|aC^g}4PpsT!_WR8z-g~V2v3E{b=Fl#E z6KhgcJ@mr2=3F~Q=R+*5n=vxwKngsIpIY#jC;1{)E=j^!*HA)XH3G4;UGTCG68=&+ zu>+EWHFbJVQ{{XBT@*P09iUKC^S0L+AMtg3aZnWF##_dy-KL6F8{X^c{OH*S);&Ca7=y*=gK329OHi#oG_| zQXa{sv8%&LYSg3cIuPv zjagg|z?O|UfW!f+New@G4cj+&71u_km2lLuTChL)+8iC-GI$0Xl(ig>d8YMC$Vh+{ z3N;v9wU!IRE#||vsk75)Fo0i2ul!A(TesmU?!i+mz*A6tm28?Jn?61q53qbG_-F4m zZ2C$&*>cusN6Ygs)>_jmDk=&8Qijsp?rLcsY`;Ii3Hp1%`7Eo(%%Hq&WJJpoLs$7( zApPfmSn~TtSysgbtgEl5WYzAYh+nzIvW%T?H0429cd|$0%I1dPhh~>ATak+ur=4rB zT_mqN)$(5DCB$m=Sji%66yAwZD5a;-Kd4(POt_M8xerGJsZR*kSVON7KMvgJC_QXP z&RH()#&u0i`(Y$b%gmahbIYq+%KCRZJOBPnZSAHVK7QIo*%xJ6Gn9UX{irJ_xbCKs zlDtG;`{Uq0hE9P>ib#lQ1K$FB55KHlnK%G|%K5P@@9}^s15&P1gHFdyVz`DXRdJ+c zS20eh2uF4$N;`l%Mq$U!!ANN@+5eOj3A;U;$3;UyoPtF(vk&LkX99~PQCg-pN z7^{cGf@$f~fY^8(L$VF~ZzM_h7Xe=aAPBjS*brzx76Vao6?Y}TI#+T(VMj5PNQRTg z$@yOm=zNF@S^Zr!@BX8KMV{{u238P3UHU(cF4;S$kFF`?@r2lq8R3;@9$}sMEx%WjYSmS0`9lJdF~mXJGj z&$sPCI=$Y%rSWkp0azNjL*;&d(?+7oOpYhI39mmfGXnCflvndabnwwyL#+Ux&(B?Nc{cYP|M*fOCn4CW56*heo z&R+!J+zI$v{Ft2kSP=M+M9h{W`2QpD!>o=g`RS1lTRuE^UhzE(;NtH=4?w;#ZV$;5 z8BH6LacTBs9p@BwH`LVF@nlK}{*QWjH?7x?nUawh;g$M-8-xX>zS<~WT3q*0#GFU& z$TpX{a?K-e`Yl%$U?2Fd9An=Kp?nmn!*2Er+5rc`q(2}};mT_Yi{@Z^pf(yov__{W3a5*#)0ZUJUMv%J+ac^x9Ymie3+dAPB#**e#jRvuvh1|3J#}SDS z`TIg~SYQM(#KxLZN4?$yfiN2dbTvGDycg92<@mi$ht1=m{a)OH8y)J0u7D#A%Oya3 zLt1!=3O3B!8PJxQz9#3$%A9C)@uLvWGzbb5ApacPEBea4GYvS>0- zNS1Sm`#=NEv|}gGEqIMiA5Z*TpjiO+SUea+>}8n8y)@8FTjBr!7y;F6YiW2&V#>zG}QE<@Rzz{jm01C?zI;{DjgWs7ja zJ}v>PRRV;YjOj1fVD$HIP{Fh;!}GUIfAjvdYyr^sN4bg6j}zG-dqG=fnyqno1{-2>l4AM+e%Bq_H@V$OQeqz);wkFd7UT_6JezC=Sjira1Z6>TbvsJ^*m= z10nMU!c^6PC}DbED<)HE)ZyoSQpifjGboUYF)BD&h+%GIE zyYgae?2zQDS0M#(G~SS;aq59;=^aM)&d_oH@eu_+qLGFB_^?w1f*&4tnpDSv?gz?dEji6J$&{d1 zpE%@_^dQ#>jBrfk2mHr|oqAQgVVoKUAq>L=m3$wH(PQ!^s4?9zi-OS>x#uECh3zJG zEutnAJU*<|V=EpCff7I@XT@-T`lJ*U8}fB~_8bn0+A$Vm0z2;8-__BvV=sn7OyCev zki@}b{^P^QY7}t)A>3bYFya0@=5QkJ4^x$%n(j}nIZo#gpd(2eoqMn`ps19+%FU4c zTOs*;>nAVIgX>k+?bZ1MD` zTF=tX2m!AE0Q}bt^yjlPB zYah0J&@-*z&vsy@4$Hd#W8Hm4vj*B;ZFr?~A|sm(imC6*E$w>;vG<>LeOZ|uGe!Z< zJ){R)S{QhLe|Kj;H%i=1*^%3YcY9NR1-uU(;!+#$eLolR>1>3L+QFalF?&9(EIa;_ zr`A?JSi9NN`Od>tRXtW~nog}1CtO}mxr*;1ZrqCf(u=wkWko+R8pceU`wP$(IDiY- z?2^=xVT{bs=&Gm|4k}t0-d-q5*r|#ZPE@;R84^H|AQ}w9K%gI#5Muan?_p$OniAmr zRjUzm)|=9ddM%GSrzZx(8a?(gxIC)t(NeMs;Ykry7PO?@Px$*G3IYnR!5cG*+0j4j zo)rW=9vH4OP?`Oi;{c*?E%KYaihK8etR^?#U~q5uqr?v58(14D1iZJ z1WGofoWpV84#jWSx@d935J(D!;P4USpzEV@b_?X}kB~Fs&B=qDkuRAG)g#p}zU@D7 zSdGanHFg73jC%~?&VvIXQ3%Paf$)B4I^4+efC-m`w%-ruiwl-G2B8)<&41~>8WS*@ z0=)QW7J}G zLuF`tJa(94#$j(09~p&FS0y>g8t#G>_BRHHi0D5t65-r3YCfT*rTMkDKkMv#^Qotw zuf=4n#>rtXj@?M0#=F?o_R13NNB!4loQ-;a8)V%YEd9DJ5?WIFDVFZ>BtBAvP*9?8eU z;X|#SD*>3u0&&|MP!i??>!c7JPQ6_&0nEy+)8>+K|VyEnG?h9_la&a#AE&|d-=10#=PgX&5M4IkXr9Auyt zf~OT5d`?Put82K*SB`V3D3D`?qV%T5Q9>jk&}h*1y(;8yx)m##p%H)_wPBI`FD z4u~vux@UX)mVdP8!j=h&Wpg5)Ik~77ZQwWU7<8(P`i_qdrAzU(*c#W>w-1+=&c&7| zEAn#(L-2Dg+@MNuXYiVb_M87*jYMh@uDv1?SQS zh9UU825az%uvSX-9X%ERgeu5T8Z$+YfjNNiM~@%3BzXviDP$SlXT|F7z{bR=C>R8; zTaJ^sxn@kJTFDKUx|?ZTZglco}h9*;Cj`P(wKkOdT;dVfggEw-^r_^f0Qpp*3Z~Co&Ub=+EdyNuPftP zqv!X2*BYc4{fqX?pNO`H!&B3U(U&}GvUZHM>3==OXpcO`*6;3eCYd-XM@T4@&lh9o z(^H)KiBm^SevYUVVbo$z@h_j7tEvz$zAL$sG5<^E>9|g=l=G$+yoA#gQ^uHnIt~F;oU>Pd?W-qUt3MYDRej^ zrCbLR`Q^xNO7+%mJ~eepPW9T2p3X17==f|mnq4YM@uU9k<_+s>>pTYI%~4BP{g*`< zvlh%TNrXp*A2sC~YYG)r8fdHfABQlWg7WpV&FYdob#F&Mx~)z^4xtFI_19aPLPy&x>?qhgO-8sCs_w6S z_2Z5_6;3F*F#Erh`f! zKaS$;@KE5yaldaUUQCz(aZ8_imf1Wtc@QLoaAmmMxVlT=u&Iz#Xf|lp;gQ6;K^&f_ zkbq1q1P2uTW3?c!7BV~yGHihiTOh-zLyijojfy8yJF0RDjYYla5$^F0hiJhG)o{@9 z)jceig_uVh@|5SHxK1AS%;) zxETErV~?Wj@00xj443FA%&BTdZwS3bvpXaKShq4f2!DARL_NfLLJJ_OKkx5CyzC`9DFykz$>A2Af(ov7xvQ>nTqZT`2byWC+$e!_J&%w?J~PMNcK>SX6XyhRIYTRfVoGD2(#AH81=;q55AA&4`#8U zbkL;-zwsHYQ_UB_Mw2mP+zp_%_V+vEbQ7(4zxwqeTmGFmPqd&nt0{t^1gOh}>c91f zt^}hoWw^8X!!~HOpsk#;eLjXyG$aQ5#}+QR63T5aCs$eJM8K7a&@Tj-GC7he#G8;j zd(#4ZqCs{-9Y&&EE$nVl(%DKc&lsmq8U1H7LgM+d_DTm)&Jv-aVNb*fXI$^sT|0N| z7qUxjXOMSPSQcjF=3jZ!(yL+AtwT7RfRs(ZO{~Z74(P0yUiy%d>&MU1$^&G7C9hXF3sjh&KiNfxDvVM+`Zd2PHQS+7e>HEVr+ zM*~5)EKdZEA3XwMoWO{n5_x1KB1?}O4njR}PTj~5JJbitlkjlBdFq7AgbR{_jr>T^ zzYlw!?n8k9h^jUEG+WwE`JU-vwi z*Y2bs6&-2P-?|d7xVgCIxpz9c1`u>L>XOnkGNz}ecOs0YZ0zb`4cKf`j*-Orv2dNW z{p|D8S?`g+SI;^ej(5TagJlvHcy*w6+siL)?r3`g=ZJp2zF?Uh%Z>uHs1ugmI79Ec z5^*m-JBC!2^RXkGhb)t9`Orfms)glqlMo`+BhBR~yfRe@zn_Y&EgJ`LZZ5O@z15M@yyW|^?AVOPKvXEBF%v^2f(Y$eiKmjGKw{D}Do>i>>&;4il#BCPO3 z&yC!c>R8{{*3sqDnl0bcT`Isc;MaOY8Tk>6{Ha>Z80sBDY(gi%d*8RO``hE#zi3s` zcx-4wsIL?p8aUb;`ksc<=}`+L6iG~maDv%UiSi z2wrMV)Tdc27V{Xe+=Wm?XlMkKtyp13j{E$E#7MyVW#{$-fnhB)qe_o`6cOA<)=>5; z56a%tFRIAvdPvigkfz^3^W6w(x)IWZJbFIVTzS;aq#b-9^2cn9vz2Wb)+cxCa z)cyIPYhG$2fO21STu zy$O?2LSE!7@(WMXgT}%oSDc~;Q?UEa;LgRy7KdPI&ICZaM*QQ2*%$tiBE2m#7(N33 z{U%u#a<;Nu&^ByR-Bom+*!IADAjOnqq?77!j4|1NWV~tif@{ibwu*`~X+%x8_r$oY zoGZ!-q5jrG5xy-y%BxsW6ovRv)KL3?Z5Y4&mlvd`#RUM2U$*QAD$$Ur;gD_!i{O66 zk1Fwk&amoK)tIC;rzL9ii3!?xkdPpDCt1P_-f^`~pF9Z!-Q&$N5$E@mqu}o!0uKYZ zX748e2XU^#v%l(8l{iapn2?rY&7kXugfDSC`qg^C?Eb!Mc4$d_Z1L=7gJmqN3rOr+3H1BLOIeOEs1yxvb(!kxi+nxZKEZ^zNrW^Yjerk>FKPVL|NVJ7Y`|5CRHEg@lazsE}05 zI!$$jdJhkb1`+QVJ_di~092hCju{S>H;(uCd`En}A*f|9#gtC!FG;{+%SQzp0dF$i zNw6zsY1M;1&u8shzw!06IKOY--ow7Yu$VYuvSsol=r{&oVKtk{P2$EX+64f$8CwufNsw?#BRl zb$kf+@~iAn2DC_avO4j%GY!3u67d}s-@2WB;i__QgTs<$u>S-ge~Ex^tK+~=fzGTn z$Vq_Mo+k;qre*6Jb$M_t8fCy8J;{gE{;eOr z)ASax$k)D(C;5WVDAe#Pv0eCEzaiTWDDF1-+1_SX(WAtvB~4snJSZspkAt0e&;`UJ zcfWylJi);MVhWe@B9FzMBn3}07EfZd@GQLgF2>dZ6;Fyf0TP}JnlCU z_ZyG`mm|ZIYx?f4ysZTrLb~w++!Cyqv73X2i8FoI0Kvkv)*rUwAtnU$ zqA%JMkYMV%yMia1qtr7`Mr({`e0r!gzF1p~!9$<{m>mfm#eBP`9YO}E39A9gaV_y) zd*$IkR2=42EAz_F?(-Rq^0QMC8Y*JMo_!qdHsN19J6GKL-7WbR8^nq3%C%cgxv_~# zCw9bc?%*w^;164X=hg>8MyDV*;~^H`t2F=~6m5#~+!Xt&K}#qzYhbNW=BGhrlsYz>NSC-MPz8v12=P;x@W5Nv`@)S5%@^j7}Z&0|hsUp@2eO253(GUP6 zL2p+T07t}Xw^#^TEJ2r(g%KYD+_D0?c8xZK$M75CRXidX##F4CupLLF)G%--s5)mL z%%zI?`wkuQVZ4mM07nE;|8jDH6T5GM`GInqLJ8!0Uvr*MFeu;?K?ms(MGu<8BcN4DaQPUX{eH*+Aw5o&gM4}SJ3VT^XF^5a zpxe;i{$UHzD}MY6W%#vkxD4pOi%y<0UH-^#w6AH;t?d=^OAiJz#i{8c_;@dmV%f|*cc7k92z(ZR77CPmF4kebNP-6qz)gli( zhQCM^e0lZrgcYL{vo-Oz*ecglzt&P)`{}_g5TH2od6}7wjhnj;co*U!lfpeDODvO~ z2sIH@hS13VwkmLT>V*_-@3@NE& zjecy4At0#51Gyl1fENdEa0OD$4hnz6$gG$W^7eRp#UVgBF#9JM4WOqOLTd6*XuzjU zh(}?Ckx`T_+tu080VWSU$t&9NfV^E%YHbh0M$LwNxS%I~4Lwl`Juz-mzj+*7X2!J9 zj+bBUgmbOq#YXS0sUXj)V3mfYx#=0%KN*h`^rq<=&71YfS4Q(B#p_Z)6<9Hue_jW$&v-*7>WS-eZ7Z6I@FffLa0-Wj#jU4TZ6FKM!=N{6V@G@ zeJf(9g_DLt;y0kcm1CEY`&4yZi)Y2bT190p@p%L!XOc$4A^t4U^A>rNUAfq~ysWzS zT0pc5i?OTt4BMQ^-0wi|u$Sw`_j3Rd$H58oHv5LV1RHM_1stnKL#sPWm!#-v& zJIo0%fr$e33jCDCO$I*f0OW2z)Z$MNPdo=y7Z<@)x`><2U4UIyvy{}(@Ya?cp2Oxv z2yCvnBNdLa-@`|7pZN4T=Y#8zjDceb^mFC9N1Q;KPkqia@w!_}N|!9f=}VRb4P|9z zm#I5uTr;f`6g8_JYzd_;!q|d`4NPo*Sq65=xUROpHI$TGmeP%W{+-t0aJ<~*VL2rz znnISZJ#x43kD&EV;+{|_^j^xi^QXqpr_UgO`ruRG8F-|kVol9^-q4r}uf3)8wvwWP zf|8Q9W(?f>ea3UIhj*eHp=&xG5^tFt+*WxxEH1^$UIdo{u};oL%|R<3w7ENEx)5lF z^9`Zij~ZMbJS(Yz;bWLT&ZKbgh_~lx2m%^sFrtK%G#N2yauH)If!d1Q-IPO)FMpr+ z2umDq0bN6!bHczu#4^712Z5?$p4~?<7ZdP5WB5>@$Kz^%i|iQ;+(UDLx*r%0Mbwmn zJ0_X;f4l$CQQx6Hur(e!+=se4hmHn$ePWU!B?VQiKwK$M*oF_P6IAfD`+R+qzy*{* z{Jmq1rr^MFKN5MxP~SoPS3ls3DC6_Nvx*(1hUTWIT`ZzTu`9q_mrHI$uAu7jUCiuK zB5-=V%eL45_A?Y|J@b9sa&-2Gclo!UIAgp1Uws0RI$(%T?=LrUv^aR)FgaMS3yR$> z$2ck0Ece%BeC8Z{hA_y9cy>LWJ~=qhP#*&<=j1`V?*$<8FoD z&VlwUL3^%7du~H}rlCEPwzPfz6&CxiK5yHy<%72y8ynyLV9S=Cw7J(7U3b2J%TvTL zfK>~}FSSp;pJmu!$a;T+Epiv{Q6IM+6J;kCDx{{ch?6-pg-R*~|T zKa`iea*%<&r!`^E$Ay*nN(RU?4r&|ARpMJ(K>{yo+G6t9G7c=yI@2~Wfu`j`bz4IbHWv8f5?50ef%)qR5@am++>nn4Ji=eZvM_e-Qg$IAw@I3NAtd+fg|TC}Jk? zE7d525#srfMD;L45k_0s^@s(YNq7Q*n}#Jp@zBp>;e(rU1oLR-^&aF`8CtL3ZFv(p^ce(%}&+Voa_ z(Ho4<3Rtlt=hAUby2R4)zQ_q$e~cBgpUDd7w)R{RDf)My*4MF-S= zzuo=%#2>W#f7==!t>M4-+|kFyoASgLTa?HOd2=S?i6`TU!Q+a>4?+f~x!_>4=7AaS z9oV@m9jrYsav9d>_B`1cyNbI2L6UoD(dItGlQp4}l5)xTY=$6(G5b#gT}sp2t?;&Q2RJ=#MgJYZ-|dePQeMlt`m|T=Ot?0NqxshP#8(j zCB`Fy&W#{PlRiptf6AdzE2z(0v_Heb*oA2Sg=l{&+Mk$!_G6a7oRi^Mc!XR$LK8oW zwNs7iyT$f>O_TC3lb>OA85EiX#xY1b)Tx zaHd0BRKz?D4W6lNLi69Z2ejs#s|pJ*HERRgn?1V*wP~}mW=+rz?4nH{C9T4qr=Ngg z#n1HOo-kv&2>`bPySjECL=Bz^GjvHnXASoE595jPqyhrw$fyYo4-b!mjSAIXc$#Mq z0g<6%$ag(9#MB9>5Eov>!b(r*&=Usq#Hq3?KSON^x9E+i(#Qt^dnW%BqOoZM18I=n zKqASVfUShVrOY~X9&`h8qS2#j^r+f`wRXh{)QNIoZyqTmUz(U35i!X8%X0#*Rea`v z5pF36n$aQ8+Y8Z>O#GUPE2QEIRI5%N6Y5jh7lrXGyN7uCDaQFUe@|Jhjja=Ei))Kf z1MtF0`#x!HEve0E1;KjrR{(sO!?C@$Y#Dm74eX zA^i?koKRZ=Q0OQ7CS90A%?F4Ri(@_<4e*`cc;jt$tWr-w3)iz7bJ?ay0qQse^g1_7 zsc&z$+sn#szvgW1!S`VymzC9{Eh#!Gp^H`zjxbf6PNy3f&`}e?U$k!@e8DI@$n%kk z4Gvmo64C24Zn5#Fu?MD>0j7=@44MjgkQg-K^2@hu!H|2sW^+tK##Whsr)f!Qn2=*K z<(#T#6~R$@jr5I(ULzYeX7`bznd1VXBK@F?Sz@PJuCX&=?Uu;aZmo2NPyG06)0yg1 zT0y8Tw_CE|(cNqcwF51YNBO@yGlu*+^_q~J|Kh5#bt%?g)KUp%rCWk*f3L0o$+_Qu zF7ebJm?5_`7#oQR=&h;ft??MKc+AxVoJ&~{_^NkB)2&f$MOrCj14QapVUzUJIT?2B zdY?cdP77;s*jT&d(B>5UtSCQcu07k$%JN|~5<;v4zEzYyf7q4|cD33)d+Cl611hj*4-VJDn9@a0u^yh>95CN%`LR(8nbSn_|23U&g zSgk6D>OV214XvAAZft6O)(xvlYRuW9M(}5NIK_unVR{#iYk zr-#uTLUmz;qDIAuC)kt|8>c}b-HXQ%bQ zDEB`xj?+;AC(1fi#yIpZ?%fQrI5{NJcR!v!Ityc?bH*WaJ@(yrA^%AJ!q5SUN&^RH z+v$V%-8i}*Q~ETfX;gBrMr}WOBsUk7+vv81yA-7$kHM+u(@dwi!;6>zFnDo(yM*&S zm^`$p#JBvJG3ZrVdE;@9nAIo(8V&~p2>1$6T5T~18nBhX3c{@5Gvka7u9h*>)uz+h2dj9>uF z`HEvxK?$diUzJe4^xiwm%iBNi?&)d=JBvdI?ST`X4-Q3E@{KiX=EMJYvFydaDDrcD zRZXJ{K$VpV2oeq=QaE@5Dh`II6Xqw15fk+&yvrzDwGw}E@}W^~r81^wjOh%t%8XW- z(JC`qrB;zC4Fs#cpc^&Tx%DmFNtCB1OZR!l=en%oJOm%A*`vbU1*}XG&j54#9-0#c z&e=&cr~Y&#r*tZcLhnBo(WlMGI1ez|;8E9U|EIK>ESPw-`BWJe16fPi9H@9hJ%ug+ z+mDL9-1o5+_0&&rfh^ZbobDomM0_dNz5xAk8Lmy$w0?^Iplp+PU9>+==@eo8$vsRW7app7L+__gxn1K=1N{|We`E0t zKPxWA>kT)cYrgB!)6jno6OX)nWPiqKH3Anv{2u@pR6raVJaJloo(AX}e_FqH?>!pQ zrI{ycLIb_u{Ob?Ny-O=itk1I1XT<*UKlWLLU~V-323r9!y(#^VKfc4dx{`BBqx6Ew zr8#Hych%K>tz0!Zad^f2*+CBw-R`=VxH!k(f| z3S#2f`khXZzLU=}r)rloNakt!M$(@ylRr>DNl-sI7thJL$KCAnH$r{WMQ9N%>z)XZr%X> z=|;V$Gx*ehVD$yqX89A}8-du>V;V%Z6~1?_?17eNMC|OCwNEQ0A7EQ>Wh2AR2hMAZ zFYEwGu`0;>L)PHQeK^i1Nq{L$vcO(*v3n55bFoAhLtb1!ewFVO+s%>96MJgKT*f@p?$z6GHci;fUT^9_fTIqqfx;jMgmS798#Pxs+CE;O6tz^Mf zC$E_ny(aO5#M?_-T3YP*E~RU7n>G=A9!F%8^3H9FT~)8w)-72Ax+Gi`CR>P1DhCr4 zRy~?mv^$QC74?i0S5(lzEWlO$&*)(z7WE z4hKen&+O2`Fcv^drbR_2R$8nkiw(P%W>?%m z%8Khz#gcz(t?Pj}-7r51@dSP!k6-$xq#jc1c=e#12R#iX>YHqsi|vk$IsLZ&e4pUix!jBQb{iM7MI%E z+?$n^olViw1i^LMzkcB^{c7O%A$Xj z^TK|rT^;sQ!POQ?ZLv5UzA7(ygPNLK+iYP!Xd=)Sbd&NbmuV47%gamUpX4@ncguAI zB0wstO1lb+^8rt7V>vF>x^)`|{VHEO(Oo3n^*y+a~LLsv` zU^N6$-^O6k!=)zAA!;=t!cJ|qnp%uz&%@4^BAy*0{(x4i&}fUV28|WkTAQ2Dc+Oiz zBO6V?LZ>21A3E8otUH(0vRb~oF$WD*73LQgc9p_1cg3T<>&zWNt2HBIF`CV?slh0@ z6!LeXxdBtiM-4WcLjk=7&D94DR@cLFd&!<9+!yUY^_J6cU**6@px}K`q(!XG>?mVw z6Uf;yeHvH^7+|>nL*3f|Hd&tg|974|d1;%rX$d8ifB_>`j1aNY>KK+r5hvVVwm$^Z)&R|Nr^*X`8nv&vU!2}3$}`@DPa-Msl+6F@mB3W+vIG^aI#*98Sed7{`lI{Cgfu9BBZ zo$AjlW1Ws}rEV9z2ij~DyST1b=rQaca`vRu?^-I!WP6?OZWwDp(Ya3s~f8c7|g;59#k4H zPXru@2lkF9zO(zke%Af7AMO0+6SXxTy4Q9Cn;+BR0lxWMzwQvsoXmoZs zJD@^icVl}697FUjr?gZrf8DJ9MtqnMPxvkfPK7Mqj#H_;Ke;%IXGOs+b+xImv4 z#q;MESZ(RNJhDLKQo`wPeHQTQB2j8ih;hFF%{OzpUrAa_egQW3FTWqWqQF>kesSrU zg~~YPNv6w7>pY;0kq~L$4afRkDvFEUH6PqR{836!Jh>)l{jC z`g^BRD*qFy)RCiPfic9B=yT>&Kv}%xj!NOb?5;1>t-9{xH`RZ9)v6UG$znBs-YnOG z#qPy>_m35)_2DD0|M6(}%_Bz+eQV3tAKm`!cXz$`(>;6X;S3uKOAtJh^6B28w{45(nG2Ky9-s zkAMCCE%!aLKm1z%A3|L_)laNj_l4Waiq3P+EvX0;48Cz7;+!_aK5-)YyI=GGNArsZ zO<$^9zNGr5FOV*6GZyIWl0XcduPy=rRzuLs7F0eQ%krrS0IuKZd<=xs6PavZO>ds()|S zp~KzXhvjpLVi>mBh69scA61={5v1hv7tt5*lYEqIY+w2%B_&y}pyo{$dfV`@jRkd1 z?s3rw=?#{0PcifLkIFqOz%Yhc(QfkoxOof*bln?)CaS%Mj(+)z?-72CfOKL27pZ75Amw!M`B;+)R5uLVMn8&HFzn`{)}xIvQo!=?3#--BDOjw3`wt4Tk>yGp@?d zPn9J7d}T?0V48HW_U!$2|C{d&zVmwDFZZr%`TV~uXD@37J}M*o9#A%}yYqjm_#<7o z@slSKIwhfG;>S-~%p{g8nQob$Z#EeJium(q^!r{rL(m6T7JCB1%^H-{8~exB;yH?! z$UW^2ruVG%_uc``Ub{y$yGAgwWLJr_O3ZNLkzjtMWnWq)3~E+0+0(ygnP|;CTq$y>XS~n5*Ohvy5-gLl zj_iS%dY?dPm&kG9m9(#O*n{$6a8N!-kB;=SSZSG*7UlZIOE}p$aj=q!wUoCYCU5tN zC8ST#?MLpa#VPLg(ZZ0(UCoES(3}Vv`pwOzmIqS?|GU&HbcW*5ef$k-ibBbEAOJOflmg>IUmf&VB`+Rnm0r|Wi9x>jk~E#fTZiwb#?|WYSNm`Rx7YPGO%0Yfrs-_q@fbTw^0|#OyjgMzmOJvm=>K!i=4DbdRU4o zJz?isXV_EO-RBHyqgG_qHg!Y8l15wL zF|W~LjtVP|Ok*rzO-0R?U;wk4NU>b7uLZp%C{sm(x+4IOu@%K)!N;`TDD=iiZ>H>- z(ux%f63Zm94ZB%2sO5xSJp7uWNFHDNLm$=2}KH7AO0W_r}{7M`ZYpicHe3BjhoN$gESJXA^VDfm#@*nFyy=ujZ6>HYqeEYh4Wq65S_{$tO zIUF}pDM945?M*qAyYIaoQf9WHg{3(@eK>p+wCl9=QaO2wY;=!*Hm!p1lu(zGH9U>pudf>IMrq2Hqjx5XhsaL#AbVK>$yv9oLpt4~37bjy6W^TBtvhw5qe(%OleR#pF(bo?jR!d660QQ8p z3YQU}^JVpG`_1mU72K~oYXIhPn_A~K|EitjwWiL_AHQLBfArpJGy%ybUZ9l0y*2Kl zH-6kn`aeof8%@GwK)7$s&Z72zlQtO=f~(uK$tDx&+Y5_H*&)VGyi0kyW5-S=gL*tR z8J`KS4W%K|8Q%;b!s-6k-aL`i8LYGBx#pD?s+mOe=xbWN8md2v@w1S}R$zZJuTH>7 zs(_pvlb-63B<_Lwg5Ik&^zit2GC6Yi&~M*5OSr0ij>`hInMluMfY<){Pk;Ed&ZIw^ zGCStaG8xCuB+}{l(QMCvEsrHpBED|4oOgL;#rXw(V?6xIUefkX?xs^T`DJMGGti{0 zS@L&jQXMvyT~aFZQ*3*6*nwL4c9|F%3! zoQN)vs&bp_uh8Tq8D@c-*%!+DXfp5M8*2Wv632$S%hf*uSl55!k7~KJNw*q33mobj zf0-uD1s4FtpYL=MgQSt7PI+>YJdHn~#N)vOum2UAEOi$RzVgZ|Zx9DB^F`_$?vFI! zQCwYB)d1UhNFKT`UHiXEli&O+WO#md>c>t*B5xl(di34Vv~gN#Y3YpO;(SB)qkohf zpZ@CdmAokT6`jlLFU0L^vr)D+ zkvoS?$Lo?-GFRzLE+eln`KOSPUS3#kH8T$$IfJ!jFmmuWq0oT??4YM&k=L^OOz!YI zgM)PJ80gFMx_)mLIwK|Z{Ht(aepMTV~70nVLJ8cySqtq>KEI#>YYyE@e$x6J}phG zy^K;hvdY(Wjg8W4)QxRj^Dy{$!>2ShB0zTV#WfEh>EcKdYpOMwWTX7AW28UuxPoFo zuaHe*pLW*1z!QEE-6}IyXecFK6>O!P9UC-G$0xKtG}DKFp%4E~AIj;&we&%kR(<8Y zy{FQ7ixysDHKq;tqfO~E$>hR?IqA~}4*cdg^{Ux?&yu!%P+sY)O_Yhe<*Z%{HI zX!-J&LF2T1Y2(e33&9Yie%487V0Pes&!d!Ef6U`P@l&#eKMy4F8Syk`%&$}PaAQlu zxuMTa$i45K2(c!BY#}8Lz&hHK0I5dUp@i-+Nprq}36cOz$P7;WC`1w$Wn?}YGqs8( zsOBt5(iouO2M9ENSm?g#y+Cg7>?}DZWg?jbGX(Sp^A%@~0WeMjm*+By0^NcX4H zr+Q*e(dPpY&Ux~5fHeFUSj45I-|bZzMdR}9a~=2537@X3+OKvUEfv)uW3#1nI8ooy zvW9xr(rK`U)1{Q%%_Ka{@euFuTH%bqlYYG!k|dM zQfK5MU0tcvo;{lkJ4DE43vCr^uAS^#H)I3hhzH)YW-G-@x-}b?Y42`0M^0Ik+L}Oz z-(Utdsj4_JgwZFATX|=66qzBuNg1KzVsvYVwt|<_-y7+#tN|pwv@_|iF+8$6Y&j6t zxX_W6zci$4`l*@AO5%pd?bA7;T-(HR6{PLYJiNMlJvC6s8qB# zL*}JEQ)6Q|T*;5dnk+3X>u=*c&!>q;MN`^LU5?4}pFG>FTMy6L#zogWoI~8_`QUD` z;`$)_J@NAepo6X1Vzn|5YtVo(uUSV_%l;a?c5@DVOJ z6nX8H-hzVE(9d3c@x_+ia`whAZE4)Il(7O~8{TR9Uk8 z^x?w?y1ToNre{@ERV}^hDjRT-N#_3-hfcqyq2XUvvE#i+z;cMc{~{CJcX{QJzV{m_&HFCv9aVvI7yWSHEhB|X@r{lAYf zrly!qYEV`xGXPK=rMe%b-uY33?>!Y}l(mbW=LBT|*O7=AX$>ln_)1|lnRr|zN<%>rFe}Vu_Ux?vk;wWnLngSm?Ra^8I-;a+R?p{xpI?sYCUz za%}Z4D!DgSP4c3f0&^&Cd(Eo)Ps%a9eJvpljNq#JHH>$o98QLEhtI3!H>a8mC67S9 zQ#O#24HzWFx@%$Z?SOi+W7a9W#&r#HbZRT*(_0v?TPUvRWogUpV3&9-@RZk~BF3r{ zMpaCt3EG)Z#Yp7X&}pF-%_?;i5>Y124ixzyirOUQ8F1|v>@mC9PKFPxlW7{=}gj5B{hcD zG?r4#yw1lkr{eA`Fk{xj#+8*KE@2jB<%vsZ;dEL!gB4ZM*5-DT1$6s=wm;S;pSyM8 zm)md_g6B~v0_xY9Z9YRq9S*;b2TEF6LZQaS=FhBc{v4_qyN}*g)k2fODJf*bAY*03 zXXyO_2??GJREj5b3l@A-Yr7Q4`b1beUKUUDz6@b_mUrdxY@nS`B}t@ySclJ$OU6)J zK5vc&(8Y*r=f_&}TXxkZWmosZ;lt^yL9VBL2a$#Lr*)`OgiSY-E-X64I znr7gnvV!D;J(RLQso}^l>1riR#)IAWfo0eWclniBE1R!n8U0w>xB4<5130{aHmk#- zNo$hvWq*Ls=^oS#hVK{ZeUn+ON@x>J5_1_}BW*HjZQ|a5Yj8iPkn*-IvPA)%3zZ7| zSwUHO74KG+m$kOKTq5DapiO8UVTLO-`jmH0PiAyg{5$)qh^cgDZd11~;}6JsrGVvfp9Ly- zJP}hIF0%iuH+o-+Ek=N^6!+`as>=|f{3VN}AGf@&xp^rwV~$7qp5IYXF-)3^x_tAC zn*nJJZR{FdP$7DJN=?l_O~SBBzO+~z{^|4E9lCdpMxq06lhW_>>6l8TsMCkKx;ka? zNsj3+#fFBR_5GHQTs@DhfTR8WZ$`B8$;FGKwiVH`jUw=t`r$FHBZ8>4S~D zGmRV~4~cA+!aY=jZrcip4?X!#7?BLTKv_2#vI+Iqg@U+;YZmgT0NXR0c?}(>F zaOO%%h?gv^tXb}!mhULL$`y+pewqE-RkwZ_iAM&Ep@9jjXuN9$%7jd$^A@wxjcxX> zX}togy$`%4l{2l{u`+*4Ye#Rw()igg-u;Os?xt#F9tkmgUObYszG)#;Q{q{&q#Bjw zB-c7CerQ==>r~?%?GHY@qo;p+HBqq@*zBvQ1K7IrO~3JU*dBZ7iMA@D%+<{0>eCU= zr`A=mf?~8_P2i*SRc1kvHn-h~3{Sv8H(UR?xv6r=qB#a;sB6!j=N=`~c?$$3Gwdt@ zWpa;h8@WX{$V6)#U$h3dJ+*UJ|LgCJ7dhS44QtmDNk-HK`r0p`Cwc4>+pY0*p~G2I zd(A>;(L}tj_xF*&%U`|PVks`1Gk;E=s=PZ8O`m^J;TZK`O-5tIz>!Eqq3k!KF>3Y+Yf&*}FU&O2MbCLhsyZ0SUL|*Ufc}>smRFwF= zEN%2})@{%Ch^ofr7dX!I)GUbad!dbN3kF)SyVJLl>01CPTux@hIx;Yorc;^OnI^S! zcYh37=5sz}&z`4h$eMc-F)4RpN4}G1q>QnE)A{wV$iGl-q<7aqjRWGBXf>@+Oc$1n zXD+*w4=yVfr(f&YBcea;?CdmDFS4Z1zSCd84pcc^Ot0i|@$fH>n9{||*4Q}Vr_q3} z;ht(1&|W0_ABy`78(OY+FTHmqdv7;v)~y}W#c@_*I|9U-rRCSOY^a7_pJP4j6;J2f z(@-8q{(dX(BUvT%nSy&7t~uXn8y!4-+Ii764fiaAiFNDC%Rm0*>$$`Z z!JJoQD`k>rPA@QP*>GmtZADm{GpSLzUn_x^zjn39J(oO@dPHEO`!dJ*^QN1{sdvKR z=tQoyps=_w-*W1=zl)qWorZcBc#Pw39vV1w7UPaNx2Sl|l~omG)2ExKUwU=U~x&F(N0m<1=~R+56T7t5&Tl z`uPt<(5FNqXc}MC4MVCDy0()|qElxs@)wrchf7Zk(Yx3?!-pd$BGRfIRPS&e9)6)3 zX+O&z|3i7BvbqYCB>P0bTz*BdZDQK8+Pfv$@1k7MSH)Ahb^p2?w?|gd-6x*Wf1$2= zPC%LFZn>6S=rs|aJPwIRbJmm%Bjok`dZxB-7|P0O8)xsRp1jp(NU9N5{07IB6%|&b zsIfCXrF21=R&LtxWT-!!`=v=^jp6pUcQ}iFgl9RRHaFi=+j1T2IJ*Zt%QESu&SaKz ze@Cr%SckqjSz5@x{a`#*dL1$PiwaWlBR}fs==jMovwhBG)qxkh#mebpV#XZ#0}U7n zd?&T{ECVh?16mo7-x(#jWMq#WJNE9$OhJKTPDvnuD0M$%f$E|aH#OSw^%yu#Cy5fx z1TQrj%@*AWR>Pa&QEAi8U=gAKV;a3PoigQ;E97KiG^s1*js@exithAKK$c&Sp~^CC z!{Zu{y&7riLrx(sfF`7bCv9HD&O)WWFK0Z@ zV?3)eD>>>5+rR$9u08+N+Wsx9FQad4L#DZz9 zO?76DcASUOU(F#WY#Cc+GnShli$U#p6IEm{M|Qn_M?4bv8U~bEkyF^5lF9IqBS+qN zJ7QJGu+=8XFIHS^wFdIJqx3+gWb{}%y@}paOpu&`^NRg>x-@*_Rq*a-Y;BC+lBl5o#cy&V+D|L-vNXkiI*DIPCQ_Ocd(%*SGLwXr>yV`tWS7kb? z+a>=81n`@X=*#tb%lGhSEibnc%&_zDk;8|N?&L2C@HX%jC7xD&I*<`+96MuZGP>bQ zw7kJ7uux`gFJ-j6H0-PZB4I&d*8XI*8lLg*QkY@~(nJ?Uw6^M(SR!Nv+@Pg&yhYzS zAnZQ9r7!A2Mq{}F#N<&vA(tg&n`zN+9}t&TBX~Y1Nm)FmZ@NGnSVpPTy1-03FkBo= zd_n$51&$wu+5>t-$^1VZuLtFn&!%wX?93;17Nm+4t+BU~5Lmn_wvdQsZxtzvt(hW5 z>p3rF?Z_C}DmKXEg~n|!ye(#}yn8ZTq4BPjv&7rGw;_5Jbw2aM-eECw*_yR$SAA%j zc!$u&%^&E6bYg3jyzbMhiImx|xtk<=^y73B{eO7lctYgNSx{CscbZX*4fY4TiYnll zT7gMYp=imkGWU@d1qqjm^qcka6aJIq-B(n0;oK6V7=8P-{{AD8F_Gse3w%Sb;5fTK zOJNr?PtunqZ4T3*Fd5^Zx7yp;_bSah+HB#vXv)jd-Jm3-b+=M<@S;&>{$LzBw&-Hk zT1IG#&cz0}pSTNeCJMi)Au+5|r|POjrOwK?GUc4ThlRO^ZOA)3%Hvw+kM4lWmeJA@iS3Yvy=$Q%ZuZE9f8YYgNM-(hSQ_{ z1HS|GuwQXrbW78~8#;$$31Tl6jzcG$mbduel>-CO)m(9@aQpnzRP($`-LnCNSuKPA z#qGb|)e`}xo*M8KheGd)rY44H_ltXbdis)^N)fm5~E93{Cj6G+M+ zQ$XQ#hJFN)Y4@Q zaw7GWl}@8Sub@~R9p{=!ydFNwC0yl3d}e3?&khDe*>&UuwP;e-OZaKwsVF0RnLBSz z(-N1{>0Vk>T~>*K@iQN(T>7z=jmy~se*%BrgIc~VaKCEIl{I4ETiDVRza9_@;iGE6 z#r?2eGRC zdfj9c!IjL!d2u6766Z`~{FP^SS6-#W4n74cx}z^}s$J5rzdU8CTwk^CB4XV*&Q}%V$e7r)qNFznRRn=eo zMql4vzV_UpaI9v`Z*sd!gZi@hAG!Tw*l3?XS3Imqvr>yB*`a3Fc?p*;C3<%R($H0t4Ee? z773DlNxwR}gj@zQY1eU3jc1NW$QLTqVuu~c)y$VvAI5u(%*B$b0k}#Fl^DhhoTcbh zjN@v?v7WnEF^*M|qPKfII;RM`}+-U zpf+B4nC_@@+lKq0YhPYh*T7k>H5ONk3y7ndQ(5V^yvA7nPB?F5sOz19T(`TncIk!A zva%&jtF`g|FDN@C4pC#NOHw<#fR*?ElUH2`QU58t=5?Q_sabi`U09R@y3xcYUGgk7 zFlDN=ghri#`O^1k%pt^eg3^}?V}CXY?ord(B)zWgp4e?TXYb# zo?_6dTouS6@s!EvvycG*1_WV)-AwrkNK1TrOsJ+@iiQ~R{RUurn{+BmK~Vw}>hddk zi!q=xK-`+_Rm+TA$Bak`W*IZ`VP@n4U@cI#U5yR>9_vFSQeS_CIU3r*u-yOigzeI$ z)w_4UNzAd&X`tnjzQ@xxw;OC^72b%dWUuEGtbXUd?yiyG1(wwDER~mB#Qx-R?bz{) zgl*xPdoYw=V;%WTcX!HIXYp&=sa>>Es*qaT9m_ajdQbsIK(lzl>4YMy0>=wf&H0V|=^vY~NV_0nZ^Ya2dV&QSUo0qH(^ zK(A0eRI*&3umrtwY|hIFmYQ7htT)>nJJs&vYB~}bPB1G}v!m8MdJ@B^2AN9orKZ$; zKSl_ECPA-a)Te#3VJj>t+`!;`$A%-3Ge%2%qQqKSiY71gct@mAtQyrV@EMW7q+l@K zmz{wrZ^J@nKw=hVGXv9i?2w+J^6Bx{p4-{8_m#e1VuuDKE2_=rQz?L}>U>7)d8M)oKp(NsG2sZ zv2lh8qyk@oO&B+2`z5hAEl`$>0j#B8($conoNcvQrm^UYawivk zdwcu$`}$LujZ*#H?JWKuU_73kO8vgKXD6S%nsB>qFzLF+3*EJKE3R1R zE?e4s9iq$EF(3}Wm=B?c>}AMzGw2~>S#dm4eeE6Ba>90PCZtT$24bDJQof+jQ?iGp z%z~BF#*Ty-ullLaXifzSQ+SW!r_My*O$syVWYfMpBssjK3~@4^2v`*nIet22m~JQE zv`D_KF_6?akprpdsH~-%+KNL?Kc0Xe=cm}nEydHQ_JwSc^joq%Lc|hy5rXk;{H0A>O!QMpDYwOLg~Ji%6pD^?)^t!&|lUysf{$>pagTKdmsG4oDlUh|(Z zh3!waeT9{cIXNBf`kVf?#85U9Q%dDKL7Rfo?-1~tq&8h_`PnACpU^) zcE@j{CQo2?FYee%HGeElL7~)mZ?7YbHjs8)fhYP3QSS38)iaWTVwUFm^`ESf{@cr8 zPnX-1p@8P4%Xb=%N8Wxr0=gLIf%7`e7MSh73fn;(%=TZV<9!f;*-ZH+8Utwti;%AI z<{`Edy%>G>Oaf~uA1jOsajBf0^qUnkfDcuYCc2hnWF}%QX4vh8B~~8>R(=au;xfDT zgGP!eGNKcH{aH1CO7fE^$TG&u$#_Y;m5kHScXYJcWdi?3BRNId+ML}oykKhx^MVFz*bQm{=LM<(k%4O_V?Ou|2DSOtEdo3Ie>UO(=qDhu`ZG~z> zaD||7OIlXk9$brU#40_aCs;2*2~N75N|_OGA2AYHBBf#^M1j>8RDus-Px?2fa_?!G zj-A;|!3OECW*+Y|$hE{Wx(|JowZVTc-@H%3ozG&1c#wEWswS`VG5Y;6`h7F~mdME^ z^t({=fIt4VZD0FJ+qMTcZFy+YY6=grrbD~ni12dbzza~51cWM9g3|TCY!CsHexqwS zTYhaBxiqfj$x3&yQ1g*D-_i0H0(y3g`}*R|)r)Hp1%ILGTK8y)m|1+u}n0S@f} zga2*Oe(oK~A@-q)v@_%ni-~~T%O>t)J99DWh&iWI8ObqVxALEwJKa7vpD7T=B3l9V z$OAz!@eut)x)#Yz^C3D_w9* zfR;#Z(#HR<@=(^~{|O9&#zWsTe!thA|7{*B)0wkmRMRwTNp9d#VGNk3cxaA~(oj(7 zXgqRU^3VtP5FVSQ(dE!qHMDgTv?XQEtD&vAF;&y1)Y#!35OqC=W4Tu~G&EG0V~09X zt2z&n;LHYZkJ=D0_i6Ug=j7I{e({D%7zmgbDT@0EeOC$C)(V&sszc{8!MT!VAQl7> z%mzegP&;HaCGxPH<{>qX3}f#`>q05eBkr@S@^Ch7CK6lKrQ%6>fu#JRk|vg(-0tH^ z(!7h*=m``Bys@Mq(Jlb9G@Al?WPu4BSpsU~wb4ouG~BVQy6T2? zD>NPKWo@T_Rpu^rVuh;TTrz@uZJXFv(Cy2M3QH zOUN~CFJ$B43RKFGqSFK@Tq%cT%?f%Huz9M*qE4jMkpv|ZF)J7i=>#&dVYba)bk3}x zX~Sx9F$G1X#&OQHfSCj1hPKk{n3Z~FMb<{DVOFY{l?$a#?3rh?7TDe^9&0-B-p9RO z?^pL~N^y^-6gzYMc_f_vlTuuuDaAK9QsM7qwPJ^+6-N=%adk+oI2k>s6`zxTO`omQ z)Z$7_Ee=cD(fFt)`Wl_6#raY#{{B%h?-T6gQZJ6j>grZjma~sE-@u~im_$PyrnY*w z#&_k~37&KO{Q&r8(7gT@ihSMtMJ5Mr)U*kmOw)`p)~3?Za1*%DOtq8(y<#EQ7u02e z)oi5rG;Y!2>GqQ8#TN4fBB~C(lM5uq{W@tzV{C~Cmin=pNvDjk8Z}THk4MPdI2@;% zU>t8vJbotWv!FN~e`hd!G#XE2s8Kk9x|6+*tf7!5aVu1!CR1j~{3o*>EMk5n8cfDP zIGG;@^MkgFDEpt^?|O09uKwTuzW*h;<$*N@rb}ClmTLMPgdRC`w^avip-@v(^QUVo zuerX?<+`|Rh1}9=eSQ;uK1Mki*UZDJmtfYe$$c!VrM-fVE2Y9eXzSodljgE-i8jTp z?H&62gBe8N^rJPH;|Mu+WaleUFz+LV`2&yJ^}Q54z}F?MNXSvb>5OE@*QWMZ;b6@;g1dB{txxeW9}pT-zS~VMq#x2u!n);mw1)DLL)4 zdOYLg-swk!Yj58l&0R=VF1NFQj?M`zXdaB2oz;?MSJbS6kQyu9 z%4-AbK6|~UjSZSp?;!1`oHwl(bv|pT_gOB{6A=qMN^||z=`Q!RXm203Wu$51 zx8ak1aX3(`d99zv_#)Ff@5Ti7G{&!1iSsPJYi;d^+@4FWUAy-BtCuXPTl<+7GIDsa zFFzv4{DhrSn&W>exdr9`aI3M=@sv-Wij5>Uu^s17f?96fIqc30%xeA|OR-5kYs2(4 z4i!><17$R!eMga7AI2NBeHb&!H#!l5_(`4WC%-hDLno~cIZ7%|imxfN;l2g&_^DKE zd?axe&SEuz8HNS%l(ZifY6uxMff`|VJT7^=p#C&>K&BsmS5+`<`_1TM?7!nA=o2|) z{XlA)k{YBhcrG*PWG1I?-|louCjX<}U;V0g&rg0cSb%w@mlifR`}2FAgG1cknjk7E zGult3(WZfcepq&u$Mcc$n&lTe&XOLloRizy{Q1>4Bb5Ey21|}f)#a82mP3_UTLJ`^ z-%xvP^|G28&xMzkEnHF`cu+T?mss9um=F+jQsc-zWH>X##h5~tWJ$Mr%!|gLmpr@v z982LW`>Z0HFpgoTG4pvc`?;QFG}W`k(5UR$)G8#(yuBSNEW60P2zvr+@MnX09`-8H z6F^#oIgCj)@mYHKT%e?{uc--R!0%q``yC_(1%B8T>ysIDt0jQb!X{z&=_+oRGzN$p zfXObCLQsy|T~o89tnA{I&Dv|zx?5LP*WC)iNQtKeLkuzym{>K<#EQh3q39onApmA* zD{)9VOUf^&mGq^^8|H#(j{HJ;$7jh&{!e_D=HHfiC?Fmrcm&7REuOO0fqoB6C8(7O z3+Aj}-`~H1V%{*0;4*f>?LirSU*^`dC|w=U-H_Cgw)#voC=x-);W=;U?2gG8d>M0o zA!B$EcaplRjHM{5DBBhuaJLN%xZAfl2L?OP6a2E=S-nsDkCMg|2aEa!*1O93*EwCG zmJN|IXCw9{zZUTB(@qGG&N)*F@6v`yC)1`dbmtyD9tj}bp>eHN_*KwLX`^sB!CwlS znZqtQRx7k0r1DydGBo`0BoQ8vJ!q9F$u%mNOAqGKgBkQd#_>x}vC`g05dQ|ys$N@P ztE<8-I(=TTpVjOW_0q(rG`r(uSP*UIjf3v>13kTH6J9y-@WM|12W61cHvU48T3K0h zSs98~U*KUcXb!oi?Kp+hz{B|}Di?3W<$DZnHnYHGv}lSh&!a_iXi-7`=!6)I<=NcK z;ee-AhAZEzJi}ZfVI&e;aG5Iq*+uGQ=>+sC`~Q#%6gX#02xpODu!lJ=3#|vcuK$mf zOtr>TXwMeqpK4N!HO_3GQZZW%m?OhOQL`b(UX~?*+FI>VwVpl7kip^aXYu(AIU@S* zaNK81QK}bTl++Y4<71G&A7j2ZFR7~bS*n8?kr?{ey^N?aEL(Lm#)S8cxXi;p<(ntP zLt~@Mg*#q)ruDl?0_pSWcf-wn`UV(H&=iSucJ}wnblSM8_0%N68mX^m7xC#kVIe`C zyL!1M*hxcVzvwm~6kv()J)BQ6I18;Ix!|A#WIycHQ-(T9G{4Pila0@g`qhlO^v7IH ztE3e}M!J}KhRHlJ+(Q^GrS&rF%un`2Sl7qp8;mn!-XXU5V9|0l7N!YE+qUfw>cuqY zV)YN(0;ckOL?dhYEma~Y5A!YM(AA3J2 z2*%o&)Jq<6)^C}JzLgX?Gi;^YgW`*d9`!J5`k^!<_8;tkUbsKC&(9bs!D=te5ff<4 zV?yuJ&cQD*d(C>ojN?_33>k&#$I!C~+}nNNGgLq2adru4gF z-sYVB`+H-tL1xay%suf$kA1TMiw=fIB6}plUMvV~_3G_*hjBBH!^5L0kCIbYai`_n z>3Z&T1#^R>BJ3<_`vQ1$ZuGzoAkmh{ft~2FJNIHafp6%$x!IqjOhi%*#)Rk4@)D|z5)yZ(Kp&ApY1x8;EJb_5u1QZ@5`_|bMb^v?@ z24#yjo)V8Fqq<}z^^Y;0wT$PYsF=Ob)Ap79(P^k?rm)i)RyFVc&pJA&@RJzqBU4G| zb31oF$tdq4uFU6~l^`Hz4_o2OVH2k^k`u1wjC3`12XQeOtadw58&vQaod_AGcrD*x1)MXml*T9^L$=wQFx?3a(oKFZIpR17BQnQU$MHW@#7z^`0$NQU%Ew#X&fEwX-_lq za$Zh>-#U)jTtbiz0%IM)ClTkpUS{gMoFyajg2pd4t}0)^?A#CI%F`kVr9OfD&IDlQ zPAB5y!SfO+#56H+N|qy)`#h#YWIB+R*!_%pqs2$&RWmL?ET6_aA`!S+GCsCRNvmfz z;k5A^EX5LpDnsxrreLup*MvTT70OXyW_2ZSS^Ly6(jDL@#e$y1il+CIr&2ZA>;c&@ z^JX1ROJS5AAawds^mo60I6h@7U&!n(WOij97#V9KktrsJqcRk-w6bx)wxnm(ti`E% zf#&Hvd)NgSFJo7Cw?|k8Hk-oXZkH>z4rztA^_U71G7R186-_=}Zw~-%uo-Kbj}Y#jzcy5*F3$qZKYQ z+O@TfO}E_A)ba&n)Z41q$$eHM3S$Lq{7TDf6yyC-cSz;{ce&;`an2Xx2nptum0gK` za^vdC6}PTiE&RA>+cEEah4E^m>j^k>81n0YEl7yyi-;;g!@9K}SwQDEA$LppZ5ze! z(wmB8xO^gU=1}D5>6B7zK|SzUQ&xy-d@P>e6KpgLqYtaK)Fv?eVcMH$#bGFPL47Jc zk@giC&r*q#%DB;!XK;CLY-c(ZN{0<57~e3d#J5-6C2Z4U+Nn|`nHK9`**c^Tc7H| zD(f@oCeB3rhy42aPUp3gTidLQVF&DfVERWg0@v+9R4!PbD-n6FEs*EAFjoc$s2xr04csJbG%vwnT*_rlGK{;mR`6DVir={T|p7N`%Yn0G=Z0#Jt_Vw$VZ@!8|Y#?)_Gfj_^ z03GaX0>=q?nP765*^Cl?t;c{lLeZ6!uPB{LWO6NYVFIR^))F&zH{W#4>XuH%hev>* zkXVsywyD@K7p6XeH0tOFp;>u(o~uTd2sQ}Klz{zDO%()nR93zi9|du|x^zNJP%JxX zF~ex2-<%*1TXDWwO^=SI;<)?sZIXBliu5|tX38mZ16+M(O~+T^Km?khn+~ReS*^$p zO^}61+tqWS37JjX=%}r=+rc(cQXa_?q{I6=*p4XR=kw>X1*T~_fhVLj z()uaLXUU{EIyzqHkBam8{`n&MYDZt+I~M0sEC4r_I}Lq-GN(C;m*8Fgpx+UT^Kxl; zltGaE-Eg@x_Xr!nz7xg|Xg{7ic z2e3{^EW;xR8l8zmVrGZ-If{99}Mo-5{;9}03VJj}kWk2Ia6QEV_>w>F>o-<}(ylN_KR?>cb z27f-?!sNYavGqY~Vf|cchpN9HA5A|)F zXt1}_YUSE$jyuOb!-W&fbWe!JBQ$P?ZTViuml|E()uQ( zo$~U#ZsxrUbccwaKSy0$yNUV2K2@Ps};IIo(g(mV;S86v%Imt0VW9)Dz9R*SnmvtIFfy;3#&bf!s-=Qv17P-q)Pfk}-M zvv$=&i)D^GppQf*>@|$((}X@}#nP{^n{=x7iHKP5D~u@8`OeXEO2E4jt(g)~Nq!?wifIy*=DIlU~f(GVTl(b3DDTQ_) zl@nCU$nm647oP|erm^a!QToyeLXuDvI44Js{;tcl3?IXqqsLRINndXI>!!bME;o<< z&ZEDYz-VdhEMM$`{^ysMcM=7`HX!?Y(JYIl%rnQPL=%YmnSeBjv|84$?K}7eJpPS? zeQU8gw0gB}{{Cn@e(<-)VxvU`j7UL%G;7A&?8#(q#ph*8j1CM>itICj`VTO}Coq=F zIw-g_tn|`gP!z-#l0Br!;&e6u-|@4lQGVQvN$wFSbtgPZ5Jf<-L7o0@PXicz4sa@Umojy z%I;brlGkYM91$Z*X=yUB)dbfmEk#TTZ1wBOtzR%{8 zcpJBh928L1oQPAdRlbS~U=XTQVW)O#Gj}rc?LzL9%blpKtjEZ#D0>_s#nUB}nnAa+ zznaTaEMPsezs<{q?-PbNiD|ef&*-9=3uZ9^Q!SU3nm^F;CGAVvL+W;Ws7|Qe&E3sq z>e|7wT|!(#dw0Q++6?vU*40lnJEkHrTe8iT(laJC39`$MkAYSjXthDenkeK+WhQ*U zKZ8EJx8xpm1Yp+E6g0W+KX^VM)IUEznB6@pxwf<$UCEXIhv0atv?!dLi~QSP4CACa zxO-2}!Tv+Pe0KW-)x@XXO5|Hh=AU&z?=m8Zs{bcoc&EnO%orN}()>s(basNI4fX@G zH1)0QEJfvs4sBMwaz>??$?SUNI+?o96dh#0(Iro+IY$Q{ICtvmS(@pQ=T=N!{ZDQB ztCW-dR=50C<+(fj4N7~@U6#D(eD0Lx&{I8lf2*|Xm;YI-D6jPXZ%y?(dzUuZs&((V z%b)e)FWe>Di$9I5HoSl9E<5C1DD?XGSJ`{U>b-qG*Q(`0C*Uktto{dLJgSIrUYTjk z-Q4lgk&$@f%{?8Pt669(L2-RLb5HKJj$OY-D13ES2QhtjXVyxt_&*uk@%|Z|qJ!*u z(bm8R>EQ3Lnh$=%2ebYFeNNpcO9}X?e9|)o<@`l@oANQ=3O)Q^>0{2{zyHAR2mAYa z_CC9#v-5#fc$}KaHBdv+f{$kYiB}uQ5^{@{UbH2%A>X&9ZTmN#?d~6nM*j$Blh&aB z%YDlRR`h%KX8ZpmZ>`cw&O4@~25G;EX^_#age2>O z@px-B(%bFYPI|8-&i_4r+o|!GS_j$jc;7mh+H?NqdeioU?2aZI$8c(2`?Fo-{rf?u zJn?UG>i^Ytb;&#YdArVW_^JL+wJY2IE%IB<=XQ|4+4G0wxvoi`aYVkDC;!Qu;qQ|G z-rtjmJjXiut-mwE**5K$-}s;$@K;)v{l<3rji&eB!|a=)ZG*_JgQ-?!`QJ|YjqH5< zH)e5%{MOoYqxE<9=lAp^D+Sa4_m9>4XJe1NewLTNXLU~XVCvj#5B~ACWXGsoUVHty zwtrwer>_0~=eh5np^$8^dvZ>hPHjsAR`bCT{O^5hN+*)p3I0KyNWZJM59{nYT`S7@ zIw@Rey4+t=in8-7J>KOaYu%6@Z&_8<)I(BFyXN4|db~R{kN4X>Q|T1G^*&q|M$OGFaP^DVu;dPi3mZ|) zD>MH-W9L9lMa80=f#+85stH zKiyK8tDcqU7zzBreKsZ?;5fB))&xk&41heMSA2yNDa&-)Ok4o1n9k{x5v9_ZJTpIG z0tucP&*}(MUTs-Z!^Eg)76Pi@j*JY;oO<_o#nV$>?FUFYn{(vI(P%WWYL!?Z0Smid zF>f|`snJkUUQX!>FBXE5`?h*93ydn8C#~H}8TX}(`-ixL9QR8Z_Y(4}+OaF@Bfs3S z!{f;t{1H~}Z|?5O`m%TKL|OG(+V`jDdX{6T21zyDZ!UFLtf;O4RvRn<_i_M#%Sn~8 zH{>NXJ4qIEiNn53nk~rgDLvtqy7Js(gT0LUuloB({pJ&c{l7dIYi?>nzS>}K3ms0V z*7KdtLSP%jO64bp-{_T4Jk15_T4sA+Z_e&2Ppph)V$P9~aL7b|4*^ShVy65an*k zQs|VQdNm{UF-EGMky;d0<`T-e{ncm*?xezq!x72Bh?yUKl>oQJP9=J151`}c_w4x& z?&=o_l){l54~H}_*2`haSeD~=60RzSak)$4|MCdSwt;V)^i$hXk?=tM8n-#dd3nzM zey4&}HECV6s%GHGX{%$wRajMP8yao|Us5qgqQ5bJ_@=pTz4g|MttSBxGI}}D-+t>` z-x`pg`18hwaSZ|D#hI@QF51%i=+nD*bw1Ym$j&`IzYT>V;kOug?8H{pY`du3bCtV5 zoFcE!p?$la-s+blsYMlnz`)VGNx`7{fXWq*O#nJiL`O40YkXvkD9`a>pnvfsE{6n0 zPLL?(0$>2R00F^bsL%14fOCTNmJ0=B>n1$X> zgnoqT0NHglAF4kes+X8FnWK^X5~4`+N}HkJAQoJ+*NYil=jp~94c!`Xka&c3U~|#n z;C`DeR$t%Jf^{t3*a(hOH?YKKZf&hzvV`gxuFW~y_mE@5O7waM1gq}YEmz;8IhI69 z*z7}!c%D0V?EZH97%=#ur*#ghNKxJh`ZQaUc6)UcXmMY!SZzLl?$y(6Vw8;EAy*Ut$ z_$=GEOR%fcX#;1Z3(oUyex!5v?j4VBd+@tm2VNf>JRBOp5#}IlI+0LL_I7>uD}G`_ zCqNBAG{Aly){Q6b^Ew#?yv%RPh$^L21}Om?Gh0BNo=E^t#n2kW5Ugr0W;JnUD_@r0 z6lIf3+S0M_o2=&89C-m_b8x^UXL0 z%NIC{RdPvO;W#V#s@;8aQ{duY;Ql9H?vKy%crJr_r_C!X8}ILbhp_6mU)l4m2mP&& ze7|e|vB9DKeS5z1nDi6lb2aKW!P%rkSjFWq8jGLxvxa#G55}qB#qlu-u^!DBRc!Ps zIk_#wEaSlk(t&3`5oJz^Mk8J$aq?ZRm_B=!Fb+A?_$_d@sNfVVs>EgC*1~635S(t- zLQu^5(KzvSLS7;Rg4l-{Tk%;r#SA9wl)yDAEHW4&>&*sB3_ge$5u)8>fDKML0xK1EN#8+)yNcW}Q~AuNN@?2T16xxGsJ_BIk_cv(2* zK+Xm|$f_H3w21~B|EbcP-DOz1_;1q{{X5t}iB4_Kd(Ss><1r>2Y` zI-5wQ#f}|6?vKg}n&+j#kM;Mb9akZ7dj-j0@&KjZI3*; zb9eV2Lx+01c0cnqZR1$rT2Ls)!ni)A4~_t{O?qOH2&C1^`D8N)z|hBoQYtbfz>Am# zF$!$77701!gtQE`xA=6%3BN;)MNcJD)QK&i2ITl)Y7|gYo~iexr~)JgLO5oDyT% z9Y#tEw?aQRK|l4-&&Q#kg#*9d^*HqK-M;?u3y4Ymkz_57^JY3+^Q9LzHt-8cPy50# zTbZ+H7*=DV#l|AL!|I>=_Q1eT`06VV9*7ry2#;-Qx_-VjiWB!>(zdiIu#gN6^>CG? zcBlEx{@NRtI@uc&LkTq{Q$>te%P#*YRhcfa8;=borp?osP2o&yqWb{r%{ONOd_LXY z{%x=Wy@$ep*|u;0!NEZVOk?3tsB?S3?OuGzjQGI5ula)yegmQ5(QVsah8%vrfB)}} zr3xe8!9A{B&@6>1@A ziC`YV3yMLEPUOO10$Ks)*lCy!KDTnpx<(H6cHc!f7p8$_^7IDe|+yVXC9eM zCPN4zh8Tw!V~nXqq!?*RCl5lzG)--^Xj9CkTuUud>ZM+4xq4>K@DRg8MT;6SrIAuD zQlzm`ikM=G5s^k3V;Tac7-I|}hA@PXOy>N)YoD2r2@kcs|NH;l-{Q0Q$awRw={ggj59(4%md5O(aw!h#*upe8Gf zt*!C#bdsYz1YfRZX*r9i@>c{>;tnOxv4Kv1u2~PsAi5F>fR)Q^1 zNg@G%2dNG>dj*5!MZw`+@Wo4R(o3cXOLmKrF?fedZ)o>-@sbUC$&O%2)Pp*yW*E_4 zz9d485wsbf2U=(@(ZWzWx9BB{sU)j|MMglS`cSLRXNMpSFSSiC1sD0SQWsz*ywnc8 zRN-fp3iSk-F!V%Sc-w}t!X!W&)`Owi^m4)4oFi-doMplwys%7Y&#sS@dgyF`dmGi` z(vr^Wqx5bIgOEy+mT`1R$8bsSMSD9W-v}!g??wVh)JU=x)Yb3SX&v=gNODU3Oug-f z9Y355++dHfr$cI3N(}4$$L`0hy_i_RjkHRq`lz@#oI)$TS3F^xZh$32UCe0=3Po4c}3m zECd&V{l#)@xE9gC#$J(z$EnXMT%Ijv<%Jvf#E;f%ub77ODl zX|dh}NZ%;a6NHl_>GdN*lplTwLhdW@ducnJ_Wo86h zNKv^$Efh`|Hg82ndLrbfSEOcCkT@20yl7n<9IunFOHNFPOGrtX7^jYzH?MF?Q{!<6 zl|F22nsVzr(Ylrnx9RXZ*rk4DlA9-83%fe6>H8TdnTt@^URZYS1wo*u{*7&dfBZQz zibL+V2tZ~yhvu0f#UYDK8+L8>#lhS3yF)bQE&77jxnyf@0J+X99-dD4_1=z=8a-jaD98Tmhu{%*O{ zjNtT#Okj)1N?X33(7I(x#0J8G&(&Aw8AdPM_V_w9#h_LQsp( zI1C7IEHv&yP>g)WFcdj~PJ*HW8^YyscOQaL<5PzAB@v&=ET7H7H=ycrPCxFH)jGhs7j3bVSG8R~p+EMCUDeCjfem~6`oj&2Oq zWuhG6&^iioLv`&o;IP!a+)&KQK;sou)_uH_b)qsu3)D@xhSmcc=Ffx0d0H%Cu?=++ z7R6)ar7#tz$0Q>=Qu>HCT!@2yNc9N^Sx`$84q~YDfP*$nZNdSfWCY8HR;`h}0EYx0 ztHO;_VEi=?pD}%QX2&OaSoPwQBGpNuc5DDPs)3E4?i{AwnQ%QhxuIUwWT&&pm zYSZAjcu1MwZ8{DKblL|^hhXL5>v^yG@fjj37Fpda(H@eYtbYF#)*ebfHb3H}>b*q! z|5&{h;s1>(&XsT&f()&${w;`t-m0(jb)EK%J>6C3+XZRTE?=FDC(D_(*eFkYRIDxY z#+#uPxcNp~rXM;bf2IxD;1?9EAYD|!{kOs>{6)ww^g!utIC~ME+4s+!lE-SrflK29 z#rQ+o=#^{}tMwS-zFP8=8CbU{Xg3|t%0DT&Wh@IO|A|;!G95D0#OWmq$qIMQoNTay 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

    -=$q^cxC1v6c5*~ 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 z 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 >GjZLQF0IXoz#S94~IrS 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

      N6pK-J!%2% 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 Px}&0SwEu%=SLuJU9z%U0dG>;EXY-e# zV_!CRhO;v9We!Z0y{N|b%go~AbL^O*hzOB}jBrBQASGi^T0*$75qD^F3mIYxBqKeY zc>a8UijD+{l32*oznrsN%G1x4W0nk@px8&n43*@Q$98^s08|9^eXxjoANFJ({zU1i zcWiaaq=qkEdVI?t3LukP+BoV z@^-vv+O-?e1C#VZY~B!vf~mNBGU<*lOr3l)w$f|31$vWhYDl7gO<8ZBOUVa*v~r1G zU8{U3RK#!%Al}G)OYM6#C%ks6125)Rf8U2RS!J8?qb$u?hd_?TBYvg$bY(&zv>#(! z_uiZC7&>|0uNN<_xJc{z{AJQm7e(%Lu6lg)kuGiEk4c6pzC?e45>VR6qTE3Vp45oF zFR5I8;%!<>he{4-iiBroUZTUe=f#@;&!H^vK<=R5jW8z^Zpmip=?bUz?Ze8{-HI3X zbU^2lMdgt;1zTVPgIU`MX_z*llYK*%f;zfZxt;C$j@< zxYti7Ee0v~ESbp1%x~nLX}}Zu%@PG3rLt1~`?mgoHFgZ+kJgL3-h@=;srQOiQ{K#H zPl?6JWxp}(nawBzKrQ)lNi^B@9C`qcdf@JwB0@RnmgMUDe4WFK>7$BBYh;GS`}?fY zLWJiu%gB+izj*A`k4g;4lmAuC%Z37ezNkhk!LbDWjwcQ(;w=8}0R>LhQv-}+r?M&* zl%N5=MMgOOGOOQz;%b>F_(z6XgB)N8y1DjZybU$fnLBD(0Ov@dbhM}tcHoNOEAxiY z)oB?c^9W_XUM!tUve`b&Q?_g&s&3n~NlD{>Oa|!#-R|mta)Zv${%T%+#1!;@R4vnB z1BE<9H@?6E#jOU+KNijH)+idJs=@ZwHWu2pATO0Z4o4p&sI(P1DAq+7Q#$OB>s`86 z2Go&k+3cvS-^*;0Eiohw;oSUHooR2q(Z;2@{IvrRdCGNuNAM zpGbYKh(5WRJ}D#?xZApMqtBO>HqT&;; z?nEwJF4i9$hxMRF)&laSHnTVoD3mx9wYHX3C#fJmo*0Hv5TNh)+aN=Vzj84=VyM3AL-tf1bY{4m^ z$ul?{L`SR+@)aGOq<)+{=YUDUz}{+Y^{cI|;U>dq(m}`}VAe-_yO7>4qPP3g+r2iV z@)$t?v2ovIaJa2ZwQY1*hH2dZ6p>&IwXTlN+v_P1rFhsQcU$;!|r4+J9Eh%Agq{Q(jt)?bz7047lDckK{ zvc#RufB&96{syGFH?(BYnhs+WQE}eseBVCzj-)tM;SLn8d%B}fIV+DiU5js;mohWv9JNm zvEN-F7WmYi6#<<1{#=Xne~7Y!Xqy?OMNqr`*vh1ij_XKGJ~ZT%B|kO04E$IRO+)TYdx{D zEGis5CYs=19QF7&M@d)^M{$7p>%606=FE4$E2OOURJz4Tr9cVrLe`COD_!!dqE{&O dOnHTJefZ`IWl=bJoOzARPmbiMuz8fC{5NLxuU-HE literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/Inter/OFL.txt b/web/common/src/styles/design/fonts/Inter/OFL.txt new file mode 100644 index 0000000000..ad214842c4 --- /dev/null +++ b/web/common/src/styles/design/fonts/Inter/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/web/common/src/styles/design/fonts/Inter/README.txt b/web/common/src/styles/design/fonts/Inter/README.txt new file mode 100644 index 0000000000..3078f199cc --- /dev/null +++ b/web/common/src/styles/design/fonts/Inter/README.txt @@ -0,0 +1,72 @@ +Inter Variable Font +=================== + +This download contains Inter as both a variable font and static fonts. + +Inter is a variable font with these axes: + slnt + wght + +This means all the styles are contained in a single file: + Inter-VariableFont_slnt,wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Inter: + static/Inter-Thin.ttf + static/Inter-ExtraLight.ttf + static/Inter-Light.ttf + static/Inter-Regular.ttf + static/Inter-Medium.ttf + static/Inter-SemiBold.ttf + static/Inter-Bold.ttf + static/Inter-ExtraBold.ttf + static/Inter-Black.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/web/common/src/styles/design/fonts/Inter/static/Inter-Black.ttf b/web/common/src/styles/design/fonts/Inter/static/Inter-Black.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5aecf7dc414ec9b807d93f6a5b8d676f5d1f0279 GIT binary patch literal 316372 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 literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/Inter/static/Inter-Bold.ttf b/web/common/src/styles/design/fonts/Inter/static/Inter-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8e82c70d1081e2857ada1b73395d4f42c2e8adc9 GIT binary patch literal 316100 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�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

      ~n`n;o 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⁡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 Ag#Z8m literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/Inter/static/Inter-Medium.ttf b/web/common/src/styles/design/fonts/Inter/static/Inter-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b53fb1c4acbe100c7a91f07564b7f1fa2d5bab12 GIT binary patch literal 314712 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 literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/Inter/static/Inter-Regular.ttf b/web/common/src/styles/design/fonts/Inter/static/Inter-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8d4eebf20665d5ae746c622a2bd42274b54d2bf1 GIT binary patch literal 309828 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

      V7T@aTyAqtk8R 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

      NU^a~Y-YT{n`0xQzR2cZRm6M*N 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

      qEIEEraMLk+K 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 z zSjzUZ#{oKx~uvx{>olytppGikHs2jaM51-{T#Pp0yMblH4|6F>G z)?P}#LTk?vwWbly0;C^GlcF>RsA33_e=Ae=P&0t^`sZ@V&R4I<;D0I`KK~(}ELXY< zt3Qq_HZnHCT6dIT&1C0%OGhoao-O<`Yya$`9DjH*ADA^Q%Woxpdh|MdxP*+9N6s8D zB+D&dvz)AO3mGtY{MM=5Q~tB)tU&K!v&Uj0EqOX=SaSDn0kTo_H@MwwY|kK_vDCs}NV8Gfr&j3opKaIfaZwE9Vsz&+chA z+ntum`;%_x>DDXTF43)DV@=wYv99b&%C_{Yv$?~3f9^1fI}P4gj?O-y!GOF%Ma7Ph zk%AK`$A5G>bC7$Q`~BR;`x&JEU4&O;f@P<$(AYsLJJJntyu0tdJ?Hl|ciz!EKMt8S z|t?%1va4l5%(ZSFib%;xl+h?VNcR)&G%LzPq#R z^}^elox&6R*Ia-d!v|vU2P&6nN5H&^NPFjG{0;Eamt;vL^wv2NdO7_n2|HIt8@Fe! z1?-cac6km21x`aTQLlk5U?{K!lBpA$y5tcU1ziM2L9)b$F$y$A)R>+Y_yfuH&)yC~ zE1X0tWT7KlF^Z5E>Y)|eOawl#LaG$MYxy#e-pFJ6sCPk1r@;f;wC>f$+11Q5YWtKe zXXwMIh`|XxRQ3(QvI_-O0Sa9irE+CoKANqgXCClAT z43lWp;@a%wh=}f8zq_H1?%vj+ja$4cF{4v{XwYd!1L9=!w zaLheztrk9s#58oMDf4>`jgAH30_hz2GMvlPdLoL^u~yU`4q%E{ z_8{aBB~fr1Dd8qx(d-jjU12A*$8kI#_M?gHDYxU}XYfTMg%kq&J2t=xH0R8bovD0~ z#*Q0Cjld_{Bt1zpKsT!+wz6sTHdh-4n@OyUB+MkgPwYWtqKQZs6NQNq@H)UE zOe04;${)5MYe@c0(xZ~BqW3O-_poF!S+tB6)!rw=NG7g5{>tYdA;&Tg-r;_J?7ws^ zsoi#KN!@oa1WAM;h(Z#*t8nU}l>I4@!v@pv!ua%{vnO<9!w9P=Pw593YXx6g!Lp}Ao8Y#^4l`|<%eS0drDinQZjFz=03H^Fw=$YuU@J9}`x9pW{GJ}>wTbY6oEH&1HUIphKwSl>r z0~&#lysIGVjNSfwCCOeg6p(A9y^VuyO?F4hmaQUk>L`B1u0c! z;vw;XI5w5OQDySGIqy zcNL?6;!!$-HLEZ393XOlk_>W(xhQThbv9d~WzKXx{ha7NExM4CavqVEr#*j40gqYs3R5>B-KgCYz{c-5Gj&eM%IFW=~bWHEwQp6kP~4 zn-?3Q5IXrTI6TDCEhJXst~-W7=vDXbvBXMw<7QpY|4Q767?7cG<(UrDu$C;MW4;yu z(H)AVv_yx^01?1PzJG@k(*li~Zd_PsV&c#c;NK)MEG#Y|BrH*xTUSRqd&b4|^Nx!0 z>Mtsz1(5hBfW*~*oApOPF|u~GOaVGPGAuNOTb3L;BsC=@G+B8GIotXqCi)B*ndI%4 zkl+Wfn7N=*{U*`*Q*&&hn@KF1#PyTrs9&e}iB1Jp{h{rkhd0ug0VrQhjQK?ASE_zw z106xP)zx7^ze00l<&7a&*TytncpC!k_cdw(>H>%R5opHLkuKJ_Q`U5nhp-kI58+n^ zX^|bJ*d}7=ERwynJa1u)Sp3B`(8IInHdRqUqvZ7R$N40~Bh9mLrMD)lIG^%o*VO^{ z>zm?Pw5mS|RP1>t;r7U`&;v(uiT)E7oMmEZpK(WWW!LC|LlcSq_u5Nik6?yOC6lDi zToS7-Fay)6+c#JfA zr?#9kc(H^lu3M?WdZa!ucvc*DLk@n(<+fF4su zVk@)|nwm*oOxgC4a0{fNm4zriihCkK3fPfJJN6x6MzS52-z@*0URt?=IJ_viUD<=q z?Go2-+N_?QWiYCm3zuVCiJoZ(@)Q4MP`*FgDXkxq z!_(}h!Bb{ydaH`QUX%Vs#$M>6Ur?xfCZ2yk?FjMyxtN*i75_|+9GUhWtJy>n^Mclw ze;%CiMfw`jtO`AH5C(;{=wnFV*keRV>m$nqTw(T3*!7r5XwDrZ(M72f7myLtv(rmR z%nJIdHnMW6jep&oE%5iq`A83He3q@NT*cm3Rf_Lt=`VA7MD$BanoVGsF3D+(RCH40 z3$ySOn>RytJ`v%61En#HAlQkqi5}UJlkR)<>Q%`DdT7@!GT^}jGGG@WyXc_@GpYv- zs-A(rgbb=yS#JZ?%ZfRXnQC;cWx+2tN z9-iJE(KXsa+v+J;x_xA?#%P+%I0xORZslZ2VCAM1&k5|hF^f%(qiriE132$zn~h)# zN7w<;hWW`>elV4jKdL3Is+Q4)>P63H64G&U+K3`T7HZa|E+AxTR9Zg6er7#ivXN?* z5;&=A#i_DwZGZ6@$qjdKnRt5r>^>eLiT(s!9*RNMr zt=vqy*HjWUyr`m&9EM1hRSqRkKE8-B@8X3p`DNo#92j zMlP$@s2;%!dBKevP%r7LrZ@J{StmpWGtgOw&E$qs(nz3Lfp7<$O289LFk+Y}kJZ7f z{)T$|{m0nsONf27_@K>o5*xxU5qdOF~d>>2VCAxptBy{6n${4Gi=prK94y@ z%%615+neL!lCw9j^Ta(9++1__)<^1geqATQ_(!Gnl+4^gkU}}VUzE?@wNXAp`#wdL z2P&V*ELC~um<*an`UpEgUtDigK2xYwd6L<9E;WvoGl-LLyqrZVk=0bJz~NZOn9UQ= z2Vn~~1?LMU#^N%JaUzTzLni|ZQ#`(bu&(`Kg~gT{PAB|rH<+Nu4^8SmCvVIQ(qh6d zyS|`rfkEZSr;zxJ9Bw{+`^BzbCKA1w>7m)~-IAa~ctdno(f2=2ctYzhogn?_ojQ7O z$3BukWH(6B=FK$!2FQe!`*skYI@nPkJ#i`b-pUC-5+(MHZzPq_wDv)*nKTEHT#22U zTheffE948LefUIf^(}G7&QMvh9a~nxc4o~25K~?>5mVI8YE%Bj)2t9FX{OSrzr`$v z?_|jug^jdCr>!>YCKtHg>p7ow8~HCcaGr`@__LNj!1-?E52EdN$(?19%m!XH6AT=s zY9@y_%>GKbGv|X5q|0VMmP5gGzmjwg?!Cz`$6F=2lzlv6OrNb^4#%PRrX*K6Tk%3D z)DU_(E5Sc?Q{jS`Cz_;)7A_*|K+AL&fS8YYiQM!$na{(A4F~IF1L|8cECh&1LVN}{ zhm4JjA3r`mZY=rmiJOs$1mol5GfAfhvi=Vm4hw(CzM!0`DR!@`BhiPI`s-zLacuZj9aU-TJ?cm_#*qNc+ieDe=H zu!iey3QM;nx4wyerbR^-qAecsnTmL|_+>t*!urpE*=-F~3g#qN1j-u}f8;1IQ zw4RbC%oN*+D9Xqm)0V~JTahlyWIC^|E=)3+P43R zq@6!PKfh;rnx5TDx{leCW;x~Jq}&TrEmQY^S=IP0*)8>zB{Tk}x70$|2pE}75D)M) z=0=@;2DP$o!311eML-88SN*QDt2bT~@Ue79>CN<2U96O&e5=KY=Sq zfTL3&32<@@#E4Cozab@npBUgGk{*n33dBZEN^)vyavTN>aFD-QJaNkEmA+2Bz*3f^ zlb=Y5%s`=3l}aQHu$+@bB8yxxi%>_=8re~(VWho}k3Djczgaq$ujOBo1a|b?+@7-92Lvm@u&bRQy^wE{Fh7;_HWPn7X?JJk)Lvx@Lkvrm~ocYXH!^HdR^G* z!M77DPf1QQuPMm?zm2kRpKkC+=`L&2;rhqh4Xb|(*FV)C^=9=KpHJ8R!|%Axo=<$9 z|2Fc{u@UMGF%-X}9F_lv^3fiweAXTcbg6iM^SeU6tUd5MYY+CjZe`Q`a;!{E&#Aqi zljJsjFWsr~gy4szP1qvjB0CYJOksPl5_Q4GO#RB_XxfIIe52{5z9@ znm%T_y=Hn^-QF8%V#7{Mg*<+f@)%H==73L7uTTq_1V-v|eGWJbTxRQVQXICz$uVva z2`i(U!_Lx;{2S@shGR=*9^@w6+WF<%| zLNyt`6NYA=E=Sz2Pl^AIUG%F9_vqbnBFoeK+Pg4)=;R>FnC+7$AL)10cI^}HKV-X+ zdQeb%bt_w&Q7%*G`8% zX#Uk7pK0?I2M(+V9X2df`_3#fp|Q;qG`30Y*|7Hn4K7#UF0tlPYx!16k#B|HA@`_^ z#osZ{a9w^A>BxIUV;)4!t{7?(Z(>Fo>vwLG2dlci4O4A{AUd?slU`B?3}P?*N-t?f z+?1|W6u&#D^ud5RL!FcpCC8Y6GQkvs8vkIBDhnev?}LufMPv*d`g0AFcpv^!;{p^R z2r@vqjqDv8CUHaQ$oo*e`1AY7!f(Doc9ZzW@`Kn5t=I$<^~x4D(hv0352R$)%gn=o z5zfleB_VO1E_7$=;RT`W)P8^%SAmzTm^WkUY{%$zDP(Z|ijXB|`NzcExpxoe<|8`C zu6VjB^eYl@h5_7V+b5nJ7g}7M&@oQEt|Fr%z6bEmEO@GT-8c=)vtj*e4SR$TK7zm3 zSkv?fuySMQ(D(27TrP)?;DY#Fd!IZ(x0jK#w>eIMZ zrqp>@|C5F}?ns6iLN$aqOVKMY92Y1W8*qIPR`Dx>0)HpEO(x0ak15Rnrb=-(un=g# zXmlttg_EeDXkt4qJNkUWwPFM#34UoJT+0qN9DGSK2S(V94%hE_A#>yYfwEv)+C&KR zP>(IZw;Xz>!7&M}Fwd?Tlflx5mY0mJFHZ7r(Mn36=vhs;RZiU$!#*O4g}y%lZdeml z3|+b%)@SpfbGJvA8h$`xm`+x7X26cgA3+MDjz_6r_=r2FC~OfrA=XYqr{0#QEBG#m)W2>4IqE1QAKd}1cA=PNL=NNm}R?j z)9F!QkD+zBmvr9X(tRUcqtyoXZTh74Bf6U9XwGmyb9ZCg9KXp2Q{@NmOy4oe25!sb zO0(fXr&!zhHq@s0s-*Nk`UX*F)}nFtV_loAbYKX%7m+J1q-MmzSuVWn@m*Gm_*~1A zX)6DUxYG0lQbF3%v^27uNPb1|^@>rl;Z*hwd4CA=p3`_J4oP4mt!2(K4U1?6B3K}ZcIwPXm~Z>nA*d@v&h#PquJcwd zR^i?{yH69sOxDTQ0%@Alv{3D6{7v9|Ge$NbLZU6i8L%8AL#2p>eBgUexUsv?BPM8& zjdMS8h3qc$jq&rdbLgjKecQ=59q;DYorHSmY(7l7Id&Ti=y->s5U6x0rh=)!^3%$j z5Qt5LvILav<5PphM|@^}3A0xVOo-^)wKu6KNOn(2>Z9(%w~q}COG+F%Bwm27LlWYK zh9)YHTa8?5XXo3?I)0UnZGY0mJ349rOsTy3gU8)(1k5n&=P7p6wOZCTh**C|5jTP5 z4I}^o17enRO|4#Eo*FVFb!6Dkkqp*m^ykpz<#k~TqP*jRLwoo1yB{(yvUhZNl%u`B zj)#D7Cyw;?jgJShJML)9ne9Y2hr@)bh@M2*?|a8#Fm^-8=VoR_@%37j0{K=+jm-$d&)>xb>9~n zSF~o*GCRA0c0z0RBdZ=iMyB)8fa`U>5J-#2RNzNxn~BXxVFwp-Jpn&-3!?hR28X%y z^m{;F~<%@%FiU5(Ea#xVTqA_$H! zk^}Dr3`ZxyvRI^OS_o6oSr{gaP{OUHdqzv&@_p1c>ql(;ZF(Vn_FeAx2gZ^yvSpLz zOottNVNy;U?7|BSr5^Mf`g7YJ9GCmu+J#SxoyKpUU^FbcASEf^S!yjjnoKgf@jr^6 zAiT?=Wa%n6GpLS(&ke zblzG+AMMJmO89JpbZ}^O{?tm)qx`E1@~XocZjn*br%(hK=HuSg@~`YidhexGK(=V* z0q>E$IIo2d@}@l~D|=it^^v?>bAj4WI||u`7%28|Cj`Gcj7=vc2GJz+x?&HxP1P6@ zxsc9=fw0wF2(pNMh;8WFuxfIK2$-8UktUgoMoczA29k-4E;P7EmjteqJvb=)Sh-!F1}^&|m3Zzb+w=UK(+2 zmyM-|lS!}pr;EOwKK*&cxU%%&{Q_-TyIeao>qIK))myeiOZ5texEU;nlRLHIrXAj~ zz3Au((mS>vd8ZHC53UQ_4<~5_0|=}d+F5{Af=2+%rGTCbQ zJxZWAMz1R)=gE-AB%K(?ln$krXfU4uCV634fqR@oVd}P24EL-2F-0T96e)HR;qYhJ z%E{c|Bd=S6|0U*i3xTgBTnL?dv7ThTnq-@@d2-pQiERUas(wo*T|Z8jQn^a<`k1cg zTaGyQ{stUXzuLgUo6AE}cIR+v-(hEI7QOBFj{4ITjqE(Lli#E$MZs-_4Ge_}7zg@S zfqvF<5`|b>&TLWQBr*mI68Ozs;=g}8-A)b2y0nU?1!w3I zM8mo9ACg*MltPY@PW}tmoye=+SOJi9veRJjjJ%jT!F}r6_`pf2Lw1V>6Y)P*k zMF%wNE1A!b?ffQr3FL7THnB&tf^qs|bpvr1b?c1L$H|;w`v5hTyp_4|Gx2#zdMzHG z5jfV3(7DS<+vB7Sad=JD2glrd%HQW7fu`+Rn*0;d>%aDN?$`{6n0_nnPkjIQC_VCy zU)TQi6}pdx#S!D6O*cyOD^ZDk{3)S@n25$EhA@F;%2wjc7F;xIb$BNxhP%G@d+7m`Yc`lxZ`8G_S|VG znPnY!Uca%UdroWn2(>uUOKPST@rN;V8Xpl_u>BeEQ!)|+IREjzD{3U zyS6KPN7DG1Y9e2Hf!5r#x=yPvEFtphm@!G8kC$C9qQ6|fMISC)NIKuVK@5uM?|W;i z_wC!faSzyUGqdt$n1M-A*s%Xb!?SBcu6n}h z>>K_o3WHZoKxnNcU(+pDZ(O3=&J#DmNgS3F6j*?^7O116Rci6Z^|V&?1}8=XWth#mj6 zp{ppp*=U7R+(Fd#TS2$;g|Mx%?`MSXUt<%MvEP-qP@c$81orNQxj>i=h&^)}%yf0i zTMhGMahkTzrO%$usdwH5>j=TFQ2JZ__b=qeOn~Cxoei0&B!$S{3o@!43Z0ga@7qZ_ z17%=vjG?`fS?!yc!5gMvzmHF9Sn>bGRdV3OZu5%^yA(95{a)&pN0i@hO+VLs$=NR7 z{mpfG9yk13evj0g-+CE;YPnF&J>d&C=+-+87xoRm3tvCKjz2rOVBxoISGXXC7=@F| zT)6TUDl5oLnD&-xM^V-!uB82e{T9Avt*wl_0|}a8<;jOiotIL1$O!Zl#BrIKtWFd# zSgWzNl9}-l<&en52HMxq840irBB|b=lIro(7h^0aSp$;WLQ*36hCe8K(df@wb3l_d zFlTV-5`18!J*A`eUAWMk=8+lAk?VPdlWepqQCk76gd-cRiIuL^#HteO>gc}jzC*fC z8E}9I>MhM1XD4Zz+Re;;B zYVI)|NWS1Z!U^dYwCi+!&jPwxQ|K`p<$Q7rJajqAi6cC8NlWN;8o)iqJ$p$H+SPTz z9-b!bo9$7Ed+zc#xw^(Jf}N}+^dqL0vw%m5lj-i#v1!Yfr;S~@*wfC+)6>e%lO9Vd zE>21-EK0R?cem})w=d&^lfkks@KL5zfDc|@jIqB)9wIxcY=U4)J7#upWI37v zD-?3X59a(mmfxp|m_HnN)-vK0@Awm(`-fyw$u@qethjNpkXJLz1q>(VDH>{qR?;>6 zS044!JMi#!_bxc=9HH!myf5?9WGPJ*Xu3s~!kc;2(Ssw3Ao61#Z+3Xs%pCT6OTUI z-*@BOtluf7a-I1);(h-qOhPgfqM_D?K0Awj zjvc4dvlWUr$58cXk+d<9eIRoG(P@E$Qc`%Afb_H==&*5kS_!-R?K<|@H62qPP$D&sDeyCMsKyHp-PyY`Wd-^wBwD(iKXdW^%sH3{0n|<@(8H4R? zT}hr^NKsHn%MQIAbO+BI(zB-<;+J;kRaDc+*xyb5<3w{PGvW*fqG~7>OyFQgNhfQ% zkqH;*y}V5q=)&pJ&F4twG^%%wbelm7H*TcGL>=%gUFHK0d}aVf@_iqPv>}bx2_T?CSbiHQ+B_116QO2-kmAwSRwgLqx8C0%or80A(SB@vUsljy22;Dc1%wVGgO zK!aLMFdsB!D~hsA&2-$-2JamWGcS9}~y=GBIYn@&R)nT)Dd9`!Q{{Z}mbWP*zn4*6@@_trV~$MWo53i(8cK>6_1`ezprHU);)N?e z)Zb=T>L>EeB>uRfR{ik)8atn>o@>sqUo`FHZ+K@FmRnU>!*M~hC{y&}?39PFxR%N$ zsv?1qCG&0hJB=&ww1|?eb~aJlOPx)WCQ7xkVf*sBZVy|QwD`8`0kU2GB<`F~e9fm|;G+t14mO2QMoG>?} z4)2$%B#dhe3`rt&pmiu0QqCJlEU1Z6cJRY|66caB=8!2-%A{{US0>U2oZ%k6H|at) z9-swPRdhDV8blDs81QXFh$5Nl=}bVa1v;{bCW&}CW5b)6Sz1EQ6G;*5q$mz)oDc2! zPR=fleJ<@OFWITr@|s>t6K;l|zl&WrKWmH6CXULQySEnJlzkjsth&jPOW~UGT<>As z&7z6#r2bas-A!$cM%yRnr)9u@VM7ogQ$3w*LFi}tD&!6*+}H?P3sf)+W56+v4G@M{ zYc5XO78$W_OwCTdk}MxEFvO3Lq)3nGPO0XL6>*81QrhP3NVrlnq*%{AI6tXvYZ*f_lcse<|6XNdT=&6&=q97c$?dxvS!`&Ut zXUyo7U-|M0 z^Dp=99p$I&#(DE95+_@Gxx9vaO&tP#LAhxdtcc?qQNvDnIBg@M4O#!8{X|L-Ye9QG zPIGYb+TcC=R&3!;k$zsiz1p|;>h0nuJ4J?_|wGEo$ zi(%%iNTkhmcM0|E!Ws}YRf03ah5|FVH`3$K67VX(%461vyz8}n4;F4%UmjQB)2x|K zfvtaNbKRl-fdK=RhSI8r6RqxUY`N{5nCf(=)D5vUBL?tDnq)ruW7kn9N=G7cN^7)3U%LxZq} zH5Yr_UYm~+`3Q~>-Q^$wLd=glr)oBQ`0DEz3?+)u{4iZ>?So_dJO7uV9Uo46<=!w=x9+TC;GxbO zOLVd;EBEsQY z@zfd)hj|R21l;6-o0z<4;%qN9G*c_h45gZ|DFx3TK6^H+_}Q}uI^!Qape1w)q`H}8 zB4kDil_l6S+HDE~CObAMyc_N%(mYuuy+9nKBF3gY+T)}x0C(|P;7*|wrV+qQrdfrD zn^Mo@-kbi;#9L&1-=1%C)Bhf}&B zV6%4xJnCs$&~wt~bb<8w`%2{v=BSIktl8;Sh$bhKfD5ExKH{515#hkhZg2W0YfYC+ zC)H=k%jiwbS^5q0T#mbO$m2b@)pTP_TL8_C;Y#OeO8lt2>eAXQ2ng4&;)LP4I z^ieYbeiqkY!Mmxzw{PMbenPxpOA7NV!4;6W&|&c;F+4Q>*xwij&&sKg!*ARc1o`-V zV0`>Q--JZo`2vZ)ZT&5YzPOOS-B+-=>~@xQ_P29OCa$k9!vOoE1#ECC`d3D|1r}yu zIa0P(fL$)@H+gt*iLGtPten+yx?Z}OwB7uIIIUPkFFZR)pReX7z)gDM(6D$CN8UM& z@zWN*Y$JlnmtUakQ8Bvy;@n?u33c@u7>_RAMODhq!jRDs_Yn*gM!k}%2`oMlH$X8K zsxV6*;>U%NTl~%oB!CB9TpTzJjZ~jZAse`B?<7?9gtU`Zz$jW$#@|!!K)W+TF((Wl zfJERvbKrV1P76aZL}LQ%GR$EHoMj7(pcn=MlZPvyXJr#%ndU+a#p`OHU*_*F|rk>t$ASZM~$q-!? zX)TkD922+o;$n}bhxnQFmaN&w#<8g4QhR3q1dKyiqkX7{$-kI|yrJ{X{EJnH4BsUu3|C^xzrk)URUVy@;tx`#K!Tdgs^JXzW zXfKHKs+IKIa~k=8+fUj~I6KLD)Ymgex6^dzCF{#{=P8(Pw4lEO+&F=?FOD%OSp;|n zE<)+$$-aXN$=0$O^z#Zlq5|k2lld_w&4?*O5!iFV{&WlJ!>?+95Y{^iw$FJammHCK zGka&|p^UxPGfwyp7;m@3Z~>qjD*AR{dNd(Km5Av!MXzB^xPug#oZMY~Ieysr&o^Cd zOQ#xKC(#cOMsnhri*!1D{g~FC=c>*gT0%NqI(C_KTC(xlZQ{7$2O2`}9;UZx=!scIum0#e)|4aW6SRA#vli86$pe{elgxXyso-voS>2Z`$KWE#j>Dz6A)V>d9| z2S`FC@se?9&QGX*%}mglOovCFPk;;o-C&`^hPMnBwv$X>b=W z$ymH-yi@pawAeZRENBwH2*QgVrgj@B16X(z3BoDbONrXq!UW(Ycu|Npf;dd}h_HzL zR>=TOBSWb(QEGZbW`+nb=+L81Tq;*Ma&TUgk+%YbY zoEkxsW&o)N?#^OGfWLG>m_%&gaYEFP&)5J%)R1FBtOUoBYld-=lO%ZmX! zO(-gy0NXRJnP#UnSejZ+te@@`Y!Julj=Ii)qRME#}M}+p&dppAKG__yZey7{f2pWGO!)d zZI8M6kUr`jCi*R!wPBk%WoGB!_DbcOD`LatoCB8c&;8t0>FI@I3 z37lOzSxDC_R@I%RQCQXz5CEnr31PA78jjI*WcC=&nMHazU58wB6ssCreaMBFsvC5; zH^hKm+M|qrNTbdmmwm`(k2VPbAzWF%j~H@4@XvO!TpqF{{g8_tlCV<<3UW3D7fRU> z16McEQ;W&Dll0Z58kYF>Z5s9H5pF2O4U@Dt7_u8sva-JPI5}TbM51A*GJEHyk8a>J za=tk{v<$~$VSqAlh_ko&__SrL`Gy_046WJ(h5OCQGw4EB(eYe=oVRqsM^s%UAUxxl zB}Rb#011Rbj%Biz7*jwEAbFBEW4N9i9P@nEO_xN-iBDvioM7VpHw zQe*B27#40<&}P%Fd1&U!JW1pz+z5?~VpT+V9lLSo12nIbVz_J)Sk@3DLkW~ISiDF- zxE*+@$K&hkEeg1ed|$u!ALxCb+P+ zWiz|H&)kPz4iyGoFw7+iz7%!l&k^xdP zLpggf;CyG!-5gGD;yuJdB}o;`-$fn!vu4d2RG~i~(wGSC4i>fn_9AjELNbDcDvW=% z#^8RDYm~X;@Gu+oK>g2EZsUg!nwjU>XS~<4aJsik+aA`oeVhi(3~_MiYS|BY$`t0* zNhLDHM20s^XP9*l>jwcy05i+P2u}P(mp8`7{2+6yp6;HLgQt&6&FE}s)5+3`kltNK z!|ra*$mGm+#ZABW+Sd7-0CsfNuTT zn|Fy-Zt^V4Os$Ii{`~6ObHm!K>kzZIa!yggu*BdNE%Hn{04pv8%(hvX14*g_Mk>Pi z;0Y}q#V|e=_QD|xUxiXNJ!3xVzJ*+yPY+!__RZp@}yehpdL&p)((LU`{4vsBBK%HMLG znn1r%o&wtEZIkMe#=t&mk+3OM0O+bi7(h2IzHQuSOS|!o!vGQwQ^Yl@w2$G$EQbE? z4}zy{%>cP3M2fxuh_4YLl95yt-|1X}PzwM|G zPf$BvKlAg%k)LPi*W9jgN$}8*&jXkjI|V&45Z$~Kk01kui)mXJ_@dUH*0d*69OeE2 zRMq_aRUJM0`F!Wzj;`e5?(&jdgtTaRtwl={GQ$@=(B044yv@YXnREBlDhtVvy!>-A z;6rQwEyT(-43=97=wM5c4vF#2W*LJn!DDj%7rby(xvHD*^n8W7hn>TyzsC;8c2)KC z@Q=u9)2XeIeAwRriKL;UB9(U0-6F_zjK_wGh_gXNeX+TU!&g)qvonu|^qX886Fm!xJp;AqSKbfN~gRpM~|q8ORJ0+ILSw5>6<<@aOB)f zmzn#)#Q3O8Mql{fO0|%y;bZ@wN+#Q=jSIQ)uN4T?3?evEHMqzJ(N;QB!OZF+0YQ>& z8=|XV$5ssI!%w;C*Nk6f6Hb= z!a_rW8QH8(*8*ZtRx$n&{w>!(#H{^(}pXey+uUfnh@l35gjH*lG>QF#ST2 zdSKDEMF-|gbFq$$nLi}au;u8*bYHh2Y{vnz=%#xZJC4?G7F^#DrP|qZ z#~!!2XS+U|OZ;^YH|&@BAqv&Z{gvCE1Lls+qZUwzcmY>65D;Oko~_I@URSR&)E8a5 z2fsYg*`yVZm^K=60RMz0^tr5TS5w24X)Z_|Ff~u08rewKbe?5Xlks9u8$j8bmEUfo z=T^~kwRTsyx@*d4E`Z(l#|tGDA_ohE#L*)4kTwjJqUhNKA8Uqwg4Ao_;xz(NPaVkx zhSW2c5ZHG(_FVzOe<$+JJh5S zEq+}m11h7osJ*y-vPt}hNP?as@$e}|q%`pzSK5@wdBTG@9s zH|=iFF)cN%U{Fq9w;pgyrE$eGozXojKzh(Iuo-9~MEX(~@y)KT&FkRMv6)^cLtBhP zsZUXrU+6BeX-1vvWp57l&(K1yHU zhmqG0LKFwh4S*^v%#M(UwH{pY#%kQTaWP$!K6^iZFSwIbS! zgx)0U3feieQ?=}1(9?q{cmNXn%$W1Xpj(7|wxgGfz{X<@2Q@ltHPrGWQzxUpyh^Zo z^8QI-UOj!i)a@)ua+tT;f1pJ>%RgOetERtPy{a?1qgQ;Vjy4_J_f6^C$!1?ikAzO0 zdUn91>)4?O=Y}O%T7OHPDClpcIEHmbbqr0UK;kW-3uMox70I;C$Rx7*ThFVXlg^nN zBMCXZt@hOEZQ+6b!@~mx%8!wZSX#~}K*ZX5`_2~q&9`bM#_PvSEr`{Rfqd04R&Gh1 zb#jCyxC=_cK(J@AkhH^wjZzbImAG-S5;b1enG$l{B0&l1%ao8Ubi@4v{RgT}YvN_@ z|Dc4-mv<66OYqU|^{Lv+Em`a>-_2e7UMIsZ31I?jM zXW=$_NcyAlAHgsmV?2hU{M&RK`Dao=L!Os|i@UpvlLr~+>FneQmAt3Dv$J!b3%eJV z>~5xit+{?1uGr7d*|xiH4|9{rqsGtOU8~zwtElf`Qz@yK39ambD>kJG(#A3&3|dt%#sLstKZzvb+Eb z=0$#+^k8F=$ancuh{A*P7xVlteqXiv_dWS)`fIQ1HzfGZEfVy06Ti)3_+ao{hM7lf zniU*eyoqttcqh8O@u0|2`wQ--@Kpc6Q43!X(Zo>;nHTkeqXwd4H(Wgboj6ogL z9s2ZY9t=X7U2pnB%OQmzt##;QuNzbx(z9nDJgkmlc%xc35}~=S)H_OTC9?PK;7(m^ zT!)262WNO@qU#LSqnvSK@d7&*;d4#sgmF&@q2uA|HIj^=8~AGJJ~9}A`?v?$e7i<6 zi7(MVJ;yq)qt)D#8k)mRq_Y~K=>hQu`fV1T0{Jr4_(yRuOf0ZVa5LeI)w01M)mrsg zKeLPiLV9X)72!P5)oaFrTx5*)`HkVg7oms_S4GPdGH)T89n|xxG{N~r8%N;9qzwY2 zGu6&)K+vZ?sJ#Wo5F@6te~MG@Pjj>AV(BHiL>c<(RV~wZvlmo3HORiR@_WIq56&@Zq&zmh zG$6P{W=UE*eodd&T&CYvP5PFYZVLI4%p<){nbFJb^3T({;}!GHqy;TXo3H~cJ%q{q zUSSLe7*J7pu_}nx4j)wkdiFonVadxm8|byG@R_zRZu;*e^7iDiw?{^y263hSL4VZX z3jMZHtiekb@=i0DK6gJv8|mK5Sqsml1uYvrX$O=2S)|w}Xb(0LJ`?0rK+aMzbUa%n zw5M1m5NqAiVOwTKL9&*dGg+gV7))n!LH%#=UCF}5uNN+SwPe-rW#z9I_l|a+5Z))o zDOc(vI|vu7C*^$`d^X5q-hMy-*Yfh;7cTjIUimKzh-c4OpR_T;AN1r17>qrEl8ynO zOk^g8I-F?F!wLm>X2|wok&_G?@D1sBj(ogmAsMN#`i1!u3$?^{<5M=No^L`Uuz4 zab2NP^@OQK1gDSes#K{>eM(4u0J$F#QkQ72;~nWQ!G1_3Twg6-mk!1I|IKSLlHcKn zb3u*MgrfljL#(j^GGUK>fZQCt;3qRnLsK0SSAO`U*>Ku*&#t?yLZ$nEFJ`GSQMO$5 zGj>y)sn|9o7&$X(Unv^klgxVuioN0o2if-aCb!7`nFA9B`uB45Rwn+*>>eXyo}xsy zPA5*-o*^QBOvnFZa~6a|!NlgHSg!MuM?@qiM?|G?3sWN_Qd1)$Q%6@6j&u$g;Mb#@ zOBLd|4h{^qQoAX8`6VU!`6eXzvKK7Txm@eSC5Qw$z|JauRR_!V-dNslD%!~W;Urov z7-kxo5l2%<*5Ywd?I`mvqDRqgEBj`LjB)Nm6oIYDp7t4o=jB?5c`Qm`=9mu@MzS!K z{L>7YAaF-nSP?w}e}*dvav1C4su=jE4BjgAW&JT_#TtOFZbZPW5NqJ<$Q7(%&ep2? zj`7QKw<8Lh_GDjs>QAoaht7lZYWtEQITDU=$ODd!{w|L>RsWucWV)h_bdydebTK<* z{AJ6tdI{%fM`?>yg~`q#{e5k^yR4d;Y#SU9WYx_rYt*nTCo`Q+wuwtRSak5|WtCXb zLEXMTJ)IW2-Zs)I(+!>mCB-PG_J1zN;FEG}1~-+H?PUH>Gd=Fg=@EOkVfu z)704=9gj}S&ly(K3&sD2`(ys{en1Z#oWWWN_s=4)NiWyG%Al#6b%bsf%f*9}zwB^w z@UEHDgwRnW^pkGTcDHh1T8dkyRiw>2&iTK({G)#U-wHZ&qt*F66zV1BCIW(OJu6Bv?z}f z6Z)KfjpajFN=SmRp7u<$=5kvvi8bG|4@7YjAU3&1;!mnW}CYuj#_qh>0L@^hTxL%Xoq0cS5r z%Z9f$EyOLB_}AC1?Yd@;n(=AculZB5_wdDlXABj@yRT(7bdIcsbbI?s@f|w$0`->L za3>W?d96fJe-dNL7e@4Tswk}0Y>oPFGkOB2#3&9CnLCRCnPuhlrC?XTwhX>r20z9L z$S78{t4B~`BeIk98d@4dU(m&m5v|jyY~5&LO)B|MP40Ey!|uM&A}c1lV>B zqf+@{NUmzstCS5nyw9s=I`CzUVe+nm4ER&y zttPF>uBH}R^a!xGW7)biSP)jN5nIF~Z&=}&ZHbWYGzcXO_BYFHHU0Hx%8nq#1vWD6j|~YLdq;K^jJ#POhj^7DkDEB z!|~7pTbcYsA1T8dw#(plUa|(^G^)9dLVz3*s$`yQ8FcXh&1}+)vhmF2Zf%%T`two* z8BFVo-!TI~hRd+N8H*VK78=;t%4gnh25J7;piQ7Y+-hM7j(yb@PhHEEv)y zV(9RQ0bR(D?mdkOdGS?TxnIzXo`c+6&FSH`&g#kESJpfj8*zl*x0MgPzoCo& zA9?QqUB#^~Y`>bBy=S%!ruSkSV?*dAnBF^t-g_tXP(p780+?oc4Wakmd+&izL$9V2 zNa&%s?EOEpZE`p{=jNPq@BO~D{`GmS$C}Y-q*t$AX-gW7MlEs=KyU2r=(Jl~o#hnv zq?>Ipf8Nwf$J1Ffs;K^z>Q%6PrqW4s_pCU3TXi-i`PEMH6Hed&OOS*8ERtM|_J-J+{HuA;QA>U_R_|dZ-HYi%p~ghPuzIJVV^K6FJwa>RYeO{r=i^W|uZ@zdmR7waxBL zRLWbP2fh);IhL48gCzDg-SG;yf42VO%>EoGrsu7j_ICRAb{U_}<7Zg@IJd9gT5`Ex z-W+*yWzEy~;!^uKd*0mH^7gy5WXa|Jd2-~+V;?Vm?f&*zmF)``ZezEkU)I(= zS(U8|7j3Wl)ZOdt8~f_E1&g-FT}`jDRUWysM};O$D)iVnva)?#zUkkND>P|Z-u8E2 zKE(btVQ2gIKN}Y1o^|N2&S+Fw)~jW{E;kK@y*mUGXiUwq3_B^Cp0a$IWADmk=9>)VZ9xJu zvrF@rN!bd=wr;0A4V5%y{y=9v_hi*_>&GzFE-j#&3`WY6fIR3r7}bd~dQo8u~Io+|X*UBi8&#Y0~0_l${~kozDn z)gG0%#ZA@5J=%RD+vKgmrK94B{BLAyQi zKAqt?KcAnqN5*+F#(ZKcW$*Yzif=6xg#Tzv-QLW7OyU6rYgA&`>j{ zdzYcg#AWQLd#P%ol9Dk>W4ZeqHo4puuJyAsHhlDqXDZ|H7ctLN1;f2>uiI@a!k*N1 zzi_wsW#k9Sr)GTEVc|2Lx+DK{9XRv5@6B4%&= z?jGfS`>T7+VQtF3W6M<5Lq|@k;Q8nG98*QNKXcc0-#p~L>8|t3CKXDee|rJv-+2Az zfBZoQO7C9-NccVKQGpIpZsQKv`Wf;tSC!}>&O!j+gIn(^S*QqDz?|v3D&|v3J z<2l>7JQ<*;F;>MKV%~g?QRJhq^Y`cjFq!igL-g8rJFV0{o6^mpL)&iS_|&q z=fgy|-RV`17W4a->2PoaFT3^Lrjma@&HeOQ|Ew(Z#I%Rk+;E9ceCs_KJD z4^_?WCx3I-aYv44=Y~4JsdC4%wOKp7&xT_)PfT{-yw&r9``#3l`*@nNrRue+dVKG; zdh52*>%J{%K2K#otr@n%N+3pe@m(-zRrh+dpGO2bAFIJfRN7hNY&~^VQ_<5r zubv{^vGIl`;J#X* z(yA%$Zl~P8PMD?AA60|Osb0S<3*XnK<(@GIHs`PZWzGi0=D6JZM%`Q1bdE~8O%?fA zcW(a*9gmDv>L2xd$6e=Ft=HeF)P4S+sNKEiS4=i=JfD79_uI)G?hOwn-FI)y+oxoFRHZH-J{=!y2Hi%xR8g(TfNVvO?Dwn!ZW@!-gg2i z2%X2%?9ULCg{^1IAf4%O6==UEZ$IqEFyp^WLYbtB3e{@HZkT@dtp3}5_wkOY-Sh8X zOVcssPwu%YI={PP=qa^!)1!F@m^Ub0`w?G?wu0uWhu(+GX{xHZ?v@qVHes^+8|}mF z*o6asm^pI&z=8G46l}$_?SrgRIl(F+d*GMFyCV@#$?SDs_UzLiJ(quY^*Ond>Xjm9 zmiu7Av)YMOhZ`>GS$A@y1A6Hq#mlG5P`*gE+ykzOu@G6 zT;P7rw|tWwaSuN1?0(ce;;^Hj`?r`XDv9xwvm;x!m;qbtJwLrfembMvI%yM}&5db) zddSL_0vR=DbNAa+6~0+HV92U2Wm37{h78&fy<}c42r1_ocwqeBOP!RqAQ(TI~z>*gvj1 zX^XRHzoUbi;mjUoq=Htv-HVkHYP1<);7K##zME z-Mq%`1lK%Uty%Q3#M#D~&{FtZr)NEjrB>^lZ#^^3%M>1>1QzHRo65X=?;b1ampb3Z zOo_WtnhP;gVw3&tif6#zZW$GoJ?=tu;n?+_qn_~iY}o@O-Vrs5y50TV4|nBLiLy?) z&X;dTU8xzqvSDz;TCDySiCu4ukGrKjEB^TcU;+CJfb6%QDBmvLF&N-Jpt9YX+^^xv z@S0b?tkn>~*2OOJ?Dmwl`{`v<1bf%nFiCH3IhZn4`PhqXmsAel+&9;T{43uqY`3Y2 z`|a&QKCS}wzie1{dh>*qH_21Zgr1J}V6#>M=IiVyq$RY1sTk(ajIqdV!pD|9kGA-F zMvvN=*5_aKc2TJ*6N~3waonBqcGA>7J_!N}ln-uHZ+a7_kLgH~oJ}Ccy6bv+dO}g^ zMbC}*Em3?+ciot3UY|>z-SIy4J)xe0gt?4QYs6T^xOX~lOcvt@%{v@mH+5cKVQ^+Y zpR`|Yv43vkwlegN7DD(q;-`0B_^Q^telzL>1mw;WkgeOv1^y+o=Pr@E>dl$)ef`ir?MgC$ye!xChT2DO^_IT&+y$-7Wo@{- zO!W09Pc(nFcwvkC*aMCoy!7NW%6L2XpUy+H>j1Wilr}m6wbv2!Y_*73%+h#NF(aS&qaF z`cmFGjzHbCXKB2fTiT;K!=baQX z@u(L6_-C(9r;9ZU6yXXk&S1vRp185!3kd>v%;y;|$%mDuyxr5Xt^6vcI{TIqvgW zrAadx@KT*E9^ddoBnlkqQv9QI&f`(+PCvt_M;Um9RA2HGAznx$D8`{CBIDdR8P_7? zT79*9f?mb_t4gkIi+kDDE3x~fd&gf(Y)A_l<+hGzScbhI+n#*z)&`~1$3;j7QtL(B z3s|O5k$zHk_YdEx6k0X+lb8Zp8~1B{Q%rTov#1pMCarK(3aw{!beGsDjf!aRjAuPT z6LzQB{z8d&M{s-FLuS{Al6q_}R^p7QKTgc8JE#T9LSXsR188xJPNu*eIvO9*ZfW`bOI((=ImLQJKMdh_TOWGQH)!*JRo;$gk`* zCEK4@EzjA8#7vwkllM#4bjLBUZW>kc4}ZQZV#N7(P#d{!x7n^YGL39>r&u}r?9cJX zc~g9CW^w~+R9`@8Xaqd~fi#|gYAlA(Nl?g~z}zrJf3MLq(vF^u90= zmcwzl1MksqBFF-SGa5iw7y+{Z@r@s;Dn)@s6?~~moXGFwwJm2};2!5p;CYX8FI<9$ zXoS|w3ixNQwy}DXT#{ARj5oN6NK8 zK2qL--x*MS$HYh=kOpd!H1r+P$36-oj6 zOm__^&-4n2JU#kPPg$jJ1^r<%tOVjtPuTRYL^2RILm-rfRxk_}!a=wzl2HM1Wh?;L zY{uoVACN60viT#MKeG83hFZ`OhQdr(569sSycY>b1cVL94agPH2ztO6AkBcCa2|dU z2_)S>(hbZDgbi#3{eiH7gbln2uSGKXKmcH?nQB4@2!mO$0Zu?9eBcA8=qm_)1);B? zhR_|tf$|L61sC8~k<7GpnKMEtREPF31ZKc8paBXd-@)W1m@)~j2n_%m3&zHRv9aJA z@J1xW7qIJ)wy+J3!9$4U^F1jbCzOYFfX!sVX0i|_%Wc4>vKE2b&1JQvCC1oD_Y6zW5J=mQZVIgmNW35XQQ*;OQ$ z12OFMDp2P65kPpfLI?LZ0dcs)1{=TdPEukMo ziWDH<1;}>+$~KfXBQy+1FO>8`PXJ*;KZq1e3|WEu3vz!!+K)m_;DShDbXJ(O3oirg zqVQ!vzeSvYev5=cb?649Q#1%R!*zHqQp^W3K@q48oq%#Kc0i;!qMWmzx>Q2eLP#IbQb*JQHSP2K=8oUxI<$^#c47H#m z41?LQ5q_i_5e#*q3($U*J`0b*BT^<6P@l_C2gZ#{8fm*3h`HYE>hJ1(y2;1RjUBDS9O5MSM7mv|7r%TgCiovd{zwR}GE_$k$6^@G3 z{~Cq@I;p=FY(L_wAI&2w;%kr&xYhuDH6-muDPgflmiwdzv6a z6XI!di|*G&k!IAxX6Hnj(|)$FfV$ZN9kw_IwEr!TuO;%eM81~D*Rm#bfG{9zOYU!Z z0wRHW+$uHXflAO4`oSbv0hDDc;%xP+NNWv}LN*}Jt?!Gp;l4KHw+(q}L!R0k7irs- zZIXzq9dWfIu6D%Lj=0(#fUEFQq`ecc-}as1gvi$%e|=4)!vv9z$kVX{5VlhfI0KKs zEz%i%bg{`3aCF_uz@bvK$n9+{9Rs&bag=>aJ}n9ATM3H zzbiJ<^@T_`6a1kG^nx+)GmzhIl*u=g$v0mEe&0M3>8=Ci+C4iIg*t%!_Mi-UPzF6H zgC4YDJvzZ~z>a!65$Ty6azGhCM?IsNEAIy!_d;L2(0#AV@Lc3uKL~*mFbGZo@_frZ zy^*i?Lf8ps;W2nb`jCe{1)&DCfD3%6t}py1(yu#&!vfeL(%*srm@hJb`vzPR8CVl2 z!-2>&5Sa!pgFS!^4#XY?QMU)BhcAJ623LhPa93mqc^wi4vtS)izlUH)L*9xEMK43Q z0(lrp9)^;Kuw;P#!pcGu=mq0oG3*9(9rjFQm=5V6KU4Mh zV2#LV!iJ}TrhwgqV>e?+XAJj@LBC^j0{4t1|6|GjSn@xX{EsF7;}Sq-pxnnbhIu06 z(?UKV{qgqs7B~s_AWCFHQpg6C09hxz6p2_bGLf>G7y(OQ4_tt!B9k;A&Pl{MsS0$0 z0V0#p-{jFEQvv`TPkARY6+KQpEHaIFrrj2qUJU9&7x)h50`g4%2_8U<$c(Dc3aCRf z9x)^=3bO&3W*!I1VCEZIm@?5vIOBVaGHK8nmv0$)IBXaqfA3@n75 za2Bv_``a;dQb7gi3FvGNWj5!T$oJ&$d&=|s4iE&Y;@XN91 z<=FG`nIbDlXT`Tb8LlAh6^Gy^Alu56fb1)geI>H2oCe$ADnyB_N(zfaR;K`TvAQDc z6V#o@oMeOgCtg8p;X?;SN4ft;$tqniG4UvuHf8%#BS7ehD(9b4p zh^)(IY;yBjkuB(LOAB}|vb7=H71`zh^1N*r5byR(@IYh-I@w7)I|smSkzKjqqR8$x z@KS^Yl(L6%+CyIVkk>sOAPi=~1~>tcB4kbW5^irPXbEAk01iMTd=S}(T>FTBUj=9n zLtviBer$OE2$2Jn!GTNgo5(?AJh)QiP$hUOav0eT{|wmU5$y3uIw1TJ>dX;jJ#tUv zXkMrct)M?l2I4q+5U#;1kz+0hgp$w<1_Egvqb!b*_OUl2$CChMcDy_g_i@4;C(Map zXbJrw0+zu(xD3xlengKyW`t0v4&?Di^7!KnSO>@8HoOx#*+b-12IvE%`I9dMK~dN# za+-Xf#->iAw=;f#ou4TNb%8eI3^s5E8#uECDE~8*<(U|fv&jK_KU)T{v9s9b*|8$$ zQUUUwN51pOaUMC&BgX~ez7PT>pgweiQ7{j-!DS%a&nY1k>O&ux0o&j*JQul0JQs=Q zVg*2^OUQZYDA2ZCP6LI3^0@q~$Q1)PzH%7ui(EwySI5C(;QBSfUK`rx5#hda8BeoW%B%{$ct}9UZw}?(yPuueqTQqc{2pi&D*lj1m21K z?gu}xV|N`m2CqckbM1XEkq@b%DO?x%NZL^;fHI9n)@b66DG1*HamEJ0Vi7kwa(@Zr z%UvJZ13Gh$f*G(3w!tyD3{L=i^jJW8o~-av6a#9-ngk_dV4*0*q7TKxKh++hL}_zG z>4jl9oPsx^jBLPiQzKlIBPskK%E{7HXC%B9wjGC*kQIsmUs^X8iL$!GB;Z@Z)>Zf* z%9R{4Ln&wq{eXB~2Sxd$fdargybP1c}iFXdO`#&12!>BNSK7tqWtmz57GS4gC9EaBcF)~ zpQtfxg`;o@9s+SDX4W_{@+T(k#LZy}tb#+p^(3U1q!Zwm4;Z`O8%SPKwIS{ki)KpPz;ta79#J$}jH{K-PRdfPV7T zhweb0^R0y4qL?32`MD=Q<&nQ8a9{p!VHC`QRj>!pcYgH!WjP?8FFU{hcqgiW1?af| z`6kNZD3Ie-)32szm-Oq4P?s;jyU7=&o{gXbBTwE!+U?uL|i` zL0?r?!7mUks%lm!0$%}XS4AIH=K(hNl@p2qHv83dxC?JYRdYbS{{^0ks*e3sPYc-q zIjU2x)h~#uVL&KQPBk`zTU1T*UK1H>M!+>uwK4;8)cOv#!UIvYDeKzgvG#3Ib+EHK zeS!Qk&!g%#fy3~-sCp)(f$TsVRIe*6fK6}&F2fT*=k*gnMj*cW#9bd7txufwDeL;! z8FN9ZL4H8C2H}AG4arYK;%SH+jVz$N8r>1qxDI>^r@$?$3GGzVv7(w)1mbF55IAl= z93F~lkq{`)7T8ToWNDQN8jEV}0&Ju8Kv8Xy0Cl@9^`k9uw>>AS9rCuT3{!x#+G|h( zXjj^!&-U2d*IxkPzupJj^EK&r*e0rDIw$}&fU@s60)Bw)a2}qC>Vyq+@`u7eo;snw zPNRYRblMG0c^Aj<3Igs=?PV z48Di0a29?M#hj4po&gF%E$9p*VLt4HpW!!AJxmCMqEHXMfibWc_QDl-C90<{1Vc$^ z1ifGaEQf<|1AZ6PD-nDFWuY1Lg~_lQj=~-IDC*l}kP|9GD;NmVVLki^_aRnP?^J+H zy+;GG_udX?fq4342kKGZAVB`U`1O4+svmjk$9?@K0`m1IfBg#s`Wb*s19}7L3^*=o z;9z(vYS0?M_6OY*HJJMc`$IiZLpUD7eM2+DE>U57MGb2R)bU}*L=8tL!^1?4NCDr& zW>Mdj0>XUP9nJ&g@f~>{nFI#F@1jN}1mYj{M$~BPNjT|*(}spm17rzDmT+VlL!BJc z6$mrt5L|>u@J`fNC!~ZB2!#sJ9Qpw5&Dh^WjYF<+Jz*uFpK+As_^d#<@y|p}&|#gZ zh!LVDBKJhfc4B8ylgJlyHfj=jnoL4cm9Lez}3P#OBdR5$?SZ6@u{%!1G!hQeD>v+_beSPqBbp{Uv9 zd-g3Lt~oV;ynLSy!r_sqx#(o>eE1;hhjKt%KTwW8&~D80ff>L(^GJ7oNoW8^MJ;Fv zuSG4yUKUbzi(HTfkYN$BFRlXUZwY!{g1eORUW!dDMZTqzV1cM*$iJ*2w1D1lU(|Ar zmtThGqE-<1iv6Ni2EZ_QDQcAl$sq)017TNlz8d>mT?g7gf0zvDVf7t&FKP{Cu*M&j z0lHXAIjyCP*6xHS5G86I`CMNH(DQox_yk;sUqx;B0&tkb=(Q9 zVK^Ymi3)ID)Q_~0Ka!u5NudXffYb0s6mui$RAs=%PAvrdf2s+uMV(Fz2jMP!6m=#L zu*ox%M4ffPGEwKq=Q+yre05P5Qi=N6Pt--OT||!;k@XUBT*?ZCpc2d!bs6_EGG1N_ zgt>ywt`N_amVlnFkngL=dX;j!8VZEJItw(s*=*yN4pqHb1!@8a1m>K1X|Dhu`CYZwNkduu7&6Lot6Y=L8TY@9zI&9%J#7EpDN*;ar~9-g_et~qPIw^dK|bgY`$auW4dJ35r3LKoF>yX7 z4^IjJdHf{>P#(XGgz0co)YDqf5s>vMI(&Lv)UOF(7n}s-{`G^X-_ZAOS)mj(gtMZa z=Yh$hUSx!-K%IDDpOeQIS46$E0KL7$PG4??m!e)}f-XSbUVRkx`Wv_}>J9O}HGwpL zZv*J}9r}LvMAUoke}7xlhae~p?O-|3rhE*B^`fGDVFc^}m< za%ThTiJNPldazYA=`Wf}21{U@@WD@*42yxyne7e0#S zOafVe^{dVX@D0oZK3{J-pf21N&GLtqKzh~_(OflQ2b>elClOSK2skU6Zy{I+J4H*7 z8rX*-p9JLee=b^p34wra16aBnumB=OV{MNXSQhF5 z`YX;H6VR2197RR zBBC3&>aSn3szst=ze_q_JeZAqy*r)l-l|vg^83h`LFqdsuuq-XjeUpsHdM^ju{!RZ zl>a|Ox&J&~_a*b4Cp(0g@7z(imtn`hAJ1Rk@0(l7e~wvHPW~N_q=M@!>Hf*j$|WmU#Kb??1-9%J#FW$N(!;>Rb7w zuTMki<8n(+A0O%O^QH7l0P&@7(*GJjr!;TlOd$Ku!DVu!P$vWR%veoy# zZ1&YT4wQ91O=Oc#c6sTIW2?0KtFU$$-D%^Q3Cmy+9DvjD{)3F?(uecKuo1s6VH3xL zbf^0^?%KF;vQf_}yS&G5#0USG#$s7(d?%Zot9W=;Q`R|~%2wwn+3Z**>l}w=U0i&g z#fDop8E52WTs)qcj$rBj-^26#^ZT6lrMq(1R|C&)-gxFo!@r9!{$8JVv_r|!|0i$B>_t&kYlzRlS;{Kn+PUFv5RXx9d{yXEFObY&2 zn6sqDKjnawuv$qK6ZcZW)BUce(%5z0^TKonr+nOzfo77pG|fjc#Ju~@(@&6827V%8V;SgkK0e$? z(y$p9?|wS}UvWJV-njoFE+dZ$=JR=l3{%Q{I_AB{LQe9*8x})s_b=Ip9=&+RIS;{B z&VkR$+y2|knNr_8DD|EDxVF#JE)MxV{+_TnH!cqCw#QAI~;in@{Iny?gbK4xPtj zm$eS}k!RVbGWjdlHHEMnu))u{_^0#Ful`ERD-bCSAtfw=5CzEci( zUHkZN3^Y4q-_2#9^-5+Klv^C0Nw}fLPZFx%lwLZ0%)eceY8*E=nt2Z3PK@`ThdUC# z2qTO*yb#A9$J`SpEsQYFA>2>?^Ee)bUxd>~`glng?|wS}*KQ9pEx)tr7U+d{TxBdQv=c#ieI*G>|Cx?BHMcyAhZI-lXJ zFQq=m#b=cM70x`gC!e#O^H1@7Aima8@sIOQ{{Mk%hVnZek2juNvdn1{EGwMz;dcD_ zC;!h}v#+dY9C^sug7RUEZTHulC#Z{M@Hx$KjX))rcL z7FlB$2X~hmHk9=;b0RV>gsJc$PF7?-Y3JQljDG4g`phAW6^ba+O9k7ND8`W8eRC=C z?V!ZPIKs7B&X~>QOoAfPIYCqIpCvPVd*cq2X=Y8vZuMoOIYDN)%E}&B8TvB2q@S6A z_`jg6zhsOuin_WB+Y6*l{$Iekgm&7tlVh@ga_#4(G5ZhO$DQckG-h1csC->m9_Y-i9-tDXFxK}FXDcIy8`AwcNhSDoO7hQ zGoLInPsnU%H^!rX;2F+amJ_hqHIBf1QhN;~IjY|Q?hD+@ck_O{D;in^JBmlx5>~d8n%mBiK!}Iv#_P8Sb4w)hG{@n>XgkxM+75YE{95=*o ziZr(laC}q>+xRq>Q9drteWaf&sWf+fMfybup98<5$Z%TrTL~qMG|JfZ#CcFk+2b&8 zU$lT^HH*t`J+$<|S6Lz9)xgbjz^VRmvDoxE=ve!GlIU~2M z=91q#kCThKXOoO^A>%Z^r_#ptt?Y0ek;_&I*^^)=ag>l_&K{mcpXS8O`ZB{&1>3nS zt8BdOM;)0=e|fj_y0oI+PIvZ}-uxy_<#>)ad>UNjH|=nHWg$#7TgvxlOSKXA1stN? zn2h@rw!*Kl4Yq?FZdd&AY~1JY$gCm*_4abbd0MX6uB)|Nal}Yh-w(VL(ptK5Jjy<| zU864F2CvKSZ(S7{2QM?#_0r6CnOB?{!-P{hp7zqu_HhZ zdkJG6wIy?^5{ILY(&H(FJD7In1l;C0*%Dy%kKrfZF*oE9z>wT805Xaa*}x-n47 zL20g+(XPu}Rx-{-A9G#9W$x$a+BR8atdm80Nm;JvV^Q^OHmJ7buzmao_O<+k$>;Sy ztmldg>%E>+));xb%yUkWHO^VI+3l72C(?2b;=%RB{SfEc;p8S<{Zn}R{(pH7_J73@ z;&D4y$UNGsHjb(gB|q6iYcsrwPm>HH2hm+3z9`JE{tk>)wsW>r&}^sSy<)PqIT(~-`u(#a@+o|o}* zOa-ZoTMhkJ)~-obV-d#@($kJpee_%~igWyqxR*;$I}Kw1;pfS>P)$$Ed1|S^ec6oR z99QAx+q@ED@4`f{aJ9)XM|e_WvtcM@n5ii!?OyNh|W#*ytyrPZ??Gn?gGJ z1~7+HNq+UcF2DM9!5u5*d{0Sb-$T$|$}w+O&e2cHXdI9hj(+NUeYZ;LOvwUPa2%5x zjxgEppq+M{R0i%5IHl{dL4POn;Itja87e#UUD$At44@fmE)u&;B&sd^3Zr1Rq==d) ziKC9dNl6`jTYREeZym+j@~F#_mh%+R9mFSgilmFK%JmcYFZG6(m=uyBdb6aC(h1X! z`x4?mU)wEWqfYY(ssQ7KJR%9IvvULOq1_hhYq)-Z>lfqNN7@u`Sb4-n{+I_jnb233 z_zsXIW>5Nw@%?0ezpnU%sg!;PRLTsvpN?@`;@1+tmY(BTu zmMMS70eS0dQ%cVLK^XmNo9puE6F&P@KI?!=8i$PcEyZ;pKczmAkl)9IE%BrO?h2C~ zz6n&ycv63IUCUHz->%#X%;`L`NmPl=3n~fr5>pYz#h=@L|1Z$i&inrhnE9~_#$KPs za*22zVt$~1S4Ljb$6M}m&pzjwa8bsmv^o4f9`;@jXRbbu0rnV@zUNt(JAPwp${E_K5q`Bfrl0NFEqi?&GQ)>4A!C9WKDQ*whk9?1 zgUwRnwjum<>#{7eO35C3d`Q3RncX+Fv06x=-z{YNG`}7yv#jD0;d&|8X>)@ZTWm^L zPku&@8a|Qyj@x5f_%zlvy3yz)i%u-Q(NRWLjrU_Dtt5>(W{=TH=au(4iJfOVJ?8km zV|Tk+OHMh$yD_)%Kjg?GN5wDdDe>&F+gZmhnd8VR-8r9w{~SpeH3s*4aYWs56_Gmj zIKUoLc*!XR8J8Au7C_%+ReC*y-vc1-w;~>wc1wQIv&k9!w&HgTcZ;NrQOK~1{?thN zCAs;nXrkwl2yKI`q|SGspLCKrilyQk#k)Q+KFD$E4>^)XZQytceg|ww_b~1b*vg#J zWxp3}w2(m-x%})6i=rO!TVRi$`#Ew-XUe9X^RTQ(Po0@JSZMdxz2gCUOwfcf;y!17 z8E+nyL5{`D!v%5QDV4z)B1M=Nm}g{DcZsLBJ^##jZh}3wv-993zqfzrJ*KaBl>BYN zJz`%&4;SovDdQ33?XWmvj*8Hy*dYt_U^!^J_E_)}k}f8TlQEVpGxmQO8(eI=-f<$& zX^AsJn<}%>=dX@Mk^{S#!L!mYwE&sU@8V3!7}FlwqT6Ifc3I)r%X5Iya+2^YYJxv> zXt%LZi;(FY?gduP+|jqnP;7M<`kO}?R7bv>@WdX`4E;Uce1wDt#Xylj2^b?CyPlnoMW{=4!=Sj@_ zFEAd_x12}2&_pij#bhZqdq>aYxvKY)@hVcf#M2nJG57H9Tug=}K2qLi9`%Ihyxy^; zy#^wdxPbW+)Z3Fem`;0dkzxhkKCn4=dFwfHiq^VU%201@_HesESaO%S- zyN=RFC}r)EP)YXDiT%~H$D{O3`eQHs(P=M98C@H9IQlEF2VJBCbi{tV@@iL&l&_sgsk(#Q81^9VdQ zaAac(vV>8I?7GQV{XA)Iq+YBe&28AtD9$5%7?;@~Z*Q(`qrDny&;8qT6!yFVn)@Nzg}V*Pz&PY~5RP{ku|0_nh3?P_ z%7FpoD{44=4Go|&esa)RjwDoZ39Wc_P5KZ z4A|}WRPMWi%P&~;V2+OiHteeQT*PO)Xy_frmZwtRkd-SX-o0pvOi1~(Bjzes3Pv;b*z#jFxk>5o@#t67Hm5>pUQ(v|VhDz`3>qd(S5e znA4nzJ@qjU$zazR!G=Hb-s<~Gxc=znWP;9ei|23lbGz)$1+0OJWUlSD+^2m!$eWl` zv8gS#9nu$g!K%hB`0wFO%d7e}!A?FdB#whTvz?C~JBc&81oeZzkCd7H7qgDzrCyxT zh2#7_;%EQGP=7wQw84f~*=?^q?`&fQNNtC%ifH_X&_>L+>jq=7$rkg3w*Jw>3g4>C zwH=Zcu5`2w%wO8rvD)*Q_OGodcB@+v+fXD!*!#%0Z_{_OeLnwly%xjF#T*j$Y=$_o z5PC^oSHDjC!MP?59bZk1NYOk=Wub>NZ!ERzRI4KgB*5c|OT%Y{wt`FZ?f9Ky*-ikv z4YGM3*l}czJ5D}@GdqUNxIQAB8_BzIXInf?MjAudr}D7&Li6Yu^_qGMy_Mce@2d~h z$Lizth5B9N2gBpY?N>5G*$kC4RLf8^L+uO=Gc?K2F2mdm3o`7^@VkE^{}lci{ImF% z^e^vU&A)+vBmXA;&HM-ZkM>{hzs3KE|Ihw6{9pLL^>+vO24o3n5zr%`f56ayF#!<) za|8AV+zxmW@G?-b$FVce#h%Bh0)qlW0<#C^3d|Q+G_YD=jlepA0|UbYX9und+!(ku zaChLjzzcyF1MdVr4165;G?SLenJH1Gq?yuY%9ts8rb?MwW@;Ct2RVXL2W1J$9uyWd zI%r(b*~~$iJ7hka`DyT?;8nq!gSQ9o4L%rrJor@b+2BXPFG3{58B#2yRY<##1zD11 zNtLC3mbO{Kv%JU>mED;=S@!ff2IUx<(~~RpviZwBD|1Ckq;F*Q$SRRFBAZ6GjO-ZM zDKac_M&#zmy^#+iAKguVH{0EUcZ=U`ez)y&$GgMsoNhjIhxP+xfvTd8YAKaKo!*LCX%WmQx01 zM9bO!PRor02L(qeYdQE2EuTTlEi-RJ%L~x* zrr>SCyMhk{9|=DBhn5}xpye>M{5x7s`cJg1MJ9;M5m_~|W@NL-R%p3%~{;@EpfNSbA^@z;a)(-WwzGfM44Ov>b7N5BBl?|Ma-y zhrE0LFyhmdCqF;=>0#@K)X0Z(9#^n^@9lXw;9>tejUNnrnDhxRyO9>ZBoC86tj7N$ z4{kiT{oujF%n#E)G#+aA50EauG56QpUwnVg{aW|)-_LRX+5M;YFX4CUe)s#G?{`3s zarX}2t9`G~-6D7BOZ@HcuIo zC=csK4~F+Jp2yF z{n*(sJYCPwAZFwAB97P?)9BUoZ z90MJ-9D^Ly9Da^Oj>L|nj%1GHj+Bm6j?|7cj-ig>jtIvT$3#Z~2P@sBGxXCZ2c8mDHfrRpdjW;v@asoxyM9W@;#9cvsbo$b{Jt)x~# ztF5)wMrvcVY1(q_uy#uOS$nCy*G;{sURp1&x1s$Sp)b)_=o|HI`e{dLM|DRT#~kMr zM}Fgl@yhX)@zPPlalqNZvB9y+G2XGlQQNWJ+10VrG0XAL@yOX$AMM!Z813xsc;MLW znBc7ItnI8v-^RhunaQu1&W~DgsVXh`CPZs#!x-w6{3OTBaeV);ysDrosxqpyV}>fL zma7$NrCP-+JJPTCy zleE)1OM9)0e64kr4q7+*KoexL79mr#i8582#QgVsS;<)LG-HZ0+9o-xZI*M|RynV& zFsEpTl#g~yCDKk?Ra6@72H(%RsnTh;%-Py?6`);IIW;%mJM*aA{CJ0IQL2)jNLAJo zt15aD)mtyDhU(Q+m|k5C(`%^ldV5}b{8~lm9n?gnEu#RkTXd zRO*={wff#Cn=6=m8f(0kiF&j)N3O^Tl~VgjyJXd}+Nc6rtm>)C>AvPXtBV?-7dJPl znR*X%td(Bxtrl84)grY_AEm!HH>%6}4)wc!#vE@(SV2~1M-oR0>y(wx^~f4z4Yr1` z;Ouox*5O*KNVrMBra)ONl;H%afL=IVp3 zOU#um(H2-&vzaD2pJiiTAL*?1;5lU~RZCB;YU^oK9X+kOuJ6&VY0-LLZI5-` zx*@rwmR3;(s>OOc{hW2vy2ZAXM|eiqU*kERwofmomsMl+iYijysiihAnwPaQRxhox zR#&TMrM8w>%Z-XgC8LT_S=+0PvvQh~jIHKX?R#yOHd|X~bTOxDm$j?f6|1rKT6?3t z(qgoa=3G70{K4vK4b&@H3-!u+BfWuMRj*<$F_-FXt+ZA}D}$9z@2~gK!(F#s5A~(` zD*c4HML(}!wF0b6RvxR6^`+_IQ{9n91#_}>S39a*Fz4uHth`o!t%cTFYh|6Ww(I@0 zF;=j3&$@4gXmhn69OtZJjth?K)*>s5xy*6Lao5q`F~E#*bT*@`3D!8PfK|{uXI?OW zHqSacIeS@ktgO~bE4%A~)ygVj9x#75-&zsYc-I{(nH6TWb#`-hclL1hadvjLv&L8> ztQ?MujvJ0hE7Uq>ov=BfRz0h(bD;UaIn<0b-#Y^8$JP<6k5$+_XbrGx zSf*9p`e>!HqO52u#)`Gvmd7P7WuCVVn0KrWR!3Fb73BKGmDv^S3UQ2437lU$zm-gm za2dqB;x%dzdqVDRE>Y{#^ zm2=nCeIu(;-l%JIGsYNMjM7FKqpVTRsBP3SIvZWAO2!bYo%OXbRL^0A8N-Z`#wcU7 z5pK-Uw;FR91AJqQ)pHu-jCrms<_&9y{z3n!M;YH+LtR;2U%0ZFtE>&?cUEF^y7iU0 z+*)i^w!*cV+6mRomED!YmD81ruf}}g%58k-%Hzt*Z1pG^ZDcloG%~6g@>rgzghnbO zwUNX;sS25=3`_FpF|3w4tb16`Cx*uE|Ah_RaH?#wTXi>&m`BZH=5fPi_?RaQUn8NB z)JSF|H&PfWjWkADBb}DU*k)`ub{M<0p~fC#pRwO6YaBF=89y4Q^n%7Yy_8nVIIkZz zF6di~OU4!Bx^cs}W!yGy8uyI{bjQN9VcJY9xA8>pt{u=0YCjrxjeFV*tEwx%e#E$J z&bPXmKN*qcC@Y1z-Sw-v!}XiF)%Dce=6Yt%v}&4*tzm|#7chd1-^^)N1#_KV#9Ux~ zV=lD1n~SV(%^6lTYnOG@>Sr0&aMzcv0w@`tGV74X>M@cRTqqlMgrrh z;b&Ym5*gQw#M&|=z<6d3HGi}Eo4Z}F42N;Z9BmwD9DIhg1!v7+<_nd@IH|pmyjohx zr=??L@6V`yxDjYAHCI~8%ssBx<}TL@Ba`u~Dq@vWMXfTbhh9kavO1}6t5Kju@xI zX*pf4-mX5bzKo`mYbjY#QC7-o8|yWajepq!EDJ=ZJ{jF7RhpLu`JT2%L;9Y{HU#wPRbS7b z8t55SU%iCtr9y5Jy^dlAMvd0%smXe0HAU~Drs`eQG`*Xeu79I`(1)mb`cO4r z4^sOG+O2=D_ULofF@2djt}j<7^)>30zE=IDuT-b?b?S`1UY*l7 zsq^|~bwl5)Ug?+AYyGl%qhHaM>cjO7`aUhGHd-6y>aS+G21p9cS5j(zl8SdxQ)`B# z(HxRibFwm9XUq0zwqlQEyLGqtD~|-I80n$)m7ZEZ>816TZ?yr^TN@~Sv_aBW8!Y{_ zA#zFEDVMcfs-f<$8tDP5F&|`TqGwV~^&r(u&#ap3!K#HGqFVA5;a1Ejwbs8-ZS-tv zul|GDr_WRS_4&MlzCaz+7pg=0B6V0_tZwT2)h+#ix~(5ncl1N9fv!QW!LA{$p{_9F z3nQD6-N<3&G;$fajXV;>+(od2@S&!x{~u@P9WF(&Mfy1{zE-N8Mgby2+UUCI`sfDxvHip@vY*<|?C16i`(;!~kBxRpkBjz8kB<&XPq4N0 z#OQ+br0B-<Lan-6QB3^a^@M{i6ZVz-Ul3I2sZSjfMp?f=7Z!gU5oI!Q;WK z;ECv_=;r8_=+@}A==SK2=uZ2!{lBq_Cxz~dR2OL zvRTpxF`IXZcZ+vVKTPJtBjW?&@yUttn0RbFE;%8-CB8M@Bi=h1A0HJTnQZRPcl){i z@q~C{JSjdXJ~%$ae;l6{pPrl)SL0sER7R8opfY~wc3pRtSniGGdK=#SWk zL!zIe-{UBb)B6#$Jc#~^{*J@+f%L)Xm+0r{hjdz;L_fw^yi__peaJiS-5@v6T^-+J zPBo{*_r>?e_og4X-Q8YpAGf#L)9sO5l1xpmOz`Gya#?a|a&B^Qa(QxIa#eDEa#3<& zaz!#Bc_n!`Sua^Pxiz^ZSs@voJdmuE%yu`qhurP%VfTQW;qGzwVy=9@`_uj7{)nHk z@5WCcrq6fo7x%0C-TmfXaj&~q-J3bH-D_@+dn0GQTNpp<-gY;;JKTfrHg~t19^M|_ z7d{+57Cvbkc3Zo<-7}sMKN5cve;fZ4{~Z4k{}%u5BA2h94 zk@=F-{C7!~wDX_(dHzxVUh=5_-Y-m&WZ7iDq;2xJ|G~fSe@k9U9`U_=Z@-b>IDXrI z8y_2F=F4zJ`(nN(8SMBgXSewGC9)Ql$IRmL~}IZv*rVIkC*%*8%Hd^#`l>=q+-vRvVzz<-`|2Hz4uh&<#nv6SND7kAQY1 zF;;5=xknXgwu80+tPkPtaG9@8oa7R-jec6vLA`X##m_* z;@hBNBY=OW5d;H?8x9>r@Lpknm4_UOzJoQ0oKWmKl!Pc-z6&r6i}v!JTo1w%p(99m z8gzdWitmji;f>G(NcaSF6bWB|9!SF1prc9nEp!YCe}RrA)0PN}RL>@kN0B)*!fsMBhNKCDC_K$rFghAFn5o*yIKh!~X)YDTrr6CEvh_?WK-E zd>8Z-`lMo^PbmrXX=M}WGs-s5XMy&mZ`F zD)dicPk{bKtoY8~Bw8E#55ZbzAjKEtMNxwf0@gnR6B2oP8~8-d@6d<@u&0R$)=BZR zy(E85czgy}JH=1*lKh#0^2oC#+!l&_3-Syxa&Le&R{WeU$)6>14}f*o{5h0Ai+0E7 zr3v;?@l&|O9}ks$0IbjA=WmJh(+2e?ga<%ZAkt@>6-hV>x)PDT8!IU};epUqi1g)V zRT7Sdu12I^H>;Cy3>5w$1m{586RZmdrbL2sp-lv9#er!i!FkXQ1nb9v=}3a}p=%PX zDFOk6O-bQCb zHzZiM4onv!ZQ67tSj)!G;u2}!rb6<4=_)=0yu**5xFz8%Xibs&??%F>q1_d!+a4s8 zJohB{-4y&Rts?c%n}qK|H&UcdHYVXbDEh1%sh>>=-p9kw!V-T8baR4#uMtSUlOuJ~ zmxR(!^;5(itt3QylKv}4Y||k4#V7nEtRi;Wl2}xYkunHU)@?}mJ5<_(Am?sJtk_|D zWh>|o#4ZEfQIWKEB6eBm&dOxyF2st>c2$ms?nbQKN7|Bb4A_HMxu>)%;aIR2v2y>t zl{29G5IY>YuW}}IKVqf5^jFS;4j@+A%RuF9=pbT8LI*3CLWdB0I8@rOa2Xgz>=978 zH}K-maz9`XgUUSw`Cifm_C)9b%5Bh5#7bEXRBnfkCRWNaM!5qzmRKpvIOR_0cw(h2 z6O_B46NxKlZcL#2*^%oEIU1pj6(FvpXy5A+1$#Xe$3fZZYd)F;Wu_b1~s z$mb8I;4{G9QD9CbG8Q+d5gG5x96<1rp40&d#TUiy0J}~22~Q$pOe6LM{wAo{6nMGM zxg?OZrLO_lpTf^}l0dGzfY?8v7ZU7V1!fA7v4**b$hgK_tgHmRgoILFxdw!9KrbVB z7Z^XqNfPndE0k`~D@phl^eSZnbgI${y_(oI&})d7>#ilqfzay|vBUMmZUVi5_@?{8%1_V-hDqP`Y?%?h0Y-HiqJ<$@&fcx zg5TG|PhXPs6X;AZ3+WsIeS%<@F`sAVbItzv{4@y%LZ2aCd_jCd=m?|@gYbE%;>8!_ z-oj=;d<9qseTf9^pi)i{qM4Z4M8?AA6@q^!h@Y%fwt>pufL#tMZCeoAN_z(OT<9Ch zJy5YVu;)SNDpC(`5qm!LZAI$j9bzwlzN<+6%p>+f=z9eFtATl+1WQBblQ4xYAi*-w zg(PeX{eZ|ihxw3%?V%r$K?8>!KZ{5xW&Bi;^gkn^*z$8_Tj&?WE)V@u z*$(;@u`58oCjLz5Hzer*{g#AM=id?R6XT~PiL4)(9|-o2@iUVo5c~W@u%C>dnBO>b%*cl*Wq3~BBH~=cw3*$gSg4dwPYYy6Lm=T%Fg>4kHTdDiJ z{o*j;xAz*VrN6wCV`Zxvm&-!hsZimxUMoDx*m~nUnunrEXo#cKx7OU zZb+<@wF{B)VAz#dDR-I3*f6XRD>kSS=?}sdVrM{W3i^z&n*u)#yAyjbw1*<+_XNFA zc1fo<2_!EYkwEgfv9bkp6C(4Ta8n}BJy~lL>@eu&#J&RU1NtKUdC-1D);qCFm}8!V z$}zzE6G7M@!J$yOpD-M3N%A@8R`@JD3brOz(%Xhuxexl6e9n3rbUR`tE%9$)7C?6( zb~;q*NszjbG6OGdMCt?hH=$Ao0?HHaN|K4t-4w~w?m&D>{=O$kB+b1@avgMUlH3U0 z2keXSkK}DX5=oi*lStAWpgaN{sGJTRL=y3%!Ni{f9YUh}p+kv(3o3OZTn5CZBf!OA zf8`G7ND>_gJ%IT0p`#SBgZxd9G)F6Qp<_rQBK~sd zWMai<4Sm1au`XjhRWZ76&oHwtklDi#7aFKMXdPF(Zq`V zj{(P`-le{dBUWsAJV{oAo%{5-Z<{kAhVEU+NX4 zA4AU|_Il`<#NGlui`ZwOXA^%PRO%ksInZ;7l{z|)B+XE%OOSMgUO7dB5gwIMi>mlM#3N<`2+a3P=T!13(14fn}`+Pxml5V5SxOe6ZBSa8$N#k zy`A89FN09p#+^XQbQg(Vf!egrqa{0pcaUl0Oj7 zhfX80*ljv^2;YnUJWTwb&>18i1$~70-=U9^MEvD3l88^uBuO{u<0R<;okhHq7=U66qTjkYod>)E|f?U*b<7mb5=4 zv79Tm0Qh%BLHIFA#NM9}{F}KTTtx8i7K89pl8C)OBZ<_>=in=(BWZk1k{J38N%n+( z3;w}5xXuQ|9R@YTpNpQzQBjS#LB3&Ul80v^S3hIeF0h$nZBs3)s_O}^v zCqmm02j8}Bi8~Iu6iIf2wj;@|(4~nx9J&ngu$`=<3GQgV}yMcoVjXXpyV{{dZ* z_+Owakz^z2%EZrtt^!s?8SjCvMm&7hu1?%Z&^1VMHncrS?t_*{g1*6a03DIeCeSsB zI~}?f=!86<30<2w_^|Cv+$qp?h&vg&E=hKWu1EZbP_!$-zYK-X34RfDL(m23p9<|t zJp9d;i8}{cA^5GPz*dQqva}HV22x;aBzXebjd-*Zc{U3k?Z);X$?eddB)JXRiy-=6 zV0#n)0aR=ak~^UrljJ6-*a)PO|4m8qBy=;990c8*B$9R?;va|hC5iZRKa%VKZ6%3^ z)=47TAoz{)K*ohaBK5x|z;*bIxqLn&a~eMfpLYTSkhc&zkR&sqgNUCG6+2GEIm<#P zfyqc`f2iae$hx+SzlBtMLGlVxX^)44Bk@`MLfR6DrS8P1f$Xu!{*d5CK&AZvC-)Iw z1VKmW$s|4nssz$zB`px#1wD-fQg>$%GaPyrku?BW8_oF$D)k4V(a>{A&;fcL3B+dS zgA2er;6f5enWhk#XUn`!2&CRGR^~x3A;DYFONs2~*vp8#hhZ-#vNvL{AVz%TO65K1 zRm2Q~PF3EAUQJ}})n2PeeO*Untxm>Rf~h`3Kpy}P;`7eXX<#}&kAgl# z60!foB!b__JWxo)4v&yTeCAOSiBCPIYyh1J9!HwTKxcuc@cBgO(m=9!NQ&RfUZEKY|)A&fbT^sDNl#G%PNViDco-^n0cISu33P-HJPBPBh%Ln)orp(0${u`< z*ru~0_E-m?4dvzA5Q=)s^Cjg6vUU|!Knp-Sj%teJp&OBTXVhJp0hO|ejUNS4Hjwm# z_Ew~=ZlpX7-B^)&-$apm*_6ohIoeEl1-d!0qJ4<0cSL=aIZ(MDuuni+6|_a^%kuv0 zK4?RE11j|@%mrJ5tpM6#v^CfUpuI(CS2^>b+kqXxhhRs5`-jIscP29LjL2hp)`U)0BtHice*yGh zg5T;1qC<$k19~Wtv0ii-@l&CPlSJ}$1o2lxk0gi?9msP_@Yg`4J%B{=at!fOcam?A zi0>Rn@cSk~bUaC(f}TJU$=8V_iJ(#+Ad%}%2B)GgtwB#CM%vKn%1r1PB<%q`Q<)Dv zi^v{tbT*MOY;+EhemXi=nGQXVcscicg5TW;BC(^e2Dni91S;he43N4M9sw5{lpHLZ=dWrzW~u5xa@qL2xTnd_{N>Tt|Z2q1P)fL2ppn zLvK_TL2pu;pf@X@L2n@*c8}y9AXyc98}XuY50Kms6@LQ$PN3%`AbOm5NpBWOzk^Es2{Dj56XfqwPr!@+KCL_hRRXc`v&zfR=SU#7 zeO{RjeS!F6p)V5u4OHqCn9fkSUXbhL8X#*4(JLgVK<5zIlZnLcAgDrLBjM^$sRIyf z4t;~jo=o&62~L5|C9=*MNgW6sfY==*;&<;5FFy4yNyPs1h?jbJkKh+Og6MtXrOxJ) zRPw(-=?E3S5xxW;5Yrp_AxT=H9}%+=^kb6rg?>WJc<3S`bKmGwVx&(MzX!wO^BbY%O^O`B?m0dJEJjLjn{Cy;S>^aGJ`dh{dl$3TA~ zUef*<{DON)8Gj{S+RASvmAwB>?BCEo6tUr-#7N!#rO0PF?;oVOG3KjXiTGf<%}Crf z2-^1}G5oRp79>Vl+HVQA!Zq;0_S=zoHgpFPBmMR}k{G_zerFQHU)t|ZV)#n?J;0tw z2fomL9}=Vf+Ycr&@{VY9IWC3{1;a3RPN0{Q7&dG_mBjG-_79U7e$oCh06V6eLM1IJ z@8lpT1teGjYDl;eG$aB1q-0458e?kBolu{ylR;7#t zD0it1k^Q9-Y%K(^MQJG#?hS260y$@C62eZUWk?|BEK9`$46gAUF`Z zB9Z-^l9U+)qoFGk+1DwpLV_{SRf+8LlvX3bSm^3R_IyffkYF6NJ(2yN5)3T_r$d{F z?4^{NNiYS9{z8zwjZ#MvTn}B7$X-HeEfS#LD0L#Tw@_M}1UEuE6WM1dtwRDiXI&!u zB&GF8a1(TWBKr`f4M+e#FKtLOo{}y3~^xv1Kn3&WH9UM(nx~2^T;&CPr+$36b^d(x$|S{Wc@v zkI>DD5nJ{l;ZIQUdtk(_;_D#%87lq_jFh!bWWBl6AV$i)1(9{<(w4-C4bV>rvJPF^ zni#RiHbmB=OWP8&D-`{SAnUcI?TOh9x&x85+tQB2><-DG+_OJ1 zCqoAiEB7Br%(>7(#EyavCh}fgX$Y~>W``1a&#p9#SZTAviMaqef>>#@`xA2^bR@CT zW)C3d3aH!<$a-Z-?g7k|P)Qfa+GR=70_G~HTo3FqP-*ADOofgo_E_iyVx~bS5?L=S zO(JGGbTW~(!_q-S-Z?8t-2hofEFD7R9kkM+MAj5bhY>RadN`5w#nKVPJPkdP$XZ}Y z$_C6cP$>_Pb-|L90hs5Zl5Zeuf2HGyyjxZ}p2#|2N&Enq7ok%3K-LpWQr|${J1d<` zWWBI-3NbH3PbIQ$SUQau=@U*TvR+s^gP1wcGl{(ydKNM7LeD0$E?7E;$oo?z@gX2< zY$fpZfWL7yP< z{zd6Y5=b7NBH@wHr%51rc!tP(8Kq}Q@CNibBKzSbX>TBS6Dn;C$R2q~+7&Q+LZvMM zE4F%>n7yF0i4|MDLd@RKIYjpBO0N>L5A-!+J3?P4W?!h}3D`BEk`G|^gU%&(E$Cas z^oPDpY$xbD#0-GGOJonQG>@2p(D#V#41J%NLD2a`_6AD}h#3rBNMxU|^Z_wLpdS+1 zGc0{XjMVMNMD|ijpAaMUyokuYO6gN#q|QGhvd2>ToS2!=FNmEA{gN2*g|CS0y_CKt zW)}1tVy}UIOXS_&(sxAmXG-4_Blh@#$bLoXM`FY_KM^Z=_?Z~7(=SB!GfKY_BR2bu z$o@v@ckmDT5cq0S06gX}uv=3Cmc|!rLYDz6;`?o&tAO_S{zzyuK%K}MWE1M3sVlxm z+ifa?9{7G7v?tgM-=l7uHV1Y1Nf+prU_1E92mIl-WQn)V|G z_0u$fm_^Wm0BJ;5LH7q^k-nTS_Xe`w+%%q~$3oH01zBTlLfaP7)Fie8_-%%u=@*GPIk6n#e1TzoHfdJDXZG(UvCM^dpTd|F7S zKOhUy$|DCh?m^_$=pt3eZ-KDB7uDfa&S0nLw=;|ba z-!`uSN;qc>w236}^JZkOxdXnx1=^9sdqCGD@!rt2NFr%=BJokswMh(LZ|+Qz&7ob1 zL%lbbK?P;s4~jO>T*JL4K)Zn+_52LeW2Tu^g8GcBsm$nBiJ22c|BC@4q_?Go}fRzzYRKoxC@}; zNGv`e^#S5J&`ID>d|m`S48Y&x@1RmQsH+%$+k7;M--I3mPDQ=6gNm)sz-RH1Gr?K- zEIxG(iGPHiOX6Rl=Mfh|rH+Ave>Y41KzbkaLK35`G*1DSAP;{-FD1@GF9Vn3S_hp< zk`nZ45*z3>B$o5Vz95!+i5)@oCsb?)VoB!)BI|U`HxgO@YrcuZDfDI{YktkQkk~_S zCE*aL*d554V)N}p)(e~O0C(bE5%eyS-VeQ-$ogXQJtPjG_X5}>`U@)OfmrPJ0C*6# z5}ih5y|Q^ak@d^whls3qHa|>KId=w$6X+vE)1&6xnuslijg04i| z)zFnm4BK=-9Si1EXcIA~K$}4aNjUxzhG`T=xpV&(ICV13*de$b%{aj5eS z=p%#xKHXs>62s0Nwj*u;6m3vQVZRQ$5GT4fal1qJAx_G&FLCnSe#FVS{fXNXD(M1; zHrzq%0uWOm=pgn2$yDfIl3WQLLXs)ap#(8BgAT(8VoC-bhLhw{=m?UaEq2(SBo{+R zlH_ve0VFvOI*KG$K@TL!`OwiMxd=LjBo{)*lH>{~`WYda03A<~SD%aIsm?zkF>pN5u5{5W)d583a0>!-q_cQbu;=YBR zOx*X-(~0{5`Z969L*WO4`z;98LZ2WYW_7R@^0*e-gO~46$7|snpFm+l!FLRTPE)}( zxb__=>RE`Z%jc5!|cL2Z?(Vinn@+&NKRP`_+(PK{Bz_k90&#Cc<$hw5x1gVp zcu(jz#798@^XIsaps1_09nSw0x-oGdLI)A|F?1MlNN;V}O>lFAp!4#?y#!sEIMh$) z-HBTO9ZK9H=rQ0F)=OvPM@W(P&OZ@{HrM%Q;!uyB;U9u~5c(@|cm{VyeG3k4vNQZZ zaMLm6K{*76I$nP{fc&}lplcA1vTkq$iT{EgO$_d_A=Ne(Eh|qdIN|>JuDbVEPP-A>U6nBR`k)8$ zzX7x-*aV-!!c9Sce3ttT0E6+l7jy`T--Zq){@Wn<8g(Yb$3o#3LIAsc4c`#Vmr(eH z5W?PH!!HE;Vi5fN1aSuk!QZgS-y6cOUV;8Y5W~)V*EJ`I?}nDaR>)63DB`(*Af5&t zl@r7d2Ikita{_k@bmyGF-HD&_GXI=-S9B&a7p!F-2=GJh@+N427NDO^um+C!eHgS0 zZqv^;Ky*0$94&E99CQ!9)bAaB$+e??_Q9@ZQ~it|Y&3bq7u+kwue)BV-?zc%BF(l- zd|t{ln|Jh_c8H<&yMA6K$gs}Hye%6nV|(gnL^UhEM-{AOx8(N*WwHJBa~Slthv{b< zH0*u)ISN*@n!bsH<)RhzdlxJdt*xK&`|DA6{hS0F;Qh}$4<<#7-bwm7L(JZb^>dq` zOZ2>cZX2wDxhT(HDp)5j(r<^?BR1CWmk!!u&4uTnjPY>&yz;;HH>>C6?H7#2+XIJU zXJceABG?~07R{)aa!zxwE4~?qZ??i436tvzYoOI&=IiA4!C~X|F3KQt?&Q(*1t9xio4O9CI+qeJP|n_jdIAhlks^HK8gQHTN;9I zCZM##aF&!_+QC?Sk`~4qXvTNL@@w};8f;_#uT=lFl%ybaJ~6lB#Jmp0GY><0l71h2 z&o(_K&y`qN@-=LU`W>FOh%FCU;&@D+>tV3xSk#fEydg>@o-65zxBOdklkmON zgp~Z>Z~wJ{T${|aF>TFKh;OsBS;j1D zmNUzn70ileC9^Uj-mGd?Gpn05OnXx@O?cLIFdfaBW-ZgntZh0YUc$O&J;cP>z-(x` zn69R5DyC{$OwDvN-4S!3r|D&Sn~luIW)rii+01Ni`XK5|KhvraXtp$4A*$ClW?QqJ z+1~76b~HPgoy{(0SF@Yh-RxoZG<%u7%|2#dv!Cg22AF|nkQr=-AokiYGu(_Y`$7nra8-;ZO$?0n)A&0<^pq}nPM(77bC{crRFkoxw*nzX|6I;&DDs) zaxEf5TyJhz95?4ybDO!{++prCcbU7*J?36>pSj;WU>-En%yjdRdDzS_kC;czV`ip# z+{`jhm?sf8=xOr|Vh25Eo;NR;7tKrNWyBGB#mq6Un%B(hh$!@?nQPuce3^I5yJntw z&%AHun+0Z}`M`W=J~AJhPt2mlzgPaH`O184zA@jL@67k+2lJ!($^2}7F~6GM%jxK6lkxL&w^xIwsK*d^>5mcvR|4O_xm z*e&cH_6U21y~5t%M&ZWcCgG-tiL`mxC+r&{EyeYgW6 zUhIUZNxMjFmT>oQk8sa$uW;{hpK#w0QD+brWnefc92^coT%}>*@Q{&-4hTnu2Zp1= zG2z&7TsS_Q5Kasyg_FaB!h^#@!b8Ku!o$NO!Xv|@!lU!36vu_fhbJH|(@FTn|5L(K z5ufSw@Qm=x@T~Cc@SO16@VxMRL~Ob+oDyD?$AGyMv0xB;CA>1cDx4Z#9bOY&8(tS) zAKrkdPB(=&hqr{chPUCD#qZDvN%w^J;#bD+4<86045x+D!-w#T<1-Lj=286a_)J8? zn1xtSPa@vS)8RAWv*B~$^N0-fV)#<{ayUDDC7ct!ia1fPhi@S2%v{8sc{_Y3d^emI zBI08>KU@$l3_l1z3_l7#4nGMOg`b9>g`bCCgkOeVg{vU_j<*vKe{qtXjMzH|+d~k8=P-M?J;EMok3vkwW9+er_i#L7Rh@`PN+;V>G{0z1`kn??fbtyX`&pUVERtA2GciwA1W#`;dLu&ajWzN9|*FrhVMbvQOA2 z5#{4)`;2`S@jjkMtdAG%OZH_u+rEMbL9g1^?CbUo`=*_1-$Fz#M5(d!?0fcoJKrvl zSTu-5@X>!GFd#y~f8r$ApY1P2FCDa}y_L@!x0S}W=lt&J!+>-;y?L{y2YQA<>d zx<%ci9#PMzSJXS&DB3vMB-&IWH!O~uQ;!;X+@-A$d1;$y+i1IJ`)G$~$7rWKqRy_- zZqe@19?_oBUeVssKGD9>e*cXd@o&5biN6z#MqHk;|BYl39m3cT68RxIDmpqkCOS4c zE;>FsAv!T)e4bOI(-4*CjOfhhtmy3Moao%>yy*Ps0!D_Am=MvW(Ph!)h~RVOf1_YT zH~u$PMRZqmcXW?Ljfn2o_&L*~hoXlOLFW;Pp%cwS44qksLi1$w6k_Q-6FrM~I?qQh zL@y!|&CAj3=#^+r^lJ1PV$Zx0y@{wgZz1xGMAn&?$JUu2Er=FIA4DHUA4MM{uFaz8 z)95qAsQDuLGWsg|8d2lEjlPS%kA8@LL?nlw5w+ph=(p(i=#S`6iRFMeqygeSgoykQ z#c}LnA185&cn)piw((LL6>nLMi?`z9$at&8tH*1^?c-A16gS5m;*Rl}@mg`GcZR z9yj7G;w|H?;;rLt;%yO?Z+k?$+Yu4^c1CoKT_wH-;%Mv{?-lPYkuc)@;{Ne~cwjsz z9vlyehsML=;qi!g|2+Q2D8%?1%?KRvc#X9&nb9^RJ{IEs9T6XicpOK^$Hd3R$Hm9T zC&VYlC&eenr^Kg9G!8`9I}_3W&W_KC&qWNt^ARiX!gxx25hCqf5?>l$7GEA;5nmZ! z6;F+?j<1QYjjxNZk8g-?L`=S$^N1g}A->=p@tug&cXxbGd~Y5L3;&&%=b_!Y$RcomU-UPo-7H{-d8 z@AG#24kB{RLzJKQ5$|gOB7S{@Nv8@LT!7uVI5UBy*hi>tYAuDk2udb(b& zx7)~V>^58^59-PP_Icdfh5UGHviHzH2S&F&U=E24|s zjwmB{y1Ni%d=~F?qy2>K=15-Q#YSd%`{Go^nsSXWX;yIrqGK z!M*5Saxc5th-ESdaZFxAM3XlV(PXZB3-L?daqqf$?mhRuo9`C5h3*6Qq5H^v>^^ae z+^6m{_qqGRed)e(U%PMIw}^}Kz5Bua=zelP=MhqVLyVL^5If~B_qY4U2j2M5TOavY z;azpvlV_xA(* zKtIS2_Cx$oKMc`QNBI5yNPmDIS_zV3Mf04h~ zU*a$Im-);675++pm7nUb_Sg7pCC;C}!Qbd_@;Cci{H^{rf4jfK-|6r2cl&$%z5YIb zzkk3#=%@MV{vrRcpWz=t?3c$7f$woY%Rf=XU-QrU=lt{j1^=Rd36bz-BX-}MJbK^j z{tf@8pX=Z9ZzCGsyNDd~9-_p|_Y3?&|AGI|f8;;*pZGD{zpXm``Q2EfAzol-w}=PPekSW+y5hrn@N~h#7v75he&z|H;dRbZ4ifMsiYku z*DQmmG|M42;0no#h{Llo;v=q#XojmNYapIsDQQCN!VZXDh=`MjO1O5?8PNsTMf9Ha zlMRv$lP*bD#5JrW)ubh_5a#+C1r#^iBFDt%%{*K$OHS z5$A5}WSeALM0497vHf;LB+Q)=wQyI&6Wl%7BiS?AE7=?IclJfBoc@TyGZ1kM2PZ?4 zp@_9NJQ*R;{*nWdQOSXb#W*Gzn~Y1wBc{;AWD+7P9h4l5=x~Q3X5ir(74vAsJUliz z4si}oNKQ;nN={BrLDa+35I6A*L{B^mQ3}sNjH2@po#+BYCz^s7MHeH2(WQu8bU9)b zU5U6wQ}naSgb4DSMzE6HgeoTH! zeolT#enq6j|HMg5!_=lx8mBJxX_BUCmbOXTrc0&m(xua7(q+@-(&f_?(iPK{(v{Oy z(pA&d($&*7()MX7ZAzQd4r#{}fsRweR!loHc3}~rFzw3NglP-o@ufW^qMbyrlc;s+ zrs-zs=4qd_Z`vM-7Vcc-6P#I z-7DQY-6!2Q-7oE*4oC;4gVMq2kaTD|EFGSXNcT@irU#^>(gV}c>6mnEIxZcbPDm%F zlZse)>7nUiOU6b^k4=wDk55lXPfSlrPyRQ07$Sw8g;-(dq~{{G)A@)Fb|K<}U6fvo zI8T={78v3^U6oEvuSOK8Yt!rgH!9cv#^g%xP47$Zm$+H!G>MdzKAg@-A4wlgA4_MZ zkEgTJC(XVPcW=hElX7t$Bgm(rKh+373kob=W7we}rmQ*Zkaf(~%+|^}Wou`hvvsm{v-Ps|vkkHh zvo2ZJtejP{YSxm~vTj-TtVh-}>y`D+Hp({6Hpw>4Hp@28`ec2xepzc)&l=el*_PQ> z+1A-M*|yns+4k8E*^b#x+0NN6*{<1c+3wjM*`C>6+1}Yc*}mC+S^sQ6HZU8M4bFyS zL$hJo@N7i3e>O5ZARCn(n2pZHWMi{&+4yWiHZhx&P0kL=4$cnA4$ThB4$qFrj?9kA zj?RwBj?IqCj?YfWPRvfqPR>rrPR&lsPS4KB&dkor&d$!s&dtut&d)B$F3hH67iAY` zmt>b_mt~h{S7cXaS7lSPtFvpeYqRUJ>$4lO8?&3To3mT8TeI7;+p{~eJF~m8yR&<; zd$aqp`?CkK2eWC}^z5PR;cQ0sNcL#>}7Jg=(fSM~gAwY&ViQs?)TdY}9}Xr4YaPY6#2({GgX@>CjS z-ltJ+@b7)~`o4O7-~9S=wMEnEo2LVFv;4KzLYcotPrbgUUf)x%>#6tesrT=x_wT9q z?^)bG|2xXX`;_~!oRzxPPlJE2wQ4=r)Go9??5_4H^SWw{eo*O2JJl*or==IQy!h`u zr2OS_KWas3tg& z^(Vc5qoV$#`Dj$spJey)@lk znr^Qm-Tdz`FUwKx$M5UBf4RbPzz%v|ze4l$(5~|IV9z`s(869kuTrGbqV>-7(T;e2 zwP@$+k9BRIm0pD%nXeY@A6m5kZ?tH+TeRFozsr86(rC%=57lzFXt`Uo+^u?jt6tx# zc57ApYCEqj_OCoW_*0%9G*1tjr&mYECH*2aZ#Pi&--`M#`w4un{>%OX$Lhaqzc|kG z1I_aT&GQ4*`?KD0toN_z{fl-})%(+*@wnFe)30%?_oqMOSntpJ!?E6<^@wBIqg>N^ zZP9ka_Ec`^k>9^u)A}secz&gY^;fCW&UMy*rOy5W%5-Xc?!)~wefl%{Hy)#b<$zMho+DbXb>0W*75B-n!?9kc1aPuuCac~ML*uke!fiqhnulomRp%_xt0BPS;q~P!v0#`N;hrC zMY-#m|2pkmdY@R z_akUteyG}2+jG4&&lkSe>p32+)EAGFn9mmWhvk-H+@Qn8<^5XHPe%MXR)s6L9)8~FgpEE6ea=)uI)b8bO z^uw~IQ{naKZnPbCqu*C`JX&S{1Usv}y5;%6vD&L!ZkK9>{R@r}U-(|_Qdhe)w0?B_U8bKPT|KV{?Nsij`7G|=Grtbqy4GV)&6m!r>Ku=vzBGS2 zuc@b=x%oKL~8>CaUihgSM({y6Tb_S5HTxx2ROx;|%kKg@rbzj|M7Cwa+{`@8Z7N?poO{svIxidd-jKt6cP}eQ1xW+NGlDRM@Uj z?%aMDK570Mg&oz87T;IX(dSJ~$B#9A?$nCsRMD^2d48oqzp1gEVf>Q&Gc^Amn)?kj zFAu6OuNP=u9%$}Q&^$e8o-b&gUufRGpt;?lxqm=&`$6;aL0PUc$00b@`?KG{vEHBK z4ot`O{v3bcSntpFiDSJ##}7Ex`|~-4W4*tQAIe2P$@W^V^epU|E9U=&sy}d?i)odf zSB#&uJy*4VEZ4O^(DN&tZ(;bW{;umKb^X3Y=jF9x-AViH3dhM9j`I0bE&3VlC#oFB zB3<=!U6-jZPG7%gI+%|#-x%=c&!wvRVWpTqYx}RVzsIzL`7L*+AC}pk%T*n>mG${t zX1j;K(r+upeRRHD;rJcHer@+0ucQ4i-KyHDs`I`&uS0&d-RgY3uJiP|&g<&Bu2knb z656x2*S=cre!0J3T%`5Lbq^e~erohrxDEUHnvM@@i=R8KZ3m1SG+|Pg;R6N_nm8;s zTDf=_>!h)+lgK&;0p%(?2{;LFQq_lirD)8|JdBZN5_2H!BsdAdoJF0dp-!iNFY{rF zNhXb0V`q%a=Z&#kWxk+#y)J%KweqUPLtP)<6?Rg%zg7bCgY?x{{2s@7deGb%&^gf= zYr1$>?yi-}&H+xK#%Je&^z*;tdTrb~`KjvQpj-^j>iOSsJs+wSom5p8U#EkIB3)f9 ztZE~w7S7Mb22`?kE*yN~p{Vu2hZdYT&lfrc-mg{+B8x`G&JFdbos~`!s$3+%B#U;# zs%P#OE!wDRI*67=Wo4~MFzTkTG} zRrS28P8uuhG;u#|ltpJ%Oy-ym+*j*I7X|7?C(K4s*FiH>JADpnajcy_J3Sn0r%(Ig zSUY_V9&oI7q~GG0^;y%!j&f0s9&8WQ?ra}b4ib@1^(%Igm<;9iLjAD*$_>4)2fwcr zepcAC7-XtlE5)K}v1q50b*u(SeIk8rf9&jVTzs$fSuFBsKFhkORc5=znnzxK45pc{ zvUc)i^^-COW95qaXGQ(1qMcAhC!6Tia{t2YEZ3?|cB}04%T;YBRkjlxtKAB}Dkkwv zzfx!V(A-XFCwaP1wJ!$`xSjr;g9IFFKcI_2bq>byy$lp2?XRYjlv>em>ZGftgUqr{GHTou zKz(Sv(r;l0w#S;bmvZ5MI_a+#epCz^=tuBht&gIA(fd_(P+Qf(X_b=%oUi>_F^MYr z86DJP_Q?9>!xQ;Zf73yCo%h4{xjm7d)-wms^>Q(p56$1*2Zs6V!cYBA{2MYO73SM90&Yw;Y^!CIww4i40p zx;R}{KP&5GtWrGhxS58vbLP9Mn=IvG(?%D=${f67@~iDw`?-qt8x{4pO3`l>i%Pl} zRVfysbdp`s$#qo+SJh&W#OslNwQmn?uk=fNpWh$tQvc5Of@AeF_A@wEdlb)c?N`e> z$*k()PraD56_c>yemx6&uzggD{BY9@lVbG)))S8P{<^4HE+*}|$yev*9`?YPZi^1y z%0>Iu{-wgrOgz`=pJkooRG2ULZE@fH?-gtN+-7^mao&F*--Ul_ zd5UqKZr+x4Q@KSqms)fZ-LiPmnEg7C+O8V9$kfpG)nL28UU_~WDC?n7Y+4kH4!U{W&`EfM<5!%|`fC*J zk&}AN9XSqe(MfMh(T=qIHSJ$(YTw%8#p=92#N1NHwZ-^WCoMHDo?-4-j8}5I<9xQ~ znvN@K`aG`bIKHNf7PVqiO#QQ_#|f7X9ZUrMNHzgyFn63Xh=<%arE zVgI7O^kszx7b!42&g%tAe{JZddP5gG8r(#}-`VaOI^JvOB3eWHjfO6!HMIX|=%QMK z{SEdI^7?~nI{LChLl=D-`qD!~7k?W1@|zzM+%whAt{M^f}+q&GCj#dK>CbEyYV2MZOCE=Oz|5McEHD zbW^sW^R|X=&Ng)Z*3eDbhR)*}x=7j3#kPj_Ck|b!K_SMbKhHk<) zbaLI$&G-fv6R-)J*8}XS{Rt=QSOe1elFmb0icMSH>}cq?wV{ic4ehrZoY!FOg8g(u zH?H~@u}KX7mFJ@Z*M6UV_9yT z$9|wiUlMB3mnvFxT+pJMOD#ISZs?{(L&x6@&Zn@5s_jBIGa5QBZRlcigX6?XLl?0d zx`^1&adtx&EgQP1-OzDxi!SQ4=wf<{ZtAt@ytG9(iyFG=(9m&OLpL89I$mq&CPYKW zZ4F)jXz2K@p^M=SK2Oj;tNrvPr3Rlb_+I@<$KMToX{n)$s|`LUFl?ItP1hT0^b@QF>i7B_tm$*BR&1i`II*VVx|+_zYh0Wx*ErrlI@%9wz1H;Qp_cts%d{&n)x~;URL46!eC||=bv5;8U0lcCX}5|#r*)AX z-?N|9MRpwL<;6TMAD=+;d_i-2K=XV-^L*5Gehkga4bAfb&GQY-?F7y556$fb&F>4% z?GM%avY*1S-oKcS7W33%KFoOo-hJcq01Z2z|6;dN6ni7|*Twareyp1URbAYN^7*26 z#dWNAeaQ^(Na^2o998D?8rP{G@;QxTy}!=i%gircf+)AmU*@a!>@oz4zcM-;v7m~J4#)ZLkb+L zi*ks7k%_uGr!;uYG`}y3uCB|bUhUSWkCyV^93!!o#RZc)EqPE|;-I?3UvR@E{!&{q zf$EY6wSG&Uw!}@5ll(9A2yT<(=DDZgt((Fe+5)tg`Wg_<(;k4^@;KI(%9aLC(c<$7 zf!zr0LDwE>Q8TvuTL?@8bN5Cw)#kzB7Pf-4 znXy~NvASb1{L^KP7B(MbPP;XITrJ+KS=@c7akxB&SBcr(725$tGby$w^c^N_DX|+W z>oBRTkKLkMtrpF#N1Z;>@^9UtQ1yQ+5_eqkpxfdDoVG+2BLhqPrTTA!k*qB7m)0Ij z95j|Z=v!I*7wi--*-CpbJ8We7a)Yj$p_cM~2dWjK<92M|sPq3@Yj{yIuVQE~H@Si# zJf8u&v4xDX;pvN;IA$M)7qRlwyQ^?yQ7%@F_~gI}QvQ8ipD56x>Wk*enP%~X)7LSp z$a>x`bmb7o%$2SP!+!L4-DblCB|omT>I!SCu57pJPF5?c7JtuOtJ10?y4GTalljJO z9W7SX8C9!}2&y_FZRJQ8=jHjq+Z=g5p?P`m7BIWJs*Y%?I)ZK0k#4KL7SUSFth5PK zbw#z++BE3Anl~&ppe=qKvr-$$JE&!U_0q4_S%0(B%`mGmDU7yT&3zgnn<_sLiYFEy{ zajbUb3>?R5SFX_FSnbM@CXUsv^dH=>NRR$n=13gpshv1d!+Dx-&Wv%K-v^r81*� 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 z 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 literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/Inter/static/Inter-Thin.ttf b/web/common/src/styles/design/fonts/Inter/static/Inter-Thin.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7aed55d560065b7a5a1975121e0b13ac25e798c8 GIT binary patch literal 310516 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

      _l*o+mcntCJeTvro_NJ}L$i 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 z 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 SMb literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf b/web/common/src/styles/design/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..914e323363b726d5d976f42e6c0dcec76e2bcfa0 GIT binary patch literal 191988 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 V9RH2)v)_Oa)d1rhhO3I>{ueb#sv`gZ literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/OFL.txt b/web/common/src/styles/design/fonts/JetBrains_Mono/OFL.txt new file mode 100644 index 0000000000..201e940f3a --- /dev/null +++ b/web/common/src/styles/design/fonts/JetBrains_Mono/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/README.txt b/web/common/src/styles/design/fonts/JetBrains_Mono/README.txt new file mode 100644 index 0000000000..0a8510da61 --- /dev/null +++ b/web/common/src/styles/design/fonts/JetBrains_Mono/README.txt @@ -0,0 +1,79 @@ +JetBrains Mono Variable Font +============================ + +This download contains JetBrains Mono as both variable fonts and static fonts. + +JetBrains Mono is a variable font with this axis: + wght + +This means all the styles are contained in these files: + JetBrainsMono-VariableFont_wght.ttf + JetBrainsMono-Italic-VariableFont_wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for JetBrains Mono: + static/JetBrainsMono-Thin.ttf + static/JetBrainsMono-ExtraLight.ttf + static/JetBrainsMono-Light.ttf + static/JetBrainsMono-Regular.ttf + static/JetBrainsMono-Medium.ttf + static/JetBrainsMono-SemiBold.ttf + static/JetBrainsMono-Bold.ttf + static/JetBrainsMono-ExtraBold.ttf + static/JetBrainsMono-ThinItalic.ttf + static/JetBrainsMono-ExtraLightItalic.ttf + static/JetBrainsMono-LightItalic.ttf + static/JetBrainsMono-Italic.ttf + static/JetBrainsMono-MediumItalic.ttf + static/JetBrainsMono-SemiBoldItalic.ttf + static/JetBrainsMono-BoldItalic.ttf + static/JetBrainsMono-ExtraBoldItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Bold.ttf b/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b7484374e77f6f89862a234bd2b4b81177f1162e GIT binary patch literal 114832 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

      +G7m17=NIP<7^j4`)Xm2x{x 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

      ^$h|l zC(Y6yU{MOFEaNNcs7RH~kmaERA zrk4SG8TQ1>(Y!i51G+Td!LH~9K#*v zQK*DH@vyKUhiX3wC5$_J9d9eQDG&%OFO4<%sMD$!2(VQ@h8Ke?c1++O}oq$ORT?t))M6Gdx zp%$$XvzO6$2W%ovmwXP|{v7;}bFher3PA~zU&gr{dfc7YqcY-#cstwEylgwwZb84u z7;$vri8iX;@{HvfK%#bEtEgRuFOE0L;Sc5Tqi(GabL)v)1Xgyi)AP1bsFyBH3FjyI}7jkBYdDhLi^QgvGuAwB3%m1 z)DL3CW0?k(9>cFfXEzWgGxbMQXA=bYq&wnuwnRD&S4?NO;k(*K{Y$YZ=rWz8e-c*2 zllY{vb-bo2SP>XSbOBZin1r(#HUdJuO0XJHzy2DHtiOi-00WJBXir>{eiomL?PdC5 z_LDeTR~#skNL z{E~E&__j!kgvd8}NH=f_E@(ICO7Wz4qJl8NJ7ik&5UDnTP;D5y+L!T3b>Uu%HBs$P zv_IkZOmi9#XmjnO;$C|{zQ2U)e)S$BMN1dOmk4W27f5%i`n3xjBOV#BVmb|5sXyMb zp%VwRBv%?u8K-b1?xW+0AVg6eDB2N|ym}RiC;{Fc=tBun3cZK74Yu%mu2Hn;J!lrr zLm9IYU0OHpqjWOqQoL<8dMB|wV7O8G{Dpf|K7;YJR$bjHCRz=5V@x!O3s_44lMJT} zrvNEdYoT(e4;n(2ob-n~JSp$smqh8eYDvL2*twwKZcf1k*1zRSjQ>i!3zujv@~FMHGJa`*d}D3NYZnRX5tuM z)HT$*0FkgaHM%OaOrJ@&0B3+dl~4HfSK{8S;voppfVl~06It;cC6If@q*4CAsDBOa zap6p(#o8I>N4$LQP2K}KN zapMJkNqDpH3xGu14!%m5i8zFKdOs{qK?|+9#QC`Fv-lMkO6C$i>@i~ODisrKNBV}7 zL?m3v9Lm5)0UZH!1eyPW4#+>{_~BZlLPR6~ppK`ts&~Ik!^y>v1Rtj$i&G%sUf4?- z4MBj9&ma6Nm59wB?<}tiG6-!2?ykOYUDA*6^kclP48o*E%VQ@b5CnBWiY2`9SDmY+ zn*gCJs_SBU0(BkYbq%OCgHiJ(Xr0U9^{$Cu)uJB4Nm<0V=B$fj z940%5pfr<>$DrR(p;rOAG6p4Egv!Eizzh-Xqo-6TR+Tkw{E0s5K{;HpQqOX#;#)+g z$w<<`g}F!8BaJ>_a{@n;G5f_v96>;b22t}xsnF2;jOzkY@wzG%Yd)r~6g=6%>q?=z z@HXPbnVuk2*E?w4JJHr@UIm1%C=X~vQYpwka$3;y>YbYDAq@i3%K_1-{=VmZ@VU{>9;`+wE-&QecAZKebi

      7!MP~$9_(Tm$!6{5B;t%(Pm7w?r;`rnI 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 literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-ExtraBoldItalic.ttf b/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-ExtraBoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..85e67db4032df81bbb1152b578ad189a0be88142 GIT binary patch literal 117960 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

      Mhdr7tfDo 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�}Bp{PTisep8efRxuB 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 literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-ExtraLightItalic.ttf b/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-ExtraLightItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..745b58eeaaf00b327eed345f96080bf42b84cbb0 GIT binary patch literal 118236 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 ArvLx| literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Italic.ttf b/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5b484dd61071dab3f81bd9648c4cc8effe9ea90d GIT binary patch literal 117948 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 literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Light.ttf b/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..296186f1db3d9d076134e5ee5e50e3aa4058e4b5 GIT binary patch literal 115024 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 L6ZmdmuOR#{h`+bn literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Medium.ttf b/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ad31fbd7f064cabb5dc7330d59abfe66a0dce39e GIT binary patch literal 114924 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

      Eca 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 z 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 CSt)e@ literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Regular.ttf b/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..02bc07ea088e3cbf370cb21e884319f74a62b0ba GIT binary patch literal 114908 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

      (t+MS=TF-p>(IRE@>;T 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 Y4oL_^q4?eT`gDBZJmGJLhZN<10)%Q94gdfE literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-SemiBold.ttf b/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c3adfd3151f691df37cfbda3bdfda5b37b1c78f0 GIT binary patch literal 114904 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 J;Cq4n{|&Ak&}IMt literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-SemiBoldItalic.ttf b/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-SemiBoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..62d58add4754ddbabd81ba5936c97595259845f0 GIT binary patch literal 118040 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 b0=fm;i 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 R4>w4;_`>sqe;VHR{{VdqIbi?* literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Thin.ttf b/web/common/src/styles/design/fonts/JetBrains_Mono/static/JetBrainsMono-Thin.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6a6a556f10c77c7b2651a802a97d3153d641d19d GIT binary patch literal 115000 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 literal 0 HcmV?d00001 diff --git a/web/common/src/styles/design/index.css b/web/common/src/styles/design/index.css new file mode 100644 index 0000000000..167e403244 --- /dev/null +++ b/web/common/src/styles/design/index.css @@ -0,0 +1,22 @@ +@import url('./fonts.css'); +@import url('./palette.css'); +@import url('./semantic-colors.css'); +@import url('./typography.css'); +@import url('./space.css'); + +:root { + /* Radius */ + --radius-2xs: var(--half); + --radius-xs: var(--step); + --radius-s: var(--step-2); + --radius-m: var(--step-3); + --radius-l: var(--step-4); + --radius-xl: var(--step-5); + --radius-2xl: var(--step-6); + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} diff --git a/web/common/src/styles/design/palette.css b/web/common/src/styles/design/palette.css new file mode 100644 index 0000000000..7fa379437f --- /dev/null +++ b/web/common/src/styles/design/palette.css @@ -0,0 +1,199 @@ +:root { + /* Colors: Brand */ + --color-tobiko: hsla(16, 94%, 50%, 1); + --color-sqlmesh: hsla(230, 94%, 50%, 1); + --color-sqlglot: hsla(148, 100%, 38%, 1); + + /* Colors: Brand Palette*/ + --color-pacific: hsla(194, 100%, 50%, 1); + + --color-pacific-5: hsla(194, 100%, 50%, 0.05); + --color-pacific-10: hsla(194, 100%, 50%, 0.1); + + --color-wasabi: hsla(112, 67%, 68%, 1); + --color-yuzu: hsla(58, 100%, 56%, 1); + + --color-uni-5: hsla(38, 96%, 51%, 0.05); + --color-uni-10: hsla(38, 96%, 51%, 0.1); + --color-uni-15: hsla(38, 96%, 51%, 0.15); + --color-uni-20: hsla(38, 96%, 51%, 0.2); + --color-uni-50: hsla(38, 96%, 51%, 0.5); + --color-uni: hsla(38, 96%, 51%, 1); + + --color-salmon-5: hsla(345, 92%, 81%, 0.05); + --color-salmon-10: hsla(345, 92%, 81%, 0.1); + --color-salmon-15: hsla(345, 92%, 81%, 0.15); + --color-salmon-20: hsla(345, 92%, 81%, 0.2); + --color-salmon-50: hsla(345, 92%, 81%, 0.5); + --color-salmon: hsla(345, 92%, 81%, 1); + + /* Colors: Base */ + --color-white: hsla(0, 0%, 100%, 1); + --color-black: hsla(0, 0%, 5%, 1); + --color-cyan: hsla(198, 100%, 63%, 1); + --color-deep-blue: hsla(210, 100%, 50%, 1); + --color-purple: hsla(264, 100%, 60%, 1); + --color-emerald: hsla(130, 65%, 50%, 1); + --color-mandarin: hsla(30, 100%, 50%, 1); + --color-scarlet: hsla(350, 85%, 60%, 1); + --color-sunflower: hsla(48, 100%, 50%, 1); + --color-peach: hsla(24, 100%, 70%, 1); + + --color-turquoise-5: hsla(150, 80%, 45%, 0.05); + --color-turquoise-10: hsla(150, 80%, 45%, 0.1); + --color-turquoise-20: hsla(150, 80%, 45%, 0.2); + --color-turquoise-50: hsla(150, 80%, 45%, 0.5); + --color-turquoise: hsla(150, 80%, 45%, 1); + + --color-fuchsia-5: hsla(320, 100%, 70%, 0.05); + --color-fuchsia-10: hsla(320, 100%, 70%, 0.1); + --color-fuchsia-20: hsla(320, 100%, 70%, 0.2); + --color-fuchsia-50: hsla(320, 100%, 70%, 0.5); + --color-fuchsia: hsla(320, 100%, 70%, 1); + + --color-gray: hsla(0, 0%, 50%, 1); + + /* Colors: Tokens */ + --color-cyan-5: hsla(198, 100%, 63%, 0.05); + --color-cyan-10: hsla(198, 100%, 63%, 0.1); + --color-cyan-15: hsla(198, 100%, 63%, 0.15); + --color-cyan-20: hsla(198, 100%, 63%, 0.2); + --color-cyan-100: hsla(183, 100%, 93%, 1); + --color-cyan-200: hsla(186, 100%, 85%, 1); + --color-cyan-300: hsla(191, 100%, 78%, 1); + --color-cyan-400: hsla(194, 100%, 72%, 1); + --color-cyan-500: var(--color-cyan); + --color-cyan-525: hsla(198, 100%, 70%, 1); + --color-cyan-550: hsla(198, 100%, 68%, 1); + --color-cyan-600: hsla(202, 70%, 52%, 1); + --color-cyan-700: hsla(206, 69%, 42%, 1); + --color-cyan-800: hsla(210, 75%, 33%, 1); + --color-cyan-900: hsla(214, 82%, 26%, 1); + + --color-deep-blue-5: hsla(216, 100%, 50%, 0.05); + --color-deep-blue-10: hsla(216, 100%, 50%, 0.1); + --color-deep-blue-15: hsla(216, 100%, 50%, 0.15); + --color-deep-blue-20: hsla(216, 100%, 50%, 0.2); + --color-deep-blue-60: hsla(216, 100%, 50%, 0.6); + --color-deep-blue-100: hsla(207, 100%, 95%, 1); + --color-deep-blue-125: hsla(207, 100%, 92%, 1); + --color-deep-blue-150: hsla(207, 100%, 88%, 1); + --color-deep-blue-200: hsla(209, 100%, 80%, 1); + --color-deep-blue-300: hsla(211, 100%, 70%, 1); + --color-deep-blue-400: hsla(213, 100%, 62%, 1); + --color-deep-blue-500: var(--color-deep-blue); + --color-deep-blue-525: hsla(216, 100%, 60%, 1); + --color-deep-blue-550: hsla(216, 100%, 54%, 1); + --color-deep-blue-600: hsla(219, 100%, 43%, 1); + --color-deep-blue-700: hsla(221, 100%, 36%, 1); + --color-deep-blue-725: hsla(223, 100%, 33%, 1); + --color-deep-blue-750: hsla(223, 100%, 31%, 1); + --color-deep-blue-800: hsla(223, 100%, 29%, 1); + --color-deep-blue-900: hsla(226, 100%, 24%, 1); + + --color-pacific-5: hsla(194, 100%, 50%, 0.05); + --color-pacific-10: hsla(194, 100%, 50%, 0.1); + --color-pacific-15: hsla(194, 100%, 50%, 0.15); + --color-pacific-20: hsla(194, 100%, 50%, 0.2); + --color-pacific-50: hsla(194, 100%, 50%, 0.5); + --color-pacific-100: hsla(194, 100%, 98%, 1); + --color-pacific-125: hsla(194, 100%, 94%, 1); + --color-pacific-150: hsla(194, 100%, 92%, 1); + --color-pacific-200: hsla(194, 100%, 86%, 1); + --color-pacific-300: hsla(194, 100%, 74%, 1); + --color-pacific-400: hsla(194, 100%, 66%, 1); + --color-pacific-500: var(--color-pacific); + --color-pacific-525: hsla(194, 100%, 46%, 1); + --color-pacific-550: hsla(194, 100%, 42%, 1); + --color-pacific-600: hsla(194, 100%, 38%, 1); + + --color-purple-5: hsla(264, 100%, 60%, 0.05); + --color-purple-10: hsla(264, 100%, 60%, 0.1); + --color-purple-15: hsla(264, 100%, 60%, 0.15); + --color-purple-20: hsla(264, 100%, 60%, 0.2); + --color-purple-100: hsla(264, 100%, 98%, 1); + --color-purple-125: hsla(264, 100%, 94%, 1); + --color-purple-150: hsla(264, 100%, 92%, 1); + --color-purple-200: hsla(260, 100%, 90%, 1); + --color-purple-300: hsla(260, 100%, 80%, 1); + --color-purple-400: hsla(260, 100%, 70%, 1); + --color-purple-500: var(--color-purple); + --color-purple-600: hsla(264, 100%, 50%, 1); + --color-purple-700: hsla(264, 100%, 40%, 1); + --color-purple-800: hsla(264, 100%, 20%, 1); + --color-purple-900: hsla(264, 100%, 8%, 1); + + --color-emerald-5: hsla(111, 66%, 55%, 0.05); + --color-emerald-10: hsla(111, 66%, 55%, 0.1); + --color-emerald-15: hsla(111, 66%, 55%, 0.15); + --color-emerald-100: hsla(92, 90%, 92%, 1); + --color-emerald-125: hsla(92, 90%, 88%, 1); + --color-emerald-150: hsla(92, 90%, 86%, 1); + --color-emerald-200: hsla(98, 90%, 84%, 1); + --color-emerald-300: hsla(102, 81%, 75%, 1); + --color-emerald-400: hsla(106, 72%, 66%, 1); + --color-emerald-500: var(--color-emerald); + --color-emerald-525: hsla(111, 66%, 63%, 1); + --color-emerald-550: hsla(111, 66%, 61%, 1); + --color-emerald-600: hsla(116, 61%, 45%, 1); + --color-emerald-700: hsla(120, 67%, 36%, 1); + --color-emerald-800: hsla(125, 74%, 28%, 1); + --color-emerald-900: hsla(130, 81%, 22%, 1); + + --color-mandarin-5: hsla(30, 94%, 62%, 0.05); + --color-mandarin-10: hsla(30, 94%, 62%, 0.1); + --color-mandarin-15: hsla(30, 94%, 62%, 0.15); + --color-mandarin-100: hsla(42, 95%, 92%, 1); + --color-mandarin-125: hsla(42, 95%, 88%, 1); + --color-mandarin-150: hsla(42, 95%, 86%, 1); + --color-mandarin-200: hsla(39, 97%, 85%, 1); + --color-mandarin-300: hsla(36, 97%, 77%, 1); + --color-mandarin-400: hsla(33, 95%, 71%, 1); + --color-mandarin-500: var(--color-mandarin); + --color-mandarin-525: hsla(30, 94%, 68%, 1); + --color-mandarin-550: hsla(30, 94%, 66%, 1); + --color-mandarin-600: hsla(27, 67%, 51%, 1); + --color-mandarin-700: hsla(24, 69%, 42%, 1); + --color-mandarin-800: hsla(21, 75%, 32%, 1); + --color-mandarin-900: hsla(18, 82%, 26%, 1); + + --color-scarlet-5: hsla(21, 100%, 50%, 0.05); + --color-scarlet-10: hsla(21, 100%, 50%, 0.1); + --color-scarlet-15: hsla(21, 100%, 50%, 0.15); + --color-scarlet-100: hsla(21, 100%, 93%, 1); + --color-scarlet-125: hsla(21, 100%, 89%, 1); + --color-scarlet-150: hsla(21, 100%, 87%, 1); + --color-scarlet-200: hsla(16, 100%, 86%, 1); + --color-scarlet-300: hsla(11, 100%, 78%, 1); + --color-scarlet-400: hsla(6, 100%, 73%, 1); + --color-scarlet-500: var(--color-scarlet); + --color-scarlet-525: hsla(356, 100%, 70%, 1); + --color-scarlet-550: hsla(356, 100%, 68%, 1); + --color-scarlet-600: hsla(356, 70%, 53%, 1); + --color-scarlet-700: hsla(351, 67%, 43%, 1); + --color-scarlet-725: hsla(346, 67%, 38%, 1); + --color-scarlet-750: hsla(346, 67%, 38%, 1); + --color-scarlet-800: hsla(345, 73%, 33%, 1); + --color-scarlet-900: hsla(341, 79%, 27%, 1); + + --color-gray-3: hsla(190, 8%, 50%, 0.03); + --color-gray-5: hsla(190, 8%, 50%, 0.05); + --color-gray-10: hsla(190, 8%, 50%, 0.1); + --color-gray-15: hsla(190, 8%, 50%, 0.15); + --color-gray-25: hsla(190, 8%, 50%, 0.25); + --color-gray-50: hsla(202, 8%, 26%, 0.5); + --color-gray-75: hsla(202, 8%, 26%, 0.75); + --color-gray-100: hsl(190, 8%, 98%); + --color-gray-125: hsl(190, 8%, 94%); + --color-gray-150: hsl(190, 8%, 92%); + --color-gray-200: hsl(190, 8%, 86%); + --color-gray-300: hsl(190, 8%, 74%); + --color-gray-400: hsl(190, 8%, 66%); + --color-gray-500: var(--color-gray); + --color-gray-525: hsl(190, 8%, 46%); + --color-gray-550: hsl(190, 8%, 42%); + --color-gray-600: hsl(190, 8%, 38%); + --color-gray-700: hsl(202, 8%, 26%); + --color-gray-800: hsl(214, 8%, 14%); + --color-gray-900: hsl(226, 8%, 4%); +} diff --git a/web/common/src/styles/design/semantic-colors.css b/web/common/src/styles/design/semantic-colors.css new file mode 100644 index 0000000000..c58e237539 --- /dev/null +++ b/web/common/src/styles/design/semantic-colors.css @@ -0,0 +1,12 @@ +@import url('./palette.css'); + +:root { + --color-light: hsl(30, 12%, 100%); + --color-dark: hsl(226, 24%, 8%); + --color-brand: var(--color-tobiko); + --color-prose: var(--color-gray-800); + + /* Badge */ + --color-badge-background: var(--color-gray-100); + --color-badge-foreground: var(--color-prose); +} diff --git a/web/common/src/styles/design/space.css b/web/common/src/styles/design/space.css new file mode 100644 index 0000000000..5568785eea --- /dev/null +++ b/web/common/src/styles/design/space.css @@ -0,0 +1,23 @@ +:root { + --one: 1px; + --base: 4px; + --half: calc(var(--base) / 2); + --step: var(--base); + --step-2: calc(var(--base) * 2); + --step-3: calc(var(--base) * 3); + --step-4: calc(var(--base) * 4); + --step-5: calc(var(--base) * 5); + --step-6: calc(var(--base) * 6); + --step-7: calc(var(--base) * 7); + --step-8: calc(var(--base) * 8); + --step-9: calc(var(--base) * 9); + --step-10: calc(var(--base) * 10); + --step-11: calc(var(--base) * 11); + --step-12: calc(var(--base) * 12); + --step-15: calc(var(--base) * 15); + --step-16: calc(var(--base) * 16); + --step-20: calc(var(--base) * 20); + --step-24: calc(var(--base) * 24); + --step-30: calc(var(--base) * 30); + --step-32: calc(var(--base) * 32); +} diff --git a/web/common/src/styles/design/typography.css b/web/common/src/styles/design/typography.css new file mode 100644 index 0000000000..bbea66a0d0 --- /dev/null +++ b/web/common/src/styles/design/typography.css @@ -0,0 +1,40 @@ +:root { + --font-size: 16px; + --leading: 1.5; + --font-weight: 500; + --font-sans: 'Inter', sans-serif; + --font-mono: 'JetBrains Mono', monospace; + + --text-2xs: 10px; + --text-xs: calc(var(--font-size) * 0.75); + --text-s: calc(var(--font-size) * 0.875); + --text-m: var(--font-size); + --text-l: calc(var(--font-size) * 1.125); + --text-xl: calc(var(--font-size) * 1.25); + --text-2xl: calc(var(--font-size) * 1.5); + --text-3xl: calc(var(--font-size) * 2); + --text-4xl: calc(var(--font-size) * 2.5); + + --text-headline: calc(var(--font-size) * 4); + --text-display: 45px; + --text-header: calc(var(--font-size) * 2); + --text-tagline: 23px; + --text-title: var(--font-size); + --text-subtitle: calc(var(--font-size) * 0.75); + + --text-leading-xs: 1; + --text-leading-s: 1.25; + --text-leading-m: var(--leading); + --text-leading-l: 1.75; + --text-leading-xl: 2; + + --text-thin: 100; + --text-extra-light: 200; + --text-light: 300; + --text-normal: 400; + --text-medium: var(--font-weight); + --text-semibold: 600; + --text-bold: 700; + --text-extra-bold: 800; + --text-black: 900; +} diff --git a/web/common/src/styles/index.css b/web/common/src/styles/index.css new file mode 100644 index 0000000000..231579cec8 --- /dev/null +++ b/web/common/src/styles/index.css @@ -0,0 +1,5 @@ +@import './design/index.css'; + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/web/common/src/styles/tokens.ts b/web/common/src/styles/tokens.ts new file mode 100644 index 0000000000..75d233fdb4 --- /dev/null +++ b/web/common/src/styles/tokens.ts @@ -0,0 +1,223 @@ +/** + * Design Token TypeScript Definitions + * Type-safe access to CSS custom properties defined in the design system + */ + +// Color Tokens +export interface ColorTokens { + // Brand Colors + '--color-tobiko': string + '--color-sqlmesh': string + '--color-sqlglot': string + '--color-pacific': string + '--color-wasabi': string + '--color-yuzu': string + '--color-uni': string + '--color-salmon': string + + // Base Colors + '--color-white': string + '--color-black': string + '--color-cyan': string + '--color-deep-blue': string + '--color-purple': string + '--color-emerald': string + '--color-mandarin': string + '--color-scarlet': string + '--color-sunflower': string + '--color-peach': string + '--color-turquoise': string + '--color-fuchsia': string + '--color-gray': string + + // Semantic Colors + '--color-light': string + '--color-dark': string + '--color-brand': string + '--color-prose': string + '--color-badge-background': string + '--color-badge-foreground': string +} + +// Spacing Tokens +export interface SpacingTokens { + '--one': string + '--base': string + '--half': string + '--step': string + '--step-2': string + '--step-3': string + '--step-4': string + '--step-5': string + '--step-6': string + '--step-7': string + '--step-8': string + '--step-9': string + '--step-10': string + '--step-11': string + '--step-12': string + '--step-15': string + '--step-16': string + '--step-20': string + '--step-24': string + '--step-30': string + '--step-32': string +} + +// Typography Tokens +export interface TypographyTokens { + // Font Families + '--font-sans': string + '--font-accent': string + '--font-serif': string + '--font-mono': string + + // Font Sizes + '--font-size': string + '--text-2xs': string + '--text-xs': string + '--text-s': string + '--text-m': string + '--text-l': string + '--text-xl': string + '--text-2xl': string + '--text-3xl': string + '--text-4xl': string + '--text-headline': string + '--text-display': string + '--text-header': string + '--text-tagline': string + '--text-title': string + '--text-subtitle': string + + // Line Heights + '--leading': string + '--text-leading-xs': string + '--text-leading-s': string + '--text-leading-m': string + '--text-leading-l': string + '--text-leading-xl': string + + // Font Weights + '--font-weight': string + '--text-thin': string + '--text-extra-light': string + '--text-light': string + '--text-normal': string + '--text-medium': string + '--text-semibold': string + '--text-bold': string + '--text-extra-bold': string + '--text-black': string +} + +// Combined Design Tokens +export interface DesignTokens + extends ColorTokens, + SpacingTokens, + TypographyTokens {} + +// Utility type for accessing CSS custom properties +export type CSSCustomProperty = T + +// Type-safe color scale definitions +export type ColorScale = + | '5' + | '10' + | '15' + | '20' + | '25' + | '50' + | '60' + | '75' + | '100' + | '125' + | '150' + | '200' + | '300' + | '400' + | '500' + | '525' + | '550' + | '600' + | '700' + | '725' + | '750' + | '800' + | '900' + +export type ColorVariant = + | 'cyan' + | 'deep-blue' + | 'pacific' + | 'purple' + | 'emerald' + | 'mandarin' + | 'scarlet' + | 'gray' + | 'uni' + | 'salmon' + | 'turquoise' + | 'fuchsia' + +// Helper function to build color custom property strings +export function colorToken( + variant: ColorVariant, + scale?: ColorScale, +): CSSCustomProperty { + return scale ? `--color-${variant}-${scale}` : `--color-${variant}` +} + +// Step scale for spacing +export type StepScale = + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 15 + | 16 + | 20 + | 24 + | 30 + | 32 + +// Helper function to build spacing custom property strings +export function spacingToken( + step?: StepScale | 'half', +): CSSCustomProperty { + if (step === 'half') return '--half' + return step ? `--step-${step}` : '--step' +} + +// Text size variants +export type TextSize = + | '2xs' + | 'xs' + | 's' + | 'm' + | 'l' + | 'xl' + | '2xl' + | '3xl' + | '4xl' +export type TextRole = + | 'headline' + | 'display' + | 'header' + | 'tagline' + | 'title' + | 'subtitle' + +// Helper function to build text size custom property strings +export function textSizeToken( + size: TextSize | TextRole, +): CSSCustomProperty { + return `--text-${size}` +} diff --git a/web/common/src/types/enums.ts b/web/common/src/types/enums.ts new file mode 100644 index 0000000000..9985bc7e5b --- /dev/null +++ b/web/common/src/types/enums.ts @@ -0,0 +1,43 @@ +export const EnumSize = { + XXS: '2xs', + XS: 'xs', + S: 's', + M: 'm', + L: 'l', + XL: 'xl', + XXL: '2xl', +} as const +export type Size = (typeof EnumSize)[keyof typeof EnumSize] + +export const EnumHeadlineLevel = { + H1: 1, + H2: 2, + H3: 3, + H4: 4, + H5: 5, + H6: 6, +} as const +export type HeadlineLevel = + (typeof EnumHeadlineLevel)[keyof typeof EnumHeadlineLevel] + +export const EnumSide = { + LEFT: 'left', + RIGHT: 'right', + BOTH: 'both', +} as const +export type Side = (typeof EnumSide)[keyof typeof EnumSide] + +export const EnumLayoutDirection = { + VERTICAL: 'vertical', + HORIZONTAL: 'horizontal', + BOTH: 'both', +} as const +export type LayoutDirection = + (typeof EnumLayoutDirection)[keyof typeof EnumLayoutDirection] + +export const EnumShape = { + Square: 'square', + Round: 'round', + Pill: 'pill', +} as const +export type Shape = (typeof EnumShape)[keyof typeof EnumShape] diff --git a/web/common/src/utils/index.ts b/web/common/src/utils/index.ts index f967d48da4..95e869c212 100644 --- a/web/common/src/utils/index.ts +++ b/web/common/src/utils/index.ts @@ -1,4 +1,10 @@ import type { Nil } from '@/types' +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} export function isNil(value: unknown): value is Nil { return value == null diff --git a/web/common/tailwind.config.js b/web/common/tailwind.config.js new file mode 100644 index 0000000000..f751f8a6b0 --- /dev/null +++ b/web/common/tailwind.config.js @@ -0,0 +1,45 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx}', './src/**/*.stories.{js,ts,jsx,tsx}'], + theme: { + colors: { + transparent: 'transparent', + white: 'var(--color-white)', + black: 'var(--color-black)', + dark: 'var(--color-dark)', + light: 'var(--color-light)', + brand: 'var(--color-brand)', + prose: 'var(--color-prose)', + badge: { + background: 'var(--color-badge-background)', + foreground: 'var(--color-badge-foreground)', + }, + }, + extend: { + borderRadius: { + '2xs': 'var(--radius-xs)', + xs: 'calc(var(--radius-xs) + 1px)', + sm: 'calc(var(--radius-xs) + 2px)', + md: 'calc(var(--radius-s))', + lg: 'calc(var(--radius-s) + 1px)', + xl: 'calc(var(--radius-s) + 2px)', + '2xl': 'calc(var(--radius-m))', + }, + fontSize: { + '2xs': 'var(--text-2xs)', + xs: 'var(--text-xs)', + s: 'var(--text-s)', + m: 'var(--text-m)', + l: 'var(--text-l)', + xl: 'var(--text-xl)', + '2xl': 'var(--text-2xl)', + '3xl': 'var(--text-3xl)', + '4xl': 'var(--text-4xl)', + }, + fontFamily: { + mono: ['var(--font-mono)'], + }, + }, + }, + plugins: [require('@tailwindcss/typography')], +} diff --git a/web/common/tsconfig.build.json b/web/common/tsconfig.build.json new file mode 100644 index 0000000000..80129f67c0 --- /dev/null +++ b/web/common/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"], + "compilerOptions": { + "noEmit": false, + "allowImportingTsExtensions": false, + "verbatimModuleSyntax": false, + "declaration": true, + "declarationMap": true, + "declarationDir": "./dist", + "emitDeclarationOnly": false, + "outDir": "./dist" + } +} diff --git a/web/common/vite.config.js b/web/common/vite.config.js index 3fce71c299..65b369ab16 100644 --- a/web/common/vite.config.js +++ b/web/common/vite.config.js @@ -2,12 +2,22 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' import dts from 'vite-plugin-dts' +import { viteStaticCopy } from 'vite-plugin-static-copy' export default defineConfig({ plugins: [ react(), dts({ insertTypesEntry: true, + declarationMap: true, + }), + viteStaticCopy({ + targets: [ + { + src: 'src/styles/design', + dest: 'styles', + }, + ], }), ], resolve: { @@ -22,15 +32,28 @@ export default defineConfig({ fileName: format => `sqlmesh-common.${format}.js`, }, rollupOptions: { - external: ['react', 'react-dom'], + external: [ + 'react', + 'react-dom', + 'clsx', + 'tailwind-merge', + 'class-variance-authority', + '@radix-ui/react-slot', + 'tailwindcss', + '@tailwindcss/typography', + ], output: { globals: { react: 'React', 'react-dom': 'ReactDOM', + clsx: 'clsx', + 'tailwind-merge': 'tailwindMerge', + 'class-variance-authority': 'classVarianceAuthority', + '@radix-ui/react-slot': 'radixSlot', }, }, }, - sourcemap: true, + sourcemap: process.env.NODE_ENV !== 'production', outDir: 'dist', }, }) From 59f8b2e542cb55ec673fe55f6be828093c3a0920 Mon Sep 17 00:00:00 2001 From: Sergio Kef Date: Mon, 18 Aug 2025 18:40:34 +0200 Subject: [PATCH 0725/1056] fix: broken link in docs (#5175) --- docs/integrations/engines/bigquery.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/engines/bigquery.md b/docs/integrations/engines/bigquery.md index 2e12954f8a..a454996ecd 100644 --- a/docs/integrations/engines/bigquery.md +++ b/docs/integrations/engines/bigquery.md @@ -145,7 +145,7 @@ pip install "sqlmesh[bigquery]" | Option | Description | Type | Required | |---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------:|:--------:| | `type` | Engine type name - must be `bigquery` | string | Y | -| `method` | Connection methods - see [allowed values below](#connection-methods). Default: `oauth`. | string | N | +| `method` | Connection methods - see [allowed values below](#authentication-methods). Default: `oauth`. | string | N | | `project` | The ID of the GCP project | string | N | | `location` | The location of for the datasets (can be regional or multi-regional) | string | N | | `execution_project` | The name of the GCP project to bill for the execution of the models. If not set, the project associated with the model will be used. | string | N | From e57457757991e691a0c95e5ffeb69a4da1e7c596 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 19 Aug 2025 09:31:18 +1200 Subject: [PATCH 0726/1056] Feat(dbt_cli): Add support for '--profile' and '--target' (#5174) --- sqlmesh/core/config/loader.py | 9 ++++++- sqlmesh/core/context.py | 5 +++- sqlmesh/dbt/loader.py | 3 ++- sqlmesh/dbt/profile.py | 4 ++- sqlmesh_dbt/cli.py | 18 ++++++++++--- sqlmesh_dbt/error.py | 41 ++++++++++++++++++++++++++++++ sqlmesh_dbt/operations.py | 6 ++++- tests/dbt/cli/test_global_flags.py | 30 ++++++++++++++++++++++ tests/dbt/cli/test_operations.py | 16 ++++++++++++ 9 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 sqlmesh_dbt/error.py create mode 100644 tests/dbt/cli/test_global_flags.py diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index 2c1554454b..a7b997e303 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -32,6 +32,7 @@ def load_configs( paths: t.Union[str | Path, t.Iterable[str | Path]], sqlmesh_path: t.Optional[Path] = None, dotenv_path: t.Optional[Path] = None, + **kwargs: t.Any, ) -> t.Dict[Path, C]: sqlmesh_path = sqlmesh_path or c.SQLMESH_PATH config = config or "config" @@ -70,6 +71,7 @@ def load_configs( project_paths=[path / name for name in ALL_CONFIG_FILENAMES], personal_paths=personal_paths, config_name=config, + **kwargs, ) for path in absolute_paths } @@ -81,6 +83,7 @@ def load_config_from_paths( personal_paths: t.Optional[t.List[Path]] = None, config_name: str = "config", load_from_env: bool = True, + **kwargs: t.Any, ) -> C: project_paths = project_paths or [] personal_paths = personal_paths or [] @@ -168,7 +171,11 @@ def load_config_from_paths( if dbt_project_file: from sqlmesh.dbt.loader import sqlmesh_config - dbt_python_config = sqlmesh_config(project_root=dbt_project_file.parent) + dbt_python_config = sqlmesh_config( + project_root=dbt_project_file.parent, + dbt_profile_name=kwargs.pop("profile", None), + dbt_target_name=kwargs.pop("target", None), + ) if type(dbt_python_config) != config_type: dbt_python_config = convert_config_type(dbt_python_config, config_type) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index eca60ecea9..9022f3f069 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -367,9 +367,12 @@ def __init__( loader: t.Optional[t.Type[Loader]] = None, load: bool = True, users: t.Optional[t.List[User]] = None, + config_loader_kwargs: t.Optional[t.Dict[str, t.Any]] = None, ): self.configs = ( - config if isinstance(config, dict) else load_configs(config, self.CONFIG_TYPE, paths) + config + if isinstance(config, dict) + else load_configs(config, self.CONFIG_TYPE, paths, **(config_loader_kwargs or {})) ) self._projects = {config.project for config in self.configs.values()} self.dag: DAG[str] = DAG() diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 3a22b61bf6..b4e8caf0bc 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -44,13 +44,14 @@ def sqlmesh_config( project_root: t.Optional[Path] = None, state_connection: t.Optional[ConnectionConfig] = None, + dbt_profile_name: t.Optional[str] = None, dbt_target_name: t.Optional[str] = None, variables: t.Optional[t.Dict[str, t.Any]] = None, register_comments: t.Optional[bool] = None, **kwargs: t.Any, ) -> Config: project_root = project_root or Path() - context = DbtContext(project_root=project_root) + context = DbtContext(project_root=project_root, profile_name=dbt_profile_name) profile = Profile.load(context, target_name=dbt_target_name) model_defaults = kwargs.pop("model_defaults", ModelDefaultsConfig()) if model_defaults.dialect is None: diff --git a/sqlmesh/dbt/profile.py b/sqlmesh/dbt/profile.py index 72634833a6..ea0384c786 100644 --- a/sqlmesh/dbt/profile.py +++ b/sqlmesh/dbt/profile.py @@ -101,8 +101,10 @@ def _read_profile( target_name = context.render(project_data.get("target")) if target_name not in outputs: + target_names = "\n".join(f"- {name}" for name in outputs) raise ConfigError( - f"Target '{target_name}' not specified in profiles for '{context.profile_name}'." + f"Target '{target_name}' not specified in profiles for '{context.profile_name}'. " + f"The valid target names for this profile are:\n{target_names}" ) target_fields = load_yaml(context.render(yaml.dump(outputs[target_name]))) diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py index 2ec59b665d..500a9d6fa0 100644 --- a/sqlmesh_dbt/cli.py +++ b/sqlmesh_dbt/cli.py @@ -2,6 +2,8 @@ import sys import click from sqlmesh_dbt.operations import DbtOperations, create +from sqlmesh_dbt.error import cli_global_error_handler +from pathlib import Path def _get_dbt_operations(ctx: click.Context) -> DbtOperations: @@ -10,9 +12,14 @@ def _get_dbt_operations(ctx: click.Context) -> DbtOperations: return ctx.obj -@click.group() +@click.group(invoke_without_command=True) +@click.option("--profile", help="Which existing profile to load. Overrides output.profile") +@click.option("-t", "--target", help="Which target to load for the given profile") @click.pass_context -def dbt(ctx: click.Context) -> None: +@cli_global_error_handler +def dbt( + ctx: click.Context, profile: t.Optional[str] = None, target: t.Optional[str] = None +) -> None: """ An ELT tool for managing your SQL transformations and data models, powered by the SQLMesh engine. """ @@ -22,7 +29,12 @@ def dbt(ctx: click.Context) -> None: return # TODO: conditionally call create() if there are times we dont want/need to import sqlmesh and load a project - ctx.obj = create() + ctx.obj = create(project_dir=Path.cwd(), profile=profile, target=target) + + if not ctx.invoked_subcommand: + click.echo( + f"No command specified. Run `{ctx.info_name} --help` to see the available commands." + ) @dbt.command() diff --git a/sqlmesh_dbt/error.py b/sqlmesh_dbt/error.py new file mode 100644 index 0000000000..f5d4bc438c --- /dev/null +++ b/sqlmesh_dbt/error.py @@ -0,0 +1,41 @@ +import typing as t +import logging +from functools import wraps +import click +import sys + +logger = logging.getLogger(__name__) + + +def cli_global_error_handler( + func: t.Callable[..., t.Any], +) -> t.Callable[..., t.Any]: + @wraps(func) + def wrapper(*args: t.List[t.Any], **kwargs: t.Any) -> t.Any: + try: + return func(*args, **kwargs) + except Exception as ex: + # these imports are deliberately deferred to avoid the penalty of importing the `sqlmesh` + # package up front for every CLI command + from sqlmesh.utils.errors import SQLMeshError + from sqlglot.errors import SqlglotError + + if isinstance(ex, (SQLMeshError, SqlglotError, ValueError)): + click.echo(click.style("Error: " + str(ex), fg="red")) + sys.exit(1) + else: + raise + finally: + context_or_obj = args[0] + sqlmesh_context = ( + context_or_obj.obj if isinstance(context_or_obj, click.Context) else context_or_obj + ) + if sqlmesh_context is not None: + # important to import this only if a context was created + # otherwise something like `sqlmesh_dbt run --help` will trigger this import because it's in the finally: block + from sqlmesh import Context + + if isinstance(sqlmesh_context, Context): + sqlmesh_context.close() + + return wrapper diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index ec07efd37b..f9aae3cdac 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -53,7 +53,10 @@ def console(self) -> DbtCliConsole: def create( - project_dir: t.Optional[Path] = None, profiles_dir: t.Optional[Path] = None, debug: bool = False + project_dir: t.Optional[Path] = None, + profile: t.Optional[str] = None, + target: t.Optional[str] = None, + debug: bool = False, ) -> DbtOperations: with Progress(transient=True) as progress: # Indeterminate progress bar before SQLMesh import to provide feedback to the user that something is indeed happening @@ -76,6 +79,7 @@ def create( sqlmesh_context = Context( paths=[project_dir], + config_loader_kwargs=dict(profile=profile, target=target), load=True, ) diff --git a/tests/dbt/cli/test_global_flags.py b/tests/dbt/cli/test_global_flags.py new file mode 100644 index 0000000000..802d359346 --- /dev/null +++ b/tests/dbt/cli/test_global_flags.py @@ -0,0 +1,30 @@ +import typing as t +from pathlib import Path +import pytest +from click.testing import Result + +pytestmark = pytest.mark.slow + + +def test_profile_and_target(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): + # profile doesnt exist - error + result = invoke_cli(["--profile", "nonexist"]) + assert result.exit_code == 1 + assert "Profile 'nonexist' not found in profiles" in result.output + + # profile exists - successful load with default target + result = invoke_cli(["--profile", "jaffle_shop"]) + assert result.exit_code == 0 + assert "No command specified" in result.output + + # profile exists but target doesnt - error + result = invoke_cli(["--profile", "jaffle_shop", "--target", "nonexist"]) + assert result.exit_code == 1 + assert "Target 'nonexist' not specified in profiles" in result.output + assert "valid target names for this profile are" in result.output + assert "- dev" in result.output + + # profile exists and so does target - successful load with specified target + result = invoke_cli(["--profile", "jaffle_shop", "--target", "dev"]) + assert result.exit_code == 0 + assert "No command specified" in result.output diff --git a/tests/dbt/cli/test_operations.py b/tests/dbt/cli/test_operations.py index c35cab992c..9d36b10f60 100644 --- a/tests/dbt/cli/test_operations.py +++ b/tests/dbt/cli/test_operations.py @@ -2,6 +2,7 @@ import pytest from sqlmesh_dbt.operations import create from sqlmesh.utils import yaml +from sqlmesh.utils.errors import SQLMeshError import time_machine pytestmark = pytest.mark.slow @@ -53,3 +54,18 @@ def test_create_uses_configured_start_date_if_supplied(jaffle_shop_duckdb: Path) for model in operations.context.models.values() if not model.kind.is_seed ) + + +def test_create_can_specify_profile_and_target(jaffle_shop_duckdb: Path): + with pytest.raises(SQLMeshError, match=r"Profile 'foo' not found"): + create(profile="foo") + + with pytest.raises( + SQLMeshError, match=r"Target 'prod' not specified in profiles for 'jaffle_shop'" + ): + create(profile="jaffle_shop", target="prod") + + dbt_project = create(profile="jaffle_shop", target="dev").project + + assert dbt_project.context.profile_name == "jaffle_shop" + assert dbt_project.context.target_name == "dev" From 98c858cb8e8335731d6760fb3bc54c2b19681837 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 19 Aug 2025 06:46:12 +0300 Subject: [PATCH 0727/1056] Feat(dbt): Add dbt graph context variable support (#5159) --- sqlmesh/dbt/adapter.py | 19 ++++++++++ sqlmesh/dbt/builtin.py | 1 + sqlmesh/dbt/context.py | 3 ++ sqlmesh/dbt/manifest.py | 35 +++++++++++++++++ sqlmesh/utils/conversions.py | 11 ++++++ sqlmesh/utils/jinja.py | 3 ++ tests/dbt/test_transformation.py | 38 ++++++++++++++++--- tests/fixtures/dbt/sushi_test/dbt_project.yml | 3 +- .../dbt/sushi_test/macros/graph_usage.sql | 18 +++++++++ 9 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/dbt/sushi_test/macros/graph_usage.sql diff --git a/sqlmesh/dbt/adapter.py b/sqlmesh/dbt/adapter.py index 00a1d86ba2..b524acc160 100644 --- a/sqlmesh/dbt/adapter.py +++ b/sqlmesh/dbt/adapter.py @@ -11,6 +11,7 @@ from sqlmesh.core.snapshot import DeployabilityIndex, Snapshot, to_table_mapping from sqlmesh.utils.errors import ConfigError, ParsetimeAdapterCallError from sqlmesh.utils.jinja import JinjaMacroRegistry +from sqlmesh.utils import AttributeDict if t.TYPE_CHECKING: import agate @@ -158,6 +159,20 @@ def compare_dbr_version(self, major: int, minor: int) -> int: # Always return -1 to fallback to Spark macro implementations. return -1 + @property + def graph(self) -> t.Any: + return AttributeDict( + { + "exposures": {}, + "groups": {}, + "metrics": {}, + "nodes": {}, + "sources": {}, + "semantic_models": {}, + "saved_queries": {}, + } + ) + class ParsetimeAdapter(BaseAdapter): def get_relation(self, database: str, schema: str, identifier: str) -> t.Optional[BaseRelation]: @@ -246,6 +261,10 @@ def __init__( **table_mapping, } + @property + def graph(self) -> t.Any: + return self.jinja_globals.get("flat_graph", super().graph) + def get_relation( self, database: t.Optional[str], schema: str, identifier: str ) -> t.Optional[BaseRelation]: diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 07edeefa2e..70e1b10099 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -452,6 +452,7 @@ def create_builtin_globals( "load_result": sql_execution.load_result, "run_query": sql_execution.run_query, "statement": sql_execution.statement, + "graph": adapter.graph, } ) diff --git a/sqlmesh/dbt/context.py b/sqlmesh/dbt/context.py index d29dc43574..2eceb005a7 100644 --- a/sqlmesh/dbt/context.py +++ b/sqlmesh/dbt/context.py @@ -242,6 +242,9 @@ def jinja_globals(self) -> t.Dict[str, JinjaGlobalAttribute]: # pass user-specified default dialect if we have already loaded the config if self.sqlmesh_config.dialect: output["dialect"] = self.sqlmesh_config.dialect + # Pass flat graph structure like dbt + if self._manifest is not None: + output["flat_graph"] = AttributeDict(self.manifest.flat_graph) return output def context_for_dependencies(self, dependencies: Dependencies) -> DbtContext: diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 4f839b9c9b..91c87f413e 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -12,6 +12,8 @@ from dbt import constants as dbt_constants, flags +from sqlmesh.utils.conversions import make_serializable + # Override the file name to prevent dbt commands from invalidating the cache. dbt_constants.PARTIAL_PARSE_FILE_NAME = "sqlmesh_partial_parse.msgpack" @@ -155,6 +157,39 @@ def all_macros(self) -> t.Dict[str, t.Dict[str, MacroInfo]]: result[package_name][macro_name] = macro_config.info return result + @property + def flat_graph(self) -> t.Dict[str, t.Any]: + return { + "exposures": { + k: make_serializable(v.to_dict(omit_none=False)) + for k, v in getattr(self._manifest, "exposures", {}).items() + }, + "groups": { + k: make_serializable(v.to_dict(omit_none=False)) + for k, v in getattr(self._manifest, "groups", {}).items() + }, + "metrics": { + k: make_serializable(v.to_dict(omit_none=False)) + for k, v in getattr(self._manifest, "metrics", {}).items() + }, + "nodes": { + k: make_serializable(v.to_dict(omit_none=False)) + for k, v in self._manifest.nodes.items() + }, + "sources": { + k: make_serializable(v.to_dict(omit_none=False)) + for k, v in self._manifest.sources.items() + }, + "semantic_models": { + k: make_serializable(v.to_dict(omit_none=False)) + for k, v in getattr(self._manifest, "semantic_models", {}).items() + }, + "saved_queries": { + k: make_serializable(v.to_dict(omit_none=False)) + for k, v in getattr(self._manifest, "saved_queries", {}).items() + }, + } + def _load_all(self) -> None: if self._is_loaded: return diff --git a/sqlmesh/utils/conversions.py b/sqlmesh/utils/conversions.py index 2b92772022..411f3c8ab1 100644 --- a/sqlmesh/utils/conversions.py +++ b/sqlmesh/utils/conversions.py @@ -1,6 +1,7 @@ from __future__ import annotations import typing as t +from datetime import date, datetime def ensure_bool(val: t.Any) -> bool: @@ -19,3 +20,13 @@ def try_str_to_bool(val: str) -> t.Union[str, bool]: return maybe_bool == "true" return val + + +def make_serializable(obj: t.Any) -> t.Any: + if isinstance(obj, (date, datetime)): + return obj.isoformat() + if isinstance(obj, dict): + return {k: make_serializable(v) for k, v in obj.items()} + if isinstance(obj, list): + return [make_serializable(item) for item in obj] + return obj diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index 6720c24581..fc9d898159 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -363,6 +363,9 @@ def add_globals(self, globals: t.Dict[str, JinjaGlobalAttribute]) -> None: Args: globals: The global objects that should be added. """ + # Keep the registry lightweight when the graph is not needed + if not "graph" in self.packages: + globals.pop("flat_graph", None) self.global_objs.update(**self._validate_global_objs(globals)) def build_macro(self, reference: MacroReference, **kwargs: t.Any) -> t.Optional[t.Callable]: diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index a16cc16f43..cefedd6814 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1606,6 +1606,7 @@ def test_on_run_start_end(): assert root_environment_statements.after_all == [ "JINJA_STATEMENT_BEGIN;\n{{ create_tables(schemas) }}\nJINJA_END;", "JINJA_STATEMENT_BEGIN;\nDROP TABLE to_be_executed_last;\nJINJA_END;", + "JINJA_STATEMENT_BEGIN;\n{{ graph_usage() }}\nJINJA_END;", ] assert root_environment_statements.jinja_macros.root_package_name == "sushi" @@ -1626,6 +1627,7 @@ def test_on_run_start_end(): snapshots=sushi_context.snapshots, runtime_stage=RuntimeStage.AFTER_ALL, environment_naming_info=EnvironmentNamingInfo(name="dev"), + engine_adapter=sushi_context.engine_adapter, ) assert rendered_before_all == [ @@ -1635,12 +1637,35 @@ def test_on_run_start_end(): ] # The jinja macro should have resolved the schemas for this environment and generated corresponding statements - assert sorted(rendered_after_all) == sorted( - [ - "CREATE OR REPLACE TABLE schema_table_snapshots__dev AS SELECT 'snapshots__dev' AS schema", - "CREATE OR REPLACE TABLE schema_table_sushi__dev AS SELECT 'sushi__dev' AS schema", - "DROP TABLE to_be_executed_last", - ] + expected_statements = [ + "CREATE OR REPLACE TABLE schema_table_snapshots__dev AS SELECT 'snapshots__dev' AS schema", + "CREATE OR REPLACE TABLE schema_table_sushi__dev AS SELECT 'sushi__dev' AS schema", + "DROP TABLE to_be_executed_last", + ] + assert sorted(rendered_after_all[:-1]) == sorted(expected_statements) + + # Assert the models with their materialisations are present in the rendered graph_table statement + graph_table_stmt = rendered_after_all[-1] + assert "'model.sushi.simple_model_a' AS unique_id, 'table' AS materialized" in graph_table_stmt + assert "'model.sushi.waiters' AS unique_id, 'ephemeral' AS materialized" in graph_table_stmt + assert "'model.sushi.simple_model_b' AS unique_id, 'table' AS materialized" in graph_table_stmt + assert ( + "'model.sushi.waiter_as_customer_by_day' AS unique_id, 'incremental' AS materialized" + in graph_table_stmt + ) + assert "'model.sushi.top_waiters' AS unique_id, 'view' AS materialized" in graph_table_stmt + assert "'model.customers.customers' AS unique_id, 'view' AS materialized" in graph_table_stmt + assert ( + "'model.customers.customer_revenue_by_day' AS unique_id, 'incremental' AS materialized" + in graph_table_stmt + ) + assert ( + "'model.sushi.waiter_revenue_by_day.v1' AS unique_id, 'incremental' AS materialized" + in graph_table_stmt + ) + assert ( + "'model.sushi.waiter_revenue_by_day.v2' AS unique_id, 'incremental' AS materialized" + in graph_table_stmt ) # Nested dbt_packages on run start / on run end @@ -1675,6 +1700,7 @@ def test_on_run_start_end(): snapshots=sushi_context.snapshots, runtime_stage=RuntimeStage.AFTER_ALL, environment_naming_info=EnvironmentNamingInfo(name="dev"), + engine_adapter=sushi_context.engine_adapter, ) # Validate order of execution to match dbt's diff --git a/tests/fixtures/dbt/sushi_test/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_project.yml index 1afa7dd2c6..073d85b4d4 100644 --- a/tests/fixtures/dbt/sushi_test/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/dbt_project.yml @@ -70,4 +70,5 @@ on-run-start: - "{{ log_value('on-run-start') }}" on-run-end: - '{{ create_tables(schemas) }}' - - 'DROP TABLE to_be_executed_last;' \ No newline at end of file + - 'DROP TABLE to_be_executed_last;' + - '{{ graph_usage() }}' \ No newline at end of file diff --git a/tests/fixtures/dbt/sushi_test/macros/graph_usage.sql b/tests/fixtures/dbt/sushi_test/macros/graph_usage.sql new file mode 100644 index 0000000000..8b133ec280 --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/macros/graph_usage.sql @@ -0,0 +1,18 @@ +{% macro graph_usage() %} +{% if execute %} + {% set model_nodes = graph.nodes.values() + | selectattr("resource_type", "equalto", "model") + | list %} + + {% set out = [] %} + {% for node in model_nodes %} + {% set line = "select '" ~ node.unique_id ~ "' as unique_id, '" ~ node.config.materialized ~ "' as materialized" %} + {% do out.append(line) %} + {% endfor %} + + {% if out %} + {% set sql_statement = "create or replace table graph_table as\n" ~ (out | join('\nunion all\n')) %} + {{ return(sql_statement) }} + {% endif %} +{% endif %} +{% endmacro %} From 4bc8926b85ab7f3fef0bd668eda354b7ef809dc5 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:31:55 +0300 Subject: [PATCH 0728/1056] Chore!: bump sqlglot to v27.8.0 (#5185) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a7380980c6..12c855f6f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.7.0", + "sqlglot[rs]~=27.8.0", "tenacity", "time-machine", "json-stream" From 50b57db9c7e168104bdfbf85445b8b510cb2332a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:42:45 +0000 Subject: [PATCH 0729/1056] chore(deps): bump @tanstack/react-router-devtools from 1.129.8 to 1.131.26 (#5180) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 46 ++++++++++++++++++++++++--------------- vscode/react/package.json | 2 +- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bedd297a5c..0e103546fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,8 +121,8 @@ importers: specifier: ^1.129.8 version: 1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-router-devtools': - 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))(@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) + 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) '@tanstack/react-virtual': specifier: ^3.13.12 version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -743,6 +743,9 @@ packages: '@codemirror/language@6.11.2': resolution: {integrity: sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==} + '@codemirror/language@6.11.3': + resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} + '@codemirror/legacy-modes@6.5.1': resolution: {integrity: sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==} @@ -2255,11 +2258,11 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.129.8': - resolution: {integrity: sha512-+gVwYRLFAoQ+U4+UGX5/VgxspoJN4dm6/z4vYaZyrOUBVo+UjjH+bpvdz9ZrooBQ9EdkrkORPH8EfZp5qgi5Bg==} + '@tanstack/react-router-devtools@1.131.26': + resolution: {integrity: sha512-QdDF2t3ILZLqblBYDWQXpQ8QsHzo2ZJcWhaeQEdAkMZ0w0mlfKdZKOGigA21KvDbyTOgkfuQBj+DlkiQPqKYMA==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.129.8 + '@tanstack/react-router': ^1.131.26 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' @@ -2293,11 +2296,11 @@ packages: resolution: {integrity: sha512-Izqf5q8TzJv0DJURynitJioPJT3dPAefrzHi2wlY/Q5+7nEG41SkjYMotTX2Q9i/Pjl91lW8gERCHpksszRdRw==} engines: {node: '>=12'} - '@tanstack/router-devtools-core@1.129.8': - resolution: {integrity: sha512-1yiAoWWYV3hWLXoHv92LMU67EjJpavoavo00EYzf7RLCy0TA/a+KyokZBS6PD38sITamHgVeY/jJBGD6hr47rQ==} + '@tanstack/router-devtools-core@1.131.26': + resolution: {integrity: sha512-TGHmRDQpYphuRbDH+jJp418vQuIydzITaUx7MiPk5U1ZZ+2O/GxcF/ycXmyYR0IHTpSky35I83X3bKTiv+thyw==} engines: {node: '>=12'} peerDependencies: - '@tanstack/router-core': ^1.129.8 + '@tanstack/router-core': ^1.131.26 csstype: ^3.0.10 solid-js: '>=1.9.5' tiny-invariant: ^1.3.3 @@ -3646,8 +3649,8 @@ packages: electron-to-chromium@1.5.190: resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==} - electron-to-chromium@1.5.200: - resolution: {integrity: sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==} + electron-to-chromium@1.5.204: + resolution: {integrity: sha512-s9VbBXWxfDrl67PlO4avwh0/GU2vcwx8Fph3wlR8LJl7ySGYId59EFE17VWVcuC3sLWNPENm6Z/uGqKbkPCcXA==} elkjs@0.8.2: resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} @@ -7057,6 +7060,15 @@ snapshots: '@lezer/lr': 1.4.2 style-mod: 4.1.2 + '@codemirror/language@6.11.3': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.1 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + style-mod: 4.1.2 + '@codemirror/legacy-modes@6.5.1': dependencies: '@codemirror/language': 6.11.2 @@ -7079,7 +7091,7 @@ snapshots: '@codemirror/theme-one-dark@6.1.2': dependencies: - '@codemirror/language': 6.11.2 + '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.1 '@lezer/highlight': 1.2.1 @@ -8735,10 +8747,10 @@ snapshots: '@tanstack/query-core': 5.83.0 react: 18.3.1 - '@tanstack/react-router-devtools@1.129.8(@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.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)': 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.129.8(@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.129.8)(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: @@ -8787,7 +8799,7 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.129.8(@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.129.8)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': dependencies: '@tanstack/router-core': 1.129.8 clsx: 2.1.1 @@ -9915,7 +9927,7 @@ snapshots: browserslist@4.25.2: dependencies: caniuse-lite: 1.0.30001735 - electron-to-chromium: 1.5.200 + electron-to-chromium: 1.5.204 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.2) @@ -10096,7 +10108,7 @@ snapshots: dependencies: '@codemirror/autocomplete': 6.18.6 '@codemirror/commands': 6.8.1 - '@codemirror/language': 6.11.2 + '@codemirror/language': 6.11.3 '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 @@ -10365,7 +10377,7 @@ snapshots: electron-to-chromium@1.5.190: {} - electron-to-chromium@1.5.200: {} + electron-to-chromium@1.5.204: {} elkjs@0.8.2: {} diff --git a/vscode/react/package.json b/vscode/react/package.json index 49b60b90dc..e12dd12179 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -22,7 +22,7 @@ "@tailwindcss/vite": "^4.1.11", "@tanstack/react-query": "^5.83.0", "@tanstack/react-router": "^1.129.8", - "@tanstack/react-router-devtools": "^1.129.8", + "@tanstack/react-router-devtools": "^1.131.26", "@tanstack/react-virtual": "^3.13.12", "@tanstack/router-plugin": "^1.129.8", "apache-arrow": "^19.0.1", From 0224d5f8743e844ffc8175ede9a1b5f58ccc3155 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 19 Aug 2025 14:34:51 -0700 Subject: [PATCH 0730/1056] Fix: Remove leftover forward-only category usage when categorizing orphaned snapshots (#5190) --- sqlmesh/core/plan/builder.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index a1adca56fb..3bfff1aa12 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -733,17 +733,11 @@ def _get_orphaned_indirect_change_category( # One of the new parents in the chain was breaking so this indirect snapshot is breaking return SnapshotChangeCategory.INDIRECT_BREAKING - if SnapshotChangeCategory.FORWARD_ONLY in previous_parent_categories: - # One of the new parents in the chain was forward-only so this indirect snapshot is forward-only - indirect_category = SnapshotChangeCategory.FORWARD_ONLY - elif ( - previous_parent_categories.intersection( - { - SnapshotChangeCategory.NON_BREAKING, - SnapshotChangeCategory.INDIRECT_NON_BREAKING, - } - ) - and indirect_category != SnapshotChangeCategory.FORWARD_ONLY + if previous_parent_categories.intersection( + { + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.INDIRECT_NON_BREAKING, + } ): # All changes in the chain were non-breaking so this indirect snapshot can be non-breaking too indirect_category = SnapshotChangeCategory.INDIRECT_NON_BREAKING From 7157652eb9e1fa5c56ae90d2d039ac65d6a34542 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:23:17 -0700 Subject: [PATCH 0731/1056] chore: allow forks run private repo tests (#5176) --- .github/workflows/private-repo-test.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/private-repo-test.yaml b/.github/workflows/private-repo-test.yaml index eaa71885ff..07253f1a00 100644 --- a/.github/workflows/private-repo-test.yaml +++ b/.github/workflows/private-repo-test.yaml @@ -1,7 +1,7 @@ name: Private Repo Testing on: - pull_request: + pull_request_target: branches: - main @@ -20,6 +20,7 @@ jobs: uses: actions/checkout@v5 with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.ref }} - name: Set up Python uses: actions/setup-python@v5 with: From 2592c5c62f08a1a714e5292fd08150bf96efb940 Mon Sep 17 00:00:00 2001 From: Andreas <65893109+fresioAS@users.noreply.github.com> Date: Wed, 20 Aug 2025 00:56:43 +0200 Subject: [PATCH 0732/1056] feat: Add support for Microsoft Fabric Warehouse (#4751) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mattias Thalén Co-authored-by: Erin Drummond --- .circleci/continue_config.yml | 2 + .circleci/install-prerequisites.sh | 2 +- Makefile | 3 + docs/guides/configuration.md | 1 + docs/integrations/engines/fabric.md | 34 ++ docs/integrations/overview.md | 1 + mkdocs.yml | 1 + pyproject.toml | 2 + sqlmesh/core/config/__init__.py | 1 + sqlmesh/core/config/connection.py | 57 +++- sqlmesh/core/engine_adapter/__init__.py | 2 + sqlmesh/core/engine_adapter/fabric.py | 291 ++++++++++++++++++ tests/conftest.py | 2 +- .../engine_adapter/integration/__init__.py | 7 + .../engine_adapter/integration/config.yaml | 16 + .../integration/test_integration.py | 20 +- tests/core/engine_adapter/test_fabric.py | 90 ++++++ tests/core/test_connection_config.py | 93 ++++++ 18 files changed, 613 insertions(+), 12 deletions(-) create mode 100644 docs/integrations/engines/fabric.md create mode 100644 sqlmesh/core/engine_adapter/fabric.py create mode 100644 tests/core/engine_adapter/test_fabric.py diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 04135574a9..5fca61d7ed 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -304,6 +304,8 @@ workflows: - bigquery - clickhouse-cloud - athena + # todo: enable fabric when cicd catalog create/drop implemented in manage-test-db.sh + #- fabric - gcp-postgres filters: branches: diff --git a/.circleci/install-prerequisites.sh b/.circleci/install-prerequisites.sh index 1eebd92c71..acd25ae02c 100755 --- a/.circleci/install-prerequisites.sh +++ b/.circleci/install-prerequisites.sh @@ -12,7 +12,7 @@ fi ENGINE="$1" -COMMON_DEPENDENCIES="libpq-dev netcat-traditional" +COMMON_DEPENDENCIES="libpq-dev netcat-traditional unixodbc-dev" ENGINE_DEPENDENCIES="" if [ "$ENGINE" == "spark" ]; then diff --git a/Makefile b/Makefile index 855d866c84..3fea757169 100644 --- a/Makefile +++ b/Makefile @@ -173,6 +173,9 @@ clickhouse-cloud-test: guard-CLICKHOUSE_CLOUD_HOST guard-CLICKHOUSE_CLOUD_USERNA athena-test: guard-AWS_ACCESS_KEY_ID guard-AWS_SECRET_ACCESS_KEY guard-ATHENA_S3_WAREHOUSE_LOCATION engine-athena-install pytest -n auto -m "athena" --retries 3 --junitxml=test-results/junit-athena.xml +fabric-test: guard-FABRIC_HOST guard-FABRIC_CLIENT_ID guard-FABRIC_CLIENT_SECRET guard-FABRIC_DATABASE engine-fabric-install + pytest -n auto -m "fabric" --retries 3 --junitxml=test-results/junit-fabric.xml + gcp-postgres-test: guard-GCP_POSTGRES_INSTANCE_CONNECTION_STRING guard-GCP_POSTGRES_USER guard-GCP_POSTGRES_PASSWORD guard-GCP_POSTGRES_KEYFILE_JSON engine-gcppostgres-install pytest -n auto -m "gcp_postgres" --retries 3 --junitxml=test-results/junit-gcp-postgres.xml diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index b137546d84..d2e294a589 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -909,6 +909,7 @@ These pages describe the connection configuration options for each execution eng * [BigQuery](../integrations/engines/bigquery.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) diff --git a/docs/integrations/engines/fabric.md b/docs/integrations/engines/fabric.md new file mode 100644 index 0000000000..eb00b5ac1d --- /dev/null +++ b/docs/integrations/engines/fabric.md @@ -0,0 +1,34 @@ +# Fabric + +## Local/Built-in Scheduler +**Engine Adapter Type**: `fabric` + +NOTE: Fabric Warehouse is not recommended to be used for the SQLMesh [state connection](../../reference/configuration.md#connections). + +### Installation +#### Microsoft Entra ID / Azure Active Directory Authentication: +``` +pip install "sqlmesh[fabric]" +``` + +### Connection options + +| Option | Description | Type | Required | +| ----------------- | ------------------------------------------------------------ | :----------: | :------: | +| `type` | Engine type name - must be `fabric` | string | Y | +| `host` | The hostname of the Fabric Warehouse server | string | Y | +| `user` | The client id to use for authentication with the Fabric Warehouse server | string | N | +| `password` | The client secret to use for authentication with the Fabric Warehouse server | string | N | +| `port` | The port number of the Fabric Warehouse server | int | N | +| `database` | The target database | string | N | +| `charset` | The character set used for the connection | string | N | +| `timeout` | The query timeout in seconds. Default: no timeout | int | N | +| `login_timeout` | The timeout for connection and login in seconds. Default: 60 | int | N | +| `appname` | The application name to use for the connection | string | N | +| `conn_properties` | The list of connection properties | list[string] | N | +| `autocommit` | Is autocommit mode enabled. Default: false | bool | N | +| `driver` | The driver to use for the connection. Default: pyodbc | string | N | +| `driver_name` | The driver name to use for the connection. E.g., *ODBC Driver 18 for SQL Server* | string | N | +| `tenant_id` | The Azure / Entra tenant UUID | string | Y | +| `workspace_id` | The Fabric workspace UUID. The preferred way to retrieve it is by running `notebookutils.runtime.context.get("currentWorkspaceId")` in a python notebook. | string | Y | +| `odbc_properties` | The dict of ODBC connection properties. E.g., authentication: ActiveDirectoryServicePrincipal. See more [here](https://learn.microsoft.com/en-us/sql/connect/odbc/dsn-connection-string-attribute?view=sql-server-ver16). | dict | N | diff --git a/docs/integrations/overview.md b/docs/integrations/overview.md index 5e850afbf6..94b9289d21 100644 --- a/docs/integrations/overview.md +++ b/docs/integrations/overview.md @@ -17,6 +17,7 @@ SQLMesh supports the following execution engines for running SQLMesh projects (e * [ClickHouse](./engines/clickhouse.md) (clickhouse) * [Databricks](./engines/databricks.md) (databricks) * [DuckDB](./engines/duckdb.md) (duckdb) +* [Fabric](./engines/fabric.md) (fabric) * [MotherDuck](./engines/motherduck.md) (motherduck) * [MSSQL](./engines/mssql.md) (mssql) * [MySQL](./engines/mysql.md) (mysql) diff --git a/mkdocs.yml b/mkdocs.yml index 34156b1b66..47ddca54e9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -83,6 +83,7 @@ nav: - integrations/engines/clickhouse.md - integrations/engines/databricks.md - integrations/engines/duckdb.md + - integrations/engines/fabric.md - integrations/engines/motherduck.md - integrations/engines/mssql.md - integrations/engines/mysql.md diff --git a/pyproject.toml b/pyproject.toml index 12c855f6f1..4e201a734d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,6 +103,7 @@ dev = [ dbt = ["dbt-core<2"] dlt = ["dlt"] duckdb = [] +fabric = ["pyodbc>=5.0.0"] gcppostgres = ["cloud-sql-python-connector[pg8000]>=1.8.0"] github = ["PyGithub>=2.6.0"] llm = ["langchain", "openai"] @@ -253,6 +254,7 @@ markers = [ "clickhouse_cloud: test for Clickhouse (cloud mode)", "databricks: test for Databricks", "duckdb: test for DuckDB", + "fabric: test for Fabric", "motherduck: test for MotherDuck", "mssql: test for MSSQL", "mysql: test for MySQL", diff --git a/sqlmesh/core/config/__init__.py b/sqlmesh/core/config/__init__.py index d8c7607d51..0dc99c0fd1 100644 --- a/sqlmesh/core/config/__init__.py +++ b/sqlmesh/core/config/__init__.py @@ -13,6 +13,7 @@ ConnectionConfig as ConnectionConfig, DatabricksConnectionConfig as DatabricksConnectionConfig, DuckDBConnectionConfig as DuckDBConnectionConfig, + FabricConnectionConfig as FabricConnectionConfig, GCPPostgresConnectionConfig as GCPPostgresConnectionConfig, MotherDuckConnectionConfig as MotherDuckConnectionConfig, MSSQLConnectionConfig as MSSQLConnectionConfig, diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 4f289262ea..b530af36da 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -43,7 +43,13 @@ logger = logging.getLogger(__name__) -RECOMMENDED_STATE_SYNC_ENGINES = {"postgres", "gcp_postgres", "mysql", "mssql", "azuresql"} +RECOMMENDED_STATE_SYNC_ENGINES = { + "postgres", + "gcp_postgres", + "mysql", + "mssql", + "azuresql", +} FORBIDDEN_STATE_SYNC_ENGINES = { # Do not support row-level operations "spark", @@ -1684,6 +1690,55 @@ def _extra_engine_config(self) -> t.Dict[str, t.Any]: return {"catalog_support": CatalogSupport.SINGLE_CATALOG_ONLY} +class FabricConnectionConfig(MSSQLConnectionConfig): + """ + Fabric Connection Configuration. + Inherits most settings from MSSQLConnectionConfig and sets the type to 'fabric'. + It is recommended to use the 'pyodbc' driver for Fabric. + """ + + type_: t.Literal["fabric"] = Field(alias="type", default="fabric") # type: ignore + DIALECT: t.ClassVar[t.Literal["fabric"]] = "fabric" # type: ignore + DISPLAY_NAME: t.ClassVar[t.Literal["Fabric"]] = "Fabric" # type: ignore + DISPLAY_ORDER: t.ClassVar[t.Literal[17]] = 17 # type: ignore + driver: t.Literal["pyodbc"] = "pyodbc" + workspace_id: str + tenant_id: str + autocommit: t.Optional[bool] = True + + @property + def _engine_adapter(self) -> t.Type[EngineAdapter]: + from sqlmesh.core.engine_adapter.fabric import FabricEngineAdapter + + return FabricEngineAdapter + + @property + def _connection_factory(self) -> t.Callable: + # Override to support catalog switching for Fabric + base_factory = super()._connection_factory + + def create_fabric_connection( + target_catalog: t.Optional[str] = None, *args: t.Any, **kwargs: t.Any + ) -> t.Callable: + kwargs["database"] = target_catalog or self.database + return base_factory(*args, **kwargs) + + return create_fabric_connection + + @property + def _extra_engine_config(self) -> t.Dict[str, t.Any]: + return { + "database": self.database, + # more operations than not require a specific catalog to be already active + # in particular, create/drop view, create/drop schema and querying information_schema + "catalog_support": CatalogSupport.REQUIRES_SET_CATALOG, + "workspace_id": self.workspace_id, + "tenant_id": self.tenant_id, + "user": self.user, + "password": self.password, + } + + class SparkConnectionConfig(ConnectionConfig): """ Vanilla Spark Connection Configuration. Use `DatabricksConnectionConfig` for Databricks. diff --git a/sqlmesh/core/engine_adapter/__init__.py b/sqlmesh/core/engine_adapter/__init__.py index 19332dc005..ab29885c7b 100644 --- a/sqlmesh/core/engine_adapter/__init__.py +++ b/sqlmesh/core/engine_adapter/__init__.py @@ -19,6 +19,7 @@ from sqlmesh.core.engine_adapter.trino import TrinoEngineAdapter from sqlmesh.core.engine_adapter.athena import AthenaEngineAdapter from sqlmesh.core.engine_adapter.risingwave import RisingwaveEngineAdapter +from sqlmesh.core.engine_adapter.fabric import FabricEngineAdapter DIALECT_TO_ENGINE_ADAPTER = { "hive": SparkEngineAdapter, @@ -35,6 +36,7 @@ "trino": TrinoEngineAdapter, "athena": AthenaEngineAdapter, "risingwave": RisingwaveEngineAdapter, + "fabric": FabricEngineAdapter, } DIALECT_ALIASES = { diff --git a/sqlmesh/core/engine_adapter/fabric.py b/sqlmesh/core/engine_adapter/fabric.py new file mode 100644 index 0000000000..6f0123d022 --- /dev/null +++ b/sqlmesh/core/engine_adapter/fabric.py @@ -0,0 +1,291 @@ +from __future__ import annotations + +import typing as t +import logging +import requests +from functools import cached_property +from sqlglot import exp +from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_result +from sqlmesh.core.engine_adapter.mssql import MSSQLEngineAdapter +from sqlmesh.core.engine_adapter.shared import ( + InsertOverwriteStrategy, + SourceQuery, +) +from sqlmesh.core.engine_adapter.base import EngineAdapter +from sqlmesh.utils.errors import SQLMeshError +from sqlmesh.utils.connection_pool import ConnectionPool + +if t.TYPE_CHECKING: + from sqlmesh.core._typing import TableName + + +from sqlmesh.core.engine_adapter.mixins import LogicalMergeMixin + +logger = logging.getLogger(__name__) + + +class FabricEngineAdapter(LogicalMergeMixin, MSSQLEngineAdapter): + """ + Adapter for Microsoft Fabric. + """ + + DIALECT = "fabric" + SUPPORTS_INDEXES = False + SUPPORTS_TRANSACTIONS = False + SUPPORTS_CREATE_DROP_CATALOG = True + INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.DELETE_INSERT + + def __init__( + self, connection_factory_or_pool: t.Union[t.Callable, t.Any], *args: t.Any, **kwargs: t.Any + ) -> None: + # Wrap connection factory to support changing the catalog dynamically at runtime + if not isinstance(connection_factory_or_pool, ConnectionPool): + original_connection_factory = connection_factory_or_pool + + connection_factory_or_pool = lambda *args, **kwargs: original_connection_factory( + target_catalog=self._target_catalog, *args, **kwargs + ) + + super().__init__(connection_factory_or_pool, *args, **kwargs) + + @property + def _target_catalog(self) -> t.Optional[str]: + return self._connection_pool.get_attribute("target_catalog") + + @_target_catalog.setter + def _target_catalog(self, value: t.Optional[str]) -> None: + self._connection_pool.set_attribute("target_catalog", value) + + def _insert_overwrite_by_condition( + self, + table_name: TableName, + source_queries: t.List[SourceQuery], + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + where: t.Optional[exp.Condition] = None, + insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None, + **kwargs: t.Any, + ) -> None: + # Override to avoid MERGE statement which isn't fully supported in Fabric + return EngineAdapter._insert_overwrite_by_condition( + self, + table_name=table_name, + source_queries=source_queries, + target_columns_to_types=target_columns_to_types, + where=where, + insert_overwrite_strategy_override=InsertOverwriteStrategy.DELETE_INSERT, + **kwargs, + ) + + @property + def api_client(self) -> FabricHttpClient: + # the requests Session is not guaranteed to be threadsafe + # so we create a http client per thread on demand + if existing_client := self._connection_pool.get_attribute("api_client"): + return existing_client + + tenant_id: t.Optional[str] = self._extra_config.get("tenant_id") + workspace_id: t.Optional[str] = self._extra_config.get("workspace_id") + client_id: t.Optional[str] = self._extra_config.get("user") + client_secret: t.Optional[str] = self._extra_config.get("password") + + if not tenant_id or not client_id or not client_secret: + raise SQLMeshError( + "Service Principal authentication requires tenant_id, client_id, and client_secret " + "in the Fabric connection configuration" + ) + + if not workspace_id: + raise SQLMeshError( + "Fabric requires the workspace_id to be configured in the connection configuration to create / drop catalogs" + ) + + client = FabricHttpClient( + tenant_id=tenant_id, + workspace_id=workspace_id, + client_id=client_id, + client_secret=client_secret, + ) + + self._connection_pool.set_attribute("api_client", client) + return client + + def _create_catalog(self, catalog_name: exp.Identifier) -> None: + """Create a catalog (warehouse) in Microsoft Fabric via REST API.""" + warehouse_name = catalog_name.sql(dialect=self.dialect, identify=False) + logger.info(f"Creating Fabric warehouse: {warehouse_name}") + + self.api_client.create_warehouse(warehouse_name) + + def _drop_catalog(self, catalog_name: exp.Identifier) -> None: + """Drop a catalog (warehouse) in Microsoft Fabric via REST API.""" + warehouse_name = catalog_name.sql(dialect=self.dialect, identify=False) + + logger.info(f"Deleting Fabric warehouse: {warehouse_name}") + self.api_client.delete_warehouse(warehouse_name) + + def set_current_catalog(self, catalog_name: str) -> None: + """ + Set the current catalog for Microsoft Fabric connections. + + Override to handle Fabric's stateless session limitation where USE statements + don't persist across queries. Instead, we close existing connections and + recreate them with the new catalog in the connection configuration. + + Args: + catalog_name: The name of the catalog (warehouse) to switch to + + Note: + Fabric doesn't support catalog switching via USE statements because each + statement runs as an independent session. This method works around this + limitation by updating the connection pool with new catalog configuration. + + See: + https://learn.microsoft.com/en-us/fabric/data-warehouse/sql-query-editor#limitations + """ + current_catalog = self.get_current_catalog() + + # If already using the requested catalog, do nothing + if current_catalog and current_catalog == catalog_name: + logger.debug(f"Already using catalog '{catalog_name}', no action needed") + return + + logger.info(f"Switching from catalog '{current_catalog}' to '{catalog_name}'") + + # note: we call close() on the connection pool instead of self.close() because self.close() calls close_all() + # on the connection pool but we just want to close the connection for this thread + self._connection_pool.close() + self._target_catalog = catalog_name # new connections will use this catalog + + catalog_after_switch = self.get_current_catalog() + + if catalog_after_switch != catalog_name: + # We need to raise an error if the catalog switch failed to prevent the operation that needed the catalog switch from being run against the wrong catalog + raise SQLMeshError( + f"Unable to switch catalog to {catalog_name}, catalog ended up as {catalog_after_switch}" + ) + + +class FabricHttpClient: + def __init__(self, tenant_id: str, workspace_id: str, client_id: str, client_secret: str): + self.tenant_id = tenant_id + self.client_id = client_id + self.client_secret = client_secret + self.workspace_id = workspace_id + + def create_warehouse(self, warehouse_name: str) -> None: + """Create a catalog (warehouse) in Microsoft Fabric via REST API.""" + logger.info(f"Creating Fabric warehouse: {warehouse_name}") + + request_data = { + "displayName": warehouse_name, + "description": f"Warehouse created by SQLMesh: {warehouse_name}", + } + + response = self.session.post(self._endpoint_url("warehouses"), json=request_data) + response.raise_for_status() + + # Handle direct success (201) or async creation (202) + if response.status_code == 201: + logger.info(f"Successfully created Fabric warehouse: {warehouse_name}") + return + + if response.status_code == 202 and (location_header := response.headers.get("location")): + logger.info(f"Warehouse creation initiated for: {warehouse_name}") + self._wait_for_completion(location_header, warehouse_name) + logger.info(f"Successfully created Fabric warehouse: {warehouse_name}") + else: + logger.error(f"Unexpected response from Fabric API: {response}\n{response.text}") + raise SQLMeshError(f"Unable to create warehouse: {response}") + + def delete_warehouse(self, warehouse_name: str) -> None: + """Drop a catalog (warehouse) in Microsoft Fabric via REST API.""" + logger.info(f"Deleting Fabric warehouse: {warehouse_name}") + + # Get the warehouse ID by listing warehouses + response = self.session.get(self._endpoint_url("warehouses")) + response.raise_for_status() + + warehouse_name_to_id = { + warehouse.get("displayName"): warehouse.get("id") + for warehouse in response.json().get("value", []) + } + + warehouse_id = warehouse_name_to_id.get(warehouse_name, None) + + if not warehouse_id: + logger.error( + f"Fabric warehouse does not exist: {warehouse_name}\n(available warehouses: {', '.join(warehouse_name_to_id)})" + ) + raise SQLMeshError( + f"Unable to delete Fabric warehouse {warehouse_name} as it doesnt exist" + ) + + # Delete the warehouse by ID + response = self.session.delete(self._endpoint_url(f"warehouses/{warehouse_id}")) + response.raise_for_status() + + logger.info(f"Successfully deleted Fabric warehouse: {warehouse_name}") + + @cached_property + def session(self) -> requests.Session: + s = requests.Session() + + access_token = self._get_access_token() + s.headers.update({"Authorization": f"Bearer {access_token}"}) + + return s + + def _endpoint_url(self, endpoint: str) -> str: + if endpoint.startswith("/"): + endpoint = endpoint[1:] + + return f"https://api.fabric.microsoft.com/v1/workspaces/{self.workspace_id}/{endpoint}" + + def _get_access_token(self) -> str: + """Get access token using Service Principal authentication.""" + + # Use Azure AD OAuth2 token endpoint + token_url = f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token" + + data = { + "grant_type": "client_credentials", + "client_id": self.client_id, + "client_secret": self.client_secret, + "scope": "https://api.fabric.microsoft.com/.default", + } + + response = requests.post(token_url, data=data) + response.raise_for_status() + token_data = response.json() + return token_data["access_token"] + + def _wait_for_completion(self, location_url: str, operation_name: str) -> None: + """Poll the operation status until completion.""" + + @retry( + wait=wait_exponential(multiplier=1, min=1, max=30), + stop=stop_after_attempt(20), + retry=retry_if_result(lambda result: result not in ["Succeeded", "Failed"]), + ) + def _poll() -> str: + response = self.session.get(location_url) + response.raise_for_status() + + result = response.json() + status = result.get("status", "Unknown") + + logger.debug(f"Operation {operation_name} status: {status}") + + if status == "Failed": + error_msg = result.get("error", {}).get("message", "Unknown error") + raise SQLMeshError(f"Operation {operation_name} failed: {error_msg}") + elif status in ["InProgress", "Running"]: + logger.debug(f"Operation {operation_name} still in progress...") + elif status not in ["Succeeded"]: + logger.warning(f"Unknown status '{status}' for operation {operation_name}") + + return status + + final_status = _poll() + if final_status != "Succeeded": + raise SQLMeshError(f"Operation {operation_name} completed with status: {final_status}") diff --git a/tests/conftest.py b/tests/conftest.py index ad09deff6f..01fef852f7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -478,7 +478,7 @@ def _make_function( connection_mock.cursor.return_value = cursor_mock cursor_mock.connection.return_value = connection_mock adapter = klass( - lambda: connection_mock, + lambda *args, **kwargs: connection_mock, dialect=dialect or klass.DIALECT, register_comments=register_comments, default_catalog=default_catalog, diff --git a/tests/core/engine_adapter/integration/__init__.py b/tests/core/engine_adapter/integration/__init__.py index 50437338ae..8476d992eb 100644 --- a/tests/core/engine_adapter/integration/__init__.py +++ b/tests/core/engine_adapter/integration/__init__.py @@ -82,6 +82,7 @@ def pytest_marks(self) -> t.List[MarkDecorator]: IntegrationTestEngine("bigquery", native_dataframe_type="bigframe", cloud=True), IntegrationTestEngine("databricks", native_dataframe_type="pyspark", cloud=True), IntegrationTestEngine("snowflake", native_dataframe_type="snowpark", cloud=True), + IntegrationTestEngine("fabric", cloud=True), IntegrationTestEngine("gcp_postgres", cloud=True), ] @@ -680,6 +681,9 @@ def create_catalog(self, catalog_name: str): except Exception: pass self.engine_adapter.cursor.connection.autocommit(False) + elif self.dialect == "fabric": + # Use the engine adapter's built-in catalog creation functionality + self.engine_adapter.create_catalog(catalog_name) elif self.dialect == "snowflake": self.engine_adapter.execute(f'CREATE DATABASE IF NOT EXISTS "{catalog_name}"') elif self.dialect == "duckdb": @@ -696,6 +700,9 @@ def drop_catalog(self, catalog_name: str): return # bigquery cannot create/drop catalogs if self.dialect == "databricks": self.engine_adapter.execute(f"DROP CATALOG IF EXISTS {catalog_name} CASCADE") + elif self.dialect == "fabric": + # Use the engine adapter's built-in catalog dropping functionality + self.engine_adapter.drop_catalog(catalog_name) else: self.engine_adapter.execute(f'DROP DATABASE IF EXISTS "{catalog_name}"') diff --git a/tests/core/engine_adapter/integration/config.yaml b/tests/core/engine_adapter/integration/config.yaml index 4aee4640a3..51241889de 100644 --- a/tests/core/engine_adapter/integration/config.yaml +++ b/tests/core/engine_adapter/integration/config.yaml @@ -186,6 +186,21 @@ gateways: state_connection: type: duckdb + inttest_fabric: + connection: + type: fabric + driver: pyodbc + host: {{ env_var("FABRIC_HOST") }} + user: {{ env_var("FABRIC_CLIENT_ID") }} + password: {{ env_var("FABRIC_CLIENT_SECRET") }} + database: {{ env_var("FABRIC_DATABASE") }} + tenant_id: {{ env_var("FABRIC_TENANT_ID") }} + workspace_id: {{ env_var("FABRIC_WORKSPACE_ID") }} + odbc_properties: + Authentication: ActiveDirectoryServicePrincipal + state_connection: + type: duckdb + inttest_gcp_postgres: connection: type: gcp_postgres @@ -197,5 +212,6 @@ gateways: enable_iam_auth: true check_import: false + model_defaults: dialect: duckdb diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 1b7d54a2d9..521b287d12 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -2499,6 +2499,7 @@ def test_dialects(ctx: TestContext): { "default": pd.Timestamp("2020-01-01 00:00:00+00:00"), "clickhouse": pd.Timestamp("2020-01-01 00:00:00"), + "fabric": pd.Timestamp("2020-01-01 00:00:00"), "mysql": pd.Timestamp("2020-01-01 00:00:00"), "spark": pd.Timestamp("2020-01-01 00:00:00"), "databricks": pd.Timestamp("2020-01-01 00:00:00"), @@ -3033,14 +3034,12 @@ def test_value_normalization( input_data: t.Tuple[t.Any, ...], expected_results: t.Tuple[str, ...], ) -> None: - if ( - ctx.dialect == "trino" - and ctx.engine_adapter.current_catalog_type == "hive" - and column_type == exp.DataType.Type.TIMESTAMPTZ - ): - pytest.skip( - "Trino on Hive doesnt support creating tables with TIMESTAMP WITH TIME ZONE fields" - ) + # Skip TIMESTAMPTZ tests for engines that don't support it + if column_type == exp.DataType.Type.TIMESTAMPTZ: + if ctx.dialect == "trino" and ctx.engine_adapter.current_catalog_type == "hive": + pytest.skip("Trino on Hive doesn't support TIMESTAMP WITH TIME ZONE fields") + if ctx.dialect == "fabric": + pytest.skip("Fabric doesn't support TIMESTAMP WITH TIME ZONE fields") if not isinstance(ctx.engine_adapter, RowDiffMixin): pytest.skip( @@ -3130,7 +3129,10 @@ def test_table_diff_grain_check_single_key(ctx: TestContext): src_table = ctx.table("source") target_table = ctx.table("target") - columns_to_types = {"key1": exp.DataType.build("int"), "value": exp.DataType.build("varchar")} + columns_to_types = { + "key1": exp.DataType.build("int"), + "value": exp.DataType.build("varchar"), + } ctx.engine_adapter.create_table(src_table, columns_to_types) ctx.engine_adapter.create_table(target_table, columns_to_types) diff --git a/tests/core/engine_adapter/test_fabric.py b/tests/core/engine_adapter/test_fabric.py new file mode 100644 index 0000000000..6b80ef7337 --- /dev/null +++ b/tests/core/engine_adapter/test_fabric.py @@ -0,0 +1,90 @@ +# type: ignore + +import typing as t + +import pytest +from pytest_mock import MockerFixture +from sqlglot import exp, parse_one + +from sqlmesh.core.engine_adapter import FabricEngineAdapter +from tests.core.engine_adapter import to_sql_calls +from sqlmesh.core.engine_adapter.shared import DataObject + +pytestmark = [pytest.mark.engine, pytest.mark.fabric] + + +@pytest.fixture +def adapter(make_mocked_engine_adapter: t.Callable) -> FabricEngineAdapter: + return make_mocked_engine_adapter(FabricEngineAdapter) + + +def test_columns(adapter: FabricEngineAdapter): + adapter.cursor.fetchall.return_value = [ + ("decimal_ps", "decimal", None, 5, 4), + ("decimal", "decimal", None, 18, 0), + ("float", "float", None, 53, None), + ("char_n", "char", 10, None, None), + ("varchar_n", "varchar", 10, None, None), + ("nvarchar_max", "nvarchar", -1, None, None), + ] + + assert adapter.columns("db.table") == { + "decimal_ps": exp.DataType.build("decimal(5, 4)", dialect=adapter.dialect), + "decimal": exp.DataType.build("decimal(18, 0)", dialect=adapter.dialect), + "float": exp.DataType.build("float(53)", dialect=adapter.dialect), + "char_n": exp.DataType.build("char(10)", dialect=adapter.dialect), + "varchar_n": exp.DataType.build("varchar(10)", dialect=adapter.dialect), + "nvarchar_max": exp.DataType.build("nvarchar(max)", dialect=adapter.dialect), + } + + # Verify that the adapter queries the uppercase INFORMATION_SCHEMA + adapter.cursor.execute.assert_called_once_with( + """SELECT [COLUMN_NAME], [DATA_TYPE], [CHARACTER_MAXIMUM_LENGTH], [NUMERIC_PRECISION], [NUMERIC_SCALE] FROM [INFORMATION_SCHEMA].[COLUMNS] WHERE [TABLE_NAME] = 'table' AND [TABLE_SCHEMA] = 'db';""" + ) + + +def test_table_exists(adapter: FabricEngineAdapter): + adapter.cursor.fetchone.return_value = (1,) + assert adapter.table_exists("db.table") + # Verify that the adapter queries the uppercase INFORMATION_SCHEMA + adapter.cursor.execute.assert_called_once_with( + """SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE [TABLE_NAME] = 'table' AND [TABLE_SCHEMA] = 'db';""" + ) + + adapter.cursor.fetchone.return_value = None + assert not adapter.table_exists("db.table") + + +def test_insert_overwrite_by_time_partition(adapter: FabricEngineAdapter): + adapter.insert_overwrite_by_time_partition( + "test_table", + parse_one("SELECT a, b FROM tbl"), + start="2022-01-01", + end="2022-01-02", + time_column="b", + time_formatter=lambda x, _: exp.Literal.string(x.strftime("%Y-%m-%d")), + target_columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("STRING")}, + ) + + # Fabric adapter should use DELETE/INSERT strategy, not MERGE. + assert to_sql_calls(adapter) == [ + """DELETE FROM [test_table] WHERE [b] BETWEEN '2022-01-01' AND '2022-01-02';""", + """INSERT INTO [test_table] ([a], [b]) SELECT [a], [b] FROM (SELECT [a] AS [a], [b] AS [b] FROM [tbl]) AS [_subquery] WHERE [b] BETWEEN '2022-01-01' AND '2022-01-02';""", + ] + + +def test_replace_query(adapter: FabricEngineAdapter, mocker: MockerFixture): + mocker.patch.object( + adapter, + "_get_data_objects", + return_value=[DataObject(schema="", name="test_table", type="table")], + ) + adapter.replace_query( + "test_table", parse_one("SELECT a FROM tbl"), {"a": exp.DataType.build("int")} + ) + + # This behavior is inherited from MSSQLEngineAdapter and should be TRUNCATE + INSERT + assert to_sql_calls(adapter) == [ + "TRUNCATE TABLE [test_table];", + "INSERT INTO [test_table] ([a]) SELECT [a] FROM [tbl];", + ] diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 7fe2487891..22d21fcef7 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -12,6 +12,7 @@ ConnectionConfig, DatabricksConnectionConfig, DuckDBAttachOptions, + FabricConnectionConfig, DuckDBConnectionConfig, GCPPostgresConnectionConfig, MotherDuckConnectionConfig, @@ -1687,3 +1688,95 @@ def mock_add_output_converter(sql_type, converter_func): expected_dt = datetime(2023, 1, 1, 12, 0, 0, 0, timezone(timedelta(hours=-8, minutes=0))) assert result == expected_dt assert result.tzinfo == timezone(timedelta(hours=-8)) + + +def test_fabric_connection_config_defaults(make_config): + """Test Fabric connection config defaults to pyodbc and autocommit=True.""" + config = make_config( + type="fabric", + host="localhost", + workspace_id="test-workspace-id", + tenant_id="test-tenant-id", + check_import=False, + ) + assert isinstance(config, FabricConnectionConfig) + assert config.driver == "pyodbc" + assert config.autocommit is True + + # Ensure it creates the FabricEngineAdapter + from sqlmesh.core.engine_adapter.fabric import FabricEngineAdapter + + assert isinstance(config.create_engine_adapter(), FabricEngineAdapter) + + +def test_fabric_connection_config_parameter_validation(make_config): + """Test Fabric connection config parameter validation.""" + # Test that FabricConnectionConfig correctly handles pyodbc-specific parameters. + config = make_config( + type="fabric", + host="localhost", + driver_name="ODBC Driver 18 for SQL Server", + trust_server_certificate=True, + encrypt=False, + odbc_properties={"Authentication": "ActiveDirectoryServicePrincipal"}, + workspace_id="test-workspace-id", + tenant_id="test-tenant-id", + check_import=False, + ) + assert isinstance(config, FabricConnectionConfig) + assert config.driver == "pyodbc" # Driver is fixed to pyodbc + assert config.driver_name == "ODBC Driver 18 for SQL Server" + assert config.trust_server_certificate is True + assert config.encrypt is False + assert config.odbc_properties == {"Authentication": "ActiveDirectoryServicePrincipal"} + + # Test that specifying a different driver for Fabric raises an error + with pytest.raises(ConfigError, match=r"Input should be 'pyodbc'"): + make_config(type="fabric", host="localhost", driver="pymssql", check_import=False) + + +def test_fabric_pyodbc_connection_string_generation(): + """Test that the Fabric pyodbc connection gets invoked with the correct ODBC connection string.""" + with patch("pyodbc.connect") as mock_pyodbc_connect: + # Create a Fabric config + config = FabricConnectionConfig( + host="testserver.datawarehouse.fabric.microsoft.com", + port=1433, + database="testdb", + user="testuser", + password="testpass", + driver_name="ODBC Driver 18 for SQL Server", + trust_server_certificate=True, + encrypt=True, + login_timeout=30, + workspace_id="test-workspace-id", + tenant_id="test-tenant-id", + check_import=False, + ) + + # Get the connection factory with kwargs and call it + factory_with_kwargs = config._connection_factory_with_kwargs + connection = factory_with_kwargs() + + # Verify pyodbc.connect was called with the correct connection string + mock_pyodbc_connect.assert_called_once() + call_args = mock_pyodbc_connect.call_args + + # Check the connection string (first argument) + conn_str = call_args[0][0] + expected_parts = [ + "DRIVER={ODBC Driver 18 for SQL Server}", + "SERVER=testserver.datawarehouse.fabric.microsoft.com,1433", + "DATABASE=testdb", + "Encrypt=YES", + "TrustServerCertificate=YES", + "Connection Timeout=30", + "UID=testuser", + "PWD=testpass", + ] + + for part in expected_parts: + assert part in conn_str + + # Check autocommit parameter, should default to True for Fabric + assert call_args[1]["autocommit"] is True From acf7f7eeb7ba54ee10ab231cc765e0d88f548a3d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 19 Aug 2025 16:05:43 -0700 Subject: [PATCH 0733/1056] Feat(dbt): Add support for adapter.rename_relation (#5188) --- docs/integrations/dbt.md | 3 +-- sqlmesh/dbt/adapter.py | 14 +++++++++++++- tests/dbt/test_adapter.py | 13 +++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/integrations/dbt.md b/docs/integrations/dbt.md index a07e02bb23..e58bde7776 100644 --- a/docs/integrations/dbt.md +++ b/docs/integrations/dbt.md @@ -355,7 +355,7 @@ SQLMesh supports running dbt projects using the majority of dbt jinja methods, i | builtins | modules | source | | | config | print | statement | | -\* `adapter.rename_relation` and `adapter.expand_target_column_types` are not currently supported. +\* `adapter.expand_target_column_types` is not currently supported. ## Unsupported dbt jinja methods @@ -364,7 +364,6 @@ The dbt jinja methods that are not currently supported are: * debug * selected_sources * adapter.expand_target_column_types -* adapter.rename_relation * graph.nodes.values * graph.metrics.values diff --git a/sqlmesh/dbt/adapter.py b/sqlmesh/dbt/adapter.py index b524acc160..7de90a8ea5 100644 --- a/sqlmesh/dbt/adapter.py +++ b/sqlmesh/dbt/adapter.py @@ -15,7 +15,6 @@ if t.TYPE_CHECKING: import agate - import pandas as pd from dbt.adapters.base import BaseRelation from dbt.adapters.base.column import Column from dbt.adapters.base.impl import AdapterResponse @@ -86,6 +85,10 @@ def drop_schema(self, relation: BaseRelation) -> None: def drop_relation(self, relation: BaseRelation) -> None: """Drops a relation (table) in the target database.""" + @abc.abstractmethod + def rename_relation(self, from_relation: BaseRelation, to_relation: BaseRelation) -> None: + """Renames a relation (table) in the target database.""" + @abc.abstractmethod def execute( self, sql: str, auto_begin: bool = False, fetch: bool = False @@ -210,6 +213,9 @@ def drop_schema(self, relation: BaseRelation) -> None: def drop_relation(self, relation: BaseRelation) -> None: self._raise_parsetime_adapter_call_error("drop relation") + def rename_relation(self, from_relation: BaseRelation, to_relation: BaseRelation) -> None: + self._raise_parsetime_adapter_call_error("rename relation") + def execute( self, sql: str, auto_begin: bool = False, fetch: bool = False ) -> t.Tuple[AdapterResponse, agate.Table]: @@ -349,6 +355,12 @@ def drop_relation(self, relation: BaseRelation) -> None: if relation.schema is not None and relation.identifier is not None: self.engine_adapter.drop_table(self._normalize(self._relation_to_table(relation))) + def rename_relation(self, from_relation: BaseRelation, to_relation: BaseRelation) -> None: + old_table_name = self._normalize(self._relation_to_table(from_relation)) + new_table_name = self._normalize(self._relation_to_table(to_relation)) + + self.engine_adapter.rename_table(old_table_name, new_table_name) + def execute( self, sql: str, auto_begin: bool = False, fetch: bool = False ) -> t.Tuple[AdapterResponse, agate.Table]: diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index 944c4ce78d..5a41d237d3 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -81,6 +81,19 @@ def test_adapter_relation(sushi_test_project: Project, runtime_renderer: t.Calla == "[]" ) + renderer(""" + {%- set old_relation = adapter.get_relation( + database=None, + schema='foo', + identifier='bar') -%} + + {%- set backup_relation = api.Relation.create(schema='foo', identifier='bar__backup') -%} + + {% do adapter.rename_relation(old_relation, backup_relation) %} + """) + assert not engine_adapter.table_exists("foo.bar") + assert engine_adapter.table_exists("foo.bar__backup") + def test_bigquery_get_columns_in_relation( sushi_test_project: Project, From 31763bf76bf243af4c45925a9b575abec6cb6b97 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 20 Aug 2025 13:13:25 +1200 Subject: [PATCH 0734/1056] Chore(fabric): Enable integration tests (#5192) --- .circleci/continue_config.yml | 9 ++++++--- .circleci/install-prerequisites.sh | 11 ++++++++++- .circleci/manage-test-db.sh | 19 +++++++++++++++++++ docs/integrations/engines/fabric.md | 3 +++ .../engine_adapter/integration/config.yaml | 1 + 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 5fca61d7ed..8f8324a2a0 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -237,6 +237,9 @@ jobs: steps: - halt_unless_core - checkout + - run: + name: Install OS-level dependencies + command: ./.circleci/install-prerequisites.sh "<< parameters.engine >>" - run: name: Generate database name command: | @@ -247,6 +250,7 @@ jobs: echo "export DATABRICKS_CATALOG='$TEST_DB_NAME'" >> "$BASH_ENV" echo "export REDSHIFT_DATABASE='$TEST_DB_NAME'" >> "$BASH_ENV" echo "export GCP_POSTGRES_DATABASE='$TEST_DB_NAME'" >> "$BASH_ENV" + echo "export FABRIC_DATABASE='$TEST_DB_NAME'" >> "$BASH_ENV" - run: name: Create test database command: ./.circleci/manage-test-db.sh << parameters.engine >> "$TEST_DB_NAME" up @@ -303,9 +307,8 @@ workflows: - redshift - bigquery - clickhouse-cloud - - athena - # todo: enable fabric when cicd catalog create/drop implemented in manage-test-db.sh - #- fabric + - athena + - fabric - gcp-postgres filters: branches: diff --git a/.circleci/install-prerequisites.sh b/.circleci/install-prerequisites.sh index acd25ae02c..cbd8491535 100755 --- a/.circleci/install-prerequisites.sh +++ b/.circleci/install-prerequisites.sh @@ -17,12 +17,21 @@ ENGINE_DEPENDENCIES="" if [ "$ENGINE" == "spark" ]; then ENGINE_DEPENDENCIES="default-jdk" +elif [ "$ENGINE" == "fabric" ]; then + echo "Installing Microsoft package repository" + + # 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 + + ENGINE_DEPENDENCIES="msodbcsql18" fi ALL_DEPENDENCIES="$COMMON_DEPENDENCIES $ENGINE_DEPENDENCIES" echo "Installing OS-level dependencies: $ALL_DEPENDENCIES" -sudo apt-get clean && sudo apt-get -y update && sudo apt-get -y install $ALL_DEPENDENCIES +sudo apt-get clean && sudo apt-get -y update && sudo ACCEPT_EULA='Y' apt-get -y install $ALL_DEPENDENCIES echo "All done" \ No newline at end of file diff --git a/.circleci/manage-test-db.sh b/.circleci/manage-test-db.sh index 80ca075912..f44bd54845 100755 --- a/.circleci/manage-test-db.sh +++ b/.circleci/manage-test-db.sh @@ -133,7 +133,26 @@ gcp-postgres_down() { gcp-postgres_exec "drop database $1" } +# Fabric +fabric_init() { + python --version #note: as at 2025-08-20, ms-fabric-cli is pinned to Python >= 3.10, <3.13 + pip install ms-fabric-cli + + # to prevent the '[EncryptionFailed] An error occurred with the encrypted cache.' error + # ref: https://microsoft.github.io/fabric-cli/#switch-to-interactive-mode-optional + fab config set encryption_fallback_enabled true + echo "Logging in to Fabric" + fab auth login -u $FABRIC_CLIENT_ID -p $FABRIC_CLIENT_SECRET --tenant $FABRIC_TENANT_ID +} + +fabric_up() { + fab create "SQLMesh CircleCI.Workspace/$1.Warehouse" +} + +fabric_down() { + fab rm -f "SQLMesh CircleCI.Workspace/$1.Warehouse" || true +} INIT_FUNC="${ENGINE}_init" UP_FUNC="${ENGINE}_up" diff --git a/docs/integrations/engines/fabric.md b/docs/integrations/engines/fabric.md index eb00b5ac1d..90ac3234fc 100644 --- a/docs/integrations/engines/fabric.md +++ b/docs/integrations/engines/fabric.md @@ -1,5 +1,8 @@ # Fabric +!!! info + The Fabric engine adapter is a community contribution. Due to this, only limited community support is available. + ## Local/Built-in Scheduler **Engine Adapter Type**: `fabric` diff --git a/tests/core/engine_adapter/integration/config.yaml b/tests/core/engine_adapter/integration/config.yaml index 51241889de..b75efc762b 100644 --- a/tests/core/engine_adapter/integration/config.yaml +++ b/tests/core/engine_adapter/integration/config.yaml @@ -198,6 +198,7 @@ gateways: workspace_id: {{ env_var("FABRIC_WORKSPACE_ID") }} odbc_properties: Authentication: ActiveDirectoryServicePrincipal + check_import: false state_connection: type: duckdb From 014fe6ae850780fd206341c8cb4835b4d6276f4e Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 20 Aug 2025 11:19:04 -0700 Subject: [PATCH 0735/1056] Feat!: Create physical tables as part of evaluation (#5189) --- docs/cloud/tcloud_getting_started.md | 3 - docs/concepts/macros/macro_variables.md | 4 +- .../incremental_time_full_walkthrough.md | 18 +- docs/integrations/dlt.md | 4 - examples/multi/repo_1/models/a.sql | 3 +- examples/multi/repo_1/models/b.sql | 3 +- examples/multi/repo_2/models/c.sql | 3 +- examples/multi/repo_2/models/d.sql | 3 +- examples/multi/repo_2/models/e.sql | 3 +- examples/sushi/config.py | 28 +- sqlmesh/core/console.py | 4 +- sqlmesh/core/context_diff.py | 6 +- sqlmesh/core/engine_adapter/base.py | 9 +- sqlmesh/core/engine_adapter/base_postgres.py | 4 +- sqlmesh/core/plan/builder.py | 8 +- sqlmesh/core/plan/common.py | 7 + sqlmesh/core/plan/evaluator.py | 12 + sqlmesh/core/plan/stages.py | 61 +- sqlmesh/core/scheduler.py | 247 ++++-- sqlmesh/core/snapshot/definition.py | 21 +- sqlmesh/core/snapshot/evaluator.py | 791 ++++++++++-------- sqlmesh/dbt/loader.py | 6 +- tests/cli/test_cli.py | 5 - tests/cli/test_integration_cli.py | 5 - tests/conftest.py | 5 +- .../integration/test_integration.py | 7 + tests/core/engine_adapter/test_mssql.py | 1 + tests/core/test_context.py | 20 +- tests/core/test_integration.py | 171 ++-- tests/core/test_plan_evaluator.py | 10 +- tests/core/test_plan_stages.py | 102 ++- tests/core/test_scheduler.py | 241 ++++-- tests/core/test_snapshot.py | 1 + tests/core/test_snapshot_evaluator.py | 361 ++------ tests/dbt/test_config.py | 6 +- .../github/cicd/test_integration.py | 8 + tests/utils/test_helpers.py | 4 +- 37 files changed, 1232 insertions(+), 963 deletions(-) diff --git a/docs/cloud/tcloud_getting_started.md b/docs/cloud/tcloud_getting_started.md index dc0491814d..00ad8a3c25 100644 --- a/docs/cloud/tcloud_getting_started.md +++ b/docs/cloud/tcloud_getting_started.md @@ -268,9 +268,6 @@ Models needing backfill (missing dates): ├── sqlmesh_example.incremental_model: 2020-01-01 - 2024-11-24 └── sqlmesh_example.seed_model: 2024-11-24 - 2024-11-24 Apply - Backfill Tables [y/n]: y -Creating physical tables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 - -All model versions have been created successfully [1/1] sqlmesh_example.seed_model evaluated in 0.00s [1/1] sqlmesh_example.incremental_model evaluated in 0.01s diff --git a/docs/concepts/macros/macro_variables.md b/docs/concepts/macros/macro_variables.md index a184f7d99f..398117b3a9 100644 --- a/docs/concepts/macros/macro_variables.md +++ b/docs/concepts/macros/macro_variables.md @@ -130,8 +130,8 @@ SQLMesh provides additional predefined variables used to modify model behavior b * @runtime_stage - A string value denoting the current stage of the SQLMesh runtime. Typically used in models to conditionally execute pre/post-statements (learn more [here](../models/sql_models.md#optional-prepost-statements)). It returns one of these values: * 'loading' - The project is being loaded into SQLMesh's runtime context. - * 'creating' - The model tables are being created. - * 'evaluating' - The model query logic is being evaluated. + * 'creating' - The model tables are being created for the first time. The data may be inserted during table creation. + * 'evaluating' - The model query logic is evaluated, and the data is inserted into the existing model table. * 'promoting' - The model is being promoted in the target environment (view created during virtual layer update). * 'demoting' - The model is being demoted in the target environment (view dropped during virtual layer update). * 'auditing' - The audit is being run. diff --git a/docs/examples/incremental_time_full_walkthrough.md b/docs/examples/incremental_time_full_walkthrough.md index 6907836b0b..4e1d577d2c 100644 --- a/docs/examples/incremental_time_full_walkthrough.md +++ b/docs/examples/incremental_time_full_walkthrough.md @@ -304,10 +304,6 @@ Models needing backfill (missing dates): Enter the backfill start date (eg. '1 year', '2020-01-01') or blank to backfill from the beginning of history: Enter the backfill end date (eg. '1 month ago', '2020-01-01') or blank to backfill up until now: Apply - Backfill Tables [y/n]: y -Creating physical table ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1/1 • 0:00:07 - -All model versions have been created successfully - [1/1] demo__dev.incrementals_demo evaluated in 6.97s Evaluating models ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1/1 • 0:00:06 @@ -640,9 +636,10 @@ Models: ├── tcloud_raw_data.product_usage └── tcloud_raw_data.sales Apply - Virtual Update [y/n]: y -Creating physical tables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 15/15 • 0:00:03 -All model versions have been created successfully +SKIP: No physical layer updates to perform + +SKIP: No model batches to execute Virtually Updating 'prod' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 0:00:02 @@ -732,10 +729,6 @@ Models needing backfill (missing dates): Enter the preview start date (eg. '1 year', '2020-01-01') or blank to backfill to preview starting from yesterday: 2024-10-27 Enter the preview end date (eg. '1 month ago', '2020-01-01') or blank to preview up until '2024-11-08 00:00:00': Apply - Preview Tables [y/n]: y -Creating physical table ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1/1 • 0:00:16 - -All model versions have been created successfully - [1/1] demo__dev.incrementals_demo evaluated in 6.18s Evaluating models ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 1/1 • 0:00:06 @@ -1249,9 +1242,10 @@ Models: THEN 'Regular User' Directly Modified: demo.incrementals_demo (Forward-only) Apply - Virtual Update [y/n]: y -Creating physical tables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 15/15 • 0:00:02 -All model versions have been created successfully +SKIP: No physical layer updates to perform + +SKIP: No model batches to execute Virtually Updating 'prod' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 0:00:02 diff --git a/docs/integrations/dlt.md b/docs/integrations/dlt.md index d772a7bd2c..a53dc184ea 100644 --- a/docs/integrations/dlt.md +++ b/docs/integrations/dlt.md @@ -102,10 +102,6 @@ Models needing backfill (missing dates): ├── sushi_dataset_sqlmesh.incremental_sushi_types: 2024-10-03 - 2024-10-03 └── sushi_dataset_sqlmesh.incremental_waiters: 2024-10-03 - 2024-10-03 Apply - Backfill Tables [y/n]: y -Creating physical table ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 3/3 • 0:00:00 - -All model versions have been created successfully - [1/1] sushi_dataset_sqlmesh.incremental__dlt_loads evaluated in 0.01s [1/1] sushi_dataset_sqlmesh.incremental_sushi_types evaluated in 0.00s [1/1] sushi_dataset_sqlmesh.incremental_waiters evaluated in 0.01s diff --git a/examples/multi/repo_1/models/a.sql b/examples/multi/repo_1/models/a.sql index 838a676ea1..31ef81b2d7 100644 --- a/examples/multi/repo_1/models/a.sql +++ b/examples/multi/repo_1/models/a.sql @@ -1,5 +1,6 @@ MODEL ( - name bronze.a + name bronze.a, + kind FULL ); SELECT diff --git a/examples/multi/repo_1/models/b.sql b/examples/multi/repo_1/models/b.sql index b32897705e..b80918d6d5 100644 --- a/examples/multi/repo_1/models/b.sql +++ b/examples/multi/repo_1/models/b.sql @@ -1,5 +1,6 @@ MODEL ( - name bronze.b + name bronze.b, + kind FULL ); SELECT diff --git a/examples/multi/repo_2/models/c.sql b/examples/multi/repo_2/models/c.sql index 6a5c42619c..08551704f4 100644 --- a/examples/multi/repo_2/models/c.sql +++ b/examples/multi/repo_2/models/c.sql @@ -1,5 +1,6 @@ MODEL ( - name silver.c + name silver.c, + kind FULL ); SELECT DISTINCT col_a diff --git a/examples/multi/repo_2/models/d.sql b/examples/multi/repo_2/models/d.sql index 3647ab6965..6935763f59 100644 --- a/examples/multi/repo_2/models/d.sql +++ b/examples/multi/repo_2/models/d.sql @@ -1,5 +1,6 @@ MODEL ( - name silver.d + name silver.d, + kind FULL ); SELECT diff --git a/examples/multi/repo_2/models/e.sql b/examples/multi/repo_2/models/e.sql index 34d0793328..168dbc143d 100644 --- a/examples/multi/repo_2/models/e.sql +++ b/examples/multi/repo_2/models/e.sql @@ -1,5 +1,6 @@ MODEL ( - name silver.e + name silver.e, + kind FULL ); SELECT diff --git a/examples/sushi/config.py b/examples/sushi/config.py index 0bf15d2767..b985e24ec5 100644 --- a/examples/sushi/config.py +++ b/examples/sushi/config.py @@ -1,6 +1,6 @@ import os -from sqlmesh.core.config.common import VirtualEnvironmentMode +from sqlmesh.core.config.common import VirtualEnvironmentMode, TableNamingConvention from sqlmesh.core.config import ( AutoCategorizationMode, BigQueryConnectionConfig, @@ -27,6 +27,11 @@ defaults = {"dialect": "duckdb"} model_defaults = ModelDefaultsConfig(**defaults) model_defaults_iceberg = ModelDefaultsConfig(**defaults, storage_format="iceberg") +before_all = [ + "CREATE SCHEMA IF NOT EXISTS raw", + "DROP VIEW IF EXISTS raw.demographics", + "CREATE VIEW raw.demographics AS (SELECT 1 AS customer_id, '00000' AS zip)", +] # A DuckDB config, in-memory by default. @@ -52,6 +57,7 @@ "nomissingexternalmodels", ], ), + before_all=before_all, ) bigquery_config = Config( @@ -63,6 +69,7 @@ }, default_gateway="bq", model_defaults=model_defaults, + before_all=before_all, ) # A configuration used for SQLMesh tests. @@ -75,6 +82,7 @@ ) ), model_defaults=model_defaults, + before_all=before_all, ) # A configuration used for SQLMesh tests with virtual environment mode set to DEV_ONLY. @@ -84,7 +92,7 @@ "plan": PlanConfig( auto_categorize_changes=CategorizerConfig.all_full(), ), - } + }, ) # A DuckDB config with a physical schema map. @@ -92,6 +100,7 @@ default_connection=DuckDBConnectionConfig(), physical_schema_mapping={"^sushi$": "company_internal"}, model_defaults=model_defaults, + before_all=before_all, ) # A config representing isolated systems with a gateway per system @@ -103,6 +112,7 @@ }, default_gateway="dev", model_defaults=model_defaults, + before_all=before_all, ) required_approvers_config = Config( @@ -137,6 +147,7 @@ ), ], model_defaults=model_defaults, + before_all=before_all, ) @@ -144,12 +155,13 @@ default_connection=DuckDBConnectionConfig(), model_defaults=model_defaults, environment_suffix_target=EnvironmentSuffixTarget.TABLE, + before_all=before_all, ) environment_suffix_catalog_config = environment_suffix_table_config.model_copy( update={ "environment_suffix_target": EnvironmentSuffixTarget.CATALOG, - } + }, ) CATALOGS = { @@ -161,6 +173,7 @@ default_connection=DuckDBConnectionConfig(catalogs=CATALOGS), default_test_connection=DuckDBConnectionConfig(catalogs=CATALOGS), model_defaults=model_defaults, + before_all=before_all, ) environment_catalog_mapping_config = Config( @@ -177,4 +190,13 @@ "^prod$": "prod_catalog", ".*": "dev_catalog", }, + before_all=before_all, +) + +hash_md5_naming_config = config.copy( + update={"physical_table_naming_convention": TableNamingConvention.HASH_MD5} +) + +table_only_naming_config = config.copy( + update={"physical_table_naming_convention": TableNamingConvention.TABLE_ONLY} ) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index cf87fd7443..43283ead90 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -4074,8 +4074,8 @@ def _format_node_error(ex: NodeExecutionFailedError) -> str: node_name = "" if isinstance(error.node, SnapshotId): node_name = error.node.name - elif isinstance(error.node, tuple): - node_name = error.node[0] + elif hasattr(error.node, "snapshot_name"): + node_name = error.node.snapshot_name msg = _format_node_error(error) msg = " " + msg.replace("\n", "\n ") diff --git a/sqlmesh/core/context_diff.py b/sqlmesh/core/context_diff.py index ff19a3c7c6..12da39f50f 100644 --- a/sqlmesh/core/context_diff.py +++ b/sqlmesh/core/context_diff.py @@ -222,7 +222,9 @@ def create( infer_python_dependencies=infer_python_dependencies, ) - previous_environment_statements = state_reader.get_environment_statements(environment) + previous_environment_statements = ( + state_reader.get_environment_statements(env.name) if env else [] + ) if existing_env and always_recreate_environment: previous_plan_id: t.Optional[str] = existing_env.plan_id @@ -288,7 +290,7 @@ def create_no_diff(cls, environment: str, state_reader: StateReader) -> ContextD previous_finalized_snapshots=env.previous_finalized_snapshots, previous_requirements=env.requirements, requirements=env.requirements, - previous_environment_statements=[], + previous_environment_statements=environment_statements, environment_statements=environment_statements, previous_gateway_managed_virtual_layer=env.gateway_managed, gateway_managed_virtual_layer=env.gateway_managed, diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 94ffbe81d2..24ee99bba5 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -443,14 +443,20 @@ def replace_query( target_table=target_table, source_columns=source_columns, ) + if not target_columns_to_types and table_exists: + target_columns_to_types = self.columns(target_table) query = source_queries[0].query_factory() - target_columns_to_types = target_columns_to_types or self.columns(target_table) self_referencing = any( quote_identifiers(table) == quote_identifiers(target_table) for table in query.find_all(exp.Table) ) # If a query references itself then it must have a table created regardless of approach used. if self_referencing: + if not target_columns_to_types: + raise SQLMeshError( + f"Cannot create a self-referencing table {target_table.sql(dialect=self.dialect)} without knowing the column types. " + "Try casting the columns to an expected type or defining the columns in the model metadata. " + ) self._create_table_from_columns( target_table, target_columns_to_types, @@ -472,6 +478,7 @@ def replace_query( **kwargs, ) if self_referencing: + assert target_columns_to_types is not None with self.temp_table( self._select_columns(target_columns_to_types).from_(target_table), name=target_table, diff --git a/sqlmesh/core/engine_adapter/base_postgres.py b/sqlmesh/core/engine_adapter/base_postgres.py index cc394efd9e..aa46fba95a 100644 --- a/sqlmesh/core/engine_adapter/base_postgres.py +++ b/sqlmesh/core/engine_adapter/base_postgres.py @@ -53,7 +53,9 @@ def columns( self.execute(sql) resp = self.cursor.fetchall() if not resp: - raise SQLMeshError("Could not get columns for table '%s'. Table not found.", table_name) + raise SQLMeshError( + f"Could not get columns for table '{table.sql(dialect=self.dialect)}'. Table not found." + ) return { column_name: exp.DataType.build(data_type, dialect=self.dialect, udt=True) for column_name, data_type in resp diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 3bfff1aa12..9451f9eb53 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -16,7 +16,7 @@ ) from sqlmesh.core.context_diff import ContextDiff from sqlmesh.core.environment import EnvironmentNamingInfo -from sqlmesh.core.plan.common import should_force_rebuild +from sqlmesh.core.plan.common import should_force_rebuild, is_breaking_kind_change from sqlmesh.core.plan.definition import ( Plan, SnapshotMapping, @@ -597,7 +597,7 @@ def _categorize_snapshots( forward_only = self._forward_only or self._is_forward_only_change(s_id) if forward_only and s_id.name in self._context_diff.modified_snapshots: new, old = self._context_diff.modified_snapshots[s_id.name] - if should_force_rebuild(old, new) or snapshot.is_seed: + if is_breaking_kind_change(old, new) or snapshot.is_seed: # Breaking kind changes and seed changes can't be forward-only. forward_only = False @@ -622,7 +622,7 @@ def _categorize_snapshot( if self._context_diff.directly_modified(s_id.name): if self._auto_categorization_enabled: new, old = self._context_diff.modified_snapshots[s_id.name] - if should_force_rebuild(old, new): + if is_breaking_kind_change(old, new): snapshot.categorize_as(SnapshotChangeCategory.BREAKING, False) return @@ -774,7 +774,7 @@ def _is_forward_only_change(self, s_id: SnapshotId) -> bool: if snapshot.name in self._context_diff.modified_snapshots: _, old = self._context_diff.modified_snapshots[snapshot.name] # If the model kind has changed in a breaking way, then we can't consider this to be a forward-only change. - if snapshot.is_model and should_force_rebuild(old, snapshot): + if snapshot.is_model and is_breaking_kind_change(old, snapshot): return False return ( snapshot.is_model and snapshot.model.forward_only and bool(snapshot.previous_versions) diff --git a/sqlmesh/core/plan/common.py b/sqlmesh/core/plan/common.py index e6b7a4d10c..8d31b0ead3 100644 --- a/sqlmesh/core/plan/common.py +++ b/sqlmesh/core/plan/common.py @@ -4,6 +4,13 @@ def should_force_rebuild(old: Snapshot, new: Snapshot) -> bool: + if new.is_view and new.is_indirect_non_breaking and not new.is_forward_only: + # View models always need to be rebuilt to reflect updated upstream dependencies. + return True + return is_breaking_kind_change(old, new) + + +def is_breaking_kind_change(old: Snapshot, new: Snapshot) -> bool: if old.virtual_environment_mode != new.virtual_environment_mode: # If the virtual environment mode has changed, then we need to rebuild return True diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index ced4631b99..46142b7eeb 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -205,6 +205,16 @@ def visit_physical_layer_update_stage( success=completion_status is not None and completion_status.is_success ) + def visit_physical_layer_schema_creation_stage( + self, stage: stages.PhysicalLayerSchemaCreationStage, plan: EvaluatablePlan + ) -> None: + try: + self.snapshot_evaluator.create_physical_schemas( + stage.snapshots, stage.deployability_index + ) + except Exception as ex: + raise PlanError("Plan application failed.") from ex + def visit_backfill_stage(self, stage: stages.BackfillStage, plan: EvaluatablePlan) -> None: if plan.empty_backfill: intervals_to_add = [] @@ -243,6 +253,8 @@ def visit_backfill_stage(self, stage: stages.BackfillStage, plan: EvaluatablePla circuit_breaker=self._circuit_breaker, start=plan.start, end=plan.end, + allow_destructive_snapshots=plan.allow_destructive_models, + selected_snapshot_ids=stage.selected_snapshot_ids, ) if errors: raise PlanError("Plan application failed.") diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 871b540203..82223dd807 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -1,6 +1,7 @@ import typing as t from dataclasses import dataclass +from sqlmesh.core import constants as c from sqlmesh.core.environment import EnvironmentStatements, EnvironmentNamingInfo, Environment from sqlmesh.core.plan.common import should_force_rebuild from sqlmesh.core.plan.definition import EvaluatablePlan @@ -71,6 +72,19 @@ class PhysicalLayerUpdateStage: deployability_index: DeployabilityIndex +@dataclass +class PhysicalLayerSchemaCreationStage: + """Create the physical schemas for the given snapshots. + + Args: + snapshots: Snapshots to create physical schemas for. + deployability_index: Deployability index for this stage. + """ + + snapshots: t.List[Snapshot] + deployability_index: DeployabilityIndex + + @dataclass class AuditOnlyRunStage: """Run audits only for given snapshots. @@ -102,12 +116,14 @@ class BackfillStage: Args: snapshot_to_intervals: Intervals to backfill. This collection can be empty in which case no backfill is needed. This can be useful to report the lack of backfills back to the user. + selected_snapshot_ids: The snapshots to include in the run DAG. all_snapshots: All snapshots in the plan by name. deployability_index: Deployability index for this stage. before_promote: Whether this stage is before the promotion stage. """ snapshot_to_intervals: SnapshotToIntervals + selected_snapshot_ids: t.Set[SnapshotId] all_snapshots: t.Dict[str, Snapshot] deployability_index: DeployabilityIndex before_promote: bool = True @@ -185,6 +201,7 @@ class FinalizeEnvironmentStage: AfterAllStage, CreateSnapshotRecordsStage, PhysicalLayerUpdateStage, + PhysicalLayerSchemaCreationStage, AuditOnlyRunStage, RestatementStage, BackfillStage, @@ -236,7 +253,6 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: self._adjust_intervals(snapshots_by_name, plan, existing_environment) deployability_index = DeployabilityIndex.create(snapshots, start=plan.start) - deployability_index_for_creation = deployability_index if plan.is_dev: before_promote_snapshots = all_selected_for_backfill_snapshots after_promote_snapshots = set() @@ -283,11 +299,23 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: if plan.new_snapshots: stages.append(CreateSnapshotRecordsStage(snapshots=plan.new_snapshots)) - stages.append( - self._get_physical_layer_update_stage( - plan, snapshots, snapshots_to_intervals, deployability_index_for_creation + snapshots_to_create = self._get_snapshots_to_create(plan, snapshots) + if snapshots_to_create: + stages.append( + PhysicalLayerSchemaCreationStage( + snapshots=snapshots_to_create, deployability_index=deployability_index + ) + ) + if not needs_backfill: + stages.append( + self._get_physical_layer_update_stage( + plan, + snapshots_to_create, + snapshots, + snapshots_to_intervals, + deployability_index, + ) ) - ) audit_only_snapshots = self._get_audit_only_snapshots(new_snapshots) if audit_only_snapshots: @@ -301,6 +329,11 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: stages.append( BackfillStage( snapshot_to_intervals=missing_intervals_before_promote, + selected_snapshot_ids={ + s_id + for s_id in before_promote_snapshots + if plan.is_selected_for_backfill(s_id.name) + }, all_snapshots=snapshots_by_name, deployability_index=deployability_index, ) @@ -310,6 +343,7 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: stages.append( BackfillStage( snapshot_to_intervals={}, + selected_snapshot_ids=set(), all_snapshots=snapshots_by_name, deployability_index=deployability_index, ) @@ -326,7 +360,7 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: MigrateSchemasStage( snapshots=snapshots_with_schema_migration, all_snapshots=snapshots, - deployability_index=deployability_index_for_creation, + deployability_index=deployability_index, ) ) @@ -340,6 +374,11 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: stages.append( BackfillStage( snapshot_to_intervals=missing_intervals_after_promote, + selected_snapshot_ids={ + s_id + for s_id in after_promote_snapshots + if plan.is_selected_for_backfill(s_id.name) + }, all_snapshots=snapshots_by_name, deployability_index=deployability_index, ) @@ -418,13 +457,14 @@ def _get_restatement_stage( def _get_physical_layer_update_stage( self, plan: EvaluatablePlan, - snapshots: t.Dict[SnapshotId, Snapshot], + snapshots_to_create: t.List[Snapshot], + all_snapshots: t.Dict[SnapshotId, Snapshot], snapshots_to_intervals: SnapshotToIntervals, deployability_index: DeployabilityIndex, ) -> PhysicalLayerUpdateStage: return PhysicalLayerUpdateStage( - snapshots=self._get_snapshots_to_create(plan, snapshots), - all_snapshots=snapshots, + snapshots=snapshots_to_create, + all_snapshots=all_snapshots, snapshots_with_missing_intervals={ s.snapshot_id for s in snapshots_to_intervals @@ -589,6 +629,9 @@ def _adjust_intervals( # Make sure the intervals are up to date and restatements are reflected self.state_reader.refresh_snapshot_intervals(snapshots_by_name.values()) + if not existing_environment: + existing_environment = self.state_reader.get_environment(c.PROD) + if existing_environment: new_snapshot_ids = set() new_snapshot_versions = set() diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index 4582b24485..e787e57a23 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -1,4 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass +import abc import logging import typing as t import time @@ -39,7 +41,6 @@ from sqlmesh.utils.date import ( TimeLike, now_timestamp, - to_timestamp, validate_date_range, ) from sqlmesh.utils.errors import ( @@ -55,9 +56,43 @@ logger = logging.getLogger(__name__) SnapshotToIntervals = t.Dict[Snapshot, Intervals] -# we store snapshot name instead of snapshots/snapshotids because pydantic -# is extremely slow to hash. snapshot names should be unique within a dag run -SchedulingUnit = t.Tuple[str, t.Tuple[Interval, int]] + + +class SchedulingUnit(abc.ABC): + snapshot_name: str + + def __lt__(self, other: SchedulingUnit) -> bool: + return (self.__class__.__name__, self.snapshot_name) < ( + other.__class__.__name__, + other.snapshot_name, + ) + + +@dataclass(frozen=True) +class EvaluateNode(SchedulingUnit): + snapshot_name: str + interval: Interval + batch_index: int + + def __lt__(self, other: SchedulingUnit) -> bool: + if not isinstance(other, EvaluateNode): + return super().__lt__(other) + return (self.__class__.__name__, self.snapshot_name, self.interval, self.batch_index) < ( + other.__class__.__name__, + other.snapshot_name, + other.interval, + other.batch_index, + ) + + +@dataclass(frozen=True) +class CreateNode(SchedulingUnit): + snapshot_name: str + + +@dataclass(frozen=True) +class DummyNode(SchedulingUnit): + snapshot_name: str class Scheduler: @@ -161,6 +196,8 @@ def evaluate( deployability_index: DeployabilityIndex, batch_index: int, environment_naming_info: t.Optional[EnvironmentNamingInfo] = None, + allow_destructive_snapshots: t.Optional[t.Set[str]] = None, + target_table_exists: t.Optional[bool] = None, **kwargs: t.Any, ) -> t.List[AuditResult]: """Evaluate a snapshot and add the processed interval to the state sync. @@ -170,9 +207,11 @@ def evaluate( start: The start datetime to render. end: The end datetime to render. execution_time: The date/time time reference to use for execution time. Defaults to now. + allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed. deployability_index: Determines snapshots that are deployable in the context of this evaluation. batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it auto_restatement_enabled: Whether to enable auto restatements. + target_table_exists: Whether the target table exists. If None, the table will be checked for existence. kwargs: Additional kwargs to pass to the renderer. Returns: @@ -190,8 +229,10 @@ def evaluate( end=end, execution_time=execution_time, snapshots=snapshots, + allow_destructive_snapshots=allow_destructive_snapshots, deployability_index=deployability_index, batch_index=batch_index, + target_table_exists=target_table_exists, **kwargs, ) audit_results = self._audit_snapshot( @@ -289,8 +330,9 @@ def batch_intervals( merged_intervals: SnapshotToIntervals, deployability_index: t.Optional[DeployabilityIndex], environment_naming_info: EnvironmentNamingInfo, + dag: t.Optional[DAG[SnapshotId]] = None, ) -> t.Dict[Snapshot, Intervals]: - dag = snapshots_to_dag(merged_intervals) + dag = dag or snapshots_to_dag(merged_intervals) snapshot_intervals: t.Dict[SnapshotId, t.Tuple[Snapshot, t.List[Interval]]] = { snapshot.snapshot_id: ( @@ -369,6 +411,8 @@ def run_merged_intervals( circuit_breaker: t.Optional[t.Callable[[], bool]] = None, start: t.Optional[TimeLike] = None, end: t.Optional[TimeLike] = None, + allow_destructive_snapshots: t.Optional[t.Set[str]] = None, + selected_snapshot_ids: t.Optional[t.Set[SnapshotId]] = None, run_environment_statements: bool = False, audit_only: bool = False, ) -> t.Tuple[t.List[NodeExecutionFailedError[SchedulingUnit]], t.List[SchedulingUnit]]: @@ -382,14 +426,22 @@ def run_merged_intervals( circuit_breaker: An optional handler which checks if the run should be aborted. start: The start of the run. end: The end of the run. + allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed. + selected_snapshot_ids: The snapshots to include in the run DAG. If None, all snapshots with missing intervals will be included. Returns: A tuple of errors and skipped intervals. """ execution_time = execution_time or now_timestamp() + selected_snapshots = [self.snapshots[sid] for sid in (selected_snapshot_ids or set())] + if not selected_snapshots: + selected_snapshots = list(merged_intervals) + + snapshot_dag = snapshots_to_dag(selected_snapshots) + batched_intervals = self.batch_intervals( - merged_intervals, deployability_index, environment_naming_info + merged_intervals, deployability_index, environment_naming_info, dag=snapshot_dag ) self.console.start_evaluation_progress( @@ -399,7 +451,16 @@ def run_merged_intervals( audit_only=audit_only, ) - dag = self._dag(batched_intervals) + snapshots_to_create = { + s.snapshot_id + for s in self.snapshot_evaluator.get_snapshots_to_create( + selected_snapshots, deployability_index + ) + } + + dag = self._dag( + batched_intervals, snapshot_dag=snapshot_dag, snapshots_to_create=snapshots_to_create + ) if run_environment_statements: environment_statements = self.state_sync.get_environment_statements( @@ -417,70 +478,81 @@ def run_merged_intervals( execution_time=execution_time, ) - def evaluate_node(node: SchedulingUnit) -> None: + def run_node(node: SchedulingUnit) -> None: if circuit_breaker and circuit_breaker(): raise CircuitBreakerError() - - snapshot_name, ((start, end), batch_idx) = node - if batch_idx == -1: + if isinstance(node, DummyNode): return - snapshot = self.snapshots_by_name[snapshot_name] - self.console.start_snapshot_evaluation_progress(snapshot) - - execution_start_ts = now_timestamp() - evaluation_duration_ms: t.Optional[int] = None - - audit_results: t.List[AuditResult] = [] - try: - assert execution_time # mypy - assert deployability_index # mypy - - if audit_only: - audit_results = self._audit_snapshot( - snapshot=snapshot, - environment_naming_info=environment_naming_info, - deployability_index=deployability_index, - snapshots=self.snapshots_by_name, - start=start, - end=end, - execution_time=execution_time, - ) - else: - audit_results = self.evaluate( - snapshot=snapshot, - environment_naming_info=environment_naming_info, - start=start, - end=end, - execution_time=execution_time, - deployability_index=deployability_index, - batch_index=batch_idx, + snapshot = self.snapshots_by_name[node.snapshot_name] + + if isinstance(node, EvaluateNode): + self.console.start_snapshot_evaluation_progress(snapshot) + execution_start_ts = now_timestamp() + evaluation_duration_ms: t.Optional[int] = None + start, end = node.interval + + audit_results: t.List[AuditResult] = [] + try: + assert execution_time # mypy + assert deployability_index # mypy + + if audit_only: + audit_results = self._audit_snapshot( + snapshot=snapshot, + environment_naming_info=environment_naming_info, + deployability_index=deployability_index, + snapshots=self.snapshots_by_name, + start=start, + end=end, + execution_time=execution_time, + ) + else: + audit_results = self.evaluate( + snapshot=snapshot, + environment_naming_info=environment_naming_info, + start=start, + end=end, + execution_time=execution_time, + deployability_index=deployability_index, + batch_index=node.batch_index, + allow_destructive_snapshots=allow_destructive_snapshots, + target_table_exists=snapshot.snapshot_id not in snapshots_to_create, + ) + + evaluation_duration_ms = now_timestamp() - execution_start_ts + finally: + num_audits = len(audit_results) + num_audits_failed = sum(1 for result in audit_results if result.count) + self.console.update_snapshot_evaluation_progress( + snapshot, + batched_intervals[snapshot][node.batch_index], + node.batch_index, + evaluation_duration_ms, + num_audits - num_audits_failed, + num_audits_failed, ) - - evaluation_duration_ms = now_timestamp() - execution_start_ts - finally: - num_audits = len(audit_results) - num_audits_failed = sum(1 for result in audit_results if result.count) - self.console.update_snapshot_evaluation_progress( - snapshot, - batched_intervals[snapshot][batch_idx], - batch_idx, - evaluation_duration_ms, - num_audits - num_audits_failed, - num_audits_failed, + elif isinstance(node, CreateNode): + self.snapshot_evaluator.create_snapshot( + snapshot=snapshot, + snapshots=self.snapshots_by_name, + deployability_index=deployability_index, + allow_destructive_snapshots=allow_destructive_snapshots or set(), ) try: with self.snapshot_evaluator.concurrent_context(): errors, skipped_intervals = concurrent_apply_to_dag( dag, - evaluate_node, + run_node, self.max_workers, raise_on_error=False, ) self.console.stop_evaluation_progress(success=not errors) - skipped_snapshots = {i[0] for i in skipped_intervals} + skipped_snapshots = { + i.snapshot_name for i in skipped_intervals if isinstance(i, EvaluateNode) + } self.console.log_skipped_models(skipped_snapshots) for skipped in skipped_snapshots: logger.info(f"SKIPPED snapshot {skipped}\n") @@ -509,11 +581,18 @@ def evaluate_node(node: SchedulingUnit) -> None: self.state_sync.recycle() - def _dag(self, batches: SnapshotToIntervals) -> DAG[SchedulingUnit]: + def _dag( + self, + batches: SnapshotToIntervals, + snapshot_dag: t.Optional[DAG[SnapshotId]] = None, + snapshots_to_create: t.Optional[t.Set[SnapshotId]] = None, + ) -> DAG[SchedulingUnit]: """Builds a DAG of snapshot intervals to be evaluated. Args: batches: The batches of snapshots and intervals to evaluate. + snapshot_dag: The DAG of all snapshots. + snapshots_to_create: The snapshots with missing physical tables. Returns: A DAG of snapshot intervals to be evaluated. @@ -522,46 +601,72 @@ def _dag(self, batches: SnapshotToIntervals) -> DAG[SchedulingUnit]: intervals_per_snapshot = { snapshot.name: intervals for snapshot, intervals in batches.items() } + snapshots_to_create = snapshots_to_create or set() + original_snapshots_to_create = snapshots_to_create.copy() + snapshot_dag = snapshot_dag or snapshots_to_dag(batches) dag = DAG[SchedulingUnit]() - terminal_node = ((to_timestamp(0), to_timestamp(0)), -1) - for snapshot, intervals in batches.items(): - if not intervals: - continue + for snapshot_id in snapshot_dag: + snapshot = self.snapshots_by_name[snapshot_id.name] + intervals = intervals_per_snapshot.get(snapshot.name, []) - upstream_dependencies = [] + upstream_dependencies: t.List[SchedulingUnit] = [] for p_sid in snapshot.parents: if p_sid in self.snapshots: p_intervals = intervals_per_snapshot.get(p_sid.name, []) - if len(p_intervals) > 1: - upstream_dependencies.append((p_sid.name, terminal_node)) + if not p_intervals and p_sid in original_snapshots_to_create: + upstream_dependencies.append(CreateNode(snapshot_name=p_sid.name)) + elif len(p_intervals) > 1: + upstream_dependencies.append(DummyNode(snapshot_name=p_sid.name)) else: for i, interval in enumerate(p_intervals): - upstream_dependencies.append((p_sid.name, (interval, i))) + upstream_dependencies.append( + EvaluateNode( + snapshot_name=p_sid.name, interval=interval, batch_index=i + ) + ) batch_concurrency = snapshot.node.batch_concurrency + batch_size = snapshot.node.batch_size if snapshot.depends_on_past: batch_concurrency = 1 + create_node: t.Optional[CreateNode] = None + if snapshot.snapshot_id in original_snapshots_to_create and ( + snapshot.is_incremental_by_time_range + or ((not batch_concurrency or batch_concurrency > 1) and batch_size) + or not intervals + ): + # Add a separate node for table creation in case when there multiple concurrent + # evaluation nodes or when there are no intervals to evaluate. + create_node = CreateNode(snapshot_name=snapshot.name) + dag.add(create_node, upstream_dependencies) + snapshots_to_create.remove(snapshot.snapshot_id) + for i, interval in enumerate(intervals): - node = (snapshot.name, (interval, i)) - dag.add(node, upstream_dependencies) + node = EvaluateNode(snapshot_name=snapshot.name, interval=interval, batch_index=i) + + if create_node: + dag.add(node, [create_node]) + else: + dag.add(node, upstream_dependencies) if len(intervals) > 1: - dag.add((snapshot.name, terminal_node), [node]) + dag.add(DummyNode(snapshot_name=snapshot.name), [node]) if batch_concurrency and i >= batch_concurrency: batch_idx_to_wait_for = i - batch_concurrency dag.add( node, [ - ( - snapshot.name, - (intervals[batch_idx_to_wait_for], batch_idx_to_wait_for), - ) + EvaluateNode( + snapshot_name=snapshot.name, + interval=intervals[batch_idx_to_wait_for], + batch_index=batch_idx_to_wait_for, + ), ], ) return dag diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 996d539e60..ec5a883f7f 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1043,8 +1043,15 @@ def categorize_as(self, category: SnapshotChangeCategory, forward_only: bool = F # If the model has a pinned version then use that. self.version = self.model.physical_version elif is_no_rebuild and self.previous_version: + self.version = self.previous_version.data_version.version + elif self.is_model and self.model.forward_only and not self.previous_version: + # If this is a new model then use a deterministic version, independent of the fingerprint. + self.version = hash_data([self.name, *self.model.kind.data_hash_values]) + else: + self.version = self.fingerprint.to_version() + + if is_no_rebuild and self.previous_version: previous_version = self.previous_version - self.version = previous_version.data_version.version self.physical_schema_ = previous_version.physical_schema self.table_naming_convention = previous_version.table_naming_convention if self.is_materialized and (category.is_indirect_non_breaking or category.is_metadata): @@ -1054,11 +1061,6 @@ def categorize_as(self, category: SnapshotChangeCategory, forward_only: bool = F or previous_version.fingerprint.to_version() ) self.dev_table_suffix = previous_version.data_version.dev_table_suffix - elif self.is_model and self.model.forward_only and not self.previous_version: - # If this is a new model then use a deterministic version, independent of the fingerprint. - self.version = hash_data([self.name, *self.model.kind.data_hash_values]) - else: - self.version = self.fingerprint.to_version() self.change_category = category self.forward_only = forward_only @@ -1383,12 +1385,11 @@ def requires_schema_migration_in_prod(self) -> bool: return ( self.is_paused and self.is_model - and not self.is_symbolic + and self.is_materialized and ( (self.previous_version and self.previous_version.version == self.version) or self.model.forward_only or bool(self.model.physical_version) - or self.is_view or not self.virtual_environment_mode.is_full ) ) @@ -1588,7 +1589,9 @@ def create( # Similarly, if the model depends on past and the start date is not aligned with the # model's start, we should consider this snapshot non-deployable. this_deployable = False - if not snapshot.is_paused or snapshot.is_indirect_non_breaking: + if not snapshot.is_paused or ( + snapshot.is_indirect_non_breaking and snapshot.intervals + ): # This snapshot represents what's currently deployed in prod. representative_shared_version_ids.add(node) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index f2f7044ba7..1531997c1b 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -60,7 +60,6 @@ SnapshotInfoLike, SnapshotTableCleanupTask, ) -from sqlmesh.core.snapshot.definition import parent_snapshots_by_name from sqlmesh.utils import random_id, CorrelationId from sqlmesh.utils.concurrency import ( concurrent_apply_to_snapshots, @@ -138,8 +137,10 @@ def evaluate( end: TimeLike, execution_time: TimeLike, snapshots: t.Dict[str, Snapshot], + allow_destructive_snapshots: t.Optional[t.Set[str]] = None, deployability_index: t.Optional[DeployabilityIndex] = None, batch_index: int = 0, + target_table_exists: t.Optional[bool] = None, **kwargs: t.Any, ) -> t.Optional[str]: """Renders the snapshot's model, executes it and stores the result in the snapshot's physical table. @@ -150,21 +151,25 @@ def evaluate( end: The end datetime to render. execution_time: The date/time time reference to use for execution time. snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations. + allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed. deployability_index: Determines snapshots that are deployable in the context of this evaluation. batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it + target_table_exists: Whether the target table exists. If None, the table will be checked for existence. kwargs: Additional kwargs to pass to the renderer. Returns: The WAP ID of this evaluation if supported, None otherwise. """ result = self._evaluate_snapshot( - snapshot, - start, - end, - execution_time, - snapshots, + start=start, + end=end, + execution_time=execution_time, + snapshot=snapshot, + snapshots=snapshots, + allow_destructive_snapshots=allow_destructive_snapshots or set(), deployability_index=deployability_index, batch_index=batch_index, + target_table_exists=target_table_exists, **kwargs, ) if result is None or isinstance(result, str): @@ -200,21 +205,40 @@ def evaluate_and_fetch( Returns: The result of the evaluation as a dataframe. """ - result = self._evaluate_snapshot( + import pandas as pd + + adapter = self.get_adapter(snapshot.model.gateway) + render_kwargs = dict( + start=start, + end=end, + execution_time=execution_time, + snapshot=snapshot, + runtime_stage=RuntimeStage.EVALUATING, + **kwargs, + ) + queries_or_dfs = self._render_snapshot_for_evaluation( snapshot, - start, - end, - execution_time, snapshots, - limit=limit, - deployability_index=deployability_index, - **kwargs, + deployability_index or DeployabilityIndex.all_deployable(), + render_kwargs, ) - if result is None or isinstance(result, str): - raise SQLMeshError( - f"Unexpected result {result} when evaluating snapshot {snapshot.snapshot_id}." - ) - return result + query_or_df = next(queries_or_dfs) + if isinstance(query_or_df, pd.DataFrame): + return query_or_df.head(limit) + if not isinstance(query_or_df, exp.Expression): + # We assume that if this branch is reached, `query_or_df` is a pyspark / snowpark / bigframe dataframe, + # so we use `limit` instead of `head` to get back a dataframe instead of List[Row] + # https://spark.apache.org/docs/3.1.1/api/python/reference/api/pyspark.sql.DataFrame.head.html#pyspark.sql.DataFrame.head + return query_or_df.limit(limit) + + assert isinstance(query_or_df, exp.Query) + + existing_limit = query_or_df.args.get("limit") + if existing_limit: + limit = min(limit, execute(exp.select(existing_limit.expression)).rows[0][0]) + assert limit is not None + + return adapter._fetch_native_df(query_or_df.limit(limit)) def promote( self, @@ -254,7 +278,11 @@ def promote( for gateway, tables in tables_by_gateway.items(): if environment_naming_info.suffix_target.is_catalog: self._create_catalogs(tables=tables, gateway=gateway) - self._create_schemas(tables=tables, gateway=gateway) + + gateway_table_pairs = [ + (gateway, table) for gateway, tables in tables_by_gateway.items() for table in tables + ] + self._create_schemas(gateway_table_pairs=gateway_table_pairs) deployability_index = deployability_index or DeployabilityIndex.all_deployable() with self.concurrent_context(): @@ -324,32 +352,66 @@ def create( Returns: CompletionStatus: The status of the creation operation (success, failure, nothing to do). """ + deployability_index = deployability_index or DeployabilityIndex.all_deployable() + + snapshots_to_create = self.get_snapshots_to_create(target_snapshots, deployability_index) + if not snapshots_to_create: + return CompletionStatus.NOTHING_TO_DO + if on_start: + on_start(snapshots_to_create) + + self._create_snapshots( + snapshots_to_create=snapshots_to_create, + snapshots={s.name: s for s in snapshots.values()}, + deployability_index=deployability_index, + on_complete=on_complete, + allow_destructive_snapshots=allow_destructive_snapshots or set(), + ) + return CompletionStatus.SUCCESS + + def create_physical_schemas( + self, snapshots: t.Iterable[Snapshot], deployability_index: DeployabilityIndex + ) -> None: + """Creates the physical schemas for the given snapshots. + + Args: + snapshots: Snapshots to create physical schemas for. + deployability_index: Determines snapshots that are deployable in the context of this creation. + """ + tables_by_gateway: t.Dict[t.Optional[str], t.List[str]] = defaultdict(list) + for snapshot in snapshots: + if snapshot.is_model and not snapshot.is_symbolic: + tables_by_gateway[snapshot.model_gateway].append( + snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot)) + ) + + gateway_table_pairs = [ + (gateway, table) for gateway, tables in tables_by_gateway.items() for table in tables + ] + self._create_schemas(gateway_table_pairs=gateway_table_pairs) + + def get_snapshots_to_create( + self, target_snapshots: t.Iterable[Snapshot], deployability_index: DeployabilityIndex + ) -> t.List[Snapshot]: + """Returns a list of snapshots that need to have their physical tables created. + + Args: + target_snapshots: Target snapshots. + deployability_index: Determines snapshots that are deployable / representative in the context of this creation. + """ snapshots_with_table_names = defaultdict(set) tables_by_gateway_and_schema: t.Dict[t.Union[str, None], t.Dict[exp.Table, set[str]]] = ( defaultdict(lambda: defaultdict(set)) ) - table_deployability: t.Dict[str, bool] = {} - allow_destructive_snapshots = allow_destructive_snapshots or set() for snapshot in target_snapshots: if not snapshot.is_model or snapshot.is_symbolic: continue - deployability_flags = [True] - if ( - snapshot.is_no_rebuild - or snapshot.is_managed - or (snapshot.is_model and snapshot.model.forward_only) - or (deployability_index and not deployability_index.is_deployable(snapshot)) - ): - deployability_flags.append(False) - for is_deployable in deployability_flags: - table = exp.to_table( - snapshot.table_name(is_deployable), dialect=snapshot.model.dialect - ) - snapshots_with_table_names[snapshot].add(table.name) - table_deployability[table.name] = is_deployable - table_schema = d.schema_(table.db, catalog=table.catalog) - tables_by_gateway_and_schema[snapshot.model_gateway][table_schema].add(table.name) + is_deployable = deployability_index.is_deployable(snapshot) + table = exp.to_table(snapshot.table_name(is_deployable), dialect=snapshot.model.dialect) + snapshots_with_table_names[snapshot].add(table.name) + table_schema = d.schema_(table.db, catalog=table.catalog) + tables_by_gateway_and_schema[snapshot.model_gateway][table_schema].add(table.name) def _get_data_objects( schema: exp.Table, @@ -378,41 +440,18 @@ def _get_data_objects( existing_objects.update(objs_for_gateway) snapshots_to_create = [] - target_deployability_flags: t.Dict[str, t.List[bool]] = defaultdict(list) for snapshot, table_names in snapshots_with_table_names.items(): missing_tables = table_names - existing_objects if missing_tables or (snapshot.is_seed and not snapshot.intervals): snapshots_to_create.append(snapshot) - for table_name in missing_tables or table_names: - target_deployability_flags[snapshot.name].append( - table_deployability[table_name] - ) - target_deployability_flags[snapshot.name].sort() - - if not snapshots_to_create: - return CompletionStatus.NOTHING_TO_DO - if on_start: - on_start(snapshots_to_create) - for gateway, tables_by_schema in tables_by_gateway_and_schema.items(): - self._create_schemas(tables=tables_by_schema, gateway=gateway) - - self._create_snapshots( - snapshots_to_create=snapshots_to_create, - snapshots=snapshots, - target_deployability_flags=target_deployability_flags, - deployability_index=deployability_index, - on_complete=on_complete, - allow_destructive_snapshots=allow_destructive_snapshots, - ) - return CompletionStatus.SUCCESS + return snapshots_to_create def _create_snapshots( self, snapshots_to_create: t.Iterable[Snapshot], - snapshots: t.Dict[SnapshotId, Snapshot], - target_deployability_flags: t.Dict[str, t.List[bool]], - deployability_index: t.Optional[DeployabilityIndex], + snapshots: t.Dict[str, Snapshot], + deployability_index: DeployabilityIndex, on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]], allow_destructive_snapshots: t.Set[str], ) -> None: @@ -420,13 +459,12 @@ def _create_snapshots( with self.concurrent_context(): errors, skipped = concurrent_apply_to_snapshots( snapshots_to_create, - lambda s: self._create_snapshot( + lambda s: self.create_snapshot( s, snapshots=snapshots, - deployability_flags=target_deployability_flags[s.name], deployability_index=deployability_index, - on_complete=on_complete, allow_destructive_snapshots=allow_destructive_snapshots, + on_complete=on_complete, ), self.ddl_concurrent_tasks, raise_on_error=False, @@ -451,12 +489,13 @@ def migrate( """ allow_destructive_snapshots = allow_destructive_snapshots or set() deployability_index = deployability_index or DeployabilityIndex.all_deployable() + snapshots_by_name = {s.name: s for s in snapshots.values()} with self.concurrent_context(): concurrent_apply_to_snapshots( target_snapshots, lambda s: self._migrate_snapshot( s, - snapshots, + snapshots_by_name, allow_destructive_snapshots, self.get_adapter(s.model_gateway), deployability_index, @@ -612,18 +651,29 @@ def close(self) -> None: except Exception: logger.exception("Failed to close Snapshot Evaluator") + def set_correlation_id(self, correlation_id: CorrelationId) -> SnapshotEvaluator: + return SnapshotEvaluator( + { + gateway: adapter.with_settings(correlation_id=correlation_id) + for gateway, adapter in self.adapters.items() + }, + self.ddl_concurrent_tasks, + self.selected_gateway, + ) + def _evaluate_snapshot( self, - snapshot: Snapshot, start: TimeLike, end: TimeLike, execution_time: TimeLike, + snapshot: Snapshot, snapshots: t.Dict[str, Snapshot], - limit: t.Optional[int] = None, - deployability_index: t.Optional[DeployabilityIndex] = None, - batch_index: int = 0, + allow_destructive_snapshots: t.Set[str], + deployability_index: t.Optional[DeployabilityIndex], + batch_index: int, + target_table_exists: t.Optional[bool], **kwargs: t.Any, - ) -> DF | str | None: + ) -> t.Optional[str]: """Renders the snapshot's model and executes it. The return value depends on whether the limit was specified. Args: @@ -632,54 +682,206 @@ def _evaluate_snapshot( end: The end datetime to render. execution_time: The date/time time reference to use for execution time. snapshots: All upstream snapshots to use for expansion and mapping of physical locations. - limit: If limit is not None, the query will not be persisted but evaluated and returned as a dataframe. + allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed. deployability_index: Determines snapshots that are deployable in the context of this evaluation. batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it + target_table_exists: Whether the target table exists. If None, the table will be checked for existence. kwargs: Additional kwargs to pass to the renderer. """ - if not snapshot.is_model or snapshot.is_seed: + if not snapshot.is_model: return None model = snapshot.model logger.info("Evaluating snapshot %s", snapshot.snapshot_id) - deployability_index = deployability_index or DeployabilityIndex.all_deployable() - table_name = ( - "" - if limit is not None - else snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot)) - ) - adapter = self.get_adapter(model.gateway) - evaluation_strategy = _evaluation_strategy(snapshot, adapter) - + deployability_index = deployability_index or DeployabilityIndex.all_deployable() + is_snapshot_deployable = deployability_index.is_deployable(snapshot) + target_table_name = snapshot.table_name(is_deployable=is_snapshot_deployable) # https://github.com/TobikoData/sqlmesh/issues/2609 # If there are no existing intervals yet; only consider this a first insert for the first snapshot in the batch - is_first_insert = not _intervals(snapshot, deployability_index) and batch_index == 0 - - from sqlmesh.core.context import ExecutionContext - + if target_table_exists is None: + target_table_exists = adapter.table_exists(target_table_name) + is_first_insert = ( + not _intervals(snapshot, deployability_index) or not target_table_exists + ) and batch_index == 0 + + # Use the 'creating' stage if the table doesn't exist yet to preserve backwards compatibility with existing projects + # that depend on a separate physical table creation stage. + runtime_stage = RuntimeStage.EVALUATING if target_table_exists else RuntimeStage.CREATING common_render_kwargs = dict( start=start, end=end, execution_time=execution_time, snapshot=snapshot, - runtime_stage=RuntimeStage.EVALUATING, + runtime_stage=runtime_stage, **kwargs, ) - + create_render_kwargs = dict( + engine_adapter=adapter, + snapshots=snapshots, + deployability_index=deployability_index, + **common_render_kwargs, + ) + create_render_kwargs["runtime_stage"] = RuntimeStage.CREATING render_statements_kwargs = dict( engine_adapter=adapter, snapshots=snapshots, deployability_index=deployability_index, **common_render_kwargs, ) - rendered_physical_properties = snapshot.model.render_physical_properties( **render_statements_kwargs ) + with ( + adapter.transaction(), + adapter.session(snapshot.model.render_session_properties(**render_statements_kwargs)), + ): + adapter.execute(model.render_pre_statements(**render_statements_kwargs)) + + if not target_table_exists or (model.is_seed and not snapshot.intervals): + if self._can_clone(snapshot, deployability_index): + self._clone_snapshot_in_dev( + snapshot=snapshot, + snapshots=snapshots, + deployability_index=deployability_index, + render_kwargs=create_render_kwargs, + rendered_physical_properties=rendered_physical_properties, + allow_destructive_snapshots=allow_destructive_snapshots, + ) + common_render_kwargs["runtime_stage"] = RuntimeStage.EVALUATING + elif model.annotated or model.is_seed or model.kind.is_scd_type_2: + self._execute_create( + snapshot=snapshot, + table_name=target_table_name, + is_table_deployable=is_snapshot_deployable, + deployability_index=deployability_index, + create_render_kwargs=create_render_kwargs, + rendered_physical_properties=rendered_physical_properties, + dry_run=False, + run_pre_post_statements=False, + ) + common_render_kwargs["runtime_stage"] = RuntimeStage.EVALUATING + + wap_id: t.Optional[str] = None + if snapshot.is_materialized and ( + model.wap_supported or adapter.wap_supported(target_table_name) + ): + wap_id = random_id()[0:8] + logger.info("Using WAP ID '%s' for snapshot %s", wap_id, snapshot.snapshot_id) + target_table_name = adapter.wap_prepare(target_table_name, wap_id) + + self._render_and_insert_snapshot( + start=start, + end=end, + execution_time=execution_time, + snapshot=snapshot, + snapshots=snapshots, + render_kwargs=common_render_kwargs, + rendered_physical_properties=rendered_physical_properties, + deployability_index=deployability_index, + is_first_insert=is_first_insert, + batch_index=batch_index, + ) + + adapter.execute(model.render_post_statements(**render_statements_kwargs)) + + return wap_id + + def create_snapshot( + self, + snapshot: Snapshot, + snapshots: t.Dict[str, Snapshot], + deployability_index: DeployabilityIndex, + allow_destructive_snapshots: t.Set[str], + on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None, + ) -> None: + """Creates a physical table for the given snapshot. + + Args: + snapshot: Snapshot to create. + snapshots: All upstream snapshots to use for expansion and mapping of physical locations. + deployability_index: Determines snapshots that are deployable in the context of this creation. + on_complete: A callback to call on each successfully created database object. + allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed. + """ + if not snapshot.is_model: + return + + logger.info("Creating a physical table for snapshot %s", snapshot.snapshot_id) + + adapter = self.get_adapter(snapshot.model.gateway) + create_render_kwargs: t.Dict[str, t.Any] = dict( + engine_adapter=adapter, + snapshots=snapshots, + runtime_stage=RuntimeStage.CREATING, + deployability_index=deployability_index, + ) + + with ( + adapter.transaction(), + adapter.session(snapshot.model.render_session_properties(**create_render_kwargs)), + ): + rendered_physical_properties = snapshot.model.render_physical_properties( + **create_render_kwargs + ) + + if self._can_clone(snapshot, deployability_index): + self._clone_snapshot_in_dev( + snapshot=snapshot, + snapshots=snapshots, + deployability_index=deployability_index, + render_kwargs=create_render_kwargs, + rendered_physical_properties=rendered_physical_properties, + allow_destructive_snapshots=allow_destructive_snapshots, + ) + else: + is_table_deployable = deployability_index.is_deployable(snapshot) + self._execute_create( + snapshot=snapshot, + table_name=snapshot.table_name(is_deployable=is_table_deployable), + is_table_deployable=is_table_deployable, + deployability_index=deployability_index, + create_render_kwargs=create_render_kwargs, + rendered_physical_properties=rendered_physical_properties, + dry_run=True, + ) + + if on_complete is not None: + on_complete(snapshot) + + def _render_and_insert_snapshot( + self, + start: TimeLike, + end: TimeLike, + execution_time: TimeLike, + snapshot: Snapshot, + snapshots: t.Dict[str, Snapshot], + render_kwargs: t.Dict[str, t.Any], + rendered_physical_properties: t.Dict[str, exp.Expression], + deployability_index: DeployabilityIndex, + is_first_insert: bool, + batch_index: int, + ) -> None: + if not snapshot.is_model or snapshot.is_seed: + return + + logger.info("Inserting data for snapshot %s", snapshot.snapshot_id) + + model = snapshot.model + table_name = snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot)) + adapter = self.get_adapter(model.gateway) + evaluation_strategy = _evaluation_strategy(snapshot, adapter) + + queries_or_dfs = self._render_snapshot_for_evaluation( + snapshot, + snapshots, + deployability_index, + render_kwargs, + ) + def apply(query_or_df: QueryOrDF, index: int = 0) -> None: if index > 0: evaluation_strategy.append( @@ -694,7 +896,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: end=end, execution_time=execution_time, physical_properties=rendered_physical_properties, - render_kwargs=render_statements_kwargs, + render_kwargs=render_kwargs, ) else: logger.info( @@ -716,203 +918,101 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: end=end, execution_time=execution_time, physical_properties=rendered_physical_properties, - render_kwargs=render_statements_kwargs, + render_kwargs=render_kwargs, ) - with ( - adapter.transaction(), - adapter.session(snapshot.model.render_session_properties(**render_statements_kwargs)), + # DataFrames, unlike SQL expressions, can provide partial results by yielding dataframes. As a result, + # if the engine supports INSERT OVERWRITE or REPLACE WHERE and the snapshot is incremental by time range, we risk + # having a partial result since each dataframe write can re-truncate partitions. To avoid this, we + # union all the dataframes together before writing. For pandas this could result in OOM and a potential + # workaround for that would be to serialize pandas to disk and then read it back with Spark. + # Note: We assume that if multiple things are yielded from `queries_or_dfs` that they are dataframes + # and not SQL expressions. + if ( + adapter.INSERT_OVERWRITE_STRATEGY + in ( + InsertOverwriteStrategy.INSERT_OVERWRITE, + InsertOverwriteStrategy.REPLACE_WHERE, + ) + and snapshot.is_incremental_by_time_range ): - wap_id: t.Optional[str] = None - if ( - table_name - and snapshot.is_materialized - and (model.wap_supported or adapter.wap_supported(table_name)) - ): - wap_id = random_id()[0:8] - logger.info("Using WAP ID '%s' for snapshot %s", wap_id, snapshot.snapshot_id) - table_name = adapter.wap_prepare(table_name, wap_id) - - if limit is None: - adapter.execute(model.render_pre_statements(**render_statements_kwargs)) - - queries_or_dfs = model.render( - context=ExecutionContext( - adapter, - snapshots, - deployability_index, - default_dialect=model.dialect, - default_catalog=model.default_catalog, - ), - **common_render_kwargs, + import pandas as pd + + query_or_df = reduce( + lambda a, b: ( + pd.concat([a, b], ignore_index=True) # type: ignore + if isinstance(a, pd.DataFrame) + else a.union_all(b) # type: ignore + ), # type: ignore + queries_or_dfs, ) + apply(query_or_df, index=0) + else: + for index, query_or_df in enumerate(queries_or_dfs): + apply(query_or_df, index) - if limit is not None: - import pandas as pd - - query_or_df = next(queries_or_dfs) - if isinstance(query_or_df, pd.DataFrame): - return query_or_df.head(limit) - if not isinstance(query_or_df, exp.Expression): - # We assume that if this branch is reached, `query_or_df` is a pyspark / snowpark / bigframe dataframe, - # so we use `limit` instead of `head` to get back a dataframe instead of List[Row] - # https://spark.apache.org/docs/3.1.1/api/python/reference/api/pyspark.sql.DataFrame.head.html#pyspark.sql.DataFrame.head - return query_or_df.limit(limit) - - assert isinstance(query_or_df, exp.Query) - - existing_limit = query_or_df.args.get("limit") - if existing_limit: - limit = min(limit, execute(exp.select(existing_limit.expression)).rows[0][0]) - assert limit is not None - - return adapter._fetch_native_df(query_or_df.limit(limit)) - - # DataFrames, unlike SQL expressions, can provide partial results by yielding dataframes. As a result, - # if the engine supports INSERT OVERWRITE or REPLACE WHERE and the snapshot is incremental by time range, we risk - # having a partial result since each dataframe write can re-truncate partitions. To avoid this, we - # union all the dataframes together before writing. For pandas this could result in OOM and a potential - # workaround for that would be to serialize pandas to disk and then read it back with Spark. - # Note: We assume that if multiple things are yielded from `queries_or_dfs` that they are dataframes - # and not SQL expressions. - if ( - adapter.INSERT_OVERWRITE_STRATEGY - in ( - InsertOverwriteStrategy.INSERT_OVERWRITE, - InsertOverwriteStrategy.REPLACE_WHERE, - ) - and snapshot.is_incremental_by_time_range - ): - import pandas as pd - - query_or_df = reduce( - lambda a, b: ( - pd.concat([a, b], ignore_index=True) # type: ignore - if isinstance(a, pd.DataFrame) - else a.union_all(b) # type: ignore - ), # type: ignore - queries_or_dfs, - ) - apply(query_or_df, index=0) - else: - for index, query_or_df in enumerate(queries_or_dfs): - apply(query_or_df, index) + def _render_snapshot_for_evaluation( + self, + snapshot: Snapshot, + snapshots: t.Dict[str, Snapshot], + deployability_index: DeployabilityIndex, + render_kwargs: t.Dict[str, t.Any], + ) -> t.Iterator[QueryOrDF]: + from sqlmesh.core.context import ExecutionContext - if limit is None: - adapter.execute(model.render_post_statements(**render_statements_kwargs)) + model = snapshot.model + adapter = self.get_adapter(model.gateway) - return wap_id + return model.render( + context=ExecutionContext( + adapter, + snapshots, + deployability_index, + default_dialect=model.dialect, + default_catalog=model.default_catalog, + ), + **render_kwargs, + ) - def _create_snapshot( + def _clone_snapshot_in_dev( self, snapshot: Snapshot, - snapshots: t.Dict[SnapshotId, Snapshot], - deployability_flags: t.List[bool], - deployability_index: t.Optional[DeployabilityIndex], - on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]], + snapshots: t.Dict[str, Snapshot], + deployability_index: DeployabilityIndex, + render_kwargs: t.Dict[str, t.Any], + rendered_physical_properties: t.Dict[str, exp.Expression], allow_destructive_snapshots: t.Set[str], ) -> None: - if not snapshot.is_model: - return - - deployability_index = deployability_index or DeployabilityIndex.all_deployable() - adapter = self.get_adapter(snapshot.model.gateway) - create_render_kwargs: t.Dict[str, t.Any] = dict( - engine_adapter=adapter, - snapshots=parent_snapshots_by_name(snapshot, snapshots), - runtime_stage=RuntimeStage.CREATING, - deployability_index=deployability_index, - ) - - with ( - adapter.transaction(), - adapter.session(snapshot.model.render_session_properties(**create_render_kwargs)), - ): - rendered_physical_properties = snapshot.model.render_physical_properties( - **create_render_kwargs - ) - - if ( - snapshot.is_forward_only - and snapshot.is_materialized - and snapshot.previous_versions - and adapter.SUPPORTS_CLONING - # managed models cannot have their schema mutated because theyre based on queries, so clone + alter wont work - and not snapshot.is_managed - # If the deployable table is missing we can't clone it - and True not in deployability_flags - ): - target_table_name = snapshot.table_name(is_deployable=False) - tmp_table_name = f"{target_table_name}__schema_migration_source" - source_table_name = snapshot.table_name() - - logger.info(f"Cloning table '{source_table_name}' into '{target_table_name}'") - self._execute_create( - snapshot=snapshot, - table_name=tmp_table_name, - is_table_deployable=False, - deployability_index=deployability_index, - create_render_kwargs=create_render_kwargs, - rendered_physical_properties=rendered_physical_properties, - dry_run=True, - ) + target_table_name = snapshot.table_name(is_deployable=False) + source_table_name = snapshot.table_name() - try: - adapter.clone_table( - target_table_name, - snapshot.table_name(), - replace=True, - rendered_physical_properties=rendered_physical_properties, - ) - alter_expressions = adapter.get_alter_expressions( - target_table_name, - tmp_table_name, - ignore_destructive=snapshot.model.on_destructive_change.is_ignore, - ) - _check_destructive_schema_change( - snapshot, alter_expressions, allow_destructive_snapshots - ) - adapter.alter_table(alter_expressions) - except Exception: - adapter.drop_table(target_table_name) - raise - finally: - adapter.drop_table(tmp_table_name) - else: - dry_run = len(deployability_flags) == 1 - for is_table_deployable in deployability_flags: - if ( - is_table_deployable - and snapshot.model.forward_only - and not deployability_index.is_representative(snapshot) - ): - logger.info( - "Skipping creation of the deployable table '%s' for the forward-only model %s. " - "The table will be created when the snapshot is deployed to production", - snapshot.table_name(is_deployable=is_table_deployable), - snapshot.snapshot_id, - ) - continue - - self._execute_create( - snapshot=snapshot, - table_name=snapshot.table_name(is_deployable=is_table_deployable), - is_table_deployable=is_table_deployable, - deployability_index=deployability_index, - create_render_kwargs=create_render_kwargs, - rendered_physical_properties=rendered_physical_properties, - dry_run=dry_run, - ) - - if on_complete is not None: - on_complete(snapshot) + try: + logger.info(f"Cloning table '{source_table_name}' into '{target_table_name}'") + adapter.clone_table( + target_table_name, + snapshot.table_name(), + replace=True, + rendered_physical_properties=rendered_physical_properties, + ) + self._migrate_target_table( + target_table_name=target_table_name, + snapshot=snapshot, + snapshots=snapshots, + deployability_index=deployability_index, + render_kwargs=render_kwargs, + rendered_physical_properties=rendered_physical_properties, + allow_destructive_snapshots=allow_destructive_snapshots, + ) + except Exception: + adapter.drop_table(target_table_name) + raise def _migrate_snapshot( self, snapshot: Snapshot, - snapshots: t.Dict[SnapshotId, Snapshot], + snapshots: t.Dict[str, Snapshot], allow_destructive_snapshots: t.Set[str], adapter: EngineAdapter, deployability_index: DeployabilityIndex, @@ -923,7 +1023,7 @@ def _migrate_snapshot( deployability_index = DeployabilityIndex.all_deployable() render_kwargs: t.Dict[str, t.Any] = dict( engine_adapter=adapter, - snapshots=parent_snapshots_by_name(snapshot, snapshots), + snapshots=snapshots, runtime_stage=RuntimeStage.CREATING, deployability_index=deployability_index, ) @@ -941,39 +1041,63 @@ def _migrate_snapshot( table_exists = False if table_exists: - evaluation_strategy = _evaluation_strategy(snapshot, adapter) - tmp_table_name = snapshot.table_name(is_deployable=False) - logger.info( - "Migrating table schema '%s' to match '%s'", - target_table_name, - tmp_table_name, - ) - evaluation_strategy.migrate( + self._migrate_target_table( target_table_name=target_table_name, - source_table_name=tmp_table_name, snapshot=snapshot, - snapshots=parent_snapshots_by_name(snapshot, snapshots), - allow_destructive_snapshots=allow_destructive_snapshots, - ignore_destructive=snapshot.model.on_destructive_change.is_ignore, - ) - else: - logger.info( - "Creating table '%s' for the snapshot of the forward-only model %s", - target_table_name, - snapshot.snapshot_id, - ) - self._execute_create( - snapshot=snapshot, - table_name=target_table_name, - is_table_deployable=True, + snapshots=snapshots, deployability_index=deployability_index, - create_render_kwargs=render_kwargs, + render_kwargs=render_kwargs, rendered_physical_properties=snapshot.model.render_physical_properties( **render_kwargs ), - dry_run=False, + allow_destructive_snapshots=allow_destructive_snapshots, + run_pre_post_statements=True, ) + def _migrate_target_table( + self, + target_table_name: str, + snapshot: Snapshot, + snapshots: t.Dict[str, Snapshot], + deployability_index: DeployabilityIndex, + render_kwargs: t.Dict[str, t.Any], + rendered_physical_properties: t.Dict[str, exp.Expression], + allow_destructive_snapshots: t.Set[str], + run_pre_post_statements: bool = False, + ) -> None: + adapter = self.get_adapter(snapshot.model.gateway) + + tmp_table_name = f"{target_table_name}_schema_tmp" + if snapshot.is_materialized: + self._execute_create( + snapshot=snapshot, + table_name=tmp_table_name, + is_table_deployable=False, + deployability_index=deployability_index, + create_render_kwargs=render_kwargs, + rendered_physical_properties=rendered_physical_properties, + dry_run=False, + run_pre_post_statements=run_pre_post_statements, + ) + try: + evaluation_strategy = _evaluation_strategy(snapshot, adapter) + logger.info( + "Migrating table schema from '%s' to '%s'", + tmp_table_name, + target_table_name, + ) + evaluation_strategy.migrate( + target_table_name=target_table_name, + source_table_name=tmp_table_name, + snapshot=snapshot, + snapshots=snapshots, + allow_destructive_snapshots=allow_destructive_snapshots, + ignore_destructive=snapshot.model.on_destructive_change.is_ignore, + ) + finally: + if snapshot.is_materialized: + adapter.drop_table(tmp_table_name) + def _promote_snapshot( self, snapshot: Snapshot, @@ -1182,19 +1306,30 @@ def _create_catalogs( def _create_schemas( self, - tables: t.Iterable[t.Union[exp.Table, str]], - gateway: t.Optional[str] = None, + gateway_table_pairs: t.Iterable[t.Tuple[t.Optional[str], t.Union[exp.Table, str]]], ) -> None: - table_exprs = [exp.to_table(t) for t in tables] - unique_schemas = {(t.args["db"], t.args.get("catalog")) for t in table_exprs if t and t.db} - # Create schemas sequentially, since some engines (eg. Postgres) may not support concurrent creation - # of schemas with the same name. - for schema_name, catalog in unique_schemas: + table_exprs = [(gateway, exp.to_table(t)) for gateway, t in gateway_table_pairs] + unique_schemas = { + (gateway, t.args["db"], t.args.get("catalog")) + for gateway, t in table_exprs + if t and t.db + } + + def _create_schema( + gateway: t.Optional[str], schema_name: str, catalog: t.Optional[str] + ) -> None: schema = schema_(schema_name, catalog) logger.info("Creating schema '%s'", schema) adapter = self.get_adapter(gateway) adapter.create_schema(schema) + with self.concurrent_context(): + concurrent_apply_to_values( + list(unique_schemas), + lambda item: _create_schema(item[0], item[1], item[2]), + self.ddl_concurrent_tasks, + ) + def get_adapter(self, gateway: t.Optional[str] = None) -> EngineAdapter: """Returns the adapter for the specified gateway or the default adapter if none is provided.""" if gateway: @@ -1212,6 +1347,7 @@ def _execute_create( create_render_kwargs: t.Dict[str, t.Any], rendered_physical_properties: t.Dict[str, exp.Expression], dry_run: bool, + run_pre_post_statements: bool = True, ) -> None: adapter = self.get_adapter(snapshot.model.gateway) evaluation_strategy = _evaluation_strategy(snapshot, adapter) @@ -1224,7 +1360,8 @@ def _execute_create( **create_render_kwargs, "table_mapping": {snapshot.name: table_name}, } - adapter.execute(snapshot.model.render_pre_statements(**create_render_kwargs)) + if run_pre_post_statements: + adapter.execute(snapshot.model.render_pre_statements(**create_render_kwargs)) evaluation_strategy.create( table_name=table_name, model=snapshot.model, @@ -1235,16 +1372,20 @@ def _execute_create( dry_run=dry_run, physical_properties=rendered_physical_properties, ) - adapter.execute(snapshot.model.render_post_statements(**create_render_kwargs)) + if run_pre_post_statements: + adapter.execute(snapshot.model.render_post_statements(**create_render_kwargs)) - def set_correlation_id(self, correlation_id: CorrelationId) -> SnapshotEvaluator: - return SnapshotEvaluator( - { - gateway: adapter.with_settings(correlation_id=correlation_id) - for gateway, adapter in self.adapters.items() - }, - self.ddl_concurrent_tasks, - self.selected_gateway, + def _can_clone(self, snapshot: Snapshot, deployability_index: DeployabilityIndex) -> bool: + adapter = self.get_adapter(snapshot.model.gateway) + return ( + snapshot.is_forward_only + and snapshot.is_materialized + and bool(snapshot.previous_versions) + and adapter.SUPPORTS_CLONING + # managed models cannot have their schema mutated because theyre based on queries, so clone + alter wont work + and not snapshot.is_managed + # If the deployable table is missing we can't clone it + and not deployability_index.is_deployable(snapshot) ) @@ -1607,10 +1748,13 @@ def _replace_query_for_model( columns_to_types = model.columns_to_types_or_raise source_columns: t.Optional[t.List[str]] = list(columns_to_types) else: - # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models. - columns_to_types, source_columns = self._get_target_and_source_columns( - model, name, render_kwargs, force_get_columns_from_target=True - ) + try: + # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models. + columns_to_types, source_columns = self._get_target_and_source_columns( + model, name, render_kwargs, force_get_columns_from_target=True + ) + except Exception: + columns_to_types, source_columns = None, None self.adapter.replace_query( name, @@ -2087,11 +2231,7 @@ def insert( render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: - deployability_index = ( - kwargs.get("deployability_index") or DeployabilityIndex.all_deployable() - ) snapshot = kwargs["snapshot"] - snapshots = kwargs["snapshots"] if ( not snapshot.is_materialized_view @@ -2131,17 +2271,6 @@ def create( render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: - is_snapshot_representative: bool = kwargs["is_snapshot_representative"] - if not is_snapshot_representative and is_table_deployable: - # If the snapshot is not representative, the query may contain references to non-deployable tables or views. - # This may happen if there was a forward-only change upstream which now requires the view query to point at dev preview tables. - # Therefore, we postpone the creation of the deployable view until the snapshot is deployed to production. - logger.info( - "Skipping creation of the deployable view '%s' for the non-representative snapshot", - table_name, - ) - return - if self.adapter.table_exists(table_name): # Make sure we don't recreate the view to prevent deletion of downstream views in engines with no late # binding support (because of DROP CASCADE). diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index b4e8caf0bc..4bfe78cca0 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -57,9 +57,9 @@ def sqlmesh_config( if model_defaults.dialect is None: model_defaults.dialect = profile.target.dialect - target_to_sqlmesh_args = {} - if register_comments is not None: - target_to_sqlmesh_args["register_comments"] = register_comments + target_to_sqlmesh_args = { + "register_comments": register_comments or False, + } loader = kwargs.pop("loader", DbtLoader) if not issubclass(loader, DbtLoader): diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 45accccaa8..f283680cfb 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -139,10 +139,6 @@ def assert_new_env(result, new_env="prod", from_env="prod", initialize=True) -> ) in result.output -def assert_physical_layer_updated(result) -> None: - assert "Physical layer updated" in result.output - - def assert_model_batches_executed(result) -> None: assert "Model batches executed" in result.output @@ -152,7 +148,6 @@ def assert_virtual_layer_updated(result) -> None: def assert_backfill_success(result) -> None: - assert_physical_layer_updated(result) assert_model_batches_executed(result) assert_virtual_layer_updated(result) diff --git a/tests/cli/test_integration_cli.py b/tests/cli/test_integration_cli.py index 9b39b948f2..5d000b9d8b 100644 --- a/tests/cli/test_integration_cli.py +++ b/tests/cli/test_integration_cli.py @@ -141,7 +141,6 @@ def do_something(evaluator): result = invoke_cli(["plan", "--no-prompts", "--auto-apply", "--skip-tests"]) assert result.returncode == 0 - assert "Physical layer updated" in result.stdout assert "Virtual layer updated" in result.stdout # render the query to ensure our macro is being invoked @@ -175,7 +174,6 @@ def do_something(evaluator): ] ) assert result.returncode == 0 - assert "Physical layer updated" in result.stdout assert "Virtual layer updated" in result.stdout log_file_contents = last_log_file_contents() @@ -236,7 +234,6 @@ def do_something(evaluator): result = invoke_cli(["plan", "--no-prompts", "--auto-apply", "--skip-tests"]) assert result.returncode == 0 - assert "Physical layer updated" in result.stdout assert "Virtual layer updated" in result.stdout # clear cache to ensure we are forced to reload everything @@ -266,7 +263,6 @@ def do_something(evaluator): ) assert result.returncode == 0 assert "Apply - Backfill Tables [y/n]:" in result.stdout - assert "Physical layer updated" not in result.stdout # the invalid snapshot in state should not prevent a plan if --select-model is used on it (since the local version can be rendered) result = invoke_cli( @@ -343,7 +339,6 @@ def test_model_selector_tags_picks_up_both_remote_and_local( result = invoke_cli(["plan", "--no-prompts", "--auto-apply", "--skip-tests"]) assert result.returncode == 0 - assert "Physical layer updated" in result.stdout assert "Virtual layer updated" in result.stdout # add a new model locally with tag:a diff --git a/tests/conftest.py b/tests/conftest.py index 01fef852f7..85780d2db4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -271,13 +271,16 @@ def push_plan(context: Context, plan: Plan) -> None: context.default_catalog, ) deployability_index = DeployabilityIndex.create(context.snapshots.values()) - evaluatable_plan = plan.to_evaluatable() + evaluatable_plan = plan.to_evaluatable().copy(update={"skip_backfill": True}) stages = plan_stages.build_plan_stages( evaluatable_plan, context.state_sync, context.default_catalog ) for stage in stages: if isinstance(stage, plan_stages.CreateSnapshotRecordsStage): plan_evaluator.visit_create_snapshot_records_stage(stage, evaluatable_plan) + elif isinstance(stage, plan_stages.PhysicalLayerSchemaCreationStage): + stage.deployability_index = deployability_index + plan_evaluator.visit_physical_layer_schema_creation_stage(stage, evaluatable_plan) elif isinstance(stage, plan_stages.PhysicalLayerUpdateStage): stage.deployability_index = deployability_index plan_evaluator.visit_physical_layer_update_stage(stage, evaluatable_plan) diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 521b287d12..d1a9c5afaa 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -1914,6 +1914,11 @@ def test_sushi(ctx: TestContext, tmp_path_factory: pytest.TempPathFactory): ], personal_paths=[pathlib.Path("~/.sqlmesh/config.yaml").expanduser()], ) + config.before_all = [ + f"CREATE SCHEMA IF NOT EXISTS {raw_test_schema}", + f"DROP VIEW IF EXISTS {raw_test_schema}.demographics", + f"CREATE VIEW {raw_test_schema}.demographics AS (SELECT 1 AS customer_id, '00000' AS zip)", + ] # To enable parallelism in integration tests config.gateways = {ctx.gateway: config.gateways[ctx.gateway]} @@ -2132,6 +2137,8 @@ def validate_comments( } for model_name, comment in comments.items(): + if not model_name in layer_models: + continue layer_table_name = layer_models[model_name]["table_name"] table_kind = "VIEW" if layer_models[model_name]["is_view"] else "BASE TABLE" diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index caa7843726..5923afa217 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -249,6 +249,7 @@ def test_incremental_by_time_datetimeoffset_precision( end="2020-01-02", execution_time="2020-01-02", snapshots={}, + target_table_exists=True, ) assert adapter.cursor.execute.call_args_list[0][0][0] == ( diff --git a/tests/core/test_context.py b/tests/core/test_context.py index a94ba74a20..3b7c5bd51d 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -689,9 +689,7 @@ def test_plan_apply_populates_cache(copy_to_temp_path, mocker): config_content = f.read() # Add cache_dir to the test_config definition - config_content = config_content.replace( - 'test_config = Config(\n gateways={"in_memory": GatewayConfig(connection=DuckDBConnectionConfig())},\n default_gateway="in_memory",\n plan=PlanConfig(\n auto_categorize_changes=CategorizerConfig(\n sql=AutoCategorizationMode.SEMI, python=AutoCategorizationMode.OFF\n )\n ),\n model_defaults=model_defaults,\n)', - f"""test_config = Config( + config_content += f"""test_config_cache_dir = Config( gateways={{"in_memory": GatewayConfig(connection=DuckDBConnectionConfig())}}, default_gateway="in_memory", plan=PlanConfig( @@ -701,14 +699,14 @@ def test_plan_apply_populates_cache(copy_to_temp_path, mocker): ), model_defaults=model_defaults, cache_dir="{custom_cache_dir.as_posix()}", -)""", - ) + before_all=before_all, +)""" with open(config_py_path, "w") as f: f.write(config_content) # Create context with the test config - context = Context(paths=sushi_path, config="test_config") + context = Context(paths=sushi_path, config="test_config_cache_dir") custom_cache_dir = context.cache_dir assert "custom_cache" in str(custom_cache_dir) assert (custom_cache_dir / "optimized_query").exists() @@ -733,7 +731,7 @@ def test_plan_apply_populates_cache(copy_to_temp_path, mocker): # New context should load same models and create the cache for optimized_query and model_definition initial_model_count = len(context.models) - context2 = Context(paths=context.path, config="test_config") + context2 = Context(paths=context.path, config="test_config_cache_dir") cached_model_count = len(context2.models) assert initial_model_count == cached_model_count > 0 @@ -1778,14 +1776,14 @@ def test_plan_environment_statements(tmp_path: pathlib.Path): ); @IF( - @runtime_stage = 'evaluating', + @runtime_stage IN ('evaluating', 'creating'), SET VARIABLE stats_model_start = now() ); SELECT 1 AS cola; @IF( - @runtime_stage = 'evaluating', + @runtime_stage IN ('evaluating', 'creating'), INSERT INTO analytic_stats (physical_table, evaluation_start, evaluation_end, evaluation_time) VALUES (@resolve_template('@{schema_name}.@{table_name}'), getvariable('stats_model_start'), now(), now() - getvariable('stats_model_start')) ); @@ -1851,11 +1849,11 @@ def access_adapter(evaluator): assert ( model.pre_statements[0].sql() - == "@IF(@runtime_stage = 'evaluating', SET VARIABLE stats_model_start = NOW())" + == "@IF(@runtime_stage IN ('evaluating', 'creating'), SET VARIABLE stats_model_start = NOW())" ) assert ( model.post_statements[0].sql() - == "@IF(@runtime_stage = 'evaluating', INSERT INTO analytic_stats (physical_table, evaluation_start, evaluation_end, evaluation_time) VALUES (@resolve_template('@{schema_name}.@{table_name}'), GETVARIABLE('stats_model_start'), NOW(), NOW() - GETVARIABLE('stats_model_start')))" + == "@IF(@runtime_stage IN ('evaluating', 'creating'), INSERT INTO analytic_stats (physical_table, evaluation_start, evaluation_end, evaluation_time) VALUES (@resolve_template('@{schema_name}.@{table_name}'), GETVARIABLE('stats_model_start'), NOW(), NOW() - GETVARIABLE('stats_model_start')))" ) stats_table = context.fetchdf("select * from memory.analytic_stats").to_dict() diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 72d8964a71..fc129424f4 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -37,7 +37,7 @@ DuckDBConnectionConfig, TableNamingConvention, ) -from sqlmesh.core.config.common import EnvironmentSuffixTarget +from sqlmesh.core.config.common import EnvironmentSuffixTarget, VirtualEnvironmentMode from sqlmesh.core.console import Console, get_console from sqlmesh.core.context import Context from sqlmesh.core.config.categorizer import CategorizerConfig @@ -488,12 +488,6 @@ def test_full_history_restatement_model_regular_plan_preview_enabled( waiter_as_customer_snapshot = context.get_snapshot( "sushi.waiter_as_customer_by_day", raise_if_missing=True ) - count_customers_active_snapshot = context.get_snapshot( - "sushi.count_customers_active", raise_if_missing=True - ) - count_customers_inactive_snapshot = context.get_snapshot( - "sushi.count_customers_inactive", raise_if_missing=True - ) plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() @@ -959,8 +953,9 @@ def test_new_forward_only_model(init_and_plan_context: t.Callable): @time_machine.travel("2023-01-08 15:00:00 UTC") def test_plan_set_choice_is_reflected_in_missing_intervals(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) + context, _ = init_and_plan_context("examples/sushi") + context.upsert_model(context.get_model("sushi.top_waiters").copy(update={"kind": FullKind()})) + context.plan("prod", skip_tests=True, no_prompts=True, auto_apply=True) model_name = "sushi.waiter_revenue_by_day" @@ -1461,6 +1456,18 @@ def test_indirect_non_breaking_downstream_of_forward_only(init_and_plan_context: plan = context.plan_builder("prod", skip_tests=True).build() assert plan.start == to_timestamp("2023-01-01") assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiter_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), SnapshotIntervals( snapshot_id=non_breaking_snapshot.snapshot_id, intervals=[ @@ -1485,8 +1492,9 @@ def test_indirect_non_breaking_downstream_of_forward_only(init_and_plan_context: @time_machine.travel("2023-01-08 15:00:00 UTC") def test_breaking_only_impacts_immediate_children(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) + context, _ = init_and_plan_context("examples/sushi") + context.upsert_model(context.get_model("sushi.top_waiters").copy(update={"kind": FullKind()})) + context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) breaking_model = context.get_model("sushi.orders") breaking_model = breaking_model.copy(update={"stamp": "force new version"}) @@ -2206,9 +2214,7 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot( context.upsert_model(add_projection_to_model(t.cast(SqlModel, forward_only_model))) forward_only_model_snapshot_id = context.get_snapshot(forward_only_model_name).snapshot_id full_downstream_model_snapshot_id = context.get_snapshot(full_downstream_model_name).snapshot_id - full_downstream_model_2_snapshot_id = context.get_snapshot( - view_downstream_model_name - ).snapshot_id + view_downstream_model_snapshot_id = context.get_snapshot(view_downstream_model_name).snapshot_id dev_plan = context.plan("dev", auto_apply=True, no_prompts=True, enable_preview=False) assert ( dev_plan.snapshots[forward_only_model_snapshot_id].change_category @@ -2219,7 +2225,7 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot( == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) assert ( - dev_plan.snapshots[full_downstream_model_2_snapshot_id].change_category + dev_plan.snapshots[view_downstream_model_snapshot_id].change_category == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) assert not dev_plan.missing_intervals @@ -2238,9 +2244,7 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot( new_full_downstream_model = load_sql_based_model(new_full_downstream_model_expressions) context.upsert_model(new_full_downstream_model) full_downstream_model_snapshot_id = context.get_snapshot(full_downstream_model_name).snapshot_id - full_downstream_model_2_snapshot_id = context.get_snapshot( - view_downstream_model_name - ).snapshot_id + view_downstream_model_snapshot_id = context.get_snapshot(view_downstream_model_name).snapshot_id dev_plan = context.plan( "dev", categorizer_config=CategorizerConfig.all_full(), @@ -2253,12 +2257,12 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot( == SnapshotChangeCategory.BREAKING ) assert ( - dev_plan.snapshots[full_downstream_model_2_snapshot_id].change_category + dev_plan.snapshots[view_downstream_model_snapshot_id].change_category == SnapshotChangeCategory.INDIRECT_BREAKING ) assert len(dev_plan.missing_intervals) == 2 assert dev_plan.missing_intervals[0].snapshot_id == full_downstream_model_snapshot_id - assert dev_plan.missing_intervals[1].snapshot_id == full_downstream_model_2_snapshot_id + assert dev_plan.missing_intervals[1].snapshot_id == view_downstream_model_snapshot_id # Check that the representative view hasn't been created yet. assert not context.engine_adapter.table_exists( @@ -2272,9 +2276,7 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot( # Finally, make a non-breaking change to the full model in the same dev environment. context.upsert_model(add_projection_to_model(t.cast(SqlModel, new_full_downstream_model))) full_downstream_model_snapshot_id = context.get_snapshot(full_downstream_model_name).snapshot_id - full_downstream_model_2_snapshot_id = context.get_snapshot( - view_downstream_model_name - ).snapshot_id + view_downstream_model_snapshot_id = context.get_snapshot(view_downstream_model_name).snapshot_id dev_plan = context.plan( "dev", categorizer_config=CategorizerConfig.all_full(), @@ -2287,10 +2289,13 @@ def test_indirect_non_breaking_view_model_non_representative_snapshot( == SnapshotChangeCategory.NON_BREAKING ) assert ( - dev_plan.snapshots[full_downstream_model_2_snapshot_id].change_category + dev_plan.snapshots[view_downstream_model_snapshot_id].change_category == SnapshotChangeCategory.INDIRECT_NON_BREAKING ) + # Deploy changes to prod + context.plan("prod", auto_apply=True, no_prompts=True) + # Check that the representative view has been created. assert context.engine_adapter.table_exists( context.get_snapshot(view_downstream_model_name).table_name() @@ -2659,6 +2664,66 @@ def test_virtual_environment_mode_dev_only_model_kind_change(init_and_plan_conte assert data_objects[0].type == "table" +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_kind_change_incremental( + init_and_plan_context: t.Callable, +): + context, _ = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + + forward_only_model_name = "memory.sushi.test_forward_only_model" + forward_only_model_expressions = d.parse( + f""" + MODEL ( + name {forward_only_model_name}, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + ), + ); + + SELECT '2023-01-01' AS ds, 'value' AS value; + """ + ) + forward_only_model = load_sql_based_model(forward_only_model_expressions) + forward_only_model = forward_only_model.copy( + update={"virtual_environment_mode": VirtualEnvironmentMode.DEV_ONLY} + ) + context.upsert_model(forward_only_model) + + context.plan("prod", auto_apply=True, no_prompts=True) + + # Change to view + model = context.get_model(forward_only_model_name) + original_kind = model.kind + model = model.copy(update={"kind": ViewKind()}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"test_forward_only_model"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "view" + + model = model.copy(update={"kind": original_kind}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"test_forward_only_model"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "table" + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_virtual_environment_mode_dev_only_model_kind_change_with_follow_up_changes_in_dev( init_and_plan_context: t.Callable, @@ -3146,7 +3211,7 @@ def test_restatement_plan_clears_correct_intervals_across_environments(tmp_path: cron '@daily' ); - select account_id, name, date from test.external_table; + select 1 as account_id, date from test.external_table; """ with open(models_dir / "model1.sql", "w") as f: f.write(model1) @@ -3945,11 +4010,12 @@ def test_plan_snapshot_table_exists_for_promoted_snapshot(init_and_plan_context: "UPDATE sqlmesh._environments SET finalized_ts = NULL WHERE name = 'dev'" ) - model = context.get_model("sushi.customers") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - context.plan( - "dev", select_models=["sushi.customers"], auto_apply=True, no_prompts=True, skip_tests=True + "prod", + restate_models=["sushi.top_waiters"], + auto_apply=True, + no_prompts=True, + skip_tests=True, ) assert context.engine_adapter.table_exists(top_waiters_snapshot.table_name()) @@ -4526,7 +4592,7 @@ def test_plan_repairs_unrenderable_snapshot_state( plan = plan_builder.build() assert plan.directly_modified == {target_snapshot.snapshot_id} if not forward_only: - assert {i.snapshot_id for i in plan.missing_intervals} == {target_snapshot.snapshot_id} + assert target_snapshot.snapshot_id in {i.snapshot_id for i in plan.missing_intervals} plan_builder.set_choice(target_snapshot, SnapshotChangeCategory.NON_BREAKING) plan = plan_builder.build() @@ -5265,11 +5331,14 @@ def test_multi(mocker): assert context.fetchdf("select * from after_1").to_dict()["repo_1"][0] == "repo_1" assert context.fetchdf("select * from after_2").to_dict()["repo_2"][0] == "repo_2" + old_context = context context = Context( paths=["examples/multi/repo_1"], - state_sync=context.state_sync, + state_sync=old_context.state_sync, gateway="memory", ) + context._engine_adapter = old_context.engine_adapter + del context.engine_adapters model = context.get_model("bronze.a") assert model.project == "repo_1" @@ -5862,7 +5931,7 @@ def get_default_catalog_and_non_tables( ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) assert len(prod_views) == 16 assert len(dev_views) == 0 - assert len(user_default_tables) == 21 + assert len(user_default_tables) == 15 assert state_metadata.schemas == ["sqlmesh"] assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( { @@ -5881,7 +5950,7 @@ def get_default_catalog_and_non_tables( ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) assert len(prod_views) == 16 assert len(dev_views) == 16 - assert len(user_default_tables) == 21 + assert len(user_default_tables) == 16 assert len(non_default_tables) == 0 assert state_metadata.schemas == ["sqlmesh"] assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( @@ -5901,7 +5970,7 @@ def get_default_catalog_and_non_tables( ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) assert len(prod_views) == 16 assert len(dev_views) == 32 - assert len(user_default_tables) == 21 + assert len(user_default_tables) == 16 assert len(non_default_tables) == 0 assert state_metadata.schemas == ["sqlmesh"] assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( @@ -5922,7 +5991,7 @@ def get_default_catalog_and_non_tables( ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) assert len(prod_views) == 16 assert len(dev_views) == 16 - assert len(user_default_tables) == 21 + assert len(user_default_tables) == 16 assert len(non_default_tables) == 0 assert state_metadata.schemas == ["sqlmesh"] assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( @@ -6074,13 +6143,13 @@ def test_restatement_of_full_model_with_start(init_and_plan_context: t.Callable) @time_machine.travel("2023-01-08 15:00:00 UTC") def test_restatement_should_not_override_environment_statements(init_and_plan_context: t.Callable): context, _ = init_and_plan_context("examples/sushi") - context.config.before_all = ["SELECT 'test_before_all';"] + context.config.before_all = ["SELECT 'test_before_all';", *context.config.before_all] context.load() context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) prod_env_statements = context.state_reader.get_environment_statements(c.PROD) - assert prod_env_statements[0].before_all == ["SELECT 'test_before_all';"] + assert prod_env_statements[0].before_all[0] == "SELECT 'test_before_all';" context.plan( restate_models=["sushi.waiter_revenue_by_day"], @@ -6090,7 +6159,7 @@ def test_restatement_should_not_override_environment_statements(init_and_plan_co ) prod_env_statements = context.state_reader.get_environment_statements(c.PROD) - assert prod_env_statements[0].before_all == ["SELECT 'test_before_all';"] + assert prod_env_statements[0].before_all[0] == "SELECT 'test_before_all';" @time_machine.travel("2023-01-08 15:00:00 UTC") @@ -6138,7 +6207,7 @@ def test_plan_production_environment_statements(tmp_path: Path): ); @IF( - @runtime_stage = 'creating', + @runtime_stage IN ('evaluating', 'creating'), INSERT INTO schema_names_for_prod (physical_schema_name) VALUES (@resolve_template('@{schema_name}')) ); @@ -6893,17 +6962,7 @@ def plan_with_output(ctx: Context, environment: str): assert "New environment `dev` will be created from `prod`" in output.stdout assert "Differences from the `prod` environment" in output.stdout - assert ( - """MODEL ( - name test.a, -+ owner test, - kind FULL - ) - SELECT -- 5 AS col -+ 10 AS col""" - in output.stdout - ) + assert "Directly Modified: test__dev.a" in output.stdout # Case 6: Ensure that target environment and create_from environment are not the same output = plan_with_output(ctx, "prod") @@ -7299,11 +7358,7 @@ def test_engine_adapters_multi_repo_all_gateways_gathered(copy_to_temp_path): def test_physical_table_naming_strategy_table_only(copy_to_temp_path: t.Callable): sushi_context = Context( paths=copy_to_temp_path("examples/sushi"), - config=Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(), - physical_table_naming_convention=TableNamingConvention.TABLE_ONLY, - ), + config="table_only_naming_config", ) assert sushi_context.config.physical_table_naming_convention == TableNamingConvention.TABLE_ONLY @@ -7334,11 +7389,7 @@ def test_physical_table_naming_strategy_table_only(copy_to_temp_path: t.Callable def test_physical_table_naming_strategy_hash_md5(copy_to_temp_path: t.Callable): sushi_context = Context( paths=copy_to_temp_path("examples/sushi"), - config=Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(), - physical_table_naming_convention=TableNamingConvention.HASH_MD5, - ), + config="hash_md5_naming_config", ) assert sushi_context.config.physical_table_naming_convention == TableNamingConvention.HASH_MD5 diff --git a/tests/core/test_plan_evaluator.py b/tests/core/test_plan_evaluator.py index a3735b08ed..575f5ae742 100644 --- a/tests/core/test_plan_evaluator.py +++ b/tests/core/test_plan_evaluator.py @@ -69,10 +69,12 @@ def test_builtin_evaluator_push(sushi_context: Context, make_snapshot): stages = plan_stages.build_plan_stages( evaluatable_plan, sushi_context.state_sync, sushi_context.default_catalog ) - assert isinstance(stages[0], plan_stages.CreateSnapshotRecordsStage) - evaluator.visit_create_snapshot_records_stage(stages[0], evaluatable_plan) - assert isinstance(stages[1], plan_stages.PhysicalLayerUpdateStage) - evaluator.visit_physical_layer_update_stage(stages[1], evaluatable_plan) + assert isinstance(stages[1], plan_stages.CreateSnapshotRecordsStage) + evaluator.visit_create_snapshot_records_stage(stages[1], evaluatable_plan) + assert isinstance(stages[2], plan_stages.PhysicalLayerSchemaCreationStage) + evaluator.visit_physical_layer_schema_creation_stage(stages[2], evaluatable_plan) + assert isinstance(stages[3], plan_stages.BackfillStage) + evaluator.visit_backfill_stage(stages[3], evaluatable_plan) assert ( len(sushi_context.state_sync.get_snapshots([new_model_snapshot, new_view_model_snapshot])) diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index aedf50e26f..7b172caf6a 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -12,6 +12,7 @@ AfterAllStage, AuditOnlyRunStage, PhysicalLayerUpdateStage, + PhysicalLayerSchemaCreationStage, CreateSnapshotRecordsStage, BeforeAllStage, BackfillStage, @@ -134,18 +135,14 @@ def test_build_plan_stages_basic( snapshot_a.snapshot_id, snapshot_b.snapshot_id, } - # Verify PhysicalLayerUpdateStage + # Verify PhysicalLayerSchemaCreationStage physical_stage = stages[1] - assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert isinstance(physical_stage, PhysicalLayerSchemaCreationStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { snapshot_a.snapshot_id, snapshot_b.snapshot_id, } - assert {s.snapshot_id for s in physical_stage.snapshots_with_missing_intervals} == { - snapshot_a.snapshot_id, - snapshot_b.snapshot_id, - } assert physical_stage.deployability_index == DeployabilityIndex.all_deployable() # Verify BackfillStage @@ -252,9 +249,9 @@ def test_build_plan_stages_with_before_all_and_after_all( snapshot_b.snapshot_id, } - # Verify PhysicalLayerUpdateStage + # Verify PhysicalLayerSchemaCreationStage physical_stage = stages[2] - assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert isinstance(physical_stage, PhysicalLayerSchemaCreationStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { snapshot_a.snapshot_id, @@ -356,13 +353,12 @@ def test_build_plan_stages_select_models( snapshot_b.snapshot_id, } - # Verify PhysicalLayerUpdateStage + # Verify PhysicalLayerSchemaCreationStage physical_stage = stages[1] - assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert isinstance(physical_stage, PhysicalLayerSchemaCreationStage) assert len(physical_stage.snapshots) == 1 assert {s.snapshot_id for s in physical_stage.snapshots} == {snapshot_a.snapshot_id} assert physical_stage.deployability_index == DeployabilityIndex.all_deployable() - assert physical_stage.snapshots_with_missing_intervals == {snapshot_a.snapshot_id} # Verify BackfillStage backfill_stage = stages[2] @@ -446,7 +442,7 @@ def test_build_plan_stages_basic_no_backfill( stages = build_plan_stages(plan, state_reader, None) # Verify stages - assert len(stages) == 7 + assert len(stages) == 8 # Verify CreateSnapshotRecordsStage create_snapshot_records_stage = stages[0] @@ -456,8 +452,17 @@ def test_build_plan_stages_basic_no_backfill( snapshot_a.snapshot_id, snapshot_b.snapshot_id, } - # Verify PhysicalLayerUpdateStage + # Verify PhysicalLayerSchemaCreationStage physical_stage = stages[1] + assert isinstance(physical_stage, PhysicalLayerSchemaCreationStage) + assert len(physical_stage.snapshots) == 2 + assert {s.snapshot_id for s in physical_stage.snapshots} == { + snapshot_a.snapshot_id, + snapshot_b.snapshot_id, + } + + # Verify PhysicalLayerUpdateStage + physical_stage = stages[2] assert isinstance(physical_stage, PhysicalLayerUpdateStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { @@ -466,28 +471,28 @@ def test_build_plan_stages_basic_no_backfill( } # Verify BackfillStage - backfill_stage = stages[2] + backfill_stage = stages[3] assert isinstance(backfill_stage, BackfillStage) assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() assert backfill_stage.snapshot_to_intervals == {} # Verify EnvironmentRecordUpdateStage - assert isinstance(stages[3], EnvironmentRecordUpdateStage) - assert stages[3].no_gaps_snapshot_names == {snapshot_a.name, snapshot_b.name} + assert isinstance(stages[4], EnvironmentRecordUpdateStage) + assert stages[4].no_gaps_snapshot_names == {snapshot_a.name, snapshot_b.name} # Verify UnpauseStage - assert isinstance(stages[4], UnpauseStage) - assert {s.name for s in stages[4].promoted_snapshots} == {snapshot_a.name, snapshot_b.name} + assert isinstance(stages[5], UnpauseStage) + assert {s.name for s in stages[5].promoted_snapshots} == {snapshot_a.name, snapshot_b.name} # Verify VirtualLayerUpdateStage - virtual_stage = stages[5] + virtual_stage = stages[6] assert isinstance(virtual_stage, VirtualLayerUpdateStage) assert len(virtual_stage.promoted_snapshots) == 2 assert len(virtual_stage.demoted_snapshots) == 0 assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} # Verify FinalizeEnvironmentStage - assert isinstance(stages[6], FinalizeEnvironmentStage) + assert isinstance(stages[7], FinalizeEnvironmentStage) def test_build_plan_stages_restatement( @@ -558,9 +563,9 @@ def test_build_plan_stages_restatement( # Verify stages assert len(stages) == 5 - # Verify PhysicalLayerUpdateStage + # Verify PhysicalLayerSchemaCreationStage physical_stage = stages[0] - assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert isinstance(physical_stage, PhysicalLayerSchemaCreationStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { snapshot_a.snapshot_id, @@ -679,17 +684,15 @@ def test_build_plan_stages_forward_only( new_snapshot_b.snapshot_id, } - # Verify PhysicalLayerUpdateStage + # Verify PhysicalLayerSchemaCreationStage physical_stage = stages[1] - assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert isinstance(physical_stage, PhysicalLayerSchemaCreationStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { new_snapshot_a.snapshot_id, new_snapshot_b.snapshot_id, } - assert physical_stage.deployability_index == DeployabilityIndex.create( - [new_snapshot_a, new_snapshot_b] - ) + assert physical_stage.deployability_index == DeployabilityIndex.all_deployable() # Verify EnvironmentRecordUpdateStage assert isinstance(stages[2], EnvironmentRecordUpdateStage) @@ -808,9 +811,9 @@ def test_build_plan_stages_forward_only_dev( new_snapshot_b.snapshot_id, } - # Verify PhysicalLayerUpdateStage + # Verify PhysicalLayerSchemaCreationStage physical_stage = stages[1] - assert isinstance(physical_stage, PhysicalLayerUpdateStage) + assert isinstance(physical_stage, PhysicalLayerSchemaCreationStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { new_snapshot_a.snapshot_id, @@ -921,7 +924,7 @@ def _get_snapshots(snapshot_ids: t.List[SnapshotId]) -> t.Dict[SnapshotId, Snaps stages = build_plan_stages(plan, state_reader, None) # Verify stages - assert len(stages) == 7 + assert len(stages) == 8 # Verify CreateSnapshotRecordsStage create_snapshot_records_stage = stages[0] @@ -932,8 +935,20 @@ def _get_snapshots(snapshot_ids: t.List[SnapshotId]) -> t.Dict[SnapshotId, Snaps new_snapshot_b.snapshot_id, } - # Verify PhysicalLayerUpdateStage + # Verify PhysicalLayerSchemaCreationStage physical_stage = stages[1] + assert isinstance(physical_stage, PhysicalLayerSchemaCreationStage) + assert len(physical_stage.snapshots) == 2 + assert {s.snapshot_id for s in physical_stage.snapshots} == { + new_snapshot_a.snapshot_id, + new_snapshot_b.snapshot_id, + } + assert physical_stage.deployability_index == DeployabilityIndex.create( + [new_snapshot_a, new_snapshot_b] + ) + + # Verify PhysicalLayerUpdateStage + physical_stage = stages[2] assert isinstance(physical_stage, PhysicalLayerUpdateStage) assert len(physical_stage.snapshots) == 2 assert {s.snapshot_id for s in physical_stage.snapshots} == { @@ -945,28 +960,28 @@ def _get_snapshots(snapshot_ids: t.List[SnapshotId]) -> t.Dict[SnapshotId, Snaps ) # Verify AuditOnlyRunStage - audit_only_stage = stages[2] + audit_only_stage = stages[3] assert isinstance(audit_only_stage, AuditOnlyRunStage) assert len(audit_only_stage.snapshots) == 1 assert audit_only_stage.snapshots[0].snapshot_id == new_snapshot_a.snapshot_id # Verify BackfillStage - backfill_stage = stages[3] + backfill_stage = stages[4] assert isinstance(backfill_stage, BackfillStage) assert len(backfill_stage.snapshot_to_intervals) == 0 # Verify EnvironmentRecordUpdateStage - assert isinstance(stages[4], EnvironmentRecordUpdateStage) + assert isinstance(stages[5], EnvironmentRecordUpdateStage) # Verify VirtualLayerUpdateStage - virtual_stage = stages[5] + virtual_stage = stages[6] assert isinstance(virtual_stage, VirtualLayerUpdateStage) assert len(virtual_stage.promoted_snapshots) == 2 assert len(virtual_stage.demoted_snapshots) == 0 assert {s.name for s in virtual_stage.promoted_snapshots} == {'"a"', '"b"'} # Verify FinalizeEnvironmentStage - assert isinstance(stages[6], FinalizeEnvironmentStage) + assert isinstance(stages[7], FinalizeEnvironmentStage) def test_build_plan_stages_forward_only_ensure_finalized_snapshots( @@ -1046,7 +1061,7 @@ def test_build_plan_stages_forward_only_ensure_finalized_snapshots( assert len(stages) == 8 assert isinstance(stages[0], CreateSnapshotRecordsStage) - assert isinstance(stages[1], PhysicalLayerUpdateStage) + assert isinstance(stages[1], PhysicalLayerSchemaCreationStage) assert isinstance(stages[2], EnvironmentRecordUpdateStage) assert isinstance(stages[3], MigrateSchemasStage) assert isinstance(stages[4], BackfillStage) @@ -1120,7 +1135,7 @@ def test_build_plan_stages_removed_model( # Verify stages assert len(stages) == 5 - assert isinstance(stages[0], PhysicalLayerUpdateStage) + assert isinstance(stages[0], PhysicalLayerSchemaCreationStage) assert isinstance(stages[1], BackfillStage) assert isinstance(stages[2], EnvironmentRecordUpdateStage) assert isinstance(stages[3], VirtualLayerUpdateStage) @@ -1202,7 +1217,7 @@ def test_build_plan_stages_environment_suffix_target_changed( # Verify stages assert len(stages) == 5 - assert isinstance(stages[0], PhysicalLayerUpdateStage) + assert isinstance(stages[0], PhysicalLayerSchemaCreationStage) assert isinstance(stages[1], BackfillStage) assert isinstance(stages[2], EnvironmentRecordUpdateStage) assert isinstance(stages[3], VirtualLayerUpdateStage) @@ -1303,17 +1318,14 @@ def test_build_plan_stages_indirect_non_breaking_view_migration( assert len(stages) == 8 assert isinstance(stages[0], CreateSnapshotRecordsStage) - assert isinstance(stages[1], PhysicalLayerUpdateStage) + assert isinstance(stages[1], PhysicalLayerSchemaCreationStage) assert isinstance(stages[2], BackfillStage) assert isinstance(stages[3], EnvironmentRecordUpdateStage) - assert isinstance(stages[4], MigrateSchemasStage) - assert isinstance(stages[5], UnpauseStage) + assert isinstance(stages[4], UnpauseStage) + assert isinstance(stages[5], BackfillStage) assert isinstance(stages[6], VirtualLayerUpdateStage) assert isinstance(stages[7], FinalizeEnvironmentStage) - migrate_schemas_stage = stages[4] - assert {s.snapshot_id for s in migrate_schemas_stage.snapshots} == {new_snapshot_c.snapshot_id} - def test_build_plan_stages_virtual_environment_mode_filtering( make_snapshot, mocker: MockerFixture diff --git a/tests/core/test_scheduler.py b/tests/core/test_scheduler.py index 742642794f..b74aa3480e 100644 --- a/tests/core/test_scheduler.py +++ b/tests/core/test_scheduler.py @@ -21,6 +21,9 @@ interval_diff, compute_interval_params, SnapshotToIntervals, + EvaluateNode, + SchedulingUnit, + DummyNode, ) from sqlmesh.core.signal import signal from sqlmesh.core.snapshot import ( @@ -160,9 +163,10 @@ def test_incremental_by_unique_key_kind_dag( batches = get_batched_missing_intervals(scheduler, start, end, end) dag = scheduler._dag(batches) assert dag.graph == { - ( + EvaluateNode( unique_by_key_snapshot.name, - ((to_timestamp("2023-01-01"), to_timestamp("2023-01-07")), 0), + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-07")), + batch_index=0, ): set(), } @@ -202,60 +206,66 @@ def test_incremental_time_self_reference_dag( assert dag.graph == { # Only run one day at a time and each day relies on the previous days - ( + EvaluateNode( incremental_self_snapshot.name, - ((to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), 0), + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, ): set(), - ( + EvaluateNode( incremental_self_snapshot.name, - ((to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), 1), + interval=(to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + batch_index=1, ): { - ( - incremental_self_snapshot.name, - ((to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), 0), - ) + EvaluateNode( + snapshot_name=incremental_self_snapshot.name, + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, + ), }, - ( + EvaluateNode( incremental_self_snapshot.name, - ((to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), 2), + interval=(to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + batch_index=2, ): { - ( - incremental_self_snapshot.name, - ((to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), 1), + EvaluateNode( + snapshot_name=incremental_self_snapshot.name, + interval=(to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + batch_index=1, ), }, - ( - incremental_self_snapshot.name, - ((to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), 3), + EvaluateNode( + snapshot_name=incremental_self_snapshot.name, + interval=(to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + batch_index=3, ): { - ( - incremental_self_snapshot.name, - ((to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), 2), + EvaluateNode( + snapshot_name=incremental_self_snapshot.name, + interval=(to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + batch_index=2, + ), + }, + DummyNode(snapshot_name=incremental_self_snapshot.name): { + EvaluateNode( + snapshot_name=incremental_self_snapshot.name, + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, + ), + EvaluateNode( + snapshot_name=incremental_self_snapshot.name, + interval=(to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + batch_index=1, + ), + EvaluateNode( + snapshot_name=incremental_self_snapshot.name, + interval=(to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + batch_index=2, + ), + EvaluateNode( + snapshot_name=incremental_self_snapshot.name, + interval=(to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + batch_index=3, ), }, - ( - incremental_self_snapshot.name, - ((to_timestamp(0), to_timestamp(0)), -1), - ): set( - [ - ( - incremental_self_snapshot.name, - ((to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), 0), - ), - ( - incremental_self_snapshot.name, - ((to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), 1), - ), - ( - incremental_self_snapshot.name, - ((to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), 2), - ), - ( - incremental_self_snapshot.name, - ((to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), 3), - ), - ] - ), } @@ -266,16 +276,26 @@ def test_incremental_time_self_reference_dag( 2, 2, { - ( - '"test_model"', - ((to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), 0), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), + batch_index=0, ): set(), - ( - '"test_model"', - ((to_timestamp("2023-01-03"), to_timestamp("2023-01-05")), 1), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-03"), to_timestamp("2023-01-05")), + batch_index=1, ): set(), - ('"test_model"', ((to_timestamp("2023-01-05"), to_timestamp("2023-01-07")), 2)): { - ('"test_model"', ((to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), 0)), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-05"), to_timestamp("2023-01-07")), + batch_index=2, + ): { + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-03")), + batch_index=0, + ), }, }, ), @@ -283,26 +303,53 @@ def test_incremental_time_self_reference_dag( 1, 3, { - ( - '"test_model"', - ((to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), 0), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, ): set(), - ( - '"test_model"', - ((to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), 1), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + batch_index=1, ): set(), - ( - '"test_model"', - ((to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), 2), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + batch_index=2, ): set(), - ('"test_model"', ((to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), 3)): { - ('"test_model"', ((to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), 0)), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + batch_index=3, + ): { + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, + ), }, - ('"test_model"', ((to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), 4)): { - ('"test_model"', ((to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), 1)), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + batch_index=4, + ): { + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + batch_index=1, + ), }, - ('"test_model"', ((to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), 5)): { - ('"test_model"', ((to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), 2)), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + batch_index=5, + ): { + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + batch_index=2, + ), }, }, ), @@ -310,29 +357,35 @@ def test_incremental_time_self_reference_dag( 1, 10, { - ( - '"test_model"', - ((to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), 0), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, ): set(), - ( - '"test_model"', - ((to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), 1), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + batch_index=1, ): set(), - ( - '"test_model"', - ((to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), 2), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + batch_index=2, ): set(), - ( - '"test_model"', - ((to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), 3), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + batch_index=3, ): set(), - ( - '"test_model"', - ((to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), 4), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + batch_index=4, ): set(), - ( - '"test_model"', - ((to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), 5), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + batch_index=5, ): set(), }, ), @@ -340,9 +393,10 @@ def test_incremental_time_self_reference_dag( 10, 10, { - ( - '"test_model"', - ((to_timestamp("2023-01-01"), to_timestamp("2023-01-07")), 0), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-07")), + batch_index=0, ): set(), }, ), @@ -350,9 +404,10 @@ def test_incremental_time_self_reference_dag( 10, 1, { - ( - '"test_model"', - ((to_timestamp("2023-01-01"), to_timestamp("2023-01-07")), 0), + EvaluateNode( + snapshot_name='"test_model"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-07")), + batch_index=0, ): set(), }, ), @@ -364,7 +419,7 @@ def test_incremental_batch_concurrency( get_batched_missing_intervals, batch_size: int, batch_concurrency: int, - expected_graph: t.Dict[str, t.Any], + expected_graph: t.Dict[SchedulingUnit, t.Set[SchedulingUnit]], ): start = to_datetime("2023-01-01") end = to_datetime("2023-01-07") @@ -392,7 +447,7 @@ def test_incremental_batch_concurrency( batches = get_batched_missing_intervals(scheduler, start, end, end) dag = scheduler._dag(batches) - graph = {k: v for k, v in dag.graph.items() if k[1][1] != -1} # exclude the terminal node.} + graph = {k: v for k, v in dag.graph.items() if isinstance(k, EvaluateNode)} assert graph == expected_graph diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index bcb704ba48..bce091595c 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -2052,6 +2052,7 @@ def test_deployability_index(make_snapshot): snapshot_f.parents = (snapshot_e.snapshot_id, snapshot_a.snapshot_id) snapshot_g = make_snapshot(SqlModel(name="g", query=parse_one("SELECT 1"))) + snapshot_g.intervals = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] snapshot_g.categorize_as(SnapshotChangeCategory.INDIRECT_NON_BREAKING) snapshot_g.parents = (snapshot_e.snapshot_id,) diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index b05d567cd2..60931b1602 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -197,12 +197,6 @@ def x(evaluator, y=None) -> None: execute_calls = [call([parse_one('CREATE TABLE "hook_called"')])] adapter_mock.execute.assert_has_calls(execute_calls) - adapter_mock.create_schema.assert_has_calls( - [ - call(to_schema("sqlmesh__test_schema")), - ] - ) - common_kwargs = dict( target_columns_to_types={"a": exp.DataType.build("int")}, table_format=None, @@ -612,7 +606,6 @@ def test_evaluate_materialized_view_with_partitioned_by_cluster_by( execute_mock.assert_has_calls( [ - call("CREATE SCHEMA IF NOT EXISTS `sqlmesh__test_schema`"), call( f"CREATE MATERIALIZED VIEW `sqlmesh__test_schema`.`test_schema__test_model__{snapshot.version}` PARTITION BY `a` CLUSTER BY `b` AS SELECT `a` AS `a`, `b` AS `b` FROM `tbl` AS `tbl`" ), @@ -812,7 +805,6 @@ def test_create_only_dev_table_exists(mocker: MockerFixture, adapter_mock, make_ evaluator = SnapshotEvaluator(adapter_mock) evaluator.create([snapshot], {}) - adapter_mock.create_schema.assert_called_once_with(to_schema("sqlmesh__test_schema")) adapter_mock.create_view.assert_not_called() adapter_mock.get_data_objects.assert_called_once_with( schema_("sqlmesh__test_schema"), @@ -847,7 +839,6 @@ def test_create_new_forward_only_model(mocker: MockerFixture, adapter_mock, make evaluator = SnapshotEvaluator(adapter_mock) evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) - adapter_mock.create_schema.assert_called_once_with(to_schema("sqlmesh__test_schema")) # Only non-deployable table should be created adapter_mock.create_table.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.dev_version}__dev", @@ -867,66 +858,57 @@ def test_create_new_forward_only_model(mocker: MockerFixture, adapter_mock, make adapter_mock.get_data_objects.assert_called_once_with( schema_("sqlmesh__test_schema"), { - f"test_schema__test_model__{snapshot.version}", f"test_schema__test_model__{snapshot.dev_version}__dev", }, ) @pytest.mark.parametrize( - "deployability_index, snapshot_category, forward_only, deployability_flags", + "deployability_index, snapshot_category, forward_only", [ - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, False, [False]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.NON_BREAKING, False, [False]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, True, [True]), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, False), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.NON_BREAKING, False), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, True), ( DeployabilityIndex.all_deployable(), SnapshotChangeCategory.INDIRECT_BREAKING, False, - [False], ), ( DeployabilityIndex.all_deployable(), SnapshotChangeCategory.INDIRECT_NON_BREAKING, False, - [True], ), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.METADATA, False, [True]), + (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.METADATA, False), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.BREAKING, False, - [True, False], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.NON_BREAKING, False, - [True, False], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.BREAKING, True, - [True], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.INDIRECT_BREAKING, False, - [True, False], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.INDIRECT_NON_BREAKING, False, - [True], ), ( DeployabilityIndex.none_deployable(), SnapshotChangeCategory.METADATA, False, - [True], ), ], ) @@ -935,7 +917,6 @@ def test_create_tables_exist( mocker: MockerFixture, adapter_mock, deployability_index: DeployabilityIndex, - deployability_flags: t.List[bool], snapshot_category: SnapshotChangeCategory, forward_only: bool, ): @@ -967,8 +948,9 @@ def test_create_tables_exist( adapter_mock.get_data_objects.assert_called_once_with( schema_("sqlmesh__db"), { - f"db__model__{snapshot.version}" if not flag else f"db__model__{snapshot.version}__dev" - for flag in set(deployability_flags + [False]) + f"db__model__{snapshot.version}" + if deployability_index.is_deployable(snapshot) + else f"db__model__{snapshot.version}__dev", }, ) adapter_mock.create_schema.assert_not_called() @@ -1005,24 +987,11 @@ def test_create_prod_table_exists_forward_only(mocker: MockerFixture, adapter_mo adapter_mock.get_data_objects.assert_called_once_with( schema_("sqlmesh__test_schema"), { - f"test_schema__test_model__{snapshot.version}__dev", f"test_schema__test_model__{snapshot.version}", }, ) - adapter_mock.create_schema.assert_called_once_with(to_schema("sqlmesh__test_schema")) - adapter_mock.create_table.assert_called_once_with( - f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", - target_columns_to_types={"a": exp.DataType.build("int")}, - table_format=None, - storage_format=None, - partitioned_by=[], - partition_interval_unit=None, - clustered_by=[], - table_properties={}, - table_description=None, - column_descriptions=None, - ) + adapter_mock.create_table.assert_not_called() def test_create_view_non_deployable_snapshot(mocker: MockerFixture, adapter_mock, make_snapshot): @@ -1304,15 +1273,7 @@ def test_migrate_missing_table(mocker: MockerFixture, make_snapshot, make_mocked evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) - adapter.cursor.execute.assert_has_calls( - [ - call('CREATE TABLE "pre" ("a" INT)'), - call( - 'CREATE TABLE IF NOT EXISTS "sqlmesh__test_schema"."test_schema__test_model__1" AS SELECT "c" AS "c", "a" AS "a" FROM "tbl" AS "tbl" WHERE "ds" BETWEEN \'1970-01-01\' AND \'1970-01-01\' AND FALSE LIMIT 0' - ), - call('DROP TABLE "pre"'), - ] - ) + adapter.cursor.execute.assert_not_called() @pytest.mark.parametrize( @@ -1350,13 +1311,7 @@ def test_migrate_view( evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) - adapter.cursor.execute.assert_has_calls( - [ - call( - 'CREATE OR REPLACE VIEW "sqlmesh__test_schema"."test_schema__test_model__1" ("c", "a") AS SELECT "c" AS "c", "a" AS "a" FROM "tbl" AS "tbl"' - ) - ] - ) + adapter.cursor.execute.assert_not_called() def test_migrate_snapshot_data_object_type_mismatch( @@ -1369,7 +1324,7 @@ def test_migrate_snapshot_data_object_type_mismatch( adapter, "get_data_object", return_value=DataObject( - schema="sqlmesh__test_schema", name="test_schema__test_model__1", type="table" + schema="sqlmesh__test_schema", name="test_schema__test_model__1", type="view" ), ) mocker.patch.object(adapter, "table_exists", return_value=False) @@ -1378,22 +1333,20 @@ def test_migrate_snapshot_data_object_type_mismatch( model = SqlModel( name="test_schema.test_model", - kind=ViewKind(), + kind=FullKind(), storage_format="parquet", query=parse_one("SELECT c, a FROM tbl"), ) snapshot = make_snapshot(model, version="1") snapshot.change_category = SnapshotChangeCategory.BREAKING snapshot.forward_only = True + snapshot.previous_versions = snapshot.all_versions evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) adapter.cursor.execute.assert_has_calls( [ - call('DROP TABLE IF EXISTS "sqlmesh__test_schema"."test_schema__test_model__1"'), - call( - 'CREATE VIEW "sqlmesh__test_schema"."test_schema__test_model__1" AS SELECT "c" AS "c", "a" AS "a" FROM "tbl" AS "tbl"' - ), + call('DROP VIEW IF EXISTS "sqlmesh__test_schema"."test_schema__test_model__1"'), ] ) @@ -1404,6 +1357,7 @@ def test_evaluate_creation_duckdb( date_kwargs: t.Dict[str, str], ): evaluator = SnapshotEvaluator(create_engine_adapter(lambda: duck_conn, "duckdb")) + evaluator.create_physical_schemas([snapshot], DeployabilityIndex.all_deployable()) evaluator.create([snapshot], {}) version = snapshot.version @@ -1440,6 +1394,7 @@ def assert_tables_exist() -> None: def test_migrate_duckdb(snapshot: Snapshot, duck_conn, make_snapshot): evaluator = SnapshotEvaluator(create_engine_adapter(lambda: duck_conn, "duckdb")) + evaluator.create_physical_schemas([snapshot], DeployabilityIndex.all_deployable()) evaluator.create([snapshot], {}) updated_model_dict = snapshot.model.dict() @@ -1610,10 +1565,10 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) ), ] - evaluator.create([snapshot], {}) + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) adapter_mock.create_table.assert_called_once_with( - f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev__schema_migration_source", + f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev_schema_tmp", target_columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, table_format=None, storage_format=None, @@ -1634,61 +1589,17 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) adapter_mock.get_alter_expressions.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", - f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev__schema_migration_source", + f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev_schema_tmp", ignore_destructive=False, ) adapter_mock.alter_table.assert_called_once_with([]) adapter_mock.drop_table.assert_called_once_with( - f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev__schema_migration_source" + f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev_schema_tmp" ) -def test_create_clone_in_dev_missing_table(mocker: MockerFixture, adapter_mock, make_snapshot): - adapter_mock.SUPPORTS_CLONING = True - adapter_mock.get_alter_expressions.return_value = [] - evaluator = SnapshotEvaluator(adapter_mock) - - model = load_sql_based_model( - parse( # type: ignore - """ - MODEL ( - name test_schema.test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - ) - ); - - SELECT 1::INT as a, ds::DATE FROM a; - """ - ), - ) - - snapshot = make_snapshot(model) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) - snapshot.previous_versions = snapshot.all_versions - - evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) - - adapter_mock.create_table.assert_called_once_with( - f"sqlmesh__test_schema.test_schema__test_model__{snapshot.dev_version}__dev", - target_columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, - table_format=None, - storage_format=None, - partitioned_by=[exp.to_column("ds", quoted=True)], - partition_interval_unit=IntervalUnit.DAY, - clustered_by=[], - table_properties={}, - table_description=None, - column_descriptions=None, - ) - - adapter_mock.clone_table.assert_not_called() - adapter_mock.alter_table.assert_not_called() - - def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_mock, make_snapshot): adapter_mock.SUPPORTS_CLONING = True adapter_mock.get_alter_expressions.return_value = [] @@ -1724,7 +1635,7 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m ] with pytest.raises(SnapshotCreationFailedError): - evaluator.create([snapshot], {}) + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) adapter_mock.clone_table.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", @@ -1735,7 +1646,7 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m adapter_mock.get_alter_expressions.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", - f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev__schema_migration_source", + f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev_schema_tmp", ignore_destructive=False, ) @@ -1743,10 +1654,10 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m adapter_mock.drop_table.assert_has_calls( [ - call(f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev"), call( - f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev__schema_migration_source" + f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev_schema_tmp" ), + call(f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev"), ] ) @@ -1787,10 +1698,10 @@ def test_create_clone_in_dev_self_referencing( ), ] - evaluator.create([snapshot], {}) + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) adapter_mock.create_table.assert_called_once_with( - f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev__schema_migration_source", + f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev_schema_tmp", target_columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, table_format=None, storage_format=None, @@ -1802,16 +1713,16 @@ def test_create_clone_in_dev_self_referencing( column_descriptions=None, ) - # Make sure the dry run references the correct ("...__schema_migration_source") table. + # Make sure the dry run references the correct ("..._schema_tmp") table. table_alias = ( "test_model" if not use_this_model - else f"test_schema__test_model__{snapshot.version}__dev__schema_migration_source" + else f"test_schema__test_model__{snapshot.version}__dev_schema_tmp" ) dry_run_query = adapter_mock.fetchall.call_args[0][0].sql() assert ( dry_run_query - == f'SELECT CAST(1 AS INT) AS "a", CAST("ds" AS DATE) AS "ds" FROM "sqlmesh__test_schema"."test_schema__test_model__{snapshot.version}__dev__schema_migration_source" AS "{table_alias}" /* test_schema.test_model */ WHERE FALSE LIMIT 0' + == f'SELECT CAST(1 AS INT) AS "a", CAST("ds" AS DATE) AS "ds" FROM "sqlmesh__test_schema"."test_schema__test_model__{snapshot.version}__dev_schema_tmp" AS "{table_alias}" /* test_schema.test_model */ WHERE FALSE LIMIT 0' ) @@ -1919,7 +1830,7 @@ def test_forward_only_snapshot_for_added_model(mocker: MockerFixture, adapter_mo snapshot = make_snapshot(model) snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) - evaluator.create([snapshot], {}) + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) common_create_args = dict( target_columns_to_types={"a": exp.DataType.build("int"), "ds": exp.DataType.build("date")}, @@ -1963,7 +1874,7 @@ def test_create_scd_type_2_by_time(adapter_mock, make_snapshot): snapshot = make_snapshot(model) snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - evaluator.create([snapshot], {}) + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) common_kwargs = dict( target_columns_to_types={ @@ -1990,11 +1901,6 @@ def test_create_scd_type_2_by_time(adapter_mock, make_snapshot): column_descriptions=None, **common_kwargs, ), - call( - snapshot.table_name(), - column_descriptions={}, - **common_kwargs, - ), ] ) @@ -2021,7 +1927,7 @@ def test_create_ctas_scd_type_2_by_time(adapter_mock, make_snapshot): snapshot = make_snapshot(model) snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - evaluator.create([snapshot], {}) + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) query = parse_one( """SELECT *, CAST(NULL AS TIMESTAMPTZ) AS valid_from, CAST(NULL AS TIMESTAMPTZ) AS valid_to FROM "tbl" AS "tbl" WHERE FALSE LIMIT 0""" @@ -2047,7 +1953,6 @@ def test_create_ctas_scd_type_2_by_time(adapter_mock, make_snapshot): column_descriptions=None, **common_kwargs, ), - call(snapshot.table_name(), query, None, column_descriptions={}, **common_kwargs), ] ) @@ -2142,7 +2047,7 @@ def test_create_scd_type_2_by_column(adapter_mock, make_snapshot): snapshot = make_snapshot(model) snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - evaluator.create([snapshot], {}) + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) common_kwargs = dict( target_columns_to_types={ @@ -2167,7 +2072,6 @@ def test_create_scd_type_2_by_column(adapter_mock, make_snapshot): snapshot.table_name(is_deployable=False), **{**common_kwargs, "column_descriptions": None}, ), - call(snapshot.table_name(), **{**common_kwargs, "column_descriptions": {}}), ] ) @@ -2193,7 +2097,7 @@ def test_create_ctas_scd_type_2_by_column(adapter_mock, make_snapshot): snapshot = make_snapshot(model) snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - evaluator.create([snapshot], {}) + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) query = parse_one( """SELECT *, CAST(NULL AS TIMESTAMP) AS valid_from, CAST(NULL AS TIMESTAMP) AS valid_to FROM "tbl" AS "tbl" WHERE FALSE LIMIT 0""" @@ -2218,9 +2122,6 @@ def test_create_ctas_scd_type_2_by_column(adapter_mock, make_snapshot): None, **{**common_kwargs, "column_descriptions": None}, ), - call( - snapshot.table_name(), query, None, **{**common_kwargs, "column_descriptions": {}} - ), ] ) @@ -3152,14 +3053,7 @@ def create_log_table(evaluator, view_name): == f'CREATE INDEX IF NOT EXISTS "test_idx" ON "sqlmesh__test_schema"."test_schema__test_model__{snapshot.version}__dev" /* test_schema.test_model */("a")' ) - post_calls = call_args[3][0][0] - assert len(post_calls) == 1 - assert ( - post_calls[0].sql(dialect="postgres") - == f'CREATE INDEX IF NOT EXISTS "test_idx" ON "sqlmesh__test_schema"."test_schema__test_model__{snapshot.version}" /* test_schema.test_model */("a")' - ) - - on_virtual_update_calls = call_args[4][0][0] + on_virtual_update_calls = call_args[2][0][0] assert ( on_virtual_update_calls[0].sql(dialect="postgres") == 'GRANT SELECT ON VIEW "test_schema__test_env"."test_model" /* test_schema.test_model */ TO ROLE "admin"' @@ -3237,7 +3131,7 @@ def model_with_statements(context, **kwargs): ) call_args = adapter_mock.execute.call_args_list - on_virtual_update_call = call_args[4][0][0][0] + on_virtual_update_call = call_args[2][0][0][0] assert ( on_virtual_update_call.sql(dialect="postgres") == 'CREATE INDEX IF NOT EXISTS "idx" ON "db"."test_model_3" /* db.test_model_3 */("id")' @@ -3513,9 +3407,47 @@ def test_create_managed(adapter_mock, make_snapshot, mocker: MockerFixture): snapshot = make_snapshot(model) snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - evaluator.create([snapshot], {}) + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.all_deployable()) + + adapter_mock.create_managed_table.assert_called_with( + table_name=snapshot.table_name(), + query=mocker.ANY, + target_columns_to_types=model.columns_to_types, + partitioned_by=model.partitioned_by, + clustered_by=model.clustered_by, + table_properties=model.physical_properties, + table_description=model.description, + column_descriptions=model.column_descriptions, + table_format=None, + ) + + +def test_create_managed_dev(adapter_mock, make_snapshot, mocker: MockerFixture): + evaluator = SnapshotEvaluator(adapter_mock) + + model = load_sql_based_model( + parse( # type: ignore + """ + MODEL ( + name test_schema.test_model, + kind MANAGED, + physical_properties ( + warehouse = 'small', + target_lag = '10 minutes' + ), + clustered_by a + ); + + select a, b from foo; + """ + ) + ) + + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) - # first call to evaluation_strategy.create(), is_table_deployable=False triggers a normal table adapter_mock.ctas.assert_called_once_with( f"{snapshot.table_name()}__dev", mocker.ANY, @@ -3530,19 +3462,6 @@ def test_create_managed(adapter_mock, make_snapshot, mocker: MockerFixture): column_descriptions=None, ) - # second call to evaluation_strategy.create(), is_table_deployable=True and is_snapshot_deployable=True triggers a managed table - adapter_mock.create_managed_table.assert_called_with( - table_name=snapshot.table_name(), - query=mocker.ANY, - target_columns_to_types=model.columns_to_types, - partitioned_by=model.partitioned_by, - clustered_by=model.clustered_by, - table_properties=model.physical_properties, - table_description=model.description, - column_descriptions=model.column_descriptions, - table_format=None, - ) - def test_evaluate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): evaluator = SnapshotEvaluator(adapter_mock) @@ -3695,7 +3614,11 @@ def test_create_managed_forward_only_with_previous_version_doesnt_clone_for_dev_ ), ] - evaluator.create(target_snapshots=[snapshot], snapshots={}) + evaluator.create( + target_snapshots=[snapshot], + snapshots={}, + deployability_index=DeployabilityIndex.none_deployable(), + ) # We dont clone managed tables to create dev previews, we use normal tables adapter_mock.clone_table.assert_not_called() @@ -3707,114 +3630,6 @@ def test_create_managed_forward_only_with_previous_version_doesnt_clone_for_dev_ assert adapter_mock.ctas.call_args_list[0].args[0] == snapshot.table_name(is_deployable=False) -@pytest.mark.parametrize( - "deployability_index, snapshot_category, forward_only, deployability_flags", - [ - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, False, [True]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.NON_BREAKING, False, [True]), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.BREAKING, True, [False]), - ( - DeployabilityIndex.all_deployable(), - SnapshotChangeCategory.INDIRECT_BREAKING, - False, - [True], - ), - ( - DeployabilityIndex.all_deployable(), - SnapshotChangeCategory.INDIRECT_NON_BREAKING, - False, - [False], - ), - (DeployabilityIndex.all_deployable(), SnapshotChangeCategory.METADATA, False, [False]), - ( - DeployabilityIndex.none_deployable(), - SnapshotChangeCategory.BREAKING, - False, - [False, True], - ), - ( - DeployabilityIndex.none_deployable(), - SnapshotChangeCategory.NON_BREAKING, - False, - [False, True], - ), - ( - DeployabilityIndex.none_deployable(), - SnapshotChangeCategory.BREAKING, - True, - [False], - ), - ( - DeployabilityIndex.none_deployable(), - SnapshotChangeCategory.INDIRECT_BREAKING, - False, - [False, True], - ), - ( - DeployabilityIndex.none_deployable(), - SnapshotChangeCategory.INDIRECT_NON_BREAKING, - False, - [False], - ), - ( - DeployabilityIndex.none_deployable(), - SnapshotChangeCategory.METADATA, - False, - [False], - ), - ], -) -def test_create_snapshot( - snapshot: Snapshot, - mocker: MockerFixture, - adapter_mock, - deployability_index: DeployabilityIndex, - deployability_flags: t.List[bool], - snapshot_category: SnapshotChangeCategory, - forward_only: bool, -): - adapter_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter") - adapter_mock.dialect = "duckdb" - - evaluator = SnapshotEvaluator(adapter_mock) - snapshot.categorize_as(category=snapshot_category, forward_only=forward_only) - evaluator._create_snapshot( - snapshot=snapshot, - snapshots={}, - deployability_flags=deployability_flags, - deployability_index=deployability_index, - on_complete=None, - allow_destructive_snapshots=set(), - ) - - common_kwargs: t.Dict[str, t.Any] = dict( - target_columns_to_types={"a": exp.DataType.build("int")}, - table_format=None, - storage_format=None, - partitioned_by=[], - partition_interval_unit=None, - clustered_by=[], - table_properties={}, - table_description=None, - ) - - tables_created = [ - call( - snapshot.table_name(is_deployable=is_deployable), - column_descriptions=(None if not is_deployable else {}), - **common_kwargs, - ) - for is_deployable in deployability_flags - ] - - adapter_mock.create_table.assert_has_calls(tables_created) - - # Even if one or two (prod and dev) tables are created, the dry run should be conducted once - adapter_mock.fetchall.assert_called_once_with( - parse_one('SELECT CAST("a" AS INT) AS "a" FROM "tbl" AS "tbl" WHERE FALSE LIMIT 0') - ) - - def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_mock, make_snapshot): adapter_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter") adapter_mock.dialect = "duckdb" @@ -3838,7 +3653,6 @@ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_moc ) adapter_mock.drop_data_object_on_type_mismatch.return_value = False - evaluator.create([new_snapshot], {}) evaluator.migrate([new_snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) common_kwargs: t.Dict[str, t.Any] = dict( @@ -3860,7 +3674,7 @@ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_moc **common_kwargs, ), call( - new_snapshot.table_name(is_deployable=False), + f"{new_snapshot.table_name()}_schema_tmp", target_columns_to_types={ "a": exp.DataType.build("int"), "b": exp.DataType.build("int"), @@ -3886,7 +3700,7 @@ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_moc adapter_mock.get_alter_expressions.assert_called_once_with( snapshot.table_name(), - new_snapshot.table_name(is_deployable=False), + f"{new_snapshot.table_name()}_schema_tmp", ignore_destructive=False, ) @@ -3925,7 +3739,8 @@ def test_migrate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): adapter_mock.create_table.assert_not_called() adapter_mock.create_managed_table.assert_not_called() - adapter_mock.ctas.assert_not_called() + adapter_mock.ctas.assert_called_once() + adapter_mock.reset_mock() # schema changes - exception thrown adapter_mock.get_alter_expressions.return_value = [exp.Alter()] @@ -3945,7 +3760,7 @@ def test_migrate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): ) adapter_mock.create_table.assert_not_called() - adapter_mock.ctas.assert_not_called() + adapter_mock.ctas.assert_called_once() adapter_mock.create_managed_table.assert_not_called() @@ -4155,7 +3970,7 @@ def columns(table_name): # The second mock adapter has to be called only for the gateway-specific model adapter_mock.get_alter_expressions.assert_called_once_with( snapshot_2.table_name(True), - snapshot_2.table_name(False), + f"{snapshot_2.table_name(True)}_schema_tmp", ignore_destructive=False, ) diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index f34a1c6c74..44b6cd7911 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -939,11 +939,11 @@ def test_connection_args(tmp_path): dbt_project_dir = "tests/fixtures/dbt/sushi_test" config = sqlmesh_config(dbt_project_dir) - assert config.gateways["in_memory"].connection.register_comments - - config = sqlmesh_config(dbt_project_dir, register_comments=False) assert not config.gateways["in_memory"].connection.register_comments + config = sqlmesh_config(dbt_project_dir, register_comments=True) + assert config.gateways["in_memory"].connection.register_comments + def test_custom_dbt_loader(): from sqlmesh.core.loader import SqlMeshLoader diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index 3fb965f310..d69311fb3d 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -89,10 +89,17 @@ def test_linter( mock_pull_request.merged = False mock_pull_request.merge = mocker.MagicMock() + before_all = [ + "CREATE SCHEMA IF NOT EXISTS raw", + "DROP VIEW IF EXISTS raw.demographics", + "CREATE VIEW raw.demographics AS (SELECT 1 AS customer_id, '00000' AS zip)", + ] + # Case 1: Test for linter errors config = Config( model_defaults=ModelDefaultsConfig(dialect="duckdb"), linter=LinterConfig(enabled=True, rules="ALL"), + before_all=before_all, ) controller = make_controller( @@ -142,6 +149,7 @@ def test_linter( config = Config( model_defaults=ModelDefaultsConfig(dialect="duckdb"), linter=LinterConfig(enabled=True, warn_rules="ALL"), + before_all=before_all, ) controller = make_controller( diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index 586d8abb6d..ae0742f1db 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -82,7 +82,9 @@ def use_terminal_console(func): def test_wrapper(*args, **kwargs): orig_console = get_console() try: - set_console(TerminalConsole()) + new_console = TerminalConsole() + new_console.console.no_color = True + set_console(new_console) func(*args, **kwargs) finally: set_console(orig_console) From eb17e24471ee1e19814ebe5df58363defde5518d Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Wed, 20 Aug 2025 21:18:47 +0000 Subject: [PATCH 0736/1056] Feat!: print auto-restatement triggers in debug console (#4980) --- sqlmesh/core/console.py | 12 ++- sqlmesh/core/scheduler.py | 18 +++- sqlmesh/core/snapshot/definition.py | 48 +++++++---- tests/core/test_integration.py | 125 +++++++++++++++++++++++----- tests/core/test_snapshot.py | 114 ++++++++++++++++++++++++- web/server/console.py | 3 +- 6 files changed, 279 insertions(+), 41 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 43283ead90..3b9fce7f4e 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -428,6 +428,7 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: """Updates the snapshot evaluation progress.""" @@ -575,6 +576,7 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: pass @@ -1056,6 +1058,7 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: """Update the snapshot evaluation progress.""" if ( @@ -3639,6 +3642,7 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: view_name, loaded_batches = self.evaluation_batch_progress[snapshot.snapshot_id] @@ -3808,11 +3812,15 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: - message = f"Evaluating {snapshot.name} | batch={batch_idx} | duration={duration_ms}ms | num_audits_passed={num_audits_passed} | num_audits_failed={num_audits_failed}" + message = f"Evaluated {snapshot.name} | batch={batch_idx} | duration={duration_ms}ms | num_audits_passed={num_audits_passed} | num_audits_failed={num_audits_failed}" + + if auto_restatement_triggers: + message += f" | auto_restatement_triggers=[{', '.join(trigger.name for trigger in auto_restatement_triggers)}]" if audit_only: - message = f"Auditing {snapshot.name} duration={duration_ms}ms | num_audits_passed={num_audits_passed} | num_audits_failed={num_audits_failed}" + message = f"Audited {snapshot.name} | duration={duration_ms}ms | num_audits_passed={num_audits_passed} | num_audits_failed={num_audits_failed}" self._write(message) diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index e787e57a23..8096ffece1 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -415,6 +415,7 @@ def run_merged_intervals( selected_snapshot_ids: t.Optional[t.Set[SnapshotId]] = None, run_environment_statements: bool = False, audit_only: bool = False, + auto_restatement_triggers: t.Dict[SnapshotId, t.List[SnapshotId]] = {}, ) -> t.Tuple[t.List[NodeExecutionFailedError[SchedulingUnit]], t.List[SchedulingUnit]]: """Runs precomputed batches of missing intervals. @@ -531,6 +532,9 @@ def run_node(node: SchedulingUnit) -> None: evaluation_duration_ms, num_audits - num_audits_failed, num_audits_failed, + auto_restatement_triggers=auto_restatement_triggers.get( + snapshot.snapshot_id + ), ) elif isinstance(node, CreateNode): self.snapshot_evaluator.create_snapshot( @@ -736,8 +740,11 @@ def _run_or_audit( for s_id, interval in (remove_intervals or {}).items(): self.snapshots[s_id].remove_interval(interval) + all_auto_restatement_triggers: t.Dict[SnapshotId, t.List[SnapshotId]] = {} if auto_restatement_enabled: - auto_restated_intervals = apply_auto_restatements(self.snapshots, execution_time) + auto_restated_intervals, all_auto_restatement_triggers = apply_auto_restatements( + self.snapshots, execution_time + ) self.state_sync.add_snapshots_intervals(auto_restated_intervals) self.state_sync.update_auto_restatements( {s.name_version: s.next_auto_restatement_ts for s in self.snapshots.values()} @@ -758,6 +765,14 @@ def _run_or_audit( if not merged_intervals: return CompletionStatus.NOTHING_TO_DO + auto_restatement_triggers: t.Dict[SnapshotId, t.List[SnapshotId]] = {} + if all_auto_restatement_triggers: + merged_intervals_snapshots = {snapshot.snapshot_id for snapshot in merged_intervals} + auto_restatement_triggers = { + s_id: all_auto_restatement_triggers.get(s_id, []) + for s_id in merged_intervals_snapshots + } + errors, _ = self.run_merged_intervals( merged_intervals=merged_intervals, deployability_index=deployability_index, @@ -768,6 +783,7 @@ def _run_or_audit( end=end, run_environment_statements=run_environment_statements, audit_only=audit_only, + auto_restatement_triggers=auto_restatement_triggers, ) return CompletionStatus.FAILURE if errors else CompletionStatus.SUCCESS diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index ec5a883f7f..45740d9810 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -21,7 +21,7 @@ from sqlmesh.core.model import Model, ModelKindMixin, ModelKindName, ViewKind, CustomKind from sqlmesh.core.model.definition import _Model from sqlmesh.core.node import IntervalUnit, NodeType -from sqlmesh.utils import sanitize_name +from sqlmesh.utils import sanitize_name, unique from sqlmesh.utils.dag import DAG from sqlmesh.utils.date import ( TimeLike, @@ -2180,7 +2180,7 @@ def snapshots_to_dag(snapshots: t.Collection[Snapshot]) -> DAG[SnapshotId]: def apply_auto_restatements( snapshots: t.Dict[SnapshotId, Snapshot], execution_time: TimeLike -) -> t.List[SnapshotIntervals]: +) -> t.Tuple[t.List[SnapshotIntervals], t.Dict[SnapshotId, t.List[SnapshotId]]]: """Applies auto restatements to the snapshots. This operation results in the removal of intervals for snapshots that are ready to be restated based @@ -2195,6 +2195,7 @@ def apply_auto_restatements( A list of SnapshotIntervals with **new** intervals that need to be restated. """ dag = snapshots_to_dag(snapshots.values()) + auto_restatement_triggers: t.Dict[SnapshotId, t.List[SnapshotId]] = {} auto_restated_intervals_per_snapshot: t.Dict[SnapshotId, Interval] = {} for s_id in dag: if s_id not in snapshots: @@ -2209,6 +2210,7 @@ def apply_auto_restatements( for parent_s_id in snapshot.parents if parent_s_id in auto_restated_intervals_per_snapshot ] + upstream_triggers = [] if next_auto_restated_interval: logger.info( "Calculated the next auto restated interval (%s, %s) for snapshot %s", @@ -2218,6 +2220,18 @@ def apply_auto_restatements( ) auto_restated_intervals.append(next_auto_restated_interval) + # auto-restated snapshot is its own trigger + upstream_triggers = [s_id] + else: + # inherit each parent's auto-restatement triggers (if any) + for parent_s_id in snapshot.parents: + if parent_s_id in auto_restatement_triggers: + upstream_triggers.extend(auto_restatement_triggers[parent_s_id]) + + # remove duplicate triggers, retaining order and keeping first seen of duplicates + if upstream_triggers: + auto_restatement_triggers[s_id] = unique(upstream_triggers) + if auto_restated_intervals: auto_restated_interval_start = sys.maxsize auto_restated_interval_end = -sys.maxsize @@ -2247,20 +2261,22 @@ def apply_auto_restatements( snapshot.apply_pending_restatement_intervals() snapshot.update_next_auto_restatement_ts(execution_time) - - return [ - SnapshotIntervals( - name=snapshots[s_id].name, - identifier=None, - version=snapshots[s_id].version, - dev_version=None, - intervals=[], - dev_intervals=[], - pending_restatement_intervals=[interval], - ) - for s_id, interval in auto_restated_intervals_per_snapshot.items() - if s_id in snapshots - ] + return ( + [ + SnapshotIntervals( + name=snapshots[s_id].name, + identifier=None, + version=snapshots[s_id].version, + dev_version=None, + intervals=[], + dev_intervals=[], + pending_restatement_intervals=[interval], + ) + for s_id, interval in auto_restated_intervals_per_snapshot.items() + if s_id in snapshots + ], + auto_restatement_triggers, + ) def parent_snapshots_by_name( diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index fc129424f4..827d84e8b9 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -1862,6 +1862,82 @@ def test_select_unchanged_model_for_backfill(init_and_plan_context: t.Callable): assert {o.name for o in schema_objects} == {"waiter_revenue_by_day", "top_waiters"} +@time_machine.travel("2023-01-08 00:00:00 UTC") +def test_snapshot_triggers(init_and_plan_context: t.Callable, mocker: MockerFixture): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # auto-restatement triggers + orders = context.get_model("sushi.orders") + orders_kind = { + **orders.kind.dict(), + "auto_restatement_cron": "@hourly", + } + orders_kwargs = { + **orders.dict(), + "kind": orders_kind, + } + context.upsert_model(PythonModel.parse_obj(orders_kwargs)) + + order_items = context.get_model("sushi.order_items") + order_items_kind = { + **order_items.kind.dict(), + "auto_restatement_cron": "@hourly", + } + order_items_kwargs = { + **order_items.dict(), + "kind": order_items_kind, + } + context.upsert_model(PythonModel.parse_obj(order_items_kwargs)) + + waiter_revenue_by_day = context.get_model("sushi.waiter_revenue_by_day") + waiter_revenue_by_day_kind = { + **waiter_revenue_by_day.kind.dict(), + "auto_restatement_cron": "@hourly", + } + waiter_revenue_by_day_kwargs = { + **waiter_revenue_by_day.dict(), + "kind": waiter_revenue_by_day_kind, + } + context.upsert_model(SqlModel.parse_obj(waiter_revenue_by_day_kwargs)) + + context.plan(auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full()) + + scheduler = context.scheduler() + + import sqlmesh + + spy = mocker.spy(sqlmesh.core.scheduler.Scheduler, "run_merged_intervals") + + with time_machine.travel("2023-01-09 00:00:01 UTC"): + scheduler.run( + environment=c.PROD, + start="2023-01-01", + auto_restatement_enabled=True, + ) + + assert spy.called + + actual_triggers = spy.call_args.kwargs["auto_restatement_triggers"] + actual_triggers = {k: v for k, v in actual_triggers.items() if v} + assert len(actual_triggers) == 12 + + for id, trigger in actual_triggers.items(): + model_name = id.name.replace('"memory"."sushi".', "").replace('"', "") + auto_restatement_triggers = [ + t.name.replace('"memory"."sushi".', "").replace('"', "") for t in trigger + ] + + if model_name in ("orders", "order_items", "waiter_revenue_by_day"): + assert auto_restatement_triggers == [model_name] + elif model_name in ("customer_revenue_lifetime", "customer_revenue_by_day"): + assert sorted(auto_restatement_triggers) == sorted(["orders", "order_items"]) + elif model_name == "top_waiters": + assert auto_restatement_triggers == ["waiter_revenue_by_day"] + else: + assert auto_restatement_triggers == ["orders"] + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_max_interval_end_per_model_not_applied_when_end_is_provided( init_and_plan_context: t.Callable, @@ -6962,7 +7038,18 @@ def plan_with_output(ctx: Context, environment: str): assert "New environment `dev` will be created from `prod`" in output.stdout assert "Differences from the `prod` environment" in output.stdout - assert "Directly Modified: test__dev.a" in output.stdout + stdout_rstrip = "\n".join([line.rstrip() for line in output.stdout.split("\n")]) + assert ( + """MODEL ( + name test.a, ++ owner test, + kind FULL + ) + SELECT +- 5 AS col ++ 10 AS col""" + in stdout_rstrip + ) # Case 6: Ensure that target environment and create_from environment are not the same output = plan_with_output(ctx, "prod") @@ -7705,7 +7792,7 @@ def test_incremental_by_time_model_ignore_destructive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -7749,7 +7836,7 @@ def test_incremental_by_time_model_ignore_destructive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 2 as id, 3 as new_column, @@ -7803,9 +7890,9 @@ def test_incremental_by_time_model_ignore_destructive_change(tmp_path: Path): start '2023-01-01', cron '@daily' ); - + SELECT - *, + *, 2 as id, CAST(4 AS STRING) as new_column, @start_ds as ds @@ -7844,8 +7931,8 @@ def test_incremental_by_time_model_ignore_destructive_change(tmp_path: Path): start '2023-01-01', cron '@daily' ); - - SELECT + + SELECT *, 2 as id, CAST(5 AS STRING) as new_column, @@ -7905,7 +7992,7 @@ def test_incremental_by_unique_key_model_ignore_destructive_change(tmp_path: Pat cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -7949,7 +8036,7 @@ def test_incremental_by_unique_key_model_ignore_destructive_change(tmp_path: Pat cron '@daily' ); - SELECT + SELECT *, 2 as id, 3 as new_column, @@ -8016,7 +8103,7 @@ def test_incremental_unmanaged_model_ignore_destructive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -8059,7 +8146,7 @@ def test_incremental_unmanaged_model_ignore_destructive_change(tmp_path: Path): ); SELECT - *, + *, 2 as id, 3 as new_column, @start_ds as ds @@ -8240,7 +8327,7 @@ def test_scd_type_2_by_column_ignore_destructive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -8285,7 +8372,7 @@ def test_scd_type_2_by_column_ignore_destructive_change(tmp_path: Path): ); SELECT - *, + *, 1 as id, 3 as new_column, @start_ds as ds @@ -8352,7 +8439,7 @@ def test_incremental_partition_ignore_destructive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -8396,7 +8483,7 @@ def test_incremental_partition_ignore_destructive_change(tmp_path: Path): ); SELECT - *, + *, 1 as id, 3 as new_column, @start_ds as ds @@ -8467,7 +8554,7 @@ def test_incremental_by_time_model_ignore_destructive_change_unit_test(tmp_path: cron '@daily' ); - SELECT + SELECT id, name, ds @@ -8479,7 +8566,7 @@ def test_incremental_by_time_model_ignore_destructive_change_unit_test(tmp_path: (models_dir / "test_model.sql").write_text(initial_model) initial_test = f""" - + test_test_model: model: test_model inputs: @@ -8534,8 +8621,8 @@ def test_incremental_by_time_model_ignore_destructive_change_unit_test(tmp_path: start '2023-01-01', cron '@daily' ); - - SELECT + + SELECT id, new_column, ds diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index bce091595c..db61b9cabf 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -3102,7 +3102,7 @@ def test_apply_auto_restatements(make_snapshot): (to_timestamp("2020-01-01"), to_timestamp("2020-01-06")), ] - restated_intervals = apply_auto_restatements( + restated_intervals, _ = apply_auto_restatements( { snapshot_a.snapshot_id: snapshot_a, snapshot_b.snapshot_id: snapshot_b, @@ -3239,7 +3239,7 @@ def test_apply_auto_restatements_disable_restatement_downstream(make_snapshot): snapshot_b.add_interval("2020-01-01", "2020-01-05") assert snapshot_a.snapshot_id in snapshot_b.parents - restated_intervals = apply_auto_restatements( + restated_intervals, _ = apply_auto_restatements( { snapshot_a.snapshot_id: snapshot_a, snapshot_b.snapshot_id: snapshot_b, @@ -3279,6 +3279,116 @@ def test_apply_auto_restatements_disable_restatement_downstream(make_snapshot): ] +def test_auto_restatement_triggers(make_snapshot): + # Auto restatements: + # a, c, d + # dag: + # a -> b + # a -> c + # [b, c, d] -> e + model_a = SqlModel( + name="test_model_a", + kind=IncrementalByTimeRangeKind( + time_column=TimeColumn(column="ds"), + auto_restatement_cron="0 10 * * *", + auto_restatement_intervals=24, + ), + start="2020-01-01", + cron="@daily", + query=parse_one("SELECT 1 as ds"), + ) + snapshot_a = make_snapshot(model_a, version="1") + snapshot_a.add_interval("2020-01-01", "2020-01-05") + snapshot_a.next_auto_restatement_ts = to_timestamp("2020-01-06 10:00:00") + + model_b = SqlModel( + name="test_model_b", + kind=IncrementalByTimeRangeKind( + time_column=TimeColumn(column="ds"), + ), + start="2020-01-01", + cron="@daily", + query=parse_one("SELECT ds FROM test_model_a"), + ) + snapshot_b = make_snapshot(model_b, nodes={model_a.fqn: model_a}, version="1") + snapshot_b.add_interval("2020-01-01", "2020-01-05") + + model_c = SqlModel( + name="test_model_c", + kind=IncrementalByTimeRangeKind( + time_column=TimeColumn(column="ds"), + auto_restatement_cron="0 10 * * *", + auto_restatement_intervals=24, + ), + start="2020-01-01", + cron="@daily", + query=parse_one("SELECT ds FROM test_model_a"), + ) + snapshot_c = make_snapshot(model_c, nodes={model_a.fqn: model_a}, version="1") + snapshot_c.add_interval("2020-01-01", "2020-01-05") + snapshot_c.next_auto_restatement_ts = to_timestamp("2020-01-06 10:00:00") + + model_d = SqlModel( + name="test_model_d", + kind=IncrementalByTimeRangeKind( + time_column=TimeColumn(column="ds"), + auto_restatement_cron="0 10 * * *", + auto_restatement_intervals=24, + ), + start="2020-01-01", + cron="@daily", + query=parse_one("SELECT 1 as ds"), + ) + snapshot_d = make_snapshot(model_d, version="1") + snapshot_d.add_interval("2020-01-01", "2020-01-05") + snapshot_d.next_auto_restatement_ts = to_timestamp("2020-01-06 10:00:00") + + model_e = SqlModel( + name="test_model_e", + kind=IncrementalByTimeRangeKind( + time_column=TimeColumn(column="ds"), + ), + start="2020-01-01", + cron="@daily", + query=parse_one( + "SELECT ds from test_model_b UNION ALL SELECT ds from test_model_c UNION ALL SELECT ds from test_model_d" + ), + ) + snapshot_e = make_snapshot( + model_e, + nodes={ + model_a.fqn: model_a, + model_b.fqn: model_b, + model_c.fqn: model_c, + model_d.fqn: model_d, + }, + version="1", + ) + snapshot_e.add_interval("2020-01-01", "2020-01-05") + + _, auto_restatement_triggers = apply_auto_restatements( + { + snapshot_a.snapshot_id: snapshot_a, + snapshot_b.snapshot_id: snapshot_b, + snapshot_c.snapshot_id: snapshot_c, + snapshot_d.snapshot_id: snapshot_d, + snapshot_e.snapshot_id: snapshot_e, + }, + "2020-01-06 10:01:00", + ) + + assert auto_restatement_triggers[snapshot_a.snapshot_id] == [snapshot_a.snapshot_id] + assert auto_restatement_triggers[snapshot_c.snapshot_id] == [snapshot_c.snapshot_id] + assert auto_restatement_triggers[snapshot_d.snapshot_id] == [snapshot_d.snapshot_id] + assert auto_restatement_triggers[snapshot_b.snapshot_id] == [snapshot_a.snapshot_id] + # a via b, c and d directly + assert sorted(auto_restatement_triggers[snapshot_e.snapshot_id]) == [ + snapshot_a.snapshot_id, + snapshot_c.snapshot_id, + snapshot_d.snapshot_id, + ] + + def test_render_signal(make_snapshot, mocker): @signal() def check_types(batch, env: str, sql: list[SQL], table: exp.Table, default: int = 0): diff --git a/web/server/console.py b/web/server/console.py index 2cda0af697..902a85418c 100644 --- a/web/server/console.py +++ b/web/server/console.py @@ -9,7 +9,7 @@ from sqlmesh.core.console import TerminalConsole from sqlmesh.core.environment import EnvironmentNamingInfo from sqlmesh.core.plan.definition import EvaluatablePlan -from sqlmesh.core.snapshot import Snapshot, SnapshotInfoLike, SnapshotTableInfo +from sqlmesh.core.snapshot import Snapshot, SnapshotInfoLike, SnapshotTableInfo, SnapshotId from sqlmesh.core.test import ModelTest from sqlmesh.core.test.result import ModelTextTestResult from sqlmesh.utils.date import now_timestamp @@ -142,6 +142,7 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: if audit_only: return From 1e2760f9771c78d9b22ee11b25f1a625c92f1d5d Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:38:24 -0700 Subject: [PATCH 0737/1056] feat!: add on_additive_change support (#5193) --- docs/concepts/models/overview.md | 7 +- docs/concepts/plans.md | 20 +- docs/guides/incremental_time.md | 130 +- docs/guides/model_selection.md | 2 +- docs/integrations/dbt.md | 18 +- docs/reference/cli.md | 2 + docs/reference/model_configuration.md | 12 +- sqlmesh/cli/main.py | 8 + sqlmesh/core/config/model.py | 5 + sqlmesh/core/console.py | 52 +- sqlmesh/core/context.py | 13 + sqlmesh/core/engine_adapter/base.py | 34 +- sqlmesh/core/engine_adapter/bigquery.py | 70 +- sqlmesh/core/engine_adapter/clickhouse.py | 8 +- sqlmesh/core/engine_adapter/mixins.py | 103 +- sqlmesh/core/model/kind.py | 58 +- sqlmesh/core/model/meta.py | 6 + sqlmesh/core/plan/builder.py | 57 +- sqlmesh/core/plan/definition.py | 3 + sqlmesh/core/plan/evaluator.py | 3 + sqlmesh/core/scheduler.py | 7 + sqlmesh/core/schema_diff.py | 448 ++--- sqlmesh/core/snapshot/definition.py | 10 + sqlmesh/core/snapshot/evaluator.py | 121 +- sqlmesh/dbt/model.py | 38 +- .../migrations/v0091_on_additive_change.py | 5 + sqlmesh/utils/errors.py | 87 +- tests/conftest.py | 48 +- .../integration/test_integration.py | 151 +- .../integration/test_integration_bigquery.py | 45 +- .../integration/test_integration_snowflake.py | 20 +- tests/core/engine_adapter/test_base.py | 86 +- tests/core/engine_adapter/test_bigquery.py | 2 +- tests/core/engine_adapter/test_clickhouse.py | 4 +- tests/core/engine_adapter/test_postgres.py | 2 +- tests/core/engine_adapter/test_redshift.py | 6 +- tests/core/engine_adapter/test_spark.py | 2 +- tests/core/test_integration.py | 1090 +++++++++++- tests/core/test_model.py | 81 +- tests/core/test_plan.py | 713 +++++++- tests/core/test_plan_stages.py | 19 + tests/core/test_schema_diff.py | 1510 ++++++++++++----- tests/core/test_snapshot.py | 5 +- tests/core/test_snapshot_evaluator.py | 108 +- tests/dbt/test_config.py | 43 +- tests/dbt/test_transformation.py | 188 +- .../github/cicd/test_integration.py | 12 +- 47 files changed, 4362 insertions(+), 1100 deletions(-) create mode 100644 sqlmesh/migrations/v0091_on_additive_change.py diff --git a/docs/concepts/models/overview.md b/docs/concepts/models/overview.md index cf57678607..d6356462b4 100644 --- a/docs/concepts/models/overview.md +++ b/docs/concepts/models/overview.md @@ -513,9 +513,12 @@ Some properties are only available in specific model kinds - see the [model conf Must be one of the following values: `allow`, `warn`, `error` (default), or `ignore`. -!!! warning "Ignore is Dangerous" +### on_additive_change +: What should happen when a change to a [forward-only model](../../guides/incremental_time.md#forward-only-models) or incremental model in a [forward-only plan](../plans.md#forward-only-plans) causes an additive modification to the table schema (i.e., adding new columns, modifying column data types in compatible ways, ect.). - `ignore` is dangerous since it can result in error or data loss. It likely should never be used but could be useful as an "escape-hatch" or a way to workaround unexpected behavior. + SQLMesh checks for additive changes at plan time based on the model definition and run time based on the model's underlying physical tables. + + Must be one of the following values: `allow` (default), `warn`, `error`, or `ignore`. ### disable_restatement : Set this to true to indicate that [data restatement](../plans.md#restatement-plans) is disabled for this model. diff --git a/docs/concepts/plans.md b/docs/concepts/plans.md index 91616a6e7e..defcd06c0d 100644 --- a/docs/concepts/plans.md +++ b/docs/concepts/plans.md @@ -355,9 +355,25 @@ Some model changes destroy existing data in a table. SQLMesh automatically detec Forward-only plans treats all of the plan's model changes as forward-only. In these plans, SQLMesh will check all modified incremental models for destructive schema changes, not just forward-only models. -SQLMesh determines what to do for each model based on this setting hierarchy: the [model's `on_destructive_change` value](../guides/incremental_time.md#destructive-changes) (if present), the `on_destructive_change` [model defaults](../reference/model_configuration.md#model-defaults) value (if present), and the SQLMesh global default of `error`. +SQLMesh determines what to do for each model based on this setting hierarchy: -If you want to temporarily allow destructive changes to models that don't allow them, use the `plan` command's `--allow-destructive-model` selector to specify which models. Learn more about model selectors [here](../guides/model_selection.md). +- **For destructive changes**: the [model's `on_destructive_change` value](../guides/incremental_time.md#schema-changes) (if present), the `on_destructive_change` [model defaults](../reference/model_configuration.md#model-defaults) value (if present), and the SQLMesh global default of `error` +- **For additive changes**: the [model's `on_additive_change` value](../guides/incremental_time.md#schema-changes) (if present), the `on_additive_change` [model defaults](../reference/model_configuration.md#model-defaults) value (if present), and the SQLMesh global default of `allow` + +If you want to temporarily allow destructive changes to models that don't allow them, use the `plan` command's `--allow-destructive-model` selector to specify which models. +Similarly, if you want to temporarily allow additive changes to models configured with `on_additive_change=error`, use the `--allow-additive-model` selector. + +For example, to allow destructive changes to all models in the `analytics` schema: +```bash +sqlmesh plan --forward-only --allow-destructive-model "analytics.*" +``` + +Or to allow destructive changes to multiple specific models: +```bash +sqlmesh plan --forward-only --allow-destructive-model "sales.revenue_model" --allow-destructive-model "marketing.campaign_model" +``` + +Learn more about model selectors [here](../guides/model_selection.md). ### Effective date Changes that are part of the forward-only plan can also be applied retroactively to the production environment by specifying the effective date: diff --git a/docs/guides/incremental_time.md b/docs/guides/incremental_time.md index 2f54516ec4..8663ae9926 100644 --- a/docs/guides/incremental_time.md +++ b/docs/guides/incremental_time.md @@ -159,24 +159,49 @@ WHERE Alternatively, all the changes contained in a *specific plan* can be classified as forward-only with a flag: `sqlmesh plan --forward-only`. A subsequent plan that did not include the forward-only flag would fully refresh the model's physical table. Learn more about forward-only plans [here](../concepts/plans.md#forward-only-plans). -### Destructive changes +### Schema changes -Some model changes destroy existing data in a table. Dropping a column from the model is the most direct cause, but changing a column's data type (such as casting a column from a `STRING` to `INTEGER`) can also require a drop. (Whether or not a specific change requires dropping a column may differ across SQL engines.) +When SQLMesh processes forward-only changes to incremental models, it compares the model's new schema with the existing physical table schema to detect potential data loss or compatibility issues. SQLMesh categorizes schema changes into two types: -Forward-only models are used to retain existing data. Before executing forward-only changes to incremental models, SQLMesh performs a check to determine if existing data will be destroyed. +#### Destructive changes -The check is performed at plan time based on the model definition. SQLMesh may not be able to resolve all of a model's column data types and complete the check, so the check is performed again at run time based on the physical tables underlying the model. +Some model changes destroy existing data in a table. Examples include: + +- **Dropping a column** from the model +- **Renaming a column** +- **Modifying a column data type** in a ways that could cause data loss + +Whether a specific change is destructive may differ across SQL engines based on their schema evolution capabilities. + +#### Additive changes + +Additive changes are any changes to the table's columns that aren't categorized as destructive. A simple example would be adding a column to a table but another would be changing a column data type to a type that is compatible (ex: INT -> STRING). + +SQLMesh performs schema change detection at plan time based on the model definition. If SQLMesh cannot resolve all of a model's column data types at plan time, the check is performed again at run time based on the physical tables underlying the model. #### Changes to forward-only models -A model's `on_destructive_change` [configuration setting](../reference/model_configuration.md#incremental-models) determines what happens when SQLMesh detects a destructive change. +SQLMesh provides two configuration settings to control how schema changes are handled: + +- **`on_destructive_change`** - Controls behavior for destructive schema changes +- **`on_additive_change`** - Controls behavior for additive schema changes + +##### Configuration options + +Both properties support four values: -By default, SQLMesh will error so no data is lost. You can set `on_destructive_change` to `warn` or `allow` in the model's `MODEL` block to allow destructive changes. -`ignore` can be used to not perform the schema change and allow the table's definition to diverge from the model definition. +- **`error`** (default for `on_destructive_change`): Stop execution and raise an error +- **`warn`**: Log a warning but proceed with the change +- **`allow`** (default for `on_additive_change`): Silently proceed with the change +- **`ignore`**: Skip the schema change check entirely for this change type !!! warning "Ignore is Dangerous" - `ignore` is dangerous since it can result in error or data loss. It likely should never be used but could be useful as an "escape-hatch" or a way to workaround unexpected behavior. +`ignore` is dangerous since it can result in error or data loss. It likely should never be used but could be useful as an "escape-hatch" or a way to workaround unexpected behavior. + +##### Destructive change handling + +The `on_destructive_change` [configuration setting](../reference/model_configuration.md#incremental-models) determines what happens when SQLMesh detects a destructive change. By default, SQLMesh will error so no data is lost. This example configures a model to silently `allow` destructive changes: @@ -191,12 +216,93 @@ MODEL ( ); ``` -A default `on_destructive_change` value can be set for all incremental models that do not specify it themselves in the [model defaults configuration](../reference/model_configuration.md#model-defaults). +##### Additive change handling + +The `on_additive_change` configuration setting determines what happens when SQLMesh detects an additive change like adding new columns. By default, SQLMesh allows these changes since they don't destroy existing data. + +This example configures a model to raise an error for additive changes (useful for strict schema control): + +``` sql linenums="1" +MODEL ( + name sqlmesh_example.new_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column model_time_column, + forward_only true, + on_additive_change error + ), +); +``` + +##### Combining both settings + +You can configure both settings together to have fine-grained control over schema evolution: + +``` sql linenums="1" +MODEL ( + name sqlmesh_example.new_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column model_time_column, + forward_only true, + on_destructive_change warn, -- Warn but allow destructive changes + on_additive_change allow -- Silently allow new columns + ), +); +``` + +##### Model defaults + +Default values for both `on_destructive_change` and `on_additive_change` can be set for all incremental models in the [model defaults configuration](../reference/model_configuration.md#model-defaults). + +##### Common use cases + +Here are some common patterns for configuring schema change handling: + +**Strict schema control** - Prevent any schema changes: +```sql linenums="1" +MODEL ( + name sqlmesh_example.strict_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column event_date, + forward_only true, + on_destructive_change error, -- Block destructive changes + on_additive_change error -- Block even new columns + ), +); +``` + +**Permissive development model** - Allow all schema changes: +```sql linenums="1" +MODEL ( + name sqlmesh_example.dev_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column event_date, + forward_only true, + on_destructive_change allow, -- Allow dropping columns + on_additive_change allow -- Allow new columns (`allow` is the default value for this setting, so it can be omitted here) + ), +); +``` + +**Production safety** - Allow safe changes, warn about risky ones: +```sql linenums="1" +MODEL ( + name sqlmesh_example.production_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column event_date, + forward_only true, + on_destructive_change warn, -- Warn about destructive changes + on_additive_change allow -- Allow new columns (`allow` is the default value for this setting, so it can be omitted here) + ), +); +``` #### Changes in forward-only plans -The SQLMesh `plan` [`--forward-only` option](../concepts/plans.md#forward-only-plans) treats all the plan's model changes as forward-only. When this option is specified, SQLMesh will check all modified incremental models for destructive schema changes, not just models configured with `forward_only true`. +The SQLMesh `plan` [`--forward-only` option](../concepts/plans.md#forward-only-plans) treats all the plan's model changes as forward-only. When this option is specified, SQLMesh will check all modified incremental models for both destructive and additive schema changes, not just models configured with `forward_only true`. + +SQLMesh determines what to do for each model based on this setting hierarchy: -SQLMesh determines what to do for each model based on this setting hierarchy: the model's `on_destructive_change` value (if present), the `on_destructive_change` [model defaults](../reference/model_configuration.md#model-defaults) value (if present), and the SQLMesh global default of `error`. +- **For destructive changes**: the model's `on_destructive_change` value (if present), the `on_destructive_change` [model defaults](../reference/model_configuration.md#model-defaults) value (if present), and the SQLMesh global default of `error` +- **For additive changes**: the model's `on_additive_change` value (if present), the `on_additive_change` [model defaults](../reference/model_configuration.md#model-defaults) value (if present), and the SQLMesh global default of `allow` -If you want to temporarily allow destructive changes to models that don't allow them, use the `plan` command's [`--allow-destructive-model` selector](../concepts/plans.md#destructive-changes) to specify which models. Learn more about model selectors [here](../guides/model_selection.md). +If you want to temporarily allow destructive changes to models that don't allow them, use the `plan` command's [`--allow-destructive-model` selector](../concepts/plans.md#destructive-changes) to specify which models. Similarly, if you want to temporarily allow additive changes to models configured with `on_additive_change=error`, use the [`--allow-additive-model` selector](../concepts/plans.md#destructive-changes). Learn more about model selectors [here](../guides/model_selection.md). diff --git a/docs/guides/model_selection.md b/docs/guides/model_selection.md index db098a1538..9cc0a4358a 100644 --- a/docs/guides/model_selection.md +++ b/docs/guides/model_selection.md @@ -2,7 +2,7 @@ This guide describes how to select specific models to include in a SQLMesh plan, which can be useful when modifying a subset of the models in a SQLMesh project. -Note: the selector syntax described below is also used for the SQLMesh `plan` [`--allow-destructive-model` selector](../concepts/plans.md#destructive-changes) and for the `table_diff` command to [diff a selection of models](./tablediff.md#diffing-multiple-models-across-environments). +Note: the selector syntax described below is also used for the SQLMesh `plan` [`--allow-destructive-model` and `--allow-additive-model` selectors](../concepts/plans.md#destructive-changes) and for the `table_diff` command to [diff a selection of models](./tablediff.md#diffing-multiple-models-across-environments). ## Background diff --git a/docs/integrations/dbt.md b/docs/integrations/dbt.md index e58bde7776..c5e4bdd2d9 100644 --- a/docs/integrations/dbt.md +++ b/docs/integrations/dbt.md @@ -273,18 +273,18 @@ Similarly, the [allow_partials](../concepts/models/overview.md#allow_partials) p #### on_schema_change -SQLMesh automatically detects destructive schema changes to [forward-only incremental models](../guides/incremental_time.md#forward-only-models) and to all incremental models in [forward-only plans](../concepts/plans.md#destructive-changes). +SQLMesh automatically detects both destructive and additive schema changes to [forward-only incremental models](../guides/incremental_time.md#forward-only-models) and to all incremental models in [forward-only plans](../concepts/plans.md#destructive-changes). -A model's [`on_destructive_change` setting](../guides/incremental_time.md#destructive-changes) determines whether it errors (default), warns, or silently allows the changes. SQLMesh always allows non-destructive forward-only schema changes, such as adding or casting a column in place. +A model's [`on_destructive_change` and `on_additive_change` settings](../guides/incremental_time.md#schema-changes) determine whether it errors, warns, silently allows, or ignores the changes. SQLMesh provides fine-grained control over both destructive changes (like dropping columns) and additive changes (like adding new columns). -`on_schema_change` configuration values are mapped to these SQLMesh `on_destructive_change` values: +`on_schema_change` configuration values are mapped to these SQLMesh settings: -| `on_schema_change` | SQLMesh `on_destructive_change` | -| ------------------ | ------------------------------- | -| ignore | warn | -| append_new_columns | warn | -| sync_all_columns | allow | -| fail | error | +| `on_schema_change` | SQLMesh `on_destructive_change` | SQLMesh `on_additive_change` | +|--------------------|---------------------------------|------------------------------| +| ignore | ignore | ignore | +| fail | error | error | +| append_new_columns | ignore | allow | +| sync_all_columns | allow | allow | ## Snapshot support diff --git a/docs/reference/cli.md b/docs/reference/cli.md index b6877962ab..a9ce9366e1 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -367,6 +367,8 @@ Options: --forward-only Create a plan for forward-only changes. --allow-destructive-model TEXT Allow destructive forward-only changes to models whose names match the expression. + --allow-additive-model TEXT Allow additive forward-only changes to + models whose names match the expression. --effective-from TEXT The effective date from which to apply forward-only changes on production. --no-prompts Disable interactive prompts for the backfill diff --git a/docs/reference/model_configuration.md b/docs/reference/model_configuration.md index 6ea3dd68b6..a5a96ebbf9 100644 --- a/docs/reference/model_configuration.md +++ b/docs/reference/model_configuration.md @@ -186,6 +186,7 @@ The SQLMesh project-level `model_defaults` key supports the following options, d - virtual_properties - session_properties (on per key basis) - on_destructive_change (described [below](#incremental-models)) +- on_additive_change (described [below](#incremental-models)) - audits (described [here](../concepts/audits.md#generic-audits)) - optimize_query - allow_partials @@ -231,11 +232,12 @@ Python model kind `name` enum value: [ModelKindName.FULL](https://sqlmesh.readth Configuration options for all incremental models (in addition to [general model properties](#general-model-properties)). -| Option | Description | Type | Required | -|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----:|:--------:| -| `forward_only` | Whether the model's changes should always be classified as [forward-only](../concepts/plans.md#forward-only-change). (Default: `False`) | bool | N | -| `on_destructive_change` | What should happen when a change to a [forward-only model](../guides/incremental_time.md#forward-only-models) or incremental model in a [forward-only plan](../concepts/plans.md#forward-only-plans) causes a destructive modification to the model schema. Valid values: `allow`, `warn`, `error`. (Default: `error`) | str | N | -| `disable_restatement` | Whether [restatements](../concepts/plans.md#restatement-plans) should be disabled for the model. (Default: `False`) | bool | N | +| Option | Description | Type | Required | +|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----:|:--------:| +| `forward_only` | Whether the model's changes should always be classified as [forward-only](../concepts/plans.md#forward-only-change). (Default: `False`) | bool | N | +| `on_destructive_change` | What should happen when a change to a [forward-only model](../guides/incremental_time.md#forward-only-models) or incremental model in a [forward-only plan](../concepts/plans.md#forward-only-plans) causes a destructive modification to the model schema. Valid values: `allow`, `warn`, `error`, `ignore`. (Default: `error`) | str | N | +| `on_additive_change` | What should happen when a change to a [forward-only model](../guides/incremental_time.md#forward-only-models) or incremental model in a [forward-only plan](../concepts/plans.md#forward-only-plans) causes an additive modification to the model schema (like adding new columns). Valid values: `allow`, `warn`, `error`, `ignore`. (Default: `allow`) | str | N | +| `disable_restatement` | Whether [restatements](../concepts/plans.md#restatement-plans) should be disabled for the model. (Default: `False`) | bool | N | #### Incremental by time range diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 5d9a12f110..961b78069e 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -450,6 +450,12 @@ def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None: multiple=True, help="Allow destructive forward-only changes to models whose names match the expression.", ) +@click.option( + "--allow-additive-model", + type=str, + multiple=True, + help="Allow additive forward-only changes to models whose names match the expression.", +) @click.option( "--effective-from", type=str, @@ -548,6 +554,7 @@ def plan( restate_models = kwargs.pop("restate_model") or None select_models = kwargs.pop("select_model") or None allow_destructive_models = kwargs.pop("allow_destructive_model") or None + allow_additive_models = kwargs.pop("allow_additive_model") or None backfill_models = kwargs.pop("backfill_model") or None ignore_cron = kwargs.pop("ignore_cron") or None setattr(get_console(), "verbosity", Verbosity(verbose)) @@ -557,6 +564,7 @@ def plan( restate_models=restate_models, select_models=select_models, allow_destructive_models=allow_destructive_models, + allow_additive_models=allow_additive_models, backfill_models=backfill_models, ignore_cron=ignore_cron, **kwargs, diff --git a/sqlmesh/core/config/model.py b/sqlmesh/core/config/model.py index 3a6266928a..5406a5497b 100644 --- a/sqlmesh/core/config/model.py +++ b/sqlmesh/core/config/model.py @@ -10,6 +10,8 @@ OnDestructiveChange, model_kind_validator, on_destructive_change_validator, + on_additive_change_validator, + OnAdditiveChange, ) from sqlmesh.core.model.meta import FunctionCall from sqlmesh.core.node import IntervalUnit @@ -34,6 +36,7 @@ class ModelDefaultsConfig(BaseConfig): storage_format: The storage format used to store the physical table, only applicable in certain engines. (eg. 'parquet', 'orc') on_destructive_change: What should happen when a forward-only model requires a destructive schema change. + on_additive_change: What should happen when a forward-only model requires an additive schema change. physical_properties: A key-value mapping of arbitrary properties that are applied to the model table / view in the physical layer. virtual_properties: A key-value mapping of arbitrary properties that are applied to the model view in the virtual layer. session_properties: A key-value mapping of properties specific to the target engine that are applied to the engine session. @@ -56,6 +59,7 @@ class ModelDefaultsConfig(BaseConfig): table_format: t.Optional[str] = None storage_format: t.Optional[str] = None on_destructive_change: t.Optional[OnDestructiveChange] = None + on_additive_change: t.Optional[OnAdditiveChange] = None physical_properties: t.Optional[t.Dict[str, t.Any]] = None virtual_properties: t.Optional[t.Dict[str, t.Any]] = None session_properties: t.Optional[t.Dict[str, t.Any]] = None @@ -71,6 +75,7 @@ class ModelDefaultsConfig(BaseConfig): _model_kind_validator = model_kind_validator _on_destructive_change_validator = on_destructive_change_validator + _on_additive_change_validator = on_additive_change_validator @field_validator("audits", mode="before") def _audits_validator(cls, v: t.Any) -> t.Any: diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 3b9fce7f4e..e046e17630 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -27,6 +27,7 @@ from rich.tree import Tree from sqlglot import exp +from sqlmesh.core.schema_diff import TableAlterOperation from sqlmesh.core.test.result import ModelTextTestResult from sqlmesh.core.environment import EnvironmentNamingInfo, EnvironmentSummary from sqlmesh.core.linter.rule import RuleViolation @@ -47,6 +48,7 @@ PythonModelEvalError, NodeAuditsErrors, format_destructive_change_msg, + format_additive_change_msg, ) from sqlmesh.utils.rich import strip_ansi_codes @@ -327,13 +329,22 @@ class PlanBuilderConsole(BaseConsole, abc.ABC): def log_destructive_change( self, snapshot_name: str, - dropped_column_names: t.List[str], - alter_expressions: t.List[exp.Alter], + alter_operations: t.List[TableAlterOperation], dialect: str, error: bool = True, ) -> None: """Display a destructive change error or warning to the user.""" + @abc.abstractmethod + def log_additive_change( + self, + snapshot_name: str, + alter_operations: t.List[TableAlterOperation], + dialect: str, + error: bool = True, + ) -> None: + """Display an additive change error or warning to the user.""" + class UnitTestConsole(abc.ABC): @abc.abstractmethod @@ -759,8 +770,16 @@ def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None: def log_destructive_change( self, snapshot_name: str, - dropped_column_names: t.List[str], - alter_expressions: t.List[exp.Alter], + alter_operations: t.List[TableAlterOperation], + dialect: str, + error: bool = True, + ) -> None: + pass + + def log_additive_change( + self, + snapshot_name: str, + alter_operations: t.List[TableAlterOperation], dialect: str, error: bool = True, ) -> None: @@ -2202,22 +2221,29 @@ def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None: def log_destructive_change( self, snapshot_name: str, - dropped_column_names: t.List[str], - alter_expressions: t.List[exp.Alter], + alter_operations: t.List[TableAlterOperation], dialect: str, error: bool = True, ) -> None: if error: - self._print( - format_destructive_change_msg( - snapshot_name, dropped_column_names, alter_expressions, dialect - ) + self._print(format_destructive_change_msg(snapshot_name, alter_operations, dialect)) + else: + self.log_warning( + format_destructive_change_msg(snapshot_name, alter_operations, dialect, error) ) + + def log_additive_change( + self, + snapshot_name: str, + alter_operations: t.List[TableAlterOperation], + dialect: str, + error: bool = True, + ) -> None: + if error: + self._print(format_additive_change_msg(snapshot_name, alter_operations, dialect)) else: self.log_warning( - format_destructive_change_msg( - snapshot_name, dropped_column_names, alter_expressions, dialect, error - ) + format_additive_change_msg(snapshot_name, alter_operations, dialect, error) ) def log_error(self, message: str) -> None: diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 9022f3f069..1a5375183c 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1279,6 +1279,7 @@ def plan( empty_backfill: t.Optional[bool] = None, forward_only: t.Optional[bool] = None, allow_destructive_models: t.Optional[t.Collection[str]] = None, + allow_additive_models: t.Optional[t.Collection[str]] = None, no_prompts: t.Optional[bool] = None, auto_apply: t.Optional[bool] = None, no_auto_categorization: t.Optional[bool] = None, @@ -1322,6 +1323,7 @@ def plan( empty_backfill: Like skip_backfill, but also records processed intervals. forward_only: Whether the purpose of the plan is to make forward only changes. allow_destructive_models: Models whose forward-only changes are allowed to be destructive. + allow_additive_models: Models whose forward-only changes are allowed to be additive. no_prompts: Whether to disable interactive prompts for the backfill time range. Please note that if this flag is set to true and there are uncategorized changes the plan creation will fail. Default: False. @@ -1360,6 +1362,7 @@ def plan( empty_backfill=empty_backfill, forward_only=forward_only, allow_destructive_models=allow_destructive_models, + allow_additive_models=allow_additive_models, no_auto_categorization=no_auto_categorization, effective_from=effective_from, include_unmodified=include_unmodified, @@ -1411,6 +1414,7 @@ def plan_builder( empty_backfill: t.Optional[bool] = None, forward_only: t.Optional[bool] = None, allow_destructive_models: t.Optional[t.Collection[str]] = None, + allow_additive_models: t.Optional[t.Collection[str]] = None, no_auto_categorization: t.Optional[bool] = None, effective_from: t.Optional[TimeLike] = None, include_unmodified: t.Optional[bool] = None, @@ -1480,6 +1484,9 @@ def plan_builder( "allow_destructive_models": list(allow_destructive_models) if allow_destructive_models is not None else None, + "allow_additive_models": list(allow_additive_models) + if allow_additive_models is not None + else None, "no_auto_categorization": no_auto_categorization, "effective_from": effective_from, "include_unmodified": include_unmodified, @@ -1533,6 +1540,11 @@ def plan_builder( else: expanded_destructive_models = None + if allow_additive_models: + expanded_additive_models = model_selector.expand_model_selections(allow_additive_models) + else: + expanded_additive_models = None + if backfill_models: backfill_models = model_selector.expand_model_selections(backfill_models) else: @@ -1642,6 +1654,7 @@ def plan_builder( is_dev=is_dev, forward_only=forward_only, allow_destructive_models=expanded_destructive_models, + allow_additive_models=expanded_additive_models, environment_ttl=environment_ttl, environment_suffix_target=self.config.environment_suffix_target, environment_catalog_mapping=self.environment_catalog_mapping, diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 24ee99bba5..97342b28cc 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -39,7 +39,7 @@ set_catalog, ) from sqlmesh.core.model.kind import TimeColumn -from sqlmesh.core.schema_diff import SchemaDiffer +from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation from sqlmesh.utils import ( CorrelationId, columns_to_types_all_known, @@ -376,6 +376,13 @@ def _columns_to_types( target_columns_to_types = columns_to_types_from_df(t.cast(pd.DataFrame, query_or_df)) if not source_columns and target_columns_to_types: source_columns = list(target_columns_to_types) + # source columns should only contain columns that are defined in the target. If there are extras then + # that means they are intended to be ignored and will be excluded + source_columns = ( + [x for x in source_columns if x in target_columns_to_types] + if source_columns and target_columns_to_types + else None + ) return target_columns_to_types, source_columns def recycle(self) -> None: @@ -1074,32 +1081,39 @@ def _drop_object( """ self.execute(exp.Drop(this=exp.to_table(name), kind=kind, exists=exists, **drop_args)) - def get_alter_expressions( + def get_alter_operations( self, current_table_name: TableName, target_table_name: TableName, *, ignore_destructive: bool = False, - ) -> t.List[exp.Alter]: + ignore_additive: bool = False, + ) -> t.List[TableAlterOperation]: """ Determines the alter statements needed to change the current table into the structure of the target table. """ - return self.SCHEMA_DIFFER.compare_columns( - current_table_name, - self.columns(current_table_name), - self.columns(target_table_name), - ignore_destructive=ignore_destructive, + return t.cast( + t.List[TableAlterOperation], + self.SCHEMA_DIFFER.compare_columns( + current_table_name, + self.columns(current_table_name), + self.columns(target_table_name), + ignore_destructive=ignore_destructive, + ignore_additive=ignore_additive, + ), ) def alter_table( self, - alter_expressions: t.List[exp.Alter], + alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]], ) -> None: """ Performs the alter statements to change the current table into the structure of the target table. """ with self.transaction(): - for alter_expression in alter_expressions: + for alter_expression in [ + x.expression if isinstance(x, TableAlterOperation) else x for x in alter_expressions + ]: self.execute(alter_expression) def create_view( diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 4fe50fdeef..cab637fb36 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -12,6 +12,7 @@ InsertOverwriteWithMergeMixin, ClusteredByMixin, RowDiffMixin, + TableAlterClusterByOperation, ) from sqlmesh.core.engine_adapter.shared import ( CatalogSupport, @@ -21,7 +22,7 @@ set_catalog, ) from sqlmesh.core.node import IntervalUnit -from sqlmesh.core.schema_diff import SchemaDiffer +from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation from sqlmesh.utils import optional_import, get_source_columns_to_types from sqlmesh.utils.date import to_datetime from sqlmesh.utils.errors import SQLMeshError @@ -51,9 +52,6 @@ NestedField = t.Tuple[str, str, t.List[str]] NestedFieldsDict = t.Dict[str, t.List[NestedField]] -# used to tag AST nodes to be specially handled in alter_table() -_CLUSTERING_META_KEY = "__sqlmesh_update_table_clustering" - @set_catalog() class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin, ClusteredByMixin, RowDiffMixin): @@ -394,29 +392,31 @@ def create_mapping_schema( def alter_table( self, - alter_expressions: t.List[exp.Alter], + alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]], ) -> None: """ Performs the alter statements to change the current table into the structure of the target table, and uses the API to add columns to structs, where SQL is not supported. """ + if not alter_expressions: + return + + cluster_by_operations, alter_statements = [], [] + for e in alter_expressions: + if isinstance(e, TableAlterClusterByOperation): + cluster_by_operations.append(e) + elif isinstance(e, TableAlterOperation): + alter_statements.append(e.expression) + else: + alter_statements.append(e) - nested_fields, non_nested_expressions = self._split_alter_expressions(alter_expressions) + for op in cluster_by_operations: + self._update_clustering_key(op) - if nested_fields: - self._update_table_schema_nested_fields(nested_fields, alter_expressions[0].this) + nested_fields, non_nested_expressions = self._split_alter_expressions(alter_statements) - # this is easier than trying to detect exp.Cluster nodes - # or exp.Command nodes that contain the string "DROP CLUSTERING KEY" - clustering_change_operations = [ - e for e in non_nested_expressions if _CLUSTERING_META_KEY in e.meta - ] - for op in clustering_change_operations: - non_nested_expressions.remove(op) - table, cluster_by = op.meta[_CLUSTERING_META_KEY] - assert isinstance(table, str) or isinstance(table, exp.Table) - - self._update_clustering_key(table, cluster_by) + if nested_fields: + self._update_table_schema_nested_fields(nested_fields, alter_statements[0].this) if non_nested_expressions: super().alter_table(non_nested_expressions) @@ -1179,35 +1179,27 @@ def _get_data_objects( for row in df.itertuples() ] - def _change_clustering_key_expr( - self, table: exp.Table, cluster_by: t.List[exp.Expression] - ) -> exp.Alter: - expr = super()._change_clustering_key_expr(table=table, cluster_by=cluster_by) - expr.meta[_CLUSTERING_META_KEY] = (table, cluster_by) - return expr + def _update_clustering_key(self, operation: TableAlterClusterByOperation) -> None: + cluster_key_expressions = getattr(operation, "cluster_key_expressions", []) + bq_table = self._get_table(operation.target_table) - def _drop_clustering_key_expr(self, table: exp.Table) -> exp.Alter: - expr = super()._drop_clustering_key_expr(table=table) - expr.meta[_CLUSTERING_META_KEY] = (table, None) - return expr - - def _update_clustering_key( - self, table_name: TableName, cluster_by: t.Optional[t.List[exp.Expression]] - ) -> None: - cluster_by = cluster_by or [] - bq_table = self._get_table(table_name) - - rendered_columns = [c.sql(dialect=self.dialect) for c in cluster_by] + rendered_columns = [c.sql(dialect=self.dialect) for c in cluster_key_expressions] bq_table.clustering_fields = ( rendered_columns or None ) # causes a drop of the key if cluster_by is empty or None self._db_call(self.client.update_table, table=bq_table, fields=["clustering_fields"]) - if cluster_by: + if cluster_key_expressions: # BigQuery only applies new clustering going forward, so this rewrites the columns to apply the new clustering to historical data # ref: https://cloud.google.com/bigquery/docs/creating-clustered-tables#modifying-cluster-spec - self.execute(exp.update(table_name, {c: c for c in cluster_by}, where=exp.true())) + self.execute( + exp.update( + operation.target_table, + {c: c for c in cluster_key_expressions}, + where=exp.true(), + ) + ) def _normalize_decimal_value(self, col: exp.Expression, precision: int) -> exp.Expression: return exp.func("FORMAT", exp.Literal.string(f"%.{precision}f"), col) diff --git a/sqlmesh/core/engine_adapter/clickhouse.py b/sqlmesh/core/engine_adapter/clickhouse.py index 5ac4e9b152..523f77766a 100644 --- a/sqlmesh/core/engine_adapter/clickhouse.py +++ b/sqlmesh/core/engine_adapter/clickhouse.py @@ -15,7 +15,7 @@ CommentCreationView, InsertOverwriteStrategy, ) -from sqlmesh.core.schema_diff import SchemaDiffer +from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation from sqlmesh.utils import get_source_columns_to_types if t.TYPE_CHECKING: @@ -596,13 +596,15 @@ def delete_from(self, table_name: TableName, where: t.Union[str, exp.Expression] def alter_table( self, - alter_expressions: t.List[exp.Alter], + alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]], ) -> None: """ Performs the alter statements to change the current table into the structure of the target table. """ with self.transaction(): - for alter_expression in alter_expressions: + for alter_expression in [ + x.expression if isinstance(x, TableAlterOperation) else x for x in alter_expressions + ]: if self.engine_run_mode.is_cluster: alter_expression.set( "cluster", exp.OnCluster(this=exp.to_identifier(self.cluster)) diff --git a/sqlmesh/core/engine_adapter/mixins.py b/sqlmesh/core/engine_adapter/mixins.py index 12c9bfc603..e2d7915cf3 100644 --- a/sqlmesh/core/engine_adapter/mixins.py +++ b/sqlmesh/core/engine_adapter/mixins.py @@ -1,7 +1,9 @@ from __future__ import annotations +import abc import logging import typing as t +from dataclasses import dataclass from sqlglot import exp, parse_one from sqlglot.helper import seq_get @@ -10,6 +12,7 @@ from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, SourceQuery from sqlmesh.core.node import IntervalUnit from sqlmesh.core.dialect import schema_ +from sqlmesh.core.schema_diff import TableAlterOperation from sqlmesh.utils.errors import SQLMeshError if t.TYPE_CHECKING: @@ -340,35 +343,74 @@ def _build_create_table_exp( return statement -class ClusteredByMixin(EngineAdapter): - def _build_clustered_by_exp( - self, - clustered_by: t.List[exp.Expression], - **kwargs: t.Any, - ) -> t.Optional[exp.Cluster]: - return exp.Cluster(expressions=[c.copy() for c in clustered_by]) +@dataclass(frozen=True) +class TableAlterClusterByOperation(TableAlterOperation, abc.ABC): + pass + - def _parse_clustering_key(self, clustering_key: t.Optional[str]) -> t.List[exp.Expression]: - if not clustering_key: - return [] +@dataclass(frozen=True) +class TableAlterChangeClusterKeyOperation(TableAlterClusterByOperation): + clustering_key: str + dialect: str + @property + def is_additive(self) -> bool: + return False + + @property + def is_destructive(self) -> bool: + return False + + @property + def _alter_actions(self) -> t.List[exp.Expression]: + return [exp.Cluster(expressions=self.cluster_key_expressions)] + + @property + def cluster_key_expressions(self) -> t.List[exp.Expression]: # Note: Assumes `clustering_key` as a string like: # - "(col_a)" # - "(col_a, col_b)" # - "func(col_a, transform(col_b))" - parsed_cluster_key = parse_one(clustering_key, dialect=self.dialect) - + parsed_cluster_key = parse_one(self.clustering_key, dialect=self.dialect) return parsed_cluster_key.expressions or [parsed_cluster_key.this] - def get_alter_expressions( + +@dataclass(frozen=True) +class TableAlterDropClusterKeyOperation(TableAlterClusterByOperation): + @property + def is_additive(self) -> bool: + return False + + @property + def is_destructive(self) -> bool: + return False + + @property + def _alter_actions(self) -> t.List[exp.Expression]: + return [exp.Command(this="DROP", expression="CLUSTERING KEY")] + + +class ClusteredByMixin(EngineAdapter): + def _build_clustered_by_exp( + self, + clustered_by: t.List[exp.Expression], + **kwargs: t.Any, + ) -> t.Optional[exp.Cluster]: + return exp.Cluster(expressions=[c.copy() for c in clustered_by]) + + def get_alter_operations( self, current_table_name: TableName, target_table_name: TableName, *, ignore_destructive: bool = False, - ) -> t.List[exp.Alter]: - expressions = super().get_alter_expressions( - current_table_name, target_table_name, ignore_destructive=ignore_destructive + ignore_additive: bool = False, + ) -> t.List[TableAlterOperation]: + operations = super().get_alter_operations( + current_table_name, + target_table_name, + ignore_destructive=ignore_destructive, + ignore_additive=ignore_additive, ) # check for a change in clustering @@ -390,32 +432,17 @@ def get_alter_expressions( if target_table_info.clustering_key and ( current_table_info.clustering_key != target_table_info.clustering_key ): - expressions.append( - self._change_clustering_key_expr( - current_table, - self._parse_clustering_key(target_table_info.clustering_key), + operations.append( + TableAlterChangeClusterKeyOperation( + target_table=current_table, + clustering_key=target_table_info.clustering_key, + dialect=self.dialect, ) ) elif current_table_info.is_clustered: - expressions.append(self._drop_clustering_key_expr(current_table)) - - return expressions + operations.append(TableAlterDropClusterKeyOperation(target_table=current_table)) - def _change_clustering_key_expr( - self, table: exp.Table, cluster_by: t.List[exp.Expression] - ) -> exp.Alter: - return exp.Alter( - this=table, - kind="TABLE", - actions=[exp.Cluster(expressions=cluster_by)], - ) - - def _drop_clustering_key_expr(self, table: exp.Table) -> exp.Alter: - return exp.Alter( - this=table, - kind="TABLE", - actions=[exp.Command(this="DROP", expression="CLUSTERING KEY")], - ) + return operations def logical_merge( diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index 470ce92c20..6fbbc3534b 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -208,6 +208,31 @@ def is_ignore(self) -> bool: return self == OnDestructiveChange.IGNORE +class OnAdditiveChange(str, Enum): + """What should happen when a forward-only model change requires an additive schema change.""" + + ERROR = "ERROR" + WARN = "WARN" + ALLOW = "ALLOW" + IGNORE = "IGNORE" + + @property + def is_error(self) -> bool: + return self == OnAdditiveChange.ERROR + + @property + def is_warn(self) -> bool: + return self == OnAdditiveChange.WARN + + @property + def is_allow(self) -> bool: + return self == OnAdditiveChange.ALLOW + + @property + def is_ignore(self) -> bool: + return self == OnAdditiveChange.IGNORE + + def _on_destructive_change_validator( cls: t.Type, v: t.Union[OnDestructiveChange, str, exp.Identifier] ) -> t.Any: @@ -218,6 +243,20 @@ def _on_destructive_change_validator( return v +def _on_additive_change_validator( + cls: t.Type, v: t.Union[OnAdditiveChange, str, exp.Identifier] +) -> t.Any: + if v and not isinstance(v, OnAdditiveChange): + return OnAdditiveChange( + v.this.upper() if isinstance(v, (exp.Identifier, exp.Literal)) else v.upper() + ) + return v + + +on_additive_change_validator = field_validator("on_additive_change", mode="before")( + _on_additive_change_validator +) + on_destructive_change_validator = field_validator("on_destructive_change", mode="before")( _on_destructive_change_validator ) @@ -336,15 +375,18 @@ def _kind_dialect_validator(cls: t.Type, v: t.Optional[str]) -> str: class _Incremental(_ModelKind): on_destructive_change: OnDestructiveChange = OnDestructiveChange.ERROR + on_additive_change: OnAdditiveChange = OnAdditiveChange.ALLOW auto_restatement_cron: t.Optional[SQLGlotCron] = None _on_destructive_change_validator = on_destructive_change_validator + _on_additive_change_validator = on_additive_change_validator @property def metadata_hash_values(self) -> t.List[t.Optional[str]]: return [ *super().metadata_hash_values, str(self.on_destructive_change), + str(self.on_additive_change), self.auto_restatement_cron, ] @@ -357,6 +399,7 @@ def to_expression( *_properties( { "on_destructive_change": self.on_destructive_change.value, + "on_additive_change": self.on_additive_change.value, "auto_restatement_cron": self.auto_restatement_cron, } ), @@ -1001,14 +1044,15 @@ def create_model_kind(v: t.Any, dialect: str, defaults: t.Dict[str, t.Any]) -> M if "dialect" in kind_type.all_fields() and props.get("dialect") is None: props["dialect"] = dialect - # only pass the on_destructive_change user default to models inheriting from _Incremental + # only pass the on_destructive_change or on_additive_change user default to models inheriting from _Incremental # that don't explicitly set it in the model definition - if ( - issubclass(kind_type, _Incremental) - and props.get("on_destructive_change") is None - and defaults.get("on_destructive_change") is not None - ): - props["on_destructive_change"] = defaults.get("on_destructive_change") + if issubclass(kind_type, _Incremental): + for on_change_property in ("on_additive_change", "on_destructive_change"): + if ( + props.get(on_change_property) is None + and defaults.get(on_change_property) is not None + ): + props[on_change_property] = defaults.get(on_change_property) if kind_type == CustomKind: # load the custom materialization class and check if it uses a custom kind type diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 2f24349a72..088398c388 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -31,6 +31,7 @@ ViewKind, _IncrementalBy, model_kind_validator, + OnAdditiveChange, ) from sqlmesh.core.node import _Node, str_or_exp_to_str from sqlmesh.core.reference import Reference @@ -517,6 +518,11 @@ def fqn(self) -> str: def on_destructive_change(self) -> OnDestructiveChange: return getattr(self.kind, "on_destructive_change", OnDestructiveChange.ALLOW) + @property + def on_additive_change(self) -> OnAdditiveChange: + """Return the model's additive change setting if it has one.""" + return getattr(self.kind, "on_additive_change", OnAdditiveChange.ALLOW) + @property def ignored_rules(self) -> t.Set[str]: return self.ignored_rules_ or set() diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 9451f9eb53..a48812d16c 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -26,7 +26,8 @@ from sqlmesh.core.schema_diff import ( get_schema_differ, has_drop_alteration, - get_dropped_column_names, + has_additive_alteration, + TableAlterOperation, ) from sqlmesh.core.snapshot import ( DeployabilityIndex, @@ -73,6 +74,7 @@ class PlanBuilder: is_dev: Whether this plan is for development purposes. forward_only: Whether the purpose of the plan is to make forward only changes. allow_destructive_models: A list of fully qualified model names whose forward-only changes are allowed to be destructive. + allow_additive_models: A list of fully qualified model names whose forward-only changes are allowed to be additive. environment_ttl: The period of time that a development environment should exist before being deleted. categorizer_config: Auto categorization settings. auto_categorization_enabled: Whether to apply auto categorization. @@ -108,6 +110,7 @@ def __init__( is_dev: bool = False, forward_only: bool = False, allow_destructive_models: t.Optional[t.Iterable[str]] = None, + allow_additive_models: t.Optional[t.Iterable[str]] = None, environment_ttl: t.Optional[str] = None, environment_suffix_target: EnvironmentSuffixTarget = EnvironmentSuffixTarget.default, environment_catalog_mapping: t.Optional[t.Dict[re.Pattern, str]] = None, @@ -136,6 +139,9 @@ def __init__( self._allow_destructive_models = set( allow_destructive_models if allow_destructive_models is not None else [] ) + self._allow_additive_models = set( + allow_additive_models if allow_additive_models is not None else [] + ) self._enable_preview = enable_preview self._end_bounded = end_bounded self._ensure_finalized_snapshots = ensure_finalized_snapshots @@ -279,7 +285,7 @@ def build(self) -> Plan: dag = self._build_dag() directly_modified, indirectly_modified = self._build_directly_and_indirectly_modified(dag) - self._check_destructive_changes(directly_modified) + self._check_destructive_additive_changes(directly_modified) self._categorize_snapshots(dag, indirectly_modified) self._adjust_snapshot_intervals() @@ -323,6 +329,7 @@ def build(self) -> Plan: forward_only=self._forward_only, explain=self._explain, allow_destructive_models=t.cast(t.Set, self._allow_destructive_models), + allow_additive_models=t.cast(t.Set, self._allow_additive_models), include_unmodified=self._include_unmodified, environment_ttl=self._environment_ttl, environment_naming_info=self.environment_naming_info, @@ -531,16 +538,20 @@ def _adjust_snapshot_intervals(self) -> None: if new.is_forward_only: new.dev_intervals = new.intervals.copy() - def _check_destructive_changes(self, directly_modified: t.Set[SnapshotId]) -> None: + def _check_destructive_additive_changes(self, directly_modified: t.Set[SnapshotId]) -> None: for s_id in sorted(directly_modified): if s_id.name not in self._context_diff.modified_snapshots: continue snapshot = self._context_diff.snapshots[s_id] + needs_destructive_check = snapshot.needs_destructive_check( + self._allow_destructive_models + ) + needs_additive_check = snapshot.needs_additive_check(self._allow_additive_models) # should we raise/warn if this snapshot has/inherits a destructive change? - should_raise_or_warn = ( - self._is_forward_only_change(s_id) or self._forward_only - ) and snapshot.needs_destructive_check(self._allow_destructive_models) + should_raise_or_warn = (self._is_forward_only_change(s_id) or self._forward_only) and ( + needs_destructive_check or needs_additive_check + ) if not should_raise_or_warn or not snapshot.is_model: continue @@ -554,22 +565,24 @@ def _check_destructive_changes(self, directly_modified: t.Set[SnapshotId]) -> No if columns_to_types_all_known(old_columns_to_types) and columns_to_types_all_known( new_columns_to_types ): - schema_diff = get_schema_differ(snapshot.model.dialect).compare_columns( - new.name, - old_columns_to_types, - new_columns_to_types, - ignore_destructive=new.model.on_destructive_change.is_ignore, + alter_operations = t.cast( + t.List[TableAlterOperation], + get_schema_differ(snapshot.model.dialect).compare_columns( + new.name, + old_columns_to_types, + new_columns_to_types, + ignore_destructive=new.model.on_destructive_change.is_ignore, + ignore_additive=new.model.on_additive_change.is_ignore, + ), ) - if has_drop_alteration(schema_diff): - snapshot_name = snapshot.name - dropped_column_names = get_dropped_column_names(schema_diff) - model_dialect = snapshot.model.dialect + snapshot_name = snapshot.name + model_dialect = snapshot.model.dialect + if needs_destructive_check and has_drop_alteration(alter_operations): self._console.log_destructive_change( snapshot_name, - dropped_column_names, - schema_diff, + alter_operations, model_dialect, error=not snapshot.model.on_destructive_change.is_warn, ) @@ -578,6 +591,16 @@ def _check_destructive_changes(self, directly_modified: t.Set[SnapshotId]) -> No "Plan requires a destructive change to a forward-only model." ) + if needs_additive_check and has_additive_alteration(alter_operations): + self._console.log_additive_change( + snapshot_name, + alter_operations, + model_dialect, + error=not snapshot.model.on_additive_change.is_warn, + ) + if snapshot.model.on_additive_change.is_error: + raise PlanError("Plan requires an additive change to a forward-only model.") + def _categorize_snapshots( self, dag: DAG[SnapshotId], indirectly_modified: SnapshotMapping ) -> None: diff --git a/sqlmesh/core/plan/definition.py b/sqlmesh/core/plan/definition.py index 300ac62faf..2f3ddb5990 100644 --- a/sqlmesh/core/plan/definition.py +++ b/sqlmesh/core/plan/definition.py @@ -44,6 +44,7 @@ class Plan(PydanticModel, frozen=True): no_gaps: bool forward_only: bool allow_destructive_models: t.Set[str] + allow_additive_models: t.Set[str] include_unmodified: bool end_bounded: bool ensure_finalized_snapshots: bool @@ -258,6 +259,7 @@ def to_evaluatable(self) -> EvaluatablePlan: restatements={s.name: i for s, i in self.restatements.items()}, is_dev=self.is_dev, allow_destructive_models=self.allow_destructive_models, + allow_additive_models=self.allow_additive_models, forward_only=self.forward_only, end_bounded=self.end_bounded, ensure_finalized_snapshots=self.ensure_finalized_snapshots, @@ -300,6 +302,7 @@ class EvaluatablePlan(PydanticModel): restatements: t.Dict[str, Interval] is_dev: bool allow_destructive_models: t.Set[str] + allow_additive_models: t.Set[str] forward_only: bool end_bounded: bool ensure_finalized_snapshots: bool diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 46142b7eeb..298d18a042 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -179,6 +179,7 @@ def visit_physical_layer_update_stage( snapshots_to_create, stage.all_snapshots, allow_destructive_snapshots=plan.allow_destructive_models, + allow_additive_snapshots=plan.allow_additive_models, deployability_index=stage.deployability_index, on_start=lambda x: self.console.start_creation_progress( x, plan.environment, self.default_catalog @@ -254,6 +255,7 @@ def visit_backfill_stage(self, stage: stages.BackfillStage, plan: EvaluatablePla start=plan.start, end=plan.end, allow_destructive_snapshots=plan.allow_destructive_models, + allow_additive_snapshots=plan.allow_additive_models, selected_snapshot_ids=stage.selected_snapshot_ids, ) if errors: @@ -322,6 +324,7 @@ def visit_migrate_schemas_stage( stage.snapshots, stage.all_snapshots, allow_destructive_snapshots=plan.allow_destructive_models, + allow_additive_snapshots=plan.allow_additive_models, deployability_index=stage.deployability_index, ) except NodeExecutionFailedError as ex: diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index 8096ffece1..2cbf769ea2 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -197,6 +197,7 @@ def evaluate( batch_index: int, environment_naming_info: t.Optional[EnvironmentNamingInfo] = None, allow_destructive_snapshots: t.Optional[t.Set[str]] = None, + allow_additive_snapshots: t.Optional[t.Set[str]] = None, target_table_exists: t.Optional[bool] = None, **kwargs: t.Any, ) -> t.List[AuditResult]: @@ -208,6 +209,7 @@ def evaluate( end: The end datetime to render. execution_time: The date/time time reference to use for execution time. Defaults to now. allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed. + allow_additive_snapshots: Snapshots for which additive schema changes are allowed. deployability_index: Determines snapshots that are deployable in the context of this evaluation. batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it auto_restatement_enabled: Whether to enable auto restatements. @@ -230,6 +232,7 @@ def evaluate( execution_time=execution_time, snapshots=snapshots, allow_destructive_snapshots=allow_destructive_snapshots, + allow_additive_snapshots=allow_additive_snapshots, deployability_index=deployability_index, batch_index=batch_index, target_table_exists=target_table_exists, @@ -412,6 +415,7 @@ def run_merged_intervals( start: t.Optional[TimeLike] = None, end: t.Optional[TimeLike] = None, allow_destructive_snapshots: t.Optional[t.Set[str]] = None, + allow_additive_snapshots: t.Optional[t.Set[str]] = None, selected_snapshot_ids: t.Optional[t.Set[SnapshotId]] = None, run_environment_statements: bool = False, audit_only: bool = False, @@ -428,6 +432,7 @@ def run_merged_intervals( start: The start of the run. end: The end of the run. allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed. + allow_additive_snapshots: Snapshots for which additive schema changes are allowed. selected_snapshot_ids: The snapshots to include in the run DAG. If None, all snapshots with missing intervals will be included. Returns: @@ -518,6 +523,7 @@ def run_node(node: SchedulingUnit) -> None: deployability_index=deployability_index, batch_index=node.batch_index, allow_destructive_snapshots=allow_destructive_snapshots, + allow_additive_snapshots=allow_additive_snapshots, target_table_exists=snapshot.snapshot_id not in snapshots_to_create, ) @@ -542,6 +548,7 @@ def run_node(node: SchedulingUnit) -> None: snapshots=self.snapshots_by_name, deployability_index=deployability_index, allow_destructive_snapshots=allow_destructive_snapshots or set(), + allow_additive_snapshots=allow_additive_snapshots or set(), ) try: diff --git a/sqlmesh/core/schema_diff.py b/sqlmesh/core/schema_diff.py index 1bf2f76672..0bbc146c17 100644 --- a/sqlmesh/core/schema_diff.py +++ b/sqlmesh/core/schema_diff.py @@ -1,8 +1,9 @@ from __future__ import annotations +import abc import logging import typing as t -from enum import Enum, auto +from dataclasses import dataclass from collections import defaultdict from pydantic import Field from sqlglot import exp @@ -10,6 +11,7 @@ from sqlmesh.utils import columns_to_types_to_struct from sqlmesh.utils.pydantic import PydanticModel +from sqlmesh.utils.errors import SQLMeshError if t.TYPE_CHECKING: from sqlmesh.core._typing import TableName @@ -17,25 +19,140 @@ logger = logging.getLogger(__name__) -class TableAlterOperationType(Enum): - ADD = auto() - DROP = auto() - ALTER_TYPE = auto() +@dataclass(frozen=True) +class TableAlterOperation(abc.ABC): + target_table: exp.Table @property - def is_add(self) -> bool: - return self == TableAlterOperationType.ADD + @abc.abstractmethod + def is_destructive(self) -> bool: + pass @property - def is_drop(self) -> bool: - return self == TableAlterOperationType.DROP + @abc.abstractmethod + def is_additive(self) -> bool: + pass @property - def is_alter_type(self) -> bool: - return self == TableAlterOperationType.ALTER_TYPE + @abc.abstractmethod + def _alter_actions(self) -> t.List[exp.Expression]: + pass + + @property + def expression(self) -> exp.Alter: + return exp.Alter( + this=self.target_table, + kind="TABLE", + actions=self._alter_actions, + ) + + +@dataclass(frozen=True) +class TableAlterColumnOperation(TableAlterOperation, abc.ABC): + column_parts: t.List[TableAlterColumn] + expected_table_struct: exp.DataType + array_element_selector: str + + @property + def column_identifiers(self) -> t.List[exp.Identifier]: + results = [] + for column in self.column_parts: + results.append(column.identifier) + if ( + column.is_array_of_struct + and len(self.column_parts) > 1 + and self.array_element_selector + ): + results.append(exp.to_identifier(self.array_element_selector)) + return results + + @property + def column(self) -> t.Union[exp.Dot, exp.Identifier]: + columns = self.column_identifiers + if len(columns) == 1: + return columns[0] + return exp.Dot.build(columns) + + +@dataclass(frozen=True) +class TableAlterTypedColumnOperation(TableAlterColumnOperation, abc.ABC): + column_type: exp.DataType + + @property + def column_def(self) -> exp.ColumnDef: + if not self.column_type: + raise SQLMeshError("Tried to access column type when it shouldn't be needed") + return exp.ColumnDef( + this=self.column, + kind=self.column_type, + ) + + +@dataclass(frozen=True) +class TableAlterAddColumnOperation(TableAlterTypedColumnOperation): + position: t.Optional[TableAlterColumnPosition] = None + is_part_of_destructive_change: bool = False + + @property + def is_additive(self) -> bool: + return not self.is_part_of_destructive_change + + @property + def is_destructive(self) -> bool: + return self.is_part_of_destructive_change + + @property + def _alter_actions(self) -> t.List[exp.Expression]: + column_def = exp.ColumnDef( + this=self.column, + kind=self.column_type, + ) + if self.position: + column_def.set("position", self.position.column_position_node) + return [column_def] -class TableAlterColumn(PydanticModel): +@dataclass(frozen=True) +class TableAlterDropColumnOperation(TableAlterColumnOperation): + cascade: bool = False + + @property + def is_additive(self) -> bool: + return False + + @property + def is_destructive(self) -> bool: + return True + + @property + def _alter_actions(self) -> t.List[exp.Expression]: + return [exp.Drop(this=self.column, kind="COLUMN", cascade=self.cascade)] + + +@dataclass(frozen=True) +class TableAlterChangeColumnTypeOperation(TableAlterTypedColumnOperation): + current_type: exp.DataType + + @property + def is_additive(self) -> bool: + return True + + @property + def is_destructive(self) -> bool: + return False + + @property + def _alter_actions(self) -> t.List[exp.Expression]: + return [ + exp.AlterColumn( + this=self.column, + dtype=self.column_type, + ) + ] + + +@dataclass(frozen=True) +class TableAlterColumn: name: str is_struct: bool is_array_of_struct: bool @@ -115,7 +232,8 @@ def identifier(self) -> exp.Identifier: return exp.to_identifier(self.name, quoted=self.quoted) -class TableAlterColumnPosition(PydanticModel): +@dataclass(frozen=True) +class TableAlterColumnPosition: is_first: bool is_last: bool after: t.Optional[exp.Identifier] = None @@ -160,129 +278,6 @@ def column_position_node(self) -> t.Optional[exp.ColumnPosition]: return exp.ColumnPosition(this=column, position=position) -class TableAlterOperation(PydanticModel): - op: TableAlterOperationType - columns: t.List[TableAlterColumn] - column_type: exp.DataType - expected_table_struct: exp.DataType - add_position: t.Optional[TableAlterColumnPosition] = None - current_type: t.Optional[exp.DataType] = None - cascade: bool = False - - @classmethod - def add( - cls, - columns: t.Union[TableAlterColumn, t.List[TableAlterColumn]], - column_type: t.Union[str, exp.DataType], - expected_table_struct: t.Union[str, exp.DataType], - position: t.Optional[TableAlterColumnPosition] = None, - ) -> TableAlterOperation: - return cls( - op=TableAlterOperationType.ADD, - columns=ensure_list(columns), - column_type=exp.DataType.build(column_type), - add_position=position, - expected_table_struct=exp.DataType.build(expected_table_struct), - ) - - @classmethod - def drop( - cls, - columns: t.Union[TableAlterColumn, t.List[TableAlterColumn]], - expected_table_struct: t.Union[str, exp.DataType], - column_type: t.Optional[t.Union[str, exp.DataType]] = None, - cascade: bool = False, - ) -> TableAlterOperation: - column_type = exp.DataType.build(column_type) if column_type else exp.DataType.build("INT") - return cls( - op=TableAlterOperationType.DROP, - columns=ensure_list(columns), - column_type=column_type, - expected_table_struct=exp.DataType.build(expected_table_struct), - cascade=cascade, - ) - - @classmethod - def alter_type( - cls, - columns: t.Union[TableAlterColumn, t.List[TableAlterColumn]], - column_type: t.Union[str, exp.DataType], - current_type: t.Union[str, exp.DataType], - expected_table_struct: t.Union[str, exp.DataType], - position: t.Optional[TableAlterColumnPosition] = None, - ) -> TableAlterOperation: - return cls( - op=TableAlterOperationType.ALTER_TYPE, - columns=ensure_list(columns), - column_type=exp.DataType.build(column_type), - add_position=position, - current_type=exp.DataType.build(current_type), - expected_table_struct=exp.DataType.build(expected_table_struct), - ) - - @property - def is_add(self) -> bool: - return self.op.is_add - - @property - def is_drop(self) -> bool: - return self.op.is_drop - - @property - def is_alter_type(self) -> bool: - return self.op.is_alter_type - - def column_identifiers(self, array_element_selector: str) -> t.List[exp.Identifier]: - results = [] - for column in self.columns: - results.append(column.identifier) - if column.is_array_of_struct and len(self.columns) > 1 and array_element_selector: - results.append(exp.to_identifier(array_element_selector)) - return results - - def column(self, array_element_selector: str) -> t.Union[exp.Dot, exp.Identifier]: - columns = self.column_identifiers(array_element_selector) - if len(columns) == 1: - return columns[0] - return exp.Dot.build(columns) - - def column_def(self, array_element_selector: str) -> exp.ColumnDef: - return exp.ColumnDef( - this=self.column(array_element_selector), - kind=self.column_type, - ) - - def expression( - self, table_name: t.Union[str, exp.Table], array_element_selector: str - ) -> exp.Alter: - if self.is_alter_type: - return exp.Alter( - this=exp.to_table(table_name), - kind="TABLE", - actions=[ - exp.AlterColumn( - this=self.column(array_element_selector), - dtype=self.column_type, - ) - ], - ) - if self.is_add: - alter_table = exp.Alter(this=exp.to_table(table_name), kind="TABLE") - column = self.column_def(array_element_selector) - alter_table.set("actions", [column]) - if self.add_position: - column.set("position", self.add_position.column_position_node) - return alter_table - if self.is_drop: - alter_table = exp.Alter(this=exp.to_table(table_name), kind="TABLE") - drop_column = exp.Drop( - this=self.column(array_element_selector), kind="COLUMN", cascade=self.cascade - ) - alter_table.set("actions", [drop_column]) - return alter_table - raise ValueError(f"Unknown operation {self.op}") - - class SchemaDiffer(PydanticModel): """ Compares a source schema against a target schema and returns a list of alter statements to have the source @@ -467,16 +462,21 @@ def _drop_operation( struct: exp.DataType, pos: int, root_struct: exp.DataType, - ) -> t.List[TableAlterOperation]: + table_name: TableName, + ) -> t.List[TableAlterColumnOperation]: columns = ensure_list(columns) - operations = [] + operations: t.List[TableAlterColumnOperation] = [] column_pos, column_kwarg = self._get_matching_kwarg(columns[-1].name, struct, pos) assert column_pos is not None assert column_kwarg struct.expressions.pop(column_pos) operations.append( - TableAlterOperation.drop( - columns, root_struct.copy(), column_kwarg.args["kind"], cascade=self.drop_cascade + TableAlterDropColumnOperation( + target_table=exp.to_table(table_name), + column_parts=columns, + expected_table_struct=root_struct.copy(), + cascade=self.drop_cascade, + array_element_selector=self.array_element_selector, ) ) return operations @@ -496,14 +496,17 @@ def _resolve_drop_operation( current_struct: exp.DataType, new_struct: exp.DataType, root_struct: exp.DataType, - ) -> t.List[TableAlterOperation]: + table_name: TableName, + ) -> t.List[TableAlterColumnOperation]: operations = [] for current_pos, current_kwarg in enumerate(current_struct.expressions.copy()): new_pos, _ = self._get_matching_kwarg(current_kwarg, new_struct, current_pos) columns = parent_columns + [TableAlterColumn.from_struct_kwarg(current_kwarg)] if new_pos is None: operations.extend( - self._drop_operation(columns, current_struct, current_pos, root_struct) + self._drop_operation( + columns, current_struct, current_pos, root_struct, table_name + ) ) return operations @@ -514,7 +517,9 @@ def _add_operation( new_kwarg: exp.ColumnDef, current_struct: exp.DataType, root_struct: exp.DataType, - ) -> t.List[TableAlterOperation]: + table_name: TableName, + is_part_of_destructive_change: bool = False, + ) -> t.List[TableAlterColumnOperation]: if self.support_positional_add: col_pos = TableAlterColumnPosition.create(new_pos, current_struct.expressions) current_struct.expressions.insert(new_pos, new_kwarg) @@ -522,11 +527,14 @@ def _add_operation( col_pos = None current_struct.expressions.append(new_kwarg) return [ - TableAlterOperation.add( - columns, - new_kwarg.args["kind"], - root_struct.copy(), - col_pos, + TableAlterAddColumnOperation( + target_table=exp.to_table(table_name), + column_parts=columns, + column_type=new_kwarg.args["kind"], + expected_table_struct=root_struct.copy(), + position=col_pos, + is_part_of_destructive_change=is_part_of_destructive_change, + array_element_selector=self.array_element_selector, ) ] @@ -536,14 +544,17 @@ def _resolve_add_operations( current_struct: exp.DataType, new_struct: exp.DataType, root_struct: exp.DataType, - ) -> t.List[TableAlterOperation]: + table_name: TableName, + ) -> t.List[TableAlterColumnOperation]: operations = [] for new_pos, new_kwarg in enumerate(new_struct.expressions): possible_current_pos, _ = self._get_matching_kwarg(new_kwarg, current_struct, new_pos) if possible_current_pos is None: columns = parent_columns + [TableAlterColumn.from_struct_kwarg(new_kwarg)] operations.extend( - self._add_operation(columns, new_pos, new_kwarg, current_struct, root_struct) + self._add_operation( + columns, new_pos, new_kwarg, current_struct, root_struct, table_name + ) ) return operations @@ -556,9 +567,11 @@ def _alter_operation( current_type: t.Union[str, exp.DataType], root_struct: exp.DataType, new_kwarg: exp.ColumnDef, + table_name: TableName, *, ignore_destructive: bool = False, - ) -> t.List[TableAlterOperation]: + ignore_additive: bool = False, + ) -> t.List[TableAlterColumnOperation]: # We don't copy on purpose here because current_type may need to be mutated inside # _get_operations (struct.expressions.pop and struct.expressions.insert) current_type = exp.DataType.build(current_type, copy=False) @@ -572,7 +585,9 @@ def _alter_operation( current_type, new_type, root_struct, + table_name, ignore_destructive=ignore_destructive, + ignore_additive=ignore_additive, ) if new_type.this == current_type.this == exp.DataType.Type.ARRAY: @@ -590,31 +605,43 @@ def _alter_operation( current_array_type, new_array_type, root_struct, + table_name, ignore_destructive=ignore_destructive, + ignore_additive=ignore_additive, ) if self._is_coerceable_type(current_type, new_type): return [] if self._is_compatible_type(current_type, new_type): + if ignore_additive: + return [] struct.expressions.pop(pos) struct.expressions.insert(pos, new_kwarg) - col_pos = ( - TableAlterColumnPosition.create(pos, struct.expressions, replacing_col=True) - if self.support_positional_add - else None - ) return [ - TableAlterOperation.alter_type( - columns, - new_type, - current_type, - root_struct.copy(), - col_pos, + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table(table_name), + column_parts=columns, + column_type=new_type, + current_type=current_type, + expected_table_struct=root_struct.copy(), + array_element_selector=self.array_element_selector, ) ] if ignore_destructive: return [] - return self._drop_operation(columns, root_struct, pos, root_struct) + self._add_operation( - columns, pos, new_kwarg, struct, root_struct + return self._drop_operation( + columns, + root_struct, + pos, + root_struct, + table_name, + ) + self._add_operation( + columns, + pos, + new_kwarg, + struct, + root_struct, + table_name, + is_part_of_destructive_change=True, ) def _resolve_alter_operations( @@ -623,9 +650,11 @@ def _resolve_alter_operations( current_struct: exp.DataType, new_struct: exp.DataType, root_struct: exp.DataType, + table_name: TableName, *, ignore_destructive: bool = False, - ) -> t.List[TableAlterOperation]: + ignore_additive: bool = False, + ) -> t.List[TableAlterColumnOperation]: operations = [] for current_pos, current_kwarg in enumerate(current_struct.expressions.copy()): _, new_kwarg = self._get_matching_kwarg(current_kwarg, new_struct, current_pos) @@ -647,7 +676,9 @@ def _resolve_alter_operations( current_type, root_struct, new_kwarg, + table_name, ignore_destructive=ignore_destructive, + ignore_additive=ignore_additive, ) ) return operations @@ -658,21 +689,26 @@ def _get_operations( current_struct: exp.DataType, new_struct: exp.DataType, root_struct: exp.DataType, + table_name: TableName, *, ignore_destructive: bool = False, - ) -> t.List[TableAlterOperation]: + ignore_additive: bool = False, + ) -> t.List[TableAlterColumnOperation]: root_struct = root_struct or current_struct parent_columns = parent_columns or [] operations = [] if not ignore_destructive: operations.extend( self._resolve_drop_operation( - parent_columns, current_struct, new_struct, root_struct + parent_columns, current_struct, new_struct, root_struct, table_name + ) + ) + if not ignore_additive: + operations.extend( + self._resolve_add_operations( + parent_columns, current_struct, new_struct, root_struct, table_name ) ) - operations.extend( - self._resolve_add_operations(parent_columns, current_struct, new_struct, root_struct) - ) operations.extend( self._resolve_alter_operations( parent_columns, @@ -680,6 +716,8 @@ def _get_operations( new_struct, root_struct, ignore_destructive=ignore_destructive, + ignore_additive=ignore_additive, + table_name=table_name, ) ) return operations @@ -688,11 +726,19 @@ def _from_structs( self, current_struct: exp.DataType, new_struct: exp.DataType, + table_name: TableName, *, ignore_destructive: bool = False, - ) -> t.List[TableAlterOperation]: + ignore_additive: bool = False, + ) -> t.List[TableAlterColumnOperation]: return self._get_operations( - [], current_struct, new_struct, current_struct, ignore_destructive=ignore_destructive + [], + current_struct, + new_struct, + current_struct, + table_name=table_name, + ignore_destructive=ignore_destructive, + ignore_additive=ignore_additive, ) def _compare_structs( @@ -702,11 +748,15 @@ def _compare_structs( new: exp.DataType, *, ignore_destructive: bool = False, - ) -> t.List[exp.Alter]: - return [ - op.expression(table_name, self.array_element_selector) - for op in self._from_structs(current, new, ignore_destructive=ignore_destructive) - ] + ignore_additive: bool = False, + ) -> t.List[TableAlterColumnOperation]: + return self._from_structs( + current, + new, + table_name=table_name, + ignore_destructive=ignore_destructive, + ignore_additive=ignore_additive, + ) def compare_columns( self, @@ -715,31 +765,45 @@ def compare_columns( new: t.Dict[str, exp.DataType], *, ignore_destructive: bool = False, - ) -> t.List[exp.Alter]: + ignore_additive: bool = False, + ) -> t.List[TableAlterColumnOperation]: return self._compare_structs( table_name, columns_to_types_to_struct(current), columns_to_types_to_struct(new), ignore_destructive=ignore_destructive, + ignore_additive=ignore_additive, ) -def has_drop_alteration(alter_expressions: t.List[exp.Alter]) -> bool: - return any( - isinstance(action, exp.Drop) - for actions in alter_expressions - for action in actions.args.get("actions", []) - ) +def has_drop_alteration(alter_operations: t.List[TableAlterOperation]) -> bool: + return any(op.is_destructive for op in alter_operations) + + +def has_additive_alteration(alter_operations: t.List[TableAlterOperation]) -> bool: + return any(op.is_additive for op in alter_operations) + + +def get_additive_changes( + alter_operations: t.List[TableAlterOperation], +) -> t.List[TableAlterOperation]: + return [x for x in alter_operations if x.is_additive] + + +def get_dropped_column_names(alter_expressions: t.List[TableAlterOperation]) -> t.List[str]: + return [ + op.column.alias_or_name + for op in alter_expressions + if isinstance(op, TableAlterDropColumnOperation) + ] -def get_dropped_column_names(alter_expressions: t.List[exp.Alter]) -> t.List[str]: - dropped_columns = [] - for actions in alter_expressions: - for action in actions.args.get("actions", []): - if isinstance(action, exp.Drop): - if action.kind == "COLUMN": - dropped_columns.append(action.alias_or_name) - return dropped_columns +def get_additive_column_names(alter_expressions: t.List[TableAlterOperation]) -> t.List[str]: + return [ + op.column.alias_or_name + for op in alter_expressions + if op.is_additive and isinstance(op, TableAlterColumnOperation) + ] def get_schema_differ(dialect: str) -> SchemaDiffer: diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 45740d9810..1a286edcfc 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1141,6 +1141,16 @@ def needs_destructive_check( and self.name not in allow_destructive_snapshots ) + def needs_additive_check( + self, + allow_additive_snapshots: t.Set[str], + ) -> bool: + return ( + self.is_model + and not self.model.on_additive_change.is_allow + and self.name not in allow_additive_snapshots + ) + def get_next_auto_restatement_interval(self, execution_time: TimeLike) -> t.Optional[Interval]: """Returns the next auto restatement interval for the snapshot. diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 1531997c1b..e891c752b1 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -50,8 +50,13 @@ ViewKind, CustomKind, ) +from sqlmesh.core.model.kind import _Incremental from sqlmesh.utils import CompletionStatus -from sqlmesh.core.schema_diff import has_drop_alteration, get_dropped_column_names +from sqlmesh.core.schema_diff import ( + has_drop_alteration, + TableAlterOperation, + has_additive_alteration, +) from sqlmesh.core.snapshot import ( DeployabilityIndex, Intervals, @@ -72,6 +77,8 @@ DestructiveChangeError, SQLMeshError, format_destructive_change_msg, + format_additive_change_msg, + AdditiveChangeError, ) if sys.version_info >= (3, 12): @@ -138,6 +145,7 @@ def evaluate( execution_time: TimeLike, snapshots: t.Dict[str, Snapshot], allow_destructive_snapshots: t.Optional[t.Set[str]] = None, + allow_additive_snapshots: t.Optional[t.Set[str]] = None, deployability_index: t.Optional[DeployabilityIndex] = None, batch_index: int = 0, target_table_exists: t.Optional[bool] = None, @@ -152,6 +160,7 @@ def evaluate( execution_time: The date/time time reference to use for execution time. snapshots: All upstream snapshots (by name) to use for expansion and mapping of physical locations. allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed. + allow_additive_snapshots: Snapshots for which additive schema changes are allowed. deployability_index: Determines snapshots that are deployable in the context of this evaluation. batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it target_table_exists: Whether the target table exists. If None, the table will be checked for existence. @@ -167,6 +176,7 @@ def evaluate( snapshot=snapshot, snapshots=snapshots, allow_destructive_snapshots=allow_destructive_snapshots or set(), + allow_additive_snapshots=allow_additive_snapshots or set(), deployability_index=deployability_index, batch_index=batch_index, target_table_exists=target_table_exists, @@ -338,6 +348,7 @@ def create( on_start: t.Optional[t.Callable] = None, on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None, allow_destructive_snapshots: t.Optional[t.Set[str]] = None, + allow_additive_snapshots: t.Optional[t.Set[str]] = None, ) -> CompletionStatus: """Creates a physical snapshot schema and table for the given collection of snapshots. @@ -348,6 +359,7 @@ def create( on_start: A callback to initialize the snapshot creation progress bar. on_complete: A callback to call on each successfully created snapshot. allow_destructive_snapshots: Set of snapshots that are allowed to have destructive schema changes. + allow_additive_snapshots: Set of snapshots that are allowed to have additive schema changes. Returns: CompletionStatus: The status of the creation operation (success, failure, nothing to do). @@ -366,6 +378,7 @@ def create( deployability_index=deployability_index, on_complete=on_complete, allow_destructive_snapshots=allow_destructive_snapshots or set(), + allow_additive_snapshots=allow_additive_snapshots or set(), ) return CompletionStatus.SUCCESS @@ -454,6 +467,7 @@ def _create_snapshots( deployability_index: DeployabilityIndex, on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]], allow_destructive_snapshots: t.Set[str], + allow_additive_snapshots: t.Set[str], ) -> None: """Internal method to create tables in parallel.""" with self.concurrent_context(): @@ -464,6 +478,7 @@ def _create_snapshots( snapshots=snapshots, deployability_index=deployability_index, allow_destructive_snapshots=allow_destructive_snapshots, + allow_additive_snapshots=allow_additive_snapshots, on_complete=on_complete, ), self.ddl_concurrent_tasks, @@ -477,6 +492,7 @@ def migrate( target_snapshots: t.Iterable[Snapshot], snapshots: t.Dict[SnapshotId, Snapshot], allow_destructive_snapshots: t.Optional[t.Set[str]] = None, + allow_additive_snapshots: t.Optional[t.Set[str]] = None, deployability_index: t.Optional[DeployabilityIndex] = None, ) -> None: """Alters a physical snapshot table to match its snapshot's schema for the given collection of snapshots. @@ -485,9 +501,11 @@ def migrate( target_snapshots: Target snapshots. snapshots: Mapping of snapshot ID to snapshot. allow_destructive_snapshots: Set of snapshots that are allowed to have destructive schema changes. + allow_additive_snapshots: Set of snapshots that are allowed to have additive schema changes. deployability_index: Determines snapshots that are deployable in the context of this evaluation. """ allow_destructive_snapshots = allow_destructive_snapshots or set() + allow_additive_snapshots = allow_additive_snapshots or set() deployability_index = deployability_index or DeployabilityIndex.all_deployable() snapshots_by_name = {s.name: s for s in snapshots.values()} with self.concurrent_context(): @@ -497,6 +515,7 @@ def migrate( s, snapshots_by_name, allow_destructive_snapshots, + allow_additive_snapshots, self.get_adapter(s.model_gateway), deployability_index, ), @@ -669,6 +688,7 @@ def _evaluate_snapshot( snapshot: Snapshot, snapshots: t.Dict[str, Snapshot], allow_destructive_snapshots: t.Set[str], + allow_additive_snapshots: t.Set[str], deployability_index: t.Optional[DeployabilityIndex], batch_index: int, target_table_exists: t.Optional[bool], @@ -683,6 +703,7 @@ def _evaluate_snapshot( execution_time: The date/time time reference to use for execution time. snapshots: All upstream snapshots to use for expansion and mapping of physical locations. allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed. + allow_additive_snapshots: Snapshots for which additive schema changes are allowed. deployability_index: Determines snapshots that are deployable in the context of this evaluation. batch_index: If the snapshot is part of a batch of related snapshots; which index in the batch is it target_table_exists: Whether the target table exists. If None, the table will be checked for existence. @@ -750,6 +771,7 @@ def _evaluate_snapshot( render_kwargs=create_render_kwargs, rendered_physical_properties=rendered_physical_properties, allow_destructive_snapshots=allow_destructive_snapshots, + allow_additive_snapshots=allow_additive_snapshots, ) common_render_kwargs["runtime_stage"] = RuntimeStage.EVALUATING elif model.annotated or model.is_seed or model.kind.is_scd_type_2: @@ -780,6 +802,7 @@ def _evaluate_snapshot( snapshot=snapshot, snapshots=snapshots, render_kwargs=common_render_kwargs, + create_render_kwargs=create_render_kwargs, rendered_physical_properties=rendered_physical_properties, deployability_index=deployability_index, is_first_insert=is_first_insert, @@ -796,6 +819,7 @@ def create_snapshot( snapshots: t.Dict[str, Snapshot], deployability_index: DeployabilityIndex, allow_destructive_snapshots: t.Set[str], + allow_additive_snapshots: t.Set[str], on_complete: t.Optional[t.Callable[[SnapshotInfoLike], None]] = None, ) -> None: """Creates a physical table for the given snapshot. @@ -806,6 +830,7 @@ def create_snapshot( deployability_index: Determines snapshots that are deployable in the context of this creation. on_complete: A callback to call on each successfully created database object. allow_destructive_snapshots: Snapshots for which destructive schema changes are allowed. + allow_additive_snapshots: Snapshots for which additive schema changes are allowed. """ if not snapshot.is_model: return @@ -836,6 +861,7 @@ def create_snapshot( render_kwargs=create_render_kwargs, rendered_physical_properties=rendered_physical_properties, allow_destructive_snapshots=allow_destructive_snapshots, + allow_additive_snapshots=allow_additive_snapshots, ) else: is_table_deployable = deployability_index.is_deployable(snapshot) @@ -860,6 +886,7 @@ def _render_and_insert_snapshot( snapshot: Snapshot, snapshots: t.Dict[str, Snapshot], render_kwargs: t.Dict[str, t.Any], + create_render_kwargs: t.Dict[str, t.Any], rendered_physical_properties: t.Dict[str, exp.Expression], deployability_index: DeployabilityIndex, is_first_insert: bool, @@ -896,7 +923,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: end=end, execution_time=execution_time, physical_properties=rendered_physical_properties, - render_kwargs=render_kwargs, + render_kwargs=create_render_kwargs, ) else: logger.info( @@ -918,7 +945,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: end=end, execution_time=execution_time, physical_properties=rendered_physical_properties, - render_kwargs=render_kwargs, + render_kwargs=create_render_kwargs, ) # DataFrames, unlike SQL expressions, can provide partial results by yielding dataframes. As a result, @@ -982,6 +1009,7 @@ def _clone_snapshot_in_dev( render_kwargs: t.Dict[str, t.Any], rendered_physical_properties: t.Dict[str, exp.Expression], allow_destructive_snapshots: t.Set[str], + allow_additive_snapshots: t.Set[str], ) -> None: adapter = self.get_adapter(snapshot.model.gateway) @@ -1004,6 +1032,7 @@ def _clone_snapshot_in_dev( render_kwargs=render_kwargs, rendered_physical_properties=rendered_physical_properties, allow_destructive_snapshots=allow_destructive_snapshots, + allow_additive_snapshots=allow_additive_snapshots, ) except Exception: adapter.drop_table(target_table_name) @@ -1014,6 +1043,7 @@ def _migrate_snapshot( snapshot: Snapshot, snapshots: t.Dict[str, Snapshot], allow_destructive_snapshots: t.Set[str], + allow_additive_snapshots: t.Set[str], adapter: EngineAdapter, deployability_index: DeployabilityIndex, ) -> None: @@ -1051,6 +1081,7 @@ def _migrate_snapshot( **render_kwargs ), allow_destructive_snapshots=allow_destructive_snapshots, + allow_additive_snapshots=allow_additive_snapshots, run_pre_post_statements=True, ) @@ -1063,6 +1094,7 @@ def _migrate_target_table( render_kwargs: t.Dict[str, t.Any], rendered_physical_properties: t.Dict[str, exp.Expression], allow_destructive_snapshots: t.Set[str], + allow_additive_snapshots: t.Set[str], run_pre_post_statements: bool = False, ) -> None: adapter = self.get_adapter(snapshot.model.gateway) @@ -1092,7 +1124,9 @@ def _migrate_target_table( snapshot=snapshot, snapshots=snapshots, allow_destructive_snapshots=allow_destructive_snapshots, + allow_additive_snapshots=allow_additive_snapshots, ignore_destructive=snapshot.model.on_destructive_change.is_ignore, + ignore_additive=snapshot.model.on_additive_change.is_ignore, ) finally: if snapshot.is_materialized: @@ -1501,6 +1535,7 @@ def migrate( snapshot: Snapshot, *, ignore_destructive: bool, + ignore_additive: bool, **kwargs: t.Any, ) -> None: """Migrates the target table schema so that it corresponds to the source table schema. @@ -1511,6 +1546,8 @@ def migrate( snapshot: The target snapshot. ignore_destructive: If True, destructive changes are not created when migrating. This is used for forward-only models that are being migrated to a new version. + ignore_additive: If True, additive changes are not created when migrating. + This is used for forward-only models that are being migrated to a new version. """ @abc.abstractmethod @@ -1587,6 +1624,7 @@ def migrate( snapshot: Snapshot, *, ignore_destructive: bool, + ignore_additive: bool, **kwarg: t.Any, ) -> None: pass @@ -1713,16 +1751,23 @@ def migrate( snapshot: Snapshot, *, ignore_destructive: bool, + ignore_additive: bool, **kwargs: t.Any, ) -> None: logger.info(f"Altering table '{target_table_name}'") - alter_expressions = self.adapter.get_alter_expressions( - target_table_name, source_table_name, ignore_destructive=ignore_destructive + alter_operations = self.adapter.get_alter_operations( + target_table_name, + source_table_name, + ignore_destructive=ignore_destructive, + ignore_additive=ignore_additive, ) _check_destructive_schema_change( - snapshot, alter_expressions, kwargs["allow_destructive_snapshots"] + snapshot, alter_operations, kwargs["allow_destructive_snapshots"] + ) + _check_additive_schema_change( + snapshot, alter_operations, kwargs["allow_additive_snapshots"] ) - self.adapter.alter_table(alter_expressions) + self.adapter.alter_table(alter_operations) def delete(self, name: str, **kwargs: t.Any) -> None: _check_table_db_is_physical_schema(name, kwargs["physical_schema"]) @@ -1783,11 +1828,13 @@ def _get_target_and_source_columns( else: target_column_to_types = ( model.columns_to_types # type: ignore - if model.annotated and not model.on_destructive_change.is_ignore + if model.annotated + and not model.on_destructive_change.is_ignore + and not model.on_additive_change.is_ignore else self.adapter.columns(table_name) ) assert target_column_to_types is not None - if model.on_destructive_change.is_ignore: + if model.on_destructive_change.is_ignore or model.on_additive_change.is_ignore: # We need to identify the columns that are only in the source so we create an empty table with # the user query to determine that with self.adapter.temp_table(model.ctas_query(**render_kwargs)) as temp_table: @@ -2305,6 +2352,7 @@ def migrate( snapshot: Snapshot, *, ignore_destructive: bool, + ignore_additive: bool, **kwargs: t.Any, ) -> None: logger.info("Migrating view '%s'", target_table_name) @@ -2552,12 +2600,16 @@ def migrate( snapshot: Snapshot, *, ignore_destructive: bool, + ignore_additive: bool, **kwargs: t.Any, ) -> None: - potential_alter_expressions = self.adapter.get_alter_expressions( - target_table_name, source_table_name, ignore_destructive=ignore_destructive + potential_alter_operations = self.adapter.get_alter_operations( + target_table_name, + source_table_name, + ignore_destructive=ignore_destructive, + ignore_additive=ignore_additive, ) - if len(potential_alter_expressions) > 0: + if len(potential_alter_operations) > 0: # this can happen if a user changes a managed model and deliberately overrides a plan to be forward only, eg `sqlmesh plan --forward-only` raise SQLMeshError( f"The schema of the managed model '{target_table_name}' cannot be updated in a forward-only fashion." @@ -2584,36 +2636,65 @@ def _intervals(snapshot: Snapshot, deployability_index: DeployabilityIndex) -> I def _check_destructive_schema_change( snapshot: Snapshot, - alter_expressions: t.List[exp.Alter], + alter_operations: t.List[TableAlterOperation], allow_destructive_snapshots: t.Set[str], ) -> None: if ( snapshot.is_no_rebuild and snapshot.needs_destructive_check(allow_destructive_snapshots) - and has_drop_alteration(alter_expressions) + and has_drop_alteration(alter_operations) ): snapshot_name = snapshot.name - dropped_column_names = get_dropped_column_names(alter_expressions) model_dialect = snapshot.model.dialect if snapshot.model.on_destructive_change.is_warn: logger.warning( format_destructive_change_msg( snapshot_name, - dropped_column_names, - alter_expressions, + alter_operations, model_dialect, error=False, ) ) return raise DestructiveChangeError( - format_destructive_change_msg( - snapshot_name, dropped_column_names, alter_expressions, model_dialect - ) + format_destructive_change_msg(snapshot_name, alter_operations, model_dialect) ) +def _check_additive_schema_change( + snapshot: Snapshot, + alter_operations: t.List[TableAlterOperation], + allow_additive_snapshots: t.Set[str], +) -> None: + # Only check additive changes for incremental models that have the on_additive_change property + if not isinstance(snapshot.model.kind, _Incremental): + return + + if snapshot.needs_additive_check(allow_additive_snapshots) and has_additive_alteration( + alter_operations + ): + # Note: IGNORE filtering is applied before this function is called + # so if we reach here, additive changes are not being ignored + snapshot_name = snapshot.name + model_dialect = snapshot.model.dialect + + if snapshot.model.on_additive_change.is_warn: + logger.warning( + format_additive_change_msg( + snapshot_name, + alter_operations, + model_dialect, + error=False, + ) + ) + return + if snapshot.model.on_additive_change.is_error: + raise AdditiveChangeError( + format_additive_change_msg(snapshot_name, alter_operations, model_dialect) + ) + + def _check_table_db_is_physical_schema(table_name: str, physical_schema: str) -> None: table = exp.to_table(table_name) if table.db != physical_schema: diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index e35d8c16f4..c646392368 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -23,7 +23,7 @@ ManagedKind, create_sql_model, ) -from sqlmesh.core.model.kind import SCDType2ByTimeKind, OnDestructiveChange +from sqlmesh.core.model.kind import SCDType2ByTimeKind, OnDestructiveChange, OnAdditiveChange from sqlmesh.dbt.basemodel import BaseModelConfig, Materialization, SnapshotStrategy from sqlmesh.dbt.common import SqlStr, extract_jinja_config, sql_str_validator from sqlmesh.utils.errors import ConfigError @@ -91,7 +91,7 @@ class ModelConfig(BaseModelConfig): unique_key: t.Optional[t.List[str]] = None partition_by: t.Optional[t.Union[t.List[str], t.Dict[str, t.Any]]] = None full_refresh: t.Optional[bool] = None - on_schema_change: t.Optional[str] = None + on_schema_change: str = "ignore" # Snapshot (SCD Type 2) Fields updated_at: t.Optional[str] = None @@ -227,17 +227,31 @@ def model_kind(self, context: DbtContext) -> ModelKind: # args common to all sqlmesh incremental kinds, regardless of materialization incremental_kind_kwargs: t.Dict[str, t.Any] = {} - if self.on_schema_change: - on_schema_change = self.on_schema_change.lower() - - on_destructive_change = OnDestructiveChange.WARN - if on_schema_change == "sync_all_columns": - on_destructive_change = OnDestructiveChange.ALLOW - elif on_schema_change in ("fail", "append_new_columns", "ignore"): - on_destructive_change = OnDestructiveChange.ERROR - - incremental_kind_kwargs["on_destructive_change"] = on_destructive_change + on_schema_change = self.on_schema_change.lower() + if materialization == Materialization.SNAPSHOT: + # dbt snapshots default to `append_new_columns` behavior and can't be changed + on_schema_change = "append_new_columns" + + if on_schema_change == "ignore": + on_destructive_change = OnDestructiveChange.IGNORE + on_additive_change = OnAdditiveChange.IGNORE + elif on_schema_change == "fail": + on_destructive_change = OnDestructiveChange.ERROR + on_additive_change = OnAdditiveChange.ERROR + elif on_schema_change == "append_new_columns": + on_destructive_change = OnDestructiveChange.IGNORE + on_additive_change = OnAdditiveChange.ALLOW + elif on_schema_change == "sync_all_columns": + on_destructive_change = OnDestructiveChange.ALLOW + on_additive_change = OnAdditiveChange.ALLOW + else: + raise ConfigError( + f"{self.canonical_name(context)}: Invalid on_schema_change value '{on_schema_change}'. " + "Valid values are 'ignore', 'fail', 'append_new_columns', 'sync_all_columns'." + ) + incremental_kind_kwargs["on_destructive_change"] = on_destructive_change + incremental_kind_kwargs["on_additive_change"] = on_additive_change for field in ("forward_only", "auto_restatement_cron"): field_val = getattr(self, field, None) if field_val is None: diff --git a/sqlmesh/migrations/v0091_on_additive_change.py b/sqlmesh/migrations/v0091_on_additive_change.py new file mode 100644 index 0000000000..56059b982f --- /dev/null +++ b/sqlmesh/migrations/v0091_on_additive_change.py @@ -0,0 +1,5 @@ +"""Add on_additive_change to incremental model metadata hash.""" + + +def migrate(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/utils/errors.py b/sqlmesh/utils/errors.py index 9974fdce0a..82ec311237 100644 --- a/sqlmesh/utils/errors.py +++ b/sqlmesh/utils/errors.py @@ -11,6 +11,7 @@ from requests.models import Response from sqlmesh.core.model import Model + from sqlmesh.core.schema_diff import TableAlterOperation class ErrorLevel(AutoName): @@ -129,6 +130,10 @@ class DestructiveChangeError(SQLMeshError): pass +class AdditiveChangeError(SQLMeshError): + pass + + class NotificationTargetError(SQLMeshError): pass @@ -210,27 +215,87 @@ def raise_for_status(response: Response) -> None: raise ApiServerError(response.text, response.status_code) -def format_destructive_change_msg( +def _format_schema_change_msg( snapshot_name: str, - dropped_column_names: t.List[str], - alter_expressions: t.List[exp.Alter], + is_destructive: bool, + alter_operations: t.List[TableAlterOperation], dialect: str, error: bool = True, ) -> str: - dropped_column_str = "', '".join(dropped_column_names) - dropped_column_msg = ( - f" that drops column{'s' if dropped_column_names and len(dropped_column_names) > 1 else ''} '{dropped_column_str}'" - if dropped_column_str + """ + Common function to format schema change messages. + + Args: + snapshot_name: Name of the model/snapshot + is_destructive: if change is destructive else it would be additive + alter_operations: List of table alter operations + dialect: SQL dialect for formatting + error: Whether this is an error or warning + """ + from sqlmesh.core.schema_diff import get_dropped_column_names, get_additive_column_names + + change_type = "destructive" if is_destructive else "additive" + setting_name = "on_destructive_change" if is_destructive else "on_additive_change" + action_verb = "drops" if is_destructive else "adds" + cli_flag = "--allow-destructive-model" if is_destructive else "--allow-additive-model" + + column_names = ( + get_dropped_column_names(alter_operations) + if is_destructive + else get_additive_column_names(alter_operations) + ) + column_str = "', '".join(column_names) + column_msg = ( + f" that {action_verb} column{'s' if column_names and len(column_names) > 1 else ''} '{column_str}'" + if column_str else "" ) + # Format ALTER expressions alter_expr_msg = "\n\nSchema changes:\n " + "\n ".join( - [alter.sql(dialect) for alter in alter_expressions] + [alter.expression.sql(dialect) for alter in alter_operations] ) + # Main warning message warning_msg = ( - f"Plan requires a destructive change to forward-only model '{snapshot_name}'s schema" + f"Plan requires {change_type} change to forward-only model '{snapshot_name}'s schema" ) - err_msg = "\n\nTo allow the destructive change, set the model's `on_destructive_change` setting to `warn` or `allow` or include the model in the plan's `--allow-destructive-model` option.\n" - return f"\n{warning_msg}{dropped_column_msg}.{alter_expr_msg}{err_msg if error else ''}" + if error: + permissive_values = "`warn`, `allow`, or `ignore`" + cli_part = f" or include the model in the plan's `{cli_flag}` option" + err_msg = f"\n\nTo allow the {change_type} change, set the model's `{setting_name}` setting to {permissive_values}{cli_part}.\n" + else: + err_msg = "" + + return f"\n{warning_msg}{column_msg}.{alter_expr_msg}{err_msg}" + + +def format_destructive_change_msg( + snapshot_name: str, + alter_expressions: t.List[TableAlterOperation], + dialect: str, + error: bool = True, +) -> str: + return _format_schema_change_msg( + snapshot_name=snapshot_name, + is_destructive=True, + alter_operations=alter_expressions, + dialect=dialect, + error=error, + ) + + +def format_additive_change_msg( + snapshot_name: str, + alter_operations: t.List[TableAlterOperation], + dialect: str, + error: bool = True, +) -> str: + return _format_schema_change_msg( + snapshot_name=snapshot_name, + is_destructive=False, + alter_operations=alter_operations, + dialect=dialect, + error=error, + ) diff --git a/tests/conftest.py b/tests/conftest.py index 85780d2db4..e5bbc4f425 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,7 +32,7 @@ from sqlmesh.core import lineage from sqlmesh.core.macros import macro from sqlmesh.core.model import IncrementalByTimeRangeKind, SqlModel, model -from sqlmesh.core.model.kind import OnDestructiveChange +from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange from sqlmesh.core.plan import BuiltInPlanEvaluator, Plan, stages as plan_stages from sqlmesh.core.snapshot import ( DeployabilityIndex, @@ -451,6 +451,52 @@ def _make_function( return _make_function +@pytest.fixture +def make_snapshot_on_additive_change(make_snapshot: t.Callable) -> t.Callable: + def _make_function( + name: str = "a", + old_query: str = "select '1' as one, '2' as two, '2022-01-01' ds", + new_query: str = "select '1' as one, '2' as two, '3' as three, '2022-01-01' ds", + on_additive_change: OnAdditiveChange = OnAdditiveChange.ERROR, + ) -> t.Tuple[Snapshot, Snapshot]: + snapshot_old = make_snapshot( + SqlModel( + name=name, + dialect="duckdb", + query=parse_one(old_query), + kind=IncrementalByTimeRangeKind( + time_column="ds", forward_only=True, on_additive_change=on_additive_change + ), + ) + ) + + snapshot = make_snapshot( + SqlModel( + name=name, + dialect="duckdb", + query=parse_one(new_query), + kind=IncrementalByTimeRangeKind( + time_column="ds", forward_only=True, on_additive_change=on_additive_change + ), + ) + ) + snapshot.previous_versions = ( + SnapshotDataVersion( + fingerprint=SnapshotFingerprint( + data_hash="test_data_hash", + metadata_hash="test_metadata_hash", + ), + version="test_version", + change_category=SnapshotChangeCategory.NON_BREAKING, + dev_table_suffix="dev", + ), + ) + + return snapshot_old, snapshot + + return _make_function + + @pytest.fixture def random_name() -> t.Callable: return lambda: f"generated_{random_id()}" diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index d1a9c5afaa..e6e4cfdeb6 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -272,9 +272,9 @@ def test_ctas_source_columns(ctx_query_and_df: TestContext): input_data = pd.DataFrame( [ - {"id": 1, "ds": "2022-01-01"}, - {"id": 2, "ds": "2022-01-02"}, - {"id": 3, "ds": "2022-01-03"}, + {"id": 1, "ds": "2022-01-01", "ignored_source": "ignored_value"}, + {"id": 2, "ds": "2022-01-02", "ignored_source": "ignored_value"}, + {"id": 3, "ds": "2022-01-03", "ignored_source": "ignored_value"}, ] ) ctx.engine_adapter.ctas( @@ -284,11 +284,12 @@ def test_ctas_source_columns(ctx_query_and_df: TestContext): column_descriptions={"id": "test id column description"}, table_format=ctx.default_table_format, target_columns_to_types=columns_to_types, - source_columns=["id", "ds"], + source_columns=["id", "ds", "ignored_source"], ) expected_data = input_data.copy() expected_data["ignored_column"] = pd.Series() + expected_data = expected_data.drop(columns=["ignored_source"]) results = ctx.get_metadata_results(schema=table.db) assert len(results.views) == 0 @@ -360,9 +361,9 @@ def test_create_view_source_columns(ctx_query_and_df: TestContext): input_data = pd.DataFrame( [ - {"id": 1, "ds": "2022-01-01"}, - {"id": 2, "ds": "2022-01-02"}, - {"id": 3, "ds": "2022-01-03"}, + {"id": 1, "ds": "2022-01-01", "ignored_source": "ignored_value"}, + {"id": 2, "ds": "2022-01-02", "ignored_source": "ignored_value"}, + {"id": 3, "ds": "2022-01-03", "ignored_source": "ignored_value"}, ] ) view = ctx.table("test_view") @@ -371,12 +372,13 @@ def test_create_view_source_columns(ctx_query_and_df: TestContext): ctx.input_data(input_data), table_description="test view description", column_descriptions={"id": "test id column description"}, - source_columns=["id", "ds"], + source_columns=["id", "ds", "ignored_source"], target_columns_to_types=columns_to_types, ) expected_data = input_data.copy() expected_data["ignored_column"] = pd.Series() + expected_data = expected_data.drop(columns=["ignored_source"]) results = ctx.get_metadata_results() assert len(results.tables) == 0 @@ -550,9 +552,9 @@ def test_replace_query_source_columns(ctx_query_and_df: TestContext): # Initial Load input_data = pd.DataFrame( [ - {"id": 1, "ds": "2022-01-01"}, - {"id": 2, "ds": "2022-01-02"}, - {"id": 3, "ds": "2022-01-03"}, + {"id": 1, "ds": "2022-01-01", "ignored_source": "ignored_value"}, + {"id": 2, "ds": "2022-01-02", "ignored_source": "ignored_value"}, + {"id": 3, "ds": "2022-01-03", "ignored_source": "ignored_value"}, ] ) ctx.engine_adapter.create_table(table, columns_to_types, table_format=ctx.default_table_format) @@ -560,11 +562,12 @@ def test_replace_query_source_columns(ctx_query_and_df: TestContext): table, ctx.input_data(input_data), table_format=ctx.default_table_format, - source_columns=["id", "ds"], + source_columns=["id", "ds", "ignored_source"], target_columns_to_types=columns_to_types, ) expected_data = input_data.copy() expected_data["ignored_column"] = pd.Series() + expected_data = expected_data.drop(columns=["ignored_source"]) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -709,19 +712,21 @@ def test_insert_append_source_columns(ctx_query_and_df: TestContext): # Initial Load input_data = pd.DataFrame( [ - {"id": 1, "ds": "2022-01-01"}, - {"id": 2, "ds": "2022-01-02"}, - {"id": 3, "ds": "2022-01-03"}, + {"id": 1, "ds": "2022-01-01", "ignored_source": "ignored_value"}, + {"id": 2, "ds": "2022-01-02", "ignored_source": "ignored_value"}, + {"id": 3, "ds": "2022-01-03", "ignored_source": "ignored_value"}, ] ) ctx.engine_adapter.insert_append( table, ctx.input_data(input_data), - source_columns=["id", "ds"], + source_columns=["id", "ds", "ignored_source"], target_columns_to_types=columns_to_types, ) expected_data = input_data.copy() expected_data["ignored_column"] = pd.Series() + expected_data = expected_data.drop(columns=["ignored_source"]) + results = ctx.get_metadata_results() assert len(results.views) == 0 assert len(results.materialized_views) == 0 @@ -733,19 +738,21 @@ def test_insert_append_source_columns(ctx_query_and_df: TestContext): if ctx.test_type == "df": append_data = pd.DataFrame( [ - {"id": 4, "ds": "2022-01-04"}, - {"id": 5, "ds": "2022-01-05"}, - {"id": 6, "ds": "2022-01-06"}, + {"id": 4, "ds": "2022-01-04", "ignored_source": "ignored_value"}, + {"id": 5, "ds": "2022-01-05", "ignored_source": "ignored_value"}, + {"id": 6, "ds": "2022-01-06", "ignored_source": "ignored_value"}, ] ) ctx.engine_adapter.insert_append( table, ctx.input_data(append_data), - source_columns=["id", "ds"], + source_columns=["id", "ds", "ignored_source"], target_columns_to_types=columns_to_types, ) append_expected_data = append_data.copy() append_expected_data["ignored_column"] = pd.Series() + append_expected_data = append_expected_data.drop(columns=["ignored_source"]) + results = ctx.get_metadata_results() assert len(results.views) == 0 assert len(results.materialized_views) == 0 @@ -871,9 +878,9 @@ def test_insert_overwrite_by_time_partition_source_columns(ctx_query_and_df: Tes ) input_data = pd.DataFrame( [ - {"id": 1, ctx.time_column: "2022-01-01"}, - {"id": 2, ctx.time_column: "2022-01-02"}, - {"id": 3, ctx.time_column: "2022-01-03"}, + {"id": 1, ctx.time_column: "2022-01-01", "ignored_source": "ignored_value"}, + {"id": 2, ctx.time_column: "2022-01-02", "ignored_source": "ignored_value"}, + {"id": 3, ctx.time_column: "2022-01-03", "ignored_source": "ignored_value"}, ] ) ctx.engine_adapter.insert_overwrite_by_time_partition( @@ -884,10 +891,11 @@ def test_insert_overwrite_by_time_partition_source_columns(ctx_query_and_df: Tes time_formatter=ctx.time_formatter, time_column=ctx.time_column, target_columns_to_types=columns_to_types, - source_columns=["id", "ds"], + source_columns=["id", "ds", "ignored_source"], ) expected_data = input_data.copy() + expected_data = expected_data.drop(columns=["ignored_source"]) expected_data.insert(len(expected_data.columns) - 1, "ignored_column", pd.Series()) results = ctx.get_metadata_results() @@ -905,9 +913,9 @@ def test_insert_overwrite_by_time_partition_source_columns(ctx_query_and_df: Tes if ctx.test_type == "df": overwrite_data = pd.DataFrame( [ - {"id": 10, ctx.time_column: "2022-01-03"}, - {"id": 4, ctx.time_column: "2022-01-04"}, - {"id": 5, ctx.time_column: "2022-01-05"}, + {"id": 10, ctx.time_column: "2022-01-03", "ignored_source": "ignored_value"}, + {"id": 4, ctx.time_column: "2022-01-04", "ignored_source": "ignored_value"}, + {"id": 5, ctx.time_column: "2022-01-05", "ignored_source": "ignored_value"}, ] ) ctx.engine_adapter.insert_overwrite_by_time_partition( @@ -918,7 +926,7 @@ def test_insert_overwrite_by_time_partition_source_columns(ctx_query_and_df: Tes time_formatter=ctx.time_formatter, time_column=ctx.time_column, target_columns_to_types=columns_to_types, - source_columns=["id", "ds"], + source_columns=["id", "ds", "ignored_source"], ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -1025,9 +1033,9 @@ def test_merge_source_columns(ctx_query_and_df: TestContext): ctx.engine_adapter.create_table(table, columns_to_types, table_format=table_format) input_data = pd.DataFrame( [ - {"id": 1, "ds": "2022-01-01"}, - {"id": 2, "ds": "2022-01-02"}, - {"id": 3, "ds": "2022-01-03"}, + {"id": 1, "ds": "2022-01-01", "ignored_source": "ignored_value"}, + {"id": 2, "ds": "2022-01-02", "ignored_source": "ignored_value"}, + {"id": 3, "ds": "2022-01-03", "ignored_source": "ignored_value"}, ] ) ctx.engine_adapter.merge( @@ -1035,11 +1043,12 @@ def test_merge_source_columns(ctx_query_and_df: TestContext): ctx.input_data(input_data), unique_key=[exp.to_identifier("id")], target_columns_to_types=columns_to_types, - source_columns=["id", "ds"], + source_columns=["id", "ds", "ignored_source"], ) expected_data = input_data.copy() expected_data["ignored_column"] = pd.Series() + expected_data = expected_data.drop(columns=["ignored_source"]) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -1052,9 +1061,9 @@ def test_merge_source_columns(ctx_query_and_df: TestContext): if ctx.test_type == "df": merge_data = pd.DataFrame( [ - {"id": 2, "ds": "2022-01-10"}, - {"id": 4, "ds": "2022-01-04"}, - {"id": 5, "ds": "2022-01-05"}, + {"id": 2, "ds": "2022-01-10", "ignored_source": "ignored_value"}, + {"id": 4, "ds": "2022-01-04", "ignored_source": "ignored_value"}, + {"id": 5, "ds": "2022-01-05", "ignored_source": "ignored_value"}, ] ) ctx.engine_adapter.merge( @@ -1062,7 +1071,7 @@ def test_merge_source_columns(ctx_query_and_df: TestContext): ctx.input_data(merge_data), unique_key=[exp.to_identifier("id")], target_columns_to_types=columns_to_types, - source_columns=["id", "ds"], + source_columns=["id", "ds", "ignored_source"], ) results = ctx.get_metadata_results() @@ -1265,9 +1274,24 @@ def test_scd_type_2_by_time_source_columns(ctx_query_and_df: TestContext): ctx.engine_adapter.create_table(table, columns_to_types, table_format=ctx.default_table_format) input_data = pd.DataFrame( [ - {"id": 1, "name": "a", "updated_at": "2022-01-01 00:00:00"}, - {"id": 2, "name": "b", "updated_at": "2022-01-02 00:00:00"}, - {"id": 3, "name": "c", "updated_at": "2022-01-03 00:00:00"}, + { + "id": 1, + "name": "a", + "updated_at": "2022-01-01 00:00:00", + "ignored_source": "ignored_value", + }, + { + "id": 2, + "name": "b", + "updated_at": "2022-01-02 00:00:00", + "ignored_source": "ignored_value", + }, + { + "id": 3, + "name": "c", + "updated_at": "2022-01-03 00:00:00", + "ignored_source": "ignored_value", + }, ] ) ctx.engine_adapter.scd_type_2_by_time( @@ -1283,7 +1307,7 @@ def test_scd_type_2_by_time_source_columns(ctx_query_and_df: TestContext): truncate=True, start="2022-01-01 00:00:00", target_columns_to_types=columns_to_types, - source_columns=["id", "name", "updated_at"], + source_columns=["id", "name", "updated_at", "ignored_source"], ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -1329,13 +1353,28 @@ def test_scd_type_2_by_time_source_columns(ctx_query_and_df: TestContext): current_data = pd.DataFrame( [ # Change `a` to `x` - {"id": 1, "name": "x", "updated_at": "2022-01-04 00:00:00"}, + { + "id": 1, + "name": "x", + "updated_at": "2022-01-04 00:00:00", + "ignored_source": "ignored_value", + }, # Delete - # {"id": 2, "name": "b", "updated_at": "2022-01-02 00:00:00"}, + # {"id": 2, "name": "b", "updated_at": "2022-01-02 00:00:00", "ignored_source": "ignored_value"}, # No change - {"id": 3, "name": "c", "updated_at": "2022-01-03 00:00:00"}, + { + "id": 3, + "name": "c", + "updated_at": "2022-01-03 00:00:00", + "ignored_source": "ignored_value", + }, # Add - {"id": 4, "name": "d", "updated_at": "2022-01-04 00:00:00"}, + { + "id": 4, + "name": "d", + "updated_at": "2022-01-04 00:00:00", + "ignored_source": "ignored_value", + }, ] ) ctx.engine_adapter.scd_type_2_by_time( @@ -1351,7 +1390,7 @@ def test_scd_type_2_by_time_source_columns(ctx_query_and_df: TestContext): truncate=False, start="2022-01-01 00:00:00", target_columns_to_types=columns_to_types, - source_columns=["id", "name", "updated_at"], + source_columns=["id", "name", "updated_at", "ignored_source"], ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -1610,10 +1649,10 @@ def test_scd_type_2_by_column_source_columns(ctx_query_and_df: TestContext): ctx.engine_adapter.create_table(table, columns_to_types, table_format=ctx.default_table_format) input_data = pd.DataFrame( [ - {"id": 1, "name": "a", "status": "active"}, - {"id": 2, "name": "b", "status": "inactive"}, - {"id": 3, "name": "c", "status": "active"}, - {"id": 4, "name": "d", "status": "active"}, + {"id": 1, "name": "a", "status": "active", "ignored_source": "ignored_value"}, + {"id": 2, "name": "b", "status": "inactive", "ignored_source": "ignored_value"}, + {"id": 3, "name": "c", "status": "active", "ignored_source": "ignored_value"}, + {"id": 4, "name": "d", "status": "active", "ignored_source": "ignored_value"}, ] ) ctx.engine_adapter.scd_type_2_by_column( @@ -1628,7 +1667,7 @@ def test_scd_type_2_by_column_source_columns(ctx_query_and_df: TestContext): truncate=True, start="2023-01-01", target_columns_to_types=columns_to_types, - source_columns=["id", "name", "status"], + source_columns=["id", "name", "status", "ignored_source"], ) results = ctx.get_metadata_results() assert len(results.views) == 0 @@ -1682,15 +1721,15 @@ def test_scd_type_2_by_column_source_columns(ctx_query_and_df: TestContext): current_data = pd.DataFrame( [ # Change `a` to `x` - {"id": 1, "name": "x", "status": "active"}, + {"id": 1, "name": "x", "status": "active", "ignored_source": "ignored_value"}, # Delete - # {"id": 2, "name": "b", status: "inactive"}, + # {"id": 2, "name": "b", status: "inactive", "ignored_source": "ignored_value"}, # No change - {"id": 3, "name": "c", "status": "active"}, + {"id": 3, "name": "c", "status": "active", "ignored_source": "ignored_value"}, # Change status to inactive - {"id": 4, "name": "d", "status": "inactive"}, + {"id": 4, "name": "d", "status": "inactive", "ignored_source": "ignored_value"}, # Add - {"id": 5, "name": "e", "status": "inactive"}, + {"id": 5, "name": "e", "status": "inactive", "ignored_source": "ignored_value"}, ] ) ctx.engine_adapter.scd_type_2_by_column( @@ -1705,7 +1744,7 @@ def test_scd_type_2_by_column_source_columns(ctx_query_and_df: TestContext): truncate=False, start="2023-01-01", target_columns_to_types=columns_to_types, - source_columns=["id", "name", "status"], + source_columns=["id", "name", "status", "ignored_source"], ) results = ctx.get_metadata_results() assert len(results.views) == 0 diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index e1cfaded13..66a647dc80 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -7,7 +7,10 @@ from sqlmesh.cli.project_init import ProjectTemplate, init_example_project from sqlmesh.core.config import Config from sqlmesh.core.engine_adapter import BigQueryEngineAdapter -from sqlmesh.core.engine_adapter.bigquery import _CLUSTERING_META_KEY +from sqlmesh.core.engine_adapter.mixins import ( + TableAlterDropClusterKeyOperation, + TableAlterChangeClusterKeyOperation, +) from sqlmesh.core.engine_adapter.shared import DataObject import sqlmesh.core.dialect as d from sqlmesh.core.model import SqlModel, load_sql_based_model @@ -68,41 +71,43 @@ def test_get_alter_expressions_includes_clustering( assert clustered_differently_table_metadata.clustering_key == "(c1,c2)" assert normal_table_metadata.clustering_key is None - assert len(engine_adapter.get_alter_expressions(normal_table, normal_table)) == 0 - assert len(engine_adapter.get_alter_expressions(clustered_table, clustered_table)) == 0 + assert len(engine_adapter.get_alter_operations(normal_table, normal_table)) == 0 + assert len(engine_adapter.get_alter_operations(clustered_table, clustered_table)) == 0 # alter table drop clustered - clustered_to_normal = engine_adapter.get_alter_expressions(clustered_table, normal_table) + clustered_to_normal = engine_adapter.get_alter_operations(clustered_table, normal_table) assert len(clustered_to_normal) == 1 - assert clustered_to_normal[0].meta[_CLUSTERING_META_KEY] == (clustered_table, None) + assert isinstance(clustered_to_normal[0], TableAlterDropClusterKeyOperation) + assert clustered_to_normal[0].target_table == clustered_table + assert not hasattr(clustered_to_normal[0], "clustering_key") # alter table add clustered - normal_to_clustered = engine_adapter.get_alter_expressions(normal_table, clustered_table) + normal_to_clustered = engine_adapter.get_alter_operations(normal_table, clustered_table) assert len(normal_to_clustered) == 1 - assert normal_to_clustered[0].meta[_CLUSTERING_META_KEY] == ( - normal_table, - [exp.to_column("c1")], - ) + operation = normal_to_clustered[0] + assert isinstance(operation, TableAlterChangeClusterKeyOperation) + assert operation.target_table == normal_table + assert operation.clustering_key == "(c1)" # alter table change clustering (c1 -> (c1, c2)) - clustered_to_clustered_differently = engine_adapter.get_alter_expressions( + clustered_to_clustered_differently = engine_adapter.get_alter_operations( clustered_table, clustered_differently_table ) assert len(clustered_to_clustered_differently) == 1 - assert clustered_to_clustered_differently[0].meta[_CLUSTERING_META_KEY] == ( - clustered_table, - [exp.to_column("c1"), exp.to_column("c2")], - ) + operation = clustered_to_clustered_differently[0] + assert isinstance(operation, TableAlterChangeClusterKeyOperation) + assert operation.target_table == clustered_table + assert operation.clustering_key == "(c1,c2)" # alter table change clustering ((c1, c2) -> c1) - clustered_differently_to_clustered = engine_adapter.get_alter_expressions( + clustered_differently_to_clustered = engine_adapter.get_alter_operations( clustered_differently_table, clustered_table ) assert len(clustered_differently_to_clustered) == 1 - assert clustered_differently_to_clustered[0].meta[_CLUSTERING_META_KEY] == ( - clustered_differently_table, - [exp.to_column("c1")], - ) + operation = clustered_differently_to_clustered[0] + assert isinstance(operation, TableAlterChangeClusterKeyOperation) + assert operation.target_table == clustered_differently_table + assert operation.clustering_key == "(c1)" def test_mutating_clustered_by_forward_only( diff --git a/tests/core/engine_adapter/integration/test_integration_snowflake.py b/tests/core/engine_adapter/integration/test_integration_snowflake.py index 12e45f1f14..01cbe1c0aa 100644 --- a/tests/core/engine_adapter/integration/test_integration_snowflake.py +++ b/tests/core/engine_adapter/integration/test_integration_snowflake.py @@ -52,42 +52,42 @@ def test_get_alter_expressions_includes_clustering( ) engine_adapter.execute(f"CREATE TABLE {normal_table} (c1 int, c2 timestamp)") - assert len(engine_adapter.get_alter_expressions(normal_table, normal_table)) == 0 - assert len(engine_adapter.get_alter_expressions(clustered_table, clustered_table)) == 0 + assert len(engine_adapter.get_alter_operations(normal_table, normal_table)) == 0 + assert len(engine_adapter.get_alter_operations(clustered_table, clustered_table)) == 0 # alter table drop clustered - clustered_to_normal = engine_adapter.get_alter_expressions(clustered_table, normal_table) + clustered_to_normal = engine_adapter.get_alter_operations(clustered_table, normal_table) assert len(clustered_to_normal) == 1 assert ( - clustered_to_normal[0].sql(dialect=ctx.dialect) + clustered_to_normal[0].expression.sql(dialect=ctx.dialect) == f"ALTER TABLE {clustered_table} DROP CLUSTERING KEY" ) # alter table add clustered - normal_to_clustered = engine_adapter.get_alter_expressions(normal_table, clustered_table) + normal_to_clustered = engine_adapter.get_alter_operations(normal_table, clustered_table) assert len(normal_to_clustered) == 1 assert ( - normal_to_clustered[0].sql(dialect=ctx.dialect) + normal_to_clustered[0].expression.sql(dialect=ctx.dialect) == f"ALTER TABLE {normal_table} CLUSTER BY (c1)" ) # alter table change clustering - clustered_to_clustered_differently = engine_adapter.get_alter_expressions( + clustered_to_clustered_differently = engine_adapter.get_alter_operations( clustered_table, clustered_differently_table ) assert len(clustered_to_clustered_differently) == 1 assert ( - clustered_to_clustered_differently[0].sql(dialect=ctx.dialect) + clustered_to_clustered_differently[0].expression.sql(dialect=ctx.dialect) == f"ALTER TABLE {clustered_table} CLUSTER BY (c1, TO_DATE(c2))" ) # alter table change clustering - clustered_differently_to_clustered = engine_adapter.get_alter_expressions( + clustered_differently_to_clustered = engine_adapter.get_alter_operations( clustered_differently_table, clustered_table ) assert len(clustered_differently_to_clustered) == 1 assert ( - clustered_differently_to_clustered[0].sql(dialect=ctx.dialect) + clustered_differently_to_clustered[0].expression.sql(dialect=ctx.dialect) == f"ALTER TABLE {clustered_differently_table} CLUSTER BY (c1)" ) diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index 02029ca6f8..3b25091e10 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -21,6 +21,9 @@ from sqlmesh.utils.errors import SQLMeshError, UnsupportedCatalogOperationError from tests.core.engine_adapter import to_sql_calls +if t.TYPE_CHECKING: + pass + pytestmark = pytest.mark.engine @@ -81,10 +84,10 @@ def test_create_view_pandas_source_columns(make_mocked_engine_adapter: t.Callabl bigint_dtype = exp.DataType.build("BIGINT") adapter.create_view( "test_view", - pd.DataFrame({"a": [1, 2, 3]}), + pd.DataFrame({"a": [1, 2, 3], "ignored_source": [4, 5, 6]}), target_columns_to_types={"a": bigint_dtype, "b": bigint_dtype}, replace=False, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ @@ -96,16 +99,16 @@ def test_create_view_query_source_columns(make_mocked_engine_adapter: t.Callable adapter = make_mocked_engine_adapter(EngineAdapter) adapter.create_view( "test_view", - parse_one("SELECT a FROM tbl"), + parse_one("SELECT a, ignored_source FROM tbl"), target_columns_to_types={ "a": exp.DataType.build("BIGINT"), "b": exp.DataType.build("BIGINT"), }, replace=False, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ - 'CREATE VIEW "test_view" ("a", "b") AS SELECT "a", CAST(NULL AS BIGINT) AS "b" FROM (SELECT "a" FROM "tbl") AS "select_source_columns"', + 'CREATE VIEW "test_view" ("a", "b") AS SELECT "a", CAST(NULL AS BIGINT) AS "b" FROM (SELECT "a", "ignored_source" FROM "tbl") AS "select_source_columns"', ] @@ -318,7 +321,7 @@ def test_insert_overwrite_by_time_partition_supports_insert_overwrite_pandas_sou ): adapter = make_mocked_engine_adapter(EngineAdapter) adapter.INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.INSERT_OVERWRITE - df = pd.DataFrame({"a": [1, 2]}) + df = pd.DataFrame({"a": [1, 2], "ignored_source": [3, 4]}) adapter.insert_overwrite_by_time_partition( "test_table", df, @@ -330,7 +333,7 @@ def test_insert_overwrite_by_time_partition_supports_insert_overwrite_pandas_sou "a": exp.DataType.build("INT"), "ds": exp.DataType.build("STRING"), }, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ """INSERT OVERWRITE TABLE "test_table" ("a", "ds") SELECT "a", "ds" FROM (SELECT CAST("a" AS INT) AS "a", CAST(NULL AS TEXT) AS "ds" FROM (VALUES (1), (2)) AS "t"("a")) AS "_subquery" WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02'""" @@ -344,7 +347,7 @@ def test_insert_overwrite_by_time_partition_supports_insert_overwrite_query_sour adapter.INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.INSERT_OVERWRITE adapter.insert_overwrite_by_time_partition( "test_table", - parse_one("SELECT a FROM tbl"), + parse_one("SELECT a, ignored_source FROM tbl"), start="2022-01-01", end="2022-01-02", time_column="ds", @@ -353,10 +356,10 @@ def test_insert_overwrite_by_time_partition_supports_insert_overwrite_query_sour "a": exp.DataType.build("INT"), "ds": exp.DataType.build("STRING"), }, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ - """INSERT OVERWRITE TABLE "test_table" ("a", "ds") SELECT "a", "ds" FROM (SELECT "a", CAST(NULL AS TEXT) AS "ds" FROM (SELECT "a" FROM "tbl") AS "select_source_columns") AS "_subquery" WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02'""" + """INSERT OVERWRITE TABLE "test_table" ("a", "ds") SELECT "a", "ds" FROM (SELECT "a", CAST(NULL AS TEXT) AS "ds" FROM (SELECT "a", "ignored_source" FROM "tbl") AS "select_source_columns") AS "_subquery" WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02'""" ] @@ -410,7 +413,7 @@ def test_insert_overwrite_by_time_partition_replace_where_pandas_source_columns( ): adapter = make_mocked_engine_adapter(EngineAdapter) adapter.INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.REPLACE_WHERE - df = pd.DataFrame({"a": [1, 2]}) + df = pd.DataFrame({"a": [1, 2], "ignored_source": [3, 4]}) adapter.insert_overwrite_by_time_partition( "test_table", df, @@ -422,7 +425,7 @@ def test_insert_overwrite_by_time_partition_replace_where_pandas_source_columns( "a": exp.DataType.build("INT"), "ds": exp.DataType.build("STRING"), }, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ """INSERT INTO "test_table" REPLACE WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02' SELECT "a", "ds" FROM (SELECT CAST("a" AS INT) AS "a", CAST(NULL AS TEXT) AS "ds" FROM (VALUES (1), (2)) AS "t"("a")) AS "_subquery" WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02'""" @@ -436,7 +439,7 @@ def test_insert_overwrite_by_time_partition_replace_where_query_source_columns( adapter.INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.REPLACE_WHERE adapter.insert_overwrite_by_time_partition( "test_table", - parse_one("SELECT a FROM tbl"), + parse_one("SELECT a, ignored_source FROM tbl"), start="2022-01-01", end="2022-01-02", time_column="ds", @@ -445,10 +448,10 @@ def test_insert_overwrite_by_time_partition_replace_where_query_source_columns( "a": exp.DataType.build("INT"), "ds": exp.DataType.build("STRING"), }, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ - """INSERT INTO "test_table" REPLACE WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02' SELECT "a", "ds" FROM (SELECT "a", CAST(NULL AS TEXT) AS "ds" FROM (SELECT "a" FROM "tbl") AS "select_source_columns") AS "_subquery" WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02'""" + """INSERT INTO "test_table" REPLACE WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02' SELECT "a", "ds" FROM (SELECT "a", CAST(NULL AS TEXT) AS "ds" FROM (SELECT "a", "ignored_source" FROM "tbl") AS "select_source_columns") AS "_subquery" WHERE "ds" BETWEEN '2022-01-01' AND '2022-01-02'""" ] @@ -572,7 +575,7 @@ def test_insert_append_pandas_batches(make_mocked_engine_adapter: t.Callable): def test_insert_append_pandas_source_columns(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) - df = pd.DataFrame({"a": [1, 2, 3]}) + df = pd.DataFrame({"a": [1, 2, 3], "ignored_source": [4, 5, 6]}) adapter.insert_append( "test_table", df, @@ -580,7 +583,7 @@ def test_insert_append_pandas_source_columns(make_mocked_engine_adapter: t.Calla "a": exp.DataType.build("INT"), "b": exp.DataType.build("INT"), }, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ 'INSERT INTO "test_table" ("a", "b") SELECT CAST("a" AS INT) AS "a", CAST(NULL AS INT) AS "b" FROM (VALUES (1), (2), (3)) AS "t"("a")', @@ -591,15 +594,15 @@ def test_insert_append_query_source_columns(make_mocked_engine_adapter: t.Callab adapter = make_mocked_engine_adapter(EngineAdapter) adapter.insert_append( "test_table", - parse_one("SELECT a FROM tbl"), + parse_one("SELECT a, ignored_source FROM tbl"), target_columns_to_types={ "a": exp.DataType.build("INT"), "b": exp.DataType.build("INT"), }, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ - 'INSERT INTO "test_table" ("a", "b") SELECT "a", CAST(NULL AS INT) AS "b" FROM (SELECT "a" FROM "tbl") AS "select_source_columns"', + 'INSERT INTO "test_table" ("a", "b") SELECT "a", CAST(NULL AS INT) AS "b" FROM (SELECT "a", "ignored_source" FROM "tbl") AS "select_source_columns"', ] @@ -1067,12 +1070,8 @@ def test_alter_table( adapter.SCHEMA_DIFFER = SchemaDiffer(**schema_differ_config) original_from_structs = adapter.SCHEMA_DIFFER._from_structs - def _from_structs( - current_struct: exp.DataType, new_struct: exp.DataType, *, ignore_destructive: bool = False - ) -> t.List[TableAlterOperation]: - operations = original_from_structs( - current_struct, new_struct, ignore_destructive=ignore_destructive - ) + def _from_structs(*args, **kwargs) -> t.List[TableAlterOperation]: + operations = original_from_structs(*args, **kwargs) if not operations: return operations assert ( @@ -1093,7 +1092,7 @@ def table_columns(table_name: str) -> t.Dict[str, exp.DataType]: adapter.columns = table_columns - adapter.alter_table(adapter.get_alter_expressions(current_table_name, target_table_name)) + adapter.alter_table(adapter.get_alter_operations(current_table_name, target_table_name)) adapter.cursor.begin.assert_called_once() adapter.cursor.commit.assert_called_once() @@ -1191,7 +1190,7 @@ def test_merge_upsert_pandas(make_mocked_engine_adapter: t.Callable): def test_merge_upsert_pandas_source_columns(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) - df = pd.DataFrame({"id": [1, 2, 3], "ts": [4, 5, 6]}) + df = pd.DataFrame({"id": [1, 2, 3], "ts": [4, 5, 6], "ignored_source": [7, 8, 9]}) adapter.merge( target_table="target", source_table=df, @@ -1201,7 +1200,7 @@ def test_merge_upsert_pandas_source_columns(make_mocked_engine_adapter: t.Callab "val": exp.DataType.build("int"), }, unique_key=[exp.to_identifier("id")], - source_columns=["id", "ts"], + source_columns=["id", "ignored_source", "ts"], ) adapter.cursor.execute.assert_called_once_with( 'MERGE INTO "target" AS "__MERGE_TARGET__" USING (SELECT CAST("id" AS INT) AS "id", CAST("ts" AS TIMESTAMP) AS "ts", CAST(NULL AS INT) AS "val" FROM (VALUES (1, 4), (2, 5), (3, 6)) AS "t"("id", "ts")) AS "__MERGE_SOURCE__" ON "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id" ' @@ -1214,17 +1213,17 @@ def test_merge_upsert_query_source_columns(make_mocked_engine_adapter: t.Callabl adapter = make_mocked_engine_adapter(EngineAdapter) adapter.merge( target_table="target", - source_table=parse_one("SELECT id, ts FROM source"), + source_table=parse_one("SELECT id, ts, ignored_source FROM source"), target_columns_to_types={ "id": exp.DataType.build("int"), "ts": exp.DataType.build("timestamp"), "val": exp.DataType.build("int"), }, unique_key=[exp.to_identifier("id")], - source_columns=["id", "ts"], + source_columns=["id", "ts", "ignored_source"], ) adapter.cursor.execute.assert_called_once_with( - 'MERGE INTO "target" AS "__MERGE_TARGET__" USING (SELECT "id", "ts", CAST(NULL AS INT) AS "val" FROM (SELECT "id", "ts" FROM "source") AS "select_source_columns") AS "__MERGE_SOURCE__" ON "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id" ' + 'MERGE INTO "target" AS "__MERGE_TARGET__" USING (SELECT "id", "ts", CAST(NULL AS INT) AS "val" FROM (SELECT "id", "ts", "ignored_source" FROM "source") AS "select_source_columns") AS "__MERGE_SOURCE__" ON "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id" ' 'WHEN MATCHED THEN UPDATE SET "__MERGE_TARGET__"."id" = "__MERGE_SOURCE__"."id", "__MERGE_TARGET__"."ts" = "__MERGE_SOURCE__"."ts", "__MERGE_TARGET__"."val" = "__MERGE_SOURCE__"."val" ' 'WHEN NOT MATCHED THEN INSERT ("id", "ts", "val") VALUES ("__MERGE_SOURCE__"."id", "__MERGE_SOURCE__"."ts", "__MERGE_SOURCE__"."val")' ) @@ -1639,6 +1638,7 @@ def test_scd_type_2_by_time_source_columns(make_mocked_engine_adapter: t.Callabl "2020-01-02 15:00:00", "2020-01-03 12:00:00", ], + "ignored_source": [4, 5, 6], } ) adapter.scd_type_2_by_time( @@ -1656,7 +1656,7 @@ def test_scd_type_2_by_time_source_columns(make_mocked_engine_adapter: t.Callabl "test_valid_from": exp.DataType.build("TIMESTAMP"), "test_valid_to": exp.DataType.build("TIMESTAMP"), }, - source_columns=["id", "name", "test_UPDATED_at"], + source_columns=["id", "name", "test_UPDATED_at", "ignored_source"], execution_time=datetime(2020, 1, 1, 0, 0, 0), start=datetime(2020, 1, 1, 0, 0, 0), is_restatement=True, @@ -3201,7 +3201,7 @@ def test_replace_query_pandas(make_mocked_engine_adapter: t.Callable): def test_replace_query_pandas_source_columns(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) - df = pd.DataFrame({"a": [1, 2, 3]}) + df = pd.DataFrame({"a": [1, 2, 3], "ignored_source": [4, 5, 6]}) adapter.replace_query( "test_table", df, @@ -3209,7 +3209,7 @@ def test_replace_query_pandas_source_columns(make_mocked_engine_adapter: t.Calla "a": exp.DataType.build("INT"), "b": exp.DataType.build("INT"), }, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ 'CREATE OR REPLACE TABLE "test_table" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT CAST("a" AS INT) AS "a", CAST(NULL AS INT) AS "b" FROM (VALUES (1), (2), (3)) AS "t"("a")) AS "_subquery"', @@ -3220,15 +3220,15 @@ def test_replace_query_query_source_columns(make_mocked_engine_adapter: t.Callab adapter = make_mocked_engine_adapter(EngineAdapter) adapter.replace_query( "test_table", - parse_one("SELECT a FROM tbl"), + parse_one("SELECT a, ignored_source FROM tbl"), target_columns_to_types={ "a": exp.DataType.build("INT"), "b": exp.DataType.build("INT"), }, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ - 'CREATE OR REPLACE TABLE "test_table" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT "a", CAST(NULL AS INT) AS "b" FROM (SELECT "a" FROM "tbl") AS "select_source_columns") AS "_subquery"', + 'CREATE OR REPLACE TABLE "test_table" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT "a", CAST(NULL AS INT) AS "b" FROM (SELECT "a", "ignored_source" FROM "tbl") AS "select_source_columns") AS "_subquery"', ] @@ -3382,7 +3382,7 @@ def test_ctas_pandas(make_mocked_engine_adapter: t.Callable): def test_ctas_pandas_source_columns(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) - df = pd.DataFrame({"a": [1, 2, 3]}) + df = pd.DataFrame({"a": [1, 2, 3], "ignored_source": [4, 5, 6]}) adapter.ctas( "test_table", df, @@ -3390,7 +3390,7 @@ def test_ctas_pandas_source_columns(make_mocked_engine_adapter: t.Callable): "a": exp.DataType.build("INT"), "b": exp.DataType.build("INT"), }, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ 'CREATE TABLE IF NOT EXISTS "test_table" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT CAST("a" AS INT) AS "a", CAST(NULL AS INT) AS "b" FROM (VALUES (1), (2), (3)) AS "t"("a")) AS "_subquery"', @@ -3401,15 +3401,15 @@ def test_ctas_query_source_columns(make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) adapter.ctas( "test_table", - parse_one("SELECT a FROM tbl"), + parse_one("SELECT a, ignored_source FROM tbl"), target_columns_to_types={ "a": exp.DataType.build("INT"), "b": exp.DataType.build("INT"), }, - source_columns=["a"], + source_columns=["a", "ignored_source"], ) assert to_sql_calls(adapter) == [ - 'CREATE TABLE IF NOT EXISTS "test_table" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT "a", CAST(NULL AS INT) AS "b" FROM (SELECT "a" FROM "tbl") AS "select_source_columns") AS "_subquery"', + 'CREATE TABLE IF NOT EXISTS "test_table" AS SELECT CAST("a" AS INT) AS "a", CAST("b" AS INT) AS "b" FROM (SELECT "a", CAST(NULL AS INT) AS "b" FROM (SELECT "a", "ignored_source" FROM "tbl") AS "select_source_columns") AS "_subquery"', ] diff --git a/tests/core/engine_adapter/test_bigquery.py b/tests/core/engine_adapter/test_bigquery.py index f5a287defb..25aa4006b5 100644 --- a/tests/core/engine_adapter/test_bigquery.py +++ b/tests/core/engine_adapter/test_bigquery.py @@ -1053,7 +1053,7 @@ def test_get_alter_expressions_includes_catalog( ) get_data_objects_mock.return_value = [] - adapter.get_alter_expressions("catalog1.foo.bar", "catalog2.bar.bing") + adapter.get_alter_operations("catalog1.foo.bar", "catalog2.bar.bing") assert get_data_objects_mock.call_count == 2 diff --git a/tests/core/engine_adapter/test_clickhouse.py b/tests/core/engine_adapter/test_clickhouse.py index 3e92a8fe9b..b75609e759 100644 --- a/tests/core/engine_adapter/test_clickhouse.py +++ b/tests/core/engine_adapter/test_clickhouse.py @@ -172,7 +172,7 @@ def table_columns(table_name: str) -> t.Dict[str, exp.DataType]: adapter.columns = table_columns # type: ignore # ON CLUSTER not added because engine_run_mode.is_cluster=False - adapter.alter_table(adapter.get_alter_expressions(current_table_name, target_table_name)) + adapter.alter_table(adapter.get_alter_operations(current_table_name, target_table_name)) mocker.patch.object( ClickhouseEngineAdapter, @@ -185,7 +185,7 @@ def table_columns(table_name: str) -> t.Dict[str, exp.DataType]: new_callable=mocker.PropertyMock(return_value=EngineRunMode.CLUSTER), ) - adapter.alter_table(adapter.get_alter_expressions(current_table_name, target_table_name)) + adapter.alter_table(adapter.get_alter_operations(current_table_name, target_table_name)) assert to_sql_calls(adapter) == [ 'ALTER TABLE "test_table" DROP COLUMN "c"', diff --git a/tests/core/engine_adapter/test_postgres.py b/tests/core/engine_adapter/test_postgres.py index 5d05dd653c..6134126a41 100644 --- a/tests/core/engine_adapter/test_postgres.py +++ b/tests/core/engine_adapter/test_postgres.py @@ -157,7 +157,7 @@ def table_columns(table_name: str) -> t.Dict[str, exp.DataType]: adapter.columns = table_columns - adapter.alter_table(adapter.get_alter_expressions(current_table_name, target_table_name)) + adapter.alter_table(adapter.get_alter_operations(current_table_name, target_table_name)) assert to_sql_calls(adapter) == [ 'ALTER TABLE "test_table" DROP COLUMN "test_column" CASCADE', ] diff --git a/tests/core/engine_adapter/test_redshift.py b/tests/core/engine_adapter/test_redshift.py index 17c3dd1866..c5e3dfff17 100644 --- a/tests/core/engine_adapter/test_redshift.py +++ b/tests/core/engine_adapter/test_redshift.py @@ -365,7 +365,7 @@ def table_columns(table_name: str) -> t.Dict[str, exp.DataType]: adapter.columns = table_columns - adapter.alter_table(adapter.get_alter_expressions(current_table_name, target_table_name)) + adapter.alter_table(adapter.get_alter_operations(current_table_name, target_table_name)) assert to_sql_calls(adapter) == [ 'ALTER TABLE "test_table" DROP COLUMN "test_column" CASCADE', ] @@ -388,7 +388,7 @@ def table_columns(table_name: str) -> t.Dict[str, exp.DataType]: adapter.columns = table_columns - adapter.alter_table(adapter.get_alter_expressions(current_table_name, target_table_name)) + adapter.alter_table(adapter.get_alter_operations(current_table_name, target_table_name)) assert to_sql_calls(adapter) == [ 'ALTER TABLE "test_table" ALTER COLUMN "test_column" TYPE VARCHAR(20)', ] @@ -411,7 +411,7 @@ def table_columns(table_name: str) -> t.Dict[str, exp.DataType]: adapter.columns = table_columns - adapter.alter_table(adapter.get_alter_expressions(current_table_name, target_table_name)) + adapter.alter_table(adapter.get_alter_operations(current_table_name, target_table_name)) assert to_sql_calls(adapter) == [ 'ALTER TABLE "test_table" DROP COLUMN "test_column" CASCADE', 'ALTER TABLE "test_table" ADD COLUMN "test_column" DECIMAL(25, 10)', diff --git a/tests/core/engine_adapter/test_spark.py b/tests/core/engine_adapter/test_spark.py index 55a925b995..2e4f6ae2a0 100644 --- a/tests/core/engine_adapter/test_spark.py +++ b/tests/core/engine_adapter/test_spark.py @@ -162,7 +162,7 @@ def table_columns(table_name: str) -> t.Dict[str, exp.DataType]: adapter.columns = table_columns - adapter.alter_table(adapter.get_alter_expressions(current_table_name, target_table_name)) + adapter.alter_table(adapter.get_alter_operations(current_table_name, target_table_name)) adapter.cursor.execute.assert_has_calls( [ diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 827d84e8b9..517d7c3ca1 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -7967,6 +7967,215 @@ def test_incremental_by_time_model_ignore_destructive_change(tmp_path: Path): context.close() +def test_incremental_by_time_model_ignore_additive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + 'other' as other_column, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column to the source table + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'other' as other_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("ALTER TABLE source_table ADD COLUMN new_column INT") + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is removed since destructive is allowed + assert "name" not in updated_df.columns + # new_column is not added since additive is ignored + assert "new_column" not in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was applied + assert "name" not in updated_df.columns + # new_column is still not added since additive is ignored + assert "new_column" not in updated_df.columns + + with time_machine.travel("2023-01-11 00:00:00 UTC"): + updated_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + CAST(1 AS STRING) as id, + 'other' as other_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(updated_model) + + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.SCHEMA_DIFFER.compatible_types = { + exp.DataType.build("INT"): {exp.DataType.build("STRING")} + } + context.plan("prod", auto_apply=True, no_prompts=True, run=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 3 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is still not added since additive is ignored + assert "new_column" not in updated_df.columns + # The additive change was ignored since we set the change as compatible therefore + # instead of getting strings in the result we still return ints + assert updated_df["id"].tolist() == [1, 1, 1] + + with time_machine.travel("2023-01-12 00:00:00 UTC"): + updated_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change allow + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + CAST(1 AS STRING) as id, + 'other' as other_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(updated_model) + + context = Context(paths=[tmp_path], config=config) + # Make the change compatible since that means we will attempt and alter now that is considered additive + context.engine_adapter.SCHEMA_DIFFER.compatible_types = { + exp.DataType.build("INT"): {exp.DataType.build("STRING")} + } + context.plan("prod", auto_apply=True, no_prompts=True, run=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 4 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is now added since it is additive is now allowed + assert "new_column" in updated_df.columns + # The change is now reflected since an additive alter could be performed + assert updated_df["id"].dropna().tolist() == ["1", "1", "1", "1"] + + context.close() + + def test_incremental_by_unique_key_model_ignore_destructive_change(tmp_path: Path): models_dir = tmp_path / "models" models_dir.mkdir() @@ -8080,7 +8289,7 @@ def test_incremental_by_unique_key_model_ignore_destructive_change(tmp_path: Pat context.close() -def test_incremental_unmanaged_model_ignore_destructive_change(tmp_path: Path): +def test_incremental_by_unique_key_model_ignore_additive_change(tmp_path: Path): models_dir = tmp_path / "models" models_dir.mkdir() data_dir = tmp_path / "data" @@ -8096,14 +8305,17 @@ def test_incremental_unmanaged_model_ignore_destructive_change(tmp_path: Path): initial_model = f""" MODEL ( name test_model, - kind INCREMENTAL_UNMANAGED( - on_destructive_change ignore + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key id, + forward_only true, + on_destructive_change allow, + on_additive_change ignore ), start '2023-01-01', cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -8138,14 +8350,17 @@ def test_incremental_unmanaged_model_ignore_destructive_change(tmp_path: Path): initial_model = """ MODEL ( name test_model, - kind INCREMENTAL_UNMANAGED( - on_destructive_change ignore + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key id, + forward_only true, + on_destructive_change allow, + on_additive_change ignore ), start '2023-01-01', cron '@daily' ); - SELECT + SELECT *, 2 as id, 3 as new_column, @@ -8162,6 +8377,567 @@ def test_incremental_unmanaged_model_ignore_destructive_change(tmp_path: Path): # The existing data should still be there and new data should be loaded updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still not in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + +def test_incremental_unmanaged_model_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_UNMANAGED( + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_UNMANAGED( + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_incremental_unmanaged_model_ignore_additive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_UNMANAGED( + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_UNMANAGED( + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + +def test_scd_type_2_by_time_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + updated_at_name ds, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_dt as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + updated_at_name ds, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 3 as new_column, + @start_dt as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_scd_type_2_by_time_ignore_additive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + updated_at_name ds, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_dt as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + updated_at_name ds, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 3 as new_column, + @start_dt as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + +def test_scd_type_2_by_column_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key id, + columns [name], + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key id, + columns [new_column], + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 1 assert "source_id" in initial_df.columns assert "id" in updated_df.columns @@ -8189,7 +8965,7 @@ def test_incremental_unmanaged_model_ignore_destructive_change(tmp_path: Path): context.close() -def test_scd_type_2_by_time_ignore_destructive_change(tmp_path: Path): +def test_scd_type_2_by_column_ignore_additive_change(tmp_path: Path): models_dir = tmp_path / "models" models_dir.mkdir() data_dir = tmp_path / "data" @@ -8203,25 +8979,27 @@ def test_scd_type_2_by_time_ignore_destructive_change(tmp_path: Path): # Initial model with 3 columns initial_model = f""" - MODEL ( - name test_model, - kind SCD_TYPE_2_BY_TIME ( - unique_key id, - updated_at_name ds, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key id, + columns [stable], + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); - SELECT - *, - 1 as id, - 'test_name' as name, - @start_dt as ds - FROM - source_table; - """ + SELECT + *, + 1 as id, + 'test_name' as name, + 'stable' as stable, + @start_ds as ds + FROM + source_table; + """ # Write initial model (models_dir / "test_model.sql").write_text(initial_model) @@ -8247,25 +9025,27 @@ def test_scd_type_2_by_time_ignore_destructive_change(tmp_path: Path): # remove `name` column and add new column initial_model = """ - MODEL ( - name test_model, - kind SCD_TYPE_2_BY_TIME ( - unique_key id, - updated_at_name ds, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key id, + columns [stable], + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); - SELECT - *, - 1 as id, - 3 as new_column, - @start_dt as ds - FROM - source_table; - """ + SELECT + *, + 1 as id, + 'stable2' as stable, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ (models_dir / "test_model.sql").write_text(initial_model) context = Context(paths=[tmp_path], config=config) @@ -8279,10 +9059,10 @@ def test_scd_type_2_by_time_ignore_destructive_change(tmp_path: Path): assert "source_id" in initial_df.columns assert "id" in updated_df.columns assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns + # name is not still in table since destructive was ignored + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns context.close() @@ -8294,15 +9074,15 @@ def test_scd_type_2_by_time_ignore_destructive_change(tmp_path: Path): assert "source_id" in initial_df.columns assert "id" in updated_df.columns assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns context.close() -def test_scd_type_2_by_column_ignore_destructive_change(tmp_path: Path): +def test_incremental_partition_ignore_destructive_change(tmp_path: Path): models_dir = tmp_path / "models" models_dir.mkdir() data_dir = tmp_path / "data" @@ -8318,11 +9098,10 @@ def test_scd_type_2_by_column_ignore_destructive_change(tmp_path: Path): initial_model = f""" MODEL ( name test_model, - kind SCD_TYPE_2_BY_COLUMN ( - unique_key id, - columns [name], + kind INCREMENTAL_BY_PARTITION ( on_destructive_change ignore ), + partitioned_by [ds], start '2023-01-01', cron '@daily' ); @@ -8362,11 +9141,10 @@ def test_scd_type_2_by_column_ignore_destructive_change(tmp_path: Path): initial_model = """ MODEL ( name test_model, - kind SCD_TYPE_2_BY_COLUMN ( - unique_key id, - columns [new_column], + kind INCREMENTAL_BY_PARTITION ( on_destructive_change ignore ), + partitioned_by [ds], start '2023-01-01', cron '@daily' ); @@ -8415,7 +9193,7 @@ def test_scd_type_2_by_column_ignore_destructive_change(tmp_path: Path): context.close() -def test_incremental_partition_ignore_destructive_change(tmp_path: Path): +def test_incremental_partition_ignore_additive_change(tmp_path: Path): models_dir = tmp_path / "models" models_dir.mkdir() data_dir = tmp_path / "data" @@ -8432,14 +9210,15 @@ def test_incremental_partition_ignore_destructive_change(tmp_path: Path): MODEL ( name test_model, kind INCREMENTAL_BY_PARTITION ( - on_destructive_change ignore + on_destructive_change allow, + on_additive_change ignore ), partitioned_by [ds], start '2023-01-01', cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -8475,7 +9254,8 @@ def test_incremental_partition_ignore_destructive_change(tmp_path: Path): MODEL ( name test_model, kind INCREMENTAL_BY_PARTITION ( - on_destructive_change ignore + on_destructive_change allow, + on_additive_change ignore ), partitioned_by [ds], start '2023-01-01', @@ -8483,7 +9263,7 @@ def test_incremental_partition_ignore_destructive_change(tmp_path: Path): ); SELECT - *, + *, 1 as id, 3 as new_column, @start_ds as ds @@ -8503,10 +9283,10 @@ def test_incremental_partition_ignore_destructive_change(tmp_path: Path): assert "source_id" in initial_df.columns assert "id" in updated_df.columns assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns context.close() @@ -8518,10 +9298,10 @@ def test_incremental_partition_ignore_destructive_change(tmp_path: Path): assert "source_id" in initial_df.columns assert "id" in updated_df.columns assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns context.close() @@ -8686,3 +9466,169 @@ def test_incremental_by_time_model_ignore_destructive_change_unit_test(tmp_path: assert test_result.testsRun == len(test_result.successes) context.close() + + +def test_incremental_by_time_model_ignore_additive_change_unit_test(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + test_dir = tmp_path / "tests" + test_dir.mkdir() + test_filepath = test_dir / "test_test_model.yaml" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + id, + name, + ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + initial_test = f""" + +test_test_model: + model: test_model + inputs: + source_table: + - id: 1 + name: 'test_name' + ds: '2025-01-01' + outputs: + query: + - id: 1 + name: 'test_name' + ds: '2025-01-01' +""" + + # Write initial test + test_filepath.write_text(initial_test) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute( + "CREATE TABLE source_table (id INT, name STRING, new_column INT, ds STRING)" + ) + context.engine_adapter.execute( + "INSERT INTO source_table VALUES (1, 'test_name', NULL, '2023-01-01')" + ) + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + test_result = context.test() + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + id, + new_column, + ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + # `new_column` is in the output since unit tests are based on the model definition that currently + # exists and doesn't take into account the historical changes to the table. Therefore `new_column` is + # not actually in the table but it is represented in the test + updated_test = f""" + test_test_model: + model: test_model + inputs: + source_table: + - id: 1 + new_column: 3 + ds: '2025-01-01' + outputs: + query: + - id: 1 + new_column: 3 + ds: '2025-01-01' + """ + + # Write initial test + test_filepath.write_text(updated_test) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + test_result = context.test() + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 1 + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not in table since destructive was ignored + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("INSERT INTO source_table VALUES (2, NULL, 3, '2023-01-09')") + context.run() + test_result = context.test() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still not in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() diff --git a/tests/core/test_model.py b/tests/core/test_model.py index df758713a6..81b74ab32a 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -1814,7 +1814,8 @@ def test_render_definition(): partition_by_time_column TRUE, forward_only FALSE, disable_restatement FALSE, - on_destructive_change 'ERROR' + on_destructive_change 'ERROR', + on_additive_change 'ALLOW' ), storage_format iceberg, partitioned_by `a`, @@ -5522,7 +5523,8 @@ def test_when_matched(): batch_concurrency 1, forward_only FALSE, disable_restatement FALSE, - on_destructive_change 'ERROR' + on_destructive_change 'ERROR', + on_additive_change 'ALLOW' ) ); @@ -5574,7 +5576,8 @@ def fingerprint_merge( batch_concurrency 1, forward_only FALSE, disable_restatement FALSE, - on_destructive_change 'ERROR' + on_destructive_change 'ERROR', + on_additive_change 'ALLOW' ) ); @@ -7466,6 +7469,46 @@ def test_forward_only_on_destructive_change_config() -> None: assert context_model.on_destructive_change.is_allow +def test_model_meta_on_additive_change_property() -> None: + """Test that ModelMeta has on_additive_change property that works like on_destructive_change.""" + from sqlmesh.core.model.kind import IncrementalByTimeRangeKind, OnAdditiveChange + from sqlmesh.core.model.meta import ModelMeta + + # Test incremental model with on_additive_change=ERROR + incremental_kind = IncrementalByTimeRangeKind( + time_column="c", + forward_only=True, + on_additive_change=OnAdditiveChange.ERROR, + ) + model_meta = ModelMeta(name="test_model", kind=incremental_kind) + assert model_meta.on_additive_change == OnAdditiveChange.ERROR + + # Test incremental model with on_additive_change=WARN + incremental_kind = IncrementalByTimeRangeKind( + time_column="c", + forward_only=True, + on_additive_change=OnAdditiveChange.WARN, + ) + model_meta = ModelMeta(name="test_model", kind=incremental_kind) + assert model_meta.on_additive_change == OnAdditiveChange.WARN + + # Test incremental model with default on_additive_change (should be ALLOW) + incremental_kind = IncrementalByTimeRangeKind( + time_column="c", + forward_only=True, + ) + model_meta = ModelMeta(name="test_model", kind=incremental_kind) + assert model_meta.on_additive_change == OnAdditiveChange.ALLOW + + incremental_kind = IncrementalByTimeRangeKind( + time_column="c", + forward_only=True, + on_additive_change=OnAdditiveChange.IGNORE, + ) + model_meta = ModelMeta(name="test_model", kind=incremental_kind) + assert model_meta.on_additive_change == OnAdditiveChange.IGNORE + + def test_incremental_by_partition(sushi_context, assert_exp_eq): expressions = d.parse( """ @@ -7784,7 +7827,8 @@ def test_model_kind_to_expression(): partition_by_time_column TRUE, forward_only FALSE, disable_restatement FALSE, -on_destructive_change 'ERROR' +on_destructive_change 'ERROR', +on_additive_change 'ALLOW' )""" ) @@ -7818,7 +7862,8 @@ def test_model_kind_to_expression(): lookback 3, forward_only TRUE, disable_restatement TRUE, -on_destructive_change 'WARN' +on_destructive_change 'WARN', +on_additive_change 'ALLOW' )""" ) @@ -7843,7 +7888,8 @@ def test_model_kind_to_expression(): batch_concurrency 1, forward_only FALSE, disable_restatement FALSE, -on_destructive_change 'ERROR' +on_destructive_change 'ERROR', +on_additive_change 'ALLOW' )""" ) @@ -7870,7 +7916,8 @@ def test_model_kind_to_expression(): batch_concurrency 1, forward_only FALSE, disable_restatement FALSE, -on_destructive_change 'ERROR' +on_destructive_change 'ERROR', +on_additive_change 'ALLOW' )""" ) @@ -7898,7 +7945,8 @@ def test_model_kind_to_expression(): batch_concurrency 1, forward_only FALSE, disable_restatement FALSE, -on_destructive_change 'ERROR' +on_destructive_change 'ERROR', +on_additive_change 'ALLOW' )""" ) @@ -7920,7 +7968,8 @@ def test_model_kind_to_expression(): == """INCREMENTAL_BY_PARTITION ( forward_only TRUE, disable_restatement FALSE, -on_destructive_change 'ERROR' +on_destructive_change 'ERROR', +on_additive_change 'ALLOW' )""" ) @@ -7972,7 +8021,8 @@ def test_model_kind_to_expression(): time_data_type TIMESTAMP, forward_only TRUE, disable_restatement TRUE, -on_destructive_change 'ERROR' +on_destructive_change 'ERROR', +on_additive_change 'ALLOW' )""" ) @@ -8003,7 +8053,8 @@ def test_model_kind_to_expression(): time_data_type TIMESTAMP, forward_only TRUE, disable_restatement TRUE, -on_destructive_change 'ERROR' +on_destructive_change 'ERROR', +on_additive_change 'ALLOW' )""" ) @@ -8034,7 +8085,8 @@ def test_model_kind_to_expression(): time_data_type TIMESTAMP, forward_only TRUE, disable_restatement TRUE, -on_destructive_change 'ERROR' +on_destructive_change 'ERROR', +on_additive_change 'ALLOW' )""" ) @@ -8215,7 +8267,8 @@ def test_merge_filter(): batch_concurrency 1, forward_only FALSE, disable_restatement FALSE, - on_destructive_change 'ERROR' + on_destructive_change 'ERROR', + on_additive_change 'ALLOW' ) ); @@ -8942,6 +8995,7 @@ def test_auto_restatement(): forward_only FALSE, disable_restatement FALSE, on_destructive_change 'ERROR', + on_additive_change 'ALLOW', auto_restatement_cron '@daily' )""" ) @@ -8971,6 +9025,7 @@ def test_auto_restatement(): forward_only FALSE, disable_restatement FALSE, on_destructive_change 'ERROR', + on_additive_change 'ALLOW', auto_restatement_cron '@daily' )""" ) diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 7254a924b1..c9c19376d9 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -4,6 +4,8 @@ from unittest.mock import patch import pytest + +from sqlmesh.core.console import TerminalConsole from sqlmesh.utils.metaprogramming import Executable from tests.core.test_table_diff import create_test_console import time_machine @@ -24,7 +26,7 @@ SqlModel, ModelKindName, ) -from sqlmesh.core.model.kind import OnDestructiveChange +from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange from sqlmesh.core.model.seed import Seed from sqlmesh.core.plan import Plan, PlanBuilder, SnapshotIntervals from sqlmesh.core.snapshot import ( @@ -480,6 +482,75 @@ def test_forward_only_plan_allow_destructive_models( assert mock_logger.call_count == 0 +def test_forward_only_plan_allow_additive_models( + mocker, make_snapshot, make_snapshot_on_additive_change +): + # forward-only model, not forward-only plan + snapshot_a_old, snapshot_a = make_snapshot_on_additive_change() + + context_diff_a = ContextDiff( + environment="prod", + is_new_environment=False, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={snapshot_a.name: (snapshot_a, snapshot_a_old)}, + snapshots={snapshot_a.snapshot_id: snapshot_a, snapshot_a_old.snapshot_id: snapshot_a_old}, + new_snapshots={snapshot_a.snapshot_id: snapshot_a}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + with pytest.raises(PlanError, match="Plan requires an additive change to a forward-only model"): + PlanBuilder(context_diff_a, forward_only=False).build() + + console = TerminalConsole() + log_warning_spy = mocker.spy(console, "log_warning") + assert PlanBuilder( + context_diff_a, forward_only=False, allow_additive_models=['"a"'], console=console + ).build() + assert log_warning_spy.call_count == 0 + + snapshot_a_old, snapshot_a = make_snapshot_on_additive_change( + on_additive_change=OnAdditiveChange.WARN + ) + + context_diff_a = ContextDiff( + environment="prod", + is_new_environment=False, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={snapshot_a.name: (snapshot_a, snapshot_a_old)}, + snapshots={snapshot_a.snapshot_id: snapshot_a, snapshot_a_old.snapshot_id: snapshot_a_old}, + new_snapshots={snapshot_a.snapshot_id: snapshot_a}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + log_warning_spy.reset_mock() + assert PlanBuilder(context_diff_a, forward_only=False, console=console).build() + log_warning_spy.assert_called_once_with(""" +Plan requires additive change to forward-only model '"a"'s schema that adds column 'three'. + +Schema changes: + ALTER TABLE "a" ADD COLUMN three TEXT""") + + def test_forward_only_model_on_destructive_change( make_snapshot, make_snapshot_on_destructive_change ): @@ -748,6 +819,7 @@ def test_missing_intervals_lookback(make_snapshot, mocker: MockerFixture): no_gaps=False, forward_only=False, allow_destructive_models=set(), + allow_additive_models=set(), include_unmodified=True, environment_naming_info=EnvironmentNamingInfo(), directly_modified={snapshot_a.snapshot_id}, @@ -3276,6 +3348,645 @@ def _build_plan() -> Plan: assert to_datetime(plan.execution_time) == to_datetime(output_execution_time) +def test_plan_builder_additive_change_error_blocks_plan(make_snapshot): + """Test that additive changes block plan when on_additive_change=ERROR.""" + # Create models with actual schema differences + # Use explicit column schemas in CTE so columns_to_types can be determined + old_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, '2022-01-01'::DATE as ds + ) + select id, name, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.ERROR, + ), + ) + + # New model with additional column (additive change) + new_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, 'email@test.com'::VARCHAR as email, '2022-01-01'::DATE as ds + ) + select id, name, email, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.ERROR, + ), + ) + + old_snapshot = make_snapshot(old_model) + new_snapshot = make_snapshot(new_model) + + # Set previous versions to simulate a modification + new_snapshot.previous_versions = ( + SnapshotDataVersion( + fingerprint=SnapshotFingerprint( + data_hash="old_data_hash", + metadata_hash="old_metadata_hash", + ), + version="old_version", + change_category=SnapshotChangeCategory.FORWARD_ONLY, + dev_table_suffix="dev", + ), + ) + + context_diff = ContextDiff( + environment="prod", + is_new_environment=False, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={old_snapshot.name: (new_snapshot, old_snapshot)}, + snapshots={ + old_snapshot.snapshot_id: old_snapshot, + new_snapshot.snapshot_id: new_snapshot, + }, + new_snapshots={new_snapshot.snapshot_id: new_snapshot}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + builder = PlanBuilder(context_diff, forward_only=True) + + # Should raise PlanError for additive changes when on_additive_change=ERROR + with pytest.raises(PlanError, match="additive change"): + builder.build() + + +def test_plan_builder_additive_change_warn_allows_plan(make_snapshot): + """Test that additive changes allow plan with warning when on_additive_change=WARN.""" + old_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, '2022-01-01'::DATE as ds + ) + select id, name, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.WARN, + ), + ) + + new_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, 'email@test.com'::VARCHAR as email, '2022-01-01'::DATE as ds + ) + select id, name, email, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.WARN, + ), + ) + + old_snapshot = make_snapshot(old_model) + new_snapshot = make_snapshot(new_model) + + # Set previous versions to simulate a modification + new_snapshot.previous_versions = ( + SnapshotDataVersion( + fingerprint=SnapshotFingerprint( + data_hash="old_data_hash", + metadata_hash="old_metadata_hash", + ), + version="old_version", + change_category=SnapshotChangeCategory.FORWARD_ONLY, + dev_table_suffix="dev", + ), + ) + + context_diff = ContextDiff( + environment="prod", + is_new_environment=False, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={old_snapshot.name: (new_snapshot, old_snapshot)}, + snapshots={ + old_snapshot.snapshot_id: old_snapshot, + new_snapshot.snapshot_id: new_snapshot, + }, + new_snapshots={new_snapshot.snapshot_id: new_snapshot}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + builder = PlanBuilder(context_diff, forward_only=True) + + # Should log warning but not fail + with patch.object(builder._console, "log_additive_change") as mock_log_additive: + plan = builder.build() + assert plan is not None + mock_log_additive.assert_called() # Should have logged an additive change + + +def test_plan_builder_additive_change_allow_permits_plan(make_snapshot): + """Test that additive changes are permitted when on_additive_change=ALLOW.""" + old_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, '2022-01-01'::DATE as ds + ) + select id, name, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.ALLOW, + ), + ) + + new_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, 'email@test.com'::VARCHAR as email, '2022-01-01'::DATE as ds + ) + select id, name, email, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.ALLOW, + ), + ) + + old_snapshot = make_snapshot(old_model) + new_snapshot = make_snapshot(new_model) + + # Set previous versions to simulate a modification + new_snapshot.previous_versions = ( + SnapshotDataVersion( + fingerprint=SnapshotFingerprint( + data_hash="old_data_hash", + metadata_hash="old_metadata_hash", + ), + version="old_version", + change_category=SnapshotChangeCategory.FORWARD_ONLY, + dev_table_suffix="dev", + ), + ) + + context_diff = ContextDiff( + environment="prod", + is_new_environment=False, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={old_snapshot.name: (new_snapshot, old_snapshot)}, + snapshots={ + old_snapshot.snapshot_id: old_snapshot, + new_snapshot.snapshot_id: new_snapshot, + }, + new_snapshots={new_snapshot.snapshot_id: new_snapshot}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + builder = PlanBuilder(context_diff, forward_only=True) + + # Should build plan without issues + plan = builder.build() + assert plan is not None + + +def test_plan_builder_additive_change_ignore_skips_validation(make_snapshot): + """Test that additive changes are ignored when on_additive_change=IGNORE.""" + old_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, '2022-01-01'::DATE as ds + ) + select id, name, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.IGNORE, + ), + ) + + new_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, 'email@test.com'::VARCHAR as email, '2022-01-01'::DATE as ds + ) + select id, name, email, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.IGNORE, + ), + ) + + old_snapshot = make_snapshot(old_model) + new_snapshot = make_snapshot(new_model) + + # Set previous versions to simulate a modification + new_snapshot.previous_versions = ( + SnapshotDataVersion( + fingerprint=SnapshotFingerprint( + data_hash="old_data_hash", + metadata_hash="old_metadata_hash", + ), + version="old_version", + change_category=SnapshotChangeCategory.FORWARD_ONLY, + dev_table_suffix="dev", + ), + ) + + context_diff = ContextDiff( + environment="prod", + is_new_environment=False, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={old_snapshot.name: (new_snapshot, old_snapshot)}, + snapshots={ + old_snapshot.snapshot_id: old_snapshot, + new_snapshot.snapshot_id: new_snapshot, + }, + new_snapshots={new_snapshot.snapshot_id: new_snapshot}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + builder = PlanBuilder(context_diff, forward_only=True) + + # Should build plan without any validation + with patch("sqlmesh.core.plan.builder.logger.warning") as mock_warning: + plan = builder.build() + assert plan is not None + mock_warning.assert_not_called() # Should not log any warnings + + +def test_plan_builder_mixed_destructive_and_additive_changes(make_snapshot): + """Test scenarios with both destructive and additive changes.""" + # Test case: on_destructive_change=IGNORE, on_additive_change=ERROR + # Should ignore destructive changes but error on additive changes + old_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, 'old_value'::VARCHAR as old_col, '2022-01-01'::DATE as ds + ) + select id, name, old_col, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.ERROR, + ), + ) + + new_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, 'new_value'::VARCHAR as new_col, '2022-01-01'::DATE as ds + ) + select id, name, new_col, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.ERROR, + ), + ) + + old_snapshot = make_snapshot(old_model) + new_snapshot = make_snapshot(new_model) + + # Set previous versions to simulate a modification + new_snapshot.previous_versions = ( + SnapshotDataVersion( + fingerprint=SnapshotFingerprint( + data_hash="old_data_hash", + metadata_hash="old_metadata_hash", + ), + version="old_version", + change_category=SnapshotChangeCategory.FORWARD_ONLY, + dev_table_suffix="dev", + ), + ) + + context_diff = ContextDiff( + environment="prod", + is_new_environment=False, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={old_snapshot.name: (new_snapshot, old_snapshot)}, + snapshots={ + old_snapshot.snapshot_id: old_snapshot, + new_snapshot.snapshot_id: new_snapshot, + }, + new_snapshots={new_snapshot.snapshot_id: new_snapshot}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + builder = PlanBuilder(context_diff, forward_only=True) + + # Should error on additive change (new_col), but ignore destructive change (old_col removal) + with pytest.raises(PlanError, match="additive change"): + builder.build() + + +def test_plan_builder_allow_additive_models_flag(make_snapshot): + """Test that --allow-additive-model flag overrides on_additive_change=ERROR.""" + old_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, '2022-01-01'::DATE as ds + ) + select id, name, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.ERROR, + ), + ) + + # New model with additional column (additive change) + new_model = SqlModel( + name="test_model", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, 'email@test.com'::VARCHAR as email, '2022-01-01'::DATE as ds + ) + select id, name, email, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.ERROR, + ), + ) + + old_snapshot = make_snapshot(old_model) + new_snapshot = make_snapshot(new_model) + + # Set previous versions to simulate a modification + new_snapshot.previous_versions = ( + SnapshotDataVersion( + fingerprint=SnapshotFingerprint( + data_hash="old_data_hash", + metadata_hash="old_metadata_hash", + ), + version="old_version", + change_category=SnapshotChangeCategory.FORWARD_ONLY, + dev_table_suffix="dev", + ), + ) + + context_diff = ContextDiff( + environment="prod", + is_new_environment=False, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={new_snapshot.name: (new_snapshot, old_snapshot)}, + snapshots={new_snapshot.snapshot_id: new_snapshot}, + new_snapshots={new_snapshot.snapshot_id: new_snapshot}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + # First, verify that without the flag, the plan fails with additive change error + builder = PlanBuilder(context_diff, forward_only=True) + with pytest.raises(PlanError, match="additive change"): + builder.build() + + # Now test that the --allow-additive-model flag allows the plan to succeed + builder_with_flag = PlanBuilder( + context_diff, + forward_only=True, + allow_additive_models={'"test_model"'}, + ) + + # Should succeed without raising an exception + plan = builder_with_flag.build() + assert plan is not None + + +def test_plan_builder_allow_additive_models_pattern_matching(make_snapshot): + """Test that --allow-additive-model flag supports pattern matching like destructive models.""" + # Create two models with additive changes + old_model_1 = SqlModel( + name="test.model_1", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, '2022-01-01'::DATE as ds + ) + select id, name, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.ERROR, + ), + ) + + new_model_1 = SqlModel( + name="test.model_1", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, 'email@test.com'::VARCHAR as email, '2022-01-01'::DATE as ds + ) + select id, name, email, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.ERROR, + ), + ) + + old_model_2 = SqlModel( + name="other.model_2", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, '2022-01-01'::DATE as ds + ) + select id, name, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.ERROR, + ), + ) + + new_model_2 = SqlModel( + name="other.model_2", + dialect="duckdb", + query=parse_one(""" + with source as ( + select 1::INT as id, 'test'::VARCHAR as name, 'phone'::VARCHAR as phone, '2022-01-01'::DATE as ds + ) + select id, name, phone, ds from source + """), + kind=IncrementalByTimeRangeKind( + time_column="ds", + forward_only=True, + on_additive_change=OnAdditiveChange.ERROR, + ), + ) + + old_snapshot_1 = make_snapshot(old_model_1) + new_snapshot_1 = make_snapshot(new_model_1) + old_snapshot_2 = make_snapshot(old_model_2) + new_snapshot_2 = make_snapshot(new_model_2) + + # Set previous versions to simulate modifications + for new_snapshot in [new_snapshot_1, new_snapshot_2]: + new_snapshot.previous_versions = ( + SnapshotDataVersion( + fingerprint=SnapshotFingerprint( + data_hash="old_data_hash", + metadata_hash="old_metadata_hash", + ), + version="old_version", + change_category=SnapshotChangeCategory.FORWARD_ONLY, + dev_table_suffix="dev", + ), + ) + + context_diff = ContextDiff( + environment="prod", + is_new_environment=False, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={ + new_snapshot_1.name: (new_snapshot_1, old_snapshot_1), + new_snapshot_2.name: (new_snapshot_2, old_snapshot_2), + }, + snapshots={ + new_snapshot_1.snapshot_id: new_snapshot_1, + new_snapshot_2.snapshot_id: new_snapshot_2, + }, + new_snapshots={ + new_snapshot_1.snapshot_id: new_snapshot_1, + new_snapshot_2.snapshot_id: new_snapshot_2, + }, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + # Test pattern matching: allow only models in "test" schema + # In real usage, patterns would be expanded by Context.expand_model_selections + # Here we simulate what the expansion would produce + builder_with_pattern = PlanBuilder( + context_diff, + forward_only=True, + allow_additive_models={'"test"."model_1"'}, # Only allow test.model_1, not other.model_2 + ) + + # Should still fail because other.model_2 is not allowed + with pytest.raises(PlanError, match="additive change"): + builder_with_pattern.build() + + # Test allowing both patterns + builder_with_both = PlanBuilder( + context_diff, + forward_only=True, + allow_additive_models={'"test"."model_1"', '"other"."model_2"'}, # Allow both models + ) + + # Should succeed + plan = builder_with_both.build() + assert plan is not None + + def test_environment_statements_change_allows_dev_environment_creation(make_snapshot): snapshot = make_snapshot( SqlModel( diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index 7b172caf6a..744c7d18bf 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -105,6 +105,7 @@ def test_build_plan_stages_basic( restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -213,6 +214,7 @@ def test_build_plan_stages_with_before_all_and_after_all( restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -322,6 +324,7 @@ def test_build_plan_stages_select_models( restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -422,6 +425,7 @@ def test_build_plan_stages_basic_no_backfill( restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -541,6 +545,7 @@ def test_build_plan_stages_restatement( }, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -651,6 +656,7 @@ def test_build_plan_stages_forward_only( restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -778,6 +784,7 @@ def test_build_plan_stages_forward_only_dev( restatements={}, is_dev=True, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -902,6 +909,7 @@ def _get_snapshots(snapshot_ids: t.List[SnapshotId]) -> t.Dict[SnapshotId, Snaps restatements={}, is_dev=True, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -1038,6 +1046,7 @@ def test_build_plan_stages_forward_only_ensure_finalized_snapshots( restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=True, @@ -1113,6 +1122,7 @@ def test_build_plan_stages_removed_model( restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -1195,6 +1205,7 @@ def test_build_plan_stages_environment_suffix_target_changed( restatements={}, is_dev=True, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -1293,6 +1304,7 @@ def test_build_plan_stages_indirect_non_breaking_view_migration( restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -1378,6 +1390,7 @@ def test_build_plan_stages_virtual_environment_mode_filtering( restatements={}, is_dev=True, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -1430,6 +1443,7 @@ def test_build_plan_stages_virtual_environment_mode_filtering( restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -1492,6 +1506,7 @@ def test_build_plan_stages_virtual_environment_mode_filtering( restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -1565,6 +1580,7 @@ def test_build_plan_stages_virtual_environment_mode_no_updates( restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -1628,6 +1644,7 @@ def test_adjust_intervals_new_forward_only_dev_intervals( restatements={}, is_dev=True, # Dev environment allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -1695,6 +1712,7 @@ def test_adjust_intervals_restatement_removal( restatements=restatements, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, @@ -1787,6 +1805,7 @@ def test_adjust_intervals_should_force_rebuild(make_snapshot, mocker: MockerFixt restatements={}, is_dev=False, allow_destructive_models=set(), + allow_additive_models=set(), forward_only=False, end_bounded=False, ensure_finalized_snapshots=False, diff --git a/tests/core/test_schema_diff.py b/tests/core/test_schema_diff.py index fd14b0b9b3..916bead3e6 100644 --- a/tests/core/test_schema_diff.py +++ b/tests/core/test_schema_diff.py @@ -10,11 +10,14 @@ TableAlterColumnPosition, TableAlterOperation, get_schema_differ, + TableAlterAddColumnOperation, + TableAlterDropColumnOperation, + TableAlterChangeColumnTypeOperation, ) def test_schema_diff_calculate(): - alter_expressions = SchemaDiffer( + alter_operations = SchemaDiffer( **{ "support_positional_add": False, "support_nested_operations": False, @@ -24,7 +27,7 @@ def test_schema_diff_calculate(): }, } ).compare_columns( - "apply_to_table", + exp.to_table("apply_to_table"), { "id": exp.DataType.build("INT"), "name": exp.DataType.build("STRING"), @@ -39,7 +42,7 @@ def test_schema_diff_calculate(): }, ) - assert [x.sql() for x in alter_expressions] == [ + assert [x.expression.sql() for x in alter_operations] == [ """ALTER TABLE apply_to_table DROP COLUMN price""", """ALTER TABLE apply_to_table ADD COLUMN new_column DOUBLE""", """ALTER TABLE apply_to_table ALTER COLUMN name SET DATA TYPE INT""", @@ -55,7 +58,7 @@ def test_schema_diff_drop_cascade(): "drop_cascade": True, } ).compare_columns( - "apply_to_table", + exp.to_table("apply_to_table"), { "id": exp.DataType.build("INT"), "name": exp.DataType.build("STRING"), @@ -67,7 +70,7 @@ def test_schema_diff_drop_cascade(): }, ) - assert [x.sql() for x in alter_expressions] == [ + assert [x.expression.sql() for x in alter_expressions] == [ """ALTER TABLE apply_to_table DROP COLUMN price CASCADE""" ] @@ -83,7 +86,7 @@ def test_schema_diff_calculate_type_transitions(): }, } ).compare_columns( - "apply_to_table", + exp.to_table("apply_to_table"), { "id": exp.DataType.build("INT"), "ds": exp.DataType.build("STRING"), @@ -94,7 +97,7 @@ def test_schema_diff_calculate_type_transitions(): }, ) - assert [x.sql() for x in alter_expressions] == [ + assert [x.expression.sql() for x in alter_expressions] == [ """ALTER TABLE apply_to_table DROP COLUMN id""", """ALTER TABLE apply_to_table ADD COLUMN id BIGINT""", """ALTER TABLE apply_to_table ALTER COLUMN ds SET DATA TYPE INT""", @@ -114,10 +117,14 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "STRING", - "STRUCT", + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ) ], {}, @@ -127,10 +134,14 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT

      ", [ - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "STRING", - expected_table_struct="STRUCT
      ", + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT
      " + ), + array_element_selector="", position=TableAlterColumnPosition.first(), ) ], @@ -141,10 +152,14 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "STRING", - expected_table_struct="STRUCT", + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", position=TableAlterColumnPosition.middle(after="id"), ) ], @@ -155,22 +170,34 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT
      ", [ - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "STRING", - expected_table_struct="STRUCT
      ", + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT
      " + ), + array_element_selector="", position=TableAlterColumnPosition.first(), ), - TableAlterOperation.add( - TableAlterColumn.primitive("address2"), - "STRING", - expected_table_struct="STRUCT
      ", + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address2")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT
      " + ), + array_element_selector="", position=TableAlterColumnPosition.middle(after="id"), ), - TableAlterOperation.add( - TableAlterColumn.primitive("address3"), - "STRING", - expected_table_struct="STRUCT
      ", + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address3")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT
      " + ), + array_element_selector="", position=TableAlterColumnPosition.last(after="age"), ), ], @@ -181,16 +208,24 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT
      ", [ - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "STRING", - expected_table_struct="STRUCT
      ", + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT
      " + ), + array_element_selector="", position=TableAlterColumnPosition.first(), ), - TableAlterOperation.add( - TableAlterColumn.primitive("address2"), - "STRING", - expected_table_struct="STRUCT
      ", + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address2")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT
      " + ), + array_element_selector="", position=TableAlterColumnPosition.middle(after="address"), ), ], @@ -204,10 +239,11 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("id"), - "STRUCT", - "INT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("id")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", ) ], {}, @@ -217,10 +253,11 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("name"), - "STRUCT", - "STRING", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("name")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", ) ], {}, @@ -230,10 +267,11 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("age"), - "STRUCT", - "INT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("age")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", ) ], {}, @@ -243,20 +281,27 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("id"), - "STRUCT", - "INT", - ), - TableAlterOperation.drop( - TableAlterColumn.primitive("middle"), - "STRUCT", - "STRING", - ), - TableAlterOperation.drop( - TableAlterColumn.primitive("age"), - "STRUCT", - "INT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("id")], + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("middle")], + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("age")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", ), ], {}, @@ -266,15 +311,21 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT
      ", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("address"), - "STRUCT", - "STRING", - ), - TableAlterOperation.drop( - TableAlterColumn.primitive("address2"), - "STRUCT", - "STRING", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address2")], + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ), ], {}, @@ -296,11 +347,15 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.alter_type( - TableAlterColumn.primitive("id"), - "STRING", - current_type="INT", - expected_table_struct="STRUCT", + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("id")], + column_type=exp.DataType.build("STRING"), + current_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ) ], dict( @@ -317,21 +372,30 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("name"), - "STRUCT", - "STRING", - ), - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "STRING", - expected_table_struct="STRUCT", - ), - TableAlterOperation.alter_type( - TableAlterColumn.primitive("id"), - "STRING", - current_type="INT", - expected_table_struct="STRUCT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("name")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("id")], + column_type=exp.DataType.build("STRING"), + current_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ), ], dict( @@ -348,13 +412,17 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_d"), ], - "INT", - expected_table_struct="STRUCT>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), + array_element_selector="", position=TableAlterColumnPosition.first(), ), ], @@ -365,13 +433,17 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_d"), ], - "INT", - expected_table_struct="STRUCT>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), + array_element_selector="", position=TableAlterColumnPosition.last(after="col_c"), ), ], @@ -382,14 +454,18 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_d"), ], - "INT", - expected_table_struct="STRUCT>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), position=TableAlterColumnPosition.middle(after="col_a"), + array_element_selector="", ), ], dict(support_positional_add=True, support_nested_operations=True), @@ -399,23 +475,31 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_d"), ], - "INT", - expected_table_struct="STRUCT>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), position=TableAlterColumnPosition.first(), + array_element_selector="", ), - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_e"), ], - "INT", - expected_table_struct="STRUCT>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), position=TableAlterColumnPosition.middle(after="col_d"), + array_element_selector="", ), ], dict(support_positional_add=True, support_nested_operations=True), @@ -425,20 +509,28 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT, txt TEXT>", [ - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.primitive("txt"), ], - "TEXT", - expected_table_struct="STRUCT, txt TEXT>", - ), - TableAlterOperation.add( - [ + column_type=exp.DataType.build("TEXT"), + expected_table_struct=exp.DataType.build( + "STRUCT, txt TEXT>" + ), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_d"), ], - "INT", - expected_table_struct="STRUCT, txt TEXT>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT, txt TEXT>" + ), + array_element_selector="", ), ], dict(support_positional_add=False, support_nested_operations=True), @@ -448,13 +540,16 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.drop( - [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_a"), ], - "STRUCT>", - "INT", + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), + array_element_selector="", ), ], dict( @@ -468,13 +563,16 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.drop( - [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_c"), ], - "STRUCT>", - "INT", + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), + array_element_selector="", ), ], dict( @@ -488,13 +586,16 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.drop( - [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_b"), ], - "STRUCT>", - "INT", + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), + array_element_selector="", ), ], dict( @@ -508,19 +609,25 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.drop( - [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), ], - expected_table_struct="STRUCT", - column_type="STRUCT", + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", ), - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), ], - expected_table_struct="STRUCT>", - column_type="STRUCT", + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), + column_type=exp.DataType.build("STRUCT"), + array_element_selector="", + is_part_of_destructive_change=True, ), ], dict( @@ -533,21 +640,27 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.drop( - [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_a"), ], - "STRUCT>", - "INT", - ), - TableAlterOperation.drop( - [ + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), + array_element_selector="", + ), + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_b"), ], - "STRUCT>", - "INT", + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), + array_element_selector="", ), ], dict( @@ -561,15 +674,18 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.alter_type( - [ + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_c"), ], - "TEXT", - expected_table_struct="STRUCT>", - position=TableAlterColumnPosition.last(after="col_b"), + column_type=exp.DataType.build("TEXT"), + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), current_type=exp.DataType.build("INT"), + array_element_selector="", ), ], dict( @@ -585,41 +701,55 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.drop( - [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_a"), ], - "STRUCT>", - "INT", - ), - TableAlterOperation.add( - [ + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_d"), ], - "INT", - expected_table_struct="STRUCT>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), position=TableAlterColumnPosition.first(), + array_element_selector="", ), - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_e"), ], - "INT", - expected_table_struct="STRUCT>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), position=TableAlterColumnPosition.middle(after="col_b"), + array_element_selector="", ), - TableAlterOperation.alter_type( - [ + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_c"), ], - "TEXT", - expected_table_struct="STRUCT>", - position=TableAlterColumnPosition.last(after="col_e"), + column_type=exp.DataType.build("TEXT"), + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), current_type=exp.DataType.build("INT"), + array_element_selector="", ), ], dict( @@ -636,19 +766,25 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>", "STRUCT>", [ - TableAlterOperation.drop( - [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), ], - expected_table_struct="STRUCT", - column_type="STRUCT", + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", ), - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), ], - expected_table_struct="STRUCT>", - column_type="STRUCT", + expected_table_struct=exp.DataType.build( + "STRUCT>" + ), + column_type=exp.DataType.build("STRUCT"), + array_element_selector="", + is_part_of_destructive_change=True, ), ], dict( @@ -664,41 +800,55 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>>", "STRUCT, col_c INT>>", [ - TableAlterOperation.drop( - [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_b"), ], - "STRUCT>>", - "INT", - ), - TableAlterOperation.add( - [ + expected_table_struct=exp.DataType.build( + "STRUCT>>" + ), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.primitive("col_c"), ], - "INT", - expected_table_struct="STRUCT, col_c INT>>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT, col_c INT>>" + ), position=TableAlterColumnPosition.last("nested_info"), + array_element_selector="", ), - TableAlterOperation.drop( - [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.struct("nested_info"), TableAlterColumn.primitive("nest_col_b"), ], - "STRUCT, col_c INT>>", - "INT", - ), - TableAlterOperation.add( - [ + expected_table_struct=exp.DataType.build( + "STRUCT, col_c INT>>" + ), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.struct("info"), TableAlterColumn.struct("nested_info"), TableAlterColumn.primitive("nest_col_c"), ], - "INT", - expected_table_struct="STRUCT, col_c INT>>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT, col_c INT>>" + ), position=TableAlterColumnPosition.last("nest_col_a"), + array_element_selector="", ), ], dict( @@ -715,14 +865,18 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>>", "STRUCT>>", [ - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.array_of_struct("infos"), TableAlterColumn.primitive("col_d"), ], - "INT", - expected_table_struct="STRUCT>>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT>>" + ), position=TableAlterColumnPosition.middle("col_b"), + array_element_selector="", ), ], dict(support_positional_add=True, support_nested_operations=True), @@ -732,13 +886,16 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>>", "STRUCT>>", [ - TableAlterOperation.drop( - [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.array_of_struct("infos"), TableAlterColumn.primitive("col_b"), ], - "STRUCT>>", - "INT", + expected_table_struct=exp.DataType.build( + "STRUCT>>" + ), + array_element_selector="", ), ], dict( @@ -752,15 +909,18 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>>", "STRUCT>>", [ - TableAlterOperation.alter_type( - [ + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.array_of_struct("infos"), TableAlterColumn.primitive("col_c"), ], - "TEXT", - expected_table_struct="STRUCT>>", - position=TableAlterColumnPosition.last("col_b"), - current_type="INT", + column_type=exp.DataType.build("TEXT"), + expected_table_struct=exp.DataType.build( + "STRUCT>>" + ), + current_type=exp.DataType.build("INT"), + array_element_selector="", ), ], dict( @@ -776,20 +936,26 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>>", "STRUCT>, col_e INT>", [ - TableAlterOperation.add( - [ - TableAlterColumn.primitive("col_e"), - ], - "INT", - expected_table_struct="STRUCT>, col_e INT>", - ), - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("col_e")], + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT>, col_e INT>" + ), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.array_of_struct("infos"), TableAlterColumn.primitive("col_d"), ], - "INT", - expected_table_struct="STRUCT>, col_e INT>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT>, col_e INT>" + ), + array_element_selector="", ), ], dict(support_positional_add=False, support_nested_operations=True), @@ -799,13 +965,17 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT>>", "STRUCT>, values ARRAY>", [ - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.array_of_primitive("values"), ], - "ARRAY", - expected_table_struct="STRUCT>, values ARRAY>", + column_type=exp.DataType.build("ARRAY"), + expected_table_struct=exp.DataType.build( + "STRUCT>, values ARRAY>" + ), position=TableAlterColumnPosition.last("infos"), + array_element_selector="", ), ], dict(support_positional_add=True, support_nested_operations=True), @@ -822,19 +992,19 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - [ - TableAlterColumn.primitive("ids"), - ], - "STRUCT", - "INT", - ), - TableAlterOperation.add( - [ - TableAlterColumn.primitive("ids"), - ], - "ARRAY", - expected_table_struct="STRUCT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("ids")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("ids")], + column_type=exp.DataType.build("ARRAY"), + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + is_part_of_destructive_change=True, ), ], {}, @@ -844,19 +1014,23 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.array_of_primitive("ids"), ], - "STRUCT", - "ARRAY", + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", ), - TableAlterOperation.add( - [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.array_of_primitive("ids"), ], - "INT", - expected_table_struct="STRUCT", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + is_part_of_destructive_change=True, ), ], {}, @@ -876,11 +1050,15 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.alter_type( - TableAlterColumn.primitive("address"), - "VARCHAR(121)", - current_type="VARCHAR(120)", - expected_table_struct="STRUCT", + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR(121)"), + current_type=exp.DataType.build("VARCHAR(120)"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ) ], {}, @@ -890,11 +1068,15 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.alter_type( - TableAlterColumn.primitive("address"), - "VARCHAR(121)", - current_type="VARCHAR(120)", - expected_table_struct="STRUCT", + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR(121)"), + current_type=exp.DataType.build("VARCHAR(120)"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ) ], dict( @@ -906,15 +1088,21 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("address"), - "STRUCT", - "VARCHAR(120)", - ), - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "VARCHAR(121)", - expected_table_struct="STRUCT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR(121)"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + is_part_of_destructive_change=True, ), ], dict( @@ -926,16 +1114,22 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("address"), - "STRUCT", - "VARCHAR(120)", - ), - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "VARCHAR(100)", - expected_table_struct="STRUCT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR(100)"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), position=TableAlterColumnPosition.last("id"), + array_element_selector="", + is_part_of_destructive_change=True, ), ], dict( @@ -949,16 +1143,20 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("address"), - "STRUCT", - "VARCHAR(120)", - ), - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "VARCHAR", - expected_table_struct="STRUCT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR"), + expected_table_struct=exp.DataType.build("STRUCT"), position=TableAlterColumnPosition.last("id"), + array_element_selector="", + is_part_of_destructive_change=True, ), ], dict( @@ -970,16 +1168,22 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("address"), - "STRUCT", - "VARCHAR", - ), - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "VARCHAR(120)", - expected_table_struct="STRUCT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR(120)"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), position=TableAlterColumnPosition.last("id"), + array_element_selector="", + is_part_of_destructive_change=True, ), ], dict( @@ -991,11 +1195,13 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", # default of 1 --> VARCHAR(1) "STRUCT", [ - TableAlterOperation.alter_type( - TableAlterColumn.primitive("address"), - "VARCHAR(2)", - current_type="VARCHAR", - expected_table_struct="STRUCT", + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR(2)"), + current_type=exp.DataType.build("VARCHAR"), + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", ) ], dict( @@ -1009,16 +1215,20 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", # default of 1 --> VARCHAR(1) [ - TableAlterOperation.drop( - TableAlterColumn.primitive("address"), - "STRUCT", - "VARCHAR(120)", - ), - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "VARCHAR", - expected_table_struct="STRUCT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR"), + expected_table_struct=exp.DataType.build("STRUCT"), position=TableAlterColumnPosition.last("id"), + array_element_selector="", + is_part_of_destructive_change=True, ), ], dict( @@ -1033,11 +1243,15 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.alter_type( - TableAlterColumn.primitive("address"), - "VARCHAR(max)", - current_type="VARCHAR(120)", - expected_table_struct="STRUCT", + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR(max)"), + current_type=exp.DataType.build("VARCHAR(120)"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ) ], dict( @@ -1051,11 +1265,15 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.alter_type( - TableAlterColumn.primitive("address"), - "VARCHAR(max)", - current_type="VARCHAR(120)", - expected_table_struct="STRUCT", + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR(max)"), + current_type=exp.DataType.build("VARCHAR(120)"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ) ], dict( @@ -1069,16 +1287,22 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("address"), - "STRUCT", - "VARCHAR(max)", - ), - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "VARCHAR(120)", - expected_table_struct="STRUCT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR(120)"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), position=TableAlterColumnPosition.last("id"), + array_element_selector="", + is_part_of_destructive_change=True, ), ], dict( @@ -1093,11 +1317,13 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.alter_type( - TableAlterColumn.primitive("address"), - "VARCHAR", - current_type="VARCHAR(120)", - expected_table_struct="STRUCT", + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR"), + current_type=exp.DataType.build("VARCHAR(120)"), + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", ) ], dict( @@ -1113,16 +1339,22 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("address"), - "STRUCT", - "VARCHAR", - ), - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "VARCHAR(120)", - expected_table_struct="STRUCT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("VARCHAR(120)"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), position=TableAlterColumnPosition.last("id"), + array_element_selector="", + is_part_of_destructive_change=True, ), ], dict( @@ -1139,11 +1371,13 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.alter_type( - TableAlterColumn.primitive("address"), - "TEXT", - current_type="VARCHAR(120)", - expected_table_struct="STRUCT", + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("TEXT"), + current_type=exp.DataType.build("VARCHAR(120)"), + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", ) ], dict( @@ -1204,13 +1438,17 @@ def test_schema_diff_calculate_type_transitions(): "STRUCT", "STRUCT", [ - TableAlterOperation.alter_type( - TableAlterColumn.primitive("total"), - "FLOAT", - current_type="INT", + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("total")], + column_type=exp.DataType.build("FLOAT"), + current_type=exp.DataType.build("INT"), # Note that the resulting table struct will not match what we defined as the desired # result since it could be coerced - expected_table_struct="STRUCT", + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ) ], dict( @@ -1232,7 +1470,9 @@ def test_struct_diff( ): resolver = SchemaDiffer(**config) operations = resolver._from_structs( - exp.DataType.build(current_struct), exp.DataType.build(new_struct) + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", ) assert operations == expected_diff @@ -1260,7 +1500,7 @@ def test_schema_diff_calculate_duckdb(duck_conn): }, ) - alter_expressions = engine_adapter.get_alter_expressions("apply_to_table", "schema_from_table") + alter_expressions = engine_adapter.get_alter_operations("apply_to_table", "schema_from_table") engine_adapter.alter_table(alter_expressions) assert engine_adapter.columns("apply_to_table") == { "id": exp.DataType.build("int"), @@ -1271,46 +1511,57 @@ def test_schema_diff_calculate_duckdb(duck_conn): def test_schema_diff_alter_op_column(): - nested = TableAlterOperation.add( - [ + nested = TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.array_of_struct("nested"), TableAlterColumn.primitive("col_a"), ], - "INT", - expected_table_struct="STRUCT>>", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build("STRUCT>>"), position=TableAlterColumnPosition.last("id"), + array_element_selector="", ) - assert nested.column("").sql() == "nested.col_a" - nested_complete_column = TableAlterOperation.add( - [ + assert nested.column.sql() == "nested.col_a" + nested_complete_column = TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.array_of_struct("nested_1", quoted=True), TableAlterColumn.struct("nested_2"), TableAlterColumn.array_of_struct("nested_3"), TableAlterColumn.primitive("col_a", quoted=True), ], - "INT", - expected_table_struct="""STRUCT>>>>>""", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + """STRUCT>>>>>""" + ), position=TableAlterColumnPosition.last("id"), + array_element_selector="", ) - assert nested_complete_column.column("").sql() == '"nested_1".nested_2.nested_3."col_a"' - nested_one_more_complete_column = TableAlterOperation.add( - [ + assert nested_complete_column.column.sql() == '"nested_1".nested_2.nested_3."col_a"' + nested_one_more_complete_column = TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.array_of_struct("nested_1", quoted=True), TableAlterColumn.struct("nested_2"), TableAlterColumn.array_of_struct("nested_3"), TableAlterColumn.struct("nested_4"), TableAlterColumn.primitive("col_a", quoted=True), ], - "INT", - expected_table_struct="""STRUCT>>>>>>""", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + """STRUCT>>>>>>""" + ), position=TableAlterColumnPosition.last("id"), + array_element_selector="", ) assert ( - nested_one_more_complete_column.column("").sql() + nested_one_more_complete_column.column.sql() == '"nested_1".nested_2.nested_3.nested_4."col_a"' ) - super_nested = TableAlterOperation.add( - [ + super_nested = TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ TableAlterColumn.array_of_struct("nested_1", quoted=True), TableAlterColumn.struct("nested_2"), TableAlterColumn.array_of_struct("nested_3"), @@ -1321,12 +1572,15 @@ def test_schema_diff_alter_op_column(): TableAlterColumn.array_of_struct("nested_8"), TableAlterColumn.primitive("col_a", quoted=True), ], - "INT", - expected_table_struct="""STRUCT>>>>>>>>>>>""", + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + """STRUCT>>>>>>>>>>>""" + ), position=TableAlterColumnPosition.last("id"), + array_element_selector="element", ) assert ( - super_nested.column("element").sql() + super_nested.column.sql() == '"nested_1".element.nested_2.nested_3.element.nested_4.nested_5."nested_6".nested_7.nested_8.element."col_a"' ) @@ -1339,10 +1593,11 @@ def test_schema_diff_alter_op_column(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("name"), - "STRUCT", - "STRING", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("name")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", ) ], [], # No operations when ignoring destructive @@ -1353,15 +1608,21 @@ def test_schema_diff_alter_op_column(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("name"), - "STRUCT", - "STRING", - ), - TableAlterOperation.add( - TableAlterColumn.primitive("name"), - "BIGINT", - "STRUCT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("name")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("name")], + column_type=exp.DataType.build("BIGINT"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + is_part_of_destructive_change=True, ), ], [], # No operations when ignoring destructive @@ -1372,18 +1633,26 @@ def test_schema_diff_alter_op_column(): "STRUCT", "STRUCT", [ - TableAlterOperation.add( - TableAlterColumn.primitive("new_col"), - "STRING", - "STRUCT", + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("new_col")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ), ], [ # Same operation when ignoring destructive - TableAlterOperation.add( - TableAlterColumn.primitive("new_col"), - "STRING", - "STRUCT", + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("new_col")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ), ], {}, @@ -1393,35 +1662,52 @@ def test_schema_diff_alter_op_column(): "STRUCT", "STRUCT", [ - TableAlterOperation.drop( - TableAlterColumn.primitive("name"), - "STRUCT", - "STRING", - ), - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "STRING", - "STRUCT", - ), - TableAlterOperation.alter_type( - TableAlterColumn.primitive("id"), - "STRING", - current_type="INT", - expected_table_struct="STRUCT", + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("name")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("id")], + column_type=exp.DataType.build("STRING"), + current_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ), ], [ # Only non-destructive operations remain - TableAlterOperation.add( - TableAlterColumn.primitive("address"), - "STRING", - "STRUCT", - ), - TableAlterOperation.alter_type( - TableAlterColumn.primitive("id"), - "STRING", - current_type="INT", - expected_table_struct="STRUCT", + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("id")], + column_type=exp.DataType.build("STRING"), + current_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", ), ], dict( @@ -1443,13 +1729,19 @@ def test_ignore_destructive_operations( # Test with destructive operations allowed (default behavior) operations_with_destructive = resolver._from_structs( - exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=False + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_destructive=False, ) assert operations_with_destructive == expected_diff_with_destructive # Test with destructive operations ignored operations_ignore_destructive = resolver._from_structs( - exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=True + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_destructive=True, ) assert operations_ignore_destructive == expected_diff_ignore_destructive @@ -1491,7 +1783,7 @@ def test_ignore_destructive_compare_columns(): assert len(alter_expressions_ignore_destructive) == 2 # Only ADD + ALTER # Verify the operations are correct - operations_sql = [expr.sql() for expr in alter_expressions_ignore_destructive] + operations_sql = [expr.expression.sql() for expr in alter_expressions_ignore_destructive] add_column_found = any("ADD COLUMN new_col DOUBLE" in op for op in operations_sql) alter_column_found = any("ALTER COLUMN id SET DATA TYPE" in op for op in operations_sql) drop_column_found = any("DROP COLUMN to_drop" in op for op in operations_sql) @@ -1513,15 +1805,21 @@ def test_ignore_destructive_nested_struct_without_support(): # With destructive operations allowed - should do DROP+ADD of entire struct operations_with_destructive = schema_differ._from_structs( - exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=False + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_destructive=False, ) assert len(operations_with_destructive) == 2 # DROP struct + ADD struct - assert operations_with_destructive[0].is_drop - assert operations_with_destructive[1].is_add + assert isinstance(operations_with_destructive[0], TableAlterDropColumnOperation) + assert isinstance(operations_with_destructive[1], TableAlterAddColumnOperation) # With destructive operations ignored - should do nothing operations_ignore_destructive = schema_differ._from_structs( - exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=True + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_destructive=True, ) assert len(operations_ignore_destructive) == 0 @@ -1582,7 +1880,10 @@ def test_ignore_destructive_edge_cases(): new_struct = "STRUCT<>" # Remove all columns operations_ignore_destructive = schema_differ._from_structs( - exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=True + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_destructive=True, ) assert len(operations_ignore_destructive) == 0 @@ -1590,10 +1891,16 @@ def test_ignore_destructive_edge_cases(): same_struct = "STRUCT" operations_same_with_destructive = schema_differ._from_structs( - exp.DataType.build(same_struct), exp.DataType.build(same_struct), ignore_destructive=False + exp.DataType.build(same_struct), + exp.DataType.build(same_struct), + "apply_to_table", + ignore_destructive=False, ) operations_same_ignore_destructive = schema_differ._from_structs( - exp.DataType.build(same_struct), exp.DataType.build(same_struct), ignore_destructive=True + exp.DataType.build(same_struct), + exp.DataType.build(same_struct), + "apply_to_table", + ignore_destructive=True, ) assert len(operations_same_with_destructive) == 0 assert len(operations_same_ignore_destructive) == 0 @@ -1603,11 +1910,346 @@ def test_ignore_destructive_edge_cases(): new_struct = "STRUCT" operations_add_with_destructive = schema_differ._from_structs( - exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=False + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_destructive=False, ) operations_add_ignore_destructive = schema_differ._from_structs( - exp.DataType.build(current_struct), exp.DataType.build(new_struct), ignore_destructive=True + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_destructive=True, ) assert len(operations_add_with_destructive) == 2 # ADD name, ADD age assert len(operations_add_ignore_destructive) == 2 # Same operations assert operations_add_with_destructive == operations_add_ignore_destructive + + +@pytest.mark.parametrize( + "current_struct, new_struct, expected_diff_with_additive, expected_diff_ignore_additive, config", + [ + # Simple ADD operation - should be ignored when ignore_additive=True + ( + "STRUCT", + "STRUCT", + [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("age")], + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ) + ], + [], # No operations when ignoring additive + {}, + ), + # Multiple ADD operations - should all be ignored when ignore_additive=True + ( + "STRUCT", + "STRUCT", + [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("name")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("age")], + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + ], + [], # No operations when ignoring additive + {}, + ), + # Pure DROP operation - should work same way regardless of ignore_additive + ( + "STRUCT", + "STRUCT", + [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("age")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + ], + [ + # Same operation when ignoring additive + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("age")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + ], + {}, + ), + # Mix of additive and non-additive operations + ( + "STRUCT", + "STRUCT", + [ + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("name")], + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("address")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("id")], + column_type=exp.DataType.build("STRING"), + current_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("something")], + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("something")], + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + is_part_of_destructive_change=True, + ), + ], + [ + # Only non-additive operations remain (alter is considered additive since it was a compatible change) + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("name")], + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + ), + TableAlterDropColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("something")], + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("something")], + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT" + ), + array_element_selector="", + is_part_of_destructive_change=True, + ), + ], + dict( + compatible_types={ + exp.DataType.build("INT"): {exp.DataType.build("STRING")}, + } + ), + ), + # ADD operations with nested structs - should be ignored when ignore_additive=True + ( + "STRUCT>", + "STRUCT, new_field STRING>", + [ + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("new_field")], + column_type=exp.DataType.build("STRING"), + expected_table_struct=exp.DataType.build( + "STRUCT, new_field STRING>" + ), + array_element_selector="", + ), + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[ + TableAlterColumn.struct("info"), + TableAlterColumn.primitive("col_c"), + ], + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT, new_field STRING>" + ), + array_element_selector="", + ), + ], + [], # No operations when ignoring additive + dict(support_nested_operations=True), + ), + ], +) +def test_ignore_additive_operations( + current_struct, + new_struct, + expected_diff_with_additive: t.List[TableAlterOperation], + expected_diff_ignore_additive: t.List[TableAlterOperation], + config: t.Dict[str, t.Any], +): + resolver = SchemaDiffer(**config) + + # Test with additive operations allowed (default behavior) + operations_with_additive = resolver._from_structs( + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_additive=False, + ) + assert operations_with_additive == expected_diff_with_additive + + # Test with additive operations ignored + operations_ignore_additive = resolver._from_structs( + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_additive=True, + ) + assert operations_ignore_additive == expected_diff_ignore_additive + + +def test_ignore_additive_edge_cases(): + """Test edge cases for ignore_additive behavior.""" + schema_differ = SchemaDiffer(support_positional_add=True) + + # Test when all operations are additive - should result in empty list + current_struct = "STRUCT" + new_struct = "STRUCT" # Add all columns + + operations_ignore_additive = schema_differ._from_structs( + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_additive=True, + ) + assert len(operations_ignore_additive) == 0 + + # Test when no operations are needed - should result in empty list regardless of ignore_additive + same_struct = "STRUCT" + + operations_same_with_additive = schema_differ._from_structs( + exp.DataType.build(same_struct), + exp.DataType.build(same_struct), + "apply_to_table", + ignore_additive=False, + ) + operations_same_ignore_additive = schema_differ._from_structs( + exp.DataType.build(same_struct), + exp.DataType.build(same_struct), + "apply_to_table", + ignore_additive=True, + ) + assert len(operations_same_with_additive) == 0 + assert len(operations_same_ignore_additive) == 0 + + # Test when only DROP operations are needed - should be same regardless of ignore_additive + current_struct = "STRUCT" + new_struct = "STRUCT" + + operations_drop_with_additive = schema_differ._from_structs( + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_additive=False, + ) + operations_drop_ignore_additive = schema_differ._from_structs( + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_additive=True, + ) + assert len(operations_drop_with_additive) == 2 # DROP name, DROP age + assert len(operations_drop_ignore_additive) == 2 # Same operations + assert operations_drop_with_additive == operations_drop_ignore_additive + + +def test_ignore_both_destructive_and_additive(): + """Test behavior when both ignore_destructive and ignore_additive are True.""" + schema_differ = SchemaDiffer( + support_positional_add=True, + compatible_types={ + exp.DataType.build("INT"): {exp.DataType.build("STRING")}, + }, + ) + + current_struct = "STRUCT" + new_struct = "STRUCT" # DROP name, ADD address, ALTER id + + operations_ignore_both = schema_differ._from_structs( + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_destructive=True, + ignore_additive=True, + ) + assert len(operations_ignore_both) == 0 + + +def test_ignore_additive_array_operations(): + """Test ignore_additive with array of struct operations.""" + schema_differ = SchemaDiffer( + support_nested_operations=True, + support_positional_add=True, + ) + + current_struct = "STRUCT>>" + new_struct = "STRUCT>>" + + # With additive operations allowed - should add to array struct + operations_with_additive = schema_differ._from_structs( + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_additive=False, + ) + assert len(operations_with_additive) == 1 # ADD to array struct + assert isinstance(operations_with_additive[0], TableAlterAddColumnOperation) + + # With additive operations ignored - should do nothing + operations_ignore_additive = schema_differ._from_structs( + exp.DataType.build(current_struct), + exp.DataType.build(new_struct), + "apply_to_table", + ignore_additive=True, + ) + assert len(operations_ignore_additive) == 0 diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index db61b9cabf..86fb434e33 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -135,6 +135,7 @@ def test_json(snapshot: Snapshot): "batch_size": 30, "forward_only": False, "on_destructive_change": "ERROR", + "on_additive_change": "ALLOW", "partition_by_time_column": True, "disable_restatement": False, "dialect": "spark", @@ -913,7 +914,7 @@ def test_fingerprint(model: Model, parent_model: Model): original_fingerprint = SnapshotFingerprint( data_hash="3301649319", - metadata_hash="1125608408", + metadata_hash="3575333731", ) assert fingerprint == original_fingerprint @@ -1013,7 +1014,7 @@ def test_fingerprint_jinja_macros(model: Model): ) original_fingerprint = SnapshotFingerprint( data_hash="2908339239", - metadata_hash="1125608408", + metadata_hash="3575333731", ) fingerprint = fingerprint_from_node(model, nodes={}) diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 60931b1602..077e65f470 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -41,7 +41,7 @@ ExternalModel, model, ) -from sqlmesh.core.model.kind import OnDestructiveChange, ExternalKind +from sqlmesh.core.model.kind import OnDestructiveChange, ExternalKind, OnAdditiveChange from sqlmesh.core.node import IntervalUnit from sqlmesh.core.snapshot import ( DeployabilityIndex, @@ -57,7 +57,12 @@ from sqlmesh.core.snapshot.evaluator import CustomMaterialization, SnapshotCreationFailedError from sqlmesh.utils.concurrency import NodeExecutionFailedError from sqlmesh.utils.date import to_timestamp -from sqlmesh.utils.errors import ConfigError, SQLMeshError, DestructiveChangeError +from sqlmesh.utils.errors import ( + ConfigError, + SQLMeshError, + DestructiveChangeError, + AdditiveChangeError, +) from sqlmesh.utils.metaprogramming import Executable from sqlmesh.utils.pydantic import list_of_fields_validator @@ -1535,7 +1540,7 @@ def python_func(**kwargs): def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot): adapter_mock.SUPPORTS_CLONING = True - adapter_mock.get_alter_expressions.return_value = [] + adapter_mock.get_alter_operations.return_value = [] evaluator = SnapshotEvaluator(adapter_mock) model = load_sql_based_model( @@ -1587,10 +1592,11 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) rendered_physical_properties={}, ) - adapter_mock.get_alter_expressions.assert_called_once_with( + adapter_mock.get_alter_operations.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev_schema_tmp", ignore_destructive=False, + ignore_additive=False, ) adapter_mock.alter_table.assert_called_once_with([]) @@ -1602,7 +1608,7 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_mock, make_snapshot): adapter_mock.SUPPORTS_CLONING = True - adapter_mock.get_alter_expressions.return_value = [] + adapter_mock.get_alter_operations.return_value = [] evaluator = SnapshotEvaluator(adapter_mock) adapter_mock.alter_table.side_effect = Exception("Migration failed") @@ -1644,10 +1650,11 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m rendered_physical_properties={}, ) - adapter_mock.get_alter_expressions.assert_called_once_with( + adapter_mock.get_alter_operations.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev_schema_tmp", ignore_destructive=False, + ignore_additive=False, ) adapter_mock.alter_table.assert_called_once_with([]) @@ -1667,7 +1674,7 @@ def test_create_clone_in_dev_self_referencing( mocker: MockerFixture, adapter_mock, make_snapshot, use_this_model: bool ): adapter_mock.SUPPORTS_CLONING = True - adapter_mock.get_alter_expressions.return_value = [] + adapter_mock.get_alter_operations.return_value = [] evaluator = SnapshotEvaluator(adapter_mock) from_table = "test_schema.test_model" if not use_this_model else "@this_model" @@ -1773,7 +1780,7 @@ def columns(table_name): assert isinstance(destructive_change_err, DestructiveChangeError) assert ( str(destructive_change_err) - == "\nPlan requires a destructive change to forward-only model '\"test_schema\".\"test_model\"'s schema that drops column 'b'.\n\nSchema changes:\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 DROP COLUMN b\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 ADD COLUMN a INT\n\nTo allow the destructive change, set the model's `on_destructive_change` setting to `warn` or `allow` or include the model in the plan's `--allow-destructive-model` option.\n" + == "\nPlan requires destructive change to forward-only model '\"test_schema\".\"test_model\"'s schema that drops column 'b'.\n\nSchema changes:\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 DROP COLUMN b\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 ADD COLUMN a INT\n\nTo allow the destructive change, set the model's `on_destructive_change` setting to `warn`, `allow`, or `ignore` or include the model in the plan's `--allow-destructive-model` option.\n" ) # WARN @@ -1794,7 +1801,7 @@ def columns(table_name): evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) assert ( mock_logger.call_args[0][0] - == "\nPlan requires a destructive change to forward-only model '\"test_schema\".\"test_model\"'s schema that drops column 'b'.\n\nSchema changes:\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 DROP COLUMN b\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 ADD COLUMN a INT" + == "\nPlan requires destructive change to forward-only model '\"test_schema\".\"test_model\"'s schema that drops column 'b'.\n\nSchema changes:\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 DROP COLUMN b\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 ADD COLUMN a INT" ) # allow destructive @@ -1808,6 +1815,79 @@ def columns(table_name): assert mock_logger.call_count == 0 +def test_on_additive_change_runtime_check( + mocker: MockerFixture, + make_snapshot, + make_mocked_engine_adapter, +): + adapter = make_mocked_engine_adapter(EngineAdapter) + + current_table = "sqlmesh__test_schema.test_schema__test_model__1" + + def columns(table_name): + if table_name == current_table: + return { + "c": exp.DataType.build("int"), + "a": exp.DataType.build("int"), + } + return { + "c": exp.DataType.build("int"), + "a": exp.DataType.build("int"), + "b": exp.DataType.build("int"), + } + + adapter.columns = columns # type: ignore + mocker.patch.object( + adapter, + "get_data_object", + return_value=DataObject(schema="test_schema", name="test_model", type=DataObjectType.TABLE), + ) + + evaluator = SnapshotEvaluator(adapter) + + # SQLMesh default: ERROR + model = SqlModel( + name="test_schema.test_model", + kind=IncrementalByTimeRangeKind(time_column="a", on_additive_change=OnAdditiveChange.ERROR), + query=parse_one("SELECT c, a, b FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), + ) + snapshot = make_snapshot(model, version="1") + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True + snapshot.previous_versions = snapshot.all_versions + + with pytest.raises(NodeExecutionFailedError) as ex: + evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + + additive_change_error = ex.value.__cause__ + assert isinstance(additive_change_error, AdditiveChangeError) + assert ( + str(additive_change_error) + == "\nPlan requires additive change to forward-only model '\"test_schema\".\"test_model\"'s schema that adds column 'b'.\n\nSchema changes:\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 ADD COLUMN b INT\n\nTo allow the additive change, set the model's `on_additive_change` setting to `warn`, `allow`, or `ignore` or include the model in the plan's `--allow-additive-model` option.\n" + ) + + # WARN + model = SqlModel( + name="test_schema.test_model", + kind=IncrementalByTimeRangeKind( + time_column="a", on_additive_change=OnDestructiveChange.WARN + ), + query=parse_one("SELECT c, a FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), + ) + snapshot = make_snapshot(model, version="1") + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True + snapshot.previous_versions = snapshot.all_versions + + logger = logging.getLogger("sqlmesh.core.snapshot.evaluator") + with patch.object(logger, "warning") as mock_logger: + evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + assert ( + mock_logger.call_args[0][0] + == "\nPlan requires additive change to forward-only model '\"test_schema\".\"test_model\"'s schema that adds column 'b'.\n\nSchema changes:\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 ADD COLUMN b INT" + ) + + def test_forward_only_snapshot_for_added_model(mocker: MockerFixture, adapter_mock, make_snapshot): adapter_mock.SUPPORTS_CLONING = False evaluator = SnapshotEvaluator(adapter_mock) @@ -3698,10 +3778,11 @@ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_moc ] ) - adapter_mock.get_alter_expressions.assert_called_once_with( + adapter_mock.get_alter_operations.assert_called_once_with( snapshot.table_name(), f"{new_snapshot.table_name()}_schema_tmp", ignore_destructive=False, + ignore_additive=False, ) @@ -3730,7 +3811,7 @@ def test_migrate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): adapter_mock.drop_data_object_on_type_mismatch.return_value = False # no schema changes - no-op - adapter_mock.get_alter_expressions.return_value = [] + adapter_mock.get_alter_operations.return_value = [] evaluator.migrate( target_snapshots=[snapshot], snapshots={}, @@ -3743,7 +3824,7 @@ def test_migrate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): adapter_mock.reset_mock() # schema changes - exception thrown - adapter_mock.get_alter_expressions.return_value = [exp.Alter()] + adapter_mock.get_alter_operations.return_value = [exp.Alter()] with pytest.raises(NodeExecutionFailedError) as ex: evaluator.migrate( @@ -3968,10 +4049,11 @@ def columns(table_name): ) # The second mock adapter has to be called only for the gateway-specific model - adapter_mock.get_alter_expressions.assert_called_once_with( + adapter_mock.get_alter_operations.assert_called_once_with( snapshot_2.table_name(True), f"{snapshot_2.table_name(True)}_schema_tmp", ignore_destructive=False, + ignore_additive=False, ) diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 44b6cd7911..72994fe33c 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -9,7 +9,7 @@ from sqlmesh.core.config import Config, ModelDefaultsConfig from sqlmesh.core.dialect import jinja_query from sqlmesh.core.model import SqlModel -from sqlmesh.core.model.kind import OnDestructiveChange +from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.loader import sqlmesh_config @@ -133,6 +133,7 @@ def test_model_to_sqlmesh_fields(): assert kind.batch_size == 5 assert kind.lookback == 3 assert kind.on_destructive_change == OnDestructiveChange.ALLOW + assert kind.on_additive_change == OnAdditiveChange.ALLOW assert ( kind.merge_filter.sql(dialect=model.dialect) == """55 > "__MERGE_SOURCE__"."b" AND "__MERGE_TARGET__"."session_start" > CURRENT_DATE + INTERVAL '7' DAY""" @@ -162,11 +163,14 @@ def test_model_to_sqlmesh_fields(): start="Jan 1 2023", batch_size=5, batch_concurrency=2, + on_schema_change="ignore", ) model = model_config.to_sqlmesh(context) assert isinstance(model.kind, IncrementalByTimeRangeKind) assert model.kind.batch_concurrency == 2 assert model.kind.time_column.column.name == "ds" + assert model.kind.on_destructive_change == OnDestructiveChange.IGNORE + assert model.kind.on_additive_change == OnAdditiveChange.IGNORE def test_test_to_sqlmesh_fields(): @@ -1032,3 +1036,40 @@ def test_depends_on(assert_exp_eq, sushi_test_project): # Make sure the query wasn't rendered assert not sqlmesh_model._query_renderer._cache + + +@pytest.mark.parametrize( + "on_schema_change, expected_additive, expected_destructive", + [ + ("ignore", OnAdditiveChange.IGNORE, OnDestructiveChange.IGNORE), + ("fail", OnAdditiveChange.ERROR, OnDestructiveChange.ERROR), + ("append_new_columns", OnAdditiveChange.ALLOW, OnDestructiveChange.IGNORE), + ("sync_all_columns", OnAdditiveChange.ALLOW, OnDestructiveChange.ALLOW), + ], +) +def test_on_schema_change_properties( + on_schema_change: str, + expected_additive: OnAdditiveChange, + expected_destructive: OnDestructiveChange, +): + model_config = ModelConfig( + name="name", + package_name="package", + alias="model", + schema="custom", + database="database", + materialized=Materialization.INCREMENTAL, + sql="SELECT * FROM foo.table", + time_column="ds", + start="Jan 1 2023", + batch_size=5, + batch_concurrency=2, + on_schema_change=on_schema_change, + ) + context = DbtContext() + context.project_name = "Foo" + context.target = DuckDbConfig(name="target", schema="foo") + model = model_config.to_sqlmesh(context) + + assert model.on_additive_change == expected_additive + assert model.on_destructive_change == expected_destructive diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index cefedd6814..fdb8345398 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -29,7 +29,12 @@ SqlModel, ViewKind, ) -from sqlmesh.core.model.kind import SCDType2ByColumnKind, SCDType2ByTimeKind +from sqlmesh.core.model.kind import ( + SCDType2ByColumnKind, + SCDType2ByTimeKind, + OnDestructiveChange, + OnAdditiveChange, +) from sqlmesh.core.state_sync.db.snapshot import _snapshot_to_json from sqlmesh.dbt.builtin import _relation_info_to_relation from sqlmesh.dbt.column import ( @@ -113,6 +118,8 @@ def test_model_kind(): updated_at_as_valid_from=True, updated_at_name="updated_at", dialect="duckdb", + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.ALLOW, ) assert ModelConfig( materialized=Materialization.SNAPSHOT, @@ -126,6 +133,8 @@ def test_model_kind(): columns=["foo"], execution_time_as_valid_from=True, dialect="duckdb", + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.ALLOW, ) assert ModelConfig( materialized=Materialization.SNAPSHOT, @@ -140,23 +149,40 @@ def test_model_kind(): columns=["foo"], execution_time_as_valid_from=True, dialect="bigquery", + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.ALLOW, ) assert ModelConfig(materialized=Materialization.INCREMENTAL, time_column="foo").model_kind( context - ) == IncrementalByTimeRangeKind(time_column="foo", dialect="duckdb", forward_only=True) + ) == IncrementalByTimeRangeKind( + time_column="foo", + dialect="duckdb", + forward_only=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, + ) assert ModelConfig( materialized=Materialization.INCREMENTAL, time_column="foo", incremental_strategy="delete+insert", forward_only=False, - ).model_kind(context) == IncrementalByTimeRangeKind(time_column="foo", dialect="duckdb") + ).model_kind(context) == IncrementalByTimeRangeKind( + time_column="foo", + dialect="duckdb", + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, + ) assert ModelConfig( materialized=Materialization.INCREMENTAL, time_column="foo", incremental_strategy="insert_overwrite", ).model_kind(context) == IncrementalByTimeRangeKind( - time_column="foo", dialect="duckdb", forward_only=True + time_column="foo", + dialect="duckdb", + forward_only=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( materialized=Materialization.INCREMENTAL, @@ -164,13 +190,22 @@ def test_model_kind(): unique_key=["bar"], dialect="bigquery", ).model_kind(context) == IncrementalByTimeRangeKind( - time_column="foo", dialect="bigquery", forward_only=True + time_column="foo", + dialect="bigquery", + forward_only=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( materialized=Materialization.INCREMENTAL, unique_key=["bar"], incremental_strategy="merge" ).model_kind(context) == IncrementalByUniqueKeyKind( - unique_key=["bar"], dialect="duckdb", forward_only=True, disable_restatement=False + unique_key=["bar"], + dialect="duckdb", + forward_only=True, + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) dbt_incremental_predicate = "DBT_INTERNAL_DEST.session_start > dateadd(day, -7, current_date)" @@ -189,30 +224,52 @@ def test_model_kind(): forward_only=True, disable_restatement=False, merge_filter=expected_sqlmesh_predicate, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig(materialized=Materialization.INCREMENTAL, unique_key=["bar"]).model_kind( context ) == IncrementalByUniqueKeyKind( - unique_key=["bar"], dialect="duckdb", forward_only=True, disable_restatement=False + unique_key=["bar"], + dialect="duckdb", + forward_only=True, + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( materialized=Materialization.INCREMENTAL, unique_key=["bar"], full_refresh=False ).model_kind(context) == IncrementalByUniqueKeyKind( - unique_key=["bar"], dialect="duckdb", forward_only=True, disable_restatement=True + unique_key=["bar"], + dialect="duckdb", + forward_only=True, + disable_restatement=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( materialized=Materialization.INCREMENTAL, unique_key=["bar"], full_refresh=True ).model_kind(context) == IncrementalByUniqueKeyKind( - unique_key=["bar"], dialect="duckdb", forward_only=True, disable_restatement=False + unique_key=["bar"], + dialect="duckdb", + forward_only=True, + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( materialized=Materialization.INCREMENTAL, unique_key=["bar"], disable_restatement=True ).model_kind(context) == IncrementalByUniqueKeyKind( - unique_key=["bar"], dialect="duckdb", forward_only=True, disable_restatement=True + unique_key=["bar"], + dialect="duckdb", + forward_only=True, + disable_restatement=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( @@ -221,7 +278,12 @@ def test_model_kind(): disable_restatement=True, full_refresh=True, ).model_kind(context) == IncrementalByUniqueKeyKind( - unique_key=["bar"], dialect="duckdb", forward_only=True, disable_restatement=True + unique_key=["bar"], + dialect="duckdb", + forward_only=True, + disable_restatement=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( @@ -236,6 +298,8 @@ def test_model_kind(): forward_only=True, disable_restatement=True, auto_restatement_cron="0 0 * * *", + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) # Test incompatibile incremental strategies @@ -245,13 +309,23 @@ def test_model_kind(): unique_key=["bar"], incremental_strategy=incremental_strategy, ).model_kind(context) == IncrementalByUniqueKeyKind( - unique_key=["bar"], dialect="duckdb", forward_only=True, disable_restatement=False + unique_key=["bar"], + dialect="duckdb", + forward_only=True, + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( materialized=Materialization.INCREMENTAL, time_column="foo", incremental_strategy="merge" ).model_kind(context) == IncrementalByTimeRangeKind( - time_column="foo", dialect="duckdb", forward_only=True, disable_restatement=False + time_column="foo", + dialect="duckdb", + forward_only=True, + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( @@ -260,7 +334,12 @@ def test_model_kind(): incremental_strategy="merge", full_refresh=True, ).model_kind(context) == IncrementalByTimeRangeKind( - time_column="foo", dialect="duckdb", forward_only=True, disable_restatement=False + time_column="foo", + dialect="duckdb", + forward_only=True, + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( @@ -269,7 +348,12 @@ def test_model_kind(): incremental_strategy="merge", full_refresh=False, ).model_kind(context) == IncrementalByTimeRangeKind( - time_column="foo", dialect="duckdb", forward_only=True, disable_restatement=False + time_column="foo", + dialect="duckdb", + forward_only=True, + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( @@ -278,7 +362,12 @@ def test_model_kind(): incremental_strategy="append", disable_restatement=True, ).model_kind(context) == IncrementalByTimeRangeKind( - time_column="foo", dialect="duckdb", forward_only=True, disable_restatement=True + time_column="foo", + dialect="duckdb", + forward_only=True, + disable_restatement=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( @@ -287,7 +376,12 @@ def test_model_kind(): incremental_strategy="insert_overwrite", partition_by={"field": "bar"}, forward_only=False, - ).model_kind(context) == IncrementalByTimeRangeKind(time_column="foo", dialect="duckdb") + ).model_kind(context) == IncrementalByTimeRangeKind( + time_column="foo", + dialect="duckdb", + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, + ) assert ModelConfig( materialized=Materialization.INCREMENTAL, @@ -303,6 +397,8 @@ def test_model_kind(): forward_only=False, auto_restatement_cron="0 0 * * *", auto_restatement_intervals=3, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( @@ -310,33 +406,56 @@ def test_model_kind(): incremental_strategy="insert_overwrite", partition_by={"field": "bar"}, ).model_kind(context) == IncrementalUnmanagedKind( - insert_overwrite=True, disable_restatement=False + insert_overwrite=True, + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig(materialized=Materialization.INCREMENTAL).model_kind( context - ) == IncrementalUnmanagedKind(insert_overwrite=True, disable_restatement=False) + ) == IncrementalUnmanagedKind( + insert_overwrite=True, + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, + ) assert ModelConfig(materialized=Materialization.INCREMENTAL, forward_only=False).model_kind( context ) == IncrementalUnmanagedKind( - insert_overwrite=True, disable_restatement=False, forward_only=False + insert_overwrite=True, + disable_restatement=False, + forward_only=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( materialized=Materialization.INCREMENTAL, incremental_strategy="append" - ).model_kind(context) == IncrementalUnmanagedKind(disable_restatement=False) + ).model_kind(context) == IncrementalUnmanagedKind( + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, + ) assert ModelConfig( materialized=Materialization.INCREMENTAL, incremental_strategy="append", full_refresh=None - ).model_kind(context) == IncrementalUnmanagedKind(disable_restatement=False) + ).model_kind(context) == IncrementalUnmanagedKind( + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, + ) assert ModelConfig( materialized=Materialization.INCREMENTAL, incremental_strategy="insert_overwrite", partition_by={"field": "bar", "data_type": "int64"}, ).model_kind(context) == IncrementalUnmanagedKind( - insert_overwrite=True, disable_restatement=False + insert_overwrite=True, + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( @@ -345,7 +464,10 @@ def test_model_kind(): partition_by={"field": "bar", "data_type": "int64"}, full_refresh=False, ).model_kind(context) == IncrementalUnmanagedKind( - insert_overwrite=True, disable_restatement=True + insert_overwrite=True, + disable_restatement=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( @@ -355,7 +477,10 @@ def test_model_kind(): disable_restatement=True, full_refresh=True, ).model_kind(context) == IncrementalUnmanagedKind( - insert_overwrite=True, disable_restatement=True + insert_overwrite=True, + disable_restatement=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( @@ -364,7 +489,10 @@ def test_model_kind(): partition_by={"field": "bar", "data_type": "int64"}, disable_restatement=True, ).model_kind(context) == IncrementalUnmanagedKind( - insert_overwrite=True, disable_restatement=True + insert_overwrite=True, + disable_restatement=True, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ModelConfig( @@ -372,7 +500,11 @@ def test_model_kind(): incremental_strategy="insert_overwrite", auto_restatement_cron="0 0 * * *", ).model_kind(context) == IncrementalUnmanagedKind( - insert_overwrite=True, auto_restatement_cron="0 0 * * *", disable_restatement=False + insert_overwrite=True, + auto_restatement_cron="0 0 * * *", + disable_restatement=False, + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.IGNORE, ) assert ( @@ -401,6 +533,7 @@ def test_model_kind_snapshot_bigquery(): updated_at_name="updated_at", time_data_type=exp.DataType.build("TIMESTAMPTZ"), dialect="bigquery", + on_destructive_change=OnDestructiveChange.IGNORE, ) # time_data_type is bigquery version even though model dialect is DuckDB @@ -419,6 +552,7 @@ def test_model_kind_snapshot_bigquery(): updated_at_name="updated_at", time_data_type=exp.DataType.build("TIMESTAMPTZ"), # bigquery version dialect="duckdb", + on_destructive_change=OnDestructiveChange.IGNORE, ) diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index d69311fb3d..e974ea6fc2 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -313,7 +313,7 @@ def test_merge_pr_has_non_breaking_change( +++ - @@ -16,7 +16,8 @@ + @@ -17,7 +17,8 @@ SELECT CAST(o.waiter_id AS INT) AS waiter_id, @@ -524,7 +524,7 @@ def test_merge_pr_has_non_breaking_change_diff_start( +++ - @@ -16,7 +16,8 @@ + @@ -17,7 +17,8 @@ SELECT CAST(o.waiter_id AS INT) AS waiter_id, @@ -1047,7 +1047,7 @@ def test_no_merge_since_no_deploy_signal( +++ - @@ -16,7 +16,8 @@ + @@ -17,7 +17,8 @@ SELECT CAST(o.waiter_id AS INT) AS waiter_id, @@ -1247,7 +1247,7 @@ def test_no_merge_since_no_deploy_signal_no_approvers_defined( +++ - @@ -16,7 +16,8 @@ + @@ -17,7 +17,8 @@ SELECT CAST(o.waiter_id AS INT) AS waiter_id, @@ -1429,7 +1429,7 @@ def test_deploy_comment_pre_categorized( +++ - @@ -16,7 +16,8 @@ + @@ -17,7 +17,8 @@ SELECT CAST(o.waiter_id AS INT) AS waiter_id, @@ -2346,7 +2346,7 @@ def test_has_required_approval_but_not_base_branch( +++ - @@ -16,7 +16,8 @@ + @@ -17,7 +17,8 @@ SELECT CAST(o.waiter_id AS INT) AS waiter_id, From f73cdfe498867f665bb77d10e33d4ceb3cc7dff3 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Thu, 21 Aug 2025 10:58:13 +1200 Subject: [PATCH 0738/1056] Fix: Use drop cascade in janitor (#5133) --- sqlmesh/core/engine_adapter/athena.py | 5 +- sqlmesh/core/engine_adapter/base.py | 11 +- sqlmesh/core/engine_adapter/base_postgres.py | 1 + sqlmesh/core/engine_adapter/bigquery.py | 1 + sqlmesh/core/engine_adapter/clickhouse.py | 2 +- sqlmesh/core/engine_adapter/duckdb.py | 1 + sqlmesh/core/engine_adapter/snowflake.py | 1 + sqlmesh/core/engine_adapter/spark.py | 1 + sqlmesh/core/engine_adapter/trino.py | 1 + sqlmesh/core/snapshot/evaluator.py | 11 +- .../engine_adapter/integration/__init__.py | 11 +- .../integration/test_integration.py | 141 +++++++++- .../integration/test_integration_postgres.py | 253 ++++++++++++++++++ tests/core/engine_adapter/test_base.py | 1 + tests/core/engine_adapter/test_bigquery.py | 27 +- tests/core/test_snapshot_evaluator.py | 22 +- 16 files changed, 465 insertions(+), 25 deletions(-) diff --git a/sqlmesh/core/engine_adapter/athena.py b/sqlmesh/core/engine_adapter/athena.py index d549de3f4c..3ed34067d2 100644 --- a/sqlmesh/core/engine_adapter/athena.py +++ b/sqlmesh/core/engine_adapter/athena.py @@ -45,6 +45,7 @@ class AthenaEngineAdapter(PandasNativeFetchDFSupportMixin, RowDiffMixin): # >>> self._execute('/* test */ DESCRIBE foo') # pyathena.error.OperationalError: FAILED: ParseException line 1:0 cannot recognize input near '/' '*' 'test' ATTACH_CORRELATION_ID = False + SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["DATABASE", "SCHEMA"] def __init__( self, *args: t.Any, s3_warehouse_location: t.Optional[str] = None, **kwargs: t.Any @@ -314,13 +315,13 @@ def _build_table_properties_exp( return None - def drop_table(self, table_name: TableName, exists: bool = True) -> None: + def drop_table(self, table_name: TableName, exists: bool = True, **kwargs: t.Any) -> None: table = exp.to_table(table_name) if self._query_table_type(table) == "hive": self._truncate_table(table) - return super().drop_table(table_name=table, exists=exists) + return super().drop_table(table_name=table, exists=exists, **kwargs) def _truncate_table(self, table_name: TableName) -> None: table = exp.to_table(table_name) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 97342b28cc..920a5aff3d 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -108,6 +108,7 @@ class EngineAdapter: SUPPORTS_CLONING = False SUPPORTS_MANAGED_MODELS = False SUPPORTS_CREATE_DROP_CATALOG = False + SUPPORTED_DROP_CASCADE_OBJECT_KINDS: t.List[str] = [] SCHEMA_DIFFER = SchemaDiffer() SUPPORTS_TUPLE_IN = True HAS_VIEW_BINDING = False @@ -1044,14 +1045,14 @@ def drop_data_object(self, data_object: DataObject, ignore_if_not_exists: bool = f"Can't drop data object '{data_object.to_table().sql(dialect=self.dialect)}' of type '{data_object.type.value}'" ) - def drop_table(self, table_name: TableName, exists: bool = True) -> None: + def drop_table(self, table_name: TableName, exists: bool = True, **kwargs: t.Any) -> None: """Drops a table. Args: table_name: The name of the table to drop. exists: If exists, defaults to True. """ - self._drop_object(name=table_name, exists=exists) + self._drop_object(name=table_name, exists=exists, **kwargs) def drop_managed_table(self, table_name: TableName, exists: bool = True) -> None: """Drops a managed table. @@ -1067,6 +1068,7 @@ def _drop_object( name: TableName | SchemaName, exists: bool = True, kind: str = "TABLE", + cascade: bool = False, **drop_args: t.Any, ) -> None: """Drops an object. @@ -1077,8 +1079,13 @@ def _drop_object( name: The name of the table to drop. exists: If exists, defaults to True. kind: What kind of object to drop. Defaults to TABLE + cascade: Whether or not to DROP ... CASCADE. + Note that this is ignored for :kind's that are not present in self.SUPPORTED_DROP_CASCADE_OBJECT_KINDS **drop_args: Any extra arguments to set on the Drop expression """ + if cascade and kind.upper() in self.SUPPORTED_DROP_CASCADE_OBJECT_KINDS: + drop_args["cascade"] = cascade + self.execute(exp.Drop(this=exp.to_table(name), kind=kind, exists=exists, **drop_args)) def get_alter_operations( diff --git a/sqlmesh/core/engine_adapter/base_postgres.py b/sqlmesh/core/engine_adapter/base_postgres.py index aa46fba95a..26446aacfd 100644 --- a/sqlmesh/core/engine_adapter/base_postgres.py +++ b/sqlmesh/core/engine_adapter/base_postgres.py @@ -24,6 +24,7 @@ class BasePostgresEngineAdapter(EngineAdapter): DEFAULT_BATCH_SIZE = 400 COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY + SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA", "TABLE", "VIEW"] def columns( self, table_name: TableName, include_pseudo_columns: bool = False diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index cab637fb36..f90506c5a1 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -66,6 +66,7 @@ class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin, ClusteredByMixin, Row SUPPORTS_CLONING = True MAX_TABLE_COMMENT_LENGTH = 1024 MAX_COLUMN_COMMENT_LENGTH = 1024 + SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA"] SCHEMA_DIFFER = SchemaDiffer( compatible_types={ diff --git a/sqlmesh/core/engine_adapter/clickhouse.py b/sqlmesh/core/engine_adapter/clickhouse.py index 523f77766a..37b1f20721 100644 --- a/sqlmesh/core/engine_adapter/clickhouse.py +++ b/sqlmesh/core/engine_adapter/clickhouse.py @@ -616,6 +616,7 @@ def _drop_object( name: TableName | SchemaName, exists: bool = True, kind: str = "TABLE", + cascade: bool = False, **drop_args: t.Any, ) -> None: """Drops an object. @@ -628,7 +629,6 @@ def _drop_object( kind: What kind of object to drop. Defaults to TABLE **drop_args: Any extra arguments to set on the Drop expression """ - drop_args.pop("cascade", None) self.execute( exp.Drop( this=exp.to_table(name), diff --git a/sqlmesh/core/engine_adapter/duckdb.py b/sqlmesh/core/engine_adapter/duckdb.py index d90a4ed736..a3bebadbe9 100644 --- a/sqlmesh/core/engine_adapter/duckdb.py +++ b/sqlmesh/core/engine_adapter/duckdb.py @@ -37,6 +37,7 @@ class DuckDBEngineAdapter(LogicalMergeMixin, GetCurrentCatalogFromFunctionMixin, COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY SUPPORTS_CREATE_DROP_CATALOG = True + SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA", "TABLE", "VIEW"] @property def catalog_support(self) -> CatalogSupport: diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index f6fc32cc0a..69ff33b5a8 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -55,6 +55,7 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi SUPPORTS_MANAGED_MODELS = True CURRENT_CATALOG_EXPRESSION = exp.func("current_database") SUPPORTS_CREATE_DROP_CATALOG = True + SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["DATABASE", "SCHEMA", "TABLE"] SCHEMA_DIFFER = SchemaDiffer( parameterized_type_defaults={ exp.DataType.build("BINARY", dialect=DIALECT).this: [(8388608,)], diff --git a/sqlmesh/core/engine_adapter/spark.py b/sqlmesh/core/engine_adapter/spark.py index 5e37ba075e..4f6e9a984f 100644 --- a/sqlmesh/core/engine_adapter/spark.py +++ b/sqlmesh/core/engine_adapter/spark.py @@ -57,6 +57,7 @@ class SparkEngineAdapter( # currently check for storage formats we say we don't support REPLACE TABLE SUPPORTS_REPLACE_TABLE = False QUOTE_IDENTIFIERS_IN_VIEWS = False + SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["DATABASE", "SCHEMA"] WAP_PREFIX = "wap_" BRANCH_PREFIX = "branch_" diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index e16cf2d76c..c62f7bef45 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -53,6 +53,7 @@ class TrinoEngineAdapter( COMMENT_CREATION_TABLE = CommentCreationTable.IN_SCHEMA_DEF_NO_CTAS COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY SUPPORTS_REPLACE_TABLE = False + SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA"] DEFAULT_CATALOG_TYPE = "hive" QUOTE_IDENTIFIERS_IN_VIEWS = False SCHEMA_DIFFER = SchemaDiffer( diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index e891c752b1..45dd7d3f3a 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -1241,6 +1241,10 @@ def _cleanup_snapshot( table_name, is_table_deployable=is_table_deployable, physical_schema=snapshot.physical_schema, + # we need to set cascade=true or we will get a 'cant drop because other objects depend on it'-style + # error on engines that enforce referential integrity, such as Postgres + # this situation can happen when a snapshot expires but downstream view snapshots that reference it have not yet expired + cascade=True, ) except Exception: # Use `get_data_object` to check if the table exists instead of `table_exists` since the former @@ -1771,7 +1775,7 @@ def migrate( def delete(self, name: str, **kwargs: t.Any) -> None: _check_table_db_is_physical_schema(name, kwargs["physical_schema"]) - self.adapter.drop_table(name) + self.adapter.drop_table(name, cascade=kwargs.pop("cascade", False)) logger.info("Dropped table '%s'", name) def _replace_query_for_model( @@ -2372,15 +2376,16 @@ def migrate( ) def delete(self, name: str, **kwargs: t.Any) -> None: + cascade = kwargs.pop("cascade", False) try: - self.adapter.drop_view(name) + self.adapter.drop_view(name, cascade=cascade) except Exception: logger.debug( "Failed to drop view '%s'. Trying to drop the materialized view instead", name, exc_info=True, ) - self.adapter.drop_view(name, materialized=True) + self.adapter.drop_view(name, materialized=True, cascade=cascade) logger.info("Dropped view '%s'", name) def _is_materialized_view(self, model: Model) -> bool: diff --git a/tests/core/engine_adapter/integration/__init__.py b/tests/core/engine_adapter/integration/__init__.py index 8476d992eb..baf45efa9c 100644 --- a/tests/core/engine_adapter/integration/__init__.py +++ b/tests/core/engine_adapter/integration/__init__.py @@ -27,7 +27,7 @@ from _pytest.mark.structures import ParameterSet if t.TYPE_CHECKING: - from sqlmesh.core._typing import TableName + from sqlmesh.core._typing import TableName, SchemaName from sqlmesh.core.engine_adapter._typing import Query TEST_SCHEMA = "test_schema" @@ -222,6 +222,13 @@ def df_type(self) -> t.Optional[str]: return self._test_type.split("-", maxsplit=1)[1] return None + @property + def engine_type(self) -> str: + if self.mark.startswith("gcp_postgres"): + return "gcp_postgres" + + return self.mark.split("_")[0] + @property def columns_to_types(self): if self._columns_to_types is None: @@ -307,7 +314,7 @@ def default_table_format(self) -> t.Optional[str]: def add_test_suffix(self, value: str) -> str: return f"{value}_{self.test_id}" - def get_metadata_results(self, schema: t.Optional[str] = None) -> MetadataResults: + def get_metadata_results(self, schema: t.Optional[SchemaName] = None) -> MetadataResults: schema = schema if schema else self.schema(TEST_SCHEMA) return MetadataResults.from_data_objects(self.engine_adapter.get_data_objects(schema)) diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index e6e4cfdeb6..3e50cf4da9 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -21,6 +21,7 @@ from sqlmesh.core.config import load_config_from_paths from sqlmesh.core.config.connection import ConnectionConfig import sqlmesh.core.dialect as d +from sqlmesh.core.environment import EnvironmentSuffixTarget from sqlmesh.core.dialect import select_from_values from sqlmesh.core.model import Model, load_sql_based_model from sqlmesh.core.engine_adapter.shared import DataObject, DataObjectType @@ -2372,11 +2373,7 @@ def _normalize_snowflake(name: str, prefix_regex: str = "(sqlmesh__)(.*)"): k: [_normalize_snowflake(name) for name in v] for k, v in object_names.items() } - if ctx.mark.startswith("gcp_postgres"): - engine_type = "gcp_postgres" - else: - engine_type = ctx.mark.split("_")[0] - init_example_project(tmp_path, engine_type, schema_name=schema_name) + init_example_project(tmp_path, ctx.engine_type, schema_name=schema_name) config = load_config_from_paths( Config, @@ -3596,3 +3593,137 @@ def test_identifier_length_limit(ctx: TestContext): match=re.escape(match), ): adapter.create_table(long_table_name, {"col": exp.DataType.build("int")}) + + +@pytest.mark.parametrize( + "environment_suffix_target", + [ + EnvironmentSuffixTarget.TABLE, + EnvironmentSuffixTarget.SCHEMA, + EnvironmentSuffixTarget.CATALOG, + ], +) +def test_janitor( + ctx: TestContext, tmp_path: pathlib.Path, environment_suffix_target: EnvironmentSuffixTarget +): + if ( + environment_suffix_target == EnvironmentSuffixTarget.CATALOG + and not ctx.engine_adapter.SUPPORTS_CREATE_DROP_CATALOG + ): + pytest.skip("Engine does not support catalog-based virtual environments") + + schema = ctx.schema() # catalog.schema + parsed_schema = d.to_schema(schema) + + init_example_project(tmp_path, ctx.engine_type, schema_name=parsed_schema.db) + + def _set_config(_gateway: str, config: Config) -> None: + config.environment_suffix_target = environment_suffix_target + config.model_defaults.dialect = ctx.dialect + + sqlmesh = ctx.create_context(path=tmp_path, config_mutator=_set_config) + + sqlmesh.plan(auto_apply=True) + + # create a new model in dev + (tmp_path / "models" / "new_model.sql").write_text(f""" + MODEL ( + name {schema}.new_model, + kind FULL + ); + + select * from {schema}.full_model + """) + sqlmesh.load() + + result = sqlmesh.plan(environment="dev", auto_apply=True) + assert result.context_diff.is_new_environment + assert len(result.context_diff.new_snapshots) == 1 + new_model = list(result.context_diff.new_snapshots.values())[0] + assert "new_model" in new_model.name.lower() + + # check physical objects + snapshot_table_name = exp.to_table(new_model.table_name(), dialect=ctx.dialect) + snapshot_schema = snapshot_table_name.db + + prod_schema = normalize_identifiers(d.to_schema(schema), dialect=ctx.dialect) + dev_env_schema = prod_schema.copy() + if environment_suffix_target == EnvironmentSuffixTarget.CATALOG: + dev_env_schema.set("catalog", exp.to_identifier(f"{prod_schema.catalog}__dev")) + else: + dev_env_schema.set("db", exp.to_identifier(f"{prod_schema.db}__dev")) + normalize_identifiers(dev_env_schema, dialect=ctx.dialect) + + md = ctx.get_metadata_results(prod_schema) + if environment_suffix_target == EnvironmentSuffixTarget.TABLE: + assert sorted([v.lower() for v in md.views]) == [ + "full_model", + "incremental_model", + "new_model__dev", + "seed_model", + ] + else: + assert sorted([v.lower() for v in md.views]) == [ + "full_model", + "incremental_model", + "seed_model", + ] + assert not md.tables + assert not md.managed_tables + + if environment_suffix_target != EnvironmentSuffixTarget.TABLE: + # note: this is "catalog__dev.schema" for EnvironmentSuffixTarget.CATALOG and "catalog.schema__dev" for EnvironmentSuffixTarget.SCHEMA + md = ctx.get_metadata_results(dev_env_schema) + assert [v.lower() for v in md.views] == ["new_model"] + assert not md.tables + assert not md.managed_tables + + md = ctx.get_metadata_results(snapshot_schema) + assert not md.views + assert not md.managed_tables + assert sorted(t.split("__")[1].lower() for t in md.tables) == [ + "full_model", + "incremental_model", + "new_model", + "seed_model", + ] + + # invalidate dev and run the janitor to clean it up + sqlmesh.invalidate_environment("dev") + assert sqlmesh.run_janitor( + ignore_ttl=True + ) # ignore_ttl to delete the new_model snapshot even though it hasnt expired yet + + # there should be no dev environment or dev tables / schemas + md = ctx.get_metadata_results(prod_schema) + assert sorted([v.lower() for v in md.views]) == [ + "full_model", + "incremental_model", + "seed_model", + ] + assert not md.tables + assert not md.managed_tables + + if environment_suffix_target != EnvironmentSuffixTarget.TABLE: + if environment_suffix_target == EnvironmentSuffixTarget.SCHEMA: + md = ctx.get_metadata_results(dev_env_schema) + else: + try: + md = ctx.get_metadata_results(dev_env_schema) + except Exception as e: + # Most engines will raise an error when @set_catalog tries to set a catalog that doesnt exist + # in this case, we just swallow the error. We know this call already worked before in the earlier checks + md = MetadataResults() + + assert not md.views + assert not md.tables + assert not md.managed_tables + + md = ctx.get_metadata_results(snapshot_schema) + assert not md.views + assert not md.managed_tables + assert sorted(t.split("__")[1].lower() for t in md.tables) == [ + "full_model", + "incremental_model", + "seed_model", + ] diff --git a/tests/core/engine_adapter/integration/test_integration_postgres.py b/tests/core/engine_adapter/integration/test_integration_postgres.py index 82172378ae..26b8cbda42 100644 --- a/tests/core/engine_adapter/integration/test_integration_postgres.py +++ b/tests/core/engine_adapter/integration/test_integration_postgres.py @@ -1,13 +1,24 @@ import typing as t import pytest from pytest import FixtureRequest +from pathlib import Path from sqlmesh.core.engine_adapter import PostgresEngineAdapter +from sqlmesh.core.config import Config, DuckDBConnectionConfig +from tests.core.engine_adapter.integration import TestContext +import time_machine +from datetime import timedelta +from sqlmesh.utils.date import to_ds +from sqlglot import exp +from sqlmesh.core.context import Context +from sqlmesh.core.state_sync import CachingStateSync, EngineAdapterStateSync +from sqlmesh.core.snapshot.definition import SnapshotId from tests.core.engine_adapter.integration import ( TestContext, generate_pytest_params, ENGINES_BY_NAME, IntegrationTestEngine, + TEST_SCHEMA, ) @@ -33,3 +44,245 @@ def test_engine_adapter(ctx: TestContext): def test_server_version_psycopg(ctx: TestContext): assert isinstance(ctx.engine_adapter, PostgresEngineAdapter) assert ctx.engine_adapter.server_version != (0, 0) + + +def test_janitor_drop_cascade(ctx: TestContext, tmp_path: Path) -> None: + """ + Scenario: + Ensure that cleaning up expired table snapshots also cleans up any unexpired view snapshots that depend on them + - We create a A (table) <- B (view) + - In dev, we modify A - triggers new version of A and a dev preview of B that both expire in 7 days + - We advance time by 3 days + - In dev, we modify B - triggers a new version of B that depends on A but expires 3 days after A + - In dev, we create B(view) <- C(view) and B(view) <- D(table) + - We advance time by 5 days so that A has reached its expiry but B, C and D have not + - We expire dev so that none of these snapshots are promoted and are thus targets for cleanup + - We run the janitor + + Expected outcome: + - All the dev versions of A and B should be dropped + - C should be dropped as well because it's a view that depends on B which was dropped + - D should not be dropped because while it depends on B which was dropped, it's a table so is still valid after B is dropped + - We should NOT get a 'ERROR: cannot drop table x because other objects depend on it' + + Note that the references in state to the views that were cascade-dropped by postgres will still exist, this is considered ok + as applying a plan will recreate the physical objects + """ + + def _all_snapshot_ids(context: Context) -> t.List[SnapshotId]: + assert isinstance(context.state_sync, CachingStateSync) + assert isinstance(context.state_sync.state_sync, EngineAdapterStateSync) + + return [ + SnapshotId(name=name, identifier=identifier) + for name, identifier in context.state_sync.state_sync.engine_adapter.fetchall( + "select name, identifier from sqlmesh._snapshots" + ) + ] + + models_dir = tmp_path / "models" + models_dir.mkdir() + schema = exp.to_table(ctx.schema(TEST_SCHEMA)).this + + (models_dir / "model_a.sql").write_text(f""" + MODEL ( + name {schema}.model_a, + kind FULL + ); + SELECT 1 as a, 2 as b; + """) + + (models_dir / "model_b.sql").write_text(f""" + MODEL ( + name {schema}.model_b, + kind VIEW + ); + SELECT a from {schema}.model_a; + """) + + def _mutate_config(gateway: str, config: Config): + config.gateways[gateway].state_connection = DuckDBConnectionConfig( + database=str(tmp_path / "state.db") + ) + + with time_machine.travel("2020-01-01 00:00:00"): + sqlmesh = ctx.create_context( + path=tmp_path, config_mutator=_mutate_config, ephemeral_state_connection=False + ) + sqlmesh.plan(auto_apply=True) + + model_a_snapshot = next(s for n, s in sqlmesh.snapshots.items() if "model_a" in n) + # expiry is last updated + ttl + assert timedelta(milliseconds=model_a_snapshot.ttl_ms) == timedelta(weeks=1) + assert to_ds(model_a_snapshot.updated_ts) == "2020-01-01" + assert to_ds(model_a_snapshot.expiration_ts) == "2020-01-08" + + model_b_snapshot = next(s for n, s in sqlmesh.snapshots.items() if "model_b" in n) + assert timedelta(milliseconds=model_b_snapshot.ttl_ms) == timedelta(weeks=1) + assert to_ds(model_b_snapshot.updated_ts) == "2020-01-01" + assert to_ds(model_b_snapshot.expiration_ts) == "2020-01-08" + + model_a_prod_snapshot = model_a_snapshot + model_b_prod_snapshot = model_b_snapshot + + # move forward 1 days + # new dev environment - touch models to create new snapshots + # model a / b expiry in prod should remain unmodified + # model a / b expiry in dev should be as at today + with time_machine.travel("2020-01-02 00:00:00"): + (models_dir / "model_a.sql").write_text(f""" + MODEL ( + name {schema}.model_a, + kind FULL + ); + SELECT 1 as a, 2 as b, 3 as c; + """) + + sqlmesh = ctx.create_context( + path=tmp_path, config_mutator=_mutate_config, ephemeral_state_connection=False + ) + sqlmesh.plan(environment="dev", auto_apply=True) + + # should now have 4 snapshots in state - 2x model a and 2x model b + # the new model b is a dev preview because its upstream model changed + all_snapshot_ids = _all_snapshot_ids(sqlmesh) + assert len(all_snapshot_ids) == 4 + assert len([s for s in all_snapshot_ids if "model_a" in s.name]) == 2 + assert len([s for s in all_snapshot_ids if "model_b" in s.name]) == 2 + + # context just has the two latest + assert len(sqlmesh.snapshots) == 2 + + # these expire 1 day later than what's in prod + model_a_snapshot = next(s for n, s in sqlmesh.snapshots.items() if "model_a" in n) + assert timedelta(milliseconds=model_a_snapshot.ttl_ms) == timedelta(weeks=1) + assert to_ds(model_a_snapshot.updated_ts) == "2020-01-02" + assert to_ds(model_a_snapshot.expiration_ts) == "2020-01-09" + + model_b_snapshot = next(s for n, s in sqlmesh.snapshots.items() if "model_b" in n) + assert timedelta(milliseconds=model_b_snapshot.ttl_ms) == timedelta(weeks=1) + assert to_ds(model_b_snapshot.updated_ts) == "2020-01-02" + assert to_ds(model_b_snapshot.expiration_ts) == "2020-01-09" + + # move forward 3 days + # touch model b in dev but leave model a + # this bumps the model b expiry but model a remains unchanged, so will expire before model b even though model b depends on it + with time_machine.travel("2020-01-05 00:00:00"): + (models_dir / "model_b.sql").write_text(f""" + MODEL ( + name {schema}.model_b, + kind VIEW + ); + SELECT a, 'b' as b from {schema}.model_a; + """) + + (models_dir / "model_c.sql").write_text(f""" + MODEL ( + name {schema}.model_c, + kind VIEW + ); + SELECT a, 'c' as c from {schema}.model_b; + """) + + (models_dir / "model_d.sql").write_text(f""" + MODEL ( + name {schema}.model_d, + kind FULL + ); + SELECT a, 'd' as d from {schema}.model_b; + """) + + sqlmesh = ctx.create_context( + path=tmp_path, config_mutator=_mutate_config, ephemeral_state_connection=False + ) + # need run=True to prevent a "start date is greater than end date" error + # since dev cant exceed what is in prod, and prod has no cadence runs, + # without run=True this plan gets start=2020-01-04 (now) end=2020-01-01 (last prod interval) which fails + sqlmesh.plan(environment="dev", auto_apply=True, run=True) + + # should now have 7 snapshots in state - 2x model a, 3x model b, 1x model c and 1x model d + all_snapshot_ids = _all_snapshot_ids(sqlmesh) + assert len(all_snapshot_ids) == 7 + assert len([s for s in all_snapshot_ids if "model_a" in s.name]) == 2 + assert len([s for s in all_snapshot_ids if "model_b" in s.name]) == 3 + assert len([s for s in all_snapshot_ids if "model_c" in s.name]) == 1 + assert len([s for s in all_snapshot_ids if "model_d" in s.name]) == 1 + + # context just has the 4 latest + assert len(sqlmesh.snapshots) == 4 + + # model a expiry should not have changed + model_a_snapshot = next(s for n, s in sqlmesh.snapshots.items() if "model_a" in n) + assert timedelta(milliseconds=model_a_snapshot.ttl_ms) == timedelta(weeks=1) + assert to_ds(model_a_snapshot.updated_ts) == "2020-01-02" + assert to_ds(model_a_snapshot.expiration_ts) == "2020-01-09" + + # model b should now expire well after model a + model_b_snapshot = next(s for n, s in sqlmesh.snapshots.items() if "model_b" in n) + assert timedelta(milliseconds=model_b_snapshot.ttl_ms) == timedelta(weeks=1) + assert to_ds(model_b_snapshot.updated_ts) == "2020-01-05" + assert to_ds(model_b_snapshot.expiration_ts) == "2020-01-12" + + # model c should expire at the same time as model b + model_c_snapshot = next(s for n, s in sqlmesh.snapshots.items() if "model_c" in n) + assert to_ds(model_c_snapshot.updated_ts) == to_ds(model_b_snapshot.updated_ts) + assert to_ds(model_c_snapshot.expiration_ts) == to_ds(model_b_snapshot.expiration_ts) + + # model d should expire at the same time as model b + model_d_snapshot = next(s for n, s in sqlmesh.snapshots.items() if "model_d" in n) + assert to_ds(model_d_snapshot.updated_ts) == to_ds(model_b_snapshot.updated_ts) + assert to_ds(model_d_snapshot.expiration_ts) == to_ds(model_b_snapshot.expiration_ts) + + # move forward to date where after model a has expired but before model b has expired + # invalidate dev to trigger cleanups + # run janitor + # - table model a is expired so will be cleaned up and this will cascade to view model b + # - view model b is not expired, but because it got cascaded to, this will cascade again to view model c + # - table model d is a not a view, so even though its parent view model b got dropped, it doesnt need to be dropped + with time_machine.travel("2020-01-10 00:00:00"): + sqlmesh = ctx.create_context( + path=tmp_path, config_mutator=_mutate_config, ephemeral_state_connection=False + ) + + before_snapshot_ids = _all_snapshot_ids(sqlmesh) + + before_objects = ctx.get_metadata_results(f"sqlmesh__{schema}") + assert set(before_objects.tables) == set( + [ + exp.to_table(s.table_name()).text("this") + for s in (model_a_prod_snapshot, model_a_snapshot, model_d_snapshot) + ] + ) + assert set(before_objects.views).issuperset( + [ + exp.to_table(s.table_name()).text("this") + for s in (model_b_prod_snapshot, model_b_snapshot, model_c_snapshot) + ] + ) + + sqlmesh.invalidate_environment("dev") + sqlmesh.run_janitor(ignore_ttl=False) + + after_snapshot_ids = _all_snapshot_ids(sqlmesh) + + assert len(before_snapshot_ids) != len(after_snapshot_ids) + + # Everything should be left in state except the model_a snapshot, which expired + assert set(after_snapshot_ids) == set(before_snapshot_ids) - set( + [model_a_snapshot.snapshot_id] + ) + + # In the db, there should be: + # - the two original snapshots that were in prod, table model_a and view model_b + # - model d, even though its not promoted in any environment, because it's a table snapshot that hasnt expired yet + # the view snapshots that depended on model_a should be gone due to the cascading delete + after_objects = ctx.get_metadata_results(f"sqlmesh__{schema}") + assert set(after_objects.tables) == set( + [ + exp.to_table(s.table_name()).text("this") + for s in (model_a_prod_snapshot, model_d_snapshot) + ] + ) + assert after_objects.views == [ + exp.to_table(model_b_prod_snapshot.table_name()).text("this") + ] diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index 3b25091e10..3661df3a3b 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -3463,6 +3463,7 @@ def test_drop_view(make_mocked_engine_adapter: t.Callable): ) def test_drop_schema(kwargs, expected, make_mocked_engine_adapter: t.Callable): adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA"] adapter.drop_schema(**kwargs) diff --git a/tests/core/engine_adapter/test_bigquery.py b/tests/core/engine_adapter/test_bigquery.py index 25aa4006b5..326c587de0 100644 --- a/tests/core/engine_adapter/test_bigquery.py +++ b/tests/core/engine_adapter/test_bigquery.py @@ -20,8 +20,10 @@ @pytest.fixture -def adapter(make_mocked_engine_adapter: t.Callable) -> BigQueryEngineAdapter: - return make_mocked_engine_adapter(BigQueryEngineAdapter) +def adapter(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture) -> BigQueryEngineAdapter: + mocked_adapter = make_mocked_engine_adapter(BigQueryEngineAdapter) + mocker.patch("sqlmesh.core.engine_adapter.bigquery.BigQueryEngineAdapter.execute") + return mocked_adapter def test_insert_overwrite_by_time_partition_query( @@ -575,6 +577,8 @@ def test_begin_end_session(mocker: MockerFixture): def _to_sql_calls(execute_mock: t.Any, identify: bool = True) -> t.List[str]: + if isinstance(execute_mock, BigQueryEngineAdapter): + execute_mock = execute_mock.execute output = [] for call in execute_mock.call_args_list: value = call[0][0] @@ -1150,3 +1154,22 @@ def test_job_cancellation_on_keyboard_interrupt_job_already_done(mocker: MockerF # Verify job status was checked but cancellation was NOT called mock_job.done.assert_called_once() mock_job.cancel.assert_not_called() + + +def test_drop_cascade(adapter: BigQueryEngineAdapter): + adapter.drop_table("foo", cascade=True) + adapter.drop_table("foo", cascade=False) + + # BigQuery doesnt support DROP CASCADE for tables + # ref: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#drop_table_statement + assert _to_sql_calls(adapter) == ["DROP TABLE IF EXISTS `foo`", "DROP TABLE IF EXISTS `foo`"] + adapter.execute.reset_mock() # type: ignore + + # But, it does for schemas + adapter.drop_schema("foo", cascade=True) + adapter.drop_schema("foo", cascade=False) + + assert _to_sql_calls(adapter) == [ + "DROP SCHEMA IF EXISTS `foo` CASCADE", + "DROP SCHEMA IF EXISTS `foo`", + ] diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 077e65f470..4ff2c3893a 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -439,7 +439,8 @@ def create_and_cleanup(name: str, dev_table_only: bool): snapshot = create_and_cleanup("catalog.test_schema.test_model", True) adapter_mock.get_data_object.assert_not_called() adapter_mock.drop_table.assert_called_once_with( - f"catalog.sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev" + f"catalog.sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev", + cascade=True, ) adapter_mock.reset_mock() @@ -448,9 +449,10 @@ def create_and_cleanup(name: str, dev_table_only: bool): adapter_mock.drop_table.assert_has_calls( [ call( - f"sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev" + f"sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev", + cascade=True, ), - call(f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}"), + call(f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}", cascade=True), ] ) adapter_mock.reset_mock() @@ -459,8 +461,11 @@ def create_and_cleanup(name: str, dev_table_only: bool): adapter_mock.get_data_object.assert_not_called() adapter_mock.drop_table.assert_has_calls( [ - call(f"sqlmesh__default.test_model__{snapshot.fingerprint.to_version()}__dev"), - call(f"sqlmesh__default.test_model__{snapshot.version}"), + call( + f"sqlmesh__default.test_model__{snapshot.fingerprint.to_version()}__dev", + cascade=True, + ), + call(f"sqlmesh__default.test_model__{snapshot.version}", cascade=True), ] ) @@ -514,7 +519,8 @@ def test_cleanup_skip_missing_table(adapter_mock, make_snapshot): f"catalog.sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev" ) adapter_mock.drop_table.assert_called_once_with( - f"catalog.sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev" + f"catalog.sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev", + cascade=True, ) @@ -4095,10 +4101,10 @@ def test_multiple_engine_cleanup(snapshot: Snapshot, adapters, make_snapshot): # The clean up will happen using the specific gateway the model was created with engine_adapters["default"].drop_table.assert_called_once_with( - f"sqlmesh__db.db__model__{snapshot.version}__dev" + f"sqlmesh__db.db__model__{snapshot.version}__dev", cascade=True ) engine_adapters["secondary"].drop_table.assert_called_once_with( - f"sqlmesh__test_schema.test_schema__test_model__{snapshot_2.version}__dev" + f"sqlmesh__test_schema.test_schema__test_model__{snapshot_2.version}__dev", cascade=True ) From c7191a7d6c81a18f5dd688d15725dc5b4cf7b596 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 20 Aug 2025 16:16:22 -0700 Subject: [PATCH 0739/1056] Fix: Ignore datetime column parsing erros when rendering seeds (#5199) --- sqlmesh/core/model/definition.py | 2 +- tests/core/test_model.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 768fcd2c02..caec1c86fe 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -1610,7 +1610,7 @@ def render_seed(self) -> t.Iterator[QueryOrDF]: for column in [*date_columns, *datetime_columns]: import pandas as pd - df[column] = pd.to_datetime(df[column]) + df[column] = pd.to_datetime(df[column], infer_datetime_format=True, errors="ignore") # type: ignore # extract datetime.date from pandas timestamp for DATE columns for column in date_columns: diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 81b74ab32a..cffcc52a4e 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9972,6 +9972,31 @@ def test_seed_dont_coerce_na_into_null(tmp_path): assert next(model.render(context=None)).to_dict() == {"code": {0: "NA"}} +def test_seed_coerce_datetime(tmp_path): + model_csv_path = (tmp_path / "model.csv").absolute() + + with open(model_csv_path, "w", encoding="utf-8") as fd: + fd.write("bad_datetime\n9999-12-31 23:59:59") + + expressions = d.parse( + f""" + MODEL ( + name db.seed, + kind SEED ( + path '{str(model_csv_path)}', + ), + columns ( + bad_datetime datetime, + ), + ); + """ + ) + + model = load_sql_based_model(expressions, path=Path("./examples/sushi/models/test_model.sql")) + df = next(model.render(context=None)) + assert df["bad_datetime"].iloc[0] == "9999-12-31 23:59:59" + + def test_missing_column_data_in_columns_key(): expressions = d.parse( """ From 9721a7867c45ecc619402c669dfecf4a7aa1d109 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 20 Aug 2025 17:01:59 -0700 Subject: [PATCH 0740/1056] Fix snowflake int tests (#5198) --- .../integration/test_integration.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 3e50cf4da9..ec5c6b4208 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -15,6 +15,7 @@ import pytz from sqlglot import exp, parse_one from sqlglot.optimizer.normalize_identifiers import normalize_identifiers +from sqlglot.optimizer.qualify_columns import quote_identifiers from sqlmesh import Config, Context from sqlmesh.cli.project_init import init_example_project @@ -1954,11 +1955,18 @@ def test_sushi(ctx: TestContext, tmp_path_factory: pytest.TempPathFactory): ], personal_paths=[pathlib.Path("~/.sqlmesh/config.yaml").expanduser()], ) - config.before_all = [ + before_all = [ f"CREATE SCHEMA IF NOT EXISTS {raw_test_schema}", f"DROP VIEW IF EXISTS {raw_test_schema}.demographics", f"CREATE VIEW {raw_test_schema}.demographics AS (SELECT 1 AS customer_id, '00000' AS zip)", ] + config.before_all = [ + quote_identifiers( + parse_one(e, dialect=config.model_defaults.dialect), + dialect=config.model_defaults.dialect, + ).sql(dialect=config.model_defaults.dialect) + for e in before_all + ] # To enable parallelism in integration tests config.gateways = {ctx.gateway: config.gateways[ctx.gateway]} @@ -2883,16 +2891,12 @@ def _run_plan(sqlmesh_context: Context, environment: str = None) -> PlanResults: assert plan_1.snapshot_for(model_a).model.view_name in plan_1.schema_metadata.views assert plan_1.snapshot_for(model_b).model.view_name in plan_1.schema_metadata.views - assert len(plan_1.internal_schema_metadata.tables) == 3 + assert len(plan_1.internal_schema_metadata.tables) == 1 assert plan_1.table_name_for(model_a) in plan_1.internal_schema_metadata.tables - assert plan_1.dev_table_name_for(model_a) in plan_1.internal_schema_metadata.tables assert ( plan_1.table_name_for(model_b) not in plan_1.internal_schema_metadata.tables ) # because its a managed table - assert ( - plan_1.dev_table_name_for(model_b) in plan_1.internal_schema_metadata.tables - ) # its dev table is a normal table however assert len(plan_1.internal_schema_metadata.managed_tables) == 1 assert plan_1.table_name_for(model_b) in plan_1.internal_schema_metadata.managed_tables From fe64851b1790735710fe4bb67e2d084c7a015af0 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 21 Aug 2025 00:13:37 +0000 Subject: [PATCH 0741/1056] Docs: clarify plan vs. run behavior of blocking audits (#5197) --- docs/concepts/audits.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/concepts/audits.md b/docs/concepts/audits.md index 903f8b4337..a5a9fccc49 100644 --- a/docs/concepts/audits.md +++ b/docs/concepts/audits.md @@ -9,6 +9,32 @@ A comprehensive suite of audits can identify data issues upstream, whether they **NOTE**: For incremental by time range models, audits are only applied to intervals being processed - not for the entire underlying table. +## Blocking audits +A failed blocking audit halts the execution of a `plan` or `run` to prevent invalid data from propagating to downstream models. The impact of a failure depends on whether you are running a `plan` or a `run`. + +SQLMesh's blocking audit process is: + +1. Evaluate the model (e.g., insert new data or rebuild the table) +2. Run the audit query against the newly updated model table. For incremental models, the audit only runs on the processed time intervals. +3. If the query returns any rows, the audit fails, halting the `plan` or `run`. + +### Plan vs. Run + +The key difference is when the model's data is promoted to the production environment: + +* **`plan`**: SQLMesh evaluates and audits all modified models *before* promoting them to production. If an audit fails, the `plan` stops, and the production table is untouched. Invalid data is contained in an isolated table and never reaches the production environment. + +* **`run`**: SQLMesh evaluates and audits models directly against the production environment. If an audit fails, the `run` stops, but the invalid data *is already present* in the production table. The "blocking" action prevents this bad data from being used to build other downstream models. + +### Fixing a Failed Audit + +If a blocking audit fails during a `run`, you must fix the invalid data in the production table. To do so: + +1. **Find the root cause**: examine upstream models and data sources +2. **Fix the source** + * If the cause is an **external data source**, fix it there. Then, run a [restatement plan](./plans.md#restatement-plans) on the first SQLMesh model that ingests the source data. This will restate all downstream models, including the one with the failed audit. + * If the cause is a **SQLMesh model**, update the model's logic. Then apply the change with a `plan`, which will automatically re-evaluate all downstream models. + ## User-Defined Audits In SQLMesh, user-defined audits are defined in `.sql` files in an `audits` directory in your SQLMesh project. Multiple audits can be defined in a single file, so you can organize them to your liking. Alternatively, audits can be defined inline within the model definition itself. From 86140b2a1a81c78ab9dac080b3b48f8f4669c0da Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 22 Aug 2025 12:47:31 +1200 Subject: [PATCH 0742/1056] Feat(dbt): Default to `virtual_environment_mode: dev_only` on init (#5208) --- sqlmesh/cli/project_init.py | 9 ++++++++- sqlmesh_dbt/operations.py | 1 - tests/cli/test_project_init.py | 9 +++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/sqlmesh/cli/project_init.py b/sqlmesh/cli/project_init.py index b6dc5050bc..87d77da4b9 100644 --- a/sqlmesh/cli/project_init.py +++ b/sqlmesh/cli/project_init.py @@ -7,6 +7,7 @@ from sqlmesh.integrations.dlt import generate_dlt_models_and_settings from sqlmesh.utils.date import yesterday_ds from sqlmesh.utils.errors import SQLMeshError +from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.config.common import DBT_PROJECT_FILENAME from sqlmesh.core.config.connection import ( @@ -114,7 +115,13 @@ def _gen_config( - ambiguousorinvalidcolumn - invalidselectstarexpansion """, - ProjectTemplate.DBT: f"""# --- Model Defaults --- + ProjectTemplate.DBT: f"""# --- Virtual Data Environment Mode --- +# Enable Virtual Data Environments (VDE) for *development* environments. +# Note that the production environment in dbt projects is not virtual by default to maintain compatibility with existing tooling. +# https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#virtual-data-environment-modes +virtual_environment_mode: {VirtualEnvironmentMode.DEV_ONLY.lower()} + +# --- Model Defaults --- # https://sqlmesh.readthedocs.io/en/stable/reference/model_configuration/#model-defaults model_defaults: start: {start or yesterday_ds()} diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index f9aae3cdac..e8e443a64a 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -34,7 +34,6 @@ def run(self, select: t.Optional[str] = None, full_refresh: bool = False) -> Non self.context.plan( select_models=select_models, - no_auto_categorization=True, # everything is breaking / foward-only run=True, no_diff=True, no_prompts=True, diff --git a/tests/cli/test_project_init.py b/tests/cli/test_project_init.py index e89e59d90c..12b42705e1 100644 --- a/tests/cli/test_project_init.py +++ b/tests/cli/test_project_init.py @@ -3,6 +3,8 @@ from sqlmesh.utils.errors import SQLMeshError from sqlmesh.cli.project_init import init_example_project, ProjectTemplate from sqlmesh.utils import yaml +from sqlmesh.core.context import Context +from sqlmesh.core.config.common import VirtualEnvironmentMode def test_project_init_dbt(tmp_path: Path): @@ -22,3 +24,10 @@ def test_project_init_dbt(tmp_path: Path): sqlmesh_config = next(f for f in files if f.name == "sqlmesh.yaml") assert "model_defaults" in sqlmesh_config.read_text() assert "start: " in sqlmesh_config.read_text() + + with (tmp_path / "profiles.yml").open("w") as f: + yaml.dump({"jaffle_shop": {"target": "dev", "outputs": {"dev": {"type": "duckdb"}}}}, f) + + ctx = Context(paths=tmp_path) + assert ctx.config.model_defaults.start + assert ctx.config.virtual_environment_mode == VirtualEnvironmentMode.DEV_ONLY From 672806a165cdb94a23fc4d1988f965028d46ecfa Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 22 Aug 2025 07:38:38 -0700 Subject: [PATCH 0743/1056] chore(web_common): set up vitest with playwright and add tests for Badge cmp (#5210) --- pnpm-lock.yaml | 307 +++++++++++---- web/common/eslint.config.mjs | 9 + web/common/package-lock.json | 360 ++++++++++++++++-- web/common/package.json | 10 +- .../src/components/Badge/Badge.test.tsx | 58 +++ web/common/tests/setup.ts | 9 + web/common/tsconfig.json | 4 +- web/common/vitest.config.ts | 24 ++ 8 files changed, 661 insertions(+), 120 deletions(-) create mode 100644 web/common/src/components/Badge/Badge.test.tsx create mode 100644 web/common/tests/setup.ts create mode 100644 web/common/vitest.config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e103546fb..126a3f7d3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,7 +92,7 @@ importers: version: 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(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)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) yaml: specifier: ^2.8.0 version: 2.8.0 @@ -391,7 +391,7 @@ importers: version: 3.5.2(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(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)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: '@swc/core-linux-x64-gnu': specifier: ^1.13.2 @@ -407,19 +407,31 @@ importers: version: 1.2.3(@types/react@18.3.23)(react@18.3.1) '@storybook/addon-docs': specifier: ^9.1.2 - version: 9.1.2(@types/react@18.3.23)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + version: 9.1.2(@types/react@18.3.23)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) '@storybook/addon-essentials': specifier: ^9.0.0-alpha.12 - version: 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + version: 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) '@storybook/addon-onboarding': specifier: ^9.1.2 - version: 9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + version: 9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) '@storybook/react-vite': specifier: ^9.1.2 - version: 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + version: 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.2(@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.16(tailwindcss@3.4.17) + '@testing-library/dom': + specifier: ^10.4.1 + version: 10.4.1 + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.0(@testing-library/dom@10.4.1)(@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) + '@types/node': + specifier: 20.11.25 + version: 20.11.25 '@types/react': specifier: ^18.3.23 version: 18.3.23 @@ -428,7 +440,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@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + version: 4.7.0(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) @@ -446,7 +461,13 @@ importers: version: 5.2.0(eslint@9.31.0(jiti@2.4.2)) eslint-plugin-storybook: specifier: ^9.1.2 - version: 9.1.2(eslint@9.31.0(jiti@2.4.2))(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3) + version: 9.1.2(eslint@9.31.0(jiti@2.4.2))(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3) + globals: + specifier: ^16.3.0 + version: 16.3.0 + playwright: + specifier: ^1.54.1 + version: 1.54.1 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -458,7 +479,7 @@ importers: version: 18.3.1(react@18.3.1) storybook: specifier: ^9.1.2 - version: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + version: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) tailwind-merge: specifier: ^3.3.1 version: 3.3.1 @@ -473,13 +494,16 @@ importers: version: 8.38.0(eslint@9.31.0(jiti@2.4.2))(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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) vite-plugin-dts: specifier: ^4.5.4 - version: 4.5.4(@types/node@24.1.0)(rollup@4.45.1)(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + 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.43.1)(tsx@4.20.3)(yaml@2.8.0)) vite-plugin-static-copy: specifier: ^3.1.1 - version: 3.1.1(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0) packages: @@ -695,10 +719,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.27.6': - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.2': resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} engines: {node: '>=6.9.0'} @@ -2737,6 +2757,21 @@ packages: webdriverio: optional: true + '@vitest/browser@3.2.4': + resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==} + peerDependencies: + playwright: '*' + safaridriver: '*' + vitest: 3.2.4 + webdriverio: ^7.0.0 || ^8.0.0 || ^9.0.0 + peerDependenciesMeta: + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + '@vitest/coverage-v8@3.2.3': resolution: {integrity: sha512-D1QKzngg8PcDoCE8FHSZhREDuEy+zcKmMiMafYse41RZpBE5EDJyKOTdqK3RQfsV2S2nyKor5KCs8PyPRFqKPg==} peerDependencies: @@ -4047,6 +4082,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@16.3.0: + resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -6977,8 +7016,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/runtime@7.27.6': {} - '@babel/runtime@7.28.2': {} '@babel/template@7.27.2': @@ -7373,6 +7410,15 @@ 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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: glob: 10.4.5 @@ -7455,23 +7501,23 @@ snapshots: '@types/react': 18.3.23 react: 18.3.1 - '@microsoft/api-extractor-model@7.30.7(@types/node@24.1.0)': + '@microsoft/api-extractor-model@7.30.7(@types/node@20.11.25)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.14.0(@types/node@24.1.0) + '@rushstack/node-core-library': 5.14.0(@types/node@20.11.25) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.52.10(@types/node@24.1.0)': + '@microsoft/api-extractor@7.52.10(@types/node@20.11.25)': dependencies: - '@microsoft/api-extractor-model': 7.30.7(@types/node@24.1.0) + '@microsoft/api-extractor-model': 7.30.7(@types/node@20.11.25) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.14.0(@types/node@24.1.0) + '@rushstack/node-core-library': 5.14.0(@types/node@20.11.25) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.15.4(@types/node@24.1.0) - '@rushstack/ts-command-line': 5.0.2(@types/node@24.1.0) + '@rushstack/terminal': 0.15.4(@types/node@20.11.25) + '@rushstack/ts-command-line': 5.0.2(@types/node@20.11.25) lodash: 4.17.21 minimatch: 10.0.3 resolve: 1.22.10 @@ -8106,7 +8152,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.45.1': optional: true - '@rushstack/node-core-library@5.14.0(@types/node@24.1.0)': + '@rushstack/node-core-library@5.14.0(@types/node@20.11.25)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -8117,23 +8163,23 @@ snapshots: resolve: 1.22.10 semver: 7.5.4 optionalDependencies: - '@types/node': 24.1.0 + '@types/node': 20.11.25 '@rushstack/rig-package@0.5.3': dependencies: resolve: 1.22.10 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.15.4(@types/node@24.1.0)': + '@rushstack/terminal@0.15.4(@types/node@20.11.25)': dependencies: - '@rushstack/node-core-library': 5.14.0(@types/node@24.1.0) + '@rushstack/node-core-library': 5.14.0(@types/node@20.11.25) supports-color: 8.1.1 optionalDependencies: - '@types/node': 24.1.0 + '@types/node': 20.11.25 - '@rushstack/ts-command-line@5.0.2(@types/node@24.1.0)': + '@rushstack/ts-command-line@5.0.2(@types/node@20.11.25)': dependencies: - '@rushstack/terminal': 0.15.4(@types/node@24.1.0) + '@rushstack/terminal': 0.15.4(@types/node@20.11.25) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -8405,11 +8451,11 @@ snapshots: axe-core: 4.10.3 storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) - '@storybook/addon-backgrounds@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/addon-backgrounds@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) ts-dedent: 2.2.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))': @@ -8425,51 +8471,51 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@storybook/addon-docs@9.1.2(@types/react@18.3.23)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/addon-docs@9.1.2(@types/react@18.3.23)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.23)(react@18.3.1) - '@storybook/csf-plugin': 9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/csf-plugin': 9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/react-dom-shim': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-essentials@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/addon-essentials@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': dependencies: - '@storybook/addon-backgrounds': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - '@storybook/addon-highlight': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - '@storybook/addon-measure': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - '@storybook/addon-outline': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@storybook/addon-backgrounds': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/addon-highlight': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/addon-measure': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/addon-outline': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) ts-dedent: 2.2.0 - '@storybook/addon-highlight@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/addon-highlight@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': dependencies: '@storybook/global': 5.0.0 - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - '@storybook/addon-measure@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/addon-measure@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': dependencies: '@storybook/global': 5.0.0 - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) tiny-invariant: 1.3.3 '@storybook/addon-onboarding@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': dependencies: storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) - '@storybook/addon-onboarding@9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/addon-onboarding@9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': dependencies: - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - '@storybook/addon-outline@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/addon-outline@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': dependencies: '@storybook/global': 5.0.0 - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) ts-dedent: 2.2.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)': @@ -8494,21 +8540,21 @@ snapshots: ts-dedent: 2.2.0 vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) - '@storybook/builder-vite@9.1.2(storybook@9.1.2(@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))': + '@storybook/builder-vite@9.1.2(storybook@9.1.2(@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@storybook/csf-plugin': 9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@storybook/csf-plugin': 9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) '@storybook/csf-plugin@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': dependencies: storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) unplugin: 1.16.1 - '@storybook/csf-plugin@9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/csf-plugin@9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': dependencies: - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -8524,11 +8570,11 @@ snapshots: react-dom: 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.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + '@storybook/react-dom-shim@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.2(@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.43.1)(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))(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: @@ -8550,21 +8596,21 @@ snapshots: - supports-color - typescript - '@storybook/react-vite@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': + '@storybook/react-vite@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.2(@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@rollup/pluginutils': 5.2.0(rollup@4.45.1) - '@storybook/builder-vite': 9.1.2(storybook@9.1.2(@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - '@storybook/react': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3) + '@storybook/builder-vite': 9.1.2(storybook@9.1.2(@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@storybook/react': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - rollup - supports-color @@ -8580,13 +8626,13 @@ snapshots: optionalDependencies: typescript: 5.8.3 - '@storybook/react@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3)': + '@storybook/react@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@storybook/react-dom-shim': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) optionalDependencies: typescript: 5.8.3 @@ -8887,7 +8933,7 @@ snapshots: '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@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)': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 '@testing-library/dom': 10.4.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9320,6 +9366,18 @@ snapshots: 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.43.1)(tsx@4.20.3)(yaml@2.8.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) + '@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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@babel/core': 7.28.0 @@ -9351,6 +9409,45 @@ 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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0) + ws: 8.18.3 + optionalDependencies: + playwright: 1.54.1 + transitivePeerDependencies: + - bufferutil + - msw + - 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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0) + ws: 8.18.3 + optionalDependencies: + playwright: 1.54.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + '@vitest/coverage-v8@3.2.3(@vitest/browser@3.2.3)(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 @@ -9441,7 +9538,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/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(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)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) '@vitest/utils@3.2.3': dependencies: @@ -10555,11 +10652,11 @@ snapshots: dependencies: eslint: 9.31.0(jiti@2.4.2) - eslint-plugin-storybook@9.1.2(eslint@9.31.0(jiti@2.4.2))(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3): + eslint-plugin-storybook@9.1.2(eslint@9.31.0(jiti@2.4.2))(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) transitivePeerDependencies: - supports-color - typescript @@ -10880,6 +10977,8 @@ snapshots: globals@14.0.0: {} + globals@16.3.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -12905,13 +13004,13 @@ snapshots: - supports-color - utf-8-validate - storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)): + storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/spy': 3.2.4 better-opn: 3.0.2 esbuild: 0.25.8 @@ -13559,9 +13658,9 @@ snapshots: dependencies: vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) - vite-plugin-dts@4.5.4(@types/node@24.1.0)(rollup@4.45.1)(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0)): dependencies: - '@microsoft/api-extractor': 7.52.10(@types/node@24.1.0) + '@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 '@vue/language-core': 2.2.0(typescript@5.8.3) @@ -13572,20 +13671,20 @@ snapshots: magic-string: 0.30.17 typescript: 5.8.3 optionalDependencies: - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-static-copy@3.1.1(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.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@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: @@ -13621,7 +13720,7 @@ snapshots: tsx: 4.20.3 yaml: 2.8.0 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(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)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -13649,6 +13748,7 @@ snapshots: 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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: @@ -13710,6 +13810,51 @@ 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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.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) + 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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/ui': 3.2.4(vitest@3.2.4) + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vscode-jsonrpc@8.2.0: {} vscode-jsonrpc@8.2.1: {} diff --git a/web/common/eslint.config.mjs b/web/common/eslint.config.mjs index ce96c1ba4c..11555dcf40 100644 --- a/web/common/eslint.config.mjs +++ b/web/common/eslint.config.mjs @@ -3,6 +3,7 @@ import storybook from 'eslint-plugin-storybook' import eslint from '@eslint/js' import tseslint from 'typescript-eslint' +import globals from 'globals' export default tseslint.config( eslint.configs.recommended, @@ -16,5 +17,13 @@ export default tseslint.config( 'no-empty': 'off', }, }, + { + files: ['vite.config.js', 'vitest.config.ts'], + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, storybook.configs['flat/recommended'], ) diff --git a/web/common/package-lock.json b/web/common/package-lock.json index abfcad57d6..77ddb8d9d1 100644 --- a/web/common/package-lock.json +++ b/web/common/package-lock.json @@ -15,6 +15,8 @@ "@storybook/addon-onboarding": "^9.1.2", "@storybook/react-vite": "^9.1.2", "@tailwindcss/typography": "^0.5.16", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.7.0", @@ -34,7 +36,8 @@ "typescript-eslint": "^8.38.0", "vite": "^6.3.5", "vite-plugin-dts": "^4.5.4", - "vite-plugin-static-copy": "^3.1.1" + "vite-plugin-static-copy": "^3.1.1", + "vitest": "^3.2.4" }, "peerDependencies": { "@radix-ui/react-slot": "^1.0.0", @@ -58,23 +61,6 @@ "url": "https://eslint.org/donate" } }, - "../../node_modules/.pnpm/@types+react-dom@18.3.7_@types+react@18.3.23/node_modules/@types/react-dom": { - "version": "18.3.7", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "../../node_modules/.pnpm/@types+react@18.3.23/node_modules/@types/react": { - "version": "18.3.23", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, "../../node_modules/.pnpm/@vitejs+plugin-react@4.7.0_vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terse_p5zuafkpgv2vlm3nhxz3zj4hsu/node_modules/@vitejs/plugin-react": { "version": "4.7.0", "dev": true, @@ -764,7 +750,6 @@ "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -1790,7 +1775,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -1832,6 +1816,34 @@ "dev": true, "license": "MIT" }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@testing-library/user-event": { "version": "14.6.1", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", @@ -1851,8 +1863,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -1937,13 +1948,33 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { - "resolved": "../../node_modules/.pnpm/@types+react@18.3.23/node_modules/@types/react", - "link": true + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } }, "node_modules/@types/react-dom": { - "resolved": "../../node_modules/.pnpm/@types+react-dom@18.3.7_@types+react@18.3.23/node_modules/@types/react-dom", - "link": true + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } }, "node_modules/@types/resolve": { "version": "1.20.6", @@ -2168,6 +2199,36 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vitest/spy": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", @@ -2225,7 +2286,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -2421,6 +2481,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -2612,6 +2682,13 @@ "node": ">=4" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -2692,8 +2769,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/eastasianwidth": { "version": "0.2.0", @@ -2716,6 +2792,13 @@ "dev": true, "license": "MIT" }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", @@ -2862,6 +2945,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -3348,7 +3441,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -3649,6 +3741,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pathval": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", @@ -3869,7 +3968,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -3980,8 +4078,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/read-cache": { "version": "1.0.0", @@ -4139,6 +4236,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4172,6 +4276,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, "node_modules/storybook": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.2.tgz", @@ -4328,6 +4446,26 @@ "node": ">=8" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -4457,6 +4595,20 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -4505,6 +4657,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/tinyrainbow": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", @@ -4677,14 +4839,37 @@ "resolved": "../../node_modules/.pnpm/vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terser@5.43.1_tsx@4.20.3_yaml@2.8.0/node_modules/vite", "link": true }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite-plugin-dts": { "resolved": "../../node_modules/.pnpm/vite-plugin-dts@4.5.4_@types+node@24.1.0_rollup@4.45.1_typescript@5.8.3_vite@6.3.5_@types+nod_ddgp24sr5pf6ze3b5hs7mrzr5e/node_modules/vite-plugin-dts", "link": true }, "node_modules/vite-plugin-static-copy": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.1.tgz", - "integrity": "sha512-oR53SkL5cX4KT1t18E/xU50vJDo0N8oaHza4EMk0Fm+2/u6nQivxavOfrDk3udWj+dizRizB/QnBvJOOQrTTAQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.2.tgz", + "integrity": "sha512-aVmYOzptLVOI2b1jL+cmkF7O6uhRv1u5fvOkQgbohWZp2CbR22kn9ZqkCUIt9umKF7UhdbsEpshn1rf4720QFg==", "dev": true, "license": "MIT", "dependencies": { @@ -4701,6 +4886,92 @@ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@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.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "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": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -4724,6 +4995,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/web/common/package.json b/web/common/package.json index 47636d5525..7ea7316bed 100644 --- a/web/common/package.json +++ b/web/common/package.json @@ -47,15 +47,22 @@ "@storybook/addon-onboarding": "^9.1.2", "@storybook/react-vite": "^9.1.2", "@tailwindcss/typography": "^0.5.16", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/node": "20.11.25", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.7.0", + "@vitest/browser": "3.2.4", "autoprefixer": "^10.4.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "eslint": "^9.31.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-storybook": "^9.1.2", + "globals": "^16.3.0", + "playwright": "^1.54.1", "postcss": "^8.5.6", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -66,7 +73,8 @@ "typescript-eslint": "^8.38.0", "vite": "^6.3.5", "vite-plugin-dts": "^4.5.4", - "vite-plugin-static-copy": "^3.1.1" + "vite-plugin-static-copy": "^3.1.1", + "vitest": "^3.2.4" }, "peerDependencies": { "@radix-ui/react-slot": "^1.0.0", diff --git a/web/common/src/components/Badge/Badge.test.tsx b/web/common/src/components/Badge/Badge.test.tsx new file mode 100644 index 0000000000..9ee5f2c58c --- /dev/null +++ b/web/common/src/components/Badge/Badge.test.tsx @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest' +import { render, screen } from '@testing-library/react' + +import { Badge } from './Badge' +import { badgeVariants } from './help' +import { EnumShape, EnumSize } from '@/types/enums' +import { cn } from '@/utils' + +describe('Badge', () => { + it('renders with default props and children', () => { + render(Test Badge) + expect(screen.getByText('Test Badge')).toBeInTheDocument() + }) + + it('applies the size class for each size', () => { + Object.values(EnumSize).forEach(size => { + const variants = cn(badgeVariants({ size })) + render(Size {size}) + expect(screen.getByText(`Size ${size}`)).toHaveClass(variants) + }) + }) + + it('applies the shape class for each shape', () => { + Object.values(EnumShape).forEach(shape => { + const variants = cn(badgeVariants({ shape })) + render(Shape {shape}) + expect(screen.getByText(`Shape ${shape}`)).toHaveClass(variants) + }) + }) + + it('supports custom size and shape', () => { + render( + + Custom Size and Shape + , + ) + expect(screen.getByText('Custom Size and Shape')).toHaveClass( + cn(badgeVariants({ size: EnumSize.XXL, shape: EnumShape.Square })), + ) + }) + + it('applies custom className', () => { + render(Custom Class) + expect(screen.getByText('Custom Class')).toHaveClass('custom-class') + }) + + it('renders as a child element when asChild is true', () => { + render( + + Link Badge + , + ) + expect(screen.getByText('Link Badge').tagName).toBe('A') + }) +}) diff --git a/web/common/tests/setup.ts b/web/common/tests/setup.ts new file mode 100644 index 0000000000..5633e30d0c --- /dev/null +++ b/web/common/tests/setup.ts @@ -0,0 +1,9 @@ +import '@testing-library/jest-dom/vitest' + +import { cleanup } from '@testing-library/react' +import { afterEach, vi } from 'vitest' + +afterEach(() => { + vi.resetAllMocks() + cleanup() +}) diff --git a/web/common/tsconfig.json b/web/common/tsconfig.json index d3baabf221..4c17f5cf76 100644 --- a/web/common/tsconfig.json +++ b/web/common/tsconfig.json @@ -1,11 +1,11 @@ { - "include": ["src/**/*.ts", "src/**/*.tsx"], + "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"], "compilerOptions": { "target": "ES2022", "jsx": "react-jsx", "module": "ESNext", "lib": ["ES2022", "DOM", "DOM.Iterable"], - "types": ["vite/client"], + "types": ["vite/client", "@testing-library/jest-dom"], /* Bundler mode */ "moduleResolution": "bundler", diff --git a/web/common/vitest.config.ts b/web/common/vitest.config.ts new file mode 100644 index 0000000000..c84cca951d --- /dev/null +++ b/web/common/vitest.config.ts @@ -0,0 +1,24 @@ +import { configDefaults, defineConfig } from 'vitest/config' + +import viteConfig from './vite.config.js' + +process.env.TZ = 'America/Los_Angeles' // set timezone to UTC-7 (UTC-8 depending on DST) for tests + +export default defineConfig({ + ...viteConfig, + test: { + testTimeout: 10000, + browser: { + provider: 'playwright', + enabled: true, + headless: true, + instances: [ + { + browser: 'chromium', + }, + ], + }, + exclude: [...configDefaults.exclude], + setupFiles: ['./tests/setup.ts'], + }, +}) From 5c252f3ccfe0bda0fc74dddcea57e68638eff5d7 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Fri, 22 Aug 2025 08:05:32 -0700 Subject: [PATCH 0744/1056] feat: mimic dbt nuanced on_schema_change behavior (#5203) --- sqlmesh/core/config/connection.py | 2 + sqlmesh/core/engine_adapter/athena.py | 2 +- sqlmesh/core/engine_adapter/base.py | 17 +- sqlmesh/core/engine_adapter/bigquery.py | 19 +- sqlmesh/core/engine_adapter/clickhouse.py | 4 +- sqlmesh/core/engine_adapter/databricks.py | 15 +- sqlmesh/core/engine_adapter/duckdb.py | 7 +- sqlmesh/core/engine_adapter/mixins.py | 8 +- sqlmesh/core/engine_adapter/mssql.py | 9 +- sqlmesh/core/engine_adapter/mysql.py | 7 +- sqlmesh/core/engine_adapter/postgres.py | 11 +- sqlmesh/core/engine_adapter/redshift.py | 13 +- sqlmesh/core/engine_adapter/snowflake.py | 7 +- sqlmesh/core/engine_adapter/spark.py | 7 +- sqlmesh/core/engine_adapter/trino.py | 7 +- sqlmesh/core/schema_diff.py | 70 ++++++-- sqlmesh/dbt/target.py | 31 ++++ tests/cli/test_cli.py | 11 +- tests/core/engine_adapter/test_base.py | 23 +-- tests/core/engine_adapter/test_clickhouse.py | 3 +- tests/core/test_connection_config.py | 15 ++ tests/core/test_integration.py | 6 +- tests/core/test_schema_diff.py | 178 ++++++++++++++----- tests/dbt/test_config.py | 5 + 24 files changed, 329 insertions(+), 148 deletions(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index b530af36da..b68b83a39a 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -100,6 +100,7 @@ class ConnectionConfig(abc.ABC, BaseConfig): register_comments: bool pre_ping: bool pretty_sql: bool = False + schema_differ_overrides: t.Optional[t.Dict[str, t.Any]] = None # Whether to share a single connection across threads or create a new connection per thread. shared_connection: t.ClassVar[bool] = False @@ -174,6 +175,7 @@ def create_engine_adapter( pre_ping=self.pre_ping, pretty_sql=self.pretty_sql, shared_connection=self.shared_connection, + schema_differ_overrides=self.schema_differ_overrides, **self._extra_engine_config, ) diff --git a/sqlmesh/core/engine_adapter/athena.py b/sqlmesh/core/engine_adapter/athena.py index 3ed34067d2..48b9e4ad4e 100644 --- a/sqlmesh/core/engine_adapter/athena.py +++ b/sqlmesh/core/engine_adapter/athena.py @@ -39,7 +39,7 @@ class AthenaEngineAdapter(PandasNativeFetchDFSupportMixin, RowDiffMixin): # CTAS, Views: No comment support at all COMMENT_CREATION_TABLE = CommentCreationTable.UNSUPPORTED COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED - SCHEMA_DIFFER = TrinoEngineAdapter.SCHEMA_DIFFER + SCHEMA_DIFFER_KWARGS = TrinoEngineAdapter.SCHEMA_DIFFER_KWARGS MAX_TIMESTAMP_PRECISION = 3 # copied from Trino # Athena does not deal with comments well, e.g: # >>> self._execute('/* test */ DESCRIBE foo') diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 920a5aff3d..fe19f7df0f 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -14,7 +14,7 @@ import logging import sys import typing as t -from functools import partial +from functools import cached_property, partial from sqlglot import Dialect, exp from sqlglot.errors import ErrorLevel @@ -109,7 +109,7 @@ class EngineAdapter: SUPPORTS_MANAGED_MODELS = False SUPPORTS_CREATE_DROP_CATALOG = False SUPPORTED_DROP_CASCADE_OBJECT_KINDS: t.List[str] = [] - SCHEMA_DIFFER = SchemaDiffer() + SCHEMA_DIFFER_KWARGS: t.Dict[str, t.Any] = {} SUPPORTS_TUPLE_IN = True HAS_VIEW_BINDING = False SUPPORTS_REPLACE_TABLE = True @@ -132,6 +132,7 @@ def __init__( pretty_sql: bool = False, shared_connection: bool = False, correlation_id: t.Optional[CorrelationId] = None, + schema_differ_overrides: t.Optional[t.Dict[str, t.Any]] = None, **kwargs: t.Any, ): self.dialect = dialect.lower() or self.DIALECT @@ -154,6 +155,7 @@ def __init__( self._pretty_sql = pretty_sql self._multithreaded = multithreaded self.correlation_id = correlation_id + self._schema_differ_overrides = schema_differ_overrides def with_settings(self, **kwargs: t.Any) -> EngineAdapter: extra_kwargs = { @@ -204,6 +206,15 @@ def comments_enabled(self) -> bool: def catalog_support(self) -> CatalogSupport: return CatalogSupport.UNSUPPORTED + @cached_property + def schema_differ(self) -> SchemaDiffer: + return SchemaDiffer( + **{ + **self.SCHEMA_DIFFER_KWARGS, + **(self._schema_differ_overrides or {}), + } + ) + @classmethod def _casted_columns( cls, @@ -1101,7 +1112,7 @@ def get_alter_operations( """ return t.cast( t.List[TableAlterOperation], - self.SCHEMA_DIFFER.compare_columns( + self.schema_differ.compare_columns( current_table_name, self.columns(current_table_name), self.columns(target_table_name), diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index f90506c5a1..4c8a125fa3 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -22,7 +22,7 @@ set_catalog, ) from sqlmesh.core.node import IntervalUnit -from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation +from sqlmesh.core.schema_diff import TableAlterOperation, NestedSupport from sqlmesh.utils import optional_import, get_source_columns_to_types from sqlmesh.utils.date import to_datetime from sqlmesh.utils.errors import SQLMeshError @@ -68,8 +68,8 @@ class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin, ClusteredByMixin, Row MAX_COLUMN_COMMENT_LENGTH = 1024 SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA"] - SCHEMA_DIFFER = SchemaDiffer( - compatible_types={ + SCHEMA_DIFFER_KWARGS = { + "compatible_types": { exp.DataType.build("INT64", dialect=DIALECT): { exp.DataType.build("NUMERIC", dialect=DIALECT), exp.DataType.build("FLOAT64", dialect=DIALECT), @@ -83,17 +83,17 @@ class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin, ClusteredByMixin, Row exp.DataType.build("DATETIME", dialect=DIALECT), }, }, - coerceable_types={ + "coerceable_types": { exp.DataType.build("FLOAT64", dialect=DIALECT): { exp.DataType.build("BIGNUMERIC", dialect=DIALECT), }, }, - support_coercing_compatible_types=True, - parameterized_type_defaults={ + "support_coercing_compatible_types": True, + "parameterized_type_defaults": { exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(38, 9), (0,)], exp.DataType.build("BIGDECIMAL", dialect=DIALECT).this: [(76.76, 38), (0,)], }, - types_with_unlimited_length={ + "types_with_unlimited_length": { # parameterized `STRING(n)` can ALTER to unparameterized `STRING` exp.DataType.build("STRING", dialect=DIALECT).this: { exp.DataType.build("STRING", dialect=DIALECT).this, @@ -103,9 +103,8 @@ class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin, ClusteredByMixin, Row exp.DataType.build("BYTES", dialect=DIALECT).this, }, }, - support_nested_operations=True, - support_nested_drop=False, - ) + "nested_support": NestedSupport.ALL_BUT_DROP, + } @property def client(self) -> BigQueryClient: diff --git a/sqlmesh/core/engine_adapter/clickhouse.py b/sqlmesh/core/engine_adapter/clickhouse.py index 37b1f20721..635e6f369b 100644 --- a/sqlmesh/core/engine_adapter/clickhouse.py +++ b/sqlmesh/core/engine_adapter/clickhouse.py @@ -15,7 +15,7 @@ CommentCreationView, InsertOverwriteStrategy, ) -from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation +from sqlmesh.core.schema_diff import TableAlterOperation from sqlmesh.utils import get_source_columns_to_types if t.TYPE_CHECKING: @@ -37,7 +37,7 @@ class ClickhouseEngineAdapter(EngineAdapterWithIndexSupport, LogicalMergeMixin): SUPPORTS_REPLACE_TABLE = False COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY - SCHEMA_DIFFER = SchemaDiffer() + SCHEMA_DIFFER_KWARGS = {} DEFAULT_TABLE_ENGINE = "MergeTree" ORDER_BY_TABLE_ENGINE_REGEX = "^.*?MergeTree.*$" diff --git a/sqlmesh/core/engine_adapter/databricks.py b/sqlmesh/core/engine_adapter/databricks.py index 4e352b27ef..da70163db4 100644 --- a/sqlmesh/core/engine_adapter/databricks.py +++ b/sqlmesh/core/engine_adapter/databricks.py @@ -15,7 +15,7 @@ ) from sqlmesh.core.engine_adapter.spark import SparkEngineAdapter from sqlmesh.core.node import IntervalUnit -from sqlmesh.core.schema_diff import SchemaDiffer +from sqlmesh.core.schema_diff import NestedSupport from sqlmesh.engines.spark.db_api.spark_session import connection, SparkSessionConnection from sqlmesh.utils.errors import SQLMeshError, MissingDefaultCatalogError @@ -34,15 +34,14 @@ class DatabricksEngineAdapter(SparkEngineAdapter): SUPPORTS_CLONING = True SUPPORTS_MATERIALIZED_VIEWS = True SUPPORTS_MATERIALIZED_VIEW_SCHEMA = True - SCHEMA_DIFFER = SchemaDiffer( - support_positional_add=True, - support_nested_operations=True, - support_nested_drop=True, - array_element_selector="element", - parameterized_type_defaults={ + SCHEMA_DIFFER_KWARGS = { + "support_positional_add": True, + "nested_support": NestedSupport.ALL, + "array_element_selector": "element", + "parameterized_type_defaults": { exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(10, 0), (0,)], }, - ) + } def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: super().__init__(*args, **kwargs) diff --git a/sqlmesh/core/engine_adapter/duckdb.py b/sqlmesh/core/engine_adapter/duckdb.py index a3bebadbe9..8fbe40a575 100644 --- a/sqlmesh/core/engine_adapter/duckdb.py +++ b/sqlmesh/core/engine_adapter/duckdb.py @@ -18,7 +18,6 @@ SourceQuery, set_catalog, ) -from sqlmesh.core.schema_diff import SchemaDiffer if t.TYPE_CHECKING: from sqlmesh.core._typing import SchemaName, TableName @@ -29,11 +28,11 @@ class DuckDBEngineAdapter(LogicalMergeMixin, GetCurrentCatalogFromFunctionMixin, RowDiffMixin): DIALECT = "duckdb" SUPPORTS_TRANSACTIONS = False - SCHEMA_DIFFER = SchemaDiffer( - parameterized_type_defaults={ + SCHEMA_DIFFER_KWARGS = { + "parameterized_type_defaults": { exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(18, 3), (0,)], }, - ) + } COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY SUPPORTS_CREATE_DROP_CATALOG = True diff --git a/sqlmesh/core/engine_adapter/mixins.py b/sqlmesh/core/engine_adapter/mixins.py index e2d7915cf3..bc83beb3d4 100644 --- a/sqlmesh/core/engine_adapter/mixins.py +++ b/sqlmesh/core/engine_adapter/mixins.py @@ -259,9 +259,9 @@ def _default_precision_to_max( ) -> t.Dict[str, exp.DataType]: # get default lengths for types that support "max" length types_with_max_default_param = { - k: [self.SCHEMA_DIFFER.parameterized_type_defaults[k][0][0]] - for k in self.SCHEMA_DIFFER.max_parameter_length - if k in self.SCHEMA_DIFFER.parameterized_type_defaults + k: [self.schema_differ.parameterized_type_defaults[k][0][0]] + for k in self.schema_differ.max_parameter_length + if k in self.schema_differ.parameterized_type_defaults } # Redshift and MSSQL have a bug where CTAS statements have non-deterministic types. If a LIMIT @@ -270,7 +270,7 @@ def _default_precision_to_max( # and supports "max" length, we convert it to "max" length to prevent inadvertent data truncation. for col_name, col_type in columns_to_types.items(): if col_type.this in types_with_max_default_param and col_type.expressions: - parameter = self.SCHEMA_DIFFER.get_type_parameters(col_type) + parameter = self.schema_differ.get_type_parameters(col_type) type_default = types_with_max_default_param[col_type.this] if parameter == type_default: col_type.set("expressions", [exp.DataTypeParam(this=exp.var("max"))]) diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index 3a43d539a9..6aefd51fc0 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -30,7 +30,6 @@ SourceQuery, set_catalog, ) -from sqlmesh.core.schema_diff import SchemaDiffer from sqlmesh.utils import get_source_columns_to_types if t.TYPE_CHECKING: @@ -54,8 +53,8 @@ class MSSQLEngineAdapter( COMMENT_CREATION_TABLE = CommentCreationTable.UNSUPPORTED COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED SUPPORTS_REPLACE_TABLE = False - SCHEMA_DIFFER = SchemaDiffer( - parameterized_type_defaults={ + SCHEMA_DIFFER_KWARGS = { + "parameterized_type_defaults": { exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(18, 0), (0,)], exp.DataType.build("BINARY", dialect=DIALECT).this: [(1,)], exp.DataType.build("VARBINARY", dialect=DIALECT).this: [(1,)], @@ -67,12 +66,12 @@ class MSSQLEngineAdapter( exp.DataType.build("DATETIME2", dialect=DIALECT).this: [(7,)], exp.DataType.build("DATETIMEOFFSET", dialect=DIALECT).this: [(7,)], }, - max_parameter_length={ + "max_parameter_length": { exp.DataType.build("VARBINARY", dialect=DIALECT).this: 2147483647, # 2 GB exp.DataType.build("VARCHAR", dialect=DIALECT).this: 2147483647, exp.DataType.build("NVARCHAR", dialect=DIALECT).this: 2147483647, }, - ) + } VARIABLE_LENGTH_DATA_TYPES = {"binary", "varbinary", "char", "varchar", "nchar", "nvarchar"} @property diff --git a/sqlmesh/core/engine_adapter/mysql.py b/sqlmesh/core/engine_adapter/mysql.py index 298dc18903..e81b30e25e 100644 --- a/sqlmesh/core/engine_adapter/mysql.py +++ b/sqlmesh/core/engine_adapter/mysql.py @@ -19,7 +19,6 @@ DataObjectType, set_catalog, ) -from sqlmesh.core.schema_diff import SchemaDiffer if t.TYPE_CHECKING: from sqlmesh.core._typing import SchemaName, TableName @@ -40,8 +39,8 @@ class MySQLEngineAdapter( MAX_COLUMN_COMMENT_LENGTH = 1024 SUPPORTS_REPLACE_TABLE = False MAX_IDENTIFIER_LENGTH = 64 - SCHEMA_DIFFER = SchemaDiffer( - parameterized_type_defaults={ + SCHEMA_DIFFER_KWARGS = { + "parameterized_type_defaults": { exp.DataType.build("BIT", dialect=DIALECT).this: [(1,)], exp.DataType.build("BINARY", dialect=DIALECT).this: [(1,)], exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(10, 0), (0,)], @@ -52,7 +51,7 @@ class MySQLEngineAdapter( exp.DataType.build("DATETIME", dialect=DIALECT).this: [(0,)], exp.DataType.build("TIMESTAMP", dialect=DIALECT).this: [(0,)], }, - ) + } def get_current_catalog(self) -> t.Optional[str]: """Returns the catalog name of the current connection.""" diff --git a/sqlmesh/core/engine_adapter/postgres.py b/sqlmesh/core/engine_adapter/postgres.py index a1ff46e9ad..faeb52b207 100644 --- a/sqlmesh/core/engine_adapter/postgres.py +++ b/sqlmesh/core/engine_adapter/postgres.py @@ -14,7 +14,6 @@ logical_merge, ) from sqlmesh.core.engine_adapter.shared import set_catalog -from sqlmesh.core.schema_diff import SchemaDiffer if t.TYPE_CHECKING: from sqlmesh.core._typing import TableName @@ -36,15 +35,15 @@ class PostgresEngineAdapter( CURRENT_CATALOG_EXPRESSION = exp.column("current_catalog") SUPPORTS_REPLACE_TABLE = False MAX_IDENTIFIER_LENGTH = 63 - SCHEMA_DIFFER = SchemaDiffer( - parameterized_type_defaults={ + SCHEMA_DIFFER_KWARGS = { + "parameterized_type_defaults": { # DECIMAL without precision is "up to 131072 digits before the decimal point; up to 16383 digits after the decimal point" exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(131072 + 16383, 16383), (0,)], exp.DataType.build("CHAR", dialect=DIALECT).this: [(1,)], exp.DataType.build("TIME", dialect=DIALECT).this: [(6,)], exp.DataType.build("TIMESTAMP", dialect=DIALECT).this: [(6,)], }, - types_with_unlimited_length={ + "types_with_unlimited_length": { # all can ALTER to `TEXT` exp.DataType.build("TEXT", dialect=DIALECT).this: { exp.DataType.build("VARCHAR", dialect=DIALECT).this, @@ -63,8 +62,8 @@ class PostgresEngineAdapter( exp.DataType.build("BPCHAR", dialect=DIALECT).this }, }, - drop_cascade=True, - ) + "drop_cascade": True, + } def _fetch_native_df( self, query: t.Union[exp.Expression, str], quote_identifiers: bool = False diff --git a/sqlmesh/core/engine_adapter/redshift.py b/sqlmesh/core/engine_adapter/redshift.py index 2589ef960e..30ebc8e30d 100644 --- a/sqlmesh/core/engine_adapter/redshift.py +++ b/sqlmesh/core/engine_adapter/redshift.py @@ -22,7 +22,6 @@ SourceQuery, set_catalog, ) -from sqlmesh.core.schema_diff import SchemaDiffer from sqlmesh.utils.errors import SQLMeshError if t.TYPE_CHECKING: @@ -48,8 +47,8 @@ class RedshiftEngineAdapter( COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED SUPPORTS_REPLACE_TABLE = False - SCHEMA_DIFFER = SchemaDiffer( - parameterized_type_defaults={ + SCHEMA_DIFFER_KWARGS = { + "parameterized_type_defaults": { exp.DataType.build("VARBYTE", dialect=DIALECT).this: [(64000,)], exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(18, 0), (0,)], exp.DataType.build("CHAR", dialect=DIALECT).this: [(1,)], @@ -57,13 +56,13 @@ class RedshiftEngineAdapter( exp.DataType.build("NCHAR", dialect=DIALECT).this: [(1,)], exp.DataType.build("NVARCHAR", dialect=DIALECT).this: [(256,)], }, - max_parameter_length={ + "max_parameter_length": { exp.DataType.build("CHAR", dialect=DIALECT).this: 4096, exp.DataType.build("VARCHAR", dialect=DIALECT).this: 65535, }, - precision_increase_allowed_types={exp.DataType.build("VARCHAR", dialect=DIALECT).this}, - drop_cascade=True, - ) + "precision_increase_allowed_types": {exp.DataType.build("VARCHAR", dialect=DIALECT).this}, + "drop_cascade": True, + } VARIABLE_LENGTH_DATA_TYPES = { "char", "character", diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index 69ff33b5a8..c5fa8540b0 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -23,7 +23,6 @@ SourceQuery, set_catalog, ) -from sqlmesh.core.schema_diff import SchemaDiffer 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 @@ -56,8 +55,8 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi CURRENT_CATALOG_EXPRESSION = exp.func("current_database") SUPPORTS_CREATE_DROP_CATALOG = True SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["DATABASE", "SCHEMA", "TABLE"] - SCHEMA_DIFFER = SchemaDiffer( - parameterized_type_defaults={ + SCHEMA_DIFFER_KWARGS = { + "parameterized_type_defaults": { exp.DataType.build("BINARY", dialect=DIALECT).this: [(8388608,)], exp.DataType.build("VARBINARY", dialect=DIALECT).this: [(8388608,)], exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(38, 0), (0,)], @@ -70,7 +69,7 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi exp.DataType.build("TIMESTAMP_NTZ", dialect=DIALECT).this: [(9,)], exp.DataType.build("TIMESTAMP_TZ", dialect=DIALECT).this: [(9,)], }, - ) + } MANAGED_TABLE_KIND = "DYNAMIC TABLE" SNOWPARK = "snowpark" diff --git a/sqlmesh/core/engine_adapter/spark.py b/sqlmesh/core/engine_adapter/spark.py index 4f6e9a984f..8a529390c1 100644 --- a/sqlmesh/core/engine_adapter/spark.py +++ b/sqlmesh/core/engine_adapter/spark.py @@ -22,7 +22,6 @@ SourceQuery, set_catalog, ) -from sqlmesh.core.schema_diff import SchemaDiffer from sqlmesh.utils import classproperty, get_source_columns_to_types from sqlmesh.utils.errors import SQLMeshError @@ -61,12 +60,12 @@ class SparkEngineAdapter( WAP_PREFIX = "wap_" BRANCH_PREFIX = "branch_" - SCHEMA_DIFFER = SchemaDiffer( - parameterized_type_defaults={ + SCHEMA_DIFFER_KWARGS = { + "parameterized_type_defaults": { # default decimal precision varies across backends exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(), (0,)], }, - ) + } @property def connection(self) -> SparkSessionConnection: diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index c62f7bef45..fc08dd10af 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -26,7 +26,6 @@ SourceQuery, set_catalog, ) -from sqlmesh.core.schema_diff import SchemaDiffer from sqlmesh.utils import get_source_columns_to_types from sqlmesh.utils.errors import SQLMeshError from sqlmesh.utils.date import TimeLike @@ -56,14 +55,14 @@ class TrinoEngineAdapter( SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA"] DEFAULT_CATALOG_TYPE = "hive" QUOTE_IDENTIFIERS_IN_VIEWS = False - SCHEMA_DIFFER = SchemaDiffer( - parameterized_type_defaults={ + SCHEMA_DIFFER_KWARGS = { + "parameterized_type_defaults": { # default decimal precision varies across backends exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(), (0,)], exp.DataType.build("CHAR", dialect=DIALECT).this: [(1,)], exp.DataType.build("TIMESTAMP", dialect=DIALECT).this: [(3,)], }, - ) + } # some catalogs support microsecond (precision 6) but it has to be specifically enabled (Hive) or just isnt available (Delta / TIMESTAMP WITH TIME ZONE) # and even if you have a TIMESTAMP(6) the date formatting functions still only support millisecond precision MAX_TIMESTAMP_PRECISION = 3 diff --git a/sqlmesh/core/schema_diff.py b/sqlmesh/core/schema_diff.py index 0bbc146c17..7b8c7f16f7 100644 --- a/sqlmesh/core/schema_diff.py +++ b/sqlmesh/core/schema_diff.py @@ -5,6 +5,8 @@ import typing as t from dataclasses import dataclass from collections import defaultdict +from enum import Enum + from pydantic import Field from sqlglot import exp from sqlglot.helper import ensure_list, seq_get @@ -132,14 +134,15 @@ def _alter_actions(self) -> t.List[exp.Expression]: @dataclass(frozen=True) class TableAlterChangeColumnTypeOperation(TableAlterTypedColumnOperation): current_type: exp.DataType + is_part_of_destructive_change: bool = False @property def is_additive(self) -> bool: - return True + return not self.is_part_of_destructive_change @property def is_destructive(self) -> bool: - return False + return self.is_part_of_destructive_change @property def _alter_actions(self) -> t.List[exp.Expression]: @@ -278,6 +281,33 @@ def column_position_node(self) -> t.Optional[exp.ColumnPosition]: return exp.ColumnPosition(this=column, position=position) +class NestedSupport(str, Enum): + # Supports all nested data type operations + ALL = "ALL" + # Does not support any nested data type operations + NONE = "NONE" + # Supports nested data type operations except for those that require dropping a nested field + ALL_BUT_DROP = "ALL_BUT_DROP" + # Ignores all nested data type operations + IGNORE = "IGNORE" + + @property + def is_all(self) -> bool: + return self == NestedSupport.ALL + + @property + def is_none(self) -> bool: + return self == NestedSupport.NONE + + @property + def is_all_but_drop(self) -> bool: + return self == NestedSupport.ALL_BUT_DROP + + @property + def is_ignore(self) -> bool: + return self == NestedSupport.IGNORE + + class SchemaDiffer(PydanticModel): """ Compares a source schema against a target schema and returns a list of alter statements to have the source @@ -297,10 +327,7 @@ class SchemaDiffer(PydanticModel): Args: support_positional_add: Whether the engine for which the diff is being computed supports adding columns in a specific position in the set of existing columns. - support_nested_operations: Whether the engine for which the diff is being computed supports modifications to - nested data types like STRUCTs and ARRAYs. - support_nested_drop: Whether the engine for which the diff is being computed supports removing individual - columns of nested STRUCTs. + nested_support: How the engine for which the diff is being computed supports nested types. compatible_types: Types that are compatible and automatically coerced in actions like UNION ALL. Dict key is data type, and value is the set of types that are compatible with it. coerceable_types: The mapping from a current type to all types that can be safely coerced to the current one without @@ -323,11 +350,14 @@ class SchemaDiffer(PydanticModel): max_parameter_length: Numeric parameter values corresponding to "max". Example: `VARCHAR(max)` -> `VARCHAR(65535)`. types_with_unlimited_length: Data types that accept values of any length up to system limits. Any explicitly parameterized type can ALTER to its unlimited length version, along with different types in some engines. + treat_alter_data_type_as_destructive: The SchemaDiffer will only output change data type operations if it + concludes the change is compatible and won't result in data loss. If this flag is set to True, it will + flag these data type changes as destructive. This was added for dbt adapter support and likely shouldn't + be set outside of that context. """ support_positional_add: bool = False - support_nested_operations: bool = False - support_nested_drop: bool = False + nested_support: NestedSupport = NestedSupport.NONE array_element_selector: str = "" compatible_types: t.Dict[exp.DataType, t.Set[exp.DataType]] = {} coerceable_types_: t.Dict[exp.DataType, t.Set[exp.DataType]] = Field( @@ -341,6 +371,7 @@ class SchemaDiffer(PydanticModel): ] = {} max_parameter_length: t.Dict[exp.DataType.Type, t.Union[int, float]] = {} types_with_unlimited_length: t.Dict[exp.DataType.Type, t.Set[exp.DataType.Type]] = {} + treat_alter_data_type_as_destructive: bool = False _coerceable_types: t.Dict[exp.DataType, t.Set[exp.DataType]] = {} @@ -575,9 +606,11 @@ def _alter_operation( # We don't copy on purpose here because current_type may need to be mutated inside # _get_operations (struct.expressions.pop and struct.expressions.insert) current_type = exp.DataType.build(current_type, copy=False) - if self.support_nested_operations: + if not self.nested_support.is_none: if new_type.this == current_type.this == exp.DataType.Type.STRUCT: - if self.support_nested_drop or not self._requires_drop_alteration( + if self.nested_support.is_ignore: + return [] + if self.nested_support.is_all or not self._requires_drop_alteration( current_type, new_type ): return self._get_operations( @@ -597,7 +630,9 @@ def _alter_operation( new_array_type = new_type.expressions[0] current_array_type = current_type.expressions[0] if new_array_type.this == current_array_type.this == exp.DataType.Type.STRUCT: - if self.support_nested_drop or not self._requires_drop_alteration( + if self.nested_support.is_ignore: + return [] + if self.nested_support.is_all or not self._requires_drop_alteration( current_array_type, new_array_type ): return self._get_operations( @@ -624,6 +659,7 @@ def _alter_operation( current_type=current_type, expected_table_struct=root_struct.copy(), array_element_selector=self.array_element_selector, + is_part_of_destructive_change=self.treat_alter_data_type_as_destructive, ) ] if ignore_destructive: @@ -806,12 +842,15 @@ def get_additive_column_names(alter_expressions: t.List[TableAlterOperation]) -> ] -def get_schema_differ(dialect: str) -> SchemaDiffer: +def get_schema_differ( + dialect: str, overrides: t.Optional[t.Dict[str, t.Any]] = None +) -> SchemaDiffer: """ Returns the appropriate SchemaDiffer for a given dialect without initializing the engine adapter. Args: dialect: The dialect for which to get the schema differ. + overrides: Optional dictionary of overrides to apply to the SchemaDiffer instance. Returns: The SchemaDiffer instance configured for the given dialect. @@ -825,7 +864,12 @@ def get_schema_differ(dialect: str) -> SchemaDiffer: dialect = dialect.lower() dialect = DIALECT_ALIASES.get(dialect, dialect) engine_adapter_class = DIALECT_TO_ENGINE_ADAPTER.get(dialect, EngineAdapter) - return getattr(engine_adapter_class, "SCHEMA_DIFFER", SchemaDiffer()) + return SchemaDiffer( + **{ + **getattr(engine_adapter_class, "SCHEMA_DIFFER_KWARGS"), + **(overrides or {}), + } + ) def _get_name_and_type(struct: exp.ColumnDef) -> t.Tuple[exp.Identifier, exp.DataType]: diff --git a/sqlmesh/dbt/target.py b/sqlmesh/dbt/target.py index 30a8f35c50..035b5b9e93 100644 --- a/sqlmesh/dbt/target.py +++ b/sqlmesh/dbt/target.py @@ -29,6 +29,7 @@ IncrementalByUniqueKeyKind, IncrementalUnmanagedKind, ) +from sqlmesh.core.schema_diff import NestedSupport from sqlmesh.dbt.common import DbtConfig from sqlmesh.dbt.relation import Policy from sqlmesh.dbt.util import DBT_VERSION @@ -50,6 +51,25 @@ "schema_", } +SCHEMA_DIFFER_OVERRIDES = { + "schema_differ_overrides": { + "treat_alter_data_type_as_destructive": True, + "nested_support": NestedSupport.IGNORE, + } +} + + +def with_schema_differ_overrides( + func: t.Callable[..., ConnectionConfig], +) -> t.Callable[..., ConnectionConfig]: + """Decorator that merges default config with kwargs.""" + + def wrapper(self: TargetConfig, **kwargs: t.Any) -> ConnectionConfig: + merged_kwargs = {**SCHEMA_DIFFER_OVERRIDES, **kwargs} + return func(self, **merged_kwargs) + + return wrapper + class TargetConfig(abc.ABC, DbtConfig): """ @@ -92,6 +112,7 @@ def default_incremental_strategy(self, kind: IncrementalKind) -> str: """The default incremental strategy for the db""" raise NotImplementedError + @with_schema_differ_overrides def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: """Converts target config to SQLMesh connection config""" raise NotImplementedError @@ -177,6 +198,7 @@ def relation_class(cls) -> t.Type[BaseRelation]: return DuckDBRelation + @with_schema_differ_overrides def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: if self.extensions is not None: kwargs["extensions"] = self.extensions @@ -286,6 +308,7 @@ def column_class(cls) -> t.Type[Column]: return SnowflakeColumn + @with_schema_differ_overrides def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: return SnowflakeConnectionConfig( user=self.user, @@ -359,6 +382,7 @@ def _validate_port(cls, v: t.Union[int, str]) -> int: def default_incremental_strategy(self, kind: IncrementalKind) -> str: return "delete+insert" if kind is IncrementalByUniqueKeyKind else "append" + @with_schema_differ_overrides def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: return PostgresConnectionConfig( host=self.host, @@ -454,6 +478,7 @@ def column_class(cls) -> t.Type[Column]: return RedshiftColumn return super(RedshiftConfig, cls).column_class + @with_schema_differ_overrides def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: return RedshiftConnectionConfig( user=self.user, @@ -504,6 +529,7 @@ def column_class(cls) -> t.Type[Column]: return DatabricksColumn + @with_schema_differ_overrides def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: return DatabricksConnectionConfig( server_hostname=self.host, @@ -605,6 +631,7 @@ def column_class(cls) -> t.Type[Column]: return BigQueryColumn + @with_schema_differ_overrides def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: job_retries = self.job_retries if self.job_retries is not None else self.retries job_execution_timeout_seconds = ( @@ -778,6 +805,7 @@ def column_class(cls) -> t.Type[Column]: def dialect(self) -> str: return "tsql" + @with_schema_differ_overrides def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: return MSSQLConnectionConfig( host=self.host, @@ -892,6 +920,7 @@ def column_class(cls) -> t.Type[Column]: return TrinoColumn + @with_schema_differ_overrides def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: return TrinoConnectionConfig( method=self._method_to_auth_enum[self.method], @@ -1002,6 +1031,7 @@ def column_class(cls) -> t.Type[Column]: return ClickHouseColumn + @with_schema_differ_overrides def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: return ClickhouseConnectionConfig( host=self.host, @@ -1085,6 +1115,7 @@ def column_class(cls) -> t.Type[Column]: def default_incremental_strategy(self, kind: IncrementalKind) -> str: return "insert_overwrite" + @with_schema_differ_overrides def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: return AthenaConnectionConfig( type="athena", diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index f283680cfb..433e2165d8 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -960,6 +960,7 @@ def test_dlt_filesystem_pipeline(tmp_path): " # register_comments: False\n" " # pre_ping: False\n" " # pretty_sql: False\n" + " # schema_differ_overrides: \n" " # aws_access_key_id: \n" " # aws_secret_access_key: \n" " # role_arn: \n" @@ -1961,11 +1962,11 @@ def test_init_dbt_template(runner: CliRunner, tmp_path: Path): @time_machine.travel(FREEZE_TIME) def test_init_project_engine_configs(tmp_path): engine_type_to_config = { - "redshift": "# concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # user: \n # password: \n # database: \n # host: \n # port: \n # source_address: \n # unix_sock: \n # ssl: \n # sslmode: \n # timeout: \n # tcp_keepalive: \n # application_name: \n # preferred_role: \n # principal_arn: \n # credentials_provider: \n # region: \n # cluster_identifier: \n # iam: \n # is_serverless: \n # serverless_acct_id: \n # serverless_work_group: \n # enable_merge: ", - "bigquery": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # method: oauth\n # project: \n # execution_project: \n # quota_project: \n # location: \n # keyfile: \n # keyfile_json: \n # token: \n # refresh_token: \n # client_id: \n # client_secret: \n # token_uri: \n # scopes: \n # impersonated_service_account: \n # job_creation_timeout_seconds: \n # job_execution_timeout_seconds: \n # job_retries: 1\n # job_retry_deadline_seconds: \n # priority: \n # maximum_bytes_billed: ", - "snowflake": "account: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # user: \n # password: \n # warehouse: \n # database: \n # role: \n # authenticator: \n # token: \n # host: \n # port: \n # application: Tobiko_SQLMesh\n # private_key: \n # private_key_path: \n # private_key_passphrase: \n # session_parameters: ", - "databricks": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # server_hostname: \n # http_path: \n # access_token: \n # auth_type: \n # oauth_client_id: \n # oauth_client_secret: \n # catalog: \n # http_headers: \n # session_configuration: \n # databricks_connect_server_hostname: \n # databricks_connect_access_token: \n # databricks_connect_cluster_id: \n # databricks_connect_use_serverless: False\n # force_databricks_connect: False\n # disable_databricks_connect: False\n # disable_spark_session: False", - "postgres": "host: \n user: \n password: \n port: \n database: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: True\n # pretty_sql: False\n # keepalives_idle: \n # connect_timeout: 10\n # role: \n # sslmode: \n # application_name: ", + "redshift": "# concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # user: \n # password: \n # database: \n # host: \n # port: \n # source_address: \n # unix_sock: \n # ssl: \n # sslmode: \n # timeout: \n # tcp_keepalive: \n # application_name: \n # preferred_role: \n # principal_arn: \n # credentials_provider: \n # region: \n # cluster_identifier: \n # iam: \n # is_serverless: \n # serverless_acct_id: \n # serverless_work_group: \n # enable_merge: ", + "bigquery": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # method: oauth\n # project: \n # execution_project: \n # quota_project: \n # location: \n # keyfile: \n # keyfile_json: \n # token: \n # refresh_token: \n # client_id: \n # client_secret: \n # token_uri: \n # scopes: \n # impersonated_service_account: \n # job_creation_timeout_seconds: \n # job_execution_timeout_seconds: \n # job_retries: 1\n # job_retry_deadline_seconds: \n # priority: \n # maximum_bytes_billed: ", + "snowflake": "account: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # user: \n # password: \n # warehouse: \n # database: \n # role: \n # authenticator: \n # token: \n # host: \n # port: \n # application: Tobiko_SQLMesh\n # private_key: \n # private_key_path: \n # private_key_passphrase: \n # session_parameters: ", + "databricks": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # server_hostname: \n # http_path: \n # access_token: \n # auth_type: \n # oauth_client_id: \n # oauth_client_secret: \n # catalog: \n # http_headers: \n # session_configuration: \n # databricks_connect_server_hostname: \n # databricks_connect_access_token: \n # databricks_connect_cluster_id: \n # databricks_connect_use_serverless: False\n # force_databricks_connect: False\n # disable_databricks_connect: False\n # disable_spark_session: False", + "postgres": "host: \n user: \n password: \n port: \n database: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: True\n # pretty_sql: False\n # schema_differ_overrides: \n # keepalives_idle: \n # connect_timeout: 10\n # role: \n # sslmode: \n # application_name: ", } for engine_type, expected_config in engine_type_to_config.items(): diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index 3661df3a3b..b2dfcc7ccc 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -15,7 +15,7 @@ from sqlmesh.core.engine_adapter import EngineAdapter, EngineAdapterWithIndexSupport from sqlmesh.core.engine_adapter.mixins import InsertOverwriteWithMergeMixin from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, DataObject -from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation +from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation, NestedSupport from sqlmesh.utils import columns_to_types_to_struct from sqlmesh.utils.date import to_ds from sqlmesh.utils.errors import SQLMeshError, UnsupportedCatalogOperationError @@ -715,8 +715,7 @@ def test_comments(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture) ( { "support_positional_add": True, - "support_nested_operations": True, - "support_nested_drop": True, + "nested_support": NestedSupport.ALL, "array_element_selector": "element", }, { @@ -774,7 +773,7 @@ def test_comments(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture) ), ( { - "support_nested_operations": True, + "nested_support": NestedSupport.ALL_BUT_DROP, "array_element_selector": "element", }, { @@ -892,8 +891,7 @@ def test_comments(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture) ( { "support_positional_add": True, - "support_nested_operations": True, - "support_nested_drop": True, + "nested_support": NestedSupport.ALL, "array_element_selector": "element", }, { @@ -922,8 +920,7 @@ def test_comments(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture) ( { "support_positional_add": True, - "support_nested_operations": True, - "support_nested_drop": True, + "nested_support": NestedSupport.ALL, "array_element_selector": "element", }, { @@ -979,8 +976,7 @@ def test_comments(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture) # Test multiple operations on a column with no positional and nested features enabled ( { - "support_nested_operations": True, - "support_nested_drop": True, + "nested_support": NestedSupport.ALL, "array_element_selector": "element", }, { @@ -1037,8 +1033,7 @@ def test_comments(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture) # Test deeply nested structures ( { - "support_nested_operations": True, - "support_nested_drop": True, + "nested_support": NestedSupport.ALL, "array_element_selector": "element", }, { @@ -1067,8 +1062,8 @@ def test_alter_table( ): adapter = make_mocked_engine_adapter(EngineAdapter) - adapter.SCHEMA_DIFFER = SchemaDiffer(**schema_differ_config) - original_from_structs = adapter.SCHEMA_DIFFER._from_structs + adapter.SCHEMA_DIFFER_KWARGS = schema_differ_config + original_from_structs = adapter.schema_differ._from_structs def _from_structs(*args, **kwargs) -> t.List[TableAlterOperation]: operations = original_from_structs(*args, **kwargs) diff --git a/tests/core/engine_adapter/test_clickhouse.py b/tests/core/engine_adapter/test_clickhouse.py index b75609e759..39e317c7fa 100644 --- a/tests/core/engine_adapter/test_clickhouse.py +++ b/tests/core/engine_adapter/test_clickhouse.py @@ -7,7 +7,6 @@ from sqlmesh.core.dialect import parse from sqlglot import exp, parse_one import typing as t -from sqlmesh.core.schema_diff import SchemaDiffer from datetime import datetime from pytest_mock.plugin import MockerFixture from sqlmesh.core import dialect as d @@ -152,7 +151,7 @@ def test_alter_table( adapter: ClickhouseEngineAdapter, mocker, ): - adapter.SCHEMA_DIFFER = SchemaDiffer() + adapter.SCHEMA_DIFFER_KWARGS = {} current_table_name = "test_table" current_table = {"a": "Int8", "b": "String", "c": "Int8"} target_table_name = "target_table" diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 22d21fcef7..907d1b70cc 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -1780,3 +1780,18 @@ def test_fabric_pyodbc_connection_string_generation(): # Check autocommit parameter, should default to True for Fabric assert call_args[1]["autocommit"] is True + + +def test_schema_differ_overrides(make_config) -> None: + default_config = make_config(type="duckdb") + assert default_config.schema_differ_overrides is None + default_adapter = default_config.create_engine_adapter() + assert default_adapter._schema_differ_overrides is None + assert default_adapter.schema_differ.parameterized_type_defaults != {} + + override: t.Dict[str, t.Any] = {"parameterized_type_defaults": {}} + config = make_config(type="duckdb", schema_differ_overrides=override) + assert config.schema_differ_overrides == override + adapter = config.create_engine_adapter() + assert adapter._schema_differ_overrides == override + assert adapter.schema_differ.parameterized_type_defaults == {} diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 517d7c3ca1..dec7309591 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -7944,7 +7944,7 @@ def test_incremental_by_time_model_ignore_destructive_change(tmp_path: Path): context = Context(paths=[tmp_path], config=config) # Make the change compatible since that means we will attempt and alter now that is considered additive - context.engine_adapter.SCHEMA_DIFFER.compatible_types = { + context.engine_adapter.SCHEMA_DIFFER_KWARGS["compatible_types"] = { exp.DataType.build("INT"): {exp.DataType.build("STRING")} } context.plan("prod", auto_apply=True, no_prompts=True, run=True) @@ -8106,7 +8106,7 @@ def test_incremental_by_time_model_ignore_additive_change(tmp_path: Path): (models_dir / "test_model.sql").write_text(updated_model) context = Context(paths=[tmp_path], config=config) - context.engine_adapter.SCHEMA_DIFFER.compatible_types = { + context.engine_adapter.SCHEMA_DIFFER_KWARGS["compatible_types"] = { exp.DataType.build("INT"): {exp.DataType.build("STRING")} } context.plan("prod", auto_apply=True, no_prompts=True, run=True) @@ -8153,7 +8153,7 @@ def test_incremental_by_time_model_ignore_additive_change(tmp_path: Path): context = Context(paths=[tmp_path], config=config) # Make the change compatible since that means we will attempt and alter now that is considered additive - context.engine_adapter.SCHEMA_DIFFER.compatible_types = { + context.engine_adapter.SCHEMA_DIFFER_KWARGS["compatible_types"] = { exp.DataType.build("INT"): {exp.DataType.build("STRING")} } context.plan("prod", auto_apply=True, no_prompts=True, run=True) diff --git a/tests/core/test_schema_diff.py b/tests/core/test_schema_diff.py index 916bead3e6..e091dea539 100644 --- a/tests/core/test_schema_diff.py +++ b/tests/core/test_schema_diff.py @@ -13,6 +13,7 @@ TableAlterAddColumnOperation, TableAlterDropColumnOperation, TableAlterChangeColumnTypeOperation, + NestedSupport, ) @@ -20,7 +21,7 @@ def test_schema_diff_calculate(): alter_operations = SchemaDiffer( **{ "support_positional_add": False, - "support_nested_operations": False, + "nested_support": NestedSupport.NONE, "array_element_selector": "", "compatible_types": { exp.DataType.build("STRING"): {exp.DataType.build("INT")}, @@ -53,7 +54,7 @@ def test_schema_diff_drop_cascade(): alter_expressions = SchemaDiffer( **{ "support_positional_add": False, - "support_nested_operations": False, + "nested_support": NestedSupport.NONE, "array_element_selector": "", "drop_cascade": True, } @@ -79,7 +80,7 @@ def test_schema_diff_calculate_type_transitions(): alter_expressions = SchemaDiffer( **{ "support_positional_add": False, - "support_nested_operations": False, + "nested_support": NestedSupport.NONE, "array_element_selector": "", "compatible_types": { exp.DataType.build("STRING"): {exp.DataType.build("INT")}, @@ -426,7 +427,7 @@ def test_schema_diff_calculate_type_transitions(): position=TableAlterColumnPosition.first(), ), ], - dict(support_positional_add=True, support_nested_operations=True), + dict(support_positional_add=True, nested_support=NestedSupport.ALL_BUT_DROP), ), # Add a column to the end of a struct ( @@ -447,7 +448,7 @@ def test_schema_diff_calculate_type_transitions(): position=TableAlterColumnPosition.last(after="col_c"), ), ], - dict(support_positional_add=True, support_nested_operations=True), + dict(support_positional_add=True, nested_support=NestedSupport.ALL_BUT_DROP), ), # Add a column to the middle of a struct ( @@ -468,7 +469,7 @@ def test_schema_diff_calculate_type_transitions(): array_element_selector="", ), ], - dict(support_positional_add=True, support_nested_operations=True), + dict(support_positional_add=True, nested_support=NestedSupport.ALL_BUT_DROP), ), # Add two columns at the start of a struct ( @@ -502,7 +503,7 @@ def test_schema_diff_calculate_type_transitions(): array_element_selector="", ), ], - dict(support_positional_add=True, support_nested_operations=True), + dict(support_positional_add=True, nested_support=NestedSupport.ALL_BUT_DROP), ), # Add columns in different levels of nesting of structs ( @@ -533,7 +534,7 @@ def test_schema_diff_calculate_type_transitions(): array_element_selector="", ), ], - dict(support_positional_add=False, support_nested_operations=True), + dict(support_positional_add=False, nested_support=NestedSupport.ALL_BUT_DROP), ), # Remove a column from the start of a struct ( @@ -554,8 +555,7 @@ def test_schema_diff_calculate_type_transitions(): ], dict( support_positional_add=True, - support_nested_operations=True, - support_nested_drop=True, + nested_support=NestedSupport.ALL, ), ), # Remove a column from the end of a struct @@ -577,8 +577,7 @@ def test_schema_diff_calculate_type_transitions(): ], dict( support_positional_add=True, - support_nested_operations=True, - support_nested_drop=True, + nested_support=NestedSupport.ALL, ), ), # Remove a column from the middle of a struct @@ -600,8 +599,7 @@ def test_schema_diff_calculate_type_transitions(): ], dict( support_positional_add=True, - support_nested_operations=True, - support_nested_drop=True, + nested_support=NestedSupport.ALL, ), ), # Remove a column from a struct where nested drop is not supported @@ -631,8 +629,7 @@ def test_schema_diff_calculate_type_transitions(): ), ], dict( - support_nested_operations=True, - support_nested_drop=False, + nested_support=NestedSupport.ALL_BUT_DROP, ), ), # Remove two columns from the start of a struct @@ -665,8 +662,7 @@ def test_schema_diff_calculate_type_transitions(): ], dict( support_positional_add=True, - support_nested_operations=True, - support_nested_drop=True, + nested_support=NestedSupport.ALL, ), ), # Change a column type in a struct @@ -690,7 +686,7 @@ def test_schema_diff_calculate_type_transitions(): ], dict( support_positional_add=True, - support_nested_operations=True, + nested_support=NestedSupport.ALL_BUT_DROP, compatible_types={ exp.DataType.build("INT"): {exp.DataType.build("TEXT")}, }, @@ -754,8 +750,7 @@ def test_schema_diff_calculate_type_transitions(): ], dict( support_positional_add=True, - support_nested_operations=True, - support_nested_drop=True, + nested_support=NestedSupport.ALL, compatible_types={ exp.DataType.build("INT"): {exp.DataType.build("TEXT")}, }, @@ -788,8 +783,7 @@ def test_schema_diff_calculate_type_transitions(): ), ], dict( - support_nested_operations=True, - support_nested_drop=False, + nested_support=NestedSupport.ALL_BUT_DROP, compatible_types={ exp.DataType.build("INT"): {exp.DataType.build("TEXT")}, }, @@ -853,8 +847,7 @@ def test_schema_diff_calculate_type_transitions(): ], dict( support_positional_add=True, - support_nested_operations=True, - support_nested_drop=True, + nested_support=NestedSupport.ALL, ), ), # ##################### @@ -879,7 +872,7 @@ def test_schema_diff_calculate_type_transitions(): array_element_selector="", ), ], - dict(support_positional_add=True, support_nested_operations=True), + dict(support_positional_add=True, nested_support=NestedSupport.ALL_BUT_DROP), ), # Remove column from array of structs ( @@ -900,8 +893,7 @@ def test_schema_diff_calculate_type_transitions(): ], dict( support_positional_add=True, - support_nested_operations=True, - support_nested_drop=True, + nested_support=NestedSupport.ALL, ), ), # Alter column type in array of structs @@ -925,7 +917,7 @@ def test_schema_diff_calculate_type_transitions(): ], dict( support_positional_add=True, - support_nested_operations=True, + nested_support=NestedSupport.ALL_BUT_DROP, compatible_types={ exp.DataType.build("INT"): {exp.DataType.build("TEXT")}, }, @@ -958,7 +950,7 @@ def test_schema_diff_calculate_type_transitions(): array_element_selector="", ), ], - dict(support_positional_add=False, support_nested_operations=True), + dict(support_positional_add=False, nested_support=NestedSupport.ALL_BUT_DROP), ), # Add an array of primitives ( @@ -978,7 +970,7 @@ def test_schema_diff_calculate_type_transitions(): array_element_selector="", ), ], - dict(support_positional_add=True, support_nested_operations=True), + dict(support_positional_add=True, nested_support=NestedSupport.ALL_BUT_DROP), ), # untyped array to support Snowflake ( @@ -1134,8 +1126,7 @@ def test_schema_diff_calculate_type_transitions(): ], dict( support_positional_add=True, - support_nested_operations=True, - support_nested_drop=True, + nested_support=NestedSupport.ALL, ), ), # Type with precision to same type with no precision and no default is DROP/ADD @@ -1398,7 +1389,7 @@ def test_schema_diff_calculate_type_transitions(): [], dict( support_positional_add=True, - support_nested_operations=True, + nested_support=NestedSupport.ALL_BUT_DROP, support_coercing_compatible_types=True, compatible_types={ exp.DataType.build("INT"): {exp.DataType.build("FLOAT")}, @@ -1411,7 +1402,7 @@ def test_schema_diff_calculate_type_transitions(): [], dict( support_positional_add=True, - support_nested_operations=True, + nested_support=NestedSupport.ALL_BUT_DROP, coerceable_types={ exp.DataType.build("FLOAT"): {exp.DataType.build("INT")}, }, @@ -1423,7 +1414,7 @@ def test_schema_diff_calculate_type_transitions(): [], dict( support_positional_add=True, - support_nested_operations=True, + nested_support=NestedSupport.ALL_BUT_DROP, support_coercing_compatible_types=True, compatible_types={ exp.DataType.build("INT"): {exp.DataType.build("FLOAT")}, @@ -1453,13 +1444,108 @@ def test_schema_diff_calculate_type_transitions(): ], dict( support_positional_add=False, - support_nested_operations=True, + nested_support=NestedSupport.ALL_BUT_DROP, support_coercing_compatible_types=True, compatible_types={ exp.DataType.build("INT"): {exp.DataType.build("FLOAT")}, }, ), ), + # ################### + # Ignore Nested Tests + # ################### + # Remove nested col_c + ( + "STRUCT>", + "STRUCT>", + [], + dict(nested_support=NestedSupport.IGNORE), + ), + # Add nested col_d + ( + "STRUCT>", + "STRUCT>", + [], + dict(nested_support=NestedSupport.IGNORE), + ), + # Change nested col_c to incompatible type + ( + "STRUCT>", + "STRUCT>", + [], + dict(nested_support=NestedSupport.IGNORE), + ), + # Change nested col_c to compatible type + ( + "STRUCT>", + "STRUCT>", + [], + dict( + nested_support=NestedSupport.IGNORE, + compatible_types={ + exp.DataType.build("INT"): {exp.DataType.build("STRING")}, + }, + ), + ), + # Mix of ignored nested and non-nested changes + ( + "STRUCT, age INT>", + "STRUCT, age STRING, new_col INT>", + [ + # `col_c` change is ignored + TableAlterAddColumnOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("new_col")], + column_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT, age INT, new_col INT>" + ), + position=TableAlterColumnPosition.last("age"), + array_element_selector="", + ), + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("age")], + column_type=exp.DataType.build("STRING"), + current_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build( + "STRUCT, age STRING, new_col INT>" + ), + array_element_selector="", + ), + ], + dict( + nested_support=NestedSupport.IGNORE, + compatible_types={ + exp.DataType.build("INT"): {exp.DataType.build("STRING")}, + }, + support_positional_add=True, + ), + ), + # ############################ + # Change Data Type Destructive + # ############################ + ( + "STRUCT", + "STRUCT", + [ + TableAlterChangeColumnTypeOperation( + target_table=exp.to_table("apply_to_table"), + column_parts=[TableAlterColumn.primitive("age")], + column_type=exp.DataType.build("STRING"), + current_type=exp.DataType.build("INT"), + expected_table_struct=exp.DataType.build("STRUCT"), + array_element_selector="", + is_part_of_destructive_change=True, + ), + ], + dict( + treat_alter_data_type_as_destructive=True, + compatible_types={ + exp.DataType.build("INT"): {exp.DataType.build("STRING")}, + }, + ), + ), ], ) def test_struct_diff( @@ -1750,7 +1836,7 @@ def test_ignore_destructive_compare_columns(): """Test ignore_destructive behavior in compare_columns method.""" schema_differ = SchemaDiffer( support_positional_add=True, - support_nested_operations=False, + nested_support=NestedSupport.NONE, compatible_types={ exp.DataType.build("INT"): {exp.DataType.build("STRING")}, }, @@ -1796,8 +1882,7 @@ def test_ignore_destructive_compare_columns(): def test_ignore_destructive_nested_struct_without_support(): """Test ignore_destructive with nested structs when nested_drop is not supported.""" schema_differ = SchemaDiffer( - support_nested_operations=True, - support_nested_drop=False, # This forces DROP+ADD for nested changes + nested_support=NestedSupport.ALL_BUT_DROP, # This forces DROP+ADD for nested changes ) current_struct = "STRUCT>" @@ -1834,8 +1919,7 @@ def test_get_schema_differ(): # Databricks should support positional add and nested operations databricks_differ = get_schema_differ("databricks") assert databricks_differ.support_positional_add is True - assert databricks_differ.support_nested_operations is True - assert databricks_differ.support_nested_drop is True + assert databricks_differ.nested_support == NestedSupport.ALL # BigQuery should have specific compatible types configured bigquery_differ = get_schema_differ("bigquery") @@ -1860,7 +1944,7 @@ def test_get_schema_differ(): schema_differ_unknown = get_schema_differ("unknown_dialect") assert isinstance(schema_differ_unknown, SchemaDiffer) assert schema_differ_unknown.support_positional_add is False - assert schema_differ_unknown.support_nested_operations is False + assert schema_differ_unknown.nested_support == NestedSupport.NONE # Test case insensitivity schema_differ_upper = get_schema_differ("BIGQUERY") @@ -1870,6 +1954,10 @@ def test_get_schema_differ(): == schema_differ_lower.support_coercing_compatible_types ) + # Test override + schema_differ_with_override = get_schema_differ("postgres", {"drop_cascade": False}) + assert schema_differ_with_override.drop_cascade is False + def test_ignore_destructive_edge_cases(): """Test edge cases for ignore_destructive behavior.""" @@ -2116,7 +2204,7 @@ def test_ignore_destructive_edge_cases(): ), ], [], # No operations when ignoring additive - dict(support_nested_operations=True), + dict(nested_support=NestedSupport.ALL_BUT_DROP), ), ], ) @@ -2228,7 +2316,7 @@ def test_ignore_both_destructive_and_additive(): def test_ignore_additive_array_operations(): """Test ignore_additive with array of struct operations.""" schema_differ = SchemaDiffer( - support_nested_operations=True, + nested_support=NestedSupport.ALL, support_positional_add=True, ) diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 72994fe33c..bc6f878801 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -35,6 +35,7 @@ TrinoConfig, AthenaConfig, ClickhouseConfig, + SCHEMA_DIFFER_OVERRIDES, ) from sqlmesh.dbt.test import TestConfig from sqlmesh.utils.errors import ConfigError @@ -542,6 +543,9 @@ def test_snowflake_config(): ) sqlmesh_config = config.to_sqlmesh() assert sqlmesh_config.application == "Tobiko_SQLMesh" + assert ( + sqlmesh_config.schema_differ_overrides == SCHEMA_DIFFER_OVERRIDES["schema_differ_overrides"] + ) def test_snowflake_config_private_key_path(): @@ -771,6 +775,7 @@ def test_databricks_config_oauth(): assert as_sqlmesh.auth_type == "databricks-oauth" assert as_sqlmesh.oauth_client_id == "client-id" assert as_sqlmesh.oauth_client_secret == "client-secret" + assert as_sqlmesh.schema_differ_overrides == SCHEMA_DIFFER_OVERRIDES["schema_differ_overrides"] def test_bigquery_config(): From 4d8e83152190316dfffdf4a214a679415c40ab09 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 22 Aug 2025 10:11:06 -0700 Subject: [PATCH 0745/1056] chore(web_common): add license to package.json (#5212) --- web/common/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/web/common/package.json b/web/common/package.json index 7ea7316bed..4cccbec171 100644 --- a/web/common/package.json +++ b/web/common/package.json @@ -3,6 +3,7 @@ "version": "0.0.1", "private": false, "type": "module", + "license": "Apache-2.0", "repository": { "type": "git", "url": "https://github.com/TobikoData/sqlmesh" From 3d144c530a738d020d77e3b2e8aba94db3b608e3 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 22 Aug 2025 12:52:48 -0700 Subject: [PATCH 0746/1056] Feat(dbt): Add support for adapter.expand_target_column_types (#5206) --- docs/integrations/dbt.md | 23 +++++------- sqlmesh/dbt/adapter.py | 45 ++++++++++++++++++++++ tests/dbt/test_adapter.py | 79 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 13 deletions(-) diff --git a/docs/integrations/dbt.md b/docs/integrations/dbt.md index c5e4bdd2d9..4342f47779 100644 --- a/docs/integrations/dbt.md +++ b/docs/integrations/dbt.md @@ -344,18 +344,16 @@ Model documentation is available in the [SQLMesh UI](../quickstart/ui.md#2-open- SQLMesh supports running dbt projects using the majority of dbt jinja methods, including: -| Method | Method | Method | Method | -| ----------- | -------------- | ------------ | ------- | -| adapter (*) | env_var | project_name | target | -| as_bool | exceptions | ref | this | -| as_native | from_yaml | return | to_yaml | -| as_number | is_incremental | run_query | var | -| as_text | load_result | schema | zip | -| api | log | set | | -| builtins | modules | source | | -| config | print | statement | | - -\* `adapter.expand_target_column_types` is not currently supported. +| Method | Method | Method | Method | +| --------- | -------------- | ------------ | ------- | +| adapter | env_var | project_name | target | +| as_bool | exceptions | ref | this | +| as_native | from_yaml | return | to_yaml | +| as_number | is_incremental | run_query | var | +| as_text | load_result | schema | zip | +| api | log | set | | +| builtins | modules | source | | +| config | print | statement | | ## Unsupported dbt jinja methods @@ -363,7 +361,6 @@ The dbt jinja methods that are not currently supported are: * debug * selected_sources -* adapter.expand_target_column_types * graph.nodes.values * graph.metrics.values diff --git a/sqlmesh/dbt/adapter.py b/sqlmesh/dbt/adapter.py index 7de90a8ea5..2dc9890ca4 100644 --- a/sqlmesh/dbt/adapter.py +++ b/sqlmesh/dbt/adapter.py @@ -12,6 +12,7 @@ from sqlmesh.utils.errors import ConfigError, ParsetimeAdapterCallError from sqlmesh.utils.jinja import JinjaMacroRegistry from sqlmesh.utils import AttributeDict +from sqlmesh.core.schema_diff import TableAlterOperation if t.TYPE_CHECKING: import agate @@ -85,6 +86,12 @@ def drop_schema(self, relation: BaseRelation) -> None: def drop_relation(self, relation: BaseRelation) -> None: """Drops a relation (table) in the target database.""" + @abc.abstractmethod + def expand_target_column_types( + self, from_relation: BaseRelation, to_relation: BaseRelation + ) -> None: + """Expand to_relation's column types to match those of from_relation.""" + @abc.abstractmethod def rename_relation(self, from_relation: BaseRelation, to_relation: BaseRelation) -> None: """Renames a relation (table) in the target database.""" @@ -213,6 +220,11 @@ def drop_schema(self, relation: BaseRelation) -> None: def drop_relation(self, relation: BaseRelation) -> None: self._raise_parsetime_adapter_call_error("drop relation") + def expand_target_column_types( + self, from_relation: BaseRelation, to_relation: BaseRelation + ) -> None: + self._raise_parsetime_adapter_call_error("expand target column types") + def rename_relation(self, from_relation: BaseRelation, to_relation: BaseRelation) -> None: self._raise_parsetime_adapter_call_error("rename relation") @@ -355,6 +367,39 @@ def drop_relation(self, relation: BaseRelation) -> None: if relation.schema is not None and relation.identifier is not None: self.engine_adapter.drop_table(self._normalize(self._relation_to_table(relation))) + def expand_target_column_types( + self, from_relation: BaseRelation, to_relation: BaseRelation + ) -> None: + from_dbt_columns = {c.name: c for c in self.get_columns_in_relation(from_relation)} + to_dbt_columns = {c.name: c for c in self.get_columns_in_relation(to_relation)} + + from_table_name = self._normalize(self._relation_to_table(from_relation)) + to_table_name = self._normalize(self._relation_to_table(to_relation)) + + from_columns = self.engine_adapter.columns(from_table_name) + to_columns = self.engine_adapter.columns(to_table_name) + + current_columns = {} + new_columns = {} + for column_name, from_column in from_dbt_columns.items(): + target_column = to_dbt_columns.get(column_name) + if target_column is not None and target_column.can_expand_to(from_column): + current_columns[column_name] = to_columns[column_name] + new_columns[column_name] = from_columns[column_name] + + alter_expressions = t.cast( + t.List[TableAlterOperation], + self.engine_adapter.schema_differ.compare_columns( + to_table_name, + current_columns, + new_columns, + ignore_destructive=True, + ), + ) + + if alter_expressions: + self.engine_adapter.alter_table(alter_expressions) + def rename_relation(self, from_relation: BaseRelation, to_relation: BaseRelation) -> None: old_table_name = self._normalize(self._relation_to_table(from_relation)) new_table_name = self._normalize(self._relation_to_table(to_relation)) diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index 5a41d237d3..445e5f29c0 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -18,6 +18,7 @@ from sqlmesh.dbt.target import BigQueryConfig, SnowflakeConfig from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.jinja import JinjaMacroRegistry +from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterChangeColumnTypeOperation pytestmark = pytest.mark.dbt @@ -349,3 +350,81 @@ def test_adapter_get_relation_normalization( renderer("{{ adapter.list_relations(database=None, schema='foo') }}") == '[]' ) + + +def test_adapter_expand_target_column_types( + sushi_test_project: Project, runtime_renderer: t.Callable, mocker: MockerFixture +): + from sqlmesh.core.engine_adapter.base import DataObject, DataObjectType + + data_object_from = DataObject( + catalog="test", schema="foo", name="from_table", type=DataObjectType.TABLE + ) + data_object_to = DataObject( + catalog="test", schema="foo", name="to_table", type=DataObjectType.TABLE + ) + from_columns = { + "int_col": exp.DataType.build("int"), + "same_text_col": exp.DataType.build("varchar(1)"), # varchar(1) -> varchar(1) + "unexpandable_text_col": exp.DataType.build("varchar(2)"), # varchar(4) -> varchar(2) + "expandable_text_col1": exp.DataType.build("varchar(16)"), # varchar(8) -> varchar(16) + "expandable_text_col2": exp.DataType.build("varchar(64)"), # varchar(32) -> varchar(64) + } + to_columns = { + "int_col": exp.DataType.build("int"), + "same_text_col": exp.DataType.build("varchar(1)"), + "unexpandable_text_col": exp.DataType.build("varchar(4)"), + "expandable_text_col1": exp.DataType.build("varchar(8)"), + "expandable_text_col2": exp.DataType.build("varchar(32)"), + } + adapter_mock = mocker.MagicMock() + adapter_mock.default_catalog = "test" + adapter_mock.get_data_object.side_effect = [data_object_from, data_object_to] + # columns() is called 4 times, twice by adapter.get_columns_in_relation() and twice by the engine_adapter + adapter_mock.columns.side_effect = [ + from_columns, + to_columns, + from_columns, + to_columns, + ] + adapter_mock.schema_differ = SchemaDiffer() + + context = sushi_test_project.context + renderer = runtime_renderer(context, engine_adapter=adapter_mock) + + renderer(""" + {%- set from_relation = adapter.get_relation( + database=None, + schema='foo', + identifier='from_table') -%} + + {% set to_relation = adapter.get_relation( + database=None, + schema='foo', + identifier='to_table') -%} + + {% do adapter.expand_target_column_types(from_relation, to_relation) %} + """) + adapter_mock.get_data_object.assert_has_calls( + [ + call(exp.to_table('"test"."foo"."from_table"')), + call(exp.to_table('"test"."foo"."to_table"')), + ] + ) + assert len(adapter_mock.alter_table.call_args.args) == 1 + alter_expressions = adapter_mock.alter_table.call_args.args[0] + assert len(alter_expressions) == 2 + alter_operation1 = alter_expressions[0] + assert isinstance(alter_operation1, TableAlterChangeColumnTypeOperation) + assert alter_operation1.expression == parse_one( + """ALTER TABLE "test"."foo"."to_table" + ALTER COLUMN expandable_text_col1 + SET DATA TYPE VARCHAR(16)""" + ) + alter_operation2 = alter_expressions[1] + assert isinstance(alter_operation2, TableAlterChangeColumnTypeOperation) + assert alter_operation2.expression == parse_one( + """ALTER TABLE "test"."foo"."to_table" + ALTER COLUMN expandable_text_col2 + SET DATA TYPE VARCHAR(64)""" + ) From 08739b8be2df8038fa4424c2445868c6214564bd Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 22 Aug 2025 13:44:34 -0700 Subject: [PATCH 0747/1056] Fix: Intercept errors when loading the dbt manifest (#5214) --- sqlmesh/dbt/manifest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 91c87f413e..125b204270 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -34,6 +34,7 @@ from dbt.tracking import do_not_track from sqlmesh.core import constants as c +from sqlmesh.utils.errors import SQLMeshError from sqlmesh.core.config import ModelDefaultsConfig from sqlmesh.dbt.basemodel import Dependencies from sqlmesh.dbt.builtin import BUILTIN_FILTERS, BUILTIN_GLOBALS, OVERRIDDEN_MACROS @@ -387,7 +388,10 @@ def _load_on_run_start_end(self) -> None: @property def _manifest(self) -> Manifest: if not self.__manifest: - self.__manifest = self._load_manifest() + try: + self.__manifest = self._load_manifest() + except Exception as ex: + raise SQLMeshError(f"Failed to load dbt manifest: {ex}") from ex return self.__manifest def _load_manifest(self) -> Manifest: From d3eae73fdaea48c8aed6a669839d270d7beb7a2b Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:34:00 -0700 Subject: [PATCH 0748/1056] feat: dbt adapter allow invalid ref for tests (#5207) --- sqlmesh/dbt/basemodel.py | 17 ++++++++ sqlmesh/dbt/context.py | 5 ++- sqlmesh/dbt/loader.py | 11 ++++- sqlmesh/utils/errors.py | 8 ++++ tests/dbt/test_model.py | 90 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 4 deletions(-) diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index d226325dbc..73e0252332 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -245,6 +245,22 @@ def tests_ref_source_dependencies(self) -> Dependencies: dependencies.macros = [] return dependencies + def remove_tests_with_invalid_refs(self, context: DbtContext) -> None: + """ + Removes tests that reference models that do not exist in the context in order to match dbt behavior. + + Args: + context: The dbt context this model resides within. + + Returns: + None + """ + self.tests = [ + test + for test in self.tests + if all(ref in context.refs for ref in test.dependencies.refs) + ] + def check_for_circular_test_refs(self, context: DbtContext) -> None: """ Checks for direct circular references between two models and raises an exception if found. @@ -295,6 +311,7 @@ def sqlmesh_model_kwargs( column_types_override: t.Optional[t.Dict[str, ColumnConfig]] = None, ) -> t.Dict[str, t.Any]: """Get common sqlmesh model parameters""" + self.remove_tests_with_invalid_refs(context) self.check_for_circular_test_refs(context) model_dialect = self.dialect(context) model_context = context.context_for_dependencies( diff --git a/sqlmesh/dbt/context.py b/sqlmesh/dbt/context.py index 2eceb005a7..307dea6477 100644 --- a/sqlmesh/dbt/context.py +++ b/sqlmesh/dbt/context.py @@ -11,7 +11,7 @@ from sqlmesh.dbt.manifest import ManifestHelper from sqlmesh.dbt.target import TargetConfig from sqlmesh.utils import AttributeDict -from sqlmesh.utils.errors import ConfigError, SQLMeshError +from sqlmesh.utils.errors import ConfigError, SQLMeshError, MissingModelError from sqlmesh.utils.jinja import ( JinjaGlobalAttribute, JinjaMacroRegistry, @@ -265,7 +265,8 @@ def context_for_dependencies(self, dependencies: Dependencies) -> DbtContext: else: models[ref] = t.cast(ModelConfig, model) else: - raise ConfigError(f"Model '{ref}' was not found.") + exception = MissingModelError(ref) + raise exception for source in dependencies.sources: if source in self.sources: diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 4bfe78cca0..be0ff59aa4 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -23,7 +23,7 @@ from sqlmesh.dbt.project import Project from sqlmesh.dbt.target import TargetConfig from sqlmesh.utils import UniqueKeyDict -from sqlmesh.utils.errors import ConfigError +from sqlmesh.utils.errors import ConfigError, MissingModelError from sqlmesh.utils.jinja import ( JinjaMacroRegistry, make_jinja_registry, @@ -162,7 +162,14 @@ def _load_audits( context.set_and_render_variables(package.variables, package.name) for test in package.tests.values(): logger.debug("Converting '%s' to sqlmesh format", test.name) - audits[test.name] = test.to_sqlmesh(context) + try: + audits[test.name] = test.to_sqlmesh(context) + except MissingModelError as e: + logger.warning( + "Skipping audit '%s' because model '%s' is not a valid ref", + test.name, + e.model_name, + ) return audits diff --git a/sqlmesh/utils/errors.py b/sqlmesh/utils/errors.py index 82ec311237..8efb0af88a 100644 --- a/sqlmesh/utils/errors.py +++ b/sqlmesh/utils/errors.py @@ -33,6 +33,14 @@ def __init__(self, message: str | Exception, location: t.Optional[Path] = None) self.location = Path(location) if isinstance(location, str) else location +class MissingModelError(ConfigError): + """Raised when a model that is referenced is missing.""" + + def __init__(self, model_name: str) -> None: + self.model_name = model_name + super().__init__(f"Model '{model_name}' was not found.") + + class MissingDependencyError(SQLMeshError): """Local environment is missing a required dependency for the given operation""" diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index cf88872fc7..c2becfbc16 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -1,10 +1,14 @@ import pytest +from pathlib import Path + +from sqlmesh import Context from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.model import ModelConfig from sqlmesh.dbt.test import TestConfig from sqlmesh.utils.errors import ConfigError +from sqlmesh.utils.yaml import YAML pytestmark = pytest.mark.dbt @@ -44,3 +48,89 @@ def test_model_test_circular_references() -> None: upstream_model.check_for_circular_test_refs(context) with pytest.raises(ConfigError, match="between tests"): downstream_model.check_for_circular_test_refs(context) + + +def test_load_invalid_ref_audit_constraints(tmp_path: Path, caplog) -> None: + yaml = YAML() + dbt_project_dir = tmp_path / "dbt" + dbt_project_dir.mkdir() + dbt_model_dir = dbt_project_dir / "models" + dbt_model_dir.mkdir() + full_model_contents = "SELECT 1 as cola" + full_model_file = dbt_model_dir / "full_model.sql" + with open(full_model_file, "w", encoding="utf-8") as f: + f.write(full_model_contents) + model_schema = { + "version": 2, + "models": [ + { + "name": "full_model", + "description": "A full model bad ref for audit and constraints", + "columns": [ + { + "name": "cola", + "description": "A column that is used in a ref audit and constraints", + "constraints": [ + { + "type": "primary_key", + "columns": ["cola"], + "expression": "ref('not_real_model') (cola)", + } + ], + "tests": [ + { + "relationships": { + "to": "ref('not_real_model')", + "field": "cola", + "description": "A test that references a model that does not exist", + } + } + ], + } + ], + } + ], + } + model_schema_file = dbt_model_dir / "schema.yml" + with open(model_schema_file, "w", encoding="utf-8") as f: + yaml.dump(model_schema, f) + dbt_project_config = { + "name": "invalid_ref_audit_constraints", + "version": "1.0.0", + "config-version": 2, + "profile": "test", + "model-paths": ["models"], + } + dbt_project_file = dbt_project_dir / "dbt_project.yml" + with open(dbt_project_file, "w", encoding="utf-8") as f: + yaml.dump(dbt_project_config, f) + sqlmesh_config = { + "model_defaults": { + "start": "2025-01-01", + } + } + sqlmesh_config_file = dbt_project_dir / "sqlmesh.yaml" + with open(sqlmesh_config_file, "w", encoding="utf-8") as f: + yaml.dump(sqlmesh_config, f) + dbt_data_dir = tmp_path / "dbt_data" + dbt_data_dir.mkdir() + dbt_data_file = dbt_data_dir / "local.db" + dbt_profile_config = { + "test": { + "outputs": {"duckdb": {"type": "duckdb", "path": str(dbt_data_file)}}, + "target": "duckdb", + } + } + db_profile_file = dbt_project_dir / "profiles.yml" + with open(db_profile_file, "w", encoding="utf-8") as f: + yaml.dump(dbt_profile_config, f) + + context = Context(paths=dbt_project_dir) + assert ( + "Skipping audit 'relationships_full_model_cola__cola__ref_not_real_model_' because model 'not_real_model' is not a valid ref" + in caplog.text + ) + fqn = '"local"."main"."full_model"' + assert fqn in context.snapshots + # The audit isn't loaded due to the invalid ref + assert context.snapshots[fqn].model.audits == [] From 42096729b9757291f0eb1e662cd015522e6ea21a Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 22 Aug 2025 16:13:35 -0700 Subject: [PATCH 0749/1056] Fix!: Improve tracking of var dependencies in dbt models (#5204) --- sqlmesh/dbt/basemodel.py | 52 ++++++++------ sqlmesh/dbt/common.py | 3 + sqlmesh/dbt/context.py | 4 +- sqlmesh/dbt/loader.py | 20 +++--- sqlmesh/dbt/manifest.py | 3 + sqlmesh/dbt/project.py | 12 ++-- tests/dbt/test_config.py | 22 ++---- tests/dbt/test_manifest.py | 2 + tests/dbt/test_transformation.py | 67 +++++++++++++++++++ tests/fixtures/dbt/sushi_test/dbt_project.yml | 1 + .../sushi_test/macros/test_dependencies.sql | 6 ++ .../models/waiter_revenue_by_day.sql | 3 +- 12 files changed, 136 insertions(+), 59 deletions(-) diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index 73e0252332..f1e1dbed03 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -313,33 +313,21 @@ def sqlmesh_model_kwargs( """Get common sqlmesh model parameters""" self.remove_tests_with_invalid_refs(context) self.check_for_circular_test_refs(context) + + dependencies = self.dependencies.copy() + if dependencies.has_dynamic_var_names: + # Include ALL variables as dependencies since we couldn't determine + # precisely which variables are referenced in the model + dependencies.variables |= set(context.variables) + model_dialect = self.dialect(context) model_context = context.context_for_dependencies( - self.dependencies.union(self.tests_ref_source_dependencies) + dependencies.union(self.tests_ref_source_dependencies) ) jinja_macros = model_context.jinja_macros.trim( - self.dependencies.macros, package=self.package_name - ) - - model_node: AttributeDict[str, t.Any] = AttributeDict( - { - k: v - for k, v in context._manifest._manifest.nodes[self.node_name].to_dict().items() - if k in self.dependencies.model_attrs - } - if context._manifest and self.node_name in context._manifest._manifest.nodes - else {} - ) - - jinja_macros.add_globals( - { - "this": self.relation_info, - "model": model_node, - "schema": self.table_schema, - "config": self.config_attribute_dict, - **model_context.jinja_globals, # type: ignore - } + dependencies.macros, package=self.package_name ) + jinja_macros.add_globals(self._model_jinja_context(model_context, dependencies)) return { "audits": [(test.name, {}) for test in self.tests], "columns": column_types_to_sqlmesh( @@ -369,3 +357,23 @@ def to_sqlmesh( virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default, ) -> Model: """Convert DBT model into sqlmesh Model""" + + def _model_jinja_context( + self, context: DbtContext, dependencies: Dependencies + ) -> t.Dict[str, t.Any]: + model_node: AttributeDict[str, t.Any] = AttributeDict( + { + k: v + for k, v in context._manifest._manifest.nodes[self.node_name].to_dict().items() + if k in dependencies.model_attrs + } + if context._manifest and self.node_name in context._manifest._manifest.nodes + else {} + ) + return { + "this": self.relation_info, + "model": model_node, + "schema": self.table_schema, + "config": self.config_attribute_dict, + **context.jinja_globals, + } diff --git a/sqlmesh/dbt/common.py b/sqlmesh/dbt/common.py index d9db5a472c..ec928576ed 100644 --- a/sqlmesh/dbt/common.py +++ b/sqlmesh/dbt/common.py @@ -184,6 +184,8 @@ class Dependencies(PydanticModel): variables: t.Set[str] = set() model_attrs: t.Set[str] = set() + has_dynamic_var_names: bool = False + def union(self, other: Dependencies) -> Dependencies: return Dependencies( macros=list(set(self.macros) | set(other.macros)), @@ -191,6 +193,7 @@ def union(self, other: Dependencies) -> Dependencies: refs=self.refs | other.refs, variables=self.variables | other.variables, model_attrs=self.model_attrs | other.model_attrs, + has_dynamic_var_names=self.has_dynamic_var_names or other.has_dynamic_var_names, ) @field_validator("macros", mode="after") diff --git a/sqlmesh/dbt/context.py b/sqlmesh/dbt/context.py index 307dea6477..d76cccbce7 100644 --- a/sqlmesh/dbt/context.py +++ b/sqlmesh/dbt/context.py @@ -8,6 +8,7 @@ from sqlmesh.core.config import Config as SQLMeshConfig from sqlmesh.dbt.builtin import _relation_info_to_relation +from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.manifest import ManifestHelper from sqlmesh.dbt.target import TargetConfig from sqlmesh.utils import AttributeDict @@ -22,7 +23,6 @@ if t.TYPE_CHECKING: from jinja2 import Environment - from sqlmesh.dbt.basemodel import Dependencies from sqlmesh.dbt.model import ModelConfig from sqlmesh.dbt.relation import Policy from sqlmesh.dbt.seed import SeedConfig @@ -101,8 +101,6 @@ def add_variables(self, variables: t.Dict[str, t.Any]) -> None: self._jinja_environment = None def set_and_render_variables(self, variables: t.Dict[str, t.Any], package: str) -> None: - self.variables = variables - jinja_environment = self.jinja_macros.build_environment(**self.jinja_globals) def _render_var(value: t.Any) -> t.Any: diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index be0ff59aa4..d321246896 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -124,8 +124,6 @@ def _to_sqlmesh(config: BMC, context: DbtContext) -> Model: ) for project in self._load_projects(): - context = project.context.copy() - macros_max_mtime = self._macros_max_mtime yaml_max_mtimes = self._compute_yaml_max_mtime_per_subfolder( project.context.project_root @@ -135,12 +133,13 @@ def _to_sqlmesh(config: BMC, context: DbtContext) -> Model: logger.debug("Converting models to sqlmesh") # Now that config is rendered, create the sqlmesh models for package in project.packages.values(): - context.set_and_render_variables(package.variables, package.name) + package_context = project.context.copy() + package_context.set_and_render_variables(package.variables, package.name) package_models: t.Dict[str, BaseModelConfig] = {**package.models, **package.seeds} for model in package_models.values(): sqlmesh_model = cache.get_or_load_models( - model.path, loader=lambda: [_to_sqlmesh(model, context)] + model.path, loader=lambda: [_to_sqlmesh(model, package_context)] )[0] models[sqlmesh_model.fqn] = sqlmesh_model @@ -155,15 +154,14 @@ def _load_audits( audits: UniqueKeyDict = UniqueKeyDict("audits") for project in self._load_projects(): - context = project.context - logger.debug("Converting audits to sqlmesh") for package in project.packages.values(): - context.set_and_render_variables(package.variables, package.name) + package_context = project.context.copy() + package_context.set_and_render_variables(package.variables, package.name) for test in package.tests.values(): logger.debug("Converting '%s' to sqlmesh format", test.name) try: - audits[test.name] = test.to_sqlmesh(context) + audits[test.name] = test.to_sqlmesh(package_context) except MissingModelError as e: logger.warning( "Skipping audit '%s' because model '%s' is not a valid ref", @@ -244,9 +242,9 @@ def _load_environment_statements(self, macros: MacroRegistry) -> t.List[Environm project_names: t.Set[str] = set() dialect = self.config.dialect for project in self._load_projects(): - context = project.context for package_name, package in project.packages.items(): - context.set_and_render_variables(package.variables, package_name) + package_context = project.context.copy() + package_context.set_and_render_variables(package.variables, package_name) on_run_start: t.List[str] = [ on_run_hook.sql for on_run_hook in sorted(package.on_run_start.values(), key=lambda h: h.index) @@ -261,7 +259,7 @@ def _load_environment_statements(self, macros: MacroRegistry) -> t.List[Environm for hook in [*package.on_run_start.values(), *package.on_run_end.values()]: dependencies = dependencies.union(hook.dependencies) - statements_context = context.context_for_dependencies(dependencies) + statements_context = package_context.context_for_dependencies(dependencies) jinja_registry = make_jinja_registry( statements_context.jinja_macros, package_name, set(dependencies.macros) ) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 125b204270..7414325902 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -554,6 +554,9 @@ def _extra_dependencies(self, target: str, package: str) -> Dependencies: args = [jinja_call_arg_name(arg) for arg in node.args] if args and args[0]: dependencies.variables.add(args[0]) + else: + # We couldn't determine the var name statically + dependencies.has_dynamic_var_names = True dependencies.macros.append(MacroReference(name="var")) elif len(call_name) == 1: macro_name = call_name[0] diff --git a/sqlmesh/dbt/project.py b/sqlmesh/dbt/project.py index d37c9cc6c4..581660943a 100644 --- a/sqlmesh/dbt/project.py +++ b/sqlmesh/dbt/project.py @@ -55,9 +55,6 @@ def load(cls, context: DbtContext, variables: t.Optional[t.Dict[str, t.Any]] = N raise ConfigError(f"Could not find {PROJECT_FILENAME} in {context.project_root}") project_yaml = load_yaml(project_file_path) - variable_overrides = variables - variables = {**project_yaml.get("vars", {}), **(variables or {})} - project_name = context.render(project_yaml.get("name", "")) context.project_name = project_name if not context.project_name: @@ -69,6 +66,7 @@ def load(cls, context: DbtContext, variables: t.Optional[t.Dict[str, t.Any]] = N profile = Profile.load(context, context.target_name) context.target = profile.target + variable_overrides = variables or {} context.manifest = ManifestHelper( project_file_path.parent, profile.path.parent, @@ -101,13 +99,17 @@ def load(cls, context: DbtContext, variables: t.Optional[t.Dict[str, t.Any]] = N package = package_loader.load(path.parent) packages[package.name] = package + all_project_variables = {**project_yaml.get("vars", {}), **(variable_overrides or {})} for name, package in packages.items(): - package_vars = variables.get(name) + package_vars = all_project_variables.get(name) if isinstance(package_vars, dict): package.variables.update(package_vars) - package.variables.update(variables) + if name == context.project_name: + package.variables.update(all_project_variables) + else: + package.variables.update(variable_overrides) return Project(context, profile, packages) diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index bc6f878801..99426ebb97 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -362,6 +362,7 @@ def test_variables(assert_exp_eq, sushi_test_project): "nested_vars": { "some_nested_var": 2, }, + "dynamic_test_var": 3, "list_var": [ {"name": "item1", "value": 1}, {"name": "item2", "value": 2}, @@ -375,25 +376,10 @@ def test_variables(assert_exp_eq, sushi_test_project): expected_customer_variables = { "some_var": ["foo", "bar"], "some_other_var": 5, - "yet_another_var": 1, + "yet_another_var": 5, "customers:bla": False, "customers:customer_id": "customer_id", "start": "Jan 1 2022", - "top_waiters:limit": 10, - "top_waiters:revenue": "revenue", - "customers:boo": ["a", "b"], - "nested_vars": { - "some_nested_var": 2, - }, - "list_var": [ - {"name": "item1", "value": 1}, - {"name": "item2", "value": 2}, - ], - "customers": { - "customers:bla": False, - "customers:customer_id": "customer_id", - "some_var": ["foo", "bar"], - }, } assert sushi_test_project.packages["sushi"].variables == expected_sushi_variables @@ -406,7 +392,9 @@ def test_nested_variables(sushi_test_project): sql="SELECT {{ var('nested_vars')['some_nested_var'] }}", dependencies=Dependencies(variables=["nested_vars"]), ) - sqlmesh_model = model_config.to_sqlmesh(sushi_test_project.context) + context = sushi_test_project.context.copy() + context.set_and_render_variables(sushi_test_project.packages["sushi"].variables, "sushi") + sqlmesh_model = model_config.to_sqlmesh(context) assert sqlmesh_model.jinja_macros.global_objs["vars"]["nested_vars"] == {"some_nested_var": 2} diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index bf64e4b8b3..2bed6acb55 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -79,6 +79,7 @@ def test_manifest_helper(caplog): waiter_revenue_by_day_config = models["waiter_revenue_by_day_v2"] assert waiter_revenue_by_day_config.dependencies == Dependencies( macros={ + MacroReference(name="dynamic_var_name_dependency"), MacroReference(name="log_value"), MacroReference(name="test_dependencies"), MacroReference(package="customers", name="duckdb__current_engine"), @@ -87,6 +88,7 @@ def test_manifest_helper(caplog): }, sources={"streaming.items", "streaming.orders", "streaming.order_items"}, variables={"yet_another_var", "nested_vars"}, + has_dynamic_var_names=True, ) assert waiter_revenue_by_day_config.materialized == "incremental" assert waiter_revenue_by_day_config.incremental_strategy == "delete+insert" diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index fdb8345398..1bcc3081f7 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -37,6 +37,7 @@ ) from sqlmesh.core.state_sync.db.snapshot import _snapshot_to_json from sqlmesh.dbt.builtin import _relation_info_to_relation +from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.column import ( ColumnConfig, column_descriptions_to_sqlmesh, @@ -50,6 +51,7 @@ from sqlmesh.dbt.target import BigQueryConfig, DuckDbConfig, SnowflakeConfig, ClickhouseConfig from sqlmesh.dbt.test import TestConfig from sqlmesh.utils.errors import ConfigError, MacroEvalError, SQLMeshError +from sqlmesh.utils.jinja import MacroReference pytestmark = [pytest.mark.dbt, pytest.mark.slow] @@ -1530,6 +1532,9 @@ def test_dbt_package_macros(sushi_test_project: Project): @pytest.mark.xdist_group("dbt_manifest") def test_dbt_vars(sushi_test_project: Project): context = sushi_test_project.context + context.set_and_render_variables( + sushi_test_project.packages["customers"].variables, "customers" + ) assert context.render("{{ var('some_other_var') }}") == "5" assert context.render("{{ var('some_other_var', 0) }}") == "5" @@ -1854,3 +1859,65 @@ def test_on_run_start_end(): "CREATE OR REPLACE TABLE schema_table_sushi__dev_nested_package AS SELECT 'sushi__dev' AS schema", ] ) + + +@pytest.mark.xdist_group("dbt_manifest") +def test_dynamic_var_names(sushi_test_project: Project, sushi_test_dbt_context: Context): + context = sushi_test_project.context + context.set_and_render_variables(sushi_test_project.packages["sushi"].variables, "sushi") + context.target = BigQueryConfig(name="production", database="main", schema="sushi") + model_config = ModelConfig( + name="model", + alias="model", + schema="test", + package_name="package", + materialized="table", + unique_key="ds", + partition_by={"field": "ds", "granularity": "month"}, + sql=""" + {% set var_name = "yet_" + "another_" + "var" %} + {% set results = run_query('select 1 as one') %} + {% if results %} + SELECT {{ results.columns[0].values()[0] }} AS one {{ var(var_name) }} AS var FROM {{ this.identifier }} + {% else %} + SELECT NULL AS one {{ var(var_name) }} AS var FROM {{ this.identifier }} + {% endif %} + """, + dependencies=Dependencies(has_dynamic_var_names=True), + ) + converted_model = model_config.to_sqlmesh(context) + assert "yet_another_var" in converted_model.jinja_macros.global_objs["vars"] # type: ignore + + # Test the existing model in the sushi project + assert ( + "dynamic_test_var" # type: ignore + in sushi_test_dbt_context.get_model( + "sushi.waiter_revenue_by_day_v2" + ).jinja_macros.global_objs["vars"] + ) + + +@pytest.mark.xdist_group("dbt_manifest") +def test_dynamic_var_names_in_macro(sushi_test_project: Project): + context = sushi_test_project.context + context.set_and_render_variables(sushi_test_project.packages["sushi"].variables, "sushi") + context.target = BigQueryConfig(name="production", database="main", schema="sushi") + model_config = ModelConfig( + name="model", + alias="model", + schema="test", + package_name="package", + materialized="table", + unique_key="ds", + partition_by={"field": "ds", "granularity": "month"}, + sql=""" + {% set var_name = "dynamic_" + "test_" + "var" %} + SELECT {{ sushi.dynamic_var_name_dependency(var_name) }} AS var + """, + dependencies=Dependencies( + macros=[MacroReference(package="sushi", name="dynamic_var_name_dependency")], + has_dynamic_var_names=True, + ), + ) + converted_model = model_config.to_sqlmesh(context) + assert "dynamic_test_var" in converted_model.jinja_macros.global_objs["vars"] # type: ignore diff --git a/tests/fixtures/dbt/sushi_test/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_project.yml index 073d85b4d4..c86057c928 100644 --- a/tests/fixtures/dbt/sushi_test/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/dbt_project.yml @@ -47,6 +47,7 @@ vars: customers:boo: ["a", "b"] yet_another_var: 1 + dynamic_test_var: 3 customers: some_var: ["foo", "bar"] diff --git a/tests/fixtures/dbt/sushi_test/macros/test_dependencies.sql b/tests/fixtures/dbt/sushi_test/macros/test_dependencies.sql index 931ce88a84..88518df380 100644 --- a/tests/fixtures/dbt/sushi_test/macros/test_dependencies.sql +++ b/tests/fixtures/dbt/sushi_test/macros/test_dependencies.sql @@ -6,3 +6,9 @@ {{ log(var("yet_another_var", 2)) }} {{ log(var("nested_vars")['some_nested_var']) }} {% endmacro %} + + +{% macro dynamic_var_name_dependency(var_name) %} + {% set results = run_query('select 1 as one') %} + {{ return(var(var_name)) }} +{% endmacro %} diff --git a/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql b/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql index 335e7ab799..317cc87e68 100644 --- a/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql +++ b/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql @@ -13,7 +13,8 @@ {{ test_dependencies() }} -{% set results = run_query('select 1 as constant') %} +{% set var_name = "dynamic_" + "test_" + "var" %} +{% set results = run_query('select ' ~ dynamic_var_name_dependency(var_name) ~ ' as constant') %} SELECT o.waiter_id::INT AS waiter_id, /* Waiter id */ From 4fc3ba6846691cc5088fa4409c024616a33257d7 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Mon, 25 Aug 2025 09:27:42 +1200 Subject: [PATCH 0750/1056] Feat(dbt_cli): Add `--select` and `--exclude` options (#5200) --- sqlmesh_dbt/cli.py | 25 ++++-- sqlmesh_dbt/console.py | 23 +++++- sqlmesh_dbt/operations.py | 56 ++++++++++---- sqlmesh_dbt/selectors.py | 130 ++++++++++++++++++++++++++++++++ tests/dbt/cli/test_list.py | 31 ++++++++ tests/dbt/cli/test_run.py | 25 ++++++ tests/dbt/cli/test_selectors.py | 78 +++++++++++++++++++ 7 files changed, 346 insertions(+), 22 deletions(-) create mode 100644 sqlmesh_dbt/selectors.py create mode 100644 tests/dbt/cli/test_selectors.py diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py index 500a9d6fa0..7d98e812b7 100644 --- a/sqlmesh_dbt/cli.py +++ b/sqlmesh_dbt/cli.py @@ -12,6 +12,18 @@ def _get_dbt_operations(ctx: click.Context) -> DbtOperations: return ctx.obj +select_option = click.option( + "-s", + "-m", + "--select", + "--models", + "--model", + multiple=True, + help="Specify the nodes to include.", +) +exclude_option = click.option("--exclude", multiple=True, help="Specify the nodes to exclude.") + + @click.group(invoke_without_command=True) @click.option("--profile", help="Which existing profile to load. Overrides output.profile") @click.option("-t", "--target", help="Which target to load for the given profile") @@ -38,23 +50,26 @@ def dbt( @dbt.command() -@click.option("-s", "-m", "--select", "--models", "--model", help="Specify the nodes to include.") +@select_option +@exclude_option @click.option( "-f", "--full-refresh", help="If specified, dbt will drop incremental models and fully-recalculate the incremental table from the model definition.", ) @click.pass_context -def run(ctx: click.Context, select: t.Optional[str], full_refresh: bool) -> None: +def run(ctx: click.Context, **kwargs: t.Any) -> None: """Compile SQL and execute against the current target database.""" - _get_dbt_operations(ctx).run(select=select, full_refresh=full_refresh) + _get_dbt_operations(ctx).run(**kwargs) @dbt.command(name="list") +@select_option +@exclude_option @click.pass_context -def list_(ctx: click.Context) -> None: +def list_(ctx: click.Context, **kwargs: t.Any) -> None: """List the resources in your project""" - _get_dbt_operations(ctx).list_() + _get_dbt_operations(ctx).list_(**kwargs) @dbt.command(name="ls", hidden=True) # hidden alias for list diff --git a/sqlmesh_dbt/console.py b/sqlmesh_dbt/console.py index 7d804ceb71..3c62adfe68 100644 --- a/sqlmesh_dbt/console.py +++ b/sqlmesh_dbt/console.py @@ -1,8 +1,27 @@ +import typing as t from sqlmesh.core.console import TerminalConsole +from sqlmesh.core.model import Model +from rich.tree import Tree class DbtCliConsole(TerminalConsole): - # TODO: build this out - def print(self, msg: str) -> None: return self._print(msg) + + def list_models( + self, models: t.List[Model], list_parents: bool = True, list_audits: bool = True + ) -> None: + model_list = Tree("[bold]Models in project:[/bold]") + + for model in models: + model_tree = model_list.add(model.name) + + if list_parents: + for parent in model.depends_on: + model_tree.add(f"depends_on: {parent}") + + if list_audits: + for audit_name in model.audit_definitions: + model_tree.add(f"audit: {audit_name}") + + self._print(model_list) diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index e8e443a64a..2b89c0f3e9 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -2,12 +2,17 @@ import typing as t from rich.progress import Progress from pathlib import Path +import logging +from sqlmesh_dbt import selectors if t.TYPE_CHECKING: # important to gate these to be able to defer importing sqlmesh until we need to from sqlmesh.core.context import Context from sqlmesh.dbt.project import Project from sqlmesh_dbt.console import DbtCliConsole + from sqlmesh.core.model import Model + +logger = logging.getLogger(__name__) class DbtOperations: @@ -15,22 +20,28 @@ def __init__(self, sqlmesh_context: Context, dbt_project: Project): self.context = sqlmesh_context self.project = dbt_project - def list_(self) -> None: - for _, model in self.context.models.items(): - self.console.print(model.name) - - def run(self, select: t.Optional[str] = None, full_refresh: bool = False) -> None: - # A dbt run both updates data and changes schemas and has no way of rolling back so more closely maps to a SQLMesh forward-only plan - # TODO: if --full-refresh specified, mark incrementals as breaking instead of forward_only? - - # TODO: we need to either convert DBT selector syntax to SQLMesh selector syntax - # or make the model selection engine configurable + def list_( + self, + select: t.Optional[t.List[str]] = None, + exclude: t.Optional[t.List[str]] = None, + ) -> None: + # dbt list prints: + # - models + # - "data tests" (audits) for those models + # it also applies selectors which is useful for testing selectors + selected_models = list(self._selected_models(select, exclude).values()) + self.console.list_models(selected_models) + + def run( + self, + select: t.Optional[t.List[str]] = None, + exclude: t.Optional[t.List[str]] = None, + full_refresh: bool = False, + ) -> None: select_models = None - if select: - if "," in select: - select_models = select.split(",") - else: - select_models = select.split(" ") + + if sqlmesh_selector := selectors.to_sqlmesh(select or [], exclude or []): + select_models = [sqlmesh_selector] self.context.plan( select_models=select_models, @@ -40,6 +51,21 @@ def run(self, select: t.Optional[str] = None, full_refresh: bool = False) -> Non auto_apply=True, ) + def _selected_models( + self, select: t.Optional[t.List[str]] = None, exclude: t.Optional[t.List[str]] = None + ) -> t.Dict[str, Model]: + if sqlmesh_selector := selectors.to_sqlmesh(select or [], exclude or []): + model_selector = self.context._new_selector() + selected_models = { + fqn: model + for fqn, model in self.context.models.items() + if fqn in model_selector.expand_model_selections([sqlmesh_selector]) + } + else: + selected_models = dict(self.context.models) + + return selected_models + @property def console(self) -> DbtCliConsole: console = self.context.console diff --git a/sqlmesh_dbt/selectors.py b/sqlmesh_dbt/selectors.py new file mode 100644 index 0000000000..16f5c2ea98 --- /dev/null +++ b/sqlmesh_dbt/selectors.py @@ -0,0 +1,130 @@ +import typing as t +import logging + +logger = logging.getLogger(__name__) + + +def to_sqlmesh(dbt_select: t.Collection[str], dbt_exclude: t.Collection[str]) -> t.Optional[str]: + """ + Given selectors defined in the format of the dbt cli --select and --exclude arguments, convert them into a selector expression that + the SQLMesh selector engine can understand. + + The main things being mapped are: + - set union (" " between items within the same selector string OR multiple --select arguments) is mapped to " | " + - set intersection ("," between items within the same selector string) is mapped to " & " + - `--exclude`. The SQLMesh selector engine does not treat this as a separate parameter and rather treats exclusion as a normal selector + that just happens to contain negation syntax, so we generate these by negating each expression and then intersecting the result + with any --select expressions + + Things that are *not* currently being mapped include: + - selectors based on file paths + - selectors based on partially qualified names like "model_a". The SQLMesh selector engine requires either: + - wildcards, eg "*model_a*" + - the full model name qualified with the schema, eg "staging.model_a" + + Examples: + --select "model_a" + -> "model_a" + --select "main.model_a" + -> "main.model_a" + --select "main.model_a" --select "main.model_b" + -> "main.model_a | main.model_b" + --select "main.model_a main.model_b" + -> "main.model_a | main.model_b" + --select "(main.model_a+ & ^main.model_b)" + -> "(main.model_a+ & ^main.model_b)" + --select "+main.model_a" --exclude "raw.src_data" + -> "+main.model_a & ^(raw.src_data)" + --select "+main.model_a" --select "main.*b+" --exclude "raw.src_data" + -> "(+main.model_a | main.*b+) & ^(raw.src_data)" + """ + if not dbt_select and not dbt_exclude: + return None + + select_expr = " | ".join(_to_sqlmesh(expr) for expr in dbt_select) + select_expr = _wrap(select_expr) if dbt_exclude and len(dbt_select) > 1 else select_expr + + exclude_expr = " | ".join(_to_sqlmesh(expr, negate=True) for expr in dbt_exclude) + exclude_expr = _wrap(exclude_expr) if dbt_select and len(dbt_exclude) > 1 else exclude_expr + + main_expr = " & ".join([expr for expr in [select_expr, exclude_expr] if expr]) + + logger.debug( + f"Expanded dbt select: {dbt_select}, exclude: {dbt_exclude} into SQLMesh: {main_expr}" + ) + + return main_expr + + +def _to_sqlmesh(selector_str: str, negate: bool = False) -> str: + unions, intersections = _split_unions_and_intersections(selector_str) + + if negate: + unions = [_negate(u) for u in unions] + intersections = [_negate(i) for i in intersections] + + union_expr = " | ".join(unions) + intersection_expr = " & ".join(intersections) + + if len(unions) > 1 and intersections: + union_expr = f"({union_expr})" + + if len(intersections) > 1 and unions: + intersection_expr = f"({intersection_expr})" + + return " | ".join([expr for expr in [union_expr, intersection_expr] if expr]) + + +def _split_unions_and_intersections(selector_str: str) -> t.Tuple[t.List[str], t.List[str]]: + # break space-separated items like: "my_first_model my_second_model" into a list of selectors to union + # and comma-separated items like: "my_first_model,my_second_model" into a list of selectors to intersect + # but, take into account brackets, eg "(my_first_model & my_second_model)" should not be split + + def _split_by(input: str, delimiter: str) -> t.Iterator[str]: + buf = "" + depth = 0 + + for char in input: + if char == delimiter and depth <= 0: + # only split on a space if we are not within parenthesis + yield buf + buf = "" + continue + elif char == "(": + depth += 1 + elif char == ")": + depth -= 1 + + buf += char + + if buf: + yield buf + + # first, break up based on spaces + segments = list(_split_by(selector_str, " ")) + + # then, within each segment, identify the unions and intersections + unions = [] + intersections = [] + + for segment in segments: + maybe_intersections = list(_split_by(segment, ",")) + if len(maybe_intersections) > 1: + intersections.extend(maybe_intersections) + else: + unions.append(segment) + + return unions, intersections + + +def _negate(expr: str) -> str: + return f"^{_wrap(expr)}" + + +def _wrap(expr: str) -> str: + already_wrapped = expr.strip().startswith("(") and expr.strip().endswith(")") + + if expr and not already_wrapped: + return f"({expr})" + + return expr diff --git a/tests/dbt/cli/test_list.py b/tests/dbt/cli/test_list.py index 9312be8635..fe3e1e6829 100644 --- a/tests/dbt/cli/test_list.py +++ b/tests/dbt/cli/test_list.py @@ -15,3 +15,34 @@ def test_list(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): assert "main.orders" in result.output assert "main.customers" in result.output assert "main.stg_payments" in result.output + assert "main.raw_orders" in result.output + + +def test_list_select(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): + result = invoke_cli(["list", "--select", "main.raw_customers+"]) + + assert result.exit_code == 0 + assert not result.exception + + assert "main.orders" in result.output + assert "main.customers" in result.output + assert "main.stg_customers" in result.output + assert "main.raw_customers" in result.output + + assert "main.stg_payments" not in result.output + assert "main.raw_orders" not in result.output + + +def test_list_select_exclude(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): + result = invoke_cli(["list", "--select", "main.raw_customers+", "--exclude", "main.orders"]) + + assert result.exit_code == 0 + assert not result.exception + + assert "main.customers" in result.output + assert "main.stg_customers" in result.output + assert "main.raw_customers" in result.output + + assert "main.orders" not in result.output + assert "main.stg_payments" not in result.output + assert "main.raw_orders" not in result.output diff --git a/tests/dbt/cli/test_run.py b/tests/dbt/cli/test_run.py index 0e4a04bcb1..4d80514fc8 100644 --- a/tests/dbt/cli/test_run.py +++ b/tests/dbt/cli/test_run.py @@ -2,6 +2,8 @@ import pytest from pathlib import Path from click.testing import Result +import time_machine +from tests.cli.test_cli import FREEZE_TIME pytestmark = pytest.mark.slow @@ -13,3 +15,26 @@ def test_run(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): assert not result.exception assert "Model batches executed" in result.output + + +def test_run_with_selectors(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): + with time_machine.travel(FREEZE_TIME): + # do an initial run to create the objects + # otherwise the selected subset may depend on something that hasnt been created + result = invoke_cli(["run"]) + assert result.exit_code == 0 + assert "main.orders" in result.output + + result = invoke_cli(["run", "--select", "main.raw_customers+", "--exclude", "main.orders"]) + + assert result.exit_code == 0 + assert not result.exception + + assert "main.stg_customers" in result.output + assert "main.stg_orders" in result.output + assert "main.stg_payments" in result.output + assert "main.customers" in result.output + + assert "main.orders" not in result.output + + assert "Model batches executed" in result.output diff --git a/tests/dbt/cli/test_selectors.py b/tests/dbt/cli/test_selectors.py new file mode 100644 index 0000000000..e494ed98a3 --- /dev/null +++ b/tests/dbt/cli/test_selectors.py @@ -0,0 +1,78 @@ +import typing as t +import pytest +from sqlmesh_dbt import selectors + + +@pytest.mark.parametrize( + "dbt_select,expected", + [ + ([], None), + (["main.model_a"], "main.model_a"), + (["main.model_a main.model_b"], "main.model_a | main.model_b"), + (["main.model_a", "main.model_b"], "main.model_a | main.model_b"), + (["(main.model_a & ^main.model_b)"], "(main.model_a & ^main.model_b)"), + ( + ["(+main.model_a & ^main.model_b)", "main.model_c"], + "(+main.model_a & ^main.model_b) | main.model_c", + ), + ], +) +def test_selection(dbt_select: t.List[str], expected: t.Optional[str]): + assert selectors.to_sqlmesh(dbt_select=dbt_select, dbt_exclude=[]) == expected + + +@pytest.mark.parametrize( + "dbt_exclude,expected", + [ + ([], None), + (["main.model_a"], "^(main.model_a)"), + (["(main.model_a & main.model_b)"], "^(main.model_a & main.model_b)"), + (["main.model_a +main.model_b"], "^(main.model_a) | ^(+main.model_b)"), + ( + ["(+main.model_a & ^main.model_b)", "main.model_c"], + "^(+main.model_a & ^main.model_b) | ^(main.model_c)", + ), + ], +) +def test_exclusion(dbt_exclude: t.List[str], expected: t.Optional[str]): + assert selectors.to_sqlmesh(dbt_select=[], dbt_exclude=dbt_exclude) == expected + + +@pytest.mark.parametrize( + "dbt_select,dbt_exclude,expected", + [ + ([], [], None), + (["+main.model_a"], ["raw.src_data"], "+main.model_a & ^(raw.src_data)"), + ( + ["+main.model_a", "main.*b+"], + ["raw.src_data"], + "(+main.model_a | main.*b+) & ^(raw.src_data)", + ), + ( + ["+main.model_a", "main.*b+"], + ["raw.src_data", "tag:disabled"], + "(+main.model_a | main.*b+) & (^(raw.src_data) | ^(tag:disabled))", + ), + ], +) +def test_selection_and_exclusion( + dbt_select: t.List[str], dbt_exclude: t.List[str], expected: t.Optional[str] +): + assert selectors.to_sqlmesh(dbt_select=dbt_select, dbt_exclude=dbt_exclude) == expected + + +@pytest.mark.parametrize( + "expression,expected", + [ + ("", ([], [])), + ("model_a", (["model_a"], [])), + ("model_a model_b", (["model_a", "model_b"], [])), + ("model_a,model_b", ([], ["model_a", "model_b"])), + ("model_a model_b,model_c", (["model_a"], ["model_b", "model_c"])), + ("model_a,model_b model_c", (["model_c"], ["model_a", "model_b"])), + ], +) +def test_split_unions_and_intersections( + expression: str, expected: t.Tuple[t.List[str], t.List[str]] +): + assert selectors._split_unions_and_intersections(expression) == expected From 9f0ba659add38b288941ae80b089aed2576cc518 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:48:16 +0300 Subject: [PATCH 0751/1056] Chore: fix dbt `references` manifest test (#5220) --- tests/dbt/test_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index c2becfbc16..030f2ec723 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -79,10 +79,10 @@ def test_load_invalid_ref_audit_constraints(tmp_path: Path, caplog) -> None: ], "tests": [ { + # References a model that doesn't exist "relationships": { "to": "ref('not_real_model')", "field": "cola", - "description": "A test that references a model that does not exist", } } ], From 0b819d2e13f3e9ae718b2d965e37b0f474fdbf5e Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 25 Aug 2025 08:47:23 -0700 Subject: [PATCH 0752/1056] Fix: Cleanup of materialized view snapshots (#5213) --- sqlmesh/core/snapshot/evaluator.py | 9 ++++- tests/core/test_snapshot_evaluator.py | 58 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 45dd7d3f3a..6baa440ba6 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -2378,14 +2378,19 @@ def migrate( def delete(self, name: str, **kwargs: t.Any) -> None: cascade = kwargs.pop("cascade", False) try: - self.adapter.drop_view(name, cascade=cascade) + # Some engines (e.g., RisingWave) don’t fail when dropping a materialized view with a DROP VIEW statement, + # because views and materialized views don’t share the same namespace. Therefore, we should not ignore if the + # view doesn't exist and let the exception handler attempt to drop the materialized view. + self.adapter.drop_view(name, cascade=cascade, ignore_if_not_exists=False) except Exception: logger.debug( "Failed to drop view '%s'. Trying to drop the materialized view instead", name, exc_info=True, ) - self.adapter.drop_view(name, materialized=True, cascade=cascade) + self.adapter.drop_view( + name, materialized=True, cascade=cascade, ignore_if_not_exists=True + ) logger.info("Dropped view '%s'", name) def _is_materialized_view(self, model: Model) -> bool: diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 4ff2c3893a..53f9bd425a 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -470,6 +470,64 @@ def create_and_cleanup(name: str, dev_table_only: bool): ) +def test_cleanup_view(adapter_mock, make_snapshot): + evaluator = SnapshotEvaluator(adapter_mock) + + model = SqlModel( + name="catalog.test_schema.test_model", + kind=ViewKind(materialized=False), + query=parse_one("SELECT a FROM tbl"), + ) + + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) + + evaluator.promote([snapshot], EnvironmentNamingInfo(name="test_env")) + evaluator.cleanup([SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True)]) + + adapter_mock.get_data_object.assert_not_called() + adapter_mock.drop_view.assert_called_once_with( + f"catalog.sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev", + cascade=True, + ignore_if_not_exists=False, + ) + + +def test_cleanup_materialized_view(adapter_mock, make_snapshot): + evaluator = SnapshotEvaluator(adapter_mock) + + model = SqlModel( + name="catalog.test_schema.test_model", + kind=ViewKind(materialized=True), + query=parse_one("SELECT a FROM tbl"), + ) + + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) + + adapter_mock.drop_view.side_effect = [RuntimeError("failed to drop view"), None] + + evaluator.promote([snapshot], EnvironmentNamingInfo(name="test_env")) + evaluator.cleanup([SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True)]) + + adapter_mock.get_data_object.assert_not_called() + adapter_mock.drop_view.assert_has_calls( + [ + call( + f"catalog.sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev", + cascade=True, + ignore_if_not_exists=False, + ), + call( + f"catalog.sqlmesh__test_schema.test_schema__test_model__{snapshot.fingerprint.to_version()}__dev", + materialized=True, + cascade=True, + ignore_if_not_exists=True, + ), + ] + ) + + def test_cleanup_fails(adapter_mock, make_snapshot): adapter_mock.drop_table.side_effect = RuntimeError("test_error") From 46f20d7a68355851ff0ab5564cf499e66100c02d Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 25 Aug 2025 09:22:53 -0700 Subject: [PATCH 0753/1056] Fix: Check if the target table exists when determining the value of the is_incremental flag (#5215) --- sqlmesh/core/snapshot/evaluator.py | 14 +++++++-- sqlmesh/dbt/builtin.py | 6 ++++ sqlmesh/dbt/seed.py | 1 + tests/core/test_integration.py | 29 +++++++++++++++++++ tests/dbt/test_config.py | 9 +++--- tests/dbt/test_manifest.py | 16 +++++----- tests/dbt/test_transformation.py | 23 +++++++++++++++ tests/fixtures/dbt/sushi_test/config.py | 9 +++--- tests/fixtures/dbt/sushi_test/dbt_project.yml | 1 - .../models/waiter_revenue_by_day.sql | 6 +--- 10 files changed, 89 insertions(+), 25 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 6baa440ba6..82924e4c3a 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -773,7 +773,8 @@ def _evaluate_snapshot( allow_destructive_snapshots=allow_destructive_snapshots, allow_additive_snapshots=allow_additive_snapshots, ) - common_render_kwargs["runtime_stage"] = RuntimeStage.EVALUATING + runtime_stage = RuntimeStage.EVALUATING + target_table_exists = True elif model.annotated or model.is_seed or model.kind.is_scd_type_2: self._execute_create( snapshot=snapshot, @@ -785,7 +786,14 @@ def _evaluate_snapshot( dry_run=False, run_pre_post_statements=False, ) - common_render_kwargs["runtime_stage"] = RuntimeStage.EVALUATING + runtime_stage = RuntimeStage.EVALUATING + target_table_exists = True + + evaluate_render_kwargs = { + **common_render_kwargs, + "runtime_stage": runtime_stage, + "snapshot_table_exists": target_table_exists, + } wap_id: t.Optional[str] = None if snapshot.is_materialized and ( @@ -801,7 +809,7 @@ def _evaluate_snapshot( execution_time=execution_time, snapshot=snapshot, snapshots=snapshots, - render_kwargs=common_render_kwargs, + render_kwargs=evaluate_render_kwargs, create_render_kwargs=create_render_kwargs, rendered_physical_properties=rendered_physical_properties, deployability_index=deployability_index, diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 70e1b10099..4b564eb781 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -407,6 +407,12 @@ def create_builtin_globals( else snapshot.dev_intervals ) is_incremental = bool(intervals) + + snapshot_table_exists = jinja_globals.get("snapshot_table_exists") + if is_incremental and snapshot_table_exists is not None: + # If we know the information about table existence, we can use it to correctly + # set the flag + is_incremental &= snapshot_table_exists else: is_incremental = False builtin_globals["is_incremental"] = lambda: is_incremental diff --git a/sqlmesh/dbt/seed.py b/sqlmesh/dbt/seed.py index 10e98cf93c..882c240289 100644 --- a/sqlmesh/dbt/seed.py +++ b/sqlmesh/dbt/seed.py @@ -86,6 +86,7 @@ def to_sqlmesh( dialect=self.dialect(context), audit_definitions=audit_definitions, virtual_environment_mode=virtual_environment_mode, + start=self.start or context.sqlmesh_config.model_defaults.start, **kwargs, ) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index dec7309591..f80c42f579 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -2037,6 +2037,35 @@ def test_dbt_select_star_is_directly_modified(sushi_test_dbt_context: Context): assert plan.snapshots[snapshot_b_id].change_category == SnapshotChangeCategory.NON_BREAKING +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_dbt_is_incremental_table_is_missing(sushi_test_dbt_context: Context): + context = sushi_test_dbt_context + + model = context.get_model("sushi.waiter_revenue_by_day_v2") + model = model.copy(update={"kind": IncrementalUnmanagedKind(), "start": "2023-01-01"}) + context.upsert_model(model) + + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + + snapshot = context.get_snapshot("sushi.waiter_revenue_by_day_v2") + assert snapshot + + # Manually drop the table + context.engine_adapter.drop_table(snapshot.table_name()) + + context.snapshot_evaluator.evaluate( + snapshot, + start="2023-01-01", + end="2023-01-08", + execution_time="2023-01-08 15:00:00", + snapshots={s.name: s for s in context.snapshots.values()}, + deployability_index=DeployabilityIndex.all_deployable(), + ) + + # Make sure the table was recreated + assert context.engine_adapter.table_exists(snapshot.table_name()) + + def test_model_attr(sushi_test_dbt_context: Context, assert_exp_eq): context = sushi_test_dbt_context model = context.get_model("sushi.top_waiters") diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 99426ebb97..695c745c1d 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -354,7 +354,6 @@ def test_variables(assert_exp_eq, sushi_test_project): # Finally, check that variable scoping & overwriting (some_var) works as expected expected_sushi_variables = { - "start": "Jan 1 2022", "yet_another_var": 1, "top_waiters:limit": 10, "top_waiters:revenue": "revenue", @@ -379,7 +378,6 @@ def test_variables(assert_exp_eq, sushi_test_project): "yet_another_var": 5, "customers:bla": False, "customers:customer_id": "customer_id", - "start": "Jan 1 2022", } assert sushi_test_project.packages["sushi"].variables == expected_sushi_variables @@ -1006,8 +1004,11 @@ def test_db_type_to_quote_policy(): def test_variable_override(): project_root = "tests/fixtures/dbt/sushi_test" project = Project.load( - DbtContext(project_root=Path(project_root)), - variables={"yet_another_var": 2, "start": "2021-01-01"}, + DbtContext( + project_root=Path(project_root), + sqlmesh_config=Config(model_defaults=ModelDefaultsConfig(start="2021-01-01")), + ), + variables={"yet_another_var": 2}, ) assert project.packages["sushi"].variables["yet_another_var"] == 2 diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index 2bed6acb55..efbd2687fd 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -4,6 +4,7 @@ import pytest +from sqlmesh.core.config import ModelDefaultsConfig from sqlmesh.dbt.basemodel import Dependencies from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.manifest import ManifestHelper @@ -24,7 +25,7 @@ def test_manifest_helper(caplog): project_path, "sushi", profile.target, - variable_overrides={"start": "2020-01-01"}, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), ) models = helper.models() @@ -135,7 +136,7 @@ def test_tests_referencing_disabled_models(): project_path, "sushi", profile.target, - variable_overrides={"start": "2020-01-01"}, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), ) assert "disabled_model" not in helper.models() @@ -151,7 +152,7 @@ def test_call_cache(): project_path, "sushi", profile.target, - variable_overrides={"start": "2020-01-01"}, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), ) unused = "0000" @@ -172,7 +173,7 @@ def test_variable_override(): project_path, "sushi", profile.target, - variable_overrides={"start": "2020-01-01"}, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), ) assert helper.models()["top_waiters"].limit_value == 10 @@ -181,7 +182,8 @@ def test_variable_override(): project_path, "sushi", profile.target, - variable_overrides={"top_waiters:limit": 1, "start": "2020-01-01"}, + variable_overrides={"top_waiters:limit": 1}, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), ) assert helper.models()["top_waiters"].limit_value == 1 @@ -196,7 +198,7 @@ def test_source_meta_external_location(): project_path, "sushi", profile.target, - variable_overrides={"start": "2020-01-01"}, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), ) sources = helper.sources() @@ -229,7 +231,7 @@ def test_top_level_dbt_adapter_macros(): project_path, "sushi", profile.target, - variable_overrides={"start": "2020-01-01"}, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), ) # Adapter macros must be marked as top-level diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 1bcc3081f7..baef96eb6d 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1346,6 +1346,29 @@ def test_is_incremental(sushi_test_project: Project, assert_exp_eq, mocker): 'SELECT 1 AS "one" FROM "tbl_a" AS "tbl_a" WHERE "ds" > (SELECT MAX("ds") FROM "model" AS "model")', ) + # If the snapshot_table_exists flag was set to False, intervals should be ignored + assert_exp_eq( + model_config.to_sqlmesh(context) + .render_query_or_raise(snapshot=snapshot, snapshot_table_exists=False) + .sql(), + 'SELECT 1 AS "one" FROM "tbl_a" AS "tbl_a"', + ) + + # If the snapshot_table_exists flag was set to True, intervals should be taken into account + assert_exp_eq( + model_config.to_sqlmesh(context) + .render_query_or_raise(snapshot=snapshot, snapshot_table_exists=True) + .sql(), + 'SELECT 1 AS "one" FROM "tbl_a" AS "tbl_a" WHERE "ds" > (SELECT MAX("ds") FROM "model" AS "model")', + ) + snapshot.intervals = [] + assert_exp_eq( + model_config.to_sqlmesh(context) + .render_query_or_raise(snaspshot=snapshot, snapshot_table_exists=True) + .sql(), + 'SELECT 1 AS "one" FROM "tbl_a" AS "tbl_a"', + ) + @pytest.mark.xdist_group("dbt_manifest") def test_is_incremental_non_incremental_model(sushi_test_project: Project, assert_exp_eq, mocker): diff --git a/tests/fixtures/dbt/sushi_test/config.py b/tests/fixtures/dbt/sushi_test/config.py index d82291f793..83118b02cf 100644 --- a/tests/fixtures/dbt/sushi_test/config.py +++ b/tests/fixtures/dbt/sushi_test/config.py @@ -3,11 +3,9 @@ from sqlmesh.core.config import ModelDefaultsConfig from sqlmesh.dbt.loader import sqlmesh_config -variables = {"start": "Jan 1 2022"} - config = sqlmesh_config( - Path(__file__).parent, variables=variables, model_defaults=ModelDefaultsConfig(dialect="duckdb") + Path(__file__).parent, model_defaults=ModelDefaultsConfig(dialect="duckdb", start="Jan 1 2022") ) @@ -15,6 +13,7 @@ test_config_with_normalization_strategy = sqlmesh_config( Path(__file__).parent, - variables=variables, - model_defaults=ModelDefaultsConfig(dialect="duckdb,normalization_strategy=LOWERCASE"), + model_defaults=ModelDefaultsConfig( + dialect="duckdb,normalization_strategy=LOWERCASE", start="Jan 1 2022" + ), ) diff --git a/tests/fixtures/dbt/sushi_test/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_project.yml index c86057c928..ecd060159b 100644 --- a/tests/fixtures/dbt/sushi_test/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/dbt_project.yml @@ -20,7 +20,6 @@ clean-targets: # directories to be removed by `dbt clean` # Full documentation: https://docs.getdbt.com/docs/configuring-models models: - +start: "{{ var('start') }}" sushi: +materialized: table +pre-hook: diff --git a/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql b/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql index 317cc87e68..5eeb0002e0 100644 --- a/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql +++ b/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql @@ -30,11 +30,7 @@ LEFT JOIN {{ source('streaming', 'items') }} AS i ON oi.item_id = i.id AND oi.ds = i.ds {% if is_incremental() %} WHERE - o.ds > (select max(ds) from {{ this }}) -{% endif %} -{% if sqlmesh_incremental is defined %} - WHERE - o.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' + o.ds > (select CAST(max(ds) AS DATE) from {{ this }}) {% endif %} GROUP BY o.waiter_id, From 3507a192385764ad8ce596b1316cc0689ebbfaec Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 25 Aug 2025 09:31:39 -0700 Subject: [PATCH 0754/1056] Fix: Small fixes for seed models (#5217) --- sqlmesh/core/model/definition.py | 19 ++++++++++- sqlmesh/dbt/seed.py | 6 +++- tests/core/test_model.py | 55 ++++++++++++++++++++++++++++++++ tests/dbt/test_transformation.py | 27 ++++++++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index caec1c86fe..8e71b3aa02 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -1586,6 +1586,7 @@ def render_seed(self) -> t.Iterator[QueryOrDF]: string_columns = [] columns_to_types = self.columns_to_types_ or {} + column_names_to_check = set(columns_to_types) for name, tpe in columns_to_types.items(): if tpe.this in (exp.DataType.Type.DATE, exp.DataType.Type.DATE32): date_columns.append(name) @@ -1605,6 +1606,14 @@ def render_seed(self) -> t.Iterator[QueryOrDF]: rename_dict[normalized_name] = column if rename_dict: df.rename(columns=rename_dict, inplace=True) + # These names have already been checked + column_names_to_check -= set(rename_dict) + + missing_columns = column_names_to_check - set(df.columns) + if missing_columns: + raise_config_error( + f"Seed model '{self.name}' has missing columns: {missing_columns}", self._path + ) # convert all date/time types to native pandas timestamp for column in [*date_columns, *datetime_columns]: @@ -1614,7 +1623,15 @@ def render_seed(self) -> t.Iterator[QueryOrDF]: # extract datetime.date from pandas timestamp for DATE columns for column in date_columns: - df[column] = df[column].dt.date + try: + df[column] = df[column].dt.date + except Exception as ex: + logger.error( + "Failed to convert column '%s' to date in seed model '%s': %s", + column, + self.name, + ex, + ) for column in bool_columns: df[column] = df[column].apply(lambda i: str_to_bool(str(i))) diff --git a/sqlmesh/dbt/seed.py b/sqlmesh/dbt/seed.py index 882c240289..a84e39e653 100644 --- a/sqlmesh/dbt/seed.py +++ b/sqlmesh/dbt/seed.py @@ -17,6 +17,7 @@ from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.model import Model, SeedKind, create_seed_model +from sqlmesh.core.model.seed import CsvSettings from sqlmesh.dbt.basemodel import BaseModelConfig from sqlmesh.dbt.column import ColumnConfig @@ -80,9 +81,12 @@ def to_sqlmesh( kwargs["columns"] = new_columns + # dbt treats single whitespace as a null value + csv_settings = CsvSettings(na_values=[" "], keep_default_na=True) + return create_seed_model( self.canonical_name(context), - SeedKind(path=seed_path), + SeedKind(path=seed_path, csv_settings=csv_settings), dialect=self.dialect(context), audit_definitions=audit_definitions, virtual_environment_mode=virtual_environment_mode, diff --git a/tests/core/test_model.py b/tests/core/test_model.py index cffcc52a4e..eecc3977e7 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9997,6 +9997,61 @@ def test_seed_coerce_datetime(tmp_path): assert df["bad_datetime"].iloc[0] == "9999-12-31 23:59:59" +def test_seed_invalid_date_column(tmp_path): + model_csv_path = (tmp_path / "model.csv").absolute() + + with open(model_csv_path, "w", encoding="utf-8") as fd: + fd.write("bad_date\n9999-12-31\n2025-01-01\n1000-01-01") + + expressions = d.parse( + f""" + MODEL ( + name db.seed, + kind SEED ( + path '{str(model_csv_path)}', + ), + columns ( + bad_date date, + ), + ); + """ + ) + + model = load_sql_based_model(expressions, path=Path("./examples/sushi/models/test_model.sql")) + df = next(model.render(context=None)) + # The conversion to date should not raise an error + assert df["bad_date"].to_list() == ["9999-12-31", "2025-01-01", "1000-01-01"] + + +def test_seed_missing_columns(tmp_path): + model_csv_path = (tmp_path / "model.csv").absolute() + + with open(model_csv_path, "w", encoding="utf-8") as fd: + fd.write("key,value\n1,2\n3,4") + + expressions = d.parse( + f""" + MODEL ( + name db.seed, + kind SEED ( + path '{str(model_csv_path)}', + ), + columns ( + key int, + value int, + missing_column int, + ), + ); + """ + ) + + model = load_sql_based_model(expressions, path=Path("./examples/sushi/models/test_model.sql")) + with pytest.raises( + ConfigError, match="Seed model 'db.seed' has missing columns: {'missing_column'}.*" + ): + next(model.render(context=None)) + + def test_missing_column_data_in_columns_key(): expressions = d.parse( """ diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index baef96eb6d..33c7132551 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -689,6 +689,33 @@ def test_seed_column_inference(tmp_path): } +def test_seed_single_whitespace_is_na(tmp_path): + seed_csv = tmp_path / "seed.csv" + with open(seed_csv, "w", encoding="utf-8") as fd: + fd.write("col_a, col_b\n") + fd.write(" ,1\n") + fd.write("2, \n") + + seed = SeedConfig( + name="test_model", + package="foo", + path=Path(seed_csv), + ) + + context = DbtContext() + context.project_name = "foo" + context.target = DuckDbConfig(name="target", schema="test") + sqlmesh_seed = seed.to_sqlmesh(context) + assert sqlmesh_seed.columns_to_types == { + "col_a": exp.DataType.build("int"), + "col_b": exp.DataType.build("int"), + } + + df = next(sqlmesh_seed.render_seed()) + assert df["col_a"].to_list() == [None, 2] + assert df["col_b"].to_list() == [1, None] + + def test_seed_partial_column_inference(tmp_path): seed_csv = tmp_path / "seed.csv" with open(seed_csv, "w", encoding="utf-8") as fd: From 4dfe8aea4b8fcc6f5f70dd8389ae6f6d00f980f5 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 25 Aug 2025 13:10:02 -0700 Subject: [PATCH 0755/1056] chore(web_common):fix version mismatch (#5222) --- pnpm-lock.yaml | 178 +++++++++++++++++++++++++++++++++++++++- web/common/.syncpackrc | 29 +++++++ web/common/package.json | 98 +++++++++++----------- 3 files changed, 255 insertions(+), 50 deletions(-) create mode 100644 web/common/.syncpackrc diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 126a3f7d3c..19c0a71fe1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -430,7 +430,7 @@ importers: specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@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) '@types/node': - specifier: 20.11.25 + specifier: ^20.11.25 version: 20.11.25 '@types/react': specifier: ^18.3.23 @@ -442,7 +442,7 @@ importers: 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.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/browser': - specifier: 3.2.4 + 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.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) autoprefixer: specifier: ^10.4.21 @@ -480,6 +480,9 @@ importers: storybook: specifier: ^9.1.2 version: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + syncpack: + specifier: ^13.0.4 + version: 13.0.4(typescript@5.8.3) tailwind-merge: specifier: ^3.3.1 version: 3.3.1 @@ -1859,6 +1862,9 @@ packages: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@stoplight/better-ajv-errors@1.0.3': resolution: {integrity: sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==} engines: {node: ^12.20 || >= 14.13} @@ -3296,6 +3302,10 @@ packages: resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} engines: {node: '>=12'} + chalk-template@1.1.0: + resolution: {integrity: sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg==} + engines: {node: '>=14.16'} + chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} engines: {node: '>=8'} @@ -3425,6 +3435,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3457,6 +3471,15 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -3681,6 +3704,9 @@ packages: resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==} engines: {node: '>=4'} + effect@3.17.9: + resolution: {integrity: sha512-Nkkn9n1zhy30Dq0MpQatDCH7nfYnOIiebkOHNxmmvoVnEDKCto+2ZwDDWFGzcN/ojwfqjRXWGC9Lo91K5kwZCg==} + electron-to-chromium@1.5.190: resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==} @@ -3725,10 +3751,17 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} @@ -3884,6 +3917,10 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -4161,6 +4198,10 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} + hosted-git-info@8.1.0: + resolution: {integrity: sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==} + engines: {node: ^18.17.0 || >=20.5.0} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -4257,6 +4298,9 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -5056,6 +5100,10 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + npm-package-arg@12.0.2: + resolution: {integrity: sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==} + engines: {node: ^18.17.0 || >=20.5.0} + npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -5179,6 +5227,10 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + parse-json@8.3.0: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} @@ -5356,6 +5408,10 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + proc-log@5.0.0: + resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} + engines: {node: ^18.17.0 || >=20.5.0} + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -5380,6 +5436,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -5510,6 +5569,10 @@ packages: resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} engines: {node: '>=18'} + read-yaml-file@2.1.0: + resolution: {integrity: sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==} + engines: {node: '>=10.13'} + read@1.0.7: resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==} engines: {node: '>=0.8'} @@ -5897,6 +5960,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -5964,6 +6031,11 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + syncpack@13.0.4: + resolution: {integrity: sha512-kJ9VlRxNCsBD5pJAE29oXeBYbPLhEySQmK4HdpsLv81I6fcDDW17xeJqMwiU3H7/woAVsbgq25DJNS8BeiN5+w==} + engines: {node: '>=18.18.0'} + hasBin: true + tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -6055,6 +6127,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tightrope@0.2.0: + resolution: {integrity: sha512-Kw36UHxJEELq2VUqdaSGR2/8cAsPgMtvX8uGVU6Jk26O66PhXec0A5ZnRYs47btbtwPDpXXF66+Fo3vimCM9aQ==} + engines: {node: '>=16'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -6139,6 +6215,9 @@ packages: typescript: '*' webpack: ^5.0.0 + ts-toolbelt@9.6.0: + resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} + tsconfck@2.1.2: resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} engines: {node: ^14.13.1 || ^16 || >=18} @@ -6354,6 +6433,10 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + validate-npm-package-name@6.0.2: + resolution: {integrity: sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==} + engines: {node: ^18.17.0 || >=20.5.0} + validator@13.15.15: resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} engines: {node: '>= 0.10'} @@ -8282,6 +8365,8 @@ snapshots: '@sindresorhus/merge-streams@2.3.0': {} + '@standard-schema/spec@1.0.0': {} + '@stoplight/better-ajv-errors@1.0.3(ajv@8.17.1)': dependencies: ajv: 8.17.1 @@ -10103,6 +10188,10 @@ snapshots: dependencies: chalk: 4.1.2 + chalk-template@1.1.0: + dependencies: + chalk: 5.4.1 + chalk@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -10239,6 +10328,8 @@ snapshots: commander@12.1.0: {} + commander@13.1.0: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -10259,6 +10350,15 @@ snapshots: core-util-is@1.0.3: {} + cosmiconfig@9.0.0(typescript@5.8.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.8.3 + crelt@1.0.6: {} cross-spawn@7.0.6: @@ -10472,6 +10572,11 @@ snapshots: dependencies: version-range: 4.14.0 + effect@3.17.9: + dependencies: + '@standard-schema/spec': 1.0.0 + fast-check: 3.23.2 + electron-to-chromium@1.5.190: {} electron-to-chromium@1.5.204: {} @@ -10513,8 +10618,14 @@ snapshots: entities@6.0.1: {} + env-paths@2.2.1: {} + environment@1.1.0: {} + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 @@ -10772,6 +10883,10 @@ snapshots: extend@3.0.2: {} + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -11072,6 +11187,10 @@ snapshots: dependencies: lru-cache: 10.4.3 + hosted-git-info@8.1.0: + dependencies: + lru-cache: 10.4.3 + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -11164,6 +11283,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -12055,6 +12176,13 @@ snapshots: normalize-range@0.1.2: {} + npm-package-arg@12.0.2: + dependencies: + hosted-git-info: 8.1.0 + proc-log: 5.0.0 + semver: 7.7.2 + validate-npm-package-name: 6.0.2 + npm-run-path@4.0.1: dependencies: path-key: 3.1.1 @@ -12244,6 +12372,13 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + parse-json@8.3.0: dependencies: '@babel/code-frame': 7.27.1 @@ -12405,6 +12540,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + proc-log@5.0.0: {} + process-nextick-args@2.0.1: {} prompts@2.4.2: @@ -12430,6 +12567,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@6.1.0: {} + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -12595,6 +12734,11 @@ snapshots: type-fest: 4.41.0 unicorn-magic: 0.1.0 + read-yaml-file@2.1.0: + dependencies: + js-yaml: 4.1.0 + strip-bom: 4.0.0 + read@1.0.7: dependencies: mute-stream: 0.0.8 @@ -13095,6 +13239,8 @@ snapshots: strip-bom@3.0.0: {} + strip-bom@4.0.0: {} + strip-final-newline@2.0.0: {} strip-indent@3.0.0: @@ -13173,6 +13319,28 @@ snapshots: symbol-tree@3.2.4: {} + syncpack@13.0.4(typescript@5.8.3): + dependencies: + chalk: 5.4.1 + chalk-template: 1.1.0 + commander: 13.1.0 + cosmiconfig: 9.0.0(typescript@5.8.3) + effect: 3.17.9 + enquirer: 2.4.1 + fast-check: 3.23.2 + globby: 14.1.0 + jsonc-parser: 3.3.1 + minimatch: 9.0.5 + npm-package-arg: 12.0.2 + ora: 8.2.0 + prompts: 2.4.2 + read-yaml-file: 2.1.0 + semver: 7.7.2 + tightrope: 0.2.0 + ts-toolbelt: 9.6.0 + transitivePeerDependencies: + - typescript + tabbable@6.2.0: {} table-layout@4.1.1: @@ -13302,6 +13470,8 @@ snapshots: dependencies: any-promise: 1.3.0 + tightrope@0.2.0: {} + tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} @@ -13367,6 +13537,8 @@ snapshots: typescript: 5.8.3 webpack: 5.99.8(esbuild@0.25.8) + ts-toolbelt@9.6.0: {} + tsconfck@2.1.2(typescript@5.8.3): optionalDependencies: typescript: 5.8.3 @@ -13598,6 +13770,8 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + validate-npm-package-name@6.0.2: {} + validator@13.15.15: {} version-range@4.14.0: {} diff --git a/web/common/.syncpackrc b/web/common/.syncpackrc new file mode 100644 index 0000000000..52d97009ce --- /dev/null +++ b/web/common/.syncpackrc @@ -0,0 +1,29 @@ +{ + "versionGroups": [ + { + "label": "Sync peer deps with dev deps", + "dependencies": [ + "**" + ], + "dependencyTypes": [ + "dev", + "peer" + ], + "policy": "same" + } + ], + "semverGroups": [ + { + "label": "Use caret ranges for all dependencies", + "dependencies": [ + "**" + ], + "dependencyTypes": [ + "dev", + "peer", + "prod" + ], + "range": "^" + } + ] +} \ No newline at end of file diff --git a/web/common/package.json b/web/common/package.json index 4cccbec171..028ce6489d 100644 --- a/web/common/package.json +++ b/web/common/package.json @@ -1,45 +1,6 @@ { "name": "@tobikodata/sqlmesh-common", "version": "0.0.1", - "private": false, - "type": "module", - "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/TobikoData/sqlmesh" - }, - "files": [ - "/dist" - ], - "main": "dist/sqlmesh-common.umd.js", - "module": "dist/sqlmesh-common.es.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/sqlmesh-common.es.js" - }, - "require": { - "types": "./dist/index.d.ts", - "default": "./dist/sqlmesh-common.umd.js" - } - }, - "./design": "./dist/styles/design/index.css", - "./design/*": "./dist/styles/design/*" - }, - "scripts": { - "ci": "pnpm run lint && pnpm run build", - "build": "tsc -p tsconfig.build.json && vite build --base './'", - "build:storybook": "storybook build", - "dev": "storybook dev -p 6006", - "lint": "eslint --max-warnings 0 --fix src", - "test": "vitest", - "test:watch": "vitest watch", - "test:ui": "vitest --ui", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" - }, "devDependencies": { "@eslint/js": "^9.31.0", "@radix-ui/react-slot": "^1.2.3", @@ -51,11 +12,11 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@types/node": "20.11.25", + "@types/node": "^20.11.25", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.7.0", - "@vitest/browser": "3.2.4", + "@vitest/browser": "^3.2.4", "autoprefixer": "^10.4.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -68,6 +29,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "storybook": "^9.1.2", + "syncpack": "^13.0.4", "tailwind-merge": "^3.3.1", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", @@ -77,14 +39,54 @@ "vite-plugin-static-copy": "^3.1.1", "vitest": "^3.2.4" }, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/sqlmesh-common.es.js" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/sqlmesh-common.umd.js" + } + }, + "./design": "./dist/styles/design/index.css", + "./design/*": "./dist/styles/design/*" + }, + "files": [ + "/dist" + ], + "license": "Apache-2.0", + "main": "dist/sqlmesh-common.umd.js", + "module": "dist/sqlmesh-common.es.js", "peerDependencies": { - "@radix-ui/react-slot": "^1.0.0", - "@tailwindcss/typography": "^0.5.0", - "class-variance-authority": "^0.7.0", + "@radix-ui/react-slot": "^1.2.3", + "@tailwindcss/typography": "^0.5.16", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "react": "^18.3.1 || ^19.0.0", - "react-dom": "^18.3.1 || ^19.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "tailwind-merge": "^3.3.1", - "tailwindcss": "^3.4.0" - } + "tailwindcss": "^3.4.17" + }, + "private": false, + "repository": "TobikoData/sqlmesh", + "scripts": { + "build": "tsc -p tsconfig.build.json && vite build --base './' && pnpm run build:css", + "build-storybook": "storybook build", + "build:css": "tailwindcss -i ./src/styles/index.css -o ./dist/styles/tailwind.min.css --minify", + "build:storybook": "storybook build", + "ci": "pnpm run syncpack && pnpm run lint && pnpm run build", + "dev": "storybook dev -p 6006", + "lint": "eslint --max-warnings 0 --fix src", + "storybook": "storybook dev -p 6006", + "syncpack": "syncpack lint", + "syncpack:fix": "syncpack fix-mismatches", + "syncpack:list": "syncpack list-mismatches", + "test": "vitest", + "test:ui": "vitest --ui", + "test:watch": "vitest watch" + }, + "type": "module", + "types": "dist/index.d.ts" } From ae4cd032bf644b54945e9f69b1d0a1b9d19ff797 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 26 Aug 2025 08:54:21 +1200 Subject: [PATCH 0756/1056] Fix(tests): Address some test flakiness (#5209) --- .circleci/manage-test-db.sh | 2 +- Makefile | 2 +- pyproject.toml | 3 +- sqlmesh/core/config/loader.py | 9 +- sqlmesh/core/engine_adapter/fabric.py | 50 ++++++++- tests/conftest.py | 33 ++++++ .../engine_adapter/integration/__init__.py | 3 + .../engine_adapter/integration/config.yaml | 2 +- .../engine_adapter/integration/conftest.py | 11 +- .../integration/test_integration.py | 100 ++++++------------ .../integration/test_integration_fabric.py | 41 +++++++ 11 files changed, 174 insertions(+), 82 deletions(-) create mode 100644 tests/core/engine_adapter/integration/test_integration_fabric.py diff --git a/.circleci/manage-test-db.sh b/.circleci/manage-test-db.sh index f44bd54845..ba1d1070fb 100755 --- a/.circleci/manage-test-db.sh +++ b/.circleci/manage-test-db.sh @@ -51,7 +51,7 @@ databricks_init() { # Note: the cluster doesnt need to be running to create / drop catalogs, but it does need to be running to run the integration tests echo "Ensuring cluster is running" - databricks clusters start $CLUSTER_ID || true + databricks clusters start $CLUSTER_ID } databricks_up() { diff --git a/Makefile b/Makefile index 3fea757169..bad2cf2907 100644 --- a/Makefile +++ b/Makefile @@ -174,7 +174,7 @@ athena-test: guard-AWS_ACCESS_KEY_ID guard-AWS_SECRET_ACCESS_KEY guard-ATHENA_S3 pytest -n auto -m "athena" --retries 3 --junitxml=test-results/junit-athena.xml fabric-test: guard-FABRIC_HOST guard-FABRIC_CLIENT_ID guard-FABRIC_CLIENT_SECRET guard-FABRIC_DATABASE engine-fabric-install - pytest -n auto -m "fabric" --retries 3 --junitxml=test-results/junit-fabric.xml + pytest -n auto -m "fabric" --retries 3 --junitxml=test-results/junit-fabric.xml gcp-postgres-test: guard-GCP_POSTGRES_INSTANCE_CONNECTION_STRING guard-GCP_POSTGRES_USER guard-GCP_POSTGRES_PASSWORD guard-GCP_POSTGRES_KEYFILE_JSON engine-gcppostgres-install pytest -n auto -m "gcp_postgres" --retries 3 --junitxml=test-results/junit-gcp-postgres.xml diff --git a/pyproject.toml b/pyproject.toml index 4e201a734d..e125bfb281 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -259,6 +259,7 @@ markers = [ "mssql: test for MSSQL", "mysql: test for MySQL", "postgres: test for Postgres", + "gcp_postgres: test for Postgres on GCP", "redshift: test for Redshift", "snowflake: test for Snowflake", "spark: test for Spark", @@ -267,7 +268,7 @@ markers = [ ] addopts = "-n 0 --dist=loadgroup" asyncio_default_fixture_loop_scope = "session" -log_cli = false # Set this to true to enable logging during tests +log_cli = true # Set this to true to enable logging during tests log_cli_format = "%(asctime)s.%(msecs)03d %(filename)s:%(lineno)d %(levelname)s %(message)s" log_cli_level = "INFO" filterwarnings = [ diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index a7b997e303..fe9deed0c2 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -83,6 +83,7 @@ def load_config_from_paths( personal_paths: t.Optional[t.List[Path]] = None, config_name: str = "config", load_from_env: bool = True, + variables: t.Optional[t.Dict[str, t.Any]] = None, **kwargs: t.Any, ) -> C: project_paths = project_paths or [] @@ -116,7 +117,7 @@ def load_config_from_paths( "YAML configs do not support multiple configs. Use Python instead.", ) yaml_config_path = path.resolve() - non_python_configs.append(load_config_from_yaml(path)) + non_python_configs.append(load_config_from_yaml(path, variables)) elif extension == "py": try: python_config = load_config_from_python_module( @@ -194,8 +195,10 @@ def load_config_from_paths( return non_python_config -def load_config_from_yaml(path: Path) -> t.Dict[str, t.Any]: - content = yaml_load(path) +def load_config_from_yaml( + path: Path, variables: t.Optional[t.Dict[str, t.Any]] = None +) -> t.Dict[str, t.Any]: + content = yaml_load(path, variables=variables) if not isinstance(content, dict): raise ConfigError( f"Invalid YAML configuration: expected a dictionary but got {type(content).__name__}. " diff --git a/sqlmesh/core/engine_adapter/fabric.py b/sqlmesh/core/engine_adapter/fabric.py index 6f0123d022..6d7d40c3bb 100644 --- a/sqlmesh/core/engine_adapter/fabric.py +++ b/sqlmesh/core/engine_adapter/fabric.py @@ -3,6 +3,7 @@ import typing as t import logging import requests +import time from functools import cached_property from sqlglot import exp from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_result @@ -15,6 +16,7 @@ from sqlmesh.utils.errors import SQLMeshError from sqlmesh.utils.connection_pool import ConnectionPool + if t.TYPE_CHECKING: from sqlmesh.core._typing import TableName @@ -172,8 +174,17 @@ def __init__(self, tenant_id: str, workspace_id: str, client_id: str, client_sec self.client_secret = client_secret self.workspace_id = workspace_id - def create_warehouse(self, warehouse_name: str) -> None: + def create_warehouse( + self, warehouse_name: str, if_not_exists: bool = True, attempt: int = 0 + ) -> None: """Create a catalog (warehouse) in Microsoft Fabric via REST API.""" + + # attempt count is arbitrary, it essentially equates to 5 minutes of 30 second waits + if attempt > 10: + raise SQLMeshError( + f"Gave up waiting for Fabric warehouse {warehouse_name} to become available" + ) + logger.info(f"Creating Fabric warehouse: {warehouse_name}") request_data = { @@ -182,7 +193,34 @@ def create_warehouse(self, warehouse_name: str) -> None: } response = self.session.post(self._endpoint_url("warehouses"), json=request_data) - response.raise_for_status() + + if ( + if_not_exists + and response.status_code == 400 + and (errorCode := response.json().get("errorCode", None)) + ): + if errorCode == "ItemDisplayNameAlreadyInUse": + logger.warning(f"Fabric warehouse {warehouse_name} already exists") + return + if errorCode == "ItemDisplayNameNotAvailableYet": + logger.warning(f"Fabric warehouse {warehouse_name} is still spinning up; waiting") + # Fabric error message is something like: + # - "Requested 'circleci_51d7087e__dev' is not available yet and is expected to become available in the upcoming minutes." + # This seems to happen if a catalog is dropped and then a new one with the same name is immediately created. + # There appears to be some delayed async process on the Fabric side that actually drops the warehouses and frees up the names to be used again + time.sleep(30) + return self.create_warehouse( + warehouse_name=warehouse_name, if_not_exists=if_not_exists, attempt=attempt + 1 + ) + + try: + response.raise_for_status() + except: + # the important information to actually debug anything is in the response body which Requests never prints + logger.exception( + f"Failed to create warehouse {warehouse_name}. status: {response.status_code}, body: {response.text}" + ) + raise # Handle direct success (201) or async creation (202) if response.status_code == 201: @@ -197,11 +235,12 @@ def create_warehouse(self, warehouse_name: str) -> None: logger.error(f"Unexpected response from Fabric API: {response}\n{response.text}") raise SQLMeshError(f"Unable to create warehouse: {response}") - def delete_warehouse(self, warehouse_name: str) -> None: + def delete_warehouse(self, warehouse_name: str, if_exists: bool = True) -> None: """Drop a catalog (warehouse) in Microsoft Fabric via REST API.""" logger.info(f"Deleting Fabric warehouse: {warehouse_name}") # Get the warehouse ID by listing warehouses + # TODO: handle continuationUri for pagination, ref: https://learn.microsoft.com/en-us/rest/api/fabric/warehouse/items/list-warehouses?tabs=HTTP#warehouses response = self.session.get(self._endpoint_url("warehouses")) response.raise_for_status() @@ -213,9 +252,12 @@ def delete_warehouse(self, warehouse_name: str) -> None: warehouse_id = warehouse_name_to_id.get(warehouse_name, None) if not warehouse_id: - logger.error( + logger.warning( f"Fabric warehouse does not exist: {warehouse_name}\n(available warehouses: {', '.join(warehouse_name_to_id)})" ) + if if_exists: + return + raise SQLMeshError( f"Unable to delete Fabric warehouse {warehouse_name} as it doesnt exist" ) diff --git a/tests/conftest.py b/tests/conftest.py index e5bbc4f425..e4911de80c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -212,6 +212,39 @@ def pytest_collection_modifyitems(items, *args, **kwargs): item.add_marker("fast") +@pytest.hookimpl(hookwrapper=True, tryfirst=True) +def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): + # The tmp_path fixture frequently throws errors like: + # - KeyError: <_pytest.stash.StashKey object at 0x79ba385fe1a0> + # in its teardown. This causes pytest to mark the test as failed even though we have zero control over this behaviour. + # So we log/swallow that particular error here rather than raising it + + # note: the hook always has to yield + outcome = yield + + # we only care about tests that used the tmp_path fixture + if "tmp_path" not in getattr(item, "fixturenames", []): + return + + result: pytest.TestReport = outcome.get_result() + + if result.when != "teardown": + return + + # If we specifically failed with a StashKey error in teardown, mark the test as passed + if result.failed: + exception = call.excinfo + if ( + exception + and isinstance(exception.value, KeyError) + and "_pytest.stash.StashKey" in repr(exception) + ): + result.outcome = "passed" + item.add_report_section( + "teardown", "stderr", f"Ignored tmp_path teardown error: {exception}" + ) + + # Ignore all local config files @pytest.fixture(scope="session", autouse=True) def ignore_local_config_files(): diff --git a/tests/core/engine_adapter/integration/__init__.py b/tests/core/engine_adapter/integration/__init__.py index baf45efa9c..c5377e309a 100644 --- a/tests/core/engine_adapter/integration/__init__.py +++ b/tests/core/engine_adapter/integration/__init__.py @@ -193,6 +193,7 @@ def __init__( engine_adapter: EngineAdapter, mark: str, gateway: str, + tmp_path: pathlib.Path, is_remote: bool = False, columns_to_types: t.Optional[t.Dict[str, t.Union[str, exp.DataType]]] = None, ): @@ -210,6 +211,7 @@ def __init__( self._catalogs: t.List[ str ] = [] # keep track of any catalogs created via self.create_catalog() so we can drop them at the end + self.tmp_path = tmp_path @property def test_type(self) -> str: @@ -655,6 +657,7 @@ def create_context( private_sqlmesh_dir / "config.yml", private_sqlmesh_dir / "config.yaml", ], + variables={"tmp_path": str(path or self.tmp_path)}, ) if config_mutator: config_mutator(self.gateway, config) diff --git a/tests/core/engine_adapter/integration/config.yaml b/tests/core/engine_adapter/integration/config.yaml index b75efc762b..8e87b2c3c8 100644 --- a/tests/core/engine_adapter/integration/config.yaml +++ b/tests/core/engine_adapter/integration/config.yaml @@ -5,7 +5,7 @@ gateways: type: duckdb catalogs: memory: ':memory:' - testing: 'testing.duckdb' + testing: "{{ var('tmp_path') }}/testing.duckdb" # Databases with docker images available inttest_trino_hive: diff --git a/tests/core/engine_adapter/integration/conftest.py b/tests/core/engine_adapter/integration/conftest.py index 4d374cfdbc..eafdf2fe1d 100644 --- a/tests/core/engine_adapter/integration/conftest.py +++ b/tests/core/engine_adapter/integration/conftest.py @@ -27,15 +27,15 @@ logger = logging.getLogger(__name__) -@pytest.fixture(scope="session") -def config() -> Config: +@pytest.fixture +def config(tmp_path: pathlib.Path) -> Config: return load_config_from_paths( Config, project_paths=[ - pathlib.Path("examples/wursthall/config.yaml"), pathlib.Path(os.path.join(os.path.dirname(__file__), "config.yaml")), ], personal_paths=[pathlib.Path("~/.sqlmesh/config.yaml").expanduser()], + variables={"tmp_path": str(tmp_path)}, ) @@ -89,7 +89,9 @@ def _create(engine_name: str, gateway: str) -> EngineAdapter: @pytest.fixture def create_test_context( - request: FixtureRequest, create_engine_adapter: t.Callable[[str, str], EngineAdapter] + request: FixtureRequest, + create_engine_adapter: t.Callable[[str, str], EngineAdapter], + tmp_path: pathlib.Path, ) -> t.Callable[[IntegrationTestEngine, str, str, str], t.Iterable[TestContext]]: def _create( engine: IntegrationTestEngine, gateway: str, test_type: str, table_format: str @@ -103,6 +105,7 @@ def _create( engine_adapter, f"{engine.engine}_{table_format}", gateway, + tmp_path=tmp_path, is_remote=is_remote, ) diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index ec5c6b4208..1960848e24 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -1,7 +1,6 @@ # type: ignore from __future__ import annotations -import os import pathlib import re import sys @@ -19,7 +18,6 @@ from sqlmesh import Config, Context from sqlmesh.cli.project_init import init_example_project -from sqlmesh.core.config import load_config_from_paths from sqlmesh.core.config.connection import ConnectionConfig import sqlmesh.core.dialect as d from sqlmesh.core.environment import EnvironmentSuffixTarget @@ -1936,49 +1934,16 @@ def test_transaction(ctx: TestContext): ctx.compare_with_current(table, input_data) -def test_sushi(ctx: TestContext, tmp_path_factory: pytest.TempPathFactory): +def test_sushi(ctx: TestContext, tmp_path: pathlib.Path): if ctx.mark == "athena_hive": pytest.skip( "Sushi end-to-end tests only need to run once for Athena because sushi needs a hybrid of both Hive and Iceberg" ) - tmp_path = tmp_path_factory.mktemp(f"sushi_{ctx.test_id}") - sushi_test_schema = ctx.add_test_suffix("sushi") sushi_state_schema = ctx.add_test_suffix("sushi_state") raw_test_schema = ctx.add_test_suffix("raw") - config = load_config_from_paths( - Config, - project_paths=[ - pathlib.Path(os.path.join(os.path.dirname(__file__), "config.yaml")), - ], - personal_paths=[pathlib.Path("~/.sqlmesh/config.yaml").expanduser()], - ) - before_all = [ - f"CREATE SCHEMA IF NOT EXISTS {raw_test_schema}", - f"DROP VIEW IF EXISTS {raw_test_schema}.demographics", - f"CREATE VIEW {raw_test_schema}.demographics AS (SELECT 1 AS customer_id, '00000' AS zip)", - ] - config.before_all = [ - quote_identifiers( - parse_one(e, dialect=config.model_defaults.dialect), - dialect=config.model_defaults.dialect, - ).sql(dialect=config.model_defaults.dialect) - for e in before_all - ] - - # To enable parallelism in integration tests - config.gateways = {ctx.gateway: config.gateways[ctx.gateway]} - current_gateway_config = config.gateways[ctx.gateway] - current_gateway_config.state_schema = sushi_state_schema - - if ctx.dialect == "athena": - # Ensure that this test is using the same s3_warehouse_location as TestContext (which includes the testrun_id) - current_gateway_config.connection.s3_warehouse_location = ( - ctx.engine_adapter.s3_warehouse_location - ) - # Copy sushi example to tmpdir shutil.copytree(pathlib.Path("./examples/sushi"), tmp_path, dirs_exist_ok=True) @@ -2000,7 +1965,23 @@ def test_sushi(ctx: TestContext, tmp_path_factory: pytest.TempPathFactory): contents = contents.replace(search, replace) f.write_text(contents) - context = Context(paths=tmp_path, config=config, gateway=ctx.gateway) + before_all = [ + f"CREATE SCHEMA IF NOT EXISTS {raw_test_schema}", + f"DROP VIEW IF EXISTS {raw_test_schema}.demographics", + f"CREATE VIEW {raw_test_schema}.demographics AS (SELECT 1 AS customer_id, '00000' AS zip)", + ] + + def _mutate_config(gateway: str, config: Config) -> None: + config.gateways[gateway].state_schema = sushi_state_schema + config.before_all = [ + quote_identifiers( + parse_one(e, dialect=config.model_defaults.dialect), + dialect=config.model_defaults.dialect, + ).sql(dialect=config.model_defaults.dialect) + for e in before_all + ] + + context = ctx.create_context(_mutate_config, path=tmp_path, ephemeral_state_connection=False) end = now() start = to_date(end - timedelta(days=7)) @@ -2355,9 +2336,7 @@ def validate_no_comments( ctx._schemas.append(schema) -def test_init_project(ctx: TestContext, tmp_path_factory: pytest.TempPathFactory): - tmp_path = tmp_path_factory.mktemp(f"init_project_{ctx.test_id}") - +def test_init_project(ctx: TestContext, tmp_path: pathlib.Path): schema_name = ctx.add_test_suffix(TEST_SCHEMA) state_schema = ctx.add_test_suffix("sqlmesh_state") @@ -2383,33 +2362,15 @@ def _normalize_snowflake(name: str, prefix_regex: str = "(sqlmesh__)(.*)"): init_example_project(tmp_path, ctx.engine_type, schema_name=schema_name) - config = load_config_from_paths( - Config, - project_paths=[ - pathlib.Path(os.path.join(os.path.dirname(__file__), "config.yaml")), - ], - personal_paths=[pathlib.Path("~/.sqlmesh/config.yaml").expanduser()], - ) - - # ensure default dialect comes from init_example_project and not ~/.sqlmesh/config.yaml - if config.model_defaults.dialect != ctx.dialect: - config.model_defaults = config.model_defaults.copy(update={"dialect": ctx.dialect}) - - # To enable parallelism in integration tests - config.gateways = {ctx.gateway: config.gateways[ctx.gateway]} - current_gateway_config = config.gateways[ctx.gateway] - - if ctx.dialect == "athena": - # Ensure that this test is using the same s3_warehouse_location as TestContext (which includes the testrun_id) - current_gateway_config.connection.s3_warehouse_location = ( - ctx.engine_adapter.s3_warehouse_location - ) + def _mutate_config(gateway: str, config: Config): + # ensure default dialect comes from init_example_project and not ~/.sqlmesh/config.yaml + if config.model_defaults.dialect != ctx.dialect: + config.model_defaults = config.model_defaults.copy(update={"dialect": ctx.dialect}) - # Ensure the state schema is unique to this test - config.gateways[ctx.gateway].state_schema = state_schema + # Ensure the state schema is unique to this test (since we deliberately use the warehouse as the state connection) + config.gateways[gateway].state_schema = state_schema - context = Context(paths=tmp_path, config=config, gateway=ctx.gateway) - ctx.engine_adapter = context.engine_adapter + context = ctx.create_context(_mutate_config, path=tmp_path, ephemeral_state_connection=False) if ctx.default_table_format: # if the default table format is explicitly set, ensure its being used @@ -3607,6 +3568,7 @@ def test_identifier_length_limit(ctx: TestContext): EnvironmentSuffixTarget.CATALOG, ], ) +@pytest.mark.xdist_group("serial") def test_janitor( ctx: TestContext, tmp_path: pathlib.Path, environment_suffix_target: EnvironmentSuffixTarget ): @@ -3621,9 +3583,10 @@ def test_janitor( init_example_project(tmp_path, ctx.engine_type, schema_name=parsed_schema.db) - def _set_config(_gateway: str, config: Config) -> None: + def _set_config(gateway: str, config: Config) -> None: config.environment_suffix_target = environment_suffix_target config.model_defaults.dialect = ctx.dialect + config.gateways[gateway].connection.concurrent_tasks = 1 sqlmesh = ctx.create_context(path=tmp_path, config_mutator=_set_config) @@ -3648,7 +3611,10 @@ def _set_config(_gateway: str, config: Config) -> None: # check physical objects snapshot_table_name = exp.to_table(new_model.table_name(), dialect=ctx.dialect) - snapshot_schema = snapshot_table_name.db + snapshot_schema = parsed_schema.copy() + snapshot_schema.set( + "db", exp.to_identifier(snapshot_table_name.db) + ) # we need this to be catalog.schema and not just schema for environment_suffix_target: catalog prod_schema = normalize_identifiers(d.to_schema(schema), dialect=ctx.dialect) dev_env_schema = prod_schema.copy() diff --git a/tests/core/engine_adapter/integration/test_integration_fabric.py b/tests/core/engine_adapter/integration/test_integration_fabric.py new file mode 100644 index 0000000000..a272005bdc --- /dev/null +++ b/tests/core/engine_adapter/integration/test_integration_fabric.py @@ -0,0 +1,41 @@ +import typing as t +import pytest +from pytest import FixtureRequest +from sqlmesh.core.engine_adapter import FabricEngineAdapter +from tests.core.engine_adapter.integration import TestContext + +from tests.core.engine_adapter.integration import ( + TestContext, + generate_pytest_params, + ENGINES_BY_NAME, + IntegrationTestEngine, +) + + +@pytest.fixture( + params=list(generate_pytest_params(ENGINES_BY_NAME["fabric"], show_variant_in_test_id=False)) +) +def ctx( + request: FixtureRequest, + create_test_context: t.Callable[[IntegrationTestEngine, str, str], t.Iterable[TestContext]], +) -> t.Iterable[TestContext]: + yield from create_test_context(*request.param) + + +@pytest.fixture +def engine_adapter(ctx: TestContext) -> FabricEngineAdapter: + assert isinstance(ctx.engine_adapter, FabricEngineAdapter) + return ctx.engine_adapter + + +def test_create_drop_catalog(ctx: TestContext, engine_adapter: FabricEngineAdapter): + catalog_name = ctx.add_test_suffix("test_catalog") + + try: + ctx.create_catalog(catalog_name) + # if already exists, should be no-op, not error + ctx.create_catalog(catalog_name) + ctx.drop_catalog(catalog_name) + finally: + # if doesnt exist, should be no-op, not error + ctx.drop_catalog(catalog_name) From 87f724a14fe5f178dff80da62378824a0a8fa02e Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 25 Aug 2025 14:37:16 -0700 Subject: [PATCH 0757/1056] chore(web_common): move badge related css vars inside component and make css exportable (#5216) --- web/common/package.json | 5 ++- web/common/src/components/Badge/Badge.css | 4 ++ web/common/src/components/Badge/Badge.tsx | 3 ++ .../src/styles/design/semantic-colors.css | 4 -- web/common/tailwind.base.config.js | 44 +++++++++++++++++++ web/common/tailwind.config.js | 42 +----------------- web/common/tsconfig.base.json | 34 ++++++++++++++ web/common/tsconfig.build.json | 9 +++- web/common/tsconfig.json | 32 +------------- web/common/vite.config.js | 12 +++++ 10 files changed, 110 insertions(+), 79 deletions(-) create mode 100644 web/common/src/components/Badge/Badge.css create mode 100644 web/common/tailwind.base.config.js create mode 100644 web/common/tsconfig.base.json diff --git a/web/common/package.json b/web/common/package.json index 028ce6489d..5ad2c29389 100644 --- a/web/common/package.json +++ b/web/common/package.json @@ -50,8 +50,9 @@ "default": "./dist/sqlmesh-common.umd.js" } }, - "./design": "./dist/styles/design/index.css", - "./design/*": "./dist/styles/design/*" + "./styles/*": "./dist/styles/*", + "./design/*": "./dist/styles/design/*", + "./configs/*": "./dist/configs/*" }, "files": [ "/dist" diff --git a/web/common/src/components/Badge/Badge.css b/web/common/src/components/Badge/Badge.css new file mode 100644 index 0000000000..029ba541f1 --- /dev/null +++ b/web/common/src/components/Badge/Badge.css @@ -0,0 +1,4 @@ +:root { + --color-badge-background: var(--color-gray-100); + --color-badge-foreground: var(--color-prose); +} diff --git a/web/common/src/components/Badge/Badge.tsx b/web/common/src/components/Badge/Badge.tsx index 9ba338e245..93f380bddd 100644 --- a/web/common/src/components/Badge/Badge.tsx +++ b/web/common/src/components/Badge/Badge.tsx @@ -6,6 +6,8 @@ import { type Size, type Shape } from '@/types/enums' import { cn } from '@/utils' import { badgeVariants } from './help' +import './Badge.css' + export interface BadgeProps extends React.HTMLAttributes, VariantProps { @@ -21,6 +23,7 @@ export const Badge = React.forwardRef( ) diff --git a/web/common/src/styles/design/semantic-colors.css b/web/common/src/styles/design/semantic-colors.css index c58e237539..4a58ba52ff 100644 --- a/web/common/src/styles/design/semantic-colors.css +++ b/web/common/src/styles/design/semantic-colors.css @@ -5,8 +5,4 @@ --color-dark: hsl(226, 24%, 8%); --color-brand: var(--color-tobiko); --color-prose: var(--color-gray-800); - - /* Badge */ - --color-badge-background: var(--color-gray-100); - --color-badge-foreground: var(--color-prose); } diff --git a/web/common/tailwind.base.config.js b/web/common/tailwind.base.config.js new file mode 100644 index 0000000000..a004a94d01 --- /dev/null +++ b/web/common/tailwind.base.config.js @@ -0,0 +1,44 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + theme: { + colors: { + transparent: 'transparent', + white: 'var(--color-white)', + black: 'var(--color-black)', + dark: 'var(--color-dark)', + light: 'var(--color-light)', + brand: 'var(--color-brand)', + prose: 'var(--color-prose)', + badge: { + background: 'var(--color-badge-background)', + foreground: 'var(--color-badge-foreground)', + }, + }, + extend: { + borderRadius: { + '2xs': 'var(--radius-xs)', + xs: 'calc(var(--radius-xs) + 1px)', + sm: 'calc(var(--radius-xs) + 2px)', + md: 'calc(var(--radius-s))', + lg: 'calc(var(--radius-s) + 1px)', + xl: 'calc(var(--radius-s) + 2px)', + '2xl': 'calc(var(--radius-m))', + }, + fontSize: { + '2xs': 'var(--text-2xs)', + xs: 'var(--text-xs)', + s: 'var(--text-s)', + m: 'var(--text-m)', + l: 'var(--text-l)', + xl: 'var(--text-xl)', + '2xl': 'var(--text-2xl)', + '3xl': 'var(--text-3xl)', + '4xl': 'var(--text-4xl)', + }, + fontFamily: { + mono: ['var(--font-mono)'], + }, + }, + }, + plugins: [require('@tailwindcss/typography')], +} diff --git a/web/common/tailwind.config.js b/web/common/tailwind.config.js index f751f8a6b0..67fe2ac528 100644 --- a/web/common/tailwind.config.js +++ b/web/common/tailwind.config.js @@ -1,45 +1,5 @@ /** @type {import('tailwindcss').Config} */ module.exports = { content: ['./src/**/*.{js,ts,jsx,tsx}', './src/**/*.stories.{js,ts,jsx,tsx}'], - theme: { - colors: { - transparent: 'transparent', - white: 'var(--color-white)', - black: 'var(--color-black)', - dark: 'var(--color-dark)', - light: 'var(--color-light)', - brand: 'var(--color-brand)', - prose: 'var(--color-prose)', - badge: { - background: 'var(--color-badge-background)', - foreground: 'var(--color-badge-foreground)', - }, - }, - extend: { - borderRadius: { - '2xs': 'var(--radius-xs)', - xs: 'calc(var(--radius-xs) + 1px)', - sm: 'calc(var(--radius-xs) + 2px)', - md: 'calc(var(--radius-s))', - lg: 'calc(var(--radius-s) + 1px)', - xl: 'calc(var(--radius-s) + 2px)', - '2xl': 'calc(var(--radius-m))', - }, - fontSize: { - '2xs': 'var(--text-2xs)', - xs: 'var(--text-xs)', - s: 'var(--text-s)', - m: 'var(--text-m)', - l: 'var(--text-l)', - xl: 'var(--text-xl)', - '2xl': 'var(--text-2xl)', - '3xl': 'var(--text-3xl)', - '4xl': 'var(--text-4xl)', - }, - fontFamily: { - mono: ['var(--font-mono)'], - }, - }, - }, - plugins: [require('@tailwindcss/typography')], + presets: [require('./tailwind.base.config')], } diff --git a/web/common/tsconfig.base.json b/web/common/tsconfig.base.json new file mode 100644 index 0000000000..99a214fe47 --- /dev/null +++ b/web/common/tsconfig.base.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2022", + "jsx": "react-jsx", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + /* Linting */ + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + /* Declaration */ + "declaration": true, + "declarationDir": "./dist", + "emitDeclarationOnly": false, + + /* Paths */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/web/common/tsconfig.build.json b/web/common/tsconfig.build.json index 80129f67c0..7eba394efd 100644 --- a/web/common/tsconfig.build.json +++ b/web/common/tsconfig.build.json @@ -1,7 +1,12 @@ { - "extends": "./tsconfig.json", + "extends": "./tsconfig.base.json", "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"], + "exclude": [ + "**/*.test.ts", + "**/*.test.tsx", + "**/*.stories.tsx", + "tests/**/*" + ], "compilerOptions": { "noEmit": false, "allowImportingTsExtensions": false, diff --git a/web/common/tsconfig.json b/web/common/tsconfig.json index 4c17f5cf76..0e230b85f9 100644 --- a/web/common/tsconfig.json +++ b/web/common/tsconfig.json @@ -1,35 +1,7 @@ { + "extends": "./tsconfig.base.json", "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"], "compilerOptions": { - "target": "ES2022", - "jsx": "react-jsx", - "module": "ESNext", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "types": ["vite/client", "@testing-library/jest-dom"], - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - /* Linting */ - "skipLibCheck": true, - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true, - - /* Declaration */ - "declaration": true, - "declarationDir": "./dist", - "emitDeclarationOnly": false, - - /* Paths */ - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } + "types": ["vite/client", "@testing-library/jest-dom"] } } diff --git a/web/common/vite.config.js b/web/common/vite.config.js index 65b369ab16..237bed29bd 100644 --- a/web/common/vite.config.js +++ b/web/common/vite.config.js @@ -10,6 +10,7 @@ export default defineConfig({ dts({ insertTypesEntry: true, declarationMap: true, + tsconfigPath: './tsconfig.build.json', }), viteStaticCopy({ targets: [ @@ -17,6 +18,10 @@ export default defineConfig({ src: 'src/styles/design', dest: 'styles', }, + { + src: 'tailwind.base.config.js', + dest: 'configs', + }, ], }), ], @@ -26,6 +31,7 @@ export default defineConfig({ }, }, build: { + cssMinify: true, lib: { entry: path.resolve(__dirname, 'src/index.ts'), name: 'sqlmesh-common', @@ -51,6 +57,12 @@ export default defineConfig({ 'class-variance-authority': 'classVarianceAuthority', '@radix-ui/react-slot': 'radixSlot', }, + assetFileNames: assetInfo => { + if (assetInfo.name && assetInfo.name.endsWith('.css')) { + return 'styles/[name].min[extname]' + } + return '[name][extname]' + }, }, }, sourcemap: process.env.NODE_ENV !== 'production', From a759712c995c0f4ab86080e3a4edf4735a3c8a60 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 25 Aug 2025 15:05:29 -0700 Subject: [PATCH 0758/1056] Fix: Catalog creation / deletion for motherduck (#5223) --- sqlmesh/core/config/connection.py | 4 ++ sqlmesh/core/engine_adapter/duckdb.py | 34 +++++++++++++---- sqlmesh/core/state_sync/common.py | 48 +++++++++++++----------- tests/core/engine_adapter/test_duckdb.py | 18 +++++++++ 4 files changed, 75 insertions(+), 29 deletions(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index b68b83a39a..1678f5d147 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -537,6 +537,10 @@ def _static_connection_kwargs(self) -> t.Dict[str, t.Any]: connection_str += f"{'&' if self.database else '?'}motherduck_token={self.token}" return {"database": connection_str, "config": custom_user_agent_config} + @property + def _extra_engine_config(self) -> t.Dict[str, t.Any]: + return {"is_motherduck": True} + class DuckDBConnectionConfig(BaseDuckDBConnectionConfig): """Configuration for the DuckDB connection.""" diff --git a/sqlmesh/core/engine_adapter/duckdb.py b/sqlmesh/core/engine_adapter/duckdb.py index 8fbe40a575..4bce813610 100644 --- a/sqlmesh/core/engine_adapter/duckdb.py +++ b/sqlmesh/core/engine_adapter/duckdb.py @@ -47,16 +47,30 @@ def set_current_catalog(self, catalog: str) -> None: self.execute(exp.Use(this=exp.to_identifier(catalog))) def _create_catalog(self, catalog_name: exp.Identifier) -> None: - db_filename = f"{catalog_name.output_name}.db" - self.execute( - exp.Attach(this=exp.alias_(exp.Literal.string(db_filename), catalog_name), exists=True) - ) + if not self._is_motherduck: + db_filename = f"{catalog_name.output_name}.db" + self.execute( + exp.Attach( + this=exp.alias_(exp.Literal.string(db_filename), catalog_name), exists=True + ) + ) + else: + self.execute( + exp.Create(this=exp.Table(this=catalog_name), kind="DATABASE", exists=True) + ) def _drop_catalog(self, catalog_name: exp.Identifier) -> None: - db_file_path = Path(f"{catalog_name.output_name}.db") - self.execute(exp.Detach(this=catalog_name, exists=True)) - if db_file_path.exists(): - db_file_path.unlink() + if not self._is_motherduck: + db_file_path = Path(f"{catalog_name.output_name}.db") + self.execute(exp.Detach(this=catalog_name, exists=True)) + if db_file_path.exists(): + db_file_path.unlink() + else: + self.execute( + exp.Drop( + this=exp.Table(this=catalog_name), kind="DATABASE", cascade=True, exists=True + ) + ) def _df_to_source_queries( self, @@ -198,3 +212,7 @@ def _create_table( expr.sql(dialect=self.dialect) for expr in partitioned_by_exps ) self.execute(f"ALTER TABLE {table_name_str} SET PARTITIONED BY ({partitioned_by_str});") + + @property + def _is_motherduck(self) -> bool: + return self._extra_config.get("is_motherduck", False) diff --git a/sqlmesh/core/state_sync/common.py b/sqlmesh/core/state_sync/common.py index 12899da82e..cd8c389e33 100644 --- a/sqlmesh/core/state_sync/common.py +++ b/sqlmesh/core/state_sync/common.py @@ -7,6 +7,7 @@ import abc from dataclasses import dataclass +from sqlglot import exp from sqlmesh.core.console import Console from sqlmesh.core.dialect import schema_ @@ -29,7 +30,7 @@ def cleanup_expired_views( warn_on_delete_failure: bool = False, console: t.Optional[Console] = None, ) -> None: - expired_schema_environments = [ + expired_schema_or_catalog_environments = [ environment for environment in environments if environment.suffix_target.is_schema or environment.suffix_target.is_catalog @@ -45,8 +46,9 @@ def get_adapter(gateway_managed: bool, gateway: t.Optional[str] = None) -> Engin return default_adapter catalogs_to_drop: t.Set[t.Tuple[EngineAdapter, str]] = set() + schemas_to_drop: t.Set[t.Tuple[EngineAdapter, exp.Table]] = set() - # Drop the schemas for the expired environments + # Collect schemas and catalogs to drop for engine_adapter, expired_catalog, expired_schema, suffix_target in { ( (engine_adapter := get_adapter(environment.gateway_managed, snapshot.model_gateway)), @@ -58,29 +60,16 @@ def get_adapter(gateway_managed: bool, gateway: t.Optional[str] = None) -> Engin ), environment.suffix_target, ) - for environment in expired_schema_environments + for environment in expired_schema_or_catalog_environments for snapshot in environment.snapshots if snapshot.is_model and not snapshot.is_symbolic }: - schema = schema_(expired_schema, expired_catalog) - try: - engine_adapter.drop_schema( - schema, - ignore_if_not_exists=True, - cascade=True, - ) - - if suffix_target.is_catalog and expired_catalog: + if suffix_target.is_catalog: + if expired_catalog: catalogs_to_drop.add((engine_adapter, expired_catalog)) - - if console: - console.update_cleanup_progress(schema.sql(dialect=engine_adapter.dialect)) - except Exception as e: - message = f"Failed to drop the expired environment schema '{schema}': {e}" - if warn_on_delete_failure: - logger.warning(message) - else: - raise SQLMeshError(message) from e + else: + schema = schema_(expired_schema, expired_catalog) + schemas_to_drop.add((engine_adapter, schema)) # Drop the views for the expired environments for engine_adapter, expired_view in { @@ -105,6 +94,23 @@ def get_adapter(gateway_managed: bool, gateway: t.Optional[str] = None) -> Engin else: raise SQLMeshError(message) from e + # Drop the schemas for the expired environments + for engine_adapter, schema in schemas_to_drop: + try: + engine_adapter.drop_schema( + schema, + ignore_if_not_exists=True, + cascade=True, + ) + if console: + console.update_cleanup_progress(schema.sql(dialect=engine_adapter.dialect)) + except Exception as e: + message = f"Failed to drop the expired environment schema '{schema}': {e}" + if warn_on_delete_failure: + logger.warning(message) + else: + raise SQLMeshError(message) from e + # Drop any catalogs that were associated with a snapshot where the engine adapter supports dropping catalogs # catalogs_to_drop is only populated when environment_suffix_target is set to 'catalog' for engine_adapter, catalog in catalogs_to_drop: diff --git a/tests/core/engine_adapter/test_duckdb.py b/tests/core/engine_adapter/test_duckdb.py index 7799cefe0c..9fd65a6e66 100644 --- a/tests/core/engine_adapter/test_duckdb.py +++ b/tests/core/engine_adapter/test_duckdb.py @@ -101,6 +101,15 @@ def test_create_catalog(make_mocked_engine_adapter: t.Callable) -> None: assert to_sql_calls(adapter) == ["ATTACH IF NOT EXISTS 'foo.db' AS \"foo\""] +def test_create_catalog_motherduck(make_mocked_engine_adapter: t.Callable) -> None: + adapter: DuckDBEngineAdapter = make_mocked_engine_adapter( + DuckDBEngineAdapter, is_motherduck=True + ) + adapter.create_catalog(exp.to_identifier("foo")) + + assert to_sql_calls(adapter) == ['CREATE DATABASE IF NOT EXISTS "foo"'] + + def test_drop_catalog(make_mocked_engine_adapter: t.Callable) -> None: adapter: DuckDBEngineAdapter = make_mocked_engine_adapter(DuckDBEngineAdapter) adapter.drop_catalog(exp.to_identifier("foo")) @@ -108,6 +117,15 @@ def test_drop_catalog(make_mocked_engine_adapter: t.Callable) -> None: assert to_sql_calls(adapter) == ['DETACH DATABASE IF EXISTS "foo"'] +def test_drop_catalog_motherduck(make_mocked_engine_adapter: t.Callable) -> None: + adapter: DuckDBEngineAdapter = make_mocked_engine_adapter( + DuckDBEngineAdapter, is_motherduck=True + ) + adapter.drop_catalog(exp.to_identifier("foo")) + + assert to_sql_calls(adapter) == ['DROP DATABASE IF EXISTS "foo" CASCADE'] + + def test_ducklake_partitioning(adapter: EngineAdapter, duck_conn, tmp_path): catalog = "a_ducklake_db" From 8751ee53352a8cdfa2089f93f3bf9c19453235eb Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 26 Aug 2025 10:20:52 +1200 Subject: [PATCH 0759/1056] Feat(dbt_cli): Add support for `--vars` (#5205) --- sqlmesh/core/config/loader.py | 1 + sqlmesh_dbt/cli.py | 44 +++++++++++++++++++++++++------- sqlmesh_dbt/error.py | 12 --------- sqlmesh_dbt/operations.py | 6 ++++- sqlmesh_dbt/options.py | 25 ++++++++++++++++++ tests/dbt/cli/test_list.py | 14 ++++++++++ tests/dbt/cli/test_operations.py | 14 ++++++++++ tests/dbt/cli/test_options.py | 23 +++++++++++++++++ 8 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 sqlmesh_dbt/options.py create mode 100644 tests/dbt/cli/test_options.py diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index fe9deed0c2..75915800e6 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -176,6 +176,7 @@ def load_config_from_paths( project_root=dbt_project_file.parent, dbt_profile_name=kwargs.pop("profile", None), dbt_target_name=kwargs.pop("target", None), + variables=variables, ) if type(dbt_python_config) != config_type: dbt_python_config = convert_config_type(dbt_python_config, config_type) diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py index 7d98e812b7..d82c2afd92 100644 --- a/sqlmesh_dbt/cli.py +++ b/sqlmesh_dbt/cli.py @@ -4,12 +4,31 @@ from sqlmesh_dbt.operations import DbtOperations, create from sqlmesh_dbt.error import cli_global_error_handler from pathlib import Path +from sqlmesh_dbt.options import YamlParamType +import functools -def _get_dbt_operations(ctx: click.Context) -> DbtOperations: - if not isinstance(ctx.obj, DbtOperations): +def _get_dbt_operations(ctx: click.Context, vars: t.Optional[t.Dict[str, t.Any]]) -> DbtOperations: + if not isinstance(ctx.obj, functools.partial): raise ValueError(f"Unexpected click context object: {type(ctx.obj)}") - return ctx.obj + + dbt_operations = ctx.obj(vars=vars) + + if not isinstance(dbt_operations, DbtOperations): + raise ValueError(f"Unexpected dbt operations type: {type(dbt_operations)}") + + @ctx.call_on_close + def _cleanup() -> None: + dbt_operations.close() + + return dbt_operations + + +vars_option = click.option( + "--vars", + type=YamlParamType(), + help="Supply variables to the project. This argument overrides variables defined in your dbt_project.yml file. This argument should be a YAML string, eg. '{my_variable: my_value}'", +) select_option = click.option( @@ -40,10 +59,15 @@ def dbt( # we dont need to import sqlmesh/load the project for CLI help return - # TODO: conditionally call create() if there are times we dont want/need to import sqlmesh and load a project - ctx.obj = create(project_dir=Path.cwd(), profile=profile, target=target) + # we have a partially applied function here because subcommands might set extra options like --vars + # that need to be known before we attempt to load the project + ctx.obj = functools.partial(create, project_dir=Path.cwd(), profile=profile, target=target) if not ctx.invoked_subcommand: + if profile or target: + # trigger a project load to validate the specified profile / target + ctx.obj() + click.echo( f"No command specified. Run `{ctx.info_name} --help` to see the available commands." ) @@ -57,19 +81,21 @@ def dbt( "--full-refresh", help="If specified, dbt will drop incremental models and fully-recalculate the incremental table from the model definition.", ) +@vars_option @click.pass_context -def run(ctx: click.Context, **kwargs: t.Any) -> None: +def run(ctx: click.Context, vars: t.Optional[t.Dict[str, t.Any]], **kwargs: t.Any) -> None: """Compile SQL and execute against the current target database.""" - _get_dbt_operations(ctx).run(**kwargs) + _get_dbt_operations(ctx, vars).run(**kwargs) @dbt.command(name="list") @select_option @exclude_option +@vars_option @click.pass_context -def list_(ctx: click.Context, **kwargs: t.Any) -> None: +def list_(ctx: click.Context, vars: t.Optional[t.Dict[str, t.Any]], **kwargs: t.Any) -> None: """List the resources in your project""" - _get_dbt_operations(ctx).list_(**kwargs) + _get_dbt_operations(ctx, vars).list_(**kwargs) @dbt.command(name="ls", hidden=True) # hidden alias for list diff --git a/sqlmesh_dbt/error.py b/sqlmesh_dbt/error.py index f5d4bc438c..005ca87c50 100644 --- a/sqlmesh_dbt/error.py +++ b/sqlmesh_dbt/error.py @@ -25,17 +25,5 @@ def wrapper(*args: t.List[t.Any], **kwargs: t.Any) -> t.Any: sys.exit(1) else: raise - finally: - context_or_obj = args[0] - sqlmesh_context = ( - context_or_obj.obj if isinstance(context_or_obj, click.Context) else context_or_obj - ) - if sqlmesh_context is not None: - # important to import this only if a context was created - # otherwise something like `sqlmesh_dbt run --help` will trigger this import because it's in the finally: block - from sqlmesh import Context - - if isinstance(sqlmesh_context, Context): - sqlmesh_context.close() return wrapper diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index 2b89c0f3e9..296000847c 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -76,11 +76,15 @@ def console(self) -> DbtCliConsole: return console + def close(self) -> None: + self.context.close() + def create( project_dir: t.Optional[Path] = None, profile: t.Optional[str] = None, target: t.Optional[str] = None, + vars: t.Optional[t.Dict[str, t.Any]] = None, debug: bool = False, ) -> DbtOperations: with Progress(transient=True) as progress: @@ -104,7 +108,7 @@ def create( sqlmesh_context = Context( paths=[project_dir], - config_loader_kwargs=dict(profile=profile, target=target), + config_loader_kwargs=dict(profile=profile, target=target, variables=vars), load=True, ) diff --git a/sqlmesh_dbt/options.py b/sqlmesh_dbt/options.py new file mode 100644 index 0000000000..5a7cabe93b --- /dev/null +++ b/sqlmesh_dbt/options.py @@ -0,0 +1,25 @@ +import typing as t +import click +from click.core import Context, Parameter + + +class YamlParamType(click.ParamType): + name = "yaml" + + def convert( + self, value: t.Any, param: t.Optional[Parameter], ctx: t.Optional[Context] + ) -> t.Any: + if not isinstance(value, str): + self.fail(f"Input value '{value}' should be a string", param, ctx) + + from sqlmesh.utils import yaml + + try: + parsed = yaml.load(source=value, render_jinja=False) + except: + self.fail(f"String '{value}' is not valid YAML", param, ctx) + + if not isinstance(parsed, dict): + self.fail(f"String '{value}' did not evaluate to a dict, got: {parsed}", param, ctx) + + return parsed diff --git a/tests/dbt/cli/test_list.py b/tests/dbt/cli/test_list.py index fe3e1e6829..e854954903 100644 --- a/tests/dbt/cli/test_list.py +++ b/tests/dbt/cli/test_list.py @@ -46,3 +46,17 @@ def test_list_select_exclude(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[.. assert "main.orders" not in result.output assert "main.stg_payments" not in result.output assert "main.raw_orders" not in result.output + + +def test_list_with_vars(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): + (jaffle_shop_duckdb / "models" / "aliased_model.sql").write_text(""" + {{ config(alias='model_' + var('foo')) }} + select 1 + """) + + result = invoke_cli(["list", "--vars", "foo: bar"]) + + assert result.exit_code == 0 + assert not result.exception + + assert "model_bar" in result.output diff --git a/tests/dbt/cli/test_operations.py b/tests/dbt/cli/test_operations.py index 9d36b10f60..9b5b3113b3 100644 --- a/tests/dbt/cli/test_operations.py +++ b/tests/dbt/cli/test_operations.py @@ -69,3 +69,17 @@ def test_create_can_specify_profile_and_target(jaffle_shop_duckdb: Path): assert dbt_project.context.profile_name == "jaffle_shop" assert dbt_project.context.target_name == "dev" + + +def test_create_can_set_project_variables(jaffle_shop_duckdb: Path): + (jaffle_shop_duckdb / "models" / "test_model.sql").write_text(""" + select '{{ var('foo') }}' as a + """) + + dbt_project = create(vars={"foo": "bar"}) + assert dbt_project.context.config.variables["foo"] == "bar" + + test_model = dbt_project.context.models['"jaffle_shop"."main"."test_model"'] + query = test_model.render_query() + assert query is not None + assert query.sql() == "SELECT 'bar' AS \"a\"" diff --git a/tests/dbt/cli/test_options.py b/tests/dbt/cli/test_options.py new file mode 100644 index 0000000000..962ff0beb3 --- /dev/null +++ b/tests/dbt/cli/test_options.py @@ -0,0 +1,23 @@ +import typing as t +import pytest +from sqlmesh_dbt.options import YamlParamType +from click.exceptions import BadParameter + + +@pytest.mark.parametrize( + "input,expected", + [ + (1, BadParameter("Input value '1' should be a string")), + ("", BadParameter("String '' is not valid YAML")), + ("['a', 'b']", BadParameter("String.*did not evaluate to a dict, got.*")), + ("foo: bar", {"foo": "bar"}), + ('{"key": "value", "date": 20180101}', {"key": "value", "date": 20180101}), + ("{key: value, date: 20180101}", {"key": "value", "date": 20180101}), + ], +) +def test_yaml_param_type(input: str, expected: t.Union[BadParameter, t.Dict[str, t.Any]]): + if isinstance(expected, BadParameter): + with pytest.raises(BadParameter, match=expected.message): + YamlParamType().convert(input, None, None) + else: + assert YamlParamType().convert(input, None, None) == expected From 9d2806b75ffb147f2c3eebc14c096d12ca2d79dd Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 25 Aug 2025 16:25:05 -0700 Subject: [PATCH 0760/1056] chore(web_common): move colors in tailwind config (#5225) --- web/common/tailwind.base.config.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/web/common/tailwind.base.config.js b/web/common/tailwind.base.config.js index a004a94d01..e1b44d12d9 100644 --- a/web/common/tailwind.base.config.js +++ b/web/common/tailwind.base.config.js @@ -1,20 +1,21 @@ /** @type {import('tailwindcss').Config} */ module.exports = { theme: { - colors: { - transparent: 'transparent', - white: 'var(--color-white)', - black: 'var(--color-black)', - dark: 'var(--color-dark)', - light: 'var(--color-light)', - brand: 'var(--color-brand)', - prose: 'var(--color-prose)', - badge: { - background: 'var(--color-badge-background)', - foreground: 'var(--color-badge-foreground)', - }, - }, + colors: {}, extend: { + colors: { + transparent: 'transparent', + white: 'var(--color-white)', + black: 'var(--color-black)', + dark: 'var(--color-dark)', + light: 'var(--color-light)', + brand: 'var(--color-brand)', + prose: 'var(--color-prose)', + badge: { + background: 'var(--color-badge-background)', + foreground: 'var(--color-badge-foreground)', + }, + }, borderRadius: { '2xs': 'var(--radius-xs)', xs: 'calc(var(--radius-xs) + 1px)', From 28cd3268fe9785de1e5c50074563b2052edfc8c3 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 25 Aug 2025 16:28:17 -0700 Subject: [PATCH 0761/1056] Fix: Make flat_graph a cached property (#5224) --- sqlmesh/dbt/manifest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 7414325902..85a8f7205e 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -8,6 +8,7 @@ import typing as t from argparse import Namespace from collections import defaultdict +from functools import cached_property from pathlib import Path from dbt import constants as dbt_constants, flags @@ -158,7 +159,7 @@ def all_macros(self) -> t.Dict[str, t.Dict[str, MacroInfo]]: result[package_name][macro_name] = macro_config.info return result - @property + @cached_property def flat_graph(self) -> t.Dict[str, t.Any]: return { "exposures": { From f9eb71b3db331676f1fa2f2ed5dc518eb64360bd Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 25 Aug 2025 19:53:47 -0700 Subject: [PATCH 0762/1056] feat: widen dbt-core compatibility range (#5211) --- .github/workflows/pr.yaml | 60 ++++++++++++++++++++++++++++++++ Makefile | 27 +++++++++----- examples/sushi_dbt/profiles.yml | 8 +++++ pyproject.toml | 2 +- sqlmesh/dbt/loader.py | 7 ++-- sqlmesh/dbt/manifest.py | 32 +++++++++++++---- sqlmesh/dbt/relation.py | 6 ++-- sqlmesh/dbt/seed.py | 48 +++++++++++++------------ sqlmesh/dbt/util.py | 6 ++-- tests/dbt/conftest.py | 14 ++++++++ tests/dbt/test_adapter.py | 7 ++++ tests/dbt/test_config.py | 35 ++++++++++++------- tests/dbt/test_integration.py | 10 +++++- tests/dbt/test_manifest.py | 21 ++++++++--- tests/dbt/test_model.py | 6 +++- tests/dbt/test_transformation.py | 35 ++++++++++++++----- 16 files changed, 249 insertions(+), 75 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index b63f6a3ab6..3e715e1318 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -8,6 +8,8 @@ on: concurrency: group: 'pr-${{ github.event.pull_request.number }}' cancel-in-progress: true +permissions: + contents: read jobs: test-vscode: env: @@ -66,3 +68,61 @@ jobs: name: playwright-report path: vscode/extension/playwright-report/ retention-days: 30 + test-dbt-versions: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + dbt-version: + [ + '1.3.0', + '1.4.0', + '1.5.0', + '1.6.0', + '1.7.0', + '1.8.0', + '1.9.0', + '1.10.0', + ] + steps: + - uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install SQLMesh dev dependencies + run: | + uv venv .venv + source .venv/bin/activate + sed -i 's/"pydantic>=2.0.0"/"pydantic"/g' pyproject.toml + if [[ "${{ matrix.dbt-version }}" == "1.10.0" ]]; then + # For 1.10.0: only add version to dbt-core, remove versions from all adapter packages + sed -i -E 's/"(dbt-core)[^"]*"/"\1~=${{ matrix.dbt-version }}"/g' pyproject.toml + # Remove version constraints from all dbt adapter packages + sed -i -E 's/"(dbt-(bigquery|duckdb|snowflake|athena-community|clickhouse|databricks|redshift|trino))[^"]*"/"\1"/g' pyproject.toml + else + # For other versions: apply version to all dbt packages + sed -i -E 's/"(dbt-[^">=<~!]+)[^"]*"/"\1~=${{ matrix.dbt-version }}"/g' pyproject.toml + fi + UV=1 make install-dev + uv pip install pydantic>=2.0.0 --reinstall + - name: Run dbt tests + # We can't run slow tests across all engines due to tests requiring DuckDB and old versions + # of DuckDB require a version of DuckDB we no longer support + run: | + source .venv/bin/activate + make dbt-fast-test + - name: Test SQLMesh info in sushi_dbt + working-directory: ./examples/sushi_dbt + 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 + 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 + echo "DBT version is ${{ matrix.dbt-version }} (>= 1.5.0), keeping version parameters" + fi + sqlmesh info --skip-connection diff --git a/Makefile b/Makefile index bad2cf2907..04306946cd 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,16 @@ .PHONY: docs +ifdef UV + PIP := uv pip +else + PIP := pip3 +endif + install-dev: - pip3 install -e ".[dev,web,slack,dlt,lsp]" ./examples/custom_materializations + $(PIP) install -e ".[dev,web,slack,dlt,lsp]" ./examples/custom_materializations install-doc: - pip3 install -r ./docs/requirements.txt + $(PIP) install -r ./docs/requirements.txt install-pre-commit: pre-commit install @@ -22,16 +28,16 @@ doc-test: python -m pytest --doctest-modules sqlmesh/core sqlmesh/utils package: - pip3 install build && python3 -m build + $(PIP) install build && python3 -m build publish: package - pip3 install twine && python3 -m twine upload dist/* + $(PIP) install twine && python3 -m twine upload dist/* package-tests: - pip3 install build && cp pyproject.toml tests/sqlmesh_pyproject.toml && python3 -m build tests/ + $(PIP) install build && cp pyproject.toml tests/sqlmesh_pyproject.toml && python3 -m build tests/ publish-tests: package-tests - pip3 install twine && python3 -m twine upload -r tobiko-private tests/dist/* + $(PIP) install twine && python3 -m twine upload -r tobiko-private tests/dist/* docs-serve: mkdocs serve @@ -93,6 +99,9 @@ engine-test: dbt-test: pytest -n auto -m "dbt and not cicdonly" +dbt-fast-test: + pytest -n auto -m "dbt and fast" --retries 3 + github-test: pytest -n auto -m "github" @@ -109,7 +118,7 @@ guard-%: fi engine-%-install: - pip3 install -e ".[dev,web,slack,lsp,${*}]" ./examples/custom_materializations + $(PIP) install -e ".[dev,web,slack,lsp,${*}]" ./examples/custom_materializations engine-docker-%-up: docker compose -f ./tests/core/engine_adapter/integration/docker/compose.${*}.yaml up -d @@ -157,11 +166,11 @@ snowflake-test: guard-SNOWFLAKE_ACCOUNT guard-SNOWFLAKE_WAREHOUSE guard-SNOWFLAK pytest -n auto -m "snowflake" --retries 3 --junitxml=test-results/junit-snowflake.xml bigquery-test: guard-BIGQUERY_KEYFILE engine-bigquery-install - pip install -e ".[bigframes]" + $(PIP) install -e ".[bigframes]" pytest -n auto -m "bigquery" --retries 3 --junitxml=test-results/junit-bigquery.xml databricks-test: guard-DATABRICKS_CATALOG guard-DATABRICKS_SERVER_HOSTNAME guard-DATABRICKS_HTTP_PATH guard-DATABRICKS_ACCESS_TOKEN guard-DATABRICKS_CONNECT_VERSION engine-databricks-install - pip install 'databricks-connect==${DATABRICKS_CONNECT_VERSION}' + $(PIP) install 'databricks-connect==${DATABRICKS_CONNECT_VERSION}' pytest -n auto -m "databricks" --retries 3 --junitxml=test-results/junit-databricks.xml redshift-test: guard-REDSHIFT_HOST guard-REDSHIFT_USER guard-REDSHIFT_PASSWORD guard-REDSHIFT_DATABASE engine-redshift-install diff --git a/examples/sushi_dbt/profiles.yml b/examples/sushi_dbt/profiles.yml index 74de4e472c..794b083793 100644 --- a/examples/sushi_dbt/profiles.yml +++ b/examples/sushi_dbt/profiles.yml @@ -3,6 +3,14 @@ sushi: in_memory: type: duckdb schema: sushi + postgres: + type: postgres + host: "host" + user: "user" + password: "password" + dbname: "dbname" + port: 5432 + schema: sushi duckdb: type: duckdb path: 'local.duckdb' diff --git a/pyproject.toml b/pyproject.toml index e125bfb281..7e532f75f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ bigframes = ["bigframes>=1.32.0"] clickhouse = ["clickhouse-connect"] databricks = ["databricks-sql-connector[pyarrow]"] dev = [ - "agate==1.7.1", + "agate", "beautifulsoup4", "clickhouse-connect", "cryptography", diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index d321246896..594c5a8807 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -188,8 +188,11 @@ def _load_projects(self) -> t.List[Project]: self._projects.append(project) - if project.context.target.database != (self.context.default_catalog or ""): - raise ConfigError("Project default catalog does not match context default catalog") + context_default_catalog = self.context.default_catalog or "" + if project.context.target.database != context_default_catalog: + raise ConfigError( + f"Project default catalog ('{project.context.target.database}') does not match context default catalog ('{context_default_catalog}')." + ) for path in project.project_files: self._track_file(path) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 85a8f7205e..690bca4a3a 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -13,10 +13,17 @@ from dbt import constants as dbt_constants, flags +from sqlmesh.dbt.util import DBT_VERSION from sqlmesh.utils.conversions import make_serializable # Override the file name to prevent dbt commands from invalidating the cache. -dbt_constants.PARTIAL_PARSE_FILE_NAME = "sqlmesh_partial_parse.msgpack" + +if DBT_VERSION >= (1, 6, 0): + dbt_constants.PARTIAL_PARSE_FILE_NAME = "sqlmesh_partial_parse.msgpack" # type: ignore +else: + from dbt.parser import manifest as dbt_manifest # type: ignore + + dbt_manifest.PARTIAL_PARSE_FILE_NAME = "sqlmesh_partial_parse.msgpack" # type: ignore import jinja2 from dbt.adapters.factory import register_adapter, reset_adapters @@ -379,11 +386,17 @@ def _load_on_run_start_end(self) -> None: if "on-run-start" in node.tags: self._on_run_start_per_package[node.package_name][node_name] = HookConfig( - sql=sql, index=node.index or 0, path=node_path, dependencies=dependencies + sql=sql, + index=getattr(node, "index", None) or 0, + path=node_path, + dependencies=dependencies, ) else: self._on_run_end_per_package[node.package_name][node_name] = HookConfig( - sql=sql, index=node.index or 0, path=node_path, dependencies=dependencies + sql=sql, + index=getattr(node, "index", None) or 0, + path=node_path, + dependencies=dependencies, ) @property @@ -599,6 +612,9 @@ def _macro_references( manifest: Manifest, node: t.Union[ManifestNode, Macro] ) -> t.Set[MacroReference]: result: t.Set[MacroReference] = set() + if not hasattr(node, "depends_on"): + return result + for macro_node_id in node.depends_on.macros: if not macro_node_id: continue @@ -614,18 +630,20 @@ def _macro_references( def _refs(node: ManifestNode) -> t.Set[str]: if DBT_VERSION >= (1, 5, 0): - result = set() + result: t.Set[str] = set() + if not hasattr(node, "refs"): + return result for r in node.refs: - ref_name = f"{r.package}.{r.name}" if r.package else r.name + ref_name = f"{r.package}.{r.name}" if r.package else r.name # type: ignore if getattr(r, "version", None): - ref_name = f"{ref_name}_v{r.version}" + ref_name = f"{ref_name}_v{r.version}" # type: ignore result.add(ref_name) return result return {".".join(r) for r in node.refs} # type: ignore def _sources(node: ManifestNode) -> t.Set[str]: - return {".".join(s) for s in node.sources} + return {".".join(s) for s in getattr(node, "sources", [])} def _model_node_id(model_name: str, package: str) -> str: diff --git a/sqlmesh/dbt/relation.py b/sqlmesh/dbt/relation.py index f68a9ff6de..fff9f75593 100644 --- a/sqlmesh/dbt/relation.py +++ b/sqlmesh/dbt/relation.py @@ -1,7 +1,7 @@ from sqlmesh.dbt.util import DBT_VERSION -if DBT_VERSION < (1, 8, 0): - from dbt.contracts.relation import * # type: ignore # noqa: F403 -else: +if DBT_VERSION >= (1, 8, 0): from dbt.adapters.contracts.relation import * # type: ignore # noqa: F403 +else: + from dbt.contracts.relation import * # type: ignore # noqa: F403 diff --git a/sqlmesh/dbt/seed.py b/sqlmesh/dbt/seed.py index a84e39e653..cf22d961cf 100644 --- a/sqlmesh/dbt/seed.py +++ b/sqlmesh/dbt/seed.py @@ -5,11 +5,13 @@ import agate -try: +from sqlmesh.dbt.util import DBT_VERSION + +if DBT_VERSION >= (1, 8, 0): from dbt_common.clients import agate_helper # type: ignore SUPPORTS_DELIMITER = True -except ImportError: +else: from dbt.clients import agate_helper # type: ignore SUPPORTS_DELIMITER = False @@ -95,27 +97,7 @@ def to_sqlmesh( ) -class Integer(agate_helper.Integer): - def cast(self, d: t.Any) -> t.Optional[int]: - if isinstance(d, str): - # The dbt's implementation doesn't support coercion of strings to integers. - if d.strip().lower() in self.null_values: - return None - try: - return int(d) - except ValueError: - raise agate.exceptions.CastError('Can not parse value "%s" as Integer.' % d) - return super().cast(d) - - def jsonify(self, d: t.Any) -> str: - return d - - -agate_helper.Integer = Integer # type: ignore - - AGATE_TYPE_MAPPING = { - agate_helper.Integer: exp.DataType.build("int"), agate_helper.Number: exp.DataType.build("double"), agate_helper.ISODateTime: exp.DataType.build("datetime"), agate.Date: exp.DataType.build("date"), @@ -123,3 +105,25 @@ def jsonify(self, d: t.Any) -> str: agate.Boolean: exp.DataType.build("boolean"), agate.Text: exp.DataType.build("text"), } + + +if DBT_VERSION >= (1, 7, 0): + + class Integer(agate_helper.Integer): + def cast(self, d: t.Any) -> t.Optional[int]: + if isinstance(d, str): + # The dbt's implementation doesn't support coercion of strings to integers. + if d.strip().lower() in self.null_values: + return None + try: + return int(d) + except ValueError: + raise agate.exceptions.CastError('Can not parse value "%s" as Integer.' % d) + return super().cast(d) + + def jsonify(self, d: t.Any) -> str: + return d + + agate_helper.Integer = Integer # type: ignore + + AGATE_TYPE_MAPPING[agate_helper.Integer] = exp.DataType.build("int") diff --git a/sqlmesh/dbt/util.py b/sqlmesh/dbt/util.py index 9ffca39167..0de16e3b3e 100644 --- a/sqlmesh/dbt/util.py +++ b/sqlmesh/dbt/util.py @@ -20,10 +20,10 @@ def _get_dbt_version() -> t.Tuple[int, int, int]: DBT_VERSION = _get_dbt_version() -if DBT_VERSION < (1, 8, 0): - from dbt.clients.agate_helper import table_from_data_flat, empty_table, as_matrix # type: ignore # noqa: F401 -else: +if DBT_VERSION >= (1, 8, 0): from dbt_common.clients.agate_helper import table_from_data_flat, empty_table, as_matrix # type: ignore # noqa: F401 +else: + from dbt.clients.agate_helper import table_from_data_flat, empty_table, as_matrix # type: ignore # noqa: F401 def pandas_to_agate(df: pd.DataFrame) -> agate.Table: diff --git a/tests/dbt/conftest.py b/tests/dbt/conftest.py index 1852a873c8..5875d9f575 100644 --- a/tests/dbt/conftest.py +++ b/tests/dbt/conftest.py @@ -7,6 +7,7 @@ from sqlmesh.core.context import Context from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.project import Project +from sqlmesh.dbt.target import PostgresConfig @pytest.fixture() @@ -25,3 +26,16 @@ def render(value: str) -> str: return render return create_renderer + + +@pytest.fixture() +def dbt_dummy_postgres_config() -> PostgresConfig: + return PostgresConfig( # type: ignore + name="postgres", + host="host", + user="user", + password="password", + dbname="dbname", + port=5432, + schema="schema", + ) diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index 445e5f29c0..85dfa29559 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -23,6 +23,7 @@ pytestmark = pytest.mark.dbt +@pytest.mark.slow def test_adapter_relation(sushi_test_project: Project, runtime_renderer: t.Callable): context = sushi_test_project.context assert context.target @@ -96,6 +97,7 @@ def test_adapter_relation(sushi_test_project: Project, runtime_renderer: t.Calla assert engine_adapter.table_exists("foo.bar__backup") +@pytest.mark.slow def test_bigquery_get_columns_in_relation( sushi_test_project: Project, runtime_renderer: t.Callable, @@ -135,6 +137,7 @@ def test_bigquery_get_columns_in_relation( @pytest.mark.cicdonly +@pytest.mark.slow def test_normalization( sushi_test_project: Project, runtime_renderer: t.Callable, mocker: MockerFixture ): @@ -232,6 +235,7 @@ def test_normalization( adapter_mock.drop_table.assert_has_calls([call(relation_bla_bob)]) +@pytest.mark.slow def test_adapter_dispatch(sushi_test_project: Project, runtime_renderer: t.Callable): context = sushi_test_project.context renderer = runtime_renderer(context) @@ -244,6 +248,7 @@ def test_adapter_dispatch(sushi_test_project: Project, runtime_renderer: t.Calla @pytest.mark.parametrize("project_dialect", ["duckdb", "bigquery"]) +@pytest.mark.slow def test_adapter_map_snapshot_tables( sushi_test_project: Project, runtime_renderer: t.Callable, @@ -320,6 +325,7 @@ def test_quote_as_configured(): adapter.quote_as_configured("foo", "database") == "foo" +@pytest.mark.slow def test_adapter_get_relation_normalization( sushi_test_project: Project, runtime_renderer: t.Callable ): @@ -352,6 +358,7 @@ def test_adapter_get_relation_normalization( ) +@pytest.mark.slow def test_adapter_expand_target_column_types( sushi_test_project: Project, runtime_renderer: t.Callable, mocker: MockerFixture ): diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 695c745c1d..1483225987 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -6,6 +6,8 @@ import pytest from dbt.adapters.base import BaseRelation, Column from pytest_mock import MockerFixture + +from sqlmesh.core.audit import StandaloneAudit from sqlmesh.core.config import Config, ModelDefaultsConfig from sqlmesh.core.dialect import jinja_query from sqlmesh.core.model import SqlModel @@ -82,7 +84,7 @@ def test_update(current: t.Dict[str, t.Any], new: t.Dict[str, t.Any], expected: assert {k: v for k, v in config.dict().items() if k in expected} == expected -def test_model_to_sqlmesh_fields(): +def test_model_to_sqlmesh_fields(dbt_dummy_postgres_config: PostgresConfig): model_config = ModelConfig( name="name", package_name="package", @@ -111,7 +113,7 @@ def test_model_to_sqlmesh_fields(): ) context = DbtContext() context.project_name = "Foo" - context.target = DuckDbConfig(name="target", schema="foo") + context.target = dbt_dummy_postgres_config model = model_config.to_sqlmesh(context) assert isinstance(model, SqlModel) @@ -119,7 +121,7 @@ def test_model_to_sqlmesh_fields(): assert model.description == "test model" assert ( model.render_query_or_raise().sql() - == 'SELECT 1 AS "a" FROM "memory"."foo"."table" AS "table"' + == 'SELECT 1 AS "a" FROM "dbname"."foo"."table" AS "table"' ) assert model.start == "Jan 1 2023" assert [col.sql() for col in model.partitioned_by] == ['"a"'] @@ -127,7 +129,7 @@ def test_model_to_sqlmesh_fields(): assert model.cron == "@hourly" assert model.interval_unit.value == "five_minute" assert model.stamp == "bar" - assert model.dialect == "duckdb" + assert model.dialect == "postgres" assert model.owner == "Sally" assert model.tags == ["test", "incremental"] kind = t.cast(IncrementalByUniqueKeyKind, model.kind) @@ -136,8 +138,8 @@ def test_model_to_sqlmesh_fields(): assert kind.on_destructive_change == OnDestructiveChange.ALLOW assert kind.on_additive_change == OnAdditiveChange.ALLOW assert ( - kind.merge_filter.sql(dialect=model.dialect) - == """55 > "__MERGE_SOURCE__"."b" AND "__MERGE_TARGET__"."session_start" > CURRENT_DATE + INTERVAL '7' DAY""" + kind.merge_filter.sql(dialect=model.dialect) # type: ignore + == """55 > "__MERGE_SOURCE__"."b" AND "__MERGE_TARGET__"."session_start" > CURRENT_DATE + INTERVAL '7'""" ) model = model_config.update_with({"dialect": "snowflake"}).to_sqlmesh(context) @@ -147,7 +149,7 @@ def test_model_to_sqlmesh_fields(): sqlmesh_config=Config(model_defaults=ModelDefaultsConfig(dialect="bigquery")) ) bq_default_context.project_name = "Foo" - bq_default_context.target = DuckDbConfig(name="target", schema="foo") + bq_default_context.target = dbt_dummy_postgres_config model_config.cluster_by = ["a", "`b`"] model = model_config.to_sqlmesh(bq_default_context) assert model.dialect == "bigquery" @@ -229,7 +231,7 @@ def test_test_to_sqlmesh_fields(): assert audit.dialect == "bigquery" -def test_singular_test_to_standalone_audit(): +def test_singular_test_to_standalone_audit(dbt_dummy_postgres_config: PostgresConfig): sql = "SELECT * FROM FOO.BAR WHERE cost > 100" test_config = TestConfig( name="bar_test", @@ -251,8 +253,8 @@ def test_singular_test_to_standalone_audit(): context = DbtContext() context.add_models({model.name: model}) context._project_name = "Foo" - context.target = DuckDbConfig(name="target", schema="foo") - standalone_audit = test_config.to_sqlmesh(context) + context.target = dbt_dummy_postgres_config + standalone_audit = t.cast(StandaloneAudit, test_config.to_sqlmesh(context)) assert standalone_audit.name == "bar_test" assert standalone_audit.description == "test description" @@ -260,12 +262,12 @@ def test_singular_test_to_standalone_audit(): assert standalone_audit.stamp == "bump" assert standalone_audit.cron == "@monthly" assert standalone_audit.interval_unit.value == "day" - assert standalone_audit.dialect == "duckdb" + assert standalone_audit.dialect == "postgres" assert standalone_audit.query == jinja_query(sql) - assert standalone_audit.depends_on == {'"memory"."foo"."bar"'} + assert standalone_audit.depends_on == {'"dbname"."foo"."bar"'} test_config.dialect_ = "bigquery" - standalone_audit = test_config.to_sqlmesh(context) + standalone_audit = t.cast(StandaloneAudit, test_config.to_sqlmesh(context)) assert standalone_audit.dialect == "bigquery" @@ -305,6 +307,7 @@ def test_model_config_sql_no_config(): ) +@pytest.mark.slow def test_variables(assert_exp_eq, sushi_test_project): # Case 1: using an undefined variable without a default value defined_variables = {} @@ -384,6 +387,7 @@ def test_variables(assert_exp_eq, sushi_test_project): assert sushi_test_project.packages["customers"].variables == expected_customer_variables +@pytest.mark.slow def test_nested_variables(sushi_test_project): model_config = ModelConfig( alias="sushi.test_nested", @@ -396,6 +400,7 @@ def test_nested_variables(sushi_test_project): assert sqlmesh_model.jinja_macros.global_objs["vars"]["nested_vars"] == {"some_nested_var": 2} +@pytest.mark.slow def test_source_config(sushi_test_project: Project): source_configs = sushi_test_project.packages["sushi"].sources assert set(source_configs) == { @@ -426,6 +431,7 @@ def test_source_config(sushi_test_project: Project): ) +@pytest.mark.slow def test_seed_config(sushi_test_project: Project, mocker: MockerFixture): seed_configs = sushi_test_project.packages["sushi"].seeds assert set(seed_configs) == {"waiter_names"} @@ -955,6 +961,7 @@ class CustomDbtLoader(DbtLoader): @pytest.mark.cicdonly +@pytest.mark.slow def test_db_type_to_relation_class(): from dbt.adapters.bigquery.relation import BigQueryRelation from dbt.adapters.databricks.relation import DatabricksRelation @@ -978,6 +985,7 @@ def test_db_type_to_relation_class(): @pytest.mark.cicdonly +@pytest.mark.slow def test_db_type_to_column_class(): from dbt.adapters.bigquery import BigQueryColumn from dbt.adapters.databricks.column import DatabricksColumn @@ -1013,6 +1021,7 @@ def test_variable_override(): assert project.packages["sushi"].variables["yet_another_var"] == 2 +@pytest.mark.slow def test_depends_on(assert_exp_eq, sushi_test_project): # Case 1: using an undefined variable without a default value context = sushi_test_project.context diff --git a/tests/dbt/test_integration.py b/tests/dbt/test_integration.py index 45c1422395..ee8c486ab2 100644 --- a/tests/dbt/test_integration.py +++ b/tests/dbt/test_integration.py @@ -7,7 +7,12 @@ import pandas as pd # noqa: TID253 import pytest -from dbt.cli.main import dbtRunner + +from sqlmesh.dbt.util import DBT_VERSION + +if DBT_VERSION >= (1, 5, 0): + from dbt.cli.main import dbtRunner # type: ignore + import time_machine from sqlmesh import Context @@ -303,6 +308,9 @@ def test_scd_type_2_by_time( test_type: TestType, invalidate_hard_deletes: bool, ): + if test_type.is_dbt_runtime and DBT_VERSION < (1, 5, 0): + pytest.skip("The dbt version being tested doesn't support the dbtRunner so skipping.") + run, adapter, context = self._init_test( create_scd_type_2_dbt_project, create_scd_type_2_sqlmesh_project, diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index efbd2687fd..7ad67c3585 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -63,9 +63,12 @@ def test_manifest_helper(caplog): assert models["items_no_hard_delete_snapshot"].invalidate_hard_deletes is False # Test versioned models - assert models["waiter_revenue_by_day_v1"].version == 1 - assert models["waiter_revenue_by_day_v2"].version == 2 - assert "waiter_revenue_by_day" not in models + if DBT_VERSION >= (1, 5, 0): + assert models["waiter_revenue_by_day_v1"].version == 1 + assert models["waiter_revenue_by_day_v2"].version == 2 + assert "waiter_revenue_by_day" not in models + else: + assert "waiter_revenue_by_day" in models waiter_as_customer_by_day_config = models["waiter_as_customer_by_day"] assert waiter_as_customer_by_day_config.dependencies == Dependencies( @@ -77,7 +80,10 @@ def test_manifest_helper(caplog): assert waiter_as_customer_by_day_config.cluster_by == ["ds"] assert waiter_as_customer_by_day_config.time_column == "ds" - waiter_revenue_by_day_config = models["waiter_revenue_by_day_v2"] + if DBT_VERSION >= (1, 5, 0): + waiter_revenue_by_day_config = models["waiter_revenue_by_day_v2"] + else: + waiter_revenue_by_day_config = models["waiter_revenue_by_day"] assert waiter_revenue_by_day_config.dependencies == Dependencies( macros={ MacroReference(name="dynamic_var_name_dependency"), @@ -218,7 +224,12 @@ def test_source_meta_external_location(): sources["parquet_file.items"].relation_info, api.Relation, api.quote_policy ) assert relation.identifier == "items" - assert relation.render() == "read_parquet('path/to/external/items.parquet')" + expected = ( + "read_parquet('path/to/external/items.parquet')" + if DBT_VERSION >= (1, 4, 0) + else '"main"."parquet_file".items' + ) + assert relation.render() == expected @pytest.mark.xdist_group("dbt_manifest") diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index 030f2ec723..df9f229900 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -6,6 +6,7 @@ from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.model import ModelConfig +from sqlmesh.dbt.target import PostgresConfig from sqlmesh.dbt.test import TestConfig from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.yaml import YAML @@ -50,7 +51,10 @@ def test_model_test_circular_references() -> None: downstream_model.check_for_circular_test_refs(context) -def test_load_invalid_ref_audit_constraints(tmp_path: Path, caplog) -> None: +@pytest.mark.slow +def test_load_invalid_ref_audit_constraints( + tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig +) -> None: yaml = YAML() dbt_project_dir = tmp_path / "dbt" dbt_project_dir.mkdir() diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 33c7132551..320b036e6d 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -6,9 +6,15 @@ from pathlib import Path from unittest.mock import patch +from sqlmesh.dbt.util import DBT_VERSION + import pytest from dbt.adapters.base import BaseRelation -from dbt.exceptions import CompilationError + +if DBT_VERSION >= (1, 4, 0): + from dbt.exceptions import CompilationError +else: + from dbt.exceptions import CompilationException as CompilationError # type: ignore import time_machine from pytest_mock.plugin import MockerFixture from sqlglot import exp, parse_one @@ -47,8 +53,14 @@ from sqlmesh.dbt.model import Materialization, ModelConfig from sqlmesh.dbt.project import Project from sqlmesh.dbt.relation import Policy -from sqlmesh.dbt.seed import SeedConfig, Integer -from sqlmesh.dbt.target import BigQueryConfig, DuckDbConfig, SnowflakeConfig, ClickhouseConfig +from sqlmesh.dbt.seed import SeedConfig +from sqlmesh.dbt.target import ( + BigQueryConfig, + DuckDbConfig, + SnowflakeConfig, + ClickhouseConfig, + PostgresConfig, +) from sqlmesh.dbt.test import TestConfig from sqlmesh.utils.errors import ConfigError, MacroEvalError, SQLMeshError from sqlmesh.utils.jinja import MacroReference @@ -56,9 +68,9 @@ pytestmark = [pytest.mark.dbt, pytest.mark.slow] -def test_model_name(): +def test_model_name(dbt_dummy_postgres_config: PostgresConfig): context = DbtContext() - context._target = DuckDbConfig(name="duckdb", schema="foo") + context._target = dbt_dummy_postgres_config assert ModelConfig(schema="foo", path="models/bar.sql").canonical_name(context) == "foo.bar" assert ( ModelConfig(schema="foo", path="models/bar.sql", alias="baz").canonical_name(context) @@ -66,10 +78,9 @@ def test_model_name(): ) assert ( ModelConfig( - database="memory", schema="foo", path="models/bar.sql", alias="baz" + database="dbname", schema="foo", path="models/bar.sql", alias="baz" ).canonical_name(context) == "foo.baz" - == "foo.baz" ) assert ( ModelConfig( @@ -680,7 +691,9 @@ def test_seed_column_inference(tmp_path): context.target = DuckDbConfig(name="target", schema="test") sqlmesh_seed = seed.to_sqlmesh(context) assert sqlmesh_seed.columns_to_types == { - "int_col": exp.DataType.build("int"), + "int_col": exp.DataType.build("int") + if DBT_VERSION >= (1, 8, 0) + else exp.DataType.build("double"), "double_col": exp.DataType.build("double"), "datetime_col": exp.DataType.build("datetime"), "date_col": exp.DataType.build("date"), @@ -793,6 +806,12 @@ def test_seed_column_order(tmp_path): def test_agate_integer_cast(): + # Not all dbt versions have agate.Integer + if DBT_VERSION < (1, 7, 0): + pytest.skip("agate.Integer not available") + + from sqlmesh.dbt.seed import Integer + agate_integer = Integer(null_values=("null", "")) assert agate_integer.cast("1") == 1 assert agate_integer.cast(1) == 1 From a026f1334520c961654ff73873459ae13b8ed421 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:39:21 +0200 Subject: [PATCH 0763/1056] chore: testing docs additions (#5221) Co-authored-by: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> --- tests/dbt/test_docs.py | 28 +++++++++++++++++++ .../fixtures/dbt/sushi_test/models/schema.yml | 2 ++ .../sushi_test/models/waiters_doc_block.md | 4 +++ 3 files changed, 34 insertions(+) create mode 100644 tests/dbt/test_docs.py create mode 100644 tests/fixtures/dbt/sushi_test/models/waiters_doc_block.md diff --git a/tests/dbt/test_docs.py b/tests/dbt/test_docs.py new file mode 100644 index 0000000000..7c21edb970 --- /dev/null +++ b/tests/dbt/test_docs.py @@ -0,0 +1,28 @@ +from pathlib import Path +import pytest + +from sqlmesh.core.config.model import ModelDefaultsConfig +from sqlmesh.dbt.context import DbtContext +from sqlmesh.dbt.manifest import ManifestHelper +from sqlmesh.dbt.profile import Profile + + +pytestmark = pytest.mark.dbt + + +@pytest.mark.xdist_group("dbt_manifest") +def test_docs_inline(): + project_path = Path("tests/fixtures/dbt/sushi_test") + profile = Profile.load(DbtContext(project_path)) + + helper = ManifestHelper( + project_path, + project_path, + "sushi", + profile.target, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), + ) + # Inline description in yaml + assert helper.models()["waiters"].description == "waiters docs block" + # Docs block from .md file + assert helper.models()["top_waiters"].description == "description of top waiters" diff --git a/tests/fixtures/dbt/sushi_test/models/schema.yml b/tests/fixtures/dbt/sushi_test/models/schema.yml index a64ce3c1fc..ac99269207 100644 --- a/tests/fixtures/dbt/sushi_test/models/schema.yml +++ b/tests/fixtures/dbt/sushi_test/models/schema.yml @@ -2,6 +2,7 @@ version: 2 models: - name: top_waiters + description: description of top waiters columns: - name: waiter_id data_type: int @@ -18,6 +19,7 @@ models: warn_after: {count: 8, period: hour} error_after: {count: 9, period: hour} - name: waiters + description: '{{ doc("waiters") }}' - name: waiter_as_customer_by_day - name: waiter_revenue_by_day versions: diff --git a/tests/fixtures/dbt/sushi_test/models/waiters_doc_block.md b/tests/fixtures/dbt/sushi_test/models/waiters_doc_block.md new file mode 100644 index 0000000000..99d1582c91 --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/models/waiters_doc_block.md @@ -0,0 +1,4 @@ +{% docs waiters %} +waiters docs block +{% enddocs %} + From f4aa6d91f743e210fd5e2ffe7e17675900edd2e7 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 26 Aug 2025 19:10:37 +0300 Subject: [PATCH 0764/1056] Chore: Reinstate test cli flag to false (#5230) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7e532f75f0..9a4fd0632c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -268,7 +268,7 @@ markers = [ ] addopts = "-n 0 --dist=loadgroup" asyncio_default_fixture_loop_scope = "session" -log_cli = true # Set this to true to enable logging during tests +log_cli = false # Set this to true to enable logging during tests log_cli_format = "%(asctime)s.%(msecs)03d %(filename)s:%(lineno)d %(levelname)s %(message)s" log_cli_level = "INFO" filterwarnings = [ From 66cd077fe597f0e0d30e73cf779c0c2ad9647624 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 26 Aug 2025 19:42:44 +0300 Subject: [PATCH 0765/1056] Fix: Move before all statements execution before snapshot creation logic (#5229) --- sqlmesh/core/scheduler.py | 22 ++++----- tests/core/test_scheduler.py | 87 ++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index 2cbf769ea2..7a653877ae 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -457,17 +457,6 @@ def run_merged_intervals( audit_only=audit_only, ) - snapshots_to_create = { - s.snapshot_id - for s in self.snapshot_evaluator.get_snapshots_to_create( - selected_snapshots, deployability_index - ) - } - - dag = self._dag( - batched_intervals, snapshot_dag=snapshot_dag, snapshots_to_create=snapshots_to_create - ) - if run_environment_statements: environment_statements = self.state_sync.get_environment_statements( environment_naming_info.name @@ -484,6 +473,17 @@ def run_merged_intervals( execution_time=execution_time, ) + snapshots_to_create = { + s.snapshot_id + for s in self.snapshot_evaluator.get_snapshots_to_create( + selected_snapshots, deployability_index + ) + } + + dag = self._dag( + batched_intervals, snapshot_dag=snapshot_dag, snapshots_to_create=snapshots_to_create + ) + def run_node(node: SchedulingUnit) -> None: if circuit_breaker and circuit_breaker(): raise CircuitBreakerError() diff --git a/tests/core/test_scheduler.py b/tests/core/test_scheduler.py index b74aa3480e..b894f60f58 100644 --- a/tests/core/test_scheduler.py +++ b/tests/core/test_scheduler.py @@ -7,6 +7,7 @@ from sqlmesh.core.context import Context, ExecutionContext from sqlmesh.core.environment import EnvironmentNamingInfo +from sqlmesh.core.macros import RuntimeStage from sqlmesh.core.model import load_sql_based_model from sqlmesh.core.model.definition import AuditResult, SqlModel from sqlmesh.core.model.kind import ( @@ -932,3 +933,89 @@ def test_scd_type_2_batch_size( # Verify batches match expectations assert batches == expected_batches + + +def test_before_all_environment_statements_called_first(mocker: MockerFixture, make_snapshot): + model = SqlModel( + name="test.model_items", + query=parse_one("SELECT id, ds FROM raw.items"), + kind=IncrementalByTimeRangeKind(time_column=TimeColumn(column="ds")), + ) + snapshot = make_snapshot(model) + + # to track the order of calls + call_order = [] + + mock_state_sync = mocker.MagicMock() + mock_state_sync.get_environment_statements.return_value = [ + ("CREATE TABLE IF NOT EXISTS test_table (id INT)", RuntimeStage.BEFORE_ALL) + ] + + def record_get_environment_statements(*args, **kwargs): + call_order.append("get_environment_statements") + return mock_state_sync.get_environment_statements.return_value + + mock_state_sync.get_environment_statements.side_effect = record_get_environment_statements + + mock_snapshot_evaluator = mocker.MagicMock() + mock_adapter = mocker.MagicMock() + mock_snapshot_evaluator.adapter = mock_adapter + + def record_get_snapshots_to_create(*args, **kwargs): + call_order.append("get_snapshots_to_create") + return [] + + mock_snapshot_evaluator.get_snapshots_to_create.side_effect = record_get_snapshots_to_create + + mock_execute_env_statements = mocker.patch( + "sqlmesh.core.scheduler.execute_environment_statements" + ) + + def record_execute_environment_statements(*args, **kwargs): + call_order.append("execute_environment_statements") + + mock_execute_env_statements.side_effect = record_execute_environment_statements + + scheduler = Scheduler( + snapshots=[snapshot], + snapshot_evaluator=mock_snapshot_evaluator, + state_sync=mock_state_sync, + default_catalog=None, + ) + merged_intervals = { + snapshot: [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + ], + } + + deployability_index = DeployabilityIndex.create([snapshot]) + environment_naming_info = EnvironmentNamingInfo(name="test_env") + + scheduler.run_merged_intervals( + merged_intervals=merged_intervals, + deployability_index=deployability_index, + environment_naming_info=environment_naming_info, + run_environment_statements=True, + ) + + mock_state_sync.get_environment_statements.assert_called_once_with("test_env") + mock_snapshot_evaluator.get_snapshots_to_create.assert_called_once() + + # execute_environment_statements is called twice + assert mock_execute_env_statements.call_count == 2 + + # first for before all and second for after all + first_call = mock_execute_env_statements.call_args_list[0] + assert first_call.kwargs["runtime_stage"] == RuntimeStage.BEFORE_ALL + second_call = mock_execute_env_statements.call_args_list[1] + assert second_call.kwargs["runtime_stage"] == RuntimeStage.AFTER_ALL + + assert "get_environment_statements" in call_order + assert "execute_environment_statements" in call_order + assert "get_snapshots_to_create" in call_order + + # Verify the before all environment statements are called first before get_snapshots_to_create + env_statements_idx = call_order.index("get_environment_statements") + execute_env_idx = call_order.index("execute_environment_statements") + snapshots_to_create_idx = call_order.index("get_snapshots_to_create") + assert env_statements_idx < execute_env_idx < snapshots_to_create_idx From ff9305cc2f2fef2ffa59888689c193f97b554370 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Tue, 26 Aug 2025 20:52:43 +0000 Subject: [PATCH 0766/1056] feat: track rows processed during model evaluation (#5162) --- .circleci/continue_config.yml | 4 +- docs/integrations/engines/snowflake.md | 8 ++ pyproject.toml | 1 + sqlmesh/core/console.py | 89 ++++++++++++----- sqlmesh/core/engine_adapter/athena.py | 1 + sqlmesh/core/engine_adapter/base.py | 72 ++++++++++++-- sqlmesh/core/engine_adapter/base_postgres.py | 1 + sqlmesh/core/engine_adapter/bigquery.py | 19 ++++ sqlmesh/core/engine_adapter/clickhouse.py | 4 +- sqlmesh/core/engine_adapter/duckdb.py | 2 + sqlmesh/core/engine_adapter/mssql.py | 1 + sqlmesh/core/engine_adapter/mysql.py | 1 + sqlmesh/core/engine_adapter/postgres.py | 1 + sqlmesh/core/engine_adapter/redshift.py | 4 +- sqlmesh/core/engine_adapter/snowflake.py | 3 + sqlmesh/core/engine_adapter/spark.py | 2 + sqlmesh/core/engine_adapter/trino.py | 3 + sqlmesh/core/scheduler.py | 7 ++ sqlmesh/core/snapshot/__init__.py | 1 + sqlmesh/core/snapshot/definition.py | 12 ++- sqlmesh/core/snapshot/evaluator.py | 38 +++++--- sqlmesh/core/snapshot/execution_tracker.py | 97 +++++++++++++++++++ sqlmesh/core/state_sync/db/environment.py | 2 + sqlmesh/core/state_sync/db/interval.py | 2 + sqlmesh/core/state_sync/db/migrator.py | 4 +- sqlmesh/core/state_sync/db/snapshot.py | 2 + sqlmesh/core/state_sync/db/version.py | 1 + .../integration/test_integration.py | 57 ++++++++++- .../integration/test_integration_snowflake.py | 30 ++++++ tests/core/test_execution_tracker.py | 50 ++++++++++ tests/core/test_integration.py | 28 +++--- tests/core/test_snapshot_evaluator.py | 20 +++- tests/core/test_table_diff.py | 12 +-- tests/core/test_test.py | 42 ++++---- web/server/console.py | 2 + 35 files changed, 525 insertions(+), 98 deletions(-) create mode 100644 sqlmesh/core/snapshot/execution_tracker.py create mode 100644 tests/core/test_execution_tracker.py diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 8f8324a2a0..e21f3d869b 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -239,7 +239,7 @@ jobs: - checkout - run: name: Install OS-level dependencies - command: ./.circleci/install-prerequisites.sh "<< parameters.engine >>" + command: ./.circleci/install-prerequisites.sh "<< parameters.engine >>" - run: name: Generate database name command: | @@ -307,7 +307,7 @@ workflows: - redshift - bigquery - clickhouse-cloud - - athena + - athena - fabric - gcp-postgres filters: diff --git a/docs/integrations/engines/snowflake.md b/docs/integrations/engines/snowflake.md index 30de0bfd14..fc2ccbd6bb 100644 --- a/docs/integrations/engines/snowflake.md +++ b/docs/integrations/engines/snowflake.md @@ -250,6 +250,14 @@ And confirm that our schemas and objects exist in the Snowflake catalog: Congratulations - your SQLMesh project is up and running on Snowflake! +### Where are the row counts? + +SQLMesh reports the number of rows processed by each model in its `plan` and `run` terminal output. + +However, due to limitations in the Snowflake Python connector, row counts cannot be determined for `CREATE TABLE AS` statements. Therefore, SQLMesh does not report row counts for certain model kinds, such as `FULL` models. + +Learn more about the connector limitation [on Github](https://github.com/snowflakedb/snowflake-connector-python/issues/645). + ## Local/Built-in Scheduler **Engine Adapter Type**: `snowflake` diff --git a/pyproject.toml b/pyproject.toml index 9a4fd0632c..f371cdee0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "croniter", "duckdb>=0.10.0,!=0.10.3", "dateparser<=1.2.1", + "humanize", "hyperscript>=0.1.0", "importlib-metadata; python_version<'3.12'", "ipywidgets", diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index e046e17630..0907b39987 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -7,6 +7,7 @@ import uuid import logging import textwrap +from humanize import metric, naturalsize from itertools import zip_longest from pathlib import Path from hyperscript import h @@ -39,6 +40,7 @@ SnapshotInfoLike, ) from sqlmesh.core.snapshot.definition import Interval, Intervals, SnapshotTableInfo +from sqlmesh.core.snapshot.execution_tracker import QueryExecutionStats from sqlmesh.core.test import ModelTest from sqlmesh.utils import rich as srich from sqlmesh.utils import Verbosity @@ -439,6 +441,7 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + execution_stats: t.Optional[QueryExecutionStats] = None, auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: """Updates the snapshot evaluation progress.""" @@ -587,6 +590,7 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + execution_stats: t.Optional[QueryExecutionStats] = None, auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: pass @@ -1032,7 +1036,9 @@ def start_evaluation_progress( # determine column widths self.evaluation_column_widths["annotation"] = ( - _calculate_annotation_str_len(batched_intervals, self.AUDIT_PADDING) + _calculate_annotation_str_len( + batched_intervals, self.AUDIT_PADDING, len(" (123.4m rows, 123.4 KiB)") + ) + 3 # brackets and opening escape backslash ) self.evaluation_column_widths["name"] = max( @@ -1077,6 +1083,7 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + execution_stats: t.Optional[QueryExecutionStats] = None, auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: """Update the snapshot evaluation progress.""" @@ -1097,7 +1104,7 @@ def update_snapshot_evaluation_progress( ).ljust(self.evaluation_column_widths["name"]) annotation = _create_evaluation_model_annotation( - snapshot, _format_evaluation_model_interval(snapshot, interval) + snapshot, _format_evaluation_model_interval(snapshot, interval), execution_stats ) audits_str = "" if num_audits_passed: @@ -3668,6 +3675,7 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + execution_stats: t.Optional[QueryExecutionStats] = None, auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: view_name, loaded_batches = self.evaluation_batch_progress[snapshot.snapshot_id] @@ -3838,6 +3846,7 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + execution_stats: t.Optional[QueryExecutionStats] = None, auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: message = f"Evaluated {snapshot.name} | batch={batch_idx} | duration={duration_ms}ms | num_audits_passed={num_audits_passed} | num_audits_failed={num_audits_failed}" @@ -4169,33 +4178,62 @@ def _format_evaluation_model_interval(snapshot: Snapshot, interval: Interval) -> return "" -def _create_evaluation_model_annotation(snapshot: Snapshot, interval_info: t.Optional[str]) -> str: +def _create_evaluation_model_annotation( + snapshot: Snapshot, + interval_info: t.Optional[str], + execution_stats: t.Optional[QueryExecutionStats], +) -> str: + annotation = None + execution_stats_str = "" + if execution_stats: + rows_processed = execution_stats.total_rows_processed + if rows_processed: + # 1.00 and 1.0 to 1 + rows_processed_str = metric(rows_processed).replace(".00", "").replace(".0", "") + execution_stats_str += f"{rows_processed_str} row{'s' if rows_processed > 1 else ''}" + + bytes_processed = execution_stats.total_bytes_processed + execution_stats_str += ( + f"{', ' if execution_stats_str else ''}{naturalsize(bytes_processed, binary=True)}" + if bytes_processed + else "" + ) + execution_stats_str = f" ({execution_stats_str})" if execution_stats_str else "" + if snapshot.is_audit: - return "run standalone audit" - if snapshot.is_model and snapshot.model.kind.is_external: - return "run external audits" - if snapshot.model.kind.is_seed: - return "insert seed file" - if snapshot.model.kind.is_full: - return "full refresh" - if snapshot.model.kind.is_view: - return "recreate view" - if snapshot.model.kind.is_incremental_by_unique_key: - return "insert/update rows" - if snapshot.model.kind.is_incremental_by_partition: - return "insert partitions" - - return interval_info if interval_info else "" - - -def _calculate_interval_str_len(snapshot: Snapshot, intervals: t.List[Interval]) -> int: + annotation = "run standalone audit" + if snapshot.is_model: + if snapshot.model.kind.is_external: + annotation = "run external audits" + if snapshot.model.kind.is_view: + annotation = "recreate view" + if snapshot.model.kind.is_seed: + annotation = f"insert seed file{execution_stats_str}" + if snapshot.model.kind.is_full: + annotation = f"full refresh{execution_stats_str}" + if snapshot.model.kind.is_incremental_by_unique_key: + annotation = f"insert/update rows{execution_stats_str}" + if snapshot.model.kind.is_incremental_by_partition: + annotation = f"insert partitions{execution_stats_str}" + + if annotation: + return annotation + + return f"{interval_info}{execution_stats_str}" if interval_info else "" + + +def _calculate_interval_str_len( + snapshot: Snapshot, + intervals: t.List[Interval], + execution_stats: t.Optional[QueryExecutionStats] = None, +) -> int: interval_str_len = 0 for interval in intervals: interval_str_len = max( interval_str_len, len( _create_evaluation_model_annotation( - snapshot, _format_evaluation_model_interval(snapshot, interval) + snapshot, _format_evaluation_model_interval(snapshot, interval), execution_stats ) ), ) @@ -4248,13 +4286,16 @@ def _calculate_audit_str_len(snapshot: Snapshot, audit_padding: int = 0) -> int: def _calculate_annotation_str_len( - batched_intervals: t.Dict[Snapshot, t.List[Interval]], audit_padding: int = 0 + batched_intervals: t.Dict[Snapshot, t.List[Interval]], + audit_padding: int = 0, + execution_stats_len: int = 0, ) -> int: annotation_str_len = 0 for snapshot, intervals in batched_intervals.items(): annotation_str_len = max( annotation_str_len, _calculate_interval_str_len(snapshot, intervals) - + _calculate_audit_str_len(snapshot, audit_padding), + + _calculate_audit_str_len(snapshot, audit_padding) + + execution_stats_len, ) return annotation_str_len diff --git a/sqlmesh/core/engine_adapter/athena.py b/sqlmesh/core/engine_adapter/athena.py index 48b9e4ad4e..aa8a5ce0c1 100644 --- a/sqlmesh/core/engine_adapter/athena.py +++ b/sqlmesh/core/engine_adapter/athena.py @@ -45,6 +45,7 @@ class AthenaEngineAdapter(PandasNativeFetchDFSupportMixin, RowDiffMixin): # >>> self._execute('/* test */ DESCRIBE foo') # pyathena.error.OperationalError: FAILED: ParseException line 1:0 cannot recognize input near '/' '*' 'test' ATTACH_CORRELATION_ID = False + SUPPORTS_QUERY_EXECUTION_TRACKING = True SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["DATABASE", "SCHEMA"] def __init__( diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index fe19f7df0f..2901831940 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -40,6 +40,7 @@ ) from sqlmesh.core.model.kind import TimeColumn from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation +from sqlmesh.core.snapshot.execution_tracker import QueryExecutionTracker from sqlmesh.utils import ( CorrelationId, columns_to_types_all_known, @@ -117,6 +118,7 @@ class EngineAdapter: QUOTE_IDENTIFIERS_IN_VIEWS = True MAX_IDENTIFIER_LENGTH: t.Optional[int] = None ATTACH_CORRELATION_ID = True + SUPPORTS_QUERY_EXECUTION_TRACKING = False def __init__( self, @@ -133,6 +135,7 @@ def __init__( shared_connection: bool = False, correlation_id: t.Optional[CorrelationId] = None, schema_differ_overrides: t.Optional[t.Dict[str, t.Any]] = None, + query_execution_tracker: t.Optional[QueryExecutionTracker] = None, **kwargs: t.Any, ): self.dialect = dialect.lower() or self.DIALECT @@ -156,11 +159,16 @@ def __init__( self._multithreaded = multithreaded self.correlation_id = correlation_id self._schema_differ_overrides = schema_differ_overrides + self._query_execution_tracker = query_execution_tracker def with_settings(self, **kwargs: t.Any) -> EngineAdapter: extra_kwargs = { "null_connection": True, "execute_log_level": kwargs.pop("execute_log_level", self._execute_log_level), + "correlation_id": kwargs.pop("correlation_id", self.correlation_id), + "query_execution_tracker": kwargs.pop( + "query_execution_tracker", self._query_execution_tracker + ), **self._extra_config, **kwargs, } @@ -854,6 +862,7 @@ def _create_table_from_source_queries( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, + track_rows_processed: bool = True, **kwargs: t.Any, ) -> None: table = exp.to_table(table_name) @@ -899,11 +908,15 @@ def _create_table_from_source_queries( replace=replace, table_description=table_description, table_kind=table_kind, + track_rows_processed=track_rows_processed, **kwargs, ) else: self._insert_append_query( - table_name, query, target_columns_to_types or self.columns(table) + table_name, + query, + target_columns_to_types or self.columns(table), + track_rows_processed=track_rows_processed, ) # Register comments with commands if the engine supports comments and we weren't able to @@ -927,6 +940,7 @@ def _create_table( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, + track_rows_processed: bool = True, **kwargs: t.Any, ) -> None: self.execute( @@ -943,7 +957,8 @@ def _create_table( ), table_kind=table_kind, **kwargs, - ) + ), + track_rows_processed=track_rows_processed, ) def _build_create_table_exp( @@ -1431,6 +1446,7 @@ def insert_append( table_name: TableName, query_or_df: QueryOrDF, target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + track_rows_processed: bool = True, source_columns: t.Optional[t.List[str]] = None, ) -> None: source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types( @@ -1439,19 +1455,27 @@ def insert_append( target_table=table_name, source_columns=source_columns, ) - self._insert_append_source_queries(table_name, source_queries, target_columns_to_types) + self._insert_append_source_queries( + table_name, source_queries, target_columns_to_types, track_rows_processed + ) def _insert_append_source_queries( self, table_name: TableName, source_queries: t.List[SourceQuery], target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + track_rows_processed: bool = True, ) -> None: with self.transaction(condition=len(source_queries) > 0): target_columns_to_types = target_columns_to_types or self.columns(table_name) for source_query in source_queries: with source_query as query: - self._insert_append_query(table_name, query, target_columns_to_types) + self._insert_append_query( + table_name, + query, + target_columns_to_types, + track_rows_processed=track_rows_processed, + ) def _insert_append_query( self, @@ -1459,10 +1483,14 @@ def _insert_append_query( query: Query, target_columns_to_types: t.Dict[str, exp.DataType], order_projections: bool = True, + track_rows_processed: bool = True, ) -> None: if order_projections: query = self._order_projections_and_filter(query, target_columns_to_types) - self.execute(exp.insert(query, table_name, columns=list(target_columns_to_types))) + self.execute( + exp.insert(query, table_name, columns=list(target_columns_to_types)), + track_rows_processed=track_rows_processed, + ) def insert_overwrite_by_partition( self, @@ -1604,7 +1632,7 @@ def _insert_overwrite_by_condition( ) if insert_overwrite_strategy.is_replace_where: insert_exp.set("where", where or exp.true()) - self.execute(insert_exp) + self.execute(insert_exp, track_rows_processed=True) def update_table( self, @@ -1625,7 +1653,9 @@ def _merge( using = exp.alias_( exp.Subquery(this=query), alias=MERGE_SOURCE_ALIAS, copy=False, table=True ) - self.execute(exp.Merge(this=this, using=using, on=on, whens=whens)) + self.execute( + exp.Merge(this=this, using=using, on=on, whens=whens), track_rows_processed=True + ) def scd_type_2_by_time( self, @@ -2374,6 +2404,7 @@ def execute( expressions: t.Union[str, exp.Expression, t.Sequence[exp.Expression]], ignore_unsupported_errors: bool = False, quote_identifiers: bool = True, + track_rows_processed: bool = False, **kwargs: t.Any, ) -> None: """Execute a sql query.""" @@ -2395,7 +2426,7 @@ def execute( expression=e if isinstance(e, exp.Expression) else None, quote_identifiers=quote_identifiers, ) - self._execute(sql, **kwargs) + self._execute(sql, track_rows_processed, **kwargs) def _attach_correlation_id(self, sql: str) -> str: if self.ATTACH_CORRELATION_ID and self.correlation_id: @@ -2420,9 +2451,29 @@ def _log_sql( logger.log(self._execute_log_level, "Executing SQL: %s", sql_to_log) - def _execute(self, sql: str, **kwargs: t.Any) -> None: + def _record_execution_stats( + self, sql: str, rowcount: t.Optional[int] = None, bytes_processed: t.Optional[int] = None + ) -> None: + if self._query_execution_tracker: + self._query_execution_tracker.record_execution(sql, rowcount, bytes_processed) + + def _execute(self, sql: str, track_rows_processed: bool = False, **kwargs: t.Any) -> None: self.cursor.execute(sql, **kwargs) + if ( + self.SUPPORTS_QUERY_EXECUTION_TRACKING + and track_rows_processed + and self._query_execution_tracker + and self._query_execution_tracker.is_tracking() + ): + if ( + rowcount := getattr(self.cursor, "rowcount", None) + ) is not None and rowcount is not None: + try: + self._record_execution_stats(sql, int(rowcount)) + except (TypeError, ValueError): + return + @contextlib.contextmanager def temp_table( self, @@ -2467,6 +2518,7 @@ def temp_table( exists=True, table_description=None, column_descriptions=None, + track_rows_processed=False, **kwargs, ) @@ -2718,7 +2770,7 @@ def _replace_by_key( insert_statement.set("where", delete_filter) insert_statement.set("this", exp.to_table(target_table)) - self.execute(insert_statement) + self.execute(insert_statement, track_rows_processed=True) finally: self.drop_table(temp_table) diff --git a/sqlmesh/core/engine_adapter/base_postgres.py b/sqlmesh/core/engine_adapter/base_postgres.py index 26446aacfd..c6ba7d6d62 100644 --- a/sqlmesh/core/engine_adapter/base_postgres.py +++ b/sqlmesh/core/engine_adapter/base_postgres.py @@ -24,6 +24,7 @@ class BasePostgresEngineAdapter(EngineAdapter): DEFAULT_BATCH_SIZE = 400 COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY + SUPPORTS_QUERY_EXECUTION_TRACKING = True SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA", "TABLE", "VIEW"] def columns( diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 4c8a125fa3..b3d02d8bbf 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -66,6 +66,7 @@ class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin, ClusteredByMixin, Row SUPPORTS_CLONING = True MAX_TABLE_COMMENT_LENGTH = 1024 MAX_COLUMN_COMMENT_LENGTH = 1024 + SUPPORTS_QUERY_EXECUTION_TRACKING = True SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA"] SCHEMA_DIFFER_KWARGS = { @@ -1049,6 +1050,7 @@ def _db_call(self, func: t.Callable[..., t.Any], *args: t.Any, **kwargs: t.Any) def _execute( self, sql: str, + track_rows_processed: bool = False, **kwargs: t.Any, ) -> None: """Execute a sql query.""" @@ -1094,6 +1096,23 @@ def _execute( self.cursor._set_rowcount(query_results) self.cursor._set_description(query_results.schema) + if ( + track_rows_processed + and self._query_execution_tracker + and self._query_execution_tracker.is_tracking() + ): + num_rows = None + if query_job.statement_type == "CREATE_TABLE_AS_SELECT": + # since table was just created, number rows in table == number rows processed + query_table = self.client.get_table(query_job.destination) + num_rows = query_table.num_rows + elif query_job.statement_type in ["INSERT", "DELETE", "MERGE", "UPDATE"]: + num_rows = query_job.num_dml_affected_rows + + self._query_execution_tracker.record_execution( + sql, num_rows, query_job.total_bytes_processed + ) + def _get_data_objects( self, schema_name: SchemaName, object_names: t.Optional[t.Set[str]] = None ) -> t.List[DataObject]: diff --git a/sqlmesh/core/engine_adapter/clickhouse.py b/sqlmesh/core/engine_adapter/clickhouse.py index 635e6f369b..ccffe64118 100644 --- a/sqlmesh/core/engine_adapter/clickhouse.py +++ b/sqlmesh/core/engine_adapter/clickhouse.py @@ -294,7 +294,7 @@ def _insert_overwrite_by_condition( ) try: - self.execute(existing_records_insert_exp) + self.execute(existing_records_insert_exp, track_rows_processed=True) finally: if table_partition_exp: self.drop_table(partitions_temp_table_name) @@ -489,6 +489,7 @@ def _create_table( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, + track_rows_processed: bool = True, **kwargs: t.Any, ) -> None: """Creates a table in the database. @@ -525,6 +526,7 @@ def _create_table( column_descriptions, table_kind, empty_ctas=(self.engine_run_mode.is_cloud and expression is not None), + track_rows_processed=track_rows_processed, **kwargs, ) diff --git a/sqlmesh/core/engine_adapter/duckdb.py b/sqlmesh/core/engine_adapter/duckdb.py index 4bce813610..3b057219e0 100644 --- a/sqlmesh/core/engine_adapter/duckdb.py +++ b/sqlmesh/core/engine_adapter/duckdb.py @@ -170,6 +170,7 @@ def _create_table( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, + track_rows_processed: bool = True, **kwargs: t.Any, ) -> None: catalog = self.get_current_catalog() @@ -193,6 +194,7 @@ def _create_table( table_description, column_descriptions, table_kind, + track_rows_processed=track_rows_processed, **kwargs, ) diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index 6aefd51fc0..50a67b4b37 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -53,6 +53,7 @@ class MSSQLEngineAdapter( COMMENT_CREATION_TABLE = CommentCreationTable.UNSUPPORTED COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED SUPPORTS_REPLACE_TABLE = False + SUPPORTS_QUERY_EXECUTION_TRACKING = True SCHEMA_DIFFER_KWARGS = { "parameterized_type_defaults": { exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(18, 0), (0,)], diff --git a/sqlmesh/core/engine_adapter/mysql.py b/sqlmesh/core/engine_adapter/mysql.py index e81b30e25e..26cc7c0197 100644 --- a/sqlmesh/core/engine_adapter/mysql.py +++ b/sqlmesh/core/engine_adapter/mysql.py @@ -39,6 +39,7 @@ class MySQLEngineAdapter( MAX_COLUMN_COMMENT_LENGTH = 1024 SUPPORTS_REPLACE_TABLE = False MAX_IDENTIFIER_LENGTH = 64 + SUPPORTS_QUERY_EXECUTION_TRACKING = True SCHEMA_DIFFER_KWARGS = { "parameterized_type_defaults": { exp.DataType.build("BIT", dialect=DIALECT).this: [(1,)], diff --git a/sqlmesh/core/engine_adapter/postgres.py b/sqlmesh/core/engine_adapter/postgres.py index faeb52b207..e9c212bd5f 100644 --- a/sqlmesh/core/engine_adapter/postgres.py +++ b/sqlmesh/core/engine_adapter/postgres.py @@ -35,6 +35,7 @@ class PostgresEngineAdapter( CURRENT_CATALOG_EXPRESSION = exp.column("current_catalog") SUPPORTS_REPLACE_TABLE = False MAX_IDENTIFIER_LENGTH = 63 + SUPPORTS_QUERY_EXECUTION_TRACKING = True SCHEMA_DIFFER_KWARGS = { "parameterized_type_defaults": { # DECIMAL without precision is "up to 131072 digits before the decimal point; up to 16383 digits after the decimal point" diff --git a/sqlmesh/core/engine_adapter/redshift.py b/sqlmesh/core/engine_adapter/redshift.py index 30ebc8e30d..7d14207b52 100644 --- a/sqlmesh/core/engine_adapter/redshift.py +++ b/sqlmesh/core/engine_adapter/redshift.py @@ -173,6 +173,7 @@ def _create_table_from_source_queries( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, + track_rows_processed: bool = True, **kwargs: t.Any, ) -> None: """ @@ -426,7 +427,8 @@ def resolve_target_table(expression: exp.Expression) -> exp.Expression: using=using, on=on.transform(resolve_target_table), whens=whens.transform(resolve_target_table), - ) + ), + track_rows_processed=True, ) def _normalize_decimal_value(self, expr: exp.Expression, precision: int) -> exp.Expression: diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index c5fa8540b0..8a6f5e2fcc 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -72,6 +72,7 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi } MANAGED_TABLE_KIND = "DYNAMIC TABLE" SNOWPARK = "snowpark" + SUPPORTS_QUERY_EXECUTION_TRACKING = True @contextlib.contextmanager def session(self, properties: SessionProperties) -> t.Iterator[None]: @@ -166,6 +167,7 @@ def _create_table( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, + track_rows_processed: bool = True, **kwargs: t.Any, ) -> None: table_format = kwargs.get("table_format") @@ -185,6 +187,7 @@ def _create_table( table_description=table_description, column_descriptions=column_descriptions, table_kind=table_kind, + track_rows_processed=False, # snowflake tracks CTAS row counts incorrectly **kwargs, ) diff --git a/sqlmesh/core/engine_adapter/spark.py b/sqlmesh/core/engine_adapter/spark.py index 8a529390c1..412e01f5bb 100644 --- a/sqlmesh/core/engine_adapter/spark.py +++ b/sqlmesh/core/engine_adapter/spark.py @@ -433,6 +433,7 @@ def _create_table( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, + track_rows_processed: bool = True, **kwargs: t.Any, ) -> None: table_name = ( @@ -461,6 +462,7 @@ def _create_table( target_columns_to_types=target_columns_to_types, table_description=table_description, column_descriptions=column_descriptions, + track_rows_processed=track_rows_processed, **kwargs, ) table_name = ( diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index fc08dd10af..4cef557d94 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -55,6 +55,7 @@ class TrinoEngineAdapter( SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA"] DEFAULT_CATALOG_TYPE = "hive" QUOTE_IDENTIFIERS_IN_VIEWS = False + SUPPORTS_QUERY_EXECUTION_TRACKING = True SCHEMA_DIFFER_KWARGS = { "parameterized_type_defaults": { # default decimal precision varies across backends @@ -357,6 +358,7 @@ def _create_table( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, table_kind: t.Optional[str] = None, + track_rows_processed: bool = True, **kwargs: t.Any, ) -> None: super()._create_table( @@ -368,6 +370,7 @@ def _create_table( table_description=table_description, column_descriptions=column_descriptions, table_kind=table_kind, + track_rows_processed=track_rows_processed, **kwargs, ) diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index 7a653877ae..210aff230d 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -20,6 +20,7 @@ DeployabilityIndex, Snapshot, SnapshotId, + SnapshotIdBatch, SnapshotEvaluator, apply_auto_restatements, earliest_start_date, @@ -531,6 +532,11 @@ def run_node(node: SchedulingUnit) -> None: finally: num_audits = len(audit_results) num_audits_failed = sum(1 for result in audit_results if result.count) + + execution_stats = self.snapshot_evaluator.execution_tracker.get_execution_stats( + SnapshotIdBatch(snapshot_id=snapshot.snapshot_id, batch_id=node.batch_index) + ) + self.console.update_snapshot_evaluation_progress( snapshot, batched_intervals[snapshot][node.batch_index], @@ -538,6 +544,7 @@ def run_node(node: SchedulingUnit) -> None: evaluation_duration_ms, num_audits - num_audits_failed, num_audits_failed, + execution_stats=execution_stats, auto_restatement_triggers=auto_restatement_triggers.get( snapshot.snapshot_id ), diff --git a/sqlmesh/core/snapshot/__init__.py b/sqlmesh/core/snapshot/__init__.py index da44278aa8..8ad574f8ac 100644 --- a/sqlmesh/core/snapshot/__init__.py +++ b/sqlmesh/core/snapshot/__init__.py @@ -8,6 +8,7 @@ SnapshotDataVersion as SnapshotDataVersion, SnapshotFingerprint as SnapshotFingerprint, SnapshotId as SnapshotId, + SnapshotIdBatch as SnapshotIdBatch, SnapshotIdLike as SnapshotIdLike, SnapshotInfoLike as SnapshotInfoLike, SnapshotIntervals as SnapshotIntervals, diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 1a286edcfc..afc8e06458 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -13,10 +13,13 @@ from sqlglot import exp from sqlglot.optimizer.normalize_identifiers import normalize_identifiers -from sqlmesh.core.config.common import TableNamingConvention, VirtualEnvironmentMode +from sqlmesh.core.config.common import ( + TableNamingConvention, + VirtualEnvironmentMode, + EnvironmentSuffixTarget, +) from sqlmesh.core import constants as c from sqlmesh.core.audit import StandaloneAudit -from sqlmesh.core.environment import EnvironmentSuffixTarget from sqlmesh.core.macros import call_macro from sqlmesh.core.model import Model, ModelKindMixin, ModelKindName, ViewKind, CustomKind from sqlmesh.core.model.definition import _Model @@ -159,6 +162,11 @@ def __str__(self) -> str: return f"SnapshotId<{self.name}: {self.identifier}>" +class SnapshotIdBatch(PydanticModel, frozen=True): + snapshot_id: SnapshotId + batch_id: int + + class SnapshotNameVersion(PydanticModel, frozen=True): name: str version: str diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 82924e4c3a..90186faba7 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -37,7 +37,6 @@ from sqlmesh.core import dialect as d from sqlmesh.core.audit import Audit, StandaloneAudit from sqlmesh.core.dialect import schema_ -from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, DataObjectType from sqlmesh.core.macros import RuntimeStage from sqlmesh.core.model import ( @@ -62,9 +61,11 @@ Intervals, Snapshot, SnapshotId, + SnapshotIdBatch, SnapshotInfoLike, SnapshotTableCleanupTask, ) +from sqlmesh.core.snapshot.execution_tracker import QueryExecutionTracker from sqlmesh.utils import random_id, CorrelationId from sqlmesh.utils.concurrency import ( concurrent_apply_to_snapshots, @@ -88,6 +89,7 @@ if t.TYPE_CHECKING: from sqlmesh.core.engine_adapter._typing import DF, QueryOrDF + from sqlmesh.core.engine_adapter.base import EngineAdapter from sqlmesh.core.environment import EnvironmentNamingInfo logger = logging.getLogger(__name__) @@ -128,6 +130,11 @@ def __init__( self.adapters = ( adapters if isinstance(adapters, t.Dict) else {selected_gateway or "": adapters} ) + self.execution_tracker = QueryExecutionTracker() + self.adapters = { + gateway: adapter.with_settings(query_execution_tracker=self.execution_tracker) + for gateway, adapter in self.adapters.items() + } self.adapter = ( next(iter(self.adapters.values())) if not selected_gateway @@ -169,19 +176,22 @@ def evaluate( Returns: The WAP ID of this evaluation if supported, None otherwise. """ - result = self._evaluate_snapshot( - start=start, - end=end, - execution_time=execution_time, - snapshot=snapshot, - snapshots=snapshots, - allow_destructive_snapshots=allow_destructive_snapshots or set(), - allow_additive_snapshots=allow_additive_snapshots or set(), - deployability_index=deployability_index, - batch_index=batch_index, - target_table_exists=target_table_exists, - **kwargs, - ) + with self.execution_tracker.track_execution( + SnapshotIdBatch(snapshot_id=snapshot.snapshot_id, batch_id=batch_index) + ): + result = self._evaluate_snapshot( + start=start, + end=end, + execution_time=execution_time, + snapshot=snapshot, + snapshots=snapshots, + allow_destructive_snapshots=allow_destructive_snapshots or set(), + allow_additive_snapshots=allow_additive_snapshots or set(), + deployability_index=deployability_index, + batch_index=batch_index, + target_table_exists=target_table_exists, + **kwargs, + ) if result is None or isinstance(result, str): return result raise SQLMeshError( diff --git a/sqlmesh/core/snapshot/execution_tracker.py b/sqlmesh/core/snapshot/execution_tracker.py new file mode 100644 index 0000000000..bcafec8d28 --- /dev/null +++ b/sqlmesh/core/snapshot/execution_tracker.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import typing as t +from contextlib import contextmanager +from threading import local +from dataclasses import dataclass, field +from sqlmesh.core.snapshot import SnapshotIdBatch + + +@dataclass +class QueryExecutionStats: + snapshot_id_batch: SnapshotIdBatch + total_rows_processed: t.Optional[int] = None + total_bytes_processed: t.Optional[int] = None + + +@dataclass +class QueryExecutionContext: + """ + Container for tracking rows processed or other execution information during snapshot evaluation. + + It accumulates statistics from multiple cursor.execute() calls during a single snapshot evaluation. + + Attributes: + snapshot_id_batch: Identifier linking this context to a specific snapshot evaluation + stats: Running sum of cursor.rowcount and possibly bytes processed from all executed queries during evaluation + """ + + snapshot_id_batch: SnapshotIdBatch + stats: QueryExecutionStats = field(init=False) + + def __post_init__(self) -> None: + self.stats = QueryExecutionStats(snapshot_id_batch=self.snapshot_id_batch) + + def add_execution( + self, sql: str, row_count: t.Optional[int], bytes_processed: t.Optional[int] + ) -> None: + if row_count is not None and row_count >= 0: + if self.stats.total_rows_processed is None: + self.stats.total_rows_processed = row_count + else: + self.stats.total_rows_processed += row_count + + # conditional on row_count because we should only count bytes corresponding to + # DML actions whose rows were captured + if bytes_processed is not None: + if self.stats.total_bytes_processed is None: + self.stats.total_bytes_processed = bytes_processed + else: + self.stats.total_bytes_processed += bytes_processed + + def get_execution_stats(self) -> QueryExecutionStats: + return self.stats + + +class QueryExecutionTracker: + """Thread-local context manager for snapshot execution statistics, such as rows processed.""" + + def __init__(self) -> None: + self._thread_local = local() + self._contexts: t.Dict[SnapshotIdBatch, QueryExecutionContext] = {} + + def get_execution_context( + self, snapshot_id_batch: SnapshotIdBatch + ) -> t.Optional[QueryExecutionContext]: + return self._contexts.get(snapshot_id_batch) + + def is_tracking(self) -> bool: + return getattr(self._thread_local, "context", None) is not None + + @contextmanager + def track_execution( + self, snapshot_id_batch: SnapshotIdBatch + ) -> t.Iterator[t.Optional[QueryExecutionContext]]: + """Context manager for tracking snapshot execution statistics such as row counts and bytes processed.""" + context = QueryExecutionContext(snapshot_id_batch=snapshot_id_batch) + self._thread_local.context = context + self._contexts[snapshot_id_batch] = context + + try: + yield context + finally: + self._thread_local.context = None + + def record_execution( + self, sql: str, row_count: t.Optional[int], bytes_processed: t.Optional[int] + ) -> None: + context = getattr(self._thread_local, "context", None) + if context is not None: + context.add_execution(sql, row_count, bytes_processed) + + def get_execution_stats( + self, snapshot_id_batch: SnapshotIdBatch + ) -> t.Optional[QueryExecutionStats]: + context = self._contexts.get(snapshot_id_batch) + self._contexts.pop(snapshot_id_batch, None) + return context.get_execution_stats() if context else None diff --git a/sqlmesh/core/state_sync/db/environment.py b/sqlmesh/core/state_sync/db/environment.py index 3196d18078..4a28d7d70a 100644 --- a/sqlmesh/core/state_sync/db/environment.py +++ b/sqlmesh/core/state_sync/db/environment.py @@ -78,6 +78,7 @@ def update_environment(self, environment: Environment) -> None: self.environments_table, _environment_to_df(environment), target_columns_to_types=self._environment_columns_to_types, + track_rows_processed=False, ) def update_environment_statements( @@ -108,6 +109,7 @@ def update_environment_statements( self.environment_statements_table, _environment_statements_to_df(environment_name, plan_id, environment_statements), target_columns_to_types=self._environment_statements_columns_to_types, + track_rows_processed=False, ) def invalidate_environment(self, name: str, protect_prod: bool = True) -> None: diff --git a/sqlmesh/core/state_sync/db/interval.py b/sqlmesh/core/state_sync/db/interval.py index bdfedace1e..75f475b75b 100644 --- a/sqlmesh/core/state_sync/db/interval.py +++ b/sqlmesh/core/state_sync/db/interval.py @@ -115,6 +115,7 @@ def remove_intervals( self.intervals_table, _intervals_to_df(intervals_to_remove, is_dev=False, is_removed=True), target_columns_to_types=self._interval_columns_to_types, + track_rows_processed=False, ) def get_snapshot_intervals( @@ -243,6 +244,7 @@ def _push_snapshot_intervals( self.intervals_table, pd.DataFrame(new_intervals), target_columns_to_types=self._interval_columns_to_types, + track_rows_processed=False, ) def _get_snapshot_intervals( diff --git a/sqlmesh/core/state_sync/db/migrator.py b/sqlmesh/core/state_sync/db/migrator.py index ca89668763..7edd7de3c4 100644 --- a/sqlmesh/core/state_sync/db/migrator.py +++ b/sqlmesh/core/state_sync/db/migrator.py @@ -413,7 +413,9 @@ def _backup_state(self) -> None: backup_name = _backup_table_name(table) self.engine_adapter.drop_table(backup_name) self.engine_adapter.create_table_like(backup_name, table) - self.engine_adapter.insert_append(backup_name, exp.select("*").from_(table)) + self.engine_adapter.insert_append( + backup_name, exp.select("*").from_(table), track_rows_processed=False + ) def _restore_table( self, diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 9cf4f2fbf5..8d504993fc 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -103,6 +103,7 @@ def push_snapshots(self, snapshots: t.Iterable[Snapshot], overwrite: bool = Fals self.snapshots_table, _snapshots_to_df(snapshots_to_store), target_columns_to_types=self._snapshot_columns_to_types, + track_rows_processed=False, ) for snapshot in snapshots: @@ -406,6 +407,7 @@ def _push_snapshots(self, snapshots: t.Iterable[Snapshot]) -> None: self.snapshots_table, _snapshots_to_df(snapshots_to_store), target_columns_to_types=self._snapshot_columns_to_types, + track_rows_processed=False, ) def _get_snapshots( diff --git a/sqlmesh/core/state_sync/db/version.py b/sqlmesh/core/state_sync/db/version.py index 492d74cc09..c95592bc31 100644 --- a/sqlmesh/core/state_sync/db/version.py +++ b/sqlmesh/core/state_sync/db/version.py @@ -55,6 +55,7 @@ def update_versions( ] ), target_columns_to_types=self._version_columns_to_types, + track_rows_processed=False, ) def get_versions(self) -> Versions: diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 1960848e24..19a45329d5 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -6,12 +6,13 @@ import sys import typing as t import shutil -from datetime import datetime, timedelta - +from datetime import datetime, timedelta, date +from unittest.mock import patch import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 import pytest import pytz +import time_machine from sqlglot import exp, parse_one from sqlglot.optimizer.normalize_identifiers import normalize_identifiers from sqlglot.optimizer.qualify_columns import quote_identifiers @@ -2382,8 +2383,30 @@ def _mutate_config(gateway: str, config: Config): ) context._models.update(replacement_models) + # capture row counts for each evaluated snapshot + actual_execution_stats = {} + + def capture_execution_stats( + snapshot, + interval, + batch_idx, + duration_ms, + num_audits_passed, + num_audits_failed, + audit_only=False, + execution_stats=None, + auto_restatement_triggers=None, + ): + if execution_stats is not None: + actual_execution_stats[snapshot.model.name.replace(f"{schema_name}.", "")] = ( + execution_stats + ) + # apply prod plan - context.plan(auto_apply=True, no_prompts=True) + with patch.object( + context.console, "update_snapshot_evaluation_progress", capture_execution_stats + ): + context.plan(auto_apply=True, no_prompts=True) prod_schema_results = ctx.get_metadata_results(object_names["view_schema"][0]) assert sorted(prod_schema_results.views) == object_names["views"] @@ -2395,6 +2418,34 @@ def _mutate_config(gateway: str, config: Config): assert len(physical_layer_results.materialized_views) == 0 assert len(physical_layer_results.tables) == len(physical_layer_results.non_temp_tables) == 3 + if ctx.engine_adapter.SUPPORTS_QUERY_EXECUTION_TRACKING: + assert actual_execution_stats["incremental_model"].total_rows_processed == 7 + # snowflake doesn't track rows for CTAS + assert actual_execution_stats["full_model"].total_rows_processed == ( + None if ctx.mark.startswith("snowflake") else 3 + ) + assert actual_execution_stats["seed_model"].total_rows_processed == ( + None if ctx.mark.startswith("snowflake") else 7 + ) + + if ctx.mark.startswith("bigquery"): + assert actual_execution_stats["incremental_model"].total_bytes_processed + assert actual_execution_stats["full_model"].total_bytes_processed + + # run that loads 0 rows in incremental model + # - some cloud DBs error because time travel messes up token expiration + if not ctx.is_remote: + actual_execution_stats = {} + with patch.object( + context.console, "update_snapshot_evaluation_progress", capture_execution_stats + ): + with time_machine.travel(date.today() + timedelta(days=1)): + context.run() + + if ctx.engine_adapter.SUPPORTS_QUERY_EXECUTION_TRACKING: + assert actual_execution_stats["incremental_model"].total_rows_processed == 0 + assert actual_execution_stats["full_model"].total_rows_processed == 3 + # make and validate unmodified dev environment no_change_plan: Plan = context.plan_builder( environment="test_dev", diff --git a/tests/core/engine_adapter/integration/test_integration_snowflake.py b/tests/core/engine_adapter/integration/test_integration_snowflake.py index 01cbe1c0aa..aed6bf83e4 100644 --- a/tests/core/engine_adapter/integration/test_integration_snowflake.py +++ b/tests/core/engine_adapter/integration/test_integration_snowflake.py @@ -12,6 +12,12 @@ from sqlmesh.core.plan import Plan from tests.core.engine_adapter.integration import TestContext from sqlmesh import model, ExecutionContext +from pytest_mock import MockerFixture +from sqlmesh.core.snapshot import SnapshotId, SnapshotIdBatch +from sqlmesh.core.snapshot.execution_tracker import ( + QueryExecutionContext, + QueryExecutionTracker, +) from sqlmesh.core.model import ModelKindName from datetime import datetime @@ -307,3 +313,27 @@ def fetch_database_names() -> t.Set[str]: engine_adapter.drop_catalog(sqlmesh_managed_catalog) # works, catalog is SQLMesh-managed assert fetch_database_names() == {non_sqlmesh_managed_catalog} + + +def test_rows_tracker( + ctx: TestContext, engine_adapter: SnowflakeEngineAdapter, mocker: MockerFixture +): + sqlmesh = ctx.create_context() + tracker = QueryExecutionTracker() + + add_execution_spy = mocker.spy(QueryExecutionContext, "add_execution") + + with tracker.track_execution( + SnapshotIdBatch(snapshot_id=SnapshotId(name="a", identifier="a"), batch_id=0) + ): + # Snowflake doesn't report row counts for CTAS, so this should not be tracked + engine_adapter._create_table("a", exp.select("1 as id")) + + assert add_execution_spy.call_count == 0 + + stats = tracker.get_execution_stats( + SnapshotIdBatch(snapshot_id=SnapshotId(name="a", identifier="a"), batch_id=0) + ) + assert stats is not None + assert stats.total_rows_processed is None + assert stats.total_bytes_processed is None diff --git a/tests/core/test_execution_tracker.py b/tests/core/test_execution_tracker.py new file mode 100644 index 0000000000..0e58395bee --- /dev/null +++ b/tests/core/test_execution_tracker.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from concurrent.futures import ThreadPoolExecutor + +from sqlmesh.core.snapshot.execution_tracker import QueryExecutionStats, QueryExecutionTracker +from sqlmesh.core.snapshot import SnapshotIdBatch, SnapshotId + + +def test_execution_tracker_thread_isolation() -> None: + def worker(id: SnapshotId, row_counts: list[int]) -> QueryExecutionStats: + with execution_tracker.track_execution(SnapshotIdBatch(snapshot_id=id, batch_id=0)) as ctx: + assert execution_tracker.is_tracking() + + for count in row_counts: + execution_tracker.record_execution("SELECT 1", count, None) + + assert ctx is not None + return ctx.get_execution_stats() + + execution_tracker = QueryExecutionTracker() + + with ThreadPoolExecutor() as executor: + futures = [ + executor.submit(worker, SnapshotId(name="batch_A", identifier="batch_A"), [10, 5]), + executor.submit(worker, SnapshotId(name="batch_B", identifier="batch_B"), [3, 7]), + ] + results = [f.result() for f in futures] + + # Main thread has no active tracking context + assert not execution_tracker.is_tracking() + + # Order of results is not deterministic, so look up by id + by_batch = {s.snapshot_id_batch: s for s in results} + + assert ( + by_batch[ + SnapshotIdBatch( + snapshot_id=SnapshotId(name="batch_A", identifier="batch_A"), batch_id=0 + ) + ].total_rows_processed + == 15 + ) + assert ( + by_batch[ + SnapshotIdBatch( + snapshot_id=SnapshotId(name="batch_B", identifier="batch_B"), batch_id=0 + ) + ].total_rows_processed + == 10 + ) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index f80c42f579..8cd50dc732 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -8022,7 +8022,7 @@ def test_incremental_by_time_model_ignore_additive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -8068,7 +8068,7 @@ def test_incremental_by_time_model_ignore_additive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 1 as id, 'other' as other_column, @@ -8124,7 +8124,7 @@ def test_incremental_by_time_model_ignore_additive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, CAST(1 AS STRING) as id, 'other' as other_column, @@ -8170,7 +8170,7 @@ def test_incremental_by_time_model_ignore_additive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, CAST(1 AS STRING) as id, 'other' as other_column, @@ -8344,7 +8344,7 @@ def test_incremental_by_unique_key_model_ignore_additive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -8389,7 +8389,7 @@ def test_incremental_by_unique_key_model_ignore_additive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 2 as id, 3 as new_column, @@ -8566,7 +8566,7 @@ def test_incremental_unmanaged_model_ignore_additive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -8610,7 +8610,7 @@ def test_incremental_unmanaged_model_ignore_additive_change(tmp_path: Path): ); SELECT - *, + *, 2 as id, 3 as new_column, @start_ds as ds @@ -9020,7 +9020,7 @@ def test_scd_type_2_by_column_ignore_additive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -9067,7 +9067,7 @@ def test_scd_type_2_by_column_ignore_additive_change(tmp_path: Path): ); SELECT - *, + *, 1 as id, 'stable2' as stable, 3 as new_column, @@ -9247,7 +9247,7 @@ def test_incremental_partition_ignore_additive_change(tmp_path: Path): cron '@daily' ); - SELECT + SELECT *, 1 as id, 'test_name' as name, @@ -9292,7 +9292,7 @@ def test_incremental_partition_ignore_additive_change(tmp_path: Path): ); SELECT - *, + *, 1 as id, 3 as new_column, @start_ds as ds @@ -9526,7 +9526,7 @@ def test_incremental_by_time_model_ignore_additive_change_unit_test(tmp_path: Pa cron '@daily' ); - SELECT + SELECT id, name, ds @@ -9595,7 +9595,7 @@ def test_incremental_by_time_model_ignore_additive_change_unit_test(tmp_path: Pa cron '@daily' ); - SELECT + SELECT id, new_column, ds diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 53f9bd425a..3b72a14f5f 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -115,6 +115,7 @@ def mock_exit(self, exc_type, exc_value, traceback): adapter_mock.HAS_VIEW_BINDING = False adapter_mock.wap_supported.return_value = False adapter_mock.get_data_objects.return_value = [] + adapter_mock.with_settings.return_value = adapter_mock return adapter_mock @@ -137,6 +138,7 @@ def adapters(mocker: MockerFixture): adapter_mock.HAS_VIEW_BINDING = False adapter_mock.wap_supported.return_value = False adapter_mock.get_data_objects.return_value = [] + adapter_mock.with_settings.return_value = adapter_mock adapters.append(adapter_mock) return adapters @@ -652,6 +654,7 @@ def test_evaluate_materialized_view_with_partitioned_by_cluster_by( adapter.table_exists = lambda *args, **kwargs: False # type: ignore adapter.get_data_objects = lambda *args, **kwargs: [] # type: ignore adapter._execute = execute_mock # type: ignore + adapter.with_settings = lambda **kwargs: adapter # type: ignore evaluator = SnapshotEvaluator(adapter) model = SqlModel( @@ -676,7 +679,8 @@ def test_evaluate_materialized_view_with_partitioned_by_cluster_by( execute_mock.assert_has_calls( [ call( - f"CREATE MATERIALIZED VIEW `sqlmesh__test_schema`.`test_schema__test_model__{snapshot.version}` PARTITION BY `a` CLUSTER BY `b` AS SELECT `a` AS `a`, `b` AS `b` FROM `tbl` AS `tbl`" + f"CREATE MATERIALIZED VIEW `sqlmesh__test_schema`.`test_schema__test_model__{snapshot.version}` PARTITION BY `a` CLUSTER BY `b` AS SELECT `a` AS `a`, `b` AS `b` FROM `tbl` AS `tbl`", + False, ), ] ) @@ -991,6 +995,7 @@ def test_create_tables_exist( ): adapter_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter") adapter_mock.dialect = "duckdb" + adapter_mock.with_settings.return_value = adapter_mock evaluator = SnapshotEvaluator(adapter_mock) snapshot.categorize_as(category=snapshot_category, forward_only=forward_only) @@ -1193,6 +1198,7 @@ def test_create_view_with_properties(mocker: MockerFixture, adapter_mock, make_s def test_promote_model_info(mocker: MockerFixture, make_snapshot): adapter_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter") adapter_mock.dialect = "duckdb" + adapter_mock.with_settings.return_value = adapter_mock evaluator = SnapshotEvaluator(adapter_mock) @@ -1221,6 +1227,7 @@ def test_promote_model_info(mocker: MockerFixture, make_snapshot): def test_promote_deployable(mocker: MockerFixture, make_snapshot): adapter_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter") adapter_mock.dialect = "duckdb" + adapter_mock.with_settings.return_value = adapter_mock evaluator = SnapshotEvaluator(adapter_mock) @@ -1266,6 +1273,7 @@ def test_promote_deployable(mocker: MockerFixture, make_snapshot): def test_migrate(mocker: MockerFixture, make_snapshot, make_mocked_engine_adapter): adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.with_settings = lambda **kwargs: adapter # type: ignore session_spy = mocker.spy(adapter, "session") current_table = "sqlmesh__test_schema.test_schema__test_model__1" @@ -1321,6 +1329,7 @@ def columns(table_name): def test_migrate_missing_table(mocker: MockerFixture, make_snapshot, make_mocked_engine_adapter): adapter = make_mocked_engine_adapter(EngineAdapter) adapter.table_exists = lambda _: False # type: ignore + adapter.with_settings = lambda **kwargs: adapter # type: ignore mocker.patch.object(adapter, "get_data_object", return_value=None) evaluator = SnapshotEvaluator(adapter) @@ -1389,6 +1398,7 @@ def test_migrate_snapshot_data_object_type_mismatch( make_mocked_engine_adapter, ): adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.with_settings = lambda **kwargs: adapter # type: ignore mocker.patch.object( adapter, "get_data_object", @@ -1803,7 +1813,7 @@ def test_on_destructive_change_runtime_check( make_mocked_engine_adapter, ): adapter = make_mocked_engine_adapter(EngineAdapter) - + adapter.with_settings = lambda **kwargs: adapter # type: ignore current_table = "sqlmesh__test_schema.test_schema__test_model__1" def columns(table_name): @@ -1885,7 +1895,7 @@ def test_on_additive_change_runtime_check( make_mocked_engine_adapter, ): adapter = make_mocked_engine_adapter(EngineAdapter) - + adapter.with_settings = lambda **kwargs: adapter # type: ignore current_table = "sqlmesh__test_schema.test_schema__test_model__1" def columns(table_name): @@ -3777,6 +3787,7 @@ def test_create_managed_forward_only_with_previous_version_doesnt_clone_for_dev_ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_mock, make_snapshot): adapter_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter") adapter_mock.dialect = "duckdb" + adapter_mock.with_settings.return_value = adapter_mock evaluator = SnapshotEvaluator(adapter_mock) evaluator.create([snapshot], {}) @@ -3986,6 +3997,7 @@ def test_multiple_engine_promotion(mocker: MockerFixture, adapter_mock, make_sna cursor_mock = mocker.Mock() connection_mock.cursor.return_value = cursor_mock adapter = EngineAdapter(lambda: connection_mock, "") + adapter.with_settings = lambda **kwargs: adapter # type: ignore engine_adapters = {"default": adapter_mock, "secondary": adapter} def columns(table_name): @@ -4045,7 +4057,9 @@ def test_multiple_engine_migration( mocker: MockerFixture, adapter_mock, make_snapshot, make_mocked_engine_adapter ): adapter_one = make_mocked_engine_adapter(EngineAdapter) + adapter_one.with_settings = lambda **kwargs: adapter_one # type: ignore adapter_two = adapter_mock + adapter_two.with_settings.return_value = adapter_two engine_adapters = {"one": adapter_one, "two": adapter_two} current_table = "sqlmesh__test_schema.test_schema__test_model__1" diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index b2848676b4..73fd37a2f7 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -360,11 +360,11 @@ def test_generated_sql(sushi_context_fixed_date: Context, mocker: MockerFixture) temp_schema="sqlmesh_temp_test", ) - spy_execute.assert_any_call(query_sql) - spy_execute.assert_any_call(summary_query_sql) - spy_execute.assert_any_call(compare_sql) - spy_execute.assert_any_call(sample_query_sql) - spy_execute.assert_any_call(drop_sql) + spy_execute.assert_any_call(query_sql, False) + spy_execute.assert_any_call(summary_query_sql, False) + spy_execute.assert_any_call(compare_sql, False) + spy_execute.assert_any_call(sample_query_sql, False) + spy_execute.assert_any_call(drop_sql, False) spy_execute.reset_mock() @@ -378,7 +378,7 @@ def test_generated_sql(sushi_context_fixed_date: Context, mocker: MockerFixture) ) query_sql_where = 'CREATE TABLE IF NOT EXISTS "memory"."sqlmesh_temp"."__temp_diff_abcdefgh" AS WITH "__source" AS (SELECT "s"."key", "s"."value", "s"."key" AS "__sqlmesh_join_key" FROM "table_diff_source" AS "s" WHERE "s"."key" = 2), "__target" AS (SELECT "t"."key", "t"."value", "t"."key" AS "__sqlmesh_join_key" FROM "table_diff_target" AS "t" WHERE "t"."key" = 2), "__stats" AS (SELECT "s"."key" AS "s__key", "s"."value" AS "s__value", "s"."__sqlmesh_join_key" AS "s____sqlmesh_join_key", "t"."key" AS "t__key", "t"."value" AS "t__value", "t"."__sqlmesh_join_key" AS "t____sqlmesh_join_key", CASE WHEN NOT "s"."__sqlmesh_join_key" IS NULL THEN 1 ELSE 0 END AS "s_exists", CASE WHEN NOT "t"."__sqlmesh_join_key" IS NULL THEN 1 ELSE 0 END AS "t_exists", CASE WHEN "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key" THEN 1 ELSE 0 END AS "row_joined", CASE WHEN "s"."key" IS NULL AND "t"."key" IS NULL THEN 1 ELSE 0 END AS "null_grain", CASE WHEN "s"."key" = "t"."key" THEN 1 WHEN ("s"."key" IS NULL) AND ("t"."key" IS NULL) THEN 1 WHEN ("s"."key" IS NULL) OR ("t"."key" IS NULL) THEN 0 ELSE 0 END AS "key_matches", CASE WHEN ROUND("s"."value", 3) = ROUND("t"."value", 3) THEN 1 WHEN ("s"."value" IS NULL) AND ("t"."value" IS NULL) THEN 1 WHEN ("s"."value" IS NULL) OR ("t"."value" IS NULL) THEN 0 ELSE 0 END AS "value_matches" FROM "__source" AS "s" FULL JOIN "__target" AS "t" ON "s"."__sqlmesh_join_key" = "t"."__sqlmesh_join_key") SELECT *, CASE WHEN "key_matches" = 1 AND "value_matches" = 1 THEN 1 ELSE 0 END AS "row_full_match" FROM "__stats"' - spy_execute.assert_any_call(query_sql_where) + spy_execute.assert_any_call(query_sql_where, False) def test_tables_and_grain_inferred_from_model(sushi_context_fixed_date: Context): diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 9c3c3aba4b..1b5425068f 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -874,7 +874,8 @@ def test_partially_inferred_schemas(sushi_context: Context, mocker: MockerFixtur 'CAST("s" AS STRUCT("d" DATE)) AS "s", ' 'CAST("a" AS INT) AS "a", ' 'CAST("b" AS TEXT) AS "b" ' - """FROM (VALUES ({'d': CAST('2020-01-01' AS DATE)}, 1, 'bla')) AS "t"("s", "a", "b")""" + """FROM (VALUES ({'d': CAST('2020-01-01' AS DATE)}, 1, 'bla')) AS "t"("s", "a", "b")""", + False, ) @@ -1329,14 +1330,15 @@ def test_freeze_time(mocker: MockerFixture) -> None: spy_execute.assert_has_calls( [ - call('CREATE SCHEMA IF NOT EXISTS "memory"."sqlmesh_test_jzngz56a"'), + call('CREATE SCHEMA IF NOT EXISTS "memory"."sqlmesh_test_jzngz56a"', False), call( "SELECT " """CAST('2023-01-01 12:05:03+00:00' AS DATE) AS "cur_date", """ """CAST('2023-01-01 12:05:03+00:00' AS TIME) AS "cur_time", """ - '''CAST('2023-01-01 12:05:03+00:00' AS TIMESTAMP) AS "cur_timestamp"''' + '''CAST('2023-01-01 12:05:03+00:00' AS TIMESTAMP) AS "cur_timestamp"''', + False, ), - call('DROP SCHEMA IF EXISTS "memory"."sqlmesh_test_jzngz56a" CASCADE'), + call('DROP SCHEMA IF EXISTS "memory"."sqlmesh_test_jzngz56a" CASCADE', False), ] ) @@ -1361,7 +1363,12 @@ def test_freeze_time(mocker: MockerFixture) -> None: _check_successful_or_raise(test.run()) spy_execute.assert_has_calls( - [call('''SELECT CAST('2023-01-01 12:05:03+00:00' AS TIMESTAMPTZ) AS "cur_timestamp"''')] + [ + call( + '''SELECT CAST('2023-01-01 12:05:03+00:00' AS TIMESTAMPTZ) AS "cur_timestamp"''', + False, + ) + ] ) @model("py_model", columns={"ts1": "timestamptz", "ts2": "timestamptz"}) @@ -1496,7 +1503,7 @@ def test_gateway(copy_to_temp_path: t.Callable, mocker: MockerFixture) -> None: 'AS "t"("id", "customer_id", "waiter_id", "start_ts", "end_ts", "event_date")' ) test_adapter = t.cast(ModelTest, result.successes[0]).engine_adapter - assert call(test_adapter, expected_view_sql) in spy_execute.mock_calls + assert call(test_adapter, expected_view_sql, False) in spy_execute.mock_calls _check_successful_or_raise(context.test()) @@ -1621,7 +1628,8 @@ def test_generate_input_data_using_sql(mocker: MockerFixture, tmp_path: Path) -> spy_execute.assert_any_call( 'CREATE OR REPLACE VIEW "memory"."sqlmesh_test_jzngz56a"."foo" AS ' - '''SELECT {'x': 1, 'n': {'y': 2}} AS "struct_value"''' + '''SELECT {'x': 1, 'n': {'y': 2}} AS "struct_value"''', + False, ) with pytest.raises( @@ -1817,9 +1825,9 @@ def test_custom_testing_schema(mocker: MockerFixture) -> None: spy_execute.assert_has_calls( [ - call('CREATE SCHEMA IF NOT EXISTS "memory"."my_schema"'), - call('SELECT 1 AS "a"'), - call('DROP SCHEMA IF EXISTS "memory"."my_schema" CASCADE'), + call('CREATE SCHEMA IF NOT EXISTS "memory"."my_schema"', False), + call('SELECT 1 AS "a"', False), + call('DROP SCHEMA IF EXISTS "memory"."my_schema" CASCADE', False), ] ) @@ -1845,9 +1853,9 @@ def test_pretty_query(mocker: MockerFixture) -> None: _check_successful_or_raise(test.run()) spy_execute.assert_has_calls( [ - call('CREATE SCHEMA IF NOT EXISTS "memory"."my_schema"'), - call('SELECT\n 1 AS "a"'), - call('DROP SCHEMA IF EXISTS "memory"."my_schema" CASCADE'), + call('CREATE SCHEMA IF NOT EXISTS "memory"."my_schema"', False), + call('SELECT\n 1 AS "a"', False), + call('DROP SCHEMA IF EXISTS "memory"."my_schema" CASCADE', False), ] ) @@ -2950,7 +2958,7 @@ def test_parameterized_name_sql_model() -> None: outputs: query: - id: 1 - name: foo + name: foo """, variables=variables, ), @@ -2999,7 +3007,7 @@ def execute( outputs: query: - id: 1 - name: foo + name: foo """, variables=variables, ), @@ -3049,7 +3057,7 @@ def test_parameterized_name_self_referential_model(): v: int outputs: query: - - v: 1 + - v: 1 """, variables=variables, ), @@ -3171,7 +3179,7 @@ def execute( - id: 5 outputs: query: - - id: 8 + - id: 8 """, variables=variables, ), diff --git a/web/server/console.py b/web/server/console.py index 902a85418c..871aaefbb1 100644 --- a/web/server/console.py +++ b/web/server/console.py @@ -10,6 +10,7 @@ from sqlmesh.core.environment import EnvironmentNamingInfo from sqlmesh.core.plan.definition import EvaluatablePlan from sqlmesh.core.snapshot import Snapshot, SnapshotInfoLike, SnapshotTableInfo, SnapshotId +from sqlmesh.core.snapshot.execution_tracker import QueryExecutionStats from sqlmesh.core.test import ModelTest from sqlmesh.core.test.result import ModelTextTestResult from sqlmesh.utils.date import now_timestamp @@ -142,6 +143,7 @@ def update_snapshot_evaluation_progress( num_audits_passed: int, num_audits_failed: int, audit_only: bool = False, + execution_stats: t.Optional[QueryExecutionStats] = None, auto_restatement_triggers: t.Optional[t.List[SnapshotId]] = None, ) -> None: if audit_only: From 91428c779fcfe2383b274595f9574465200444dc Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 27 Aug 2025 18:04:08 +1200 Subject: [PATCH 0767/1056] Fix(dbt_cli): Make multiple --exclude's actually work (#5233) --- sqlmesh_dbt/cli.py | 15 +++++++++++++-- sqlmesh_dbt/operations.py | 9 +++++++-- sqlmesh_dbt/selectors.py | 20 +++++++++++++------- tests/dbt/cli/test_list.py | 15 +++++++++++++++ tests/dbt/cli/test_selectors.py | 7 ++++--- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py index d82c2afd92..2daa3f9d54 100644 --- a/sqlmesh_dbt/cli.py +++ b/sqlmesh_dbt/cli.py @@ -46,10 +46,19 @@ def _cleanup() -> None: @click.group(invoke_without_command=True) @click.option("--profile", help="Which existing profile to load. Overrides output.profile") @click.option("-t", "--target", help="Which target to load for the given profile") +@click.option( + "-d", + "--debug/--no-debug", + default=False, + help="Display debug logging during dbt execution. Useful for debugging and making bug reports events to help when debugging.", +) @click.pass_context @cli_global_error_handler def dbt( - ctx: click.Context, profile: t.Optional[str] = None, target: t.Optional[str] = None + ctx: click.Context, + profile: t.Optional[str] = None, + target: t.Optional[str] = None, + debug: bool = False, ) -> None: """ An ELT tool for managing your SQL transformations and data models, powered by the SQLMesh engine. @@ -61,7 +70,9 @@ def dbt( # we have a partially applied function here because subcommands might set extra options like --vars # that need to be known before we attempt to load the project - ctx.obj = functools.partial(create, project_dir=Path.cwd(), profile=profile, target=target) + ctx.obj = functools.partial( + create, project_dir=Path.cwd(), profile=profile, target=target, debug=debug + ) if not ctx.invoked_subcommand: if profile or target: diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index 296000847c..270bba6511 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -16,9 +16,10 @@ class DbtOperations: - def __init__(self, sqlmesh_context: Context, dbt_project: Project): + def __init__(self, sqlmesh_context: Context, dbt_project: Project, debug: bool = False): self.context = sqlmesh_context self.project = dbt_project + self.debug = debug def list_( self, @@ -55,6 +56,10 @@ def _selected_models( self, select: t.Optional[t.List[str]] = None, exclude: t.Optional[t.List[str]] = None ) -> t.Dict[str, Model]: if sqlmesh_selector := selectors.to_sqlmesh(select or [], exclude or []): + if self.debug: + self.console.print(f"dbt --select: {select}") + self.console.print(f"dbt --exclude: {exclude}") + self.console.print(f"sqlmesh equivalent: '{sqlmesh_selector}'") model_selector = self.context._new_selector() selected_models = { fqn: model @@ -119,7 +124,7 @@ def create( # so that DbtOperations can query information from the DBT project files in order to invoke SQLMesh correctly dbt_project = dbt_loader._projects[0] - return DbtOperations(sqlmesh_context, dbt_project) + return DbtOperations(sqlmesh_context, dbt_project, debug=debug) def init_project_if_required(project_dir: Path) -> None: diff --git a/sqlmesh_dbt/selectors.py b/sqlmesh_dbt/selectors.py index 16f5c2ea98..120d5dcb36 100644 --- a/sqlmesh_dbt/selectors.py +++ b/sqlmesh_dbt/selectors.py @@ -37,6 +37,10 @@ def to_sqlmesh(dbt_select: t.Collection[str], dbt_exclude: t.Collection[str]) -> -> "+main.model_a & ^(raw.src_data)" --select "+main.model_a" --select "main.*b+" --exclude "raw.src_data" -> "(+main.model_a | main.*b+) & ^(raw.src_data)" + --select "+main.model_a" --select "main.*b+" --exclude "raw.src_data" --exclude "main.model_c" + -> "(+main.model_a | main.*b+) & ^(raw.src_data | main.model_c)" + --select "+main.model_a main.*b+" --exclude "raw.src_data main.model_c" + -> "(+main.model_a | main.*b+) & ^(raw.src_data | main.model_c)" """ if not dbt_select and not dbt_exclude: return None @@ -44,8 +48,13 @@ def to_sqlmesh(dbt_select: t.Collection[str], dbt_exclude: t.Collection[str]) -> select_expr = " | ".join(_to_sqlmesh(expr) for expr in dbt_select) select_expr = _wrap(select_expr) if dbt_exclude and len(dbt_select) > 1 else select_expr - exclude_expr = " | ".join(_to_sqlmesh(expr, negate=True) for expr in dbt_exclude) - exclude_expr = _wrap(exclude_expr) if dbt_select and len(dbt_exclude) > 1 else exclude_expr + exclude_expr = "" + + if dbt_exclude: + exclude_expr = " | ".join(_to_sqlmesh(expr) for expr in dbt_exclude) + exclude_expr = _negate( + _wrap(exclude_expr) if dbt_select and len(dbt_exclude) > 1 else exclude_expr + ) main_expr = " & ".join([expr for expr in [select_expr, exclude_expr] if expr]) @@ -56,13 +65,9 @@ def to_sqlmesh(dbt_select: t.Collection[str], dbt_exclude: t.Collection[str]) -> return main_expr -def _to_sqlmesh(selector_str: str, negate: bool = False) -> str: +def _to_sqlmesh(selector_str: str) -> str: unions, intersections = _split_unions_and_intersections(selector_str) - if negate: - unions = [_negate(u) for u in unions] - intersections = [_negate(i) for i in intersections] - union_expr = " | ".join(unions) intersection_expr = " & ".join(intersections) @@ -79,6 +84,7 @@ def _split_unions_and_intersections(selector_str: str) -> t.Tuple[t.List[str], t # break space-separated items like: "my_first_model my_second_model" into a list of selectors to union # and comma-separated items like: "my_first_model,my_second_model" into a list of selectors to intersect # but, take into account brackets, eg "(my_first_model & my_second_model)" should not be split + # also take into account both types in the same string, eg "my_first_model my_second_model model_3,model_4,model_5" def _split_by(input: str, delimiter: str) -> t.Iterator[str]: buf = "" diff --git a/tests/dbt/cli/test_list.py b/tests/dbt/cli/test_list.py index e854954903..3701822b4c 100644 --- a/tests/dbt/cli/test_list.py +++ b/tests/dbt/cli/test_list.py @@ -34,6 +34,7 @@ def test_list_select(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Resul def test_list_select_exclude(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): + # single exclude result = invoke_cli(["list", "--select", "main.raw_customers+", "--exclude", "main.orders"]) assert result.exit_code == 0 @@ -47,6 +48,20 @@ def test_list_select_exclude(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[.. assert "main.stg_payments" not in result.output assert "main.raw_orders" not in result.output + # multiple exclude + for args in ( + ["--select", "main.stg_orders+", "--exclude", "main.customers", "--exclude", "main.orders"], + ["--select", "main.stg_orders+", "--exclude", "main.customers main.orders"], + ): + result = invoke_cli(["list", *args]) + assert result.exit_code == 0 + assert not result.exception + + assert "main.stg_orders" in result.output + + assert "main.customers" not in result.output + assert "main.orders" not in result.output + def test_list_with_vars(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): (jaffle_shop_duckdb / "models" / "aliased_model.sql").write_text(""" diff --git a/tests/dbt/cli/test_selectors.py b/tests/dbt/cli/test_selectors.py index e494ed98a3..6041a50d0a 100644 --- a/tests/dbt/cli/test_selectors.py +++ b/tests/dbt/cli/test_selectors.py @@ -27,10 +27,11 @@ def test_selection(dbt_select: t.List[str], expected: t.Optional[str]): ([], None), (["main.model_a"], "^(main.model_a)"), (["(main.model_a & main.model_b)"], "^(main.model_a & main.model_b)"), - (["main.model_a +main.model_b"], "^(main.model_a) | ^(+main.model_b)"), + (["main.model_a,main.model_b"], "^(main.model_a & main.model_b)"), + (["main.model_a +main.model_b"], "^(main.model_a | +main.model_b)"), ( ["(+main.model_a & ^main.model_b)", "main.model_c"], - "^(+main.model_a & ^main.model_b) | ^(main.model_c)", + "^((+main.model_a & ^main.model_b) | main.model_c)", ), ], ) @@ -51,7 +52,7 @@ def test_exclusion(dbt_exclude: t.List[str], expected: t.Optional[str]): ( ["+main.model_a", "main.*b+"], ["raw.src_data", "tag:disabled"], - "(+main.model_a | main.*b+) & (^(raw.src_data) | ^(tag:disabled))", + "(+main.model_a | main.*b+) & ^(raw.src_data | tag:disabled)", ), ], ) From ca2d067ffabee59f4b5020cb34f9115179ee8c4c Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:30:04 +0300 Subject: [PATCH 0768/1056] Fix(vscode): Use url instead of endpoint for for consistent API fields (#5235) --- sqlmesh/lsp/api.py | 2 +- sqlmesh/lsp/main.py | 4 ++-- vscode/bus/src/callbacks.ts | 2 +- vscode/extension/src/commands/tableDiff.ts | 4 ++-- vscode/extension/src/lsp/custom.ts | 2 +- vscode/react/src/api/instance.ts | 2 +- vscode/react/src/components/tablediff/RerunController.tsx | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sqlmesh/lsp/api.py b/sqlmesh/lsp/api.py index 3135614d4b..882ca9825b 100644 --- a/sqlmesh/lsp/api.py +++ b/sqlmesh/lsp/api.py @@ -25,7 +25,7 @@ class ApiRequest(CustomMethodRequestBaseClass): """ requestId: str - endpoint: str + url: str method: t.Optional[str] = "GET" params: t.Optional[t.Dict[str, t.Any]] = None body: t.Optional[t.Dict[str, t.Any]] = None diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 4d91dcc071..71dc5e1e2b 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -326,7 +326,7 @@ def _custom_api( ls.log_trace(f"API request: {request}") context = self._context_get_or_load() - parsed_url = urllib.parse.urlparse(request.endpoint) + parsed_url = urllib.parse.urlparse(request.url) path_parts = parsed_url.path.strip("/").split("/") if request.method == "GET": @@ -423,7 +423,7 @@ def _custom_api( ) return ApiResponseGetTableDiff(data=table_diff_result) - raise NotImplementedError(f"API request not implemented: {request.endpoint}") + raise NotImplementedError(f"API request not implemented: {request.url}") def _custom_supported_methods( self, ls: LanguageServer, params: SupportedMethodsRequest diff --git a/vscode/bus/src/callbacks.ts b/vscode/bus/src/callbacks.ts index 180ed0f330..0601fd892a 100644 --- a/vscode/bus/src/callbacks.ts +++ b/vscode/bus/src/callbacks.ts @@ -51,7 +51,7 @@ export type RPCMethods = { } api_query: { params: { - endpoint: string + url: string method: string params: any body: any diff --git a/vscode/extension/src/commands/tableDiff.ts b/vscode/extension/src/commands/tableDiff.ts index ac1a7a3069..d9587d261b 100644 --- a/vscode/extension/src/commands/tableDiff.ts +++ b/vscode/extension/src/commands/tableDiff.ts @@ -206,7 +206,7 @@ export function showTableDiff( return await lspClient.call_custom_method('sqlmesh/api', { method: 'GET', - endpoint: '/api/table_diff', + url: '/api/table_diff', params: { source: selectedSourceEnv.label, target: selectedTargetEnv.label, @@ -484,7 +484,7 @@ export function showTableDiff( return await lspClient.call_custom_method('sqlmesh/api', { method: 'GET', - endpoint: '/api/table_diff', + url: '/api/table_diff', params: { source: sourceEnvironment, target: targetEnvironment, diff --git a/vscode/extension/src/lsp/custom.ts b/vscode/extension/src/lsp/custom.ts index 152f316cdf..c8999d5b00 100644 --- a/vscode/extension/src/lsp/custom.ts +++ b/vscode/extension/src/lsp/custom.ts @@ -50,7 +50,7 @@ interface AllModelsResponse extends BaseResponse { } export interface AbstractAPICallRequest { - endpoint: string + url: string method: string params: Record body: Record diff --git a/vscode/react/src/api/instance.ts b/vscode/react/src/api/instance.ts index 781a98ef88..3627b273de 100644 --- a/vscode/react/src/api/instance.ts +++ b/vscode/react/src/api/instance.ts @@ -39,7 +39,7 @@ export async function fetchAPI( _options?: Partial, ): Promise { const request = { - endpoint: config.url, + url: config.url, method: config.method, params: config.params, body: config.data, diff --git a/vscode/react/src/components/tablediff/RerunController.tsx b/vscode/react/src/components/tablediff/RerunController.tsx index 6a609ecaaa..4d0e7885b2 100644 --- a/vscode/react/src/components/tablediff/RerunController.tsx +++ b/vscode/react/src/components/tablediff/RerunController.tsx @@ -93,7 +93,7 @@ export function RerunController({ const apiPromise = callRpc('api_query', { method: 'GET', - endpoint: '/api/table_diff', + url: '/api/table_diff', params: params, body: {}, }) From a7feeb7a02970a681642ecc2f04e7a06ae7110f0 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:44:06 +0300 Subject: [PATCH 0769/1056] Fix: handle whitespace trimming in dbt test-to-macro jinja normalization (#5232) --- examples/sushi_dbt/models/schema.yml | 2 + .../tests/generic/greater_than_amount.sql | 5 +++ sqlmesh/dbt/manifest.py | 13 ++++-- tests/dbt/test_manifest.py | 44 ++++++++++++++++++- 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 examples/sushi_dbt/tests/generic/greater_than_amount.sql diff --git a/examples/sushi_dbt/models/schema.yml b/examples/sushi_dbt/models/schema.yml index eb5d288d5d..6dd6484b08 100644 --- a/examples/sushi_dbt/models/schema.yml +++ b/examples/sushi_dbt/models/schema.yml @@ -21,6 +21,8 @@ models: tests: - less_than_amount: amount: 1000 + - greater_than_amount: + amount: 0 - name: ds description: Date - name: top_waiters diff --git a/examples/sushi_dbt/tests/generic/greater_than_amount.sql b/examples/sushi_dbt/tests/generic/greater_than_amount.sql new file mode 100644 index 0000000000..0778e48a83 --- /dev/null +++ b/examples/sushi_dbt/tests/generic/greater_than_amount.sql @@ -0,0 +1,5 @@ +{%- test greater_than_amount(model, column_name, amount) -%} + select * + from {{ model }} + where {{ column_name }} < {{ amount }} +{%- endtest -%} diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 690bca4a3a..bc7660f4ce 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -673,12 +673,17 @@ def _node_base_config(node: ManifestNode) -> t.Dict[str, t.Any]: def _convert_jinja_test_to_macro(test_jinja: str) -> str: - TEST_TAG_REGEX = r"\s*{%\s*test\s+" - ENDTEST_REGEX = r"{%\s*endtest\s*%}" + TEST_TAG_REGEX = r"\s*{%-?\s*test\s+" + ENDTEST_REGEX = r"{%-?\s*endtest\s*-?%}" + match = re.match(TEST_TAG_REGEX, test_jinja) if not match: # already a macro return test_jinja - macro = "{% macro test_" + test_jinja[match.span()[-1] :] - return re.sub(ENDTEST_REGEX, "{% endmacro %}", macro) + test_tag = test_jinja[: match.span()[-1]] + + macro_tag = re.sub(r"({%-?\s*)test\s+", r"\1macro test_", test_tag) + macro = macro_tag + test_jinja[match.span()[-1] :] + + return re.sub(ENDTEST_REGEX, lambda m: m.group(0).replace("endtest", "endmacro"), macro) diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index 7ad67c3585..27a1d22910 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -7,7 +7,7 @@ from sqlmesh.core.config import ModelDefaultsConfig from sqlmesh.dbt.basemodel import Dependencies from sqlmesh.dbt.context import DbtContext -from sqlmesh.dbt.manifest import ManifestHelper +from sqlmesh.dbt.manifest import ManifestHelper, _convert_jinja_test_to_macro from sqlmesh.dbt.profile import Profile from sqlmesh.dbt.builtin import Api, _relation_info_to_relation from sqlmesh.dbt.util import DBT_VERSION @@ -257,3 +257,45 @@ def test_top_level_dbt_adapter_macros(): customers_macros = helper.macros("customers") assert not customers_macros["default__current_engine"].info.is_top_level assert not customers_macros["duckdb__current_engine"].info.is_top_level + + +def test_convert_jinja_test_to_macro(): + # Test block with whitespace trimming + test_input = """{%- test assert_positive(model, column_name) -%} + select * from {{ model }} where {{ column_name }} <= 0 +{%- endtest -%}""" + + expected_output = """{%- macro test_assert_positive(model, column_name) -%} + select * from {{ model }} where {{ column_name }} <= 0 +{%- endmacro -%}""" + + assert _convert_jinja_test_to_macro(test_input) == expected_output + + # Test block without whitespace trimming + test_input_no_ws = """{% test assert_positive(model, column_name) %} + select * from {{ model }} where {{ column_name }} <= 0 +{% endtest %}""" + + expected_output_no_ws = """{% macro test_assert_positive(model, column_name) %} + select * from {{ model }} where {{ column_name }} <= 0 +{% endmacro %}""" + + assert _convert_jinja_test_to_macro(test_input_no_ws) == expected_output_no_ws + + # Test block with mixed whitespace trimming + test_input_mixed = """{%- test complex_test(model, column_name='id') %} + select count(*) from {{ model }} where {{ column_name }} is null +{% endtest -%}""" + + expected_output_mixed = """{%- macro test_complex_test(model, column_name='id') %} + select count(*) from {{ model }} where {{ column_name }} is null +{% endmacro -%}""" + + assert _convert_jinja_test_to_macro(test_input_mixed) == expected_output_mixed + + # Test already converted macro (should return unchanged) + macro_input = """{%- macro test_already_converted(model) -%} + select * from {{ model }} +{%- endmacro -%}""" + + assert _convert_jinja_test_to_macro(macro_input) == macro_input From 254e9e11eb1e96ff08b127b7a18f77237a87ea07 Mon Sep 17 00:00:00 2001 From: Chris Rericha <67359577+crericha@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:19:27 -0400 Subject: [PATCH 0770/1056] Fix: Handle case where partition_by is explicitly passed in as None to dbt ModelConfig (#5240) --- sqlmesh/dbt/model.py | 6 +++++- tests/dbt/test_transformation.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index c646392368..6d31efe772 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -154,7 +154,11 @@ def _validate_sql(cls, v: t.Union[str, SqlStr]) -> SqlStr: @field_validator("partition_by", mode="before") @classmethod - def _validate_partition_by(cls, v: t.Any) -> t.Union[t.List[str], t.Dict[str, t.Any]]: + def _validate_partition_by( + cls, v: t.Any + ) -> t.Optional[t.Union[t.List[str], t.Dict[str, t.Any]]]: + if v is None: + return None if isinstance(v, str): return [v] if isinstance(v, list): diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 320b036e6d..c976e56744 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1329,6 +1329,23 @@ def test_partition_by(sushi_test_project: Project): assert model_config.to_sqlmesh(context).partitioned_by == [exp.to_column("ds", quoted=True)] +@pytest.mark.xdist_group("dbt_manifest") +def test_partition_by_none(sushi_test_project: Project): + context = sushi_test_project.context + context.target = BigQueryConfig(name="production", database="main", schema="sushi") + model_config = ModelConfig( + name="model", + alias="model", + schema="test", + package_name="package", + materialized="table", + unique_key="ds", + partition_by=None, + sql="""SELECT 1 AS one, ds FROM foo""", + ) + assert model_config.partition_by is None + + @pytest.mark.xdist_group("dbt_manifest") def test_relation_info_to_relation(): assert _relation_info_to_relation( From 3d315be428152256c1a6acb1dfe252fe0f8088f8 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:00:13 +0300 Subject: [PATCH 0771/1056] Fix!: render `{% raw %}...{% endraw %}` correctly (#5237) --- .../sushi/models/customer_revenue_by_day.sql | 4 +-- .../models/waiter_as_customer_by_day.sql | 2 +- sqlmesh/core/macros.py | 17 ------------- tests/core/test_model.py | 25 ++++++++++++++++--- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/examples/sushi/models/customer_revenue_by_day.sql b/examples/sushi/models/customer_revenue_by_day.sql index 3b7f3724cb..248af2db8d 100644 --- a/examples/sushi/models/customer_revenue_by_day.sql +++ b/examples/sushi/models/customer_revenue_by_day.sql @@ -21,7 +21,7 @@ WITH order_total AS ( LEFT JOIN sushi.items AS i ON oi.item_id = i.id AND oi.event_date = i.event_date WHERE - oi.event_date BETWEEN CAST('{{ start_ds }}' as DATE) AND CAST('{{ end_ds }}' as DATE) + oi.event_date BETWEEN @start_date AND @end_date GROUP BY oi.order_id, oi.event_date @@ -35,7 +35,7 @@ FROM sushi.orders AS o LEFT JOIN order_total AS ot ON o.id = ot.order_id AND o.event_date = ot.event_date WHERE - o.event_date BETWEEN CAST('{{ start_ds }}' as DATE) AND CAST('{{ end_ds }}' as DATE) + o.event_date BETWEEN @start_date AND @end_date GROUP BY o.customer_id, o.event_date diff --git a/examples/sushi/models/waiter_as_customer_by_day.sql b/examples/sushi/models/waiter_as_customer_by_day.sql index 7dc12db873..dd9f79b5a3 100644 --- a/examples/sushi/models/waiter_as_customer_by_day.sql +++ b/examples/sushi/models/waiter_as_customer_by_day.sql @@ -27,6 +27,6 @@ SELECT FROM sushi.waiters AS w JOIN sushi.customers as c ON w.waiter_id = c.customer_id JOIN sushi.waiter_names as wn ON w.waiter_id = wn.id -WHERE w.event_date BETWEEN @start_date AND @end_date; +WHERE w.event_date BETWEEN CAST('{{ start_ds }}' as DATE) AND @end_date; JINJA_END; diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index 42a4a8b8dc..9e7df5d111 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -12,7 +12,6 @@ from datetime import datetime, date import sqlglot -from jinja2 import Environment from sqlglot import Generator, exp, parse_one from sqlglot.executor.env import ENV from sqlglot.executor.python import Python @@ -40,7 +39,6 @@ ) from sqlmesh.utils.date import DatetimeRanges, to_datetime, to_date from sqlmesh.utils.errors import MacroEvalError, SQLMeshError -from sqlmesh.utils.jinja import JinjaMacroRegistry, has_jinja from sqlmesh.utils.metaprogramming import ( Executable, SqlValue, @@ -193,7 +191,6 @@ def __init__( self.columns_to_types_called = False self.default_catalog = default_catalog - self._jinja_env: t.Optional[Environment] = None self._schema = schema self._resolve_table = resolve_table self._resolve_tables = resolve_tables @@ -282,12 +279,6 @@ def evaluate_macros( if node.this != text: changed = True return exp.to_identifier(text, quoted=node.quoted or None) - if node.is_string: - text = node.this - if has_jinja(text): - changed = True - node.set("this", self.jinja_env.from_string(node.this).render()) - return node if isinstance(node, MacroFunc): changed = True return self.evaluate(node) @@ -436,14 +427,6 @@ def parse_one( """ return sqlglot.maybe_parse(sql, dialect=self.dialect, into=into, **opts) - @property - def jinja_env(self) -> Environment: - if not self._jinja_env: - jinja_env_methods = {**self.locals, **self.env} - del jinja_env_methods["self"] - self._jinja_env = JinjaMacroRegistry().build_environment(**jinja_env_methods) - return self._jinja_env - def columns_to_types(self, model_name: TableName | exp.Column) -> t.Dict[str, exp.DataType]: """Returns the columns-to-types mapping corresponding to the specified model.""" diff --git a/tests/core/test_model.py b/tests/core/test_model.py index eecc3977e7..9266a56c10 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -2566,11 +2566,15 @@ def test_parse(assert_exp_eq): dialect '', ); + JINJA_QUERY_BEGIN; + SELECT id::INT AS id, ds FROM x - WHERE ds BETWEEN '{{ start_ds }}' AND @end_ds + WHERE ds BETWEEN '{{ start_ds }}' AND @end_ds; + + JINJA_END; """ ) model = load_sql_based_model(expressions, dialect="hive") @@ -2580,8 +2584,8 @@ def test_parse(assert_exp_eq): } assert not model.annotated assert model.dialect == "" - assert isinstance(model.query, exp.Select) - assert isinstance(SqlModel.parse_raw(model.json()).query, exp.Select) + assert isinstance(model.query, d.JinjaQuery) + assert isinstance(SqlModel.parse_raw(model.json()).query, d.JinjaQuery) assert_exp_eq( model.render_query(), """ @@ -11543,3 +11547,18 @@ def test_text_diff_optimize_query(): diff = model1.text_diff(model2) assert diff, "Expected diff to show optimize_query change" assert "+ optimize_query" in diff.lower() + + +def test_raw_jinja_raw_tag(): + expressions = d.parse( + """ + MODEL (name test); + + JINJA_QUERY_BEGIN; + SELECT {% raw %} '{{ foo }}' {% endraw %} AS col; + JINJA_END; + """ + ) + + model = load_sql_based_model(expressions) + assert model.render_query().sql() == "SELECT '{{ foo }}' AS \"col\"" From 95d20fba46b3f53e5ac09d401b80aef51ea3b658 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:06:40 +0300 Subject: [PATCH 0772/1056] Chore: Add integration test to verify before all execution order (#5241) --- tests/core/test_integration.py | 71 +++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 8cd50dc732..2781909c88 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -42,7 +42,7 @@ from sqlmesh.core.context import Context from sqlmesh.core.config.categorizer import CategorizerConfig from sqlmesh.core.config.plan import PlanConfig -from sqlmesh.core.engine_adapter import EngineAdapter +from sqlmesh.core.engine_adapter import EngineAdapter, DuckDBEngineAdapter from sqlmesh.core.environment import EnvironmentNamingInfo from sqlmesh.core.macros import macro from sqlmesh.core.model import ( @@ -6427,6 +6427,75 @@ def test_environment_statements_error_handling(tmp_path: Path): ctx.plan(auto_apply=True, no_prompts=True) +def test_before_all_after_all_execution_order(tmp_path: Path, mocker: MockerFixture): + model = """ + MODEL ( + name test_schema.model_that_depends_on_before_all, + kind FULL, + ); + + SELECT id, value FROM before_all_created_table + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + with open(models_dir / "model.sql", "w") as f: + f.write(model) + + # before_all statement that creates a table that the above model depends on + before_all_statement = ( + "CREATE TABLE IF NOT EXISTS before_all_created_table AS SELECT 1 AS id, 'test' AS value" + ) + + # after_all that depends on the model + after_all_statement = "CREATE TABLE IF NOT EXISTS after_all_created_table AS SELECT id, value FROM test_schema.model_that_depends_on_before_all" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + before_all=[before_all_statement], + after_all=[after_all_statement], + ) + + execute_calls: t.List[str] = [] + + original_duckdb_execute = DuckDBEngineAdapter.execute + + def track_duckdb_execute(self, expression, **kwargs): + sql = expression if isinstance(expression, str) else expression.sql(dialect="duckdb") + state_tables = [ + "_snapshots", + "_environments", + "_versions", + "_intervals", + "_auto_restatements", + "_environment_statements", + "_plan_dags", + ] + + # to ignore the state queries + if not any(table in sql.lower() for table in state_tables): + execute_calls.append(sql) + + return original_duckdb_execute(self, expression, **kwargs) + + ctx = Context(paths=[tmp_path], config=config) + + # the plan would fail if the execution order ever changes and before_all statements dont execute first + ctx.plan(auto_apply=True, no_prompts=True) + + mocker.patch.object(DuckDBEngineAdapter, "execute", track_duckdb_execute) + + # run with the patched execute + ctx.run("prod", start="2023-01-01", end="2023-01-02") + + # validate explicitly that the first execute is for the before_all + assert "before_all_created_table" in execute_calls[0] + + # and that the last is the sole after all that depends on the model + assert "after_all_created_table" in execute_calls[-1] + + @time_machine.travel("2025-03-08 00:00:00 UTC") def test_tz(init_and_plan_context): context, _ = init_and_plan_context("examples/sushi") From 0bea61e183b651e013ce3c20bbe2f74f1a4c2d12 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:25:37 -0700 Subject: [PATCH 0773/1056] fix: find variable node if nested (#5238) --- sqlmesh/utils/jinja.py | 18 +++++++++++++++++- tests/dbt/converter/test_jinja.py | 13 ++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index fc9d898159..d2d830c521 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -200,6 +200,14 @@ def _extract(node: nodes.Node, parent: t.Optional[nodes.Node] = None) -> None: return extracted +def is_variable_node(n: nodes.Node) -> bool: + return ( + isinstance(n, nodes.Call) + and isinstance(n.node, nodes.Name) + and n.node.name in (c.VAR, c.BLUEPRINT_VAR) + ) + + def extract_macro_references_and_variables( *jinja_strs: str, dbt_target_name: t.Optional[str] = None ) -> t.Tuple[t.Set[MacroReference], t.Set[str]]: @@ -230,7 +238,15 @@ def extract_macro_references_and_variables( for call_name, node in extract_call_names(jinja_str): if call_name[0] in (c.VAR, c.BLUEPRINT_VAR): - assert isinstance(node, nodes.Call) + if not is_variable_node(node): + # Find the variable node which could be nested + for n in node.find_all(nodes.Call): + if is_variable_node(n): + node = n + break + else: + raise ValueError(f"Could not find variable name in {jinja_str}") + node = t.cast(nodes.Call, node) args = [jinja_call_arg_name(arg) for arg in node.args] if args and args[0]: variable_name = args[0].lower() diff --git a/tests/dbt/converter/test_jinja.py b/tests/dbt/converter/test_jinja.py index 5d9e8f3d73..5d3e4508d3 100644 --- a/tests/dbt/converter/test_jinja.py +++ b/tests/dbt/converter/test_jinja.py @@ -1,5 +1,9 @@ import pytest -from sqlmesh.utils.jinja import JinjaMacroRegistry, MacroExtractor +from sqlmesh.utils.jinja import ( + JinjaMacroRegistry, + MacroExtractor, + extract_macro_references_and_variables, +) from sqlmesh.dbt.converter.jinja import JinjaGenerator, convert_jinja_query, convert_jinja_macro import sqlmesh.dbt.converter.jinja_transforms as jt from pathlib import Path @@ -437,3 +441,10 @@ def test_convert_jinja_macro(input: str, expected: str, sushi_dbt_context: Conte result = convert_jinja_macro(sushi_dbt_context, input.strip()) assert " ".join(result.split()) == " ".join(expected.strip().split()) + + +def test_extract_macro_references_and_variables() -> None: + input = """JINJA_QUERY('{%- set something = "'"~var("variable").split("|") -%}""" + _, variables = extract_macro_references_and_variables(input) + assert len(variables) == 1 + assert variables == {"variable"} From db58405e8d2838f943a943c9faa038b40b5a851d Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:26:35 +0300 Subject: [PATCH 0774/1056] Fix!: stop treating dbt schema data types as columns_to_types (#5231) --- examples/sushi_dbt/models/schema.yml | 2 ++ examples/sushi_dbt/models/top_waiters.sql | 3 ++- sqlmesh/dbt/basemodel.py | 24 ++++++++++++++---- sqlmesh/dbt/seed.py | 15 ++++------- tests/dbt/test_config.py | 14 ++++++++++ tests/dbt/test_transformation.py | 31 +++++++++++++++++++++-- 6 files changed, 71 insertions(+), 18 deletions(-) diff --git a/examples/sushi_dbt/models/schema.yml b/examples/sushi_dbt/models/schema.yml index 6dd6484b08..d42d64bcce 100644 --- a/examples/sushi_dbt/models/schema.yml +++ b/examples/sushi_dbt/models/schema.yml @@ -36,6 +36,8 @@ models: field: waiter_id - name: revenue description: Revenue from orders served by this waiter + - name: unused_column + data_type: int - name: waiters columns: - name: waiter_id diff --git a/examples/sushi_dbt/models/top_waiters.sql b/examples/sushi_dbt/models/top_waiters.sql index f839b31dc2..e4a74fd8b3 100644 --- a/examples/sushi_dbt/models/top_waiters.sql +++ b/examples/sushi_dbt/models/top_waiters.sql @@ -6,7 +6,8 @@ SELECT waiter_id::INT AS waiter_id, - revenue::DOUBLE AS revenue + revenue::DOUBLE AS revenue, + 1 AS unused_column FROM {{ ref('waiter_revenue_by_day', version=1) }} WHERE ds = ( diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index f1e1dbed03..fad86f618e 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -328,12 +328,9 @@ def sqlmesh_model_kwargs( dependencies.macros, package=self.package_name ) jinja_macros.add_globals(self._model_jinja_context(model_context, dependencies)) - return { + + model_kwargs = { "audits": [(test.name, {}) for test in self.tests], - "columns": column_types_to_sqlmesh( - column_types_override or self.columns, self.dialect(context) - ) - or None, "column_descriptions": column_descriptions_to_sqlmesh(self.columns) or None, "depends_on": { model.canonical_name(context) for model in model_context.refs.values() @@ -349,6 +346,23 @@ def sqlmesh_model_kwargs( **self.sqlmesh_config_kwargs, } + # dbt doesn't respect the data_type field for DDL statements– instead, it optionally uses + # it to validate the actual data types at runtime through contracts or external plugins. + # Only the `columns_types` config of seed models is actually respected. We don't set the + # columns attribute to self.columns intentionally in all other cases, as that could result + # in unfaithful types when models are materialized. + # + # See: + # - https://docs.getdbt.com/reference/resource-properties/columns + # - https://docs.getdbt.com/reference/resource-configs/contract + # - https://docs.getdbt.com/reference/resource-configs/column_types + if column_types_override: + model_kwargs["columns"] = ( + column_types_to_sqlmesh(column_types_override, self.dialect(context)) or None + ) + + return model_kwargs + @abstractmethod def to_sqlmesh( self, diff --git a/sqlmesh/dbt/seed.py b/sqlmesh/dbt/seed.py index cf22d961cf..08e89ee584 100644 --- a/sqlmesh/dbt/seed.py +++ b/sqlmesh/dbt/seed.py @@ -1,6 +1,5 @@ from __future__ import annotations -import copy import typing as t import agate @@ -50,15 +49,11 @@ def to_sqlmesh( """Converts the dbt seed into a SQLMesh model.""" seed_path = self.path.absolute().as_posix() - if column_types := self.column_types: - column_types_override = copy.deepcopy(self.columns) - for name, data_type in column_types.items(): - column = column_types_override.setdefault(name, ColumnConfig(name=name)) - column.data_type = data_type - column.quote = self.quote_columns or column.quote - kwargs = self.sqlmesh_model_kwargs(context, column_types_override) - else: - kwargs = self.sqlmesh_model_kwargs(context) + column_types_override = { + name: ColumnConfig(name=name, data_type=data_type, quote=self.quote_columns) + for name, data_type in (self.column_types or {}).items() + } + kwargs = self.sqlmesh_model_kwargs(context, column_types_override) columns = kwargs.get("columns") or {} diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 1483225987..852ad02d5e 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -7,11 +7,13 @@ from dbt.adapters.base import BaseRelation, Column from pytest_mock import MockerFixture +from sqlglot import exp from sqlmesh.core.audit import StandaloneAudit from sqlmesh.core.config import Config, ModelDefaultsConfig from sqlmesh.core.dialect import jinja_query from sqlmesh.core.model import SqlModel from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange +from sqlmesh.dbt.column import ColumnConfig from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.loader import sqlmesh_config @@ -1076,3 +1078,15 @@ def test_on_schema_change_properties( assert model.on_additive_change == expected_additive assert model.on_destructive_change == expected_destructive + + +def test_sqlmesh_model_kwargs_columns_override(): + context = DbtContext() + context.project_name = "Foo" + context.target = DuckDbConfig(name="target", schema="foo") + + kwargs = ModelConfig(dialect="duckdb").sqlmesh_model_kwargs( + context, + {"c": ColumnConfig(name="c", data_type="uinteger")}, + ) + assert kwargs.get("columns") == {"c": exp.DataType.build(exp.DataType.Type.UINT)} diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index c976e56744..779160c27d 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -608,7 +608,10 @@ def test_model_columns(): name="target", schema="test", database="test", account="foo", user="bar", password="baz" ) sqlmesh_model = model.to_sqlmesh(context) - assert sqlmesh_model.columns_to_types == expected_column_types + + # Columns being present in a schema.yaml are not respected in DDLs, so SQLMesh doesn't + # set the corresponding columns_to_types_ attribute either to match dbt's behavior + assert sqlmesh_model.columns_to_types == None assert sqlmesh_model.column_descriptions == expected_column_descriptions @@ -623,8 +626,11 @@ def test_seed_columns(): }, ) + # dbt doesn't respect the data_type field in the DDLs– instead, it optionally uses it to + # validate the actual data types at runtime through contracts or external plugins. Thus, + # the actual data type is int, because that is what is inferred from the seed file. expected_column_types = { - "id": exp.DataType.build("text"), + "id": exp.DataType.build("int"), "name": exp.DataType.build("text"), } expected_column_descriptions = { @@ -671,6 +677,27 @@ def test_seed_column_types(): assert sqlmesh_seed.columns_to_types == expected_column_types assert sqlmesh_seed.column_descriptions == expected_column_descriptions + seed = SeedConfig( + name="foo", + package="package", + path=Path("examples/sushi_dbt/seeds/waiter_names.csv"), + column_types={ + "name": "text", + }, + columns={ + # The `data_type` field does not affect the materialized seed's column type + "id": ColumnConfig(name="name", data_type="text"), + }, + quote_columns=True, + ) + + expected_column_types = { + "id": exp.DataType.build("int"), + "name": exp.DataType.build("text"), + } + sqlmesh_seed = seed.to_sqlmesh(context) + assert sqlmesh_seed.columns_to_types == expected_column_types + def test_seed_column_inference(tmp_path): seed_csv = tmp_path / "seed.csv" From 54d21eb934412852a8ad6ca1d40ef8a49e6b6cc1 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 28 Aug 2025 01:27:18 +0300 Subject: [PATCH 0775/1056] Fix(dbt_cli): Add global error handling group for dbt subcommands (#5239) --- sqlmesh_dbt/cli.py | 4 +- sqlmesh_dbt/error.py | 7 ++++ tests/dbt/cli/test_global_flags.py | 65 ++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py index 2daa3f9d54..c215663f0a 100644 --- a/sqlmesh_dbt/cli.py +++ b/sqlmesh_dbt/cli.py @@ -2,7 +2,7 @@ import sys import click from sqlmesh_dbt.operations import DbtOperations, create -from sqlmesh_dbt.error import cli_global_error_handler +from sqlmesh_dbt.error import cli_global_error_handler, ErrorHandlingGroup from pathlib import Path from sqlmesh_dbt.options import YamlParamType import functools @@ -43,7 +43,7 @@ def _cleanup() -> None: exclude_option = click.option("--exclude", multiple=True, help="Specify the nodes to exclude.") -@click.group(invoke_without_command=True) +@click.group(cls=ErrorHandlingGroup, invoke_without_command=True) @click.option("--profile", help="Which existing profile to load. Overrides output.profile") @click.option("-t", "--target", help="Which target to load for the given profile") @click.option( diff --git a/sqlmesh_dbt/error.py b/sqlmesh_dbt/error.py index 005ca87c50..49a2f8195b 100644 --- a/sqlmesh_dbt/error.py +++ b/sqlmesh_dbt/error.py @@ -27,3 +27,10 @@ def wrapper(*args: t.List[t.Any], **kwargs: t.Any) -> t.Any: raise return wrapper + + +class ErrorHandlingGroup(click.Group): + def add_command(self, cmd: click.Command, name: t.Optional[str] = None) -> None: + if cmd.callback: + cmd.callback = cli_global_error_handler(cmd.callback) + super().add_command(cmd, name=name) diff --git a/tests/dbt/cli/test_global_flags.py b/tests/dbt/cli/test_global_flags.py index 802d359346..66dee7236c 100644 --- a/tests/dbt/cli/test_global_flags.py +++ b/tests/dbt/cli/test_global_flags.py @@ -1,7 +1,10 @@ import typing as t from pathlib import Path import pytest +from pytest_mock import MockerFixture from click.testing import Result +from sqlmesh.utils.errors import SQLMeshError +from sqlglot.errors import SqlglotError pytestmark = pytest.mark.slow @@ -28,3 +31,65 @@ def test_profile_and_target(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[... result = invoke_cli(["--profile", "jaffle_shop", "--target", "dev"]) assert result.exit_code == 0 assert "No command specified" in result.output + + +def test_run_error_handler( + jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result], mocker: MockerFixture +) -> None: + mock_run = mocker.patch("sqlmesh_dbt.operations.DbtOperations.run") + mock_run.side_effect = SQLMeshError("Test error message") + + result = invoke_cli(["run"]) + assert result.exit_code == 1 + assert "Error: Test error message" in result.output + assert "Traceback" not in result.output + + # test SqlglotError in run command + mock_run = mocker.patch("sqlmesh_dbt.operations.DbtOperations.run") + mock_run.side_effect = SqlglotError("Invalid SQL syntax") + + result = invoke_cli(["run"]) + + assert result.exit_code == 1 + assert "Error: Invalid SQL syntax" in result.output + assert "Traceback" not in result.output + + # test ValueError in run command + mock_run = mocker.patch("sqlmesh_dbt.operations.DbtOperations.run") + mock_run.side_effect = ValueError("Invalid configuration value") + + result = invoke_cli(["run"]) + + assert result.exit_code == 1 + assert "Error: Invalid configuration value" in result.output + assert "Traceback" not in result.output + + # test SQLMeshError in list command + mock_list = mocker.patch("sqlmesh_dbt.operations.DbtOperations.list_") + mock_list.side_effect = SQLMeshError("List command error") + + result = invoke_cli(["list"]) + + assert result.exit_code == 1 + assert "Error: List command error" in result.output + assert "Traceback" not in result.output + + # test SQLMeshError in main command without subcommand + mock_create = mocker.patch("sqlmesh_dbt.cli.create") + mock_create.side_effect = SQLMeshError("Failed to load project") + result = invoke_cli(["--profile", "jaffle_shop"]) + + assert result.exit_code == 1 + assert "Error: Failed to load project" in result.output + assert "Traceback" not in result.output + mocker.stopall() + + # test error with select option + mock_run_select = mocker.patch("sqlmesh_dbt.operations.DbtOperations.run") + mock_run_select.side_effect = SQLMeshError("Error with selector") + + result = invoke_cli(["run", "--select", "model1"]) + + assert result.exit_code == 1 + assert "Error: Error with selector" in result.output + assert "Traceback" not in result.output From 029d8d5fd03ca21d04ae433f0574e8ca75edeeea Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Thu, 28 Aug 2025 15:16:15 +1200 Subject: [PATCH 0776/1056] Revert "Feat(experimental): DBT project conversion (#4495)" (#5246) --- sqlmesh/cli/main.py | 37 -- sqlmesh/core/config/root.py | 9 +- sqlmesh/core/constants.py | 3 - sqlmesh/core/loader.py | 109 +--- sqlmesh/core/model/definition.py | 61 +- sqlmesh/core/model/kind.py | 5 +- sqlmesh/core/renderer.py | 1 - sqlmesh/core/test/definition.py | 4 +- sqlmesh/dbt/adapter.py | 3 - sqlmesh/dbt/builtin.py | 2 +- sqlmesh/dbt/converter/__init__.py | 0 sqlmesh/dbt/converter/common.py | 40 -- sqlmesh/dbt/converter/console.py | 117 ---- sqlmesh/dbt/converter/convert.py | 420 ------------ sqlmesh/dbt/converter/jinja.py | 604 ------------------ sqlmesh/dbt/converter/jinja_builtins.py | 109 ---- sqlmesh/dbt/converter/jinja_transforms.py | 465 -------------- sqlmesh/dbt/loader.py | 11 +- sqlmesh/dbt/model.py | 1 - sqlmesh/dbt/target.py | 93 +-- sqlmesh/utils/jinja.py | 99 +-- tests/core/test_config.py | 27 - tests/core/test_loader.py | 129 ---- tests/core/test_model.py | 93 +-- tests/dbt/converter/conftest.py | 21 - .../fixtures/empty_dbt_project/.gitignore | 2 - .../empty_dbt_project/analyses/.gitkeep | 0 .../fixtures/empty_dbt_project/config.py | 7 - .../empty_dbt_project/dbt_project.yml | 22 - .../empty_dbt_project/macros/.gitkeep | 0 .../empty_dbt_project/models/.gitkeep | 0 .../empty_dbt_project/models/sources.yml | 6 - .../empty_dbt_project/packages/.gitkeep | 0 .../fixtures/empty_dbt_project/profiles.yml | 6 - .../fixtures/empty_dbt_project/seeds/.gitkeep | 0 .../empty_dbt_project/seeds/items.csv | 94 --- .../empty_dbt_project/seeds/properties.yml | 13 - .../empty_dbt_project/snapshots/.gitkeep | 0 .../fixtures/empty_dbt_project/tests/.gitkeep | 0 .../converter/fixtures/jinja_nested_if.sql | 15 - .../fixtures/macro_dbt_incremental.sql | 11 - .../fixtures/macro_func_with_params.sql | 17 - .../fixtures/model_query_incremental.sql | 34 - tests/dbt/converter/test_convert.py | 105 --- tests/dbt/converter/test_jinja.py | 450 ------------- tests/dbt/converter/test_jinja_transforms.py | 453 ------------- .../github/cicd/test_integration.py | 8 +- tests/utils/test_jinja.py | 78 --- 48 files changed, 46 insertions(+), 3738 deletions(-) delete mode 100644 sqlmesh/dbt/converter/__init__.py delete mode 100644 sqlmesh/dbt/converter/common.py delete mode 100644 sqlmesh/dbt/converter/console.py delete mode 100644 sqlmesh/dbt/converter/convert.py delete mode 100644 sqlmesh/dbt/converter/jinja.py delete mode 100644 sqlmesh/dbt/converter/jinja_builtins.py delete mode 100644 sqlmesh/dbt/converter/jinja_transforms.py delete mode 100644 tests/dbt/converter/conftest.py delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/.gitignore delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/analyses/.gitkeep delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/config.py delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/dbt_project.yml delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/macros/.gitkeep delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/models/.gitkeep delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/models/sources.yml delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/packages/.gitkeep delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/profiles.yml delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/seeds/.gitkeep delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/seeds/items.csv delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/seeds/properties.yml delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/snapshots/.gitkeep delete mode 100644 tests/dbt/converter/fixtures/empty_dbt_project/tests/.gitkeep delete mode 100644 tests/dbt/converter/fixtures/jinja_nested_if.sql delete mode 100644 tests/dbt/converter/fixtures/macro_dbt_incremental.sql delete mode 100644 tests/dbt/converter/fixtures/macro_func_with_params.sql delete mode 100644 tests/dbt/converter/fixtures/model_query_incremental.sql delete mode 100644 tests/dbt/converter/test_convert.py delete mode 100644 tests/dbt/converter/test_jinja.py delete mode 100644 tests/dbt/converter/test_jinja_transforms.py diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 961b78069e..2d8673405f 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -39,7 +39,6 @@ "rollback", "run", "table_name", - "dbt", ) SKIP_CONTEXT_COMMANDS = ("init", "ui") @@ -1307,39 +1306,3 @@ def state_import(obj: Context, input_file: Path, replace: bool, no_confirm: bool """Import a state export file back into the state database""" confirm = not no_confirm obj.import_state(input_file=input_file, clear=replace, confirm=confirm) - - -@cli.group(no_args_is_help=True, hidden=True) -def dbt() -> None: - """Commands for doing dbt-specific things""" - pass - - -@dbt.command("convert") -@click.option( - "-i", - "--input-dir", - help="Path to the DBT project", - required=True, - type=click.Path(exists=True, dir_okay=True, file_okay=False, readable=True, path_type=Path), -) -@click.option( - "-o", - "--output-dir", - required=True, - help="Path to write out the converted SQLMesh project", - type=click.Path(exists=False, dir_okay=True, file_okay=False, readable=True, path_type=Path), -) -@click.option("--no-prompts", is_flag=True, help="Disable interactive prompts", default=False) -@click.pass_obj -@error_handler -@cli_analytics -def dbt_convert(obj: Context, input_dir: Path, output_dir: Path, no_prompts: bool) -> None: - """Convert a DBT project to a SQLMesh project""" - from sqlmesh.dbt.converter.convert import convert_project_files - - convert_project_files( - input_dir.absolute(), - output_dir.absolute(), - no_prompts=no_prompts, - ) diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index 65889cb7cf..9b6fae63e3 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -42,7 +42,7 @@ scheduler_config_validator, ) from sqlmesh.core.config.ui import UIConfig -from sqlmesh.core.loader import Loader, SqlMeshLoader, MigratedDbtProjectLoader +from sqlmesh.core.loader import Loader, SqlMeshLoader from sqlmesh.core.notification_target import NotificationTarget from sqlmesh.core.user import User from sqlmesh.utils.date import to_timestamp, now @@ -227,13 +227,6 @@ def _normalize_and_validate_fields(cls, data: t.Any) -> t.Any: f"^{k}$": v for k, v in physical_schema_override.items() } - if ( - (variables := data.get("variables", "")) - and isinstance(variables, dict) - and c.MIGRATED_DBT_PROJECT_NAME in variables - ): - data["loader"] = MigratedDbtProjectLoader - return data @model_validator(mode="after") diff --git a/sqlmesh/core/constants.py b/sqlmesh/core/constants.py index 2df7697b9d..a1d117f4fb 100644 --- a/sqlmesh/core/constants.py +++ b/sqlmesh/core/constants.py @@ -32,9 +32,6 @@ MAX_MODEL_DEFINITION_SIZE = 10000 """Maximum number of characters in a model definition""" -MIGRATED_DBT_PROJECT_NAME = "__dbt_project_name__" -MIGRATED_DBT_PACKAGES = "__dbt_packages__" - # The maximum number of fork processes, used for loading projects # None means default to process pool, 1 means don't fork, :N is number of processes diff --git a/sqlmesh/core/loader.py b/sqlmesh/core/loader.py index 8126b39107..6647a2edba 100644 --- a/sqlmesh/core/loader.py +++ b/sqlmesh/core/loader.py @@ -38,11 +38,7 @@ from sqlmesh.core.test import ModelTestMetadata, filter_tests_by_patterns from sqlmesh.utils import UniqueKeyDict, sys_path from sqlmesh.utils.errors import ConfigError -from sqlmesh.utils.jinja import ( - JinjaMacroRegistry, - MacroExtractor, - SQLMESH_DBT_COMPATIBILITY_PACKAGE, -) +from sqlmesh.utils.jinja import JinjaMacroRegistry, MacroExtractor from sqlmesh.utils.metaprogramming import import_python_file from sqlmesh.utils.pydantic import validation_error_message from sqlmesh.utils.process import create_process_pool_executor @@ -561,7 +557,6 @@ def _load_sql_models( signals: UniqueKeyDict[str, signal], cache: CacheBase, gateway: t.Optional[str], - loading_default_kwargs: t.Optional[t.Dict[str, t.Any]] = None, ) -> UniqueKeyDict[str, Model]: """Loads the sql models into a Dict""" models: UniqueKeyDict[str, Model] = UniqueKeyDict("models") @@ -604,7 +599,6 @@ def _load_sql_models( signal_definitions=signals, default_catalog_per_gateway=self.context.default_catalog_per_gateway, virtual_environment_mode=self.config.virtual_environment_mode, - **loading_default_kwargs or {}, ) with create_process_pool_executor( @@ -971,104 +965,3 @@ def _model_cache_entry_id(self, model_path: Path) -> str: self._loader.context.gateway or self._loader.config.default_gateway_name, ] ) - - -class MigratedDbtProjectLoader(SqlMeshLoader): - @property - def migrated_dbt_project_name(self) -> str: - return self.config.variables[c.MIGRATED_DBT_PROJECT_NAME] - - def _load_scripts(self) -> t.Tuple[MacroRegistry, JinjaMacroRegistry]: - from sqlmesh.dbt.converter.common import infer_dbt_package_from_path - from sqlmesh.dbt.target import TARGET_TYPE_TO_CONFIG_CLASS - - # Store a copy of the macro registry - standard_macros = macro.get_registry() - - jinja_macros = JinjaMacroRegistry( - create_builtins_module=SQLMESH_DBT_COMPATIBILITY_PACKAGE, - top_level_packages=["dbt", self.migrated_dbt_project_name], - ) - extractor = MacroExtractor() - - macros_max_mtime: t.Optional[float] = None - - for path in self._glob_paths( - self.config_path / c.MACROS, - ignore_patterns=self.config.ignore_patterns, - extension=".py", - ): - if import_python_file(path, self.config_path): - self._track_file(path) - macro_file_mtime = self._path_mtimes[path] - macros_max_mtime = ( - max(macros_max_mtime, macro_file_mtime) - if macros_max_mtime - else macro_file_mtime - ) - - for path in self._glob_paths( - self.config_path / c.MACROS, - ignore_patterns=self.config.ignore_patterns, - extension=".sql", - ): - self._track_file(path) - macro_file_mtime = self._path_mtimes[path] - macros_max_mtime = ( - max(macros_max_mtime, macro_file_mtime) if macros_max_mtime else macro_file_mtime - ) - - with open(path, "r", encoding="utf-8") as file: - try: - package = infer_dbt_package_from_path(path) or self.migrated_dbt_project_name - - jinja_macros.add_macros( - extractor.extract(file.read(), dialect=self.config.model_defaults.dialect), - package=package, - ) - except Exception as e: - raise ConfigError(f"Failed to load macro file: {e}", path) - - self._macros_max_mtime = macros_max_mtime - - macros = macro.get_registry() - macro.set_registry(standard_macros) - - connection_config = self.context.connection_config - # this triggers the DBT create_builtins_module to have a `target` property which is required for a bunch of DBT macros to work - if dbt_config_type := TARGET_TYPE_TO_CONFIG_CLASS.get(connection_config.type_): - try: - jinja_macros.add_globals( - { - "target": dbt_config_type.from_sqlmesh( - connection_config, - name=self.config.default_gateway_name, - ).attribute_dict() - } - ) - except NotImplementedError: - raise ConfigError(f"Unsupported dbt target type: {connection_config.type_}") - - return macros, jinja_macros - - def _load_sql_models( - self, - macros: MacroRegistry, - jinja_macros: JinjaMacroRegistry, - audits: UniqueKeyDict[str, ModelAudit], - signals: UniqueKeyDict[str, signal], - cache: CacheBase, - gateway: t.Optional[str], - loading_default_kwargs: t.Optional[t.Dict[str, t.Any]] = None, - ) -> UniqueKeyDict[str, Model]: - return super()._load_sql_models( - macros=macros, - jinja_macros=jinja_macros, - audits=audits, - signals=signals, - cache=cache, - gateway=gateway, - loading_default_kwargs=dict( - migrated_dbt_project_name=self.migrated_dbt_project_name, - ), - ) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 8e71b3aa02..dba8eedc31 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2061,7 +2061,6 @@ def load_sql_based_model( variables: t.Optional[t.Dict[str, t.Any]] = None, infer_names: t.Optional[bool] = False, blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None, - migrated_dbt_project_name: t.Optional[str] = None, **kwargs: t.Any, ) -> Model: """Load a model from a parsed SQLMesh model SQL file. @@ -2239,7 +2238,6 @@ def load_sql_based_model( query_or_seed_insert, kind=kind, time_column_format=time_column_format, - migrated_dbt_project_name=migrated_dbt_project_name, **common_kwargs, ) @@ -2451,7 +2449,6 @@ def _create_model( signal_definitions: t.Optional[SignalRegistry] = None, variables: t.Optional[t.Dict[str, t.Any]] = None, blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None, - migrated_dbt_project_name: t.Optional[str] = None, **kwargs: t.Any, ) -> Model: validate_extra_and_required_fields( @@ -2531,31 +2528,16 @@ def _create_model( if jinja_macros: jinja_macros = ( - jinja_macros - if jinja_macros.trimmed - else jinja_macros.trim(jinja_macro_references, package=migrated_dbt_project_name) + jinja_macros if jinja_macros.trimmed else jinja_macros.trim(jinja_macro_references) ) else: jinja_macros = JinjaMacroRegistry() - if migrated_dbt_project_name: - # extract {{ var() }} references used in all jinja macro dependencies to check for any variables specific - # to a migrated DBT package and resolve them accordingly - # vars are added into __sqlmesh_vars__ in the Python env so that the native SQLMesh var() function can resolve them - variables = variables or {} - - nested_macro_used_variables, flattened_package_variables = ( - _extract_migrated_dbt_variable_references(jinja_macros, variables) + for jinja_macro in jinja_macros.root_macros.values(): + referenced_variables.update( + extract_macro_references_and_variables(jinja_macro.definition)[1] ) - referenced_variables.update(nested_macro_used_variables) - variables.update(flattened_package_variables) - else: - for jinja_macro in jinja_macros.root_macros.values(): - referenced_variables.update( - extract_macro_references_and_variables(jinja_macro.definition)[1] - ) - # Merge model-specific audits with default audits if default_audits := defaults.pop("audits", None): kwargs["audits"] = default_audits + d.extract_function_calls(kwargs.pop("audits", [])) @@ -2943,7 +2925,7 @@ def render_expression( "cron_tz": lambda value: exp.Literal.string(value), "partitioned_by_": _single_expr_or_tuple, "clustered_by": _single_expr_or_tuple, - "depends_on_": lambda value: exp.Tuple(expressions=sorted(value)) if value else "()", + "depends_on_": lambda value: exp.Tuple(expressions=sorted(value)), "pre": _list_of_calls_to_exp, "post": _list_of_calls_to_exp, "audits": _list_of_calls_to_exp, @@ -3020,37 +3002,4 @@ def clickhouse_partition_func( ) -def _extract_migrated_dbt_variable_references( - jinja_macros: JinjaMacroRegistry, project_variables: t.Dict[str, t.Any] -) -> t.Tuple[t.Set[str], t.Dict[str, t.Any]]: - if not jinja_macros.trimmed: - raise ValueError("Expecting a trimmed JinjaMacroRegistry") - - used_variables = set() - # note: JinjaMacroRegistry is trimmed here so "all_macros" should be just be all the macros used by this model - for _, _, jinja_macro in jinja_macros.all_macros: - _, extracted_variable_names = extract_macro_references_and_variables(jinja_macro.definition) - used_variables.update(extracted_variable_names) - - flattened = {} - if (dbt_package_variables := project_variables.get(c.MIGRATED_DBT_PACKAGES)) and isinstance( - dbt_package_variables, dict - ): - # flatten the nested dict structure from the migrated dbt package variables in the SQLmesh config into __dbt_packages.. - # to match what extract_macro_references_and_variables() returns. This allows the usage checks in create_python_env() to work - def _flatten(prefix: str, root: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: - acc = {} - for k, v in root.items(): - key_with_prefix = f"{prefix}.{k}" - if isinstance(v, dict): - acc.update(_flatten(key_with_prefix, v)) - else: - acc[key_with_prefix] = v - return acc - - flattened = _flatten(c.MIGRATED_DBT_PACKAGES, dbt_package_variables) - - return used_variables, flattened - - TIME_COL_PARTITION_FUNC = {"clickhouse": clickhouse_partition_func} diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index 6fbbc3534b..dc5f533c21 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -4,7 +4,7 @@ from enum import Enum from typing_extensions import Self -from pydantic import Field, BeforeValidator +from pydantic import Field from sqlglot import exp from sqlglot.optimizer.normalize_identifiers import normalize_identifiers from sqlglot.optimizer.qualify_columns import quote_identifiers @@ -33,7 +33,6 @@ field_validator, get_dialect, validate_string, - positive_int_validator, validate_expression, ) @@ -505,7 +504,7 @@ class IncrementalByUniqueKeyKind(_IncrementalBy): unique_key: SQLGlotListOfFields when_matched: t.Optional[exp.Whens] = None merge_filter: t.Optional[exp.Expression] = None - batch_concurrency: t.Annotated[t.Literal[1], BeforeValidator(positive_int_validator)] = 1 + batch_concurrency: t.Literal[1] = 1 @field_validator("when_matched", mode="before") def _when_matched_validator( diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index 8b733d4c55..4078d718a6 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -179,7 +179,6 @@ def _resolve_table(table: str | exp.Table) -> str: ) render_kwargs = { - "dialect": self._dialect, **date_dict( to_datetime(execution_time or c.EPOCH), start_time, diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index 8123f52d26..b995310d09 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -648,7 +648,9 @@ def _create_df( if partial: columns = referenced_columns - return pd.DataFrame.from_records(rows, columns=columns) + return pd.DataFrame.from_records( + rows, columns=[str(c) for c in columns] if columns else None + ) def _add_missing_columns( self, query: exp.Query, all_columns: t.Optional[t.Collection[str]] = None diff --git a/sqlmesh/dbt/adapter.py b/sqlmesh/dbt/adapter.py index 2dc9890ca4..9e1ade1565 100644 --- a/sqlmesh/dbt/adapter.py +++ b/sqlmesh/dbt/adapter.py @@ -40,9 +40,6 @@ def __init__( self.jinja_globals = jinja_globals.copy() if jinja_globals else {} self.jinja_globals["adapter"] = self self.project_dialect = project_dialect - self.jinja_globals["dialect"] = ( - project_dialect # so the dialect is available in the jinja env created by self.dispatch() - ) self.quote_policy = quote_policy or Policy() @abc.abstractmethod diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 4b564eb781..4edfea687a 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -157,7 +157,7 @@ class Var: def __init__(self, variables: t.Dict[str, t.Any]) -> None: self.variables = variables - def __call__(self, name: str, default: t.Optional[t.Any] = None, **kwargs: t.Any) -> t.Any: + def __call__(self, name: str, default: t.Optional[t.Any] = None) -> t.Any: return self.variables.get(name, default) def has_var(self, name: str) -> bool: diff --git a/sqlmesh/dbt/converter/__init__.py b/sqlmesh/dbt/converter/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sqlmesh/dbt/converter/common.py b/sqlmesh/dbt/converter/common.py deleted file mode 100644 index 2bf4131065..0000000000 --- a/sqlmesh/dbt/converter/common.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import annotations -import jinja2.nodes as j -from sqlglot import exp -import typing as t -import sqlmesh.core.constants as c -from pathlib import Path - - -# jinja transform is a function that takes (current node, previous node, parent node) and returns a new Node or None -# returning None means the current node is removed from the tree -# returning a different Node means the current node is replaced with the new Node -JinjaTransform = t.Callable[[j.Node, t.Optional[j.Node], t.Optional[j.Node]], t.Optional[j.Node]] -SQLGlotTransform = t.Callable[[exp.Expression], t.Optional[exp.Expression]] - - -def _sqlmesh_predefined_macro_variables() -> t.Set[str]: - def _gen() -> t.Iterable[str]: - for suffix in ("dt", "date", "ds", "ts", "tstz", "hour", "epoch", "millis"): - for prefix in ("start", "end", "execution"): - yield f"{prefix}_{suffix}" - - for item in ("runtime_stage", "gateway", "this_model", "this_env", "model_kind_name"): - yield item - - return set(_gen()) - - -SQLMESH_PREDEFINED_MACRO_VARIABLES = _sqlmesh_predefined_macro_variables() - - -def infer_dbt_package_from_path(path: Path) -> t.Optional[str]: - """ - Given a path like "sqlmesh-project/macros/__dbt_packages__/foo/bar.sql" - - Infer that 'foo' is the DBT package - """ - if c.MIGRATED_DBT_PACKAGES in path.parts: - idx = path.parts.index(c.MIGRATED_DBT_PACKAGES) - return path.parts[idx + 1] - return None diff --git a/sqlmesh/dbt/converter/console.py b/sqlmesh/dbt/converter/console.py deleted file mode 100644 index 3fb12bcbc5..0000000000 --- a/sqlmesh/dbt/converter/console.py +++ /dev/null @@ -1,117 +0,0 @@ -from __future__ import annotations -import typing as t -from pathlib import Path -from rich.console import Console as RichConsole -from rich.tree import Tree -from rich.progress import Progress, TextColumn, BarColumn, MofNCompleteColumn, TimeElapsedColumn -from sqlmesh.core.console import PROGRESS_BAR_WIDTH -from sqlmesh.utils import columns_to_types_all_known -from sqlmesh.utils import rich as srich -import logging -from rich.prompt import Confirm - -logger = logging.getLogger(__name__) - -if t.TYPE_CHECKING: - from sqlmesh.dbt.converter.convert import ConversionReport - - -def make_progress_bar( - console: t.Optional[RichConsole] = None, - justify: t.Literal["default", "left", "center", "right", "full"] = "right", -) -> Progress: - return Progress( - TextColumn("[bold blue]{task.description}", justify=justify), - BarColumn(bar_width=PROGRESS_BAR_WIDTH), - "[progress.percentage]{task.percentage:>3.1f}%", - "•", - MofNCompleteColumn(), - "•", - TimeElapsedColumn(), - console=console, - ) - - -class DbtConversionConsole: - """Console for displaying DBT project conversion progress""" - - def __init__(self, console: t.Optional[RichConsole] = None) -> None: - self.console: RichConsole = console or srich.console - - def log_message(self, message: str) -> None: - self.console.print(message) - - def start_project_conversion(self, input_path: Path) -> None: - self.log_message(f"DBT project loaded from {input_path}; starting conversion") - - def prompt_clear_directory(self, prefix: str, path: Path) -> bool: - return Confirm.ask( - f"{prefix}'{path}' is not empty.\nWould you like to clear it?", console=self.console - ) - - # Models - def start_models_conversion(self, model_count: int) -> None: - self.progress_bar = make_progress_bar(justify="left", console=self.console) - self.progress_bar.start() - self.models_progress_task_id = self.progress_bar.add_task( - "Converting models", total=model_count - ) - - def start_model_conversion(self, model_name: str) -> None: - logger.debug(f"Converting model {model_name}") - self.progress_bar.update(self.models_progress_task_id, description=None, refresh=True) - - def complete_model_conversion(self) -> None: - self.progress_bar.update(self.models_progress_task_id, refresh=True, advance=1) - - def complete_models_conversion(self) -> None: - self.progress_bar.update(self.models_progress_task_id, description=None, refresh=True) - - # Audits - - def start_audits_conversion(self, audit_count: int) -> None: - self.audits_progress_task_id = self.progress_bar.add_task( - "Converting audits", total=audit_count - ) - - def start_audit_conversion(self, audit_name: str) -> None: - self.progress_bar.update(self.audits_progress_task_id, description=None, refresh=True) - - def complete_audit_conversion(self) -> None: - self.progress_bar.update(self.audits_progress_task_id, refresh=True, advance=1) - - def complete_audits_conversion(self) -> None: - self.progress_bar.update(self.audits_progress_task_id, description=None, refresh=True) - - # Macros - - def start_macros_conversion(self, macro_count: int) -> None: - self.macros_progress_task_id = self.progress_bar.add_task( - "Converting macros", total=macro_count - ) - - def start_macro_conversion(self, macro_name: str) -> None: - self.progress_bar.update(self.macros_progress_task_id, description=None, refresh=True) - - def complete_macro_conversion(self) -> None: - self.progress_bar.update(self.macros_progress_task_id, refresh=True, advance=1) - - def complete_macros_conversion(self) -> None: - self.progress_bar.update(self.macros_progress_task_id, description=None, refresh=True) - self.progress_bar.stop() - - def output_report(self, report: ConversionReport) -> None: - tree = Tree( - "[blue]The following models are self-referencing and their column types could not be statically inferred:" - ) - - for output_path, model in report.self_referencing_models: - if not model.columns_to_types or not columns_to_types_all_known(model.columns_to_types): - tree_node = tree.add(f"[green]{model.name}") - tree_node.add(output_path.as_posix()) - - self.console.print(tree) - - self.log_message( - "[red]These will need to be manually fixed.[/red]\nEither specify the column types in the MODEL block or ensure the outer SELECT lists all columns" - ) diff --git a/sqlmesh/dbt/converter/convert.py b/sqlmesh/dbt/converter/convert.py deleted file mode 100644 index 7eab536946..0000000000 --- a/sqlmesh/dbt/converter/convert.py +++ /dev/null @@ -1,420 +0,0 @@ -import typing as t -from pathlib import Path -import shutil -import os - -from sqlmesh.dbt.loader import sqlmesh_config, DbtLoader, DbtContext, Project -from sqlmesh.core.context import Context -import sqlmesh.core.dialect as d -from sqlmesh.core import constants as c - -from sqlmesh.core.model.kind import SeedKind -from sqlmesh.core.model import SqlModel, SeedModel -from sqlmesh.dbt.converter.jinja import convert_jinja_query, convert_jinja_macro -from sqlmesh.dbt.converter.common import infer_dbt_package_from_path -import dataclasses -from dataclasses import dataclass - -from sqlmesh.dbt.converter.console import DbtConversionConsole -from sqlmesh.utils.jinja import JinjaMacroRegistry, extract_macro_references_and_variables -from sqlmesh.utils import yaml - - -@dataclass -class ConversionReport: - self_referencing_models: t.List[t.Tuple[Path, SqlModel]] = dataclasses.field( - default_factory=list - ) - - -@dataclass -class InputPaths: - # todo: read paths from DBT project yaml - - base: Path - - @property - def models(self) -> Path: - return self.base / "models" - - @property - def seeds(self) -> Path: - return self.base / "seeds" - - @property - def tests(self) -> Path: - return self.base / "tests" - - @property - def macros(self) -> Path: - return self.base / "macros" - - @property - def snapshots(self) -> Path: - return self.base / "snapshots" - - @property - def packages(self) -> Path: - return self.base / "dbt_packages" - - -@dataclass -class OutputPaths: - base: Path - - @property - def models(self) -> Path: - return self.base / "models" - - @property - def seeds(self) -> Path: - return self.base / "seeds" - - @property - def audits(self) -> Path: - return self.base / "audits" - - @property - def macros(self) -> Path: - return self.base / "macros" - - -def convert_project_files(src: Path, dest: Path, no_prompts: bool = True) -> None: - console = DbtConversionConsole() - report = ConversionReport() - - console.log_message(f"Converting project at '{src}' to '{dest}'") - - ctx, dbt_project = _load_project(src) - dbt_load_context = dbt_project.context - - console.start_project_conversion(src) - - input_paths, output_paths = _ensure_paths(src, dest, console, no_prompts) - - model_count = len(ctx.models) - - # DBT Models -> SQLMesh Models - console.start_models_conversion(model_count) - _convert_models(ctx, input_paths, output_paths, report, console) - console.complete_models_conversion() - - # DBT Tests -> Standalone Audits - console.start_audits_conversion(len(ctx.standalone_audits)) - _convert_standalone_audits(ctx, input_paths, output_paths, console) - console.complete_audits_conversion() - - # DBT Macros -> SQLMesh Jinja Macros - all_macros = list( - iterate_macros(input_paths.macros, output_paths.macros, dbt_load_context, ctx) - ) - console.start_macros_conversion(len(all_macros)) - for package, macro_text, input_id, output_file_path, should_transform in all_macros: - console.start_macro_conversion(input_id) - - output_file_path.parent.mkdir(parents=True, exist_ok=True) - converted = ( - convert_jinja_macro(ctx, macro_text, package) if should_transform else macro_text - ) - output_file_path.write_text(converted, encoding="utf8") - - console.complete_macro_conversion() - - console.complete_macros_conversion() - - # Generate SQLMesh config - # TODO: read all profiles from config and convert to gateways instead of just the current profile? - console.log_message("Writing SQLMesh config") - new_config = _generate_sqlmesh_config(ctx, dbt_project, dbt_load_context) - (dest / "config.yml").write_text(yaml.dump(new_config)) - - if report.self_referencing_models: - console.output_report(report) - - console.log_message("All done") - - -def _load_project(src: Path) -> t.Tuple[Context, Project]: - config = sqlmesh_config(project_root=src) - - ctx = Context(config=config, paths=src) - - dbt_loader = ctx._loaders[0] - assert isinstance(dbt_loader, DbtLoader) - - dbt_project = dbt_loader._projects[0] - - return ctx, dbt_project - - -def _ensure_paths( - src: Path, dest: Path, console: DbtConversionConsole, no_prompts: bool -) -> t.Tuple[InputPaths, OutputPaths]: - if not dest.exists(): - console.log_message(f"Creating output directory: {dest}") - dest.mkdir() - - if dest.is_file(): - raise ValueError(f"Output path must be a directory") - - if any(dest.iterdir()): - if not no_prompts and console.prompt_clear_directory("Output directory ", dest): - for path in dest.glob("**/*"): - if path.is_file(): - path.unlink() - elif path.is_dir(): - shutil.rmtree(path) - console.log_message(f"Output directory '{dest}' cleared") - else: - raise ValueError("Please ensure the output directory is empty") - - input_paths = InputPaths(src) - output_paths = OutputPaths(dest) - - for dir in (output_paths.models, output_paths.seeds, output_paths.audits, output_paths.macros): - dir.mkdir() - - return input_paths, output_paths - - -def _convert_models( - ctx: Context, - input_paths: InputPaths, - output_paths: OutputPaths, - report: ConversionReport, - console: DbtConversionConsole, -) -> None: - # Iterating in DAG order helps minimize re-rendering when the fingerprint cache is busted when we call upsert_model() to check if - # a self-referencing model has all its columns_to_types known or not - for fqn in ctx.dag: - model = ctx.models.get(fqn) - - if not model: - # some entries in the dag are not models - continue - - model_name = fqn - - # todo: support DBT model_paths[] being not `models` or being a list - # todo: write out column_descriptions() into model block - console.start_model_conversion(model_name) - - if model.kind.is_external: - # skip external models - # they can be created with `sqlmesh create_external_models` post-conversion - console.complete_model_conversion() # still advance the progress bar - continue - - if model.kind.is_seed: - # this will produce the original seed file, eg "items.csv" - if model._path is None: - raise ValueError(f"Unhandled model path for model {model_name}") - seed_filename = model._path.relative_to(input_paths.seeds) - - # seed definition - rename "items.csv" -> "items.sql" - model_filename = seed_filename.with_suffix(".sql") - - # copy the seed data itself to the seeds dir - shutil.copyfile(model._path, output_paths.seeds / seed_filename) - - # monkeypatch the model kind to have a relative reference to the seed file - assert isinstance(model.kind, SeedKind) - model.kind.path = str(Path("../seeds", seed_filename)) - else: - if model._path is None: - raise ValueError(f"Unhandled model path for model {model_name}") - if input_paths.models in model._path.parents: - model_filename = model._path.relative_to(input_paths.models) - elif input_paths.snapshots in model._path.parents: - # /base/path/snapshots/foo.sql -> /output/path/models/dbt_snapshots/foo.sql - model_filename = "dbt_snapshots" / model._path.relative_to(input_paths.snapshots) - elif input_paths.packages in model._path.parents: - model_filename = c.MIGRATED_DBT_PACKAGES / model._path.relative_to( - input_paths.packages - ) - else: - raise ValueError(f"Unhandled model path: {model._path}") - - # todo: a SQLGLot transform on `audits` in the model definition to lowercase the names? - model_output_path = output_paths.models / model_filename - model_output_path.parent.mkdir(parents=True, exist_ok=True) - model_package = infer_dbt_package_from_path(model_output_path) - - def _render(e: d.exp.Expression) -> str: - if isinstance(e, d.Jinja): - e = convert_jinja_query(ctx, model, e, model_package) - rendered = e.sql(dialect=model.dialect, pretty=True) - if not isinstance(e, d.Jinja): - rendered += ";" - return rendered - - model_to_render = model.model_copy( - update=dict(depends_on_=None if len(model.depends_on) > 0 else set()) - ) - if isinstance(model, (SqlModel, SeedModel)): - # Keep depends_on for SQL Models because sometimes the entire query is a macro call. - # If we clear it and rely on inference, the SQLMesh native loader will throw: - # - ConfigError: Dependencies must be provided explicitly for models that can be rendered only at runtime - model_to_render = model.model_copy( - update=dict(depends_on_=resolve_fqns_to_model_names(ctx, model.depends_on)) - ) - - rendered_queries = [ - _render(q) - for q in model_to_render.render_definition(render_query=False, include_python=False) - ] - - # add inline audits - # todo: handle these better - # maybe output generic audits for the 4 DBT audits (not_null, unique, accepted_values, relationships) and emit definitions for them? - for _, audit in model.audit_definitions.items(): - rendered_queries.append("\n" + _render(d.parse_one(f"AUDIT (name {audit.name})"))) - # todo: or do we want the original? - rendered_queries.append(_render(model.render_audit_query(audit))) - - model_definition = "\n".join(rendered_queries) - - model_output_path.write_text(model_definition) - - console.complete_model_conversion() - - -def _convert_standalone_audits( - ctx: Context, input_paths: InputPaths, output_paths: OutputPaths, console: DbtConversionConsole -) -> None: - for _, audit in ctx.standalone_audits.items(): - console.start_audit_conversion(audit.name) - audit_definition = audit.render_definition(include_python=False) - - stringified = [] - for expression in audit_definition: - if isinstance(expression, d.JinjaQuery): - expression = convert_jinja_query(ctx, audit, expression) - stringified.append(expression.sql(dialect=audit.dialect, pretty=True)) - - audit_definition_string = ";\n".join(stringified) - - if audit._path is None: - continue - audit_filename = audit._path.relative_to(input_paths.tests) - audit_output_path = output_paths.audits / audit_filename - audit_output_path.write_text(audit_definition_string) - console.complete_audit_conversion() - return None - - -def _generate_sqlmesh_config( - ctx: Context, dbt_project: Project, dbt_load_context: DbtContext -) -> t.Dict[str, t.Any]: - DEFAULT_ARGS: t.Dict[str, t.Any] - from sqlmesh.utils.pydantic import DEFAULT_ARGS - - base_config = ctx.config.model_dump( - mode="json", include={"gateways", "model_defaults", "variables"}, **DEFAULT_ARGS - ) - # Extend with the variables loaded from DBT - if "variables" not in base_config: - base_config["variables"] = {} - if c.MIGRATED_DBT_PACKAGES not in base_config["variables"]: - base_config["variables"][c.MIGRATED_DBT_PACKAGES] = {} - - # this is used when loading with the native loader to set the package name for top level macros - base_config["variables"][c.MIGRATED_DBT_PROJECT_NAME] = dbt_project.context.project_name - - migrated_package_names = [] - for package in dbt_project.packages.values(): - dbt_load_context.set_and_render_variables(package.variables, package.name) - - if package.name == dbt_project.context.project_name: - base_config["variables"].update(dbt_load_context.variables) - else: - base_config["variables"][c.MIGRATED_DBT_PACKAGES][package.name] = ( - dbt_load_context.variables - ) - migrated_package_names.append(package.name) - - for package_name in migrated_package_names: - # these entries are duplicates because the DBT loader already applies any project specific overrides to the - # package level variables - base_config["variables"].pop(package_name, None) - - return base_config - - -def iterate_macros( - input_macros_dir: Path, output_macros_dir: Path, dbt_load_context: DbtContext, ctx: Context -) -> t.Iterator[t.Tuple[t.Optional[str], str, str, Path, bool]]: - """ - Return an iterator over all the macros that need to be migrated - - The main project level ones are read from the source macros directory (it's assumed these are written by the user) - - The rest / library level ones are read from the DBT manifest based on merging together all the model JinjaMacroRegistry's from the SQLMesh context - """ - - all_macro_references = set() - - for dirpath, _, files in os.walk( - input_macros_dir - ): # note: pathlib doesnt have a walk function until python 3.12 - for name in files: - if name.lower().endswith(".sql"): - input_file_path = Path(dirpath) / name - - output_file_path = output_macros_dir / ( - input_file_path.relative_to(input_macros_dir) - ) - - input_file_contents = input_file_path.read_text(encoding="utf8") - - # as we migrate user-defined macros, keep track of other macros they reference from other packages/libraries - # so we can be sure theyre included - # (since there is no guarantee a model references a user-defined macro which means the dependencies may not be pulled in automatically) - macro_refs, _ = extract_macro_references_and_variables( - input_file_contents, dbt_target_name=dbt_load_context.target_name - ) - all_macro_references.update(macro_refs) - - yield ( - None, - input_file_contents, - str(input_file_path), - output_file_path, - True, - ) - - jmr = JinjaMacroRegistry() - for model in ctx.models.values(): - jmr = jmr.merge(model.jinja_macros) - - # add any macros that are referenced in user macros but not necessarily directly in models - # this can happen if a user has defined a macro that is currently unused in a model but we still want to migrate it - jmr = jmr.merge( - dbt_load_context.jinja_macros.trim( - all_macro_references, package=dbt_load_context.project_name - ) - ) - - for package, name, macro in jmr.all_macros: - if package and package != dbt_load_context.project_name: - output_file_path = output_macros_dir / c.MIGRATED_DBT_PACKAGES / package / f"{name}.sql" - - yield ( - package, - macro.definition, - f"{package}.{name}", - output_file_path, - "var(" in macro.definition, # todo: check for ref() etc as well? - ) - - -def resolve_fqns_to_model_names(ctx: Context, fqns: t.Set[str]) -> t.Set[str]: - # model.depends_on is provided by the DbtLoader as a list of fully qualified table name strings - # if we output them verbatim, when loading them back we get errors like: - # - ConfigError: Failed to load model definition: 'Dot' object has no attribute 'catalog' - # So we need to resolve them to model names instead. - # External models also need to be excluded because the "name" is still a FQN string so cause the above error - - return { - ctx.models[i].name for i in fqns if i in ctx.models and not ctx.models[i].kind.is_external - } diff --git a/sqlmesh/dbt/converter/jinja.py b/sqlmesh/dbt/converter/jinja.py deleted file mode 100644 index 783ae5a74f..0000000000 --- a/sqlmesh/dbt/converter/jinja.py +++ /dev/null @@ -1,604 +0,0 @@ -import typing as t -import jinja2.nodes as j -import sqlmesh.core.dialect as d -from sqlmesh.core.context import Context -from sqlmesh.core.snapshot import Node -from sqlmesh.core.model import SqlModel, load_sql_based_model -from sqlglot import exp -from sqlmesh.dbt.converter.common import JinjaTransform -from inspect import signature -from more_itertools import windowed -from itertools import chain -from sqlmesh.dbt.context import DbtContext -import sqlmesh.dbt.converter.jinja_transforms as jt -from sqlmesh.utils.errors import ConfigError -from sqlmesh.utils.jinja import SQLMESH_DBT_COMPATIBILITY_PACKAGE - -# for j.Operand.op -OPERATOR_MAP = { - "eq": "==", - "ne": "!=", - "lt": "<", - "gt": ">", - "lteq": "<=", - "gteq": ">=", - "in": "in", - "notin": "not in", -} - - -def lpad_windowed(iterable: t.Iterable[j.Node]) -> t.Iterator[t.Tuple[t.Optional[j.Node], j.Node]]: - for prev, curr in windowed(chain([None], iterable), 2): - if curr is None: - raise ValueError("Current item cannot be None") - yield prev, curr - - -class JinjaGenerator: - def generate( - self, node: j.Node, prev: t.Optional[j.Node] = None, parent: t.Optional[j.Node] = None - ) -> str: - if not isinstance(node, j.Node): - raise ValueError(f"Generator only works with Jinja AST nodes, not: {type(node)}") - - acc = "" - - node_type = type(node) - generator_fn_name = f"_generate_{node_type.__name__.lower()}" - - if generator_fn := getattr(self, generator_fn_name, None): - sig = signature(generator_fn) - kwargs: t.Dict[str, t.Optional[j.Node]] = {"node": node} - if "prev" in sig.parameters: - kwargs["prev"] = prev - if "parent" in sig.parameters: - kwargs["parent"] = parent - acc += generator_fn(**kwargs) - else: - raise NotImplementedError(f"Generator for node type '{type(node)}' is not implemented") - - return acc - - def _generate_template(self, node: j.Template) -> str: - acc = [] - for prev, curr in lpad_windowed(node.body): - if curr: - acc.append(self.generate(curr, prev, node)) - - return "".join(acc) - - def _generate_output(self, node: j.Output) -> str: - acc = [] - for prev, curr in lpad_windowed(node.nodes): - acc.append(self.generate(curr, prev, node)) - - return "".join(acc) - - def _generate_templatedata(self, node: j.TemplateData) -> str: - return node.data - - def _generate_name( - self, node: j.Name, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> str: - return self._wrap_in_expression_if_necessary(node.name, prev, parent) - - def _generate_getitem( - self, node: j.Getitem, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> str: - item_name = self.generate(node.node, parent=node) - if node.arg: - if node.node.find(j.Filter): - # for when someone has {{ (foo | bar | baz)[0] }} - item_name = f"({item_name})" - item_name = f"{item_name}[{self.generate(node.arg, parent=node)}]" - - return self._wrap_in_expression_if_necessary(item_name, prev, parent) - - def _generate_getattr( - self, node: j.Getattr, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> str: - what_str = self.generate(node.node, parent=node) - - return self._wrap_in_expression_if_necessary(f"{what_str}.{node.attr}", prev, parent) - - def _generate_const( - self, node: j.Const, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> str: - quotechar = "" - node_value: str - if isinstance(node.value, str): - quotechar = "'" if "'" not in node.value else '"' - node_value = node.value - else: - node_value = str(node.value) - - const_value = quotechar + node_value + quotechar - - return self._wrap_in_expression_if_necessary(const_value, prev, parent) - - def _generate_keyword(self, node: j.Keyword) -> str: - return node.key + "=" + self.generate(node.value, parent=node) - - def _generate_test(self, node: j.Test, parent: t.Optional[j.Node]) -> str: - var_name = self.generate(node.node, parent=node) - test = "is" if not isinstance(parent, j.Not) else "is not" - if node.name: - return f"{var_name} {test} {node.name}" - return var_name - - def _generate_assign(self, node: j.Assign) -> str: - target_str = self.generate(node.target, parent=node) - what_str = self.generate(node.node, parent=node) - return "{% set " + target_str + " = " + what_str + " %}" - - def _generate_assignblock(self, node: j.AssignBlock) -> str: - target_str = self.generate(node.target, parent=node) - body_str = "".join(self.generate(c, parent=node) for c in node.body) - # todo: node.filter? - return "{% set " + target_str + " %}" + body_str + "{% endset %}" - - def _generate_call( - self, node: j.Call, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> str: - call_name = self.generate(node.node, parent=node) - call_args = ", ".join(self.generate(a, parent=node) for a in node.args) - call_kwargs = ", ".join(self.generate(a, parent=node) for a in node.kwargs) - sep = ", " if call_args and call_kwargs else "" - call_str = call_name + f"({call_args}{sep}{call_kwargs})" - - return self._wrap_in_expression_if_necessary(call_str, prev, parent) - - def _generate_if(self, node: j.If, parent: t.Optional[j.Node]) -> str: - test_str = self.generate(node.test, parent=node) - body_str = "".join(self.generate(c, parent=node) for c in node.body) - elifs_str = "".join(self.generate(c, parent=node) for c in node.elif_) - elses_str = "".join(self.generate(c, parent=node) for c in node.else_) - - end_block_name: t.Optional[str] - block_name, end_block_name = "if", "endif" - if isinstance(parent, j.If): - if node in parent.elif_: - block_name, end_block_name = "elif", None - - end_block = "{% " + end_block_name + " %}" if end_block_name else "" - - elses_str = "{% else %}" + elses_str if elses_str else "" - - return ( - "{% " - + block_name - + " " - + test_str - + " %}" - + body_str - + elifs_str - + elses_str - + end_block - ) - - def _generate_macro(self, node: j.Macro, prev: t.Optional[j.Node]) -> str: - name_str = node.name - rendered_defaults = list(reversed([self.generate(d, parent=node) for d in node.defaults])) - rendered_args = [self.generate(a, parent=node) for a in node.args] - - # the defaults, if they exist, line up with the last arguments in the list - # so we reverse the lists to match the arrays and then reverse the result to get the original order - args_with_defaults = [ - (arg, next(iter(rendered_defaults[idx : idx + 1]), None)) - for idx, arg in enumerate(reversed(rendered_args)) - ] - args_with_defaults = list(reversed(args_with_defaults)) - - args_str = ", ".join(f"{a}={d}" if d is not None else a for a, d in args_with_defaults) - body_str = "".join(self.generate(c, parent=node) for c in node.body) - - # crude sql comment detection that will cause false positives that hopefully shouldnt matter - # this is to work around a WONTFIX bug in the SQLGlot tokenizer that if the macro body contains a SQL comment - # and {% endmacro %} is on the same line, it gets included as comment instead of a proper token - # the bug also occurs if the {% macro %} tag is on a line that starts with a SQL comment - start_tag = "{% macro " - if prev: - prev_str = self.generate(prev) - if "--" in prev_str and not prev_str.rstrip(" ").endswith("\n"): - start_tag = "\n" + start_tag - - end_tag = "{% endmacro %}" - if "--" in body_str and not body_str.rstrip(" ").endswith("\n"): - end_tag = "\n" + end_tag - - return start_tag + name_str + "(" + args_str + ")" + " %}" + body_str + end_tag - - def _generate_for(self, node: j.For) -> str: - target_str = self.generate(node.target, parent=node) - iter_str = self.generate(node.iter, parent=node) - test_str = "if " + self.generate(node.test, parent=node) if node.test else None - body_str = "".join(self.generate(c, parent=node) for c in node.body) - - acc = "{% for " + target_str + " in " + iter_str - if test_str: - acc += f" {test_str}" - acc += " %}" - acc += body_str - acc += "{% endfor %}" - - return acc - - def _generate_list(self, node: j.List, parent: t.Optional[j.Node]) -> str: - items_str_array = [self.generate(i, parent=node) for i in node.items] - items_on_newline = ( - not isinstance(parent, j.Pair) - and len(items_str_array) > 1 - and any(len(i) > 50 for i in items_str_array) - ) - item_separator = "\n\t" if items_on_newline else " " - items_str = f",{item_separator}".join(items_str_array) - start_separator = "\n\t" if items_on_newline else "" - end_separator = "\n" if items_on_newline else "" - return f"[{start_separator}{items_str}{end_separator}]" - - def _generate_dict(self, node: j.Dict) -> str: - items_str = ", ".join(self.generate(c, parent=node) for c in node.items) - return "{ " + items_str + " }" - - def _generate_pair(self, node: j.Pair) -> str: - key_str = self.generate(node.key, parent=node) - value_str = self.generate(node.value, parent=node) - return f"{key_str}: {value_str}" - - def _generate_not(self, node: j.Not) -> str: - if isinstance(node.node, j.Test): - return self.generate(node.node, parent=node) - - return self.__generate_unaryexp(node) - - def _generate_neg(self, node: j.Neg) -> str: - return self.__generate_unaryexp(node) - - def _generate_pos(self, node: j.Pos) -> str: - return self.__generate_unaryexp(node) - - def _generate_compare(self, node: j.Compare) -> str: - what_str = self.generate(node.expr, parent=node) - - # todo: is this correct? need to test with multiple ops - ops_str = "".join(self.generate(o, parent=node) for o in node.ops) - - return f"{what_str} {ops_str}" - - def _generate_slice(self, node: j.Slice) -> str: - start_str = self.generate(node.start, parent=node) if node.start else "" - stop_str = self.generate(node.stop, parent=node) if node.stop else "" - # todo: need a syntax example of step - return f"{start_str}:{stop_str}" - - def _generate_operand(self, node: j.Operand) -> str: - assert isinstance(node, j.Operand) - value_str = self.generate(node.expr, parent=node) - - return f"{OPERATOR_MAP[node.op]} " + value_str - - def _generate_add(self, node: j.Add, parent: t.Optional[j.Node]) -> str: - return self.__generate_binexp(node, parent) - - def _generate_mul(self, node: j.Mul, parent: t.Optional[j.Node]) -> str: - return self.__generate_binexp(node, parent) - - def _generate_div(self, node: j.Div, parent: t.Optional[j.Node]) -> str: - return self.__generate_binexp(node, parent) - - def _generate_sub(self, node: j.Sub, parent: t.Optional[j.Node]) -> str: - return self.__generate_binexp(node, parent) - - def _generate_floordiv(self, node: j.FloorDiv, parent: t.Optional[j.Node]) -> str: - return self.__generate_binexp(node, parent) - - def _generate_mod(self, node: j.Mod, parent: t.Optional[j.Node]) -> str: - return self.__generate_binexp(node, parent) - - def _generate_pow(self, node: j.Pow, parent: t.Optional[j.Node]) -> str: - return self.__generate_binexp(node, parent) - - def _generate_or(self, node: j.Or, parent: t.Optional[j.Node]) -> str: - return self.__generate_binexp(node, parent) - - def _generate_and(self, node: j.And, parent: t.Optional[j.Node]) -> str: - return self.__generate_binexp(node, parent) - - def _generate_concat(self, node: j.Concat) -> str: - return " ~ ".join(self.generate(c, parent=node) for c in node.nodes) - - def _generate_tuple(self, node: j.Tuple, parent: t.Optional[j.Node]) -> str: - parenthesis = isinstance(parent, (j.Operand, j.Call)) - items_str = ", ".join(self.generate(i, parent=node) for i in node.items) - return items_str if not parenthesis else f"({items_str})" - - def _generate_filter( - self, node: j.Filter, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> str: - # node.node may be None if this Filter is part of a FilterBlock - what_str = self.generate(node.node, parent=node) if node.node else None - if isinstance(node.node, j.CondExpr): - what_str = f"({what_str})" - - args_str = ", ".join(self.generate(a, parent=node) for a in node.args + node.kwargs) - if args_str: - args_str = f"({args_str})" - - filter_expr = f"{node.name}{args_str}" - if what_str: - filter_expr = f"{what_str} | {filter_expr}" - - return self._wrap_in_expression_if_necessary(filter_expr, prev=prev, parent=parent) - - def _generate_filterblock(self, node: j.FilterBlock) -> str: - filter_str = self.generate(node.filter, parent=node) - body_str = "".join(self.generate(c, parent=node) for c in node.body) - return "{% filter " + filter_str + " %}" + body_str + "{% endfilter %}" - - def _generate_exprstmt(self, node: j.ExprStmt) -> str: - node_str = self.generate(node.node, parent=node) - return "{% do " + node_str + " %}" - - def _generate_condexpr( - self, node: j.CondExpr, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> str: - test_sql = self.generate(node.test, parent=node) - expr1_sql = self.generate(node.expr1, parent=node) - - if node.expr2 is None: - raise ValueError("CondExpr lacked an 'else', not sure how to handle this") - - expr2_sql = self.generate(node.expr2, parent=node) - return self._wrap_in_expression_if_necessary( - f"{expr1_sql} if {test_sql} else {expr2_sql}", prev, parent - ) - - def __generate_binexp(self, node: j.BinExpr, parent: t.Optional[j.Node]) -> str: - left_str = self.generate(node.left, parent=node) - right_str = self.generate(node.right, parent=node) - - wrap_left = isinstance(node.left, j.BinExpr) - wrap_right = isinstance(node.right, j.BinExpr) - - acc = f"({left_str})" if wrap_left else left_str - acc += f" {node.operator} " - acc += f"({right_str})" if wrap_right else right_str - - return acc - - def __generate_unaryexp(self, node: j.UnaryExpr) -> str: - body_str = self.generate(node.node, parent=node) - return f"{node.operator} {body_str}" - - def _generate_nsref(self, node: j.NSRef) -> str: - return f"{node.name}.{node.attr}" - - def _generate_callblock(self, node: j.CallBlock) -> str: - call = self.generate(node.call, parent=node) - body = "".join(self.generate(e, parent=node) for e in node.body) - args = ", ".join(self.generate(arg, parent=node) for arg in node.args) - - open_tag = "{% call" - - if args: - open_tag += "(" + args + ")" - - if len(node.defaults) > 0: - raise NotImplementedError("Not sure how to handle CallBlock.defaults") - - return open_tag + " " + call + " %}" + body + "{% endcall %}" - - def _wrap_in_expression_if_necessary( - self, string: str, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> str: - wrap = False - if isinstance(prev, j.TemplateData): - wrap = True - elif prev is None and isinstance(parent, j.Output): - wrap = True - elif parent: - # if the node is nested inside eg an {% if %} block, dont wrap it in {{ }} - wrap = not any(isinstance(parent, t) for t in (j.Operand, j.Stmt, j.Expr, j.Helper)) - - return "{{ " + string + " }}" if wrap else string - - -def _contains_jinja(query: str) -> bool: - if "{{" in query: - return True - if "{%" in query: - return True - return False - - -def transform(base: j.Node, handler: JinjaTransform) -> j.Node: - sig = signature(handler) - - def _build_handler_kwargs( - node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> t.Dict[str, t.Any]: - kwargs: t.Dict[str, t.Optional[j.Node]] = {"node": node} - if "prev" in sig.parameters: - kwargs["prev"] = prev - if "parent" in sig.parameters: - kwargs["parent"] = parent - return kwargs - - def _transform( - node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> t.Optional[j.Node]: - transformed_node: t.Optional[j.Node] = handler(**_build_handler_kwargs(node, prev, parent)) # type: ignore - - if not transformed_node: - return None - - node = transformed_node - - new_children: t.Dict[j.Node, t.Optional[j.Node]] = {} - prev = None - for child in list(node.iter_child_nodes()): - transformed_child = _transform(node=child, prev=prev, parent=node) - if transformed_child != child: - new_children[child] = transformed_child - prev = child - - if new_children: - replacement_fields: t.Dict[str, t.Union[j.Node, t.List[j.Node]]] = {} - for name, value in node.iter_fields(): - assert isinstance(name, str) - - if isinstance(value, list): - replacement_value_list = [new_children.get(i, i) for i in value] - replacement_fields[name] = [r for r in replacement_value_list if r is not None] - elif isinstance(value, j.Node): - replacement_value = new_children.get(value) or value - replacement_fields[name] = replacement_value - for name, value in replacement_fields.items(): - setattr(node, name, value) - - return node - - transformed = _transform(node=base, prev=None, parent=None) - if transformed is None: - raise ValueError( - f"Transform '{handler.__name__}' consumed the entire AST; this indicates a bug" - ) - return transformed - - -def convert_jinja_query( - context: Context, - node: Node, - query: d.Jinja, - package: t.Optional[str] = None, - exclude: t.Optional[t.List[t.Callable]] = None, -) -> t.Union[d.JinjaQuery, d.JinjaStatement, exp.Query, exp.DDL]: - jinja_env = node.jinja_macros.build_environment() - - ast: j.Node = jinja_env.parse(query.text("this")) # type: ignore - - transforms = [ - # transform {{ ref("foo") }} -> schema.foo (NOT "fully_qualified"."schema"."foo") - jt.resolve_dbt_ref_to_model_name(context.models, jinja_env, node.dialect), - # Rewrite ref() calls that cant be converted to strings (maybe theyre macro aguments) to __migrated_ref() calls - jt.rewrite_dbt_ref_to_migrated_ref(context.models, jinja_env, node.dialect), - # transform {{ source("upstream"."foo") }} -> upstream.foo (NOT "fully_qualified"."upstream"."foo") - jt.resolve_dbt_source_to_model_name(context.models, jinja_env, node.dialect), - # Rewrite source() calls that cant be converted to strings (maybe theyre macro aguments) to __migrated_source() calls - jt.rewrite_dbt_source_to_migrated_source(context.models, jinja_env, node.dialect), - # transform {{ this }} -> model.name - jt.resolve_dbt_this_to_model_name(node.name), - # deuplicate where both {% if sqlmesh_incremental %} and {% if is_incremental() %} are used - jt.deduplicate_incremental_checks(), - # unpack {% if is_incremental() %} blocks because they arent necessary when running a native project - jt.unpack_incremental_checks(), - ] - - if package: - transforms.append(jt.append_dbt_package_kwarg_to_var_calls(package)) - - transforms = [ - t for t in transforms if not any(e.__name__ in t.__name__ for e in (exclude or [])) - ] - - for handler in transforms: - ast = transform(ast, handler) - - generator = JinjaGenerator() - pre_post_processing = generator.generate(ast) - if isinstance(node, SqlModel) and isinstance(query, d.JinjaQuery) and not node.depends_on_self: - # is it self-referencing now is_incremental() has been removed? - # if so, and columns_to_types are not all known, then we can't remove is_incremental() or we will get a load error - - # try to load the converted model with the native loader - model_definition = node.copy(update=dict(audits=[])).render_definition()[0].sql() - - # we need the Jinja builtins that inclide the compatibility shims because the transforms may have created eg __migrated_ref() calls - jinja_macros = node.jinja_macros.copy( - update=dict(create_builtins_module=SQLMESH_DBT_COMPATIBILITY_PACKAGE) - ) - - converted_node = load_sql_based_model( - expressions=[d.parse_one(model_definition), d.JinjaQuery(this=pre_post_processing)], - jinja_macros=jinja_macros, - defaults=context.config.model_defaults.dict(), - default_catalog=node.default_catalog, - ) - original_model = context.models[node.fqn] - - if converted_node.depends_on_self: - try: - # we need to upsert the model into the context to trigger columns_to_types inference - # note that this can sometimes bust the optimized query cache which can lead to long pauses converting some models in large projects - context.upsert_model(converted_node) - except ConfigError as e: - if "Self-referencing models require inferrable column types" in str(e): - # we have a self-referencing model where the columns_to_types cannot be inferred - # run the conversion again without the unpack_incremental_checks transform - return convert_jinja_query( - context, node, query, exclude=[jt.unpack_incremental_checks] - ) - raise - except Exception: - # todo: perhaps swallow this so that we just continue on with the original logic - raise - finally: - context.upsert_model(original_model) # put the original model definition back - - ast = transform(ast, jt.rewrite_sqlmesh_predefined_variables_to_sqlmesh_macro_syntax()) - post_processed = generator.generate(ast) - - # post processing - have we removed all the jinja so this can effectively be a normal SQL query? - if not _contains_jinja(post_processed): - parsed = d.parse_one(post_processed, dialect=node.dialect) - - # converting DBT '{{ start_ds }}' to a SQLMesh macro results in single quoted '@start_ds' but we really need unquoted @start_ds - transformed = parsed.transform(jt.unwrap_macros_in_string_literals()) - if isinstance(transformed, (exp.Query, exp.DDL)): - return transformed - - raise ValueError( - f"Transformation resulted in a {type(transformed)} node instead of Query / DDL statement" - ) - - if isinstance(query, d.JinjaQuery): - return d.JinjaQuery(this=pre_post_processing) - if isinstance(query, d.JinjaStatement): - return d.JinjaStatement(this=pre_post_processing) - - raise ValueError(f"Not sure how to handle: {type(query)}") - - -def convert_jinja_macro(context: Context, src: str, package: t.Optional[str] = None) -> str: - jinja_macros = DbtContext().jinja_macros # ensures the correct create_builtins_module is set - jinja_macros = jinja_macros.merge(context._jinja_macros) - - jinja_env = jinja_macros.build_environment() - - dialect = context.default_dialect - if not dialect: - raise ValueError("No project dialect configured?") - - transforms = [ - # transform {{ ref("foo") }} -> schema.foo (NOT "fully_qualified"."schema"."foo") - jt.resolve_dbt_ref_to_model_name(context.models, jinja_env, dialect), - # Rewrite ref() calls that cant be converted to strings (maybe theyre macro aguments) to __migrated_ref() calls - jt.rewrite_dbt_ref_to_migrated_ref(context.models, jinja_env, dialect), - # transform {{ source("foo", "bar") }} -> `qualified`.`foo`.`bar` - jt.resolve_dbt_source_to_model_name(context.models, jinja_env, dialect), - # transform {{ var('foo') }} -> {{ var('foo', __dbt_package='') }} - jt.append_dbt_package_kwarg_to_var_calls(package), - # deduplicate where both {% if sqlmesh_incremental %} and {% if is_incremental() %} are used - jt.deduplicate_incremental_checks(), - # unpack {% if sqlmesh_incremental %} blocks because they arent necessary when running a native project - jt.unpack_incremental_checks(), - ] - - ast: j.Node = jinja_env.parse(src) - - for handler in transforms: - ast = transform(ast, handler) - - generator = JinjaGenerator() - - return generator.generate(ast) diff --git a/sqlmesh/dbt/converter/jinja_builtins.py b/sqlmesh/dbt/converter/jinja_builtins.py deleted file mode 100644 index 59303ad344..0000000000 --- a/sqlmesh/dbt/converter/jinja_builtins.py +++ /dev/null @@ -1,109 +0,0 @@ -import typing as t -import functools -from sqlmesh.utils.jinja import JinjaMacroRegistry -from dbt.adapters.base.relation import BaseRelation -from sqlmesh.dbt.builtin import Api -from sqlmesh.core.engine_adapter import EngineAdapter -from sqlmesh.utils.errors import ConfigError -from dbt.adapters.base import BaseRelation -from sqlglot import exp - -from dbt.adapters.base import BaseRelation - - -def migrated_ref( - dbt_api: Api, - database: t.Optional[str] = None, - schema: t.Optional[str] = None, - identifier: t.Optional[str] = None, - version: t.Optional[int] = None, - sqlmesh_model_name: t.Optional[str] = None, -) -> BaseRelation: - if version: - raise ValueError("dbt model versions are not supported in converted projects.") - - return dbt_api.Relation.create(database=database, schema=schema, identifier=identifier) - - -def migrated_source( - dbt_api: Api, - database: t.Optional[str] = None, - schema: t.Optional[str] = None, - identifier: t.Optional[str] = None, -) -> BaseRelation: - return dbt_api.Relation.create(database=database, schema=schema, identifier=identifier) - - -def create_builtin_globals( - jinja_macros: JinjaMacroRegistry, - global_vars: t.Dict[str, t.Any], - engine_adapter: t.Optional[EngineAdapter], - *args: t.Any, - **kwargs: t.Any, -) -> t.Dict[str, t.Any]: - import sqlmesh.utils.jinja as sqlmesh_native_jinja - import sqlmesh.dbt.builtin as sqlmesh_dbt_jinja - - # Capture dialect before the dbt builtins pops it - dialect = global_vars.get("dialect") - - sqlmesh_native_globals = sqlmesh_native_jinja.create_builtin_globals( - jinja_macros, global_vars, *args, **kwargs - ) - - if this_model := global_vars.get("this_model"): - # create a DBT-compatible version of @this_model for {{ this }} - if isinstance(this_model, str): - if not dialect: - raise ConfigError("No dialect?") - - # in audits, `this_model` is a SQL SELECT query that selects from the current table - # elsewhere, it's a fqn string - parsed: exp.Expression = exp.maybe_parse(this_model, dialect=dialect) - - table: t.Optional[exp.Table] = None - if isinstance(parsed, exp.Column): - table = exp.to_table(this_model, dialect=dialect) - elif isinstance(parsed, exp.Query): - table = parsed.find(exp.Table) - else: - raise ConfigError(f"Not sure how to handle this_model: {this_model}") - - if table: - # sqlmesh_dbt_jinja.create_builtin_globals() will construct a Relation for {{ this }} based on the supplied dict - global_vars["this"] = { - "database": table.catalog, - "schema": table.db, - "identifier": table.name, - } - - else: - raise ConfigError(f"Unhandled this_model type: {type(this_model)}") - - sqlmesh_dbt_globals = sqlmesh_dbt_jinja.create_builtin_globals( - jinja_macros, global_vars, engine_adapter, *args, **kwargs - ) - - def source(dbt_api: Api, source_name: str, table_name: str) -> BaseRelation: - # some source() calls cant be converted to __migrated_source() calls because they contain dynamic parameters - # this is a fallback and will be wrong in some situations because `sources` in DBT can be aliased in config - # TODO: maybe we migrate sources into the SQLMesh variables so we can look them up here? - return dbt_api.Relation.create(database=source_name, identifier=table_name) - - def ref(dbt_api: Api, ref_name: str, package: t.Optional[str] = None) -> BaseRelation: - # some ref() calls cant be converted to __migrated_ref() calls because they contain dynamic parameters - raise NotImplementedError( - f"Unable to resolve ref: {ref_name}. Please replace it with an actual model name or use a SQLMesh macro to generate dynamic model name." - ) - - dbt_compatibility_shims = { - "dialect": dialect, - "__migrated_ref": functools.partial(migrated_ref, sqlmesh_dbt_globals["api"]), - "__migrated_source": functools.partial(migrated_source, sqlmesh_dbt_globals["api"]), - "source": functools.partial(source, sqlmesh_dbt_globals["api"]), - "ref": functools.partial(ref, sqlmesh_dbt_globals["api"]), - # make {{ config(...) }} a no-op, some macros call it but its meaningless in a SQLMesh Native project - "config": lambda *_args, **_kwargs: None, - } - - return {**sqlmesh_native_globals, **sqlmesh_dbt_globals, **dbt_compatibility_shims} diff --git a/sqlmesh/dbt/converter/jinja_transforms.py b/sqlmesh/dbt/converter/jinja_transforms.py deleted file mode 100644 index 4c4cf03edc..0000000000 --- a/sqlmesh/dbt/converter/jinja_transforms.py +++ /dev/null @@ -1,465 +0,0 @@ -import typing as t -from types import MappingProxyType -from sqlmesh.core.model import Model -from jinja2 import Environment -import jinja2.nodes as j -from sqlmesh.dbt.converter.common import ( - SQLMESH_PREDEFINED_MACRO_VARIABLES, - JinjaTransform, - SQLGlotTransform, -) -from dbt.adapters.base.relation import BaseRelation -from sqlmesh.core.dialect import normalize_model_name -from sqlglot import exp -import sqlmesh.core.dialect as d -from functools import wraps - - -def _make_standalone_call_transform(fn_name: str, handler: JinjaTransform) -> JinjaTransform: - """ - Creates a transform that identifies standalone Call nodes (that arent nested in other Call nodes) and replaces them with nodes - containing the result of the handler() function - """ - - def _handle( - node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> t.Optional[j.Node]: - if isinstance(node, j.Call): - if isinstance(parent, (j.Call, j.List, j.Keyword)): - return node - - if (name := node.find(j.Name)) and name.name == fn_name: - return handler(node, prev, parent) - - return node - - return _handle - - -def _make_single_expression_transform( - mapping: t.Union[ - t.Dict[str, str], - t.Callable[[j.Node, t.Optional[j.Node], t.Optional[j.Node], str], t.Optional[str]], - ], -) -> JinjaTransform: - """ - Creates a transform that looks for standalone {{ expression }} nodes - It then looks up 'expression' in the provided mapping and replaces it with a TemplateData node containing the value - """ - - def _handle(node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node]) -> j.Node: - # the assumption is that individual expressions are nested in between TemplateData - if prev and not isinstance(prev, j.TemplateData): - return node - - if isinstance(node, j.Name) and not isinstance(parent, j.Getattr): - if isinstance(mapping, dict): - result = mapping.get(node.name) - else: - result = mapping(node, prev, parent, node.name) - if result is not None: - return j.TemplateData(result) - - return node - - return _handle - - -def _dbt_relation_to_model_name( - models: MappingProxyType[str, t.Union[Model, str]], relation: BaseRelation, dialect: str -) -> t.Optional[str]: - model_fqn = normalize_model_name( - table=relation.render(), default_catalog=relation.database, dialect=dialect - ) - if resolved_value := models.get(model_fqn): - return resolved_value if isinstance(resolved_value, str) else resolved_value.name - return None - - -def _dbt_relation_to_kwargs(relation: BaseRelation) -> t.List[j.Keyword]: - kwargs = [] - if database := relation.database: - kwargs.append(j.Keyword("database", j.Const(database))) - if schema := relation.schema: - kwargs.append(j.Keyword("schema", j.Const(schema))) - if identifier := relation.identifier: - kwargs.append(j.Keyword("identifier", j.Const(identifier))) - return kwargs - - -ASTTransform = t.TypeVar("ASTTransform", JinjaTransform, SQLGlotTransform) - - -def ast_transform(fn: t.Callable[..., ASTTransform]) -> t.Callable[..., ASTTransform]: - """ - Decorator to mark functions as being Jinja or SQLGlot AST transforms - - The purpose is to set __name__ to be the outer function name so that the transforms have stable names for an exclude list - The function itself as well as the ASTTransform returned by the function should have the same __name__ for this to work - """ - - @wraps(fn) - def wrapper(*args: t.Any, **kwargs: t.Any) -> ASTTransform: - result = fn(*args, **kwargs) - result.__name__ = fn.__name__ - return result - - return wrapper - - -@ast_transform -def resolve_dbt_ref_to_model_name( - models: MappingProxyType[str, t.Union[Model, str]], env: Environment, dialect: str -) -> JinjaTransform: - """ - Takes an expression like "{{ ref('foo') }}" - And turns it into "sqlmesh.foo" based on the provided list of models and resolver() function - - Args: - models: A dict of models (or model names) keyed by model fqn - jinja_env: Should contain an implementation of {{ ref() }} to turn a DBT relation name into a DBT relation object - - Returns: - A string containing the **model name** (not fqn) of the model referenced by the DBT "{{ ref() }}" call - """ - - ref: t.Callable = env.globals["ref"] # type: ignore - - def _resolve( - node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> t.Optional[j.Node]: - if isinstance(node, j.Call) and node.args and isinstance(node.args[0], j.Const): - ref_name = node.args[0].value - version = None - if version_kwarg := next((k for k in node.kwargs if k.key in ("version", "v")), None): - if isinstance(version_kwarg.value, j.Const): - version = version_kwarg.value.value - else: - # the version arg is present but its some kind of dynamic runtime value - # this means we cant resolve the ref to a model - return node - - if relation := ref(ref_name, version=version): - if not isinstance(relation, BaseRelation): - raise ValueError( - f"ref() returned non-relation type for '{ref_name}': {relation}" - ) - if model_name := _dbt_relation_to_model_name(models, relation, dialect): - return j.TemplateData(model_name) - return j.TemplateData(f"__unresolved_ref__.{ref_name}") - - return node - - return _make_standalone_call_transform("ref", _resolve) - - -@ast_transform -def rewrite_dbt_ref_to_migrated_ref( - models: MappingProxyType[str, t.Union[Model, str]], env: Environment, dialect: str -) -> JinjaTransform: - """ - Takes an expression like "{{ ref('foo') }}" - And turns it into "{{ __migrated_ref(database='foo', schema='bar', identifier='baz', sqlmesh_model_name='') }}" - so that the SQLMesh Native loader can construct a Relation instance without needing the Context - - Args: - models: A dict of models (or model names) keyed by model fqn - jinja_env: Should contain an implementation of {{ ref() }} to turn a DBT relation name into a DBT relation object - - Returns: - A new Call node with enough data to reconstruct the Relation - """ - - ref: t.Callable = env.globals["ref"] # type: ignore - - def _rewrite( - node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> t.Optional[j.Node]: - if isinstance(node, j.Call) and isinstance(node.node, j.Name) and node.node.name == "ref": - if node.args and isinstance(node.args[0], j.Const): - ref_name = node.args[0].value - version_kwarg = next((k for k in node.kwargs if k.key == "version"), None) - if (relation := ref(ref_name)) and isinstance(relation, BaseRelation): - if model_name := _dbt_relation_to_model_name(models, relation, dialect): - kwargs = _dbt_relation_to_kwargs(relation) - if version_kwarg: - kwargs.append(version_kwarg) - kwargs.append(j.Keyword("sqlmesh_model_name", j.Const(model_name))) - return j.Call(j.Name("__migrated_ref", "load"), [], kwargs, None, None) - - return node - - return _rewrite - - -@ast_transform -def resolve_dbt_source_to_model_name( - models: MappingProxyType[str, t.Union[Model, str]], env: Environment, dialect: str -) -> JinjaTransform: - """ - Takes an expression like "{{ source('foo', 'bar') }}" - And turns it into "foo.bar" based on the provided list of models and resolver() function - - Args: - models: A dict of models (or model names) keyed by model fqn - jinja_env: Should contain an implementation of {{ source() }} to turn a DBT source name / table name into a DBT relation object - - Returns: - A string containing the table fqn of the external table referenced by the DBT "{{ source() }}" call - """ - source: t.Callable = env.globals["source"] # type: ignore - - def _resolve( - node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> t.Optional[j.Node]: - if isinstance(node, j.Call) and isinstance(parent, (j.TemplateData, j.Output)): - if ( - len(node.args) == 2 - and isinstance(node.args[0], j.Const) - and isinstance(node.args[1], j.Const) - ): - source_name = node.args[0].value - table_name = node.args[1].value - if relation := source(source_name, table_name): - if not isinstance(relation, BaseRelation): - raise ValueError( - f"source() returned non-relation type for '{source_name}.{table_name}': {relation}" - ) - if model_name := _dbt_relation_to_model_name(models, relation, dialect): - return j.TemplateData(model_name) - return j.TemplateData(relation.render()) - # source() didnt resolve anything, just pass through the arguments verbatim - return j.TemplateData(f"{source_name}.{table_name}") - - return node - - return _make_standalone_call_transform("source", _resolve) - - -@ast_transform -def rewrite_dbt_source_to_migrated_source( - models: MappingProxyType[str, t.Union[Model, str]], env: Environment, dialect: str -) -> JinjaTransform: - """ - Takes an expression like "{{ source('foo', 'bar') }}" - And turns it into "{{ __migrated_source(database='foo', identifier='bar') }}" - so that the SQLMesh Native loader can construct a Relation instance without needing the Context - - Args: - models: A dict of models (or model names) keyed by model fqn - jinja_env: Should contain an implementation of {{ source() }} to turn a DBT source name / table name into a DBT relation object - - Returns: - A new Call node with enough data to reconstruct the Relation - """ - - source: t.Callable = env.globals["source"] # type: ignore - - def _rewrite( - node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> t.Optional[j.Node]: - if ( - isinstance(node, j.Call) - and isinstance(node.node, j.Name) - and node.node.name == "source" - ): - if ( - len(node.args) == 2 - and isinstance(node.args[0], j.Const) - and isinstance(node.args[1], j.Const) - ): - source_name = node.args[0].value - table_name = node.args[1].value - if (relation := source(source_name, table_name)) and isinstance( - relation, BaseRelation - ): - kwargs = _dbt_relation_to_kwargs(relation) - return j.Call(j.Name("__migrated_source", "load"), [], kwargs, None, None) - - return node - - return _rewrite - - -@ast_transform -def resolve_dbt_this_to_model_name(model_name: str) -> JinjaTransform: - """ - Takes an expression like "{{ this }}" and turns it into the provided "model_name" string - """ - return _make_single_expression_transform({"this": model_name}) - - -@ast_transform -def deduplicate_incremental_checks() -> JinjaTransform: - """ - Some files may have been designed to run with both the SQLMesh DBT loader and DBT itself and contain sections like: - - --- - select * from foo - where - {% if is_incremental() %}ds > (select max(ds)) from {{ this }}{% endif %} - {% if sqlmesh_incremental is defined %}ds BETWEEN {{ start_ds }} and {{ end_ds }}{% endif %} - --- - - This is transform detects usages of {% if sqlmesh_incremental ... %} - If it finds them, it: - - removes occurances of {% if is_incremental() %} in favour of the {% if sqlmesh_incremental %} check - - If no instances of {% if sqlmesh_incremental %} are found, nothing changes - - For for example, the above will be transformed into: - --- - select * from foo - where - ds BETWEEN {{ start_ds }} and {{ end_ds }} - --- - - But if it didnt contain the {% if sqlmesh_incremental %} block, this transform would output: - --- - select * from foo - where - {% if is_incremental() %}ds > (select max(ds)) from {{ this }}){% endif %} - --- - - """ - has_sqlmesh_incremental = False - - def _handle( - node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> t.Optional[j.Node]: - nonlocal has_sqlmesh_incremental - - if isinstance(node, j.Template): - for if_node in node.find_all(j.If): - if test_name := if_node.test.find(j.Name): - if test_name.name == "sqlmesh_incremental": - has_sqlmesh_incremental = True - - # only remove the {% if is_incremental() %} checks in the present of {% sqlmesh_incremental is defined %} checks - if has_sqlmesh_incremental: - if isinstance(node, j.If) and node.test: - if test_name := node.test.find(j.Name): - if test_name.name == "is_incremental": - return None - - return node - - return _handle - - -@ast_transform -def unpack_incremental_checks() -> JinjaTransform: - """ - This takes queries like: - - > select * from foo where {% if sqlmesh_incremental is defined %}ds BETWEEN {{ start_ds }} and {{ end_ds }}{% endif %} - > select * from foo where {% if is_incremental() %}ds > (select max(ds)) from foo.table){% endif %} - - And, if possible, removes the {% if sqlmesh_incremental is defined %} / {% is_incremental %} block to achieve: - - > select * from foo where ds BETWEEN {{ start_ds }} and {{ end_ds }} - > select * from foo where ds > (select max(ds)) from foo.table) - - Note that if there is a {% else %} portion to the block, there is no SQLMesh equivalent so in that case the check is untouched. - - Also, if both may be present in a model, run the deduplicate_incremental_checks() transform first so only one gets unpacked by this transform - """ - - def _handle(node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node]) -> j.Node: - if isinstance(node, j.If) and node.test: - if test_name := node.test.find(j.Name): - if ( - test_name.name in ("is_incremental", "sqlmesh_incremental") - and not node.elif_ - and not node.else_ - ): - return j.Output(node.body) - - return node - - return _handle - - -@ast_transform -def rewrite_sqlmesh_predefined_variables_to_sqlmesh_macro_syntax() -> JinjaTransform: - """ - If there are SQLMesh predefined variables in Jinja form, eg "{{ start_dt }}" - Rewrite them to eg "@start_dt" - - Example: - - select * from foo where ds between {{ start_dt }} and {{ end_dt }} - - > select * from foo where ds between @start_dt and @end_dt - """ - - mapping = {v: f"@{v}" for v in SQLMESH_PREDEFINED_MACRO_VARIABLES} - - literal_remapping = {"dt": "ts", "date": "ds"} - - def _mapping_func( - node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node], name: str - ) -> t.Optional[str]: - wrapped_in_literal = False - if prev and isinstance(prev, j.TemplateData): - data = prev.data.strip() - if data.endswith("'"): - wrapped_in_literal = True - - if wrapped_in_literal: - for original, new in literal_remapping.items(): - if name.endswith(original): - name = name.removesuffix(original) + new - - return mapping.get(name) - - return _make_single_expression_transform(_mapping_func) - - -@ast_transform -def append_dbt_package_kwarg_to_var_calls(package_name: t.Optional[str]) -> JinjaTransform: - """ " - If there are calls like: - - > {% if 'col_name' in var('history_columns') %} - - Assuming package_name=foo, change it to: - - > {% if 'col_name' in var('history_columns', __dbt_package="foo") %} - - The point of this is to give a hint to the "var" shim in SQLMesh Native so it knows which key - under "__dbt_packages__" in the project variables to look for - """ - - def _append( - node: j.Node, prev: t.Optional[j.Node], parent: t.Optional[j.Node] - ) -> t.Optional[j.Node]: - if package_name and isinstance(node, j.Call): - node.kwargs.append(j.Keyword("__dbt_package", j.Const(package_name))) - return node - - return _make_standalone_call_transform("var", _append) - - -@ast_transform -def unwrap_macros_in_string_literals() -> SQLGlotTransform: - """ - Given a query containing string literals *that match SQLMesh predefined macro variables* like: - - > select * from foo where ds between '@start_dt' and '@end_dt' - - Unwrap them into: - - > select * from foo where ds between @start_dt and @end_dt - """ - values_to_check = {f"@{var}": var for var in SQLMESH_PREDEFINED_MACRO_VARIABLES} - - def _transform(e: exp.Expression) -> exp.Expression: - if isinstance(e, exp.Literal) and e.is_string: - if (value := e.text("this")) and value in values_to_check: - return d.MacroVar( - this=values_to_check[value] - ) # MacroVar adds in the @ so dont want to add it twice - return e - - return _transform diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 594c5a8807..cd1c4b6c1a 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -98,12 +98,11 @@ def _load_scripts(self) -> t.Tuple[MacroRegistry, JinjaMacroRegistry]: for file in macro_files: self._track_file(file) - jinja_macros = JinjaMacroRegistry() - for project in self._load_projects(): - jinja_macros = jinja_macros.merge(project.context.jinja_macros) - jinja_macros.add_globals(project.context.jinja_globals) - - return (macro.get_registry(), jinja_macros) + # This doesn't do anything, the actual content will be loaded from the manifest + return ( + macro.get_registry(), + JinjaMacroRegistry(), + ) def _load_models( self, diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 6d31efe772..d2d1a52abc 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -590,7 +590,6 @@ def to_sqlmesh( kind=kind, start=self.start or context.sqlmesh_config.model_defaults.start, audit_definitions=audit_definitions, - path=model_kwargs.pop("path", self.path), # This ensures that we bypass query rendering that would otherwise be required to extract additional # dependencies from the model's SQL. # Note: any table dependencies that are not referenced using the `ref` macro will not be included. diff --git a/sqlmesh/dbt/target.py b/sqlmesh/dbt/target.py index 035b5b9e93..82574a044c 100644 --- a/sqlmesh/dbt/target.py +++ b/sqlmesh/dbt/target.py @@ -103,8 +103,26 @@ def load(cls, data: t.Dict[str, t.Any]) -> TargetConfig: The configuration of the provided profile target """ db_type = data["type"] - if config_class := TARGET_TYPE_TO_CONFIG_CLASS.get(db_type): - return config_class(**data) + if db_type == "databricks": + return DatabricksConfig(**data) + if db_type == "duckdb": + return DuckDbConfig(**data) + if db_type == "postgres": + return PostgresConfig(**data) + if db_type == "redshift": + return RedshiftConfig(**data) + if db_type == "snowflake": + return SnowflakeConfig(**data) + if db_type == "bigquery": + return BigQueryConfig(**data) + if db_type == "sqlserver": + return MSSQLConfig(**data) + if db_type == "trino": + return TrinoConfig(**data) + if db_type == "clickhouse": + return ClickhouseConfig(**data) + if db_type == "athena": + return AthenaConfig(**data) raise ConfigError(f"{db_type} not supported.") @@ -117,10 +135,6 @@ def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: """Converts target config to SQLMesh connection config""" raise NotImplementedError - @classmethod - def from_sqlmesh(cls, config: ConnectionConfig, **kwargs: t.Dict[str, t.Any]) -> "TargetConfig": - raise NotImplementedError - def attribute_dict(self) -> AttributeDict: fields = self.dict(include=SERIALIZABLE_FIELDS).copy() fields["target_name"] = self.name @@ -214,18 +228,6 @@ def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: **kwargs, ) - @classmethod - def from_sqlmesh(cls, config: ConnectionConfig, **kwargs: t.Dict[str, t.Any]) -> "DuckDbConfig": - if not isinstance(config, DuckDBConnectionConfig): - raise ValueError(f"Incorrect config type: {type(config)}") - - return cls( - path=config.database, - extensions=config.extensions, - settings=config.connector_config, - **kwargs, - ) - class SnowflakeConfig(TargetConfig): """ @@ -398,28 +400,6 @@ def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: **kwargs, ) - @classmethod - def from_sqlmesh( - cls, config: ConnectionConfig, **kwargs: t.Dict[str, t.Any] - ) -> "PostgresConfig": - if not isinstance(config, PostgresConnectionConfig): - raise ValueError(f"Incorrect config type: {type(config)}") - - return cls( - schema="public", - host=config.host, - user=config.user, - password=config.password, - port=config.port, - dbname=config.database, - keepalives_idle=config.keepalives_idle, - threads=config.concurrent_tasks, - connect_timeout=config.connect_timeout, - role=config.role, - sslmode=config.sslmode, - **kwargs, - ) - class RedshiftConfig(TargetConfig): """ @@ -664,39 +644,6 @@ def to_sqlmesh(self, **kwargs: t.Any) -> ConnectionConfig: **kwargs, ) - @classmethod - def from_sqlmesh( - cls, config: ConnectionConfig, **kwargs: t.Dict[str, t.Any] - ) -> "BigQueryConfig": - if not isinstance(config, BigQueryConnectionConfig): - raise ValueError(f"Incorrect config type: {type(config)}") - - return cls( - schema="__unknown__", - method=config.method, - project=config.project, - execution_project=config.execution_project, - quota_project=config.quota_project, - location=config.location, - threads=config.concurrent_tasks, - keyfile=config.keyfile, - keyfile_json=config.keyfile_json, - token=config.token, - refresh_token=config.refresh_token, - client_id=config.client_id, - client_secret=config.client_secret, - token_uri=config.token_uri, - scopes=config.scopes, - impersonated_service_account=config.impersonated_service_account, - job_creation_timeout_seconds=config.job_creation_timeout_seconds, - job_execution_timeout_seconds=config.job_execution_timeout_seconds, - job_retries=config.job_retries, - job_retry_deadline_seconds=config.job_retry_deadline_seconds, - priority=config.priority, - maximum_bytes_billed=config.maximum_bytes_billed, - **kwargs, - ) - class MSSQLConfig(TargetConfig): """ diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index d2d830c521..9764e625a4 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -22,7 +22,6 @@ CallNames = t.Tuple[t.Tuple[str, ...], t.Union[nodes.Call, nodes.Getattr]] SQLMESH_JINJA_PACKAGE = "sqlmesh.utils.jinja" -SQLMESH_DBT_COMPATIBILITY_PACKAGE = "sqlmesh.dbt.converter.jinja_builtins" def environment(**kwargs: t.Any) -> Environment: @@ -95,11 +94,7 @@ def extract(self, jinja: str, dialect: str = "") -> t.Dict[str, MacroInfo]: macro_str = self._find_sql(macro_start, self._next) macros[name] = MacroInfo( definition=macro_str, - depends_on=list( - extract_macro_references_and_variables(macro_str, dbt_target_name=dialect)[ - 0 - ] - ), + depends_on=list(extract_macro_references_and_variables(macro_str)[0]), ) self._advance() @@ -171,35 +166,6 @@ def parse() -> t.List[CallNames]: return parse() -def extract_dbt_adapter_dispatch_targets(jinja_str: str) -> t.List[t.Tuple[str, t.Optional[str]]]: - """ - Given a jinja string, identify {{ adapter.dispatch('foo','bar') }} calls and extract the (foo, bar) part as a tuple - """ - ast = ENVIRONMENT.parse(jinja_str) - - extracted = [] - - def _extract(node: nodes.Node, parent: t.Optional[nodes.Node] = None) -> None: - if ( - isinstance(node, nodes.Getattr) - and isinstance(parent, nodes.Call) - and (node_name := node.find(nodes.Name)) - ): - if node_name.name == "adapter" and node.attr == "dispatch": - call_args = [arg.value for arg in parent.args if isinstance(arg, nodes.Const)][0:2] - if len(call_args) == 1: - call_args.append(None) - macro_name, package = call_args - extracted.append((macro_name, package)) - - for child_node in node.iter_child_nodes(): - _extract(child_node, parent=node) - - _extract(ast) - - return extracted - - def is_variable_node(n: nodes.Node) -> bool: return ( isinstance(n, nodes.Call) @@ -209,33 +175,11 @@ def is_variable_node(n: nodes.Node) -> bool: def extract_macro_references_and_variables( - *jinja_strs: str, dbt_target_name: t.Optional[str] = None + *jinja_strs: str, ) -> t.Tuple[t.Set[MacroReference], t.Set[str]]: macro_references = set() variables = set() for jinja_str in jinja_strs: - if dbt_target_name and "adapter.dispatch" in jinja_str: - for dispatch_target_name, package in extract_dbt_adapter_dispatch_targets(jinja_str): - # here we are guessing at the macro names that the {{ adapter.dispatch() }} call will invoke - # there is a defined resolution order: https://docs.getdbt.com/reference/dbt-jinja-functions/dispatch - # we rely on JinjaMacroRegistry.trim() to tune the dependencies down into just the ones that actually exist - macro_references.add( - MacroReference(package=package, name=f"default__{dispatch_target_name}") - ) - macro_references.add( - MacroReference( - package=package, name=f"{dbt_target_name}__{dispatch_target_name}" - ) - ) - if package and package.startswith("dbt"): - # handle the case where macros like `current_timestamp()` in the `dbt` package expect an implementation in eg the `dbt_bigquery` package - macro_references.add( - MacroReference( - package=f"dbt_{dbt_target_name}", - name=f"{dbt_target_name}__{dispatch_target_name}", - ) - ) - for call_name, node in extract_call_names(jinja_str): if call_name[0] in (c.VAR, c.BLUEPRINT_VAR): if not is_variable_node(node): @@ -249,24 +193,7 @@ def extract_macro_references_and_variables( node = t.cast(nodes.Call, node) args = [jinja_call_arg_name(arg) for arg in node.args] if args and args[0]: - variable_name = args[0].lower() - - # check if this {{ var() }} reference is from a migrated DBT package - # if it is, there will be a __dbt_package= kwarg - dbt_package = next( - ( - kwarg.value - for kwarg in node.kwargs - if isinstance(kwarg, nodes.Keyword) and kwarg.key == "__dbt_package" - ), - None, - ) - if dbt_package and isinstance(dbt_package, nodes.Const): - dbt_package = dbt_package.value - # this convention is a flat way of referencing the nested values under `__dbt_packages__` in the SQLMesh project variables - variable_name = f"{c.MIGRATED_DBT_PACKAGES}.{dbt_package}.{variable_name}" - - variables.add(variable_name) + variables.add(args[0].lower()) elif call_name[0] == c.GATEWAY: variables.add(c.GATEWAY) elif len(call_name) == 1: @@ -344,19 +271,6 @@ def _convert( def trimmed(self) -> bool: return self._trimmed - @property - def all_macros(self) -> t.Iterable[t.Tuple[t.Optional[str], str, MacroInfo]]: - """ - Returns (package, macro_name, MacroInfo) tuples for every macro in this registry - Root macros will have package=None - """ - for name, macro in self.root_macros.items(): - yield None, name, macro - - for package, macros in self.packages.items(): - for name, macro in macros.items(): - yield (package, name, macro) - def add_macros(self, macros: t.Dict[str, MacroInfo], package: t.Optional[str] = None) -> None: """Adds macros to the target package. @@ -698,12 +612,7 @@ def jinja_call_arg_name(node: nodes.Node) -> str: def create_var(variables: t.Dict[str, t.Any]) -> t.Callable: - def _var( - var_name: str, default: t.Optional[t.Any] = None, **kwargs: t.Any - ) -> t.Optional[t.Any]: - if dbt_package := kwargs.get("__dbt_package"): - var_name = f"{c.MIGRATED_DBT_PACKAGES}.{dbt_package}.{var_name}" - + def _var(var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]: value = variables.get(var_name.lower(), default) if isinstance(value, SqlValue): return value.sql diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 10881dc493..d0fad16e76 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -31,7 +31,6 @@ from sqlmesh.core.engine_adapter.athena import AthenaEngineAdapter from sqlmesh.core.engine_adapter.duckdb import DuckDBEngineAdapter from sqlmesh.core.engine_adapter.redshift import RedshiftEngineAdapter -from sqlmesh.core.loader import MigratedDbtProjectLoader from sqlmesh.core.notification_target import ConsoleNotificationTarget from sqlmesh.core.user import User from sqlmesh.utils.errors import ConfigError @@ -1066,32 +1065,6 @@ def test_config_complex_types_supplied_as_json_strings_from_env(tmp_path: Path) assert conn.keyfile_json == {"foo": "bar"} -def test_loader_for_migrated_dbt_project(tmp_path: Path): - config_path = tmp_path / "config.yaml" - config_path.write_text(""" - gateways: - bigquery: - connection: - type: bigquery - project: unit-test - - default_gateway: bigquery - - model_defaults: - dialect: bigquery - - variables: - __dbt_project_name__: sushi -""") - - config = load_config_from_paths( - Config, - project_paths=[config_path], - ) - - assert config.loader == MigratedDbtProjectLoader - - def test_config_user_macro_function(tmp_path: Path) -> None: config_path = tmp_path / "config.yaml" config_path.write_text(""" diff --git a/tests/core/test_loader.py b/tests/core/test_loader.py index b3d605e353..14a20ec09a 100644 --- a/tests/core/test_loader.py +++ b/tests/core/test_loader.py @@ -4,9 +4,6 @@ from sqlmesh.core.config import Config, ModelDefaultsConfig from sqlmesh.core.context import Context from sqlmesh.utils.errors import ConfigError -import sqlmesh.core.constants as c -from sqlmesh.core.config import load_config_from_yaml -from sqlmesh.utils.yaml import dump @pytest.fixture @@ -204,129 +201,3 @@ def my_model(context, **kwargs): assert model.description == "model_payload_a" path_b.write_text(model_payload_b) context.load() # raise no error to duplicate key if the functions are identical (by registry class_method) - - -def test_load_migrated_dbt_adapter_dispatch_macros(tmp_path: Path): - init_example_project(tmp_path, engine_type="duckdb") - - migrated_package_path = tmp_path / "macros" / c.MIGRATED_DBT_PACKAGES / "dbt_utils" - migrated_package_path.mkdir(parents=True) - - (migrated_package_path / "deduplicate.sql").write_text(""" - {%- macro deduplicate(relation) -%} - {{ return(adapter.dispatch('deduplicate', 'dbt_utils')(relation)) }} - {% endmacro %} - """) - - (migrated_package_path / "default__deduplicate.sql").write_text(""" - {%- macro default__deduplicate(relation) -%} - select 'default impl' from {{ relation }} - {% endmacro %} - """) - - (migrated_package_path / "duckdb__deduplicate.sql").write_text(""" - {%- macro duckdb__deduplicate(relation) -%} - select 'duckdb impl' from {{ relation }} - {% endmacro %} - """) - - # this should be pruned from the JinjaMacroRegistry because the target is duckdb, not bigquery - (migrated_package_path / "bigquery__deduplicate.sql").write_text(""" - {%- macro bigquery__deduplicate(relation) -%} - select 'bigquery impl' from {{ relation }} - {% endmacro %} - """) - - (tmp_path / "models" / "test_model.sql").write_text(""" - MODEL ( - name sqlmesh_example.test, - kind FULL, - ); -JINJA_QUERY_BEGIN; -{{ dbt_utils.deduplicate(__migrated_ref(schema='sqlmesh_example', identifier='full_model')) }} -JINJA_END; - """) - - config_path = tmp_path / "config.yaml" - assert config_path.exists() - config = load_config_from_yaml(config_path) - config["variables"] = {} - config["variables"][c.MIGRATED_DBT_PROJECT_NAME] = "test" - - config_path.write_text(dump(config)) - - ctx = Context(paths=tmp_path) - - model = ctx.models['"db"."sqlmesh_example"."test"'] - assert model.dialect == "duckdb" - assert {(package, name) for package, name, _ in model.jinja_macros.all_macros} == { - ("dbt_utils", "deduplicate"), - ("dbt_utils", "default__deduplicate"), - ("dbt_utils", "duckdb__deduplicate"), - } - - assert ( - model.render_query_or_raise().sql(dialect="duckdb") - == """SELECT \'duckdb impl\' AS "duckdb impl" FROM "db"."sqlmesh_example"."full_model" AS "full_model\"""" - ) - - -def test_load_migrated_dbt_adapter_dispatch_macros_in_different_packages(tmp_path: Path): - # some things like dbt.current_timestamp() dispatch to macros in a different package - init_example_project(tmp_path, engine_type="duckdb") - - migrated_package_path_dbt = tmp_path / "macros" / c.MIGRATED_DBT_PACKAGES / "dbt" - migrated_package_path_dbt_duckdb = tmp_path / "macros" / c.MIGRATED_DBT_PACKAGES / "dbt_duckdb" - migrated_package_path_dbt.mkdir(parents=True) - migrated_package_path_dbt_duckdb.mkdir(parents=True) - - (migrated_package_path_dbt / "current_timestamp.sql").write_text(""" - {%- macro current_timestamp(relation) -%} - {{ return(adapter.dispatch('current_timestamp', 'dbt')()) }} - {% endmacro %} - """) - - (migrated_package_path_dbt / "default__current_timestamp.sql").write_text(""" - {% macro default__current_timestamp() -%} - {{ exceptions.raise_not_implemented('current_timestamp macro not implemented') }} - {%- endmacro %} - """) - - (migrated_package_path_dbt_duckdb / "duckdb__current_timestamp.sql").write_text(""" - {%- macro duckdb__current_timestamp() -%} - 'duckdb current_timestamp impl' - {% endmacro %} - """) - - (tmp_path / "models" / "test_model.sql").write_text(""" - MODEL ( - name sqlmesh_example.test, - kind FULL, - ); -JINJA_QUERY_BEGIN; -select {{ dbt.current_timestamp() }} as a -JINJA_END; - """) - - config_path = tmp_path / "config.yaml" - assert config_path.exists() - config = load_config_from_yaml(config_path) - config["variables"] = {} - config["variables"][c.MIGRATED_DBT_PROJECT_NAME] = "test" - - config_path.write_text(dump(config)) - - ctx = Context(paths=tmp_path) - - model = ctx.models['"db"."sqlmesh_example"."test"'] - assert model.dialect == "duckdb" - assert {(package, name) for package, name, _ in model.jinja_macros.all_macros} == { - ("dbt", "current_timestamp"), - ("dbt", "default__current_timestamp"), - ("dbt_duckdb", "duckdb__current_timestamp"), - } - - assert ( - model.render_query_or_raise().sql(dialect="duckdb") - == "SELECT 'duckdb current_timestamp impl' AS \"a\"" - ) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 9266a56c10..3850e08164 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -15,7 +15,6 @@ from sqlmesh.cli.project_init import init_example_project, ProjectTemplate from sqlmesh.core.environment import EnvironmentNamingInfo from sqlmesh.core.model.kind import TimeColumn, ModelKindName -from pydantic import ValidationError from sqlmesh import CustomMaterialization, CustomKind from pydantic import model_validator, ValidationError @@ -66,13 +65,7 @@ from sqlmesh.core.snapshot import Snapshot, SnapshotChangeCategory from sqlmesh.utils.date import TimeLike, to_datetime, to_ds, to_timestamp from sqlmesh.utils.errors import ConfigError, SQLMeshError, LinterError -from sqlmesh.utils.jinja import ( - JinjaMacroRegistry, - MacroInfo, - MacroExtractor, - MacroReference, - SQLMESH_DBT_COMPATIBILITY_PACKAGE, -) +from sqlmesh.utils.jinja import JinjaMacroRegistry, MacroInfo, MacroExtractor from sqlmesh.utils.metaprogramming import Executable, SqlValue from sqlmesh.core.macros import RuntimeStage from tests.utils.test_helpers import use_terminal_console @@ -6386,59 +6379,6 @@ def model_with_variables(context, **kwargs): assert df.to_dict(orient="records") == [{"a": "test_value", "b": "default_value", "c": None}] -def test_variables_migrated_dbt_package_macro(): - expressions = parse( - """ - MODEL( - name test_model, - kind FULL, - ); - - JINJA_QUERY_BEGIN; - SELECT '{{ var('TEST_VAR_A') }}' as a, '{{ test.test_macro_var() }}' as b - JINJA_END; - """, - default_dialect="bigquery", - ) - - jinja_macros = JinjaMacroRegistry( - create_builtins_module=SQLMESH_DBT_COMPATIBILITY_PACKAGE, - packages={ - "test": { - "test_macro_var": MacroInfo( - definition=""" - {% macro test_macro_var() %} - {{- var('test_var_b', __dbt_package='test') }} - {%- endmacro %}""", - depends_on=[MacroReference(name="var")], - ) - } - }, - ) - - model = load_sql_based_model( - expressions, - variables={ - "test_var_a": "test_var_a_value", - c.MIGRATED_DBT_PACKAGES: { - "test": {"test_var_b": "test_var_b_value", "unused": "unused_value"}, - }, - "test_var_c": "test_var_c_value", - }, - jinja_macros=jinja_macros, - migrated_dbt_project_name="test", - dialect="bigquery", - ) - assert model.python_env[c.SQLMESH_VARS] == Executable.value( - {"test_var_a": "test_var_a_value", "__dbt_packages__.test.test_var_b": "test_var_b_value"}, - sort_root_dict=True, - ) - assert ( - model.render_query().sql(dialect="bigquery") - == "SELECT 'test_var_a_value' AS `a`, 'test_var_b_value' AS `b`" - ) - - def test_load_external_model_python(sushi_context) -> None: @model( "test_load_external_model_python", @@ -8150,37 +8090,6 @@ def test_model_kind_to_expression(): ) -def test_incremental_by_unique_key_batch_concurrency(): - with pytest.raises(ValidationError, match=r"Input should be 1"): - load_sql_based_model( - d.parse(""" - MODEL ( - name db.table, - kind INCREMENTAL_BY_UNIQUE_KEY ( - unique_key a, - batch_concurrency 2 - ) - ); - select 1; - """) - ) - - model = load_sql_based_model( - d.parse(""" - MODEL ( - name db.table, - kind INCREMENTAL_BY_UNIQUE_KEY ( - unique_key a, - batch_concurrency 1 - ) - ); - select 1; - """) - ) - assert isinstance(model.kind, IncrementalByUniqueKeyKind) - assert model.kind.batch_concurrency == 1 - - def test_bad_model_kind(): with pytest.raises( SQLMeshError, diff --git a/tests/dbt/converter/conftest.py b/tests/dbt/converter/conftest.py deleted file mode 100644 index e8dffeb263..0000000000 --- a/tests/dbt/converter/conftest.py +++ /dev/null @@ -1,21 +0,0 @@ -from pathlib import Path -import typing as t -import pytest -from sqlmesh.core.context import Context - - -@pytest.fixture -def sushi_dbt_context(copy_to_temp_path: t.Callable) -> Context: - return Context(paths=copy_to_temp_path("examples/sushi_dbt")) - - -@pytest.fixture -def empty_dbt_context(copy_to_temp_path: t.Callable) -> Context: - fixture_path = Path(__file__).parent / "fixtures" / "empty_dbt_project" - assert fixture_path.exists() - - actual_path = copy_to_temp_path(fixture_path)[0] - - ctx = Context(paths=actual_path) - - return ctx diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/.gitignore b/tests/dbt/converter/fixtures/empty_dbt_project/.gitignore deleted file mode 100644 index 232ccd1d8c..0000000000 --- a/tests/dbt/converter/fixtures/empty_dbt_project/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target/ -logs/ diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/analyses/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/analyses/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/config.py b/tests/dbt/converter/fixtures/empty_dbt_project/config.py deleted file mode 100644 index e7e28c98e4..0000000000 --- a/tests/dbt/converter/fixtures/empty_dbt_project/config.py +++ /dev/null @@ -1,7 +0,0 @@ -from pathlib import Path - -from sqlmesh.dbt.loader import sqlmesh_config - -config = sqlmesh_config(Path(__file__).parent) - -test_config = config diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/dbt_project.yml b/tests/dbt/converter/fixtures/empty_dbt_project/dbt_project.yml deleted file mode 100644 index 007649e553..0000000000 --- a/tests/dbt/converter/fixtures/empty_dbt_project/dbt_project.yml +++ /dev/null @@ -1,22 +0,0 @@ - -name: 'test' -version: '1.0.0' -config-version: 2 -profile: 'test' - -model-paths: ["models"] -analysis-paths: ["analyses"] -test-paths: ["tests"] -seed-paths: ["seeds"] -macro-paths: ["macros"] -snapshot-paths: ["snapshots"] - -target-path: "target" - -models: - +start: Jan 1 2022 - -seeds: - +schema: raw - -vars: {} diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/macros/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/macros/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/models/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/models/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/models/sources.yml b/tests/dbt/converter/fixtures/empty_dbt_project/models/sources.yml deleted file mode 100644 index 49354831f4..0000000000 --- a/tests/dbt/converter/fixtures/empty_dbt_project/models/sources.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 - -sources: - - name: external - tables: - - name: orders diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/packages/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/packages/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/profiles.yml b/tests/dbt/converter/fixtures/empty_dbt_project/profiles.yml deleted file mode 100644 index 6d91ecbe65..0000000000 --- a/tests/dbt/converter/fixtures/empty_dbt_project/profiles.yml +++ /dev/null @@ -1,6 +0,0 @@ -test: - outputs: - in_memory: - type: duckdb - schema: project - target: in_memory diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/seeds/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/seeds/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/seeds/items.csv b/tests/dbt/converter/fixtures/empty_dbt_project/seeds/items.csv deleted file mode 100644 index 0f87cb2507..0000000000 --- a/tests/dbt/converter/fixtures/empty_dbt_project/seeds/items.csv +++ /dev/null @@ -1,94 +0,0 @@ -id,name,price,ds -0,Maguro,4.34,2022-01-01 -1,Ika,7.35,2022-01-01 -2,Aji,6.06,2022-01-01 -3,Hotate,8.5,2022-01-01 -4,Escolar,8.46,2022-01-01 -5,Sake,4.91,2022-01-01 -6,Tamago,4.94,2022-01-01 -7,Umi Masu,8.61,2022-01-01 -8,Bincho,9.71,2022-01-01 -9,Toro,9.13,2022-01-01 -10,Aoyagi,5.5,2022-01-01 -11,Hamachi,6.51,2022-01-01 -12,Tobiko,7.78,2022-01-01 -13,Unagi,7.99,2022-01-01 -14,Tako,5.59,2022-01-01 -0,Kani,8.22,2022-01-02 -1,Amaebi,9.14,2022-01-02 -2,Uni,4.55,2022-01-02 -3,Sake Toro,5.01,2022-01-02 -4,Maguro,9.95,2022-01-02 -5,Katsuo,9.03,2022-01-02 -6,Hamachi Toro,3.76,2022-01-02 -7,Iwashi,5.56,2022-01-02 -8,Tamago,6.96,2022-01-02 -9,Tai,5.84,2022-01-02 -10,Ika,3.23,2022-01-02 -0,Hirame,7.74,2022-01-03 -1,Uni,3.98,2022-01-03 -2,Tai,4.09,2022-01-03 -3,Kanpachi,7.55,2022-01-03 -4,Tobiko,9.87,2022-01-03 -5,Hotate,7.86,2022-01-03 -6,Iwashi,8.33,2022-01-03 -7,Ikura,5.98,2022-01-03 -8,Maguro,3.97,2022-01-03 -9,Tsubugai,4.51,2022-01-03 -10,Tako,8.35,2022-01-03 -11,Sake,3.38,2022-01-03 -12,Tamago,6.43,2022-01-03 -13,Ika,4.26,2022-01-03 -14,Unagi,7.42,2022-01-03 -0,Ikura,5.02,2022-01-04 -1,Tobiko,9.15,2022-01-04 -2,Hamachi,6.66,2022-01-04 -3,Bincho,8.4,2022-01-04 -4,Tsubugai,5.26,2022-01-04 -5,Hotate,8.92,2022-01-04 -6,Toro,7.52,2022-01-04 -7,Aji,7.49,2022-01-04 -8,Ebi,5.67,2022-01-04 -9,Kanpachi,7.51,2022-01-04 -10,Kani,6.97,2022-01-04 -11,Hirame,4.51,2022-01-04 -0,Saba,7.41,2022-01-05 -1,Unagi,8.45,2022-01-05 -2,Uni,3.67,2022-01-05 -3,Maguro,8.76,2022-01-05 -4,Katsuo,5.99,2022-01-05 -5,Bincho,9.15,2022-01-05 -6,Sake Toro,3.67,2022-01-05 -7,Aji,9.55,2022-01-05 -8,Umi Masu,9.88,2022-01-05 -9,Hamachi,6.53,2022-01-05 -10,Tai,6.83,2022-01-05 -11,Tsubugai,4.62,2022-01-05 -12,Ikura,4.86,2022-01-05 -13,Ahi,9.66,2022-01-05 -14,Hotate,7.85,2022-01-05 -0,Hamachi Toro,4.87,2022-01-06 -1,Ika,3.26,2022-01-06 -2,Kanpachi,8.63,2022-01-06 -3,Hirame,5.34,2022-01-06 -4,Katsuo,9.24,2022-01-06 -5,Iwashi,8.67,2022-01-06 -6,Sake Toro,9.75,2022-01-06 -7,Bincho,9.7,2022-01-06 -8,Aji,7.14,2022-01-06 -9,Hokigai,5.18,2022-01-06 -10,Umi Masu,9.43,2022-01-06 -11,Unagi,3.35,2022-01-06 -12,Sake,4.58,2022-01-06 -13,Aoyagi,5.54,2022-01-06 -0,Amaebi,6.94,2022-01-07 -1,Ebi,7.84,2022-01-07 -2,Saba,5.28,2022-01-07 -3,Anago,4.53,2022-01-07 -4,Escolar,7.28,2022-01-07 -5,Ahi,6.48,2022-01-07 -6,Katsuo,5.16,2022-01-07 -7,Umi Masu,6.09,2022-01-07 -8,Maguro,7.7,2022-01-07 -9,Hokigai,7.37,2022-01-07 -10,Sake Toro,6.99,2022-01-07 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/seeds/properties.yml b/tests/dbt/converter/fixtures/empty_dbt_project/seeds/properties.yml deleted file mode 100644 index 86ce6964fe..0000000000 --- a/tests/dbt/converter/fixtures/empty_dbt_project/seeds/properties.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: 2 - -seeds: - - name: items - columns: - - name: id - description: Item id - - name: name - description: Name of the item - - name: price - description: Price of the item - - name: ds - description: Date \ No newline at end of file diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/snapshots/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/snapshots/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/dbt/converter/fixtures/empty_dbt_project/tests/.gitkeep b/tests/dbt/converter/fixtures/empty_dbt_project/tests/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/dbt/converter/fixtures/jinja_nested_if.sql b/tests/dbt/converter/fixtures/jinja_nested_if.sql deleted file mode 100644 index e7a1bed137..0000000000 --- a/tests/dbt/converter/fixtures/jinja_nested_if.sql +++ /dev/null @@ -1,15 +0,0 @@ -{% if foo == 'bar' %} - baz - {% if baz == 'bing' %} - bong - {% else %} - qux - {% endif %} -{% elif a == fn(b) %} - {% if c == 'f' and fn1(a, c, 'foo') == 'test' %} - output1 - {% elif z is defined %} - output2 - {% endif %} - output -{% endif %} \ No newline at end of file diff --git a/tests/dbt/converter/fixtures/macro_dbt_incremental.sql b/tests/dbt/converter/fixtures/macro_dbt_incremental.sql deleted file mode 100644 index a76f60713b..0000000000 --- a/tests/dbt/converter/fixtures/macro_dbt_incremental.sql +++ /dev/null @@ -1,11 +0,0 @@ -{% macro incremental_by_time(col, time_type) %} - {% if is_incremental() %} - WHERE - {{ col }} > (select max({{ col }}) from {{ this }}) - {% endif %} - {% if sqlmesh_incremental is defined %} - {% set dates = incremental_dates_by_time_type(time_type) %} - WHERE - {{ col }} BETWEEN '{{ dates[0] }}' AND '{{ dates[1] }}' - {% endif %} -{% endmacro %} \ No newline at end of file diff --git a/tests/dbt/converter/fixtures/macro_func_with_params.sql b/tests/dbt/converter/fixtures/macro_func_with_params.sql deleted file mode 100644 index 06bb757ef9..0000000000 --- a/tests/dbt/converter/fixtures/macro_func_with_params.sql +++ /dev/null @@ -1,17 +0,0 @@ -{% macro func_with_params(amount, category) %} - case - {% for row in [ - { 'category': '1', 'range': [0, 10], 'consider': True }, - { 'category': '2', 'range': [11, 20], 'consider': None } - ] %} - when {{ category }} = '{{ row.category }}' - and {{ amount }} >= {{ row.range[0] }} - {% if row.consider is not none %} - and {{ amount }} < {{ row.range[1] }} - {% endif %} - then - ({{ amount }} * {{ row.range[0] }} + {{ row.range[1] }}) * 4 - {% endfor %} - else null - end -{% endmacro %} \ No newline at end of file diff --git a/tests/dbt/converter/fixtures/model_query_incremental.sql b/tests/dbt/converter/fixtures/model_query_incremental.sql deleted file mode 100644 index a9603dbcbb..0000000000 --- a/tests/dbt/converter/fixtures/model_query_incremental.sql +++ /dev/null @@ -1,34 +0,0 @@ -WITH cte AS ( - SELECT - oi.order_id AS order_id, - FROM {{ ref('order_items') }} AS oi - LEFT JOIN {{ ref('items') }} AS i - ON oi.item_id = i.id AND oi.ds = i.ds -{% if is_incremental() %} -WHERE - oi.ds > (select max(ds) from {{ this }}) -{% endif %} -{% if sqlmesh_incremental is defined %} -WHERE - oi.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' -{% endif %} -GROUP BY - oi.order_id, - oi.ds -) -SELECT - o.customer_id::INT AS customer_id, /* Customer id */ - SUM(ot.total)::NUMERIC AS revenue, /* Revenue from orders made by this customer */ - o.ds::TEXT AS ds /* Date */ -FROM {{ ref('orders') }} AS o - LEFT JOIN order_total AS ot - ON o.id = ot.order_id AND o.ds = ot.ds -{% if is_incremental() %} - WHERE o.ds > (select max(ds) from {{ this }}) -{% endif %} -{% if sqlmesh_incremental is defined %} - WHERE o.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' -{% endif %} -GROUP BY - o.customer_id, - o.ds \ No newline at end of file diff --git a/tests/dbt/converter/test_convert.py b/tests/dbt/converter/test_convert.py deleted file mode 100644 index 001b1f82cc..0000000000 --- a/tests/dbt/converter/test_convert.py +++ /dev/null @@ -1,105 +0,0 @@ -from pathlib import Path -from sqlmesh.core.context import Context -from sqlmesh.dbt.converter.convert import convert_project_files, resolve_fqns_to_model_names -import uuid -import sqlmesh.core.constants as c - - -def test_convert_project_files(sushi_dbt_context: Context, tmp_path: Path) -> None: - src_context = sushi_dbt_context - src_path = sushi_dbt_context.path - output_path = tmp_path / f"output_{uuid.uuid4().hex}" - - convert_project_files(src_path, output_path) - - target_context = Context(paths=output_path) - - assert src_context.models.keys() == target_context.models.keys() - - target_context.plan(auto_apply=True) - - -def test_convert_project_files_includes_library_macros( - sushi_dbt_context: Context, tmp_path: Path -) -> None: - src_path = sushi_dbt_context.path - output_path = tmp_path / f"output_{uuid.uuid4().hex}" - - (src_path / "macros" / "call_library.sql").write_text(""" -{% macro call_library() %} - {{ dbt.current_timestamp() }} -{% endmacro %} -""") - - convert_project_files(src_path, output_path) - - migrated_output_macros_path = output_path / "macros" / c.MIGRATED_DBT_PACKAGES - assert (migrated_output_macros_path / "dbt" / "current_timestamp.sql").exists() - # note: the DBT manifest is smart enough to prune "dbt / default__current_timestamp.sql" from the list so it is not migrated - assert (migrated_output_macros_path / "dbt_duckdb" / "duckdb__current_timestamp.sql").exists() - - -def test_resolve_fqns_to_model_names(empty_dbt_context: Context) -> None: - ctx = empty_dbt_context - - # macro that uses a property of {{ ref() }} and also creates another ref() - (ctx.path / "macros" / "foo.sql").write_text( - """ -{% macro foo(relation) %} - {{ relation.name }} r - left join {{ source('external', 'orders') }} et - on r.id = et.id -{% endmacro %} -""" - ) - - # model 1 - can be fully unwrapped - (ctx.path / "models" / "model1.sql").write_text( - """ -{{ - config( - materialized='incremental', - incremental_strategy='delete+insert', - time_column='ds' - ) -}} - -select * from {{ ref('items') }} -{% if is_incremental() %} - where ds > (select max(ds) from {{ this }}) -{% endif %} -""" - ) - - # model 2 - has ref passed to macro as parameter and also another ref nested in macro - (ctx.path / "models" / "model2.sql").write_text( - """ -select * from {{ foo(ref('model1')) }} union select * from {{ ref('items') }} -""" - ) - - ctx.load() - - assert len(ctx.models) == 3 - - model1 = ctx.models['"memory"."project"."model1"'] - model2 = ctx.models['"memory"."project"."model2"'] - - assert model1.depends_on == {'"memory"."project_raw"."items"'} - assert model2.depends_on == { - '"memory"."project"."model1"', - '"memory"."external"."orders"', - '"memory"."project_raw"."items"', - } - - # All dependencies in model 1 can be tracked by the native loader but its very difficult to cover all the edge cases at conversion time - # so we still populate depends_on() - assert resolve_fqns_to_model_names(ctx, model1.depends_on) == {"project_raw.items"} - - # For model 2, the external model "external.orders" should be removed from depends_on - # If it was output verbatim as depends_on ("memory"."external"."orders"), the native loader would throw an error like: - # - Error: Failed to load model definition, 'Dot' object is not iterable - assert resolve_fqns_to_model_names(ctx, model2.depends_on) == { - "project.model1", - "project_raw.items", - } diff --git a/tests/dbt/converter/test_jinja.py b/tests/dbt/converter/test_jinja.py deleted file mode 100644 index 5d3e4508d3..0000000000 --- a/tests/dbt/converter/test_jinja.py +++ /dev/null @@ -1,450 +0,0 @@ -import pytest -from sqlmesh.utils.jinja import ( - JinjaMacroRegistry, - MacroExtractor, - extract_macro_references_and_variables, -) -from sqlmesh.dbt.converter.jinja import JinjaGenerator, convert_jinja_query, convert_jinja_macro -import sqlmesh.dbt.converter.jinja_transforms as jt -from pathlib import Path -from sqlmesh.core.context import Context -import sqlmesh.core.dialect as d -from sqlglot import exp -from _pytest.mark.structures import ParameterSet -from sqlmesh.core.model import SqlModel, load_sql_based_model -from sqlmesh.utils import columns_to_types_all_known - - -def _load_fixture(name: str) -> ParameterSet: - return pytest.param( - (Path(__file__).parent / "fixtures" / name).read_text(encoding="utf8"), id=name - ) - - -@pytest.mark.parametrize( - "original_jinja", - [ - "select 1", - "select bar from {{ ref('foo') }} as f", - "select max(ds) from {{ this }}", - "{% if is_incremental() %}where ds > (select max(ds) from {{ this }}){% endif %}", - "foo {% if sqlmesh_incremental is defined %} bar {% endif %} bar", - "foo between '{{ start_ds }}' and '{{ end_ds }}'", - "{{ 42 }}", - "{{ foo.bar }}", - "{{ 'baz' }}", - "{{ col }} BETWEEN '{{ dates[0] }}' AND '{{ dates[1] }}'", - "{% set foo = bar(baz, bing='bong') %}", - "{% if a == 'ds' %}foo{% elif a == 'ts' %}bar{% elif a < 'ys' or (b != 'ds' and c >= 'ts') %}baz{% else %}bing{% endif %}", - "{% set my_string = my_string ~ stuff ~ ', ' ~ 1 %}", - "{{ context.do_some_action('param') }}", - "{% set big_ole_block %}foo{% endset %}", - "{% if not loop.last %}foo{% endif %}", - "{% for a, b in some_func(a=foo['bar'][0], b=c.d[5]).items() %}foo_{{ a }}_{{ b }}{% endfor %}", - "{{ column | replace(prefix, '') }}", - "{{ column | filter('a', foo='bar') }}", - "{% filter upper %}foo{% endfilter %}", - "{% filter foo(0, bar='baz') %}foo{% endfilter %}", - "{% if foo in ('bar', 'baz') %}bar{% endif %}", - "{% if foo not in ('bar', 'baz') %}bing{% endif %}", - "{% if (field.a if field.a else field.b) | lower not in ('c', 'd') %}foo{% endif %}", - "{% do foo.bar('baz') %}", - "{% set a = (col | lower + '_') + b %}", - "{{ foo[1:10] | lower }}", - "{{ foo[1:] }}", - "{{ foo[:1] }}", - "{% for col in all_columns if col.name in columns_to_compare and col.name in special_names %}{{ col }}{% endfor %}", - "{{ ' or ' if not loop.first else '' }}", - "{% set foo = ['a', 'b', c, d.e, f[0], g.h.i[0][1]] %}", - """{% set foo = "('%Y%m%d', partition_id)" %}""", - "{% set foo = (graph.nodes.values() | selectattr('name', 'equalto', model_name) | list)[0] %}", - "{% set foo.bar = baz.bing(database='foo') %}", - "{{ return(('some', 'tuple')) }}", - "{% call foo('bar', baz=True) %}bar{% endcall %}", - "{% call(user) dump_users(list_of_user) %}bar{% endcall %}", - "{% macro foo(a, b='default', c=None) %}{% endmacro %}", - # "{# some comment #}", #todo: comments get stripped entirely - # "foo\n{%- if bar -%} baz {% endif -%}", #todo: whitespace trim handling is a nice-to-have - _load_fixture("model_query_incremental.sql"), - _load_fixture("macro_dbt_incremental.sql"), - _load_fixture("jinja_nested_if.sql"), - ], -) -def test_generator_roundtrip(original_jinja: str) -> None: - registry = JinjaMacroRegistry() - env = registry.build_environment() - - ast = env.parse(original_jinja) - generated = JinjaGenerator().generate(ast) - - assert generated == original_jinja - - me = MacroExtractor() - # basically just test this doesnt throw an exception. - # The MacroExtractor uses SQLGLot's tokenizer and not Jinja's so these need to work when the converted project is loaded by the native loader - me.extract(generated) - - -def test_generator_sql_comment_macro(): - jinja_str = "-- before sql comment{% macro foo() %}-- inner sql comment{% endmacro %}" - - registry = JinjaMacroRegistry() - env = registry.build_environment() - - ast = env.parse(jinja_str) - generated = JinjaGenerator().generate(ast) - - assert ( - generated == "-- before sql comment\n{% macro foo() %}-- inner sql comment\n{% endmacro %}" - ) - - # check roundtripping an existing newline doesnt keep adding newlines - assert JinjaGenerator().generate(env.parse(generated)) == generated - - -@pytest.mark.parametrize("original_jinja", [_load_fixture("macro_func_with_params.sql")]) -def test_generator_roundtrip_ignore_whitespace(original_jinja: str) -> None: - """ - This makes the following assumptions: - - SQL isnt too sensitive about indentation / whitespace - - The Jinja AST doesnt capture enough information to perfectly replicate the input template with regards to whitespace handling - - So if, disregarding whitespace, the original input string is the same as the AST being run through the generator: the test passes - """ - registry = JinjaMacroRegistry() - env = registry.build_environment() - - ast = env.parse(original_jinja) - - generated = JinjaGenerator().generate(ast) - - assert " ".join(original_jinja.split()) == " ".join(generated.split()) - - -def test_convert_jinja_query(sushi_dbt_context: Context) -> None: - model = sushi_dbt_context.models['"memory"."sushi"."customer_revenue_by_day"'] - assert isinstance(model, SqlModel) - - query = model.query - assert isinstance(query, d.JinjaQuery) - - result = convert_jinja_query(sushi_dbt_context, model, query) - - assert isinstance(result, exp.Query) - - assert ( - result.sql(dialect=model.dialect, pretty=True) - == """WITH order_total AS ( - SELECT - oi.order_id AS order_id, - SUM(oi.quantity * i.price) AS total, - oi.ds AS ds - FROM sushi_raw.order_items AS oi - LEFT JOIN sushi_raw.items AS i - ON oi.item_id = i.id AND oi.ds = i.ds - WHERE - oi.ds BETWEEN @start_ds AND @end_ds - GROUP BY - oi.order_id, - oi.ds -) -SELECT - CAST(o.customer_id AS INT) AS customer_id, /* Customer id */ - CAST(SUM(ot.total) AS DOUBLE) AS revenue, /* Revenue from orders made by this customer */ - CAST(o.ds AS TEXT) AS ds /* Date */ -FROM sushi_raw.orders AS o -LEFT JOIN order_total AS ot - ON o.id = ot.order_id AND o.ds = ot.ds -WHERE - o.ds BETWEEN @start_ds AND @end_ds -GROUP BY - o.customer_id, - o.ds""" - ) - - -def test_convert_jinja_query_exclude_transform(empty_dbt_context: Context) -> None: - ctx = empty_dbt_context - - (ctx.path / "models" / "model1.sql").write_text(""" - {{ - config( - materialized='incremental', - incremental_strategy='delete+insert', - time_column='ds' - ) - }} - - select * from {{ ref('items') }} - {% if is_incremental() %} - where ds > (select max(ds) from {{ this }}) - {% endif %} - """) - - ctx.load() - - model = ctx.models['"memory"."project"."model1"'] - assert isinstance(model, SqlModel) - - query = model.query - assert isinstance(query, d.JinjaQuery) - - converted_query = convert_jinja_query( - ctx, - model, - query, - exclude=[jt.resolve_dbt_ref_to_model_name, jt.rewrite_dbt_ref_to_migrated_ref], - ) - sql = converted_query.sql() - - assert "{{ ref('items') }}" in sql - assert "{{ this }}" not in sql - assert "{% if is_incremental() %}" not in sql - assert "{% endif %}" not in sql - - -def test_convert_jinja_query_self_referencing(empty_dbt_context: Context) -> None: - ctx = empty_dbt_context - - (ctx.path / "models" / "model1.sql").write_text(""" - {{ - config( - materialized='incremental', - incremental_strategy='delete+insert', - time_column='ds' - ) - }} - - select * from {{ ref('items') }} - {% if is_incremental() %} - where ds > (select max(ds) from {{ this }}) - {% endif %} - """) - - ctx.load() - - model = ctx.models['"memory"."project"."model1"'] - assert model.columns_to_types_or_raise - assert ( - not model.depends_on_self - ) # the DBT loader doesnt detect self-references within is_incremental blocks - assert isinstance(model, SqlModel) - - query = model.query - assert isinstance(query, d.JinjaQuery) - - converted_query = convert_jinja_query(ctx, model, query) - converted_model_definition = model.copy().render_definition()[0].sql() - - # load from scratch to use the native loader and clear @cached_property's - ctx.upsert_model( - load_sql_based_model( - expressions=[d.parse_one(converted_model_definition), converted_query], - default_catalog=ctx.default_catalog, - ) - ) - converted_model = ctx.models['"memory"."project"."model1"'] - assert isinstance(converted_model, SqlModel) - - assert not "{% is_incremental" in converted_model.query.sql() - assert ( - converted_model.depends_on_self - ) # Once the is_incremental blocks are removed, the model can be detected as self referencing - assert columns_to_types_all_known( - converted_model.columns_to_types_or_raise - ) # columns to types must all be known for self-referencing models - - -def test_convert_jinja_query_self_referencing_columns_to_types_not_all_known( - empty_dbt_context: Context, -) -> None: - ctx = empty_dbt_context - - (ctx.path / "models" / "model1.sql").write_text(""" - {{ - config( - materialized='incremental', - incremental_strategy='delete+insert', - time_column='ds' - ) - }} - - select id, name, ds from external.table - {% if is_incremental() %} - where ds > (select max(ds) from {{ this }}) - {% endif %} - """) - - ctx.load() - - model = ctx.models['"memory"."project"."model1"'] - assert model.columns_to_types_or_raise - assert ( - not model.depends_on_self - ) # the DBT loader doesnt detect self-references within is_incremental blocks - assert isinstance(model, SqlModel) - - query = model.query - assert isinstance(query, d.JinjaQuery) - - converted_query = convert_jinja_query(ctx, model, query) - converted_model_definition = model.render_definition()[0].sql() - - # load from scratch to use the native loader and clear @cached_property's - ctx.upsert_model( - load_sql_based_model( - expressions=[d.parse_one(converted_model_definition), converted_query], - jinja_macros=model.jinja_macros, - default_catalog=ctx.default_catalog, - ) - ) - converted_model = ctx.models['"memory"."project"."model1"'] - assert isinstance(converted_model, SqlModel) - - # {% is_incremental() %} block should be retained because removing it would make the model self-referencing but the columns_to_types - # arent all known so this would create a load error like: Error: Self-referencing models require inferrable column types. - assert "{% if is_incremental" in converted_model.query.sql() - assert "{{ this }}" not in converted_model.query.sql() - assert not converted_model.depends_on_self - - assert not columns_to_types_all_known( - converted_model.columns_to_types_or_raise - ) # this is ok because the model is not self-referencing - - -def test_convert_jinja_query_migrated_ref(empty_dbt_context: Context) -> None: - ctx = empty_dbt_context - - (ctx.path / "models" / "model1.sql").write_text(""" - {{ - config( - materialized='incremental', - incremental_strategy='delete+insert', - time_column='ds' - ) - }} - - {% macro ref_handler(relation) %} - {{ relation.name }} - {% endmacro %} - - select * from {{ ref_handler(ref("items")) }} - """) - - ctx.load() - - model = ctx.models['"memory"."project"."model1"'] - assert isinstance(model, SqlModel) - query = model.query - assert isinstance(query, d.JinjaQuery) - - converted_query = convert_jinja_query(ctx, model, query) - - assert ( - """select * from {{ ref_handler(__migrated_ref(database='memory', schema='project_raw', identifier='items', sqlmesh_model_name='project_raw.items')) }}""" - in converted_query.sql() - ) - - -def test_convert_jinja_query_post_statement(empty_dbt_context: Context) -> None: - ctx = empty_dbt_context - - (ctx.path / "models" / "model1.sql").write_text(""" - {{ - config( - materialized='incremental', - incremental_strategy='delete+insert', - time_column='ds', - post_hook="create index foo_idx on {{ this }} (id)" - ) - }} - - select * from {{ ref("items") }} - """) - - ctx.load() - - model = ctx.models['"memory"."project"."model1"'] - assert isinstance(model, SqlModel) - - assert model.post_statements - post_statement = model.post_statements[0] - assert isinstance(post_statement, d.JinjaStatement) - - converted_post_statement = convert_jinja_query(ctx, model, post_statement) - - assert "CREATE INDEX foo_idx ON project.model1(id)" in converted_post_statement.sql( - dialect="duckdb" - ) - - -@pytest.mark.parametrize( - "input,expected", - [ - ( - """ - {% macro incremental_by_time(col, time_type) %} - {% if is_incremental() %} - WHERE - {{ col }} > (select max({{ col }}) from {{ this }}) - {% endif %} - {% if sqlmesh_incremental is defined %} - {% set dates = incremental_dates_by_time_type(time_type) %} - WHERE - {{ col }} BETWEEN '{{ dates[0] }}' AND '{{ dates[1] }}' - {% endif %} - {% endmacro %} - """, - """ - {% macro incremental_by_time(col, time_type) %} - {% set dates = incremental_dates_by_time_type(time_type) %} - WHERE - {{ col }} BETWEEN '{{ dates[0] }}' AND '{{ dates[1] }}' - {% endmacro %} - """, - ), - ( - """ - {% macro foo(iterations) %} - with base as ( - select * from {{ ref('customer_revenue_by_day') }} - ), - iter as ( - {% for i in range(0, iterations) %} - 'iter_{{ i }}' as iter_num_{{ i }} - {% if not loop.last %},{% endif %} - {% endfor %} - ) - select 1 - {% endmacro %}""", - """ - {% macro foo(iterations) %} - with base as ( - select * from sushi.customer_revenue_by_day - ), - iter as ( - {% for i in range(0, iterations) %} - 'iter_{{ i }}' as iter_num_{{ i }} - {% if not loop.last %},{% endif %} - {% endfor %} - ) - select 1 - {% endmacro %}""", - ), - ( - """{% macro expand_ref(model_name) %}{{ ref(model_name) }}{% endmacro %}""", - """{% macro expand_ref(model_name) %}{{ ref(model_name) }}{% endmacro %}""", - ), - ], -) -def test_convert_jinja_macro(input: str, expected: str, sushi_dbt_context: Context) -> None: - result = convert_jinja_macro(sushi_dbt_context, input.strip()) - - assert " ".join(result.split()) == " ".join(expected.strip().split()) - - -def test_extract_macro_references_and_variables() -> None: - input = """JINJA_QUERY('{%- set something = "'"~var("variable").split("|") -%}""" - _, variables = extract_macro_references_and_variables(input) - assert len(variables) == 1 - assert variables == {"variable"} diff --git a/tests/dbt/converter/test_jinja_transforms.py b/tests/dbt/converter/test_jinja_transforms.py deleted file mode 100644 index c7d060ea40..0000000000 --- a/tests/dbt/converter/test_jinja_transforms.py +++ /dev/null @@ -1,453 +0,0 @@ -import pytest -import typing as t -from sqlglot import parse_one -from sqlmesh.core.model import create_sql_model, create_external_model -from sqlmesh.dbt.converter.jinja import transform, JinjaGenerator -import sqlmesh.dbt.converter.jinja_transforms as jt -from sqlmesh.dbt.converter.common import JinjaTransform -from sqlmesh.utils.jinja import environment, Environment, ENVIRONMENT -from sqlmesh.core.context import Context -from sqlmesh.core.config import Config, ModelDefaultsConfig - - -def transform_str( - input: str, handler: JinjaTransform, environment: t.Optional[Environment] = None -) -> str: - environment = environment or ENVIRONMENT - ast = environment.parse(input) - return JinjaGenerator().generate(transform(ast, handler)) - - -@pytest.mark.parametrize( - "input,expected", - [ - ("select * from {{ ref('bar') }} as t", "select * from foo.bar as t"), - ("select * from {{ ref('bar', version=1) }} as t", "select * from foo.bar_v1 as t"), - ("select * from {{ ref('bar', v=1) }} as t", "select * from foo.bar_v1 as t"), - ( - "select * from {{ ref('unknown') }} as t", - "select * from __unresolved_ref__.unknown as t", - ), - ( - "{% macro foo() %}select * from {{ ref('bar') }}{% endmacro %}", - "{% macro foo() %}select * from foo.bar{% endmacro %}", - ), - # these shouldnt be transformed as the macro call might rely on some property of the Relation object returned by ref() - ("{{ dbt_utils.union_relations([ref('foo')]) }},", None), - ("select * from {% if some_macro(ref('bar')) %}foo{% endif %}", None), - ( - "select * from {% if some_macro(ref('bar')) %}{{ ref('bar') }}{% endif %}", - "select * from {% if some_macro(ref('bar')) %}foo.bar{% endif %}", - ), - ("{{ some_macro(ref('bar')) }}", None), - ("{{ some_macro(table=ref('bar')) }}", None), - ], -) -def test_resolve_dbt_ref_to_model_name(input: str, expected: t.Optional[str]) -> None: - expected = expected or input - - from dbt.adapters.base import BaseRelation - - # note: bigquery dialect chosen because its identifiers have backticks - # but internally SQLMesh stores model fqn with double quotes - config = Config(model_defaults=ModelDefaultsConfig(dialect="bigquery")) - ctx = Context(config=config) - ctx.default_catalog = "sqlmesh" - - assert ctx.default_catalog == "sqlmesh" - assert ctx.default_dialect == "bigquery" - - model = create_sql_model( - name="foo.bar", query=parse_one("select 1"), default_catalog=ctx.default_catalog - ) - model2 = create_sql_model( - name="foo.bar_v1", query=parse_one("select 1"), default_catalog=ctx.default_catalog - ) - ctx.upsert_model(model) - ctx.upsert_model(model2) - - assert '"sqlmesh"."foo"."bar"' in ctx.models - - def _resolve_ref(ref_name: str, version: t.Optional[int] = None) -> t.Optional[BaseRelation]: - if ref_name == "bar": - identifier = "bar" - if version: - identifier = f"bar_v{version}" - - relation = BaseRelation.create( - database="sqlmesh", schema="foo", identifier=identifier, quote_character="`" - ) - assert ( - relation.render() == "`sqlmesh`.`foo`.`bar`" - if not version - else f"`sqlmesh`.`foo`.`bar_v{version}`" - ) - return relation - return None - - jinja_env = environment() - jinja_env.globals["ref"] = _resolve_ref - - assert ( - transform_str( - input, - jt.resolve_dbt_ref_to_model_name(ctx.models, jinja_env, dialect=ctx.default_dialect), - ) - == expected - ) - - -@pytest.mark.parametrize( - "input,expected", - [ - ( - "select * from {{ ref('bar') }} as t", - "select * from {{ __migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar') }} as t", - ), - ( - "{% macro foo() %}select * from {{ ref('bar') }}{% endmacro %}", - "{% macro foo() %}select * from {{ __migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar') }}{% endmacro %}", - ), - ( - "{{ dbt_utils.union_relations([ref('bar')]) }}", - "{{ dbt_utils.union_relations([__migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar')]) }}", - ), - ( - "select * from {% if some_macro(ref('bar')) %}foo{% endif %}", - "select * from {% if some_macro(__migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar')) %}foo{% endif %}", - ), - ( - "select * from {% if some_macro(ref('bar')) %}{{ ref('bar') }}{% endif %}", - "select * from {% if some_macro(__migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar')) %}{{ __migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar') }}{% endif %}", - ), - ( - "{{ some_macro(ref('bar')) }}", - "{{ some_macro(__migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar')) }}", - ), - ( - "{{ some_macro(table=ref('bar')) }}", - "{{ some_macro(table=__migrated_ref(database='sqlmesh', schema='foo', identifier='bar', sqlmesh_model_name='foo.bar')) }}", - ), - ], -) -def test_rewrite_dbt_ref_to_migrated_ref(input: str, expected: t.Optional[str]) -> None: - expected = expected or input - - from dbt.adapters.base import BaseRelation - - # note: bigquery dialect chosen because its identifiers have backticks - # but internally SQLMesh stores model fqn with double quotes - config = Config(model_defaults=ModelDefaultsConfig(dialect="bigquery")) - ctx = Context(config=config) - ctx.default_catalog = "sqlmesh" - - assert ctx.default_catalog == "sqlmesh" - assert ctx.default_dialect == "bigquery" - - model = create_sql_model( - name="foo.bar", query=parse_one("select 1"), default_catalog=ctx.default_catalog - ) - ctx.upsert_model(model) - - assert '"sqlmesh"."foo"."bar"' in ctx.models - - def _resolve_ref(ref_name: str) -> t.Optional[BaseRelation]: - if ref_name == "bar": - relation = BaseRelation.create( - database="sqlmesh", schema="foo", identifier="bar", quote_character="`" - ) - assert relation.render() == "`sqlmesh`.`foo`.`bar`" - return relation - return None - - jinja_env = environment() - jinja_env.globals["ref"] = _resolve_ref - - assert ( - transform_str( - input, - jt.rewrite_dbt_ref_to_migrated_ref(ctx.models, jinja_env, dialect=ctx.default_dialect), - ) - == expected - ) - - -@pytest.mark.parametrize( - "input,expected", - [ - ("select * from {{ source('upstream', 'foo') }} as t", "select * from upstream.foo as t"), - ("select * from {{ source('unknown', 'foo') }} as t", "select * from unknown.foo as t"), - ( - "{% macro foo() %}select * from {{ source('upstream', 'foo') }}{% endmacro %}", - "{% macro foo() %}select * from upstream.foo{% endmacro %}", - ), - # these shouldnt be transformed as the macro call might rely on some property of the Relation object returned by source() - ("select * from {% if some_macro(source('upstream', 'foo')) %}foo{% endif %}", None), - ("{{ dbt_utils.union_relations([source('upstream', 'foo')]) }},", None), - ( - "select * from {% if some_macro(source('upstream', 'foo')) %}{{ source('upstream', 'foo') }}{% endif %}", - "select * from {% if some_macro(source('upstream', 'foo')) %}upstream.foo{% endif %}", - ), - ("{{ some_macro(source('upstream', 'foo')) }}", None), - ("{% set results = run_query('select foo from ' ~ source('schema', 'table')) %}", None), - ], -) -def test_resolve_dbt_source_to_model_name(input: str, expected: t.Optional[str]) -> None: - expected = expected or input - - from dbt.adapters.base import BaseRelation - - # note: bigquery dialect chosen because its identifiers have backticks - # but internally SQLMesh stores model fqn with double quotes - config = Config(model_defaults=ModelDefaultsConfig(dialect="bigquery")) - ctx = Context(config=config) - ctx.default_catalog = "sqlmesh" - - assert ctx.default_catalog == "sqlmesh" - assert ctx.default_dialect == "bigquery" - - model = create_external_model(name="upstream.foo", default_catalog=ctx.default_catalog) - ctx.upsert_model(model) - - assert '"sqlmesh"."upstream"."foo"' in ctx.models - - def _resolve_source(schema_name: str, table_name: str) -> t.Optional[BaseRelation]: - if schema_name == "upstream" and table_name == "foo": - relation = BaseRelation.create( - database="sqlmesh", schema="upstream", identifier="foo", quote_character="`" - ) - assert relation.render() == "`sqlmesh`.`upstream`.`foo`" - return relation - return None - - jinja_env = environment() - jinja_env.globals["source"] = _resolve_source - - assert ( - transform_str( - input, - jt.resolve_dbt_source_to_model_name(ctx.models, jinja_env, dialect=ctx.default_dialect), - ) - == expected - ) - - -@pytest.mark.parametrize( - "input,expected", - [ - ( - "select * from {{ source('upstream', 'foo') }} as t", - "select * from {{ __migrated_source(database='sqlmesh', schema='upstream', identifier='foo') }} as t", - ), - ( - "select * from {{ source('unknown', 'foo') }} as t", - "select * from {{ source('unknown', 'foo') }} as t", - ), - ( - "{% macro foo() %}select * from {{ source('upstream', 'foo') }}{% endmacro %}", - "{% macro foo() %}select * from {{ __migrated_source(database='sqlmesh', schema='upstream', identifier='foo') }}{% endmacro %}", - ), - ( - "select * from {% if some_macro(source('upstream', 'foo')) %}foo{% endif %}", - "select * from {% if some_macro(__migrated_source(database='sqlmesh', schema='upstream', identifier='foo')) %}foo{% endif %}", - ), - ( - "{{ dbt_utils.union_relations([source('upstream', 'foo')]) }},", - "{{ dbt_utils.union_relations([__migrated_source(database='sqlmesh', schema='upstream', identifier='foo')]) }},", - ), - ( - "select * from {% if some_macro(source('upstream', 'foo')) %}{{ source('upstream', 'foo') }}{% endif %}", - "select * from {% if some_macro(__migrated_source(database='sqlmesh', schema='upstream', identifier='foo')) %}{{ __migrated_source(database='sqlmesh', schema='upstream', identifier='foo') }}{% endif %}", - ), - ( - "{{ some_macro(source('upstream', 'foo')) }}", - "{{ some_macro(__migrated_source(database='sqlmesh', schema='upstream', identifier='foo')) }}", - ), - ( - "{% set results = run_query('select foo from ' ~ source('upstream', 'foo')) %}", - "{% set results = run_query('select foo from ' ~ __migrated_source(database='sqlmesh', schema='upstream', identifier='foo')) %}", - ), - ], -) -def test_rewrite_dbt_source_to_migrated_source(input: str, expected: t.Optional[str]) -> None: - expected = expected or input - - from dbt.adapters.base import BaseRelation - - # note: bigquery dialect chosen because its identifiers have backticks - # but internally SQLMesh stores model fqn with double quotes - config = Config(model_defaults=ModelDefaultsConfig(dialect="bigquery")) - ctx = Context(config=config) - ctx.default_catalog = "sqlmesh" - - assert ctx.default_catalog == "sqlmesh" - assert ctx.default_dialect == "bigquery" - - model = create_external_model(name="upstream.foo", default_catalog=ctx.default_catalog) - ctx.upsert_model(model) - - assert '"sqlmesh"."upstream"."foo"' in ctx.models - - def _resolve_source(schema_name: str, table_name: str) -> t.Optional[BaseRelation]: - if schema_name == "upstream" and table_name == "foo": - relation = BaseRelation.create( - database="sqlmesh", schema="upstream", identifier="foo", quote_character="`" - ) - assert relation.render() == "`sqlmesh`.`upstream`.`foo`" - return relation - return None - - jinja_env = environment() - jinja_env.globals["source"] = _resolve_source - - assert ( - transform_str( - input, - jt.rewrite_dbt_source_to_migrated_source( - ctx.models, jinja_env, dialect=ctx.default_dialect - ), - ) - == expected - ) - - -@pytest.mark.parametrize( - "input,expected", - [ - ("select * from {{ this }}", "select * from foo.bar"), - ("{% if foo(this) %}bar{% endif %}", None), - ("select * from {{ this.identifier }}", None), - ], -) -def test_resolve_dbt_this_to_model_name(input: str, expected: t.Optional[str]): - expected = expected or input - assert transform_str(input, jt.resolve_dbt_this_to_model_name("foo.bar")) == expected - - -@pytest.mark.parametrize( - "input,expected", - [ - # sqlmesh_incremental present, is_incremental() block removed - ( - """ - select * from foo where - {% if is_incremental() %}ds > (select max(ds)) from {{ this }}){% endif %} - {% if sqlmesh_incremental is defined %}ds BETWEEN {{ start_ds }} and {{ end_ds }}{% endif %} - """, - """ - select * from foo - where - {% if sqlmesh_incremental is defined %}ds BETWEEN {{ start_ds }} and {{ end_ds }}{% endif %} - """, - ), - # sqlmesh_incremental is NOT present; is_incremental() blocks untouched - ( - """ - select * from foo - where - {% if is_incremental() %}ds > (select max(ds)) from {{ this }}){% endif %} - """, - """ - select * from foo - where - {% if is_incremental() %}ds > (select max(ds)) from {{ this }}){% endif %} - """, - ), - ], -) -def test_deduplicate_incremental_checks(input: str, expected: str) -> None: - assert " ".join(transform_str(input, jt.deduplicate_incremental_checks()).split()) == " ".join( - expected.split() - ) - - -@pytest.mark.parametrize( - "input,expected", - [ - # is_incremental() removed - ( - "select * from foo where {% if is_incremental() %}ds >= (select max(ds) from {{ this }} ){% endif %}", - "select * from foo where ds >= (select max(ds) from {{ this }} )", - ), - # sqlmesh_incremental removed - ( - "select * from foo where {% if sqlmesh_incremental is defined %}ds BETWEEN {{ start_ds }} and {{ end_ds }}{% endif %}", - "select * from foo where ds BETWEEN {{ start_ds }} and {{ end_ds }}", - ), - # else untouched - ( - "select * from foo where {% if is_incremental() %}ds >= (select max(ds) from {{ this }} ){% else %}ds is not null{% endif %}", - "select * from foo where {% if is_incremental() %}ds >= (select max(ds) from {{ this }} ){% else %}ds is not null{% endif %}", - ), - ], -) -def test_unpack_incremental_checks(input: str, expected: str) -> None: - assert " ".join(transform_str(input, jt.unpack_incremental_checks()).split()) == " ".join( - expected.split() - ) - - -@pytest.mark.parametrize( - "input,expected", - [ - ("{{ start_ds }}", "@start_ds"), - ( - "select id, ds from foo where ds between {{ start_ts }} and {{ end_ts }}", - "select id, ds from foo where ds between @start_ts and @end_ts", - ), - ("select {{ some_macro(start_ts) }}", None), - ("{{ start_date }}", "@start_date"), - ("'{{ start_date }}'", "'@start_ds'"), # date inside string literal should remain a string - ], -) -def test_rewrite_sqlmesh_predefined_variables_to_sqlmesh_macro_syntax( - input: str, expected: t.Optional[str] -) -> None: - expected = expected or input - assert ( - transform_str(input, jt.rewrite_sqlmesh_predefined_variables_to_sqlmesh_macro_syntax()) - == expected - ) - - -@pytest.mark.parametrize( - "input,expected,package", - [ - ("{{ var('foo') }}", "{{ var('foo') }}", None), - ("{{ var('foo') }}", "{{ var('foo', __dbt_package='test') }}", "test"), - ( - "{{ var('foo', 'default') }}", - "{{ var('foo', 'default', __dbt_package='test') }}", - "test", - ), - ( - "{% if 'col_name' in var('history_columns') %}bar{% endif %}", - "{% if 'col_name' in var('history_columns', __dbt_package='test') %}bar{% endif %}", - "test", - ), - ], -) -def test_append_dbt_package_kwarg_to_var_calls( - input: str, expected: str, package: t.Optional[str] -) -> None: - assert ( - transform_str(input, jt.append_dbt_package_kwarg_to_var_calls(package_name=package)) - == expected - ) - - -@pytest.mark.parametrize( - "input,expected", - [ - ( - "select * from foo where ds between '@start_dt' and '@end_dt'", - "SELECT * FROM foo WHERE ds BETWEEN @start_dt AND @end_dt", - ), - ( - "select * from foo where bar <> '@unrelated'", - "SELECT * FROM foo WHERE bar <> '@unrelated'", - ), - ], -) -def test_unwrap_macros_in_string_literals(input: str, expected: str) -> None: - assert parse_one(input).transform(jt.unwrap_macros_in_string_literals()).sql() == expected diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index e974ea6fc2..f78419889d 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -37,11 +37,9 @@ def get_environment_objects(controller: GithubController, environment: str) -> t def get_num_days_loaded(controller: GithubController, environment: str, model: str) -> int: - return int( - controller._context.engine_adapter.fetchdf( - f"SELECT distinct event_date FROM sushi__{environment}.{model}" - ).count() - ) + return controller._context.engine_adapter.fetchdf( + f"SELECT distinct event_date FROM sushi__{environment}.{model}" + ).shape[0] def get_columns( diff --git a/tests/utils/test_jinja.py b/tests/utils/test_jinja.py index 3660adaa95..5eb00aeb3c 100644 --- a/tests/utils/test_jinja.py +++ b/tests/utils/test_jinja.py @@ -9,8 +9,6 @@ MacroReturnVal, call_name, nodes, - extract_macro_references_and_variables, - extract_dbt_adapter_dispatch_targets, ) @@ -177,54 +175,6 @@ def test_macro_registry_trim(): assert not trimmed_registry_for_package_b.root_macros -def test_macro_registry_trim_keeps_dbt_adapter_dispatch(): - registry = JinjaMacroRegistry() - extractor = MacroExtractor() - - registry.add_macros( - extractor.extract( - """ - {% macro foo(col) %} - {{ adapter.dispatch('foo', 'test_package') }} - {% endmacro %} - - {% macro default__foo(col) %} - foo_{{ col }} - {% endmacro %} - - {% macro unrelated() %}foo{% endmacro %} - """, - dialect="duckdb", - ), - package="test_package", - ) - - assert sorted(list(registry.packages["test_package"].keys())) == [ - "default__foo", - "foo", - "unrelated", - ] - assert sorted(str(r) for r in registry.packages["test_package"]["foo"].depends_on) == [ - "adapter.dispatch", - "test_package.default__foo", - "test_package.duckdb__foo", - ] - - query_str = """ - select * from {{ test_package.foo('bar') }} - """ - - references, _ = extract_macro_references_and_variables(query_str, dbt_target_name="test") - references_list = list(references) - assert len(references_list) == 1 - assert str(references_list[0]) == "test_package.foo" - - trimmed_registry = registry.trim(references) - - # duckdb__foo is missing from this list because it's not actually defined as a macro - assert sorted(list(trimmed_registry.packages["test_package"].keys())) == ["default__foo", "foo"] - - def test_macro_return(): macros = "{% macro test_return() %}{{ macro_return([1, 2, 3]) }}{% endmacro %}" @@ -352,31 +302,3 @@ def test_dbt_adapter_macro_scope(): rendered = registry.build_environment().from_string("{{ spark__macro_a() }}").render() assert rendered.strip() == "macro_a" - - -def test_extract_dbt_adapter_dispatch_targets(): - assert extract_dbt_adapter_dispatch_targets(""" - {% macro my_macro(arg1, arg2) -%} - {{ return(adapter.dispatch('my_macro')(arg1, arg2)) }} - {% endmacro %} - """) == [("my_macro", None)] - - assert extract_dbt_adapter_dispatch_targets(""" - {% macro my_macro(arg1, arg2) -%} - {{ return(adapter.dispatch('my_macro', 'foo')(arg1, arg2)) }} - {% endmacro %} - """) == [("my_macro", "foo")] - - assert extract_dbt_adapter_dispatch_targets("""{{ adapter.dispatch('my_macro') }}""") == [ - ("my_macro", None) - ] - - assert extract_dbt_adapter_dispatch_targets(""" - {% macro foo() %} - {{ adapter.dispatch('my_macro') }} - {{ some_other_call() }} - {{ return(adapter.dispatch('other_macro', 'other_package')) }} - {% endmacro %} - """) == [("my_macro", None), ("other_macro", "other_package")] - - assert extract_dbt_adapter_dispatch_targets("no jinja") == [] From bba2e3ac91dfa264af3cbe3c897facebca890ad4 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:49:57 +0300 Subject: [PATCH 0777/1056] Chore!: bump sqlglot to v27.9.0 (#5244) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f371cdee0e..d3886562d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.8.0", + "sqlglot[rs]~=27.9.0", "tenacity", "time-machine", "json-stream" From 1c4d0a8035f2b370f9dd7062c01abc70979a361d Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:54:38 +0300 Subject: [PATCH 0778/1056] Chore!: add migration script to warn about dbt `data_type`-related diffs (#5245) --- .../v0092_warn_about_dbt_data_type_diff.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py diff --git a/sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py b/sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py new file mode 100644 index 0000000000..08ff1b1de2 --- /dev/null +++ b/sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py @@ -0,0 +1,48 @@ +""" +Warns dbt users about potential diffs due to corrected data_type handling. + +SQLMesh previously treated dbt's schema.yml data_type field as columns_to_types, which +doesn't match dbt's behavior. dbt only uses data_type for contracts/validation, not DDL. +This fix may cause diffs if tables were created with incorrect types. + +More context: https://github.com/TobikoData/sqlmesh/pull/5231 +""" + +import json + +from sqlglot import exp + +from sqlmesh.core.console import get_console + +SQLMESH_DBT_PACKAGE = "sqlmesh.dbt" + + +def migrate(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + warning = ( + "SQLMesh previously misinterpreted dbt's schema.yml 'data_type' field as actual " + "column types, but dbt only uses these for contracts/validation, not in actual " + "DDL statements. This has been fixed to match dbt's actual behavior. Your existing " + "tables may have been created with incorrect column types. After this migration, run " + "'sqlmesh diff prod' to check for column type differences, and if any are found, " + "apply a plan to correct the table schemas. For more details, see: " + "https://github.com/TobikoData/sqlmesh/pull/5231." + ) + + for (snapshot,) in engine_adapter.fetchall( + exp.select("snapshot").from_(snapshots_table), quote_identifiers=True + ): + parsed_snapshot = json.loads(snapshot) + node = parsed_snapshot["node"] + + jinja_macros = node.get("jinja_macros") or {} + create_builtins_module = jinja_macros.get("create_builtins_module") or "" + + if create_builtins_module == SQLMESH_DBT_PACKAGE and node.get("columns"): + get_console().log_warning(warning) + return From 4c94ee13d2a3da1ae24b4429798ae1920d68be52 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Thu, 28 Aug 2025 17:30:33 +0300 Subject: [PATCH 0779/1056] Fix(risingwave): Recreate materialized views (#5195) --- sqlmesh/core/snapshot/evaluator.py | 22 ++++--- .../integration/test_integration.py | 65 +++++++++++++++++++ .../integration/test_integration_bigquery.py | 47 -------------- 3 files changed, 79 insertions(+), 55 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 90186faba7..87a6d15c42 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -2300,13 +2300,19 @@ def insert( render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: - snapshot = kwargs["snapshot"] + # We should recreate MVs across supported engines (Snowflake, BigQuery etc) because + # if upstream tables were recreated (e.g FULL models), the MVs would be silently invalidated. + # The only exception to that rule is RisingWave which doesn't support CREATE OR REPLACE, so upstream + # models don't recreate their physical tables for the MVs to be invalidated. + # However, even for RW we still want to recreate MVs to avoid stale references, as is the case with normal views. + # The flag is_first_insert is used for that matter as a signal to recreate the MV if the snapshot's intervals + # have been cleared by `should_force_rebuild` + is_materialized_view = self._is_materialized_view(model) + must_recreate_view = not self.adapter.HAS_VIEW_BINDING or ( + is_materialized_view and is_first_insert + ) - if ( - not snapshot.is_materialized_view - and self.adapter.HAS_VIEW_BINDING - and self.adapter.table_exists(table_name) - ): + if self.adapter.table_exists(table_name) and not must_recreate_view: logger.info("Skipping creation of the view '%s'", table_name) return @@ -2315,8 +2321,8 @@ def insert( table_name, query_or_df, model.columns_to_types, - replace=not self.adapter.HAS_VIEW_BINDING, - materialized=self._is_materialized_view(model), + replace=must_recreate_view, + materialized=is_materialized_view, view_properties=kwargs.get("physical_properties", model.physical_properties), table_description=model.description, column_descriptions=model.column_descriptions, diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 19a45329d5..fcbc711f49 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -7,7 +7,10 @@ import typing as t import shutil from datetime import datetime, timedelta, date +from unittest import mock from unittest.mock import patch +import logging + import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 import pytest @@ -3748,3 +3751,65 @@ def _set_config(gateway: str, config: Config) -> None: "incremental_model", "seed_model", ] + + +def test_materialized_view_evaluation(ctx: TestContext, mocker: MockerFixture): + adapter = ctx.engine_adapter + dialect = ctx.dialect + + if not adapter.SUPPORTS_MATERIALIZED_VIEWS: + pytest.skip(f"Skipping engine {dialect} as it does not support materialized views") + elif dialect in ("snowflake", "databricks"): + pytest.skip(f"Skipping {dialect} as they're not enabled on standard accounts") + + model_name = ctx.table("test_tbl") + mview_name = ctx.table("test_mview") + + sqlmesh = ctx.create_context() + + sqlmesh.upsert_model( + load_sql_based_model( + d.parse( + f""" + MODEL (name {model_name}, kind FULL); + + SELECT 1 AS col + """ + ) + ) + ) + + sqlmesh.upsert_model( + load_sql_based_model( + d.parse( + f""" + MODEL (name {mview_name}, kind VIEW (materialized true)); + + SELECT * FROM {model_name} + """ + ) + ) + ) + + def _assert_mview_value(value: int): + df = adapter.fetchdf(f"SELECT * FROM {mview_name.sql(dialect=dialect)}") + assert df["col"][0] == value + + # Case 1: Ensure that plan is successful and we can query the materialized view + sqlmesh.plan(auto_apply=True, no_prompts=True) + + _assert_mview_value(value=1) + + # Case 2: Ensure that we can change the underlying table and the materialized view is recreated + sqlmesh.upsert_model( + load_sql_based_model(d.parse(f"""MODEL (name {model_name}, kind FULL); SELECT 2 AS col""")) + ) + + logger = logging.getLogger("sqlmesh.core.snapshot.evaluator") + + with mock.patch.object(logger, "info") as mock_logger: + sqlmesh.plan(auto_apply=True, no_prompts=True) + + assert any("Replacing view" in call[0][0] for call in mock_logger.call_args_list) + + _assert_mview_value(value=2) diff --git a/tests/core/engine_adapter/integration/test_integration_bigquery.py b/tests/core/engine_adapter/integration/test_integration_bigquery.py index 66a647dc80..0a6dd6b2a4 100644 --- a/tests/core/engine_adapter/integration/test_integration_bigquery.py +++ b/tests/core/engine_adapter/integration/test_integration_bigquery.py @@ -441,53 +441,6 @@ def test_table_diff_table_name_matches_column_name(ctx: TestContext): assert row_diff.full_match_count == 1 -def test_materialized_view_evaluation(ctx: TestContext, engine_adapter: BigQueryEngineAdapter): - model_name = ctx.table("test_tbl") - mview_name = ctx.table("test_mview") - - sqlmesh = ctx.create_context() - - sqlmesh.upsert_model( - load_sql_based_model( - d.parse( - f""" - MODEL (name {model_name}, kind FULL); - - SELECT 1 AS col - """ - ) - ) - ) - - sqlmesh.upsert_model( - load_sql_based_model( - d.parse( - f""" - MODEL (name {mview_name}, kind VIEW (materialized true)); - - SELECT * FROM {model_name} - """ - ) - ) - ) - - # Case 1: Ensure that plan is successful and we can query the materialized view - sqlmesh.plan(auto_apply=True, no_prompts=True) - - df = engine_adapter.fetchdf(f"SELECT * FROM {mview_name.sql(dialect=ctx.dialect)}") - assert df["col"][0] == 1 - - # Case 2: Ensure that we can change the underlying table and the materialized view is recreated - sqlmesh.upsert_model( - load_sql_based_model(d.parse(f"""MODEL (name {model_name}, kind FULL); SELECT 2 AS col""")) - ) - - sqlmesh.plan(auto_apply=True, no_prompts=True) - - df = engine_adapter.fetchdf(f"SELECT * FROM {mview_name.sql(dialect=ctx.dialect)}") - assert df["col"][0] == 2 - - def test_correlation_id_in_job_labels(ctx: TestContext): model_name = ctx.table("test") From 711d68cc394b423e0462147dcfd9f1dde03dbf4f Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 28 Aug 2025 09:50:04 -0700 Subject: [PATCH 0780/1056] chore: create make command install dbt version (#5251) --- .github/workflows/pr.yaml | 25 ++----------------------- Makefile | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 3e715e1318..65c45b38e5 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -73,17 +73,7 @@ jobs: strategy: fail-fast: false matrix: - dbt-version: - [ - '1.3.0', - '1.4.0', - '1.5.0', - '1.6.0', - '1.7.0', - '1.8.0', - '1.9.0', - '1.10.0', - ] + dbt-version: ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10'] steps: - uses: actions/checkout@v5 - name: Set up Python @@ -96,18 +86,7 @@ jobs: run: | uv venv .venv source .venv/bin/activate - sed -i 's/"pydantic>=2.0.0"/"pydantic"/g' pyproject.toml - if [[ "${{ matrix.dbt-version }}" == "1.10.0" ]]; then - # For 1.10.0: only add version to dbt-core, remove versions from all adapter packages - sed -i -E 's/"(dbt-core)[^"]*"/"\1~=${{ matrix.dbt-version }}"/g' pyproject.toml - # Remove version constraints from all dbt adapter packages - sed -i -E 's/"(dbt-(bigquery|duckdb|snowflake|athena-community|clickhouse|databricks|redshift|trino))[^"]*"/"\1"/g' pyproject.toml - else - # For other versions: apply version to all dbt packages - sed -i -E 's/"(dbt-[^">=<~!]+)[^"]*"/"\1~=${{ matrix.dbt-version }}"/g' pyproject.toml - fi - UV=1 make install-dev - uv pip install pydantic>=2.0.0 --reinstall + UV=1 make install-dev-dbt-${{ matrix.dbt-version }} - name: Run dbt tests # We can't run slow tests across all engines due to tests requiring DuckDB and old versions # of DuckDB require a version of DuckDB we no longer support diff --git a/Makefile b/Makefile index 04306946cd..5335da5dc6 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,13 @@ else PIP := pip3 endif +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + SED_INPLACE = sed -i '' +else + SED_INPLACE = sed -i +endif + install-dev: $(PIP) install -e ".[dev,web,slack,dlt,lsp]" ./examples/custom_materializations @@ -15,6 +22,33 @@ install-doc: install-pre-commit: pre-commit install +install-dev-dbt-%: + @version="$*"; \ + period_count=$$(echo "$$version" | tr -cd '.' | wc -c); \ + if [ "$$period_count" -eq 0 ]; then \ + version="$${version:0:1}.$${version:1}"; \ + elif [ "$$period_count" -eq 1 ]; then \ + version="$$version.0"; \ + fi; \ + 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"; \ + $(SED_INPLACE) -E 's/"(dbt-core)[^"]*"/"\1~='"$$version"'"/g' pyproject.toml; \ + $(SED_INPLACE) -E 's/"(dbt-(bigquery|duckdb|snowflake|athena-community|clickhouse|databricks|redshift|trino))[^"]*"/"\1"/g' pyproject.toml; \ + else \ + echo "Applying version $$version to all dbt packages"; \ + $(SED_INPLACE) -E 's/"(dbt-[^"><=~!]+)[^"]*"/"\1~='"$$version"'"/g' pyproject.toml; \ + fi; \ + $(MAKE) install-dev; \ + if [ "$$version" = "1.6.0" ]; then \ + echo "Applying pydantic override for dbt 1.6.0"; \ + $(PIP) install 'pydantic>=2.0.0' --reinstall; \ + fi; \ + mv pyproject.toml.backup pyproject.toml; \ + echo "Restored original pyproject.toml" + style: pre-commit run --all-files From 5318b81ed839cf3baa429592335a8f2c37f6405c Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 28 Aug 2025 19:55:20 +0300 Subject: [PATCH 0781/1056] Fix: properly handle empty `vars` mapping in dbt_project.yml (#5248) --- sqlmesh/dbt/project.py | 2 +- tests/dbt/test_config.py | 57 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/project.py b/sqlmesh/dbt/project.py index 581660943a..4af30958f5 100644 --- a/sqlmesh/dbt/project.py +++ b/sqlmesh/dbt/project.py @@ -99,7 +99,7 @@ def load(cls, context: DbtContext, variables: t.Optional[t.Dict[str, t.Any]] = N package = package_loader.load(path.parent) packages[package.name] = package - all_project_variables = {**project_yaml.get("vars", {}), **(variable_overrides or {})} + all_project_variables = {**(project_yaml.get("vars") or {}), **(variable_overrides or {})} for name, package in packages.items(): package_vars = all_project_variables.get(name) diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 852ad02d5e..a79ee59b69 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -1090,3 +1090,60 @@ def test_sqlmesh_model_kwargs_columns_override(): {"c": ColumnConfig(name="c", data_type="uinteger")}, ) assert kwargs.get("columns") == {"c": exp.DataType.build(exp.DataType.Type.UINT)} + + +def test_empty_vars_config(tmp_path): + """Test that a dbt project can be loaded with an empty vars config.""" + dbt_project_dir = tmp_path / "test_project" + dbt_project_dir.mkdir() + + # Create a minimal dbt_project.yml with empty vars + dbt_project_yml = dbt_project_dir / "dbt_project.yml" + dbt_project_yml.write_text(""" +name: test_empty_vars + +version: "1.0.0" +config-version: 2 + +profile: test_empty_vars + +models: + +start: Jan 1 2022 + +# Empty vars section - various ways to specify empty +vars: + """) + + # Create a minimal profiles.yml + profiles_yml = dbt_project_dir / "profiles.yml" + profiles_yml.write_text(""" +test_empty_vars: + outputs: + dev: + type: duckdb + schema: test + target: dev + """) + + # Create a simple model + model = dbt_project_dir / "models" / "some_model.sql" + model.parent.mkdir(parents=True, exist_ok=True) + model.write_text("SELECT 1 as id") + + # Load the project + from sqlmesh.dbt.context import DbtContext + from sqlmesh.dbt.project import Project + from sqlmesh.core.config import Config + + context = DbtContext(project_root=dbt_project_dir, sqlmesh_config=Config()) + + # This should not raise an error even with empty vars + project = Project.load(context) + + # Verify the project loaded successfully + assert project.packages["test_empty_vars"] is not None + assert project.packages["test_empty_vars"].name == "test_empty_vars" + + # Verify the variables are empty (not causing any issues) + assert project.packages["test_empty_vars"].variables == {} + assert project.context.variables == {} From a58dcf02dbf259e6b2382d679d835e378ba823c5 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 28 Aug 2025 12:06:29 -0700 Subject: [PATCH 0782/1056] feat: support custom macros in dbt vars (#5250) --- sqlmesh/dbt/context.py | 5 ++++- tests/dbt/test_config.py | 9 ++++++++- tests/dbt/test_manifest.py | 8 ++++++-- tests/fixtures/dbt/sushi_test/dbt_project.yml | 2 +- .../fixtures/dbt/sushi_test/macros/top_waiters_limit.sql | 3 +++ 5 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/dbt/sushi_test/macros/top_waiters_limit.sql diff --git a/sqlmesh/dbt/context.py b/sqlmesh/dbt/context.py index d76cccbce7..6af291f478 100644 --- a/sqlmesh/dbt/context.py +++ b/sqlmesh/dbt/context.py @@ -101,7 +101,10 @@ def add_variables(self, variables: t.Dict[str, t.Any]) -> None: self._jinja_environment = None def set_and_render_variables(self, variables: t.Dict[str, t.Any], package: str) -> None: - jinja_environment = self.jinja_macros.build_environment(**self.jinja_globals) + package_macros = self.jinja_macros.copy( + update={"top_level_packages": [*self.jinja_macros.top_level_packages, package]} + ) + jinja_environment = package_macros.build_environment(**self.jinja_globals) def _render_var(value: t.Any) -> t.Any: if isinstance(value, str): diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index a79ee59b69..bfbef72ff8 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -8,6 +8,8 @@ from pytest_mock import MockerFixture from sqlglot import exp + +from sqlmesh import Context from sqlmesh.core.audit import StandaloneAudit from sqlmesh.core.config import Config, ModelDefaultsConfig from sqlmesh.core.dialect import jinja_query @@ -360,7 +362,7 @@ def test_variables(assert_exp_eq, sushi_test_project): # Finally, check that variable scoping & overwriting (some_var) works as expected expected_sushi_variables = { "yet_another_var": 1, - "top_waiters:limit": 10, + "top_waiters:limit": "{{ get_top_waiters_limit() }}", "top_waiters:revenue": "revenue", "customers:boo": ["a", "b"], "nested_vars": { @@ -389,6 +391,11 @@ def test_variables(assert_exp_eq, sushi_test_project): assert sushi_test_project.packages["customers"].variables == expected_customer_variables +@pytest.mark.slow +def test_jinja_in_dbt_variables(sushi_test_dbt_context: Context): + assert sushi_test_dbt_context.render("sushi.top_waiters").sql().endswith("LIMIT 10") + + @pytest.mark.slow def test_nested_variables(sushi_test_project): model_config = ModelConfig( diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index 27a1d22910..5f2f6fb37f 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -34,7 +34,11 @@ def test_manifest_helper(caplog): refs={"sushi.waiter_revenue_by_day", "waiter_revenue_by_day"}, variables={"top_waiters:revenue", "top_waiters:limit"}, model_attrs={"columns", "config"}, - macros=[MacroReference(name="ref"), MacroReference(name="var")], + macros=[ + MacroReference(name="get_top_waiters_limit"), + MacroReference(name="ref"), + MacroReference(name="var"), + ], ) assert models["top_waiters"].materialized == "view" assert models["top_waiters"].dialect_ == "postgres" @@ -181,7 +185,7 @@ def test_variable_override(): profile.target, model_defaults=ModelDefaultsConfig(start="2020-01-01"), ) - assert helper.models()["top_waiters"].limit_value == 10 + assert helper.models()["top_waiters"].limit_value.strip() == "10" helper = ManifestHelper( project_path, diff --git a/tests/fixtures/dbt/sushi_test/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_project.yml index ecd060159b..ea0041d107 100644 --- a/tests/fixtures/dbt/sushi_test/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/dbt_project.yml @@ -39,7 +39,7 @@ sources: identifier: false vars: - top_waiters:limit: 10 + top_waiters:limit: "{{ get_top_waiters_limit() }}" 'top_waiters:revenue': "revenue" # The following are only used for testing purposes diff --git a/tests/fixtures/dbt/sushi_test/macros/top_waiters_limit.sql b/tests/fixtures/dbt/sushi_test/macros/top_waiters_limit.sql new file mode 100644 index 0000000000..5fb56335e9 --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/macros/top_waiters_limit.sql @@ -0,0 +1,3 @@ +{% macro get_top_waiters_limit() %} +10 +{% endmacro %} From 5011741bc440644667e83ba391a9ff69944bb66f Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 28 Aug 2025 12:45:04 -0700 Subject: [PATCH 0783/1056] chore: improve adapter compatibility old dbt releases (#5252) --- Makefile | 8 ++++++-- tests/dbt/test_config.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 5335da5dc6..668769e2ef 100644 --- a/Makefile +++ b/Makefile @@ -43,8 +43,12 @@ install-dev-dbt-%: fi; \ $(MAKE) install-dev; \ if [ "$$version" = "1.6.0" ]; then \ - echo "Applying pydantic override for dbt 1.6.0"; \ - $(PIP) install 'pydantic>=2.0.0' --reinstall; \ + 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' --reinstall; \ + fi; \ + if [ "$$version" = "1.7.0" ]; then \ + echo "Applying overrides for dbt 1.7.0"; \ + $(PIP) install 'databricks-sdk==0.28.0' --reinstall; \ fi; \ mv pyproject.toml.backup pyproject.toml; \ echo "Restored original pyproject.toml" diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index bfbef72ff8..a20bdb071d 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -15,6 +15,7 @@ from sqlmesh.core.dialect import jinja_query from sqlmesh.core.model import SqlModel from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange +from sqlmesh.dbt.builtin import Api from sqlmesh.dbt.column import ColumnConfig from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext @@ -1099,6 +1100,23 @@ def test_sqlmesh_model_kwargs_columns_override(): assert kwargs.get("columns") == {"c": exp.DataType.build(exp.DataType.Type.UINT)} +@pytest.mark.parametrize( + "dialect", + [ + "databricks", + "duckdb", + "postgres", + "redshift", + "snowflake", + "bigquery", + "trino", + "clickhouse", + ], +) +def test_api_class_loading(dialect: str): + Api(dialect) + + def test_empty_vars_config(tmp_path): """Test that a dbt project can be loaded with an empty vars config.""" dbt_project_dir = tmp_path / "test_project" From d53a58ef9c490f875eb47e96399a733c2f81442a Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:59:06 -0700 Subject: [PATCH 0784/1056] fix: pydantic v1 issues with dbt 1.6 semantic models (#5255) --- .github/workflows/pr.yaml | 39 +++++++++++++++++++ sqlmesh/dbt/manifest.py | 9 +++++ .../fixtures/dbt/sushi_test/models/schema.yml | 20 ++++++++++ 3 files changed, 68 insertions(+) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 65c45b38e5..ad187df449 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -92,6 +92,44 @@ jobs: # of DuckDB require a version of DuckDB we no longer support run: | source .venv/bin/activate + + # Remove semantic_models and metrics sections for DBT versions < 1.6.0 + # Using explicit list to avoid version comparison issues + if [[ "${{ matrix.dbt-version }}" == "1.3" ]] || \ + [[ "${{ matrix.dbt-version }}" == "1.4" ]] || \ + [[ "${{ matrix.dbt-version }}" == "1.5" ]]; then + + echo "DBT version is ${{ matrix.dbt-version }} (< 1.6.0), removing semantic_models and metrics sections..." + + schema_file="tests/fixtures/dbt/sushi_test/models/schema.yml" + if [[ -f "$schema_file" ]]; then + echo "Modifying $schema_file..." + + # Create a temporary file + temp_file=$(mktemp) + + # Use awk to remove semantic_models and metrics sections + awk ' + /^semantic_models:/ { in_semantic=1; next } + /^metrics:/ { in_metrics=1; next } + /^[^ ]/ && (in_semantic || in_metrics) { + in_semantic=0; + in_metrics=0 + } + !in_semantic && !in_metrics { print } + ' "$schema_file" > "$temp_file" + + # Move the temp file back + mv "$temp_file" "$schema_file" + + echo "Successfully removed semantic_models and metrics sections" + else + echo "Schema file not found at $schema_file, skipping..." + fi + else + echo "DBT version is ${{ matrix.dbt-version }} (>= 1.6.0), keeping semantic_models and metrics sections" + fi + make dbt-fast-test - name: Test SQLMesh info in sushi_dbt working-directory: ./examples/sushi_dbt @@ -104,4 +142,5 @@ jobs: else echo "DBT version is ${{ matrix.dbt-version }} (>= 1.5.0), keeping version parameters" fi + sqlmesh info --skip-connection diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index bc7660f4ce..edb9004d6f 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -80,6 +80,13 @@ IGNORED_PACKAGES = {"elementary"} BUILTIN_CALLS = {*BUILTIN_GLOBALS, *BUILTIN_FILTERS} +# Patch Semantic Manifest to skip validation and avoid Pydantic v1 errors on DBT 1.6 +# We patch for 1.7+ since we don't care about semantic models +if DBT_VERSION >= (1, 6, 0): + from dbt.contracts.graph.semantic_manifest import SemanticManifest # type: ignore + + SemanticManifest.validate = lambda _: True # type: ignore + class ManifestHelper: def __init__( @@ -456,6 +463,8 @@ def _load_manifest(self) -> Manifest: register_adapter(runtime_config) # type: ignore manifest = ManifestLoader.get_full_manifest(runtime_config) + # This adapter doesn't care about semantic models so we clear them out to avoid issues + manifest.semantic_models = {} reset_adapters() return manifest diff --git a/tests/fixtures/dbt/sushi_test/models/schema.yml b/tests/fixtures/dbt/sushi_test/models/schema.yml index ac99269207..48b8b814d3 100644 --- a/tests/fixtures/dbt/sushi_test/models/schema.yml +++ b/tests/fixtures/dbt/sushi_test/models/schema.yml @@ -52,3 +52,23 @@ sources: tables: - name: items - name: orders + +semantic_models: + - name: top_waiters + description: Some description + model: ref('top_waiters') + measures: + - name: total_waiters + agg: sum + expr: waiter + dimensions: + - name: waiter + type: categorical + +metrics: + - name: some_waiter_thing + description: Something + type: simple + label: testing + type_params: + measure: total_waiters \ No newline at end of file From 59d44eb8df3305557e314ea4edcef8852cac21d7 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:48:55 -0700 Subject: [PATCH 0785/1056] feat: dbt adapter allow invalid source ref for tests (#5259) --- sqlmesh/dbt/basemodel.py | 3 ++- sqlmesh/dbt/context.py | 7 +++---- sqlmesh/dbt/loader.py | 10 ++++++---- sqlmesh/utils/errors.py | 13 +++++++++---- tests/dbt/test_model.py | 15 +++++++++++++-- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index fad86f618e..bc63e9c4d0 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -247,7 +247,7 @@ def tests_ref_source_dependencies(self) -> Dependencies: def remove_tests_with_invalid_refs(self, context: DbtContext) -> None: """ - Removes tests that reference models that do not exist in the context in order to match dbt behavior. + Removes tests that reference models or sources that do not exist in the context in order to match dbt behavior. Args: context: The dbt context this model resides within. @@ -259,6 +259,7 @@ def remove_tests_with_invalid_refs(self, context: DbtContext) -> None: test for test in self.tests if all(ref in context.refs for ref in test.dependencies.refs) + and all(source in context.sources for source in test.dependencies.sources) ] def check_for_circular_test_refs(self, context: DbtContext) -> None: diff --git a/sqlmesh/dbt/context.py b/sqlmesh/dbt/context.py index 6af291f478..a56a6ca4d6 100644 --- a/sqlmesh/dbt/context.py +++ b/sqlmesh/dbt/context.py @@ -12,7 +12,7 @@ from sqlmesh.dbt.manifest import ManifestHelper from sqlmesh.dbt.target import TargetConfig from sqlmesh.utils import AttributeDict -from sqlmesh.utils.errors import ConfigError, SQLMeshError, MissingModelError +from sqlmesh.utils.errors import ConfigError, SQLMeshError, MissingModelError, MissingSourceError from sqlmesh.utils.jinja import ( JinjaGlobalAttribute, JinjaMacroRegistry, @@ -266,14 +266,13 @@ def context_for_dependencies(self, dependencies: Dependencies) -> DbtContext: else: models[ref] = t.cast(ModelConfig, model) else: - exception = MissingModelError(ref) - raise exception + raise MissingModelError(ref) for source in dependencies.sources: if source in self.sources: sources[source] = self.sources[source] else: - raise ConfigError(f"Source '{source}' was not found.") + raise MissingSourceError(source) variables = {k: v for k, v in self.variables.items() if k in dependencies.variables} diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index cd1c4b6c1a..8fd7926ad5 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -23,7 +23,7 @@ from sqlmesh.dbt.project import Project from sqlmesh.dbt.target import TargetConfig from sqlmesh.utils import UniqueKeyDict -from sqlmesh.utils.errors import ConfigError, MissingModelError +from sqlmesh.utils.errors import ConfigError, MissingModelError, BaseMissingReferenceError from sqlmesh.utils.jinja import ( JinjaMacroRegistry, make_jinja_registry, @@ -161,11 +161,13 @@ def _load_audits( logger.debug("Converting '%s' to sqlmesh format", test.name) try: audits[test.name] = test.to_sqlmesh(package_context) - except MissingModelError as e: + except BaseMissingReferenceError as e: + ref_type = "model" if isinstance(e, MissingModelError) else "source" logger.warning( - "Skipping audit '%s' because model '%s' is not a valid ref", + "Skipping audit '%s' because %s '%s' is not a valid ref", test.name, - e.model_name, + ref_type, + e.ref, ) return audits diff --git a/sqlmesh/utils/errors.py b/sqlmesh/utils/errors.py index 8efb0af88a..bbd1db3802 100644 --- a/sqlmesh/utils/errors.py +++ b/sqlmesh/utils/errors.py @@ -33,12 +33,17 @@ def __init__(self, message: str | Exception, location: t.Optional[Path] = None) self.location = Path(location) if isinstance(location, str) else location -class MissingModelError(ConfigError): +class BaseMissingReferenceError(ConfigError): + def __init__(self, ref: str) -> None: + self.ref = ref + + +class MissingModelError(BaseMissingReferenceError): """Raised when a model that is referenced is missing.""" - def __init__(self, model_name: str) -> None: - self.model_name = model_name - super().__init__(f"Model '{model_name}' was not found.") + +class MissingSourceError(BaseMissingReferenceError): + """Raised when a source that is referenced is missing.""" class MissingDependencyError(SQLMeshError): diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index df9f229900..7b3e120e25 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -87,8 +87,15 @@ def test_load_invalid_ref_audit_constraints( "relationships": { "to": "ref('not_real_model')", "field": "cola", - } - } + }, + }, + { + # Reference a source that doesn't exist + "relationships": { + "to": "source('not_real_source', 'not_real_table')", + "field": "cola", + }, + }, ], } ], @@ -134,6 +141,10 @@ def test_load_invalid_ref_audit_constraints( "Skipping audit 'relationships_full_model_cola__cola__ref_not_real_model_' because model 'not_real_model' is not a valid ref" in caplog.text ) + assert ( + "Skipping audit 'relationships_full_model_cola__cola__source_not_real_source_not_real_table_' because source 'not_real_source.not_real_table' is not a valid ref" + in caplog.text + ) fqn = '"local"."main"."full_model"' assert fqn in context.snapshots # The audit isn't loaded due to the invalid ref From 6d36a11786de257c151de7050616abe939ca0462 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 29 Aug 2025 12:00:22 +1200 Subject: [PATCH 0786/1056] Feat(dbt_cli): Set proper plan flags and also allow --empty and --environment (#5226) --- sqlmesh/cli/project_init.py | 7 ++ sqlmesh/core/context.py | 7 +- sqlmesh_dbt/cli.py | 17 +++- sqlmesh_dbt/operations.py | 110 +++++++++++++++++--- tests/cli/test_cli.py | 5 +- tests/dbt/cli/test_operations.py | 167 +++++++++++++++++++++++++++++++ 6 files changed, 293 insertions(+), 20 deletions(-) diff --git a/sqlmesh/cli/project_init.py b/sqlmesh/cli/project_init.py index 87d77da4b9..81ff534dc4 100644 --- a/sqlmesh/cli/project_init.py +++ b/sqlmesh/cli/project_init.py @@ -121,6 +121,13 @@ def _gen_config( # https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#virtual-data-environment-modes virtual_environment_mode: {VirtualEnvironmentMode.DEV_ONLY.lower()} +# --- Plan Defaults --- +# https://sqlmesh.readthedocs.io/en/stable/reference/configuration/#plan +plan: + # For Virtual Data Environments, this ensures that any changes are always considered against prod, + # rather than the previous state of that environment + always_recreate_environment: True + # --- Model Defaults --- # https://sqlmesh.readthedocs.io/en/stable/reference/model_configuration/#model-defaults model_defaults: diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 1a5375183c..4c1ffb1e92 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1518,8 +1518,11 @@ def plan_builder( include_unmodified = self.config.plan.include_unmodified if skip_backfill and not no_gaps and not is_dev: - raise ConfigError( - "When targeting the production environment either the backfill should not be skipped or the lack of data gaps should be enforced (--no-gaps flag)." + # note: we deliberately don't mention the --no-gaps flag in case the plan came from the sqlmesh_dbt command + # todo: perhaps we could have better error messages if we check sys.argv[0] for which cli is running? + self.console.log_warning( + "Skipping the backfill stage for production can lead to unexpected results, such as tables being empty or incremental data with non-contiguous time ranges being made available.\n" + "If you are doing this deliberately to create an empty version of a table to test a change, please consider using Virtual Data Environments instead." ) if not skip_linter: diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py index c215663f0a..370f115d61 100644 --- a/sqlmesh_dbt/cli.py +++ b/sqlmesh_dbt/cli.py @@ -92,11 +92,24 @@ def dbt( "--full-refresh", help="If specified, dbt will drop incremental models and fully-recalculate the incremental table from the model definition.", ) +@click.option( + "--env", + "--environment", + help="Run against a specific Virtual Data Environment (VDE) instead of the main environment", +) +@click.option( + "--empty/--no-empty", default=False, help="If specified, limit input refs and sources" +) @vars_option @click.pass_context -def run(ctx: click.Context, vars: t.Optional[t.Dict[str, t.Any]], **kwargs: t.Any) -> None: +def run( + ctx: click.Context, + vars: t.Optional[t.Dict[str, t.Any]], + env: t.Optional[str] = None, + **kwargs: t.Any, +) -> None: """Compile SQL and execute against the current target database.""" - _get_dbt_operations(ctx, vars).run(**kwargs) + _get_dbt_operations(ctx, vars).run(environment=env, **kwargs) @dbt.command(name="list") diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index 270bba6511..ac7ad031f3 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -11,6 +11,7 @@ from sqlmesh.dbt.project import Project from sqlmesh_dbt.console import DbtCliConsole from sqlmesh.core.model import Model + from sqlmesh.core.plan import Plan logger = logging.getLogger(__name__) @@ -35,21 +36,20 @@ def list_( def run( self, + environment: t.Optional[str] = None, select: t.Optional[t.List[str]] = None, exclude: t.Optional[t.List[str]] = None, full_refresh: bool = False, - ) -> None: - select_models = None - - if sqlmesh_selector := selectors.to_sqlmesh(select or [], exclude or []): - select_models = [sqlmesh_selector] - - self.context.plan( - select_models=select_models, - run=True, - no_diff=True, - no_prompts=True, - auto_apply=True, + empty: bool = False, + ) -> Plan: + return self.context.plan( + **self._plan_options( + environment=environment, + select=select, + exclude=exclude, + full_refresh=full_refresh, + empty=empty, + ) ) def _selected_models( @@ -71,6 +71,86 @@ def _selected_models( return selected_models + def _plan_options( + self, + environment: t.Optional[str] = None, + select: t.Optional[t.List[str]] = None, + exclude: t.Optional[t.List[str]] = None, + empty: bool = False, + full_refresh: bool = False, + ) -> t.Dict[str, t.Any]: + import sqlmesh.core.constants as c + + # convert --select and --exclude to a selector expression for the SQLMesh selector engine + select_models = None + if sqlmesh_selector := selectors.to_sqlmesh(select or [], exclude or []): + select_models = [sqlmesh_selector] + + is_dev = environment and environment != c.PROD + is_prod = not is_dev + + options: t.Dict[str, t.Any] = {} + + if is_prod or (is_dev and select_models): + # prod plans should "catch up" before applying the changes so that after the command finishes prod is the latest it can be + # dev plans *with* selectors should do the same as the user is saying "specifically update these models to the latest" + # dev plans *without* selectors should just have the defaults of never exceeding prod as the user is saying "just create this env" without focusing on any specific models + options.update( + dict( + # always catch the data up to latest rather than only operating on what has been loaded before + run=True, + # don't taking cron schedules into account when deciding what models to run, do everything even if it just ran + ignore_cron=True, + ) + ) + + if is_dev: + options.update( + dict( + # don't create views for all of prod in the dev environment + include_unmodified=False, + # always plan from scratch against prod. note that this is coupled with the `always_recreate_environment=True` setting in the default config file. + # the result is that rather than planning against the previous state of an existing dev environment, the full scope of changes vs prod are always shown + create_from=c.PROD, + # Always enable dev previews for incremental / forward-only models. + # Due to how DBT does incrementals (INCREMENTAL_UNMANAGED on the SQLMesh engine), this will result in the full model being refreshed + # with the entire dataset, which can potentially be very large. If this is undesirable, users have two options: + # - work around this using jinja to conditionally add extra filters to the WHERE clause or a LIMIT to the model query + # - upgrade to SQLMesh's incremental models, where we have variables for the start/end date and inject leak guards to + # limit the amount of data backfilled + # + # Note: enable_preview=True is *different* behaviour to the `sqlmesh` CLI, which uses enable_preview=None. + # This means the `sqlmesh` CLI will only enable dev previews for dbt projects if the target adapter supports cloning, + # whereas we enable it unconditionally here + enable_preview=True, + ) + ) + + if empty: + # `dbt --empty` adds LIMIT 0 to the queries, resulting in empty tables. In addition, it happily clobbers existing tables regardless of if they are populated. + # This *partially* lines up with --skip-backfill in SQLMesh, which indicates to not populate tables if they happened to be created/updated as part of this plan. + # However, if a table already exists and has data in it, there is no change so SQLMesh will not recreate the table and thus it will not be cleared. + # So in order to fully replicate dbt's --empty, we also need --full-refresh semantics in order to replace existing tables + options["skip_backfill"] = True + full_refresh = True + + if full_refresh: + # TODO: handling this requires some updates in the engine to enable restatements+changes in the same plan without affecting prod + # if the plan targets dev + pass + + return dict( + environment=environment, + select_models=select_models, + # dont output a diff of model changes + no_diff=True, + # don't throw up any prompts like "set the effective date" - use defaults + no_prompts=True, + # start doing work immediately (since no_diff is set, there isnt really anything for the user to say yes/no to) + auto_apply=True, + **options, + ) + @property def console(self) -> DbtCliConsole: console = self.context.console @@ -103,6 +183,12 @@ def create( from sqlmesh_dbt.console import DbtCliConsole from sqlmesh.utils.errors import SQLMeshError + # clear any existing handlers set up by click/rich as defaults so that once SQLMesh logging config is applied, + # we dont get duplicate messages logged from things like console.log_warning() + root_logger = logging.getLogger() + while root_logger.hasHandlers(): + root_logger.removeHandler(root_logger.handlers[0]) + configure_logging(force_debug=debug) set_console(DbtCliConsole()) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 433e2165d8..ef5b80e151 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -260,10 +260,7 @@ def test_plan_skip_backfill(runner, tmp_path, flag): # plan for `prod` errors if `--skip-backfill` is passed without --no-gaps result = runner.invoke(cli, ["--log-file-dir", tmp_path, "--paths", tmp_path, "plan", flag]) assert result.exit_code == 1 - assert ( - "Error: When targeting the production environment either the backfill should not be skipped or the lack of data gaps should be enforced (--no-gaps flag)." - in result.output - ) + assert "Skipping the backfill stage for production can lead to unexpected" in result.output # plan executes virtual update without executing model batches # Input: `y` to perform virtual update diff --git a/tests/dbt/cli/test_operations.py b/tests/dbt/cli/test_operations.py index 9b5b3113b3..15051542c6 100644 --- a/tests/dbt/cli/test_operations.py +++ b/tests/dbt/cli/test_operations.py @@ -1,13 +1,36 @@ +import typing as t from pathlib import Path import pytest from sqlmesh_dbt.operations import create from sqlmesh.utils import yaml from sqlmesh.utils.errors import SQLMeshError import time_machine +from sqlmesh.core.console import NoopConsole +from sqlmesh.core.plan import PlanBuilder +from sqlmesh.core.config.common import VirtualEnvironmentMode pytestmark = pytest.mark.slow +class PlanCapturingConsole(NoopConsole): + def plan( + self, + plan_builder: PlanBuilder, + auto_apply: bool, + default_catalog: t.Optional[str], + no_diff: bool = False, + no_prompts: bool = False, + ) -> None: + self.plan_builder = plan_builder + self.auto_apply = auto_apply + self.default_catalog = default_catalog + self.no_diff = no_diff + self.no_prompts = no_prompts + + # normal console starts applying the plan here; we dont because we just want to capture the parameters + # and check they were set correctly + + def test_create_sets_and_persists_default_start_date(jaffle_shop_duckdb: Path): with time_machine.travel("2020-01-02 00:00:00 UTC"): from sqlmesh.utils.date import yesterday_ds, to_ds @@ -71,6 +94,18 @@ def test_create_can_specify_profile_and_target(jaffle_shop_duckdb: Path): assert dbt_project.context.target_name == "dev" +def test_default_options(jaffle_shop_duckdb: Path): + operations = create() + + config = operations.context.config + dbt_project = operations.project + + assert config.plan.always_recreate_environment is True + assert config.virtual_environment_mode == VirtualEnvironmentMode.DEV_ONLY + assert config.model_defaults.start is not None + assert config.model_defaults.dialect == dbt_project.context.target.dialect + + def test_create_can_set_project_variables(jaffle_shop_duckdb: Path): (jaffle_shop_duckdb / "models" / "test_model.sql").write_text(""" select '{{ var('foo') }}' as a @@ -83,3 +118,135 @@ def test_create_can_set_project_variables(jaffle_shop_duckdb: Path): query = test_model.render_query() assert query is not None assert query.sql() == "SELECT 'bar' AS \"a\"" + + +def test_run_option_mapping(jaffle_shop_duckdb: Path): + operations = create(project_dir=jaffle_shop_duckdb) + console = PlanCapturingConsole() + operations.context.console = console + + plan = operations.run() + assert plan.environment.name == "prod" + assert console.no_prompts is True + assert console.no_diff is True + assert console.auto_apply is True + assert plan.end_bounded is False + assert plan.ignore_cron is True + assert plan.skip_backfill is False + assert plan.selected_models_to_backfill is None + assert {s.name for s in plan.snapshots} == {k for k in operations.context.snapshots} + + plan = operations.run(select=["main.stg_orders+"]) + assert plan.environment.name == "prod" + assert console.no_prompts is True + assert console.no_diff is True + assert console.auto_apply is True + assert plan.end_bounded is False + assert plan.ignore_cron is True + assert plan.skip_backfill is False + assert plan.selected_models_to_backfill == { + '"jaffle_shop"."main"."customers"', + '"jaffle_shop"."main"."orders"', + '"jaffle_shop"."main"."stg_orders"', + } + assert {s.name for s in plan.snapshots} == plan.selected_models_to_backfill + + plan = operations.run(select=["main.stg_orders+"], exclude=["main.customers"]) + assert plan.environment.name == "prod" + assert console.no_prompts is True + assert console.no_diff is True + assert console.auto_apply is True + assert plan.end_bounded is False + assert plan.ignore_cron is True + assert plan.skip_backfill is False + assert plan.selected_models_to_backfill == { + '"jaffle_shop"."main"."orders"', + '"jaffle_shop"."main"."stg_orders"', + } + assert {s.name for s in plan.snapshots} == plan.selected_models_to_backfill + + plan = operations.run(exclude=["main.customers"]) + assert plan.environment.name == "prod" + assert console.no_prompts is True + assert console.no_diff is True + assert console.auto_apply is True + assert plan.end_bounded is False + assert plan.ignore_cron is True + assert plan.skip_backfill is False + assert plan.selected_models_to_backfill == {k for k in operations.context.snapshots} - { + '"jaffle_shop"."main"."customers"' + } + assert {s.name for s in plan.snapshots} == plan.selected_models_to_backfill + + plan = operations.run(empty=True) + assert plan.environment.name == "prod" + assert console.no_prompts is True + assert console.no_diff is True + assert console.auto_apply is True + assert plan.end_bounded is False + assert plan.ignore_cron is True + assert plan.skip_backfill is True + assert plan.selected_models_to_backfill is None + assert {s.name for s in plan.snapshots} == {k for k in operations.context.snapshots} + + +def test_run_option_mapping_dev(jaffle_shop_duckdb: Path): + # create prod so that dev has something to compare against + operations = create(project_dir=jaffle_shop_duckdb) + operations.run() + + (jaffle_shop_duckdb / "models" / "new_model.sql").write_text("select 1") + + operations = create(project_dir=jaffle_shop_duckdb) + + console = PlanCapturingConsole() + operations.context.console = console + + plan = operations.run(environment="dev") + assert plan.environment.name == "dev" + assert console.no_prompts is True + assert console.no_diff is True + assert console.auto_apply is True + assert plan.include_unmodified is False + assert plan.context_diff.create_from == "prod" + assert plan.context_diff.is_new_environment is True + assert console.plan_builder._enable_preview is True + assert plan.end_bounded is True + assert plan.ignore_cron is False + assert plan.skip_backfill is False + assert plan.selected_models_to_backfill == {'"jaffle_shop"."main"."new_model"'} + + plan = operations.run(environment="dev", empty=True) + assert plan.environment.name == "dev" + assert console.no_prompts is True + assert console.no_diff is True + assert console.auto_apply is True + assert plan.include_unmodified is False + assert plan.context_diff.create_from == "prod" + assert plan.context_diff.is_new_environment is True + assert console.plan_builder._enable_preview is True + assert plan.end_bounded is True + assert plan.ignore_cron is False + assert plan.skip_backfill is True + assert plan.selected_models_to_backfill == {'"jaffle_shop"."main"."new_model"'} + + plan = operations.run(environment="dev", select=["main.stg_orders+"]) + assert plan.environment.name == "dev" + assert console.no_prompts is True + assert console.no_diff is True + assert console.auto_apply is True + assert plan.include_unmodified is False + assert plan.context_diff.create_from == "prod" + assert plan.context_diff.is_new_environment is True + assert console.plan_builder._enable_preview is True + # dev plans with --select have run=True, ignore_cron=True set + # as opposed to dev plans that dont have a specific selector + assert plan.end_bounded is False + assert plan.ignore_cron is True + assert plan.skip_backfill is False + # note: the new model in the dev environment is ignored in favour of the explicitly selected ones + assert plan.selected_models_to_backfill == { + '"jaffle_shop"."main"."customers"', + '"jaffle_shop"."main"."orders"', + '"jaffle_shop"."main"."stg_orders"', + } From b581636603cf9d7bee83bb8f6fd12eeaaa0b8b8b Mon Sep 17 00:00:00 2001 From: Tori Wei <41123940+toriwei@users.noreply.github.com> Date: Thu, 28 Aug 2025 17:00:42 -0700 Subject: [PATCH 0787/1056] fix: update TestConfig limit type (#5257) --- sqlmesh/dbt/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh/dbt/test.py b/sqlmesh/dbt/test.py index 5f005ebc36..035c62acda 100644 --- a/sqlmesh/dbt/test.py +++ b/sqlmesh/dbt/test.py @@ -83,7 +83,7 @@ class TestConfig(GeneralConfig): severity: Severity = Severity.ERROR store_failures: t.Optional[bool] = None where: t.Optional[str] = None - limit: t.Optional[str] = None + limit: t.Optional[int] = None fail_calc: str = "count(*)" warn_if: str = "!=0" error_if: str = "!=0" From ecdbbde03dc9acb4a807fd69a45904a64c7304e6 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 29 Aug 2025 12:19:35 +1200 Subject: [PATCH 0788/1056] Chore: Address more integration test flakiness (#5258) --- .circleci/manage-test-db.sh | 6 +++- sqlmesh/core/engine_adapter/fabric.py | 6 ++++ tests/conftest.py | 45 +++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/.circleci/manage-test-db.sh b/.circleci/manage-test-db.sh index ba1d1070fb..f79072f335 100755 --- a/.circleci/manage-test-db.sh +++ b/.circleci/manage-test-db.sh @@ -80,7 +80,11 @@ redshift_down() { EXIT_CODE=1 ATTEMPTS=0 while [ $EXIT_CODE -ne 0 ] && [ $ATTEMPTS -lt 5 ]; do - redshift_exec "select pg_terminate_backend(procpid) from pg_stat_activity where datname = '$1'" + # note: sometimes this pg_terminate_backend() call can randomly fail with: ERROR: Insufficient privileges + # if it does, let's proceed with the drop anyway rather than aborting and never attempting the drop + redshift_exec "select pg_terminate_backend(procpid) from pg_stat_activity where datname = '$1'" || true + + # perform drop redshift_exec "drop database $1;" && EXIT_CODE=$? || EXIT_CODE=$? if [ $EXIT_CODE -ne 0 ]; then echo "Unable to drop database; retrying..." diff --git a/sqlmesh/core/engine_adapter/fabric.py b/sqlmesh/core/engine_adapter/fabric.py index 6d7d40c3bb..585622e866 100644 --- a/sqlmesh/core/engine_adapter/fabric.py +++ b/sqlmesh/core/engine_adapter/fabric.py @@ -153,6 +153,12 @@ def set_current_catalog(self, catalog_name: str) -> None: logger.info(f"Switching from catalog '{current_catalog}' to '{catalog_name}'") + # commit the transaction before closing the connection to help prevent errors like: + # > Snapshot isolation transaction failed in database because the object accessed by the statement has been modified by a + # > DDL statement in another concurrent transaction since the start of this transaction + # on subsequent queries in the new connection + self._connection_pool.commit() + # note: we call close() on the connection pool instead of self.close() because self.close() calls close_all() # on the connection pool but we just want to close the connection for this thread self._connection_pool.close() diff --git a/tests/conftest.py b/tests/conftest.py index e4911de80c..b6523f72a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -245,6 +245,51 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): ) +def pytest_configure(config: pytest.Config): + # we need to adjust the hook order if pytest-retry is present because it: + # - also declares a `pytest_runtest_makereport` with `hookwrapper=True, tryfirst=True` + # - this supersedes our one because pytest always loads plugins first and they take precedence over user code + # + # but, we need our one to run first because it's capturing and ignoring certain errors that cause pytest-retry to fail + # and not retry. so we need to adjust the order the hooks are called which pytest does NOT make easy. + # + # we can't just unload the pytest-retry plugin, load our hook and reload the pytest-retry plugin either. + # this causes an error: + # > Hook 'pytest_set_excluded_exceptions' is already registered within namespace + # because unregister() apparently doesnt unregister plugins cleanly in such a way they can be re-registered + # + # so what we end up doing below is a small monkey-patch to adjust the call order of the hooks + pm = config.pluginmanager + + from pluggy._hooks import HookCaller + + hook_caller: HookCaller = pm.hook.pytest_runtest_makereport + hook_impls = hook_caller.get_hookimpls() + + # find the index of our one + our_makereport_idx = next( + (i for i, v in enumerate(hook_impls) if v.plugin_name.endswith("tests/conftest.py")), None + ) + + # find the index of the pytest-retry one + pytest_retry_makereport_idx = next( + (i for i, v in enumerate(hook_impls) if v.plugin_name == "pytest-retry"), None + ) + + if ( + pytest_retry_makereport_idx is not None + and our_makereport_idx is not None + and our_makereport_idx > pytest_retry_makereport_idx + ): + our_makereport_hook = hook_impls.pop(our_makereport_idx) + + # inject our one to run before the pytest-retry one + hook_impls.insert(pytest_retry_makereport_idx, our_makereport_hook) + + # HookCaller doesnt have a setter method for this. + hook_caller._hookimpls = hook_impls # type: ignore + + # Ignore all local config files @pytest.fixture(scope="session", autouse=True) def ignore_local_config_files(): From 84b39010af6af66294370d1043b2deb2ffcf2957 Mon Sep 17 00:00:00 2001 From: Chris Rericha <67359577+crericha@users.noreply.github.com> Date: Thu, 28 Aug 2025 23:15:15 -0400 Subject: [PATCH 0789/1056] Fix: Automatically repair dbt test circular references by moving upstream test to downstream model (#5253) --- sqlmesh/dbt/basemodel.py | 39 +++++++++++++++++++-------------------- tests/dbt/test_model.py | 37 ++++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index bc63e9c4d0..212b314997 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -4,6 +4,7 @@ from abc import abstractmethod from enum import Enum from pathlib import Path +import logging from pydantic import Field from sqlglot.helper import ensure_list @@ -38,6 +39,9 @@ BMC = t.TypeVar("BMC", bound="BaseModelConfig") +logger = logging.getLogger(__name__) + + class Materialization(str, Enum): """DBT model materializations""" @@ -262,12 +266,11 @@ def remove_tests_with_invalid_refs(self, context: DbtContext) -> None: and all(source in context.sources for source in test.dependencies.sources) ] - def check_for_circular_test_refs(self, context: DbtContext) -> None: + def fix_circular_test_refs(self, context: DbtContext) -> None: """ - Checks for direct circular references between two models and raises an exception if found. - This addresses the most common circular reference seen when importing a dbt project - - relationship tests in both directions. In the future, we may want to increase coverage by - checking for indirect circular references. + Checks for direct circular references between two models and moves the test to the downstream + model if found. This addresses the most common circular reference - relationship tests in both + directions. In the future, we may want to increase coverage by checking for indirect circular references. Args: context: The dbt context this model resides within. @@ -275,24 +278,20 @@ def check_for_circular_test_refs(self, context: DbtContext) -> None: Returns: None """ - for test in self.tests: + for test in self.tests.copy(): for ref in test.dependencies.refs: - model = context.refs[ref] if ref == self.name or ref in self.dependencies.refs: continue - elif self.name in model.dependencies.refs: - raise ConfigError( - f"Test '{test.name}' for model '{self.name}' depends on downstream model '{model.name}'." - " Move the test to the downstream model to avoid circular references." - ) - elif self.name in model.tests_ref_source_dependencies.refs: - circular_test = next( - test.name for test in model.tests if ref in test.dependencies.refs - ) - raise ConfigError( - f"Circular reference detected between tests for models '{self.name}' and '{model.name}':" - f" '{test.name}' ({self.name}), '{circular_test}' ({model.name})." + model = context.refs[ref] + if ( + self.name in model.dependencies.refs + or self.name in model.tests_ref_source_dependencies.refs + ): + logger.info( + f"Moving test '{test.name}' from model '{self.name}' to '{model.name}' to avoid circular reference." ) + model.tests.append(test) + self.tests.remove(test) @property def sqlmesh_config_fields(self) -> t.Set[str]: @@ -313,7 +312,7 @@ def sqlmesh_model_kwargs( ) -> t.Dict[str, t.Any]: """Get common sqlmesh model parameters""" self.remove_tests_with_invalid_refs(context) - self.check_for_circular_test_refs(context) + self.fix_circular_test_refs(context) dependencies = self.dependencies.copy() if dependencies.has_dynamic_var_names: diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index 7b3e120e25..00361cf8b8 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -8,7 +8,6 @@ from sqlmesh.dbt.model import ModelConfig from sqlmesh.dbt.target import PostgresConfig from sqlmesh.dbt.test import TestConfig -from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.yaml import YAML pytestmark = pytest.mark.dbt @@ -30,25 +29,41 @@ def test_model_test_circular_references() -> None: sql="", dependencies=Dependencies(refs={"upstream", "downstream"}), ) + + # No circular reference downstream_model.tests = [downstream_test] - downstream_model.check_for_circular_test_refs(context) + downstream_model.fix_circular_test_refs(context) + assert upstream_model.tests == [] + assert downstream_model.tests == [downstream_test] + # Upstream model reference in downstream model downstream_model.tests = [] upstream_model.tests = [upstream_test] - with pytest.raises(ConfigError, match="downstream model"): - upstream_model.check_for_circular_test_refs(context) + upstream_model.fix_circular_test_refs(context) + assert upstream_model.tests == [] + assert downstream_model.tests == [upstream_test] + upstream_model.tests = [upstream_test] downstream_model.tests = [downstream_test] - with pytest.raises(ConfigError, match="downstream model"): - upstream_model.check_for_circular_test_refs(context) - downstream_model.check_for_circular_test_refs(context) + upstream_model.fix_circular_test_refs(context) + assert upstream_model.tests == [] + assert downstream_model.tests == [downstream_test, upstream_test] + + downstream_model.fix_circular_test_refs(context) + assert upstream_model.tests == [] + assert downstream_model.tests == [downstream_test, upstream_test] # Test only references + upstream_model.tests = [upstream_test] + downstream_model.tests = [downstream_test] downstream_model.dependencies = Dependencies() - with pytest.raises(ConfigError, match="between tests"): - upstream_model.check_for_circular_test_refs(context) - with pytest.raises(ConfigError, match="between tests"): - downstream_model.check_for_circular_test_refs(context) + upstream_model.fix_circular_test_refs(context) + assert upstream_model.tests == [] + assert downstream_model.tests == [downstream_test, upstream_test] + + downstream_model.fix_circular_test_refs(context) + assert upstream_model.tests == [] + assert downstream_model.tests == [downstream_test, upstream_test] @pytest.mark.slow From 8a6b16889163fab9232e22669ce4f688101ce300 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:39:31 +0300 Subject: [PATCH 0790/1056] ci: limit the vscode test job to 30mins (#5260) --- .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 ad187df449..2e94855d3c 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -58,7 +58,7 @@ jobs: run: pnpm exec playwright install - name: Run e2e tests working-directory: ./vscode/extension - timeout-minutes: 90 + timeout-minutes: 30 run: | source ../../.venv/bin/activate pnpm run test:e2e From 1a9edc75c9e211a1a3ff07a7aef270dca20434dc Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:12:44 +0300 Subject: [PATCH 0791/1056] Chore: Add heplful error message when drop operation fails (#5261) --- sqlmesh/core/schema_diff.py | 7 +++++-- tests/core/test_schema_diff.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/schema_diff.py b/sqlmesh/core/schema_diff.py index 7b8c7f16f7..e1f9d72a6c 100644 --- a/sqlmesh/core/schema_diff.py +++ b/sqlmesh/core/schema_diff.py @@ -498,8 +498,11 @@ def _drop_operation( columns = ensure_list(columns) operations: t.List[TableAlterColumnOperation] = [] column_pos, column_kwarg = self._get_matching_kwarg(columns[-1].name, struct, pos) - assert column_pos is not None - assert column_kwarg + if column_pos is None or not column_kwarg: + raise SQLMeshError( + f"Cannot drop column '{columns[-1].name}' from table '{table_name}' - column not found. " + f"This may indicate a mismatch between the expected and actual table schemas." + ) struct.expressions.pop(column_pos) operations.append( TableAlterDropColumnOperation( diff --git a/tests/core/test_schema_diff.py b/tests/core/test_schema_diff.py index e091dea539..52bd6bb606 100644 --- a/tests/core/test_schema_diff.py +++ b/tests/core/test_schema_diff.py @@ -15,6 +15,7 @@ TableAlterChangeColumnTypeOperation, NestedSupport, ) +from sqlmesh.utils.errors import SQLMeshError def test_schema_diff_calculate(): @@ -2341,3 +2342,27 @@ def test_ignore_additive_array_operations(): ignore_additive=True, ) assert len(operations_ignore_additive) == 0 + + +def test_drop_operation_missing_column_error(): + schema_differ = SchemaDiffer( + nested_support=NestedSupport.NONE, + support_positional_add=False, + ) + + # a struct that doesn't contain the column we're going to drop + current_struct = exp.DataType.build("STRUCT") + + with pytest.raises(SQLMeshError) as error_message: + schema_differ._drop_operation( + columns=[TableAlterColumn.primitive("missing_column")], + struct=current_struct, + pos=0, + root_struct=current_struct, + table_name="test_table", + ) + + assert ( + str(error_message.value) + == "Cannot drop column 'missing_column' from table 'test_table' - column not found. This may indicate a mismatch between the expected and actual table schemas." + ) From 614a85dfe0c256be96584dc69af86b90c9655b40 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 29 Aug 2025 08:43:16 -0700 Subject: [PATCH 0792/1056] chore(web_common): add more shared components (#5243) --- pnpm-lock.yaml | 331 ++++++++++++++++++ web/common/.gitignore | 2 + web/common/package.json | 8 + web/common/src/components/Badge/Badge.css | 2 +- .../src/components/Badge/Badge.stories.tsx | 37 +- .../src/components/Badge/Badge.test.tsx | 58 --- web/common/src/components/Badge/Badge.tsx | 39 ++- web/common/src/components/Badge/help.ts | 29 -- web/common/src/components/Button/Button.css | 31 ++ .../src/components/Button/Button.stories.tsx | 120 +++++++ web/common/src/components/Button/Button.tsx | 89 +++++ .../CopyButton/CopyButton.stories.tsx | 126 +++++++ .../components/CopyButton/CopyButton.test.tsx | 112 ++++++ .../src/components/CopyButton/CopyButton.tsx | 63 ++++ .../HorizontalContainer.stories.tsx | 78 +++++ .../HorizontalContainer.test.tsx | 61 ++++ .../HorizontalContainer.tsx | 44 +++ .../src/components/ModelName/ModelName.css | 17 + .../ModelName/ModelName.stories.tsx | 92 +++++ .../components/ModelName/ModelName.test.tsx | 72 ++++ .../src/components/ModelName/ModelName.tsx | 218 ++++++++++++ .../ScrollContainer/ScrollContainer.css | 4 + .../ScrollContainer.stories.tsx | 109 ++++++ .../ScrollContainer/ScrollContainer.test.tsx | 67 ++++ .../ScrollContainer/ScrollContainer.tsx | 39 +++ web/common/src/components/Tooltip/Tooltip.css | 4 + .../components/Tooltip/Tooltip.stories.tsx | 18 + web/common/src/components/Tooltip/Tooltip.tsx | 58 +++ .../VerticalContainer.stories.tsx | 78 +++++ .../VerticalContainer.test.tsx | 61 ++++ .../VerticalContainer/VerticalContainer.tsx | 44 +++ web/common/src/index.ts | 64 ++-- web/common/src/styles/design/palette.css | 2 + .../src/styles/design/semantic-colors.css | 57 +++ web/common/src/styles/tokens.ts | 223 ------------ web/common/src/types.ts | 18 + web/common/src/types/enums.ts | 43 --- web/common/src/types/index.ts | 3 - web/common/src/utils.ts | 32 ++ web/common/src/utils/index.ts | 14 - web/common/tailwind.base.config.js | 98 +++++- 41 files changed, 2233 insertions(+), 432 deletions(-) delete mode 100644 web/common/src/components/Badge/Badge.test.tsx delete mode 100644 web/common/src/components/Badge/help.ts create mode 100644 web/common/src/components/Button/Button.css create mode 100644 web/common/src/components/Button/Button.stories.tsx create mode 100644 web/common/src/components/Button/Button.tsx create mode 100644 web/common/src/components/CopyButton/CopyButton.stories.tsx create mode 100644 web/common/src/components/CopyButton/CopyButton.test.tsx create mode 100644 web/common/src/components/CopyButton/CopyButton.tsx create mode 100644 web/common/src/components/HorizontalContainer/HorizontalContainer.stories.tsx create mode 100644 web/common/src/components/HorizontalContainer/HorizontalContainer.test.tsx create mode 100644 web/common/src/components/HorizontalContainer/HorizontalContainer.tsx create mode 100644 web/common/src/components/ModelName/ModelName.css create mode 100644 web/common/src/components/ModelName/ModelName.stories.tsx create mode 100644 web/common/src/components/ModelName/ModelName.test.tsx create mode 100644 web/common/src/components/ModelName/ModelName.tsx create mode 100644 web/common/src/components/ScrollContainer/ScrollContainer.css create mode 100644 web/common/src/components/ScrollContainer/ScrollContainer.stories.tsx create mode 100644 web/common/src/components/ScrollContainer/ScrollContainer.test.tsx create mode 100644 web/common/src/components/ScrollContainer/ScrollContainer.tsx create mode 100644 web/common/src/components/Tooltip/Tooltip.css create mode 100644 web/common/src/components/Tooltip/Tooltip.stories.tsx create mode 100644 web/common/src/components/Tooltip/Tooltip.tsx create mode 100644 web/common/src/components/VerticalContainer/VerticalContainer.stories.tsx create mode 100644 web/common/src/components/VerticalContainer/VerticalContainer.test.tsx create mode 100644 web/common/src/components/VerticalContainer/VerticalContainer.tsx delete mode 100644 web/common/src/styles/tokens.ts create mode 100644 web/common/src/types.ts delete mode 100644 web/common/src/types/enums.ts delete mode 100644 web/common/src/types/index.ts create mode 100644 web/common/src/utils.ts delete mode 100644 web/common/src/utils/index.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19c0a71fe1..352b733183 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -405,6 +405,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + 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.2 version: 9.1.2(@types/react@18.3.23)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) @@ -417,6 +420,9 @@ importers: '@storybook/react-vite': specifier: ^9.1.2 version: 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.2(@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@storybook/test': + specifier: ^8.6.14 + version: 8.6.14(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.16(tailwindcss@3.4.17) @@ -444,6 +450,9 @@ importers: '@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.43.1)(tsx@4.20.3)(yaml@2.8.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) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) @@ -465,6 +474,9 @@ importers: globals: specifier: ^16.3.0 version: 16.3.0 + lucide-react: + specifier: ^0.542.0 + version: 0.542.0(react@18.3.1) playwright: specifier: ^1.54.1 version: 1.54.1 @@ -486,6 +498,9 @@ importers: tailwind-merge: specifier: ^3.3.1 version: 3.3.1 + tailwind-scrollbar: + specifier: ^4.0.2 + version: 4.0.2(react@18.3.1)(tailwindcss@3.4.17) tailwindcss: specifier: ^3.4.17 version: 3.4.17 @@ -1284,6 +1299,9 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-arrow@1.1.7': resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} peerDependencies: @@ -1363,6 +1381,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.2': resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} peerDependencies: @@ -1420,6 +1451,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.9': resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} peerDependencies: @@ -1446,6 +1490,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: @@ -1494,6 +1551,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: @@ -2035,6 +2105,11 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + '@storybook/instrumenter@8.6.14': + resolution: {integrity: sha512-iG4MlWCcz1L7Yu8AwgsnfVAmMbvyRSk700Mfy2g4c8y5O+Cv1ejshE1LBBsCwHgkuqU0H4R0qu4g23+6UnUemQ==} + peerDependencies: + storybook: ^8.6.14 + '@storybook/react-dom-shim@9.0.18': resolution: {integrity: sha512-qGR/d9x9qWRRxITaBVQkMnb73kwOm+N8fkbZRxc7U4lxupXRvkMIDh247nn71SYVBnvbh6//AL7P6ghiPWZYjA==} peerDependencies: @@ -2091,6 +2166,11 @@ packages: typescript: optional: true + '@storybook/test@8.6.14': + resolution: {integrity: sha512-GkPNBbbZmz+XRdrhMtkxPotCLOQ1BaGNp/gFZYdGDk2KmUWBKmvc5JxxOhtoXM2703IzNFlQHSSNnhrDZYuLlw==} + peerDependencies: + storybook: ^8.6.14 + '@swc/core-darwin-arm64@1.13.2': resolution: {integrity: sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==} engines: {node: '>=10'} @@ -2377,10 +2457,18 @@ packages: resolution: {integrity: sha512-a+MxoAXG+Sq94Jp67OtveKOp2vQq75AWdVI8DRt6w19B0NEqpfm784FTLbVp/qdR1wmxCOmKAvElGSIiBOx5OQ==} engines: {node: '>=12'} + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} + '@testing-library/jest-dom@6.5.0': + resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/jest-dom@6.6.3': resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} @@ -2400,6 +2488,12 @@ packages: '@types/react-dom': optional: true + '@testing-library/user-event@14.5.2': + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@testing-library/user-event@14.6.1': resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} engines: {node: '>=12', npm: '>=6'} @@ -2610,6 +2704,9 @@ packages: '@types/pluralize@0.0.33': resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -2787,6 +2884,9 @@ packages: '@vitest/browser': optional: true + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -2812,6 +2912,12 @@ packages: vite: optional: true + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@3.2.3': resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==} @@ -2824,6 +2930,9 @@ packages: '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + '@vitest/spy@3.2.3': resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==} @@ -2835,6 +2944,12 @@ packages: peerDependencies: vitest: 3.2.4 + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@3.2.3': resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==} @@ -2987,6 +3102,15 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@xyflow/react@12.8.4': + resolution: {integrity: sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.68': + resolution: {integrity: sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -4817,6 +4941,11 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lucide-react@0.542.0: + resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} @@ -5408,6 +5537,11 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prism-react-renderer@2.4.1: + resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==} + peerDependencies: + react: '>=16.0.0' + proc-log@5.0.0: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -6050,6 +6184,12 @@ packages: tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwind-scrollbar@4.0.2: + resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==} + engines: {node: '>=12.13.0'} + peerDependencies: + tailwindcss: 4.x + tailwindcss@3.4.17: resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} @@ -6151,10 +6291,18 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tinyspy@4.0.3: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} @@ -7751,6 +7899,8 @@ snapshots: '@radix-ui/primitive@1.1.2': {} + '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-arrow@1.1.7(@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)': dependencies: '@radix-ui/react-primitive': 2.1.3(@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) @@ -7817,6 +7967,19 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-dismissable-layer@1.1.11(@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 @@ -7885,6 +8048,24 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-popper@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)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@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) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-portal@1.1.9(@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)': dependencies: '@radix-ui/react-primitive': 2.1.3(@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) @@ -7905,6 +8086,16 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-presence@1.1.5(@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)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-primitive@2.1.3(@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)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) @@ -7967,6 +8158,26 @@ snapshots: optionalDependencies: '@types/react': 18.3.23 + '@radix-ui/react-tooltip@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)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@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) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 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) + '@radix-ui/react-portal': 1.1.9(@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) + '@radix-ui/react-presence': 1.1.5(@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) + '@radix-ui/react-primitive': 2.1.3(@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) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@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) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 @@ -8649,6 +8860,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@storybook/instrumenter@8.6.14(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + '@storybook/global': 5.0.0 + '@vitest/utils': 2.1.9 + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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))': dependencies: react: 18.3.1 @@ -8721,6 +8938,17 @@ snapshots: optionalDependencies: typescript: 5.8.3 + '@storybook/test@8.6.14(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/instrumenter': 8.6.14(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@testing-library/dom': 10.4.0 + '@testing-library/jest-dom': 6.5.0 + '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) + '@vitest/expect': 2.0.5 + '@vitest/spy': 2.0.5 + storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@swc/core-darwin-arm64@1.13.2': optional: true @@ -8995,6 +9223,17 @@ snapshots: '@tanstack/virtual-file-routes@1.129.7': {} + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 @@ -9006,6 +9245,16 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 + '@testing-library/jest-dom@6.5.0': + dependencies: + '@adobe/css-tools': 4.4.3 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + '@testing-library/jest-dom@6.6.3': dependencies: '@adobe/css-tools': 4.4.3 @@ -9026,6 +9275,10 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 @@ -9283,6 +9536,8 @@ snapshots: '@types/pluralize@0.0.33': {} + '@types/prismjs@1.26.5': {} + '@types/prop-types@15.7.15': {} '@types/react-dom@18.3.7(@types/react@18.3.23)': @@ -9554,6 +9809,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.2.1 + tinyrainbow: 1.2.0 + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -9586,6 +9848,14 @@ snapshots: optionalDependencies: vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.2.3': dependencies: tinyrainbow: 2.0.0 @@ -9606,6 +9876,10 @@ snapshots: magic-string: 0.30.17 pathe: 2.0.3 + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@3.2.3': dependencies: tinyspy: 4.0.3 @@ -9625,6 +9899,19 @@ snapshots: 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.43.1)(tsx@4.20.3)(yaml@2.8.0) + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + estree-walker: 3.0.3 + loupe: 3.1.4 + tinyrainbow: 1.2.0 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.1.4 + tinyrainbow: 1.2.0 + '@vitest/utils@3.2.3': dependencies: '@vitest/pretty-format': 3.2.3 @@ -9861,6 +10148,29 @@ snapshots: '@xtuc/long@4.2.2': {} + '@xyflow/react@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)': + dependencies: + '@xyflow/system': 0.0.68 + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.68': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -11758,6 +12068,10 @@ snapshots: dependencies: yallist: 4.0.0 + lucide-react@0.542.0(react@18.3.1): + dependencies: + react: 18.3.1 + lunr@2.3.9: {} lz-string@1.5.0: {} @@ -12540,6 +12854,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + prism-react-renderer@2.4.1(react@18.3.1): + dependencies: + '@types/prismjs': 1.26.5 + clsx: 2.1.1 + react: 18.3.1 + proc-log@5.0.0: {} process-nextick-args@2.0.1: {} @@ -13358,6 +13678,13 @@ snapshots: tailwind-merge@3.3.1: {} + tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@3.4.17): + dependencies: + prism-react-renderer: 2.4.1(react@18.3.1) + tailwindcss: 3.4.17 + transitivePeerDependencies: + - react + tailwindcss@3.4.17: dependencies: '@alloc/quick-lru': 5.2.0 @@ -13487,8 +13814,12 @@ snapshots: tinypool@1.1.1: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} + tinyspy@3.0.2: {} + tinyspy@4.0.3: {} tldts-core@6.1.86: {} diff --git a/web/common/.gitignore b/web/common/.gitignore index 392d3f0ae6..93662554cd 100644 --- a/web/common/.gitignore +++ b/web/common/.gitignore @@ -4,3 +4,5 @@ tsconfig.tsbuildinfo *storybook.log storybook-static +**/__snapshots__/** +**/__screenshots__/** \ No newline at end of file diff --git a/web/common/package.json b/web/common/package.json index 5ad2c29389..2501b9f81a 100644 --- a/web/common/package.json +++ b/web/common/package.json @@ -4,10 +4,12 @@ "devDependencies": { "@eslint/js": "^9.31.0", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.8", "@storybook/addon-docs": "^9.1.2", "@storybook/addon-essentials": "^9.0.0-alpha.12", "@storybook/addon-onboarding": "^9.1.2", "@storybook/react-vite": "^9.1.2", + "@storybook/test": "^8.6.14", "@tailwindcss/typography": "^0.5.16", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", @@ -17,6 +19,7 @@ "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.7.0", "@vitest/browser": "^3.2.4", + "@xyflow/react": "^12.8.4", "autoprefixer": "^10.4.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -24,6 +27,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-storybook": "^9.1.2", "globals": "^16.3.0", + "lucide-react": "^0.542.0", "playwright": "^1.54.1", "postcss": "^8.5.6", "react": "^18.3.1", @@ -31,6 +35,7 @@ "storybook": "^9.1.2", "syncpack": "^13.0.4", "tailwind-merge": "^3.3.1", + "tailwind-scrollbar": "^4.0.2", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", @@ -62,9 +67,12 @@ "module": "dist/sqlmesh-common.es.js", "peerDependencies": { "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/typography": "^0.5.16", + "@xyflow/react": "^12.8.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "lucide-react": "^0.542.0", "react": "^18.3.1", "react-dom": "^18.3.1", "tailwind-merge": "^3.3.1", diff --git a/web/common/src/components/Badge/Badge.css b/web/common/src/components/Badge/Badge.css index 029ba541f1..582a1264fb 100644 --- a/web/common/src/components/Badge/Badge.css +++ b/web/common/src/components/Badge/Badge.css @@ -1,4 +1,4 @@ :root { - --color-badge-background: var(--color-gray-100); + --color-badge-background: var(--color-neutral-100); --color-badge-foreground: var(--color-prose); } diff --git a/web/common/src/components/Badge/Badge.stories.tsx b/web/common/src/components/Badge/Badge.stories.tsx index aec5bd0bca..09754d29a8 100644 --- a/web/common/src/components/Badge/Badge.stories.tsx +++ b/web/common/src/components/Badge/Badge.stories.tsx @@ -1,23 +1,11 @@ import type { Meta, StoryObj } from '@storybook/react-vite' -import { EnumShape, EnumSize } from '@/types/enums' +import type { Shape, Size } from '@/types' import { Badge } from './Badge' const meta: Meta = { title: 'Components/Badge', component: Badge, - tags: ['autodocs'], - argTypes: { - size: { - control: { type: 'select' }, - options: Object.values(EnumSize), - }, - shape: { - control: { type: 'select' }, - options: Object.values(EnumShape), - }, - children: { control: 'text' }, - }, } export default meta @@ -29,10 +17,13 @@ export const Default: Story = { }, } +const sizes: Size[] = ['2xs', 'xs', 's', 'm', 'l', 'xl', '2xl'] +const shapes: Shape[] = ['square', 'round', 'pill'] + export const Sizes: Story = { render: args => (
      - {Object.values(EnumSize).map(size => ( + {sizes.map(size => ( (
      - {Object.values(EnumShape).map(shape => ( + {shapes.map(shape => ( Primary Badge Secondary Badge Failed Badge diff --git a/web/common/src/components/Badge/Badge.test.tsx b/web/common/src/components/Badge/Badge.test.tsx deleted file mode 100644 index 9ee5f2c58c..0000000000 --- a/web/common/src/components/Badge/Badge.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { render, screen } from '@testing-library/react' - -import { Badge } from './Badge' -import { badgeVariants } from './help' -import { EnumShape, EnumSize } from '@/types/enums' -import { cn } from '@/utils' - -describe('Badge', () => { - it('renders with default props and children', () => { - render(Test Badge) - expect(screen.getByText('Test Badge')).toBeInTheDocument() - }) - - it('applies the size class for each size', () => { - Object.values(EnumSize).forEach(size => { - const variants = cn(badgeVariants({ size })) - render(Size {size}) - expect(screen.getByText(`Size ${size}`)).toHaveClass(variants) - }) - }) - - it('applies the shape class for each shape', () => { - Object.values(EnumShape).forEach(shape => { - const variants = cn(badgeVariants({ shape })) - render(Shape {shape}) - expect(screen.getByText(`Shape ${shape}`)).toHaveClass(variants) - }) - }) - - it('supports custom size and shape', () => { - render( - - Custom Size and Shape - , - ) - expect(screen.getByText('Custom Size and Shape')).toHaveClass( - cn(badgeVariants({ size: EnumSize.XXL, shape: EnumShape.Square })), - ) - }) - - it('applies custom className', () => { - render(Custom Class) - expect(screen.getByText('Custom Class')).toHaveClass('custom-class') - }) - - it('renders as a child element when asChild is true', () => { - render( - - Link Badge - , - ) - expect(screen.getByText('Link Badge').tagName).toBe('A') - }) -}) diff --git a/web/common/src/components/Badge/Badge.tsx b/web/common/src/components/Badge/Badge.tsx index 93f380bddd..2cf561ebc1 100644 --- a/web/common/src/components/Badge/Badge.tsx +++ b/web/common/src/components/Badge/Badge.tsx @@ -1,16 +1,13 @@ import { Slot } from '@radix-ui/react-slot' -import { type VariantProps } from 'class-variance-authority' import React from 'react' -import { type Size, type Shape } from '@/types/enums' +import type { Shape, Size } from '@/types' import { cn } from '@/utils' -import { badgeVariants } from './help' +import { cva } from 'class-variance-authority' import './Badge.css' -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps { +export interface BadgeProps extends React.HTMLAttributes { asChild?: boolean size?: Size shape?: Shape @@ -30,3 +27,33 @@ export const Badge = React.forwardRef( }, ) Badge.displayName = 'Badge' + +const size: Record = { + '2xs': 'h-5 px-2 text-2xs leading-none rounded-2xs', + xs: 'h-6 px-2 text-2xs rounded-xs', + s: 'h-7 px-3 text-xs rounded-sm', + m: 'h-8 px-4 rounded-md', + l: 'h-9 px-4 rounded-lg', + xl: 'h-10 px-4 rounded-xl', + '2xl': 'h-11 px-6 rounded-2xl', +} + +const shape: Record = { + square: 'rounded-none', + round: 'rounded-inherit', + pill: 'rounded-full', +} + +const badgeVariants = cva( + 'bg-badge-background text-badge-foreground font-mono inline-flex align-middle items-center justify-center gap-2 leading-none whitespace-nowrap font-semibold', + { + variants: { + size, + shape, + }, + defaultVariants: { + size: 's', + shape: 'round', + }, + }, +) diff --git a/web/common/src/components/Badge/help.ts b/web/common/src/components/Badge/help.ts deleted file mode 100644 index df489eb8d2..0000000000 --- a/web/common/src/components/Badge/help.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { cva } from 'class-variance-authority' - -import { EnumShape, EnumSize } from '@/types/enums' - -export const badgeVariants = cva( - 'bg-badge-background text-badge-foreground font-mono inline-flex align-middle items-center justify-center gap-2 leading-none whitespace-nowrap font-semibold', - { - variants: { - size: { - [EnumSize.XXS]: 'h-5 px-2 text-2xs leading-none rounded-2xs', - [EnumSize.XS]: 'h-6 px-2 text-2xs rounded-xs', - [EnumSize.S]: 'h-7 px-3 text-xs rounded-sm', - [EnumSize.M]: 'h-8 px-4 rounded-md', - [EnumSize.L]: 'h-9 px-4 rounded-lg', - [EnumSize.XL]: 'h-10 px-4 rounded-xl', - [EnumSize.XXL]: 'h-11 px-6 rounded-2xl', - }, - shape: { - [EnumShape.Square]: 'rounded-none', - [EnumShape.Round]: 'rounded-inherit', - [EnumShape.Pill]: 'rounded-full', - }, - }, - defaultVariants: { - size: EnumSize.S, - shape: EnumShape.Round, - }, - }, -) diff --git a/web/common/src/components/Button/Button.css b/web/common/src/components/Button/Button.css new file mode 100644 index 0000000000..339c14675b --- /dev/null +++ b/web/common/src/components/Button/Button.css @@ -0,0 +1,31 @@ +:root { + --color-button-primary-background: var(--color-action); + --color-button-primary-foreground: var(--color-light); + --color-button-primary-hover: var(--color-action-hover); + --color-button-primary-active: var(--color-action-active); + + --color-button-secondary-background: var(--color-neutral-100); + --color-button-secondary-foreground: var(--color-prose); + --color-button-secondary-hover: var(--color-neutral-125); + --color-button-secondary-active: var(--color-neutral-150); + + --color-button-alternative-background: var(--color-light); + --color-button-alternative-foreground: var(--color-prose); + --color-button-alternative-hover: var(--color-neutral-125); + --color-button-alternative-active: var(--color-neutral-150); + + --color-button-destructive-background: var(--color-neutral-100); + --color-button-destructive-foreground: var(--color-destructive-foreground); + --color-button-destructive-hover: var(--color-neutral-125); + --color-button-destructive-active: var(--color-neutral-150); + + --color-button-danger-background: var(--color-destructive); + --color-button-danger-foreground: var(--color-light); + --color-button-danger-hover: var(--color-destructive-hover); + --color-button-danger-active: var(--color-destructive-active); + + --color-button-transparent-background: transparent; + --color-button-transparent-foreground: var(--color-prose); + --color-button-secondary-hover: var(--color-neutral-125); + --color-button-secondary-active: var(--color-neutral-150); +} diff --git a/web/common/src/components/Button/Button.stories.tsx b/web/common/src/components/Button/Button.stories.tsx new file mode 100644 index 0000000000..57fb9f26e2 --- /dev/null +++ b/web/common/src/components/Button/Button.stories.tsx @@ -0,0 +1,120 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import type { Size } from '@/types' +import { Button, type ButtonVariant } from './Button' +import { fn, expect, userEvent, within } from 'storybook/test' + +const buttonVariants: ButtonVariant[] = [ + 'primary', + 'secondary', + 'alternative', + 'destructive', + 'danger', + 'transparent', +] + +const meta: Meta = { + title: 'Components/Button', + component: Button, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + children: 'Default Button', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + await expect(canvas.getByText('Default Button')).toBeInTheDocument() + }, +} + +export const Variants: Story = { + render: args => ( +
      + {Object.values(buttonVariants).map(variant => ( + + ))} +
      + ), +} + +const sizes: Size[] = ['2xs', 'xs', 's', 'm', 'l', 'xl', '2xl'] + +export const Sizes: Story = { + render: args => ( +
      + {sizes.map(size => ( + + ))} +
      + ), +} + +export const Disabled: Story = { + args: { + children: 'Disabled Button', + disabled: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const button = canvas.getByRole('button') + await expect(button).toBeDisabled() + await expect(button).toHaveTextContent('Disabled Button') + }, +} + +export const AsChild: Story = { + render: args => ( +
      + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const linkElement = canvas.getByText('Link as Button') + await expect(linkElement.tagName).toBe('A') + await expect(linkElement).toHaveAttribute('href', '#') + }, +} + +export const InteractiveClick: Story = { + args: { + children: 'Click Me', + onClick: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement) + const user = userEvent.setup() + const button = canvas.getByRole('button') + await expect(button).toBeInTheDocument() + await user.click(button) + await expect(args.onClick).toHaveBeenCalledTimes(1) + await user.click(button) + await expect(args.onClick).toHaveBeenCalledTimes(2) + }, +} diff --git a/web/common/src/components/Button/Button.tsx b/web/common/src/components/Button/Button.tsx new file mode 100644 index 0000000000..46f9c8cf1b --- /dev/null +++ b/web/common/src/components/Button/Button.tsx @@ -0,0 +1,89 @@ +import React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva } from 'class-variance-authority' + +import { cn } from '@/utils' +import type { Shape, Size } from '@/types' + +import './Button.css' + +export type ButtonVariant = + | 'primary' + | 'secondary' + | 'alternative' + | 'destructive' + | 'danger' + | 'transparent' + +export interface ButtonProps + extends React.ButtonHTMLAttributes { + variant?: ButtonVariant + size?: Size + shape?: Shape + asChild?: boolean +} + +export const Button = React.forwardRef( + ({ className, variant, disabled, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + }, +) +Button.displayName = 'Button' + +const size: Record = { + '2xs': 'h-5 px-2 text-2xs leading-none rounded-2xs', + xs: 'h-6 px-2 text-2xs rounded-xs', + s: 'h-7 px-3 text-xs rounded-sm', + m: 'h-8 px-4 rounded-md', + l: 'h-9 px-4 rounded-lg', + xl: 'h-10 px-4 rounded-xl', + '2xl': 'h-11 px-6 rounded-2xl', +} + +const variant: Record = { + primary: + 'bg-button-primary-background text-button-primary-foreground hover:bg-button-primary-hover active:bg-button-primary-active', + secondary: + 'bg-button-secondary-background text-button-secondary-foreground hover:bg-button-secondary-hover active:bg-button-secondary-active', + alternative: + 'bg-button-alternative-background text-button-alternative-foreground border-neutral-200 hover:bg-button-alternative-hover active:bg-button-alternative-active', + destructive: + 'bg-button-destructive-background text-button-destructive-foreground hover:bg-button-destructive-hover active:bg-button-destructive-active', + danger: + 'bg-button-danger-background text-button-danger-foreground hover:bg-button-danger-hover active:bg-button-danger-active', + transparent: + 'bg-button-transparent-background text-button-transparent-foreground hover:bg-button-transparent-hover active:bg-button-transparent-active', +} + +const shape: Record = { + square: 'rounded-none', + round: 'rounded-inherit', + pill: 'rounded-full', +} + +const buttonVariants = cva( + 'inline-flex items-center w-fit justify-center gap-1 whitespace-nowrap leading-none font-semibold ring-offset-light transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focused focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 border border-[transparent]', + { + variants: { + variant, + size, + shape, + }, + defaultVariants: { + variant: 'primary', + size: 's', + shape: 'round', + }, + }, +) diff --git a/web/common/src/components/CopyButton/CopyButton.stories.tsx b/web/common/src/components/CopyButton/CopyButton.stories.tsx new file mode 100644 index 0000000000..191171f495 --- /dev/null +++ b/web/common/src/components/CopyButton/CopyButton.stories.tsx @@ -0,0 +1,126 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { expect, userEvent, waitFor, within, fn } from 'storybook/test' + +import { CopyButton } from './CopyButton' +import { Check, Copy } from 'lucide-react' + +const meta: Meta = { + title: 'Components/CopyButton', + component: CopyButton, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + text: 'Hello, World!', + children: copied => (copied ? 'Copied!' : 'Copy'), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const button = canvas.getByRole('button') + await expect(button).toHaveTextContent('Copy') + await expect(button).toBeEnabled() + const writeTextSpy = fn().mockResolvedValue(undefined) + if (navigator.clipboard) { + navigator.clipboard.writeText = writeTextSpy + } else { + Object.defineProperty(navigator, 'clipboard', { + writable: true, + value: { + writeText: writeTextSpy, + }, + }) + } + const user = userEvent.setup() + await user.click(button) + await expect(writeTextSpy).toHaveBeenCalledWith('Hello, World!') + await waitFor(() => { + expect(button).toHaveTextContent('Copied!') + }) + await expect(button).toBeDisabled() + }, +} + +export const WithIcons: Story = { + args: { + text: 'Copy this text with icon feedback', + children: copied => ( + <> + {copied ? ( + <> + + Copied! + + ) : ( + <> + + Copy + + )} + + ), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const button = canvas.getByRole('button') + + // Initial state with Copy icon + await expect(button).toHaveTextContent('Copy') + + // Mock clipboard API + Object.defineProperty(navigator, 'clipboard', { + writable: true, + value: { + writeText: fn().mockResolvedValue(undefined), + }, + }) + + const user = userEvent.setup() + await user.click(button) + + // Should switch to Check icon and Copied text + await waitFor(() => { + expect(button).toHaveTextContent('Copied!') + }) + }, +} + +export const IconOnly: Story = { + args: { + text: 'This is the text to copy', + children: copied => (copied ? : ), + }, +} + +export const CustomDelay: Story = { + args: { + text: 'This stays copied for 5 seconds', + delay: 5000, + children: copied => (copied ? 'Copied! (5s delay)' : 'Copy (5s feedback)'), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const button = canvas.getByRole('button') + + // Mock clipboard API + Object.defineProperty(navigator, 'clipboard', { + writable: true, + value: { + writeText: fn().mockResolvedValue(undefined), + }, + }) + + const user = userEvent.setup() + await user.click(button) + + // Should show copied state + await waitFor(() => { + expect(button).toHaveTextContent('Copied! (5s delay)') + }) + + // Button should remain disabled for custom delay + await expect(button).toBeDisabled() + }, +} diff --git a/web/common/src/components/CopyButton/CopyButton.test.tsx b/web/common/src/components/CopyButton/CopyButton.test.tsx new file mode 100644 index 0000000000..b2f8b38407 --- /dev/null +++ b/web/common/src/components/CopyButton/CopyButton.test.tsx @@ -0,0 +1,112 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { + vi, + describe, + it, + expect, + afterEach, + beforeEach, + type MockInstance, +} from 'vitest' + +import { CopyButton } from './CopyButton' + +describe('CopyButton', () => { + let writeTextSpy: MockInstance + + beforeEach(() => { + writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText') + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('copies text to clipboard on click', async () => { + const user = userEvent.setup() + const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText') + render( + + {copied => (copied ? 'Copied!' : 'Copy')} + , + ) + const button = screen.getByRole('button') + await user.click(button) + expect(writeTextSpy).toHaveBeenCalledWith('Hello, World!') + expect(writeTextSpy).toHaveBeenCalledTimes(1) + }) + + it('shows copied state after clicking', async () => { + const user = userEvent.setup() + render( + + {copied => (copied ? 'Copied!' : 'Copy')} + , + ) + const button = screen.getByRole('button') + expect(button).toHaveTextContent('Copy') + await user.click(button) + await waitFor(() => { + expect(button).toHaveTextContent('Copied!') + }) + }) + + it('disables button while in copied state', async () => { + const user = userEvent.setup() + render( + + {copied => (copied ? 'Copied!' : 'Copy')} + , + ) + const button = screen.getByRole('button') + expect(button).toBeEnabled() + await user.click(button) + await waitFor(() => { + expect(button).toBeDisabled() + }) + }) + + it('resets to initial state after delay', async () => { + const user = userEvent.setup() + render( + + {copied => (copied ? 'Copied!' : 'Copy')} + , + ) + const button = screen.getByRole('button') + await user.click(button) + await waitFor(() => { + expect(button).toHaveTextContent('Copied!') + }) + await waitFor( + () => { + expect(button).toHaveTextContent('Copy') + expect(button).toBeEnabled() + }, + { timeout: 200 }, + ) + }) + + it('calls onClick handler if provided', async () => { + const onClickSpy = vi.fn() + const user = userEvent.setup() + render( + + {() => 'Copy'} + , + ) + await user.click(screen.getByRole('button')) + expect(onClickSpy).toHaveBeenCalled() + expect(writeTextSpy).toHaveBeenCalled() + }) +}) diff --git a/web/common/src/components/CopyButton/CopyButton.tsx b/web/common/src/components/CopyButton/CopyButton.tsx new file mode 100644 index 0000000000..5eba483010 --- /dev/null +++ b/web/common/src/components/CopyButton/CopyButton.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react' + +import { Button, type ButtonProps } from '@/components/Button/Button' +import { cn } from '@/utils' + +type TimerID = ReturnType + +export interface CopyButtonProps extends Omit { + text: string + delay?: number + children: (copied: boolean) => React.ReactNode +} + +export const CopyButton = React.forwardRef( + ( + { + text, + title = 'Copy to clipboard', + variant = 'secondary', + size = 'xs', + delay = 2000, + disabled = false, + className, + children, + onClick, + ...props + }, + ref, + ) => { + const [copied, setCopied] = useState(null) + + const copy = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + + if (copied) { + clearTimeout(copied) + } + + navigator.clipboard.writeText(text).then(() => { + setCopied(setTimeout(() => setCopied(null), delay)) + }) + + onClick?.(e) + } + + return ( + + ) + }, +) +CopyButton.displayName = 'CopyButton' diff --git a/web/common/src/components/HorizontalContainer/HorizontalContainer.stories.tsx b/web/common/src/components/HorizontalContainer/HorizontalContainer.stories.tsx new file mode 100644 index 0000000000..0857900abc --- /dev/null +++ b/web/common/src/components/HorizontalContainer/HorizontalContainer.stories.tsx @@ -0,0 +1,78 @@ +import { HorizontalContainer } from './HorizontalContainer' + +export default { + title: 'Components/Containers/HorizontalContainer', + component: HorizontalContainer, +} + +const content = Array.from({ length: 20 }, (_, i) => ( +
      + Col {i + 1} +
      +)) + +export const Default = (args: any) => ( +
      + + {content} + +
      +) +Default.storyName = 'Default (No Scroll)' + +export const WithScroll = (args: any) => ( +
      + +
      {content}
      +
      +
      +) + +export const CustomClassName = (args: any) => ( +
      + + {content} + +
      +) +CustomClassName.storyName = 'With Custom ClassName' + +export const NestedHorizontalContainer = (args: any) => ( +
      + +
      Left
      +
      + +
      {content}
      +
      +
      +
      + Right +
      +
      +
      +) +NestedHorizontalContainer.storyName = 'Nested HorizontalContainer' diff --git a/web/common/src/components/HorizontalContainer/HorizontalContainer.test.tsx b/web/common/src/components/HorizontalContainer/HorizontalContainer.test.tsx new file mode 100644 index 0000000000..52c62c7029 --- /dev/null +++ b/web/common/src/components/HorizontalContainer/HorizontalContainer.test.tsx @@ -0,0 +1,61 @@ +import { createRef } from 'react' +import { describe, expect, it } from 'vitest' + +import { render, screen } from '@testing-library/react' +import { HorizontalContainer } from './HorizontalContainer' + +describe('HorizontalContainer', () => { + it('renders children correctly', () => { + render( + +
      Test Child
      +
      , + ) + expect(screen.getByText('Test Child')).toBeInTheDocument() + }) + + it('should force layout to be horizontal (still having flex-row)', () => { + render( + +
      Child
      +
      , + ) + const container = screen.getByText('Child').parentElement + expect(container).toHaveClass('flex-row') + }) + + it('renders ScrollContainer when scroll is true', () => { + render( + +
      Scroll Child
      +
      , + ) + expect( + screen.getByText('Scroll Child').parentElement?.parentElement, + ).toHaveClass('overflow-x-scroll scrollbar-h-[6px]') + }) + + it('renders a div when scroll is false', () => { + render( + +
      Div Child
      +
      , + ) + const container = screen.getByText('Div Child').parentElement + expect(container).toHaveClass('overflow-hidden') + }) + + it('forwards ref to the div element when scroll is false', () => { + const ref = createRef() + render( + +
      Ref Child
      +
      , + ) + expect(ref.current).toBeInstanceOf(HTMLElement) + expect(ref.current?.tagName).toBe('DIV') + }) +}) diff --git a/web/common/src/components/HorizontalContainer/HorizontalContainer.tsx b/web/common/src/components/HorizontalContainer/HorizontalContainer.tsx new file mode 100644 index 0000000000..c1e2c66ed0 --- /dev/null +++ b/web/common/src/components/HorizontalContainer/HorizontalContainer.tsx @@ -0,0 +1,44 @@ +import React from 'react' + +import { cn } from '@/utils' +import { ScrollContainer } from '../ScrollContainer/ScrollContainer' + +export interface HorizontalContainerProps + extends React.HTMLAttributes { + scroll?: boolean +} + +export const HorizontalContainer = React.forwardRef< + HTMLDivElement, + HorizontalContainerProps +>(({ children, className, scroll = false, ...props }, ref) => { + return scroll ? ( + + + {children} + + + ) : ( +
      + {children} +
      + ) +}) + +HorizontalContainer.displayName = 'HorizontalContainer' diff --git a/web/common/src/components/ModelName/ModelName.css b/web/common/src/components/ModelName/ModelName.css new file mode 100644 index 0000000000..42e11a061b --- /dev/null +++ b/web/common/src/components/ModelName/ModelName.css @@ -0,0 +1,17 @@ +:root { + --color-model-name-grayscale-link-underline: var(--color-neutral-125); + --color-model-name-grayscale-link-underline-hover: var(--color-neutral-500); + --color-model-name-link-underline: var(--color-link-underline); + --color-model-name-link-underline-hover: var(--color-link-hover); + + --color-model-name-grayscale-catalog: var(--color-neutral-400); + --color-model-name-grayscale-schema: var(--color-neutral-600); + --color-model-name-grayscale-model: var(--color-neutral-800); + + --color-model-name-catalog: var(--color-catalog); + --color-model-name-schema: var(--color-schema); + --color-model-name-model: var(--color-model); + + --color-model-name-copy-icon: var(--color-neutral-600); + --color-model-name-copy-icon-hover: var(--color-neutral-100); +} diff --git a/web/common/src/components/ModelName/ModelName.stories.tsx b/web/common/src/components/ModelName/ModelName.stories.tsx new file mode 100644 index 0000000000..fe54681f10 --- /dev/null +++ b/web/common/src/components/ModelName/ModelName.stories.tsx @@ -0,0 +1,92 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { ModelName } from './ModelName' + +const meta: Meta = { + title: 'Components/ModelName', + component: ModelName, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + name: 'catalog.schema.model', + }, +} + +export const WithoutCatalog: Story = { + args: { + name: 'catalog.schema.model', + hideCatalog: true, + }, +} + +export const WithoutSchema: Story = { + args: { + name: 'catalog.schema.model', + hideSchema: true, + }, +} + +export const WithoutIcon: Story = { + args: { + name: 'catalog.schema.model', + hideIcon: true, + }, +} + +export const WithTooltip: Story = { + args: { + name: 'catalog.schema.model', + hideCatalog: true, + hideSchema: true, + showTooltip: true, + }, +} + +export const WithoutTooltip: Story = { + args: { + name: 'catalog.model', + showTooltip: false, + }, +} + +export const CustomClassName: Story = { + args: { + name: 'catalog.schema.model', + className: 'text-xl font-bold', + }, +} + +export const LongName: Story = { + args: { + name: 'veryveryverylongcatalogname.veryveryverylongschamename.veryveryverylongmodelnameveryveryverylongmodelname', + }, +} + +export const Grayscale: Story = { + args: { + name: 'catalog.schema.model', + grayscale: true, + }, +} + +export const Link: Story = { + args: { + name: 'catalog.schema.model', + link: 'https://www.google.com', + grayscale: false, + showCopy: true, + }, +} + +export const LinkGrayscale: Story = { + args: { + name: 'catalog.schema.model', + link: 'https://www.google.com', + grayscale: true, + showCopy: true, + }, +} diff --git a/web/common/src/components/ModelName/ModelName.test.tsx b/web/common/src/components/ModelName/ModelName.test.tsx new file mode 100644 index 0000000000..37e78650dd --- /dev/null +++ b/web/common/src/components/ModelName/ModelName.test.tsx @@ -0,0 +1,72 @@ +import userEvent from '@testing-library/user-event' +import { describe, expect, it, vi } from 'vitest' +import { render, screen, within } from '@testing-library/react' +import { ModelName } from './ModelName' + +describe('ModelName', () => { + it('renders full model name with catalog, schema, and model', () => { + render() + expect(screen.getByText('cat')).toBeInTheDocument() + expect(screen.getByText('sch')).toBeInTheDocument() + expect(screen.getByText('model')).toBeInTheDocument() + }) + + it('hides catalog when hideCatalog is true', () => { + render( + , + ) + expect(screen.queryByText('cat')).not.toBeInTheDocument() + expect(screen.getByText('sch')).toBeInTheDocument() + expect(screen.getByText('model')).toBeInTheDocument() + }) + + it('hides schema when hideSchema is true', () => { + render( + , + ) + expect(screen.getByText('cat')).toBeInTheDocument() + expect(screen.queryByText('sch')).not.toBeInTheDocument() + expect(screen.getByText('model')).toBeInTheDocument() + }) + + it('hides icon when hideIcon is true', () => { + const { container } = render( + , + ) + // Should not render the Box icon SVG + expect(container.querySelector('svg')).toBeNull() + }) + + it('shows tooltip when showTooltip is true and catalog or schema is hidden', async () => { + render( + , + ) + // Tooltip trigger is present (icon) + const modelName = screen.getByTestId('model-name') + expect(modelName).toBeInTheDocument() + await userEvent.hover(modelName) + const tooltip = await screen.findByRole('tooltip') + expect(tooltip).toBeInTheDocument() + within(tooltip).getByText('cat.sch.model') + }) + + it('throws error if name is empty', () => { + // Suppress error output for this test + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}) + expect(() => render()).toThrow() + spy.mockRestore() + }) +}) diff --git a/web/common/src/components/ModelName/ModelName.tsx b/web/common/src/components/ModelName/ModelName.tsx new file mode 100644 index 0000000000..58ab33e1cb --- /dev/null +++ b/web/common/src/components/ModelName/ModelName.tsx @@ -0,0 +1,218 @@ +import { Box, Check, Copy } from 'lucide-react' +import { useMemo } from 'react' + +import { cn, truncate } from '@/utils' +import { Tooltip } from '@/components/Tooltip/Tooltip' +import React from 'react' + +import './ModelName.css' +import { CopyButton } from '../CopyButton/CopyButton' + +export interface ModelNameProps extends React.HTMLAttributes { + name: string + hideCatalog?: boolean + hideSchema?: boolean + hideIcon?: boolean + showTooltip?: boolean + showCopy?: boolean + truncateMaxChars?: number + truncateLimitBefore?: number + truncateLimitAfter?: number + grayscale?: boolean + link?: string + className?: string +} + +const MODEL_NAME_TOOLTIP_SIDE_OFFSET = 6 +const MODEL_NAME_ICON_SIZE = 16 + +export const ModelName = React.forwardRef( + ( + { + name, + hideCatalog = false, + hideSchema = false, + hideIcon = false, + showTooltip = true, + showCopy = false, + truncateMaxChars = 25, + truncateLimitBefore = 5, + truncateLimitAfter = 7, + grayscale = false, + link, + className, + ...props + }, + ref, + ) => { + if (!name) throw new Error('Model name should not be empty') + + const truncateMaxCharsModel = truncateMaxChars * 2 + + const { catalog, schema, model, withTooltip } = useMemo(() => { + const [model, schema, catalog] = name.split('.').reverse() + + return { + catalog: hideCatalog ? undefined : catalog, + schema: hideSchema ? undefined : schema, + model, + withTooltip: + ((hideCatalog && catalog) || + (hideSchema && schema) || + [catalog, schema].some(v => v && v.length > truncateMaxChars) || + model.length > truncateMaxCharsModel) && + showTooltip, + } + }, [ + name, + hideCatalog, + hideSchema, + truncateMaxCharsModel, + showTooltip, + truncateMaxChars, + ]) + + function renderTooltip() { + return ( + + {name} + + ) + } + + function renderIcon() { + return ( + + ) + } + + console.assert(name.length > 0, 'Model name should not be empty') + + function renderName() { + return ( + + {catalog && ( + <> + + {_truncate(catalog)} + + . + + )} + {schema && ( + <> + + {_truncate(schema)} + + . + + )} + + {truncate(model, truncateMaxCharsModel, 15)} + + + ) + } + + function renderNameWithTooltip() { + return withTooltip ? renderTooltip() : renderName() + } + + function _truncate(name: string, maxChars: number = truncateMaxChars) { + return truncate( + name, + maxChars, + truncateLimitBefore, + '...', + truncateLimitAfter, + ) + } + + return ( + + {!hideIcon && renderIcon()} + {link ? ( + + {renderNameWithTooltip()} + + ) : ( + renderNameWithTooltip() + )} + {showCopy && ( + + {copied => + copied ? ( + + ) : ( + + ) + } + + )} + + ) + }, +) + +ModelName.displayName = 'ModelName' diff --git a/web/common/src/components/ScrollContainer/ScrollContainer.css b/web/common/src/components/ScrollContainer/ScrollContainer.css new file mode 100644 index 0000000000..98ba5055a2 --- /dev/null +++ b/web/common/src/components/ScrollContainer/ScrollContainer.css @@ -0,0 +1,4 @@ +:root { + --scrollbar-thumb: var(--color-neutral-300); + --scrollbar-track: var(--color-neutral-100); +} diff --git a/web/common/src/components/ScrollContainer/ScrollContainer.stories.tsx b/web/common/src/components/ScrollContainer/ScrollContainer.stories.tsx new file mode 100644 index 0000000000..46b972d8f8 --- /dev/null +++ b/web/common/src/components/ScrollContainer/ScrollContainer.stories.tsx @@ -0,0 +1,109 @@ +import { ScrollContainer } from './ScrollContainer' + +export default { + title: 'Components/Containers/ScrollContainer', + component: ScrollContainer, +} + +const content = Array.from({ length: 30 }, (_, i) => ( +
      + Row {i + 1} +
      +)) + +export const VerticalScroll = (args: any) => ( +
      + + {content} + +
      +) + +export const HorizontalScroll = (args: any) => ( +
      + +
      + {Array.from({ length: 10 }, (_, i) => ( + + Column {i + 1} + + ))} +
      +
      +
      +) + +export const BothDirectionsScroll = (args: any) => ( +
      + +
      + {Array.from({ length: 30 }, (_, i) => ( +
      + Row {i + 1} - This is a long line of text that should cause + horizontal scrolling when combined with the vertical scroll +
      + ))} +
      +
      +
      +) + +export const CustomClassName = (args: any) => ( +
      + + {content} + +
      +) +CustomClassName.storyName = 'With Custom ClassName' + +export const PageContentLayout = (args: any) => ( +
      + +
      +
      + Actions +
      +
      + Content +
      +
      + End +
      +
      +
      +
      +) diff --git a/web/common/src/components/ScrollContainer/ScrollContainer.test.tsx b/web/common/src/components/ScrollContainer/ScrollContainer.test.tsx new file mode 100644 index 0000000000..b557ecfd38 --- /dev/null +++ b/web/common/src/components/ScrollContainer/ScrollContainer.test.tsx @@ -0,0 +1,67 @@ +import { createRef } from 'react' +import { describe, expect, it } from 'vitest' +import { render, screen } from '@testing-library/react' + +import { ScrollContainer } from './ScrollContainer' + +describe('ScrollContainer', () => { + it('renders children correctly', () => { + render( + +
      Test Child
      +
      , + ) + expect(screen.getByText('Test Child')).toBeInTheDocument() + }) + + it('applies custom className', () => { + render( + +
      Child
      +
      , + ) + const container = screen.getByText('Child').parentElement + expect(container).toHaveClass('custom-class') + }) + + it('applies vertical and horizontal scroll classes based on direction', () => { + const { rerender } = render( + +
      Child
      +
      , + ) + let container = screen.getByText('Child').parentElement + expect(container).toHaveClass('overflow-y-scroll scrollbar-w-[6px]') + expect(container).toHaveClass('overflow-x-hidden') + + rerender( + +
      Child
      +
      , + ) + container = screen.getByText('Child').parentElement + expect(container).toHaveClass('overflow-y-hidden') + expect(container).toHaveClass('overflow-x-scroll scrollbar-h-[6px]') + + rerender( + +
      Child
      +
      , + ) + container = screen.getByText('Child').parentElement + expect(container).toHaveClass('overflow-y-scroll scrollbar-w-[6px]') + expect(container).toHaveClass('overflow-x-scroll scrollbar-h-[6px]') + }) + + it('forwards ref to the span element', () => { + const ref = createRef() + render( + // @ts-expect-error: ScrollContainer's ref type is HTMLDivElement, but it renders a span + +
      Child
      +
      , + ) + expect(ref.current).toBeInstanceOf(HTMLElement) + expect(ref.current?.tagName).toBe('DIV') + }) +}) diff --git a/web/common/src/components/ScrollContainer/ScrollContainer.tsx b/web/common/src/components/ScrollContainer/ScrollContainer.tsx new file mode 100644 index 0000000000..19ce969e2f --- /dev/null +++ b/web/common/src/components/ScrollContainer/ScrollContainer.tsx @@ -0,0 +1,39 @@ +import React from 'react' + +import { cn } from '@/utils' +import type { LayoutDirection } from '@/types' + +import './ScrollContainer.css' + +export interface ScrollContainerProps + extends React.HTMLAttributes { + direction?: LayoutDirection +} + +export const ScrollContainer = React.forwardRef< + HTMLDivElement, + ScrollContainerProps +>(({ children, className, direction = 'vertical', ...props }, ref) => { + const vertical = direction === 'vertical' || direction === 'both' + const horizontal = direction === 'horizontal' || direction === 'both' + return ( +
      + {children} +
      + ) +}) + +ScrollContainer.displayName = 'ScrollContainer' diff --git a/web/common/src/components/Tooltip/Tooltip.css b/web/common/src/components/Tooltip/Tooltip.css new file mode 100644 index 0000000000..ba080f6974 --- /dev/null +++ b/web/common/src/components/Tooltip/Tooltip.css @@ -0,0 +1,4 @@ +:root { + --color-tooltip-background: var(--color-dark); + --color-tooltip-foreground: var(--color-light); +} diff --git a/web/common/src/components/Tooltip/Tooltip.stories.tsx b/web/common/src/components/Tooltip/Tooltip.stories.tsx new file mode 100644 index 0000000000..37f76f7e27 --- /dev/null +++ b/web/common/src/components/Tooltip/Tooltip.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { Tooltip } from '@/components/Tooltip/Tooltip' +import { Button } from '@/components/Button/Button' + +const meta: Meta = { + title: 'Components/Tooltip', + component: Tooltip, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + trigger: , + children: 'This is a tooltip', + }, +} diff --git a/web/common/src/components/Tooltip/Tooltip.tsx b/web/common/src/components/Tooltip/Tooltip.tsx new file mode 100644 index 0000000000..8096e8742e --- /dev/null +++ b/web/common/src/components/Tooltip/Tooltip.tsx @@ -0,0 +1,58 @@ +import { + TooltipProvider, + Tooltip as TooltipRoot, + TooltipTrigger, + TooltipPortal, + TooltipContent, +} from '@radix-ui/react-tooltip' +import React from 'react' + +import { cn } from '@/utils' +import type { Position } from '@/types' + +import './Tooltip.css' + +export type TooltipSide = Extract +export type TooltipAlign = Extract + +export function Tooltip({ + delayDuration = 200, + sideOffset = 0, + alignOffset = 0, + side = 'right', + align = 'center', + trigger, + children, + className, +}: { + trigger: React.ReactNode + side?: TooltipSide + align?: TooltipAlign + delayDuration?: number + sideOffset?: number + alignOffset?: number + children: React.ReactNode + className?: string +}) { + return ( + + + {trigger} + + + {children} + + + + + ) +} diff --git a/web/common/src/components/VerticalContainer/VerticalContainer.stories.tsx b/web/common/src/components/VerticalContainer/VerticalContainer.stories.tsx new file mode 100644 index 0000000000..8283b50a8d --- /dev/null +++ b/web/common/src/components/VerticalContainer/VerticalContainer.stories.tsx @@ -0,0 +1,78 @@ +import { VerticalContainer } from './VerticalContainer' + +export default { + title: 'Components/Containers/VerticalContainer', + component: VerticalContainer, +} + +const content = Array.from({ length: 20 }, (_, i) => ( +
      + Row {i + 1} +
      +)) + +export const Default = (args: any) => ( +
      + + {content} + +
      +) +Default.storyName = 'Default (No Scroll)' + +export const WithScroll = (args: any) => ( +
      + + {content} + +
      +) + +export const CustomClassName = (args: any) => ( +
      + + {content} + +
      +) +CustomClassName.storyName = 'With Custom ClassName' + +export const NestedVerticalContainer = (args: any) => ( +
      + +
      Header
      +
      + {content} +
      +
      + Footer +
      +
      +
      +) +NestedVerticalContainer.storyName = 'Nested VerticalContainer' diff --git a/web/common/src/components/VerticalContainer/VerticalContainer.test.tsx b/web/common/src/components/VerticalContainer/VerticalContainer.test.tsx new file mode 100644 index 0000000000..75a5c50c4e --- /dev/null +++ b/web/common/src/components/VerticalContainer/VerticalContainer.test.tsx @@ -0,0 +1,61 @@ +import { createRef } from 'react' +import { describe, expect, it } from 'vitest' + +import { render, screen } from '@testing-library/react' +import { VerticalContainer } from './VerticalContainer' + +describe('VerticalContainer', () => { + it('renders children correctly', () => { + render( + +
      Test Child
      +
      , + ) + expect(screen.getByText('Test Child')).toBeInTheDocument() + }) + + it('should force layout to be vertical (still having flex-col)', () => { + render( + +
      Child
      +
      , + ) + const container = screen.getByText('Child').parentElement + expect(container).toHaveClass('flex-col') + }) + + it('renders ScrollContainer when scroll is true', () => { + render( + +
      Scroll Child
      +
      , + ) + expect( + screen.getByText('Scroll Child').parentElement?.parentElement, + ).toHaveClass('overflow-y-scroll scrollbar-w-[6px]') + }) + + it('renders a div when scroll is false', () => { + render( + +
      Div Child
      +
      , + ) + const container = screen.getByText('Div Child').parentElement + expect(container).toHaveClass('overflow-hidden') + }) + + it('forwards ref to the div element when scroll is false', () => { + const ref = createRef() + render( + +
      Ref Child
      +
      , + ) + expect(ref.current).toBeInstanceOf(HTMLElement) + expect(ref.current?.tagName).toBe('DIV') + }) +}) diff --git a/web/common/src/components/VerticalContainer/VerticalContainer.tsx b/web/common/src/components/VerticalContainer/VerticalContainer.tsx new file mode 100644 index 0000000000..e592265dca --- /dev/null +++ b/web/common/src/components/VerticalContainer/VerticalContainer.tsx @@ -0,0 +1,44 @@ +import React from 'react' + +import { cn } from '@/utils' +import { ScrollContainer } from '../ScrollContainer/ScrollContainer' + +export interface VerticalContainerProps + extends React.HTMLAttributes { + scroll?: boolean +} + +export const VerticalContainer = React.forwardRef< + HTMLDivElement, + VerticalContainerProps +>(({ children, className, scroll = false, ...props }, ref) => { + return scroll ? ( + + + {children} + + + ) : ( +
      + {children} +
      + ) +}) + +VerticalContainer.displayName = 'VerticalContainer' diff --git a/web/common/src/index.ts b/web/common/src/index.ts index 309e993504..6cd52abead 100644 --- a/web/common/src/index.ts +++ b/web/common/src/index.ts @@ -1,37 +1,39 @@ // Components export { Badge, type BadgeProps } from '@/components/Badge/Badge' +export { Button, type ButtonProps } from '@/components/Button/Button' +export { + CopyButton, + type CopyButtonProps, +} from '@/components/CopyButton/CopyButton' +export { + HorizontalContainer, + type HorizontalContainerProps, +} from '@/components/HorizontalContainer/HorizontalContainer' +export { + ScrollContainer, + type ScrollContainerProps, +} from '@/components/ScrollContainer/ScrollContainer' +export { + VerticalContainer, + type VerticalContainerProps, +} from '@/components/VerticalContainer/VerticalContainer' +export { + ModelName, + type ModelNameProps, +} from '@/components/ModelName/ModelName' +export { Tooltip } from '@/components/Tooltip/Tooltip' // Utils -export { cn, isNil, notNil } from '@/utils' +export { cn, truncate } from '@/utils' // Types -export type { Nil, Optional, Maybe } from '@/types' -export { - EnumSize, - EnumHeadlineLevel, - EnumSide, - EnumLayoutDirection, - EnumShape, - type Size, - type HeadlineLevel, - type Side, - type LayoutDirection, - type Shape, -} from '@/types/enums' - -// Design Tokens -export { - colorToken, - spacingToken, - textSizeToken, - type ColorTokens, - type SpacingTokens, - type TypographyTokens, - type DesignTokens, - type ColorScale, - type ColorVariant, - type StepScale, - type TextSize, - type TextRole, - type CSSCustomProperty, -} from '@/styles/tokens' +export type { + Brand, + Branded, + Size, + HeadlineLevel, + Side, + LayoutDirection, + Shape, + Position, +} from '@/types' diff --git a/web/common/src/styles/design/palette.css b/web/common/src/styles/design/palette.css index 7fa379437f..4370038c97 100644 --- a/web/common/src/styles/design/palette.css +++ b/web/common/src/styles/design/palette.css @@ -194,6 +194,8 @@ --color-gray-550: hsl(190, 8%, 42%); --color-gray-600: hsl(190, 8%, 38%); --color-gray-700: hsl(202, 8%, 26%); + --color-gray-725: hsl(202, 8%, 22%); + --color-gray-750: hsl(202, 8%, 20%); --color-gray-800: hsl(214, 8%, 14%); --color-gray-900: hsl(226, 8%, 4%); } diff --git a/web/common/src/styles/design/semantic-colors.css b/web/common/src/styles/design/semantic-colors.css index 4a58ba52ff..f2a45e5eef 100644 --- a/web/common/src/styles/design/semantic-colors.css +++ b/web/common/src/styles/design/semantic-colors.css @@ -5,4 +5,61 @@ --color-dark: hsl(226, 24%, 8%); --color-brand: var(--color-tobiko); --color-prose: var(--color-gray-800); + --color-focused: var(--color-brand); + + /* Neutral */ + --color-neutral-3: var(--color-gray-5); + --color-neutral-5: var(--color-gray-5); + --color-neutral-10: var(--color-gray-10); + --color-neutral-15: var(--color-gray-15); + --color-neutral-20: var(--color-gray-20); + --color-neutral-25: var(--color-gray-25); + --color-neutral-100: var(--color-gray-100); + --color-neutral-125: var(--color-gray-125); + --color-neutral-150: var(--color-gray-150); + --color-neutral-200: var(--color-gray-200); + --color-neutral-300: var(--color-gray-300); + --color-neutral-400: var(--color-gray-400); + --color-neutral-500: var(--color-gray-500); + --color-neutral: var(--color-gray-500); + --color-neutral-525: var(--color-gray-525); + --color-neutral-550: var(--color-gray-550); + --color-neutral-600: var(--color-gray-600); + --color-neutral-700: var(--color-gray-700); + --color-neutral-725: var(--color-gray-725); + --color-neutral-750: var(--color-gray-750); + --color-neutral-800: var(--color-gray-800); + --color-neutral-900: var(--color-gray-900); + + /* Model */ + --color-catalog: var(--color-deep-blue-800); + --color-schema: var(--color-deep-blue-600); + --color-model: var(--color-deep-blue-500); + + /* Destructive */ + --color-destructive: var(--color-scarlet-500); + --color-destructive-foreground: var(--color-scarlet-600); + --color-destructive-hover: var(--color-scarlet-525); + --color-destructive-active: var(--color-scarlet-550); + + /* Success */ + --color-success: var(--color-emerald-500); + + /* Warning */ + --color-warning: var(--color-mandarin-500); + + /* Action */ + --color-action: var(--color-deep-blue-500); + --color-action-hover: var(--color-deep-blue-525); + --color-action-active: var(--color-deep-blue-550); + + /* Accent */ + --color-accent: var(--color-purple-500); + + /* Link */ + --color-link: var(--color-action); + --color-link-hover: var(--color-action-hover); + --color-link-active: var(--color-action-active); + --color-link-visited: var(--color-purple-600); + --color-link-underline: var(--color-deep-blue-125); } diff --git a/web/common/src/styles/tokens.ts b/web/common/src/styles/tokens.ts deleted file mode 100644 index 75d233fdb4..0000000000 --- a/web/common/src/styles/tokens.ts +++ /dev/null @@ -1,223 +0,0 @@ -/** - * Design Token TypeScript Definitions - * Type-safe access to CSS custom properties defined in the design system - */ - -// Color Tokens -export interface ColorTokens { - // Brand Colors - '--color-tobiko': string - '--color-sqlmesh': string - '--color-sqlglot': string - '--color-pacific': string - '--color-wasabi': string - '--color-yuzu': string - '--color-uni': string - '--color-salmon': string - - // Base Colors - '--color-white': string - '--color-black': string - '--color-cyan': string - '--color-deep-blue': string - '--color-purple': string - '--color-emerald': string - '--color-mandarin': string - '--color-scarlet': string - '--color-sunflower': string - '--color-peach': string - '--color-turquoise': string - '--color-fuchsia': string - '--color-gray': string - - // Semantic Colors - '--color-light': string - '--color-dark': string - '--color-brand': string - '--color-prose': string - '--color-badge-background': string - '--color-badge-foreground': string -} - -// Spacing Tokens -export interface SpacingTokens { - '--one': string - '--base': string - '--half': string - '--step': string - '--step-2': string - '--step-3': string - '--step-4': string - '--step-5': string - '--step-6': string - '--step-7': string - '--step-8': string - '--step-9': string - '--step-10': string - '--step-11': string - '--step-12': string - '--step-15': string - '--step-16': string - '--step-20': string - '--step-24': string - '--step-30': string - '--step-32': string -} - -// Typography Tokens -export interface TypographyTokens { - // Font Families - '--font-sans': string - '--font-accent': string - '--font-serif': string - '--font-mono': string - - // Font Sizes - '--font-size': string - '--text-2xs': string - '--text-xs': string - '--text-s': string - '--text-m': string - '--text-l': string - '--text-xl': string - '--text-2xl': string - '--text-3xl': string - '--text-4xl': string - '--text-headline': string - '--text-display': string - '--text-header': string - '--text-tagline': string - '--text-title': string - '--text-subtitle': string - - // Line Heights - '--leading': string - '--text-leading-xs': string - '--text-leading-s': string - '--text-leading-m': string - '--text-leading-l': string - '--text-leading-xl': string - - // Font Weights - '--font-weight': string - '--text-thin': string - '--text-extra-light': string - '--text-light': string - '--text-normal': string - '--text-medium': string - '--text-semibold': string - '--text-bold': string - '--text-extra-bold': string - '--text-black': string -} - -// Combined Design Tokens -export interface DesignTokens - extends ColorTokens, - SpacingTokens, - TypographyTokens {} - -// Utility type for accessing CSS custom properties -export type CSSCustomProperty = T - -// Type-safe color scale definitions -export type ColorScale = - | '5' - | '10' - | '15' - | '20' - | '25' - | '50' - | '60' - | '75' - | '100' - | '125' - | '150' - | '200' - | '300' - | '400' - | '500' - | '525' - | '550' - | '600' - | '700' - | '725' - | '750' - | '800' - | '900' - -export type ColorVariant = - | 'cyan' - | 'deep-blue' - | 'pacific' - | 'purple' - | 'emerald' - | 'mandarin' - | 'scarlet' - | 'gray' - | 'uni' - | 'salmon' - | 'turquoise' - | 'fuchsia' - -// Helper function to build color custom property strings -export function colorToken( - variant: ColorVariant, - scale?: ColorScale, -): CSSCustomProperty { - return scale ? `--color-${variant}-${scale}` : `--color-${variant}` -} - -// Step scale for spacing -export type StepScale = - | 2 - | 3 - | 4 - | 5 - | 6 - | 7 - | 8 - | 9 - | 10 - | 11 - | 12 - | 15 - | 16 - | 20 - | 24 - | 30 - | 32 - -// Helper function to build spacing custom property strings -export function spacingToken( - step?: StepScale | 'half', -): CSSCustomProperty { - if (step === 'half') return '--half' - return step ? `--step-${step}` : '--step' -} - -// Text size variants -export type TextSize = - | '2xs' - | 'xs' - | 's' - | 'm' - | 'l' - | 'xl' - | '2xl' - | '3xl' - | '4xl' -export type TextRole = - | 'headline' - | 'display' - | 'header' - | 'tagline' - | 'title' - | 'subtitle' - -// Helper function to build text size custom property strings -export function textSizeToken( - size: TextSize | TextRole, -): CSSCustomProperty { - return `--text-${size}` -} diff --git a/web/common/src/types.ts b/web/common/src/types.ts new file mode 100644 index 0000000000..d23ea8b86b --- /dev/null +++ b/web/common/src/types.ts @@ -0,0 +1,18 @@ +export declare const __brand: unique symbol + +export type Brand = { [__brand]: B } +export type Branded = T & Brand + +export type Size = '2xs' | 'xs' | 's' | 'm' | 'l' | 'xl' | '2xl' +export type HeadlineLevel = 1 | 2 | 3 | 4 | 5 | 6 +export type Side = 'left' | 'right' | 'both' +export type LayoutDirection = 'vertical' | 'horizontal' | 'both' +export type Shape = 'square' | 'round' | 'pill' +export type Position = + | 'top' + | 'right' + | 'bottom' + | 'left' + | 'center' + | 'start' + | 'end' diff --git a/web/common/src/types/enums.ts b/web/common/src/types/enums.ts deleted file mode 100644 index 9985bc7e5b..0000000000 --- a/web/common/src/types/enums.ts +++ /dev/null @@ -1,43 +0,0 @@ -export const EnumSize = { - XXS: '2xs', - XS: 'xs', - S: 's', - M: 'm', - L: 'l', - XL: 'xl', - XXL: '2xl', -} as const -export type Size = (typeof EnumSize)[keyof typeof EnumSize] - -export const EnumHeadlineLevel = { - H1: 1, - H2: 2, - H3: 3, - H4: 4, - H5: 5, - H6: 6, -} as const -export type HeadlineLevel = - (typeof EnumHeadlineLevel)[keyof typeof EnumHeadlineLevel] - -export const EnumSide = { - LEFT: 'left', - RIGHT: 'right', - BOTH: 'both', -} as const -export type Side = (typeof EnumSide)[keyof typeof EnumSide] - -export const EnumLayoutDirection = { - VERTICAL: 'vertical', - HORIZONTAL: 'horizontal', - BOTH: 'both', -} as const -export type LayoutDirection = - (typeof EnumLayoutDirection)[keyof typeof EnumLayoutDirection] - -export const EnumShape = { - Square: 'square', - Round: 'round', - Pill: 'pill', -} as const -export type Shape = (typeof EnumShape)[keyof typeof EnumShape] diff --git a/web/common/src/types/index.ts b/web/common/src/types/index.ts deleted file mode 100644 index a2d8ca7e51..0000000000 --- a/web/common/src/types/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type Nil = undefined | null -export type Optional = T | undefined -export type Maybe = T | Nil diff --git a/web/common/src/utils.ts b/web/common/src/utils.ts new file mode 100644 index 0000000000..56557b61cf --- /dev/null +++ b/web/common/src/utils.ts @@ -0,0 +1,32 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} +export function truncate( + text: string, + maxChars = 0, + limitBefore = 5, + delimiter = '...', + limitAfter?: number, +): string { + const textLength = text.length + + limitBefore = Math.abs(limitBefore) + limitAfter = limitAfter == null ? limitBefore : Math.abs(limitAfter) + + if (maxChars > textLength || limitBefore + limitAfter >= textLength) { + return text + } + + if (limitAfter === 0) { + return text.substring(0, limitBefore) + delimiter + } + + return ( + text.substring(0, limitBefore) + + delimiter + + text.substring(textLength - limitAfter) + ) +} diff --git a/web/common/src/utils/index.ts b/web/common/src/utils/index.ts deleted file mode 100644 index 95e869c212..0000000000 --- a/web/common/src/utils/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Nil } from '@/types' -import { clsx, type ClassValue } from 'clsx' -import { twMerge } from 'tailwind-merge' - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} - -export function isNil(value: unknown): value is Nil { - return value == null -} -export function notNil(value: unknown): value is NonNullable { - return value != null -} diff --git a/web/common/tailwind.base.config.js b/web/common/tailwind.base.config.js index e1b44d12d9..005c353379 100644 --- a/web/common/tailwind.base.config.js +++ b/web/common/tailwind.base.config.js @@ -11,10 +11,100 @@ module.exports = { light: 'var(--color-light)', brand: 'var(--color-brand)', prose: 'var(--color-prose)', + focused: 'var(--color-focused)', + neutral: { + DEFAULT: 'var(--color-neutral)', + 3: 'var(--color-neutral-3)', + 5: 'var(--color-neutral-5)', + 10: 'var(--color-neutral-10)', + 15: 'var(--color-neutral-15)', + 20: 'var(--color-neutral-20)', + 25: 'var(--color-neutral-25)', + 100: 'var(--color-neutral-100)', + 125: 'var(--color-neutral-125)', + 150: 'var(--color-neutral-150)', + 200: 'var(--color-neutral-200)', + 300: 'var(--color-neutral-300)', + 400: 'var(--color-neutral-400)', + 500: 'var(--color-neutral-500)', + 525: 'var(--color-neutral-525)', + 550: 'var(--color-neutral-550)', + 600: 'var(--color-neutral-600)', + 700: 'var(--color-neutral-700)', + 725: 'var(--color-neutral-725)', + 750: 'var(--color-neutral-750)', + 800: 'var(--color-neutral-800)', + 900: 'var(--color-neutral-900)', + }, + link: { + underline: 'var(--color-link-underline)', + hover: 'var(--color-link-hover)', + active: 'var(--color-link-active)', + visited: 'var(--color-link-visited)', + }, + 'model-name': { + 'grayscale-link-underline': + 'var(--color-model-name-grayscale-link-underline)', + 'grayscale-link-underline-hover': + 'var(--color-model-name-grayscale-link-hover)', + 'grayscale-catalog': 'var(--color-model-name-grayscale-catalog)', + 'grayscale-schema': 'var(--color-model-name-grayscale-schema)', + 'grayscale-model': 'var(--color-model-name-grayscale-model)', + 'link-underline': 'var(--color-model-name-link-underline)', + 'link-underline-hover': + 'var(--color-model-name-link-underline-hover)', + catalog: 'var(--color-model-name-catalog)', + schema: 'var(--color-model-name-schema)', + model: 'var(--color-model-name-model)', + 'copy-icon': 'var(--color-model-name-copy-icon)', + 'copy-icon-hover': 'var(--color-model-name-copy-icon-hover)', + }, badge: { background: 'var(--color-badge-background)', foreground: 'var(--color-badge-foreground)', }, + button: { + primary: { + background: 'var(--color-button-primary-background)', + foreground: 'var(--color-button-primary-foreground)', + hover: 'var(--color-button-primary-hover)', + active: 'var(--color-button-primary-active)', + }, + secondary: { + background: 'var(--color-button-secondary-background)', + foreground: 'var(--color-button-secondary-foreground)', + hover: 'var(--color-button-secondary-hover)', + active: 'var(--color-button-secondary-active)', + }, + alternative: { + background: 'var(--color-button-alternative-background)', + foreground: 'var(--color-button-alternative-foreground)', + hover: 'var(--color-button-alternative-hover)', + active: 'var(--color-button-alternative-active)', + }, + destructive: { + background: 'var(--color-button-destructive-background)', + foreground: 'var(--color-button-destructive-foreground)', + hover: 'var(--color-button-destructive-hover)', + active: 'var(--color-button-destructive-active)', + }, + danger: { + background: 'var(--color-button-danger-background)', + foreground: 'var(--color-button-danger-foreground)', + hover: 'var(--color-button-danger-hover)', + active: 'var(--color-button-danger-active)', + }, + transparent: { + background: 'var(--color-button-transparent-background)', + foreground: 'var(--color-button-transparent-foreground)', + hover: 'var(--color-button-transparent-hover)', + active: 'var(--color-button-transparent-active)', + }, + }, + tooltip: { + background: 'var(--color-tooltip-background)', + foreground: 'var(--color-tooltip-foreground)', + }, }, borderRadius: { '2xs': 'var(--radius-xs)', @@ -41,5 +131,11 @@ module.exports = { }, }, }, - plugins: [require('@tailwindcss/typography')], + plugins: [ + require('@tailwindcss/typography'), + require('tailwind-scrollbar')({ + nocompatible: true, + preferredStrategy: 'pseudoelements', + }), + ], } From 4e2dd2935f94e81cc49702d52065b2c6de487471 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 29 Aug 2025 10:41:23 -0700 Subject: [PATCH 0793/1056] Fix: Regression in WAP support (#5264) --- sqlmesh/core/engine_adapter/spark.py | 9 + sqlmesh/core/snapshot/evaluator.py | 39 ++-- tests/core/engine_adapter/test_spark.py | 16 ++ tests/core/test_snapshot_evaluator.py | 254 +++++++++++++++++++++++- 4 files changed, 298 insertions(+), 20 deletions(-) diff --git a/sqlmesh/core/engine_adapter/spark.py b/sqlmesh/core/engine_adapter/spark.py index 412e01f5bb..7d6a4d969b 100644 --- a/sqlmesh/core/engine_adapter/spark.py +++ b/sqlmesh/core/engine_adapter/spark.py @@ -402,6 +402,15 @@ def get_current_database(self) -> str: return self.spark.catalog.currentDatabase() return self.fetchone(exp.select(exp.func("current_database")))[0] # type: ignore + def get_data_object(self, target_name: TableName) -> t.Optional[DataObject]: + target_table = exp.to_table(target_name) + if isinstance(target_table.this, exp.Dot) and target_table.this.expression.name.startswith( + f"{self.BRANCH_PREFIX}{self.WAP_PREFIX}" + ): + # Exclude the branch name + target_table.set("this", target_table.this.this) + return super().get_data_object(target_table) + def create_state_table( self, table_name: str, diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 87a6d15c42..c53c0a88db 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -651,7 +651,7 @@ def audit( snapshot.snapshot_id, wap_id, ) - self._wap_publish_snapshot(snapshot, wap_id, deployability_index) + self.wap_publish_snapshot(snapshot, wap_id, deployability_index) return results @@ -806,8 +806,10 @@ def _evaluate_snapshot( } wap_id: t.Optional[str] = None - if snapshot.is_materialized and ( - model.wap_supported or adapter.wap_supported(target_table_name) + if ( + snapshot.is_materialized + and target_table_exists + and (model.wap_supported or adapter.wap_supported(target_table_name)) ): wap_id = random_id()[0:8] logger.info("Using WAP ID '%s' for snapshot %s", wap_id, snapshot.snapshot_id) @@ -823,6 +825,7 @@ def _evaluate_snapshot( create_render_kwargs=create_render_kwargs, rendered_physical_properties=rendered_physical_properties, deployability_index=deployability_index, + target_table_name=target_table_name, is_first_insert=is_first_insert, batch_index=batch_index, ) @@ -896,6 +899,17 @@ def create_snapshot( if on_complete is not None: on_complete(snapshot) + def wap_publish_snapshot( + self, + snapshot: Snapshot, + wap_id: str, + deployability_index: t.Optional[DeployabilityIndex], + ) -> None: + deployability_index = deployability_index or DeployabilityIndex.all_deployable() + table_name = snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot)) + adapter = self.get_adapter(snapshot.model_gateway) + adapter.wap_publish(table_name, wap_id) + def _render_and_insert_snapshot( self, start: TimeLike, @@ -907,6 +921,7 @@ def _render_and_insert_snapshot( create_render_kwargs: t.Dict[str, t.Any], rendered_physical_properties: t.Dict[str, exp.Expression], deployability_index: DeployabilityIndex, + target_table_name: str, is_first_insert: bool, batch_index: int, ) -> None: @@ -916,7 +931,6 @@ def _render_and_insert_snapshot( logger.info("Inserting data for snapshot %s", snapshot.snapshot_id) model = snapshot.model - table_name = snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot)) adapter = self.get_adapter(model.gateway) evaluation_strategy = _evaluation_strategy(snapshot, adapter) @@ -930,7 +944,7 @@ def _render_and_insert_snapshot( def apply(query_or_df: QueryOrDF, index: int = 0) -> None: if index > 0: evaluation_strategy.append( - table_name=table_name, + table_name=target_table_name, query_or_df=query_or_df, model=snapshot.model, snapshot=snapshot, @@ -948,10 +962,10 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: "Inserting batch (%s, %s) into %s'", time_like_to_str(start), time_like_to_str(end), - table_name, + target_table_name, ) evaluation_strategy.insert( - table_name=table_name, + table_name=target_table_name, query_or_df=query_or_df, is_first_insert=is_first_insert, model=snapshot.model, @@ -1278,17 +1292,6 @@ def _cleanup_snapshot( if on_complete is not None: on_complete(table_name) - def _wap_publish_snapshot( - self, - snapshot: Snapshot, - wap_id: str, - deployability_index: t.Optional[DeployabilityIndex], - ) -> None: - deployability_index = deployability_index or DeployabilityIndex.all_deployable() - table_name = snapshot.table_name(is_deployable=deployability_index.is_deployable(snapshot)) - adapter = self.get_adapter(snapshot.model_gateway) - adapter.wap_publish(table_name, wap_id) - def _audit( self, audit: Audit, diff --git a/tests/core/engine_adapter/test_spark.py b/tests/core/engine_adapter/test_spark.py index 2e4f6ae2a0..f1929639a2 100644 --- a/tests/core/engine_adapter/test_spark.py +++ b/tests/core/engine_adapter/test_spark.py @@ -1080,3 +1080,19 @@ def test_table_format(adapter: SparkEngineAdapter, mocker: MockerFixture): "CREATE TABLE IF NOT EXISTS `test_table` (`cola` TIMESTAMP, `colb` STRING, `colc` STRING) USING ICEBERG", "CREATE TABLE IF NOT EXISTS `test_table` USING ICEBERG TBLPROPERTIES ('write.format.default'='orc') AS SELECT CAST(`cola` AS TIMESTAMP) AS `cola`, CAST(`colb` AS STRING) AS `colb`, CAST(`colc` AS STRING) AS `colc` FROM (SELECT CAST(1 AS TIMESTAMP) AS `cola`, CAST(2 AS STRING) AS `colb`, 'foo' AS `colc`) AS `_subquery`", ] + + +def test_get_data_object_wap_branch(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): + adapter = make_mocked_engine_adapter(SparkEngineAdapter, patch_get_data_objects=False) + mocker.patch.object(adapter, "_get_data_objects", return_value=[]) + + table = exp.to_table( + "`catalog`.`sqlmesh__test`.`test__test_view__630979748`.`branch_wap_472234d7`", + dialect="spark", + ) + adapter.get_data_object(table) + + adapter._get_data_objects.assert_called_once_with( + d.schema_("sqlmesh__test", "catalog"), + {"test__test_view__630979748"}, + ) diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 3b72a14f5f..60908ed7c4 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -1,7 +1,7 @@ from __future__ import annotations import typing as t from typing_extensions import Self -from unittest.mock import call, patch +from unittest.mock import call, patch, Mock import re import logging import pytest @@ -2907,7 +2907,7 @@ def test_standalone_audit(mocker: MockerFixture, adapter_mock, make_snapshot): adapter_mock.session.assert_not_called() -def test_audit_wap(adapter_mock, make_snapshot): +def test_audit_wap(adapter_mock: Mock, make_snapshot: t.Callable[..., Snapshot]) -> None: evaluator = SnapshotEvaluator(adapter_mock) custom_audit = ModelAudit( @@ -4331,3 +4331,253 @@ def test_multiple_engine_virtual_layer(snapshot: Snapshot, adapters, make_snapsh "test_schema__test_env.test_model", cascade=False, ) + + +def test_wap_basic( + adapter_mock: Mock, make_snapshot: t.Callable[..., Snapshot], mocker: MockerFixture +) -> None: + evaluator = SnapshotEvaluator(adapter_mock) + + model = SqlModel( + name="test_schema.test_table", + kind=FullKind(), + query=parse_one("SELECT a::int FROM tbl"), + ) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + adapter_mock.wap_supported.return_value = True + + expected_wap_table = "test_schema.test_table.branch_wap_12345678" + adapter_mock.wap_prepare.return_value = expected_wap_table + + wap_id = evaluator.evaluate( + snapshot, + start="2020-01-01", + end="2020-01-01", + execution_time="2020-01-01", + snapshots={}, + target_table_exists=True, # Use parameter to control table existence + ) + + assert wap_id is not None + assert len(wap_id) == 8 + + expected_table_name = snapshot.table_name() + adapter_mock.wap_prepare.assert_called_once_with(expected_table_name, wap_id) + adapter_mock.replace_query.assert_called_once_with( + expected_wap_table, + mocker.ANY, + table_format=mocker.ANY, + storage_format=mocker.ANY, + partitioned_by=mocker.ANY, + partition_interval_unit=mocker.ANY, + clustered_by=mocker.ANY, + table_properties=mocker.ANY, + table_description=mocker.ANY, + column_descriptions=mocker.ANY, + target_columns_to_types=mocker.ANY, + source_columns=mocker.ANY, + ) + + +def test_wap_model_wap_supported( + adapter_mock: Mock, make_snapshot: t.Callable[..., Snapshot], mocker: MockerFixture +) -> None: + evaluator = SnapshotEvaluator(adapter_mock) + + model = SqlModel( + name="test_schema.test_table", + kind=FullKind(), + query=parse_one("SELECT a::int FROM tbl"), + storage_format="iceberg", # Model supports WAP via iceberg format + ) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + adapter_mock.wap_supported.return_value = False + + expected_wap_table = "test_schema.test_table.branch_wap_12345678" + adapter_mock.wap_prepare.return_value = expected_wap_table + + wap_id = evaluator.evaluate( + snapshot, + start="2020-01-01", + end="2020-01-01", + execution_time="2020-01-01", + snapshots={}, + target_table_exists=True, # Use parameter to control table existence + ) + assert wap_id is not None + + expected_table_name = snapshot.table_name() + adapter_mock.wap_prepare.assert_called_once_with(expected_table_name, wap_id) + adapter_mock.replace_query.assert_called_once_with( + expected_wap_table, + mocker.ANY, + table_format=mocker.ANY, + storage_format=mocker.ANY, + partitioned_by=mocker.ANY, + partition_interval_unit=mocker.ANY, + clustered_by=mocker.ANY, + table_properties=mocker.ANY, + table_description=mocker.ANY, + column_descriptions=mocker.ANY, + target_columns_to_types=mocker.ANY, + source_columns=mocker.ANY, + ) + + +def test_wap_no_wap_support(adapter_mock: Mock, make_snapshot: t.Callable[..., Snapshot]) -> None: + evaluator = SnapshotEvaluator(adapter_mock) + + model = SqlModel( + name="test_schema.test_table", + kind=FullKind(), + query=parse_one("SELECT a::int FROM tbl"), + ) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + adapter_mock.wap_supported.return_value = False + + wap_id = evaluator.evaluate( + snapshot, + start="2020-01-01", + end="2020-01-01", + execution_time="2020-01-01", + snapshots={}, + target_table_exists=True, + ) + + assert wap_id is None + adapter_mock.wap_prepare.assert_not_called() + + +def test_wap_non_materialized_snapshot( + adapter_mock: Mock, make_snapshot: t.Callable[..., Snapshot] +) -> None: + evaluator = SnapshotEvaluator(adapter_mock) + + model = SqlModel( + name="test_schema.test_table", + kind=ViewKind(), # View kind is not materialized + query=parse_one("SELECT a::int FROM tbl"), + ) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + adapter_mock.wap_supported.return_value = True + + wap_id = evaluator.evaluate( + snapshot, start="2020-01-01", end="2020-01-01", execution_time="2020-01-01", snapshots={} + ) + + assert wap_id is None + adapter_mock.wap_prepare.assert_not_called() + + +def test_wap_publish_snapshot(adapter_mock: Mock, make_snapshot: t.Callable[..., Snapshot]) -> None: + evaluator = SnapshotEvaluator(adapter_mock) + + model = SqlModel( + name="test_schema.test_table", + kind=FullKind(), + query=parse_one("SELECT a::int FROM tbl"), + ) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + wap_id = "test_wap_id" + deployability_index = DeployabilityIndex.all_deployable() + + evaluator.wap_publish_snapshot(snapshot, wap_id, deployability_index) + + expected_table_name = snapshot.table_name(is_deployable=True) + adapter_mock.wap_publish.assert_called_once_with(expected_table_name, wap_id) + + +def test_wap_during_audit(adapter_mock: Mock, make_snapshot: t.Callable[..., Snapshot]) -> None: + evaluator = SnapshotEvaluator(adapter_mock) + + custom_audit = ModelAudit( + name="custom_audit", + query="SELECT * FROM test_schema.test_table WHERE invalid_condition", + ) + + model = SqlModel( + name="test_schema.test_table", + kind=FullKind(), + query=parse_one("SELECT a::int FROM tbl"), + audits=[ + ("not_null", {"columns": exp.to_column("a")}), + ("custom_audit", {}), + ], + audit_definitions={custom_audit.name: custom_audit}, + ) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + wap_id = "test_wap_id" + expected_wap_table_name = f"test_schema.test_table.branch_wap_{wap_id}" + adapter_mock.wap_table_name.return_value = expected_wap_table_name + adapter_mock.fetchone.return_value = (0,) + + results = evaluator.audit(snapshot, snapshots={}, wap_id=wap_id) + + assert len(results) == 2 + + adapter_mock.wap_table_name.assert_called_once_with(snapshot.table_name(), wap_id) + adapter_mock.wap_publish.assert_called_once_with(snapshot.table_name(), wap_id) + + +def test_wap_prepare_failure(adapter_mock: Mock, make_snapshot: t.Callable[..., Snapshot]) -> None: + evaluator = SnapshotEvaluator(adapter_mock) + + model = SqlModel( + name="test_schema.test_table", + kind=FullKind(), + query=parse_one("SELECT a::int FROM tbl"), + ) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + adapter_mock.wap_supported.return_value = True + + adapter_mock.wap_prepare.side_effect = Exception("WAP prepare failed") + + with pytest.raises(Exception, match="WAP prepare failed"): + evaluator.evaluate( + snapshot, + start="2020-01-01", + end="2020-01-01", + execution_time="2020-01-01", + snapshots={}, + target_table_exists=True, + ) + + +def test_wap_publish_failure(adapter_mock: Mock, make_snapshot: t.Callable[..., Snapshot]) -> None: + """Test error handling when WAP publish fails.""" + evaluator = SnapshotEvaluator(adapter_mock) + + model = SqlModel( + name="test_schema.test_table", + kind=FullKind(), + query=parse_one("SELECT a::int FROM tbl"), + audits=[("not_null", {"columns": exp.to_column("a")})], + ) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + wap_id = "test_wap_id" + expected_wap_table_name = f"test_schema.test_table.branch_wap_{wap_id}" + adapter_mock.wap_table_name.return_value = expected_wap_table_name + adapter_mock.fetchone.return_value = (0,) + + # Mock WAP publish to raise an exception + adapter_mock.wap_publish.side_effect = Exception("WAP publish failed") + + # Execute audit with WAP ID and expect it to raise the exception + with pytest.raises(Exception, match="WAP publish failed"): + evaluator.audit(snapshot, snapshots={}, wap_id=wap_id) From 61a65ac9aef7505547354d2b22e40bf70a878013 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 29 Aug 2025 14:47:14 -0700 Subject: [PATCH 0794/1056] Chore: Allow duplicate keys in dbt project yaml files (#5254) --- sqlmesh/dbt/common.py | 4 +++- sqlmesh/utils/yaml.py | 21 +++++++++++++++++++++ tests/utils/test_yaml.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/common.py b/sqlmesh/dbt/common.py index ec928576ed..c74fd933da 100644 --- a/sqlmesh/dbt/common.py +++ b/sqlmesh/dbt/common.py @@ -36,7 +36,9 @@ def load_yaml(source: str | Path) -> t.Dict: try: - return load(source, render_jinja=False) + return load( + source, render_jinja=False, allow_duplicate_keys=True, keep_last_duplicate_key=True + ) except DuplicateKeyError as ex: raise ConfigError(f"{source}: {ex}" if isinstance(source, Path) else f"{ex}") diff --git a/sqlmesh/utils/yaml.py b/sqlmesh/utils/yaml.py index 0eb18d8188..d72e9d49e5 100644 --- a/sqlmesh/utils/yaml.py +++ b/sqlmesh/utils/yaml.py @@ -8,6 +8,7 @@ from pathlib import Path from ruamel import yaml +from ruamel.yaml.constructor import SafeConstructor from sqlmesh.core.constants import VAR from sqlmesh.utils.errors import SQLMeshError @@ -32,12 +33,30 @@ def YAML(typ: t.Optional[str] = "safe") -> yaml.YAML: return yaml_obj +class SafeConstructorOverride(SafeConstructor): + def check_mapping_key( + self, + node: t.Any, + key_node: t.Any, + mapping: t.Any, + key: t.Any, + value: t.Any, + ) -> bool: + """This function normally returns True if key is unique. + + It is only used by the construct_mapping function. By always returning True, + keys will always be updated and so the last value will be kept for mappings. + """ + return True + + def load( source: str | Path, raise_if_empty: bool = True, render_jinja: bool = True, allow_duplicate_keys: bool = False, variables: t.Optional[t.Dict[str, t.Any]] = None, + keep_last_duplicate_key: bool = False, ) -> t.Dict: """Loads a YAML object from either a raw string or a file.""" path: t.Optional[Path] = None @@ -56,6 +75,8 @@ def load( ) yaml = YAML() + if allow_duplicate_keys and keep_last_duplicate_key: + yaml.Constructor = SafeConstructorOverride yaml.allow_duplicate_keys = allow_duplicate_keys contents = yaml.load(source) if contents is None: diff --git a/tests/utils/test_yaml.py b/tests/utils/test_yaml.py index f2734576b6..5a2e04e5be 100644 --- a/tests/utils/test_yaml.py +++ b/tests/utils/test_yaml.py @@ -45,3 +45,37 @@ def test_yaml() -> None: decimal_value = Decimal(123.45) assert yaml.load(yaml.dump(decimal_value)) == str(decimal_value) + + +def test_load_keep_last_duplicate_key() -> None: + input_str = """ +name: first_name +name: second_name +name: third_name + +foo: bar + +mapping: + key: first_value + key: second_value + key: third_value + +sequence: + - one + - two +""" + # Default behavior of ruamel is to keep the first key encountered + assert yaml.load(input_str, allow_duplicate_keys=True) == { + "name": "first_name", + "foo": "bar", + "mapping": {"key": "first_value"}, + "sequence": ["one", "two"], + } + + # Test keeping last key + assert yaml.load(input_str, allow_duplicate_keys=True, keep_last_duplicate_key=True) == { + "name": "third_name", + "foo": "bar", + "mapping": {"key": "third_value"}, + "sequence": ["one", "two"], + } From 43349da8519c48ee93a24e825c7ff86def7257e7 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 1 Sep 2025 07:57:18 -0700 Subject: [PATCH 0795/1056] fix: ignore extra keys in node config (#5265) --- sqlmesh/dbt/manifest.py | 18 +++++++++++------- tests/dbt/test_model.py | 3 ++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index edb9004d6f..ca20554e3b 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -360,16 +360,20 @@ def _load_models_and_seeds(self) -> None: ) self._models_per_package[node.package_name][node_name] = ModelConfig( - sql=sql, - dependencies=dependencies, - tests=tests, - **node_config, + **dict( + node_config, + sql=sql, + dependencies=dependencies, + tests=tests, + ) ) else: self._seeds_per_package[node.package_name][node_name] = SeedConfig( - dependencies=Dependencies(macros=macro_references), - tests=tests, - **node_config, + **dict( + node_config, + dependencies=Dependencies(macros=macro_references), + tests=tests, + ) ) def _load_on_run_start_end(self) -> None: diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index 00361cf8b8..e33c41e68c 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -75,7 +75,8 @@ def test_load_invalid_ref_audit_constraints( dbt_project_dir.mkdir() dbt_model_dir = dbt_project_dir / "models" dbt_model_dir.mkdir() - full_model_contents = "SELECT 1 as cola" + # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it + full_model_contents = """{{ config(tags=["blah"], tests=[{"blah": {"to": "ref('completely_ignored')", "field": "blah2"} }]) }} SELECT 1 as cola""" full_model_file = dbt_model_dir / "full_model.sql" with open(full_model_file, "w", encoding="utf-8") as f: f.write(full_model_contents) From da49d5785786584c7e7b5329911f80ce0b514227 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 1 Sep 2025 08:23:52 -0700 Subject: [PATCH 0796/1056] Fix: Evaluation of metadata snapshots with audit changes (#5267) --- sqlmesh/core/plan/stages.py | 26 +++++++++++++++++--------- sqlmesh/core/scheduler.py | 3 +++ tests/core/test_integration.py | 25 +++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 82223dd807..91c8c6ff14 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -574,23 +574,31 @@ def _get_audit_only_snapshots( ) -> t.Dict[SnapshotId, Snapshot]: metadata_snapshots = [] for snapshot in new_snapshots.values(): - if not snapshot.is_metadata or not snapshot.is_model or not snapshot.evaluatable: + if ( + not snapshot.is_metadata + or not snapshot.is_model + or not snapshot.evaluatable + or not snapshot.previous_version + ): continue metadata_snapshots.append(snapshot) # Bulk load all the previous snapshots - previous_snapshots = self.state_reader.get_snapshots( - [ - s.previous_version.snapshot_id(s.name) - for s in metadata_snapshots - if s.previous_version - ] - ).values() + previous_snapshot_ids = [ + s.previous_version.snapshot_id(s.name) for s in metadata_snapshots if s.previous_version + ] + previous_snapshots = { + s.name: s for s in self.state_reader.get_snapshots(previous_snapshot_ids).values() + } # Check if any of the snapshots have modifications to the audits field by comparing the hashes audit_snapshots = {} - for snapshot, previous_snapshot in zip(metadata_snapshots, previous_snapshots): + for snapshot in metadata_snapshots: + if snapshot.name not in previous_snapshots: + continue + + previous_snapshot = previous_snapshots[snapshot.name] new_audits_hash = snapshot.model.audit_metadata_hash() previous_audit_hash = previous_snapshot.model.audit_metadata_hash() diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index 210aff230d..dc6499c1a3 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -626,6 +626,9 @@ def _dag( dag = DAG[SchedulingUnit]() for snapshot_id in snapshot_dag: + if snapshot_id.name not in self.snapshots_by_name: + continue + snapshot = self.snapshots_by_name[snapshot_id.name] intervals = intervals_per_snapshot.get(snapshot.name, []) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 2781909c88..0e779481fd 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -6294,6 +6294,31 @@ def test_restatement_shouldnt_backfill_beyond_prod_intervals(init_and_plan_conte ].intervals[-1][1] == to_timestamp("2023-01-08 00:00:00 UTC") +@time_machine.travel("2023-01-08 15:00:00 UTC") +@use_terminal_console +def test_audit_only_metadata_change(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Add a new audit + model = context.get_model("sushi.waiter_revenue_by_day") + audits = model.audits.copy() + audits.append(("number_of_rows", {"threshold": exp.Literal.number(1)})) + model = model.copy(update={"audits": audits}) + context.upsert_model(model) + + plan = context.plan_builder("prod", skip_tests=True).build() + assert len(plan.new_snapshots) == 2 + assert all(s.change_category.is_metadata for s in plan.new_snapshots) + assert not plan.missing_intervals + + with capture_output() as output: + context.apply(plan) + + assert "Auditing models" in output.stdout + assert model.name in output.stdout + + def initial_add(context: Context, environment: str): assert not context.state_reader.get_environment(environment) From 2eb39a4d89651f5ca92ec5d65abfd2e745ad111c Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 1 Sep 2025 18:46:01 +0300 Subject: [PATCH 0797/1056] Fix: allow dbt models to be empty (#5270) --- sqlmesh/dbt/loader.py | 5 +++++ tests/fixtures/dbt/sushi_test/models/empty_model.sql | 0 2 files changed, 5 insertions(+) create mode 100644 tests/fixtures/dbt/sushi_test/models/empty_model.sql diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 8fd7926ad5..4f473a20ab 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -19,6 +19,7 @@ from sqlmesh.dbt.basemodel import BMC, BaseModelConfig from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext +from sqlmesh.dbt.model import ModelConfig from sqlmesh.dbt.profile import Profile from sqlmesh.dbt.project import Project from sqlmesh.dbt.target import TargetConfig @@ -137,6 +138,10 @@ def _to_sqlmesh(config: BMC, context: DbtContext) -> Model: package_models: t.Dict[str, BaseModelConfig] = {**package.models, **package.seeds} for model in package_models.values(): + if isinstance(model, ModelConfig) and not model.sql_no_config: + logger.info(f"Skipping empty model '{model.name}' at path '{model.path}'.") + continue + sqlmesh_model = cache.get_or_load_models( model.path, loader=lambda: [_to_sqlmesh(model, package_context)] )[0] diff --git a/tests/fixtures/dbt/sushi_test/models/empty_model.sql b/tests/fixtures/dbt/sushi_test/models/empty_model.sql new file mode 100644 index 0000000000..e69de29bb2 From 37523dc169a73577432cba11d9f7ca22b61bd596 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:01:10 +0300 Subject: [PATCH 0798/1056] chore: adding an analysis to dbt tests (#5276) --- .../analyses/waiter_performance_analysis.sql | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/fixtures/dbt/sushi_test/analyses/waiter_performance_analysis.sql diff --git a/tests/fixtures/dbt/sushi_test/analyses/waiter_performance_analysis.sql b/tests/fixtures/dbt/sushi_test/analyses/waiter_performance_analysis.sql new file mode 100644 index 0000000000..06d8bdb8fd --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/analyses/waiter_performance_analysis.sql @@ -0,0 +1,10 @@ +-- Simple analysis: Top performing waiters by total revenue + +SELECT + waiter_id, + SUM(revenue) AS total_revenue, + COUNT(*) AS days_worked +FROM {{ ref('waiter_revenue_by_day') }} +GROUP BY waiter_id +ORDER BY total_revenue DESC +LIMIT 10 \ No newline at end of file From 70c8deb485751a5a358e031f57247c2b6d4edce4 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:50:27 -0700 Subject: [PATCH 0799/1056] chore: add dbt microbatch interface (#5272) --- sqlmesh/dbt/common.py | 8 +- sqlmesh/dbt/model.py | 130 +++++++++++++++++++------ tests/dbt/test_model.py | 209 ++++++++++++++++++++++++++++++++-------- 3 files changed, 278 insertions(+), 69 deletions(-) diff --git a/sqlmesh/dbt/common.py b/sqlmesh/dbt/common.py index c74fd933da..ba982c2bb2 100644 --- a/sqlmesh/dbt/common.py +++ b/sqlmesh/dbt/common.py @@ -132,6 +132,10 @@ def _validate_meta(cls, v: t.Dict[str, t.Union[str, t.Any]]) -> t.Dict[str, t.An def config_attribute_dict(self) -> AttributeDict[str, t.Any]: return AttributeDict(self.dict(exclude=EXCLUDED_CONFIG_ATTRIBUTE_KEYS)) + def _get_field_value(self, field: str) -> t.Optional[t.Any]: + field_val = getattr(self, field, None) + return field_val if field_val is not None else self.meta.get(field, None) + def replace(self, other: T) -> None: """ Replace the contents of this instance with the passed in instance. @@ -152,9 +156,7 @@ def sqlmesh_config_kwargs(self) -> t.Dict[str, t.Any]: """ kwargs = {} for field in self.sqlmesh_config_fields: - field_val = getattr(self, field, None) - if field_val is None: - field_val = self.meta.get(field, None) + field_val = self._get_field_value(field) if field_val is not None: kwargs[field] = field_val return kwargs diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index d2d1a52abc..080900eace 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -1,5 +1,6 @@ from __future__ import annotations +import datetime import typing as t from sqlglot import exp @@ -34,7 +35,7 @@ from sqlmesh.dbt.context import DbtContext -INCREMENTAL_BY_TIME_STRATEGIES = set(["delete+insert", "insert_overwrite"]) +INCREMENTAL_BY_TIME_STRATEGIES = set(["delete+insert", "insert_overwrite", "microbatch"]) INCREMENTAL_BY_UNIQUE_KEY_STRATEGIES = set(["merge"]) @@ -73,9 +74,7 @@ class ModelConfig(BaseModelConfig): time_column: t.Optional[str] = None cron: t.Optional[str] = None interval_unit: t.Optional[str] = None - batch_size: t.Optional[int] = None batch_concurrency: t.Optional[int] = None - lookback: t.Optional[int] = None forward_only: bool = True disable_restatement: t.Optional[bool] = None allow_partials: t.Optional[bool] = None @@ -100,6 +99,15 @@ class ModelConfig(BaseModelConfig): target_schema: t.Optional[str] = None check_cols: t.Optional[t.Union[t.List[str], str]] = None + # Microbatch Fields + event_time: t.Optional[str] = None + begin: t.Optional[datetime.datetime] = None + concurrent_batches: t.Optional[bool] = None + + # Shared SQLMesh and DBT configuration fields + batch_size: t.Optional[t.Union[int, str]] = None + lookback: t.Optional[int] = None + # redshift bind: t.Optional[bool] = None @@ -220,6 +228,17 @@ def snapshot_strategy(self) -> t.Optional[SnapshotStrategy]: def table_schema(self) -> str: return self.target_schema or super().table_schema + def _get_overlapping_field_value( + self, context: DbtContext, dbt_field_name: str, sqlmesh_field_name: str + ) -> t.Optional[t.Any]: + dbt_field = self._get_field_value(dbt_field_name) + sqlmesh_field = getattr(self, sqlmesh_field_name, None) + if dbt_field is not None and sqlmesh_field is not None: + get_console().log_warning( + f"Both '{dbt_field_name}' and '{sqlmesh_field_name}' are set for model '{self.canonical_name(context)}'. '{sqlmesh_field_name}' will be used." + ) + return sqlmesh_field if sqlmesh_field is not None else dbt_field + def model_kind(self, context: DbtContext) -> ModelKind: """ Get the sqlmesh ModelKind @@ -256,12 +275,9 @@ def model_kind(self, context: DbtContext) -> ModelKind: incremental_kind_kwargs["on_destructive_change"] = on_destructive_change incremental_kind_kwargs["on_additive_change"] = on_additive_change - for field in ("forward_only", "auto_restatement_cron"): - field_val = getattr(self, field, None) - if field_val is None: - field_val = self.meta.get(field, None) - if field_val is not None: - incremental_kind_kwargs[field] = field_val + auto_restatement_cron_value = self._get_field_value("auto_restatement_cron") + if auto_restatement_cron_value is not None: + incremental_kind_kwargs["auto_restatement_cron"] = auto_restatement_cron_value if materialization == Materialization.TABLE: return FullKind() @@ -269,14 +285,34 @@ def model_kind(self, context: DbtContext) -> ModelKind: return ViewKind() if materialization == Materialization.INCREMENTAL: incremental_by_kind_kwargs: t.Dict[str, t.Any] = {"dialect": self.dialect(context)} + forward_only_value = self._get_field_value("forward_only") + if forward_only_value is not None: + incremental_kind_kwargs["forward_only"] = forward_only_value + + is_incremental_by_time_range = self.time_column or ( + self.incremental_strategy and self.incremental_strategy == "microbatch" + ) + # Get shared incremental by kwargs for field in ("batch_size", "batch_concurrency", "lookback"): - field_val = getattr(self, field, None) - if field_val is None: - field_val = self.meta.get(field, None) + field_val = self._get_field_value(field) if field_val is not None: + # Check if `batch_size` is representing an interval unit and if so that will be handled at the model level + if field == "batch_size" and isinstance(field_val, str): + continue incremental_by_kind_kwargs[field] = field_val - if self.time_column: + disable_restatement = self.disable_restatement + if disable_restatement is None: + if is_incremental_by_time_range: + disable_restatement = False + else: + disable_restatement = ( + not self.full_refresh if self.full_refresh is not None else False + ) + incremental_by_kind_kwargs["disable_restatement"] = disable_restatement + + # Incremental by time range which includes microbatch + if is_incremental_by_time_range: strategy = self.incremental_strategy or target.default_incremental_strategy( IncrementalByTimeRangeKind ) @@ -287,22 +323,37 @@ def model_kind(self, context: DbtContext) -> ModelKind: f"Supported strategies include {collection_to_str(INCREMENTAL_BY_TIME_STRATEGIES)}." ) + if strategy == "microbatch": + time_column = self._get_overlapping_field_value( + context, "event_time", "time_column" + ) + if not time_column: + raise ConfigError( + f"{self.canonical_name(context)}: 'event_time' is required for microbatch incremental strategy." + ) + concurrent_batches = self._get_field_value("concurrent_batches") + if concurrent_batches is True: + if incremental_by_kind_kwargs.get("batch_size"): + get_console().log_warning( + f"'concurrent_batches' is set to True and 'batch_size' are defined in '{self.canonical_name(context)}'. The batch size will be set to the value of `batch_size`." + ) + incremental_by_kind_kwargs["batch_size"] = incremental_by_kind_kwargs.get( + "batch_size", 1 + ) + else: + if not self.time_column: + raise ConfigError( + f"{self.canonical_name(context)}: 'time_column' is required for incremental by time range models not defined using microbatch." + ) + time_column = self.time_column + return IncrementalByTimeRangeKind( - time_column=self.time_column, - disable_restatement=( - self.disable_restatement if self.disable_restatement is not None else False - ), + time_column=time_column, auto_restatement_intervals=self.auto_restatement_intervals, **incremental_kind_kwargs, **incremental_by_kind_kwargs, ) - disable_restatement = self.disable_restatement - if disable_restatement is None: - disable_restatement = ( - not self.full_refresh if self.full_refresh is not None else False - ) - if self.unique_key: strategy = self.incremental_strategy or target.default_incremental_strategy( IncrementalByUniqueKeyKind @@ -315,11 +366,11 @@ def model_kind(self, context: DbtContext) -> ModelKind: f"Unique key is not compatible with '{strategy}' incremental strategy in model '{self.canonical_name(context)}'. " f"Supported strategies include {collection_to_str(INCREMENTAL_BY_UNIQUE_KEY_STRATEGIES)}. Falling back to 'merge' strategy." ) - strategy = "merge" + merge_filter = None if self.incremental_predicates: dialect = self.dialect(context) - incremental_kind_kwargs["merge_filter"] = exp.and_( + merge_filter = exp.and_( *[ d.parse_one(predicate, dialect=dialect) for predicate in self.incremental_predicates @@ -329,7 +380,7 @@ def model_kind(self, context: DbtContext) -> ModelKind: return IncrementalByUniqueKeyKind( unique_key=self.unique_key, - disable_restatement=disable_restatement, + merge_filter=merge_filter, **incremental_kind_kwargs, **incremental_by_kind_kwargs, ) @@ -339,7 +390,7 @@ def model_kind(self, context: DbtContext) -> ModelKind: ) return IncrementalUnmanagedKind( insert_overwrite=strategy in INCREMENTAL_BY_TIME_STRATEGIES, - disable_restatement=disable_restatement, + disable_restatement=incremental_by_kind_kwargs["disable_restatement"], **incremental_kind_kwargs, ) if materialization == Materialization.EPHEMERAL: @@ -438,6 +489,9 @@ def sqlmesh_config_fields(self) -> t.Set[str]: "interval_unit", "allow_partials", "physical_version", + "start", + # In microbatch models `begin` is the same as `start` + "begin", } def to_sqlmesh( @@ -583,12 +637,32 @@ def to_sqlmesh( # Set allow_partials to True for dbt models to preserve the original semantics. allow_partials = True + if kind.is_incremental: + if self.batch_size and isinstance(self.batch_size, str): + if "interval_unit" in model_kwargs: + get_console().log_warning( + f"Both 'interval_unit' and 'batch_size' are set for model '{self.canonical_name(context)}'. 'interval_unit' will be used." + ) + else: + model_kwargs["interval_unit"] = self.batch_size + self.batch_size = None + if begin := model_kwargs.pop("begin", None): + if "start" in model_kwargs: + get_console().log_warning( + f"Both 'begin' and 'start' are set for model '{self.canonical_name(context)}'. 'start' will be used." + ) + else: + model_kwargs["start"] = begin + + model_kwargs["start"] = model_kwargs.get( + "start", context.sqlmesh_config.model_defaults.start + ) + model = create_sql_model( self.canonical_name(context), query, dialect=model_dialect, kind=kind, - start=self.start or context.sqlmesh_config.model_defaults.start, audit_definitions=audit_definitions, # This ensures that we bypass query rendering that would otherwise be required to extract additional # dependencies from the model's SQL. diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index e33c41e68c..7d4672c512 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -1,8 +1,12 @@ +import datetime +import typing as t import pytest from pathlib import Path +from sqlglot import exp from sqlmesh import Context +from sqlmesh.core.model import TimeColumn, IncrementalByTimeRangeKind from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.model import ModelConfig @@ -13,6 +17,49 @@ pytestmark = pytest.mark.dbt +@pytest.fixture +def create_empty_project(tmp_path: Path) -> t.Callable[[], t.Tuple[Path, Path]]: + def _create_empty_project() -> t.Tuple[Path, Path]: + yaml = YAML() + dbt_project_dir = tmp_path / "dbt" + dbt_project_dir.mkdir() + dbt_model_dir = dbt_project_dir / "models" + dbt_model_dir.mkdir() + dbt_project_config = { + "name": "empty_project", + "version": "1.0.0", + "config-version": 2, + "profile": "test", + "model-paths": ["models"], + } + dbt_project_file = dbt_project_dir / "dbt_project.yml" + with open(dbt_project_file, "w", encoding="utf-8") as f: + YAML().dump(dbt_project_config, f) + sqlmesh_config = { + "model_defaults": { + "start": "2025-01-01", + } + } + sqlmesh_config_file = dbt_project_dir / "sqlmesh.yaml" + with open(sqlmesh_config_file, "w", encoding="utf-8") as f: + YAML().dump(sqlmesh_config, f) + dbt_data_dir = tmp_path / "dbt_data" + dbt_data_dir.mkdir() + dbt_data_file = dbt_data_dir / "local.db" + dbt_profile_config = { + "test": { + "outputs": {"duckdb": {"type": "duckdb", "path": str(dbt_data_file)}}, + "target": "duckdb", + } + } + db_profile_file = dbt_project_dir / "profiles.yml" + with open(db_profile_file, "w", encoding="utf-8") as f: + yaml.dump(dbt_profile_config, f) + return dbt_project_dir, dbt_model_dir + + return _create_empty_project + + def test_model_test_circular_references() -> None: upstream_model = ModelConfig(name="upstream") downstream_model = ModelConfig(name="downstream", dependencies=Dependencies(refs={"upstream"})) @@ -68,16 +115,13 @@ def test_model_test_circular_references() -> None: @pytest.mark.slow def test_load_invalid_ref_audit_constraints( - tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig + tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project ) -> None: yaml = YAML() - dbt_project_dir = tmp_path / "dbt" - dbt_project_dir.mkdir() - dbt_model_dir = dbt_project_dir / "models" - dbt_model_dir.mkdir() + project_dir, model_dir = create_empty_project() # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it full_model_contents = """{{ config(tags=["blah"], tests=[{"blah": {"to": "ref('completely_ignored')", "field": "blah2"} }]) }} SELECT 1 as cola""" - full_model_file = dbt_model_dir / "full_model.sql" + full_model_file = model_dir / "full_model.sql" with open(full_model_file, "w", encoding="utf-8") as f: f.write(full_model_contents) model_schema = { @@ -118,41 +162,11 @@ def test_load_invalid_ref_audit_constraints( } ], } - model_schema_file = dbt_model_dir / "schema.yml" + model_schema_file = model_dir / "schema.yml" with open(model_schema_file, "w", encoding="utf-8") as f: yaml.dump(model_schema, f) - dbt_project_config = { - "name": "invalid_ref_audit_constraints", - "version": "1.0.0", - "config-version": 2, - "profile": "test", - "model-paths": ["models"], - } - dbt_project_file = dbt_project_dir / "dbt_project.yml" - with open(dbt_project_file, "w", encoding="utf-8") as f: - yaml.dump(dbt_project_config, f) - sqlmesh_config = { - "model_defaults": { - "start": "2025-01-01", - } - } - sqlmesh_config_file = dbt_project_dir / "sqlmesh.yaml" - with open(sqlmesh_config_file, "w", encoding="utf-8") as f: - yaml.dump(sqlmesh_config, f) - dbt_data_dir = tmp_path / "dbt_data" - dbt_data_dir.mkdir() - dbt_data_file = dbt_data_dir / "local.db" - dbt_profile_config = { - "test": { - "outputs": {"duckdb": {"type": "duckdb", "path": str(dbt_data_file)}}, - "target": "duckdb", - } - } - db_profile_file = dbt_project_dir / "profiles.yml" - with open(db_profile_file, "w", encoding="utf-8") as f: - yaml.dump(dbt_profile_config, f) - context = Context(paths=dbt_project_dir) + context = Context(paths=project_dir) assert ( "Skipping audit 'relationships_full_model_cola__cola__ref_not_real_model_' because model 'not_real_model' is not a valid ref" in caplog.text @@ -165,3 +179,122 @@ def test_load_invalid_ref_audit_constraints( assert fqn in context.snapshots # The audit isn't loaded due to the invalid ref assert context.snapshots[fqn].model.audits == [] + + +@pytest.mark.slow +def test_load_microbatch_all_defined( + tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project +) -> None: + project_dir, model_dir = create_empty_project() + # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it + microbatch_contents = """ + {{ + config( + materialized='incremental', + incremental_strategy='microbatch', + event_time='ds', + begin='2020-01-01', + batch_size='day', + lookback=2, + concurrent_batches=true + ) + }} + + SELECT 1 as cola, '2025-01-01' as ds + """ + microbatch_model_file = model_dir / "microbatch.sql" + with open(microbatch_model_file, "w", encoding="utf-8") as f: + f.write(microbatch_contents) + + snapshot_fqn = '"local"."main"."microbatch"' + context = Context(paths=project_dir) + model = context.snapshots[snapshot_fqn].model + # Validate model-level attributes + assert model.start == datetime.datetime(2020, 1, 1, 0, 0) + assert model.interval_unit.is_day + # Validate model kind attributes + assert isinstance(model.kind, IncrementalByTimeRangeKind) + assert model.kind.lookback == 2 + assert model.kind.time_column == TimeColumn( + column=exp.to_column("ds", quoted=True), format="%Y-%m-%d" + ) + assert model.kind.batch_size == 1 + + +@pytest.mark.slow +def test_load_microbatch_all_defined_diff_values( + tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project +) -> None: + project_dir, model_dir = create_empty_project() + # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it + microbatch_contents = """ + {{ + config( + materialized='incremental', + incremental_strategy='microbatch', + cron='@yearly', + event_time='blah', + begin='2022-01-01', + batch_size='year', + lookback=20, + concurrent_batches=false + ) + }} + + SELECT 1 as cola, '2022-01-01' as blah + """ + microbatch_model_file = model_dir / "microbatch.sql" + with open(microbatch_model_file, "w", encoding="utf-8") as f: + f.write(microbatch_contents) + + snapshot_fqn = '"local"."main"."microbatch"' + context = Context(paths=project_dir) + model = context.snapshots[snapshot_fqn].model + # Validate model-level attributes + assert model.start == datetime.datetime(2022, 1, 1, 0, 0) + assert model.interval_unit.is_year + # Validate model kind attributes + assert isinstance(model.kind, IncrementalByTimeRangeKind) + assert model.kind.lookback == 20 + assert model.kind.time_column == TimeColumn( + column=exp.to_column("blah", quoted=True), format="%Y-%m-%d" + ) + assert model.kind.batch_size is None + + +@pytest.mark.slow +def test_load_microbatch_required_only( + tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project +) -> None: + project_dir, model_dir = create_empty_project() + # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it + microbatch_contents = """ + {{ + config( + materialized='incremental', + incremental_strategy='microbatch', + begin='2021-01-01', + event_time='ds', + batch_size='hour', + ) + }} + + SELECT 1 as cola, '2021-01-01' as ds + """ + microbatch_model_file = model_dir / "microbatch.sql" + with open(microbatch_model_file, "w", encoding="utf-8") as f: + f.write(microbatch_contents) + + snapshot_fqn = '"local"."main"."microbatch"' + context = Context(paths=project_dir) + model = context.snapshots[snapshot_fqn].model + # Validate model-level attributes + assert model.start == datetime.datetime(2021, 1, 1, 0, 0) + assert model.interval_unit.is_hour + # Validate model kind attributes + assert isinstance(model.kind, IncrementalByTimeRangeKind) + assert model.kind.lookback == 1 + assert model.kind.time_column == TimeColumn( + column=exp.to_column("ds", quoted=True), format="%Y-%m-%d" + ) + assert model.kind.batch_size is None From 454e942fedd7fa768e10024515b1f1b134acb3d9 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:12:29 -0700 Subject: [PATCH 0800/1056] feat: trino support replace table iceberg and delta (#5279) --- sqlmesh/core/engine_adapter/athena.py | 1 + sqlmesh/core/engine_adapter/base.py | 18 +++++++- sqlmesh/core/engine_adapter/mixins.py | 2 +- sqlmesh/core/engine_adapter/redshift.py | 1 + sqlmesh/core/engine_adapter/trino.py | 41 ++++++++++++++++-- tests/core/engine_adapter/test_trino.py | 55 +++++++++++++++++++++---- 6 files changed, 104 insertions(+), 14 deletions(-) diff --git a/sqlmesh/core/engine_adapter/athena.py b/sqlmesh/core/engine_adapter/athena.py index aa8a5ce0c1..bd84ba5276 100644 --- a/sqlmesh/core/engine_adapter/athena.py +++ b/sqlmesh/core/engine_adapter/athena.py @@ -437,6 +437,7 @@ def replace_query( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, source_columns: t.Optional[t.List[str]] = None, + supports_replace_table_override: t.Optional[bool] = None, **kwargs: t.Any, ) -> None: table = exp.to_table(table_name) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 2901831940..c48ce2154d 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -432,8 +432,16 @@ def get_catalog_type(self, catalog: t.Optional[str]) -> str: ) return self.DEFAULT_CATALOG_TYPE + def get_catalog_type_from_table(self, table: TableName) -> str: + """Get the catalog type from a table name if it has a catalog specified, otherwise return the current catalog type""" + catalog = exp.to_table(table).catalog or self.get_current_catalog() + return self.get_catalog_type(catalog) + @property def current_catalog_type(self) -> str: + # `get_catalog_type_from_table` should be used over this property. Reason is that the table that is the target + # of the operation is what matters and not the catalog type of the connection. + # This still remains for legacy reasons and should be refactored out. return self.get_catalog_type(self.get_current_catalog()) def replace_query( @@ -444,6 +452,7 @@ def replace_query( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, source_columns: t.Optional[t.List[str]] = None, + supports_replace_table_override: t.Optional[bool] = None, **kwargs: t.Any, ) -> None: """Replaces an existing table with a query. @@ -494,12 +503,17 @@ def replace_query( ) # All engines support `CREATE TABLE AS` so we use that if the table doesn't already exist and we # use `CREATE OR REPLACE TABLE AS` if the engine supports it - if self.SUPPORTS_REPLACE_TABLE or not table_exists: + supports_replace_table = ( + self.SUPPORTS_REPLACE_TABLE + if supports_replace_table_override is None + else supports_replace_table_override + ) + if supports_replace_table or not table_exists: return self._create_table_from_source_queries( target_table, source_queries, target_columns_to_types, - replace=self.SUPPORTS_REPLACE_TABLE, + replace=supports_replace_table, table_description=table_description, column_descriptions=column_descriptions, **kwargs, diff --git a/sqlmesh/core/engine_adapter/mixins.py b/sqlmesh/core/engine_adapter/mixins.py index bc83beb3d4..865e47fb93 100644 --- a/sqlmesh/core/engine_adapter/mixins.py +++ b/sqlmesh/core/engine_adapter/mixins.py @@ -228,7 +228,7 @@ def _build_view_properties_exp( def _truncate_comment(self, comment: str, length: t.Optional[int]) -> str: # iceberg and delta do not have a comment length limit - if self.current_catalog_type in ("iceberg", "delta"): + if self.current_catalog_type in ("iceberg", "delta_lake"): return comment return super()._truncate_comment(comment, length) diff --git a/sqlmesh/core/engine_adapter/redshift.py b/sqlmesh/core/engine_adapter/redshift.py index 7d14207b52..7979268473 100644 --- a/sqlmesh/core/engine_adapter/redshift.py +++ b/sqlmesh/core/engine_adapter/redshift.py @@ -253,6 +253,7 @@ def replace_query( table_description: t.Optional[str] = None, column_descriptions: t.Optional[t.Dict[str, str]] = None, source_columns: t.Optional[t.List[str]] = None, + supports_replace_table_override: t.Optional[bool] = None, **kwargs: t.Any, ) -> None: """ diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index 4cef557d94..90b3da5240 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -34,6 +34,8 @@ from sqlmesh.core._typing import SchemaName, SessionProperties, TableName from sqlmesh.core.engine_adapter._typing import DF, QueryOrDF +CATALOG_TYPES_SUPPORTING_REPLACE_TABLE = {"iceberg", "delta_lake"} + @set_catalog() class TrinoEngineAdapter( @@ -115,6 +117,37 @@ def session(self, properties: SessionProperties) -> t.Iterator[None]: finally: self.execute(f"RESET SESSION AUTHORIZATION") + def replace_query( + self, + table_name: TableName, + query_or_df: QueryOrDF, + target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, + table_description: t.Optional[str] = None, + column_descriptions: t.Optional[t.Dict[str, str]] = None, + source_columns: t.Optional[t.List[str]] = None, + supports_replace_table_override: t.Optional[bool] = None, + **kwargs: t.Any, + ) -> None: + catalog_type = self.get_catalog_type(self.get_catalog_type_from_table(table_name)) + # User may have a custom catalog type name so we are assuming they keep the catalog type still in the name + # Ex: `acme_iceberg` would be identified as an iceberg catalog and therefore supports replace table + supports_replace_table_override = None + for replace_table_catalog_type in CATALOG_TYPES_SUPPORTING_REPLACE_TABLE: + if replace_table_catalog_type in catalog_type: + supports_replace_table_override = True + break + + super().replace_query( + table_name=table_name, + query_or_df=query_or_df, + target_columns_to_types=target_columns_to_types, + table_description=table_description, + column_descriptions=column_descriptions, + source_columns=source_columns, + supports_replace_table_override=supports_replace_table_override, + **kwargs, + ) + def _insert_overwrite_by_condition( self, table_name: TableName, @@ -250,7 +283,7 @@ def _build_schema_exp( expressions: t.Optional[t.List[exp.PrimaryKey]] = None, is_view: bool = False, ) -> exp.Schema: - if self.current_catalog_type == "delta_lake": + if "delta_lake" in self.get_catalog_type_from_table(table): target_columns_to_types = self._to_delta_ts(target_columns_to_types) return super()._build_schema_exp( @@ -277,7 +310,9 @@ def _scd_type_2( source_columns: t.Optional[t.List[str]] = None, **kwargs: t.Any, ) -> None: - if target_columns_to_types and self.current_catalog_type == "delta_lake": + if target_columns_to_types and "delta_lake" in self.get_catalog_type_from_table( + target_table + ): target_columns_to_types = self._to_delta_ts(target_columns_to_types) return super()._scd_type_2( @@ -381,7 +416,7 @@ def _create_table( else: table_name = table_name_or_schema - if self.current_catalog_type == "hive": + if "hive" in self.get_catalog_type_from_table(table_name): # the Trino Hive connector can take a few seconds for metadata changes to propagate to all internal threads # (even if metadata TTL is set to 0s) # Blocking until the table shows up means that subsequent code expecting it to exist immediately will not fail diff --git a/tests/core/engine_adapter/test_trino.py b/tests/core/engine_adapter/test_trino.py index 745c2bbdfb..930834c8d1 100644 --- a/tests/core/engine_adapter/test_trino.py +++ b/tests/core/engine_adapter/test_trino.py @@ -24,8 +24,8 @@ def trino_mocked_engine_adapter( def mock_catalog_type(catalog_name): if "iceberg" in catalog_name: return "iceberg" - if "delta" in catalog_name: - return "delta" + if "delta_lake" in catalog_name: + return "delta_lake" return "hive" mocker.patch( @@ -50,7 +50,7 @@ def test_set_current_catalog(trino_mocked_engine_adapter: TrinoEngineAdapter): ] -@pytest.mark.parametrize("storage_type", ["iceberg", "delta"]) +@pytest.mark.parametrize("storage_type", ["iceberg", "delta_lake"]) def test_get_catalog_type( trino_mocked_engine_adapter: TrinoEngineAdapter, mocker: MockerFixture, storage_type: str ): @@ -64,13 +64,14 @@ def test_get_catalog_type( assert adapter.get_catalog_type("foo") == TrinoEngineAdapter.DEFAULT_CATALOG_TYPE assert adapter.get_catalog_type("datalake_hive") == "hive" assert adapter.get_catalog_type("datalake_iceberg") == "iceberg" - assert adapter.get_catalog_type("datalake_delta") == "delta" + assert adapter.get_catalog_type("datalake_delta_lake") == "delta_lake" mocker.patch( "sqlmesh.core.engine_adapter.trino.TrinoEngineAdapter.get_current_catalog", return_value=f"system_{storage_type}", ) - assert adapter.current_catalog_type == storage_type + expected_current_type = storage_type + assert adapter.current_catalog_type == expected_current_type def test_get_catalog_type_cached( @@ -103,7 +104,7 @@ def mock_fetchone(sql): assert fetchone_mock.call_count == 2 -@pytest.mark.parametrize("storage_type", ["hive", "delta"]) +@pytest.mark.parametrize("storage_type", ["hive", "delta_lake"]) def test_partitioned_by_hive_delta( trino_mocked_engine_adapter: TrinoEngineAdapter, mocker: MockerFixture, storage_type: str ): @@ -113,7 +114,8 @@ def test_partitioned_by_hive_delta( "sqlmesh.core.engine_adapter.trino.TrinoEngineAdapter.get_current_catalog", return_value=f"datalake_{storage_type}", ) - assert adapter.get_catalog_type(f"datalake_{storage_type}") == storage_type + expected_type = storage_type + assert adapter.get_catalog_type(f"datalake_{storage_type}") == expected_type columns_to_types = { "cola": exp.DataType.build("INT"), @@ -314,7 +316,7 @@ def test_comments_hive(mocker: MockerFixture, make_mocked_engine_adapter: t.Call ] -@pytest.mark.parametrize("storage_type", ["iceberg", "delta"]) +@pytest.mark.parametrize("storage_type", ["iceberg", "delta_lake"]) def test_comments_iceberg_delta( mocker: MockerFixture, make_mocked_engine_adapter: t.Callable, storage_type: str ): @@ -646,3 +648,40 @@ def test_session_authorization(trino_mocked_engine_adapter: TrinoEngineAdapter): "SELECT 1", "RESET SESSION AUTHORIZATION", ] + + +@pytest.mark.parametrize( + "catalog_name,expected_replace", + [ + ("hive_catalog", False), + ("iceberg_catalog", True), + ("delta_catalog", False), + ("acme_delta_lake", True), + ("acme_iceberg", True), + ("custom_delta_lake_something", True), + ("my_iceberg_store", True), + ("plain_catalog", False), + ], +) +def test_replace_table_catalog_support( + trino_mocked_engine_adapter: TrinoEngineAdapter, catalog_name, expected_replace +): + adapter = trino_mocked_engine_adapter + + adapter.replace_query( + table_name=".".join([catalog_name, "schema", "test_table"]), + query_or_df=parse_one("SELECT 1 AS col"), + ) + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 1 + if expected_replace: + assert ( + sql_calls[0] + == f'CREATE OR REPLACE TABLE "{catalog_name}"."schema"."test_table" AS SELECT 1 AS "col"' + ) + else: + assert ( + sql_calls[0] + == f'CREATE TABLE IF NOT EXISTS "{catalog_name}"."schema"."test_table" AS SELECT 1 AS "col"' + ) From 75f825e9fab32d4efd181b7a9fdff34169d2c76a Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 2 Sep 2025 12:30:48 -0700 Subject: [PATCH 0801/1056] Fix!: Avoid using rendered query when computing the data hash (#5256) --- sqlmesh/core/audit/definition.py | 48 ++-- sqlmesh/core/context_diff.py | 9 +- sqlmesh/core/model/common.py | 69 +++++- sqlmesh/core/model/definition.py | 214 ++++++++++++------ sqlmesh/core/node.py | 44 +++- sqlmesh/core/snapshot/categorizer.py | 11 +- sqlmesh/core/snapshot/definition.py | 15 ++ .../v0093_use_raw_sql_in_fingerprint.py | 5 + tests/core/state_sync/test_export_import.py | 4 +- tests/core/test_audit.py | 4 + tests/core/test_context.py | 6 +- tests/core/test_integration.py | 109 +++++++-- tests/core/test_model.py | 69 ++++-- tests/core/test_selector.py | 11 +- tests/core/test_snapshot.py | 24 +- tests/core/test_snapshot_evaluator.py | 3 + tests/core/test_table_diff.py | 11 +- tests/core/test_test.py | 11 +- .../github/cicd/test_integration.py | 57 +++-- 19 files changed, 533 insertions(+), 191 deletions(-) create mode 100644 sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py diff --git a/sqlmesh/core/audit/definition.py b/sqlmesh/core/audit/definition.py index 210ae9da1b..561ee539f6 100644 --- a/sqlmesh/core/audit/definition.py +++ b/sqlmesh/core/audit/definition.py @@ -15,11 +15,10 @@ bool_validator, default_catalog_validator, depends_on_validator, - expression_validator, sort_python_env, sorted_python_env_payloads, ) -from sqlmesh.core.model.common import make_python_env, single_value_or_tuple +from sqlmesh.core.model.common import make_python_env, single_value_or_tuple, ParsableSql from sqlmesh.core.node import _Node from sqlmesh.core.renderer import QueryRenderer from sqlmesh.utils.date import TimeLike @@ -67,15 +66,26 @@ class AuditMixin(AuditCommonMetaMixin): jinja_macros: A registry of jinja macros to use when rendering the audit query. """ - query: t.Union[exp.Query, d.JinjaQuery] + query_: ParsableSql defaults: t.Dict[str, exp.Expression] - expressions_: t.Optional[t.List[exp.Expression]] + expressions_: t.Optional[t.List[ParsableSql]] jinja_macros: JinjaMacroRegistry formatting: t.Optional[bool] + @property + def query(self) -> t.Union[exp.Query, d.JinjaQuery]: + return t.cast(t.Union[exp.Query, d.JinjaQuery], self.query_.parse(self.dialect)) + @property def expressions(self) -> t.List[exp.Expression]: - return self.expressions_ or [] + if not self.expressions_: + return [] + result = [] + for e in self.expressions_: + parsed = e.parse(self.dialect) + if not isinstance(parsed, exp.Semicolon): + result.append(parsed) + return result @property def macro_definitions(self) -> t.List[d.MacroDef]: @@ -122,16 +132,16 @@ class ModelAudit(PydanticModel, AuditMixin, frozen=True): skip: bool = False blocking: bool = True standalone: t.Literal[False] = False - query: t.Union[exp.Query, d.JinjaQuery] + query_: ParsableSql = Field(alias="query") defaults: t.Dict[str, exp.Expression] = {} - expressions_: t.Optional[t.List[exp.Expression]] = Field(default=None, alias="expressions") + expressions_: t.Optional[t.List[ParsableSql]] = Field(default=None, alias="expressions") jinja_macros: JinjaMacroRegistry = JinjaMacroRegistry() formatting: t.Optional[bool] = Field(default=None, exclude=True) _path: t.Optional[Path] = None # Validators - _query_validator = expression_validator + _query_validator = ParsableSql.validator() _bool_validator = bool_validator _string_validator = audit_string_validator _map_validator = audit_map_validator @@ -153,9 +163,9 @@ class StandaloneAudit(_Node, AuditMixin): skip: bool = False blocking: bool = False standalone: t.Literal[True] = True - query: t.Union[exp.Query, d.JinjaQuery] + query_: ParsableSql = Field(alias="query") defaults: t.Dict[str, exp.Expression] = {} - expressions_: t.Optional[t.List[exp.Expression]] = Field(default=None, alias="expressions") + expressions_: t.Optional[t.List[ParsableSql]] = Field(default=None, alias="expressions") jinja_macros: JinjaMacroRegistry = JinjaMacroRegistry() default_catalog: t.Optional[str] = None depends_on_: t.Optional[t.Set[str]] = Field(default=None, alias="depends_on") @@ -165,7 +175,7 @@ class StandaloneAudit(_Node, AuditMixin): source_type: t.Literal["audit"] = "audit" # Validators - _query_validator = expression_validator + _query_validator = ParsableSql.validator() _bool_validator = bool_validator _string_validator = audit_string_validator _map_validator = audit_map_validator @@ -276,8 +286,8 @@ def metadata_hash(self) -> str: self.cron_tz.key if self.cron_tz else None, ] - query = self.render_audit_query() or self.query - data.append(gen(query)) + data.append(self.query_.sql) + data.extend([e.sql for e in self.expressions_ or []]) self._metadata_hash = hash_data(data) return self._metadata_hash @@ -461,11 +471,17 @@ def load_audit( if project is not None: extra_kwargs["project"] = project - dialect = meta_fields.pop("dialect", dialect) + dialect = meta_fields.pop("dialect", dialect) or "" + + parsable_query = ParsableSql.from_parsed_expression(query, dialect, use_meta_sql=True) + parsable_statements = [ + ParsableSql.from_parsed_expression(s, dialect, use_meta_sql=True) for s in statements + ] + try: audit = audit_class( - query=query, - expressions=statements, + query=parsable_query, + expressions=parsable_statements, dialect=dialect, **extra_kwargs, **meta_fields, diff --git a/sqlmesh/core/context_diff.py b/sqlmesh/core/context_diff.py index 12da39f50f..07d13b1c2f 100644 --- a/sqlmesh/core/context_diff.py +++ b/sqlmesh/core/context_diff.py @@ -435,7 +435,7 @@ def directly_modified(self, name: str) -> bool: return False current, previous = self.modified_snapshots[name] - return current.fingerprint.data_hash != previous.fingerprint.data_hash + return current.is_directly_modified(previous) def indirectly_modified(self, name: str) -> bool: """Returns whether or not a node was indirectly modified in this context. @@ -451,10 +451,7 @@ def indirectly_modified(self, name: str) -> bool: return False current, previous = self.modified_snapshots[name] - return ( - current.fingerprint.data_hash == previous.fingerprint.data_hash - and current.fingerprint.parent_data_hash != previous.fingerprint.parent_data_hash - ) + return current.is_indirectly_modified(previous) def metadata_updated(self, name: str) -> bool: """Returns whether or not the given node's metadata has been updated. @@ -470,7 +467,7 @@ def metadata_updated(self, name: str) -> bool: return False current, previous = self.modified_snapshots[name] - return current.fingerprint.metadata_hash != previous.fingerprint.metadata_hash + return current.is_metadata_updated(previous) def text_diff(self, name: str) -> str: """Finds the difference of a node between the current and remote environment. diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index 9a68ec18c0..0a55f80cee 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -21,7 +21,7 @@ prepare_env, serialize_env, ) -from sqlmesh.utils.pydantic import PydanticModel, ValidationInfo, field_validator +from sqlmesh.utils.pydantic import PydanticModel, ValidationInfo, field_validator, get_dialect if t.TYPE_CHECKING: from sqlglot.dialects.dialect import DialectType @@ -616,11 +616,6 @@ def parse_strings_with_macro_refs(value: t.Any, dialect: DialectType) -> t.Any: expression_validator: t.Callable = field_validator( - "query", - "expressions_", - "pre_statements_", - "post_statements_", - "on_virtual_update_", "unique_key", mode="before", check_fields=False, @@ -663,3 +658,65 @@ def parse_strings_with_macro_refs(value: t.Any, dialect: DialectType) -> t.Any: mode="before", check_fields=False, )(depends_on) + + +class ParsableSql(PydanticModel): + sql: str + + _parsed: t.Optional[exp.Expression] = None + _parsed_dialect: t.Optional[str] = None + + def parse(self, dialect: str) -> exp.Expression: + if self._parsed is None or self._parsed_dialect != dialect: + self._parsed = d.parse_one(self.sql, dialect=dialect) + self._parsed_dialect = dialect + return self._parsed + + @classmethod + def from_parsed_expression( + cls, parsed_expression: exp.Expression, dialect: str, use_meta_sql: bool = False + ) -> ParsableSql: + sql = ( + parsed_expression.meta.get("sql") or parsed_expression.sql(dialect=dialect) + if use_meta_sql + else parsed_expression.sql(dialect=dialect) + ) + result = cls(sql=sql) + result._parsed = parsed_expression + result._parsed_dialect = dialect + return result + + @classmethod + def validator(cls) -> classmethod: + def _validate_parsable_sql( + v: t.Any, info: ValidationInfo + ) -> t.Optional[t.Union[ParsableSql, t.List[ParsableSql]]]: + if v is None: + return v + if isinstance(v, str): + return ParsableSql(sql=v) + if isinstance(v, exp.Expression): + return ParsableSql.from_parsed_expression( + v, get_dialect(info.data), use_meta_sql=False + ) + if isinstance(v, list): + dialect = get_dialect(info.data) + return [ + ParsableSql(sql=s) + if isinstance(s, str) + else ParsableSql.from_parsed_expression(s, dialect, use_meta_sql=False) + if isinstance(s, exp.Expression) + else ParsableSql.parse_obj(s) + for s in v + ] + return ParsableSql.parse_obj(v) + + return field_validator( + "query_", + "expressions_", + "pre_statements_", + "post_statements_", + "on_virtual_update_", + mode="before", + check_fields=False, + )(_validate_parsable_sql) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index dba8eedc31..f3ffcde05a 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -24,7 +24,7 @@ from sqlmesh.core.node import IntervalUnit from sqlmesh.core.macros import MacroRegistry, macro from sqlmesh.core.model.common import ( - expression_validator, + ParsableSql, make_python_env, parse_dependencies, parse_strings_with_macro_refs, @@ -62,6 +62,7 @@ if t.TYPE_CHECKING: from sqlglot.dialects.dialect import DialectType + from sqlmesh.core.node import _Node from sqlmesh.core._typing import Self, TableName, SessionProperties from sqlmesh.core.context import ExecutionContext from sqlmesh.core.engine_adapter import EngineAdapter @@ -150,21 +151,17 @@ class _Model(ModelMeta, frozen=True): audit_definitions: t.Dict[str, ModelAudit] = {} mapping_schema: t.Dict[str, t.Any] = {} extract_dependencies_from_query: bool = True + pre_statements_: t.Optional[t.List[ParsableSql]] = Field(default=None, alias="pre_statements") + post_statements_: t.Optional[t.List[ParsableSql]] = Field(default=None, alias="post_statements") + on_virtual_update_: t.Optional[t.List[ParsableSql]] = Field( + default=None, alias="on_virtual_update" + ) _full_depends_on: t.Optional[t.Set[str]] = None _statement_renderer_cache: t.Dict[int, ExpressionRenderer] = {} + _is_metadata_only_change_cache: t.Dict[int, bool] = {} - pre_statements_: t.Optional[t.List[exp.Expression]] = Field( - default=None, alias="pre_statements" - ) - post_statements_: t.Optional[t.List[exp.Expression]] = Field( - default=None, alias="post_statements" - ) - on_virtual_update_: t.Optional[t.List[exp.Expression]] = Field( - default=None, alias="on_virtual_update" - ) - - _expressions_validator = expression_validator + _expressions_validator = ParsableSql.validator() def __getstate__(self) -> t.Dict[t.Any, t.Any]: state = super().__getstate__() @@ -543,15 +540,15 @@ def render_audit_query( @property def pre_statements(self) -> t.List[exp.Expression]: - return self.pre_statements_ or [] + return self._get_parsed_statements("pre_statements_") @property def post_statements(self) -> t.List[exp.Expression]: - return self.post_statements_ or [] + return self._get_parsed_statements("post_statements_") @property def on_virtual_update(self) -> t.List[exp.Expression]: - return self.on_virtual_update_ or [] + return self._get_parsed_statements("on_virtual_update_") @property def macro_definitions(self) -> t.List[d.MacroDef]: @@ -562,6 +559,17 @@ def macro_definitions(self) -> t.List[d.MacroDef]: if isinstance(s, d.MacroDef) ] + def _get_parsed_statements(self, attr_name: str) -> t.List[exp.Expression]: + value = getattr(self, attr_name) + if not value: + return [] + result = [] + for v in value: + parsed = v.parse(self.dialect) + if not isinstance(parsed, exp.Semicolon): + result.append(parsed) + return result + def _render_statements( self, statements: t.Iterable[exp.Expression], @@ -1025,6 +1033,45 @@ def is_breaking_change(self, previous: Model) -> t.Optional[bool]: """ raise NotImplementedError + def is_metadata_only_change(self, other: _Node) -> bool: + if self._is_metadata_only_change_cache.get(id(other), None) is not None: + return self._is_metadata_only_change_cache[id(other)] + + is_metadata_change = True + if ( + not isinstance(other, _Model) + or self.metadata_hash == other.metadata_hash + or self._data_hash_values_no_sql != other._data_hash_values_no_sql + ): + is_metadata_change = False + else: + this_statements = [ + s + for s in [*self.pre_statements, *self.post_statements] + if not self._is_metadata_statement(s) + ] + other_statements = [ + s + for s in [*other.pre_statements, *other.post_statements] + if not other._is_metadata_statement(s) + ] + if len(this_statements) != len(other_statements): + is_metadata_change = False + else: + for this_statement, other_statement in zip(this_statements, other_statements): + this_rendered = ( + self._statement_renderer(this_statement).render() or this_statement + ) + other_rendered = ( + other._statement_renderer(other_statement).render() or other_statement + ) + if this_rendered != other_rendered: + is_metadata_change = False + break + + self._is_metadata_only_change_cache[id(other)] = is_metadata_change + return is_metadata_change + @property def data_hash(self) -> str: """ @@ -1039,6 +1086,20 @@ def data_hash(self) -> str: @property def _data_hash_values(self) -> t.List[str]: + return self._data_hash_values_no_sql + self._data_hash_values_sql + + @property + def _data_hash_values_sql(self) -> t.List[str]: + data = [] + + for statements in [self.pre_statements_, self.post_statements_]: + for statement in statements or []: + data.append(statement.sql) + + return data + + @property + def _data_hash_values_no_sql(self) -> t.List[str]: data = [ str( # Exclude metadata only macro funcs [(k, v) for k, v in self.sorted_python_env if not v.is_metadata] @@ -1066,18 +1127,6 @@ def _data_hash_values(self) -> t.List[str]: data.append(key) data.append(gen(value)) - for statement in (*self.pre_statements, *self.post_statements): - statement_exprs: t.List[exp.Expression] = [] - if not isinstance(statement, d.MacroDef): - rendered = self._statement_renderer(statement).render() - if self._is_metadata_statement(statement): - continue - if rendered: - statement_exprs = rendered - else: - statement_exprs = [statement] - data.extend(gen(e) for e in statement_exprs) - return data # type: ignore def _audit_metadata_hash_values(self) -> t.List[str]: @@ -1093,13 +1142,9 @@ def _audit_metadata_hash_values(self) -> t.List[str]: metadata.append(gen(arg_value)) else: audit = self.audit_definitions[audit_name] - query = ( - self.render_audit_query(audit, **t.cast(t.Dict[str, t.Any], audit_args)) - or audit.query - ) metadata.extend( [ - gen(query), + audit.query_.sql, audit.dialect, str(audit.skip), str(audit.blocking), @@ -1170,12 +1215,9 @@ def _additional_metadata(self) -> t.List[str]: if metadata_only_macros: additional_metadata.append(str(metadata_only_macros)) - for statement in (*self.pre_statements, *self.post_statements): - if self._is_metadata_statement(statement): - additional_metadata.append(gen(statement)) - - for statement in self.on_virtual_update: - additional_metadata.append(gen(statement)) + for statements in [self.pre_statements_, self.post_statements_, self.on_virtual_update_]: + for statement in statements or []: + additional_metadata.append(statement.sql) return additional_metadata @@ -1274,7 +1316,7 @@ class SqlModel(_Model): on_virtual_update: The list of SQL statements to be executed after the virtual update. """ - query: t.Union[exp.Query, d.JinjaQuery, d.MacroFunc] + query_: ParsableSql = Field(alias="query") source_type: t.Literal["sql"] = "sql" _columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None @@ -1298,6 +1340,11 @@ def copy(self, **kwargs: t.Any) -> Self: model._full_depends_on = None return model + @property + def query(self) -> t.Union[exp.Query, d.JinjaQuery, d.MacroFunc]: + parsed_query = self.query_.parse(self.dialect) + return t.cast(t.Union[exp.Query, d.JinjaQuery, d.MacroFunc], parsed_query) + def render_query( self, *, @@ -1500,6 +1547,24 @@ def is_breaking_change(self, previous: Model) -> t.Optional[bool]: return False + def is_metadata_only_change(self, previous: _Node) -> bool: + if self._is_metadata_only_change_cache.get(id(previous), None) is not None: + return self._is_metadata_only_change_cache[id(previous)] + + if not super().is_metadata_only_change(previous): + return False + + if not isinstance(previous, SqlModel): + self._is_metadata_only_change_cache[id(previous)] = False + return False + + this_rendered_query = self.render_query() or self.query + previous_rendered_query = previous.render_query() or previous.query + is_metadata_change = this_rendered_query == previous_rendered_query + + self._is_metadata_only_change_cache[id(previous)] = is_metadata_change + return is_metadata_change + @cached_property def _query_renderer(self) -> QueryRenderer: no_quote_identifiers = self.kind.is_view and self.dialect in ("trino", "spark") @@ -1519,17 +1584,22 @@ def _query_renderer(self) -> QueryRenderer: ) @property - def _data_hash_values(self) -> t.List[str]: - data = super()._data_hash_values + def _data_hash_values_no_sql(self) -> t.List[str]: + return [ + *super()._data_hash_values_no_sql, + *self.jinja_macros.data_hash_values, + ] - query = self.render_query() or self.query - data.append(gen(query)) - data.extend(self.jinja_macros.data_hash_values) - return data + @property + def _data_hash_values_sql(self) -> t.List[str]: + return [ + *super()._data_hash_values_sql, + self.query_.sql, + ] @property def _additional_metadata(self) -> t.List[str]: - return [*super()._additional_metadata, gen(self.query)] + return [*super()._additional_metadata, self.query_.sql] @property def violated_rules_for_query(self) -> t.Dict[type[Rule], t.Any]: @@ -1753,8 +1823,8 @@ def _reader(self) -> CsvSeedReader: return self.seed.reader(dialect=self.dialect, settings=self.kind.csv_settings) @property - def _data_hash_values(self) -> t.List[str]: - data = super()._data_hash_values + def _data_hash_values_no_sql(self) -> t.List[str]: + data = super()._data_hash_values_no_sql for column_name, column_hash in self.column_hashes.items(): data.append(column_name) data.append(column_hash) @@ -1847,8 +1917,8 @@ def is_breaking_change(self, previous: Model) -> t.Optional[bool]: return None @property - def _data_hash_values(self) -> t.List[str]: - data = super()._data_hash_values + def _data_hash_values_no_sql(self) -> t.List[str]: + data = super()._data_hash_values_no_sql data.append(self.entrypoint) return data @@ -2227,6 +2297,7 @@ def load_sql_based_model( variables=variables, inline_audits=inline_audits, blueprint_variables=blueprint_variables, + use_original_sql=True, **meta_fields, ) @@ -2449,6 +2520,7 @@ def _create_model( signal_definitions: t.Optional[SignalRegistry] = None, variables: t.Optional[t.Dict[str, t.Any]] = None, blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None, + use_original_sql: bool = False, **kwargs: t.Any, ) -> Model: validate_extra_and_required_fields( @@ -2482,34 +2554,26 @@ def _create_model( statements: t.List[t.Union[exp.Expression, t.Tuple[exp.Expression, bool]]] = [] - # Merge default pre_statements with model-specific pre_statements - if "pre_statements" in defaults: - kwargs["pre_statements"] = [ - exp.maybe_parse(stmt, dialect=dialect) for stmt in defaults["pre_statements"] - ] + kwargs.get("pre_statements", []) - - # Merge default post_statements with model-specific post_statements - if "post_statements" in defaults: - kwargs["post_statements"] = [ - exp.maybe_parse(stmt, dialect=dialect) for stmt in defaults["post_statements"] - ] + kwargs.get("post_statements", []) - - # Merge default on_virtual_update with model-specific on_virtual_update - if "on_virtual_update" in defaults: - kwargs["on_virtual_update"] = [ - exp.maybe_parse(stmt, dialect=dialect) for stmt in defaults["on_virtual_update"] - ] + kwargs.get("on_virtual_update", []) - - if "pre_statements" in kwargs: - statements.extend(kwargs["pre_statements"]) if "query" in kwargs: statements.append(kwargs["query"]) - if "post_statements" in kwargs: - statements.extend(kwargs["post_statements"]) + kwargs["query"] = ParsableSql.from_parsed_expression( + kwargs["query"], dialect, use_meta_sql=use_original_sql + ) - # Macros extracted from these statements need to be treated as metadata only - if "on_virtual_update" in kwargs: - statements.extend((stmt, True) for stmt in kwargs["on_virtual_update"]) + # Merge default statements with model-specific statements + for statement_field in ["pre_statements", "post_statements", "on_virtual_update"]: + if statement_field in defaults: + kwargs[statement_field] = [ + exp.maybe_parse(stmt, dialect=dialect) for stmt in defaults[statement_field] + ] + kwargs.get(statement_field, []) + if statement_field in kwargs: + # Macros extracted from these statements need to be treated as metadata only + is_metadata = statement_field == "on_virtual_update" + statements.extend((stmt, is_metadata) for stmt in kwargs[statement_field]) + kwargs[statement_field] = [ + ParsableSql.from_parsed_expression(stmt, dialect, use_meta_sql=use_original_sql) + for stmt in kwargs[statement_field] + ] # This is done to allow variables like @gateway to be used in these properties # since rendering shifted from load time to run time. diff --git a/sqlmesh/core/node.py b/sqlmesh/core/node.py index 4f0a66dc2e..ea2264f7fa 100644 --- a/sqlmesh/core/node.py +++ b/sqlmesh/core/node.py @@ -307,16 +307,6 @@ def batch_concurrency(self) -> t.Optional[int]: """The maximal number of batches that can run concurrently for a backfill.""" return None - @property - def data_hash(self) -> str: - """ - Computes the data hash for the node. - - Returns: - The data hash for the node. - """ - raise NotImplementedError - @property def interval_unit(self) -> IntervalUnit: """Returns the interval unit using which data intervals are computed for this node.""" @@ -332,6 +322,16 @@ def depends_on(self) -> t.Set[str]: def fqn(self) -> str: return self.name + @property + def data_hash(self) -> str: + """ + Computes the data hash for the node. + + Returns: + The data hash for the node. + """ + raise NotImplementedError + @property def metadata_hash(self) -> str: """ @@ -342,6 +342,30 @@ def metadata_hash(self) -> str: """ raise NotImplementedError + def is_metadata_only_change(self, previous: _Node) -> bool: + """Determines if this node is a metadata only change in relation to the `previous` node. + + Args: + previous: The previous node to compare against. + + Returns: + True if this node is a metadata only change, False otherwise. + """ + return self.data_hash == previous.data_hash and self.metadata_hash != previous.metadata_hash + + def is_data_change(self, previous: _Node) -> bool: + """Determines if this node is a data change in relation to the `previous` node. + + Args: + previous: The previous node to compare against. + + Returns: + True if this node is a data change, False otherwise. + """ + return ( + self.data_hash != previous.data_hash or self.metadata_hash != previous.metadata_hash + ) and not self.is_metadata_only_change(previous) + def croniter(self, value: TimeLike) -> CroniterCache: if self._croniter is None: self._croniter = CroniterCache(self.cron, value, tz=self.cron_tz) diff --git a/sqlmesh/core/snapshot/categorizer.py b/sqlmesh/core/snapshot/categorizer.py index 88a1ef37ab..78ea7466ed 100644 --- a/sqlmesh/core/snapshot/categorizer.py +++ b/sqlmesh/core/snapshot/categorizer.py @@ -47,11 +47,12 @@ def categorize_change( if type(new_model) != type(old_model): return default_category - if new.fingerprint.data_hash == old.fingerprint.data_hash: - if new.fingerprint.metadata_hash == old.fingerprint.metadata_hash: - raise SQLMeshError( - f"{new} is unmodified or indirectly modified and should not be categorized" - ) + if new.fingerprint == old.fingerprint: + raise SQLMeshError( + f"{new} is unmodified or indirectly modified and should not be categorized" + ) + + if not new.is_directly_modified(old): if new.fingerprint.parent_data_hash == old.fingerprint.parent_data_hash: return SnapshotChangeCategory.NON_BREAKING return None diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index afc8e06458..dea4ef64e5 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1230,6 +1230,21 @@ def apply_pending_restatement_intervals(self) -> None: ) self.intervals = remove_interval(self.intervals, *pending_restatement_interval) + def is_directly_modified(self, other: Snapshot) -> bool: + """Returns whether or not this snapshot is directly modified in relation to the other snapshot.""" + return self.node.is_data_change(other.node) + + def is_indirectly_modified(self, other: Snapshot) -> bool: + """Returns whether or not this snapshot is indirectly modified in relation to the other snapshot.""" + return ( + self.fingerprint.parent_data_hash != other.fingerprint.parent_data_hash + and not self.node.is_data_change(other.node) + ) + + def is_metadata_updated(self, other: Snapshot) -> bool: + """Returns whether or not this snapshot contains metadata changes in relation to the other snapshot.""" + return self.fingerprint.metadata_hash != other.fingerprint.metadata_hash + @property def physical_schema(self) -> str: if self.physical_schema_ is not None: diff --git a/sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py b/sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py new file mode 100644 index 0000000000..53d4cb1727 --- /dev/null +++ b/sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py @@ -0,0 +1,5 @@ +"""Use the raw SQL when computing the model fingerprint.""" + + +def migrate(state_sync, **kwargs): # type: ignore + pass diff --git a/tests/core/state_sync/test_export_import.py b/tests/core/state_sync/test_export_import.py index 2d20199d33..c303a63e59 100644 --- a/tests/core/state_sync/test_export_import.py +++ b/tests/core/state_sync/test_export_import.py @@ -289,8 +289,8 @@ def test_export_local_state( full_model = next(s for s in snapshots if "full_model" in s["name"]) new_model = next(s for s in snapshots if "new_model" in s["name"]) - assert "'1' as modified" in full_model["node"]["query"] - assert "SELECT 1 as id" in new_model["node"]["query"] + assert "'1' as modified" in full_model["node"]["query"]["sql"] + assert "SELECT 1 as id" in new_model["node"]["query"]["sql"] def test_import_invalid_file(tmp_path: Path, state_sync: StateSync) -> None: diff --git a/tests/core/test_audit.py b/tests/core/test_audit.py index 81335e5f1a..ed67975e9e 100644 --- a/tests/core/test_audit.py +++ b/tests/core/test_audit.py @@ -80,6 +80,8 @@ def test_load(assert_exp_eq): col IS NULL """, ) + assert audit.query_._parsed is not None + assert audit.query_._parsed_dialect == "spark" def test_load_standalone(assert_exp_eq): @@ -121,6 +123,8 @@ def test_load_standalone(assert_exp_eq): col IS NULL """, ) + assert audit.query_._parsed is not None + assert audit.query_._parsed_dialect == "spark" def test_load_standalone_default_catalog(assert_exp_eq): diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 3b7c5bd51d..196889a87c 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -39,6 +39,7 @@ 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.common import ParsableSql from sqlmesh.core.model.cache import OptimizedQueryCache from sqlmesh.core.renderer import render_statements from sqlmesh.core.model.kind import ModelKindName @@ -2303,7 +2304,10 @@ def test_prompt_if_uncategorized_snapshot(mocker: MockerFixture, tmp_path: Path) incremental_model = context.get_model("sqlmesh_example.incremental_model") incremental_model_query = incremental_model.render_query() new_incremental_model_query = t.cast(exp.Select, incremental_model_query).select("1 AS z") - context.upsert_model("sqlmesh_example.incremental_model", query=new_incremental_model_query) + context.upsert_model( + "sqlmesh_example.incremental_model", + query_=ParsableSql(sql=new_incremental_model_query.sql(dialect=incremental_model.dialect)), + ) mock_console = mocker.Mock() spy_plan = mocker.spy(mock_console, "plan") diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 0e779481fd..c22e904374 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -16,6 +16,7 @@ from pathlib import Path from sqlmesh.core.console import set_console, get_console, TerminalConsole from sqlmesh.core.config.naming import NameInferenceConfig +from sqlmesh.core.model.common import ParsableSql from sqlmesh.utils.concurrency import NodeExecutionFailedError import time_machine from pytest_mock.plugin import MockerFixture @@ -2023,7 +2024,7 @@ def test_dbt_select_star_is_directly_modified(sushi_test_dbt_context: Context): model = context.get_model("sushi.simple_model_a") context.upsert_model( model, - query=d.parse_one("SELECT 1 AS a, 2 AS b"), + query_=ParsableSql(sql="SELECT 1 AS a, 2 AS b"), ) snapshot_a_id = context.get_snapshot("sushi.simple_model_a").snapshot_id # type: ignore @@ -2605,8 +2606,8 @@ def test_unaligned_start_snapshot_with_non_deployable_downstream(init_and_plan_c context.upsert_model(SqlModel.parse_obj(kwargs)) context.upsert_model( downstream_model_name, - query=d.parse_one( - "SELECT customer_id, MAX(revenue) AS max_revenue FROM memory.sushi.customer_revenue_lifetime_new GROUP BY 1" + query_=ParsableSql( + sql="SELECT customer_id, MAX(revenue) AS max_revenue FROM memory.sushi.customer_revenue_lifetime_new GROUP BY 1" ), ) @@ -2637,7 +2638,13 @@ def test_virtual_environment_mode_dev_only(init_and_plan_context: t.Callable): # Make a change in dev original_model = context.get_model("sushi.waiter_revenue_by_day") original_fingerprint = context.get_snapshot(original_model.name).fingerprint - model = original_model.copy(update={"query": original_model.query.order_by("waiter_id")}) + model = original_model.copy( + update={ + "query_": ParsableSql( + sql=original_model.query.order_by("waiter_id").sql(dialect=original_model.dialect) + ) + } + ) model = add_projection_to_model(t.cast(SqlModel, model)) context.upsert_model(model) @@ -4681,12 +4688,12 @@ def test_plan_repairs_unrenderable_snapshot_state( f"name = '{target_snapshot.name}' AND identifier = '{target_snapshot.identifier}'", ) + context.clear_caches() + target_snapshot_in_state = context.state_sync.get_snapshots([target_snapshot.snapshot_id])[ + target_snapshot.snapshot_id + ] + with pytest.raises(Exception): - context_copy = context.copy() - context_copy.clear_caches() - target_snapshot_in_state = context_copy.state_sync.get_snapshots( - [target_snapshot.snapshot_id] - )[target_snapshot.snapshot_id] target_snapshot_in_state.model.render_query_or_raise() # Repair the snapshot by creating a new version of it @@ -4695,11 +4702,11 @@ def test_plan_repairs_unrenderable_snapshot_state( plan_builder = context.plan_builder("prod", forward_only=forward_only) plan = plan_builder.build() - assert plan.directly_modified == {target_snapshot.snapshot_id} if not forward_only: assert target_snapshot.snapshot_id in {i.snapshot_id for i in plan.missing_intervals} - plan_builder.set_choice(target_snapshot, SnapshotChangeCategory.NON_BREAKING) - plan = plan_builder.build() + assert plan.directly_modified == {target_snapshot.snapshot_id} + plan_builder.set_choice(target_snapshot, SnapshotChangeCategory.NON_BREAKING) + plan = plan_builder.build() context.apply(plan) @@ -5383,7 +5390,10 @@ def test_auto_categorization(sushi_context: Context): ).fingerprint model = t.cast(SqlModel, sushi_context.get_model("sushi.customers", raise_if_missing=True)) - sushi_context.upsert_model("sushi.customers", query=model.query.select("'foo' AS foo")) # type: ignore + sushi_context.upsert_model( + "sushi.customers", + query_=ParsableSql(sql=model.query.select("'foo' AS foo").sql(dialect=model.dialect)), # type: ignore + ) apply_to_environment(sushi_context, environment) assert ( @@ -5447,7 +5457,13 @@ def test_multi(mocker): model = context.get_model("bronze.a") assert model.project == "repo_1" - context.upsert_model(model.copy(update={"query": model.query.select("'c' AS c")})) + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql(sql=model.query.select("'c' AS c").sql(dialect=model.dialect)) + } + ) + ) plan = context.plan_builder().build() assert set(snapshot.name for snapshot in plan.directly_modified) == { @@ -5615,7 +5631,15 @@ def test_multi_virtual_layer(copy_to_temp_path): model = context.get_model("db_1.first_schema.model_one") - context.upsert_model(model.copy(update={"query": model.query.select("'c' AS extra")})) + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'c' AS extra").sql(dialect=model.dialect) + ) + } + ) + ) plan = context.plan_builder().build() context.apply(plan) @@ -5641,9 +5665,25 @@ def test_multi_virtual_layer(copy_to_temp_path): # Create dev environment with changed models model = context.get_model("db_2.second_schema.model_one") - context.upsert_model(model.copy(update={"query": model.query.select("'d' AS extra")})) + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'d' AS extra").sql(dialect=model.dialect) + ) + } + ) + ) model = context.get_model("first_schema.model_two") - context.upsert_model(model.copy(update={"query": model.query.select("'d2' AS col")})) + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'d2' AS col").sql(dialect=model.dialect) + ) + } + ) + ) plan = context.plan_builder("dev").build() context.apply(plan) @@ -6630,11 +6670,12 @@ def change_data_type( assert model is not None if isinstance(model, SqlModel): - data_types = model.query.find_all(DataType) + query = model.query.copy() + data_types = query.find_all(DataType) for data_type in data_types: if data_type.this == old_type: data_type.set("this", new_type) - context.upsert_model(model_name, query=model.query) + context.upsert_model(model_name, query_=ParsableSql(sql=query.sql(dialect=model.dialect))) elif model.columns_to_types_ is not None: for k, v in model.columns_to_types_.items(): if v.this == old_type: @@ -6921,7 +6962,15 @@ def test_destroy(copy_to_temp_path): model = context.get_model("db_1.first_schema.model_one") - context.upsert_model(model.copy(update={"query": model.query.select("'c' AS extra")})) + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'c' AS extra").sql(dialect=model.dialect) + ) + } + ) + ) plan = context.plan_builder().build() context.apply(plan) @@ -6932,9 +6981,25 @@ def test_destroy(copy_to_temp_path): # Create dev environment with changed models model = context.get_model("db_2.second_schema.model_one") - context.upsert_model(model.copy(update={"query": model.query.select("'d' AS extra")})) + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'d' AS extra").sql(dialect=model.dialect) + ) + } + ) + ) model = context.get_model("first_schema.model_two") - context.upsert_model(model.copy(update={"query": model.query.select("'d2' AS col")})) + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'d2' AS col").sql(dialect=model.dialect) + ) + } + ) + ) plan = context.plan_builder("dev").build() context.apply(plan) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 3850e08164..be1df5f2d6 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -22,6 +22,7 @@ from sqlmesh.core import dialect as d from sqlmesh.core.console import get_console from sqlmesh.core.audit import ModelAudit, load_audit +from sqlmesh.core.model.common import ParsableSql from sqlmesh.core.config import ( Config, DuckDBConnectionConfig, @@ -5732,7 +5733,7 @@ def test_default_catalog_sql(assert_exp_eq): The system is not designed to actually support having an engine that doesn't support default catalog to start supporting it or the reverse of that. If that did happen then bugs would occur. """ - HASH_WITH_CATALOG = "1269513823" + HASH_WITH_CATALOG = "2768215345" # Test setting default catalog doesn't change hash if it matches existing logic expressions = d.parse( @@ -8308,15 +8309,9 @@ def noop(evaluator) -> None: new_model = load_sql_based_model( expressions, path=Path("./examples/sushi/models/test_model.sql") ) - if metadata_only: - assert "noop" not in new_model._data_hash_values[0] - assert "noop" in new_model._additional_metadata[0] - assert model.data_hash == new_model.data_hash - assert model.metadata_hash != new_model.metadata_hash - else: - assert "noop" in new_model._data_hash_values[0] - assert model.data_hash != new_model.data_hash - assert model.metadata_hash == new_model.metadata_hash + assert model.metadata_hash != new_model.metadata_hash + assert model.data_hash != new_model.data_hash + assert new_model.is_metadata_only_change(model) == metadata_only @macro(metadata_only=metadata_only) # type: ignore def noop(evaluator) -> None: @@ -8336,6 +8331,7 @@ def noop(evaluator) -> None: assert "print" in updated_model._data_hash_values[0] assert new_model.data_hash != updated_model.data_hash assert new_model.metadata_hash == updated_model.metadata_hash + assert updated_model.is_metadata_only_change(new_model) == metadata_only def test_managed_kind_sql(): @@ -8874,7 +8870,9 @@ def test_column_description_metadata_change(): context.upsert_model(model) context.plan(no_prompts=True, auto_apply=True) - context.upsert_model("db.test_model", query=parse_one("SELECT 1 AS id /* description 2 */")) + context.upsert_model( + "db.test_model", query_=ParsableSql(sql="SELECT 1 AS id /* description 2 */") + ) plan = context.plan(no_prompts=True, auto_apply=True) snapshots = list(plan.snapshots.values()) @@ -10729,7 +10727,7 @@ def f(): Context(paths=tmp_path, config=config) -def test_semicolon_is_not_included_in_model_state(tmp_path, assert_exp_eq): +def test_semicolon_is_metadata_only_change(tmp_path, assert_exp_eq): init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) db_connection = DuckDBConnectionConfig(database=str(tmp_path / "db.db")) @@ -10818,7 +10816,9 @@ def test_semicolon_is_not_included_in_model_state(tmp_path, assert_exp_eq): ctx.load() plan = ctx.plan(no_prompts=True, auto_apply=True) - assert not plan.context_diff.modified_snapshots + assert len(plan.context_diff.modified_snapshots) == 1 + assert len(plan.new_snapshots) == 1 + assert plan.new_snapshots[0].is_metadata def test_invalid_audit_reference(): @@ -11471,3 +11471,46 @@ def test_raw_jinja_raw_tag(): model = load_sql_based_model(expressions) assert model.render_query().sql() == "SELECT '{{ foo }}' AS \"col\"" + + +def test_use_original_sql(): + expressions = d.parse( + """ + MODEL (name test); + + CREATE TABLE pre ( + a INT + ); + + SELECT + 1, + 2; + + CREATE TABLE post ( + b INT + ); + """ + ) + + model = load_sql_based_model(expressions) + assert model.query_.sql == "SELECT\n 1,\n 2" + assert model.pre_statements_[0].sql == "CREATE TABLE pre (\n a INT\n )" + assert model.post_statements_[0].sql == "CREATE TABLE post (\n b INT\n );" + + # Now manually create the model and make sure that the original SQL is not used + model_query = d.parse_one("SELECT 1 AS one") + assert model_query.meta["sql"] == "SELECT 1 AS one" + model_query = model_query.select("2 AS two") + + pre_statements = [d.parse_one("CREATE TABLE pre (\n a INT\n )")] + post_statements = [d.parse_one("CREATE TABLE post (\n b INT\n );")] + + model = create_sql_model( + "test", + model_query, + pre_statements=pre_statements, + post_statements=post_statements, + ) + assert model.query_.sql == "SELECT 1 AS one, 2 AS two" + assert model.pre_statements_[0].sql == "CREATE TABLE pre (a INT)" + assert model.post_statements_[0].sql == "CREATE TABLE post (b INT)" diff --git a/tests/core/test_selector.py b/tests/core/test_selector.py index 9f3bc9f698..80b9ef691e 100644 --- a/tests/core/test_selector.py +++ b/tests/core/test_selector.py @@ -11,6 +11,7 @@ from sqlmesh.core.audit import StandaloneAudit from sqlmesh.core.environment import Environment from sqlmesh.core.model import Model, SqlModel +from sqlmesh.core.model.common import ParsableSql from sqlmesh.core.selector import Selector from sqlmesh.core.snapshot import SnapshotChangeCategory from sqlmesh.utils import UniqueKeyDict @@ -293,7 +294,13 @@ def test_select_change_schema(mocker: MockerFixture, make_snapshot): } local_models: UniqueKeyDict[str, Model] = UniqueKeyDict("models") - local_parent = parent.copy(update={"query": parent.query.select("2 as b", append=False)}) # type: ignore + local_parent = parent.copy( + update={ + "query_": ParsableSql( + sql=parent.query.select("2 as b", append=False).sql(dialect=parent.dialect) # type: ignore + ) + } + ) local_models[local_parent.fqn] = local_parent local_child = child.copy(update={"mapping_schema": {'"db"': {'"parent"': {"b": "INT"}}}}) local_models[local_child.fqn] = local_child @@ -301,7 +308,7 @@ def test_select_change_schema(mocker: MockerFixture, make_snapshot): selector = Selector(state_reader_mock, local_models) selected = selector.select_models(["db.parent"], env_name) - assert selected[local_child.fqn].data_hash != child.data_hash + assert selected[local_child.fqn].render_query() != child.render_query() _assert_models_equal( selected, diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index 86fb434e33..d63b642f60 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -79,7 +79,7 @@ def parent_model(): name="parent.tbl", kind=dict(time_column="ds", name=ModelKindName.INCREMENTAL_BY_TIME_RANGE), dialect="spark", - query=parse_one("SELECT 1, ds"), + query="SELECT 1, ds", ) @@ -92,7 +92,7 @@ def model(): dialect="spark", cron="1 0 * * *", start="2020-01-01", - query=parse_one("SELECT @EACH([1, 2], x -> x), ds FROM parent.tbl"), + query="SELECT @EACH([1, 2], x -> x), ds FROM parent.tbl", ) @@ -148,7 +148,9 @@ def test_json(snapshot: Snapshot): "project": "", "python_env": {}, "owner": "owner", - "query": "SELECT @EACH([1, 2], x -> x), ds FROM parent.tbl", + "query": { + "sql": "SELECT @EACH([1, 2], x -> x), ds FROM parent.tbl", + }, "jinja_macros": { "create_builtins_module": "sqlmesh.utils.jinja", "global_objs": {}, @@ -186,7 +188,7 @@ def test_json_custom_materialization(make_snapshot: t.Callable): dialect="spark", cron="1 0 * * *", start="2020-01-01", - query=parse_one("SELECT @EACH([1, 2], x -> x), ds FROM parent.tbl"), + query="SELECT @EACH([1, 2], x -> x), ds FROM parent.tbl", ) snapshot = make_snapshot( @@ -913,8 +915,8 @@ def test_fingerprint(model: Model, parent_model: Model): fingerprint = fingerprint_from_node(model, nodes={}) original_fingerprint = SnapshotFingerprint( - data_hash="3301649319", - metadata_hash="3575333731", + data_hash="2406542604", + metadata_hash="3341445192", ) assert fingerprint == original_fingerprint @@ -941,7 +943,7 @@ def test_fingerprint(model: Model, parent_model: Model): model = SqlModel(**{**model.dict(), "query": parse_one("select 1, ds -- annotation")}) fingerprint = fingerprint_from_node(model, nodes={}) assert new_fingerprint != fingerprint - assert new_fingerprint.data_hash == fingerprint.data_hash + assert new_fingerprint.data_hash != fingerprint.data_hash assert new_fingerprint.metadata_hash != fingerprint.metadata_hash model = SqlModel( @@ -951,14 +953,14 @@ def test_fingerprint(model: Model, parent_model: Model): assert new_fingerprint != fingerprint assert new_fingerprint.data_hash != fingerprint.data_hash assert new_fingerprint.metadata_hash != fingerprint.metadata_hash - assert fingerprint.metadata_hash == original_fingerprint.metadata_hash + assert fingerprint.metadata_hash != original_fingerprint.metadata_hash model = SqlModel(**{**original_model.dict(), "post_statements": [parse_one("DROP TABLE test")]}) fingerprint = fingerprint_from_node(model, nodes={}) assert new_fingerprint != fingerprint assert new_fingerprint.data_hash != fingerprint.data_hash assert new_fingerprint.metadata_hash != fingerprint.metadata_hash - assert fingerprint.metadata_hash == original_fingerprint.metadata_hash + assert fingerprint.metadata_hash != original_fingerprint.metadata_hash def test_fingerprint_seed_model(): @@ -1013,8 +1015,8 @@ def test_fingerprint_jinja_macros(model: Model): } ) original_fingerprint = SnapshotFingerprint( - data_hash="2908339239", - metadata_hash="3575333731", + data_hash="93332825", + metadata_hash="3341445192", ) fingerprint = fingerprint_from_node(model, nodes={}) diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 60908ed7c4..9b1e81c0f4 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -252,6 +252,9 @@ def increment_stage_counter(evaluator) -> None: snapshot = make_snapshot(model) snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + snapshot.model.render_pre_statements() + assert f"RuntimeStage value: {RuntimeStage.LOADING.value}" in capsys.readouterr().out evaluator.create([snapshot], {}) diff --git a/tests/core/test_table_diff.py b/tests/core/test_table_diff.py index 73fd37a2f7..839cbb415e 100644 --- a/tests/core/test_table_diff.py +++ b/tests/core/test_table_diff.py @@ -10,6 +10,7 @@ from sqlmesh.core.context import Context from sqlmesh.core.config import AutoCategorizationMode, CategorizerConfig, DuckDBConnectionConfig from sqlmesh.core.model import SqlModel, load_sql_based_model +from sqlmesh.core.model.common import ParsableSql from sqlmesh.core.table_diff import TableDiff, SchemaDiff import numpy as np # noqa: TID253 from sqlmesh.utils.errors import SQLMeshError @@ -48,8 +49,14 @@ def capture_console_output(method_name: str, **kwargs) -> str: def test_data_diff(sushi_context_fixed_date, capsys, caplog): model = sushi_context_fixed_date.models['"memory"."sushi"."customer_revenue_by_day"'] - model.query.select(exp.cast("'1'", "VARCHAR").as_("modified_col"), "1 AS y", copy=False) - sushi_context_fixed_date.upsert_model(model) + sushi_context_fixed_date.upsert_model( + model, + query_=ParsableSql( + sql=model.query.select(exp.cast("'1'", "VARCHAR").as_("modified_col"), "1 AS y").sql( + model.dialect + ) + ), + ) sushi_context_fixed_date.plan( "source_dev", diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 1b5425068f..d889c7bb33 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -29,6 +29,7 @@ from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.macros import MacroEvaluator, macro from sqlmesh.core.model import Model, SqlModel, load_sql_based_model, model +from sqlmesh.core.model.common import ParsableSql from sqlmesh.core.test.definition import ModelTest, PythonModelTest, SqlModelTest from sqlmesh.core.test.result import ModelTextTestResult from sqlmesh.core.test.context import TestExecutionContext @@ -1985,12 +1986,18 @@ def test_test_generation(tmp_path: Path) -> None: ) context = Context(paths=tmp_path, config=config) - query = context.get_model("sqlmesh_example.full_model").render_query() + model = context.get_model("sqlmesh_example.full_model") + query = model.render_query() assert isinstance(query, exp.Query) context.upsert_model( "sqlmesh_example.full_model", - query=exp.select(*query.named_selects).from_("cte").with_("cte", as_=query), + query_=ParsableSql( + sql=exp.select(*query.named_selects) + .from_("cte") + .with_("cte", as_=query) + .sql(dialect=model.dialect) + ), ) context.plan(auto_apply=True) diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index f78419889d..ce357f6d36 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -16,6 +16,7 @@ from sqlmesh.core.config import CategorizerConfig, Config, ModelDefaultsConfig, LinterConfig from sqlmesh.core.engine_adapter.shared import DataObject from sqlmesh.core.user import User, UserRole +from sqlmesh.core.model.common import ParsableSql from sqlmesh.integrations.github.cicd import command from sqlmesh.integrations.github.cicd.config import GithubCICDBotConfig, MergeMethod from sqlmesh.integrations.github.cicd.controller import ( @@ -249,8 +250,10 @@ def test_merge_pr_has_non_breaking_change( ] # Make a non-breaking change model = controller._context.get_model("sushi.waiter_revenue_by_day").copy() - model.query.select(exp.alias_("1", "new_col"), copy=False) - controller._context.upsert_model(model) + controller._context.upsert_model( + model, + query_=ParsableSql(sql=model.query.select(exp.alias_("1", "new_col")).sql(model.dialect)), + ) github_output_file = tmp_path / "github_output.txt" @@ -458,8 +461,10 @@ def test_merge_pr_has_non_breaking_change_diff_start( ] # Make a non-breaking change model = controller._context.get_model("sushi.waiter_revenue_by_day").copy() - model.query.select(exp.alias_("1", "new_col"), copy=False) - controller._context.upsert_model(model) + controller._context.upsert_model( + model, + query_=ParsableSql(sql=model.query.select(exp.alias_("1", "new_col")).sql(model.dialect)), + ) github_output_file = tmp_path / "github_output.txt" @@ -666,8 +671,10 @@ def test_merge_pr_has_non_breaking_change_no_categorization( ] # Make a non-breaking change model = controller._context.get_model("sushi.waiter_revenue_by_day").copy() - model.query.select(exp.alias_("1", "new_col"), copy=False) - controller._context.upsert_model(model) + controller._context.upsert_model( + model, + query_=ParsableSql(sql=model.query.select(exp.alias_("1", "new_col")).sql(model.dialect)), + ) github_output_file = tmp_path / "github_output.txt" @@ -983,8 +990,10 @@ def test_no_merge_since_no_deploy_signal( ] # Make a non-breaking change model = controller._context.get_model("sushi.waiter_revenue_by_day").copy() - model.query.select(exp.alias_("1", "new_col"), copy=False) - controller._context.upsert_model(model) + controller._context.upsert_model( + model, + query_=ParsableSql(sql=model.query.select(exp.alias_("1", "new_col")).sql(model.dialect)), + ) github_output_file = tmp_path / "github_output.txt" @@ -1183,8 +1192,10 @@ def test_no_merge_since_no_deploy_signal_no_approvers_defined( controller._context.users = [User(username="test", github_username="test_github", roles=[])] # Make a non-breaking change model = controller._context.get_model("sushi.waiter_revenue_by_day").copy() - model.query.select(exp.alias_("1", "new_col"), copy=False) - controller._context.upsert_model(model) + controller._context.upsert_model( + model, + query_=ParsableSql(sql=model.query.select(exp.alias_("1", "new_col")).sql(model.dialect)), + ) github_output_file = tmp_path / "github_output.txt" @@ -1357,8 +1368,10 @@ def test_deploy_comment_pre_categorized( controller._context.users = [User(username="test", github_username="test_github", roles=[])] # Make a non-breaking change model = controller._context.get_model("sushi.waiter_revenue_by_day").copy() - model.query.select(exp.alias_("1", "new_col"), copy=False) - controller._context.upsert_model(model) + controller._context.upsert_model( + model, + query_=ParsableSql(sql=model.query.select(exp.alias_("1", "new_col")).sql(model.dialect)), + ) # Manually categorize the change as non-breaking and don't backfill anything controller._context.plan( @@ -1557,8 +1570,12 @@ def test_error_msg_when_applying_plan_with_bug( ] # Make an error by adding a column that doesn't exist model = controller._context.get_model("sushi.waiter_revenue_by_day").copy() - model.query.select(exp.alias_("non_existing_col", "new_col"), copy=False) - controller._context.upsert_model(model) + controller._context.upsert_model( + model, + query_=ParsableSql( + sql=model.query.select(exp.alias_("non_existing_col", "new_col")).sql(model.dialect) + ), + ) github_output_file = tmp_path / "github_output.txt" @@ -1716,8 +1733,10 @@ def test_overlapping_changes_models( # These changes have shared children and this ensures we don't repeat the children in the output # Make a non-breaking change model = controller._context.get_model("sushi.customers").copy() - model.query.select(exp.alias_("1", "new_col"), copy=False) - controller._context.upsert_model(model) + controller._context.upsert_model( + model, + query_=ParsableSql(sql=model.query.select(exp.alias_("1", "new_col")).sql(model.dialect)), + ) # Make a breaking change model = controller._context.get_model("sushi.waiter_names").copy() @@ -2283,8 +2302,10 @@ def test_has_required_approval_but_not_base_branch( ] # Make a non-breaking change model = controller._context.get_model("sushi.waiter_revenue_by_day").copy() - model.query.select(exp.alias_("1", "new_col"), copy=False) - controller._context.upsert_model(model) + controller._context.upsert_model( + model, + query_=ParsableSql(sql=model.query.select(exp.alias_("1", "new_col")).sql(model.dialect)), + ) github_output_file = tmp_path / "github_output.txt" From 7cdbcde13ff06d50da386969d13d4717a7301a80 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:09:42 -0700 Subject: [PATCH 0802/1056] fix: trino catalog lookup (#5283) --- sqlmesh/core/engine_adapter/trino.py | 2 +- tests/core/engine_adapter/test_trino.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index 90b3da5240..0e6853dd4a 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -128,7 +128,7 @@ def replace_query( supports_replace_table_override: t.Optional[bool] = None, **kwargs: t.Any, ) -> None: - catalog_type = self.get_catalog_type(self.get_catalog_type_from_table(table_name)) + catalog_type = self.get_catalog_type_from_table(table_name) # User may have a custom catalog type name so we are assuming they keep the catalog type still in the name # Ex: `acme_iceberg` would be identified as an iceberg catalog and therefore supports replace table supports_replace_table_override = None diff --git a/tests/core/engine_adapter/test_trino.py b/tests/core/engine_adapter/test_trino.py index 930834c8d1..07c4657eb3 100644 --- a/tests/core/engine_adapter/test_trino.py +++ b/tests/core/engine_adapter/test_trino.py @@ -70,8 +70,7 @@ def test_get_catalog_type( "sqlmesh.core.engine_adapter.trino.TrinoEngineAdapter.get_current_catalog", return_value=f"system_{storage_type}", ) - expected_current_type = storage_type - assert adapter.current_catalog_type == expected_current_type + assert adapter.current_catalog_type == storage_type def test_get_catalog_type_cached( @@ -114,8 +113,7 @@ def test_partitioned_by_hive_delta( "sqlmesh.core.engine_adapter.trino.TrinoEngineAdapter.get_current_catalog", return_value=f"datalake_{storage_type}", ) - expected_type = storage_type - assert adapter.get_catalog_type(f"datalake_{storage_type}") == expected_type + assert adapter.get_catalog_type(f"datalake_{storage_type}") == storage_type columns_to_types = { "cola": exp.DataType.build("INT"), From b0c623e690882031a397d32e0165f2a9f06ed322 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 2 Sep 2025 15:42:51 -0700 Subject: [PATCH 0803/1056] Fix!: Limit the number of fetched full snapshots when deleting expired snapshots (#5281) --- sqlmesh/core/state_sync/base.py | 7 +- sqlmesh/core/state_sync/cache.py | 7 +- sqlmesh/core/state_sync/db/facade.py | 16 +- sqlmesh/core/state_sync/db/snapshot.py | 139 ++++++------------ ...add_dev_version_and_fingerprint_columns.py | 116 +++++++++++++++ tests/core/state_sync/test_state_sync.py | 41 ++++-- 6 files changed, 200 insertions(+), 126 deletions(-) create mode 100644 sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index 6c2097d760..4d3d51a469 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -292,7 +292,7 @@ def export(self, environment_names: t.Optional[t.List[str]] = None) -> StateStre @abc.abstractmethod def get_expired_snapshots( - self, current_ts: int, ignore_ttl: bool = False + self, current_ts: t.Optional[int] = None, ignore_ttl: bool = False ) -> t.List[SnapshotTableCleanupTask]: """Aggregates the id's of the expired snapshots and creates a list of table cleanup tasks. @@ -341,7 +341,7 @@ def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None: @abc.abstractmethod def delete_expired_snapshots( self, ignore_ttl: bool = False, current_ts: t.Optional[int] = None - ) -> t.List[SnapshotTableCleanupTask]: + ) -> None: """Removes expired snapshots. Expired snapshots are snapshots that have exceeded their time-to-live @@ -350,9 +350,6 @@ def delete_expired_snapshots( Args: ignore_ttl: Ignore the TTL on the snapshot when considering it expired. This has the effect of deleting all snapshots that are not referenced in any environment - - Returns: - The list of snapshot table cleanup tasks. """ @abc.abstractmethod diff --git a/sqlmesh/core/state_sync/cache.py b/sqlmesh/core/state_sync/cache.py index cc6a0fcb86..8aa5054e13 100644 --- a/sqlmesh/core/state_sync/cache.py +++ b/sqlmesh/core/state_sync/cache.py @@ -8,7 +8,6 @@ SnapshotId, SnapshotIdLike, SnapshotInfoLike, - SnapshotTableCleanupTask, ) from sqlmesh.core.snapshot.definition import Interval, SnapshotIntervals from sqlmesh.core.state_sync.base import DelegatingStateSync, StateSync @@ -109,12 +108,10 @@ def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None: def delete_expired_snapshots( self, ignore_ttl: bool = False, current_ts: t.Optional[int] = None - ) -> t.List[SnapshotTableCleanupTask]: + ) -> None: current_ts = current_ts or now_timestamp() self.snapshot_cache.clear() - return self.state_sync.delete_expired_snapshots( - current_ts=current_ts, ignore_ttl=ignore_ttl - ) + self.state_sync.delete_expired_snapshots(current_ts=current_ts, ignore_ttl=ignore_ttl) def add_snapshots_intervals(self, snapshots_intervals: t.Sequence[SnapshotIntervals]) -> None: for snapshot_intervals in snapshots_intervals: diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 858b1aa072..898ba75651 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -262,8 +262,9 @@ def invalidate_environment(self, name: str, protect_prod: bool = True) -> None: self.environment_state.invalidate_environment(name, protect_prod) def get_expired_snapshots( - self, current_ts: int, ignore_ttl: bool = False + self, current_ts: t.Optional[int] = None, ignore_ttl: bool = False ) -> t.List[SnapshotTableCleanupTask]: + current_ts = current_ts or now_timestamp() return self.snapshot_state.get_expired_snapshots( self.environment_state.get_environments(), current_ts=current_ts, ignore_ttl=ignore_ttl ) @@ -274,16 +275,13 @@ def get_expired_environments(self, current_ts: int) -> t.List[EnvironmentSummary @transactional() def delete_expired_snapshots( self, ignore_ttl: bool = False, current_ts: t.Optional[int] = None - ) -> t.List[SnapshotTableCleanupTask]: + ) -> None: current_ts = current_ts or now_timestamp() - expired_snapshot_ids, cleanup_targets = self.snapshot_state._get_expired_snapshots( + for expired_snapshot_ids, cleanup_targets in self.snapshot_state._get_expired_snapshots( self.environment_state.get_environments(), ignore_ttl=ignore_ttl, current_ts=current_ts - ) - - self.snapshot_state.delete_snapshots(expired_snapshot_ids) - self.interval_state.cleanup_intervals(cleanup_targets, expired_snapshot_ids) - - return cleanup_targets + ): + self.snapshot_state.delete_snapshots(expired_snapshot_ids) + self.interval_state.cleanup_intervals(cleanup_targets, expired_snapshot_ids) @transactional() def delete_expired_environments( diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 8d504993fc..af10f0192e 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -17,7 +17,6 @@ fetchall, create_batches, ) -from sqlmesh.core.node import IntervalUnit from sqlmesh.core.environment import Environment from sqlmesh.core.model import SeedModel, ModelKindName from sqlmesh.core.snapshot.cache import SnapshotCache @@ -30,7 +29,6 @@ Snapshot, SnapshotId, SnapshotFingerprint, - SnapshotChangeCategory, ) from sqlmesh.utils.migration import index_text_type, blob_text_type from sqlmesh.utils.date import now_timestamp, TimeLike, to_timestamp @@ -46,6 +44,9 @@ class SnapshotState: SNAPSHOT_BATCH_SIZE = 1000 + # Use a smaller batch size for expired snapshots to account for fetching + # of all snapshots that share the same version. + EXPIRED_SNAPSHOT_BATCH_SIZE = 200 def __init__( self, @@ -63,6 +64,7 @@ def __init__( "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), "version": exp.DataType.build(index_type), + "dev_version": exp.DataType.build(index_type), "snapshot": exp.DataType.build(blob_type), "kind_name": exp.DataType.build("text"), "updated_ts": exp.DataType.build("bigint"), @@ -70,6 +72,7 @@ def __init__( "ttl_ms": exp.DataType.build("bigint"), "unrestorable": exp.DataType.build("boolean"), "forward_only": exp.DataType.build("boolean"), + "fingerprint": exp.DataType.build(blob_type), } self._auto_restatement_columns_to_types = { @@ -175,19 +178,21 @@ def get_expired_snapshots( The set of expired snapshot ids. The list of table cleanup tasks. """ - _, cleanup_targets = self._get_expired_snapshots( + all_cleanup_targets = [] + for _, cleanup_targets in self._get_expired_snapshots( environments=environments, current_ts=current_ts, ignore_ttl=ignore_ttl, - ) - return cleanup_targets + ): + all_cleanup_targets.extend(cleanup_targets) + return all_cleanup_targets def _get_expired_snapshots( self, environments: t.Iterable[Environment], current_ts: int, ignore_ttl: bool = False, - ) -> t.Tuple[t.Set[SnapshotId], t.List[SnapshotTableCleanupTask]]: + ) -> t.Iterator[t.Tuple[t.Set[SnapshotId], t.List[SnapshotTableCleanupTask]]]: expired_query = exp.select("name", "identifier", "version").from_(self.snapshots_table) if not ignore_ttl: @@ -202,7 +207,7 @@ def _get_expired_snapshots( for name, identifier, version in fetchall(self.engine_adapter, expired_query) } if not expired_candidates: - return set(), [] + return promoted_snapshot_ids = { snapshot.snapshot_id @@ -218,10 +223,8 @@ def _is_snapshot_used(snapshot: SharedVersionSnapshot) -> bool: unique_expired_versions = unique(expired_candidates.values()) version_batches = create_batches( - unique_expired_versions, batch_size=self.SNAPSHOT_BATCH_SIZE + unique_expired_versions, batch_size=self.EXPIRED_SNAPSHOT_BATCH_SIZE ) - cleanup_targets = [] - expired_snapshot_ids = set() for versions_batch in version_batches: snapshots = self._get_snapshots_with_same_version(versions_batch) @@ -232,8 +235,9 @@ def _is_snapshot_used(snapshot: SharedVersionSnapshot) -> bool: snapshots_by_dev_version[(s.name, s.dev_version)].add(s.snapshot_id) expired_snapshots = [s for s in snapshots if not _is_snapshot_used(s)] - expired_snapshot_ids.update([s.snapshot_id for s in expired_snapshots]) + all_expired_snapshot_ids = {s.snapshot_id for s in expired_snapshots} + cleanup_targets: t.List[t.Tuple[SnapshotId, bool]] = [] for snapshot in expired_snapshots: shared_version_snapshots = snapshots_by_version[(snapshot.name, snapshot.version)] shared_version_snapshots.discard(snapshot.snapshot_id) @@ -244,14 +248,30 @@ def _is_snapshot_used(snapshot: SharedVersionSnapshot) -> bool: shared_dev_version_snapshots.discard(snapshot.snapshot_id) if not shared_dev_version_snapshots: - cleanup_targets.append( - SnapshotTableCleanupTask( - snapshot=snapshot.full_snapshot.table_info, - dev_table_only=bool(shared_version_snapshots), - ) + dev_table_only = bool(shared_version_snapshots) + cleanup_targets.append((snapshot.snapshot_id, dev_table_only)) + + snapshot_ids_to_cleanup = [snapshot_id for snapshot_id, _ in cleanup_targets] + for snapshot_id_batch in create_batches( + snapshot_ids_to_cleanup, batch_size=self.SNAPSHOT_BATCH_SIZE + ): + snapshot_id_batch_set = set(snapshot_id_batch) + full_snapshots = self._get_snapshots(snapshot_id_batch_set) + cleanup_tasks = [ + SnapshotTableCleanupTask( + snapshot=full_snapshots[snapshot_id].table_info, + dev_table_only=dev_table_only, ) + for snapshot_id, dev_table_only in cleanup_targets + if snapshot_id in full_snapshots + ] + all_expired_snapshot_ids -= snapshot_id_batch_set + yield snapshot_id_batch_set, cleanup_tasks - return expired_snapshot_ids, cleanup_targets + if all_expired_snapshot_ids: + # Remaining expired snapshots for which there are no tables + # to cleanup + yield all_expired_snapshot_ids, [] def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None: """Deletes snapshots. @@ -593,14 +613,11 @@ def _get_snapshots_with_same_version( ): query = ( exp.select( - "snapshot", "name", "identifier", "version", - "updated_ts", - "unpaused_ts", - "unrestorable", - "forward_only", + "dev_version", + "fingerprint", ) .from_(exp.to_table(self.snapshots_table).as_("snapshots")) .where(where) @@ -611,17 +628,14 @@ def _get_snapshots_with_same_version( snapshot_rows.extend(fetchall(self.engine_adapter, query)) return [ - SharedVersionSnapshot.from_snapshot_record( + SharedVersionSnapshot( name=name, identifier=identifier, version=version, - updated_ts=updated_ts, - unpaused_ts=unpaused_ts, - unrestorable=unrestorable, - forward_only=forward_only, - snapshot=snapshot, + dev_version=dev_version, + fingerprint=SnapshotFingerprint.parse_raw(fingerprint), ) - for snapshot, name, identifier, version, updated_ts, unpaused_ts, unrestorable, forward_only in snapshot_rows + for name, identifier, version, dev_version, fingerprint in snapshot_rows ] @@ -676,6 +690,8 @@ def _snapshots_to_df(snapshots: t.Iterable[Snapshot]) -> pd.DataFrame: "ttl_ms": snapshot.ttl_ms, "unrestorable": snapshot.unrestorable, "forward_only": snapshot.forward_only, + "dev_version": snapshot.dev_version, + "fingerprint": snapshot.fingerprint.json(), } for snapshot in snapshots ] @@ -707,76 +723,11 @@ class SharedVersionSnapshot(PydanticModel): dev_version_: t.Optional[str] = Field(alias="dev_version") identifier: str fingerprint: SnapshotFingerprint - interval_unit: IntervalUnit - change_category: SnapshotChangeCategory - updated_ts: int - unpaused_ts: t.Optional[int] - unrestorable: bool - disable_restatement: bool - effective_from: t.Optional[TimeLike] - raw_snapshot: t.Dict[str, t.Any] - forward_only: bool @property def snapshot_id(self) -> SnapshotId: return SnapshotId(name=self.name, identifier=self.identifier) - @property - def is_forward_only(self) -> bool: - return self.forward_only or self.change_category == SnapshotChangeCategory.FORWARD_ONLY - - @property - def normalized_effective_from_ts(self) -> t.Optional[int]: - return ( - to_timestamp(self.interval_unit.cron_floor(self.effective_from)) - if self.effective_from - else None - ) - @property def dev_version(self) -> str: return self.dev_version_ or self.fingerprint.to_version() - - @property - def full_snapshot(self) -> Snapshot: - return Snapshot( - **{ - **self.raw_snapshot, - "updated_ts": self.updated_ts, - "unpaused_ts": self.unpaused_ts, - "unrestorable": self.unrestorable, - "forward_only": self.forward_only, - } - ) - - @classmethod - def from_snapshot_record( - cls, - *, - name: str, - identifier: str, - version: str, - updated_ts: int, - unpaused_ts: t.Optional[int], - unrestorable: bool, - forward_only: bool, - snapshot: str, - ) -> SharedVersionSnapshot: - raw_snapshot = json.loads(snapshot) - raw_node = raw_snapshot["node"] - return SharedVersionSnapshot( - name=name, - version=version, - dev_version=raw_snapshot.get("dev_version"), - identifier=identifier, - fingerprint=raw_snapshot["fingerprint"], - interval_unit=raw_node.get("interval_unit", IntervalUnit.from_cron(raw_node["cron"])), - change_category=raw_snapshot["change_category"], - updated_ts=updated_ts, - unpaused_ts=unpaused_ts, - unrestorable=unrestorable, - disable_restatement=raw_node.get("kind", {}).get("disable_restatement", False), - effective_from=raw_snapshot.get("effective_from"), - raw_snapshot=raw_snapshot, - forward_only=forward_only, - ) diff --git a/sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py b/sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py new file mode 100644 index 0000000000..0163b36ab4 --- /dev/null +++ b/sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py @@ -0,0 +1,116 @@ +"""Add dev_version and fingerprint columns to the snapshots table.""" + +import json + +from sqlglot import exp + +from sqlmesh.utils.migration import index_text_type, blob_text_type + + +def migrate(state_sync, **kwargs): # type: ignore + import pandas as pd + + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + index_type = index_text_type(engine_adapter.dialect) + blob_type = blob_text_type(engine_adapter.dialect) + + add_dev_version_exp = exp.Alter( + this=exp.to_table(snapshots_table), + kind="TABLE", + actions=[ + exp.ColumnDef( + this=exp.to_column("dev_version"), + kind=exp.DataType.build(index_type), + ) + ], + ) + engine_adapter.execute(add_dev_version_exp) + + add_fingerprint_exp = exp.Alter( + this=exp.to_table(snapshots_table), + kind="TABLE", + actions=[ + exp.ColumnDef( + this=exp.to_column("fingerprint"), + kind=exp.DataType.build(blob_type), + ) + ], + ) + engine_adapter.execute(add_fingerprint_exp) + + new_snapshots = [] + + for ( + name, + identifier, + version, + snapshot, + kind_name, + updated_ts, + unpaused_ts, + ttl_ms, + unrestorable, + forward_only, + _, + _, + ) in engine_adapter.fetchall( + exp.select( + "name", + "identifier", + "version", + "snapshot", + "kind_name", + "updated_ts", + "unpaused_ts", + "ttl_ms", + "unrestorable", + "forward_only", + "dev_version", + "fingerprint", + ).from_(snapshots_table), + quote_identifiers=True, + ): + parsed_snapshot = json.loads(snapshot) + new_snapshots.append( + { + "name": name, + "identifier": identifier, + "version": version, + "snapshot": snapshot, + "kind_name": kind_name, + "updated_ts": updated_ts, + "unpaused_ts": unpaused_ts, + "ttl_ms": ttl_ms, + "unrestorable": unrestorable, + "forward_only": forward_only, + "dev_version": parsed_snapshot.get("dev_version"), + "fingerprint": json.dumps(parsed_snapshot.get("fingerprint")), + } + ) + + if new_snapshots: + engine_adapter.delete_from(snapshots_table, "TRUE") + + engine_adapter.insert_append( + snapshots_table, + pd.DataFrame(new_snapshots), + target_columns_to_types={ + "name": exp.DataType.build(index_type), + "identifier": exp.DataType.build(index_type), + "version": exp.DataType.build(index_type), + "snapshot": exp.DataType.build(blob_type), + "kind_name": exp.DataType.build(index_type), + "updated_ts": exp.DataType.build("bigint"), + "unpaused_ts": exp.DataType.build("bigint"), + "ttl_ms": exp.DataType.build("bigint"), + "unrestorable": exp.DataType.build("boolean"), + "forward_only": exp.DataType.build("boolean"), + "dev_version": exp.DataType.build(index_type), + "fingerprint": exp.DataType.build(blob_type), + }, + ) diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index e7046be13d..be8e4ad3e0 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -1156,10 +1156,11 @@ def test_delete_expired_snapshots(state_sync: EngineAdapterStateSync, make_snaps new_snapshot.snapshot_id, } - assert state_sync.delete_expired_snapshots() == [ + assert state_sync.get_expired_snapshots() == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True), SnapshotTableCleanupTask(snapshot=new_snapshot.table_info, dev_table_only=False), ] + state_sync.delete_expired_snapshots() assert not state_sync.get_snapshots(all_snapshots) @@ -1186,9 +1187,10 @@ def test_delete_expired_snapshots_seed( state_sync.push_snapshots(all_snapshots) assert set(state_sync.get_snapshots(all_snapshots)) == {snapshot.snapshot_id} - assert state_sync.delete_expired_snapshots() == [ + assert state_sync.get_expired_snapshots() == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=False), ] + state_sync.delete_expired_snapshots() assert not state_sync.get_snapshots(all_snapshots) @@ -1226,10 +1228,11 @@ def test_delete_expired_snapshots_batching( snapshot_b.snapshot_id, } - assert state_sync.delete_expired_snapshots() == [ + assert state_sync.get_expired_snapshots() == [ SnapshotTableCleanupTask(snapshot=snapshot_a.table_info, dev_table_only=False), SnapshotTableCleanupTask(snapshot=snapshot_b.table_info, dev_table_only=False), ] + state_sync.delete_expired_snapshots() assert not state_sync.get_snapshots(all_snapshots) @@ -1262,7 +1265,8 @@ def test_delete_expired_snapshots_promoted( state_sync.promote(env) all_snapshots = [snapshot] - assert not state_sync.delete_expired_snapshots() + assert not state_sync.get_expired_snapshots() + state_sync.delete_expired_snapshots() assert set(state_sync.get_snapshots(all_snapshots)) == {snapshot.snapshot_id} env.snapshots_ = [] @@ -1271,9 +1275,10 @@ def test_delete_expired_snapshots_promoted( now_timestamp_mock = mocker.patch("sqlmesh.core.state_sync.db.facade.now_timestamp") now_timestamp_mock.return_value = now_timestamp() + 11000 - assert state_sync.delete_expired_snapshots() == [ + assert state_sync.get_expired_snapshots() == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=False) ] + state_sync.delete_expired_snapshots() assert not state_sync.get_snapshots(all_snapshots) @@ -1310,9 +1315,10 @@ def test_delete_expired_snapshots_dev_table_cleanup_only( new_snapshot.snapshot_id, } - assert state_sync.delete_expired_snapshots() == [ + assert state_sync.get_expired_snapshots() == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True) ] + state_sync.delete_expired_snapshots() assert set(state_sync.get_snapshots(all_snapshots)) == {new_snapshot.snapshot_id} @@ -1351,7 +1357,8 @@ def test_delete_expired_snapshots_shared_dev_table( new_snapshot.snapshot_id, } - assert not state_sync.delete_expired_snapshots() # No dev table cleanup + assert not state_sync.get_expired_snapshots() # No dev table cleanup + state_sync.delete_expired_snapshots() assert set(state_sync.get_snapshots(all_snapshots)) == {new_snapshot.snapshot_id} @@ -1396,13 +1403,17 @@ def test_delete_expired_snapshots_ignore_ttl( state_sync.promote(env) # default TTL = 1 week, nothing to clean up yet if we take TTL into account - assert not state_sync.delete_expired_snapshots() + assert not state_sync.get_expired_snapshots() + state_sync.delete_expired_snapshots() + assert state_sync.snapshots_exist([snapshot_c.snapshot_id]) == {snapshot_c.snapshot_id} # If we ignore TTL, only snapshot_c should get cleaned up because snapshot_a and snapshot_b are part of an environment assert snapshot_a.table_info != snapshot_b.table_info != snapshot_c.table_info - assert state_sync.delete_expired_snapshots(ignore_ttl=True) == [ + assert state_sync.get_expired_snapshots(ignore_ttl=True) == [ SnapshotTableCleanupTask(snapshot=snapshot_c.table_info, dev_table_only=False) ] + state_sync.delete_expired_snapshots(ignore_ttl=True) + assert not state_sync.snapshots_exist([snapshot_c.snapshot_id]) def test_delete_expired_snapshots_cleanup_intervals( @@ -1465,10 +1476,11 @@ def test_delete_expired_snapshots_cleanup_intervals( ] assert not stored_new_snapshot.dev_intervals - assert state_sync.delete_expired_snapshots() == [ + assert state_sync.get_expired_snapshots() == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True), SnapshotTableCleanupTask(snapshot=new_snapshot.table_info, dev_table_only=False), ] + state_sync.delete_expired_snapshots() assert not get_snapshot_intervals(snapshot) @@ -1552,9 +1564,10 @@ def test_delete_expired_snapshots_cleanup_intervals_shared_version( ) # Delete the expired snapshot - assert state_sync.delete_expired_snapshots() == [ + assert state_sync.get_expired_snapshots() == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True), ] + state_sync.delete_expired_snapshots() assert not state_sync.get_snapshots([snapshot]) # Check new snapshot's intervals @@ -1671,7 +1684,8 @@ def test_delete_expired_snapshots_cleanup_intervals_shared_dev_version( ) # Delete the expired snapshot - assert state_sync.delete_expired_snapshots() == [] + assert state_sync.get_expired_snapshots() == [] + state_sync.delete_expired_snapshots() assert not state_sync.get_snapshots([snapshot]) # Check new snapshot's intervals @@ -1764,9 +1778,10 @@ def test_compact_intervals_after_cleanup( state_sync.add_interval(snapshot_c, "2023-01-07", "2023-01-09", is_dev=True) # Only the dev table of the original snapshot should be deleted - assert state_sync.delete_expired_snapshots() == [ + assert state_sync.get_expired_snapshots() == [ SnapshotTableCleanupTask(snapshot=snapshot_a.table_info, dev_table_only=True), ] + state_sync.delete_expired_snapshots() assert state_sync.engine_adapter.fetchone("SELECT COUNT(*) FROM sqlmesh._intervals")[0] == 5 # type: ignore From 9e9159d69bb9acc9c697fe08982cd7efb6bc80a6 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Tue, 2 Sep 2025 17:17:25 -0700 Subject: [PATCH 0804/1056] chore(web_common): tweak button styling (#5284) --- web/common/src/components/Button/Button.css | 8 ++++---- web/common/src/components/Button/Button.tsx | 2 +- web/common/src/components/CopyButton/CopyButton.tsx | 3 --- web/common/src/index.ts | 6 +++++- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/web/common/src/components/Button/Button.css b/web/common/src/components/Button/Button.css index 339c14675b..7e8b856bf3 100644 --- a/web/common/src/components/Button/Button.css +++ b/web/common/src/components/Button/Button.css @@ -4,7 +4,7 @@ --color-button-primary-hover: var(--color-action-hover); --color-button-primary-active: var(--color-action-active); - --color-button-secondary-background: var(--color-neutral-100); + --color-button-secondary-background: var(--color-neutral-10); --color-button-secondary-foreground: var(--color-prose); --color-button-secondary-hover: var(--color-neutral-125); --color-button-secondary-active: var(--color-neutral-150); @@ -14,7 +14,7 @@ --color-button-alternative-hover: var(--color-neutral-125); --color-button-alternative-active: var(--color-neutral-150); - --color-button-destructive-background: var(--color-neutral-100); + --color-button-destructive-background: var(--color-neutral-10); --color-button-destructive-foreground: var(--color-destructive-foreground); --color-button-destructive-hover: var(--color-neutral-125); --color-button-destructive-active: var(--color-neutral-150); @@ -26,6 +26,6 @@ --color-button-transparent-background: transparent; --color-button-transparent-foreground: var(--color-prose); - --color-button-secondary-hover: var(--color-neutral-125); - --color-button-secondary-active: var(--color-neutral-150); + --color-button-transparent-hover: var(--color-neutral-125); + --color-button-transparent-active: var(--color-neutral-150); } diff --git a/web/common/src/components/Button/Button.tsx b/web/common/src/components/Button/Button.tsx index 46f9c8cf1b..e4bebd0798 100644 --- a/web/common/src/components/Button/Button.tsx +++ b/web/common/src/components/Button/Button.tsx @@ -30,7 +30,7 @@ export const Button = React.forwardRef( @@ -20,7 +19,6 @@ export const CopyButton = React.forwardRef( size = 'xs', delay = 2000, disabled = false, - className, children, onClick, ...props @@ -53,7 +51,6 @@ export const CopyButton = React.forwardRef( onClick={copy} disabled={disabled || !!copied} {...props} - className={cn(className, copied && 'pointer-events-none')} > {children(copied != null)} diff --git a/web/common/src/index.ts b/web/common/src/index.ts index 6cd52abead..dce05a9c83 100644 --- a/web/common/src/index.ts +++ b/web/common/src/index.ts @@ -1,6 +1,10 @@ // Components export { Badge, type BadgeProps } from '@/components/Badge/Badge' -export { Button, type ButtonProps } from '@/components/Button/Button' +export { + Button, + type ButtonProps, + type ButtonVariant, +} from '@/components/Button/Button' export { CopyButton, type CopyButtonProps, From cabbd5c869d6e663b6db508de81a8d30f3f74c92 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:58:01 +0300 Subject: [PATCH 0805/1056] Fix: complete lookback support for custom materializations (#5278) --- sqlmesh/core/model/meta.py | 3 +- tests/core/test_model.py | 69 +++++++++++++++++++++++++++++++++++++ tests/core/test_snapshot.py | 37 ++++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 088398c388..11e384b813 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -29,7 +29,6 @@ SCDType2ByTimeKind, TimeColumn, ViewKind, - _IncrementalBy, model_kind_validator, OnAdditiveChange, ) @@ -414,7 +413,7 @@ def column_descriptions(self) -> t.Dict[str, str]: @property def lookback(self) -> int: """The incremental lookback window.""" - return (self.kind.lookback if isinstance(self.kind, _IncrementalBy) else 0) or 0 + return getattr(self.kind, "lookback", 0) or 0 def lookback_start(self, start: TimeLike) -> TimeLike: if self.lookback == 0: diff --git a/tests/core/test_model.py b/tests/core/test_model.py index be1df5f2d6..8794f38826 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -7686,6 +7686,75 @@ class MyTestStrategy(CustomMaterialization): ) +def test_custom_kind_lookback_property(): + """Test that CustomKind's lookback property is correctly accessed via ModelMeta.lookback. + + This test verifies the fix for issue #5268 where CustomKind models were not respecting + the lookback parameter because the isinstance check for _IncrementalBy failed. + """ + + # Test 1: CustomKind with lookback = 3 + class MyTestStrategy(CustomMaterialization): + pass + + expressions = d.parse( + """ + MODEL ( + name db.custom_table, + kind CUSTOM ( + materialization 'MyTestStrategy', + lookback 3 + ) + ); + SELECT a, b FROM upstream + """ + ) + + model = load_sql_based_model(expressions) + assert model.kind.is_custom + + # Verify that the kind itself has lookback = 3 + kind = t.cast(CustomKind, model.kind) + assert kind.lookback == 3 + + # The bug: model.lookback should return 3, but with the old implementation + # using isinstance(self.kind, _IncrementalBy), it would return 0 + assert model.lookback == 3, "CustomKind lookback not accessible via model.lookback property" + + # Test 2: CustomKind without lookback (should default to 0) + expressions_no_lookback = d.parse( + """ + MODEL ( + name db.custom_table_no_lookback, + kind CUSTOM ( + materialization 'MyTestStrategy' + ) + ); + SELECT a, b FROM upstream + """ + ) + + model_no_lookback = load_sql_based_model(expressions_no_lookback) + assert model_no_lookback.lookback == 0 + + # Test 3: Ensure IncrementalByTimeRangeKind still works correctly + incremental_expressions = d.parse( + """ + MODEL ( + name db.incremental_table, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + lookback 5 + ) + ); + SELECT ds, a, b FROM upstream + """ + ) + + incremental_model = load_sql_based_model(incremental_expressions) + assert incremental_model.lookback == 5 + + def test_time_column_format_in_custom_kind(): class TimeColumnCustomKind(CustomKind): # type: ignore[no-untyped-def] _time_column: TimeColumn diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index d63b642f60..9e36ecc3ae 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -681,6 +681,43 @@ def test_lookback(make_snapshot): assert snapshot.missing_intervals("2023-01-28", "2023-01-30", "2023-01-31 04:00:00") == [] +def test_lookback_custom_materialization(make_snapshot): + from sqlmesh import CustomMaterialization + + class MyTestStrategy(CustomMaterialization): + pass + + expressions = parse( + """ + MODEL ( + name name, + kind CUSTOM ( + materialization 'MyTestStrategy', + lookback 2 + ), + start '2023-01-01', + cron '0 5 * * *', + ); + + SELECT ds FROM parent.tbl + """ + ) + + snapshot = make_snapshot(load_sql_based_model(expressions)) + + assert snapshot.missing_intervals("2023-01-01", "2023-01-01") == [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + ] + + snapshot.add_interval("2023-01-01", "2023-01-04") + assert snapshot.missing_intervals("2023-01-01", "2023-01-04") == [] + assert snapshot.missing_intervals("2023-01-01", "2023-01-05") == [ + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + ] + + def test_seed_intervals(make_snapshot): snapshot_a = make_snapshot( SeedModel( From 7f5adcfcffc09c0087313c6ae78645cd4c428faa Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:37:33 +0300 Subject: [PATCH 0806/1056] Feat(dbt): Add config object to provide methods aligned with dbt (#5271) --- sqlmesh/dbt/builtin.py | 54 ++++++++++++++ tests/dbt/test_transformation.py | 116 ++++++++++++++++++++++++++++++- 2 files changed, 169 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 4edfea687a..24669807bb 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -164,6 +164,58 @@ def has_var(self, name: str) -> bool: return name in self.variables +class Config: + def __init__(self, config_dict: t.Dict[str, t.Any]) -> None: + self._config = config_dict + + def __call__(self, **kwargs: t.Any) -> str: + self._config.update(**kwargs) + return "" + + def set(self, name: str, value: t.Any) -> str: + self._config.update({name: value}) + return "" + + def _validate(self, name: str, validator: t.Callable, value: t.Optional[t.Any] = None) -> None: + try: + validator(value) + except Exception as e: + raise ConfigError(f"Config validation failed for '{name}': {e}") + + def require(self, name: str, validator: t.Optional[t.Callable] = None) -> t.Any: + if name not in self._config: + raise ConfigError(f"Missing required config: {name}") + + value = self._config[name] + + if validator is not None: + self._validate(name, validator, value) + + return value + + def get( + self, name: str, default: t.Any = None, validator: t.Optional[t.Callable] = None + ) -> t.Any: + value = self._config.get(name, default) + + if validator is not None and value is not None: + self._validate(name, validator, value) + + return value + + def persist_relation_docs(self) -> bool: + persist_docs = self.get("persist_docs", default={}) + if not isinstance(persist_docs, dict): + return False + return persist_docs.get("relation", False) + + def persist_column_docs(self) -> bool: + persist_docs = self.get("persist_docs", default={}) + if not isinstance(persist_docs, dict): + return False + return persist_docs.get("columns", False) + + def env_var(name: str, default: t.Optional[str] = None) -> t.Optional[str]: if name not in os.environ and default is None: raise ConfigError(f"Missing environment variable '{name}'") @@ -395,6 +447,8 @@ def create_builtin_globals( if variables is not None: builtin_globals["var"] = Var(variables) + builtin_globals["config"] = Config(jinja_globals.pop("config", {})) + deployability_index = ( jinja_globals.get("deployability_index") or DeployabilityIndex.all_deployable() ) diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 779160c27d..e81fcbe862 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -946,7 +946,7 @@ def test_schema_jinja(sushi_test_project: Project, assert_exp_eq): @pytest.mark.xdist_group("dbt_manifest") def test_config_jinja(sushi_test_project: Project): - hook = "{{ config(alias='bar') }} {{ config.alias }}" + hook = "{{ config(alias='bar') }} {{ config.get('alias') }}" model_config = ModelConfig( name="model", package_name="package", @@ -961,6 +961,120 @@ def test_config_jinja(sushi_test_project: Project): assert model.render_pre_statements()[0].sql() == '"bar"' +@pytest.mark.xdist_group("dbt_manifest") +def test_config_jinja_get_methods(sushi_test_project: Project): + model_config = ModelConfig( + name="model_conf", + package_name="package", + schema="sushi", + sql="""SELECT 1 AS one FROM foo""", + alias="model_alias", + **{ + "pre-hook": [ + "{{ config(materialized='incremental', unique_key='id') }}" + "{{ config.get('missed', 'a') + config.get('missed', default='b')}}", + "{{ config.set('alias', 'new_alias')}}", + "{{ config.get('package_name') + '_' + config.require('unique_key')}}", + "{{ config.get('alias') or 'default'}}", + ] + }, + **{"post-hook": "{{config.require('missing_key')}}"}, + ) + context = sushi_test_project.context + model = t.cast(SqlModel, model_config.to_sqlmesh(context)) + + assert model.render_pre_statements()[0].sql() == '"ab"' + assert model.render_pre_statements()[1].sql() == '"package_id"' + assert model.render_pre_statements()[2].sql() == '"new_alias"' + + with pytest.raises(ConfigError, match="Missing required config: missing_key"): + model.render_post_statements() + + # test get methods with operations + model_2_config = ModelConfig( + name="model_2", + package_name="package", + schema="sushi", + sql="""SELECT 1 AS one FROM foo""", + alias="mod", + materialized="table", + threads=8, + partition_by="date", + cluster_by=["user_id", "product_id"], + **{ + "pre-hook": [ + "{{ config.get('partition_by', default='none') }}", + "{{ config.get('cluster_by', default=[]) | length }}", + "{% if config.get('threads') > 4 %}high_threads{% else %}low_threads{% endif %}", + ] + }, + ) + model2 = t.cast(SqlModel, model_2_config.to_sqlmesh(context)) + + pre_statements2 = model2.render_pre_statements() + assert pre_statements2[0].sql() == "ARRAY('date')" + assert pre_statements2[1].sql() == "2" + assert pre_statements2[2].sql() == '"high_threads"' + + # test seting variable and conditional + model_invalid_timeout = ModelConfig( + name="invalid_timeout_test", + package_name="package", + schema="sushi", + sql="""SELECT 1 AS one FROM foo""", + alias="invalid_timeout_alias", + connection_timeout=44, + **{ + "pre-hook": [ + """ + {%- set value = config.require('connection_timeout') -%} + {%- set is_valid = value >= 10 and value <= 30 -%} + {%- if not is_valid -%} + {{ exceptions.raise_compiler_error("Validation failed for 'connection_timeout': Value must be between 10 and 30, got: " ~ value) }} + {%- endif -%} + {{ value }} + """, + ] + }, + ) + + model_invalid = t.cast(SqlModel, model_invalid_timeout.to_sqlmesh(context)) + with pytest.raises( + ConfigError, + match="Validation failed for 'connection_timeout': Value must be between 10 and 30, got: 44", + ): + model_invalid.render_pre_statements() + + # test persist_docs methods + model_config_persist = ModelConfig( + name="persist_docs_model", + package_name="package", + schema="sushi", + sql="""SELECT 1 AS one FROM foo""", + alias="persist_alias", + **{ + "pre-hook": [ + "{{ config(persist_docs={'relation': true, 'columns': true}) }}", + "{{ config.persist_relation_docs() }}", + "{{ config.persist_column_docs() }}", + "{{ config(persist_docs={'relation': false, 'columns': true}) }}", + "{{ config.persist_relation_docs() }}", + "{{ config.persist_column_docs() }}", + ] + }, + ) + model3 = t.cast(SqlModel, model_config_persist.to_sqlmesh(context)) + + pre_statements3 = model3.render_pre_statements() + + # it should filter out empty returns, so we get 4 statements + assert len(pre_statements3) == 4 + assert pre_statements3[0].sql() == "TRUE" + assert pre_statements3[1].sql() == "TRUE" + assert pre_statements3[2].sql() == "FALSE" + assert pre_statements3[3].sql() == "TRUE" + + @pytest.mark.xdist_group("dbt_manifest") def test_model_this(assert_exp_eq, sushi_test_project: Project): model_config = ModelConfig( From d774db9831d09683c2c92c48deb75c10aec36671 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 3 Sep 2025 08:00:13 -0700 Subject: [PATCH 0807/1056] Fix: Make sure that changes to seed models are reflected in the dev-only VDE mode (#5282) --- sqlmesh/core/console.py | 2 +- sqlmesh/core/plan/common.py | 5 +- sqlmesh/core/scheduler.py | 9 +++- sqlmesh/core/snapshot/evaluator.py | 27 +++++------ tests/core/test_integration.py | 75 ++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 18 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 0907b39987..af28f75932 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -2076,7 +2076,7 @@ def _show_categorized_snapshots(self, plan: Plan, default_catalog: t.Optional[st if text_diff: self._print("") self._print(Syntax(text_diff, "sql", word_wrap=True)) - self._print(tree) + self._print(tree) def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> None: """Displays the models with missing dates.""" diff --git a/sqlmesh/core/plan/common.py b/sqlmesh/core/plan/common.py index 8d31b0ead3..929837eb7e 100644 --- a/sqlmesh/core/plan/common.py +++ b/sqlmesh/core/plan/common.py @@ -5,7 +5,10 @@ def should_force_rebuild(old: Snapshot, new: Snapshot) -> bool: if new.is_view and new.is_indirect_non_breaking and not new.is_forward_only: - # View models always need to be rebuilt to reflect updated upstream dependencies. + # View models always need to be rebuilt to reflect updated upstream dependencies + return True + if new.is_seed: + # Seed models always need to be rebuilt to reflect changes in the seed file return True return is_breaking_kind_change(old, new) diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index dc6499c1a3..44d6b14c10 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -474,10 +474,17 @@ def run_merged_intervals( execution_time=execution_time, ) + # We only need to create physical tables if the snapshot is not representative or if it + # needs backfill + snapshots_to_create_candidates = [ + s + for s in selected_snapshots + if not deployability_index.is_representative(s) or s in batched_intervals + ] snapshots_to_create = { s.snapshot_id for s in self.snapshot_evaluator.get_snapshots_to_create( - selected_snapshots, deployability_index + snapshots_to_create_candidates, deployability_index ) } diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index c53c0a88db..6d6525a771 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -2102,21 +2102,18 @@ def create( return super().create(table_name, model, is_table_deployable, render_kwargs, **kwargs) - if is_table_deployable: - # For seeds we insert data at the time of table creation. - try: - for index, df in enumerate(model.render_seed()): - if index == 0: - self._replace_query_for_model( - model, table_name, df, render_kwargs, **kwargs - ) - else: - self.adapter.insert_append( - table_name, df, target_columns_to_types=model.columns_to_types - ) - except Exception: - self.adapter.drop_table(table_name) - raise + # For seeds we insert data at the time of table creation. + try: + for index, df in enumerate(model.render_seed()): + if index == 0: + self._replace_query_for_model(model, table_name, df, render_kwargs, **kwargs) + else: + self.adapter.insert_append( + table_name, df, target_columns_to_types=model.columns_to_types + ) + except Exception: + self.adapter.drop_table(table_name) + raise def insert( self, diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index c22e904374..c00733238a 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -2918,6 +2918,81 @@ def test_virtual_environment_mode_dev_only_model_kind_change_manual_categorizati ] +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_seed_model_change( + init_and_plan_context: t.Callable, +): + context, _ = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.load() + context.plan("prod", auto_apply=True, no_prompts=True) + + seed_model = context.get_model("sushi.waiter_names") + with open(seed_model.seed_path, "a") as fd: + fd.write("\n123,New Test Name") + + context.load() + seed_model_snapshot = context.get_snapshot("sushi.waiter_names") + plan = context.plan_builder("dev").build() + assert plan.directly_modified == {seed_model_snapshot.snapshot_id} + assert len(plan.missing_intervals) == 2 + context.apply(plan) + + actual_seed_df_in_dev = context.fetchdf("SELECT * FROM sushi__dev.waiter_names WHERE id = 123") + assert actual_seed_df_in_dev.to_dict("records") == [{"id": 123, "name": "New Test Name"}] + actual_seed_df_in_prod = context.fetchdf("SELECT * FROM sushi.waiter_names WHERE id = 123") + assert actual_seed_df_in_prod.empty + + plan = context.plan_builder("prod").build() + assert plan.directly_modified == {seed_model_snapshot.snapshot_id} + assert len(plan.missing_intervals) == 1 + assert plan.missing_intervals[0].snapshot_id == seed_model_snapshot.snapshot_id + context.apply(plan) + + actual_seed_df_in_prod = context.fetchdf("SELECT * FROM sushi.waiter_names WHERE id = 123") + assert actual_seed_df_in_prod.to_dict("records") == [{"id": 123, "name": "New Test Name"}] + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_change_downstream_of_seed( + init_and_plan_context: t.Callable, +): + """This test covers a scenario when a model downstream of a seed model is modified and explicitly selected + causing an (unhydrated) seed model to sourced from the state. If SQLMesh attempts to create + a table for the unchanged seed model, it will fail because the seed model is not hydrated. + """ + context, _ = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.load() + context.plan("prod", auto_apply=True, no_prompts=True) + + # Make sure that a different version of the seed model is loaded + seed_model = context.get_model("sushi.waiter_names") + seed_model = seed_model.copy(update={"stamp": "force new version"}) + context.upsert_model(seed_model) + + # Make a change to the downstream model + model = context.get_model("sushi.waiter_as_customer_by_day") + model = model.copy(update={"stamp": "force new version"}) + context.upsert_model(model) + + # It is important to clear the cache so that the hydrated seed model is not sourced from the cache + context.clear_caches() + + # Make sure to use the selector so that the seed model is sourced from the state + plan = context.plan_builder("dev", select_models=[model.name]).build() + assert len(plan.directly_modified) == 1 + assert list(plan.directly_modified)[0].name == model.fqn + assert len(plan.missing_intervals) == 1 + assert plan.missing_intervals[0].snapshot_id.name == model.fqn + + # Make sure there's no error when applying the plan + context.apply(plan) + context.plan("prod", auto_apply=True, no_prompts=True) + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_restatement_plan_ignores_changes(init_and_plan_context: t.Callable): context, plan = init_and_plan_context("examples/sushi") From d3079815cff6c73fe99f76c719967e83394d4258 Mon Sep 17 00:00:00 2001 From: Chris Rericha <67359577+crericha@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:46:36 -0400 Subject: [PATCH 0808/1056] Fix: Match dbt handling of cluster_by config for view materializations (#5266) --- sqlmesh/dbt/model.py | 28 ++++++++++++++++++---------- tests/dbt/test_transformation.py | 11 +++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 080900eace..9833b49876 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -2,6 +2,7 @@ import datetime import typing as t +import logging from sqlglot import exp from sqlglot.errors import SqlglotError @@ -34,6 +35,8 @@ from sqlmesh.core.audit.definition import ModelAudit from sqlmesh.dbt.context import DbtContext +logger = logging.getLogger(__name__) + INCREMENTAL_BY_TIME_STRATEGIES = set(["delete+insert", "insert_overwrite", "microbatch"]) INCREMENTAL_BY_UNIQUE_KEY_STRATEGIES = set(["merge"]) @@ -503,6 +506,7 @@ def to_sqlmesh( """Converts the dbt model into a SQLMesh model.""" model_dialect = self.dialect(context) query = d.jinja_query(self.sql_no_config) + kind = self.model_kind(context) optional_kwargs: t.Dict[str, t.Any] = {} physical_properties: t.Dict[str, t.Any] = {} @@ -522,15 +526,20 @@ def to_sqlmesh( optional_kwargs["partitioned_by"] = partitioned_by if self.cluster_by: - clustered_by = [] - for c in self.cluster_by: - try: - clustered_by.append(d.parse_one(c, dialect=model_dialect)) - except SqlglotError as e: - raise ConfigError( - f"Failed to parse model '{self.canonical_name(context)}' cluster_by field '{c}' in '{self.path}': {e}" - ) from e - optional_kwargs["clustered_by"] = clustered_by + if isinstance(kind, ViewKind): + logger.warning( + f"Ignoring cluster_by config for model '{self.name}'; cluster_by is not supported for views." + ) + else: + clustered_by = [] + for c in self.cluster_by: + try: + clustered_by.append(d.parse_one(c, dialect=model_dialect)) + except SqlglotError as e: + raise ConfigError( + f"Failed to parse model '{self.canonical_name(context)}' cluster_by field '{c}' in '{self.path}': {e}" + ) from e + optional_kwargs["clustered_by"] = clustered_by model_kwargs = self.sqlmesh_model_kwargs(context) if self.sql_header: @@ -627,7 +636,6 @@ def to_sqlmesh( if physical_properties: model_kwargs["physical_properties"] = physical_properties - kind = self.model_kind(context) allow_partials = model_kwargs.pop("allow_partials", None) if ( allow_partials is None diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index e81fcbe862..d6de34c1c6 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1837,6 +1837,17 @@ def test_model_cluster_by(): exp.to_column('"QUX"'), ] + model = ModelConfig( + name="model", + alias="model", + package_name="package", + target_schema="test", + cluster_by=["Bar", "qux"], + sql="SELECT * FROM baz", + materialized=Materialization.VIEW.value, + ) + assert model.to_sqlmesh(context).clustered_by == [] + def test_snowflake_dynamic_table(): context = DbtContext() From 110d7f28ccdbbc9531610eb326e47d8ac050470d Mon Sep 17 00:00:00 2001 From: Chris Rericha <67359577+crericha@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:47:04 -0400 Subject: [PATCH 0809/1056] Fix: Ignore bigquery partition_by config when the target isn't bigquery (#5280) Co-authored-by: Iaroslav Zeigerman --- sqlmesh/dbt/model.py | 21 +++++++++++++++++---- tests/dbt/test_transformation.py | 3 +++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 9833b49876..a941eb8880 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -38,6 +38,9 @@ logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__) + + INCREMENTAL_BY_TIME_STRATEGIES = set(["delete+insert", "insert_overwrite", "microbatch"]) INCREMENTAL_BY_UNIQUE_KEY_STRATEGIES = set(["merge"]) @@ -521,14 +524,24 @@ def to_sqlmesh( raise ConfigError( f"Failed to parse model '{self.canonical_name(context)}' partition_by field '{p}' in '{self.path}': {e}" ) from e - else: - partitioned_by.append(self._big_query_partition_by_expr(context)) - optional_kwargs["partitioned_by"] = partitioned_by + elif isinstance(self.partition_by, dict): + if context.target.dialect == "bigquery": + partitioned_by.append(self._big_query_partition_by_expr(context)) + else: + logger.warning( + "Ignoring partition_by config for model '%s' targeting %s. The format of the config field is only supported for BigQuery.", + self.name, + context.target.dialect, + ) + + if partitioned_by: + optional_kwargs["partitioned_by"] = partitioned_by if self.cluster_by: if isinstance(kind, ViewKind): logger.warning( - f"Ignoring cluster_by config for model '{self.name}'; cluster_by is not supported for views." + "Ignoring cluster_by config for model '%s'; cluster_by is not supported for views.", + self.name, ) else: clustered_by = [] diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index d6de34c1c6..22a331b8aa 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1469,6 +1469,9 @@ def test_partition_by(sushi_test_project: Project): model_config.partition_by = {"field": "ds", "data_type": "date", "granularity": "day"} assert model_config.to_sqlmesh(context).partitioned_by == [exp.to_column("ds", quoted=True)] + context.target = DuckDbConfig(name="target", schema="foo") + assert model_config.to_sqlmesh(context).partitioned_by == [] + @pytest.mark.xdist_group("dbt_manifest") def test_partition_by_none(sushi_test_project: Project): From be40445eb93bd6316e66022c254a6007687f00f5 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 3 Sep 2025 21:17:36 +0300 Subject: [PATCH 0810/1056] Fix: ensure root package is treated top-level in dbt (#5290) --- sqlmesh/utils/jinja.py | 5 ++++- .../dbt_packages/my_macros/dbt_project.yml | 17 +++++++++++++++++ .../my_macros/macros/log_value_alt.sql | 3 +++ .../dbt_packages/my_package/dbt_project.yml | 17 +++++++++++++++++ .../my_package/models/dummy_model.sql | 4 ++++ tests/fixtures/dbt/sushi_test/dbt_project.yml | 5 ++++- 6 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/dbt/sushi_test/dbt_packages/my_macros/dbt_project.yml create mode 100644 tests/fixtures/dbt/sushi_test/dbt_packages/my_macros/macros/log_value_alt.sql create mode 100644 tests/fixtures/dbt/sushi_test/dbt_packages/my_package/dbt_project.yml create mode 100644 tests/fixtures/dbt/sushi_test/dbt_packages/my_package/models/dummy_model.sql diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index 9764e625a4..74d498be38 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -330,13 +330,16 @@ def build_environment(self, **kwargs: t.Any) -> Environment: if macro.is_top_level and macro_name not in root_macros: root_macros[macro_name] = macro_wrapper + top_level_packages = self.top_level_packages.copy() + if self.root_package_name is not None: package_macros[self.root_package_name].update(root_macros) + top_level_packages.append(self.root_package_name) env = environment() builtin_globals = self._create_builtin_globals(kwargs) - for top_level_package_name in self.top_level_packages: + for top_level_package_name in top_level_packages: # Make sure that the top-level package doesn't fully override the same builtin package. package_macros[top_level_package_name] = AttributeDict( { diff --git a/tests/fixtures/dbt/sushi_test/dbt_packages/my_macros/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_packages/my_macros/dbt_project.yml new file mode 100644 index 0000000000..f0386b4e57 --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/dbt_packages/my_macros/dbt_project.yml @@ -0,0 +1,17 @@ +name: 'my_macros' +version: '1.0.0' +config-version: 2 + +profile: 'my_macros' + +model-paths: ["models"] +analysis-paths: ["analyses"] +test-paths: ["tests"] +seed-paths: ["seeds"] +macro-paths: ["macros"] +snapshot-paths: ["snapshots"] + +target-path: "target" +clean-targets: + - "target" + - "dbt_packages" diff --git a/tests/fixtures/dbt/sushi_test/dbt_packages/my_macros/macros/log_value_alt.sql b/tests/fixtures/dbt/sushi_test/dbt_packages/my_macros/macros/log_value_alt.sql new file mode 100644 index 0000000000..a88f316d3e --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/dbt_packages/my_macros/macros/log_value_alt.sql @@ -0,0 +1,3 @@ +{% macro log_value_alt(v) %} + {{ log("Entered value is: " ~ v) }} +{% endmacro %} diff --git a/tests/fixtures/dbt/sushi_test/dbt_packages/my_package/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_packages/my_package/dbt_project.yml new file mode 100644 index 0000000000..9c4797ebe0 --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/dbt_packages/my_package/dbt_project.yml @@ -0,0 +1,17 @@ +name: 'my_project' +version: '1.0.0' +config-version: 2 + +profile: 'my_project' + +model-paths: ["models"] +analysis-paths: ["analyses"] +test-paths: ["tests"] +seed-paths: ["seeds"] +macro-paths: ["macros"] +snapshot-paths: ["snapshots"] + +target-path: "target" +clean-targets: + - "target" + - "dbt_packages" diff --git a/tests/fixtures/dbt/sushi_test/dbt_packages/my_package/models/dummy_model.sql b/tests/fixtures/dbt/sushi_test/dbt_packages/my_package/models/dummy_model.sql new file mode 100644 index 0000000000..ff815fe55d --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/dbt_packages/my_package/models/dummy_model.sql @@ -0,0 +1,4 @@ +{{ log_value_alt(1) }} + +SELECT + 1 AS c diff --git a/tests/fixtures/dbt/sushi_test/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_project.yml index ea0041d107..2a25389e43 100644 --- a/tests/fixtures/dbt/sushi_test/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/dbt_project.yml @@ -8,7 +8,10 @@ model-paths: ["models"] analysis-paths: ["analyses"] test-paths: ["tests"] seed-paths: ["seeds"] -macro-paths: ["macros"] +macro-paths: [ + "macros", + "dbt_packages/my_macros/macros", +] snapshot-paths: ["snapshots"] target-path: "target" # directory which will store compiled SQL files From a3e7bda2802916e7010ce8cc7e97db8312834a87 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 3 Sep 2025 21:38:25 +0300 Subject: [PATCH 0811/1056] Fix(dbt): Respect the seed settings provided in the config of a dbt project (#5291) --- sqlmesh/dbt/seed.py | 6 +- tests/dbt/test_config.py | 21 ++++- tests/dbt/test_transformation.py | 91 +++++++++++++++++++ .../dbt/sushi_test/seeds/properties.yml | 10 ++ .../seeds/waiter_revenue_semicolon.csv | 7 ++ 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/dbt/sushi_test/seeds/waiter_revenue_semicolon.csv diff --git a/sqlmesh/dbt/seed.py b/sqlmesh/dbt/seed.py index 08e89ee584..38cd635d91 100644 --- a/sqlmesh/dbt/seed.py +++ b/sqlmesh/dbt/seed.py @@ -79,7 +79,11 @@ def to_sqlmesh( kwargs["columns"] = new_columns # dbt treats single whitespace as a null value - csv_settings = CsvSettings(na_values=[" "], keep_default_na=True) + csv_settings = CsvSettings( + delimiter=self.delimiter, + na_values=[" "], + keep_default_na=True, + ) return create_seed_model( self.canonical_name(context), diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index a20bdb071d..ae8713d933 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -444,7 +444,7 @@ def test_source_config(sushi_test_project: Project): @pytest.mark.slow def test_seed_config(sushi_test_project: Project, mocker: MockerFixture): seed_configs = sushi_test_project.packages["sushi"].seeds - assert set(seed_configs) == {"waiter_names"} + assert set(seed_configs) == {"waiter_names", "waiter_revenue_semicolon"} raw_items_seed = seed_configs["waiter_names"] expected_config = { @@ -465,6 +465,25 @@ def test_seed_config(sushi_test_project: Project, mocker: MockerFixture): == '"MEMORY"."SUSHI"."WAITER_NAMES"' ) + waiter_revenue_semicolon_seed = seed_configs["waiter_revenue_semicolon"] + + expected_config_semicolon = { + "path": Path(sushi_test_project.context.project_root, "seeds/waiter_revenue_semicolon.csv"), + "schema_": "sushi", + "delimiter": ";", + } + actual_config_semicolon = { + k: getattr(waiter_revenue_semicolon_seed, k) for k, v in expected_config_semicolon.items() + } + assert actual_config_semicolon == expected_config_semicolon + + assert waiter_revenue_semicolon_seed.canonical_name(context) == "sushi.waiter_revenue_semicolon" + assert ( + waiter_revenue_semicolon_seed.to_sqlmesh(context).name == "sushi.waiter_revenue_semicolon" + ) + assert waiter_revenue_semicolon_seed.delimiter == ";" + assert set(waiter_revenue_semicolon_seed.columns.keys()) == {"waiter_id", "revenue", "quarter"} + def test_quoting(): model = ModelConfig(alias="bar", schema="foo") diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 22a331b8aa..c8cc688a38 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -806,6 +806,58 @@ def test_seed_partial_column_inference(tmp_path): assert list(seed_df.columns) == list(sqlmesh_seed.columns_to_types.keys()) +def test_seed_delimiter(tmp_path): + seed_csv = tmp_path / "seed_with_delimiter.csv" + + with open(seed_csv, "w", encoding="utf-8") as fd: + fd.writelines("\n".join(["id|name|city", "0|Ayrton|SP", "1|Max|MC", "2|Niki|VIE"])) + + seed = SeedConfig( + name="test_model_pipe", + package="package", + path=Path(seed_csv), + delimiter="|", + ) + + context = DbtContext() + context.project_name = "TestProject" + context.target = DuckDbConfig(name="target", schema="test") + sqlmesh_seed = seed.to_sqlmesh(context) + + # Verify columns are correct with the custom pipe (|) delimiter + expected_columns = {"id", "name", "city"} + assert set(sqlmesh_seed.columns_to_types.keys()) == expected_columns + + seed_df = next(sqlmesh_seed.render_seed()) + assert list(seed_df.columns) == list(sqlmesh_seed.columns_to_types.keys()) + assert len(seed_df) == 3 + + assert seed_df.iloc[0]["name"] == "Ayrton" + assert seed_df.iloc[0]["city"] == "SP" + assert seed_df.iloc[1]["name"] == "Max" + assert seed_df.iloc[1]["city"] == "MC" + + # test with semicolon delimiter + seed_csv_semicolon = tmp_path / "seed_with_semicolon.csv" + with open(seed_csv_semicolon, "w", encoding="utf-8") as fd: + fd.writelines("\n".join(["id;value;status", "1;100;active", "2;200;inactive"])) + + seed_semicolon = SeedConfig( + name="test_model_semicolon", + package="package", + path=Path(seed_csv_semicolon), + delimiter=";", + ) + + sqlmesh_seed_semicolon = seed_semicolon.to_sqlmesh(context) + expected_columns_semicolon = {"id", "value", "status"} + assert set(sqlmesh_seed_semicolon.columns_to_types.keys()) == expected_columns_semicolon + + seed_df_semicolon = next(sqlmesh_seed_semicolon.render_seed()) + assert seed_df_semicolon.iloc[0]["value"] == 100 + assert seed_df_semicolon.iloc[0]["status"] == "active" + + def test_seed_column_order(tmp_path): seed_csv = tmp_path / "seed.csv" @@ -910,6 +962,45 @@ def test_hooks(sushi_test_dbt_context: Context, model_fqn: str): assert "post-hook" in mock_logger.call_args[0][0] +@pytest.mark.xdist_group("dbt_manifest") +def test_seed_delimiter_integration(sushi_test_dbt_context: Context): + seed_fqn = '"memory"."sushi"."waiter_revenue_semicolon"' + assert seed_fqn in sushi_test_dbt_context.models + + seed_model = sushi_test_dbt_context.models[seed_fqn] + assert seed_model.columns_to_types is not None + + # this should be loaded with semicolon delimiter otherwise it'd resylt in an one column table + assert set(seed_model.columns_to_types.keys()) == {"waiter_id", "revenue", "quarter"} + + # columns_to_types values are correct types as well + assert seed_model.columns_to_types == { + "waiter_id": exp.DataType.build("int"), + "revenue": exp.DataType.build("double"), + "quarter": exp.DataType.build("text"), + } + + df = sushi_test_dbt_context.fetchdf(f"SELECT * FROM {seed_fqn}") + + assert len(df) == 6 + waiter_ids = set(df["waiter_id"].tolist()) + quarters = set(df["quarter"].tolist()) + assert waiter_ids == {1, 2, 3} + assert quarters == {"Q1", "Q2"} + + q1_w1_rows = df[(df["waiter_id"] == 1) & (df["quarter"] == "Q1")] + assert len(q1_w1_rows) == 1 + assert float(q1_w1_rows.iloc[0]["revenue"]) == 100.50 + + q2_w2_rows = df[(df["waiter_id"] == 2) & (df["quarter"] == "Q2")] + assert len(q2_w2_rows) == 1 + assert float(q2_w2_rows.iloc[0]["revenue"]) == 225.50 + + q2_w3_rows = df[(df["waiter_id"] == 3) & (df["quarter"] == "Q2")] + assert len(q2_w3_rows) == 1 + assert float(q2_w3_rows.iloc[0]["revenue"]) == 175.75 + + @pytest.mark.xdist_group("dbt_manifest") def test_target_jinja(sushi_test_project: Project): context = sushi_test_project.context diff --git a/tests/fixtures/dbt/sushi_test/seeds/properties.yml b/tests/fixtures/dbt/sushi_test/seeds/properties.yml index f370c1962f..480447f1ef 100644 --- a/tests/fixtures/dbt/sushi_test/seeds/properties.yml +++ b/tests/fixtures/dbt/sushi_test/seeds/properties.yml @@ -2,3 +2,13 @@ version: 2 seeds: - name: waiter_names + - name: waiter_revenue_semicolon + config: + delimiter: ";" + columns: + - name: waiter_id + data_type: int + - name: revenue + data_type: decimal + - name: quarter + data_type: text diff --git a/tests/fixtures/dbt/sushi_test/seeds/waiter_revenue_semicolon.csv b/tests/fixtures/dbt/sushi_test/seeds/waiter_revenue_semicolon.csv new file mode 100644 index 0000000000..df477a3ed4 --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/seeds/waiter_revenue_semicolon.csv @@ -0,0 +1,7 @@ +waiter_id;revenue;quarter +1;100.50;Q1 +2;200.75;Q1 +3;150.25;Q1 +1;125.00;Q2 +2;225.50;Q2 +3;175.75;Q2 \ No newline at end of file From 5ade7237b489beac0818c377584cf9ca6d203873 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:37:55 +0300 Subject: [PATCH 0812/1056] Fix!: depend on all attributes of dbt `model` when passed to a macro (#5269) --- sqlmesh/core/model/definition.py | 4 +- sqlmesh/core/renderer.py | 51 +++++++++++-------- sqlmesh/dbt/basemodel.py | 33 ++++++------ sqlmesh/dbt/builtin.py | 11 ++++ sqlmesh/dbt/common.py | 18 +++++-- sqlmesh/dbt/loader.py | 2 +- sqlmesh/dbt/manifest.py | 46 ++++++++++++++--- sqlmesh/dbt/model.py | 27 +--------- sqlmesh/dbt/test.py | 5 +- .../v0095_warn_about_dbt_raw_sql_diff.py | 47 +++++++++++++++++ tests/core/test_context.py | 20 ++++++++ tests/dbt/test_config.py | 37 -------------- tests/dbt/test_manifest.py | 3 +- .../macros/check_model_is_table.sql | 15 ++++++ .../sushi_test/models/model_with_raw_code.sql | 11 ++++ 15 files changed, 210 insertions(+), 120 deletions(-) create mode 100644 sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py create mode 100644 tests/fixtures/dbt/sushi_test/macros/check_model_is_table.sql create mode 100644 tests/fixtures/dbt/sushi_test/models/model_with_raw_code.sql diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index f3ffcde05a..b772a9d9d9 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -594,7 +594,7 @@ def _statement_renderer(self, expression: exp.Expression) -> ExpressionRenderer: python_env=self.python_env, only_execution_time=False, default_catalog=self.default_catalog, - model_fqn=self.fqn, + model=self, ) return self._statement_renderer_cache[expression_key] @@ -1573,7 +1573,6 @@ def _query_renderer(self) -> QueryRenderer: self.dialect, self.macro_definitions, schema=self.mapping_schema, - model_fqn=self.fqn, path=self._path, jinja_macro_registry=self.jinja_macros, python_env=self.python_env, @@ -1581,6 +1580,7 @@ def _query_renderer(self) -> QueryRenderer: default_catalog=self.default_catalog, quote_identifiers=not no_quote_identifiers, optimize_query=self.optimize_query, + model=self, ) @property diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index 4078d718a6..49144bf55c 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -31,6 +31,7 @@ from sqlglot.dialects.dialect import DialectType from sqlmesh.core.linter.rule import Rule + from sqlmesh.core.model.definition import _Model from sqlmesh.core.snapshot import DeployabilityIndex, Snapshot @@ -50,9 +51,9 @@ def __init__( schema: t.Optional[t.Dict[str, t.Any]] = None, default_catalog: t.Optional[str] = None, quote_identifiers: bool = True, - model_fqn: t.Optional[str] = None, normalize_identifiers: bool = True, optimize_query: t.Optional[bool] = True, + model: t.Optional[_Model] = None, ): self._expression = expression self._dialect = dialect @@ -66,8 +67,9 @@ def __init__( self._quote_identifiers = quote_identifiers self.update_schema({} if schema is None else schema) self._cache: t.List[t.Optional[exp.Expression]] = [] - self._model_fqn = model_fqn + self._model_fqn = model.fqn if model else None self._optimize_query_flag = optimize_query is not False + self._model = model def update_schema(self, schema: t.Dict[str, t.Any]) -> None: self.schema = d.normalize_mapping_schema(schema, dialect=self._dialect) @@ -188,30 +190,32 @@ def _resolve_table(table: str | exp.Table) -> str: } variables = kwargs.pop("variables", {}) - jinja_env_kwargs = { - **{ - **render_kwargs, - **_prepare_python_env_for_jinja(macro_evaluator, self._python_env), - **variables, - }, - "snapshots": snapshots or {}, - "table_mapping": table_mapping, - "deployability_index": deployability_index, - "default_catalog": self._default_catalog, - "runtime_stage": runtime_stage.value, - "resolve_table": _resolve_table, - } - if this_model: - render_kwargs["this_model"] = this_model - jinja_env_kwargs["this_model"] = this_model.sql( - dialect=self._dialect, identify=True, comments=False - ) - - jinja_env = self._jinja_macro_registry.build_environment(**jinja_env_kwargs) expressions = [self._expression] if isinstance(self._expression, d.Jinja): try: + jinja_env_kwargs = { + **{ + **render_kwargs, + **_prepare_python_env_for_jinja(macro_evaluator, self._python_env), + **variables, + }, + "snapshots": snapshots or {}, + "table_mapping": table_mapping, + "deployability_index": deployability_index, + "default_catalog": self._default_catalog, + "runtime_stage": runtime_stage.value, + "resolve_table": _resolve_table, + "model_instance": self._model, + } + + if this_model: + jinja_env_kwargs["this_model"] = this_model.sql( + dialect=self._dialect, identify=True, comments=False + ) + + jinja_env = self._jinja_macro_registry.build_environment(**jinja_env_kwargs) + expressions = [] rendered_expression = jinja_env.from_string(self._expression.name).render() logger.debug( @@ -229,6 +233,9 @@ def _resolve_table(table: str | exp.Table) -> str: f"Could not render or parse jinja at '{self._path}'.\n{ex}" ) from ex + if this_model: + render_kwargs["this_model"] = this_model + macro_evaluator.locals.update(render_kwargs) if variables: diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index 212b314997..548718cf89 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -22,6 +22,7 @@ DbtConfig, Dependencies, GeneralConfig, + RAW_CODE_KEY, SqlStr, sql_str_validator, ) @@ -167,14 +168,6 @@ def _validate_grants(cls, v: t.Dict[str, str]) -> t.Dict[str, t.List[str]]: }, } - @property - def sql_no_config(self) -> SqlStr: - return SqlStr("") - - @property - def sql_embedded_config(self) -> SqlStr: - return SqlStr("") - @property def table_schema(self) -> str: """ @@ -375,15 +368,21 @@ def to_sqlmesh( def _model_jinja_context( self, context: DbtContext, dependencies: Dependencies ) -> t.Dict[str, t.Any]: - model_node: AttributeDict[str, t.Any] = AttributeDict( - { - k: v - for k, v in context._manifest._manifest.nodes[self.node_name].to_dict().items() - if k in dependencies.model_attrs - } - if context._manifest and self.node_name in context._manifest._manifest.nodes - else {} - ) + if context._manifest and self.node_name in context._manifest._manifest.nodes: + attributes = context._manifest._manifest.nodes[self.node_name].to_dict() + if dependencies.model_attrs.all_attrs: + model_node: AttributeDict[str, t.Any] = AttributeDict(attributes) + else: + model_node = AttributeDict( + filter(lambda kv: kv[0] in dependencies.model_attrs.attrs, attributes.items()) + ) + + # We exclude the raw SQL code to reduce the payload size. It's still accessible through + # the JinjaQuery instance stored in the resulting SQLMesh model's `query` field. + model_node.pop(RAW_CODE_KEY, None) + else: + model_node = AttributeDict({}) + return { "this": self.relation_info, "model": model_node, diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 24669807bb..0a2d837c28 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -16,8 +16,10 @@ from sqlmesh.core.console import get_console from sqlmesh.core.engine_adapter import EngineAdapter +from sqlmesh.core.model.definition import SqlModel from sqlmesh.core.snapshot.definition import DeployabilityIndex from sqlmesh.dbt.adapter import BaseAdapter, ParsetimeAdapter, RuntimeAdapter +from sqlmesh.dbt.common import RAW_CODE_KEY from sqlmesh.dbt.relation import Policy from sqlmesh.dbt.target import TARGET_TYPE_TO_CONFIG_CLASS from sqlmesh.dbt.util import DBT_VERSION @@ -469,12 +471,21 @@ def create_builtin_globals( is_incremental &= snapshot_table_exists else: is_incremental = False + builtin_globals["is_incremental"] = lambda: is_incremental builtin_globals["builtins"] = AttributeDict( {k: builtin_globals.get(k) for k in ("ref", "source", "config", "var")} ) + if (model := jinja_globals.pop("model", None)) is not None: + if isinstance(model_instance := jinja_globals.pop("model_instance", None), SqlModel): + builtin_globals["model"] = AttributeDict( + {**model, RAW_CODE_KEY: model_instance.query.name} + ) + else: + builtin_globals["model"] = AttributeDict(model.copy()) + if engine_adapter is not None: builtin_globals["flags"] = Flags(which="run") adapter: BaseAdapter = RuntimeAdapter( diff --git a/sqlmesh/dbt/common.py b/sqlmesh/dbt/common.py index ba982c2bb2..240d59084a 100644 --- a/sqlmesh/dbt/common.py +++ b/sqlmesh/dbt/common.py @@ -2,23 +2,26 @@ import re import typing as t +from dataclasses import dataclass from pathlib import Path from ruamel.yaml.constructor import DuplicateKeyError from sqlglot.helper import ensure_list +from sqlmesh.dbt.util import DBT_VERSION from sqlmesh.core.config.base import BaseConfig, UpdateStrategy +from sqlmesh.core.config.common import DBT_PROJECT_FILENAME from sqlmesh.utils import AttributeDict from sqlmesh.utils.conversions import ensure_bool, try_str_to_bool from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.jinja import MacroReference from sqlmesh.utils.pydantic import PydanticModel, field_validator from sqlmesh.utils.yaml import load -from sqlmesh.core.config.common import DBT_PROJECT_FILENAME T = t.TypeVar("T", bound="GeneralConfig") PROJECT_FILENAME = DBT_PROJECT_FILENAME +RAW_CODE_KEY = "raw_code" if DBT_VERSION >= (1, 3, 0) else "raw_sql" # type: ignore JINJA_ONLY = { "adapter", @@ -172,6 +175,12 @@ def sqlmesh_config_fields(self) -> t.Set[str]: return set() +@dataclass +class ModelAttrs: + attrs: t.Set[str] + all_attrs: bool = False + + class Dependencies(PydanticModel): """ DBT dependencies for a model, macro, etc. @@ -186,7 +195,7 @@ class Dependencies(PydanticModel): sources: t.Set[str] = set() refs: t.Set[str] = set() variables: t.Set[str] = set() - model_attrs: t.Set[str] = set() + model_attrs: ModelAttrs = ModelAttrs(attrs=set()) has_dynamic_var_names: bool = False @@ -196,7 +205,10 @@ def union(self, other: Dependencies) -> Dependencies: sources=self.sources | other.sources, refs=self.refs | other.refs, variables=self.variables | other.variables, - model_attrs=self.model_attrs | other.model_attrs, + model_attrs=ModelAttrs( + attrs=self.model_attrs.attrs | other.model_attrs.attrs, + all_attrs=self.model_attrs.all_attrs or other.model_attrs.all_attrs, + ), has_dynamic_var_names=self.has_dynamic_var_names or other.has_dynamic_var_names, ) diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 4f473a20ab..695aff3c45 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -138,7 +138,7 @@ def _to_sqlmesh(config: BMC, context: DbtContext) -> Model: package_models: t.Dict[str, BaseModelConfig] = {**package.models, **package.seeds} for model in package_models.values(): - if isinstance(model, ModelConfig) and not model.sql_no_config: + if isinstance(model, ModelConfig) and not model.sql.strip(): logger.info(f"Skipping empty model '{model.name}' at path '{model.path}'.") continue diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index ca20554e3b..67a1ae7ed3 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -44,8 +44,8 @@ from sqlmesh.core import constants as c from sqlmesh.utils.errors import SQLMeshError from sqlmesh.core.config import ModelDefaultsConfig -from sqlmesh.dbt.basemodel import Dependencies from sqlmesh.dbt.builtin import BUILTIN_FILTERS, BUILTIN_GLOBALS, OVERRIDDEN_MACROS +from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.model import ModelConfig from sqlmesh.dbt.package import HookConfig, MacroConfig from sqlmesh.dbt.seed import SeedConfig @@ -354,7 +354,9 @@ def _load_models_and_seeds(self) -> None: dependencies = Dependencies( macros=macro_references, refs=_refs(node), sources=_sources(node) ) - dependencies = dependencies.union(self._extra_dependencies(sql, node.package_name)) + dependencies = dependencies.union( + self._extra_dependencies(sql, node.package_name, track_all_model_attrs=True) + ) dependencies = dependencies.union( self._flatten_dependencies_from_macros(dependencies.macros, node.package_name) ) @@ -552,17 +554,37 @@ def _flatten_dependencies_from_macros( dependencies = dependencies.union(macro_dependencies) return dependencies - def _extra_dependencies(self, target: str, package: str) -> Dependencies: - # We sometimes observe that the manifest doesn't capture all macros, refs, and sources within a macro. - # This behavior has been observed with macros like dbt.current_timestamp(), dbt_utils.slugify(), and source(). - # Here we apply our custom extractor to make a best effort to supplement references captured in the manifest. + def _extra_dependencies( + self, + target: str, + package: str, + track_all_model_attrs: bool = False, + ) -> Dependencies: + """ + We sometimes observe that the manifest doesn't capture all macros, refs, and sources within a macro. + This behavior has been observed with macros like dbt.current_timestamp(), dbt_utils.slugify(), and source(). + Here we apply our custom extractor to make a best effort to supplement references captured in the manifest. + """ dependencies = Dependencies() + + # Whether all `model` attributes (e.g., `model.config`) should be included in the dependencies + all_model_attrs = False + for call_name, node in extract_call_names(target, cache=self._calls): if call_name[0] == "config": continue - elif isinstance(node, jinja2.nodes.Getattr): + + if ( + track_all_model_attrs + and not all_model_attrs + and isinstance(node, jinja2.nodes.Call) + and any(isinstance(a, jinja2.nodes.Name) and a.name == "model" for a in node.args) + ): + all_model_attrs = True + + if isinstance(node, jinja2.nodes.Getattr): if call_name[0] == "model": - dependencies.model_attrs.add(call_name[1]) + dependencies.model_attrs.attrs.add(call_name[1]) elif call_name[0] == "source": args = [jinja_call_arg_name(arg) for arg in node.args] if args and all(arg for arg in args): @@ -606,6 +628,14 @@ def _extra_dependencies(self, target: str, package: str) -> Dependencies: call_name[0], call_name[1], dependencies.macros.append ) + # When `model` is referenced as-is, e.g. it's passed as an argument to a macro call like + # `{{ foo(model) }}`, we can't easily track the attributes that are actually used, because + # it may be aliased and hence tracking actual uses of `model` requires a proper data flow + # analysis. We conservatively deal with this by including all of its supported attributes + # if a standalone reference is found. + if all_model_attrs: + dependencies.model_attrs.all_attrs = True + return dependencies diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index a941eb8880..20d0f8cd1a 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -27,7 +27,7 @@ ) from sqlmesh.core.model.kind import SCDType2ByTimeKind, OnDestructiveChange, OnAdditiveChange from sqlmesh.dbt.basemodel import BaseModelConfig, Materialization, SnapshotStrategy -from sqlmesh.dbt.common import SqlStr, extract_jinja_config, sql_str_validator +from sqlmesh.dbt.common import SqlStr, sql_str_validator from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.pydantic import field_validator @@ -138,10 +138,6 @@ class ModelConfig(BaseModelConfig): inserts_only: t.Optional[bool] = None incremental_predicates: t.Optional[t.List[str]] = None - # Private fields - _sql_embedded_config: t.Optional[SqlStr] = None - _sql_no_config: t.Optional[SqlStr] = None - _sql_validator = sql_str_validator @field_validator( @@ -432,25 +428,6 @@ def model_kind(self, context: DbtContext) -> ModelKind: raise ConfigError(f"{materialization.value} materialization not supported.") - @property - def sql_no_config(self) -> SqlStr: - if self._sql_no_config is None: - self._sql_no_config = SqlStr("") - self._extract_sql_config() - return self._sql_no_config - - @property - def sql_embedded_config(self) -> SqlStr: - if self._sql_embedded_config is None: - self._sql_embedded_config = SqlStr("") - self._extract_sql_config() - return self._sql_embedded_config - - def _extract_sql_config(self) -> None: - no_config, embedded_config = extract_jinja_config(self.sql) - self._sql_no_config = SqlStr(no_config) - self._sql_embedded_config = SqlStr(embedded_config) - def _big_query_partition_by_expr(self, context: DbtContext) -> exp.Expression: assert isinstance(self.partition_by, dict) data_type = self.partition_by["data_type"].lower() @@ -508,7 +485,7 @@ def to_sqlmesh( ) -> Model: """Converts the dbt model into a SQLMesh model.""" model_dialect = self.dialect(context) - query = d.jinja_query(self.sql_no_config) + query = d.jinja_query(self.sql) kind = self.model_kind(context) optional_kwargs: t.Dict[str, t.Any] = {} diff --git a/sqlmesh/dbt/test.py b/sqlmesh/dbt/test.py index 035c62acda..b5eec21623 100644 --- a/sqlmesh/dbt/test.py +++ b/sqlmesh/dbt/test.py @@ -12,7 +12,6 @@ Dependencies, GeneralConfig, SqlStr, - extract_jinja_config, sql_str_validator, ) from sqlmesh.utils import AttributeDict @@ -134,9 +133,7 @@ def to_sqlmesh(self, context: DbtContext) -> Audit: } ) - sql_no_config, _sql_config_only = extract_jinja_config(self.sql) - sql_no_config = sql_no_config.replace("**_dbt_generic_test_kwargs", self._kwargs()) - query = d.jinja_query(sql_no_config) + query = d.jinja_query(self.sql.replace("**_dbt_generic_test_kwargs", self._kwargs())) skip = not self.enabled blocking = self.severity == Severity.ERROR diff --git a/sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py b/sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py new file mode 100644 index 0000000000..ce39946b0d --- /dev/null +++ b/sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py @@ -0,0 +1,47 @@ +""" +Warns dbt users about potential diffs due to inclusion of {{ config(...) }} blocks in model SQL. + +Prior to this fix, SQLMesh wasn't including the {{ config(...) }} block in the model's SQL payload +when processing dbt models. Now these config blocks are properly included in the raw SQL, which +may cause diffs to appear for existing dbt models even though the actual SQL logic hasn't changed. + +This is a one-time diff that will appear after upgrading, and applying a plan will resolve it. +""" + +import json + +from sqlglot import exp + +from sqlmesh.core.console import get_console + +SQLMESH_DBT_PACKAGE = "sqlmesh.dbt" + + +def migrate(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + warning = ( + "SQLMesh detected that it may not be able to fully migrate the state database. This should not impact " + "the migration process, but may result in unexpected changes being reported by the next `sqlmesh plan` " + "command. Please run `sqlmesh diff prod` after the migration has completed, before making any new " + "changes. If any unexpected changes are reported, consider running a forward-only plan to apply these " + "changes and avoid unnecessary backfills: sqlmesh plan prod --forward-only. " + "See https://sqlmesh.readthedocs.io/en/stable/concepts/plans/#forward-only-plans for more details.\n" + ) + + for (snapshot,) in engine_adapter.fetchall( + exp.select("snapshot").from_(snapshots_table), quote_identifiers=True + ): + parsed_snapshot = json.loads(snapshot) + node = parsed_snapshot["node"] + + jinja_macros = node.get("jinja_macros") or {} + create_builtins_module = jinja_macros.get("create_builtins_module") or "" + + if create_builtins_module == SQLMESH_DBT_PACKAGE: + get_console().log_warning(warning) + return diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 196889a87c..f09ff25c33 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1534,6 +1534,26 @@ def test_plan_enable_preview_default(sushi_context: Context, sushi_dbt_context: assert sushi_dbt_context._plan_preview_enabled +@pytest.mark.slow +def test_raw_code_handling(sushi_test_dbt_context: Context): + model = sushi_test_dbt_context.models['"memory"."sushi"."model_with_raw_code"'] + assert "raw_code" not in model.jinja_macros.global_objs["model"] # type: ignore + + # logging "pre-hook" (in dbt_projects.yml) + the actual pre-hook in the model file + assert len(model.pre_statements) == 2 + + original_file_path = model.jinja_macros.global_objs["model"]["original_file_path"] # type: ignore + model_file_path = sushi_test_dbt_context.path / original_file_path + + raw_code_length = len(model_file_path.read_text()) - 1 + + hook = model.render_pre_statements()[0] + assert ( + hook.sql() + == f'''CREATE TABLE "t" AS SELECT 'Length is {raw_code_length}' AS "length_col"''' + ) + + def test_catalog_name_needs_to_be_quoted(): config = Config( model_defaults=ModelDefaultsConfig(dialect="duckdb"), diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index ae8713d933..ecd95a43c4 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -276,42 +276,6 @@ def test_singular_test_to_standalone_audit(dbt_dummy_postgres_config: PostgresCo assert standalone_audit.dialect == "bigquery" -def test_model_config_sql_no_config(): - assert ( - ModelConfig( - sql="""{{ - config( - materialized='table', - incremental_strategy='delete+"insert' - ) -}} -query""" - ).sql_no_config.strip() - == "query" - ) - - assert ( - ModelConfig( - sql="""{{ - config( - materialized='table', - incremental_strategy='delete+insert', - post_hook=" '{{ var('new') }}' " - ) -}} -query""" - ).sql_no_config.strip() - == "query" - ) - - assert ( - ModelConfig( - sql="""before {{config(materialized='table', post_hook=" {{ var('new') }} ")}} after""" - ).sql_no_config.strip() - == "before after" - ) - - @pytest.mark.slow def test_variables(assert_exp_eq, sushi_test_project): # Case 1: using an undefined variable without a default value @@ -350,7 +314,6 @@ def test_variables(assert_exp_eq, sushi_test_project): # Case 3: using a defined variable with a default value model_config.sql = "SELECT {{ var('foo', 5) }}" - model_config._sql_no_config = None assert_exp_eq(model_config.to_sqlmesh(**kwargs).render_query(), 'SELECT 6 AS "6"') diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index 5f2f6fb37f..ba8971e9b2 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -6,6 +6,7 @@ from sqlmesh.core.config import ModelDefaultsConfig from sqlmesh.dbt.basemodel import Dependencies +from sqlmesh.dbt.common import ModelAttrs from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.manifest import ManifestHelper, _convert_jinja_test_to_macro from sqlmesh.dbt.profile import Profile @@ -33,7 +34,7 @@ def test_manifest_helper(caplog): assert models["top_waiters"].dependencies == Dependencies( refs={"sushi.waiter_revenue_by_day", "waiter_revenue_by_day"}, variables={"top_waiters:revenue", "top_waiters:limit"}, - model_attrs={"columns", "config"}, + model_attrs=ModelAttrs(attrs={"columns", "config"}), macros=[ MacroReference(name="get_top_waiters_limit"), MacroReference(name="ref"), diff --git a/tests/fixtures/dbt/sushi_test/macros/check_model_is_table.sql b/tests/fixtures/dbt/sushi_test/macros/check_model_is_table.sql new file mode 100644 index 0000000000..42dc5615e4 --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/macros/check_model_is_table.sql @@ -0,0 +1,15 @@ +{%- macro check_model_is_table(model) -%} + {%- if model.config.materialized != 'table' -%} + {%- do exceptions.raise_compiler_error( + "Model must use the table materialization. Please check any model overrides." + ) -%} + {%- endif -%} +{%- endmacro -%} + +{%- macro check_model_is_table_alt(foo) -%} + {%- if foo.config.materialized != 'table' -%} + {%- do exceptions.raise_compiler_error( + "Model must use the table materialization. Please check any model overrides." + ) -%} + {%- endif -%} +{%- endmacro -%} diff --git a/tests/fixtures/dbt/sushi_test/models/model_with_raw_code.sql b/tests/fixtures/dbt/sushi_test/models/model_with_raw_code.sql new file mode 100644 index 0000000000..386e7f40ef --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/models/model_with_raw_code.sql @@ -0,0 +1,11 @@ +{{ + config( + pre_hook=['CREATE TABLE t AS SELECT \'Length is {{ model.raw_code|length }}\' AS length_col'] + ) +}} + +{{ check_model_is_table(model) }} +{{ check_model_is_table_alt(model) }} + +SELECT + 1 AS c From 442c4a62c0ad8381c7c140574b815227ed667c6f Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:56:48 +0300 Subject: [PATCH 0813/1056] Fix: make jinja-based model rendering more lenient (#5300) --- sqlmesh/core/renderer.py | 5 +++++ .../dbt/sushi_test/models/dynamic_graph_model.sql | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 tests/fixtures/dbt/sushi_test/models/dynamic_graph_model.sql diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index 49144bf55c..e1d7bf4bcf 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -549,6 +549,11 @@ def render( expressions = [e for e in expressions if not isinstance(e, exp.Semicolon)] if not expressions: + # We assume that if there are no expressions, then the model contains dynamic Jinja SQL + # and we thus treat it similar to models with adapter calls to match dbt's behavior. + if isinstance(self._expression, d.JinjaQuery): + return None + raise ConfigError(f"Failed to render query at '{self._path}':\n{self._expression}") if len(expressions) > 1: diff --git a/tests/fixtures/dbt/sushi_test/models/dynamic_graph_model.sql b/tests/fixtures/dbt/sushi_test/models/dynamic_graph_model.sql new file mode 100644 index 0000000000..18da9c3c7b --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/models/dynamic_graph_model.sql @@ -0,0 +1,8 @@ + +{% if execute %} + {% for dataset, nodes in graph.nodes.values() | selectattr("resource_type", "equalto", "model") | groupby('schema') %} + {% if loop.first %} + SELECT 1 AS c + {% endif %} + {% endfor %} +{% endif %} From 87322577a67e495604987bf70a8d7be50907a61e Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 4 Sep 2025 20:13:08 +0300 Subject: [PATCH 0814/1056] Fix: Add support for args syntax in dbt config (#5299) --- sqlmesh/dbt/builtin.py | 20 +++- tests/dbt/test_transformation.py | 94 ++++++++++++++++++- .../dbt/sushi_test/models/top_waiters.sql | 9 +- 3 files changed, 116 insertions(+), 7 deletions(-) diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 0a2d837c28..c4fe0540b7 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -170,8 +170,24 @@ class Config: def __init__(self, config_dict: t.Dict[str, t.Any]) -> None: self._config = config_dict - def __call__(self, **kwargs: t.Any) -> str: - self._config.update(**kwargs) + def __call__(self, *args: t.Any, **kwargs: t.Any) -> str: + if args and kwargs: + raise ConfigError( + "Invalid inline model config: cannot mix positional and keyword arguments" + ) + + if args: + if len(args) == 1 and isinstance(args[0], dict): + # Single dict argument: config({"materialized": "table"}) + self._config.update(args[0]) + else: + raise ConfigError( + f"Invalid inline model config: expected a single dictionary, got {len(args)} arguments" + ) + elif kwargs: + # Keyword arguments: config(materialized="table") + self._config.update(kwargs) + return "" def set(self, name: str, value: t.Any) -> str: diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index c8cc688a38..1db3d469d8 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -10,6 +10,7 @@ import pytest from dbt.adapters.base import BaseRelation +from jinja2 import Template if DBT_VERSION >= (1, 4, 0): from dbt.exceptions import CompilationError @@ -42,7 +43,7 @@ OnAdditiveChange, ) from sqlmesh.core.state_sync.db.snapshot import _snapshot_to_json -from sqlmesh.dbt.builtin import _relation_info_to_relation +from sqlmesh.dbt.builtin import _relation_info_to_relation, Config from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.column import ( ColumnConfig, @@ -1052,6 +1053,97 @@ def test_config_jinja(sushi_test_project: Project): assert model.render_pre_statements()[0].sql() == '"bar"' +@pytest.mark.xdist_group("dbt_manifest") +def test_config_dict_syntax(): + # Test dictionary syntax + config = Config({}) + result = config({"materialized": "table", "alias": "dict_table"}) + assert result == "" + assert config._config["materialized"] == "table" + assert config._config["alias"] == "dict_table" + + # Test kwargs syntax still works + config2 = Config({}) + result = config2(materialized="view", alias="kwargs_table") + assert result == "" + assert config2._config["materialized"] == "view" + assert config2._config["alias"] == "kwargs_table" + + # Test that mixing args and kwargs is rejected + config3 = Config({}) + try: + config3({"materialized": "table"}, alias="mixed") + assert False, "Should have raised ConfigError" + except Exception as e: + assert "cannot mix positional and keyword arguments" in str(e) + + # Test nested dicts + config4 = Config({}) + config4({"meta": {"owner": "data_team", "priority": 1}, "tags": ["daily", "critical"]}) + assert config4._config["meta"]["owner"] == "data_team" + assert config4._config["tags"] == ["daily", "critical"] + + # Test multiple positional arguments are rejected + config4 = Config({}) + try: + config4({"materialized": "table"}, {"alias": "test"}) + assert False + except Exception as e: + assert "expected a single dictionary, got 2 arguments" in str(e) + + +def test_config_dict_in_jinja(): + # Test dict syntax directly with Config class + config = Config({}) + template = Template("{{ config({'materialized': 'table', 'unique_key': 'id'}) }}done") + result = template.render(config=config) + assert result == "done" + assert config._config["materialized"] == "table" + assert config._config["unique_key"] == "id" + + # Test with nested dict and list values + config2 = Config({}) + complex_template = Template("""{{ config({ + 'tags': ['test', 'dict'], + 'meta': {'owner': 'data_team'} + }) }}result""") + result = complex_template.render(config=config2) + assert result == "result" + assert config2._config["tags"] == ["test", "dict"] + assert config2._config["meta"]["owner"] == "data_team" + + # Test that kwargs still work + config3 = Config({}) + kwargs_template = Template("{{ config(materialized='view', alias='my_view') }}done") + result = kwargs_template.render(config=config3) + assert result == "done" + assert config3._config["materialized"] == "view" + assert config3._config["alias"] == "my_view" + + +@pytest.mark.xdist_group("dbt_manifest") +def test_config_dict_syntax_in_sushi_project(sushi_test_project: Project): + assert sushi_test_project is not None + assert sushi_test_project.context is not None + + sushi_package = sushi_test_project.packages.get("sushi") + assert sushi_package is not None + + top_waiters_found = False + for model_config in sushi_package.models.values(): + if model_config.name == "top_waiters": + # top_waiters model now uses dict config syntax with: + # config({'materialized': 'view', 'limit_value': var('top_waiters:limit'), 'meta': {...}}) + top_waiters_found = True + assert model_config.materialized == "view" + assert model_config.meta is not None + assert model_config.meta.get("owner") == "analytics_team" + assert model_config.meta.get("priority") == "high" + break + + assert top_waiters_found + + @pytest.mark.xdist_group("dbt_manifest") def test_config_jinja_get_methods(sushi_test_project: Project): model_config = ModelConfig( diff --git a/tests/fixtures/dbt/sushi_test/models/top_waiters.sql b/tests/fixtures/dbt/sushi_test/models/top_waiters.sql index 265a99eb7a..ce7e2154c5 100644 --- a/tests/fixtures/dbt/sushi_test/models/top_waiters.sql +++ b/tests/fixtures/dbt/sushi_test/models/top_waiters.sql @@ -1,8 +1,9 @@ {{ - config( - materialized='view', - limit_value=var('top_waiters:limit'), - ) + config({ + 'materialized': 'view', + 'limit_value': var('top_waiters:limit'), + 'meta': {'owner': 'analytics_team', 'priority': 'high'} + }) }} {% set columns = model.columns %} From 745c383eaa6d6883d7ddb5395eb102622ebf212e Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 5 Sep 2025 05:39:41 +1200 Subject: [PATCH 0815/1056] Chore: improve test stability and address some warnings (#5293) --- .circleci/install-prerequisites.sh | 5 ++ .circleci/manage-test-db.sh | 4 +- Makefile | 34 ++++---- pyproject.toml | 11 ++- sqlmesh/core/test/context.py | 2 + sqlmesh/dbt/test.py | 4 + tests/conftest.py | 78 ------------------- tests/dbt/test_integration.py | 4 + tests/engines/spark/test_db_api.py | 8 +- .../github/cicd/test_github_commands.py | 2 + 10 files changed, 49 insertions(+), 103 deletions(-) diff --git a/.circleci/install-prerequisites.sh b/.circleci/install-prerequisites.sh index cbd8491535..446221dba6 100755 --- a/.circleci/install-prerequisites.sh +++ b/.circleci/install-prerequisites.sh @@ -34,4 +34,9 @@ echo "Installing OS-level dependencies: $ALL_DEPENDENCIES" sudo apt-get clean && sudo apt-get -y update && sudo ACCEPT_EULA='Y' apt-get -y install $ALL_DEPENDENCIES +if [ "$ENGINE" == "spark" ]; then + echo "Using Java version for spark:" + java -version +fi + echo "All done" \ No newline at end of file diff --git a/.circleci/manage-test-db.sh b/.circleci/manage-test-db.sh index f79072f335..f90b567ce8 100755 --- a/.circleci/manage-test-db.sh +++ b/.circleci/manage-test-db.sh @@ -51,7 +51,9 @@ databricks_init() { # Note: the cluster doesnt need to be running to create / drop catalogs, but it does need to be running to run the integration tests echo "Ensuring cluster is running" - databricks clusters start $CLUSTER_ID + # the || true is to prevent the following error from causing an abort: + # > Error: is in unexpected state Running. + databricks clusters start $CLUSTER_ID || true } databricks_up() { diff --git a/Makefile b/Makefile index 668769e2ef..96305c4bfb 100644 --- a/Makefile +++ b/Makefile @@ -138,7 +138,7 @@ dbt-test: pytest -n auto -m "dbt and not cicdonly" dbt-fast-test: - pytest -n auto -m "dbt and fast" --retries 3 + pytest -n auto -m "dbt and fast" --reruns 3 github-test: pytest -n auto -m "github" @@ -173,58 +173,58 @@ engine-%-down: ################## clickhouse-test: engine-clickhouse-up - pytest -n auto -m "clickhouse" --retries 3 --junitxml=test-results/junit-clickhouse.xml + pytest -n auto -m "clickhouse" --reruns 3 --junitxml=test-results/junit-clickhouse.xml duckdb-test: engine-duckdb-install - pytest -n auto -m "duckdb" --retries 3 --junitxml=test-results/junit-duckdb.xml + pytest -n auto -m "duckdb" --reruns 3 --junitxml=test-results/junit-duckdb.xml mssql-test: engine-mssql-up - pytest -n auto -m "mssql" --retries 3 --junitxml=test-results/junit-mssql.xml + pytest -n auto -m "mssql" --reruns 3 --junitxml=test-results/junit-mssql.xml mysql-test: engine-mysql-up - pytest -n auto -m "mysql" --retries 3 --junitxml=test-results/junit-mysql.xml + pytest -n auto -m "mysql" --reruns 3 --junitxml=test-results/junit-mysql.xml postgres-test: engine-postgres-up - pytest -n auto -m "postgres" --retries 3 --junitxml=test-results/junit-postgres.xml + pytest -n auto -m "postgres" --reruns 3 --junitxml=test-results/junit-postgres.xml spark-test: engine-spark-up - pytest -n auto -m "spark" --retries 3 --junitxml=test-results/junit-spark.xml + pytest -n auto -m "spark" --reruns 3 --junitxml=test-results/junit-spark.xml && pytest -n auto -m "pyspark" --reruns 3 --junitxml=test-results/junit-pyspark.xml trino-test: engine-trino-up - pytest -n auto -m "trino" --retries 3 --junitxml=test-results/junit-trino.xml + pytest -n auto -m "trino" --reruns 3 --junitxml=test-results/junit-trino.xml risingwave-test: engine-risingwave-up - pytest -n auto -m "risingwave" --retries 3 --junitxml=test-results/junit-risingwave.xml + pytest -n auto -m "risingwave" --reruns 3 --junitxml=test-results/junit-risingwave.xml ################# # Cloud Engines # ################# snowflake-test: guard-SNOWFLAKE_ACCOUNT guard-SNOWFLAKE_WAREHOUSE guard-SNOWFLAKE_DATABASE guard-SNOWFLAKE_USER guard-SNOWFLAKE_PASSWORD engine-snowflake-install - pytest -n auto -m "snowflake" --retries 3 --junitxml=test-results/junit-snowflake.xml + pytest -n auto -m "snowflake" --reruns 3 --junitxml=test-results/junit-snowflake.xml bigquery-test: guard-BIGQUERY_KEYFILE engine-bigquery-install $(PIP) install -e ".[bigframes]" - pytest -n auto -m "bigquery" --retries 3 --junitxml=test-results/junit-bigquery.xml + pytest -n auto -m "bigquery" --reruns 3 --junitxml=test-results/junit-bigquery.xml databricks-test: guard-DATABRICKS_CATALOG guard-DATABRICKS_SERVER_HOSTNAME guard-DATABRICKS_HTTP_PATH guard-DATABRICKS_ACCESS_TOKEN guard-DATABRICKS_CONNECT_VERSION engine-databricks-install $(PIP) install 'databricks-connect==${DATABRICKS_CONNECT_VERSION}' - pytest -n auto -m "databricks" --retries 3 --junitxml=test-results/junit-databricks.xml + pytest -n auto -m "databricks" --reruns 3 --junitxml=test-results/junit-databricks.xml redshift-test: guard-REDSHIFT_HOST guard-REDSHIFT_USER guard-REDSHIFT_PASSWORD guard-REDSHIFT_DATABASE engine-redshift-install - pytest -n auto -m "redshift" --retries 3 --junitxml=test-results/junit-redshift.xml + pytest -n auto -m "redshift" --reruns 3 --junitxml=test-results/junit-redshift.xml clickhouse-cloud-test: guard-CLICKHOUSE_CLOUD_HOST guard-CLICKHOUSE_CLOUD_USERNAME guard-CLICKHOUSE_CLOUD_PASSWORD engine-clickhouse-install - pytest -n 1 -m "clickhouse_cloud" --retries 3 --junitxml=test-results/junit-clickhouse-cloud.xml + pytest -n 1 -m "clickhouse_cloud" --reruns 3 --junitxml=test-results/junit-clickhouse-cloud.xml athena-test: guard-AWS_ACCESS_KEY_ID guard-AWS_SECRET_ACCESS_KEY guard-ATHENA_S3_WAREHOUSE_LOCATION engine-athena-install - pytest -n auto -m "athena" --retries 3 --junitxml=test-results/junit-athena.xml + pytest -n auto -m "athena" --reruns 3 --junitxml=test-results/junit-athena.xml fabric-test: guard-FABRIC_HOST guard-FABRIC_CLIENT_ID guard-FABRIC_CLIENT_SECRET guard-FABRIC_DATABASE engine-fabric-install - pytest -n auto -m "fabric" --retries 3 --junitxml=test-results/junit-fabric.xml + pytest -n auto -m "fabric" --reruns 3 --junitxml=test-results/junit-fabric.xml gcp-postgres-test: guard-GCP_POSTGRES_INSTANCE_CONNECTION_STRING guard-GCP_POSTGRES_USER guard-GCP_POSTGRES_PASSWORD guard-GCP_POSTGRES_KEYFILE_JSON engine-gcppostgres-install - pytest -n auto -m "gcp_postgres" --retries 3 --junitxml=test-results/junit-gcp-postgres.xml + pytest -n auto -m "gcp_postgres" --reruns 3 --junitxml=test-results/junit-gcp-postgres.xml vscode_settings: mkdir -p .vscode diff --git a/pyproject.toml b/pyproject.toml index d3886562d6..9ee91ef1a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ dev = [ "pytest", "pytest-asyncio", "pytest-mock", - "pytest-retry", + "pytest-rerunfailures", "pytest-xdist", "pytz", "redshift_connector", @@ -264,8 +264,13 @@ markers = [ "redshift: test for Redshift", "snowflake: test for Snowflake", "spark: test for Spark", + "pyspark: test for PySpark that need to run separately from the other spark tests", "trino: test for Trino (all connectors)", - "risingwave: test for Risingwave" + "risingwave: test for Risingwave", + + # Other + "set_default_connection", + "registry_isolation" ] addopts = "-n 0 --dist=loadgroup" asyncio_default_fixture_loop_scope = "session" @@ -275,7 +280,7 @@ log_cli_level = "INFO" filterwarnings = [ "ignore:The localize method is no longer necessary, as this time zone supports the fold attribute" ] -retry_delay = 10 +reruns_delay = 10 [tool.ruff] line-length = 100 diff --git a/sqlmesh/core/test/context.py b/sqlmesh/core/test/context.py index 5ad9673ca8..a326c3c1b3 100644 --- a/sqlmesh/core/test/context.py +++ b/sqlmesh/core/test/context.py @@ -18,6 +18,8 @@ class TestExecutionContext(ExecutionContext): models: All upstream models to use for expansion and mapping of physical locations. """ + __test__ = False # prevent pytest trying to collect this as a test class + def __init__( self, engine_adapter: EngineAdapter, diff --git a/sqlmesh/dbt/test.py b/sqlmesh/dbt/test.py index b5eec21623..833df6495f 100644 --- a/sqlmesh/dbt/test.py +++ b/sqlmesh/dbt/test.py @@ -60,6 +60,10 @@ class TestConfig(GeneralConfig): error_if: Conditional expression (default "!=0") to detect if error condition met (Not supported). """ + __test__ = ( + False # prevent pytest trying to collect this as a test class when it's imported in a test + ) + # SQLMesh fields path: Path = Path() name: str diff --git a/tests/conftest.py b/tests/conftest.py index b6523f72a4..e5bbc4f425 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -212,84 +212,6 @@ def pytest_collection_modifyitems(items, *args, **kwargs): item.add_marker("fast") -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): - # The tmp_path fixture frequently throws errors like: - # - KeyError: <_pytest.stash.StashKey object at 0x79ba385fe1a0> - # in its teardown. This causes pytest to mark the test as failed even though we have zero control over this behaviour. - # So we log/swallow that particular error here rather than raising it - - # note: the hook always has to yield - outcome = yield - - # we only care about tests that used the tmp_path fixture - if "tmp_path" not in getattr(item, "fixturenames", []): - return - - result: pytest.TestReport = outcome.get_result() - - if result.when != "teardown": - return - - # If we specifically failed with a StashKey error in teardown, mark the test as passed - if result.failed: - exception = call.excinfo - if ( - exception - and isinstance(exception.value, KeyError) - and "_pytest.stash.StashKey" in repr(exception) - ): - result.outcome = "passed" - item.add_report_section( - "teardown", "stderr", f"Ignored tmp_path teardown error: {exception}" - ) - - -def pytest_configure(config: pytest.Config): - # we need to adjust the hook order if pytest-retry is present because it: - # - also declares a `pytest_runtest_makereport` with `hookwrapper=True, tryfirst=True` - # - this supersedes our one because pytest always loads plugins first and they take precedence over user code - # - # but, we need our one to run first because it's capturing and ignoring certain errors that cause pytest-retry to fail - # and not retry. so we need to adjust the order the hooks are called which pytest does NOT make easy. - # - # we can't just unload the pytest-retry plugin, load our hook and reload the pytest-retry plugin either. - # this causes an error: - # > Hook 'pytest_set_excluded_exceptions' is already registered within namespace - # because unregister() apparently doesnt unregister plugins cleanly in such a way they can be re-registered - # - # so what we end up doing below is a small monkey-patch to adjust the call order of the hooks - pm = config.pluginmanager - - from pluggy._hooks import HookCaller - - hook_caller: HookCaller = pm.hook.pytest_runtest_makereport - hook_impls = hook_caller.get_hookimpls() - - # find the index of our one - our_makereport_idx = next( - (i for i, v in enumerate(hook_impls) if v.plugin_name.endswith("tests/conftest.py")), None - ) - - # find the index of the pytest-retry one - pytest_retry_makereport_idx = next( - (i for i, v in enumerate(hook_impls) if v.plugin_name == "pytest-retry"), None - ) - - if ( - pytest_retry_makereport_idx is not None - and our_makereport_idx is not None - and our_makereport_idx > pytest_retry_makereport_idx - ): - our_makereport_hook = hook_impls.pop(our_makereport_idx) - - # inject our one to run before the pytest-retry one - hook_impls.insert(pytest_retry_makereport_idx, our_makereport_hook) - - # HookCaller doesnt have a setter method for this. - hook_caller._hookimpls = hook_impls # type: ignore - - # Ignore all local config files @pytest.fixture(scope="session", autouse=True) def ignore_local_config_files(): diff --git a/tests/dbt/test_integration.py b/tests/dbt/test_integration.py index ee8c486ab2..5a944d55d4 100644 --- a/tests/dbt/test_integration.py +++ b/tests/dbt/test_integration.py @@ -27,6 +27,8 @@ class TestType(str, Enum): + __test__ = False # prevent pytest trying to collect this as a test class + DBT_RUNTIME = "dbt_runtime" DBT_ADAPTER = "dbt_adapter" SQLMESH = "sqlmesh" @@ -53,6 +55,8 @@ def is_sqlmesh_runtime(self) -> bool: class TestStrategy(str, Enum): + __test__ = False # prevent pytest trying to collect this as a test class + CHECK = "check" TIMESTAMP = "timestamp" diff --git a/tests/engines/spark/test_db_api.py b/tests/engines/spark/test_db_api.py index 8bbfe7e9ab..eab7a0c223 100644 --- a/tests/engines/spark/test_db_api.py +++ b/tests/engines/spark/test_db_api.py @@ -4,10 +4,10 @@ from sqlmesh.engines.spark.db_api import errors from sqlmesh.engines.spark.db_api import spark_session as spark_session_db -pytestmark = [ - pytest.mark.slow, - pytest.mark.spark_pyspark, -] +# note: this is deliberately not marked with 'spark' so that it +# can run separately from the spark integration tests. +# running them at the same time mutates some global state in the SparkSession which breaks these tests +pytestmark = [pytest.mark.slow, pytest.mark.pyspark] def test_spark_session_cursor(spark_session: SparkSession): diff --git a/tests/integrations/github/cicd/test_github_commands.py b/tests/integrations/github/cicd/test_github_commands.py index 296fea5938..01e4c9af31 100644 --- a/tests/integrations/github/cicd/test_github_commands.py +++ b/tests/integrations/github/cicd/test_github_commands.py @@ -5,6 +5,8 @@ from unittest import TestCase, mock from unittest.result import TestResult +TestResult.__test__ = False # prevent pytest trying to collect this as a test class + import pytest from pytest_mock.plugin import MockerFixture From c7f0d16d1d19dc39857e27fd4efdc2e5f0e79747 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 4 Sep 2025 20:57:49 +0300 Subject: [PATCH 0816/1056] Chore!: bump sqlglot to v27.12.0 (#5302) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9ee91ef1a6..4267f65319 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.9.0", + "sqlglot[rs]~=27.12.0", "tenacity", "time-machine", "json-stream" From e0cd5310cb1729a724869261653921d4a4805167 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 4 Sep 2025 11:18:17 -0700 Subject: [PATCH 0817/1056] Fix: Prefetch all relevant data objects during snapshot migration (#5301) --- sqlmesh/core/snapshot/evaluator.py | 120 +++++---- tests/core/test_snapshot_evaluator.py | 338 +++++++++++++------------- 2 files changed, 242 insertions(+), 216 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 6d6525a771..b96b0b8718 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -37,7 +37,7 @@ from sqlmesh.core import dialect as d from sqlmesh.core.audit import Audit, StandaloneAudit from sqlmesh.core.dialect import schema_ -from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, DataObjectType +from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, DataObjectType, DataObject from sqlmesh.core.macros import RuntimeStage from sqlmesh.core.model import ( AuditResult, @@ -422,50 +422,14 @@ def get_snapshots_to_create( target_snapshots: Target snapshots. deployability_index: Determines snapshots that are deployable / representative in the context of this creation. """ - snapshots_with_table_names = defaultdict(set) - tables_by_gateway_and_schema: t.Dict[t.Union[str, None], t.Dict[exp.Table, set[str]]] = ( - defaultdict(lambda: defaultdict(set)) - ) - + existing_data_objects = self._get_data_objects(target_snapshots, deployability_index) + snapshots_to_create = [] for snapshot in target_snapshots: if not snapshot.is_model or snapshot.is_symbolic: continue - is_deployable = deployability_index.is_deployable(snapshot) - table = exp.to_table(snapshot.table_name(is_deployable), dialect=snapshot.model.dialect) - snapshots_with_table_names[snapshot].add(table.name) - table_schema = d.schema_(table.db, catalog=table.catalog) - tables_by_gateway_and_schema[snapshot.model_gateway][table_schema].add(table.name) - - def _get_data_objects( - schema: exp.Table, - object_names: t.Optional[t.Set[str]] = None, - gateway: t.Optional[str] = None, - ) -> t.Set[str]: - logger.info("Listing data objects in schema %s", schema.sql()) - objs = self.get_adapter(gateway).get_data_objects(schema, object_names) - return {obj.name for obj in objs} - - with self.concurrent_context(): - existing_objects: t.Set[str] = set() - # A schema can be shared across multiple engines, so we need to group tables by both gateway and schema - for gateway, tables_by_schema in tables_by_gateway_and_schema.items(): - objs_for_gateway = { - obj - for objs in concurrent_apply_to_values( - list(tables_by_schema), - lambda s: _get_data_objects( - schema=s, object_names=tables_by_schema.get(s), gateway=gateway - ), - self.ddl_concurrent_tasks, - ) - for obj in objs - } - existing_objects.update(objs_for_gateway) - - snapshots_to_create = [] - for snapshot, table_names in snapshots_with_table_names.items(): - missing_tables = table_names - existing_objects - if missing_tables or (snapshot.is_seed and not snapshot.intervals): + if snapshot.snapshot_id not in existing_data_objects or ( + snapshot.is_seed and not snapshot.intervals + ): snapshots_to_create.append(snapshot) return snapshots_to_create @@ -514,16 +478,26 @@ def migrate( allow_additive_snapshots: Set of snapshots that are allowed to have additive schema changes. deployability_index: Determines snapshots that are deployable in the context of this evaluation. """ + deployability_index = deployability_index or DeployabilityIndex.all_deployable() + target_data_objects = self._get_data_objects(target_snapshots, deployability_index) + if not target_data_objects: + return + + if not snapshots: + snapshots = {s.snapshot_id: s for s in target_snapshots} + allow_destructive_snapshots = allow_destructive_snapshots or set() allow_additive_snapshots = allow_additive_snapshots or set() - deployability_index = deployability_index or DeployabilityIndex.all_deployable() snapshots_by_name = {s.name: s for s in snapshots.values()} + snapshots_with_data_objects = [snapshots[s_id] for s_id in target_data_objects] with self.concurrent_context(): + # Only migrate snapshots for which there's an existing data object concurrent_apply_to_snapshots( - target_snapshots, + snapshots_with_data_objects, lambda s: self._migrate_snapshot( s, snapshots_by_name, + target_data_objects[s.snapshot_id], allow_destructive_snapshots, allow_additive_snapshots, self.get_adapter(s.model_gateway), @@ -1074,6 +1048,7 @@ def _migrate_snapshot( self, snapshot: Snapshot, snapshots: t.Dict[str, Snapshot], + target_data_object: t.Optional[DataObject], allow_destructive_snapshots: t.Set[str], allow_additive_snapshots: t.Set[str], adapter: EngineAdapter, @@ -1095,7 +1070,6 @@ def _migrate_snapshot( adapter.transaction(), adapter.session(snapshot.model.render_session_properties(**render_kwargs)), ): - target_data_object = adapter.get_data_object(target_table_name) table_exists = target_data_object is not None if adapter.drop_data_object_on_type_mismatch( target_data_object, _snapshot_to_data_object_type(snapshot) @@ -1447,6 +1421,62 @@ def _can_clone(self, snapshot: Snapshot, deployability_index: DeployabilityIndex and not deployability_index.is_deployable(snapshot) ) + def _get_data_objects( + self, + target_snapshots: t.Iterable[Snapshot], + deployability_index: DeployabilityIndex, + ) -> t.Dict[SnapshotId, DataObject]: + """Returns a dictionary of snapshot IDs to existing data objects of their physical tables. + + Args: + target_snapshots: Target snapshots. + deployability_index: The deployability index to determine whether to look for a deployable or + a non-deployable physical table. + + Returns: + A dictionary of snapshot IDs to existing data objects of their physical tables. If the data object + for a snapshot is not found, it will not be included in the dictionary. + """ + tables_by_gateway_and_schema: t.Dict[t.Union[str, None], t.Dict[exp.Table, set[str]]] = ( + defaultdict(lambda: defaultdict(set)) + ) + snapshots_by_table_name: t.Dict[str, Snapshot] = {} + for snapshot in target_snapshots: + if not snapshot.is_model or snapshot.is_symbolic: + continue + is_deployable = deployability_index.is_deployable(snapshot) + table = exp.to_table(snapshot.table_name(is_deployable), dialect=snapshot.model.dialect) + table_schema = d.schema_(table.db, catalog=table.catalog) + tables_by_gateway_and_schema[snapshot.model_gateway][table_schema].add(table.name) + snapshots_by_table_name[table.name] = snapshot + + def _get_data_objects_in_schema( + schema: exp.Table, + object_names: t.Optional[t.Set[str]] = None, + gateway: t.Optional[str] = None, + ) -> t.List[DataObject]: + logger.info("Listing data objects in schema %s", schema.sql()) + return self.get_adapter(gateway).get_data_objects(schema, object_names) + + with self.concurrent_context(): + existing_objects: t.List[DataObject] = [] + # A schema can be shared across multiple engines, so we need to group tables by both gateway and schema + for gateway, tables_by_schema in tables_by_gateway_and_schema.items(): + objs_for_gateway = [ + obj + for objs in concurrent_apply_to_values( + list(tables_by_schema), + lambda s: _get_data_objects_in_schema( + schema=s, object_names=tables_by_schema.get(s), gateway=gateway + ), + self.ddl_concurrent_tasks, + ) + for obj in objs + ] + existing_objects.extend(objs_for_gateway) + + return {snapshots_by_table_name[obj.name].snapshot_id: obj for obj in existing_objects} + def _evaluation_strategy(snapshot: SnapshotInfoLike, adapter: EngineAdapter) -> EvaluationStrategy: klass: t.Type diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 9b1e81c0f4..6f610a696a 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -880,12 +880,12 @@ def test_create_only_dev_table_exists(mocker: MockerFixture, adapter_mock, make_ adapter_mock.table_exists.return_value = True evaluator = SnapshotEvaluator(adapter_mock) - evaluator.create([snapshot], {}) + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) adapter_mock.create_view.assert_not_called() adapter_mock.get_data_objects.assert_called_once_with( schema_("sqlmesh__test_schema"), { - f"test_schema__test_model__{snapshot.version}", + f"test_schema__test_model__{snapshot.version}__dev", }, ) @@ -1003,14 +1003,15 @@ def test_create_tables_exist( evaluator = SnapshotEvaluator(adapter_mock) snapshot.categorize_as(category=snapshot_category, forward_only=forward_only) + table_name = ( + f"db__model__{snapshot.version}" + if deployability_index.is_deployable(snapshot) + else f"db__model__{snapshot.version}__dev" + ) + adapter_mock.get_data_objects.return_value = [ DataObject( - name=f"db__model__{snapshot.version}__dev", - schema="sqlmesh__db", - type=DataObjectType.TABLE, - ), - DataObject( - name=f"db__model__{snapshot.version}", + name=table_name, schema="sqlmesh__db", type=DataObjectType.TABLE, ), @@ -1024,11 +1025,7 @@ def test_create_tables_exist( adapter_mock.get_data_objects.assert_called_once_with( schema_("sqlmesh__db"), - { - f"db__model__{snapshot.version}" - if deployability_index.is_deployable(snapshot) - else f"db__model__{snapshot.version}__dev", - }, + {table_name}, ) adapter_mock.create_schema.assert_not_called() adapter_mock.create_table.assert_not_called() @@ -1279,7 +1276,20 @@ def test_migrate(mocker: MockerFixture, make_snapshot, make_mocked_engine_adapte adapter.with_settings = lambda **kwargs: adapter # type: ignore session_spy = mocker.spy(adapter, "session") - current_table = "sqlmesh__test_schema.test_schema__test_model__1" + model = SqlModel( + name="test_schema.test_model", + kind=IncrementalByTimeRangeKind( + time_column="a", on_destructive_change=OnDestructiveChange.ALLOW + ), + storage_format="parquet", + query=parse_one("SELECT c, a FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), + ) + snapshot = make_snapshot(model, version="1") + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True + snapshot.previous_versions = snapshot.all_versions + + current_table = snapshot.table_name() def columns(table_name): if table_name == current_table: @@ -1296,32 +1306,27 @@ def columns(table_name): adapter.table_exists = lambda _: True # type: ignore mocker.patch.object( adapter, - "get_data_object", - return_value=DataObject(schema="test_schema", name="test_model", type="table"), + "get_data_objects", + return_value=[ + DataObject( + schema="test_schema", + name=f"test_schema__test_model__{snapshot.version}", + type="table", + ) + ], ) evaluator = SnapshotEvaluator(adapter) - model = SqlModel( - name="test_schema.test_model", - kind=IncrementalByTimeRangeKind( - time_column="a", on_destructive_change=OnDestructiveChange.ALLOW - ), - storage_format="parquet", - query=parse_one("SELECT c, a FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), - ) - snapshot = make_snapshot(model, version="1") - snapshot.change_category = SnapshotChangeCategory.BREAKING - snapshot.forward_only = True - snapshot.previous_versions = snapshot.all_versions - - evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + evaluator.migrate([snapshot], {}) adapter.cursor.execute.assert_has_calls( [ - call('ALTER TABLE "sqlmesh__test_schema"."test_schema__test_model__1" DROP COLUMN "b"'), call( - 'ALTER TABLE "sqlmesh__test_schema"."test_schema__test_model__1" ADD COLUMN "a" INT' + f'ALTER TABLE "sqlmesh__test_schema"."test_schema__test_model__{snapshot.version}" DROP COLUMN "b"' + ), + call( + f'ALTER TABLE "sqlmesh__test_schema"."test_schema__test_model__{snapshot.version}" ADD COLUMN "a" INT' ), ] ) @@ -1371,15 +1376,6 @@ def test_migrate_view( change_category: SnapshotChangeCategory, forward_only: bool, ): - adapter = make_mocked_engine_adapter(EngineAdapter) - mocker.patch.object( - adapter, - "get_data_object", - return_value=DataObject(schema="test_schema", name="test_model", type="view"), - ) - - evaluator = SnapshotEvaluator(adapter) - model = SqlModel( name="test_schema.test_model", kind=ViewKind(), @@ -1390,7 +1386,20 @@ def test_migrate_view( snapshot.change_category = change_category snapshot.forward_only = forward_only - evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + adapter = make_mocked_engine_adapter(EngineAdapter) + mocker.patch( + "sqlmesh.core.engine_adapter.base.EngineAdapter.get_data_objects", + return_value=[ + DataObject( + schema="test_schema", + name=f"test_schema__test_model__{snapshot.version}", + type="view", + ) + ], + ) + + evaluator = SnapshotEvaluator(adapter) + evaluator.migrate([snapshot], {}) adapter.cursor.execute.assert_not_called() @@ -1400,19 +1409,6 @@ def test_migrate_snapshot_data_object_type_mismatch( make_snapshot, make_mocked_engine_adapter, ): - adapter = make_mocked_engine_adapter(EngineAdapter) - adapter.with_settings = lambda **kwargs: adapter # type: ignore - mocker.patch.object( - adapter, - "get_data_object", - return_value=DataObject( - schema="sqlmesh__test_schema", name="test_schema__test_model__1", type="view" - ), - ) - mocker.patch.object(adapter, "table_exists", return_value=False) - - evaluator = SnapshotEvaluator(adapter) - model = SqlModel( name="test_schema.test_model", kind=FullKind(), @@ -1424,11 +1420,29 @@ def test_migrate_snapshot_data_object_type_mismatch( snapshot.forward_only = True snapshot.previous_versions = snapshot.all_versions - evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.with_settings = lambda **kwargs: adapter # type: ignore + mocker.patch( + "sqlmesh.core.engine_adapter.base.EngineAdapter.get_data_objects", + return_value=[ + DataObject( + schema="sqlmesh__test_schema", + name=f"test_schema__test_model__{snapshot.version}", + type="view", + ) + ], + ) + mocker.patch.object(adapter, "table_exists", return_value=False) + + evaluator = SnapshotEvaluator(adapter) + + evaluator.migrate([snapshot], {}) adapter.cursor.execute.assert_has_calls( [ - call('DROP VIEW IF EXISTS "sqlmesh__test_schema"."test_schema__test_model__1"'), + call( + f'DROP VIEW IF EXISTS "sqlmesh__test_schema"."test_schema__test_model__{snapshot.version}"' + ), ] ) @@ -1639,14 +1653,6 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.previous_versions = snapshot.all_versions - adapter_mock.get_data_objects.return_value = [ - DataObject( - name=f"test_schema__test_model__{snapshot.version}", - schema="sqlmesh__test_schema", - type=DataObjectType.TABLE, - ), - ] - evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) adapter_mock.create_table.assert_called_once_with( @@ -1663,7 +1669,7 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) ) adapter_mock.clone_table.assert_called_once_with( - f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", + f"sqlmesh__test_schema.test_schema__test_model__{snapshot.dev_version}__dev", f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}", replace=True, rendered_physical_properties={}, @@ -1709,14 +1715,6 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.previous_versions = snapshot.all_versions - adapter_mock.get_data_objects.return_value = [ - DataObject( - name=f"test_schema__test_model__{snapshot.version}", - schema="sqlmesh__test_schema", - type=DataObjectType.TABLE, - ), - ] - with pytest.raises(SnapshotCreationFailedError): evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) @@ -1774,14 +1772,6 @@ def test_create_clone_in_dev_self_referencing( snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.previous_versions = snapshot.all_versions - adapter_mock.get_data_objects.return_value = [ - DataObject( - name=f"test_schema__test_model__{snapshot.version}", - schema="sqlmesh__test_schema", - type=DataObjectType.TABLE, - ), - ] - evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) adapter_mock.create_table.assert_called_once_with( @@ -1815,9 +1805,21 @@ def test_on_destructive_change_runtime_check( make_snapshot, make_mocked_engine_adapter, ): + # SQLMesh default: ERROR + model = SqlModel( + name="test_schema.test_model", + kind=IncrementalByTimeRangeKind(time_column="a"), + query=parse_one("SELECT c, a FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), + ) + snapshot = make_snapshot(model, version="1") + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True + snapshot.previous_versions = snapshot.all_versions + adapter = make_mocked_engine_adapter(EngineAdapter) adapter.with_settings = lambda **kwargs: adapter # type: ignore - current_table = "sqlmesh__test_schema.test_schema__test_model__1" + + current_table = snapshot.table_name() def columns(table_name): if table_name == current_table: @@ -1831,27 +1833,21 @@ def columns(table_name): } adapter.columns = columns # type: ignore - mocker.patch.object( - adapter, - "get_data_object", - return_value=DataObject(schema="test_schema", name="test_model", type=DataObjectType.TABLE), + mocker.patch( + "sqlmesh.core.engine_adapter.base.EngineAdapter.get_data_objects", + return_value=[ + DataObject( + schema="test_schema", + name=f"test_schema__test_model__{snapshot.version}", + type=DataObjectType.TABLE, + ) + ], ) evaluator = SnapshotEvaluator(adapter) - # SQLMesh default: ERROR - model = SqlModel( - name="test_schema.test_model", - kind=IncrementalByTimeRangeKind(time_column="a"), - query=parse_one("SELECT c, a FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), - ) - snapshot = make_snapshot(model, version="1") - snapshot.change_category = SnapshotChangeCategory.BREAKING - snapshot.forward_only = True - snapshot.previous_versions = snapshot.all_versions - with pytest.raises(NodeExecutionFailedError) as ex: - evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + evaluator.migrate([snapshot], {}) destructive_change_err = ex.value.__cause__ assert isinstance(destructive_change_err, DestructiveChangeError) @@ -1875,7 +1871,7 @@ def columns(table_name): logger = logging.getLogger("sqlmesh.core.snapshot.evaluator") with patch.object(logger, "warning") as mock_logger: - evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + evaluator.migrate([snapshot], {}) assert ( mock_logger.call_args[0][0] == "\nPlan requires destructive change to forward-only model '\"test_schema\".\"test_model\"'s schema that drops column 'b'.\n\nSchema changes:\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 DROP COLUMN b\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 ADD COLUMN a INT" @@ -1887,7 +1883,6 @@ def columns(table_name): [snapshot], {}, {'"test_schema"."test_model"'}, - deployability_index=DeployabilityIndex.none_deployable(), ) assert mock_logger.call_count == 0 @@ -1897,9 +1892,20 @@ def test_on_additive_change_runtime_check( make_snapshot, make_mocked_engine_adapter, ): + # SQLMesh default: ERROR + model = SqlModel( + name="test_schema.test_model", + kind=IncrementalByTimeRangeKind(time_column="a", on_additive_change=OnAdditiveChange.ERROR), + query=parse_one("SELECT c, a, b FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), + ) + snapshot = make_snapshot(model, version="1") + snapshot.change_category = SnapshotChangeCategory.BREAKING + snapshot.forward_only = True + snapshot.previous_versions = snapshot.all_versions + adapter = make_mocked_engine_adapter(EngineAdapter) adapter.with_settings = lambda **kwargs: adapter # type: ignore - current_table = "sqlmesh__test_schema.test_schema__test_model__1" + current_table = snapshot.table_name() def columns(table_name): if table_name == current_table: @@ -1914,27 +1920,21 @@ def columns(table_name): } adapter.columns = columns # type: ignore - mocker.patch.object( - adapter, - "get_data_object", - return_value=DataObject(schema="test_schema", name="test_model", type=DataObjectType.TABLE), + mocker.patch( + "sqlmesh.core.engine_adapter.base.EngineAdapter.get_data_objects", + return_value=[ + DataObject( + schema="test_schema", + name=f"test_schema__test_model__{snapshot.version}", + type=DataObjectType.TABLE, + ) + ], ) evaluator = SnapshotEvaluator(adapter) - # SQLMesh default: ERROR - model = SqlModel( - name="test_schema.test_model", - kind=IncrementalByTimeRangeKind(time_column="a", on_additive_change=OnAdditiveChange.ERROR), - query=parse_one("SELECT c, a, b FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"), - ) - snapshot = make_snapshot(model, version="1") - snapshot.change_category = SnapshotChangeCategory.BREAKING - snapshot.forward_only = True - snapshot.previous_versions = snapshot.all_versions - with pytest.raises(NodeExecutionFailedError) as ex: - evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + evaluator.migrate([snapshot], {}) additive_change_error = ex.value.__cause__ assert isinstance(additive_change_error, AdditiveChangeError) @@ -1958,7 +1958,7 @@ def columns(table_name): logger = logging.getLogger("sqlmesh.core.snapshot.evaluator") with patch.object(logger, "warning") as mock_logger: - evaluator.migrate([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + evaluator.migrate([snapshot], {}) assert ( mock_logger.call_args[0][0] == "\nPlan requires additive change to forward-only model '\"test_schema\".\"test_model\"'s schema that adds column 'b'.\n\nSchema changes:\n ALTER TABLE sqlmesh__test_schema.test_schema__test_model__1 ADD COLUMN b INT" @@ -2804,11 +2804,6 @@ def test_create_seed_no_intervals(mocker: MockerFixture, adapter_mock, make_snap schema="sqlmesh__db", type=DataObjectType.TABLE, ), - DataObject( - name=f"db__seed__{snapshot.version}__dev", - schema="sqlmesh__db", - type=DataObjectType.TABLE, - ), ] evaluator = SnapshotEvaluator(adapter_mock) @@ -3763,14 +3758,6 @@ def test_create_managed_forward_only_with_previous_version_doesnt_clone_for_dev_ ), ) - adapter_mock.get_data_objects.return_value = [ - DataObject( - name=f"test_schema__test_model__{snapshot.version}", - schema="sqlmesh__test_schema", - type=DataObjectType.MANAGED_TABLE, - ), - ] - evaluator.create( target_snapshots=[snapshot], snapshots={}, @@ -3806,12 +3793,16 @@ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_moc assert new_snapshot.table_name() == snapshot.table_name() - adapter_mock.get_data_object.return_value = DataObject( - schema="test_schema", name="test_model", type=DataObjectType.TABLE - ) + adapter_mock.get_data_objects.return_value = [ + DataObject( + schema="test_schema", + name=f"db__model__{new_snapshot.version}", + type=DataObjectType.TABLE, + ) + ] adapter_mock.drop_data_object_on_type_mismatch.return_value = False - evaluator.migrate([new_snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + evaluator.migrate([new_snapshot], {}) common_kwargs: t.Dict[str, t.Any] = dict( table_format=None, @@ -3883,9 +3874,13 @@ def test_migrate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.previous_versions = snapshot.all_versions - adapter_mock.get_data_object.return_value = DataObject( - schema="test_schema", name="test_model", type=DataObjectType.MANAGED_TABLE - ) + adapter_mock.get_data_objects.return_value = [ + DataObject( + schema="test_schema", + name=f"test_schema__test_model__{snapshot.version}", + type=DataObjectType.MANAGED_TABLE, + ) + ] adapter_mock.drop_data_object_on_type_mismatch.return_value = False # no schema changes - no-op @@ -3893,7 +3888,6 @@ def test_migrate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): evaluator.migrate( target_snapshots=[snapshot], snapshots={}, - deployability_index=DeployabilityIndex.none_deployable(), ) adapter_mock.create_table.assert_not_called() @@ -3908,7 +3902,6 @@ def test_migrate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): evaluator.migrate( target_snapshots=[snapshot], snapshots={}, - deployability_index=DeployabilityIndex.none_deployable(), ) sqlmesh_err = ex.value.__cause__ @@ -4065,34 +4058,6 @@ def test_multiple_engine_migration( adapter_two.with_settings.return_value = adapter_two engine_adapters = {"one": adapter_one, "two": adapter_two} - current_table = "sqlmesh__test_schema.test_schema__test_model__1" - - def columns(table_name): - if table_name == current_table: - return { - "c": exp.DataType.build("int"), - "b": exp.DataType.build("int"), - } - return { - "c": exp.DataType.build("int"), - "a": exp.DataType.build("int"), - } - - adapter_two.columns.side_effect = columns - adapter_two.get_data_object.return_value = DataObject( - schema="test_schema", name="test_model_2", type=DataObjectType.TABLE - ) - adapter_two.drop_data_object_on_type_mismatch.return_value = False - - mocker.patch.object(adapter_one, "columns", side_effect=columns) - mocker.patch.object( - adapter_one, - "get_data_object", - return_value=DataObject(schema="test_schema", name="test_model", type=DataObjectType.TABLE), - ) - - evaluator = SnapshotEvaluator(engine_adapters) - model = SqlModel( name="test_schema.test_model", kind=IncrementalByTimeRangeKind( @@ -4116,10 +4081,41 @@ def columns(table_name): snapshot_2.change_category = SnapshotChangeCategory.BREAKING snapshot_2.forward_only = True snapshot_2.previous_versions = snapshot_2.all_versions - evaluator.migrate( - [snapshot_1, snapshot_2], {}, deployability_index=DeployabilityIndex.none_deployable() + + def columns(table_name): + if table_name == snapshot_1.table_name(): + return { + "c": exp.DataType.build("int"), + "b": exp.DataType.build("int"), + } + return { + "c": exp.DataType.build("int"), + "a": exp.DataType.build("int"), + } + + adapter_two.columns.side_effect = columns + adapter_two.drop_data_object_on_type_mismatch.return_value = False + + mocker.patch.object(adapter_one, "columns", side_effect=columns) + mocker.patch( + "sqlmesh.core.engine_adapter.base.EngineAdapter.get_data_objects", + return_value=[ + DataObject( + schema="test_schema", + name=f"test_schema__test_model__{snapshot_1.version}", + type=DataObjectType.TABLE, + ), + DataObject( + schema="test_schema", + name=f"test_schema__test_model_2__{snapshot_2.version}", + type=DataObjectType.TABLE, + ), + ], ) + evaluator = SnapshotEvaluator(engine_adapters) + evaluator.migrate([snapshot_1, snapshot_2], {}) + adapter_one.cursor.execute.assert_has_calls( [ call('ALTER TABLE "sqlmesh__test_schema"."test_schema__test_model__1" DROP COLUMN "b"'), From 58e3eca3a7260fa943eafb6042e297e016b30ce3 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:55:55 -0700 Subject: [PATCH 0818/1056] fix: dbt microbatch parameter conversion (#5298) --- sqlmesh/dbt/model.py | 16 +++++++--------- tests/dbt/test_model.py | 7 +++++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 20d0f8cd1a..2f040ef5f8 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -333,15 +333,8 @@ def model_kind(self, context: DbtContext) -> ModelKind: raise ConfigError( f"{self.canonical_name(context)}: 'event_time' is required for microbatch incremental strategy." ) - concurrent_batches = self._get_field_value("concurrent_batches") - if concurrent_batches is True: - if incremental_by_kind_kwargs.get("batch_size"): - get_console().log_warning( - f"'concurrent_batches' is set to True and 'batch_size' are defined in '{self.canonical_name(context)}'. The batch size will be set to the value of `batch_size`." - ) - incremental_by_kind_kwargs["batch_size"] = incremental_by_kind_kwargs.get( - "batch_size", 1 - ) + # dbt microbatch always processes batches in a size of 1 + incremental_by_kind_kwargs["batch_size"] = 1 else: if not self.time_column: raise ConfigError( @@ -651,6 +644,11 @@ def to_sqlmesh( ) else: model_kwargs["start"] = begin + # If user explicitly disables concurrent batches then we want to set depends on past to true which we + # will do by including the model in the depends_on + if self.concurrent_batches is not None and self.concurrent_batches is False: + depends_on = model_kwargs.get("depends_on", set()) + depends_on.add(self.canonical_name(context)) model_kwargs["start"] = model_kwargs.get( "start", context.sqlmesh_config.model_defaults.start diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index 7d4672c512..9f4f75bdbd 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -219,6 +219,7 @@ def test_load_microbatch_all_defined( column=exp.to_column("ds", quoted=True), format="%Y-%m-%d" ) assert model.kind.batch_size == 1 + assert model.depends_on_self is False @pytest.mark.slow @@ -259,7 +260,8 @@ def test_load_microbatch_all_defined_diff_values( assert model.kind.time_column == TimeColumn( column=exp.to_column("blah", quoted=True), format="%Y-%m-%d" ) - assert model.kind.batch_size is None + assert model.kind.batch_size == 1 + assert model.depends_on_self is True @pytest.mark.slow @@ -297,4 +299,5 @@ def test_load_microbatch_required_only( assert model.kind.time_column == TimeColumn( column=exp.to_column("ds", quoted=True), format="%Y-%m-%d" ) - assert model.kind.batch_size is None + assert model.kind.batch_size == 1 + assert model.depends_on_self is False From cef3859d59a5be50202383cbb214807d5e4b2834 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 4 Sep 2025 16:29:16 -0700 Subject: [PATCH 0819/1056] Fix: Allow partials by default for all models converted from a dbt project (#5303) --- sqlmesh/dbt/model.py | 8 ++------ tests/dbt/test_config.py | 1 + tests/dbt/test_transformation.py | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 2f040ef5f8..9997d464ae 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -83,7 +83,7 @@ class ModelConfig(BaseModelConfig): batch_concurrency: t.Optional[int] = None forward_only: bool = True disable_restatement: t.Optional[bool] = None - allow_partials: t.Optional[bool] = None + allow_partials: bool = True physical_version: t.Optional[str] = None auto_restatement_cron: t.Optional[str] = None auto_restatement_intervals: t.Optional[int] = None @@ -620,11 +620,7 @@ def to_sqlmesh( model_kwargs["physical_properties"] = physical_properties allow_partials = model_kwargs.pop("allow_partials", None) - if ( - allow_partials is None - and kind.is_materialized - and not kind.is_incremental_by_time_range - ): + if allow_partials is None: # Set allow_partials to True for dbt models to preserve the original semantics. allow_partials = True diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index ecd95a43c4..4e3e78eea9 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -137,6 +137,7 @@ def test_model_to_sqlmesh_fields(dbt_dummy_postgres_config: PostgresConfig): assert model.dialect == "postgres" assert model.owner == "Sally" assert model.tags == ["test", "incremental"] + assert model.allow_partials kind = t.cast(IncrementalByUniqueKeyKind, model.kind) assert kind.batch_size == 5 assert kind.lookback == 3 diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 1db3d469d8..b5de61031b 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -2110,11 +2110,11 @@ def test_allow_partials_by_default(): sql="SELECT * FROM baz", materialized=Materialization.TABLE.value, ) - assert model.allow_partials is None + assert model.allow_partials assert model.to_sqlmesh(context).allow_partials model.materialized = Materialization.INCREMENTAL.value - assert model.allow_partials is None + assert model.allow_partials assert model.to_sqlmesh(context).allow_partials model.allow_partials = True From 61a0b115ed596a39d13d91bd1667685f37293e89 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:00:24 -0700 Subject: [PATCH 0820/1056] feat: add dedicated `incremental_by_time_range` strategy (#5306) --- docs/integrations/dbt.md | 24 +-- .../models/customer_revenue_by_day.sql | 2 +- .../models/waiter_as_customer_by_day.sql | 2 +- .../models/waiter_revenue_by_day.sql | 2 +- .../models/waiter_revenue_by_day_v1.sql | 2 +- sqlmesh/dbt/model.py | 79 ++++++---- tests/dbt/test_manifest.py | 5 +- tests/dbt/test_model.py | 146 ++++++++++++++++++ 8 files changed, 214 insertions(+), 48 deletions(-) diff --git a/docs/integrations/dbt.md b/docs/integrations/dbt.md index 4342f47779..7cbef5b8fa 100644 --- a/docs/integrations/dbt.md +++ b/docs/integrations/dbt.md @@ -219,7 +219,7 @@ This section describes how to adapt dbt's incremental models to run on sqlmesh a SQLMesh supports two approaches to implement [idempotent](../concepts/glossary.md#idempotency) incremental loads: * Using merge (with the sqlmesh [`INCREMENTAL_BY_UNIQUE_KEY` model kind](../concepts/models/model_kinds.md#incremental_by_unique_key)) -* Using insert-overwrite/delete+insert (with the sqlmesh [`INCREMENTAL_BY_TIME_RANGE` model kind](../concepts/models/model_kinds.md#incremental_by_time_range)) +* Using [`INCREMENTAL_BY_TIME_RANGE` model kind](../concepts/models/model_kinds.md#incremental_by_time_range) #### Incremental by unique key @@ -233,28 +233,22 @@ To enable incremental_by_unique_key incrementality, the model configuration shou #### Incremental by time range -To enable incremental_by_time_range incrementality, the model configuration should contain: +To enable incremental_by_time_range incrementality, the model configuration must contain: -* The `time_column` key with the model's time column field name as the value (see [`time column`](../concepts/models/model_kinds.md#time-column) for details) * The `materialized` key with value `'incremental'` -* Either: - * The `incremental_strategy` key with value `'insert_overwrite'` or - * The `incremental_strategy` key with value `'delete+insert'` - * Note: in this context, these two strategies are synonyms. Regardless of which one is specified SQLMesh will use the [`best incremental strategy`](../concepts/models/model_kinds.md#materialization-strategy) for the target engine. +* The `incremental_strategy` key with the value `incremental_by_time_range` +* The `time_column` key with the model's time column field name as the value (see [`time column`](../concepts/models/model_kinds.md#time-column) for details) ### Incremental logic -SQLMesh requires a new jinja block gated by `{% if sqlmesh_incremental is defined %}`. The new block should supersede the existing `{% if is_incremental() %}` block and contain the `WHERE` clause selecting the time interval. +Unlike dbt incremental strategies, SQLMesh does not require the use of `is_incremental` jinja blocks to implement incremental logic. +Instead, SQLMesh provides predefined time macro variables that can be used in the model's SQL to filter data based on the time column. For example, the SQL `WHERE` clause with the "ds" column goes in a new jinja block gated by `{% if sqlmesh_incremental is defined %}` as follows: ```bash -> {% if sqlmesh_incremental is defined %} > WHERE > ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' -> {% elif is_incremental() %} -> ; < your existing is_incremental block > -> {% endif %} ``` `{{ start_ds }}` and `{{ end_ds }}` are the jinja equivalents of SQLMesh's `@start_ds` and `@end_ds` predefined time macro variables. See all [predefined time variables](../concepts/macros/macro_variables.md) available in jinja. @@ -263,13 +257,11 @@ For example, the SQL `WHERE` clause with the "ds" column goes in a new jinja blo SQLMesh provides configuration parameters that enable control over how incremental computations occur. These parameters are set in the model's `config` block. -The [`batch_size` parameter](../concepts/models/overview.md#batch_size) determines the maximum number of time intervals to run in a single job. - -The [`lookback` parameter](../concepts/models/overview.md#lookback) is used to capture late arriving data. It sets the number of units of late arriving data the model should expect and must be a positive integer. +See [Incremental Model Properties](../concepts/models/overview.md#incremental-model-properties) for the full list of incremental model configuration parameters. **Note:** By default, all incremental dbt models are configured to be [forward-only](../concepts/plans.md#forward-only-plans). However, you can change this behavior by setting the `forward_only: false` setting either in the configuration of an individual model or globally for all models in the `dbt_project.yaml` file. The [forward-only](../concepts/plans.md#forward-only-plans) mode aligns more closely with the typical operation of dbt and therefore better meets user's expectations. -Similarly, the [allow_partials](../concepts/models/overview.md#allow_partials) parameter is set to `true` by default for incremental dbt models unless the time column is specified, or the `allow_partials` parameter is explicitly set to `false` in the model configuration. +Similarly, the [allow_partials](../concepts/models/overview.md#allow_partials) parameter is set to `true` by default unless the `allow_partials` parameter is explicitly set to `false` in the model configuration. #### on_schema_change diff --git a/examples/sushi_dbt/models/customer_revenue_by_day.sql b/examples/sushi_dbt/models/customer_revenue_by_day.sql index f3f49cfc14..9810481eff 100644 --- a/examples/sushi_dbt/models/customer_revenue_by_day.sql +++ b/examples/sushi_dbt/models/customer_revenue_by_day.sql @@ -1,7 +1,7 @@ {{ config( materialized='incremental', - incremental_strategy='delete+insert', + incremental_strategy='incremental_by_time_range', cluster_by=['ds'], time_column='ds', ) diff --git a/examples/sushi_dbt/models/waiter_as_customer_by_day.sql b/examples/sushi_dbt/models/waiter_as_customer_by_day.sql index 3d4967aec7..a1145c2b5c 100644 --- a/examples/sushi_dbt/models/waiter_as_customer_by_day.sql +++ b/examples/sushi_dbt/models/waiter_as_customer_by_day.sql @@ -1,7 +1,7 @@ {{ config( materialized='incremental', - incremental_strategy='delete+insert', + incremental_strategy='incremental_by_time_range', cluster_by=['ds'], time_column='ds', ) diff --git a/examples/sushi_dbt/models/waiter_revenue_by_day.sql b/examples/sushi_dbt/models/waiter_revenue_by_day.sql index d430c6125b..670e238962 100644 --- a/examples/sushi_dbt/models/waiter_revenue_by_day.sql +++ b/examples/sushi_dbt/models/waiter_revenue_by_day.sql @@ -1,7 +1,7 @@ {{ config( materialized='incremental', - incremental_strategy='delete+insert', + incremental_strategy='incremental_by_time_range', cluster_by=['ds'], time_column='ds', ) diff --git a/examples/sushi_dbt/models/waiter_revenue_by_day_v1.sql b/examples/sushi_dbt/models/waiter_revenue_by_day_v1.sql index d430c6125b..670e238962 100644 --- a/examples/sushi_dbt/models/waiter_revenue_by_day_v1.sql +++ b/examples/sushi_dbt/models/waiter_revenue_by_day_v1.sql @@ -1,7 +1,7 @@ {{ config( materialized='incremental', - incremental_strategy='delete+insert', + incremental_strategy='incremental_by_time_range', cluster_by=['ds'], time_column='ds', ) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 9997d464ae..a4ebf93ae5 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -25,7 +25,14 @@ ManagedKind, create_sql_model, ) -from sqlmesh.core.model.kind import SCDType2ByTimeKind, OnDestructiveChange, OnAdditiveChange +from sqlmesh.core.model.kind import ( + SCDType2ByTimeKind, + OnDestructiveChange, + OnAdditiveChange, + on_destructive_change_validator, + on_additive_change_validator, + TimeColumn, +) from sqlmesh.dbt.basemodel import BaseModelConfig, Materialization, SnapshotStrategy from sqlmesh.dbt.common import SqlStr, sql_str_validator from sqlmesh.utils.errors import ConfigError @@ -41,7 +48,9 @@ logger = logging.getLogger(__name__) -INCREMENTAL_BY_TIME_STRATEGIES = set(["delete+insert", "insert_overwrite", "microbatch"]) +INCREMENTAL_BY_TIME_RANGE_STRATEGIES = set( + ["delete+insert", "insert_overwrite", "microbatch", "incremental_by_time_range"] +) INCREMENTAL_BY_UNIQUE_KEY_STRATEGIES = set(["merge"]) @@ -77,7 +86,7 @@ class ModelConfig(BaseModelConfig): # sqlmesh fields sql: SqlStr = SqlStr("") - time_column: t.Optional[str] = None + time_column: t.Optional[TimeColumn] = None cron: t.Optional[str] = None interval_unit: t.Optional[str] = None batch_concurrency: t.Optional[int] = None @@ -87,6 +96,9 @@ class ModelConfig(BaseModelConfig): physical_version: t.Optional[str] = None auto_restatement_cron: t.Optional[str] = None auto_restatement_intervals: t.Optional[int] = None + partition_by_time_column: t.Optional[bool] = None + on_destructive_change: t.Optional[OnDestructiveChange] = None + on_additive_change: t.Optional[OnAdditiveChange] = None # DBT configuration fields cluster_by: t.Optional[t.List[str]] = None @@ -139,6 +151,9 @@ class ModelConfig(BaseModelConfig): incremental_predicates: t.Optional[t.List[str]] = None _sql_validator = sql_str_validator + _on_destructive_change_validator = on_destructive_change_validator + _on_additive_change_validator = on_additive_change_validator + _time_column_validator = TimeColumn.validator() @field_validator( "unique_key", @@ -230,17 +245,6 @@ def snapshot_strategy(self) -> t.Optional[SnapshotStrategy]: def table_schema(self) -> str: return self.target_schema or super().table_schema - def _get_overlapping_field_value( - self, context: DbtContext, dbt_field_name: str, sqlmesh_field_name: str - ) -> t.Optional[t.Any]: - dbt_field = self._get_field_value(dbt_field_name) - sqlmesh_field = getattr(self, sqlmesh_field_name, None) - if dbt_field is not None and sqlmesh_field is not None: - get_console().log_warning( - f"Both '{dbt_field_name}' and '{sqlmesh_field_name}' are set for model '{self.canonical_name(context)}'. '{sqlmesh_field_name}' will be used." - ) - return sqlmesh_field if sqlmesh_field is not None else dbt_field - def model_kind(self, context: DbtContext) -> ModelKind: """ Get the sqlmesh ModelKind @@ -275,8 +279,12 @@ def model_kind(self, context: DbtContext) -> ModelKind: "Valid values are 'ignore', 'fail', 'append_new_columns', 'sync_all_columns'." ) - incremental_kind_kwargs["on_destructive_change"] = on_destructive_change - incremental_kind_kwargs["on_additive_change"] = on_additive_change + incremental_kind_kwargs["on_destructive_change"] = ( + self._get_field_value("on_destructive_change") or on_destructive_change + ) + incremental_kind_kwargs["on_additive_change"] = ( + self._get_field_value("on_additive_change") or on_additive_change + ) auto_restatement_cron_value = self._get_field_value("auto_restatement_cron") if auto_restatement_cron_value is not None: incremental_kind_kwargs["auto_restatement_cron"] = auto_restatement_cron_value @@ -292,7 +300,8 @@ def model_kind(self, context: DbtContext) -> ModelKind: incremental_kind_kwargs["forward_only"] = forward_only_value is_incremental_by_time_range = self.time_column or ( - self.incremental_strategy and self.incremental_strategy == "microbatch" + self.incremental_strategy + and self.incremental_strategy in {"microbatch", "incremental_by_time_range"} ) # Get shared incremental by kwargs for field in ("batch_size", "batch_concurrency", "lookback"): @@ -313,22 +322,29 @@ def model_kind(self, context: DbtContext) -> ModelKind: ) incremental_by_kind_kwargs["disable_restatement"] = disable_restatement - # Incremental by time range which includes microbatch if is_incremental_by_time_range: strategy = self.incremental_strategy or target.default_incremental_strategy( IncrementalByTimeRangeKind ) - if strategy not in INCREMENTAL_BY_TIME_STRATEGIES: + if strategy not in INCREMENTAL_BY_TIME_RANGE_STRATEGIES: get_console().log_warning( f"SQLMesh incremental by time strategy is not compatible with '{strategy}' incremental strategy in model '{self.canonical_name(context)}'. " - f"Supported strategies include {collection_to_str(INCREMENTAL_BY_TIME_STRATEGIES)}." + f"Supported strategies include {collection_to_str(INCREMENTAL_BY_TIME_RANGE_STRATEGIES)}." ) - if strategy == "microbatch": - time_column = self._get_overlapping_field_value( - context, "event_time", "time_column" + if self.time_column and strategy != "incremental_by_time_range": + get_console().log_warning( + f"Using `time_column` on a model with incremental_strategy '{strategy}' has been deprecated. " + f"Please use `incremental_by_time_range` instead in model '{self.canonical_name(context)}'." ) + + if strategy == "microbatch": + if self.time_column: + raise ConfigError( + f"{self.canonical_name(context)}: 'time_column' cannot be used with 'microbatch' incremental strategy. Use 'event_time' instead." + ) + time_column = self._get_field_value("event_time") if not time_column: raise ConfigError( f"{self.canonical_name(context)}: 'event_time' is required for microbatch incremental strategy." @@ -342,11 +358,22 @@ def model_kind(self, context: DbtContext) -> ModelKind: ) time_column = self.time_column + incremental_by_time_range_kwargs = { + "time_column": time_column, + } + if self.auto_restatement_intervals: + incremental_by_time_range_kwargs["auto_restatement_intervals"] = ( + self.auto_restatement_intervals + ) + if self.partition_by_time_column is not None: + incremental_by_time_range_kwargs["partition_by_time_column"] = ( + self.partition_by_time_column + ) + return IncrementalByTimeRangeKind( - time_column=time_column, - auto_restatement_intervals=self.auto_restatement_intervals, **incremental_kind_kwargs, **incremental_by_kind_kwargs, + **incremental_by_time_range_kwargs, ) if self.unique_key: @@ -384,7 +411,7 @@ def model_kind(self, context: DbtContext) -> ModelKind: IncrementalUnmanagedKind ) return IncrementalUnmanagedKind( - insert_overwrite=strategy in INCREMENTAL_BY_TIME_STRATEGIES, + insert_overwrite=strategy in INCREMENTAL_BY_TIME_RANGE_STRATEGIES, disable_restatement=incremental_by_kind_kwargs["disable_restatement"], **incremental_kind_kwargs, ) diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index ba8971e9b2..1ea94cceb0 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -5,6 +5,7 @@ import pytest from sqlmesh.core.config import ModelDefaultsConfig +from sqlmesh.core.model import TimeColumn from sqlmesh.dbt.basemodel import Dependencies from sqlmesh.dbt.common import ModelAttrs from sqlmesh.dbt.context import DbtContext @@ -83,7 +84,7 @@ def test_manifest_helper(caplog): assert waiter_as_customer_by_day_config.materialized == "incremental" assert waiter_as_customer_by_day_config.incremental_strategy == "delete+insert" assert waiter_as_customer_by_day_config.cluster_by == ["ds"] - assert waiter_as_customer_by_day_config.time_column == "ds" + assert waiter_as_customer_by_day_config.time_column == TimeColumn.create("ds", "duckdb") if DBT_VERSION >= (1, 5, 0): waiter_revenue_by_day_config = models["waiter_revenue_by_day_v2"] @@ -105,7 +106,7 @@ def test_manifest_helper(caplog): assert waiter_revenue_by_day_config.materialized == "incremental" assert waiter_revenue_by_day_config.incremental_strategy == "delete+insert" assert waiter_revenue_by_day_config.cluster_by == ["ds"] - assert waiter_revenue_by_day_config.time_column == "ds" + assert waiter_revenue_by_day_config.time_column == TimeColumn.create("ds", "duckdb") assert waiter_revenue_by_day_config.dialect_ == "bigquery" assert helper.models("customers")["customers"].dependencies == Dependencies( diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index 9f4f75bdbd..5037a69d65 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -7,6 +7,7 @@ from sqlglot import exp from sqlmesh import Context from sqlmesh.core.model import TimeColumn, IncrementalByTimeRangeKind +from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.model import ModelConfig @@ -301,3 +302,148 @@ def test_load_microbatch_required_only( ) assert model.kind.batch_size == 1 assert model.depends_on_self is False + + +@pytest.mark.slow +def test_load_incremental_time_range_strategy_required_only( + tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project +) -> None: + project_dir, model_dir = create_empty_project() + # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it + incremental_time_range_contents = """ + {{ + config( + materialized='incremental', + incremental_strategy='incremental_by_time_range', + time_column='ds', + ) + }} + + SELECT 1 as cola, '2021-01-01' as ds + """ + incremental_time_range_model_file = model_dir / "incremental_time_range.sql" + with open(incremental_time_range_model_file, "w", encoding="utf-8") as f: + f.write(incremental_time_range_contents) + + snapshot_fqn = '"local"."main"."incremental_time_range"' + context = Context(paths=project_dir) + model = context.snapshots[snapshot_fqn].model + # Validate model-level attributes + assert model.start == "2025-01-01" + assert model.interval_unit.is_day + # Validate model kind attributes + assert isinstance(model.kind, IncrementalByTimeRangeKind) + assert model.kind.lookback == 1 + assert model.kind.time_column == TimeColumn( + column=exp.to_column("ds", quoted=True), format="%Y-%m-%d" + ) + assert model.kind.batch_size is None + assert model.depends_on_self is False + assert model.kind.auto_restatement_intervals is None + assert model.kind.partition_by_time_column is True + + +@pytest.mark.slow +def test_load_incremental_time_range_strategy_all_defined( + tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project +) -> None: + project_dir, model_dir = create_empty_project() + # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it + incremental_time_range_contents = """ + {{ + config( + materialized='incremental', + incremental_strategy='incremental_by_time_range', + time_column={ + 'column': 'ds', + 'format': '%Y%m%d' + }, + auto_restatement_intervals=3, + partition_by_time_column=false, + lookback=5, + batch_size=3, + batch_concurrency=2, + forward_only=true, + disable_restatement=true, + on_destructive_change='allow', + on_additive_change='error', + auto_restatement_cron='@hourly', + on_schema_change='ignore' + ) + }} + + SELECT 1 as cola, '2021-01-01' as ds + """ + incremental_time_range_model_file = model_dir / "incremental_time_range.sql" + with open(incremental_time_range_model_file, "w", encoding="utf-8") as f: + f.write(incremental_time_range_contents) + + snapshot_fqn = '"local"."main"."incremental_time_range"' + context = Context(paths=project_dir) + model = context.snapshots[snapshot_fqn].model + # Validate model-level attributes + assert model.start == "2025-01-01" + assert model.interval_unit.is_day + # Validate model kind attributes + assert isinstance(model.kind, IncrementalByTimeRangeKind) + # `on_schema_change` is ignored since the user explicitly overrode the values + assert model.kind.on_destructive_change == OnDestructiveChange.ALLOW + assert model.kind.on_additive_change == OnAdditiveChange.ERROR + assert model.kind.forward_only is True + assert model.kind.disable_restatement is True + assert model.kind.auto_restatement_cron == "@hourly" + assert model.kind.auto_restatement_intervals == 3 + assert model.kind.partition_by_time_column is False + assert model.kind.lookback == 5 + assert model.kind.time_column == TimeColumn( + column=exp.to_column("ds", quoted=True), format="%Y%m%d" + ) + assert model.kind.batch_size == 3 + assert model.kind.batch_concurrency == 2 + assert model.depends_on_self is False + + +@pytest.mark.slow +def test_load_deprecated_incremental_time_column( + tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project +) -> None: + project_dir, model_dir = create_empty_project() + # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it + incremental_time_range_contents = """ + {{ + config( + materialized='incremental', + incremental_strategy='delete+insert', + time_column='ds' + ) + }} + + SELECT 1 as cola, '2021-01-01' as ds + """ + incremental_time_range_model_file = model_dir / "incremental_time_range.sql" + with open(incremental_time_range_model_file, "w", encoding="utf-8") as f: + f.write(incremental_time_range_contents) + + snapshot_fqn = '"local"."main"."incremental_time_range"' + context = Context(paths=project_dir) + model = context.snapshots[snapshot_fqn].model + # Validate model-level attributes + assert model.start == "2025-01-01" + assert model.interval_unit.is_day + # Validate model-level attributes + assert model.start == "2025-01-01" + assert model.interval_unit.is_day + # Validate model kind attributes + assert isinstance(model.kind, IncrementalByTimeRangeKind) + assert model.kind.lookback == 1 + assert model.kind.time_column == TimeColumn( + column=exp.to_column("ds", quoted=True), format="%Y-%m-%d" + ) + assert model.kind.batch_size is None + assert model.depends_on_self is False + assert model.kind.auto_restatement_intervals is None + assert model.kind.partition_by_time_column is True + assert ( + "Using `time_column` on a model with incremental_strategy 'delete+insert' has been deprecated. Please use `incremental_by_time_range` instead in model 'main.incremental_time_range'." + in caplog.text + ) From c40d7911451b14789f346865a64e94591da05a4b Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 5 Sep 2025 16:01:09 -0700 Subject: [PATCH 0821/1056] Chore: Split migration script implementations into DDL and DML (#5307) --- sqlmesh/core/context.py | 1 + sqlmesh/core/state_sync/db/environment.py | 10 +++++---- sqlmesh/core/state_sync/db/migrator.py | 7 ++++++- sqlmesh/migrations/v0001_init.py | 6 +++++- sqlmesh/migrations/v0002_remove_identify.py | 6 +++++- sqlmesh/migrations/v0003_move_batch_size.py | 6 +++++- .../v0004_environmnent_add_finalized_at.py | 6 +++++- sqlmesh/migrations/v0005_create_seed_table.py | 6 +++++- sqlmesh/migrations/v0006_change_seed_hash.py | 6 +++++- .../v0007_env_table_info_to_kind.py | 6 +++++- .../v0008_create_intervals_table.py | 6 +++++- .../migrations/v0009_remove_pre_post_hooks.py | 6 +++++- .../migrations/v0010_seed_hash_batch_size.py | 6 +++++- .../migrations/v0011_add_model_kind_name.py | 16 +++++++++++--- .../v0012_update_jinja_expressions.py | 6 +++++- .../v0013_serde_using_model_dialects.py | 6 +++++- sqlmesh/migrations/v0014_fix_dev_intervals.py | 6 +++++- ...5_environment_add_promoted_snapshot_ids.py | 6 +++++- sqlmesh/migrations/v0016_fix_windows_path.py | 6 +++++- .../migrations/v0017_fix_windows_seed_path.py | 6 +++++- .../v0018_rename_snapshot_model_to_node.py | 6 +++++- .../migrations/v0019_add_env_suffix_target.py | 9 +++++++- ...ve_redundant_attributes_from_dbt_models.py | 6 +++++- .../migrations/v0021_fix_table_properties.py | 6 +++++- .../migrations/v0022_move_project_to_model.py | 6 +++++- ..._added_models_with_forward_only_parents.py | 6 +++++- ...replace_model_kind_name_enum_with_value.py | 6 +++++- ...x_intervals_and_missing_change_category.py | 6 +++++- .../v0026_remove_dialect_from_seed.py | 6 +++++- .../v0027_minute_interval_to_five.py | 6 +++++- .../migrations/v0028_add_plan_dags_table.py | 6 +++++- ...029_generate_schema_types_using_dialect.py | 6 +++++- .../v0030_update_unrestorable_snapshots.py | 6 +++++- .../v0031_remove_dbt_target_fields.py | 6 +++++- .../migrations/v0032_add_sqlmesh_version.py | 6 +++++- .../v0033_mysql_fix_blob_text_type.py | 6 +++++- .../migrations/v0034_add_default_catalog.py | 6 +++++- .../v0035_add_catalog_name_override.py | 6 +++++- .../v0036_delete_plan_dags_bug_fix.py | 6 +++++- .../v0037_remove_dbt_is_incremental_macro.py | 6 +++++- .../v0038_add_expiration_ts_to_snapshot.py | 21 ++++++++++++------- ...39_include_environment_in_plan_dag_spec.py | 6 +++++- .../v0040_add_previous_finalized_snapshots.py | 6 +++++- .../v0041_remove_hash_raw_query_attribute.py | 6 +++++- .../v0042_trim_indirect_versions.py | 6 +++++- ...remove_obsolete_attributes_in_plan_dags.py | 6 +++++- ...4_quote_identifiers_in_model_attributes.py | 6 +++++- .../migrations/v0045_move_gateway_variable.py | 6 +++++- .../migrations/v0046_add_batch_concurrency.py | 6 +++++- .../v0047_change_scd_string_to_column.py | 6 +++++- .../v0048_drop_indirect_versions.py | 6 +++++- ..._identifier_with_version_in_seeds_table.py | 6 +++++- sqlmesh/migrations/v0050_drop_seeds_table.py | 6 +++++- .../v0051_rename_column_descriptions.py | 6 +++++- ...rmalize_name_in_environment_naming_info.py | 9 +++++++- ...0053_custom_model_kind_extra_attributes.py | 6 +++++- .../migrations/v0054_fix_trailing_comments.py | 6 +++++- ...used_ts_ttl_ms_unrestorable_to_snapshot.py | 20 ++++++++++++------ .../migrations/v0056_restore_table_indexes.py | 6 +++++- sqlmesh/migrations/v0057_add_table_format.py | 6 +++++- sqlmesh/migrations/v0058_add_requirements.py | 6 +++++- .../migrations/v0059_add_physical_version.py | 6 +++++- .../migrations/v0060_move_audits_to_model.py | 6 +++++- .../v0061_mysql_fix_blob_text_type.py | 6 +++++- sqlmesh/migrations/v0062_add_model_gateway.py | 6 +++++- sqlmesh/migrations/v0063_change_signals.py | 6 +++++- .../v0064_join_when_matched_strings.py | 6 +++++- .../migrations/v0065_add_model_optimize.py | 6 +++++- .../migrations/v0066_add_auto_restatements.py | 11 +++++++++- .../v0067_add_tsql_date_full_precision.py | 6 +++++- ...clude_unrendered_query_in_metadata_hash.py | 6 +++++- .../v0069_update_dev_table_suffix.py | 6 +++++- .../v0070_include_grains_in_metadata_hash.py | 6 +++++- .../v0071_add_dev_version_to_intervals.py | 14 ++++++++++--- .../v0072_add_environment_statements.py | 6 +++++- ...073_remove_symbolic_disable_restatement.py | 6 +++++- ...4_add_partition_by_time_column_property.py | 6 +++++- .../migrations/v0075_remove_validate_query.py | 6 +++++- sqlmesh/migrations/v0076_add_cron_tz.py | 6 +++++- .../v0077_fix_column_type_hash_calculation.py | 6 +++++- ...v0078_warn_if_non_migratable_python_env.py | 6 +++++- .../v0079_add_gateway_managed_property.py | 9 +++++++- ...080_add_batch_size_to_scd_type_2_models.py | 6 +++++- .../migrations/v0081_update_partitioned_by.py | 6 +++++- ...rn_if_incorrectly_duplicated_statements.py | 6 +++++- ...se_sql_for_scd_time_data_type_data_hash.py | 6 +++++- ...ize_quote_when_matched_and_merge_filter.py | 6 +++++- .../migrations/v0085_deterministic_repr.py | 6 +++++- .../v0086_check_deterministic_bug.py | 6 +++++- .../v0087_normalize_blueprint_variables.py | 6 +++++- ...88_warn_about_variable_python_env_diffs.py | 6 +++++- .../v0089_add_virtual_environment_mode.py | 6 +++++- .../v0090_add_forward_only_column.py | 14 ++++++++++--- .../migrations/v0091_on_additive_change.py | 6 +++++- .../v0092_warn_about_dbt_data_type_diff.py | 6 +++++- .../v0093_use_raw_sql_in_fingerprint.py | 6 +++++- ...add_dev_version_and_fingerprint_columns.py | 17 ++++++++++++--- .../v0095_warn_about_dbt_raw_sql_diff.py | 6 +++++- tests/integrations/jupyter/test_magics.py | 12 +++++------ 99 files changed, 555 insertions(+), 125 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 4c1ffb1e92..9660243753 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -587,6 +587,7 @@ def state_sync(self) -> StateSync: self._state_sync = self._new_state_sync() if self._state_sync.get_versions(validate=False).schema_version == 0: + self.console.log_status_update("Initializing new project state...") self._state_sync.migrate(default_catalog=self.default_catalog) self._state_sync.get_versions() self._state_sync = CachingStateSync(self._state_sync) # type: ignore diff --git a/sqlmesh/core/state_sync/db/environment.py b/sqlmesh/core/state_sync/db/environment.py index 4a28d7d70a..e3f1d1ec9e 100644 --- a/sqlmesh/core/state_sync/db/environment.py +++ b/sqlmesh/core/state_sync/db/environment.py @@ -285,11 +285,13 @@ def get_environment_statements(self, environment: str) -> t.List[EnvironmentStat return [] def _environment_from_row(self, row: t.Tuple[str, ...]) -> Environment: - return Environment(**{field: row[i] for i, field in enumerate(Environment.all_fields())}) + return Environment( + **{field: row[i] for i, field in enumerate(sorted(Environment.all_fields()))} + ) def _environment_summmary_from_row(self, row: t.Tuple[str, ...]) -> EnvironmentSummary: return EnvironmentSummary( - **{field: row[i] for i, field in enumerate(EnvironmentSummary.all_fields())} + **{field: row[i] for i, field in enumerate(sorted(EnvironmentSummary.all_fields()))} ) def _environments_query( @@ -298,7 +300,7 @@ def _environments_query( lock_for_update: bool = False, required_fields: t.Optional[t.List[str]] = None, ) -> exp.Select: - query_fields = required_fields if required_fields else Environment.all_fields() + query_fields = required_fields if required_fields else sorted(Environment.all_fields()) query = ( exp.select(*(exp.to_identifier(field) for field in query_fields)) .from_(self.environments_table) @@ -328,7 +330,7 @@ def _fetch_environment_summaries( self.engine_adapter, self._environments_query( where=where, - required_fields=list(EnvironmentSummary.all_fields()), + required_fields=sorted(EnvironmentSummary.all_fields()), ), ) ] diff --git a/sqlmesh/core/state_sync/db/migrator.py b/sqlmesh/core/state_sync/db/migrator.py index 7edd7de3c4..616bd8659f 100644 --- a/sqlmesh/core/state_sync/db/migrator.py +++ b/sqlmesh/core/state_sync/db/migrator.py @@ -173,9 +173,14 @@ def _apply_migrations( snapshot_count_before = self.snapshot_state.count() if versions.schema_version else None + state_table_exist = any(self.engine_adapter.table_exists(t) for t in self._state_tables) + for migration in migrations: logger.info(f"Applying migration {migration}") - migration.migrate(state_sync, default_catalog=default_catalog) + migration.migrate_schemas(state_sync, default_catalog=default_catalog) + if state_table_exist: + # No need to run DML for the initial migration since all tables are empty + migration.migrate_rows(state_sync, default_catalog=default_catalog) snapshot_count_after = self.snapshot_state.count() diff --git a/sqlmesh/migrations/v0001_init.py b/sqlmesh/migrations/v0001_init.py index 778c36bc23..42d623d1d0 100644 --- a/sqlmesh/migrations/v0001_init.py +++ b/sqlmesh/migrations/v0001_init.py @@ -9,7 +9,7 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" @@ -58,3 +58,7 @@ def migrate(state_sync, **kwargs): # type: ignore "sqlglot_version": exp.DataType.build("text"), }, ) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0002_remove_identify.py b/sqlmesh/migrations/v0002_remove_identify.py index 0152e719f7..d8f9a1c0cd 100644 --- a/sqlmesh/migrations/v0002_remove_identify.py +++ b/sqlmesh/migrations/v0002_remove_identify.py @@ -1,5 +1,9 @@ """Remove identify=True kwarg for rendering sql""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0003_move_batch_size.py b/sqlmesh/migrations/v0003_move_batch_size.py index 8148325750..e8efff6162 100644 --- a/sqlmesh/migrations/v0003_move_batch_size.py +++ b/sqlmesh/migrations/v0003_move_batch_size.py @@ -5,7 +5,11 @@ from sqlglot import exp -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore snapshots_table = "_snapshots" if state_sync.schema: snapshots_table = f"{state_sync.schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0004_environmnent_add_finalized_at.py b/sqlmesh/migrations/v0004_environmnent_add_finalized_at.py index 11e826808f..bddbef5971 100644 --- a/sqlmesh/migrations/v0004_environmnent_add_finalized_at.py +++ b/sqlmesh/migrations/v0004_environmnent_add_finalized_at.py @@ -3,7 +3,7 @@ from sqlglot import exp -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter environments_table = "_environments" if state_sync.schema: @@ -21,3 +21,7 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0005_create_seed_table.py b/sqlmesh/migrations/v0005_create_seed_table.py index 1e1e7dc34e..803a47f724 100644 --- a/sqlmesh/migrations/v0005_create_seed_table.py +++ b/sqlmesh/migrations/v0005_create_seed_table.py @@ -5,7 +5,7 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter seeds_table = "_seeds" if state_sync.schema: @@ -22,3 +22,7 @@ def migrate(state_sync, **kwargs): # type: ignore }, primary_key=("name", "identifier"), ) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0006_change_seed_hash.py b/sqlmesh/migrations/v0006_change_seed_hash.py index d6d4e1bf9c..c9f771a912 100644 --- a/sqlmesh/migrations/v0006_change_seed_hash.py +++ b/sqlmesh/migrations/v0006_change_seed_hash.py @@ -1,5 +1,9 @@ """Seed hashes moved from to_string to to_json for performance.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0007_env_table_info_to_kind.py b/sqlmesh/migrations/v0007_env_table_info_to_kind.py index f09f0d2b72..52d483b3cb 100644 --- a/sqlmesh/migrations/v0007_env_table_info_to_kind.py +++ b/sqlmesh/migrations/v0007_env_table_info_to_kind.py @@ -12,7 +12,11 @@ def _hash(data): # type: ignore return str(zlib.crc32(";".join("" if d is None else d for d in data).encode("utf-8"))) -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0008_create_intervals_table.py b/sqlmesh/migrations/v0008_create_intervals_table.py index 0746febcaa..7ba8888608 100644 --- a/sqlmesh/migrations/v0008_create_intervals_table.py +++ b/sqlmesh/migrations/v0008_create_intervals_table.py @@ -5,7 +5,7 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter intervals_table = "_intervals" if state_sync.schema: @@ -36,3 +36,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.create_index( intervals_table, "name_identifier_idx", ("name", "identifier", "created_ts") ) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0009_remove_pre_post_hooks.py b/sqlmesh/migrations/v0009_remove_pre_post_hooks.py index 3671f547d3..534f366d69 100644 --- a/sqlmesh/migrations/v0009_remove_pre_post_hooks.py +++ b/sqlmesh/migrations/v0009_remove_pre_post_hooks.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0010_seed_hash_batch_size.py b/sqlmesh/migrations/v0010_seed_hash_batch_size.py index 2f73e73161..20186e0068 100644 --- a/sqlmesh/migrations/v0010_seed_hash_batch_size.py +++ b/sqlmesh/migrations/v0010_seed_hash_batch_size.py @@ -1,5 +1,9 @@ """Seed metadata hashes now correctly include the batch_size.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0011_add_model_kind_name.py b/sqlmesh/migrations/v0011_add_model_kind_name.py index 77aa68506a..3d76d61597 100644 --- a/sqlmesh/migrations/v0011_add_model_kind_name.py +++ b/sqlmesh/migrations/v0011_add_model_kind_name.py @@ -7,9 +7,7 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore - import pandas as pd - +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" @@ -30,6 +28,18 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + +def migrate_rows(state_sync, **kwargs): # type: ignore + import pandas as pd + + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + index_type = index_text_type(engine_adapter.dialect) + new_snapshots = [] for name, identifier, version, snapshot in engine_adapter.fetchall( diff --git a/sqlmesh/migrations/v0012_update_jinja_expressions.py b/sqlmesh/migrations/v0012_update_jinja_expressions.py index 28bc4acdca..99897fa59d 100644 --- a/sqlmesh/migrations/v0012_update_jinja_expressions.py +++ b/sqlmesh/migrations/v0012_update_jinja_expressions.py @@ -9,7 +9,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0013_serde_using_model_dialects.py b/sqlmesh/migrations/v0013_serde_using_model_dialects.py index 7e5e2cc217..5d865930e7 100644 --- a/sqlmesh/migrations/v0013_serde_using_model_dialects.py +++ b/sqlmesh/migrations/v0013_serde_using_model_dialects.py @@ -9,7 +9,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0014_fix_dev_intervals.py b/sqlmesh/migrations/v0014_fix_dev_intervals.py index f0e922783c..d5f4d86f9d 100644 --- a/sqlmesh/migrations/v0014_fix_dev_intervals.py +++ b/sqlmesh/migrations/v0014_fix_dev_intervals.py @@ -1,7 +1,11 @@ """Fix snapshot intervals that have been erroneously marked as dev.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore schema = state_sync.schema intervals_table = "_intervals" if schema: diff --git a/sqlmesh/migrations/v0015_environment_add_promoted_snapshot_ids.py b/sqlmesh/migrations/v0015_environment_add_promoted_snapshot_ids.py index c544e275c5..b1e42e1eb7 100644 --- a/sqlmesh/migrations/v0015_environment_add_promoted_snapshot_ids.py +++ b/sqlmesh/migrations/v0015_environment_add_promoted_snapshot_ids.py @@ -4,7 +4,7 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter environments_table = "_environments" if state_sync.schema: @@ -24,3 +24,7 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0016_fix_windows_path.py b/sqlmesh/migrations/v0016_fix_windows_path.py index e37c45afca..3570cc368e 100644 --- a/sqlmesh/migrations/v0016_fix_windows_path.py +++ b/sqlmesh/migrations/v0016_fix_windows_path.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0017_fix_windows_seed_path.py b/sqlmesh/migrations/v0017_fix_windows_seed_path.py index 5d91443009..57bdd3609d 100644 --- a/sqlmesh/migrations/v0017_fix_windows_seed_path.py +++ b/sqlmesh/migrations/v0017_fix_windows_seed_path.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py b/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py index 5229c54f81..e17eeded61 100644 --- a/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py +++ b/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0019_add_env_suffix_target.py b/sqlmesh/migrations/v0019_add_env_suffix_target.py index cc1503f02d..88227c8fdd 100644 --- a/sqlmesh/migrations/v0019_add_env_suffix_target.py +++ b/sqlmesh/migrations/v0019_add_env_suffix_target.py @@ -3,7 +3,7 @@ from sqlglot import exp -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter environments_table = "_environments" if state_sync.schema: @@ -21,6 +21,13 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + +def migrate_rows(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + environments_table = "_environments" + if state_sync.schema: + environments_table = f"{state_sync.schema}.{environments_table}" + state_sync.engine_adapter.update_table( environments_table, {"suffix_target": "schema"}, diff --git a/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py b/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py index d4c449ff34..788974ccee 100644 --- a/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py +++ b/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0021_fix_table_properties.py b/sqlmesh/migrations/v0021_fix_table_properties.py index 41429b5650..c878cedb8b 100644 --- a/sqlmesh/migrations/v0021_fix_table_properties.py +++ b/sqlmesh/migrations/v0021_fix_table_properties.py @@ -8,7 +8,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0022_move_project_to_model.py b/sqlmesh/migrations/v0022_move_project_to_model.py index a5a529ef31..5a4eaa77f0 100644 --- a/sqlmesh/migrations/v0022_move_project_to_model.py +++ b/sqlmesh/migrations/v0022_move_project_to_model.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0023_fix_added_models_with_forward_only_parents.py b/sqlmesh/migrations/v0023_fix_added_models_with_forward_only_parents.py index 6ae64955b8..2fa490b0ce 100644 --- a/sqlmesh/migrations/v0023_fix_added_models_with_forward_only_parents.py +++ b/sqlmesh/migrations/v0023_fix_added_models_with_forward_only_parents.py @@ -8,7 +8,11 @@ from sqlmesh.utils.dag import DAG -def migrate(state_sync: t.Any, **kwargs) -> None: # type: ignore +def migrate_schemas(state_sync: t.Any, **kwargs) -> None: # type: ignore + pass + + +def migrate_rows(state_sync: t.Any, **kwargs) -> None: # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py b/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py index abdbb716ea..81a9f79dde 100644 --- a/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py +++ b/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py b/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py index b99e208806..08c03c6a87 100644 --- a/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py +++ b/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py @@ -10,7 +10,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0026_remove_dialect_from_seed.py b/sqlmesh/migrations/v0026_remove_dialect_from_seed.py index 73ec09aa76..10d77b430b 100644 --- a/sqlmesh/migrations/v0026_remove_dialect_from_seed.py +++ b/sqlmesh/migrations/v0026_remove_dialect_from_seed.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0027_minute_interval_to_five.py b/sqlmesh/migrations/v0027_minute_interval_to_five.py index ce8b272734..8878536b6f 100644 --- a/sqlmesh/migrations/v0027_minute_interval_to_five.py +++ b/sqlmesh/migrations/v0027_minute_interval_to_five.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0028_add_plan_dags_table.py b/sqlmesh/migrations/v0028_add_plan_dags_table.py index d8e67f6045..b03fa45bba 100644 --- a/sqlmesh/migrations/v0028_add_plan_dags_table.py +++ b/sqlmesh/migrations/v0028_add_plan_dags_table.py @@ -5,7 +5,7 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema plan_dags_table = "_plan_dags" @@ -27,3 +27,7 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.create_index(plan_dags_table, "dag_id_idx", ("dag_id",)) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py b/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py index 1f2dda5f5f..a8b2800fe0 100644 --- a/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py +++ b/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py b/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py index 3cd27d2ee2..5f2d7f1dbf 100644 --- a/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py +++ b/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py @@ -9,7 +9,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync: t.Any, **kwargs: t.Any) -> None: # type: ignore +def migrate_schemas(state_sync: t.Any, **kwargs: t.Any) -> None: # type: ignore + pass + + +def migrate_rows(state_sync: t.Any, **kwargs: t.Any) -> None: # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0031_remove_dbt_target_fields.py b/sqlmesh/migrations/v0031_remove_dbt_target_fields.py index d13ec92e0b..e99aaa7fa4 100644 --- a/sqlmesh/migrations/v0031_remove_dbt_target_fields.py +++ b/sqlmesh/migrations/v0031_remove_dbt_target_fields.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0032_add_sqlmesh_version.py b/sqlmesh/migrations/v0032_add_sqlmesh_version.py index 12eb50512c..032709f889 100644 --- a/sqlmesh/migrations/v0032_add_sqlmesh_version.py +++ b/sqlmesh/migrations/v0032_add_sqlmesh_version.py @@ -5,7 +5,7 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter versions_table = "_versions" if state_sync.schema: @@ -23,3 +23,7 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0033_mysql_fix_blob_text_type.py b/sqlmesh/migrations/v0033_mysql_fix_blob_text_type.py index f0fb6607d3..5b3d0f2347 100644 --- a/sqlmesh/migrations/v0033_mysql_fix_blob_text_type.py +++ b/sqlmesh/migrations/v0033_mysql_fix_blob_text_type.py @@ -5,7 +5,7 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter if engine_adapter.dialect != "mysql": return @@ -43,3 +43,7 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0034_add_default_catalog.py b/sqlmesh/migrations/v0034_add_default_catalog.py index d6469fa4b1..15a040364f 100644 --- a/sqlmesh/migrations/v0034_add_default_catalog.py +++ b/sqlmesh/migrations/v0034_add_default_catalog.py @@ -63,7 +63,11 @@ def update_dbt_relations( relation["database"] = default_catalog -def migrate(state_sync, default_catalog: t.Optional[str], **kwargs): # type: ignore +def migrate_schemas(state_sync, default_catalog: t.Optional[str], **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, default_catalog: t.Optional[str], **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0035_add_catalog_name_override.py b/sqlmesh/migrations/v0035_add_catalog_name_override.py index 5f8c5aa14c..3e2a42bd60 100644 --- a/sqlmesh/migrations/v0035_add_catalog_name_override.py +++ b/sqlmesh/migrations/v0035_add_catalog_name_override.py @@ -3,7 +3,7 @@ from sqlglot import exp -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter environments_table = "_environments" if state_sync.schema: @@ -20,3 +20,7 @@ def migrate(state_sync, **kwargs): # type: ignore ], ) engine_adapter.execute(alter_table_exp) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0036_delete_plan_dags_bug_fix.py b/sqlmesh/migrations/v0036_delete_plan_dags_bug_fix.py index 7f9f49d61d..9cd10ccbe0 100644 --- a/sqlmesh/migrations/v0036_delete_plan_dags_bug_fix.py +++ b/sqlmesh/migrations/v0036_delete_plan_dags_bug_fix.py @@ -1,7 +1,11 @@ """Add missing delete from migration #34.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema plan_dags_table = "_plan_dags" diff --git a/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py b/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py index 6ca7bef406..083f8301b4 100644 --- a/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py +++ b/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py @@ -8,7 +8,11 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py b/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py index 54bb30a54b..5ddb3a4ee7 100644 --- a/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py +++ b/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py @@ -9,18 +9,13 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore - import pandas as pd - +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - alter_table_exp = exp.Alter( this=exp.to_table(snapshots_table), kind="TABLE", @@ -33,8 +28,20 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) - new_snapshots = [] +def migrate_rows(state_sync, **kwargs): # type: ignore + import pandas as pd + + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + index_type = index_text_type(engine_adapter.dialect) + blob_type = blob_text_type(engine_adapter.dialect) + + new_snapshots = [] for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), quote_identifiers=True, diff --git a/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py b/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py index 39fc6b6a0f..fb1c0b1ec7 100644 --- a/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py +++ b/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py @@ -8,7 +8,11 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0040_add_previous_finalized_snapshots.py b/sqlmesh/migrations/v0040_add_previous_finalized_snapshots.py index 53e33fcaac..f15bd69eed 100644 --- a/sqlmesh/migrations/v0040_add_previous_finalized_snapshots.py +++ b/sqlmesh/migrations/v0040_add_previous_finalized_snapshots.py @@ -5,7 +5,7 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter environments_table = "_environments" if state_sync.schema: @@ -24,3 +24,7 @@ def migrate(state_sync, **kwargs): # type: ignore ], ) engine_adapter.execute(alter_table_exp) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py b/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py index fee9ac2955..a99e96b686 100644 --- a/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py +++ b/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py @@ -8,7 +8,11 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0042_trim_indirect_versions.py b/sqlmesh/migrations/v0042_trim_indirect_versions.py index 6759e8140d..5a8f6285b4 100644 --- a/sqlmesh/migrations/v0042_trim_indirect_versions.py +++ b/sqlmesh/migrations/v0042_trim_indirect_versions.py @@ -8,7 +8,11 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py b/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py index 8b27e90963..767f4b236b 100644 --- a/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py +++ b/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py @@ -8,7 +8,11 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0044_quote_identifiers_in_model_attributes.py b/sqlmesh/migrations/v0044_quote_identifiers_in_model_attributes.py index 82eae3db3b..de5344d4ce 100644 --- a/sqlmesh/migrations/v0044_quote_identifiers_in_model_attributes.py +++ b/sqlmesh/migrations/v0044_quote_identifiers_in_model_attributes.py @@ -1,5 +1,9 @@ """Quoted identifiers in model SQL attributes.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0045_move_gateway_variable.py b/sqlmesh/migrations/v0045_move_gateway_variable.py index 12115e03e0..754f958fac 100644 --- a/sqlmesh/migrations/v0045_move_gateway_variable.py +++ b/sqlmesh/migrations/v0045_move_gateway_variable.py @@ -9,7 +9,11 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0046_add_batch_concurrency.py b/sqlmesh/migrations/v0046_add_batch_concurrency.py index a76dc358b5..f23d27e80a 100644 --- a/sqlmesh/migrations/v0046_add_batch_concurrency.py +++ b/sqlmesh/migrations/v0046_add_batch_concurrency.py @@ -4,5 +4,9 @@ """ -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0047_change_scd_string_to_column.py b/sqlmesh/migrations/v0047_change_scd_string_to_column.py index 72ebbf2654..9233a54ca9 100644 --- a/sqlmesh/migrations/v0047_change_scd_string_to_column.py +++ b/sqlmesh/migrations/v0047_change_scd_string_to_column.py @@ -1,5 +1,9 @@ """Changes the SCD Type 2 columns from strings to columns.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0048_drop_indirect_versions.py b/sqlmesh/migrations/v0048_drop_indirect_versions.py index 991fb43827..31874268dd 100644 --- a/sqlmesh/migrations/v0048_drop_indirect_versions.py +++ b/sqlmesh/migrations/v0048_drop_indirect_versions.py @@ -8,7 +8,11 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0049_replace_identifier_with_version_in_seeds_table.py b/sqlmesh/migrations/v0049_replace_identifier_with_version_in_seeds_table.py index 186b5f7856..b01bee41e1 100644 --- a/sqlmesh/migrations/v0049_replace_identifier_with_version_in_seeds_table.py +++ b/sqlmesh/migrations/v0049_replace_identifier_with_version_in_seeds_table.py @@ -5,7 +5,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0050_drop_seeds_table.py b/sqlmesh/migrations/v0050_drop_seeds_table.py index 706fae63ed..0236284061 100644 --- a/sqlmesh/migrations/v0050_drop_seeds_table.py +++ b/sqlmesh/migrations/v0050_drop_seeds_table.py @@ -1,7 +1,7 @@ """Drop the seeds table.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter seeds_table = "_seeds" @@ -9,3 +9,7 @@ def migrate(state_sync, **kwargs): # type: ignore seeds_table = f"{state_sync.schema}.{seeds_table}" engine_adapter.drop_table(seeds_table) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0051_rename_column_descriptions.py b/sqlmesh/migrations/v0051_rename_column_descriptions.py index a6b4b72577..f76a4a05a6 100644 --- a/sqlmesh/migrations/v0051_rename_column_descriptions.py +++ b/sqlmesh/migrations/v0051_rename_column_descriptions.py @@ -8,7 +8,11 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0052_add_normalize_name_in_environment_naming_info.py b/sqlmesh/migrations/v0052_add_normalize_name_in_environment_naming_info.py index 8c2917de4c..27980033fa 100644 --- a/sqlmesh/migrations/v0052_add_normalize_name_in_environment_naming_info.py +++ b/sqlmesh/migrations/v0052_add_normalize_name_in_environment_naming_info.py @@ -3,7 +3,7 @@ from sqlglot import exp -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter environments_table = "_environments" if state_sync.schema: @@ -21,6 +21,13 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + +def migrate_rows(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + environments_table = "_environments" + if state_sync.schema: + environments_table = f"{state_sync.schema}.{environments_table}" + state_sync.engine_adapter.update_table( environments_table, {"normalize_name": False}, diff --git a/sqlmesh/migrations/v0053_custom_model_kind_extra_attributes.py b/sqlmesh/migrations/v0053_custom_model_kind_extra_attributes.py index bc242964a5..d1c83658e8 100644 --- a/sqlmesh/migrations/v0053_custom_model_kind_extra_attributes.py +++ b/sqlmesh/migrations/v0053_custom_model_kind_extra_attributes.py @@ -1,5 +1,9 @@ """Add batch_size, batch_concurrency, and batch_interval to the CUSTOM model kind.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0054_fix_trailing_comments.py b/sqlmesh/migrations/v0054_fix_trailing_comments.py index 0084626e3d..8e7de52067 100644 --- a/sqlmesh/migrations/v0054_fix_trailing_comments.py +++ b/sqlmesh/migrations/v0054_fix_trailing_comments.py @@ -1,5 +1,9 @@ """Fix support for trailing comments in SQL model definitions.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py b/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py index b323afa04f..96f39772cd 100644 --- a/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py +++ b/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py @@ -9,18 +9,13 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore - import pandas as pd - +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - add_column_exps = [ exp.Alter( this=exp.to_table(snapshots_table), @@ -78,6 +73,19 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(drop_column_exp) + +def migrate_rows(state_sync, **kwargs): # type: ignore + import pandas as pd + + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + index_type = index_text_type(engine_adapter.dialect) + blob_type = blob_text_type(engine_adapter.dialect) + new_snapshots = [] for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( diff --git a/sqlmesh/migrations/v0056_restore_table_indexes.py b/sqlmesh/migrations/v0056_restore_table_indexes.py index 4ffec4e9cb..b460c1ebf7 100644 --- a/sqlmesh/migrations/v0056_restore_table_indexes.py +++ b/sqlmesh/migrations/v0056_restore_table_indexes.py @@ -6,7 +6,7 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore schema = state_sync.schema engine_adapter = state_sync.engine_adapter if not engine_adapter.SUPPORTS_INDEXES: @@ -116,3 +116,7 @@ def migrate(state_sync, **kwargs): # type: ignore engine_adapter.rename_table(new_snapshots_table, snapshots_table) engine_adapter.rename_table(new_environments_table, environments_table) engine_adapter.rename_table(new_intervals_table, intervals_table) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0057_add_table_format.py b/sqlmesh/migrations/v0057_add_table_format.py index e34b6a4a50..b59911ef3a 100644 --- a/sqlmesh/migrations/v0057_add_table_format.py +++ b/sqlmesh/migrations/v0057_add_table_format.py @@ -1,5 +1,9 @@ """Add table_format to the model top-level properties""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0058_add_requirements.py b/sqlmesh/migrations/v0058_add_requirements.py index d7bb03b2e6..73de67d4e5 100644 --- a/sqlmesh/migrations/v0058_add_requirements.py +++ b/sqlmesh/migrations/v0058_add_requirements.py @@ -5,7 +5,7 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter environments_table = "_environments" if state_sync.schema: @@ -24,3 +24,7 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0059_add_physical_version.py b/sqlmesh/migrations/v0059_add_physical_version.py index ae24ee3906..a8dfa24b7a 100644 --- a/sqlmesh/migrations/v0059_add_physical_version.py +++ b/sqlmesh/migrations/v0059_add_physical_version.py @@ -1,5 +1,9 @@ """Add the physical_version model attribute.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0060_move_audits_to_model.py b/sqlmesh/migrations/v0060_move_audits_to_model.py index ca61055579..b4d351cf5c 100644 --- a/sqlmesh/migrations/v0060_move_audits_to_model.py +++ b/sqlmesh/migrations/v0060_move_audits_to_model.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py b/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py index 6517ffe5a4..9e66db9f66 100644 --- a/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py +++ b/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py @@ -9,7 +9,7 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter if engine_adapter.dialect != "mysql": return @@ -47,3 +47,7 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0062_add_model_gateway.py b/sqlmesh/migrations/v0062_add_model_gateway.py index 41625358e0..524a94044a 100644 --- a/sqlmesh/migrations/v0062_add_model_gateway.py +++ b/sqlmesh/migrations/v0062_add_model_gateway.py @@ -1,5 +1,9 @@ """Add the gateway model attribute.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0063_change_signals.py b/sqlmesh/migrations/v0063_change_signals.py index cf01bd2420..8806c9ea60 100644 --- a/sqlmesh/migrations/v0063_change_signals.py +++ b/sqlmesh/migrations/v0063_change_signals.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0064_join_when_matched_strings.py b/sqlmesh/migrations/v0064_join_when_matched_strings.py index 455bf9e2c0..6da3164a38 100644 --- a/sqlmesh/migrations/v0064_join_when_matched_strings.py +++ b/sqlmesh/migrations/v0064_join_when_matched_strings.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0065_add_model_optimize.py b/sqlmesh/migrations/v0065_add_model_optimize.py index cf6eaa4034..09240aa61e 100644 --- a/sqlmesh/migrations/v0065_add_model_optimize.py +++ b/sqlmesh/migrations/v0065_add_model_optimize.py @@ -1,5 +1,9 @@ """Add the optimize_query model attribute.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0066_add_auto_restatements.py b/sqlmesh/migrations/v0066_add_auto_restatements.py index 769ba03e6b..96d2cd45e8 100644 --- a/sqlmesh/migrations/v0066_add_auto_restatements.py +++ b/sqlmesh/migrations/v0066_add_auto_restatements.py @@ -5,7 +5,7 @@ from sqlmesh.utils.migration import index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema auto_restatements_table = "_auto_restatements" @@ -39,6 +39,15 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + +def migrate_rows(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + intervals_table = "_intervals" + + if schema: + intervals_table = f"{schema}.{intervals_table}" + engine_adapter.update_table( intervals_table, {"is_pending_restatement": False}, diff --git a/sqlmesh/migrations/v0067_add_tsql_date_full_precision.py b/sqlmesh/migrations/v0067_add_tsql_date_full_precision.py index 1178ee012d..d4fd93eda4 100644 --- a/sqlmesh/migrations/v0067_add_tsql_date_full_precision.py +++ b/sqlmesh/migrations/v0067_add_tsql_date_full_precision.py @@ -1,5 +1,9 @@ """Add full precision for tsql to support nanoseconds.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py b/sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py index 22ef12dca7..6f7ddbdc1c 100644 --- a/sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py +++ b/sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py @@ -1,5 +1,9 @@ """Include the unrendered query in the metadata hash.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0069_update_dev_table_suffix.py b/sqlmesh/migrations/v0069_update_dev_table_suffix.py index 1d714a5ba2..57b41a816c 100644 --- a/sqlmesh/migrations/v0069_update_dev_table_suffix.py +++ b/sqlmesh/migrations/v0069_update_dev_table_suffix.py @@ -7,7 +7,11 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py b/sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py index dc75ac333d..4b339d8e97 100644 --- a/sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py +++ b/sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py @@ -1,5 +1,9 @@ """Include grains in the metadata hash.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py b/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py index 7e14b2d4e1..4e6cbab4f0 100644 --- a/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py +++ b/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py @@ -8,14 +8,12 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema intervals_table = "_intervals" - snapshots_table = "_snapshots" if schema: intervals_table = f"{schema}.{intervals_table}" - snapshots_table = f"{schema}.{snapshots_table}" index_type = index_text_type(engine_adapter.dialect) alter_table_exp = exp.Alter( @@ -30,6 +28,16 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + +def migrate_rows(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + intervals_table = "_intervals" + snapshots_table = "_snapshots" + if schema: + intervals_table = f"{schema}.{intervals_table}" + snapshots_table = f"{schema}.{snapshots_table}" + used_dev_versions: t.Set[t.Tuple[str, str]] = set() used_versions: t.Set[t.Tuple[str, str]] = set() used_snapshot_ids: t.Set[t.Tuple[str, str]] = set() diff --git a/sqlmesh/migrations/v0072_add_environment_statements.py b/sqlmesh/migrations/v0072_add_environment_statements.py index 47ba5b61a0..e73faf2b9a 100644 --- a/sqlmesh/migrations/v0072_add_environment_statements.py +++ b/sqlmesh/migrations/v0072_add_environment_statements.py @@ -5,7 +5,7 @@ from sqlmesh.utils.migration import blob_text_type, index_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema environment_statements_table = "_environment_statements" @@ -25,3 +25,7 @@ def migrate(state_sync, **kwargs): # type: ignore }, primary_key=("environment_name",), ) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py b/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py index a460399378..40e74d6426 100644 --- a/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py +++ b/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py @@ -6,7 +6,11 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0074_add_partition_by_time_column_property.py b/sqlmesh/migrations/v0074_add_partition_by_time_column_property.py index 30fbce46e0..04f1a27254 100644 --- a/sqlmesh/migrations/v0074_add_partition_by_time_column_property.py +++ b/sqlmesh/migrations/v0074_add_partition_by_time_column_property.py @@ -2,5 +2,9 @@ (default: True to keep the original behaviour)""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0075_remove_validate_query.py b/sqlmesh/migrations/v0075_remove_validate_query.py index 137430bec4..f6d4e255d9 100644 --- a/sqlmesh/migrations/v0075_remove_validate_query.py +++ b/sqlmesh/migrations/v0075_remove_validate_query.py @@ -8,7 +8,11 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0076_add_cron_tz.py b/sqlmesh/migrations/v0076_add_cron_tz.py index cfc393a4a6..300474aa18 100644 --- a/sqlmesh/migrations/v0076_add_cron_tz.py +++ b/sqlmesh/migrations/v0076_add_cron_tz.py @@ -1,5 +1,9 @@ """Add 'cron_tz' property to node definition.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py b/sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py index 67dc460497..2aec1140f1 100644 --- a/sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py +++ b/sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py @@ -1,5 +1,9 @@ """Use the model's dialect when calculating the hash for the column types.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py b/sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py index 6e754f5e03..c24b6a5168 100644 --- a/sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py +++ b/sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py @@ -24,7 +24,11 @@ from sqlmesh.core.console import get_console -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0079_add_gateway_managed_property.py b/sqlmesh/migrations/v0079_add_gateway_managed_property.py index 15031372ff..8d24601102 100644 --- a/sqlmesh/migrations/v0079_add_gateway_managed_property.py +++ b/sqlmesh/migrations/v0079_add_gateway_managed_property.py @@ -3,7 +3,7 @@ from sqlglot import exp -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter environments_table = "_environments" if state_sync.schema: @@ -21,6 +21,13 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + +def migrate_rows(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + environments_table = "_environments" + if state_sync.schema: + environments_table = f"{state_sync.schema}.{environments_table}" + state_sync.engine_adapter.update_table( environments_table, {"gateway_managed": False}, diff --git a/sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py b/sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py index 8b40fc33c8..582bdd3da9 100644 --- a/sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py +++ b/sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py @@ -1,5 +1,9 @@ """Add batch_size to SCD Type 2 models and add updated_at_name to by time which changes their data hash.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0081_update_partitioned_by.py b/sqlmesh/migrations/v0081_update_partitioned_by.py index e5c98bd8e3..611d8f6973 100644 --- a/sqlmesh/migrations/v0081_update_partitioned_by.py +++ b/sqlmesh/migrations/v0081_update_partitioned_by.py @@ -8,7 +8,11 @@ from sqlmesh.utils.migration import blob_text_type -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py b/sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py index 7fb9affb1d..6eadbfc2c3 100644 --- a/sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py +++ b/sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py @@ -34,7 +34,11 @@ from sqlmesh.core.console import get_console -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py b/sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py index a6ad2bb553..38c84afafd 100644 --- a/sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py +++ b/sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py @@ -1,5 +1,9 @@ """Use sql(...) instead of gen when computing the data hash of the time data type.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py b/sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py index 24a6db9384..5401c97d77 100644 --- a/sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py +++ b/sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py @@ -5,5 +5,9 @@ """ -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0085_deterministic_repr.py b/sqlmesh/migrations/v0085_deterministic_repr.py index b5f0203c6d..1a90277bbe 100644 --- a/sqlmesh/migrations/v0085_deterministic_repr.py +++ b/sqlmesh/migrations/v0085_deterministic_repr.py @@ -36,7 +36,11 @@ def _dict_sort(obj: t.Any) -> str: return repr(obj) -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0086_check_deterministic_bug.py b/sqlmesh/migrations/v0086_check_deterministic_bug.py index 17527e81ce..0679414881 100644 --- a/sqlmesh/migrations/v0086_check_deterministic_bug.py +++ b/sqlmesh/migrations/v0086_check_deterministic_bug.py @@ -10,7 +10,11 @@ KEYS_TO_MAKE_DETERMINISTIC = ["__sqlmesh__vars__", "__sqlmesh__blueprint__vars__"] -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0087_normalize_blueprint_variables.py b/sqlmesh/migrations/v0087_normalize_blueprint_variables.py index 12648b5a2e..2f23a0653e 100644 --- a/sqlmesh/migrations/v0087_normalize_blueprint_variables.py +++ b/sqlmesh/migrations/v0087_normalize_blueprint_variables.py @@ -35,7 +35,11 @@ class SqlValue: sql: str -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore import pandas as pd engine_adapter = state_sync.engine_adapter diff --git a/sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py b/sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py index eb33a8041f..405aad725f 100644 --- a/sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py +++ b/sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py @@ -35,7 +35,11 @@ METADATA_HASH_EXPRESSIONS = {"on_virtual_update", "audits", "signals", "audit_definitions"} -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0089_add_virtual_environment_mode.py b/sqlmesh/migrations/v0089_add_virtual_environment_mode.py index 024ff03a0e..63d491418f 100644 --- a/sqlmesh/migrations/v0089_add_virtual_environment_mode.py +++ b/sqlmesh/migrations/v0089_add_virtual_environment_mode.py @@ -1,5 +1,9 @@ """Add virtual_environment_mode to the model definition.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0090_add_forward_only_column.py b/sqlmesh/migrations/v0090_add_forward_only_column.py index cdc3fc857a..b68c0f65ea 100644 --- a/sqlmesh/migrations/v0090_add_forward_only_column.py +++ b/sqlmesh/migrations/v0090_add_forward_only_column.py @@ -7,9 +7,7 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate(state_sync, **kwargs): # type: ignore - import pandas as pd - +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" @@ -28,6 +26,16 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(alter_table_exp) + +def migrate_rows(state_sync, **kwargs): # type: ignore + import pandas as pd + + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + new_snapshots = [] for ( diff --git a/sqlmesh/migrations/v0091_on_additive_change.py b/sqlmesh/migrations/v0091_on_additive_change.py index 56059b982f..c0170bd438 100644 --- a/sqlmesh/migrations/v0091_on_additive_change.py +++ b/sqlmesh/migrations/v0091_on_additive_change.py @@ -1,5 +1,9 @@ """Add on_additive_change to incremental model metadata hash.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py b/sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py index 08ff1b1de2..1ff069bc82 100644 --- a/sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py +++ b/sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py @@ -17,7 +17,11 @@ SQLMESH_DBT_PACKAGE = "sqlmesh.dbt" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py b/sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py index 53d4cb1727..f629c1d27d 100644 --- a/sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py +++ b/sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py @@ -1,5 +1,9 @@ """Use the raw SQL when computing the model fingerprint.""" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py b/sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py index 0163b36ab4..1abc4fa4af 100644 --- a/sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py +++ b/sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py @@ -7,9 +7,7 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate(state_sync, **kwargs): # type: ignore - import pandas as pd - +def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" @@ -43,6 +41,19 @@ def migrate(state_sync, **kwargs): # type: ignore ) engine_adapter.execute(add_fingerprint_exp) + +def migrate_rows(state_sync, **kwargs): # type: ignore + import pandas as pd + + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + index_type = index_text_type(engine_adapter.dialect) + blob_type = blob_text_type(engine_adapter.dialect) + new_snapshots = [] for ( diff --git a/sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py b/sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py index ce39946b0d..802d996df5 100644 --- a/sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py +++ b/sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py @@ -17,7 +17,11 @@ SQLMESH_DBT_PACKAGE = "sqlmesh.dbt" -def migrate(state_sync, **kwargs): # type: ignore +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore engine_adapter = state_sync.engine_adapter schema = state_sync.schema snapshots_table = "_snapshots" diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index c8e38f448c..0a39c155cf 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -168,9 +168,9 @@ def test_render( assert output.stdout == "" assert output.stderr == "" - assert len(output.outputs) == 1 - assert len(convert_all_html_output_to_text(output)[0]) > 2200 - assert len(convert_all_html_output_to_tags(output)[0]) > 150 + assert len(output.outputs) == 2 + assert len(convert_all_html_output_to_text(output)[1]) > 2200 + assert len(convert_all_html_output_to_tags(output)[1]) > 150 @pytest.mark.slow @@ -182,9 +182,9 @@ def test_render_no_format( assert output.stdout == "" assert output.stderr == "" - assert len(output.outputs) == 1 - assert len(convert_all_html_output_to_text(output)[0]) >= 700 - assert len(convert_all_html_output_to_tags(output)[0]) >= 50 + assert len(output.outputs) == 2 + assert len(convert_all_html_output_to_text(output)[1]) >= 700 + assert len(convert_all_html_output_to_tags(output)[1]) >= 50 @pytest.mark.slow From 09eb13218fa8318fea1895218124458ab1c529e8 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Sat, 6 Sep 2025 19:05:41 -0700 Subject: [PATCH 0822/1056] Chore!: Remove the 'prompt' CLI command (#5310) --- pyproject.toml | 2 -- sqlmesh/cli/main.py | 44 ----------------------------- sqlmesh/integrations/llm.py | 56 ------------------------------------- 3 files changed, 102 deletions(-) delete mode 100644 sqlmesh/integrations/llm.py diff --git a/pyproject.toml b/pyproject.toml index 4267f65319..5b66f8ba32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,7 +107,6 @@ duckdb = [] fabric = ["pyodbc>=5.0.0"] gcppostgres = ["cloud-sql-python-connector[pg8000]>=1.8.0"] github = ["PyGithub>=2.6.0"] -llm = ["langchain", "openai"] motherduck = ["duckdb>=1.2.0"] mssql = ["pymssql"] mssql-odbc = ["pyodbc>=5.0.0"] @@ -213,7 +212,6 @@ module = [ "pymssql.*", "pyodbc.*", "psycopg2.*", - "langchain.*", "pytest_lazyfixture.*", "dbt.adapters.*", "slack_sdk.*", diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 2d8673405f..2f18c0a4b7 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -1079,50 +1079,6 @@ def rewrite(obj: Context, sql: str, read: str = "", write: str = "") -> None: ) -@cli.command("prompt") -@click.argument("prompt") -@click.option( - "-e", - "--evaluate", - is_flag=True, - help="Evaluate the generated SQL query and display the results.", -) -@click.option( - "-t", - "--temperature", - type=float, - help="Sampling temperature. 0.0 - precise and predictable, 0.5 - balanced, 1.0 - creative. Default: 0.7", - default=0.7, -) -@opt.verbose -@click.pass_context -@error_handler -@cli_analytics -def prompt( - ctx: click.Context, - prompt: str, - evaluate: bool, - temperature: float, - verbose: int, -) -> None: - """Uses LLM to generate a SQL query from a prompt.""" - from sqlmesh.integrations.llm import LLMIntegration - - context = ctx.obj - - llm_integration = LLMIntegration( - context.models.values(), - context.engine_adapter.dialect, - temperature=temperature, - verbosity=Verbosity(verbose), - ) - query = llm_integration.query(prompt) - - context.console.log_status_update(query) - if evaluate: - context.console.log_success(context.fetchdf(query)) - - @cli.command("clean") @click.pass_obj @error_handler diff --git a/sqlmesh/integrations/llm.py b/sqlmesh/integrations/llm.py deleted file mode 100644 index a44ec79997..0000000000 --- a/sqlmesh/integrations/llm.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -import typing as t - -from langchain import LLMChain, PromptTemplate -from langchain.chat_models import ChatOpenAI - -from sqlmesh.utils import Verbosity -from sqlmesh.core.model import Model - -_QUERY_PROMPT_TEMPLATE = """Given an input request, create a syntactically correct {dialect} SQL query. -Use full table names. -Convert string operands to lowercase in the WHERE clause. -Reply with a SQL query and nothing else. - -Use the following tables and columns: - -{table_info} - -Request: {input}""" - - -class LLMIntegration: - def __init__( - self, - models: t.Iterable[Model], - dialect: str, - temperature: float = 0.7, - verbosity: Verbosity = Verbosity.DEFAULT, - ): - query_prompt_template = PromptTemplate.from_template(_QUERY_PROMPT_TEMPLATE).partial( - dialect=dialect, table_info=_to_table_info(models) - ) - llm = ChatOpenAI(temperature=temperature) # type: ignore - self._query_chain = LLMChain( - llm=llm, prompt=query_prompt_template, verbose=verbosity >= Verbosity.VERBOSE - ) - - def query(self, prompt: str) -> str: - result = self._query_chain.predict(input=prompt).strip() - select_pos = result.find("SELECT") - if select_pos >= 0: - return result[select_pos:] - return result - - -def _to_table_info(models: t.Iterable[Model]) -> str: - infos = [] - for model in models: - if not model.kind.is_materialized: - continue - - columns_csv = ", ".join(model.columns_to_types_or_raise) - infos.append(f"Table: {model.name}\nColumns: {columns_csv}\n") - - return "\n".join(infos) From eab30a61f02d231dc20d1d75398f8fc5efcc3d5e Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Mon, 8 Sep 2025 12:49:07 +1200 Subject: [PATCH 0823/1056] Fix: exclude bugged version of dbt-snowflake so pip resolves compatible version of dbt-adapters (#5313) --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5b66f8ba32..cdc9a4063f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,10 @@ dev = [ "dbt-bigquery", "dbt-core", "dbt-duckdb>=1.7.1", - "dbt-snowflake", + # version 1.10.1 of dbt-snowflake declares that it's compatible with dbt-adapters>=1.16 but in reality + # it depends on the 'InvalidCatalogIntegrationConfigError' class that only exists as of dbt-adapters==1.16.6 + # so we exclude it to prevent failures and hope that upstream releases a new version with the correct constraint + "dbt-snowflake!=1.10.1", "dbt-athena-community", "dbt-clickhouse", "dbt-databricks", From a6b32980ba138e4f9d24c9f28e8185dc8978e7e5 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Mon, 8 Sep 2025 13:36:28 +1200 Subject: [PATCH 0824/1056] Feat(state_sync): Add the ability to fetch all versions of a snapshot by name (#5273) --- sqlmesh/core/snapshot/__init__.py | 1 + sqlmesh/core/snapshot/definition.py | 37 +++++++++- sqlmesh/core/state_sync/base.py | 19 ++++++ sqlmesh/core/state_sync/db/facade.py | 11 +++ sqlmesh/core/state_sync/db/snapshot.py | 75 +++++++++++++------- tests/core/state_sync/test_state_sync.py | 87 ++++++++++++++++++++++++ tests/core/test_snapshot.py | 33 +++++++++ 7 files changed, 236 insertions(+), 27 deletions(-) diff --git a/sqlmesh/core/snapshot/__init__.py b/sqlmesh/core/snapshot/__init__.py index 8ad574f8ac..32842cc4b2 100644 --- a/sqlmesh/core/snapshot/__init__.py +++ b/sqlmesh/core/snapshot/__init__.py @@ -4,6 +4,7 @@ Node as Node, QualifiedViewName as QualifiedViewName, Snapshot as Snapshot, + SnapshotIdAndVersion as SnapshotIdAndVersion, SnapshotChangeCategory as SnapshotChangeCategory, SnapshotDataVersion as SnapshotDataVersion, SnapshotFingerprint as SnapshotFingerprint, diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index dea4ef64e5..c124c2098f 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -588,6 +588,37 @@ def name_version(self) -> SnapshotNameVersion: return SnapshotNameVersion(name=self.name, version=self.version) +class SnapshotIdAndVersion(PydanticModel): + """A stripped down version of a snapshot that is used in situations where we want to fetch the main fields of the snapshots table + without the overhead of parsing the full snapshot payload and fetching intervals. + """ + + name: str + version: str + dev_version_: t.Optional[str] = Field(alias="dev_version") + identifier: str + fingerprint_: t.Union[str, SnapshotFingerprint] = Field(alias="fingerprint") + + @property + def snapshot_id(self) -> SnapshotId: + return SnapshotId(name=self.name, identifier=self.identifier) + + @property + def name_version(self) -> SnapshotNameVersion: + return SnapshotNameVersion(name=self.name, version=self.version) + + @property + def fingerprint(self) -> SnapshotFingerprint: + value = self.fingerprint_ + if isinstance(value, str): + self.fingerprint_ = value = SnapshotFingerprint.parse_raw(value) + return value + + @property + def dev_version(self) -> str: + return self.dev_version_ or self.fingerprint.to_version() + + class Snapshot(PydanticModel, SnapshotInfoMixin): """A snapshot represents a node at a certain point in time. @@ -1463,9 +1494,11 @@ class SnapshotTableCleanupTask(PydanticModel): dev_table_only: bool -SnapshotIdLike = t.Union[SnapshotId, SnapshotTableInfo, Snapshot] +SnapshotIdLike = t.Union[SnapshotId, SnapshotTableInfo, SnapshotIdAndVersion, Snapshot] SnapshotInfoLike = t.Union[SnapshotTableInfo, Snapshot] -SnapshotNameVersionLike = t.Union[SnapshotNameVersion, SnapshotTableInfo, Snapshot] +SnapshotNameVersionLike = t.Union[ + SnapshotNameVersion, SnapshotTableInfo, SnapshotIdAndVersion, Snapshot +] class DeployabilityIndex(PydanticModel, frozen=True): diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index 4d3d51a469..a8f73b6937 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -23,6 +23,7 @@ SnapshotTableCleanupTask, SnapshotTableInfo, SnapshotNameVersion, + SnapshotIdAndVersion, ) from sqlmesh.core.snapshot.definition import Interval, SnapshotIntervals from sqlmesh.utils import major_minor @@ -97,6 +98,24 @@ def get_snapshots( A dictionary of snapshot ids to snapshots for ones that could be found. """ + @abc.abstractmethod + def get_snapshots_by_names( + self, + snapshot_names: t.Iterable[str], + current_ts: t.Optional[int] = None, + exclude_expired: bool = True, + ) -> t.Set[SnapshotIdAndVersion]: + """Return the snapshot records for all versions of the specified snapshot names. + + Args: + snapshot_names: Iterable of snapshot names to fetch all snapshot records for + current_ts: Sets the current time for identifying which snapshots have expired so they can be excluded (only relevant if :exclude_expired=True) + exclude_expired: Whether or not to return the snapshot id's of expired snapshots in the result + + Returns: + A set containing all the matched snapshot records. To fetch full snapshots, pass it into StateSync.get_snapshots() + """ + @abc.abstractmethod def snapshots_exist(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> t.Set[SnapshotId]: """Checks if multiple snapshots exist in the state sync. diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 898ba75651..85bebcc5d6 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -29,6 +29,7 @@ from sqlmesh.core.environment import Environment, EnvironmentStatements, EnvironmentSummary from sqlmesh.core.snapshot import ( Snapshot, + SnapshotIdAndVersion, SnapshotId, SnapshotIdLike, SnapshotInfoLike, @@ -366,6 +367,16 @@ def get_snapshots( Snapshot.hydrate_with_intervals_by_version(snapshots.values(), intervals) return snapshots + def get_snapshots_by_names( + self, + snapshot_names: t.Iterable[str], + current_ts: t.Optional[int] = None, + exclude_expired: bool = True, + ) -> t.Set[SnapshotIdAndVersion]: + return self.snapshot_state.get_snapshots_by_names( + snapshot_names=snapshot_names, current_ts=current_ts, exclude_expired=exclude_expired + ) + @transactional() def add_interval( self, diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index af10f0192e..1904e51c55 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -6,7 +6,6 @@ from pathlib import Path from collections import defaultdict from sqlglot import exp -from pydantic import Field from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.state_sync.db.utils import ( @@ -27,12 +26,12 @@ SnapshotNameVersion, SnapshotInfoLike, Snapshot, + SnapshotIdAndVersion, SnapshotId, SnapshotFingerprint, ) from sqlmesh.utils.migration import index_text_type, blob_text_type from sqlmesh.utils.date import now_timestamp, TimeLike, to_timestamp -from sqlmesh.utils.pydantic import PydanticModel from sqlmesh.utils import unique if t.TYPE_CHECKING: @@ -215,7 +214,7 @@ def _get_expired_snapshots( for snapshot in environment.snapshots } - def _is_snapshot_used(snapshot: SharedVersionSnapshot) -> bool: + def _is_snapshot_used(snapshot: SnapshotIdAndVersion) -> bool: return ( snapshot.snapshot_id in promoted_snapshot_ids or snapshot.snapshot_id not in expired_candidates @@ -308,6 +307,52 @@ def get_snapshots( """ return self._get_snapshots(snapshot_ids) + def get_snapshots_by_names( + self, + snapshot_names: t.Iterable[str], + current_ts: t.Optional[int] = None, + exclude_expired: bool = True, + ) -> t.Set[SnapshotIdAndVersion]: + """Return the snapshot records for all versions of the specified snapshot names. + + Args: + snapshot_names: Iterable of snapshot names to fetch all snapshot records for + current_ts: Sets the current time for identifying which snapshots have expired so they can be excluded (only relevant if :exclude_expired=True) + exclude_expired: Whether or not to return the snapshot id's of expired snapshots in the result + + Returns: + A set containing all the matched snapshot records. To fetch full snapshots, pass it into StateSync.get_snapshots() + """ + if not snapshot_names: + return set() + + if exclude_expired: + current_ts = current_ts or now_timestamp() + unexpired_expr = (exp.column("updated_ts") + exp.column("ttl_ms")) > current_ts + else: + unexpired_expr = None + + return { + SnapshotIdAndVersion( + name=name, + identifier=identifier, + version=version, + dev_version=dev_version, + fingerprint=fingerprint, + ) + for where in snapshot_name_filter( + snapshot_names=snapshot_names, + batch_size=self.SNAPSHOT_BATCH_SIZE, + ) + for name, identifier, version, dev_version, fingerprint in fetchall( + self.engine_adapter, + exp.select("name", "identifier", "version", "dev_version", "fingerprint") + .from_(self.snapshots_table) + .where(where) + .and_(unexpired_expr), + ) + } + def snapshots_exist(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> t.Set[SnapshotId]: """Checks if snapshots exist. @@ -591,7 +636,7 @@ def _get_snapshots_with_same_version( self, snapshots: t.Collection[SnapshotNameVersionLike], lock_for_update: bool = False, - ) -> t.List[SharedVersionSnapshot]: + ) -> t.List[SnapshotIdAndVersion]: """Fetches all snapshots that share the same version as the snapshots. The output includes the snapshots with the specified identifiers. @@ -628,7 +673,7 @@ def _get_snapshots_with_same_version( snapshot_rows.extend(fetchall(self.engine_adapter, query)) return [ - SharedVersionSnapshot( + SnapshotIdAndVersion( name=name, identifier=identifier, version=version, @@ -711,23 +756,3 @@ def _auto_restatements_to_df(auto_restatements: t.Dict[SnapshotNameVersion, int] for name_version, ts in auto_restatements.items() ] ) - - -class SharedVersionSnapshot(PydanticModel): - """A stripped down version of a snapshot that is used for fetching snapshots that share the same version - with a significantly reduced parsing overhead. - """ - - name: str - version: str - dev_version_: t.Optional[str] = Field(alias="dev_version") - identifier: str - fingerprint: SnapshotFingerprint - - @property - def snapshot_id(self) -> SnapshotId: - return SnapshotId(name=self.name, identifier=self.identifier) - - @property - def dev_version(self) -> str: - return self.dev_version_ or self.fingerprint.to_version() diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index be8e4ad3e0..327ec82210 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -3569,3 +3569,90 @@ def test_update_environment_statements(state_sync: EngineAdapterStateSync): "@grant_schema_usage()", "@grant_select_privileges()", ] + + +def test_get_snapshots_by_names( + state_sync: EngineAdapterStateSync, make_snapshot: t.Callable[..., Snapshot] +): + assert state_sync.get_snapshots_by_names(snapshot_names=[]) == set() + + snap_a_v1, snap_a_v2 = ( + make_snapshot( + SqlModel( + name="a", + query=parse_one(f"select {i}, ds"), + ), + version="a", + ) + for i in range(2) + ) + + snap_b = make_snapshot( + SqlModel( + name="b", + query=parse_one(f"select 'b' as b, ds"), + ), + version="b", + ) + + state_sync.push_snapshots([snap_a_v1, snap_a_v2, snap_b]) + + assert {s.snapshot_id for s in state_sync.get_snapshots_by_names(snapshot_names=['"a"'])} == { + snap_a_v1.snapshot_id, + snap_a_v2.snapshot_id, + } + assert { + s.snapshot_id for s in state_sync.get_snapshots_by_names(snapshot_names=['"a"', '"b"']) + } == { + snap_a_v1.snapshot_id, + snap_a_v2.snapshot_id, + snap_b.snapshot_id, + } + + +def test_get_snapshots_by_names_include_expired( + state_sync: EngineAdapterStateSync, make_snapshot: t.Callable[..., Snapshot] +): + now_ts = now_timestamp() + + normal_a = make_snapshot( + SqlModel( + name="a", + query=parse_one(f"select 1, ds"), + ), + version="a", + ) + + expired_a = make_snapshot( + SqlModel( + name="a", + query=parse_one(f"select 2, ds"), + ), + version="a", + ttl="in 10 seconds", + ) + expired_a.updated_ts = now_ts - ( + 1000 * 15 + ) # last updated 15 seconds ago, expired 10 seconds from last updated = expired 5 seconds ago + + state_sync.push_snapshots([normal_a, expired_a]) + + assert { + s.snapshot_id + for s in state_sync.get_snapshots_by_names(snapshot_names=['"a"'], current_ts=now_ts) + } == {normal_a.snapshot_id} + assert { + s.snapshot_id + for s in state_sync.get_snapshots_by_names(snapshot_names=['"a"'], exclude_expired=False) + } == { + normal_a.snapshot_id, + expired_a.snapshot_id, + } + + # wind back time to 10 seconds ago (before the expired snapshot is expired - it expired 5 seconds ago) to test it stil shows in a normal query + assert { + s.snapshot_id + for s in state_sync.get_snapshots_by_names( + snapshot_names=['"a"'], current_ts=(now_ts - (10 * 1000)) + ) + } == {normal_a.snapshot_id, expired_a.snapshot_id} diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index 9e36ecc3ae..c37bd57d2e 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -44,6 +44,7 @@ QualifiedViewName, Snapshot, SnapshotId, + SnapshotIdAndVersion, SnapshotChangeCategory, SnapshotFingerprint, SnapshotIntervals, @@ -3532,3 +3533,35 @@ def test_table_name_virtual_environment_mode( assert table_name_result.endswith(snapshot.version) else: assert table_name_result.endswith(f"{snapshot.dev_version}__dev") + + +def test_snapshot_id_and_version_fingerprint_lazy_init(): + snapshot = SnapshotIdAndVersion( + name="a", + identifier="1234", + version="2345", + dev_version=None, + fingerprint='{"data_hash":"1","metadata_hash":"2","parent_data_hash":"3","parent_metadata_hash":"4"}', + ) + + # starts off as a string in the private property + assert isinstance(snapshot.fingerprint_, str) + + # gets parsed into SnapshotFingerprint on first access of public property + fingerprint = snapshot.fingerprint + assert isinstance(fingerprint, SnapshotFingerprint) + assert isinstance(snapshot.fingerprint_, SnapshotFingerprint) + + assert fingerprint.data_hash == "1" + assert fingerprint.metadata_hash == "2" + assert fingerprint.parent_data_hash == "3" + assert fingerprint.parent_metadata_hash == "4" + assert snapshot.dev_version is not None # dev version uses fingerprint + + # can also be supplied as a SnapshotFingerprint to begin with instead of a str + snapshot = SnapshotIdAndVersion( + name="a", identifier="1234", version="2345", dev_version=None, fingerprint=fingerprint + ) + + assert isinstance(snapshot.fingerprint_, SnapshotFingerprint) + assert snapshot.fingerprint == fingerprint From a6468f5621a531f198fcfc66c9fbce822c2d1530 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:53:45 +0300 Subject: [PATCH 0825/1056] chore: add singular test to dbt fixtures (#5315) Co-authored-by: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> --- tests/core/test_integration.py | 1 + tests/fixtures/dbt/sushi_test/tests/test_top_waiters.sql | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 tests/fixtures/dbt/sushi_test/tests/test_top_waiters.sql diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index c00733238a..3a72dcca60 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -2045,6 +2045,7 @@ def test_dbt_is_incremental_table_is_missing(sushi_test_dbt_context: Context): model = context.get_model("sushi.waiter_revenue_by_day_v2") model = model.copy(update={"kind": IncrementalUnmanagedKind(), "start": "2023-01-01"}) context.upsert_model(model) + context._standalone_audits["test_top_waiters"].start = "2023-01-01" context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) diff --git a/tests/fixtures/dbt/sushi_test/tests/test_top_waiters.sql b/tests/fixtures/dbt/sushi_test/tests/test_top_waiters.sql new file mode 100644 index 0000000000..db6233db07 --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/tests/test_top_waiters.sql @@ -0,0 +1,4 @@ +-- Check that revenue is positive +SELECT waiter_id +FROM {{ ref('top_waiters') }} +WHERE revenue < 0 From a677000ae98d088c50447c61e14b138a409c807e Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 8 Sep 2025 15:18:20 +0000 Subject: [PATCH 0826/1056] Fix: make sure builtin dbt macros can be referenced with the 'context.' prefix (#5308) --- sqlmesh/dbt/builtin.py | 1 + tests/dbt/test_transformation.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index c4fe0540b7..8d48c4a77a 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -545,6 +545,7 @@ def create_builtin_globals( builtin_globals["run_started_at"] = jinja_globals.get("execution_dt") or now() builtin_globals["dbt"] = AttributeDict(builtin_globals) + builtin_globals["context"] = builtin_globals["dbt"] return {**builtin_globals, **jinja_globals} diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index b5de61031b..89e4bca154 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1435,6 +1435,13 @@ def test_flags(sushi_test_project: Project): assert context.render("{{ flags.WHICH }}") == "parse" +@pytest.mark.xdist_group("dbt_manifest") +def test_context_namespace(sushi_test_project: Project): + context = sushi_test_project.context + + assert context.render("{{ context.flags.FULL_REFRESH }}") == "None" + + @pytest.mark.xdist_group("dbt_manifest") def test_relation(sushi_test_project: Project): context = sushi_test_project.context From 971eccfe4513a3b7d1e41d36a4e2dae0bd2ffd71 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:28:53 -0700 Subject: [PATCH 0827/1056] feat: dbt microbatch ref filter support (#5292) --- sqlmesh/core/renderer.py | 20 +++++- sqlmesh/dbt/basemodel.py | 9 +++ sqlmesh/dbt/source.py | 7 ++ sqlmesh/utils/date.py | 7 ++ tests/core/test_snapshot.py | 4 +- tests/dbt/test_model.py | 132 ++++++++++++++++++++++++++++++++++++ 6 files changed, 177 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index e1d7bf4bcf..3502118e14 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -16,7 +16,14 @@ from sqlmesh.core import constants as c from sqlmesh.core import dialect as d from sqlmesh.core.macros import MacroEvaluator, RuntimeStage -from sqlmesh.utils.date import TimeLike, date_dict, make_inclusive, to_datetime +from sqlmesh.utils.date import ( + TimeLike, + date_dict, + make_inclusive, + to_datetime, + make_ts_exclusive, + to_tstz, +) from sqlmesh.utils.errors import ( ConfigError, ParsetimeAdapterCallError, @@ -214,6 +221,17 @@ def _resolve_table(table: str | exp.Table) -> str: dialect=self._dialect, identify=True, comments=False ) + all_refs = list( + self._jinja_macro_registry.global_objs.get("sources", {}).values() # type: ignore + ) + list( + self._jinja_macro_registry.global_objs.get("refs", {}).values() # type: ignore + ) + for ref in all_refs: + if ref.event_time_filter: + ref.event_time_filter["start"] = render_kwargs["start_tstz"] + ref.event_time_filter["end"] = to_tstz( + make_ts_exclusive(render_kwargs["end_tstz"], dialect=self._dialect) + ) jinja_env = self._jinja_macro_registry.build_environment(**jinja_env_kwargs) expressions = [] diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index 548718cf89..a68a6ed598 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -28,6 +28,7 @@ ) from sqlmesh.dbt.relation import Policy, RelationType from sqlmesh.dbt.test import TestConfig +from sqlmesh.dbt.util import DBT_VERSION from sqlmesh.utils import AttributeDict from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.pydantic import field_validator @@ -130,6 +131,7 @@ class BaseModelConfig(GeneralConfig): grants: t.Dict[str, t.List[str]] = {} columns: t.Dict[str, ColumnConfig] = {} quoting: t.Dict[str, t.Optional[bool]] = {} + event_time: t.Optional[str] = None version: t.Optional[int] = None latest_version: t.Optional[int] = None @@ -222,6 +224,12 @@ def relation_info(self) -> AttributeDict[str, t.Any]: else: relation_type = RelationType.Table + extras = {} + if DBT_VERSION >= (1, 9, 0) and self.event_time: + extras["event_time_filter"] = { + "field_name": self.event_time, + } + return AttributeDict( { "database": self.database, @@ -229,6 +237,7 @@ def relation_info(self) -> AttributeDict[str, t.Any]: "identifier": self.table_name, "type": relation_type.value, "quote_policy": AttributeDict(self.quoting), + **extras, } ) diff --git a/sqlmesh/dbt/source.py b/sqlmesh/dbt/source.py index 5b182c9a9a..76ee682e77 100644 --- a/sqlmesh/dbt/source.py +++ b/sqlmesh/dbt/source.py @@ -8,6 +8,7 @@ from sqlmesh.dbt.column import ColumnConfig from sqlmesh.dbt.common import GeneralConfig from sqlmesh.dbt.relation import RelationType +from sqlmesh.dbt.util import DBT_VERSION from sqlmesh.utils import AttributeDict from sqlmesh.utils.errors import ConfigError @@ -46,6 +47,7 @@ class SourceConfig(GeneralConfig): external: t.Optional[t.Dict[str, t.Any]] = {} source_meta: t.Optional[t.Dict[str, t.Any]] = {} columns: t.Dict[str, ColumnConfig] = {} + event_time: t.Optional[str] = None _canonical_name: t.Optional[str] = None @@ -94,6 +96,11 @@ def relation_info(self) -> AttributeDict: if external_location: extras["external"] = external_location.replace("{name}", self.table_name) + if DBT_VERSION >= (1, 9, 0) and self.event_time: + extras["event_time_filter"] = { + "field_name": self.event_time, + } + return AttributeDict( { "database": self.database, diff --git a/sqlmesh/utils/date.py b/sqlmesh/utils/date.py index 53a53cd62a..931cebf535 100644 --- a/sqlmesh/utils/date.py +++ b/sqlmesh/utils/date.py @@ -343,6 +343,13 @@ def make_exclusive(time: TimeLike) -> datetime: return dt +def make_ts_exclusive(time: TimeLike, dialect: DialectType) -> datetime: + ts = to_datetime(time) + if dialect == "tsql": + return to_utc_timestamp(ts) - pd.Timedelta(1, unit="ns") + return ts + timedelta(microseconds=1) + + def to_utc_timestamp(time: datetime) -> pd.Timestamp: import pandas as pd diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index c37bd57d2e..eff3ad2b60 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -1079,7 +1079,9 @@ def test_fingerprint_jinja_macros_global_objs(model: Model, global_obj_key: str) ) fingerprint = fingerprint_from_node(model, nodes={}) model = model.copy() - model.jinja_macros.global_objs[global_obj_key] = AttributeDict({"test": "test"}) + model.jinja_macros.global_objs[global_obj_key] = AttributeDict( + {"test": AttributeDict({"test": "test"})} + ) updated_fingerprint = fingerprint_from_node(model, nodes={}) assert updated_fingerprint.data_hash != fingerprint.data_hash assert updated_fingerprint.metadata_hash == fingerprint.metadata_hash diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index 5037a69d65..14c042422e 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -447,3 +447,135 @@ def test_load_deprecated_incremental_time_column( "Using `time_column` on a model with incremental_strategy 'delete+insert' has been deprecated. Please use `incremental_by_time_range` instead in model 'main.incremental_time_range'." in caplog.text ) + + +@pytest.mark.slow +def test_load_microbatch_with_ref( + tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project +) -> None: + yaml = YAML() + project_dir, model_dir = create_empty_project() + source_schema = { + "version": 2, + "sources": [ + { + "name": "my_source", + "tables": [{"name": "my_table", "config": {"event_time": "ds_source"}}], + } + ], + } + source_schema_file = model_dir / "source_schema.yml" + with open(source_schema_file, "w", encoding="utf-8") as f: + yaml.dump(source_schema, f) + # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it + microbatch_contents = """ + {{ + config( + materialized='incremental', + incremental_strategy='microbatch', + event_time='ds', + begin='2020-01-01', + batch_size='day' + ) + }} + + SELECT cola, ds_source as ds FROM {{ source('my_source', 'my_table') }} + """ + microbatch_model_file = model_dir / "microbatch.sql" + with open(microbatch_model_file, "w", encoding="utf-8") as f: + f.write(microbatch_contents) + + microbatch_two_contents = """ + {{ + config( + materialized='incremental', + incremental_strategy='microbatch', + event_time='ds', + begin='2020-01-05', + batch_size='day' + ) + }} + + SELECT cola, ds FROM {{ ref('microbatch') }} + """ + microbatch_two_model_file = model_dir / "microbatch_two.sql" + with open(microbatch_two_model_file, "w", encoding="utf-8") as f: + f.write(microbatch_two_contents) + + microbatch_snapshot_fqn = '"local"."main"."microbatch"' + microbatch_two_snapshot_fqn = '"local"."main"."microbatch_two"' + context = Context(paths=project_dir) + assert ( + context.render(microbatch_snapshot_fqn, start="2025-01-01", end="2025-01-10").sql() + == 'SELECT "cola" AS "cola", "ds_source" AS "ds" FROM (SELECT * FROM "local"."my_source"."my_table" AS "my_table" WHERE "ds_source" >= \'2025-01-01 00:00:00+00:00\' AND "ds_source" < \'2025-01-11 00:00:00+00:00\') AS "_q_0"' + ) + assert ( + context.render(microbatch_two_snapshot_fqn, start="2025-01-01", end="2025-01-10").sql() + == 'SELECT "_q_0"."cola" AS "cola", "_q_0"."ds" AS "ds" FROM (SELECT "microbatch"."cola" AS "cola", "microbatch"."ds" AS "ds" FROM "local"."main"."microbatch" AS "microbatch" WHERE "microbatch"."ds" < \'2025-01-11 00:00:00+00:00\' AND "microbatch"."ds" >= \'2025-01-01 00:00:00+00:00\') AS "_q_0"' + ) + + +@pytest.mark.slow +def test_load_microbatch_with_ref_no_filter( + tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project +) -> None: + yaml = YAML() + project_dir, model_dir = create_empty_project() + source_schema = { + "version": 2, + "sources": [ + { + "name": "my_source", + "tables": [{"name": "my_table", "config": {"event_time": "ds"}}], + } + ], + } + source_schema_file = model_dir / "source_schema.yml" + with open(source_schema_file, "w", encoding="utf-8") as f: + yaml.dump(source_schema, f) + # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it + microbatch_contents = """ + {{ + config( + materialized='incremental', + incremental_strategy='microbatch', + event_time='ds', + begin='2020-01-01', + batch_size='day' + ) + }} + + SELECT cola, ds FROM {{ source('my_source', 'my_table').render() }} + """ + microbatch_model_file = model_dir / "microbatch.sql" + with open(microbatch_model_file, "w", encoding="utf-8") as f: + f.write(microbatch_contents) + + microbatch_two_contents = """ + {{ + config( + materialized='incremental', + incremental_strategy='microbatch', + event_time='ds', + begin='2020-01-01', + batch_size='day' + ) + }} + + SELECT cola, ds FROM {{ ref('microbatch').render() }} + """ + microbatch_two_model_file = model_dir / "microbatch_two.sql" + with open(microbatch_two_model_file, "w", encoding="utf-8") as f: + f.write(microbatch_two_contents) + + microbatch_snapshot_fqn = '"local"."main"."microbatch"' + microbatch_two_snapshot_fqn = '"local"."main"."microbatch_two"' + context = Context(paths=project_dir) + assert ( + context.render(microbatch_snapshot_fqn, start="2025-01-01", end="2025-01-10").sql() + == 'SELECT "cola" AS "cola", "ds" AS "ds" FROM "local"."my_source"."my_table" AS "my_table"' + ) + assert ( + context.render(microbatch_two_snapshot_fqn, start="2025-01-01", end="2025-01-10").sql() + == 'SELECT "microbatch"."cola" AS "cola", "microbatch"."ds" AS "ds" FROM "local"."main"."microbatch" AS "microbatch"' + ) From b8bc22424513a01d91d8742b93091529a9ad2888 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 8 Sep 2025 09:40:53 -0700 Subject: [PATCH 0828/1056] Chore!: Remove old migration scripts up to and including v0060 (#5309) --- sqlmesh/core/context.py | 3 +- sqlmesh/core/state_sync/base.py | 6 +- sqlmesh/core/state_sync/db/facade.py | 6 - sqlmesh/core/state_sync/db/migrator.py | 31 +- ...ore_table_indexes.py => v0000_baseline.py} | 78 ++-- sqlmesh/migrations/v0001_init.py | 64 --- sqlmesh/migrations/v0002_remove_identify.py | 9 - sqlmesh/migrations/v0003_move_batch_size.py | 38 -- .../v0004_environmnent_add_finalized_at.py | 27 -- sqlmesh/migrations/v0005_create_seed_table.py | 28 -- sqlmesh/migrations/v0006_change_seed_hash.py | 9 - .../v0007_env_table_info_to_kind.py | 103 ----- .../v0008_create_intervals_table.py | 42 -- .../migrations/v0009_remove_pre_post_hooks.py | 66 ---- .../migrations/v0010_seed_hash_batch_size.py | 9 - .../migrations/v0011_add_model_kind_name.py | 73 ---- .../v0012_update_jinja_expressions.py | 90 ----- .../v0013_serde_using_model_dialects.py | 91 ----- sqlmesh/migrations/v0014_fix_dev_intervals.py | 18 - ...5_environment_add_promoted_snapshot_ids.py | 30 -- sqlmesh/migrations/v0016_fix_windows_path.py | 63 --- .../migrations/v0017_fix_windows_seed_path.py | 59 --- .../v0018_rename_snapshot_model_to_node.py | 57 --- .../migrations/v0019_add_env_suffix_target.py | 35 -- ...ve_redundant_attributes_from_dbt_models.py | 84 ---- .../migrations/v0021_fix_table_properties.py | 66 ---- .../migrations/v0022_move_project_to_model.py | 58 --- ..._added_models_with_forward_only_parents.py | 69 ---- ...replace_model_kind_name_enum_with_value.py | 59 --- ...x_intervals_and_missing_change_category.py | 121 ------ .../v0026_remove_dialect_from_seed.py | 59 --- .../v0027_minute_interval_to_five.py | 61 --- .../migrations/v0028_add_plan_dags_table.py | 33 -- ...029_generate_schema_types_using_dialect.py | 73 ---- .../v0030_update_unrestorable_snapshots.py | 69 ---- .../v0031_remove_dbt_target_fields.py | 69 ---- .../migrations/v0032_add_sqlmesh_version.py | 29 -- .../v0033_mysql_fix_blob_text_type.py | 49 --- .../migrations/v0034_add_default_catalog.py | 371 ------------------ .../v0035_add_catalog_name_override.py | 26 -- .../v0036_delete_plan_dags_bug_fix.py | 18 - .../v0037_remove_dbt_is_incremental_macro.py | 65 --- .../v0038_add_expiration_ts_to_snapshot.py | 80 ---- ...39_include_environment_in_plan_dag_spec.py | 72 ---- .../v0040_add_previous_finalized_snapshots.py | 30 -- .../v0041_remove_hash_raw_query_attribute.py | 63 --- .../v0042_trim_indirect_versions.py | 70 ---- ...remove_obsolete_attributes_in_plan_dags.py | 65 --- ...4_quote_identifiers_in_model_attributes.py | 9 - .../migrations/v0045_move_gateway_variable.py | 74 ---- .../migrations/v0046_add_batch_concurrency.py | 12 - .../v0047_change_scd_string_to_column.py | 9 - .../v0048_drop_indirect_versions.py | 63 --- ..._identifier_with_version_in_seeds_table.py | 61 --- sqlmesh/migrations/v0050_drop_seeds_table.py | 15 - .../v0051_rename_column_descriptions.py | 69 ---- ...rmalize_name_in_environment_naming_info.py | 35 -- ...0053_custom_model_kind_extra_attributes.py | 9 - .../migrations/v0054_fix_trailing_comments.py | 9 - ...used_ts_ttl_ms_unrestorable_to_snapshot.py | 140 ------- sqlmesh/migrations/v0057_add_table_format.py | 9 - sqlmesh/migrations/v0058_add_requirements.py | 30 -- .../migrations/v0059_add_physical_version.py | 9 - .../migrations/v0060_move_audits_to_model.py | 90 ----- .../v0061_mysql_fix_blob_text_type.py | 3 - .../v0096_remove_plan_dags_table.py | 15 + sqlmesh/utils/errors.py | 4 + tests/cli/test_cli.py | 1 - tests/core/state_sync/test_export_import.py | 12 +- tests/core/state_sync/test_state_sync.py | 81 +++- tests/core/test_integration.py | 6 - tests/fixtures/migrations/environments.json | 2 +- tests/fixtures/migrations/intervals.json | 1 + tests/fixtures/migrations/snapshots.json | 2 +- tests/fixtures/migrations/versions.json | 1 + tooling/validating_migration_numbers.sh | 4 +- 76 files changed, 152 insertions(+), 3417 deletions(-) rename sqlmesh/migrations/{v0056_restore_table_indexes.py => v0000_baseline.py} (52%) delete mode 100644 sqlmesh/migrations/v0001_init.py delete mode 100644 sqlmesh/migrations/v0002_remove_identify.py delete mode 100644 sqlmesh/migrations/v0003_move_batch_size.py delete mode 100644 sqlmesh/migrations/v0004_environmnent_add_finalized_at.py delete mode 100644 sqlmesh/migrations/v0005_create_seed_table.py delete mode 100644 sqlmesh/migrations/v0006_change_seed_hash.py delete mode 100644 sqlmesh/migrations/v0007_env_table_info_to_kind.py delete mode 100644 sqlmesh/migrations/v0008_create_intervals_table.py delete mode 100644 sqlmesh/migrations/v0009_remove_pre_post_hooks.py delete mode 100644 sqlmesh/migrations/v0010_seed_hash_batch_size.py delete mode 100644 sqlmesh/migrations/v0011_add_model_kind_name.py delete mode 100644 sqlmesh/migrations/v0012_update_jinja_expressions.py delete mode 100644 sqlmesh/migrations/v0013_serde_using_model_dialects.py delete mode 100644 sqlmesh/migrations/v0014_fix_dev_intervals.py delete mode 100644 sqlmesh/migrations/v0015_environment_add_promoted_snapshot_ids.py delete mode 100644 sqlmesh/migrations/v0016_fix_windows_path.py delete mode 100644 sqlmesh/migrations/v0017_fix_windows_seed_path.py delete mode 100644 sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py delete mode 100644 sqlmesh/migrations/v0019_add_env_suffix_target.py delete mode 100644 sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py delete mode 100644 sqlmesh/migrations/v0021_fix_table_properties.py delete mode 100644 sqlmesh/migrations/v0022_move_project_to_model.py delete mode 100644 sqlmesh/migrations/v0023_fix_added_models_with_forward_only_parents.py delete mode 100644 sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py delete mode 100644 sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py delete mode 100644 sqlmesh/migrations/v0026_remove_dialect_from_seed.py delete mode 100644 sqlmesh/migrations/v0027_minute_interval_to_five.py delete mode 100644 sqlmesh/migrations/v0028_add_plan_dags_table.py delete mode 100644 sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py delete mode 100644 sqlmesh/migrations/v0030_update_unrestorable_snapshots.py delete mode 100644 sqlmesh/migrations/v0031_remove_dbt_target_fields.py delete mode 100644 sqlmesh/migrations/v0032_add_sqlmesh_version.py delete mode 100644 sqlmesh/migrations/v0033_mysql_fix_blob_text_type.py delete mode 100644 sqlmesh/migrations/v0034_add_default_catalog.py delete mode 100644 sqlmesh/migrations/v0035_add_catalog_name_override.py delete mode 100644 sqlmesh/migrations/v0036_delete_plan_dags_bug_fix.py delete mode 100644 sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py delete mode 100644 sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py delete mode 100644 sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py delete mode 100644 sqlmesh/migrations/v0040_add_previous_finalized_snapshots.py delete mode 100644 sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py delete mode 100644 sqlmesh/migrations/v0042_trim_indirect_versions.py delete mode 100644 sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py delete mode 100644 sqlmesh/migrations/v0044_quote_identifiers_in_model_attributes.py delete mode 100644 sqlmesh/migrations/v0045_move_gateway_variable.py delete mode 100644 sqlmesh/migrations/v0046_add_batch_concurrency.py delete mode 100644 sqlmesh/migrations/v0047_change_scd_string_to_column.py delete mode 100644 sqlmesh/migrations/v0048_drop_indirect_versions.py delete mode 100644 sqlmesh/migrations/v0049_replace_identifier_with_version_in_seeds_table.py delete mode 100644 sqlmesh/migrations/v0050_drop_seeds_table.py delete mode 100644 sqlmesh/migrations/v0051_rename_column_descriptions.py delete mode 100644 sqlmesh/migrations/v0052_add_normalize_name_in_environment_naming_info.py delete mode 100644 sqlmesh/migrations/v0053_custom_model_kind_extra_attributes.py delete mode 100644 sqlmesh/migrations/v0054_fix_trailing_comments.py delete mode 100644 sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py delete mode 100644 sqlmesh/migrations/v0057_add_table_format.py delete mode 100644 sqlmesh/migrations/v0058_add_requirements.py delete mode 100644 sqlmesh/migrations/v0059_add_physical_version.py delete mode 100644 sqlmesh/migrations/v0060_move_audits_to_model.py create mode 100644 sqlmesh/migrations/v0096_remove_plan_dags_table.py create mode 100644 tests/fixtures/migrations/intervals.json create mode 100644 tests/fixtures/migrations/versions.json diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 9660243753..a8715adc4c 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -588,7 +588,7 @@ def state_sync(self) -> StateSync: if self._state_sync.get_versions(validate=False).schema_version == 0: self.console.log_status_update("Initializing new project state...") - self._state_sync.migrate(default_catalog=self.default_catalog) + self._state_sync.migrate() self._state_sync.get_versions() self._state_sync = CachingStateSync(self._state_sync) # type: ignore return self._state_sync @@ -2356,7 +2356,6 @@ def migrate(self) -> None: self._load_materializations() try: self._new_state_sync().migrate( - default_catalog=self.default_catalog, promoted_snapshots_only=self.config.migration.promoted_snapshots_only, ) except Exception as e: diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index a8f73b6937..4219472cb6 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -61,11 +61,14 @@ def _schema_version_validator(cls, v: t.Any) -> int: return 0 if v is None else int(v) +MIN_SCHEMA_VERSION = 60 +MIN_SQLMESH_VERSION = "0.134.0" MIGRATIONS = [ importlib.import_module(f"sqlmesh.migrations.{migration}") for migration in sorted(info.name for info in pkgutil.iter_modules(migrations.__path__)) ] -SCHEMA_VERSION: int = len(MIGRATIONS) +# -1 to account for the baseline script +SCHEMA_VERSION: int = MIN_SCHEMA_VERSION + len(MIGRATIONS) - 1 class PromotionResult(PydanticModel): @@ -469,7 +472,6 @@ def compact_intervals(self) -> None: @abc.abstractmethod def migrate( self, - default_catalog: t.Optional[str], skip_backup: bool = False, promoted_snapshots_only: bool = True, ) -> None: diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 85bebcc5d6..93c4b87e9e 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -22,7 +22,6 @@ from pathlib import Path from datetime import datetime -from sqlglot import exp from sqlmesh.core.console import Console, get_console from sqlmesh.core.engine_adapter import EngineAdapter @@ -90,7 +89,6 @@ def __init__( console: t.Optional[Console] = None, cache_dir: Path = Path(), ): - self.plan_dags_table = exp.table_("_plan_dags", db=schema) self.interval_state = IntervalState(engine_adapter, schema=schema) self.environment_state = EnvironmentState(engine_adapter, schema=schema) self.snapshot_state = SnapshotState(engine_adapter, schema=schema, cache_dir=cache_dir) @@ -101,7 +99,6 @@ def __init__( snapshot_state=self.snapshot_state, environment_state=self.environment_state, interval_state=self.interval_state, - plan_dags_table=self.plan_dags_table, console=console, ) # Make sure that if an empty string is provided that we treat it as None @@ -308,7 +305,6 @@ def remove_state(self, including_backup: bool = False) -> None: self.environment_state.environments_table, self.environment_state.environment_statements_table, self.interval_state.intervals_table, - self.plan_dags_table, self.version_state.versions_table, ): self.engine_adapter.drop_table(table) @@ -453,14 +449,12 @@ def close(self) -> None: @transactional() def migrate( self, - default_catalog: t.Optional[str], skip_backup: bool = False, promoted_snapshots_only: bool = True, ) -> None: """Migrate the state sync to the latest SQLMesh / SQLGlot version.""" self.migrator.migrate( self, - default_catalog, skip_backup=skip_backup, promoted_snapshots_only=promoted_snapshots_only, ) diff --git a/sqlmesh/core/state_sync/db/migrator.py b/sqlmesh/core/state_sync/db/migrator.py index 616bd8659f..b803a5cc40 100644 --- a/sqlmesh/core/state_sync/db/migrator.py +++ b/sqlmesh/core/state_sync/db/migrator.py @@ -27,6 +27,8 @@ ) from sqlmesh.core.state_sync.base import ( MIGRATIONS, + MIN_SCHEMA_VERSION, + MIN_SQLMESH_VERSION, ) from sqlmesh.core.state_sync.base import StateSync from sqlmesh.core.state_sync.db.environment import EnvironmentState @@ -41,7 +43,7 @@ from sqlmesh.utils import major_minor from sqlmesh.utils.dag import DAG from sqlmesh.utils.date import now_timestamp -from sqlmesh.utils.errors import SQLMeshError +from sqlmesh.utils.errors import SQLMeshError, StateMigrationError logger = logging.getLogger(__name__) @@ -61,7 +63,6 @@ def __init__( snapshot_state: SnapshotState, environment_state: EnvironmentState, interval_state: IntervalState, - plan_dags_table: TableName, console: t.Optional[Console] = None, ): self.engine_adapter = engine_adapter @@ -70,7 +71,6 @@ def __init__( self.snapshot_state = snapshot_state self.environment_state = environment_state self.interval_state = interval_state - self.plan_dags_table = plan_dags_table self._state_tables = [ self.snapshot_state.snapshots_table, @@ -79,7 +79,6 @@ def __init__( ] self._optional_state_tables = [ self.interval_state.intervals_table, - self.plan_dags_table, self.snapshot_state.auto_restatements_table, self.environment_state.environment_statements_table, ] @@ -87,7 +86,6 @@ def __init__( def migrate( self, state_sync: StateSync, - default_catalog: t.Optional[str], skip_backup: bool = False, promoted_snapshots_only: bool = True, ) -> None: @@ -96,15 +94,13 @@ def migrate( migration_start_ts = time.perf_counter() try: - migrate_rows = self._apply_migrations(state_sync, default_catalog, skip_backup) + migrate_rows = self._apply_migrations(state_sync, skip_backup) if not migrate_rows and major_minor(SQLMESH_VERSION) == versions.minor_sqlmesh_version: return if migrate_rows: self._migrate_rows(promoted_snapshots_only) - # Cleanup plan DAGs since we currently don't migrate snapshot records that are in there. - self.engine_adapter.delete_from(self.plan_dags_table, "TRUE") self.version_state.update_versions() analytics.collector.on_migration_end( @@ -126,6 +122,8 @@ def migrate( ) self.console.log_migration_status(success=False) + if isinstance(e, StateMigrationError): + raise raise SQLMeshError("SQLMesh migration failed.") from e self.console.log_migration_status() @@ -156,11 +154,20 @@ def rollback(self) -> None: def _apply_migrations( self, state_sync: StateSync, - default_catalog: t.Optional[str], skip_backup: bool, ) -> bool: versions = self.version_state.get_versions() - migrations = MIGRATIONS[versions.schema_version :] + first_script_index = 0 + if versions.schema_version and versions.schema_version < MIN_SCHEMA_VERSION: + raise StateMigrationError( + "The current state belongs to an old version of SQLMesh that is no longer supported. " + f"Please upgrade to {MIN_SQLMESH_VERSION} first before upgrading to {SQLMESH_VERSION}." + ) + elif versions.schema_version > 0: + # -1 to skip the baseline migration script + first_script_index = versions.schema_version - (MIN_SCHEMA_VERSION - 1) + + migrations = MIGRATIONS[first_script_index:] should_backup = any( [ migrations, @@ -177,10 +184,10 @@ def _apply_migrations( for migration in migrations: logger.info(f"Applying migration {migration}") - migration.migrate_schemas(state_sync, default_catalog=default_catalog) + migration.migrate_schemas(state_sync) if state_table_exist: # No need to run DML for the initial migration since all tables are empty - migration.migrate_rows(state_sync, default_catalog=default_catalog) + migration.migrate_rows(state_sync) snapshot_count_after = self.snapshot_state.count() diff --git a/sqlmesh/migrations/v0056_restore_table_indexes.py b/sqlmesh/migrations/v0000_baseline.py similarity index 52% rename from sqlmesh/migrations/v0056_restore_table_indexes.py rename to sqlmesh/migrations/v0000_baseline.py index b460c1ebf7..4891900a76 100644 --- a/sqlmesh/migrations/v0056_restore_table_indexes.py +++ b/sqlmesh/migrations/v0000_baseline.py @@ -1,31 +1,27 @@ -"""Readds indexes and primary keys in case tables were restored from a backup.""" +"""The baseline migration script that sets up the initial state tables.""" from sqlglot import exp -from sqlmesh.utils import random_id -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type +from sqlmesh.utils.migration import blob_text_type, index_text_type def migrate_schemas(state_sync, **kwargs): # type: ignore schema = state_sync.schema engine_adapter = state_sync.engine_adapter - if not engine_adapter.SUPPORTS_INDEXES: - return intervals_table = "_intervals" snapshots_table = "_snapshots" environments_table = "_environments" + versions_table = "_versions" if state_sync.schema: + engine_adapter.create_schema(schema) intervals_table = f"{schema}.{intervals_table}" snapshots_table = f"{schema}.{snapshots_table}" environments_table = f"{schema}.{environments_table}" - - table_suffix = random_id(short=True) + versions_table = f"{schema}.{versions_table}" index_type = index_text_type(engine_adapter.dialect) blob_type = blob_text_type(engine_adapter.dialect) - new_snapshots_table = f"{snapshots_table}__{table_suffix}" snapshots_columns_to_types = { "name": exp.DataType.build(index_type), "identifier": exp.DataType.build(index_type), @@ -38,7 +34,6 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore "unrestorable": exp.DataType.build("boolean"), } - new_environments_table = f"{environments_table}__{table_suffix}" environments_columns_to_types = { "name": exp.DataType.build(index_type), "snapshots": exp.DataType.build(blob_type), @@ -53,9 +48,9 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore "catalog_name_override": exp.DataType.build("text"), "previous_finalized_snapshots": exp.DataType.build(blob_type), "normalize_name": exp.DataType.build("boolean"), + "requirements": exp.DataType.build(blob_type), } - new_intervals_table = f"{intervals_table}__{table_suffix}" intervals_columns_to_types = { "id": exp.DataType.build(index_type), "created_ts": exp.DataType.build("bigint"), @@ -69,53 +64,34 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore "is_compacted": exp.DataType.build("boolean"), } - # Recreate the snapshots table and its indexes. - engine_adapter.create_table( - new_snapshots_table, snapshots_columns_to_types, primary_key=("name", "identifier") - ) - engine_adapter.create_index( - new_snapshots_table, "_snapshots_name_version_idx", ("name", "version") - ) - engine_adapter.insert_append( - new_snapshots_table, - exp.select("*").from_(snapshots_table), - target_columns_to_types=snapshots_columns_to_types, - ) + versions_columns_to_types = { + "schema_version": exp.DataType.build("int"), + "sqlglot_version": exp.DataType.build(index_type), + "sqlmesh_version": exp.DataType.build(index_type), + } - # Recreate the environments table and its indexes. - engine_adapter.create_table( - new_environments_table, environments_columns_to_types, primary_key=("name",) - ) - engine_adapter.insert_append( - new_environments_table, - exp.select("*").from_(environments_table), - target_columns_to_types=environments_columns_to_types, + # Create the versions table. + engine_adapter.create_state_table(versions_table, versions_columns_to_types) + + # Create the snapshots table and its indexes. + engine_adapter.create_state_table( + snapshots_table, snapshots_columns_to_types, primary_key=("name", "identifier") ) + engine_adapter.create_index(snapshots_table, "_snapshots_name_version_idx", ("name", "version")) - # Recreate the intervals table and its indexes. - engine_adapter.create_table( - new_intervals_table, intervals_columns_to_types, primary_key=("id",) + # Create the environments table and its indexes. + engine_adapter.create_state_table( + environments_table, environments_columns_to_types, primary_key=("name",) ) - engine_adapter.create_index( - new_intervals_table, "_intervals_name_identifier_idx", ("name", "identifier") + + # Create the intervals table and its indexes. + engine_adapter.create_state_table( + intervals_table, intervals_columns_to_types, primary_key=("id",) ) engine_adapter.create_index( - new_intervals_table, "_intervals_name_version_idx", ("name", "version") + intervals_table, "_intervals_name_identifier_idx", ("name", "identifier") ) - engine_adapter.insert_append( - new_intervals_table, - exp.select("*").from_(intervals_table), - target_columns_to_types=intervals_columns_to_types, - ) - - # Drop old tables. - for table in (snapshots_table, environments_table, intervals_table): - engine_adapter.drop_table(table) - - # Replace old tables with new ones. - engine_adapter.rename_table(new_snapshots_table, snapshots_table) - engine_adapter.rename_table(new_environments_table, environments_table) - engine_adapter.rename_table(new_intervals_table, intervals_table) + engine_adapter.create_index(intervals_table, "_intervals_name_version_idx", ("name", "version")) def migrate_rows(state_sync, **kwargs): # type: ignore diff --git a/sqlmesh/migrations/v0001_init.py b/sqlmesh/migrations/v0001_init.py deleted file mode 100644 index 42d623d1d0..0000000000 --- a/sqlmesh/migrations/v0001_init.py +++ /dev/null @@ -1,64 +0,0 @@ -"""All migrations should be named _XXXX.py, they will be executed sequentially. - -If a migration alters the payload of any pydantic models, you should not actually use them because -the running model may not be able to load them. Make sure that these migration files are standalone. -""" - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - environments_table = "_environments" - versions_table = "_versions" - - if schema: - engine_adapter.create_schema(schema) - snapshots_table = f"{schema}.{snapshots_table}" - environments_table = f"{schema}.{environments_table}" - versions_table = f"{schema}.{versions_table}" - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.create_state_table( - snapshots_table, - { - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - }, - primary_key=("name", "identifier"), - ) - - engine_adapter.create_index(snapshots_table, "name_version_idx", ("name", "version")) - - engine_adapter.create_state_table( - environments_table, - { - "name": exp.DataType.build(index_type), - "snapshots": exp.DataType.build("text"), - "start_at": exp.DataType.build("text"), - "end_at": exp.DataType.build("text"), - "plan_id": exp.DataType.build("text"), - "previous_plan_id": exp.DataType.build("text"), - "expiration_ts": exp.DataType.build("bigint"), - }, - primary_key=("name",), - ) - - engine_adapter.create_state_table( - versions_table, - { - "schema_version": exp.DataType.build("int"), - "sqlglot_version": exp.DataType.build("text"), - }, - ) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0002_remove_identify.py b/sqlmesh/migrations/v0002_remove_identify.py deleted file mode 100644 index d8f9a1c0cd..0000000000 --- a/sqlmesh/migrations/v0002_remove_identify.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Remove identify=True kwarg for rendering sql""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0003_move_batch_size.py b/sqlmesh/migrations/v0003_move_batch_size.py deleted file mode 100644 index e8efff6162..0000000000 --- a/sqlmesh/migrations/v0003_move_batch_size.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Move batch_size from the model and into the kind.""" - -import json - -from sqlglot import exp - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - snapshots_table = "_snapshots" - if state_sync.schema: - snapshots_table = f"{state_sync.schema}.{snapshots_table}" - - for row in state_sync.engine_adapter.fetchall( - exp.select("*").from_(snapshots_table), quote_identifiers=True - ): - name, identifier, _, snapshot = row - snapshot = json.loads(snapshot) - model = snapshot["model"] - if "batch_size" in model: - batch_size = model.pop("batch_size") - kind = model.get("kind") - - if kind: - if kind["name"] in ("INCREMENTAL_BY_TIME_RANGE", "INCREMENTAL_BY_UNIQUE_KEY"): - kind["batch_size"] = batch_size - - # this is not efficient, i'm doing this because i'm lazy and no one has snapshots at the time of writing this migration - # do not copy this code in future migrations - - state_sync.engine_adapter.update_table( - snapshots_table, - {"snapshot": json.dumps(snapshot)}, - where=f"name = '{name}' and identifier = '{identifier}'", - ) diff --git a/sqlmesh/migrations/v0004_environmnent_add_finalized_at.py b/sqlmesh/migrations/v0004_environmnent_add_finalized_at.py deleted file mode 100644 index bddbef5971..0000000000 --- a/sqlmesh/migrations/v0004_environmnent_add_finalized_at.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Add support for environment finalization.""" - -from sqlglot import exp - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - environments_table = "_environments" - if state_sync.schema: - environments_table = f"{state_sync.schema}.{environments_table}" - - alter_table_exp = exp.Alter( - this=exp.to_table(environments_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("finalized_ts"), - kind=exp.DataType.build("bigint"), - ) - ], - ) - - engine_adapter.execute(alter_table_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0005_create_seed_table.py b/sqlmesh/migrations/v0005_create_seed_table.py deleted file mode 100644 index 803a47f724..0000000000 --- a/sqlmesh/migrations/v0005_create_seed_table.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Create a dedicated table to store the content of seeds.""" - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - seeds_table = "_seeds" - if state_sync.schema: - seeds_table = f"{state_sync.schema}.{seeds_table}" - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.create_state_table( - seeds_table, - { - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "content": exp.DataType.build("text"), - }, - primary_key=("name", "identifier"), - ) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0006_change_seed_hash.py b/sqlmesh/migrations/v0006_change_seed_hash.py deleted file mode 100644 index c9f771a912..0000000000 --- a/sqlmesh/migrations/v0006_change_seed_hash.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Seed hashes moved from to_string to to_json for performance.""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0007_env_table_info_to_kind.py b/sqlmesh/migrations/v0007_env_table_info_to_kind.py deleted file mode 100644 index 52d483b3cb..0000000000 --- a/sqlmesh/migrations/v0007_env_table_info_to_kind.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Change environments because snapshot table info now stores model kind name.""" - -import json -import zlib - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def _hash(data): # type: ignore - return str(zlib.crc32(";".join("" if d is None else d for d in data).encode("utf-8"))) - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - environments_table = "_environments" - snapshots_table = "_snapshots" - if schema: - environments_table = f"{schema}.{environments_table}" - snapshots_table = f"{schema}.{snapshots_table}" - snapshots_to_kind = {} - - for name, identifier, snapshot in engine_adapter.fetchall( - exp.select("name", "identifier", "snapshot").from_(snapshots_table), - quote_identifiers=True, - ): - snapshot = json.loads(snapshot) - snapshots_to_kind[(name, identifier)] = snapshot["model"]["kind"]["name"] - - environments = engine_adapter.fetchall( - exp.select("*").from_(environments_table), quote_identifiers=True - ) - new_environments = [] - - for ( - name, - snapshots, - start_at, - end_at, - plan_id, - previous_plan_id, - expiration_ts, - finalized_ts, - ) in environments: - new_snapshots = [] - - for snapshot in json.loads(snapshots): - snapshot.pop("is_materialized", None) - snapshot.pop("is_embedded_kind", None) - - fingerprint = snapshot["fingerprint"] - identifier = _hash( - [ - fingerprint["data_hash"], - fingerprint["metadata_hash"], - fingerprint["parent_data_hash"], - fingerprint["parent_metadata_hash"], - ] - ) - - snapshot["kind_name"] = snapshots_to_kind.get((snapshot["name"], identifier), "VIEW") - new_snapshots.append(snapshot) - - new_environments.append( - { - "name": name, - "snapshots": json.dumps(new_snapshots), - "start_at": start_at, - "end_at": end_at, - "plan_id": plan_id, - "previous_plan_id": previous_plan_id, - "expiration_ts": expiration_ts, - "finalized_ts": finalized_ts, - } - ) - - if new_environments: - engine_adapter.delete_from(environments_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - environments_table, - pd.DataFrame(new_environments), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "snapshots": exp.DataType.build("text"), - "start_at": exp.DataType.build("text"), - "end_at": exp.DataType.build("text"), - "plan_id": exp.DataType.build("text"), - "previous_plan_id": exp.DataType.build("text"), - "expiration_ts": exp.DataType.build("bigint"), - "finalized_ts": exp.DataType.build("bigint"), - }, - ) diff --git a/sqlmesh/migrations/v0008_create_intervals_table.py b/sqlmesh/migrations/v0008_create_intervals_table.py deleted file mode 100644 index 7ba8888608..0000000000 --- a/sqlmesh/migrations/v0008_create_intervals_table.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Create a dedicated table to store snapshot intervals.""" - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - intervals_table = "_intervals" - if state_sync.schema: - intervals_table = f"{state_sync.schema}.{intervals_table}" - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.create_state_table( - intervals_table, - { - "id": exp.DataType.build(index_type), - "created_ts": exp.DataType.build("bigint"), - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "start_ts": exp.DataType.build("bigint"), - "end_ts": exp.DataType.build("bigint"), - "is_dev": exp.DataType.build("boolean"), - "is_removed": exp.DataType.build("boolean"), - "is_compacted": exp.DataType.build("boolean"), - }, - primary_key=("id",), - ) - - engine_adapter.create_index( - intervals_table, "name_version_idx", ("name", "version", "created_ts") - ) - engine_adapter.create_index( - intervals_table, "name_identifier_idx", ("name", "identifier", "created_ts") - ) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0009_remove_pre_post_hooks.py b/sqlmesh/migrations/v0009_remove_pre_post_hooks.py deleted file mode 100644 index 534f366d69..0000000000 --- a/sqlmesh/migrations/v0009_remove_pre_post_hooks.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Remove pre- / post- hooks from existing snapshots.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshopt in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot").from_(snapshots_table), - quote_identifiers=True, - ): - snapshot = json.loads(snapshopt) - pre_hooks = snapshot["model"].pop("pre", []) - post_hooks = snapshot["model"].pop("post", []) - - expressions = snapshot["model"].pop("expressions", None) - if expressions and snapshot["model"]["source_type"] == "sql": - snapshot["model"]["pre_statements"] = expressions - - if pre_hooks or post_hooks: - print( - "WARNING: Hooks are no longer supported by SQLMesh, use pre and post SQL statements instead. " - f"Removing 'pre' and 'post' attributes from snapshot name='{name}', identifier='{identifier}'" - ) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(snapshot), - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - }, - ) diff --git a/sqlmesh/migrations/v0010_seed_hash_batch_size.py b/sqlmesh/migrations/v0010_seed_hash_batch_size.py deleted file mode 100644 index 20186e0068..0000000000 --- a/sqlmesh/migrations/v0010_seed_hash_batch_size.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Seed metadata hashes now correctly include the batch_size.""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0011_add_model_kind_name.py b/sqlmesh/migrations/v0011_add_model_kind_name.py deleted file mode 100644 index 3d76d61597..0000000000 --- a/sqlmesh/migrations/v0011_add_model_kind_name.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Add the kind_name column to the snapshots table.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - index_type = index_text_type(engine_adapter.dialect) - - alter_table_exp = exp.Alter( - this=exp.to_table(snapshots_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("kind_name"), - kind=exp.DataType.build(index_type), - ) - ], - ) - engine_adapter.execute(alter_table_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - index_type = index_text_type(engine_adapter.dialect) - - new_snapshots = [] - - for name, identifier, version, snapshot in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": snapshot, - "kind_name": parsed_snapshot["model"]["kind"]["name"], - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0012_update_jinja_expressions.py b/sqlmesh/migrations/v0012_update_jinja_expressions.py deleted file mode 100644 index 99897fa59d..0000000000 --- a/sqlmesh/migrations/v0012_update_jinja_expressions.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Fix expressions that contain jinja.""" - -import json -import typing as t - -from sqlglot import exp - -from sqlmesh.utils.jinja import has_jinja -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - audits = parsed_snapshot.get("audits", []) - model = parsed_snapshot["model"] - - if "query" in model and has_jinja(model["query"]): - model["query"] = _wrap_query(model["query"]) - - _wrap_statements(model, "pre_statements") - _wrap_statements(model, "post_statements") - - for audit in audits: - if has_jinja(audit["query"]): - audit["query"] = _wrap_query(audit["query"]) - _wrap_statements(audit, "expressions") - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) - - -def _wrap_statements(obj: t.Dict, key: str) -> None: - updated_statements = [] - for statement in obj.get(key, []): - if has_jinja(statement): - statement = _wrap_statement(statement) - updated_statements.append(statement) - - if updated_statements: - obj[key] = updated_statements - - -def _wrap_query(sql: str) -> str: - return f"JINJA_QUERY_BEGIN;\n{sql}\nJINJA_END;" - - -def _wrap_statement(sql: str) -> str: - return f"JINJA_STATEMENT_BEGIN;\n{sql}\nJINJA_END;" diff --git a/sqlmesh/migrations/v0013_serde_using_model_dialects.py b/sqlmesh/migrations/v0013_serde_using_model_dialects.py deleted file mode 100644 index 5d865930e7..0000000000 --- a/sqlmesh/migrations/v0013_serde_using_model_dialects.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Serialize SQL using the dialect of each model.""" - -import json -import typing as t - -from sqlglot import exp, parse_one - -from sqlmesh.utils.jinja import has_jinja -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - model = parsed_snapshot["model"] - dialect = model["dialect"] - - _update_expression(model, "query", dialect) - _update_expression_list(model, "pre_statements", dialect) - _update_expression_list(model, "post_statements", dialect) - - for audit in parsed_snapshot.get("audits", []): - dialect = audit["dialect"] - _update_expression(audit, "query", dialect) - _update_expression_list(audit, "expressions", dialect) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) - - -# Note: previously we used to do serde using the SQLGlot dialect, so we need to parse the -# stored queries using that dialect and then write them back using the correct dialect. - - -def _update_expression(obj: t.Dict, key: str, dialect: str) -> None: - if key in obj and not has_jinja(obj[key]): - obj[key] = parse_one(obj[key]).sql(dialect=dialect) - - -def _update_expression_list(obj: t.Dict, key: str, dialect: str) -> None: - if key in obj: - obj[key] = [ - ( - parse_one(expression).sql(dialect=dialect) - if not has_jinja(expression) - else expression - ) - for expression in obj[key] - if expression - ] diff --git a/sqlmesh/migrations/v0014_fix_dev_intervals.py b/sqlmesh/migrations/v0014_fix_dev_intervals.py deleted file mode 100644 index d5f4d86f9d..0000000000 --- a/sqlmesh/migrations/v0014_fix_dev_intervals.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Fix snapshot intervals that have been erroneously marked as dev.""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - schema = state_sync.schema - intervals_table = "_intervals" - if schema: - intervals_table = f"{schema}.{intervals_table}" - - state_sync.engine_adapter.update_table( - intervals_table, - {"is_dev": False}, - where="1=1", - ) diff --git a/sqlmesh/migrations/v0015_environment_add_promoted_snapshot_ids.py b/sqlmesh/migrations/v0015_environment_add_promoted_snapshot_ids.py deleted file mode 100644 index b1e42e1eb7..0000000000 --- a/sqlmesh/migrations/v0015_environment_add_promoted_snapshot_ids.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Include a set of snapshot IDs filtered for promotion.""" - -from sqlglot import exp -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - environments_table = "_environments" - if state_sync.schema: - environments_table = f"{state_sync.schema}.{environments_table}" - - blob_type = blob_text_type(engine_adapter.dialect) - - alter_table_exp = exp.Alter( - this=exp.to_table(environments_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("promoted_snapshot_ids"), - kind=exp.DataType.build(blob_type), - ) - ], - ) - - engine_adapter.execute(alter_table_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0016_fix_windows_path.py b/sqlmesh/migrations/v0016_fix_windows_path.py deleted file mode 100644 index 3570cc368e..0000000000 --- a/sqlmesh/migrations/v0016_fix_windows_path.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Fix paths that have a Windows forward slash in them.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - model = parsed_snapshot["model"] - python_env = model.get("python_env") - if python_env: - for py_definition in python_env.values(): - path = py_definition.get("path") - if path: - py_definition["path"] = path.replace("\\", "/") - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0017_fix_windows_seed_path.py b/sqlmesh/migrations/v0017_fix_windows_seed_path.py deleted file mode 100644 index 57bdd3609d..0000000000 --- a/sqlmesh/migrations/v0017_fix_windows_seed_path.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Fix seed paths that have a Windows forward slash in them.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - model_kind = parsed_snapshot["model"]["kind"] - if "path" in model_kind: - model_kind["path"] = model_kind["path"].replace("\\", "/") - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py b/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py deleted file mode 100644 index e17eeded61..0000000000 --- a/sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Replace snapshot model field with node.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - parsed_snapshot["node"] = parsed_snapshot.pop("model") - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0019_add_env_suffix_target.py b/sqlmesh/migrations/v0019_add_env_suffix_target.py deleted file mode 100644 index 88227c8fdd..0000000000 --- a/sqlmesh/migrations/v0019_add_env_suffix_target.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Add support for environment suffix target.""" - -from sqlglot import exp - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - environments_table = "_environments" - if state_sync.schema: - environments_table = f"{state_sync.schema}.{environments_table}" - - alter_table_exp = exp.Alter( - this=exp.to_table(environments_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("suffix_target"), - kind=exp.DataType.build("text"), - ) - ], - ) - engine_adapter.execute(alter_table_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - environments_table = "_environments" - if state_sync.schema: - environments_table = f"{state_sync.schema}.{environments_table}" - - state_sync.engine_adapter.update_table( - environments_table, - {"suffix_target": "schema"}, - where="1=1", - ) diff --git a/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py b/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py deleted file mode 100644 index 788974ccee..0000000000 --- a/sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Remove redundant attributes from dbt models.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - jinja_macros_global_objs = parsed_snapshot["node"]["jinja_macros"]["global_objs"] - if "config" in jinja_macros_global_objs and isinstance( - jinja_macros_global_objs["config"], dict - ): - for key in CONFIG_ATTRIBUTE_KEYS_TO_REMOVE: - jinja_macros_global_objs["config"].pop(key, None) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) - - -CONFIG_ATTRIBUTE_KEYS_TO_REMOVE = [ - "config", - "config_call_dict", - "depends_on", - "dependencies", - "metrics", - "original_file_path", - "packages", - "patch_path", - "path", - "post-hook", - "pre-hook", - "raw_code", - "refs", - "resource_type", - "sources", - "sql", - "tests", - "unrendered_config", -] diff --git a/sqlmesh/migrations/v0021_fix_table_properties.py b/sqlmesh/migrations/v0021_fix_table_properties.py deleted file mode 100644 index c878cedb8b..0000000000 --- a/sqlmesh/migrations/v0021_fix_table_properties.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Fix table properties that have extra quoting due to a bug.""" - -import json - -from sqlglot import exp - -from sqlmesh.core import dialect as d -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - found_table_properties = False - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - table_properties = parsed_snapshot["node"].get("table_properties") - if table_properties: - found_table_properties = True - dialect = parsed_snapshot["node"].get("dialect") - parsed_snapshot["node"]["table_properties"] = exp.Tuple( - expressions=[ - exp.Literal.string(k).eq(d.parse_one(v)) for k, v in table_properties.items() - ] - ).sql(dialect=dialect) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if found_table_properties: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0022_move_project_to_model.py b/sqlmesh/migrations/v0022_move_project_to_model.py deleted file mode 100644 index 5a4eaa77f0..0000000000 --- a/sqlmesh/migrations/v0022_move_project_to_model.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Move project attr from snapshot to model.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - - parsed_snapshot["node"]["project"] = parsed_snapshot.pop("project", "") - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - if new_snapshots: - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0023_fix_added_models_with_forward_only_parents.py b/sqlmesh/migrations/v0023_fix_added_models_with_forward_only_parents.py deleted file mode 100644 index 2fa490b0ce..0000000000 --- a/sqlmesh/migrations/v0023_fix_added_models_with_forward_only_parents.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Fix snapshots of added models with forward only parents.""" - -import json -import typing as t - -from sqlglot import exp - -from sqlmesh.utils.dag import DAG - - -def migrate_schemas(state_sync: t.Any, **kwargs) -> None: # type: ignore - pass - - -def migrate_rows(state_sync: t.Any, **kwargs) -> None: # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - environments_table = "_environments" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - environments_table = f"{schema}.{environments_table}" - - dag: DAG[t.Tuple[str, str]] = DAG() - snapshot_mapping: t.Dict[t.Tuple[str, str], t.Dict[str, t.Any]] = {} - - for identifier, snapshot in engine_adapter.fetchall( - exp.select("identifier", "snapshot").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - - snapshot_id = (parsed_snapshot["name"], identifier) - snapshot_mapping[snapshot_id] = parsed_snapshot - - parent_ids = [ - (parent["name"], parent["identifier"]) for parent in parsed_snapshot["parents"] - ] - dag.add(snapshot_id, parent_ids) - - snapshots_to_delete = set() - - for snapshot_id in dag: - if snapshot_id not in snapshot_mapping: - continue - parsed_snapshot = snapshot_mapping[snapshot_id] - is_breaking = parsed_snapshot.get("change_category") == 1 - has_previous_versions = bool(parsed_snapshot.get("previous_versions", [])) - - has_paused_forward_only_parent = False - if is_breaking and not has_previous_versions: - for upstream_id in dag.upstream(snapshot_id): - if upstream_id not in snapshot_mapping: - continue - upstream_snapshot = snapshot_mapping[upstream_id] - upstream_change_category = upstream_snapshot.get("change_category") - is_forward_only_upstream = upstream_change_category == 3 - if is_forward_only_upstream and not upstream_snapshot.get("unpaused_ts"): - has_paused_forward_only_parent = True - break - - if has_paused_forward_only_parent: - snapshots_to_delete.add(snapshot_id) - - if snapshots_to_delete: - where = t.cast(exp.Tuple, exp.convert((exp.column("name"), exp.column("identifier")))).isin( - *snapshots_to_delete - ) - engine_adapter.delete_from(snapshots_table, where) diff --git a/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py b/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py deleted file mode 100644 index 81a9f79dde..0000000000 --- a/sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Replace snapshot model_kind_name enum with value.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - corrected_kind_name = None - parsed_snapshot = json.loads(snapshot) - if "kind" in parsed_snapshot["node"]: - corrected_kind_name = parsed_snapshot["node"]["kind"].get("name") - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": snapshot, - "kind_name": corrected_kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py b/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py deleted file mode 100644 index 08c03c6a87..0000000000 --- a/sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Normalize intervals and fix missing change category.""" - -import json -import zlib - -from sqlglot import exp - -from sqlmesh.utils import random_id -from sqlmesh.utils.date import now_timestamp -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - intervals_table = "_intervals" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - intervals_table = f"{schema}.{intervals_table}" - - migration_required = False - new_snapshots = [] - new_intervals = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - - if not parsed_snapshot.get("change_category"): - fingerprint = parsed_snapshot.get("fingerprint") - version = _hash( - [ - fingerprint["data_hash"], - fingerprint["parent_data_hash"], - ] - ) - parsed_snapshot["change_category"] = ( - 4 if version == parsed_snapshot.get("version") else 5 - ) - migration_required = True - - def _add_interval(start_ts: int, end_ts: int, is_dev: bool) -> None: - new_intervals.append( - { - "id": random_id(), - "created_ts": now_timestamp(), - "name": name, - "identifier": identifier, - "version": version, - "start_ts": start_ts, - "end_ts": end_ts, - "is_dev": is_dev, - "is_removed": False, - "is_compacted": True, - } - ) - - for interval in parsed_snapshot.pop("intervals", []): - _add_interval(interval[0], interval[1], False) - migration_required = True - - for interval in parsed_snapshot.pop("dev_intervals", []): - _add_interval(interval[0], interval[1], True) - migration_required = True - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if migration_required: - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.delete_from(snapshots_table, "TRUE") - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) - - if new_intervals: - engine_adapter.insert_append( - intervals_table, - pd.DataFrame(new_intervals), - target_columns_to_types={ - "id": exp.DataType.build(index_type), - "created_ts": exp.DataType.build("bigint"), - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "start_ts": exp.DataType.build("bigint"), - "end_ts": exp.DataType.build("bigint"), - "is_dev": exp.DataType.build("boolean"), - "is_removed": exp.DataType.build("boolean"), - "is_compacted": exp.DataType.build("boolean"), - }, - ) - - -def _hash(data): # type: ignore - return str(zlib.crc32(";".join("" if d is None else d for d in data).encode("utf-8"))) diff --git a/sqlmesh/migrations/v0026_remove_dialect_from_seed.py b/sqlmesh/migrations/v0026_remove_dialect_from_seed.py deleted file mode 100644 index 10d77b430b..0000000000 --- a/sqlmesh/migrations/v0026_remove_dialect_from_seed.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Remove dialect from seeds.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - node = parsed_snapshot["node"] - if "seed" in node: - node["seed"].pop("dialect", None) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0027_minute_interval_to_five.py b/sqlmesh/migrations/v0027_minute_interval_to_five.py deleted file mode 100644 index 8878536b6f..0000000000 --- a/sqlmesh/migrations/v0027_minute_interval_to_five.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Change any interval unit of minute to five_minute.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - - node = parsed_snapshot["node"] - - if node.get("interval_unit") == "minute": - node["interval_unit"] = "five_minute" - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0028_add_plan_dags_table.py b/sqlmesh/migrations/v0028_add_plan_dags_table.py deleted file mode 100644 index b03fa45bba..0000000000 --- a/sqlmesh/migrations/v0028_add_plan_dags_table.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Creates the '_plan_dags' table if Airflow is used.""" - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - plan_dags_table = "_plan_dags" - - if schema: - engine_adapter.create_schema(schema) - plan_dags_table = f"{schema}.{plan_dags_table}" - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.create_state_table( - plan_dags_table, - { - "request_id": exp.DataType.build(index_type), - "dag_id": exp.DataType.build(index_type), - "dag_spec": exp.DataType.build("text"), - }, - primary_key=("request_id",), - ) - - engine_adapter.create_index(plan_dags_table, "dag_id_idx", ("dag_id",)) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py b/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py deleted file mode 100644 index a8b2800fe0..0000000000 --- a/sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Generate mapping schema data types using the corresponding model's dialect.""" - -import json - -from sqlglot import exp, parse_one - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - node = parsed_snapshot["node"] - - mapping_schema = node.get("mapping_schema") - if mapping_schema: - node["mapping_schema"] = _convert_schema_types(mapping_schema, node["dialect"]) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) - - -def _convert_schema_types(schema, dialect): # type: ignore - if not schema: - return schema - - for k, v in schema.items(): - if isinstance(v, dict): - _convert_schema_types(v, dialect) - else: - schema[k] = parse_one(v).sql(dialect=dialect) - - return schema diff --git a/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py b/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py deleted file mode 100644 index 5f2d7f1dbf..0000000000 --- a/sqlmesh/migrations/v0030_update_unrestorable_snapshots.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Update unrestorable snapshots.""" - -import json -import typing as t -from collections import defaultdict - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync: t.Any, **kwargs: t.Any) -> None: # type: ignore - pass - - -def migrate_rows(state_sync: t.Any, **kwargs: t.Any) -> None: # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - snapshots_by_version = defaultdict(list) - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - snapshots_by_version[(name, version)].append((identifier, kind_name, parsed_snapshot)) - - for (name, version), snapshots in snapshots_by_version.items(): - has_forward_only = any(s["change_category"] == 3 for _, _, s in snapshots) - for identifier, kind_name, snapshot in snapshots: - if ( - has_forward_only - and snapshot["change_category"] != 3 - and not snapshot.get("unpaused_ts") - ): - snapshot["unrestorable"] = True - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(snapshot), - "kind_name": kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0031_remove_dbt_target_fields.py b/sqlmesh/migrations/v0031_remove_dbt_target_fields.py deleted file mode 100644 index e99aaa7fa4..0000000000 --- a/sqlmesh/migrations/v0031_remove_dbt_target_fields.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Remove dbt target fields from snapshots outside of limited list of approved fields""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - found_dbt_target = False - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - node = parsed_snapshot["node"] - dbt_target = node.get("jinja_macros", {}).get("global_objs", {}).get("target", {}) - # Double check that `target_name` exists as a field since we know that all dbt targets have `target_name` - # We do this in case someone has a target macro defined that is not related to dbt - if dbt_target and dbt_target.get("target_name"): - found_dbt_target = True - node["jinja_macros"]["global_objs"]["target"] = { - "type": dbt_target.get("type", "None"), - "name": dbt_target.get("name", "None"), - "schema": dbt_target.get("schema", "None"), - "database": dbt_target.get("database", "None"), - "target_name": dbt_target["target_name"], - } - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if found_dbt_target: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0032_add_sqlmesh_version.py b/sqlmesh/migrations/v0032_add_sqlmesh_version.py deleted file mode 100644 index 032709f889..0000000000 --- a/sqlmesh/migrations/v0032_add_sqlmesh_version.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Add new 'sqlmesh_version' column to the version state table.""" - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - versions_table = "_versions" - if state_sync.schema: - versions_table = f"{state_sync.schema}.{versions_table}" - index_type = index_text_type(engine_adapter.dialect) - alter_table_exp = exp.Alter( - this=exp.to_table(versions_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("sqlmesh_version"), - kind=exp.DataType.build(index_type), - ) - ], - ) - - engine_adapter.execute(alter_table_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0033_mysql_fix_blob_text_type.py b/sqlmesh/migrations/v0033_mysql_fix_blob_text_type.py deleted file mode 100644 index 5b3d0f2347..0000000000 --- a/sqlmesh/migrations/v0033_mysql_fix_blob_text_type.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Use LONGTEXT type for blob fields in MySQL.""" - -from sqlglot import exp - -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - if engine_adapter.dialect != "mysql": - return - - schema = state_sync.schema - environments_table = "_environments" - snapshots_table = "_snapshots" - seeds_table = "_seeds" - plan_dags_table = "_plan_dags" - - if schema: - environments_table = f"{schema}.{environments_table}" - snapshots_table = f"{schema}.{snapshots_table}" - seeds_table = f"{state_sync.schema}.{seeds_table}" - plan_dags_table = f"{schema}.{plan_dags_table}" - - targets = [ - (environments_table, "snapshots"), - (snapshots_table, "snapshot"), - (seeds_table, "content"), - (plan_dags_table, "dag_spec"), - ] - - for table_name, column_name in targets: - blob_type = blob_text_type(engine_adapter.dialect) - alter_table_exp = exp.Alter( - this=exp.to_table(table_name), - kind="TABLE", - actions=[ - exp.AlterColumn( - this=exp.to_column(column_name), - dtype=exp.DataType.build(blob_type), - ) - ], - ) - - engine_adapter.execute(alter_table_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0034_add_default_catalog.py b/sqlmesh/migrations/v0034_add_default_catalog.py deleted file mode 100644 index 15a040364f..0000000000 --- a/sqlmesh/migrations/v0034_add_default_catalog.py +++ /dev/null @@ -1,371 +0,0 @@ -"""Add default catalog to snapshots and update names to match new normalization rules.""" - -from __future__ import annotations - -import json -import typing as t - -from sqlglot import exp -from sqlglot.dialects.dialect import DialectType -from sqlglot.helper import dict_depth, seq_get -from sqlglot.optimizer.normalize_identifiers import normalize_identifiers - -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type - - -def set_default_catalog( - table: exp.Table, - default_catalog: t.Optional[str], -) -> exp.Table: - if default_catalog and not table.catalog and table.db: - table.set("catalog", exp.parse_identifier(default_catalog)) - - return table - - -def normalize_model_name( - table: str | exp.Table, - default_catalog: t.Optional[str], - dialect: DialectType = None, -) -> str: - table = exp.to_table(table, dialect=dialect) - - table = set_default_catalog(table, default_catalog) - return exp.table_name(normalize_identifiers(table, dialect=dialect), identify=True) - - -def normalize_mapping_schema(mapping_schema: t.Dict, dialect: str) -> t.Dict: - # Example input: {'"catalog"': {'schema': {'table': {'column': 'INT'}}}} - # Example output: {'"catalog"': {'"schema"': {'"table"': {'column': 'INT'}}}} - normalized_mapping_schema = {} - for key, value in mapping_schema.items(): - if isinstance(value, dict): - normalized_mapping_schema[normalize_model_name(key, None, dialect)] = ( - normalize_mapping_schema(value, dialect) - ) - else: - normalized_mapping_schema[key] = value - return normalized_mapping_schema - - -def update_dbt_relations( - source: t.Optional[t.Dict], keys: t.List[str], default_catalog: t.Optional[str] -) -> None: - if not default_catalog or not source: - return - for key in keys: - relations = source.get(key) - if relations: - relations = [relations] if "database" in relations else relations.values() - for relation in relations: - if not relation["database"]: - relation["database"] = default_catalog - - -def migrate_schemas(state_sync, default_catalog: t.Optional[str], **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, default_catalog: t.Optional[str], **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - environments_table = "_environments" - intervals_table = "_intervals" - seeds_table = "_seeds" - - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - environments_table = f"{schema}.{environments_table}" - intervals_table = f"{schema}.{intervals_table}" - seeds_table = f"{schema}.{seeds_table}" - - new_snapshots = [] - snapshot_to_dialect = {} - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - # This is here in the case where the user originally had catalog in this model name, and therefore - # we would have before created the table with the catalog in the name. New logic removes the catalog, - # and therefore we need to make sure the table name is the same as the original table name, so we include - # this override - parsed_snapshot["base_table_name_override"] = parsed_snapshot["name"] - node = parsed_snapshot["node"] - dialect = node.get("dialect") - normalized_name = ( - normalize_model_name(name, default_catalog=default_catalog, dialect=dialect) - if node["source_type"] != "audit" - else name - ) - parsed_snapshot["name"] = normalized_name - # At the time of migration all nodes had default catalog, so we don't have to check type - node["default_catalog"] = default_catalog - snapshot_to_dialect[name] = dialect - mapping_schema = node.get("mapping_schema", {}) - if mapping_schema: - normalized_default_catalog = ( - normalize_model_name(default_catalog, default_catalog=None, dialect=dialect) - if default_catalog - else None - ) - mapping_schema_depth = dict_depth(mapping_schema) - if mapping_schema_depth == 3 and normalized_default_catalog: - mapping_schema = {normalized_default_catalog: mapping_schema} - node["mapping_schema"] = normalize_mapping_schema(mapping_schema, dialect) - depends_on = node.get("depends_on", []) - if depends_on: - node["depends_on"] = [ - normalize_model_name(dep, default_catalog, dialect) for dep in depends_on - ] - if parsed_snapshot["parents"]: - parsed_snapshot["parents"] = [ - { - "name": normalize_model_name(parent["name"], default_catalog, dialect), - "identifier": parent["identifier"], - } - for parent in parsed_snapshot["parents"] - ] - if parsed_snapshot["indirect_versions"]: - parsed_snapshot["indirect_versions"] = { - normalize_model_name(name, default_catalog, dialect): snapshot_data_versions - for name, snapshot_data_versions in parsed_snapshot["indirect_versions"].items() - } - # dbt specific migration - jinja_macros = node.get("jinja_macros") - if ( - default_catalog - and jinja_macros - and jinja_macros.get("create_builtins_module") == "sqlmesh.dbt" - ): - update_dbt_relations( - jinja_macros.get("global_objs"), ["refs", "sources", "this"], default_catalog - ) - - new_snapshots.append( - { - "name": normalized_name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build(blob_type), - "kind_name": exp.DataType.build(index_type), - }, - ) - - new_environments = [] - default_dialect = seq_get(list(snapshot_to_dialect.values()), 0) - for ( - name, - snapshots, - start_at, - end_at, - plan_id, - previous_plan_id, - expiration_ts, - finalized_ts, - promoted_snapshot_ids, - suffix_target, - ) in engine_adapter.fetchall( - exp.select( - "name", - "snapshots", - "start_at", - "end_at", - "plan_id", - "previous_plan_id", - "expiration_ts", - "finalized_ts", - "promoted_snapshot_ids", - "suffix_target", - ).from_(environments_table), - quote_identifiers=True, - ): - new_snapshots = [] - for snapshot in json.loads(snapshots): - snapshot_name = snapshot["name"] - snapshot["base_table_name_override"] = snapshot_name - dialect = snapshot_to_dialect.get(snapshot_name, default_dialect) - node_type = snapshot.get("node_type") - normalized_name = ( - normalize_model_name(snapshot_name, default_catalog, dialect) - if node_type is None or node_type == "model" - else snapshot_name - ) - snapshot["name"] = normalized_name - if snapshot["parents"]: - snapshot["parents"] = [ - { - "name": normalize_model_name(parent["name"], default_catalog, dialect), - "identifier": parent["identifier"], - } - for parent in snapshot["parents"] - ] - new_snapshots.append(snapshot) - - new_environments.append( - { - "name": name, - "snapshots": json.dumps(new_snapshots), - "start_at": start_at, - "end_at": end_at, - "plan_id": plan_id, - "previous_plan_id": previous_plan_id, - "expiration_ts": expiration_ts, - "finalized_ts": finalized_ts, - "promoted_snapshot_ids": promoted_snapshot_ids, - "suffix_target": suffix_target, - } - ) - - if new_environments: - engine_adapter.delete_from(environments_table, "TRUE") - - engine_adapter.insert_append( - environments_table, - pd.DataFrame(new_environments), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "snapshots": exp.DataType.build(blob_type), - "start_at": exp.DataType.build("text"), - "end_at": exp.DataType.build("text"), - "plan_id": exp.DataType.build("text"), - "previous_plan_id": exp.DataType.build("text"), - "expiration_ts": exp.DataType.build("bigint"), - "finalized_ts": exp.DataType.build("bigint"), - "promoted_snapshot_ids": exp.DataType.build(blob_type), - "suffix_target": exp.DataType.build("text"), - }, - ) - - # We update environment to not be finalized in order to force them to update their views - # in order to make sure the views now have the fully qualified names - # We only do this if a default catalog was applied otherwise the current views are fine - # We do this post creating the new environments in order to avoid having to find a way to - # expression a null timestamp value in pandas that works across all engines - if default_catalog: - engine_adapter.execute( - exp.update(environments_table, {"finalized_ts": None}, where="1=1"), - quote_identifiers=True, - ) - - new_intervals = [] - for ( - id, - created_ts, - name, - identifier, - version, - start_ts, - end_ts, - is_dev, - is_removed, - is_compacted, - ) in engine_adapter.fetchall( - exp.select( - "id", - "created_ts", - "name", - "identifier", - "version", - "start_ts", - "end_ts", - "is_dev", - "is_removed", - "is_compacted", - ).from_(intervals_table), - quote_identifiers=True, - ): - dialect = snapshot_to_dialect.get(name, default_dialect) - normalized_name = normalize_model_name(name, default_catalog, dialect) - new_intervals.append( - { - "id": id, - "created_ts": created_ts, - "name": normalized_name, - "identifier": identifier, - "version": version, - "start_ts": start_ts, - "end_ts": end_ts, - "is_dev": is_dev, - "is_removed": is_removed, - "is_compacted": is_compacted, - } - ) - - if new_intervals: - engine_adapter.delete_from(intervals_table, "TRUE") - - engine_adapter.insert_append( - intervals_table, - pd.DataFrame(new_intervals), - target_columns_to_types={ - "id": exp.DataType.build(index_type), - "created_ts": exp.DataType.build("bigint"), - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "start_ts": exp.DataType.build("bigint"), - "end_ts": exp.DataType.build("bigint"), - "is_dev": exp.DataType.build("boolean"), - "is_removed": exp.DataType.build("boolean"), - "is_compacted": exp.DataType.build("boolean"), - }, - ) - - new_seeds = [] - for ( - name, - identifier, - content, - ) in engine_adapter.fetchall( - exp.select( - "name", - "identifier", - "content", - ).from_(seeds_table), - quote_identifiers=True, - ): - dialect = snapshot_to_dialect.get(name, default_dialect) - normalized_name = normalize_model_name(name, default_catalog, dialect) - new_seeds.append( - { - "name": normalized_name, - "identifier": identifier, - "content": content, - } - ) - - if new_seeds: - engine_adapter.delete_from(seeds_table, "TRUE") - - engine_adapter.insert_append( - seeds_table, - pd.DataFrame(new_seeds), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "content": exp.DataType.build("text"), - }, - ) diff --git a/sqlmesh/migrations/v0035_add_catalog_name_override.py b/sqlmesh/migrations/v0035_add_catalog_name_override.py deleted file mode 100644 index 3e2a42bd60..0000000000 --- a/sqlmesh/migrations/v0035_add_catalog_name_override.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Add support for environment catalog name override.""" - -from sqlglot import exp - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - environments_table = "_environments" - if state_sync.schema: - environments_table = f"{state_sync.schema}.{environments_table}" - - alter_table_exp = exp.Alter( - this=exp.to_table(environments_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("catalog_name_override"), - kind=exp.DataType.build("text"), - ) - ], - ) - engine_adapter.execute(alter_table_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0036_delete_plan_dags_bug_fix.py b/sqlmesh/migrations/v0036_delete_plan_dags_bug_fix.py deleted file mode 100644 index 9cd10ccbe0..0000000000 --- a/sqlmesh/migrations/v0036_delete_plan_dags_bug_fix.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Add missing delete from migration #34.""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - plan_dags_table = "_plan_dags" - if state_sync.schema: - plan_dags_table = f"{schema}.{plan_dags_table}" - - # At the time of migration plan_dags table is only needed for in-flight DAGs and therefore we can safely - # just delete it instead of migrating it - # If reusing this code verify that this is still the case - engine_adapter.delete_from(plan_dags_table, "TRUE") diff --git a/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py b/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py deleted file mode 100644 index 083f8301b4..0000000000 --- a/sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Remove dbt is_incremental macro""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - blob_type = blob_text_type(engine_adapter.dialect) - new_snapshots = [] - found_dbt_package = False - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - node = parsed_snapshot["node"] - dbt_package = node.get("jinja_macros", {}).get("packages", {}).get("dbt", {}) - - if dbt_package: - found_dbt_package = True - dbt_package.pop("is_incremental", None) - dbt_package.pop("should_full_refresh", None) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - } - ) - - if found_dbt_package: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build(blob_type), - "kind_name": exp.DataType.build(index_type), - }, - ) diff --git a/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py b/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py deleted file mode 100644 index 5ddb3a4ee7..0000000000 --- a/sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Add the expiration_ts column to the snapshots table.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.date import to_datetime, to_timestamp -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - alter_table_exp = exp.Alter( - this=exp.to_table(snapshots_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("expiration_ts"), - kind=exp.DataType.build("bigint"), - ) - ], - ) - engine_adapter.execute(alter_table_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - - new_snapshots = [] - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - - updated_ts = parsed_snapshot["updated_ts"] - ttl = parsed_snapshot["ttl"] - expiration_ts = to_timestamp(ttl, relative_base=to_datetime(updated_ts)) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": snapshot, - "kind_name": kind_name, - "expiration_ts": expiration_ts, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build(blob_type), - "kind_name": exp.DataType.build(index_type), - "expiration_ts": exp.DataType.build("bigint"), - }, - ) diff --git a/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py b/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py deleted file mode 100644 index fb1c0b1ec7..0000000000 --- a/sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Include environment in plan dag spec.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - plan_dags_table = "_plan_dags" - if state_sync.schema: - plan_dags_table = f"{schema}.{plan_dags_table}" - - new_specs = [] - - for request_id, dag_id, dag_spec in engine_adapter.fetchall( - exp.select("request_id", "dag_id", "dag_spec").from_(plan_dags_table), - quote_identifiers=True, - ): - parsed_dag_spec = json.loads(dag_spec) - - environment_naming_info = parsed_dag_spec.pop("environment_naming_info") - promoted_snapshots = parsed_dag_spec.pop("promoted_snapshots", []) - start = parsed_dag_spec.pop("start") - parsed_dag_spec.pop("end", None) - plan_id = parsed_dag_spec.pop("plan_id") - previous_plan_id = parsed_dag_spec.pop("previous_plan_id", None) - expiration_ts = parsed_dag_spec.pop("environment_expiration_ts", None) - - parsed_dag_spec["environment"] = { - **environment_naming_info, - "snapshots": promoted_snapshots, - "start_at": start, - "end_at": start, - "plan_id": plan_id, - "previous_plan_id": previous_plan_id, - "expiration_ts": expiration_ts, - } - - new_specs.append( - { - "request_id": request_id, - "dag_id": dag_id, - "dag_spec": json.dumps(parsed_dag_spec), - } - ) - - if new_specs: - engine_adapter.delete_from(plan_dags_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - plan_dags_table, - pd.DataFrame(new_specs), - target_columns_to_types={ - "request_id": exp.DataType.build(index_type), - "dag_id": exp.DataType.build(index_type), - "dag_spec": exp.DataType.build(blob_type), - }, - ) diff --git a/sqlmesh/migrations/v0040_add_previous_finalized_snapshots.py b/sqlmesh/migrations/v0040_add_previous_finalized_snapshots.py deleted file mode 100644 index f15bd69eed..0000000000 --- a/sqlmesh/migrations/v0040_add_previous_finalized_snapshots.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Add support for environment previous finalized snapshots.""" - -from sqlglot import exp - -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - environments_table = "_environments" - if state_sync.schema: - environments_table = f"{state_sync.schema}.{environments_table}" - - blob_type = blob_text_type(engine_adapter.dialect) - - alter_table_exp = exp.Alter( - this=exp.to_table(environments_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("previous_finalized_snapshots"), - kind=exp.DataType.build(blob_type), - ) - ], - ) - engine_adapter.execute(alter_table_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py b/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py deleted file mode 100644 index a99e96b686..0000000000 --- a/sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Remove hash_raw_query from existing snapshots.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name, expiration_ts in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name", "expiration_ts").from_( - snapshots_table - ), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - parsed_snapshot["node"].pop("hash_raw_query", None) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - "expiration_ts": expiration_ts, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build(blob_type), - "kind_name": exp.DataType.build(index_type), - "expiration_ts": exp.DataType.build("bigint"), - }, - ) diff --git a/sqlmesh/migrations/v0042_trim_indirect_versions.py b/sqlmesh/migrations/v0042_trim_indirect_versions.py deleted file mode 100644 index 5a8f6285b4..0000000000 --- a/sqlmesh/migrations/v0042_trim_indirect_versions.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Trim irrelevant attributes from indirect versions.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name, expiration_ts in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name", "expiration_ts").from_( - snapshots_table - ), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - for indirect_versions in parsed_snapshot["indirect_versions"].values(): - for indirect_version in indirect_versions: - # Only keep version and change_category. - version = indirect_version.get("version") - change_category = indirect_version.get("change_category") - indirect_version.clear() - indirect_version["version"] = version - indirect_version["change_category"] = change_category - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - "expiration_ts": expiration_ts, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build(blob_type), - "kind_name": exp.DataType.build(index_type), - "expiration_ts": exp.DataType.build("bigint"), - }, - ) diff --git a/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py b/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py deleted file mode 100644 index 767f4b236b..0000000000 --- a/sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Trim irrelevant attributes from the plan DAGs state.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - plan_dags_table = "_plan_dags" - if schema: - plan_dags_table = f"{schema}.{plan_dags_table}" - - new_dag_specs = [] - - for request_id, dag_id, dag_spec in engine_adapter.fetchall( - exp.select("request_id", "dag_id", "dag_spec").from_(plan_dags_table), - quote_identifiers=True, - ): - parsed_dag_spec = json.loads(dag_spec) - for snapshot in parsed_dag_spec.get("new_snapshots", []): - snapshot["node"].pop("hash_raw_query", None) - - for indirect_versions in snapshot.get("indirect_versions", {}).values(): - for indirect_version in indirect_versions: - # Only keep version and change_category. - version = indirect_version.get("version") - change_category = indirect_version.get("change_category") - indirect_version.clear() - indirect_version["version"] = version - indirect_version["change_category"] = change_category - - new_dag_specs.append( - { - "request_id": request_id, - "dag_id": dag_id, - "dag_spec": json.dumps(parsed_dag_spec), - } - ) - - if new_dag_specs: - engine_adapter.delete_from(plan_dags_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - plan_dags_table, - pd.DataFrame(new_dag_specs), - target_columns_to_types={ - "request_id": exp.DataType.build(index_type), - "dag_id": exp.DataType.build(index_type), - "dag_spec": exp.DataType.build(blob_type), - }, - ) diff --git a/sqlmesh/migrations/v0044_quote_identifiers_in_model_attributes.py b/sqlmesh/migrations/v0044_quote_identifiers_in_model_attributes.py deleted file mode 100644 index de5344d4ce..0000000000 --- a/sqlmesh/migrations/v0044_quote_identifiers_in_model_attributes.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Quoted identifiers in model SQL attributes.""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0045_move_gateway_variable.py b/sqlmesh/migrations/v0045_move_gateway_variable.py deleted file mode 100644 index 754f958fac..0000000000 --- a/sqlmesh/migrations/v0045_move_gateway_variable.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Move the gateway variable.""" - -import ast -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - migration_needed = False - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name, expiration_ts in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name", "expiration_ts").from_( - snapshots_table - ), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - python_env = parsed_snapshot["node"].get("python_env") - if python_env: - gateway = python_env.pop("gateway", None) - if gateway is not None: - migration_needed = True - sqlmesh_vars = {"gateway": ast.literal_eval(gateway["payload"])} - python_env["__sqlmesh__vars__"] = { - "payload": repr(sqlmesh_vars), - "kind": "value", - } - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - "expiration_ts": expiration_ts, - } - ) - - if migration_needed and new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build(blob_type), - "kind_name": exp.DataType.build(index_type), - "expiration_ts": exp.DataType.build("bigint"), - }, - ) diff --git a/sqlmesh/migrations/v0046_add_batch_concurrency.py b/sqlmesh/migrations/v0046_add_batch_concurrency.py deleted file mode 100644 index f23d27e80a..0000000000 --- a/sqlmesh/migrations/v0046_add_batch_concurrency.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Add the batch_concurrency attribute to the incremental model kinds. - -This results in a change to the metadata hash. -""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0047_change_scd_string_to_column.py b/sqlmesh/migrations/v0047_change_scd_string_to_column.py deleted file mode 100644 index 9233a54ca9..0000000000 --- a/sqlmesh/migrations/v0047_change_scd_string_to_column.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Changes the SCD Type 2 columns from strings to columns.""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0048_drop_indirect_versions.py b/sqlmesh/migrations/v0048_drop_indirect_versions.py deleted file mode 100644 index 31874268dd..0000000000 --- a/sqlmesh/migrations/v0048_drop_indirect_versions.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Drop the indirect_versions attribute in snapshots.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name, expiration_ts in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name", "expiration_ts").from_( - snapshots_table - ), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - parsed_snapshot.pop("indirect_versions", None) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - "expiration_ts": expiration_ts, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build(blob_type), - "kind_name": exp.DataType.build(index_type), - "expiration_ts": exp.DataType.build("bigint"), - }, - ) diff --git a/sqlmesh/migrations/v0049_replace_identifier_with_version_in_seeds_table.py b/sqlmesh/migrations/v0049_replace_identifier_with_version_in_seeds_table.py deleted file mode 100644 index b01bee41e1..0000000000 --- a/sqlmesh/migrations/v0049_replace_identifier_with_version_in_seeds_table.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Use version instead of identifier in the seeds table.""" - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - - snapshots_table = "_snapshots" - seeds_table = "_seeds" - new_seeds_table = f"{seeds_table}_v49" - - if state_sync.schema: - snapshots_table = f"{state_sync.schema}.{snapshots_table}" - seeds_table = f"{state_sync.schema}.{seeds_table}" - new_seeds_table = f"{state_sync.schema}.{new_seeds_table}" - - index_type = index_text_type(engine_adapter.dialect) - - engine_adapter.drop_table(new_seeds_table) - engine_adapter.create_state_table( - new_seeds_table, - { - "name": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "content": exp.DataType.build("text"), - }, - primary_key=("name", "version"), - ) - - name_col = exp.column("name", table="seeds") - version_col = exp.column("version", table="snapshots") - query = ( - exp.select( - name_col, - version_col, - exp.func("MAX", exp.column("content", table="seeds")).as_("content"), - ) - .from_(exp.to_table(seeds_table).as_("seeds")) - .join( - exp.to_table(snapshots_table).as_("snapshots"), - on=exp.and_( - exp.column("name", table="seeds").eq(exp.column("name", table="snapshots")), - exp.column("identifier", table="seeds").eq( - exp.column("identifier", table="snapshots") - ), - ), - ) - .where(exp.column("version", table="snapshots").is_(exp.null()).not_()) - .group_by(name_col, version_col) - ) - - engine_adapter.insert_append(new_seeds_table, query) - engine_adapter.drop_table(seeds_table) - engine_adapter.rename_table(new_seeds_table, seeds_table) diff --git a/sqlmesh/migrations/v0050_drop_seeds_table.py b/sqlmesh/migrations/v0050_drop_seeds_table.py deleted file mode 100644 index 0236284061..0000000000 --- a/sqlmesh/migrations/v0050_drop_seeds_table.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Drop the seeds table.""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - - seeds_table = "_seeds" - if state_sync.schema: - seeds_table = f"{state_sync.schema}.{seeds_table}" - - engine_adapter.drop_table(seeds_table) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0051_rename_column_descriptions.py b/sqlmesh/migrations/v0051_rename_column_descriptions.py deleted file mode 100644 index f76a4a05a6..0000000000 --- a/sqlmesh/migrations/v0051_rename_column_descriptions.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Rename the node attribute `column_descriptions_` to `column_descriptions` in snapshots.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - found_col_descriptions = False - - for name, identifier, version, snapshot, kind_name, expiration_ts in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name", "expiration_ts").from_( - snapshots_table - ), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - - if "column_descriptions_" in parsed_snapshot["node"]: - found_col_descriptions = True - parsed_snapshot["node"]["column_descriptions"] = parsed_snapshot["node"].pop( - "column_descriptions_" - ) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - "expiration_ts": expiration_ts, - } - ) - - if found_col_descriptions: - engine_adapter.delete_from(snapshots_table, "TRUE") - - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build(blob_type), - "kind_name": exp.DataType.build(index_type), - "expiration_ts": exp.DataType.build("bigint"), - }, - ) diff --git a/sqlmesh/migrations/v0052_add_normalize_name_in_environment_naming_info.py b/sqlmesh/migrations/v0052_add_normalize_name_in_environment_naming_info.py deleted file mode 100644 index 27980033fa..0000000000 --- a/sqlmesh/migrations/v0052_add_normalize_name_in_environment_naming_info.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Add flag that controls whether environment names will be normalized.""" - -from sqlglot import exp - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - environments_table = "_environments" - if state_sync.schema: - environments_table = f"{state_sync.schema}.{environments_table}" - - alter_table_exp = exp.Alter( - this=exp.to_table(environments_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("normalize_name"), - kind=exp.DataType.build("boolean"), - ) - ], - ) - engine_adapter.execute(alter_table_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - environments_table = "_environments" - if state_sync.schema: - environments_table = f"{state_sync.schema}.{environments_table}" - - state_sync.engine_adapter.update_table( - environments_table, - {"normalize_name": False}, - where=exp.true(), - ) diff --git a/sqlmesh/migrations/v0053_custom_model_kind_extra_attributes.py b/sqlmesh/migrations/v0053_custom_model_kind_extra_attributes.py deleted file mode 100644 index d1c83658e8..0000000000 --- a/sqlmesh/migrations/v0053_custom_model_kind_extra_attributes.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Add batch_size, batch_concurrency, and batch_interval to the CUSTOM model kind.""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0054_fix_trailing_comments.py b/sqlmesh/migrations/v0054_fix_trailing_comments.py deleted file mode 100644 index 8e7de52067..0000000000 --- a/sqlmesh/migrations/v0054_fix_trailing_comments.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Fix support for trailing comments in SQL model definitions.""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py b/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py deleted file mode 100644 index 96f39772cd..0000000000 --- a/sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Add updated_ts, unpaused_ts, ttl_ms, and unrestorable columns to the snapshots table.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.date import to_datetime, to_timestamp -from sqlmesh.utils.migration import index_text_type -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - add_column_exps = [ - exp.Alter( - this=exp.to_table(snapshots_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column(column_name), - kind=exp.DataType.build("bigint"), - ) - ], - ) - for column_name in ["updated_ts", "unpaused_ts", "ttl_ms"] - ] + [ - exp.Alter( - this=exp.to_table(snapshots_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("unrestorable"), - kind=exp.DataType.build("boolean"), - ) - ], - ) - ] - engine_adapter.execute(add_column_exps) - - if engine_adapter.dialect == "databricks": - # Databricks will throw an error like: - # > databricks.sql.exc.ServerOperationError: [DELTA_UNSUPPORTED_DROP_COLUMN] DROP COLUMN is not supported for your Delta table. - # when we try to drop `expiration_ts` below unless we set delta.columnMapping.mode to 'name' - alter_table_exp = exp.Alter( - this=exp.to_table(snapshots_table), - kind="TABLE", - actions=[ - exp.AlterSet( - expressions=[ - exp.Properties( - expressions=[ - exp.Property( - this=exp.Literal.string("delta.columnMapping.mode"), - value=exp.Literal.string("name"), - ) - ] - ) - ] - ) - ], - ) - engine_adapter.execute(alter_table_exp) - - drop_column_exp = exp.Alter( - this=exp.to_table(snapshots_table), - kind="TABLE", - actions=[exp.Drop(this=exp.to_column("expiration_ts"), kind="COLUMN")], - ) - engine_adapter.execute(drop_column_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - index_type = index_text_type(engine_adapter.dialect) - blob_type = blob_text_type(engine_adapter.dialect) - - new_snapshots = [] - - for name, identifier, version, snapshot, kind_name in engine_adapter.fetchall( - exp.select("name", "identifier", "version", "snapshot", "kind_name").from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - updated_ts = parsed_snapshot.pop("updated_ts") - unpaused_ts = parsed_snapshot.pop("unpaused_ts", None) - ttl_ms = max( - to_timestamp( - parsed_snapshot["ttl"], - relative_base=to_datetime(updated_ts), - check_categorical_relative_expression=False, - ) - - updated_ts, - 0, - ) - unrestorable = parsed_snapshot.pop("unrestorable", False) - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - "updated_ts": updated_ts, - "unpaused_ts": unpaused_ts, - "ttl_ms": ttl_ms, - "unrestorable": unrestorable, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build(blob_type), - "kind_name": exp.DataType.build(index_type), - "updated_ts": exp.DataType.build("bigint"), - "unpaused_ts": exp.DataType.build("bigint"), - "ttl_ms": exp.DataType.build("bigint"), - "unrestorable": exp.DataType.build("boolean"), - }, - ) diff --git a/sqlmesh/migrations/v0057_add_table_format.py b/sqlmesh/migrations/v0057_add_table_format.py deleted file mode 100644 index b59911ef3a..0000000000 --- a/sqlmesh/migrations/v0057_add_table_format.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Add table_format to the model top-level properties""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0058_add_requirements.py b/sqlmesh/migrations/v0058_add_requirements.py deleted file mode 100644 index 73de67d4e5..0000000000 --- a/sqlmesh/migrations/v0058_add_requirements.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Add requirements to environments table""" - -from sqlglot import exp - -from sqlmesh.utils.migration import blob_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - environments_table = "_environments" - if state_sync.schema: - environments_table = f"{state_sync.schema}.{environments_table}" - - blob_type = blob_text_type(engine_adapter.dialect) - alter_table_exp = exp.Alter( - this=exp.to_table(environments_table), - kind="TABLE", - actions=[ - exp.ColumnDef( - this=exp.to_column("requirements"), - kind=exp.DataType.build(blob_type), - ) - ], - ) - - engine_adapter.execute(alter_table_exp) - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0059_add_physical_version.py b/sqlmesh/migrations/v0059_add_physical_version.py deleted file mode 100644 index a8dfa24b7a..0000000000 --- a/sqlmesh/migrations/v0059_add_physical_version.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Add the physical_version model attribute.""" - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - pass diff --git a/sqlmesh/migrations/v0060_move_audits_to_model.py b/sqlmesh/migrations/v0060_move_audits_to_model.py deleted file mode 100644 index b4d351cf5c..0000000000 --- a/sqlmesh/migrations/v0060_move_audits_to_model.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Move audits from snapshots to models.""" - -import json - -from sqlglot import exp - -from sqlmesh.utils.migration import index_text_type - - -def migrate_schemas(state_sync, **kwargs): # type: ignore - pass - - -def migrate_rows(state_sync, **kwargs): # type: ignore - import pandas as pd - - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema - snapshots_table = "_snapshots" - index_type = index_text_type(engine_adapter.dialect) - if schema: - snapshots_table = f"{schema}.{snapshots_table}" - - new_snapshots = [] - - for ( - name, - identifier, - version, - snapshot, - kind_name, - updated_ts, - unpaused_ts, - ttl_ms, - unrestorable, - ) in engine_adapter.fetchall( - exp.select( - "name", - "identifier", - "version", - "snapshot", - "kind_name", - "updated_ts", - "unpaused_ts", - "ttl_ms", - "unrestorable", - ).from_(snapshots_table), - quote_identifiers=True, - ): - parsed_snapshot = json.loads(snapshot) - - audit_definitions = parsed_snapshot.pop("audits", []) - node = parsed_snapshot["node"] - node.pop("inline_audits", None) - - if audit_definitions: - node["audit_definitions"] = {audit["name"]: audit for audit in audit_definitions} - - new_snapshots.append( - { - "name": name, - "identifier": identifier, - "version": version, - "snapshot": json.dumps(parsed_snapshot), - "kind_name": kind_name, - "updated_ts": updated_ts, - "unpaused_ts": unpaused_ts, - "ttl_ms": ttl_ms, - "unrestorable": unrestorable, - } - ) - - if new_snapshots: - engine_adapter.delete_from(snapshots_table, "TRUE") - - engine_adapter.insert_append( - snapshots_table, - pd.DataFrame(new_snapshots), - target_columns_to_types={ - "name": exp.DataType.build(index_type), - "identifier": exp.DataType.build(index_type), - "version": exp.DataType.build(index_type), - "snapshot": exp.DataType.build("text"), - "kind_name": exp.DataType.build(index_type), - "updated_ts": exp.DataType.build("bigint"), - "unpaused_ts": exp.DataType.build("bigint"), - "ttl_ms": exp.DataType.build("bigint"), - "unrestorable": exp.DataType.build("boolean"), - }, - ) diff --git a/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py b/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py index 9e66db9f66..34b765b3ad 100644 --- a/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py +++ b/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py @@ -17,12 +17,10 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore schema = state_sync.schema environments_table = "_environments" snapshots_table = "_snapshots" - plan_dags_table = "_plan_dags" if schema: environments_table = f"{schema}.{environments_table}" snapshots_table = f"{schema}.{snapshots_table}" - plan_dags_table = f"{schema}.{plan_dags_table}" targets = [ (environments_table, "snapshots"), @@ -30,7 +28,6 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore (environments_table, "previous_finalized_snapshots"), (environments_table, "requirements"), (snapshots_table, "snapshot"), - (plan_dags_table, "dag_spec"), ] for table_name, column_name in targets: diff --git a/sqlmesh/migrations/v0096_remove_plan_dags_table.py b/sqlmesh/migrations/v0096_remove_plan_dags_table.py new file mode 100644 index 0000000000..e342d6b1a8 --- /dev/null +++ b/sqlmesh/migrations/v0096_remove_plan_dags_table.py @@ -0,0 +1,15 @@ +"""Remove the obsolete _plan_dags table.""" + + +def migrate_schemas(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + plan_dags_table = "_plan_dags" + if schema: + plan_dags_table = f"{schema}.{plan_dags_table}" + + engine_adapter.drop_table(plan_dags_table) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/sqlmesh/utils/errors.py b/sqlmesh/utils/errors.py index bbd1db3802..d90965c25c 100644 --- a/sqlmesh/utils/errors.py +++ b/sqlmesh/utils/errors.py @@ -86,6 +86,10 @@ class AuditConfigError(ConfigError): pass +class StateMigrationError(SQLMeshError): + pass + + class AuditError(SQLMeshError): def __init__( self, diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index ef5b80e151..e97e03b29e 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -872,7 +872,6 @@ def test_info_on_new_project_does_not_create_state_sync(runner, tmp_path): assert not context.engine_adapter.table_exists("sqlmesh._snapshots") assert not context.engine_adapter.table_exists("sqlmesh._environments") assert not context.engine_adapter.table_exists("sqlmesh._intervals") - assert not context.engine_adapter.table_exists("sqlmesh._plan_dags") assert not context.engine_adapter.table_exists("sqlmesh._versions") diff --git a/tests/core/state_sync/test_export_import.py b/tests/core/state_sync/test_export_import.py index c303a63e59..769fa2c2fa 100644 --- a/tests/core/state_sync/test_export_import.py +++ b/tests/core/state_sync/test_export_import.py @@ -44,7 +44,7 @@ def test_export_empty_state(tmp_path: Path, state_sync: StateSync) -> None: with pytest.raises(SQLMeshError, match=r"Please run a migration"): export_state(state_sync, output_file) - state_sync.migrate(default_catalog=None) + state_sync.migrate() export_state(state_sync, output_file) @@ -326,7 +326,7 @@ def test_import_invalid_file(tmp_path: Path, state_sync: StateSync) -> None: def test_import_from_older_version_export_fails(tmp_path: Path, state_sync: StateSync) -> None: - state_sync.migrate(default_catalog=None) + state_sync.migrate() current_version = state_sync.get_versions() major, minor = current_version.minor_sqlmesh_version @@ -354,7 +354,7 @@ def test_import_from_older_version_export_fails(tmp_path: Path, state_sync: Stat def test_import_from_newer_version_export_fails(tmp_path: Path, state_sync: StateSync) -> None: - state_sync.migrate(default_catalog=None) + state_sync.migrate() current_version = state_sync.get_versions() major, minor = current_version.minor_sqlmesh_version @@ -472,7 +472,7 @@ def test_roundtrip(tmp_path: Path, example_project_config: Config, state_sync: S state_sync.engine_adapter.drop_schema("sqlmesh", cascade=True) # state was destroyed, plan should have changes - state_sync.migrate(default_catalog=None) + state_sync.migrate() plan = context.plan() assert plan.has_changes @@ -509,7 +509,7 @@ def test_roundtrip(tmp_path: Path, example_project_config: Config, state_sync: S with pytest.raises(SQLMeshError, match=r"Please run a migration"): state_sync.get_versions(validate=True) - state_sync.migrate(default_catalog=None) + state_sync.migrate() import_state(state_sync, state_file) # should be no changes in dev @@ -610,7 +610,7 @@ def test_roundtrip_includes_environment_statements(tmp_path: Path) -> None: with pytest.raises(SQLMeshError, match=r"Please run a migration"): state_sync.get_versions(validate=True) - state_sync.migrate(default_catalog=None) + state_sync.migrate() import_state(state_sync, state_file) assert not context.plan().has_changes diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index 327ec82210..51a646ce5d 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -47,7 +47,7 @@ Versions, ) from sqlmesh.utils.date import now_timestamp, to_datetime, to_timestamp -from sqlmesh.utils.errors import SQLMeshError +from sqlmesh.utils.errors import SQLMeshError, StateMigrationError pytestmark = pytest.mark.slow @@ -59,7 +59,7 @@ def state_sync(duck_conn, tmp_path): schema=c.SQLMESH, cache_dir=tmp_path / c.CACHE, ) - state_sync.migrate(default_catalog=None) + state_sync.migrate() return state_sync @@ -2031,7 +2031,7 @@ def test_version_schema(state_sync: EngineAdapterStateSync, tmp_path) -> None: ): state_sync.get_versions() - state_sync.migrate(default_catalog=None) + state_sync.migrate() # migration version is behind, always raise state_sync.version_state.update_versions(schema_version=SCHEMA_VERSION + 1) @@ -2137,7 +2137,7 @@ def test_migrate(state_sync: EngineAdapterStateSync, mocker: MockerFixture, tmp_ backup_state_mock = mocker.patch( "sqlmesh.core.state_sync.db.migrator.StateMigrator._backup_state" ) - state_sync.migrate(default_catalog=None) + state_sync.migrate() migrate_rows_mock.assert_not_called() backup_state_mock.assert_not_called() @@ -2148,7 +2148,7 @@ def test_migrate(state_sync: EngineAdapterStateSync, mocker: MockerFixture, tmp_ cache_dir=tmp_path / c.CACHE, ) - state_sync.migrate(default_catalog=None) + state_sync.migrate() migrate_rows_mock.assert_called_once() backup_state_mock.assert_called_once() assert state_sync.get_versions() == Versions( @@ -2205,7 +2205,7 @@ def test_first_migration_failure(duck_conn, mocker: MockerFixture, tmp_path) -> SQLMeshError, match="SQLMesh migration failed.", ): - state_sync.migrate(default_catalog=None) + state_sync.migrate() assert not state_sync.engine_adapter.table_exists(state_sync.snapshot_state.snapshots_table) assert not state_sync.engine_adapter.table_exists( state_sync.environment_state.environments_table @@ -2215,7 +2215,15 @@ def test_first_migration_failure(duck_conn, mocker: MockerFixture, tmp_path) -> def test_migrate_rows(state_sync: EngineAdapterStateSync, mocker: MockerFixture) -> None: - delete_versions(state_sync) + state_sync.engine_adapter.replace_query( + "sqlmesh._versions", + pd.read_json("tests/fixtures/migrations/versions.json"), + target_columns_to_types={ + "schema_version": exp.DataType.build("int"), + "sqlglot_version": exp.DataType.build("text"), + "sqlmesh_version": exp.DataType.build("text"), + }, + ) state_sync.engine_adapter.replace_query( "sqlmesh._snapshots", @@ -2225,6 +2233,11 @@ def test_migrate_rows(state_sync: EngineAdapterStateSync, mocker: MockerFixture) "identifier": exp.DataType.build("text"), "version": exp.DataType.build("text"), "snapshot": exp.DataType.build("text"), + "kind_name": exp.DataType.build("text"), + "updated_ts": exp.DataType.build("bigint"), + "unpaused_ts": exp.DataType.build("bigint"), + "ttl_ms": exp.DataType.build("bigint"), + "unrestorable": exp.DataType.build("boolean"), }, ) @@ -2239,21 +2252,43 @@ def test_migrate_rows(state_sync: EngineAdapterStateSync, mocker: MockerFixture) "plan_id": exp.DataType.build("text"), "previous_plan_id": exp.DataType.build("text"), "expiration_ts": exp.DataType.build("bigint"), + "finalized_ts": exp.DataType.build("bigint"), + "promoted_snapshot_ids": exp.DataType.build("text"), + "suffix_target": exp.DataType.build("text"), + "catalog_name_override": exp.DataType.build("text"), + "previous_finalized_snapshots": exp.DataType.build("text"), + "normalize_name": exp.DataType.build("boolean"), + "requirements": exp.DataType.build("text"), }, ) - state_sync.engine_adapter.drop_table("sqlmesh._seeds") - state_sync.engine_adapter.drop_table("sqlmesh._intervals") + state_sync.engine_adapter.replace_query( + "sqlmesh._intervals", + pd.read_json("tests/fixtures/migrations/intervals.json"), + target_columns_to_types={ + "id": exp.DataType.build("text"), + "created_ts": exp.DataType.build("bigint"), + "name": exp.DataType.build("text"), + "identifier": exp.DataType.build("text"), + "version": exp.DataType.build("text"), + "start_ts": exp.DataType.build("bigint"), + "end_ts": exp.DataType.build("bigint"), + "is_dev": exp.DataType.build("boolean"), + "is_removed": exp.DataType.build("boolean"), + "is_compacted": exp.DataType.build("boolean"), + }, + ) old_snapshots = state_sync.engine_adapter.fetchdf("select * from sqlmesh._snapshots") old_environments = state_sync.engine_adapter.fetchdf("select * from sqlmesh._environments") - state_sync.migrate(default_catalog=None, skip_backup=True) + state_sync.migrate(skip_backup=True) new_snapshots = state_sync.engine_adapter.fetchdf("select * from sqlmesh._snapshots") new_environments = state_sync.engine_adapter.fetchdf("select * from sqlmesh._environments") - assert len(old_snapshots) * 2 == len(new_snapshots) + assert len(old_snapshots) == 24 + assert len(new_snapshots) == 36 assert len(old_environments) == len(new_environments) start = "2023-01-01" @@ -2332,7 +2367,7 @@ def test_restore_snapshots_table(state_sync: EngineAdapterStateSync) -> None: old_snapshots_count = state_sync.engine_adapter.fetchone( "select count(*) from sqlmesh._snapshots" ) - assert old_snapshots_count == (12,) + assert old_snapshots_count == (24,) state_sync.migrator._backup_state() state_sync.engine_adapter.delete_from("sqlmesh._snapshots", "TRUE") @@ -3656,3 +3691,25 @@ def test_get_snapshots_by_names_include_expired( snapshot_names=['"a"'], current_ts=(now_ts - (10 * 1000)) ) } == {normal_a.snapshot_id, expired_a.snapshot_id} + + +def test_state_version_is_too_old( + state_sync: EngineAdapterStateSync, mocker: MockerFixture +) -> None: + state_sync.engine_adapter.replace_query( + "sqlmesh._versions", + pd.DataFrame( + [{"schema_version": 59, "sqlmesh_version": "0.133.0", "sqlglot_version": "25.31.4"}] + ), + target_columns_to_types={ + "schema_version": exp.DataType.build("int"), + "sqlglot_version": exp.DataType.build("text"), + "sqlmesh_version": exp.DataType.build("text"), + }, + ) + + with pytest.raises( + StateMigrationError, + 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) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 3a72dcca60..ca0789d262 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -6158,7 +6158,6 @@ def get_default_catalog_and_non_tables( { "physical.sqlmesh._environments", "physical.sqlmesh._intervals", - "physical.sqlmesh._plan_dags", "physical.sqlmesh._snapshots", "physical.sqlmesh._versions", } @@ -6178,7 +6177,6 @@ def get_default_catalog_and_non_tables( { "physical.sqlmesh._environments", "physical.sqlmesh._intervals", - "physical.sqlmesh._plan_dags", "physical.sqlmesh._snapshots", "physical.sqlmesh._versions", } @@ -6198,7 +6196,6 @@ def get_default_catalog_and_non_tables( { "physical.sqlmesh._environments", "physical.sqlmesh._intervals", - "physical.sqlmesh._plan_dags", "physical.sqlmesh._snapshots", "physical.sqlmesh._versions", } @@ -6219,7 +6216,6 @@ def get_default_catalog_and_non_tables( { "physical.sqlmesh._environments", "physical.sqlmesh._intervals", - "physical.sqlmesh._plan_dags", "physical.sqlmesh._snapshots", "physical.sqlmesh._versions", } @@ -6611,7 +6607,6 @@ def track_duckdb_execute(self, expression, **kwargs): "_intervals", "_auto_restatements", "_environment_statements", - "_plan_dags", ] # to ignore the state queries @@ -7098,7 +7093,6 @@ def test_destroy(copy_to_temp_path): "_auto_restatements", "_environment_statements", "_intervals", - "_plan_dags", "_versions", } for table_name in state_tables: diff --git a/tests/fixtures/migrations/environments.json b/tests/fixtures/migrations/environments.json index e841e38463..cbe4945863 100644 --- a/tests/fixtures/migrations/environments.json +++ b/tests/fixtures/migrations/environments.json @@ -1 +1 @@ -{"name":{"0":"staging","1":"dev"},"snapshots":{"0":"[{\"name\": \"sushi.waiter_as_customer_by_day\", \"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"1267397572\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.waiters\", \"identifier\": \"3386889721\"}, {\"name\": \"sushi.waiter_names\", \"identifier\": \"3233103305\"}, {\"name\": \"sushi.customers\", \"identifier\": \"3148897116\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.waiter_revenue_by_day\", \"fingerprint\": {\"data_hash\": \"2443934302\", \"metadata_hash\": \"2904050331\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"version\": \"2695875565\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.order_items\", \"identifier\": \"1806777563\"}, {\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.top_waiters\", \"fingerprint\": {\"data_hash\": \"2891807529\", \"metadata_hash\": \"3392493998\", \"parent_data_hash\": \"1940707936\", \"parent_metadata_hash\": \"1276363398\"}, \"version\": \"3010914162\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.waiter_revenue_by_day\", \"identifier\": \"1609279380\"}], \"previous_versions\": [], \"is_materialized\": false, \"is_embedded_kind\": false}, {\"name\": \"sushi.waiters\", \"fingerprint\": {\"data_hash\": \"3501061139\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"version\": \"2059227798\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [], \"is_materialized\": false, \"is_embedded_kind\": true}, {\"name\": \"sushi.customers\", \"fingerprint\": {\"data_hash\": \"3553985282\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"version\": \"2359719298\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.waiter_names\", \"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"2505706914\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.customer_revenue_by_day\", \"fingerprint\": {\"data_hash\": \"2657552867\", \"metadata_hash\": \"129771006\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"version\": \"1291364031\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.order_items\", \"identifier\": \"1806777563\"}, {\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.items\", \"fingerprint\": {\"data_hash\": \"1960378930\", \"metadata_hash\": \"2900807542\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"312608270\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.order_items\", \"fingerprint\": {\"data_hash\": \"653664599\", \"metadata_hash\": \"1960934702\", \"parent_data_hash\": \"3170724558\", \"parent_metadata_hash\": \"867324801\"}, \"version\": \"1015284155\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.orders\", \"fingerprint\": {\"data_hash\": \"1628439771\", \"metadata_hash\": \"2745052130\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"925846788\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}]","1":"[{\"name\": \"sushi.waiter_as_customer_by_day\", \"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2824767713\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"3668757715\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.waiters\", \"identifier\": \"3386889721\"}, {\"name\": \"sushi.waiter_names\", \"identifier\": \"1604207722\"}, {\"name\": \"sushi.customers\", \"identifier\": \"3148897116\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"1267397572\"}], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.waiter_revenue_by_day\", \"fingerprint\": {\"data_hash\": \"2443934302\", \"metadata_hash\": \"2904050331\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"version\": \"2695875565\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.order_items\", \"identifier\": \"1806777563\"}, {\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.top_waiters\", \"fingerprint\": {\"data_hash\": \"2891807529\", \"metadata_hash\": \"3392493998\", \"parent_data_hash\": \"1940707936\", \"parent_metadata_hash\": \"1276363398\"}, \"version\": \"3010914162\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.waiter_revenue_by_day\", \"identifier\": \"1609279380\"}], \"previous_versions\": [], \"is_materialized\": false, \"is_embedded_kind\": false}, {\"name\": \"sushi.waiters\", \"fingerprint\": {\"data_hash\": \"3501061139\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"version\": \"2059227798\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [], \"is_materialized\": false, \"is_embedded_kind\": true}, {\"name\": \"sushi.customers\", \"fingerprint\": {\"data_hash\": \"3553985282\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"version\": \"2359719298\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.waiter_names\", \"fingerprint\": {\"data_hash\": \"4133862560\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"1204702829\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"2505706914\"}], \"change_category\": 1, \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.customer_revenue_by_day\", \"fingerprint\": {\"data_hash\": \"2657552867\", \"metadata_hash\": \"129771006\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"version\": \"1291364031\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.order_items\", \"identifier\": \"1806777563\"}, {\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.items\", \"fingerprint\": {\"data_hash\": \"1960378930\", \"metadata_hash\": \"2900807542\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"312608270\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.order_items\", \"fingerprint\": {\"data_hash\": \"653664599\", \"metadata_hash\": \"1960934702\", \"parent_data_hash\": \"3170724558\", \"parent_metadata_hash\": \"867324801\"}, \"version\": \"1015284155\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}, {\"name\": \"sushi.orders\", \"fingerprint\": {\"data_hash\": \"1628439771\", \"metadata_hash\": \"2745052130\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"925846788\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"previous_versions\": [], \"is_materialized\": true, \"is_embedded_kind\": false}]"},"start_at":{"0":"2023-01-01","1":"2023-01-01"},"end_at":{"0":"2023-01-07","1":"2023-01-07"},"plan_id":{"0":"2b16ff4b77dc44789b628b4a8a4ed38a","1":"d5dcc7aafce742aab763331525196613"},"previous_plan_id":{"0":null,"1":"79f4bab2177b495ab877b674bc511f2b"},"expiration_ts":{"0":1681419197966,"1":1681419273635}} \ No newline at end of file +{"name":{"0":"staging","1":"dev"},"snapshots":{"0":"[{\"name\": \"\\\"sushi\\\".\\\"waiter_as_customer_by_day\\\"\", \"temp_version\": \"1267397572\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"849558693\", \"metadata_hash\": \"2088684978\", \"parent_data_hash\": \"2705906012\", \"parent_metadata_hash\": \"665080906\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"1267397572\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"1267397572\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"waiter_names\\\"\", \"identifier\": \"1609854746\"}, {\"name\": \"\\\"sushi\\\".\\\"waiters\\\"\", \"identifier\": \"4123940212\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}, {\"name\": \"\\\"sushi\\\".\\\"customers\\\"\", \"identifier\": \"1461038955\"}], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"waiter_revenue_by_day\\\"\", \"temp_version\": \"2695875565\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"2224089837\", \"metadata_hash\": \"2504236462\", \"parent_data_hash\": \"2738168331\", \"parent_metadata_hash\": \"1795276494\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"2443934302\", \"metadata_hash\": \"2904050331\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"version\": \"2695875565\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"2695875565\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"identifier\": \"3721860967\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}, {\"name\": \"\\\"sushi\\\".\\\"order_items\\\"\", \"identifier\": \"1422946820\"}], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"top_waiters\\\"\", \"temp_version\": \"3010914162\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"4131026946\", \"metadata_hash\": \"154190563\", \"parent_data_hash\": \"929243525\", \"parent_metadata_hash\": \"2366450878\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"2891807529\", \"metadata_hash\": \"3392493998\", \"parent_data_hash\": \"1940707936\", \"parent_metadata_hash\": \"1276363398\"}, \"version\": \"3010914162\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"3010914162\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"waiter_revenue_by_day\\\"\", \"identifier\": \"2175947464\"}], \"kind_name\": \"VIEW\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"waiters\\\"\", \"temp_version\": \"2059227798\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"2037801255\", \"metadata_hash\": \"3063653103\", \"parent_data_hash\": \"458609840\", \"parent_metadata_hash\": \"2007040660\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"3501061139\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"version\": \"2059227798\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"2059227798\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}], \"kind_name\": \"EMBEDDED\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"customers\\\"\", \"temp_version\": \"2359719298\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"2431070412\", \"metadata_hash\": \"3063653103\", \"parent_data_hash\": \"458609840\", \"parent_metadata_hash\": \"2007040660\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"3553985282\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"version\": \"2359719298\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"2359719298\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}], \"kind_name\": \"FULL\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"waiter_names\\\"\", \"temp_version\": \"2505706914\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"3604872020\", \"metadata_hash\": \"3468846895\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"2505706914\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"2505706914\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"kind_name\": \"SEED\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"customer_revenue_by_day\\\"\", \"temp_version\": \"1291364031\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"131732542\", \"metadata_hash\": \"1368842087\", \"parent_data_hash\": \"2738168331\", \"parent_metadata_hash\": \"1795276494\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"2657552867\", \"metadata_hash\": \"129771006\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"version\": \"1291364031\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"1291364031\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"identifier\": \"3721860967\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}, {\"name\": \"\\\"sushi\\\".\\\"order_items\\\"\", \"identifier\": \"1422946820\"}], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"temp_version\": \"312608270\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"1862622614\", \"metadata_hash\": \"3651173237\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"1960378930\", \"metadata_hash\": \"2900807542\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"312608270\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"312608270\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"order_items\\\"\", \"temp_version\": \"1015284155\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"4010068827\", \"metadata_hash\": \"799196655\", \"parent_data_hash\": \"2342431947\", \"parent_metadata_hash\": \"1746080605\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"653664599\", \"metadata_hash\": \"1960934702\", \"parent_data_hash\": \"3170724558\", \"parent_metadata_hash\": \"867324801\"}, \"version\": \"1015284155\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"1015284155\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"identifier\": \"3721860967\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"temp_version\": \"925846788\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"1588786367\", \"metadata_hash\": \"1674367104\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"1628439771\", \"metadata_hash\": \"2745052130\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"925846788\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"925846788\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}]","1":"[{\"name\": \"\\\"sushi\\\".\\\"waiter_as_customer_by_day\\\"\", \"temp_version\": \"3668757715\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"1936268024\", \"metadata_hash\": \"2088684978\", \"parent_data_hash\": \"3055854652\", \"parent_metadata_hash\": \"665080906\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"1267397572\"}, {\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2824767713\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"3668757715\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"3668757715\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"waiter_names\\\"\", \"identifier\": \"2725136291\"}, {\"name\": \"\\\"sushi\\\".\\\"waiters\\\"\", \"identifier\": \"4123940212\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}, {\"name\": \"\\\"sushi\\\".\\\"customers\\\"\", \"identifier\": \"1461038955\"}], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"waiter_revenue_by_day\\\"\", \"temp_version\": \"2695875565\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"2224089837\", \"metadata_hash\": \"2504236462\", \"parent_data_hash\": \"2738168331\", \"parent_metadata_hash\": \"1795276494\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"2443934302\", \"metadata_hash\": \"2904050331\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"version\": \"2695875565\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"2695875565\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"identifier\": \"3721860967\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}, {\"name\": \"\\\"sushi\\\".\\\"order_items\\\"\", \"identifier\": \"1422946820\"}], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"top_waiters\\\"\", \"temp_version\": \"3010914162\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"4131026946\", \"metadata_hash\": \"154190563\", \"parent_data_hash\": \"929243525\", \"parent_metadata_hash\": \"2366450878\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"2891807529\", \"metadata_hash\": \"3392493998\", \"parent_data_hash\": \"1940707936\", \"parent_metadata_hash\": \"1276363398\"}, \"version\": \"3010914162\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"3010914162\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"waiter_revenue_by_day\\\"\", \"identifier\": \"2175947464\"}], \"kind_name\": \"VIEW\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"waiters\\\"\", \"temp_version\": \"2059227798\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"2037801255\", \"metadata_hash\": \"3063653103\", \"parent_data_hash\": \"458609840\", \"parent_metadata_hash\": \"2007040660\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"3501061139\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"version\": \"2059227798\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"2059227798\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}], \"kind_name\": \"EMBEDDED\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"customers\\\"\", \"temp_version\": \"2359719298\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"2431070412\", \"metadata_hash\": \"3063653103\", \"parent_data_hash\": \"458609840\", \"parent_metadata_hash\": \"2007040660\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"3553985282\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"version\": \"2359719298\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"2359719298\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}], \"kind_name\": \"FULL\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"waiter_names\\\"\", \"temp_version\": \"1204702829\", \"change_category\": 1, \"fingerprint\": {\"data_hash\": \"1437406487\", \"metadata_hash\": \"3468846895\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"2505706914\"}, {\"fingerprint\": {\"data_hash\": \"4133862560\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"1204702829\", \"change_category\": 1, \"physical_schema\": \"sqlmesh\"}], \"version\": \"1204702829\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"kind_name\": \"SEED\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"customer_revenue_by_day\\\"\", \"temp_version\": \"1291364031\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"131732542\", \"metadata_hash\": \"1368842087\", \"parent_data_hash\": \"2738168331\", \"parent_metadata_hash\": \"1795276494\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"2657552867\", \"metadata_hash\": \"129771006\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"version\": \"1291364031\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"1291364031\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"identifier\": \"3721860967\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}, {\"name\": \"\\\"sushi\\\".\\\"order_items\\\"\", \"identifier\": \"1422946820\"}], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"temp_version\": \"312608270\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"1862622614\", \"metadata_hash\": \"3651173237\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"1960378930\", \"metadata_hash\": \"2900807542\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"312608270\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"312608270\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"order_items\\\"\", \"temp_version\": \"1015284155\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"4010068827\", \"metadata_hash\": \"799196655\", \"parent_data_hash\": \"2342431947\", \"parent_metadata_hash\": \"1746080605\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"653664599\", \"metadata_hash\": \"1960934702\", \"parent_data_hash\": \"3170724558\", \"parent_metadata_hash\": \"867324801\"}, \"version\": \"1015284155\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"1015284155\", \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"identifier\": \"3721860967\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"1250207606\"}], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"temp_version\": \"925846788\", \"change_category\": 4, \"fingerprint\": {\"data_hash\": \"1588786367\", \"metadata_hash\": \"1674367104\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"1628439771\", \"metadata_hash\": \"2745052130\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"925846788\", \"change_category\": 4, \"physical_schema\": \"sqlmesh\"}], \"version\": \"925846788\", \"physical_schema\": \"sqlmesh\", \"parents\": [], \"kind_name\": \"INCREMENTAL_BY_TIME_RANGE\", \"node_type\": \"model\"}]"},"start_at":{"0":"2023-01-01 00:00:00","1":"2023-01-01 00:00:00"},"end_at":{"0":"2023-01-07 00:00:00","1":"2023-01-07 00:00:00"},"plan_id":{"0":"2b16ff4b77dc44789b628b4a8a4ed38a","1":"d5dcc7aafce742aab763331525196613"},"previous_plan_id":{"0":null,"1":"79f4bab2177b495ab877b674bc511f2b"},"expiration_ts":{"0":1681419197966,"1":1681419273635},"finalized_ts":{"0":null,"1":null},"promoted_snapshot_ids":{"0":null,"1":null},"suffix_target":{"0":"schema","1":"schema"},"catalog_name_override":{"0":null,"1":null},"previous_finalized_snapshots":{"0":null,"1":null},"normalize_name":{"0":false,"1":false},"requirements":{"0":"{}","1":"{}"}} \ No newline at end of file diff --git a/tests/fixtures/migrations/intervals.json b/tests/fixtures/migrations/intervals.json new file mode 100644 index 0000000000..276fdd60de --- /dev/null +++ b/tests/fixtures/migrations/intervals.json @@ -0,0 +1 @@ +{"id":{"0":"1a1121bc700040d8af4f78ad96e025f1","1":"b901107d2ede4f50be32090eb2559d1a","2":"b366e44fd5e541008cb987a503b5ed7a","3":"ccbcd24427ac432da53fa158313ad800","4":"4fd6bdae011c4978aac8eb5a47521753","5":"d8549fb5f3674b29b4aa2b9988a42052","6":"3f8120d2a2c74f3baca25172537a7788","7":"f417d94c20e44dc5b1a0c29478672ac4","8":"6fd67cfbfcc743c8a87c32a95431c079","9":"b5a8f45c901e4c97aa634eb3ee5f521e","10":"46c7fdaccfd84ba68d766021d7d76511"},"created_ts":{"0":1757115220259,"1":1757115220259,"2":1757115220259,"3":1757115220259,"4":1757115220259,"5":1757115220259,"6":1757115220259,"7":1757115220259,"8":1757115220259,"9":1757115220260,"10":1757115220260},"name":{"0":"\"sushi\".\"waiter_as_customer_by_day\"","1":"\"sushi\".\"waiter_revenue_by_day\"","2":"\"sushi\".\"top_waiters\"","3":"\"sushi\".\"customers\"","4":"\"sushi\".\"waiter_names\"","5":"\"sushi\".\"customer_revenue_by_day\"","6":"\"sushi\".\"items\"","7":"\"sushi\".\"order_items\"","8":"\"sushi\".\"orders\"","9":"\"sushi\".\"waiter_as_customer_by_day\"","10":"\"sushi\".\"waiter_names\""},"identifier":{"0":"1281222509","1":"1609279380","2":"599861134","3":"3148897116","4":"3233103305","5":"1308408370","6":"2957171338","7":"1806777563","8":"3564161223","9":"1084858582","10":"1604207722"},"version":{"0":"1267397572","1":"2695875565","2":"3010914162","3":"2359719298","4":"2505706914","5":"1291364031","6":"312608270","7":"1015284155","8":"925846788","9":"3668757715","10":"1204702829"},"start_ts":{"0":1672531200000,"1":1672531200000,"2":1672531200000,"3":1672531200000,"4":1672531200000,"5":1672531200000,"6":1672531200000,"7":1672531200000,"8":1672531200000,"9":1672531200000,"10":1672531200000},"end_ts":{"0":1673136000000,"1":1673136000000,"2":1673136000000,"3":1673136000000,"4":1673136000000,"5":1673136000000,"6":1673136000000,"7":1673136000000,"8":1673136000000,"9":1673136000000,"10":1673136000000},"is_dev":{"0":false,"1":false,"2":false,"3":false,"4":false,"5":false,"6":false,"7":false,"8":false,"9":false,"10":false},"is_removed":{"0":false,"1":false,"2":false,"3":false,"4":false,"5":false,"6":false,"7":false,"8":false,"9":false,"10":false},"is_compacted":{"0":true,"1":true,"2":true,"3":true,"4":true,"5":true,"6":true,"7":true,"8":true,"9":true,"10":true}} \ No newline at end of file diff --git a/tests/fixtures/migrations/snapshots.json b/tests/fixtures/migrations/snapshots.json index 45cebe613b..638009abf1 100644 --- a/tests/fixtures/migrations/snapshots.json +++ b/tests/fixtures/migrations/snapshots.json @@ -1 +1 @@ -{"name":{"0":"sushi.waiter_as_customer_by_day","1":"sushi.waiter_revenue_by_day","2":"sushi.top_waiters","3":"sushi.waiters","4":"sushi.customers","5":"sushi.waiter_names","6":"sushi.customer_revenue_by_day","7":"sushi.items","8":"sushi.order_items","9":"sushi.orders","10":"sushi.waiter_as_customer_by_day","11":"sushi.waiter_names"},"identifier":{"0":"1281222509","1":"1609279380","2":"599861134","3":"3386889721","4":"3148897116","5":"3233103305","6":"1308408370","7":"2957171338","8":"1806777563","9":"3564161223","10":"1084858582","11":"1604207722"},"version":{"0":"1267397572","1":"2695875565","2":"3010914162","3":"2059227798","4":"2359719298","5":"2505706914","6":"1291364031","7":"312608270","8":"1015284155","9":"925846788","10":"3668757715","11":"1204702829"},"snapshot":{"0":"{\"name\": \"sushi.waiter_as_customer_by_day\", \"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_as_customer_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT w.ds AS ds, w.waiter_id AS waiter_id, wn.name AS waiter_name FROM sushi.waiters AS w JOIN sushi.customers AS c ON w.waiter_id = c.customer_id JOIN sushi.waiter_names AS wn ON w.waiter_id = wn.id\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.waiters\", \"identifier\": \"3386889721\"}, {\"name\": \"sushi.waiter_names\", \"identifier\": \"3233103305\"}, {\"name\": \"sushi.customers\", \"identifier\": \"3148897116\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376348, \"updated_ts\": 1680814376348, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"1267397572\"}","1":"{\"name\": \"sushi.waiter_revenue_by_day\", \"fingerprint\": {\"data_hash\": \"2443934302\", \"metadata_hash\": \"2904050331\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_revenue_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"Table of revenue generated by waiters by day.\", \"batch_size\": 10, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"number_of_rows\", {\"threshold\": \"0\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT CAST(o.waiter_id AS INT) AS waiter_id \/* Waiter id *\/, CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue \/* Revenue from orders taken by this waiter *\/, CAST(o.ds AS TEXT) AS ds \/* Date *\/ FROM sushi.orders AS o LEFT JOIN sushi.order_items AS oi ON o.id = oi.order_id AND o.ds = oi.ds LEFT JOIN sushi.items AS i ON oi.item_id = i.id AND oi.ds = i.ds WHERE o.ds BETWEEN @start_ds AND @end_ds GROUP BY o.waiter_id, o.ds\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.order_items\", \"identifier\": \"1806777563\"}, {\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376361, \"updated_ts\": 1680814376361, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2695875565\"}","2":"{\"name\": \"sushi.top_waiters\", \"fingerprint\": {\"data_hash\": \"2891807529\", \"metadata_hash\": \"3392493998\", \"parent_data_hash\": \"1940707936\", \"parent_metadata_hash\": \"1276363398\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.top_waiters\", \"kind\": {\"name\": \"VIEW\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"View of top waiters.\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"unique_values\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT CAST(waiter_id AS INT) AS waiter_id, CAST(revenue AS DOUBLE) AS revenue FROM sushi.waiter_revenue_by_day WHERE ds = (SELECT MAX(ds) FROM sushi.waiter_revenue_by_day) ORDER BY revenue DESC LIMIT 10\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.waiter_revenue_by_day\", \"identifier\": \"1609279380\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376384, \"updated_ts\": 1680814376384, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"3010914162\"}","3":"{\"name\": \"sushi.waiters\", \"fingerprint\": {\"data_hash\": \"3501061139\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiters\", \"kind\": {\"name\": \"EMBEDDED\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"python_env\": {\"incremental_by_ds\": {\"payload\": \"def incremental_by_ds(evaluator, column):\\n expression = evaluator.transform(exp.Between(this=column, low=MacroVar(\\n this='start_ds'), high=MacroVar(this='end_ds')))\\n if not isinstance(expression, exp.Expression):\\n raise MacroEvalError(\\n f'Return type is {type(expression)}, expected exp.Expression')\\n return expression\", \"kind\": \"definition\", \"name\": \"incremental_by_ds\", \"path\": \"macros\/macros.py\"}, \"exp\": {\"payload\": \"import sqlglot.expressions as exp\", \"kind\": \"import\"}, \"MacroVar\": {\"payload\": \"from sqlmesh.core.dialect import MacroVar\", \"kind\": \"import\"}, \"MacroEvalError\": {\"payload\": \"from sqlmesh.utils.errors import MacroEvalError\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT DISTINCT CAST(waiter_id AS INT) AS waiter_id, CAST(ds AS TEXT) AS ds FROM sushi.orders AS o WHERE @incremental_by_ds(ds)\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [], \"dev_intervals\": [], \"created_ts\": 1680814376387, \"updated_ts\": 1680814376387, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2059227798\"}","4":"{\"name\": \"sushi.customers\", \"fingerprint\": {\"data_hash\": \"3553985282\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.customers\", \"kind\": {\"name\": \"FULL\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [[\"noop\", {\"x\": \"1\"}]], \"post\": [[\"noop\", {}], [\"noop\", {\"y\": \"ARRAY('a', 2)\"}]], \"audits\": [], \"expressions\": [], \"python_env\": {\"noop\": {\"payload\": \"def noop(context, start, end, latest, **kwargs):\\n pass\", \"kind\": \"definition\", \"name\": \"noop\", \"path\": \"hooks\/hooks.py\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT DISTINCT CAST(customer_id AS INT) AS customer_id FROM sushi.orders AS o\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376388, \"updated_ts\": 1680814376388, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2359719298\"}","5":"{\"name\": \"sushi.waiter_names\", \"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_names\", \"kind\": {\"name\": \"SEED\", \"path\": \"..\/seeds\/waiter_names.csv\", \"batch_size\": 5}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"seed\": {\"content\": \"id,name\\n0,Toby\\n1,Tyson\\n2,Ryan\\n3,George\\n4,Chris\\n5,Max\\n6,Vincent\\n7,Iaroslav\\n8,Emma\\n9,Maia\\n\"}, \"source_type\": \"seed\"}, \"parents\": [], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376389, \"updated_ts\": 1680814376389, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"2505706914\"}","6":"{\"name\": \"sushi.customer_revenue_by_day\", \"fingerprint\": {\"data_hash\": \"2657552867\", \"metadata_hash\": \"129771006\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.customer_revenue_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"hive\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"Table of revenue from customers by day.\", \"batch_size\": 10, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"WITH order_total AS (SELECT oi.order_id AS order_id, SUM(oi.quantity * i.price) AS total, oi.ds AS ds FROM sushi.order_items AS oi LEFT JOIN sushi.items AS i ON oi.item_id = i.id AND oi.ds = i.ds WHERE oi.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' GROUP BY oi.order_id, oi.ds) SELECT CAST(o.customer_id AS INT) AS customer_id \/* Customer id *\/, CAST(SUM(ot.total) AS DOUBLE) AS revenue \/* Revenue from orders made by this customer *\/, CAST(o.ds AS TEXT) AS ds \/* Date *\/ FROM sushi.orders AS o LEFT JOIN order_total AS ot ON o.id = ot.order_id AND o.ds = ot.ds WHERE o.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' GROUP BY o.customer_id, o.ds\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.order_items\", \"identifier\": \"1806777563\"}, {\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376391, \"updated_ts\": 1680814376391, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"1291364031\"}","7":"{\"name\": \"sushi.items\", \"fingerprint\": {\"data_hash\": \"1960378930\", \"metadata_hash\": \"2900807542\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.items\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"\", \"cron\": \"@daily\", \"start\": \"Jan 1 2022\", \"batch_size\": 30, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"depends_on\": [], \"columns\": {\"id\": \"INT\", \"name\": \"TEXT\", \"price\": \"DOUBLE\", \"ds\": \"TEXT\"}, \"audits\": [[\"accepted_values\", {\"column\": \"name\", \"values\": \"ARRAY('Ahi', 'Aji', 'Amaebi', 'Anago', 'Aoyagi', 'Bincho', 'Katsuo', 'Ebi', 'Escolar', 'Hamachi', 'Hamachi Toro', 'Hirame', 'Hokigai', 'Hotate', 'Ika', 'Ikura', 'Iwashi', 'Kani', 'Kanpachi', 'Maguro', 'Saba', 'Sake', 'Sake Toro', 'Tai', 'Tako', 'Tamago', 'Tobiko', 'Toro', 'Tsubugai', 'Umi Masu', 'Unagi', 'Uni')\"}], [\"not_null\", {\"columns\": \"ARRAY(name, price)\"}], [\"assert_items_price_exceeds_threshold\", {\"price\": \"0\"}]], \"expressions\": [], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n dfs = []\\n for dt in iter_dates(start, end):\\n num_items = random.randint(10, len(ITEMS))\\n dfs.append(pd.DataFrame({'name': random.sample(ITEMS, num_items),\\n 'price': np.random.uniform(3.0, 10.0, size=num_items).round(2),\\n 'ds': to_ds(dt)}).reset_index().rename(columns={'index': 'id'}))\\n return pd.concat(dfs)\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/items.py\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"ITEMS\": {\"payload\": \"['Ahi', 'Aji', 'Amaebi', 'Anago', 'Aoyagi', 'Bincho', 'Katsuo', 'Ebi', 'Escolar', 'Hamachi', 'Hamachi Toro', 'Hirame', 'Hokigai', 'Hotate', 'Ika', 'Ikura', 'Iwashi', 'Kani', 'Kanpachi', 'Maguro', 'Saba', 'Sake', 'Sake Toro', 'Tai', 'Tako', 'Tamago', 'Tobiko', 'Toro', 'Tsubugai', 'Umi Masu', 'Unagi', 'Uni']\", \"kind\": \"value\"}, \"pd\": {\"payload\": \"import pandas as pd\", \"kind\": \"import\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\"}, \"parents\": [], \"audits\": [{\"name\": \"assert_items_price_exceeds_threshold\", \"dialect\": \"\", \"skip\": false, \"blocking\": true, \"query\": \"SELECT * FROM @this_model WHERE price <= @price\", \"expressions\": []}], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376399, \"updated_ts\": 1680814376399, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"312608270\"}","8":"{\"name\": \"sushi.order_items\", \"fingerprint\": {\"data_hash\": \"653664599\", \"metadata_hash\": \"1960934702\", \"parent_data_hash\": \"3170724558\", \"parent_metadata_hash\": \"867324801\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.order_items\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"\", \"cron\": \"@daily\", \"batch_size\": 30, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"depends_on\": [\"sushi.items\", \"sushi.orders\"], \"columns\": {\"id\": \"INT\", \"order_id\": \"INT\", \"item_id\": \"INT\", \"quantity\": \"INT\", \"ds\": \"TEXT\"}, \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(id, order_id, item_id, quantity)\"}], [\"assert_order_items_quantity_exceeds_threshold\", {\"quantity\": \"0\"}]], \"expressions\": [], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n orders_table = context.table('sushi.orders')\\n items_table = context.table(ITEMS)\\n for dt in iter_dates(start, end):\\n orders = context.fetchdf(\\n f\\\"\\\"\\\"\\n SELECT *\\n FROM {orders_table}\\n WHERE ds = '{to_ds(dt)}'\\n \\\"\\\"\\\"\\n )\\n items = context.fetchdf(\\n f\\\"\\\"\\\"\\n SELECT *\\n FROM {items_table}\\n WHERE ds = '{to_ds(dt)}'\\n \\\"\\\"\\\"\\n )\\n for order_id in orders['id']:\\n n = random.randint(1, 5)\\n yield pd.DataFrame({'order_id': order_id, 'item_id': items.\\n sample(n=n)['id'], 'quantity': np.random.randint(1, 10, n),\\n 'ds': to_ds(dt)}).reset_index().rename(columns={'index': 'id'})\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/order_items.py\"}, \"ITEMS\": {\"payload\": \"'sushi.items'\", \"kind\": \"value\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}, \"pd\": {\"payload\": \"import pandas as pd\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\"}, \"parents\": [{\"name\": \"sushi.items\", \"identifier\": \"2957171338\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [{\"name\": \"assert_order_items_quantity_exceeds_threshold\", \"dialect\": \"\", \"skip\": false, \"blocking\": true, \"query\": \"SELECT * FROM @this_model WHERE quantity <= @quantity\", \"expressions\": []}], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376401, \"updated_ts\": 1680814376401, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"1015284155\"}","9":"{\"name\": \"sushi.orders\", \"fingerprint\": {\"data_hash\": \"1628439771\", \"metadata_hash\": \"2745052130\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.orders\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"\", \"cron\": \"@daily\", \"description\": \"Table of sushi orders.\", \"start\": \"2022-01-01\", \"batch_size\": 30, \"partitioned_by\": [], \"pre\": [], \"post\": [], \"depends_on\": [], \"columns\": {\"id\": \"INT\", \"customer_id\": \"INT\", \"waiter_id\": \"INT\", \"start_ts\": \"INT\", \"end_ts\": \"INT\", \"ds\": \"TEXT\"}, \"audits\": [], \"expressions\": [], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n dfs = []\\n for dt in iter_dates(start, end):\\n num_orders = random.randint(10, 30)\\n start_ts = [int((dt + timedelta(seconds=random.randint(0, 80000))).\\n timestamp()) for _ in range(num_orders)]\\n end_ts = [int(s + random.randint(0, 60 * 60)) for s in start_ts]\\n dfs.append(pd.DataFrame({'customer_id': random.choices(CUSTOMERS, k\\n =num_orders), 'waiter_id': random.choices(WAITERS, k=num_orders\\n ), 'start_ts': start_ts, 'end_ts': end_ts, 'ds': to_ds(dt)}).\\n reset_index().rename(columns={'index': 'id'}))\\n return pd.concat(dfs)\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/orders.py\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"pd\": {\"payload\": \"import pandas as pd # noqa: TID253\", \"kind\": \"import\"}, \"CUSTOMERS\": {\"payload\": \"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]\", \"kind\": \"value\"}, \"WAITERS\": {\"payload\": \"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\", \"kind\": \"value\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\"}, \"parents\": [], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814376402, \"updated_ts\": 1680814376402, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"indirect_versions\": {}, \"version\": \"925846788\"}","10":"{\"name\": \"sushi.waiter_as_customer_by_day\", \"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2824767713\", \"parent_metadata_hash\": \"1349779748\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_as_customer_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"expressions\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"table_properties\": {\"key\": \"'value'\"}, \"query\": \"SELECT w.ds AS ds, w.waiter_id AS waiter_id, wn.name AS waiter_name FROM sushi.waiters AS w JOIN sushi.customers AS c ON w.waiter_id = c.customer_id JOIN sushi.waiter_names AS wn ON w.waiter_id = wn.id\", \"source_type\": \"sql\"}, \"parents\": [{\"name\": \"sushi.waiters\", \"identifier\": \"3386889721\"}, {\"name\": \"sushi.waiter_names\", \"identifier\": \"1604207722\"}, {\"name\": \"sushi.customers\", \"identifier\": \"3148897116\"}, {\"name\": \"sushi.orders\", \"identifier\": \"3564161223\"}], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814464891, \"updated_ts\": 1680814464891, \"ttl\": \"in 1 week\", \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"1267397572\"}], \"indirect_versions\": {}, \"version\": \"3668757715\"}","11":"{\"name\": \"sushi.waiter_names\", \"fingerprint\": {\"data_hash\": \"4133862560\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"model\": {\"name\": \"sushi.waiter_names\", \"kind\": {\"name\": \"SEED\", \"path\": \"..\/seeds\/waiter_names.csv\", \"batch_size\": 5}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"pre\": [], \"post\": [], \"audits\": [], \"expressions\": [], \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"seed\": {\"content\": \"id,name\\n0,Toby\\n1,Tyson\\n2,Ryan\\n3,George\\n4,Chris\\n5,Max\\n6,Vincent\\n7,Iaroslav\\n8,Emma\\n9,Maia\\n10,Jim\\n\"}, \"source_type\": \"seed\"}, \"parents\": [], \"audits\": [], \"intervals\": [[1672531200000, 1673136000000]], \"dev_intervals\": [], \"created_ts\": 1680814464932, \"updated_ts\": 1680814464932, \"ttl\": \"in 1 week\", \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"2505706914\"}], \"indirect_versions\": {\"sushi.waiter_as_customer_by_day\": [{\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"1267397572\"}, {\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2824767713\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"3668757715\"}]}, \"version\": \"1204702829\", \"change_category\": 1}"}} \ No newline at end of file +{"name":{"0":"\"sushi\".\"waiter_as_customer_by_day\"","1":"\"sushi\".\"waiter_revenue_by_day\"","2":"\"sushi\".\"top_waiters\"","3":"\"sushi\".\"waiters\"","4":"\"sushi\".\"customers\"","5":"\"sushi\".\"waiter_names\"","6":"\"sushi\".\"customer_revenue_by_day\"","7":"\"sushi\".\"items\"","8":"\"sushi\".\"order_items\"","9":"\"sushi\".\"orders\"","10":"\"sushi\".\"waiter_as_customer_by_day\"","11":"\"sushi\".\"waiter_names\"","12":"\"sushi\".\"waiter_as_customer_by_day\"","13":"\"sushi\".\"waiter_names\"","14":"\"sushi\".\"customer_revenue_by_day\"","15":"\"sushi\".\"top_waiters\"","16":"\"sushi\".\"waiter_revenue_by_day\"","17":"\"sushi\".\"order_items\"","18":"\"sushi\".\"items\"","19":"\"sushi\".\"waiter_as_customer_by_day\"","20":"\"sushi\".\"waiter_names\"","21":"\"sushi\".\"customers\"","22":"\"sushi\".\"waiters\"","23":"\"sushi\".\"orders\""},"identifier":{"0":"1281222509","1":"1609279380","2":"599861134","3":"3386889721","4":"3148897116","5":"3233103305","6":"1308408370","7":"2957171338","8":"1806777563","9":"3564161223","10":"1084858582","11":"1604207722","12":"3998224796","13":"2725136291","14":"3566886383","15":"129039563","16":"2175947464","17":"1422946820","18":"3721860967","19":"1341746752","20":"1609854746","21":"1461038955","22":"4123940212","23":"1250207606"},"version":{"0":"1267397572","1":"2695875565","2":"3010914162","3":"2059227798","4":"2359719298","5":"2505706914","6":"1291364031","7":"312608270","8":"1015284155","9":"925846788","10":"3668757715","11":"3668757715","12":"3668757715","13":"1204702829","14":"1291364031","15":"3010914162","16":"2695875565","17":"1015284155","18":"312608270","19":"1267397572","20":"2505706914","21":"2359719298","22":"2059227798","23":"925846788"},"snapshot":{"0":"{\"name\": \"\\\"sushi\\\".\\\"waiter_as_customer_by_day\\\"\", \"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"waiters\\\"\", \"identifier\": \"3386889721\"}, {\"name\": \"\\\"sushi\\\".\\\"waiter_names\\\"\", \"identifier\": \"3233103305\"}, {\"name\": \"\\\"sushi\\\".\\\"customers\\\"\", \"identifier\": \"3148897116\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"3564161223\"}], \"created_ts\": 1680814376348, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"version\": \"1267397572\", \"node\": {\"name\": \"sushi.waiter_as_customer_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT w.ds AS ds, w.waiter_id AS waiter_id, wn.name AS waiter_name FROM sushi.waiters AS w JOIN sushi.customers AS c ON w.waiter_id = c.customer_id JOIN sushi.waiter_names AS wn ON w.waiter_id = wn.id\", \"source_type\": \"sql\", \"project\": \"\", \"default_catalog\": null}, \"change_category\": 4, \"base_table_name_override\": \"sushi.waiter_as_customer_by_day\"}","1":"{\"name\": \"\\\"sushi\\\".\\\"waiter_revenue_by_day\\\"\", \"fingerprint\": {\"data_hash\": \"2443934302\", \"metadata_hash\": \"2904050331\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"order_items\\\"\", \"identifier\": \"1806777563\"}, {\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"identifier\": \"2957171338\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"3564161223\"}], \"created_ts\": 1680814376361, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"version\": \"2695875565\", \"node\": {\"name\": \"sushi.waiter_revenue_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}, \"batch_size\": 10}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"Table of revenue generated by waiters by day.\", \"partitioned_by\": [], \"audits\": [[\"number_of_rows\", {\"threshold\": \"0\"}]], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT CAST(o.waiter_id AS INT) AS waiter_id \/* Waiter id *\/, CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue \/* Revenue from orders taken by this waiter *\/, CAST(o.ds AS TEXT) AS ds \/* Date *\/ FROM sushi.orders AS o LEFT JOIN sushi.order_items AS oi ON o.id = oi.order_id AND o.ds = oi.ds LEFT JOIN sushi.items AS i ON oi.item_id = i.id AND oi.ds = i.ds WHERE o.ds BETWEEN @start_ds AND @end_ds GROUP BY o.waiter_id, o.ds\", \"source_type\": \"sql\", \"project\": \"\", \"default_catalog\": null}, \"change_category\": 4, \"base_table_name_override\": \"sushi.waiter_revenue_by_day\"}","2":"{\"name\": \"\\\"sushi\\\".\\\"top_waiters\\\"\", \"fingerprint\": {\"data_hash\": \"2891807529\", \"metadata_hash\": \"3392493998\", \"parent_data_hash\": \"1940707936\", \"parent_metadata_hash\": \"1276363398\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"waiter_revenue_by_day\\\"\", \"identifier\": \"1609279380\"}], \"created_ts\": 1680814376384, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"version\": \"3010914162\", \"node\": {\"name\": \"sushi.top_waiters\", \"kind\": {\"name\": \"VIEW\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"View of top waiters.\", \"partitioned_by\": [], \"audits\": [[\"unique_values\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT CAST(waiter_id AS INT) AS waiter_id, CAST(revenue AS DOUBLE) AS revenue FROM sushi.waiter_revenue_by_day WHERE ds = (SELECT MAX(ds) FROM sushi.waiter_revenue_by_day) ORDER BY revenue DESC LIMIT 10\", \"source_type\": \"sql\", \"project\": \"\", \"default_catalog\": null}, \"change_category\": 4, \"base_table_name_override\": \"sushi.top_waiters\"}","3":"{\"name\": \"\\\"sushi\\\".\\\"waiters\\\"\", \"fingerprint\": {\"data_hash\": \"3501061139\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"3564161223\"}], \"created_ts\": 1680814376387, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"version\": \"2059227798\", \"node\": {\"name\": \"sushi.waiters\", \"kind\": {\"name\": \"EMBEDDED\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"audits\": [], \"python_env\": {\"incremental_by_ds\": {\"payload\": \"def incremental_by_ds(evaluator, column):\\n expression = evaluator.transform(exp.Between(this=column, low=MacroVar(\\n this='start_ds'), high=MacroVar(this='end_ds')))\\n if not isinstance(expression, exp.Expression):\\n raise MacroEvalError(\\n f'Return type is {type(expression)}, expected exp.Expression')\\n return expression\", \"kind\": \"definition\", \"name\": \"incremental_by_ds\", \"path\": \"macros\/macros.py\"}, \"exp\": {\"payload\": \"import sqlglot.expressions as exp\", \"kind\": \"import\"}, \"MacroVar\": {\"payload\": \"from sqlmesh.core.dialect import MacroVar\", \"kind\": \"import\"}, \"MacroEvalError\": {\"payload\": \"from sqlmesh.utils.errors import MacroEvalError\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT DISTINCT CAST(waiter_id AS INT) AS waiter_id, CAST(ds AS TEXT) AS ds FROM sushi.orders AS o WHERE @incremental_by_ds(ds)\", \"source_type\": \"sql\", \"project\": \"\", \"default_catalog\": null}, \"change_category\": 4, \"base_table_name_override\": \"sushi.waiters\"}","4":"{\"name\": \"\\\"sushi\\\".\\\"customers\\\"\", \"fingerprint\": {\"data_hash\": \"3553985282\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"777615193\", \"parent_metadata_hash\": \"2042613269\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"3564161223\"}], \"created_ts\": 1680814376388, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"version\": \"2359719298\", \"node\": {\"name\": \"sushi.customers\", \"kind\": {\"name\": \"FULL\"}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"audits\": [], \"python_env\": {\"noop\": {\"payload\": \"def noop(context, start, end, latest, **kwargs):\\n pass\", \"kind\": \"definition\", \"name\": \"noop\", \"path\": \"hooks\/hooks.py\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"SELECT DISTINCT CAST(customer_id AS INT) AS customer_id FROM sushi.orders AS o\", \"source_type\": \"sql\", \"project\": \"\", \"default_catalog\": null}, \"change_category\": 4, \"base_table_name_override\": \"sushi.customers\"}","5":"{\"name\": \"\\\"sushi\\\".\\\"waiter_names\\\"\", \"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [], \"created_ts\": 1680814376389, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"version\": \"2505706914\", \"node\": {\"name\": \"sushi.waiter_names\", \"kind\": {\"name\": \"SEED\", \"path\": \"..\/seeds\/waiter_names.csv\", \"batch_size\": 5}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"audits\": [], \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"seed\": {\"content\": \"id,name\\n0,Toby\\n1,Tyson\\n2,Ryan\\n3,George\\n4,Chris\\n5,Max\\n6,Vincent\\n7,Iaroslav\\n8,Emma\\n9,Maia\\n\"}, \"source_type\": \"seed\", \"project\": \"\", \"default_catalog\": null}, \"change_category\": 4, \"base_table_name_override\": \"sushi.waiter_names\"}","6":"{\"name\": \"\\\"sushi\\\".\\\"customer_revenue_by_day\\\"\", \"fingerprint\": {\"data_hash\": \"2657552867\", \"metadata_hash\": \"129771006\", \"parent_data_hash\": \"764310396\", \"parent_metadata_hash\": \"3147731239\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"order_items\\\"\", \"identifier\": \"1806777563\"}, {\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"identifier\": \"2957171338\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"3564161223\"}], \"created_ts\": 1680814376391, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"version\": \"1291364031\", \"node\": {\"name\": \"sushi.customer_revenue_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}, \"batch_size\": 10}, \"dialect\": \"hive\", \"cron\": \"@daily\", \"owner\": \"jen\", \"description\": \"Table of revenue from customers by day.\", \"partitioned_by\": [], \"audits\": [], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"query\": \"JINJA_QUERY_BEGIN;\\nWITH order_total AS (SELECT oi.order_id AS order_id, SUM(oi.quantity * i.price) AS total, oi.ds AS ds FROM sushi.order_items AS oi LEFT JOIN sushi.items AS i ON oi.item_id = i.id AND oi.ds = i.ds WHERE oi.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' GROUP BY oi.order_id, oi.ds) SELECT CAST(o.customer_id AS INT) AS customer_id \/* Customer id *\/, CAST(SUM(ot.total) AS DOUBLE) AS revenue \/* Revenue from orders made by this customer *\/, CAST(o.ds AS TEXT) AS ds \/* Date *\/ FROM sushi.orders AS o LEFT JOIN order_total AS ot ON o.id = ot.order_id AND o.ds = ot.ds WHERE o.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' GROUP BY o.customer_id, o.ds\\nJINJA_END;\", \"source_type\": \"sql\", \"project\": \"\", \"default_catalog\": null}, \"change_category\": 4, \"base_table_name_override\": \"sushi.customer_revenue_by_day\"}","7":"{\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"fingerprint\": {\"data_hash\": \"1960378930\", \"metadata_hash\": \"2900807542\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [], \"created_ts\": 1680814376399, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"version\": \"312608270\", \"node\": {\"name\": \"sushi.items\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}, \"batch_size\": 30}, \"dialect\": \"\", \"cron\": \"@daily\", \"start\": \"Jan 1 2022\", \"partitioned_by\": [], \"depends_on\": [], \"columns\": {\"id\": \"INT\", \"name\": \"TEXT\", \"price\": \"DOUBLE\", \"ds\": \"TEXT\"}, \"audits\": [[\"accepted_values\", {\"column\": \"name\", \"values\": \"ARRAY('Ahi', 'Aji', 'Amaebi', 'Anago', 'Aoyagi', 'Bincho', 'Katsuo', 'Ebi', 'Escolar', 'Hamachi', 'Hamachi Toro', 'Hirame', 'Hokigai', 'Hotate', 'Ika', 'Ikura', 'Iwashi', 'Kani', 'Kanpachi', 'Maguro', 'Saba', 'Sake', 'Sake Toro', 'Tai', 'Tako', 'Tamago', 'Tobiko', 'Toro', 'Tsubugai', 'Umi Masu', 'Unagi', 'Uni')\"}], [\"not_null\", {\"columns\": \"ARRAY(name, price)\"}], [\"assert_items_price_exceeds_threshold\", {\"price\": \"0\"}]], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n dfs = []\\n for dt in iter_dates(start, end):\\n num_items = random.randint(10, len(ITEMS))\\n dfs.append(pd.DataFrame({'name': random.sample(ITEMS, num_items),\\n 'price': np.random.uniform(3.0, 10.0, size=num_items).round(2),\\n 'ds': to_ds(dt)}).reset_index().rename(columns={'index': 'id'}))\\n return pd.concat(dfs)\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/items.py\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"ITEMS\": {\"payload\": \"['Ahi', 'Aji', 'Amaebi', 'Anago', 'Aoyagi', 'Bincho', 'Katsuo', 'Ebi', 'Escolar', 'Hamachi', 'Hamachi Toro', 'Hirame', 'Hokigai', 'Hotate', 'Ika', 'Ikura', 'Iwashi', 'Kani', 'Kanpachi', 'Maguro', 'Saba', 'Sake', 'Sake Toro', 'Tai', 'Tako', 'Tamago', 'Tobiko', 'Toro', 'Tsubugai', 'Umi Masu', 'Unagi', 'Uni']\", \"kind\": \"value\"}, \"pd\": {\"payload\": \"import pandas as pd\", \"kind\": \"import\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\", \"project\": \"\", \"default_catalog\": null, \"audit_definitions\": {\"assert_items_price_exceeds_threshold\": {\"name\": \"assert_items_price_exceeds_threshold\", \"dialect\": \"\", \"skip\": false, \"blocking\": true, \"query\": \"SELECT * FROM @this_model WHERE price <= @price\", \"expressions\": []}}}, \"change_category\": 4, \"base_table_name_override\": \"sushi.items\"}","8":"{\"name\": \"\\\"sushi\\\".\\\"order_items\\\"\", \"fingerprint\": {\"data_hash\": \"653664599\", \"metadata_hash\": \"1960934702\", \"parent_data_hash\": \"3170724558\", \"parent_metadata_hash\": \"867324801\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"items\\\"\", \"identifier\": \"2957171338\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"3564161223\"}], \"created_ts\": 1680814376401, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"version\": \"1015284155\", \"node\": {\"name\": \"sushi.order_items\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}, \"batch_size\": 30}, \"dialect\": \"\", \"cron\": \"@daily\", \"partitioned_by\": [], \"depends_on\": [\"\\\"sushi\\\".\\\"items\\\"\", \"\\\"sushi\\\".\\\"orders\\\"\"], \"columns\": {\"id\": \"INT\", \"order_id\": \"INT\", \"item_id\": \"INT\", \"quantity\": \"INT\", \"ds\": \"TEXT\"}, \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(id, order_id, item_id, quantity)\"}], [\"assert_order_items_quantity_exceeds_threshold\", {\"quantity\": \"0\"}]], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n orders_table = context.table('sushi.orders')\\n items_table = context.table(ITEMS)\\n for dt in iter_dates(start, end):\\n orders = context.fetchdf(\\n f\\\"\\\"\\\"\\n SELECT *\\n FROM {orders_table}\\n WHERE ds = '{to_ds(dt)}'\\n \\\"\\\"\\\"\\n )\\n items = context.fetchdf(\\n f\\\"\\\"\\\"\\n SELECT *\\n FROM {items_table}\\n WHERE ds = '{to_ds(dt)}'\\n \\\"\\\"\\\"\\n )\\n for order_id in orders['id']:\\n n = random.randint(1, 5)\\n yield pd.DataFrame({'order_id': order_id, 'item_id': items.\\n sample(n=n)['id'], 'quantity': np.random.randint(1, 10, n),\\n 'ds': to_ds(dt)}).reset_index().rename(columns={'index': 'id'})\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/order_items.py\"}, \"ITEMS\": {\"payload\": \"'sushi.items'\", \"kind\": \"value\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}, \"pd\": {\"payload\": \"import pandas as pd\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\", \"project\": \"\", \"default_catalog\": null, \"audit_definitions\": {\"assert_order_items_quantity_exceeds_threshold\": {\"name\": \"assert_order_items_quantity_exceeds_threshold\", \"dialect\": \"\", \"skip\": false, \"blocking\": true, \"query\": \"SELECT * FROM @this_model WHERE quantity <= @quantity\", \"expressions\": []}}}, \"change_category\": 4, \"base_table_name_override\": \"sushi.order_items\"}","9":"{\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"fingerprint\": {\"data_hash\": \"1628439771\", \"metadata_hash\": \"2745052130\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [], \"created_ts\": 1680814376402, \"ttl\": \"in 1 week\", \"previous_versions\": [], \"version\": \"925846788\", \"node\": {\"name\": \"sushi.orders\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}, \"batch_size\": 30}, \"dialect\": \"\", \"cron\": \"@daily\", \"description\": \"Table of sushi orders.\", \"start\": \"2022-01-01\", \"partitioned_by\": [], \"depends_on\": [], \"columns\": {\"id\": \"INT\", \"customer_id\": \"INT\", \"waiter_id\": \"INT\", \"start_ts\": \"INT\", \"end_ts\": \"INT\", \"ds\": \"TEXT\"}, \"audits\": [], \"python_env\": {\"execute\": {\"payload\": \"def execute(context, start, end, latest, **kwargs):\\n dfs = []\\n for dt in iter_dates(start, end):\\n num_orders = random.randint(10, 30)\\n start_ts = [int((dt + timedelta(seconds=random.randint(0, 80000))).\\n timestamp()) for _ in range(num_orders)]\\n end_ts = [int(s + random.randint(0, 60 * 60)) for s in start_ts]\\n dfs.append(pd.DataFrame({'customer_id': random.choices(CUSTOMERS, k\\n =num_orders), 'waiter_id': random.choices(WAITERS, k=num_orders\\n ), 'start_ts': start_ts, 'end_ts': end_ts, 'ds': to_ds(dt)}).\\n reset_index().rename(columns={'index': 'id'}))\\n return pd.concat(dfs)\", \"kind\": \"definition\", \"name\": \"execute\", \"path\": \"models\/orders.py\"}, \"iter_dates\": {\"payload\": \"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\", \"kind\": \"definition\", \"name\": \"iter_dates\", \"path\": \"helper.py\"}, \"timedelta\": {\"payload\": \"from datetime import timedelta\", \"kind\": \"import\"}, \"set_seed\": {\"payload\": \"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\", \"kind\": \"definition\", \"name\": \"set_seed\", \"path\": \"helper.py\"}, \"random\": {\"payload\": \"import random\", \"kind\": \"import\"}, \"np\": {\"payload\": \"import numpy as np\", \"kind\": \"import\"}, \"pd\": {\"payload\": \"import pandas as pd\", \"kind\": \"import\"}, \"CUSTOMERS\": {\"payload\": \"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]\", \"kind\": \"value\"}, \"WAITERS\": {\"payload\": \"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\", \"kind\": \"value\"}, \"to_ds\": {\"payload\": \"from sqlmesh.utils.date import to_ds\", \"kind\": \"import\"}}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"entrypoint\": \"execute\", \"source_type\": \"python\", \"project\": \"\", \"default_catalog\": null}, \"change_category\": 4, \"base_table_name_override\": \"sushi.orders\"}","10":"{\"name\": \"\\\"sushi\\\".\\\"waiter_as_customer_by_day\\\"\", \"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2824767713\", \"parent_metadata_hash\": \"1349779748\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [{\"name\": \"\\\"sushi\\\".\\\"waiters\\\"\", \"identifier\": \"3386889721\"}, {\"name\": \"\\\"sushi\\\".\\\"waiter_names\\\"\", \"identifier\": \"1604207722\"}, {\"name\": \"\\\"sushi\\\".\\\"customers\\\"\", \"identifier\": \"3148897116\"}, {\"name\": \"\\\"sushi\\\".\\\"orders\\\"\", \"identifier\": \"3564161223\"}], \"created_ts\": 1680814464891, \"ttl\": \"in 1 week\", \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"486172035\", \"metadata_hash\": \"1992853678\", \"parent_data_hash\": \"2154574190\", \"parent_metadata_hash\": \"1349779748\"}, \"version\": \"1267397572\"}], \"version\": \"3668757715\", \"node\": {\"name\": \"sushi.waiter_as_customer_by_day\", \"kind\": {\"name\": \"INCREMENTAL_BY_TIME_RANGE\", \"time_column\": {\"column\": \"ds\", \"format\": \"%Y-%m-%d\"}}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"audits\": [[\"not_null\", {\"columns\": \"ARRAY(waiter_id)\"}]], \"python_env\": {}, \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"table_properties\": \"('key' = 'value')\", \"query\": \"SELECT w.ds AS ds, w.waiter_id AS waiter_id, wn.name AS waiter_name FROM sushi.waiters AS w JOIN sushi.customers AS c ON w.waiter_id = c.customer_id JOIN sushi.waiter_names AS wn ON w.waiter_id = wn.id\", \"source_type\": \"sql\", \"project\": \"\", \"default_catalog\": null}, \"change_category\": 4, \"base_table_name_override\": \"sushi.waiter_as_customer_by_day\"}","11":"{\"name\": \"\\\"sushi\\\".\\\"waiter_names\\\"\", \"fingerprint\": {\"data_hash\": \"4133862560\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"physical_schema\": \"sqlmesh\", \"parents\": [], \"created_ts\": 1680814464932, \"ttl\": \"in 1 week\", \"previous_versions\": [{\"fingerprint\": {\"data_hash\": \"1876476880\", \"metadata_hash\": \"570478986\", \"parent_data_hash\": \"0\", \"parent_metadata_hash\": \"0\"}, \"version\": \"2505706914\"}], \"version\": \"1204702829\", \"change_category\": 1, \"node\": {\"name\": \"sushi.waiter_names\", \"kind\": {\"name\": \"SEED\", \"path\": \"..\/seeds\/waiter_names.csv\", \"batch_size\": 5}, \"dialect\": \"duckdb\", \"cron\": \"@daily\", \"owner\": \"jen\", \"partitioned_by\": [], \"audits\": [], \"jinja_macros\": {\"packages\": {}, \"root_macros\": {}, \"global_objs\": {}}, \"seed\": {\"content\": \"id,name\\n0,Toby\\n1,Tyson\\n2,Ryan\\n3,George\\n4,Chris\\n5,Max\\n6,Vincent\\n7,Iaroslav\\n8,Emma\\n9,Maia\\n10,Jim\\n\"}, \"source_type\": \"seed\", \"project\": \"\", \"default_catalog\": null}, \"base_table_name_override\": \"sushi.waiter_names\"}","12":"{\"name\":\"\\\"sushi\\\".\\\"waiter_as_customer_by_day\\\"\",\"temp_version\":\"3668757715\",\"change_category\":4,\"fingerprint\":{\"data_hash\":\"1936268024\",\"metadata_hash\":\"2088684978\",\"parent_data_hash\":\"3055854652\",\"parent_metadata_hash\":\"665080906\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"486172035\",\"metadata_hash\":\"1992853678\",\"parent_data_hash\":\"2154574190\",\"parent_metadata_hash\":\"1349779748\"},\"version\":\"1267397572\"},{\"fingerprint\":{\"data_hash\":\"486172035\",\"metadata_hash\":\"1992853678\",\"parent_data_hash\":\"2824767713\",\"parent_metadata_hash\":\"1349779748\"},\"version\":\"3668757715\",\"change_category\":4,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.waiter_as_customer_by_day\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.waiter_as_customer_by_day\",\"project\":\"\",\"owner\":\"jen\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"duckdb\",\"kind\":{\"name\":\"INCREMENTAL_BY_TIME_RANGE\",\"on_destructive_change\":\"ERROR\",\"dialect\":\"duckdb\",\"forward_only\":false,\"disable_restatement\":false,\"time_column\":{\"column\":\"ds\",\"format\":\"%Y-%m-%d\"}},\"partitioned_by\":[],\"clustered_by\":[],\"audits\":[[\"not_null\",{\"columns\":\"ARRAY(waiter_id)\"}]],\"grains\":[],\"references\":[],\"physical_properties\":\"('key' = 'value')\",\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"query\":\"SELECT w.ds AS ds, w.waiter_id AS waiter_id, wn.name AS waiter_name FROM sushi.waiters AS w JOIN sushi.customers AS c ON w.waiter_id = c.customer_id JOIN sushi.waiter_names AS wn ON w.waiter_id = wn.id\",\"source_type\":\"sql\"},\"parents\":[{\"name\":\"\\\"sushi\\\".\\\"waiter_names\\\"\",\"identifier\":\"2725136291\"},{\"name\":\"\\\"sushi\\\".\\\"waiters\\\"\",\"identifier\":\"4123940212\"},{\"name\":\"\\\"sushi\\\".\\\"orders\\\"\",\"identifier\":\"1250207606\"},{\"name\":\"\\\"sushi\\\".\\\"customers\\\"\",\"identifier\":\"1461038955\"}],\"created_ts\":1680814464891,\"ttl\":\"in 1 week\",\"version\":\"3668757715\",\"migrated\":true}","13":"{\"name\":\"\\\"sushi\\\".\\\"waiter_names\\\"\",\"temp_version\":\"1204702829\",\"change_category\":1,\"fingerprint\":{\"data_hash\":\"1437406487\",\"metadata_hash\":\"3468846895\",\"parent_data_hash\":\"0\",\"parent_metadata_hash\":\"0\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"1876476880\",\"metadata_hash\":\"570478986\",\"parent_data_hash\":\"0\",\"parent_metadata_hash\":\"0\"},\"version\":\"2505706914\"},{\"fingerprint\":{\"data_hash\":\"4133862560\",\"metadata_hash\":\"570478986\",\"parent_data_hash\":\"0\",\"parent_metadata_hash\":\"0\"},\"version\":\"1204702829\",\"change_category\":1,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.waiter_names\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.waiter_names\",\"project\":\"\",\"owner\":\"jen\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"duckdb\",\"kind\":{\"name\":\"SEED\",\"path\":\"..\/seeds\/waiter_names.csv\",\"batch_size\":5},\"partitioned_by\":[],\"clustered_by\":[],\"audits\":[],\"grains\":[],\"references\":[],\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"seed\":{\"content\":\"\"},\"column_hashes\":{\"id\":\"3061821109\",\"name\":\"2706736258\"},\"derived_columns_to_types\":{\"id\":\"BIGINT\",\"name\":\"TEXT\"},\"is_hydrated\":false,\"source_type\":\"seed\"},\"parents\":[],\"created_ts\":1680814464932,\"ttl\":\"in 1 week\",\"version\":\"1204702829\",\"migrated\":true}","14":"{\"name\":\"\\\"sushi\\\".\\\"customer_revenue_by_day\\\"\",\"temp_version\":\"1291364031\",\"change_category\":4,\"fingerprint\":{\"data_hash\":\"131732542\",\"metadata_hash\":\"1368842087\",\"parent_data_hash\":\"2738168331\",\"parent_metadata_hash\":\"1795276494\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"2657552867\",\"metadata_hash\":\"129771006\",\"parent_data_hash\":\"764310396\",\"parent_metadata_hash\":\"3147731239\"},\"version\":\"1291364031\",\"change_category\":4,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.customer_revenue_by_day\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.customer_revenue_by_day\",\"project\":\"\",\"description\":\"Table of revenue from customers by day.\",\"owner\":\"jen\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"hive\",\"kind\":{\"name\":\"INCREMENTAL_BY_TIME_RANGE\",\"on_destructive_change\":\"ERROR\",\"dialect\":\"hive\",\"batch_size\":10,\"forward_only\":false,\"disable_restatement\":false,\"time_column\":{\"column\":\"ds\",\"format\":\"%Y-%m-%d\"}},\"partitioned_by\":[],\"clustered_by\":[],\"audits\":[],\"grains\":[],\"references\":[],\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"query\":\"JINJA_QUERY_BEGIN;\\nWITH order_total AS (SELECT oi.order_id AS order_id, SUM(oi.quantity * i.price) AS total, oi.ds AS ds FROM sushi.order_items AS oi LEFT JOIN sushi.items AS i ON oi.item_id = i.id AND oi.ds = i.ds WHERE oi.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' GROUP BY oi.order_id, oi.ds) SELECT CAST(o.customer_id AS INT) AS customer_id \/* Customer id *\/, CAST(SUM(ot.total) AS DOUBLE) AS revenue \/* Revenue from orders made by this customer *\/, CAST(o.ds AS TEXT) AS ds \/* Date *\/ FROM sushi.orders AS o LEFT JOIN order_total AS ot ON o.id = ot.order_id AND o.ds = ot.ds WHERE o.ds BETWEEN '{{ start_ds }}' AND '{{ end_ds }}' GROUP BY o.customer_id, o.ds\\nJINJA_END\",\"source_type\":\"sql\"},\"parents\":[{\"name\":\"\\\"sushi\\\".\\\"items\\\"\",\"identifier\":\"3721860967\"},{\"name\":\"\\\"sushi\\\".\\\"orders\\\"\",\"identifier\":\"1250207606\"},{\"name\":\"\\\"sushi\\\".\\\"order_items\\\"\",\"identifier\":\"1422946820\"}],\"created_ts\":1680814376391,\"ttl\":\"in 1 week\",\"version\":\"1291364031\",\"migrated\":true}","15":"{\"name\":\"\\\"sushi\\\".\\\"top_waiters\\\"\",\"temp_version\":\"3010914162\",\"change_category\":4,\"fingerprint\":{\"data_hash\":\"4131026946\",\"metadata_hash\":\"154190563\",\"parent_data_hash\":\"929243525\",\"parent_metadata_hash\":\"2366450878\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"2891807529\",\"metadata_hash\":\"3392493998\",\"parent_data_hash\":\"1940707936\",\"parent_metadata_hash\":\"1276363398\"},\"version\":\"3010914162\",\"change_category\":4,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.top_waiters\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.top_waiters\",\"project\":\"\",\"description\":\"View of top waiters.\",\"owner\":\"jen\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"duckdb\",\"kind\":{\"name\":\"VIEW\",\"materialized\":false},\"partitioned_by\":[],\"clustered_by\":[],\"audits\":[[\"unique_values\",{\"columns\":\"ARRAY(waiter_id)\"}]],\"grains\":[],\"references\":[],\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"query\":\"SELECT CAST(waiter_id AS INT) AS waiter_id, CAST(revenue AS DOUBLE) AS revenue FROM sushi.waiter_revenue_by_day WHERE ds = (SELECT MAX(ds) FROM sushi.waiter_revenue_by_day) ORDER BY revenue DESC LIMIT 10\",\"source_type\":\"sql\"},\"parents\":[{\"name\":\"\\\"sushi\\\".\\\"waiter_revenue_by_day\\\"\",\"identifier\":\"2175947464\"}],\"created_ts\":1680814376384,\"ttl\":\"in 1 week\",\"version\":\"3010914162\",\"migrated\":true}","16":"{\"name\":\"\\\"sushi\\\".\\\"waiter_revenue_by_day\\\"\",\"temp_version\":\"2695875565\",\"change_category\":4,\"fingerprint\":{\"data_hash\":\"2224089837\",\"metadata_hash\":\"2504236462\",\"parent_data_hash\":\"2738168331\",\"parent_metadata_hash\":\"1795276494\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"2443934302\",\"metadata_hash\":\"2904050331\",\"parent_data_hash\":\"764310396\",\"parent_metadata_hash\":\"3147731239\"},\"version\":\"2695875565\",\"change_category\":4,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.waiter_revenue_by_day\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.waiter_revenue_by_day\",\"project\":\"\",\"description\":\"Table of revenue generated by waiters by day.\",\"owner\":\"jen\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"duckdb\",\"kind\":{\"name\":\"INCREMENTAL_BY_TIME_RANGE\",\"on_destructive_change\":\"ERROR\",\"dialect\":\"duckdb\",\"batch_size\":10,\"forward_only\":false,\"disable_restatement\":false,\"time_column\":{\"column\":\"ds\",\"format\":\"%Y-%m-%d\"}},\"partitioned_by\":[],\"clustered_by\":[],\"audits\":[[\"number_of_rows\",{\"threshold\":\"0\"}]],\"grains\":[],\"references\":[],\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"query\":\"SELECT CAST(o.waiter_id AS INT) AS waiter_id \/* Waiter id *\/, CAST(SUM(oi.quantity * i.price) AS DOUBLE) AS revenue \/* Revenue from orders taken by this waiter *\/, CAST(o.ds AS TEXT) AS ds \/* Date *\/ FROM sushi.orders AS o LEFT JOIN sushi.order_items AS oi ON o.id = oi.order_id AND o.ds = oi.ds LEFT JOIN sushi.items AS i ON oi.item_id = i.id AND oi.ds = i.ds WHERE o.ds BETWEEN @start_ds AND @end_ds GROUP BY o.waiter_id, o.ds\",\"source_type\":\"sql\"},\"parents\":[{\"name\":\"\\\"sushi\\\".\\\"items\\\"\",\"identifier\":\"3721860967\"},{\"name\":\"\\\"sushi\\\".\\\"orders\\\"\",\"identifier\":\"1250207606\"},{\"name\":\"\\\"sushi\\\".\\\"order_items\\\"\",\"identifier\":\"1422946820\"}],\"created_ts\":1680814376361,\"ttl\":\"in 1 week\",\"version\":\"2695875565\",\"migrated\":true}","17":"{\"name\":\"\\\"sushi\\\".\\\"order_items\\\"\",\"temp_version\":\"1015284155\",\"change_category\":4,\"fingerprint\":{\"data_hash\":\"4010068827\",\"metadata_hash\":\"799196655\",\"parent_data_hash\":\"2342431947\",\"parent_metadata_hash\":\"1746080605\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"653664599\",\"metadata_hash\":\"1960934702\",\"parent_data_hash\":\"3170724558\",\"parent_metadata_hash\":\"867324801\"},\"version\":\"1015284155\",\"change_category\":4,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.order_items\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.order_items\",\"project\":\"\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"\",\"kind\":{\"name\":\"INCREMENTAL_BY_TIME_RANGE\",\"on_destructive_change\":\"ERROR\",\"dialect\":\"\",\"batch_size\":30,\"forward_only\":false,\"disable_restatement\":false,\"time_column\":{\"column\":\"ds\",\"format\":\"%Y-%m-%d\"}},\"partitioned_by\":[],\"clustered_by\":[],\"depends_on\":[\"\\\"sushi\\\".\\\"items\\\"\",\"\\\"sushi\\\".\\\"orders\\\"\"],\"columns\":{\"id\":\"INT\",\"order_id\":\"INT\",\"item_id\":\"INT\",\"quantity\":\"INT\",\"ds\":\"TEXT\"},\"audits\":[[\"not_null\",{\"columns\":\"ARRAY(id, order_id, item_id, quantity)\"}],[\"assert_order_items_quantity_exceeds_threshold\",{\"quantity\":\"0\"}]],\"grains\":[],\"references\":[],\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{\"execute\":{\"payload\":\"def execute(context, start, end, latest, **kwargs):\\n orders_table = context.table('sushi.orders')\\n items_table = context.table(ITEMS)\\n for dt in iter_dates(start, end):\\n orders = context.fetchdf(\\n f\\\"\\\"\\\"\\n SELECT *\\n FROM {orders_table}\\n WHERE ds = '{to_ds(dt)}'\\n \\\"\\\"\\\"\\n )\\n items = context.fetchdf(\\n f\\\"\\\"\\\"\\n SELECT *\\n FROM {items_table}\\n WHERE ds = '{to_ds(dt)}'\\n \\\"\\\"\\\"\\n )\\n for order_id in orders['id']:\\n n = random.randint(1, 5)\\n yield pd.DataFrame({'order_id': order_id, 'item_id': items.\\n sample(n=n)['id'], 'quantity': np.random.randint(1, 10, n),\\n 'ds': to_ds(dt)}).reset_index().rename(columns={'index': 'id'})\",\"kind\":\"definition\",\"name\":\"execute\",\"path\":\"models\/order_items.py\"},\"ITEMS\":{\"payload\":\"'sushi.items'\",\"kind\":\"value\"},\"iter_dates\":{\"payload\":\"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\",\"kind\":\"definition\",\"name\":\"iter_dates\",\"path\":\"helper.py\"},\"timedelta\":{\"payload\":\"from datetime import timedelta\",\"kind\":\"import\"},\"set_seed\":{\"payload\":\"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\",\"kind\":\"definition\",\"name\":\"set_seed\",\"path\":\"helper.py\"},\"random\":{\"payload\":\"import random\",\"kind\":\"import\"},\"np\":{\"payload\":\"import numpy as np\",\"kind\":\"import\"},\"to_ds\":{\"payload\":\"from sqlmesh.utils.date import to_ds\",\"kind\":\"import\"},\"pd\":{\"payload\":\"import pandas as pd\",\"kind\":\"import\"}},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{\"assert_order_items_quantity_exceeds_threshold\":{\"name\":\"assert_order_items_quantity_exceeds_threshold\",\"dialect\":\"\",\"skip\":false,\"blocking\":true,\"standalone\":false,\"query\":\"SELECT * FROM @this_model WHERE quantity <= @quantity\",\"defaults\":{},\"expressions\":[],\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]}}},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"entrypoint\":\"execute\",\"source_type\":\"python\"},\"parents\":[{\"name\":\"\\\"sushi\\\".\\\"items\\\"\",\"identifier\":\"3721860967\"},{\"name\":\"\\\"sushi\\\".\\\"orders\\\"\",\"identifier\":\"1250207606\"}],\"created_ts\":1680814376401,\"ttl\":\"in 1 week\",\"version\":\"1015284155\",\"migrated\":true}","18":"{\"name\":\"\\\"sushi\\\".\\\"items\\\"\",\"temp_version\":\"312608270\",\"change_category\":4,\"fingerprint\":{\"data_hash\":\"1862622614\",\"metadata_hash\":\"3651173237\",\"parent_data_hash\":\"0\",\"parent_metadata_hash\":\"0\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"1960378930\",\"metadata_hash\":\"2900807542\",\"parent_data_hash\":\"0\",\"parent_metadata_hash\":\"0\"},\"version\":\"312608270\",\"change_category\":4,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.items\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.items\",\"project\":\"\",\"start\":\"Jan 1 2022\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"\",\"kind\":{\"name\":\"INCREMENTAL_BY_TIME_RANGE\",\"on_destructive_change\":\"ERROR\",\"dialect\":\"\",\"batch_size\":30,\"forward_only\":false,\"disable_restatement\":false,\"time_column\":{\"column\":\"ds\",\"format\":\"%Y-%m-%d\"}},\"partitioned_by\":[],\"clustered_by\":[],\"depends_on\":[],\"columns\":{\"id\":\"INT\",\"name\":\"TEXT\",\"price\":\"DOUBLE\",\"ds\":\"TEXT\"},\"audits\":[[\"accepted_values\",{\"column\":\"name\",\"values\":\"ARRAY('Ahi', 'Aji', 'Amaebi', 'Anago', 'Aoyagi', 'Bincho', 'Katsuo', 'Ebi', 'Escolar', 'Hamachi', 'Hamachi Toro', 'Hirame', 'Hokigai', 'Hotate', 'Ika', 'Ikura', 'Iwashi', 'Kani', 'Kanpachi', 'Maguro', 'Saba', 'Sake', 'Sake Toro', 'Tai', 'Tako', 'Tamago', 'Tobiko', 'Toro', 'Tsubugai', 'Umi Masu', 'Unagi', 'Uni')\"}],[\"not_null\",{\"columns\":\"ARRAY(name, price)\"}],[\"assert_items_price_exceeds_threshold\",{\"price\":\"0\"}]],\"grains\":[],\"references\":[],\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{\"execute\":{\"payload\":\"def execute(context, start, end, latest, **kwargs):\\n dfs = []\\n for dt in iter_dates(start, end):\\n num_items = random.randint(10, len(ITEMS))\\n dfs.append(pd.DataFrame({'name': random.sample(ITEMS, num_items),\\n 'price': np.random.uniform(3.0, 10.0, size=num_items).round(2),\\n 'ds': to_ds(dt)}).reset_index().rename(columns={'index': 'id'}))\\n return pd.concat(dfs)\",\"kind\":\"definition\",\"name\":\"execute\",\"path\":\"models\/items.py\"},\"iter_dates\":{\"payload\":\"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\",\"kind\":\"definition\",\"name\":\"iter_dates\",\"path\":\"helper.py\"},\"timedelta\":{\"payload\":\"from datetime import timedelta\",\"kind\":\"import\"},\"set_seed\":{\"payload\":\"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\",\"kind\":\"definition\",\"name\":\"set_seed\",\"path\":\"helper.py\"},\"random\":{\"payload\":\"import random\",\"kind\":\"import\"},\"np\":{\"payload\":\"import numpy as np\",\"kind\":\"import\"},\"ITEMS\":{\"payload\":\"['Ahi', 'Aji', 'Amaebi', 'Anago', 'Aoyagi', 'Bincho', 'Katsuo', 'Ebi', 'Escolar', 'Hamachi', 'Hamachi Toro', 'Hirame', 'Hokigai', 'Hotate', 'Ika', 'Ikura', 'Iwashi', 'Kani', 'Kanpachi', 'Maguro', 'Saba', 'Sake', 'Sake Toro', 'Tai', 'Tako', 'Tamago', 'Tobiko', 'Toro', 'Tsubugai', 'Umi Masu', 'Unagi', 'Uni']\",\"kind\":\"value\"},\"pd\":{\"payload\":\"import pandas as pd\",\"kind\":\"import\"},\"to_ds\":{\"payload\":\"from sqlmesh.utils.date import to_ds\",\"kind\":\"import\"}},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{\"assert_items_price_exceeds_threshold\":{\"name\":\"assert_items_price_exceeds_threshold\",\"dialect\":\"\",\"skip\":false,\"blocking\":true,\"standalone\":false,\"query\":\"SELECT * FROM @this_model WHERE price <= @price\",\"defaults\":{},\"expressions\":[],\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]}}},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"entrypoint\":\"execute\",\"source_type\":\"python\"},\"parents\":[],\"created_ts\":1680814376399,\"ttl\":\"in 1 week\",\"version\":\"312608270\",\"migrated\":true}","19":"{\"name\":\"\\\"sushi\\\".\\\"waiter_as_customer_by_day\\\"\",\"temp_version\":\"1267397572\",\"change_category\":4,\"fingerprint\":{\"data_hash\":\"849558693\",\"metadata_hash\":\"2088684978\",\"parent_data_hash\":\"2705906012\",\"parent_metadata_hash\":\"665080906\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"486172035\",\"metadata_hash\":\"1992853678\",\"parent_data_hash\":\"2154574190\",\"parent_metadata_hash\":\"1349779748\"},\"version\":\"1267397572\",\"change_category\":4,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.waiter_as_customer_by_day\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.waiter_as_customer_by_day\",\"project\":\"\",\"owner\":\"jen\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"duckdb\",\"kind\":{\"name\":\"INCREMENTAL_BY_TIME_RANGE\",\"on_destructive_change\":\"ERROR\",\"dialect\":\"duckdb\",\"forward_only\":false,\"disable_restatement\":false,\"time_column\":{\"column\":\"ds\",\"format\":\"%Y-%m-%d\"}},\"partitioned_by\":[],\"clustered_by\":[],\"audits\":[[\"not_null\",{\"columns\":\"ARRAY(waiter_id)\"}]],\"grains\":[],\"references\":[],\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"query\":\"SELECT w.ds AS ds, w.waiter_id AS waiter_id, wn.name AS waiter_name FROM sushi.waiters AS w JOIN sushi.customers AS c ON w.waiter_id = c.customer_id JOIN sushi.waiter_names AS wn ON w.waiter_id = wn.id\",\"source_type\":\"sql\"},\"parents\":[{\"name\":\"\\\"sushi\\\".\\\"waiter_names\\\"\",\"identifier\":\"1609854746\"},{\"name\":\"\\\"sushi\\\".\\\"waiters\\\"\",\"identifier\":\"4123940212\"},{\"name\":\"\\\"sushi\\\".\\\"orders\\\"\",\"identifier\":\"1250207606\"},{\"name\":\"\\\"sushi\\\".\\\"customers\\\"\",\"identifier\":\"1461038955\"}],\"created_ts\":1680814376348,\"ttl\":\"in 1 week\",\"version\":\"1267397572\",\"migrated\":true}","20":"{\"name\":\"\\\"sushi\\\".\\\"waiter_names\\\"\",\"temp_version\":\"2505706914\",\"change_category\":4,\"fingerprint\":{\"data_hash\":\"3604872020\",\"metadata_hash\":\"3468846895\",\"parent_data_hash\":\"0\",\"parent_metadata_hash\":\"0\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"1876476880\",\"metadata_hash\":\"570478986\",\"parent_data_hash\":\"0\",\"parent_metadata_hash\":\"0\"},\"version\":\"2505706914\",\"change_category\":4,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.waiter_names\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.waiter_names\",\"project\":\"\",\"owner\":\"jen\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"duckdb\",\"kind\":{\"name\":\"SEED\",\"path\":\"..\/seeds\/waiter_names.csv\",\"batch_size\":5},\"partitioned_by\":[],\"clustered_by\":[],\"audits\":[],\"grains\":[],\"references\":[],\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"seed\":{\"content\":\"\"},\"column_hashes\":{\"id\":\"3679804453\",\"name\":\"537745575\"},\"derived_columns_to_types\":{\"id\":\"BIGINT\",\"name\":\"TEXT\"},\"is_hydrated\":false,\"source_type\":\"seed\"},\"parents\":[],\"created_ts\":1680814376389,\"ttl\":\"in 1 week\",\"version\":\"2505706914\",\"migrated\":true}","21":"{\"name\":\"\\\"sushi\\\".\\\"customers\\\"\",\"temp_version\":\"2359719298\",\"change_category\":4,\"fingerprint\":{\"data_hash\":\"2431070412\",\"metadata_hash\":\"3063653103\",\"parent_data_hash\":\"458609840\",\"parent_metadata_hash\":\"2007040660\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"3553985282\",\"metadata_hash\":\"570478986\",\"parent_data_hash\":\"777615193\",\"parent_metadata_hash\":\"2042613269\"},\"version\":\"2359719298\",\"change_category\":4,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.customers\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.customers\",\"project\":\"\",\"owner\":\"jen\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"duckdb\",\"kind\":{\"name\":\"FULL\"},\"partitioned_by\":[],\"clustered_by\":[],\"audits\":[],\"grains\":[],\"references\":[],\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{\"noop\":{\"payload\":\"def noop(context, start, end, latest, **kwargs):\\n pass\",\"kind\":\"definition\",\"name\":\"noop\",\"path\":\"hooks\/hooks.py\"}},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"query\":\"SELECT DISTINCT CAST(customer_id AS INT) AS customer_id FROM sushi.orders AS o\",\"source_type\":\"sql\"},\"parents\":[{\"name\":\"\\\"sushi\\\".\\\"orders\\\"\",\"identifier\":\"1250207606\"}],\"created_ts\":1680814376388,\"ttl\":\"in 1 week\",\"version\":\"2359719298\",\"migrated\":true}","22":"{\"name\":\"\\\"sushi\\\".\\\"waiters\\\"\",\"temp_version\":\"2059227798\",\"change_category\":4,\"fingerprint\":{\"data_hash\":\"2037801255\",\"metadata_hash\":\"3063653103\",\"parent_data_hash\":\"458609840\",\"parent_metadata_hash\":\"2007040660\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"3501061139\",\"metadata_hash\":\"570478986\",\"parent_data_hash\":\"777615193\",\"parent_metadata_hash\":\"2042613269\"},\"version\":\"2059227798\",\"change_category\":4,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.waiters\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.waiters\",\"project\":\"\",\"owner\":\"jen\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"duckdb\",\"kind\":{\"name\":\"EMBEDDED\",\"disable_restatement\":true},\"partitioned_by\":[],\"clustered_by\":[],\"audits\":[],\"grains\":[],\"references\":[],\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{\"incremental_by_ds\":{\"payload\":\"def incremental_by_ds(evaluator, column):\\n expression = evaluator.transform(exp.Between(this=column, low=MacroVar(\\n this='start_ds'), high=MacroVar(this='end_ds')))\\n if not isinstance(expression, exp.Expression):\\n raise MacroEvalError(\\n f'Return type is {type(expression)}, expected exp.Expression')\\n return expression\",\"kind\":\"definition\",\"name\":\"incremental_by_ds\",\"path\":\"macros\/macros.py\"},\"exp\":{\"payload\":\"import sqlglot.expressions as exp\",\"kind\":\"import\"},\"MacroVar\":{\"payload\":\"from sqlmesh.core.dialect import MacroVar\",\"kind\":\"import\"},\"MacroEvalError\":{\"payload\":\"from sqlmesh.utils.errors import MacroEvalError\",\"kind\":\"import\"}},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"query\":\"SELECT DISTINCT CAST(waiter_id AS INT) AS waiter_id, CAST(ds AS TEXT) AS ds FROM sushi.orders AS o WHERE @incremental_by_ds(ds)\",\"source_type\":\"sql\"},\"parents\":[{\"name\":\"\\\"sushi\\\".\\\"orders\\\"\",\"identifier\":\"1250207606\"}],\"created_ts\":1680814376387,\"ttl\":\"in 1 week\",\"version\":\"2059227798\",\"migrated\":true}","23":"{\"name\":\"\\\"sushi\\\".\\\"orders\\\"\",\"temp_version\":\"925846788\",\"change_category\":4,\"fingerprint\":{\"data_hash\":\"1588786367\",\"metadata_hash\":\"1674367104\",\"parent_data_hash\":\"0\",\"parent_metadata_hash\":\"0\"},\"previous_versions\":[{\"fingerprint\":{\"data_hash\":\"1628439771\",\"metadata_hash\":\"2745052130\",\"parent_data_hash\":\"0\",\"parent_metadata_hash\":\"0\"},\"version\":\"925846788\",\"change_category\":4,\"physical_schema\":\"sqlmesh\"}],\"base_table_name_override\":\"sushi.orders\",\"physical_schema\":\"sqlmesh\",\"node\":{\"name\":\"sushi.orders\",\"project\":\"\",\"description\":\"Table of sushi orders.\",\"start\":\"2022-01-01\",\"cron\":\"@daily\",\"tags\":[],\"dialect\":\"\",\"kind\":{\"name\":\"INCREMENTAL_BY_TIME_RANGE\",\"on_destructive_change\":\"ERROR\",\"dialect\":\"\",\"batch_size\":30,\"forward_only\":false,\"disable_restatement\":false,\"time_column\":{\"column\":\"ds\",\"format\":\"%Y-%m-%d\"}},\"partitioned_by\":[],\"clustered_by\":[],\"depends_on\":[],\"columns\":{\"id\":\"INT\",\"customer_id\":\"INT\",\"waiter_id\":\"INT\",\"start_ts\":\"INT\",\"end_ts\":\"INT\",\"ds\":\"TEXT\"},\"audits\":[],\"grains\":[],\"references\":[],\"allow_partials\":false,\"signals\":[],\"enabled\":true,\"python_env\":{\"execute\":{\"payload\":\"def execute(context, start, end, latest, **kwargs):\\n dfs = []\\n for dt in iter_dates(start, end):\\n num_orders = random.randint(10, 30)\\n start_ts = [int((dt + timedelta(seconds=random.randint(0, 80000))).\\n timestamp()) for _ in range(num_orders)]\\n end_ts = [int(s + random.randint(0, 60 * 60)) for s in start_ts]\\n dfs.append(pd.DataFrame({'customer_id': random.choices(CUSTOMERS, k\\n =num_orders), 'waiter_id': random.choices(WAITERS, k=num_orders\\n ), 'start_ts': start_ts, 'end_ts': end_ts, 'ds': to_ds(dt)}).\\n reset_index().rename(columns={'index': 'id'}))\\n return pd.concat(dfs)\",\"kind\":\"definition\",\"name\":\"execute\",\"path\":\"models\/orders.py\"},\"iter_dates\":{\"payload\":\"def iter_dates(start, end):\\n for i in range((end - start).days + 1):\\n dt = start + timedelta(days=i)\\n set_seed(dt)\\n yield dt\",\"kind\":\"definition\",\"name\":\"iter_dates\",\"path\":\"helper.py\"},\"timedelta\":{\"payload\":\"from datetime import timedelta\",\"kind\":\"import\"},\"set_seed\":{\"payload\":\"def set_seed(dt):\\n ts = int(dt.timestamp())\\n random.seed(ts)\\n np.random.seed(ts)\",\"kind\":\"definition\",\"name\":\"set_seed\",\"path\":\"helper.py\"},\"random\":{\"payload\":\"import random\",\"kind\":\"import\"},\"np\":{\"payload\":\"import numpy as np\",\"kind\":\"import\"},\"pd\":{\"payload\":\"import pandas as pd\",\"kind\":\"import\"},\"CUSTOMERS\":{\"payload\":\"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]\",\"kind\":\"value\"},\"WAITERS\":{\"payload\":\"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\",\"kind\":\"value\"},\"to_ds\":{\"payload\":\"from sqlmesh.utils.date import to_ds\",\"kind\":\"import\"}},\"jinja_macros\":{\"packages\":{},\"root_macros\":{},\"global_objs\":{},\"create_builtins_module\":\"sqlmesh.utils.jinja\",\"top_level_packages\":[]},\"audit_definitions\":{},\"mapping_schema\":{},\"extract_dependencies_from_query\":true,\"entrypoint\":\"execute\",\"source_type\":\"python\"},\"parents\":[],\"created_ts\":1680814376402,\"ttl\":\"in 1 week\",\"version\":\"925846788\",\"migrated\":true}"},"kind_name":{"0":"INCREMENTAL_BY_TIME_RANGE","1":"INCREMENTAL_BY_TIME_RANGE","2":"VIEW","3":"EMBEDDED","4":"FULL","5":"SEED","6":"INCREMENTAL_BY_TIME_RANGE","7":"INCREMENTAL_BY_TIME_RANGE","8":"INCREMENTAL_BY_TIME_RANGE","9":"INCREMENTAL_BY_TIME_RANGE","10":"INCREMENTAL_BY_TIME_RANGE","11":"SEED","12":"INCREMENTAL_BY_TIME_RANGE","13":"SEED","14":"INCREMENTAL_BY_TIME_RANGE","15":"VIEW","16":"INCREMENTAL_BY_TIME_RANGE","17":"INCREMENTAL_BY_TIME_RANGE","18":"INCREMENTAL_BY_TIME_RANGE","19":"INCREMENTAL_BY_TIME_RANGE","20":"SEED","21":"FULL","22":"EMBEDDED","23":"INCREMENTAL_BY_TIME_RANGE"},"updated_ts":{"0":1680814376348,"1":1680814376361,"2":1680814376384,"3":1680814376387,"4":1680814376388,"5":1680814376389,"6":1680814376391,"7":1680814376399,"8":1680814376401,"9":1680814376402,"10":1680814464891,"11":1680814464932,"12":1680814464891,"13":1680814464932,"14":1680814376391,"15":1680814376384,"16":1680814376361,"17":1680814376401,"18":1680814376399,"19":1680814376348,"20":1680814376389,"21":1680814376388,"22":1680814376387,"23":1680814376402},"unpaused_ts":{"0":null,"1":null,"2":null,"3":null,"4":null,"5":null,"6":null,"7":null,"8":null,"9":null,"10":null,"11":null,"12":null,"13":null,"14":null,"15":null,"16":null,"17":null,"18":null,"19":null,"20":null,"21":null,"22":null,"23":null},"ttl_ms":{"0":604800000,"1":604800000,"2":604800000,"3":604800000,"4":604800000,"5":604800000,"6":604800000,"7":604800000,"8":604800000,"9":604800000,"10":604800000,"11":604800000,"12":604800000,"13":604800000,"14":604800000,"15":604800000,"16":604800000,"17":604800000,"18":604800000,"19":604800000,"20":604800000,"21":604800000,"22":604800000,"23":604800000},"unrestorable":{"0":false,"1":false,"2":false,"3":false,"4":false,"5":false,"6":false,"7":false,"8":false,"9":false,"10":false,"11":false,"12":false,"13":false,"14":false,"15":false,"16":false,"17":false,"18":false,"19":false,"20":false,"21":false,"22":false,"23":false}} \ No newline at end of file diff --git a/tests/fixtures/migrations/versions.json b/tests/fixtures/migrations/versions.json new file mode 100644 index 0000000000..5eac7ed987 --- /dev/null +++ b/tests/fixtures/migrations/versions.json @@ -0,0 +1 @@ +{"schema_version":{"0":60},"sqlglot_version":{"0":"25.31.4"},"sqlmesh_version":{"0":"0.134.0"}} diff --git a/tooling/validating_migration_numbers.sh b/tooling/validating_migration_numbers.sh index 6dbb597dc1..6997d41fe1 100755 --- a/tooling/validating_migration_numbers.sh +++ b/tooling/validating_migration_numbers.sh @@ -14,7 +14,9 @@ numbers=() for file in "${migration_files[@]}"; do if [[ $file =~ ^v0*([0-9]+)_ ]]; then num=${BASH_REMATCH[1]} - numbers+=("$num") + if [[ "$num" -gt 0 ]]; then + numbers+=("$num") + fi fi done From 101e73b8d806d6b51dda3e1f67e223993b07bb10 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:24:47 -0700 Subject: [PATCH 0829/1056] fix: scd type 2 support table properties (#5317) --- sqlmesh/core/snapshot/evaluator.py | 57 ++++++---------------- tests/core/engine_adapter/test_bigquery.py | 34 +++++++++++++ tests/core/test_snapshot_evaluator.py | 53 ++++++++++++++++++-- 3 files changed, 99 insertions(+), 45 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index b96b0b8718..3ed9c20765 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -2241,6 +2241,11 @@ def insert( column_descriptions=model.column_descriptions, truncate=is_first_insert, source_columns=source_columns, + storage_format=model.storage_format, + partitioned_by=model.partitioned_by, + partition_interval_unit=model.partition_interval_unit, + clustered_by=model.clustered_by, + table_properties=kwargs.get("physical_properties", model.physical_properties), ) elif isinstance(model.kind, SCDType2ByColumnKind): self.adapter.scd_type_2_by_column( @@ -2259,6 +2264,11 @@ def insert( column_descriptions=model.column_descriptions, truncate=is_first_insert, source_columns=source_columns, + storage_format=model.storage_format, + partitioned_by=model.partitioned_by, + partition_interval_unit=model.partition_interval_unit, + clustered_by=model.clustered_by, + table_properties=kwargs.get("physical_properties", model.physical_properties), ) else: raise SQLMeshError( @@ -2273,51 +2283,14 @@ def append( render_kwargs: t.Dict[str, t.Any], **kwargs: t.Any, ) -> None: - # Source columns from the underlying table to prevent unintentional table schema changes during restatement of incremental models. - columns_to_types, source_columns = self._get_target_and_source_columns( - model, + return self.insert( table_name, + query_or_df, + model, + is_first_insert=False, render_kwargs=render_kwargs, - force_get_columns_from_target=True, + **kwargs, ) - if isinstance(model.kind, SCDType2ByTimeKind): - self.adapter.scd_type_2_by_time( - target_table=table_name, - source_table=query_or_df, - unique_key=model.unique_key, - valid_from_col=model.kind.valid_from_name, - valid_to_col=model.kind.valid_to_name, - updated_at_col=model.kind.updated_at_name, - invalidate_hard_deletes=model.kind.invalidate_hard_deletes, - updated_at_as_valid_from=model.kind.updated_at_as_valid_from, - target_columns_to_types=columns_to_types, - table_format=model.table_format, - table_description=model.description, - column_descriptions=model.column_descriptions, - source_columns=source_columns, - **kwargs, - ) - elif isinstance(model.kind, SCDType2ByColumnKind): - self.adapter.scd_type_2_by_column( - target_table=table_name, - source_table=query_or_df, - unique_key=model.unique_key, - valid_from_col=model.kind.valid_from_name, - valid_to_col=model.kind.valid_to_name, - check_columns=model.kind.columns, - target_columns_to_types=columns_to_types, - table_format=model.table_format, - invalidate_hard_deletes=model.kind.invalidate_hard_deletes, - execution_time_as_valid_from=model.kind.execution_time_as_valid_from, - table_description=model.description, - column_descriptions=model.column_descriptions, - source_columns=source_columns, - **kwargs, - ) - else: - raise SQLMeshError( - f"Unexpected SCD Type 2 kind: {model.kind}. This is not expected and please report this as a bug." - ) class ViewStrategy(PromotableStrategy): diff --git a/tests/core/engine_adapter/test_bigquery.py b/tests/core/engine_adapter/test_bigquery.py index 326c587de0..4328fa8923 100644 --- a/tests/core/engine_adapter/test_bigquery.py +++ b/tests/core/engine_adapter/test_bigquery.py @@ -1,5 +1,6 @@ # type: ignore import typing as t +from datetime import datetime import pandas as pd # noqa: TID253 import pytest @@ -1173,3 +1174,36 @@ def test_drop_cascade(adapter: BigQueryEngineAdapter): "DROP SCHEMA IF EXISTS `foo` CASCADE", "DROP SCHEMA IF EXISTS `foo`", ] + + +def test_scd_type_2_by_partitioning(adapter: BigQueryEngineAdapter): + adapter.scd_type_2_by_time( + target_table="target", + source_table=t.cast( + exp.Select, parse_one("SELECT id, name, price, test_UPDATED_at FROM source") + ), + unique_key=[ + exp.to_column("id"), + ], + updated_at_col=exp.column("test_UPDATED_at", quoted=True), + valid_from_col=exp.to_column("valid_from", quoted=True), + valid_to_col=exp.to_column("valid_to", quoted=True), + target_columns_to_types={ + "id": exp.DataType.build("INT"), + "name": exp.DataType.build("VARCHAR"), + "price": exp.DataType.build("DOUBLE"), + "test_UPDATED_at": exp.DataType.build("TIMESTAMP"), + "valid_from": exp.DataType.build("TIMESTAMP"), + "valid_to": exp.DataType.build("TIMESTAMP"), + }, + execution_time=datetime(2020, 1, 1, 0, 0, 0), + partitioned_by=[parse_one("TIMESTAMP_TRUNC(valid_from, DAY)")], + ) + + calls = _to_sql_calls(adapter) + + # Initial call to create the table and then another to replace since it is self-referencing + assert len(calls) == 2 + # Both calls should contain the partition logic (the scd logic is already covered by other tests) + assert "PARTITION BY TIMESTAMP_TRUNC(`valid_from`, DAY)" in calls[0] + assert "PARTITION BY TIMESTAMP_TRUNC(`valid_from`, DAY)" in calls[1] diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 6f610a696a..955c7f7859 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -1,5 +1,6 @@ from __future__ import annotations import typing as t + from typing_extensions import Self from unittest.mock import call, patch, Mock import re @@ -2062,7 +2063,7 @@ def test_create_scd_type_2_by_time(adapter_mock, make_snapshot): ) -def test_create_ctas_scd_type_2_by_time(adapter_mock, make_snapshot): +def test_create_ctas_scd_type_2_by_time(adapter_mock, make_snapshot, mocker): evaluator = SnapshotEvaluator(adapter_mock) model = load_sql_based_model( parse( # type: ignore @@ -2073,7 +2074,8 @@ def test_create_ctas_scd_type_2_by_time(adapter_mock, make_snapshot): unique_key id, time_data_type TIMESTAMPTZ, invalidate_hard_deletes false - ) + ), + partitioned_by cola ); SELECT * FROM tbl; @@ -2086,6 +2088,7 @@ def test_create_ctas_scd_type_2_by_time(adapter_mock, make_snapshot): evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + source_query = parse_one('SELECT * FROM "tbl" AS "tbl"') query = parse_one( """SELECT *, CAST(NULL AS TIMESTAMPTZ) AS valid_from, CAST(NULL AS TIMESTAMPTZ) AS valid_to FROM "tbl" AS "tbl" WHERE FALSE LIMIT 0""" ) @@ -2094,7 +2097,9 @@ def test_create_ctas_scd_type_2_by_time(adapter_mock, make_snapshot): common_kwargs = dict( table_format=None, storage_format=None, - partitioned_by=[], + partitioned_by=[ + exp.to_column("cola", quoted=True), + ], partition_interval_unit=None, clustered_by=[], table_properties={}, @@ -2113,6 +2118,38 @@ def test_create_ctas_scd_type_2_by_time(adapter_mock, make_snapshot): ] ) + adapter_mock.reset_mock() + + evaluator.evaluate( + snapshot, + start="2020-01-01", + end="2020-01-02", + execution_time="2020-01-02", + snapshots={}, + deployability_index=DeployabilityIndex.none_deployable(), + ) + + adapter_mock.scd_type_2_by_time.assert_has_calls( + [ + call( + column_descriptions={}, + execution_time="2020-01-02", + invalidate_hard_deletes=False, + source_columns=None, + source_table=source_query, + target_columns_to_types=mocker.ANY, + target_table=snapshot.table_name(is_deployable=False), + truncate=True, + unique_key=[exp.to_column("id", quoted=True)], + updated_at_as_valid_from=False, + updated_at_col=exp.column("updated_at", quoted=True), + valid_from_col=exp.column("valid_from", quoted=True), + valid_to_col=exp.column("valid_to", quoted=True), + **common_kwargs, + ), + ] + ) + @pytest.mark.parametrize( "intervals,truncate", @@ -2178,6 +2215,11 @@ def test_insert_into_scd_type_2_by_time( updated_at_as_valid_from=False, truncate=truncate, source_columns=None, + clustered_by=[], + partition_interval_unit=None, + partitioned_by=[], + storage_format=None, + table_properties={}, ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) @@ -2347,6 +2389,11 @@ def test_insert_into_scd_type_2_by_column( column_descriptions={}, truncate=truncate, source_columns=None, + clustered_by=[], + partition_interval_unit=None, + partitioned_by=[], + storage_format=None, + table_properties={}, ) adapter_mock.columns.assert_called_once_with(snapshot.table_name()) From 45a2db65ee6002b5b8ce485dbe8908e1438b07c3 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:42:25 +0300 Subject: [PATCH 0830/1056] Fix: disable query validations for dbt models (#5305) --- sqlmesh/cli/project_init.py | 1 + sqlmesh/core/context.py | 4 +- sqlmesh/core/linter/definition.py | 26 +++---- sqlmesh/core/linter/rules/builtin.py | 29 ++++++++ sqlmesh/core/model/definition.py | 37 ++++------ tests/cli/test_cli.py | 3 + tests/core/test_context.py | 49 ++++++++++++ tests/core/test_model.py | 74 ++++++++++++------- .../sushi_test/models/non_validated_model.sql | 5 ++ 9 files changed, 164 insertions(+), 64 deletions(-) create mode 100644 tests/fixtures/dbt/sushi_test/models/non_validated_model.sql diff --git a/sqlmesh/cli/project_init.py b/sqlmesh/cli/project_init.py index 81ff534dc4..0790562de7 100644 --- a/sqlmesh/cli/project_init.py +++ b/sqlmesh/cli/project_init.py @@ -114,6 +114,7 @@ def _gen_config( rules: - ambiguousorinvalidcolumn - invalidselectstarexpansion + - noambiguousprojections """, ProjectTemplate.DBT: f"""# --- Virtual Data Environment Mode --- # Enable Virtual Data Environments (VDE) for *development* environments. diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index a8715adc4c..78a391d12f 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -3130,7 +3130,9 @@ def lint_models( found_error = False model_list = ( - list(self.get_model(model) for model in models) if models else self.models.values() + list(self.get_model(model, raise_if_missing=True) for model in models) + if models + else self.models.values() ) all_violations = [] for model in model_list: diff --git a/sqlmesh/core/linter/definition.py b/sqlmesh/core/linter/definition.py index c7cee6aaa9..7dc64bbf95 100644 --- a/sqlmesh/core/linter/definition.py +++ b/sqlmesh/core/linter/definition.py @@ -1,15 +1,15 @@ from __future__ import annotations -import typing as t -from sqlmesh.core.config.linter import LinterConfig -from sqlmesh.core.model import Model -from sqlmesh.utils.errors import raise_config_error -from sqlmesh.core.console import LinterConsole, get_console + import operator as op +import typing as t from collections.abc import Iterator, Iterable, Set, Mapping, Callable from functools import reduce -from sqlmesh.core.model import Model -from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix + +from sqlmesh.core.config.linter import LinterConfig from sqlmesh.core.console import LinterConsole, get_console +from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix +from sqlmesh.core.model import Model +from sqlmesh.utils.errors import raise_config_error if t.TYPE_CHECKING: from sqlmesh.core.context import GenericContext @@ -38,6 +38,12 @@ def __init__( self.rules = rules self.warn_rules = warn_rules + if overlapping := rules.intersection(warn_rules): + overlapping_rules = ", ".join(rule for rule in overlapping) + raise_config_error( + f"Rules cannot simultaneously warn and raise an error: [{overlapping_rules}]" + ) + @classmethod def from_rules(cls, all_rules: RuleSet, config: LinterConfig) -> Linter: ignored_rules = select_rules(all_rules, config.ignored_rules) @@ -46,12 +52,6 @@ def from_rules(cls, all_rules: RuleSet, config: LinterConfig) -> Linter: rules = select_rules(included_rules, config.rules) warn_rules = select_rules(included_rules, config.warn_rules) - if overlapping := rules.intersection(warn_rules): - overlapping_rules = ", ".join(rule for rule in overlapping) - raise_config_error( - f"Rules cannot simultaneously warn and raise an error: [{overlapping_rules}]" - ) - return Linter(config.enabled, all_rules, rules, warn_rules) def lint_model( diff --git a/sqlmesh/core/linter/rules/builtin.py b/sqlmesh/core/linter/rules/builtin.py index a793f79434..f6bef4b4ef 100644 --- a/sqlmesh/core/linter/rules/builtin.py +++ b/sqlmesh/core/linter/rules/builtin.py @@ -274,4 +274,33 @@ def create_fix(self, model_name: str) -> t.Optional[Fix]: ) +class NoAmbiguousProjections(Rule): + """All projections in a model must have unique & inferrable names or explicit aliases.""" + + def check_model(self, model: Model) -> t.Optional[RuleViolation]: + query = model.render_query() + if query is None: + return None + + name_counts: t.Dict[str, int] = {} + projection_list = query.selects + for expression in projection_list: + alias = expression.output_name + if alias == "*": + continue + + if not alias: + return self.violation( + f"Outer projection '{expression.sql(dialect=model.dialect)}' must have inferrable names or explicit aliases." + ) + + name_counts[alias] = name_counts.get(alias, 0) + 1 + + for name, count in name_counts.items(): + if count > 1: + return self.violation(f"Found duplicate outer select name '{name}'") + + return None + + BUILTIN_RULES = RuleSet(subclasses(__name__, Rule, (Rule,))) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index b772a9d9d9..dbbb8ff3a8 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -1417,12 +1417,20 @@ def columns_to_types(self) -> t.Optional[t.Dict[str, exp.DataType]]: unknown = exp.DataType.build("unknown") - self._columns_to_types = { + columns_to_types = {} + for select in query.selects: + output_name = select.output_name + + # If model validation is disabled, we cannot assume that projections + # will have inferrable output names or even that they will be unique + if not output_name or output_name in columns_to_types: + return None + # copy data type because it is used in the engine to build CTAS and other queries # this can change the parent which will mess up the diffing algo - select.output_name: (select.type or unknown).copy() - for select in query.selects - } + columns_to_types[output_name] = (select.type or unknown).copy() + + self._columns_to_types = columns_to_types if "*" in self._columns_to_types: return None @@ -1473,22 +1481,6 @@ def validate_definition(self) -> None: if not projection_list: raise_config_error("Query missing select statements", self._path) - name_counts: t.Dict[str, int] = {} - for expression in projection_list: - alias = expression.output_name - if alias == "*": - continue - if not alias: - raise_config_error( - f"Outer projection '{expression.sql(dialect=self.dialect)}' must have inferrable names or explicit aliases.", - self._path, - ) - name_counts[alias] = name_counts.get(alias, 0) + 1 - - for name, count in name_counts.items(): - if count > 1: - raise_config_error(f"Found duplicate outer select name '{name}'", self._path) - if self.depends_on_self and not self.annotated: raise_config_error( "Self-referencing models require inferrable column types. There are three options available to mitigate this issue: add explicit types to all projections in the outermost SELECT statement, leverage external models (https://sqlmesh.readthedocs.io/en/stable/concepts/models/external_models/), or use the `columns` model attribute (https://sqlmesh.readthedocs.io/en/stable/concepts/models/overview/#columns).", @@ -1846,8 +1838,9 @@ def validate_definition(self) -> None: super().validate_definition() if self.kind and not self.kind.supports_python_models: - raise SQLMeshError( - f"Cannot create Python model '{self.name}' as the '{self.kind.name}' kind doesn't support Python models" + raise_config_error( + f"Cannot create Python model '{self.name}' as the '{self.kind.name}' kind doesn't support Python models", + self._path, ) def render( diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index e97e03b29e..e460387bbc 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -982,6 +982,7 @@ def test_dlt_filesystem_pipeline(tmp_path): " rules:\n" " - ambiguousorinvalidcolumn\n" " - invalidselectstarexpansion\n" + " - noambiguousprojections\n" ) with open(config_path) as file: @@ -1048,6 +1049,7 @@ def test_dlt_pipeline(runner, tmp_path): rules: - ambiguousorinvalidcolumn - invalidselectstarexpansion + - noambiguousprojections """ with open(tmp_path / "config.yaml") as file: @@ -1990,6 +1992,7 @@ def test_init_project_engine_configs(tmp_path): rules: - ambiguousorinvalidcolumn - invalidselectstarexpansion + - noambiguousprojections """ with open(tmp_path / "config.yaml") as file: diff --git a/tests/core/test_context.py b/tests/core/test_context.py index f09ff25c33..454f208db5 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1554,6 +1554,38 @@ def test_raw_code_handling(sushi_test_dbt_context: Context): ) +@pytest.mark.slow +def test_dbt_models_are_not_validated(sushi_test_dbt_context: Context): + model = sushi_test_dbt_context.models['"memory"."sushi"."non_validated_model"'] + + assert model.render_query_or_raise().sql(comments=False) == 'SELECT 1 AS "c", 2 AS "c"' + assert sushi_test_dbt_context.fetchdf( + 'SELECT * FROM "memory"."sushi"."non_validated_model"' + ).to_dict() == {"c": {0: 1}, "c_1": {0: 2}} + + # Write a new incremental model file that should fail validation + models_dir = sushi_test_dbt_context.path / "models" + incremental_model_path = models_dir / "invalid_incremental.sql" + incremental_model_content = """{{ + config( + materialized='incremental', + incremental_strategy='delete+insert', + ) +}} + +SELECT + 1 AS c""" + + incremental_model_path.write_text(incremental_model_content) + + # Reload the context - this should raise a validation error for the incremental model + with pytest.raises( + ConfigError, + match="Unmanaged incremental models with insert / overwrite enabled must specify the partitioned_by field", + ): + Context(paths=sushi_test_dbt_context.path, config="test_config") + + def test_catalog_name_needs_to_be_quoted(): config = Config( model_defaults=ModelDefaultsConfig(dialect="duckdb"), @@ -3085,3 +3117,20 @@ def test_plan_no_start_configured(): match=r"Model '.*xvg.*': Start date / time .* can't be greater than end date / time .*\.\nSet the `start` attribute in your project config model defaults to avoid this issue", ): context.plan("dev", execution_time="1999-01-05") + + +def test_lint_model_projections(tmp_path: Path): + init_example_project(tmp_path, engine_type="duckdb", dialect="duckdb") + + context = Context(paths=tmp_path) + context.upsert_model( + load_sql_based_model( + parse("""MODEL(name sqlmesh_example.m); SELECT 1 AS x, 2 AS x"""), + default_catalog="db", + ) + ) + + config_err = "Linter detected errors in the code. Please fix them before proceeding." + + with pytest.raises(LinterError, match=config_err): + prod_plan = context.plan(no_prompts=True, auto_apply=True) diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 8794f38826..a3159c8f0a 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -31,12 +31,12 @@ ModelDefaultsConfig, LinterConfig, ) +from sqlmesh.core import constants as c from sqlmesh.core.context import Context, ExecutionContext from sqlmesh.core.dialect import parse from sqlmesh.core.engine_adapter.base import MERGE_SOURCE_ALIAS, MERGE_TARGET_ALIAS from sqlmesh.core.engine_adapter.duckdb import DuckDBEngineAdapter from sqlmesh.core.macros import MacroEvaluator, macro -from sqlmesh.core import constants as c from sqlmesh.core.model import ( CustomKind, PythonModel, @@ -198,14 +198,7 @@ def test_model_multiple_select_statements(): load_sql_based_model(expressions) -@pytest.mark.parametrize( - "query, error", - [ - ("y::int, x::int AS y", "duplicate"), - ("* FROM db.table", "require inferrable column types"), - ], -) -def test_model_validation(query, error): +def test_model_validation(tmp_path): expressions = d.parse( f""" MODEL ( @@ -213,14 +206,56 @@ def test_model_validation(query, error): kind FULL, ); - SELECT {query} + SELECT + y::int, + x::int AS y + FROM db.ext + """ + ) + + ctx = Context( + config=Config(linter=LinterConfig(enabled=True, rules=["noambiguousprojections"])), + paths=tmp_path, + ) + ctx.upsert_model(load_sql_based_model(expressions, default_catalog="memory")) + + errors = ctx.lint_models(["db.table"], raise_on_error=False) + assert errors, "Expected NoAmbiguousProjections violation" + assert errors[0].violation_msg == "Found duplicate outer select name 'y'" + + expressions = d.parse( + """ + MODEL ( + name db.table, + kind FULL, + ); + + SELECT a, a UNION SELECT c, c + """ + ) + + ctx.upsert_model(load_sql_based_model(expressions, default_catalog="memory")) + + errors = ctx.lint_models(["db.table"], raise_on_error=False) + assert errors, "Expected NoAmbiguousProjections violation" + assert errors[0].violation_msg == "Found duplicate outer select name 'a'" + + expressions = d.parse( + f""" + MODEL ( + name db.table, + kind FULL, + ); + + SELECT * FROM db.table """ ) model = load_sql_based_model(expressions) with pytest.raises(ConfigError) as ex: model.validate_definition() - assert error in str(ex.value) + + assert "require inferrable column types" in str(ex.value) def test_model_union_query(sushi_context, assert_exp_eq): @@ -405,23 +440,6 @@ def get_date(evaluator): ) -def test_model_validation_union_query(): - expressions = d.parse( - """ - MODEL ( - name db.table, - kind FULL, - ); - - SELECT a, a UNION SELECT c, c - """ - ) - - model = load_sql_based_model(expressions) - with pytest.raises(ConfigError, match=r"Found duplicate outer select name 'a'"): - model.validate_definition() - - @use_terminal_console def test_model_qualification(tmp_path: Path): with patch.object(get_console(), "log_warning") as mock_logger: diff --git a/tests/fixtures/dbt/sushi_test/models/non_validated_model.sql b/tests/fixtures/dbt/sushi_test/models/non_validated_model.sql new file mode 100644 index 0000000000..3140c5d723 --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/models/non_validated_model.sql @@ -0,0 +1,5 @@ +{{ config(materialized='table') }} + +SELECT + 1 AS c, + 2 AS c, From 89d5740e3495e8eb4136a86386bbf388b7408d42 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 9 Sep 2025 08:50:25 +1200 Subject: [PATCH 0831/1056] Fix(fabric): Ensure that Fabric connections do not try to use a catalog once it's been dropped (#5314) --- sqlmesh/core/engine_adapter/fabric.py | 9 +++ sqlmesh/utils/connection_pool.py | 3 +- .../integration/test_integration.py | 8 ++ .../integration/test_integration_fabric.py | 76 +++++++++++++++++++ tests/utils/test_connection_pool.py | 23 ++++++ 5 files changed, 118 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/engine_adapter/fabric.py b/sqlmesh/core/engine_adapter/fabric.py index 585622e866..a528be3cb4 100644 --- a/sqlmesh/core/engine_adapter/fabric.py +++ b/sqlmesh/core/engine_adapter/fabric.py @@ -121,10 +121,19 @@ def _create_catalog(self, catalog_name: exp.Identifier) -> None: def _drop_catalog(self, catalog_name: exp.Identifier) -> None: """Drop a catalog (warehouse) in Microsoft Fabric via REST API.""" warehouse_name = catalog_name.sql(dialect=self.dialect, identify=False) + current_catalog = self.get_current_catalog() logger.info(f"Deleting Fabric warehouse: {warehouse_name}") self.api_client.delete_warehouse(warehouse_name) + if warehouse_name == current_catalog: + # Somewhere around 2025-09-08, Fabric started validating the "Database=" connection argument and throwing 'Authentication failed' if the database doesnt exist + # In addition, set_current_catalog() is implemented using a threadlocal variable "target_catalog" + # So, when we drop a warehouse, and there are still threads with "target_catalog" set to reference it, any operations on those threads + # that use an either use an existing connection pointing to this warehouse or trigger a new connection + # will fail with an 'Authentication Failed' error unless we close all connections here, which also clears all the threadlocal data + self.close() + def set_current_catalog(self, catalog_name: str) -> None: """ Set the current catalog for Microsoft Fabric connections. diff --git a/sqlmesh/utils/connection_pool.py b/sqlmesh/utils/connection_pool.py index a4f9486184..9a70db6885 100644 --- a/sqlmesh/utils/connection_pool.py +++ b/sqlmesh/utils/connection_pool.py @@ -227,7 +227,8 @@ def close_all(self, exclude_calling_thread: bool = False) -> None: self._thread_connections.pop(thread_id) self._thread_cursors.pop(thread_id, None) self._discard_transaction(thread_id) - self._thread_attributes.pop(thread_id, None) + + self._thread_attributes.clear() class ThreadLocalSharedConnectionPool(_ThreadLocalBase): diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index fcbc711f49..42ff8b881f 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -3743,6 +3743,14 @@ def _set_config(gateway: str, config: Config) -> None: assert not md.tables assert not md.managed_tables + if ctx.dialect == "fabric": + # TestContext is using a different EngineAdapter instance / connection pool instance to the SQLMesh context + # When the SQLMesh context drops :snapshot_schema using its EngineAdapter, connections in TestContext are unaware + # and still have their threadlocal "target_catalog" attribute pointing to a catalog that no longer exists + # Trying to establish a connection to a nonexistant catalog produces an error, so we close all connections here + # to clear the threadlocal attributes + ctx.engine_adapter.close() + md = ctx.get_metadata_results(snapshot_schema) assert not md.views assert not md.managed_tables diff --git a/tests/core/engine_adapter/integration/test_integration_fabric.py b/tests/core/engine_adapter/integration/test_integration_fabric.py index a272005bdc..41f399b3b8 100644 --- a/tests/core/engine_adapter/integration/test_integration_fabric.py +++ b/tests/core/engine_adapter/integration/test_integration_fabric.py @@ -1,8 +1,12 @@ import typing as t +import threading +import queue import pytest from pytest import FixtureRequest from sqlmesh.core.engine_adapter import FabricEngineAdapter +from sqlmesh.utils.connection_pool import ThreadLocalConnectionPool from tests.core.engine_adapter.integration import TestContext +from concurrent.futures import ThreadPoolExecutor from tests.core.engine_adapter.integration import ( TestContext, @@ -39,3 +43,75 @@ def test_create_drop_catalog(ctx: TestContext, engine_adapter: FabricEngineAdapt finally: # if doesnt exist, should be no-op, not error ctx.drop_catalog(catalog_name) + + +def test_drop_catalog_clears_threadlocals_that_reference_it( + ctx: TestContext, engine_adapter: FabricEngineAdapter +): + catalog_name = ctx.add_test_suffix("test_drop_catalog") + default_catalog = engine_adapter.get_current_catalog() + + assert isinstance(engine_adapter._connection_pool, ThreadLocalConnectionPool) + + # sets the connection attribute for this thread + engine_adapter.create_catalog(catalog_name) + assert engine_adapter._target_catalog is None + engine_adapter.set_current_catalog(catalog_name) + assert engine_adapter.get_current_catalog() == catalog_name + assert engine_adapter._target_catalog == catalog_name + + lock = threading.RLock() + + def _set_and_return_catalog_in_another_thread( + q: queue.Queue, engine_adapter: FabricEngineAdapter + ) -> t.Optional[str]: + q.put("thread_started") + + assert engine_adapter.get_current_catalog() == default_catalog + assert engine_adapter._target_catalog is None + + engine_adapter.set_current_catalog(catalog_name) + assert engine_adapter.get_current_catalog() == catalog_name + assert engine_adapter._target_catalog == catalog_name + + q.put("catalog_set_in_thread") + + # block this thread while we drop the catalog in the main test thread + lock.acquire() + + # the current catalog should have been cleared from the threadlocal connection attributes + # when this catalog was dropped by the outer thread, causing it to fall back to the default catalog + try: + assert engine_adapter._target_catalog is None + return engine_adapter.get_current_catalog() + finally: + lock.release() + + q: queue.Queue = queue.Queue() + + with ThreadPoolExecutor() as executor: + lock.acquire() # we have the lock, thread will be blocked until we release it + + future = executor.submit(_set_and_return_catalog_in_another_thread, q, engine_adapter) + + assert q.get() == "thread_started" + assert not future.done() + + try: + assert q.get(timeout=20) == "catalog_set_in_thread" + except: + if exec := future.exception(): + raise exec + raise + + ctx.drop_catalog(catalog_name) + assert not future.done() + + lock.release() # yield the lock to the thread + + # block until thread complete + result = future.result() + + # both threads should be automatically using the default catalog now + assert result == default_catalog + assert engine_adapter.get_current_catalog() == default_catalog diff --git a/tests/utils/test_connection_pool.py b/tests/utils/test_connection_pool.py index 96c2f69012..c5926a3824 100644 --- a/tests/utils/test_connection_pool.py +++ b/tests/utils/test_connection_pool.py @@ -210,6 +210,29 @@ def thread(): assert cursor_mock_thread_two.begin.call_count == 1 +def test_thread_local_connection_pool_attributes(mocker: MockerFixture): + pool = ThreadLocalConnectionPool(connection_factory=lambda: mocker.Mock()) + + pool.set_attribute("foo", "bar") + current_threadid = get_ident() + + def _in_thread(pool: ThreadLocalConnectionPool): + assert get_ident() != current_threadid + pool.set_attribute("foo", "baz") + + with ThreadPoolExecutor() as executor: + future = executor.submit(_in_thread, pool) + assert not future.exception() + + assert pool.get_all_attributes("foo") == ["bar", "baz"] + assert pool.get_attribute("foo") == "bar" + + pool.close_all() + + assert pool.get_all_attributes("foo") == [] + assert pool.get_attribute("foo") is None + + def test_thread_local_shared_connection_pool(mocker: MockerFixture): cursor_mock_thread_one = mocker.Mock() cursor_mock_thread_two = mocker.Mock() From cd3700237a467cefe0f9b96d867329c8ef2f4d7f Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 9 Sep 2025 02:39:49 +0300 Subject: [PATCH 0832/1056] Chore!: bump sqlglot to v27.13.2 (#5324) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cdc9a4063f..2e39219710 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.12.0", + "sqlglot[rs]~=27.13.2", "tenacity", "time-machine", "json-stream" From 90b6414a2a1d34b5e9f68dd4ee19d72737fbd11c Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 9 Sep 2025 07:56:27 -0700 Subject: [PATCH 0833/1056] feat: dbt add invocation args which support (#5316) --- sqlmesh/dbt/builtin.py | 9 +++++++-- tests/dbt/test_transformation.py | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 8d48c4a77a..0503f1dc92 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -502,8 +502,14 @@ def create_builtin_globals( else: builtin_globals["model"] = AttributeDict(model.copy()) + builtin_globals["flags"] = ( + Flags(which="run") if engine_adapter is not None else Flags(which="parse") + ) + builtin_globals["invocation_args_dict"] = { + k.lower(): v for k, v in builtin_globals["flags"].__dict__.items() + } + if engine_adapter is not None: - builtin_globals["flags"] = Flags(which="run") adapter: BaseAdapter = RuntimeAdapter( engine_adapter, jinja_macros, @@ -521,7 +527,6 @@ def create_builtin_globals( project_dialect=project_dialect, ) else: - builtin_globals["flags"] = Flags(which="parse") adapter = ParsetimeAdapter( jinja_macros, jinja_globals={**builtin_globals, **jinja_globals}, diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 89e4bca154..a769287aec 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1435,6 +1435,14 @@ def test_flags(sushi_test_project: Project): assert context.render("{{ flags.WHICH }}") == "parse" +def test_invocation_args_dict(sushi_test_project: Project): + context = sushi_test_project.context + + assert context.render("{{ invocation_args_dict['full_refresh'] }}") == "None" + assert context.render("{{ invocation_args_dict['store_failures'] }}") == "None" + assert context.render("{{ invocation_args_dict['which'] }}") == "parse" + + @pytest.mark.xdist_group("dbt_manifest") def test_context_namespace(sushi_test_project: Project): context = sushi_test_project.context From 12c568e5f8cf675cab98627ba996ae55ce486f30 Mon Sep 17 00:00:00 2001 From: Chris Rericha <67359577+crericha@users.noreply.github.com> Date: Tue, 9 Sep 2025 11:14:44 -0400 Subject: [PATCH 0834/1056] Chore: Separate jinja rendering and parsing try blocks for more precise error output (#5330) --- sqlmesh/core/renderer.py | 18 +++++++++++------- tests/core/test_model.py | 19 ++++++++++++++++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index 3502118e14..34fa5095e4 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -239,17 +239,21 @@ def _resolve_table(table: str | exp.Table) -> str: logger.debug( f"Rendered Jinja expression for model '{self._model_fqn}' at '{self._path}': '{rendered_expression}'" ) - if rendered_expression.strip(): + except ParsetimeAdapterCallError: + raise + except Exception as ex: + raise ConfigError(f"Could not render jinja at '{self._path}'.\n{ex}") from ex + + if rendered_expression.strip(): + try: expressions = [e for e in parse(rendered_expression, read=self._dialect) if e] if not expressions: raise ConfigError(f"Failed to parse an expression:\n{self._expression}") - except ParsetimeAdapterCallError: - raise - except Exception as ex: - raise ConfigError( - f"Could not render or parse jinja at '{self._path}'.\n{ex}" - ) from ex + except Exception as ex: + raise ConfigError( + f"Could not parse the rendered jinja at '{self._path}'.\n{ex}" + ) from ex if this_model: render_kwargs["this_model"] = this_model diff --git a/tests/core/test_model.py b/tests/core/test_model.py index a3159c8f0a..a252418a79 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -8588,6 +8588,23 @@ def test_comments_in_jinja_query(): model.render_query() +def test_jinja_render_parse_error(): + expressions = d.parse( + """ + MODEL (name db.test_model); + + JINJA_QUERY_BEGIN; + {{ unknown_macro() }} + JINJA_END; + """ + ) + + model = load_sql_based_model(expressions) + + with pytest.raises(ConfigError, match=r"Could not render jinja"): + model.render_query() + + def test_jinja_render_debug_logging(caplog): """Test that rendered Jinja expressions are logged for debugging.""" import logging @@ -8609,7 +8626,7 @@ def test_jinja_render_debug_logging(caplog): model = load_sql_based_model(expressions) # Attempt to render - this should fail due to invalid SQL syntax - with pytest.raises(ConfigError, match=r"Could not render or parse jinja"): + with pytest.raises(ConfigError, match=r"Could not parse the rendered jinja"): model.render_query() # Check that the rendered Jinja was logged From 0c8dcb5049ac8967b8d6ba996c15a21a8ac203dc Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:00:32 +0300 Subject: [PATCH 0835/1056] Fix: Make the dbt graph available during parse time too (#5329) --- sqlmesh/dbt/adapter.py | 7 ++----- tests/dbt/test_transformation.py | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/sqlmesh/dbt/adapter.py b/sqlmesh/dbt/adapter.py index 9e1ade1565..236d4cee6b 100644 --- a/sqlmesh/dbt/adapter.py +++ b/sqlmesh/dbt/adapter.py @@ -168,7 +168,8 @@ def compare_dbr_version(self, major: int, minor: int) -> int: @property def graph(self) -> t.Any: - return AttributeDict( + flat_graph = self.jinja_globals.get("flat_graph", None) + return flat_graph or AttributeDict( { "exposures": {}, "groups": {}, @@ -276,10 +277,6 @@ def __init__( **table_mapping, } - @property - def graph(self) -> t.Any: - return self.jinja_globals.get("flat_graph", super().graph) - def get_relation( self, database: t.Optional[str], schema: str, identifier: str ) -> t.Optional[BaseRelation]: diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index a769287aec..6779e196df 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -2197,7 +2197,7 @@ def test_on_run_start_end(): runtime_stage=RuntimeStage.BEFORE_ALL, ) - rendered_after_all = render_statements( + runtime_rendered_after_all = render_statements( root_environment_statements.after_all, dialect=sushi_context.default_dialect, python_env=root_environment_statements.python_env, @@ -2208,6 +2208,22 @@ def test_on_run_start_end(): engine_adapter=sushi_context.engine_adapter, ) + # not passing engine adapter simulates "parse-time" rendering + parse_time_rendered_after_all = render_statements( + root_environment_statements.after_all, + dialect=sushi_context.default_dialect, + python_env=root_environment_statements.python_env, + jinja_macros=root_environment_statements.jinja_macros, + snapshots=sushi_context.snapshots, + runtime_stage=RuntimeStage.AFTER_ALL, + environment_naming_info=EnvironmentNamingInfo(name="dev"), + ) + + # validate that the graph_table statement is the same between parse-time and runtime rendering + assert sorted(parse_time_rendered_after_all) == sorted(runtime_rendered_after_all) + graph_table_stmt = runtime_rendered_after_all[-1] + assert graph_table_stmt == parse_time_rendered_after_all[-1] + assert rendered_before_all == [ "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table TEXT, evaluation_time TEXT)", "CREATE TABLE IF NOT EXISTS to_be_executed_last (col TEXT)", @@ -2220,10 +2236,9 @@ def test_on_run_start_end(): "CREATE OR REPLACE TABLE schema_table_sushi__dev AS SELECT 'sushi__dev' AS schema", "DROP TABLE to_be_executed_last", ] - assert sorted(rendered_after_all[:-1]) == sorted(expected_statements) + assert sorted(runtime_rendered_after_all[:-1]) == sorted(expected_statements) # Assert the models with their materialisations are present in the rendered graph_table statement - graph_table_stmt = rendered_after_all[-1] assert "'model.sushi.simple_model_a' AS unique_id, 'table' AS materialized" in graph_table_stmt assert "'model.sushi.waiters' AS unique_id, 'ephemeral' AS materialized" in graph_table_stmt assert "'model.sushi.simple_model_b' AS unique_id, 'table' AS materialized" in graph_table_stmt From 32407f9967a2645fd7f9f2ed8f69f6b69f799e59 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:37:56 +0300 Subject: [PATCH 0836/1056] Chore: Provide additional info in jinja rendering errors (#5318) --- sqlmesh/core/renderer.py | 6 ++-- sqlmesh/utils/jinja.py | 26 +++++++++++++- tests/core/test_context.py | 71 ++++++++++++++++++++++++++++++-------- tests/dbt/test_model.py | 54 +++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 17 deletions(-) diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index 34fa5095e4..a4e0eb61ed 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -30,7 +30,7 @@ SQLMeshError, raise_config_error, ) -from sqlmesh.utils.jinja import JinjaMacroRegistry +from sqlmesh.utils.jinja import JinjaMacroRegistry, extract_error_details from sqlmesh.utils.metaprogramming import Executable, prepare_env if t.TYPE_CHECKING: @@ -242,7 +242,9 @@ def _resolve_table(table: str | exp.Table) -> str: except ParsetimeAdapterCallError: raise except Exception as ex: - raise ConfigError(f"Could not render jinja at '{self._path}'.\n{ex}") from ex + raise ConfigError( + f"Could not render jinja for '{self._path}'.\n" + extract_error_details(ex) + ) from ex if rendered_expression.strip(): try: diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index 74d498be38..c9339cf404 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -7,8 +7,11 @@ import zlib from collections import defaultdict from enum import Enum +from sys import exc_info +from traceback import walk_tb -from jinja2 import Environment, Template, nodes +from jinja2 import Environment, Template, nodes, UndefinedError +from jinja2.runtime import Macro from sqlglot import Dialect, Expression, Parser, TokenType from sqlmesh.core import constants as c @@ -664,3 +667,24 @@ def make_jinja_registry( jinja_registry = jinja_registry.trim(jinja_references) return jinja_registry + + +def extract_error_details(ex: Exception) -> str: + """Extracts a readable message from a Jinja2 error, to include missing name and macro.""" + + error_details = "" + if isinstance(ex, UndefinedError): + if match := re.search(r"'(\w+)'", str(ex)): + error_details += f"\nUndefined macro/variable: '{match.group(1)}'" + try: + _, _, exc_traceback = exc_info() + for frame, _ in walk_tb(exc_traceback): + if frame.f_code.co_name == "_invoke": + macro = frame.f_locals.get("self") + if isinstance(macro, Macro): + error_details += f" in macro: '{macro.name}'\n" + break + except: + # to fall back to the generic error message if frame analysis fails + pass + return error_details or str(ex) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 454f208db5..a9d6f7967f 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -631,6 +631,49 @@ def test_env_and_default_schema_normalization(mocker: MockerFixture): assert list(context.fetchdf('select c from "DEFAULT__DEV"."X"')["c"])[0] == 1 +def test_jinja_macro_undefined_variable_error(tmp_path: pathlib.Path): + models_dir = tmp_path / "models" + models_dir.mkdir(parents=True) + macros_dir = tmp_path / "macros" + macros_dir.mkdir(parents=True) + + macro_file = macros_dir / "my_macros.sql" + macro_file.write_text(""" +{%- macro generate_select(table_name) -%} + {%- if target.name == 'production' -%} + {%- set results = run_query('SELECT 1') -%} + {%- endif -%} + SELECT {{ results.columns[0].values()[0] }} FROM {{ table_name }} +{%- endmacro -%} +""") + + model_file = models_dir / "my_model.sql" + model_file.write_text(""" +MODEL ( + name my_schema.my_model, + kind FULL +); + +JINJA_QUERY_BEGIN; +{{ generate_select('users') }} +JINJA_END; +""") + + config_file = tmp_path / "config.yaml" + config_file.write_text(""" +model_defaults: + dialect: duckdb +""") + + with pytest.raises(ConfigError) as exc_info: + Context(paths=str(tmp_path)) + + error_message = str(exc_info.value) + assert "Failed to load model" in error_message + assert "Could not render jinja for" in error_message + assert "Undefined macro/variable: 'target' in macro: 'generate_select'" in error_message + + def test_clear_caches(tmp_path: pathlib.Path): models_dir = tmp_path / "models" @@ -2497,7 +2540,7 @@ def test_plan_min_intervals(tmp_path: Path): ), start '2020-01-01', cron '@daily' - ); + ); select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt; """) @@ -2510,9 +2553,9 @@ def test_plan_min_intervals(tmp_path: Path): ), start '2020-01-01', cron '@weekly' - ); + ); - select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt; + select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt; """) (tmp_path / "models" / "monthly_model.sql").write_text(""" @@ -2523,9 +2566,9 @@ def test_plan_min_intervals(tmp_path: Path): ), start '2020-01-01', cron '@monthly' - ); + ); - select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt; + select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt; """) (tmp_path / "models" / "ended_daily_model.sql").write_text(""" @@ -2537,9 +2580,9 @@ def test_plan_min_intervals(tmp_path: Path): start '2020-01-01', end '2020-01-18', cron '@daily' - ); + ); - select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt; + select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt; """) context.load() @@ -2672,7 +2715,7 @@ def test_plan_min_intervals_adjusted_for_downstream(tmp_path: Path): ), start '2020-01-01', cron '@hourly' - ); + ); select @start_dt as start_dt, @end_dt as end_dt; """) @@ -2681,11 +2724,11 @@ def test_plan_min_intervals_adjusted_for_downstream(tmp_path: Path): MODEL ( name sqlmesh_example.two_hourly_model, kind INCREMENTAL_BY_TIME_RANGE ( - time_column start_dt + time_column start_dt ), start '2020-01-01', cron '0 */2 * * *' - ); + ); select start_dt, end_dt from sqlmesh_example.hourly_model where start_dt between @start_dt and @end_dt; """) @@ -2694,11 +2737,11 @@ def test_plan_min_intervals_adjusted_for_downstream(tmp_path: Path): MODEL ( name sqlmesh_example.unrelated_monthly_model, kind INCREMENTAL_BY_TIME_RANGE ( - time_column start_dt + time_column start_dt ), start '2020-01-01', cron '@monthly' - ); + ); select @start_dt as start_dt, @end_dt as end_dt; """) @@ -2711,7 +2754,7 @@ def test_plan_min_intervals_adjusted_for_downstream(tmp_path: Path): ), start '2020-01-01', cron '@daily' - ); + ); select start_dt, end_dt from sqlmesh_example.hourly_model where start_dt between @start_dt and @end_dt; """) @@ -2724,7 +2767,7 @@ def test_plan_min_intervals_adjusted_for_downstream(tmp_path: Path): ), start '2020-01-01', cron '@weekly' - ); + ); select start_dt, end_dt from sqlmesh_example.daily_model where start_dt between @start_dt and @end_dt; """) diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index 14c042422e..bfc18144ef 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -5,6 +5,7 @@ from pathlib import Path from sqlglot import exp +from sqlglot.errors import SchemaError from sqlmesh import Context from sqlmesh.core.model import TimeColumn, IncrementalByTimeRangeKind from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange @@ -579,3 +580,56 @@ def test_load_microbatch_with_ref_no_filter( context.render(microbatch_two_snapshot_fqn, start="2025-01-01", end="2025-01-10").sql() == 'SELECT "microbatch"."cola" AS "cola", "microbatch"."ds" AS "ds" FROM "local"."main"."microbatch" AS "microbatch"' ) + + +@pytest.mark.slow +def test_dbt_jinja_macro_undefined_variable_error(create_empty_project): + project_dir, model_dir = create_empty_project() + + dbt_profile_config = { + "test": { + "outputs": { + "duckdb": { + "type": "duckdb", + "path": str(project_dir.parent / "dbt_data" / "main.db"), + } + }, + "target": "duckdb", + } + } + db_profile_file = project_dir / "profiles.yml" + with open(db_profile_file, "w", encoding="utf-8") as f: + YAML().dump(dbt_profile_config, f) + + macros_dir = project_dir / "macros" + macros_dir.mkdir() + + # the execute guard in the macro is so that dbt won't fail on the manifest loading earlier + macro_file = macros_dir / "my_macro.sql" + macro_file.write_text(""" +{%- macro select_columns(table_name) -%} + {% if execute %} + {%- if target.name == 'production' -%} + {%- set columns = run_query('SELECT column_name FROM information_schema.columns WHERE table_name = \'' ~ table_name ~ '\'') -%} + {%- endif -%} + SELECT {{ columns.rows[0][0] }} FROM {{ table_name }} + {%- endif -%} +{%- endmacro -%} +""") + + model_file = model_dir / "my_model.sql" + model_file.write_text(""" +{{ config( + materialized='table' +) }} + +{{ select_columns('users') }} +""") + + with pytest.raises(SchemaError) as exc_info: + Context(paths=project_dir) + + error_message = str(exc_info.value) + assert "Failed to update model schemas" in error_message + assert "Could not render jinja for" in error_message + assert "Undefined macro/variable: 'columns' in macro: 'select_columns'" in error_message From 4c42b455386eeb18ad38b101ef0bf859ba1c6aff Mon Sep 17 00:00:00 2001 From: Chris Rericha <67359577+crericha@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:00:25 -0400 Subject: [PATCH 0837/1056] Fix: Only include the event_time_filter when rendering a microbatch model (#5333) --- sqlmesh/core/renderer.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index a4e0eb61ed..18377e0258 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -221,17 +221,19 @@ def _resolve_table(table: str | exp.Table) -> str: dialect=self._dialect, identify=True, comments=False ) - all_refs = list( - self._jinja_macro_registry.global_objs.get("sources", {}).values() # type: ignore - ) + list( - self._jinja_macro_registry.global_objs.get("refs", {}).values() # type: ignore - ) - for ref in all_refs: - if ref.event_time_filter: - ref.event_time_filter["start"] = render_kwargs["start_tstz"] - ref.event_time_filter["end"] = to_tstz( - make_ts_exclusive(render_kwargs["end_tstz"], dialect=self._dialect) - ) + if self._model and self._model.kind.is_incremental_by_time_range: + all_refs = list( + self._jinja_macro_registry.global_objs.get("sources", {}).values() # type: ignore + ) + list( + self._jinja_macro_registry.global_objs.get("refs", {}).values() # type: ignore + ) + for ref in all_refs: + if ref.event_time_filter: + ref.event_time_filter["start"] = render_kwargs["start_tstz"] + ref.event_time_filter["end"] = to_tstz( + make_ts_exclusive(render_kwargs["end_tstz"], dialect=self._dialect) + ) + jinja_env = self._jinja_macro_registry.build_environment(**jinja_env_kwargs) expressions = [] From 0583f962d5d24518dfcfee083a75cf7d3128f3d6 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 10 Sep 2025 00:16:51 +0300 Subject: [PATCH 0838/1056] Feat(dbt): Add support for selected resources context variable (#5177) --- sqlmesh/core/context.py | 5 ++ sqlmesh/core/environment.py | 2 + sqlmesh/core/node.py | 1 + sqlmesh/core/plan/builder.py | 3 + sqlmesh/core/plan/definition.py | 4 + sqlmesh/core/plan/evaluator.py | 3 + sqlmesh/core/scheduler.py | 5 ++ sqlmesh/dbt/builtin.py | 1 + sqlmesh/dbt/model.py | 1 + sqlmesh/dbt/seed.py | 1 + .../migrations/v0097_add_dbt_name_in_node.py | 9 ++ tests/dbt/test_model.py | 79 +++++++++++++++++- tests/dbt/test_transformation.py | 82 +++++++++++++++++++ 13 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 sqlmesh/migrations/v0097_add_dbt_name_in_node.py diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 78a391d12f..0339f6506c 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1677,6 +1677,11 @@ def plan_builder( end_override_per_model=max_interval_end_per_model, console=self.console, user_provided_flags=user_provided_flags, + selected_models={ + dbt_name + for model in model_selector.expand_model_selections(select_models or "*") + if (dbt_name := snapshots[model].node.dbt_name) + }, explain=explain or False, ignore_cron=ignore_cron or False, ) diff --git a/sqlmesh/core/environment.py b/sqlmesh/core/environment.py index 2a0d4f115d..4a1f417468 100644 --- a/sqlmesh/core/environment.py +++ b/sqlmesh/core/environment.py @@ -312,6 +312,7 @@ def execute_environment_statements( start: t.Optional[TimeLike] = None, end: t.Optional[TimeLike] = None, execution_time: t.Optional[TimeLike] = None, + selected_models: t.Optional[t.Set[str]] = None, ) -> None: try: rendered_expressions = [ @@ -327,6 +328,7 @@ def execute_environment_statements( execution_time=execution_time, environment_naming_info=environment_naming_info, engine_adapter=adapter, + selected_models=selected_models, ) ] except Exception as e: diff --git a/sqlmesh/core/node.py b/sqlmesh/core/node.py index ea2264f7fa..b04a59a39f 100644 --- a/sqlmesh/core/node.py +++ b/sqlmesh/core/node.py @@ -199,6 +199,7 @@ class _Node(PydanticModel): interval_unit_: t.Optional[IntervalUnit] = Field(alias="interval_unit", default=None) tags: t.List[str] = [] stamp: t.Optional[str] = None + dbt_name: t.Optional[str] = None # dbt node name _path: t.Optional[Path] = None _data_hash: t.Optional[str] = None _metadata_hash: t.Optional[str] = None diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index a48812d16c..a84b3b60dc 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -129,6 +129,7 @@ def __init__( end_override_per_model: t.Optional[t.Dict[str, datetime]] = None, console: t.Optional[PlanBuilderConsole] = None, user_provided_flags: t.Optional[t.Dict[str, UserProvidedFlags]] = None, + selected_models: t.Optional[t.Set[str]] = None, ): self._context_diff = context_diff self._no_gaps = no_gaps @@ -169,6 +170,7 @@ def __init__( self._console = console or get_console() self._choices: t.Dict[SnapshotId, SnapshotChangeCategory] = {} self._user_provided_flags = user_provided_flags + self._selected_models = selected_models self._explain = explain self._start = start @@ -347,6 +349,7 @@ def build(self) -> Plan: ensure_finalized_snapshots=self._ensure_finalized_snapshots, ignore_cron=self._ignore_cron, user_provided_flags=self._user_provided_flags, + selected_models=self._selected_models, ) self._latest_plan = plan return plan diff --git a/sqlmesh/core/plan/definition.py b/sqlmesh/core/plan/definition.py index 2f3ddb5990..aaf6ec5dc0 100644 --- a/sqlmesh/core/plan/definition.py +++ b/sqlmesh/core/plan/definition.py @@ -70,6 +70,8 @@ class Plan(PydanticModel, frozen=True): execution_time_: t.Optional[TimeLike] = Field(default=None, alias="execution_time") user_provided_flags: t.Optional[t.Dict[str, UserProvidedFlags]] = None + selected_models: t.Optional[t.Set[str]] = None + """Models that have been selected for this plan (used for dbt selected_resources)""" @cached_property def start(self) -> TimeLike: @@ -282,6 +284,7 @@ def to_evaluatable(self) -> EvaluatablePlan: }, environment_statements=self.context_diff.environment_statements, user_provided_flags=self.user_provided_flags, + selected_models=self.selected_models, ) @cached_property @@ -319,6 +322,7 @@ class EvaluatablePlan(PydanticModel): disabled_restatement_models: t.Set[str] environment_statements: t.Optional[t.List[EnvironmentStatements]] = None user_provided_flags: t.Optional[t.Dict[str, UserProvidedFlags]] = None + selected_models: t.Optional[t.Set[str]] = None def is_selected_for_backfill(self, model_fqn: str) -> bool: return self.models_to_backfill is None or model_fqn in self.models_to_backfill diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 298d18a042..03b0b64016 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -137,6 +137,7 @@ def visit_before_all_stage(self, stage: stages.BeforeAllStage, plan: Evaluatable start=plan.start, end=plan.end, execution_time=plan.execution_time, + selected_models=plan.selected_models, ) def visit_after_all_stage(self, stage: stages.AfterAllStage, plan: EvaluatablePlan) -> None: @@ -150,6 +151,7 @@ def visit_after_all_stage(self, stage: stages.AfterAllStage, plan: EvaluatablePl start=plan.start, end=plan.end, execution_time=plan.execution_time, + selected_models=plan.selected_models, ) def visit_create_snapshot_records_stage( @@ -257,6 +259,7 @@ def visit_backfill_stage(self, stage: stages.BackfillStage, plan: EvaluatablePla allow_destructive_snapshots=plan.allow_destructive_models, allow_additive_snapshots=plan.allow_additive_models, selected_snapshot_ids=stage.selected_snapshot_ids, + selected_models=plan.selected_models, ) if errors: raise PlanError("Plan application failed.") diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index 44d6b14c10..ec204927d4 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -416,6 +416,7 @@ def run_merged_intervals( start: t.Optional[TimeLike] = None, end: t.Optional[TimeLike] = None, allow_destructive_snapshots: t.Optional[t.Set[str]] = None, + selected_models: t.Optional[t.Set[str]] = None, allow_additive_snapshots: t.Optional[t.Set[str]] = None, selected_snapshot_ids: t.Optional[t.Set[SnapshotId]] = None, run_environment_statements: bool = False, @@ -472,6 +473,7 @@ def run_merged_intervals( start=start, end=end, execution_time=execution_time, + selected_models=selected_models, ) # We only need to create physical tables if the snapshot is not representative or if it @@ -533,6 +535,7 @@ def run_node(node: SchedulingUnit) -> None: allow_destructive_snapshots=allow_destructive_snapshots, allow_additive_snapshots=allow_additive_snapshots, target_table_exists=snapshot.snapshot_id not in snapshots_to_create, + selected_models=selected_models, ) evaluation_duration_ms = now_timestamp() - execution_start_ts @@ -602,6 +605,7 @@ def run_node(node: SchedulingUnit) -> None: start=start, end=end, execution_time=execution_time, + selected_models=selected_models, ) self.state_sync.recycle() @@ -808,6 +812,7 @@ def _run_or_audit( run_environment_statements=run_environment_statements, audit_only=audit_only, auto_restatement_triggers=auto_restatement_triggers, + selected_models={s.node.dbt_name for s in merged_intervals if s.node.dbt_name}, ) return CompletionStatus.FAILURE if errors else CompletionStatus.SUCCESS diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 0503f1dc92..e284c11797 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -545,6 +545,7 @@ def create_builtin_globals( "run_query": sql_execution.run_query, "statement": sql_execution.statement, "graph": adapter.graph, + "selected_resources": list(jinja_globals.get("selected_models") or []), } ) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index a4ebf93ae5..3d5da1beaa 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -689,6 +689,7 @@ def to_sqlmesh( extract_dependencies_from_query=False, allow_partials=allow_partials, virtual_environment_mode=virtual_environment_mode, + dbt_name=self.node_name, **optional_kwargs, **model_kwargs, ) diff --git a/sqlmesh/dbt/seed.py b/sqlmesh/dbt/seed.py index 38cd635d91..d6ecc768f9 100644 --- a/sqlmesh/dbt/seed.py +++ b/sqlmesh/dbt/seed.py @@ -92,6 +92,7 @@ def to_sqlmesh( audit_definitions=audit_definitions, virtual_environment_mode=virtual_environment_mode, start=self.start or context.sqlmesh_config.model_defaults.start, + dbt_name=self.node_name, **kwargs, ) diff --git a/sqlmesh/migrations/v0097_add_dbt_name_in_node.py b/sqlmesh/migrations/v0097_add_dbt_name_in_node.py new file mode 100644 index 0000000000..f8909e4430 --- /dev/null +++ b/sqlmesh/migrations/v0097_add_dbt_name_in_node.py @@ -0,0 +1,9 @@ +"""Add 'dbt_name' property to node definition.""" + + +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index bfc18144ef..d3103d3681 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -201,7 +201,7 @@ def test_load_microbatch_all_defined( concurrent_batches=true ) }} - + SELECT 1 as cola, '2025-01-01' as ds """ microbatch_model_file = model_dir / "microbatch.sql" @@ -633,3 +633,80 @@ def test_dbt_jinja_macro_undefined_variable_error(create_empty_project): assert "Failed to update model schemas" in error_message assert "Could not render jinja for" in error_message assert "Undefined macro/variable: 'columns' in macro: 'select_columns'" in error_message + + +@pytest.mark.slow +def test_node_name_populated_for_dbt_models(dbt_dummy_postgres_config: PostgresConfig) -> None: + model_config = ModelConfig( + name="test_model", + package_name="test_package", + sql="SELECT 1 as id", + database="test_db", + schema_="test_schema", + alias="test_model", + ) + + context = DbtContext() + context.project_name = "test_project" + context.target = dbt_dummy_postgres_config + + # check after convert to SQLMesh model that node_name is populated correctly + sqlmesh_model = model_config.to_sqlmesh(context) + assert sqlmesh_model.dbt_name == "model.test_package.test_model" + + +@pytest.mark.slow +def test_load_model_dbt_node_name(tmp_path: Path) -> None: + yaml = YAML() + dbt_project_dir = tmp_path / "dbt" + dbt_project_dir.mkdir() + dbt_model_dir = dbt_project_dir / "models" + dbt_model_dir.mkdir() + + model_contents = "SELECT 1 as id, 'test' as name" + model_file = dbt_model_dir / "simple_model.sql" + with open(model_file, "w", encoding="utf-8") as f: + f.write(model_contents) + + dbt_project_config = { + "name": "test_project", + "version": "1.0.0", + "config-version": 2, + "profile": "test", + "model-paths": ["models"], + } + dbt_project_file = dbt_project_dir / "dbt_project.yml" + with open(dbt_project_file, "w", encoding="utf-8") as f: + yaml.dump(dbt_project_config, f) + + sqlmesh_config = { + "model_defaults": { + "start": "2025-01-01", + } + } + sqlmesh_config_file = dbt_project_dir / "sqlmesh.yaml" + with open(sqlmesh_config_file, "w", encoding="utf-8") as f: + yaml.dump(sqlmesh_config, f) + + dbt_data_dir = tmp_path / "dbt_data" + dbt_data_dir.mkdir() + dbt_data_file = dbt_data_dir / "local.db" + dbt_profile_config = { + "test": { + "outputs": {"duckdb": {"type": "duckdb", "path": str(dbt_data_file)}}, + "target": "duckdb", + } + } + db_profile_file = dbt_project_dir / "profiles.yml" + with open(db_profile_file, "w", encoding="utf-8") as f: + yaml.dump(dbt_profile_config, f) + + context = Context(paths=dbt_project_dir) + + # find the model by its sqlmesh fully qualified name + model_fqn = '"local"."main"."simple_model"' + assert model_fqn in context.snapshots + + # Verify that node_name is the equivalent dbt one + model = context.snapshots[model_fqn].model + assert model.dbt_name == "model.test_project.simple_model" diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 6779e196df..551c6cc16f 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -45,6 +45,7 @@ from sqlmesh.core.state_sync.db.snapshot import _snapshot_to_json from sqlmesh.dbt.builtin import _relation_info_to_relation, Config from sqlmesh.dbt.common import Dependencies +from sqlmesh.dbt.builtin import _relation_info_to_relation from sqlmesh.dbt.column import ( ColumnConfig, column_descriptions_to_sqlmesh, @@ -2375,3 +2376,84 @@ def test_dynamic_var_names_in_macro(sushi_test_project: Project): ) converted_model = model_config.to_sqlmesh(context) assert "dynamic_test_var" in converted_model.jinja_macros.global_objs["vars"] # type: ignore + + +def test_selected_resources_with_selectors(): + sushi_context = Context(paths=["tests/fixtures/dbt/sushi_test"]) + + # A plan with a specific model selection + plan_builder = sushi_context.plan_builder(select_models=["sushi.customers"]) + plan = plan_builder.build() + assert len(plan.selected_models) == 1 + selected_model = list(plan.selected_models)[0] + assert "customers" in selected_model + + # Plan without model selections should include all models + plan_builder = sushi_context.plan_builder() + plan = plan_builder.build() + assert plan.selected_models is not None + assert len(plan.selected_models) > 10 + + # with downstream models should select customers and at least one downstream model + plan_builder = sushi_context.plan_builder(select_models=["sushi.customers+"]) + plan = plan_builder.build() + assert plan.selected_models is not None + assert len(plan.selected_models) >= 2 + assert any("customers" in model for model in plan.selected_models) + + # Test wildcard selection + plan_builder = sushi_context.plan_builder(select_models=["sushi.waiter_*"]) + plan = plan_builder.build() + assert plan.selected_models is not None + assert len(plan.selected_models) >= 4 + assert all("waiter" in model for model in plan.selected_models) + + +@pytest.mark.xdist_group("dbt_manifest") +def test_selected_resources_context_variable( + sushi_test_project: Project, sushi_test_dbt_context: Context +): + context = sushi_test_project.context + + # empty selected resources + direct_access = context.render("{{ selected_resources }}") + assert direct_access == "[]" + + # selected_resources is iterable and count items + test_jinja = """ + {%- set resources = [] -%} + {%- for resource in selected_resources -%} + {%- do resources.append(resource) -%} + {%- endfor -%} + {{ resources | length }} + """ + result = context.render(test_jinja) + assert result.strip() == "0" + + # selected_resources in conditions + test_condition = """ + {%- if selected_resources -%} + has_resources + {%- else -%} + no_resources + {%- endif -%} + """ + result = context.render(test_condition) + assert result.strip() == "no_resources" + + # selected resources in dbt format + selected_resources = [ + "model.jaffle_shop.customers", + "model.jaffle_shop.items", + "model.jaffle_shop.orders", + ] + + # check the jinja macros rendering + result = context.render("{{ selected_resources }}", selected_resources=selected_resources) + assert result == selected_resources.__repr__() + + result = context.render(test_jinja, selected_resources=selected_resources) + assert result.strip() == "3" + + result = context.render(test_condition, selected_resources=selected_resources) + assert result.strip() == "has_resources" From a6945cb7b4a3d1a4809b14857b949c784d1e7139 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 9 Sep 2025 14:18:40 -0700 Subject: [PATCH 0839/1056] Fix: Preserve the DAG evaluation order even when a transitive dependency is not included (#5335) --- sqlmesh/core/scheduler.py | 57 +++++++++++++----- tests/core/test_integration.py | 77 ++++++++++++++++++++++++ tests/core/test_scheduler.py | 107 +++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 15 deletions(-) diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index ec204927d4..fd2e1cf004 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -446,7 +446,13 @@ def run_merged_intervals( if not selected_snapshots: selected_snapshots = list(merged_intervals) - snapshot_dag = snapshots_to_dag(selected_snapshots) + # Build the full DAG from all snapshots to preserve transitive dependencies + full_dag = snapshots_to_dag(self.snapshots.values()) + + # Create a subdag that includes the selected snapshots and all their upstream dependencies + # This ensures that transitive dependencies are preserved even when intermediate nodes are not selected + selected_snapshot_ids_set = {s.snapshot_id for s in selected_snapshots} + snapshot_dag = full_dag.subdag(*selected_snapshot_ids_set) batched_intervals = self.batch_intervals( merged_intervals, deployability_index, environment_naming_info, dag=snapshot_dag @@ -646,20 +652,11 @@ def _dag( upstream_dependencies: t.List[SchedulingUnit] = [] for p_sid in snapshot.parents: - if p_sid in self.snapshots: - p_intervals = intervals_per_snapshot.get(p_sid.name, []) - - if not p_intervals and p_sid in original_snapshots_to_create: - upstream_dependencies.append(CreateNode(snapshot_name=p_sid.name)) - elif len(p_intervals) > 1: - upstream_dependencies.append(DummyNode(snapshot_name=p_sid.name)) - else: - for i, interval in enumerate(p_intervals): - upstream_dependencies.append( - EvaluateNode( - snapshot_name=p_sid.name, interval=interval, batch_index=i - ) - ) + upstream_dependencies.extend( + self._find_upstream_dependencies( + p_sid, intervals_per_snapshot, original_snapshots_to_create + ) + ) batch_concurrency = snapshot.node.batch_concurrency batch_size = snapshot.node.batch_size @@ -703,6 +700,36 @@ def _dag( ) return dag + def _find_upstream_dependencies( + self, + parent_sid: SnapshotId, + intervals_per_snapshot: t.Dict[str, Intervals], + snapshots_to_create: t.Set[SnapshotId], + ) -> t.List[SchedulingUnit]: + if parent_sid not in self.snapshots: + return [] + + p_intervals = intervals_per_snapshot.get(parent_sid.name, []) + + if p_intervals: + if len(p_intervals) > 1: + return [DummyNode(snapshot_name=parent_sid.name)] + interval = p_intervals[0] + return [EvaluateNode(snapshot_name=parent_sid.name, interval=interval, batch_index=0)] + if parent_sid in snapshots_to_create: + return [CreateNode(snapshot_name=parent_sid.name)] + # This snapshot has no intervals and doesn't need creation which means + # that it can be a transitive dependency + transitive_deps: t.List[SchedulingUnit] = [] + parent_snapshot = self.snapshots[parent_sid] + for grandparent_sid in parent_snapshot.parents: + transitive_deps.extend( + self._find_upstream_dependencies( + grandparent_sid, intervals_per_snapshot, snapshots_to_create + ) + ) + return transitive_deps + def _run_or_audit( self, environment: str | EnvironmentNamingInfo, diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index ca0789d262..948882c4dc 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -1672,6 +1672,83 @@ def test_plan_ignore_cron( ) +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_run_respects_excluded_transitive_dependencies(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + # Graph: C <- B <- A + # B is a transitive dependency linking A and C + # Note that the alphabetical ordering of the model names is intentional and helps + # surface the problem + expressions_a = d.parse( + f""" + MODEL ( + name memory.sushi.test_model_c, + kind FULL, + allow_partials true, + cron '@hourly', + ); + + SELECT @execution_ts AS execution_ts + """ + ) + model_c = load_sql_based_model(expressions_a) + context.upsert_model(model_c) + + # A VIEW model with no partials allowed and a daily cron instead of hourly. + expressions_b = d.parse( + f""" + MODEL ( + name memory.sushi.test_model_b, + kind VIEW, + allow_partials false, + cron '@daily', + ); + + SELECT * FROM memory.sushi.test_model_c + """ + ) + model_b = load_sql_based_model(expressions_b) + context.upsert_model(model_b) + + expressions_a = d.parse( + f""" + MODEL ( + name memory.sushi.test_model_a, + kind FULL, + allow_partials true, + cron '@hourly', + ); + + SELECT * FROM memory.sushi.test_model_b + """ + ) + model_a = load_sql_based_model(expressions_a) + context.upsert_model(model_a) + + context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) + assert ( + context.fetchdf("SELECT execution_ts FROM memory.sushi.test_model_c")["execution_ts"].iloc[ + 0 + ] + == "2023-01-08 15:00:00" + ) + + with time_machine.travel("2023-01-08 17:00:00 UTC", tick=False): + context.run( + "prod", + select_models=["*test_model_c", "*test_model_a"], + no_auto_upstream=True, + ignore_cron=True, + ) + assert ( + context.fetchdf("SELECT execution_ts FROM memory.sushi.test_model_a")[ + "execution_ts" + ].iloc[0] + == "2023-01-08 17:00:00" + ) + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_run_with_select_models_no_auto_upstream( init_and_plan_context: t.Callable, diff --git a/tests/core/test_scheduler.py b/tests/core/test_scheduler.py index b894f60f58..71803f58a4 100644 --- a/tests/core/test_scheduler.py +++ b/tests/core/test_scheduler.py @@ -32,6 +32,7 @@ SnapshotEvaluator, SnapshotChangeCategory, DeployabilityIndex, + snapshots_to_dag, ) from sqlmesh.utils.date import to_datetime, to_timestamp, DatetimeRanges, TimeLike from sqlmesh.utils.errors import CircuitBreakerError, NodeAuditsErrors @@ -1019,3 +1020,109 @@ def record_execute_environment_statements(*args, **kwargs): execute_env_idx = call_order.index("execute_environment_statements") snapshots_to_create_idx = call_order.index("get_snapshots_to_create") assert env_statements_idx < execute_env_idx < snapshots_to_create_idx + + +def test_dag_transitive_deps(mocker: MockerFixture, make_snapshot): + # Create a simple dependency chain: A <- B <- C + snapshot_a = make_snapshot(SqlModel(name="a", query=parse_one("SELECT 1 as id"))) + snapshot_b = make_snapshot(SqlModel(name="b", query=parse_one("SELECT * FROM a"))) + snapshot_c = make_snapshot(SqlModel(name="c", query=parse_one("SELECT * FROM b"))) + + snapshot_b = snapshot_b.model_copy(update={"parents": (snapshot_a.snapshot_id,)}) + snapshot_c = snapshot_c.model_copy(update={"parents": (snapshot_b.snapshot_id,)}) + + snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot_c.categorize_as(SnapshotChangeCategory.BREAKING) + + scheduler = Scheduler( + snapshots=[snapshot_a, snapshot_b, snapshot_c], + snapshot_evaluator=mocker.Mock(), + state_sync=mocker.Mock(), + default_catalog=None, + ) + + # Test scenario: select only A and C (skip B) + merged_intervals = { + snapshot_a: [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))], + snapshot_c: [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))], + } + + deployability_index = DeployabilityIndex.create([snapshot_a, snapshot_b, snapshot_c]) + + full_dag = snapshots_to_dag([snapshot_a, snapshot_b, snapshot_c]) + + dag = scheduler._dag(merged_intervals, snapshot_dag=full_dag) + assert dag.graph == { + EvaluateNode( + snapshot_name='"a"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, + ): set(), + EvaluateNode( + snapshot_name='"c"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, + ): { + EvaluateNode( + snapshot_name='"a"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, + ) + }, + } + + +def test_dag_multiple_chain_transitive_deps(mocker: MockerFixture, make_snapshot): + # Create a more complex dependency graph: + # A <- B <- D <- E + # A <- C <- D <- E + # Select A and E only + snapshots = {} + for name in ["a", "b", "c", "d", "e"]: + snapshots[name] = make_snapshot(SqlModel(name=name, query=parse_one("SELECT 1 as id"))) + snapshots[name].categorize_as(SnapshotChangeCategory.BREAKING) + + # Set up dependencies + snapshots["b"] = snapshots["b"].model_copy(update={"parents": (snapshots["a"].snapshot_id,)}) + snapshots["c"] = snapshots["c"].model_copy(update={"parents": (snapshots["a"].snapshot_id,)}) + snapshots["d"] = snapshots["d"].model_copy( + update={"parents": (snapshots["b"].snapshot_id, snapshots["c"].snapshot_id)} + ) + snapshots["e"] = snapshots["e"].model_copy(update={"parents": (snapshots["d"].snapshot_id,)}) + + scheduler = Scheduler( + snapshots=list(snapshots.values()), + snapshot_evaluator=mocker.Mock(), + state_sync=mocker.Mock(), + default_catalog=None, + ) + + # Only provide intervals for A and E + batched_intervals = { + snapshots["a"]: [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))], + snapshots["e"]: [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))], + } + + # Create subdag including transitive dependencies + full_dag = snapshots_to_dag(snapshots.values()) + + dag = scheduler._dag(batched_intervals, snapshot_dag=full_dag) + assert dag.graph == { + EvaluateNode( + snapshot_name='"a"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, + ): set(), + EvaluateNode( + snapshot_name='"e"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, + ): { + EvaluateNode( + snapshot_name='"a"', + interval=(to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + batch_index=0, + ) + }, + } From fdf2f898d72e417b1624eef42ce1216da5283d7a Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:36:56 -0700 Subject: [PATCH 0840/1056] fix: dbt time column serialization (#5336) --- sqlmesh/dbt/model.py | 4 +--- tests/dbt/test_manifest.py | 9 ++++----- tests/dbt/test_model.py | 11 +++++++++-- .../sushi_test/models/waiter_as_customer_by_day.sql | 2 +- .../dbt/sushi_test/models/waiter_revenue_by_day.sql | 2 +- .../sushi_test/models/waiter_revenue_by_day_v1.sql | 2 +- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 3d5da1beaa..58a8ea7f29 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -31,7 +31,6 @@ OnAdditiveChange, on_destructive_change_validator, on_additive_change_validator, - TimeColumn, ) from sqlmesh.dbt.basemodel import BaseModelConfig, Materialization, SnapshotStrategy from sqlmesh.dbt.common import SqlStr, sql_str_validator @@ -86,7 +85,7 @@ class ModelConfig(BaseModelConfig): # sqlmesh fields sql: SqlStr = SqlStr("") - time_column: t.Optional[TimeColumn] = None + time_column: t.Optional[t.Union[str, t.Dict[str, str]]] = None cron: t.Optional[str] = None interval_unit: t.Optional[str] = None batch_concurrency: t.Optional[int] = None @@ -153,7 +152,6 @@ class ModelConfig(BaseModelConfig): _sql_validator = sql_str_validator _on_destructive_change_validator = on_destructive_change_validator _on_additive_change_validator = on_additive_change_validator - _time_column_validator = TimeColumn.validator() @field_validator( "unique_key", diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index 1ea94cceb0..e5e98eae49 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -5,7 +5,6 @@ import pytest from sqlmesh.core.config import ModelDefaultsConfig -from sqlmesh.core.model import TimeColumn from sqlmesh.dbt.basemodel import Dependencies from sqlmesh.dbt.common import ModelAttrs from sqlmesh.dbt.context import DbtContext @@ -82,9 +81,9 @@ def test_manifest_helper(caplog): macros=[MacroReference(name="ref")], ) assert waiter_as_customer_by_day_config.materialized == "incremental" - assert waiter_as_customer_by_day_config.incremental_strategy == "delete+insert" + assert waiter_as_customer_by_day_config.incremental_strategy == "incremental_by_time_range" assert waiter_as_customer_by_day_config.cluster_by == ["ds"] - assert waiter_as_customer_by_day_config.time_column == TimeColumn.create("ds", "duckdb") + assert waiter_as_customer_by_day_config.time_column == "ds" if DBT_VERSION >= (1, 5, 0): waiter_revenue_by_day_config = models["waiter_revenue_by_day_v2"] @@ -104,9 +103,9 @@ def test_manifest_helper(caplog): has_dynamic_var_names=True, ) assert waiter_revenue_by_day_config.materialized == "incremental" - assert waiter_revenue_by_day_config.incremental_strategy == "delete+insert" + assert waiter_revenue_by_day_config.incremental_strategy == "incremental_by_time_range" assert waiter_revenue_by_day_config.cluster_by == ["ds"] - assert waiter_revenue_by_day_config.time_column == TimeColumn.create("ds", "duckdb") + assert waiter_revenue_by_day_config.time_column == "ds" assert waiter_revenue_by_day_config.dialect_ == "bigquery" assert helper.models("customers")["customers"].dependencies == Dependencies( diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index d3103d3681..b343d9462b 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -9,6 +9,7 @@ from sqlmesh import Context from sqlmesh.core.model import TimeColumn, IncrementalByTimeRangeKind from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange +from sqlmesh.core.state_sync.db.snapshot import _snapshot_to_json from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.model import ModelConfig @@ -328,7 +329,8 @@ def test_load_incremental_time_range_strategy_required_only( snapshot_fqn = '"local"."main"."incremental_time_range"' context = Context(paths=project_dir) - model = context.snapshots[snapshot_fqn].model + snapshot = context.snapshots[snapshot_fqn] + model = snapshot.model # Validate model-level attributes assert model.start == "2025-01-01" assert model.interval_unit.is_day @@ -342,6 +344,8 @@ def test_load_incremental_time_range_strategy_required_only( assert model.depends_on_self is False assert model.kind.auto_restatement_intervals is None assert model.kind.partition_by_time_column is True + # make sure the snapshot can be serialized to json + assert isinstance(_snapshot_to_json(snapshot), str) @pytest.mark.slow @@ -381,7 +385,8 @@ def test_load_incremental_time_range_strategy_all_defined( snapshot_fqn = '"local"."main"."incremental_time_range"' context = Context(paths=project_dir) - model = context.snapshots[snapshot_fqn].model + snapshot = context.snapshots[snapshot_fqn] + model = snapshot.model # Validate model-level attributes assert model.start == "2025-01-01" assert model.interval_unit.is_day @@ -402,6 +407,8 @@ def test_load_incremental_time_range_strategy_all_defined( assert model.kind.batch_size == 3 assert model.kind.batch_concurrency == 2 assert model.depends_on_self is False + # make sure the snapshot can be serialized to json + assert isinstance(_snapshot_to_json(snapshot), str) @pytest.mark.slow diff --git a/tests/fixtures/dbt/sushi_test/models/waiter_as_customer_by_day.sql b/tests/fixtures/dbt/sushi_test/models/waiter_as_customer_by_day.sql index 82237634b5..ed845b67cb 100644 --- a/tests/fixtures/dbt/sushi_test/models/waiter_as_customer_by_day.sql +++ b/tests/fixtures/dbt/sushi_test/models/waiter_as_customer_by_day.sql @@ -1,7 +1,7 @@ {{ config( materialized='incremental', - incremental_strategy='delete+insert', + incremental_strategy='incremental_by_time_range', cluster_by=['ds'], time_column='ds', ) diff --git a/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql b/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql index 5eeb0002e0..2731b07019 100644 --- a/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql +++ b/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day.sql @@ -1,7 +1,7 @@ {{ config( materialized='incremental', - incremental_strategy='delete+insert', + incremental_strategy='incremental_by_time_range', cluster_by=['ds'], time_column='ds', dialect="bigquery" diff --git a/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day_v1.sql b/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day_v1.sql index 335e7ab799..e229dc8b91 100644 --- a/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day_v1.sql +++ b/tests/fixtures/dbt/sushi_test/models/waiter_revenue_by_day_v1.sql @@ -1,7 +1,7 @@ {{ config( materialized='incremental', - incremental_strategy='delete+insert', + incremental_strategy='incremental_by_time_range', cluster_by=['ds'], time_column='ds', dialect="bigquery" From cc2e1f1118583a259a9f7fca3bcb422f8d9a75e5 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 9 Sep 2025 15:40:35 -0700 Subject: [PATCH 0841/1056] Fix: Redundant creation of view models during the first evaluation (#5337) --- sqlmesh/core/snapshot/evaluator.py | 9 +++++++-- tests/core/test_snapshot_evaluator.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 3ed9c20765..961062fe45 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -50,7 +50,7 @@ CustomKind, ) from sqlmesh.core.model.kind import _Incremental -from sqlmesh.utils import CompletionStatus +from sqlmesh.utils import CompletionStatus, columns_to_types_all_known from sqlmesh.core.schema_diff import ( has_drop_alteration, TableAlterOperation, @@ -747,6 +747,11 @@ def _evaluate_snapshot( adapter.execute(model.render_pre_statements(**render_statements_kwargs)) if not target_table_exists or (model.is_seed and not snapshot.intervals): + columns_to_types_provided = ( + model.kind.is_materialized + and model.columns_to_types_ + and columns_to_types_all_known(model.columns_to_types_) + ) if self._can_clone(snapshot, deployability_index): self._clone_snapshot_in_dev( snapshot=snapshot, @@ -759,7 +764,7 @@ def _evaluate_snapshot( ) runtime_stage = RuntimeStage.EVALUATING target_table_exists = True - elif model.annotated or model.is_seed or model.kind.is_scd_type_2: + elif columns_to_types_provided or model.is_seed or model.kind.is_scd_type_2: self._execute_create( snapshot=snapshot, table_name=target_table_name, diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 955c7f7859..6a39f600de 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -646,7 +646,7 @@ def test_evaluate_materialized_view( ) # Ensure that the materialized view is recreated even if it exists - assert adapter_mock.create_view.assert_called + assert adapter_mock.create_view.call_count == 1 def test_evaluate_materialized_view_with_partitioned_by_cluster_by( From 913414de97e01fd95780778ec03823fa3a27dcbc Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 10 Sep 2025 09:32:21 -0700 Subject: [PATCH 0842/1056] Fix: Deployability of a child snapshot when its parent snapshot is indirect non-breaking without intervals (#5340) --- sqlmesh/core/snapshot/definition.py | 13 +++------- tests/core/test_integration.py | 40 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index c124c2098f..35109ec36e 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1644,6 +1644,7 @@ def create( snapshot.is_valid_start(start, snapshot_start) if start is not None else True ) + children_deployable = is_valid_start and not has_auto_restatement if ( snapshot.is_forward_only or snapshot.is_indirect_non_breaking @@ -1660,15 +1661,9 @@ def create( ): # This snapshot represents what's currently deployed in prod. representative_shared_version_ids.add(node) - - # A child can still be deployable even if its parent is not - children_deployable = ( - is_valid_start - and not ( - snapshot.is_paused and (snapshot.is_forward_only or is_forward_only_model) - ) - and not has_auto_restatement - ) + else: + # If the parent is not representative then its children can't be deployable. + children_deployable = False else: children_deployable = False if not snapshots[node].is_paused: diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 948882c4dc..8d4991adb9 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -1347,6 +1347,46 @@ def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_co ) +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_changes_downstream_of_indirect_non_breaking_snapshot_without_intervals( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Make a breaking change first but don't backfill it + model = context.get_model("sushi.orders") + model = model.copy(update={"stamp": "force new version"}) + context.upsert_model(model) + plan_builder = context.plan_builder( + "dev", skip_backfill=True, skip_tests=True, no_auto_categorization=True + ) + plan_builder.set_choice(context.get_snapshot(model), SnapshotChangeCategory.BREAKING) + context.apply(plan_builder.build()) + + # Now make a non-breaking change to the same snapshot. + model = model.copy(update={"stamp": "force another new version"}) + context.upsert_model(model) + plan_builder = context.plan_builder( + "dev", skip_backfill=True, skip_tests=True, no_auto_categorization=True + ) + plan_builder.set_choice(context.get_snapshot(model), SnapshotChangeCategory.NON_BREAKING) + context.apply(plan_builder.build()) + + # Now make a change to a model downstream of the above model. + downstream_model = context.get_model("sushi.top_waiters") + downstream_model = downstream_model.copy(update={"stamp": "yet another new version"}) + context.upsert_model(downstream_model) + plan = context.plan_builder("dev", skip_tests=True).build() + + # If the parent is not representative then the child cannot be deployable + deployability_index = plan.deployability_index + assert not deployability_index.is_representative( + context.get_snapshot("sushi.waiter_revenue_by_day") + ) + assert not deployability_index.is_deployable(context.get_snapshot("sushi.top_waiters")) + + @time_machine.travel("2023-01-08 15:00:00 UTC", tick=True) def test_metadata_change_after_forward_only_results_in_migration(init_and_plan_context: t.Callable): context, plan = init_and_plan_context("examples/sushi") From 746682c3ba95c5c99eae1df801c070c13e5d4a3c Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 10 Sep 2025 09:34:50 -0700 Subject: [PATCH 0843/1056] Chore: Fix the failing clickhouse test (#5339) --- .../integration/test_integration.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 42ff8b881f..ccea105bcf 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -2630,6 +2630,10 @@ def _mutate_config(current_gateway_name: str, config: Config): assert context.default_dialect == "duckdb" schema = ctx.schema(TEST_SCHEMA) + seed_columns_to_types = { + "item_id": exp.DataType.build("integer"), + "event_date": exp.DataType.build("date"), + } seed_query = ctx.input_data( pd.DataFrame( [ @@ -2643,13 +2647,15 @@ def _mutate_config(current_gateway_name: str, config: Config): ], columns=["item_id", "event_date"], ), - columns_to_types={ - "item_id": exp.DataType.build("integer"), - "event_date": exp.DataType.build("date"), - }, + columns_to_types=seed_columns_to_types, ) context.upsert_model( - create_sql_model(name=f"{schema}.seed_model", query=seed_query, kind="FULL") + create_sql_model( + name=f"{schema}.seed_model", + query=seed_query, + kind="FULL", + columns=seed_columns_to_types, + ) ) table_format = "" From 16a032f494f515aa135854b217075d6046dbb664 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:23:45 -0700 Subject: [PATCH 0844/1056] chore: improve cycle detection error (#5338) --- sqlmesh/utils/dag.py | 75 +++++++++++++++++++++++++++++++++++------ tests/utils/test_dag.py | 11 +++--- 2 files changed, 70 insertions(+), 16 deletions(-) diff --git a/sqlmesh/utils/dag.py b/sqlmesh/utils/dag.py index 1274e0616b..c39fd2a1d2 100644 --- a/sqlmesh/utils/dag.py +++ b/sqlmesh/utils/dag.py @@ -99,6 +99,53 @@ def upstream(self, node: T) -> t.Set[T]: return self._upstream[node] + def _find_cycle_path(self, nodes_in_cycle: t.Dict[T, t.Set[T]]) -> t.Optional[t.List[T]]: + """Find the exact cycle path using DFS when a cycle is detected. + + Args: + nodes_in_cycle: Dictionary of nodes that are part of the cycle and their dependencies + + Returns: + List of nodes forming the cycle path, or None if no cycle found + """ + if not nodes_in_cycle: + return None + + # Use DFS to find a cycle path + visited: t.Set[T] = set() + path: t.List[T] = [] + + def dfs(node: T) -> t.Optional[t.List[T]]: + if node in path: + # Found a cycle - extract the cycle path + cycle_start = path.index(node) + return path[cycle_start:] + [node] + + if node in visited: + return None + + visited.add(node) + path.append(node) + + # Only follow edges to nodes that are still in the unprocessed set + for neighbor in nodes_in_cycle.get(node, set()): + if neighbor in nodes_in_cycle: + cycle = dfs(neighbor) + if cycle: + return cycle + + path.pop() + return None + + # Try starting DFS from each unvisited node + for start_node in nodes_in_cycle: + if start_node not in visited: + cycle = dfs(start_node) + if cycle: + return cycle[:-1] # Remove the duplicate node at the end + + return None + @property def roots(self) -> t.Set[T]: """Returns all nodes in the graph without any upstream dependencies.""" @@ -125,23 +172,31 @@ def sorted(self) -> t.List[T]: next_nodes = {node for node, deps in unprocessed_nodes.items() if not deps} if not next_nodes: - # Sort cycle candidates to make the order deterministic - cycle_candidates_msg = ( - "\nPossible candidates to check for circular references: " - + ", ".join(str(node) for node in sorted(cycle_candidates)) - ) + # A cycle was detected - find the exact cycle path + cycle_path = self._find_cycle_path(unprocessed_nodes) - if last_processed_nodes: - last_processed_msg = "\nLast nodes added to the DAG: " + ", ".join( - str(node) for node in last_processed_nodes + last_processed_msg = "" + if cycle_path: + node_output = " ->\n".join( + str(node) for node in (cycle_path + [cycle_path[0]]) ) + cycle_msg = f"\nCycle:\n{node_output}" else: - last_processed_msg = "" + # Fallback message in case a cycle can't be found + cycle_candidates_msg = ( + "\nPossible candidates to check for circular references: " + + ", ".join(str(node) for node in sorted(cycle_candidates)) + ) + cycle_msg = cycle_candidates_msg + if last_processed_nodes: + last_processed_msg = "\nLast nodes added to the DAG: " + ", ".join( + str(node) for node in last_processed_nodes + ) raise SQLMeshError( "Detected a cycle in the DAG. " "Please make sure there are no circular references between nodes." - f"{last_processed_msg}{cycle_candidates_msg}" + f"{last_processed_msg}{cycle_msg}" ) for node in next_nodes: diff --git a/tests/utils/test_dag.py b/tests/utils/test_dag.py index 444e78555c..7c142ee4a0 100644 --- a/tests/utils/test_dag.py +++ b/tests/utils/test_dag.py @@ -57,8 +57,7 @@ def test_sorted_with_cycles(): expected_error_message = ( "Detected a cycle in the DAG. Please make sure there are no circular references between nodes.\n" - "Last nodes added to the DAG: c\n" - "Possible candidates to check for circular references: d, e" + "Cycle:\nd ->\ne ->\nd" ) assert expected_error_message == str(ex.value) @@ -70,7 +69,7 @@ def test_sorted_with_cycles(): expected_error_message = ( "Detected a cycle in the DAG. Please make sure there are no circular references between nodes.\n" - "Possible candidates to check for circular references: a, b, c" + "Cycle:\na ->\nb ->\nc ->\na" ) assert expected_error_message == str(ex.value) @@ -81,11 +80,11 @@ def test_sorted_with_cycles(): dag.sorted expected_error_message = ( - "Last nodes added to the DAG: c\n" - + "Possible candidates to check for circular references: b, d" + "Detected a cycle in the DAG. Please make sure there are no circular references between nodes.\n" + + "Cycle:\nb ->\nd ->\nb" ) - assert expected_error_message in str(ex.value) + assert expected_error_message == str(ex.value) def test_reversed_graph(): From 68fa7bcc8cacc6c3f42063e8f63385335bf8f158 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Wed, 10 Sep 2025 13:59:05 -0700 Subject: [PATCH 0845/1056] chore(web_common): move more shared components (#5320) --- pnpm-lock.yaml | 674 ++--- web/common/.storybook/main.ts | 6 +- web/common/eslint.config.mjs | 1 - web/common/package-lock.json | 2427 +++++++++++++++-- web/common/package.json | 17 +- web/common/src/components/Badge/Badge.tsx | 2 +- web/common/src/components/Button/Button.tsx | 1 + .../src/components/CopyButton/CopyButton.tsx | 1 + .../HorizontalContainer.stories.tsx | 13 +- .../src/components/Input/Input.stories.tsx | 121 + web/common/src/components/Input/Input.tsx | 51 + .../LoadingContainer.stories.tsx | 79 + .../LoadingContainer/LoadingContainer.tsx | 47 + .../LoadingContainer/LoadingIcon.tsx | 247 ++ .../MessageContainer.stories.tsx | 63 + .../MessageContainer/MessageContainer.tsx | 40 + .../components/Metadata/Metadata.stories.tsx | 40 + .../src/components/Metadata/Metadata.tsx | 37 + .../ModelName/ModelName.stories.tsx | 4 +- .../src/components/ModelName/ModelName.tsx | 15 +- .../ScrollContainer/ScrollContainer.css | 2 +- .../ScrollContainer.stories.tsx | 12 +- web/common/src/components/Tooltip/Tooltip.tsx | 10 +- .../src/components/Typography/Description.tsx | 21 + .../src/components/Typography/Headline.tsx | 31 + .../src/components/Typography/Information.tsx | 58 + .../src/components/Typography/Tagline.tsx | 20 + web/common/src/components/Typography/Text.tsx | 20 + .../Typography/Typography.stories.tsx | 87 + .../src/components/Typography/help.spec.ts | 23 + web/common/src/components/Typography/help.ts | 24 + .../VerticalContainer.stories.tsx | 13 +- .../components/VirtualList/FilterableList.tsx | 107 + .../VirtualList/VirtualList.stories.tsx | 238 ++ .../components/VirtualList/VirtualList.tsx | 118 + .../src/styles/design/semantic-colors.css | 16 + web/common/src/types.ts | 2 + web/common/tailwind.base.config.js | 16 + 38 files changed, 4028 insertions(+), 676 deletions(-) create mode 100644 web/common/src/components/Input/Input.stories.tsx create mode 100644 web/common/src/components/Input/Input.tsx create mode 100644 web/common/src/components/LoadingContainer/LoadingContainer.stories.tsx create mode 100644 web/common/src/components/LoadingContainer/LoadingContainer.tsx create mode 100644 web/common/src/components/LoadingContainer/LoadingIcon.tsx create mode 100644 web/common/src/components/MessageContainer/MessageContainer.stories.tsx create mode 100644 web/common/src/components/MessageContainer/MessageContainer.tsx create mode 100644 web/common/src/components/Metadata/Metadata.stories.tsx create mode 100644 web/common/src/components/Metadata/Metadata.tsx create mode 100644 web/common/src/components/Typography/Description.tsx create mode 100644 web/common/src/components/Typography/Headline.tsx create mode 100644 web/common/src/components/Typography/Information.tsx create mode 100644 web/common/src/components/Typography/Tagline.tsx create mode 100644 web/common/src/components/Typography/Text.tsx create mode 100644 web/common/src/components/Typography/Typography.stories.tsx create mode 100644 web/common/src/components/Typography/help.spec.ts create mode 100644 web/common/src/components/Typography/help.ts create mode 100644 web/common/src/components/VirtualList/FilterableList.tsx create mode 100644 web/common/src/components/VirtualList/VirtualList.stories.tsx create mode 100644 web/common/src/components/VirtualList/VirtualList.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 352b733183..daaf7eb993 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,7 +92,7 @@ importers: version: 8.38.0(eslint@9.31.0(jiti@2.4.2))(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.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) yaml: specifier: ^2.8.0 version: 2.8.0 @@ -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.43.1)(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.8.0)) '@tanstack/react-query': specifier: ^5.83.0 version: 5.83.0(react@18.3.1) @@ -128,7 +128,7 @@ importers: 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.43.1)(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))(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)) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -177,7 +177,7 @@ importers: 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) '@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.43.1)(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))(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)) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -192,10 +192,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@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.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)) '@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.43.1)(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.8.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) @@ -213,10 +213,10 @@ importers: 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.43.1)(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.8.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.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -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.43.1)(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.8.0)) ajv: specifier: ^8.17.1 version: 8.17.1 @@ -385,13 +385,13 @@ importers: version: 8.38.0(eslint@9.31.0(jiti@2.4.2))(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.43.1)(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.8.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.43.1)(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.8.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.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: '@swc/core-linux-x64-gnu': specifier: ^1.13.2 @@ -409,23 +409,17 @@ importers: specifier: ^1.2.8 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.2 - version: 9.1.2(@types/react@18.3.23)(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - '@storybook/addon-essentials': - specifier: ^9.0.0-alpha.12 - version: 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - '@storybook/addon-onboarding': - specifier: ^9.1.2 - version: 9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + 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))) '@storybook/react-vite': - specifier: ^9.1.2 - version: 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.2(@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - '@storybook/test': - specifier: ^8.6.14 - version: 8.6.14(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) + 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)) '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.16(tailwindcss@3.4.17) + '@tanstack/react-virtual': + specifier: ^3.13.12 + version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -446,10 +440,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.43.1)(tsx@4.20.3)(yaml@2.8.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)) '@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.43.1)(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.8.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) @@ -469,8 +463,11 @@ importers: specifier: ^5.2.0 version: 5.2.0(eslint@9.31.0(jiti@2.4.2)) eslint-plugin-storybook: - specifier: ^9.1.2 - version: 9.1.2(eslint@9.31.0(jiti@2.4.2))(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3) + 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) + fuse.js: + specifier: ^7.1.0 + version: 7.1.0 globals: specifier: ^16.3.0 version: 16.3.0 @@ -490,8 +487,8 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) storybook: - specifier: ^9.1.2 - version: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + 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)) syncpack: specifier: ^13.0.4 version: 13.0.4(typescript@5.8.3) @@ -499,8 +496,8 @@ importers: specifier: ^3.3.1 version: 3.3.1 tailwind-scrollbar: - specifier: ^4.0.2 - version: 4.0.2(react@18.3.1)(tailwindcss@3.4.17) + specifier: ^3.1.0 + version: 3.1.0(tailwindcss@3.4.17) tailwindcss: specifier: ^3.4.17 version: 3.4.17 @@ -512,16 +509,16 @@ importers: version: 8.38.0(eslint@9.31.0(jiti@2.4.2))(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.43.1)(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.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0)) + 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)) 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.43.1)(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.8.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.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) packages: @@ -769,6 +766,9 @@ packages: '@codemirror/autocomplete@6.18.6': resolution: {integrity: sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==} + '@codemirror/autocomplete@6.18.7': + resolution: {integrity: sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ==} + '@codemirror/commands@6.8.1': resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==} @@ -802,6 +802,9 @@ packages: '@codemirror/view@6.38.1': resolution: {integrity: sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==} + '@codemirror/view@6.38.2': + resolution: {integrity: sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==} + '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} @@ -1916,11 +1919,14 @@ packages: '@shikijs/engine-oniguruma@3.8.1': resolution: {integrity: sha512-KGQJZHlNY7c656qPFEQpIoqOuC4LrxjyNndRdzk5WKB/Ie87+NJCF1xo9KkOUxwxylk7rT6nhlZyTGTC4fCe1g==} - '@shikijs/langs@3.8.1': - resolution: {integrity: sha512-TjOFg2Wp1w07oKnXjs0AUMb4kJvujML+fJ1C5cmEj45lhjbUXtziT1x2bPQb9Db6kmPhkG5NI2tgYW1/DzhUuQ==} + '@shikijs/langs@3.12.2': + resolution: {integrity: sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==} + + '@shikijs/themes@3.12.2': + resolution: {integrity: sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==} - '@shikijs/themes@3.8.1': - resolution: {integrity: sha512-Vu3t3BBLifc0GB0UPg2Pox1naTemrrvyZv2lkiSw3QayVV60me1ujFQwPZGgUTmwXl1yhCPW8Lieesm0CYruLQ==} + '@shikijs/types@3.12.2': + resolution: {integrity: sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==} '@shikijs/types@3.8.1': resolution: {integrity: sha512-5C39Q8/8r1I26suLh+5TPk1DTrbY/kn3IdWA5HdizR0FhlhD05zx5nKCqhzSfDHH3p4S0ZefxWd77DLV+8FhGg==} @@ -2013,51 +2019,21 @@ packages: peerDependencies: storybook: ^9.0.18 - '@storybook/addon-backgrounds@9.0.0-alpha.12': - resolution: {integrity: sha512-oiQL8GIs2jNhN1cfbWa6iJIdey/WC+TFlmIeoWzYsJ79EQCxpL5JgmzCMGIkZ+p7L4MUR/5S5b5fh6ApyWcUKw==} - peerDependencies: - storybook: ^9.0.0-alpha.12 - '@storybook/addon-docs@9.0.18': resolution: {integrity: sha512-1mLhaRDx8s1JAF51o56OmwMnIsg4BOQJ8cn+4wbMjh14pDFALrovlFl/BpAXnV1VaZqHjCB4ZWuP+y5CwXEpeQ==} peerDependencies: storybook: ^9.0.18 - '@storybook/addon-docs@9.1.2': - resolution: {integrity: sha512-U3eHJ8lQFfEZ/OcgdKkUBbW2Y2tpAsHfy8lQOBgs5Pgj9biHEJcUmq+drOS/sJhle673eoBcUFmspXulI4KP1w==} + '@storybook/addon-docs@9.1.5': + resolution: {integrity: sha512-q1j5RRElxFSnHOh60eS3dS2TAyAHzcQeH/2B9UXo6MUHu7HmhNpw3qt2YibIw0zEogHCvZhLNx6TNzSy+7wRUw==} peerDependencies: - storybook: ^9.1.2 - - '@storybook/addon-essentials@9.0.0-alpha.12': - resolution: {integrity: sha512-wmUT9Q4rl6SvVgrIYDj97uHHkMSGba1A+/rMHypIw7OtrdUp+w1OKZRDNVrU0AfqfbaptT5dRrBsDr/eFZ9v8Q==} - peerDependencies: - storybook: ^9.0.0-alpha.12 - - '@storybook/addon-highlight@9.0.0-alpha.12': - resolution: {integrity: sha512-b8E1AjBaWFvBoWUfXXlAYfAIanuaHLZwJhmOcqJGtbx9RIC5uHfyGC8KHJgeyKMzvHhZD86vWBo5KUAFLFVUrg==} - peerDependencies: - storybook: ^9.0.0-alpha.12 - - '@storybook/addon-measure@9.0.0-alpha.12': - resolution: {integrity: sha512-ZtAKi/mlvVYaBMlPokvrHF94YFsyYAlz3IpKu+uz5QymN3VweSIgGsDJmAqV49lVzyVk40KWCVypi4O3L7nvdQ==} - peerDependencies: - storybook: ^9.0.0-alpha.12 + storybook: ^9.1.5 '@storybook/addon-onboarding@9.0.18': resolution: {integrity: sha512-A079BfJ3g3wYOtAuq9cPf2l6JHo+6UzEw1A2AbSNBBNP4hKfXpHcLadIVwuyOxuKjDUWzY5f4dJa3hCMurHXGQ==} peerDependencies: storybook: ^9.0.18 - '@storybook/addon-onboarding@9.1.2': - resolution: {integrity: sha512-WfYIBmRtwUF13Hcu6BdsqATsAuBK0dwsz7O4tL0FGrIwY/vdzZ5jNzYvzzgilzlu9QiPvzEIBvs6X4BVulN3LQ==} - peerDependencies: - storybook: ^9.1.2 - - '@storybook/addon-outline@9.0.0-alpha.12': - resolution: {integrity: sha512-I7opVIK8bNUYSC+P+b8AwP6sE2pFyXH5F0gz8WA0pdkRcxerQmYnhlsXrI5T0QMu79tZnjVNrQTUrqpy/Z5oqQ==} - peerDependencies: - storybook: ^9.0.0-alpha.12 - '@storybook/addon-vitest@9.0.18': resolution: {integrity: sha512-uPLh9H7kRho+raxyIBCm8Ymd3j0VPuWIQ1HSAkdx8itmNafNqs4HE67Z8Cfl259YzdWU/j5BhZqoiT62BCbIDw==} peerDependencies: @@ -2079,10 +2055,10 @@ packages: storybook: ^9.0.18 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - '@storybook/builder-vite@9.1.2': - resolution: {integrity: sha512-5Y7e5wnSzFxCGP63UNRRZVoxHe1znU4dYXazJBobAlEcUPBk7A0sH2716tA6bS4oz92oG9tgvn1g996hRrw4ow==} + '@storybook/builder-vite@9.1.5': + resolution: {integrity: sha512-sgt/9+Yl/5O7Bj5hdbHfadN8e/e4CNiDZKDcbLOMpOjKKoqF8vm19I1QocWIAiKjTOhF+4E9v9LddjtAGnfqHQ==} peerDependencies: - storybook: ^9.1.2 + storybook: ^9.1.5 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 '@storybook/csf-plugin@9.0.18': @@ -2090,10 +2066,10 @@ packages: peerDependencies: storybook: ^9.0.18 - '@storybook/csf-plugin@9.1.2': - resolution: {integrity: sha512-bfMh6r+RieBLPWtqqYN70le2uTE4JzOYPMYSCagHykUti3uM/1vRFaZNkZtUsRy5GwEzE5jLdDXioG1lOEeT2Q==} + '@storybook/csf-plugin@9.1.5': + resolution: {integrity: sha512-PmHuF+j11Z7BxAI2/4wQYn0gH1d67gNvycyR+EWgp4P/AWam9wFbuI/T1R45CRQTV2/VrfGdts/tFrvo5kXWig==} peerDependencies: - storybook: ^9.1.2 + storybook: ^9.1.5 '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} @@ -2105,11 +2081,6 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - '@storybook/instrumenter@8.6.14': - resolution: {integrity: sha512-iG4MlWCcz1L7Yu8AwgsnfVAmMbvyRSk700Mfy2g4c8y5O+Cv1ejshE1LBBsCwHgkuqU0H4R0qu4g23+6UnUemQ==} - peerDependencies: - storybook: ^8.6.14 - '@storybook/react-dom-shim@9.0.18': resolution: {integrity: sha512-qGR/d9x9qWRRxITaBVQkMnb73kwOm+N8fkbZRxc7U4lxupXRvkMIDh247nn71SYVBnvbh6//AL7P6ghiPWZYjA==} peerDependencies: @@ -2117,12 +2088,12 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta storybook: ^9.0.18 - '@storybook/react-dom-shim@9.1.2': - resolution: {integrity: sha512-nw7BLAHCJswPZGsuL0Gs2AvFUWriusCTgPBmcHppSw/AqvT4XRFRDE+5q3j04/XKuZBrAA2sC4L+HuC0uzEChQ==} + '@storybook/react-dom-shim@9.1.5': + resolution: {integrity: sha512-blSq9uzSYnfgEYPHYKgM5O14n8hbXNiXx2GiVJyDSg8QPNicbsBg+lCb1TC7/USfV26pNZr/lGNNKGkcCEN6Gw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.2 + storybook: ^9.1.5 '@storybook/react-vite@9.0.18': resolution: {integrity: sha512-dHzUoeY0/S35TvSYxCkPuBlNQZx4Zj9QDhAZ0qdv+nSll++uPgqSe2y2vF+2p+XVYhjDn+YX5LORv00YtuQezg==} @@ -2133,13 +2104,13 @@ packages: storybook: ^9.0.18 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - '@storybook/react-vite@9.1.2': - resolution: {integrity: sha512-dv3CBjOzmMoSyIotMtdmsBRjB25i19OjFP0IZqauLeUoVm6QddILW7JRcZVLrzhATyBEn+sEAdWQ4j79Z11HAg==} + '@storybook/react-vite@9.1.5': + resolution: {integrity: sha512-OYbkHHNCrn8MNPd+4KxMjcSR4M/YHa84h8sWDUHhKRTRtZFmj8i/QDW3E8tGx2BRLxXw3dTYe9J5UYBhJDDxFA==} engines: {node: '>=20.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.2 + storybook: ^9.1.5 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 '@storybook/react@9.0.18': @@ -2154,23 +2125,18 @@ packages: typescript: optional: true - '@storybook/react@9.1.2': - resolution: {integrity: sha512-VVXu1HrhDExj/yj+heFYc8cgIzBruXy1UYT3LW0WiJyadgzYz3J41l/Lf/j2FCppyxwlXb19Uv51plb1F1C77w==} + '@storybook/react@9.1.5': + resolution: {integrity: sha512-fBVP7Go09gzpImtaMcZ2DipLEWdWeTmz7BrACr3Z8uCyKcoH8/d1Wv0JgIiBo1UKDh5ZgYx5pLafaPNqmVAepg==} engines: {node: '>=20.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.1.2 + storybook: ^9.1.5 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: optional: true - '@storybook/test@8.6.14': - resolution: {integrity: sha512-GkPNBbbZmz+XRdrhMtkxPotCLOQ1BaGNp/gFZYdGDk2KmUWBKmvc5JxxOhtoXM2703IzNFlQHSSNnhrDZYuLlw==} - peerDependencies: - storybook: ^8.6.14 - '@swc/core-darwin-arm64@1.13.2': resolution: {integrity: sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==} engines: {node: '>=10'} @@ -2457,18 +2423,10 @@ packages: resolution: {integrity: sha512-a+MxoAXG+Sq94Jp67OtveKOp2vQq75AWdVI8DRt6w19B0NEqpfm784FTLbVp/qdR1wmxCOmKAvElGSIiBOx5OQ==} engines: {node: '>=12'} - '@testing-library/dom@10.4.0': - resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} - engines: {node: '>=18'} - '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.5.0': - resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/jest-dom@6.6.3': resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} @@ -2488,12 +2446,6 @@ packages: '@types/react-dom': optional: true - '@testing-library/user-event@14.5.2': - resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - '@testing-library/user-event@14.6.1': resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} engines: {node: '>=12', npm: '>=6'} @@ -2704,9 +2656,6 @@ packages: '@types/pluralize@0.0.33': resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} - '@types/prismjs@1.26.5': - resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} - '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -2884,9 +2833,6 @@ packages: '@vitest/browser': optional: true - '@vitest/expect@2.0.5': - resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} - '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -2912,12 +2858,6 @@ packages: vite: optional: true - '@vitest/pretty-format@2.0.5': - resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} - - '@vitest/pretty-format@2.1.9': - resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@vitest/pretty-format@3.2.3': resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==} @@ -2930,9 +2870,6 @@ packages: '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} - '@vitest/spy@2.0.5': - resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} - '@vitest/spy@3.2.3': resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==} @@ -2944,12 +2881,6 @@ packages: peerDependencies: vitest: 3.2.4 - '@vitest/utils@2.0.5': - resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} - - '@vitest/utils@2.1.9': - resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - '@vitest/utils@3.2.3': resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==} @@ -3352,8 +3283,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.25.2: - resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==} + browserslist@4.25.4: + resolution: {integrity: sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3412,8 +3343,8 @@ packages: caniuse-lite@1.0.30001727: resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} - caniuse-lite@1.0.30001735: - resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} + caniuse-lite@1.0.30001741: + resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -3834,8 +3765,8 @@ packages: electron-to-chromium@1.5.190: resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==} - electron-to-chromium@1.5.204: - resolution: {integrity: sha512-s9VbBXWxfDrl67PlO4avwh0/GU2vcwx8Fph3wlR8LJl7ySGYId59EFE17VWVcuC3sLWNPENm6Z/uGqKbkPCcXA==} + electron-to-chromium@1.5.215: + resolution: {integrity: sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==} elkjs@0.8.2: resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} @@ -3944,12 +3875,12 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-storybook@9.1.2: - resolution: {integrity: sha512-EQa/kChrYrekxv36q3pvW57anqxMlAP4EdPXEDyA/EDrCQJaaTbWEdsMnVZtD744RjPP0M5wzaUjHbMhNooAwQ==} + eslint-plugin-storybook@9.1.5: + resolution: {integrity: sha512-vCfaZ2Wk1N1vvK4vmNZoA6y2CYxJwbgIs6BE8/toPf4Z6hCAipoobP6a/30Rs0g/B2TSxTSj41TfrJKJrowpjQ==} engines: {node: '>=20.0.0'} peerDependencies: eslint: '>=8' - storybook: ^9.1.2 + storybook: ^9.1.5 eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} @@ -4172,6 +4103,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + fuse.js@7.1.0: + resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} + engines: {node: '>=10'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -4963,9 +4898,6 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - map-or-similar@1.5.0: - resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} - markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true @@ -5001,9 +4933,6 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - memoizerific@1.11.3: - resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -5213,6 +5142,9 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.20: + resolution: {integrity: sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==} + node-sarif-builder@3.2.0: resolution: {integrity: sha512-kVIOdynrF2CRodHZeP/97Rh1syTUHBNiw17hUCIVhlhEsWlfJm19MuO56s4MdKbr22xWx6mzMnNAgXzVlIYM9Q==} engines: {node: '>=18'} @@ -5537,11 +5469,6 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - prism-react-renderer@2.4.1: - resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==} - peerDependencies: - react: '>=16.0.0' - proc-log@5.0.0: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -5867,6 +5794,12 @@ packages: peerDependencies: seroval: ^1.0 + seroval-plugins@1.3.3: + resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + seroval@1.3.2: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} @@ -6036,8 +5969,8 @@ packages: prettier: optional: true - storybook@9.1.2: - resolution: {integrity: sha512-TYcq7WmgfVCAQge/KueGkVlM/+g33sQcmbATlC3X6y/g2FEeSSLGrb6E6d3iemht8oio+aY6ld3YOdAnMwx45Q==} + storybook@9.1.5: + resolution: {integrity: sha512-cGwJ2AE6nxlwqQlOiI+HKX5qa7+FOV7Ha7Qa+GoASBIQSSnLfbY6UldgAxHCJGJOFtgW/wuqfDtNvni6sj1/OQ==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -6184,11 +6117,11 @@ packages: tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} - tailwind-scrollbar@4.0.2: - resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==} + tailwind-scrollbar@3.1.0: + resolution: {integrity: sha512-pmrtDIZeHyu2idTejfV59SbaJyvp1VRjYxAjZBH0jnyrPRo6HL1kD5Glz8VPagasqr6oAx6M05+Tuw429Z8jxg==} engines: {node: '>=12.13.0'} peerDependencies: - tailwindcss: 4.x + tailwindcss: 3.x tailwindcss@3.4.17: resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} @@ -6202,6 +6135,10 @@ packages: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} engines: {node: '>=6'} + tapable@2.2.3: + resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} + engines: {node: '>=6'} + tar-fs@2.1.3: resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==} @@ -6233,8 +6170,8 @@ packages: uglify-js: optional: true - terser@5.43.1: - resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + terser@5.44.0: + resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} engines: {node: '>=10'} hasBin: true @@ -6291,18 +6228,10 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} - tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} - engines: {node: '>=14.0.0'} - tinyspy@4.0.3: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} @@ -7295,6 +7224,13 @@ snapshots: '@codemirror/view': 6.38.1 '@lezer/common': 1.2.3 + '@codemirror/autocomplete@6.18.7': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.2 + '@lezer/common': 1.2.3 + '@codemirror/commands@6.8.1': dependencies: '@codemirror/language': 6.11.2 @@ -7331,7 +7267,7 @@ snapshots: '@codemirror/language@6.11.3': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.1 + '@codemirror/view': 6.38.2 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 @@ -7344,13 +7280,13 @@ snapshots: '@codemirror/lint@6.8.5': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.1 + '@codemirror/view': 6.38.2 crelt: 1.0.6 '@codemirror/search@6.5.10': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.1 + '@codemirror/view': 6.38.2 crelt: 1.0.6 '@codemirror/state@6.5.2': @@ -7361,7 +7297,7 @@ snapshots: dependencies: '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.1 + '@codemirror/view': 6.38.2 '@lezer/highlight': 1.2.1 '@codemirror/view@6.38.1': @@ -7371,6 +7307,13 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 + '@codemirror/view@6.38.2': + dependencies: + '@codemirror/state': 6.5.2 + crelt: 1.0.6 + style-mod: 4.1.2 + w3c-keyname: 2.2.8 + '@csstools/color-helpers@5.0.2': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -7570,9 +7513,9 @@ snapshots: '@gerrit0/mini-shiki@3.8.1': dependencies: '@shikijs/engine-oniguruma': 3.8.1 - '@shikijs/langs': 3.8.1 - '@shikijs/themes': 3.8.1 - '@shikijs/types': 3.8.1 + '@shikijs/langs': 3.12.2 + '@shikijs/themes': 3.12.2 + '@shikijs/types': 3.12.2 '@shikijs/vscode-textmate': 10.0.2 '@headlessui/react@2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -7641,21 +7584,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.43.1)(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.8.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.43.1)(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) 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.43.1)(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.8.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.43.1)(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.8.0) optionalDependencies: typescript: 5.8.3 @@ -8559,13 +8502,18 @@ snapshots: '@shikijs/types': 3.8.1 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.8.1': + '@shikijs/langs@3.12.2': dependencies: - '@shikijs/types': 3.8.1 + '@shikijs/types': 3.12.2 - '@shikijs/themes@3.8.1': + '@shikijs/themes@3.12.2': dependencies: - '@shikijs/types': 3.8.1 + '@shikijs/types': 3.12.2 + + '@shikijs/types@3.12.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 '@shikijs/types@3.8.1': dependencies: @@ -8747,13 +8695,6 @@ snapshots: axe-core: 4.10.3 storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) - '@storybook/addon-backgrounds@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': - dependencies: - '@storybook/global': 5.0.0 - memoizerific: 1.11.3 - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - ts-dedent: 2.2.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))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.23)(react@18.3.1) @@ -8767,53 +8708,23 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@storybook/addon-docs@9.1.2(@types/react@18.3.23)(storybook@9.1.2(@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.43.1)(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)(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)))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.23)(react@18.3.1) - '@storybook/csf-plugin': 9.1.2(storybook@9.1.2(@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.43.1)(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)(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/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-dom-shim': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(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)(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))) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.2(@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.43.1)(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)) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-essentials@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': - dependencies: - '@storybook/addon-backgrounds': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - '@storybook/addon-highlight': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - '@storybook/addon-measure': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - '@storybook/addon-outline': 9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - ts-dedent: 2.2.0 - - '@storybook/addon-highlight@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': - dependencies: - '@storybook/global': 5.0.0 - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - - '@storybook/addon-measure@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': - dependencies: - '@storybook/global': 5.0.0 - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - tiny-invariant: 1.3.3 - '@storybook/addon-onboarding@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': dependencies: storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) - '@storybook/addon-onboarding@9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': - dependencies: - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - - '@storybook/addon-outline@9.0.0-alpha.12(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': - dependencies: - '@storybook/global': 5.0.0 - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - ts-dedent: 2.2.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)': dependencies: '@storybook/global': 5.0.0 @@ -8822,35 +8733,35 @@ snapshots: storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) 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.43.1)(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.8.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.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.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.43.1)(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))(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))': 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) ts-dedent: 2.2.0 - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.8.0) - '@storybook/builder-vite@9.1.2(storybook@9.1.2(@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.43.1)(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.43.1)(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)(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))': dependencies: - '@storybook/csf-plugin': 9.1.2(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - storybook: 9.1.2(@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.43.1)(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)(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)) ts-dedent: 2.2.0 - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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/csf-plugin@9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': dependencies: storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) unplugin: 1.16.1 - '@storybook/csf-plugin@9.1.2(storybook@9.1.2(@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.43.1)(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)(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)))': dependencies: - storybook: 9.1.2(@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.43.1)(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)) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -8860,29 +8771,23 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/instrumenter@8.6.14(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': - dependencies: - '@storybook/global': 5.0.0 - '@vitest/utils': 2.1.9 - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.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))': 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/react-dom-shim@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(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)(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)))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.2(@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.43.1)(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/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.43.1)(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))(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))': 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.43.1)(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.8.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.43.1)(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))(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) find-up: 7.0.0 magic-string: 0.30.17 @@ -8892,27 +8797,27 @@ snapshots: resolve: 1.22.10 storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) tsconfig-paths: 4.2.0 - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.8.0) transitivePeerDependencies: - rollup - supports-color - typescript - '@storybook/react-vite@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.2(@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.43.1)(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.43.1)(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)(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))': 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.43.1)(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.8.0)) '@rollup/pluginutils': 5.2.0(rollup@4.45.1) - '@storybook/builder-vite': 9.1.2(storybook@9.1.2(@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.43.1)(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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - '@storybook/react': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(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)(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) find-up: 7.0.0 magic-string: 0.30.17 react: 18.3.1 react-docgen: 8.0.0 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 - storybook: 9.1.2(@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.43.1)(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)) tsconfig-paths: 4.2.0 - vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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) transitivePeerDependencies: - rollup - supports-color @@ -8928,27 +8833,16 @@ snapshots: optionalDependencies: typescript: 5.8.3 - '@storybook/react@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(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)(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)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@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.43.1)(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)(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))) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.2(@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.43.1)(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)) optionalDependencies: typescript: 5.8.3 - '@storybook/test@8.6.14(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)))': - dependencies: - '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.6.14(storybook@9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0))) - '@testing-library/dom': 10.4.0 - '@testing-library/jest-dom': 6.5.0 - '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) - '@vitest/expect': 2.0.5 - '@vitest/spy': 2.0.5 - storybook: 9.1.2(@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.43.1)(tsx@4.20.3)(yaml@2.8.0)) - '@swc/core-darwin-arm64@1.13.2': optional: true @@ -9090,12 +8984,12 @@ 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.43.1)(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.8.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.43.1)(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.8.0) '@tanstack/history@1.129.7': {} @@ -9181,7 +9075,7 @@ 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.43.1)(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))(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))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) @@ -9199,7 +9093,7 @@ snapshots: 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.43.1)(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.8.0) webpack: 5.99.8(esbuild@0.25.8) transitivePeerDependencies: - supports-color @@ -9223,17 +9117,6 @@ snapshots: '@tanstack/virtual-file-routes@1.129.7': {} - '@testing-library/dom@10.4.0': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.28.2 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 @@ -9245,16 +9128,6 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.5.0': - dependencies: - '@adobe/css-tools': 4.4.3 - aria-query: 5.3.2 - chalk: 3.0.0 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - lodash: 4.17.21 - redent: 3.0.0 - '@testing-library/jest-dom@6.6.3': dependencies: '@adobe/css-tools': 4.4.3 @@ -9275,10 +9148,6 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) - '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': - dependencies: - '@testing-library/dom': 10.4.0 - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 @@ -9536,8 +9405,6 @@ snapshots: '@types/pluralize@0.0.33': {} - '@types/prismjs@1.26.5': {} - '@types/prop-types@15.7.15': {} '@types/react-dom@18.3.7(@types/react@18.3.23)': @@ -9698,15 +9565,15 @@ 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.43.1)(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.8.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.43.1)(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.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0))': + '@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))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) @@ -9714,11 +9581,11 @@ snapshots: '@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.43.1)(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) 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.43.1)(tsx@4.20.3)(yaml@2.8.0))': + '@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))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) @@ -9726,20 +9593,20 @@ snapshots: '@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.43.1)(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.8.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.43.1)(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.8.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.43.1)(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.8.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.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) ws: 8.18.3 optionalDependencies: playwright: 1.54.1 @@ -9749,16 +9616,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.43.1)(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.8.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.43.1)(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.8.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.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) ws: 8.18.3 optionalDependencies: playwright: 1.54.1 @@ -9768,16 +9635,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.43.1)(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.8.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.43.1)(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.8.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.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) ws: 8.18.3 optionalDependencies: playwright: 1.54.1 @@ -9803,19 +9670,12 @@ snapshots: 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.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.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.43.1)(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.8.0))(vitest@3.2.4) transitivePeerDependencies: - supports-color - '@vitest/expect@2.0.5': - dependencies: - '@vitest/spy': 2.0.5 - '@vitest/utils': 2.0.5 - chai: 5.2.1 - tinyrainbow: 1.2.0 - '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -9824,37 +9684,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.43.1)(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.8.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.43.1)(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.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.43.1)(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.8.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.43.1)(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) - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.8.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.43.1)(tsx@4.20.3)(yaml@2.8.0) - - '@vitest/pretty-format@2.0.5': - dependencies: - tinyrainbow: 1.2.0 - - '@vitest/pretty-format@2.1.9': - dependencies: - tinyrainbow: 1.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) '@vitest/pretty-format@3.2.3': dependencies: @@ -9876,10 +9728,6 @@ snapshots: magic-string: 0.30.17 pathe: 2.0.3 - '@vitest/spy@2.0.5': - dependencies: - tinyspy: 3.0.2 - '@vitest/spy@3.2.3': dependencies: tinyspy: 4.0.3 @@ -9897,20 +9745,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.43.1)(tsx@4.20.3)(yaml@2.8.0) - - '@vitest/utils@2.0.5': - dependencies: - '@vitest/pretty-format': 2.0.5 - estree-walker: 3.0.3 - loupe: 3.1.4 - tinyrainbow: 1.2.0 - - '@vitest/utils@2.1.9': - dependencies: - '@vitest/pretty-format': 2.1.9 - loupe: 3.1.4 - tinyrainbow: 1.2.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/utils@3.2.3': dependencies: @@ -10416,12 +10251,12 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) - browserslist@4.25.2: + browserslist@4.25.4: dependencies: - caniuse-lite: 1.0.30001735 - electron-to-chromium: 1.5.204 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.2) + caniuse-lite: 1.0.30001741 + electron-to-chromium: 1.5.215 + node-releases: 2.0.20 + update-browserslist-db: 1.1.3(browserslist@4.25.4) buffer-crc32@0.2.13: {} @@ -10482,7 +10317,7 @@ snapshots: caniuse-lite@1.0.30001727: {} - caniuse-lite@1.0.30001735: {} + caniuse-lite@1.0.30001741: {} ccount@2.0.1: {} @@ -10602,13 +10437,13 @@ snapshots: codemirror@6.0.1: dependencies: - '@codemirror/autocomplete': 6.18.6 + '@codemirror/autocomplete': 6.18.7 '@codemirror/commands': 6.8.1 '@codemirror/language': 6.11.3 '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.1 + '@codemirror/view': 6.38.2 color-convert@2.0.1: dependencies: @@ -10889,7 +10724,7 @@ snapshots: electron-to-chromium@1.5.190: {} - electron-to-chromium@1.5.204: {} + electron-to-chromium@1.5.215: {} elkjs@0.8.2: {} @@ -10917,7 +10752,7 @@ snapshots: enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.2 + tapable: 2.2.3 enquirer@2.4.1: dependencies: @@ -11073,11 +10908,11 @@ snapshots: dependencies: eslint: 9.31.0(jiti@2.4.2) - eslint-plugin-storybook@9.1.2(eslint@9.31.0(jiti@2.4.2))(storybook@9.1.2(@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.43.1)(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))(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): 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.2(@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.43.1)(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)) transitivePeerDependencies: - supports-color - typescript @@ -11314,6 +11149,8 @@ snapshots: functions-have-names@1.2.3: {} + fuse.js@7.1.0: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -12090,8 +11927,6 @@ snapshots: dependencies: semver: 7.7.2 - map-or-similar@1.5.0: {} - markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -12194,10 +12029,6 @@ snapshots: mdurl@2.0.0: {} - memoizerific@1.11.3: - dependencies: - map-or-similar: 1.5.0 - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -12475,6 +12306,8 @@ snapshots: node-releases@2.0.19: {} + node-releases@2.0.20: {} + node-sarif-builder@3.2.0: dependencies: '@types/sarif': 2.1.7 @@ -12854,12 +12687,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - prism-react-renderer@2.4.1(react@18.3.1): - dependencies: - '@types/prismjs': 1.26.5 - clsx: 2.1.1 - react: 18.3.1 - proc-log@5.0.0: {} process-nextick-args@2.0.1: {} @@ -13271,6 +13098,10 @@ snapshots: dependencies: seroval: 1.3.2 + seroval-plugins@1.3.3(seroval@1.3.2): + dependencies: + seroval: 1.3.2 + seroval@1.3.2: {} set-cookie-parser@2.7.1: {} @@ -13403,7 +13234,7 @@ snapshots: dependencies: csstype: 3.1.3 seroval: 1.3.2 - seroval-plugins: 1.3.2(seroval@1.3.2) + seroval-plugins: 1.3.3(seroval@1.3.2) source-map-js@1.2.1: {} @@ -13468,13 +13299,13 @@ snapshots: - supports-color - utf-8-validate - storybook@9.1.2(@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.43.1)(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)): 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.43.1)(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.8.0)) '@vitest/spy': 3.2.4 better-opn: 3.0.2 esbuild: 0.25.8 @@ -13678,12 +13509,9 @@ snapshots: tailwind-merge@3.3.1: {} - tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@3.4.17): + tailwind-scrollbar@3.1.0(tailwindcss@3.4.17): dependencies: - prism-react-renderer: 2.4.1(react@18.3.1) tailwindcss: 3.4.17 - transitivePeerDependencies: - - react tailwindcss@3.4.17: dependencies: @@ -13716,6 +13544,8 @@ snapshots: tapable@2.2.2: {} + tapable@2.2.3: {} + tar-fs@2.1.3: dependencies: chownr: 1.1.4 @@ -13753,12 +13583,12 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.43.1 + terser: 5.44.0 webpack: 5.99.8(esbuild@0.25.8) optionalDependencies: esbuild: 0.25.8 - terser@5.43.1: + terser@5.44.0: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 @@ -13814,12 +13644,8 @@ snapshots: tinypool@1.1.1: {} - tinyrainbow@1.2.0: {} - tinyrainbow@2.0.0: {} - tinyspy@3.0.2: {} - tinyspy@4.0.3: {} tldts-core@6.1.86: {} @@ -14051,9 +13877,9 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.1.3(browserslist@4.25.2): + update-browserslist-db@1.1.3(browserslist@4.25.4): dependencies: - browserslist: 4.25.2 + browserslist: 4.25.4 escalade: 3.2.0 picocolors: 1.1.1 @@ -14117,13 +13943,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.43.1)(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): dependencies: cac: 6.7.14 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.43.1)(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) transitivePeerDependencies: - '@types/node' - jiti @@ -14138,13 +13964,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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): dependencies: cac: 6.7.14 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@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -14159,11 +13985,11 @@ 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.43.1)(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.8.0)): dependencies: - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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.8.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.43.1)(tsx@4.20.3)(yaml@2.8.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)): dependencies: '@microsoft/api-extractor': 7.52.10(@types/node@20.11.25) '@rollup/pluginutils': 5.2.0(rollup@4.45.1) @@ -14176,22 +14002,22 @@ snapshots: 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.43.1)(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) 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.43.1)(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.8.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.43.1)(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) - vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(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): dependencies: esbuild: 0.25.8 fdir: 6.4.6(picomatch@4.0.3) @@ -14204,11 +14030,11 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.43.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.43.1)(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.8.0): dependencies: esbuild: 0.25.8 fdir: 6.4.6(picomatch@4.0.3) @@ -14221,15 +14047,15 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - terser: 5.43.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)(lightningcss@1.30.1)(terser@5.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.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.43.1)(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.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -14247,13 +14073,13 @@ 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.43.1)(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.43.1)(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) + 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) 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.43.1)(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.8.0))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: @@ -14270,11 +14096,11 @@ 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.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.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.43.1)(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.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -14292,13 +14118,13 @@ 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.43.1)(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.43.1)(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.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) 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.43.1)(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.8.0))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: @@ -14315,11 +14141,11 @@ 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.43.1)(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)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.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.43.1)(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.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -14337,13 +14163,13 @@ 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.43.1)(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.43.1)(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.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) 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.43.1)(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.8.0))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: @@ -14409,7 +14235,7 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 - browserslist: 4.25.2 + browserslist: 4.25.4 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 @@ -14422,7 +14248,7 @@ snapshots: mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 4.3.2 - tapable: 2.2.2 + tapable: 2.2.3 terser-webpack-plugin: 5.3.14(esbuild@0.25.8)(webpack@5.99.8(esbuild@0.25.8)) watchpack: 2.4.4 webpack-sources: 3.3.3 diff --git a/web/common/.storybook/main.ts b/web/common/.storybook/main.ts index 8fe508f79a..8994b8a737 100644 --- a/web/common/.storybook/main.ts +++ b/web/common/.storybook/main.ts @@ -2,11 +2,7 @@ import type { StorybookConfig } from '@storybook/react-vite' const config: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: [ - '@storybook/addon-essentials', - '@storybook/addon-docs', - '@storybook/addon-onboarding', - ], + addons: ['@storybook/addon-docs', '@storybook/addon-onboarding'], framework: { name: '@storybook/react-vite', options: {}, diff --git a/web/common/eslint.config.mjs b/web/common/eslint.config.mjs index 11555dcf40..a5eac98b5d 100644 --- a/web/common/eslint.config.mjs +++ b/web/common/eslint.config.mjs @@ -10,7 +10,6 @@ export default tseslint.config( tseslint.configs.recommended, { rules: { - '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-empty-object-type': 'off', '@typescript-eslint/no-unused-expressions': 'off', diff --git a/web/common/package-lock.json b/web/common/package-lock.json index 77ddb8d9d1..eaaaee941b 100644 --- a/web/common/package-lock.json +++ b/web/common/package-lock.json @@ -7,30 +7,46 @@ "": { "name": "@tobikodata/sqlmesh-common", "version": "0.0.1", + "license": "Apache-2.0", "devDependencies": { "@eslint/js": "^9.31.0", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-slot": "^1.2.3", - "@storybook/addon-docs": "^9.1.2", - "@storybook/addon-essentials": "^9.0.0-alpha.12", - "@storybook/addon-onboarding": "^9.1.2", - "@storybook/react-vite": "^9.1.2", + "@radix-ui/react-tooltip": "^1.2.8", + "@storybook/addon-docs": "^9.1.5", + "@storybook/addon-onboarding": "^9.1.5", + "@storybook/react-vite": "^9.1.5", "@tailwindcss/typography": "^0.5.16", + "@tanstack/react-virtual": "^3.13.12", "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", + "@types/node": "^20.11.25", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.7.0", + "@vitest/browser": "^3.2.4", + "@xyflow/react": "^12.8.4", "autoprefixer": "^10.4.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "eslint": "^9.31.0", "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-storybook": "^9.1.2", + "eslint-plugin-storybook": "^9.1.5", + "fuse.js": "^7.1.0", + "globals": "^16.3.0", + "lucide-react": "^0.542.0", + "playwright": "^1.54.1", "postcss": "^8.5.6", "react": "^18.3.1", "react-dom": "^18.3.1", - "storybook": "^9.1.2", + "storybook": "^9.1.5", + "syncpack": "^13.0.4", "tailwind-merge": "^3.3.1", + "tailwind-scrollbar": "^4.0.2", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", @@ -40,14 +56,23 @@ "vitest": "^3.2.4" }, "peerDependencies": { - "@radix-ui/react-slot": "^1.0.0", - "@tailwindcss/typography": "^0.5.0", - "class-variance-authority": "^0.7.0", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.8", + "@tailwindcss/typography": "^0.5.16", + "@tanstack/react-virtual": "^3.13.12", + "@xyflow/react": "^12.8.4", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "react": "^18.3.1 || ^19.0.0", - "react-dom": "^18.3.1 || ^19.0.0", + "cmdk": "^1.1.1", + "fuse.js": "^7.1.0", + "lucide-react": "^0.542.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "tailwind-merge": "^3.3.1", - "tailwindcss": "^3.4.0" + "tailwindcss": "^3.4.17" } }, "../../node_modules/.pnpm/@eslint+js@9.31.0/node_modules/@eslint/js": { @@ -1267,6 +1292,48 @@ "resolved": "../../node_modules/.pnpm/@eslint+js@9.31.0/node_modules/@eslint/js", "link": true }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1412,6 +1479,71 @@ "node": ">=14" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1428,15 +1560,12 @@ } } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -1447,221 +1576,751 @@ } } }, - "node_modules/@rollup/pluginutils": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", - "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { - "rollup": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { "optional": true } } }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@storybook/addon-backgrounds": { - "version": "9.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-9.0.0-alpha.12.tgz", - "integrity": "sha512-oiQL8GIs2jNhN1cfbWa6iJIdey/WC+TFlmIeoWzYsJ79EQCxpL5JgmzCMGIkZ+p7L4MUR/5S5b5fh6ApyWcUKw==", + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { - "storybook": "^9.0.0-alpha.12" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@storybook/addon-docs": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.2.tgz", - "integrity": "sha512-U3eHJ8lQFfEZ/OcgdKkUBbW2Y2tpAsHfy8lQOBgs5Pgj9biHEJcUmq+drOS/sJhle673eoBcUFmspXulI4KP1w==", + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", "dev": true, "license": "MIT", "dependencies": { - "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "9.1.2", - "@storybook/icons": "^1.4.0", - "@storybook/react-dom-shim": "9.1.2", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { - "storybook": "^9.1.2" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@storybook/addon-essentials": { - "version": "9.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-9.0.0-alpha.12.tgz", - "integrity": "sha512-wmUT9Q4rl6SvVgrIYDj97uHHkMSGba1A+/rMHypIw7OtrdUp+w1OKZRDNVrU0AfqfbaptT5dRrBsDr/eFZ9v8Q==", + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", "dev": true, "license": "MIT", - "dependencies": { - "@storybook/addon-backgrounds": "9.0.0-alpha.12", - "@storybook/addon-highlight": "9.0.0-alpha.12", - "@storybook/addon-measure": "9.0.0-alpha.12", - "@storybook/addon-outline": "9.0.0-alpha.12", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, "peerDependencies": { - "storybook": "^9.0.0-alpha.12" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@storybook/addon-highlight": { - "version": "9.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-9.0.0-alpha.12.tgz", - "integrity": "sha512-b8E1AjBaWFvBoWUfXXlAYfAIanuaHLZwJhmOcqJGtbx9RIC5uHfyGC8KHJgeyKMzvHhZD86vWBo5KUAFLFVUrg==", + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { - "storybook": "^9.0.0-alpha.12" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@storybook/addon-measure": { - "version": "9.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-9.0.0-alpha.12.tgz", - "integrity": "sha512-ZtAKi/mlvVYaBMlPokvrHF94YFsyYAlz3IpKu+uz5QymN3VweSIgGsDJmAqV49lVzyVk40KWCVypi4O3L7nvdQ==", + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/global": "^5.0.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { - "storybook": "^9.0.0-alpha.12" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@storybook/addon-onboarding": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-9.1.2.tgz", - "integrity": "sha512-WfYIBmRtwUF13Hcu6BdsqATsAuBK0dwsz7O4tL0FGrIwY/vdzZ5jNzYvzzgilzlu9QiPvzEIBvs6X4BVulN3LQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { - "storybook": "^9.1.2" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@storybook/addon-outline": { - "version": "9.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-9.0.0-alpha.12.tgz", - "integrity": "sha512-I7opVIK8bNUYSC+P+b8AwP6sE2pFyXH5F0gz8WA0pdkRcxerQmYnhlsXrI5T0QMu79tZnjVNrQTUrqpy/Z5oqQ==", + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/global": "^5.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { - "storybook": "^9.0.0-alpha.12" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@storybook/builder-vite": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.2.tgz", - "integrity": "sha512-5Y7e5wnSzFxCGP63UNRRZVoxHe1znU4dYXazJBobAlEcUPBk7A0sH2716tA6bS4oz92oG9tgvn1g996hRrw4ow==", + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "9.1.2", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { - "storybook": "^9.1.2", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@storybook/csf-plugin": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.2.tgz", - "integrity": "sha512-bfMh6r+RieBLPWtqqYN70le2uTE4JzOYPMYSCagHykUti3uM/1vRFaZNkZtUsRy5GwEzE5jLdDXioG1lOEeT2Q==", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "dev": true, "license": "MIT", "dependencies": { - "unplugin": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { - "storybook": "^9.1.2" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@storybook/global": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", - "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/addon-docs": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.5.tgz", + "integrity": "sha512-q1j5RRElxFSnHOh60eS3dS2TAyAHzcQeH/2B9UXo6MUHu7HmhNpw3qt2YibIw0zEogHCvZhLNx6TNzSy+7wRUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdx-js/react": "^3.0.0", + "@storybook/csf-plugin": "9.1.5", + "@storybook/icons": "^1.4.0", + "@storybook/react-dom-shim": "9.1.5", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.5" + } + }, + "node_modules/@storybook/addon-onboarding": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-9.1.5.tgz", + "integrity": "sha512-UJpkWLbugcSGzSUzivTTNdO0Y8gpAn//qJzn2TobwkPJgSwQEoHcjUfWjgZ3mSpQrSQO2e1O1yC3SJTBQt/fqQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.5" + } + }, + "node_modules/@storybook/builder-vite": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.5.tgz", + "integrity": "sha512-sgt/9+Yl/5O7Bj5hdbHfadN8e/e4CNiDZKDcbLOMpOjKKoqF8vm19I1QocWIAiKjTOhF+4E9v9LddjtAGnfqHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf-plugin": "9.1.5", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.5", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@storybook/csf-plugin": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.5.tgz", + "integrity": "sha512-PmHuF+j11Z7BxAI2/4wQYn0gH1d67gNvycyR+EWgp4P/AWam9wFbuI/T1R45CRQTV2/VrfGdts/tFrvo5kXWig==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.5" + } + }, + "node_modules/@storybook/global": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true, + "license": "MIT" }, "node_modules/@storybook/icons": { "version": "1.4.0", @@ -1678,14 +2337,14 @@ } }, "node_modules/@storybook/react": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.2.tgz", - "integrity": "sha512-VVXu1HrhDExj/yj+heFYc8cgIzBruXy1UYT3LW0WiJyadgzYz3J41l/Lf/j2FCppyxwlXb19Uv51plb1F1C77w==", + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.5.tgz", + "integrity": "sha512-fBVP7Go09gzpImtaMcZ2DipLEWdWeTmz7BrACr3Z8uCyKcoH8/d1Wv0JgIiBo1UKDh5ZgYx5pLafaPNqmVAepg==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "9.1.2" + "@storybook/react-dom-shim": "9.1.5" }, "engines": { "node": ">=20.0.0" @@ -1697,7 +2356,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.2", + "storybook": "^9.1.5", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -1707,9 +2366,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.2.tgz", - "integrity": "sha512-nw7BLAHCJswPZGsuL0Gs2AvFUWriusCTgPBmcHppSw/AqvT4XRFRDE+5q3j04/XKuZBrAA2sC4L+HuC0uzEChQ==", + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.5.tgz", + "integrity": "sha512-blSq9uzSYnfgEYPHYKgM5O14n8hbXNiXx2GiVJyDSg8QPNicbsBg+lCb1TC7/USfV26pNZr/lGNNKGkcCEN6Gw==", "dev": true, "license": "MIT", "funding": { @@ -1719,20 +2378,20 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.2" + "storybook": "^9.1.5" } }, "node_modules/@storybook/react-vite": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.2.tgz", - "integrity": "sha512-dv3CBjOzmMoSyIotMtdmsBRjB25i19OjFP0IZqauLeUoVm6QddILW7JRcZVLrzhATyBEn+sEAdWQ4j79Z11HAg==", + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.5.tgz", + "integrity": "sha512-OYbkHHNCrn8MNPd+4KxMjcSR4M/YHa84h8sWDUHhKRTRtZFmj8i/QDW3E8tGx2BRLxXw3dTYe9J5UYBhJDDxFA==", "dev": true, "license": "MIT", "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "9.1.2", - "@storybook/react": "9.1.2", + "@storybook/builder-vite": "9.1.5", + "@storybook/react": "9.1.5", "find-up": "^7.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", @@ -1749,7 +2408,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.2", + "storybook": "^9.1.5", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, @@ -1769,6 +2428,35 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -1920,6 +2608,61 @@ "@types/deep-eql": "*" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -1948,6 +2691,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "20.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz", + "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -2142,6 +2902,42 @@ "resolved": "../../node_modules/.pnpm/@vitejs+plugin-react@4.7.0_vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terse_p5zuafkpgv2vlm3nhxz3zj4hsu/node_modules/@vitejs/plugin-react", "link": true }, + "node_modules/@vitest/browser": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.2.4.tgz", + "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/user-event": "^14.6.1", + "@vitest/mocker": "3.2.4", + "@vitest/utils": "3.2.4", + "magic-string": "^0.30.17", + "sirv": "^3.0.1", + "tinyrainbow": "^2.0.0", + "ws": "^8.18.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "3.2.4", + "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -2257,6 +3053,40 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@xyflow/react": { + "version": "12.8.4", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.4.tgz", + "integrity": "sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.68", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.68", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.68.tgz", + "integrity": "sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2267,7 +3097,17 @@ "acorn": "bin/acorn" }, "engines": { - "node": ">=0.4.0" + "node": ">=0.4.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/ansi-regex": { @@ -2321,6 +3161,26 @@ "dev": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -2491,6 +3351,16 @@ "node": ">=8" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -2539,6 +3409,35 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-1.1.2.tgz", + "integrity": "sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -2600,6 +3499,42 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2610,6 +3545,23 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2647,6 +3599,33 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2689,6 +3668,120 @@ "dev": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -2737,6 +3830,13 @@ "node": ">=6" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "dev": true, + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2778,6 +3878,17 @@ "dev": true, "license": "MIT" }, + "node_modules/effect": { + "version": "3.17.13", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.17.13.tgz", + "integrity": "sha512-JMz5oBxs/6mu4FP9Csjub4jYMUwMLrp+IzUmSDVIzn2NoeoyOXMl7x1lghfr3dLKWffWrdnv/d8nFFdgrHXPqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.201", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.201.tgz", @@ -2792,6 +3903,53 @@ "dev": true, "license": "MIT" }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2882,9 +4040,9 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.2.tgz", - "integrity": "sha512-EQa/kChrYrekxv36q3pvW57anqxMlAP4EdPXEDyA/EDrCQJaaTbWEdsMnVZtD744RjPP0M5wzaUjHbMhNooAwQ==", + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.5.tgz", + "integrity": "sha512-vCfaZ2Wk1N1vvK4vmNZoA6y2CYxJwbgIs6BE8/toPf4Z6hCAipoobP6a/30Rs0g/B2TSxTSj41TfrJKJrowpjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2895,7 +4053,7 @@ }, "peerDependencies": { "eslint": ">=8", - "storybook": "^9.1.2" + "storybook": "^9.1.5" } }, "node_modules/eslint-visitor-keys": { @@ -2955,6 +4113,29 @@ "node": ">=12.0.0" } }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -3097,6 +4278,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3107,6 +4298,29 @@ "node": ">=6.9.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", + "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3125,20 +4339,67 @@ "glob": "dist/esm/bin.mjs" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=10.13.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/graceful-fs": { @@ -3161,6 +4422,46 @@ "node": ">= 0.4" } }, + "node_modules/hosted-git-info": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -3171,6 +4472,13 @@ "node": ">=8" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3249,6 +4557,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3259,6 +4580,19 @@ "node": ">=0.12.0" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -3312,6 +4646,19 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3325,6 +4672,13 @@ "node": ">=6" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3338,6 +4692,13 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -3351,6 +4712,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -3408,6 +4779,36 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3435,6 +4836,16 @@ "dev": true, "license": "ISC" }, + "node_modules/lucide-react": { + "version": "0.542.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.542.0.tgz", + "integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -3455,23 +4866,6 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/map-or-similar": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", - "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", - "dev": true, - "license": "MIT" - }, - "node_modules/memoizerific": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", - "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", - "dev": true, - "license": "MIT", - "dependencies": { - "map-or-similar": "^1.5.0" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3496,6 +4890,19 @@ "node": ">=8.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -3542,6 +4949,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3607,6 +5024,22 @@ "node": ">=0.10.0" } }, + "node_modules/npm-package-arg": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", + "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3627,6 +5060,22 @@ "node": ">= 6" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", @@ -3645,6 +5094,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -3697,6 +5195,38 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", @@ -3741,6 +5271,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3795,7 +5338,54 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/postcss": { @@ -3977,6 +5567,61 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4080,6 +5725,78 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4090,6 +5807,30 @@ "pify": "^2.3.0" } }, + "node_modules/read-yaml-file": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-2.1.0.tgz", + "integrity": "sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-yaml": "^4.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=10.13" + } + }, + "node_modules/read-yaml-file/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4155,6 +5896,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -4256,6 +6024,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4290,10 +6093,23 @@ "dev": true, "license": "MIT" }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/storybook": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.2.tgz", - "integrity": "sha512-TYcq7WmgfVCAQge/KueGkVlM/+g33sQcmbATlC3X6y/g2FEeSSLGrb6E6d3iemht8oio+aY6ld3YOdAnMwx45Q==", + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.5.tgz", + "integrity": "sha512-cGwJ2AE6nxlwqQlOiI+HKX5qa7+FOV7Ha7Qa+GoASBIQSSnLfbY6UldgAxHCJGJOFtgW/wuqfDtNvni6sj1/OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4502,6 +6318,57 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/syncpack": { + "version": "13.0.4", + "resolved": "https://registry.npmjs.org/syncpack/-/syncpack-13.0.4.tgz", + "integrity": "sha512-kJ9VlRxNCsBD5pJAE29oXeBYbPLhEySQmK4HdpsLv81I6fcDDW17xeJqMwiU3H7/woAVsbgq25DJNS8BeiN5+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "chalk-template": "^1.1.0", + "commander": "^13.1.0", + "cosmiconfig": "^9.0.0", + "effect": "^3.13.7", + "enquirer": "^2.4.1", + "fast-check": "^3.23.2", + "globby": "^14.1.0", + "jsonc-parser": "^3.3.1", + "minimatch": "9.0.5", + "npm-package-arg": "^12.0.2", + "ora": "^8.2.0", + "prompts": "^2.4.2", + "read-yaml-file": "^2.1.0", + "semver": "^7.7.1", + "tightrope": "0.2.0", + "ts-toolbelt": "^9.6.0" + }, + "bin": { + "syncpack": "dist/bin.js", + "syncpack-fix-mismatches": "dist/bin-fix-mismatches/index.js", + "syncpack-format": "dist/bin-format/index.js", + "syncpack-lint": "dist/bin-lint/index.js", + "syncpack-lint-semver-ranges": "dist/bin-lint-semver-ranges/index.js", + "syncpack-list": "dist/bin-list/index.js", + "syncpack-list-mismatches": "dist/bin-list-mismatches/index.js", + "syncpack-prompt": "dist/bin-prompt/index.js", + "syncpack-set-semver-ranges": "dist/bin-set-semver-ranges/index.js", + "syncpack-update": "dist/bin-update/index.js" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/syncpack/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -4513,6 +6380,22 @@ "url": "https://github.com/sponsors/dcastil" } }, + "node_modules/tailwind-scrollbar": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-4.0.2.tgz", + "integrity": "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prism-react-renderer": "^2.4.1" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "tailwindcss": "4.x" + } + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", @@ -4588,6 +6471,16 @@ "node": ">=0.8" } }, + "node_modules/tightrope": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/tightrope/-/tightrope-0.2.0.tgz", + "integrity": "sha512-Kw36UHxJEELq2VUqdaSGR2/8cAsPgMtvX8uGVU6Jk26O66PhXec0A5ZnRYs47btbtwPDpXXF66+Fo3vimCM9aQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -4700,6 +6593,16 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -4730,6 +6633,13 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -4760,6 +6670,13 @@ "resolved": "../../node_modules/.pnpm/typescript-eslint@8.38.0_eslint@9.31.0_jiti@2.4.2__typescript@5.8.3/node_modules/typescript-eslint", "link": true }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unicorn-magic": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", @@ -4828,6 +6745,61 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4835,6 +6807,16 @@ "dev": true, "license": "MIT" }, + "node_modules/validate-npm-package-name": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", + "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/vite": { "resolved": "../../node_modules/.pnpm/vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terser@5.43.1_tsx@4.20.3_yaml@2.8.0/node_modules/vite", "link": true @@ -5167,6 +7149,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/web/common/package.json b/web/common/package.json index 2501b9f81a..f576696b61 100644 --- a/web/common/package.json +++ b/web/common/package.json @@ -5,12 +5,10 @@ "@eslint/js": "^9.31.0", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", - "@storybook/addon-docs": "^9.1.2", - "@storybook/addon-essentials": "^9.0.0-alpha.12", - "@storybook/addon-onboarding": "^9.1.2", - "@storybook/react-vite": "^9.1.2", - "@storybook/test": "^8.6.14", + "@storybook/addon-docs": "^9.1.5", + "@storybook/react-vite": "^9.1.5", "@tailwindcss/typography": "^0.5.16", + "@tanstack/react-virtual": "^3.13.12", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", @@ -25,17 +23,18 @@ "clsx": "^2.1.1", "eslint": "^9.31.0", "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-storybook": "^9.1.2", + "eslint-plugin-storybook": "^9.1.5", + "fuse.js": "^7.1.0", "globals": "^16.3.0", "lucide-react": "^0.542.0", "playwright": "^1.54.1", "postcss": "^8.5.6", "react": "^18.3.1", "react-dom": "^18.3.1", - "storybook": "^9.1.2", + "storybook": "^9.1.5", "syncpack": "^13.0.4", "tailwind-merge": "^3.3.1", - "tailwind-scrollbar": "^4.0.2", + "tailwind-scrollbar": "^3.1.0", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", @@ -69,9 +68,11 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/typography": "^0.5.16", + "@tanstack/react-virtual": "^3.13.12", "@xyflow/react": "^12.8.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "fuse.js": "^7.1.0", "lucide-react": "^0.542.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/web/common/src/components/Badge/Badge.tsx b/web/common/src/components/Badge/Badge.tsx index 2cf561ebc1..cd6df21c26 100644 --- a/web/common/src/components/Badge/Badge.tsx +++ b/web/common/src/components/Badge/Badge.tsx @@ -18,9 +18,9 @@ export const Badge = React.forwardRef( const Comp = asChild ? Slot : 'span' return ( ) diff --git a/web/common/src/components/Button/Button.tsx b/web/common/src/components/Button/Button.tsx index e4bebd0798..cc34ce192a 100644 --- a/web/common/src/components/Button/Button.tsx +++ b/web/common/src/components/Button/Button.tsx @@ -28,6 +28,7 @@ export const Button = React.forwardRef( const Comp = asChild ? Slot : 'button' return ( ( return (
      )) -export const Default = (args: any) => ( +export const Default = (args: HorizontalContainerProps) => (
      ( ) Default.storyName = 'Default (No Scroll)' -export const WithScroll = (args: any) => ( +export const WithScroll = (args: HorizontalContainerProps) => (
      (
      ) -export const CustomClassName = (args: any) => ( +export const CustomClassName = (args: HorizontalContainerProps) => (
      ( ) CustomClassName.storyName = 'With Custom ClassName' -export const NestedHorizontalContainer = (args: any) => ( +export const NestedHorizontalContainer = (args: HorizontalContainerProps) => (
      + +export const Default: Story = { + args: { + placeholder: 'Enter text...', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const input = canvas.getByPlaceholderText('Enter text...') + await expect(input).toBeInTheDocument() + await expect(input).toHaveAttribute('type', 'text') + }, +} + +export const InputTypes: Story = { + render: () => ( +
      + + + + + + + + + + +
      + ), +} + +export const States: Story = { + render: () => ( +
      + + + + +
      + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const disabledInput = canvas.getByPlaceholderText('Disabled input') + const readOnlyInput = canvas.getByPlaceholderText('Read-only input') + + await expect(disabledInput).toBeDisabled() + await expect(readOnlyInput).toHaveAttribute('readonly') + await expect(readOnlyInput).toHaveValue('Read-only text') + }, +} + +export const InteractiveTyping: Story = { + args: { + placeholder: 'Type something...', + onChange: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement) + const user = userEvent.setup() + const input = canvas.getByPlaceholderText('Type something...') + + await expect(input).toBeInTheDocument() + await user.type(input, 'Hello World') + await expect(input).toHaveValue('Hello World') + await expect(args.onChange).toHaveBeenCalled() + }, +} diff --git a/web/common/src/components/Input/Input.tsx b/web/common/src/components/Input/Input.tsx new file mode 100644 index 0000000000..5c25b0a698 --- /dev/null +++ b/web/common/src/components/Input/Input.tsx @@ -0,0 +1,51 @@ +import * as React from 'react' +import { cn } from '@/utils' +import type { Size } from '@/types' +import { cva } from 'class-variance-authority' + +export interface InputProps extends React.ComponentProps<'input'> { + inputSize?: Size +} + +export const Input = React.forwardRef( + ({ className, type = 'text', readOnly, inputSize = 's', ...props }, ref) => { + return ( + + ) + }, +) + +const size: Record = { + '2xs': 'h-5 px-2 text-2xs rounded-2xs', + xs: 'h-6 px-2 text-2xs rounded-xs', + s: 'h-7 px-3 text-xs rounded-sm', + m: 'h-8 px-4 rounded-md', + l: 'h-9 px-4 rounded-lg', + xl: 'h-10 px-4 rounded-xl', + '2xl': 'h-11 px-6 rounded-2xl', +} + +const inputVariants = cva( + 'inline-flex whitespace-nowrap leading-none ring-offset-light transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focused focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-60', + { + variants: { + size, + }, + defaultVariants: { + size: 's', + }, + }, +) diff --git a/web/common/src/components/LoadingContainer/LoadingContainer.stories.tsx b/web/common/src/components/LoadingContainer/LoadingContainer.stories.tsx new file mode 100644 index 0000000000..80c978a905 --- /dev/null +++ b/web/common/src/components/LoadingContainer/LoadingContainer.stories.tsx @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import type { Side } from '@/types' +import { LoadingContainer } from './LoadingContainer' +import { expect, within } from 'storybook/test' + +const meta: Meta = { + title: 'Components/Containers/LoadingContainer', + component: LoadingContainer, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + isLoading: true, + }, +} + +export const WithMessage: Story = { + args: { + isLoading: true, + message: 'Loading data...', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + await expect(canvas.getByText('Loading data...')).toBeInTheDocument() + }, +} + +export const WithContent: Story = { + args: { + isLoading: true, + message: 'Processing', + children: ( +
      + Main Content +
      + ), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + await expect(canvas.getByText('Main Content')).toBeInTheDocument() + await expect(canvas.getByText('Processing')).toBeInTheDocument() + }, +} + +const sides: Side[] = ['left', 'right', 'both'] + +export const LoadingSides: Story = { + render: () => ( +
      + {sides.map(side => ( + +
      + Content +
      +
      + ))} +
      + ), +} diff --git a/web/common/src/components/LoadingContainer/LoadingContainer.tsx b/web/common/src/components/LoadingContainer/LoadingContainer.tsx new file mode 100644 index 0000000000..af9250b729 --- /dev/null +++ b/web/common/src/components/LoadingContainer/LoadingContainer.tsx @@ -0,0 +1,47 @@ +import type { Side } from '@/types' +import { cn } from '@/utils' +import React from 'react' +import { LoadingIcon } from './LoadingIcon' + +export interface LoadingContainerProps + extends React.HTMLAttributes { + isLoading?: boolean + message?: string + side?: Side + className?: string +} + +export const LoadingContainer = React.forwardRef< + HTMLDivElement, + LoadingContainerProps +>( + ({ + isLoading = true, + side = 'left', + message, + children, + className, + }: LoadingContainerProps) => { + function renderLoading() { + return ( + <> + + {message && {message}} + + ) + } + + return isLoading ? ( +
      + {(side === 'left' || side === 'both') && renderLoading()} + {children} + {(side === 'right' || side === 'both') && renderLoading()} +
      + ) : ( + children + ) + }, +) diff --git a/web/common/src/components/LoadingContainer/LoadingIcon.tsx b/web/common/src/components/LoadingContainer/LoadingIcon.tsx new file mode 100644 index 0000000000..b683929f8f --- /dev/null +++ b/web/common/src/components/LoadingContainer/LoadingIcon.tsx @@ -0,0 +1,247 @@ +import { cn } from '@/utils' +import React from 'react' + +export const LoadingIcon = React.forwardRef< + SVGSVGElement, + React.SVGProps +>( + ( + { + className, + ...props + }: { + className?: string + }, + ref, + ) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + }, +) diff --git a/web/common/src/components/MessageContainer/MessageContainer.stories.tsx b/web/common/src/components/MessageContainer/MessageContainer.stories.tsx new file mode 100644 index 0000000000..554fe4b0cd --- /dev/null +++ b/web/common/src/components/MessageContainer/MessageContainer.stories.tsx @@ -0,0 +1,63 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import MessageContainer from './MessageContainer' + +const meta = { + title: 'Components/Containers/MessageContainer', + component: MessageContainer, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + children: 'This is a default message container with some content', + isLoading: false, + wrap: false, + }, +} + +export const WithLongText: Story = { + args: { + children: + 'This is a very long message that demonstrates how the MessageContainer handles overflow text when the content exceeds the available width. By default, it will truncate with ellipsis.', + isLoading: false, + wrap: false, + }, +} + +export const WithWrapping: Story = { + args: { + children: + 'This is a very long message that demonstrates how the MessageContainer handles overflow text when wrapping is enabled. With wrap set to true, the text will wrap to multiple lines instead of being truncated.', + isLoading: false, + wrap: true, + }, +} + +export const Loading: Story = { + args: { + children: 'This content is loading...', + isLoading: true, + wrap: false, + }, +} + +export const LoadingWithWrap: Story = { + args: { + children: + 'This is a longer message that is currently loading. When both loading and wrap are enabled, the skeleton animation will respect the wrapping behavior.', + isLoading: true, + wrap: true, + }, +} + +export const WithCustomStyling: Story = { + args: { + children: 'Custom styled message container', + className: + 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 font-bold', + isLoading: false, + wrap: false, + }, +} diff --git a/web/common/src/components/MessageContainer/MessageContainer.tsx b/web/common/src/components/MessageContainer/MessageContainer.tsx new file mode 100644 index 0000000000..2a1b4783a3 --- /dev/null +++ b/web/common/src/components/MessageContainer/MessageContainer.tsx @@ -0,0 +1,40 @@ +import { cn } from '@/utils' +import { LoadingContainer } from '../LoadingContainer/LoadingContainer' +import { HorizontalContainer } from '../HorizontalContainer/HorizontalContainer' + +export default function MessageContainer({ + children, + className, + wrap = false, + isLoading = false, +}: { + children: React.ReactNode + className?: string + wrap?: boolean + isLoading?: boolean +}) { + return ( + + {isLoading ? ( + + {children} + + ) : ( + children + )} + + ) +} diff --git a/web/common/src/components/Metadata/Metadata.stories.tsx b/web/common/src/components/Metadata/Metadata.stories.tsx new file mode 100644 index 0000000000..ba1f949138 --- /dev/null +++ b/web/common/src/components/Metadata/Metadata.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { Metadata } from './Metadata' + +const meta: Meta = { + title: 'Components/Metadata', + component: Metadata, + args: { + label: 'Label', + value: 'Value', + }, + argTypes: { + label: { control: 'text' }, + value: { control: 'text' }, + className: { control: 'text' }, + }, +} +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const CustomClassName: Story = { + args: { + label: 'Custom Label', + value: 'Custom Value', + className: 'bg-neutral-100 p-2 rounded border border-neutral-200', + }, +} + +export const WithReactNode: Story = { + args: { + label: ( + + ReactNode Label + + ), + value: ReactNode Value, + }, +} diff --git a/web/common/src/components/Metadata/Metadata.tsx b/web/common/src/components/Metadata/Metadata.tsx new file mode 100644 index 0000000000..9227844fd3 --- /dev/null +++ b/web/common/src/components/Metadata/Metadata.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { HorizontalContainer } from '../HorizontalContainer/HorizontalContainer' +import { cn } from '@/utils' + +export interface MetadataProps extends React.HTMLAttributes { + label: React.ReactNode + value: React.ReactNode +} + +export const Metadata = React.forwardRef( + ({ label, value, className, ...props }, ref) => { + return ( + + {typeof label === 'string' ? ( +
      {label}
      + ) : ( + label + )} + {typeof value === 'string' ? ( +
      {value}
      + ) : ( + value + )} +
      + ) + }, +) + +Metadata.displayName = 'Metadata' diff --git a/web/common/src/components/ModelName/ModelName.stories.tsx b/web/common/src/components/ModelName/ModelName.stories.tsx index fe54681f10..db2631dc6b 100644 --- a/web/common/src/components/ModelName/ModelName.stories.tsx +++ b/web/common/src/components/ModelName/ModelName.stories.tsx @@ -76,7 +76,7 @@ export const Grayscale: Story = { export const Link: Story = { args: { name: 'catalog.schema.model', - link: 'https://www.google.com', + renderLink: modelName => {modelName}, grayscale: false, showCopy: true, }, @@ -85,7 +85,7 @@ export const Link: Story = { export const LinkGrayscale: Story = { args: { name: 'catalog.schema.model', - link: 'https://www.google.com', + renderLink: modelName => {modelName}, grayscale: true, showCopy: true, }, diff --git a/web/common/src/components/ModelName/ModelName.tsx b/web/common/src/components/ModelName/ModelName.tsx index 58ab33e1cb..7777dee5ce 100644 --- a/web/common/src/components/ModelName/ModelName.tsx +++ b/web/common/src/components/ModelName/ModelName.tsx @@ -19,7 +19,7 @@ export interface ModelNameProps extends React.HTMLAttributes { truncateLimitBefore?: number truncateLimitAfter?: number grayscale?: boolean - link?: string + renderLink?: (modelName: React.ReactNode) => React.ReactNode className?: string } @@ -39,7 +39,7 @@ export const ModelName = React.forwardRef( truncateLimitBefore = 5, truncateLimitAfter = 7, grayscale = false, - link, + renderLink, className, ...props }, @@ -94,7 +94,7 @@ export const ModelName = React.forwardRef( grayscale ? 'text-model-name-grayscale-model' : 'text-model-name-model', - link && '-mt-[4px]', + renderLink && '-mt-[4px]', )} /> ) @@ -174,9 +174,8 @@ export const ModelName = React.forwardRef( {...props} > {!hideIcon && renderIcon()} - {link ? ( - ( : 'border-model-name-link-underline hover:border-model-name-link-underline-hover', )} > - {renderNameWithTooltip()} - + {renderLink(renderNameWithTooltip())} + ) : ( renderNameWithTooltip() )} diff --git a/web/common/src/components/ScrollContainer/ScrollContainer.css b/web/common/src/components/ScrollContainer/ScrollContainer.css index 98ba5055a2..0ab7dd033f 100644 --- a/web/common/src/components/ScrollContainer/ScrollContainer.css +++ b/web/common/src/components/ScrollContainer/ScrollContainer.css @@ -1,4 +1,4 @@ :root { --scrollbar-thumb: var(--color-neutral-300); - --scrollbar-track: var(--color-neutral-100); + --scrollbar-track: var(--color-transparent); } diff --git a/web/common/src/components/ScrollContainer/ScrollContainer.stories.tsx b/web/common/src/components/ScrollContainer/ScrollContainer.stories.tsx index 46b972d8f8..065a9a5177 100644 --- a/web/common/src/components/ScrollContainer/ScrollContainer.stories.tsx +++ b/web/common/src/components/ScrollContainer/ScrollContainer.stories.tsx @@ -1,4 +1,4 @@ -import { ScrollContainer } from './ScrollContainer' +import { ScrollContainer, type ScrollContainerProps } from './ScrollContainer' export default { title: 'Components/Containers/ScrollContainer', @@ -14,7 +14,7 @@ const content = Array.from({ length: 30 }, (_, i) => (
      )) -export const VerticalScroll = (args: any) => ( +export const VerticalScroll = (args: ScrollContainerProps) => (
      (
      ) -export const HorizontalScroll = (args: any) => ( +export const HorizontalScroll = (args: ScrollContainerProps) => (
      (
      ) -export const BothDirectionsScroll = (args: any) => ( +export const BothDirectionsScroll = (args: ScrollContainerProps) => (
      (
      ) -export const CustomClassName = (args: any) => ( +export const CustomClassName = (args: ScrollContainerProps) => (
      ( ) CustomClassName.storyName = 'With Custom ClassName' -export const PageContentLayout = (args: any) => ( +export const PageContentLayout = (args: ScrollContainerProps) => (
      diff --git a/web/common/src/components/Tooltip/Tooltip.tsx b/web/common/src/components/Tooltip/Tooltip.tsx index 8096e8742e..426b97aaa2 100644 --- a/web/common/src/components/Tooltip/Tooltip.tsx +++ b/web/common/src/components/Tooltip/Tooltip.tsx @@ -26,20 +26,26 @@ export function Tooltip({ className, }: { trigger: React.ReactNode + children: React.ReactNode side?: TooltipSide align?: TooltipAlign delayDuration?: number sideOffset?: number alignOffset?: number - children: React.ReactNode className?: string }) { return ( - {trigger} + + {trigger} + + {children} +
      + ) +} diff --git a/web/common/src/components/Typography/Headline.tsx b/web/common/src/components/Typography/Headline.tsx new file mode 100644 index 0000000000..77eb809d94 --- /dev/null +++ b/web/common/src/components/Typography/Headline.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { getHeadlineTextSize } from './help' +import type { HeadlineLevel } from '@/types' +import { cn } from '@/utils' + +export function Headline({ + level = 1, + children, + className, + ...props +}: { + level: HeadlineLevel + children: React.ReactNode + className?: string +}) { + const Tag = `h${level}` as keyof JSX.IntrinsicElements + + return ( + + {children} + + ) +} diff --git a/web/common/src/components/Typography/Information.tsx b/web/common/src/components/Typography/Information.tsx new file mode 100644 index 0000000000..ae2d8ebfd0 --- /dev/null +++ b/web/common/src/components/Typography/Information.tsx @@ -0,0 +1,58 @@ +import { Info } from 'lucide-react' +import React from 'react' + +import { cn } from '@/utils' +import { getTextSize } from './help' +import type { Size } from '@/types' +import { Tooltip } from '../Tooltip/Tooltip' + +export function Information({ + children, + className, + classNameTooltip, + side = 'right', + size = 's', + sideOffset = 4, + delayDuration = 200, + info, + infoIcon = ( + + ), + ...props +}: { + children?: React.ReactNode + className?: string + classNameTooltip?: string + side?: 'right' | 'left' + size?: Size + sideOffset?: number + delayDuration?: number + info?: React.ReactNode + infoIcon?: React.ReactNode +}) { + return ( +
      + {children} + + {info} + +
      + ) +} diff --git a/web/common/src/components/Typography/Tagline.tsx b/web/common/src/components/Typography/Tagline.tsx new file mode 100644 index 0000000000..85a0309e36 --- /dev/null +++ b/web/common/src/components/Typography/Tagline.tsx @@ -0,0 +1,20 @@ +import { cn } from '@/utils' + +export function Tagline({ + className, + children, + ...props +}: { + className?: string + children?: React.ReactNode +}) { + return ( +
      + {children} +
      + ) +} diff --git a/web/common/src/components/Typography/Text.tsx b/web/common/src/components/Typography/Text.tsx new file mode 100644 index 0000000000..8fdd4c0119 --- /dev/null +++ b/web/common/src/components/Typography/Text.tsx @@ -0,0 +1,20 @@ +import { cn } from '@/utils' + +export function Text({ + className, + children, + ...props +}: { + className?: string + children?: React.ReactNode +}) { + return ( +
      + {children} +
      + ) +} diff --git a/web/common/src/components/Typography/Typography.stories.tsx b/web/common/src/components/Typography/Typography.stories.tsx new file mode 100644 index 0000000000..1aaf06b0db --- /dev/null +++ b/web/common/src/components/Typography/Typography.stories.tsx @@ -0,0 +1,87 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { Description as DescriptionComponent } from './Description' +import { Headline } from './Headline' +import { Information as InformationComponent } from './Information' +import { Tagline as TaglineComponent } from './Tagline' +import { Text as TextComponent } from './Text' + +export default { + title: 'Components/Typography', + decorators: [ + Story => ( +
      + +
      + ), + ], +} satisfies Meta + +export const Headlines: StoryObj = { + render: () => ( +
      + Headline Level 1 - Bold and Large + + Headline Level 2 - Semibold and Slightly Smaller + + Headline Level 3 - Medium Weight + Headline Level 4 - Smaller Size + Headline Level 5 - Compact + Headline Level 6 - Smallest +
      + ), +} + +export const Text: StoryObj = { + render: () => ( + + Regular paragraph text with prose styling for readability. + + ), +} + +export const Tagline: StoryObj = { + render: () => ( + + Tagline text with prose styling for readability. + + ), +} + +export const Description: StoryObj = { + render: () => ( + + Description text with prose styling for readability. + + ), +} + +export const Information: StoryObj = { + render: () => ( +
      + + Hover for XS tooltip + + + Hover for Small tooltip + + + Hover for Medium tooltip + + + Hover for Large tooltip + +
      + ), +} diff --git a/web/common/src/components/Typography/help.spec.ts b/web/common/src/components/Typography/help.spec.ts new file mode 100644 index 0000000000..1db09fec1f --- /dev/null +++ b/web/common/src/components/Typography/help.spec.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from 'vitest' + +import { getHeadlineTextSize, getTextSize } from './help' + +describe('Typography Utils', () => { + test('getHeadlineTextSize', () => { + expect(getHeadlineTextSize(1)).toBe('text-4xl font-bold') + expect(getHeadlineTextSize(2)).toBe('text-3xl font-bold') + expect(getHeadlineTextSize(3)).toBe('text-2xl font-bold') + expect(getHeadlineTextSize(4)).toBe('text-l font-semibold') + expect(getHeadlineTextSize(5)).toBe('text-s font-semibold') + expect(getHeadlineTextSize(6)).toBe('text-xs font-semibold') + }) + test('getTextSize', () => { + expect(getTextSize('s')).toBe('text-sm') + expect(getTextSize('m')).toBe('text-base') + expect(getTextSize('l')).toBe('text-lg') + expect(getTextSize('xl')).toBe('text-xl') + expect(getTextSize('2xl')).toBe('text-2xl') + expect(getTextSize('2xs')).toBe('text-xs') + expect(getTextSize('xs')).toBe('text-xs') + }) +}) diff --git a/web/common/src/components/Typography/help.ts b/web/common/src/components/Typography/help.ts new file mode 100644 index 0000000000..429c6f3852 --- /dev/null +++ b/web/common/src/components/Typography/help.ts @@ -0,0 +1,24 @@ +import type { HeadlineLevel, Size } from '@/types' + +export function getHeadlineTextSize(level: HeadlineLevel) { + return { + 1: 'text-4xl font-bold', + 2: 'text-3xl font-bold', + 3: 'text-2xl font-bold', + 4: 'text-l font-semibold', + 5: 'text-s font-semibold', + 6: 'text-xs font-semibold', + }[level] +} + +export function getTextSize(size: Size) { + return { + '2xs': 'text-xs', + xs: 'text-xs', + s: 'text-sm', + m: 'text-base', + l: 'text-lg', + xl: 'text-xl', + '2xl': 'text-2xl', + }[size] +} diff --git a/web/common/src/components/VerticalContainer/VerticalContainer.stories.tsx b/web/common/src/components/VerticalContainer/VerticalContainer.stories.tsx index 8283b50a8d..0142635f47 100644 --- a/web/common/src/components/VerticalContainer/VerticalContainer.stories.tsx +++ b/web/common/src/components/VerticalContainer/VerticalContainer.stories.tsx @@ -1,4 +1,7 @@ -import { VerticalContainer } from './VerticalContainer' +import { + VerticalContainer, + type VerticalContainerProps, +} from './VerticalContainer' export default { title: 'Components/Containers/VerticalContainer', @@ -14,7 +17,7 @@ const content = Array.from({ length: 20 }, (_, i) => (
      )) -export const Default = (args: any) => ( +export const Default = (args: VerticalContainerProps) => (
      ( ) Default.storyName = 'Default (No Scroll)' -export const WithScroll = (args: any) => ( +export const WithScroll = (args: VerticalContainerProps) => (
      (
      ) -export const CustomClassName = (args: any) => ( +export const CustomClassName = (args: VerticalContainerProps) => (
      ( ) CustomClassName.storyName = 'With Custom ClassName' -export const NestedVerticalContainer = (args: any) => ( +export const NestedVerticalContainer = (args: VerticalContainerProps) => (
      ({ + items, + disabled, + placeholder, + autoFocus, + filterOptions, + className, + children, +}: { + items: TItem[] + filterOptions?: IFuseOptions + disabled?: boolean + placeholder?: string + autoFocus?: boolean + className?: string + children: (options: TItem[], resetSearch: () => void) => React.ReactNode +}) { + const [search, setSearch] = React.useState('') + + const fuse = new Fuse(items, filterOptions) + + const filteredItems = search + ? fuse.search(search).map(result => result.item) + : items + + const resetSearch = React.useCallback(() => { + setSearch('') + }, []) + + return ( + + + ) => + setSearch(e.target.value) + } + inputSize="xs" + className="w-full" + /> + + + {filteredItems.length > 0 ? ( + children(filteredItems, resetSearch) + ) : ( + + )} + + ) +} + +function Counter({ + itemsLength, + filteredItemsLength, + className, +}: { + itemsLength: number + filteredItemsLength: number + className?: string +}) { + return ( + + {itemsLength !== filteredItemsLength && ( + <> + {filteredItemsLength} + / + + )} + {itemsLength} + + ) +} + +function EmptyMessage({ + message = 'No Results Found', +}: { + message?: React.ReactNode +}) { + return ( + + {message} + + ) +} diff --git a/web/common/src/components/VirtualList/VirtualList.stories.tsx b/web/common/src/components/VirtualList/VirtualList.stories.tsx new file mode 100644 index 0000000000..c61709183e --- /dev/null +++ b/web/common/src/components/VirtualList/VirtualList.stories.tsx @@ -0,0 +1,238 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { VerticalContainer } from '../VerticalContainer/VerticalContainer' +import { Description } from '../Typography/Description' +import { Metadata } from '../Metadata/Metadata' +import { FilterableList } from './FilterableList' +import { VirtualList } from './VirtualList' +import { Badge } from '../Badge/Badge' + +interface MockItem { + id: string + name: string + description: string + category?: string + isActive?: boolean +} + +const meta: Meta = { + title: 'Components/VirtualList', + component: VirtualList, + decorators: [ + Story => ( +
      + +
      + ), + ], + argTypes: { + estimatedListItemHeight: { + control: 'number', + description: 'Estimated height of each item in pixels', + }, + className: { + control: 'text', + description: 'Additional CSS classes', + }, + }, +} + +export default meta +type Story = StoryObj> + +const generateMockItems = (count: number): MockItem[] => { + const categories = ['Documents', 'Images', 'Videos', 'Audio', 'Other'] + return Array.from({ length: count }, (_, i) => ({ + id: `item-${i}`, + name: `Item ${i + 1}`, + description: `This is the description for item ${i + 1}`, + category: categories[i % categories.length], + isActive: i === 10, + })) +} + +function renderListItem(item: MockItem, height = 48) { + return ( + + {item.category}} + className="h-6" + /> + {item.description} + + ) +} + +export const Default: Story = { + args: { + items: generateMockItems(100), + estimatedListItemHeight: 48, + renderListItem, + }, +} + +export const WithSelection: Story = { + args: { + items: generateMockItems(100), + isSelected: (item: MockItem) => item.isActive === true, + estimatedListItemHeight: 48, + renderListItem, + }, +} + +export const LargeDataset: Story = { + args: { + items: generateMockItems(10000), + estimatedListItemHeight: 48, + renderListItem, + }, +} + +export const VariableHeightContent: Story = { + args: { + items: generateMockItems(50).map((item, i) => ({ + ...item, + description: + i % 3 === 0 + ? `This is a much longer description for ${item.name} that will wrap to multiple lines and demonstrate how the virtual list handles variable content heights.` + : item.description, + })), + estimatedListItemHeight: 80, + renderListItem: item => renderListItem(item, 80), + }, +} + +export const EmptyState: Story = { + args: { + items: [], + estimatedListItemHeight: 60, + renderListItem: () => null, + }, +} + +export const SingleItem: Story = { + args: { + items: generateMockItems(1), + estimatedListItemHeight: 48, + renderListItem, + }, +} + +export const WithScrollToSelected: Story = { + args: { + items: generateMockItems(200).map((item, i) => ({ + ...item, + isActive: i === 150, + })), + estimatedListItemHeight: 48, + isSelected: (item: MockItem) => item.isActive === true, + renderListItem, + }, +} + +// FilterableList Stories +export const FilterableListDefault: Story = { + render: () => { + const items = generateMockItems(50) + return ( + + {filteredItems => ( + + )} + + ) + }, +} + +export const FilterableListWithCounter: Story = { + render: () => { + const items = generateMockItems(100) + return ( + + {filteredItems => ( + + )} + + ) + }, +} + +export const FilterableListDisabled: Story = { + render: () => { + const items = generateMockItems(20) + return ( +
      + + {filteredItems => ( + ( +
      +
      {item.name}
      +
      + {item.description} +
      +
      + )} + /> + )} +
      +
      + ) + }, +} diff --git a/web/common/src/components/VirtualList/VirtualList.tsx b/web/common/src/components/VirtualList/VirtualList.tsx new file mode 100644 index 0000000000..daec45fe16 --- /dev/null +++ b/web/common/src/components/VirtualList/VirtualList.tsx @@ -0,0 +1,118 @@ +import { useVirtualizer } from '@tanstack/react-virtual' +import React from 'react' +import { HorizontalContainer } from '../HorizontalContainer/HorizontalContainer' +import { cn } from '@/utils' +import { Button } from '../Button/Button' +import { ScrollContainer } from '../ScrollContainer/ScrollContainer' +import { VerticalContainer } from '../VerticalContainer/VerticalContainer' + +export function VirtualList({ + items, + estimatedListItemHeight, + renderListItem, + isSelected, + className, +}: { + items: TItem[] + estimatedListItemHeight: number + renderListItem: (item: TItem) => React.ReactNode + isSelected?: (item: TItem) => boolean + className?: string +}) { + const scrollableAreaRef = React.useRef(null) + + const [activeItemIndex] = React.useMemo(() => { + let activeIndex = -1 + const itemsLength = items.length + + for (let i = 0; i < itemsLength; i++) { + if (isSelected?.(items[i])) { + activeIndex = i + break + } + } + + return [activeIndex] + }, [items, isSelected]) + + const rowVirtualizer = useVirtualizer({ + count: items.length, + getScrollElement: () => scrollableAreaRef.current, + estimateSize: () => estimatedListItemHeight, + }) + + const scrollToItem = React.useCallback( + ({ + itemIndex, + isSmoothScroll = true, + }: { + itemIndex: number + isSmoothScroll?: boolean + }): void => { + rowVirtualizer.scrollToIndex(itemIndex, { + align: 'center', + behavior: isSmoothScroll ? 'smooth' : 'auto', + }) + }, + [rowVirtualizer], + ) + + const isOutsideVisibleRange = React.useCallback( + (itemIndex: number): boolean => { + const range = rowVirtualizer.range + return ( + range !== null && + (range.startIndex > itemIndex || range?.endIndex < itemIndex) + ) + }, + [rowVirtualizer], + ) + + /** + * The return button should appear when the + * active item is available in the list (not + * filtered out) and it is not in the visible + * range of the virtualized list + */ + const shouldShowReturnButton = + activeItemIndex > -1 && isOutsideVisibleRange(activeItemIndex) + const rows = rowVirtualizer.getVirtualItems() + const totalSize = rowVirtualizer.getTotalSize() + + return ( + + {shouldShowReturnButton && ( + + )} + +
      0 ? `${totalSize}px` : '100%', + contain: 'strict', + }} + > + + {rows.map(row => renderListItem(items[row.index]))} + +
      +
      +
      + ) +} diff --git a/web/common/src/styles/design/semantic-colors.css b/web/common/src/styles/design/semantic-colors.css index f2a45e5eef..4217b7f654 100644 --- a/web/common/src/styles/design/semantic-colors.css +++ b/web/common/src/styles/design/semantic-colors.css @@ -62,4 +62,20 @@ --color-link-active: var(--color-action-active); --color-link-visited: var(--color-purple-600); --color-link-underline: var(--color-deep-blue-125); + + /* Typography */ + --color-typography-headline: var(--color-prose); + --color-typography-tagline: var(--color-neutral-600); + --color-typography-description: var(--color-neutral-500); + --color-typography-info: var(--color-typography-tagline); + + /* Message */ + --color-message-lucid: var(--color-neutral-3); + + /* Input */ + --color-input-background: var(--color-light); + --color-input-background-lucid: var(--color-neutral-5); + --color-input-foreground: var(--color-prose); + --color-input-placeholder: var(--color-neutral-400); + --color-input-border: var(--color-neutral-300); } diff --git a/web/common/src/types.ts b/web/common/src/types.ts index d23ea8b86b..3de26b205d 100644 --- a/web/common/src/types.ts +++ b/web/common/src/types.ts @@ -3,6 +3,8 @@ export declare const __brand: unique symbol export type Brand = { [__brand]: B } export type Branded = T & Brand +export type Callback = (data?: T) => void + export type Size = '2xs' | 'xs' | 's' | 'm' | 'l' | 'xl' | '2xl' export type HeadlineLevel = 1 | 2 | 3 | 4 | 5 | 6 export type Side = 'left' | 'right' | 'both' diff --git a/web/common/tailwind.base.config.js b/web/common/tailwind.base.config.js index 005c353379..cbba9768c2 100644 --- a/web/common/tailwind.base.config.js +++ b/web/common/tailwind.base.config.js @@ -36,6 +36,15 @@ module.exports = { 800: 'var(--color-neutral-800)', 900: 'var(--color-neutral-900)', }, + typography: { + heading: 'var(--color-typography-headline)', + tagline: 'var(--color-typography-tagline)', + description: 'var(--color-typography-description)', + info: 'var(--color-typography-info)', + }, + message: { + lucid: 'var(--color-message-lucid)', + }, link: { underline: 'var(--color-link-underline)', hover: 'var(--color-link-hover)', @@ -63,6 +72,13 @@ module.exports = { background: 'var(--color-badge-background)', foreground: 'var(--color-badge-foreground)', }, + input: { + 'background-lucid': 'var(--color-input-background-lucid)', + background: 'var(--color-input-background)', + foreground: 'var(--color-input-foreground)', + placeholder: 'var(--color-input-placeholder)', + border: 'var(--color-input-border)', + }, button: { primary: { background: 'var(--color-button-primary-background)', From 203b74ec5e092ed2b7aa53f272db37347c0151ec Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Thu, 11 Sep 2025 09:08:41 +1200 Subject: [PATCH 0846/1056] Fix: When restating prod, clear intervals across all related snapshots, not just promoted ones (#5274) --- sqlmesh/core/plan/common.py | 184 ++++++++++++++++++++++++- sqlmesh/core/plan/evaluator.py | 118 +++------------- sqlmesh/core/snapshot/__init__.py | 1 + sqlmesh/core/snapshot/definition.py | 29 +++- sqlmesh/core/state_sync/base.py | 3 +- sqlmesh/core/state_sync/cache.py | 3 +- sqlmesh/core/state_sync/db/facade.py | 3 +- sqlmesh/core/state_sync/db/interval.py | 12 +- sqlmesh/core/state_sync/db/snapshot.py | 11 +- tests/core/test_integration.py | 174 +++++++++++++++++++++++ tests/core/test_snapshot.py | 25 ++++ 11 files changed, 447 insertions(+), 116 deletions(-) diff --git a/sqlmesh/core/plan/common.py b/sqlmesh/core/plan/common.py index 929837eb7e..4ae8a3112c 100644 --- a/sqlmesh/core/plan/common.py +++ b/sqlmesh/core/plan/common.py @@ -1,6 +1,15 @@ from __future__ import annotations +import typing as t +import logging +from dataclasses import dataclass, field -from sqlmesh.core.snapshot import Snapshot +from sqlmesh.core.state_sync import StateReader +from sqlmesh.core.snapshot import Snapshot, SnapshotId, SnapshotIdAndVersion, SnapshotNameVersion +from sqlmesh.core.snapshot.definition import Interval +from sqlmesh.utils.dag import DAG +from sqlmesh.utils.date import now_timestamp + +logger = logging.getLogger(__name__) def should_force_rebuild(old: Snapshot, new: Snapshot) -> bool: @@ -27,3 +36,176 @@ def is_breaking_kind_change(old: Snapshot, new: Snapshot) -> bool: # If the partitioning hasn't changed, then we don't need to rebuild return False return True + + +@dataclass +class SnapshotIntervalClearRequest: + # affected snapshot + snapshot: SnapshotIdAndVersion + + # which interval to clear + interval: Interval + + # which environments this snapshot is currently promoted + # note that this can be empty if the snapshot exists because its ttl has not expired + # but it is not part of any particular environment + environment_names: t.Set[str] = field(default_factory=set) + + @property + def snapshot_id(self) -> SnapshotId: + return self.snapshot.snapshot_id + + @property + def sorted_environment_names(self) -> t.List[str]: + return list(sorted(self.environment_names)) + + +def identify_restatement_intervals_across_snapshot_versions( + state_reader: StateReader, + prod_restatements: t.Dict[str, Interval], + disable_restatement_models: t.Set[str], + loaded_snapshots: t.Dict[SnapshotId, Snapshot], + current_ts: t.Optional[int] = None, +) -> t.Dict[SnapshotId, SnapshotIntervalClearRequest]: + """ + Given a map of snapshot names + intervals to restate in prod: + - Look up matching snapshots (match based on name - regardless of version, to get all versions) + - For each match, also match downstream snapshots in each dev environment while filtering out models that have restatement disabled + - Return a list of all snapshots that are affected + the interval that needs to be cleared for each + + The goal here is to produce a list of intervals to invalidate across all dev snapshots so that a subsequent plan or + cadence run in those environments causes the intervals to be repopulated. + """ + if not prod_restatements: + return {} + + # Although :loaded_snapshots is sourced from RestatementStage.all_snapshots, since the only time we ever need + # to clear intervals across all environments is for prod, the :loaded_snapshots here are always from prod + prod_name_versions: t.Set[SnapshotNameVersion] = { + s.name_version for s in loaded_snapshots.values() + } + + snapshot_intervals_to_clear: t.Dict[SnapshotId, SnapshotIntervalClearRequest] = {} + + for env_summary in state_reader.get_environments_summary(): + # Fetch the full environment object one at a time to avoid loading all environments into memory at once + env = state_reader.get_environment(env_summary.name) + if not env: + logger.warning("Environment %s not found", env_summary.name) + continue + + snapshots_by_name = {s.name: s.table_info for s in env.snapshots} + + # We dont just restate matching snapshots, we also have to restate anything downstream of them + # so that if A gets restated in prod and dev has A <- B <- C, B and C get restated in dev + env_dag = DAG({s.name: {p.name for p in s.parents} for s in env.snapshots}) + + for restate_snapshot_name, interval in prod_restatements.items(): + if restate_snapshot_name not in snapshots_by_name: + # snapshot is not promoted in this environment + continue + + affected_snapshot_names = [ + x + for x in ([restate_snapshot_name] + env_dag.downstream(restate_snapshot_name)) + if x not in disable_restatement_models + ] + + for affected_snapshot_name in affected_snapshot_names: + affected_snapshot = snapshots_by_name[affected_snapshot_name] + + # Don't clear intervals for a dev snapshot if it shares the same physical version with prod. + # Otherwise, prod will be affected by what should be a dev operation + if affected_snapshot.name_version in prod_name_versions: + continue + + clear_request = snapshot_intervals_to_clear.get(affected_snapshot.snapshot_id) + if not clear_request: + clear_request = SnapshotIntervalClearRequest( + snapshot=affected_snapshot.id_and_version, interval=interval + ) + snapshot_intervals_to_clear[affected_snapshot.snapshot_id] = clear_request + + clear_request.environment_names |= set([env.name]) + + # snapshot_intervals_to_clear now contains the entire hierarchy of affected snapshots based + # on building the DAG for each environment and including downstream snapshots + # but, what if there are affected snapshots that arent part of any environment? + unique_snapshot_names = set(snapshot_id.name for snapshot_id in snapshot_intervals_to_clear) + + current_ts = current_ts or now_timestamp() + all_matching_non_prod_snapshots = { + s.snapshot_id: s + for s in state_reader.get_snapshots_by_names( + snapshot_names=unique_snapshot_names, current_ts=current_ts, exclude_expired=True + ) + # Don't clear intervals for a snapshot if it shares the same physical version with prod. + # Otherwise, prod will be affected by what should be a dev operation + if s.name_version not in prod_name_versions + } + + # identify the ones that we havent picked up yet, which are the ones that dont exist in any environment + if remaining_snapshot_ids := set(all_matching_non_prod_snapshots).difference( + snapshot_intervals_to_clear + ): + # these snapshot id's exist in isolation and may be related to a downstream dependency of the :prod_restatements, + # rather than directly related, so we can't simply look up the interval to clear based on :prod_restatements. + # To figure out the interval that should be cleared, we can match to the existing list based on name + # and conservatively take the widest interval that shows up + snapshot_name_to_widest_interval: t.Dict[str, Interval] = {} + for s_id, clear_request in snapshot_intervals_to_clear.items(): + current_start, current_end = snapshot_name_to_widest_interval.get( + s_id.name, clear_request.interval + ) + next_start, next_end = clear_request.interval + + next_start = min(current_start, next_start) + next_end = max(current_end, next_end) + + snapshot_name_to_widest_interval[s_id.name] = (next_start, next_end) + + for remaining_snapshot_id in remaining_snapshot_ids: + remaining_snapshot = all_matching_non_prod_snapshots[remaining_snapshot_id] + snapshot_intervals_to_clear[remaining_snapshot_id] = SnapshotIntervalClearRequest( + snapshot=remaining_snapshot, + interval=snapshot_name_to_widest_interval[remaining_snapshot_id.name], + ) + + # for any affected full_history_restatement_only snapshots, we need to widen the intervals being restated to + # include the whole time range for that snapshot. This requires a call to state to load the full snapshot record, + # so we only do it if necessary + full_history_restatement_snapshot_ids = [ + # FIXME: full_history_restatement_only is just one indicator that the snapshot can only be fully refreshed, the other one is Model.depends_on_self + # however, to figure out depends_on_self, we have to render all the model queries which, alongside having to fetch full snapshots from state, + # is problematic in secure environments that are deliberately isolated from arbitrary user code (since rendering a query may require user macros to be present) + # So for now, these are not considered + s_id + for s_id, s in snapshot_intervals_to_clear.items() + if s.snapshot.full_history_restatement_only + ] + if full_history_restatement_snapshot_ids: + # only load full snapshot records that we havent already loaded + additional_snapshots = state_reader.get_snapshots( + [ + s.snapshot_id + for s in full_history_restatement_snapshot_ids + if s.snapshot_id not in loaded_snapshots + ] + ) + + all_snapshots = loaded_snapshots | additional_snapshots + + for full_snapshot_id in full_history_restatement_snapshot_ids: + full_snapshot = all_snapshots[full_snapshot_id] + intervals_to_clear = snapshot_intervals_to_clear[full_snapshot_id] + + original_start, original_end = intervals_to_clear.interval + + # get_removal_interval() widens intervals if necessary + new_interval = full_snapshot.get_removal_interval( + start=original_start, end=original_end + ) + + intervals_to_clear.interval = new_interval + + return snapshot_intervals_to_clear diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 03b0b64016..79053e018b 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -22,7 +22,7 @@ from sqlmesh.core.console import Console, get_console from sqlmesh.core.environment import EnvironmentNamingInfo, execute_environment_statements from sqlmesh.core.macros import RuntimeStage -from sqlmesh.core.snapshot.definition import Interval, to_view_mapping +from sqlmesh.core.snapshot.definition import to_view_mapping from sqlmesh.core.plan import stages from sqlmesh.core.plan.definition import EvaluatablePlan from sqlmesh.core.scheduler import Scheduler @@ -33,17 +33,15 @@ SnapshotIntervals, SnapshotId, SnapshotInfoLike, - SnapshotTableInfo, SnapshotCreationFailedError, - SnapshotNameVersion, ) from sqlmesh.utils import to_snake_case from sqlmesh.core.state_sync import StateSync +from sqlmesh.core.plan.common import identify_restatement_intervals_across_snapshot_versions from sqlmesh.utils import CorrelationId from sqlmesh.utils.concurrency import NodeExecutionFailedError from sqlmesh.utils.errors import PlanError, SQLMeshError -from sqlmesh.utils.dag import DAG -from sqlmesh.utils.date import now +from sqlmesh.utils.date import now, to_timestamp logger = logging.getLogger(__name__) @@ -289,7 +287,9 @@ def visit_audit_only_run_stage( def visit_restatement_stage( self, stage: stages.RestatementStage, plan: EvaluatablePlan ) -> None: - snapshot_intervals_to_restate = {(s, i) for s, i in stage.snapshot_intervals.items()} + snapshot_intervals_to_restate = { + (s.id_and_version, i) for s, i in stage.snapshot_intervals.items() + } # Restating intervals on prod plans should mean that the intervals are cleared across # all environments, not just the version currently in prod @@ -298,11 +298,16 @@ def visit_restatement_stage( # # Without this rule, its possible that promoting a dev table to prod will introduce old data to prod snapshot_intervals_to_restate.update( - self._restatement_intervals_across_all_environments( - prod_restatements=plan.restatements, - disable_restatement_models=plan.disabled_restatement_models, - loaded_snapshots={s.snapshot_id: s for s in stage.all_snapshots.values()}, - ) + { + (s.snapshot, s.interval) + for s in identify_restatement_intervals_across_snapshot_versions( + state_reader=self.state_sync, + prod_restatements=plan.restatements, + disable_restatement_models=plan.disabled_restatement_models, + loaded_snapshots={s.snapshot_id: s for s in stage.all_snapshots.values()}, + current_ts=to_timestamp(plan.execution_time or now()), + ).values() + } ) self.state_sync.remove_intervals( @@ -422,97 +427,6 @@ def _demote_snapshots( on_complete=on_complete, ) - def _restatement_intervals_across_all_environments( - self, - prod_restatements: t.Dict[str, Interval], - disable_restatement_models: t.Set[str], - loaded_snapshots: t.Dict[SnapshotId, Snapshot], - ) -> t.Set[t.Tuple[SnapshotTableInfo, Interval]]: - """ - Given a map of snapshot names + intervals to restate in prod: - - Look up matching snapshots across all environments (match based on name - regardless of version) - - For each match, also match downstream snapshots while filtering out models that have restatement disabled - - Return all matches mapped to the intervals of the prod snapshot being restated - - The goal here is to produce a list of intervals to invalidate across all environments so that a cadence - run in those environments causes the intervals to be repopulated - """ - if not prod_restatements: - return set() - - prod_name_versions: t.Set[SnapshotNameVersion] = { - s.name_version for s in loaded_snapshots.values() - } - - snapshots_to_restate: t.Dict[SnapshotId, t.Tuple[SnapshotTableInfo, Interval]] = {} - - for env_summary in self.state_sync.get_environments_summary(): - # Fetch the full environment object one at a time to avoid loading all environments into memory at once - env = self.state_sync.get_environment(env_summary.name) - if not env: - logger.warning("Environment %s not found", env_summary.name) - continue - - keyed_snapshots = {s.name: s.table_info for s in env.snapshots} - - # We dont just restate matching snapshots, we also have to restate anything downstream of them - # so that if A gets restated in prod and dev has A <- B <- C, B and C get restated in dev - env_dag = DAG({s.name: {p.name for p in s.parents} for s in env.snapshots}) - - for restatement, intervals in prod_restatements.items(): - if restatement not in keyed_snapshots: - continue - affected_snapshot_names = [ - x - for x in ([restatement] + env_dag.downstream(restatement)) - if x not in disable_restatement_models - ] - snapshots_to_restate.update( - { - keyed_snapshots[a].snapshot_id: (keyed_snapshots[a], intervals) - for a in affected_snapshot_names - # Don't restate a snapshot if it shares the version with a snapshot in prod - if keyed_snapshots[a].name_version not in prod_name_versions - } - ) - - # for any affected full_history_restatement_only snapshots, we need to widen the intervals being restated to - # include the whole time range for that snapshot. This requires a call to state to load the full snapshot record, - # so we only do it if necessary - full_history_restatement_snapshot_ids = [ - # FIXME: full_history_restatement_only is just one indicator that the snapshot can only be fully refreshed, the other one is Model.depends_on_self - # however, to figure out depends_on_self, we have to render all the model queries which, alongside having to fetch full snapshots from state, - # is problematic in secure environments that are deliberately isolated from arbitrary user code (since rendering a query may require user macros to be present) - # So for now, these are not considered - s_id - for s_id, s in snapshots_to_restate.items() - if s[0].full_history_restatement_only - ] - if full_history_restatement_snapshot_ids: - # only load full snapshot records that we havent already loaded - additional_snapshots = self.state_sync.get_snapshots( - [ - s.snapshot_id - for s in full_history_restatement_snapshot_ids - if s.snapshot_id not in loaded_snapshots - ] - ) - - all_snapshots = loaded_snapshots | additional_snapshots - - for full_snapshot_id in full_history_restatement_snapshot_ids: - full_snapshot = all_snapshots[full_snapshot_id] - _, original_intervals = snapshots_to_restate[full_snapshot_id] - original_start, original_end = original_intervals - - # get_removal_interval() widens intervals if necessary - new_intervals = full_snapshot.get_removal_interval( - start=original_start, end=original_end - ) - snapshots_to_restate[full_snapshot_id] = (full_snapshot.table_info, new_intervals) - - return set(snapshots_to_restate.values()) - def _update_intervals_for_new_snapshots(self, snapshots: t.Collection[Snapshot]) -> None: snapshots_intervals: t.List[SnapshotIntervals] = [] for snapshot in snapshots: diff --git a/sqlmesh/core/snapshot/__init__.py b/sqlmesh/core/snapshot/__init__.py index 32842cc4b2..65e5c2a822 100644 --- a/sqlmesh/core/snapshot/__init__.py +++ b/sqlmesh/core/snapshot/__init__.py @@ -11,6 +11,7 @@ SnapshotId as SnapshotId, SnapshotIdBatch as SnapshotIdBatch, SnapshotIdLike as SnapshotIdLike, + SnapshotIdAndVersionLike as SnapshotIdAndVersionLike, SnapshotInfoLike as SnapshotInfoLike, SnapshotIntervals as SnapshotIntervals, SnapshotNameVersion as SnapshotNameVersion, diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 35109ec36e..c17e94be10 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -587,14 +587,26 @@ def name_version(self) -> SnapshotNameVersion: """Returns the name and version of the snapshot.""" return SnapshotNameVersion(name=self.name, version=self.version) + @property + def id_and_version(self) -> SnapshotIdAndVersion: + return SnapshotIdAndVersion( + name=self.name, + kind_name=self.kind_name, + identifier=self.identifier, + version=self.version, + dev_version=self.dev_version, + fingerprint=self.fingerprint, + ) + -class SnapshotIdAndVersion(PydanticModel): +class SnapshotIdAndVersion(PydanticModel, ModelKindMixin): """A stripped down version of a snapshot that is used in situations where we want to fetch the main fields of the snapshots table without the overhead of parsing the full snapshot payload and fetching intervals. """ name: str version: str + kind_name_: t.Optional[ModelKindName] = Field(default=None, alias="kind_name") dev_version_: t.Optional[str] = Field(alias="dev_version") identifier: str fingerprint_: t.Union[str, SnapshotFingerprint] = Field(alias="fingerprint") @@ -603,6 +615,10 @@ class SnapshotIdAndVersion(PydanticModel): def snapshot_id(self) -> SnapshotId: return SnapshotId(name=self.name, identifier=self.identifier) + @property + def id_and_version(self) -> SnapshotIdAndVersion: + return self + @property def name_version(self) -> SnapshotNameVersion: return SnapshotNameVersion(name=self.name, version=self.version) @@ -618,6 +634,10 @@ def fingerprint(self) -> SnapshotFingerprint: def dev_version(self) -> str: return self.dev_version_ or self.fingerprint.to_version() + @property + def model_kind_name(self) -> t.Optional[ModelKindName]: + return self.kind_name_ + class Snapshot(PydanticModel, SnapshotInfoMixin): """A snapshot represents a node at a certain point in time. @@ -1424,6 +1444,10 @@ def name_version(self) -> SnapshotNameVersion: """Returns the name and version of the snapshot.""" return SnapshotNameVersion(name=self.name, version=self.version) + @property + def id_and_version(self) -> SnapshotIdAndVersion: + return self.table_info.id_and_version + @property def disable_restatement(self) -> bool: """Is restatement disabled for the node""" @@ -1494,7 +1518,8 @@ class SnapshotTableCleanupTask(PydanticModel): dev_table_only: bool -SnapshotIdLike = t.Union[SnapshotId, SnapshotTableInfo, SnapshotIdAndVersion, Snapshot] +SnapshotIdLike = t.Union[SnapshotId, SnapshotIdAndVersion, SnapshotTableInfo, Snapshot] +SnapshotIdAndVersionLike = t.Union[SnapshotIdAndVersion, SnapshotTableInfo, Snapshot] SnapshotInfoLike = t.Union[SnapshotTableInfo, Snapshot] SnapshotNameVersionLike = t.Union[ SnapshotNameVersion, SnapshotTableInfo, SnapshotIdAndVersion, Snapshot diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index 4219472cb6..450d6f7408 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -19,6 +19,7 @@ Snapshot, SnapshotId, SnapshotIdLike, + SnapshotIdAndVersionLike, SnapshotInfoLike, SnapshotTableCleanupTask, SnapshotTableInfo, @@ -390,7 +391,7 @@ def remove_state(self, including_backup: bool = False) -> None: @abc.abstractmethod def remove_intervals( self, - snapshot_intervals: t.Sequence[t.Tuple[SnapshotInfoLike, Interval]], + snapshot_intervals: t.Sequence[t.Tuple[SnapshotIdAndVersionLike, Interval]], remove_shared_versions: bool = False, ) -> None: """Remove an interval from a list of snapshots and sync it to the store. diff --git a/sqlmesh/core/state_sync/cache.py b/sqlmesh/core/state_sync/cache.py index 8aa5054e13..3de4e7bf51 100644 --- a/sqlmesh/core/state_sync/cache.py +++ b/sqlmesh/core/state_sync/cache.py @@ -7,6 +7,7 @@ Snapshot, SnapshotId, SnapshotIdLike, + SnapshotIdAndVersionLike, SnapshotInfoLike, ) from sqlmesh.core.snapshot.definition import Interval, SnapshotIntervals @@ -128,7 +129,7 @@ def add_snapshots_intervals(self, snapshots_intervals: t.Sequence[SnapshotInterv def remove_intervals( self, - snapshot_intervals: t.Sequence[t.Tuple[SnapshotInfoLike, Interval]], + snapshot_intervals: t.Sequence[t.Tuple[SnapshotIdAndVersionLike, Interval]], remove_shared_versions: bool = False, ) -> None: for s, _ in snapshot_intervals: diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 93c4b87e9e..29fc9f1740 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -31,6 +31,7 @@ SnapshotIdAndVersion, SnapshotId, SnapshotIdLike, + SnapshotIdAndVersionLike, SnapshotInfoLike, SnapshotIntervals, SnapshotNameVersion, @@ -407,7 +408,7 @@ def add_snapshots_intervals(self, snapshots_intervals: t.Sequence[SnapshotInterv @transactional() def remove_intervals( self, - snapshot_intervals: t.Sequence[t.Tuple[SnapshotInfoLike, Interval]], + snapshot_intervals: t.Sequence[t.Tuple[SnapshotIdAndVersionLike, Interval]], remove_shared_versions: bool = False, ) -> None: self.interval_state.remove_intervals(snapshot_intervals, remove_shared_versions) diff --git a/sqlmesh/core/state_sync/db/interval.py b/sqlmesh/core/state_sync/db/interval.py index 75f475b75b..b15ad2d57b 100644 --- a/sqlmesh/core/state_sync/db/interval.py +++ b/sqlmesh/core/state_sync/db/interval.py @@ -15,10 +15,10 @@ from sqlmesh.core.snapshot import ( SnapshotIntervals, SnapshotIdLike, + SnapshotIdAndVersionLike, SnapshotNameVersionLike, SnapshotTableCleanupTask, SnapshotNameVersion, - SnapshotInfoLike, Snapshot, ) from sqlmesh.core.snapshot.definition import Interval @@ -68,11 +68,11 @@ def add_snapshots_intervals(self, snapshots_intervals: t.Sequence[SnapshotInterv def remove_intervals( self, - snapshot_intervals: t.Sequence[t.Tuple[SnapshotInfoLike, Interval]], + snapshot_intervals: t.Sequence[t.Tuple[SnapshotIdAndVersionLike, Interval]], remove_shared_versions: bool = False, ) -> None: intervals_to_remove: t.Sequence[ - t.Tuple[t.Union[SnapshotInfoLike, SnapshotIntervals], Interval] + t.Tuple[t.Union[SnapshotIdAndVersionLike, SnapshotIntervals], Interval] ] = snapshot_intervals if remove_shared_versions: name_version_mapping = {s.name_version: interval for s, interval in snapshot_intervals} @@ -431,7 +431,9 @@ def _delete_intervals_by_version(self, targets: t.List[SnapshotTableCleanupTask] def _intervals_to_df( - snapshot_intervals: t.Sequence[t.Tuple[t.Union[SnapshotInfoLike, SnapshotIntervals], Interval]], + snapshot_intervals: t.Sequence[ + t.Tuple[t.Union[SnapshotIdAndVersionLike, SnapshotIntervals], Interval] + ], is_dev: bool, is_removed: bool, ) -> pd.DataFrame: @@ -451,7 +453,7 @@ def _intervals_to_df( def _interval_to_df( - snapshot: t.Union[SnapshotInfoLike, SnapshotIntervals], + snapshot: t.Union[SnapshotIdAndVersionLike, SnapshotIntervals], start_ts: int, end_ts: int, is_dev: bool = False, diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 1904e51c55..4a8b2c44c5 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -337,6 +337,7 @@ def get_snapshots_by_names( name=name, identifier=identifier, version=version, + kind_name=kind_name or None, dev_version=dev_version, fingerprint=fingerprint, ) @@ -344,9 +345,11 @@ def get_snapshots_by_names( snapshot_names=snapshot_names, batch_size=self.SNAPSHOT_BATCH_SIZE, ) - for name, identifier, version, dev_version, fingerprint in fetchall( + for name, identifier, version, kind_name, dev_version, fingerprint in fetchall( self.engine_adapter, - exp.select("name", "identifier", "version", "dev_version", "fingerprint") + exp.select( + "name", "identifier", "version", "kind_name", "dev_version", "fingerprint" + ) .from_(self.snapshots_table) .where(where) .and_(unexpired_expr), @@ -661,6 +664,7 @@ def _get_snapshots_with_same_version( "name", "identifier", "version", + "kind_name", "dev_version", "fingerprint", ) @@ -677,10 +681,11 @@ def _get_snapshots_with_same_version( name=name, identifier=identifier, version=version, + kind_name=kind_name or None, dev_version=dev_version, fingerprint=SnapshotFingerprint.parse_raw(fingerprint), ) - for name, identifier, version, dev_version, fingerprint in snapshot_rows + for name, identifier, version, kind_name, dev_version, fingerprint in snapshot_rows ] diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 8d4991adb9..ef7c59ea7d 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -4278,6 +4278,180 @@ def test_prod_restatement_plan_missing_model_in_dev( ) +def test_prod_restatement_plan_includes_related_unpromoted_snapshots(tmp_path: Path): + """ + Scenario: + - I have models A <- B in prod + - I have models A <- B <- C in dev + - Both B and C have gone through a few iterations in dev so multiple snapshot versions exist + for them but not all of them are promoted / active + - I restate A in prod + + Outcome: + - Intervals should be cleared for all of the versions of B and C, regardless + of if they are active in any particular environment, in case they ever get made + active + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + (models_dir / "a.sql").write_text(""" + MODEL ( + name test.a, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@daily' + ); + + select 1 as a, now() as ts; + """) + + (models_dir / "b.sql").write_text(""" + MODEL ( + name test.b, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@daily' + ); + + select a, ts from test.a + """) + + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01")) + ctx = Context(paths=[tmp_path], config=config) + + def _all_snapshots() -> t.Dict[SnapshotId, Snapshot]: + all_snapshot_ids = [ + SnapshotId(name=name, identifier=identifier) + for (name, identifier) in ctx.state_sync.state_sync.engine_adapter.fetchall( # type: ignore + "select name, identifier from sqlmesh._snapshots" + ) + ] + return ctx.state_sync.get_snapshots(all_snapshot_ids) + + # plan + apply prod + ctx.plan(environment="prod", auto_apply=True) + assert len(_all_snapshots()) == 2 + + # create dev with new version of B + (models_dir / "b.sql").write_text(""" + MODEL ( + name test.b, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@daily' + ); + + select a, ts, 'b dev 1' as change from test.a + """) + + ctx.load() + ctx.plan(environment="dev", auto_apply=True) + assert len(_all_snapshots()) == 3 + + # update B (new version) and create C + (models_dir / "b.sql").write_text(""" + MODEL ( + name test.b, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@daily' + ); + + select a, ts, 'b dev 2' as change from test.a + """) + + (models_dir / "c.sql").write_text(""" + MODEL ( + name test.c, + kind FULL, + cron '@daily' + ); + + select *, 'c initial' as val from test.b + """) + + ctx.load() + ctx.plan(environment="dev", auto_apply=True) + assert len(_all_snapshots()) == 5 + + # update C (new version), create D (unrelated) + (models_dir / "c.sql").write_text(""" + MODEL ( + name test.c, + kind FULL, + cron '@daily' + ); + + select *, 'c updated' as val from test.b + """) + + (models_dir / "d.sql").write_text(""" + MODEL ( + name test.d, + cron '@daily' + ); + + select 1 as unrelated + """) + + ctx.load() + ctx.plan(environment="dev", auto_apply=True) + all_snapshots_prior_to_restatement = _all_snapshots() + assert len(all_snapshots_prior_to_restatement) == 7 + + def _snapshot_instances(lst: t.Dict[SnapshotId, Snapshot], name_match: str) -> t.List[Snapshot]: + return [s for s_id, s in lst.items() if name_match in s_id.name] + + # verify initial state + + # 1 instance of A (prod) + assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"a"')) == 1 + + # 3 instances of B (original in prod + 2 updates in dev) + assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"b"')) == 3 + + # 2 instances of C (initial + update in dev) + assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"c"')) == 2 + + # 1 instance of D (initial - dev) + assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"d"')) == 1 + + # restate A in prod + ctx.plan(environment="prod", restate_models=['"memory"."test"."a"'], auto_apply=True) + + all_snapshots_after_restatement = _all_snapshots() + + # All versions of B and C in dev should have had intervals cleared + # D in dev should not be touched and A + B in prod shoud also not be touched + a = _snapshot_instances(all_snapshots_after_restatement, '"a"') + assert len(a) == 1 + + b = _snapshot_instances(all_snapshots_after_restatement, '"b"') + # the 1 B instance in prod should be populated and 2 in dev (1 active) should be cleared + assert len(b) == 3 + assert len([s for s in b if not s.intervals]) == 2 + + c = _snapshot_instances(all_snapshots_after_restatement, '"c"') + # the 2 instances of C in dev (1 active) should be cleared + assert len(c) == 2 + assert len([s for s in c if not s.intervals]) == 2 + + d = _snapshot_instances(all_snapshots_after_restatement, '"d"') + # D should not be touched since it's in no way downstream of A in prod + assert len(d) == 1 + assert d[0].intervals + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_dev_restatement_of_prod_model(init_and_plan_context: t.Callable): context, plan = init_and_plan_context("examples/sushi") diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index eff3ad2b60..c769991b86 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -3567,3 +3567,28 @@ def test_snapshot_id_and_version_fingerprint_lazy_init(): assert isinstance(snapshot.fingerprint_, SnapshotFingerprint) assert snapshot.fingerprint == fingerprint + + +def test_snapshot_id_and_version_optional_kind_name(): + snapshot = SnapshotIdAndVersion( + name="a", + identifier="1234", + version="2345", + dev_version=None, + fingerprint="", + ) + + assert snapshot.model_kind_name is None + + snapshot = SnapshotIdAndVersion( + name="a", + identifier="1234", + version="2345", + kind_name="INCREMENTAL_UNMANAGED", + dev_version=None, + fingerprint="", + ) + + assert snapshot.model_kind_name + assert snapshot.is_incremental_unmanaged + assert snapshot.full_history_restatement_only From dc302ebb95fecf3416c1201b8c58d9b154b157a2 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 11 Sep 2025 15:02:10 +0300 Subject: [PATCH 0847/1056] Fix: normalize unit testing fixture catalog (#5343) --- sqlmesh/core/test/definition.py | 7 ++- .../integration/test_integration_snowflake.py | 63 +++++++++++++++---- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index b995310d09..a12dafec19 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -100,8 +100,11 @@ def __init__( self._validate_and_normalize_test() if self.engine_adapter.default_catalog: - self._fixture_catalog: t.Optional[exp.Identifier] = exp.parse_identifier( - self.engine_adapter.default_catalog, dialect=self._test_adapter_dialect + self._fixture_catalog: t.Optional[exp.Identifier] = normalize_identifiers( + exp.parse_identifier( + self.engine_adapter.default_catalog, dialect=self._test_adapter_dialect + ), + dialect=self._test_adapter_dialect, ) else: self._fixture_catalog = None diff --git a/tests/core/engine_adapter/integration/test_integration_snowflake.py b/tests/core/engine_adapter/integration/test_integration_snowflake.py index aed6bf83e4..f9862c51cb 100644 --- a/tests/core/engine_adapter/integration/test_integration_snowflake.py +++ b/tests/core/engine_adapter/integration/test_integration_snowflake.py @@ -1,26 +1,25 @@ -import typing as t import pytest +import typing as t +from datetime import datetime +from pathlib import Path from pytest import FixtureRequest +from pytest_mock import MockerFixture + +import sqlmesh.core.dialect as d from sqlglot import exp -from pathlib import Path -from sqlglot.optimizer.qualify_columns import quote_identifiers +from sqlmesh import Config, ExecutionContext, model from sqlglot.helper import seq_get +from sqlglot.optimizer.qualify_columns import quote_identifiers +from sqlmesh.core.config import ModelDefaultsConfig from sqlmesh.core.engine_adapter import SnowflakeEngineAdapter from sqlmesh.core.engine_adapter.shared import DataObject -import sqlmesh.core.dialect as d -from sqlmesh.core.model import SqlModel, load_sql_based_model +from sqlmesh.core.model import ModelKindName, SqlModel, load_sql_based_model from sqlmesh.core.plan import Plan -from tests.core.engine_adapter.integration import TestContext -from sqlmesh import model, ExecutionContext -from pytest_mock import MockerFixture from sqlmesh.core.snapshot import SnapshotId, SnapshotIdBatch from sqlmesh.core.snapshot.execution_tracker import ( QueryExecutionContext, QueryExecutionTracker, ) -from sqlmesh.core.model import ModelKindName -from datetime import datetime - from tests.core.engine_adapter.integration import ( TestContext, generate_pytest_params, @@ -337,3 +336,45 @@ def test_rows_tracker( assert stats is not None assert stats.total_rows_processed is None assert stats.total_bytes_processed is None + + +def test_unit_test(tmp_path: Path, ctx: TestContext): + models_path = tmp_path / "models" + tests_path = tmp_path / "tests" + + models_path.mkdir() + tests_path.mkdir() + + test_payload = """ +test_dummy_model: + model: s.dummy + inputs: + s.src_table: + rows: + - c: 1 + outputs: + query: + - c: 1 + """ + + (models_path / "dummy_model.sql").write_text(f"MODEL (name s.dummy); SELECT c FROM s.src_table") + (tests_path / "test_dummy_model.yaml").write_text(test_payload) + + def _config_mutator(gateway_name: str, config: Config): + config.model_defaults = ModelDefaultsConfig(dialect="snowflake") + test_connection = config.gateways[gateway_name].connection.copy() # type: ignore + + # Force the database to lowercase to test that we normalize (if we didn't, the test would fail) + test_connection.database = test_connection.database.lower() # type: ignore + config.gateways[gateway_name].test_connection = test_connection + + sqlmesh = ctx.create_context(path=tmp_path, config_mutator=_config_mutator) + + test_conn = sqlmesh.config.get_test_connection(ctx.gateway) + assert test_conn.type_ == "snowflake" + + catalog = test_conn.get_catalog() + assert catalog is not None and catalog.islower() + + test_results = sqlmesh.test() + assert not test_results.errors From b31ab787039f75d1d51f158e8f43e483f9287a18 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:43:19 +0300 Subject: [PATCH 0848/1056] Chore: make `column_descriptions` validation more lenient (#5354) --- sqlmesh/core/model/meta.py | 8 ++++++-- tests/core/test_model.py | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 11e384b813..9208fbdbb5 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -247,11 +247,15 @@ def _column_descriptions_validator( columns_to_types = info.data.get("columns_to_types_") if columns_to_types: - for column_name in col_descriptions: + from sqlmesh.core.console import get_console + + console = get_console() + for column_name in list(col_descriptions): if column_name not in columns_to_types: - raise ConfigError( + console.log_warning( f"In model '{info.data['name']}', a description is provided for column '{column_name}' but it is not a column in the model." ) + del col_descriptions[column_name] return col_descriptions diff --git a/tests/core/test_model.py b/tests/core/test_model.py index a252418a79..1511e37c53 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -2934,11 +2934,16 @@ def a_model(context): def b_model(context): pass - with pytest.raises(ConfigError, match="a description is provided for column 'COL'"): + with patch.object(get_console(), "log_warning") as mock_logger: py_model = model.get_registry()["col_descriptions_quoted"].model( module_path=Path("."), path=Path("."), ) + assert '"COL"' not in py_model.column_descriptions + assert ( + mock_logger.mock_calls[0].args[0] + == "In model 'col_descriptions_quoted', a description is provided for column 'COL' but it is not a column in the model." + ) def test_python_model_unsupported_kind() -> None: From 7676aa1d375eebfce8487f617a31bfe87c026fc3 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:36:29 +0000 Subject: [PATCH 0849/1056] Chore: update row test bc redshift now uses CTAS (#5344) --- tests/core/engine_adapter/integration/test_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index ccea105bcf..e02877e0c6 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -2423,9 +2423,9 @@ def capture_execution_stats( if ctx.engine_adapter.SUPPORTS_QUERY_EXECUTION_TRACKING: assert actual_execution_stats["incremental_model"].total_rows_processed == 7 - # snowflake doesn't track rows for CTAS + # snowflake and redshift don't track rows for CTAS assert actual_execution_stats["full_model"].total_rows_processed == ( - None if ctx.mark.startswith("snowflake") else 3 + None if ctx.mark.startswith("snowflake") or ctx.mark.startswith("redshift") else 3 ) assert actual_execution_stats["seed_model"].total_rows_processed == ( None if ctx.mark.startswith("snowflake") else 7 From 00547b0ea44599b8bf5c6c98f2d22dfe5e70db8d Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 11 Sep 2025 18:13:36 +0300 Subject: [PATCH 0850/1056] Fix(databricks): Materialized view drop failure due to incorrect type (#5351) --- sqlmesh/core/engine_adapter/databricks.py | 4 +- tests/core/engine_adapter/test_databricks.py | 72 +++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/engine_adapter/databricks.py b/sqlmesh/core/engine_adapter/databricks.py index da70163db4..2571cb7214 100644 --- a/sqlmesh/core/engine_adapter/databricks.py +++ b/sqlmesh/core/engine_adapter/databricks.py @@ -266,7 +266,9 @@ def _get_data_objects( exp.column("table_catalog").as_("catalog"), exp.case(exp.column("table_type")) .when(exp.Literal.string("VIEW"), exp.Literal.string("view")) - .when(exp.Literal.string("MATERIALIZED_VIEW"), exp.Literal.string("view")) + .when( + exp.Literal.string("MATERIALIZED_VIEW"), exp.Literal.string("materialized_view") + ) .else_(exp.Literal.string("table")) .as_("type"), ) diff --git a/tests/core/engine_adapter/test_databricks.py b/tests/core/engine_adapter/test_databricks.py index cd4c8c4074..fcd7aec0fa 100644 --- a/tests/core/engine_adapter/test_databricks.py +++ b/tests/core/engine_adapter/test_databricks.py @@ -8,7 +8,7 @@ from sqlmesh.core import dialect as d from sqlmesh.core.engine_adapter import DatabricksEngineAdapter -from sqlmesh.core.engine_adapter.shared import DataObject +from sqlmesh.core.engine_adapter.shared import DataObject, DataObjectType from sqlmesh.core.node import IntervalUnit from tests.core.engine_adapter import to_sql_calls @@ -219,3 +219,73 @@ def test_create_table_clustered_by(mocker: MockFixture, make_mocked_engine_adapt assert sql_calls == [ "CREATE TABLE IF NOT EXISTS `test_table` (`cola` INT, `colb` STRING) CLUSTER BY (`cola`)", ] + + +def test_get_data_objects_distinguishes_view_types(mocker): + adapter = DatabricksEngineAdapter(lambda: None, default_catalog="test_catalog") + + # (Databricks requires DBSQL Serverless or Pro warehouse to test materialized views which we do not have setup) + # so this mocks the fetchdf call to simulate the response we would expect from the correct SQL query + mock_df = pd.DataFrame( + [ + { + "name": "regular_view", + "schema": "test_schema", + "catalog": "test_catalog", + "type": "view", + }, + { + "name": "mat_view", + "schema": "test_schema", + "catalog": "test_catalog", + "type": "materialized_view", + }, + { + "name": "regular_table", + "schema": "test_schema", + "catalog": "test_catalog", + "type": "table", + }, + ] + ) + + mocker.patch.object(adapter, "fetchdf", return_value=mock_df) + + data_objects = adapter._get_data_objects( + schema_name=exp.Table(db="test_schema", catalog="test_catalog") + ) + + adapter.fetchdf.assert_called_once() + call_args = adapter.fetchdf.call_args + sql_query_exp = call_args[0][0] + + # _get_data_objects query should distinguish between VIEW and MATERIALIZED_VIEW types + sql_query = sql_query_exp.sql(dialect="databricks") + assert ( + "CASE table_type WHEN 'VIEW' THEN 'view' WHEN 'MATERIALIZED_VIEW' THEN 'materialized_view' ELSE 'table' END AS type" + in sql_query + ) + + objects_by_name = {obj.name: obj for obj in data_objects} + assert objects_by_name["regular_view"].type == DataObjectType.VIEW + assert objects_by_name["mat_view"].type == DataObjectType.MATERIALIZED_VIEW + assert objects_by_name["regular_table"].type == DataObjectType.TABLE + + +def test_drop_data_object_materialized_view_calls_correct_drop(mocker: MockFixture): + adapter = DatabricksEngineAdapter(lambda: None, default_catalog="test_catalog") + + mv_data_object = DataObject( + catalog="test_catalog", + schema="test_schema", + name="test_mv", + type=DataObjectType.MATERIALIZED_VIEW, + ) + + drop_view_mock = mocker.patch.object(adapter, "drop_view") + adapter.drop_data_object(mv_data_object) + + # Ensure drop_view is called with materialized=True + drop_view_mock.assert_called_once_with( + mv_data_object.to_table(), ignore_if_not_exists=True, materialized=True + ) From 5f223bd0e2cbd905fc1bd7a9d63dd40d1f923dec Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:16:05 -0700 Subject: [PATCH 0851/1056] fix: dbt prevent all cycles from tests (#5345) --- sqlmesh/dbt/basemodel.py | 28 ---- sqlmesh/dbt/manifest.py | 4 +- sqlmesh/dbt/test.py | 10 +- tests/dbt/cli/test_list.py | 1 - tests/dbt/cli/test_operations.py | 15 +- tests/dbt/test_model.py | 240 ++++++++++++++++++++++++------- 6 files changed, 215 insertions(+), 83 deletions(-) diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index a68a6ed598..3534b95bc3 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -268,33 +268,6 @@ def remove_tests_with_invalid_refs(self, context: DbtContext) -> None: and all(source in context.sources for source in test.dependencies.sources) ] - def fix_circular_test_refs(self, context: DbtContext) -> None: - """ - Checks for direct circular references between two models and moves the test to the downstream - model if found. This addresses the most common circular reference - relationship tests in both - directions. In the future, we may want to increase coverage by checking for indirect circular references. - - Args: - context: The dbt context this model resides within. - - Returns: - None - """ - for test in self.tests.copy(): - for ref in test.dependencies.refs: - if ref == self.name or ref in self.dependencies.refs: - continue - model = context.refs[ref] - if ( - self.name in model.dependencies.refs - or self.name in model.tests_ref_source_dependencies.refs - ): - logger.info( - f"Moving test '{test.name}' from model '{self.name}' to '{model.name}' to avoid circular reference." - ) - model.tests.append(test) - self.tests.remove(test) - @property def sqlmesh_config_fields(self) -> t.Set[str]: return {"description", "owner", "stamp", "storage_format"} @@ -314,7 +287,6 @@ def sqlmesh_model_kwargs( ) -> t.Dict[str, t.Any]: """Get common sqlmesh model parameters""" self.remove_tests_with_invalid_refs(context) - self.fix_circular_test_refs(context) dependencies = self.dependencies.copy() if dependencies.has_dynamic_var_names: diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 67a1ae7ed3..15377e59dc 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -338,10 +338,12 @@ def _load_models_and_seeds(self) -> None: continue macro_references = _macro_references(self._manifest, node) - tests = ( + all_tests = ( self._tests_by_owner[node.name] + self._tests_by_owner[f"{node.package_name}.{node.name}"] ) + # Only include non-standalone tests (tests that don't reference other models) + tests = [test for test in all_tests if not test.is_standalone] node_config = _node_base_config(node) node_name = node.name diff --git a/sqlmesh/dbt/test.py b/sqlmesh/dbt/test.py index 833df6495f..5c18ff4d81 100644 --- a/sqlmesh/dbt/test.py +++ b/sqlmesh/dbt/test.py @@ -108,7 +108,15 @@ def _lowercase_name(cls, v: str) -> str: @property def is_standalone(self) -> bool: - return not self.model_name + # A test is standalone if: + # 1. It has no model_name (already standalone), OR + # 2. It references other models besides its own model + if not self.model_name: + return True + + # Check if test has references to other models + other_refs = {ref for ref in self.dependencies.refs if ref != self.model_name} + return bool(other_refs) @property def sqlmesh_config_fields(self) -> t.Set[str]: diff --git a/tests/dbt/cli/test_list.py b/tests/dbt/cli/test_list.py index 3701822b4c..1bc22ce87e 100644 --- a/tests/dbt/cli/test_list.py +++ b/tests/dbt/cli/test_list.py @@ -24,7 +24,6 @@ def test_list_select(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Resul assert result.exit_code == 0 assert not result.exception - assert "main.orders" in result.output assert "main.customers" in result.output assert "main.stg_customers" in result.output assert "main.raw_customers" in result.output diff --git a/tests/dbt/cli/test_operations.py b/tests/dbt/cli/test_operations.py index 15051542c6..f8ce239d3b 100644 --- a/tests/dbt/cli/test_operations.py +++ b/tests/dbt/cli/test_operations.py @@ -126,6 +126,7 @@ def test_run_option_mapping(jaffle_shop_duckdb: Path): operations.context.console = console plan = operations.run() + standalone_audit_name = "relationships_orders_customer_id__customer_id__ref_customers_" assert plan.environment.name == "prod" assert console.no_prompts is True assert console.no_diff is True @@ -149,7 +150,9 @@ def test_run_option_mapping(jaffle_shop_duckdb: Path): '"jaffle_shop"."main"."orders"', '"jaffle_shop"."main"."stg_orders"', } - assert {s.name for s in plan.snapshots} == plan.selected_models_to_backfill + assert {s.name for s in plan.snapshots} == ( + plan.selected_models_to_backfill | {standalone_audit_name} + ) plan = operations.run(select=["main.stg_orders+"], exclude=["main.customers"]) assert plan.environment.name == "prod" @@ -163,7 +166,9 @@ def test_run_option_mapping(jaffle_shop_duckdb: Path): '"jaffle_shop"."main"."orders"', '"jaffle_shop"."main"."stg_orders"', } - assert {s.name for s in plan.snapshots} == plan.selected_models_to_backfill + assert {s.name for s in plan.snapshots} == ( + plan.selected_models_to_backfill | {standalone_audit_name} + ) plan = operations.run(exclude=["main.customers"]) assert plan.environment.name == "prod" @@ -175,8 +180,10 @@ def test_run_option_mapping(jaffle_shop_duckdb: Path): assert plan.skip_backfill is False assert plan.selected_models_to_backfill == {k for k in operations.context.snapshots} - { '"jaffle_shop"."main"."customers"' - } - assert {s.name for s in plan.snapshots} == plan.selected_models_to_backfill + } - {standalone_audit_name} + assert {s.name for s in plan.snapshots} == ( + plan.selected_models_to_backfill | {standalone_audit_name} + ) plan = operations.run(empty=True) assert plan.environment.name == "prod" diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index b343d9462b..dc2ebc492b 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -63,57 +63,201 @@ def _create_empty_project() -> t.Tuple[Path, Path]: return _create_empty_project -def test_model_test_circular_references() -> None: - upstream_model = ModelConfig(name="upstream") - downstream_model = ModelConfig(name="downstream", dependencies=Dependencies(refs={"upstream"})) - context = DbtContext(_refs={"upstream": upstream_model, "downstream": downstream_model}) - - # Test and downstream model references - downstream_test = TestConfig( - name="downstream_with_upstream", - sql="", - dependencies=Dependencies(refs={"upstream", "downstream"}), +def test_test_config_is_standalone_behavior() -> None: + """Test that TestConfig.is_standalone correctly identifies tests with cross-model references""" + + # Test with no model_name (should be standalone) + standalone_test = TestConfig( + name="standalone_test", + sql="SELECT 1", + model_name=None, + dependencies=Dependencies(refs={"some_model"}), ) - upstream_test = TestConfig( - name="upstream_with_downstream", - sql="", - dependencies=Dependencies(refs={"upstream", "downstream"}), + assert standalone_test.is_standalone is True + + # Test with only self-reference (should not be standalone) + self_ref_test = TestConfig( + name="self_ref_test", + sql="SELECT * FROM {{ this }}", + model_name="my_model", + dependencies=Dependencies(refs={"my_model"}), ) + assert self_ref_test.is_standalone is False + + # Test with no references (should not be standalone) + no_ref_test = TestConfig( + name="no_ref_test", + sql="SELECT 1", + model_name="my_model", + dependencies=Dependencies(), + ) + assert no_ref_test.is_standalone is False + + # Test with references to other models (should be standalone) + cross_ref_test = TestConfig( + name="cross_ref_test", + sql="SELECT * FROM {{ ref('other_model') }}", + model_name="my_model", + dependencies=Dependencies(refs={"my_model", "other_model"}), + ) + assert cross_ref_test.is_standalone is True + + # Test with only references to other models, no self-reference (should be standalone) + other_only_test = TestConfig( + name="other_only_test", + sql="SELECT * FROM {{ ref('other_model') }}", + model_name="my_model", + dependencies=Dependencies(refs={"other_model"}), + ) + assert other_only_test.is_standalone is True + + +def test_test_to_sqlmesh_creates_correct_audit_type( + dbt_dummy_postgres_config: PostgresConfig, +) -> None: + """Test that TestConfig.to_sqlmesh creates the correct audit type based on is_standalone""" + from sqlmesh.core.audit.definition import StandaloneAudit, ModelAudit + + # Set up models in context + my_model = ModelConfig( + name="my_model", sql="SELECT 1", schema="test_schema", database="test_db", alias="my_model" + ) + other_model = ModelConfig( + name="other_model", + sql="SELECT 2", + schema="test_schema", + database="test_db", + alias="other_model", + ) + context = DbtContext( + _refs={"my_model": my_model, "other_model": other_model}, + _target=dbt_dummy_postgres_config, + ) + + # Test with only self-reference (should create ModelAudit) + self_ref_test = TestConfig( + name="self_ref_test", + sql="SELECT * FROM {{ this }}", + model_name="my_model", + dependencies=Dependencies(refs={"my_model"}), + ) + audit = self_ref_test.to_sqlmesh(context) + assert isinstance(audit, ModelAudit) + assert audit.name == "self_ref_test" + + # Test with references to other models (should create StandaloneAudit) + cross_ref_test = TestConfig( + name="cross_ref_test", + sql="SELECT * FROM {{ ref('other_model') }}", + model_name="my_model", + dependencies=Dependencies(refs={"my_model", "other_model"}), + ) + audit = cross_ref_test.to_sqlmesh(context) + assert isinstance(audit, StandaloneAudit) + assert audit.name == "cross_ref_test" + + # Test with no model_name (should create StandaloneAudit) + standalone_test = TestConfig( + name="standalone_test", + sql="SELECT 1", + model_name=None, + dependencies=Dependencies(), + ) + audit = standalone_test.to_sqlmesh(context) + assert isinstance(audit, StandaloneAudit) + assert audit.name == "standalone_test" + + +@pytest.mark.slow +def test_manifest_filters_standalone_tests_from_models( + tmp_path: Path, create_empty_project +) -> None: + """Integration test that verifies models only contain non-standalone tests after manifest loading.""" + yaml = YAML() + project_dir, model_dir = create_empty_project() + + # Create two models + model1_contents = "SELECT 1 as id" + model1_file = model_dir / "model1.sql" + with open(model1_file, "w", encoding="utf-8") as f: + f.write(model1_contents) + + model2_contents = "SELECT 2 as id" + model2_file = model_dir / "model2.sql" + with open(model2_file, "w", encoding="utf-8") as f: + f.write(model2_contents) + + # Create schema with both standalone and non-standalone tests + schema_yaml = { + "version": 2, + "models": [ + { + "name": "model1", + "columns": [ + { + "name": "id", + "tests": [ + "not_null", # Non-standalone test - only references model1 + { + "relationships": { # Standalone test - references model2 + "to": "ref('model2')", + "field": "id", + } + }, + ], + } + ], + }, + { + "name": "model2", + "columns": [ + {"name": "id", "tests": ["not_null"]} # Non-standalone test + ], + }, + ], + } + + schema_file = model_dir / "schema.yml" + with open(schema_file, "w", encoding="utf-8") as f: + yaml.dump(schema_yaml, f) + + # Load the project through SQLMesh Context + from sqlmesh.core.context import Context + + context = Context(paths=project_dir) - # No circular reference - downstream_model.tests = [downstream_test] - downstream_model.fix_circular_test_refs(context) - assert upstream_model.tests == [] - assert downstream_model.tests == [downstream_test] - - # Upstream model reference in downstream model - downstream_model.tests = [] - upstream_model.tests = [upstream_test] - upstream_model.fix_circular_test_refs(context) - assert upstream_model.tests == [] - assert downstream_model.tests == [upstream_test] - - upstream_model.tests = [upstream_test] - downstream_model.tests = [downstream_test] - upstream_model.fix_circular_test_refs(context) - assert upstream_model.tests == [] - assert downstream_model.tests == [downstream_test, upstream_test] - - downstream_model.fix_circular_test_refs(context) - assert upstream_model.tests == [] - assert downstream_model.tests == [downstream_test, upstream_test] - - # Test only references - upstream_model.tests = [upstream_test] - downstream_model.tests = [downstream_test] - downstream_model.dependencies = Dependencies() - upstream_model.fix_circular_test_refs(context) - assert upstream_model.tests == [] - assert downstream_model.tests == [downstream_test, upstream_test] - - downstream_model.fix_circular_test_refs(context) - assert upstream_model.tests == [] - assert downstream_model.tests == [downstream_test, upstream_test] + model1_snapshot = context.snapshots['"local"."main"."model1"'] + model2_snapshot = context.snapshots['"local"."main"."model2"'] + + # Verify model1 only has non-standalone test in its audits + # Should only have "not_null" test, not the "relationships" test + model1_audit_names = [audit[0] for audit in model1_snapshot.model.audits] + assert len(model1_audit_names) == 1 + assert model1_audit_names[0] == "not_null_model1_id" + + # Verify model2 has its non-standalone test + model2_audit_names = [audit[0] for audit in model2_snapshot.model.audits] + assert len(model2_audit_names) == 1 + assert model2_audit_names[0] == "not_null_model2_id" + + # Verify the standalone test (relationships) exists as a StandaloneAudit + all_non_standalone_audits = [name for name in context._audits] + assert sorted(all_non_standalone_audits) == [ + "not_null_model1_id", + "not_null_model2_id", + ] + + standalone_audits = [name for name in context._standalone_audits] + assert len(standalone_audits) == 1 + assert standalone_audits[0] == "relationships_model1_id__id__ref_model2_" + + plan_builder = context.plan_builder() + dag = plan_builder._build_dag() + assert [x.name for x in dag.sorted] == [ + '"local"."main"."model1"', + '"local"."main"."model2"', + "relationships_model1_id__id__ref_model2_", + ] @pytest.mark.slow From f9b412d4e10f66f0767ac046cb46279d7b4ab109 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Thu, 11 Sep 2025 11:46:29 -0700 Subject: [PATCH 0852/1056] chore(web_common): export components with types (#5347) --- .../src/components/CopyButton/CopyButton.tsx | 31 +++++----------- .../LoadingContainer/LoadingContainer.tsx | 2 +- .../MessageContainer.stories.tsx | 2 +- .../MessageContainer/MessageContainer.tsx | 16 +++++---- .../src/components/ModelName/ModelName.tsx | 3 +- web/common/src/components/Tooltip/Tooltip.tsx | 24 ++++++------- .../src/components/Typography/Description.tsx | 10 +++--- .../src/components/Typography/Headline.tsx | 12 ++++--- .../src/components/Typography/Information.tsx | 24 +++++++------ .../src/components/Typography/Tagline.tsx | 10 +++--- web/common/src/components/Typography/Text.tsx | 10 +++--- .../components/VirtualList/FilterableList.tsx | 22 ++++++------ .../components/VirtualList/VirtualList.tsx | 16 +++++---- web/common/src/hooks/useCopyClipboard.ts | 16 +++++++++ web/common/src/index.ts | 35 ++++++++++++++++++- 15 files changed, 138 insertions(+), 95 deletions(-) create mode 100644 web/common/src/hooks/useCopyClipboard.ts diff --git a/web/common/src/components/CopyButton/CopyButton.tsx b/web/common/src/components/CopyButton/CopyButton.tsx index e6a8bc446d..45aae3d817 100644 --- a/web/common/src/components/CopyButton/CopyButton.tsx +++ b/web/common/src/components/CopyButton/CopyButton.tsx @@ -1,8 +1,7 @@ -import React, { useState } from 'react' +import React from 'react' import { Button, type ButtonProps } from '@/components/Button/Button' - -type TimerID = ReturnType +import { useCopyClipboard } from '@/hooks/useCopyClipboard' export interface CopyButtonProps extends Omit { text: string @@ -25,22 +24,7 @@ export const CopyButton = React.forwardRef( }, ref, ) => { - const [copied, setCopied] = useState(null) - - const copy = (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - - if (copied) { - clearTimeout(copied) - } - - navigator.clipboard.writeText(text).then(() => { - setCopied(setTimeout(() => setCopied(null), delay)) - }) - - onClick?.(e) - } + const [copyToClipboard, isCopied] = useCopyClipboard(delay) return ( ) }, diff --git a/web/common/src/components/LoadingContainer/LoadingContainer.tsx b/web/common/src/components/LoadingContainer/LoadingContainer.tsx index af9250b729..af21ddd04c 100644 --- a/web/common/src/components/LoadingContainer/LoadingContainer.tsx +++ b/web/common/src/components/LoadingContainer/LoadingContainer.tsx @@ -6,7 +6,7 @@ import { LoadingIcon } from './LoadingIcon' export interface LoadingContainerProps extends React.HTMLAttributes { isLoading?: boolean - message?: string + message?: React.ReactNode side?: Side className?: string } diff --git a/web/common/src/components/MessageContainer/MessageContainer.stories.tsx b/web/common/src/components/MessageContainer/MessageContainer.stories.tsx index 554fe4b0cd..395361bea7 100644 --- a/web/common/src/components/MessageContainer/MessageContainer.stories.tsx +++ b/web/common/src/components/MessageContainer/MessageContainer.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite' -import MessageContainer from './MessageContainer' +import { MessageContainer } from './MessageContainer' const meta = { title: 'Components/Containers/MessageContainer', diff --git a/web/common/src/components/MessageContainer/MessageContainer.tsx b/web/common/src/components/MessageContainer/MessageContainer.tsx index 2a1b4783a3..d51213bfaf 100644 --- a/web/common/src/components/MessageContainer/MessageContainer.tsx +++ b/web/common/src/components/MessageContainer/MessageContainer.tsx @@ -2,17 +2,19 @@ import { cn } from '@/utils' import { LoadingContainer } from '../LoadingContainer/LoadingContainer' import { HorizontalContainer } from '../HorizontalContainer/HorizontalContainer' -export default function MessageContainer({ - children, - className, - wrap = false, - isLoading = false, -}: { +export interface MessageContainerProps { children: React.ReactNode className?: string wrap?: boolean isLoading?: boolean -}) { +} + +export function MessageContainer({ + children, + className, + wrap = false, + isLoading = false, +}: MessageContainerProps) { return ( ( return ( {catalog && ( <> @@ -138,6 +138,7 @@ export const ModelName = React.forwardRef( )} -export type TooltipAlign = Extract +export interface TooltipProps { + trigger: React.ReactNode + children: React.ReactNode + side?: 'top' | 'bottom' | 'left' | 'right' + align?: 'center' | 'start' | 'end' + delayDuration?: number + sideOffset?: number + alignOffset?: number + className?: string +} export function Tooltip({ delayDuration = 200, @@ -24,16 +31,7 @@ export function Tooltip({ trigger, children, className, -}: { - trigger: React.ReactNode - children: React.ReactNode - side?: TooltipSide - align?: TooltipAlign - delayDuration?: number - sideOffset?: number - alignOffset?: number - className?: string -}) { +}: TooltipProps) { return ( diff --git a/web/common/src/components/Typography/Description.tsx b/web/common/src/components/Typography/Description.tsx index 62047b4a03..512a216fac 100644 --- a/web/common/src/components/Typography/Description.tsx +++ b/web/common/src/components/Typography/Description.tsx @@ -1,14 +1,16 @@ import { cn } from '@/utils' import React from 'react' +export interface DescriptionProps { + children?: React.ReactNode + className?: string +} + export function Description({ children, className, ...props -}: { - children?: React.ReactNode - className?: string -}) { +}: DescriptionProps) { return (
      ), ...props -}: { - children?: React.ReactNode - className?: string - classNameTooltip?: string - side?: 'right' | 'left' - size?: Size - sideOffset?: number - delayDuration?: number - info?: React.ReactNode - infoIcon?: React.ReactNode -}) { +}: InformationProps) { return (
      { + items: TItem[] + filterOptions?: IFuseOptions + disabled?: boolean + placeholder?: string + autoFocus?: boolean + className?: string + children: (options: TItem[], resetSearch: () => void) => React.ReactNode +} + export function FilterableList({ items, disabled, @@ -16,15 +26,7 @@ export function FilterableList({ filterOptions, className, children, -}: { - items: TItem[] - filterOptions?: IFuseOptions - disabled?: boolean - placeholder?: string - autoFocus?: boolean - className?: string - children: (options: TItem[], resetSearch: () => void) => React.ReactNode -}) { +}: FilterableListProps) { const [search, setSearch] = React.useState('') const fuse = new Fuse(items, filterOptions) diff --git a/web/common/src/components/VirtualList/VirtualList.tsx b/web/common/src/components/VirtualList/VirtualList.tsx index daec45fe16..94e5d93c05 100644 --- a/web/common/src/components/VirtualList/VirtualList.tsx +++ b/web/common/src/components/VirtualList/VirtualList.tsx @@ -6,19 +6,21 @@ import { Button } from '../Button/Button' import { ScrollContainer } from '../ScrollContainer/ScrollContainer' import { VerticalContainer } from '../VerticalContainer/VerticalContainer' +export interface VirtualListProps { + items: TItem[] + estimatedListItemHeight: number + renderListItem: (item: TItem) => React.ReactNode + isSelected?: (item: TItem) => boolean + className?: string +} + export function VirtualList({ items, estimatedListItemHeight, renderListItem, isSelected, className, -}: { - items: TItem[] - estimatedListItemHeight: number - renderListItem: (item: TItem) => React.ReactNode - isSelected?: (item: TItem) => boolean - className?: string -}) { +}: VirtualListProps) { const scrollableAreaRef = React.useRef(null) const [activeItemIndex] = React.useMemo(() => { diff --git a/web/common/src/hooks/useCopyClipboard.ts b/web/common/src/hooks/useCopyClipboard.ts new file mode 100644 index 0000000000..e961d3e094 --- /dev/null +++ b/web/common/src/hooks/useCopyClipboard.ts @@ -0,0 +1,16 @@ +import { useState } from 'react' + +type TimerID = ReturnType + +export function useCopyClipboard( + delay: number = 2000, +): [(text: string) => void, TimerID | null] { + const [isCopied, setIsCopied] = useState(null) + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text) + setIsCopied(setTimeout(() => setIsCopied(null), delay)) + } + + return [copyToClipboard, isCopied] +} diff --git a/web/common/src/index.ts b/web/common/src/index.ts index dce05a9c83..0748a6c78e 100644 --- a/web/common/src/index.ts +++ b/web/common/src/index.ts @@ -25,7 +25,39 @@ export { ModelName, type ModelNameProps, } from '@/components/ModelName/ModelName' -export { Tooltip } from '@/components/Tooltip/Tooltip' +export { Tooltip, type TooltipProps } from '@/components/Tooltip/Tooltip' +export { + VirtualList, + type VirtualListProps, +} from '@/components/VirtualList/VirtualList' +export { + FilterableList, + type FilterableListProps, +} from '@/components/VirtualList/FilterableList' +export { + MessageContainer, + type MessageContainerProps, +} from '@/components/MessageContainer/MessageContainer' +export { Input, type InputProps } from '@/components/Input/Input' +export { + LoadingContainer, + type LoadingContainerProps, +} from '@/components/LoadingContainer/LoadingContainer' +export { Metadata, type MetadataProps } from '@/components/Metadata/Metadata' +export { + Description, + type DescriptionProps, +} from '@/components/Typography/Description' +export { Headline, type HeadlineProps } from '@/components/Typography/Headline' +export { + Information, + type InformationProps, +} from '@/components/Typography/Information' +export { Tagline, type TaglineProps } from '@/components/Typography/Tagline' +export { Text, type TextProps } from '@/components/Typography/Text' + +// Hooks +export { useCopyClipboard } from '@/hooks/useCopyClipboard' // Utils export { cn, truncate } from '@/utils' @@ -40,4 +72,5 @@ export type { LayoutDirection, Shape, Position, + Callback, } from '@/types' From f29a6b2ba929a002a486075e1e9f56487cec1e1a Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 11 Sep 2025 23:59:50 +0300 Subject: [PATCH 0853/1056] Fix: include `warehouse` in dbt `target` attribute dict (#5356) --- sqlmesh/dbt/target.py | 1 + tests/dbt/test_transformation.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/sqlmesh/dbt/target.py b/sqlmesh/dbt/target.py index 82574a044c..f5fd119027 100644 --- a/sqlmesh/dbt/target.py +++ b/sqlmesh/dbt/target.py @@ -49,6 +49,7 @@ "name", "database", "schema_", + "warehouse", } SCHEMA_DIFFER_OVERRIDES = { diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 551c6cc16f..9876edbe38 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1014,6 +1014,18 @@ def test_target_jinja(sushi_test_project: Project): assert context.render("{{ target.path }}") == "None" assert context.render("{{ target.profile_name }}") == "None" + context = DbtContext() + context._target = SnowflakeConfig( + name="target", + schema="test", + database="test", + account="account", + user="user", + password="password", + warehouse="warehouse", + ) + assert context.render("{{ target.warehouse }}") == "warehouse" + @pytest.mark.xdist_group("dbt_manifest") def test_project_name_jinja(sushi_test_project: Project): From 22c044fda390527bc77d0b7551f6e51a6260b27c Mon Sep 17 00:00:00 2001 From: Tori Wei <41123940+toriwei@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:21:21 -0700 Subject: [PATCH 0854/1056] fix: match dbt behavior to ignore partition_by for views (#5359) --- sqlmesh/dbt/model.py | 46 ++++++++++++++++++-------------- tests/dbt/test_transformation.py | 12 +++++++++ 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 58a8ea7f29..124d900c4b 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -510,27 +510,33 @@ def to_sqlmesh( physical_properties: t.Dict[str, t.Any] = {} if self.partition_by: - partitioned_by = [] - if isinstance(self.partition_by, list): - for p in self.partition_by: - try: - partitioned_by.append(d.parse_one(p, dialect=model_dialect)) - except SqlglotError as e: - raise ConfigError( - f"Failed to parse model '{self.canonical_name(context)}' partition_by field '{p}' in '{self.path}': {e}" - ) from e - elif isinstance(self.partition_by, dict): - if context.target.dialect == "bigquery": - partitioned_by.append(self._big_query_partition_by_expr(context)) - else: - logger.warning( - "Ignoring partition_by config for model '%s' targeting %s. The format of the config field is only supported for BigQuery.", - self.name, - context.target.dialect, - ) + if isinstance(kind, ViewKind): + logger.warning( + "Ignoring partition_by config for model '%s'; partition_by is not supported for views.", + self.name, + ) + else: + partitioned_by = [] + if isinstance(self.partition_by, list): + for p in self.partition_by: + try: + partitioned_by.append(d.parse_one(p, dialect=model_dialect)) + except SqlglotError as e: + raise ConfigError( + f"Failed to parse model '{self.canonical_name(context)}' partition_by field '{p}' in '{self.path}': {e}" + ) from e + elif isinstance(self.partition_by, dict): + if context.target.dialect == "bigquery": + partitioned_by.append(self._big_query_partition_by_expr(context)) + else: + logger.warning( + "Ignoring partition_by config for model '%s' targeting %s. The format of the config field is only supported for BigQuery.", + self.name, + context.target.dialect, + ) - if partitioned_by: - optional_kwargs["partitioned_by"] = partitioned_by + if partitioned_by: + optional_kwargs["partitioned_by"] = partitioned_by if self.cluster_by: if isinstance(kind, ViewKind): diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 9876edbe38..22b75abab6 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1683,6 +1683,18 @@ def test_partition_by(sushi_test_project: Project): context.target = DuckDbConfig(name="target", schema="foo") assert model_config.to_sqlmesh(context).partitioned_by == [] + model_config = ModelConfig( + name="model", + alias="model", + schema="test", + package_name="package", + materialized=Materialization.VIEW.value, + unique_key="ds", + partition_by={"field": "ds", "granularity": "month"}, + sql="""SELECT 1 AS one, ds FROM foo""", + ) + assert model_config.to_sqlmesh(context).partitioned_by == [] + @pytest.mark.xdist_group("dbt_manifest") def test_partition_by_none(sushi_test_project: Project): From 3bef91ad962d4c97aeb1de938f86f7f2af788538 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 12 Sep 2025 00:41:05 +0300 Subject: [PATCH 0855/1056] Chore!: bump sqlglot to v27.14.0 (#5360) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2e39219710..6823f7750b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.13.2", + "sqlglot[rs]~=27.14.0", "tenacity", "time-machine", "json-stream" From 85cfe5a5654ffbe66b78425f58bdd5b89b617ab0 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Thu, 11 Sep 2025 15:37:55 -0700 Subject: [PATCH 0856/1056] fix: add missing ref (#5361) --- .../LoadingContainer/LoadingContainer.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/web/common/src/components/LoadingContainer/LoadingContainer.tsx b/web/common/src/components/LoadingContainer/LoadingContainer.tsx index af21ddd04c..4698266e4e 100644 --- a/web/common/src/components/LoadingContainer/LoadingContainer.tsx +++ b/web/common/src/components/LoadingContainer/LoadingContainer.tsx @@ -15,13 +15,16 @@ export const LoadingContainer = React.forwardRef< HTMLDivElement, LoadingContainerProps >( - ({ - isLoading = true, - side = 'left', - message, - children, - className, - }: LoadingContainerProps) => { + ( + { + isLoading = true, + side = 'left', + message, + children, + className, + }: LoadingContainerProps, + ref, + ) => { function renderLoading() { return ( <> @@ -33,6 +36,7 @@ export const LoadingContainer = React.forwardRef< return isLoading ? (
      From 26ebace1f60b0656c39fcaff9ab33b664570c888 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 12 Sep 2025 11:10:39 +1200 Subject: [PATCH 0857/1056] Feat: prevent other processes seeing missing intervals during restatement (#5285) --- sqlmesh/core/console.py | 48 ++++ sqlmesh/core/plan/evaluator.py | 94 +++++-- sqlmesh/core/plan/explainer.py | 79 +++++- sqlmesh/core/plan/stages.py | 38 +-- sqlmesh/core/snapshot/definition.py | 24 +- tests/core/test_integration.py | 416 +++++++++++++++++++++++++++- tests/core/test_plan_stages.py | 353 ++++++++++++++++++++++- 7 files changed, 986 insertions(+), 66 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index af28f75932..3b6cb1ce07 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -551,6 +551,22 @@ def log_skipped_models(self, snapshot_names: t.Set[str]) -> None: def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None: """Display list of models that failed during evaluation to the user.""" + @abc.abstractmethod + def log_models_updated_during_restatement( + self, + snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]], + environment_naming_info: EnvironmentNamingInfo, + default_catalog: t.Optional[str], + ) -> None: + """Display a list of models where new versions got deployed to the specified :environment while we were restating data the old versions + + Args: + snapshots: a list of (snapshot_we_restated, snapshot_it_got_replaced_with_during_restatement) tuples + environment: which environment got updated while we were restating models + environment_naming_info: how snapshots are named in that :environment (for display name purposes) + default_catalog: the configured default catalog (for display name purposes) + """ + @abc.abstractmethod def loading_start(self, message: t.Optional[str] = None) -> uuid.UUID: """Starts loading and returns a unique ID that can be used to stop the loading. Optionally can display a message.""" @@ -771,6 +787,14 @@ def log_skipped_models(self, snapshot_names: t.Set[str]) -> None: def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None: pass + def log_models_updated_during_restatement( + self, + snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]], + environment_naming_info: EnvironmentNamingInfo, + default_catalog: t.Optional[str], + ) -> None: + pass + def log_destructive_change( self, snapshot_name: str, @@ -2225,6 +2249,30 @@ def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None: for node_name, msg in error_messages.items(): self._print(f" [red]{node_name}[/red]\n\n{msg}") + def log_models_updated_during_restatement( + self, + snapshots: t.List[t.Tuple[SnapshotTableInfo, SnapshotTableInfo]], + environment_naming_info: EnvironmentNamingInfo, + default_catalog: t.Optional[str] = None, + ) -> None: + if snapshots: + tree = Tree( + f"[yellow]The following models had new versions deployed while data was being restated:[/yellow]" + ) + + for restated_snapshot, updated_snapshot in snapshots: + display_name = restated_snapshot.display_name( + environment_naming_info, + default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, + dialect=self.dialect, + ) + current_branch = tree.add(display_name) + current_branch.add(f"restated version: '{restated_snapshot.version}'") + current_branch.add(f"currently active version: '{updated_snapshot.version}'") + + self._print(tree) + self._print("") # newline spacer + def log_destructive_change( self, snapshot_name: str, diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 79053e018b..03ecb770bf 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -22,7 +22,7 @@ from sqlmesh.core.console import Console, get_console from sqlmesh.core.environment import EnvironmentNamingInfo, execute_environment_statements from sqlmesh.core.macros import RuntimeStage -from sqlmesh.core.snapshot.definition import to_view_mapping +from sqlmesh.core.snapshot.definition import to_view_mapping, SnapshotTableInfo from sqlmesh.core.plan import stages from sqlmesh.core.plan.definition import EvaluatablePlan from sqlmesh.core.scheduler import Scheduler @@ -40,7 +40,7 @@ from sqlmesh.core.plan.common import identify_restatement_intervals_across_snapshot_versions from sqlmesh.utils import CorrelationId from sqlmesh.utils.concurrency import NodeExecutionFailedError -from sqlmesh.utils.errors import PlanError, SQLMeshError +from sqlmesh.utils.errors import PlanError, ConflictingPlanError, SQLMeshError from sqlmesh.utils.date import now, to_timestamp logger = logging.getLogger(__name__) @@ -287,34 +287,78 @@ def visit_audit_only_run_stage( def visit_restatement_stage( self, stage: stages.RestatementStage, plan: EvaluatablePlan ) -> None: - snapshot_intervals_to_restate = { - (s.id_and_version, i) for s, i in stage.snapshot_intervals.items() - } - - # Restating intervals on prod plans should mean that the intervals are cleared across - # all environments, not just the version currently in prod - # This ensures that work done in dev environments can still be promoted to prod - # by forcing dev environments to re-run intervals that changed in prod + # Restating intervals on prod plans means that once the data for the intervals being restated has been backfilled + # (which happens in the backfill stage) then we need to clear those intervals *from state* across all other environments. + # + # This ensures that work done in dev environments can still be promoted to prod by forcing dev environments to + # re-run intervals that changed in prod (because after this stage runs they are cleared from state and thus show as missing) + # + # It also means that any new dev environments created while this restatement plan was running also get the + # correct intervals cleared because we look up matching snapshots as at right now and not as at the time the plan + # was created, which could have been several hours ago if there was a lot of data to restate. # # Without this rule, its possible that promoting a dev table to prod will introduce old data to prod - snapshot_intervals_to_restate.update( - { - (s.snapshot, s.interval) - for s in identify_restatement_intervals_across_snapshot_versions( - state_reader=self.state_sync, - prod_restatements=plan.restatements, - disable_restatement_models=plan.disabled_restatement_models, - loaded_snapshots={s.snapshot_id: s for s in stage.all_snapshots.values()}, - current_ts=to_timestamp(plan.execution_time or now()), - ).values() - } - ) - self.state_sync.remove_intervals( - snapshot_intervals=list(snapshot_intervals_to_restate), - remove_shared_versions=plan.is_prod, + intervals_to_clear = identify_restatement_intervals_across_snapshot_versions( + state_reader=self.state_sync, + prod_restatements=plan.restatements, + disable_restatement_models=plan.disabled_restatement_models, + loaded_snapshots={s.snapshot_id: s for s in stage.all_snapshots.values()}, + current_ts=to_timestamp(plan.execution_time or now()), ) + if not intervals_to_clear: + # Nothing to do + return + + # While the restatements were being processed, did any of the snapshots being restated get new versions deployed? + # If they did, they will not reflect the data that just got restated, so we need to notify the user + deployed_during_restatement: t.Dict[ + str, t.Tuple[SnapshotTableInfo, SnapshotTableInfo] + ] = {} # tuple of (restated_snapshot, current_prod_snapshot) + + if deployed_env := self.state_sync.get_environment(plan.environment.name): + promoted_snapshots_by_name = {s.name: s for s in deployed_env.snapshots} + + for name in plan.restatements: + snapshot = stage.all_snapshots[name] + version = snapshot.table_info.version + if ( + prod_snapshot := promoted_snapshots_by_name.get(name) + ) and prod_snapshot.version != version: + deployed_during_restatement[name] = ( + snapshot.table_info, + prod_snapshot.table_info, + ) + + # we need to *not* clear the intervals on the snapshots where new versions were deployed while the restatement was running in order to prevent + # subsequent plans from having unexpected intervals to backfill. + # we instead list the affected models and abort the plan with an error so the user can decide what to do + # (either re-attempt the restatement plan or leave things as they are) + filtered_intervals_to_clear = [ + (s.snapshot, s.interval) + for s in intervals_to_clear.values() + if s.snapshot.name not in deployed_during_restatement + ] + + if filtered_intervals_to_clear: + # We still clear intervals in other envs for models that were successfully restated without having new versions promoted during restatement + self.state_sync.remove_intervals( + snapshot_intervals=filtered_intervals_to_clear, + remove_shared_versions=plan.is_prod, + ) + + if deployed_env and deployed_during_restatement: + self.console.log_models_updated_during_restatement( + list(deployed_during_restatement.values()), + plan.environment.naming_info, + self.default_catalog, + ) + raise ConflictingPlanError( + f"Another plan ({deployed_env.summary.plan_id}) deployed new versions of {len(deployed_during_restatement)} models in the target environment '{plan.environment.name}' while they were being restated by this plan.\n" + "Please re-apply your plan if these new versions should be restated." + ) + def visit_environment_record_update_stage( self, stage: stages.EnvironmentRecordUpdateStage, plan: EvaluatablePlan ) -> None: diff --git a/sqlmesh/core/plan/explainer.py b/sqlmesh/core/plan/explainer.py index ee829aeac1..b722d00d58 100644 --- a/sqlmesh/core/plan/explainer.py +++ b/sqlmesh/core/plan/explainer.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import abc import typing as t import logging +from dataclasses import dataclass from rich.console import Console as RichConsole from rich.tree import Tree @@ -8,15 +11,17 @@ from sqlmesh.core import constants as c from sqlmesh.core.console import Console, TerminalConsole, get_console from sqlmesh.core.environment import EnvironmentNamingInfo +from sqlmesh.core.plan.common import ( + SnapshotIntervalClearRequest, + identify_restatement_intervals_across_snapshot_versions, +) from sqlmesh.core.plan.definition import EvaluatablePlan, SnapshotIntervals from sqlmesh.core.plan import stages from sqlmesh.core.plan.evaluator import ( PlanEvaluator, ) from sqlmesh.core.state_sync import StateReader -from sqlmesh.core.snapshot.definition import ( - SnapshotInfoMixin, -) +from sqlmesh.core.snapshot.definition import SnapshotInfoMixin, SnapshotIdAndVersion from sqlmesh.utils import Verbosity, rich as srich, to_snake_case from sqlmesh.utils.date import to_ts from sqlmesh.utils.errors import SQLMeshError @@ -45,6 +50,15 @@ def evaluate( explainer_console = _get_explainer_console( self.console, plan.environment, self.default_catalog ) + + # add extra metadata that's only needed at this point for better --explain output + plan_stages = [ + ExplainableRestatementStage.from_restatement_stage(stage, self.state_reader, plan) + if isinstance(stage, stages.RestatementStage) + else stage + for stage in plan_stages + ] + explainer_console.explain(plan_stages) @@ -54,6 +68,38 @@ def explain(self, stages: t.List[stages.PlanStage]) -> None: pass +@dataclass +class ExplainableRestatementStage(stages.RestatementStage): + """ + This brings forward some calculations that would usually be done in the evaluator so the user can be given a better indication + of what might happen when they ask for the plan to be explained + """ + + snapshot_intervals_to_clear: t.Dict[str, SnapshotIntervalClearRequest] + """Which snapshots from other environments would have intervals cleared as part of restatement, keyed by name""" + + @classmethod + def from_restatement_stage( + cls: t.Type[ExplainableRestatementStage], + stage: stages.RestatementStage, + state_reader: StateReader, + plan: EvaluatablePlan, + ) -> ExplainableRestatementStage: + all_restatement_intervals = identify_restatement_intervals_across_snapshot_versions( + state_reader=state_reader, + prod_restatements=plan.restatements, + disable_restatement_models=plan.disabled_restatement_models, + loaded_snapshots={s.snapshot_id: s for s in stage.all_snapshots.values()}, + ) + + return cls( + snapshot_intervals_to_clear={ + s.snapshot.name: s for s in all_restatement_intervals.values() + }, + all_snapshots=stage.all_snapshots, + ) + + MAX_TREE_LENGTH = 10 @@ -146,11 +192,22 @@ def visit_audit_only_run_stage(self, stage: stages.AuditOnlyRunStage) -> Tree: tree.add(display_name) return tree - def visit_restatement_stage(self, stage: stages.RestatementStage) -> Tree: + def visit_explainable_restatement_stage(self, stage: ExplainableRestatementStage) -> Tree: + return self.visit_restatement_stage(stage) + + def visit_restatement_stage( + self, stage: t.Union[ExplainableRestatementStage, stages.RestatementStage] + ) -> Tree: tree = Tree("[bold]Invalidate data intervals as part of restatement[/bold]") - for snapshot_table_info, interval in stage.snapshot_intervals.items(): - display_name = self._display_name(snapshot_table_info) - tree.add(f"{display_name} [{to_ts(interval[0])} - {to_ts(interval[1])}]") + + if isinstance(stage, ExplainableRestatementStage) and ( + snapshot_intervals := stage.snapshot_intervals_to_clear + ): + for clear_request in snapshot_intervals.values(): + display_name = self._display_name(clear_request.snapshot) + interval = clear_request.interval + tree.add(f"{display_name} [{to_ts(interval[0])} - {to_ts(interval[1])}]") + return tree def visit_backfill_stage(self, stage: stages.BackfillStage) -> Tree: @@ -265,12 +322,14 @@ def visit_finalize_environment_stage( def _display_name( self, - snapshot: SnapshotInfoMixin, + snapshot: t.Union[SnapshotInfoMixin, SnapshotIdAndVersion], environment_naming_info: t.Optional[EnvironmentNamingInfo] = None, ) -> str: return snapshot.display_name( - environment_naming_info or self.environment_naming_info, - self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, + environment_naming_info=environment_naming_info or self.environment_naming_info, + default_catalog=self.default_catalog + if self.verbosity < Verbosity.VERY_VERBOSE + else None, dialect=self.dialect, ) diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 91c8c6ff14..0d829a6739 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -12,7 +12,6 @@ Snapshot, SnapshotTableInfo, SnapshotId, - Interval, ) @@ -98,14 +97,19 @@ class AuditOnlyRunStage: @dataclass class RestatementStage: - """Restate intervals for given snapshots. + """Clear intervals from state for snapshots in *other* environments, when restatements are requested in prod. + + This stage is effectively a "marker" stage to trigger the plan evaluator to perform the "clear intervals" logic after the BackfillStage has completed. + The "clear intervals" logic is executed just-in-time using the latest state available in order to pick up new snapshots that may have + been created while the BackfillStage was running, which is why we do not build a list of snapshots to clear at plan time and defer to evaluation time. + + Note that this stage is only present on `prod` plans because dev plans do not need to worry about clearing intervals in other environments. Args: - snapshot_intervals: Intervals to restate. - all_snapshots: All snapshots in the plan by name. + all_snapshots: All snapshots in the plan by name. Note that this does not include the snapshots from other environments that will get their + intervals cleared, it's included here as an optimization to prevent having to re-fetch the current plan's snapshots """ - snapshot_intervals: t.Dict[SnapshotTableInfo, Interval] all_snapshots: t.Dict[str, Snapshot] @@ -321,10 +325,6 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: if audit_only_snapshots: stages.append(AuditOnlyRunStage(snapshots=list(audit_only_snapshots.values()))) - restatement_stage = self._get_restatement_stage(plan, snapshots_by_name) - if restatement_stage: - stages.append(restatement_stage) - if missing_intervals_before_promote: stages.append( BackfillStage( @@ -349,6 +349,15 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: ) ) + # note: "restatement stage" (which is clearing intervals in state - not actually performing the restatements, that's the backfill stage) + # needs to come *after* the backfill stage so that at no time do other plans / runs see empty prod intervals and compete with this plan to try to fill them. + # in addition, when we update intervals in state, we only clear intervals from dev snapshots to force dev models to be backfilled based on the new prod data. + # we can leave prod intervals alone because by the time this plan finishes, the intervals in state have not actually changed, since restatement replaces + # data for existing intervals and does not produce new ones + restatement_stage = self._get_restatement_stage(plan, snapshots_by_name) + if restatement_stage: + stages.append(restatement_stage) + stages.append( EnvironmentRecordUpdateStage( no_gaps_snapshot_names={s.name for s in before_promote_snapshots} @@ -443,15 +452,12 @@ def _get_after_all_stage( def _get_restatement_stage( self, plan: EvaluatablePlan, snapshots_by_name: t.Dict[str, Snapshot] ) -> t.Optional[RestatementStage]: - snapshot_intervals_to_restate = {} - for name, interval in plan.restatements.items(): - restated_snapshot = snapshots_by_name[name] - restated_snapshot.remove_interval(interval) - snapshot_intervals_to_restate[restated_snapshot.table_info] = interval - if not snapshot_intervals_to_restate or plan.is_dev: + if not plan.restatements or plan.is_dev: + # The RestatementStage to clear intervals from state across all environments is not needed for plans against dev, only prod return None + return RestatementStage( - snapshot_intervals=snapshot_intervals_to_restate, all_snapshots=snapshots_by_name + all_snapshots=snapshots_by_name, ) def _get_physical_layer_update_stage( diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index c17e94be10..9522366721 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -638,6 +638,16 @@ def dev_version(self) -> str: def model_kind_name(self) -> t.Optional[ModelKindName]: return self.kind_name_ + def display_name( + self, + environment_naming_info: EnvironmentNamingInfo, + default_catalog: t.Optional[str], + dialect: DialectType = None, + ) -> str: + return model_display_name( + self.name, environment_naming_info, default_catalog, dialect=dialect + ) + class Snapshot(PydanticModel, SnapshotInfoMixin): """A snapshot represents a node at a certain point in time. @@ -1788,7 +1798,19 @@ def display_name( """ if snapshot_info_like.is_audit: return snapshot_info_like.name - view_name = exp.to_table(snapshot_info_like.name) + + return model_display_name( + snapshot_info_like.name, environment_naming_info, default_catalog, dialect + ) + + +def model_display_name( + node_name: str, + environment_naming_info: EnvironmentNamingInfo, + default_catalog: t.Optional[str], + dialect: DialectType = None, +) -> str: + view_name = exp.to_table(node_name) catalog = ( None diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index ef7c59ea7d..0fad472cd5 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -14,7 +14,7 @@ import pytest from pytest import MonkeyPatch from pathlib import Path -from sqlmesh.core.console import set_console, get_console, TerminalConsole +from sqlmesh.core.console import set_console, get_console, TerminalConsole, CaptureTerminalConsole from sqlmesh.core.config.naming import NameInferenceConfig from sqlmesh.core.model.common import ParsableSql from sqlmesh.utils.concurrency import NodeExecutionFailedError @@ -24,7 +24,9 @@ from sqlglot.expressions import DataType import re from IPython.utils.capture import capture_output - +from concurrent.futures import ThreadPoolExecutor, TimeoutError +import time +import queue from sqlmesh import CustomMaterialization from sqlmesh.cli.project_init import init_example_project @@ -72,7 +74,13 @@ SnapshotTableInfo, ) from sqlmesh.utils.date import TimeLike, now, to_date, to_datetime, to_timestamp -from sqlmesh.utils.errors import NoChangesPlanError, SQLMeshError, PlanError, ConfigError +from sqlmesh.utils.errors import ( + NoChangesPlanError, + SQLMeshError, + PlanError, + ConfigError, + ConflictingPlanError, +) from sqlmesh.utils.pydantic import validate_string from tests.conftest import DuckDBMetadata, SushiDataValidator from sqlmesh.utils import CorrelationId @@ -10181,3 +10189,405 @@ def test_incremental_by_time_model_ignore_additive_change_unit_test(tmp_path: Pa assert test_result.testsRun == len(test_result.successes) context.close() + + +def test_restatement_plan_interval_external_visibility(tmp_path: Path): + """ + Scenario: + - `prod` environment exists, models A <- B + - `dev` environment created, models A <- B(dev) <- C (dev) + - Restatement plan is triggered against `prod` for model A + - During restatement, a new dev environment `dev_2` is created with a new version of B(dev_2) + + Outcome: + - At no point are the prod_intervals considered "missing" from state for A + - The intervals for B(dev) and C(dev) are cleared + - The intervals for B(dev_2) are also cleared even though the environment didnt exist at the time the plan was started, + because they are based on the data from a partially restated version of A + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + lock_file_path = tmp_path / "test.lock" # python model blocks while this file is present + + evaluation_lock_file_path = ( + tmp_path / "evaluation.lock" + ) # python model creates this file if it's in the wait loop and deletes it once done + + # Note: to make execution block so we can test stuff, we use a Python model that blocks until it no longer detects the presence of a file + (models_dir / "model_a.py").write_text(f""" +from sqlmesh.core.model import model +from sqlmesh.core.macros import MacroEvaluator + +@model( + "test.model_a", + is_sql=True, + kind="FULL" +) +def entrypoint(evaluator: MacroEvaluator) -> str: + from pathlib import Path + import time + + if evaluator.runtime_stage == 'evaluating': + while True: + if Path("{str(lock_file_path)}").exists(): + Path("{str(evaluation_lock_file_path)}").touch() + print("lock exists; sleeping") + time.sleep(2) + else: + Path("{str(evaluation_lock_file_path)}").unlink(missing_ok=True) + break + + return "select 'model_a' as m" +""") + + (models_dir / "model_b.sql").write_text(""" + MODEL ( + name test.model_b, + kind FULL + ); + + select a.m as m, 'model_b' as mb from test.model_a as a + """) + + config = Config( + gateways={ + "": GatewayConfig( + connection=DuckDBConnectionConfig(database=str(tmp_path / "db.db")), + state_connection=DuckDBConnectionConfig(database=str(tmp_path / "state.db")), + ) + }, + model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01"), + ) + ctx = Context(paths=[tmp_path], config=config) + + ctx.plan(environment="prod", auto_apply=True) + + assert len(ctx.snapshots) == 2 + assert all(s.intervals for s in ctx.snapshots.values()) + + prod_model_a_snapshot_id = ctx.snapshots['"db"."test"."model_a"'].snapshot_id + prod_model_b_snapshot_id = ctx.snapshots['"db"."test"."model_b"'].snapshot_id + + # dev models + # new version of B + (models_dir / "model_b.sql").write_text(""" + MODEL ( + name test.model_b, + kind FULL + ); + + select a.m as m, 'model_b' as mb, 'dev' as dev_version from test.model_a as a + """) + + # add C + (models_dir / "model_c.sql").write_text(""" + MODEL ( + name test.model_c, + kind FULL + ); + + select b.*, 'model_c' as mc from test.model_b as b + """) + + ctx.load() + ctx.plan(environment="dev", auto_apply=True) + + dev_model_b_snapshot_id = ctx.snapshots['"db"."test"."model_b"'].snapshot_id + dev_model_c_snapshot_id = ctx.snapshots['"db"."test"."model_c"'].snapshot_id + + assert dev_model_b_snapshot_id != prod_model_b_snapshot_id + + # now, we restate A in prod but touch the lockfile so it hangs during evaluation + # we also have to do it in its own thread due to the hang + lock_file_path.touch() + + def _run_restatement_plan(tmp_path: Path, config: Config, q: queue.Queue): + q.put("thread_started") + + # give this thread its own Context object to prevent segfaulting the Python interpreter + restatement_ctx = Context(paths=[tmp_path], config=config) + + # dev2 not present before the restatement plan starts + assert restatement_ctx.state_sync.get_environment("dev2") is None + + q.put("plan_started") + plan = restatement_ctx.plan( + environment="prod", restate_models=['"db"."test"."model_a"'], auto_apply=True + ) + q.put("plan_completed") + + # dev2 was created during the restatement plan + assert restatement_ctx.state_sync.get_environment("dev2") is not None + + return plan + + executor = ThreadPoolExecutor() + q: queue.Queue = queue.Queue() + restatement_plan_future = executor.submit(_run_restatement_plan, tmp_path, config, q) + assert q.get() == "thread_started" + + try: + if e := restatement_plan_future.exception(timeout=1): + # abort early if the plan thread threw an exception + raise e + except TimeoutError: + # that's ok, we dont actually expect the plan to have finished in 1 second + pass + + # while that restatement is running, we can simulate another process and check that it sees no empty intervals + assert q.get() == "plan_started" + + # dont check for potentially missing intervals until the plan is in the evaluation loop + attempts = 0 + while not evaluation_lock_file_path.exists(): + time.sleep(2) + attempts += 1 + if attempts > 10: + raise ValueError("Gave up waiting for evaluation loop") + + ctx.clear_caches() # get rid of the file cache so that data is re-fetched from state + prod_models_from_state = ctx.state_sync.get_snapshots( + snapshot_ids=[prod_model_a_snapshot_id, prod_model_b_snapshot_id] + ) + + # prod intervals should be present still + assert all(m.intervals for m in prod_models_from_state.values()) + + # so should dev intervals since prod restatement is still running + assert all(m.intervals for m in ctx.snapshots.values()) + + # now, lets create a new dev environment "dev2", while the prod restatement plan is still running, + # that changes model_b while still being based on the original version of model_a + (models_dir / "model_b.sql").write_text(""" + MODEL ( + name test.model_b, + kind FULL + ); + + select a.m as m, 'model_b' as mb, 'dev2' as dev_version from test.model_a as a + """) + ctx.load() + ctx.plan(environment="dev2", auto_apply=True) + + dev2_model_b_snapshot_id = ctx.snapshots['"db"."test"."model_b"'].snapshot_id + assert dev2_model_b_snapshot_id != dev_model_b_snapshot_id + assert dev2_model_b_snapshot_id != prod_model_b_snapshot_id + + # as at this point, everything still has intervals + ctx.clear_caches() + assert all( + s.intervals + for s in ctx.state_sync.get_snapshots( + snapshot_ids=[ + prod_model_a_snapshot_id, + prod_model_b_snapshot_id, + dev_model_b_snapshot_id, + dev_model_c_snapshot_id, + dev2_model_b_snapshot_id, + ] + ).values() + ) + + # now, we finally let that restatement plan complete + # first, verify it's still blocked where it should be + assert not restatement_plan_future.done() + + lock_file_path.unlink() # remove lock file, plan should be able to proceed now + + if e := restatement_plan_future.exception(): # blocks until future complete + raise e + + assert restatement_plan_future.result() + assert q.get() == "plan_completed" + + ctx.clear_caches() + + # check that intervals in prod are present + assert all( + s.intervals + for s in ctx.state_sync.get_snapshots( + snapshot_ids=[ + prod_model_a_snapshot_id, + prod_model_b_snapshot_id, + ] + ).values() + ) + + # check that intervals in dev have been cleared, including the dev2 env that + # was created after the restatement plan started + assert all( + not s.intervals + for s in ctx.state_sync.get_snapshots( + snapshot_ids=[ + dev_model_b_snapshot_id, + dev_model_c_snapshot_id, + dev2_model_b_snapshot_id, + ] + ).values() + ) + + executor.shutdown() + + +def test_restatement_plan_detects_prod_deployment_during_restatement(tmp_path: Path): + """ + Scenario: + - `prod` environment exists, model A + - `dev` environment created, model A(dev) + - Restatement plan is triggered against `prod` for model A + - During restatement, someone else deploys A(dev) to prod, replacing the model that is currently being restated. + + Outcome: + - The deployment plan for dev -> prod should succeed in deploying the new version of A + - The prod restatement plan should fail with a ConflictingPlanError and warn about the model that got updated while undergoing restatement + - The new version of A should have no intervals cleared. The user needs to rerun the restatement if the intervals should still be cleared + """ + orig_console = get_console() + console = CaptureTerminalConsole() + set_console(console) + + models_dir = tmp_path / "models" + models_dir.mkdir() + + lock_file_path = tmp_path / "test.lock" # python model blocks while this file is present + + evaluation_lock_file_path = ( + tmp_path / "evaluation.lock" + ) # python model creates this file if it's in the wait loop and deletes it once done + + # Note: to make execution block so we can test stuff, we use a Python model that blocks until it no longer detects the presence of a file + (models_dir / "model_a.py").write_text(f""" +from sqlmesh.core.model import model +from sqlmesh.core.macros import MacroEvaluator + +@model( + "test.model_a", + is_sql=True, + kind="FULL" +) +def entrypoint(evaluator: MacroEvaluator) -> str: + from pathlib import Path + import time + + if evaluator.runtime_stage == 'evaluating': + while True: + if Path("{str(lock_file_path)}").exists(): + Path("{str(evaluation_lock_file_path)}").touch() + print("lock exists; sleeping") + time.sleep(2) + else: + Path("{str(evaluation_lock_file_path)}").unlink(missing_ok=True) + break + + return "select 'model_a' as m" +""") + + config = Config( + gateways={ + "": GatewayConfig( + connection=DuckDBConnectionConfig(database=str(tmp_path / "db.db")), + state_connection=DuckDBConnectionConfig(database=str(tmp_path / "state.db")), + ) + }, + model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01"), + ) + ctx = Context(paths=[tmp_path], config=config) + + # create prod + ctx.plan(environment="prod", auto_apply=True) + original_prod = ctx.state_sync.get_environment("prod") + assert original_prod + + # update model_a for dev + (models_dir / "model_a.py").unlink() + (models_dir / "model_a.sql").write_text(""" + MODEL ( + name test.model_a, + kind FULL + ); + + select 1 as changed + """) + + # create dev + ctx.load() + plan = ctx.plan(environment="dev", auto_apply=True) + assert len(plan.modified_snapshots) == 1 + new_model_a_snapshot_id = list(plan.modified_snapshots)[0] + + # now, trigger a prod restatement plan in a different thread and block it to simulate a long restatement + def _run_restatement_plan(tmp_path: Path, config: Config, q: queue.Queue): + q.put("thread_started") + + # give this thread its own Context object to prevent segfaulting the Python interpreter + restatement_ctx = Context(paths=[tmp_path], config=config) + + # ensure dev is present before the restatement plan starts + assert restatement_ctx.state_sync.get_environment("dev") is not None + + q.put("plan_started") + expected_error = None + try: + restatement_ctx.plan( + environment="prod", restate_models=['"db"."test"."model_a"'], auto_apply=True + ) + except ConflictingPlanError as e: + expected_error = e + + q.put("plan_completed") + return expected_error + + executor = ThreadPoolExecutor() + q: queue.Queue = queue.Queue() + lock_file_path.touch() + + restatement_plan_future = executor.submit(_run_restatement_plan, tmp_path, config, q) + restatement_plan_future.add_done_callback(lambda _: executor.shutdown()) + + assert q.get() == "thread_started" + + try: + if e := restatement_plan_future.exception(timeout=1): + # abort early if the plan thread threw an exception + raise e + except TimeoutError: + # that's ok, we dont actually expect the plan to have finished in 1 second + pass + + assert q.get() == "plan_started" + + # ok, now the prod restatement plan is running, let's deploy dev to prod + ctx.plan(environment="prod", auto_apply=True) + + new_prod = ctx.state_sync.get_environment("prod") + assert new_prod + assert new_prod.plan_id != original_prod.plan_id + assert new_prod.previous_plan_id == original_prod.plan_id + + # new prod is deployed but restatement plan is still running + assert not restatement_plan_future.done() + + # allow restatement plan to complete + lock_file_path.unlink() + + plan_error = restatement_plan_future.result() + assert isinstance(plan_error, ConflictingPlanError) + assert "please re-apply your plan" in repr(plan_error).lower() + + output = " ".join(re.split("\s+", console.captured_output, flags=re.UNICODE)) + assert ( + f"The following models had new versions deployed while data was being restated: └── test.model_a" + in output + ) + + # check that no intervals have been cleared from the model_a currently in prod + model_a = ctx.state_sync.get_snapshots(snapshot_ids=[new_model_a_snapshot_id])[ + new_model_a_snapshot_id + ] + assert isinstance(model_a.node, SqlModel) + assert model_a.node.render_query_or_raise().sql() == 'SELECT 1 AS "changed"' + assert len(model_a.intervals) + + set_console(orig_console) diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index 744c7d18bf..4ada7d458d 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -6,6 +6,7 @@ from sqlmesh.core.config import EnvironmentSuffixTarget from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.model import SqlModel, ModelKindName +from sqlmesh.core.plan.common import SnapshotIntervalClearRequest from sqlmesh.core.plan.definition import EvaluatablePlan from sqlmesh.core.plan.stages import ( build_plan_stages, @@ -23,11 +24,13 @@ FinalizeEnvironmentStage, UnpauseStage, ) +from sqlmesh.core.plan.explainer import ExplainableRestatementStage from sqlmesh.core.snapshot.definition import ( SnapshotChangeCategory, DeployabilityIndex, Snapshot, SnapshotId, + SnapshotIdLike, ) from sqlmesh.core.state_sync import StateReader from sqlmesh.core.environment import Environment, EnvironmentStatements @@ -499,15 +502,29 @@ def test_build_plan_stages_basic_no_backfill( assert isinstance(stages[7], FinalizeEnvironmentStage) -def test_build_plan_stages_restatement( +def test_build_plan_stages_restatement_prod_only( snapshot_a: Snapshot, snapshot_b: Snapshot, mocker: MockerFixture ) -> None: + """ + Scenario: + - Prod restatement triggered in a project with no dev environments + + Expected Outcome: + - Plan still contains a RestatementStage in case a dev environment was + created during restatement + """ + # Mock state reader to return existing snapshots and environment state_reader = mocker.Mock(spec=StateReader) state_reader.get_snapshots.return_value = { snapshot_a.snapshot_id: snapshot_a, snapshot_b.snapshot_id: snapshot_b, } + state_reader.get_snapshots_by_names.return_value = { + snapshot_a.id_and_version, + snapshot_b.id_and_version, + } + existing_environment = Environment( name="prod", snapshots=[snapshot_a.table_info, snapshot_b.table_info], @@ -518,7 +535,9 @@ def test_build_plan_stages_restatement( promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], finalized_ts=to_timestamp("2023-01-02"), ) + state_reader.get_environment.return_value = existing_environment + state_reader.get_environments_summary.return_value = [existing_environment.summary] environment = Environment( name="prod", @@ -577,17 +596,164 @@ def test_build_plan_stages_restatement( snapshot_b.snapshot_id, } - # Verify RestatementStage - restatement_stage = stages[1] + # Verify BackfillStage + backfill_stage = stages[1] + assert isinstance(backfill_stage, BackfillStage) + assert len(backfill_stage.snapshot_to_intervals) == 2 + assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() + expected_backfill_interval = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] + for intervals in backfill_stage.snapshot_to_intervals.values(): + assert intervals == expected_backfill_interval + + # Verify RestatementStage exists but is empty + restatement_stage = stages[2] assert isinstance(restatement_stage, RestatementStage) - assert len(restatement_stage.snapshot_intervals) == 2 - expected_interval = (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")) - for snapshot_info, interval in restatement_stage.snapshot_intervals.items(): - assert interval == expected_interval - assert snapshot_info.name in ('"a"', '"b"') + restatement_stage = ExplainableRestatementStage.from_restatement_stage( + restatement_stage, state_reader, plan + ) + assert not restatement_stage.snapshot_intervals_to_clear + + # Verify EnvironmentRecordUpdateStage + assert isinstance(stages[3], EnvironmentRecordUpdateStage) + + # Verify FinalizeEnvironmentStage + assert isinstance(stages[4], FinalizeEnvironmentStage) + + +def test_build_plan_stages_restatement_prod_identifies_dev_intervals( + snapshot_a: Snapshot, + snapshot_b: Snapshot, + make_snapshot: t.Callable[..., Snapshot], + mocker: MockerFixture, +) -> None: + """ + Scenario: + - Prod restatement triggered in a project with a dev environment + - The dev environment contains a different physical version of the affected model + + Expected Outcome: + - Plan contains a RestatementStage that highlights the affected dev version + """ + # Dev version of snapshot_a, same name but different version + snapshot_a_dev = make_snapshot( + SqlModel( + name="a", + query=parse_one("select 1, changed, ds"), + kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"), + ) + ) + snapshot_a_dev.categorize_as(SnapshotChangeCategory.BREAKING) + assert snapshot_a_dev.snapshot_id != snapshot_a.snapshot_id + assert snapshot_a_dev.table_info != snapshot_a.table_info + + # Mock state reader to return existing snapshots and environment + state_reader = mocker.Mock(spec=StateReader) + snapshots_in_state = { + snapshot_a.snapshot_id: snapshot_a, + snapshot_b.snapshot_id: snapshot_b, + snapshot_a_dev.snapshot_id: snapshot_a_dev, + } + + def _get_snapshots(snapshot_ids: t.Iterable[SnapshotIdLike]): + return { + k: v + for k, v in snapshots_in_state.items() + if k in {s.snapshot_id for s in snapshot_ids} + } + + state_reader.get_snapshots.side_effect = _get_snapshots + state_reader.get_snapshots_by_names.return_value = set() + + existing_prod_environment = Environment( + name="prod", + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + + # dev has new version of snapshot_a but same version of snapshot_b + existing_dev_environment = Environment( + name="dev", + snapshots=[snapshot_a_dev.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a_dev.snapshot_id, snapshot_b.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + + state_reader.get_environment.side_effect = ( + lambda name: existing_dev_environment if name == "dev" else existing_prod_environment + ) + state_reader.get_environments_summary.return_value = [ + existing_prod_environment.summary, + existing_dev_environment.summary, + ] + + environment = Environment( + name="prod", + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id="previous_plan", + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + ) + + # Create evaluatable plan with restatements + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[], # No new snapshots + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={ + '"a"': (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + '"b"': (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + }, + is_dev=False, + allow_destructive_models=set(), + allow_additive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + ignore_cron=False, + directly_modified_snapshots=[], # No changes + indirectly_modified_snapshots={}, # No changes + metadata_updated_snapshots=[], + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 5 + + # Verify PhysicalLayerSchemaCreationStage + physical_stage = stages[0] + assert isinstance(physical_stage, PhysicalLayerSchemaCreationStage) + assert len(physical_stage.snapshots) == 2 + assert {s.snapshot_id for s in physical_stage.snapshots} == { + snapshot_a.snapshot_id, + snapshot_b.snapshot_id, + } # Verify BackfillStage - backfill_stage = stages[2] + backfill_stage = stages[1] assert isinstance(backfill_stage, BackfillStage) assert len(backfill_stage.snapshot_to_intervals) == 2 assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() @@ -595,6 +761,23 @@ def test_build_plan_stages_restatement( for intervals in backfill_stage.snapshot_to_intervals.values(): assert intervals == expected_backfill_interval + # Verify RestatementStage + restatement_stage = stages[2] + assert isinstance(restatement_stage, RestatementStage) + restatement_stage = ExplainableRestatementStage.from_restatement_stage( + restatement_stage, state_reader, plan + ) + + # note: we only clear the intervals from state for "a" in dev, we leave prod alone + assert restatement_stage.snapshot_intervals_to_clear + assert len(restatement_stage.snapshot_intervals_to_clear) == 1 + snapshot_name, clear_request = list(restatement_stage.snapshot_intervals_to_clear.items())[0] + assert isinstance(clear_request, SnapshotIntervalClearRequest) + assert snapshot_name == '"a"' + assert clear_request.snapshot_id == snapshot_a_dev.snapshot_id + assert clear_request.snapshot == snapshot_a_dev.id_and_version + assert clear_request.interval == (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")) + # Verify EnvironmentRecordUpdateStage assert isinstance(stages[3], EnvironmentRecordUpdateStage) @@ -602,6 +785,155 @@ def test_build_plan_stages_restatement( assert isinstance(stages[4], FinalizeEnvironmentStage) +def test_build_plan_stages_restatement_dev_does_not_clear_intervals( + snapshot_a: Snapshot, + snapshot_b: Snapshot, + make_snapshot: t.Callable[..., Snapshot], + mocker: MockerFixture, +) -> None: + """ + Scenario: + - Restatement triggered against the dev environment + + Expected Outcome: + - BackfillStage only touches models in that dev environment + - Plan does not contain a RestatementStage because making changes in dev doesnt mean we need + to clear intervals from other environments + """ + # Dev version of snapshot_a, same name but different version + snapshot_a_dev = make_snapshot( + SqlModel( + name="a", + query=parse_one("select 1, changed, ds"), + kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"), + ) + ) + snapshot_a_dev.categorize_as(SnapshotChangeCategory.BREAKING) + assert snapshot_a_dev.snapshot_id != snapshot_a.snapshot_id + assert snapshot_a_dev.table_info != snapshot_a.table_info + + # Mock state reader to return existing snapshots and environment + state_reader = mocker.Mock(spec=StateReader) + snapshots_in_state = { + snapshot_a.snapshot_id: snapshot_a, + snapshot_b.snapshot_id: snapshot_b, + snapshot_a_dev.snapshot_id: snapshot_a_dev, + } + state_reader.get_snapshots.side_effect = lambda snapshot_info_like: { + k: v + for k, v in snapshots_in_state.items() + if k in [sil.snapshot_id for sil in snapshot_info_like] + } + + # prod has snapshot_a, snapshot_b + existing_prod_environment = Environment( + name="prod", + snapshots=[snapshot_a.table_info, snapshot_b.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_prod_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a.snapshot_id, snapshot_b.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + + # dev has new version of snapshot_a + existing_dev_environment = Environment( + name="dev", + snapshots=[snapshot_a_dev.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="previous_dev_plan", + previous_plan_id=None, + promoted_snapshot_ids=[snapshot_a_dev.snapshot_id], + finalized_ts=to_timestamp("2023-01-02"), + ) + + state_reader.get_environment.side_effect = ( + lambda name: existing_dev_environment if name == "dev" else existing_prod_environment + ) + state_reader.get_environments_summary.return_value = [ + existing_prod_environment.summary, + existing_dev_environment.summary, + ] + + environment = Environment( + name="dev", + snapshots=[snapshot_a_dev.table_info], + start_at="2023-01-01", + end_at="2023-01-02", + plan_id="test_plan", + previous_plan_id="previous_dev_plan", + promoted_snapshot_ids=[snapshot_a_dev.snapshot_id], + ) + + # Create evaluatable plan with restatements + plan = EvaluatablePlan( + start="2023-01-01", + end="2023-01-02", + new_snapshots=[], # No new snapshots + environment=environment, + no_gaps=False, + skip_backfill=False, + empty_backfill=False, + restatements={ + '"a"': (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + }, + is_dev=True, + allow_destructive_models=set(), + allow_additive_models=set(), + forward_only=False, + end_bounded=False, + ensure_finalized_snapshots=False, + ignore_cron=False, + directly_modified_snapshots=[], # No changes + indirectly_modified_snapshots={}, # No changes + metadata_updated_snapshots=[], + removed_snapshots=[], + requires_backfill=True, + models_to_backfill=None, + execution_time="2023-01-02", + disabled_restatement_models=set(), + environment_statements=None, + user_provided_flags=None, + ) + + # Build plan stages + stages = build_plan_stages(plan, state_reader, None) + + # Verify stages + assert len(stages) == 5 + + # Verify no RestatementStage + assert not any(s for s in stages if isinstance(s, RestatementStage)) + + # Verify PhysicalLayerSchemaCreationStage + physical_stage = stages[0] + assert isinstance(physical_stage, PhysicalLayerSchemaCreationStage) + assert len(physical_stage.snapshots) == 1 + assert {s.snapshot_id for s in physical_stage.snapshots} == { + snapshot_a_dev.snapshot_id, + } + + # Verify BackfillStage + backfill_stage = stages[1] + assert isinstance(backfill_stage, BackfillStage) + assert len(backfill_stage.snapshot_to_intervals) == 1 + assert backfill_stage.deployability_index == DeployabilityIndex.all_deployable() + backfill_snapshot, backfill_intervals = list(backfill_stage.snapshot_to_intervals.items())[0] + assert backfill_snapshot.snapshot_id == snapshot_a_dev.snapshot_id + assert backfill_intervals == [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))] + + # Verify EnvironmentRecordUpdateStage + assert isinstance(stages[2], EnvironmentRecordUpdateStage) + + # Verify VirtualLayerUpdateStage (all non-prod plans get this regardless) + assert isinstance(stages[3], VirtualLayerUpdateStage) + + # Verify FinalizeEnvironmentStage + assert isinstance(stages[4], FinalizeEnvironmentStage) + + def test_build_plan_stages_forward_only( snapshot_a: Snapshot, snapshot_b: Snapshot, make_snapshot, mocker: MockerFixture ) -> None: @@ -1686,6 +2018,7 @@ def test_adjust_intervals_restatement_removal( state_reader.refresh_snapshot_intervals = mocker.Mock() state_reader.get_snapshots.return_value = {} state_reader.get_environment.return_value = None + state_reader.get_environments_summary.return_value = [] environment = Environment( snapshots=[snapshot_a.table_info, snapshot_b.table_info], @@ -1738,8 +2071,6 @@ def test_adjust_intervals_restatement_removal( restatement_stages = [stage for stage in stages if isinstance(stage, RestatementStage)] assert len(restatement_stages) == 1 - restatement_stage = restatement_stages[0] - assert len(restatement_stage.snapshot_intervals) == 2 backfill_stages = [stage for stage in stages if isinstance(stage, BackfillStage)] assert len(backfill_stages) == 1 From 0e9c8a019843add8cc9637be063bc666ec16db3b Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:22:22 +0300 Subject: [PATCH 0858/1056] Fix!: don't consume connected tokens when parsing quoted identifiers (#5357) --- sqlmesh/core/dialect.py | 1 + sqlmesh/core/macros.py | 10 +++++----- sqlmesh/core/snapshot/evaluator.py | 5 ++++- tests/core/engine_adapter/test_clickhouse.py | 4 ++-- tests/core/test_dialect.py | 5 +++++ tests/core/test_format.py | 2 +- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index ed904cc4b3..6f5e3745fd 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -174,6 +174,7 @@ def _parse_id_var( while ( identifier + and not identifier.args.get("quoted") and self._is_connected() and ( self._match_texts(("{", SQLMESH_MACRO_PREFIX)) diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index 9e7df5d111..554638aec7 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -1128,7 +1128,7 @@ def haversine_distance( def pivot( evaluator: MacroEvaluator, column: SQL, - values: t.List[SQL], + values: t.List[exp.Expression], alias: bool = True, agg: exp.Expression = exp.Literal.string("SUM"), cmp: exp.Expression = exp.Literal.string("="), @@ -1146,10 +1146,10 @@ def pivot( >>> from sqlmesh.core.macros import MacroEvaluator >>> sql = "SELECT date_day, @PIVOT(status, ['cancelled', 'completed']) FROM rides GROUP BY 1" >>> MacroEvaluator().transform(parse_one(sql)).sql() - 'SELECT date_day, SUM(CASE WHEN status = \\'cancelled\\' THEN 1 ELSE 0 END) AS "\\'cancelled\\'", SUM(CASE WHEN status = \\'completed\\' THEN 1 ELSE 0 END) AS "\\'completed\\'" FROM rides GROUP BY 1' + 'SELECT date_day, SUM(CASE WHEN status = \\'cancelled\\' THEN 1 ELSE 0 END) AS "cancelled", SUM(CASE WHEN status = \\'completed\\' THEN 1 ELSE 0 END) AS "completed" FROM rides GROUP BY 1' >>> sql = "SELECT @PIVOT(a, ['v'], then_value := tv, suffix := '_sfx', quote := FALSE)" >>> MacroEvaluator(dialect="bigquery").transform(parse_one(sql)).sql("bigquery") - "SELECT SUM(CASE WHEN a = 'v' THEN tv ELSE 0 END) AS `v_sfx`" + "SELECT SUM(CASE WHEN a = 'v' THEN tv ELSE 0 END) AS v_sfx" """ aggregates: t.List[exp.Expression] = [] for value in values: @@ -1157,12 +1157,12 @@ def pivot( if distinct: proj += "DISTINCT " - proj += f"CASE WHEN {column} {cmp.name} {value} THEN {then_value} ELSE {else_value} END) " + proj += f"CASE WHEN {column} {cmp.name} {value.sql(evaluator.dialect)} THEN {then_value} ELSE {else_value} END) " node = evaluator.parse_one(proj) if alias: node = node.as_( - f"{prefix.name}{value}{suffix.name}", + f"{prefix.name}{value.name}{suffix.name}", quoted=quote, copy=False, dialect=evaluator.dialect, diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 961062fe45..8528dd4d1c 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -1110,7 +1110,10 @@ def _migrate_target_table( ) -> None: adapter = self.get_adapter(snapshot.model.gateway) - tmp_table_name = f"{target_table_name}_schema_tmp" + target_table = exp.to_table(target_table_name) + target_table.this.set("this", f"{target_table.name}_schema_tmp") + + tmp_table_name = target_table.sql() if snapshot.is_materialized: self._execute_create( snapshot=snapshot, diff --git a/tests/core/engine_adapter/test_clickhouse.py b/tests/core/engine_adapter/test_clickhouse.py index 39e317c7fa..188ae7f394 100644 --- a/tests/core/engine_adapter/test_clickhouse.py +++ b/tests/core/engine_adapter/test_clickhouse.py @@ -640,7 +640,7 @@ def test_scd_type_2_by_time( "test_valid_from", "test_valid_to", TRUE AS "_exists" - FROM ""__temp_target_efgh"" + FROM "__temp_target_efgh" WHERE NOT "test_valid_to" IS NULL ), "latest" AS ( @@ -652,7 +652,7 @@ def test_scd_type_2_by_time( "test_valid_from", "test_valid_to", TRUE AS "_exists" - FROM ""__temp_target_efgh"" + FROM "__temp_target_efgh" WHERE "test_valid_to" IS NULL ), "deleted" AS ( diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 11ffec3720..58e372c634 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -717,3 +717,8 @@ def test_sqlglot_extended_correctly(dialect: str) -> None: assert isinstance(value, exp.Table) assert value.sql() == "foo" assert ast.sql(dialect=dialect) == "MODEL (\nname foo\n)" + + +def test_connected_identifier(): + ast = d.parse_one("""SELECT ("x"at time zone 'utc')::timestamp as x""", "redshift") + assert ast.sql("redshift") == """SELECT CAST(("x" AT TIME ZONE 'utc') AS TIMESTAMP) AS x""" diff --git a/tests/core/test_format.py b/tests/core/test_format.py index 9b51220a9f..7d544eadf0 100644 --- a/tests/core/test_format.py +++ b/tests/core/test_format.py @@ -28,7 +28,7 @@ def test_format_files(tmp_path: pathlib.Path, mocker: MockerFixture): f3 = create_temp_file( tmp_path, pathlib.Path(audits_dir, "audit_1.sql"), - "AUDIT(name assert_positive_id, dialect 'duckdb'); SELECT * FROM @this_model WHERE \"CaseSensitive\"_item_id < 0;", + "AUDIT(name assert_positive_id, dialect 'duckdb'); SELECT * FROM @this_model WHERE \"CaseSensitive_item_id\" < 0;", ) f4 = create_temp_file( tmp_path, From acda46439ead3167556ff3ba3e01c801e10729b6 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 12 Sep 2025 09:42:45 -0700 Subject: [PATCH 0859/1056] fix: add package to peer deps (#5363) --- web/common/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/common/package.json b/web/common/package.json index f576696b61..5f869c8a25 100644 --- a/web/common/package.json +++ b/web/common/package.json @@ -77,6 +77,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "tailwind-merge": "^3.3.1", + "tailwind-scrollbar": "^3.1.0", "tailwindcss": "^3.4.17" }, "private": false, @@ -92,6 +93,7 @@ "storybook": "storybook dev -p 6006", "syncpack": "syncpack lint", "syncpack:fix": "syncpack fix-mismatches", + "syncpack:format": "syncpack format", "syncpack:list": "syncpack list-mismatches", "test": "vitest", "test:ui": "vitest --ui", From 0c02d505496763edcc07039122d0207024cc2ee8 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 12 Sep 2025 14:00:41 -0700 Subject: [PATCH 0860/1056] Fix: The resolution order for dbt variables (#5366) --- sqlmesh/dbt/project.py | 19 +++++++++++-------- tests/dbt/test_config.py | 17 ++++++++++++++--- tests/fixtures/dbt/sushi_test/dbt_project.yml | 3 ++- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/sqlmesh/dbt/project.py b/sqlmesh/dbt/project.py index 4af30958f5..355b18630e 100644 --- a/sqlmesh/dbt/project.py +++ b/sqlmesh/dbt/project.py @@ -99,17 +99,20 @@ def load(cls, context: DbtContext, variables: t.Optional[t.Dict[str, t.Any]] = N package = package_loader.load(path.parent) packages[package.name] = package + # Variable resolution precedence: + # 1. Variable overrides + # 2. Package-scoped variables in the root project's dbt_project.yml + # 3. Global project variables in the root project's dbt_project.yml + # 4. Variables in the package's dbt_project.yml all_project_variables = {**(project_yaml.get("vars") or {}), **(variable_overrides or {})} for name, package in packages.items(): - package_vars = all_project_variables.get(name) - - if isinstance(package_vars, dict): - package.variables.update(package_vars) - - if name == context.project_name: - package.variables.update(all_project_variables) + if isinstance(all_project_variables.get(name), dict): + project_vars_copy = all_project_variables.copy() + package_scoped_vars = project_vars_copy.pop(name) + package.variables.update(project_vars_copy) + package.variables.update(package_scoped_vars) else: - package.variables.update(variable_overrides) + package.variables.update(all_project_variables) return Project(context, profile, packages) diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 4e3e78eea9..30dae478a1 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -343,15 +343,26 @@ def test_variables(assert_exp_eq, sushi_test_project): "customers:customer_id": "customer_id", "some_var": ["foo", "bar"], }, + "some_var": "should be overridden in customers package", } expected_customer_variables = { - "some_var": ["foo", "bar"], + "some_var": ["foo", "bar"], # Takes precedence over the root project variable "some_other_var": 5, - "yet_another_var": 5, "customers:bla": False, "customers:customer_id": "customer_id", + "yet_another_var": 1, # Make sure that the project variable takes precedence + "top_waiters:limit": "{{ get_top_waiters_limit() }}", + "top_waiters:revenue": "revenue", + "customers:boo": ["a", "b"], + "nested_vars": { + "some_nested_var": 2, + }, + "dynamic_test_var": 3, + "list_var": [ + {"name": "item1", "value": 1}, + {"name": "item2", "value": 2}, + ], } - assert sushi_test_project.packages["sushi"].variables == expected_sushi_variables assert sushi_test_project.packages["customers"].variables == expected_customer_variables diff --git a/tests/fixtures/dbt/sushi_test/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_project.yml index 2a25389e43..920dea7216 100644 --- a/tests/fixtures/dbt/sushi_test/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/dbt_project.yml @@ -50,6 +50,7 @@ vars: yet_another_var: 1 dynamic_test_var: 3 + some_var: 'should be overridden in customers package' customers: some_var: ["foo", "bar"] @@ -74,4 +75,4 @@ on-run-start: on-run-end: - '{{ create_tables(schemas) }}' - 'DROP TABLE to_be_executed_last;' - - '{{ graph_usage() }}' \ No newline at end of file + - '{{ graph_usage() }}' From c1291d4b41f5e027a0f717be1782d70efc7a9f2c Mon Sep 17 00:00:00 2001 From: Chris Rericha <67359577+crericha@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:19:55 -0400 Subject: [PATCH 0861/1056] Fix: Update dbt loader to support loading multiple models from same file (#5346) --- sqlmesh/dbt/loader.py | 17 ++++++++++++----- tests/dbt/test_model.py | 12 ++++++++++++ .../snapshots/items_check_snapshot.sql | 15 --------------- .../{items_snapshot.sql => items_snapshots.sql} | 16 ++++++++++++++++ 4 files changed, 40 insertions(+), 20 deletions(-) delete mode 100644 tests/fixtures/dbt/sushi_test/snapshots/items_check_snapshot.sql rename tests/fixtures/dbt/sushi_test/snapshots/{items_snapshot.sql => items_snapshots.sql} (53%) diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 695aff3c45..f7d97e74c8 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -5,6 +5,7 @@ import typing as t import sqlmesh.core.dialect as d from pathlib import Path +from collections import defaultdict from sqlmesh.core.config import ( Config, ConnectionConfig, @@ -137,16 +138,22 @@ def _to_sqlmesh(config: BMC, context: DbtContext) -> Model: package_context.set_and_render_variables(package.variables, package.name) package_models: t.Dict[str, BaseModelConfig] = {**package.models, **package.seeds} + package_models_by_path: t.Dict[Path, t.List[BaseModelConfig]] = defaultdict(list) for model in package_models.values(): if isinstance(model, ModelConfig) and not model.sql.strip(): logger.info(f"Skipping empty model '{model.name}' at path '{model.path}'.") continue + package_models_by_path[model.path].append(model) - sqlmesh_model = cache.get_or_load_models( - model.path, loader=lambda: [_to_sqlmesh(model, package_context)] - )[0] - - models[sqlmesh_model.fqn] = sqlmesh_model + for path, path_models in package_models_by_path.items(): + sqlmesh_models = cache.get_or_load_models( + path, + loader=lambda: [ + _to_sqlmesh(model, package_context) for model in path_models + ], + ) + for sqlmesh_model in sqlmesh_models: + models[sqlmesh_model.fqn] = sqlmesh_model models.update(self._load_external_models(audits, cache)) diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index dc2ebc492b..caa409807f 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -733,6 +733,18 @@ def test_load_microbatch_with_ref_no_filter( ) +@pytest.mark.slow +def test_load_multiple_snapshots_defined_in_same_file(sushi_test_dbt_context: Context) -> None: + context = sushi_test_dbt_context + assert context.get_model("snapshots.items_snapshot") + assert context.get_model("snapshots.items_check_snapshot") + + # Make sure cache works too + context.load() + assert context.get_model("snapshots.items_snapshot") + assert context.get_model("snapshots.items_check_snapshot") + + @pytest.mark.slow def test_dbt_jinja_macro_undefined_variable_error(create_empty_project): project_dir, model_dir = create_empty_project() diff --git a/tests/fixtures/dbt/sushi_test/snapshots/items_check_snapshot.sql b/tests/fixtures/dbt/sushi_test/snapshots/items_check_snapshot.sql deleted file mode 100644 index fdda412e7f..0000000000 --- a/tests/fixtures/dbt/sushi_test/snapshots/items_check_snapshot.sql +++ /dev/null @@ -1,15 +0,0 @@ -{% snapshot items_check_snapshot %} - -{{ - config( - target_schema='snapshots', - unique_key='id', - strategy='check', - check_cols=['ds'], - invalidate_hard_deletes=True, - ) -}} - -select * from {{ source('streaming', 'items') }} - -{% endsnapshot %} diff --git a/tests/fixtures/dbt/sushi_test/snapshots/items_snapshot.sql b/tests/fixtures/dbt/sushi_test/snapshots/items_snapshots.sql similarity index 53% rename from tests/fixtures/dbt/sushi_test/snapshots/items_snapshot.sql rename to tests/fixtures/dbt/sushi_test/snapshots/items_snapshots.sql index c5c922d217..77d79d03ba 100644 --- a/tests/fixtures/dbt/sushi_test/snapshots/items_snapshot.sql +++ b/tests/fixtures/dbt/sushi_test/snapshots/items_snapshots.sql @@ -14,3 +14,19 @@ select * from {{ source('streaming', 'items') }} {% endsnapshot %} + +{% snapshot items_check_snapshot %} + +{{ + config( + target_schema='snapshots', + unique_key='id', + strategy='check', + check_cols=['ds'], + invalidate_hard_deletes=True, + ) +}} + +select * from {{ source('streaming', 'items') }} + +{% endsnapshot %} From 5eaf0151d7ef094a9eae7eecd84d6ac4cbd886d5 Mon Sep 17 00:00:00 2001 From: Chris Rericha <67359577+crericha@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:20:10 -0400 Subject: [PATCH 0862/1056] Fix: dbt manifest can have null macro dependencies that translate into 'None' (#5367) --- sqlmesh/dbt/manifest.py | 2 +- tests/dbt/test_manifest.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 15377e59dc..0e33569888 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -661,7 +661,7 @@ def _macro_references( return result for macro_node_id in node.depends_on.macros: - if not macro_node_id: + if not macro_node_id or macro_node_id == "None": continue macro_node = manifest.macros[macro_node_id] diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index e5e98eae49..e6c02bcb4c 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -304,3 +304,23 @@ def test_convert_jinja_test_to_macro(): {%- endmacro -%}""" assert _convert_jinja_test_to_macro(macro_input) == macro_input + + +@pytest.mark.xdist_group("dbt_manifest") +def test_macro_depenency_none_str(): + project_path = Path("tests/fixtures/dbt/sushi_test") + profile = Profile.load(DbtContext(project_path)) + helper = ManifestHelper( + project_path, + project_path, + "sushi", + profile.target, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), + ) + node = helper._manifest.nodes["model.customers.customer_revenue_by_day"] + node.depends_on.macros.append("None") + + from sqlmesh.dbt.manifest import _macro_references + + # "None" macro shouldn't raise a KeyError + _macro_references(helper._manifest, node) From 9d7f3a0b8930a3ad35f8f313c5ec40706b421fdf Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:26:42 +0100 Subject: [PATCH 0863/1056] Chore: add pre and post hook to dbt fixture (#5372) --- tests/fixtures/dbt/sushi_test/models/schema.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/fixtures/dbt/sushi_test/models/schema.yml b/tests/fixtures/dbt/sushi_test/models/schema.yml index 48b8b814d3..87a201c418 100644 --- a/tests/fixtures/dbt/sushi_test/models/schema.yml +++ b/tests/fixtures/dbt/sushi_test/models/schema.yml @@ -20,6 +20,12 @@ models: error_after: {count: 9, period: hour} - name: waiters description: '{{ doc("waiters") }}' + config: + # Exercise pre and post hooks + pre_hook: + - SELECT 1 + post_hook: + - SELECT 1 - name: waiter_as_customer_by_day - name: waiter_revenue_by_day versions: @@ -71,4 +77,4 @@ metrics: type: simple label: testing type_params: - measure: total_waiters \ No newline at end of file + measure: total_waiters From 9dbce8152cfe983c647ae00c4eb496d33d05a2f7 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:01:11 +0300 Subject: [PATCH 0864/1056] Fix: port sqlglot pipe syntax fix in _parse_select (#5373) --- sqlmesh/core/dialect.py | 2 ++ tests/core/test_dialect.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py index 6f5e3745fd..332550d57c 100644 --- a/sqlmesh/core/dialect.py +++ b/sqlmesh/core/dialect.py @@ -350,6 +350,7 @@ def _parse_select( parse_subquery_alias: bool = True, parse_set_operation: bool = True, consume_pipe: bool = True, + from_: t.Optional[exp.From] = None, ) -> t.Optional[exp.Expression]: select = self.__parse_select( # type: ignore nested=nested, @@ -357,6 +358,7 @@ def _parse_select( parse_subquery_alias=parse_subquery_alias, parse_set_operation=parse_set_operation, consume_pipe=consume_pipe, + from_=from_, ) if ( diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 58e372c634..52ea673778 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -722,3 +722,11 @@ def test_sqlglot_extended_correctly(dialect: str) -> None: def test_connected_identifier(): ast = d.parse_one("""SELECT ("x"at time zone 'utc')::timestamp as x""", "redshift") assert ast.sql("redshift") == """SELECT CAST(("x" AT TIME ZONE 'utc') AS TIMESTAMP) AS x""" + + +def test_pipe_syntax(): + ast = d.parse_one("SELECT * FROM (FROM t2 |> SELECT id)", "bigquery") + assert ( + ast.sql("bigquery") + == "SELECT * FROM (WITH __tmp1 AS (SELECT id FROM t2) SELECT * FROM __tmp1)" + ) From 7ad241acb53da84516eecf09e0adcee7444a5e7a Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 15 Sep 2025 10:18:21 -0700 Subject: [PATCH 0865/1056] fix: adjust styling for component (#5375) --- web/common/src/components/Metadata/Metadata.tsx | 14 ++++++++++++-- web/common/src/components/ModelName/ModelName.tsx | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/web/common/src/components/Metadata/Metadata.tsx b/web/common/src/components/Metadata/Metadata.tsx index 9227844fd3..a9f908bbf1 100644 --- a/web/common/src/components/Metadata/Metadata.tsx +++ b/web/common/src/components/Metadata/Metadata.tsx @@ -20,12 +20,22 @@ export const Metadata = React.forwardRef( {...props} > {typeof label === 'string' ? ( -
      {label}
      +
      + {label} +
      ) : ( label )} {typeof value === 'string' ? ( -
      {value}
      +
      + {value} +
      ) : ( value )} diff --git a/web/common/src/components/ModelName/ModelName.tsx b/web/common/src/components/ModelName/ModelName.tsx index 1c902018a1..0685d4b872 100644 --- a/web/common/src/components/ModelName/ModelName.tsx +++ b/web/common/src/components/ModelName/ModelName.tsx @@ -192,6 +192,7 @@ export const ModelName = React.forwardRef( {showCopy && ( From 745db0f6abc33991640af996abb69e6dae9707f7 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Mon, 15 Sep 2025 20:39:39 +0300 Subject: [PATCH 0866/1056] Fix: treat all instances of macro variables as case-insensitive (#5352) --- sqlmesh/core/macros.py | 33 ++++++++++++++++++++++-------- tests/core/test_macros.py | 13 ++++++++++++ tests/core/test_model.py | 43 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index 554638aec7..b58817950d 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -128,6 +128,17 @@ def _macro_str_replace(text: str) -> str: return f"self.template({text}, locals())" +class CaseInsensitiveMapping(t.Dict[str, t.Any]): + def __init__(self, data: t.Dict[str, t.Any]) -> None: + super().__init__(data) + + def __getitem__(self, key: str) -> t.Any: + return super().__getitem__(key.lower()) + + def get(self, key: str, default: t.Any = None, /) -> t.Any: + return super().get(key.lower(), default) + + class MacroDialect(Python): class Generator(Python.Generator): TRANSFORMS = { @@ -256,14 +267,18 @@ def evaluate_macros( changed = True variables = self.variables - if node.name not in self.locals and node.name.lower() not in variables: + # This makes all variables case-insensitive, e.g. @X is the same as @x. We do this + # for consistency, since `variables` and `blueprint_variables` are normalized. + var_name = node.name.lower() + + if var_name not in self.locals and var_name not in variables: if not isinstance(node.parent, StagedFilePath): raise SQLMeshError(f"Macro variable '{node.name}' is undefined.") return node # Precedence order is locals (e.g. @DEF) > blueprint variables > config variables - value = self.locals.get(node.name, variables.get(node.name.lower())) + value = self.locals.get(var_name, variables.get(var_name)) if isinstance(value, list): return exp.convert( tuple( @@ -313,11 +328,11 @@ def template(self, text: t.Any, local_variables: t.Dict[str, t.Any]) -> str: """ # We try to convert all variables into sqlglot expressions because they're going to be converted # into strings; in sql we don't convert strings because that would result in adding quotes - mapping = { - k: convert_sql(v, self.dialect) + base_mapping = { + k.lower(): convert_sql(v, self.dialect) for k, v in chain(self.variables.items(), self.locals.items(), local_variables.items()) } - return MacroStrTemplate(str(text)).safe_substitute(mapping) + return MacroStrTemplate(str(text)).safe_substitute(CaseInsensitiveMapping(base_mapping)) def evaluate(self, node: MacroFunc) -> exp.Expression | t.List[exp.Expression] | None: if isinstance(node, MacroDef): @@ -327,7 +342,9 @@ def evaluate(self, node: MacroFunc) -> exp.Expression | t.List[exp.Expression] | args[0] if len(args) == 1 else exp.Tuple(expressions=list(args)) ) else: - self.locals[node.name] = self.transform(node.expression) + # Make variables defined through `@DEF` case-insensitive + self.locals[node.name.lower()] = self.transform(node.expression) + return node if isinstance(node, (MacroSQL, MacroStrReplace)): @@ -630,7 +647,7 @@ def substitute( ) -> exp.Expression | t.List[exp.Expression] | None: if isinstance(node, (exp.Identifier, exp.Var)): if not isinstance(node.parent, exp.Column): - name = node.name + name = node.name.lower() if name in args: return args[name].copy() if name in evaluator.locals: @@ -663,7 +680,7 @@ def substitute( return expressions, lambda args: func.this.transform( substitute, { - expression.name: arg + expression.name.lower(): arg for expression, arg in zip( func.expressions, args.expressions if isinstance(args, exp.Tuple) else [args] ) diff --git a/tests/core/test_macros.py b/tests/core/test_macros.py index 77d8fb84ae..fb10f64b27 100644 --- a/tests/core/test_macros.py +++ b/tests/core/test_macros.py @@ -292,6 +292,16 @@ def test_ast_correctness(macro_evaluator): "SELECT 'a' + a_z + 'c' + c_a, 'b' + b_z + 'c' + c_b", {"y": "c"}, ), + ( + """select @each(['a'], x -> @X)""", + "SELECT 'a'", + {}, + ), + ( + """select @each(['a'], X -> @x)""", + "SELECT 'a'", + {}, + ), ( '"is_@{x}"', '"is_b"', @@ -1112,7 +1122,9 @@ def test_macro_with_spaces(): for sql, expected in ( ("@x", '"a b"'), + ("@X", '"a b"'), ("@{x}", '"a b"'), + ("@{X}", '"a b"'), ("a_@x", '"a_a b"'), ("a.@x", 'a."a b"'), ("@y", "'a b'"), @@ -1121,6 +1133,7 @@ def test_macro_with_spaces(): ("a.@{y}", 'a."a b"'), ("@z", 'a."b c"'), ("d.@z", 'd.a."b c"'), + ("@'test_@{X}_suffix'", "'test_a b_suffix'"), ): assert evaluator.transform(parse_one(sql)).sql() == expected diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 1511e37c53..ebe4d11a20 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -9377,9 +9377,9 @@ def test_model_blueprinting(tmp_path: Path) -> None: model_defaults=ModelDefaultsConfig(dialect="duckdb"), ) - blueprint_sql = tmp_path / "macros" / "identity_macro.py" - blueprint_sql.parent.mkdir(parents=True, exist_ok=True) - blueprint_sql.write_text( + identity_macro = tmp_path / "macros" / "identity_macro.py" + identity_macro.parent.mkdir(parents=True, exist_ok=True) + identity_macro.write_text( """from sqlmesh import macro @macro() @@ -11623,3 +11623,40 @@ def test_use_original_sql(): assert model.query_.sql == "SELECT 1 AS one, 2 AS two" assert model.pre_statements_[0].sql == "CREATE TABLE pre (a INT)" assert model.post_statements_[0].sql == "CREATE TABLE post (b INT)" + + +def test_case_sensitive_macro_locals(tmp_path: Path) -> None: + init_example_project(tmp_path, engine_type="duckdb", template=ProjectTemplate.EMPTY) + + db_path = str(tmp_path / "db.db") + db_connection = DuckDBConnectionConfig(database=db_path) + + config = Config( + gateways={"gw": GatewayConfig(connection=db_connection)}, + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + ) + + macro_file = tmp_path / "macros" / "some_macro_with_globals.py" + macro_file.parent.mkdir(parents=True, exist_ok=True) + macro_file.write_text( + """from sqlmesh import macro + +x = 1 +X = 2 + +@macro() +def my_macro(evaluator): + assert evaluator.locals.get("x") == 1 + assert evaluator.locals.get("X") == 2 + + return x + X +""" + ) + test_model = tmp_path / "models" / "test_model.sql" + test_model.parent.mkdir(parents=True, exist_ok=True) + test_model.write_text("MODEL (name test_model, kind FULL); SELECT @my_macro() AS c") + + context = Context(paths=tmp_path, config=config) + model = context.get_model("test_model", raise_if_missing=True) + + assert model.render_query_or_raise().sql() == 'SELECT 3 AS "c"' From 1b2979bd75cd226869f8904a15a09df6277311fd Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 15 Sep 2025 11:04:55 -0700 Subject: [PATCH 0867/1056] Fix: Variable overrides for dbt packages (#5376) --- sqlmesh/dbt/project.py | 2 ++ tests/dbt/test_config.py | 10 ++++++++++ tests/fixtures/dbt/sushi_test/config.py | 10 ++++++++++ 3 files changed, 22 insertions(+) diff --git a/sqlmesh/dbt/project.py b/sqlmesh/dbt/project.py index 355b18630e..2b0a2e0c3f 100644 --- a/sqlmesh/dbt/project.py +++ b/sqlmesh/dbt/project.py @@ -113,6 +113,8 @@ def load(cls, context: DbtContext, variables: t.Optional[t.Dict[str, t.Any]] = N package.variables.update(package_scoped_vars) else: package.variables.update(all_project_variables) + if variable_overrides: + package.variables.update(variable_overrides) return Project(context, profile, packages) diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 30dae478a1..fe226d4926 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -367,6 +367,16 @@ def test_variables(assert_exp_eq, sushi_test_project): assert sushi_test_project.packages["customers"].variables == expected_customer_variables +@pytest.mark.slow +def test_variables_override(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context( + "tests/fixtures/dbt/sushi_test", config="test_config_with_var_override" + ) + dbt_project = context._loaders[0]._load_projects()[0] # type: ignore + assert dbt_project.packages["sushi"].variables["some_var"] == "overridden_from_config_py" + assert dbt_project.packages["customers"].variables["some_var"] == "overridden_from_config_py" + + @pytest.mark.slow def test_jinja_in_dbt_variables(sushi_test_dbt_context: Context): assert sushi_test_dbt_context.render("sushi.top_waiters").sql().endswith("LIMIT 10") diff --git a/tests/fixtures/dbt/sushi_test/config.py b/tests/fixtures/dbt/sushi_test/config.py index 83118b02cf..a68b3e2333 100644 --- a/tests/fixtures/dbt/sushi_test/config.py +++ b/tests/fixtures/dbt/sushi_test/config.py @@ -11,6 +11,16 @@ test_config = config + +test_config_with_var_override = sqlmesh_config( + Path(__file__).parent, + model_defaults=ModelDefaultsConfig(dialect="duckdb", start="Jan 1 2022"), + variables={ + "some_var": "overridden_from_config_py", + }, +) + + test_config_with_normalization_strategy = sqlmesh_config( Path(__file__).parent, model_defaults=ModelDefaultsConfig( From 906be74fa652a928e2273fcafaf13a482af8ffad Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 15 Sep 2025 12:34:31 -0700 Subject: [PATCH 0868/1056] fix(web_common): loading icon size (#5378) --- web/common/src/components/LoadingContainer/LoadingContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/common/src/components/LoadingContainer/LoadingContainer.tsx b/web/common/src/components/LoadingContainer/LoadingContainer.tsx index 4698266e4e..bba205ae6a 100644 --- a/web/common/src/components/LoadingContainer/LoadingContainer.tsx +++ b/web/common/src/components/LoadingContainer/LoadingContainer.tsx @@ -28,7 +28,7 @@ export const LoadingContainer = React.forwardRef< function renderLoading() { return ( <> - + {message && {message}} ) From 24622986f616791f8490988a7449dbad824b8053 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:45:01 -0700 Subject: [PATCH 0869/1056] chore: refactor to remove merge mixin (#5380) --- sqlmesh/core/engine_adapter/base.py | 24 +++++++++++++ sqlmesh/core/engine_adapter/bigquery.py | 5 +-- sqlmesh/core/engine_adapter/fabric.py | 29 +-------------- sqlmesh/core/engine_adapter/mixins.py | 47 ------------------------- sqlmesh/core/engine_adapter/mssql.py | 3 +- sqlmesh/core/engine_adapter/shared.py | 6 ++++ tests/core/engine_adapter/test_base.py | 6 ++-- tests/core/engine_adapter/test_mssql.py | 41 --------------------- 8 files changed, 37 insertions(+), 124 deletions(-) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index c48ce2154d..94900f0193 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -1633,6 +1633,30 @@ def _insert_overwrite_by_condition( target_columns_to_types=target_columns_to_types, order_projections=False, ) + elif insert_overwrite_strategy.is_merge: + columns = [exp.column(col) for col in target_columns_to_types] + when_not_matched_by_source = exp.When( + matched=False, + source=True, + condition=where, + then=exp.Delete(), + ) + when_not_matched_by_target = exp.When( + matched=False, + source=False, + then=exp.Insert( + this=exp.Tuple(expressions=columns), + expression=exp.Tuple(expressions=columns), + ), + ) + self._merge( + target_table=table_name, + query=query, + on=exp.false(), + whens=exp.Whens( + expressions=[when_not_matched_by_source, when_not_matched_by_target] + ), + ) else: insert_exp = exp.insert( query, diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index b3d02d8bbf..00b33f67a5 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -9,7 +9,6 @@ from sqlmesh.core.dialect import to_schema from sqlmesh.core.engine_adapter.mixins import ( - InsertOverwriteWithMergeMixin, ClusteredByMixin, RowDiffMixin, TableAlterClusterByOperation, @@ -20,6 +19,7 @@ DataObjectType, SourceQuery, set_catalog, + InsertOverwriteStrategy, ) from sqlmesh.core.node import IntervalUnit from sqlmesh.core.schema_diff import TableAlterOperation, NestedSupport @@ -54,7 +54,7 @@ @set_catalog() -class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin, ClusteredByMixin, RowDiffMixin): +class BigQueryEngineAdapter(ClusteredByMixin, RowDiffMixin): """ BigQuery Engine Adapter using the `google-cloud-bigquery` library's DB API. """ @@ -68,6 +68,7 @@ class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin, ClusteredByMixin, Row MAX_COLUMN_COMMENT_LENGTH = 1024 SUPPORTS_QUERY_EXECUTION_TRACKING = True SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA"] + INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.MERGE SCHEMA_DIFFER_KWARGS = { "compatible_types": { diff --git a/sqlmesh/core/engine_adapter/fabric.py b/sqlmesh/core/engine_adapter/fabric.py index a528be3cb4..8e2fb0e496 100644 --- a/sqlmesh/core/engine_adapter/fabric.py +++ b/sqlmesh/core/engine_adapter/fabric.py @@ -7,22 +7,15 @@ from functools import cached_property from sqlglot import exp from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_result +from sqlmesh.core.engine_adapter.mixins import LogicalMergeMixin from sqlmesh.core.engine_adapter.mssql import MSSQLEngineAdapter from sqlmesh.core.engine_adapter.shared import ( InsertOverwriteStrategy, - SourceQuery, ) -from sqlmesh.core.engine_adapter.base import EngineAdapter from sqlmesh.utils.errors import SQLMeshError from sqlmesh.utils.connection_pool import ConnectionPool -if t.TYPE_CHECKING: - from sqlmesh.core._typing import TableName - - -from sqlmesh.core.engine_adapter.mixins import LogicalMergeMixin - logger = logging.getLogger(__name__) @@ -58,26 +51,6 @@ def _target_catalog(self) -> t.Optional[str]: def _target_catalog(self, value: t.Optional[str]) -> None: self._connection_pool.set_attribute("target_catalog", value) - def _insert_overwrite_by_condition( - self, - table_name: TableName, - source_queries: t.List[SourceQuery], - target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, - where: t.Optional[exp.Condition] = None, - insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None, - **kwargs: t.Any, - ) -> None: - # Override to avoid MERGE statement which isn't fully supported in Fabric - return EngineAdapter._insert_overwrite_by_condition( - self, - table_name=table_name, - source_queries=source_queries, - target_columns_to_types=target_columns_to_types, - where=where, - insert_overwrite_strategy_override=InsertOverwriteStrategy.DELETE_INSERT, - **kwargs, - ) - @property def api_client(self) -> FabricHttpClient: # the requests Session is not guaranteed to be threadsafe diff --git a/sqlmesh/core/engine_adapter/mixins.py b/sqlmesh/core/engine_adapter/mixins.py index 865e47fb93..1d66da0607 100644 --- a/sqlmesh/core/engine_adapter/mixins.py +++ b/sqlmesh/core/engine_adapter/mixins.py @@ -9,7 +9,6 @@ from sqlglot.helper import seq_get from sqlmesh.core.engine_adapter.base import EngineAdapter -from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, SourceQuery from sqlmesh.core.node import IntervalUnit from sqlmesh.core.dialect import schema_ from sqlmesh.core.schema_diff import TableAlterOperation @@ -75,52 +74,6 @@ def _fetch_native_df( return df -class InsertOverwriteWithMergeMixin(EngineAdapter): - def _insert_overwrite_by_condition( - self, - table_name: TableName, - source_queries: t.List[SourceQuery], - target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None, - where: t.Optional[exp.Condition] = None, - insert_overwrite_strategy_override: t.Optional[InsertOverwriteStrategy] = None, - **kwargs: t.Any, - ) -> None: - """ - Some engines do not support `INSERT OVERWRITE` but instead support - doing an "INSERT OVERWRITE" using a Merge expression but with the - predicate being `False`. - """ - target_columns_to_types = target_columns_to_types or self.columns(table_name) - for source_query in source_queries: - with source_query as query: - query = self._order_projections_and_filter( - query, target_columns_to_types, where=where - ) - columns = [exp.column(col) for col in target_columns_to_types] - when_not_matched_by_source = exp.When( - matched=False, - source=True, - condition=where, - then=exp.Delete(), - ) - when_not_matched_by_target = exp.When( - matched=False, - source=False, - then=exp.Insert( - this=exp.Tuple(expressions=columns), - expression=exp.Tuple(expressions=columns), - ), - ) - self._merge( - target_table=table_name, - query=query, - on=exp.false(), - whens=exp.Whens( - expressions=[when_not_matched_by_source, when_not_matched_by_target] - ), - ) - - class HiveMetastoreTablePropertiesMixin(EngineAdapter): MAX_TABLE_COMMENT_LENGTH = 4000 MAX_COLUMN_COMMENT_LENGTH = 4000 diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index 50a67b4b37..fd0bf1011b 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -16,7 +16,6 @@ ) from sqlmesh.core.engine_adapter.mixins import ( GetCurrentCatalogFromFunctionMixin, - InsertOverwriteWithMergeMixin, PandasNativeFetchDFSupportMixin, VarcharSizeWorkaroundMixin, RowDiffMixin, @@ -41,7 +40,6 @@ class MSSQLEngineAdapter( EngineAdapterWithIndexSupport, PandasNativeFetchDFSupportMixin, - InsertOverwriteWithMergeMixin, GetCurrentCatalogFromFunctionMixin, VarcharSizeWorkaroundMixin, RowDiffMixin, @@ -74,6 +72,7 @@ class MSSQLEngineAdapter( }, } VARIABLE_LENGTH_DATA_TYPES = {"binary", "varbinary", "char", "varchar", "nchar", "nvarchar"} + INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.MERGE @property def catalog_support(self) -> CatalogSupport: diff --git a/sqlmesh/core/engine_adapter/shared.py b/sqlmesh/core/engine_adapter/shared.py index 55f04a995e..ba0e1fa619 100644 --- a/sqlmesh/core/engine_adapter/shared.py +++ b/sqlmesh/core/engine_adapter/shared.py @@ -243,6 +243,8 @@ class InsertOverwriteStrategy(Enum): # Issue a single INSERT query to replace a data range. The assumption is that the query engine will transparently match partition bounds # and replace data rather than append to it. Trino is an example of this when `hive.insert-existing-partitions-behavior=OVERWRITE` is configured INTO_IS_OVERWRITE = 4 + # Do the INSERT OVERWRITE using merge since the engine doesn't support it natively + MERGE = 5 @property def is_delete_insert(self) -> bool: @@ -260,6 +262,10 @@ def is_replace_where(self) -> bool: def is_into_is_overwrite(self) -> bool: return self == InsertOverwriteStrategy.INTO_IS_OVERWRITE + @property + def is_merge(self) -> bool: + return self == InsertOverwriteStrategy.MERGE + class SourceQuery: def __init__( diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index b2dfcc7ccc..220c3291f7 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -13,7 +13,6 @@ from sqlmesh.core import dialect as d from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core.engine_adapter import EngineAdapter, EngineAdapterWithIndexSupport -from sqlmesh.core.engine_adapter.mixins import InsertOverwriteWithMergeMixin from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, DataObject from sqlmesh.core.schema_diff import SchemaDiffer, TableAlterOperation, NestedSupport from sqlmesh.utils import columns_to_types_to_struct @@ -21,8 +20,6 @@ from sqlmesh.utils.errors import SQLMeshError, UnsupportedCatalogOperationError from tests.core.engine_adapter import to_sql_calls -if t.TYPE_CHECKING: - pass pytestmark = pytest.mark.engine @@ -482,7 +479,8 @@ def test_insert_overwrite_no_where(make_mocked_engine_adapter: t.Callable): def test_insert_overwrite_by_condition_column_contains_unsafe_characters( make_mocked_engine_adapter: t.Callable, mocker: MockerFixture ): - adapter = make_mocked_engine_adapter(InsertOverwriteWithMergeMixin) + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.MERGE source_queries, columns_to_types = adapter._get_source_queries_and_columns_to_types( parse_one("SELECT 1 AS c"), None, target_table="test_table" diff --git a/tests/core/engine_adapter/test_mssql.py b/tests/core/engine_adapter/test_mssql.py index 5923afa217..a405bb7576 100644 --- a/tests/core/engine_adapter/test_mssql.py +++ b/tests/core/engine_adapter/test_mssql.py @@ -16,7 +16,6 @@ from sqlmesh.core.engine_adapter.shared import ( DataObject, DataObjectType, - InsertOverwriteStrategy, ) from sqlmesh.utils.date import to_ds from tests.core.engine_adapter import to_sql_calls @@ -342,46 +341,6 @@ def test_insert_overwrite_by_time_partition_supports_insert_overwrite_pandas_exi ] -def test_insert_overwrite_by_time_partition_replace_where_pandas( - make_mocked_engine_adapter: t.Callable, mocker: MockerFixture, make_temp_table_name: t.Callable -): - mocker.patch( - "sqlmesh.core.engine_adapter.mssql.MSSQLEngineAdapter.table_exists", - return_value=False, - ) - - adapter = make_mocked_engine_adapter(MSSQLEngineAdapter) - adapter.INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.REPLACE_WHERE - - temp_table_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter._get_temp_table") - table_name = "test_table" - temp_table_id = "abcdefgh" - temp_table_mock.return_value = make_temp_table_name(table_name, temp_table_id) - - df = pd.DataFrame({"a": [1, 2], "ds": ["2022-01-01", "2022-01-02"]}) - adapter.insert_overwrite_by_time_partition( - table_name, - df, - start="2022-01-01", - end="2022-01-02", - time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), - time_column="ds", - target_columns_to_types={ - "a": exp.DataType.build("INT"), - "ds": exp.DataType.build("STRING"), - }, - ) - adapter._connection_pool.get().bulk_copy.assert_called_with( - f"__temp_test_table_{temp_table_id}", [(1, "2022-01-01"), (2, "2022-01-02")] - ) - - assert to_sql_calls(adapter) == [ - f"""IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '__temp_test_table_{temp_table_id}') EXEC('CREATE TABLE [__temp_test_table_{temp_table_id}] ([a] INTEGER, [ds] VARCHAR(MAX))');""", - f"""MERGE INTO [test_table] AS [__MERGE_TARGET__] USING (SELECT [a] AS [a], [ds] AS [ds] FROM (SELECT CAST([a] AS INTEGER) AS [a], CAST([ds] AS VARCHAR(MAX)) AS [ds] FROM [__temp_test_table_{temp_table_id}]) AS [_subquery] WHERE [ds] BETWEEN '2022-01-01' AND '2022-01-02') AS [__MERGE_SOURCE__] ON (1 = 0) WHEN NOT MATCHED BY SOURCE AND [ds] BETWEEN '2022-01-01' AND '2022-01-02' THEN DELETE WHEN NOT MATCHED THEN INSERT ([a], [ds]) VALUES ([a], [ds]);""", - f"DROP TABLE IF EXISTS [__temp_test_table_{temp_table_id}];", - ] - - def test_insert_append_pandas( make_mocked_engine_adapter: t.Callable, mocker: MockerFixture, make_temp_table_name: t.Callable ): From e949176a6bd907bcf6f0e054968e6c5c822f9a98 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:46:32 -0700 Subject: [PATCH 0870/1056] feat: add catalog type overrides (#5382) --- docs/integrations/engines/trino.md | 47 +++++++++++----- sqlmesh/core/config/connection.py | 2 + sqlmesh/core/engine_adapter/base.py | 10 +++- sqlmesh/core/engine_adapter/trino.py | 4 +- tests/cli/test_cli.py | 11 ++-- tests/core/engine_adapter/test_trino.py | 72 +++++++++++++++++++++++++ tests/core/test_connection_config.py | 19 +++++++ 7 files changed, 145 insertions(+), 20 deletions(-) diff --git a/docs/integrations/engines/trino.md b/docs/integrations/engines/trino.md index c590ee32ba..ec1139e20d 100644 --- a/docs/integrations/engines/trino.md +++ b/docs/integrations/engines/trino.md @@ -81,19 +81,21 @@ hive.metastore.glue.default-warehouse-dir=s3://my-bucket/ ### Connection options -| Option | Description | Type | Required | -|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------:|:--------:| -| `type` | Engine type name - must be `trino` | string | Y | -| `user` | The username (of the account) to log in to your cluster. When connecting to Starburst Galaxy clusters, you must include the role of the user as a suffix to the username. | string | Y | -| `host` | The hostname of your cluster. Don't include the `http://` or `https://` prefix. | string | Y | -| `catalog` | The name of a catalog in your cluster. | string | Y | -| `http_scheme` | The HTTP scheme to use when connecting to your cluster. By default, it's `https` and can only be `http` for no-auth or basic auth. | string | N | -| `port` | The port to connect to your cluster. By default, it's `443` for `https` scheme and `80` for `http` | int | N | -| `roles` | Mapping of catalog name to a role | dict | N | -| `http_headers` | Additional HTTP headers to send with each request. | dict | N | -| `session_properties` | Trino session properties. Run `SHOW SESSION` to see all options. | dict | N | -| `retries` | Number of retries to attempt when a request fails. Default: `3` | int | N | -| `timezone` | Timezone to use for the connection. Default: client-side local timezone | string | N | +| Option | Description | Type | Required | +|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------:|:--------:| +| `type` | Engine type name - must be `trino` | string | Y | +| `user` | The username (of the account) to log in to your cluster. When connecting to Starburst Galaxy clusters, you must include the role of the user as a suffix to the username. | string | Y | +| `host` | The hostname of your cluster. Don't include the `http://` or `https://` prefix. | string | Y | +| `catalog` | The name of a catalog in your cluster. | string | Y | +| `http_scheme` | The HTTP scheme to use when connecting to your cluster. By default, it's `https` and can only be `http` for no-auth or basic auth. | string | N | +| `port` | The port to connect to your cluster. By default, it's `443` for `https` scheme and `80` for `http` | int | N | +| `roles` | Mapping of catalog name to a role | dict | N | +| `http_headers` | Additional HTTP headers to send with each request. | dict | N | +| `session_properties` | Trino session properties. Run `SHOW SESSION` to see all options. | dict | N | +| `retries` | Number of retries to attempt when a request fails. Default: `3` | int | N | +| `timezone` | Timezone to use for the connection. Default: client-side local timezone | string | N | +| `schema_location_mapping` | A mapping of regex patterns to S3 locations to use for the `LOCATION` property when creating schemas. See [Table and Schema locations](#table-and-schema-locations) for more details. | dict | N | +| `catalog_type_overrides` | A mapping of catalog names to their connector type. This is used to enable/disable connector specific behavior. See [Catalog Type Overrides](#catalog-type-overrides) for more details. | dict | N | ## Table and Schema locations @@ -204,6 +206,25 @@ SELECT ... This will cause SQLMesh to set the specified `LOCATION` when issuing a `CREATE TABLE` statement. +## Catalog Type Overrides + +SQLMesh attempts to determine the connector type of a catalog by querying the `system.metadata.catalogs` table and checking the `connector_name` column. +It checks if the connector name is `hive` for Hive connector behavior or contains `iceberg` or `delta_lake` for Iceberg or Delta Lake connector behavior respectively. +However, the connector name may not always be a reliable way to determine the connector type, for example when using a custom connector or a fork of an existing connector. +To handle such cases, you can use the `catalog_type_overrides` connection property to explicitly specify the connector type for specific catalogs. +For example, to specify that the `datalake` catalog is using the Iceberg connector and the `analytics` catalog is using the Hive connector, you can configure the connection as follows: + +```yaml title="config.yaml" +gateways: + trino: + connection: + type: trino + ... + catalog_type_overrides: + datalake: iceberg + analytics: hive +``` + ## Authentication === "No Auth" diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 1678f5d147..553ffd58a5 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -101,6 +101,7 @@ class ConnectionConfig(abc.ABC, BaseConfig): pre_ping: bool pretty_sql: bool = False schema_differ_overrides: t.Optional[t.Dict[str, t.Any]] = None + catalog_type_overrides: t.Optional[t.Dict[str, str]] = None # Whether to share a single connection across threads or create a new connection per thread. shared_connection: t.ClassVar[bool] = False @@ -176,6 +177,7 @@ def create_engine_adapter( pretty_sql=self.pretty_sql, shared_connection=self.shared_connection, schema_differ_overrides=self.schema_differ_overrides, + catalog_type_overrides=self.catalog_type_overrides, **self._extra_engine_config, ) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 94900f0193..e046dc9b4d 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -223,6 +223,10 @@ def schema_differ(self) -> SchemaDiffer: } ) + @property + def _catalog_type_overrides(self) -> t.Dict[str, str]: + return self._extra_config.get("catalog_type_overrides") or {} + @classmethod def _casted_columns( cls, @@ -430,7 +434,11 @@ def get_catalog_type(self, catalog: t.Optional[str]) -> str: raise UnsupportedCatalogOperationError( f"{self.dialect} does not support catalogs and a catalog was provided: {catalog}" ) - return self.DEFAULT_CATALOG_TYPE + return ( + self._catalog_type_overrides.get(catalog, self.DEFAULT_CATALOG_TYPE) + if catalog + else self.DEFAULT_CATALOG_TYPE + ) def get_catalog_type_from_table(self, table: TableName) -> str: """Get the catalog type from a table name if it has a catalog specified, otherwise return the current catalog type""" diff --git a/sqlmesh/core/engine_adapter/trino.py b/sqlmesh/core/engine_adapter/trino.py index 0e6853dd4a..21846b8693 100644 --- a/sqlmesh/core/engine_adapter/trino.py +++ b/sqlmesh/core/engine_adapter/trino.py @@ -71,7 +71,7 @@ class TrinoEngineAdapter( MAX_TIMESTAMP_PRECISION = 3 @property - def schema_location_mapping(self) -> t.Optional[dict[re.Pattern, str]]: + def schema_location_mapping(self) -> t.Optional[t.Dict[re.Pattern, str]]: return self._extra_config.get("schema_location_mapping") @property @@ -86,6 +86,8 @@ def set_current_catalog(self, catalog: str) -> None: def get_catalog_type(self, catalog: t.Optional[str]) -> str: row: t.Tuple = tuple() if catalog: + if catalog_type_override := self._catalog_type_overrides.get(catalog): + return catalog_type_override row = ( self.fetchone( f"select connector_name from system.metadata.catalogs where catalog_name='{catalog}'" diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index e460387bbc..ba987d76da 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -957,6 +957,7 @@ def test_dlt_filesystem_pipeline(tmp_path): " # pre_ping: False\n" " # pretty_sql: False\n" " # schema_differ_overrides: \n" + " # catalog_type_overrides: \n" " # aws_access_key_id: \n" " # aws_secret_access_key: \n" " # role_arn: \n" @@ -1960,11 +1961,11 @@ def test_init_dbt_template(runner: CliRunner, tmp_path: Path): @time_machine.travel(FREEZE_TIME) def test_init_project_engine_configs(tmp_path): engine_type_to_config = { - "redshift": "# concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # user: \n # password: \n # database: \n # host: \n # port: \n # source_address: \n # unix_sock: \n # ssl: \n # sslmode: \n # timeout: \n # tcp_keepalive: \n # application_name: \n # preferred_role: \n # principal_arn: \n # credentials_provider: \n # region: \n # cluster_identifier: \n # iam: \n # is_serverless: \n # serverless_acct_id: \n # serverless_work_group: \n # enable_merge: ", - "bigquery": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # method: oauth\n # project: \n # execution_project: \n # quota_project: \n # location: \n # keyfile: \n # keyfile_json: \n # token: \n # refresh_token: \n # client_id: \n # client_secret: \n # token_uri: \n # scopes: \n # impersonated_service_account: \n # job_creation_timeout_seconds: \n # job_execution_timeout_seconds: \n # job_retries: 1\n # job_retry_deadline_seconds: \n # priority: \n # maximum_bytes_billed: ", - "snowflake": "account: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # user: \n # password: \n # warehouse: \n # database: \n # role: \n # authenticator: \n # token: \n # host: \n # port: \n # application: Tobiko_SQLMesh\n # private_key: \n # private_key_path: \n # private_key_passphrase: \n # session_parameters: ", - "databricks": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # server_hostname: \n # http_path: \n # access_token: \n # auth_type: \n # oauth_client_id: \n # oauth_client_secret: \n # catalog: \n # http_headers: \n # session_configuration: \n # databricks_connect_server_hostname: \n # databricks_connect_access_token: \n # databricks_connect_cluster_id: \n # databricks_connect_use_serverless: False\n # force_databricks_connect: False\n # disable_databricks_connect: False\n # disable_spark_session: False", - "postgres": "host: \n user: \n password: \n port: \n database: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: True\n # pretty_sql: False\n # schema_differ_overrides: \n # keepalives_idle: \n # connect_timeout: 10\n # role: \n # sslmode: \n # application_name: ", + "redshift": "# concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # catalog_type_overrides: \n # user: \n # password: \n # database: \n # host: \n # port: \n # source_address: \n # unix_sock: \n # ssl: \n # sslmode: \n # timeout: \n # tcp_keepalive: \n # application_name: \n # preferred_role: \n # principal_arn: \n # credentials_provider: \n # region: \n # cluster_identifier: \n # iam: \n # is_serverless: \n # serverless_acct_id: \n # serverless_work_group: \n # enable_merge: ", + "bigquery": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # catalog_type_overrides: \n # method: oauth\n # project: \n # execution_project: \n # quota_project: \n # location: \n # keyfile: \n # keyfile_json: \n # token: \n # refresh_token: \n # client_id: \n # client_secret: \n # token_uri: \n # scopes: \n # impersonated_service_account: \n # job_creation_timeout_seconds: \n # job_execution_timeout_seconds: \n # job_retries: 1\n # job_retry_deadline_seconds: \n # priority: \n # maximum_bytes_billed: ", + "snowflake": "account: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # catalog_type_overrides: \n # user: \n # password: \n # warehouse: \n # database: \n # role: \n # authenticator: \n # token: \n # host: \n # port: \n # application: Tobiko_SQLMesh\n # private_key: \n # private_key_path: \n # private_key_passphrase: \n # session_parameters: ", + "databricks": "# concurrent_tasks: 1\n # register_comments: True\n # pre_ping: False\n # pretty_sql: False\n # schema_differ_overrides: \n # catalog_type_overrides: \n # server_hostname: \n # http_path: \n # access_token: \n # auth_type: \n # oauth_client_id: \n # oauth_client_secret: \n # catalog: \n # http_headers: \n # session_configuration: \n # databricks_connect_server_hostname: \n # databricks_connect_access_token: \n # databricks_connect_cluster_id: \n # databricks_connect_use_serverless: False\n # force_databricks_connect: False\n # disable_databricks_connect: False\n # disable_spark_session: False", + "postgres": "host: \n user: \n password: \n port: \n database: \n # concurrent_tasks: 4\n # register_comments: True\n # pre_ping: True\n # pretty_sql: False\n # schema_differ_overrides: \n # catalog_type_overrides: \n # keepalives_idle: \n # connect_timeout: 10\n # role: \n # sslmode: \n # application_name: ", } for engine_type, expected_config in engine_type_to_config.items(): diff --git a/tests/core/engine_adapter/test_trino.py b/tests/core/engine_adapter/test_trino.py index 07c4657eb3..526cb05b04 100644 --- a/tests/core/engine_adapter/test_trino.py +++ b/tests/core/engine_adapter/test_trino.py @@ -11,6 +11,7 @@ from sqlmesh.core.model import load_sql_based_model from sqlmesh.core.model.definition import SqlModel from sqlmesh.core.dialect import schema_ +from sqlmesh.utils.date import to_ds from sqlmesh.utils.errors import SQLMeshError from tests.core.engine_adapter import to_sql_calls @@ -683,3 +684,74 @@ def test_replace_table_catalog_support( sql_calls[0] == f'CREATE TABLE IF NOT EXISTS "{catalog_name}"."schema"."test_table" AS SELECT 1 AS "col"' ) + + +@pytest.mark.parametrize( + "catalog_type_overrides", [{}, {"my_catalog": "hive"}, {"other_catalog": "iceberg"}] +) +def test_insert_overwrite_time_partition_hive( + make_mocked_engine_adapter: t.Callable, catalog_type_overrides: t.Dict[str, str] +): + config = TrinoConnectionConfig( + user="user", + host="host", + catalog="catalog", + catalog_type_overrides=catalog_type_overrides, + ) + adapter: TrinoEngineAdapter = make_mocked_engine_adapter( + TrinoEngineAdapter, catalog_type_overrides=config.catalog_type_overrides + ) + adapter.fetchone = MagicMock(return_value=None) # type: ignore + + adapter.insert_overwrite_by_time_partition( + table_name=".".join(["my_catalog", "schema", "test_table"]), + query_or_df=parse_one("SELECT a, b FROM tbl"), + start="2022-01-01", + end="2022-01-02", + time_column="b", + time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), + target_columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("STRING")}, + ) + + assert to_sql_calls(adapter) == [ + "SET SESSION my_catalog.insert_existing_partitions_behavior='OVERWRITE'", + 'INSERT INTO "my_catalog"."schema"."test_table" ("a", "b") SELECT "a", "b" FROM (SELECT "a", "b" FROM "tbl") AS "_subquery" WHERE "b" BETWEEN \'2022-01-01\' AND \'2022-01-02\'', + "SET SESSION my_catalog.insert_existing_partitions_behavior='APPEND'", + ] + + +@pytest.mark.parametrize( + "catalog_type_overrides", + [ + {"my_catalog": "iceberg"}, + {"my_catalog": "unknown"}, + ], +) +def test_insert_overwrite_time_partition_iceberg( + make_mocked_engine_adapter: t.Callable, catalog_type_overrides: t.Dict[str, str] +): + config = TrinoConnectionConfig( + user="user", + host="host", + catalog="catalog", + catalog_type_overrides=catalog_type_overrides, + ) + adapter: TrinoEngineAdapter = make_mocked_engine_adapter( + TrinoEngineAdapter, catalog_type_overrides=config.catalog_type_overrides + ) + adapter.fetchone = MagicMock(return_value=None) # type: ignore + + adapter.insert_overwrite_by_time_partition( + table_name=".".join(["my_catalog", "schema", "test_table"]), + query_or_df=parse_one("SELECT a, b FROM tbl"), + start="2022-01-01", + end="2022-01-02", + time_column="b", + time_formatter=lambda x, _: exp.Literal.string(to_ds(x)), + target_columns_to_types={"a": exp.DataType.build("INT"), "b": exp.DataType.build("STRING")}, + ) + + assert to_sql_calls(adapter) == [ + 'DELETE FROM "my_catalog"."schema"."test_table" WHERE "b" BETWEEN \'2022-01-01\' AND \'2022-01-02\'', + 'INSERT INTO "my_catalog"."schema"."test_table" ("a", "b") SELECT "a", "b" FROM (SELECT "a", "b" FROM "tbl") AS "_subquery" WHERE "b" BETWEEN \'2022-01-01\' AND \'2022-01-02\'', + ] diff --git a/tests/core/test_connection_config.py b/tests/core/test_connection_config.py index 907d1b70cc..4e71e18148 100644 --- a/tests/core/test_connection_config.py +++ b/tests/core/test_connection_config.py @@ -425,6 +425,25 @@ def test_trino_schema_location_mapping(make_config): assert all((isinstance(v, str) for v in config.schema_location_mapping.values())) +def test_trino_catalog_type_override(make_config): + required_kwargs = dict( + type="trino", + user="user", + host="host", + catalog="catalog", + ) + + config = make_config( + **required_kwargs, + catalog_type_overrides={"my_catalog": "iceberg"}, + ) + + assert config.catalog_type_overrides is not None + assert len(config.catalog_type_overrides) == 1 + + assert config.catalog_type_overrides == {"my_catalog": "iceberg"} + + def test_duckdb(make_config): config = make_config( type="duckdb", From 8dd5e38227878f7f5a249a4b0b49e60062e57427 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 16 Sep 2025 10:47:41 +1200 Subject: [PATCH 0871/1056] Feat: Improve CLI and --explain output for restatements (#5348) --- sqlmesh/core/console.py | 29 ++++++++++++++++++++- sqlmesh/core/plan/builder.py | 1 + sqlmesh/core/plan/definition.py | 9 +++++++ sqlmesh/core/plan/explainer.py | 45 +++++++++++++++++++++++++-------- tests/cli/test_cli.py | 2 +- tests/core/test_plan_stages.py | 6 +++-- 6 files changed, 77 insertions(+), 15 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 3b6cb1ce07..d9567ae484 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -2022,7 +2022,34 @@ def _prompt_categorize( plan = plan_builder.build() if plan.restatements: - self._print("\n[bold]Restating models\n") + # A plan can have restatements for the following reasons: + # - The user specifically called `sqlmesh plan` with --restate-model. + # This creates a "restatement plan" which disallows all other changes and simply force-backfills + # the selected models and their downstream dependencies using the versions of the models stored in state. + # - There are no specific restatements (so changes are allowed) AND dev previews need to be computed. + # The "restatements" feature is currently reused for dev previews. + if plan.selected_models_to_restate: + # There were legitimate restatements, no dev previews + tree = Tree( + "[bold]Models selected for restatement:[/bold]\n" + "This causes backfill of the model itself as well as affected downstream models" + ) + model_fqn_to_snapshot = {s.name: s for s in plan.snapshots.values()} + for model_fqn in plan.selected_models_to_restate: + snapshot = model_fqn_to_snapshot[model_fqn] + display_name = snapshot.display_name( + plan.environment_naming_info, + default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None, + dialect=self.dialect, + ) + tree.add( + display_name + ) # note: we deliberately dont show any intervals here; they get shown in the backfill section + self._print(tree) + else: + # We are computing dev previews, do not confuse the user by printing out something to do + # with restatements. Dev previews are already highlighted in the backfill step + pass else: self.show_environment_difference_summary( plan.context_diff, diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index a84b3b60dc..79af460d1d 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -338,6 +338,7 @@ def build(self) -> Plan: directly_modified=directly_modified, indirectly_modified=indirectly_modified, deployability_index=deployability_index, + selected_models_to_restate=self._restate_models, restatements=restatements, start_override_per_model=self._start_override_per_model, end_override_per_model=end_override_per_model, diff --git a/sqlmesh/core/plan/definition.py b/sqlmesh/core/plan/definition.py index aaf6ec5dc0..5ed3e4b188 100644 --- a/sqlmesh/core/plan/definition.py +++ b/sqlmesh/core/plan/definition.py @@ -58,7 +58,16 @@ class Plan(PydanticModel, frozen=True): indirectly_modified: t.Dict[SnapshotId, t.Set[SnapshotId]] deployability_index: DeployabilityIndex + selected_models_to_restate: t.Optional[t.Set[str]] = None + """Models that have been explicitly selected for restatement by a user""" restatements: t.Dict[SnapshotId, Interval] + """ + All models being restated, which are typically the explicitly selected ones + their downstream dependencies. + + Note that dev previews are also considered restatements, so :selected_models_to_restate can be empty + while :restatements is still populated with dev previews + """ + start_override_per_model: t.Optional[t.Dict[str, datetime]] end_override_per_model: t.Optional[t.Dict[str, datetime]] diff --git a/sqlmesh/core/plan/explainer.py b/sqlmesh/core/plan/explainer.py index b722d00d58..f0a1e44aff 100644 --- a/sqlmesh/core/plan/explainer.py +++ b/sqlmesh/core/plan/explainer.py @@ -4,6 +4,7 @@ import typing as t import logging from dataclasses import dataclass +from collections import defaultdict from rich.console import Console as RichConsole from rich.tree import Tree @@ -21,7 +22,11 @@ PlanEvaluator, ) from sqlmesh.core.state_sync import StateReader -from sqlmesh.core.snapshot.definition import SnapshotInfoMixin, SnapshotIdAndVersion +from sqlmesh.core.snapshot.definition import ( + SnapshotInfoMixin, + SnapshotIdAndVersion, + model_display_name, +) from sqlmesh.utils import Verbosity, rich as srich, to_snake_case from sqlmesh.utils.date import to_ts from sqlmesh.utils.errors import SQLMeshError @@ -75,8 +80,8 @@ class ExplainableRestatementStage(stages.RestatementStage): of what might happen when they ask for the plan to be explained """ - snapshot_intervals_to_clear: t.Dict[str, SnapshotIntervalClearRequest] - """Which snapshots from other environments would have intervals cleared as part of restatement, keyed by name""" + snapshot_intervals_to_clear: t.Dict[str, t.List[SnapshotIntervalClearRequest]] + """Which snapshots from other environments would have intervals cleared as part of restatement, grouped by name.""" @classmethod def from_restatement_stage( @@ -92,10 +97,13 @@ def from_restatement_stage( loaded_snapshots={s.snapshot_id: s for s in stage.all_snapshots.values()}, ) + # Group the interval clear requests by snapshot name to make them easier to write to the console + snapshot_intervals_to_clear = defaultdict(list) + for clear_request in all_restatement_intervals.values(): + snapshot_intervals_to_clear[clear_request.snapshot.name].append(clear_request) + return cls( - snapshot_intervals_to_clear={ - s.snapshot.name: s for s in all_restatement_intervals.values() - }, + snapshot_intervals_to_clear=snapshot_intervals_to_clear, all_snapshots=stage.all_snapshots, ) @@ -198,15 +206,30 @@ def visit_explainable_restatement_stage(self, stage: ExplainableRestatementStage def visit_restatement_stage( self, stage: t.Union[ExplainableRestatementStage, stages.RestatementStage] ) -> Tree: - tree = Tree("[bold]Invalidate data intervals as part of restatement[/bold]") + tree = Tree( + "[bold]Invalidate data intervals in state for development environments to prevent old data from being promoted[/bold]\n" + "This only affects state and will not clear physical data from the tables until the next plan for each environment" + ) if isinstance(stage, ExplainableRestatementStage) and ( snapshot_intervals := stage.snapshot_intervals_to_clear ): - for clear_request in snapshot_intervals.values(): - display_name = self._display_name(clear_request.snapshot) - interval = clear_request.interval - tree.add(f"{display_name} [{to_ts(interval[0])} - {to_ts(interval[1])}]") + for name, clear_requests in snapshot_intervals.items(): + display_name = model_display_name( + name, self.environment_naming_info, self.default_catalog, self.dialect + ) + interval_start = min(cr.interval[0] for cr in clear_requests) + interval_end = max(cr.interval[1] for cr in clear_requests) + + if not interval_start or not interval_end: + continue + + node = tree.add(f"{display_name} [{to_ts(interval_start)} - {to_ts(interval_end)}]") + + all_environment_names = sorted( + set(env_name for cr in clear_requests for env_name in cr.environment_names) + ) + node.add("in environments: " + ", ".join(all_environment_names)) return tree diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index ba987d76da..1be44e18f9 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -247,7 +247,7 @@ def test_plan_restate_model(runner, tmp_path): ) assert result.exit_code == 0 assert_duckdb_test(result) - assert "Restating models" in result.output + assert "Models selected for restatement" in result.output assert "sqlmesh_example.full_model [full refresh" in result.output assert_model_batches_executed(result) assert "Virtual layer updated" not in result.output diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index 4ada7d458d..444ce1bb9b 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -771,9 +771,11 @@ def _get_snapshots(snapshot_ids: t.Iterable[SnapshotIdLike]): # note: we only clear the intervals from state for "a" in dev, we leave prod alone assert restatement_stage.snapshot_intervals_to_clear assert len(restatement_stage.snapshot_intervals_to_clear) == 1 - snapshot_name, clear_request = list(restatement_stage.snapshot_intervals_to_clear.items())[0] - assert isinstance(clear_request, SnapshotIntervalClearRequest) + snapshot_name, clear_requests = list(restatement_stage.snapshot_intervals_to_clear.items())[0] assert snapshot_name == '"a"' + assert len(clear_requests) == 1 + clear_request = clear_requests[0] + assert isinstance(clear_request, SnapshotIntervalClearRequest) assert clear_request.snapshot_id == snapshot_a_dev.snapshot_id assert clear_request.snapshot == snapshot_a_dev.id_and_version assert clear_request.interval == (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")) From d7dda8f75a30d3c94a38ec6db9b91b9dbf96df64 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 15 Sep 2025 17:06:13 -0700 Subject: [PATCH 0872/1056] Fix: Modifying a standalone audit when using the dev-only virtual environment mode (#5383) --- sqlmesh/core/plan/common.py | 7 +++++++ tests/core/test_integration.py | 27 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/plan/common.py b/sqlmesh/core/plan/common.py index 4ae8a3112c..99601cb484 100644 --- a/sqlmesh/core/plan/common.py +++ b/sqlmesh/core/plan/common.py @@ -23,6 +23,13 @@ def should_force_rebuild(old: Snapshot, new: Snapshot) -> bool: def is_breaking_kind_change(old: Snapshot, new: Snapshot) -> bool: + if new.is_model != old.is_model: + # If one is a model and the other isn't, then we need to rebuild + return True + if not new.is_model or not old.is_model: + # If neither are models, then we don't need to rebuild + # Note that the remaining checks only apply to model snapshots + return False if old.virtual_environment_mode != new.virtual_environment_mode: # If the virtual environment mode has changed, then we need to rebuild return True diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 0fad472cd5..c2c11ced80 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -3116,7 +3116,32 @@ def test_virtual_environment_mode_dev_only_model_change_downstream_of_seed( # Make sure there's no error when applying the plan context.apply(plan) - context.plan("prod", auto_apply=True, no_prompts=True) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_change_standalone_audit( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.apply(plan) + + # Change a model upstream from a standalone audit + model = context.get_model("sushi.items") + model = model.copy(update={"stamp": "force new version"}) + context.upsert_model(model) + + plan = context.plan_builder("prod", skip_tests=True).build() + + # Make sure the standalone audit is among modified + assert ( + context.get_snapshot("assert_item_price_above_zero").snapshot_id + in plan.indirectly_modified[context.get_snapshot("sushi.items").snapshot_id] + ) + + # Make sure there's no error when applying the plan + context.apply(plan) @time_machine.travel("2023-01-08 15:00:00 UTC") From dbc7de68495f30fa0d99ad5736103225a8c0d8f5 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 16 Sep 2025 10:49:59 -0700 Subject: [PATCH 0873/1056] Fix: Regression that caused view snapshos not to be migrated (#5389) --- pyproject.toml | 3 ++- sqlmesh/core/plan/stages.py | 11 +++++++- sqlmesh/core/snapshot/definition.py | 20 +++++++-------- sqlmesh/core/snapshot/evaluator.py | 25 +++++++++++++------ tests/core/test_context.py | 2 +- tests/core/test_integration.py | 20 +++++++++++++++ tests/core/test_plan_stages.py | 11 ++++---- tests/core/test_snapshot_evaluator.py | 8 +++++- .../sushi_test/models/model_with_raw_code.sql | 2 +- 9 files changed, 75 insertions(+), 27 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6823f7750b..4b526527fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -225,7 +225,8 @@ module = [ "pydantic_core.*", "dlt.*", "bigframes.*", - "json_stream.*" + "json_stream.*", + "duckdb.*" ] ignore_missing_imports = true diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 0d829a6739..29b34d0fe0 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -12,6 +12,7 @@ Snapshot, SnapshotTableInfo, SnapshotId, + snapshots_to_dag, ) @@ -248,6 +249,7 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: stored_snapshots = self.state_reader.get_snapshots(plan.environment.snapshots) snapshots = {**new_snapshots, **stored_snapshots} snapshots_by_name = {s.name: s for s in snapshots.values()} + dag = snapshots_to_dag(snapshots.values()) all_selected_for_backfill_snapshots = { s.snapshot_id for s in snapshots.values() if plan.is_selected_for_backfill(s.name) @@ -271,8 +273,15 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: after_promote_snapshots = all_selected_for_backfill_snapshots - before_promote_snapshots deployability_index = DeployabilityIndex.all_deployable() + snapshot_ids_with_schema_migration = [ + s.snapshot_id for s in snapshots.values() if s.requires_schema_migration_in_prod + ] + # Include all upstream dependencies of snapshots that require schema migration to make sure + # the upstream tables are created before the schema updates are applied snapshots_with_schema_migration = [ - s for s in snapshots.values() if s.requires_schema_migration_in_prod + snapshots[s_id] + for s_id in dag.subdag(*snapshot_ids_with_schema_migration) + if snapshots[s_id].supports_schema_migration_in_prod ] snapshots_to_intervals = self._missing_intervals( diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 9522366721..0d64736ffb 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1477,19 +1477,19 @@ def expiration_ts(self) -> int: check_categorical_relative_expression=False, ) + @property + def supports_schema_migration_in_prod(self) -> bool: + """Returns whether or not this snapshot supports schema migration when deployed to production.""" + return self.is_paused and self.is_model and not self.is_symbolic + @property def requires_schema_migration_in_prod(self) -> bool: """Returns whether or not this snapshot requires a schema migration when deployed to production.""" - return ( - self.is_paused - and self.is_model - and self.is_materialized - and ( - (self.previous_version and self.previous_version.version == self.version) - or self.model.forward_only - or bool(self.model.physical_version) - or not self.virtual_environment_mode.is_full - ) + return self.supports_schema_migration_in_prod and ( + (self.previous_version and self.previous_version.version == self.version) + or self.model.forward_only + or bool(self.model.physical_version) + or not self.virtual_environment_mode.is_full ) @property diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 8528dd4d1c..4d5023e901 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -489,15 +489,14 @@ def migrate( allow_destructive_snapshots = allow_destructive_snapshots or set() allow_additive_snapshots = allow_additive_snapshots or set() snapshots_by_name = {s.name: s for s in snapshots.values()} - snapshots_with_data_objects = [snapshots[s_id] for s_id in target_data_objects] with self.concurrent_context(): # Only migrate snapshots for which there's an existing data object concurrent_apply_to_snapshots( - snapshots_with_data_objects, + snapshots_by_name.values(), lambda s: self._migrate_snapshot( s, snapshots_by_name, - target_data_objects[s.snapshot_id], + target_data_objects.get(s.snapshot_id), allow_destructive_snapshots, allow_additive_snapshots, self.get_adapter(s.model_gateway), @@ -1059,7 +1058,7 @@ def _migrate_snapshot( adapter: EngineAdapter, deployability_index: DeployabilityIndex, ) -> None: - if not snapshot.requires_schema_migration_in_prod: + if not snapshot.is_model or snapshot.is_symbolic: return deployability_index = DeployabilityIndex.all_deployable() @@ -1081,6 +1080,10 @@ def _migrate_snapshot( ): table_exists = False + rendered_physical_properties = snapshot.model.render_physical_properties( + **render_kwargs + ) + if table_exists: self._migrate_target_table( target_table_name=target_table_name, @@ -1088,13 +1091,21 @@ def _migrate_snapshot( snapshots=snapshots, deployability_index=deployability_index, render_kwargs=render_kwargs, - rendered_physical_properties=snapshot.model.render_physical_properties( - **render_kwargs - ), + rendered_physical_properties=rendered_physical_properties, allow_destructive_snapshots=allow_destructive_snapshots, allow_additive_snapshots=allow_additive_snapshots, run_pre_post_statements=True, ) + else: + self._execute_create( + snapshot=snapshot, + table_name=snapshot.table_name(is_deployable=True), + is_table_deployable=True, + deployability_index=deployability_index, + create_render_kwargs=render_kwargs, + rendered_physical_properties=rendered_physical_properties, + dry_run=True, + ) def _migrate_target_table( self, diff --git a/tests/core/test_context.py b/tests/core/test_context.py index a9d6f7967f..b7ce64eb4c 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1593,7 +1593,7 @@ def test_raw_code_handling(sushi_test_dbt_context: Context): hook = model.render_pre_statements()[0] assert ( hook.sql() - == f'''CREATE TABLE "t" AS SELECT 'Length is {raw_code_length}' AS "length_col"''' + == f'''CREATE TABLE IF NOT EXISTS "t" AS SELECT 'Length is {raw_code_length}' AS "length_col"''' ) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index c2c11ced80..0d6990e17b 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -942,6 +942,26 @@ def test_forward_only_parent_created_in_dev_child_created_in_prod( context.apply(plan) +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_forward_only_view_migration( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + model = context.get_model("sushi.top_waiters") + assert model.kind.is_view + model = add_projection_to_model(t.cast(SqlModel, model)) + context.upsert_model(model) + + # Apply a forward-only plan + context.plan("prod", skip_tests=True, no_prompts=True, auto_apply=True, forward_only=True) + + # Make sure that the new column got reflected in the view schema + df = context.fetchdf("SELECT one FROM sushi.top_waiters LIMIT 1") + assert len(df) == 1 + + @time_machine.travel("2023-01-08 00:00:00 UTC") def test_new_forward_only_model(init_and_plan_context: t.Callable): context, _ = init_and_plan_context("examples/sushi") diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index 444ce1bb9b..1a049490fd 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -1661,16 +1661,17 @@ def test_build_plan_stages_indirect_non_breaking_view_migration( stages = build_plan_stages(plan, state_reader, None) # Verify stages - assert len(stages) == 8 + assert len(stages) == 9 assert isinstance(stages[0], CreateSnapshotRecordsStage) assert isinstance(stages[1], PhysicalLayerSchemaCreationStage) assert isinstance(stages[2], BackfillStage) assert isinstance(stages[3], EnvironmentRecordUpdateStage) - assert isinstance(stages[4], UnpauseStage) - assert isinstance(stages[5], BackfillStage) - assert isinstance(stages[6], VirtualLayerUpdateStage) - assert isinstance(stages[7], FinalizeEnvironmentStage) + assert isinstance(stages[4], MigrateSchemasStage) + assert isinstance(stages[5], UnpauseStage) + assert isinstance(stages[6], BackfillStage) + assert isinstance(stages[7], VirtualLayerUpdateStage) + assert isinstance(stages[8], FinalizeEnvironmentStage) def test_build_plan_stages_virtual_environment_mode_filtering( diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 6a39f600de..8003c6014e 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -1402,7 +1402,13 @@ def test_migrate_view( evaluator = SnapshotEvaluator(adapter) evaluator.migrate([snapshot], {}) - adapter.cursor.execute.assert_not_called() + adapter.cursor.execute.assert_has_calls( + [ + call( + f'CREATE OR REPLACE VIEW "sqlmesh__test_schema"."test_schema__test_model__{snapshot.version}" ("c", "a") AS SELECT "c" AS "c", "a" AS "a" FROM "tbl" AS "tbl"' + ), + ] + ) def test_migrate_snapshot_data_object_type_mismatch( diff --git a/tests/fixtures/dbt/sushi_test/models/model_with_raw_code.sql b/tests/fixtures/dbt/sushi_test/models/model_with_raw_code.sql index 386e7f40ef..1424f6e970 100644 --- a/tests/fixtures/dbt/sushi_test/models/model_with_raw_code.sql +++ b/tests/fixtures/dbt/sushi_test/models/model_with_raw_code.sql @@ -1,6 +1,6 @@ {{ config( - pre_hook=['CREATE TABLE t AS SELECT \'Length is {{ model.raw_code|length }}\' AS length_col'] + pre_hook=['CREATE TABLE IF NOT EXISTS t AS SELECT \'Length is {{ model.raw_code|length }}\' AS length_col'] ) }} From d26c779bc4ae46276239c57effad3db11fd88501 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 17 Sep 2025 13:14:38 +1200 Subject: [PATCH 0874/1056] Feat(sqlmesh_dbt): Implement --full-refresh (#5370) --- sqlmesh/core/context.py | 23 +++++- sqlmesh/core/plan/builder.py | 16 ++-- sqlmesh/core/plan/definition.py | 4 + sqlmesh/core/plan/stages.py | 18 +++-- sqlmesh_dbt/cli.py | 4 +- sqlmesh_dbt/operations.py | 79 ++++++++++++++---- tests/core/test_plan.py | 31 +------ tests/core/test_plan_stages.py | 21 +++++ tests/dbt/cli/conftest.py | 59 +++++++++++++- .../fixtures/empty_project/dbt_project.yml | 18 +++++ .../cli/fixtures/empty_project/profiles.yml | 9 +++ tests/dbt/cli/test_operations.py | 80 ++++++++++++++++++- tests/dbt/cli/test_run.py | 39 +++++++++ 13 files changed, 333 insertions(+), 68 deletions(-) create mode 100644 tests/dbt/cli/fixtures/empty_project/dbt_project.yml create mode 100644 tests/dbt/cli/fixtures/empty_project/profiles.yml diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 0339f6506c..d7a2984f3a 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1429,6 +1429,7 @@ def plan_builder( explain: t.Optional[bool] = None, ignore_cron: t.Optional[bool] = None, min_intervals: t.Optional[int] = None, + always_include_local_changes: t.Optional[bool] = None, ) -> PlanBuilder: """Creates a plan builder. @@ -1467,6 +1468,8 @@ def plan_builder( diff_rendered: Whether the diff should compare raw vs rendered models min_intervals: Adjust the plan start date on a per-model basis in order to ensure at least this many intervals are covered on every model when checking for missing intervals + always_include_local_changes: Usually when restatements are present, local changes in the filesystem are ignored. + However, it can be desirable to deploy changes + restatements in the same plan, so this flag overrides the default behaviour. Returns: The plan builder. @@ -1583,13 +1586,20 @@ def plan_builder( "Selector did not return any models. Please check your model selection and try again." ) + if always_include_local_changes is None: + # default behaviour - if restatements are detected; we operate entirely out of state and ignore local changes + force_no_diff = restate_models is not None or ( + backfill_models is not None and not backfill_models + ) + else: + force_no_diff = not always_include_local_changes + snapshots = self._snapshots(models_override) context_diff = self._context_diff( environment or c.PROD, snapshots=snapshots, create_from=create_from, - force_no_diff=restate_models is not None - or (backfill_models is not None and not backfill_models), + force_no_diff=force_no_diff, ensure_finalized_snapshots=self.config.plan.use_finalized_state, diff_rendered=diff_rendered, always_recreate_environment=self.config.plan.always_recreate_environment, @@ -1644,6 +1654,14 @@ def plan_builder( elif forward_only is None: forward_only = self.config.plan.forward_only + # When handling prod restatements, only clear intervals from other model versions if we are using full virtual environments + # If we are not, then there is no point, because none of the data in dev environments can be promoted by definition + restate_all_snapshots = ( + expanded_restate_models is not None + and not is_dev + and self.config.virtual_environment_mode.is_full + ) + return self.PLAN_BUILDER_TYPE( context_diff=context_diff, start=start, @@ -1651,6 +1669,7 @@ def plan_builder( execution_time=execution_time, apply=self.apply, restate_models=expanded_restate_models, + restate_all_snapshots=restate_all_snapshots, backfill_models=backfill_models, no_gaps=no_gaps, skip_backfill=skip_backfill, diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 79af460d1d..2eb4c54aeb 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -65,6 +65,9 @@ class PlanBuilder: restate_models: A list of models for which the data should be restated for the time range specified in this plan. Note: models defined outside SQLMesh (external) won't be a part of the restatement. + restate_all_snapshots: If restatements are present, this flag indicates whether or not the intervals + being restated should be cleared from state for other versions of this model (typically, versions that are present in other environments). + If set to None, the default behaviour is to not clear anything unless the target environment is prod. backfill_models: A list of fully qualified model names for which the data should be backfilled as part of this plan. no_gaps: Whether to ensure that new snapshots for nodes that are already a part of the target environment have no data gaps when compared against previous @@ -103,6 +106,7 @@ def __init__( execution_time: t.Optional[TimeLike] = None, apply: t.Optional[t.Callable[[Plan], None]] = None, restate_models: t.Optional[t.Iterable[str]] = None, + restate_all_snapshots: bool = False, backfill_models: t.Optional[t.Iterable[str]] = None, no_gaps: bool = False, skip_backfill: bool = False, @@ -154,6 +158,7 @@ def __init__( self._auto_categorization_enabled = auto_categorization_enabled self._include_unmodified = include_unmodified self._restate_models = set(restate_models) if restate_models is not None else None + self._restate_all_snapshots = restate_all_snapshots self._effective_from = effective_from # note: this deliberately doesnt default to now() here. @@ -277,7 +282,6 @@ def build(self) -> Plan: if self._latest_plan: return self._latest_plan - self._ensure_no_new_snapshots_with_restatements() self._ensure_new_env_with_changes() self._ensure_valid_date_range() self._ensure_no_broken_references() @@ -340,6 +344,7 @@ def build(self) -> Plan: deployability_index=deployability_index, selected_models_to_restate=self._restate_models, restatements=restatements, + restate_all_snapshots=self._restate_all_snapshots, start_override_per_model=self._start_override_per_model, end_override_per_model=end_override_per_model, selected_models_to_backfill=self._backfill_models, @@ -859,15 +864,6 @@ def _ensure_no_broken_references(self) -> None: f"""Removed {broken_references_msg} are referenced in '{snapshot.name}'. Please remove broken references before proceeding.""" ) - def _ensure_no_new_snapshots_with_restatements(self) -> None: - if self._restate_models is not None and ( - self._context_diff.new_snapshots or self._context_diff.modified_snapshots - ): - raise PlanError( - "Model changes and restatements can't be a part of the same plan. " - "Revert or apply changes before proceeding with restatements." - ) - def _ensure_new_env_with_changes(self) -> None: if ( self._is_dev diff --git a/sqlmesh/core/plan/definition.py b/sqlmesh/core/plan/definition.py index 5ed3e4b188..3ed260791a 100644 --- a/sqlmesh/core/plan/definition.py +++ b/sqlmesh/core/plan/definition.py @@ -67,6 +67,8 @@ class Plan(PydanticModel, frozen=True): Note that dev previews are also considered restatements, so :selected_models_to_restate can be empty while :restatements is still populated with dev previews """ + restate_all_snapshots: bool + """Whether or not to clear intervals from state for other versions of the models listed in :restatements""" start_override_per_model: t.Optional[t.Dict[str, datetime]] end_override_per_model: t.Optional[t.Dict[str, datetime]] @@ -268,6 +270,7 @@ def to_evaluatable(self) -> EvaluatablePlan: skip_backfill=self.skip_backfill, empty_backfill=self.empty_backfill, restatements={s.name: i for s, i in self.restatements.items()}, + restate_all_snapshots=self.restate_all_snapshots, is_dev=self.is_dev, allow_destructive_models=self.allow_destructive_models, allow_additive_models=self.allow_additive_models, @@ -312,6 +315,7 @@ class EvaluatablePlan(PydanticModel): skip_backfill: bool empty_backfill: bool restatements: t.Dict[str, Interval] + restate_all_snapshots: bool is_dev: bool allow_destructive_models: t.Set[str] allow_additive_models: t.Set[str] diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 29b34d0fe0..9425608619 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -14,6 +14,7 @@ SnapshotId, snapshots_to_dag, ) +from sqlmesh.utils.errors import PlanError @dataclass @@ -461,13 +462,18 @@ def _get_after_all_stage( def _get_restatement_stage( self, plan: EvaluatablePlan, snapshots_by_name: t.Dict[str, Snapshot] ) -> t.Optional[RestatementStage]: - if not plan.restatements or plan.is_dev: - # The RestatementStage to clear intervals from state across all environments is not needed for plans against dev, only prod - return None + if plan.restate_all_snapshots: + if plan.is_dev: + raise PlanError( + "Clearing intervals from state across dev model versions is only valid for prod plans" + ) - return RestatementStage( - all_snapshots=snapshots_by_name, - ) + if plan.restatements: + return RestatementStage( + all_snapshots=snapshots_by_name, + ) + + return None def _get_physical_layer_update_stage( self, diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py index 370f115d61..fa75d303a1 100644 --- a/sqlmesh_dbt/cli.py +++ b/sqlmesh_dbt/cli.py @@ -90,7 +90,9 @@ def dbt( @click.option( "-f", "--full-refresh", - help="If specified, dbt will drop incremental models and fully-recalculate the incremental table from the model definition.", + is_flag=True, + default=False, + help="If specified, sqlmesh will drop incremental models and fully-recalculate the incremental table from the model definition.", ) @click.option( "--env", diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index ac7ad031f3..f95d0d931e 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -11,7 +11,7 @@ from sqlmesh.dbt.project import Project from sqlmesh_dbt.console import DbtCliConsole from sqlmesh.core.model import Model - from sqlmesh.core.plan import Plan + from sqlmesh.core.plan import Plan, PlanBuilder logger = logging.getLogger(__name__) @@ -42,8 +42,39 @@ def run( full_refresh: bool = False, empty: bool = False, ) -> Plan: - return self.context.plan( - **self._plan_options( + plan_builder = self._plan_builder( + environment=environment, + select=select, + exclude=exclude, + full_refresh=full_refresh, + empty=empty, + ) + + plan = plan_builder.build() + + self.console.plan( + plan_builder, + default_catalog=self.context.default_catalog, + # start doing work immediately (since no_diff is set, there isnt really anything for the user to say yes/no to) + auto_apply=True, + # dont output a diff of model changes + no_diff=True, + # don't throw up any prompts like "set the effective date" - use defaults + no_prompts=True, + ) + + return plan + + def _plan_builder( + self, + environment: t.Optional[str] = None, + select: t.Optional[t.List[str]] = None, + exclude: t.Optional[t.List[str]] = None, + full_refresh: bool = False, + empty: bool = False, + ) -> PlanBuilder: + return self.context.plan_builder( + **self._plan_builder_options( environment=environment, select=select, exclude=exclude, @@ -71,13 +102,15 @@ def _selected_models( return selected_models - def _plan_options( + def _plan_builder_options( self, - environment: t.Optional[str] = None, + # upstream dbt options select: t.Optional[t.List[str]] = None, exclude: t.Optional[t.List[str]] = None, empty: bool = False, full_refresh: bool = False, + # sqlmesh extra options + environment: t.Optional[str] = None, ) -> t.Dict[str, t.Any]: import sqlmesh.core.constants as c @@ -130,24 +163,38 @@ def _plan_options( # `dbt --empty` adds LIMIT 0 to the queries, resulting in empty tables. In addition, it happily clobbers existing tables regardless of if they are populated. # This *partially* lines up with --skip-backfill in SQLMesh, which indicates to not populate tables if they happened to be created/updated as part of this plan. # However, if a table already exists and has data in it, there is no change so SQLMesh will not recreate the table and thus it will not be cleared. - # So in order to fully replicate dbt's --empty, we also need --full-refresh semantics in order to replace existing tables + # Currently, SQLMesh has no way to say "restate with empty data", because --restate-model coupled with --skip-backfill ends up being a no-op options["skip_backfill"] = True - full_refresh = True + + self.console.log_warning( + "dbt's `--empty` drops the tables for all selected models and replaces them with empty ones.\n" + "This can easily result in accidental data loss, so SQLMesh limits this to only new or modified models and leaves the tables for existing unmodified models alone.\n\n" + "If you were creating empty tables to preview model changes, please consider using `--environment` to preview these changes in an isolated Virtual Data Environment instead.\n\n" + "Otherwise, if you really do want dbt's `--empty` behaviour of clearing every selected table, please file an issue on GitHub so we can better understand the use-case.\n" + ) + + if full_refresh: + # --full-refresh is implemented in terms of "add every model as a restatement" + # however, `--empty` sets skip_backfill=True, which causes the BackfillStage of the plan to be skipped. + # the re-processing of data intervals happens in the BackfillStage, so if it gets skipped, restatements become a no-op + raise ValueError("`--full-refresh` alongside `--empty` is not currently supported.") if full_refresh: - # TODO: handling this requires some updates in the engine to enable restatements+changes in the same plan without affecting prod - # if the plan targets dev - pass + options.update( + dict( + # Add every selected model as a restatement to force them to get repopulated from scratch + restate_models=list(self.context.models) + if not select_models + else select_models, + # by default in SQLMesh, restatements only operate on what has been committed to state. + # in order to emulate dbt, we need to use the local filesystem instead, so we override this default + always_include_local_changes=True, + ) + ) return dict( environment=environment, select_models=select_models, - # dont output a diff of model changes - no_diff=True, - # don't throw up any prompts like "set the effective date" - use defaults - no_prompts=True, - # start doing work immediately (since no_diff is set, there isnt really anything for the user to say yes/no to) - auto_apply=True, **options, ) diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index c9c19376d9..4f6b99a4ee 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -826,6 +826,7 @@ def test_missing_intervals_lookback(make_snapshot, mocker: MockerFixture): indirectly_modified={}, deployability_index=DeployabilityIndex.all_deployable(), restatements={}, + restate_all_snapshots=False, end_bounded=False, ensure_finalized_snapshots=False, start_override_per_model=None, @@ -1074,36 +1075,6 @@ def test_restate_missing_model(make_snapshot, mocker: MockerFixture): PlanBuilder(context_diff, restate_models=["missing"]).build() -def test_new_snapshots_with_restatements(make_snapshot, mocker: MockerFixture): - snapshot_a = make_snapshot(SqlModel(name="a", query=parse_one("select 1, ds"))) - - context_diff = ContextDiff( - environment="test_environment", - is_new_environment=True, - is_unfinalized_environment=False, - normalize_environment_name=True, - create_from="prod", - create_from_env_exists=True, - added=set(), - removed_snapshots={}, - modified_snapshots={}, - snapshots={snapshot_a.snapshot_id: snapshot_a}, - new_snapshots={snapshot_a.snapshot_id: snapshot_a}, - previous_plan_id=None, - previously_promoted_snapshot_ids=set(), - previous_finalized_snapshots=None, - previous_gateway_managed_virtual_layer=False, - gateway_managed_virtual_layer=False, - environment_statements=[], - ) - - with pytest.raises( - PlanError, - match=r"Model changes and restatements can't be a part of the same plan.*", - ): - PlanBuilder(context_diff, restate_models=["a"]).build() - - def test_end_validation(make_snapshot, mocker: MockerFixture): snapshot_a = make_snapshot( SqlModel( diff --git a/tests/core/test_plan_stages.py b/tests/core/test_plan_stages.py index 1a049490fd..f93a8a4780 100644 --- a/tests/core/test_plan_stages.py +++ b/tests/core/test_plan_stages.py @@ -106,6 +106,7 @@ def test_build_plan_stages_basic( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -215,6 +216,7 @@ def test_build_plan_stages_with_before_all_and_after_all( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -325,6 +327,7 @@ def test_build_plan_stages_select_models( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -426,6 +429,7 @@ def test_build_plan_stages_basic_no_backfill( skip_backfill=skip_backfill, empty_backfill=empty_backfill, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -562,6 +566,7 @@ def test_build_plan_stages_restatement_prod_only( '"a"': (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), '"b"': (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), }, + restate_all_snapshots=True, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -718,6 +723,7 @@ def _get_snapshots(snapshot_ids: t.Iterable[SnapshotIdLike]): '"a"': (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), '"b"': (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), }, + restate_all_snapshots=True, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -881,6 +887,7 @@ def test_build_plan_stages_restatement_dev_does_not_clear_intervals( restatements={ '"a"': (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), }, + restate_all_snapshots=False, is_dev=True, allow_destructive_models=set(), allow_additive_models=set(), @@ -988,6 +995,7 @@ def test_build_plan_stages_forward_only( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -1116,6 +1124,7 @@ def test_build_plan_stages_forward_only_dev( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=True, allow_destructive_models=set(), allow_additive_models=set(), @@ -1241,6 +1250,7 @@ def _get_snapshots(snapshot_ids: t.List[SnapshotId]) -> t.Dict[SnapshotId, Snaps skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=True, allow_destructive_models=set(), allow_additive_models=set(), @@ -1378,6 +1388,7 @@ def test_build_plan_stages_forward_only_ensure_finalized_snapshots( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -1454,6 +1465,7 @@ def test_build_plan_stages_removed_model( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -1537,6 +1549,7 @@ def test_build_plan_stages_environment_suffix_target_changed( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=True, allow_destructive_models=set(), allow_additive_models=set(), @@ -1636,6 +1649,7 @@ def test_build_plan_stages_indirect_non_breaking_view_migration( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -1723,6 +1737,7 @@ def test_build_plan_stages_virtual_environment_mode_filtering( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=True, allow_destructive_models=set(), allow_additive_models=set(), @@ -1776,6 +1791,7 @@ def test_build_plan_stages_virtual_environment_mode_filtering( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -1839,6 +1855,7 @@ def test_build_plan_stages_virtual_environment_mode_filtering( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -1913,6 +1930,7 @@ def test_build_plan_stages_virtual_environment_mode_no_updates( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -1977,6 +1995,7 @@ def test_adjust_intervals_new_forward_only_dev_intervals( skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=True, # Dev environment allow_destructive_models=set(), allow_additive_models=set(), @@ -2046,6 +2065,7 @@ def test_adjust_intervals_restatement_removal( skip_backfill=False, empty_backfill=False, restatements=restatements, + restate_all_snapshots=True, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), @@ -2137,6 +2157,7 @@ def test_adjust_intervals_should_force_rebuild(make_snapshot, mocker: MockerFixt skip_backfill=False, empty_backfill=False, restatements={}, + restate_all_snapshots=False, is_dev=False, allow_destructive_models=set(), allow_additive_models=set(), diff --git a/tests/dbt/cli/conftest.py b/tests/dbt/cli/conftest.py index dfad2f0046..e555f9144a 100644 --- a/tests/dbt/cli/conftest.py +++ b/tests/dbt/cli/conftest.py @@ -3,7 +3,15 @@ import os import functools from click.testing import CliRunner, Result +from sqlmesh_dbt.operations import init_project_if_required import pytest +import uuid + + +class EmptyProjectCreator(t.Protocol): + def __call__( + self, project_name: t.Optional[str] = None, target_name: t.Optional[str] = None + ) -> Path: ... @pytest.fixture @@ -14,7 +22,7 @@ def jaffle_shop_duckdb(copy_to_temp_path: t.Callable[..., t.List[Path]]) -> t.It current_path = os.getcwd() output_path = copy_to_temp_path(paths=fixture_path)[0] - # so that we can invoke commands from the perspective of a user that is alrady in the correct directory + # so that we can invoke commands from the perspective of a user that is already in the correct directory os.chdir(output_path) yield output_path @@ -22,6 +30,55 @@ def jaffle_shop_duckdb(copy_to_temp_path: t.Callable[..., t.List[Path]]) -> t.It os.chdir(current_path) +@pytest.fixture +def create_empty_project( + copy_to_temp_path: t.Callable[..., t.List[Path]], +) -> t.Iterable[t.Callable[..., Path]]: + default_project_name = f"test_{str(uuid.uuid4())[:8]}" + default_target_name = "duckdb" + fixture_path = Path(__file__).parent / "fixtures" / "empty_project" + assert fixture_path.exists() + + current_path = os.getcwd() + + def _create_empty_project( + project_name: t.Optional[str] = None, target_name: t.Optional[str] = None + ) -> Path: + project_name = project_name or default_project_name + target_name = target_name or default_target_name + output_path = copy_to_temp_path(paths=fixture_path)[0] + + dbt_project_yml = output_path / "dbt_project.yml" + profiles_yml = output_path / "profiles.yml" + + assert dbt_project_yml.exists() + assert profiles_yml.exists() + + (output_path / "models").mkdir() + (output_path / "seeds").mkdir() + + dbt_project_yml.write_text( + dbt_project_yml.read_text().replace("empty_project", project_name) + ) + profiles_yml.write_text( + profiles_yml.read_text() + .replace("empty_project", project_name) + .replace("__DEFAULT_TARGET__", target_name) + ) + + init_project_if_required(output_path) + + # so that we can invoke commands from the perspective of a user that is already in the correct directory + os.chdir(output_path) + + return output_path + + yield _create_empty_project + + # cleanup - switch cwd back to original + os.chdir(current_path) + + @pytest.fixture def invoke_cli() -> t.Callable[..., Result]: from sqlmesh_dbt.cli import dbt diff --git a/tests/dbt/cli/fixtures/empty_project/dbt_project.yml b/tests/dbt/cli/fixtures/empty_project/dbt_project.yml new file mode 100644 index 0000000000..beceadcd33 --- /dev/null +++ b/tests/dbt/cli/fixtures/empty_project/dbt_project.yml @@ -0,0 +1,18 @@ +name: 'empty_project' + +config-version: 2 +version: '0.1' + +profile: 'empty_project' + +model-paths: ["models"] +seed-paths: ["seeds"] +test-paths: ["tests"] +analysis-paths: ["analysis"] +macro-paths: ["macros"] + +target-path: "target" +clean-targets: + - "target" + - "dbt_modules" + - "logs" \ No newline at end of file diff --git a/tests/dbt/cli/fixtures/empty_project/profiles.yml b/tests/dbt/cli/fixtures/empty_project/profiles.yml new file mode 100644 index 0000000000..a4f9836b7e --- /dev/null +++ b/tests/dbt/cli/fixtures/empty_project/profiles.yml @@ -0,0 +1,9 @@ +empty_project: + + target: __DEFAULT_TARGET__ + + outputs: + duckdb: + type: duckdb + path: 'empty_project.duckdb' + threads: 4 diff --git a/tests/dbt/cli/test_operations.py b/tests/dbt/cli/test_operations.py index f8ce239d3b..e9c4dc0063 100644 --- a/tests/dbt/cli/test_operations.py +++ b/tests/dbt/cli/test_operations.py @@ -2,17 +2,18 @@ from pathlib import Path import pytest from sqlmesh_dbt.operations import create +from sqlmesh_dbt.console import DbtCliConsole from sqlmesh.utils import yaml from sqlmesh.utils.errors import SQLMeshError import time_machine -from sqlmesh.core.console import NoopConsole from sqlmesh.core.plan import PlanBuilder from sqlmesh.core.config.common import VirtualEnvironmentMode +from tests.dbt.cli.conftest import EmptyProjectCreator pytestmark = pytest.mark.slow -class PlanCapturingConsole(NoopConsole): +class PlanCapturingConsole(DbtCliConsole): def plan( self, plan_builder: PlanBuilder, @@ -257,3 +258,78 @@ def test_run_option_mapping_dev(jaffle_shop_duckdb: Path): '"jaffle_shop"."main"."orders"', '"jaffle_shop"."main"."stg_orders"', } + + +@pytest.mark.parametrize( + "env_name,vde_mode", + [ + ("prod", VirtualEnvironmentMode.DEV_ONLY), + ("prod", VirtualEnvironmentMode.FULL), + ("dev", VirtualEnvironmentMode.DEV_ONLY), + ("dev", VirtualEnvironmentMode.FULL), + ], +) +def test_run_option_full_refresh( + create_empty_project: EmptyProjectCreator, env_name: str, vde_mode: VirtualEnvironmentMode +): + # create config file prior to load + project_path = create_empty_project(project_name="test") + + config_path = project_path / "sqlmesh.yaml" + config = yaml.load(config_path) + config["virtual_environment_mode"] = vde_mode.value + + with config_path.open("w") as f: + yaml.dump(config, f) + + (project_path / "models" / "model_a.sql").write_text("select 1") + (project_path / "models" / "model_b.sql").write_text("select 2") + + operations = create(project_dir=project_path) + + assert operations.context.config.virtual_environment_mode == vde_mode + + console = PlanCapturingConsole() + operations.context.console = console + + plan = operations.run(environment=env_name, full_refresh=True) + + # both models added as backfills + restatements regardless of env / vde mode setting + assert plan.environment.name == env_name + assert len(plan.restatements) == 2 + assert list(plan.restatements)[0].name == '"test"."main"."model_a"' + assert list(plan.restatements)[1].name == '"test"."main"."model_b"' + + assert plan.requires_backfill + assert not plan.empty_backfill + assert not plan.skip_backfill + assert plan.models_to_backfill == set(['"test"."main"."model_a"', '"test"."main"."model_b"']) + + if vde_mode == VirtualEnvironmentMode.DEV_ONLY: + # We do not clear intervals across all model versions in the default DEV_ONLY mode, even when targeting prod, + # because dev data is hardcoded to preview only so by definition and can never be deployed + assert not plan.restate_all_snapshots + else: + if env_name == "prod": + # in FULL mode, we do it for prod + assert plan.restate_all_snapshots + else: + # but not dev + assert not plan.restate_all_snapshots + + +def test_run_option_full_refresh_with_selector(jaffle_shop_duckdb: Path): + operations = create(project_dir=jaffle_shop_duckdb) + assert len(operations.context.models) > 5 + + console = PlanCapturingConsole() + operations.context.console = console + + plan = operations.run(select=["main.stg_customers"], full_refresh=True) + assert len(plan.restatements) == 1 + assert list(plan.restatements)[0].name == '"jaffle_shop"."main"."stg_customers"' + + assert plan.requires_backfill + assert not plan.empty_backfill + assert not plan.skip_backfill + assert plan.models_to_backfill == set(['"jaffle_shop"."main"."stg_customers"']) diff --git a/tests/dbt/cli/test_run.py b/tests/dbt/cli/test_run.py index 4d80514fc8..9af1de8561 100644 --- a/tests/dbt/cli/test_run.py +++ b/tests/dbt/cli/test_run.py @@ -3,7 +3,9 @@ from pathlib import Path from click.testing import Result import time_machine +from sqlmesh_dbt.operations import create from tests.cli.test_cli import FREEZE_TIME +from tests.dbt.cli.conftest import EmptyProjectCreator pytestmark = pytest.mark.slow @@ -38,3 +40,40 @@ def test_run_with_selectors(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[... assert "main.orders" not in result.output assert "Model batches executed" in result.output + + +def test_run_with_changes_and_full_refresh( + create_empty_project: EmptyProjectCreator, invoke_cli: t.Callable[..., Result] +): + project_path = create_empty_project(project_name="test") + + engine_adapter = create(project_path).context.engine_adapter + engine_adapter.execute("create table external_table as select 'foo' as a, 'bar' as b") + + (project_path / "models" / "model_a.sql").write_text("select a, b from external_table") + (project_path / "models" / "model_b.sql").write_text("select a, b from {{ ref('model_a') }}") + + # populate initial env + result = invoke_cli(["run"]) + assert result.exit_code == 0 + assert not result.exception + + assert engine_adapter.fetchall("select a, b from model_b") == [("foo", "bar")] + + engine_adapter.execute("insert into external_table (a, b) values ('baz', 'bing')") + (project_path / "models" / "model_b.sql").write_text( + "select a, b, 'changed' as c from {{ ref('model_a') }}" + ) + + # run with --full-refresh. this should: + # - fully refresh model_a (pick up the new records from external_table) + # - deploy the local change to model_b (introducing the 'changed' column) + result = invoke_cli(["run", "--full-refresh"]) + assert result.exit_code == 0 + assert not result.exception + + assert engine_adapter.fetchall("select a, b from model_a") == [("foo", "bar"), ("baz", "bing")] + assert engine_adapter.fetchall("select a, b, c from model_b") == [ + ("foo", "bar", "changed"), + ("baz", "bing", "changed"), + ] From e46caecec90e9bddcfcce9d656c62b3f8d48538c Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 16 Sep 2025 20:32:17 -0700 Subject: [PATCH 0875/1056] Fix: Create empty physical tables for fully annotated self-referential models (#5391) --- sqlmesh/core/snapshot/evaluator.py | 8 ++++++-- tests/core/test_integration.py | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 4d5023e901..3e622d2dd1 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -746,11 +746,15 @@ def _evaluate_snapshot( adapter.execute(model.render_pre_statements(**render_statements_kwargs)) if not target_table_exists or (model.is_seed and not snapshot.intervals): - columns_to_types_provided = ( + # Only create the empty table if the columns were provided explicitly by the user + should_create_empty_table = ( model.kind.is_materialized and model.columns_to_types_ and columns_to_types_all_known(model.columns_to_types_) ) + if not should_create_empty_table: + # Or if the model is self-referential and its query is fully annotated with types + should_create_empty_table = model.depends_on_self and model.annotated if self._can_clone(snapshot, deployability_index): self._clone_snapshot_in_dev( snapshot=snapshot, @@ -763,7 +767,7 @@ def _evaluate_snapshot( ) runtime_stage = RuntimeStage.EVALUATING target_table_exists = True - elif columns_to_types_provided or model.is_seed or model.kind.is_scd_type_2: + elif should_create_empty_table or model.is_seed or model.kind.is_scd_type_2: self._execute_create( snapshot=snapshot, table_name=target_table_name, diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 0d6990e17b..a0e41f329f 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -980,6 +980,32 @@ def test_new_forward_only_model(init_and_plan_context: t.Callable): assert context.engine_adapter.table_exists(snapshot.table_name(is_deployable=False)) +@time_machine.travel("2023-01-08 00:00:00 UTC") +def test_annotated_self_referential_model(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + # Projections are fully annotated in the query but columns were not specified explicitly + expressions = d.parse( + f""" + MODEL ( + name memory.sushi.test_self_ref, + kind FULL, + start '2023-01-01', + ); + + SELECT 1::INT AS one FROM memory.sushi.test_self_ref; + """ + ) + model = load_sql_based_model(expressions) + assert model.depends_on_self + context.upsert_model(model) + + context.plan("prod", skip_tests=True, no_prompts=True, auto_apply=True) + + df = context.fetchdf("SELECT one FROM memory.sushi.test_self_ref") + assert len(df) == 0 + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_plan_set_choice_is_reflected_in_missing_intervals(init_and_plan_context: t.Callable): context, _ = init_and_plan_context("examples/sushi") From 226dab91640363d572d857c7dac3bf656810cab3 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 17 Sep 2025 21:43:56 +0300 Subject: [PATCH 0876/1056] Chore!: bump sqlglot to v27.15.2 (#5393) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4b526527fc..675cd019ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.14.0", + "sqlglot[rs]~=27.15.3", "tenacity", "time-machine", "json-stream" From 69206c7c518b97fc60998d94d20551a9ed1604f4 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 17 Sep 2025 13:05:15 -0700 Subject: [PATCH 0877/1056] Chore include dev only vde in engine integration test (#5396) --- sqlmesh/core/snapshot/evaluator.py | 3 ++- .../core/engine_adapter/integration/test_integration.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 3e622d2dd1..cdcbdfc078 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -1440,8 +1440,9 @@ def _can_clone(self, snapshot: Snapshot, deployability_index: DeployabilityIndex and adapter.SUPPORTS_CLONING # managed models cannot have their schema mutated because theyre based on queries, so clone + alter wont work and not snapshot.is_managed - # If the deployable table is missing we can't clone it and not deployability_index.is_deployable(snapshot) + # If the deployable table is missing we can't clone it + and adapter.table_exists(snapshot.table_name()) ) def _get_data_objects( diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index e02877e0c6..5a708e1e4c 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -22,6 +22,7 @@ from sqlmesh import Config, Context from sqlmesh.cli.project_init import init_example_project +from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.config.connection import ConnectionConfig import sqlmesh.core.dialect as d from sqlmesh.core.environment import EnvironmentSuffixTarget @@ -1938,7 +1939,12 @@ def test_transaction(ctx: TestContext): ctx.compare_with_current(table, input_data) -def test_sushi(ctx: TestContext, tmp_path: pathlib.Path): +@pytest.mark.parametrize( + "virtual_environment_mode", [VirtualEnvironmentMode.FULL, VirtualEnvironmentMode.DEV_ONLY] +) +def test_sushi( + ctx: TestContext, tmp_path: pathlib.Path, virtual_environment_mode: VirtualEnvironmentMode +): if ctx.mark == "athena_hive": pytest.skip( "Sushi end-to-end tests only need to run once for Athena because sushi needs a hybrid of both Hive and Iceberg" @@ -1984,6 +1990,7 @@ def _mutate_config(gateway: str, config: Config) -> None: ).sql(dialect=config.model_defaults.dialect) for e in before_all ] + config.virtual_environment_mode = virtual_environment_mode context = ctx.create_context(_mutate_config, path=tmp_path, ephemeral_state_connection=False) From df7d61be3a8d5e17c75d1691ca1cff1a438054ec Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Wed, 17 Sep 2025 13:46:27 -0700 Subject: [PATCH 0878/1056] Fix: Make table schema migration retriable (#5395) --- sqlmesh/core/engine_adapter/base.py | 3 +++ sqlmesh/core/engine_adapter/databricks.py | 1 + sqlmesh/core/engine_adapter/snowflake.py | 1 + sqlmesh/core/snapshot/evaluator.py | 14 ++++++++++++-- sqlmesh/utils/errors.py | 4 ++++ tests/core/engine_adapter/test_base.py | 2 +- tests/core/engine_adapter/test_databricks.py | 2 +- tests/core/engine_adapter/test_snowflake.py | 6 +++--- tests/core/test_snapshot_evaluator.py | 4 +--- 9 files changed, 27 insertions(+), 10 deletions(-) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index e046dc9b4d..d8747c979d 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -1044,6 +1044,7 @@ def clone_table( target_table_name: TableName, source_table_name: TableName, replace: bool = False, + exists: bool = True, clone_kwargs: t.Optional[t.Dict[str, t.Any]] = None, **kwargs: t.Any, ) -> None: @@ -1053,6 +1054,7 @@ def clone_table( target_table_name: The name of the table that should be created. 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. """ if not self.SUPPORTS_CLONING: raise NotImplementedError(f"Engine does not support cloning: {type(self)}") @@ -1063,6 +1065,7 @@ def clone_table( this=exp.to_table(target_table_name), kind="TABLE", replace=replace, + exists=exists, clone=exp.Clone( this=exp.to_table(source_table_name), **(clone_kwargs or {}), diff --git a/sqlmesh/core/engine_adapter/databricks.py b/sqlmesh/core/engine_adapter/databricks.py index 2571cb7214..946a7bdf74 100644 --- a/sqlmesh/core/engine_adapter/databricks.py +++ b/sqlmesh/core/engine_adapter/databricks.py @@ -299,6 +299,7 @@ def clone_table( target_table_name: TableName, source_table_name: TableName, replace: bool = False, + exists: bool = True, clone_kwargs: t.Optional[t.Dict[str, t.Any]] = None, **kwargs: t.Any, ) -> None: diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index 8a6f5e2fcc..c6b0e71ac3 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -610,6 +610,7 @@ def clone_table( target_table_name: TableName, source_table_name: TableName, replace: bool = False, + exists: bool = True, clone_kwargs: t.Optional[t.Dict[str, t.Any]] = None, **kwargs: t.Any, ) -> None: diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index cdcbdfc078..1e38c786d7 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -32,6 +32,7 @@ from sqlglot import exp, select from sqlglot.executor import execute +from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_not_exception_type from sqlmesh.core import constants as c from sqlmesh.core import dialect as d @@ -76,6 +77,7 @@ from sqlmesh.utils.errors import ( ConfigError, DestructiveChangeError, + MigrationNotSupportedError, SQLMeshError, format_destructive_change_msg, format_additive_change_msg, @@ -1035,7 +1037,6 @@ def _clone_snapshot_in_dev( adapter.clone_table( target_table_name, snapshot.table_name(), - replace=True, rendered_physical_properties=rendered_physical_properties, ) self._migrate_target_table( @@ -1111,6 +1112,15 @@ def _migrate_snapshot( dry_run=True, ) + # Retry in case when the table is migrated concurrently from another plan application + @retry( + reraise=True, + stop=stop_after_attempt(5), + wait=wait_exponential(min=1, max=16), + retry=retry_if_not_exception_type( + (DestructiveChangeError, AdditiveChangeError, MigrationNotSupportedError) + ), + ) def _migrate_target_table( self, target_table_name: str, @@ -2672,7 +2682,7 @@ def migrate( ) if len(potential_alter_operations) > 0: # this can happen if a user changes a managed model and deliberately overrides a plan to be forward only, eg `sqlmesh plan --forward-only` - raise SQLMeshError( + raise MigrationNotSupportedError( f"The schema of the managed model '{target_table_name}' cannot be updated in a forward-only fashion." ) diff --git a/sqlmesh/utils/errors.py b/sqlmesh/utils/errors.py index d90965c25c..ca3e1bfb05 100644 --- a/sqlmesh/utils/errors.py +++ b/sqlmesh/utils/errors.py @@ -151,6 +151,10 @@ class AdditiveChangeError(SQLMeshError): pass +class MigrationNotSupportedError(SQLMeshError): + pass + + class NotificationTargetError(SQLMeshError): pass diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index 220c3291f7..140fac43eb 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -3347,7 +3347,7 @@ def test_clone_table(make_mocked_engine_adapter: t.Callable): adapter.clone_table("target_table", "source_table") adapter.cursor.execute.assert_called_once_with( - "CREATE TABLE `target_table` CLONE `source_table`" + "CREATE TABLE IF NOT EXISTS `target_table` CLONE `source_table`" ) diff --git a/tests/core/engine_adapter/test_databricks.py b/tests/core/engine_adapter/test_databricks.py index fcd7aec0fa..f482361c3c 100644 --- a/tests/core/engine_adapter/test_databricks.py +++ b/tests/core/engine_adapter/test_databricks.py @@ -106,7 +106,7 @@ def test_clone_table(mocker: MockFixture, make_mocked_engine_adapter: t.Callable adapter = make_mocked_engine_adapter(DatabricksEngineAdapter, default_catalog="test_catalog") adapter.clone_table("target_table", "source_table") adapter.cursor.execute.assert_called_once_with( - "CREATE TABLE `target_table` SHALLOW CLONE `source_table`" + "CREATE TABLE IF NOT EXISTS `target_table` SHALLOW CLONE `source_table`" ) diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index 9a1e068aa6..75ce8edbe0 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -688,7 +688,7 @@ def test_clone_table(mocker: MockerFixture, make_mocked_engine_adapter: t.Callab adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter, default_catalog="test_catalog") adapter.clone_table("target_table", "source_table") adapter.cursor.execute.assert_called_once_with( - 'CREATE TABLE "target_table" CLONE "source_table"' + 'CREATE TABLE IF NOT EXISTS "target_table" CLONE "source_table"' ) # Validate with transient type we create the clone table accordingly @@ -700,7 +700,7 @@ def test_clone_table(mocker: MockerFixture, make_mocked_engine_adapter: t.Callab "target_table", "source_table", rendered_physical_properties=rendered_physical_properties ) adapter.cursor.execute.assert_called_once_with( - 'CREATE TRANSIENT TABLE "target_table" CLONE "source_table"' + 'CREATE TRANSIENT TABLE IF NOT EXISTS "target_table" CLONE "source_table"' ) # Validate other engine adapters would work as usual even when we pass the properties @@ -710,7 +710,7 @@ def test_clone_table(mocker: MockerFixture, make_mocked_engine_adapter: t.Callab "target_table", "source_table", rendered_physical_properties=rendered_physical_properties ) adapter.cursor.execute.assert_called_once_with( - 'CREATE TABLE "target_table" CLONE "source_table"' + 'CREATE TABLE IF NOT EXISTS "target_table" CLONE "source_table"' ) diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 8003c6014e..660eafac70 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -1678,7 +1678,6 @@ def test_create_clone_in_dev(mocker: MockerFixture, adapter_mock, make_snapshot) adapter_mock.clone_table.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.dev_version}__dev", f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}", - replace=True, rendered_physical_properties={}, ) @@ -1701,7 +1700,7 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m adapter_mock.get_alter_operations.return_value = [] evaluator = SnapshotEvaluator(adapter_mock) - adapter_mock.alter_table.side_effect = Exception("Migration failed") + adapter_mock.alter_table.side_effect = DestructiveChangeError("Migration failed") model = load_sql_based_model( parse( # type: ignore @@ -1728,7 +1727,6 @@ def test_drop_clone_in_dev_when_migration_fails(mocker: MockerFixture, adapter_m adapter_mock.clone_table.assert_called_once_with( f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev", f"sqlmesh__test_schema.test_schema__test_model__{snapshot.version}", - replace=True, rendered_physical_properties={}, ) From 52961f0fe3f70a76ca34255fd1a07964c932677b Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Thu, 18 Sep 2025 15:57:34 +1200 Subject: [PATCH 0879/1056] Fix: Dont backfill seeds if the seed data didnt actually change (#5398) --- sqlmesh/core/plan/common.py | 3 +- tests/core/test_integration.py | 118 +++++++++++++++++++++++++++++++++ tests/core/test_plan.py | 59 +++++++++++++++++ 3 files changed, 179 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/plan/common.py b/sqlmesh/core/plan/common.py index 99601cb484..2ae34fbfba 100644 --- a/sqlmesh/core/plan/common.py +++ b/sqlmesh/core/plan/common.py @@ -16,8 +16,9 @@ def should_force_rebuild(old: Snapshot, new: Snapshot) -> bool: if new.is_view and new.is_indirect_non_breaking and not new.is_forward_only: # View models always need to be rebuilt to reflect updated upstream dependencies return True - if new.is_seed: + if new.is_seed and not new.is_metadata: # Seed models always need to be rebuilt to reflect changes in the seed file + # Unless only their metadata has been updated (eg description added) and the seed file has not been touched return True return is_breaking_kind_change(old, new) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index a0e41f329f..5b44d80149 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -10662,3 +10662,121 @@ def _run_restatement_plan(tmp_path: Path, config: Config, q: queue.Queue): assert len(model_a.intervals) set_console(orig_console) + + +def test_seed_model_metadata_update_does_not_trigger_backfill(tmp_path: Path): + """ + Scenario: + - Create a seed model; perform initial population + - Modify the model with a metadata-only change and trigger a plan + + Outcome: + - The seed model is modified (metadata-only) but this should NOT trigger backfill + - There should be no missing_intervals on the plan to backfill + """ + + models_path = tmp_path / "models" + seeds_path = tmp_path / "seeds" + models_path.mkdir() + seeds_path.mkdir() + + seed_model_path = models_path / "seed.sql" + seed_path = seeds_path / "seed_data.csv" + + seed_path.write_text("\n".join(["id,name", "1,test"])) + + seed_model_path.write_text(""" + MODEL ( + name test.source_data, + kind SEED ( + path '../seeds/seed_data.csv' + ) + ); + """) + + config = Config( + gateways={"": GatewayConfig(connection=DuckDBConnectionConfig())}, + model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01"), + ) + ctx = Context(paths=tmp_path, config=config) + + plan = ctx.plan(auto_apply=True) + + original_seed_snapshot = ctx.snapshots['"memory"."test"."source_data"'] + assert plan.directly_modified == {original_seed_snapshot.snapshot_id} + assert plan.metadata_updated == set() + assert plan.missing_intervals + + # prove data loaded + assert ctx.engine_adapter.fetchall("select id, name from memory.test.source_data") == [ + (1, "test") + ] + + # prove no diff + ctx.load() + plan = ctx.plan(auto_apply=True) + assert not plan.has_changes + assert not plan.missing_intervals + + # make metadata-only change + seed_model_path.write_text(""" + MODEL ( + name test.source_data, + kind SEED ( + path '../seeds/seed_data.csv' + ), + description 'updated by test' + ); + """) + + ctx.load() + plan = ctx.plan(auto_apply=True) + assert plan.has_changes + + new_seed_snapshot = ctx.snapshots['"memory"."test"."source_data"'] + assert ( + new_seed_snapshot.version == original_seed_snapshot.version + ) # should be using the same physical table + assert ( + new_seed_snapshot.snapshot_id != original_seed_snapshot.snapshot_id + ) # but still be different due to the metadata change + assert plan.directly_modified == set() + assert plan.metadata_updated == {new_seed_snapshot.snapshot_id} + + # there should be no missing intervals to backfill since all we did is update a description + assert not plan.missing_intervals + + # there should still be no diff or missing intervals in 3 days time + assert new_seed_snapshot.model.interval_unit.is_day + with time_machine.travel(timedelta(days=3)): + ctx.clear_caches() + ctx.load() + plan = ctx.plan(auto_apply=True) + assert not plan.has_changes + assert not plan.missing_intervals + + # change seed data + seed_path.write_text("\n".join(["id,name", "1,test", "2,updated"])) + + # new plan - NOW we should backfill because data changed + ctx.load() + plan = ctx.plan(auto_apply=True) + assert plan.has_changes + + updated_seed_snapshot = ctx.snapshots['"memory"."test"."source_data"'] + + assert ( + updated_seed_snapshot.snapshot_id + != new_seed_snapshot.snapshot_id + != original_seed_snapshot.snapshot_id + ) + assert not updated_seed_snapshot.forward_only + assert plan.directly_modified == {updated_seed_snapshot.snapshot_id} + assert plan.metadata_updated == set() + assert plan.missing_intervals + + # prove backfilled data loaded + assert ctx.engine_adapter.fetchall("select id, name from memory.test.source_data") == [ + (1, "test"), + (2, "updated"), + ] diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 4f6b99a4ee..59bc91d1bf 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -1189,6 +1189,65 @@ def test_forward_only_plan_seed_models(make_snapshot, mocker: MockerFixture): assert not snapshot_a_updated.is_forward_only +def test_seed_model_metadata_change_no_missing_intervals( + make_snapshot: t.Callable[..., Snapshot], +): + snapshot_a = make_snapshot( + SeedModel( + name="a", + kind=SeedKind(path="./path/to/seed"), + seed=Seed(content="content"), + column_hashes={"col": "hash1"}, + depends_on=set(), + ) + ) + snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot_a.add_interval("2022-01-01", now()) + + snapshot_a_metadata_updated = make_snapshot( + SeedModel( + name="a", + kind=SeedKind(path="./path/to/seed"), + seed=Seed(content="content"), + column_hashes={"col": "hash1"}, + depends_on=set(), + description="foo", + ) + ) + assert snapshot_a_metadata_updated.version is None + assert snapshot_a_metadata_updated.change_category is None + + context_diff = ContextDiff( + environment="prod", + is_new_environment=True, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={ + snapshot_a_metadata_updated.name: (snapshot_a_metadata_updated, snapshot_a) + }, + snapshots={snapshot_a_metadata_updated.snapshot_id: snapshot_a_metadata_updated}, + new_snapshots={snapshot_a_metadata_updated.snapshot_id: snapshot_a}, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + plan = PlanBuilder(context_diff).build() + assert snapshot_a_metadata_updated.change_category == SnapshotChangeCategory.METADATA + assert not snapshot_a_metadata_updated.is_forward_only + assert not plan.missing_intervals # plan should have no missing intervals + assert ( + snapshot_a_metadata_updated.intervals == snapshot_a.intervals + ) # intervals should have been copied + + def test_start_inference(make_snapshot, mocker: MockerFixture): snapshot_a = make_snapshot( SqlModel(name="a", query=parse_one("select 1, ds"), start="2022-01-01") From 862ef50b04eafb103d7c0de2453f29d506e4fa92 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 18 Sep 2025 10:24:26 +0100 Subject: [PATCH 0880/1056] fix: remove need for uvicorn (#5394) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 675cd019ff..d5c33bbe49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -135,7 +135,7 @@ lsp = [ # Duplicate of web "fastapi==0.115.5", "watchfiles>=0.19.0", - "uvicorn[standard]==0.22.0", + # "uvicorn[standard]==0.22.0", "sse-starlette>=0.2.2", "pyarrow", # For lsp From 341cc3c5726b47562615c51495a94b053cf50b8e Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:18:19 +0300 Subject: [PATCH 0881/1056] Chore: Pin cryptography instead of leaving it to the resolver (#5401) --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d5c33bbe49..56b52fd447 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ dev = [ "agate", "beautifulsoup4", "clickhouse-connect", - "cryptography", + "cryptography<46.0.0", "databricks-sql-connector", "dbt-bigquery", "dbt-core", @@ -119,7 +119,7 @@ postgres = ["psycopg2"] redshift = ["redshift_connector"] slack = ["slack_sdk"] snowflake = [ - "cryptography", + "cryptography<46.0.0", "snowflake-connector-python[pandas,secure-local-storage]", "snowflake-snowpark-python", ] From c5e4b174e0954b2c7f8eb8f12fead2a1447efac5 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 18 Sep 2025 08:57:00 -0500 Subject: [PATCH 0882/1056] Fix: load dbt model containing only a comment and non-sql jinja (#5368) Co-authored-by: Jo <46752250+georgesittas@users.noreply.github.com> --- sqlmesh/core/renderer.py | 29 +++++++++++++++++++---------- tests/dbt/test_model.py | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/sqlmesh/core/renderer.py b/sqlmesh/core/renderer.py index 18377e0258..9e750159be 100644 --- a/sqlmesh/core/renderer.py +++ b/sqlmesh/core/renderer.py @@ -6,7 +6,7 @@ from functools import partial from pathlib import Path -from sqlglot import exp, parse +from sqlglot import exp, Dialect from sqlglot.errors import SqlglotError from sqlglot.helper import ensure_list from sqlglot.optimizer.annotate_types import annotate_types @@ -249,15 +249,24 @@ def _resolve_table(table: str | exp.Table) -> str: ) from ex if rendered_expression.strip(): - try: - expressions = [e for e in parse(rendered_expression, read=self._dialect) if e] - - if not expressions: - raise ConfigError(f"Failed to parse an expression:\n{self._expression}") - except Exception as ex: - raise ConfigError( - f"Could not parse the rendered jinja at '{self._path}'.\n{ex}" - ) from ex + # ensure there is actual SQL and not just comments and non-SQL jinja + dialect = Dialect.get_or_raise(self._dialect) + tokens = dialect.tokenize(rendered_expression) + + if tokens: + try: + expressions = [ + e for e in dialect.parser().parse(tokens, rendered_expression) if e + ] + + if not expressions: + raise ConfigError( + f"Failed to parse an expression:\n{rendered_expression}" + ) + except Exception as ex: + raise ConfigError( + f"Could not parse the rendered jinja at '{self._path}'.\n{ex}" + ) from ex if this_model: render_kwargs["this_model"] = this_model diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index caa409807f..7bcfe98768 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -873,3 +873,24 @@ def test_load_model_dbt_node_name(tmp_path: Path) -> None: # Verify that node_name is the equivalent dbt one model = context.snapshots[model_fqn].model assert model.dbt_name == "model.test_project.simple_model" + + +@pytest.mark.slow +def test_jinja_config_no_query(tmp_path, create_empty_project): + project_dir, model_dir = create_empty_project() + + # model definition contains only a comment and non-SQL jinja + model_contents = "/* comment */ {{ config(materialized='table') }}" + model_file = model_dir / "comment_config_model.sql" + with open(model_file, "w", encoding="utf-8") as f: + f.write(model_contents) + + schema_yaml = {"version": 2, "models": [{"name": "comment_config_model"}]} + schema_file = model_dir / "schema.yml" + with open(schema_file, "w", encoding="utf-8") as f: + YAML().dump(schema_yaml, f) + + context = Context(paths=project_dir) + + # loads without error and contains empty query (which will error at runtime) + assert not context.snapshots['"local"."main"."comment_config_model"'].model.render_query() From 54e6b5771cc8f0311606e36fa7ab16be38226bea Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 18 Sep 2025 09:03:05 -0500 Subject: [PATCH 0883/1056] Fix: tests should not be sensitive to output column order (#5369) --- sqlmesh/core/test/definition.py | 8 ++++---- tests/core/test_test.py | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index a12dafec19..4336e00ce0 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -644,16 +644,16 @@ def _create_df( return self._execute(query) rows = values["rows"] + columns_str: t.Optional[t.List[str]] = None if columns: + columns_str = [str(c) for c in columns] referenced_columns = list(dict.fromkeys(col for row in rows for col in row)) _raise_if_unexpected_columns(columns, referenced_columns) if partial: - columns = referenced_columns + columns_str = [c for c in columns_str if c in referenced_columns] - return pd.DataFrame.from_records( - rows, columns=[str(c) for c in columns] if columns else None - ) + return pd.DataFrame.from_records(rows, columns=columns_str) def _add_missing_columns( self, query: exp.Query, all_columns: t.Optional[t.Collection[str]] = None diff --git a/tests/core/test_test.py b/tests/core/test_test.py index d889c7bb33..56a44cc955 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -770,6 +770,42 @@ def test_partial_data_column_order(sushi_context: Context) -> None: ).run() ) + # - output df must differ if sorted by (id, event_date) vs. (event_date, id) + # - output partial must be true + _check_successful_or_raise( + _create_test( + body=load_yaml( + """ +test_foo: + model: sushi.foo + inputs: + sushi.items: + - id: 9876 + event_date: 2020-01-01 + - id: 1234 + name: hello + event_date: 2020-01-02 + outputs: + partial: true + query: + - event_date: 2020-01-01 + id: 9876 + - event_date: 2020-01-02 + id: 1234 + name: hello + """ + ), + test_name="test_foo", + model=sushi_context.upsert_model( + _create_model( + "SELECT id, name, price, event_date FROM sushi.items", + default_catalog=sushi_context.default_catalog, + ) + ), + context=sushi_context, + ).run() + ) + def test_partial_data_missing_schemas(sushi_context: Context) -> None: _check_successful_or_raise( From 7cfddd87584079710f80130f7070f1d417daa874 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 18 Sep 2025 09:22:16 -0500 Subject: [PATCH 0884/1056] Fix: support auto_restatement_cron in python models (#5141) --- sqlmesh/core/model/definition.py | 7 +++++++ tests/core/test_model.py | 34 ++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index dbbb8ff3a8..c9eaa43b3e 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -2866,6 +2866,13 @@ def render_field_value(value: t.Any) -> t.Any: for key, value in field_value.items(): if key in RUNTIME_RENDERED_MODEL_FIELDS: rendered_dict[key] = parse_strings_with_macro_refs(value, dialect) + elif ( + # don't parse kind auto_restatement_cron="@..." kwargs (e.g. @daily) into MacroVar + key == "auto_restatement_cron" + and isinstance(value, str) + and value.lower() in CRON_SHORTCUTS + ): + rendered_dict[key] = value elif (rendered := render_field_value(value)) is not None: rendered_dict[key] = rendered diff --git a/tests/core/test_model.py b/tests/core/test_model.py index ebe4d11a20..00ff48b0d2 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -2898,7 +2898,15 @@ def my_model_2(context): # no warning with valid kind dict with patch.object(get_console(), "log_warning") as mock_logger: - @model("kind_valid_dict", kind=dict(name=ModelKindName.FULL), columns={'"COL"': "int"}) + @model( + "kind_valid_dict", + kind=dict( + name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, + time_column="ds", + auto_restatement_cron="@hourly", + ), + columns={'"ds"': "date", '"COL"': "int"}, + ) def my_model(context): pass @@ -2907,11 +2915,33 @@ def my_model(context): path=Path("."), ) - assert isinstance(python_model.kind, FullKind) + assert isinstance(python_model.kind, IncrementalByTimeRangeKind) assert not mock_logger.call_args +def test_python_model_decorator_auto_restatement_cron() -> None: + @model( + "auto_restatement_model", + cron="@daily", + kind=dict( + name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, + time_column="ds", + auto_restatement_cron="@hourly", + ), + columns={'"ds"': "date", '"COL"': "int"}, + ) + def my_model(context): + pass + + python_model = model.get_registry()["auto_restatement_model"].model( + module_path=Path("."), + path=Path("."), + ) + + assert python_model.auto_restatement_cron == "@hourly" + + def test_python_model_decorator_col_descriptions() -> None: # `columns` and `column_descriptions` column names are different cases, but name normalization makes both lower @model("col_descriptions", columns={"col": "int"}, column_descriptions={"COL": "a column"}) From 24c4455446cf868b655b6da34309a34c1a67c206 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 18 Sep 2025 13:37:09 -0700 Subject: [PATCH 0885/1056] fix: include pre/post when cloning (#5405) --- sqlmesh/core/snapshot/evaluator.py | 3 ++ tests/core/test_snapshot_evaluator.py | 49 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 1e38c786d7..658bb1c400 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -867,6 +867,7 @@ def create_snapshot( rendered_physical_properties=rendered_physical_properties, allow_destructive_snapshots=allow_destructive_snapshots, allow_additive_snapshots=allow_additive_snapshots, + run_pre_post_statements=True, ) else: is_table_deployable = deployability_index.is_deployable(snapshot) @@ -1026,6 +1027,7 @@ def _clone_snapshot_in_dev( rendered_physical_properties: t.Dict[str, exp.Expression], allow_destructive_snapshots: t.Set[str], allow_additive_snapshots: t.Set[str], + run_pre_post_statements: bool = False, ) -> None: adapter = self.get_adapter(snapshot.model.gateway) @@ -1048,6 +1050,7 @@ def _clone_snapshot_in_dev( rendered_physical_properties=rendered_physical_properties, allow_destructive_snapshots=allow_destructive_snapshots, allow_additive_snapshots=allow_additive_snapshots, + run_pre_post_statements=run_pre_post_statements, ) except Exception: adapter.drop_table(target_table_name) diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 660eafac70..1c3d1e6adc 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -854,6 +854,55 @@ def test_create_prod_table_exists(mocker: MockerFixture, adapter_mock, make_snap ) +def test_pre_hook_forward_only_clone( + mocker: MockerFixture, make_mocked_engine_adapter, make_snapshot +): + """ + Verifies that pre-statements are executed when creating a clone of a forward-only model. + """ + pre_statement = """CREATE TEMPORARY FUNCTION "example_udf"("x" BIGINT) AS ("x" + 1)""" + model = load_sql_based_model( + parse( # type: ignore + f""" + MODEL ( + name test_schema.test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds + ) + ); + + {pre_statement}; + + SELECT a::int, ds::string FROM tbl; + """ + ), + ) + + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) + snapshot.previous_versions = snapshot.all_versions + + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.with_settings = lambda **kwargs: adapter + adapter.table_exists = lambda _: True # type: ignore + adapter.SUPPORTS_CLONING = True + mocker.patch.object( + adapter, + "get_data_objects", + return_value=[], + ) + mocker.patch.object( + adapter, + "get_alter_operations", + return_value=[], + ) + + evaluator = SnapshotEvaluator(adapter) + + evaluator.create([snapshot], {}, deployability_index=DeployabilityIndex.none_deployable()) + adapter.cursor.execute.assert_any_call(pre_statement) + + def test_create_only_dev_table_exists(mocker: MockerFixture, adapter_mock, make_snapshot): model = load_sql_based_model( parse( # type: ignore From 087dffbd983feaf7d3f66b239d3bb94562195fb1 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 19 Sep 2025 00:12:04 +0300 Subject: [PATCH 0886/1056] Fix(dbt): Update dispatch signature to match dbt method (#5410) --- sqlmesh/dbt/adapter.py | 20 ++++++++++++-------- tests/dbt/test_adapter.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/sqlmesh/dbt/adapter.py b/sqlmesh/dbt/adapter.py index 236d4cee6b..12e38e4749 100644 --- a/sqlmesh/dbt/adapter.py +++ b/sqlmesh/dbt/adapter.py @@ -115,28 +115,32 @@ def quote_as_configured(self, value: str, component_type: str) -> str: """Returns the value quoted according to the quote policy.""" return self.quote(value) if getattr(self.quote_policy, component_type, False) else value - def dispatch(self, name: str, package: t.Optional[str] = None) -> t.Callable: + def dispatch( + self, + macro_name: str, + macro_namespace: t.Optional[str] = None, + ) -> t.Callable: """Returns a dialect-specific version of a macro with the given name.""" target_type = self.jinja_globals["target"]["type"] - macro_suffix = f"__{name}" + macro_suffix = f"__{macro_name}" def _relevance(package_name_pair: t.Tuple[t.Optional[str], str]) -> t.Tuple[int, int]: """Lower scores more relevant.""" - macro_package, macro_name = package_name_pair + macro_package, name = package_name_pair - package_score = 0 if macro_package == package else 1 + package_score = 0 if macro_package == macro_namespace else 1 name_score = 1 - if macro_name.startswith("default"): + if name.startswith("default"): name_score = 2 - elif macro_name.startswith(target_type): + elif name.startswith(target_type): name_score = 0 return name_score, package_score jinja_env = self.jinja_macros.build_environment(**self.jinja_globals).globals packages_to_check: t.List[t.Optional[str]] = [ - package, + macro_namespace, *(k for k in jinja_env if k.startswith("dbt")), ] candidates = {} @@ -156,7 +160,7 @@ def _relevance(package_name_pair: t.Tuple[t.Optional[str], str]) -> t.Tuple[int, sorted_candidates = sorted(candidates, key=_relevance) return candidates[sorted_candidates[0]] - raise ConfigError(f"Macro '{name}', package '{package}' was not found.") + raise ConfigError(f"Macro '{macro_name}', package '{macro_namespace}' was not found.") def type(self) -> str: return self.project_dialect or "" diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index 85dfa29559..5617d8c5c3 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -243,6 +243,34 @@ def test_adapter_dispatch(sushi_test_project: Project, runtime_renderer: t.Calla assert renderer("{{ adapter.dispatch('current_timestamp')() }}") == "now()" assert renderer("{{ adapter.dispatch('current_timestamp', 'dbt')() }}") == "now()" + # test with keyword arguments + assert ( + renderer( + "{{ adapter.dispatch(macro_name='current_engine', macro_namespace='customers')() }}" + ) + == "duckdb" + ) + assert renderer("{{ adapter.dispatch(macro_name='current_timestamp')() }}") == "now()" + assert ( + renderer("{{ adapter.dispatch(macro_name='current_timestamp', macro_namespace='dbt')() }}") + == "now()" + ) + + # mixing positional and keyword arguments + assert ( + renderer("{{ adapter.dispatch('current_engine', macro_namespace='customers')() }}") + == "duckdb" + ) + assert ( + renderer("{{ adapter.dispatch('current_timestamp', macro_namespace=None)() }}") == "now()" + ) + assert ( + renderer("{{ adapter.dispatch('current_timestamp', macro_namespace='dbt')() }}") == "now()" + ) + + with pytest.raises(ConfigError, match=r"Macro 'current_engine'.*was not found."): + renderer("{{ adapter.dispatch(macro_name='current_engine')() }}") + with pytest.raises(ConfigError, match=r"Macro 'current_engine'.*was not found."): renderer("{{ adapter.dispatch('current_engine')() }}") From 513e4814e5c367e1d6b18d1e1bb8e410568760c7 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 19 Sep 2025 00:20:53 +0300 Subject: [PATCH 0887/1056] Chore!: bump sqlglot to v27.16.2 (#5403) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 56b52fd447..bd34906434 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.15.3", + "sqlglot[rs]~=27.16.3", "tenacity", "time-machine", "json-stream" From 88a3679df3980a0211b7f4ec75f215ad5033cd15 Mon Sep 17 00:00:00 2001 From: Tori Wei <41123940+toriwei@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:33:13 -0700 Subject: [PATCH 0888/1056] fix: ignore cluster_by config for embedded model kind (#5409) --- sqlmesh/dbt/model.py | 5 +++-- tests/dbt/test_transformation.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 124d900c4b..efad5e790b 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -539,10 +539,11 @@ def to_sqlmesh( optional_kwargs["partitioned_by"] = partitioned_by if self.cluster_by: - if isinstance(kind, ViewKind): + if isinstance(kind, (ViewKind, EmbeddedKind)): logger.warning( - "Ignoring cluster_by config for model '%s'; cluster_by is not supported for views.", + "Ignoring cluster_by config for model '%s'; cluster_by is not supported for %s.", self.name, + "views" if isinstance(kind, ViewKind) else "ephemeral models", ) else: clustered_by = [] diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 22b75abab6..29651f9140 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -2074,6 +2074,17 @@ def test_model_cluster_by(): ) assert model.to_sqlmesh(context).clustered_by == [] + model = ModelConfig( + name="model", + alias="model", + package_name="package", + target_schema="test", + cluster_by=["Bar", "qux"], + sql="SELECT * FROM baz", + materialized=Materialization.EPHEMERAL.value, + ) + assert model.to_sqlmesh(context).clustered_by == [] + def test_snowflake_dynamic_table(): context = DbtContext() From 2d13c2f73beac805291a4a38e04138368c935c31 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:09:58 +0300 Subject: [PATCH 0889/1056] Fix: Adapt test to use thread specific consoles (#5414) --- tests/core/test_integration.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 5b44d80149..204092c914 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -14,7 +14,13 @@ import pytest from pytest import MonkeyPatch from pathlib import Path -from sqlmesh.core.console import set_console, get_console, TerminalConsole, CaptureTerminalConsole +from sqlmesh.core.console import ( + MarkdownConsole, + set_console, + get_console, + TerminalConsole, + CaptureTerminalConsole, +) from sqlmesh.core.config.naming import NameInferenceConfig from sqlmesh.core.model.common import ParsableSql from sqlmesh.utils.concurrency import NodeExecutionFailedError @@ -10589,9 +10595,16 @@ def entrypoint(evaluator: MacroEvaluator) -> str: new_model_a_snapshot_id = list(plan.modified_snapshots)[0] # now, trigger a prod restatement plan in a different thread and block it to simulate a long restatement + thread_console = None + def _run_restatement_plan(tmp_path: Path, config: Config, q: queue.Queue): + nonlocal thread_console q.put("thread_started") + # Give this thread its own markdown console to avoid Rich LiveError + thread_console = MarkdownConsole() + set_console(thread_console) + # give this thread its own Context object to prevent segfaulting the Python interpreter restatement_ctx = Context(paths=[tmp_path], config=config) @@ -10647,7 +10660,7 @@ def _run_restatement_plan(tmp_path: Path, config: Config, q: queue.Queue): assert isinstance(plan_error, ConflictingPlanError) assert "please re-apply your plan" in repr(plan_error).lower() - output = " ".join(re.split("\s+", console.captured_output, flags=re.UNICODE)) + output = " ".join(re.split("\\s+", thread_console.captured_output, flags=re.UNICODE)) # type: ignore assert ( f"The following models had new versions deployed while data was being restated: └── test.model_a" in output From af3a16fc101e14d29fef7d067e71c2648908dc80 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 19 Sep 2025 08:30:06 -0700 Subject: [PATCH 0890/1056] Fix: Exempt seed models from migration (#5413) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sqlmesh/core/snapshot/definition.py | 2 +- tests/core/test_integration.py | 36 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 0d64736ffb..600d84fe83 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -1480,7 +1480,7 @@ def expiration_ts(self) -> int: @property def supports_schema_migration_in_prod(self) -> bool: """Returns whether or not this snapshot supports schema migration when deployed to production.""" - return self.is_paused and self.is_model and not self.is_symbolic + return self.is_paused and self.is_model and not self.is_symbolic and not self.is_seed @property def requires_schema_migration_in_prod(self) -> bool: diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 204092c914..a3f9584aa3 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -1678,6 +1678,42 @@ def test_run_with_select_models( } +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_seed_model_promote_to_prod_after_dev( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + with open(context.path / "seeds" / "waiter_names.csv", "a") as f: + f.write("\n10,New Waiter") + + context.load() + + waiter_names_snapshot = context.get_snapshot("sushi.waiter_names") + plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) + assert waiter_names_snapshot.snapshot_id in plan.directly_modified + + # Trigger a metadata change to reuse the previous version + waiter_names_model = waiter_names_snapshot.model.copy( + update={"description": "Updated description"} + ) + context.upsert_model(waiter_names_model) + context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) + + # Promote all changes to prod + waiter_names_snapshot = context.get_snapshot("sushi.waiter_names") + plan = context.plan_builder("prod", skip_tests=True).build() + # Clear the cache to source the dehydrated model instance from the state + context.clear_caches() + context.apply(plan) + + assert ( + context.engine_adapter.fetchone("SELECT COUNT(*) FROM sushi.waiter_names WHERE id = 10")[0] + == 1 + ) + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_plan_with_run( init_and_plan_context: t.Callable, From 672b440071b6c116119fd4b1ab816e032056fc9e Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:33:18 -0700 Subject: [PATCH 0891/1056] fix: traverse target snapshots instead of all snapshots (#5416) --- sqlmesh/core/snapshot/evaluator.py | 2 +- tests/core/test_snapshot_evaluator.py | 57 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 658bb1c400..86fa897005 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -494,7 +494,7 @@ def migrate( with self.concurrent_context(): # Only migrate snapshots for which there's an existing data object concurrent_apply_to_snapshots( - snapshots_by_name.values(), + target_snapshots, lambda s: self._migrate_snapshot( s, snapshots_by_name, diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 1c3d1e6adc..5e7b078787 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -3955,6 +3955,63 @@ def test_migrate_snapshot(snapshot: Snapshot, mocker: MockerFixture, adapter_moc ) +def test_migrate_only_processes_target_snapshots( + mocker: MockerFixture, adapter_mock, make_snapshot +): + evaluator = SnapshotEvaluator(adapter_mock) + + target_model = SqlModel( + name="test_schema.target_model", + kind=FullKind(), + query=parse_one("SELECT 1 AS a"), + ) + extra_model = SqlModel( + name="test_schema.extra_model", + kind=FullKind(), + query=parse_one("SELECT 1 AS a"), + ) + + target_snapshot = make_snapshot(target_model) + extra_snapshot = make_snapshot(extra_model) + target_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + extra_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + target_snapshots = [target_snapshot] + snapshots = { + target_snapshot.snapshot_id: target_snapshot, + extra_snapshot.snapshot_id: extra_snapshot, + } + + mocker.patch.object( + evaluator, + "_get_data_objects", + return_value={target_snapshot.snapshot_id: mocker.Mock()}, + ) + migrate_mock = mocker.patch.object(evaluator, "_migrate_snapshot") + + def apply_side_effect(snapshot_iterable, fn, *_args, **_kwargs): + for snapshot in snapshot_iterable: + fn(snapshot) + return ([], []) + + apply_mock = mocker.patch( + "sqlmesh.core.snapshot.evaluator.concurrent_apply_to_snapshots", + side_effect=apply_side_effect, + ) + + evaluator.migrate(target_snapshots=target_snapshots, snapshots=snapshots) + + assert apply_mock.call_count == 1 + called_snapshots = list(apply_mock.call_args.args[0]) + assert called_snapshots == target_snapshots + + migrate_mock.assert_called_once() + called_snapshot, snapshots_by_name, *_ = migrate_mock.call_args.args + assert called_snapshot is target_snapshot + assert target_snapshot.name in snapshots_by_name + assert extra_snapshot.name in snapshots_by_name + + def test_migrate_managed(adapter_mock, make_snapshot, mocker: MockerFixture): evaluator = SnapshotEvaluator(adapter_mock) From 50aee2cc8f3d3d356ddeff0138470c9984d17167 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 19 Sep 2025 21:18:26 +0300 Subject: [PATCH 0892/1056] Fix: Pass a copy of the properties to avoid mutation of actual dict (#5417) --- sqlmesh/core/snapshot/evaluator.py | 4 +- tests/core/test_snapshot_evaluator.py | 76 +++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 86fa897005..688f9d8d5b 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -763,7 +763,7 @@ def _evaluate_snapshot( snapshots=snapshots, deployability_index=deployability_index, render_kwargs=create_render_kwargs, - rendered_physical_properties=rendered_physical_properties, + rendered_physical_properties=rendered_physical_properties.copy(), allow_destructive_snapshots=allow_destructive_snapshots, allow_additive_snapshots=allow_additive_snapshots, ) @@ -776,7 +776,7 @@ def _evaluate_snapshot( is_table_deployable=is_snapshot_deployable, deployability_index=deployability_index, create_render_kwargs=create_render_kwargs, - rendered_physical_properties=rendered_physical_properties, + rendered_physical_properties=rendered_physical_properties.copy(), dry_run=False, run_pre_post_statements=False, ) diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 5e7b078787..66128cfeee 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -4737,3 +4737,79 @@ def test_wap_publish_failure(adapter_mock: Mock, make_snapshot: t.Callable[..., # Execute audit with WAP ID and expect it to raise the exception with pytest.raises(Exception, match="WAP publish failed"): evaluator.audit(snapshot, snapshots={}, wap_id=wap_id) + + +def test_properties_are_preserved_in_both_create_statements( + adapter_mock: Mock, make_snapshot: t.Callable[..., Snapshot] +) -> None: + # the below mocks are needed to create a situation + # where we trigger two create statements during evaluation + transaction_mock = Mock() + transaction_mock.__enter__ = Mock() + transaction_mock.__exit__ = Mock() + session_mock = Mock() + session_mock.__enter__ = Mock() + session_mock.__exit__ = Mock() + adapter_mock = Mock() + adapter_mock.transaction.return_value = transaction_mock + adapter_mock.session.return_value = session_mock + adapter_mock.dialect = "trino" + adapter_mock.HAS_VIEW_BINDING = False + adapter_mock.wap_supported.return_value = False + adapter_mock.get_data_objects.return_value = [] + adapter_mock.with_settings.return_value = adapter_mock + adapter_mock.table_exists.return_value = False + + props = [] + + def mutate_view_properties(*args, **kwargs): + view_props = kwargs.get("view_properties") + if isinstance(view_props, dict): + props.append(view_props["creatable_type"].sql()) + # simulate view pop + view_props.pop("creatable_type") + return None + + adapter_mock.create_view.side_effect = mutate_view_properties + + evaluator = SnapshotEvaluator(adapter_mock) + + # create a view model with SECURITY INVOKER physical property + # AND self referenctial to trigger two create statements + model = load_sql_based_model( + parse( # type: ignore + """ + MODEL ( + name test_schema.security_view, + kind VIEW, + physical_properties ( + 'creatable_type' = 'SECURITY INVOKER' + ) + ); + + SELECT 1 as col from test_schema.security_view; + """ + ), + ) + + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + evaluator.evaluate( + snapshot, + start="2024-01-01", + end="2024-01-02", + execution_time="2024-01-02", + snapshots={}, + ) + + # Verify create_view was called twice + assert adapter_mock.create_view.call_count == 2 + first_call = adapter_mock.create_view.call_args_list[0] + second_call = adapter_mock.create_view.call_args_list[1] + + # First call should be CREATE VIEW (replace=False) second CREATE OR REPLACE VIEW (replace=True) + assert first_call.kwargs.get("replace") == False + assert second_call.kwargs.get("replace") == True + + # Both calls should have view_properties with security invoker + assert props == ["'SECURITY INVOKER'", "'SECURITY INVOKER'"] From d41c3e0e3a6c010babf75a9b46795834a0c56730 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Fri, 19 Sep 2025 15:06:50 -0700 Subject: [PATCH 0893/1056] Fix: Disable WAP support by default for Spark and Iceberg (#5415) --- sqlmesh/core/config/connection.py | 5 +++++ sqlmesh/core/engine_adapter/base.py | 5 +++++ sqlmesh/core/engine_adapter/spark.py | 14 +++++++------ sqlmesh/core/snapshot/evaluator.py | 1 + tests/core/engine_adapter/test_spark.py | 28 +++++++++++++++++-------- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/sqlmesh/core/config/connection.py b/sqlmesh/core/config/connection.py index 553ffd58a5..dbda66614e 100644 --- a/sqlmesh/core/config/connection.py +++ b/sqlmesh/core/config/connection.py @@ -1755,6 +1755,7 @@ class SparkConnectionConfig(ConnectionConfig): config_dir: t.Optional[str] = None catalog: t.Optional[str] = None config: t.Dict[str, t.Any] = {} + wap_enabled: bool = False concurrent_tasks: int = 4 register_comments: bool = True @@ -1801,6 +1802,10 @@ def _static_connection_kwargs(self) -> t.Dict[str, t.Any]: .getOrCreate(), } + @property + def _extra_engine_config(self) -> t.Dict[str, t.Any]: + return {"wap_enabled": self.wap_enabled} + class TrinoAuthenticationMethod(str, Enum): NO_AUTH = "no-auth" diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index d8747c979d..47e6a4260c 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -2357,6 +2357,11 @@ def fetch_pyspark_df( """Fetches a PySpark DataFrame from the cursor""" raise NotImplementedError(f"Engine does not support PySpark DataFrames: {type(self)}") + @property + def wap_enabled(self) -> bool: + """Returns whether WAP is enabled for this engine.""" + return self._extra_config.get("wap_enabled", False) + def wap_supported(self, table_name: TableName) -> bool: """Returns whether WAP for the target table is supported.""" return False diff --git a/sqlmesh/core/engine_adapter/spark.py b/sqlmesh/core/engine_adapter/spark.py index 7d6a4d969b..18ba6ea106 100644 --- a/sqlmesh/core/engine_adapter/spark.py +++ b/sqlmesh/core/engine_adapter/spark.py @@ -457,12 +457,14 @@ def _create_table( if wap_id.startswith(f"{self.BRANCH_PREFIX}{self.WAP_PREFIX}"): table_name.set("this", table_name.this.this) - wap_supported = ( - kwargs.get("storage_format") or "" - ).lower() == "iceberg" or self.wap_supported(table_name) - do_dummy_insert = ( - False if not wap_supported or not exists else not self.table_exists(table_name) - ) + do_dummy_insert = False + if self.wap_enabled: + wap_supported = ( + kwargs.get("storage_format") or "" + ).lower() == "iceberg" or self.wap_supported(table_name) + do_dummy_insert = ( + False if not wap_supported or not exists else not self.table_exists(table_name) + ) super()._create_table( table_name_or_schema, expression, diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 688f9d8d5b..e22b1a850b 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -793,6 +793,7 @@ def _evaluate_snapshot( if ( snapshot.is_materialized and target_table_exists + and adapter.wap_enabled and (model.wap_supported or adapter.wap_supported(target_table_name)) ): wap_id = random_id()[0:8] diff --git a/tests/core/engine_adapter/test_spark.py b/tests/core/engine_adapter/test_spark.py index f1929639a2..bc4e352bd7 100644 --- a/tests/core/engine_adapter/test_spark.py +++ b/tests/core/engine_adapter/test_spark.py @@ -66,14 +66,15 @@ def test_create_table_properties(make_mocked_engine_adapter: t.Callable): ) +@pytest.mark.parametrize("wap_enabled", [True, False]) def test_replace_query_table_properties_not_exists( - mocker: MockerFixture, make_mocked_engine_adapter: t.Callable + mocker: MockerFixture, make_mocked_engine_adapter: t.Callable, wap_enabled: bool ): mocker.patch( "sqlmesh.core.engine_adapter.spark.SparkEngineAdapter.table_exists", return_value=False, ) - adapter = make_mocked_engine_adapter(SparkEngineAdapter) + adapter = make_mocked_engine_adapter(SparkEngineAdapter, wap_enabled=wap_enabled) columns_to_types = { "cola": exp.DataType.build("INT"), @@ -89,10 +90,13 @@ def test_replace_query_table_properties_not_exists( table_properties={"a": exp.convert(1)}, ) - assert to_sql_calls(adapter) == [ + expected_sql_calls = [ "CREATE TABLE IF NOT EXISTS `test_table` USING ICEBERG PARTITIONED BY (`colb`) TBLPROPERTIES ('a'=1) AS SELECT CAST(`cola` AS INT) AS `cola`, CAST(`colb` AS STRING) AS `colb`, CAST(`colc` AS STRING) AS `colc` FROM (SELECT 1 AS `cola`, '2' AS `colb`, '3' AS `colc`) AS `_subquery`", - "INSERT INTO `test_table` SELECT * FROM `test_table`", ] + if wap_enabled: + expected_sql_calls.append("INSERT INTO `test_table` SELECT * FROM `test_table`") + + assert to_sql_calls(adapter) == expected_sql_calls def test_replace_query_table_properties_exists( @@ -825,13 +829,16 @@ def test_wap_publish(make_mocked_engine_adapter: t.Callable, mocker: MockerFixtu ) -def test_create_table_iceberg(mocker: MockerFixture, make_mocked_engine_adapter: t.Callable): +@pytest.mark.parametrize("wap_enabled", [True, False]) +def test_create_table_iceberg( + mocker: MockerFixture, make_mocked_engine_adapter: t.Callable, wap_enabled: bool +): mocker.patch( "sqlmesh.core.engine_adapter.spark.SparkEngineAdapter.table_exists", return_value=False, ) - adapter = make_mocked_engine_adapter(SparkEngineAdapter) + adapter = make_mocked_engine_adapter(SparkEngineAdapter, wap_enabled=wap_enabled) columns_to_types = { "cola": exp.DataType.build("INT"), @@ -846,10 +853,13 @@ def test_create_table_iceberg(mocker: MockerFixture, make_mocked_engine_adapter: storage_format="ICEBERG", ) - assert to_sql_calls(adapter) == [ + expected_sql_calls = [ "CREATE TABLE IF NOT EXISTS `test_table` (`cola` INT, `colb` STRING, `colc` STRING) USING ICEBERG PARTITIONED BY (`colb`)", - "INSERT INTO `test_table` SELECT * FROM `test_table`", ] + if wap_enabled: + expected_sql_calls.append("INSERT INTO `test_table` SELECT * FROM `test_table`") + + assert to_sql_calls(adapter) == expected_sql_calls def test_comments_hive(mocker: MockerFixture, make_mocked_engine_adapter: t.Callable): @@ -973,7 +983,7 @@ def test_create_table_with_wap(make_mocked_engine_adapter: t.Callable, mocker: M "sqlmesh.core.engine_adapter.spark.SparkEngineAdapter.table_exists", return_value=False, ) - adapter = make_mocked_engine_adapter(SparkEngineAdapter) + adapter = make_mocked_engine_adapter(SparkEngineAdapter, wap_enabled=True) adapter.create_table( "catalog.schema.table.branch_wap_12345", From 6d00e35ca0dd0e9f85a675a3136b3a52f9b551dc Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Mon, 22 Sep 2025 15:58:06 -0700 Subject: [PATCH 0894/1056] Fix: Reporting deletion of physical tables for snapshots of symbolic / audit models (#5422) --- sqlmesh/core/snapshot/evaluator.py | 4 ++- tests/core/test_snapshot_evaluator.py | 39 ++++++++++++++++++++++- tests/integrations/jupyter/test_magics.py | 3 -- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index e22b1a850b..baf4dd67f1 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -518,10 +518,12 @@ def cleanup( target_snapshots: Snapshots to cleanup. on_complete: A callback to call on each successfully deleted database object. """ + target_snapshots = [ + t for t in target_snapshots if t.snapshot.is_model and not t.snapshot.is_symbolic + ] snapshots_to_dev_table_only = { t.snapshot.snapshot_id: t.dev_table_only for t in target_snapshots } - with self.concurrent_context(): concurrent_apply_to_snapshots( [t.snapshot for t in target_snapshots], diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 66128cfeee..2df91afb10 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -436,10 +436,14 @@ def create_and_cleanup(name: str, dev_table_only: bool): snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True) snapshot.version = "test_version" + on_cleanup_mock = mocker.Mock() + evaluator.promote([snapshot], EnvironmentNamingInfo(name="test_env")) evaluator.cleanup( - [SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=dev_table_only)] + [SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=dev_table_only)], + on_complete=on_cleanup_mock, ) + assert on_cleanup_mock.call_count == 1 if dev_table_only else 2 return snapshot snapshot = create_and_cleanup("catalog.test_schema.test_model", True) @@ -611,6 +615,39 @@ def create_and_cleanup_external_model(name: str, dev_table_only: bool): adapter_mock.drop_table.assert_not_called() +def test_cleanup_symbolic_and_audit_snapshots_no_callback( + mocker: MockerFixture, adapter_mock, make_snapshot +): + evaluator = SnapshotEvaluator(adapter_mock) + on_complete_mock = mocker.Mock() + + # Test external model + external_model = ExternalModel( + name="test_schema.external_model", + kind=ExternalKind(), + ) + external_snapshot = make_snapshot(external_model) + external_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + # Test standalone audit + audit = StandaloneAudit(name="test_audit", query=parse_one("SELECT NULL LIMIT 0")) + audit_snapshot = make_snapshot(audit) + audit_snapshot.categorize_as(SnapshotChangeCategory.NON_BREAKING) + + evaluator.cleanup( + [ + SnapshotTableCleanupTask(snapshot=external_snapshot.table_info, dev_table_only=False), + SnapshotTableCleanupTask(snapshot=audit_snapshot.table_info, dev_table_only=False), + ], + on_complete=on_complete_mock, + ) + + # Verify that no physical tables were attempted to be dropped + adapter_mock.drop_table.assert_not_called() + adapter_mock.get_data_object.assert_not_called() + on_complete_mock.assert_not_called() + + @pytest.mark.parametrize("view_exists", [True, False]) def test_evaluate_materialized_view( mocker: MockerFixture, adapter_mock, make_snapshot, view_exists: bool diff --git a/tests/integrations/jupyter/test_magics.py b/tests/integrations/jupyter/test_magics.py index 0a39c155cf..991df8fc15 100644 --- a/tests/integrations/jupyter/test_magics.py +++ b/tests/integrations/jupyter/test_magics.py @@ -906,9 +906,6 @@ def test_destroy( "Are you ABSOLUTELY SURE you want to proceed with deletion? [y/n]:", "Environment 'prod' invalidated.", "Deleted object memory.sushi", - 'Deleted object "memory"."raw"."model1"', - 'Deleted object "memory"."raw"."model2"', - 'Deleted object "memory"."raw"."demographics"', "State tables removed.", "Destroy completed successfully.", ] From 012e5426befbc9ddb8aabde6332a8da1a30338b9 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 22 Sep 2025 16:32:31 -0700 Subject: [PATCH 0895/1056] fix: lowercase column names snowflake (#5425) --- sqlmesh/core/engine_adapter/snowflake.py | 3 +- tests/core/engine_adapter/test_snowflake.py | 35 ++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index c6b0e71ac3..355fb9719c 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -526,7 +526,8 @@ def _get_data_objects( type=DataObjectType.from_str(row.type), # type: ignore clustering_key=row.clustering_key, # type: ignore ) - for row in df.itertuples() + # lowercase the column names for cases where Snowflake might return uppercase column names for certain catalogs + for row in df.rename(columns={col: col.lower() for col in df.columns}).itertuples() ] def set_current_catalog(self, catalog: str) -> None: diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index 75ce8edbe0..62c4a4f3eb 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -7,9 +7,10 @@ import sqlmesh.core.dialect as d from sqlmesh.core.dialect import normalize_model_name +from sqlmesh.core.engine_adapter import SnowflakeEngineAdapter from sqlmesh.core.engine_adapter.base import EngineAdapter +from sqlmesh.core.engine_adapter.shared import DataObjectType from sqlmesh.core.model import load_sql_based_model -from sqlmesh.core.engine_adapter import SnowflakeEngineAdapter from sqlmesh.core.model.definition import SqlModel from sqlmesh.core.node import IntervalUnit from sqlmesh.utils.errors import SQLMeshError @@ -39,6 +40,38 @@ def test_get_temp_table(mocker: MockerFixture, make_mocked_engine_adapter: t.Cal assert value.sql(dialect=adapter.dialect) == '"CATALOG"."DB"."__temp_TEST_TABLE_abcdefgh"' +def test_get_data_objects_lowercases_columns( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +) -> None: + adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter, patch_get_data_objects=False) + + adapter.get_current_catalog = mocker.Mock(return_value="TEST_CATALOG") + + adapter.fetchdf = mocker.Mock( + return_value=pd.DataFrame( # type: ignore[assignment] + [ + { + "CATALOG": "TEST_CATALOG", + "NAME": "MY_TABLE", + "SCHEMA_NAME": "PUBLIC", + "TYPE": "TABLE", + "CLUSTERING_KEY": "ID", + } + ] + ) + ) + + data_objects = adapter._get_data_objects("TEST_CATALOG.PUBLIC") + + assert len(data_objects) == 1 + data_object = data_objects[0] + assert data_object.catalog == "TEST_CATALOG" + assert data_object.schema_name == "PUBLIC" + assert data_object.name == "MY_TABLE" + assert data_object.type == DataObjectType.TABLE + assert data_object.clustering_key == "ID" + + @pytest.mark.parametrize( "current_warehouse, current_warehouse_exp, configured_warehouse, configured_warehouse_exp, should_change", [ From bf40bbeafc07723d76d64da44c635387cda57b62 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:18:36 -0700 Subject: [PATCH 0896/1056] chore: fix dbt ci tests (#5426) --- Makefile | 11 ++++++++++- pyproject.toml | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 96305c4bfb..40874f7972 100644 --- a/Makefile +++ b/Makefile @@ -36,11 +36,16 @@ install-dev-dbt-%: if [ "$$version" = "1.10.0" ]; then \ echo "Applying special handling for dbt 1.10.0"; \ $(SED_INPLACE) -E 's/"(dbt-core)[^"]*"/"\1~='"$$version"'"/g' pyproject.toml; \ - $(SED_INPLACE) -E 's/"(dbt-(bigquery|duckdb|snowflake|athena-community|clickhouse|databricks|redshift|trino))[^"]*"/"\1"/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; \ else \ echo "Applying version $$version to all dbt packages"; \ $(SED_INPLACE) -E 's/"(dbt-[^"><=~!]+)[^"]*"/"\1~='"$$version"'"/g' pyproject.toml; \ fi; \ + if printf '%s\n' "$$version" | awk -F. '{ if ($$1 == 1 && (($$2 >= 3 && $$2 <= 5) || $$2 == 10)) exit 0; exit 1 }'; then \ + 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 "Applying overrides for dbt 1.6.0"; \ @@ -50,6 +55,10 @@ install-dev-dbt-%: echo "Applying overrides for dbt 1.7.0"; \ $(PIP) install 'databricks-sdk==0.28.0' --reinstall; \ fi; \ + if [ "$$version" = "1.5.0" ]; then \ + echo "Applying overrides for dbt 1.5.0"; \ + $(PIP) install 'dbt-databricks==1.5.6' 'numpy<2' --reinstall; \ + fi; \ mv pyproject.toml.backup pyproject.toml; \ echo "Restored original pyproject.toml" diff --git a/pyproject.toml b/pyproject.toml index bd34906434..1d34b340b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ dev = [ "google-cloud-bigquery-storage", "httpx", "mypy~=1.13.0", + "numpy", "pandas-stubs", "pre-commit", "psycopg2-binary", From e00e86064995841f097b62c9d32b6e99ed2a0aba Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:35:26 -0700 Subject: [PATCH 0897/1056] chore: fix tests to include dataframe (#5423) --- sqlmesh/core/engine_adapter/clickhouse.py | 3 ++- tests/core/engine_adapter/integration/conftest.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/engine_adapter/clickhouse.py b/sqlmesh/core/engine_adapter/clickhouse.py index ccffe64118..84d6ad311e 100644 --- a/sqlmesh/core/engine_adapter/clickhouse.py +++ b/sqlmesh/core/engine_adapter/clickhouse.py @@ -112,8 +112,9 @@ def query_factory() -> Query: storage_format=exp.var("MergeTree"), **kwargs, ) + ordered_df = df[list(source_columns_to_types)] - self.cursor.client.insert_df(temp_table.sql(dialect=self.dialect), df=df) + self.cursor.client.insert_df(temp_table.sql(dialect=self.dialect), df=ordered_df) return exp.select(*self._casted_columns(target_columns_to_types, source_columns)).from_( temp_table diff --git a/tests/core/engine_adapter/integration/conftest.py b/tests/core/engine_adapter/integration/conftest.py index eafdf2fe1d..30f934da63 100644 --- a/tests/core/engine_adapter/integration/conftest.py +++ b/tests/core/engine_adapter/integration/conftest.py @@ -148,7 +148,7 @@ def ctx_df( yield from create_test_context(*request.param) -@pytest.fixture(params=list(generate_pytest_params(ENGINES, query=True, df=False))) +@pytest.fixture(params=list(generate_pytest_params(ENGINES, query=True, df=True))) def ctx_query_and_df( request: FixtureRequest, create_test_context: t.Callable[[IntegrationTestEngine, str], t.Iterable[TestContext]], From 34dc9fde5214b20f22a0dd6dbeab1a693293aeee Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Tue, 23 Sep 2025 15:23:13 +1200 Subject: [PATCH 0898/1056] Feat!: bring dbt node information through to SQLMesh (#5412) --- .circleci/continue_config.yml | 7 +- .circleci/test_migration.sh | 41 ++++--- examples/sushi_dbt/config.py | 2 + sqlmesh/cli/project_init.py | 2 +- sqlmesh/core/audit/definition.py | 10 +- sqlmesh/core/context.py | 4 +- sqlmesh/core/model/definition.py | 4 + sqlmesh/core/node.py | 103 ++++++++++++++++- sqlmesh/core/scheduler.py | 4 +- sqlmesh/dbt/basemodel.py | 17 +-- sqlmesh/dbt/model.py | 2 +- sqlmesh/dbt/seed.py | 2 +- sqlmesh/dbt/test.py | 11 ++ .../v0098_add_dbt_node_info_in_node.py | 105 ++++++++++++++++++ sqlmesh_dbt/console.py | 20 +++- sqlmesh_dbt/operations.py | 10 +- tests/core/test_audit.py | 24 +++- tests/core/test_model.py | 29 ++++- tests/dbt/cli/conftest.py | 75 ------------- tests/dbt/cli/test_list.py | 51 +++++---- tests/dbt/cli/test_operations.py | 8 +- tests/dbt/cli/test_run.py | 8 +- tests/dbt/conftest.py | 87 +++++++++++++++ tests/dbt/test_config.py | 8 ++ tests/dbt/test_integration.py | 64 +++++++++++ tests/dbt/test_model.py | 102 +++++------------ .../dbt}/empty_project/dbt_project.yml | 2 +- .../dbt}/empty_project/profiles.yml | 2 +- .../dbt}/jaffle_shop_duckdb/dbt_project.yml | 0 .../jaffle_shop_duckdb/models/customers.sql | 0 .../dbt}/jaffle_shop_duckdb/models/docs.md | 0 .../dbt}/jaffle_shop_duckdb/models/orders.sql | 0 .../jaffle_shop_duckdb/models/overview.md | 0 .../dbt}/jaffle_shop_duckdb/models/schema.yml | 0 .../models/staging/schema.yml | 0 .../models/staging/stg_customers.sql | 0 .../models/staging/stg_orders.sql | 0 .../models/staging/stg_payments.sql | 0 .../dbt}/jaffle_shop_duckdb/profiles.yml | 0 .../dbt}/jaffle_shop_duckdb/seeds/.gitkeep | 0 .../seeds/raw_customers.csv | 0 .../jaffle_shop_duckdb/seeds/raw_orders.csv | 0 .../jaffle_shop_duckdb/seeds/raw_payments.csv | 0 43 files changed, 575 insertions(+), 229 deletions(-) create mode 100644 sqlmesh/migrations/v0098_add_dbt_node_info_in_node.py rename tests/{dbt/cli/fixtures => fixtures/dbt}/empty_project/dbt_project.yml (94%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/empty_project/profiles.yml (85%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/dbt_project.yml (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/models/customers.sql (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/models/docs.md (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/models/orders.sql (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/models/overview.md (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/models/schema.yml (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/models/staging/schema.yml (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/models/staging/stg_customers.sql (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/models/staging/stg_orders.sql (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/models/staging/stg_payments.sql (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/profiles.yml (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/seeds/.gitkeep (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/seeds/raw_customers.csv (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/seeds/raw_orders.csv (100%) rename tests/{dbt/cli/fixtures => fixtures/dbt}/jaffle_shop_duckdb/seeds/raw_payments.csv (100%) diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index e21f3d869b..c549c0ae78 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -144,8 +144,11 @@ jobs: - halt_unless_core - checkout - run: - name: Run the migration test - command: ./.circleci/test_migration.sh + name: Run the migration test - sushi + command: ./.circleci/test_migration.sh sushi "--gateway duckdb_persistent" + - run: + name: Run the migration test - sushi_dbt + command: ./.circleci/test_migration.sh sushi_dbt "--config migration_test_config" ui_style: docker: diff --git a/.circleci/test_migration.sh b/.circleci/test_migration.sh index a85d933bd3..9b8fe89e6e 100755 --- a/.circleci/test_migration.sh +++ b/.circleci/test_migration.sh @@ -1,11 +1,6 @@ #!/usr/bin/env bash set -ex -GATEWAY_NAME="duckdb_persistent" -TMP_DIR=$(mktemp -d) -SUSHI_DIR="$TMP_DIR/sushi" - - if [[ -z $(git tag --points-at HEAD) ]]; then # If the current commit is not tagged, we need to find the last tag LAST_TAG=$(git describe --tags --abbrev=0) @@ -14,28 +9,48 @@ else LAST_TAG=$(git tag --sort=-creatordate | head -n 2 | tail -n 1) fi +if [ "$1" == "" ]; then + echo "Usage: $0 " + echo "eg $0 sushi '--gateway duckdb_persistent'" + exit 1 +fi + + +TMP_DIR=$(mktemp -d) +EXAMPLE_NAME="$1" +SQLMESH_OPTS="$2" +EXAMPLE_DIR="./examples/$EXAMPLE_NAME" +TEST_DIR="$TMP_DIR/$EXAMPLE_NAME" + +echo "Running migration test for '$EXAMPLE_NAME' in '$TEST_DIR' for example project '$EXAMPLE_DIR' using options '$SQLMESH_OPTS'" + git checkout $LAST_TAG # Install dependencies from the previous release. make install-dev -cp -r ./examples/sushi $TMP_DIR +cp -r $EXAMPLE_DIR $TEST_DIR + +# this is only needed temporarily until the released tag for $LAST_TAG includes this config +if [ "$EXAMPLE_NAME" == "sushi_dbt" ]; then + echo 'migration_test_config = sqlmesh_config(Path(__file__).parent, dbt_target_name="duckdb")' >> $TEST_DIR/config.py +fi # Run initial plan -pushd $SUSHI_DIR +pushd $TEST_DIR rm -rf ./data/* -sqlmesh --gateway $GATEWAY_NAME plan --no-prompts --auto-apply +sqlmesh $SQLMESH_OPTS plan --no-prompts --auto-apply rm -rf .cache popd -# Switch back to the starting state of the repository +# Switch back to the starting state of the repository git checkout - # Install updated dependencies. make install-dev # Migrate and make sure the diff is empty -pushd $SUSHI_DIR -sqlmesh --gateway $GATEWAY_NAME migrate -sqlmesh --gateway $GATEWAY_NAME diff prod -popd +pushd $TEST_DIR +sqlmesh $SQLMESH_OPTS migrate +sqlmesh $SQLMESH_OPTS diff prod +popd \ No newline at end of file diff --git a/examples/sushi_dbt/config.py b/examples/sushi_dbt/config.py index e7e28c98e4..2305cf79f2 100644 --- a/examples/sushi_dbt/config.py +++ b/examples/sushi_dbt/config.py @@ -5,3 +5,5 @@ config = sqlmesh_config(Path(__file__).parent) test_config = config + +migration_test_config = sqlmesh_config(Path(__file__).parent, dbt_target_name="duckdb") diff --git a/sqlmesh/cli/project_init.py b/sqlmesh/cli/project_init.py index 0790562de7..6b4f6c7a83 100644 --- a/sqlmesh/cli/project_init.py +++ b/sqlmesh/cli/project_init.py @@ -298,6 +298,7 @@ def init_example_project( dlt_path: t.Optional[str] = None, schema_name: str = "sqlmesh_example", cli_mode: InitCliMode = InitCliMode.DEFAULT, + start: t.Optional[str] = None, ) -> Path: root_path = Path(path) @@ -336,7 +337,6 @@ def init_example_project( models: t.Set[t.Tuple[str, str]] = set() settings = None - start = None if engine_type and template == ProjectTemplate.DLT: project_dialect = dialect or DIALECT_TO_TYPE.get(engine_type) if pipeline and project_dialect: diff --git a/sqlmesh/core/audit/definition.py b/sqlmesh/core/audit/definition.py index 561ee539f6..9f470872fe 100644 --- a/sqlmesh/core/audit/definition.py +++ b/sqlmesh/core/audit/definition.py @@ -19,7 +19,7 @@ sorted_python_env_payloads, ) from sqlmesh.core.model.common import make_python_env, single_value_or_tuple, ParsableSql -from sqlmesh.core.node import _Node +from sqlmesh.core.node import _Node, DbtInfoMixin, DbtNodeInfo from sqlmesh.core.renderer import QueryRenderer from sqlmesh.utils.date import TimeLike from sqlmesh.utils.errors import AuditConfigError, SQLMeshError, raise_config_error @@ -120,7 +120,7 @@ def audit_map_validator(cls: t.Type, v: t.Any, values: t.Any) -> t.Dict[str, t.A return {} -class ModelAudit(PydanticModel, AuditMixin, frozen=True): +class ModelAudit(PydanticModel, AuditMixin, DbtInfoMixin, frozen=True): """ Audit is an assertion made about your tables. @@ -137,6 +137,7 @@ class ModelAudit(PydanticModel, AuditMixin, frozen=True): expressions_: t.Optional[t.List[ParsableSql]] = Field(default=None, alias="expressions") jinja_macros: JinjaMacroRegistry = JinjaMacroRegistry() formatting: t.Optional[bool] = Field(default=None, exclude=True) + dbt_node_info_: t.Optional[DbtNodeInfo] = Field(alias="dbt_node_info", default=None) _path: t.Optional[Path] = None @@ -150,6 +151,10 @@ def __str__(self) -> str: path = f": {self._path.name}" if self._path else "" return f"{self.__class__.__name__}<{self.name}{path}>" + @property + def dbt_node_info(self) -> t.Optional[DbtNodeInfo]: + return self.dbt_node_info_ + class StandaloneAudit(_Node, AuditMixin): """ @@ -552,4 +557,5 @@ def _maybe_parse_arg_pair(e: exp.Expression) -> t.Tuple[str, exp.Expression]: "depends_on_": lambda value: exp.Tuple(expressions=sorted(value)), "tags": single_value_or_tuple, "default_catalog": exp.to_identifier, + "dbt_node_info_": lambda value: value.to_expression(), } diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index d7a2984f3a..437fbd6edd 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1697,9 +1697,9 @@ def plan_builder( console=self.console, user_provided_flags=user_provided_flags, selected_models={ - dbt_name + dbt_unique_id for model in model_selector.expand_model_selections(select_models or "*") - if (dbt_name := snapshots[model].node.dbt_name) + if (dbt_unique_id := snapshots[model].node.dbt_unique_id) }, explain=explain or False, ignore_cron=ignore_cron or False, diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index c9eaa43b3e..974901cb55 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -1197,6 +1197,9 @@ def metadata_hash(self) -> str: for k, v in sorted(args.items()): metadata.append(f"{k}:{gen(v)}") + if self.dbt_node_info: + metadata.append(self.dbt_node_info.json(sort_keys=True)) + metadata.extend(self._additional_metadata) self._metadata_hash = hash_data(metadata) @@ -3019,6 +3022,7 @@ def render_expression( "formatting": str, "optimize_query": str, "virtual_environment_mode": lambda value: exp.Literal.string(value.value), + "dbt_node_info_": lambda value: value.to_expression(), } diff --git a/sqlmesh/core/node.py b/sqlmesh/core/node.py index b04a59a39f..4a3bf2564b 100644 --- a/sqlmesh/core/node.py +++ b/sqlmesh/core/node.py @@ -153,6 +153,101 @@ def milliseconds(self) -> int: return self.seconds * 1000 +class DbtNodeInfo(PydanticModel): + """ + Represents dbt-specific model information set by the dbt loader and intended to be made available at the Snapshot level + (as opposed to hidden within the individual model jinja macro registries). + + This allows for things like injecting implementations of variables / functions into the Jinja context that are compatible with + their dbt equivalents but are backed by the sqlmesh snapshots in any given plan / environment + """ + + unique_id: str + """This is the node/resource name/unique_id that's used as the node key in the dbt manifest. + It's prefixed by the resource type and is exposed in context variables like {{ selected_resources }}. + + Examples: + - test.jaffle_shop.unique_stg_orders_order_id.e3b841c71a + - seed.jaffle_shop.raw_payments + - model.jaffle_shop.stg_orders + """ + + name: str + """Name of this object in the dbt global namespace, used by things like {{ ref() }} calls. + + Examples: + - unique_stg_orders_order_id + - raw_payments + - stg_orders + """ + + fqn: str + """Used for selectors in --select/--exclude. + Takes the filesystem into account so may be structured differently to :unique_id. + + Examples: + - jaffle_shop.staging.unique_stg_orders_order_id + - jaffle_shop.raw_payments + - jaffle_shop.staging.stg_orders + """ + + alias: t.Optional[str] = None + """This is dbt's way of overriding the _physical table_ a model is written to. + + It's used in the following situation: + - Say you have two models, "stg_customers" and "customers" + - You want "stg_customers" to be written to the "staging" schema as eg "staging.customers" - NOT "staging.stg_customers" + - But you cant rename the file to "customers" because it will conflict with your other model file "customers" + - Even if you put it in a different folder, eg "staging/customers.sql" - dbt still has a global namespace so it will conflict + when you try to do something like "{{ ref('customers') }}" + - So dbt's solution to this problem is to keep calling it "stg_customers" at the dbt project/model level, + but allow overriding the physical table to "customers" via something like "{{ config(alias='customers', schema='staging') }}" + + Note that if :alias is set, it does *not* replace :name at the model level and cannot be used interchangably with :name. + It also does not affect the :fqn or :unique_id. It's just used to override :name when it comes time to generate the physical table name. + """ + + @model_validator(mode="after") + def post_init(self) -> Self: + # by default, dbt sets alias to the same as :name + # however, we only want to include :alias if it is actually different / actually providing an override + if self.alias == self.name: + self.alias = None + return self + + def to_expression(self) -> exp.Expression: + """Produce a SQLGlot expression representing this object, for use in things like the model/audit definition renderers""" + return exp.tuple_( + *( + exp.PropertyEQ(this=exp.var(k), expression=exp.Literal.string(v)) + for k, v in sorted(self.model_dump(exclude_none=True).items()) + ) + ) + + +class DbtInfoMixin: + """This mixin encapsulates properties that only exist for dbt compatibility and are otherwise not required + for native projects""" + + @property + def dbt_node_info(self) -> t.Optional[DbtNodeInfo]: + raise NotImplementedError() + + @property + def dbt_unique_id(self) -> t.Optional[str]: + """Used for compatibility with jinja context variables such as {{ selected_resources }}""" + if self.dbt_node_info: + return self.dbt_node_info.unique_id + return None + + @property + def dbt_fqn(self) -> t.Optional[str]: + """Used in the selector engine for compatibility with selectors that select models by dbt fqn""" + if self.dbt_node_info: + return self.dbt_node_info.fqn + return None + + # this must be sorted in descending order INTERVAL_SECONDS = { IntervalUnit.YEAR: 60 * 60 * 24 * 365, @@ -165,7 +260,7 @@ def milliseconds(self) -> int: } -class _Node(PydanticModel): +class _Node(DbtInfoMixin, PydanticModel): """ Node is the core abstraction for entity that can be executed within the scheduler. @@ -199,7 +294,7 @@ class _Node(PydanticModel): interval_unit_: t.Optional[IntervalUnit] = Field(alias="interval_unit", default=None) tags: t.List[str] = [] stamp: t.Optional[str] = None - dbt_name: t.Optional[str] = None # dbt node name + dbt_node_info_: t.Optional[DbtNodeInfo] = Field(alias="dbt_node_info", default=None) _path: t.Optional[Path] = None _data_hash: t.Optional[str] = None _metadata_hash: t.Optional[str] = None @@ -446,6 +541,10 @@ def is_audit(self) -> bool: """Return True if this is an audit node""" return False + @property + def dbt_node_info(self) -> t.Optional[DbtNodeInfo]: + return self.dbt_node_info_ + class NodeType(str, Enum): MODEL = "model" diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index fd2e1cf004..af4d72b165 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -839,7 +839,9 @@ def _run_or_audit( run_environment_statements=run_environment_statements, audit_only=audit_only, auto_restatement_triggers=auto_restatement_triggers, - selected_models={s.node.dbt_name for s in merged_intervals if s.node.dbt_name}, + selected_models={ + s.node.dbt_unique_id for s in merged_intervals if s.node.dbt_unique_id + }, ) return CompletionStatus.FAILURE if errors else CompletionStatus.SUCCESS diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index 3534b95bc3..4637bbf91c 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -13,6 +13,7 @@ from sqlmesh.core.config.base import UpdateStrategy from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.model import Model +from sqlmesh.core.node import DbtNodeInfo from sqlmesh.dbt.column import ( ColumnConfig, column_descriptions_to_sqlmesh, @@ -120,8 +121,10 @@ class BaseModelConfig(GeneralConfig): grain: t.Union[str, t.List[str]] = [] # DBT configuration fields + unique_id: str = "" name: str = "" package_name: str = "" + fqn: t.List[str] = [] schema_: str = Field("", alias="schema") database: t.Optional[str] = None alias: t.Optional[str] = None @@ -273,12 +276,10 @@ def sqlmesh_config_fields(self) -> t.Set[str]: return {"description", "owner", "stamp", "storage_format"} @property - def node_name(self) -> str: - resource_type = getattr(self, "resource_type", "model") - node_name = f"{resource_type}.{self.package_name}.{self.name}" - if self.version: - node_name += f".v{self.version}" - return node_name + def node_info(self) -> DbtNodeInfo: + return DbtNodeInfo( + unique_id=self.unique_id, name=self.name, fqn=".".join(self.fqn), alias=self.alias + ) def sqlmesh_model_kwargs( self, @@ -349,8 +350,8 @@ def to_sqlmesh( def _model_jinja_context( self, context: DbtContext, dependencies: Dependencies ) -> t.Dict[str, t.Any]: - if context._manifest and self.node_name in context._manifest._manifest.nodes: - attributes = context._manifest._manifest.nodes[self.node_name].to_dict() + if context._manifest and self.unique_id in context._manifest._manifest.nodes: + attributes = context._manifest._manifest.nodes[self.unique_id].to_dict() if dependencies.model_attrs.all_attrs: model_node: AttributeDict[str, t.Any] = AttributeDict(attributes) else: diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index efad5e790b..9386b0b4f8 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -694,7 +694,7 @@ def to_sqlmesh( extract_dependencies_from_query=False, allow_partials=allow_partials, virtual_environment_mode=virtual_environment_mode, - dbt_name=self.node_name, + dbt_node_info=self.node_info, **optional_kwargs, **model_kwargs, ) diff --git a/sqlmesh/dbt/seed.py b/sqlmesh/dbt/seed.py index d6ecc768f9..c0c8186f29 100644 --- a/sqlmesh/dbt/seed.py +++ b/sqlmesh/dbt/seed.py @@ -92,7 +92,7 @@ def to_sqlmesh( audit_definitions=audit_definitions, virtual_environment_mode=virtual_environment_mode, start=self.start or context.sqlmesh_config.model_defaults.start, - dbt_name=self.node_name, + dbt_node_info=self.node_info, **kwargs, ) diff --git a/sqlmesh/dbt/test.py b/sqlmesh/dbt/test.py index 5c18ff4d81..747c9d469c 100644 --- a/sqlmesh/dbt/test.py +++ b/sqlmesh/dbt/test.py @@ -8,6 +8,7 @@ from pydantic import Field import sqlmesh.core.dialect as d from sqlmesh.core.audit import Audit, ModelAudit, StandaloneAudit +from sqlmesh.core.node import DbtNodeInfo from sqlmesh.dbt.common import ( Dependencies, GeneralConfig, @@ -79,8 +80,10 @@ class TestConfig(GeneralConfig): dialect_: t.Optional[str] = Field(None, alias="dialect") # dbt fields + unique_id: str = "" package_name: str = "" alias: t.Optional[str] = None + fqn: t.List[str] = [] schema_: t.Optional[str] = Field("", alias="schema") database: t.Optional[str] = None severity: Severity = Severity.ERROR @@ -155,6 +158,7 @@ def to_sqlmesh(self, context: DbtContext) -> Audit: jinja_macros.add_globals({"this": self.relation_info}) audit = StandaloneAudit( name=self.name, + dbt_node_info=self.node_info, dialect=self.dialect(context), skip=skip, query=query, @@ -171,6 +175,7 @@ def to_sqlmesh(self, context: DbtContext) -> Audit: else: audit = ModelAudit( name=self.name, + dbt_node_info=self.node_info, dialect=self.dialect(context), skip=skip, blocking=blocking, @@ -214,6 +219,12 @@ def relation_info(self) -> AttributeDict: } ) + @property + def node_info(self) -> DbtNodeInfo: + return DbtNodeInfo( + unique_id=self.unique_id, name=self.name, fqn=".".join(self.fqn), alias=self.alias + ) + def _remove_jinja_braces(jinja_str: str) -> str: no_braces = jinja_str diff --git a/sqlmesh/migrations/v0098_add_dbt_node_info_in_node.py b/sqlmesh/migrations/v0098_add_dbt_node_info_in_node.py new file mode 100644 index 0000000000..c8acd0bafd --- /dev/null +++ b/sqlmesh/migrations/v0098_add_dbt_node_info_in_node.py @@ -0,0 +1,105 @@ +"""Replace 'dbt_name' with 'dbt_node_info' in the snapshot definition""" + +import json +from sqlglot import exp +from sqlmesh.utils.migration import index_text_type, blob_text_type + + +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore + import pandas as pd + + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + snapshots_table = "_snapshots" + if schema: + snapshots_table = f"{schema}.{snapshots_table}" + + index_type = index_text_type(engine_adapter.dialect) + blob_type = blob_text_type(engine_adapter.dialect) + + new_snapshots = [] + migration_needed = False + + for ( + name, + identifier, + version, + snapshot, + kind_name, + updated_ts, + unpaused_ts, + ttl_ms, + unrestorable, + forward_only, + dev_version, + fingerprint, + ) in engine_adapter.fetchall( + exp.select( + "name", + "identifier", + "version", + "snapshot", + "kind_name", + "updated_ts", + "unpaused_ts", + "ttl_ms", + "unrestorable", + "forward_only", + "dev_version", + "fingerprint", + ).from_(snapshots_table), + quote_identifiers=True, + ): + parsed_snapshot = json.loads(snapshot) + if dbt_name := parsed_snapshot["node"].get("dbt_name"): + parsed_snapshot["node"].pop("dbt_name") + parsed_snapshot["node"]["dbt_node_info"] = { + "unique_id": dbt_name, + # these will get populated as metadata-only changes on the next plan + "name": "", + "fqn": "", + } + migration_needed = True + + new_snapshots.append( + { + "name": name, + "identifier": identifier, + "version": version, + "snapshot": json.dumps(parsed_snapshot), + "kind_name": kind_name, + "updated_ts": updated_ts, + "unpaused_ts": unpaused_ts, + "ttl_ms": ttl_ms, + "unrestorable": unrestorable, + "forward_only": forward_only, + "dev_version": dev_version, + "fingerprint": fingerprint, + } + ) + + if migration_needed and new_snapshots: + engine_adapter.delete_from(snapshots_table, "TRUE") + + engine_adapter.insert_append( + snapshots_table, + pd.DataFrame(new_snapshots), + target_columns_to_types={ + "name": exp.DataType.build(index_type), + "identifier": exp.DataType.build(index_type), + "version": exp.DataType.build(index_type), + "snapshot": exp.DataType.build(blob_type), + "kind_name": exp.DataType.build(index_type), + "updated_ts": exp.DataType.build("bigint"), + "unpaused_ts": exp.DataType.build("bigint"), + "ttl_ms": exp.DataType.build("bigint"), + "unrestorable": exp.DataType.build("boolean"), + "forward_only": exp.DataType.build("boolean"), + "dev_version": exp.DataType.build(index_type), + "fingerprint": exp.DataType.build(blob_type), + }, + ) diff --git a/sqlmesh_dbt/console.py b/sqlmesh_dbt/console.py index 3c62adfe68..6bf7a1618f 100644 --- a/sqlmesh_dbt/console.py +++ b/sqlmesh_dbt/console.py @@ -1,6 +1,7 @@ import typing as t from sqlmesh.core.console import TerminalConsole from sqlmesh.core.model import Model +from sqlmesh.core.snapshot.definition import Node from rich.tree import Tree @@ -9,19 +10,26 @@ def print(self, msg: str) -> None: return self._print(msg) def list_models( - self, models: t.List[Model], list_parents: bool = True, list_audits: bool = True + self, + models: t.List[Model], + all_nodes: t.Dict[str, Node], + list_parents: bool = True, + list_audits: bool = True, ) -> None: model_list = Tree("[bold]Models in project:[/bold]") for model in models: - model_tree = model_list.add(model.name) + model_tree = model_list.add(model.dbt_fqn or model.name) if list_parents: - for parent in model.depends_on: - model_tree.add(f"depends_on: {parent}") + for parent_name in model.depends_on: + if parent := all_nodes.get(parent_name): + parent_name = parent.dbt_fqn or parent_name + + model_tree.add(f"depends_on: {parent_name}") if list_audits: - for audit_name in model.audit_definitions: - model_tree.add(f"audit: {audit_name}") + for audit_name, audit in model.audit_definitions.items(): + model_tree.add(f"audit: {audit.dbt_fqn or audit_name}") self._print(model_list) diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index f95d0d931e..e15a2cb93e 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -32,7 +32,9 @@ def list_( # - "data tests" (audits) for those models # it also applies selectors which is useful for testing selectors selected_models = list(self._selected_models(select, exclude).values()) - self.console.list_models(selected_models) + self.console.list_models( + selected_models, {k: v.node for k, v in self.context.snapshots.items()} + ) def run( self, @@ -260,7 +262,7 @@ def create( return DbtOperations(sqlmesh_context, dbt_project, debug=debug) -def init_project_if_required(project_dir: Path) -> None: +def init_project_if_required(project_dir: Path, start: t.Optional[str] = None) -> None: """ SQLMesh needs a start date to as the starting point for calculating intervals on incremental models, amongst other things @@ -276,4 +278,6 @@ def init_project_if_required(project_dir: Path) -> None: if not any(f.exists() for f in [project_dir / file for file in ALL_CONFIG_FILENAMES]): get_console().log_warning("No existing SQLMesh config detected; creating one") - init_example_project(path=project_dir, engine_type=None, template=ProjectTemplate.DBT) + init_example_project( + path=project_dir, engine_type=None, template=ProjectTemplate.DBT, start=start + ) diff --git a/tests/core/test_audit.py b/tests/core/test_audit.py index ed67975e9e..2ffcbbc4b2 100644 --- a/tests/core/test_audit.py +++ b/tests/core/test_audit.py @@ -5,6 +5,7 @@ from sqlmesh.core import constants as c from sqlmesh.core.config.model import ModelDefaultsConfig from sqlmesh.core.context import Context +from sqlmesh.core.node import DbtNodeInfo from sqlmesh.core.audit import ( ModelAudit, StandaloneAudit, @@ -12,7 +13,7 @@ load_audit, load_multiple_audits, ) -from sqlmesh.core.dialect import parse +from sqlmesh.core.dialect import parse, jinja_query from sqlmesh.core.model import ( FullKind, IncrementalByTimeRangeKind, @@ -730,6 +731,27 @@ def test_render_definition(): assert "def test_macro(evaluator, v):" in format_model_expressions(audit.render_definition()) +def test_render_definition_dbt_node_info(): + node_info = DbtNodeInfo( + unique_id="test.project.my_audit", name="my_audit", fqn="project.my_audit" + ) + + audit = StandaloneAudit(name="my_audit", dbt_node_info=node_info, query=jinja_query("select 1")) + + assert ( + audit.render_definition()[0].sql(pretty=True) + == """AUDIT ( + name my_audit, + dbt_node_info ( + fqn := 'project.my_audit', + name := 'my_audit', + unique_id := 'test.project.my_audit' + ), + standalone TRUE +)""" + ) + + def test_text_diff(): expressions = parse( """ diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 00ff48b0d2..726ac52b66 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -61,7 +61,7 @@ from sqlmesh.core.model.common import parse_expression from sqlmesh.core.model.kind import ModelKindName, _model_kind_validator from sqlmesh.core.model.seed import CsvSettings -from sqlmesh.core.node import IntervalUnit, _Node +from sqlmesh.core.node import IntervalUnit, _Node, DbtNodeInfo from sqlmesh.core.signal import signal from sqlmesh.core.snapshot import Snapshot, SnapshotChangeCategory from sqlmesh.utils.date import TimeLike, to_datetime, to_ds, to_timestamp @@ -2100,6 +2100,33 @@ def test_render_definition_with_virtual_update_statements(): ) +def test_render_definition_dbt_node_info(): + node_info = DbtNodeInfo(unique_id="model.db.table", name="table", fqn="db.table") + model = load_sql_based_model( + d.parse( + f""" + MODEL ( + name db.table, + kind FULL + ); + + select 1 as a; + """ + ), + dbt_node_info=node_info, + ) + + assert model.dbt_node_info + assert ( + model.render_definition()[0].sql(pretty=True) + == """MODEL ( + name db.table, + dbt_node_info (fqn := 'db.table', name := 'table', unique_id := 'model.db.table'), + kind FULL +)""" + ) + + def test_cron(): daily = _Node(name="x", cron="@daily") assert to_datetime(daily.cron_prev("2020-01-01")) == to_datetime("2019-12-31") diff --git a/tests/dbt/cli/conftest.py b/tests/dbt/cli/conftest.py index e555f9144a..26757bf3ab 100644 --- a/tests/dbt/cli/conftest.py +++ b/tests/dbt/cli/conftest.py @@ -1,82 +1,7 @@ import typing as t -from pathlib import Path -import os import functools from click.testing import CliRunner, Result -from sqlmesh_dbt.operations import init_project_if_required import pytest -import uuid - - -class EmptyProjectCreator(t.Protocol): - def __call__( - self, project_name: t.Optional[str] = None, target_name: t.Optional[str] = None - ) -> Path: ... - - -@pytest.fixture -def jaffle_shop_duckdb(copy_to_temp_path: t.Callable[..., t.List[Path]]) -> t.Iterable[Path]: - fixture_path = Path(__file__).parent / "fixtures" / "jaffle_shop_duckdb" - assert fixture_path.exists() - - current_path = os.getcwd() - output_path = copy_to_temp_path(paths=fixture_path)[0] - - # so that we can invoke commands from the perspective of a user that is already in the correct directory - os.chdir(output_path) - - yield output_path - - os.chdir(current_path) - - -@pytest.fixture -def create_empty_project( - copy_to_temp_path: t.Callable[..., t.List[Path]], -) -> t.Iterable[t.Callable[..., Path]]: - default_project_name = f"test_{str(uuid.uuid4())[:8]}" - default_target_name = "duckdb" - fixture_path = Path(__file__).parent / "fixtures" / "empty_project" - assert fixture_path.exists() - - current_path = os.getcwd() - - def _create_empty_project( - project_name: t.Optional[str] = None, target_name: t.Optional[str] = None - ) -> Path: - project_name = project_name or default_project_name - target_name = target_name or default_target_name - output_path = copy_to_temp_path(paths=fixture_path)[0] - - dbt_project_yml = output_path / "dbt_project.yml" - profiles_yml = output_path / "profiles.yml" - - assert dbt_project_yml.exists() - assert profiles_yml.exists() - - (output_path / "models").mkdir() - (output_path / "seeds").mkdir() - - dbt_project_yml.write_text( - dbt_project_yml.read_text().replace("empty_project", project_name) - ) - profiles_yml.write_text( - profiles_yml.read_text() - .replace("empty_project", project_name) - .replace("__DEFAULT_TARGET__", target_name) - ) - - init_project_if_required(output_path) - - # so that we can invoke commands from the perspective of a user that is already in the correct directory - os.chdir(output_path) - - return output_path - - yield _create_empty_project - - # cleanup - switch cwd back to original - os.chdir(current_path) @pytest.fixture diff --git a/tests/dbt/cli/test_list.py b/tests/dbt/cli/test_list.py index 1bc22ce87e..4d294decc1 100644 --- a/tests/dbt/cli/test_list.py +++ b/tests/dbt/cli/test_list.py @@ -12,10 +12,10 @@ def test_list(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): assert result.exit_code == 0 assert not result.exception - assert "main.orders" in result.output - assert "main.customers" in result.output - assert "main.stg_payments" in result.output - assert "main.raw_orders" in result.output + assert "─ jaffle_shop.orders" in result.output + assert "─ jaffle_shop.customers" in result.output + assert "─ jaffle_shop.staging.stg_payments" in result.output + assert "─ jaffle_shop.raw_orders" in result.output def test_list_select(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): @@ -24,12 +24,12 @@ def test_list_select(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Resul assert result.exit_code == 0 assert not result.exception - assert "main.customers" in result.output - assert "main.stg_customers" in result.output - assert "main.raw_customers" in result.output + assert "─ jaffle_shop.customers" in result.output + assert "─ jaffle_shop.staging.stg_customers" in result.output + assert "─ jaffle_shop.raw_customers" in result.output - assert "main.stg_payments" not in result.output - assert "main.raw_orders" not in result.output + assert "─ jaffle_shop.staging.stg_payments" not in result.output + assert "─ jaffle_shop.raw_orders" not in result.output def test_list_select_exclude(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): @@ -39,13 +39,13 @@ def test_list_select_exclude(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[.. assert result.exit_code == 0 assert not result.exception - assert "main.customers" in result.output - assert "main.stg_customers" in result.output - assert "main.raw_customers" in result.output + assert "─ jaffle_shop.customers" in result.output + assert "─ jaffle_shop.staging.stg_customers" in result.output + assert "─ jaffle_shop.raw_customers" in result.output - assert "main.orders" not in result.output - assert "main.stg_payments" not in result.output - assert "main.raw_orders" not in result.output + assert "─ jaffle_shop.orders" not in result.output + assert "─ jaffle_shop.staging.stg_payments" not in result.output + assert "─ jaffle_shop.raw_orders" not in result.output # multiple exclude for args in ( @@ -56,21 +56,26 @@ def test_list_select_exclude(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[.. assert result.exit_code == 0 assert not result.exception - assert "main.stg_orders" in result.output + assert "─ jaffle_shop.staging.stg_orders" in result.output - assert "main.customers" not in result.output - assert "main.orders" not in result.output + assert "─ jaffle_shop.customers" not in result.output + assert "─ jaffle_shop.orders" not in result.output def test_list_with_vars(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): - (jaffle_shop_duckdb / "models" / "aliased_model.sql").write_text(""" - {{ config(alias='model_' + var('foo')) }} - select 1 + ( + jaffle_shop_duckdb / "models" / "vars_model.sql" + ).write_text(""" + select * from {{ ref('custom' + var('foo')) }} """) - result = invoke_cli(["list", "--vars", "foo: bar"]) + result = invoke_cli(["list", "--vars", "foo: ers"]) assert result.exit_code == 0 assert not result.exception - assert "model_bar" in result.output + assert ( + """├── jaffle_shop.vars_model +│ └── depends_on: jaffle_shop.customers""" + in result.output + ) diff --git a/tests/dbt/cli/test_operations.py b/tests/dbt/cli/test_operations.py index e9c4dc0063..769887efe4 100644 --- a/tests/dbt/cli/test_operations.py +++ b/tests/dbt/cli/test_operations.py @@ -8,7 +8,7 @@ import time_machine from sqlmesh.core.plan import PlanBuilder from sqlmesh.core.config.common import VirtualEnvironmentMode -from tests.dbt.cli.conftest import EmptyProjectCreator +from tests.dbt.conftest import EmptyProjectCreator pytestmark = pytest.mark.slow @@ -273,7 +273,7 @@ def test_run_option_full_refresh( create_empty_project: EmptyProjectCreator, env_name: str, vde_mode: VirtualEnvironmentMode ): # create config file prior to load - project_path = create_empty_project(project_name="test") + project_path, models_path = create_empty_project(project_name="test") config_path = project_path / "sqlmesh.yaml" config = yaml.load(config_path) @@ -282,8 +282,8 @@ def test_run_option_full_refresh( with config_path.open("w") as f: yaml.dump(config, f) - (project_path / "models" / "model_a.sql").write_text("select 1") - (project_path / "models" / "model_b.sql").write_text("select 2") + (models_path / "model_a.sql").write_text("select 1") + (models_path / "model_b.sql").write_text("select 2") operations = create(project_dir=project_path) diff --git a/tests/dbt/cli/test_run.py b/tests/dbt/cli/test_run.py index 9af1de8561..788a7b04a8 100644 --- a/tests/dbt/cli/test_run.py +++ b/tests/dbt/cli/test_run.py @@ -5,7 +5,7 @@ import time_machine from sqlmesh_dbt.operations import create from tests.cli.test_cli import FREEZE_TIME -from tests.dbt.cli.conftest import EmptyProjectCreator +from tests.dbt.conftest import EmptyProjectCreator pytestmark = pytest.mark.slow @@ -45,13 +45,13 @@ def test_run_with_selectors(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[... def test_run_with_changes_and_full_refresh( create_empty_project: EmptyProjectCreator, invoke_cli: t.Callable[..., Result] ): - project_path = create_empty_project(project_name="test") + project_path, models_path = create_empty_project(project_name="test") engine_adapter = create(project_path).context.engine_adapter engine_adapter.execute("create table external_table as select 'foo' as a, 'bar' as b") - (project_path / "models" / "model_a.sql").write_text("select a, b from external_table") - (project_path / "models" / "model_b.sql").write_text("select a, b from {{ ref('model_a') }}") + (models_path / "model_a.sql").write_text("select a, b from external_table") + (models_path / "model_b.sql").write_text("select a, b from {{ ref('model_a') }}") # populate initial env result = invoke_cli(["run"]) diff --git a/tests/dbt/conftest.py b/tests/dbt/conftest.py index 5875d9f575..56d77e7496 100644 --- a/tests/dbt/conftest.py +++ b/tests/dbt/conftest.py @@ -1,6 +1,8 @@ from __future__ import annotations import typing as t +import os +from pathlib import Path import pytest @@ -8,6 +10,17 @@ from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.project import Project from sqlmesh.dbt.target import PostgresConfig +from sqlmesh_dbt.operations import init_project_if_required +import uuid + + +class EmptyProjectCreator(t.Protocol): + def __call__( + self, + project_name: t.Optional[str] = None, + target_name: t.Optional[str] = None, + start: t.Optional[str] = None, + ) -> t.Tuple[Path, Path]: ... @pytest.fixture() @@ -15,6 +28,80 @@ def sushi_test_project(sushi_test_dbt_context: Context) -> Project: return sushi_test_dbt_context._loaders[0]._load_projects()[0] # type: ignore +@pytest.fixture +def create_empty_project( + copy_to_temp_path: t.Callable[..., t.List[Path]], +) -> t.Iterable[EmptyProjectCreator]: + default_project_name = f"test_{str(uuid.uuid4())[:8]}" + default_target_name = "duckdb" + fixture_path = Path(__file__).parent.parent / "fixtures" / "dbt" / "empty_project" + assert fixture_path.exists() + + current_path = os.getcwd() + + def _create_empty_project( + project_name: t.Optional[str] = None, + target_name: t.Optional[str] = None, + start: t.Optional[str] = None, + ) -> t.Tuple[Path, Path]: + project_name = project_name or default_project_name + target_name = target_name or default_target_name + output_path = copy_to_temp_path(paths=fixture_path)[0] + + dbt_project_yml = output_path / "dbt_project.yml" + profiles_yml = output_path / "profiles.yml" + + assert dbt_project_yml.exists() + assert profiles_yml.exists() + + models_path = output_path / "models" + (models_path).mkdir() + (output_path / "seeds").mkdir() + + dbt_project_yml.write_text( + dbt_project_yml.read_text().replace("empty_project", project_name) + ) + profiles_yml.write_text( + profiles_yml.read_text() + .replace("empty_project", project_name) + .replace("__DEFAULT_TARGET__", target_name) + ) + + init_project_if_required(output_path, start) + + # so that we can invoke commands from the perspective of a user that is already in the correct directory + os.chdir(output_path) + + return output_path, models_path + + yield _create_empty_project + + # cleanup - switch cwd back to original + os.chdir(current_path) + + +@pytest.fixture +def jaffle_shop_duckdb(copy_to_temp_path: t.Callable[..., t.List[Path]]) -> t.Iterable[Path]: + fixture_path = Path(__file__).parent.parent / "fixtures" / "dbt" / "jaffle_shop_duckdb" + assert fixture_path.exists() + + current_path = os.getcwd() + output_path = copy_to_temp_path(paths=fixture_path)[0] + + # so that we can invoke commands from the perspective of a user that is alrady in the correct directory + os.chdir(output_path) + + yield output_path + + os.chdir(current_path) + + +@pytest.fixture +def jaffle_shop_duckdb_context(jaffle_shop_duckdb: Path) -> Context: + init_project_if_required(jaffle_shop_duckdb) + return Context(paths=[jaffle_shop_duckdb]) + + @pytest.fixture() def runtime_renderer() -> t.Callable: def create_renderer(context: DbtContext, **kwargs: t.Any) -> t.Callable: diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index fe226d4926..0e96024aa1 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -91,8 +91,10 @@ def test_update(current: t.Dict[str, t.Any], new: t.Dict[str, t.Any], expected: def test_model_to_sqlmesh_fields(dbt_dummy_postgres_config: PostgresConfig): model_config = ModelConfig( + unique_id="model.package.name", name="name", package_name="package", + fqn=["package", "name"], alias="model", schema="custom", database="database", @@ -123,6 +125,8 @@ def test_model_to_sqlmesh_fields(dbt_dummy_postgres_config: PostgresConfig): assert isinstance(model, SqlModel) assert model.name == "database.custom.model" + assert model.dbt_unique_id == "model.package.name" + assert model.dbt_fqn == "package.name" assert model.description == "test model" assert ( model.render_query_or_raise().sql() @@ -185,7 +189,9 @@ def test_model_to_sqlmesh_fields(dbt_dummy_postgres_config: PostgresConfig): def test_test_to_sqlmesh_fields(): sql = "SELECT * FROM FOO WHERE cost > 100" test_config = TestConfig( + unique_id="test.test_package.foo_test", name="foo_test", + fqn=["test_package", "foo_test"], sql=sql, model_name="Foo", column_name="cost", @@ -199,6 +205,8 @@ def test_test_to_sqlmesh_fields(): audit = test_config.to_sqlmesh(context) assert audit.name == "foo_test" + assert audit.dbt_unique_id == "test.test_package.foo_test" + assert audit.dbt_fqn == "test_package.foo_test" assert audit.dialect == "duckdb" assert not audit.skip assert audit.blocking diff --git a/tests/dbt/test_integration.py b/tests/dbt/test_integration.py index 5a944d55d4..e1f051dbcf 100644 --- a/tests/dbt/test_integration.py +++ b/tests/dbt/test_integration.py @@ -540,3 +540,67 @@ def test_scd_type_2_by_column( ) df_expected = create_df(expected_table_data, self.target_schema) compare_dataframes(df_actual, df_expected, msg=f"Failed on time {time}") + + +def test_dbt_node_info(jaffle_shop_duckdb_context: Context): + ctx = jaffle_shop_duckdb_context + + customers = ctx.models['"jaffle_shop"."main"."customers"'] + assert customers.dbt_unique_id == "model.jaffle_shop.customers" + assert customers.dbt_fqn == "jaffle_shop.customers" + assert customers.dbt_node_info + assert customers.dbt_node_info.name == "customers" + + orders = ctx.models['"jaffle_shop"."main"."orders"'] + assert orders.dbt_unique_id == "model.jaffle_shop.orders" + assert orders.dbt_fqn == "jaffle_shop.orders" + assert orders.dbt_node_info + assert orders.dbt_node_info.name == "orders" + + stg_customers = ctx.models['"jaffle_shop"."main"."stg_customers"'] + assert stg_customers.dbt_unique_id == "model.jaffle_shop.stg_customers" + assert stg_customers.dbt_fqn == "jaffle_shop.staging.stg_customers" + assert stg_customers.dbt_node_info + assert stg_customers.dbt_node_info.name == "stg_customers" + + stg_orders = ctx.models['"jaffle_shop"."main"."stg_orders"'] + assert stg_orders.dbt_unique_id == "model.jaffle_shop.stg_orders" + assert stg_orders.dbt_fqn == "jaffle_shop.staging.stg_orders" + assert stg_orders.dbt_node_info + assert stg_orders.dbt_node_info.name == "stg_orders" + + raw_customers = ctx.models['"jaffle_shop"."main"."raw_customers"'] + assert raw_customers.dbt_unique_id == "seed.jaffle_shop.raw_customers" + assert raw_customers.dbt_fqn == "jaffle_shop.raw_customers" + assert raw_customers.dbt_node_info + assert raw_customers.dbt_node_info.name == "raw_customers" + + raw_orders = ctx.models['"jaffle_shop"."main"."raw_orders"'] + assert raw_orders.dbt_unique_id == "seed.jaffle_shop.raw_orders" + assert raw_orders.dbt_fqn == "jaffle_shop.raw_orders" + assert raw_orders.dbt_node_info + assert raw_orders.dbt_node_info.name == "raw_orders" + + raw_payments = ctx.models['"jaffle_shop"."main"."raw_payments"'] + assert raw_payments.dbt_unique_id == "seed.jaffle_shop.raw_payments" + assert raw_payments.dbt_fqn == "jaffle_shop.raw_payments" + assert raw_payments.dbt_node_info + assert raw_payments.dbt_node_info.name == "raw_payments" + + relationship_audit = ctx.snapshots[ + "relationships_orders_customer_id__customer_id__ref_customers_" + ] + assert relationship_audit.node.is_audit + assert ( + relationship_audit.node.dbt_unique_id + == "test.jaffle_shop.relationships_orders_customer_id__customer_id__ref_customers_.c6ec7f58f2" + ) + assert ( + relationship_audit.node.dbt_fqn + == "jaffle_shop.relationships_orders_customer_id__customer_id__ref_customers_" + ) + assert relationship_audit.node.dbt_node_info + assert ( + relationship_audit.node.dbt_node_info.name + == "relationships_orders_customer_id__customer_id__ref_customers_" + ) diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index 7bcfe98768..a64b29e89d 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -1,5 +1,4 @@ import datetime -import typing as t import pytest from pathlib import Path @@ -16,53 +15,11 @@ from sqlmesh.dbt.target import PostgresConfig from sqlmesh.dbt.test import TestConfig from sqlmesh.utils.yaml import YAML +from sqlmesh.utils.date import to_ds pytestmark = pytest.mark.dbt -@pytest.fixture -def create_empty_project(tmp_path: Path) -> t.Callable[[], t.Tuple[Path, Path]]: - def _create_empty_project() -> t.Tuple[Path, Path]: - yaml = YAML() - dbt_project_dir = tmp_path / "dbt" - dbt_project_dir.mkdir() - dbt_model_dir = dbt_project_dir / "models" - dbt_model_dir.mkdir() - dbt_project_config = { - "name": "empty_project", - "version": "1.0.0", - "config-version": 2, - "profile": "test", - "model-paths": ["models"], - } - dbt_project_file = dbt_project_dir / "dbt_project.yml" - with open(dbt_project_file, "w", encoding="utf-8") as f: - YAML().dump(dbt_project_config, f) - sqlmesh_config = { - "model_defaults": { - "start": "2025-01-01", - } - } - sqlmesh_config_file = dbt_project_dir / "sqlmesh.yaml" - with open(sqlmesh_config_file, "w", encoding="utf-8") as f: - YAML().dump(sqlmesh_config, f) - dbt_data_dir = tmp_path / "dbt_data" - dbt_data_dir.mkdir() - dbt_data_file = dbt_data_dir / "local.db" - dbt_profile_config = { - "test": { - "outputs": {"duckdb": {"type": "duckdb", "path": str(dbt_data_file)}}, - "target": "duckdb", - } - } - db_profile_file = dbt_project_dir / "profiles.yml" - with open(db_profile_file, "w", encoding="utf-8") as f: - yaml.dump(dbt_profile_config, f) - return dbt_project_dir, dbt_model_dir - - return _create_empty_project - - def test_test_config_is_standalone_behavior() -> None: """Test that TestConfig.is_standalone correctly identifies tests with cross-model references""" @@ -174,7 +131,7 @@ def test_manifest_filters_standalone_tests_from_models( ) -> None: """Integration test that verifies models only contain non-standalone tests after manifest loading.""" yaml = YAML() - project_dir, model_dir = create_empty_project() + project_dir, model_dir = create_empty_project(project_name="local") # Create two models model1_contents = "SELECT 1 as id" @@ -265,7 +222,7 @@ def test_load_invalid_ref_audit_constraints( tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project ) -> None: yaml = YAML() - project_dir, model_dir = create_empty_project() + project_dir, model_dir = create_empty_project(project_name="local") # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it full_model_contents = """{{ config(tags=["blah"], tests=[{"blah": {"to": "ref('completely_ignored')", "field": "blah2"} }]) }} SELECT 1 as cola""" full_model_file = model_dir / "full_model.sql" @@ -332,7 +289,7 @@ def test_load_invalid_ref_audit_constraints( def test_load_microbatch_all_defined( tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project ) -> None: - project_dir, model_dir = create_empty_project() + project_dir, model_dir = create_empty_project(project_name="local") # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it microbatch_contents = """ {{ @@ -373,7 +330,7 @@ def test_load_microbatch_all_defined( def test_load_microbatch_all_defined_diff_values( tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project ) -> None: - project_dir, model_dir = create_empty_project() + project_dir, model_dir = create_empty_project(project_name="local") # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it microbatch_contents = """ {{ @@ -415,7 +372,7 @@ def test_load_microbatch_all_defined_diff_values( def test_load_microbatch_required_only( tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project ) -> None: - project_dir, model_dir = create_empty_project() + project_dir, model_dir = create_empty_project(project_name="local") # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it microbatch_contents = """ {{ @@ -454,7 +411,7 @@ def test_load_microbatch_required_only( def test_load_incremental_time_range_strategy_required_only( tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project ) -> None: - project_dir, model_dir = create_empty_project() + project_dir, model_dir = create_empty_project(project_name="local", start="2025-01-01") # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it incremental_time_range_contents = """ {{ @@ -476,7 +433,7 @@ def test_load_incremental_time_range_strategy_required_only( snapshot = context.snapshots[snapshot_fqn] model = snapshot.model # Validate model-level attributes - assert model.start == "2025-01-01" + assert to_ds(model.start or "") == "2025-01-01" assert model.interval_unit.is_day # Validate model kind attributes assert isinstance(model.kind, IncrementalByTimeRangeKind) @@ -496,7 +453,7 @@ def test_load_incremental_time_range_strategy_required_only( def test_load_incremental_time_range_strategy_all_defined( tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project ) -> None: - project_dir, model_dir = create_empty_project() + project_dir, model_dir = create_empty_project(project_name="local", start="2025-01-01") # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it incremental_time_range_contents = """ {{ @@ -532,7 +489,7 @@ def test_load_incremental_time_range_strategy_all_defined( snapshot = context.snapshots[snapshot_fqn] model = snapshot.model # Validate model-level attributes - assert model.start == "2025-01-01" + assert to_ds(model.start or "") == "2025-01-01" assert model.interval_unit.is_day # Validate model kind attributes assert isinstance(model.kind, IncrementalByTimeRangeKind) @@ -559,7 +516,7 @@ def test_load_incremental_time_range_strategy_all_defined( def test_load_deprecated_incremental_time_column( tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project ) -> None: - project_dir, model_dir = create_empty_project() + project_dir, model_dir = create_empty_project(project_name="local", start="2025-01-01") # add `tests` to model config since this is loaded by dbt and ignored and we shouldn't error when loading it incremental_time_range_contents = """ {{ @@ -580,10 +537,10 @@ def test_load_deprecated_incremental_time_column( context = Context(paths=project_dir) model = context.snapshots[snapshot_fqn].model # Validate model-level attributes - assert model.start == "2025-01-01" + assert to_ds(model.start or "") == "2025-01-01" assert model.interval_unit.is_day # Validate model-level attributes - assert model.start == "2025-01-01" + assert to_ds(model.start or "") == "2025-01-01" assert model.interval_unit.is_day # Validate model kind attributes assert isinstance(model.kind, IncrementalByTimeRangeKind) @@ -606,7 +563,7 @@ def test_load_microbatch_with_ref( tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project ) -> None: yaml = YAML() - project_dir, model_dir = create_empty_project() + project_dir, model_dir = create_empty_project(project_name="local") source_schema = { "version": 2, "sources": [ @@ -672,7 +629,7 @@ def test_load_microbatch_with_ref_no_filter( tmp_path: Path, caplog, dbt_dummy_postgres_config: PostgresConfig, create_empty_project ) -> None: yaml = YAML() - project_dir, model_dir = create_empty_project() + project_dir, model_dir = create_empty_project(project_name="local") source_schema = { "version": 2, "sources": [ @@ -749,21 +706,6 @@ def test_load_multiple_snapshots_defined_in_same_file(sushi_test_dbt_context: Co def test_dbt_jinja_macro_undefined_variable_error(create_empty_project): project_dir, model_dir = create_empty_project() - dbt_profile_config = { - "test": { - "outputs": { - "duckdb": { - "type": "duckdb", - "path": str(project_dir.parent / "dbt_data" / "main.db"), - } - }, - "target": "duckdb", - } - } - db_profile_file = project_dir / "profiles.yml" - with open(db_profile_file, "w", encoding="utf-8") as f: - YAML().dump(dbt_profile_config, f) - macros_dir = project_dir / "macros" macros_dir.mkdir() @@ -801,6 +743,8 @@ def test_dbt_jinja_macro_undefined_variable_error(create_empty_project): @pytest.mark.slow def test_node_name_populated_for_dbt_models(dbt_dummy_postgres_config: PostgresConfig) -> None: model_config = ModelConfig( + unique_id="model.test_package.test_model", + fqn=["test_package", "test_model"], name="test_model", package_name="test_package", sql="SELECT 1 as id", @@ -815,7 +759,8 @@ def test_node_name_populated_for_dbt_models(dbt_dummy_postgres_config: PostgresC # check after convert to SQLMesh model that node_name is populated correctly sqlmesh_model = model_config.to_sqlmesh(context) - assert sqlmesh_model.dbt_name == "model.test_package.test_model" + assert sqlmesh_model.dbt_unique_id == "model.test_package.test_model" + assert sqlmesh_model.dbt_fqn == "test_package.test_model" @pytest.mark.slow @@ -872,12 +817,15 @@ def test_load_model_dbt_node_name(tmp_path: Path) -> None: # Verify that node_name is the equivalent dbt one model = context.snapshots[model_fqn].model - assert model.dbt_name == "model.test_project.simple_model" + assert model.dbt_unique_id == "model.test_project.simple_model" + assert model.dbt_fqn == "test_project.simple_model" + assert model.dbt_node_info + assert model.dbt_node_info.name == "simple_model" @pytest.mark.slow -def test_jinja_config_no_query(tmp_path, create_empty_project): - project_dir, model_dir = create_empty_project() +def test_jinja_config_no_query(create_empty_project): + project_dir, model_dir = create_empty_project(project_name="local") # model definition contains only a comment and non-SQL jinja model_contents = "/* comment */ {{ config(materialized='table') }}" diff --git a/tests/dbt/cli/fixtures/empty_project/dbt_project.yml b/tests/fixtures/dbt/empty_project/dbt_project.yml similarity index 94% rename from tests/dbt/cli/fixtures/empty_project/dbt_project.yml rename to tests/fixtures/dbt/empty_project/dbt_project.yml index beceadcd33..dab3d1e0e8 100644 --- a/tests/dbt/cli/fixtures/empty_project/dbt_project.yml +++ b/tests/fixtures/dbt/empty_project/dbt_project.yml @@ -1,7 +1,7 @@ name: 'empty_project' +version: '1.0.0' config-version: 2 -version: '0.1' profile: 'empty_project' diff --git a/tests/dbt/cli/fixtures/empty_project/profiles.yml b/tests/fixtures/dbt/empty_project/profiles.yml similarity index 85% rename from tests/dbt/cli/fixtures/empty_project/profiles.yml rename to tests/fixtures/dbt/empty_project/profiles.yml index a4f9836b7e..b352fc5792 100644 --- a/tests/dbt/cli/fixtures/empty_project/profiles.yml +++ b/tests/fixtures/dbt/empty_project/profiles.yml @@ -6,4 +6,4 @@ empty_project: duckdb: type: duckdb path: 'empty_project.duckdb' - threads: 4 + threads: 4 diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/dbt_project.yml b/tests/fixtures/dbt/jaffle_shop_duckdb/dbt_project.yml similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/dbt_project.yml rename to tests/fixtures/dbt/jaffle_shop_duckdb/dbt_project.yml diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/customers.sql b/tests/fixtures/dbt/jaffle_shop_duckdb/models/customers.sql similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/customers.sql rename to tests/fixtures/dbt/jaffle_shop_duckdb/models/customers.sql diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/docs.md b/tests/fixtures/dbt/jaffle_shop_duckdb/models/docs.md similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/docs.md rename to tests/fixtures/dbt/jaffle_shop_duckdb/models/docs.md diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/orders.sql b/tests/fixtures/dbt/jaffle_shop_duckdb/models/orders.sql similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/orders.sql rename to tests/fixtures/dbt/jaffle_shop_duckdb/models/orders.sql diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/overview.md b/tests/fixtures/dbt/jaffle_shop_duckdb/models/overview.md similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/overview.md rename to tests/fixtures/dbt/jaffle_shop_duckdb/models/overview.md diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/schema.yml b/tests/fixtures/dbt/jaffle_shop_duckdb/models/schema.yml similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/schema.yml rename to tests/fixtures/dbt/jaffle_shop_duckdb/models/schema.yml diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/schema.yml b/tests/fixtures/dbt/jaffle_shop_duckdb/models/staging/schema.yml similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/schema.yml rename to tests/fixtures/dbt/jaffle_shop_duckdb/models/staging/schema.yml diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_customers.sql b/tests/fixtures/dbt/jaffle_shop_duckdb/models/staging/stg_customers.sql similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_customers.sql rename to tests/fixtures/dbt/jaffle_shop_duckdb/models/staging/stg_customers.sql diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_orders.sql b/tests/fixtures/dbt/jaffle_shop_duckdb/models/staging/stg_orders.sql similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_orders.sql rename to tests/fixtures/dbt/jaffle_shop_duckdb/models/staging/stg_orders.sql diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_payments.sql b/tests/fixtures/dbt/jaffle_shop_duckdb/models/staging/stg_payments.sql similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/models/staging/stg_payments.sql rename to tests/fixtures/dbt/jaffle_shop_duckdb/models/staging/stg_payments.sql diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/profiles.yml b/tests/fixtures/dbt/jaffle_shop_duckdb/profiles.yml similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/profiles.yml rename to tests/fixtures/dbt/jaffle_shop_duckdb/profiles.yml diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/.gitkeep b/tests/fixtures/dbt/jaffle_shop_duckdb/seeds/.gitkeep similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/.gitkeep rename to tests/fixtures/dbt/jaffle_shop_duckdb/seeds/.gitkeep diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_customers.csv b/tests/fixtures/dbt/jaffle_shop_duckdb/seeds/raw_customers.csv similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_customers.csv rename to tests/fixtures/dbt/jaffle_shop_duckdb/seeds/raw_customers.csv diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_orders.csv b/tests/fixtures/dbt/jaffle_shop_duckdb/seeds/raw_orders.csv similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_orders.csv rename to tests/fixtures/dbt/jaffle_shop_duckdb/seeds/raw_orders.csv diff --git a/tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_payments.csv b/tests/fixtures/dbt/jaffle_shop_duckdb/seeds/raw_payments.csv similarity index 100% rename from tests/dbt/cli/fixtures/jaffle_shop_duckdb/seeds/raw_payments.csv rename to tests/fixtures/dbt/jaffle_shop_duckdb/seeds/raw_payments.csv From 147a5bbdbcda5227500f5c068438de81342e9e1e Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 23 Sep 2025 08:09:05 -0700 Subject: [PATCH 0899/1056] Fix: Support seed model schema changes in dev-only VDE mode (#5419) --- sqlmesh/core/plan/common.py | 6 +++- sqlmesh/core/plan/stages.py | 2 +- sqlmesh/core/snapshot/evaluator.py | 18 ++++++++-- tests/core/test_integration.py | 56 ++++++++++++++++++++++++++++++ tests/core/test_plan.py | 1 + 5 files changed, 78 insertions(+), 5 deletions(-) diff --git a/sqlmesh/core/plan/common.py b/sqlmesh/core/plan/common.py index 2ae34fbfba..bece17639c 100644 --- a/sqlmesh/core/plan/common.py +++ b/sqlmesh/core/plan/common.py @@ -16,7 +16,11 @@ def should_force_rebuild(old: Snapshot, new: Snapshot) -> bool: if new.is_view and new.is_indirect_non_breaking and not new.is_forward_only: # View models always need to be rebuilt to reflect updated upstream dependencies return True - if new.is_seed and not new.is_metadata: + if new.is_seed and not ( + new.is_metadata + and new.previous_version + and new.previous_version.snapshot_id(new.name) == old.snapshot_id + ): # Seed models always need to be rebuilt to reflect changes in the seed file # Unless only their metadata has been updated (eg description added) and the seed file has not been touched return True diff --git a/sqlmesh/core/plan/stages.py b/sqlmesh/core/plan/stages.py index 9425608619..729e1705b4 100644 --- a/sqlmesh/core/plan/stages.py +++ b/sqlmesh/core/plan/stages.py @@ -268,7 +268,7 @@ def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]: before_promote_snapshots = { s.snapshot_id for s in snapshots.values() - if deployability_index.is_representative(s) + if (deployability_index.is_representative(s) or s.is_seed) and plan.is_selected_for_backfill(s.name) } after_promote_snapshots = all_selected_for_backfill_snapshots - before_promote_snapshots diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index baf4dd67f1..70cc31b0a4 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -1141,10 +1141,10 @@ def _migrate_target_table( ) -> None: adapter = self.get_adapter(snapshot.model.gateway) - target_table = exp.to_table(target_table_name) - target_table.this.set("this", f"{target_table.name}_schema_tmp") + tmp_table = exp.to_table(target_table_name) + tmp_table.this.set("this", f"{tmp_table.name}_schema_tmp") + tmp_table_name = tmp_table.sql() - tmp_table_name = target_table.sql() if snapshot.is_materialized: self._execute_create( snapshot=snapshot, @@ -2185,6 +2185,18 @@ def create( self.adapter.drop_table(table_name) raise + def migrate( + self, + target_table_name: str, + source_table_name: str, + snapshot: Snapshot, + *, + ignore_destructive: bool, + ignore_additive: bool, + **kwargs: t.Any, + ) -> None: + raise NotImplementedError("Seeds do not support migrations.") + def insert( self, table_name: str, diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index a3f9584aa3..bac495a5f1 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -3232,6 +3232,62 @@ def test_virtual_environment_mode_dev_only_model_change_standalone_audit( context.apply(plan) +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_seed_model_change_schema( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.apply(plan) + + new_csv = [] + with open(context.path / "seeds" / "waiter_names.csv", "r") as fd: + is_header = True + for idx, line in enumerate(fd): + line = line.strip() + if not line: + continue + if is_header: + new_csv.append(line + ",new_column") + is_header = False + else: + new_csv.append(line + f",v{idx}") + + with open(context.path / "seeds" / "waiter_names.csv", "w") as fd: + fd.write("\n".join(new_csv)) + + context.load() + + downstream_model = context.get_model("sushi.waiter_as_customer_by_day") + downstream_model_kind = downstream_model.kind.dict() + downstream_model_kwargs = { + **downstream_model.dict(), + "kind": { + **downstream_model_kind, + "on_destructive_change": "allow", + }, + "audits": [], + # Use the new column + "query": "SELECT '2023-01-07' AS event_date, new_column AS new_column FROM sushi.waiter_names", + } + context.upsert_model(SqlModel.parse_obj(downstream_model_kwargs)) + + context.plan("dev", auto_apply=True, no_prompts=True, skip_tests=True, enable_preview=True) + + assert ( + context.engine_adapter.fetchone( + "SELECT COUNT(*) FROM sushi__dev.waiter_as_customer_by_day" + )[0] + == len(new_csv) - 1 + ) + + # Deploy to prod + context.clear_caches() + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + assert "new_column" in context.engine_adapter.columns("sushi.waiter_as_customer_by_day") + + @time_machine.travel("2023-01-08 15:00:00 UTC") def test_restatement_plan_ignores_changes(init_and_plan_context: t.Callable): context, plan = init_and_plan_context("examples/sushi") diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 59bc91d1bf..40967f1fbe 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -1214,6 +1214,7 @@ def test_seed_model_metadata_change_no_missing_intervals( description="foo", ) ) + snapshot_a_metadata_updated.previous_versions = snapshot_a.all_versions assert snapshot_a_metadata_updated.version is None assert snapshot_a_metadata_updated.change_category is None From 1fb5010147e27629103bf2873c12ae13fd6a5f78 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Tue, 23 Sep 2025 19:21:00 +0300 Subject: [PATCH 0900/1056] Chore!: bump sqlglot to v27.17.0 (#5429) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1d34b340b5..59880e61c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.16.3", + "sqlglot[rs]~=27.17.0", "tenacity", "time-machine", "json-stream" From 131a4db8398c3aab180b886e35844023f86b7641 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:17:15 -0700 Subject: [PATCH 0901/1056] fix: bigquery snowflake source columns support (#5428) --- sqlmesh/core/engine_adapter/bigquery.py | 9 +++++---- sqlmesh/core/engine_adapter/snowflake.py | 14 ++++++++------ tests/core/engine_adapter/test_bigquery.py | 8 +++++++- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 00b33f67a5..0dfa2325e8 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -169,17 +169,18 @@ def _df_to_source_queries( ) def query_factory() -> Query: - if bigframes_pd and isinstance(df, bigframes_pd.DataFrame): - df.to_gbq( + ordered_df = df[list(source_columns_to_types)] + if bigframes_pd and isinstance(ordered_df, bigframes_pd.DataFrame): + ordered_df.to_gbq( f"{temp_bq_table.project}.{temp_bq_table.dataset_id}.{temp_bq_table.table_id}", if_exists="replace", ) elif not self.table_exists(temp_table): # Make mypy happy - assert isinstance(df, pd.DataFrame) + assert isinstance(ordered_df, pd.DataFrame) self._db_call(self.client.create_table, table=temp_bq_table, exists_ok=False) result = self.__load_pandas_to_table( - temp_bq_table, df, source_columns_to_types, replace=False + temp_bq_table, ordered_df, source_columns_to_types, replace=False ) if result.errors: raise SQLMeshError(result.errors) diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index 355fb9719c..9c27b45115 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -378,6 +378,8 @@ def query_factory() -> Query: elif isinstance(df, pd.DataFrame): from snowflake.connector.pandas_tools import write_pandas + ordered_df = df[list(source_columns_to_types)] + # Workaround for https://github.com/snowflakedb/snowflake-connector-python/issues/1034 # The above issue has already been fixed upstream, but we keep the following # line anyway in order to support a wider range of Snowflake versions. @@ -388,16 +390,16 @@ def query_factory() -> Query: # See: https://stackoverflow.com/a/75627721 for column, kind in source_columns_to_types.items(): - if is_datetime64_any_dtype(df.dtypes[column]): + if is_datetime64_any_dtype(ordered_df.dtypes[column]): if kind.is_type("date"): # type: ignore - df[column] = pd.to_datetime(df[column]).dt.date # type: ignore - elif getattr(df.dtypes[column], "tz", None) is not None: # type: ignore - df[column] = pd.to_datetime(df[column]).dt.strftime( + ordered_df[column] = pd.to_datetime(ordered_df[column]).dt.date # type: ignore + elif getattr(ordered_df.dtypes[column], "tz", None) is not None: # type: ignore + ordered_df[column] = pd.to_datetime(ordered_df[column]).dt.strftime( "%Y-%m-%d %H:%M:%S.%f%z" ) # type: ignore # https://github.com/snowflakedb/snowflake-connector-python/issues/1677 else: # type: ignore - df[column] = pd.to_datetime(df[column]).dt.strftime( + ordered_df[column] = pd.to_datetime(ordered_df[column]).dt.strftime( "%Y-%m-%d %H:%M:%S.%f" ) # type: ignore @@ -407,7 +409,7 @@ def query_factory() -> Query: write_pandas( self._connection_pool.get(), - df, + ordered_df, temp_table.name, schema=temp_table.db or None, database=database.sql(dialect=self.dialect) if database else None, diff --git a/tests/core/engine_adapter/test_bigquery.py b/tests/core/engine_adapter/test_bigquery.py index 4328fa8923..f195bbaa2a 100644 --- a/tests/core/engine_adapter/test_bigquery.py +++ b/tests/core/engine_adapter/test_bigquery.py @@ -487,7 +487,13 @@ def temp_table_exists(table: exp.Table) -> bool: retry_resp_call.errors = None retry_mock.return_value = retry_resp db_call_mock.return_value = AttributeDict({"errors": None}) - df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + df = pd.DataFrame( + { + "id": [1, 2, 3], + "ts": ["2025-01-01 00:00:00", "2025-01-01 00:00:00", "2025-01-01 00:00:00"], + "val": [7, 8, 9], + } + ) adapter.merge( target_table="target", source_table=df, From 97c6a127ed36b448666d495b89f526a60f69f985 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 24 Sep 2025 10:40:13 +1200 Subject: [PATCH 0902/1056] Fix: Sort nested AttributeDict's to prevent visual diff (#5397) --- sqlmesh/utils/jinja.py | 16 +++++++++++++++- tests/utils/test_jinja.py | 27 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index c9339cf404..508c6dce2d 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -206,6 +206,20 @@ def extract_macro_references_and_variables( return macro_references, variables +def sort_dict_recursive( + item: t.Dict[str, t.Any], +) -> t.Dict[str, t.Any]: + sorted_dict: t.Dict[str, t.Any] = {} + for k, v in sorted(item.items()): + if isinstance(v, list): + sorted_dict[k] = sorted(v) + elif isinstance(v, dict): + sorted_dict[k] = sort_dict_recursive(v) + else: + sorted_dict[k] = v + return sorted_dict + + JinjaGlobalAttribute = t.Union[str, int, float, bool, AttributeDict] @@ -440,7 +454,7 @@ def to_expressions(self) -> t.List[Expression]: d.PythonCode( expressions=[ f"{k} = '{v}'" if isinstance(v, str) else f"{k} = {v}" - for k, v in sorted(filtered_objs.items()) + for k, v in sort_dict_recursive(filtered_objs).items() ] ) ) diff --git a/tests/utils/test_jinja.py b/tests/utils/test_jinja.py index 5eb00aeb3c..1cf7c1bf95 100644 --- a/tests/utils/test_jinja.py +++ b/tests/utils/test_jinja.py @@ -302,3 +302,30 @@ def test_dbt_adapter_macro_scope(): rendered = registry.build_environment().from_string("{{ spark__macro_a() }}").render() assert rendered.strip() == "macro_a" + + +def test_macro_registry_to_expressions_sorted(): + refs = AttributeDict( + { + "payments": { + "database": "jaffle_shop", + "schema": "main", + "nested": {"foo": "bar", "baz": "bing"}, + }, + "orders": {"schema": "main", "database": "jaffle_shop", "nested_list": ["b", "a", "c"]}, + } + ) + + registry = JinjaMacroRegistry() + registry.add_globals({"sources": {}, "refs": refs}) + + # Ensure that the AttributeDict string representation is sorted + # in order to prevent an unexpected *visual* diff in ModelDiff + # (note that the actual diff is based on the data hashes, so this is purely visual) + expressions = registry.to_expressions() + assert len(expressions) == 1 + assert ( + expressions[0].sql(dialect="duckdb") + == "refs = {'orders': {'database': 'jaffle_shop', 'nested_list': ['a', 'b', 'c'], 'schema': 'main'}, 'payments': {'database': 'jaffle_shop', 'nested': {'baz': 'bing', 'foo': 'bar'}, 'schema': 'main'}}\n" + "sources = {}" + ) From e7e4841eccf7feece5fb0d9ed7f8580c00e897a1 Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 23 Sep 2025 18:19:15 -0700 Subject: [PATCH 0903/1056] Chore: Break up the core integration tests (#5432) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Makefile | 6 +- pyproject.toml | 1 + tests/conftest.py | 10 +- tests/core/integration/__init__.py | 0 tests/core/integration/conftest.py | 8 + tests/core/integration/test_audits.py | 348 + .../core/integration/test_auto_restatement.py | 219 + tests/core/integration/test_aux_commands.py | 367 + .../core/integration/test_change_scenarios.py | 1484 +++ tests/core/integration/test_config.py | 580 + tests/core/integration/test_cron.py | 247 + tests/core/integration/test_dbt.py | 125 + tests/core/integration/test_dev_only_vde.py | 477 + tests/core/integration/test_forward_only.py | 1510 +++ tests/core/integration/test_model_kinds.py | 2644 ++++ tests/core/integration/test_multi_repo.py | 456 + tests/core/integration/test_plan_options.py | 478 + tests/core/integration/test_restatement.py | 1882 +++ tests/core/integration/test_run.py | 247 + tests/core/integration/utils.py | 350 + tests/core/test_dialect.py | 2 + tests/core/test_integration.py | 10887 ---------------- 22 files changed, 11437 insertions(+), 10891 deletions(-) create mode 100644 tests/core/integration/__init__.py create mode 100644 tests/core/integration/conftest.py create mode 100644 tests/core/integration/test_audits.py create mode 100644 tests/core/integration/test_auto_restatement.py create mode 100644 tests/core/integration/test_aux_commands.py create mode 100644 tests/core/integration/test_change_scenarios.py create mode 100644 tests/core/integration/test_config.py create mode 100644 tests/core/integration/test_cron.py create mode 100644 tests/core/integration/test_dbt.py create mode 100644 tests/core/integration/test_dev_only_vde.py create mode 100644 tests/core/integration/test_forward_only.py create mode 100644 tests/core/integration/test_model_kinds.py create mode 100644 tests/core/integration/test_multi_repo.py create mode 100644 tests/core/integration/test_plan_options.py create mode 100644 tests/core/integration/test_restatement.py create mode 100644 tests/core/integration/test_run.py create mode 100644 tests/core/integration/utils.py delete mode 100644 tests/core/test_integration.py diff --git a/Makefile b/Makefile index 40874f7972..fbf77b8f9b 100644 --- a/Makefile +++ b/Makefile @@ -117,13 +117,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 -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" slow-test: - pytest -n auto -m "(fast or slow) and not cicdonly" && pytest -m "isolated" && pytest -m "registry_isolation" + pytest -n auto -m "(fast or slow) and not cicdonly" && pytest -m "isolated" && pytest -m "registry_isolation" && pytest -m "dialect_isolated" cicd-test: - pytest -n auto -m "fast or slow" --junitxml=test-results/junit-cicd.xml && pytest -m "isolated" && pytest -m "registry_isolation" + pytest -n auto -m "fast or slow" --junitxml=test-results/junit-cicd.xml && pytest -m "isolated" && 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/pyproject.toml b/pyproject.toml index 59880e61c5..b3e13b63ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -241,6 +241,7 @@ markers = [ "remote: test that involves interacting with a remote DB", "cicdonly: test that only runs on CI/CD", "isolated: tests that need to run sequentially usually because they use fork", + "dialect_isolated: tests that need to run separately due to global dialect overrides", # Test Domain Markers # default: core functionality diff --git a/tests/conftest.py b/tests/conftest.py index e5bbc4f425..7a61281ad0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -202,7 +202,15 @@ def validate( def pytest_collection_modifyitems(items, *args, **kwargs): - test_type_markers = {"fast", "slow", "docker", "remote", "isolated", "registry_isolation"} + test_type_markers = { + "fast", + "slow", + "docker", + "remote", + "isolated", + "registry_isolation", + "dialect_isolated", + } for item in items: for marker in item.iter_markers(): if marker.name in test_type_markers: diff --git a/tests/core/integration/__init__.py b/tests/core/integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/integration/conftest.py b/tests/core/integration/conftest.py new file mode 100644 index 0000000000..99875e5974 --- /dev/null +++ b/tests/core/integration/conftest.py @@ -0,0 +1,8 @@ +import pytest +from pytest_mock.plugin import MockerFixture + + +@pytest.fixture(autouse=True) +def mock_choices(mocker: MockerFixture): + mocker.patch("sqlmesh.core.console.TerminalConsole._get_snapshot_change_category") + mocker.patch("sqlmesh.core.console.TerminalConsole._prompt_backfill") diff --git a/tests/core/integration/test_audits.py b/tests/core/integration/test_audits.py new file mode 100644 index 0000000000..457974fdac --- /dev/null +++ b/tests/core/integration/test_audits.py @@ -0,0 +1,348 @@ +from __future__ import annotations + +import typing as t +from textwrap import dedent +import pytest +from pathlib import Path +import time_machine +from sqlglot import exp +from IPython.utils.capture import capture_output + +from sqlmesh.core.config import ( + Config, + ModelDefaultsConfig, +) +from sqlmesh.core.context import Context +from sqlmesh.utils.errors import ( + PlanError, +) +from tests.utils.test_helpers import use_terminal_console +from tests.utils.test_filesystem import create_temp_file + +pytestmark = pytest.mark.slow + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +@use_terminal_console +def test_audit_only_metadata_change(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Add a new audit + model = context.get_model("sushi.waiter_revenue_by_day") + audits = model.audits.copy() + audits.append(("number_of_rows", {"threshold": exp.Literal.number(1)})) + model = model.copy(update={"audits": audits}) + context.upsert_model(model) + + plan = context.plan_builder("prod", skip_tests=True).build() + assert len(plan.new_snapshots) == 2 + assert all(s.change_category.is_metadata for s in plan.new_snapshots) + assert not plan.missing_intervals + + with capture_output() as output: + context.apply(plan) + + assert "Auditing models" in output.stdout + assert model.name in output.stdout + + +@use_terminal_console +def test_audits_running_on_metadata_changes(tmp_path: Path): + def setup_senario(model_before: str, model_after: str): + models_dir = Path("models") + create_temp_file(tmp_path, models_dir / "test.sql", model_before) + + # Create first snapshot + context = Context(paths=tmp_path, config=Config()) + context.plan("prod", no_prompts=True, auto_apply=True) + + # Create second (metadata) snapshot + create_temp_file(tmp_path, models_dir / "test.sql", model_after) + context.load() + + with capture_output() as output: + with pytest.raises(PlanError): + context.plan("prod", no_prompts=True, auto_apply=True) + + assert 'Failed models\n\n "model"' in output.stdout + + return output + + # Ensure incorrect audits (bad data, incorrect definition etc) are evaluated immediately + output = setup_senario( + "MODEL (name model); SELECT NULL AS col", + "MODEL (name model, audits (not_null(columns=[col]))); SELECT NULL AS col", + ) + assert "'not_null' audit error: 1 row failed" in output.stdout + + output = setup_senario( + "MODEL (name model); SELECT NULL AS col", + "MODEL (name model, audits (not_null(columns=[this_col_does_not_exist]))); SELECT NULL AS col", + ) + assert ( + 'Binder Error: Referenced column "this_col_does_not_exist" not found in \nFROM clause!' + in output.stdout + ) + + +@pytest.mark.slow +def test_default_audits_applied_in_plan(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir(exist_ok=True) + + # Create a model with data that will pass the audits + create_temp_file( + tmp_path, + models_dir / "orders.sql", + dedent(""" + MODEL ( + name test.orders, + kind FULL + ); + + SELECT + 1 AS order_id, + 'customer_1' AS customer_id, + 100.50 AS amount, + '2024-01-01'::DATE AS order_date + UNION ALL + SELECT + 2 AS order_id, + 'customer_2' AS customer_id, + 200.75 AS amount, + '2024-01-02'::DATE AS order_date + """), + ) + + config = Config( + model_defaults=ModelDefaultsConfig( + dialect="duckdb", + audits=[ + "not_null(columns := [order_id, customer_id])", + "unique_values(columns := [order_id])", + ], + ) + ) + + context = Context(paths=tmp_path, config=config) + + # Create and apply plan, here audits should pass + plan = context.plan("prod", no_prompts=True) + context.apply(plan) + + # Verify model has the default audits + model = context.get_model("test.orders") + assert len(model.audits) == 2 + + audit_names = [audit[0] for audit in model.audits] + assert "not_null" in audit_names + assert "unique_values" in audit_names + + # Verify audit arguments are preserved + for audit_name, audit_args in model.audits: + if audit_name == "not_null": + assert "columns" in audit_args + columns = [col.name for col in audit_args["columns"].expressions] + assert "order_id" in columns + assert "customer_id" in columns + elif audit_name == "unique_values": + assert "columns" in audit_args + columns = [col.name for col in audit_args["columns"].expressions] + assert "order_id" in columns + + +@pytest.mark.slow +def test_default_audits_fail_on_bad_data(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir(exist_ok=True) + + # Create a model with data that violates NOT NULL constraint + create_temp_file( + tmp_path, + models_dir / "bad_orders.sql", + dedent(""" + MODEL ( + name test.bad_orders, + kind FULL + ); + + SELECT + 1 AS order_id, + NULL AS customer_id, -- This violates NOT NULL + 100.50 AS amount, + '2024-01-01'::DATE AS order_date + UNION ALL + SELECT + 2 AS order_id, + 'customer_2' AS customer_id, + 200.75 AS amount, + '2024-01-02'::DATE AS order_date + """), + ) + + config = Config( + model_defaults=ModelDefaultsConfig( + dialect="duckdb", audits=["not_null(columns := [customer_id])"] + ) + ) + + context = Context(paths=tmp_path, config=config) + + # Plan should fail due to audit failure + with pytest.raises(PlanError): + context.plan("prod", no_prompts=True, auto_apply=True) + + +@pytest.mark.slow +def test_default_audits_with_model_specific_audits(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir(exist_ok=True) + audits_dir = tmp_path / "audits" + audits_dir.mkdir(exist_ok=True) + + create_temp_file( + tmp_path, + audits_dir / "range_check.sql", + dedent(""" + AUDIT ( + name range_check + ); + + SELECT * FROM @this_model + WHERE @column < @min_value OR @column > @max_value + """), + ) + + # Create a model with its own audits in addition to defaults + create_temp_file( + tmp_path, + models_dir / "products.sql", + dedent(""" + MODEL ( + name test.products, + kind FULL, + audits ( + range_check(column := price, min_value := 0, max_value := 10000) + ) + ); + + SELECT + 1 AS product_id, + 'Widget' AS product_name, + 99.99 AS price + UNION ALL + SELECT + 2 AS product_id, + 'Gadget' AS product_name, + 149.99 AS price + """), + ) + + config = Config( + model_defaults=ModelDefaultsConfig( + dialect="duckdb", + audits=[ + "not_null(columns := [product_id, product_name])", + "unique_values(columns := [product_id])", + ], + ) + ) + + context = Context(paths=tmp_path, config=config) + + # Create and apply plan + plan = context.plan("prod", no_prompts=True) + context.apply(plan) + + # Verify model has both default and model-specific audits + model = context.get_model("test.products") + assert len(model.audits) == 3 + + audit_names = [audit[0] for audit in model.audits] + assert "not_null" in audit_names + assert "unique_values" in audit_names + assert "range_check" in audit_names + + # Verify audit execution order, default audits first then model-specific + assert model.audits[0][0] == "not_null" + assert model.audits[1][0] == "unique_values" + assert model.audits[2][0] == "range_check" + + +@pytest.mark.slow +def test_default_audits_with_custom_audit_definitions(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir(exist_ok=True) + audits_dir = tmp_path / "audits" + audits_dir.mkdir(exist_ok=True) + + # Create custom audit definition + create_temp_file( + tmp_path, + audits_dir / "positive_amount.sql", + dedent(""" + AUDIT ( + name positive_amount + ); + + SELECT * FROM @this_model + WHERE @column <= 0 + """), + ) + + # Create a model + create_temp_file( + tmp_path, + models_dir / "transactions.sql", + dedent(""" + MODEL ( + name test.transactions, + kind FULL + ); + + SELECT + 1 AS transaction_id, + 'TXN001' AS transaction_code, + 250.00 AS amount, + '2024-01-01'::DATE AS transaction_date + UNION ALL + SELECT + 2 AS transaction_id, + 'TXN002' AS transaction_code, + 150.00 AS amount, + '2024-01-02'::DATE AS transaction_date + """), + ) + + config = Config( + model_defaults=ModelDefaultsConfig( + dialect="duckdb", + audits=[ + "not_null(columns := [transaction_id, transaction_code])", + "unique_values(columns := [transaction_id])", + "positive_amount(column := amount)", + ], + ) + ) + + context = Context(paths=tmp_path, config=config) + + # Create and apply plan + plan = context.plan("prod", no_prompts=True) + context.apply(plan) + + # Verify model has all default audits including custom + model = context.get_model("test.transactions") + assert len(model.audits) == 3 + + audit_names = [audit[0] for audit in model.audits] + assert "not_null" in audit_names + assert "unique_values" in audit_names + assert "positive_amount" in audit_names + + # Verify custom audit arguments + for audit_name, audit_args in model.audits: + if audit_name == "positive_amount": + assert "column" in audit_args + assert audit_args["column"].name == "amount" diff --git a/tests/core/integration/test_auto_restatement.py b/tests/core/integration/test_auto_restatement.py new file mode 100644 index 0000000000..70ca227fd3 --- /dev/null +++ b/tests/core/integration/test_auto_restatement.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +import typing as t +import pandas as pd # noqa: TID253 +import pytest +import time_machine +from sqlglot import exp + +from sqlmesh.core import dialect as d +from sqlmesh.core.macros import macro +from sqlmesh.core.model import ( + load_sql_based_model, +) +from sqlmesh.core.plan import SnapshotIntervals +from sqlmesh.utils.date import to_timestamp + +pytestmark = pytest.mark.slow + + +@time_machine.travel("2023-01-08 01:00:00 UTC") +def test_run_auto_restatement(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + context.engine_adapter.execute( + "CREATE TABLE _test_auto_restatement_intervals (name STRING, start_ds STRING, end_ds STRING)" + ) + + @macro() + def record_intervals( + evaluator, name: exp.Expression, start: exp.Expression, end: exp.Expression, **kwargs: t.Any + ) -> None: + if evaluator.runtime_stage == "evaluating": + evaluator.engine_adapter.insert_append( + "_test_auto_restatement_intervals", + pd.DataFrame({"name": [name.name], "start_ds": [start.name], "end_ds": [end.name]}), + ) + + new_model_expr = d.parse( + """ + MODEL ( + name memory.sushi.new_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + auto_restatement_cron '0 6 * * 7', -- At 6am every Sunday + auto_restatement_intervals 3, + ), + start '2023-01-01', + ); + + @record_intervals('new_model', @start_ds, @end_ds); + + SELECT '2023-01-07' AS ds, 1 AS a; + """ + ) + new_model = load_sql_based_model(new_model_expr) + context.upsert_model(new_model) + + new_model_downstream_expr = d.parse( + """ + MODEL ( + name memory.sushi.new_model_downstream, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + ), + cron '@hourly', + ); + + @record_intervals('new_model_downstream', @start_ts, @end_ts); + + SELECT * FROM memory.sushi.new_model; + """ + ) + new_model_downstream = load_sql_based_model(new_model_downstream_expr) + context.upsert_model(new_model_downstream) + + plan = context.plan_builder("prod").build() + context.apply(plan) + + with time_machine.travel("2023-01-08 06:01:00 UTC"): + assert context.run() + + recorded_intervals_df = context.engine_adapter.fetchdf( + "SELECT start_ds, end_ds FROM _test_auto_restatement_intervals WHERE name = 'new_model'" + ) + # The first interval is the first backfill and the second interval should be the 3 auto restated intervals + assert recorded_intervals_df.to_dict() == { + "start_ds": {0: "2023-01-01", 1: "2023-01-05"}, + "end_ds": {0: "2023-01-07", 1: "2023-01-07"}, + } + recorded_intervals_downstream_df = context.engine_adapter.fetchdf( + "SELECT start_ds, end_ds FROM _test_auto_restatement_intervals WHERE name = 'new_model_downstream'" + ) + # The first interval is the first backfill, the second interval should be the 3 days of restated intervals, and + # the third interval should catch up to the current hour + assert recorded_intervals_downstream_df.to_dict() == { + "start_ds": { + 0: "2023-01-01 00:00:00", + 1: "2023-01-05 00:00:00", + 2: "2023-01-08 01:00:00", + }, + "end_ds": { + 0: "2023-01-08 00:59:59.999999", + 1: "2023-01-07 23:59:59.999999", + 2: "2023-01-08 05:59:59.999999", + }, + } + + snapshot = context.get_snapshot(new_model.name) + snapshot = context.state_sync.state_sync.get_snapshots([snapshot.snapshot_id])[ + snapshot.snapshot_id + ] + assert snapshot.next_auto_restatement_ts == to_timestamp("2023-01-15 06:00:00") + assert not snapshot.pending_restatement_intervals + + snapshot_downstream = context.get_snapshot(new_model_downstream.name) + snapshot_downstream = context.state_sync.state_sync.get_snapshots( + [snapshot_downstream.snapshot_id] + )[snapshot_downstream.snapshot_id] + assert not snapshot_downstream.next_auto_restatement_ts + assert not snapshot_downstream.pending_restatement_intervals + + +@time_machine.travel("2023-01-08 01:00:00 UTC") +def test_run_auto_restatement_plan_preview(init_and_plan_context: t.Callable): + context, init_plan = init_and_plan_context("examples/sushi") + context.apply(init_plan) + + new_model_expr = d.parse( + """ + MODEL ( + name memory.sushi.new_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + auto_restatement_cron '0 6 * * 7', + ), + start '2023-01-01', + ); + + SELECT '2023-01-07' AS ds, 1 AS a; + """ + ) + new_model = load_sql_based_model(new_model_expr) + context.upsert_model(new_model) + snapshot = context.get_snapshot(new_model.name) + + plan_dev = context.plan_builder("dev").build() + # Make sure that a limited preview is computed by default + assert to_timestamp(plan_dev.start) == to_timestamp("2023-01-07") + assert plan_dev.missing_intervals == [ + SnapshotIntervals( + snapshot.snapshot_id, + [(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], + ) + ] + assert not plan_dev.deployability_index.is_deployable(snapshot.snapshot_id) + context.apply(plan_dev) + + plan_prod = context.plan_builder("prod").build() + assert plan_prod.missing_intervals == [ + SnapshotIntervals( + context.get_snapshot(new_model.name).snapshot_id, + [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ) + ] + context.apply(plan_prod) + + +@time_machine.travel("2023-01-08 01:00:00 UTC") +def test_run_auto_restatement_failure(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + @macro() + def fail_auto_restatement(evaluator, start: exp.Expression, **kwargs: t.Any) -> None: + if evaluator.runtime_stage == "evaluating" and start.name != "2023-01-01": + raise Exception("Failed") + + new_model_expr = d.parse( + """ + MODEL ( + name memory.sushi.new_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + auto_restatement_cron '0 6 * * 7', -- At 6am every Sunday + auto_restatement_intervals 3, + ), + start '2023-01-01', + ); + + @fail_auto_restatement(@start_ds); + + SELECT '2023-01-07' AS ds, 1 AS a; + """ + ) + new_model = load_sql_based_model(new_model_expr) + context.upsert_model(new_model) + + plan = context.plan_builder("prod").build() + context.apply(plan) + + with time_machine.travel("2023-01-08 06:01:00 UTC"): + run_status = context.run() + assert run_status.is_failure + + snapshot = context.get_snapshot(new_model.name) + snapshot = context.state_sync.state_sync.get_snapshots([snapshot.snapshot_id])[ + snapshot.snapshot_id + ] + assert snapshot.next_auto_restatement_ts == to_timestamp("2023-01-15 06:00:00") + assert snapshot.pending_restatement_intervals == [ + (to_timestamp("2023-01-05"), to_timestamp("2023-01-08")) + ] diff --git a/tests/core/integration/test_aux_commands.py b/tests/core/integration/test_aux_commands.py new file mode 100644 index 0000000000..ecdd3e05fc --- /dev/null +++ b/tests/core/integration/test_aux_commands.py @@ -0,0 +1,367 @@ +from __future__ import annotations + +import typing as t +from unittest.mock import patch +import pytest +from pathlib import Path +from sqlmesh.core.config.naming import NameInferenceConfig +from sqlmesh.core.model.common import ParsableSql +import time_machine +from pytest_mock.plugin import MockerFixture + +from sqlmesh.core.config import ( + Config, + GatewayConfig, + ModelDefaultsConfig, + DuckDBConnectionConfig, +) +from sqlmesh.core.context import Context +from sqlmesh.core.model import ( + SqlModel, +) +from sqlmesh.utils.errors import ( + SQLMeshError, +) +from sqlmesh.utils.date import now +from tests.conftest import DuckDBMetadata +from tests.utils.test_helpers import use_terminal_console +from tests.utils.test_filesystem import create_temp_file +from tests.core.integration.utils import add_projection_to_model, apply_to_environment + +pytestmark = pytest.mark.slow + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_table_name(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") + assert snapshot + assert ( + context.table_name("sushi.waiter_revenue_by_day", "prod") + == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{snapshot.version}" + ) + + with pytest.raises(SQLMeshError, match="Environment 'dev' was not found."): + context.table_name("sushi.waiter_revenue_by_day", "dev") + + with pytest.raises( + SQLMeshError, match="Model 'sushi.missing' was not found in environment 'prod'." + ): + context.table_name("sushi.missing", "prod") + + # Add a new projection + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + context.plan("dev_a", auto_apply=True, no_prompts=True, skip_tests=True) + + new_snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") + assert new_snapshot.version != snapshot.version + + assert ( + context.table_name("sushi.waiter_revenue_by_day", "dev_a") + == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{new_snapshot.version}" + ) + + # Make a forward-only change + context.upsert_model(model, stamp="forward_only") + + context.plan("dev_b", auto_apply=True, no_prompts=True, skip_tests=True, forward_only=True) + + forward_only_snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") + assert forward_only_snapshot.version == snapshot.version + assert forward_only_snapshot.dev_version != snapshot.version + + assert ( + context.table_name("sushi.waiter_revenue_by_day", "dev_b") + == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{forward_only_snapshot.dev_version}__dev" + ) + + assert ( + context.table_name("sushi.waiter_revenue_by_day", "dev_b", prod=True) + == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{snapshot.version}" + ) + + +def test_janitor_cleanup_order(mocker: MockerFixture, tmp_path: Path): + def setup_scenario(): + models_dir = tmp_path / "models" + + if not models_dir.exists(): + models_dir.mkdir() + + model1_path = models_dir / "model1.sql" + + with open(model1_path, "w") as f: + f.write("MODEL(name test.model1, kind FULL); SELECT 1 AS col") + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + ) + ctx = Context(paths=[tmp_path], config=config) + + ctx.plan("dev", no_prompts=True, auto_apply=True) + + model1_snapshot = ctx.get_snapshot("test.model1") + + # Delete the model file to cause a snapshot expiration + model1_path.unlink() + + ctx.load() + + ctx.plan("dev", no_prompts=True, auto_apply=True) + + # Invalidate the environment to cause an environment cleanup + ctx.invalidate_environment("dev") + + try: + ctx._run_janitor(ignore_ttl=True) + except: + pass + + return ctx, model1_snapshot + + # Case 1: Assume that the snapshot cleanup yields an error, the snapshot records + # should still exist in the state sync so the next janitor can retry + mocker.patch( + "sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.cleanup", + side_effect=Exception("snapshot cleanup error"), + ) + ctx, model1_snapshot = setup_scenario() + + # - Check that the snapshot record exists in the state sync + state_snapshot = ctx.state_sync.state_sync.get_snapshots([model1_snapshot.snapshot_id]) + assert state_snapshot + + # - Run the janitor again, this time it should succeed + mocker.patch("sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.cleanup") + ctx._run_janitor(ignore_ttl=True) + + # - Check that the snapshot record does not exist in the state sync anymore + state_snapshot = ctx.state_sync.state_sync.get_snapshots([model1_snapshot.snapshot_id]) + assert not state_snapshot + + # Case 2: Assume that the view cleanup yields an error, the enviroment + # record should still exist + mocker.patch( + "sqlmesh.core.context.cleanup_expired_views", side_effect=Exception("view cleanup error") + ) + ctx, model1_snapshot = setup_scenario() + + views = ctx.fetchdf("FROM duckdb_views() SELECT * EXCLUDE(sql) WHERE NOT internal") + assert views.empty + + # - Check that the environment record exists in the state sync + assert ctx.state_sync.get_environment("dev") + + # - Run the janitor again, this time it should succeed + mocker.patch("sqlmesh.core.context.cleanup_expired_views") + ctx._run_janitor(ignore_ttl=True) + + # - Check that the environment record does not exist in the state sync anymore + assert not ctx.state_sync.get_environment("dev") + + +@use_terminal_console +def test_destroy(copy_to_temp_path): + # Testing project with two gateways to verify cleanup is performed across engines + paths = copy_to_temp_path("tests/fixtures/multi_virtual_layer") + path = Path(paths[0]) + first_db_path = str(path / "db_1.db") + second_db_path = str(path / "db_2.db") + + config = Config( + gateways={ + "first": GatewayConfig( + connection=DuckDBConnectionConfig(database=first_db_path), + variables={"overriden_var": "gateway_1"}, + ), + "second": GatewayConfig( + connection=DuckDBConnectionConfig(database=second_db_path), + variables={"overriden_var": "gateway_2"}, + ), + }, + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + model_naming=NameInferenceConfig(infer_names=True), + default_gateway="first", + gateway_managed_virtual_layer=True, + variables={"overriden_var": "global", "global_one": 88}, + ) + + context = Context(paths=paths, config=config) + plan = context.plan_builder().build() + assert len(plan.new_snapshots) == 4 + context.apply(plan) + + # Confirm cache exists + cache_path = Path(path) / ".cache" + assert cache_path.exists() + assert len(list(cache_path.iterdir())) > 0 + + model = context.get_model("db_1.first_schema.model_one") + + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'c' AS extra").sql(dialect=model.dialect) + ) + } + ) + ) + plan = context.plan_builder().build() + context.apply(plan) + + state_environments = context.state_reader.get_environments() + state_snapshots = context.state_reader.get_snapshots(context.snapshots.values()) + + assert len(state_snapshots) == len(state_environments[0].snapshots) + + # Create dev environment with changed models + model = context.get_model("db_2.second_schema.model_one") + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'d' AS extra").sql(dialect=model.dialect) + ) + } + ) + ) + model = context.get_model("first_schema.model_two") + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'d2' AS col").sql(dialect=model.dialect) + ) + } + ) + ) + plan = context.plan_builder("dev").build() + context.apply(plan) + + dev_environment = context.state_sync.get_environment("dev") + assert dev_environment is not None + + state_environments = context.state_reader.get_environments() + state_snapshots = context.state_reader.get_snapshots(context.snapshots.values()) + assert ( + len(state_snapshots) + == len(state_environments[0].snapshots) + == len(state_environments[1].snapshots) + ) + + # The state tables at this point should be able to be retrieved + state_tables = { + "_environments", + "_snapshots", + "_intervals", + "_auto_restatements", + "_environment_statements", + "_intervals", + "_versions", + } + for table_name in state_tables: + context.fetchdf(f"SELECT * FROM db_1.sqlmesh.{table_name}") + + # The actual tables as well + context.engine_adapters["second"].fetchdf(f"SELECT * FROM db_2.second_schema.model_one") + context.engine_adapters["second"].fetchdf(f"SELECT * FROM db_2.second_schema.model_two") + context.fetchdf(f"SELECT * FROM db_1.first_schema.model_one") + context.fetchdf(f"SELECT * FROM db_1.first_schema.model_two") + + # Use the destroy command to remove all data objects and state + # Mock the console confirmation to automatically return True + with patch.object(context.console, "_confirm", return_value=True): + context._destroy() + + # Ensure all tables have been removed + for table_name in state_tables: + with pytest.raises( + Exception, match=f"Catalog Error: Table with name {table_name} does not exist!" + ): + context.fetchdf(f"SELECT * FROM db_1.sqlmesh.{table_name}") + + # Validate tables have been deleted as well + with pytest.raises( + Exception, match=r"Catalog Error: Table with name model_two does not exist!" + ): + context.fetchdf("SELECT * FROM db_1.first_schema.model_two") + with pytest.raises( + Exception, match=r"Catalog Error: Table with name model_one does not exist!" + ): + context.fetchdf("SELECT * FROM db_1.first_schema.model_one") + + with pytest.raises( + Exception, match=r"Catalog Error: Table with name model_two does not exist!" + ): + context.engine_adapters["second"].fetchdf("SELECT * FROM db_2.second_schema.model_two") + with pytest.raises( + Exception, match=r"Catalog Error: Table with name model_one does not exist!" + ): + context.engine_adapters["second"].fetchdf("SELECT * FROM db_2.second_schema.model_one") + + # Ensure the cache has been removed + assert not cache_path.exists() + + +@use_terminal_console +def test_render_path_instead_of_model(tmp_path: Path): + create_temp_file(tmp_path, Path("models/test.sql"), "MODEL (name test_model); SELECT 1 AS col") + ctx = Context(paths=tmp_path, config=Config()) + + # Case 1: Fail gracefully when the user is passing in a path instead of a model name + for test_model in ["models/test.sql", "models/test.py"]: + with pytest.raises( + SQLMeshError, + match="Resolving models by path is not supported, please pass in the model name instead.", + ): + ctx.render(test_model) + + # Case 2: Fail gracefully when the model name is not found + with pytest.raises(SQLMeshError, match="Cannot find model with name 'incorrect_model'"): + ctx.render("incorrect_model") + + # Case 3: Render the model successfully + assert ctx.render("test_model").sql() == 'SELECT 1 AS "col"' + + +def test_invalidating_environment(sushi_context: Context): + apply_to_environment(sushi_context, "dev") + start_environment = sushi_context.state_sync.get_environment("dev") + assert start_environment is not None + metadata = DuckDBMetadata.from_context(sushi_context) + start_schemas = set(metadata.schemas) + assert "sushi__dev" in start_schemas + sushi_context.invalidate_environment("dev") + invalidate_environment = sushi_context.state_sync.get_environment("dev") + assert invalidate_environment is not None + schemas_prior_to_janitor = set(metadata.schemas) + assert invalidate_environment.expiration_ts < start_environment.expiration_ts # type: ignore + assert start_schemas == schemas_prior_to_janitor + sushi_context._run_janitor() + schemas_after_janitor = set(metadata.schemas) + assert sushi_context.state_sync.get_environment("dev") is None + assert start_schemas - schemas_after_janitor == {"sushi__dev"} + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_evaluate_uncategorized_snapshot(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Add a new projection + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + # Downstream model references the new projection + downstream_model = context.get_model("sushi.top_waiters") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, downstream_model), literal=False)) + + df = context.evaluate( + "sushi.top_waiters", start="2023-01-05", end="2023-01-06", execution_time=now() + ) + assert set(df["one"].tolist()) == {1} diff --git a/tests/core/integration/test_change_scenarios.py b/tests/core/integration/test_change_scenarios.py new file mode 100644 index 0000000000..816f41afe6 --- /dev/null +++ b/tests/core/integration/test_change_scenarios.py @@ -0,0 +1,1484 @@ +from __future__ import annotations + +import typing as t +import json +from datetime import timedelta +from unittest import mock +import pandas as pd # noqa: TID253 +import pytest +from pathlib import Path +from sqlmesh.core.model.common import ParsableSql +import time_machine +from sqlglot.expressions import DataType +import re + +from sqlmesh.cli.project_init import init_example_project +from sqlmesh.core import constants as c +from sqlmesh.core import dialect as d +from sqlmesh.core.config import ( + AutoCategorizationMode, + Config, + GatewayConfig, + ModelDefaultsConfig, + DuckDBConnectionConfig, +) +from sqlmesh.core.context import Context +from sqlmesh.core.config.categorizer import CategorizerConfig +from sqlmesh.core.model import ( + FullKind, + ModelKind, + ModelKindName, + SqlModel, + PythonModel, + ViewKind, + load_sql_based_model, +) +from sqlmesh.core.model.kind import model_kind_type_from_name +from sqlmesh.core.plan import Plan, SnapshotIntervals +from sqlmesh.core.snapshot import ( + SnapshotChangeCategory, +) +from sqlmesh.utils.date import now, to_timestamp +from sqlmesh.utils.errors import ( + SQLMeshError, +) +from tests.core.integration.utils import ( + apply_to_environment, + add_projection_to_model, + initial_add, + change_data_type, + validate_apply_basics, + change_model_kind, + validate_model_kind_change, + validate_query_change, + validate_plan_changes, +) + +pytestmark = pytest.mark.slow + + +def test_auto_categorization(sushi_context: Context): + environment = "dev" + for config in sushi_context.configs.values(): + config.plan.auto_categorize_changes.sql = AutoCategorizationMode.FULL + initial_add(sushi_context, environment) + + version = sushi_context.get_snapshot( + "sushi.waiter_as_customer_by_day", raise_if_missing=True + ).version + fingerprint = sushi_context.get_snapshot( + "sushi.waiter_as_customer_by_day", raise_if_missing=True + ).fingerprint + + model = t.cast(SqlModel, sushi_context.get_model("sushi.customers", raise_if_missing=True)) + sushi_context.upsert_model( + "sushi.customers", + query_=ParsableSql(sql=model.query.select("'foo' AS foo").sql(dialect=model.dialect)), # type: ignore + ) + apply_to_environment(sushi_context, environment) + + assert ( + sushi_context.get_snapshot( + "sushi.waiter_as_customer_by_day", raise_if_missing=True + ).change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert ( + sushi_context.get_snapshot( + "sushi.waiter_as_customer_by_day", raise_if_missing=True + ).fingerprint + != fingerprint + ) + assert ( + sushi_context.get_snapshot("sushi.waiter_as_customer_by_day", raise_if_missing=True).version + == version + ) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_breaking_only_impacts_immediate_children(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + context.upsert_model(context.get_model("sushi.top_waiters").copy(update={"kind": FullKind()})) + context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) + + breaking_model = context.get_model("sushi.orders") + breaking_model = breaking_model.copy(update={"stamp": "force new version"}) + context.upsert_model(breaking_model) + breaking_snapshot = context.get_snapshot(breaking_model, raise_if_missing=True) + + non_breaking_model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, non_breaking_model))) + non_breaking_snapshot = context.get_snapshot(non_breaking_model, raise_if_missing=True) + top_waiter_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan_builder = context.plan_builder("dev", skip_tests=True, enable_preview=False) + plan_builder.set_choice(breaking_snapshot, SnapshotChangeCategory.BREAKING) + plan = plan_builder.build() + assert ( + plan.context_diff.snapshots[breaking_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.BREAKING + ) + assert ( + plan.context_diff.snapshots[non_breaking_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiter_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert plan.start == to_timestamp("2023-01-01") + assert not any(i.snapshot_id == top_waiter_snapshot.snapshot_id for i in plan.missing_intervals) + + context.apply(plan) + assert ( + not context.plan_builder("dev", skip_tests=True, enable_preview=False) + .build() + .requires_backfill + ) + + # Deploy everything to prod. + plan = context.plan_builder("prod", skip_tests=True).build() + assert not plan.missing_intervals + + context.apply(plan) + assert ( + not context.plan_builder("prod", skip_tests=True, enable_preview=False) + .build() + .requires_backfill + ) + + +@pytest.mark.parametrize( + "context_fixture", + ["sushi_context", "sushi_dbt_context", "sushi_test_dbt_context", "sushi_no_default_catalog"], +) +def test_model_add(context_fixture: Context, request): + initial_add(request.getfixturevalue(context_fixture), "dev") + + +def test_model_removed(sushi_context: Context): + environment = "dev" + initial_add(sushi_context, environment) + + top_waiters_snapshot_id = sushi_context.get_snapshot( + "sushi.top_waiters", raise_if_missing=True + ).snapshot_id + + sushi_context._models.pop('"memory"."sushi"."top_waiters"') + + def _validate_plan(context, plan): + validate_plan_changes(plan, removed=[top_waiters_snapshot_id]) + assert not plan.missing_intervals + + def _validate_apply(context): + assert not sushi_context.get_snapshot("sushi.top_waiters", raise_if_missing=False) + assert sushi_context.state_reader.get_snapshots([top_waiters_snapshot_id]) + env = sushi_context.state_reader.get_environment(environment) + assert env + assert all(snapshot.name != '"memory"."sushi"."top_waiters"' for snapshot in env.snapshots) + + apply_to_environment( + sushi_context, + environment, + SnapshotChangeCategory.BREAKING, + plan_validators=[_validate_plan], + apply_validators=[_validate_apply], + ) + + +def test_non_breaking_change(sushi_context: Context): + environment = "dev" + initial_add(sushi_context, environment) + validate_query_change(sushi_context, environment, SnapshotChangeCategory.NON_BREAKING, False) + + +def test_breaking_change(sushi_context: Context): + environment = "dev" + initial_add(sushi_context, environment) + validate_query_change(sushi_context, environment, SnapshotChangeCategory.BREAKING, False) + + +def test_logical_change(sushi_context: Context): + environment = "dev" + initial_add(sushi_context, environment) + previous_sushi_items_version = sushi_context.get_snapshot( + "sushi.items", raise_if_missing=True + ).version + + change_data_type( + sushi_context, + "sushi.items", + DataType.Type.DOUBLE, + DataType.Type.FLOAT, + ) + apply_to_environment(sushi_context, environment, SnapshotChangeCategory.NON_BREAKING) + + change_data_type( + sushi_context, + "sushi.items", + DataType.Type.FLOAT, + DataType.Type.DOUBLE, + ) + apply_to_environment(sushi_context, environment, SnapshotChangeCategory.NON_BREAKING) + + assert ( + sushi_context.get_snapshot("sushi.items", raise_if_missing=True).version + == previous_sushi_items_version + ) + + +@pytest.mark.parametrize( + "from_, to", + [ + (ModelKindName.INCREMENTAL_BY_TIME_RANGE, ModelKindName.FULL), + (ModelKindName.FULL, ModelKindName.INCREMENTAL_BY_TIME_RANGE), + ], +) +def test_model_kind_change(from_: ModelKindName, to: ModelKindName, sushi_context: Context): + environment = f"test_model_kind_change__{from_.value.lower()}__{to.value.lower()}" + incremental_snapshot = sushi_context.get_snapshot("sushi.items", raise_if_missing=True).copy() + + if from_ != ModelKindName.INCREMENTAL_BY_TIME_RANGE: + change_model_kind(sushi_context, from_) + apply_to_environment(sushi_context, environment, SnapshotChangeCategory.NON_BREAKING) + + if to == ModelKindName.INCREMENTAL_BY_TIME_RANGE: + sushi_context.upsert_model(incremental_snapshot.model) + else: + change_model_kind(sushi_context, to) + + logical = to in (ModelKindName.INCREMENTAL_BY_TIME_RANGE, ModelKindName.EMBEDDED) + validate_model_kind_change(to, sushi_context, environment, logical=logical) + + +def test_environment_isolation(sushi_context: Context): + prod_snapshots = sushi_context.snapshots.values() + + change_data_type( + sushi_context, + "sushi.items", + DataType.Type.DOUBLE, + DataType.Type.FLOAT, + ) + directly_modified = ['"memory"."sushi"."items"'] + indirectly_modified = [ + '"memory"."sushi"."order_items"', + '"memory"."sushi"."waiter_revenue_by_day"', + '"memory"."sushi"."customer_revenue_by_day"', + '"memory"."sushi"."customer_revenue_lifetime"', + '"memory"."sushi"."top_waiters"', + "assert_item_price_above_zero", + ] + + apply_to_environment(sushi_context, "dev", SnapshotChangeCategory.BREAKING) + + # Verify prod unchanged + validate_apply_basics(sushi_context, "prod", prod_snapshots) + + def _validate_plan(context, plan): + validate_plan_changes(plan, modified=directly_modified + indirectly_modified) + assert not plan.missing_intervals + + apply_to_environment( + sushi_context, + "prod", + SnapshotChangeCategory.BREAKING, + plan_validators=[_validate_plan], + ) + + +def test_environment_promotion(sushi_context: Context): + initial_add(sushi_context, "dev") + + # Simulate prod "ahead" + change_data_type(sushi_context, "sushi.items", DataType.Type.DOUBLE, DataType.Type.FLOAT) + apply_to_environment(sushi_context, "prod", SnapshotChangeCategory.BREAKING) + + # Simulate rebase + apply_to_environment(sushi_context, "dev", SnapshotChangeCategory.BREAKING) + + # Make changes in dev + change_data_type(sushi_context, "sushi.items", DataType.Type.FLOAT, DataType.Type.DECIMAL) + apply_to_environment(sushi_context, "dev", SnapshotChangeCategory.NON_BREAKING) + + change_data_type(sushi_context, "sushi.top_waiters", DataType.Type.DOUBLE, DataType.Type.INT) + apply_to_environment(sushi_context, "dev", SnapshotChangeCategory.BREAKING) + + change_data_type( + sushi_context, + "sushi.customer_revenue_by_day", + DataType.Type.DOUBLE, + DataType.Type.FLOAT, + ) + apply_to_environment( + sushi_context, + "dev", + SnapshotChangeCategory.FORWARD_ONLY, + allow_destructive_models=['"memory"."sushi"."customer_revenue_by_day"'], + ) + + # Promote to prod + def _validate_plan(context, plan): + sushi_items_snapshot = context.get_snapshot("sushi.items", raise_if_missing=True) + sushi_top_waiters_snapshot = context.get_snapshot( + "sushi.top_waiters", raise_if_missing=True + ) + sushi_customer_revenue_by_day_snapshot = context.get_snapshot( + "sushi.customer_revenue_by_day", raise_if_missing=True + ) + + assert ( + plan.context_diff.modified_snapshots[sushi_items_snapshot.name][0].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.modified_snapshots[sushi_top_waiters_snapshot.name][0].change_category + == SnapshotChangeCategory.BREAKING + ) + assert ( + plan.context_diff.modified_snapshots[sushi_customer_revenue_by_day_snapshot.name][ + 0 + ].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert plan.context_diff.snapshots[ + sushi_customer_revenue_by_day_snapshot.snapshot_id + ].is_forward_only + + apply_to_environment( + sushi_context, + "prod", + SnapshotChangeCategory.NON_BREAKING, + plan_validators=[_validate_plan], + allow_destructive_models=['"memory"."sushi"."customer_revenue_by_day"'], + ) + + +def test_no_override(sushi_context: Context) -> None: + change_data_type( + sushi_context, + "sushi.items", + DataType.Type.INT, + DataType.Type.BIGINT, + ) + + change_data_type( + sushi_context, + "sushi.order_items", + DataType.Type.INT, + DataType.Type.BIGINT, + ) + + plan_builder = sushi_context.plan_builder("prod") + plan = plan_builder.build() + + sushi_items_snapshot = sushi_context.get_snapshot("sushi.items", raise_if_missing=True) + sushi_order_items_snapshot = sushi_context.get_snapshot( + "sushi.order_items", raise_if_missing=True + ) + sushi_water_revenue_by_day_snapshot = sushi_context.get_snapshot( + "sushi.waiter_revenue_by_day", raise_if_missing=True + ) + + items = plan.context_diff.snapshots[sushi_items_snapshot.snapshot_id] + order_items = plan.context_diff.snapshots[sushi_order_items_snapshot.snapshot_id] + waiter_revenue = plan.context_diff.snapshots[sushi_water_revenue_by_day_snapshot.snapshot_id] + + plan_builder.set_choice(items, SnapshotChangeCategory.BREAKING).set_choice( + order_items, SnapshotChangeCategory.NON_BREAKING + ) + plan_builder.build() + assert items.is_new_version + assert waiter_revenue.is_new_version + plan_builder.set_choice(items, SnapshotChangeCategory.NON_BREAKING) + plan_builder.build() + assert not waiter_revenue.is_new_version + + +@pytest.mark.parametrize( + "change_categories, expected", + [ + ([SnapshotChangeCategory.NON_BREAKING], SnapshotChangeCategory.BREAKING), + ([SnapshotChangeCategory.BREAKING], SnapshotChangeCategory.BREAKING), + ( + [SnapshotChangeCategory.NON_BREAKING, SnapshotChangeCategory.NON_BREAKING], + SnapshotChangeCategory.BREAKING, + ), + ( + [SnapshotChangeCategory.NON_BREAKING, SnapshotChangeCategory.BREAKING], + SnapshotChangeCategory.BREAKING, + ), + ( + [SnapshotChangeCategory.BREAKING, SnapshotChangeCategory.NON_BREAKING], + SnapshotChangeCategory.BREAKING, + ), + ( + [SnapshotChangeCategory.BREAKING, SnapshotChangeCategory.BREAKING], + SnapshotChangeCategory.BREAKING, + ), + ], +) +def test_revert( + sushi_context: Context, + change_categories: t.List[SnapshotChangeCategory], + expected: SnapshotChangeCategory, +): + environment = "prod" + original_snapshot_id = sushi_context.get_snapshot("sushi.items", raise_if_missing=True) + + types = (DataType.Type.DOUBLE, DataType.Type.FLOAT, DataType.Type.DECIMAL) + assert len(change_categories) < len(types) + + for i, category in enumerate(change_categories): + change_data_type(sushi_context, "sushi.items", *types[i : i + 2]) + apply_to_environment(sushi_context, environment, category) + assert ( + sushi_context.get_snapshot("sushi.items", raise_if_missing=True) != original_snapshot_id + ) + + change_data_type(sushi_context, "sushi.items", types[len(change_categories)], types[0]) + + def _validate_plan(_, plan): + snapshot = next(s for s in plan.snapshots.values() if s.name == '"memory"."sushi"."items"') + assert snapshot.change_category == expected + assert not plan.missing_intervals + + apply_to_environment( + sushi_context, + environment, + change_categories[-1], + plan_validators=[_validate_plan], + ) + assert sushi_context.get_snapshot("sushi.items", raise_if_missing=True) == original_snapshot_id + + +def test_revert_after_downstream_change(sushi_context: Context): + environment = "prod" + change_data_type(sushi_context, "sushi.items", DataType.Type.DOUBLE, DataType.Type.FLOAT) + apply_to_environment(sushi_context, environment, SnapshotChangeCategory.BREAKING) + + change_data_type( + sushi_context, + "sushi.waiter_revenue_by_day", + DataType.Type.DOUBLE, + DataType.Type.FLOAT, + ) + apply_to_environment(sushi_context, environment, SnapshotChangeCategory.NON_BREAKING) + + change_data_type(sushi_context, "sushi.items", DataType.Type.FLOAT, DataType.Type.DOUBLE) + + def _validate_plan(_, plan): + snapshot = next(s for s in plan.snapshots.values() if s.name == '"memory"."sushi"."items"') + assert snapshot.change_category == SnapshotChangeCategory.BREAKING + assert plan.missing_intervals + + apply_to_environment( + sushi_context, + environment, + SnapshotChangeCategory.BREAKING, + plan_validators=[_validate_plan], + ) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + # Make sure that the most downstream model is a materialized model. + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": FullKind()}) + context.upsert_model(model) + context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) + + # Make sushi.orders a forward-only model. + model = context.get_model("sushi.orders") + updated_model_kind = model.kind.copy(update={"forward_only": True}) + model = model.copy(update={"stamp": "force new version", "kind": updated_model_kind}) + context.upsert_model(model) + snapshot = context.get_snapshot(model, raise_if_missing=True) + + plan = context.plan_builder( + "dev", + skip_tests=True, + enable_preview=False, + categorizer_config=CategorizerConfig.all_full(), + ).build() + assert ( + plan.context_diff.snapshots[snapshot.snapshot_id].change_category + == SnapshotChangeCategory.BREAKING + ) + assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only + assert not plan.requires_backfill + context.apply(plan) + + # Make a non-breaking change to a model. + model = context.get_model("sushi.top_waiters") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() + assert len(plan.new_snapshots) == 1 + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert plan.start == to_timestamp("2023-01-01") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + # Apply the non-breaking changes. + context.apply(plan) + + # Make a non-breaking change upstream from the previously modified model. + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + waiter_revenue_by_day_snapshot = context.get_snapshot( + "sushi.waiter_revenue_by_day", raise_if_missing=True + ) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() + assert len(plan.new_snapshots) == 2 + assert ( + plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert plan.start == to_timestamp("2023-01-01") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + # Apply the upstream non-breaking changes. + context.apply(plan) + assert not context.plan_builder("dev", skip_tests=True).build().requires_backfill + + # Deploy everything to prod. + plan = context.plan_builder("prod", skip_tests=True, enable_preview=False).build() + assert plan.start == to_timestamp("2023-01-01") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + context.apply(plan) + assert ( + not context.plan_builder("prod", skip_tests=True, enable_preview=False) + .build() + .requires_backfill + ) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +@pytest.mark.parametrize("forward_only", [False, True]) +def test_plan_repairs_unrenderable_snapshot_state( + init_and_plan_context: t.Callable, forward_only: bool +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + target_snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") + assert target_snapshot + + # Manually corrupt the snapshot's query + raw_snapshot = context.state_sync.state_sync.engine_adapter.fetchone( + f"SELECT snapshot FROM sqlmesh._snapshots WHERE name = '{target_snapshot.name}' AND identifier = '{target_snapshot.identifier}'" + )[0] # type: ignore + parsed_snapshot = json.loads(raw_snapshot) + parsed_snapshot["node"]["query"] = "SELECT @missing_macro()" + context.state_sync.state_sync.engine_adapter.update_table( + "sqlmesh._snapshots", + {"snapshot": json.dumps(parsed_snapshot)}, + f"name = '{target_snapshot.name}' AND identifier = '{target_snapshot.identifier}'", + ) + + context.clear_caches() + target_snapshot_in_state = context.state_sync.get_snapshots([target_snapshot.snapshot_id])[ + target_snapshot.snapshot_id + ] + + with pytest.raises(Exception): + target_snapshot_in_state.model.render_query_or_raise() + + # Repair the snapshot by creating a new version of it + context.upsert_model(target_snapshot.model.name, stamp="repair") + target_snapshot = context.get_snapshot(target_snapshot.name) + + plan_builder = context.plan_builder("prod", forward_only=forward_only) + plan = plan_builder.build() + if not forward_only: + assert target_snapshot.snapshot_id in {i.snapshot_id for i in plan.missing_intervals} + assert plan.directly_modified == {target_snapshot.snapshot_id} + plan_builder.set_choice(target_snapshot, SnapshotChangeCategory.NON_BREAKING) + plan = plan_builder.build() + + context.apply(plan) + + context.clear_caches() + assert context.get_snapshot(target_snapshot.name).model.render_query_or_raise() + target_snapshot_in_state = context.state_sync.get_snapshots([target_snapshot.snapshot_id])[ + target_snapshot.snapshot_id + ] + assert target_snapshot_in_state.model.render_query_or_raise() + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_no_backfill_for_model_downstream_of_metadata_change(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + # Make sushi.waiter_revenue_by_day a forward-only model. + forward_only_model = context.get_model("sushi.waiter_revenue_by_day") + updated_model_kind = forward_only_model.kind.copy(update={"forward_only": True}) + forward_only_model = forward_only_model.copy(update={"kind": updated_model_kind}) + context.upsert_model(forward_only_model) + + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + + # Make a metadata change upstream of the forward-only model. + context.upsert_model("sushi.orders", owner="new_owner") + + plan = context.plan_builder("test_dev").build() + assert plan.has_changes + assert not plan.directly_modified + assert not plan.indirectly_modified + assert not plan.missing_intervals + assert all( + snapshot.change_category == SnapshotChangeCategory.METADATA + for snapshot in plan.new_snapshots + ) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_plan_set_choice_is_reflected_in_missing_intervals(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + context.upsert_model(context.get_model("sushi.top_waiters").copy(update={"kind": FullKind()})) + context.plan("prod", skip_tests=True, no_prompts=True, auto_apply=True) + + model_name = "sushi.waiter_revenue_by_day" + + model = context.get_model(model_name) + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + snapshot = context.get_snapshot(model, raise_if_missing=True) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan_builder = context.plan_builder("dev", skip_tests=True) + plan = plan_builder.build() + assert len(plan.new_snapshots) == 2 + assert ( + plan.context_diff.snapshots[snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert plan.start == to_timestamp("2023-01-01") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + # Change the category to BREAKING + plan = plan_builder.set_choice( + plan.context_diff.snapshots[snapshot.snapshot_id], SnapshotChangeCategory.BREAKING + ).build() + assert ( + plan.context_diff.snapshots[snapshot.snapshot_id].change_category + == SnapshotChangeCategory.BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_BREAKING + ) + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + # Change the category back to NON_BREAKING + plan = plan_builder.set_choice( + plan.context_diff.snapshots[snapshot.snapshot_id], SnapshotChangeCategory.NON_BREAKING + ).build() + assert ( + plan.context_diff.snapshots[snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + context.apply(plan) + + dev_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" + ) + assert dev_df["event_date"].tolist() == [ + pd.to_datetime(x) + for x in [ + "2023-01-01", + "2023-01-02", + "2023-01-03", + "2023-01-04", + "2023-01-05", + "2023-01-06", + "2023-01-07", + ] + ] + + # Promote changes to prod + prod_plan = context.plan_builder(skip_tests=True).build() + assert not prod_plan.missing_intervals + + context.apply(prod_plan) + prod_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi.waiter_revenue_by_day WHERE one IS NOT NULL ORDER BY event_date" + ) + assert prod_df["event_date"].tolist() == [ + pd.to_datetime(x) + for x in [ + "2023-01-01", + "2023-01-02", + "2023-01-03", + "2023-01-04", + "2023-01-05", + "2023-01-06", + "2023-01-07", + ] + ] + + +def test_plan_production_environment_statements(tmp_path: Path): + model_a = """ + MODEL ( + name test_schema.a, + kind FULL, + ); + + @IF( + @runtime_stage IN ('evaluating', 'creating'), + INSERT INTO schema_names_for_prod (physical_schema_name) VALUES (@resolve_template('@{schema_name}')) + ); + + SELECT 1 AS account_id + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + for path, defn in {"a.sql": model_a}.items(): + with open(models_dir / path, "w") as f: + f.write(defn) + + before_all = [ + "CREATE TABLE IF NOT EXISTS schema_names_for_@this_env (physical_schema_name VARCHAR)", + "@IF(@runtime_stage = 'before_all', CREATE TABLE IF NOT EXISTS should_create AS SELECT @runtime_stage)", + ] + after_all = [ + "@IF(@this_env = 'prod', CREATE TABLE IF NOT EXISTS after_t AS SELECT @var_5)", + "@IF(@runtime_stage = 'before_all', CREATE TABLE IF NOT EXISTS not_create AS SELECT @runtime_stage)", + ] + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + before_all=before_all, + after_all=after_all, + variables={"var_5": 5}, + ) + ctx = Context(paths=[tmp_path], config=config) + ctx.plan(auto_apply=True, no_prompts=True) + + before_t = ctx.fetchdf("select * from schema_names_for_prod").to_dict() + assert before_t["physical_schema_name"][0] == "sqlmesh__test_schema" + + after_t = ctx.fetchdf("select * from after_t").to_dict() + assert after_t["5"][0] == 5 + + environment_statements = ctx.state_reader.get_environment_statements(c.PROD) + assert environment_statements[0].before_all == before_all + assert environment_statements[0].after_all == after_all + assert environment_statements[0].python_env.keys() == {"__sqlmesh__vars__"} + assert environment_statements[0].python_env["__sqlmesh__vars__"].payload == "{'var_5': 5}" + + should_create = ctx.fetchdf("select * from should_create").to_dict() + assert should_create["before_all"][0] == "before_all" + + with pytest.raises( + Exception, match=r"Catalog Error: Table with name not_create does not exist!" + ): + ctx.fetchdf("select * from not_create") + + +def test_environment_statements_error_handling(tmp_path: Path): + model_a = """ + MODEL ( + name test_schema.a, + kind FULL, + ); + + SELECT 1 AS account_id + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + for path, defn in {"a.sql": model_a}.items(): + with open(models_dir / path, "w") as f: + f.write(defn) + + before_all = [ + "CREATE TABLE identical_table (physical_schema_name VARCHAR)", + "CREATE TABLE identical_table (physical_schema_name VARCHAR)", + ] + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + before_all=before_all, + ) + ctx = Context(paths=[tmp_path], config=config) + + expected_error_message = re.escape( + """An error occurred during execution of the following 'before_all' statement: + +CREATE TABLE identical_table (physical_schema_name TEXT) + +Catalog Error: Table with name "identical_table" already exists!""" + ) + + with pytest.raises(SQLMeshError, match=expected_error_message): + ctx.plan(auto_apply=True, no_prompts=True) + + after_all = [ + "@bad_macro()", + ] + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + after_all=after_all, + ) + ctx = Context(paths=[tmp_path], config=config) + + expected_error_message = re.escape( + """An error occurred during rendering of the 'after_all' statements: + +Failed to resolve macros for + +@bad_macro() + +Macro 'bad_macro' does not exist.""" + ) + + with pytest.raises(SQLMeshError, match=expected_error_message): + ctx.plan(auto_apply=True, no_prompts=True) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_full_model_change_with_plan_start_not_matching_model_start( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + model = context.get_model("sushi.top_waiters") + context.upsert_model(model, kind=model_kind_type_from_name("FULL")()) # type: ignore + + # Apply the change with --skip-backfill first and no plan start + context.plan("dev", skip_tests=True, skip_backfill=True, no_prompts=True, auto_apply=True) + + # Apply the plan again but this time don't skip backfill and set start + # to be later than the model start + context.plan("dev", skip_tests=True, no_prompts=True, auto_apply=True, start="1 day ago") + + # Check that the number of rows is not 0 + row_num = context.engine_adapter.fetchone(f"SELECT COUNT(*) FROM sushi__dev.top_waiters")[0] + assert row_num > 0 + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_hourly_model_with_lookback_no_backfill_in_dev(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + + model_name = "sushi.waiter_revenue_by_day" + + model = context.get_model(model_name) + model = SqlModel.parse_obj( + { + **model.dict(), + "kind": model.kind.copy(update={"lookback": 1}), + "cron": "@hourly", + "audits": [], + } + ) + context.upsert_model(model) + + plan = context.plan_builder("prod", skip_tests=True).build() + context.apply(plan) + + top_waiters_model = context.get_model("sushi.top_waiters") + top_waiters_model = add_projection_to_model(t.cast(SqlModel, top_waiters_model), literal=True) + context.upsert_model(top_waiters_model) + + context.get_snapshot(model, raise_if_missing=True) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + with time_machine.travel(now() + timedelta(hours=2)): + plan = context.plan_builder("dev", skip_tests=True).build() + # Make sure the waiter_revenue_by_day model is not backfilled. + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_max_interval_end_per_model_not_applied_when_end_is_provided( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + with time_machine.travel("2023-01-09 00:00:00 UTC"): + context.run() + + plan = context.plan_builder( + restate_models=["*"], start="2023-01-09", end="2023-01-09" + ).build() + context.apply(plan) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_plan_against_expired_environment(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + modified_models = {model.fqn, context.get_model("sushi.top_waiters").fqn} + + plan = context.plan_builder("dev").build() + assert plan.has_changes + assert set(plan.context_diff.modified_snapshots) == modified_models + assert plan.missing_intervals + context.apply(plan) + + # Make sure there are no changes when comparing against the existing environment. + plan = context.plan_builder("dev").build() + assert not plan.has_changes + assert not plan.context_diff.modified_snapshots + assert not plan.missing_intervals + + # Invalidate the environment and make sure that the plan detects the changes. + context.invalidate_environment("dev") + plan = context.plan_builder("dev").build() + assert plan.has_changes + assert set(plan.context_diff.modified_snapshots) == modified_models + assert not plan.missing_intervals + context.apply(plan) + + +def test_plan_environment_statements_doesnt_cause_extra_diff(tmp_path: Path): + model_a = """ + MODEL ( + name test_schema.a, + kind FULL, + ); + + SELECT 1; + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + (models_dir / "a.sql").write_text(model_a) + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + before_all=["select 1 as before_all"], + after_all=["select 2 as after_all"], + ) + ctx = Context(paths=[tmp_path], config=config) + + # first plan - should apply changes + assert ctx.plan(auto_apply=True, no_prompts=True).has_changes + + # second plan - nothing has changed so should report no changes + assert not ctx.plan(auto_apply=True, no_prompts=True).has_changes + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_plan_snapshot_table_exists_for_promoted_snapshot(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + context.plan("dev", auto_apply=True, no_prompts=True, skip_tests=True) + + # Drop the views and make sure SQLMesh recreates them later + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + context.engine_adapter.drop_view(top_waiters_snapshot.table_name()) + context.engine_adapter.drop_view(top_waiters_snapshot.table_name(False)) + + # Make the environment unfinalized to force recreation of all views in the virtual layer + context.state_sync.state_sync.engine_adapter.execute( + "UPDATE sqlmesh._environments SET finalized_ts = NULL WHERE name = 'dev'" + ) + + context.plan( + "prod", + restate_models=["sushi.top_waiters"], + auto_apply=True, + no_prompts=True, + skip_tests=True, + ) + assert context.engine_adapter.table_exists(top_waiters_snapshot.table_name()) + + +def test_plan_twice_with_star_macro_yields_no_diff(tmp_path: Path): + init_example_project(tmp_path, engine_type="duckdb") + + star_model_definition = """ + MODEL ( + name sqlmesh_example.star_model, + kind FULL + ); + + SELECT @STAR(sqlmesh_example.full_model) FROM sqlmesh_example.full_model + """ + + star_model_path = tmp_path / "models" / "star_model.sql" + star_model_path.write_text(star_model_definition) + + db_path = str(tmp_path / "db.db") + config = Config( + gateways={"main": GatewayConfig(connection=DuckDBConnectionConfig(database=db_path))}, + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + ) + context = Context(paths=tmp_path, config=config) + context.plan(auto_apply=True, no_prompts=True) + + # Instantiate new context to remove caches etc + new_context = Context(paths=tmp_path, config=config) + + star_model = new_context.get_model("sqlmesh_example.star_model") + assert ( + star_model.render_query_or_raise().sql() + == 'SELECT CAST("full_model"."item_id" AS INT) AS "item_id", CAST("full_model"."num_orders" AS BIGINT) AS "num_orders" FROM "db"."sqlmesh_example"."full_model" AS "full_model"' + ) + + new_plan = new_context.plan_builder().build() + assert not new_plan.has_changes + assert not new_plan.new_snapshots + + +class OldPythonModel(PythonModel): + kind: ModelKind = ViewKind() + + +def test_python_model_default_kind_change(init_and_plan_context: t.Callable): + """ + Around 2024-07-17 Python models had their default Kind changed from VIEW to FULL in order to + avoid some edge cases where the views might not get updated in certain situations. + + This test ensures that if a user had a Python `kind: VIEW` model stored in state, + it can still be loaded without error and just show as a breaking change from `kind: VIEW` + to `kind: FULL` + """ + + # note: we deliberately dont specify a Kind here to allow the defaults to be picked up + python_model_file = """import typing as t +import pandas as pd # noqa: TID253 +from sqlmesh import ExecutionContext, model + +@model( + "sushi.python_view_model", + columns={ + "id": "int", + } +) +def execute( + context: ExecutionContext, + **kwargs: t.Any, +) -> pd.DataFrame: + return pd.DataFrame([ + {"id": 1} + ]) +""" + + context: Context + context, _ = init_and_plan_context("examples/sushi") + + with open(context.path / "models" / "python_view_model.py", mode="w", encoding="utf8") as f: + f.write(python_model_file) + + # monkey-patch PythonModel to default to kind: View again + # and ViewKind to allow python models again + with ( + mock.patch.object(ViewKind, "supports_python_models", return_value=True), + mock.patch("sqlmesh.core.model.definition.PythonModel", OldPythonModel), + ): + context.load() + + # check the monkey-patching worked + model = context.get_model("sushi.python_view_model") + assert model.kind.name == ModelKindName.VIEW + assert model.source_type == "python" + + # apply plan + plan: Plan = context.plan(auto_apply=True) + + # check that run() still works even though we have a Python model with kind: View in the state + snapshot_ids = [s for s in plan.directly_modified if "python_view_model" in s.name] + snapshot_from_state = list(context.state_sync.get_snapshots(snapshot_ids).values())[0] + assert snapshot_from_state.model.kind.name == ModelKindName.VIEW + assert snapshot_from_state.model.source_type == "python" + context.run() + + # reload context to load model with new defaults + # this also shows the earlier monkey-patching is no longer in effect + context.load() + model = context.get_model("sushi.python_view_model") + assert model.kind.name == ModelKindName.FULL + assert model.source_type == "python" + + plan = context.plan( + categorizer_config=CategorizerConfig.all_full() + ) # the default categorizer_config doesnt auto-categorize python models + + assert plan.has_changes + assert not plan.indirectly_modified + + assert len(plan.directly_modified) == 1 + snapshot_id = list(plan.directly_modified)[0] + assert snapshot_id.name == '"memory"."sushi"."python_view_model"' + assert plan.modified_snapshots[snapshot_id].change_category == SnapshotChangeCategory.BREAKING + + context.apply(plan) + + df = context.engine_adapter.fetchdf("SELECT id FROM sushi.python_view_model") + assert df["id"].to_list() == [1] + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +@pytest.mark.parametrize( + "parent_a_category,parent_b_category,expected_child_category", + [ + ( + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.INDIRECT_BREAKING, + ), + ( + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.INDIRECT_NON_BREAKING, + ), + ( + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.INDIRECT_NON_BREAKING, + ), + ( + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.INDIRECT_BREAKING, + ), + ( + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.METADATA, + ), + ( + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.METADATA, + ), + ( + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.BREAKING, + SnapshotChangeCategory.INDIRECT_BREAKING, + ), + ( + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.NON_BREAKING, + SnapshotChangeCategory.INDIRECT_NON_BREAKING, + ), + ( + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.METADATA, + SnapshotChangeCategory.METADATA, + ), + ], +) +def test_rebase_two_changed_parents( + init_and_plan_context: t.Callable, + parent_a_category: SnapshotChangeCategory, # This change is deployed to prod first + parent_b_category: SnapshotChangeCategory, # This change is deployed to prod second + expected_child_category: SnapshotChangeCategory, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + initial_model_a = context.get_model("sushi.orders") + initial_model_b = context.get_model("sushi.items") + + # Make change A and deploy it to dev_a + context.upsert_model(initial_model_a.name, stamp="1") + plan_builder = context.plan_builder("dev_a", skip_tests=True) + plan_builder.set_choice(context.get_snapshot(initial_model_a.name), parent_a_category) + context.apply(plan_builder.build()) + + # Make change B and deploy it to dev_b + context.upsert_model(initial_model_a) + context.upsert_model(initial_model_b.name, stamp="1") + plan_builder = context.plan_builder("dev_b", skip_tests=True) + plan_builder.set_choice(context.get_snapshot(initial_model_b.name), parent_b_category) + context.apply(plan_builder.build()) + + # Deploy change A to prod + context.upsert_model(initial_model_a.name, stamp="1") + context.upsert_model(initial_model_b) + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + + # Apply change B in addition to A and plan against prod + context.upsert_model(initial_model_b.name, stamp="1") + plan = context.plan_builder("prod", skip_tests=True).build() + + # Validate the category of child snapshots + direct_child_snapshot = plan.snapshots[context.get_snapshot("sushi.order_items").snapshot_id] + assert direct_child_snapshot.change_category == expected_child_category + + indirect_child_snapshot = plan.snapshots[context.get_snapshot("sushi.top_waiters").snapshot_id] + assert indirect_child_snapshot.change_category == expected_child_category + + +@pytest.mark.parametrize( + "context_fixture", + ["sushi_context", "sushi_no_default_catalog"], +) +def test_unaligned_start_snapshots(context_fixture: Context, request): + context = request.getfixturevalue(context_fixture) + environment = "dev" + apply_to_environment(context, environment) + # Make breaking change to model upstream of a depends_on_self model + context.upsert_model("sushi.order_items", stamp="1") + # Apply the change starting at a date later then the beginning of the downstream depends_on_self model + plan = apply_to_environment( + context, + environment, + choice=SnapshotChangeCategory.BREAKING, + plan_start="2 days ago", + enable_preview=True, + ) + revenue_lifetime_snapshot = context.get_snapshot( + "sushi.customer_revenue_lifetime", raise_if_missing=True + ) + # Validate that the depends_on_self model is non-deployable + assert not plan.deployability_index.is_deployable(revenue_lifetime_snapshot) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_unaligned_start_snapshot_with_non_deployable_downstream(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + downstream_model_name = "memory.sushi.customer_max_revenue" + + expressions = d.parse( + f""" + MODEL ( + name {downstream_model_name}, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key customer_id, + forward_only true, + ), + ); + + SELECT + customer_id, MAX(revenue) AS max_revenue + FROM memory.sushi.customer_revenue_lifetime + GROUP BY 1; + """ + ) + + downstream_model = load_sql_based_model(expressions) + assert downstream_model.forward_only + context.upsert_model(downstream_model) + + context.plan(auto_apply=True, no_prompts=True) + + customer_revenue_lifetime_model = context.get_model("sushi.customer_revenue_lifetime") + kwargs = { + **customer_revenue_lifetime_model.dict(), + "name": "memory.sushi.customer_revenue_lifetime_new", + "kind": dict( + name="INCREMENTAL_UNMANAGED" + ), # Make it incremental unmanaged to ensure the depends_on_past behavior. + } + context.upsert_model(SqlModel.parse_obj(kwargs)) + context.upsert_model( + downstream_model_name, + query_=ParsableSql( + sql="SELECT customer_id, MAX(revenue) AS max_revenue FROM memory.sushi.customer_revenue_lifetime_new GROUP BY 1" + ), + ) + + plan = context.plan_builder("dev", enable_preview=True).build() + assert {s.name for s in plan.new_snapshots} == { + '"memory"."sushi"."customer_revenue_lifetime_new"', + '"memory"."sushi"."customer_max_revenue"', + } + for snapshot_interval in plan.missing_intervals: + assert not plan.deployability_index.is_deployable(snapshot_interval.snapshot_id) + assert snapshot_interval.intervals[0][0] == to_timestamp("2023-01-07") + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_indirect_non_breaking_view_is_updated_with_new_table_references( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Add a new projection to the base model + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + + # Run the janitor to delete the old snapshot record + context.run_janitor(ignore_ttl=True) + + # Check the downstream view and make sure it's still queryable + assert context.get_model("sushi.top_waiters").kind.is_view + row_num = context.engine_adapter.fetchone(f"SELECT COUNT(*) FROM sushi.top_waiters")[0] + assert row_num > 0 + + +@time_machine.travel("2023-01-08 00:00:00 UTC") +def test_annotated_self_referential_model(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + # Projections are fully annotated in the query but columns were not specified explicitly + expressions = d.parse( + f""" + MODEL ( + name memory.sushi.test_self_ref, + kind FULL, + start '2023-01-01', + ); + + SELECT 1::INT AS one FROM memory.sushi.test_self_ref; + """ + ) + model = load_sql_based_model(expressions) + assert model.depends_on_self + context.upsert_model(model) + + context.plan("prod", skip_tests=True, no_prompts=True, auto_apply=True) + + df = context.fetchdf("SELECT one FROM memory.sushi.test_self_ref") + assert len(df) == 0 diff --git a/tests/core/integration/test_config.py b/tests/core/integration/test_config.py new file mode 100644 index 0000000000..5d571cd7c5 --- /dev/null +++ b/tests/core/integration/test_config.py @@ -0,0 +1,580 @@ +from __future__ import annotations + +import typing as t +from unittest.mock import patch +import logging +import pytest +from pytest import MonkeyPatch +from pathlib import Path +from pytest_mock.plugin import MockerFixture +from sqlglot import exp +from IPython.utils.capture import capture_output + +from sqlmesh.core.config import ( + Config, + GatewayConfig, + ModelDefaultsConfig, + DuckDBConnectionConfig, + TableNamingConvention, + AutoCategorizationMode, +) +from sqlmesh.core.config.common import EnvironmentSuffixTarget +from sqlmesh.core.context import Context +from sqlmesh.core.config.plan import PlanConfig +from sqlmesh.core.engine_adapter import DuckDBEngineAdapter +from sqlmesh.core.model import SqlModel +from sqlmesh.core.model.common import ParsableSql +from sqlmesh.core.snapshot import ( + SnapshotChangeCategory, +) +from sqlmesh.utils.errors import ( + ConfigError, +) +from tests.conftest import DuckDBMetadata +from tests.utils.test_helpers import use_terminal_console +from tests.utils.test_filesystem import create_temp_file +from tests.core.integration.utils import apply_to_environment, initial_add + +pytestmark = pytest.mark.slow + + +@pytest.mark.set_default_connection(disable=True) +def test_missing_connection_config(): + # This is testing the actual implementation of Config.get_connection + # To make writing tests easier, it's patched by the autouse fixture provide_sqlmesh_default_connection + # Case 1: No default_connection or gateways specified should raise a ConfigError + with pytest.raises(ConfigError): + ctx = Context(config=Config()) + + # Case 2: No connection specified in the gateway should raise a ConfigError + with pytest.raises(ConfigError): + ctx = Context(config=Config(gateways={"incorrect": GatewayConfig()})) + + # Case 3: Specifying a default_connection or connection in the gateway should work + ctx = Context(config=Config(default_connection=DuckDBConnectionConfig())) + ctx = Context( + config=Config(gateways={"default": GatewayConfig(connection=DuckDBConnectionConfig())}) + ) + + +def test_physical_table_naming_strategy_table_only(copy_to_temp_path: t.Callable): + sushi_context = Context( + paths=copy_to_temp_path("examples/sushi"), + config="table_only_naming_config", + ) + + assert sushi_context.config.physical_table_naming_convention == TableNamingConvention.TABLE_ONLY + sushi_context.plan(auto_apply=True) + + adapter = sushi_context.engine_adapter + + snapshot_tables = [ + dict(catalog=str(r[0]), schema=str(r[1]), table=str(r[2])) + for r in adapter.fetchall( + "select table_catalog, table_schema, table_name from information_schema.tables where table_type='BASE TABLE'" + ) + ] + + assert all([not t["table"].startswith("sushi") for t in snapshot_tables]) + + prod_env = sushi_context.state_reader.get_environment("prod") + assert prod_env + + prod_env_snapshots = sushi_context.state_reader.get_snapshots(prod_env.snapshots) + + assert all( + s.table_naming_convention == TableNamingConvention.TABLE_ONLY + for s in prod_env_snapshots.values() + ) + + +def test_physical_table_naming_strategy_hash_md5(copy_to_temp_path: t.Callable): + sushi_context = Context( + paths=copy_to_temp_path("examples/sushi"), + config="hash_md5_naming_config", + ) + + assert sushi_context.config.physical_table_naming_convention == TableNamingConvention.HASH_MD5 + sushi_context.plan(auto_apply=True) + + adapter = sushi_context.engine_adapter + + snapshot_tables = [ + dict(catalog=str(r[0]), schema=str(r[1]), table=str(r[2])) + for r in adapter.fetchall( + "select table_catalog, table_schema, table_name from information_schema.tables where table_type='BASE TABLE'" + ) + ] + + assert all([not t["table"].startswith("sushi") for t in snapshot_tables]) + assert all([t["table"].startswith("sqlmesh_md5") for t in snapshot_tables]) + + prod_env = sushi_context.state_reader.get_environment("prod") + assert prod_env + + prod_env_snapshots = sushi_context.state_reader.get_snapshots(prod_env.snapshots) + + assert all( + s.table_naming_convention == TableNamingConvention.HASH_MD5 + for s in prod_env_snapshots.values() + ) + + +def test_environment_suffix_target_table(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context( + "examples/sushi", config="environment_suffix_table_config" + ) + context.apply(plan) + metadata = DuckDBMetadata.from_context(context) + environments_schemas = {"sushi"} + internal_schemas = {"sqlmesh", "sqlmesh__sushi"} + starting_schemas = environments_schemas | internal_schemas + # Make sure no new schemas are created + assert set(metadata.schemas) - starting_schemas == {"raw"} + prod_views = {x for x in metadata.qualified_views if x.db in environments_schemas} + # Make sure that all models are present + assert len(prod_views) == 16 + apply_to_environment(context, "dev") + # Make sure no new schemas are created + assert set(metadata.schemas) - starting_schemas == {"raw"} + dev_views = { + x for x in metadata.qualified_views if x.db in environments_schemas and "__dev" in x.name + } + # Make sure that there is a view with `__dev` for each view that exists in prod + assert len(dev_views) == len(prod_views) + assert {x.name.replace("__dev", "") for x in dev_views} - {x.name for x in prod_views} == set() + context.invalidate_environment("dev") + context._run_janitor() + views_after_janitor = metadata.qualified_views + # Make sure that the number of views after the janitor is the same as when you subtract away dev views + assert len(views_after_janitor) == len( + {x.sql(dialect="duckdb") for x in views_after_janitor} + - {x.sql(dialect="duckdb") for x in dev_views} + ) + # Double check there are no dev views + assert len({x for x in views_after_janitor if "__dev" in x.name}) == 0 + # Make sure prod views were not removed + assert {x.sql(dialect="duckdb") for x in prod_views} - { + x.sql(dialect="duckdb") for x in views_after_janitor + } == set() + + +def test_environment_suffix_target_catalog(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + monkeypatch.chdir(tmp_path) + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(catalogs={"main_warehouse": ":memory:"}), + environment_suffix_target=EnvironmentSuffixTarget.CATALOG, + ) + + assert config.default_connection + + models_dir = tmp_path / "models" + models_dir.mkdir() + + (models_dir / "model.sql").write_text(""" + MODEL ( + name example_schema.test_model, + kind FULL + ); + + SELECT '1' as a""") + + (models_dir / "fqn_model.sql").write_text(""" + MODEL ( + name memory.example_fqn_schema.test_model_fqn, + kind FULL + ); + + SELECT '1' as a""") + + ctx = Context(config=config, paths=tmp_path) + + metadata = DuckDBMetadata.from_context(ctx) + assert ctx.default_catalog == "main_warehouse" + assert metadata.catalogs == {"main_warehouse", "memory"} + + ctx.plan(auto_apply=True) + + # prod should go to the default catalog and not be overridden to a catalog called 'prod' + assert ( + ctx.engine_adapter.fetchone("select * from main_warehouse.example_schema.test_model")[0] # type: ignore + == "1" + ) + assert ( + ctx.engine_adapter.fetchone("select * from memory.example_fqn_schema.test_model_fqn")[0] # type: ignore + == "1" + ) + assert metadata.catalogs == {"main_warehouse", "memory"} + assert metadata.schemas_in_catalog("main_warehouse") == [ + "example_schema", + "sqlmesh__example_schema", + ] + assert metadata.schemas_in_catalog("memory") == [ + "example_fqn_schema", + "sqlmesh__example_fqn_schema", + ] + + # dev should be overridden to go to a catalogs called 'main_warehouse__dev' and 'memory__dev' + ctx.plan(environment="dev", include_unmodified=True, auto_apply=True) + assert ( + ctx.engine_adapter.fetchone("select * from main_warehouse__dev.example_schema.test_model")[ + 0 + ] # type: ignore + == "1" + ) + assert ( + ctx.engine_adapter.fetchone("select * from memory__dev.example_fqn_schema.test_model_fqn")[ + 0 + ] # type: ignore + == "1" + ) + assert metadata.catalogs == {"main_warehouse", "main_warehouse__dev", "memory", "memory__dev"} + + # schemas in dev envs should match prod and not have a suffix + assert metadata.schemas_in_catalog("main_warehouse") == [ + "example_schema", + "sqlmesh__example_schema", + ] + assert metadata.schemas_in_catalog("main_warehouse__dev") == ["example_schema"] + assert metadata.schemas_in_catalog("memory") == [ + "example_fqn_schema", + "sqlmesh__example_fqn_schema", + ] + assert metadata.schemas_in_catalog("memory__dev") == ["example_fqn_schema"] + + ctx.invalidate_environment("dev", sync=True) + + # dev catalogs cleaned up + assert metadata.catalogs == {"main_warehouse", "memory"} + + # prod catalogs still contain physical layer and views still work + assert metadata.schemas_in_catalog("main_warehouse") == [ + "example_schema", + "sqlmesh__example_schema", + ] + assert metadata.schemas_in_catalog("memory") == [ + "example_fqn_schema", + "sqlmesh__example_fqn_schema", + ] + + assert ( + ctx.engine_adapter.fetchone("select * from main_warehouse.example_schema.test_model")[0] # type: ignore + == "1" + ) + assert ( + ctx.engine_adapter.fetchone("select * from memory.example_fqn_schema.test_model_fqn")[0] # type: ignore + == "1" + ) + + +def test_environment_catalog_mapping(init_and_plan_context: t.Callable): + environments_schemas = {"raw", "sushi"} + + def get_prod_dev_views(metadata: DuckDBMetadata) -> t.Tuple[t.Set[exp.Table], t.Set[exp.Table]]: + views = metadata.qualified_views + prod_views = { + x for x in views if x.catalog == "prod_catalog" if x.db in environments_schemas + } + dev_views = {x for x in views if x.catalog == "dev_catalog" if x.db in environments_schemas} + return prod_views, dev_views + + def get_default_catalog_and_non_tables( + metadata: DuckDBMetadata, default_catalog: t.Optional[str] + ) -> t.Tuple[t.Set[exp.Table], t.Set[exp.Table]]: + tables = metadata.qualified_tables + user_default_tables = { + x for x in tables if x.catalog == default_catalog and x.db != "sqlmesh" + } + non_default_tables = {x for x in tables if x.catalog != default_catalog} + return user_default_tables, non_default_tables + + context, plan = init_and_plan_context( + "examples/sushi", config="environment_catalog_mapping_config" + ) + context.apply(plan) + metadata = DuckDBMetadata(context.engine_adapter) + state_metadata = DuckDBMetadata.from_context(context.state_sync.state_sync) + prod_views, dev_views = get_prod_dev_views(metadata) + ( + user_default_tables, + non_default_tables, + ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) + assert len(prod_views) == 16 + assert len(dev_views) == 0 + assert len(user_default_tables) == 15 + assert state_metadata.schemas == ["sqlmesh"] + assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( + { + "physical.sqlmesh._environments", + "physical.sqlmesh._intervals", + "physical.sqlmesh._snapshots", + "physical.sqlmesh._versions", + } + ) + apply_to_environment(context, "dev") + prod_views, dev_views = get_prod_dev_views(metadata) + ( + user_default_tables, + non_default_tables, + ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) + assert len(prod_views) == 16 + assert len(dev_views) == 16 + assert len(user_default_tables) == 16 + assert len(non_default_tables) == 0 + assert state_metadata.schemas == ["sqlmesh"] + assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( + { + "physical.sqlmesh._environments", + "physical.sqlmesh._intervals", + "physical.sqlmesh._snapshots", + "physical.sqlmesh._versions", + } + ) + apply_to_environment(context, "prodnot") + prod_views, dev_views = get_prod_dev_views(metadata) + ( + user_default_tables, + non_default_tables, + ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) + assert len(prod_views) == 16 + assert len(dev_views) == 32 + assert len(user_default_tables) == 16 + assert len(non_default_tables) == 0 + assert state_metadata.schemas == ["sqlmesh"] + assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( + { + "physical.sqlmesh._environments", + "physical.sqlmesh._intervals", + "physical.sqlmesh._snapshots", + "physical.sqlmesh._versions", + } + ) + context.invalidate_environment("dev") + context._run_janitor() + prod_views, dev_views = get_prod_dev_views(metadata) + ( + user_default_tables, + non_default_tables, + ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) + assert len(prod_views) == 16 + assert len(dev_views) == 16 + assert len(user_default_tables) == 16 + assert len(non_default_tables) == 0 + assert state_metadata.schemas == ["sqlmesh"] + assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( + { + "physical.sqlmesh._environments", + "physical.sqlmesh._intervals", + "physical.sqlmesh._snapshots", + "physical.sqlmesh._versions", + } + ) + + +@use_terminal_console +def test_plan_always_recreate_environment(tmp_path: Path): + def plan_with_output(ctx: Context, environment: str): + with patch.object(logger, "info") as mock_logger: + with capture_output() as output: + ctx.load() + ctx.plan(environment, no_prompts=True, auto_apply=True) + + # Facade logs info "Promoting environment {environment}" + assert mock_logger.call_args[0][1] == environment + + return output + + models_dir = tmp_path / "models" + + logger = logging.getLogger("sqlmesh.core.state_sync.db.facade") + + create_temp_file( + tmp_path, models_dir / "a.sql", "MODEL (name test.a, kind FULL); SELECT 1 AS col" + ) + + config = Config(plan=PlanConfig(always_recreate_environment=True)) + ctx = Context(paths=[tmp_path], config=config) + + # Case 1: Neither prod nor dev exists, so dev is initialized + output = plan_with_output(ctx, "dev") + + assert """`dev` environment will be initialized""" in output.stdout + + # Case 2: Prod does not exist, so dev is updated + create_temp_file( + tmp_path, models_dir / "a.sql", "MODEL (name test.a, kind FULL); SELECT 5 AS col" + ) + + output = plan_with_output(ctx, "dev") + assert "`dev` environment will be initialized" in output.stdout + + # Case 3: Prod is initialized, so plan comparisons moving forward should be against prod + output = plan_with_output(ctx, "prod") + assert "`prod` environment will be initialized" in output.stdout + + # Case 4: Dev is updated with a breaking change. Prod exists now so plan comparisons moving forward should be against prod + create_temp_file( + tmp_path, models_dir / "a.sql", "MODEL (name test.a, kind FULL); SELECT 10 AS col" + ) + ctx.load() + + plan = ctx.plan_builder("dev").build() + + assert ( + next(iter(plan.context_diff.snapshots.values())).change_category + == SnapshotChangeCategory.BREAKING + ) + + output = plan_with_output(ctx, "dev") + assert "New environment `dev` will be created from `prod`" in output.stdout + assert "Differences from the `prod` environment" in output.stdout + + # Case 5: Dev is updated with a metadata change, but comparison against prod shows both the previous and the current changes + # so it's still classified as a breaking change + create_temp_file( + tmp_path, + models_dir / "a.sql", + "MODEL (name test.a, kind FULL, owner 'test'); SELECT 10 AS col", + ) + ctx.load() + + plan = ctx.plan_builder("dev").build() + + assert ( + next(iter(plan.context_diff.snapshots.values())).change_category + == SnapshotChangeCategory.BREAKING + ) + + output = plan_with_output(ctx, "dev") + assert "New environment `dev` will be created from `prod`" in output.stdout + assert "Differences from the `prod` environment" in output.stdout + + stdout_rstrip = "\n".join([line.rstrip() for line in output.stdout.split("\n")]) + assert ( + """MODEL ( + name test.a, ++ owner test, + kind FULL + ) + SELECT +- 5 AS col ++ 10 AS col""" + in stdout_rstrip + ) + + # Case 6: Ensure that target environment and create_from environment are not the same + output = plan_with_output(ctx, "prod") + assert not "New environment `prod` will be created from `prod`" in output.stdout + + # Case 7: Check that we can still run Context::diff() against any environment + for environment in ["dev", "prod"]: + context_diff = ctx._context_diff(environment) + assert context_diff.environment == environment + + +def test_before_all_after_all_execution_order(tmp_path: Path, mocker: MockerFixture): + model = """ + MODEL ( + name test_schema.model_that_depends_on_before_all, + kind FULL, + ); + + SELECT id, value FROM before_all_created_table + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + with open(models_dir / "model.sql", "w") as f: + f.write(model) + + # before_all statement that creates a table that the above model depends on + before_all_statement = ( + "CREATE TABLE IF NOT EXISTS before_all_created_table AS SELECT 1 AS id, 'test' AS value" + ) + + # after_all that depends on the model + after_all_statement = "CREATE TABLE IF NOT EXISTS after_all_created_table AS SELECT id, value FROM test_schema.model_that_depends_on_before_all" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + before_all=[before_all_statement], + after_all=[after_all_statement], + ) + + execute_calls: t.List[str] = [] + + original_duckdb_execute = DuckDBEngineAdapter.execute + + def track_duckdb_execute(self, expression, **kwargs): + sql = expression if isinstance(expression, str) else expression.sql(dialect="duckdb") + state_tables = [ + "_snapshots", + "_environments", + "_versions", + "_intervals", + "_auto_restatements", + "_environment_statements", + ] + + # to ignore the state queries + if not any(table in sql.lower() for table in state_tables): + execute_calls.append(sql) + + return original_duckdb_execute(self, expression, **kwargs) + + ctx = Context(paths=[tmp_path], config=config) + + # the plan would fail if the execution order ever changes and before_all statements dont execute first + ctx.plan(auto_apply=True, no_prompts=True) + + mocker.patch.object(DuckDBEngineAdapter, "execute", track_duckdb_execute) + + # run with the patched execute + ctx.run("prod", start="2023-01-01", end="2023-01-02") + + # validate explicitly that the first execute is for the before_all + assert "before_all_created_table" in execute_calls[0] + + # and that the last is the sole after all that depends on the model + assert "after_all_created_table" in execute_calls[-1] + + +def test_auto_categorization(sushi_context: Context): + environment = "dev" + for config in sushi_context.configs.values(): + config.plan.auto_categorize_changes.sql = AutoCategorizationMode.FULL + initial_add(sushi_context, environment) + + version = sushi_context.get_snapshot( + "sushi.waiter_as_customer_by_day", raise_if_missing=True + ).version + fingerprint = sushi_context.get_snapshot( + "sushi.waiter_as_customer_by_day", raise_if_missing=True + ).fingerprint + + model = t.cast(SqlModel, sushi_context.get_model("sushi.customers", raise_if_missing=True)) + sushi_context.upsert_model( + "sushi.customers", + query_=ParsableSql(sql=model.query.select("'foo' AS foo").sql(dialect=model.dialect)), # type: ignore + ) + apply_to_environment(sushi_context, environment) + + assert ( + sushi_context.get_snapshot( + "sushi.waiter_as_customer_by_day", raise_if_missing=True + ).change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert ( + sushi_context.get_snapshot( + "sushi.waiter_as_customer_by_day", raise_if_missing=True + ).fingerprint + != fingerprint + ) + assert ( + sushi_context.get_snapshot("sushi.waiter_as_customer_by_day", raise_if_missing=True).version + == version + ) diff --git a/tests/core/integration/test_cron.py b/tests/core/integration/test_cron.py new file mode 100644 index 0000000000..fa327ac36f --- /dev/null +++ b/tests/core/integration/test_cron.py @@ -0,0 +1,247 @@ +from __future__ import annotations + +import typing as t +import pytest +import time_machine + +from sqlmesh.core import dialect as d +from sqlmesh.core.model import ( + SqlModel, + load_sql_based_model, +) +from sqlmesh.core.plan import SnapshotIntervals +from sqlmesh.utils.date import to_timestamp +from tests.core.integration.utils import add_projection_to_model + +pytestmark = pytest.mark.slow + + +@time_machine.travel("2023-01-08 00:00:00 UTC") +@pytest.mark.parametrize( + "forward_only, expected_intervals", + [ + ( + False, + [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + ], + ), + ( + True, + [ + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + ], + ), + ], +) +def test_cron_not_aligned_with_day_boundary( + init_and_plan_context: t.Callable, + forward_only: bool, + expected_intervals: t.List[t.Tuple[int, int]], +): + context, plan = init_and_plan_context("examples/sushi") + + model = context.get_model("sushi.waiter_revenue_by_day") + model = SqlModel.parse_obj( + { + **model.dict(), + "kind": model.kind.copy(update={"forward_only": forward_only}), + "cron": "0 12 * * *", + } + ) + context.upsert_model(model) + + plan = context.plan_builder("prod", skip_tests=True).build() + context.apply(plan) + + waiter_revenue_by_day_snapshot = context.get_snapshot(model.name, raise_if_missing=True) + assert waiter_revenue_by_day_snapshot.intervals == [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-07")) + ] + + model = add_projection_to_model(t.cast(SqlModel, model), literal=True) + context.upsert_model(model) + + waiter_revenue_by_day_snapshot = context.get_snapshot( + "sushi.waiter_revenue_by_day", raise_if_missing=True + ) + + with time_machine.travel("2023-01-08 00:10:00 UTC"): # Past model's cron. + plan = context.plan_builder( + "dev", select_models=[model.name], skip_tests=True, enable_preview=True + ).build() + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, + intervals=expected_intervals, + ), + ] + + +@time_machine.travel("2023-01-08 00:00:00 UTC") +def test_cron_not_aligned_with_day_boundary_new_model(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + existing_model = context.get_model("sushi.waiter_revenue_by_day") + existing_model = SqlModel.parse_obj( + { + **existing_model.dict(), + "kind": existing_model.kind.copy(update={"forward_only": True}), + } + ) + context.upsert_model(existing_model) + + plan = context.plan_builder("prod", skip_tests=True).build() + context.apply(plan) + + # Add a new model and make a change to a forward-only model. + # The cron of the new model is not aligned with the day boundary. + new_model = load_sql_based_model( + d.parse( + """ + MODEL ( + name memory.sushi.new_model, + kind FULL, + cron '0 8 * * *', + start '2023-01-01', + ); + + SELECT 1 AS one; + """ + ) + ) + context.upsert_model(new_model) + + existing_model = add_projection_to_model(t.cast(SqlModel, existing_model), literal=True) + context.upsert_model(existing_model) + + plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=context.get_snapshot( + "memory.sushi.new_model", raise_if_missing=True + ).snapshot_id, + intervals=[(to_timestamp("2023-01-06"), to_timestamp("2023-01-07"))], + ), + SnapshotIntervals( + snapshot_id=context.get_snapshot( + "sushi.waiter_revenue_by_day", raise_if_missing=True + ).snapshot_id, + intervals=[ + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + +@time_machine.travel("2023-01-08 00:00:00 UTC", tick=False) +def test_parent_cron_after_child(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + + model = context.get_model("sushi.waiter_revenue_by_day") + model = SqlModel.parse_obj( + { + **model.dict(), + "cron": "50 23 * * *", + } + ) + context.upsert_model(model) + + plan = context.plan_builder("prod", skip_tests=True).build() + context.apply(plan) + + waiter_revenue_by_day_snapshot = context.get_snapshot(model.name, raise_if_missing=True) + assert waiter_revenue_by_day_snapshot.intervals == [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-07")) + ] + + top_waiters_model = context.get_model("sushi.top_waiters") + top_waiters_model = add_projection_to_model(t.cast(SqlModel, top_waiters_model), literal=True) + context.upsert_model(top_waiters_model) + + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + with time_machine.travel("2023-01-08 23:55:00 UTC"): # Past parent's cron, but before child's + plan = context.plan_builder("dev", skip_tests=True).build() + # Make sure the waiter_revenue_by_day model is not backfilled. + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + +@time_machine.travel("2025-03-08 00:00:00 UTC") +def test_tz(init_and_plan_context): + context, _ = init_and_plan_context("examples/sushi") + + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model( + SqlModel.parse_obj( + {**model.dict(), "cron_tz": "America/Los_Angeles", "start": "2025-03-07"} + ) + ) + + def assert_intervals(plan, intervals): + assert ( + next( + intervals.intervals + for intervals in plan.missing_intervals + if intervals.snapshot_id.name == model.fqn + ) + == intervals + ) + + plan = context.plan_builder("prod", skip_tests=True).build() + + # we have missing intervals but not waiter_revenue_by_day because it's not midnight pacific yet + assert plan.missing_intervals + + with pytest.raises(StopIteration): + assert_intervals(plan, []) + + # now we're ready 8AM UTC == midnight PST + with time_machine.travel("2025-03-08 08:00:00 UTC"): + plan = context.plan_builder("prod", skip_tests=True).build() + assert_intervals(plan, [(to_timestamp("2025-03-07"), to_timestamp("2025-03-08"))]) + + with time_machine.travel("2025-03-09 07:00:00 UTC"): + plan = context.plan_builder("prod", skip_tests=True).build() + + assert_intervals( + plan, + [ + (to_timestamp("2025-03-07"), to_timestamp("2025-03-08")), + ], + ) + + with time_machine.travel("2025-03-09 08:00:00 UTC"): + plan = context.plan_builder("prod", skip_tests=True).build() + + assert_intervals( + plan, + [ + (to_timestamp("2025-03-07"), to_timestamp("2025-03-08")), + (to_timestamp("2025-03-08"), to_timestamp("2025-03-09")), + ], + ) + + context.apply(plan) + + plan = context.plan_builder("prod", skip_tests=True).build() + assert not plan.missing_intervals diff --git a/tests/core/integration/test_dbt.py b/tests/core/integration/test_dbt.py new file mode 100644 index 0000000000..5e600899dd --- /dev/null +++ b/tests/core/integration/test_dbt.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import typing as t +import pytest +from sqlmesh.core.model.common import ParsableSql +import time_machine + +from sqlmesh.core.context import Context +from sqlmesh.core.model import ( + IncrementalUnmanagedKind, +) +from sqlmesh.core.snapshot import ( + DeployabilityIndex, + SnapshotChangeCategory, +) + +if t.TYPE_CHECKING: + pass + +pytestmark = pytest.mark.slow + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_dbt_select_star_is_directly_modified(sushi_test_dbt_context: Context): + context = sushi_test_dbt_context + + model = context.get_model("sushi.simple_model_a") + context.upsert_model( + model, + query_=ParsableSql(sql="SELECT 1 AS a, 2 AS b"), + ) + + snapshot_a_id = context.get_snapshot("sushi.simple_model_a").snapshot_id # type: ignore + snapshot_b_id = context.get_snapshot("sushi.simple_model_b").snapshot_id # type: ignore + + plan = context.plan_builder("dev", skip_tests=True).build() + assert plan.directly_modified == {snapshot_a_id, snapshot_b_id} + assert {i.snapshot_id for i in plan.missing_intervals} == {snapshot_a_id, snapshot_b_id} + + assert plan.snapshots[snapshot_a_id].change_category == SnapshotChangeCategory.NON_BREAKING + assert plan.snapshots[snapshot_b_id].change_category == SnapshotChangeCategory.NON_BREAKING + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_dbt_is_incremental_table_is_missing(sushi_test_dbt_context: Context): + context = sushi_test_dbt_context + + model = context.get_model("sushi.waiter_revenue_by_day_v2") + model = model.copy(update={"kind": IncrementalUnmanagedKind(), "start": "2023-01-01"}) + context.upsert_model(model) + context._standalone_audits["test_top_waiters"].start = "2023-01-01" + + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + + snapshot = context.get_snapshot("sushi.waiter_revenue_by_day_v2") + assert snapshot + + # Manually drop the table + context.engine_adapter.drop_table(snapshot.table_name()) + + context.snapshot_evaluator.evaluate( + snapshot, + start="2023-01-01", + end="2023-01-08", + execution_time="2023-01-08 15:00:00", + snapshots={s.name: s for s in context.snapshots.values()}, + deployability_index=DeployabilityIndex.all_deployable(), + ) + + # Make sure the table was recreated + assert context.engine_adapter.table_exists(snapshot.table_name()) + + +def test_model_attr(sushi_test_dbt_context: Context, assert_exp_eq): + context = sushi_test_dbt_context + model = context.get_model("sushi.top_waiters") + assert_exp_eq( + model.render_query(), + """ + SELECT + CAST("waiter_id" AS INT) AS "waiter_id", + CAST("revenue" AS DOUBLE) AS "revenue", + 3 AS "model_columns" + FROM "memory"."sushi"."waiter_revenue_by_day_v2" AS "waiter_revenue_by_day_v2" + WHERE + "ds" = ( + SELECT + MAX("ds") + FROM "memory"."sushi"."waiter_revenue_by_day_v2" AS "waiter_revenue_by_day_v2" + ) + ORDER BY + "revenue" DESC NULLS FIRST + LIMIT 10 + """, + ) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_dbt_requirements(sushi_dbt_context: Context): + assert set(sushi_dbt_context.requirements) == {"dbt-core", "dbt-duckdb"} + assert sushi_dbt_context.requirements["dbt-core"].startswith("1.") + assert sushi_dbt_context.requirements["dbt-duckdb"].startswith("1.") + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_dbt_dialect_with_normalization_strategy(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context( + "tests/fixtures/dbt/sushi_test", config="test_config_with_normalization_strategy" + ) + assert context.default_dialect == "duckdb,normalization_strategy=LOWERCASE" + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_dbt_before_all_with_var_ref_source(init_and_plan_context: t.Callable): + _, plan = init_and_plan_context( + "tests/fixtures/dbt/sushi_test", config="test_config_with_normalization_strategy" + ) + environment_statements = plan.to_evaluatable().environment_statements + assert environment_statements + rendered_statements = [e.render_before_all(dialect="duckdb") for e in environment_statements] + assert rendered_statements[0] == [ + "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table TEXT, evaluation_time TEXT)", + "CREATE TABLE IF NOT EXISTS to_be_executed_last (col TEXT)", + "SELECT 1 AS var, 'items' AS src, 'waiters' AS ref", + ] diff --git a/tests/core/integration/test_dev_only_vde.py b/tests/core/integration/test_dev_only_vde.py new file mode 100644 index 0000000000..611e207771 --- /dev/null +++ b/tests/core/integration/test_dev_only_vde.py @@ -0,0 +1,477 @@ +from __future__ import annotations + +import typing as t +import pytest +from sqlmesh.core.model.common import ParsableSql +import time_machine + +from sqlmesh.core import dialect as d +from sqlmesh.core.config.common import VirtualEnvironmentMode +from sqlmesh.core.model import ( + FullKind, + IncrementalUnmanagedKind, + SqlModel, + ViewKind, + load_sql_based_model, +) +from sqlmesh.core.plan import SnapshotIntervals +from sqlmesh.core.snapshot import ( + SnapshotChangeCategory, +) +from sqlmesh.utils.date import to_date, to_timestamp +from tests.core.integration.utils import add_projection_to_model + +pytestmark = pytest.mark.slow + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + + assert all( + s.virtual_environment_mode.is_dev_only or not s.is_model or s.is_symbolic + for s in context.snapshots.values() + ) + + # Init prod + context.plan("prod", auto_apply=True, no_prompts=True) + + # Make a change in dev + original_model = context.get_model("sushi.waiter_revenue_by_day") + original_fingerprint = context.get_snapshot(original_model.name).fingerprint + model = original_model.copy( + update={ + "query_": ParsableSql( + sql=original_model.query.order_by("waiter_id").sql(dialect=original_model.dialect) + ) + } + ) + model = add_projection_to_model(t.cast(SqlModel, model)) + context.upsert_model(model) + + plan_dev = context.plan_builder("dev").build() + assert to_timestamp(plan_dev.start) == to_timestamp("2023-01-07") + assert plan_dev.requires_backfill + assert plan_dev.missing_intervals == [ + SnapshotIntervals( + snapshot_id=context.get_snapshot("sushi.top_waiters").snapshot_id, + intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], + ), + SnapshotIntervals( + snapshot_id=context.get_snapshot("sushi.waiter_revenue_by_day").snapshot_id, + intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], + ), + ] + assert plan_dev.context_diff.snapshots[context.get_snapshot(model.name).snapshot_id].intervals + assert plan_dev.context_diff.snapshots[ + context.get_snapshot("sushi.top_waiters").snapshot_id + ].intervals + assert plan_dev.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].dev_intervals + assert plan_dev.context_diff.snapshots[ + context.get_snapshot("sushi.top_waiters").snapshot_id + ].dev_intervals + context.apply(plan_dev) + + # Make sure the waiter_revenue_by_day model is a table in prod and a view in dev + table_types_df = context.engine_adapter.fetchdf( + "SELECT table_schema, table_type FROM INFORMATION_SCHEMA.TABLES WHERE table_name = 'waiter_revenue_by_day'" + ) + assert table_types_df.to_dict("records") == [ + {"table_schema": "sushi", "table_type": "BASE TABLE"}, + {"table_schema": "sushi__dev", "table_type": "VIEW"}, + ] + + # Check that the specified dates were backfilled + min_event_date = context.engine_adapter.fetchone( + "SELECT MIN(event_date) FROM sushi__dev.waiter_revenue_by_day" + )[0] + assert min_event_date == to_date("2023-01-07") + + # Make sure the changes are applied without backfill in prod + plan_prod = context.plan_builder("prod").build() + assert not plan_prod.requires_backfill + assert not plan_prod.missing_intervals + context.apply(plan_prod) + assert "one" in context.engine_adapter.columns("sushi.waiter_revenue_by_day") + + # Make sure the revert of a breaking changes results in a full rebuild + context.upsert_model(original_model) + assert context.get_snapshot(original_model.name).fingerprint == original_fingerprint + + plan_prod = context.plan_builder( + "prod", allow_destructive_models=["sushi.waiter_revenue_by_day"] + ).build() + assert not plan_prod.requires_backfill + assert not plan_prod.missing_intervals + context.apply(plan_prod) + assert "one" not in context.engine_adapter.columns("sushi.waiter_revenue_by_day") + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_kind_change(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.apply(plan) + + # Change to full kind + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": FullKind()}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.missing_intervals + assert prod_plan.requires_backfill + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "table" + + # Change back to view + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": ViewKind()}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "view" + + # Change to incremental + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": IncrementalUnmanagedKind()}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "table" + + # Change back to full + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": FullKind()}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "table" + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_kind_change_incremental( + init_and_plan_context: t.Callable, +): + context, _ = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + + forward_only_model_name = "memory.sushi.test_forward_only_model" + forward_only_model_expressions = d.parse( + f""" + MODEL ( + name {forward_only_model_name}, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + ), + ); + + SELECT '2023-01-01' AS ds, 'value' AS value; + """ + ) + forward_only_model = load_sql_based_model(forward_only_model_expressions) + forward_only_model = forward_only_model.copy( + update={"virtual_environment_mode": VirtualEnvironmentMode.DEV_ONLY} + ) + context.upsert_model(forward_only_model) + + context.plan("prod", auto_apply=True, no_prompts=True) + + # Change to view + model = context.get_model(forward_only_model_name) + original_kind = model.kind + model = model.copy(update={"kind": ViewKind()}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"test_forward_only_model"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "view" + + model = model.copy(update={"kind": original_kind}) + context.upsert_model(model) + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"test_forward_only_model"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "table" + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_kind_change_with_follow_up_changes_in_dev( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.apply(plan) + + # Make sure the initial state is a view + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "view" + + # Change to incremental unmanaged kind + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": IncrementalUnmanagedKind()}) + context.upsert_model(model) + dev_plan = context.plan_builder("dev", skip_tests=True).build() + assert dev_plan.missing_intervals + assert dev_plan.requires_backfill + context.apply(dev_plan) + + # Make a follow-up forward-only change + model = add_projection_to_model(t.cast(SqlModel, model)) + context.upsert_model(model) + dev_plan = context.plan_builder("dev", skip_tests=True, forward_only=True).build() + context.apply(dev_plan) + + # Deploy to prod + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals + assert not prod_plan.context_diff.snapshots[ + context.get_snapshot(model.name).snapshot_id + ].intervals + context.apply(prod_plan) + data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) + assert len(data_objects) == 1 + assert data_objects[0].type == "table" + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_kind_change_manual_categorization( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.apply(plan) + + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": FullKind()}) + context.upsert_model(model) + dev_plan_builder = context.plan_builder("dev", skip_tests=True, no_auto_categorization=True) + dev_plan_builder.set_choice( + dev_plan_builder._context_diff.snapshots[context.get_snapshot(model.name).snapshot_id], + SnapshotChangeCategory.NON_BREAKING, + ) + dev_plan = dev_plan_builder.build() + assert dev_plan.requires_backfill + assert len(dev_plan.missing_intervals) == 1 + context.apply(dev_plan) + + prod_plan = context.plan_builder("prod", skip_tests=True).build() + assert prod_plan.requires_backfill + assert prod_plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=context.get_snapshot("sushi.top_waiters").snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_seed_model_change( + init_and_plan_context: t.Callable, +): + context, _ = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.load() + context.plan("prod", auto_apply=True, no_prompts=True) + + seed_model = context.get_model("sushi.waiter_names") + with open(seed_model.seed_path, "a") as fd: + fd.write("\n123,New Test Name") + + context.load() + seed_model_snapshot = context.get_snapshot("sushi.waiter_names") + plan = context.plan_builder("dev").build() + assert plan.directly_modified == {seed_model_snapshot.snapshot_id} + assert len(plan.missing_intervals) == 2 + context.apply(plan) + + actual_seed_df_in_dev = context.fetchdf("SELECT * FROM sushi__dev.waiter_names WHERE id = 123") + assert actual_seed_df_in_dev.to_dict("records") == [{"id": 123, "name": "New Test Name"}] + actual_seed_df_in_prod = context.fetchdf("SELECT * FROM sushi.waiter_names WHERE id = 123") + assert actual_seed_df_in_prod.empty + + plan = context.plan_builder("prod").build() + assert plan.directly_modified == {seed_model_snapshot.snapshot_id} + assert len(plan.missing_intervals) == 1 + assert plan.missing_intervals[0].snapshot_id == seed_model_snapshot.snapshot_id + context.apply(plan) + + actual_seed_df_in_prod = context.fetchdf("SELECT * FROM sushi.waiter_names WHERE id = 123") + assert actual_seed_df_in_prod.to_dict("records") == [{"id": 123, "name": "New Test Name"}] + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_change_downstream_of_seed( + init_and_plan_context: t.Callable, +): + """This test covers a scenario when a model downstream of a seed model is modified and explicitly selected + causing an (unhydrated) seed model to sourced from the state. If SQLMesh attempts to create + a table for the unchanged seed model, it will fail because the seed model is not hydrated. + """ + context, _ = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.load() + context.plan("prod", auto_apply=True, no_prompts=True) + + # Make sure that a different version of the seed model is loaded + seed_model = context.get_model("sushi.waiter_names") + seed_model = seed_model.copy(update={"stamp": "force new version"}) + context.upsert_model(seed_model) + + # Make a change to the downstream model + model = context.get_model("sushi.waiter_as_customer_by_day") + model = model.copy(update={"stamp": "force new version"}) + context.upsert_model(model) + + # It is important to clear the cache so that the hydrated seed model is not sourced from the cache + context.clear_caches() + + # Make sure to use the selector so that the seed model is sourced from the state + plan = context.plan_builder("dev", select_models=[model.name]).build() + assert len(plan.directly_modified) == 1 + assert list(plan.directly_modified)[0].name == model.fqn + assert len(plan.missing_intervals) == 1 + assert plan.missing_intervals[0].snapshot_id.name == model.fqn + + # Make sure there's no error when applying the plan + context.apply(plan) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_model_change_standalone_audit( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.apply(plan) + + # Change a model upstream from a standalone audit + model = context.get_model("sushi.items") + model = model.copy(update={"stamp": "force new version"}) + context.upsert_model(model) + + plan = context.plan_builder("prod", skip_tests=True).build() + + # Make sure the standalone audit is among modified + assert ( + context.get_snapshot("assert_item_price_above_zero").snapshot_id + in plan.indirectly_modified[context.get_snapshot("sushi.items").snapshot_id] + ) + + # Make sure there's no error when applying the plan + context.apply(plan) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_virtual_environment_mode_dev_only_seed_model_change_schema( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context( + "examples/sushi", config="test_config_virtual_environment_mode_dev_only" + ) + context.apply(plan) + + new_csv = [] + with open(context.path / "seeds" / "waiter_names.csv", "r") as fd: + is_header = True + for idx, line in enumerate(fd): + line = line.strip() + if not line: + continue + if is_header: + new_csv.append(line + ",new_column") + is_header = False + else: + new_csv.append(line + f",v{idx}") + + with open(context.path / "seeds" / "waiter_names.csv", "w") as fd: + fd.write("\n".join(new_csv)) + + context.load() + + downstream_model = context.get_model("sushi.waiter_as_customer_by_day") + downstream_model_kind = downstream_model.kind.dict() + downstream_model_kwargs = { + **downstream_model.dict(), + "kind": { + **downstream_model_kind, + "on_destructive_change": "allow", + }, + "audits": [], + # Use the new column + "query": "SELECT '2023-01-07' AS event_date, new_column AS new_column FROM sushi.waiter_names", + } + context.upsert_model(SqlModel.parse_obj(downstream_model_kwargs)) + + context.plan("dev", auto_apply=True, no_prompts=True, skip_tests=True, enable_preview=True) + + assert ( + context.engine_adapter.fetchone( + "SELECT COUNT(*) FROM sushi__dev.waiter_as_customer_by_day" + )[0] + == len(new_csv) - 1 + ) + + # Deploy to prod + context.clear_caches() + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + assert "new_column" in context.engine_adapter.columns("sushi.waiter_as_customer_by_day") diff --git a/tests/core/integration/test_forward_only.py b/tests/core/integration/test_forward_only.py new file mode 100644 index 0000000000..4d61915305 --- /dev/null +++ b/tests/core/integration/test_forward_only.py @@ -0,0 +1,1510 @@ +from __future__ import annotations + +import typing as t +import numpy as np # noqa: TID253 +import pandas as pd # noqa: TID253 +import pytest +import time_machine +from pytest_mock.plugin import MockerFixture + +from sqlmesh.core import dialect as d +from sqlmesh.core.context import Context +from sqlmesh.core.config.categorizer import CategorizerConfig +from sqlmesh.core.model import ( + FullKind, + SqlModel, + load_sql_based_model, +) +from sqlmesh.core.plan import PlanBuilder, SnapshotIntervals +from sqlmesh.core.snapshot import ( + SnapshotChangeCategory, +) +from sqlmesh.utils.date import to_datetime, to_timestamp +from tests.core.integration.utils import add_projection_to_model + +pytestmark = pytest.mark.slow + + +@pytest.fixture(autouse=True) +def mock_choices(mocker: MockerFixture): + mocker.patch("sqlmesh.core.console.TerminalConsole._get_snapshot_change_category") + mocker.patch("sqlmesh.core.console.TerminalConsole._prompt_backfill") + + +def plan_choice(plan_builder: PlanBuilder, choice: SnapshotChangeCategory) -> None: + for snapshot in plan_builder.build().snapshots.values(): + if not snapshot.version: + plan_builder.set_choice(snapshot, choice) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +@pytest.mark.parametrize( + "context_fixture", + ["sushi_context", "sushi_no_default_catalog"], +) +def test_forward_only_plan_with_effective_date(context_fixture: Context, request): + context = request.getfixturevalue(context_fixture) + model_name = "sushi.waiter_revenue_by_day" + model = context.get_model(model_name) + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model)), start="2023-01-01") + snapshot = context.get_snapshot(model, raise_if_missing=True) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan_builder = context.plan_builder("dev", skip_tests=True, forward_only=True) + plan = plan_builder.build() + assert len(plan.new_snapshots) == 2 + assert ( + plan.context_diff.snapshots[snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only + assert plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].is_forward_only + + assert to_timestamp(plan.start) == to_timestamp("2023-01-07") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], + ), + ] + + plan = plan_builder.set_effective_from("2023-01-05").build() + # Default start should be set to effective_from + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + plan = plan_builder.set_start("2023-01-06").build() + # Start override should take precedence + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + plan = plan_builder.set_effective_from("2023-01-04").build() + # Start should remain unchanged + assert plan.start == "2023-01-06" + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + context.apply(plan) + + dev_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" + ) + assert dev_df["event_date"].tolist() == [ + pd.to_datetime("2023-01-06"), + pd.to_datetime("2023-01-07"), + ] + + prod_plan = context.plan_builder(skip_tests=True).build() + # Make sure that the previously set effective_from is respected + assert prod_plan.start == to_timestamp("2023-01-04") + assert prod_plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + context.apply(prod_plan) + + prod_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi.waiter_revenue_by_day WHERE one IS NOT NULL ORDER BY event_date" + ) + assert prod_df["event_date"].tolist() == [ + pd.to_datetime(x) for x in ["2023-01-04", "2023-01-05", "2023-01-06", "2023-01-07"] + ] + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_forward_only_model_regular_plan(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + model_name = "sushi.waiter_revenue_by_day" + + model = context.get_model(model_name) + model = add_projection_to_model(t.cast(SqlModel, model)) + forward_only_kind = model.kind.copy(update={"forward_only": True}) + model = model.copy(update={"kind": forward_only_kind}) + + context.upsert_model(model) + snapshot = context.get_snapshot(model, raise_if_missing=True) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() + assert len(plan.new_snapshots) == 2 + assert ( + plan.context_diff.snapshots[snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only + assert plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].is_forward_only + + assert plan.start == to_datetime("2023-01-01") + assert not plan.missing_intervals + + context.apply(plan) + + dev_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" + ) + assert not dev_df["event_date"].tolist() + + # Run a restatement plan to preview changes + plan_builder = context.plan_builder( + "dev", skip_tests=True, restate_models=[model_name], enable_preview=False + ) + plan_builder.set_start("2023-01-06") + assert plan_builder.build().missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + # Make sure that changed start is reflected in missing intervals + plan_builder.set_start("2023-01-07") + assert plan_builder.build().missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + context.apply(plan_builder.build()) + + dev_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" + ) + assert dev_df["event_date"].tolist() == [pd.to_datetime("2023-01-07")] + + # Promote changes to prod + prod_plan = context.plan_builder(skip_tests=True).build() + assert not prod_plan.missing_intervals + + context.apply(prod_plan) + + # The change was applied in a forward-only manner so no values in the new column should be populated + prod_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi.waiter_revenue_by_day WHERE one IS NOT NULL ORDER BY event_date" + ) + assert not prod_df["event_date"].tolist() + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_forward_only_model_regular_plan_preview_enabled(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + model_name = "sushi.waiter_revenue_by_day" + + model = context.get_model(model_name) + model = add_projection_to_model(t.cast(SqlModel, model)) + forward_only_kind = model.kind.copy(update={"forward_only": True}) + model = model.copy(update={"kind": forward_only_kind}) + + context.upsert_model(model) + snapshot = context.get_snapshot(model, raise_if_missing=True) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() + assert len(plan.new_snapshots) == 2 + assert ( + plan.context_diff.snapshots[snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only + assert plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].is_forward_only + + assert to_timestamp(plan.start) == to_timestamp("2023-01-07") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + context.apply(plan) + + dev_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" + ) + assert dev_df["event_date"].tolist() == [pd.to_datetime("2023-01-07")] + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_forward_only_model_restate_full_history_in_dev(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + model_name = "memory.sushi.customer_max_revenue" + expressions = d.parse( + f""" + MODEL ( + name {model_name}, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key customer_id, + forward_only true, + ), + ); + + SELECT + customer_id, MAX(revenue) AS max_revenue + FROM memory.sushi.customer_revenue_lifetime + GROUP BY 1; + """ + ) + + model = load_sql_based_model(expressions) + assert model.forward_only + assert model.kind.full_history_restatement_only + context.upsert_model(model) + + context.plan("prod", skip_tests=True, auto_apply=True, enable_preview=False) + + model_kwargs = { + **model.dict(), + # Make a breaking change. + "query": model.query.order_by("customer_id"), # type: ignore + } + context.upsert_model(SqlModel.parse_obj(model_kwargs)) + + # Apply the model change in dev + plan = context.plan_builder( + "dev", + skip_tests=True, + enable_preview=False, + categorizer_config=CategorizerConfig.all_full(), + ).build() + assert not plan.missing_intervals + context.apply(plan) + + snapshot = context.get_snapshot(model, raise_if_missing=True) + snapshot_table_name = snapshot.table_name(False) + + # Manually insert a dummy value to check that the table is recreated during the restatement + context.engine_adapter.insert_append( + snapshot_table_name, + pd.DataFrame({"customer_id": [-1], "max_revenue": [100]}), + ) + df = context.engine_adapter.fetchdf( + "SELECT COUNT(*) AS cnt FROM sushi__dev.customer_max_revenue WHERE customer_id = -1" + ) + assert df["cnt"][0] == 1 + + # Apply a restatement plan in dev + plan = context.plan("dev", restate_models=[model.name], auto_apply=True, enable_preview=False) + assert len(plan.missing_intervals) == 1 + + # Check that the dummy value is not present + df = context.engine_adapter.fetchdf( + "SELECT COUNT(*) AS cnt FROM sushi__dev.customer_max_revenue WHERE customer_id = -1" + ) + assert df["cnt"][0] == 0 + + # Check that the table is not empty + df = context.engine_adapter.fetchdf( + "SELECT COUNT(*) AS cnt FROM sushi__dev.customer_max_revenue" + ) + assert df["cnt"][0] > 0 + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_full_history_restatement_model_regular_plan_preview_enabled( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + model_name = "sushi.marketing" # SCD2 model + + model = context.get_model(model_name) + model = add_projection_to_model(t.cast(SqlModel, model)) + + context.upsert_model(model) + snapshot = context.get_snapshot(model, raise_if_missing=True) + customers_snapshot = context.get_snapshot("sushi.customers", raise_if_missing=True) + active_customers_snapshot = context.get_snapshot( + "sushi.active_customers", raise_if_missing=True + ) + waiter_as_customer_snapshot = context.get_snapshot( + "sushi.waiter_as_customer_by_day", raise_if_missing=True + ) + + plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() + + assert len(plan.new_snapshots) == 6 + assert ( + plan.context_diff.snapshots[snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[customers_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[active_customers_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[waiter_as_customer_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert all(s.is_forward_only for s in plan.new_snapshots) + + assert to_timestamp(plan.start) == to_timestamp("2023-01-07") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + context.apply(plan) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_metadata_changed_regular_plan_preview_enabled(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + model_name = "sushi.waiter_revenue_by_day" + + model = context.get_model(model_name) + model = model.copy(update={"owner": "new_owner"}) + + context.upsert_model(model) + snapshot = context.get_snapshot(model, raise_if_missing=True) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() + assert len(plan.new_snapshots) == 2 + assert ( + plan.context_diff.snapshots[snapshot.snapshot_id].change_category + == SnapshotChangeCategory.METADATA + ) + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.METADATA + ) + assert not plan.missing_intervals + assert not plan.restatements + + +@time_machine.travel("2023-01-08 00:00:00 UTC") +def test_forward_only_preview_child_that_runs_before_parent(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + # This model runs at minute 30 of every hour + upstream_model = load_sql_based_model( + d.parse( + """ + MODEL ( + name memory.sushi.upstream_model, + kind FULL, + cron '30 * * * *', + start '2023-01-01', + ); + + SELECT 1 AS a; + """ + ) + ) + context.upsert_model(upstream_model) + + # This model runs at minute 0 of every hour, so it runs before the upstream model + downstream_model = load_sql_based_model( + d.parse( + """ + MODEL ( + name memory.sushi.downstream_model, + kind INCREMENTAL_BY_TIME_RANGE( + time_column event_date, + forward_only True, + ), + cron '0 * * * *', + start '2023-01-01', + ); + + SELECT a, '2023-01-06' AS event_date FROM memory.sushi.upstream_model; + """ + ) + ) + context.upsert_model(downstream_model) + + context.plan("prod", skip_tests=True, auto_apply=True) + + with time_machine.travel("2023-01-08 00:05:00 UTC"): + # The downstream model runs but not the upstream model + context.run("prod") + + # Now it's time for the upstream model to run but it hasn't run yet + with time_machine.travel("2023-01-08 00:35:00 UTC"): + # Make a change to the downstream model. + downstream_model = add_projection_to_model(t.cast(SqlModel, downstream_model), literal=True) + context.upsert_model(downstream_model) + + # The plan should only backfill the downstream model despite upstream missing intervals + plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=context.get_snapshot( + downstream_model.name, raise_if_missing=True + ).snapshot_id, + intervals=[ + (to_timestamp("2023-01-07 23:00:00"), to_timestamp("2023-01-08 00:00:00")) + ], + ), + ] + + +@time_machine.travel("2023-01-08 00:00:00 UTC") +def test_forward_only_monthly_model(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + model = context.get_model("sushi.waiter_revenue_by_day") + model = SqlModel.parse_obj( + { + **model.dict(), + "kind": model.kind.copy(update={"forward_only": True}), + "cron": "0 0 1 * *", + "start": "2022-01-01", + "audits": [], + } + ) + context.upsert_model(model) + + plan = context.plan_builder("prod", skip_tests=True).build() + context.apply(plan) + + waiter_revenue_by_day_snapshot = context.get_snapshot(model.name, raise_if_missing=True) + assert waiter_revenue_by_day_snapshot.intervals == [ + (to_timestamp("2022-01-01"), to_timestamp("2023-01-01")) + ] + + model = add_projection_to_model(t.cast(SqlModel, model), literal=True) + context.upsert_model(model) + + waiter_revenue_by_day_snapshot = context.get_snapshot( + "sushi.waiter_revenue_by_day", raise_if_missing=True + ) + + plan = context.plan_builder( + "dev", select_models=[model.name], skip_tests=True, enable_preview=True + ).build() + assert to_timestamp(plan.start) == to_timestamp("2022-12-01") + assert to_timestamp(plan.end) == to_timestamp("2023-01-08") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, + intervals=[(to_timestamp("2022-12-01"), to_timestamp("2023-01-01"))], + ), + ] + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_forward_only_parent_created_in_dev_child_created_in_prod( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + waiter_revenue_by_day_model = context.get_model("sushi.waiter_revenue_by_day") + waiter_revenue_by_day_model = add_projection_to_model( + t.cast(SqlModel, waiter_revenue_by_day_model) + ) + forward_only_kind = waiter_revenue_by_day_model.kind.copy(update={"forward_only": True}) + waiter_revenue_by_day_model = waiter_revenue_by_day_model.copy( + update={"kind": forward_only_kind} + ) + context.upsert_model(waiter_revenue_by_day_model) + + waiter_revenue_by_day_snapshot = context.get_snapshot( + waiter_revenue_by_day_model, raise_if_missing=True + ) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() + assert len(plan.new_snapshots) == 2 + assert ( + plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert all(s.is_forward_only for s in plan.new_snapshots) + assert plan.start == to_datetime("2023-01-01") + assert not plan.missing_intervals + + context.apply(plan) + + # Update the child to refer to a newly added column. + top_waiters_model = context.get_model("sushi.top_waiters") + top_waiters_model = add_projection_to_model(t.cast(SqlModel, top_waiters_model), literal=False) + context.upsert_model(top_waiters_model) + + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder("prod", skip_tests=True, enable_preview=False).build() + assert len(plan.new_snapshots) == 1 + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + + context.apply(plan) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_forward_only_view_migration( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + model = context.get_model("sushi.top_waiters") + assert model.kind.is_view + model = add_projection_to_model(t.cast(SqlModel, model)) + context.upsert_model(model) + + # Apply a forward-only plan + context.plan("prod", skip_tests=True, no_prompts=True, auto_apply=True, forward_only=True) + + # Make sure that the new column got reflected in the view schema + df = context.fetchdf("SELECT one FROM sushi.top_waiters LIMIT 1") + assert len(df) == 1 + + +@time_machine.travel("2023-01-08 00:00:00 UTC") +def test_new_forward_only_model(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + context.plan("dev", skip_tests=True, no_prompts=True, auto_apply=True, enable_preview=False) + + snapshot = context.get_snapshot("sushi.marketing") + + # The deployable table should not exist yet + assert not context.engine_adapter.table_exists(snapshot.table_name()) + assert context.engine_adapter.table_exists(snapshot.table_name(is_deployable=False)) + + context.plan("prod", skip_tests=True, no_prompts=True, auto_apply=True) + + assert context.engine_adapter.table_exists(snapshot.table_name()) + assert context.engine_adapter.table_exists(snapshot.table_name(is_deployable=False)) + + +@time_machine.travel("2023-01-08 15:00:00 UTC", tick=True) +@pytest.mark.parametrize("has_view_binding", [False, True]) +def test_non_breaking_change_after_forward_only_in_dev( + init_and_plan_context: t.Callable, has_view_binding: bool +): + context, plan = init_and_plan_context("examples/sushi") + context.snapshot_evaluator.adapter.HAS_VIEW_BINDING = has_view_binding + context.apply(plan) + + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + waiter_revenue_by_day_snapshot = context.get_snapshot( + "sushi.waiter_revenue_by_day", raise_if_missing=True + ) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder("dev", skip_tests=True, forward_only=True).build() + assert len(plan.new_snapshots) == 2 + assert ( + plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert all(s.is_forward_only for s in plan.new_snapshots) + assert to_timestamp(plan.start) == to_timestamp("2023-01-07") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, + intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], + ), + ] + + # Apply the forward-only changes first. + context.apply(plan) + + dev_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" + ) + assert dev_df["event_date"].tolist() == [pd.to_datetime("2023-01-07")] + + # Make a non-breaking change to a model downstream. + model = context.get_model("sushi.top_waiters") + # Select 'one' column from the updated upstream model. + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model), literal=False)) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder("dev", skip_tests=True).build() + assert len(plan.new_snapshots) == 1 + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert to_timestamp(plan.start) == to_timestamp("2023-01-01") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + # Apply the non-breaking changes. + context.apply(plan) + + dev_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT waiter_id FROM sushi__dev.top_waiters WHERE one IS NOT NULL" + ) + assert not dev_df.empty + + prod_df = context.engine_adapter.fetchdf("DESCRIBE sushi.top_waiters") + assert "one" not in prod_df["column_name"].tolist() + + # Deploy both changes to prod. + plan = context.plan_builder("prod", skip_tests=True).build() + assert plan.start == to_timestamp("2023-01-01") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + context.apply(plan) + + prod_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi.waiter_revenue_by_day WHERE one IS NOT NULL ORDER BY event_date" + ) + assert prod_df.empty + + prod_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT waiter_id FROM sushi.top_waiters WHERE one IS NOT NULL" + ) + assert prod_df.empty + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + # Make sure that the most downstream model is a materialized model. + model = context.get_model("sushi.top_waiters") + model = model.copy(update={"kind": FullKind()}) + context.upsert_model(model) + context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) + + # Make sushi.orders a forward-only model. + model = context.get_model("sushi.orders") + updated_model_kind = model.kind.copy(update={"forward_only": True}) + model = model.copy(update={"stamp": "force new version", "kind": updated_model_kind}) + context.upsert_model(model) + snapshot = context.get_snapshot(model, raise_if_missing=True) + + plan = context.plan_builder( + "dev", + skip_tests=True, + enable_preview=False, + categorizer_config=CategorizerConfig.all_full(), + ).build() + assert ( + plan.context_diff.snapshots[snapshot.snapshot_id].change_category + == SnapshotChangeCategory.BREAKING + ) + assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only + assert not plan.requires_backfill + context.apply(plan) + + # Make a non-breaking change to a model. + model = context.get_model("sushi.top_waiters") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() + assert len(plan.new_snapshots) == 1 + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert plan.start == to_timestamp("2023-01-01") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + # Apply the non-breaking changes. + context.apply(plan) + + # Make a non-breaking change upstream from the previously modified model. + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + waiter_revenue_by_day_snapshot = context.get_snapshot( + "sushi.waiter_revenue_by_day", raise_if_missing=True + ) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() + assert len(plan.new_snapshots) == 2 + assert ( + plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert plan.start == to_timestamp("2023-01-01") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + # Apply the upstream non-breaking changes. + context.apply(plan) + assert not context.plan_builder("dev", skip_tests=True).build().requires_backfill + + # Deploy everything to prod. + plan = context.plan_builder("prod", skip_tests=True, enable_preview=False).build() + assert plan.start == to_timestamp("2023-01-01") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiters_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + context.apply(plan) + assert ( + not context.plan_builder("prod", skip_tests=True, enable_preview=False) + .build() + .requires_backfill + ) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_changes_downstream_of_indirect_non_breaking_snapshot_without_intervals( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Make a breaking change first but don't backfill it + model = context.get_model("sushi.orders") + model = model.copy(update={"stamp": "force new version"}) + context.upsert_model(model) + plan_builder = context.plan_builder( + "dev", skip_backfill=True, skip_tests=True, no_auto_categorization=True + ) + plan_builder.set_choice(context.get_snapshot(model), SnapshotChangeCategory.BREAKING) + context.apply(plan_builder.build()) + + # Now make a non-breaking change to the same snapshot. + model = model.copy(update={"stamp": "force another new version"}) + context.upsert_model(model) + plan_builder = context.plan_builder( + "dev", skip_backfill=True, skip_tests=True, no_auto_categorization=True + ) + plan_builder.set_choice(context.get_snapshot(model), SnapshotChangeCategory.NON_BREAKING) + context.apply(plan_builder.build()) + + # Now make a change to a model downstream of the above model. + downstream_model = context.get_model("sushi.top_waiters") + downstream_model = downstream_model.copy(update={"stamp": "yet another new version"}) + context.upsert_model(downstream_model) + plan = context.plan_builder("dev", skip_tests=True).build() + + # If the parent is not representative then the child cannot be deployable + deployability_index = plan.deployability_index + assert not deployability_index.is_representative( + context.get_snapshot("sushi.waiter_revenue_by_day") + ) + assert not deployability_index.is_deployable(context.get_snapshot("sushi.top_waiters")) + + +@time_machine.travel("2023-01-08 15:00:00 UTC", tick=True) +def test_metadata_change_after_forward_only_results_in_migration(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Make a forward-only change + model = context.get_model("sushi.waiter_revenue_by_day") + model = model.copy(update={"kind": model.kind.copy(update={"forward_only": True})}) + model = add_projection_to_model(t.cast(SqlModel, model)) + context.upsert_model(model) + plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) + assert len(plan.new_snapshots) == 2 + assert all(s.is_forward_only for s in plan.new_snapshots) + + # Follow-up with a metadata change in the same environment + model = model.copy(update={"owner": "new_owner"}) + context.upsert_model(model) + plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) + assert len(plan.new_snapshots) == 2 + assert all(s.change_category == SnapshotChangeCategory.METADATA for s in plan.new_snapshots) + + # Deploy the latest change to prod + context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) + + # Check that the new column was added in prod + columns = context.engine_adapter.columns("sushi.waiter_revenue_by_day") + assert "one" in columns + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_indirect_non_breaking_downstream_of_forward_only(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Make sushi.orders a forward-only model. + forward_only_model = context.get_model("sushi.orders") + updated_model_kind = forward_only_model.kind.copy(update={"forward_only": True}) + forward_only_model = forward_only_model.copy( + update={"stamp": "force new version", "kind": updated_model_kind} + ) + context.upsert_model(forward_only_model) + forward_only_snapshot = context.get_snapshot(forward_only_model, raise_if_missing=True) + + non_breaking_model = context.get_model("sushi.waiter_revenue_by_day") + non_breaking_model = non_breaking_model.copy(update={"start": "2023-01-01"}) + context.upsert_model(add_projection_to_model(t.cast(SqlModel, non_breaking_model))) + non_breaking_snapshot = context.get_snapshot(non_breaking_model, raise_if_missing=True) + top_waiter_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) + + plan = context.plan_builder( + "dev", + skip_tests=True, + enable_preview=False, + categorizer_config=CategorizerConfig.all_full(), + ).build() + assert ( + plan.context_diff.snapshots[forward_only_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.BREAKING + ) + assert ( + plan.context_diff.snapshots[non_breaking_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + plan.context_diff.snapshots[top_waiter_snapshot.snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert plan.context_diff.snapshots[forward_only_snapshot.snapshot_id].is_forward_only + assert not plan.context_diff.snapshots[non_breaking_snapshot.snapshot_id].is_forward_only + assert not plan.context_diff.snapshots[top_waiter_snapshot.snapshot_id].is_forward_only + + assert plan.start == to_timestamp("2023-01-01") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiter_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=non_breaking_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + context.apply(plan) + assert ( + not context.plan_builder("dev", skip_tests=True, enable_preview=False) + .build() + .requires_backfill + ) + + # Deploy everything to prod. + plan = context.plan_builder("prod", skip_tests=True).build() + assert plan.start == to_timestamp("2023-01-01") + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=top_waiter_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + SnapshotIntervals( + snapshot_id=non_breaking_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ), + ] + + context.apply(plan) + assert ( + not context.plan_builder("prod", skip_tests=True, enable_preview=False) + .build() + .requires_backfill + ) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_indirect_non_breaking_view_model_non_representative_snapshot( + init_and_plan_context: t.Callable, +): + context, _ = init_and_plan_context("examples/sushi") + + # Forward-only parent + forward_only_model_name = "memory.sushi.test_forward_only_model" + forward_only_model_expressions = d.parse( + f""" + MODEL ( + name {forward_only_model_name}, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + ), + ); + + SELECT '2023-01-01' AS ds, 'value' AS value; + """ + ) + forward_only_model = load_sql_based_model(forward_only_model_expressions) + assert forward_only_model.forward_only + context.upsert_model(forward_only_model) + + # FULL downstream model. + full_downstream_model_name = "memory.sushi.test_full_downstream_model" + full_downstream_model_expressions = d.parse( + f""" + MODEL ( + name {full_downstream_model_name}, + kind FULL, + ); + + SELECT ds, value FROM {forward_only_model_name}; + """ + ) + full_downstream_model = load_sql_based_model(full_downstream_model_expressions) + context.upsert_model(full_downstream_model) + + # VIEW downstream of the previous FULL model. + view_downstream_model_name = "memory.sushi.test_view_downstream_model" + view_downstream_model_expressions = d.parse( + f""" + MODEL ( + name {view_downstream_model_name}, + kind VIEW, + ); + + SELECT ds, value FROM {full_downstream_model_name}; + """ + ) + view_downstream_model = load_sql_based_model(view_downstream_model_expressions) + context.upsert_model(view_downstream_model) + + # Apply the initial plan with all 3 models. + context.plan(auto_apply=True, no_prompts=True) + + # Make a change to the forward-only model and apply it in dev. + context.upsert_model(add_projection_to_model(t.cast(SqlModel, forward_only_model))) + forward_only_model_snapshot_id = context.get_snapshot(forward_only_model_name).snapshot_id + full_downstream_model_snapshot_id = context.get_snapshot(full_downstream_model_name).snapshot_id + view_downstream_model_snapshot_id = context.get_snapshot(view_downstream_model_name).snapshot_id + dev_plan = context.plan("dev", auto_apply=True, no_prompts=True, enable_preview=False) + assert ( + dev_plan.snapshots[forward_only_model_snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + dev_plan.snapshots[full_downstream_model_snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert ( + dev_plan.snapshots[view_downstream_model_snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + assert not dev_plan.missing_intervals + + # Make a follow-up breaking change to the downstream full model. + new_full_downstream_model_expressions = d.parse( + f""" + MODEL ( + name {full_downstream_model_name}, + kind FULL, + ); + + SELECT ds, 'new_value' AS value FROM {forward_only_model_name}; + """ + ) + new_full_downstream_model = load_sql_based_model(new_full_downstream_model_expressions) + context.upsert_model(new_full_downstream_model) + full_downstream_model_snapshot_id = context.get_snapshot(full_downstream_model_name).snapshot_id + view_downstream_model_snapshot_id = context.get_snapshot(view_downstream_model_name).snapshot_id + dev_plan = context.plan( + "dev", + categorizer_config=CategorizerConfig.all_full(), + auto_apply=True, + no_prompts=True, + enable_preview=False, + ) + assert ( + dev_plan.snapshots[full_downstream_model_snapshot_id].change_category + == SnapshotChangeCategory.BREAKING + ) + assert ( + dev_plan.snapshots[view_downstream_model_snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_BREAKING + ) + assert len(dev_plan.missing_intervals) == 2 + assert dev_plan.missing_intervals[0].snapshot_id == full_downstream_model_snapshot_id + assert dev_plan.missing_intervals[1].snapshot_id == view_downstream_model_snapshot_id + + # Check that the representative view hasn't been created yet. + assert not context.engine_adapter.table_exists( + context.get_snapshot(view_downstream_model_name).table_name() + ) + + # Now promote the very first change to prod without promoting the 2nd breaking change. + context.upsert_model(full_downstream_model) + context.plan(auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full()) + + # Finally, make a non-breaking change to the full model in the same dev environment. + context.upsert_model(add_projection_to_model(t.cast(SqlModel, new_full_downstream_model))) + full_downstream_model_snapshot_id = context.get_snapshot(full_downstream_model_name).snapshot_id + view_downstream_model_snapshot_id = context.get_snapshot(view_downstream_model_name).snapshot_id + dev_plan = context.plan( + "dev", + categorizer_config=CategorizerConfig.all_full(), + auto_apply=True, + no_prompts=True, + enable_preview=False, + ) + assert ( + dev_plan.snapshots[full_downstream_model_snapshot_id].change_category + == SnapshotChangeCategory.NON_BREAKING + ) + assert ( + dev_plan.snapshots[view_downstream_model_snapshot_id].change_category + == SnapshotChangeCategory.INDIRECT_NON_BREAKING + ) + + # Deploy changes to prod + context.plan("prod", auto_apply=True, no_prompts=True) + + # Check that the representative view has been created. + assert context.engine_adapter.table_exists( + context.get_snapshot(view_downstream_model_name).table_name() + ) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_indirect_non_breaking_view_model_non_representative_snapshot_migration( + init_and_plan_context: t.Callable, +): + context, _ = init_and_plan_context("examples/sushi") + + forward_only_model_expr = d.parse( + """ + MODEL ( + name memory.sushi.forward_only_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only TRUE, + on_destructive_change 'allow', + ), + ); + + SELECT '2023-01-07' AS ds, 1 AS a; + """ + ) + forward_only_model = load_sql_based_model(forward_only_model_expr) + context.upsert_model(forward_only_model) + + downstream_view_a_expr = d.parse( + """ + MODEL ( + name memory.sushi.downstream_view_a, + kind VIEW, + ); + + SELECT a from memory.sushi.forward_only_model; + """ + ) + downstream_view_a = load_sql_based_model(downstream_view_a_expr) + context.upsert_model(downstream_view_a) + + downstream_view_b_expr = d.parse( + """ + MODEL ( + name memory.sushi.downstream_view_b, + kind VIEW, + ); + + SELECT a from memory.sushi.downstream_view_a; + """ + ) + downstream_view_b = load_sql_based_model(downstream_view_b_expr) + context.upsert_model(downstream_view_b) + + context.plan(auto_apply=True, no_prompts=True, skip_tests=True) + + # Make a forward-only change + context.upsert_model(add_projection_to_model(t.cast(SqlModel, forward_only_model))) + # Make a non-breaking change downstream + context.upsert_model(add_projection_to_model(t.cast(SqlModel, downstream_view_a))) + + context.plan(auto_apply=True, no_prompts=True, skip_tests=True) + + # Make sure the downstrean indirect non-breaking view is available in prod + count = context.engine_adapter.fetchone("SELECT COUNT(*) FROM memory.sushi.downstream_view_b")[ + 0 + ] + assert count > 0 + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_new_forward_only_model_concurrent_versions(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + new_model_expr = d.parse( + """ + MODEL ( + name memory.sushi.new_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only TRUE, + on_destructive_change 'allow', + ), + ); + + SELECT '2023-01-07' AS ds, 1 AS a; + """ + ) + new_model = load_sql_based_model(new_model_expr) + + # Add the first version of the model and apply it to dev_a. + context.upsert_model(new_model) + snapshot_a = context.get_snapshot(new_model.name) + plan_a = context.plan_builder("dev_a").build() + snapshot_a = plan_a.snapshots[snapshot_a.snapshot_id] + + assert snapshot_a.snapshot_id in plan_a.context_diff.new_snapshots + assert snapshot_a.snapshot_id in plan_a.context_diff.added + assert snapshot_a.change_category == SnapshotChangeCategory.BREAKING + + context.apply(plan_a) + + new_model_alt_expr = d.parse( + """ + MODEL ( + name memory.sushi.new_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only TRUE, + on_destructive_change 'allow', + ), + ); + + SELECT '2023-01-07' AS ds, 1 AS b; + """ + ) + new_model_alt = load_sql_based_model(new_model_alt_expr) + + # Add the second version of the model but don't apply it yet + context.upsert_model(new_model_alt) + snapshot_b = context.get_snapshot(new_model_alt.name) + plan_b = context.plan_builder("dev_b").build() + snapshot_b = plan_b.snapshots[snapshot_b.snapshot_id] + + assert snapshot_b.snapshot_id in plan_b.context_diff.new_snapshots + assert snapshot_b.snapshot_id in plan_b.context_diff.added + assert snapshot_b.change_category == SnapshotChangeCategory.BREAKING + + assert snapshot_b.fingerprint != snapshot_a.fingerprint + assert snapshot_b.version == snapshot_a.version + + # Apply the 1st version to prod + context.upsert_model(new_model) + plan_prod_a = context.plan_builder("prod").build() + assert snapshot_a.snapshot_id in plan_prod_a.snapshots + assert ( + plan_prod_a.snapshots[snapshot_a.snapshot_id].change_category + == SnapshotChangeCategory.BREAKING + ) + context.apply(plan_prod_a) + + df = context.fetchdf("SELECT * FROM memory.sushi.new_model") + assert df.to_dict() == {"ds": {0: "2023-01-07"}, "a": {0: 1}} + + # Modify the 1st version in prod to trigger a forward-only change + new_model = add_projection_to_model(t.cast(SqlModel, new_model)) + context.upsert_model(new_model) + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + + # Apply the 2nd version to dev_b. + # At this point the snapshot of the 2nd version has already been categorized but not + # persisted in the state. This means that when the snapshot of the 1st version was + # being unpaused during promotion to prod, the state of the 2nd version snapshot was not updated + context.apply(plan_b) + + # Apply the 2nd version to prod + context.upsert_model(new_model_alt) + plan_prod_b = context.plan_builder("prod").build() + assert ( + plan_prod_b.snapshots[snapshot_b.snapshot_id].change_category + == SnapshotChangeCategory.BREAKING + ) + assert not plan_prod_b.requires_backfill + context.apply(plan_prod_b) + + df = context.fetchdf("SELECT * FROM memory.sushi.new_model").replace({np.nan: None}) + assert df.to_dict() == {"ds": {0: "2023-01-07"}, "b": {0: None}} + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_new_forward_only_model_same_dev_environment(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + new_model_expr = d.parse( + """ + MODEL ( + name memory.sushi.new_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only TRUE, + on_destructive_change 'allow', + ), + ); + + SELECT '2023-01-07' AS ds, 1 AS a; + """ + ) + new_model = load_sql_based_model(new_model_expr) + + # Add the first version of the model and apply it to dev. + context.upsert_model(new_model) + snapshot_a = context.get_snapshot(new_model.name) + plan_a = context.plan_builder("dev").build() + snapshot_a = plan_a.snapshots[snapshot_a.snapshot_id] + + assert snapshot_a.snapshot_id in plan_a.context_diff.new_snapshots + assert snapshot_a.snapshot_id in plan_a.context_diff.added + assert snapshot_a.change_category == SnapshotChangeCategory.BREAKING + + context.apply(plan_a) + + df = context.fetchdf("SELECT * FROM memory.sushi__dev.new_model") + assert df.to_dict() == {"ds": {0: "2023-01-07"}, "a": {0: 1}} + + new_model_alt_expr = d.parse( + """ + MODEL ( + name memory.sushi.new_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only TRUE, + on_destructive_change 'allow', + ), + ); + + SELECT '2023-01-07' AS ds, 1 AS b; + """ + ) + new_model_alt = load_sql_based_model(new_model_alt_expr) + + # Add the second version of the model and apply it to the same environment. + context.upsert_model(new_model_alt) + snapshot_b = context.get_snapshot(new_model_alt.name) + + context.invalidate_environment("dev", sync=True) + plan_b = context.plan_builder("dev").build() + snapshot_b = plan_b.snapshots[snapshot_b.snapshot_id] + + context.apply(plan_b) + + df = context.fetchdf("SELECT * FROM memory.sushi__dev.new_model").replace({np.nan: None}) + assert df.to_dict() == {"ds": {0: "2023-01-07"}, "b": {0: 1}} diff --git a/tests/core/integration/test_model_kinds.py b/tests/core/integration/test_model_kinds.py new file mode 100644 index 0000000000..1cc1bf7aeb --- /dev/null +++ b/tests/core/integration/test_model_kinds.py @@ -0,0 +1,2644 @@ +from __future__ import annotations + +import typing as t +from collections import Counter +from datetime import timedelta +from unittest import mock +import pandas as pd # noqa: TID253 +import pytest +from pathlib import Path +import time_machine +from pytest_mock.plugin import MockerFixture +from sqlglot import exp + +from sqlmesh import CustomMaterialization +from sqlmesh.core import dialect as d +from sqlmesh.core.config import ( + Config, + ModelDefaultsConfig, + DuckDBConnectionConfig, + GatewayConfig, +) +from sqlmesh.core.console import Console +from sqlmesh.core.context import Context +from sqlmesh.core.config.categorizer import CategorizerConfig +from sqlmesh.core.model import ( + Model, + SqlModel, + CustomKind, + load_sql_based_model, +) +from sqlmesh.core.plan import SnapshotIntervals +from sqlmesh.utils.date import to_date, to_timestamp +from sqlmesh.utils.pydantic import validate_string +from tests.conftest import SushiDataValidator +from sqlmesh.utils import CorrelationId +from tests.utils.test_filesystem import create_temp_file + +if t.TYPE_CHECKING: + from sqlmesh import QueryOrDF + +pytestmark = pytest.mark.slow + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_incremental_by_partition(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + source_name = "raw.test_incremental_by_partition" + model_name = "memory.sushi.test_incremental_by_partition" + + expressions = d.parse( + f""" + MODEL ( + name {model_name}, + kind INCREMENTAL_BY_PARTITION (disable_restatement false), + partitioned_by [key], + allow_partials true, + start '2023-01-07', + ); + + SELECT key, value FROM {source_name}; + """ + ) + model = load_sql_based_model(expressions) + context.upsert_model(model) + + context.engine_adapter.ctas( + source_name, + d.parse_one("SELECT 'key_a' AS key, 1 AS value"), + ) + + context.plan(auto_apply=True, no_prompts=True) + assert context.engine_adapter.fetchall(f"SELECT * FROM {model_name}") == [ + ("key_a", 1), + ] + + context.engine_adapter.replace_query( + source_name, + d.parse_one("SELECT 'key_b' AS key, 1 AS value"), + ) + context.run(ignore_cron=True) + assert context.engine_adapter.fetchall(f"SELECT * FROM {model_name}") == [ + ("key_a", 1), + ("key_b", 1), + ] + + context.engine_adapter.replace_query( + source_name, + d.parse_one("SELECT 'key_a' AS key, 2 AS value"), + ) + # Run 1 minute later. + with time_machine.travel("2023-01-08 15:01:00 UTC"): + context.run(ignore_cron=True) + assert context.engine_adapter.fetchall(f"SELECT * FROM {model_name}") == [ + ("key_b", 1), + ("key_a", 2), + ] + + # model should fully refresh on restatement + context.engine_adapter.replace_query( + source_name, + d.parse_one("SELECT 'key_c' AS key, 3 AS value"), + ) + context.plan(auto_apply=True, no_prompts=True, restate_models=[model_name]) + assert context.engine_adapter.fetchall(f"SELECT * FROM {model_name}") == [ + ("key_c", 3), + ] + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_custom_materialization(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + custom_insert_called = False + + class CustomFullMaterialization(CustomMaterialization): + NAME = "test_custom_full" + + def insert( + self, + table_name: str, + query_or_df: QueryOrDF, + model: Model, + is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], + **kwargs: t.Any, + ) -> None: + nonlocal custom_insert_called + custom_insert_called = True + + self._replace_query_for_model(model, table_name, query_or_df, render_kwargs) + + model = context.get_model("sushi.top_waiters") + kwargs = { + **model.dict(), + # Make a breaking change. + "kind": dict(name="CUSTOM", materialization="test_custom_full"), + } + context.upsert_model(SqlModel.parse_obj(kwargs)) + + context.plan(auto_apply=True, no_prompts=True) + + assert custom_insert_called + + +# needs to be defined at the top level. If its defined within the test body, +# adding to the snapshot cache fails with: AttributeError: Can't pickle local object +class TestCustomKind(CustomKind): + __test__ = False # prevent pytest warning since this isnt a class containing tests + + @property + def custom_property(self) -> str: + return validate_string(self.materialization_properties.get("custom_property")) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_custom_materialization_with_custom_kind(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + custom_insert_calls = [] + + class CustomFullMaterialization(CustomMaterialization[TestCustomKind]): + NAME = "test_custom_full_with_custom_kind" + + def insert( + self, + table_name: str, + query_or_df: QueryOrDF, + model: Model, + is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], + **kwargs: t.Any, + ) -> None: + assert isinstance(model.kind, TestCustomKind) + + nonlocal custom_insert_calls + custom_insert_calls.append(model.kind.custom_property) + + self._replace_query_for_model(model, table_name, query_or_df, render_kwargs) + + model = context.get_model("sushi.top_waiters") + kwargs = { + **model.dict(), + # Make a breaking change. + "kind": dict( + name="CUSTOM", + materialization="test_custom_full_with_custom_kind", + materialization_properties={"custom_property": "pytest"}, + ), + } + context.upsert_model(SqlModel.parse_obj(kwargs)) + + context.plan(auto_apply=True) + + assert custom_insert_calls == ["pytest"] + + # no changes + context.plan(auto_apply=True) + + assert custom_insert_calls == ["pytest"] + + # change a property on the custom kind, breaking change + kwargs["kind"]["materialization_properties"]["custom_property"] = "some value" + context.upsert_model(SqlModel.parse_obj(kwargs)) + context.plan(auto_apply=True) + + assert custom_insert_calls == ["pytest", "some value"] + + +def test_incremental_time_self_reference( + mocker: MockerFixture, sushi_context: Context, sushi_data_validator: SushiDataValidator +): + start_ts = to_timestamp("1 week ago") + start_date, end_date = to_date("1 week ago"), to_date("yesterday") + if to_timestamp(start_date) < start_ts: + # The start date must be aligned by the interval unit. + start_date += timedelta(days=1) + + df = sushi_context.engine_adapter.fetchdf( + "SELECT MIN(event_date) FROM sushi.customer_revenue_lifetime" + ) + assert df.iloc[0, 0] == pd.to_datetime(start_date) + df = sushi_context.engine_adapter.fetchdf( + "SELECT MAX(event_date) FROM sushi.customer_revenue_lifetime" + ) + assert df.iloc[0, 0] == pd.to_datetime(end_date) + results = sushi_data_validator.validate("sushi.customer_revenue_lifetime", start_date, end_date) + plan = sushi_context.plan_builder( + restate_models=["sushi.customer_revenue_lifetime", "sushi.customer_revenue_by_day"], + start=start_date, + end="5 days ago", + ).build() + revenue_lifeteime_snapshot = sushi_context.get_snapshot( + "sushi.customer_revenue_lifetime", raise_if_missing=True + ) + revenue_by_day_snapshot = sushi_context.get_snapshot( + "sushi.customer_revenue_by_day", raise_if_missing=True + ) + assert sorted(plan.missing_intervals, key=lambda x: x.snapshot_id) == sorted( + [ + SnapshotIntervals( + snapshot_id=revenue_lifeteime_snapshot.snapshot_id, + intervals=[ + (to_timestamp(to_date("7 days ago")), to_timestamp(to_date("6 days ago"))), + (to_timestamp(to_date("6 days ago")), to_timestamp(to_date("5 days ago"))), + (to_timestamp(to_date("5 days ago")), to_timestamp(to_date("4 days ago"))), + (to_timestamp(to_date("4 days ago")), to_timestamp(to_date("3 days ago"))), + (to_timestamp(to_date("3 days ago")), to_timestamp(to_date("2 days ago"))), + (to_timestamp(to_date("2 days ago")), to_timestamp(to_date("1 days ago"))), + (to_timestamp(to_date("1 day ago")), to_timestamp(to_date("today"))), + ], + ), + SnapshotIntervals( + snapshot_id=revenue_by_day_snapshot.snapshot_id, + intervals=[ + (to_timestamp(to_date("7 days ago")), to_timestamp(to_date("6 days ago"))), + (to_timestamp(to_date("6 days ago")), to_timestamp(to_date("5 days ago"))), + ], + ), + ], + key=lambda x: x.snapshot_id, + ) + sushi_context.console = mocker.Mock(spec=Console) + sushi_context.apply(plan) + num_batch_calls = Counter( + [x[0][0] for x in sushi_context.console.update_snapshot_evaluation_progress.call_args_list] # type: ignore + ) + # Validate that we made 7 calls to the customer_revenue_lifetime snapshot and 1 call to the customer_revenue_by_day snapshot + assert num_batch_calls == { + sushi_context.get_snapshot("sushi.customer_revenue_lifetime", raise_if_missing=True): 7, + sushi_context.get_snapshot("sushi.customer_revenue_by_day", raise_if_missing=True): 1, + } + # Validate that the results are the same as before the restate + assert results == sushi_data_validator.validate( + "sushi.customer_revenue_lifetime", start_date, end_date + ) + + +def test_incremental_by_time_model_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + assert updated_df["new_column"].dropna().tolist() == [3] + + with time_machine.travel("2023-01-11 00:00:00 UTC"): + updated_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + CAST(4 AS STRING) as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(updated_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True, run=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 3 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + # The destructive change was ignored but this change is coercable and therefore we still return ints + assert updated_df["new_column"].dropna().tolist() == [3, 4] + + with time_machine.travel("2023-01-12 00:00:00 UTC"): + updated_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + CAST(5 AS STRING) as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(updated_model) + + context = Context(paths=[tmp_path], config=config) + # Make the change compatible since that means we will attempt and alter now that is considered additive + context.engine_adapter.SCHEMA_DIFFER_KWARGS["compatible_types"] = { + exp.DataType.build("INT"): {exp.DataType.build("STRING")} + } + context.plan("prod", auto_apply=True, no_prompts=True, run=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 4 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + # The change is now reflected since an additive alter could be performed + assert updated_df["new_column"].dropna().tolist() == ["3", "4", "5"] + + context.close() + + +def test_incremental_by_time_model_ignore_additive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + 'other' as other_column, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column to the source table + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'other' as other_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("ALTER TABLE source_table ADD COLUMN new_column INT") + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is removed since destructive is allowed + assert "name" not in updated_df.columns + # new_column is not added since additive is ignored + assert "new_column" not in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was applied + assert "name" not in updated_df.columns + # new_column is still not added since additive is ignored + assert "new_column" not in updated_df.columns + + with time_machine.travel("2023-01-11 00:00:00 UTC"): + updated_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + CAST(1 AS STRING) as id, + 'other' as other_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(updated_model) + + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.SCHEMA_DIFFER_KWARGS["compatible_types"] = { + exp.DataType.build("INT"): {exp.DataType.build("STRING")} + } + context.plan("prod", auto_apply=True, no_prompts=True, run=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 3 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is still not added since additive is ignored + assert "new_column" not in updated_df.columns + # The additive change was ignored since we set the change as compatible therefore + # instead of getting strings in the result we still return ints + assert updated_df["id"].tolist() == [1, 1, 1] + + with time_machine.travel("2023-01-12 00:00:00 UTC"): + updated_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change allow + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + CAST(1 AS STRING) as id, + 'other' as other_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(updated_model) + + context = Context(paths=[tmp_path], config=config) + # Make the change compatible since that means we will attempt and alter now that is considered additive + context.engine_adapter.SCHEMA_DIFFER_KWARGS["compatible_types"] = { + exp.DataType.build("INT"): {exp.DataType.build("STRING")} + } + context.plan("prod", auto_apply=True, no_prompts=True, run=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 4 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is now added since it is additive is now allowed + assert "new_column" in updated_df.columns + # The change is now reflected since an additive alter could be performed + assert updated_df["id"].dropna().tolist() == ["1", "1", "1", "1"] + + context.close() + + +def test_incremental_by_unique_key_model_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key id, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key id, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_incremental_by_unique_key_model_ignore_additive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key id, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key id, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still not in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + +def test_incremental_unmanaged_model_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_UNMANAGED( + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_UNMANAGED( + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_incremental_unmanaged_model_ignore_additive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_UNMANAGED( + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_UNMANAGED( + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 2 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + +def test_scd_type_2_by_time_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + updated_at_name ds, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_dt as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + updated_at_name ds, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 3 as new_column, + @start_dt as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_scd_type_2_by_time_ignore_additive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + updated_at_name ds, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_dt as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_TIME ( + unique_key id, + updated_at_name ds, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 3 as new_column, + @start_dt as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + +def test_scd_type_2_by_column_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key id, + columns [name], + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key id, + columns [new_column], + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_scd_type_2_by_column_ignore_additive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key id, + columns [stable], + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + 'stable' as stable, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind SCD_TYPE_2_BY_COLUMN ( + unique_key id, + columns [stable], + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'stable2' as stable, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was ignored + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + +def test_incremental_partition_ignore_destructive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_PARTITION ( + on_destructive_change ignore + ), + partitioned_by [ds], + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_PARTITION ( + on_destructive_change ignore + ), + partitioned_by [ds], + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + + context.close() + + +def test_incremental_partition_ignore_additive_change(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_PARTITION ( + on_destructive_change allow, + on_additive_change ignore + ), + partitioned_by [ds], + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 'test_name' as name, + @start_ds as ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") + context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_PARTITION ( + on_destructive_change allow, + on_additive_change ignore + ), + partitioned_by [ds], + start '2023-01-01', + cron '@daily' + ); + + SELECT + *, + 1 as id, + 3 as new_column, + @start_ds as ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + + assert len(updated_df) == 1 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.run() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "source_id" in initial_df.columns + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not still in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + + context.close() + + +def test_incremental_by_time_model_ignore_destructive_change_unit_test(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + test_dir = tmp_path / "tests" + test_dir.mkdir() + test_filepath = test_dir / "test_test_model.yaml" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + id, + name, + ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + initial_test = f""" + +test_test_model: + model: test_model + inputs: + source_table: + - id: 1 + name: 'test_name' + ds: '2025-01-01' + outputs: + query: + - id: 1 + name: 'test_name' + ds: '2025-01-01' +""" + + # Write initial test + test_filepath.write_text(initial_test) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute( + "CREATE TABLE source_table (id INT, name STRING, new_column INT, ds STRING)" + ) + context.engine_adapter.execute( + "INSERT INTO source_table VALUES (1, 'test_name', NULL, '2023-01-01')" + ) + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + test_result = context.test() + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + id, + new_column, + ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + updated_test = f""" + + test_test_model: + model: test_model + inputs: + source_table: + - id: 1 + new_column: 3 + ds: '2025-01-01' + outputs: + query: + - id: 1 + new_column: 3 + ds: '2025-01-01' + """ + + # Write initial test + test_filepath.write_text(updated_test) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + test_result = context.test() + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 1 + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("INSERT INTO source_table VALUES (2, NULL, 3, '2023-01-09')") + context.run() + test_result = context.test() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still in table since destructive was ignored + assert "name" in updated_df.columns + # new_column is added since it is additive and allowed + assert "new_column" in updated_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() + + +def test_incremental_by_time_model_ignore_additive_change_unit_test(tmp_path: Path): + models_dir = tmp_path / "models" + models_dir.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + data_filepath = data_dir / "test.duckdb" + test_dir = tmp_path / "tests" + test_dir.mkdir() + test_filepath = test_dir / "test_test_model.yaml" + + config = Config( + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + default_connection=DuckDBConnectionConfig(database=str(data_filepath)), + ) + + # Initial model with 3 columns + initial_model = f""" + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + id, + name, + ds + FROM + source_table; + """ + + # Write initial model + (models_dir / "test_model.sql").write_text(initial_model) + + initial_test = f""" + +test_test_model: + model: test_model + inputs: + source_table: + - id: 1 + name: 'test_name' + ds: '2025-01-01' + outputs: + query: + - id: 1 + name: 'test_name' + ds: '2025-01-01' +""" + + # Write initial test + test_filepath.write_text(initial_test) + + with time_machine.travel("2023-01-08 00:00:00 UTC"): + # Create context and apply initial model + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute( + "CREATE TABLE source_table (id INT, name STRING, new_column INT, ds STRING)" + ) + context.engine_adapter.execute( + "INSERT INTO source_table VALUES (1, 'test_name', NULL, '2023-01-01')" + ) + + # Apply initial plan and load data + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + test_result = context.test() + + # Verify initial data was loaded + initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(initial_df) == 1 + assert "id" in initial_df.columns + assert "name" in initial_df.columns + assert "ds" in initial_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() + + # remove `name` column and add new column + initial_model = """ + MODEL ( + name test_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds, + forward_only true, + on_destructive_change allow, + on_additive_change ignore + ), + start '2023-01-01', + cron '@daily' + ); + + SELECT + id, + new_column, + ds + FROM + source_table; + """ + (models_dir / "test_model.sql").write_text(initial_model) + + # `new_column` is in the output since unit tests are based on the model definition that currently + # exists and doesn't take into account the historical changes to the table. Therefore `new_column` is + # not actually in the table but it is represented in the test + updated_test = f""" + test_test_model: + model: test_model + inputs: + source_table: + - id: 1 + new_column: 3 + ds: '2025-01-01' + outputs: + query: + - id: 1 + new_column: 3 + ds: '2025-01-01' + """ + + # Write initial test + test_filepath.write_text(updated_test) + + context = Context(paths=[tmp_path], config=config) + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + test_result = context.test() + + # Verify data loading continued to work + # The existing data should still be there and new data should be loaded + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 1 + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is not in table since destructive was ignored + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() + + with time_machine.travel("2023-01-10 00:00:00 UTC"): + context = Context(paths=[tmp_path], config=config) + context.engine_adapter.execute("INSERT INTO source_table VALUES (2, NULL, 3, '2023-01-09')") + context.run() + test_result = context.test() + updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') + assert len(updated_df) == 2 + assert "id" in updated_df.columns + assert "ds" in updated_df.columns + # name is still not in table since destructive was allowed + assert "name" not in updated_df.columns + # new_column is not added since it is additive and ignored + assert "new_column" not in updated_df.columns + assert len(test_result.successes) == 1 + assert test_result.testsRun == len(test_result.successes) + + context.close() + + +@time_machine.travel("2020-01-01 00:00:00 UTC") +def test_scd_type_2_full_restatement_no_start_date(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Initial product catalog of 3 products + raw_products = d.parse(""" + MODEL ( + name memory.store.raw_products, + kind FULL + ); + + SELECT * FROM VALUES + (101, 'Laptop Pro', 1299.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP), + (102, 'Wireless Mouse', 49.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP), + (103, 'Office Chair', 199.99, 'Furniture', '2020-01-01 00:00:00'::TIMESTAMP) + AS t(product_id, product_name, price, category, last_updated); + """) + + # SCD Type 2 model for product history tracking + product_history = d.parse(""" + MODEL ( + name memory.store.product_history, + kind SCD_TYPE_2_BY_TIME ( + unique_key product_id, + updated_at_name last_updated, + disable_restatement false + ), + owner catalog_team, + cron '0 */6 * * *', + grain product_id, + description 'Product catalog change history' + ); + + SELECT + product_id::INT AS product_id, + product_name::TEXT AS product_name, + price::DECIMAL(10,2) AS price, + category::TEXT AS category, + last_updated AS last_updated + FROM + memory.store.raw_products; + """) + + raw_products_model = load_sql_based_model(raw_products) + product_history_model = load_sql_based_model(product_history) + context.upsert_model(raw_products_model) + context.upsert_model(product_history_model) + + # Initial plan and apply + plan = context.plan_builder("prod", skip_tests=True).build() + context.apply(plan) + + query = "SELECT product_id, product_name, price, category, last_updated, valid_from, valid_to FROM memory.store.product_history ORDER BY product_id, valid_from" + initial_data = context.engine_adapter.fetchdf(query) + + # Validate initial state of 3 products all active + assert len(initial_data) == 3 + assert initial_data["valid_to"].isna().all() + initial_product_names = set(initial_data["product_name"].tolist()) + assert initial_product_names == {"Laptop Pro", "Wireless Mouse", "Office Chair"} + + # Price update and category change + with time_machine.travel("2020-01-15 12:00:00 UTC"): + raw_products_v2 = d.parse(""" + MODEL ( + name memory.store.raw_products, + kind FULL + ); + + SELECT * FROM VALUES + (101, 'Laptop Pro', 1199.99, 'Electronics', '2020-01-15 00:00:00'::TIMESTAMP), + (102, 'Wireless Mouse', 49.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP), + (103, 'Ergonomic Office Chair', 229.99, 'Office Furniture', '2020-01-15 00:00:00'::TIMESTAMP) + AS t(product_id, product_name, price, category, last_updated); + """) + raw_products_v2_model = load_sql_based_model(raw_products_v2) + context.upsert_model(raw_products_v2_model) + context.plan( + auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() + ) + context.run() + + data_after_first_change = context.engine_adapter.fetchdf(query) + + # Should have 5 records (3 original closed, 2 new activε, 1 unchanged) + assert len(data_after_first_change) == 5 + + # Second change + with time_machine.travel("2020-02-01 10:00:00 UTC"): + raw_products_v3 = d.parse(""" + MODEL ( + name memory.store.raw_products, + kind FULL + ); + + SELECT * FROM VALUES + (101, 'Laptop Pro Max', 1399.99, 'Electronics', '2020-02-01 00:00:00'::TIMESTAMP), + (103, 'Ergonomic Office Chair', 229.99, 'Office Furniture', '2020-01-15 00:00:00'::TIMESTAMP), + (102, 'Wireless Mouse', 49.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP) + AS t(product_id, product_name, price, category, last_updated); + """) + raw_products_v3_model = load_sql_based_model(raw_products_v3) + context.upsert_model(raw_products_v3_model) + context.plan( + auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() + ) + context.run() + data_after_second_change = context.engine_adapter.fetchdf(query) + assert len(data_after_second_change) == 6 + + # Store the current state before full restatement + data_before_full_restatement = data_after_second_change.copy() + + # Perform full restatement (no start date provided) + with time_machine.travel("2020-02-01 15:00:00 UTC"): + plan = context.plan_builder( + "prod", skip_tests=True, restate_models=["memory.store.product_history"] + ).build() + context.apply(plan) + data_after_full_restatement = context.engine_adapter.fetchdf(query) + assert len(data_after_full_restatement) == 3 + + # Check that all currently active products before restatement are still active after restatement + active_before = data_before_full_restatement[ + data_before_full_restatement["valid_to"].isna() + ] + active_after = data_after_full_restatement + assert set(active_before["product_id"]) == set(active_after["product_id"]) + + expected_products = { + 101: { + "product_name": "Laptop Pro Max", + "price": 1399.99, + "category": "Electronics", + "last_updated": "2020-02-01", + }, + 102: { + "product_name": "Wireless Mouse", + "price": 49.99, + "category": "Electronics", + "last_updated": "2020-01-01", + }, + 103: { + "product_name": "Ergonomic Office Chair", + "price": 229.99, + "category": "Office Furniture", + "last_updated": "2020-01-15", + }, + } + for _, row in data_after_full_restatement.iterrows(): + pid = row["product_id"] + assert pid in expected_products + expected = expected_products[pid] + assert row["product_name"] == expected["product_name"] + assert float(row["price"]) == expected["price"] + assert row["category"] == expected["category"] + + # valid_from should be the epoch, valid_to should be NaT + assert str(row["valid_from"]) == "1970-01-01 00:00:00" + assert pd.isna(row["valid_to"]) + + +def test_plan_evaluator_correlation_id(tmp_path: Path): + def _correlation_id_in_sqls(correlation_id: CorrelationId, mock_logger): + sqls = [call[0][0] for call in mock_logger.call_args_list] + return any(f"/* {correlation_id} */" in sql for sql in sqls) + + ctx = Context(paths=[tmp_path], config=Config()) + + # Case: Ensure that the correlation id (plan_id) is included in the SQL for each plan + for i in range(2): + create_temp_file( + tmp_path, + Path("models", "test.sql"), + f"MODEL (name test.a, kind FULL); SELECT {i} AS col", + ) + + with mock.patch("sqlmesh.core.engine_adapter.base.EngineAdapter._log_sql") as mock_logger: + ctx.load() + plan = ctx.plan(auto_apply=True, no_prompts=True) + + correlation_id = CorrelationId.from_plan_id(plan.plan_id) + assert str(correlation_id) == f"SQLMESH_PLAN: {plan.plan_id}" + + assert _correlation_id_in_sqls(correlation_id, mock_logger) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_scd_type_2_regular_run_with_offset(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + raw_employee_status = d.parse(""" + MODEL ( + name memory.hr_system.raw_employee_status, + kind FULL + ); + + SELECT + 1001 AS employee_id, + 'engineering' AS department, + 'EMEA' AS region, + '2023-01-08 15:00:00 UTC' AS last_modified; + """) + + employee_history = d.parse(""" + MODEL ( + name memory.hr_system.employee_history, + kind SCD_TYPE_2_BY_TIME ( + unique_key employee_id, + updated_at_name last_modified, + disable_restatement false + ), + owner hr_analytics, + cron '0 7 * * *', + grain employee_id, + description 'Historical tracking of employee status changes' + ); + + SELECT + employee_id::INT AS employee_id, + department::TEXT AS department, + region::TEXT AS region, + last_modified AS last_modified + FROM + memory.hr_system.raw_employee_status; + """) + + raw_employee_status_model = load_sql_based_model(raw_employee_status) + employee_history_model = load_sql_based_model(employee_history) + context.upsert_model(raw_employee_status_model) + context.upsert_model(employee_history_model) + + # Initial plan and apply + plan = context.plan_builder("prod", skip_tests=True).build() + context.apply(plan) + + query = "SELECT employee_id, department, region, valid_from, valid_to FROM memory.hr_system.employee_history ORDER BY employee_id, valid_from" + initial_data = context.engine_adapter.fetchdf(query) + + assert len(initial_data) == 1 + assert initial_data["valid_to"].isna().all() + assert initial_data["department"].tolist() == ["engineering"] + assert initial_data["region"].tolist() == ["EMEA"] + + # Apply a future plan with source changes a few hours before the cron time of the SCD Type 2 model BUT on the same day + with time_machine.travel("2023-01-09 00:10:00 UTC"): + raw_employee_status_v2 = d.parse(""" + MODEL ( + name memory.hr_system.raw_employee_status, + kind FULL + ); + + SELECT + 1001 AS employee_id, + 'engineering' AS department, + 'AMER' AS region, + '2023-01-09 00:10:00 UTC' AS last_modified; + """) + raw_employee_status_v2_model = load_sql_based_model(raw_employee_status_v2) + context.upsert_model(raw_employee_status_v2_model) + context.plan( + auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() + ) + + # The 7th hour of the day the run is kicked off for the SCD Type 2 model + with time_machine.travel("2023-01-09 07:00:01 UTC"): + context.run() + data_after_change = context.engine_adapter.fetchdf(query) + + # Validate the SCD2 records for employee 1001 + assert len(data_after_change) == 2 + assert data_after_change.iloc[0]["employee_id"] == 1001 + assert data_after_change.iloc[0]["department"] == "engineering" + assert data_after_change.iloc[0]["region"] == "EMEA" + assert str(data_after_change.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" + assert str(data_after_change.iloc[0]["valid_to"]) == "2023-01-09 00:10:00" + assert data_after_change.iloc[1]["employee_id"] == 1001 + assert data_after_change.iloc[1]["department"] == "engineering" + assert data_after_change.iloc[1]["region"] == "AMER" + assert str(data_after_change.iloc[1]["valid_from"]) == "2023-01-09 00:10:00" + assert pd.isna(data_after_change.iloc[1]["valid_to"]) + + # Update source model again a bit later on the same day + raw_employee_status_v2 = d.parse(""" + MODEL ( + name memory.hr_system.raw_employee_status, + kind FULL + ); + + SELECT + 1001 AS employee_id, + 'sales' AS department, + 'ANZ' AS region, + '2023-01-09 07:26:00 UTC' AS last_modified; + """) + raw_employee_status_v2_model = load_sql_based_model(raw_employee_status_v2) + context.upsert_model(raw_employee_status_v2_model) + context.plan( + auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() + ) + + # A day later the run is kicked off for the SCD Type 2 model again + with time_machine.travel("2023-01-10 07:00:00 UTC"): + context.run() + data_after_change = context.engine_adapter.fetchdf(query) + + # Validate the SCD2 history for employee 1001 after second change with the historical records intact + assert len(data_after_change) == 3 + assert data_after_change.iloc[0]["employee_id"] == 1001 + assert data_after_change.iloc[0]["department"] == "engineering" + assert data_after_change.iloc[0]["region"] == "EMEA" + assert str(data_after_change.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" + assert str(data_after_change.iloc[0]["valid_to"]) == "2023-01-09 00:10:00" + assert data_after_change.iloc[1]["employee_id"] == 1001 + assert data_after_change.iloc[1]["department"] == "engineering" + assert data_after_change.iloc[1]["region"] == "AMER" + assert str(data_after_change.iloc[1]["valid_from"]) == "2023-01-09 00:10:00" + assert str(data_after_change.iloc[1]["valid_to"]) == "2023-01-09 07:26:00" + assert data_after_change.iloc[2]["employee_id"] == 1001 + assert data_after_change.iloc[2]["department"] == "sales" + assert data_after_change.iloc[2]["region"] == "ANZ" + assert str(data_after_change.iloc[2]["valid_from"]) == "2023-01-09 07:26:00" + assert pd.isna(data_after_change.iloc[2]["valid_to"]) + + # Now test restatement works (full restatement support currently) + with time_machine.travel("2023-01-10 07:38:00 UTC"): + plan = context.plan_builder( + "prod", + skip_tests=True, + restate_models=["memory.hr_system.employee_history"], + start="2023-01-09 00:10:00", + ).build() + context.apply(plan) + restated_data = context.engine_adapter.fetchdf(query) + + # Validate the SCD2 history after restatement has been wiped bar one + assert len(restated_data) == 1 + assert restated_data.iloc[0]["employee_id"] == 1001 + assert restated_data.iloc[0]["department"] == "sales" + assert restated_data.iloc[0]["region"] == "ANZ" + assert str(restated_data.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" + assert pd.isna(restated_data.iloc[0]["valid_to"]) + + +def test_seed_model_metadata_update_does_not_trigger_backfill(tmp_path: Path): + """ + Scenario: + - Create a seed model; perform initial population + - Modify the model with a metadata-only change and trigger a plan + + Outcome: + - The seed model is modified (metadata-only) but this should NOT trigger backfill + - There should be no missing_intervals on the plan to backfill + """ + + models_path = tmp_path / "models" + seeds_path = tmp_path / "seeds" + models_path.mkdir() + seeds_path.mkdir() + + seed_model_path = models_path / "seed.sql" + seed_path = seeds_path / "seed_data.csv" + + seed_path.write_text("\n".join(["id,name", "1,test"])) + + seed_model_path.write_text(""" + MODEL ( + name test.source_data, + kind SEED ( + path '../seeds/seed_data.csv' + ) + ); + """) + + config = Config( + gateways={"": GatewayConfig(connection=DuckDBConnectionConfig())}, + model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01"), + ) + ctx = Context(paths=tmp_path, config=config) + + plan = ctx.plan(auto_apply=True) + + original_seed_snapshot = ctx.snapshots['"memory"."test"."source_data"'] + assert plan.directly_modified == {original_seed_snapshot.snapshot_id} + assert plan.metadata_updated == set() + assert plan.missing_intervals + + # prove data loaded + assert ctx.engine_adapter.fetchall("select id, name from memory.test.source_data") == [ + (1, "test") + ] + + # prove no diff + ctx.load() + plan = ctx.plan(auto_apply=True) + assert not plan.has_changes + assert not plan.missing_intervals + + # make metadata-only change + seed_model_path.write_text(""" + MODEL ( + name test.source_data, + kind SEED ( + path '../seeds/seed_data.csv' + ), + description 'updated by test' + ); + """) + + ctx.load() + plan = ctx.plan(auto_apply=True) + assert plan.has_changes + + new_seed_snapshot = ctx.snapshots['"memory"."test"."source_data"'] + assert ( + new_seed_snapshot.version == original_seed_snapshot.version + ) # should be using the same physical table + assert ( + new_seed_snapshot.snapshot_id != original_seed_snapshot.snapshot_id + ) # but still be different due to the metadata change + assert plan.directly_modified == set() + assert plan.metadata_updated == {new_seed_snapshot.snapshot_id} + + # there should be no missing intervals to backfill since all we did is update a description + assert not plan.missing_intervals + + # there should still be no diff or missing intervals in 3 days time + assert new_seed_snapshot.model.interval_unit.is_day + with time_machine.travel(timedelta(days=3)): + ctx.clear_caches() + ctx.load() + plan = ctx.plan(auto_apply=True) + assert not plan.has_changes + assert not plan.missing_intervals + + # change seed data + seed_path.write_text("\n".join(["id,name", "1,test", "2,updated"])) + + # new plan - NOW we should backfill because data changed + ctx.load() + plan = ctx.plan(auto_apply=True) + assert plan.has_changes + + updated_seed_snapshot = ctx.snapshots['"memory"."test"."source_data"'] + + assert ( + updated_seed_snapshot.snapshot_id + != new_seed_snapshot.snapshot_id + != original_seed_snapshot.snapshot_id + ) + assert not updated_seed_snapshot.forward_only + assert plan.directly_modified == {updated_seed_snapshot.snapshot_id} + assert plan.metadata_updated == set() + assert plan.missing_intervals + + # prove backfilled data loaded + assert ctx.engine_adapter.fetchall("select id, name from memory.test.source_data") == [ + (1, "test"), + (2, "updated"), + ] + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_seed_model_promote_to_prod_after_dev( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + with open(context.path / "seeds" / "waiter_names.csv", "a") as f: + f.write("\n10,New Waiter") + + context.load() + + waiter_names_snapshot = context.get_snapshot("sushi.waiter_names") + plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) + assert waiter_names_snapshot.snapshot_id in plan.directly_modified + + # Trigger a metadata change to reuse the previous version + waiter_names_model = waiter_names_snapshot.model.copy( + update={"description": "Updated description"} + ) + context.upsert_model(waiter_names_model) + context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) + + # Promote all changes to prod + waiter_names_snapshot = context.get_snapshot("sushi.waiter_names") + plan = context.plan_builder("prod", skip_tests=True).build() + # Clear the cache to source the dehydrated model instance from the state + context.clear_caches() + context.apply(plan) + + assert ( + context.engine_adapter.fetchone("SELECT COUNT(*) FROM sushi.waiter_names WHERE id = 10")[0] + == 1 + ) diff --git a/tests/core/integration/test_multi_repo.py b/tests/core/integration/test_multi_repo.py new file mode 100644 index 0000000000..6477b08741 --- /dev/null +++ b/tests/core/integration/test_multi_repo.py @@ -0,0 +1,456 @@ +from __future__ import annotations + +from unittest.mock import patch +from textwrap import dedent +import os +import pytest +from pathlib import Path +from sqlmesh.core.console import ( + get_console, +) +from sqlmesh.core.config.naming import NameInferenceConfig +from sqlmesh.core.model.common import ParsableSql +from sqlmesh.utils.concurrency import NodeExecutionFailedError + +from sqlmesh.core import constants as c +from sqlmesh.core.config import ( + Config, + GatewayConfig, + ModelDefaultsConfig, + DuckDBConnectionConfig, +) +from sqlmesh.core.console import get_console +from sqlmesh.core.context import Context +from sqlmesh.utils.date import now +from tests.conftest import DuckDBMetadata +from tests.utils.test_helpers import use_terminal_console +from tests.core.integration.utils import validate_apply_basics + + +pytestmark = pytest.mark.slow + + +@use_terminal_console +def test_multi(mocker): + context = Context(paths=["examples/multi/repo_1", "examples/multi/repo_2"], gateway="memory") + + with patch.object(get_console(), "log_warning") as mock_logger: + context.plan_builder(environment="dev") + warnings = mock_logger.call_args[0][0] + repo1_path, repo2_path = context.configs.keys() + assert f"Linter warnings for {repo1_path}" in warnings + assert f"Linter warnings for {repo2_path}" not in warnings + + assert ( + context.render("bronze.a").sql() + == '''SELECT 1 AS "col_a", 'b' AS "col_b", 1 AS "one", 'repo_1' AS "dup"''' + ) + assert ( + context.render("silver.d").sql() + == '''SELECT "c"."col_a" AS "col_a", 2 AS "two", 'repo_2' AS "dup" FROM "memory"."silver"."c" AS "c"''' + ) + context._new_state_sync().reset(default_catalog=context.default_catalog) + plan = context.plan_builder().build() + assert len(plan.new_snapshots) == 5 + context.apply(plan) + + # Ensure before_all, after_all statements for multiple repos have executed + environment_statements = context.state_reader.get_environment_statements(c.PROD) + assert len(environment_statements) == 2 + assert context.fetchdf("select * from before_1").to_dict()["1"][0] == 1 + assert context.fetchdf("select * from before_2").to_dict()["2"][0] == 2 + assert context.fetchdf("select * from after_1").to_dict()["repo_1"][0] == "repo_1" + assert context.fetchdf("select * from after_2").to_dict()["repo_2"][0] == "repo_2" + + old_context = context + context = Context( + paths=["examples/multi/repo_1"], + state_sync=old_context.state_sync, + gateway="memory", + ) + context._engine_adapter = old_context.engine_adapter + del context.engine_adapters + + model = context.get_model("bronze.a") + assert model.project == "repo_1" + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql(sql=model.query.select("'c' AS c").sql(dialect=model.dialect)) + } + ) + ) + plan = context.plan_builder().build() + + assert set(snapshot.name for snapshot in plan.directly_modified) == { + '"memory"."bronze"."a"', + '"memory"."bronze"."b"', + '"memory"."silver"."e"', + } + assert sorted([x.name for x in list(plan.indirectly_modified.values())[0]]) == [ + '"memory"."silver"."c"', + '"memory"."silver"."d"', + ] + assert len(plan.missing_intervals) == 3 + context.apply(plan) + validate_apply_basics(context, c.PROD, plan.snapshots.values()) + + # Ensure that before_all and after_all statements of both repos are there despite planning with repo_1 + environment_statements = context.state_reader.get_environment_statements(c.PROD) + assert len(environment_statements) == 2 + + # Ensure that environment statements have the project field set correctly + sorted_env_statements = sorted(environment_statements, key=lambda es: es.project) + assert sorted_env_statements[0].project == "repo_1" + assert sorted_env_statements[1].project == "repo_2" + + # Assert before_all and after_all for each project + assert sorted_env_statements[0].before_all == [ + "CREATE TABLE IF NOT EXISTS before_1 AS select @one()" + ] + assert sorted_env_statements[0].after_all == [ + "CREATE TABLE IF NOT EXISTS after_1 AS select @dup()" + ] + assert sorted_env_statements[1].before_all == [ + "CREATE TABLE IF NOT EXISTS before_2 AS select @two()" + ] + assert sorted_env_statements[1].after_all == [ + "CREATE TABLE IF NOT EXISTS after_2 AS select @dup()" + ] + + +@use_terminal_console +def test_multi_repo_single_project_environment_statements_update(copy_to_temp_path): + paths = copy_to_temp_path("examples/multi") + repo_1_path = f"{paths[0]}/repo_1" + repo_2_path = f"{paths[0]}/repo_2" + + context = Context(paths=[repo_1_path, repo_2_path], gateway="memory") + context._new_state_sync().reset(default_catalog=context.default_catalog) + + initial_plan = context.plan_builder().build() + context.apply(initial_plan) + + # Get initial statements + initial_statements = context.state_reader.get_environment_statements(c.PROD) + assert len(initial_statements) == 2 + + # Modify repo_1's config to add a new before_all statement + repo_1_config_path = f"{repo_1_path}/config.yaml" + with open(repo_1_config_path, "r") as f: + config_content = f.read() + + # Add a new before_all statement to repo_1 only + modified_config = config_content.replace( + "CREATE TABLE IF NOT EXISTS before_1 AS select @one()", + "CREATE TABLE IF NOT EXISTS before_1 AS select @one()\n - CREATE TABLE IF NOT EXISTS before_1_modified AS select 999", + ) + + with open(repo_1_config_path, "w") as f: + f.write(modified_config) + + # Create new context with modified config but only for repo_1 + context_repo_1_only = Context( + paths=[repo_1_path], state_sync=context.state_sync, gateway="memory" + ) + + # Plan with only repo_1, this should preserve repo_2's statements from state + repo_1_plan = context_repo_1_only.plan_builder(environment="dev").build() + context_repo_1_only.apply(repo_1_plan) + updated_statements = context_repo_1_only.state_reader.get_environment_statements("dev") + + # Should still have statements from both projects + assert len(updated_statements) == 2 + + # Sort by project + sorted_updated = sorted(updated_statements, key=lambda es: es.project or "") + + # Verify repo_1 has the new statement + repo_1_updated = sorted_updated[0] + assert repo_1_updated.project == "repo_1" + assert len(repo_1_updated.before_all) == 2 + assert "CREATE TABLE IF NOT EXISTS before_1_modified" in repo_1_updated.before_all[1] + + # Verify repo_2 statements are preserved from state + repo_2_preserved = sorted_updated[1] + assert repo_2_preserved.project == "repo_2" + assert len(repo_2_preserved.before_all) == 1 + assert "CREATE TABLE IF NOT EXISTS before_2" in repo_2_preserved.before_all[0] + assert "CREATE TABLE IF NOT EXISTS after_2 AS select @dup()" in repo_2_preserved.after_all[0] + + +@use_terminal_console +def test_multi_virtual_layer(copy_to_temp_path): + paths = copy_to_temp_path("tests/fixtures/multi_virtual_layer") + path = Path(paths[0]) + first_db_path = str(path / "db_1.db") + second_db_path = str(path / "db_2.db") + + config = Config( + gateways={ + "first": GatewayConfig( + connection=DuckDBConnectionConfig(database=first_db_path), + variables={"overriden_var": "gateway_1"}, + ), + "second": GatewayConfig( + connection=DuckDBConnectionConfig(database=second_db_path), + variables={"overriden_var": "gateway_2"}, + ), + }, + model_defaults=ModelDefaultsConfig(dialect="duckdb"), + model_naming=NameInferenceConfig(infer_names=True), + default_gateway="first", + gateway_managed_virtual_layer=True, + variables={"overriden_var": "global", "global_one": 88}, + ) + + context = Context(paths=paths, config=config) + assert context.default_catalog_per_gateway == {"first": "db_1", "second": "db_2"} + assert len(context.engine_adapters) == 2 + + # For the model without gateway the default should be used and the gateway variable should overide the global + assert ( + context.render("first_schema.model_one").sql() + == 'SELECT \'gateway_1\' AS "item_id", 88 AS "global_one", 1 AS "macro_one"' + ) + + # For model with gateway specified the appropriate variable should be used to overide + assert ( + context.render("db_2.second_schema.model_one").sql() + == 'SELECT \'gateway_2\' AS "item_id", 88 AS "global_one", 1 AS "macro_one"' + ) + + plan = context.plan_builder().build() + assert len(plan.new_snapshots) == 4 + context.apply(plan) + + # Validate the tables that source from the first tables are correct as well with evaluate + assert ( + context.evaluate( + "first_schema.model_two", start=now(), end=now(), execution_time=now() + ).to_string() + == " item_id global_one\n0 gateway_1 88" + ) + assert ( + context.evaluate( + "db_2.second_schema.model_two", start=now(), end=now(), execution_time=now() + ).to_string() + == " item_id global_one\n0 gateway_2 88" + ) + + assert sorted(set(snapshot.name for snapshot in plan.directly_modified)) == [ + '"db_1"."first_schema"."model_one"', + '"db_1"."first_schema"."model_two"', + '"db_2"."second_schema"."model_one"', + '"db_2"."second_schema"."model_two"', + ] + + model = context.get_model("db_1.first_schema.model_one") + + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'c' AS extra").sql(dialect=model.dialect) + ) + } + ) + ) + plan = context.plan_builder().build() + context.apply(plan) + + state_environments = context.state_reader.get_environments() + state_snapshots = context.state_reader.get_snapshots(context.snapshots.values()) + + assert state_environments[0].gateway_managed + assert len(state_snapshots) == len(state_environments[0].snapshots) + assert [snapshot.name for snapshot in plan.directly_modified] == [ + '"db_1"."first_schema"."model_one"' + ] + assert [x.name for x in list(plan.indirectly_modified.values())[0]] == [ + '"db_1"."first_schema"."model_two"' + ] + + assert len(plan.missing_intervals) == 1 + assert ( + context.evaluate( + "db_1.first_schema.model_one", start=now(), end=now(), execution_time=now() + ).to_string() + == " item_id global_one macro_one extra\n0 gateway_1 88 1 c" + ) + + # Create dev environment with changed models + model = context.get_model("db_2.second_schema.model_one") + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'d' AS extra").sql(dialect=model.dialect) + ) + } + ) + ) + model = context.get_model("first_schema.model_two") + context.upsert_model( + model.copy( + update={ + "query_": ParsableSql( + sql=model.query.select("'d2' AS col").sql(dialect=model.dialect) + ) + } + ) + ) + plan = context.plan_builder("dev").build() + context.apply(plan) + + dev_environment = context.state_sync.get_environment("dev") + assert dev_environment is not None + + metadata_engine_1 = DuckDBMetadata.from_context(context) + start_schemas_1 = set(metadata_engine_1.schemas) + assert sorted(start_schemas_1) == sorted( + {"first_schema__dev", "sqlmesh", "first_schema", "sqlmesh__first_schema"} + ) + + metadata_engine_2 = DuckDBMetadata(context._get_engine_adapter("second")) + start_schemas_2 = set(metadata_engine_2.schemas) + assert sorted(start_schemas_2) == sorted( + {"sqlmesh__second_schema", "second_schema", "second_schema__dev"} + ) + + # Invalidate dev environment + context.invalidate_environment("dev") + invalidate_environment = context.state_sync.get_environment("dev") + assert invalidate_environment is not None + assert invalidate_environment.expiration_ts < dev_environment.expiration_ts # type: ignore + assert sorted(start_schemas_1) == sorted(set(metadata_engine_1.schemas)) + assert sorted(start_schemas_2) == sorted(set(metadata_engine_2.schemas)) + + # Run janitor + context._run_janitor() + assert context.state_sync.get_environment("dev") is None + removed_schemas = start_schemas_1 - set(metadata_engine_1.schemas) + assert removed_schemas == {"first_schema__dev"} + removed_schemas = start_schemas_2 - set(metadata_engine_2.schemas) + assert removed_schemas == {"second_schema__dev"} + prod_environment = context.state_sync.get_environment("prod") + + # Remove the second gateway's second model and apply plan + second_model = path / "models/second_schema/model_two.sql" + os.remove(second_model) + assert not second_model.exists() + context = Context(paths=paths, config=config) + plan = context.plan_builder().build() + context.apply(plan) + prod_environment = context.state_sync.get_environment("prod") + assert len(prod_environment.snapshots_) == 3 + + # Changing the flag should show a diff + context.config.gateway_managed_virtual_layer = False + plan = context.plan_builder().build() + assert not plan.requires_backfill + assert ( + plan.context_diff.previous_gateway_managed_virtual_layer + != plan.context_diff.gateway_managed_virtual_layer + ) + assert plan.context_diff.has_changes + + # This should error since the default_gateway won't have access to create the view on a non-shared catalog + with pytest.raises(NodeExecutionFailedError, match=r"Execution failed for node SnapshotId*"): + context.apply(plan) + + +def test_multi_dbt(mocker): + context = Context(paths=["examples/multi_dbt/bronze", "examples/multi_dbt/silver"]) + context._new_state_sync().reset(default_catalog=context.default_catalog) + plan = context.plan_builder().build() + assert len(plan.new_snapshots) == 4 + context.apply(plan) + validate_apply_basics(context, c.PROD, plan.snapshots.values()) + + environment_statements = context.state_sync.get_environment_statements(c.PROD) + assert len(environment_statements) == 2 + bronze_statements = environment_statements[0] + assert bronze_statements.before_all == [ + "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);\nJINJA_END;" + ] + assert not bronze_statements.after_all + silver_statements = environment_statements[1] + assert not silver_statements.before_all + assert silver_statements.after_all == [ + "JINJA_STATEMENT_BEGIN;\n{{ store_schemas(schemas) }}\nJINJA_END;" + ] + assert "store_schemas" in silver_statements.jinja_macros.root_macros + analytics_table = context.fetchdf("select * from analytic_stats;") + assert sorted(analytics_table.columns) == sorted(["physical_table", "evaluation_time"]) + schema_table = context.fetchdf("select * from schema_table;") + assert sorted(schema_table.all_schemas[0]) == sorted(["bronze", "silver"]) + + +def test_multi_hybrid(mocker): + context = Context( + paths=["examples/multi_hybrid/dbt_repo", "examples/multi_hybrid/sqlmesh_repo"] + ) + context._new_state_sync().reset(default_catalog=context.default_catalog) + plan = context.plan_builder().build() + + assert len(plan.new_snapshots) == 5 + assert context.dag.roots == {'"memory"."dbt_repo"."e"'} + assert context.dag.graph['"memory"."dbt_repo"."c"'] == {'"memory"."sqlmesh_repo"."b"'} + assert context.dag.graph['"memory"."sqlmesh_repo"."b"'] == {'"memory"."sqlmesh_repo"."a"'} + assert context.dag.graph['"memory"."sqlmesh_repo"."a"'] == {'"memory"."dbt_repo"."e"'} + assert context.dag.downstream('"memory"."dbt_repo"."e"') == [ + '"memory"."sqlmesh_repo"."a"', + '"memory"."sqlmesh_repo"."b"', + '"memory"."dbt_repo"."c"', + '"memory"."dbt_repo"."d"', + ] + + sqlmesh_model_a = context.get_model("sqlmesh_repo.a") + dbt_model_c = context.get_model("dbt_repo.c") + assert sqlmesh_model_a.project == "sqlmesh_repo" + + sqlmesh_rendered = ( + 'SELECT "e"."col_a" AS "col_a", "e"."col_b" AS "col_b" FROM "memory"."dbt_repo"."e" AS "e"' + ) + dbt_rendered = 'SELECT DISTINCT ROUND(CAST(("b"."col_a" / NULLIF(100, 0)) AS DECIMAL(16, 2)), 2) AS "rounded_col_a" FROM "memory"."sqlmesh_repo"."b" AS "b"' + assert sqlmesh_model_a.render_query().sql() == sqlmesh_rendered + assert dbt_model_c.render_query().sql() == dbt_rendered + + context.apply(plan) + validate_apply_basics(context, c.PROD, plan.snapshots.values()) + + +def test_engine_adapters_multi_repo_all_gateways_gathered(copy_to_temp_path): + paths = copy_to_temp_path("examples/multi") + repo_1_path = paths[0] / "repo_1" + repo_2_path = paths[0] / "repo_2" + + # Add an extra gateway to repo_2's config + repo_2_config_path = repo_2_path / "config.yaml" + config_content = repo_2_config_path.read_text() + + modified_config = config_content.replace( + "default_gateway: local", + dedent(""" + extra: + connection: + type: duckdb + database: extra.duckdb + + default_gateway: local + """), + ) + + repo_2_config_path.write_text(modified_config) + + # Create context with both repos but using the repo_1 path first + context = Context( + paths=(repo_1_path, repo_2_path), + gateway="memory", + ) + + # Verify all gateways from both repos are present + gathered_gateways = context.engine_adapters.keys() + expected_gateways = {"local", "memory", "extra"} + assert gathered_gateways == expected_gateways diff --git a/tests/core/integration/test_plan_options.py b/tests/core/integration/test_plan_options.py new file mode 100644 index 0000000000..52cd215cc5 --- /dev/null +++ b/tests/core/integration/test_plan_options.py @@ -0,0 +1,478 @@ +from __future__ import annotations + +import typing as t +import pytest +from sqlmesh.core.console import ( + set_console, + get_console, + TerminalConsole, +) +import time_machine + +from sqlmesh.core import dialect as d +from sqlmesh.core.console import get_console +from sqlmesh.core.model import ( + SqlModel, + load_sql_based_model, +) +from sqlmesh.core.plan import SnapshotIntervals +from sqlmesh.core.snapshot import ( + SnapshotChangeCategory, +) +from sqlmesh.utils.date import to_datetime, to_timestamp +from sqlmesh.utils.errors import ( + NoChangesPlanError, +) +from tests.core.integration.utils import ( + add_projection_to_model, +) + +pytestmark = pytest.mark.slow + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_empty_backfill(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + plan = context.plan_builder("prod", skip_tests=True, empty_backfill=True).build() + assert plan.missing_intervals + assert plan.empty_backfill + assert not plan.requires_backfill + + context.apply(plan) + + for model in context.models.values(): + if model.is_seed or model.kind.is_symbolic: + continue + row_num = context.engine_adapter.fetchone(f"SELECT COUNT(*) FROM {model.name}")[0] + assert row_num == 0 + + plan = context.plan_builder("prod", skip_tests=True).build() + assert not plan.requires_backfill + assert not plan.has_changes + assert not plan.missing_intervals + + snapshots = plan.snapshots + for snapshot in snapshots.values(): + if not snapshot.intervals: + continue + assert snapshot.intervals[-1][1] <= to_timestamp("2023-01-08") + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_empty_backfill_new_model(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + new_model = load_sql_based_model( + d.parse( + """ + MODEL ( + name memory.sushi.new_model, + kind FULL, + cron '0 8 * * *', + start '2023-01-01', + ); + + SELECT 1 AS one; + """ + ) + ) + new_model_name = context.upsert_model(new_model).fqn + + with time_machine.travel("2023-01-09 00:00:00 UTC"): + plan = context.plan_builder("dev", skip_tests=True, empty_backfill=True).build() + assert plan.end == to_datetime("2023-01-09") + assert plan.missing_intervals + assert plan.empty_backfill + assert not plan.requires_backfill + + context.apply(plan) + + for model in context.models.values(): + if model.is_seed or model.kind.is_symbolic: + continue + row_num = context.engine_adapter.fetchone(f"SELECT COUNT(*) FROM sushi__dev.new_model")[ + 0 + ] + assert row_num == 0 + + plan = context.plan_builder("prod", skip_tests=True).build() + assert not plan.requires_backfill + assert not plan.missing_intervals + + snapshots = plan.snapshots + for snapshot in snapshots.values(): + if not snapshot.intervals: + continue + elif snapshot.name == new_model_name: + assert snapshot.intervals[-1][1] == to_timestamp("2023-01-09") + else: + assert snapshot.intervals[-1][1] <= to_timestamp("2023-01-08") + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_plan_explain(init_and_plan_context: t.Callable): + old_console = get_console() + set_console(TerminalConsole()) + + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + waiter_revenue_by_day_model = context.get_model("sushi.waiter_revenue_by_day") + waiter_revenue_by_day_model = add_projection_to_model( + t.cast(SqlModel, waiter_revenue_by_day_model) + ) + context.upsert_model(waiter_revenue_by_day_model) + + waiter_revenue_by_day_snapshot = context.get_snapshot(waiter_revenue_by_day_model.name) + top_waiters_snapshot = context.get_snapshot("sushi.top_waiters") + + common_kwargs = dict(skip_tests=True, no_prompts=True, explain=True) + + # For now just making sure the plan doesn't error + context.plan("dev", **common_kwargs) + context.plan("dev", **common_kwargs, skip_backfill=True) + context.plan("dev", **common_kwargs, empty_backfill=True) + context.plan("dev", **common_kwargs, forward_only=True, enable_preview=True) + context.plan("prod", **common_kwargs) + context.plan("prod", **common_kwargs, forward_only=True) + context.plan("prod", **common_kwargs, restate_models=[waiter_revenue_by_day_model.name]) + + set_console(old_console) + + # Make sure that the now changes were actually applied + for target_env in ("dev", "prod"): + plan = context.plan_builder(target_env, skip_tests=True).build() + assert plan.has_changes + assert plan.missing_intervals + assert plan.directly_modified == {waiter_revenue_by_day_snapshot.snapshot_id} + assert len(plan.new_snapshots) == 2 + assert {s.snapshot_id for s in plan.new_snapshots} == { + waiter_revenue_by_day_snapshot.snapshot_id, + top_waiters_snapshot.snapshot_id, + } + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_plan_ignore_cron( + init_and_plan_context: t.Callable, +): + context, _ = init_and_plan_context("examples/sushi") + + expressions = d.parse( + f""" + MODEL ( + name memory.sushi.test_allow_partials, + kind INCREMENTAL_UNMANAGED, + allow_partials true, + start '2023-01-01', + ); + + SELECT @end_ts AS end_ts + """ + ) + model = load_sql_based_model(expressions) + + context.upsert_model(model) + context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) + + assert ( + context.engine_adapter.fetchone("SELECT MAX(end_ts) FROM memory.sushi.test_allow_partials")[ + 0 + ] + == "2023-01-07 23:59:59.999999" + ) + + plan_no_ignore_cron = context.plan_builder( + "prod", run=True, ignore_cron=False, skip_tests=True + ).build() + assert not plan_no_ignore_cron.missing_intervals + + plan = context.plan_builder("prod", run=True, ignore_cron=True, skip_tests=True).build() + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=context.get_snapshot(model, raise_if_missing=True).snapshot_id, + intervals=[ + (to_timestamp("2023-01-08"), to_timestamp("2023-01-08 15:00:00")), + ], + ) + ] + context.apply(plan) + + assert ( + context.engine_adapter.fetchone("SELECT MAX(end_ts) FROM memory.sushi.test_allow_partials")[ + 0 + ] + == "2023-01-08 14:59:59.999999" + ) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_plan_with_run( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + with time_machine.travel("2023-01-09 00:00:00 UTC"): + plan = context.plan(run=True) + assert plan.has_changes + assert plan.missing_intervals + + context.apply(plan) + + snapshots = context.state_sync.state_sync.get_snapshots(context.snapshots.values()) + assert {s.name: s.intervals[0][1] for s in snapshots.values() if s.intervals} == { + '"memory"."sushi"."waiter_revenue_by_day"': to_timestamp("2023-01-09"), + '"memory"."sushi"."order_items"': to_timestamp("2023-01-09"), + '"memory"."sushi"."orders"': to_timestamp("2023-01-09"), + '"memory"."sushi"."items"': to_timestamp("2023-01-09"), + '"memory"."sushi"."customer_revenue_lifetime"': to_timestamp("2023-01-09"), + '"memory"."sushi"."customer_revenue_by_day"': to_timestamp("2023-01-09"), + '"memory"."sushi"."latest_order"': to_timestamp("2023-01-09"), + '"memory"."sushi"."waiter_names"': to_timestamp("2023-01-08"), + '"memory"."sushi"."raw_marketing"': to_timestamp("2023-01-09"), + '"memory"."sushi"."marketing"': to_timestamp("2023-01-09"), + '"memory"."sushi"."waiter_as_customer_by_day"': to_timestamp("2023-01-09"), + '"memory"."sushi"."top_waiters"': to_timestamp("2023-01-09"), + '"memory"."raw"."demographics"': to_timestamp("2023-01-09"), + "assert_item_price_above_zero": to_timestamp("2023-01-09"), + '"memory"."sushi"."active_customers"': to_timestamp("2023-01-09"), + '"memory"."sushi"."customers"': to_timestamp("2023-01-09"), + '"memory"."sushi"."count_customers_active"': to_timestamp("2023-01-09"), + '"memory"."sushi"."count_customers_inactive"': to_timestamp("2023-01-09"), + } + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_select_models(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Modify 2 models. + model = context.get_model("sushi.waiter_revenue_by_day") + kwargs = { + **model.dict(), + # Make a breaking change. + "query": model.query.order_by("waiter_id"), # type: ignore + } + context.upsert_model(SqlModel.parse_obj(kwargs)) + + model = context.get_model("sushi.customer_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + expected_intervals = [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ] + + waiter_revenue_by_day_snapshot_id = context.get_snapshot( + "sushi.waiter_revenue_by_day", raise_if_missing=True + ).snapshot_id + + # Select one of the modified models. + plan_builder = context.plan_builder( + "dev", select_models=["*waiter_revenue_by_day"], skip_tests=True + ) + snapshot = plan_builder._context_diff.snapshots[waiter_revenue_by_day_snapshot_id] + plan_builder.set_choice(snapshot, SnapshotChangeCategory.BREAKING) + plan = plan_builder.build() + + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=waiter_revenue_by_day_snapshot_id, + intervals=expected_intervals, + ), + ] + + context.apply(plan) + + dev_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" + ) + assert len(dev_df) == 7 + + # Make sure that we only create a view for the selected model. + schema_objects = context.engine_adapter.get_data_objects("sushi__dev") + assert len(schema_objects) == 1 + assert schema_objects[0].name == "waiter_revenue_by_day" + + # Validate the other modified model. + assert not context.get_snapshot("sushi.customer_revenue_by_day").change_category + assert not context.get_snapshot("sushi.customer_revenue_by_day").version + + # Validate the downstream model. + assert not context.engine_adapter.table_exists( + context.get_snapshot("sushi.top_waiters").table_name() + ) + assert not context.engine_adapter.table_exists( + context.get_snapshot("sushi.top_waiters").table_name(False) + ) + + # Make sure that tables are created when deploying to prod. + plan = context.plan("prod", skip_tests=True) + context.apply(plan) + assert context.engine_adapter.table_exists( + context.get_snapshot("sushi.top_waiters").table_name() + ) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_select_models_for_backfill(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + expected_intervals = [ + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ] + + plan = context.plan_builder( + "dev", backfill_models=["+*waiter_revenue_by_day"], skip_tests=True + ).build() + + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=context.get_snapshot("sushi.items", raise_if_missing=True).snapshot_id, + intervals=expected_intervals, + ), + SnapshotIntervals( + snapshot_id=context.get_snapshot( + "sushi.order_items", raise_if_missing=True + ).snapshot_id, + intervals=expected_intervals, + ), + SnapshotIntervals( + snapshot_id=context.get_snapshot("sushi.orders", raise_if_missing=True).snapshot_id, + intervals=expected_intervals, + ), + SnapshotIntervals( + snapshot_id=context.get_snapshot( + "sushi.waiter_revenue_by_day", raise_if_missing=True + ).snapshot_id, + intervals=expected_intervals, + ), + ] + + context.apply(plan) + + dev_df = context.engine_adapter.fetchdf( + "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" + ) + assert len(dev_df) == 1 + + schema_objects = context.engine_adapter.get_data_objects("sushi__dev") + assert {o.name for o in schema_objects} == { + "items", + "order_items", + "orders", + "waiter_revenue_by_day", + } + + assert not context.engine_adapter.table_exists( + context.get_snapshot("sushi.customer_revenue_by_day").table_name() + ) + + # Make sure that tables are created when deploying to prod. + plan = context.plan("prod") + context.apply(plan) + assert context.engine_adapter.table_exists( + context.get_snapshot("sushi.customer_revenue_by_day").table_name() + ) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_select_unchanged_model_for_backfill(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # Modify 2 models. + model = context.get_model("sushi.waiter_revenue_by_day") + kwargs = { + **model.dict(), + # Make a breaking change. + "query": d.parse_one( + f"{model.query.sql(dialect='duckdb')} ORDER BY waiter_id", dialect="duckdb" + ), + } + context.upsert_model(SqlModel.parse_obj(kwargs)) + + model = context.get_model("sushi.customer_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + expected_intervals = [ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ] + + waiter_revenue_by_day_snapshot_id = context.get_snapshot( + "sushi.waiter_revenue_by_day", raise_if_missing=True + ).snapshot_id + + # Select one of the modified models. + plan_builder = context.plan_builder( + "dev", select_models=["*waiter_revenue_by_day"], skip_tests=True + ) + snapshot = plan_builder._context_diff.snapshots[waiter_revenue_by_day_snapshot_id] + plan_builder.set_choice(snapshot, SnapshotChangeCategory.BREAKING) + plan = plan_builder.build() + + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=waiter_revenue_by_day_snapshot_id, + intervals=expected_intervals, + ), + ] + + context.apply(plan) + + # Make sure that we only create a view for the selected model. + schema_objects = context.engine_adapter.get_data_objects("sushi__dev") + assert {o.name for o in schema_objects} == {"waiter_revenue_by_day"} + + # Now select a model downstream from the previously modified one in order to backfill it. + plan = context.plan_builder("dev", select_models=["*top_waiters"], skip_tests=True).build() + + assert not plan.has_changes + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=context.get_snapshot( + "sushi.top_waiters", raise_if_missing=True + ).snapshot_id, + intervals=expected_intervals, + ), + ] + + context.apply(plan) + + # Make sure that a view has been created for the downstream selected model. + schema_objects = context.engine_adapter.get_data_objects("sushi__dev") + assert {o.name for o in schema_objects} == {"waiter_revenue_by_day", "top_waiters"} + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_create_environment_no_changes_with_selector(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + with pytest.raises(NoChangesPlanError): + context.plan_builder("dev").build() + + plan = context.plan_builder("dev", select_models=["*top_waiters"]).build() + assert not plan.missing_intervals + context.apply(plan) + + schema_objects = context.engine_adapter.get_data_objects("sushi__dev") + assert {o.name for o in schema_objects} == {"top_waiters"} diff --git a/tests/core/integration/test_restatement.py b/tests/core/integration/test_restatement.py new file mode 100644 index 0000000000..a00d8d7ab5 --- /dev/null +++ b/tests/core/integration/test_restatement.py @@ -0,0 +1,1882 @@ +from __future__ import annotations + +import typing as t +import pandas as pd # noqa: TID253 +import pytest +from pathlib import Path +from sqlmesh.core.console import ( + MarkdownConsole, + set_console, + get_console, + CaptureTerminalConsole, +) +import time_machine +from sqlglot import exp +import re +from concurrent.futures import ThreadPoolExecutor, TimeoutError +import time +import queue + +from sqlmesh.core import constants as c +from sqlmesh.core.config import ( + Config, + GatewayConfig, + ModelDefaultsConfig, + DuckDBConnectionConfig, +) +from sqlmesh.core.context import Context +from sqlmesh.core.model import ( + IncrementalByTimeRangeKind, + IncrementalUnmanagedKind, + SqlModel, +) +from sqlmesh.core.plan import SnapshotIntervals +from sqlmesh.core.snapshot import ( + Snapshot, + SnapshotId, +) +from sqlmesh.utils.date import to_timestamp +from sqlmesh.utils.errors import ( + ConflictingPlanError, +) +from tests.core.integration.utils import add_projection_to_model + +pytestmark = pytest.mark.slow + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_restatement_plan_ignores_changes(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + restated_snapshot = context.get_snapshot("sushi.top_waiters") + + # Simulate a change. + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + plan = context.plan_builder(restate_models=["sushi.top_waiters"]).build() + assert plan.snapshots != context.snapshots + + assert not plan.directly_modified + assert not plan.has_changes + assert not plan.new_snapshots + assert plan.requires_backfill + assert plan.restatements == { + restated_snapshot.snapshot_id: (to_timestamp("2023-01-01"), to_timestamp("2023-01-09")) + } + assert plan.missing_intervals == [ + SnapshotIntervals( + snapshot_id=restated_snapshot.snapshot_id, + intervals=[ + (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), + (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), + (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), + (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), + (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), + (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), + (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), + ], + ) + ] + + context.apply(plan) + + +@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, +): + context, _ = init_and_plan_context("examples/sushi") + + # Change kind to incremental unmanaged + model = context.get_model("sushi.waiter_revenue_by_day") + previous_kind = model.kind.copy(update={"forward_only": True}) + assert isinstance(previous_kind, IncrementalByTimeRangeKind) + + model = model.copy( + update={ + "kind": IncrementalUnmanagedKind(), + "physical_version": "pinned_version_12345", + "partitioned_by_": [exp.column("event_date")], + } + ) + context.upsert_model(model) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Make some change and deploy it to both dev and prod environments + model = add_projection_to_model(t.cast(SqlModel, model)) + context.upsert_model(model) + context.plan("dev_a", auto_apply=True, no_prompts=True) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Change the kind back to incremental by time range and deploy to prod + model = model.copy(update={"kind": previous_kind}) + context.upsert_model(model) + context.plan("prod", auto_apply=True, no_prompts=True) + + # Restate the model and verify that the interval hasn't been expanded because of the old snapshot + # with the same version + context.plan( + restate_models=["sushi.waiter_revenue_by_day"], + start="2023-01-06", + end="2023-01-08", + auto_apply=True, + no_prompts=True, + ) + + assert ( + context.fetchdf( + "SELECT COUNT(*) AS cnt FROM sushi.waiter_revenue_by_day WHERE one IS NOT NULL AND event_date < '2023-01-06'" + )["cnt"][0] + == 0 + ) + plan = context.plan_builder("prod").build() + assert not plan.missing_intervals + + +def test_restatement_plan_hourly_with_downstream_daily_restates_correct_intervals(tmp_path: Path): + model_a = """ + MODEL ( + name test.a, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@hourly' + ); + + select account_id, ts from test.external_table; + """ + + model_b = """ + MODEL ( + name test.b, + kind FULL, + cron '@daily' + ); + + select account_id, ts from test.a; + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + for path, defn in {"a.sql": model_a, "b.sql": model_b}.items(): + with open(models_dir / path, "w") as f: + f.write(defn) + + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ctx = Context(paths=[tmp_path], config=config) + + engine_adapter = ctx.engine_adapter + engine_adapter.create_schema("test") + + # source data + df = pd.DataFrame( + { + "account_id": [1001, 1002, 1003, 1004], + "ts": [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ], + } + ) + columns_to_types = { + "account_id": exp.DataType.build("int"), + "ts": exp.DataType.build("timestamp"), + } + external_table = exp.table_(table="external_table", db="test", quoted=True) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) + engine_adapter.insert_append( + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types + ) + + # plan + apply + ctx.plan(auto_apply=True, no_prompts=True) + + def _dates_in_table(table_name: str) -> t.List[str]: + return [ + str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") + ] + + # verify initial state + for tbl in ["test.a", "test.b"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + + # restate A + engine_adapter.execute("delete from test.external_table where ts = '2024-01-01 01:30:00'") + ctx.plan( + restate_models=["test.a"], + start="2024-01-01 01:00:00", + end="2024-01-01 02:00:00", + auto_apply=True, + no_prompts=True, + ) + + # verify result + for tbl in ["test.a", "test.b"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ], f"Table {tbl} wasnt cleared" + + # Put some data + df = pd.DataFrame( + { + "account_id": [1001, 1002, 1003, 1004], + "ts": [ + "2024-01-01 01:30:00", + "2024-01-01 23:30:00", + "2024-01-02 03:30:00", + "2024-01-03 12:30:00", + ], + } + ) + engine_adapter.replace_query( + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types + ) + + # Restate A across a day boundary with the expectation that two day intervals in B are affected + ctx.plan( + restate_models=["test.a"], + start="2024-01-01 02:00:00", + end="2024-01-02 04:00:00", + auto_apply=True, + no_prompts=True, + ) + + for tbl in ["test.a", "test.b"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", # present already + # "2024-01-01 02:30:00", #removed in last restatement + "2024-01-01 23:30:00", # added in last restatement + "2024-01-02 03:30:00", # added in last restatement + ], f"Table {tbl} wasnt cleared" + + +def test_restatement_plan_respects_disable_restatements(tmp_path: Path): + model_a = """ + MODEL ( + name test.a, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01', + cron '@daily' + ); + + select account_id, ts from test.external_table; + """ + + model_b = """ + MODEL ( + name test.b, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts", + disable_restatement true, + ), + start '2024-01-01', + cron '@daily' + ); + + select account_id, ts from test.a; + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + for path, defn in {"a.sql": model_a, "b.sql": model_b}.items(): + with open(models_dir / path, "w") as f: + f.write(defn) + + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ctx = Context(paths=[tmp_path], config=config) + + engine_adapter = ctx.engine_adapter + engine_adapter.create_schema("test") + + # source data + df = pd.DataFrame( + { + "account_id": [1001, 1002, 1003, 1004], + "ts": [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ], + } + ) + columns_to_types = { + "account_id": exp.DataType.build("int"), + "ts": exp.DataType.build("timestamp"), + } + external_table = exp.table_(table="external_table", db="test", quoted=True) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) + engine_adapter.insert_append( + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types + ) + + # plan + apply + ctx.plan(auto_apply=True, no_prompts=True) + + def _dates_in_table(table_name: str) -> t.List[str]: + return [ + str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") + ] + + def get_snapshot_intervals(snapshot_id): + return list(ctx.state_sync.get_snapshots([snapshot_id]).values())[0].intervals + + # verify initial state + for tbl in ["test.a", "test.b"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + + # restate A and expect b to be ignored + starting_b_intervals = get_snapshot_intervals(ctx.snapshots['"memory"."test"."b"'].snapshot_id) + engine_adapter.execute("delete from test.external_table where ts = '2024-01-01 01:30:00'") + ctx.plan( + restate_models=["test.a"], + start="2024-01-01", + end="2024-01-02", + auto_apply=True, + no_prompts=True, + ) + + # verify A was changed and not b + assert _dates_in_table("test.a") == [ + "2024-01-01 00:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + assert _dates_in_table("test.b") == [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + + # Verify B intervals were not touched + b_intervals = get_snapshot_intervals(ctx.snapshots['"memory"."test"."b"'].snapshot_id) + assert starting_b_intervals == b_intervals + + +def test_restatement_plan_clears_correct_intervals_across_environments(tmp_path: Path): + model1 = """ + MODEL ( + name test.incremental_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "date" + ), + start '2024-01-01', + cron '@daily' + ); + + select account_id, date from test.external_table; + """ + + model2 = """ + MODEL ( + name test.downstream_of_incremental, + kind FULL + ); + + select account_id, date from test.incremental_model; + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + with open(models_dir / "model1.sql", "w") as f: + f.write(model1) + + with open(models_dir / "model2.sql", "w") as f: + f.write(model2) + + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ctx = Context(paths=[tmp_path], config=config) + + engine_adapter = ctx.engine_adapter + engine_adapter.create_schema("test") + + # source data + df = pd.DataFrame( + { + "account_id": [1001, 1002, 1003, 1004, 1005], + "name": ["foo", "bar", "baz", "bing", "bong"], + "date": ["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-04", "2024-01-05"], + } + ) + columns_to_types = { + "account_id": exp.DataType.build("int"), + "name": exp.DataType.build("varchar"), + "date": exp.DataType.build("date"), + } + external_table = exp.table_(table="external_table", db="test", quoted=True) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) + engine_adapter.insert_append( + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types + ) + + # first, create the prod models + ctx.plan(auto_apply=True, no_prompts=True) + assert engine_adapter.fetchone("select count(*) from test.incremental_model") == (5,) + assert engine_adapter.fetchone("select count(*) from test.downstream_of_incremental") == (5,) + assert not engine_adapter.table_exists("test__dev.incremental_model") + + # then, make a dev version + model1 = """ + MODEL ( + name test.incremental_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "date" + ), + start '2024-01-01', + cron '@daily' + ); + + select 1 as account_id, date from test.external_table; + """ + with open(models_dir / "model1.sql", "w") as f: + f.write(model1) + ctx.load() + + ctx.plan(environment="dev", auto_apply=True, no_prompts=True) + assert engine_adapter.table_exists("test__dev.incremental_model") + assert engine_adapter.fetchone("select count(*) from test__dev.incremental_model") == (5,) + + # drop some source data so when we restate the interval it essentially clears it which is easy to verify + engine_adapter.execute("delete from test.external_table where date = '2024-01-01'") + assert engine_adapter.fetchone("select count(*) from test.external_table") == (4,) + + # now, restate intervals in dev and verify prod is NOT affected + ctx.plan( + environment="dev", + start="2024-01-01", + end="2024-01-02", + restate_models=["test.incremental_model"], + auto_apply=True, + no_prompts=True, + ) + assert engine_adapter.fetchone("select count(*) from test.incremental_model") == (5,) + assert engine_adapter.fetchone( + "select count(*) from test.incremental_model where date = '2024-01-01'" + ) == (1,) + assert engine_adapter.fetchone("select count(*) from test__dev.incremental_model") == (4,) + assert engine_adapter.fetchone( + "select count(*) from test__dev.incremental_model where date = '2024-01-01'" + ) == (0,) + + # prod still should not be affected by a run because the restatement only happened in dev + ctx.run() + assert engine_adapter.fetchone("select count(*) from test.incremental_model") == (5,) + assert engine_adapter.fetchone( + "select count(*) from test.incremental_model where date = '2024-01-01'" + ) == (1,) + + # drop another interval from the source data + engine_adapter.execute("delete from test.external_table where date = '2024-01-02'") + + # now, restate intervals in prod and verify that dev IS affected + ctx.plan( + start="2024-01-01", + end="2024-01-03", + restate_models=["test.incremental_model"], + auto_apply=True, + no_prompts=True, + ) + assert engine_adapter.fetchone("select count(*) from test.incremental_model") == (3,) + assert engine_adapter.fetchone( + "select count(*) from test.incremental_model where date = '2024-01-01'" + ) == (0,) + assert engine_adapter.fetchone( + "select count(*) from test.incremental_model where date = '2024-01-02'" + ) == (0,) + assert engine_adapter.fetchone( + "select count(*) from test.incremental_model where date = '2024-01-03'" + ) == (1,) + + # dev not affected yet until `sqlmesh run` is run + assert engine_adapter.fetchone("select count(*) from test__dev.incremental_model") == (4,) + assert engine_adapter.fetchone( + "select count(*) from test__dev.incremental_model where date = '2024-01-01'" + ) == (0,) + assert engine_adapter.fetchone( + "select count(*) from test__dev.incremental_model where date = '2024-01-02'" + ) == (1,) + assert engine_adapter.fetchone( + "select count(*) from test__dev.incremental_model where date = '2024-01-03'" + ) == (1,) + + # the restatement plan for prod should have cleared dev intervals too, which means this `sqlmesh run` re-runs 2024-01-01 and 2024-01-02 + ctx.run(environment="dev") + assert engine_adapter.fetchone("select count(*) from test__dev.incremental_model") == (3,) + assert engine_adapter.fetchone( + "select count(*) from test__dev.incremental_model where date = '2024-01-01'" + ) == (0,) + assert engine_adapter.fetchone( + "select count(*) from test__dev.incremental_model where date = '2024-01-02'" + ) == (0,) + assert engine_adapter.fetchone( + "select count(*) from test__dev.incremental_model where date = '2024-01-03'" + ) == (1,) + + # the downstream full model should always reflect whatever the incremental model is showing + assert engine_adapter.fetchone("select count(*) from test.downstream_of_incremental") == (3,) + assert engine_adapter.fetchone("select count(*) from test__dev.downstream_of_incremental") == ( + 3, + ) + + +def test_prod_restatement_plan_clears_correct_intervals_in_derived_dev_tables(tmp_path: Path): + """ + Scenario: + I have models A[hourly] <- B[daily] <- C in prod + I create dev and add 2 new models D and E so that my dev DAG looks like A <- B <- C <- D[daily] <- E + I prod, I restate *one hour* of A + Outcome: + D and E should be restated in dev despite not being a part of prod + since B and D are daily, the whole day should be restated even though only 1hr of the upstream model was restated + """ + + model_a = """ + MODEL ( + name test.a, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@hourly' + ); + + select account_id, ts from test.external_table; + """ + + def _derived_full_model_def(name: str, upstream: str) -> str: + return f""" + MODEL ( + name test.{name}, + kind FULL + ); + + select account_id, ts from test.{upstream}; + """ + + def _derived_incremental_model_def(name: str, upstream: str) -> str: + return f""" + MODEL ( + name test.{name}, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ts + ), + cron '@daily' + ); + + select account_id, ts from test.{upstream} where ts between @start_ts and @end_ts; + """ + + model_b = _derived_incremental_model_def("b", upstream="a") + model_c = _derived_full_model_def("c", upstream="b") + + models_dir = tmp_path / "models" + models_dir.mkdir() + + for path, defn in {"a.sql": model_a, "b.sql": model_b, "c.sql": model_c}.items(): + with open(models_dir / path, "w") as f: + f.write(defn) + + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ctx = Context(paths=[tmp_path], config=config) + + engine_adapter = ctx.engine_adapter + engine_adapter.create_schema("test") + + # source data + df = pd.DataFrame( + { + "account_id": [1001, 1002, 1003, 1004], + "ts": [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ], + } + ) + columns_to_types = { + "account_id": exp.DataType.build("int"), + "ts": exp.DataType.build("timestamp"), + } + external_table = exp.table_(table="external_table", db="test", quoted=True) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) + engine_adapter.insert_append( + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types + ) + + # plan + apply A, B, C in prod + ctx.plan(auto_apply=True, no_prompts=True) + + # add D[daily], E in dev + model_d = _derived_incremental_model_def("d", upstream="c") + model_e = _derived_full_model_def("e", upstream="d") + + for path, defn in { + "d.sql": model_d, + "e.sql": model_e, + }.items(): + with open(models_dir / path, "w") as f: + f.write(defn) + + # plan + apply dev + ctx.load() + ctx.plan(environment="dev", auto_apply=True, no_prompts=True) + + def _dates_in_table(table_name: str) -> t.List[str]: + return [ + str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") + ] + + # verify initial state + for tbl in ["test.a", "test.b", "test.c", "test__dev.d", "test__dev.e"]: + assert engine_adapter.table_exists(tbl) + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + + for tbl in ["test.d", "test.e"]: + assert not engine_adapter.table_exists(tbl) + + # restate A in prod + engine_adapter.execute("delete from test.external_table where ts = '2024-01-01 01:30:00'") + ctx.plan( + restate_models=["test.a"], + start="2024-01-01 01:00:00", + end="2024-01-01 02:00:00", + auto_apply=True, + no_prompts=True, + ) + + # verify result + for tbl in ["test.a", "test.b", "test.c"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ], f"Table {tbl} wasnt cleared" + + # dev shouldnt have been affected yet + for tbl in ["test__dev.d", "test__dev.e"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ], f"Table {tbl} was prematurely cleared" + + # run dev to trigger the processing of the prod restatement + ctx.run(environment="dev") + + # data should now be cleared from dev + # note that D is a daily model, so clearing an hour interval from A should have triggered the full day in D + for tbl in ["test__dev.d", "test__dev.e"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ], f"Table {tbl} wasnt cleared" + + +def test_prod_restatement_plan_clears_unaligned_intervals_in_derived_dev_tables(tmp_path: Path): + """ + Scenario: + I have a model A[hourly] in prod + I create dev and add a model B[daily] + I prod, I restate *one hour* of A + + Outcome: + The whole day for B should be restated. The restatement plan for prod has no hints about B's cadence because + B only exists in dev and there are no other downstream models in prod that would cause the restatement intervals + to be widened. + + Therefore, this test checks that SQLMesh does the right thing when an interval is partially cleared + """ + + model_a = """ + MODEL ( + name test.a, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@hourly' + ); + + select account_id, ts from test.external_table; + """ + + model_b = """ + MODEL ( + name test.b, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ts + ), + cron '@daily' + ); + + select account_id, ts from test.a where ts between @start_ts and @end_ts; + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + with open(models_dir / "a.sql", "w") as f: + f.write(model_a) + + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ctx = Context(paths=[tmp_path], config=config) + + engine_adapter = ctx.engine_adapter + engine_adapter.create_schema("test") + + # source data + df = pd.DataFrame( + { + "account_id": [1001, 1002, 1003, 1004], + "ts": [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ], + } + ) + columns_to_types = { + "account_id": exp.DataType.build("int"), + "ts": exp.DataType.build("timestamp"), + } + external_table = exp.table_(table="external_table", db="test", quoted=True) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) + engine_adapter.insert_append( + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types + ) + + # plan + apply A[hourly] in prod + ctx.plan(auto_apply=True, no_prompts=True) + + # add B[daily] in dev + with open(models_dir / "b.sql", "w") as f: + f.write(model_b) + + # plan + apply dev + ctx.load() + ctx.plan(environment="dev", auto_apply=True, no_prompts=True) + + def _dates_in_table(table_name: str) -> t.List[str]: + return [ + str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") + ] + + # verify initial state + for tbl in ["test.a", "test__dev.b"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + + # restate A in prod + engine_adapter.execute("delete from test.external_table where ts = '2024-01-01 01:30:00'") + ctx.plan( + restate_models=["test.a"], + start="2024-01-01 01:00:00", + end="2024-01-01 02:00:00", + auto_apply=True, + no_prompts=True, + ) + + # verify result + assert _dates_in_table("test.a") == [ + "2024-01-01 00:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + + # dev shouldnt have been affected yet + assert _dates_in_table("test__dev.b") == [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + + # mess with A independently of SQLMesh to prove a whole day gets restated for B instead of just 1hr + snapshot_table_name = ctx.table_name("test.a", "dev") + engine_adapter.execute( + f"delete from {snapshot_table_name} where cast(ts as date) == '2024-01-01'" + ) + engine_adapter.execute( + f"insert into {snapshot_table_name} (account_id, ts) values (1007, '2024-01-02 01:30:00')" + ) + + assert _dates_in_table("test.a") == ["2024-01-02 00:30:00", "2024-01-02 01:30:00"] + + # run dev to trigger the processing of the prod restatement + ctx.run(environment="dev") + + # B should now have no data for 2024-01-01 + # To prove a single day was restated vs the whole model, it also shouldnt have the '2024-01-02 01:30:00' record + assert _dates_in_table("test__dev.b") == ["2024-01-02 00:30:00"] + + +def test_prod_restatement_plan_causes_dev_intervals_to_be_processed_in_next_dev_plan( + tmp_path: Path, +): + """ + Scenario: + I have a model A[hourly] in prod + I create dev and add a model B[daily] + I prod, I restate *one hour* of A + In dev, I run a normal plan instead of a cadence run + + Outcome: + The whole day for B should be restated as part of a normal plan + """ + + model_a = """ + MODEL ( + name test.a, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@hourly' + ); + + select account_id, ts from test.external_table; + """ + + model_b = """ + MODEL ( + name test.b, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ts + ), + cron '@daily' + ); + + select account_id, ts from test.a where ts between @start_ts and @end_ts; + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + with open(models_dir / "a.sql", "w") as f: + f.write(model_a) + + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ctx = Context(paths=[tmp_path], config=config) + + engine_adapter = ctx.engine_adapter + engine_adapter.create_schema("test") + + # source data + df = pd.DataFrame( + { + "account_id": [1001, 1002, 1003, 1004], + "ts": [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ], + } + ) + columns_to_types = { + "account_id": exp.DataType.build("int"), + "ts": exp.DataType.build("timestamp"), + } + external_table = exp.table_(table="external_table", db="test", quoted=True) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) + engine_adapter.insert_append( + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types + ) + + # plan + apply A[hourly] in prod + ctx.plan(auto_apply=True, no_prompts=True) + + # add B[daily] in dev + with open(models_dir / "b.sql", "w") as f: + f.write(model_b) + + # plan + apply dev + ctx.load() + ctx.plan(environment="dev", auto_apply=True, no_prompts=True) + + def _dates_in_table(table_name: str) -> t.List[str]: + return [ + str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") + ] + + # verify initial state + for tbl in ["test.a", "test__dev.b"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + + # restate A in prod + engine_adapter.execute("delete from test.external_table where ts = '2024-01-01 01:30:00'") + ctx.plan( + restate_models=["test.a"], + start="2024-01-01 01:00:00", + end="2024-01-01 02:00:00", + auto_apply=True, + no_prompts=True, + ) + + # verify result + assert _dates_in_table("test.a") == [ + "2024-01-01 00:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + + # dev shouldnt have been affected yet + assert _dates_in_table("test__dev.b") == [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + + # plan dev which should trigger the missing intervals to get repopulated + ctx.plan(environment="dev", auto_apply=True, no_prompts=True) + + # dev should have the restated data + for tbl in ["test.a", "test__dev.b"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ] + + +def test_prod_restatement_plan_causes_dev_intervals_to_be_widened_on_full_restatement_only_model( + tmp_path, +): + """ + Scenario: + I have am INCREMENTAL_BY_TIME_RANGE model A[daily] in prod + I create dev and add a INCREMENTAL_BY_UNIQUE_KEY model B (which supports full restatement only) + I prod, I restate one day of A which should cause intervals in dev to be cleared (but not processed) + In dev, I run a plan + + Outcome: + In the dev plan, the entire model for B should be rebuilt because it does not support partial restatement + """ + + model_a = """ + MODEL ( + name test.a, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@daily' + ); + + select account_id, ts from test.external_table where ts between @start_ts and @end_ts; + """ + + model_b = """ + MODEL ( + name test.b, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key (account_id, ts) + ), + cron '@daily' + ); + + select account_id, ts from test.a where ts between @start_ts and @end_ts; + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + with open(models_dir / "a.sql", "w") as f: + f.write(model_a) + + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ctx = Context(paths=[tmp_path], config=config) + + engine_adapter = ctx.engine_adapter + engine_adapter.create_schema("test") + + # source data + df = pd.DataFrame( + { + "account_id": [1001, 1002, 1003, 1004], + "ts": [ + "2024-01-01 00:30:00", + "2024-01-02 01:30:00", + "2024-01-03 02:30:00", + "2024-01-04 00:30:00", + ], + } + ) + columns_to_types = { + "account_id": exp.DataType.build("int"), + "ts": exp.DataType.build("timestamp"), + } + external_table = exp.table_(table="external_table", db="test", quoted=True) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) + engine_adapter.insert_append( + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types + ) + + # plan + apply A[daily] in prod + ctx.plan(auto_apply=True) + + # add B[daily] in dev + with open(models_dir / "b.sql", "w") as f: + f.write(model_b) + + # plan + apply dev + ctx.load() + ctx.plan(environment="dev", auto_apply=True) + + def _dates_in_table(table_name: str) -> t.List[str]: + return [ + str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") + ] + + # verify initial state + for tbl in ["test.a", "test__dev.b"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-02 01:30:00", + "2024-01-03 02:30:00", + "2024-01-04 00:30:00", + ] + + # restate A in prod + engine_adapter.execute("delete from test.external_table where ts = '2024-01-02 01:30:00'") + ctx.plan( + restate_models=["test.a"], + start="2024-01-02 00:00:00", + end="2024-01-03 00:00:00", + auto_apply=True, + no_prompts=True, + ) + + # verify result + assert _dates_in_table("test.a") == [ + "2024-01-01 00:30:00", + "2024-01-03 02:30:00", + "2024-01-04 00:30:00", + ] + + # dev shouldnt have been affected yet + assert _dates_in_table("test__dev.b") == [ + "2024-01-01 00:30:00", + "2024-01-02 01:30:00", + "2024-01-03 02:30:00", + "2024-01-04 00:30:00", + ] + + # plan dev which should trigger the missing intervals to get repopulated + ctx.plan(environment="dev", auto_apply=True) + + # dev should have fully refreshed + # this is proven by the fact that INCREMENTAL_BY_UNIQUE_KEY cant propagate deletes, so if the + # model was not fully rebuilt, the deleted record would still be present + for tbl in ["test.a", "test__dev.b"]: + assert _dates_in_table(tbl) == [ + "2024-01-01 00:30:00", + "2024-01-03 02:30:00", + "2024-01-04 00:30:00", + ] + + +def test_prod_restatement_plan_missing_model_in_dev( + tmp_path: Path, +): + """ + Scenario: + I have a model B in prod but only model A in dev + I restate B in prod + + Outcome: + The A model should be ignore and the plan shouldn't fail + """ + + model_a = """ + MODEL ( + name test.a, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@hourly' + ); + + select account_id, ts from test.external_table; + """ + + model_b = """ + MODEL ( + name test.b, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ts + ), + cron '@daily' + ); + + select account_id, ts from test.external_table where ts between @start_ts and @end_ts; + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + with open(models_dir / "a.sql", "w") as f: + f.write(model_a) + + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + ctx = Context(paths=[tmp_path], config=config) + + engine_adapter = ctx.engine_adapter + engine_adapter.create_schema("test") + + # source data + df = pd.DataFrame( + { + "account_id": [1001, 1002, 1003, 1004], + "ts": [ + "2024-01-01 00:30:00", + "2024-01-01 01:30:00", + "2024-01-01 02:30:00", + "2024-01-02 00:30:00", + ], + } + ) + columns_to_types = { + "account_id": exp.DataType.build("int"), + "ts": exp.DataType.build("timestamp"), + } + external_table = exp.table_(table="external_table", db="test", quoted=True) + engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) + engine_adapter.insert_append( + table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types + ) + + # plan + apply A[hourly] in dev + ctx.plan("dev", auto_apply=True, no_prompts=True) + + # add B[daily] in prod and remove A + with open(models_dir / "b.sql", "w") as f: + f.write(model_b) + Path(models_dir / "a.sql").unlink() + + # plan + apply dev + ctx.load() + ctx.plan(auto_apply=True, no_prompts=True) + + # restate B in prod + ctx.plan( + restate_models=["test.b"], + start="2024-01-01", + end="2024-01-02", + auto_apply=True, + no_prompts=True, + ) + + +def test_prod_restatement_plan_includes_related_unpromoted_snapshots(tmp_path: Path): + """ + Scenario: + - I have models A <- B in prod + - I have models A <- B <- C in dev + - Both B and C have gone through a few iterations in dev so multiple snapshot versions exist + for them but not all of them are promoted / active + - I restate A in prod + + Outcome: + - Intervals should be cleared for all of the versions of B and C, regardless + of if they are active in any particular environment, in case they ever get made + active + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + (models_dir / "a.sql").write_text(""" + MODEL ( + name test.a, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@daily' + ); + + select 1 as a, now() as ts; + """) + + (models_dir / "b.sql").write_text(""" + MODEL ( + name test.b, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@daily' + ); + + select a, ts from test.a + """) + + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01")) + ctx = Context(paths=[tmp_path], config=config) + + def _all_snapshots() -> t.Dict[SnapshotId, Snapshot]: + all_snapshot_ids = [ + SnapshotId(name=name, identifier=identifier) + for (name, identifier) in ctx.state_sync.state_sync.engine_adapter.fetchall( # type: ignore + "select name, identifier from sqlmesh._snapshots" + ) + ] + return ctx.state_sync.get_snapshots(all_snapshot_ids) + + # plan + apply prod + ctx.plan(environment="prod", auto_apply=True) + assert len(_all_snapshots()) == 2 + + # create dev with new version of B + (models_dir / "b.sql").write_text(""" + MODEL ( + name test.b, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@daily' + ); + + select a, ts, 'b dev 1' as change from test.a + """) + + ctx.load() + ctx.plan(environment="dev", auto_apply=True) + assert len(_all_snapshots()) == 3 + + # update B (new version) and create C + (models_dir / "b.sql").write_text(""" + MODEL ( + name test.b, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column "ts" + ), + start '2024-01-01 00:00:00', + cron '@daily' + ); + + select a, ts, 'b dev 2' as change from test.a + """) + + (models_dir / "c.sql").write_text(""" + MODEL ( + name test.c, + kind FULL, + cron '@daily' + ); + + select *, 'c initial' as val from test.b + """) + + ctx.load() + ctx.plan(environment="dev", auto_apply=True) + assert len(_all_snapshots()) == 5 + + # update C (new version), create D (unrelated) + (models_dir / "c.sql").write_text(""" + MODEL ( + name test.c, + kind FULL, + cron '@daily' + ); + + select *, 'c updated' as val from test.b + """) + + (models_dir / "d.sql").write_text(""" + MODEL ( + name test.d, + cron '@daily' + ); + + select 1 as unrelated + """) + + ctx.load() + ctx.plan(environment="dev", auto_apply=True) + all_snapshots_prior_to_restatement = _all_snapshots() + assert len(all_snapshots_prior_to_restatement) == 7 + + def _snapshot_instances(lst: t.Dict[SnapshotId, Snapshot], name_match: str) -> t.List[Snapshot]: + return [s for s_id, s in lst.items() if name_match in s_id.name] + + # verify initial state + + # 1 instance of A (prod) + assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"a"')) == 1 + + # 3 instances of B (original in prod + 2 updates in dev) + assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"b"')) == 3 + + # 2 instances of C (initial + update in dev) + assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"c"')) == 2 + + # 1 instance of D (initial - dev) + assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"d"')) == 1 + + # restate A in prod + ctx.plan(environment="prod", restate_models=['"memory"."test"."a"'], auto_apply=True) + + all_snapshots_after_restatement = _all_snapshots() + + # All versions of B and C in dev should have had intervals cleared + # D in dev should not be touched and A + B in prod shoud also not be touched + a = _snapshot_instances(all_snapshots_after_restatement, '"a"') + assert len(a) == 1 + + b = _snapshot_instances(all_snapshots_after_restatement, '"b"') + # the 1 B instance in prod should be populated and 2 in dev (1 active) should be cleared + assert len(b) == 3 + assert len([s for s in b if not s.intervals]) == 2 + + c = _snapshot_instances(all_snapshots_after_restatement, '"c"') + # the 2 instances of C in dev (1 active) should be cleared + assert len(c) == 2 + assert len([s for s in c if not s.intervals]) == 2 + + d = _snapshot_instances(all_snapshots_after_restatement, '"d"') + # D should not be touched since it's in no way downstream of A in prod + assert len(d) == 1 + assert d[0].intervals + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_dev_restatement_of_prod_model(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + model = context.get_model("sushi.waiter_revenue_by_day") + context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) + + context.plan("dev", auto_apply=True, no_prompts=True, skip_tests=True) + + restatement_plan = context.plan_builder("dev", restate_models=["*"]).build() + assert set(restatement_plan.restatements) == { + context.get_snapshot("sushi.waiter_revenue_by_day").snapshot_id, + context.get_snapshot("sushi.top_waiters").snapshot_id, + } + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_restatement_of_full_model_with_start(init_and_plan_context: t.Callable): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + restatement_plan = context.plan( + restate_models=["sushi.customers"], + start="2023-01-07", + auto_apply=True, + no_prompts=True, + ) + + sushi_customer_interval = restatement_plan.restatements[ + context.get_snapshot("sushi.customers").snapshot_id + ] + assert sushi_customer_interval == (to_timestamp("2023-01-01"), to_timestamp("2023-01-09")) + waiter_by_day_interval = restatement_plan.restatements[ + context.get_snapshot("sushi.waiter_as_customer_by_day").snapshot_id + ] + assert waiter_by_day_interval == (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")) + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_restatement_should_not_override_environment_statements(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + context.config.before_all = ["SELECT 'test_before_all';", *context.config.before_all] + context.load() + + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + + prod_env_statements = context.state_reader.get_environment_statements(c.PROD) + assert prod_env_statements[0].before_all[0] == "SELECT 'test_before_all';" + + context.plan( + restate_models=["sushi.waiter_revenue_by_day"], + start="2023-01-07", + auto_apply=True, + no_prompts=True, + ) + + prod_env_statements = context.state_reader.get_environment_statements(c.PROD) + assert prod_env_statements[0].before_all[0] == "SELECT 'test_before_all';" + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_restatement_shouldnt_backfill_beyond_prod_intervals(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + model = context.get_model("sushi.top_waiters") + context.upsert_model(SqlModel.parse_obj({**model.dict(), "cron": "@hourly"})) + + context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) + context.run() + + with time_machine.travel("2023-01-09 02:00:00 UTC"): + # It's time to backfill the waiter_revenue_by_day model but it hasn't run yet + restatement_plan = context.plan( + restate_models=["sushi.waiter_revenue_by_day"], + no_prompts=True, + skip_tests=True, + ) + intervals_by_id = {i.snapshot_id: i for i in restatement_plan.missing_intervals} + # Make sure the intervals don't go beyond the prod intervals + assert intervals_by_id[context.get_snapshot("sushi.top_waiters").snapshot_id].intervals[-1][ + 1 + ] == to_timestamp("2023-01-08 15:00:00 UTC") + assert intervals_by_id[ + context.get_snapshot("sushi.waiter_revenue_by_day").snapshot_id + ].intervals[-1][1] == to_timestamp("2023-01-08 00:00:00 UTC") + + +def test_restatement_plan_interval_external_visibility(tmp_path: Path): + """ + Scenario: + - `prod` environment exists, models A <- B + - `dev` environment created, models A <- B(dev) <- C (dev) + - Restatement plan is triggered against `prod` for model A + - During restatement, a new dev environment `dev_2` is created with a new version of B(dev_2) + + Outcome: + - At no point are the prod_intervals considered "missing" from state for A + - The intervals for B(dev) and C(dev) are cleared + - The intervals for B(dev_2) are also cleared even though the environment didnt exist at the time the plan was started, + because they are based on the data from a partially restated version of A + """ + + models_dir = tmp_path / "models" + models_dir.mkdir() + + lock_file_path = tmp_path / "test.lock" # python model blocks while this file is present + + evaluation_lock_file_path = ( + tmp_path / "evaluation.lock" + ) # python model creates this file if it's in the wait loop and deletes it once done + + # Note: to make execution block so we can test stuff, we use a Python model that blocks until it no longer detects the presence of a file + (models_dir / "model_a.py").write_text(f""" +from sqlmesh.core.model import model +from sqlmesh.core.macros import MacroEvaluator + +@model( + "test.model_a", + is_sql=True, + kind="FULL" +) +def entrypoint(evaluator: MacroEvaluator) -> str: + from pathlib import Path + import time + + if evaluator.runtime_stage == 'evaluating': + while True: + if Path("{str(lock_file_path)}").exists(): + Path("{str(evaluation_lock_file_path)}").touch() + print("lock exists; sleeping") + time.sleep(2) + else: + Path("{str(evaluation_lock_file_path)}").unlink(missing_ok=True) + break + + return "select 'model_a' as m" +""") + + (models_dir / "model_b.sql").write_text(""" + MODEL ( + name test.model_b, + kind FULL + ); + + select a.m as m, 'model_b' as mb from test.model_a as a + """) + + config = Config( + gateways={ + "": GatewayConfig( + connection=DuckDBConnectionConfig(database=str(tmp_path / "db.db")), + state_connection=DuckDBConnectionConfig(database=str(tmp_path / "state.db")), + ) + }, + model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01"), + ) + ctx = Context(paths=[tmp_path], config=config) + + ctx.plan(environment="prod", auto_apply=True) + + assert len(ctx.snapshots) == 2 + assert all(s.intervals for s in ctx.snapshots.values()) + + prod_model_a_snapshot_id = ctx.snapshots['"db"."test"."model_a"'].snapshot_id + prod_model_b_snapshot_id = ctx.snapshots['"db"."test"."model_b"'].snapshot_id + + # dev models + # new version of B + (models_dir / "model_b.sql").write_text(""" + MODEL ( + name test.model_b, + kind FULL + ); + + select a.m as m, 'model_b' as mb, 'dev' as dev_version from test.model_a as a + """) + + # add C + (models_dir / "model_c.sql").write_text(""" + MODEL ( + name test.model_c, + kind FULL + ); + + select b.*, 'model_c' as mc from test.model_b as b + """) + + ctx.load() + ctx.plan(environment="dev", auto_apply=True) + + dev_model_b_snapshot_id = ctx.snapshots['"db"."test"."model_b"'].snapshot_id + dev_model_c_snapshot_id = ctx.snapshots['"db"."test"."model_c"'].snapshot_id + + assert dev_model_b_snapshot_id != prod_model_b_snapshot_id + + # now, we restate A in prod but touch the lockfile so it hangs during evaluation + # we also have to do it in its own thread due to the hang + lock_file_path.touch() + + def _run_restatement_plan(tmp_path: Path, config: Config, q: queue.Queue): + q.put("thread_started") + + # give this thread its own Context object to prevent segfaulting the Python interpreter + restatement_ctx = Context(paths=[tmp_path], config=config) + + # dev2 not present before the restatement plan starts + assert restatement_ctx.state_sync.get_environment("dev2") is None + + q.put("plan_started") + plan = restatement_ctx.plan( + environment="prod", restate_models=['"db"."test"."model_a"'], auto_apply=True + ) + q.put("plan_completed") + + # dev2 was created during the restatement plan + assert restatement_ctx.state_sync.get_environment("dev2") is not None + + return plan + + executor = ThreadPoolExecutor() + q: queue.Queue = queue.Queue() + restatement_plan_future = executor.submit(_run_restatement_plan, tmp_path, config, q) + assert q.get() == "thread_started" + + try: + if e := restatement_plan_future.exception(timeout=1): + # abort early if the plan thread threw an exception + raise e + except TimeoutError: + # that's ok, we dont actually expect the plan to have finished in 1 second + pass + + # while that restatement is running, we can simulate another process and check that it sees no empty intervals + assert q.get() == "plan_started" + + # dont check for potentially missing intervals until the plan is in the evaluation loop + attempts = 0 + while not evaluation_lock_file_path.exists(): + time.sleep(2) + attempts += 1 + if attempts > 10: + raise ValueError("Gave up waiting for evaluation loop") + + ctx.clear_caches() # get rid of the file cache so that data is re-fetched from state + prod_models_from_state = ctx.state_sync.get_snapshots( + snapshot_ids=[prod_model_a_snapshot_id, prod_model_b_snapshot_id] + ) + + # prod intervals should be present still + assert all(m.intervals for m in prod_models_from_state.values()) + + # so should dev intervals since prod restatement is still running + assert all(m.intervals for m in ctx.snapshots.values()) + + # now, lets create a new dev environment "dev2", while the prod restatement plan is still running, + # that changes model_b while still being based on the original version of model_a + (models_dir / "model_b.sql").write_text(""" + MODEL ( + name test.model_b, + kind FULL + ); + + select a.m as m, 'model_b' as mb, 'dev2' as dev_version from test.model_a as a + """) + ctx.load() + ctx.plan(environment="dev2", auto_apply=True) + + dev2_model_b_snapshot_id = ctx.snapshots['"db"."test"."model_b"'].snapshot_id + assert dev2_model_b_snapshot_id != dev_model_b_snapshot_id + assert dev2_model_b_snapshot_id != prod_model_b_snapshot_id + + # as at this point, everything still has intervals + ctx.clear_caches() + assert all( + s.intervals + for s in ctx.state_sync.get_snapshots( + snapshot_ids=[ + prod_model_a_snapshot_id, + prod_model_b_snapshot_id, + dev_model_b_snapshot_id, + dev_model_c_snapshot_id, + dev2_model_b_snapshot_id, + ] + ).values() + ) + + # now, we finally let that restatement plan complete + # first, verify it's still blocked where it should be + assert not restatement_plan_future.done() + + lock_file_path.unlink() # remove lock file, plan should be able to proceed now + + if e := restatement_plan_future.exception(): # blocks until future complete + raise e + + assert restatement_plan_future.result() + assert q.get() == "plan_completed" + + ctx.clear_caches() + + # check that intervals in prod are present + assert all( + s.intervals + for s in ctx.state_sync.get_snapshots( + snapshot_ids=[ + prod_model_a_snapshot_id, + prod_model_b_snapshot_id, + ] + ).values() + ) + + # check that intervals in dev have been cleared, including the dev2 env that + # was created after the restatement plan started + assert all( + not s.intervals + for s in ctx.state_sync.get_snapshots( + snapshot_ids=[ + dev_model_b_snapshot_id, + dev_model_c_snapshot_id, + dev2_model_b_snapshot_id, + ] + ).values() + ) + + executor.shutdown() + + +def test_restatement_plan_detects_prod_deployment_during_restatement(tmp_path: Path): + """ + Scenario: + - `prod` environment exists, model A + - `dev` environment created, model A(dev) + - Restatement plan is triggered against `prod` for model A + - During restatement, someone else deploys A(dev) to prod, replacing the model that is currently being restated. + + Outcome: + - The deployment plan for dev -> prod should succeed in deploying the new version of A + - The prod restatement plan should fail with a ConflictingPlanError and warn about the model that got updated while undergoing restatement + - The new version of A should have no intervals cleared. The user needs to rerun the restatement if the intervals should still be cleared + """ + orig_console = get_console() + console = CaptureTerminalConsole() + set_console(console) + + models_dir = tmp_path / "models" + models_dir.mkdir() + + lock_file_path = tmp_path / "test.lock" # python model blocks while this file is present + + evaluation_lock_file_path = ( + tmp_path / "evaluation.lock" + ) # python model creates this file if it's in the wait loop and deletes it once done + + # Note: to make execution block so we can test stuff, we use a Python model that blocks until it no longer detects the presence of a file + (models_dir / "model_a.py").write_text(f""" +from sqlmesh.core.model import model +from sqlmesh.core.macros import MacroEvaluator + +@model( + "test.model_a", + is_sql=True, + kind="FULL" +) +def entrypoint(evaluator: MacroEvaluator) -> str: + from pathlib import Path + import time + + if evaluator.runtime_stage == 'evaluating': + while True: + if Path("{str(lock_file_path)}").exists(): + Path("{str(evaluation_lock_file_path)}").touch() + print("lock exists; sleeping") + time.sleep(2) + else: + Path("{str(evaluation_lock_file_path)}").unlink(missing_ok=True) + break + + return "select 'model_a' as m" +""") + + config = Config( + gateways={ + "": GatewayConfig( + connection=DuckDBConnectionConfig(database=str(tmp_path / "db.db")), + state_connection=DuckDBConnectionConfig(database=str(tmp_path / "state.db")), + ) + }, + model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01"), + ) + ctx = Context(paths=[tmp_path], config=config) + + # create prod + ctx.plan(environment="prod", auto_apply=True) + original_prod = ctx.state_sync.get_environment("prod") + assert original_prod + + # update model_a for dev + (models_dir / "model_a.py").unlink() + (models_dir / "model_a.sql").write_text(""" + MODEL ( + name test.model_a, + kind FULL + ); + + select 1 as changed + """) + + # create dev + ctx.load() + plan = ctx.plan(environment="dev", auto_apply=True) + assert len(plan.modified_snapshots) == 1 + new_model_a_snapshot_id = list(plan.modified_snapshots)[0] + + # now, trigger a prod restatement plan in a different thread and block it to simulate a long restatement + thread_console = None + + def _run_restatement_plan(tmp_path: Path, config: Config, q: queue.Queue): + nonlocal thread_console + q.put("thread_started") + + # Give this thread its own markdown console to avoid Rich LiveError + thread_console = MarkdownConsole() + set_console(thread_console) + + # give this thread its own Context object to prevent segfaulting the Python interpreter + restatement_ctx = Context(paths=[tmp_path], config=config) + + # ensure dev is present before the restatement plan starts + assert restatement_ctx.state_sync.get_environment("dev") is not None + + q.put("plan_started") + expected_error = None + try: + restatement_ctx.plan( + environment="prod", restate_models=['"db"."test"."model_a"'], auto_apply=True + ) + except ConflictingPlanError as e: + expected_error = e + + q.put("plan_completed") + return expected_error + + executor = ThreadPoolExecutor() + q: queue.Queue = queue.Queue() + lock_file_path.touch() + + restatement_plan_future = executor.submit(_run_restatement_plan, tmp_path, config, q) + restatement_plan_future.add_done_callback(lambda _: executor.shutdown()) + + assert q.get() == "thread_started" + + try: + if e := restatement_plan_future.exception(timeout=1): + # abort early if the plan thread threw an exception + raise e + except TimeoutError: + # that's ok, we dont actually expect the plan to have finished in 1 second + pass + + assert q.get() == "plan_started" + + # ok, now the prod restatement plan is running, let's deploy dev to prod + ctx.plan(environment="prod", auto_apply=True) + + new_prod = ctx.state_sync.get_environment("prod") + assert new_prod + assert new_prod.plan_id != original_prod.plan_id + assert new_prod.previous_plan_id == original_prod.plan_id + + # new prod is deployed but restatement plan is still running + assert not restatement_plan_future.done() + + # allow restatement plan to complete + lock_file_path.unlink() + + plan_error = restatement_plan_future.result() + assert isinstance(plan_error, ConflictingPlanError) + assert "please re-apply your plan" in repr(plan_error).lower() + + output = " ".join(re.split("\\s+", thread_console.captured_output, flags=re.UNICODE)) # type: ignore + assert ( + f"The following models had new versions deployed while data was being restated: └── test.model_a" + in output + ) + + # check that no intervals have been cleared from the model_a currently in prod + model_a = ctx.state_sync.get_snapshots(snapshot_ids=[new_model_a_snapshot_id])[ + new_model_a_snapshot_id + ] + assert isinstance(model_a.node, SqlModel) + assert model_a.node.render_query_or_raise().sql() == 'SELECT 1 AS "changed"' + assert len(model_a.intervals) + + set_console(orig_console) diff --git a/tests/core/integration/test_run.py b/tests/core/integration/test_run.py new file mode 100644 index 0000000000..a3b84b5a9e --- /dev/null +++ b/tests/core/integration/test_run.py @@ -0,0 +1,247 @@ +from __future__ import annotations + +import typing as t +import pytest +import time_machine +from pytest_mock.plugin import MockerFixture + +from sqlmesh.core import constants as c +from sqlmesh.core import dialect as d +from sqlmesh.core.config.categorizer import CategorizerConfig +from sqlmesh.core.model import ( + SqlModel, + PythonModel, + load_sql_based_model, +) +from sqlmesh.utils.date import to_timestamp + +if t.TYPE_CHECKING: + pass + +pytestmark = pytest.mark.slow + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_run_with_select_models( + init_and_plan_context: t.Callable, +): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + with time_machine.travel("2023-01-09 00:00:00 UTC"): + assert context.run(select_models=["*waiter_revenue_by_day"]) + + snapshots = context.state_sync.state_sync.get_snapshots(context.snapshots.values()) + # Only waiter_revenue_by_day and its parents should be backfilled up to 2023-01-09. + assert {s.name: s.intervals[0][1] for s in snapshots.values() if s.intervals} == { + '"memory"."sushi"."waiter_revenue_by_day"': to_timestamp("2023-01-09"), + '"memory"."sushi"."order_items"': to_timestamp("2023-01-09"), + '"memory"."sushi"."orders"': to_timestamp("2023-01-09"), + '"memory"."sushi"."items"': to_timestamp("2023-01-09"), + '"memory"."sushi"."customer_revenue_lifetime"': to_timestamp("2023-01-08"), + '"memory"."sushi"."customer_revenue_by_day"': to_timestamp("2023-01-08"), + '"memory"."sushi"."latest_order"': to_timestamp("2023-01-08"), + '"memory"."sushi"."waiter_names"': to_timestamp("2023-01-08"), + '"memory"."sushi"."raw_marketing"': to_timestamp("2023-01-08"), + '"memory"."sushi"."marketing"': to_timestamp("2023-01-08"), + '"memory"."sushi"."waiter_as_customer_by_day"': to_timestamp("2023-01-08"), + '"memory"."sushi"."top_waiters"': to_timestamp("2023-01-08"), + '"memory"."raw"."demographics"': to_timestamp("2023-01-08"), + "assert_item_price_above_zero": to_timestamp("2023-01-08"), + '"memory"."sushi"."active_customers"': to_timestamp("2023-01-08"), + '"memory"."sushi"."customers"': to_timestamp("2023-01-08"), + '"memory"."sushi"."count_customers_active"': to_timestamp("2023-01-08"), + '"memory"."sushi"."count_customers_inactive"': to_timestamp("2023-01-08"), + } + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_run_with_select_models_no_auto_upstream( + init_and_plan_context: t.Callable, +): + context, _ = init_and_plan_context("examples/sushi") + + model = context.get_model("sushi.waiter_revenue_by_day") + model = SqlModel.parse_obj({**model.dict(), "audits": []}) + context.upsert_model(model) + + context.plan("prod", no_prompts=True, skip_tests=True, auto_apply=True) + + with time_machine.travel("2023-01-09 00:00:00 UTC"): + assert context.run(select_models=["*waiter_revenue_by_day"], no_auto_upstream=True) + + snapshots = context.state_sync.state_sync.get_snapshots(context.snapshots.values()) + # Only waiter_revenue_by_day should be backfilled up to 2023-01-09. + assert {s.name: s.intervals[0][1] for s in snapshots.values() if s.intervals} == { + '"memory"."sushi"."waiter_revenue_by_day"': to_timestamp("2023-01-09"), + '"memory"."sushi"."order_items"': to_timestamp("2023-01-08"), + '"memory"."sushi"."orders"': to_timestamp("2023-01-08"), + '"memory"."sushi"."items"': to_timestamp("2023-01-08"), + '"memory"."sushi"."customer_revenue_lifetime"': to_timestamp("2023-01-08"), + '"memory"."sushi"."customer_revenue_by_day"': to_timestamp("2023-01-08"), + '"memory"."sushi"."latest_order"': to_timestamp("2023-01-08"), + '"memory"."sushi"."waiter_names"': to_timestamp("2023-01-08"), + '"memory"."sushi"."raw_marketing"': to_timestamp("2023-01-08"), + '"memory"."sushi"."marketing"': to_timestamp("2023-01-08"), + '"memory"."sushi"."waiter_as_customer_by_day"': to_timestamp("2023-01-08"), + '"memory"."sushi"."top_waiters"': to_timestamp("2023-01-08"), + '"memory"."raw"."demographics"': to_timestamp("2023-01-08"), + "assert_item_price_above_zero": to_timestamp("2023-01-08"), + '"memory"."sushi"."active_customers"': to_timestamp("2023-01-08"), + '"memory"."sushi"."customers"': to_timestamp("2023-01-08"), + '"memory"."sushi"."count_customers_active"': to_timestamp("2023-01-08"), + '"memory"."sushi"."count_customers_inactive"': to_timestamp("2023-01-08"), + } + + +@time_machine.travel("2023-01-08 15:00:00 UTC") +def test_run_respects_excluded_transitive_dependencies(init_and_plan_context: t.Callable): + context, _ = init_and_plan_context("examples/sushi") + + # Graph: C <- B <- A + # B is a transitive dependency linking A and C + # Note that the alphabetical ordering of the model names is intentional and helps + # surface the problem + expressions_a = d.parse( + f""" + MODEL ( + name memory.sushi.test_model_c, + kind FULL, + allow_partials true, + cron '@hourly', + ); + + SELECT @execution_ts AS execution_ts + """ + ) + model_c = load_sql_based_model(expressions_a) + context.upsert_model(model_c) + + # A VIEW model with no partials allowed and a daily cron instead of hourly. + expressions_b = d.parse( + f""" + MODEL ( + name memory.sushi.test_model_b, + kind VIEW, + allow_partials false, + cron '@daily', + ); + + SELECT * FROM memory.sushi.test_model_c + """ + ) + model_b = load_sql_based_model(expressions_b) + context.upsert_model(model_b) + + expressions_a = d.parse( + f""" + MODEL ( + name memory.sushi.test_model_a, + kind FULL, + allow_partials true, + cron '@hourly', + ); + + SELECT * FROM memory.sushi.test_model_b + """ + ) + model_a = load_sql_based_model(expressions_a) + context.upsert_model(model_a) + + context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) + assert ( + context.fetchdf("SELECT execution_ts FROM memory.sushi.test_model_c")["execution_ts"].iloc[ + 0 + ] + == "2023-01-08 15:00:00" + ) + + with time_machine.travel("2023-01-08 17:00:00 UTC", tick=False): + context.run( + "prod", + select_models=["*test_model_c", "*test_model_a"], + no_auto_upstream=True, + ignore_cron=True, + ) + assert ( + context.fetchdf("SELECT execution_ts FROM memory.sushi.test_model_a")[ + "execution_ts" + ].iloc[0] + == "2023-01-08 17:00:00" + ) + + +@time_machine.travel("2023-01-08 00:00:00 UTC") +def test_snapshot_triggers(init_and_plan_context: t.Callable, mocker: MockerFixture): + context, plan = init_and_plan_context("examples/sushi") + context.apply(plan) + + # auto-restatement triggers + orders = context.get_model("sushi.orders") + orders_kind = { + **orders.kind.dict(), + "auto_restatement_cron": "@hourly", + } + orders_kwargs = { + **orders.dict(), + "kind": orders_kind, + } + context.upsert_model(PythonModel.parse_obj(orders_kwargs)) + + order_items = context.get_model("sushi.order_items") + order_items_kind = { + **order_items.kind.dict(), + "auto_restatement_cron": "@hourly", + } + order_items_kwargs = { + **order_items.dict(), + "kind": order_items_kind, + } + context.upsert_model(PythonModel.parse_obj(order_items_kwargs)) + + waiter_revenue_by_day = context.get_model("sushi.waiter_revenue_by_day") + waiter_revenue_by_day_kind = { + **waiter_revenue_by_day.kind.dict(), + "auto_restatement_cron": "@hourly", + } + waiter_revenue_by_day_kwargs = { + **waiter_revenue_by_day.dict(), + "kind": waiter_revenue_by_day_kind, + } + context.upsert_model(SqlModel.parse_obj(waiter_revenue_by_day_kwargs)) + + context.plan(auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full()) + + scheduler = context.scheduler() + + import sqlmesh + + spy = mocker.spy(sqlmesh.core.scheduler.Scheduler, "run_merged_intervals") + + with time_machine.travel("2023-01-09 00:00:01 UTC"): + scheduler.run( + environment=c.PROD, + start="2023-01-01", + auto_restatement_enabled=True, + ) + + assert spy.called + + actual_triggers = spy.call_args.kwargs["auto_restatement_triggers"] + actual_triggers = {k: v for k, v in actual_triggers.items() if v} + assert len(actual_triggers) == 12 + + for id, trigger in actual_triggers.items(): + model_name = id.name.replace('"memory"."sushi".', "").replace('"', "") + auto_restatement_triggers = [ + t.name.replace('"memory"."sushi".', "").replace('"', "") for t in trigger + ] + + if model_name in ("orders", "order_items", "waiter_revenue_by_day"): + assert auto_restatement_triggers == [model_name] + elif model_name in ("customer_revenue_lifetime", "customer_revenue_by_day"): + assert sorted(auto_restatement_triggers) == sorted(["orders", "order_items"]) + elif model_name == "top_waiters": + assert auto_restatement_triggers == ["waiter_revenue_by_day"] + else: + assert auto_restatement_triggers == ["orders"] diff --git a/tests/core/integration/utils.py b/tests/core/integration/utils.py new file mode 100644 index 0000000000..bc731e6cc8 --- /dev/null +++ b/tests/core/integration/utils.py @@ -0,0 +1,350 @@ +from __future__ import annotations + +import typing as t +from sqlmesh.core.model.common import ParsableSql +from sqlglot import exp +from sqlglot.expressions import DataType + +from sqlmesh.core import constants as c +from sqlmesh.core.context import Context +from sqlmesh.core.engine_adapter import EngineAdapter +from sqlmesh.core.environment import EnvironmentNamingInfo +from sqlmesh.core.model import ( + IncrementalByTimeRangeKind, + IncrementalByUniqueKeyKind, + ModelKind, + ModelKindName, + SqlModel, + TimeColumn, +) +from sqlmesh.core.model.kind import model_kind_type_from_name +from sqlmesh.core.plan import Plan, PlanBuilder +from sqlmesh.core.snapshot import ( + DeployabilityIndex, + Snapshot, + SnapshotChangeCategory, + SnapshotId, + SnapshotInfoLike, + SnapshotTableInfo, +) +from sqlmesh.utils.date import TimeLike + + +def select_all(table: str, adapter: EngineAdapter) -> t.Iterable: + return adapter.fetchall(f"select * from {table} order by 1") + + +def snapshots_to_versions(snapshots: t.Iterable[Snapshot]) -> t.Dict[str, str]: + return {snapshot.name: snapshot.version or "" for snapshot in snapshots} + + +def to_snapshot_info(snapshot: SnapshotInfoLike) -> SnapshotTableInfo: + return snapshot.table_info + + +def start(context: Context) -> TimeLike: + env = context.state_sync.get_environment("prod") + assert env + return env.start_at + + +def add_projection_to_model(model: SqlModel, literal: bool = True) -> SqlModel: + one_expr = exp.Literal.number(1).as_("one") if literal else exp.column("one") + kwargs = { + **model.dict(), + "query": model.query.select(one_expr), # type: ignore + } + return SqlModel.parse_obj(kwargs) + + +def plan_choice(plan_builder: PlanBuilder, choice: SnapshotChangeCategory) -> None: + for snapshot in plan_builder.build().snapshots.values(): + if not snapshot.version: + plan_builder.set_choice(snapshot, choice) + + +def apply_to_environment( + context: Context, + environment: str, + choice: t.Optional[SnapshotChangeCategory] = None, + plan_validators: t.Optional[t.Iterable[t.Callable]] = None, + apply_validators: t.Optional[t.Iterable[t.Callable]] = None, + plan_start: t.Optional[TimeLike] = None, + allow_destructive_models: t.Optional[t.List[str]] = None, + enable_preview: bool = False, +): + plan_validators = plan_validators or [] + apply_validators = apply_validators or [] + + plan_builder = context.plan_builder( + environment, + start=plan_start or start(context) if environment != c.PROD else None, + forward_only=choice == SnapshotChangeCategory.FORWARD_ONLY, + include_unmodified=True, + allow_destructive_models=allow_destructive_models if allow_destructive_models else [], + enable_preview=enable_preview, + ) + if environment != c.PROD: + plan_builder.set_start(plan_start or start(context)) + + if choice: + if choice == SnapshotChangeCategory.FORWARD_ONLY: + # FORWARD_ONLY is deprecated, fallback to NON_BREAKING to keep the existing tests + choice = SnapshotChangeCategory.NON_BREAKING + plan_choice(plan_builder, choice) + for validator in plan_validators: + validator(context, plan_builder.build()) + + plan = plan_builder.build() + context.apply(plan) + + validate_apply_basics(context, environment, plan.snapshots.values(), plan.deployability_index) + for validator in apply_validators: + validator(context) + return plan + + +def change_data_type( + context: Context, model_name: str, old_type: DataType.Type, new_type: DataType.Type +) -> None: + model = context.get_model(model_name) + assert model is not None + + if isinstance(model, SqlModel): + query = model.query.copy() + data_types = query.find_all(DataType) + for data_type in data_types: + if data_type.this == old_type: + data_type.set("this", new_type) + context.upsert_model(model_name, query_=ParsableSql(sql=query.sql(dialect=model.dialect))) + elif model.columns_to_types_ is not None: + for k, v in model.columns_to_types_.items(): + if v.this == old_type: + model.columns_to_types_[k] = DataType.build(new_type) + context.upsert_model(model_name, columns=model.columns_to_types_) + + +def validate_snapshots_in_state_sync(snapshots: t.Iterable[Snapshot], context: Context) -> None: + snapshot_infos = map(to_snapshot_info, snapshots) + state_sync_table_infos = map( + to_snapshot_info, context.state_reader.get_snapshots(snapshots).values() + ) + assert set(snapshot_infos) == set(state_sync_table_infos) + + +def validate_state_sync_environment( + snapshots: t.Iterable[Snapshot], env: str, context: Context +) -> None: + environment = context.state_reader.get_environment(env) + assert environment + snapshot_infos = map(to_snapshot_info, snapshots) + environment_table_infos = map(to_snapshot_info, environment.snapshots) + assert set(snapshot_infos) == set(environment_table_infos) + + +def validate_tables( + snapshots: t.Iterable[Snapshot], + context: Context, + deployability_index: t.Optional[DeployabilityIndex] = None, +) -> None: + adapter = context.engine_adapter + deployability_index = deployability_index or DeployabilityIndex.all_deployable() + for snapshot in snapshots: + is_deployable = deployability_index.is_representative(snapshot) + if not snapshot.is_model or snapshot.is_external: + continue + table_should_exist = not snapshot.is_embedded + assert adapter.table_exists(snapshot.table_name(is_deployable)) == table_should_exist + if table_should_exist: + assert select_all(snapshot.table_name(is_deployable), adapter) + + +def validate_environment_views( + snapshots: t.Iterable[Snapshot], + environment: str, + context: Context, + deployability_index: t.Optional[DeployabilityIndex] = None, +) -> None: + adapter = context.engine_adapter + deployability_index = deployability_index or DeployabilityIndex.all_deployable() + for snapshot in snapshots: + is_deployable = deployability_index.is_representative(snapshot) + if not snapshot.is_model or snapshot.is_symbolic: + continue + view_name = snapshot.qualified_view_name.for_environment( + EnvironmentNamingInfo.from_environment_catalog_mapping( + context.config.environment_catalog_mapping, + name=environment, + suffix_target=context.config.environment_suffix_target, + ) + ) + + assert adapter.table_exists(view_name) + assert select_all(snapshot.table_name(is_deployable), adapter) == select_all( + view_name, adapter + ) + + +def validate_apply_basics( + context: Context, + environment: str, + snapshots: t.Iterable[Snapshot], + deployability_index: t.Optional[DeployabilityIndex] = None, +) -> None: + validate_snapshots_in_state_sync(snapshots, context) + validate_state_sync_environment(snapshots, environment, context) + validate_tables(snapshots, context, deployability_index) + validate_environment_views(snapshots, environment, context, deployability_index) + + +def validate_plan_changes( + plan: Plan, + *, + added: t.Optional[t.Iterable[SnapshotId]] = None, + modified: t.Optional[t.Iterable[str]] = None, + removed: t.Optional[t.Iterable[SnapshotId]] = None, +) -> None: + added = added or [] + modified = modified or [] + removed = removed or [] + assert set(added) == plan.context_diff.added + assert set(modified) == set(plan.context_diff.modified_snapshots) + assert set(removed) == set(plan.context_diff.removed_snapshots) + + +def validate_versions_same( + model_names: t.List[str], + versions: t.Dict[str, str], + other_versions: t.Dict[str, str], +) -> None: + for name in model_names: + assert versions[name] == other_versions[name] + + +def validate_versions_different( + model_names: t.List[str], + versions: t.Dict[str, str], + other_versions: t.Dict[str, str], +) -> None: + for name in model_names: + assert versions[name] != other_versions[name] + + +def validate_query_change( + context: Context, + environment: str, + change_category: SnapshotChangeCategory, + logical: bool, +): + versions = snapshots_to_versions(context.snapshots.values()) + + change_data_type( + context, + "sushi.items", + DataType.Type.DOUBLE, + DataType.Type.FLOAT, + ) + + directly_modified = ['"memory"."sushi"."items"'] + indirectly_modified = [ + '"memory"."sushi"."order_items"', + '"memory"."sushi"."waiter_revenue_by_day"', + '"memory"."sushi"."customer_revenue_by_day"', + '"memory"."sushi"."customer_revenue_lifetime"', + '"memory"."sushi"."top_waiters"', + "assert_item_price_above_zero", + ] + not_modified = [ + snapshot.name + for snapshot in context.snapshots.values() + if snapshot.name not in directly_modified and snapshot.name not in indirectly_modified + ] + + if change_category == SnapshotChangeCategory.BREAKING and not logical: + models_same = not_modified + models_different = directly_modified + indirectly_modified + elif change_category == SnapshotChangeCategory.FORWARD_ONLY: + models_same = not_modified + directly_modified + indirectly_modified + models_different = [] + else: + models_same = not_modified + indirectly_modified + models_different = directly_modified + + def _validate_plan(context, plan): + validate_plan_changes(plan, modified=directly_modified + indirectly_modified) + assert bool(plan.missing_intervals) != logical + + def _validate_apply(context): + current_versions = snapshots_to_versions(context.snapshots.values()) + validate_versions_same(models_same, versions, current_versions) + validate_versions_different(models_different, versions, current_versions) + + apply_to_environment( + context, + environment, + change_category, + plan_validators=[_validate_plan], + apply_validators=[_validate_apply], + ) + + +def initial_add(context: Context, environment: str): + assert not context.state_reader.get_environment(environment) + + plan = context.plan(environment, start=start(context), create_from="nonexistent_env") + validate_plan_changes(plan, added={x.snapshot_id for x in context.snapshots.values()}) + + context.apply(plan) + validate_apply_basics(context, environment, plan.snapshots.values()) + + +def change_model_kind(context: Context, kind: ModelKindName): + if kind in (ModelKindName.VIEW, ModelKindName.EMBEDDED, ModelKindName.FULL): + context.upsert_model( + "sushi.items", + partitioned_by=[], + ) + context.upsert_model("sushi.items", kind=model_kind_type_from_name(kind)()) # type: ignore + + +def validate_model_kind_change( + kind_name: ModelKindName, + context: Context, + environment: str, + *, + logical: bool, +): + directly_modified = ['"memory"."sushi"."items"'] + indirectly_modified = [ + '"memory"."sushi"."order_items"', + '"memory"."sushi"."waiter_revenue_by_day"', + '"memory"."sushi"."customer_revenue_by_day"', + '"memory"."sushi"."customer_revenue_lifetime"', + '"memory"."sushi"."top_waiters"', + "assert_item_price_above_zero", + ] + if kind_name == ModelKindName.INCREMENTAL_BY_TIME_RANGE: + kind: ModelKind = IncrementalByTimeRangeKind(time_column=TimeColumn(column="event_date")) + elif kind_name == ModelKindName.INCREMENTAL_BY_UNIQUE_KEY: + kind = IncrementalByUniqueKeyKind(unique_key="id") + else: + kind = model_kind_type_from_name(kind_name)() # type: ignore + + def _validate_plan(context, plan): + validate_plan_changes(plan, modified=directly_modified + indirectly_modified) + assert ( + next( + snapshot + for snapshot in plan.snapshots.values() + if snapshot.name == '"memory"."sushi"."items"' + ).model.kind.name + == kind.name + ) + assert bool(plan.missing_intervals) != logical + + apply_to_environment( + context, + environment, + SnapshotChangeCategory.NON_BREAKING, + plan_validators=[_validate_plan], + ) diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py index 52ea673778..02068b1c59 100644 --- a/tests/core/test_dialect.py +++ b/tests/core/test_dialect.py @@ -16,6 +16,8 @@ from sqlmesh.core.model import SqlModel, load_sql_based_model from sqlmesh.core.config.connection import DIALECT_TO_TYPE +pytestmark = pytest.mark.dialect_isolated + def test_format_model_expressions(): x = format_model_expressions( diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py deleted file mode 100644 index bac495a5f1..0000000000 --- a/tests/core/test_integration.py +++ /dev/null @@ -1,10887 +0,0 @@ -from __future__ import annotations - -import typing as t -import json -from collections import Counter -from datetime import timedelta -from unittest import mock -from unittest.mock import patch -import logging -from textwrap import dedent -import os -import numpy as np # noqa: TID253 -import pandas as pd # noqa: TID253 -import pytest -from pytest import MonkeyPatch -from pathlib import Path -from sqlmesh.core.console import ( - MarkdownConsole, - set_console, - get_console, - TerminalConsole, - CaptureTerminalConsole, -) -from sqlmesh.core.config.naming import NameInferenceConfig -from sqlmesh.core.model.common import ParsableSql -from sqlmesh.utils.concurrency import NodeExecutionFailedError -import time_machine -from pytest_mock.plugin import MockerFixture -from sqlglot import exp -from sqlglot.expressions import DataType -import re -from IPython.utils.capture import capture_output -from concurrent.futures import ThreadPoolExecutor, TimeoutError -import time -import queue - -from sqlmesh import CustomMaterialization -from sqlmesh.cli.project_init import init_example_project -from sqlmesh.core import constants as c -from sqlmesh.core import dialect as d -from sqlmesh.core.config import ( - AutoCategorizationMode, - Config, - GatewayConfig, - ModelDefaultsConfig, - DuckDBConnectionConfig, - TableNamingConvention, -) -from sqlmesh.core.config.common import EnvironmentSuffixTarget, VirtualEnvironmentMode -from sqlmesh.core.console import Console, get_console -from sqlmesh.core.context import Context -from sqlmesh.core.config.categorizer import CategorizerConfig -from sqlmesh.core.config.plan import PlanConfig -from sqlmesh.core.engine_adapter import EngineAdapter, DuckDBEngineAdapter -from sqlmesh.core.environment import EnvironmentNamingInfo -from sqlmesh.core.macros import macro -from sqlmesh.core.model import ( - FullKind, - IncrementalByTimeRangeKind, - IncrementalByUniqueKeyKind, - IncrementalUnmanagedKind, - Model, - ModelKind, - ModelKindName, - SqlModel, - PythonModel, - ViewKind, - CustomKind, - TimeColumn, - load_sql_based_model, -) -from sqlmesh.core.model.kind import model_kind_type_from_name -from sqlmesh.core.plan import Plan, PlanBuilder, SnapshotIntervals -from sqlmesh.core.snapshot import ( - DeployabilityIndex, - Snapshot, - SnapshotChangeCategory, - SnapshotId, - SnapshotInfoLike, - SnapshotTableInfo, -) -from sqlmesh.utils.date import TimeLike, now, to_date, to_datetime, to_timestamp -from sqlmesh.utils.errors import ( - NoChangesPlanError, - SQLMeshError, - PlanError, - ConfigError, - ConflictingPlanError, -) -from sqlmesh.utils.pydantic import validate_string -from tests.conftest import DuckDBMetadata, SushiDataValidator -from sqlmesh.utils import CorrelationId -from tests.utils.test_helpers import use_terminal_console -from tests.utils.test_filesystem import create_temp_file - -if t.TYPE_CHECKING: - from sqlmesh import QueryOrDF - -pytestmark = pytest.mark.slow - - -@pytest.fixture(autouse=True) -def mock_choices(mocker: MockerFixture): - mocker.patch("sqlmesh.core.console.TerminalConsole._get_snapshot_change_category") - mocker.patch("sqlmesh.core.console.TerminalConsole._prompt_backfill") - - -def plan_choice(plan_builder: PlanBuilder, choice: SnapshotChangeCategory) -> None: - for snapshot in plan_builder.build().snapshots.values(): - if not snapshot.version: - plan_builder.set_choice(snapshot, choice) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -@pytest.mark.parametrize( - "context_fixture", - ["sushi_context", "sushi_no_default_catalog"], -) -def test_forward_only_plan_with_effective_date(context_fixture: Context, request): - context = request.getfixturevalue(context_fixture) - model_name = "sushi.waiter_revenue_by_day" - model = context.get_model(model_name) - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model)), start="2023-01-01") - snapshot = context.get_snapshot(model, raise_if_missing=True) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan_builder = context.plan_builder("dev", skip_tests=True, forward_only=True) - plan = plan_builder.build() - assert len(plan.new_snapshots) == 2 - assert ( - plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only - assert plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].is_forward_only - - assert to_timestamp(plan.start) == to_timestamp("2023-01-07") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], - ), - ] - - plan = plan_builder.set_effective_from("2023-01-05").build() - # Default start should be set to effective_from - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - plan = plan_builder.set_start("2023-01-06").build() - # Start override should take precedence - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - plan = plan_builder.set_effective_from("2023-01-04").build() - # Start should remain unchanged - assert plan.start == "2023-01-06" - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - context.apply(plan) - - dev_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" - ) - assert dev_df["event_date"].tolist() == [ - pd.to_datetime("2023-01-06"), - pd.to_datetime("2023-01-07"), - ] - - prod_plan = context.plan_builder(skip_tests=True).build() - # Make sure that the previously set effective_from is respected - assert prod_plan.start == to_timestamp("2023-01-04") - assert prod_plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - context.apply(prod_plan) - - prod_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi.waiter_revenue_by_day WHERE one IS NOT NULL ORDER BY event_date" - ) - assert prod_df["event_date"].tolist() == [ - pd.to_datetime(x) for x in ["2023-01-04", "2023-01-05", "2023-01-06", "2023-01-07"] - ] - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_forward_only_model_regular_plan(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - model_name = "sushi.waiter_revenue_by_day" - - model = context.get_model(model_name) - model = add_projection_to_model(t.cast(SqlModel, model)) - forward_only_kind = model.kind.copy(update={"forward_only": True}) - model = model.copy(update={"kind": forward_only_kind}) - - context.upsert_model(model) - snapshot = context.get_snapshot(model, raise_if_missing=True) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() - assert len(plan.new_snapshots) == 2 - assert ( - plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only - assert plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].is_forward_only - - assert plan.start == to_datetime("2023-01-01") - assert not plan.missing_intervals - - context.apply(plan) - - dev_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" - ) - assert not dev_df["event_date"].tolist() - - # Run a restatement plan to preview changes - plan_builder = context.plan_builder( - "dev", skip_tests=True, restate_models=[model_name], enable_preview=False - ) - plan_builder.set_start("2023-01-06") - assert plan_builder.build().missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - # Make sure that changed start is reflected in missing intervals - plan_builder.set_start("2023-01-07") - assert plan_builder.build().missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - context.apply(plan_builder.build()) - - dev_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" - ) - assert dev_df["event_date"].tolist() == [pd.to_datetime("2023-01-07")] - - # Promote changes to prod - prod_plan = context.plan_builder(skip_tests=True).build() - assert not prod_plan.missing_intervals - - context.apply(prod_plan) - - # The change was applied in a forward-only manner so no values in the new column should be populated - prod_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi.waiter_revenue_by_day WHERE one IS NOT NULL ORDER BY event_date" - ) - assert not prod_df["event_date"].tolist() - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_forward_only_model_regular_plan_preview_enabled(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - model_name = "sushi.waiter_revenue_by_day" - - model = context.get_model(model_name) - model = add_projection_to_model(t.cast(SqlModel, model)) - forward_only_kind = model.kind.copy(update={"forward_only": True}) - model = model.copy(update={"kind": forward_only_kind}) - - context.upsert_model(model) - snapshot = context.get_snapshot(model, raise_if_missing=True) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() - assert len(plan.new_snapshots) == 2 - assert ( - plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only - assert plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].is_forward_only - - assert to_timestamp(plan.start) == to_timestamp("2023-01-07") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - context.apply(plan) - - dev_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" - ) - assert dev_df["event_date"].tolist() == [pd.to_datetime("2023-01-07")] - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_forward_only_model_restate_full_history_in_dev(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - model_name = "memory.sushi.customer_max_revenue" - expressions = d.parse( - f""" - MODEL ( - name {model_name}, - kind INCREMENTAL_BY_UNIQUE_KEY ( - unique_key customer_id, - forward_only true, - ), - ); - - SELECT - customer_id, MAX(revenue) AS max_revenue - FROM memory.sushi.customer_revenue_lifetime - GROUP BY 1; - """ - ) - - model = load_sql_based_model(expressions) - assert model.forward_only - assert model.kind.full_history_restatement_only - context.upsert_model(model) - - context.plan("prod", skip_tests=True, auto_apply=True, enable_preview=False) - - model_kwargs = { - **model.dict(), - # Make a breaking change. - "query": model.query.order_by("customer_id"), # type: ignore - } - context.upsert_model(SqlModel.parse_obj(model_kwargs)) - - # Apply the model change in dev - plan = context.plan_builder( - "dev", - skip_tests=True, - enable_preview=False, - categorizer_config=CategorizerConfig.all_full(), - ).build() - assert not plan.missing_intervals - context.apply(plan) - - snapshot = context.get_snapshot(model, raise_if_missing=True) - snapshot_table_name = snapshot.table_name(False) - - # Manually insert a dummy value to check that the table is recreated during the restatement - context.engine_adapter.insert_append( - snapshot_table_name, - pd.DataFrame({"customer_id": [-1], "max_revenue": [100]}), - ) - df = context.engine_adapter.fetchdf( - "SELECT COUNT(*) AS cnt FROM sushi__dev.customer_max_revenue WHERE customer_id = -1" - ) - assert df["cnt"][0] == 1 - - # Apply a restatement plan in dev - plan = context.plan("dev", restate_models=[model.name], auto_apply=True, enable_preview=False) - assert len(plan.missing_intervals) == 1 - - # Check that the dummy value is not present - df = context.engine_adapter.fetchdf( - "SELECT COUNT(*) AS cnt FROM sushi__dev.customer_max_revenue WHERE customer_id = -1" - ) - assert df["cnt"][0] == 0 - - # Check that the table is not empty - df = context.engine_adapter.fetchdf( - "SELECT COUNT(*) AS cnt FROM sushi__dev.customer_max_revenue" - ) - assert df["cnt"][0] > 0 - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_full_history_restatement_model_regular_plan_preview_enabled( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - model_name = "sushi.marketing" # SCD2 model - - model = context.get_model(model_name) - model = add_projection_to_model(t.cast(SqlModel, model)) - - context.upsert_model(model) - snapshot = context.get_snapshot(model, raise_if_missing=True) - customers_snapshot = context.get_snapshot("sushi.customers", raise_if_missing=True) - active_customers_snapshot = context.get_snapshot( - "sushi.active_customers", raise_if_missing=True - ) - waiter_as_customer_snapshot = context.get_snapshot( - "sushi.waiter_as_customer_by_day", raise_if_missing=True - ) - - plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() - - assert len(plan.new_snapshots) == 6 - assert ( - plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[customers_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[active_customers_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[waiter_as_customer_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert all(s.is_forward_only for s in plan.new_snapshots) - - assert to_timestamp(plan.start) == to_timestamp("2023-01-07") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - context.apply(plan) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_metadata_changed_regular_plan_preview_enabled(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - model_name = "sushi.waiter_revenue_by_day" - - model = context.get_model(model_name) - model = model.copy(update={"owner": "new_owner"}) - - context.upsert_model(model) - snapshot = context.get_snapshot(model, raise_if_missing=True) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() - assert len(plan.new_snapshots) == 2 - assert ( - plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.METADATA - ) - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.METADATA - ) - assert not plan.missing_intervals - assert not plan.restatements - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_hourly_model_with_lookback_no_backfill_in_dev(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - - model_name = "sushi.waiter_revenue_by_day" - - model = context.get_model(model_name) - model = SqlModel.parse_obj( - { - **model.dict(), - "kind": model.kind.copy(update={"lookback": 1}), - "cron": "@hourly", - "audits": [], - } - ) - context.upsert_model(model) - - plan = context.plan_builder("prod", skip_tests=True).build() - context.apply(plan) - - top_waiters_model = context.get_model("sushi.top_waiters") - top_waiters_model = add_projection_to_model(t.cast(SqlModel, top_waiters_model), literal=True) - context.upsert_model(top_waiters_model) - - context.get_snapshot(model, raise_if_missing=True) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - with time_machine.travel(now() + timedelta(hours=2)): - plan = context.plan_builder("dev", skip_tests=True).build() - # Make sure the waiter_revenue_by_day model is not backfilled. - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - -@time_machine.travel("2023-01-08 00:00:00 UTC", tick=False) -def test_parent_cron_after_child(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - - model = context.get_model("sushi.waiter_revenue_by_day") - model = SqlModel.parse_obj( - { - **model.dict(), - "cron": "50 23 * * *", - } - ) - context.upsert_model(model) - - plan = context.plan_builder("prod", skip_tests=True).build() - context.apply(plan) - - waiter_revenue_by_day_snapshot = context.get_snapshot(model.name, raise_if_missing=True) - assert waiter_revenue_by_day_snapshot.intervals == [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-07")) - ] - - top_waiters_model = context.get_model("sushi.top_waiters") - top_waiters_model = add_projection_to_model(t.cast(SqlModel, top_waiters_model), literal=True) - context.upsert_model(top_waiters_model) - - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - with time_machine.travel("2023-01-08 23:55:00 UTC"): # Past parent's cron, but before child's - plan = context.plan_builder("dev", skip_tests=True).build() - # Make sure the waiter_revenue_by_day model is not backfilled. - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - -@time_machine.travel("2023-01-08 00:00:00 UTC") -@pytest.mark.parametrize( - "forward_only, expected_intervals", - [ - ( - False, - [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - ], - ), - ( - True, - [ - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - ], - ), - ], -) -def test_cron_not_aligned_with_day_boundary( - init_and_plan_context: t.Callable, - forward_only: bool, - expected_intervals: t.List[t.Tuple[int, int]], -): - context, plan = init_and_plan_context("examples/sushi") - - model = context.get_model("sushi.waiter_revenue_by_day") - model = SqlModel.parse_obj( - { - **model.dict(), - "kind": model.kind.copy(update={"forward_only": forward_only}), - "cron": "0 12 * * *", - } - ) - context.upsert_model(model) - - plan = context.plan_builder("prod", skip_tests=True).build() - context.apply(plan) - - waiter_revenue_by_day_snapshot = context.get_snapshot(model.name, raise_if_missing=True) - assert waiter_revenue_by_day_snapshot.intervals == [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-07")) - ] - - model = add_projection_to_model(t.cast(SqlModel, model), literal=True) - context.upsert_model(model) - - waiter_revenue_by_day_snapshot = context.get_snapshot( - "sushi.waiter_revenue_by_day", raise_if_missing=True - ) - - with time_machine.travel("2023-01-08 00:10:00 UTC"): # Past model's cron. - plan = context.plan_builder( - "dev", select_models=[model.name], skip_tests=True, enable_preview=True - ).build() - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, - intervals=expected_intervals, - ), - ] - - -@time_machine.travel("2023-01-08 00:00:00 UTC") -def test_cron_not_aligned_with_day_boundary_new_model(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - existing_model = context.get_model("sushi.waiter_revenue_by_day") - existing_model = SqlModel.parse_obj( - { - **existing_model.dict(), - "kind": existing_model.kind.copy(update={"forward_only": True}), - } - ) - context.upsert_model(existing_model) - - plan = context.plan_builder("prod", skip_tests=True).build() - context.apply(plan) - - # Add a new model and make a change to a forward-only model. - # The cron of the new model is not aligned with the day boundary. - new_model = load_sql_based_model( - d.parse( - """ - MODEL ( - name memory.sushi.new_model, - kind FULL, - cron '0 8 * * *', - start '2023-01-01', - ); - - SELECT 1 AS one; - """ - ) - ) - context.upsert_model(new_model) - - existing_model = add_projection_to_model(t.cast(SqlModel, existing_model), literal=True) - context.upsert_model(existing_model) - - plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=context.get_snapshot( - "memory.sushi.new_model", raise_if_missing=True - ).snapshot_id, - intervals=[(to_timestamp("2023-01-06"), to_timestamp("2023-01-07"))], - ), - SnapshotIntervals( - snapshot_id=context.get_snapshot( - "sushi.waiter_revenue_by_day", raise_if_missing=True - ).snapshot_id, - intervals=[ - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - -@time_machine.travel("2023-01-08 00:00:00 UTC") -def test_forward_only_preview_child_that_runs_before_parent(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - # This model runs at minute 30 of every hour - upstream_model = load_sql_based_model( - d.parse( - """ - MODEL ( - name memory.sushi.upstream_model, - kind FULL, - cron '30 * * * *', - start '2023-01-01', - ); - - SELECT 1 AS a; - """ - ) - ) - context.upsert_model(upstream_model) - - # This model runs at minute 0 of every hour, so it runs before the upstream model - downstream_model = load_sql_based_model( - d.parse( - """ - MODEL ( - name memory.sushi.downstream_model, - kind INCREMENTAL_BY_TIME_RANGE( - time_column event_date, - forward_only True, - ), - cron '0 * * * *', - start '2023-01-01', - ); - - SELECT a, '2023-01-06' AS event_date FROM memory.sushi.upstream_model; - """ - ) - ) - context.upsert_model(downstream_model) - - context.plan("prod", skip_tests=True, auto_apply=True) - - with time_machine.travel("2023-01-08 00:05:00 UTC"): - # The downstream model runs but not the upstream model - context.run("prod") - - # Now it's time for the upstream model to run but it hasn't run yet - with time_machine.travel("2023-01-08 00:35:00 UTC"): - # Make a change to the downstream model. - downstream_model = add_projection_to_model(t.cast(SqlModel, downstream_model), literal=True) - context.upsert_model(downstream_model) - - # The plan should only backfill the downstream model despite upstream missing intervals - plan = context.plan_builder("dev", skip_tests=True, enable_preview=True).build() - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=context.get_snapshot( - downstream_model.name, raise_if_missing=True - ).snapshot_id, - intervals=[ - (to_timestamp("2023-01-07 23:00:00"), to_timestamp("2023-01-08 00:00:00")) - ], - ), - ] - - -@time_machine.travel("2023-01-08 00:00:00 UTC") -def test_forward_only_monthly_model(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - model = context.get_model("sushi.waiter_revenue_by_day") - model = SqlModel.parse_obj( - { - **model.dict(), - "kind": model.kind.copy(update={"forward_only": True}), - "cron": "0 0 1 * *", - "start": "2022-01-01", - "audits": [], - } - ) - context.upsert_model(model) - - plan = context.plan_builder("prod", skip_tests=True).build() - context.apply(plan) - - waiter_revenue_by_day_snapshot = context.get_snapshot(model.name, raise_if_missing=True) - assert waiter_revenue_by_day_snapshot.intervals == [ - (to_timestamp("2022-01-01"), to_timestamp("2023-01-01")) - ] - - model = add_projection_to_model(t.cast(SqlModel, model), literal=True) - context.upsert_model(model) - - waiter_revenue_by_day_snapshot = context.get_snapshot( - "sushi.waiter_revenue_by_day", raise_if_missing=True - ) - - plan = context.plan_builder( - "dev", select_models=[model.name], skip_tests=True, enable_preview=True - ).build() - assert to_timestamp(plan.start) == to_timestamp("2022-12-01") - assert to_timestamp(plan.end) == to_timestamp("2023-01-08") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, - intervals=[(to_timestamp("2022-12-01"), to_timestamp("2023-01-01"))], - ), - ] - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_forward_only_parent_created_in_dev_child_created_in_prod( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - waiter_revenue_by_day_model = context.get_model("sushi.waiter_revenue_by_day") - waiter_revenue_by_day_model = add_projection_to_model( - t.cast(SqlModel, waiter_revenue_by_day_model) - ) - forward_only_kind = waiter_revenue_by_day_model.kind.copy(update={"forward_only": True}) - waiter_revenue_by_day_model = waiter_revenue_by_day_model.copy( - update={"kind": forward_only_kind} - ) - context.upsert_model(waiter_revenue_by_day_model) - - waiter_revenue_by_day_snapshot = context.get_snapshot( - waiter_revenue_by_day_model, raise_if_missing=True - ) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() - assert len(plan.new_snapshots) == 2 - assert ( - plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert all(s.is_forward_only for s in plan.new_snapshots) - assert plan.start == to_datetime("2023-01-01") - assert not plan.missing_intervals - - context.apply(plan) - - # Update the child to refer to a newly added column. - top_waiters_model = context.get_model("sushi.top_waiters") - top_waiters_model = add_projection_to_model(t.cast(SqlModel, top_waiters_model), literal=False) - context.upsert_model(top_waiters_model) - - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan = context.plan_builder("prod", skip_tests=True, enable_preview=False).build() - assert len(plan.new_snapshots) == 1 - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - - context.apply(plan) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_forward_only_view_migration( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - model = context.get_model("sushi.top_waiters") - assert model.kind.is_view - model = add_projection_to_model(t.cast(SqlModel, model)) - context.upsert_model(model) - - # Apply a forward-only plan - context.plan("prod", skip_tests=True, no_prompts=True, auto_apply=True, forward_only=True) - - # Make sure that the new column got reflected in the view schema - df = context.fetchdf("SELECT one FROM sushi.top_waiters LIMIT 1") - assert len(df) == 1 - - -@time_machine.travel("2023-01-08 00:00:00 UTC") -def test_new_forward_only_model(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - context.plan("dev", skip_tests=True, no_prompts=True, auto_apply=True, enable_preview=False) - - snapshot = context.get_snapshot("sushi.marketing") - - # The deployable table should not exist yet - assert not context.engine_adapter.table_exists(snapshot.table_name()) - assert context.engine_adapter.table_exists(snapshot.table_name(is_deployable=False)) - - context.plan("prod", skip_tests=True, no_prompts=True, auto_apply=True) - - assert context.engine_adapter.table_exists(snapshot.table_name()) - assert context.engine_adapter.table_exists(snapshot.table_name(is_deployable=False)) - - -@time_machine.travel("2023-01-08 00:00:00 UTC") -def test_annotated_self_referential_model(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - # Projections are fully annotated in the query but columns were not specified explicitly - expressions = d.parse( - f""" - MODEL ( - name memory.sushi.test_self_ref, - kind FULL, - start '2023-01-01', - ); - - SELECT 1::INT AS one FROM memory.sushi.test_self_ref; - """ - ) - model = load_sql_based_model(expressions) - assert model.depends_on_self - context.upsert_model(model) - - context.plan("prod", skip_tests=True, no_prompts=True, auto_apply=True) - - df = context.fetchdf("SELECT one FROM memory.sushi.test_self_ref") - assert len(df) == 0 - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_plan_set_choice_is_reflected_in_missing_intervals(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - context.upsert_model(context.get_model("sushi.top_waiters").copy(update={"kind": FullKind()})) - context.plan("prod", skip_tests=True, no_prompts=True, auto_apply=True) - - model_name = "sushi.waiter_revenue_by_day" - - model = context.get_model(model_name) - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - snapshot = context.get_snapshot(model, raise_if_missing=True) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan_builder = context.plan_builder("dev", skip_tests=True) - plan = plan_builder.build() - assert len(plan.new_snapshots) == 2 - assert ( - plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert plan.start == to_timestamp("2023-01-01") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - # Change the category to BREAKING - plan = plan_builder.set_choice( - plan.context_diff.snapshots[snapshot.snapshot_id], SnapshotChangeCategory.BREAKING - ).build() - assert ( - plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.BREAKING - ) - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_BREAKING - ) - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - # Change the category back to NON_BREAKING - plan = plan_builder.set_choice( - plan.context_diff.snapshots[snapshot.snapshot_id], SnapshotChangeCategory.NON_BREAKING - ).build() - assert ( - plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - context.apply(plan) - - dev_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" - ) - assert dev_df["event_date"].tolist() == [ - pd.to_datetime(x) - for x in [ - "2023-01-01", - "2023-01-02", - "2023-01-03", - "2023-01-04", - "2023-01-05", - "2023-01-06", - "2023-01-07", - ] - ] - - # Promote changes to prod - prod_plan = context.plan_builder(skip_tests=True).build() - assert not prod_plan.missing_intervals - - context.apply(prod_plan) - prod_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi.waiter_revenue_by_day WHERE one IS NOT NULL ORDER BY event_date" - ) - assert prod_df["event_date"].tolist() == [ - pd.to_datetime(x) - for x in [ - "2023-01-01", - "2023-01-02", - "2023-01-03", - "2023-01-04", - "2023-01-05", - "2023-01-06", - "2023-01-07", - ] - ] - - -@time_machine.travel("2023-01-08 15:00:00 UTC", tick=True) -@pytest.mark.parametrize("has_view_binding", [False, True]) -def test_non_breaking_change_after_forward_only_in_dev( - init_and_plan_context: t.Callable, has_view_binding: bool -): - context, plan = init_and_plan_context("examples/sushi") - context.snapshot_evaluator.adapter.HAS_VIEW_BINDING = has_view_binding - context.apply(plan) - - model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - waiter_revenue_by_day_snapshot = context.get_snapshot( - "sushi.waiter_revenue_by_day", raise_if_missing=True - ) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan = context.plan_builder("dev", skip_tests=True, forward_only=True).build() - assert len(plan.new_snapshots) == 2 - assert ( - plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert all(s.is_forward_only for s in plan.new_snapshots) - assert to_timestamp(plan.start) == to_timestamp("2023-01-07") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, - intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], - ), - ] - - # Apply the forward-only changes first. - context.apply(plan) - - dev_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" - ) - assert dev_df["event_date"].tolist() == [pd.to_datetime("2023-01-07")] - - # Make a non-breaking change to a model downstream. - model = context.get_model("sushi.top_waiters") - # Select 'one' column from the updated upstream model. - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model), literal=False)) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan = context.plan_builder("dev", skip_tests=True).build() - assert len(plan.new_snapshots) == 1 - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert to_timestamp(plan.start) == to_timestamp("2023-01-01") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - # Apply the non-breaking changes. - context.apply(plan) - - dev_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT waiter_id FROM sushi__dev.top_waiters WHERE one IS NOT NULL" - ) - assert not dev_df.empty - - prod_df = context.engine_adapter.fetchdf("DESCRIBE sushi.top_waiters") - assert "one" not in prod_df["column_name"].tolist() - - # Deploy both changes to prod. - plan = context.plan_builder("prod", skip_tests=True).build() - assert plan.start == to_timestamp("2023-01-01") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - context.apply(plan) - - prod_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi.waiter_revenue_by_day WHERE one IS NOT NULL ORDER BY event_date" - ) - assert prod_df.empty - - prod_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT waiter_id FROM sushi.top_waiters WHERE one IS NOT NULL" - ) - assert prod_df.empty - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_indirect_non_breaking_change_after_forward_only_in_dev(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - # Make sure that the most downstream model is a materialized model. - model = context.get_model("sushi.top_waiters") - model = model.copy(update={"kind": FullKind()}) - context.upsert_model(model) - context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) - - # Make sushi.orders a forward-only model. - model = context.get_model("sushi.orders") - updated_model_kind = model.kind.copy(update={"forward_only": True}) - model = model.copy(update={"stamp": "force new version", "kind": updated_model_kind}) - context.upsert_model(model) - snapshot = context.get_snapshot(model, raise_if_missing=True) - - plan = context.plan_builder( - "dev", - skip_tests=True, - enable_preview=False, - categorizer_config=CategorizerConfig.all_full(), - ).build() - assert ( - plan.context_diff.snapshots[snapshot.snapshot_id].change_category - == SnapshotChangeCategory.BREAKING - ) - assert plan.context_diff.snapshots[snapshot.snapshot_id].is_forward_only - assert not plan.requires_backfill - context.apply(plan) - - # Make a non-breaking change to a model. - model = context.get_model("sushi.top_waiters") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() - assert len(plan.new_snapshots) == 1 - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert plan.start == to_timestamp("2023-01-01") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - # Apply the non-breaking changes. - context.apply(plan) - - # Make a non-breaking change upstream from the previously modified model. - model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - waiter_revenue_by_day_snapshot = context.get_snapshot( - "sushi.waiter_revenue_by_day", raise_if_missing=True - ) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan = context.plan_builder("dev", skip_tests=True, enable_preview=False).build() - assert len(plan.new_snapshots) == 2 - assert ( - plan.context_diff.snapshots[waiter_revenue_by_day_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[top_waiters_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert plan.start == to_timestamp("2023-01-01") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - # Apply the upstream non-breaking changes. - context.apply(plan) - assert not context.plan_builder("dev", skip_tests=True).build().requires_backfill - - # Deploy everything to prod. - plan = context.plan_builder("prod", skip_tests=True, enable_preview=False).build() - assert plan.start == to_timestamp("2023-01-01") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiters_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=waiter_revenue_by_day_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - context.apply(plan) - assert ( - not context.plan_builder("prod", skip_tests=True, enable_preview=False) - .build() - .requires_backfill - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_changes_downstream_of_indirect_non_breaking_snapshot_without_intervals( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - # Make a breaking change first but don't backfill it - model = context.get_model("sushi.orders") - model = model.copy(update={"stamp": "force new version"}) - context.upsert_model(model) - plan_builder = context.plan_builder( - "dev", skip_backfill=True, skip_tests=True, no_auto_categorization=True - ) - plan_builder.set_choice(context.get_snapshot(model), SnapshotChangeCategory.BREAKING) - context.apply(plan_builder.build()) - - # Now make a non-breaking change to the same snapshot. - model = model.copy(update={"stamp": "force another new version"}) - context.upsert_model(model) - plan_builder = context.plan_builder( - "dev", skip_backfill=True, skip_tests=True, no_auto_categorization=True - ) - plan_builder.set_choice(context.get_snapshot(model), SnapshotChangeCategory.NON_BREAKING) - context.apply(plan_builder.build()) - - # Now make a change to a model downstream of the above model. - downstream_model = context.get_model("sushi.top_waiters") - downstream_model = downstream_model.copy(update={"stamp": "yet another new version"}) - context.upsert_model(downstream_model) - plan = context.plan_builder("dev", skip_tests=True).build() - - # If the parent is not representative then the child cannot be deployable - deployability_index = plan.deployability_index - assert not deployability_index.is_representative( - context.get_snapshot("sushi.waiter_revenue_by_day") - ) - assert not deployability_index.is_deployable(context.get_snapshot("sushi.top_waiters")) - - -@time_machine.travel("2023-01-08 15:00:00 UTC", tick=True) -def test_metadata_change_after_forward_only_results_in_migration(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - # Make a forward-only change - model = context.get_model("sushi.waiter_revenue_by_day") - model = model.copy(update={"kind": model.kind.copy(update={"forward_only": True})}) - model = add_projection_to_model(t.cast(SqlModel, model)) - context.upsert_model(model) - plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) - assert len(plan.new_snapshots) == 2 - assert all(s.is_forward_only for s in plan.new_snapshots) - - # Follow-up with a metadata change in the same environment - model = model.copy(update={"owner": "new_owner"}) - context.upsert_model(model) - plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) - assert len(plan.new_snapshots) == 2 - assert all(s.change_category == SnapshotChangeCategory.METADATA for s in plan.new_snapshots) - - # Deploy the latest change to prod - context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) - - # Check that the new column was added in prod - columns = context.engine_adapter.columns("sushi.waiter_revenue_by_day") - assert "one" in columns - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_indirect_non_breaking_downstream_of_forward_only(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - # Make sushi.orders a forward-only model. - forward_only_model = context.get_model("sushi.orders") - updated_model_kind = forward_only_model.kind.copy(update={"forward_only": True}) - forward_only_model = forward_only_model.copy( - update={"stamp": "force new version", "kind": updated_model_kind} - ) - context.upsert_model(forward_only_model) - forward_only_snapshot = context.get_snapshot(forward_only_model, raise_if_missing=True) - - non_breaking_model = context.get_model("sushi.waiter_revenue_by_day") - non_breaking_model = non_breaking_model.copy(update={"start": "2023-01-01"}) - context.upsert_model(add_projection_to_model(t.cast(SqlModel, non_breaking_model))) - non_breaking_snapshot = context.get_snapshot(non_breaking_model, raise_if_missing=True) - top_waiter_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan = context.plan_builder( - "dev", - skip_tests=True, - enable_preview=False, - categorizer_config=CategorizerConfig.all_full(), - ).build() - assert ( - plan.context_diff.snapshots[forward_only_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.BREAKING - ) - assert ( - plan.context_diff.snapshots[non_breaking_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[top_waiter_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert plan.context_diff.snapshots[forward_only_snapshot.snapshot_id].is_forward_only - assert not plan.context_diff.snapshots[non_breaking_snapshot.snapshot_id].is_forward_only - assert not plan.context_diff.snapshots[top_waiter_snapshot.snapshot_id].is_forward_only - - assert plan.start == to_timestamp("2023-01-01") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiter_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=non_breaking_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - context.apply(plan) - assert ( - not context.plan_builder("dev", skip_tests=True, enable_preview=False) - .build() - .requires_backfill - ) - - # Deploy everything to prod. - plan = context.plan_builder("prod", skip_tests=True).build() - assert plan.start == to_timestamp("2023-01-01") - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=top_waiter_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - SnapshotIntervals( - snapshot_id=non_breaking_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - context.apply(plan) - assert ( - not context.plan_builder("prod", skip_tests=True, enable_preview=False) - .build() - .requires_backfill - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_breaking_only_impacts_immediate_children(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - context.upsert_model(context.get_model("sushi.top_waiters").copy(update={"kind": FullKind()})) - context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) - - breaking_model = context.get_model("sushi.orders") - breaking_model = breaking_model.copy(update={"stamp": "force new version"}) - context.upsert_model(breaking_model) - breaking_snapshot = context.get_snapshot(breaking_model, raise_if_missing=True) - - non_breaking_model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, non_breaking_model))) - non_breaking_snapshot = context.get_snapshot(non_breaking_model, raise_if_missing=True) - top_waiter_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - - plan_builder = context.plan_builder("dev", skip_tests=True, enable_preview=False) - plan_builder.set_choice(breaking_snapshot, SnapshotChangeCategory.BREAKING) - plan = plan_builder.build() - assert ( - plan.context_diff.snapshots[breaking_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.BREAKING - ) - assert ( - plan.context_diff.snapshots[non_breaking_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.snapshots[top_waiter_snapshot.snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert plan.start == to_timestamp("2023-01-01") - assert not any(i.snapshot_id == top_waiter_snapshot.snapshot_id for i in plan.missing_intervals) - - context.apply(plan) - assert ( - not context.plan_builder("dev", skip_tests=True, enable_preview=False) - .build() - .requires_backfill - ) - - # Deploy everything to prod. - plan = context.plan_builder("prod", skip_tests=True).build() - assert not plan.missing_intervals - - context.apply(plan) - assert ( - not context.plan_builder("prod", skip_tests=True, enable_preview=False) - .build() - .requires_backfill - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_run_with_select_models( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - with time_machine.travel("2023-01-09 00:00:00 UTC"): - assert context.run(select_models=["*waiter_revenue_by_day"]) - - snapshots = context.state_sync.state_sync.get_snapshots(context.snapshots.values()) - # Only waiter_revenue_by_day and its parents should be backfilled up to 2023-01-09. - assert {s.name: s.intervals[0][1] for s in snapshots.values() if s.intervals} == { - '"memory"."sushi"."waiter_revenue_by_day"': to_timestamp("2023-01-09"), - '"memory"."sushi"."order_items"': to_timestamp("2023-01-09"), - '"memory"."sushi"."orders"': to_timestamp("2023-01-09"), - '"memory"."sushi"."items"': to_timestamp("2023-01-09"), - '"memory"."sushi"."customer_revenue_lifetime"': to_timestamp("2023-01-08"), - '"memory"."sushi"."customer_revenue_by_day"': to_timestamp("2023-01-08"), - '"memory"."sushi"."latest_order"': to_timestamp("2023-01-08"), - '"memory"."sushi"."waiter_names"': to_timestamp("2023-01-08"), - '"memory"."sushi"."raw_marketing"': to_timestamp("2023-01-08"), - '"memory"."sushi"."marketing"': to_timestamp("2023-01-08"), - '"memory"."sushi"."waiter_as_customer_by_day"': to_timestamp("2023-01-08"), - '"memory"."sushi"."top_waiters"': to_timestamp("2023-01-08"), - '"memory"."raw"."demographics"': to_timestamp("2023-01-08"), - "assert_item_price_above_zero": to_timestamp("2023-01-08"), - '"memory"."sushi"."active_customers"': to_timestamp("2023-01-08"), - '"memory"."sushi"."customers"': to_timestamp("2023-01-08"), - '"memory"."sushi"."count_customers_active"': to_timestamp("2023-01-08"), - '"memory"."sushi"."count_customers_inactive"': to_timestamp("2023-01-08"), - } - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_seed_model_promote_to_prod_after_dev( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - with open(context.path / "seeds" / "waiter_names.csv", "a") as f: - f.write("\n10,New Waiter") - - context.load() - - waiter_names_snapshot = context.get_snapshot("sushi.waiter_names") - plan = context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) - assert waiter_names_snapshot.snapshot_id in plan.directly_modified - - # Trigger a metadata change to reuse the previous version - waiter_names_model = waiter_names_snapshot.model.copy( - update={"description": "Updated description"} - ) - context.upsert_model(waiter_names_model) - context.plan("dev", skip_tests=True, auto_apply=True, no_prompts=True) - - # Promote all changes to prod - waiter_names_snapshot = context.get_snapshot("sushi.waiter_names") - plan = context.plan_builder("prod", skip_tests=True).build() - # Clear the cache to source the dehydrated model instance from the state - context.clear_caches() - context.apply(plan) - - assert ( - context.engine_adapter.fetchone("SELECT COUNT(*) FROM sushi.waiter_names WHERE id = 10")[0] - == 1 - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_plan_with_run( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - - with time_machine.travel("2023-01-09 00:00:00 UTC"): - plan = context.plan(run=True) - assert plan.has_changes - assert plan.missing_intervals - - context.apply(plan) - - snapshots = context.state_sync.state_sync.get_snapshots(context.snapshots.values()) - assert {s.name: s.intervals[0][1] for s in snapshots.values() if s.intervals} == { - '"memory"."sushi"."waiter_revenue_by_day"': to_timestamp("2023-01-09"), - '"memory"."sushi"."order_items"': to_timestamp("2023-01-09"), - '"memory"."sushi"."orders"': to_timestamp("2023-01-09"), - '"memory"."sushi"."items"': to_timestamp("2023-01-09"), - '"memory"."sushi"."customer_revenue_lifetime"': to_timestamp("2023-01-09"), - '"memory"."sushi"."customer_revenue_by_day"': to_timestamp("2023-01-09"), - '"memory"."sushi"."latest_order"': to_timestamp("2023-01-09"), - '"memory"."sushi"."waiter_names"': to_timestamp("2023-01-08"), - '"memory"."sushi"."raw_marketing"': to_timestamp("2023-01-09"), - '"memory"."sushi"."marketing"': to_timestamp("2023-01-09"), - '"memory"."sushi"."waiter_as_customer_by_day"': to_timestamp("2023-01-09"), - '"memory"."sushi"."top_waiters"': to_timestamp("2023-01-09"), - '"memory"."raw"."demographics"': to_timestamp("2023-01-09"), - "assert_item_price_above_zero": to_timestamp("2023-01-09"), - '"memory"."sushi"."active_customers"': to_timestamp("2023-01-09"), - '"memory"."sushi"."customers"': to_timestamp("2023-01-09"), - '"memory"."sushi"."count_customers_active"': to_timestamp("2023-01-09"), - '"memory"."sushi"."count_customers_inactive"': to_timestamp("2023-01-09"), - } - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_plan_ignore_cron( - init_and_plan_context: t.Callable, -): - context, _ = init_and_plan_context("examples/sushi") - - expressions = d.parse( - f""" - MODEL ( - name memory.sushi.test_allow_partials, - kind INCREMENTAL_UNMANAGED, - allow_partials true, - start '2023-01-01', - ); - - SELECT @end_ts AS end_ts - """ - ) - model = load_sql_based_model(expressions) - - context.upsert_model(model) - context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) - - assert ( - context.engine_adapter.fetchone("SELECT MAX(end_ts) FROM memory.sushi.test_allow_partials")[ - 0 - ] - == "2023-01-07 23:59:59.999999" - ) - - plan_no_ignore_cron = context.plan_builder( - "prod", run=True, ignore_cron=False, skip_tests=True - ).build() - assert not plan_no_ignore_cron.missing_intervals - - plan = context.plan_builder("prod", run=True, ignore_cron=True, skip_tests=True).build() - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=context.get_snapshot(model, raise_if_missing=True).snapshot_id, - intervals=[ - (to_timestamp("2023-01-08"), to_timestamp("2023-01-08 15:00:00")), - ], - ) - ] - context.apply(plan) - - assert ( - context.engine_adapter.fetchone("SELECT MAX(end_ts) FROM memory.sushi.test_allow_partials")[ - 0 - ] - == "2023-01-08 14:59:59.999999" - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_run_respects_excluded_transitive_dependencies(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - # Graph: C <- B <- A - # B is a transitive dependency linking A and C - # Note that the alphabetical ordering of the model names is intentional and helps - # surface the problem - expressions_a = d.parse( - f""" - MODEL ( - name memory.sushi.test_model_c, - kind FULL, - allow_partials true, - cron '@hourly', - ); - - SELECT @execution_ts AS execution_ts - """ - ) - model_c = load_sql_based_model(expressions_a) - context.upsert_model(model_c) - - # A VIEW model with no partials allowed and a daily cron instead of hourly. - expressions_b = d.parse( - f""" - MODEL ( - name memory.sushi.test_model_b, - kind VIEW, - allow_partials false, - cron '@daily', - ); - - SELECT * FROM memory.sushi.test_model_c - """ - ) - model_b = load_sql_based_model(expressions_b) - context.upsert_model(model_b) - - expressions_a = d.parse( - f""" - MODEL ( - name memory.sushi.test_model_a, - kind FULL, - allow_partials true, - cron '@hourly', - ); - - SELECT * FROM memory.sushi.test_model_b - """ - ) - model_a = load_sql_based_model(expressions_a) - context.upsert_model(model_a) - - context.plan("prod", skip_tests=True, auto_apply=True, no_prompts=True) - assert ( - context.fetchdf("SELECT execution_ts FROM memory.sushi.test_model_c")["execution_ts"].iloc[ - 0 - ] - == "2023-01-08 15:00:00" - ) - - with time_machine.travel("2023-01-08 17:00:00 UTC", tick=False): - context.run( - "prod", - select_models=["*test_model_c", "*test_model_a"], - no_auto_upstream=True, - ignore_cron=True, - ) - assert ( - context.fetchdf("SELECT execution_ts FROM memory.sushi.test_model_a")[ - "execution_ts" - ].iloc[0] - == "2023-01-08 17:00:00" - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_run_with_select_models_no_auto_upstream( - init_and_plan_context: t.Callable, -): - context, _ = init_and_plan_context("examples/sushi") - - model = context.get_model("sushi.waiter_revenue_by_day") - model = SqlModel.parse_obj({**model.dict(), "audits": []}) - context.upsert_model(model) - - context.plan("prod", no_prompts=True, skip_tests=True, auto_apply=True) - - with time_machine.travel("2023-01-09 00:00:00 UTC"): - assert context.run(select_models=["*waiter_revenue_by_day"], no_auto_upstream=True) - - snapshots = context.state_sync.state_sync.get_snapshots(context.snapshots.values()) - # Only waiter_revenue_by_day should be backfilled up to 2023-01-09. - assert {s.name: s.intervals[0][1] for s in snapshots.values() if s.intervals} == { - '"memory"."sushi"."waiter_revenue_by_day"': to_timestamp("2023-01-09"), - '"memory"."sushi"."order_items"': to_timestamp("2023-01-08"), - '"memory"."sushi"."orders"': to_timestamp("2023-01-08"), - '"memory"."sushi"."items"': to_timestamp("2023-01-08"), - '"memory"."sushi"."customer_revenue_lifetime"': to_timestamp("2023-01-08"), - '"memory"."sushi"."customer_revenue_by_day"': to_timestamp("2023-01-08"), - '"memory"."sushi"."latest_order"': to_timestamp("2023-01-08"), - '"memory"."sushi"."waiter_names"': to_timestamp("2023-01-08"), - '"memory"."sushi"."raw_marketing"': to_timestamp("2023-01-08"), - '"memory"."sushi"."marketing"': to_timestamp("2023-01-08"), - '"memory"."sushi"."waiter_as_customer_by_day"': to_timestamp("2023-01-08"), - '"memory"."sushi"."top_waiters"': to_timestamp("2023-01-08"), - '"memory"."raw"."demographics"': to_timestamp("2023-01-08"), - "assert_item_price_above_zero": to_timestamp("2023-01-08"), - '"memory"."sushi"."active_customers"': to_timestamp("2023-01-08"), - '"memory"."sushi"."customers"': to_timestamp("2023-01-08"), - '"memory"."sushi"."count_customers_active"': to_timestamp("2023-01-08"), - '"memory"."sushi"."count_customers_inactive"': to_timestamp("2023-01-08"), - } - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_select_models(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - # Modify 2 models. - model = context.get_model("sushi.waiter_revenue_by_day") - kwargs = { - **model.dict(), - # Make a breaking change. - "query": model.query.order_by("waiter_id"), # type: ignore - } - context.upsert_model(SqlModel.parse_obj(kwargs)) - - model = context.get_model("sushi.customer_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - - expected_intervals = [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ] - - waiter_revenue_by_day_snapshot_id = context.get_snapshot( - "sushi.waiter_revenue_by_day", raise_if_missing=True - ).snapshot_id - - # Select one of the modified models. - plan_builder = context.plan_builder( - "dev", select_models=["*waiter_revenue_by_day"], skip_tests=True - ) - snapshot = plan_builder._context_diff.snapshots[waiter_revenue_by_day_snapshot_id] - plan_builder.set_choice(snapshot, SnapshotChangeCategory.BREAKING) - plan = plan_builder.build() - - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=waiter_revenue_by_day_snapshot_id, - intervals=expected_intervals, - ), - ] - - context.apply(plan) - - dev_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" - ) - assert len(dev_df) == 7 - - # Make sure that we only create a view for the selected model. - schema_objects = context.engine_adapter.get_data_objects("sushi__dev") - assert len(schema_objects) == 1 - assert schema_objects[0].name == "waiter_revenue_by_day" - - # Validate the other modified model. - assert not context.get_snapshot("sushi.customer_revenue_by_day").change_category - assert not context.get_snapshot("sushi.customer_revenue_by_day").version - - # Validate the downstream model. - assert not context.engine_adapter.table_exists( - context.get_snapshot("sushi.top_waiters").table_name() - ) - assert not context.engine_adapter.table_exists( - context.get_snapshot("sushi.top_waiters").table_name(False) - ) - - # Make sure that tables are created when deploying to prod. - plan = context.plan("prod", skip_tests=True) - context.apply(plan) - assert context.engine_adapter.table_exists( - context.get_snapshot("sushi.top_waiters").table_name() - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_select_unchanged_model_for_backfill(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - # Modify 2 models. - model = context.get_model("sushi.waiter_revenue_by_day") - kwargs = { - **model.dict(), - # Make a breaking change. - "query": d.parse_one( - f"{model.query.sql(dialect='duckdb')} ORDER BY waiter_id", dialect="duckdb" - ), - } - context.upsert_model(SqlModel.parse_obj(kwargs)) - - model = context.get_model("sushi.customer_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - - expected_intervals = [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ] - - waiter_revenue_by_day_snapshot_id = context.get_snapshot( - "sushi.waiter_revenue_by_day", raise_if_missing=True - ).snapshot_id - - # Select one of the modified models. - plan_builder = context.plan_builder( - "dev", select_models=["*waiter_revenue_by_day"], skip_tests=True - ) - snapshot = plan_builder._context_diff.snapshots[waiter_revenue_by_day_snapshot_id] - plan_builder.set_choice(snapshot, SnapshotChangeCategory.BREAKING) - plan = plan_builder.build() - - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=waiter_revenue_by_day_snapshot_id, - intervals=expected_intervals, - ), - ] - - context.apply(plan) - - # Make sure that we only create a view for the selected model. - schema_objects = context.engine_adapter.get_data_objects("sushi__dev") - assert {o.name for o in schema_objects} == {"waiter_revenue_by_day"} - - # Now select a model downstream from the previously modified one in order to backfill it. - plan = context.plan_builder("dev", select_models=["*top_waiters"], skip_tests=True).build() - - assert not plan.has_changes - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=context.get_snapshot( - "sushi.top_waiters", raise_if_missing=True - ).snapshot_id, - intervals=expected_intervals, - ), - ] - - context.apply(plan) - - # Make sure that a view has been created for the downstream selected model. - schema_objects = context.engine_adapter.get_data_objects("sushi__dev") - assert {o.name for o in schema_objects} == {"waiter_revenue_by_day", "top_waiters"} - - -@time_machine.travel("2023-01-08 00:00:00 UTC") -def test_snapshot_triggers(init_and_plan_context: t.Callable, mocker: MockerFixture): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - # auto-restatement triggers - orders = context.get_model("sushi.orders") - orders_kind = { - **orders.kind.dict(), - "auto_restatement_cron": "@hourly", - } - orders_kwargs = { - **orders.dict(), - "kind": orders_kind, - } - context.upsert_model(PythonModel.parse_obj(orders_kwargs)) - - order_items = context.get_model("sushi.order_items") - order_items_kind = { - **order_items.kind.dict(), - "auto_restatement_cron": "@hourly", - } - order_items_kwargs = { - **order_items.dict(), - "kind": order_items_kind, - } - context.upsert_model(PythonModel.parse_obj(order_items_kwargs)) - - waiter_revenue_by_day = context.get_model("sushi.waiter_revenue_by_day") - waiter_revenue_by_day_kind = { - **waiter_revenue_by_day.kind.dict(), - "auto_restatement_cron": "@hourly", - } - waiter_revenue_by_day_kwargs = { - **waiter_revenue_by_day.dict(), - "kind": waiter_revenue_by_day_kind, - } - context.upsert_model(SqlModel.parse_obj(waiter_revenue_by_day_kwargs)) - - context.plan(auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full()) - - scheduler = context.scheduler() - - import sqlmesh - - spy = mocker.spy(sqlmesh.core.scheduler.Scheduler, "run_merged_intervals") - - with time_machine.travel("2023-01-09 00:00:01 UTC"): - scheduler.run( - environment=c.PROD, - start="2023-01-01", - auto_restatement_enabled=True, - ) - - assert spy.called - - actual_triggers = spy.call_args.kwargs["auto_restatement_triggers"] - actual_triggers = {k: v for k, v in actual_triggers.items() if v} - assert len(actual_triggers) == 12 - - for id, trigger in actual_triggers.items(): - model_name = id.name.replace('"memory"."sushi".', "").replace('"', "") - auto_restatement_triggers = [ - t.name.replace('"memory"."sushi".', "").replace('"', "") for t in trigger - ] - - if model_name in ("orders", "order_items", "waiter_revenue_by_day"): - assert auto_restatement_triggers == [model_name] - elif model_name in ("customer_revenue_lifetime", "customer_revenue_by_day"): - assert sorted(auto_restatement_triggers) == sorted(["orders", "order_items"]) - elif model_name == "top_waiters": - assert auto_restatement_triggers == ["waiter_revenue_by_day"] - else: - assert auto_restatement_triggers == ["orders"] - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_max_interval_end_per_model_not_applied_when_end_is_provided( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - with time_machine.travel("2023-01-09 00:00:00 UTC"): - context.run() - - plan = context.plan_builder( - restate_models=["*"], start="2023-01-09", end="2023-01-09" - ).build() - context.apply(plan) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_select_models_for_backfill(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - expected_intervals = [ - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ] - - plan = context.plan_builder( - "dev", backfill_models=["+*waiter_revenue_by_day"], skip_tests=True - ).build() - - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=context.get_snapshot("sushi.items", raise_if_missing=True).snapshot_id, - intervals=expected_intervals, - ), - SnapshotIntervals( - snapshot_id=context.get_snapshot( - "sushi.order_items", raise_if_missing=True - ).snapshot_id, - intervals=expected_intervals, - ), - SnapshotIntervals( - snapshot_id=context.get_snapshot("sushi.orders", raise_if_missing=True).snapshot_id, - intervals=expected_intervals, - ), - SnapshotIntervals( - snapshot_id=context.get_snapshot( - "sushi.waiter_revenue_by_day", raise_if_missing=True - ).snapshot_id, - intervals=expected_intervals, - ), - ] - - context.apply(plan) - - dev_df = context.engine_adapter.fetchdf( - "SELECT DISTINCT event_date FROM sushi__dev.waiter_revenue_by_day ORDER BY event_date" - ) - assert len(dev_df) == 1 - - schema_objects = context.engine_adapter.get_data_objects("sushi__dev") - assert {o.name for o in schema_objects} == { - "items", - "order_items", - "orders", - "waiter_revenue_by_day", - } - - assert not context.engine_adapter.table_exists( - context.get_snapshot("sushi.customer_revenue_by_day").table_name() - ) - - # Make sure that tables are created when deploying to prod. - plan = context.plan("prod") - context.apply(plan) - assert context.engine_adapter.table_exists( - context.get_snapshot("sushi.customer_revenue_by_day").table_name() - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_dbt_select_star_is_directly_modified(sushi_test_dbt_context: Context): - context = sushi_test_dbt_context - - model = context.get_model("sushi.simple_model_a") - context.upsert_model( - model, - query_=ParsableSql(sql="SELECT 1 AS a, 2 AS b"), - ) - - snapshot_a_id = context.get_snapshot("sushi.simple_model_a").snapshot_id # type: ignore - snapshot_b_id = context.get_snapshot("sushi.simple_model_b").snapshot_id # type: ignore - - plan = context.plan_builder("dev", skip_tests=True).build() - assert plan.directly_modified == {snapshot_a_id, snapshot_b_id} - assert {i.snapshot_id for i in plan.missing_intervals} == {snapshot_a_id, snapshot_b_id} - - assert plan.snapshots[snapshot_a_id].change_category == SnapshotChangeCategory.NON_BREAKING - assert plan.snapshots[snapshot_b_id].change_category == SnapshotChangeCategory.NON_BREAKING - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_dbt_is_incremental_table_is_missing(sushi_test_dbt_context: Context): - context = sushi_test_dbt_context - - model = context.get_model("sushi.waiter_revenue_by_day_v2") - model = model.copy(update={"kind": IncrementalUnmanagedKind(), "start": "2023-01-01"}) - context.upsert_model(model) - context._standalone_audits["test_top_waiters"].start = "2023-01-01" - - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - - snapshot = context.get_snapshot("sushi.waiter_revenue_by_day_v2") - assert snapshot - - # Manually drop the table - context.engine_adapter.drop_table(snapshot.table_name()) - - context.snapshot_evaluator.evaluate( - snapshot, - start="2023-01-01", - end="2023-01-08", - execution_time="2023-01-08 15:00:00", - snapshots={s.name: s for s in context.snapshots.values()}, - deployability_index=DeployabilityIndex.all_deployable(), - ) - - # Make sure the table was recreated - assert context.engine_adapter.table_exists(snapshot.table_name()) - - -def test_model_attr(sushi_test_dbt_context: Context, assert_exp_eq): - context = sushi_test_dbt_context - model = context.get_model("sushi.top_waiters") - assert_exp_eq( - model.render_query(), - """ - SELECT - CAST("waiter_id" AS INT) AS "waiter_id", - CAST("revenue" AS DOUBLE) AS "revenue", - 3 AS "model_columns" - FROM "memory"."sushi"."waiter_revenue_by_day_v2" AS "waiter_revenue_by_day_v2" - WHERE - "ds" = ( - SELECT - MAX("ds") - FROM "memory"."sushi"."waiter_revenue_by_day_v2" AS "waiter_revenue_by_day_v2" - ) - ORDER BY - "revenue" DESC NULLS FIRST - LIMIT 10 - """, - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_incremental_by_partition(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - source_name = "raw.test_incremental_by_partition" - model_name = "memory.sushi.test_incremental_by_partition" - - expressions = d.parse( - f""" - MODEL ( - name {model_name}, - kind INCREMENTAL_BY_PARTITION (disable_restatement false), - partitioned_by [key], - allow_partials true, - start '2023-01-07', - ); - - SELECT key, value FROM {source_name}; - """ - ) - model = load_sql_based_model(expressions) - context.upsert_model(model) - - context.engine_adapter.ctas( - source_name, - d.parse_one("SELECT 'key_a' AS key, 1 AS value"), - ) - - context.plan(auto_apply=True, no_prompts=True) - assert context.engine_adapter.fetchall(f"SELECT * FROM {model_name}") == [ - ("key_a", 1), - ] - - context.engine_adapter.replace_query( - source_name, - d.parse_one("SELECT 'key_b' AS key, 1 AS value"), - ) - context.run(ignore_cron=True) - assert context.engine_adapter.fetchall(f"SELECT * FROM {model_name}") == [ - ("key_a", 1), - ("key_b", 1), - ] - - context.engine_adapter.replace_query( - source_name, - d.parse_one("SELECT 'key_a' AS key, 2 AS value"), - ) - # Run 1 minute later. - with time_machine.travel("2023-01-08 15:01:00 UTC"): - context.run(ignore_cron=True) - assert context.engine_adapter.fetchall(f"SELECT * FROM {model_name}") == [ - ("key_b", 1), - ("key_a", 2), - ] - - # model should fully refresh on restatement - context.engine_adapter.replace_query( - source_name, - d.parse_one("SELECT 'key_c' AS key, 3 AS value"), - ) - context.plan(auto_apply=True, no_prompts=True, restate_models=[model_name]) - assert context.engine_adapter.fetchall(f"SELECT * FROM {model_name}") == [ - ("key_c", 3), - ] - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_custom_materialization(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - custom_insert_called = False - - class CustomFullMaterialization(CustomMaterialization): - NAME = "test_custom_full" - - def insert( - self, - table_name: str, - query_or_df: QueryOrDF, - model: Model, - is_first_insert: bool, - render_kwargs: t.Dict[str, t.Any], - **kwargs: t.Any, - ) -> None: - nonlocal custom_insert_called - custom_insert_called = True - - self._replace_query_for_model(model, table_name, query_or_df, render_kwargs) - - model = context.get_model("sushi.top_waiters") - kwargs = { - **model.dict(), - # Make a breaking change. - "kind": dict(name="CUSTOM", materialization="test_custom_full"), - } - context.upsert_model(SqlModel.parse_obj(kwargs)) - - context.plan(auto_apply=True, no_prompts=True) - - assert custom_insert_called - - -# needs to be defined at the top level. If its defined within the test body, -# adding to the snapshot cache fails with: AttributeError: Can't pickle local object -class TestCustomKind(CustomKind): - __test__ = False # prevent pytest warning since this isnt a class containing tests - - @property - def custom_property(self) -> str: - return validate_string(self.materialization_properties.get("custom_property")) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_custom_materialization_with_custom_kind(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - custom_insert_calls = [] - - class CustomFullMaterialization(CustomMaterialization[TestCustomKind]): - NAME = "test_custom_full_with_custom_kind" - - def insert( - self, - table_name: str, - query_or_df: QueryOrDF, - model: Model, - is_first_insert: bool, - render_kwargs: t.Dict[str, t.Any], - **kwargs: t.Any, - ) -> None: - assert isinstance(model.kind, TestCustomKind) - - nonlocal custom_insert_calls - custom_insert_calls.append(model.kind.custom_property) - - self._replace_query_for_model(model, table_name, query_or_df, render_kwargs) - - model = context.get_model("sushi.top_waiters") - kwargs = { - **model.dict(), - # Make a breaking change. - "kind": dict( - name="CUSTOM", - materialization="test_custom_full_with_custom_kind", - materialization_properties={"custom_property": "pytest"}, - ), - } - context.upsert_model(SqlModel.parse_obj(kwargs)) - - context.plan(auto_apply=True) - - assert custom_insert_calls == ["pytest"] - - # no changes - context.plan(auto_apply=True) - - assert custom_insert_calls == ["pytest"] - - # change a property on the custom kind, breaking change - kwargs["kind"]["materialization_properties"]["custom_property"] = "some value" - context.upsert_model(SqlModel.parse_obj(kwargs)) - context.plan(auto_apply=True) - - assert custom_insert_calls == ["pytest", "some value"] - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_indirect_non_breaking_view_model_non_representative_snapshot( - init_and_plan_context: t.Callable, -): - context, _ = init_and_plan_context("examples/sushi") - - # Forward-only parent - forward_only_model_name = "memory.sushi.test_forward_only_model" - forward_only_model_expressions = d.parse( - f""" - MODEL ( - name {forward_only_model_name}, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - ), - ); - - SELECT '2023-01-01' AS ds, 'value' AS value; - """ - ) - forward_only_model = load_sql_based_model(forward_only_model_expressions) - assert forward_only_model.forward_only - context.upsert_model(forward_only_model) - - # FULL downstream model. - full_downstream_model_name = "memory.sushi.test_full_downstream_model" - full_downstream_model_expressions = d.parse( - f""" - MODEL ( - name {full_downstream_model_name}, - kind FULL, - ); - - SELECT ds, value FROM {forward_only_model_name}; - """ - ) - full_downstream_model = load_sql_based_model(full_downstream_model_expressions) - context.upsert_model(full_downstream_model) - - # VIEW downstream of the previous FULL model. - view_downstream_model_name = "memory.sushi.test_view_downstream_model" - view_downstream_model_expressions = d.parse( - f""" - MODEL ( - name {view_downstream_model_name}, - kind VIEW, - ); - - SELECT ds, value FROM {full_downstream_model_name}; - """ - ) - view_downstream_model = load_sql_based_model(view_downstream_model_expressions) - context.upsert_model(view_downstream_model) - - # Apply the initial plan with all 3 models. - context.plan(auto_apply=True, no_prompts=True) - - # Make a change to the forward-only model and apply it in dev. - context.upsert_model(add_projection_to_model(t.cast(SqlModel, forward_only_model))) - forward_only_model_snapshot_id = context.get_snapshot(forward_only_model_name).snapshot_id - full_downstream_model_snapshot_id = context.get_snapshot(full_downstream_model_name).snapshot_id - view_downstream_model_snapshot_id = context.get_snapshot(view_downstream_model_name).snapshot_id - dev_plan = context.plan("dev", auto_apply=True, no_prompts=True, enable_preview=False) - assert ( - dev_plan.snapshots[forward_only_model_snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - dev_plan.snapshots[full_downstream_model_snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert ( - dev_plan.snapshots[view_downstream_model_snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert not dev_plan.missing_intervals - - # Make a follow-up breaking change to the downstream full model. - new_full_downstream_model_expressions = d.parse( - f""" - MODEL ( - name {full_downstream_model_name}, - kind FULL, - ); - - SELECT ds, 'new_value' AS value FROM {forward_only_model_name}; - """ - ) - new_full_downstream_model = load_sql_based_model(new_full_downstream_model_expressions) - context.upsert_model(new_full_downstream_model) - full_downstream_model_snapshot_id = context.get_snapshot(full_downstream_model_name).snapshot_id - view_downstream_model_snapshot_id = context.get_snapshot(view_downstream_model_name).snapshot_id - dev_plan = context.plan( - "dev", - categorizer_config=CategorizerConfig.all_full(), - auto_apply=True, - no_prompts=True, - enable_preview=False, - ) - assert ( - dev_plan.snapshots[full_downstream_model_snapshot_id].change_category - == SnapshotChangeCategory.BREAKING - ) - assert ( - dev_plan.snapshots[view_downstream_model_snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_BREAKING - ) - assert len(dev_plan.missing_intervals) == 2 - assert dev_plan.missing_intervals[0].snapshot_id == full_downstream_model_snapshot_id - assert dev_plan.missing_intervals[1].snapshot_id == view_downstream_model_snapshot_id - - # Check that the representative view hasn't been created yet. - assert not context.engine_adapter.table_exists( - context.get_snapshot(view_downstream_model_name).table_name() - ) - - # Now promote the very first change to prod without promoting the 2nd breaking change. - context.upsert_model(full_downstream_model) - context.plan(auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full()) - - # Finally, make a non-breaking change to the full model in the same dev environment. - context.upsert_model(add_projection_to_model(t.cast(SqlModel, new_full_downstream_model))) - full_downstream_model_snapshot_id = context.get_snapshot(full_downstream_model_name).snapshot_id - view_downstream_model_snapshot_id = context.get_snapshot(view_downstream_model_name).snapshot_id - dev_plan = context.plan( - "dev", - categorizer_config=CategorizerConfig.all_full(), - auto_apply=True, - no_prompts=True, - enable_preview=False, - ) - assert ( - dev_plan.snapshots[full_downstream_model_snapshot_id].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - dev_plan.snapshots[view_downstream_model_snapshot_id].change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - - # Deploy changes to prod - context.plan("prod", auto_apply=True, no_prompts=True) - - # Check that the representative view has been created. - assert context.engine_adapter.table_exists( - context.get_snapshot(view_downstream_model_name).table_name() - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_indirect_non_breaking_view_model_non_representative_snapshot_migration( - init_and_plan_context: t.Callable, -): - context, _ = init_and_plan_context("examples/sushi") - - forward_only_model_expr = d.parse( - """ - MODEL ( - name memory.sushi.forward_only_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only TRUE, - on_destructive_change 'allow', - ), - ); - - SELECT '2023-01-07' AS ds, 1 AS a; - """ - ) - forward_only_model = load_sql_based_model(forward_only_model_expr) - context.upsert_model(forward_only_model) - - downstream_view_a_expr = d.parse( - """ - MODEL ( - name memory.sushi.downstream_view_a, - kind VIEW, - ); - - SELECT a from memory.sushi.forward_only_model; - """ - ) - downstream_view_a = load_sql_based_model(downstream_view_a_expr) - context.upsert_model(downstream_view_a) - - downstream_view_b_expr = d.parse( - """ - MODEL ( - name memory.sushi.downstream_view_b, - kind VIEW, - ); - - SELECT a from memory.sushi.downstream_view_a; - """ - ) - downstream_view_b = load_sql_based_model(downstream_view_b_expr) - context.upsert_model(downstream_view_b) - - context.plan(auto_apply=True, no_prompts=True, skip_tests=True) - - # Make a forward-only change - context.upsert_model(add_projection_to_model(t.cast(SqlModel, forward_only_model))) - # Make a non-breaking change downstream - context.upsert_model(add_projection_to_model(t.cast(SqlModel, downstream_view_a))) - - context.plan(auto_apply=True, no_prompts=True, skip_tests=True) - - # Make sure the downstrean indirect non-breaking view is available in prod - count = context.engine_adapter.fetchone("SELECT COUNT(*) FROM memory.sushi.downstream_view_b")[ - 0 - ] - assert count > 0 - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -@pytest.mark.parametrize( - "parent_a_category,parent_b_category,expected_child_category", - [ - ( - SnapshotChangeCategory.BREAKING, - SnapshotChangeCategory.BREAKING, - SnapshotChangeCategory.INDIRECT_BREAKING, - ), - ( - SnapshotChangeCategory.NON_BREAKING, - SnapshotChangeCategory.NON_BREAKING, - SnapshotChangeCategory.INDIRECT_NON_BREAKING, - ), - ( - SnapshotChangeCategory.BREAKING, - SnapshotChangeCategory.NON_BREAKING, - SnapshotChangeCategory.INDIRECT_NON_BREAKING, - ), - ( - SnapshotChangeCategory.NON_BREAKING, - SnapshotChangeCategory.BREAKING, - SnapshotChangeCategory.INDIRECT_BREAKING, - ), - ( - SnapshotChangeCategory.NON_BREAKING, - SnapshotChangeCategory.METADATA, - SnapshotChangeCategory.METADATA, - ), - ( - SnapshotChangeCategory.BREAKING, - SnapshotChangeCategory.METADATA, - SnapshotChangeCategory.METADATA, - ), - ( - SnapshotChangeCategory.METADATA, - SnapshotChangeCategory.BREAKING, - SnapshotChangeCategory.INDIRECT_BREAKING, - ), - ( - SnapshotChangeCategory.METADATA, - SnapshotChangeCategory.NON_BREAKING, - SnapshotChangeCategory.INDIRECT_NON_BREAKING, - ), - ( - SnapshotChangeCategory.METADATA, - SnapshotChangeCategory.METADATA, - SnapshotChangeCategory.METADATA, - ), - ], -) -def test_rebase_two_changed_parents( - init_and_plan_context: t.Callable, - parent_a_category: SnapshotChangeCategory, # This change is deployed to prod first - parent_b_category: SnapshotChangeCategory, # This change is deployed to prod second - expected_child_category: SnapshotChangeCategory, -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - initial_model_a = context.get_model("sushi.orders") - initial_model_b = context.get_model("sushi.items") - - # Make change A and deploy it to dev_a - context.upsert_model(initial_model_a.name, stamp="1") - plan_builder = context.plan_builder("dev_a", skip_tests=True) - plan_builder.set_choice(context.get_snapshot(initial_model_a.name), parent_a_category) - context.apply(plan_builder.build()) - - # Make change B and deploy it to dev_b - context.upsert_model(initial_model_a) - context.upsert_model(initial_model_b.name, stamp="1") - plan_builder = context.plan_builder("dev_b", skip_tests=True) - plan_builder.set_choice(context.get_snapshot(initial_model_b.name), parent_b_category) - context.apply(plan_builder.build()) - - # Deploy change A to prod - context.upsert_model(initial_model_a.name, stamp="1") - context.upsert_model(initial_model_b) - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - - # Apply change B in addition to A and plan against prod - context.upsert_model(initial_model_b.name, stamp="1") - plan = context.plan_builder("prod", skip_tests=True).build() - - # Validate the category of child snapshots - direct_child_snapshot = plan.snapshots[context.get_snapshot("sushi.order_items").snapshot_id] - assert direct_child_snapshot.change_category == expected_child_category - - indirect_child_snapshot = plan.snapshots[context.get_snapshot("sushi.top_waiters").snapshot_id] - assert indirect_child_snapshot.change_category == expected_child_category - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_unaligned_start_snapshot_with_non_deployable_downstream(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - downstream_model_name = "memory.sushi.customer_max_revenue" - - expressions = d.parse( - f""" - MODEL ( - name {downstream_model_name}, - kind INCREMENTAL_BY_UNIQUE_KEY ( - unique_key customer_id, - forward_only true, - ), - ); - - SELECT - customer_id, MAX(revenue) AS max_revenue - FROM memory.sushi.customer_revenue_lifetime - GROUP BY 1; - """ - ) - - downstream_model = load_sql_based_model(expressions) - assert downstream_model.forward_only - context.upsert_model(downstream_model) - - context.plan(auto_apply=True, no_prompts=True) - - customer_revenue_lifetime_model = context.get_model("sushi.customer_revenue_lifetime") - kwargs = { - **customer_revenue_lifetime_model.dict(), - "name": "memory.sushi.customer_revenue_lifetime_new", - "kind": dict( - name="INCREMENTAL_UNMANAGED" - ), # Make it incremental unmanaged to ensure the depends_on_past behavior. - } - context.upsert_model(SqlModel.parse_obj(kwargs)) - context.upsert_model( - downstream_model_name, - query_=ParsableSql( - sql="SELECT customer_id, MAX(revenue) AS max_revenue FROM memory.sushi.customer_revenue_lifetime_new GROUP BY 1" - ), - ) - - plan = context.plan_builder("dev", enable_preview=True).build() - assert {s.name for s in plan.new_snapshots} == { - '"memory"."sushi"."customer_revenue_lifetime_new"', - '"memory"."sushi"."customer_max_revenue"', - } - for snapshot_interval in plan.missing_intervals: - assert not plan.deployability_index.is_deployable(snapshot_interval.snapshot_id) - assert snapshot_interval.intervals[0][0] == to_timestamp("2023-01-07") - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_virtual_environment_mode_dev_only(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context( - "examples/sushi", config="test_config_virtual_environment_mode_dev_only" - ) - - assert all( - s.virtual_environment_mode.is_dev_only or not s.is_model or s.is_symbolic - for s in context.snapshots.values() - ) - - # Init prod - context.plan("prod", auto_apply=True, no_prompts=True) - - # Make a change in dev - original_model = context.get_model("sushi.waiter_revenue_by_day") - original_fingerprint = context.get_snapshot(original_model.name).fingerprint - model = original_model.copy( - update={ - "query_": ParsableSql( - sql=original_model.query.order_by("waiter_id").sql(dialect=original_model.dialect) - ) - } - ) - model = add_projection_to_model(t.cast(SqlModel, model)) - context.upsert_model(model) - - plan_dev = context.plan_builder("dev").build() - assert to_timestamp(plan_dev.start) == to_timestamp("2023-01-07") - assert plan_dev.requires_backfill - assert plan_dev.missing_intervals == [ - SnapshotIntervals( - snapshot_id=context.get_snapshot("sushi.top_waiters").snapshot_id, - intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], - ), - SnapshotIntervals( - snapshot_id=context.get_snapshot("sushi.waiter_revenue_by_day").snapshot_id, - intervals=[(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], - ), - ] - assert plan_dev.context_diff.snapshots[context.get_snapshot(model.name).snapshot_id].intervals - assert plan_dev.context_diff.snapshots[ - context.get_snapshot("sushi.top_waiters").snapshot_id - ].intervals - assert plan_dev.context_diff.snapshots[ - context.get_snapshot(model.name).snapshot_id - ].dev_intervals - assert plan_dev.context_diff.snapshots[ - context.get_snapshot("sushi.top_waiters").snapshot_id - ].dev_intervals - context.apply(plan_dev) - - # Make sure the waiter_revenue_by_day model is a table in prod and a view in dev - table_types_df = context.engine_adapter.fetchdf( - "SELECT table_schema, table_type FROM INFORMATION_SCHEMA.TABLES WHERE table_name = 'waiter_revenue_by_day'" - ) - assert table_types_df.to_dict("records") == [ - {"table_schema": "sushi", "table_type": "BASE TABLE"}, - {"table_schema": "sushi__dev", "table_type": "VIEW"}, - ] - - # Check that the specified dates were backfilled - min_event_date = context.engine_adapter.fetchone( - "SELECT MIN(event_date) FROM sushi__dev.waiter_revenue_by_day" - )[0] - assert min_event_date == to_date("2023-01-07") - - # Make sure the changes are applied without backfill in prod - plan_prod = context.plan_builder("prod").build() - assert not plan_prod.requires_backfill - assert not plan_prod.missing_intervals - context.apply(plan_prod) - assert "one" in context.engine_adapter.columns("sushi.waiter_revenue_by_day") - - # Make sure the revert of a breaking changes results in a full rebuild - context.upsert_model(original_model) - assert context.get_snapshot(original_model.name).fingerprint == original_fingerprint - - plan_prod = context.plan_builder( - "prod", allow_destructive_models=["sushi.waiter_revenue_by_day"] - ).build() - assert not plan_prod.requires_backfill - assert not plan_prod.missing_intervals - context.apply(plan_prod) - assert "one" not in context.engine_adapter.columns("sushi.waiter_revenue_by_day") - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_virtual_environment_mode_dev_only_model_kind_change(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context( - "examples/sushi", config="test_config_virtual_environment_mode_dev_only" - ) - context.apply(plan) - - # Change to full kind - model = context.get_model("sushi.top_waiters") - model = model.copy(update={"kind": FullKind()}) - context.upsert_model(model) - prod_plan = context.plan_builder("prod", skip_tests=True).build() - assert prod_plan.missing_intervals - assert prod_plan.requires_backfill - assert not prod_plan.context_diff.snapshots[ - context.get_snapshot(model.name).snapshot_id - ].intervals - context.apply(prod_plan) - data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) - assert len(data_objects) == 1 - assert data_objects[0].type == "table" - - # Change back to view - model = context.get_model("sushi.top_waiters") - model = model.copy(update={"kind": ViewKind()}) - context.upsert_model(model) - prod_plan = context.plan_builder("prod", skip_tests=True).build() - assert prod_plan.requires_backfill - assert prod_plan.missing_intervals - assert not prod_plan.context_diff.snapshots[ - context.get_snapshot(model.name).snapshot_id - ].intervals - context.apply(prod_plan) - data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) - assert len(data_objects) == 1 - assert data_objects[0].type == "view" - - # Change to incremental - model = context.get_model("sushi.top_waiters") - model = model.copy(update={"kind": IncrementalUnmanagedKind()}) - context.upsert_model(model) - prod_plan = context.plan_builder("prod", skip_tests=True).build() - assert prod_plan.requires_backfill - assert prod_plan.missing_intervals - assert not prod_plan.context_diff.snapshots[ - context.get_snapshot(model.name).snapshot_id - ].intervals - context.apply(prod_plan) - data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) - assert len(data_objects) == 1 - assert data_objects[0].type == "table" - - # Change back to full - model = context.get_model("sushi.top_waiters") - model = model.copy(update={"kind": FullKind()}) - context.upsert_model(model) - prod_plan = context.plan_builder("prod", skip_tests=True).build() - assert prod_plan.requires_backfill - assert prod_plan.missing_intervals - assert not prod_plan.context_diff.snapshots[ - context.get_snapshot(model.name).snapshot_id - ].intervals - context.apply(prod_plan) - data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) - assert len(data_objects) == 1 - assert data_objects[0].type == "table" - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_virtual_environment_mode_dev_only_model_kind_change_incremental( - init_and_plan_context: t.Callable, -): - context, _ = init_and_plan_context( - "examples/sushi", config="test_config_virtual_environment_mode_dev_only" - ) - - forward_only_model_name = "memory.sushi.test_forward_only_model" - forward_only_model_expressions = d.parse( - f""" - MODEL ( - name {forward_only_model_name}, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - ), - ); - - SELECT '2023-01-01' AS ds, 'value' AS value; - """ - ) - forward_only_model = load_sql_based_model(forward_only_model_expressions) - forward_only_model = forward_only_model.copy( - update={"virtual_environment_mode": VirtualEnvironmentMode.DEV_ONLY} - ) - context.upsert_model(forward_only_model) - - context.plan("prod", auto_apply=True, no_prompts=True) - - # Change to view - model = context.get_model(forward_only_model_name) - original_kind = model.kind - model = model.copy(update={"kind": ViewKind()}) - context.upsert_model(model) - prod_plan = context.plan_builder("prod", skip_tests=True).build() - assert prod_plan.requires_backfill - assert prod_plan.missing_intervals - assert not prod_plan.context_diff.snapshots[ - context.get_snapshot(model.name).snapshot_id - ].intervals - context.apply(prod_plan) - data_objects = context.engine_adapter.get_data_objects("sushi", {"test_forward_only_model"}) - assert len(data_objects) == 1 - assert data_objects[0].type == "view" - - model = model.copy(update={"kind": original_kind}) - context.upsert_model(model) - prod_plan = context.plan_builder("prod", skip_tests=True).build() - assert prod_plan.requires_backfill - assert prod_plan.missing_intervals - assert not prod_plan.context_diff.snapshots[ - context.get_snapshot(model.name).snapshot_id - ].intervals - context.apply(prod_plan) - data_objects = context.engine_adapter.get_data_objects("sushi", {"test_forward_only_model"}) - assert len(data_objects) == 1 - assert data_objects[0].type == "table" - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_virtual_environment_mode_dev_only_model_kind_change_with_follow_up_changes_in_dev( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context( - "examples/sushi", config="test_config_virtual_environment_mode_dev_only" - ) - context.apply(plan) - - # Make sure the initial state is a view - data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) - assert len(data_objects) == 1 - assert data_objects[0].type == "view" - - # Change to incremental unmanaged kind - model = context.get_model("sushi.top_waiters") - model = model.copy(update={"kind": IncrementalUnmanagedKind()}) - context.upsert_model(model) - dev_plan = context.plan_builder("dev", skip_tests=True).build() - assert dev_plan.missing_intervals - assert dev_plan.requires_backfill - context.apply(dev_plan) - - # Make a follow-up forward-only change - model = add_projection_to_model(t.cast(SqlModel, model)) - context.upsert_model(model) - dev_plan = context.plan_builder("dev", skip_tests=True, forward_only=True).build() - context.apply(dev_plan) - - # Deploy to prod - prod_plan = context.plan_builder("prod", skip_tests=True).build() - assert prod_plan.requires_backfill - assert prod_plan.missing_intervals - assert not prod_plan.context_diff.snapshots[ - context.get_snapshot(model.name).snapshot_id - ].intervals - context.apply(prod_plan) - data_objects = context.engine_adapter.get_data_objects("sushi", {"top_waiters"}) - assert len(data_objects) == 1 - assert data_objects[0].type == "table" - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_virtual_environment_mode_dev_only_model_kind_change_manual_categorization( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context( - "examples/sushi", config="test_config_virtual_environment_mode_dev_only" - ) - context.apply(plan) - - model = context.get_model("sushi.top_waiters") - model = model.copy(update={"kind": FullKind()}) - context.upsert_model(model) - dev_plan_builder = context.plan_builder("dev", skip_tests=True, no_auto_categorization=True) - dev_plan_builder.set_choice( - dev_plan_builder._context_diff.snapshots[context.get_snapshot(model.name).snapshot_id], - SnapshotChangeCategory.NON_BREAKING, - ) - dev_plan = dev_plan_builder.build() - assert dev_plan.requires_backfill - assert len(dev_plan.missing_intervals) == 1 - context.apply(dev_plan) - - prod_plan = context.plan_builder("prod", skip_tests=True).build() - assert prod_plan.requires_backfill - assert prod_plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=context.get_snapshot("sushi.top_waiters").snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ), - ] - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_virtual_environment_mode_dev_only_seed_model_change( - init_and_plan_context: t.Callable, -): - context, _ = init_and_plan_context( - "examples/sushi", config="test_config_virtual_environment_mode_dev_only" - ) - context.load() - context.plan("prod", auto_apply=True, no_prompts=True) - - seed_model = context.get_model("sushi.waiter_names") - with open(seed_model.seed_path, "a") as fd: - fd.write("\n123,New Test Name") - - context.load() - seed_model_snapshot = context.get_snapshot("sushi.waiter_names") - plan = context.plan_builder("dev").build() - assert plan.directly_modified == {seed_model_snapshot.snapshot_id} - assert len(plan.missing_intervals) == 2 - context.apply(plan) - - actual_seed_df_in_dev = context.fetchdf("SELECT * FROM sushi__dev.waiter_names WHERE id = 123") - assert actual_seed_df_in_dev.to_dict("records") == [{"id": 123, "name": "New Test Name"}] - actual_seed_df_in_prod = context.fetchdf("SELECT * FROM sushi.waiter_names WHERE id = 123") - assert actual_seed_df_in_prod.empty - - plan = context.plan_builder("prod").build() - assert plan.directly_modified == {seed_model_snapshot.snapshot_id} - assert len(plan.missing_intervals) == 1 - assert plan.missing_intervals[0].snapshot_id == seed_model_snapshot.snapshot_id - context.apply(plan) - - actual_seed_df_in_prod = context.fetchdf("SELECT * FROM sushi.waiter_names WHERE id = 123") - assert actual_seed_df_in_prod.to_dict("records") == [{"id": 123, "name": "New Test Name"}] - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_virtual_environment_mode_dev_only_model_change_downstream_of_seed( - init_and_plan_context: t.Callable, -): - """This test covers a scenario when a model downstream of a seed model is modified and explicitly selected - causing an (unhydrated) seed model to sourced from the state. If SQLMesh attempts to create - a table for the unchanged seed model, it will fail because the seed model is not hydrated. - """ - context, _ = init_and_plan_context( - "examples/sushi", config="test_config_virtual_environment_mode_dev_only" - ) - context.load() - context.plan("prod", auto_apply=True, no_prompts=True) - - # Make sure that a different version of the seed model is loaded - seed_model = context.get_model("sushi.waiter_names") - seed_model = seed_model.copy(update={"stamp": "force new version"}) - context.upsert_model(seed_model) - - # Make a change to the downstream model - model = context.get_model("sushi.waiter_as_customer_by_day") - model = model.copy(update={"stamp": "force new version"}) - context.upsert_model(model) - - # It is important to clear the cache so that the hydrated seed model is not sourced from the cache - context.clear_caches() - - # Make sure to use the selector so that the seed model is sourced from the state - plan = context.plan_builder("dev", select_models=[model.name]).build() - assert len(plan.directly_modified) == 1 - assert list(plan.directly_modified)[0].name == model.fqn - assert len(plan.missing_intervals) == 1 - assert plan.missing_intervals[0].snapshot_id.name == model.fqn - - # Make sure there's no error when applying the plan - context.apply(plan) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_virtual_environment_mode_dev_only_model_change_standalone_audit( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context( - "examples/sushi", config="test_config_virtual_environment_mode_dev_only" - ) - context.apply(plan) - - # Change a model upstream from a standalone audit - model = context.get_model("sushi.items") - model = model.copy(update={"stamp": "force new version"}) - context.upsert_model(model) - - plan = context.plan_builder("prod", skip_tests=True).build() - - # Make sure the standalone audit is among modified - assert ( - context.get_snapshot("assert_item_price_above_zero").snapshot_id - in plan.indirectly_modified[context.get_snapshot("sushi.items").snapshot_id] - ) - - # Make sure there's no error when applying the plan - context.apply(plan) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_virtual_environment_mode_dev_only_seed_model_change_schema( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context( - "examples/sushi", config="test_config_virtual_environment_mode_dev_only" - ) - context.apply(plan) - - new_csv = [] - with open(context.path / "seeds" / "waiter_names.csv", "r") as fd: - is_header = True - for idx, line in enumerate(fd): - line = line.strip() - if not line: - continue - if is_header: - new_csv.append(line + ",new_column") - is_header = False - else: - new_csv.append(line + f",v{idx}") - - with open(context.path / "seeds" / "waiter_names.csv", "w") as fd: - fd.write("\n".join(new_csv)) - - context.load() - - downstream_model = context.get_model("sushi.waiter_as_customer_by_day") - downstream_model_kind = downstream_model.kind.dict() - downstream_model_kwargs = { - **downstream_model.dict(), - "kind": { - **downstream_model_kind, - "on_destructive_change": "allow", - }, - "audits": [], - # Use the new column - "query": "SELECT '2023-01-07' AS event_date, new_column AS new_column FROM sushi.waiter_names", - } - context.upsert_model(SqlModel.parse_obj(downstream_model_kwargs)) - - context.plan("dev", auto_apply=True, no_prompts=True, skip_tests=True, enable_preview=True) - - assert ( - context.engine_adapter.fetchone( - "SELECT COUNT(*) FROM sushi__dev.waiter_as_customer_by_day" - )[0] - == len(new_csv) - 1 - ) - - # Deploy to prod - context.clear_caches() - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - assert "new_column" in context.engine_adapter.columns("sushi.waiter_as_customer_by_day") - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_restatement_plan_ignores_changes(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - restated_snapshot = context.get_snapshot("sushi.top_waiters") - - # Simulate a change. - model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - - plan = context.plan_builder(restate_models=["sushi.top_waiters"]).build() - assert plan.snapshots != context.snapshots - - assert not plan.directly_modified - assert not plan.has_changes - assert not plan.new_snapshots - assert plan.requires_backfill - assert plan.restatements == { - restated_snapshot.snapshot_id: (to_timestamp("2023-01-01"), to_timestamp("2023-01-09")) - } - assert plan.missing_intervals == [ - SnapshotIntervals( - snapshot_id=restated_snapshot.snapshot_id, - intervals=[ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ) - ] - - context.apply(plan) - - -@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, -): - context, _ = init_and_plan_context("examples/sushi") - - # Change kind to incremental unmanaged - model = context.get_model("sushi.waiter_revenue_by_day") - previous_kind = model.kind.copy(update={"forward_only": True}) - assert isinstance(previous_kind, IncrementalByTimeRangeKind) - - model = model.copy( - update={ - "kind": IncrementalUnmanagedKind(), - "physical_version": "pinned_version_12345", - "partitioned_by_": [exp.column("event_date")], - } - ) - context.upsert_model(model) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Make some change and deploy it to both dev and prod environments - model = add_projection_to_model(t.cast(SqlModel, model)) - context.upsert_model(model) - context.plan("dev_a", auto_apply=True, no_prompts=True) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Change the kind back to incremental by time range and deploy to prod - model = model.copy(update={"kind": previous_kind}) - context.upsert_model(model) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Restate the model and verify that the interval hasn't been expanded because of the old snapshot - # with the same version - context.plan( - restate_models=["sushi.waiter_revenue_by_day"], - start="2023-01-06", - end="2023-01-08", - auto_apply=True, - no_prompts=True, - ) - - assert ( - context.fetchdf( - "SELECT COUNT(*) AS cnt FROM sushi.waiter_revenue_by_day WHERE one IS NOT NULL AND event_date < '2023-01-06'" - )["cnt"][0] - == 0 - ) - plan = context.plan_builder("prod").build() - assert not plan.missing_intervals - - -def test_restatement_plan_hourly_with_downstream_daily_restates_correct_intervals(tmp_path: Path): - model_a = """ - MODEL ( - name test.a, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts" - ), - start '2024-01-01 00:00:00', - cron '@hourly' - ); - - select account_id, ts from test.external_table; - """ - - model_b = """ - MODEL ( - name test.b, - kind FULL, - cron '@daily' - ); - - select account_id, ts from test.a; - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - for path, defn in {"a.sql": model_a, "b.sql": model_b}.items(): - with open(models_dir / path, "w") as f: - f.write(defn) - - config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) - ctx = Context(paths=[tmp_path], config=config) - - engine_adapter = ctx.engine_adapter - engine_adapter.create_schema("test") - - # source data - df = pd.DataFrame( - { - "account_id": [1001, 1002, 1003, 1004], - "ts": [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ], - } - ) - columns_to_types = { - "account_id": exp.DataType.build("int"), - "ts": exp.DataType.build("timestamp"), - } - external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) - engine_adapter.insert_append( - table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types - ) - - # plan + apply - ctx.plan(auto_apply=True, no_prompts=True) - - def _dates_in_table(table_name: str) -> t.List[str]: - return [ - str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") - ] - - # verify initial state - for tbl in ["test.a", "test.b"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - - # restate A - engine_adapter.execute("delete from test.external_table where ts = '2024-01-01 01:30:00'") - ctx.plan( - restate_models=["test.a"], - start="2024-01-01 01:00:00", - end="2024-01-01 02:00:00", - auto_apply=True, - no_prompts=True, - ) - - # verify result - for tbl in ["test.a", "test.b"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ], f"Table {tbl} wasnt cleared" - - # Put some data - df = pd.DataFrame( - { - "account_id": [1001, 1002, 1003, 1004], - "ts": [ - "2024-01-01 01:30:00", - "2024-01-01 23:30:00", - "2024-01-02 03:30:00", - "2024-01-03 12:30:00", - ], - } - ) - engine_adapter.replace_query( - table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types - ) - - # Restate A across a day boundary with the expectation that two day intervals in B are affected - ctx.plan( - restate_models=["test.a"], - start="2024-01-01 02:00:00", - end="2024-01-02 04:00:00", - auto_apply=True, - no_prompts=True, - ) - - for tbl in ["test.a", "test.b"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", # present already - # "2024-01-01 02:30:00", #removed in last restatement - "2024-01-01 23:30:00", # added in last restatement - "2024-01-02 03:30:00", # added in last restatement - ], f"Table {tbl} wasnt cleared" - - -def test_restatement_plan_respects_disable_restatements(tmp_path: Path): - model_a = """ - MODEL ( - name test.a, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts" - ), - start '2024-01-01', - cron '@daily' - ); - - select account_id, ts from test.external_table; - """ - - model_b = """ - MODEL ( - name test.b, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts", - disable_restatement true, - ), - start '2024-01-01', - cron '@daily' - ); - - select account_id, ts from test.a; - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - for path, defn in {"a.sql": model_a, "b.sql": model_b}.items(): - with open(models_dir / path, "w") as f: - f.write(defn) - - config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) - ctx = Context(paths=[tmp_path], config=config) - - engine_adapter = ctx.engine_adapter - engine_adapter.create_schema("test") - - # source data - df = pd.DataFrame( - { - "account_id": [1001, 1002, 1003, 1004], - "ts": [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ], - } - ) - columns_to_types = { - "account_id": exp.DataType.build("int"), - "ts": exp.DataType.build("timestamp"), - } - external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) - engine_adapter.insert_append( - table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types - ) - - # plan + apply - ctx.plan(auto_apply=True, no_prompts=True) - - def _dates_in_table(table_name: str) -> t.List[str]: - return [ - str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") - ] - - def get_snapshot_intervals(snapshot_id): - return list(ctx.state_sync.get_snapshots([snapshot_id]).values())[0].intervals - - # verify initial state - for tbl in ["test.a", "test.b"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - - # restate A and expect b to be ignored - starting_b_intervals = get_snapshot_intervals(ctx.snapshots['"memory"."test"."b"'].snapshot_id) - engine_adapter.execute("delete from test.external_table where ts = '2024-01-01 01:30:00'") - ctx.plan( - restate_models=["test.a"], - start="2024-01-01", - end="2024-01-02", - auto_apply=True, - no_prompts=True, - ) - - # verify A was changed and not b - assert _dates_in_table("test.a") == [ - "2024-01-01 00:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - assert _dates_in_table("test.b") == [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - - # Verify B intervals were not touched - b_intervals = get_snapshot_intervals(ctx.snapshots['"memory"."test"."b"'].snapshot_id) - assert starting_b_intervals == b_intervals - - -def test_restatement_plan_clears_correct_intervals_across_environments(tmp_path: Path): - model1 = """ - MODEL ( - name test.incremental_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "date" - ), - start '2024-01-01', - cron '@daily' - ); - - select account_id, date from test.external_table; - """ - - model2 = """ - MODEL ( - name test.downstream_of_incremental, - kind FULL - ); - - select account_id, date from test.incremental_model; - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - with open(models_dir / "model1.sql", "w") as f: - f.write(model1) - - with open(models_dir / "model2.sql", "w") as f: - f.write(model2) - - config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) - ctx = Context(paths=[tmp_path], config=config) - - engine_adapter = ctx.engine_adapter - engine_adapter.create_schema("test") - - # source data - df = pd.DataFrame( - { - "account_id": [1001, 1002, 1003, 1004, 1005], - "name": ["foo", "bar", "baz", "bing", "bong"], - "date": ["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-04", "2024-01-05"], - } - ) - columns_to_types = { - "account_id": exp.DataType.build("int"), - "name": exp.DataType.build("varchar"), - "date": exp.DataType.build("date"), - } - external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) - engine_adapter.insert_append( - table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types - ) - - # first, create the prod models - ctx.plan(auto_apply=True, no_prompts=True) - assert engine_adapter.fetchone("select count(*) from test.incremental_model") == (5,) - assert engine_adapter.fetchone("select count(*) from test.downstream_of_incremental") == (5,) - assert not engine_adapter.table_exists("test__dev.incremental_model") - - # then, make a dev version - model1 = """ - MODEL ( - name test.incremental_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "date" - ), - start '2024-01-01', - cron '@daily' - ); - - select 1 as account_id, date from test.external_table; - """ - with open(models_dir / "model1.sql", "w") as f: - f.write(model1) - ctx.load() - - ctx.plan(environment="dev", auto_apply=True, no_prompts=True) - assert engine_adapter.table_exists("test__dev.incremental_model") - assert engine_adapter.fetchone("select count(*) from test__dev.incremental_model") == (5,) - - # drop some source data so when we restate the interval it essentially clears it which is easy to verify - engine_adapter.execute("delete from test.external_table where date = '2024-01-01'") - assert engine_adapter.fetchone("select count(*) from test.external_table") == (4,) - - # now, restate intervals in dev and verify prod is NOT affected - ctx.plan( - environment="dev", - start="2024-01-01", - end="2024-01-02", - restate_models=["test.incremental_model"], - auto_apply=True, - no_prompts=True, - ) - assert engine_adapter.fetchone("select count(*) from test.incremental_model") == (5,) - assert engine_adapter.fetchone( - "select count(*) from test.incremental_model where date = '2024-01-01'" - ) == (1,) - assert engine_adapter.fetchone("select count(*) from test__dev.incremental_model") == (4,) - assert engine_adapter.fetchone( - "select count(*) from test__dev.incremental_model where date = '2024-01-01'" - ) == (0,) - - # prod still should not be affected by a run because the restatement only happened in dev - ctx.run() - assert engine_adapter.fetchone("select count(*) from test.incremental_model") == (5,) - assert engine_adapter.fetchone( - "select count(*) from test.incremental_model where date = '2024-01-01'" - ) == (1,) - - # drop another interval from the source data - engine_adapter.execute("delete from test.external_table where date = '2024-01-02'") - - # now, restate intervals in prod and verify that dev IS affected - ctx.plan( - start="2024-01-01", - end="2024-01-03", - restate_models=["test.incremental_model"], - auto_apply=True, - no_prompts=True, - ) - assert engine_adapter.fetchone("select count(*) from test.incremental_model") == (3,) - assert engine_adapter.fetchone( - "select count(*) from test.incremental_model where date = '2024-01-01'" - ) == (0,) - assert engine_adapter.fetchone( - "select count(*) from test.incremental_model where date = '2024-01-02'" - ) == (0,) - assert engine_adapter.fetchone( - "select count(*) from test.incremental_model where date = '2024-01-03'" - ) == (1,) - - # dev not affected yet until `sqlmesh run` is run - assert engine_adapter.fetchone("select count(*) from test__dev.incremental_model") == (4,) - assert engine_adapter.fetchone( - "select count(*) from test__dev.incremental_model where date = '2024-01-01'" - ) == (0,) - assert engine_adapter.fetchone( - "select count(*) from test__dev.incremental_model where date = '2024-01-02'" - ) == (1,) - assert engine_adapter.fetchone( - "select count(*) from test__dev.incremental_model where date = '2024-01-03'" - ) == (1,) - - # the restatement plan for prod should have cleared dev intervals too, which means this `sqlmesh run` re-runs 2024-01-01 and 2024-01-02 - ctx.run(environment="dev") - assert engine_adapter.fetchone("select count(*) from test__dev.incremental_model") == (3,) - assert engine_adapter.fetchone( - "select count(*) from test__dev.incremental_model where date = '2024-01-01'" - ) == (0,) - assert engine_adapter.fetchone( - "select count(*) from test__dev.incremental_model where date = '2024-01-02'" - ) == (0,) - assert engine_adapter.fetchone( - "select count(*) from test__dev.incremental_model where date = '2024-01-03'" - ) == (1,) - - # the downstream full model should always reflect whatever the incremental model is showing - assert engine_adapter.fetchone("select count(*) from test.downstream_of_incremental") == (3,) - assert engine_adapter.fetchone("select count(*) from test__dev.downstream_of_incremental") == ( - 3, - ) - - -def test_prod_restatement_plan_clears_correct_intervals_in_derived_dev_tables(tmp_path: Path): - """ - Scenario: - I have models A[hourly] <- B[daily] <- C in prod - I create dev and add 2 new models D and E so that my dev DAG looks like A <- B <- C <- D[daily] <- E - I prod, I restate *one hour* of A - Outcome: - D and E should be restated in dev despite not being a part of prod - since B and D are daily, the whole day should be restated even though only 1hr of the upstream model was restated - """ - - model_a = """ - MODEL ( - name test.a, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts" - ), - start '2024-01-01 00:00:00', - cron '@hourly' - ); - - select account_id, ts from test.external_table; - """ - - def _derived_full_model_def(name: str, upstream: str) -> str: - return f""" - MODEL ( - name test.{name}, - kind FULL - ); - - select account_id, ts from test.{upstream}; - """ - - def _derived_incremental_model_def(name: str, upstream: str) -> str: - return f""" - MODEL ( - name test.{name}, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ts - ), - cron '@daily' - ); - - select account_id, ts from test.{upstream} where ts between @start_ts and @end_ts; - """ - - model_b = _derived_incremental_model_def("b", upstream="a") - model_c = _derived_full_model_def("c", upstream="b") - - models_dir = tmp_path / "models" - models_dir.mkdir() - - for path, defn in {"a.sql": model_a, "b.sql": model_b, "c.sql": model_c}.items(): - with open(models_dir / path, "w") as f: - f.write(defn) - - config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) - ctx = Context(paths=[tmp_path], config=config) - - engine_adapter = ctx.engine_adapter - engine_adapter.create_schema("test") - - # source data - df = pd.DataFrame( - { - "account_id": [1001, 1002, 1003, 1004], - "ts": [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ], - } - ) - columns_to_types = { - "account_id": exp.DataType.build("int"), - "ts": exp.DataType.build("timestamp"), - } - external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) - engine_adapter.insert_append( - table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types - ) - - # plan + apply A, B, C in prod - ctx.plan(auto_apply=True, no_prompts=True) - - # add D[daily], E in dev - model_d = _derived_incremental_model_def("d", upstream="c") - model_e = _derived_full_model_def("e", upstream="d") - - for path, defn in { - "d.sql": model_d, - "e.sql": model_e, - }.items(): - with open(models_dir / path, "w") as f: - f.write(defn) - - # plan + apply dev - ctx.load() - ctx.plan(environment="dev", auto_apply=True, no_prompts=True) - - def _dates_in_table(table_name: str) -> t.List[str]: - return [ - str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") - ] - - # verify initial state - for tbl in ["test.a", "test.b", "test.c", "test__dev.d", "test__dev.e"]: - assert engine_adapter.table_exists(tbl) - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - - for tbl in ["test.d", "test.e"]: - assert not engine_adapter.table_exists(tbl) - - # restate A in prod - engine_adapter.execute("delete from test.external_table where ts = '2024-01-01 01:30:00'") - ctx.plan( - restate_models=["test.a"], - start="2024-01-01 01:00:00", - end="2024-01-01 02:00:00", - auto_apply=True, - no_prompts=True, - ) - - # verify result - for tbl in ["test.a", "test.b", "test.c"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ], f"Table {tbl} wasnt cleared" - - # dev shouldnt have been affected yet - for tbl in ["test__dev.d", "test__dev.e"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ], f"Table {tbl} was prematurely cleared" - - # run dev to trigger the processing of the prod restatement - ctx.run(environment="dev") - - # data should now be cleared from dev - # note that D is a daily model, so clearing an hour interval from A should have triggered the full day in D - for tbl in ["test__dev.d", "test__dev.e"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ], f"Table {tbl} wasnt cleared" - - -def test_prod_restatement_plan_clears_unaligned_intervals_in_derived_dev_tables(tmp_path: Path): - """ - Scenario: - I have a model A[hourly] in prod - I create dev and add a model B[daily] - I prod, I restate *one hour* of A - - Outcome: - The whole day for B should be restated. The restatement plan for prod has no hints about B's cadence because - B only exists in dev and there are no other downstream models in prod that would cause the restatement intervals - to be widened. - - Therefore, this test checks that SQLMesh does the right thing when an interval is partially cleared - """ - - model_a = """ - MODEL ( - name test.a, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts" - ), - start '2024-01-01 00:00:00', - cron '@hourly' - ); - - select account_id, ts from test.external_table; - """ - - model_b = """ - MODEL ( - name test.b, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ts - ), - cron '@daily' - ); - - select account_id, ts from test.a where ts between @start_ts and @end_ts; - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - with open(models_dir / "a.sql", "w") as f: - f.write(model_a) - - config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) - ctx = Context(paths=[tmp_path], config=config) - - engine_adapter = ctx.engine_adapter - engine_adapter.create_schema("test") - - # source data - df = pd.DataFrame( - { - "account_id": [1001, 1002, 1003, 1004], - "ts": [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ], - } - ) - columns_to_types = { - "account_id": exp.DataType.build("int"), - "ts": exp.DataType.build("timestamp"), - } - external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) - engine_adapter.insert_append( - table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types - ) - - # plan + apply A[hourly] in prod - ctx.plan(auto_apply=True, no_prompts=True) - - # add B[daily] in dev - with open(models_dir / "b.sql", "w") as f: - f.write(model_b) - - # plan + apply dev - ctx.load() - ctx.plan(environment="dev", auto_apply=True, no_prompts=True) - - def _dates_in_table(table_name: str) -> t.List[str]: - return [ - str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") - ] - - # verify initial state - for tbl in ["test.a", "test__dev.b"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - - # restate A in prod - engine_adapter.execute("delete from test.external_table where ts = '2024-01-01 01:30:00'") - ctx.plan( - restate_models=["test.a"], - start="2024-01-01 01:00:00", - end="2024-01-01 02:00:00", - auto_apply=True, - no_prompts=True, - ) - - # verify result - assert _dates_in_table("test.a") == [ - "2024-01-01 00:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - - # dev shouldnt have been affected yet - assert _dates_in_table("test__dev.b") == [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - - # mess with A independently of SQLMesh to prove a whole day gets restated for B instead of just 1hr - snapshot_table_name = ctx.table_name("test.a", "dev") - engine_adapter.execute( - f"delete from {snapshot_table_name} where cast(ts as date) == '2024-01-01'" - ) - engine_adapter.execute( - f"insert into {snapshot_table_name} (account_id, ts) values (1007, '2024-01-02 01:30:00')" - ) - - assert _dates_in_table("test.a") == ["2024-01-02 00:30:00", "2024-01-02 01:30:00"] - - # run dev to trigger the processing of the prod restatement - ctx.run(environment="dev") - - # B should now have no data for 2024-01-01 - # To prove a single day was restated vs the whole model, it also shouldnt have the '2024-01-02 01:30:00' record - assert _dates_in_table("test__dev.b") == ["2024-01-02 00:30:00"] - - -def test_prod_restatement_plan_causes_dev_intervals_to_be_processed_in_next_dev_plan( - tmp_path: Path, -): - """ - Scenario: - I have a model A[hourly] in prod - I create dev and add a model B[daily] - I prod, I restate *one hour* of A - In dev, I run a normal plan instead of a cadence run - - Outcome: - The whole day for B should be restated as part of a normal plan - """ - - model_a = """ - MODEL ( - name test.a, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts" - ), - start '2024-01-01 00:00:00', - cron '@hourly' - ); - - select account_id, ts from test.external_table; - """ - - model_b = """ - MODEL ( - name test.b, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ts - ), - cron '@daily' - ); - - select account_id, ts from test.a where ts between @start_ts and @end_ts; - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - with open(models_dir / "a.sql", "w") as f: - f.write(model_a) - - config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) - ctx = Context(paths=[tmp_path], config=config) - - engine_adapter = ctx.engine_adapter - engine_adapter.create_schema("test") - - # source data - df = pd.DataFrame( - { - "account_id": [1001, 1002, 1003, 1004], - "ts": [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ], - } - ) - columns_to_types = { - "account_id": exp.DataType.build("int"), - "ts": exp.DataType.build("timestamp"), - } - external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) - engine_adapter.insert_append( - table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types - ) - - # plan + apply A[hourly] in prod - ctx.plan(auto_apply=True, no_prompts=True) - - # add B[daily] in dev - with open(models_dir / "b.sql", "w") as f: - f.write(model_b) - - # plan + apply dev - ctx.load() - ctx.plan(environment="dev", auto_apply=True, no_prompts=True) - - def _dates_in_table(table_name: str) -> t.List[str]: - return [ - str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") - ] - - # verify initial state - for tbl in ["test.a", "test__dev.b"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - - # restate A in prod - engine_adapter.execute("delete from test.external_table where ts = '2024-01-01 01:30:00'") - ctx.plan( - restate_models=["test.a"], - start="2024-01-01 01:00:00", - end="2024-01-01 02:00:00", - auto_apply=True, - no_prompts=True, - ) - - # verify result - assert _dates_in_table("test.a") == [ - "2024-01-01 00:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - - # dev shouldnt have been affected yet - assert _dates_in_table("test__dev.b") == [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - - # plan dev which should trigger the missing intervals to get repopulated - ctx.plan(environment="dev", auto_apply=True, no_prompts=True) - - # dev should have the restated data - for tbl in ["test.a", "test__dev.b"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ] - - -def test_prod_restatement_plan_causes_dev_intervals_to_be_widened_on_full_restatement_only_model( - tmp_path, -): - """ - Scenario: - I have am INCREMENTAL_BY_TIME_RANGE model A[daily] in prod - I create dev and add a INCREMENTAL_BY_UNIQUE_KEY model B (which supports full restatement only) - I prod, I restate one day of A which should cause intervals in dev to be cleared (but not processed) - In dev, I run a plan - - Outcome: - In the dev plan, the entire model for B should be rebuilt because it does not support partial restatement - """ - - model_a = """ - MODEL ( - name test.a, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts" - ), - start '2024-01-01 00:00:00', - cron '@daily' - ); - - select account_id, ts from test.external_table where ts between @start_ts and @end_ts; - """ - - model_b = """ - MODEL ( - name test.b, - kind INCREMENTAL_BY_UNIQUE_KEY ( - unique_key (account_id, ts) - ), - cron '@daily' - ); - - select account_id, ts from test.a where ts between @start_ts and @end_ts; - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - with open(models_dir / "a.sql", "w") as f: - f.write(model_a) - - config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) - ctx = Context(paths=[tmp_path], config=config) - - engine_adapter = ctx.engine_adapter - engine_adapter.create_schema("test") - - # source data - df = pd.DataFrame( - { - "account_id": [1001, 1002, 1003, 1004], - "ts": [ - "2024-01-01 00:30:00", - "2024-01-02 01:30:00", - "2024-01-03 02:30:00", - "2024-01-04 00:30:00", - ], - } - ) - columns_to_types = { - "account_id": exp.DataType.build("int"), - "ts": exp.DataType.build("timestamp"), - } - external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) - engine_adapter.insert_append( - table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types - ) - - # plan + apply A[daily] in prod - ctx.plan(auto_apply=True) - - # add B[daily] in dev - with open(models_dir / "b.sql", "w") as f: - f.write(model_b) - - # plan + apply dev - ctx.load() - ctx.plan(environment="dev", auto_apply=True) - - def _dates_in_table(table_name: str) -> t.List[str]: - return [ - str(r[0]) for r in engine_adapter.fetchall(f"select ts from {table_name} order by ts") - ] - - # verify initial state - for tbl in ["test.a", "test__dev.b"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-02 01:30:00", - "2024-01-03 02:30:00", - "2024-01-04 00:30:00", - ] - - # restate A in prod - engine_adapter.execute("delete from test.external_table where ts = '2024-01-02 01:30:00'") - ctx.plan( - restate_models=["test.a"], - start="2024-01-02 00:00:00", - end="2024-01-03 00:00:00", - auto_apply=True, - no_prompts=True, - ) - - # verify result - assert _dates_in_table("test.a") == [ - "2024-01-01 00:30:00", - "2024-01-03 02:30:00", - "2024-01-04 00:30:00", - ] - - # dev shouldnt have been affected yet - assert _dates_in_table("test__dev.b") == [ - "2024-01-01 00:30:00", - "2024-01-02 01:30:00", - "2024-01-03 02:30:00", - "2024-01-04 00:30:00", - ] - - # plan dev which should trigger the missing intervals to get repopulated - ctx.plan(environment="dev", auto_apply=True) - - # dev should have fully refreshed - # this is proven by the fact that INCREMENTAL_BY_UNIQUE_KEY cant propagate deletes, so if the - # model was not fully rebuilt, the deleted record would still be present - for tbl in ["test.a", "test__dev.b"]: - assert _dates_in_table(tbl) == [ - "2024-01-01 00:30:00", - "2024-01-03 02:30:00", - "2024-01-04 00:30:00", - ] - - -def test_prod_restatement_plan_missing_model_in_dev( - tmp_path: Path, -): - """ - Scenario: - I have a model B in prod but only model A in dev - I restate B in prod - - Outcome: - The A model should be ignore and the plan shouldn't fail - """ - - model_a = """ - MODEL ( - name test.a, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts" - ), - start '2024-01-01 00:00:00', - cron '@hourly' - ); - - select account_id, ts from test.external_table; - """ - - model_b = """ - MODEL ( - name test.b, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ts - ), - cron '@daily' - ); - - select account_id, ts from test.external_table where ts between @start_ts and @end_ts; - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - with open(models_dir / "a.sql", "w") as f: - f.write(model_a) - - config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) - ctx = Context(paths=[tmp_path], config=config) - - engine_adapter = ctx.engine_adapter - engine_adapter.create_schema("test") - - # source data - df = pd.DataFrame( - { - "account_id": [1001, 1002, 1003, 1004], - "ts": [ - "2024-01-01 00:30:00", - "2024-01-01 01:30:00", - "2024-01-01 02:30:00", - "2024-01-02 00:30:00", - ], - } - ) - columns_to_types = { - "account_id": exp.DataType.build("int"), - "ts": exp.DataType.build("timestamp"), - } - external_table = exp.table_(table="external_table", db="test", quoted=True) - engine_adapter.create_table(table_name=external_table, target_columns_to_types=columns_to_types) - engine_adapter.insert_append( - table_name=external_table, query_or_df=df, target_columns_to_types=columns_to_types - ) - - # plan + apply A[hourly] in dev - ctx.plan("dev", auto_apply=True, no_prompts=True) - - # add B[daily] in prod and remove A - with open(models_dir / "b.sql", "w") as f: - f.write(model_b) - Path(models_dir / "a.sql").unlink() - - # plan + apply dev - ctx.load() - ctx.plan(auto_apply=True, no_prompts=True) - - # restate B in prod - ctx.plan( - restate_models=["test.b"], - start="2024-01-01", - end="2024-01-02", - auto_apply=True, - no_prompts=True, - ) - - -def test_prod_restatement_plan_includes_related_unpromoted_snapshots(tmp_path: Path): - """ - Scenario: - - I have models A <- B in prod - - I have models A <- B <- C in dev - - Both B and C have gone through a few iterations in dev so multiple snapshot versions exist - for them but not all of them are promoted / active - - I restate A in prod - - Outcome: - - Intervals should be cleared for all of the versions of B and C, regardless - of if they are active in any particular environment, in case they ever get made - active - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - (models_dir / "a.sql").write_text(""" - MODEL ( - name test.a, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts" - ), - start '2024-01-01 00:00:00', - cron '@daily' - ); - - select 1 as a, now() as ts; - """) - - (models_dir / "b.sql").write_text(""" - MODEL ( - name test.b, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts" - ), - start '2024-01-01 00:00:00', - cron '@daily' - ); - - select a, ts from test.a - """) - - config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01")) - ctx = Context(paths=[tmp_path], config=config) - - def _all_snapshots() -> t.Dict[SnapshotId, Snapshot]: - all_snapshot_ids = [ - SnapshotId(name=name, identifier=identifier) - for (name, identifier) in ctx.state_sync.state_sync.engine_adapter.fetchall( # type: ignore - "select name, identifier from sqlmesh._snapshots" - ) - ] - return ctx.state_sync.get_snapshots(all_snapshot_ids) - - # plan + apply prod - ctx.plan(environment="prod", auto_apply=True) - assert len(_all_snapshots()) == 2 - - # create dev with new version of B - (models_dir / "b.sql").write_text(""" - MODEL ( - name test.b, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts" - ), - start '2024-01-01 00:00:00', - cron '@daily' - ); - - select a, ts, 'b dev 1' as change from test.a - """) - - ctx.load() - ctx.plan(environment="dev", auto_apply=True) - assert len(_all_snapshots()) == 3 - - # update B (new version) and create C - (models_dir / "b.sql").write_text(""" - MODEL ( - name test.b, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column "ts" - ), - start '2024-01-01 00:00:00', - cron '@daily' - ); - - select a, ts, 'b dev 2' as change from test.a - """) - - (models_dir / "c.sql").write_text(""" - MODEL ( - name test.c, - kind FULL, - cron '@daily' - ); - - select *, 'c initial' as val from test.b - """) - - ctx.load() - ctx.plan(environment="dev", auto_apply=True) - assert len(_all_snapshots()) == 5 - - # update C (new version), create D (unrelated) - (models_dir / "c.sql").write_text(""" - MODEL ( - name test.c, - kind FULL, - cron '@daily' - ); - - select *, 'c updated' as val from test.b - """) - - (models_dir / "d.sql").write_text(""" - MODEL ( - name test.d, - cron '@daily' - ); - - select 1 as unrelated - """) - - ctx.load() - ctx.plan(environment="dev", auto_apply=True) - all_snapshots_prior_to_restatement = _all_snapshots() - assert len(all_snapshots_prior_to_restatement) == 7 - - def _snapshot_instances(lst: t.Dict[SnapshotId, Snapshot], name_match: str) -> t.List[Snapshot]: - return [s for s_id, s in lst.items() if name_match in s_id.name] - - # verify initial state - - # 1 instance of A (prod) - assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"a"')) == 1 - - # 3 instances of B (original in prod + 2 updates in dev) - assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"b"')) == 3 - - # 2 instances of C (initial + update in dev) - assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"c"')) == 2 - - # 1 instance of D (initial - dev) - assert len(_snapshot_instances(all_snapshots_prior_to_restatement, '"d"')) == 1 - - # restate A in prod - ctx.plan(environment="prod", restate_models=['"memory"."test"."a"'], auto_apply=True) - - all_snapshots_after_restatement = _all_snapshots() - - # All versions of B and C in dev should have had intervals cleared - # D in dev should not be touched and A + B in prod shoud also not be touched - a = _snapshot_instances(all_snapshots_after_restatement, '"a"') - assert len(a) == 1 - - b = _snapshot_instances(all_snapshots_after_restatement, '"b"') - # the 1 B instance in prod should be populated and 2 in dev (1 active) should be cleared - assert len(b) == 3 - assert len([s for s in b if not s.intervals]) == 2 - - c = _snapshot_instances(all_snapshots_after_restatement, '"c"') - # the 2 instances of C in dev (1 active) should be cleared - assert len(c) == 2 - assert len([s for s in c if not s.intervals]) == 2 - - d = _snapshot_instances(all_snapshots_after_restatement, '"d"') - # D should not be touched since it's in no way downstream of A in prod - assert len(d) == 1 - assert d[0].intervals - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_dev_restatement_of_prod_model(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - - context.plan("dev", auto_apply=True, no_prompts=True, skip_tests=True) - - restatement_plan = context.plan_builder("dev", restate_models=["*"]).build() - assert set(restatement_plan.restatements) == { - context.get_snapshot("sushi.waiter_revenue_by_day").snapshot_id, - context.get_snapshot("sushi.top_waiters").snapshot_id, - } - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_plan_snapshot_table_exists_for_promoted_snapshot(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - - context.plan("dev", auto_apply=True, no_prompts=True, skip_tests=True) - - # Drop the views and make sure SQLMesh recreates them later - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters", raise_if_missing=True) - context.engine_adapter.drop_view(top_waiters_snapshot.table_name()) - context.engine_adapter.drop_view(top_waiters_snapshot.table_name(False)) - - # Make the environment unfinalized to force recreation of all views in the virtual layer - context.state_sync.state_sync.engine_adapter.execute( - "UPDATE sqlmesh._environments SET finalized_ts = NULL WHERE name = 'dev'" - ) - - context.plan( - "prod", - restate_models=["sushi.top_waiters"], - auto_apply=True, - no_prompts=True, - skip_tests=True, - ) - assert context.engine_adapter.table_exists(top_waiters_snapshot.table_name()) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_plan_against_expired_environment(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - - modified_models = {model.fqn, context.get_model("sushi.top_waiters").fqn} - - plan = context.plan_builder("dev").build() - assert plan.has_changes - assert set(plan.context_diff.modified_snapshots) == modified_models - assert plan.missing_intervals - context.apply(plan) - - # Make sure there are no changes when comparing against the existing environment. - plan = context.plan_builder("dev").build() - assert not plan.has_changes - assert not plan.context_diff.modified_snapshots - assert not plan.missing_intervals - - # Invalidate the environment and make sure that the plan detects the changes. - context.invalidate_environment("dev") - plan = context.plan_builder("dev").build() - assert plan.has_changes - assert set(plan.context_diff.modified_snapshots) == modified_models - assert not plan.missing_intervals - context.apply(plan) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_new_forward_only_model_concurrent_versions(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - new_model_expr = d.parse( - """ - MODEL ( - name memory.sushi.new_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only TRUE, - on_destructive_change 'allow', - ), - ); - - SELECT '2023-01-07' AS ds, 1 AS a; - """ - ) - new_model = load_sql_based_model(new_model_expr) - - # Add the first version of the model and apply it to dev_a. - context.upsert_model(new_model) - snapshot_a = context.get_snapshot(new_model.name) - plan_a = context.plan_builder("dev_a").build() - snapshot_a = plan_a.snapshots[snapshot_a.snapshot_id] - - assert snapshot_a.snapshot_id in plan_a.context_diff.new_snapshots - assert snapshot_a.snapshot_id in plan_a.context_diff.added - assert snapshot_a.change_category == SnapshotChangeCategory.BREAKING - - context.apply(plan_a) - - new_model_alt_expr = d.parse( - """ - MODEL ( - name memory.sushi.new_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only TRUE, - on_destructive_change 'allow', - ), - ); - - SELECT '2023-01-07' AS ds, 1 AS b; - """ - ) - new_model_alt = load_sql_based_model(new_model_alt_expr) - - # Add the second version of the model but don't apply it yet - context.upsert_model(new_model_alt) - snapshot_b = context.get_snapshot(new_model_alt.name) - plan_b = context.plan_builder("dev_b").build() - snapshot_b = plan_b.snapshots[snapshot_b.snapshot_id] - - assert snapshot_b.snapshot_id in plan_b.context_diff.new_snapshots - assert snapshot_b.snapshot_id in plan_b.context_diff.added - assert snapshot_b.change_category == SnapshotChangeCategory.BREAKING - - assert snapshot_b.fingerprint != snapshot_a.fingerprint - assert snapshot_b.version == snapshot_a.version - - # Apply the 1st version to prod - context.upsert_model(new_model) - plan_prod_a = context.plan_builder("prod").build() - assert snapshot_a.snapshot_id in plan_prod_a.snapshots - assert ( - plan_prod_a.snapshots[snapshot_a.snapshot_id].change_category - == SnapshotChangeCategory.BREAKING - ) - context.apply(plan_prod_a) - - df = context.fetchdf("SELECT * FROM memory.sushi.new_model") - assert df.to_dict() == {"ds": {0: "2023-01-07"}, "a": {0: 1}} - - # Modify the 1st version in prod to trigger a forward-only change - new_model = add_projection_to_model(t.cast(SqlModel, new_model)) - context.upsert_model(new_model) - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - - # Apply the 2nd version to dev_b. - # At this point the snapshot of the 2nd version has already been categorized but not - # persisted in the state. This means that when the snapshot of the 1st version was - # being unpaused during promotion to prod, the state of the 2nd version snapshot was not updated - context.apply(plan_b) - - # Apply the 2nd version to prod - context.upsert_model(new_model_alt) - plan_prod_b = context.plan_builder("prod").build() - assert ( - plan_prod_b.snapshots[snapshot_b.snapshot_id].change_category - == SnapshotChangeCategory.BREAKING - ) - assert not plan_prod_b.requires_backfill - context.apply(plan_prod_b) - - df = context.fetchdf("SELECT * FROM memory.sushi.new_model").replace({np.nan: None}) - assert df.to_dict() == {"ds": {0: "2023-01-07"}, "b": {0: None}} - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_new_forward_only_model_same_dev_environment(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - new_model_expr = d.parse( - """ - MODEL ( - name memory.sushi.new_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only TRUE, - on_destructive_change 'allow', - ), - ); - - SELECT '2023-01-07' AS ds, 1 AS a; - """ - ) - new_model = load_sql_based_model(new_model_expr) - - # Add the first version of the model and apply it to dev. - context.upsert_model(new_model) - snapshot_a = context.get_snapshot(new_model.name) - plan_a = context.plan_builder("dev").build() - snapshot_a = plan_a.snapshots[snapshot_a.snapshot_id] - - assert snapshot_a.snapshot_id in plan_a.context_diff.new_snapshots - assert snapshot_a.snapshot_id in plan_a.context_diff.added - assert snapshot_a.change_category == SnapshotChangeCategory.BREAKING - - context.apply(plan_a) - - df = context.fetchdf("SELECT * FROM memory.sushi__dev.new_model") - assert df.to_dict() == {"ds": {0: "2023-01-07"}, "a": {0: 1}} - - new_model_alt_expr = d.parse( - """ - MODEL ( - name memory.sushi.new_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only TRUE, - on_destructive_change 'allow', - ), - ); - - SELECT '2023-01-07' AS ds, 1 AS b; - """ - ) - new_model_alt = load_sql_based_model(new_model_alt_expr) - - # Add the second version of the model and apply it to the same environment. - context.upsert_model(new_model_alt) - snapshot_b = context.get_snapshot(new_model_alt.name) - - context.invalidate_environment("dev", sync=True) - plan_b = context.plan_builder("dev").build() - snapshot_b = plan_b.snapshots[snapshot_b.snapshot_id] - - context.apply(plan_b) - - df = context.fetchdf("SELECT * FROM memory.sushi__dev.new_model").replace({np.nan: None}) - assert df.to_dict() == {"ds": {0: "2023-01-07"}, "b": {0: 1}} - - -@time_machine.travel("2023-01-08 01:00:00 UTC") -def test_run_auto_restatement(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - context.engine_adapter.execute( - "CREATE TABLE _test_auto_restatement_intervals (name STRING, start_ds STRING, end_ds STRING)" - ) - - @macro() - def record_intervals( - evaluator, name: exp.Expression, start: exp.Expression, end: exp.Expression, **kwargs: t.Any - ) -> None: - if evaluator.runtime_stage == "evaluating": - evaluator.engine_adapter.insert_append( - "_test_auto_restatement_intervals", - pd.DataFrame({"name": [name.name], "start_ds": [start.name], "end_ds": [end.name]}), - ) - - new_model_expr = d.parse( - """ - MODEL ( - name memory.sushi.new_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - auto_restatement_cron '0 6 * * 7', -- At 6am every Sunday - auto_restatement_intervals 3, - ), - start '2023-01-01', - ); - - @record_intervals('new_model', @start_ds, @end_ds); - - SELECT '2023-01-07' AS ds, 1 AS a; - """ - ) - new_model = load_sql_based_model(new_model_expr) - context.upsert_model(new_model) - - new_model_downstream_expr = d.parse( - """ - MODEL ( - name memory.sushi.new_model_downstream, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - ), - cron '@hourly', - ); - - @record_intervals('new_model_downstream', @start_ts, @end_ts); - - SELECT * FROM memory.sushi.new_model; - """ - ) - new_model_downstream = load_sql_based_model(new_model_downstream_expr) - context.upsert_model(new_model_downstream) - - plan = context.plan_builder("prod").build() - context.apply(plan) - - with time_machine.travel("2023-01-08 06:01:00 UTC"): - assert context.run() - - recorded_intervals_df = context.engine_adapter.fetchdf( - "SELECT start_ds, end_ds FROM _test_auto_restatement_intervals WHERE name = 'new_model'" - ) - # The first interval is the first backfill and the second interval should be the 3 auto restated intervals - assert recorded_intervals_df.to_dict() == { - "start_ds": {0: "2023-01-01", 1: "2023-01-05"}, - "end_ds": {0: "2023-01-07", 1: "2023-01-07"}, - } - recorded_intervals_downstream_df = context.engine_adapter.fetchdf( - "SELECT start_ds, end_ds FROM _test_auto_restatement_intervals WHERE name = 'new_model_downstream'" - ) - # The first interval is the first backfill, the second interval should be the 3 days of restated intervals, and - # the third interval should catch up to the current hour - assert recorded_intervals_downstream_df.to_dict() == { - "start_ds": { - 0: "2023-01-01 00:00:00", - 1: "2023-01-05 00:00:00", - 2: "2023-01-08 01:00:00", - }, - "end_ds": { - 0: "2023-01-08 00:59:59.999999", - 1: "2023-01-07 23:59:59.999999", - 2: "2023-01-08 05:59:59.999999", - }, - } - - snapshot = context.get_snapshot(new_model.name) - snapshot = context.state_sync.state_sync.get_snapshots([snapshot.snapshot_id])[ - snapshot.snapshot_id - ] - assert snapshot.next_auto_restatement_ts == to_timestamp("2023-01-15 06:00:00") - assert not snapshot.pending_restatement_intervals - - snapshot_downstream = context.get_snapshot(new_model_downstream.name) - snapshot_downstream = context.state_sync.state_sync.get_snapshots( - [snapshot_downstream.snapshot_id] - )[snapshot_downstream.snapshot_id] - assert not snapshot_downstream.next_auto_restatement_ts - assert not snapshot_downstream.pending_restatement_intervals - - -@time_machine.travel("2023-01-08 01:00:00 UTC") -def test_run_auto_restatement_plan_preview(init_and_plan_context: t.Callable): - context, init_plan = init_and_plan_context("examples/sushi") - context.apply(init_plan) - - new_model_expr = d.parse( - """ - MODEL ( - name memory.sushi.new_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - auto_restatement_cron '0 6 * * 7', - ), - start '2023-01-01', - ); - - SELECT '2023-01-07' AS ds, 1 AS a; - """ - ) - new_model = load_sql_based_model(new_model_expr) - context.upsert_model(new_model) - snapshot = context.get_snapshot(new_model.name) - - plan_dev = context.plan_builder("dev").build() - # Make sure that a limited preview is computed by default - assert to_timestamp(plan_dev.start) == to_timestamp("2023-01-07") - assert plan_dev.missing_intervals == [ - SnapshotIntervals( - snapshot.snapshot_id, - [(to_timestamp("2023-01-07"), to_timestamp("2023-01-08"))], - ) - ] - assert not plan_dev.deployability_index.is_deployable(snapshot.snapshot_id) - context.apply(plan_dev) - - plan_prod = context.plan_builder("prod").build() - assert plan_prod.missing_intervals == [ - SnapshotIntervals( - context.get_snapshot(new_model.name).snapshot_id, - [ - (to_timestamp("2023-01-01"), to_timestamp("2023-01-02")), - (to_timestamp("2023-01-02"), to_timestamp("2023-01-03")), - (to_timestamp("2023-01-03"), to_timestamp("2023-01-04")), - (to_timestamp("2023-01-04"), to_timestamp("2023-01-05")), - (to_timestamp("2023-01-05"), to_timestamp("2023-01-06")), - (to_timestamp("2023-01-06"), to_timestamp("2023-01-07")), - (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")), - ], - ) - ] - context.apply(plan_prod) - - -@time_machine.travel("2023-01-08 01:00:00 UTC") -def test_run_auto_restatement_failure(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - @macro() - def fail_auto_restatement(evaluator, start: exp.Expression, **kwargs: t.Any) -> None: - if evaluator.runtime_stage == "evaluating" and start.name != "2023-01-01": - raise Exception("Failed") - - new_model_expr = d.parse( - """ - MODEL ( - name memory.sushi.new_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - auto_restatement_cron '0 6 * * 7', -- At 6am every Sunday - auto_restatement_intervals 3, - ), - start '2023-01-01', - ); - - @fail_auto_restatement(@start_ds); - - SELECT '2023-01-07' AS ds, 1 AS a; - """ - ) - new_model = load_sql_based_model(new_model_expr) - context.upsert_model(new_model) - - plan = context.plan_builder("prod").build() - context.apply(plan) - - with time_machine.travel("2023-01-08 06:01:00 UTC"): - run_status = context.run() - assert run_status.is_failure - - snapshot = context.get_snapshot(new_model.name) - snapshot = context.state_sync.state_sync.get_snapshots([snapshot.snapshot_id])[ - snapshot.snapshot_id - ] - assert snapshot.next_auto_restatement_ts == to_timestamp("2023-01-15 06:00:00") - assert snapshot.pending_restatement_intervals == [ - (to_timestamp("2023-01-05"), to_timestamp("2023-01-08")) - ] - - -def test_plan_twice_with_star_macro_yields_no_diff(tmp_path: Path): - init_example_project(tmp_path, engine_type="duckdb") - - star_model_definition = """ - MODEL ( - name sqlmesh_example.star_model, - kind FULL - ); - - SELECT @STAR(sqlmesh_example.full_model) FROM sqlmesh_example.full_model - """ - - star_model_path = tmp_path / "models" / "star_model.sql" - star_model_path.write_text(star_model_definition) - - db_path = str(tmp_path / "db.db") - config = Config( - gateways={"main": GatewayConfig(connection=DuckDBConnectionConfig(database=db_path))}, - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - ) - context = Context(paths=tmp_path, config=config) - context.plan(auto_apply=True, no_prompts=True) - - # Instantiate new context to remove caches etc - new_context = Context(paths=tmp_path, config=config) - - star_model = new_context.get_model("sqlmesh_example.star_model") - assert ( - star_model.render_query_or_raise().sql() - == 'SELECT CAST("full_model"."item_id" AS INT) AS "item_id", CAST("full_model"."num_orders" AS BIGINT) AS "num_orders" FROM "db"."sqlmesh_example"."full_model" AS "full_model"' - ) - - new_plan = new_context.plan_builder().build() - assert not new_plan.has_changes - assert not new_plan.new_snapshots - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_create_environment_no_changes_with_selector(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - with pytest.raises(NoChangesPlanError): - context.plan_builder("dev").build() - - plan = context.plan_builder("dev", select_models=["*top_waiters"]).build() - assert not plan.missing_intervals - context.apply(plan) - - schema_objects = context.engine_adapter.get_data_objects("sushi__dev") - assert {o.name for o in schema_objects} == {"top_waiters"} - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_empty_backfill(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - plan = context.plan_builder("prod", skip_tests=True, empty_backfill=True).build() - assert plan.missing_intervals - assert plan.empty_backfill - assert not plan.requires_backfill - - context.apply(plan) - - for model in context.models.values(): - if model.is_seed or model.kind.is_symbolic: - continue - row_num = context.engine_adapter.fetchone(f"SELECT COUNT(*) FROM {model.name}")[0] - assert row_num == 0 - - plan = context.plan_builder("prod", skip_tests=True).build() - assert not plan.requires_backfill - assert not plan.has_changes - assert not plan.missing_intervals - - snapshots = plan.snapshots - for snapshot in snapshots.values(): - if not snapshot.intervals: - continue - assert snapshot.intervals[-1][1] <= to_timestamp("2023-01-08") - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_empty_backfill_new_model(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - new_model = load_sql_based_model( - d.parse( - """ - MODEL ( - name memory.sushi.new_model, - kind FULL, - cron '0 8 * * *', - start '2023-01-01', - ); - - SELECT 1 AS one; - """ - ) - ) - new_model_name = context.upsert_model(new_model).fqn - - with time_machine.travel("2023-01-09 00:00:00 UTC"): - plan = context.plan_builder("dev", skip_tests=True, empty_backfill=True).build() - assert plan.end == to_datetime("2023-01-09") - assert plan.missing_intervals - assert plan.empty_backfill - assert not plan.requires_backfill - - context.apply(plan) - - for model in context.models.values(): - if model.is_seed or model.kind.is_symbolic: - continue - row_num = context.engine_adapter.fetchone(f"SELECT COUNT(*) FROM sushi__dev.new_model")[ - 0 - ] - assert row_num == 0 - - plan = context.plan_builder("prod", skip_tests=True).build() - assert not plan.requires_backfill - assert not plan.missing_intervals - - snapshots = plan.snapshots - for snapshot in snapshots.values(): - if not snapshot.intervals: - continue - elif snapshot.name == new_model_name: - assert snapshot.intervals[-1][1] == to_timestamp("2023-01-09") - else: - assert snapshot.intervals[-1][1] <= to_timestamp("2023-01-08") - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -@pytest.mark.parametrize("forward_only", [False, True]) -def test_plan_repairs_unrenderable_snapshot_state( - init_and_plan_context: t.Callable, forward_only: bool -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - target_snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") - assert target_snapshot - - # Manually corrupt the snapshot's query - raw_snapshot = context.state_sync.state_sync.engine_adapter.fetchone( - f"SELECT snapshot FROM sqlmesh._snapshots WHERE name = '{target_snapshot.name}' AND identifier = '{target_snapshot.identifier}'" - )[0] # type: ignore - parsed_snapshot = json.loads(raw_snapshot) - parsed_snapshot["node"]["query"] = "SELECT @missing_macro()" - context.state_sync.state_sync.engine_adapter.update_table( - "sqlmesh._snapshots", - {"snapshot": json.dumps(parsed_snapshot)}, - f"name = '{target_snapshot.name}' AND identifier = '{target_snapshot.identifier}'", - ) - - context.clear_caches() - target_snapshot_in_state = context.state_sync.get_snapshots([target_snapshot.snapshot_id])[ - target_snapshot.snapshot_id - ] - - with pytest.raises(Exception): - target_snapshot_in_state.model.render_query_or_raise() - - # Repair the snapshot by creating a new version of it - context.upsert_model(target_snapshot.model.name, stamp="repair") - target_snapshot = context.get_snapshot(target_snapshot.name) - - plan_builder = context.plan_builder("prod", forward_only=forward_only) - plan = plan_builder.build() - if not forward_only: - assert target_snapshot.snapshot_id in {i.snapshot_id for i in plan.missing_intervals} - assert plan.directly_modified == {target_snapshot.snapshot_id} - plan_builder.set_choice(target_snapshot, SnapshotChangeCategory.NON_BREAKING) - plan = plan_builder.build() - - context.apply(plan) - - context.clear_caches() - assert context.get_snapshot(target_snapshot.name).model.render_query_or_raise() - target_snapshot_in_state = context.state_sync.get_snapshots([target_snapshot.snapshot_id])[ - target_snapshot.snapshot_id - ] - assert target_snapshot_in_state.model.render_query_or_raise() - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_no_backfill_for_model_downstream_of_metadata_change(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - # Make sushi.waiter_revenue_by_day a forward-only model. - forward_only_model = context.get_model("sushi.waiter_revenue_by_day") - updated_model_kind = forward_only_model.kind.copy(update={"forward_only": True}) - forward_only_model = forward_only_model.copy(update={"kind": updated_model_kind}) - context.upsert_model(forward_only_model) - - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - - # Make a metadata change upstream of the forward-only model. - context.upsert_model("sushi.orders", owner="new_owner") - - plan = context.plan_builder("test_dev").build() - assert plan.has_changes - assert not plan.directly_modified - assert not plan.indirectly_modified - assert not plan.missing_intervals - assert all( - snapshot.change_category == SnapshotChangeCategory.METADATA - for snapshot in plan.new_snapshots - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_evaluate_uncategorized_snapshot(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - # Add a new projection - model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - - # Downstream model references the new projection - downstream_model = context.get_model("sushi.top_waiters") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, downstream_model), literal=False)) - - df = context.evaluate( - "sushi.top_waiters", start="2023-01-05", end="2023-01-06", execution_time=now() - ) - assert set(df["one"].tolist()) == {1} - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_table_name(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") - assert snapshot - assert ( - context.table_name("sushi.waiter_revenue_by_day", "prod") - == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{snapshot.version}" - ) - - with pytest.raises(SQLMeshError, match="Environment 'dev' was not found."): - context.table_name("sushi.waiter_revenue_by_day", "dev") - - with pytest.raises( - SQLMeshError, match="Model 'sushi.missing' was not found in environment 'prod'." - ): - context.table_name("sushi.missing", "prod") - - # Add a new projection - model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - - context.plan("dev_a", auto_apply=True, no_prompts=True, skip_tests=True) - - new_snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") - assert new_snapshot.version != snapshot.version - - assert ( - context.table_name("sushi.waiter_revenue_by_day", "dev_a") - == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{new_snapshot.version}" - ) - - # Make a forward-only change - context.upsert_model(model, stamp="forward_only") - - context.plan("dev_b", auto_apply=True, no_prompts=True, skip_tests=True, forward_only=True) - - forward_only_snapshot = context.get_snapshot("sushi.waiter_revenue_by_day") - assert forward_only_snapshot.version == snapshot.version - assert forward_only_snapshot.dev_version != snapshot.version - - assert ( - context.table_name("sushi.waiter_revenue_by_day", "dev_b") - == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{forward_only_snapshot.dev_version}__dev" - ) - - assert ( - context.table_name("sushi.waiter_revenue_by_day", "dev_b", prod=True) - == f"memory.sqlmesh__sushi.sushi__waiter_revenue_by_day__{snapshot.version}" - ) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_full_model_change_with_plan_start_not_matching_model_start( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - model = context.get_model("sushi.top_waiters") - context.upsert_model(model, kind=model_kind_type_from_name("FULL")()) # type: ignore - - # Apply the change with --skip-backfill first and no plan start - context.plan("dev", skip_tests=True, skip_backfill=True, no_prompts=True, auto_apply=True) - - # Apply the plan again but this time don't skip backfill and set start - # to be later than the model start - context.plan("dev", skip_tests=True, no_prompts=True, auto_apply=True, start="1 day ago") - - # Check that the number of rows is not 0 - row_num = context.engine_adapter.fetchone(f"SELECT COUNT(*) FROM sushi__dev.top_waiters")[0] - assert row_num > 0 - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_indirect_non_breaking_view_is_updated_with_new_table_references( - init_and_plan_context: t.Callable, -): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - # Add a new projection to the base model - model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model(add_projection_to_model(t.cast(SqlModel, model))) - - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - - # Run the janitor to delete the old snapshot record - context.run_janitor(ignore_ttl=True) - - # Check the downstream view and make sure it's still queryable - assert context.get_model("sushi.top_waiters").kind.is_view - row_num = context.engine_adapter.fetchone(f"SELECT COUNT(*) FROM sushi.top_waiters")[0] - assert row_num > 0 - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_plan_explain(init_and_plan_context: t.Callable): - old_console = get_console() - set_console(TerminalConsole()) - - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - waiter_revenue_by_day_model = context.get_model("sushi.waiter_revenue_by_day") - waiter_revenue_by_day_model = add_projection_to_model( - t.cast(SqlModel, waiter_revenue_by_day_model) - ) - context.upsert_model(waiter_revenue_by_day_model) - - waiter_revenue_by_day_snapshot = context.get_snapshot(waiter_revenue_by_day_model.name) - top_waiters_snapshot = context.get_snapshot("sushi.top_waiters") - - common_kwargs = dict(skip_tests=True, no_prompts=True, explain=True) - - # For now just making sure the plan doesn't error - context.plan("dev", **common_kwargs) - context.plan("dev", **common_kwargs, skip_backfill=True) - context.plan("dev", **common_kwargs, empty_backfill=True) - context.plan("dev", **common_kwargs, forward_only=True, enable_preview=True) - context.plan("prod", **common_kwargs) - context.plan("prod", **common_kwargs, forward_only=True) - context.plan("prod", **common_kwargs, restate_models=[waiter_revenue_by_day_model.name]) - - set_console(old_console) - - # Make sure that the now changes were actually applied - for target_env in ("dev", "prod"): - plan = context.plan_builder(target_env, skip_tests=True).build() - assert plan.has_changes - assert plan.missing_intervals - assert plan.directly_modified == {waiter_revenue_by_day_snapshot.snapshot_id} - assert len(plan.new_snapshots) == 2 - assert {s.snapshot_id for s in plan.new_snapshots} == { - waiter_revenue_by_day_snapshot.snapshot_id, - top_waiters_snapshot.snapshot_id, - } - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_dbt_requirements(sushi_dbt_context: Context): - assert set(sushi_dbt_context.requirements) == {"dbt-core", "dbt-duckdb"} - assert sushi_dbt_context.requirements["dbt-core"].startswith("1.") - assert sushi_dbt_context.requirements["dbt-duckdb"].startswith("1.") - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_dbt_dialect_with_normalization_strategy(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context( - "tests/fixtures/dbt/sushi_test", config="test_config_with_normalization_strategy" - ) - assert context.default_dialect == "duckdb,normalization_strategy=LOWERCASE" - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_dbt_before_all_with_var_ref_source(init_and_plan_context: t.Callable): - _, plan = init_and_plan_context( - "tests/fixtures/dbt/sushi_test", config="test_config_with_normalization_strategy" - ) - environment_statements = plan.to_evaluatable().environment_statements - assert environment_statements - rendered_statements = [e.render_before_all(dialect="duckdb") for e in environment_statements] - assert rendered_statements[0] == [ - "CREATE TABLE IF NOT EXISTS analytic_stats (physical_table TEXT, evaluation_time TEXT)", - "CREATE TABLE IF NOT EXISTS to_be_executed_last (col TEXT)", - "SELECT 1 AS var, 'items' AS src, 'waiters' AS ref", - ] - - -@pytest.mark.parametrize( - "context_fixture", - ["sushi_context", "sushi_dbt_context", "sushi_test_dbt_context", "sushi_no_default_catalog"], -) -def test_model_add(context_fixture: Context, request): - initial_add(request.getfixturevalue(context_fixture), "dev") - - -def test_model_removed(sushi_context: Context): - environment = "dev" - initial_add(sushi_context, environment) - - top_waiters_snapshot_id = sushi_context.get_snapshot( - "sushi.top_waiters", raise_if_missing=True - ).snapshot_id - - sushi_context._models.pop('"memory"."sushi"."top_waiters"') - - def _validate_plan(context, plan): - validate_plan_changes(plan, removed=[top_waiters_snapshot_id]) - assert not plan.missing_intervals - - def _validate_apply(context): - assert not sushi_context.get_snapshot("sushi.top_waiters", raise_if_missing=False) - assert sushi_context.state_reader.get_snapshots([top_waiters_snapshot_id]) - env = sushi_context.state_reader.get_environment(environment) - assert env - assert all(snapshot.name != '"memory"."sushi"."top_waiters"' for snapshot in env.snapshots) - - apply_to_environment( - sushi_context, - environment, - SnapshotChangeCategory.BREAKING, - plan_validators=[_validate_plan], - apply_validators=[_validate_apply], - ) - - -def test_non_breaking_change(sushi_context: Context): - environment = "dev" - initial_add(sushi_context, environment) - validate_query_change(sushi_context, environment, SnapshotChangeCategory.NON_BREAKING, False) - - -def test_breaking_change(sushi_context: Context): - environment = "dev" - initial_add(sushi_context, environment) - validate_query_change(sushi_context, environment, SnapshotChangeCategory.BREAKING, False) - - -def test_logical_change(sushi_context: Context): - environment = "dev" - initial_add(sushi_context, environment) - previous_sushi_items_version = sushi_context.get_snapshot( - "sushi.items", raise_if_missing=True - ).version - - change_data_type( - sushi_context, - "sushi.items", - DataType.Type.DOUBLE, - DataType.Type.FLOAT, - ) - apply_to_environment(sushi_context, environment, SnapshotChangeCategory.NON_BREAKING) - - change_data_type( - sushi_context, - "sushi.items", - DataType.Type.FLOAT, - DataType.Type.DOUBLE, - ) - apply_to_environment(sushi_context, environment, SnapshotChangeCategory.NON_BREAKING) - - assert ( - sushi_context.get_snapshot("sushi.items", raise_if_missing=True).version - == previous_sushi_items_version - ) - - -def validate_query_change( - context: Context, - environment: str, - change_category: SnapshotChangeCategory, - logical: bool, -): - versions = snapshots_to_versions(context.snapshots.values()) - - change_data_type( - context, - "sushi.items", - DataType.Type.DOUBLE, - DataType.Type.FLOAT, - ) - - directly_modified = ['"memory"."sushi"."items"'] - indirectly_modified = [ - '"memory"."sushi"."order_items"', - '"memory"."sushi"."waiter_revenue_by_day"', - '"memory"."sushi"."customer_revenue_by_day"', - '"memory"."sushi"."customer_revenue_lifetime"', - '"memory"."sushi"."top_waiters"', - "assert_item_price_above_zero", - ] - not_modified = [ - snapshot.name - for snapshot in context.snapshots.values() - if snapshot.name not in directly_modified and snapshot.name not in indirectly_modified - ] - - if change_category == SnapshotChangeCategory.BREAKING and not logical: - models_same = not_modified - models_different = directly_modified + indirectly_modified - elif change_category == SnapshotChangeCategory.FORWARD_ONLY: - models_same = not_modified + directly_modified + indirectly_modified - models_different = [] - else: - models_same = not_modified + indirectly_modified - models_different = directly_modified - - def _validate_plan(context, plan): - validate_plan_changes(plan, modified=directly_modified + indirectly_modified) - assert bool(plan.missing_intervals) != logical - - def _validate_apply(context): - current_versions = snapshots_to_versions(context.snapshots.values()) - validate_versions_same(models_same, versions, current_versions) - validate_versions_different(models_different, versions, current_versions) - - apply_to_environment( - context, - environment, - change_category, - plan_validators=[_validate_plan], - apply_validators=[_validate_apply], - ) - - -@pytest.mark.parametrize( - "from_, to", - [ - (ModelKindName.INCREMENTAL_BY_TIME_RANGE, ModelKindName.FULL), - (ModelKindName.FULL, ModelKindName.INCREMENTAL_BY_TIME_RANGE), - ], -) -def test_model_kind_change(from_: ModelKindName, to: ModelKindName, sushi_context: Context): - environment = f"test_model_kind_change__{from_.value.lower()}__{to.value.lower()}" - incremental_snapshot = sushi_context.get_snapshot("sushi.items", raise_if_missing=True).copy() - - if from_ != ModelKindName.INCREMENTAL_BY_TIME_RANGE: - change_model_kind(sushi_context, from_) - apply_to_environment(sushi_context, environment, SnapshotChangeCategory.NON_BREAKING) - - if to == ModelKindName.INCREMENTAL_BY_TIME_RANGE: - sushi_context.upsert_model(incremental_snapshot.model) - else: - change_model_kind(sushi_context, to) - - logical = to in (ModelKindName.INCREMENTAL_BY_TIME_RANGE, ModelKindName.EMBEDDED) - validate_model_kind_change(to, sushi_context, environment, logical=logical) - - -def change_model_kind(context: Context, kind: ModelKindName): - if kind in (ModelKindName.VIEW, ModelKindName.EMBEDDED, ModelKindName.FULL): - context.upsert_model( - "sushi.items", - partitioned_by=[], - ) - context.upsert_model("sushi.items", kind=model_kind_type_from_name(kind)()) # type: ignore - - -def validate_model_kind_change( - kind_name: ModelKindName, - context: Context, - environment: str, - *, - logical: bool, -): - directly_modified = ['"memory"."sushi"."items"'] - indirectly_modified = [ - '"memory"."sushi"."order_items"', - '"memory"."sushi"."waiter_revenue_by_day"', - '"memory"."sushi"."customer_revenue_by_day"', - '"memory"."sushi"."customer_revenue_lifetime"', - '"memory"."sushi"."top_waiters"', - "assert_item_price_above_zero", - ] - if kind_name == ModelKindName.INCREMENTAL_BY_TIME_RANGE: - kind: ModelKind = IncrementalByTimeRangeKind(time_column=TimeColumn(column="event_date")) - elif kind_name == ModelKindName.INCREMENTAL_BY_UNIQUE_KEY: - kind = IncrementalByUniqueKeyKind(unique_key="id") - else: - kind = model_kind_type_from_name(kind_name)() # type: ignore - - def _validate_plan(context, plan): - validate_plan_changes(plan, modified=directly_modified + indirectly_modified) - assert ( - next( - snapshot - for snapshot in plan.snapshots.values() - if snapshot.name == '"memory"."sushi"."items"' - ).model.kind.name - == kind.name - ) - assert bool(plan.missing_intervals) != logical - - apply_to_environment( - context, - environment, - SnapshotChangeCategory.NON_BREAKING, - plan_validators=[_validate_plan], - ) - - -def test_environment_isolation(sushi_context: Context): - prod_snapshots = sushi_context.snapshots.values() - - change_data_type( - sushi_context, - "sushi.items", - DataType.Type.DOUBLE, - DataType.Type.FLOAT, - ) - directly_modified = ['"memory"."sushi"."items"'] - indirectly_modified = [ - '"memory"."sushi"."order_items"', - '"memory"."sushi"."waiter_revenue_by_day"', - '"memory"."sushi"."customer_revenue_by_day"', - '"memory"."sushi"."customer_revenue_lifetime"', - '"memory"."sushi"."top_waiters"', - "assert_item_price_above_zero", - ] - - apply_to_environment(sushi_context, "dev", SnapshotChangeCategory.BREAKING) - - # Verify prod unchanged - validate_apply_basics(sushi_context, "prod", prod_snapshots) - - def _validate_plan(context, plan): - validate_plan_changes(plan, modified=directly_modified + indirectly_modified) - assert not plan.missing_intervals - - apply_to_environment( - sushi_context, - "prod", - SnapshotChangeCategory.BREAKING, - plan_validators=[_validate_plan], - ) - - -def test_environment_promotion(sushi_context: Context): - initial_add(sushi_context, "dev") - - # Simulate prod "ahead" - change_data_type(sushi_context, "sushi.items", DataType.Type.DOUBLE, DataType.Type.FLOAT) - apply_to_environment(sushi_context, "prod", SnapshotChangeCategory.BREAKING) - - # Simulate rebase - apply_to_environment(sushi_context, "dev", SnapshotChangeCategory.BREAKING) - - # Make changes in dev - change_data_type(sushi_context, "sushi.items", DataType.Type.FLOAT, DataType.Type.DECIMAL) - apply_to_environment(sushi_context, "dev", SnapshotChangeCategory.NON_BREAKING) - - change_data_type(sushi_context, "sushi.top_waiters", DataType.Type.DOUBLE, DataType.Type.INT) - apply_to_environment(sushi_context, "dev", SnapshotChangeCategory.BREAKING) - - change_data_type( - sushi_context, - "sushi.customer_revenue_by_day", - DataType.Type.DOUBLE, - DataType.Type.FLOAT, - ) - apply_to_environment( - sushi_context, - "dev", - SnapshotChangeCategory.FORWARD_ONLY, - allow_destructive_models=['"memory"."sushi"."customer_revenue_by_day"'], - ) - - # Promote to prod - def _validate_plan(context, plan): - sushi_items_snapshot = context.get_snapshot("sushi.items", raise_if_missing=True) - sushi_top_waiters_snapshot = context.get_snapshot( - "sushi.top_waiters", raise_if_missing=True - ) - sushi_customer_revenue_by_day_snapshot = context.get_snapshot( - "sushi.customer_revenue_by_day", raise_if_missing=True - ) - - assert ( - plan.context_diff.modified_snapshots[sushi_items_snapshot.name][0].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert ( - plan.context_diff.modified_snapshots[sushi_top_waiters_snapshot.name][0].change_category - == SnapshotChangeCategory.BREAKING - ) - assert ( - plan.context_diff.modified_snapshots[sushi_customer_revenue_by_day_snapshot.name][ - 0 - ].change_category - == SnapshotChangeCategory.NON_BREAKING - ) - assert plan.context_diff.snapshots[ - sushi_customer_revenue_by_day_snapshot.snapshot_id - ].is_forward_only - - apply_to_environment( - sushi_context, - "prod", - SnapshotChangeCategory.NON_BREAKING, - plan_validators=[_validate_plan], - allow_destructive_models=['"memory"."sushi"."customer_revenue_by_day"'], - ) - - -def test_no_override(sushi_context: Context) -> None: - change_data_type( - sushi_context, - "sushi.items", - DataType.Type.INT, - DataType.Type.BIGINT, - ) - - change_data_type( - sushi_context, - "sushi.order_items", - DataType.Type.INT, - DataType.Type.BIGINT, - ) - - plan_builder = sushi_context.plan_builder("prod") - plan = plan_builder.build() - - sushi_items_snapshot = sushi_context.get_snapshot("sushi.items", raise_if_missing=True) - sushi_order_items_snapshot = sushi_context.get_snapshot( - "sushi.order_items", raise_if_missing=True - ) - sushi_water_revenue_by_day_snapshot = sushi_context.get_snapshot( - "sushi.waiter_revenue_by_day", raise_if_missing=True - ) - - items = plan.context_diff.snapshots[sushi_items_snapshot.snapshot_id] - order_items = plan.context_diff.snapshots[sushi_order_items_snapshot.snapshot_id] - waiter_revenue = plan.context_diff.snapshots[sushi_water_revenue_by_day_snapshot.snapshot_id] - - plan_builder.set_choice(items, SnapshotChangeCategory.BREAKING).set_choice( - order_items, SnapshotChangeCategory.NON_BREAKING - ) - plan_builder.build() - assert items.is_new_version - assert waiter_revenue.is_new_version - plan_builder.set_choice(items, SnapshotChangeCategory.NON_BREAKING) - plan_builder.build() - assert not waiter_revenue.is_new_version - - -@pytest.mark.parametrize( - "change_categories, expected", - [ - ([SnapshotChangeCategory.NON_BREAKING], SnapshotChangeCategory.BREAKING), - ([SnapshotChangeCategory.BREAKING], SnapshotChangeCategory.BREAKING), - ( - [SnapshotChangeCategory.NON_BREAKING, SnapshotChangeCategory.NON_BREAKING], - SnapshotChangeCategory.BREAKING, - ), - ( - [SnapshotChangeCategory.NON_BREAKING, SnapshotChangeCategory.BREAKING], - SnapshotChangeCategory.BREAKING, - ), - ( - [SnapshotChangeCategory.BREAKING, SnapshotChangeCategory.NON_BREAKING], - SnapshotChangeCategory.BREAKING, - ), - ( - [SnapshotChangeCategory.BREAKING, SnapshotChangeCategory.BREAKING], - SnapshotChangeCategory.BREAKING, - ), - ], -) -def test_revert( - sushi_context: Context, - change_categories: t.List[SnapshotChangeCategory], - expected: SnapshotChangeCategory, -): - environment = "prod" - original_snapshot_id = sushi_context.get_snapshot("sushi.items", raise_if_missing=True) - - types = (DataType.Type.DOUBLE, DataType.Type.FLOAT, DataType.Type.DECIMAL) - assert len(change_categories) < len(types) - - for i, category in enumerate(change_categories): - change_data_type(sushi_context, "sushi.items", *types[i : i + 2]) - apply_to_environment(sushi_context, environment, category) - assert ( - sushi_context.get_snapshot("sushi.items", raise_if_missing=True) != original_snapshot_id - ) - - change_data_type(sushi_context, "sushi.items", types[len(change_categories)], types[0]) - - def _validate_plan(_, plan): - snapshot = next(s for s in plan.snapshots.values() if s.name == '"memory"."sushi"."items"') - assert snapshot.change_category == expected - assert not plan.missing_intervals - - apply_to_environment( - sushi_context, - environment, - change_categories[-1], - plan_validators=[_validate_plan], - ) - assert sushi_context.get_snapshot("sushi.items", raise_if_missing=True) == original_snapshot_id - - -def test_revert_after_downstream_change(sushi_context: Context): - environment = "prod" - change_data_type(sushi_context, "sushi.items", DataType.Type.DOUBLE, DataType.Type.FLOAT) - apply_to_environment(sushi_context, environment, SnapshotChangeCategory.BREAKING) - - change_data_type( - sushi_context, - "sushi.waiter_revenue_by_day", - DataType.Type.DOUBLE, - DataType.Type.FLOAT, - ) - apply_to_environment(sushi_context, environment, SnapshotChangeCategory.NON_BREAKING) - - change_data_type(sushi_context, "sushi.items", DataType.Type.FLOAT, DataType.Type.DOUBLE) - - def _validate_plan(_, plan): - snapshot = next(s for s in plan.snapshots.values() if s.name == '"memory"."sushi"."items"') - assert snapshot.change_category == SnapshotChangeCategory.BREAKING - assert plan.missing_intervals - - apply_to_environment( - sushi_context, - environment, - SnapshotChangeCategory.BREAKING, - plan_validators=[_validate_plan], - ) - - -def test_auto_categorization(sushi_context: Context): - environment = "dev" - for config in sushi_context.configs.values(): - config.plan.auto_categorize_changes.sql = AutoCategorizationMode.FULL - initial_add(sushi_context, environment) - - version = sushi_context.get_snapshot( - "sushi.waiter_as_customer_by_day", raise_if_missing=True - ).version - fingerprint = sushi_context.get_snapshot( - "sushi.waiter_as_customer_by_day", raise_if_missing=True - ).fingerprint - - model = t.cast(SqlModel, sushi_context.get_model("sushi.customers", raise_if_missing=True)) - sushi_context.upsert_model( - "sushi.customers", - query_=ParsableSql(sql=model.query.select("'foo' AS foo").sql(dialect=model.dialect)), # type: ignore - ) - apply_to_environment(sushi_context, environment) - - assert ( - sushi_context.get_snapshot( - "sushi.waiter_as_customer_by_day", raise_if_missing=True - ).change_category - == SnapshotChangeCategory.INDIRECT_NON_BREAKING - ) - assert ( - sushi_context.get_snapshot( - "sushi.waiter_as_customer_by_day", raise_if_missing=True - ).fingerprint - != fingerprint - ) - assert ( - sushi_context.get_snapshot("sushi.waiter_as_customer_by_day", raise_if_missing=True).version - == version - ) - - -@use_terminal_console -def test_multi(mocker): - context = Context(paths=["examples/multi/repo_1", "examples/multi/repo_2"], gateway="memory") - - with patch.object(get_console(), "log_warning") as mock_logger: - context.plan_builder(environment="dev") - warnings = mock_logger.call_args[0][0] - repo1_path, repo2_path = context.configs.keys() - assert f"Linter warnings for {repo1_path}" in warnings - assert f"Linter warnings for {repo2_path}" not in warnings - - assert ( - context.render("bronze.a").sql() - == '''SELECT 1 AS "col_a", 'b' AS "col_b", 1 AS "one", 'repo_1' AS "dup"''' - ) - assert ( - context.render("silver.d").sql() - == '''SELECT "c"."col_a" AS "col_a", 2 AS "two", 'repo_2' AS "dup" FROM "memory"."silver"."c" AS "c"''' - ) - context._new_state_sync().reset(default_catalog=context.default_catalog) - plan = context.plan_builder().build() - assert len(plan.new_snapshots) == 5 - context.apply(plan) - - # Ensure before_all, after_all statements for multiple repos have executed - environment_statements = context.state_reader.get_environment_statements(c.PROD) - assert len(environment_statements) == 2 - assert context.fetchdf("select * from before_1").to_dict()["1"][0] == 1 - assert context.fetchdf("select * from before_2").to_dict()["2"][0] == 2 - assert context.fetchdf("select * from after_1").to_dict()["repo_1"][0] == "repo_1" - assert context.fetchdf("select * from after_2").to_dict()["repo_2"][0] == "repo_2" - - old_context = context - context = Context( - paths=["examples/multi/repo_1"], - state_sync=old_context.state_sync, - gateway="memory", - ) - context._engine_adapter = old_context.engine_adapter - del context.engine_adapters - - model = context.get_model("bronze.a") - assert model.project == "repo_1" - context.upsert_model( - model.copy( - update={ - "query_": ParsableSql(sql=model.query.select("'c' AS c").sql(dialect=model.dialect)) - } - ) - ) - plan = context.plan_builder().build() - - assert set(snapshot.name for snapshot in plan.directly_modified) == { - '"memory"."bronze"."a"', - '"memory"."bronze"."b"', - '"memory"."silver"."e"', - } - assert sorted([x.name for x in list(plan.indirectly_modified.values())[0]]) == [ - '"memory"."silver"."c"', - '"memory"."silver"."d"', - ] - assert len(plan.missing_intervals) == 3 - context.apply(plan) - validate_apply_basics(context, c.PROD, plan.snapshots.values()) - - # Ensure that before_all and after_all statements of both repos are there despite planning with repo_1 - environment_statements = context.state_reader.get_environment_statements(c.PROD) - assert len(environment_statements) == 2 - - # Ensure that environment statements have the project field set correctly - sorted_env_statements = sorted(environment_statements, key=lambda es: es.project) - assert sorted_env_statements[0].project == "repo_1" - assert sorted_env_statements[1].project == "repo_2" - - # Assert before_all and after_all for each project - assert sorted_env_statements[0].before_all == [ - "CREATE TABLE IF NOT EXISTS before_1 AS select @one()" - ] - assert sorted_env_statements[0].after_all == [ - "CREATE TABLE IF NOT EXISTS after_1 AS select @dup()" - ] - assert sorted_env_statements[1].before_all == [ - "CREATE TABLE IF NOT EXISTS before_2 AS select @two()" - ] - assert sorted_env_statements[1].after_all == [ - "CREATE TABLE IF NOT EXISTS after_2 AS select @dup()" - ] - - -@use_terminal_console -def test_multi_repo_single_project_environment_statements_update(copy_to_temp_path): - paths = copy_to_temp_path("examples/multi") - repo_1_path = f"{paths[0]}/repo_1" - repo_2_path = f"{paths[0]}/repo_2" - - context = Context(paths=[repo_1_path, repo_2_path], gateway="memory") - context._new_state_sync().reset(default_catalog=context.default_catalog) - - initial_plan = context.plan_builder().build() - context.apply(initial_plan) - - # Get initial statements - initial_statements = context.state_reader.get_environment_statements(c.PROD) - assert len(initial_statements) == 2 - - # Modify repo_1's config to add a new before_all statement - repo_1_config_path = f"{repo_1_path}/config.yaml" - with open(repo_1_config_path, "r") as f: - config_content = f.read() - - # Add a new before_all statement to repo_1 only - modified_config = config_content.replace( - "CREATE TABLE IF NOT EXISTS before_1 AS select @one()", - "CREATE TABLE IF NOT EXISTS before_1 AS select @one()\n - CREATE TABLE IF NOT EXISTS before_1_modified AS select 999", - ) - - with open(repo_1_config_path, "w") as f: - f.write(modified_config) - - # Create new context with modified config but only for repo_1 - context_repo_1_only = Context( - paths=[repo_1_path], state_sync=context.state_sync, gateway="memory" - ) - - # Plan with only repo_1, this should preserve repo_2's statements from state - repo_1_plan = context_repo_1_only.plan_builder(environment="dev").build() - context_repo_1_only.apply(repo_1_plan) - updated_statements = context_repo_1_only.state_reader.get_environment_statements("dev") - - # Should still have statements from both projects - assert len(updated_statements) == 2 - - # Sort by project - sorted_updated = sorted(updated_statements, key=lambda es: es.project or "") - - # Verify repo_1 has the new statement - repo_1_updated = sorted_updated[0] - assert repo_1_updated.project == "repo_1" - assert len(repo_1_updated.before_all) == 2 - assert "CREATE TABLE IF NOT EXISTS before_1_modified" in repo_1_updated.before_all[1] - - # Verify repo_2 statements are preserved from state - repo_2_preserved = sorted_updated[1] - assert repo_2_preserved.project == "repo_2" - assert len(repo_2_preserved.before_all) == 1 - assert "CREATE TABLE IF NOT EXISTS before_2" in repo_2_preserved.before_all[0] - assert "CREATE TABLE IF NOT EXISTS after_2 AS select @dup()" in repo_2_preserved.after_all[0] - - -@use_terminal_console -def test_multi_virtual_layer(copy_to_temp_path): - paths = copy_to_temp_path("tests/fixtures/multi_virtual_layer") - path = Path(paths[0]) - first_db_path = str(path / "db_1.db") - second_db_path = str(path / "db_2.db") - - config = Config( - gateways={ - "first": GatewayConfig( - connection=DuckDBConnectionConfig(database=first_db_path), - variables={"overriden_var": "gateway_1"}, - ), - "second": GatewayConfig( - connection=DuckDBConnectionConfig(database=second_db_path), - variables={"overriden_var": "gateway_2"}, - ), - }, - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - model_naming=NameInferenceConfig(infer_names=True), - default_gateway="first", - gateway_managed_virtual_layer=True, - variables={"overriden_var": "global", "global_one": 88}, - ) - - context = Context(paths=paths, config=config) - assert context.default_catalog_per_gateway == {"first": "db_1", "second": "db_2"} - assert len(context.engine_adapters) == 2 - - # For the model without gateway the default should be used and the gateway variable should overide the global - assert ( - context.render("first_schema.model_one").sql() - == 'SELECT \'gateway_1\' AS "item_id", 88 AS "global_one", 1 AS "macro_one"' - ) - - # For model with gateway specified the appropriate variable should be used to overide - assert ( - context.render("db_2.second_schema.model_one").sql() - == 'SELECT \'gateway_2\' AS "item_id", 88 AS "global_one", 1 AS "macro_one"' - ) - - plan = context.plan_builder().build() - assert len(plan.new_snapshots) == 4 - context.apply(plan) - - # Validate the tables that source from the first tables are correct as well with evaluate - assert ( - context.evaluate( - "first_schema.model_two", start=now(), end=now(), execution_time=now() - ).to_string() - == " item_id global_one\n0 gateway_1 88" - ) - assert ( - context.evaluate( - "db_2.second_schema.model_two", start=now(), end=now(), execution_time=now() - ).to_string() - == " item_id global_one\n0 gateway_2 88" - ) - - assert sorted(set(snapshot.name for snapshot in plan.directly_modified)) == [ - '"db_1"."first_schema"."model_one"', - '"db_1"."first_schema"."model_two"', - '"db_2"."second_schema"."model_one"', - '"db_2"."second_schema"."model_two"', - ] - - model = context.get_model("db_1.first_schema.model_one") - - context.upsert_model( - model.copy( - update={ - "query_": ParsableSql( - sql=model.query.select("'c' AS extra").sql(dialect=model.dialect) - ) - } - ) - ) - plan = context.plan_builder().build() - context.apply(plan) - - state_environments = context.state_reader.get_environments() - state_snapshots = context.state_reader.get_snapshots(context.snapshots.values()) - - assert state_environments[0].gateway_managed - assert len(state_snapshots) == len(state_environments[0].snapshots) - assert [snapshot.name for snapshot in plan.directly_modified] == [ - '"db_1"."first_schema"."model_one"' - ] - assert [x.name for x in list(plan.indirectly_modified.values())[0]] == [ - '"db_1"."first_schema"."model_two"' - ] - - assert len(plan.missing_intervals) == 1 - assert ( - context.evaluate( - "db_1.first_schema.model_one", start=now(), end=now(), execution_time=now() - ).to_string() - == " item_id global_one macro_one extra\n0 gateway_1 88 1 c" - ) - - # Create dev environment with changed models - model = context.get_model("db_2.second_schema.model_one") - context.upsert_model( - model.copy( - update={ - "query_": ParsableSql( - sql=model.query.select("'d' AS extra").sql(dialect=model.dialect) - ) - } - ) - ) - model = context.get_model("first_schema.model_two") - context.upsert_model( - model.copy( - update={ - "query_": ParsableSql( - sql=model.query.select("'d2' AS col").sql(dialect=model.dialect) - ) - } - ) - ) - plan = context.plan_builder("dev").build() - context.apply(plan) - - dev_environment = context.state_sync.get_environment("dev") - assert dev_environment is not None - - metadata_engine_1 = DuckDBMetadata.from_context(context) - start_schemas_1 = set(metadata_engine_1.schemas) - assert sorted(start_schemas_1) == sorted( - {"first_schema__dev", "sqlmesh", "first_schema", "sqlmesh__first_schema"} - ) - - metadata_engine_2 = DuckDBMetadata(context._get_engine_adapter("second")) - start_schemas_2 = set(metadata_engine_2.schemas) - assert sorted(start_schemas_2) == sorted( - {"sqlmesh__second_schema", "second_schema", "second_schema__dev"} - ) - - # Invalidate dev environment - context.invalidate_environment("dev") - invalidate_environment = context.state_sync.get_environment("dev") - assert invalidate_environment is not None - assert invalidate_environment.expiration_ts < dev_environment.expiration_ts # type: ignore - assert sorted(start_schemas_1) == sorted(set(metadata_engine_1.schemas)) - assert sorted(start_schemas_2) == sorted(set(metadata_engine_2.schemas)) - - # Run janitor - context._run_janitor() - assert context.state_sync.get_environment("dev") is None - removed_schemas = start_schemas_1 - set(metadata_engine_1.schemas) - assert removed_schemas == {"first_schema__dev"} - removed_schemas = start_schemas_2 - set(metadata_engine_2.schemas) - assert removed_schemas == {"second_schema__dev"} - prod_environment = context.state_sync.get_environment("prod") - - # Remove the second gateway's second model and apply plan - second_model = path / "models/second_schema/model_two.sql" - os.remove(second_model) - assert not second_model.exists() - context = Context(paths=paths, config=config) - plan = context.plan_builder().build() - context.apply(plan) - prod_environment = context.state_sync.get_environment("prod") - assert len(prod_environment.snapshots_) == 3 - - # Changing the flag should show a diff - context.config.gateway_managed_virtual_layer = False - plan = context.plan_builder().build() - assert not plan.requires_backfill - assert ( - plan.context_diff.previous_gateway_managed_virtual_layer - != plan.context_diff.gateway_managed_virtual_layer - ) - assert plan.context_diff.has_changes - - # This should error since the default_gateway won't have access to create the view on a non-shared catalog - with pytest.raises(NodeExecutionFailedError, match=r"Execution failed for node SnapshotId*"): - context.apply(plan) - - -def test_multi_dbt(mocker): - context = Context(paths=["examples/multi_dbt/bronze", "examples/multi_dbt/silver"]) - context._new_state_sync().reset(default_catalog=context.default_catalog) - plan = context.plan_builder().build() - assert len(plan.new_snapshots) == 4 - context.apply(plan) - validate_apply_basics(context, c.PROD, plan.snapshots.values()) - - environment_statements = context.state_sync.get_environment_statements(c.PROD) - assert len(environment_statements) == 2 - bronze_statements = environment_statements[0] - assert bronze_statements.before_all == [ - "JINJA_STATEMENT_BEGIN;\nCREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);\nJINJA_END;" - ] - assert not bronze_statements.after_all - silver_statements = environment_statements[1] - assert not silver_statements.before_all - assert silver_statements.after_all == [ - "JINJA_STATEMENT_BEGIN;\n{{ store_schemas(schemas) }}\nJINJA_END;" - ] - assert "store_schemas" in silver_statements.jinja_macros.root_macros - analytics_table = context.fetchdf("select * from analytic_stats;") - assert sorted(analytics_table.columns) == sorted(["physical_table", "evaluation_time"]) - schema_table = context.fetchdf("select * from schema_table;") - assert sorted(schema_table.all_schemas[0]) == sorted(["bronze", "silver"]) - - -def test_multi_hybrid(mocker): - context = Context( - paths=["examples/multi_hybrid/dbt_repo", "examples/multi_hybrid/sqlmesh_repo"] - ) - context._new_state_sync().reset(default_catalog=context.default_catalog) - plan = context.plan_builder().build() - - assert len(plan.new_snapshots) == 5 - assert context.dag.roots == {'"memory"."dbt_repo"."e"'} - assert context.dag.graph['"memory"."dbt_repo"."c"'] == {'"memory"."sqlmesh_repo"."b"'} - assert context.dag.graph['"memory"."sqlmesh_repo"."b"'] == {'"memory"."sqlmesh_repo"."a"'} - assert context.dag.graph['"memory"."sqlmesh_repo"."a"'] == {'"memory"."dbt_repo"."e"'} - assert context.dag.downstream('"memory"."dbt_repo"."e"') == [ - '"memory"."sqlmesh_repo"."a"', - '"memory"."sqlmesh_repo"."b"', - '"memory"."dbt_repo"."c"', - '"memory"."dbt_repo"."d"', - ] - - sqlmesh_model_a = context.get_model("sqlmesh_repo.a") - dbt_model_c = context.get_model("dbt_repo.c") - assert sqlmesh_model_a.project == "sqlmesh_repo" - - sqlmesh_rendered = ( - 'SELECT "e"."col_a" AS "col_a", "e"."col_b" AS "col_b" FROM "memory"."dbt_repo"."e" AS "e"' - ) - dbt_rendered = 'SELECT DISTINCT ROUND(CAST(("b"."col_a" / NULLIF(100, 0)) AS DECIMAL(16, 2)), 2) AS "rounded_col_a" FROM "memory"."sqlmesh_repo"."b" AS "b"' - assert sqlmesh_model_a.render_query().sql() == sqlmesh_rendered - assert dbt_model_c.render_query().sql() == dbt_rendered - - context.apply(plan) - validate_apply_basics(context, c.PROD, plan.snapshots.values()) - - -def test_incremental_time_self_reference( - mocker: MockerFixture, sushi_context: Context, sushi_data_validator: SushiDataValidator -): - start_ts = to_timestamp("1 week ago") - start_date, end_date = to_date("1 week ago"), to_date("yesterday") - if to_timestamp(start_date) < start_ts: - # The start date must be aligned by the interval unit. - start_date += timedelta(days=1) - - df = sushi_context.engine_adapter.fetchdf( - "SELECT MIN(event_date) FROM sushi.customer_revenue_lifetime" - ) - assert df.iloc[0, 0] == pd.to_datetime(start_date) - df = sushi_context.engine_adapter.fetchdf( - "SELECT MAX(event_date) FROM sushi.customer_revenue_lifetime" - ) - assert df.iloc[0, 0] == pd.to_datetime(end_date) - results = sushi_data_validator.validate("sushi.customer_revenue_lifetime", start_date, end_date) - plan = sushi_context.plan_builder( - restate_models=["sushi.customer_revenue_lifetime", "sushi.customer_revenue_by_day"], - start=start_date, - end="5 days ago", - ).build() - revenue_lifeteime_snapshot = sushi_context.get_snapshot( - "sushi.customer_revenue_lifetime", raise_if_missing=True - ) - revenue_by_day_snapshot = sushi_context.get_snapshot( - "sushi.customer_revenue_by_day", raise_if_missing=True - ) - assert sorted(plan.missing_intervals, key=lambda x: x.snapshot_id) == sorted( - [ - SnapshotIntervals( - snapshot_id=revenue_lifeteime_snapshot.snapshot_id, - intervals=[ - (to_timestamp(to_date("7 days ago")), to_timestamp(to_date("6 days ago"))), - (to_timestamp(to_date("6 days ago")), to_timestamp(to_date("5 days ago"))), - (to_timestamp(to_date("5 days ago")), to_timestamp(to_date("4 days ago"))), - (to_timestamp(to_date("4 days ago")), to_timestamp(to_date("3 days ago"))), - (to_timestamp(to_date("3 days ago")), to_timestamp(to_date("2 days ago"))), - (to_timestamp(to_date("2 days ago")), to_timestamp(to_date("1 days ago"))), - (to_timestamp(to_date("1 day ago")), to_timestamp(to_date("today"))), - ], - ), - SnapshotIntervals( - snapshot_id=revenue_by_day_snapshot.snapshot_id, - intervals=[ - (to_timestamp(to_date("7 days ago")), to_timestamp(to_date("6 days ago"))), - (to_timestamp(to_date("6 days ago")), to_timestamp(to_date("5 days ago"))), - ], - ), - ], - key=lambda x: x.snapshot_id, - ) - sushi_context.console = mocker.Mock(spec=Console) - sushi_context.apply(plan) - num_batch_calls = Counter( - [x[0][0] for x in sushi_context.console.update_snapshot_evaluation_progress.call_args_list] # type: ignore - ) - # Validate that we made 7 calls to the customer_revenue_lifetime snapshot and 1 call to the customer_revenue_by_day snapshot - assert num_batch_calls == { - sushi_context.get_snapshot("sushi.customer_revenue_lifetime", raise_if_missing=True): 7, - sushi_context.get_snapshot("sushi.customer_revenue_by_day", raise_if_missing=True): 1, - } - # Validate that the results are the same as before the restate - assert results == sushi_data_validator.validate( - "sushi.customer_revenue_lifetime", start_date, end_date - ) - - -def test_invalidating_environment(sushi_context: Context): - apply_to_environment(sushi_context, "dev") - start_environment = sushi_context.state_sync.get_environment("dev") - assert start_environment is not None - metadata = DuckDBMetadata.from_context(sushi_context) - start_schemas = set(metadata.schemas) - assert "sushi__dev" in start_schemas - sushi_context.invalidate_environment("dev") - invalidate_environment = sushi_context.state_sync.get_environment("dev") - assert invalidate_environment is not None - schemas_prior_to_janitor = set(metadata.schemas) - assert invalidate_environment.expiration_ts < start_environment.expiration_ts # type: ignore - assert start_schemas == schemas_prior_to_janitor - sushi_context._run_janitor() - schemas_after_janitor = set(metadata.schemas) - assert sushi_context.state_sync.get_environment("dev") is None - assert start_schemas - schemas_after_janitor == {"sushi__dev"} - - -def test_environment_suffix_target_table(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context( - "examples/sushi", config="environment_suffix_table_config" - ) - context.apply(plan) - metadata = DuckDBMetadata.from_context(context) - environments_schemas = {"sushi"} - internal_schemas = {"sqlmesh", "sqlmesh__sushi"} - starting_schemas = environments_schemas | internal_schemas - # Make sure no new schemas are created - assert set(metadata.schemas) - starting_schemas == {"raw"} - prod_views = {x for x in metadata.qualified_views if x.db in environments_schemas} - # Make sure that all models are present - assert len(prod_views) == 16 - apply_to_environment(context, "dev") - # Make sure no new schemas are created - assert set(metadata.schemas) - starting_schemas == {"raw"} - dev_views = { - x for x in metadata.qualified_views if x.db in environments_schemas and "__dev" in x.name - } - # Make sure that there is a view with `__dev` for each view that exists in prod - assert len(dev_views) == len(prod_views) - assert {x.name.replace("__dev", "") for x in dev_views} - {x.name for x in prod_views} == set() - context.invalidate_environment("dev") - context._run_janitor() - views_after_janitor = metadata.qualified_views - # Make sure that the number of views after the janitor is the same as when you subtract away dev views - assert len(views_after_janitor) == len( - {x.sql(dialect="duckdb") for x in views_after_janitor} - - {x.sql(dialect="duckdb") for x in dev_views} - ) - # Double check there are no dev views - assert len({x for x in views_after_janitor if "__dev" in x.name}) == 0 - # Make sure prod views were not removed - assert {x.sql(dialect="duckdb") for x in prod_views} - { - x.sql(dialect="duckdb") for x in views_after_janitor - } == set() - - -def test_environment_suffix_target_catalog(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: - monkeypatch.chdir(tmp_path) - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(catalogs={"main_warehouse": ":memory:"}), - environment_suffix_target=EnvironmentSuffixTarget.CATALOG, - ) - - assert config.default_connection - - models_dir = tmp_path / "models" - models_dir.mkdir() - - (models_dir / "model.sql").write_text(""" - MODEL ( - name example_schema.test_model, - kind FULL - ); - - SELECT '1' as a""") - - (models_dir / "fqn_model.sql").write_text(""" - MODEL ( - name memory.example_fqn_schema.test_model_fqn, - kind FULL - ); - - SELECT '1' as a""") - - ctx = Context(config=config, paths=tmp_path) - - metadata = DuckDBMetadata.from_context(ctx) - assert ctx.default_catalog == "main_warehouse" - assert metadata.catalogs == {"main_warehouse", "memory"} - - ctx.plan(auto_apply=True) - - # prod should go to the default catalog and not be overridden to a catalog called 'prod' - assert ( - ctx.engine_adapter.fetchone("select * from main_warehouse.example_schema.test_model")[0] # type: ignore - == "1" - ) - assert ( - ctx.engine_adapter.fetchone("select * from memory.example_fqn_schema.test_model_fqn")[0] # type: ignore - == "1" - ) - assert metadata.catalogs == {"main_warehouse", "memory"} - assert metadata.schemas_in_catalog("main_warehouse") == [ - "example_schema", - "sqlmesh__example_schema", - ] - assert metadata.schemas_in_catalog("memory") == [ - "example_fqn_schema", - "sqlmesh__example_fqn_schema", - ] - - # dev should be overridden to go to a catalogs called 'main_warehouse__dev' and 'memory__dev' - ctx.plan(environment="dev", include_unmodified=True, auto_apply=True) - assert ( - ctx.engine_adapter.fetchone("select * from main_warehouse__dev.example_schema.test_model")[ - 0 - ] # type: ignore - == "1" - ) - assert ( - ctx.engine_adapter.fetchone("select * from memory__dev.example_fqn_schema.test_model_fqn")[ - 0 - ] # type: ignore - == "1" - ) - assert metadata.catalogs == {"main_warehouse", "main_warehouse__dev", "memory", "memory__dev"} - - # schemas in dev envs should match prod and not have a suffix - assert metadata.schemas_in_catalog("main_warehouse") == [ - "example_schema", - "sqlmesh__example_schema", - ] - assert metadata.schemas_in_catalog("main_warehouse__dev") == ["example_schema"] - assert metadata.schemas_in_catalog("memory") == [ - "example_fqn_schema", - "sqlmesh__example_fqn_schema", - ] - assert metadata.schemas_in_catalog("memory__dev") == ["example_fqn_schema"] - - ctx.invalidate_environment("dev", sync=True) - - # dev catalogs cleaned up - assert metadata.catalogs == {"main_warehouse", "memory"} - - # prod catalogs still contain physical layer and views still work - assert metadata.schemas_in_catalog("main_warehouse") == [ - "example_schema", - "sqlmesh__example_schema", - ] - assert metadata.schemas_in_catalog("memory") == [ - "example_fqn_schema", - "sqlmesh__example_fqn_schema", - ] - - assert ( - ctx.engine_adapter.fetchone("select * from main_warehouse.example_schema.test_model")[0] # type: ignore - == "1" - ) - assert ( - ctx.engine_adapter.fetchone("select * from memory.example_fqn_schema.test_model_fqn")[0] # type: ignore - == "1" - ) - - -def test_environment_catalog_mapping(init_and_plan_context: t.Callable): - environments_schemas = {"raw", "sushi"} - - def get_prod_dev_views(metadata: DuckDBMetadata) -> t.Tuple[t.Set[exp.Table], t.Set[exp.Table]]: - views = metadata.qualified_views - prod_views = { - x for x in views if x.catalog == "prod_catalog" if x.db in environments_schemas - } - dev_views = {x for x in views if x.catalog == "dev_catalog" if x.db in environments_schemas} - return prod_views, dev_views - - def get_default_catalog_and_non_tables( - metadata: DuckDBMetadata, default_catalog: t.Optional[str] - ) -> t.Tuple[t.Set[exp.Table], t.Set[exp.Table]]: - tables = metadata.qualified_tables - user_default_tables = { - x for x in tables if x.catalog == default_catalog and x.db != "sqlmesh" - } - non_default_tables = {x for x in tables if x.catalog != default_catalog} - return user_default_tables, non_default_tables - - context, plan = init_and_plan_context( - "examples/sushi", config="environment_catalog_mapping_config" - ) - context.apply(plan) - metadata = DuckDBMetadata(context.engine_adapter) - state_metadata = DuckDBMetadata.from_context(context.state_sync.state_sync) - prod_views, dev_views = get_prod_dev_views(metadata) - ( - user_default_tables, - non_default_tables, - ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) - assert len(prod_views) == 16 - assert len(dev_views) == 0 - assert len(user_default_tables) == 15 - assert state_metadata.schemas == ["sqlmesh"] - assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( - { - "physical.sqlmesh._environments", - "physical.sqlmesh._intervals", - "physical.sqlmesh._snapshots", - "physical.sqlmesh._versions", - } - ) - apply_to_environment(context, "dev") - prod_views, dev_views = get_prod_dev_views(metadata) - ( - user_default_tables, - non_default_tables, - ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) - assert len(prod_views) == 16 - assert len(dev_views) == 16 - assert len(user_default_tables) == 16 - assert len(non_default_tables) == 0 - assert state_metadata.schemas == ["sqlmesh"] - assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( - { - "physical.sqlmesh._environments", - "physical.sqlmesh._intervals", - "physical.sqlmesh._snapshots", - "physical.sqlmesh._versions", - } - ) - apply_to_environment(context, "prodnot") - prod_views, dev_views = get_prod_dev_views(metadata) - ( - user_default_tables, - non_default_tables, - ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) - assert len(prod_views) == 16 - assert len(dev_views) == 32 - assert len(user_default_tables) == 16 - assert len(non_default_tables) == 0 - assert state_metadata.schemas == ["sqlmesh"] - assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( - { - "physical.sqlmesh._environments", - "physical.sqlmesh._intervals", - "physical.sqlmesh._snapshots", - "physical.sqlmesh._versions", - } - ) - context.invalidate_environment("dev") - context._run_janitor() - prod_views, dev_views = get_prod_dev_views(metadata) - ( - user_default_tables, - non_default_tables, - ) = get_default_catalog_and_non_tables(metadata, context.default_catalog) - assert len(prod_views) == 16 - assert len(dev_views) == 16 - assert len(user_default_tables) == 16 - assert len(non_default_tables) == 0 - assert state_metadata.schemas == ["sqlmesh"] - assert {x.sql() for x in state_metadata.qualified_tables}.issuperset( - { - "physical.sqlmesh._environments", - "physical.sqlmesh._intervals", - "physical.sqlmesh._snapshots", - "physical.sqlmesh._versions", - } - ) - - -@pytest.mark.parametrize( - "context_fixture", - ["sushi_context", "sushi_no_default_catalog"], -) -def test_unaligned_start_snapshots(context_fixture: Context, request): - context = request.getfixturevalue(context_fixture) - environment = "dev" - apply_to_environment(context, environment) - # Make breaking change to model upstream of a depends_on_self model - context.upsert_model("sushi.order_items", stamp="1") - # Apply the change starting at a date later then the beginning of the downstream depends_on_self model - plan = apply_to_environment( - context, - environment, - choice=SnapshotChangeCategory.BREAKING, - plan_start="2 days ago", - enable_preview=True, - ) - revenue_lifetime_snapshot = context.get_snapshot( - "sushi.customer_revenue_lifetime", raise_if_missing=True - ) - # Validate that the depends_on_self model is non-deployable - assert not plan.deployability_index.is_deployable(revenue_lifetime_snapshot) - - -class OldPythonModel(PythonModel): - kind: ModelKind = ViewKind() - - -def test_python_model_default_kind_change(init_and_plan_context: t.Callable): - """ - Around 2024-07-17 Python models had their default Kind changed from VIEW to FULL in order to - avoid some edge cases where the views might not get updated in certain situations. - - This test ensures that if a user had a Python `kind: VIEW` model stored in state, - it can still be loaded without error and just show as a breaking change from `kind: VIEW` - to `kind: FULL` - """ - - # note: we deliberately dont specify a Kind here to allow the defaults to be picked up - python_model_file = """import typing as t -import pandas as pd # noqa: TID253 -from sqlmesh import ExecutionContext, model - -@model( - "sushi.python_view_model", - columns={ - "id": "int", - } -) -def execute( - context: ExecutionContext, - **kwargs: t.Any, -) -> pd.DataFrame: - return pd.DataFrame([ - {"id": 1} - ]) -""" - - context: Context - context, _ = init_and_plan_context("examples/sushi") - - with open(context.path / "models" / "python_view_model.py", mode="w", encoding="utf8") as f: - f.write(python_model_file) - - # monkey-patch PythonModel to default to kind: View again - # and ViewKind to allow python models again - with ( - mock.patch.object(ViewKind, "supports_python_models", return_value=True), - mock.patch("sqlmesh.core.model.definition.PythonModel", OldPythonModel), - ): - context.load() - - # check the monkey-patching worked - model = context.get_model("sushi.python_view_model") - assert model.kind.name == ModelKindName.VIEW - assert model.source_type == "python" - - # apply plan - plan: Plan = context.plan(auto_apply=True) - - # check that run() still works even though we have a Python model with kind: View in the state - snapshot_ids = [s for s in plan.directly_modified if "python_view_model" in s.name] - snapshot_from_state = list(context.state_sync.get_snapshots(snapshot_ids).values())[0] - assert snapshot_from_state.model.kind.name == ModelKindName.VIEW - assert snapshot_from_state.model.source_type == "python" - context.run() - - # reload context to load model with new defaults - # this also shows the earlier monkey-patching is no longer in effect - context.load() - model = context.get_model("sushi.python_view_model") - assert model.kind.name == ModelKindName.FULL - assert model.source_type == "python" - - plan = context.plan( - categorizer_config=CategorizerConfig.all_full() - ) # the default categorizer_config doesnt auto-categorize python models - - assert plan.has_changes - assert not plan.indirectly_modified - - assert len(plan.directly_modified) == 1 - snapshot_id = list(plan.directly_modified)[0] - assert snapshot_id.name == '"memory"."sushi"."python_view_model"' - assert plan.modified_snapshots[snapshot_id].change_category == SnapshotChangeCategory.BREAKING - - context.apply(plan) - - df = context.engine_adapter.fetchdf("SELECT id FROM sushi.python_view_model") - assert df["id"].to_list() == [1] - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_restatement_of_full_model_with_start(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - restatement_plan = context.plan( - restate_models=["sushi.customers"], - start="2023-01-07", - auto_apply=True, - no_prompts=True, - ) - - sushi_customer_interval = restatement_plan.restatements[ - context.get_snapshot("sushi.customers").snapshot_id - ] - assert sushi_customer_interval == (to_timestamp("2023-01-01"), to_timestamp("2023-01-09")) - waiter_by_day_interval = restatement_plan.restatements[ - context.get_snapshot("sushi.waiter_as_customer_by_day").snapshot_id - ] - assert waiter_by_day_interval == (to_timestamp("2023-01-07"), to_timestamp("2023-01-08")) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_restatement_should_not_override_environment_statements(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - context.config.before_all = ["SELECT 'test_before_all';", *context.config.before_all] - context.load() - - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - - prod_env_statements = context.state_reader.get_environment_statements(c.PROD) - assert prod_env_statements[0].before_all[0] == "SELECT 'test_before_all';" - - context.plan( - restate_models=["sushi.waiter_revenue_by_day"], - start="2023-01-07", - auto_apply=True, - no_prompts=True, - ) - - prod_env_statements = context.state_reader.get_environment_statements(c.PROD) - assert prod_env_statements[0].before_all[0] == "SELECT 'test_before_all';" - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_restatement_shouldnt_backfill_beyond_prod_intervals(init_and_plan_context: t.Callable): - context, _ = init_and_plan_context("examples/sushi") - - model = context.get_model("sushi.top_waiters") - context.upsert_model(SqlModel.parse_obj({**model.dict(), "cron": "@hourly"})) - - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - context.run() - - with time_machine.travel("2023-01-09 02:00:00 UTC"): - # It's time to backfill the waiter_revenue_by_day model but it hasn't run yet - restatement_plan = context.plan( - restate_models=["sushi.waiter_revenue_by_day"], - no_prompts=True, - skip_tests=True, - ) - intervals_by_id = {i.snapshot_id: i for i in restatement_plan.missing_intervals} - # Make sure the intervals don't go beyond the prod intervals - assert intervals_by_id[context.get_snapshot("sushi.top_waiters").snapshot_id].intervals[-1][ - 1 - ] == to_timestamp("2023-01-08 15:00:00 UTC") - assert intervals_by_id[ - context.get_snapshot("sushi.waiter_revenue_by_day").snapshot_id - ].intervals[-1][1] == to_timestamp("2023-01-08 00:00:00 UTC") - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -@use_terminal_console -def test_audit_only_metadata_change(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - # Add a new audit - model = context.get_model("sushi.waiter_revenue_by_day") - audits = model.audits.copy() - audits.append(("number_of_rows", {"threshold": exp.Literal.number(1)})) - model = model.copy(update={"audits": audits}) - context.upsert_model(model) - - plan = context.plan_builder("prod", skip_tests=True).build() - assert len(plan.new_snapshots) == 2 - assert all(s.change_category.is_metadata for s in plan.new_snapshots) - assert not plan.missing_intervals - - with capture_output() as output: - context.apply(plan) - - assert "Auditing models" in output.stdout - assert model.name in output.stdout - - -def initial_add(context: Context, environment: str): - assert not context.state_reader.get_environment(environment) - - plan = context.plan(environment, start=start(context), create_from="nonexistent_env") - validate_plan_changes(plan, added={x.snapshot_id for x in context.snapshots.values()}) - - context.apply(plan) - validate_apply_basics(context, environment, plan.snapshots.values()) - - -def test_plan_production_environment_statements(tmp_path: Path): - model_a = """ - MODEL ( - name test_schema.a, - kind FULL, - ); - - @IF( - @runtime_stage IN ('evaluating', 'creating'), - INSERT INTO schema_names_for_prod (physical_schema_name) VALUES (@resolve_template('@{schema_name}')) - ); - - SELECT 1 AS account_id - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - for path, defn in {"a.sql": model_a}.items(): - with open(models_dir / path, "w") as f: - f.write(defn) - - before_all = [ - "CREATE TABLE IF NOT EXISTS schema_names_for_@this_env (physical_schema_name VARCHAR)", - "@IF(@runtime_stage = 'before_all', CREATE TABLE IF NOT EXISTS should_create AS SELECT @runtime_stage)", - ] - after_all = [ - "@IF(@this_env = 'prod', CREATE TABLE IF NOT EXISTS after_t AS SELECT @var_5)", - "@IF(@runtime_stage = 'before_all', CREATE TABLE IF NOT EXISTS not_create AS SELECT @runtime_stage)", - ] - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - before_all=before_all, - after_all=after_all, - variables={"var_5": 5}, - ) - ctx = Context(paths=[tmp_path], config=config) - ctx.plan(auto_apply=True, no_prompts=True) - - before_t = ctx.fetchdf("select * from schema_names_for_prod").to_dict() - assert before_t["physical_schema_name"][0] == "sqlmesh__test_schema" - - after_t = ctx.fetchdf("select * from after_t").to_dict() - assert after_t["5"][0] == 5 - - environment_statements = ctx.state_reader.get_environment_statements(c.PROD) - assert environment_statements[0].before_all == before_all - assert environment_statements[0].after_all == after_all - assert environment_statements[0].python_env.keys() == {"__sqlmesh__vars__"} - assert environment_statements[0].python_env["__sqlmesh__vars__"].payload == "{'var_5': 5}" - - should_create = ctx.fetchdf("select * from should_create").to_dict() - assert should_create["before_all"][0] == "before_all" - - with pytest.raises( - Exception, match=r"Catalog Error: Table with name not_create does not exist!" - ): - ctx.fetchdf("select * from not_create") - - -def test_environment_statements_error_handling(tmp_path: Path): - model_a = """ - MODEL ( - name test_schema.a, - kind FULL, - ); - - SELECT 1 AS account_id - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - for path, defn in {"a.sql": model_a}.items(): - with open(models_dir / path, "w") as f: - f.write(defn) - - before_all = [ - "CREATE TABLE identical_table (physical_schema_name VARCHAR)", - "CREATE TABLE identical_table (physical_schema_name VARCHAR)", - ] - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - before_all=before_all, - ) - ctx = Context(paths=[tmp_path], config=config) - - expected_error_message = re.escape( - """An error occurred during execution of the following 'before_all' statement: - -CREATE TABLE identical_table (physical_schema_name TEXT) - -Catalog Error: Table with name "identical_table" already exists!""" - ) - - with pytest.raises(SQLMeshError, match=expected_error_message): - ctx.plan(auto_apply=True, no_prompts=True) - - after_all = [ - "@bad_macro()", - ] - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - after_all=after_all, - ) - ctx = Context(paths=[tmp_path], config=config) - - expected_error_message = re.escape( - """An error occurred during rendering of the 'after_all' statements: - -Failed to resolve macros for - -@bad_macro() - -Macro 'bad_macro' does not exist.""" - ) - - with pytest.raises(SQLMeshError, match=expected_error_message): - ctx.plan(auto_apply=True, no_prompts=True) - - -def test_before_all_after_all_execution_order(tmp_path: Path, mocker: MockerFixture): - model = """ - MODEL ( - name test_schema.model_that_depends_on_before_all, - kind FULL, - ); - - SELECT id, value FROM before_all_created_table - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - with open(models_dir / "model.sql", "w") as f: - f.write(model) - - # before_all statement that creates a table that the above model depends on - before_all_statement = ( - "CREATE TABLE IF NOT EXISTS before_all_created_table AS SELECT 1 AS id, 'test' AS value" - ) - - # after_all that depends on the model - after_all_statement = "CREATE TABLE IF NOT EXISTS after_all_created_table AS SELECT id, value FROM test_schema.model_that_depends_on_before_all" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - before_all=[before_all_statement], - after_all=[after_all_statement], - ) - - execute_calls: t.List[str] = [] - - original_duckdb_execute = DuckDBEngineAdapter.execute - - def track_duckdb_execute(self, expression, **kwargs): - sql = expression if isinstance(expression, str) else expression.sql(dialect="duckdb") - state_tables = [ - "_snapshots", - "_environments", - "_versions", - "_intervals", - "_auto_restatements", - "_environment_statements", - ] - - # to ignore the state queries - if not any(table in sql.lower() for table in state_tables): - execute_calls.append(sql) - - return original_duckdb_execute(self, expression, **kwargs) - - ctx = Context(paths=[tmp_path], config=config) - - # the plan would fail if the execution order ever changes and before_all statements dont execute first - ctx.plan(auto_apply=True, no_prompts=True) - - mocker.patch.object(DuckDBEngineAdapter, "execute", track_duckdb_execute) - - # run with the patched execute - ctx.run("prod", start="2023-01-01", end="2023-01-02") - - # validate explicitly that the first execute is for the before_all - assert "before_all_created_table" in execute_calls[0] - - # and that the last is the sole after all that depends on the model - assert "after_all_created_table" in execute_calls[-1] - - -@time_machine.travel("2025-03-08 00:00:00 UTC") -def test_tz(init_and_plan_context): - context, _ = init_and_plan_context("examples/sushi") - - model = context.get_model("sushi.waiter_revenue_by_day") - context.upsert_model( - SqlModel.parse_obj( - {**model.dict(), "cron_tz": "America/Los_Angeles", "start": "2025-03-07"} - ) - ) - - def assert_intervals(plan, intervals): - assert ( - next( - intervals.intervals - for intervals in plan.missing_intervals - if intervals.snapshot_id.name == model.fqn - ) - == intervals - ) - - plan = context.plan_builder("prod", skip_tests=True).build() - - # we have missing intervals but not waiter_revenue_by_day because it's not midnight pacific yet - assert plan.missing_intervals - - with pytest.raises(StopIteration): - assert_intervals(plan, []) - - # now we're ready 8AM UTC == midnight PST - with time_machine.travel("2025-03-08 08:00:00 UTC"): - plan = context.plan_builder("prod", skip_tests=True).build() - assert_intervals(plan, [(to_timestamp("2025-03-07"), to_timestamp("2025-03-08"))]) - - with time_machine.travel("2025-03-09 07:00:00 UTC"): - plan = context.plan_builder("prod", skip_tests=True).build() - - assert_intervals( - plan, - [ - (to_timestamp("2025-03-07"), to_timestamp("2025-03-08")), - ], - ) - - with time_machine.travel("2025-03-09 08:00:00 UTC"): - plan = context.plan_builder("prod", skip_tests=True).build() - - assert_intervals( - plan, - [ - (to_timestamp("2025-03-07"), to_timestamp("2025-03-08")), - (to_timestamp("2025-03-08"), to_timestamp("2025-03-09")), - ], - ) - - context.apply(plan) - - plan = context.plan_builder("prod", skip_tests=True).build() - assert not plan.missing_intervals - - -def apply_to_environment( - context: Context, - environment: str, - choice: t.Optional[SnapshotChangeCategory] = None, - plan_validators: t.Optional[t.Iterable[t.Callable]] = None, - apply_validators: t.Optional[t.Iterable[t.Callable]] = None, - plan_start: t.Optional[TimeLike] = None, - allow_destructive_models: t.Optional[t.List[str]] = None, - enable_preview: bool = False, -): - plan_validators = plan_validators or [] - apply_validators = apply_validators or [] - - plan_builder = context.plan_builder( - environment, - start=plan_start or start(context) if environment != c.PROD else None, - forward_only=choice == SnapshotChangeCategory.FORWARD_ONLY, - include_unmodified=True, - allow_destructive_models=allow_destructive_models if allow_destructive_models else [], - enable_preview=enable_preview, - ) - if environment != c.PROD: - plan_builder.set_start(plan_start or start(context)) - - if choice: - if choice == SnapshotChangeCategory.FORWARD_ONLY: - # FORWARD_ONLY is deprecated, fallback to NON_BREAKING to keep the existing tests - choice = SnapshotChangeCategory.NON_BREAKING - plan_choice(plan_builder, choice) - for validator in plan_validators: - validator(context, plan_builder.build()) - - plan = plan_builder.build() - context.apply(plan) - - validate_apply_basics(context, environment, plan.snapshots.values(), plan.deployability_index) - for validator in apply_validators: - validator(context) - return plan - - -def change_data_type( - context: Context, model_name: str, old_type: DataType.Type, new_type: DataType.Type -) -> None: - model = context.get_model(model_name) - assert model is not None - - if isinstance(model, SqlModel): - query = model.query.copy() - data_types = query.find_all(DataType) - for data_type in data_types: - if data_type.this == old_type: - data_type.set("this", new_type) - context.upsert_model(model_name, query_=ParsableSql(sql=query.sql(dialect=model.dialect))) - elif model.columns_to_types_ is not None: - for k, v in model.columns_to_types_.items(): - if v.this == old_type: - model.columns_to_types_[k] = DataType.build(new_type) - context.upsert_model(model_name, columns=model.columns_to_types_) - - -def validate_plan_changes( - plan: Plan, - *, - added: t.Optional[t.Iterable[SnapshotId]] = None, - modified: t.Optional[t.Iterable[str]] = None, - removed: t.Optional[t.Iterable[SnapshotId]] = None, -) -> None: - added = added or [] - modified = modified or [] - removed = removed or [] - assert set(added) == plan.context_diff.added - assert set(modified) == set(plan.context_diff.modified_snapshots) - assert set(removed) == set(plan.context_diff.removed_snapshots) - - -def validate_versions_same( - model_names: t.List[str], - versions: t.Dict[str, str], - other_versions: t.Dict[str, str], -) -> None: - for name in model_names: - assert versions[name] == other_versions[name] - - -def validate_versions_different( - model_names: t.List[str], - versions: t.Dict[str, str], - other_versions: t.Dict[str, str], -) -> None: - for name in model_names: - assert versions[name] != other_versions[name] - - -def validate_apply_basics( - context: Context, - environment: str, - snapshots: t.Iterable[Snapshot], - deployability_index: t.Optional[DeployabilityIndex] = None, -) -> None: - validate_snapshots_in_state_sync(snapshots, context) - validate_state_sync_environment(snapshots, environment, context) - validate_tables(snapshots, context, deployability_index) - validate_environment_views(snapshots, environment, context, deployability_index) - - -def validate_snapshots_in_state_sync(snapshots: t.Iterable[Snapshot], context: Context) -> None: - snapshot_infos = map(to_snapshot_info, snapshots) - state_sync_table_infos = map( - to_snapshot_info, context.state_reader.get_snapshots(snapshots).values() - ) - assert set(snapshot_infos) == set(state_sync_table_infos) - - -def validate_state_sync_environment( - snapshots: t.Iterable[Snapshot], env: str, context: Context -) -> None: - environment = context.state_reader.get_environment(env) - assert environment - snapshot_infos = map(to_snapshot_info, snapshots) - environment_table_infos = map(to_snapshot_info, environment.snapshots) - assert set(snapshot_infos) == set(environment_table_infos) - - -def validate_tables( - snapshots: t.Iterable[Snapshot], - context: Context, - deployability_index: t.Optional[DeployabilityIndex] = None, -) -> None: - adapter = context.engine_adapter - deployability_index = deployability_index or DeployabilityIndex.all_deployable() - for snapshot in snapshots: - is_deployable = deployability_index.is_representative(snapshot) - if not snapshot.is_model or snapshot.is_external: - continue - table_should_exist = not snapshot.is_embedded - assert adapter.table_exists(snapshot.table_name(is_deployable)) == table_should_exist - if table_should_exist: - assert select_all(snapshot.table_name(is_deployable), adapter) - - -def validate_environment_views( - snapshots: t.Iterable[Snapshot], - environment: str, - context: Context, - deployability_index: t.Optional[DeployabilityIndex] = None, -) -> None: - adapter = context.engine_adapter - deployability_index = deployability_index or DeployabilityIndex.all_deployable() - for snapshot in snapshots: - is_deployable = deployability_index.is_representative(snapshot) - if not snapshot.is_model or snapshot.is_symbolic: - continue - view_name = snapshot.qualified_view_name.for_environment( - EnvironmentNamingInfo.from_environment_catalog_mapping( - context.config.environment_catalog_mapping, - name=environment, - suffix_target=context.config.environment_suffix_target, - ) - ) - - assert adapter.table_exists(view_name) - assert select_all(snapshot.table_name(is_deployable), adapter) == select_all( - view_name, adapter - ) - - -def select_all(table: str, adapter: EngineAdapter) -> t.Iterable: - return adapter.fetchall(f"select * from {table} order by 1") - - -def snapshots_to_versions(snapshots: t.Iterable[Snapshot]) -> t.Dict[str, str]: - return {snapshot.name: snapshot.version or "" for snapshot in snapshots} - - -def to_snapshot_info(snapshot: SnapshotInfoLike) -> SnapshotTableInfo: - return snapshot.table_info - - -def start(context: Context) -> TimeLike: - env = context.state_sync.get_environment("prod") - assert env - return env.start_at - - -def add_projection_to_model(model: SqlModel, literal: bool = True) -> SqlModel: - one_expr = exp.Literal.number(1).as_("one") if literal else exp.column("one") - kwargs = { - **model.dict(), - "query": model.query.select(one_expr), # type: ignore - } - return SqlModel.parse_obj(kwargs) - - -def test_plan_environment_statements_doesnt_cause_extra_diff(tmp_path: Path): - model_a = """ - MODEL ( - name test_schema.a, - kind FULL, - ); - - SELECT 1; - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - (models_dir / "a.sql").write_text(model_a) - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - before_all=["select 1 as before_all"], - after_all=["select 2 as after_all"], - ) - ctx = Context(paths=[tmp_path], config=config) - - # first plan - should apply changes - assert ctx.plan(auto_apply=True, no_prompts=True).has_changes - - # second plan - nothing has changed so should report no changes - assert not ctx.plan(auto_apply=True, no_prompts=True).has_changes - - -def test_janitor_cleanup_order(mocker: MockerFixture, tmp_path: Path): - def setup_scenario(): - models_dir = tmp_path / "models" - - if not models_dir.exists(): - models_dir.mkdir() - - model1_path = models_dir / "model1.sql" - - with open(model1_path, "w") as f: - f.write("MODEL(name test.model1, kind FULL); SELECT 1 AS col") - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - ) - ctx = Context(paths=[tmp_path], config=config) - - ctx.plan("dev", no_prompts=True, auto_apply=True) - - model1_snapshot = ctx.get_snapshot("test.model1") - - # Delete the model file to cause a snapshot expiration - model1_path.unlink() - - ctx.load() - - ctx.plan("dev", no_prompts=True, auto_apply=True) - - # Invalidate the environment to cause an environment cleanup - ctx.invalidate_environment("dev") - - try: - ctx._run_janitor(ignore_ttl=True) - except: - pass - - return ctx, model1_snapshot - - # Case 1: Assume that the snapshot cleanup yields an error, the snapshot records - # should still exist in the state sync so the next janitor can retry - mocker.patch( - "sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.cleanup", - side_effect=Exception("snapshot cleanup error"), - ) - ctx, model1_snapshot = setup_scenario() - - # - Check that the snapshot record exists in the state sync - state_snapshot = ctx.state_sync.state_sync.get_snapshots([model1_snapshot.snapshot_id]) - assert state_snapshot - - # - Run the janitor again, this time it should succeed - mocker.patch("sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.cleanup") - ctx._run_janitor(ignore_ttl=True) - - # - Check that the snapshot record does not exist in the state sync anymore - state_snapshot = ctx.state_sync.state_sync.get_snapshots([model1_snapshot.snapshot_id]) - assert not state_snapshot - - # Case 2: Assume that the view cleanup yields an error, the enviroment - # record should still exist - mocker.patch( - "sqlmesh.core.context.cleanup_expired_views", side_effect=Exception("view cleanup error") - ) - ctx, model1_snapshot = setup_scenario() - - views = ctx.fetchdf("FROM duckdb_views() SELECT * EXCLUDE(sql) WHERE NOT internal") - assert views.empty - - # - Check that the environment record exists in the state sync - assert ctx.state_sync.get_environment("dev") - - # - Run the janitor again, this time it should succeed - mocker.patch("sqlmesh.core.context.cleanup_expired_views") - ctx._run_janitor(ignore_ttl=True) - - # - Check that the environment record does not exist in the state sync anymore - assert not ctx.state_sync.get_environment("dev") - - -@use_terminal_console -def test_destroy(copy_to_temp_path): - # Testing project with two gateways to verify cleanup is performed across engines - paths = copy_to_temp_path("tests/fixtures/multi_virtual_layer") - path = Path(paths[0]) - first_db_path = str(path / "db_1.db") - second_db_path = str(path / "db_2.db") - - config = Config( - gateways={ - "first": GatewayConfig( - connection=DuckDBConnectionConfig(database=first_db_path), - variables={"overriden_var": "gateway_1"}, - ), - "second": GatewayConfig( - connection=DuckDBConnectionConfig(database=second_db_path), - variables={"overriden_var": "gateway_2"}, - ), - }, - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - model_naming=NameInferenceConfig(infer_names=True), - default_gateway="first", - gateway_managed_virtual_layer=True, - variables={"overriden_var": "global", "global_one": 88}, - ) - - context = Context(paths=paths, config=config) - plan = context.plan_builder().build() - assert len(plan.new_snapshots) == 4 - context.apply(plan) - - # Confirm cache exists - cache_path = Path(path) / ".cache" - assert cache_path.exists() - assert len(list(cache_path.iterdir())) > 0 - - model = context.get_model("db_1.first_schema.model_one") - - context.upsert_model( - model.copy( - update={ - "query_": ParsableSql( - sql=model.query.select("'c' AS extra").sql(dialect=model.dialect) - ) - } - ) - ) - plan = context.plan_builder().build() - context.apply(plan) - - state_environments = context.state_reader.get_environments() - state_snapshots = context.state_reader.get_snapshots(context.snapshots.values()) - - assert len(state_snapshots) == len(state_environments[0].snapshots) - - # Create dev environment with changed models - model = context.get_model("db_2.second_schema.model_one") - context.upsert_model( - model.copy( - update={ - "query_": ParsableSql( - sql=model.query.select("'d' AS extra").sql(dialect=model.dialect) - ) - } - ) - ) - model = context.get_model("first_schema.model_two") - context.upsert_model( - model.copy( - update={ - "query_": ParsableSql( - sql=model.query.select("'d2' AS col").sql(dialect=model.dialect) - ) - } - ) - ) - plan = context.plan_builder("dev").build() - context.apply(plan) - - dev_environment = context.state_sync.get_environment("dev") - assert dev_environment is not None - - state_environments = context.state_reader.get_environments() - state_snapshots = context.state_reader.get_snapshots(context.snapshots.values()) - assert ( - len(state_snapshots) - == len(state_environments[0].snapshots) - == len(state_environments[1].snapshots) - ) - - # The state tables at this point should be able to be retrieved - state_tables = { - "_environments", - "_snapshots", - "_intervals", - "_auto_restatements", - "_environment_statements", - "_intervals", - "_versions", - } - for table_name in state_tables: - context.fetchdf(f"SELECT * FROM db_1.sqlmesh.{table_name}") - - # The actual tables as well - context.engine_adapters["second"].fetchdf(f"SELECT * FROM db_2.second_schema.model_one") - context.engine_adapters["second"].fetchdf(f"SELECT * FROM db_2.second_schema.model_two") - context.fetchdf(f"SELECT * FROM db_1.first_schema.model_one") - context.fetchdf(f"SELECT * FROM db_1.first_schema.model_two") - - # Use the destroy command to remove all data objects and state - # Mock the console confirmation to automatically return True - with patch.object(context.console, "_confirm", return_value=True): - context._destroy() - - # Ensure all tables have been removed - for table_name in state_tables: - with pytest.raises( - Exception, match=f"Catalog Error: Table with name {table_name} does not exist!" - ): - context.fetchdf(f"SELECT * FROM db_1.sqlmesh.{table_name}") - - # Validate tables have been deleted as well - with pytest.raises( - Exception, match=r"Catalog Error: Table with name model_two does not exist!" - ): - context.fetchdf("SELECT * FROM db_1.first_schema.model_two") - with pytest.raises( - Exception, match=r"Catalog Error: Table with name model_one does not exist!" - ): - context.fetchdf("SELECT * FROM db_1.first_schema.model_one") - - with pytest.raises( - Exception, match=r"Catalog Error: Table with name model_two does not exist!" - ): - context.engine_adapters["second"].fetchdf("SELECT * FROM db_2.second_schema.model_two") - with pytest.raises( - Exception, match=r"Catalog Error: Table with name model_one does not exist!" - ): - context.engine_adapters["second"].fetchdf("SELECT * FROM db_2.second_schema.model_one") - - # Ensure the cache has been removed - assert not cache_path.exists() - - -@use_terminal_console -def test_audits_running_on_metadata_changes(tmp_path: Path): - def setup_senario(model_before: str, model_after: str): - models_dir = Path("models") - create_temp_file(tmp_path, models_dir / "test.sql", model_before) - - # Create first snapshot - context = Context(paths=tmp_path, config=Config()) - context.plan("prod", no_prompts=True, auto_apply=True) - - # Create second (metadata) snapshot - create_temp_file(tmp_path, models_dir / "test.sql", model_after) - context.load() - - with capture_output() as output: - with pytest.raises(PlanError): - context.plan("prod", no_prompts=True, auto_apply=True) - - assert 'Failed models\n\n "model"' in output.stdout - - return output - - # Ensure incorrect audits (bad data, incorrect definition etc) are evaluated immediately - output = setup_senario( - "MODEL (name model); SELECT NULL AS col", - "MODEL (name model, audits (not_null(columns=[col]))); SELECT NULL AS col", - ) - assert "'not_null' audit error: 1 row failed" in output.stdout - - output = setup_senario( - "MODEL (name model); SELECT NULL AS col", - "MODEL (name model, audits (not_null(columns=[this_col_does_not_exist]))); SELECT NULL AS col", - ) - assert ( - 'Binder Error: Referenced column "this_col_does_not_exist" not found in \nFROM clause!' - in output.stdout - ) - - -@pytest.mark.set_default_connection(disable=True) -def test_missing_connection_config(): - # This is testing the actual implementation of Config.get_connection - # To make writing tests easier, it's patched by the autouse fixture provide_sqlmesh_default_connection - # Case 1: No default_connection or gateways specified should raise a ConfigError - with pytest.raises(ConfigError): - ctx = Context(config=Config()) - - # Case 2: No connection specified in the gateway should raise a ConfigError - with pytest.raises(ConfigError): - ctx = Context(config=Config(gateways={"incorrect": GatewayConfig()})) - - # Case 3: Specifying a default_connection or connection in the gateway should work - ctx = Context(config=Config(default_connection=DuckDBConnectionConfig())) - ctx = Context( - config=Config(gateways={"default": GatewayConfig(connection=DuckDBConnectionConfig())}) - ) - - -@use_terminal_console -def test_render_path_instead_of_model(tmp_path: Path): - create_temp_file(tmp_path, Path("models/test.sql"), "MODEL (name test_model); SELECT 1 AS col") - ctx = Context(paths=tmp_path, config=Config()) - - # Case 1: Fail gracefully when the user is passing in a path instead of a model name - for test_model in ["models/test.sql", "models/test.py"]: - with pytest.raises( - SQLMeshError, - match="Resolving models by path is not supported, please pass in the model name instead.", - ): - ctx.render(test_model) - - # Case 2: Fail gracefully when the model name is not found - with pytest.raises(SQLMeshError, match="Cannot find model with name 'incorrect_model'"): - ctx.render("incorrect_model") - - # Case 3: Render the model successfully - assert ctx.render("test_model").sql() == 'SELECT 1 AS "col"' - - -@use_terminal_console -def test_plan_always_recreate_environment(tmp_path: Path): - def plan_with_output(ctx: Context, environment: str): - with patch.object(logger, "info") as mock_logger: - with capture_output() as output: - ctx.load() - ctx.plan(environment, no_prompts=True, auto_apply=True) - - # Facade logs info "Promoting environment {environment}" - assert mock_logger.call_args[0][1] == environment - - return output - - models_dir = tmp_path / "models" - - logger = logging.getLogger("sqlmesh.core.state_sync.db.facade") - - create_temp_file( - tmp_path, models_dir / "a.sql", "MODEL (name test.a, kind FULL); SELECT 1 AS col" - ) - - config = Config(plan=PlanConfig(always_recreate_environment=True)) - ctx = Context(paths=[tmp_path], config=config) - - # Case 1: Neither prod nor dev exists, so dev is initialized - output = plan_with_output(ctx, "dev") - - assert """`dev` environment will be initialized""" in output.stdout - - # Case 2: Prod does not exist, so dev is updated - create_temp_file( - tmp_path, models_dir / "a.sql", "MODEL (name test.a, kind FULL); SELECT 5 AS col" - ) - - output = plan_with_output(ctx, "dev") - assert "`dev` environment will be initialized" in output.stdout - - # Case 3: Prod is initialized, so plan comparisons moving forward should be against prod - output = plan_with_output(ctx, "prod") - assert "`prod` environment will be initialized" in output.stdout - - # Case 4: Dev is updated with a breaking change. Prod exists now so plan comparisons moving forward should be against prod - create_temp_file( - tmp_path, models_dir / "a.sql", "MODEL (name test.a, kind FULL); SELECT 10 AS col" - ) - ctx.load() - - plan = ctx.plan_builder("dev").build() - - assert ( - next(iter(plan.context_diff.snapshots.values())).change_category - == SnapshotChangeCategory.BREAKING - ) - - output = plan_with_output(ctx, "dev") - assert "New environment `dev` will be created from `prod`" in output.stdout - assert "Differences from the `prod` environment" in output.stdout - - # Case 5: Dev is updated with a metadata change, but comparison against prod shows both the previous and the current changes - # so it's still classified as a breaking change - create_temp_file( - tmp_path, - models_dir / "a.sql", - "MODEL (name test.a, kind FULL, owner 'test'); SELECT 10 AS col", - ) - ctx.load() - - plan = ctx.plan_builder("dev").build() - - assert ( - next(iter(plan.context_diff.snapshots.values())).change_category - == SnapshotChangeCategory.BREAKING - ) - - output = plan_with_output(ctx, "dev") - assert "New environment `dev` will be created from `prod`" in output.stdout - assert "Differences from the `prod` environment" in output.stdout - - stdout_rstrip = "\n".join([line.rstrip() for line in output.stdout.split("\n")]) - assert ( - """MODEL ( - name test.a, -+ owner test, - kind FULL - ) - SELECT -- 5 AS col -+ 10 AS col""" - in stdout_rstrip - ) - - # Case 6: Ensure that target environment and create_from environment are not the same - output = plan_with_output(ctx, "prod") - assert not "New environment `prod` will be created from `prod`" in output.stdout - - # Case 7: Check that we can still run Context::diff() against any environment - for environment in ["dev", "prod"]: - context_diff = ctx._context_diff(environment) - assert context_diff.environment == environment - - -@time_machine.travel("2020-01-01 00:00:00 UTC") -def test_scd_type_2_full_restatement_no_start_date(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - # Initial product catalog of 3 products - raw_products = d.parse(""" - MODEL ( - name memory.store.raw_products, - kind FULL - ); - - SELECT * FROM VALUES - (101, 'Laptop Pro', 1299.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP), - (102, 'Wireless Mouse', 49.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP), - (103, 'Office Chair', 199.99, 'Furniture', '2020-01-01 00:00:00'::TIMESTAMP) - AS t(product_id, product_name, price, category, last_updated); - """) - - # SCD Type 2 model for product history tracking - product_history = d.parse(""" - MODEL ( - name memory.store.product_history, - kind SCD_TYPE_2_BY_TIME ( - unique_key product_id, - updated_at_name last_updated, - disable_restatement false - ), - owner catalog_team, - cron '0 */6 * * *', - grain product_id, - description 'Product catalog change history' - ); - - SELECT - product_id::INT AS product_id, - product_name::TEXT AS product_name, - price::DECIMAL(10,2) AS price, - category::TEXT AS category, - last_updated AS last_updated - FROM - memory.store.raw_products; - """) - - raw_products_model = load_sql_based_model(raw_products) - product_history_model = load_sql_based_model(product_history) - context.upsert_model(raw_products_model) - context.upsert_model(product_history_model) - - # Initial plan and apply - plan = context.plan_builder("prod", skip_tests=True).build() - context.apply(plan) - - query = "SELECT product_id, product_name, price, category, last_updated, valid_from, valid_to FROM memory.store.product_history ORDER BY product_id, valid_from" - initial_data = context.engine_adapter.fetchdf(query) - - # Validate initial state of 3 products all active - assert len(initial_data) == 3 - assert initial_data["valid_to"].isna().all() - initial_product_names = set(initial_data["product_name"].tolist()) - assert initial_product_names == {"Laptop Pro", "Wireless Mouse", "Office Chair"} - - # Price update and category change - with time_machine.travel("2020-01-15 12:00:00 UTC"): - raw_products_v2 = d.parse(""" - MODEL ( - name memory.store.raw_products, - kind FULL - ); - - SELECT * FROM VALUES - (101, 'Laptop Pro', 1199.99, 'Electronics', '2020-01-15 00:00:00'::TIMESTAMP), - (102, 'Wireless Mouse', 49.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP), - (103, 'Ergonomic Office Chair', 229.99, 'Office Furniture', '2020-01-15 00:00:00'::TIMESTAMP) - AS t(product_id, product_name, price, category, last_updated); - """) - raw_products_v2_model = load_sql_based_model(raw_products_v2) - context.upsert_model(raw_products_v2_model) - context.plan( - auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() - ) - context.run() - - data_after_first_change = context.engine_adapter.fetchdf(query) - - # Should have 5 records (3 original closed, 2 new activε, 1 unchanged) - assert len(data_after_first_change) == 5 - - # Second change - with time_machine.travel("2020-02-01 10:00:00 UTC"): - raw_products_v3 = d.parse(""" - MODEL ( - name memory.store.raw_products, - kind FULL - ); - - SELECT * FROM VALUES - (101, 'Laptop Pro Max', 1399.99, 'Electronics', '2020-02-01 00:00:00'::TIMESTAMP), - (103, 'Ergonomic Office Chair', 229.99, 'Office Furniture', '2020-01-15 00:00:00'::TIMESTAMP), - (102, 'Wireless Mouse', 49.99, 'Electronics', '2020-01-01 00:00:00'::TIMESTAMP) - AS t(product_id, product_name, price, category, last_updated); - """) - raw_products_v3_model = load_sql_based_model(raw_products_v3) - context.upsert_model(raw_products_v3_model) - context.plan( - auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() - ) - context.run() - data_after_second_change = context.engine_adapter.fetchdf(query) - assert len(data_after_second_change) == 6 - - # Store the current state before full restatement - data_before_full_restatement = data_after_second_change.copy() - - # Perform full restatement (no start date provided) - with time_machine.travel("2020-02-01 15:00:00 UTC"): - plan = context.plan_builder( - "prod", skip_tests=True, restate_models=["memory.store.product_history"] - ).build() - context.apply(plan) - data_after_full_restatement = context.engine_adapter.fetchdf(query) - assert len(data_after_full_restatement) == 3 - - # Check that all currently active products before restatement are still active after restatement - active_before = data_before_full_restatement[ - data_before_full_restatement["valid_to"].isna() - ] - active_after = data_after_full_restatement - assert set(active_before["product_id"]) == set(active_after["product_id"]) - - expected_products = { - 101: { - "product_name": "Laptop Pro Max", - "price": 1399.99, - "category": "Electronics", - "last_updated": "2020-02-01", - }, - 102: { - "product_name": "Wireless Mouse", - "price": 49.99, - "category": "Electronics", - "last_updated": "2020-01-01", - }, - 103: { - "product_name": "Ergonomic Office Chair", - "price": 229.99, - "category": "Office Furniture", - "last_updated": "2020-01-15", - }, - } - for _, row in data_after_full_restatement.iterrows(): - pid = row["product_id"] - assert pid in expected_products - expected = expected_products[pid] - assert row["product_name"] == expected["product_name"] - assert float(row["price"]) == expected["price"] - assert row["category"] == expected["category"] - - # valid_from should be the epoch, valid_to should be NaT - assert str(row["valid_from"]) == "1970-01-01 00:00:00" - assert pd.isna(row["valid_to"]) - - -def test_plan_evaluator_correlation_id(tmp_path: Path): - def _correlation_id_in_sqls(correlation_id: CorrelationId, mock_logger): - sqls = [call[0][0] for call in mock_logger.call_args_list] - return any(f"/* {correlation_id} */" in sql for sql in sqls) - - ctx = Context(paths=[tmp_path], config=Config()) - - # Case: Ensure that the correlation id (plan_id) is included in the SQL for each plan - for i in range(2): - create_temp_file( - tmp_path, - Path("models", "test.sql"), - f"MODEL (name test.a, kind FULL); SELECT {i} AS col", - ) - - with mock.patch("sqlmesh.core.engine_adapter.base.EngineAdapter._log_sql") as mock_logger: - ctx.load() - plan = ctx.plan(auto_apply=True, no_prompts=True) - - correlation_id = CorrelationId.from_plan_id(plan.plan_id) - assert str(correlation_id) == f"SQLMESH_PLAN: {plan.plan_id}" - - assert _correlation_id_in_sqls(correlation_id, mock_logger) - - -@time_machine.travel("2023-01-08 15:00:00 UTC") -def test_scd_type_2_regular_run_with_offset(init_and_plan_context: t.Callable): - context, plan = init_and_plan_context("examples/sushi") - context.apply(plan) - - raw_employee_status = d.parse(""" - MODEL ( - name memory.hr_system.raw_employee_status, - kind FULL - ); - - SELECT - 1001 AS employee_id, - 'engineering' AS department, - 'EMEA' AS region, - '2023-01-08 15:00:00 UTC' AS last_modified; - """) - - employee_history = d.parse(""" - MODEL ( - name memory.hr_system.employee_history, - kind SCD_TYPE_2_BY_TIME ( - unique_key employee_id, - updated_at_name last_modified, - disable_restatement false - ), - owner hr_analytics, - cron '0 7 * * *', - grain employee_id, - description 'Historical tracking of employee status changes' - ); - - SELECT - employee_id::INT AS employee_id, - department::TEXT AS department, - region::TEXT AS region, - last_modified AS last_modified - FROM - memory.hr_system.raw_employee_status; - """) - - raw_employee_status_model = load_sql_based_model(raw_employee_status) - employee_history_model = load_sql_based_model(employee_history) - context.upsert_model(raw_employee_status_model) - context.upsert_model(employee_history_model) - - # Initial plan and apply - plan = context.plan_builder("prod", skip_tests=True).build() - context.apply(plan) - - query = "SELECT employee_id, department, region, valid_from, valid_to FROM memory.hr_system.employee_history ORDER BY employee_id, valid_from" - initial_data = context.engine_adapter.fetchdf(query) - - assert len(initial_data) == 1 - assert initial_data["valid_to"].isna().all() - assert initial_data["department"].tolist() == ["engineering"] - assert initial_data["region"].tolist() == ["EMEA"] - - # Apply a future plan with source changes a few hours before the cron time of the SCD Type 2 model BUT on the same day - with time_machine.travel("2023-01-09 00:10:00 UTC"): - raw_employee_status_v2 = d.parse(""" - MODEL ( - name memory.hr_system.raw_employee_status, - kind FULL - ); - - SELECT - 1001 AS employee_id, - 'engineering' AS department, - 'AMER' AS region, - '2023-01-09 00:10:00 UTC' AS last_modified; - """) - raw_employee_status_v2_model = load_sql_based_model(raw_employee_status_v2) - context.upsert_model(raw_employee_status_v2_model) - context.plan( - auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() - ) - - # The 7th hour of the day the run is kicked off for the SCD Type 2 model - with time_machine.travel("2023-01-09 07:00:01 UTC"): - context.run() - data_after_change = context.engine_adapter.fetchdf(query) - - # Validate the SCD2 records for employee 1001 - assert len(data_after_change) == 2 - assert data_after_change.iloc[0]["employee_id"] == 1001 - assert data_after_change.iloc[0]["department"] == "engineering" - assert data_after_change.iloc[0]["region"] == "EMEA" - assert str(data_after_change.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" - assert str(data_after_change.iloc[0]["valid_to"]) == "2023-01-09 00:10:00" - assert data_after_change.iloc[1]["employee_id"] == 1001 - assert data_after_change.iloc[1]["department"] == "engineering" - assert data_after_change.iloc[1]["region"] == "AMER" - assert str(data_after_change.iloc[1]["valid_from"]) == "2023-01-09 00:10:00" - assert pd.isna(data_after_change.iloc[1]["valid_to"]) - - # Update source model again a bit later on the same day - raw_employee_status_v2 = d.parse(""" - MODEL ( - name memory.hr_system.raw_employee_status, - kind FULL - ); - - SELECT - 1001 AS employee_id, - 'sales' AS department, - 'ANZ' AS region, - '2023-01-09 07:26:00 UTC' AS last_modified; - """) - raw_employee_status_v2_model = load_sql_based_model(raw_employee_status_v2) - context.upsert_model(raw_employee_status_v2_model) - context.plan( - auto_apply=True, no_prompts=True, categorizer_config=CategorizerConfig.all_full() - ) - - # A day later the run is kicked off for the SCD Type 2 model again - with time_machine.travel("2023-01-10 07:00:00 UTC"): - context.run() - data_after_change = context.engine_adapter.fetchdf(query) - - # Validate the SCD2 history for employee 1001 after second change with the historical records intact - assert len(data_after_change) == 3 - assert data_after_change.iloc[0]["employee_id"] == 1001 - assert data_after_change.iloc[0]["department"] == "engineering" - assert data_after_change.iloc[0]["region"] == "EMEA" - assert str(data_after_change.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" - assert str(data_after_change.iloc[0]["valid_to"]) == "2023-01-09 00:10:00" - assert data_after_change.iloc[1]["employee_id"] == 1001 - assert data_after_change.iloc[1]["department"] == "engineering" - assert data_after_change.iloc[1]["region"] == "AMER" - assert str(data_after_change.iloc[1]["valid_from"]) == "2023-01-09 00:10:00" - assert str(data_after_change.iloc[1]["valid_to"]) == "2023-01-09 07:26:00" - assert data_after_change.iloc[2]["employee_id"] == 1001 - assert data_after_change.iloc[2]["department"] == "sales" - assert data_after_change.iloc[2]["region"] == "ANZ" - assert str(data_after_change.iloc[2]["valid_from"]) == "2023-01-09 07:26:00" - assert pd.isna(data_after_change.iloc[2]["valid_to"]) - - # Now test restatement works (full restatement support currently) - with time_machine.travel("2023-01-10 07:38:00 UTC"): - plan = context.plan_builder( - "prod", - skip_tests=True, - restate_models=["memory.hr_system.employee_history"], - start="2023-01-09 00:10:00", - ).build() - context.apply(plan) - restated_data = context.engine_adapter.fetchdf(query) - - # Validate the SCD2 history after restatement has been wiped bar one - assert len(restated_data) == 1 - assert restated_data.iloc[0]["employee_id"] == 1001 - assert restated_data.iloc[0]["department"] == "sales" - assert restated_data.iloc[0]["region"] == "ANZ" - assert str(restated_data.iloc[0]["valid_from"]) == "1970-01-01 00:00:00" - assert pd.isna(restated_data.iloc[0]["valid_to"]) - - -def test_engine_adapters_multi_repo_all_gateways_gathered(copy_to_temp_path): - paths = copy_to_temp_path("examples/multi") - repo_1_path = paths[0] / "repo_1" - repo_2_path = paths[0] / "repo_2" - - # Add an extra gateway to repo_2's config - repo_2_config_path = repo_2_path / "config.yaml" - config_content = repo_2_config_path.read_text() - - modified_config = config_content.replace( - "default_gateway: local", - dedent(""" - extra: - connection: - type: duckdb - database: extra.duckdb - - default_gateway: local - """), - ) - - repo_2_config_path.write_text(modified_config) - - # Create context with both repos but using the repo_1 path first - context = Context( - paths=(repo_1_path, repo_2_path), - gateway="memory", - ) - - # Verify all gateways from both repos are present - gathered_gateways = context.engine_adapters.keys() - expected_gateways = {"local", "memory", "extra"} - assert gathered_gateways == expected_gateways - - -def test_physical_table_naming_strategy_table_only(copy_to_temp_path: t.Callable): - sushi_context = Context( - paths=copy_to_temp_path("examples/sushi"), - config="table_only_naming_config", - ) - - assert sushi_context.config.physical_table_naming_convention == TableNamingConvention.TABLE_ONLY - sushi_context.plan(auto_apply=True) - - adapter = sushi_context.engine_adapter - - snapshot_tables = [ - dict(catalog=str(r[0]), schema=str(r[1]), table=str(r[2])) - for r in adapter.fetchall( - "select table_catalog, table_schema, table_name from information_schema.tables where table_type='BASE TABLE'" - ) - ] - - assert all([not t["table"].startswith("sushi") for t in snapshot_tables]) - - prod_env = sushi_context.state_reader.get_environment("prod") - assert prod_env - - prod_env_snapshots = sushi_context.state_reader.get_snapshots(prod_env.snapshots) - - assert all( - s.table_naming_convention == TableNamingConvention.TABLE_ONLY - for s in prod_env_snapshots.values() - ) - - -def test_physical_table_naming_strategy_hash_md5(copy_to_temp_path: t.Callable): - sushi_context = Context( - paths=copy_to_temp_path("examples/sushi"), - config="hash_md5_naming_config", - ) - - assert sushi_context.config.physical_table_naming_convention == TableNamingConvention.HASH_MD5 - sushi_context.plan(auto_apply=True) - - adapter = sushi_context.engine_adapter - - snapshot_tables = [ - dict(catalog=str(r[0]), schema=str(r[1]), table=str(r[2])) - for r in adapter.fetchall( - "select table_catalog, table_schema, table_name from information_schema.tables where table_type='BASE TABLE'" - ) - ] - - assert all([not t["table"].startswith("sushi") for t in snapshot_tables]) - assert all([t["table"].startswith("sqlmesh_md5") for t in snapshot_tables]) - - prod_env = sushi_context.state_reader.get_environment("prod") - assert prod_env - - prod_env_snapshots = sushi_context.state_reader.get_snapshots(prod_env.snapshots) - - assert all( - s.table_naming_convention == TableNamingConvention.HASH_MD5 - for s in prod_env_snapshots.values() - ) - - -@pytest.mark.slow -def test_default_audits_applied_in_plan(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir(exist_ok=True) - - # Create a model with data that will pass the audits - create_temp_file( - tmp_path, - models_dir / "orders.sql", - dedent(""" - MODEL ( - name test.orders, - kind FULL - ); - - SELECT - 1 AS order_id, - 'customer_1' AS customer_id, - 100.50 AS amount, - '2024-01-01'::DATE AS order_date - UNION ALL - SELECT - 2 AS order_id, - 'customer_2' AS customer_id, - 200.75 AS amount, - '2024-01-02'::DATE AS order_date - """), - ) - - config = Config( - model_defaults=ModelDefaultsConfig( - dialect="duckdb", - audits=[ - "not_null(columns := [order_id, customer_id])", - "unique_values(columns := [order_id])", - ], - ) - ) - - context = Context(paths=tmp_path, config=config) - - # Create and apply plan, here audits should pass - plan = context.plan("prod", no_prompts=True) - context.apply(plan) - - # Verify model has the default audits - model = context.get_model("test.orders") - assert len(model.audits) == 2 - - audit_names = [audit[0] for audit in model.audits] - assert "not_null" in audit_names - assert "unique_values" in audit_names - - # Verify audit arguments are preserved - for audit_name, audit_args in model.audits: - if audit_name == "not_null": - assert "columns" in audit_args - columns = [col.name for col in audit_args["columns"].expressions] - assert "order_id" in columns - assert "customer_id" in columns - elif audit_name == "unique_values": - assert "columns" in audit_args - columns = [col.name for col in audit_args["columns"].expressions] - assert "order_id" in columns - - -@pytest.mark.slow -def test_default_audits_fail_on_bad_data(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir(exist_ok=True) - - # Create a model with data that violates NOT NULL constraint - create_temp_file( - tmp_path, - models_dir / "bad_orders.sql", - dedent(""" - MODEL ( - name test.bad_orders, - kind FULL - ); - - SELECT - 1 AS order_id, - NULL AS customer_id, -- This violates NOT NULL - 100.50 AS amount, - '2024-01-01'::DATE AS order_date - UNION ALL - SELECT - 2 AS order_id, - 'customer_2' AS customer_id, - 200.75 AS amount, - '2024-01-02'::DATE AS order_date - """), - ) - - config = Config( - model_defaults=ModelDefaultsConfig( - dialect="duckdb", audits=["not_null(columns := [customer_id])"] - ) - ) - - context = Context(paths=tmp_path, config=config) - - # Plan should fail due to audit failure - with pytest.raises(PlanError): - context.plan("prod", no_prompts=True, auto_apply=True) - - -@pytest.mark.slow -def test_default_audits_with_model_specific_audits(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir(exist_ok=True) - audits_dir = tmp_path / "audits" - audits_dir.mkdir(exist_ok=True) - - create_temp_file( - tmp_path, - audits_dir / "range_check.sql", - dedent(""" - AUDIT ( - name range_check - ); - - SELECT * FROM @this_model - WHERE @column < @min_value OR @column > @max_value - """), - ) - - # Create a model with its own audits in addition to defaults - create_temp_file( - tmp_path, - models_dir / "products.sql", - dedent(""" - MODEL ( - name test.products, - kind FULL, - audits ( - range_check(column := price, min_value := 0, max_value := 10000) - ) - ); - - SELECT - 1 AS product_id, - 'Widget' AS product_name, - 99.99 AS price - UNION ALL - SELECT - 2 AS product_id, - 'Gadget' AS product_name, - 149.99 AS price - """), - ) - - config = Config( - model_defaults=ModelDefaultsConfig( - dialect="duckdb", - audits=[ - "not_null(columns := [product_id, product_name])", - "unique_values(columns := [product_id])", - ], - ) - ) - - context = Context(paths=tmp_path, config=config) - - # Create and apply plan - plan = context.plan("prod", no_prompts=True) - context.apply(plan) - - # Verify model has both default and model-specific audits - model = context.get_model("test.products") - assert len(model.audits) == 3 - - audit_names = [audit[0] for audit in model.audits] - assert "not_null" in audit_names - assert "unique_values" in audit_names - assert "range_check" in audit_names - - # Verify audit execution order, default audits first then model-specific - assert model.audits[0][0] == "not_null" - assert model.audits[1][0] == "unique_values" - assert model.audits[2][0] == "range_check" - - -@pytest.mark.slow -def test_default_audits_with_custom_audit_definitions(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir(exist_ok=True) - audits_dir = tmp_path / "audits" - audits_dir.mkdir(exist_ok=True) - - # Create custom audit definition - create_temp_file( - tmp_path, - audits_dir / "positive_amount.sql", - dedent(""" - AUDIT ( - name positive_amount - ); - - SELECT * FROM @this_model - WHERE @column <= 0 - """), - ) - - # Create a model - create_temp_file( - tmp_path, - models_dir / "transactions.sql", - dedent(""" - MODEL ( - name test.transactions, - kind FULL - ); - - SELECT - 1 AS transaction_id, - 'TXN001' AS transaction_code, - 250.00 AS amount, - '2024-01-01'::DATE AS transaction_date - UNION ALL - SELECT - 2 AS transaction_id, - 'TXN002' AS transaction_code, - 150.00 AS amount, - '2024-01-02'::DATE AS transaction_date - """), - ) - - config = Config( - model_defaults=ModelDefaultsConfig( - dialect="duckdb", - audits=[ - "not_null(columns := [transaction_id, transaction_code])", - "unique_values(columns := [transaction_id])", - "positive_amount(column := amount)", - ], - ) - ) - - context = Context(paths=tmp_path, config=config) - - # Create and apply plan - plan = context.plan("prod", no_prompts=True) - context.apply(plan) - - # Verify model has all default audits including custom - model = context.get_model("test.transactions") - assert len(model.audits) == 3 - - audit_names = [audit[0] for audit in model.audits] - assert "not_null" in audit_names - assert "unique_values" in audit_names - assert "positive_amount" in audit_names - - # Verify custom audit arguments - for audit_name, audit_args in model.audits: - if audit_name == "positive_amount": - assert "column" in audit_args - assert audit_args["column"].name == "amount" - - -def test_incremental_by_time_model_ignore_destructive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - @start_ds as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 2 as id, - 3 as new_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - assert updated_df["new_column"].dropna().tolist() == [3] - - with time_machine.travel("2023-01-11 00:00:00 UTC"): - updated_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 2 as id, - CAST(4 AS STRING) as new_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(updated_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True, run=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 3 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - # The destructive change was ignored but this change is coercable and therefore we still return ints - assert updated_df["new_column"].dropna().tolist() == [3, 4] - - with time_machine.travel("2023-01-12 00:00:00 UTC"): - updated_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 2 as id, - CAST(5 AS STRING) as new_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(updated_model) - - context = Context(paths=[tmp_path], config=config) - # Make the change compatible since that means we will attempt and alter now that is considered additive - context.engine_adapter.SCHEMA_DIFFER_KWARGS["compatible_types"] = { - exp.DataType.build("INT"): {exp.DataType.build("STRING")} - } - context.plan("prod", auto_apply=True, no_prompts=True, run=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 4 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - # The change is now reflected since an additive alter could be performed - assert updated_df["new_column"].dropna().tolist() == ["3", "4", "5"] - - context.close() - - -def test_incremental_by_time_model_ignore_additive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - 'other' as other_column, - @start_ds as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column to the source table - initial_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'other' as other_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("ALTER TABLE source_table ADD COLUMN new_column INT") - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is removed since destructive is allowed - assert "name" not in updated_df.columns - # new_column is not added since additive is ignored - assert "new_column" not in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not still in table since destructive was applied - assert "name" not in updated_df.columns - # new_column is still not added since additive is ignored - assert "new_column" not in updated_df.columns - - with time_machine.travel("2023-01-11 00:00:00 UTC"): - updated_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - CAST(1 AS STRING) as id, - 'other' as other_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(updated_model) - - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.SCHEMA_DIFFER_KWARGS["compatible_types"] = { - exp.DataType.build("INT"): {exp.DataType.build("STRING")} - } - context.plan("prod", auto_apply=True, no_prompts=True, run=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 3 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not still in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is still not added since additive is ignored - assert "new_column" not in updated_df.columns - # The additive change was ignored since we set the change as compatible therefore - # instead of getting strings in the result we still return ints - assert updated_df["id"].tolist() == [1, 1, 1] - - with time_machine.travel("2023-01-12 00:00:00 UTC"): - updated_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change allow, - on_additive_change allow - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - CAST(1 AS STRING) as id, - 'other' as other_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(updated_model) - - context = Context(paths=[tmp_path], config=config) - # Make the change compatible since that means we will attempt and alter now that is considered additive - context.engine_adapter.SCHEMA_DIFFER_KWARGS["compatible_types"] = { - exp.DataType.build("INT"): {exp.DataType.build("STRING")} - } - context.plan("prod", auto_apply=True, no_prompts=True, run=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 4 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not still in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is now added since it is additive is now allowed - assert "new_column" in updated_df.columns - # The change is now reflected since an additive alter could be performed - assert updated_df["id"].dropna().tolist() == ["1", "1", "1", "1"] - - context.close() - - -def test_incremental_by_unique_key_model_ignore_destructive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind INCREMENTAL_BY_UNIQUE_KEY ( - unique_key id, - forward_only true, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - @start_ds as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_UNIQUE_KEY ( - unique_key id, - forward_only true, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 2 as id, - 3 as new_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - - context.close() - - -def test_incremental_by_unique_key_model_ignore_additive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind INCREMENTAL_BY_UNIQUE_KEY ( - unique_key id, - forward_only true, - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - @start_ds as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_UNIQUE_KEY ( - unique_key id, - forward_only true, - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 2 as id, - 3 as new_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still not in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - - context.close() - - -def test_incremental_unmanaged_model_ignore_destructive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind INCREMENTAL_UNMANAGED( - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - @start_ds as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_UNMANAGED( - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 2 as id, - 3 as new_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - - context.close() - - -def test_incremental_unmanaged_model_ignore_additive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind INCREMENTAL_UNMANAGED( - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - @start_ds as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_UNMANAGED( - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 2 as id, - 3 as new_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not still in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - - context.close() - - -def test_scd_type_2_by_time_ignore_destructive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind SCD_TYPE_2_BY_TIME ( - unique_key id, - updated_at_name ds, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - @start_dt as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind SCD_TYPE_2_BY_TIME ( - unique_key id, - updated_at_name ds, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 3 as new_column, - @start_dt as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - - context.close() - - -def test_scd_type_2_by_time_ignore_additive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind SCD_TYPE_2_BY_TIME ( - unique_key id, - updated_at_name ds, - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - @start_dt as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind SCD_TYPE_2_BY_TIME ( - unique_key id, - updated_at_name ds, - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 3 as new_column, - @start_dt as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not still in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not still in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - - context.close() - - -def test_scd_type_2_by_column_ignore_destructive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind SCD_TYPE_2_BY_COLUMN ( - unique_key id, - columns [name], - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - @start_ds as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind SCD_TYPE_2_BY_COLUMN ( - unique_key id, - columns [new_column], - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 3 as new_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - - context.close() - - -def test_scd_type_2_by_column_ignore_additive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind SCD_TYPE_2_BY_COLUMN ( - unique_key id, - columns [stable], - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - 'stable' as stable, - @start_ds as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind SCD_TYPE_2_BY_COLUMN ( - unique_key id, - columns [stable], - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'stable2' as stable, - 3 as new_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not still in table since destructive was ignored - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not still in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - - context.close() - - -def test_incremental_partition_ignore_destructive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind INCREMENTAL_BY_PARTITION ( - on_destructive_change ignore - ), - partitioned_by [ds], - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - @start_ds as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_PARTITION ( - on_destructive_change ignore - ), - partitioned_by [ds], - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 3 as new_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - - context.close() - - -def test_incremental_partition_ignore_additive_change(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind INCREMENTAL_BY_PARTITION ( - on_destructive_change allow, - on_additive_change ignore - ), - partitioned_by [ds], - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 'test_name' as name, - @start_ds as ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("CREATE TABLE source_table (source_id INT)") - context.engine_adapter.execute("INSERT INTO source_table VALUES (1)") - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_PARTITION ( - on_destructive_change allow, - on_additive_change ignore - ), - partitioned_by [ds], - start '2023-01-01', - cron '@daily' - ); - - SELECT - *, - 1 as id, - 3 as new_column, - @start_ds as ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True) - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - - assert len(updated_df) == 1 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not still in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.run() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "source_id" in initial_df.columns - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not still in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - - context.close() - - -def test_incremental_by_time_model_ignore_destructive_change_unit_test(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - test_dir = tmp_path / "tests" - test_dir.mkdir() - test_filepath = test_dir / "test_test_model.yaml" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - id, - name, - ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - initial_test = f""" - -test_test_model: - model: test_model - inputs: - source_table: - - id: 1 - name: 'test_name' - ds: '2025-01-01' - outputs: - query: - - id: 1 - name: 'test_name' - ds: '2025-01-01' -""" - - # Write initial test - test_filepath.write_text(initial_test) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute( - "CREATE TABLE source_table (id INT, name STRING, new_column INT, ds STRING)" - ) - context.engine_adapter.execute( - "INSERT INTO source_table VALUES (1, 'test_name', NULL, '2023-01-01')" - ) - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - test_result = context.test() - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - assert len(test_result.successes) == 1 - assert test_result.testsRun == len(test_result.successes) - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - id, - new_column, - ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - updated_test = f""" - - test_test_model: - model: test_model - inputs: - source_table: - - id: 1 - new_column: 3 - ds: '2025-01-01' - outputs: - query: - - id: 1 - new_column: 3 - ds: '2025-01-01' - """ - - # Write initial test - test_filepath.write_text(updated_test) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - test_result = context.test() - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 1 - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - assert len(test_result.successes) == 1 - assert test_result.testsRun == len(test_result.successes) - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("INSERT INTO source_table VALUES (2, NULL, 3, '2023-01-09')") - context.run() - test_result = context.test() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still in table since destructive was ignored - assert "name" in updated_df.columns - # new_column is added since it is additive and allowed - assert "new_column" in updated_df.columns - assert len(test_result.successes) == 1 - assert test_result.testsRun == len(test_result.successes) - - context.close() - - -def test_incremental_by_time_model_ignore_additive_change_unit_test(tmp_path: Path): - models_dir = tmp_path / "models" - models_dir.mkdir() - data_dir = tmp_path / "data" - data_dir.mkdir() - data_filepath = data_dir / "test.duckdb" - test_dir = tmp_path / "tests" - test_dir.mkdir() - test_filepath = test_dir / "test_test_model.yaml" - - config = Config( - model_defaults=ModelDefaultsConfig(dialect="duckdb"), - default_connection=DuckDBConnectionConfig(database=str(data_filepath)), - ) - - # Initial model with 3 columns - initial_model = f""" - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - id, - name, - ds - FROM - source_table; - """ - - # Write initial model - (models_dir / "test_model.sql").write_text(initial_model) - - initial_test = f""" - -test_test_model: - model: test_model - inputs: - source_table: - - id: 1 - name: 'test_name' - ds: '2025-01-01' - outputs: - query: - - id: 1 - name: 'test_name' - ds: '2025-01-01' -""" - - # Write initial test - test_filepath.write_text(initial_test) - - with time_machine.travel("2023-01-08 00:00:00 UTC"): - # Create context and apply initial model - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute( - "CREATE TABLE source_table (id INT, name STRING, new_column INT, ds STRING)" - ) - context.engine_adapter.execute( - "INSERT INTO source_table VALUES (1, 'test_name', NULL, '2023-01-01')" - ) - - # Apply initial plan and load data - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - test_result = context.test() - - # Verify initial data was loaded - initial_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(initial_df) == 1 - assert "id" in initial_df.columns - assert "name" in initial_df.columns - assert "ds" in initial_df.columns - assert len(test_result.successes) == 1 - assert test_result.testsRun == len(test_result.successes) - - context.close() - - # remove `name` column and add new column - initial_model = """ - MODEL ( - name test_model, - kind INCREMENTAL_BY_TIME_RANGE ( - time_column ds, - forward_only true, - on_destructive_change allow, - on_additive_change ignore - ), - start '2023-01-01', - cron '@daily' - ); - - SELECT - id, - new_column, - ds - FROM - source_table; - """ - (models_dir / "test_model.sql").write_text(initial_model) - - # `new_column` is in the output since unit tests are based on the model definition that currently - # exists and doesn't take into account the historical changes to the table. Therefore `new_column` is - # not actually in the table but it is represented in the test - updated_test = f""" - test_test_model: - model: test_model - inputs: - source_table: - - id: 1 - new_column: 3 - ds: '2025-01-01' - outputs: - query: - - id: 1 - new_column: 3 - ds: '2025-01-01' - """ - - # Write initial test - test_filepath.write_text(updated_test) - - context = Context(paths=[tmp_path], config=config) - context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) - test_result = context.test() - - # Verify data loading continued to work - # The existing data should still be there and new data should be loaded - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 1 - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is not in table since destructive was ignored - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - assert len(test_result.successes) == 1 - assert test_result.testsRun == len(test_result.successes) - - context.close() - - with time_machine.travel("2023-01-10 00:00:00 UTC"): - context = Context(paths=[tmp_path], config=config) - context.engine_adapter.execute("INSERT INTO source_table VALUES (2, NULL, 3, '2023-01-09')") - context.run() - test_result = context.test() - updated_df = context.fetchdf('SELECT * FROM "default"."test_model"') - assert len(updated_df) == 2 - assert "id" in updated_df.columns - assert "ds" in updated_df.columns - # name is still not in table since destructive was allowed - assert "name" not in updated_df.columns - # new_column is not added since it is additive and ignored - assert "new_column" not in updated_df.columns - assert len(test_result.successes) == 1 - assert test_result.testsRun == len(test_result.successes) - - context.close() - - -def test_restatement_plan_interval_external_visibility(tmp_path: Path): - """ - Scenario: - - `prod` environment exists, models A <- B - - `dev` environment created, models A <- B(dev) <- C (dev) - - Restatement plan is triggered against `prod` for model A - - During restatement, a new dev environment `dev_2` is created with a new version of B(dev_2) - - Outcome: - - At no point are the prod_intervals considered "missing" from state for A - - The intervals for B(dev) and C(dev) are cleared - - The intervals for B(dev_2) are also cleared even though the environment didnt exist at the time the plan was started, - because they are based on the data from a partially restated version of A - """ - - models_dir = tmp_path / "models" - models_dir.mkdir() - - lock_file_path = tmp_path / "test.lock" # python model blocks while this file is present - - evaluation_lock_file_path = ( - tmp_path / "evaluation.lock" - ) # python model creates this file if it's in the wait loop and deletes it once done - - # Note: to make execution block so we can test stuff, we use a Python model that blocks until it no longer detects the presence of a file - (models_dir / "model_a.py").write_text(f""" -from sqlmesh.core.model import model -from sqlmesh.core.macros import MacroEvaluator - -@model( - "test.model_a", - is_sql=True, - kind="FULL" -) -def entrypoint(evaluator: MacroEvaluator) -> str: - from pathlib import Path - import time - - if evaluator.runtime_stage == 'evaluating': - while True: - if Path("{str(lock_file_path)}").exists(): - Path("{str(evaluation_lock_file_path)}").touch() - print("lock exists; sleeping") - time.sleep(2) - else: - Path("{str(evaluation_lock_file_path)}").unlink(missing_ok=True) - break - - return "select 'model_a' as m" -""") - - (models_dir / "model_b.sql").write_text(""" - MODEL ( - name test.model_b, - kind FULL - ); - - select a.m as m, 'model_b' as mb from test.model_a as a - """) - - config = Config( - gateways={ - "": GatewayConfig( - connection=DuckDBConnectionConfig(database=str(tmp_path / "db.db")), - state_connection=DuckDBConnectionConfig(database=str(tmp_path / "state.db")), - ) - }, - model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01"), - ) - ctx = Context(paths=[tmp_path], config=config) - - ctx.plan(environment="prod", auto_apply=True) - - assert len(ctx.snapshots) == 2 - assert all(s.intervals for s in ctx.snapshots.values()) - - prod_model_a_snapshot_id = ctx.snapshots['"db"."test"."model_a"'].snapshot_id - prod_model_b_snapshot_id = ctx.snapshots['"db"."test"."model_b"'].snapshot_id - - # dev models - # new version of B - (models_dir / "model_b.sql").write_text(""" - MODEL ( - name test.model_b, - kind FULL - ); - - select a.m as m, 'model_b' as mb, 'dev' as dev_version from test.model_a as a - """) - - # add C - (models_dir / "model_c.sql").write_text(""" - MODEL ( - name test.model_c, - kind FULL - ); - - select b.*, 'model_c' as mc from test.model_b as b - """) - - ctx.load() - ctx.plan(environment="dev", auto_apply=True) - - dev_model_b_snapshot_id = ctx.snapshots['"db"."test"."model_b"'].snapshot_id - dev_model_c_snapshot_id = ctx.snapshots['"db"."test"."model_c"'].snapshot_id - - assert dev_model_b_snapshot_id != prod_model_b_snapshot_id - - # now, we restate A in prod but touch the lockfile so it hangs during evaluation - # we also have to do it in its own thread due to the hang - lock_file_path.touch() - - def _run_restatement_plan(tmp_path: Path, config: Config, q: queue.Queue): - q.put("thread_started") - - # give this thread its own Context object to prevent segfaulting the Python interpreter - restatement_ctx = Context(paths=[tmp_path], config=config) - - # dev2 not present before the restatement plan starts - assert restatement_ctx.state_sync.get_environment("dev2") is None - - q.put("plan_started") - plan = restatement_ctx.plan( - environment="prod", restate_models=['"db"."test"."model_a"'], auto_apply=True - ) - q.put("plan_completed") - - # dev2 was created during the restatement plan - assert restatement_ctx.state_sync.get_environment("dev2") is not None - - return plan - - executor = ThreadPoolExecutor() - q: queue.Queue = queue.Queue() - restatement_plan_future = executor.submit(_run_restatement_plan, tmp_path, config, q) - assert q.get() == "thread_started" - - try: - if e := restatement_plan_future.exception(timeout=1): - # abort early if the plan thread threw an exception - raise e - except TimeoutError: - # that's ok, we dont actually expect the plan to have finished in 1 second - pass - - # while that restatement is running, we can simulate another process and check that it sees no empty intervals - assert q.get() == "plan_started" - - # dont check for potentially missing intervals until the plan is in the evaluation loop - attempts = 0 - while not evaluation_lock_file_path.exists(): - time.sleep(2) - attempts += 1 - if attempts > 10: - raise ValueError("Gave up waiting for evaluation loop") - - ctx.clear_caches() # get rid of the file cache so that data is re-fetched from state - prod_models_from_state = ctx.state_sync.get_snapshots( - snapshot_ids=[prod_model_a_snapshot_id, prod_model_b_snapshot_id] - ) - - # prod intervals should be present still - assert all(m.intervals for m in prod_models_from_state.values()) - - # so should dev intervals since prod restatement is still running - assert all(m.intervals for m in ctx.snapshots.values()) - - # now, lets create a new dev environment "dev2", while the prod restatement plan is still running, - # that changes model_b while still being based on the original version of model_a - (models_dir / "model_b.sql").write_text(""" - MODEL ( - name test.model_b, - kind FULL - ); - - select a.m as m, 'model_b' as mb, 'dev2' as dev_version from test.model_a as a - """) - ctx.load() - ctx.plan(environment="dev2", auto_apply=True) - - dev2_model_b_snapshot_id = ctx.snapshots['"db"."test"."model_b"'].snapshot_id - assert dev2_model_b_snapshot_id != dev_model_b_snapshot_id - assert dev2_model_b_snapshot_id != prod_model_b_snapshot_id - - # as at this point, everything still has intervals - ctx.clear_caches() - assert all( - s.intervals - for s in ctx.state_sync.get_snapshots( - snapshot_ids=[ - prod_model_a_snapshot_id, - prod_model_b_snapshot_id, - dev_model_b_snapshot_id, - dev_model_c_snapshot_id, - dev2_model_b_snapshot_id, - ] - ).values() - ) - - # now, we finally let that restatement plan complete - # first, verify it's still blocked where it should be - assert not restatement_plan_future.done() - - lock_file_path.unlink() # remove lock file, plan should be able to proceed now - - if e := restatement_plan_future.exception(): # blocks until future complete - raise e - - assert restatement_plan_future.result() - assert q.get() == "plan_completed" - - ctx.clear_caches() - - # check that intervals in prod are present - assert all( - s.intervals - for s in ctx.state_sync.get_snapshots( - snapshot_ids=[ - prod_model_a_snapshot_id, - prod_model_b_snapshot_id, - ] - ).values() - ) - - # check that intervals in dev have been cleared, including the dev2 env that - # was created after the restatement plan started - assert all( - not s.intervals - for s in ctx.state_sync.get_snapshots( - snapshot_ids=[ - dev_model_b_snapshot_id, - dev_model_c_snapshot_id, - dev2_model_b_snapshot_id, - ] - ).values() - ) - - executor.shutdown() - - -def test_restatement_plan_detects_prod_deployment_during_restatement(tmp_path: Path): - """ - Scenario: - - `prod` environment exists, model A - - `dev` environment created, model A(dev) - - Restatement plan is triggered against `prod` for model A - - During restatement, someone else deploys A(dev) to prod, replacing the model that is currently being restated. - - Outcome: - - The deployment plan for dev -> prod should succeed in deploying the new version of A - - The prod restatement plan should fail with a ConflictingPlanError and warn about the model that got updated while undergoing restatement - - The new version of A should have no intervals cleared. The user needs to rerun the restatement if the intervals should still be cleared - """ - orig_console = get_console() - console = CaptureTerminalConsole() - set_console(console) - - models_dir = tmp_path / "models" - models_dir.mkdir() - - lock_file_path = tmp_path / "test.lock" # python model blocks while this file is present - - evaluation_lock_file_path = ( - tmp_path / "evaluation.lock" - ) # python model creates this file if it's in the wait loop and deletes it once done - - # Note: to make execution block so we can test stuff, we use a Python model that blocks until it no longer detects the presence of a file - (models_dir / "model_a.py").write_text(f""" -from sqlmesh.core.model import model -from sqlmesh.core.macros import MacroEvaluator - -@model( - "test.model_a", - is_sql=True, - kind="FULL" -) -def entrypoint(evaluator: MacroEvaluator) -> str: - from pathlib import Path - import time - - if evaluator.runtime_stage == 'evaluating': - while True: - if Path("{str(lock_file_path)}").exists(): - Path("{str(evaluation_lock_file_path)}").touch() - print("lock exists; sleeping") - time.sleep(2) - else: - Path("{str(evaluation_lock_file_path)}").unlink(missing_ok=True) - break - - return "select 'model_a' as m" -""") - - config = Config( - gateways={ - "": GatewayConfig( - connection=DuckDBConnectionConfig(database=str(tmp_path / "db.db")), - state_connection=DuckDBConnectionConfig(database=str(tmp_path / "state.db")), - ) - }, - model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01"), - ) - ctx = Context(paths=[tmp_path], config=config) - - # create prod - ctx.plan(environment="prod", auto_apply=True) - original_prod = ctx.state_sync.get_environment("prod") - assert original_prod - - # update model_a for dev - (models_dir / "model_a.py").unlink() - (models_dir / "model_a.sql").write_text(""" - MODEL ( - name test.model_a, - kind FULL - ); - - select 1 as changed - """) - - # create dev - ctx.load() - plan = ctx.plan(environment="dev", auto_apply=True) - assert len(plan.modified_snapshots) == 1 - new_model_a_snapshot_id = list(plan.modified_snapshots)[0] - - # now, trigger a prod restatement plan in a different thread and block it to simulate a long restatement - thread_console = None - - def _run_restatement_plan(tmp_path: Path, config: Config, q: queue.Queue): - nonlocal thread_console - q.put("thread_started") - - # Give this thread its own markdown console to avoid Rich LiveError - thread_console = MarkdownConsole() - set_console(thread_console) - - # give this thread its own Context object to prevent segfaulting the Python interpreter - restatement_ctx = Context(paths=[tmp_path], config=config) - - # ensure dev is present before the restatement plan starts - assert restatement_ctx.state_sync.get_environment("dev") is not None - - q.put("plan_started") - expected_error = None - try: - restatement_ctx.plan( - environment="prod", restate_models=['"db"."test"."model_a"'], auto_apply=True - ) - except ConflictingPlanError as e: - expected_error = e - - q.put("plan_completed") - return expected_error - - executor = ThreadPoolExecutor() - q: queue.Queue = queue.Queue() - lock_file_path.touch() - - restatement_plan_future = executor.submit(_run_restatement_plan, tmp_path, config, q) - restatement_plan_future.add_done_callback(lambda _: executor.shutdown()) - - assert q.get() == "thread_started" - - try: - if e := restatement_plan_future.exception(timeout=1): - # abort early if the plan thread threw an exception - raise e - except TimeoutError: - # that's ok, we dont actually expect the plan to have finished in 1 second - pass - - assert q.get() == "plan_started" - - # ok, now the prod restatement plan is running, let's deploy dev to prod - ctx.plan(environment="prod", auto_apply=True) - - new_prod = ctx.state_sync.get_environment("prod") - assert new_prod - assert new_prod.plan_id != original_prod.plan_id - assert new_prod.previous_plan_id == original_prod.plan_id - - # new prod is deployed but restatement plan is still running - assert not restatement_plan_future.done() - - # allow restatement plan to complete - lock_file_path.unlink() - - plan_error = restatement_plan_future.result() - assert isinstance(plan_error, ConflictingPlanError) - assert "please re-apply your plan" in repr(plan_error).lower() - - output = " ".join(re.split("\\s+", thread_console.captured_output, flags=re.UNICODE)) # type: ignore - assert ( - f"The following models had new versions deployed while data was being restated: └── test.model_a" - in output - ) - - # check that no intervals have been cleared from the model_a currently in prod - model_a = ctx.state_sync.get_snapshots(snapshot_ids=[new_model_a_snapshot_id])[ - new_model_a_snapshot_id - ] - assert isinstance(model_a.node, SqlModel) - assert model_a.node.render_query_or_raise().sql() == 'SELECT 1 AS "changed"' - assert len(model_a.intervals) - - set_console(orig_console) - - -def test_seed_model_metadata_update_does_not_trigger_backfill(tmp_path: Path): - """ - Scenario: - - Create a seed model; perform initial population - - Modify the model with a metadata-only change and trigger a plan - - Outcome: - - The seed model is modified (metadata-only) but this should NOT trigger backfill - - There should be no missing_intervals on the plan to backfill - """ - - models_path = tmp_path / "models" - seeds_path = tmp_path / "seeds" - models_path.mkdir() - seeds_path.mkdir() - - seed_model_path = models_path / "seed.sql" - seed_path = seeds_path / "seed_data.csv" - - seed_path.write_text("\n".join(["id,name", "1,test"])) - - seed_model_path.write_text(""" - MODEL ( - name test.source_data, - kind SEED ( - path '../seeds/seed_data.csv' - ) - ); - """) - - config = Config( - gateways={"": GatewayConfig(connection=DuckDBConnectionConfig())}, - model_defaults=ModelDefaultsConfig(dialect="duckdb", start="2024-01-01"), - ) - ctx = Context(paths=tmp_path, config=config) - - plan = ctx.plan(auto_apply=True) - - original_seed_snapshot = ctx.snapshots['"memory"."test"."source_data"'] - assert plan.directly_modified == {original_seed_snapshot.snapshot_id} - assert plan.metadata_updated == set() - assert plan.missing_intervals - - # prove data loaded - assert ctx.engine_adapter.fetchall("select id, name from memory.test.source_data") == [ - (1, "test") - ] - - # prove no diff - ctx.load() - plan = ctx.plan(auto_apply=True) - assert not plan.has_changes - assert not plan.missing_intervals - - # make metadata-only change - seed_model_path.write_text(""" - MODEL ( - name test.source_data, - kind SEED ( - path '../seeds/seed_data.csv' - ), - description 'updated by test' - ); - """) - - ctx.load() - plan = ctx.plan(auto_apply=True) - assert plan.has_changes - - new_seed_snapshot = ctx.snapshots['"memory"."test"."source_data"'] - assert ( - new_seed_snapshot.version == original_seed_snapshot.version - ) # should be using the same physical table - assert ( - new_seed_snapshot.snapshot_id != original_seed_snapshot.snapshot_id - ) # but still be different due to the metadata change - assert plan.directly_modified == set() - assert plan.metadata_updated == {new_seed_snapshot.snapshot_id} - - # there should be no missing intervals to backfill since all we did is update a description - assert not plan.missing_intervals - - # there should still be no diff or missing intervals in 3 days time - assert new_seed_snapshot.model.interval_unit.is_day - with time_machine.travel(timedelta(days=3)): - ctx.clear_caches() - ctx.load() - plan = ctx.plan(auto_apply=True) - assert not plan.has_changes - assert not plan.missing_intervals - - # change seed data - seed_path.write_text("\n".join(["id,name", "1,test", "2,updated"])) - - # new plan - NOW we should backfill because data changed - ctx.load() - plan = ctx.plan(auto_apply=True) - assert plan.has_changes - - updated_seed_snapshot = ctx.snapshots['"memory"."test"."source_data"'] - - assert ( - updated_seed_snapshot.snapshot_id - != new_seed_snapshot.snapshot_id - != original_seed_snapshot.snapshot_id - ) - assert not updated_seed_snapshot.forward_only - assert plan.directly_modified == {updated_seed_snapshot.snapshot_id} - assert plan.metadata_updated == set() - assert plan.missing_intervals - - # prove backfilled data loaded - assert ctx.engine_adapter.fetchall("select id, name from memory.test.source_data") == [ - (1, "test"), - (2, "updated"), - ] From e3e57d5ddd2a26a702076c270ad38e3b80939d4c Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 23 Sep 2025 18:24:59 -0700 Subject: [PATCH 0904/1056] Chore: Remove dead code --- tests/core/integration/test_forward_only.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/tests/core/integration/test_forward_only.py b/tests/core/integration/test_forward_only.py index 4d61915305..2dddf18efd 100644 --- a/tests/core/integration/test_forward_only.py +++ b/tests/core/integration/test_forward_only.py @@ -5,7 +5,6 @@ import pandas as pd # noqa: TID253 import pytest import time_machine -from pytest_mock.plugin import MockerFixture from sqlmesh.core import dialect as d from sqlmesh.core.context import Context @@ -15,7 +14,7 @@ SqlModel, load_sql_based_model, ) -from sqlmesh.core.plan import PlanBuilder, SnapshotIntervals +from sqlmesh.core.plan import SnapshotIntervals from sqlmesh.core.snapshot import ( SnapshotChangeCategory, ) @@ -25,18 +24,6 @@ pytestmark = pytest.mark.slow -@pytest.fixture(autouse=True) -def mock_choices(mocker: MockerFixture): - mocker.patch("sqlmesh.core.console.TerminalConsole._get_snapshot_change_category") - mocker.patch("sqlmesh.core.console.TerminalConsole._prompt_backfill") - - -def plan_choice(plan_builder: PlanBuilder, choice: SnapshotChangeCategory) -> None: - for snapshot in plan_builder.build().snapshots.values(): - if not snapshot.version: - plan_builder.set_choice(snapshot, choice) - - @time_machine.travel("2023-01-08 15:00:00 UTC") @pytest.mark.parametrize( "context_fixture", From aa515c543723077ec647d71da3c3e8725e9fbf18 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:17:00 +0300 Subject: [PATCH 0905/1056] Fix: warn on dbt variable definition failure instead of raising (#5427) --- sqlmesh/dbt/context.py | 5 ++++- tests/dbt/test_config.py | 2 ++ tests/fixtures/dbt/sushi_test/dbt_project.yml | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/context.py b/sqlmesh/dbt/context.py index a56a6ca4d6..67e70d3c79 100644 --- a/sqlmesh/dbt/context.py +++ b/sqlmesh/dbt/context.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import typing as t from dataclasses import dataclass, field, replace from pathlib import Path @@ -28,6 +29,8 @@ from sqlmesh.dbt.seed import SeedConfig from sqlmesh.dbt.source import SourceConfig +logger = logging.getLogger(__name__) + @dataclass class DbtContext: @@ -125,7 +128,7 @@ def _var(name: str, default: t.Optional[t.Any] = None) -> t.Any: try: rendered_variables[k] = _render_var(v) except Exception as ex: - raise ConfigError(f"Failed to render variable '{k}', value '{v}': {ex}") from ex + logger.warning(f"Failed to render variable '{k}', value '{v}': {ex}") self.variables = rendered_variables diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 0e96024aa1..c484b8e126 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -352,6 +352,7 @@ def test_variables(assert_exp_eq, sushi_test_project): "some_var": ["foo", "bar"], }, "some_var": "should be overridden in customers package", + "invalid_var": "{{ ref('ref_without_closing_paren' }}", } expected_customer_variables = { "some_var": ["foo", "bar"], # Takes precedence over the root project variable @@ -370,6 +371,7 @@ def test_variables(assert_exp_eq, sushi_test_project): {"name": "item1", "value": 1}, {"name": "item2", "value": 2}, ], + "invalid_var": "{{ ref('ref_without_closing_paren' }}", } assert sushi_test_project.packages["sushi"].variables == expected_sushi_variables assert sushi_test_project.packages["customers"].variables == expected_customer_variables diff --git a/tests/fixtures/dbt/sushi_test/dbt_project.yml b/tests/fixtures/dbt/sushi_test/dbt_project.yml index 920dea7216..0b5f6b0f83 100644 --- a/tests/fixtures/dbt/sushi_test/dbt_project.yml +++ b/tests/fixtures/dbt/sushi_test/dbt_project.yml @@ -66,6 +66,9 @@ vars: - name: 'item2' value: 2 + # Despite this being an invalid variable definition, dbt doesn't mind if it's unused + invalid_var: "{{ ref('ref_without_closing_paren' }}" + on-run-start: - 'CREATE TABLE IF NOT EXISTS analytic_stats (physical_table VARCHAR, evaluation_time VARCHAR);' From 0bda998a2138959afd7a0083772900203af2514f Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 24 Sep 2025 12:00:10 -0700 Subject: [PATCH 0906/1056] Fix: Include root package in search candidates when resolving dbt macros (#5349) --- sqlmesh/dbt/adapter.py | 13 +++++++++---- tests/dbt/test_adapter.py | 1 + tests/fixtures/dbt/sushi_test/macros/distinct.sql | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/dbt/sushi_test/macros/distinct.sql diff --git a/sqlmesh/dbt/adapter.py b/sqlmesh/dbt/adapter.py index 12e38e4749..7f7c7eb4fb 100644 --- a/sqlmesh/dbt/adapter.py +++ b/sqlmesh/dbt/adapter.py @@ -139,10 +139,15 @@ def _relevance(package_name_pair: t.Tuple[t.Optional[str], str]) -> t.Tuple[int, return name_score, package_score jinja_env = self.jinja_macros.build_environment(**self.jinja_globals).globals - packages_to_check: t.List[t.Optional[str]] = [ - macro_namespace, - *(k for k in jinja_env if k.startswith("dbt")), - ] + + packages_to_check: t.List[t.Optional[str]] = [None] + if macro_namespace is not None: + if macro_namespace in jinja_env: + packages_to_check = [self.jinja_macros.root_package_name, macro_namespace] + + # Add dbt packages as fallback + packages_to_check.extend(k for k in jinja_env if k.startswith("dbt")) + candidates = {} for macro_package in packages_to_check: macros = jinja_env.get(macro_package, {}) if macro_package else jinja_env diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index 5617d8c5c3..381401ce73 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -242,6 +242,7 @@ def test_adapter_dispatch(sushi_test_project: Project, runtime_renderer: t.Calla assert renderer("{{ adapter.dispatch('current_engine', 'customers')() }}") == "duckdb" assert renderer("{{ adapter.dispatch('current_timestamp')() }}") == "now()" assert renderer("{{ adapter.dispatch('current_timestamp', 'dbt')() }}") == "now()" + assert renderer("{{ adapter.dispatch('select_distinct', 'customers')() }}") == "distinct" # test with keyword arguments assert ( diff --git a/tests/fixtures/dbt/sushi_test/macros/distinct.sql b/tests/fixtures/dbt/sushi_test/macros/distinct.sql new file mode 100644 index 0000000000..1b339a9349 --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/macros/distinct.sql @@ -0,0 +1 @@ +{% macro default__select_distinct() %}distinct{% endmacro %} From 368c5ddf84e15fa9786745891ce5ed9ff4ebe59a Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Thu, 25 Sep 2025 09:07:49 +1200 Subject: [PATCH 0907/1056] Feat(sqlmesh_dbt): Select based on dbt name, not sqlmesh name (#5420) --- sqlmesh/core/context.py | 6 +- sqlmesh/core/selector.py | 77 ++++++++++++- sqlmesh_dbt/operations.py | 5 +- tests/core/test_selector.py | 20 ++-- tests/dbt/cli/test_list.py | 8 +- tests/dbt/cli/test_operations.py | 10 +- tests/dbt/cli/test_run.py | 2 +- tests/dbt/cli/test_selectors.py | 192 +++++++++++++++++++++++++++++++ tests/dbt/conftest.py | 3 +- 9 files changed, 293 insertions(+), 30 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 437fbd6edd..e3feb1e14b 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -93,7 +93,7 @@ from sqlmesh.core.reference import ReferenceGraph from sqlmesh.core.scheduler import Scheduler, CompletionStatus from sqlmesh.core.schema_loader import create_external_models_file -from sqlmesh.core.selector import Selector +from sqlmesh.core.selector import Selector, NativeSelector from sqlmesh.core.snapshot import ( DeployabilityIndex, Snapshot, @@ -368,6 +368,7 @@ def __init__( load: bool = True, users: t.Optional[t.List[User]] = None, config_loader_kwargs: t.Optional[t.Dict[str, t.Any]] = None, + selector: t.Optional[t.Type[Selector]] = None, ): self.configs = ( config @@ -390,6 +391,7 @@ def __init__( self._engine_adapter: t.Optional[EngineAdapter] = None self._linters: t.Dict[str, Linter] = {} self._loaded: bool = False + self._selector_cls = selector or NativeSelector self.path, self.config = t.cast(t.Tuple[Path, C], next(iter(self.configs.items()))) @@ -2893,7 +2895,7 @@ def _new_state_sync(self) -> StateSync: def _new_selector( self, models: t.Optional[UniqueKeyDict[str, Model]] = None, dag: t.Optional[DAG[str]] = None ) -> Selector: - return Selector( + return self._selector_cls( self.state_reader, models=models or self._models, context_path=self.path, diff --git a/sqlmesh/core/selector.py b/sqlmesh/core/selector.py index c44065bdc0..1484d06cee 100644 --- a/sqlmesh/core/selector.py +++ b/sqlmesh/core/selector.py @@ -3,6 +3,8 @@ import fnmatch import typing as t from pathlib import Path +from itertools import zip_longest +import abc from sqlglot import exp from sqlglot.errors import ParseError @@ -26,7 +28,7 @@ from sqlmesh.core.state_sync import StateReader -class Selector: +class Selector(abc.ABC): def __init__( self, state_reader: StateReader, @@ -167,13 +169,13 @@ def get_model(fqn: str) -> t.Optional[Model]: def expand_model_selections( self, model_selections: t.Iterable[str], models: t.Optional[t.Dict[str, Model]] = None ) -> t.Set[str]: - """Expands a set of model selections into a set of model names. + """Expands a set of model selections into a set of model fqns that can be looked up in the Context. Args: model_selections: A set of model selections. Returns: - A set of model names. + A set of model fqns. """ node = parse(" | ".join(f"({s})" for s in model_selections)) @@ -194,10 +196,9 @@ def evaluate(node: exp.Expression) -> t.Set[str]: return { fqn for fqn, model in all_models.items() - if fnmatch.fnmatchcase(model.name, node.this) + if fnmatch.fnmatchcase(self._model_name(model), node.this) } - fqn = normalize_model_name(pattern, self._default_catalog, self._dialect) - return {fqn} if fqn in all_models else set() + return self._pattern_to_model_fqns(pattern, all_models) if isinstance(node, exp.And): return evaluate(node.left) & evaluate(node.right) if isinstance(node, exp.Or): @@ -241,6 +242,70 @@ def evaluate(node: exp.Expression) -> t.Set[str]: return evaluate(node) + @abc.abstractmethod + def _model_name(self, model: Model) -> str: + """Given a model, return the name that a selector pattern contining wildcards should be fnmatch'd on""" + pass + + @abc.abstractmethod + def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Model]) -> t.Set[str]: + """Given a pattern, return the keys of the matching models from :all_models""" + pass + + +class NativeSelector(Selector): + """Implementation of selectors that matches objects based on SQLMesh native names""" + + def _model_name(self, model: Model) -> str: + return model.name + + def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Model]) -> t.Set[str]: + fqn = normalize_model_name(pattern, self._default_catalog, self._dialect) + return {fqn} if fqn in all_models else set() + + +class DbtSelector(Selector): + """Implementation of selectors that matches objects based on the DBT names instead of the SQLMesh native names""" + + def _model_name(self, model: Model) -> str: + if dbt_fqn := model.dbt_fqn: + return dbt_fqn + raise SQLMeshError("dbt node information must be populated to use dbt selectors") + + def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Model]) -> t.Set[str]: + # a pattern like "staging.customers" should match a model called "jaffle_shop.staging.customers" + # but not a model called "jaffle_shop.customers.staging" + # also a pattern like "aging" should not match "staging" so we need to consider components; not substrings + pattern_components = pattern.split(".") + first_pattern_component = pattern_components[0] + matches = set() + for fqn, model in all_models.items(): + if not model.dbt_fqn: + continue + + dbt_fqn_components = model.dbt_fqn.split(".") + try: + starting_idx = dbt_fqn_components.index(first_pattern_component) + except ValueError: + continue + for pattern_component, fqn_component in zip_longest( + pattern_components, dbt_fqn_components[starting_idx:] + ): + if pattern_component and not fqn_component: + # the pattern still goes but we have run out of fqn components to match; no match + break + if fqn_component and not pattern_component: + # all elements of the pattern have matched elements of the fqn; match + matches.add(fqn) + break + if pattern_component != fqn_component: + # the pattern explicitly doesnt match a component; no match + break + else: + # called if no explicit break, indicating all components of the pattern matched all components of the fqn + matches.add(fqn) + return matches + class SelectorDialect(Dialect): IDENTIFIERS_CAN_START_WITH_DIGIT = True diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index e15a2cb93e..a157705ffd 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -185,7 +185,7 @@ def _plan_builder_options( options.update( dict( # Add every selected model as a restatement to force them to get repopulated from scratch - restate_models=list(self.context.models) + restate_models=[m.dbt_fqn for m in self.context.models.values() if m.dbt_fqn] if not select_models else select_models, # by default in SQLMesh, restatements only operate on what has been committed to state. @@ -231,6 +231,7 @@ def create( from sqlmesh.core.console import set_console from sqlmesh_dbt.console import DbtCliConsole from sqlmesh.utils.errors import SQLMeshError + from sqlmesh.core.selector import DbtSelector # clear any existing handlers set up by click/rich as defaults so that once SQLMesh logging config is applied, # we dont get duplicate messages logged from things like console.log_warning() @@ -250,6 +251,8 @@ def create( paths=[project_dir], config_loader_kwargs=dict(profile=profile, target=target, variables=vars), load=True, + # DbtSelector selects based on dbt model fqn's rather than SQLMesh model names + selector=DbtSelector, ) dbt_loader = sqlmesh_context._loaders[0] diff --git a/tests/core/test_selector.py b/tests/core/test_selector.py index 80b9ef691e..46d666db64 100644 --- a/tests/core/test_selector.py +++ b/tests/core/test_selector.py @@ -12,7 +12,7 @@ from sqlmesh.core.environment import Environment from sqlmesh.core.model import Model, SqlModel from sqlmesh.core.model.common import ParsableSql -from sqlmesh.core.selector import Selector +from sqlmesh.core.selector import NativeSelector from sqlmesh.core.snapshot import SnapshotChangeCategory from sqlmesh.utils import UniqueKeyDict from sqlmesh.utils.date import now_timestamp @@ -88,7 +88,7 @@ def test_select_models(mocker: MockerFixture, make_snapshot, default_catalog: t. local_models[modified_model_v2.fqn] = modified_model_v2.copy( update={"mapping_schema": added_model_schema} ) - selector = Selector(state_reader_mock, local_models, default_catalog=default_catalog) + selector = NativeSelector(state_reader_mock, local_models, default_catalog=default_catalog) _assert_models_equal( selector.select_models(["db.added_model"], env_name), @@ -243,7 +243,7 @@ def test_select_models_expired_environment(mocker: MockerFixture, make_snapshot) local_models: UniqueKeyDict[str, Model] = UniqueKeyDict("models") local_models[modified_model_v2.fqn] = modified_model_v2 - selector = Selector(state_reader_mock, local_models) + selector = NativeSelector(state_reader_mock, local_models) _assert_models_equal( selector.select_models(["*.modified_model"], env_name, fallback_env_name="prod"), @@ -305,7 +305,7 @@ def test_select_change_schema(mocker: MockerFixture, make_snapshot): local_child = child.copy(update={"mapping_schema": {'"db"': {'"parent"': {"b": "INT"}}}}) local_models[local_child.fqn] = local_child - selector = Selector(state_reader_mock, local_models) + selector = NativeSelector(state_reader_mock, local_models) selected = selector.select_models(["db.parent"], env_name) assert selected[local_child.fqn].render_query() != child.render_query() @@ -339,7 +339,7 @@ def test_select_models_missing_env(mocker: MockerFixture, make_snapshot): local_models: UniqueKeyDict[str, Model] = UniqueKeyDict("models") local_models[model.fqn] = model - selector = Selector(state_reader_mock, local_models) + selector = NativeSelector(state_reader_mock, local_models) assert selector.select_models([model.name], "missing_env").keys() == {model.fqn} assert not selector.select_models(["missing"], "missing_env") @@ -563,7 +563,7 @@ def test_expand_model_selections( ) models[model.fqn] = model - selector = Selector(mocker.Mock(), models) + selector = NativeSelector(mocker.Mock(), models) assert selector.expand_model_selections(selections) == output @@ -576,7 +576,7 @@ def test_model_selection_normalized(mocker: MockerFixture, make_snapshot): dialect="bigquery", ) models[model.fqn] = model - selector = Selector(mocker.Mock(), models, dialect="bigquery") + selector = NativeSelector(mocker.Mock(), models, dialect="bigquery") assert selector.expand_model_selections(["db.test_Model"]) == {'"db"."test_Model"'} @@ -624,7 +624,7 @@ def test_expand_git_selection( git_client_mock.list_uncommitted_changed_files.return_value = [] git_client_mock.list_committed_changed_files.return_value = [model_a._path, model_c._path] - selector = Selector(mocker.Mock(), models) + selector = NativeSelector(mocker.Mock(), models) selector._git_client = git_client_mock assert selector.expand_model_selections(expressions) == expected_fqns @@ -658,7 +658,7 @@ def test_select_models_with_external_parent(mocker: MockerFixture): local_models: UniqueKeyDict[str, Model] = UniqueKeyDict("models") local_models[added_model.fqn] = added_model - selector = Selector(state_reader_mock, local_models, default_catalog=default_catalog) + selector = NativeSelector(state_reader_mock, local_models, default_catalog=default_catalog) expanded_selections = selector.expand_model_selections(["+*added_model*"]) assert expanded_selections == {added_model.fqn} @@ -699,7 +699,7 @@ def test_select_models_local_tags_take_precedence_over_remote( local_models[local_existing.fqn] = local_existing local_models[local_new.fqn] = local_new - selector = Selector(state_reader_mock, local_models) + selector = NativeSelector(state_reader_mock, local_models) selected = selector.select_models(["tag:a"], env_name) diff --git a/tests/dbt/cli/test_list.py b/tests/dbt/cli/test_list.py index 4d294decc1..712d80b2fe 100644 --- a/tests/dbt/cli/test_list.py +++ b/tests/dbt/cli/test_list.py @@ -19,7 +19,7 @@ def test_list(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): def test_list_select(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): - result = invoke_cli(["list", "--select", "main.raw_customers+"]) + result = invoke_cli(["list", "--select", "raw_customers+"]) assert result.exit_code == 0 assert not result.exception @@ -34,7 +34,7 @@ def test_list_select(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Resul def test_list_select_exclude(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): # single exclude - result = invoke_cli(["list", "--select", "main.raw_customers+", "--exclude", "main.orders"]) + result = invoke_cli(["list", "--select", "raw_customers+", "--exclude", "orders"]) assert result.exit_code == 0 assert not result.exception @@ -49,8 +49,8 @@ def test_list_select_exclude(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[.. # multiple exclude for args in ( - ["--select", "main.stg_orders+", "--exclude", "main.customers", "--exclude", "main.orders"], - ["--select", "main.stg_orders+", "--exclude", "main.customers main.orders"], + ["--select", "stg_orders+", "--exclude", "customers", "--exclude", "orders"], + ["--select", "stg_orders+", "--exclude", "customers orders"], ): result = invoke_cli(["list", *args]) assert result.exit_code == 0 diff --git a/tests/dbt/cli/test_operations.py b/tests/dbt/cli/test_operations.py index 769887efe4..b23c87882a 100644 --- a/tests/dbt/cli/test_operations.py +++ b/tests/dbt/cli/test_operations.py @@ -138,7 +138,7 @@ def test_run_option_mapping(jaffle_shop_duckdb: Path): assert plan.selected_models_to_backfill is None assert {s.name for s in plan.snapshots} == {k for k in operations.context.snapshots} - plan = operations.run(select=["main.stg_orders+"]) + plan = operations.run(select=["stg_orders+"]) assert plan.environment.name == "prod" assert console.no_prompts is True assert console.no_diff is True @@ -155,7 +155,7 @@ def test_run_option_mapping(jaffle_shop_duckdb: Path): plan.selected_models_to_backfill | {standalone_audit_name} ) - plan = operations.run(select=["main.stg_orders+"], exclude=["main.customers"]) + plan = operations.run(select=["stg_orders+"], exclude=["customers"]) assert plan.environment.name == "prod" assert console.no_prompts is True assert console.no_diff is True @@ -171,7 +171,7 @@ def test_run_option_mapping(jaffle_shop_duckdb: Path): plan.selected_models_to_backfill | {standalone_audit_name} ) - plan = operations.run(exclude=["main.customers"]) + plan = operations.run(exclude=["customers"]) assert plan.environment.name == "prod" assert console.no_prompts is True assert console.no_diff is True @@ -238,7 +238,7 @@ def test_run_option_mapping_dev(jaffle_shop_duckdb: Path): assert plan.skip_backfill is True assert plan.selected_models_to_backfill == {'"jaffle_shop"."main"."new_model"'} - plan = operations.run(environment="dev", select=["main.stg_orders+"]) + plan = operations.run(environment="dev", select=["stg_orders+"]) assert plan.environment.name == "dev" assert console.no_prompts is True assert console.no_diff is True @@ -325,7 +325,7 @@ def test_run_option_full_refresh_with_selector(jaffle_shop_duckdb: Path): console = PlanCapturingConsole() operations.context.console = console - plan = operations.run(select=["main.stg_customers"], full_refresh=True) + plan = operations.run(select=["stg_customers"], full_refresh=True) assert len(plan.restatements) == 1 assert list(plan.restatements)[0].name == '"jaffle_shop"."main"."stg_customers"' diff --git a/tests/dbt/cli/test_run.py b/tests/dbt/cli/test_run.py index 788a7b04a8..7aeb8dd4d7 100644 --- a/tests/dbt/cli/test_run.py +++ b/tests/dbt/cli/test_run.py @@ -27,7 +27,7 @@ def test_run_with_selectors(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[... assert result.exit_code == 0 assert "main.orders" in result.output - result = invoke_cli(["run", "--select", "main.raw_customers+", "--exclude", "main.orders"]) + result = invoke_cli(["run", "--select", "raw_customers+", "--exclude", "orders"]) assert result.exit_code == 0 assert not result.exception diff --git a/tests/dbt/cli/test_selectors.py b/tests/dbt/cli/test_selectors.py index 6041a50d0a..99907bda84 100644 --- a/tests/dbt/cli/test_selectors.py +++ b/tests/dbt/cli/test_selectors.py @@ -1,6 +1,9 @@ import typing as t import pytest from sqlmesh_dbt import selectors +from sqlmesh.core.selector import DbtSelector +from sqlmesh.core.context import Context +from pathlib import Path @pytest.mark.parametrize( @@ -77,3 +80,192 @@ def test_split_unions_and_intersections( expression: str, expected: t.Tuple[t.List[str], t.List[str]] ): assert selectors._split_unions_and_intersections(expression) == expected + + +@pytest.mark.parametrize( + "dbt_select,expected", + [ + (["aging"], set()), + ( + ["staging"], + { + '"jaffle_shop"."main"."stg_customers"', + '"jaffle_shop"."main"."stg_orders"', + '"jaffle_shop"."main"."stg_payments"', + }, + ), + (["staging.stg_customers"], {'"jaffle_shop"."main"."stg_customers"'}), + (["stg_customers.staging"], set()), + ( + ["+customers"], + { + '"jaffle_shop"."main"."customers"', + '"jaffle_shop"."main"."stg_customers"', + '"jaffle_shop"."main"."stg_orders"', + '"jaffle_shop"."main"."stg_payments"', + '"jaffle_shop"."main"."raw_customers"', + '"jaffle_shop"."main"."raw_orders"', + '"jaffle_shop"."main"."raw_payments"', + }, + ), + (["customers+"], {'"jaffle_shop"."main"."customers"'}), + ( + ["customers+", "stg_orders"], + {'"jaffle_shop"."main"."customers"', '"jaffle_shop"."main"."stg_orders"'}, + ), + (["*.staging.stg_c*"], {'"jaffle_shop"."main"."stg_customers"'}), + (["tag:agg"], {'"jaffle_shop"."main"."agg_orders"'}), + ( + ["staging.stg_customers", "tag:agg"], + { + '"jaffle_shop"."main"."stg_customers"', + '"jaffle_shop"."main"."agg_orders"', + }, + ), + ( + ["+tag:agg"], + { + '"jaffle_shop"."main"."agg_orders"', + '"jaffle_shop"."main"."orders"', + '"jaffle_shop"."main"."stg_orders"', + '"jaffle_shop"."main"."stg_payments"', + '"jaffle_shop"."main"."raw_orders"', + '"jaffle_shop"."main"."raw_payments"', + }, + ), + ( + ["tag:agg+"], + { + '"jaffle_shop"."main"."agg_orders"', + }, + ), + ( + ["tag:b*"], + set(), + ), + ( + ["tag:a*"], + { + '"jaffle_shop"."main"."agg_orders"', + }, + ), + ], +) +def test_select_by_dbt_names( + jaffle_shop_duckdb: Path, + jaffle_shop_duckdb_context: Context, + dbt_select: t.List[str], + expected: t.Set[str], +): + (jaffle_shop_duckdb / "models" / "agg_orders.sql").write_text(""" + {{ config(tags=["agg"]) }} + select order_date, count(*) as num_orders from {{ ref('orders') }} + """) + + ctx = jaffle_shop_duckdb_context + ctx.load() + assert '"jaffle_shop"."main"."agg_orders"' in ctx.models + + selector = ctx._new_selector() + assert isinstance(selector, DbtSelector) + + sqlmesh_selector = selectors.to_sqlmesh(dbt_select=dbt_select, dbt_exclude=[]) + assert sqlmesh_selector + + assert selector.expand_model_selections([sqlmesh_selector]) == expected + + +@pytest.mark.parametrize( + "dbt_exclude,expected", + [ + (["jaffle_shop"], set()), + ( + ["staging"], + { + '"jaffle_shop"."main"."agg_orders"', + '"jaffle_shop"."main"."customers"', + '"jaffle_shop"."main"."orders"', + '"jaffle_shop"."main"."raw_customers"', + '"jaffle_shop"."main"."raw_orders"', + '"jaffle_shop"."main"."raw_payments"', + }, + ), + (["+customers"], {'"jaffle_shop"."main"."orders"', '"jaffle_shop"."main"."agg_orders"'}), + ( + ["+tag:agg"], + { + '"jaffle_shop"."main"."customers"', + '"jaffle_shop"."main"."stg_customers"', + '"jaffle_shop"."main"."raw_customers"', + }, + ), + ], +) +def test_exclude_by_dbt_names( + jaffle_shop_duckdb: Path, + jaffle_shop_duckdb_context: Context, + dbt_exclude: t.List[str], + expected: t.Set[str], +): + (jaffle_shop_duckdb / "models" / "agg_orders.sql").write_text(""" + {{ config(tags=["agg"]) }} + select order_date, count(*) as num_orders from {{ ref('orders') }} + """) + + ctx = jaffle_shop_duckdb_context + ctx.load() + assert '"jaffle_shop"."main"."agg_orders"' in ctx.models + + selector = ctx._new_selector() + assert isinstance(selector, DbtSelector) + + sqlmesh_selector = selectors.to_sqlmesh(dbt_select=[], dbt_exclude=dbt_exclude) + assert sqlmesh_selector + + assert selector.expand_model_selections([sqlmesh_selector]) == expected + + +@pytest.mark.parametrize( + "dbt_select,dbt_exclude,expected", + [ + (["jaffle_shop"], ["jaffle_shop"], set()), + ( + ["staging"], + ["stg_customers"], + { + '"jaffle_shop"."main"."stg_orders"', + '"jaffle_shop"."main"."stg_payments"', + }, + ), + ( + ["staging.stg_customers", "tag:agg"], + ["tag:agg"], + { + '"jaffle_shop"."main"."stg_customers"', + }, + ), + ], +) +def test_selection_and_exclusion_by_dbt_names( + jaffle_shop_duckdb: Path, + jaffle_shop_duckdb_context: Context, + dbt_select: t.List[str], + dbt_exclude: t.List[str], + expected: t.Set[str], +): + (jaffle_shop_duckdb / "models" / "agg_orders.sql").write_text(""" + {{ config(tags=["agg"]) }} + select order_date, count(*) as num_orders from {{ ref('orders') }} + """) + + ctx = jaffle_shop_duckdb_context + ctx.load() + assert '"jaffle_shop"."main"."agg_orders"' in ctx.models + + selector = ctx._new_selector() + assert isinstance(selector, DbtSelector) + + sqlmesh_selector = selectors.to_sqlmesh(dbt_select=dbt_select, dbt_exclude=dbt_exclude) + assert sqlmesh_selector + + assert selector.expand_model_selections([sqlmesh_selector]) == expected diff --git a/tests/dbt/conftest.py b/tests/dbt/conftest.py index 56d77e7496..846dfc6aa9 100644 --- a/tests/dbt/conftest.py +++ b/tests/dbt/conftest.py @@ -7,6 +7,7 @@ import pytest from sqlmesh.core.context import Context +from sqlmesh.core.selector import DbtSelector from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.project import Project from sqlmesh.dbt.target import PostgresConfig @@ -99,7 +100,7 @@ def jaffle_shop_duckdb(copy_to_temp_path: t.Callable[..., t.List[Path]]) -> t.It @pytest.fixture def jaffle_shop_duckdb_context(jaffle_shop_duckdb: Path) -> Context: init_project_if_required(jaffle_shop_duckdb) - return Context(paths=[jaffle_shop_duckdb]) + return Context(paths=[jaffle_shop_duckdb], selector=DbtSelector) @pytest.fixture() From 92e4a32ed61dbf6106935c76000ad09c2fd2ffa5 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:09:58 +0300 Subject: [PATCH 0908/1056] Chore!: bump sqlglot to v27.18.0 (#5439) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b3e13b63ee..35376dd095 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.17.0", + "sqlglot[rs]~=27.18.0", "tenacity", "time-machine", "json-stream" From 15a0e1002e995999ef93f249b45cdcd9d4f53718 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:40:02 -0700 Subject: [PATCH 0909/1056] fix: include forward_only parsed snapshot (#5442) --- sqlmesh/core/state_sync/db/migrator.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/state_sync/db/migrator.py b/sqlmesh/core/state_sync/db/migrator.py index b803a5cc40..3e3f978b96 100644 --- a/sqlmesh/core/state_sync/db/migrator.py +++ b/sqlmesh/core/state_sync/db/migrator.py @@ -229,6 +229,7 @@ def _migrate_snapshot_rows( "updated_ts": updated_ts, "unpaused_ts": unpaused_ts, "unrestorable": unrestorable, + "forward_only": forward_only, } for where in ( snapshot_id_filter( @@ -237,10 +238,16 @@ def _migrate_snapshot_rows( if snapshots is not None else [None] ) - for name, identifier, raw_snapshot, updated_ts, unpaused_ts, unrestorable in fetchall( + for name, identifier, raw_snapshot, updated_ts, unpaused_ts, unrestorable, forward_only in fetchall( self.engine_adapter, exp.select( - "name", "identifier", "snapshot", "updated_ts", "unpaused_ts", "unrestorable" + "name", + "identifier", + "snapshot", + "updated_ts", + "unpaused_ts", + "unrestorable", + "forward_only", ) .from_(self.snapshot_state.snapshots_table) .where(where) From d15446c7912c3703afe18587931f4dc5eeae6130 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:24:36 -0500 Subject: [PATCH 0910/1056] Fix: route linter warnings correctly in github output (#5441) --- sqlmesh/core/console.py | 5 +- .../github/cicd/test_github_controller.py | 59 ++++++++++++++++--- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index d9567ae484..8af837b08a 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -3641,7 +3641,10 @@ def show_linter_violations( msg = f"\nLinter {severity} for `{model._path}`:\n{violations_msg}\n" self._print(msg) - self._errors.append(msg) + if is_error: + self._errors.append(msg) + else: + self._warnings.append(msg) @property def captured_warnings(self) -> str: diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index a27f75f459..8242d697b6 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -15,6 +15,7 @@ from sqlmesh.core.model import SqlModel from sqlmesh.core.user import User, UserRole from sqlmesh.core.plan.definition import Plan +from sqlmesh.core.linter.rule import RuleViolation from sqlmesh.integrations.github.cicd.config import GithubCICDBotConfig, MergeMethod from sqlmesh.integrations.github.cicd.controller import ( BotCommand, @@ -29,6 +30,29 @@ pytestmark = pytest.mark.github + +def add_linter_violations(controller: GithubController): + class _MockModel: + _path = "tests/linter_test.sql" + + class _MockLinterRule: + name = "mock_linter_rule" + + controller._console.show_linter_violations( + [ + RuleViolation( + rule=_MockLinterRule(), violation_msg="Linter warning", violation_range=None + ) + ], + _MockModel(), + ) + controller._console.show_linter_violations( + [RuleViolation(rule=_MockLinterRule(), violation_msg="Linter error", violation_range=None)], + _MockModel(), + is_error=True, + ) + + github_controller_approvers_params = [ ( "2 approvers, 1 required", @@ -660,12 +684,18 @@ def test_get_plan_summary_includes_warnings_and_errors( controller._console.log_warning("Warning 1\nWith multiline") controller._console.log_warning("Warning 2") controller._console.log_error("Error 1") + add_linter_violations(controller) summary = controller.get_plan_summary(controller.prod_plan) - assert ("> [!WARNING]\n>\n> - Warning 1\n> With multiline\n>\n> - Warning 2\n\n") in summary - - assert ("> [!CAUTION]\n>\n> Error 1\n\n") in summary + assert ("> [!WARNING]\n>\n> - Warning 1\n> With multiline\n>\n> - Warning 2\n>\n>") in summary + assert ( + "> Linter warnings for `tests/linter_test.sql`:\n> - mock_linter_rule: Linter warning\n>" + ) in summary + assert ("> [!CAUTION]\n>\n> - Error 1\n>\n>") in summary + assert ( + "> Linter **errors** for `tests/linter_test.sql`:\n> - mock_linter_rule: Linter error\n>" + ) in summary def test_get_pr_environment_summary_includes_warnings_and_errors( @@ -679,24 +709,39 @@ def test_get_pr_environment_summary_includes_warnings_and_errors( controller._console.log_warning("Warning 1") controller._console.log_error("Error 1") + add_linter_violations(controller) # completed with no exception triggers a SUCCESS conclusion and only shows warnings success_summary = controller.get_pr_environment_summary( conclusion=GithubCheckConclusion.SUCCESS ) - assert "> [!WARNING]\n>\n> Warning 1\n" in success_summary - assert "> [!CAUTION]\n>\n> Error 1" not in success_summary + assert "> [!WARNING]\n>\n> - Warning 1\n" in success_summary + assert ( + "> Linter warnings for `tests/linter_test.sql`:\n> - mock_linter_rule: Linter warning\n" + in success_summary + ) + assert "Error 1" not in success_summary + assert "mock_linter_rule: Linter error" not in success_summary # since they got consumed in the previous call controller._console.log_warning("Warning 1") controller._console.log_error("Error 1") + add_linter_violations(controller) # completed with an exception triggers a FAILED conclusion and shows errors error_summary = controller.get_pr_environment_summary( conclusion=GithubCheckConclusion.FAILURE, exception=SQLMeshError("Something broke") ) - assert "> [!WARNING]\n>\n> Warning 1\n" in error_summary - assert "> [!CAUTION]\n>\n> Error 1" in error_summary + assert "> [!WARNING]\n>\n> - Warning 1\n>\n" in error_summary + assert ( + "> Linter warnings for `tests/linter_test.sql`:\n> - mock_linter_rule: Linter warning\n" + in error_summary + ) + assert "[!CAUTION]\n>
      \n>\n> - Error 1\n>\n" in error_summary + assert ( + "> Linter **errors** for `tests/linter_test.sql`:\n> - mock_linter_rule: Linter error\n" + in error_summary + ) def test_pr_comment_deploy_indicator_includes_command_namespace( From 71f3eb7f0c4cd52ed1a7144acd6424ca6078205f Mon Sep 17 00:00:00 2001 From: Tori Wei <41123940+toriwei@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:16:28 -0700 Subject: [PATCH 0911/1056] fix: prevent duplicate audit names (#5431) --- sqlmesh/dbt/basemodel.py | 2 +- sqlmesh/dbt/loader.py | 3 ++- sqlmesh/dbt/test.py | 4 ++++ tests/core/integration/test_dbt.py | 2 +- tests/dbt/test_model.py | 10 +++++----- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index 4637bbf91c..4dcf44a0af 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -305,7 +305,7 @@ def sqlmesh_model_kwargs( jinja_macros.add_globals(self._model_jinja_context(model_context, dependencies)) model_kwargs = { - "audits": [(test.name, {}) for test in self.tests], + "audits": [(test.canonical_name, {}) for test in self.tests], "column_descriptions": column_descriptions_to_sqlmesh(self.columns) or None, "depends_on": { model.canonical_name(context) for model in model_context.refs.values() diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index f7d97e74c8..eb117a3e40 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -172,7 +172,8 @@ def _load_audits( for test in package.tests.values(): logger.debug("Converting '%s' to sqlmesh format", test.name) try: - audits[test.name] = test.to_sqlmesh(package_context) + audits[test.canonical_name] = test.to_sqlmesh(package_context) + except BaseMissingReferenceError as e: ref_type = "model" if isinstance(e, MissingModelError) else "source" logger.warning( diff --git a/sqlmesh/dbt/test.py b/sqlmesh/dbt/test.py index 747c9d469c..1bd8a8e6e2 100644 --- a/sqlmesh/dbt/test.py +++ b/sqlmesh/dbt/test.py @@ -109,6 +109,10 @@ def _validate_severity(cls, v: t.Union[Severity, str]) -> Severity: def _lowercase_name(cls, v: str) -> str: return v.lower() + @property + def canonical_name(self) -> str: + return f"{self.package_name}.{self.name}" if self.package_name else self.name + @property def is_standalone(self) -> bool: # A test is standalone if: diff --git a/tests/core/integration/test_dbt.py b/tests/core/integration/test_dbt.py index 5e600899dd..6f23acb97e 100644 --- a/tests/core/integration/test_dbt.py +++ b/tests/core/integration/test_dbt.py @@ -48,7 +48,7 @@ def test_dbt_is_incremental_table_is_missing(sushi_test_dbt_context: Context): model = context.get_model("sushi.waiter_revenue_by_day_v2") model = model.copy(update={"kind": IncrementalUnmanagedKind(), "start": "2023-01-01"}) context.upsert_model(model) - context._standalone_audits["test_top_waiters"].start = "2023-01-01" + context._standalone_audits["sushi.test_top_waiters"].start = "2023-01-01" context.plan("prod", auto_apply=True, no_prompts=True, skip_tests=True) diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index a64b29e89d..d212872cb7 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -190,23 +190,23 @@ def test_manifest_filters_standalone_tests_from_models( # Should only have "not_null" test, not the "relationships" test model1_audit_names = [audit[0] for audit in model1_snapshot.model.audits] assert len(model1_audit_names) == 1 - assert model1_audit_names[0] == "not_null_model1_id" + assert model1_audit_names[0] == "local.not_null_model1_id" # Verify model2 has its non-standalone test model2_audit_names = [audit[0] for audit in model2_snapshot.model.audits] assert len(model2_audit_names) == 1 - assert model2_audit_names[0] == "not_null_model2_id" + assert model2_audit_names[0] == "local.not_null_model2_id" # Verify the standalone test (relationships) exists as a StandaloneAudit all_non_standalone_audits = [name for name in context._audits] assert sorted(all_non_standalone_audits) == [ - "not_null_model1_id", - "not_null_model2_id", + "local.not_null_model1_id", + "local.not_null_model2_id", ] standalone_audits = [name for name in context._standalone_audits] assert len(standalone_audits) == 1 - assert standalone_audits[0] == "relationships_model1_id__id__ref_model2_" + assert standalone_audits[0] == "local.relationships_model1_id__id__ref_model2_" plan_builder = context.plan_builder() dag = plan_builder._build_dag() From 08950c8e9e2dbd73853aa5d46c34dfb11320b36f Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:44:40 -0700 Subject: [PATCH 0912/1056] chore!: bump SQLGlot to v27.19.0 (#5446) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 35376dd095..9b192d6a78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.18.0", + "sqlglot[rs]~=27.19.0", "tenacity", "time-machine", "json-stream" From 0bf202c471ee0a3989383cc5d261ff4a41b5adf1 Mon Sep 17 00:00:00 2001 From: Tomasz Zorawik <67728999+xardasos@users.noreply.github.com> Date: Sat, 27 Sep 2025 01:25:36 +0200 Subject: [PATCH 0913/1056] Feat!: Categorize indirect MV changes as breaking for seamless version switching (#5374) --- sqlmesh/core/plan/builder.py | 8 ++ tests/core/test_plan.py | 142 ++++++++++++++++++++++++++++++++++- 2 files changed, 149 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 2eb4c54aeb..7d753cc330 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -680,6 +680,14 @@ def _categorize_snapshot( if mode == AutoCategorizationMode.FULL: snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only) elif self._context_diff.indirectly_modified(snapshot.name): + if snapshot.is_materialized_view and not forward_only: + # We categorize changes as breaking to allow for instantaneous switches in a virtual layer. + # Otherwise, there might be a potentially long downtime during MVs recreation. + # In the case of forward-only changes this optimization is not applicable because we want to continue + # using the same (existing) table version. + snapshot.categorize_as(SnapshotChangeCategory.INDIRECT_BREAKING, forward_only) + return + all_upstream_forward_only = set() all_upstream_categories = set() direct_parent_categories = set() diff --git a/tests/core/test_plan.py b/tests/core/test_plan.py index 40967f1fbe..4b330c376f 100644 --- a/tests/core/test_plan.py +++ b/tests/core/test_plan.py @@ -26,7 +26,7 @@ SqlModel, ModelKindName, ) -from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange +from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange, ViewKind from sqlmesh.core.model.seed import Seed from sqlmesh.core.plan import Plan, PlanBuilder, SnapshotIntervals from sqlmesh.core.snapshot import ( @@ -4162,3 +4162,143 @@ def test_plan_ignore_cron_flag(make_snapshot): ], ) ] + + +def test_indirect_change_to_materialized_view_is_breaking(make_snapshot): + snapshot_a_old = make_snapshot( + SqlModel( + name="a", + query=parse_one("select 1 as col_a"), + kind=ViewKind(materialized=True), + ) + ) + snapshot_a_old.categorize_as(SnapshotChangeCategory.BREAKING) + + snapshot_b_old = make_snapshot( + SqlModel( + name="b", + query=parse_one("select col_a from a"), + kind=ViewKind(materialized=True), + ), + nodes={'"a"': snapshot_a_old.model}, + ) + snapshot_b_old.categorize_as(SnapshotChangeCategory.BREAKING) + + snapshot_a_new = make_snapshot( + SqlModel( + name="a", + query=parse_one("select 1 as col_a, 2 as col_b"), + kind=ViewKind(materialized=True), + ) + ) + + snapshot_a_new.previous_versions = snapshot_a_old.all_versions + + snapshot_b_new = make_snapshot( + snapshot_b_old.model, + nodes={'"a"': snapshot_a_new.model}, + ) + snapshot_b_new.previous_versions = snapshot_b_old.all_versions + + context_diff = ContextDiff( + environment="test_environment", + is_new_environment=True, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={ + snapshot_a_new.name: (snapshot_a_new, snapshot_a_old), + snapshot_b_new.name: (snapshot_b_new, snapshot_b_old), + }, + snapshots={ + snapshot_a_new.snapshot_id: snapshot_a_new, + snapshot_b_new.snapshot_id: snapshot_b_new, + }, + new_snapshots={ + snapshot_a_new.snapshot_id: snapshot_a_new, + snapshot_b_new.snapshot_id: snapshot_b_new, + }, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + PlanBuilder(context_diff, forward_only=False).build() + + assert snapshot_b_new.change_category == SnapshotChangeCategory.INDIRECT_BREAKING + + +def test_forward_only_indirect_change_to_materialized_view(make_snapshot): + snapshot_a_old = make_snapshot( + SqlModel( + name="a", + query=parse_one("select 1 as col_a"), + ) + ) + snapshot_a_old.categorize_as(SnapshotChangeCategory.BREAKING) + + snapshot_b_old = make_snapshot( + SqlModel( + name="b", + query=parse_one("select col_a from a"), + kind=ViewKind(materialized=True), + ), + nodes={'"a"': snapshot_a_old.model}, + ) + snapshot_b_old.categorize_as(SnapshotChangeCategory.BREAKING) + + snapshot_a_new = make_snapshot( + SqlModel( + name="a", + query=parse_one("select 1 as col_a, 2 as col_b"), + ) + ) + + snapshot_a_new.previous_versions = snapshot_a_old.all_versions + + snapshot_b_new = make_snapshot( + snapshot_b_old.model, + nodes={'"a"': snapshot_a_new.model}, + ) + snapshot_b_new.previous_versions = snapshot_b_old.all_versions + + context_diff = ContextDiff( + environment="test_environment", + is_new_environment=True, + is_unfinalized_environment=False, + normalize_environment_name=True, + create_from="prod", + create_from_env_exists=True, + added=set(), + removed_snapshots={}, + modified_snapshots={ + snapshot_a_new.name: (snapshot_a_new, snapshot_a_old), + snapshot_b_new.name: (snapshot_b_new, snapshot_b_old), + }, + snapshots={ + snapshot_a_new.snapshot_id: snapshot_a_new, + snapshot_b_new.snapshot_id: snapshot_b_new, + }, + new_snapshots={ + snapshot_a_new.snapshot_id: snapshot_a_new, + snapshot_b_new.snapshot_id: snapshot_b_new, + }, + previous_plan_id=None, + previously_promoted_snapshot_ids=set(), + previous_finalized_snapshots=None, + previous_gateway_managed_virtual_layer=False, + gateway_managed_virtual_layer=False, + environment_statements=[], + ) + + PlanBuilder(context_diff, forward_only=True).build() + + # Forward-only indirect changes to MVs should not always be classified as indirect breaking. + # Instead, we want to preserve the standard categorization. + assert snapshot_b_new.change_category == SnapshotChangeCategory.INDIRECT_NON_BREAKING From b29e71bc1a2c82cebd362d497af8ba0236f5e3b1 Mon Sep 17 00:00:00 2001 From: Vaggelis Danias Date: Mon, 29 Sep 2025 17:34:26 +0300 Subject: [PATCH 0914/1056] Feat!: Skip model evaluation if upstream external model(s) have not changed (#5277) --- sqlmesh/core/context.py | 7 + sqlmesh/core/engine_adapter/base.py | 4 + sqlmesh/core/engine_adapter/bigquery.py | 22 +++ sqlmesh/core/engine_adapter/snowflake.py | 16 ++ sqlmesh/core/plan/evaluator.py | 1 + sqlmesh/core/scheduler.py | 15 +- sqlmesh/core/signal.py | 44 ++++- sqlmesh/core/snapshot/definition.py | 31 ++++ sqlmesh/core/state_sync/base.py | 4 + sqlmesh/core/state_sync/db/facade.py | 3 +- sqlmesh/core/state_sync/db/interval.py | 22 ++- .../v0099_add_last_altered_to_intervals.py | 27 +++ .../integration/test_integration.py | 158 +++++++++++++++++- 13 files changed, 346 insertions(+), 8 deletions(-) create mode 100644 sqlmesh/migrations/v0099_add_last_altered_to_intervals.py diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index e3feb1e14b..e31a04fe81 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -274,6 +274,7 @@ def __init__( deployability_index: t.Optional[DeployabilityIndex] = None, default_dialect: t.Optional[str] = None, default_catalog: t.Optional[str] = None, + is_restatement: t.Optional[bool] = None, variables: t.Optional[t.Dict[str, t.Any]] = None, blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None, ): @@ -284,6 +285,7 @@ def __init__( self._default_dialect = default_dialect self._variables = variables or {} self._blueprint_variables = blueprint_variables or {} + self._is_restatement = is_restatement @property def default_dialect(self) -> t.Optional[str]: @@ -308,6 +310,10 @@ def gateway(self) -> t.Optional[str]: """Returns the gateway name.""" return self.var(c.GATEWAY) + @property + def is_restatement(self) -> t.Optional[bool]: + return self._is_restatement + def var(self, var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]: """Returns a variable value.""" return self._variables.get(var_name.lower(), default) @@ -328,6 +334,7 @@ def with_variables( self.deployability_index, self._default_dialect, self._default_catalog, + self._is_restatement, variables=variables, blueprint_variables=blueprint_variables, ) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 47e6a4260c..68c6404081 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -119,6 +119,7 @@ class EngineAdapter: MAX_IDENTIFIER_LENGTH: t.Optional[int] = None ATTACH_CORRELATION_ID = True SUPPORTS_QUERY_EXECUTION_TRACKING = False + SUPPORTS_METADATA_TABLE_LAST_MODIFIED_TS = False def __init__( self, @@ -2927,6 +2928,9 @@ def _check_identifier_length(self, expression: exp.Expression) -> None: f"Identifier name '{name}' (length {name_length}) exceeds {self.dialect.capitalize()}'s max identifier limit of {self.MAX_IDENTIFIER_LENGTH} characters" ) + def get_table_last_modified_ts(self, table_names: t.List[TableName]) -> t.List[int]: + raise NotImplementedError() + class EngineAdapterWithIndexSupport(EngineAdapter): SUPPORTS_INDEXES = True diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 0dfa2325e8..26abad9ebc 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -755,6 +755,28 @@ def table_exists(self, table_name: TableName) -> bool: except NotFound: return False + def get_table_last_modified_ts(self, table_names: t.List[TableName]) -> t.List[int]: + from sqlmesh.utils.date import to_timestamp + + datasets_to_tables: t.DefaultDict[str, t.List[str]] = defaultdict(list) + for table_name in table_names: + table = exp.to_table(table_name) + datasets_to_tables[table.db].append(table.name) + + results = [] + + for dataset, tables in datasets_to_tables.items(): + query = ( + f"SELECT TIMESTAMP_MILLIS(last_modified_time) FROM `{dataset}.__TABLES__` WHERE " + ) + for i, table_name in enumerate(tables): + query += f"TABLE_ID = '{table_name}'" + if i < len(tables) - 1: + query += " OR " + results.extend(self.fetchall(query)) + + return [to_timestamp(row[0]) for row in results] + def _get_table(self, table_name: TableName) -> BigQueryTable: """ Returns a BigQueryTable object for the given table name. diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index 9c27b45115..1554589779 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -54,6 +54,7 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi SUPPORTS_MANAGED_MODELS = True CURRENT_CATALOG_EXPRESSION = exp.func("current_database") SUPPORTS_CREATE_DROP_CATALOG = True + SUPPORTS_METADATA_TABLE_LAST_MODIFIED_TS = True SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["DATABASE", "SCHEMA", "TABLE"] SCHEMA_DIFFER_KWARGS = { "parameterized_type_defaults": { @@ -669,3 +670,18 @@ def close(self) -> t.Any: self._connection_pool.set_attribute(self.SNOWPARK, None) return super().close() + + def get_table_last_modified_ts(self, table_names: t.List[TableName]) -> t.List[int]: + from sqlmesh.utils.date import to_timestamp + + num_tables = len(table_names) + + query = "SELECT LAST_ALTERED FROM INFORMATION_SCHEMA.TABLES WHERE" + for i, table_name in enumerate(table_names): + table = exp.to_table(table_name) + query += f"""(TABLE_NAME = '{table.name}' AND TABLE_SCHEMA = '{table.db}' AND TABLE_CATALOG = '{table.catalog}')""" + if i < num_tables - 1: + query += " OR " + + result = self.fetchall(query) + return [to_timestamp(row[0]) for row in result] diff --git a/sqlmesh/core/plan/evaluator.py b/sqlmesh/core/plan/evaluator.py index 03ecb770bf..f2f432a97e 100644 --- a/sqlmesh/core/plan/evaluator.py +++ b/sqlmesh/core/plan/evaluator.py @@ -258,6 +258,7 @@ def visit_backfill_stage(self, stage: stages.BackfillStage, plan: EvaluatablePla allow_additive_snapshots=plan.allow_additive_models, selected_snapshot_ids=stage.selected_snapshot_ids, selected_models=plan.selected_models, + is_restatement=bool(plan.restatements), ) if errors: raise PlanError("Plan application failed.") diff --git a/sqlmesh/core/scheduler.py b/sqlmesh/core/scheduler.py index af4d72b165..7e27205fc6 100644 --- a/sqlmesh/core/scheduler.py +++ b/sqlmesh/core/scheduler.py @@ -251,7 +251,9 @@ def evaluate( **kwargs, ) - self.state_sync.add_interval(snapshot, start, end, is_dev=not is_deployable) + self.state_sync.add_interval( + snapshot, start, end, is_dev=not is_deployable, last_altered_ts=now_timestamp() + ) return audit_results def run( @@ -335,6 +337,7 @@ def batch_intervals( deployability_index: t.Optional[DeployabilityIndex], environment_naming_info: EnvironmentNamingInfo, dag: t.Optional[DAG[SnapshotId]] = None, + is_restatement: bool = False, ) -> t.Dict[Snapshot, Intervals]: dag = dag or snapshots_to_dag(merged_intervals) @@ -367,6 +370,7 @@ def batch_intervals( deployability_index, default_dialect=adapter.dialect, default_catalog=self.default_catalog, + is_restatement=is_restatement, ) intervals = self._check_ready_intervals( @@ -422,6 +426,7 @@ def run_merged_intervals( run_environment_statements: bool = False, audit_only: bool = False, auto_restatement_triggers: t.Dict[SnapshotId, t.List[SnapshotId]] = {}, + is_restatement: bool = False, ) -> t.Tuple[t.List[NodeExecutionFailedError[SchedulingUnit]], t.List[SchedulingUnit]]: """Runs precomputed batches of missing intervals. @@ -455,9 +460,12 @@ def run_merged_intervals( snapshot_dag = full_dag.subdag(*selected_snapshot_ids_set) batched_intervals = self.batch_intervals( - merged_intervals, deployability_index, environment_naming_info, dag=snapshot_dag + merged_intervals, + deployability_index, + environment_naming_info, + dag=snapshot_dag, + is_restatement=is_restatement, ) - self.console.start_evaluation_progress( batched_intervals, environment_naming_info, @@ -956,6 +964,7 @@ def _check_ready_intervals( python_env=signals.python_env, dialect=snapshot.model.dialect, path=snapshot.model._path, + snapshot=snapshot, kwargs=kwargs, ) except SQLMeshError as e: diff --git a/sqlmesh/core/signal.py b/sqlmesh/core/signal.py index d9ee670922..52e6c59c8d 100644 --- a/sqlmesh/core/signal.py +++ b/sqlmesh/core/signal.py @@ -1,8 +1,14 @@ from __future__ import annotations - +import typing as t from sqlmesh.utils import UniqueKeyDict, registry_decorator +if t.TYPE_CHECKING: + from sqlmesh.core.context import ExecutionContext + from sqlmesh.core.snapshot.definition import Snapshot + from sqlmesh.utils.date import DatetimeRanges + from sqlmesh.core.snapshot.definition import DeployabilityIndex + class signal(registry_decorator): """Specifies a function which intervals are ready from a list of scheduled intervals. @@ -33,3 +39,39 @@ class signal(registry_decorator): SignalRegistry = UniqueKeyDict[str, signal] + + +@signal() +def freshness(batch: DatetimeRanges, snapshot: Snapshot, context: ExecutionContext) -> bool: + adapter = context.engine_adapter + if context.is_restatement or not adapter.SUPPORTS_METADATA_TABLE_LAST_MODIFIED_TS: + return True + + deployability_index = context.deployability_index or DeployabilityIndex.all_deployable() + + last_altered_ts = ( + snapshot.last_altered_ts + if deployability_index.is_deployable(snapshot) + else snapshot.dev_last_altered_ts + ) + if not last_altered_ts: + return True + + parent_snapshots = {context.snapshots[p.name] for p in snapshot.parents} + if len(parent_snapshots) != len(snapshot.node.depends_on) or not all( + p.is_external for p in parent_snapshots + ): + # The mismatch can happen if e.g an external model is not registered in the project + return True + + # Finding new data means that the upstream depedencies have been altered + # since the last time the model was evaluated + upstream_dep_has_new_data = any( + upstream_last_altered_ts > last_altered_ts + for upstream_last_altered_ts in adapter.get_table_last_modified_ts( + [p.name for p in parent_snapshots] + ) + ) + + # Returning true is a no-op, returning False nullifies the batch so the model will not be evaluated. + return upstream_dep_has_new_data diff --git a/sqlmesh/core/snapshot/definition.py b/sqlmesh/core/snapshot/definition.py index 600d84fe83..23ab0b21db 100644 --- a/sqlmesh/core/snapshot/definition.py +++ b/sqlmesh/core/snapshot/definition.py @@ -185,6 +185,8 @@ class SnapshotIntervals(PydanticModel): intervals: Intervals = [] dev_intervals: Intervals = [] pending_restatement_intervals: Intervals = [] + last_altered_ts: t.Optional[int] = None + dev_last_altered_ts: t.Optional[int] = None @property def snapshot_id(self) -> t.Optional[SnapshotId]: @@ -205,6 +207,12 @@ def add_dev_interval(self, start: int, end: int) -> None: def add_pending_restatement_interval(self, start: int, end: int) -> None: self._add_interval(start, end, "pending_restatement_intervals") + def update_last_altered_ts(self, last_altered_ts: t.Optional[int]) -> None: + self._update_last_altered_ts(last_altered_ts, "last_altered_ts") + + def update_dev_last_altered_ts(self, last_altered_ts: t.Optional[int]) -> None: + self._update_last_altered_ts(last_altered_ts, "dev_last_altered_ts") + def remove_interval(self, start: int, end: int) -> None: self._remove_interval(start, end, "intervals") @@ -224,6 +232,13 @@ def _add_interval(self, start: int, end: int, interval_attr: str) -> None: target_intervals = merge_intervals([*target_intervals, (start, end)]) setattr(self, interval_attr, target_intervals) + def _update_last_altered_ts( + self, last_altered_ts: t.Optional[int], last_altered_attr: str + ) -> None: + if last_altered_ts: + existing_last_altered_ts = getattr(self, last_altered_attr) + setattr(self, last_altered_attr, max(existing_last_altered_ts or 0, last_altered_ts)) + def _remove_interval(self, start: int, end: int, interval_attr: str) -> None: target_intervals = getattr(self, interval_attr) target_intervals = remove_interval(target_intervals, start, end) @@ -713,6 +728,10 @@ class Snapshot(PydanticModel, SnapshotInfoMixin): dev_table_suffix: str = "dev" table_naming_convention: TableNamingConvention = TableNamingConvention.default forward_only: bool = False + # Physical table last modified timestamp, not to be confused with the "updated_ts" field + # which is for the snapshot record itself + last_altered_ts: t.Optional[int] = None + dev_last_altered_ts: t.Optional[int] = None @field_validator("ttl") @classmethod @@ -751,6 +770,7 @@ def hydrate_with_intervals_by_version( ) for interval in snapshot_intervals: snapshot.merge_intervals(interval) + result.append(snapshot) return result @@ -957,12 +977,20 @@ def merge_intervals(self, other: t.Union[Snapshot, SnapshotIntervals]) -> None: if not apply_effective_from or end <= effective_from_ts: self.add_interval(start, end) + if other.last_altered_ts: + self.last_altered_ts = max(self.last_altered_ts or 0, other.last_altered_ts) + if self.dev_version == other.dev_version: # Merge dev intervals if the dev versions match which would mean # that this and the other snapshot are pointing to the same dev table. for start, end in other.dev_intervals: self.add_interval(start, end, is_dev=True) + if other.dev_last_altered_ts: + self.dev_last_altered_ts = max( + self.dev_last_altered_ts or 0, other.dev_last_altered_ts + ) + self.pending_restatement_intervals = merge_intervals( [*self.pending_restatement_intervals, *other.pending_restatement_intervals] ) @@ -1081,6 +1109,7 @@ def check_ready_intervals( python_env=signals.python_env, dialect=self.model.dialect, path=self.model._path, + snapshot=self, kwargs=kwargs, ) except SQLMeshError as e: @@ -2421,6 +2450,7 @@ def check_ready_intervals( python_env: t.Dict[str, Executable], dialect: DialectType = None, path: t.Optional[Path] = None, + snapshot: t.Optional[Snapshot] = None, kwargs: t.Optional[t.Dict] = None, ) -> Intervals: checked_intervals: Intervals = [] @@ -2436,6 +2466,7 @@ def check_ready_intervals( provided_args=(batch,), provided_kwargs=(kwargs or {}), context=context, + snapshot=snapshot, ) except Exception as ex: raise SignalEvalError(format_evaluated_code_exception(ex, python_env)) diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index 450d6f7408..2f8a68dd4a 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -496,6 +496,7 @@ def add_interval( start: TimeLike, end: TimeLike, is_dev: bool = False, + last_altered_ts: t.Optional[int] = None, ) -> None: """Add an interval to a snapshot and sync it to the store. @@ -504,6 +505,7 @@ def add_interval( start: The start of the interval to add. end: The end of the interval to add. is_dev: Indicates whether the given interval is being added while in development mode + last_altered_ts: The timestamp of the last modification of the physical table """ start_ts, end_ts = snapshot.inclusive_exclusive(start, end, strict=False, expand=False) if not snapshot.version: @@ -516,6 +518,8 @@ def add_interval( dev_version=snapshot.dev_version, intervals=intervals if not is_dev else [], dev_intervals=intervals if is_dev else [], + last_altered_ts=last_altered_ts if not is_dev else None, + dev_last_altered_ts=last_altered_ts if is_dev else None, ) self.add_snapshots_intervals([snapshot_intervals]) diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 29fc9f1740..3c23ef339c 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -381,8 +381,9 @@ def add_interval( start: TimeLike, end: TimeLike, is_dev: bool = False, + last_altered_ts: t.Optional[int] = None, ) -> None: - super().add_interval(snapshot, start, end, is_dev) + super().add_interval(snapshot, start, end, is_dev, last_altered_ts) @transactional() def add_snapshots_intervals(self, snapshots_intervals: t.Sequence[SnapshotIntervals]) -> None: diff --git a/sqlmesh/core/state_sync/db/interval.py b/sqlmesh/core/state_sync/db/interval.py index b15ad2d57b..8ccdc58fa0 100644 --- a/sqlmesh/core/state_sync/db/interval.py +++ b/sqlmesh/core/state_sync/db/interval.py @@ -60,6 +60,7 @@ def __init__( "is_removed": exp.DataType.build("boolean"), "is_compacted": exp.DataType.build("boolean"), "is_pending_restatement": exp.DataType.build("boolean"), + "last_altered_ts": exp.DataType.build("bigint"), } def add_snapshots_intervals(self, snapshots_intervals: t.Sequence[SnapshotIntervals]) -> None: @@ -215,13 +216,23 @@ def _push_snapshot_intervals( for start_ts, end_ts in snapshot.intervals: new_intervals.append( _interval_to_df( - snapshot, start_ts, end_ts, is_dev=False, is_compacted=is_compacted + snapshot, + start_ts, + end_ts, + is_dev=False, + is_compacted=is_compacted, + last_altered_ts=snapshot.last_altered_ts, ) ) for start_ts, end_ts in snapshot.dev_intervals: new_intervals.append( _interval_to_df( - snapshot, start_ts, end_ts, is_dev=True, is_compacted=is_compacted + snapshot, + start_ts, + end_ts, + is_dev=True, + is_compacted=is_compacted, + last_altered_ts=snapshot.dev_last_altered_ts, ) ) @@ -236,6 +247,7 @@ def _push_snapshot_intervals( is_dev=False, is_compacted=is_compacted, is_pending_restatement=True, + last_altered_ts=snapshot.last_altered_ts, ) ) @@ -284,6 +296,7 @@ def _get_snapshot_intervals( is_dev, is_removed, is_pending_restatement, + last_altered_ts, ) in rows: interval_ids.add(interval_id) merge_key = (name, version, dev_version, identifier) @@ -318,8 +331,10 @@ def _get_snapshot_intervals( else: if is_dev: intervals[merge_key].add_dev_interval(start, end) + intervals[merge_key].update_dev_last_altered_ts(last_altered_ts) else: intervals[merge_key].add_interval(start, end) + intervals[merge_key].update_last_altered_ts(last_altered_ts) # Remove all pending restatement intervals recorded before the current interval has been added intervals[ pending_restatement_interval_merge_key @@ -340,6 +355,7 @@ def _get_snapshot_intervals_query(self, uncompacted_only: bool) -> exp.Select: "is_dev", "is_removed", "is_pending_restatement", + "last_altered_ts", ) .from_(exp.to_table(self.intervals_table).as_("intervals")) .order_by( @@ -460,6 +476,7 @@ def _interval_to_df( is_removed: bool = False, is_compacted: bool = False, is_pending_restatement: bool = False, + last_altered_ts: t.Optional[int] = None, ) -> t.Dict[str, t.Any]: return { "id": random_id(), @@ -474,4 +491,5 @@ def _interval_to_df( "is_removed": is_removed, "is_compacted": is_compacted, "is_pending_restatement": is_pending_restatement, + "last_altered_ts": last_altered_ts, } diff --git a/sqlmesh/migrations/v0099_add_last_altered_to_intervals.py b/sqlmesh/migrations/v0099_add_last_altered_to_intervals.py new file mode 100644 index 0000000000..1a119a338d --- /dev/null +++ b/sqlmesh/migrations/v0099_add_last_altered_to_intervals.py @@ -0,0 +1,27 @@ +"""Add dev version to the intervals table.""" + +from sqlglot import exp + + +def migrate_schemas(state_sync, **kwargs): # type: ignore + engine_adapter = state_sync.engine_adapter + schema = state_sync.schema + intervals_table = "_intervals" + if schema: + intervals_table = f"{schema}.{intervals_table}" + + alter_table_exp = exp.Alter( + this=exp.to_table(intervals_table), + kind="TABLE", + actions=[ + exp.ColumnDef( + this=exp.to_column("last_altered_ts"), + kind=exp.DataType.build("BIGINT", dialect=engine_adapter.dialect), + ) + ], + ) + engine_adapter.execute(alter_table_exp) + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 5a708e1e4c..5190d26e98 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -10,6 +10,11 @@ from unittest import mock from unittest.mock import patch import logging +from IPython.utils.capture import capture_output + + +import time_machine +from pytest_mock.plugin import MockerFixture import numpy as np # noqa: TID253 import pandas as pd # noqa: TID253 @@ -45,6 +50,7 @@ TEST_SCHEMA, wait_until, ) +from tests.utils.test_helpers import use_terminal_console DATA_TYPE = exp.DataType.Type VARCHAR_100 = exp.DataType.build("varchar(100)") @@ -3774,7 +3780,7 @@ def _set_config(gateway: str, config: Config) -> None: ] -def test_materialized_view_evaluation(ctx: TestContext, mocker: MockerFixture): +def test_materialized_view_evaluation(ctx: TestContext): adapter = ctx.engine_adapter dialect = ctx.dialect @@ -3834,3 +3840,153 @@ def _assert_mview_value(value: int): assert any("Replacing view" in call[0][0] for call in mock_logger.call_args_list) _assert_mview_value(value=2) + + +@use_terminal_console +def test_external_model_freshness(ctx: TestContext, mocker: MockerFixture, tmp_path: pathlib.Path): + adapter = ctx.engine_adapter + if not adapter.SUPPORTS_METADATA_TABLE_LAST_MODIFIED_TS: + pytest.skip("This test only runs for engines that support metadata-based freshness") + + def _assert_snapshot_last_altered_ts( + context: Context, + snapshot_id: str, + last_altered_ts: datetime, + dev_last_altered_ts: t.Optional[datetime] = None, + ): + from sqlmesh.utils.date import to_datetime + + snapshot = context.state_sync.get_snapshots([snapshot_id])[snapshot_id] + + assert to_datetime(snapshot.last_altered_ts).replace( + microsecond=0 + ) == last_altered_ts.replace(microsecond=0) + + if dev_last_altered_ts: + assert to_datetime(snapshot.dev_last_altered_ts).replace( + microsecond=0 + ) == dev_last_altered_ts.replace(microsecond=0) + + import sqlmesh + + spy = mocker.spy(sqlmesh.core.snapshot.evaluator.SnapshotEvaluator, "evaluate") + + def _assert_model_evaluation(lambda_func, was_evaluated, day_delta=0): + spy.reset_mock() + timestamp = now(minute_floor=False) + timedelta(days=day_delta) + with time_machine.travel(timestamp, tick=False): + with capture_output() as output: + plan_or_run_result = lambda_func() + + evaluate_function_called = spy.call_count == 1 + signal_was_checked = "Checking signals for" in output.stdout + + assert signal_was_checked + if was_evaluated: + assert "All ready" in output.stdout + assert evaluate_function_called + else: + assert "None ready" in output.stdout + assert not evaluate_function_called + + return timestamp, plan_or_run_result + + # Create & initialize schema + schema = ctx.add_test_suffix(TEST_SCHEMA) + ctx._schemas.append(schema) + adapter.create_schema(schema) + + # Create & initialize external models + external_table1 = f"{schema}.external_table1" + external_table2 = f"{schema}.external_table2" + + external_models_yaml = tmp_path / "external_models.yaml" + external_models_yaml.write_text(f""" +- name: {external_table1} + columns: + col1: int + +- name: {external_table2} + columns: + col2: int +""") + + adapter.execute( + f"CREATE TABLE {external_table1} AS (SELECT 1 AS col1)", quote_identifiers=False + ) + adapter.execute( + f"CREATE TABLE {external_table2} AS (SELECT 2 AS col2)", quote_identifiers=False + ) + + # Create model that depends on external models + model_name = f"{schema}.new_model" + model_path = tmp_path / "models" / "new_model.sql" + (tmp_path / "models").mkdir(parents=True, exist_ok=True) + model_path.write_text(f""" + MODEL ( + name {model_name}, + start '2024-01-01', + kind FULL, + signals ( + freshness(), + ) + ); + + SELECT col1 * col2 AS col FROM {external_table1}, {external_table2}; + """) + + # Initialize context + def _set_config(gateway: str, config: Config) -> None: + config.model_defaults.dialect = ctx.dialect + + context = ctx.create_context(path=tmp_path, config_mutator=_set_config) + + # Case 1: Model is evaluated for the first plan + prod_plan_ts, prod_plan = _assert_model_evaluation( + lambda: context.plan(auto_apply=True, no_prompts=True), was_evaluated=True + ) + + prod_snapshot_id = next(iter(prod_plan.context_diff.new_snapshots)) + _assert_snapshot_last_altered_ts(context, prod_snapshot_id, last_altered_ts=prod_plan_ts) + + # Case 2: Model is NOT evaluated on run if external models are not fresh + _assert_model_evaluation(lambda: context.run(), was_evaluated=False, day_delta=1) + + # Case 3: Differentiate last_altered_ts between snapshots with shared version + # For instance, creating a FORWARD_ONLY change in dev (reusing the version but creating a dev preview) should not cause + # any side effects to the prod snapshot's last_altered_ts hydration + model_path.write_text(model_path.read_text().replace("col1 * col2", "col1 + col2")) + context.load() + dev_plan_ts = now(minute_floor=False) + timedelta(days=2) + with time_machine.travel(dev_plan_ts, tick=False): + dev_plan = context.plan( + environment="dev", forward_only=True, auto_apply=True, no_prompts=True + ) + + context.state_sync.clear_cache() + dev_snapshot_id = next(iter(dev_plan.context_diff.new_snapshots)) + _assert_snapshot_last_altered_ts( + context, + dev_snapshot_id, + last_altered_ts=prod_plan_ts, + dev_last_altered_ts=dev_plan_ts, + ) + _assert_snapshot_last_altered_ts(context, prod_snapshot_id, last_altered_ts=prod_plan_ts) + + # Case 4: Model is evaluated on run if any external model is fresh + adapter.execute(f"INSERT INTO {external_table2} (col2) VALUES (3)", quote_identifiers=False) + _assert_model_evaluation(lambda: context.run(), was_evaluated=True, day_delta=2) + + # Case 5: Model is evaluated if changed (case 3) even if the external model is not fresh + model_path.write_text(model_path.read_text().replace("col1 + col2", "col1 * col2 * 5")) + context.load() + _assert_model_evaluation( + lambda: context.plan(auto_apply=True, no_prompts=True), was_evaluated=True, day_delta=3 + ) + + # Case 6: Model is evaluated on a restatement plan even if the external model is not fresh + _assert_model_evaluation( + lambda: context.plan(restate_models=[model_name], auto_apply=True, no_prompts=True), + was_evaluated=True, + day_delta=4, + ) From 89e74bc4bd87e7ba76c3112c9896bb0d080a7ec1 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:04:29 +0100 Subject: [PATCH 0915/1056] chore: add dbt unit tests to fixture (#5451) --- .../fixtures/dbt/sushi_test/models/schema.yml | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/fixtures/dbt/sushi_test/models/schema.yml b/tests/fixtures/dbt/sushi_test/models/schema.yml index 87a201c418..21985f19ff 100644 --- a/tests/fixtures/dbt/sushi_test/models/schema.yml +++ b/tests/fixtures/dbt/sushi_test/models/schema.yml @@ -1,6 +1,55 @@ version: 2 models: + - name: simple_model_a + description: A simple model for testing + columns: + - name: a + data_type: int + unit_tests: + - name: test_simple_model_a_outputs_one + description: Test that simple_model_a outputs 1 as column a + model: simple_model_a + given: [] # No input models needed + expect: + format: csv + rows: | + a + 1 + - name: simple_model_b + description: Model that references simple_model_a + columns: + - name: a + data_type: int + unit_tests: + - name: test_simple_model_b_with_mock_input + description: Test simple_model_b with mocked simple_model_a input + model: simple_model_b + given: + - input: ref('simple_model_a') + format: csv + rows: | + a + 10 + 20 + 30 + expect: + format: csv + rows: | + a + 10 + 20 + 30 + - name: test_simple_model_b_with_sql_input + description: Test simple_model_b with SQL-defined input data + model: simple_model_b + given: + - input: ref('simple_model_a') + format: sql + rows: SELECT 42 AS a + expect: + format: sql + rows: SELECT 42 AS a - name: top_waiters description: description of top waiters columns: From e4cb63e6524ae1abe9c881299e290d6241af2849 Mon Sep 17 00:00:00 2001 From: Tori Wei <41123940+toriwei@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:46:26 -0700 Subject: [PATCH 0916/1056] fix: normalize audit canonical name (#5448) --- sqlmesh/dbt/test.py | 2 +- tests/dbt/test_config.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/test.py b/sqlmesh/dbt/test.py index 1bd8a8e6e2..7d8a369068 100644 --- a/sqlmesh/dbt/test.py +++ b/sqlmesh/dbt/test.py @@ -111,7 +111,7 @@ def _lowercase_name(cls, v: str) -> str: @property def canonical_name(self) -> str: - return f"{self.package_name}.{self.name}" if self.package_name else self.name + return f"{self.package_name}.{self.name}".lower() if self.package_name else self.name @property def is_standalone(self) -> bool: diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index c484b8e126..b3ee0c422a 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -245,6 +245,31 @@ def test_test_to_sqlmesh_fields(): assert audit.dialect == "bigquery" +def test_test_config_canonical_name(): + test_config_upper_case_package = TestConfig( + name="foo_test", + package_name="TEST_PACKAGE", + sql="SELECT 1", + ) + + assert test_config_upper_case_package.canonical_name == "test_package.foo_test" + + test_config_mixed_case_package = TestConfig( + name="Bar_Test", + package_name="MixedCase_Package", + sql="SELECT 1", + ) + + assert test_config_mixed_case_package.canonical_name == "mixedcase_package.bar_test" + + test_config_no_package = TestConfig( + name="foo_bar_test", + sql="SELECT 1", + ) + + assert test_config_no_package.canonical_name == "foo_bar_test" + + def test_singular_test_to_standalone_audit(dbt_dummy_postgres_config: PostgresConfig): sql = "SELECT * FROM FOO.BAR WHERE cost > 100" test_config = TestConfig( From 622fd16d4f23e1d5926025e15c00e03f35c9b5f1 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:55:44 -0500 Subject: [PATCH 0917/1056] Chore: add fields to dbt jinja target variable (#5449) --- sqlmesh/dbt/target.py | 17 +++++++++++++-- tests/dbt/test_transformation.py | 36 +++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/sqlmesh/dbt/target.py b/sqlmesh/dbt/target.py index f5fd119027..c53c818933 100644 --- a/sqlmesh/dbt/target.py +++ b/sqlmesh/dbt/target.py @@ -45,11 +45,24 @@ # We only serialize a subset of fields in order to avoid persisting sensitive information SERIALIZABLE_FIELDS = { - "type", + # core "name", - "database", "schema_", + "type", + "threads", + # snowflake + "database", "warehouse", + "user", + "role", + "account", + # postgres/redshift + "dbname", + "host", + "port", + # bigquery + "project", + "dataset", } SCHEMA_DIFFER_OVERRIDES = { diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 29651f9140..b9db817d29 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1023,8 +1023,41 @@ def test_target_jinja(sushi_test_project: Project): user="user", password="password", warehouse="warehouse", + role="role", + threads=1, ) + assert context.render("{{ target.threads }}") == "1" + assert context.render("{{ target.database }}") == "test" assert context.render("{{ target.warehouse }}") == "warehouse" + assert context.render("{{ target.user }}") == "user" + assert context.render("{{ target.role }}") == "role" + assert context.render("{{ target.account }}") == "account" + + context = DbtContext() + context._target = PostgresConfig( + name="target", + schema="test", + database="test", + dbname="test", + host="host", + port=5432, + user="user", + password="password", + ) + assert context.render("{{ target.dbname }}") == "test" + assert context.render("{{ target.host }}") == "host" + assert context.render("{{ target.port }}") == "5432" + + context = DbtContext() + context._target = BigQueryConfig( + name="target", + schema="test", + database="test", + project="project", + dataset="dataset", + ) + assert context.render("{{ target.project }}") == "project" + assert context.render("{{ target.dataset }}") == "dataset" @pytest.mark.xdist_group("dbt_manifest") @@ -1965,8 +1998,9 @@ def test_snapshot_json_payload(): assert snapshot_json["node"]["jinja_macros"]["global_objs"]["target"] == { "type": "duckdb", "name": "in_memory", - "schema": "sushi", "database": "memory", + "schema": "sushi", + "threads": 1, "target_name": "in_memory", } From a90db9a4e322bc4bc4e55311815d0b1eb8d71b79 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:19:14 -0500 Subject: [PATCH 0918/1056] Fix: pop 'begin' model config for all model kinds (#5453) --- sqlmesh/dbt/model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 9386b0b4f8..7d9a7e3348 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -656,6 +656,9 @@ def to_sqlmesh( # Set allow_partials to True for dbt models to preserve the original semantics. allow_partials = True + # pop begin for all models so we don't pass it through for non-incremental materializations + # (happens if model config is microbatch but project config overrides) + begin = model_kwargs.pop("begin", None) if kind.is_incremental: if self.batch_size and isinstance(self.batch_size, str): if "interval_unit" in model_kwargs: @@ -665,7 +668,7 @@ def to_sqlmesh( else: model_kwargs["interval_unit"] = self.batch_size self.batch_size = None - if begin := model_kwargs.pop("begin", None): + if begin: if "start" in model_kwargs: get_console().log_warning( f"Both 'begin' and 'start' are set for model '{self.canonical_name(context)}'. 'start' will be used." From cbcb6d2c3759b2fdc0e193153926eaf1c9f052e1 Mon Sep 17 00:00:00 2001 From: Tori Wei <41123940+toriwei@users.noreply.github.com> Date: Tue, 30 Sep 2025 08:03:04 -0700 Subject: [PATCH 0919/1056] fix: ignore partition_by field for ephemeral models (#5454) --- sqlmesh/dbt/model.py | 5 +++-- tests/dbt/test_transformation.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 7d9a7e3348..f6cb81f30f 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -510,10 +510,11 @@ def to_sqlmesh( physical_properties: t.Dict[str, t.Any] = {} if self.partition_by: - if isinstance(kind, ViewKind): + if isinstance(kind, (ViewKind, EmbeddedKind)): logger.warning( - "Ignoring partition_by config for model '%s'; partition_by is not supported for views.", + "Ignoring partition_by config for model '%s'; partition_by is not supported for %s.", self.name, + "views" if isinstance(kind, ViewKind) else "ephemeral models", ) else: partitioned_by = [] diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index b9db817d29..a640d620b7 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1728,6 +1728,18 @@ def test_partition_by(sushi_test_project: Project): ) assert model_config.to_sqlmesh(context).partitioned_by == [] + model_config = ModelConfig( + name="model", + alias="model", + schema="test", + package_name="package", + materialized=Materialization.EPHEMERAL.value, + unique_key="ds", + partition_by={"field": "ds", "granularity": "month"}, + sql="""SELECT 1 AS one, ds FROM foo""", + ) + assert model_config.to_sqlmesh(context).partitioned_by == [] + @pytest.mark.xdist_group("dbt_manifest") def test_partition_by_none(sushi_test_project: Project): From a255e1710f9a12aa8edc8aae8f8f0ba02af50777 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 30 Sep 2025 19:06:08 +0300 Subject: [PATCH 0920/1056] Feat(dbt): Add support for dbt custom materializations (#5435) Co-authored-by: Iaroslav Zeigerman --- sqlmesh/core/model/kind.py | 47 ++ sqlmesh/core/snapshot/evaluator.py | 205 ++++- sqlmesh/dbt/basemodel.py | 14 + sqlmesh/dbt/builtin.py | 1 + sqlmesh/dbt/manifest.py | 70 +- sqlmesh/dbt/model.py | 27 + sqlmesh/dbt/package.py | 17 +- sqlmesh/utils/jinja.py | 1 + tests/dbt/test_custom_materializations.py | 721 ++++++++++++++++++ tests/dbt/test_manifest.py | 2 +- tests/dbt/test_model.py | 11 + tests/dbt/test_transformation.py | 125 ++- .../materializations/custom_incremental.sql | 61 ++ .../models/custom_incremental_model.sql | 20 + .../models/custom_incremental_with_filter.sql | 9 + tests/fixtures/dbt/sushi_test/profiles.yml | 1 + 16 files changed, 1321 insertions(+), 11 deletions(-) create mode 100644 tests/dbt/test_custom_materializations.py create mode 100644 tests/fixtures/dbt/sushi_test/macros/materializations/custom_incremental.sql create mode 100644 tests/fixtures/dbt/sushi_test/models/custom_incremental_model.sql create mode 100644 tests/fixtures/dbt/sushi_test/models/custom_incremental_with_filter.sql diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index dc5f533c21..7b8e88ac17 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -119,6 +119,10 @@ def is_custom(self) -> bool: def is_managed(self) -> bool: return self.model_kind_name == ModelKindName.MANAGED + @property + def is_dbt_custom(self) -> bool: + return self.model_kind_name == ModelKindName.DBT_CUSTOM + @property def is_symbolic(self) -> bool: """A symbolic model is one that doesn't execute at all.""" @@ -170,6 +174,7 @@ class ModelKindName(str, ModelKindMixin, Enum): EXTERNAL = "EXTERNAL" CUSTOM = "CUSTOM" MANAGED = "MANAGED" + DBT_CUSTOM = "DBT_CUSTOM" @property def model_kind_name(self) -> t.Optional[ModelKindName]: @@ -887,6 +892,46 @@ def supports_python_models(self) -> bool: return False +class DbtCustomKind(_ModelKind): + name: t.Literal[ModelKindName.DBT_CUSTOM] = ModelKindName.DBT_CUSTOM + materialization: str + adapter: str = "default" + definition: str + dialect: t.Optional[str] = Field(None, validate_default=True) + + _dialect_validator = kind_dialect_validator + + @field_validator("materialization", "adapter", "definition", mode="before") + @classmethod + def _validate_fields(cls, v: t.Any) -> str: + return validate_string(v) + + @property + def data_hash_values(self) -> t.List[t.Optional[str]]: + return [ + *super().data_hash_values, + self.materialization, + self.definition, + self.adapter, + self.dialect, + ] + + def to_expression( + self, expressions: t.Optional[t.List[exp.Expression]] = None, **kwargs: t.Any + ) -> d.ModelKind: + return super().to_expression( + expressions=[ + *(expressions or []), + *_properties( + { + "materialization": exp.Literal.string(self.materialization), + "adapter": exp.Literal.string(self.adapter), + } + ), + ], + ) + + class EmbeddedKind(_ModelKind): name: t.Literal[ModelKindName.EMBEDDED] = ModelKindName.EMBEDDED @@ -992,6 +1037,7 @@ def to_expression( SCDType2ByColumnKind, CustomKind, ManagedKind, + DbtCustomKind, ], Field(discriminator="name"), ] @@ -1011,6 +1057,7 @@ def to_expression( ModelKindName.SCD_TYPE_2_BY_COLUMN: SCDType2ByColumnKind, ModelKindName.CUSTOM: CustomKind, ModelKindName.MANAGED: ManagedKind, + ModelKindName.DBT_CUSTOM: DbtCustomKind, } diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 70cc31b0a4..4ac87199c6 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -50,7 +50,7 @@ ViewKind, CustomKind, ) -from sqlmesh.core.model.kind import _Incremental +from sqlmesh.core.model.kind import _Incremental, DbtCustomKind from sqlmesh.utils import CompletionStatus, columns_to_types_all_known from sqlmesh.core.schema_diff import ( has_drop_alteration, @@ -67,7 +67,7 @@ SnapshotTableCleanupTask, ) from sqlmesh.core.snapshot.execution_tracker import QueryExecutionTracker -from sqlmesh.utils import random_id, CorrelationId +from sqlmesh.utils import random_id, CorrelationId, AttributeDict from sqlmesh.utils.concurrency import ( concurrent_apply_to_snapshots, concurrent_apply_to_values, @@ -83,6 +83,7 @@ format_additive_change_msg, AdditiveChangeError, ) +from sqlmesh.utils.jinja import MacroReturnVal if sys.version_info >= (3, 12): from importlib import metadata @@ -747,7 +748,10 @@ def _evaluate_snapshot( adapter.transaction(), adapter.session(snapshot.model.render_session_properties(**render_statements_kwargs)), ): - adapter.execute(model.render_pre_statements(**render_statements_kwargs)) + evaluation_strategy = _evaluation_strategy(snapshot, adapter) + evaluation_strategy.run_pre_statements( + snapshot=snapshot, render_kwargs=render_statements_kwargs + ) if not target_table_exists or (model.is_seed and not snapshot.intervals): # Only create the empty table if the columns were provided explicitly by the user @@ -817,7 +821,9 @@ def _evaluate_snapshot( batch_index=batch_index, ) - adapter.execute(model.render_post_statements(**render_statements_kwargs)) + evaluation_strategy.run_post_statements( + snapshot=snapshot, render_kwargs=render_statements_kwargs + ) return wap_id @@ -1433,7 +1439,9 @@ def _execute_create( "table_mapping": {snapshot.name: table_name}, } if run_pre_post_statements: - adapter.execute(snapshot.model.render_pre_statements(**create_render_kwargs)) + evaluation_strategy.run_pre_statements( + snapshot=snapshot, render_kwargs=create_render_kwargs + ) evaluation_strategy.create( table_name=table_name, model=snapshot.model, @@ -1445,7 +1453,9 @@ def _execute_create( physical_properties=rendered_physical_properties, ) if run_pre_post_statements: - adapter.execute(snapshot.model.render_post_statements(**create_render_kwargs)) + evaluation_strategy.run_post_statements( + snapshot=snapshot, render_kwargs=create_render_kwargs + ) def _can_clone(self, snapshot: Snapshot, deployability_index: DeployabilityIndex) -> bool: adapter = self.get_adapter(snapshot.model.gateway) @@ -1456,6 +1466,7 @@ def _can_clone(self, snapshot: Snapshot, deployability_index: DeployabilityIndex and adapter.SUPPORTS_CLONING # managed models cannot have their schema mutated because theyre based on queries, so clone + alter wont work and not snapshot.is_managed + and not snapshot.is_dbt_custom and not deployability_index.is_deployable(snapshot) # If the deployable table is missing we can't clone it and adapter.table_exists(snapshot.table_name()) @@ -1540,6 +1551,19 @@ def _evaluation_strategy(snapshot: SnapshotInfoLike, adapter: EngineAdapter) -> klass = ViewStrategy elif snapshot.is_scd_type_2: klass = SCDType2Strategy + elif snapshot.is_dbt_custom: + if hasattr(snapshot, "model") and isinstance( + (model_kind := snapshot.model.kind), DbtCustomKind + ): + return DbtCustomMaterializationStrategy( + adapter=adapter, + materialization_name=model_kind.materialization, + materialization_template=model_kind.definition, + ) + + raise SQLMeshError( + f"Expected DbtCustomKind for dbt custom materialization in model '{snapshot.name}'" + ) elif snapshot.is_custom: if snapshot.custom_materialization is None: raise SQLMeshError( @@ -1679,6 +1703,24 @@ def demote(self, view_name: str, **kwargs: t.Any) -> None: view_name: The name of the target view in the virtual layer. """ + @abc.abstractmethod + def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None: + """Executes the snapshot's pre statements. + + Args: + snapshot: The target snapshot. + render_kwargs: Additional key-value arguments to pass when rendering the statements. + """ + + @abc.abstractmethod + def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None: + """Executes the snapshot's post statements. + + Args: + snapshot: The target snapshot. + render_kwargs: Additional key-value arguments to pass when rendering the statements. + """ + class SymbolicStrategy(EvaluationStrategy): def insert( @@ -1740,6 +1782,12 @@ def promote( def demote(self, view_name: str, **kwargs: t.Any) -> None: pass + def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Dict[str, t.Any]) -> None: + pass + + def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Dict[str, t.Any]) -> None: + pass + class EmbeddedStrategy(SymbolicStrategy): def promote( @@ -1787,6 +1835,12 @@ def demote(self, view_name: str, **kwargs: t.Any) -> None: logger.info("Dropping view '%s'", view_name) self.adapter.drop_view(view_name, cascade=False) + def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None: + self.adapter.execute(snapshot.model.render_pre_statements(**render_kwargs)) + + def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None: + self.adapter.execute(snapshot.model.render_post_statements(**render_kwargs)) + class MaterializableStrategy(PromotableStrategy, abc.ABC): def create( @@ -2593,6 +2647,145 @@ def get_custom_materialization_type_or_raise( raise SQLMeshError(f"Custom materialization '{name}' not present in the Python environment") +class DbtCustomMaterializationStrategy(MaterializableStrategy): + def __init__( + self, + adapter: EngineAdapter, + materialization_name: str, + materialization_template: str, + ): + super().__init__(adapter) + self.materialization_name = materialization_name + self.materialization_template = materialization_template + + def create( + self, + table_name: str, + model: Model, + is_table_deployable: bool, + render_kwargs: t.Dict[str, t.Any], + **kwargs: t.Any, + ) -> None: + original_query = model.render_query_or_raise(**render_kwargs) + self._execute_materialization( + table_name=table_name, + query_or_df=original_query.limit(0), + model=model, + is_first_insert=True, + render_kwargs=render_kwargs, + create_only=True, + **kwargs, + ) + + def insert( + self, + table_name: str, + query_or_df: QueryOrDF, + model: Model, + is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], + **kwargs: t.Any, + ) -> None: + self._execute_materialization( + table_name=table_name, + query_or_df=query_or_df, + model=model, + is_first_insert=is_first_insert, + render_kwargs=render_kwargs, + **kwargs, + ) + + def append( + self, + table_name: str, + query_or_df: QueryOrDF, + model: Model, + render_kwargs: t.Dict[str, t.Any], + **kwargs: t.Any, + ) -> None: + return self.insert( + table_name, + query_or_df, + model, + is_first_insert=False, + render_kwargs=render_kwargs, + **kwargs, + ) + + def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None: + # in dbt custom materialisations it's up to the user when to run the pre hooks + pass + + def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None: + # in dbt custom materialisations it's up to the user when to run the post hooks + pass + + def _execute_materialization( + self, + table_name: str, + query_or_df: QueryOrDF, + model: Model, + is_first_insert: bool, + render_kwargs: t.Dict[str, t.Any], + create_only: bool = False, + **kwargs: t.Any, + ) -> None: + jinja_macros = model.jinja_macros + + # For vdes we need to use the table, since we don't know the schema/table at parse time + parts = exp.to_table(table_name, dialect=self.adapter.dialect) + + existing_globals = jinja_macros.global_objs + relation_info = existing_globals.get("this") + if isinstance(relation_info, dict): + relation_info["database"] = parts.catalog + relation_info["identifier"] = parts.name + relation_info["name"] = parts.name + + jinja_globals = { + **existing_globals, + "this": relation_info, + "database": parts.catalog, + "schema": parts.db, + "identifier": parts.name, + "target": existing_globals.get("target", {"type": self.adapter.dialect}), + "execution_dt": kwargs.get("execution_time"), + "engine_adapter": self.adapter, + "sql": str(query_or_df), + "is_first_insert": is_first_insert, + "create_only": create_only, + # FIXME: Add support for transaction=False + "pre_hooks": [ + AttributeDict({"sql": s.this.this, "transaction": True}) + for s in model.pre_statements + ], + "post_hooks": [ + AttributeDict({"sql": s.this.this, "transaction": True}) + for s in model.post_statements + ], + "model_instance": model, + **kwargs, + } + + try: + jinja_env = jinja_macros.build_environment(**jinja_globals) + template = jinja_env.from_string(self.materialization_template) + + try: + template.render() + except MacroReturnVal as ret: + # this is a successful return from a macro call (dbt uses this list of Relations to update their relation cache) + returned_relations = ret.value.get("relations", []) + logger.info( + f"Materialization {self.materialization_name} returned relations: {returned_relations}" + ) + + except Exception as e: + raise SQLMeshError( + f"Failed to execute dbt materialization '{self.materialization_name}': {e}" + ) from e + + class EngineManagedStrategy(MaterializableStrategy): def create( self, diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index 4dcf44a0af..0b75955129 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -57,6 +57,12 @@ class Materialization(str, Enum): # Snowflake, https://docs.getdbt.com/reference/resource-configs/snowflake-configs#dynamic-tables DYNAMIC_TABLE = "dynamic_table" + CUSTOM = "custom" + + @classmethod + def _missing_(cls, value): # type: ignore + return cls.CUSTOM + class SnapshotStrategy(str, Enum): """DBT snapshot strategies""" @@ -295,6 +301,14 @@ def sqlmesh_model_kwargs( # precisely which variables are referenced in the model dependencies.variables |= set(context.variables) + if ( + getattr(self, "model_materialization", None) == Materialization.CUSTOM + and hasattr(self, "_get_custom_materialization") + and (custom_mat := self._get_custom_materialization(context)) + ): + # include custom materialization dependencies as they might use macros + dependencies = dependencies.union(custom_mat.dependencies) + model_dialect = self.dialect(context) model_context = context.context_for_dependencies( dependencies.union(self.tests_ref_source_dependencies) diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index e284c11797..b8180bc011 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -546,6 +546,7 @@ def create_builtin_globals( "statement": sql_execution.statement, "graph": adapter.graph, "selected_resources": list(jinja_globals.get("selected_models") or []), + "write": lambda input: None, # We don't support writing yet } ) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 0e33569888..17c5e91700 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -47,7 +47,7 @@ from sqlmesh.dbt.builtin import BUILTIN_FILTERS, BUILTIN_GLOBALS, OVERRIDDEN_MACROS from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.model import ModelConfig -from sqlmesh.dbt.package import HookConfig, MacroConfig +from sqlmesh.dbt.package import HookConfig, MacroConfig, MaterializationConfig from sqlmesh.dbt.seed import SeedConfig from sqlmesh.dbt.source import SourceConfig from sqlmesh.dbt.target import TargetConfig @@ -75,6 +75,7 @@ SourceConfigs = t.Dict[str, SourceConfig] MacroConfigs = t.Dict[str, MacroConfig] HookConfigs = t.Dict[str, HookConfig] +MaterializationConfigs = t.Dict[str, MaterializationConfig] IGNORED_PACKAGES = {"elementary"} @@ -135,6 +136,7 @@ def __init__( self._on_run_start_per_package: t.Dict[str, HookConfigs] = defaultdict(dict) self._on_run_end_per_package: t.Dict[str, HookConfigs] = defaultdict(dict) + self._materializations: MaterializationConfigs = {} def tests(self, package_name: t.Optional[str] = None) -> TestConfigs: self._load_all() @@ -164,6 +166,10 @@ def on_run_end(self, package_name: t.Optional[str] = None) -> HookConfigs: self._load_all() return self._on_run_end_per_package[package_name or self._project_name] + def materializations(self) -> MaterializationConfigs: + self._load_all() + return self._materializations + @property def all_macros(self) -> t.Dict[str, t.Dict[str, MacroInfo]]: self._load_all() @@ -213,6 +219,7 @@ def _load_all(self) -> None: self._calls = {k: (v, False) for k, v in (self._call_cache.get("") or {}).items()} self._load_macros() + self._load_materializations() self._load_sources() self._load_tests() self._load_models_and_seeds() @@ -250,11 +257,14 @@ def _load_sources(self) -> None: def _load_macros(self) -> None: for macro in self._manifest.macros.values(): + if macro.name.startswith("materialization_"): + continue + if macro.name.startswith("test_"): macro.macro_sql = _convert_jinja_test_to_macro(macro.macro_sql) dependencies = Dependencies(macros=_macro_references(self._manifest, macro)) - if not macro.name.startswith("materialization_") and not macro.name.startswith("test_"): + if not macro.name.startswith("test_"): dependencies = dependencies.union( self._extra_dependencies(macro.macro_sql, macro.package_name) ) @@ -281,6 +291,32 @@ def _load_macros(self) -> None: if pos > 0 and name[pos + 2 :] in adapter_macro_names: macro_config.info.is_top_level = True + def _load_materializations(self) -> None: + for macro in self._manifest.macros.values(): + if macro.name.startswith("materialization_"): + # Extract name and adapter ( "materialization_{name}_{adapter}" or "materialization_{name}_default") + name_parts = macro.name.split("_") + if len(name_parts) >= 3: + mat_name = "_".join(name_parts[1:-1]) + adapter = name_parts[-1] + + dependencies = Dependencies(macros=_macro_references(self._manifest, macro)) + macro.macro_sql = _strip_jinja_materialization_tags(macro.macro_sql) + dependencies = dependencies.union( + self._extra_dependencies(macro.macro_sql, macro.package_name) + ) + + materialization_config = MaterializationConfig( + name=mat_name, + adapter=adapter, + definition=macro.macro_sql, + dependencies=dependencies, + path=Path(macro.original_file_path), + ) + + key = f"{mat_name}_{adapter}" + self._materializations[key] = materialization_config + def _load_tests(self) -> None: for node in self._manifest.nodes.values(): if node.resource_type != "test": @@ -359,6 +395,12 @@ def _load_models_and_seeds(self) -> None: dependencies = dependencies.union( self._extra_dependencies(sql, node.package_name, track_all_model_attrs=True) ) + for hook in [*node_config.get("pre-hook", []), *node_config.get("post-hook", [])]: + dependencies = dependencies.union( + self._extra_dependencies( + hook["sql"], node.package_name, track_all_model_attrs=True + ) + ) dependencies = dependencies.union( self._flatten_dependencies_from_macros(dependencies.macros, node.package_name) ) @@ -732,3 +774,27 @@ def _convert_jinja_test_to_macro(test_jinja: str) -> str: macro = macro_tag + test_jinja[match.span()[-1] :] return re.sub(ENDTEST_REGEX, lambda m: m.group(0).replace("endtest", "endmacro"), macro) + + +def _strip_jinja_materialization_tags(materialization_jinja: str) -> str: + MATERIALIZATION_TAG_REGEX = r"\s*{%-?\s*materialization\s+[^%]*%}\s*\n?" + ENDMATERIALIZATION_REGEX = r"{%-?\s*endmaterialization\s*-?%}\s*\n?" + + if not re.match(MATERIALIZATION_TAG_REGEX, materialization_jinja): + return materialization_jinja + + materialization_jinja = re.sub( + MATERIALIZATION_TAG_REGEX, + "", + materialization_jinja, + flags=re.IGNORECASE, + ) + + materialization_jinja = re.sub( + ENDMATERIALIZATION_REGEX, + "", + materialization_jinja, + flags=re.IGNORECASE, + ) + + return materialization_jinja.strip() diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index f6cb81f30f..f47283d06e 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -31,6 +31,7 @@ OnAdditiveChange, on_destructive_change_validator, on_additive_change_validator, + DbtCustomKind, ) from sqlmesh.dbt.basemodel import BaseModelConfig, Materialization, SnapshotStrategy from sqlmesh.dbt.common import SqlStr, sql_str_validator @@ -40,6 +41,7 @@ if t.TYPE_CHECKING: from sqlmesh.core.audit.definition import ModelAudit from sqlmesh.dbt.context import DbtContext + from sqlmesh.dbt.package import MaterializationConfig logger = logging.getLogger(__name__) @@ -444,6 +446,19 @@ def model_kind(self, context: DbtContext) -> ModelKind: if materialization == Materialization.DYNAMIC_TABLE: return ManagedKind() + if materialization == Materialization.CUSTOM: + if custom_materialization := self._get_custom_materialization(context): + return DbtCustomKind( + materialization=self.materialized, + adapter=custom_materialization.adapter, + dialect=self.dialect(context), + definition=custom_materialization.definition, + ) + + raise ConfigError( + f"Unknown materialization '{self.materialized}'. Custom materializations must be defined in your dbt project." + ) + raise ConfigError(f"{materialization.value} materialization not supported.") def _big_query_partition_by_expr(self, context: DbtContext) -> exp.Expression: @@ -483,6 +498,18 @@ def _big_query_partition_by_expr(self, context: DbtContext) -> exp.Expression: dialect="bigquery", ) + def _get_custom_materialization(self, context: DbtContext) -> t.Optional[MaterializationConfig]: + materializations = context.manifest.materializations() + name, target_adapter = self.materialized, context.target.dialect + + adapter_specific_key = f"{name}_{target_adapter}" + default_key = f"{name}_default" + if adapter_specific_key in materializations: + return materializations[adapter_specific_key] + if default_key in materializations: + return materializations[default_key] + return None + @property def sqlmesh_config_fields(self) -> t.Set[str]: return super().sqlmesh_config_fields | { diff --git a/sqlmesh/dbt/package.py b/sqlmesh/dbt/package.py index 420cf3cb73..dbaa832c22 100644 --- a/sqlmesh/dbt/package.py +++ b/sqlmesh/dbt/package.py @@ -37,6 +37,16 @@ class HookConfig(PydanticModel): dependencies: Dependencies +class MaterializationConfig(PydanticModel): + """Class to contain custom materialization configuration.""" + + name: str + adapter: str + definition: str + dependencies: Dependencies + path: Path + + class Package(PydanticModel): """Class to contain package configuration""" @@ -47,6 +57,7 @@ class Package(PydanticModel): models: t.Dict[str, ModelConfig] variables: t.Dict[str, t.Any] macros: t.Dict[str, MacroConfig] + materializations: t.Dict[str, MaterializationConfig] on_run_start: t.Dict[str, HookConfig] on_run_end: t.Dict[str, HookConfig] files: t.Set[Path] @@ -94,6 +105,7 @@ def load(self, package_root: Path) -> Package: models = _fix_paths(self._context.manifest.models(package_name), package_root) seeds = _fix_paths(self._context.manifest.seeds(package_name), package_root) macros = _fix_paths(self._context.manifest.macros(package_name), package_root) + materializations = _fix_paths(self._context.manifest.materializations(), package_root) on_run_start = _fix_paths(self._context.manifest.on_run_start(package_name), package_root) on_run_end = _fix_paths(self._context.manifest.on_run_end(package_name), package_root) sources = self._context.manifest.sources(package_name) @@ -114,13 +126,16 @@ def load(self, package_root: Path) -> Package: seeds=seeds, variables=package_variables, macros=macros, + materializations=materializations, files=config_paths, on_run_start=on_run_start, on_run_end=on_run_end, ) -T = t.TypeVar("T", TestConfig, ModelConfig, MacroConfig, SeedConfig, HookConfig) +T = t.TypeVar( + "T", TestConfig, ModelConfig, MacroConfig, MaterializationConfig, SeedConfig, HookConfig +) def _fix_paths(configs: t.Dict[str, T], package_root: Path) -> t.Dict[str, T]: diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index 508c6dce2d..59e9f6dd2f 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -369,6 +369,7 @@ def build_environment(self, **kwargs: t.Any) -> Environment: context.update(builtin_globals) context.update(root_macros) context.update(package_macros) + context["render"] = lambda input: env.from_string(input).render() env.globals.update(context) env.filters.update(self._environment.filters) diff --git a/tests/dbt/test_custom_materializations.py b/tests/dbt/test_custom_materializations.py new file mode 100644 index 0000000000..9e7a94315c --- /dev/null +++ b/tests/dbt/test_custom_materializations.py @@ -0,0 +1,721 @@ +from __future__ import annotations + +import typing as t +from pathlib import Path + +import pytest + +from sqlmesh import Context +from sqlmesh.core.config import ModelDefaultsConfig +from sqlmesh.core.model.kind import DbtCustomKind +from sqlmesh.dbt.context import DbtContext +from sqlmesh.dbt.manifest import ManifestHelper +from sqlmesh.dbt.model import ModelConfig +from sqlmesh.dbt.profile import Profile +from sqlmesh.dbt.basemodel import Materialization + +pytestmark = pytest.mark.dbt + + +@pytest.mark.xdist_group("dbt_manifest") +def test_custom_materialization_manifest_loading(): + project_path = Path("tests/fixtures/dbt/sushi_test") + profile = Profile.load(DbtContext(project_path)) + + helper = ManifestHelper( + project_path, + project_path, + "sushi", + profile.target, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), + ) + materializations = helper.materializations() + + # custom materialization should have loaded from the manifest + assert "custom_incremental_default" in materializations + custom_incremental = materializations["custom_incremental_default"] + assert custom_incremental.name == "custom_incremental" + assert custom_incremental.adapter == "default" + assert "make_temp_relation(new_relation)" in custom_incremental.definition + assert "run_hooks(pre_hooks)" in custom_incremental.definition + assert " {{ return({'relations': [new_relation]}) }}" in custom_incremental.definition + + +@pytest.mark.xdist_group("dbt_manifest") +def test_custom_materialization_model_config(): + project_path = Path("tests/fixtures/dbt/sushi_test") + profile = Profile.load(DbtContext(project_path)) + + helper = ManifestHelper( + project_path, + project_path, + "sushi", + profile.target, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), + ) + + models = helper.models() + + custom_model = models["custom_incremental_model"] + assert isinstance(custom_model, ModelConfig) + assert custom_model.materialized == "custom_incremental" + assert custom_model.model_materialization == Materialization.CUSTOM + + # pre and post hooks should also be handled in custom materializations + assert len(custom_model.pre_hook) == 2 + assert ( + custom_model.pre_hook[1].sql + == "CREATE TABLE IF NOT EXISTS hook_table (id INTEGER, length_col TEXT, updated_at TIMESTAMP)" + ) + assert len(custom_model.post_hook) == 2 + assert "COALESCE(MAX(id), 0)" in custom_model.post_hook[1].sql + + custom_filter_model = models["custom_incremental_with_filter"] + assert isinstance(custom_filter_model, ModelConfig) + assert custom_filter_model.materialized == "custom_incremental" + assert custom_filter_model.model_materialization == Materialization.CUSTOM + assert custom_filter_model.interval == "2 day" + assert custom_filter_model.time_column == "created_at" + + # verify also that the global hooks are inherited in the model without + assert len(custom_filter_model.pre_hook) == 1 + assert len(custom_filter_model.post_hook) == 1 + + +@pytest.mark.xdist_group("dbt_manifest") +def test_custom_materialization_model_kind(): + project_path = Path("tests/fixtures/dbt/sushi_test") + context = DbtContext(project_path) + profile = Profile.load(DbtContext(project_path)) + + helper = ManifestHelper( + project_path, + project_path, + "sushi", + profile.target, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), + ) + + context._target = profile.target + context._manifest = helper + models = helper.models() + + # custom materialization models get DbtCustomKind populated + custom_model = models["custom_incremental_model"] + kind = custom_model.model_kind(context) + assert isinstance(kind, DbtCustomKind) + assert kind.materialization == "custom_incremental" + assert kind.adapter == "default" + assert "create_table_as" in kind.definition + + custom_filter_model = models["custom_incremental_with_filter"] + kind = custom_filter_model.model_kind(context) + assert isinstance(kind, DbtCustomKind) + assert kind.materialization == "custom_incremental" + assert kind.adapter == "default" + assert "run_hooks" in kind.definition + + # the DbtCustomKind shouldnt be set for normal strategies + regular_model = models["simple_model_a"] + regular_kind = regular_model.model_kind(context) + assert not isinstance(regular_kind, DbtCustomKind) + + # verify in sqlmesh as well + sqlmesh_context = Context( + paths=["tests/fixtures/dbt/sushi_test"], + config=None, + ) + + custom_incremental = sqlmesh_context.get_model("sushi.custom_incremental_model") + assert isinstance(custom_incremental.kind, DbtCustomKind) + assert custom_incremental.kind.materialization == "custom_incremental" + + custom_with_filter = sqlmesh_context.get_model("sushi.custom_incremental_with_filter") + assert isinstance(custom_with_filter.kind, DbtCustomKind) + assert custom_with_filter.kind.materialization == "custom_incremental" + + +@pytest.mark.xdist_group("dbt_manifest") +def test_custom_materialization_dependencies(): + project_path = Path("tests/fixtures/dbt/sushi_test") + context = DbtContext(project_path) + profile = Profile.load(DbtContext(project_path)) + + helper = ManifestHelper( + project_path, + project_path, + "sushi", + profile.target, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), + ) + + context._target = profile.target + context._manifest = helper + models = helper.models() + + # custom materialization uses macros that should appear in dependencies + for model_name in ["custom_incremental_model", "custom_incremental_with_filter"]: + materialization_deps = models[model_name]._get_custom_materialization(context) + assert materialization_deps is not None + assert len(materialization_deps.dependencies.macros) > 0 + macro_names = [macro.name for macro in materialization_deps.dependencies.macros] + expected_macros = [ + "build_incremental_filter_sql", + "Relation", + "create_table_as", + "make_temp_relation", + "run_hooks", + "statement", + ] + assert any(macro in macro_names for macro in expected_macros) + + +@pytest.mark.xdist_group("dbt_manifest") +def test_adapter_specific_materialization_override(copy_to_temp_path: t.Callable): + path = copy_to_temp_path("tests/fixtures/dbt/sushi_test") + temp_project = path[0] + + macros_dir = temp_project / "macros" / "materializations" + macros_dir.mkdir(parents=True, exist_ok=True) + + adapter_mat_content = """ +{%- materialization custom_adapter_test, default -%} + {%- set new_relation = api.Relation.create(database=database, schema=schema, identifier=identifier) -%} + + {{ run_hooks(pre_hooks, inside_transaction=False) }} + + {%- call statement('main') -%} + CREATE TABLE {{ new_relation }} AS ( + SELECT 'default_adapter' as adapter_type, * FROM ({{ sql }}) AS subquery + ) + {%- endcall -%} + + {{ run_hooks(post_hooks, inside_transaction=False) }} + + {{ return({'relations': [new_relation]}) }} +{%- endmaterialization -%} + +{%- materialization custom_adapter_test, adapter='postgres' -%} + {%- set new_relation = api.Relation.create(database=database, schema=schema, identifier=identifier) -%} + + {{ run_hooks(pre_hooks, inside_transaction=False) }} + + {%- call statement('main') -%} + CREATE TABLE {{ new_relation }} AS ( + SELECT 'postgres_adapter'::text as adapter_type, * FROM ({{ sql }}) AS subquery + ) + {%- endcall -%} + + {{ run_hooks(post_hooks, inside_transaction=False) }} + + {{ return({'relations': [new_relation]}) }} +{%- endmaterialization -%} + +{%- materialization custom_adapter_test, adapter='duckdb' -%} + {%- set new_relation = api.Relation.create(database=database, schema=schema, identifier=identifier) -%} + + {{ run_hooks(pre_hooks, inside_transaction=False) }} + + {%- call statement('main') -%} + CREATE TABLE {{ new_relation }} AS ( + SELECT 'duckdb_adapter' as adapter_type, * FROM ({{ sql }}) AS subquery + ) + {%- endcall -%} + + {{ run_hooks(post_hooks, inside_transaction=False) }} + + {{ return({'relations': [new_relation]}) }} +{%- endmaterialization -%} +""".strip() + + (macros_dir / "custom_adapter_test.sql").write_text(adapter_mat_content) + + models_dir = temp_project / "models" + models_dir.mkdir(parents=True, exist_ok=True) + + test_model_content = """ +{{ config( + materialized='custom_adapter_test', +) }} + +SELECT + 1 as id, + 'test' as name +""".strip() + + (models_dir / "test_adapter_specific.sql").write_text(test_model_content) + + context = DbtContext(temp_project) + profile = Profile.load(context) + + helper = ManifestHelper( + temp_project, + temp_project, + "sushi", + profile.target, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), + ) + + materializations = helper.materializations() + assert "custom_adapter_test_default" in materializations + assert "custom_adapter_test_duckdb" in materializations + assert "custom_adapter_test_postgres" in materializations + + default_mat = materializations["custom_adapter_test_default"] + assert "default_adapter" in default_mat.definition + assert default_mat.adapter == "default" + + duckdb_mat = materializations["custom_adapter_test_duckdb"] + assert "duckdb_adapter" in duckdb_mat.definition + assert duckdb_mat.adapter == "duckdb" + + postgres_mat = materializations["custom_adapter_test_postgres"] + assert "postgres_adapter" in postgres_mat.definition + assert postgres_mat.adapter == "postgres" + + # verify that the correct adapter is selected based on target + context._target = profile.target + context._manifest = helper + models = helper.models() + + test_model = models["test_adapter_specific"] + + kind = test_model.model_kind(context) + assert isinstance(kind, DbtCustomKind) + assert kind.materialization == "custom_adapter_test" + # Should use duckdb adapter since that's the default target + assert "duckdb_adapter" in kind.definition or "default_adapter" in kind.definition + + # test also that adapter-specific materializations execute with correct adapter + sushi_context = Context(paths=path) + + plan = sushi_context.plan(select_models=["sushi.test_adapter_specific"]) + sushi_context.apply(plan) + + # check that the table was created with the correct adapter type + result = sushi_context.engine_adapter.fetchdf("SELECT * FROM sushi.test_adapter_specific") + assert len(result) == 1 + assert "adapter_type" in result.columns + assert result["adapter_type"][0] == "duckdb_adapter" + assert result["id"][0] == 1 + assert result["name"][0] == "test" + + +@pytest.mark.xdist_group("dbt_manifest") +def test_missing_custom_materialization_error(): + from sqlmesh.utils.errors import ConfigError + + project_path = Path("tests/fixtures/dbt/sushi_test") + context = DbtContext(project_path) + profile = Profile.load(context) + + # the materialization is non-existent + fake_model_config = ModelConfig( + name="test_model", + path=project_path / "models" / "fake_model.sql", + raw_code="SELECT 1 as id", + materialized="non_existent_custom", + schema="test_schema", + ) + + context._target = profile.target + helper = ManifestHelper( + project_path, + project_path, + "sushi", + profile.target, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), + ) + context._manifest = helper + + # Should raise ConfigError when trying to get the model kind + with pytest.raises(ConfigError) as e: + fake_model_config.model_kind(context) + + assert "Unknown materialization 'non_existent_custom'" in str(e.value) + assert "Custom materializations must be defined" in str(e.value) + + +@pytest.mark.xdist_group("dbt_manifest") +def test_broken_jinja_materialization_error(copy_to_temp_path: t.Callable): + path = copy_to_temp_path("tests/fixtures/dbt/sushi_test") + temp_project = path[0] + + macros_dir = temp_project / "macros" / "materializations" + macros_dir.mkdir(parents=True, exist_ok=True) + + # Create broken Jinja materialization + broken_mat_content = """ +{%- materialization broken_jinja, default -%} + {%- set new_relation = api.Relation.create(database=database, schema=schema, identifier=identifier) -%} + + {{ run_hooks(pre_hooks, inside_transaction=False) }} + + {# An intentional undefined variable that will cause runtime error #} + {%- set broken_var = undefined_variable_that_does_not_exist + 10 -%} + + {%- call statement('main') -%} + CREATE TABLE {{ new_relation }} AS ( + SELECT * FROM ({{ sql }}) AS subquery + WHERE 1 = {{ broken_var }} + ) + {%- endcall -%} + + {{ run_hooks(post_hooks, inside_transaction=False) }} + + {{ return({'relations': [new_relation]}) }} +{%- endmaterialization -%} +""".strip() + + (macros_dir / "broken_jinja.sql").write_text(broken_mat_content) + + models_dir = temp_project / "models" + models_dir.mkdir(parents=True, exist_ok=True) + + test_model_content = """ +{{ config( + materialized='broken_jinja', +) }} + +SELECT + 1 as id, + 'This should fail with Jinja error' as error_msg +""".strip() + + (models_dir / "test_broken_jinja.sql").write_text(test_model_content) + + sushi_context = Context(paths=path) + + # The model will load fine jinja won't fail at parse time + model = sushi_context.get_model("sushi.test_broken_jinja") + assert isinstance(model.kind, DbtCustomKind) + assert model.kind.materialization == "broken_jinja" + + # but execution should fail + with pytest.raises(Exception) as e: + plan = sushi_context.plan(select_models=["sushi.test_broken_jinja"]) + sushi_context.apply(plan) + + assert "plan application failed" in str(e.value).lower() + + +@pytest.mark.xdist_group("dbt_manifest") +def test_failing_hooks_in_materialization(copy_to_temp_path: t.Callable): + path = copy_to_temp_path("tests/fixtures/dbt/sushi_test") + temp_project = path[0] + + models_dir = temp_project / "models" + models_dir.mkdir(parents=True, exist_ok=True) + + test_model_content = """ +{{ config( + materialized='custom_incremental', + pre_hook="CREATE TABLE will_fail_due_to_intentional_syntax_error (", + post_hook="DROP TABLE non_existent_table_that_will_fail", +) }} + +SELECT + 1 as id, + 'Testing hook failures' as test_msg +""".strip() + + (models_dir / "test_failing_hooks.sql").write_text(test_model_content) + + sushi_context = Context(paths=[str(temp_project)]) + + # in this case the pre_hook has invalid syntax + with pytest.raises(Exception) as e: + plan = sushi_context.plan(select_models=["sushi.test_failing_hooks"]) + sushi_context.apply(plan) + + assert "plan application failed" in str(e.value).lower() + + +@pytest.mark.xdist_group("dbt_manifest") +def test_custom_materialization_virtual_environments(copy_to_temp_path: t.Callable): + path = copy_to_temp_path("tests/fixtures/dbt/sushi_test") + temp_project = path[0] + + models_dir = temp_project / "models" + models_dir.mkdir(parents=True, exist_ok=True) + + test_model_content = """ +{{ config( + materialized='custom_incremental', + time_column='created_at', +) }} + +SELECT + CURRENT_TIMESTAMP as created_at, + 1 as id, + 'venv_test' as test_type +""".strip() + + (models_dir / "test_venv_model.sql").write_text(test_model_content) + + sushi_context = Context(paths=path) + prod_plan = sushi_context.plan(select_models=["sushi.test_venv_model"]) + sushi_context.apply(prod_plan) + prod_result = sushi_context.engine_adapter.fetchdf( + "SELECT * FROM sushi.test_venv_model ORDER BY id" + ) + assert len(prod_result) == 1 + assert prod_result["id"][0] == 1 + assert prod_result["test_type"][0] == "venv_test" + + # Create dev environment and check the dev table was created with proper naming + dev_plan = sushi_context.plan("dev", select_models=["sushi.test_venv_model"]) + sushi_context.apply(dev_plan) + dev_result = sushi_context.engine_adapter.fetchdf( + "SELECT * FROM sushi__dev.test_venv_model ORDER BY id" + ) + assert len(dev_result) == 1 + assert dev_result["id"][0] == 1 + assert dev_result["test_type"][0] == "venv_test" + + dev_tables = sushi_context.engine_adapter.fetchdf(""" + SELECT table_name, table_schema + FROM system.information_schema.tables + WHERE table_schema LIKE 'sushi%dev%' + AND table_name LIKE '%test_venv_model%' + """) + + prod_tables = sushi_context.engine_adapter.fetchdf(""" + SELECT table_name, table_schema + FROM system.information_schema.tables + WHERE table_schema = 'sushi' + AND table_name LIKE '%test_venv_model%' + """) + + # Verify both environments have their own tables + assert len(dev_tables) >= 1 + assert len(prod_tables) >= 1 + + +@pytest.mark.xdist_group("dbt_manifest") +def test_virtual_environment_schema_names(copy_to_temp_path: t.Callable): + path = copy_to_temp_path("tests/fixtures/dbt/sushi_test") + temp_project = path[0] + + models_dir = temp_project / "models" + models_dir.mkdir(parents=True, exist_ok=True) + + test_model_content = """ +{{ config( + materialized='custom_incremental', + time_column='created_at', +) }} + +SELECT + CURRENT_TIMESTAMP as created_at, + 1 as id, + 'schema_naming_test' as test_type +""".strip() + + (models_dir / "test_schema_naming.sql").write_text(test_model_content) + + context = Context(paths=path) + prod_plan = context.plan(select_models=["sushi.test_schema_naming"]) + context.apply(prod_plan) + + dev_plan = context.plan("dev", select_models=["sushi.test_schema_naming"]) + context.apply(dev_plan) + + prod_result = context.engine_adapter.fetchdf( + "SELECT * FROM sushi.test_schema_naming ORDER BY id" + ) + assert len(prod_result) == 1 + assert prod_result["test_type"][0] == "schema_naming_test" + + dev_result = context.engine_adapter.fetchdf( + "SELECT * FROM sushi__dev.test_schema_naming ORDER BY id" + ) + assert len(dev_result) == 1 + assert dev_result["test_type"][0] == "schema_naming_test" + + # to examine the schema structure + all_schemas_query = """ + SELECT DISTINCT table_schema, COUNT(*) as table_count + FROM system.information_schema.tables + WHERE table_schema LIKE '%sushi%' + AND table_name LIKE '%test_schema_naming%' + GROUP BY table_schema + ORDER BY table_schema + """ + + schema_info = context.engine_adapter.fetchdf(all_schemas_query) + + schema_names = schema_info["table_schema"].tolist() + + # - virtual schemas: sushi, sushi__dev (for views) + view_schemas = [s for s in schema_names if not s.startswith("sqlmesh__")] + + # - physical schema: sqlmesh__sushi (for actual data tables) + physical_schemas = [s for s in schema_names if s.startswith("sqlmesh__")] + + # verify we got both of them + assert len(view_schemas) >= 2 + assert len(physical_schemas) >= 1 + assert "sushi" in view_schemas + assert "sushi__dev" in view_schemas + assert any("sqlmesh__sushi" in s for s in physical_schemas) + + +@pytest.mark.xdist_group("dbt_manifest") +def test_custom_materialization_lineage_tracking(copy_to_temp_path: t.Callable): + path = copy_to_temp_path("tests/fixtures/dbt/sushi_test") + temp_project = path[0] + + models_dir = temp_project / "models" + models_dir.mkdir(parents=True, exist_ok=True) + + # create a custom materialization model that depends on simple_model_a and waiter_names seed + lineage_model_content = """ +{{ config( + materialized='custom_incremental', + time_column='created_at', +) }} + +SELECT + CURRENT_TIMESTAMP as created_at, + w.id as waiter_id, + w.name as waiter_name, + s.a as simple_value, + w.id * s.a as computed_value, + 'lineage_test' as model_type +FROM {{ ref('waiter_names') }} w +CROSS JOIN {{ ref('simple_model_a') }} s +""".strip() + + (models_dir / "enhanced_waiter_data.sql").write_text(lineage_model_content) + + # Create another custom materialization model that depends on the first one and simple_model_b + downstream_model_content = """ +{{ config( + materialized='custom_incremental', + time_column='analysis_date', +) }} + +SELECT + CURRENT_TIMESTAMP as analysis_date, + e.waiter_name, + e.simple_value, + e.computed_value, + b.a as model_b_value, + e.computed_value + b.a as final_computation, + CASE + WHEN e.computed_value >= 5 THEN 'High' + WHEN e.computed_value >= 2 THEN 'Medium' + ELSE 'Low' + END as category, + 'downstream_lineage_test' as model_type +FROM {{ ref('enhanced_waiter_data') }} e +CROSS JOIN {{ ref('simple_model_b') }} b +WHERE e.computed_value >= 0 +""".strip() + + (models_dir / "waiter_analytics_summary.sql").write_text(downstream_model_content) + + context = Context(paths=path) + enhanced_data_model = context.get_model("sushi.enhanced_waiter_data") + analytics_summary_model = context.get_model("sushi.waiter_analytics_summary") + + # Verify that custom materialization models have proper model kinds + assert isinstance(enhanced_data_model.kind, DbtCustomKind) + assert enhanced_data_model.kind.materialization == "custom_incremental" + + assert isinstance(analytics_summary_model.kind, DbtCustomKind) + assert analytics_summary_model.kind.materialization == "custom_incremental" + + # - enhanced_waiter_data should depend on waiter_names and simple_model_a + enhanced_data_deps = enhanced_data_model.depends_on + assert '"memory"."sushi"."simple_model_a"' in enhanced_data_deps + assert '"memory"."sushi"."waiter_names"' in enhanced_data_deps + + # - waiter_analytics_summary should depend on enhanced_waiter_data and simple_model_b + analytics_deps = analytics_summary_model.depends_on + assert '"memory"."sushi"."enhanced_waiter_data"' in analytics_deps + assert '"memory"."sushi"."simple_model_b"' in analytics_deps + + # build only the models that have dependences + plan = context.plan( + select_models=[ + "sushi.waiter_names", + "sushi.simple_model_a", + "sushi.simple_model_b", + "sushi.enhanced_waiter_data", + "sushi.waiter_analytics_summary", + ] + ) + context.apply(plan) + + # Verify that all δοwnstream models were built and contain expected data + waiter_names_result = context.engine_adapter.fetchdf( + "SELECT COUNT(*) as count FROM sushi.waiter_names" + ) + assert waiter_names_result["count"][0] > 0 + + simple_a_result = context.engine_adapter.fetchdf("SELECT a FROM sushi.simple_model_a") + assert len(simple_a_result) > 0 + assert simple_a_result["a"][0] == 1 + + simple_b_result = context.engine_adapter.fetchdf("SELECT a FROM sushi.simple_model_b") + assert len(simple_b_result) > 0 + assert simple_b_result["a"][0] == 1 + + # Check intermediate custom materialization model + enhanced_data_result = context.engine_adapter.fetchdf(""" + SELECT + waiter_name, + simple_value, + computed_value, + model_type + FROM sushi.enhanced_waiter_data + ORDER BY waiter_id + LIMIT 5 + """) + + assert len(enhanced_data_result) > 0 + assert enhanced_data_result["model_type"][0] == "lineage_test" + assert all(val == 1 for val in enhanced_data_result["simple_value"]) + assert all(val >= 0 for val in enhanced_data_result["computed_value"]) + assert any(val == "Ryan" for val in enhanced_data_result["waiter_name"]) + + # Check final downstream custom materialization model + analytics_summary_result = context.engine_adapter.fetchdf(""" + SELECT + waiter_name, + category, + model_type, + final_computation + FROM sushi.waiter_analytics_summary + ORDER BY waiter_name + LIMIT 5 + """) + + assert len(analytics_summary_result) > 0 + assert analytics_summary_result["model_type"][0] == "downstream_lineage_test" + assert all(cat in ["High", "Medium", "Low"] for cat in analytics_summary_result["category"]) + assert all(val >= 0 for val in analytics_summary_result["final_computation"]) + + # Test that lineage information is preserved in dev environments + dev_plan = context.plan("dev", select_models=["sushi.waiter_analytics_summary"]) + context.apply(dev_plan) + + dev_analytics_result = context.engine_adapter.fetchdf(""" + SELECT + COUNT(*) as count, + COUNT(DISTINCT waiter_name) as unique_waiters + FROM sushi__dev.waiter_analytics_summary + """) + + prod_analytics_result = context.engine_adapter.fetchdf(""" + SELECT + COUNT(*) as count, + COUNT(DISTINCT waiter_name) as unique_waiters + FROM sushi.waiter_analytics_summary + """) + + # Dev and prod should have the same data as they share physical data + assert dev_analytics_result["count"][0] == prod_analytics_result["count"][0] + assert dev_analytics_result["unique_waiters"][0] == prod_analytics_result["unique_waiters"][0] diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index e6c02bcb4c..e2e7bc706c 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -232,7 +232,7 @@ def test_source_meta_external_location(): expected = ( "read_parquet('path/to/external/items.parquet')" if DBT_VERSION >= (1, 4, 0) - else '"main"."parquet_file".items' + else '"memory"."parquet_file".items' ) assert relation.render() == expected diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index d212872cb7..e29c6768bf 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -842,3 +842,14 @@ def test_jinja_config_no_query(create_empty_project): # loads without error and contains empty query (which will error at runtime) assert not context.snapshots['"local"."main"."comment_config_model"'].model.render_query() + + +@pytest.mark.slow +def test_load_custom_materialisations(sushi_test_dbt_context: Context) -> None: + context = sushi_test_dbt_context + assert context.get_model("sushi.custom_incremental_model") + assert context.get_model("sushi.custom_incremental_with_filter") + + context.load() + assert context.get_model("sushi.custom_incremental_model") + assert context.get_model("sushi.custom_incremental_with_filter") diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index a640d620b7..9a9ce8f906 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1,5 +1,5 @@ import agate -from datetime import datetime +from datetime import datetime, timedelta import json import logging import typing as t @@ -113,6 +113,129 @@ def test_materialization(): ModelConfig(name="model", alias="model", schema="schema", materialized="dictionary") +def test_dbt_custom_materialization(): + sushi_context = Context(paths=["tests/fixtures/dbt/sushi_test"]) + + plan_builder = sushi_context.plan_builder(select_models=["sushi.custom_incremental_model"]) + plan = plan_builder.build() + assert len(plan.selected_models) == 1 + selected_model = list(plan.selected_models)[0] + assert selected_model == "model.sushi.custom_incremental_model" + + qoery = "SELECT * FROM sushi.custom_incremental_model ORDER BY created_at" + hook_table = "SELECT * FROM hook_table ORDER BY id" + sushi_context.apply(plan) + result = sushi_context.engine_adapter.fetchdf(qoery) + assert len(result) == 1 + assert {"created_at", "id"}.issubset(result.columns) + + # assert the pre/post hooks executed as well as part of the custom materialization + hook_result = sushi_context.engine_adapter.fetchdf(hook_table) + assert len(hook_result) == 1 + assert {"length_col", "id", "updated_at"}.issubset(hook_result.columns) + assert int(hook_result["length_col"][0]) >= 519 + assert hook_result["id"][0] == 1 + + # running with execution time one day in the future to simulate an incremental insert + tomorrow = datetime.now() + timedelta(days=1) + sushi_context.run(select_models=["sushi.custom_incremental_model"], execution_time=tomorrow) + + result_after_run = sushi_context.engine_adapter.fetchdf(qoery) + assert {"created_at", "id"}.issubset(result_after_run.columns) + + # this should have added new unique values for the new row + assert len(result_after_run) == 2 + assert result_after_run["id"].is_unique + assert result_after_run["created_at"].is_unique + + # validate the hooks executed as part of the run as well + hook_result = sushi_context.engine_adapter.fetchdf(hook_table) + assert len(hook_result) == 2 + assert hook_result["id"][1] == 2 + assert int(hook_result["length_col"][1]) >= 519 + assert hook_result["id"].is_monotonic_increasing + assert hook_result["updated_at"].is_unique + assert not hook_result["length_col"].is_unique + + +def test_dbt_custom_materialization_with_time_filter_and_macro(): + sushi_context = Context(paths=["tests/fixtures/dbt/sushi_test"]) + today = datetime.now() + + # select both custom materialiasation models with the wildcard + selector = ["sushi.custom_incremental*"] + plan_builder = sushi_context.plan_builder(select_models=selector, execution_time=today) + plan = plan_builder.build() + + assert len(plan.selected_models) == 2 + assert { + "model.sushi.custom_incremental_model", + "model.sushi.custom_incremental_with_filter", + }.issubset(plan.selected_models) + + # the model that daily (default cron) populates with data + select_daily = "SELECT * FROM sushi.custom_incremental_model ORDER BY created_at" + + # this model uses `run_started_at` as a filter (which we populate with execution time) with 2 day interval + select_filter = "SELECT * FROM sushi.custom_incremental_with_filter ORDER BY created_at" + + sushi_context.apply(plan) + result = sushi_context.engine_adapter.fetchdf(select_daily) + assert len(result) == 1 + assert {"created_at", "id"}.issubset(result.columns) + + result = sushi_context.engine_adapter.fetchdf(select_filter) + assert len(result) == 1 + assert {"created_at", "id"}.issubset(result.columns) + + # - run ONE DAY LATER + a_day_later = today + timedelta(days=1) + sushi_context.run(select_models=selector, execution_time=a_day_later) + result_after_run = sushi_context.engine_adapter.fetchdf(select_daily) + + # the new row is inserted in the normal incremental model + assert len(result_after_run) == 2 + assert {"created_at", "id"}.issubset(result_after_run.columns) + assert result_after_run["id"].is_unique + assert result_after_run["created_at"].is_unique + + # this model due to the filter shouldn't populate with any new data + result_after_run_filter = sushi_context.engine_adapter.fetchdf(select_filter) + assert len(result_after_run_filter) == 1 + assert {"created_at", "id"}.issubset(result_after_run_filter.columns) + assert result.equals(result_after_run_filter) + assert result_after_run_filter["id"].is_unique + assert result_after_run_filter["created_at"][0].date() == today.date() + + # - run TWO DAYS LATER + two_days_later = a_day_later + timedelta(days=1) + sushi_context.run(select_models=selector, execution_time=two_days_later) + result_after_run = sushi_context.engine_adapter.fetchdf(select_daily) + + # again a new row is inserted in the normal model + assert len(result_after_run) == 3 + assert {"created_at", "id"}.issubset(result_after_run.columns) + assert result_after_run["id"].is_unique + assert result_after_run["created_at"].is_unique + + # the model with the filter now should populate as well + result_after_run_filter = sushi_context.engine_adapter.fetchdf(select_filter) + assert len(result_after_run_filter) == 2 + assert {"created_at", "id"}.issubset(result_after_run_filter.columns) + assert result_after_run_filter["id"].is_unique + assert result_after_run_filter["created_at"][0].date() == today.date() + assert result_after_run_filter["created_at"][1].date() == two_days_later.date() + + # assert hooks have executed for both plan and incremental runs + hook_result = sushi_context.engine_adapter.fetchdf("SELECT * FROM hook_table ORDER BY id") + assert len(hook_result) == 3 + hook_result["id"][0] == 1 + assert hook_result["id"].is_monotonic_increasing + assert hook_result["updated_at"].is_unique + assert int(hook_result["length_col"][1]) >= 519 + assert not hook_result["length_col"].is_unique + + def test_model_kind(): context = DbtContext() context.project_name = "Test" diff --git a/tests/fixtures/dbt/sushi_test/macros/materializations/custom_incremental.sql b/tests/fixtures/dbt/sushi_test/macros/materializations/custom_incremental.sql new file mode 100644 index 0000000000..c61899c8ff --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/macros/materializations/custom_incremental.sql @@ -0,0 +1,61 @@ +{%- macro build_incremental_filter_sql(sql, time_column, existing_relation, interval_config) -%} + {# macro to build the filter and also test use of macro inside materialisation #} + WITH source_data AS ( + {{ sql }} + ) + SELECT * FROM source_data + WHERE {{ time_column }} >= ( + SELECT COALESCE(MAX({{ time_column }}), '1900-01-01') + {%- if interval_config %} + INTERVAL {{ interval_config }} {%- endif %} + FROM {{ existing_relation }} + ) +{%- endmacro -%} + +{%- materialization custom_incremental, default -%} + {%- set existing_relation = adapter.get_relation(database=database, schema=schema, identifier=identifier) -%} + {%- set new_relation = api.Relation.create(database=database, schema=schema, identifier=identifier) -%} + {%- set temp_relation = make_temp_relation(new_relation) -%} + + {%- set time_column = config.get('time_column') -%} + {%- set interval_config = config.get('interval') -%} + + {{ run_hooks(pre_hooks) }} + + {%- if existing_relation is none -%} + {# The first insert creates new table if it doesn't exist #} + {%- call statement('main') -%} + CREATE TABLE {{ new_relation }} + AS {{ sql }} + {%- endcall -%} + {%- else -%} + {# Incremental load, appending new data with optional time filtering #} + {%- if time_column is not none -%} + {%- set filtered_sql -%} + {{ build_incremental_filter_sql(sql, time_column, existing_relation, interval_config) }} + {%- endset -%} + {%- else -%} + {%- set filtered_sql = sql -%} + {%- endif -%} + + {{log(filtered_sql, info=true)}} + + {%- call statement('create_temp') -%} + {{ create_table_as(True, temp_relation, filtered_sql) }} + CREATE TABLE {{ temp_relation }} + AS {{ filtered_sql }} + {%- endcall -%} + + {%- call statement('insert') -%} + INSERT INTO {{ new_relation }} + SELECT * FROM {{ temp_relation }} + {%- endcall -%} + + {%- call statement('drop_temp') -%} + DROP TABLE {{ temp_relation }} + {%- endcall -%} + {%- endif -%} + + {{ run_hooks(post_hooks) }} + + {{ return({'relations': [new_relation]}) }} +{%- endmaterialization -%} diff --git a/tests/fixtures/dbt/sushi_test/models/custom_incremental_model.sql b/tests/fixtures/dbt/sushi_test/models/custom_incremental_model.sql new file mode 100644 index 0000000000..c7e9a8f7ea --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/models/custom_incremental_model.sql @@ -0,0 +1,20 @@ +{{ config( + materialized='custom_incremental', + pre_hook=[ + "CREATE TABLE IF NOT EXISTS hook_table (id INTEGER, length_col TEXT, updated_at TIMESTAMP)" + ], + post_hook=[ + """ + INSERT INTO hook_table + SELECT + COALESCE(MAX(id), 0) + 1 AS id, + '{{ model.raw_code | length }}' AS length_col, + CURRENT_TIMESTAMP AS updated_at + FROM hook_table + """ + ] +) }} + +SELECT + current_timestamp as created_at, + hash(current_timestamp) as id, \ No newline at end of file diff --git a/tests/fixtures/dbt/sushi_test/models/custom_incremental_with_filter.sql b/tests/fixtures/dbt/sushi_test/models/custom_incremental_with_filter.sql new file mode 100644 index 0000000000..94cbdc9333 --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/models/custom_incremental_with_filter.sql @@ -0,0 +1,9 @@ +{{ config( + materialized='custom_incremental', + time_column='created_at', + interval='2 day' +) }} + +SELECT + CAST('{{ run_started_at }}' AS TIMESTAMP) as created_at, + hash('{{ run_started_at }}') as id, \ No newline at end of file diff --git a/tests/fixtures/dbt/sushi_test/profiles.yml b/tests/fixtures/dbt/sushi_test/profiles.yml index 056c3c2b91..f49ad8ea0f 100644 --- a/tests/fixtures/dbt/sushi_test/profiles.yml +++ b/tests/fixtures/dbt/sushi_test/profiles.yml @@ -3,6 +3,7 @@ sushi: in_memory: type: duckdb schema: sushi + database: memory duckdb: type: duckdb path: 'local.duckdb' From c9b49b4e7ef96b77d83b042227acbb25743c34d9 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:15:37 +0300 Subject: [PATCH 0921/1056] Chore!: bump sqlglot to v27.20.0 (#5460) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9b192d6a78..053b242813 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.19.0", + "sqlglot[rs]~=27.20.0", "tenacity", "time-machine", "json-stream" From a95955c24c3fa15327c2c26c93e8b81cf114c9ce Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 1 Oct 2025 07:49:35 -0700 Subject: [PATCH 0922/1056] fix: support unicode characters in file cache (#5463) --- sqlmesh/utils/__init__.py | 9 ++++++++- sqlmesh/utils/cache.py | 2 +- tests/utils/__init__.py | 23 +++++++++++++++++++++++ tests/utils/test_cache.py | 1 + 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/sqlmesh/utils/__init__.py b/sqlmesh/utils/__init__.py index c220de4847..5b1b077216 100644 --- a/sqlmesh/utils/__init__.py +++ b/sqlmesh/utils/__init__.py @@ -21,6 +21,7 @@ from functools import lru_cache, reduce, wraps from pathlib import Path +import unicodedata from sqlglot import exp from sqlglot.dialects.dialect import Dialects @@ -291,8 +292,14 @@ def sqlglot_dialects() -> str: NON_ALNUM = re.compile(r"[^a-zA-Z0-9_]") +NON_ALUM_INCLUDE_UNICODE = re.compile(r"\W", flags=re.UNICODE) -def sanitize_name(name: str) -> str: + +def sanitize_name(name: str, *, include_unicode: bool = False) -> str: + if include_unicode: + s = unicodedata.normalize("NFC", name) + s = NON_ALUM_INCLUDE_UNICODE.sub("_", s) + return s return NON_ALNUM.sub("_", name) diff --git a/sqlmesh/utils/cache.py b/sqlmesh/utils/cache.py index 002248f511..4b557e43b6 100644 --- a/sqlmesh/utils/cache.py +++ b/sqlmesh/utils/cache.py @@ -133,7 +133,7 @@ def clear(self) -> None: def _cache_entry_path(self, name: str, entry_id: str = "") -> Path: entry_file_name = "__".join(p for p in (self._cache_version, name, entry_id) if p) - full_path = self._path / sanitize_name(entry_file_name) + full_path = self._path / sanitize_name(entry_file_name, include_unicode=True) if IS_WINDOWS: # handle paths longer than 260 chars full_path = fix_windows_path(full_path) diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index e69de29bb2..744ad37757 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -0,0 +1,23 @@ +import pytest + +from sqlmesh.utils import sanitize_name + + +@pytest.mark.parametrize( + "raw,exclude_unicode,include_unicode", + [ + ("simple", "simple", "simple"), + ("snake_case", "snake_case", "snake_case"), + ("客户数据", "____", "客户数据"), + ("客户-数据 v2", "______v2", "客户_数据_v2"), + ("中文,逗号", "_____", "中文_逗号"), + ("a/b", "a_b", "a_b"), + ("spaces\tand\nnewlines", "spaces_and_newlines", "spaces_and_newlines"), + ("data📦2025", "data_2025", "data_2025"), + ("MiXeD123_名字", "MiXeD123___", "MiXeD123_名字"), + ("", "", ""), + ], +) +def test_sanitize_name_no_(raw, exclude_unicode, include_unicode): + assert sanitize_name(raw) == exclude_unicode + assert sanitize_name(raw, include_unicode=True) == include_unicode diff --git a/tests/utils/test_cache.py b/tests/utils/test_cache.py index cd1fdb0115..0b6d335446 100644 --- a/tests/utils/test_cache.py +++ b/tests/utils/test_cache.py @@ -39,6 +39,7 @@ def test_file_cache(tmp_path: Path, mocker: MockerFixture): loader.assert_called_once() assert "___test_model_" in cache._cache_entry_path('"test_model"').name + assert "客户数据" in cache._cache_entry_path("客户数据").name def test_optimized_query_cache(tmp_path: Path, mocker: MockerFixture): From 956670c14c04037b7d0322de918c994697a8de14 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Thu, 2 Oct 2025 16:24:50 +1300 Subject: [PATCH 0923/1056] Feat(sqlmesh_dbt): Implement --model and --resource-type (#5443) --- sqlmesh/core/selector.py | 81 ++++++++++++++++--- sqlmesh_dbt/cli.py | 32 +++++++- sqlmesh_dbt/operations.py | 26 ++++-- sqlmesh_dbt/selectors.py | 40 ++++++++- tests/core/test_selector_dbt.py | 63 +++++++++++++++ ...st_selector.py => test_selector_native.py} | 0 tests/dbt/cli/test_list.py | 23 ++++++ tests/dbt/cli/test_selectors.py | 58 +++++++++++++ 8 files changed, 306 insertions(+), 17 deletions(-) create mode 100644 tests/core/test_selector_dbt.py rename tests/core/{test_selector.py => test_selector_native.py} (100%) diff --git a/sqlmesh/core/selector.py b/sqlmesh/core/selector.py index 1484d06cee..3865327acd 100644 --- a/sqlmesh/core/selector.py +++ b/sqlmesh/core/selector.py @@ -16,6 +16,7 @@ from sqlmesh.core.dialect import normalize_model_name from sqlmesh.core.environment import Environment from sqlmesh.core.model import update_model_schemas +from sqlmesh.core.audit import StandaloneAudit from sqlmesh.utils import UniqueKeyDict from sqlmesh.utils.dag import DAG from sqlmesh.utils.git import GitClient @@ -25,6 +26,7 @@ if t.TYPE_CHECKING: from typing_extensions import Literal as Lit # noqa from sqlmesh.core.model import Model + from sqlmesh.core.node import Node from sqlmesh.core.state_sync import StateReader @@ -167,7 +169,7 @@ def get_model(fqn: str) -> t.Optional[Model]: return models def expand_model_selections( - self, model_selections: t.Iterable[str], models: t.Optional[t.Dict[str, Model]] = None + self, model_selections: t.Iterable[str], models: t.Optional[t.Dict[str, Node]] = None ) -> t.Set[str]: """Expands a set of model selections into a set of model fqns that can be looked up in the Context. @@ -180,7 +182,7 @@ def expand_model_selections( node = parse(" | ".join(f"({s})" for s in model_selections)) - all_models = models or self._models + all_models: t.Dict[str, Node] = models or dict(self._models) models_by_tags: t.Dict[str, t.Set[str]] = {} for fqn, model in all_models.items(): @@ -226,6 +228,13 @@ def evaluate(node: exp.Expression) -> t.Set[str]: if fnmatch.fnmatchcase(tag, pattern) } return models_by_tags.get(pattern, set()) + if isinstance(node, ResourceType): + resource_type = node.name.lower() + return { + fqn + for fqn, model in all_models.items() + if self._matches_resource_type(resource_type, model) + } if isinstance(node, Direction): selected = set() @@ -243,36 +252,49 @@ def evaluate(node: exp.Expression) -> t.Set[str]: return evaluate(node) @abc.abstractmethod - def _model_name(self, model: Model) -> str: + def _model_name(self, model: Node) -> str: """Given a model, return the name that a selector pattern contining wildcards should be fnmatch'd on""" pass @abc.abstractmethod - def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Model]) -> t.Set[str]: + def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Node]) -> t.Set[str]: """Given a pattern, return the keys of the matching models from :all_models""" pass + @abc.abstractmethod + def _matches_resource_type(self, resource_type: str, model: Node) -> bool: + """Indicate whether or not the supplied model matches the supplied resource type""" + pass + class NativeSelector(Selector): """Implementation of selectors that matches objects based on SQLMesh native names""" - def _model_name(self, model: Model) -> str: + def _model_name(self, model: Node) -> str: return model.name - def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Model]) -> t.Set[str]: + def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Node]) -> t.Set[str]: fqn = normalize_model_name(pattern, self._default_catalog, self._dialect) return {fqn} if fqn in all_models else set() + def _matches_resource_type(self, resource_type: str, model: Node) -> bool: + if resource_type == "model": + return model.is_model + if resource_type == "audit": + return isinstance(model, StandaloneAudit) + + raise SQLMeshError(f"Unsupported resource type: {resource_type}") + class DbtSelector(Selector): """Implementation of selectors that matches objects based on the DBT names instead of the SQLMesh native names""" - def _model_name(self, model: Model) -> str: + def _model_name(self, model: Node) -> str: if dbt_fqn := model.dbt_fqn: return dbt_fqn raise SQLMeshError("dbt node information must be populated to use dbt selectors") - def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Model]) -> t.Set[str]: + def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Node]) -> t.Set[str]: # a pattern like "staging.customers" should match a model called "jaffle_shop.staging.customers" # but not a model called "jaffle_shop.customers.staging" # also a pattern like "aging" should not match "staging" so we need to consider components; not substrings @@ -306,6 +328,40 @@ def _pattern_to_model_fqns(self, pattern: str, all_models: t.Dict[str, Model]) - matches.add(fqn) return matches + def _matches_resource_type(self, resource_type: str, model: Node) -> bool: + """ + ref: https://docs.getdbt.com/reference/node-selection/methods#resource_type + + # supported by SQLMesh + "model" + "seed" + "source" # external model + "test" # standalone audit + + # not supported by SQLMesh yet, commented out to throw an error if someone tries to use them + "analysis" + "exposure" + "metric" + "saved_query" + "semantic_model" + "snapshot" + "unit_test" + """ + if resource_type not in ("model", "seed", "source", "test"): + raise SQLMeshError(f"Unsupported resource type: {resource_type}") + + if isinstance(model, StandaloneAudit): + return resource_type == "test" + + if resource_type == "model": + return model.is_model and not model.kind.is_external and not model.kind.is_seed + if resource_type == "source": + return model.kind.is_external + if resource_type == "seed": + return model.kind.is_seed + + return False + class SelectorDialect(Dialect): IDENTIFIERS_CAN_START_WITH_DIGIT = True @@ -336,6 +392,10 @@ class Tag(exp.Expression): pass +class ResourceType(exp.Expression): + pass + + class Direction(exp.Expression): pass @@ -388,7 +448,8 @@ def _parse_var() -> exp.Expression: upstream = _match(TokenType.PLUS) downstream = None tag = _parse_kind("tag") - git = False if tag else _parse_kind("git") + resource_type = False if tag else _parse_kind("resource_type") + git = False if resource_type else _parse_kind("git") lstar = "*" if _match(TokenType.STAR) else "" directions = {} @@ -414,6 +475,8 @@ def _parse_var() -> exp.Expression: if tag: this = Tag(this=this) + if resource_type: + this = ResourceType(this=this) if git: this = Git(this=this) if directions: diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py index fa75d303a1..83230de3fd 100644 --- a/sqlmesh_dbt/cli.py +++ b/sqlmesh_dbt/cli.py @@ -33,15 +33,39 @@ def _cleanup() -> None: select_option = click.option( "-s", - "-m", "--select", + multiple=True, + help="Specify the nodes to include.", +) +model_option = click.option( + "-m", "--models", "--model", multiple=True, - help="Specify the nodes to include.", + help="Specify the model nodes to include; other nodes are excluded.", ) exclude_option = click.option("--exclude", multiple=True, help="Specify the nodes to exclude.") +# TODO: expand this out into --resource-type/--resource-types and --exclude-resource-type/--exclude-resource-types +resource_types = [ + "metric", + "semantic_model", + "saved_query", + "source", + "analysis", + "model", + "test", + "unit_test", + "exposure", + "snapshot", + "seed", + "default", + "all", +] +resource_type_option = click.option( + "--resource-type", type=click.Choice(resource_types, case_sensitive=False) +) + @click.group(cls=ErrorHandlingGroup, invoke_without_command=True) @click.option("--profile", help="Which existing profile to load. Overrides output.profile") @@ -86,7 +110,9 @@ def dbt( @dbt.command() @select_option +@model_option @exclude_option +@resource_type_option @click.option( "-f", "--full-refresh", @@ -116,7 +142,9 @@ def run( @dbt.command(name="list") @select_option +@model_option @exclude_option +@resource_type_option @vars_option @click.pass_context def list_(ctx: click.Context, vars: t.Optional[t.Dict[str, t.Any]], **kwargs: t.Any) -> None: diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index a157705ffd..6e8b452b28 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -26,12 +26,16 @@ def list_( self, select: t.Optional[t.List[str]] = None, exclude: t.Optional[t.List[str]] = None, + models: t.Optional[t.List[str]] = None, + resource_type: t.Optional[str] = None, ) -> None: # dbt list prints: # - models # - "data tests" (audits) for those models # it also applies selectors which is useful for testing selectors - selected_models = list(self._selected_models(select, exclude).values()) + selected_models = list( + self._selected_models(select, exclude, models, resource_type).values() + ) self.console.list_models( selected_models, {k: v.node for k, v in self.context.snapshots.items()} ) @@ -41,13 +45,19 @@ def run( environment: t.Optional[str] = None, select: t.Optional[t.List[str]] = None, exclude: t.Optional[t.List[str]] = None, + models: t.Optional[t.List[str]] = None, + resource_type: t.Optional[str] = None, full_refresh: bool = False, empty: bool = False, ) -> Plan: + consolidated_select, consolidated_exclude = selectors.consolidate( + select or [], exclude or [], models or [], resource_type + ) + plan_builder = self._plan_builder( environment=environment, - select=select, - exclude=exclude, + select=consolidated_select, + exclude=consolidated_exclude, full_refresh=full_refresh, empty=empty, ) @@ -86,9 +96,15 @@ def _plan_builder( ) def _selected_models( - self, select: t.Optional[t.List[str]] = None, exclude: t.Optional[t.List[str]] = None + self, + select: t.Optional[t.List[str]] = None, + exclude: t.Optional[t.List[str]] = None, + models: t.Optional[t.List[str]] = None, + resource_type: t.Optional[str] = None, ) -> t.Dict[str, Model]: - if sqlmesh_selector := selectors.to_sqlmesh(select or [], exclude or []): + if sqlmesh_selector := selectors.to_sqlmesh( + *selectors.consolidate(select or [], exclude or [], models or [], resource_type) + ): if self.debug: self.console.print(f"dbt --select: {select}") self.console.print(f"dbt --exclude: {exclude}") diff --git a/sqlmesh_dbt/selectors.py b/sqlmesh_dbt/selectors.py index 120d5dcb36..5821586ad3 100644 --- a/sqlmesh_dbt/selectors.py +++ b/sqlmesh_dbt/selectors.py @@ -4,7 +4,45 @@ logger = logging.getLogger(__name__) -def to_sqlmesh(dbt_select: t.Collection[str], dbt_exclude: t.Collection[str]) -> t.Optional[str]: +def consolidate( + select: t.List[str], + exclude: t.List[str], + models: t.List[str], + resource_type: t.Optional[str], +) -> t.Tuple[t.List[str], t.List[str]]: + """ + Given a bunch of dbt CLI arguments that may or may not be defined: + --select, --exclude, --models, --resource-type + + Combine them into a single set of --select/--exclude node selectors, throwing an error if mutually exclusive combinations are provided + Note that the returned value is still in dbt format, pass it to to_sqlmesh() to create a selector for the sqlmesh selector engine + """ + if models and select: + raise ValueError('"models" and "select" are mutually exclusive arguments') + + if models and resource_type: + raise ValueError('"models" and "resource_type" are mutually exclusive arguments') + + if models: + # --models implies resource_type:model + resource_type = "model" + + if resource_type: + resource_type_selector = f"resource_type:{resource_type}" + all_selectors = [*select, *models] + select = ( + [ + f"resource_type:{resource_type},{original_selector}" + for original_selector in all_selectors + ] + if all_selectors + else [resource_type_selector] + ) + + return select, exclude + + +def to_sqlmesh(dbt_select: t.List[str], dbt_exclude: t.List[str]) -> t.Optional[str]: """ Given selectors defined in the format of the dbt cli --select and --exclude arguments, convert them into a selector expression that the SQLMesh selector engine can understand. diff --git a/tests/core/test_selector_dbt.py b/tests/core/test_selector_dbt.py new file mode 100644 index 0000000000..112c5740ac --- /dev/null +++ b/tests/core/test_selector_dbt.py @@ -0,0 +1,63 @@ +import typing as t +import pytest +from pytest_mock import MockerFixture +from sqlglot import exp +from sqlmesh.core.model.kind import SeedKind, ExternalKind, FullKind +from sqlmesh.core.model.seed import Seed +from sqlmesh.core.model.definition import SqlModel, SeedModel, ExternalModel +from sqlmesh.core.audit.definition import StandaloneAudit +from sqlmesh.core.snapshot.definition import Node +from sqlmesh.core.selector import DbtSelector +from sqlmesh.core.selector import parse, ResourceType +from sqlmesh.utils.errors import SQLMeshError +import sqlmesh.core.dialect as d +from sqlmesh.utils import UniqueKeyDict + + +def test_parse_resource_type(): + assert parse("resource_type:foo") == ResourceType(this=exp.Var(this="foo")) + + +@pytest.mark.parametrize( + "resource_type,expected", + [ + ("model", {'"test"."normal_model"'}), + ("seed", {'"test"."seed_model"'}), + ("test", {'"test"."standalone_audit"'}), + ("source", {'"external"."model"'}), + ], +) +def test_expand_model_selections_resource_type( + mocker: MockerFixture, resource_type: str, expected: t.Set[str] +): + models: t.Dict[str, Node] = { + '"test"."normal_model"': SqlModel( + name="test.normal_model", + kind=FullKind(), + query=d.parse_one("SELECT 'normal_model' AS what"), + ), + '"test"."seed_model"': SeedModel( + name="test.seed_model", kind=SeedKind(path="/tmp/foo"), seed=Seed(content="id,name") + ), + '"test"."standalone_audit"': StandaloneAudit( + name="test.standalone_audit", query=d.parse_one("SELECT 'standalone_audit' AS what") + ), + '"external"."model"': ExternalModel(name="external.model", kind=ExternalKind()), + } + + selector = DbtSelector(state_reader=mocker.Mock(), models=UniqueKeyDict("models")) + + assert selector.expand_model_selections([f"resource_type:{resource_type}"], models) == expected + + +def test_unsupported_resource_type(mocker: MockerFixture): + selector = DbtSelector(state_reader=mocker.Mock(), models=UniqueKeyDict("models")) + + models: t.Dict[str, Node] = { + '"test"."normal_model"': SqlModel( + name="test.normal_model", query=d.parse_one("SELECT 'normal_model' AS what") + ), + } + + with pytest.raises(SQLMeshError, match="Unsupported"): + selector.expand_model_selections(["resource_type:analysis"], models) diff --git a/tests/core/test_selector.py b/tests/core/test_selector_native.py similarity index 100% rename from tests/core/test_selector.py rename to tests/core/test_selector_native.py diff --git a/tests/dbt/cli/test_list.py b/tests/dbt/cli/test_list.py index 712d80b2fe..3e6a55125c 100644 --- a/tests/dbt/cli/test_list.py +++ b/tests/dbt/cli/test_list.py @@ -79,3 +79,26 @@ def test_list_with_vars(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Re │ └── depends_on: jaffle_shop.customers""" in result.output ) + + +def test_list_models_mutually_exclusive( + jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result] +): + result = invoke_cli(["list", "--select", "foo", "--models", "bar"]) + assert result.exit_code != 0 + assert '"models" and "select" are mutually exclusive arguments' in result.output + + result = invoke_cli(["list", "--resource-type", "test", "--models", "bar"]) + assert result.exit_code != 0 + assert '"models" and "resource_type" are mutually exclusive arguments' in result.output + + +def test_list_models(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): + result = invoke_cli(["list", "--models", "jaffle_shop"]) + assert result.exit_code == 0 + assert not result.exception + + assert "─ jaffle_shop.customers" in result.output + assert ( + "─ jaffle_shop.raw_customers" not in result.output + ) # should be excluded because dbt --models excludes seeds diff --git a/tests/dbt/cli/test_selectors.py b/tests/dbt/cli/test_selectors.py index 99907bda84..3d50fe6ed2 100644 --- a/tests/dbt/cli/test_selectors.py +++ b/tests/dbt/cli/test_selectors.py @@ -269,3 +269,61 @@ def test_selection_and_exclusion_by_dbt_names( assert sqlmesh_selector assert selector.expand_model_selections([sqlmesh_selector]) == expected + + +@pytest.mark.parametrize( + "input_args,expected", + [ + ( + dict(select=["jaffle_shop"], models=["jaffle_shop"]), + '"models" and "select" are mutually exclusive', + ), + ( + dict(models=["jaffle_shop"], resource_type="test"), + '"models" and "resource_type" are mutually exclusive', + ), + ( + dict(select=["jaffle_shop"], resource_type="test"), + (["resource_type:test,jaffle_shop"], []), + ), + (dict(resource_type="model"), (["resource_type:model"], [])), + (dict(models=["stg_customers"]), (["resource_type:model,stg_customers"], [])), + ( + dict(models=["stg_customers"], exclude=["orders"]), + (["resource_type:model,stg_customers"], ["orders"]), + ), + ], +) +def test_consolidate(input_args: t.Dict[str, t.Any], expected: t.Union[t.Tuple[str, str], str]): + all_input_args: t.Dict[str, t.Any] = dict(select=[], exclude=[], models=[], resource_type=None) + + all_input_args.update(input_args) + + def _do_assert(): + assert selectors.consolidate(**all_input_args) == expected + + if isinstance(expected, str): + with pytest.raises(ValueError, match=expected): + _do_assert() + else: + _do_assert() + + +def test_models_by_dbt_names(jaffle_shop_duckdb_context: Context): + ctx = jaffle_shop_duckdb_context + + selector = ctx._new_selector() + assert isinstance(selector, DbtSelector) + + selector_expr = selectors.to_sqlmesh( + *selectors.consolidate(select=[], exclude=[], models=["jaffle_shop"], resource_type=None) + ) + assert selector_expr + + assert selector.expand_model_selections([selector_expr]) == { + '"jaffle_shop"."main"."customers"', + '"jaffle_shop"."main"."orders"', + '"jaffle_shop"."main"."stg_customers"', + '"jaffle_shop"."main"."stg_orders"', + '"jaffle_shop"."main"."stg_payments"', + } From 51772bd2b2dc45c9d53706a82613d4802f838ece Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:16:08 +0300 Subject: [PATCH 0924/1056] Chore: add Makefile command to recursively clean `.cache` dirs (#5471) --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index fbf77b8f9b..89b5be5131 100644 --- a/Makefile +++ b/Makefile @@ -107,6 +107,9 @@ ui-build: clean-build: rm -rf build/ && rm -rf dist/ && rm -rf *.egg-info +clean-caches: + find . -type d -name ".cache" -exec rm -rf {} + 2>/dev/null && echo "Successfully removed all .cache directories" + dev-publish: ui-build clean-build publish jupyter-example: From 4933d2910c4ecfc90e6b5d6896d25dd33576e4d6 Mon Sep 17 00:00:00 2001 From: Alexander Butler <41213451+z3z1ma@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:45:18 +0100 Subject: [PATCH 0925/1056] Fix: dont try to serialize engine adapter to sql in macro template method (#5455) Co-authored-by: Jo <46752250+georgesittas@users.noreply.github.com> --- sqlmesh/core/macros.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py index b58817950d..af7c344081 100644 --- a/sqlmesh/core/macros.py +++ b/sqlmesh/core/macros.py @@ -331,6 +331,11 @@ def template(self, text: t.Any, local_variables: t.Dict[str, t.Any]) -> str: base_mapping = { k.lower(): convert_sql(v, self.dialect) for k, v in chain(self.variables.items(), self.locals.items(), local_variables.items()) + if k.lower() + not in ( + "engine_adapter", + "snapshot", + ) } return MacroStrTemplate(str(text)).safe_substitute(CaseInsensitiveMapping(base_mapping)) From d8b3e063951f5d59fe7d10a0cbf32754acc58dc4 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:04:24 +0300 Subject: [PATCH 0926/1056] Chore: rename clean-caches to clear-caches (#5472) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 89b5be5131..2b3e10cb1b 100644 --- a/Makefile +++ b/Makefile @@ -107,7 +107,7 @@ ui-build: clean-build: rm -rf build/ && rm -rf dist/ && rm -rf *.egg-info -clean-caches: +clear-caches: find . -type d -name ".cache" -exec rm -rf {} + 2>/dev/null && echo "Successfully removed all .cache directories" dev-publish: ui-build clean-build publish From c67a2fd05a39729aad68e0344451648e48bf6e93 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:07:17 +0300 Subject: [PATCH 0927/1056] fix: use bitnami legacy for spark (#5470) --- tests/core/engine_adapter/integration/docker/spark/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/engine_adapter/integration/docker/spark/Dockerfile b/tests/core/engine_adapter/integration/docker/spark/Dockerfile index 7fb39b840c..cfbe7d1e88 100644 --- a/tests/core/engine_adapter/integration/docker/spark/Dockerfile +++ b/tests/core/engine_adapter/integration/docker/spark/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/bitnami/spark:3.5 +FROM bitnamilegacy/spark:3.5.2 USER root RUN install_packages curl USER 1001 From 22e37d25d88855b583c3e74192326b52b282a6a7 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:16:28 -0700 Subject: [PATCH 0928/1056] fix: unicode in model name databricks (#5465) --- sqlmesh/core/engine_adapter/databricks.py | 2 + .../integration/test_integration.py | 37 +++++++++++++++++++ tests/core/engine_adapter/test_databricks.py | 2 +- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/sqlmesh/core/engine_adapter/databricks.py b/sqlmesh/core/engine_adapter/databricks.py index 946a7bdf74..173e1b08af 100644 --- a/sqlmesh/core/engine_adapter/databricks.py +++ b/sqlmesh/core/engine_adapter/databricks.py @@ -34,6 +34,8 @@ class DatabricksEngineAdapter(SparkEngineAdapter): SUPPORTS_CLONING = True SUPPORTS_MATERIALIZED_VIEWS = True SUPPORTS_MATERIALIZED_VIEW_SCHEMA = True + # Spark has this set to false for compatibility when mixing with Trino but that isn't a concern with Databricks + QUOTE_IDENTIFIERS_IN_VIEWS = True SCHEMA_DIFFER_KWARGS = { "support_positional_add": True, "nested_support": NestedSupport.ALL, diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 5190d26e98..995875c778 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -3990,3 +3990,40 @@ def _set_config(gateway: str, config: Config) -> None: was_evaluated=True, day_delta=4, ) + + +def test_unicode_characters(ctx: TestContext, tmp_path: Path): + # Engines that don't quote identifiers in views are incompatible with unicode characters in model names + # at the time of writing this is Spark/Trino and they do this for compatibility reasons. + # I also think Spark may not support unicode in general but that would need to be verified. + if not ctx.engine_adapter.QUOTE_IDENTIFIERS_IN_VIEWS: + pytest.skip("Skipping as these engines have issues with unicode characters in model names") + + model_name = "客户数据" + table = ctx.table(model_name).sql(dialect=ctx.dialect) + (tmp_path / "models").mkdir(exist_ok=True) + + model_def = f""" + MODEL ( + name {table}, + kind FULL, + dialect '{ctx.dialect}' + ); + SELECT 1 as id + """ + + (tmp_path / "models" / "客户数据.sql").write_text(model_def) + + context = ctx.create_context(path=tmp_path) + context.plan(auto_apply=True, no_prompts=True) + + results = ctx.get_metadata_results() + assert len(results.views) == 1 + assert results.views[0].lower() == model_name + + schema = d.to_schema(ctx.schema(), dialect=ctx.dialect) + schema_name = schema.args["db"].this + schema.args["db"].set("this", "sqlmesh__" + schema_name) + table_results = ctx.get_metadata_results(schema) + assert len(table_results.tables) == 1 + assert table_results.tables[0].lower().startswith(schema_name.lower() + "________") diff --git a/tests/core/engine_adapter/test_databricks.py b/tests/core/engine_adapter/test_databricks.py index f482361c3c..27988fed39 100644 --- a/tests/core/engine_adapter/test_databricks.py +++ b/tests/core/engine_adapter/test_databricks.py @@ -195,7 +195,7 @@ def test_materialized_view_properties(mocker: MockFixture, make_mocked_engine_ad sql_calls = to_sql_calls(adapter) # https://docs.databricks.com/en/sql/language-manual/sql-ref-syntax-ddl-create-materialized-view.html#syntax assert sql_calls == [ - "CREATE OR REPLACE MATERIALIZED VIEW test_table PARTITIONED BY (ds) AS SELECT 1", + "CREATE OR REPLACE MATERIALIZED VIEW `test_table` PARTITIONED BY (`ds`) AS SELECT 1", ] From 8342c37cfc72e5e791bf60fc14ce877420ef4adb Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Thu, 2 Oct 2025 11:05:33 -0700 Subject: [PATCH 0929/1056] feat(web_common): add components for lineage (#5385) --- pnpm-lock.yaml | 268 +- web/common/.storybook/main.ts | 2 +- web/common/.syncpackrc | 4 +- web/common/package-lock.json | 7183 ----------------- web/common/package.json | 133 +- .../src/components/CopyButton/CopyButton.tsx | 1 + web/common/src/components/Input/Input.css | 7 + web/common/src/components/Input/Input.tsx | 6 +- web/common/src/components/Lineage/Lineage.css | 3 + .../ColumnLevelLineageContext.ts | 101 + .../LineageColumnLevel/FactoryColumn.tsx | 257 + .../Lineage/LineageColumnLevel/help.ts | 233 + .../useColumnLevelLineage.ts | 49 + .../Lineage/LineageColumnLevel/useColumns.tsx | 58 + .../src/components/Lineage/LineageContext.ts | 103 + .../Lineage/LineageControlButton.tsx | 43 + .../components/Lineage/LineageControlIcon.tsx | 42 + .../src/components/Lineage/LineageLayout.tsx | 401 + .../Lineage/edge/EdgeWithGradient.tsx | 114 + .../Lineage/edge/FactoryEdgeWithGradient.tsx | 58 + .../src/components/Lineage/help.test.ts | 768 ++ web/common/src/components/Lineage/help.ts | 270 + web/common/src/components/Lineage/index.ts | 28 + .../components/Lineage/layout/dagreLayout.ts | 90 + .../src/components/Lineage/layout/help.ts | 100 + .../components/Lineage/node/NodeAppendix.tsx | 44 + .../src/components/Lineage/node/NodeBadge.tsx | 23 + .../src/components/Lineage/node/NodeBase.tsx | 31 + .../components/Lineage/node/NodeContainer.tsx | 21 + .../components/Lineage/node/NodeDetail.tsx | 26 + .../components/Lineage/node/NodeDivider.tsx | 3 + .../components/Lineage/node/NodeHandle.tsx | 31 + .../Lineage/node/NodeHandleIcon.tsx | 22 + .../components/Lineage/node/NodeHandles.tsx | 50 + .../components/Lineage/node/NodeHeader.tsx | 28 + .../src/components/Lineage/node/NodePort.tsx | 64 + .../src/components/Lineage/node/NodePorts.tsx | 44 + .../components/Lineage/node/base-handle.tsx | 27 + .../src/components/Lineage/node/base-node.tsx | 17 + .../Lineage/node/useNodeMetadata.tsx | 43 + .../Lineage/stories/Lineage.stories.tsx | 192 + .../Lineage/stories/ModelLineage.tsx | 416 + .../Lineage/stories/ModelLineageContext.ts | 97 + .../components/Lineage/stories/ModelNode.tsx | 331 + .../Lineage/stories/ModelNodeColumn.tsx | 76 + .../Lineage/stories/dagreLayout.worker.ts | 24 + .../src/components/Lineage/stories/help.ts | 29 + web/common/src/components/Lineage/utils.ts | 108 + .../MessageContainer/MessageContainer.css | 3 + .../MessageContainer/MessageContainer.tsx | 4 +- .../src/components/Metadata/Metadata.css | 4 + .../src/components/ModelName/ModelName.tsx | 8 +- .../src/components/Typography/Information.tsx | 2 +- .../components/VirtualList/FilterableList.css | 9 + .../components/VirtualList/FilterableList.tsx | 7 +- .../components/VirtualList/VirtualList.tsx | 12 +- .../src/styles/design/semantic-colors.css | 10 - web/common/tailwind.base.config.js | 32 +- web/common/tailwind.config.js | 7 +- web/common/tailwind.lineage.config.js | 95 + web/common/tsconfig.base.json | 2 +- web/common/tsconfig.build.json | 3 +- web/common/vite.config.js | 18 +- 63 files changed, 4902 insertions(+), 7383 deletions(-) delete mode 100644 web/common/package-lock.json create mode 100644 web/common/src/components/Input/Input.css create mode 100644 web/common/src/components/Lineage/Lineage.css create mode 100644 web/common/src/components/Lineage/LineageColumnLevel/ColumnLevelLineageContext.ts create mode 100644 web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx create mode 100644 web/common/src/components/Lineage/LineageColumnLevel/help.ts create mode 100644 web/common/src/components/Lineage/LineageColumnLevel/useColumnLevelLineage.ts create mode 100644 web/common/src/components/Lineage/LineageColumnLevel/useColumns.tsx create mode 100644 web/common/src/components/Lineage/LineageContext.ts create mode 100644 web/common/src/components/Lineage/LineageControlButton.tsx create mode 100644 web/common/src/components/Lineage/LineageControlIcon.tsx create mode 100644 web/common/src/components/Lineage/LineageLayout.tsx create mode 100644 web/common/src/components/Lineage/edge/EdgeWithGradient.tsx create mode 100644 web/common/src/components/Lineage/edge/FactoryEdgeWithGradient.tsx create mode 100644 web/common/src/components/Lineage/help.test.ts create mode 100644 web/common/src/components/Lineage/help.ts create mode 100644 web/common/src/components/Lineage/index.ts create mode 100644 web/common/src/components/Lineage/layout/dagreLayout.ts create mode 100644 web/common/src/components/Lineage/layout/help.ts create mode 100644 web/common/src/components/Lineage/node/NodeAppendix.tsx create mode 100644 web/common/src/components/Lineage/node/NodeBadge.tsx create mode 100644 web/common/src/components/Lineage/node/NodeBase.tsx create mode 100644 web/common/src/components/Lineage/node/NodeContainer.tsx create mode 100644 web/common/src/components/Lineage/node/NodeDetail.tsx create mode 100644 web/common/src/components/Lineage/node/NodeDivider.tsx create mode 100644 web/common/src/components/Lineage/node/NodeHandle.tsx create mode 100644 web/common/src/components/Lineage/node/NodeHandleIcon.tsx create mode 100644 web/common/src/components/Lineage/node/NodeHandles.tsx create mode 100644 web/common/src/components/Lineage/node/NodeHeader.tsx create mode 100644 web/common/src/components/Lineage/node/NodePort.tsx create mode 100644 web/common/src/components/Lineage/node/NodePorts.tsx create mode 100644 web/common/src/components/Lineage/node/base-handle.tsx create mode 100644 web/common/src/components/Lineage/node/base-node.tsx create mode 100644 web/common/src/components/Lineage/node/useNodeMetadata.tsx create mode 100644 web/common/src/components/Lineage/stories/Lineage.stories.tsx create mode 100644 web/common/src/components/Lineage/stories/ModelLineage.tsx create mode 100644 web/common/src/components/Lineage/stories/ModelLineageContext.ts create mode 100644 web/common/src/components/Lineage/stories/ModelNode.tsx create mode 100644 web/common/src/components/Lineage/stories/ModelNodeColumn.tsx create mode 100644 web/common/src/components/Lineage/stories/dagreLayout.worker.ts create mode 100644 web/common/src/components/Lineage/stories/help.ts create mode 100644 web/common/src/components/Lineage/utils.ts create mode 100644 web/common/src/components/MessageContainer/MessageContainer.css create mode 100644 web/common/src/components/Metadata/Metadata.css create mode 100644 web/common/src/components/VirtualList/FilterableList.css create mode 100644 web/common/tailwind.lineage.config.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index daaf7eb993..2fec93a8f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -400,124 +400,151 @@ importers: web/common: devDependencies: '@eslint/js': - specifier: ^9.31.0 + specifier: 9.31.0 version: 9.31.0 '@radix-ui/react-slot': - specifier: ^1.2.3 + specifier: 1.2.3 version: 1.2.3(@types/react@18.3.23)(react@18.3.1) '@radix-ui/react-tooltip': - specifier: ^1.2.8 + specifier: 1.2.8 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 + 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))) '@storybook/react-vite': - specifier: ^9.1.5 + 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)) '@tailwindcss/typography': - specifier: ^0.5.16 + specifier: 0.5.16 version: 0.5.16(tailwindcss@3.4.17) '@tanstack/react-virtual': - specifier: ^3.13.12 + specifier: 3.13.12 version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@testing-library/dom': - specifier: ^10.4.1 + specifier: 10.4.1 version: 10.4.1 '@testing-library/jest-dom': - specifier: ^6.6.3 + specifier: 6.6.3 version: 6.6.3 '@testing-library/react': - specifier: ^16.3.0 + specifier: 16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@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) + '@testing-library/user-event': + specifier: 14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) + '@types/dagre': + specifier: 0.7.53 + version: 0.7.53 + '@types/lodash': + specifier: 4.17.20 + version: 4.17.20 '@types/node': - specifier: ^20.11.25 + specifier: 20.11.25 version: 20.11.25 '@types/react': - specifier: ^18.3.23 + specifier: 18.3.23 version: 18.3.23 '@types/react-dom': - specifier: ^18.3.7 + specifier: 18.3.7 version: 18.3.7(@types/react@18.3.23) '@vitejs/plugin-react': - specifier: ^4.7.0 + 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)) '@vitest/browser': - specifier: ^3.2.4 + 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) '@xyflow/react': - specifier: ^12.8.4 + 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) autoprefixer: - specifier: ^10.4.21 + specifier: 10.4.21 version: 10.4.21(postcss@8.5.6) + browserslist: + specifier: 4.26.2 + version: 4.26.2 + caniuse-lite: + specifier: 1.0.30001746 + version: 1.0.30001746 class-variance-authority: - specifier: ^0.7.1 + specifier: 0.7.1 version: 0.7.1 clsx: - specifier: ^2.1.1 + specifier: 2.1.1 version: 2.1.1 + cronstrue: + specifier: 3.3.0 + version: 3.3.0 + dagre: + specifier: 0.8.5 + version: 0.8.5 + deepmerge: + specifier: 4.3.1 + version: 4.3.1 eslint: - specifier: ^9.31.0 + specifier: 9.31.0 version: 9.31.0(jiti@2.4.2) eslint-plugin-react-hooks: - specifier: ^5.2.0 + specifier: 5.2.0 version: 5.2.0(eslint@9.31.0(jiti@2.4.2)) eslint-plugin-storybook: - specifier: ^9.1.5 + 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) fuse.js: - specifier: ^7.1.0 + specifier: 7.1.0 version: 7.1.0 globals: - specifier: ^16.3.0 + specifier: 16.3.0 version: 16.3.0 + lodash: + specifier: 4.17.21 + version: 4.17.21 lucide-react: - specifier: ^0.542.0 + specifier: 0.542.0 version: 0.542.0(react@18.3.1) playwright: - specifier: ^1.54.1 + specifier: 1.54.1 version: 1.54.1 postcss: - specifier: ^8.5.6 + specifier: 8.5.6 version: 8.5.6 react: - specifier: ^18.3.1 + specifier: 18.3.1 version: 18.3.1 react-dom: - specifier: ^18.3.1 + specifier: 18.3.1 version: 18.3.1(react@18.3.1) storybook: - specifier: ^9.1.5 + 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)) syncpack: - specifier: ^13.0.4 + specifier: 13.0.4 version: 13.0.4(typescript@5.8.3) tailwind-merge: - specifier: ^3.3.1 + specifier: 3.3.1 version: 3.3.1 tailwind-scrollbar: - specifier: ^3.1.0 + specifier: 3.1.0 version: 3.1.0(tailwindcss@3.4.17) tailwindcss: - specifier: ^3.4.17 + specifier: 3.4.17 version: 3.4.17 typescript: - specifier: ^5.8.3 + specifier: 5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.38.0 + specifier: 8.38.0 version: 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) vite: - specifier: ^6.3.5 + 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) vite-plugin-dts: - specifier: ^4.5.4 + 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)) vite-plugin-static-copy: - specifier: ^3.1.1 + 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)) vitest: - specifier: ^3.2.4 + 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) packages: @@ -766,8 +793,8 @@ packages: '@codemirror/autocomplete@6.18.6': resolution: {integrity: sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==} - '@codemirror/autocomplete@6.18.7': - resolution: {integrity: sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ==} + '@codemirror/autocomplete@6.19.0': + resolution: {integrity: sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==} '@codemirror/commands@6.8.1': resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==} @@ -802,8 +829,8 @@ packages: '@codemirror/view@6.38.1': resolution: {integrity: sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==} - '@codemirror/view@6.38.2': - resolution: {integrity: sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==} + '@codemirror/view@6.38.4': + resolution: {integrity: sha512-hduz0suCcUSC/kM8Fq3A9iLwInJDl8fD1xLpTIk+5xkNm8z/FT7UsIa9sOXrkpChh+XXc18RzswE8QqELsVl+g==} '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} @@ -1179,6 +1206,9 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} @@ -2587,6 +2617,9 @@ packages: '@types/d3@7.4.3': resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/dagre@0.7.53': + resolution: {integrity: sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2629,6 +2662,9 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -3244,6 +3280,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.8.9: + resolution: {integrity: sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==} + hasBin: true + better-opn@3.0.2: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} @@ -3278,13 +3318,8 @@ packages: browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - browserslist@4.25.1: - resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - browserslist@4.25.4: - resolution: {integrity: sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==} + browserslist@4.26.2: + resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3340,11 +3375,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001727: - resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} - - caniuse-lite@1.0.30001741: - resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==} + caniuse-lite@1.0.30001746: + resolution: {integrity: sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -3538,6 +3570,10 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cronstrue@3.3.0: + resolution: {integrity: sha512-iwJytzJph1hosXC09zY8F5ACDJKerr0h3/2mOxg9+5uuFObYlgK0m35uUPk4GCvhHc2abK7NfnR9oMqY0qZFAg==} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3602,6 +3638,9 @@ packages: resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} engines: {node: '>=12'} + dagre@0.8.5: + resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -3655,6 +3694,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + default-browser-id@5.0.0: resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} engines: {node: '>=18'} @@ -3762,11 +3805,8 @@ packages: effect@3.17.9: resolution: {integrity: sha512-Nkkn9n1zhy30Dq0MpQatDCH7nfYnOIiebkOHNxmmvoVnEDKCto+2ZwDDWFGzcN/ojwfqjRXWGC9Lo91K5kwZCg==} - electron-to-chromium@1.5.190: - resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==} - - electron-to-chromium@1.5.215: - resolution: {integrity: sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==} + electron-to-chromium@1.5.227: + resolution: {integrity: sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==} elkjs@0.8.2: resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} @@ -4209,6 +4249,9 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphlib@2.1.8: + resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -5139,11 +5182,8 @@ packages: node-readfiles@0.2.0: resolution: {integrity: sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==} - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - - node-releases@2.0.20: - resolution: {integrity: sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==} + node-releases@2.0.21: + resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} node-sarif-builder@3.2.0: resolution: {integrity: sha512-kVIOdynrF2CRodHZeP/97Rh1syTUHBNiw17hUCIVhlhEsWlfJm19MuO56s4MdKbr22xWx6mzMnNAgXzVlIYM9Q==} @@ -7047,7 +7087,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.1 + browserslist: 4.26.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -7224,11 +7264,11 @@ snapshots: '@codemirror/view': 6.38.1 '@lezer/common': 1.2.3 - '@codemirror/autocomplete@6.18.7': + '@codemirror/autocomplete@6.19.0': dependencies: '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.2 + '@codemirror/view': 6.38.4 '@lezer/common': 1.2.3 '@codemirror/commands@6.8.1': @@ -7267,7 +7307,7 @@ snapshots: '@codemirror/language@6.11.3': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.2 + '@codemirror/view': 6.38.4 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 @@ -7280,13 +7320,13 @@ snapshots: '@codemirror/lint@6.8.5': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.2 + '@codemirror/view': 6.38.4 crelt: 1.0.6 '@codemirror/search@6.5.10': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.2 + '@codemirror/view': 6.38.4 crelt: 1.0.6 '@codemirror/state@6.5.2': @@ -7297,7 +7337,7 @@ snapshots: dependencies: '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.2 + '@codemirror/view': 6.38.4 '@lezer/highlight': 1.2.1 '@codemirror/view@6.38.1': @@ -7307,7 +7347,7 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 - '@codemirror/view@6.38.2': + '@codemirror/view@6.38.4': dependencies: '@codemirror/state': 6.5.2 crelt: 1.0.6 @@ -7617,7 +7657,7 @@ snapshots: '@jridgewell/source-map@0.3.11': dependencies: '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/sourcemap-codec@1.5.4': {} @@ -7633,6 +7673,11 @@ snapshots: '@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 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jsdevtools/ono@7.1.3': {} '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': @@ -9331,6 +9376,8 @@ snapshots: '@types/d3-transition': 3.0.9 '@types/d3-zoom': 3.0.8 + '@types/dagre@0.7.53': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -9378,6 +9425,8 @@ snapshots: dependencies: '@types/node': 20.11.25 + '@types/lodash@4.17.20': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -10173,8 +10222,8 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.6): dependencies: - browserslist: 4.25.1 - caniuse-lite: 1.0.30001727 + browserslist: 4.26.2 + caniuse-lite: 1.0.30001746 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -10208,6 +10257,8 @@ snapshots: base64-js@1.5.1: optional: true + baseline-browser-mapping@2.8.9: {} + better-opn@3.0.2: dependencies: open: 8.4.2 @@ -10244,19 +10295,13 @@ snapshots: browser-stdout@1.3.1: {} - browserslist@4.25.1: - dependencies: - caniuse-lite: 1.0.30001727 - electron-to-chromium: 1.5.190 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.1) - - browserslist@4.25.4: + browserslist@4.26.2: dependencies: - caniuse-lite: 1.0.30001741 - electron-to-chromium: 1.5.215 - node-releases: 2.0.20 - update-browserslist-db: 1.1.3(browserslist@4.25.4) + baseline-browser-mapping: 2.8.9 + caniuse-lite: 1.0.30001746 + electron-to-chromium: 1.5.227 + node-releases: 2.0.21 + update-browserslist-db: 1.1.3(browserslist@4.26.2) buffer-crc32@0.2.13: {} @@ -10315,9 +10360,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001727: {} - - caniuse-lite@1.0.30001741: {} + caniuse-lite@1.0.30001746: {} ccount@2.0.1: {} @@ -10437,13 +10480,13 @@ snapshots: codemirror@6.0.1: dependencies: - '@codemirror/autocomplete': 6.18.7 + '@codemirror/autocomplete': 6.19.0 '@codemirror/commands': 6.8.1 '@codemirror/language': 6.11.3 '@codemirror/lint': 6.8.5 '@codemirror/search': 6.5.10 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.2 + '@codemirror/view': 6.38.4 color-convert@2.0.1: dependencies: @@ -10506,6 +10549,8 @@ snapshots: crelt@1.0.6: {} + cronstrue@3.3.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -10569,6 +10614,11 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) + dagre@0.8.5: + dependencies: + graphlib: 2.1.8 + lodash: 4.17.21 + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -10620,6 +10670,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + default-browser-id@5.0.0: {} default-browser@5.2.1: @@ -10722,9 +10774,7 @@ snapshots: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 - electron-to-chromium@1.5.190: {} - - electron-to-chromium@1.5.215: {} + electron-to-chromium@1.5.227: {} elkjs@0.8.2: {} @@ -11274,6 +11324,10 @@ snapshots: graphemer@1.4.0: {} + graphlib@2.1.8: + dependencies: + lodash: 4.17.21 + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -12304,9 +12358,7 @@ snapshots: dependencies: es6-promise: 3.3.1 - node-releases@2.0.19: {} - - node-releases@2.0.20: {} + node-releases@2.0.21: {} node-sarif-builder@3.2.0: dependencies: @@ -13427,7 +13479,7 @@ snapshots: sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 glob: 10.4.5 lines-and-columns: 1.2.4 @@ -13579,7 +13631,7 @@ snapshots: terser-webpack-plugin@5.3.14(esbuild@0.25.8)(webpack@5.99.8(esbuild@0.25.8)): dependencies: - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 @@ -13871,15 +13923,9 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.1.3(browserslist@4.25.1): - dependencies: - browserslist: 4.25.1 - escalade: 3.2.0 - picocolors: 1.1.1 - - update-browserslist-db@1.1.3(browserslist@4.25.4): + update-browserslist-db@1.1.3(browserslist@4.26.2): dependencies: - browserslist: 4.25.4 + browserslist: 4.26.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -14235,7 +14281,7 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 - browserslist: 4.25.4 + browserslist: 4.26.2 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 diff --git a/web/common/.storybook/main.ts b/web/common/.storybook/main.ts index 8994b8a737..e916ea6f64 100644 --- a/web/common/.storybook/main.ts +++ b/web/common/.storybook/main.ts @@ -2,7 +2,7 @@ import type { StorybookConfig } from '@storybook/react-vite' const config: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-docs', '@storybook/addon-onboarding'], + addons: ['@storybook/addon-docs'], framework: { name: '@storybook/react-vite', options: {}, diff --git a/web/common/.syncpackrc b/web/common/.syncpackrc index 52d97009ce..edc87cc315 100644 --- a/web/common/.syncpackrc +++ b/web/common/.syncpackrc @@ -14,7 +14,7 @@ ], "semverGroups": [ { - "label": "Use caret ranges for all dependencies", + "label": "Use exact versions for all dependencies", "dependencies": [ "**" ], @@ -23,7 +23,7 @@ "peer", "prod" ], - "range": "^" + "range": "" } ] } \ No newline at end of file diff --git a/web/common/package-lock.json b/web/common/package-lock.json deleted file mode 100644 index eaaaee941b..0000000000 --- a/web/common/package-lock.json +++ /dev/null @@ -1,7183 +0,0 @@ -{ - "name": "@tobikodata/sqlmesh-common", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@tobikodata/sqlmesh-common", - "version": "0.0.1", - "license": "Apache-2.0", - "devDependencies": { - "@eslint/js": "^9.31.0", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-tooltip": "^1.2.8", - "@storybook/addon-docs": "^9.1.5", - "@storybook/addon-onboarding": "^9.1.5", - "@storybook/react-vite": "^9.1.5", - "@tailwindcss/typography": "^0.5.16", - "@tanstack/react-virtual": "^3.13.12", - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.3.0", - "@types/node": "^20.11.25", - "@types/react": "^18.3.23", - "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react": "^4.7.0", - "@vitest/browser": "^3.2.4", - "@xyflow/react": "^12.8.4", - "autoprefixer": "^10.4.21", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "eslint": "^9.31.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-storybook": "^9.1.5", - "fuse.js": "^7.1.0", - "globals": "^16.3.0", - "lucide-react": "^0.542.0", - "playwright": "^1.54.1", - "postcss": "^8.5.6", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "storybook": "^9.1.5", - "syncpack": "^13.0.4", - "tailwind-merge": "^3.3.1", - "tailwind-scrollbar": "^4.0.2", - "tailwindcss": "^3.4.17", - "typescript": "^5.8.3", - "typescript-eslint": "^8.38.0", - "vite": "^6.3.5", - "vite-plugin-dts": "^4.5.4", - "vite-plugin-static-copy": "^3.1.1", - "vitest": "^3.2.4" - }, - "peerDependencies": { - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-tooltip": "^1.2.8", - "@tailwindcss/typography": "^0.5.16", - "@tanstack/react-virtual": "^3.13.12", - "@xyflow/react": "^12.8.4", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "fuse.js": "^7.1.0", - "lucide-react": "^0.542.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "tailwind-merge": "^3.3.1", - "tailwindcss": "^3.4.17" - } - }, - "../../node_modules/.pnpm/@eslint+js@9.31.0/node_modules/@eslint/js": { - "version": "9.31.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "../../node_modules/.pnpm/@vitejs+plugin-react@4.7.0_vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terse_p5zuafkpgv2vlm3nhxz3zj4hsu/node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "devDependencies": { - "@vitejs/react-common": "workspace:*", - "babel-plugin-react-compiler": "19.1.0-rc.2", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "rolldown": "1.0.0-beta.27", - "tsdown": "^0.12.9", - "vitest": "^3.2.4" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "../../node_modules/.pnpm/eslint@9.31.0_jiti@2.4.2/node_modules/eslint": { - "version": "9.31.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "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.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "devDependencies": { - "@arethetypeswrong/cli": "^0.18.0", - "@babel/core": "^7.4.3", - "@babel/preset-env": "^7.4.3", - "@cypress/webpack-preprocessor": "^6.0.2", - "@eslint/json": "^0.13.0", - "@trunkio/launcher": "^1.3.4", - "@types/esquery": "^1.5.4", - "@types/node": "^22.13.14", - "@typescript-eslint/parser": "^8.4.0", - "babel-loader": "^8.0.5", - "c8": "^7.12.0", - "chai": "^4.0.1", - "cheerio": "^0.22.0", - "common-tags": "^1.8.0", - "core-js": "^3.1.3", - "cypress": "^14.1.0", - "ejs": "^3.0.2", - "eslint": "file:.", - "eslint-config-eslint": "file:packages/eslint-config-eslint", - "eslint-plugin-eslint-plugin": "^6.0.0", - "eslint-plugin-expect-type": "^0.6.0", - "eslint-plugin-yml": "^1.14.0", - "eslint-release": "^3.3.0", - "eslint-rule-composer": "^0.3.0", - "eslump": "^3.0.0", - "esprima": "^4.0.1", - "fast-glob": "^3.2.11", - "fs-teardown": "^0.1.3", - "glob": "^10.0.0", - "globals": "^16.2.0", - "got": "^11.8.3", - "gray-matter": "^4.0.3", - "jiti": "^2.2.0", - "jiti-v2.0": "npm:jiti@2.0.x", - "jiti-v2.1": "npm:jiti@2.1.x", - "knip": "^5.60.2", - "lint-staged": "^11.0.0", - "load-perf": "^0.2.0", - "markdown-it": "^12.2.0", - "markdown-it-container": "^3.0.0", - "marked": "^4.0.8", - "metascraper": "^5.25.7", - "metascraper-description": "^5.25.7", - "metascraper-image": "^5.29.3", - "metascraper-logo": "^5.25.7", - "metascraper-logo-favicon": "^5.25.7", - "metascraper-title": "^5.25.7", - "mocha": "^11.7.1", - "node-polyfill-webpack-plugin": "^1.0.3", - "npm-license": "^0.3.3", - "pirates": "^4.0.5", - "progress": "^2.0.3", - "proxyquire": "^2.0.1", - "recast": "^0.23.0", - "regenerator-runtime": "^0.14.0", - "semver": "^7.5.3", - "shelljs": "^0.10.0", - "sinon": "^11.0.0", - "typescript": "^5.3.3", - "webpack": "^5.23.0", - "webpack-cli": "^4.5.0", - "yorkie": "^2.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "../../node_modules/.pnpm/typescript-eslint@8.38.0_eslint@9.31.0_jiti@2.4.2__typescript@5.8.3/node_modules/typescript-eslint": { - "version": "8.38.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.38.0", - "@typescript-eslint/parser": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/utils": "8.38.0" - }, - "devDependencies": { - "@vitest/coverage-v8": "^3.1.3", - "eslint": "*", - "rimraf": "*", - "typescript": "*", - "vitest": "^3.1.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript": { - "version": "5.8.3", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "devDependencies": { - "@dprint/formatter": "^0.4.1", - "@dprint/typescript": "0.93.3", - "@esfx/canceltoken": "^1.0.0", - "@eslint/js": "^9.17.0", - "@octokit/rest": "^21.0.2", - "@types/chai": "^4.3.20", - "@types/diff": "^5.2.3", - "@types/minimist": "^1.2.5", - "@types/mocha": "^10.0.10", - "@types/ms": "^0.7.34", - "@types/node": "latest", - "@types/source-map-support": "^0.5.10", - "@types/which": "^3.0.4", - "@typescript-eslint/rule-tester": "^8.18.1", - "@typescript-eslint/type-utils": "^8.18.1", - "@typescript-eslint/utils": "^8.18.1", - "azure-devops-node-api": "^14.1.0", - "c8": "^10.1.3", - "chai": "^4.5.0", - "chalk": "^4.1.2", - "chokidar": "^3.6.0", - "diff": "^5.2.0", - "dprint": "^0.47.6", - "esbuild": "^0.24.0", - "eslint": "^9.17.0", - "eslint-formatter-autolinkable-stylish": "^1.4.0", - "eslint-plugin-regexp": "^2.7.0", - "fast-xml-parser": "^4.5.1", - "glob": "^10.4.5", - "globals": "^15.13.0", - "hereby": "^1.10.0", - "jsonc-parser": "^3.3.1", - "knip": "^5.41.0", - "minimist": "^1.2.8", - "mocha": "^10.8.2", - "mocha-fivemat-progress-reporter": "^0.1.0", - "monocart-coverage-reports": "^2.11.4", - "ms": "^2.1.3", - "playwright": "^1.49.1", - "source-map-support": "^0.5.21", - "tslib": "^2.8.1", - "typescript": "^5.7.2", - "typescript-eslint": "^8.18.1", - "which": "^3.0.1" - }, - "engines": { - "node": ">=14.17" - } - }, - "../../node_modules/.pnpm/vite-plugin-dts@4.5.4_@types+node@24.1.0_rollup@4.45.1_typescript@5.8.3_vite@6.3.5_@types+nod_ddgp24sr5pf6ze3b5hs7mrzr5e/node_modules/vite-plugin-dts": { - "version": "4.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@microsoft/api-extractor": "^7.50.1", - "@rollup/pluginutils": "^5.1.4", - "@volar/typescript": "^2.4.11", - "@vue/language-core": "2.2.0", - "compare-versions": "^6.1.1", - "debug": "^4.4.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.0.0", - "magic-string": "^0.30.17" - }, - "devDependencies": { - "@commitlint/cli": "^19.7.1", - "@types/debug": "^4.1.12", - "@types/minimist": "^1.2.5", - "@types/node": "^22.13.5", - "@types/prompts": "^2.4.9", - "@types/semver": "^7.5.8", - "@vexip-ui/commitlint-config": "^0.5.0", - "@vexip-ui/eslint-config": "^0.12.1", - "@vexip-ui/prettier-config": "^1.0.0", - "@vexip-ui/scripts": "^1.2.0", - "@vue/eslint-config-standard": "^8.0.1", - "@vue/eslint-config-typescript": "^13.0.0", - "conventional-changelog-cli": "^5.0.0", - "eslint": "^8.57.0", - "execa": "^9.5.2", - "husky": "^9.1.7", - "is-ci": "^4.1.0", - "lint-staged": "^15.4.3", - "minimist": "^1.2.8", - "pinst": "^3.0.0", - "prettier": "^3.5.2", - "pretty-quick": "^4.0.0", - "prompts": "^2.4.2", - "rimraf": "^6.0.1", - "semver": "^7.7.1", - "tsx": "^4.19.3", - "typescript": "5.7.3", - "unbuild": "^3.3.1", - "vite": "^6.2.0", - "vitest": "^3.0.7" - }, - "peerDependencies": { - "typescript": "*", - "vite": "*" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "../../node_modules/.pnpm/vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terser@5.43.1_tsx@4.20.3_yaml@2.8.0/node_modules/vite": { - "version": "6.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "devDependencies": { - "@ampproject/remapping": "^2.3.0", - "@babel/parser": "^7.27.0", - "@jridgewell/trace-mapping": "^0.3.25", - "@polka/compression": "^1.0.0-next.25", - "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^28.0.3", - "@rollup/plugin-dynamic-import-vars": "2.1.4", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "16.0.1", - "@rollup/pluginutils": "^5.1.4", - "@types/escape-html": "^1.0.4", - "@types/pnpapi": "^0.0.5", - "artichokie": "^0.3.1", - "cac": "^6.7.14", - "chokidar": "^3.6.0", - "connect": "^3.7.0", - "convert-source-map": "^2.0.0", - "cors": "^2.8.5", - "cross-spawn": "^7.0.6", - "debug": "^4.4.0", - "dep-types": "link:./src/types", - "dotenv": "^16.5.0", - "dotenv-expand": "^12.0.2", - "es-module-lexer": "^1.6.0", - "escape-html": "^1.0.3", - "estree-walker": "^3.0.3", - "etag": "^1.8.1", - "http-proxy": "^1.18.1", - "launch-editor-middleware": "^2.10.0", - "lightningcss": "^1.29.3", - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "mrmime": "^2.0.1", - "nanoid": "^5.1.5", - "open": "^10.1.1", - "parse5": "^7.2.1", - "pathe": "^2.0.3", - "periscopic": "^4.0.2", - "picocolors": "^1.1.1", - "postcss-import": "^16.1.0", - "postcss-load-config": "^6.0.1", - "postcss-modules": "^6.0.1", - "resolve.exports": "^2.0.3", - "rollup-plugin-dts": "^6.2.1", - "rollup-plugin-esbuild": "^6.2.1", - "rollup-plugin-license": "^3.6.0", - "sass": "^1.86.3", - "sass-embedded": "^1.86.3", - "sirv": "^3.0.1", - "source-map-support": "^0.5.21", - "strip-literal": "^3.0.0", - "terser": "^5.39.0", - "tsconfck": "^3.1.5", - "tslib": "^2.8.1", - "types": "link:./types", - "ufo": "^1.6.1", - "ws": "^8.18.1" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint/js": { - "resolved": "../../node_modules/.pnpm/@eslint+js@9.31.0/node_modules/@eslint/js", - "link": true - }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.1.tgz", - "integrity": "sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^10.0.0", - "magic-string": "^0.30.0", - "react-docgen-typescript": "^2.2.2" - }, - "peerDependencies": { - "typescript": ">= 4.3.x", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mdx-js/react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", - "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mdx": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/pluginutils": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", - "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@storybook/addon-docs": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.5.tgz", - "integrity": "sha512-q1j5RRElxFSnHOh60eS3dS2TAyAHzcQeH/2B9UXo6MUHu7HmhNpw3qt2YibIw0zEogHCvZhLNx6TNzSy+7wRUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "9.1.5", - "@storybook/icons": "^1.4.0", - "@storybook/react-dom-shim": "9.1.5", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^9.1.5" - } - }, - "node_modules/@storybook/addon-onboarding": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-9.1.5.tgz", - "integrity": "sha512-UJpkWLbugcSGzSUzivTTNdO0Y8gpAn//qJzn2TobwkPJgSwQEoHcjUfWjgZ3mSpQrSQO2e1O1yC3SJTBQt/fqQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^9.1.5" - } - }, - "node_modules/@storybook/builder-vite": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.5.tgz", - "integrity": "sha512-sgt/9+Yl/5O7Bj5hdbHfadN8e/e4CNiDZKDcbLOMpOjKKoqF8vm19I1QocWIAiKjTOhF+4E9v9LddjtAGnfqHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/csf-plugin": "9.1.5", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^9.1.5", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@storybook/csf-plugin": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.5.tgz", - "integrity": "sha512-PmHuF+j11Z7BxAI2/4wQYn0gH1d67gNvycyR+EWgp4P/AWam9wFbuI/T1R45CRQTV2/VrfGdts/tFrvo5kXWig==", - "dev": true, - "license": "MIT", - "dependencies": { - "unplugin": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^9.1.5" - } - }, - "node_modules/@storybook/global": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", - "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@storybook/icons": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.4.0.tgz", - "integrity": "sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" - } - }, - "node_modules/@storybook/react": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.5.tgz", - "integrity": "sha512-fBVP7Go09gzpImtaMcZ2DipLEWdWeTmz7BrACr3Z8uCyKcoH8/d1Wv0JgIiBo1UKDh5ZgYx5pLafaPNqmVAepg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "9.1.5" - }, - "engines": { - "node": ">=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.5", - "typescript": ">= 4.9.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/react-dom-shim": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.5.tgz", - "integrity": "sha512-blSq9uzSYnfgEYPHYKgM5O14n8hbXNiXx2GiVJyDSg8QPNicbsBg+lCb1TC7/USfV26pNZr/lGNNKGkcCEN6Gw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.5" - } - }, - "node_modules/@storybook/react-vite": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.5.tgz", - "integrity": "sha512-OYbkHHNCrn8MNPd+4KxMjcSR4M/YHa84h8sWDUHhKRTRtZFmj8i/QDW3E8tGx2BRLxXw3dTYe9J5UYBhJDDxFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", - "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "9.1.5", - "@storybook/react": "9.1.5", - "find-up": "^7.0.0", - "magic-string": "^0.30.0", - "react-docgen": "^8.0.0", - "resolve": "^1.22.8", - "tsconfig-paths": "^4.2.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.5", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@tailwindcss/typography": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", - "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.castarray": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "postcss-selector-parser": "6.0.10" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" - } - }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.12", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", - "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.13.12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.12", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", - "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz", - "integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", - "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", - "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*" - } - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/doctrine": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", - "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mdx": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", - "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz", - "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/prismjs": { - "version": "1.26.5", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", - "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.23", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", - "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/resolve": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", - "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", - "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.1", - "@typescript-eslint/types": "^8.39.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", - "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", - "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", - "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", - "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.39.1", - "@typescript-eslint/tsconfig-utils": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", - "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", - "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.39.1", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "resolved": "../../node_modules/.pnpm/@vitejs+plugin-react@4.7.0_vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terse_p5zuafkpgv2vlm3nhxz3zj4hsu/node_modules/@vitejs/plugin-react", - "link": true - }, - "node_modules/@vitest/browser": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.2.4.tgz", - "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@testing-library/dom": "^10.4.0", - "@testing-library/user-event": "^14.6.1", - "@vitest/mocker": "3.2.4", - "@vitest/utils": "3.2.4", - "magic-string": "^0.30.17", - "sirv": "^3.0.1", - "tinyrainbow": "^2.0.0", - "ws": "^8.18.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "playwright": "*", - "vitest": "3.2.4", - "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "playwright": { - "optional": true - }, - "safaridriver": { - "optional": true - }, - "webdriverio": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@xyflow/react": { - "version": "12.8.4", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.4.tgz", - "integrity": "sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xyflow/system": "0.0.68", - "classcat": "^5.0.3", - "zustand": "^4.4.0" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@xyflow/system": { - "version": "0.0.68", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.68.tgz", - "integrity": "sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-drag": "^3.0.7", - "@types/d3-interpolate": "^3.0.4", - "@types/d3-selection": "^3.0.10", - "@types/d3-transition": "^3.0.8", - "@types/d3-zoom": "^3.0.8", - "d3-drag": "^3.0.0", - "d3-interpolate": "^3.0.1", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-types": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", - "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/better-opn": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", - "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "open": "^8.0.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", - "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001733", - "electron-to-chromium": "^1.5.199", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001735", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", - "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", - "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk-template": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-1.1.2.tgz", - "integrity": "sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.2.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/chalk/chalk-template?sponsor=1" - } - }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/classcat": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", - "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cmdk": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", - "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-id": "^1.1.0", - "@radix-ui/react-primitive": "^2.0.2" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "dev": true, - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "dev": true, - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/effect": { - "version": "3.17.13", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.17.13.tgz", - "integrity": "sha512-JMz5oBxs/6mu4FP9Csjub4jYMUwMLrp+IzUmSDVIzn2NoeoyOXMl7x1lghfr3dLKWffWrdnv/d8nFFdgrHXPqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "fast-check": "^3.23.1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.201", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.201.tgz", - "integrity": "sha512-ZG65vsrLClodGqywuigc+7m0gr4ISoTQttfVh7nfpLv0M7SIwF4WbFNEOywcqTiujs12AUeeXbFyQieDICAIxg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/enquirer/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" - } - }, - "node_modules/esbuild-register": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", - "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "esbuild": ">=0.12 <1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint": { - "resolved": "../../node_modules/.pnpm/eslint@9.31.0_jiti@2.4.2/node_modules/eslint", - "link": true - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-storybook": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.5.tgz", - "integrity": "sha512-vCfaZ2Wk1N1vvK4vmNZoA6y2CYxJwbgIs6BE8/toPf4Z6hCAipoobP6a/30Rs0g/B2TSxTSj41TfrJKJrowpjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "^8.8.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "eslint": ">=8", - "storybook": "^9.1.5" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-check": { - "version": "3.23.2", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", - "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^6.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", - "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^7.2.0", - "path-exists": "^5.0.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-extra": { - "version": "11.3.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", - "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fuse.js": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", - "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", - "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", - "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.3", - "path-type": "^6.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby/node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.castarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", - "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", - "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/lucide-react": { - "version": "0.542.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.542.0.tgz", - "integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-package-arg": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", - "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", - "dev": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ora/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", - "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-type": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", - "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/playwright": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", - "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.55.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", - "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/prism-react-renderer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", - "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prismjs": "^1.26.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, - "node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-docgen": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.0.tgz", - "integrity": "sha512-kmob/FOTwep7DUWf9KjuenKX0vyvChr3oTdvvPt09V60Iz75FJp+T/0ZeHMbAfJj2WaVWqAPP5Hmm3PYzSPPKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.18.9", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9", - "@types/babel__core": "^7.18.0", - "@types/babel__traverse": "^7.18.0", - "@types/doctrine": "^0.0.9", - "@types/resolve": "^1.20.2", - "doctrine": "^3.0.0", - "resolve": "^1.22.1", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": "^20.9.0 || >=22" - } - }, - "node_modules/react-docgen-typescript": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", - "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "typescript": ">= 4.3.x" - } - }, - "node_modules/react-docgen/node_modules/strip-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", - "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-remove-scroll": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/read-yaml-file": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-2.1.0.tgz", - "integrity": "sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-yaml": "^4.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": ">=10.13" - } - }, - "node_modules/read-yaml-file/node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/recast": { - "version": "0.23.11", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", - "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "dev": true, - "license": "MIT" - }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/storybook": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.5.tgz", - "integrity": "sha512-cGwJ2AE6nxlwqQlOiI+HKX5qa7+FOV7Ha7Qa+GoASBIQSSnLfbY6UldgAxHCJGJOFtgW/wuqfDtNvni6sj1/OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/user-event": "^14.6.1", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/spy": "3.2.4", - "better-opn": "^3.0.2", - "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", - "esbuild-register": "^3.5.0", - "recast": "^0.23.5", - "semver": "^7.6.2", - "ws": "^8.18.0" - }, - "bin": { - "storybook": "bin/index.cjs" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "prettier": "^2 || ^3" - }, - "peerDependenciesMeta": { - "prettier": { - "optional": true - } - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-literal": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", - "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/syncpack": { - "version": "13.0.4", - "resolved": "https://registry.npmjs.org/syncpack/-/syncpack-13.0.4.tgz", - "integrity": "sha512-kJ9VlRxNCsBD5pJAE29oXeBYbPLhEySQmK4HdpsLv81I6fcDDW17xeJqMwiU3H7/woAVsbgq25DJNS8BeiN5+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.4.1", - "chalk-template": "^1.1.0", - "commander": "^13.1.0", - "cosmiconfig": "^9.0.0", - "effect": "^3.13.7", - "enquirer": "^2.4.1", - "fast-check": "^3.23.2", - "globby": "^14.1.0", - "jsonc-parser": "^3.3.1", - "minimatch": "9.0.5", - "npm-package-arg": "^12.0.2", - "ora": "^8.2.0", - "prompts": "^2.4.2", - "read-yaml-file": "^2.1.0", - "semver": "^7.7.1", - "tightrope": "0.2.0", - "ts-toolbelt": "^9.6.0" - }, - "bin": { - "syncpack": "dist/bin.js", - "syncpack-fix-mismatches": "dist/bin-fix-mismatches/index.js", - "syncpack-format": "dist/bin-format/index.js", - "syncpack-lint": "dist/bin-lint/index.js", - "syncpack-lint-semver-ranges": "dist/bin-lint-semver-ranges/index.js", - "syncpack-list": "dist/bin-list/index.js", - "syncpack-list-mismatches": "dist/bin-list-mismatches/index.js", - "syncpack-prompt": "dist/bin-prompt/index.js", - "syncpack-set-semver-ranges": "dist/bin-set-semver-ranges/index.js", - "syncpack-update": "dist/bin-update/index.js" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/syncpack/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tailwind-merge": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwind-scrollbar": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-4.0.2.tgz", - "integrity": "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "prism-react-renderer": "^2.4.1" - }, - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "tailwindcss": "4.x" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tightrope": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/tightrope/-/tightrope-0.2.0.tgz", - "integrity": "sha512-Kw36UHxJEELq2VUqdaSGR2/8cAsPgMtvX8uGVU6Jk26O66PhXec0A5ZnRYs47btbtwPDpXXF66+Fo3vimCM9aQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", - "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.10" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/ts-toolbelt": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", - "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/typescript": { - "resolved": "../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript", - "link": true - }, - "node_modules/typescript-eslint": { - "resolved": "../../node_modules/.pnpm/typescript-eslint@8.38.0_eslint@9.31.0_jiti@2.4.2__typescript@5.8.3/node_modules/typescript-eslint", - "link": true - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unplugin": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", - "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.14.0", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/validate-npm-package-name": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", - "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/vite": { - "resolved": "../../node_modules/.pnpm/vite@6.3.5_@types+node@24.1.0_jiti@2.4.2_lightningcss@1.30.1_terser@5.43.1_tsx@4.20.3_yaml@2.8.0/node_modules/vite", - "link": true - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-plugin-dts": { - "resolved": "../../node_modules/.pnpm/vite-plugin-dts@4.5.4_@types+node@24.1.0_rollup@4.45.1_typescript@5.8.3_vite@6.3.5_@types+nod_ddgp24sr5pf6ze3b5hs7mrzr5e/node_modules/vite-plugin-dts", - "link": true - }, - "node_modules/vite-plugin-static-copy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.2.tgz", - "integrity": "sha512-aVmYOzptLVOI2b1jL+cmkF7O6uhRv1u5fvOkQgbohWZp2CbR22kn9ZqkCUIt9umKF7UhdbsEpshn1rf4720QFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.6.0", - "fs-extra": "^11.3.0", - "p-map": "^7.0.3", - "picocolors": "^1.1.1", - "tinyglobby": "^0.2.14" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@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.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "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": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - } - } -} diff --git a/web/common/package.json b/web/common/package.json index 5f869c8a25..ef91337174 100644 --- a/web/common/package.json +++ b/web/common/package.json @@ -2,46 +2,55 @@ "name": "@tobikodata/sqlmesh-common", "version": "0.0.1", "devDependencies": { - "@eslint/js": "^9.31.0", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-tooltip": "^1.2.8", - "@storybook/addon-docs": "^9.1.5", - "@storybook/react-vite": "^9.1.5", - "@tailwindcss/typography": "^0.5.16", - "@tanstack/react-virtual": "^3.13.12", - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.3.0", - "@types/node": "^20.11.25", - "@types/react": "^18.3.23", - "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react": "^4.7.0", - "@vitest/browser": "^3.2.4", - "@xyflow/react": "^12.8.4", - "autoprefixer": "^10.4.21", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "eslint": "^9.31.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-storybook": "^9.1.5", - "fuse.js": "^7.1.0", - "globals": "^16.3.0", - "lucide-react": "^0.542.0", - "playwright": "^1.54.1", - "postcss": "^8.5.6", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "storybook": "^9.1.5", - "syncpack": "^13.0.4", - "tailwind-merge": "^3.3.1", - "tailwind-scrollbar": "^3.1.0", - "tailwindcss": "^3.4.17", - "typescript": "^5.8.3", - "typescript-eslint": "^8.38.0", - "vite": "^6.3.5", - "vite-plugin-dts": "^4.5.4", - "vite-plugin-static-copy": "^3.1.1", - "vitest": "^3.2.4" + "@eslint/js": "9.31.0", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-tooltip": "1.2.8", + "@storybook/addon-docs": "9.1.5", + "@storybook/react-vite": "9.1.5", + "@tailwindcss/typography": "0.5.16", + "@tanstack/react-virtual": "3.13.12", + "@testing-library/dom": "10.4.1", + "@testing-library/jest-dom": "6.6.3", + "@testing-library/react": "16.3.0", + "@testing-library/user-event": "14.6.1", + "@types/dagre": "0.7.53", + "@types/lodash": "4.17.20", + "@types/node": "20.11.25", + "@types/react": "18.3.23", + "@types/react-dom": "18.3.7", + "@vitejs/plugin-react": "4.7.0", + "@vitest/browser": "3.2.4", + "@xyflow/react": "12.8.4", + "autoprefixer": "10.4.21", + "browserslist": "4.26.2", + "caniuse-lite": "1.0.30001746", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cronstrue": "3.3.0", + "dagre": "0.8.5", + "deepmerge": "4.3.1", + "eslint": "9.31.0", + "eslint-plugin-react-hooks": "5.2.0", + "eslint-plugin-storybook": "9.1.5", + "fuse.js": "7.1.0", + "globals": "16.3.0", + "lodash": "4.17.21", + "lucide-react": "0.542.0", + "playwright": "1.54.1", + "postcss": "8.5.6", + "react": "18.3.1", + "react-dom": "18.3.1", + "storybook": "9.1.5", + "syncpack": "13.0.4", + "tailwind-merge": "3.3.1", + "tailwind-scrollbar": "3.1.0", + "tailwindcss": "3.4.17", + "typescript": "5.8.3", + "typescript-eslint": "8.38.0", + "vite": "6.3.5", + "vite-plugin-dts": "4.5.4", + "vite-plugin-static-copy": "3.1.1", + "vitest": "3.2.4" }, "exports": { ".": { @@ -56,7 +65,17 @@ }, "./styles/*": "./dist/styles/*", "./design/*": "./dist/styles/design/*", - "./configs/*": "./dist/configs/*" + "./configs/*": "./dist/configs/*", + "./lineage": { + "import": { + "types": "./dist/lineage/index.d.ts", + "default": "./dist/lineage/index.es.js" + }, + "require": { + "types": "./dist/lineage/index.d.ts", + "default": "./dist/lineage/index.umd.js" + } + } }, "files": [ "/dist" @@ -65,20 +84,24 @@ "main": "dist/sqlmesh-common.umd.js", "module": "dist/sqlmesh-common.es.js", "peerDependencies": { - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-tooltip": "^1.2.8", - "@tailwindcss/typography": "^0.5.16", - "@tanstack/react-virtual": "^3.13.12", - "@xyflow/react": "^12.8.4", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "fuse.js": "^7.1.0", - "lucide-react": "^0.542.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "tailwind-merge": "^3.3.1", - "tailwind-scrollbar": "^3.1.0", - "tailwindcss": "^3.4.17" + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-tooltip": "1.2.8", + "@tailwindcss/typography": "0.5.16", + "@tanstack/react-virtual": "3.13.12", + "@xyflow/react": "12.8.4", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cronstrue": "3.3.0", + "dagre": "0.8.5", + "deepmerge": "4.3.1", + "fuse.js": "7.1.0", + "lodash": "4.17.21", + "lucide-react": "0.542.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "tailwind-merge": "3.3.1", + "tailwind-scrollbar": "3.1.0", + "tailwindcss": "3.4.17" }, "private": false, "repository": "TobikoData/sqlmesh", diff --git a/web/common/src/components/CopyButton/CopyButton.tsx b/web/common/src/components/CopyButton/CopyButton.tsx index 45aae3d817..3647121f82 100644 --- a/web/common/src/components/CopyButton/CopyButton.tsx +++ b/web/common/src/components/CopyButton/CopyButton.tsx @@ -36,6 +36,7 @@ export const CopyButton = React.forwardRef( onClick={e => { e.stopPropagation() copyToClipboard(text) + onClick?.(e) }} disabled={disabled || !!isCopied} {...props} diff --git a/web/common/src/components/Input/Input.css b/web/common/src/components/Input/Input.css new file mode 100644 index 0000000000..0baae3c6bb --- /dev/null +++ b/web/common/src/components/Input/Input.css @@ -0,0 +1,7 @@ +:root { + --color-input-background: var(--color-light); + --color-input-background-translucid: var(--color-neutral-5); + --color-input-foreground: var(--color-prose); + --color-input-placeholder: var(--color-neutral-400); + --color-input-border: var(--color-neutral-300); +} diff --git a/web/common/src/components/Input/Input.tsx b/web/common/src/components/Input/Input.tsx index 5c25b0a698..10ba151ab4 100644 --- a/web/common/src/components/Input/Input.tsx +++ b/web/common/src/components/Input/Input.tsx @@ -3,6 +3,8 @@ import { cn } from '@/utils' import type { Size } from '@/types' import { cva } from 'class-variance-authority' +import './Input.css' + export interface InputProps extends React.ComponentProps<'input'> { inputSize?: Size } @@ -15,9 +17,9 @@ export const Input = React.forwardRef( className={cn( inputVariants({ size: inputSize }), 'border items-center border-input-border bg-input-background text-input-foreground transition-colors placeholder:text-input-placeholder', - 'file:border-0 file:h-fit file:bg-background-lucid file:rounded-sm file:flex-col file:mt-0.5', + 'file:border-0 file:h-fit file:bg-background-translucid file:rounded-sm file:flex-col file:mt-0.5', type === 'file' && - 'bg-input-background-lucid border-[transparent] pl-1', + 'bg-input-background-translucid border-[transparent] pl-1', className, )} ref={ref} diff --git a/web/common/src/components/Lineage/Lineage.css b/web/common/src/components/Lineage/Lineage.css new file mode 100644 index 0000000000..7855ced10a --- /dev/null +++ b/web/common/src/components/Lineage/Lineage.css @@ -0,0 +1,3 @@ +.react-flow__node { + height: auto !important; +} diff --git a/web/common/src/components/Lineage/LineageColumnLevel/ColumnLevelLineageContext.ts b/web/common/src/components/Lineage/LineageColumnLevel/ColumnLevelLineageContext.ts new file mode 100644 index 0000000000..227fc70394 --- /dev/null +++ b/web/common/src/components/Lineage/LineageColumnLevel/ColumnLevelLineageContext.ts @@ -0,0 +1,101 @@ +import React from 'react' + +import { type PortId } from '../utils' + +export type LineageColumn = { + source?: string | null + expression?: string | null + models: Record +} + +export type ColumnLevelModelConnections< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, +> = Record +export type ColumnLevelDetails< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, +> = Omit & { + models: ColumnLevelModelConnections< + TAdjacencyListKey, + TAdjacencyListColumnKey + > +} +export type ColumnLevelConnections< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, +> = Record< + TAdjacencyListColumnKey, + ColumnLevelDetails +> +export type ColumnLevelLineageAdjacencyList< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, +> = Record< + TAdjacencyListKey, + ColumnLevelConnections +> + +export type ColumnLevelLineageContextValue< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, + TColumnID extends string = PortId, +> = { + adjacencyListColumnLevel: ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > + selectedColumns: Set + columnLevelLineage: Map< + TColumnID, + ColumnLevelLineageAdjacencyList + > + setColumnLevelLineage: React.Dispatch< + React.SetStateAction< + Map< + TColumnID, + ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > + > + > + > + showColumns: boolean + setShowColumns: React.Dispatch> + fetchingColumns: Set + setFetchingColumns: React.Dispatch>> +} + +export function getColumnLevelLineageContextInitial< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, + TColumnID extends string = PortId, +>() { + return { + adjacencyListColumnLevel: {}, + columnLevelLineage: new Map< + TColumnID, + ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > + >(), + setColumnLevelLineage: () => {}, + showColumns: false, + setShowColumns: () => {}, + selectedColumns: new Set(), + fetchingColumns: new Set(), + setFetchingColumns: () => {}, + } as const +} + +export type ColumnLevelLineageContextHook< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, + TColumnID extends string = PortId, +> = () => ColumnLevelLineageContextValue< + TAdjacencyListKey, + TAdjacencyListColumnKey, + TColumnID +> diff --git a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx new file mode 100644 index 0000000000..7b5e9e0ae0 --- /dev/null +++ b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx @@ -0,0 +1,257 @@ +import { + AlertCircle, + CircleOff, + FileCode, + FileMinus, + Workflow, +} from 'lucide-react' +import React from 'react' + +import { cn } from '@/utils' +import { NodeBadge } from '../node/NodeBadge' +import { NodePort } from '../node/NodePort' +import { type NodeId, type PortId } from '../utils' +import { + type ColumnLevelLineageAdjacencyList, + type ColumnLevelLineageContextHook, +} from './ColumnLevelLineageContext' +import { Tooltip } from '@/components/Tooltip/Tooltip' +import { Metadata } from '@/components/Metadata/Metadata' +import { HorizontalContainer } from '@/components/HorizontalContainer/HorizontalContainer' +import { Information } from '@/components/Typography/Information' +import { LoadingContainer } from '@/components/LoadingContainer/LoadingContainer' + +export function FactoryColumn< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, + TNodeID extends string = NodeId, + TColumnID extends string = PortId, +>( + useLineage: ColumnLevelLineageContextHook< + TAdjacencyListKey, + TAdjacencyListColumnKey, + TColumnID + >, +) { + return React.memo(function FactoryColumn({ + id, + nodeId, + modelName, + name, + description, + type, + className, + data, + isFetching = false, + error, + renderError, + renderExpression, + renderSource, + onClick, + onCancel, + }: { + id: TColumnID + nodeId: TNodeID + modelName: TAdjacencyListKey + name: TAdjacencyListColumnKey + type: string + description?: string | null + className?: string + data?: ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > + isFetching?: boolean + error?: Error | null + renderError?: (error: Error) => React.ReactNode + renderExpression?: (expression: string) => React.ReactNode + renderSource?: ( + source: string, + expression?: string | null, + ) => React.ReactNode + onClick?: () => void + onCancel?: () => void + }) { + const { selectedColumns, adjacencyListColumnLevel, columnLevelLineage } = + useLineage() + + const column = adjacencyListColumnLevel?.[modelName]?.[name] + const currentColumnLineage = columnLevelLineage.get(id) + const isSelectedColumn = selectedColumns.has(id) + const isTriggeredColumn = + column != null && currentColumnLineage != null && isSelectedColumn + + // Column that has no upstream connections + const isSourceColumn = React.useMemo(() => { + if (data == null) return false + + const models = Object.values(data) + + console.assert( + data[modelName], + `Model: ${modelName} not found in column lineage data`, + ) + console.assert( + data[modelName][name], + `Column: ${name} for model: ${modelName} not found in column lineage data`, + ) + + const columns = Object.values(data[modelName]) + + if (models.length > 1 || columns.length > 1) return false + + const columnModels = data[modelName][name].models + + return Object.keys(columnModels).length === 0 + }, [data, modelName, name]) + + const isDisabledColumn = isSourceColumn && !isSelectedColumn + + function renderColumnStates() { + if (isFetching) return <> + if (error && renderError) + return ( + + } + side="left" + sideOffset={20} + delayDuration={0} + className="bg-lineage-model-column-error-background p-0" + > + {renderError(error)} + + ) + + return ( + <> + {isSourceColumn ? ( + + ) : ( + + )} + {column?.source && renderSource && ( + + } + side="left" + sideOffset={20} + className="p-0 min-w-[30rem] max-w-xl bg-lineage-model-column-source-background" + delayDuration={0} + > + {renderSource(column.source, column.expression)} + + )} + {column?.expression && renderExpression && ( + + } + side="left" + sideOffset={20} + className="p-0 min-w-[30rem] max-w-xl bg-lineage-model-column-expression-background" + delayDuration={0} + > + {renderExpression(column.expression)} + + )} + + ) + } + + function renderColumn() { + return ( + + + {renderColumnStates()} + {description ? ( + + + + ) : ( + + )} + + + } + value={{type}} + className={cn( + 'relative overflow-visible group p-0', + isDisabledColumn && 'cursor-not-allowed', + className, + )} + /> + ) + } + + function handleSelectColumn(e: React.MouseEvent) { + e.stopPropagation() + e.preventDefault() + + if (isFetching) { + onCancel?.() + } else if ((isSelectedColumn || isSourceColumn) && !isTriggeredColumn) { + return + } else { + onClick?.() + } + } + + return isSelectedColumn ? ( + + {renderColumn()} + + ) : ( + renderColumn() + ) + }) +} + +function DisplayColumName({ name }: { name: string }) { + return ( + + {name} + + ) +} diff --git a/web/common/src/components/Lineage/LineageColumnLevel/help.ts b/web/common/src/components/Lineage/LineageColumnLevel/help.ts new file mode 100644 index 0000000000..30115450cd --- /dev/null +++ b/web/common/src/components/Lineage/LineageColumnLevel/help.ts @@ -0,0 +1,233 @@ +import { + toEdgeID, + toNodeID, + toPortID, + type LineageEdge, + type LineageEdgeData, + type EdgeId, + type NodeId, + type PortId, + type TransformEdgeFn, +} from '../utils' +import { + type ColumnLevelConnections, + type ColumnLevelDetails, + type ColumnLevelLineageAdjacencyList, +} from './ColumnLevelLineageContext' + +export const MAX_COLUMNS_TO_DISPLAY = 5 + +export function getAdjacencyListKeysFromColumnLineage< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, +>( + columnLineage: ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + >, +) { + const adjacencyListKeys = new Set() + + const targets = Object.entries(columnLineage) as [ + TAdjacencyListKey, + ColumnLevelConnections, + ][] + + for (const [sourceModelName, targetColumns] of targets) { + adjacencyListKeys.add(sourceModelName) + + const targetConnections = Object.entries(targetColumns) as [ + TAdjacencyListColumnKey, + ColumnLevelDetails, + ][] + + for (const [, { models: sourceModels }] of targetConnections) { + for (const targetModelName of Object.keys( + sourceModels, + ) as TAdjacencyListKey[]) { + adjacencyListKeys.add(targetModelName) + } + } + } + + return Array.from(adjacencyListKeys) +} + +export function getEdgesFromColumnLineage< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TEdgeID extends string = EdgeId, + TNodeID extends string = NodeId, + TPortID extends string = PortId, +>({ + columnLineage, + transformEdge, +}: { + columnLineage: ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > + transformEdge: TransformEdgeFn +}) { + const edges: LineageEdge[] = [] + const modelLevelEdgeIDs = new Map() + const targets = Object.entries(columnLineage || {}) as [ + TAdjacencyListKey, + ColumnLevelConnections, + ][] + + for (const [targetModelName, targetColumns] of targets) { + const targetConnections = Object.entries(targetColumns) as [ + TAdjacencyListColumnKey, + ColumnLevelDetails, + ][] + + const targetNodeId = toNodeID(targetModelName) + + for (const [ + targetColumnName, + { models: sourceModels }, + ] of targetConnections) { + const sources = Object.entries(sourceModels) as [ + TAdjacencyListKey, + TAdjacencyListColumnKey[], + ][] + + for (const [sourceModelName, sourceColumns] of sources) { + const sourceNodeId = toNodeID(sourceModelName) + + modelLevelEdgeIDs.set( + toEdgeID(sourceModelName, targetModelName), + [sourceNodeId, targetNodeId], + ) + + sourceColumns.forEach(sourceColumnName => { + const edgeId = toEdgeID( + sourceModelName, + sourceColumnName, + targetModelName, + targetColumnName, + ) + const sourceColumnId = toPortID( + sourceModelName, + sourceColumnName, + ) + const targetColumnId = toPortID( + targetModelName, + targetColumnName, + ) + + edges.push( + transformEdge( + 'port', + edgeId, + sourceNodeId, + targetNodeId, + sourceColumnId, + targetColumnId, + ), + ) + }) + } + } + } + + Array.from(modelLevelEdgeIDs.entries()).forEach( + ([edgeId, [sourceNodeId, targetNodeId]]) => { + edges.push(transformEdge('edge', edgeId, sourceNodeId, targetNodeId)) + }, + ) + return edges +} + +export function getConnectedColumnsIDs< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, + TColumnID extends string = PortId, +>( + adjacencyList: ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + >, +) { + const connectedColumns = new Set() + const targets = Object.entries(adjacencyList) as [ + TAdjacencyListKey, + ColumnLevelConnections, + ][] + + for (const [sourceModelName, targetColumns] of targets) { + const targetConnections = Object.entries(targetColumns) as [ + TAdjacencyListColumnKey, + ColumnLevelDetails, + ][] + + for (const [ + sourceColumnName, + { models: sourceModels }, + ] of targetConnections) { + connectedColumns.add(toPortID(sourceModelName, sourceColumnName)) + + const sources = Object.entries(sourceModels) as [ + TAdjacencyListKey, + TAdjacencyListColumnKey[], + ][] + + for (const [targetModelName, sourceColumns] of sources) { + sourceColumns.forEach(sourceColumnName => { + connectedColumns.add(toPortID(targetModelName, sourceColumnName)) + }) + } + } + } + return connectedColumns +} + +export function calculateNodeColumnsCount(columnsCount: number = 0) { + return Math.min(columnsCount, MAX_COLUMNS_TO_DISPLAY) +} + +export function calculateSelectedColumnsHeight( + selectedColumnsCount: number = 0, +) { + const selectedColumnsTopSeparatorHeight = 1 + const selectedColumnSeparatorHeight = 1 + const selectedColumnHeight = 24 // tailwind h-6 + const selectedColumnsSeparators = + selectedColumnsCount > 1 ? selectedColumnsCount - 1 : 0 + + return [ + selectedColumnsCount > 0 ? selectedColumnsTopSeparatorHeight : 0, + selectedColumnsCount * selectedColumnHeight, + selectedColumnsCount > 0 + ? selectedColumnsSeparators * selectedColumnSeparatorHeight + : 0, + ].reduce((acc, h) => acc + h, 0) +} + +export function calculateColumnsHeight({ + columnsCount = 0, + hasColumnsFilter = true, +}: { + columnsCount: number + hasColumnsFilter?: boolean +}) { + const hasColumns = columnsCount > 0 + const columnHeight = 24 // tailwind h-6 + const columnsTopSeparator = 1 + const columnSeparator = 1 + const columnsContainerPadding = 4 + const columnsPadding = 4 + const columnsFilterHeight = hasColumnsFilter && hasColumns ? columnHeight : 0 + const columnsSeparators = columnsCount > 1 ? columnsCount - 1 : 0 + + return [ + hasColumns ? columnsSeparators * columnSeparator : 0, + columnsCount * columnHeight, + hasColumns ? columnsPadding * 2 : 0, + hasColumns ? columnsContainerPadding * 2 : 0, + hasColumns ? columnsFilterHeight : 0, + hasColumns ? columnsTopSeparator : 0, + ].reduce((acc, height) => acc + height, 0) +} diff --git a/web/common/src/components/Lineage/LineageColumnLevel/useColumnLevelLineage.ts b/web/common/src/components/Lineage/LineageColumnLevel/useColumnLevelLineage.ts new file mode 100644 index 0000000000..da1a6b8ee8 --- /dev/null +++ b/web/common/src/components/Lineage/LineageColumnLevel/useColumnLevelLineage.ts @@ -0,0 +1,49 @@ +import merge from 'deepmerge' +import React from 'react' + +import { type PortId } from '../utils' +import { type ColumnLevelLineageAdjacencyList } from './ColumnLevelLineageContext' +import { + getAdjacencyListKeysFromColumnLineage, + getConnectedColumnsIDs, +} from './help' + +export function useColumnLevelLineage< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, + TColumnID extends string = PortId, +>( + columnLevelLineage: Map< + TColumnID, + ColumnLevelLineageAdjacencyList + >, +) { + const adjacencyListColumnLevel = React.useMemo(() => { + return merge.all(Array.from(columnLevelLineage.values()), { + arrayMerge: (dest, source) => Array.from(new Set([...dest, ...source])), + }) as ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > + }, [columnLevelLineage]) + + const selectedColumns = React.useMemo(() => { + return getConnectedColumnsIDs< + TAdjacencyListKey, + TAdjacencyListColumnKey, + TColumnID + >(adjacencyListColumnLevel) + }, [adjacencyListColumnLevel]) + + const adjacencyListKeysColumnLevel = React.useMemo(() => { + return adjacencyListColumnLevel != null + ? getAdjacencyListKeysFromColumnLineage(adjacencyListColumnLevel) + : [] + }, [adjacencyListColumnLevel]) + + return { + adjacencyListColumnLevel, + selectedColumns, + adjacencyListKeysColumnLevel, + } +} diff --git a/web/common/src/components/Lineage/LineageColumnLevel/useColumns.tsx b/web/common/src/components/Lineage/LineageColumnLevel/useColumns.tsx new file mode 100644 index 0000000000..3ed1278a5c --- /dev/null +++ b/web/common/src/components/Lineage/LineageColumnLevel/useColumns.tsx @@ -0,0 +1,58 @@ +import React from 'react' + +import { toPortID } from '../utils' +import { type PortId } from '../utils' + +export interface Column { + data_type: string + description?: string | null +} + +export function useColumns< + TAdjacencyListKey extends string, + TAdjacencyListColumnKey extends string, + TColumn extends Column, + TColumnID extends string = PortId, +>( + selectedPorts: Set, + adjacencyListKey: TAdjacencyListKey, + rawColumns?: Record, +) { + const columnNames = React.useMemo(() => { + return new Set( + Object.keys(rawColumns ?? {}).map(column => + toPortID(adjacencyListKey, column), + ), + ) + }, [rawColumns, adjacencyListKey]) + + const [selectedColumns, columns] = React.useMemo(() => { + const selected = [] + const output = [] + + for (const [column, info] of Object.entries(rawColumns ?? {}) as [ + TAdjacencyListColumnKey, + TColumn, + ][]) { + const columnId = toPortID(adjacencyListKey, column) + const nodeColumn = { + name: column, + ...info, + id: columnId, + } + + if (selectedPorts.has(columnId)) { + selected.push(nodeColumn) + } else { + output.push(nodeColumn) + } + } + return [selected, output] + }, [rawColumns, adjacencyListKey, selectedPorts]) + + return { + columns, + columnNames, + selectedColumns, + } +} diff --git a/web/common/src/components/Lineage/LineageContext.ts b/web/common/src/components/Lineage/LineageContext.ts new file mode 100644 index 0000000000..6f4ee7e165 --- /dev/null +++ b/web/common/src/components/Lineage/LineageContext.ts @@ -0,0 +1,103 @@ +import React from 'react' + +import { + type EdgeId, + type LineageEdge, + type LineageEdgeData, + type LineageNode, + type LineageNodeData, + type LineageNodesMap, + type NodeId, + type PortId, + ZOOM_THRESHOLD, +} from './utils' + +export interface LineageContextValue< + TNodeData extends LineageNodeData = LineageNodeData, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +> { + // Node selection + showOnlySelectedNodes: boolean + setShowOnlySelectedNodes: React.Dispatch> + selectedNodes: Set + setSelectedNodes: React.Dispatch>> + selectedEdges: Set + setSelectedEdges: React.Dispatch>> + selectedNodeId: TNodeID | null + setSelectedNodeId: React.Dispatch> + + // Layout + isBuildingLayout: boolean + setIsBuildingLayout: React.Dispatch> + zoom: number + setZoom: React.Dispatch> + + // Nodes and Edges + edges: LineageEdge[] + setEdges: React.Dispatch< + React.SetStateAction[]> + > + nodes: LineageNode[] + nodesMap: LineageNodesMap + setNodesMap: React.Dispatch>> + currentNode: LineageNode | null +} + +export function getInitial< + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, +>() { + return { + showOnlySelectedNodes: false, + setShowOnlySelectedNodes: () => {}, + selectedNodes: new Set(), + setSelectedNodes: () => {}, + selectedEdges: new Set(), + setSelectedEdges: () => {}, + selectedNodeId: null, + setSelectedNodeId: () => {}, + zoom: ZOOM_THRESHOLD, + setZoom: () => {}, + edges: [], + setEdges: () => {}, + nodes: [], + nodesMap: {}, + setNodesMap: () => {}, + isBuildingLayout: false, + setIsBuildingLayout: () => {}, + currentNode: null, + } +} + +export type LineageContextHook< + TNodeData extends LineageNodeData = LineageNodeData, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +> = () => LineageContextValue + +export function createLineageContext< + TNodeData extends LineageNodeData = LineageNodeData, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, + TLineageContextValue extends LineageContextValue< + TNodeData, + TEdgeData, + TNodeID, + TEdgeID, + TPortID + > = LineageContextValue, +>(initial: TLineageContextValue) { + const LineageContext = React.createContext(initial) + + return { + Provider: LineageContext.Provider, + useLineage: () => React.useContext(LineageContext), + } +} diff --git a/web/common/src/components/Lineage/LineageControlButton.tsx b/web/common/src/components/Lineage/LineageControlButton.tsx new file mode 100644 index 0000000000..5f1abaa952 --- /dev/null +++ b/web/common/src/components/Lineage/LineageControlButton.tsx @@ -0,0 +1,43 @@ +import { ControlButton } from '@xyflow/react' + +import { cn } from '@/utils' +import { Tooltip } from '../Tooltip/Tooltip' + +export function LineageControlButton({ + text, + onClick, + disabled = false, + className, + children, +}: { + text: string + children: React.ReactNode + onClick?: (e: React.MouseEvent) => void + disabled?: boolean + className?: string +}) { + return ( + + + {children} + +
      + } + > + {text} + + ) +} diff --git a/web/common/src/components/Lineage/LineageControlIcon.tsx b/web/common/src/components/Lineage/LineageControlIcon.tsx new file mode 100644 index 0000000000..2c7f01e48c --- /dev/null +++ b/web/common/src/components/Lineage/LineageControlIcon.tsx @@ -0,0 +1,42 @@ +import React from 'react' + +import { cn } from '@/utils' + +export interface LineageControlIconProps extends React.SVGProps { + Icon: React.ElementType + size?: number + className?: string +} + +export const LineageControlIcon = React.forwardRef< + HTMLSpanElement, + LineageControlIconProps +>( + ( + { + Icon, + size = 16, + className, + ...props + }: { + Icon: React.ElementType + size?: number + className?: string + }, + ref, + ) => { + return ( + + ) + }, +) + +LineageControlIcon.displayName = 'LineageControlIcon' diff --git a/web/common/src/components/Lineage/LineageLayout.tsx b/web/common/src/components/Lineage/LineageLayout.tsx new file mode 100644 index 0000000000..411ace4e65 --- /dev/null +++ b/web/common/src/components/Lineage/LineageLayout.tsx @@ -0,0 +1,401 @@ +import { + Background, + BackgroundVariant, + Controls, + type EdgeTypes, + type NodeTypes, + ReactFlow, + ReactFlowProvider, + type SetCenter, + getConnectedEdges, + getIncomers, + getOutgoers, + useReactFlow, + useViewport, +} from '@xyflow/react' + +import '@xyflow/react/dist/style.css' +import './Lineage.css' + +import { debounce } from 'lodash' +import { CircuitBoard, Crosshair, LocateFixed, RotateCcw } from 'lucide-react' +import React from 'react' + +import { cn } from '@/utils' +import { type LineageContextHook } from './LineageContext' +import { LineageControlButton } from './LineageControlButton' +import { LineageControlIcon } from './LineageControlIcon' +import { + DEFAULT_ZOOM, + type LineageEdge, + type LineageEdgeData, + type LineageNode, + type LineageNodeData, + MAX_ZOOM, + MIN_ZOOM, + NODES_TRESHOLD, + NODES_TRESHOLD_ZOOM, + type NodeId, + type EdgeId, + ZOOM_THRESHOLD, + type PortId, +} from './utils' +import { VerticalContainer } from '../VerticalContainer/VerticalContainer' +import { MessageContainer } from '../MessageContainer/MessageContainer' +import { LoadingContainer } from '../LoadingContainer/LoadingContainer' + +export function LineageLayout< + TNodeData extends LineageNodeData = LineageNodeData, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +>({ + nodeTypes, + edgeTypes, + className, + controls, + useLineage, + onNodeClick, + onNodeDoubleClick, +}: { + useLineage: LineageContextHook< + TNodeData, + TEdgeData, + TNodeID, + TEdgeID, + TPortID + > + nodeTypes?: NodeTypes + edgeTypes?: EdgeTypes + className?: string + controls?: + | React.ReactNode + | (({ setCenter }: { setCenter: SetCenter }) => React.ReactNode) + onNodeClick?: ( + event: React.MouseEvent, + node: LineageNode, + ) => void + onNodeDoubleClick?: ( + event: React.MouseEvent, + node: LineageNode, + ) => void +}) { + return ( + + + + ) +} + +function LineageLayoutBase< + TNodeData extends LineageNodeData = LineageNodeData, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +>({ + nodeTypes, + edgeTypes, + className, + controls, + useLineage, + onNodeClick, + onNodeDoubleClick, +}: { + useLineage: LineageContextHook< + TNodeData, + TEdgeData, + TNodeID, + TEdgeID, + TPortID + > + nodeTypes?: NodeTypes + edgeTypes?: EdgeTypes + className?: string + controls?: + | React.ReactNode + | (({ setCenter }: { setCenter: SetCenter }) => React.ReactNode) + onNodeClick?: ( + event: React.MouseEvent, + node: LineageNode, + ) => void + onNodeDoubleClick?: ( + event: React.MouseEvent, + node: LineageNode, + ) => void +}) { + const { zoom: viewportZoom } = useViewport() + const { setCenter } = useReactFlow() + + const { + isBuildingLayout, + currentNode, + zoom, + nodes, + edges, + nodesMap, + showOnlySelectedNodes, + selectedNodeId, + setZoom, + setSelectedNodeId, + setShowOnlySelectedNodes, + setSelectedNodes, + setSelectedEdges, + } = useLineage() + + const updateZoom = React.useMemo(() => debounce(setZoom, 200), [setZoom]) + + const zoomToCurrentNode = React.useCallback( + (zoom: number = DEFAULT_ZOOM) => { + if (currentNode) { + setCenter(currentNode.position.x, currentNode.position.y, { + zoom, + duration: 0, + }) + } + }, + [currentNode, setCenter], + ) + + const zoomToSelectedNode = React.useCallback( + (zoom: number = DEFAULT_ZOOM) => { + const node = selectedNodeId ? nodesMap[selectedNodeId] : null + if (node) { + setCenter(node.position.x, node.position.y, { + zoom, + duration: 0, + }) + } + }, + [nodesMap, selectedNodeId, setCenter], + ) + + const getAllIncomers = React.useCallback( + ( + node: LineageNode, + visited: Set = new Set(), + ): LineageNode[] => { + if (visited.has(node.id)) return [] + + visited.add(node.id) + + return Array.from( + new Set>([ + node, + ...getIncomers(node, nodes, edges) + .map(n => getAllIncomers(n, visited)) + .flat(), + ]), + ) + }, + [nodes, edges], + ) + + const getAllOutgoers = React.useCallback( + ( + node: LineageNode, + visited: Set = new Set(), + ): LineageNode[] => { + if (visited.has(node.id)) return [] + + visited.add(node.id) + + return Array.from( + new Set>([ + node, + ...getOutgoers(node, nodes, edges) + .map(n => getAllOutgoers(n, visited)) + .flat(), + ]), + ) + }, + [nodes, edges], + ) + + React.useEffect(() => { + if (selectedNodeId == null) { + setShowOnlySelectedNodes(false) + setSelectedNodes(new Set()) + setSelectedEdges(new Set()) + + return + } + + const node = selectedNodeId ? nodesMap[selectedNodeId] : null + + if (node == null) { + setSelectedNodeId(null) + return + } + + const incomers = getAllIncomers(node) + const outgoers = getAllOutgoers(node) + const connectedNodes = [...incomers, ...outgoers] + + if (currentNode) { + connectedNodes.push(currentNode) + } + + const connectedEdges = getConnectedEdges< + LineageNode, + LineageEdge + >(connectedNodes, edges) + const selectedNodes = new Set(connectedNodes.map(node => node.id)) + const selectedEdges = new Set( + connectedEdges.reduce((acc, edge) => { + if ([edge.source, edge.target].every(id => selectedNodes.has(id))) { + edge.zIndex = 2 + acc.add(edge.id) + } else { + edge.zIndex = 1 + } + return acc + }, new Set()), + ) + + setSelectedNodes(selectedNodes) + setSelectedEdges(selectedEdges) + }, [ + currentNode, + selectedNodeId, + setSelectedNodes, + setSelectedEdges, + getAllIncomers, + getAllOutgoers, + setShowOnlySelectedNodes, + setSelectedNodeId, + ]) + + React.useEffect(() => { + if (selectedNodeId) { + zoomToSelectedNode(zoom) + } else { + zoomToCurrentNode(zoom) + } + }, [zoomToCurrentNode, zoomToSelectedNode]) + + React.useEffect(() => { + updateZoom(viewportZoom) + }, [updateZoom, viewportZoom]) + + React.useEffect(() => { + if (currentNode?.id) { + setSelectedNodeId(currentNode.id) + } else if (selectedNodeId) { + // setSelectedNodeId(selectedNodeId); + } else { + const node = nodes.length > 0 ? nodes[nodes.length - 1] : null + + if (node) { + setCenter(node.position.x, node.position.y, { + zoom: zoom, + duration: 0, + }) + } + } + }, [currentNode?.id, setSelectedNodeId, nodes, setCenter]) + + return ( + + {isBuildingLayout && ( + + + Building layout... + + + )} + , + LineageEdge + > + className="shrink-0" + nodes={nodes} + edges={edges} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + nodesDraggable={false} + nodesConnectable={false} + zoomOnDoubleClick={false} + panOnScroll={true} + zoomOnScroll={true} + minZoom={nodes.length > NODES_TRESHOLD ? NODES_TRESHOLD_ZOOM : MIN_ZOOM} + maxZoom={MAX_ZOOM} + fitView={false} + nodeOrigin={[0.5, 0.5]} + onlyRenderVisibleElements + onNodeClick={onNodeClick} + onNodeDoubleClick={onNodeDoubleClick} + > + {zoom > ZOOM_THRESHOLD && ( + + )} + + {currentNode && ( + zoomToCurrentNode(DEFAULT_ZOOM)} + disabled={isBuildingLayout} + > + + + )} + {selectedNodeId && ( + <> + setShowOnlySelectedNodes(!showOnlySelectedNodes)} + disabled={isBuildingLayout} + > + + + zoomToSelectedNode(DEFAULT_ZOOM)} + disabled={isBuildingLayout} + > + + + + )} + {controls && typeof controls === 'function' + ? controls({ setCenter }) + : controls} + + + + ) +} diff --git a/web/common/src/components/Lineage/edge/EdgeWithGradient.tsx b/web/common/src/components/Lineage/edge/EdgeWithGradient.tsx new file mode 100644 index 0000000000..2a1da5eed1 --- /dev/null +++ b/web/common/src/components/Lineage/edge/EdgeWithGradient.tsx @@ -0,0 +1,114 @@ +import { + type Edge, + type EdgeProps, + getBezierPath, + getSmoothStepPath, + getStraightPath, +} from '@xyflow/react' +import React, { useId } from 'react' + +import { type EdgeId, type LineageEdgeData, type PathType } from '../utils' + +export interface EdgeData extends LineageEdgeData { + startColor?: string + endColor?: string + strokeWidth?: number + pathType?: PathType +} + +export const EdgeWithGradient = React.memo( + ({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + style, + data, + markerEnd, + }: EdgeProps>) => { + const edgeId = id as EdgeId + + const gradientId = useId() + const startColor = data?.startColor || 'var(--color-lineage-edge)' + const endColor = data?.endColor || 'var(--color-lineage-edge)' + const pathType = data?.pathType || 'bezier' + const strokeWidth = data?.strokeWidth || 4 + const edgePath = getEdgePath(pathType) + + function getEdgePath(pathType: PathType) { + return { + straight: getStraightPath({ + sourceX, + sourceY, + targetX, + targetY, + }), + smoothstep: getSmoothStepPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + borderRadius: 10, + }), + bezier: getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }), + step: getSmoothStepPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + borderRadius: 0, + }), + }[pathType] + } + + return ( + <> + + + + + + + + + ) + }, +) diff --git a/web/common/src/components/Lineage/edge/FactoryEdgeWithGradient.tsx b/web/common/src/components/Lineage/edge/FactoryEdgeWithGradient.tsx new file mode 100644 index 0000000000..a89027ffef --- /dev/null +++ b/web/common/src/components/Lineage/edge/FactoryEdgeWithGradient.tsx @@ -0,0 +1,58 @@ +import React from 'react' + +import { type LineageContextHook } from '../LineageContext' +import { + type EdgeId, + type LineageNodeData, + type NodeId, + type PortId, +} from '../utils' +import { EdgeWithGradient, type EdgeData } from './EdgeWithGradient' +import type { Edge, EdgeProps } from '@xyflow/react' + +export function FactoryEdgeWithGradient< + TNodeData extends LineageNodeData = LineageNodeData, + TEdgeData extends EdgeData = EdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +>( + useLineage: LineageContextHook< + TNodeData, + TEdgeData, + TNodeID, + TEdgeID, + TPortID + >, +) { + return React.memo(({ data, id, ...props }: EdgeProps>) => { + const edgeId = id as TEdgeID + + const { selectedEdges } = useLineage() + + const isActive = selectedEdges.has(edgeId) + + let startColor = 'var(--color-lineage-edge)' + let endColor = 'var(--color-lineage-edge)' + + if (isActive && data?.startColor) { + startColor = data?.startColor + } + + if (isActive && data?.endColor) { + endColor = data?.endColor + } + + return ( + + ) + }) +} diff --git a/web/common/src/components/Lineage/help.test.ts b/web/common/src/components/Lineage/help.test.ts new file mode 100644 index 0000000000..51dcb12108 --- /dev/null +++ b/web/common/src/components/Lineage/help.test.ts @@ -0,0 +1,768 @@ +import { describe, expect, test } from 'vitest' +import { Position } from '@xyflow/react' + +import { + getOnlySelectedNodes, + getTransformedNodes, + getTransformedModelEdges, + getTransformedModelEdgesSourceTargets, + getTransformedModelEdgesTargetSources, + createNode, + calculateNodeBaseHeight, + calculateNodeDetailsHeight, + createEdge, +} from './help' +import type { + LineageNode, + LineageNodesMap, + LineageNodeData, + LineageDetails, + LineageAdjacencyList, + NodeId, + EdgeId, + PortId, +} from './utils' +import { toNodeID, toEdgeID } from './utils' + +describe('Lineage Help Functions', () => { + describe('getOnlySelectedNodes', () => { + test('should return only selected nodes from the node map', () => { + const nodesMap = { + node1: { + id: 'node1' as NodeId, + position: { x: 0, y: 0 }, + data: {}, + }, + node2: { + id: 'node2' as NodeId, + position: { x: 100, y: 100 }, + data: {}, + }, + node3: { + id: 'node3' as NodeId, + position: { x: 200, y: 200 }, + data: {}, + }, + } + + const selectedNodes = new Set([ + 'node1' as NodeId, + 'node3' as NodeId, + ]) + const result = getOnlySelectedNodes(nodesMap, selectedNodes) + + expect(Object.keys(result)).toHaveLength(2) + expect(result).toHaveProperty('node1') + expect(result).toHaveProperty('node3') + expect(result).not.toHaveProperty('node2') + }) + + test('should return empty object when no nodes are selected', () => { + const nodesMap = { + node1: { + id: 'node1' as NodeId, + position: { x: 0, y: 0 }, + data: {}, + }, + } + + const selectedNodes = new Set() + const result = getOnlySelectedNodes(nodesMap, selectedNodes) + + expect(Object.keys(result)).toHaveLength(0) + }) + + test('should handle empty node map', () => { + const nodesMap: LineageNodesMap = {} + const selectedNodes = new Set(['node1' as NodeId]) + const result = getOnlySelectedNodes(nodesMap, selectedNodes) + + expect(Object.keys(result)).toHaveLength(0) + }) + }) + + describe('getTransformedNodes', () => { + test('should transform nodes using the provided transform function', () => { + const adjacencyListKeys = ['model1', 'model2'] + const lineageDetails: LineageDetails< + string, + { name: string; type: string } + > = { + model1: { name: 'Model 1', type: 'table' }, + model2: { name: 'Model 2', type: 'view' }, + } + + const transformNode = ( + nodeId: NodeId, + data: { name: string; type: string }, + ) => + ({ + id: nodeId, + position: { x: 0, y: 0 }, + data: { label: data.name, nodeType: data.type }, + }) as LineageNode<{ label: string; nodeType: string }> + + const result = getTransformedNodes( + adjacencyListKeys, + lineageDetails, + transformNode, + ) + + const encodedModel1 = toNodeID('model1') + const encodedModel2 = toNodeID('model2') + + expect(Object.keys(result)).toHaveLength(2) + expect(result[encodedModel1]).toEqual({ + id: encodedModel1, + position: { x: 0, y: 0 }, + data: { label: 'Model 1', nodeType: 'table' }, + }) + expect(result[encodedModel2]).toEqual({ + id: encodedModel2, + position: { x: 0, y: 0 }, + data: { label: 'Model 2', nodeType: 'view' }, + }) + }) + + test('should handle empty adjacency list', () => { + const adjacencyListKeys: string[] = [] + const lineageDetails: LineageDetails = {} + const transformNode = (nodeId: NodeId, data: { name: string }) => + ({ + id: nodeId, + position: { x: 0, y: 0 }, + data: { label: data.name }, + }) as LineageNode<{ label: string }> + + const result = getTransformedNodes( + adjacencyListKeys, + lineageDetails, + transformNode, + ) + + expect(Object.keys(result)).toHaveLength(0) + }) + }) + + describe('getTransformedModelEdges', () => { + test('should transform edges using the provided transform function', () => { + const adjacencyListKeys = ['model1', 'model2', 'model3'] + const lineageAdjacencyList: LineageAdjacencyList = { + model1: ['model2', 'model3'], + model2: ['model3'], + model3: [], + } + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdges( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(3) + + const model1Id = toNodeID('model1') + const model2Id = toNodeID('model2') + const model3Id = toNodeID('model3') + + expect(result[0]).toEqual({ + id: toEdgeID('model1', 'model2'), + source: model1Id, + target: model2Id, + type: 'edge', + zIndex: 1, + }) + expect(result[1]).toEqual({ + id: toEdgeID('model1', 'model3'), + source: model1Id, + target: model3Id, + type: 'edge', + zIndex: 1, + }) + expect(result[2]).toEqual({ + id: toEdgeID('model2', 'model3'), + source: model2Id, + target: model3Id, + type: 'edge', + zIndex: 1, + }) + }) + + test('should skip edges where target is not in adjacency list', () => { + const adjacencyListKeys = ['model1'] + const lineageAdjacencyList: LineageAdjacencyList = { + model1: ['model2'], // model2 is not in the adjacency list + } + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdges( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(0) + }) + + test('should handle empty adjacency list', () => { + const adjacencyListKeys: string[] = [] + const lineageAdjacencyList: LineageAdjacencyList = {} + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdges( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(0) + }) + + test('should handle nodes with no targets', () => { + const adjacencyListKeys = ['model1', 'model2'] + const lineageAdjacencyList = { + model1: [], + model2: null, + } as unknown as LineageAdjacencyList + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdges( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(0) + }) + }) + + describe('getTransformedModelEdgesSourceTargets', () => { + test('should transform edges from source to targets using the provided transform function', () => { + const adjacencyListKeys = ['model1', 'model2', 'model3'] + const lineageAdjacencyList: LineageAdjacencyList = { + model1: ['model2', 'model3'], + model2: ['model3'], + model3: [], + } + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdgesSourceTargets( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(3) + + const model1Id = toNodeID('model1') + const model2Id = toNodeID('model2') + const model3Id = toNodeID('model3') + + expect(result[0]).toEqual({ + id: toEdgeID('model1', 'model2'), + source: model1Id, + target: model2Id, + type: 'edge', + zIndex: 1, + }) + expect(result[1]).toEqual({ + id: toEdgeID('model1', 'model3'), + source: model1Id, + target: model3Id, + type: 'edge', + zIndex: 1, + }) + expect(result[2]).toEqual({ + id: toEdgeID('model2', 'model3'), + source: model2Id, + target: model3Id, + type: 'edge', + zIndex: 1, + }) + }) + + test('should skip edges where target is not in adjacency list', () => { + const adjacencyListKeys = ['model1'] + const lineageAdjacencyList: LineageAdjacencyList = { + model1: ['model2'], // model2 is not in the adjacency list + } + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdgesSourceTargets( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(0) + }) + + test('should handle empty adjacency list', () => { + const adjacencyListKeys: string[] = [] + const lineageAdjacencyList: LineageAdjacencyList = {} + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdgesSourceTargets( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(0) + }) + + test('should handle nodes with no targets', () => { + const adjacencyListKeys = ['model1', 'model2'] + const lineageAdjacencyList = { + model1: [], + model2: null, + } as unknown as LineageAdjacencyList + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdgesSourceTargets( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(0) + }) + }) + + describe('getTransformedModelEdgesTargetSources', () => { + test('should transform edges from target to sources using the provided transform function', () => { + const adjacencyListKeys = ['model1', 'model2', 'model3'] + const lineageAdjacencyList: LineageAdjacencyList = { + model1: [], + model2: ['model1'], + model3: ['model1', 'model2'], + } + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdgesTargetSources( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(3) + + const model1Id = toNodeID('model1') + const model2Id = toNodeID('model2') + const model3Id = toNodeID('model3') + + expect(result[0]).toEqual({ + id: toEdgeID('model1', 'model2'), + source: model1Id, + target: model2Id, + type: 'edge', + zIndex: 1, + }) + expect(result[1]).toEqual({ + id: toEdgeID('model1', 'model3'), + source: model1Id, + target: model3Id, + type: 'edge', + zIndex: 1, + }) + expect(result[2]).toEqual({ + id: toEdgeID('model2', 'model3'), + source: model2Id, + target: model3Id, + type: 'edge', + zIndex: 1, + }) + }) + + test('should skip edges where source is not in adjacency list', () => { + const adjacencyListKeys = ['model2'] + const lineageAdjacencyList: LineageAdjacencyList = { + model2: ['model1'], // model1 is not in the adjacency list + } + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdgesTargetSources( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(0) + }) + + test('should handle empty adjacency list', () => { + const adjacencyListKeys: string[] = [] + const lineageAdjacencyList: LineageAdjacencyList = {} + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdgesTargetSources( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(0) + }) + + test('should handle nodes with no sources', () => { + const adjacencyListKeys = ['model1', 'model2'] + const lineageAdjacencyList = { + model1: [], + model2: null, + } as unknown as LineageAdjacencyList + + const transformEdge = ( + type: string, + edgeId: EdgeId, + sourceId: NodeId, + targetId: NodeId, + ) => ({ + id: edgeId, + source: sourceId, + target: targetId, + type, + zIndex: 1, + }) + + const result = getTransformedModelEdgesTargetSources( + adjacencyListKeys, + lineageAdjacencyList, + transformEdge, + ) + + expect(result).toHaveLength(0) + }) + }) + + describe('createNode', () => { + test('should create a node with provided data', () => { + const nodeId = 'test-node' as NodeId + const data = { label: 'Test Node', value: 42 } + const node = createNode('custom', nodeId, data) + + expect(node).toEqual({ + id: nodeId, + sourcePosition: Position.Right, + targetPosition: Position.Left, + width: 300, // DEFAULT_NODE_WIDTH + height: 32, // DEFAULT_NODE_HEIGHT + data, + type: 'custom', + hidden: false, + position: { x: 0, y: 0 }, + zIndex: 10, + }) + }) + + test('should create a node with minimal data', () => { + const nodeId = 'minimal' as NodeId + const data = {} + const node = createNode('default', nodeId, data) + + expect(node.id).toBe(nodeId) + expect(node.type).toBe('default') + expect(node.data).toEqual({}) + expect(node.hidden).toBe(false) + }) + }) + + describe('calculateNodeBaseHeight', () => { + test('should calculate base height with no additional components', () => { + const height = calculateNodeBaseHeight({}) + // border (2*2) + base (28) = 32 + expect(height).toBe(32) + }) + + test('should include footer height when specified', () => { + const height = calculateNodeBaseHeight({ includeNodeFooterHeight: true }) + // border (2*2) + base (28) + footer (20) = 52 + expect(height).toBe(52) + }) + + test('should include ceiling height when specified', () => { + const height = calculateNodeBaseHeight({ includeCeilingHeight: true }) + // border (2*2) + base (28) + ceiling (20) + ceilingGap (4) = 56 + expect(height).toBe(56) + }) + + test('should include floor height when specified', () => { + const height = calculateNodeBaseHeight({ includeFloorHeight: true }) + // border (2*2) + base (28) + floor (20) + floorGap (4) = 56 + expect(height).toBe(56) + }) + + test('should include all components when specified', () => { + const height = calculateNodeBaseHeight({ + includeNodeFooterHeight: true, + includeCeilingHeight: true, + includeFloorHeight: true, + }) + // border (2*2) + base (28) + footer (20) + ceiling (20) + ceilingGap (4) + floor (20) + floorGap (4) = 100 + expect(height).toBe(100) + }) + }) + + describe('calculateNodeDetailsHeight', () => { + test('should return 0 when no details', () => { + const height = calculateNodeDetailsHeight({}) + expect(height).toBe(0) + }) + + test('should calculate height for single detail', () => { + const height = calculateNodeDetailsHeight({ nodeDetailsCount: 1 }) + // 1 * 24 (nodeOptionHeight) = 24 + expect(height).toBe(24) + }) + + test('should calculate height for multiple details with separators', () => { + const height = calculateNodeDetailsHeight({ nodeDetailsCount: 3 }) + // 3 * 24 (nodeOptionHeight) + 2 * 1 (separators between items) = 74 + expect(height).toBe(74) + }) + + test('should handle zero details count', () => { + const height = calculateNodeDetailsHeight({ nodeDetailsCount: 0 }) + expect(height).toBe(0) + }) + }) + + describe('createEdge', () => { + test('should create edge with basic parameters', () => { + const edgeId = 'edge1' as EdgeId + const sourceId = 'source1' as NodeId + const targetId = 'target1' as NodeId + + const edge = createEdge('straight', edgeId, sourceId, targetId) + + expect(edge).toEqual({ + id: edgeId, + source: sourceId, + target: targetId, + type: 'straight', + sourceHandle: undefined, + targetHandle: undefined, + data: undefined, + zIndex: 1, + }) + }) + + test('should create edge with handles', () => { + const edgeId = 'edge2' as EdgeId + const sourceId = 'source2' as NodeId + const targetId = 'target2' as NodeId + const sourceHandleId = 'handle1' as PortId + const targetHandleId = 'handle2' as PortId + + const edge = createEdge( + 'bezier', + edgeId, + sourceId, + targetId, + sourceHandleId, + targetHandleId, + ) + + expect(edge).toEqual({ + id: edgeId, + source: sourceId, + target: targetId, + type: 'bezier', + sourceHandle: sourceHandleId, + targetHandle: targetHandleId, + data: undefined, + zIndex: 1, + }) + }) + + test('should create edge with data', () => { + const edgeId = 'edge3' as EdgeId + const sourceId = 'source3' as NodeId + const targetId = 'target3' as NodeId + const data = { label: 'Connection', weight: 5 } + + const edge = createEdge( + 'smoothstep', + edgeId, + sourceId, + targetId, + undefined, + undefined, + data, + ) + + expect(edge).toEqual({ + id: edgeId, + source: sourceId, + target: targetId, + type: 'smoothstep', + sourceHandle: undefined, + targetHandle: undefined, + data, + zIndex: 1, + }) + }) + + test('should create edge with all parameters', () => { + const edgeId = 'edge4' as EdgeId + const sourceId = 'source4' as NodeId + const targetId = 'target4' as NodeId + const sourceHandleId = 'handle3' as PortId + const targetHandleId = 'handle4' as PortId + const data = { animated: true } + + const edge = createEdge( + 'step', + edgeId, + sourceId, + targetId, + sourceHandleId, + targetHandleId, + data, + ) + + expect(edge).toEqual({ + id: edgeId, + source: sourceId, + target: targetId, + type: 'step', + sourceHandle: sourceHandleId, + targetHandle: targetHandleId, + data, + zIndex: 1, + }) + }) + }) +}) diff --git a/web/common/src/components/Lineage/help.ts b/web/common/src/components/Lineage/help.ts new file mode 100644 index 0000000000..a052ff707b --- /dev/null +++ b/web/common/src/components/Lineage/help.ts @@ -0,0 +1,270 @@ +import { Position } from '@xyflow/react' + +import { + DEFAULT_NODE_HEIGHT, + DEFAULT_NODE_WIDTH, + type EdgeId, + type LineageAdjacencyList, + type LineageDetails, + type LineageEdge, + type LineageEdgeData, + type LineageNode, + type LineageNodeData, + type LineageNodesMap, + type NodeId, + type PortId, + toEdgeID, + toNodeID, + type TransformEdgeFn, + type TransformNodeFn, +} from './utils' + +export function getOnlySelectedNodes< + TNodeData extends LineageNodeData = LineageNodeData, + TNodeID extends string = NodeId, +>(nodeMaps: LineageNodesMap, selectedNodes: Set) { + return (Object.values(nodeMaps) as LineageNode[]).reduce( + (acc, node) => + selectedNodes.has(node.id) ? { ...acc, [node.id]: node } : acc, + {} as LineageNodesMap, + ) +} + +export function getTransformedNodes< + TAdjacencyListKey extends string, + TDetailsNode, + TNodeData extends LineageNodeData = LineageNodeData, + TNodeID extends string = NodeId, +>( + adjacencyListKeys: TAdjacencyListKey[], + lineageDetails: LineageDetails, + transformNode: TransformNodeFn, +): LineageNodesMap { + const nodesCount = adjacencyListKeys.length + const nodesMap: LineageNodesMap = Object.create(null) + + for (let i = 0; i < nodesCount; i++) { + const adjacencyListKey = adjacencyListKeys[i] + const encodedNodeId = toNodeID(adjacencyListKey) + nodesMap[encodedNodeId] = transformNode( + encodedNodeId, + lineageDetails[adjacencyListKey], + ) + } + + return nodesMap +} + +export function getTransformedModelEdges< + TAdjacencyListKey extends string, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +>( + adjacencyListKeys: TAdjacencyListKey[], + lineageAdjacencyList: LineageAdjacencyList, + transformEdge: TransformEdgeFn, +) { + const nodesCount = adjacencyListKeys.length + + if (nodesCount === 0) return [] + + const edges = [] + + for (let i = 0; i < nodesCount; i++) { + const adjacencyListKey = adjacencyListKeys[i] + const nodeId = toNodeID(adjacencyListKey) + const targets = lineageAdjacencyList[adjacencyListKey] + const targetsCount = targets?.length || 0 + + if (targets == null || targetsCount < 1) continue + + for (let j = 0; j < targetsCount; j++) { + const target = targets[j] + + if (!(target in lineageAdjacencyList)) continue + + const edgeId = toEdgeID(adjacencyListKey, target) + + edges.push( + transformEdge('edge', edgeId, nodeId, toNodeID(target)), + ) + } + } + + return edges +} + +export function getTransformedModelEdgesSourceTargets< + TAdjacencyListKey extends string, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +>( + adjacencyListKeys: TAdjacencyListKey[], + lineageAdjacencyList: LineageAdjacencyList, + transformEdge: TransformEdgeFn, +) { + const nodesCount = adjacencyListKeys.length + + if (nodesCount === 0) return [] + + const edges = [] + + for (let i = 0; i < nodesCount; i++) { + const sourceAdjacencyListKey = adjacencyListKeys[i] + const sourceNodeId = toNodeID(sourceAdjacencyListKey) + const targets = lineageAdjacencyList[sourceAdjacencyListKey] + const targetsCount = targets?.length || 0 + + if (targets == null || targetsCount < 1) continue + + for (let j = 0; j < targetsCount; j++) { + const targetAdjacencyListKey = targets[j] + + if (!(targetAdjacencyListKey in lineageAdjacencyList)) continue + + const edgeId = toEdgeID( + sourceAdjacencyListKey, + targetAdjacencyListKey, + ) + const targetNodeId = toNodeID(targetAdjacencyListKey) + + edges.push(transformEdge('edge', edgeId, sourceNodeId, targetNodeId)) + } + } + + return edges +} + +export function getTransformedModelEdgesTargetSources< + TAdjacencyListKey extends string, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +>( + adjacencyListKeys: TAdjacencyListKey[], + lineageAdjacencyList: LineageAdjacencyList, + transformEdge: TransformEdgeFn, +) { + const nodesCount = adjacencyListKeys.length + + if (nodesCount === 0) return [] + + const edges = [] + + for (let i = 0; i < nodesCount; i++) { + const targetAdjacencyListKey = adjacencyListKeys[i] + const targetNodeId = toNodeID(targetAdjacencyListKey) + const sources = lineageAdjacencyList[targetAdjacencyListKey] + const sourcesCount = sources?.length || 0 + + if (sources == null || sourcesCount < 1) continue + + for (let j = 0; j < sourcesCount; j++) { + const sourceAdjacencyListKey = sources[j] + + if (!(sourceAdjacencyListKey in lineageAdjacencyList)) continue + + const edgeId = toEdgeID( + sourceAdjacencyListKey, + targetAdjacencyListKey, + ) + const sourceNodeId = toNodeID(sourceAdjacencyListKey) + + edges.push(transformEdge('edge', edgeId, sourceNodeId, targetNodeId)) + } + } + + return edges +} + +export function createNode< + TNodeData extends LineageNodeData = LineageNodeData, + TNodeID extends string = NodeId, +>(type: string, nodeId: TNodeID, data: TNodeData) { + return { + id: nodeId, + sourcePosition: Position.Right, + targetPosition: Position.Left, + width: DEFAULT_NODE_WIDTH, + height: DEFAULT_NODE_HEIGHT, + data, + type, + hidden: false, + position: { x: 0, y: 0 }, + zIndex: 10, + } +} + +export function calculateNodeBaseHeight({ + includeNodeFooterHeight = false, + includeCeilingHeight = false, + includeFloorHeight = false, +}: { + includeNodeFooterHeight?: boolean + includeCeilingHeight?: boolean + includeFloorHeight?: boolean +}) { + const border = 2 + const footerHeight = 20 // tailwind h-5 + const base = 28 // tailwind h-7 + const ceilingHeight = 20 // tailwind h-5 + const floorHeight = 20 // tailwind h-5 + + const ceilingGap = 4 + const floorGap = 4 + + return [ + border * 2, + base, + includeNodeFooterHeight ? footerHeight : 0, + includeCeilingHeight ? ceilingHeight + ceilingGap : 0, + includeFloorHeight ? floorHeight + floorGap : 0, + ].reduce((acc, h) => acc + h, 0) +} + +export function calculateNodeDetailsHeight({ + nodeDetailsCount = 0, +}: { + nodeDetailsCount?: number +}) { + const nodeOptionHeight = 24 // tailwind h-6 + + const nodeOptionsSeparator = 1 + const nodeOptionsSeparators = nodeDetailsCount > 1 ? nodeDetailsCount - 1 : 0 + + return [ + nodeOptionsSeparators * nodeOptionsSeparator, + nodeDetailsCount * nodeOptionHeight, + ].reduce((acc, h) => acc + h, 0) +} + +export function createEdge< + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +>( + type: string, + edgeId: TEdgeID, + sourceId: TNodeID, + targetId: TNodeID, + sourceHandleId?: TPortID, + targetHandleId?: TPortID, + data?: TEdgeData, +): LineageEdge { + return { + id: edgeId, + source: sourceId, + target: targetId, + type, + sourceHandle: sourceHandleId ? sourceHandleId : undefined, + targetHandle: targetHandleId ? targetHandleId : undefined, + data, + zIndex: 1, + } +} diff --git a/web/common/src/components/Lineage/index.ts b/web/common/src/components/Lineage/index.ts new file mode 100644 index 0000000000..0fbc17047c --- /dev/null +++ b/web/common/src/components/Lineage/index.ts @@ -0,0 +1,28 @@ +export * from './utils' +export * from './LineageLayout' +export * from './LineageContext' +export * from './LineageControlButton' +export * from './LineageControlIcon' +export * from './help' +export * from './node/base-handle' +export * from './node/base-node' +export * from './node/NodeContainer' +export * from './node/NodeBase' +export * from './node/NodeDivider' +export * from './node/NodeHandleIcon' +export * from './node/NodeHandles' +export * from './node/NodeHandle' +export * from './node/NodeHeader' +export * from './node/NodePorts' +export * from './node/NodePort' +export * from './node/NodeAppendix' +export * from './node/NodeBadge' +export * from './node/useNodeMetadata' +export * from './edge/EdgeWithGradient' +export * from './edge/FactoryEdgeWithGradient' +export * from './layout/dagreLayout' +export * from './LineageColumnLevel/ColumnLevelLineageContext' +export * from './LineageColumnLevel/FactoryColumn' +export * from './LineageColumnLevel/useColumns' +export * from './LineageColumnLevel/useColumnLevelLineage' +export * from './LineageColumnLevel/help' diff --git a/web/common/src/components/Lineage/layout/dagreLayout.ts b/web/common/src/components/Lineage/layout/dagreLayout.ts new file mode 100644 index 0000000000..83714a2220 --- /dev/null +++ b/web/common/src/components/Lineage/layout/dagreLayout.ts @@ -0,0 +1,90 @@ +import { + DEFAULT_NODE_WIDTH, + type EdgeId, + type LineageEdge, + type LineageEdgeData, + type LineageNodeData, + type LineageNodesMap, + type NodeId, + type PortId, +} from '../utils' +import dagre from 'dagre' + +export function buildLayout< + TNodeData extends LineageNodeData = LineageNodeData, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +>({ + edges, + nodesMap, +}: { + edges: LineageEdge[] + nodesMap: LineageNodesMap +}) { + const nodes = Object.values(nodesMap) + const nodeCount = nodes.length + const edgeCount = edges.length + + if (nodeCount === 0) + return { + edges: [], + nodesMap: {}, + } + + const g = new dagre.graphlib.Graph({ + compound: true, + multigraph: true, + directed: true, + }) + + g.setGraph({ + rankdir: 'LR', + nodesep: 0, + ranksep: 48, + edgesep: 0, + ranker: 'longest-path', + }) + + g.setDefaultEdgeLabel(() => ({})) + + // Building layout already heavy operation, so trying to optimize it a bit + for (let i = 0; i < edgeCount; i++) { + g.setEdge(edges[i].source, edges[i].target) + } + + for (let i = 0; i < nodeCount; i++) { + const node = nodes[i] + g.setNode(node.id, { + width: node.width || DEFAULT_NODE_WIDTH, + height: node.height || 0, + }) + } + + dagre.layout(g) + + // Building layout already heavy operation, so trying to optimize it a bit + for (let i = 0; i < nodeCount; i++) { + const node = nodes[i] + const width = node.width || DEFAULT_NODE_WIDTH + const height = node.height || 0 + const nodeId = node.id as NodeId + const nodeWithPosition = g.node(nodeId) + const halfWidth = width / 2 + const halfHeight = height / 2 + + nodesMap[nodeId] = { + ...node, + position: { + x: nodeWithPosition.x - halfWidth, + y: nodeWithPosition.y - halfHeight, + }, + } + } + + return { + edges, + nodesMap, + } +} diff --git a/web/common/src/components/Lineage/layout/help.ts b/web/common/src/components/Lineage/layout/help.ts new file mode 100644 index 0000000000..91b3ebc4a3 --- /dev/null +++ b/web/common/src/components/Lineage/layout/help.ts @@ -0,0 +1,100 @@ +import { + type LineageEdge, + type LineageEdgeData, + type LineageNodeData, + type LineageNodesMap, + type NodeId, + type PortId, + type LayoutedGraph, + type EdgeId, +} from '../utils' + +const DEFAULT_TIMEOUT = 1000 * 60 // 1 minute + +let workerInstance: Worker | null = null + +export function getWorker(url: URL): Worker { + if (workerInstance) return workerInstance + + workerInstance = new Worker(url, { type: 'module' }) + + return workerInstance +} + +export async function getLayoutedGraph< + TNodeData extends LineageNodeData = LineageNodeData, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +>( + edges: LineageEdge[], + nodesMap: LineageNodesMap, + workerUrl: URL, +): Promise> { + let timeoutId: NodeJS.Timeout | null = null + + return new Promise((resolve, reject) => { + const nodes = Object.values(nodesMap) + + if (nodes.length === 0) return resolve({ edges: [], nodesMap: {} }) + + const worker = getWorker(workerUrl) + + if (worker == null) + return errorHandler(new ErrorEvent('Failed to create worker')) + + timeoutId = setTimeout( + () => errorHandler(new ErrorEvent('Layout calculation timed out')), + DEFAULT_TIMEOUT, + ) + + worker.addEventListener('message', handler) + worker.addEventListener('error', errorHandler) + + try { + worker.postMessage({ edges, nodesMap } as LayoutedGraph< + TNodeData, + TEdgeData, + TNodeID, + TEdgeID, + TPortID + >) + } catch (postError) { + errorHandler(postError as ErrorEvent) + } + + function handler( + event: MessageEvent< + LayoutedGraph & { + error: ErrorEvent + } + >, + ) { + cleanup() + + if (event.data.error) return errorHandler(event.data.error) + + resolve(event.data) + } + + function errorHandler(error: ErrorEvent) { + cleanup() + reject(error) + } + + function cleanup() { + if (timeoutId) { + clearTimeout(timeoutId) + timeoutId = null + } + worker?.removeEventListener('message', handler) + worker?.removeEventListener('error', errorHandler) + } + }) +} + +export function cleanupLayoutWorker(): void { + workerInstance?.terminate() + workerInstance = null +} diff --git a/web/common/src/components/Lineage/node/NodeAppendix.tsx b/web/common/src/components/Lineage/node/NodeAppendix.tsx new file mode 100644 index 0000000000..76d64affed --- /dev/null +++ b/web/common/src/components/Lineage/node/NodeAppendix.tsx @@ -0,0 +1,44 @@ +import { cva, type VariantProps } from 'class-variance-authority' +import { forwardRef, type HTMLAttributes } from 'react' + +import { cn } from '@/utils' + +const appendixVariants = cva( + 'node-appendix absolute flex w-full flex-col items-center', + { + variants: { + position: { + top: '-translate-y-[100%] -my-1', + bottom: 'top-[100%] my-1', + left: '-left-[100%] -mx-1', + right: 'left-[100%] mx-1', + }, + }, + defaultVariants: { + position: 'top', + }, + }, +) + +export interface NodeAppendixProps + extends HTMLAttributes, + VariantProps { + className?: string + position?: 'top' | 'bottom' | 'left' | 'right' +} + +export const NodeAppendix = forwardRef( + ({ children, className, position, ...props }, ref) => { + return ( +
      + {children} +
      + ) + }, +) + +NodeAppendix.displayName = 'NodeAppendix' diff --git a/web/common/src/components/Lineage/node/NodeBadge.tsx b/web/common/src/components/Lineage/node/NodeBadge.tsx new file mode 100644 index 0000000000..943e5e9267 --- /dev/null +++ b/web/common/src/components/Lineage/node/NodeBadge.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import { cn } from '@/utils' +import { Badge, type BadgeProps } from '@/components/Badge/Badge' + +export const NodeBadge = React.forwardRef( + ({ className, children, ...props }, ref) => { + return ( + + {children} + + ) + }, +) +NodeBadge.displayName = 'NodeBadge' diff --git a/web/common/src/components/Lineage/node/NodeBase.tsx b/web/common/src/components/Lineage/node/NodeBase.tsx new file mode 100644 index 0000000000..78033a4099 --- /dev/null +++ b/web/common/src/components/Lineage/node/NodeBase.tsx @@ -0,0 +1,31 @@ +import { type NodeProps } from '@xyflow/react' +import React from 'react' + +import { BaseNode } from '@/components/Lineage/node/base-node' +import { cn } from '@/utils' + +export interface NodeBaseProps extends NodeProps { + className?: string + children?: React.ReactNode +} + +export const NodeBase = React.memo( + React.forwardRef( + ({ className, children }, ref) => { + return ( + + {children} + + ) + }, + ), +) +NodeBase.displayName = 'NodeBase' diff --git a/web/common/src/components/Lineage/node/NodeContainer.tsx b/web/common/src/components/Lineage/node/NodeContainer.tsx new file mode 100644 index 0000000000..0506771eae --- /dev/null +++ b/web/common/src/components/Lineage/node/NodeContainer.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +import { cn } from '@/utils' +import { VerticalContainer } from '@/components/VerticalContainer/VerticalContainer' + +export const NodeContainer = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => { + return ( + + {children} + + ) +}) +NodeContainer.displayName = 'NodeContainer' diff --git a/web/common/src/components/Lineage/node/NodeDetail.tsx b/web/common/src/components/Lineage/node/NodeDetail.tsx new file mode 100644 index 0000000000..96b8cafbb8 --- /dev/null +++ b/web/common/src/components/Lineage/node/NodeDetail.tsx @@ -0,0 +1,26 @@ +import { Metadata, cn } from '@tobikodata/sqlmesh-common' + +import { NodeDivider } from './NodeDivider' + +export function NodeDetail({ + label, + value, + hasDivider = true, + className, +}: { + label: string + value: string + hasDivider?: boolean + className?: string +}) { + return ( + <> + {hasDivider && } + + + ) +} diff --git a/web/common/src/components/Lineage/node/NodeDivider.tsx b/web/common/src/components/Lineage/node/NodeDivider.tsx new file mode 100644 index 0000000000..5f35f0c7e6 --- /dev/null +++ b/web/common/src/components/Lineage/node/NodeDivider.tsx @@ -0,0 +1,3 @@ +export function NodeDivider() { + return
      +} diff --git a/web/common/src/components/Lineage/node/NodeHandle.tsx b/web/common/src/components/Lineage/node/NodeHandle.tsx new file mode 100644 index 0000000000..e737ff4327 --- /dev/null +++ b/web/common/src/components/Lineage/node/NodeHandle.tsx @@ -0,0 +1,31 @@ +import { Position } from '@xyflow/react' +import React from 'react' + +import { cn } from '@/utils' +import { BaseHandle } from './base-handle' + +export const NodeHandle = React.memo(function NodeHandle({ + type, + id, + children, + className, + ...props +}: { + type: 'target' | 'source' + id: string + children: React.ReactNode + className?: string +}) { + return ( + + {children} + + ) +}) diff --git a/web/common/src/components/Lineage/node/NodeHandleIcon.tsx b/web/common/src/components/Lineage/node/NodeHandleIcon.tsx new file mode 100644 index 0000000000..d7335a69b3 --- /dev/null +++ b/web/common/src/components/Lineage/node/NodeHandleIcon.tsx @@ -0,0 +1,22 @@ +import { ArrowRight } from 'lucide-react' + +import { cn } from '@/utils' + +export function NodeHandleIcon({ + className, + iconSize = 20, +}: { + className?: string + iconSize?: number +}) { + return ( + + ) +} diff --git a/web/common/src/components/Lineage/node/NodeHandles.tsx b/web/common/src/components/Lineage/node/NodeHandles.tsx new file mode 100644 index 0000000000..71bee716b4 --- /dev/null +++ b/web/common/src/components/Lineage/node/NodeHandles.tsx @@ -0,0 +1,50 @@ +import React from 'react' + +import { cn } from '@/utils' +import { HorizontalContainer } from '@/components/HorizontalContainer/HorizontalContainer' +import { NodeHandle } from './NodeHandle' + +export const NodeHandles = React.memo(function NodeHandles({ + leftIcon, + rightIcon, + leftId, + rightId, + className, + handleClassName, + children, +}: { + leftId?: string + rightId?: string + className?: string + handleClassName?: string + children: React.ReactNode + leftIcon: React.ReactNode + rightIcon: React.ReactNode +}) { + return ( + + {leftId && ( + + {leftIcon} + + )} + {children} + {rightId && ( + + {rightIcon} + + )} + + ) +}) diff --git a/web/common/src/components/Lineage/node/NodeHeader.tsx b/web/common/src/components/Lineage/node/NodeHeader.tsx new file mode 100644 index 0000000000..334af2c5ed --- /dev/null +++ b/web/common/src/components/Lineage/node/NodeHeader.tsx @@ -0,0 +1,28 @@ +import { type HTMLAttributes, forwardRef } from 'react' + +import { cn } from '@/utils' + +/* NODE HEADER -------------------------------------------------------------- */ + +export type NodeHeaderProps = HTMLAttributes + +/** + * A container for a consistent header layout intended to be used inside the + * `` component. + */ +export const NodeHeader = forwardRef( + ({ className, ...props }, ref) => { + return ( +
      + ) + }, +) + +NodeHeader.displayName = 'NodeHeader' diff --git a/web/common/src/components/Lineage/node/NodePort.tsx b/web/common/src/components/Lineage/node/NodePort.tsx new file mode 100644 index 0000000000..ecf6206382 --- /dev/null +++ b/web/common/src/components/Lineage/node/NodePort.tsx @@ -0,0 +1,64 @@ +import { useNodeConnections, useUpdateNodeInternals } from '@xyflow/react' +import React from 'react' + +import { cn } from '@/utils' +import { type NodeId, type PortId } from '../utils' +import { NodeHandles } from './NodeHandles' + +export const NodePort = React.memo(function NodePort< + TPortId extends string = PortId, + TNodeID extends string = NodeId, +>({ + id, + nodeId, + className, + children, +}: { + id: TPortId + nodeId: TNodeID + className?: string + children: React.ReactNode +}) { + const updateNodeInternals = useUpdateNodeInternals() + + const sources = useNodeConnections({ + id: nodeId, + handleType: 'source', + handleId: id, + }) + const targets = useNodeConnections({ + id: nodeId, + handleType: 'target', + handleId: id, + }) + + const leftId = targets.length > 0 ? id : undefined + const rightId = sources.length > 0 ? id : undefined + + React.useEffect(() => { + if (leftId || rightId) { + updateNodeInternals(nodeId) + } + }, [updateNodeInternals, nodeId, leftId, rightId]) + + return ( + + } + rightIcon={ + + } + leftId={leftId} + rightId={rightId} + className={cn( + 'relative overflow-visible group p-0 bg-lineage-node-port-background h-auto', + className, + )} + handleClassName="absolute" + > + {children} + + ) +}) diff --git a/web/common/src/components/Lineage/node/NodePorts.tsx b/web/common/src/components/Lineage/node/NodePorts.tsx new file mode 100644 index 0000000000..f417dea9e4 --- /dev/null +++ b/web/common/src/components/Lineage/node/NodePorts.tsx @@ -0,0 +1,44 @@ +import { cn } from '@/utils' +import { VirtualList } from '@/components/VirtualList/VirtualList' +import { FilterableList } from '@/components/VirtualList/FilterableList' +import type { IFuseOptions } from 'fuse.js' + +export function NodePorts({ + ports, + estimatedListItemHeight, + renderPort, + className, + isFilterable = true, + filterOptions, +}: { + ports: TPort[] + estimatedListItemHeight: number + renderPort: (port: TPort) => React.ReactNode + className?: string + isFilterable?: boolean + filterOptions?: IFuseOptions +}) { + function renderVirtualList(items: TPort[]) { + return ( + renderPort(item)} + className={cn(!isFilterable && className)} + /> + ) + } + return isFilterable ? ( + + {renderVirtualList} + + ) : ( + renderVirtualList(ports) + ) +} diff --git a/web/common/src/components/Lineage/node/base-handle.tsx b/web/common/src/components/Lineage/node/base-handle.tsx new file mode 100644 index 0000000000..76d66bdeaf --- /dev/null +++ b/web/common/src/components/Lineage/node/base-handle.tsx @@ -0,0 +1,27 @@ +import { Handle, type HandleProps } from '@xyflow/react' +import { forwardRef } from 'react' +import type { ForwardRefExoticComponent, RefAttributes } from 'react' + +import { cn } from '@/utils' + +export const BaseHandle: ForwardRefExoticComponent< + HandleProps & RefAttributes +> = forwardRef( + ({ className, children, ...props }, ref) => { + return ( + + {children} + + ) + }, +) + +BaseHandle.displayName = 'BaseHandle' diff --git a/web/common/src/components/Lineage/node/base-node.tsx b/web/common/src/components/Lineage/node/base-node.tsx new file mode 100644 index 0000000000..d349ca601a --- /dev/null +++ b/web/common/src/components/Lineage/node/base-node.tsx @@ -0,0 +1,17 @@ +import { type HTMLAttributes, forwardRef } from 'react' + +import { cn } from '@/utils' + +export const BaseNode = forwardRef< + HTMLDivElement, + HTMLAttributes & { selected?: boolean } +>(({ className, ...props }, ref) => ( +
      +)) + +BaseNode.displayName = 'BaseNode' diff --git a/web/common/src/components/Lineage/node/useNodeMetadata.tsx b/web/common/src/components/Lineage/node/useNodeMetadata.tsx new file mode 100644 index 0000000000..3601b752fd --- /dev/null +++ b/web/common/src/components/Lineage/node/useNodeMetadata.tsx @@ -0,0 +1,43 @@ +import { + type Node, + type NodeProps as ReactFlowNodeProps, + useNodeConnections, +} from '@xyflow/react' + +import { type LineageNode, type LineageNodeData, type NodeId } from '../utils' + +export type NodeProps = + ReactFlowNodeProps> + +export function useNodeMetadata< + TNodeData extends LineageNodeData = LineageNodeData, + TNodeID extends string = NodeId, +>( + nodeId: TNodeID, + currentNode: LineageNode | null, + selectedNodeId: TNodeID | null, + selectedNodes: Set, +) { + const sources = useNodeConnections({ + id: nodeId, + handleType: 'source', + }) + const targets = useNodeConnections({ + id: nodeId, + handleType: 'target', + }) + + const leftId = targets.length > 0 ? nodeId : undefined + const rightId = sources.length > 0 ? nodeId : undefined + const isCurrent = currentNode?.id === nodeId + const isSelected = selectedNodeId === nodeId + const isActive = selectedNodes.has(nodeId) + + return { + leftId, + rightId, + isCurrent, + isSelected, + isActive, + } +} diff --git a/web/common/src/components/Lineage/stories/Lineage.stories.tsx b/web/common/src/components/Lineage/stories/Lineage.stories.tsx new file mode 100644 index 0000000000..4ad8ca9f8b --- /dev/null +++ b/web/common/src/components/Lineage/stories/Lineage.stories.tsx @@ -0,0 +1,192 @@ +import type { LineageAdjacencyList, LineageDetails } from '../utils' + +import { ModelLineage } from './ModelLineage' +import type { ModelLineageNodeDetails, ModelName } from './ModelLineageContext' + +export default { + title: 'Components/Lineage', +} + +export const LineageModel = () => { + return ( +
      + + + } + lineageDetails={ + { + 'sqlmesh.sushi.raw_orders': { + name: 'sqlmesh.sushi.raw_orders', + display_name: 'sushi.raw_orders', + identifier: '123456789', + version: '123456789', + dialect: 'bigquery', + cron: '0 0 * * *', + owner: 'admin', + kind: 'INCREMENTAL_BY_TIME', + model_type: 'python', + tags: ['test', 'tag', 'another tag'], + columns: { + user_id: { + data_type: 'STRING', + description: 'node', + }, + event_id: { + data_type: 'STRING', + description: 'node', + }, + created_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + }, + }, + 'sqlmesh.sushi.orders': { + name: 'sqlmesh.sushi.orders', + display_name: 'sushi.orders', + identifier: '123456789', + version: '123456789', + dialect: 'bigquery', + cron: '0 0 * * *', + owner: 'admin', + kind: 'INCREMENTAL_BY_TIME', + model_type: 'sql', + tags: ['test', 'tag', 'another tag'], + columns: { + user_id: { + data_type: 'STRING', + description: 'node', + columnLineageData: { + 'sqlmesh.sushi.orders': { + user_id: { + source: 'sqlmesh.sushi.raw_orders', + expression: + 'select user_id from sqlmesh.sushi.raw_orders', + models: { + 'sqlmesh.sushi.raw_orders': ['user_id'], + }, + }, + }, + }, + }, + event_id: { + data_type: 'STRING', + description: 'node', + columnLineageData: { + 'sqlmesh.sushi.orders': { + event_id: { + models: { + 'sqlmesh.sushi.raw_orders': ['event_id'], + }, + }, + }, + }, + }, + product_id: { + data_type: 'STRING', + description: 'node', + }, + customer_id: { + data_type: 'STRING', + description: 'node', + }, + updated_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + deleted_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + expired_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + start_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + end_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + created_ts: { + data_type: 'TIMESTAMP', + description: 'node', + }, + }, + }, + } as LineageDetails + } + className="rounded-2xl" + /> +
      + ) +} diff --git a/web/common/src/components/Lineage/stories/ModelLineage.tsx b/web/common/src/components/Lineage/stories/ModelLineage.tsx new file mode 100644 index 0000000000..d704b6a209 --- /dev/null +++ b/web/common/src/components/Lineage/stories/ModelLineage.tsx @@ -0,0 +1,416 @@ +import { debounce } from 'lodash' +import { Focus, Rows2, Rows3 } from 'lucide-react' +import React from 'react' + +import { type ColumnLevelLineageAdjacencyList } from '../LineageColumnLevel/ColumnLevelLineageContext' +import { + MAX_COLUMNS_TO_DISPLAY, + calculateColumnsHeight, + calculateNodeColumnsCount, + calculateSelectedColumnsHeight, + getEdgesFromColumnLineage, +} from '../LineageColumnLevel/help' +import { useColumnLevelLineage } from '../LineageColumnLevel/useColumnLevelLineage' +import { LineageControlButton } from '../LineageControlButton' +import { LineageControlIcon } from '../LineageControlIcon' +import { LineageLayout } from '../LineageLayout' +import { FactoryEdgeWithGradient } from '../edge/FactoryEdgeWithGradient' +import { + toNodeID, + toPortID, + type LineageAdjacencyList, + type LineageDetails, +} from '../utils' +import { + calculateNodeBaseHeight, + calculateNodeDetailsHeight, + createEdge, + createNode, + getOnlySelectedNodes, + getTransformedModelEdgesSourceTargets, + getTransformedNodes, +} from '../help' +import { + type LineageEdge, + type LineageNodesMap, + ZOOM_THRESHOLD, +} from '../utils' +import { + type EdgeData, + ModelLineageContext, + type ModelLineageNodeDetails, + type ModelName, + type ColumnName, + type NodeData, + useModelLineage, + type ModelNodeId, + type ModelColumnID, + type ModelEdgeId, + type NodeType, +} from './ModelLineageContext' +import { ModelNode } from './ModelNode' +import { getNodeTypeColorVar } from './help' +import { EdgeWithGradient } from '../edge/EdgeWithGradient' +import { cleanupLayoutWorker, getLayoutedGraph } from '../layout/help' + +const nodeTypes = { + node: ModelNode, +} +const edgeTypes = { + edge: FactoryEdgeWithGradient(useModelLineage), + port: EdgeWithGradient, +} + +export const ModelLineage = ({ + selectedModelName, + adjacencyList, + lineageDetails, + className, +}: { + adjacencyList: LineageAdjacencyList + lineageDetails: LineageDetails + selectedModelName?: ModelName + className?: string +}) => { + const [zoom, setZoom] = React.useState(ZOOM_THRESHOLD) + const [isBuildingLayout, setIsBuildingLayout] = React.useState(false) + const [edges, setEdges] = React.useState< + LineageEdge[] + >([]) + const [nodesMap, setNodesMap] = React.useState< + LineageNodesMap + >({}) + const [showOnlySelectedNodes, setShowOnlySelectedNodes] = + React.useState(false) + const [selectedNodes, setSelectedNodes] = React.useState>( + new Set(), + ) + const [selectedEdges, setSelectedEdges] = React.useState>( + new Set(), + ) + const [selectedNodeId, setSelectedNodeId] = + React.useState(null) + + const [showColumns, setShowColumns] = React.useState(false) + const [columnLevelLineage, setColumnLevelLineage] = React.useState< + Map> + >(new Map()) + const [fetchingColumns, setFetchingColumns] = React.useState< + Set + >(new Set()) + + const { + adjacencyListColumnLevel, + selectedColumns, + adjacencyListKeysColumnLevel, + } = useColumnLevelLineage( + columnLevelLineage, + ) + + const adjacencyListKeys = React.useMemo(() => { + let keys: ModelName[] = [] + + if (adjacencyListKeysColumnLevel.length > 0) { + keys = adjacencyListKeysColumnLevel + } else { + keys = Object.keys(adjacencyList) as ModelName[] + } + + return keys + }, [adjacencyListKeysColumnLevel, adjacencyList]) + + const transformNode = React.useCallback( + (nodeId: ModelNodeId, detail: ModelLineageNodeDetails) => { + const columns = detail.columns + + const node = createNode('node', nodeId, { + name: detail.name, + identifier: detail.identifier, + model_type: detail.model_type as NodeType, + kind: detail.kind!, + cron: detail.cron, + displayName: detail.display_name, + owner: detail.owner!, + dialect: detail.dialect, + version: detail.version, + tags: detail.tags || [], + columns, + }) + const selectedColumnsCount = new Set( + Object.keys(columns ?? {}).map(k => toPortID(detail.name, k)), + ).intersection(selectedColumns).size + // We are trying to project the node hight so we are including the ceiling and floor heights + const nodeBaseHeight = calculateNodeBaseHeight({ + includeNodeFooterHeight: false, + includeCeilingHeight: true, + includeFloorHeight: true, + }) + const nodeDetailsHeight = calculateNodeDetailsHeight({ + nodeDetailsCount: 0, + }) + const selectedColumnsHeight = + calculateSelectedColumnsHeight(selectedColumnsCount) + + const columnsHeight = calculateColumnsHeight({ + columnsCount: calculateNodeColumnsCount( + Object.keys(columns ?? {}).length, + ), + hasColumnsFilter: + Object.keys(columns ?? {}).length > MAX_COLUMNS_TO_DISPLAY, + }) + + node.height = + nodeBaseHeight + + nodeDetailsHeight + + selectedColumnsHeight + + columnsHeight + + return node + }, + [selectedColumns], + ) + + const transformedNodesMap = React.useMemo(() => { + return getTransformedNodes< + ModelName, + ModelLineageNodeDetails, + NodeData, + ModelNodeId + >(adjacencyListKeys, lineageDetails, transformNode) + }, [adjacencyListKeys, lineageDetails, transformNode]) + + const transformEdge = React.useCallback( + ( + edgeType: string, + edgeId: ModelEdgeId, + sourceId: ModelNodeId, + targetId: ModelNodeId, + sourceHandleId?: ModelColumnID, + targetHandleId?: ModelColumnID, + ) => { + const sourceNode = transformedNodesMap[sourceId] + const targetNode = transformedNodesMap[targetId] + const data: EdgeData = {} + + if (sourceHandleId) { + data.startColor = 'var(--color-lineage-node-port-edge-source)' + } else { + if (sourceNode?.data?.model_type) { + data.startColor = getNodeTypeColorVar( + sourceNode.data.model_type as NodeType, + ) + } + } + + if (targetHandleId) { + data.endColor = 'var(--color-lineage-node-port-edge-target)' + } else { + if (targetNode?.data?.model_type) { + data.endColor = getNodeTypeColorVar( + targetNode.data.model_type as NodeType, + ) + } + } + + if (sourceHandleId && targetHandleId) { + data.strokeWidth = 2 + } + + return createEdge( + edgeType, + edgeId, + sourceId, + targetId, + sourceHandleId, + targetHandleId, + data, + ) + }, + [transformedNodesMap], + ) + + const edgesColumnLevel = React.useMemo( + () => + getEdgesFromColumnLineage< + ModelName, + ColumnName, + EdgeData, + ModelEdgeId, + ModelNodeId, + ModelColumnID + >({ + columnLineage: adjacencyListColumnLevel, + transformEdge, + }), + [adjacencyListColumnLevel, transformEdge], + ) + + const transformedEdges = React.useMemo(() => { + return edgesColumnLevel.length > 0 + ? edgesColumnLevel + : getTransformedModelEdgesSourceTargets< + ModelName, + EdgeData, + ModelNodeId, + ModelEdgeId, + ModelColumnID + >(adjacencyListKeys, adjacencyList, transformEdge) + }, [adjacencyListKeys, adjacencyList, transformEdge, edgesColumnLevel]) + + const calculateLayout = React.useMemo(() => { + return debounce( + ( + eds: LineageEdge[], + nds: LineageNodesMap, + ) => + getLayoutedGraph( + eds, + nds, + new URL('./dagreLayout.worker.ts', import.meta.url), + ) + .then(({ edges, nodesMap }) => { + setEdges(edges) + setNodesMap(nodesMap) + }) + .catch(error => { + console.error('Layout processing failed:', error) + setEdges([]) + setNodesMap({}) + }) + .finally(() => { + setIsBuildingLayout(false) + }), + 200, + ) + }, []) + + const nodes = React.useMemo(() => { + return Object.values(nodesMap) + }, [nodesMap]) + + const currentNode = React.useMemo(() => { + return selectedModelName + ? nodesMap[toNodeID(selectedModelName)] + : null + }, [selectedModelName, nodesMap]) + + const handleReset = React.useCallback(() => { + setShowColumns(false) + setEdges([]) + setNodesMap({}) + setShowOnlySelectedNodes(false) + setSelectedNodes(new Set()) + setSelectedEdges(new Set()) + setSelectedNodeId(null) + setColumnLevelLineage(new Map()) + }, []) + + React.useEffect(() => { + setIsBuildingLayout(true) + + if (showOnlySelectedNodes) { + const onlySelectedNodesMap = getOnlySelectedNodes( + transformedNodesMap, + selectedNodes, + ) + const onlySelectedEdges = transformedEdges.filter(edge => + selectedEdges.has(edge.id as ModelEdgeId), + ) + calculateLayout(onlySelectedEdges, onlySelectedNodesMap) + } else { + calculateLayout(transformedEdges, transformedNodesMap) + } + }, [ + calculateLayout, + showOnlySelectedNodes, + transformedEdges, + transformedNodesMap, + ]) + + React.useEffect(() => { + const currentNodeId = selectedModelName + ? toNodeID(selectedModelName) + : undefined + + if (currentNodeId && currentNodeId in nodesMap) { + setSelectedNodeId(currentNodeId) + } else { + handleReset() + } + }, [handleReset, selectedModelName]) + + // Cleanup worker on unmount + React.useEffect(() => () => cleanupLayoutWorker(), []) + + function toggleColumns() { + setShowColumns(prev => !prev) + } + + return ( + + + useLineage={useModelLineage} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + className={className} + controls={ + <> + toggleColumns()} + disabled={isBuildingLayout} + > + {showColumns ? ( + + ) : ( + + )} + + handleReset()} + disabled={isBuildingLayout} + > + + + + } + /> + + ) +} diff --git a/web/common/src/components/Lineage/stories/ModelLineageContext.ts b/web/common/src/components/Lineage/stories/ModelLineageContext.ts new file mode 100644 index 0000000000..98d2131766 --- /dev/null +++ b/web/common/src/components/Lineage/stories/ModelLineageContext.ts @@ -0,0 +1,97 @@ +import type { Branded } from '@/types' +import { + type ColumnLevelLineageAdjacencyList, + type ColumnLevelLineageContextValue, + getColumnLevelLineageContextInitial, +} from '../LineageColumnLevel/ColumnLevelLineageContext' +import { type Column } from '../LineageColumnLevel/useColumns' +import { + type LineageContextValue, + createLineageContext, + getInitial as getLineageContextInitial, +} from '../LineageContext' +import { type PathType } from '../utils' + +export type ModelName = Branded +export type ColumnName = Branded +export type ModelColumnID = Branded +export type ModelNodeId = Branded +export type ModelEdgeId = Branded +export type ModelColumn = Column & { + id: ModelColumnID + name: ColumnName + columnLineageData?: ColumnLevelLineageAdjacencyList +} + +export type NodeType = 'sql' | 'python' +export type ModelLineageNodeDetails = { + name: ModelName + display_name: string + identifier: string + version: string + dialect: string + cron: string + owner?: string + kind?: string + model_type?: string + tags?: string[] + columns?: Record +} + +export type NodeData = { + name: ModelName + displayName: string + model_type: NodeType + identifier: string + version: string + kind: string + cron: string + owner: string + dialect: string + columns?: Record + tags: string[] +} + +export type EdgeData = { + pathType?: PathType + startColor?: string + endColor?: string + strokeWidth?: number +} + +export type ModelLineageContextValue = ColumnLevelLineageContextValue< + ModelName, + ColumnName, + ModelColumnID +> & + LineageContextValue< + NodeData, + EdgeData, + ModelNodeId, + ModelEdgeId, + ModelColumnID + > + +export const initial = { + ...getLineageContextInitial(), + ...getColumnLevelLineageContextInitial< + ModelName, + ColumnName, + ModelColumnID + >(), +} + +export const { Provider, useLineage } = createLineageContext< + NodeData, + EdgeData, + ModelNodeId, + ModelEdgeId, + ModelColumnID, + ModelLineageContextValue +>(initial) + +export const ModelLineageContext = { + Provider, +} + +export const useModelLineage = useLineage diff --git a/web/common/src/components/Lineage/stories/ModelNode.tsx b/web/common/src/components/Lineage/stories/ModelNode.tsx new file mode 100644 index 0000000000..2f4705f1c1 --- /dev/null +++ b/web/common/src/components/Lineage/stories/ModelNode.tsx @@ -0,0 +1,331 @@ +import cronstrue from 'cronstrue' +import React from 'react' + +import { cn } from '@/utils' +import { HorizontalContainer } from '../../HorizontalContainer/HorizontalContainer' +import { VerticalContainer } from '../../VerticalContainer/VerticalContainer' +import { + MAX_COLUMNS_TO_DISPLAY, + calculateColumnsHeight, + calculateNodeColumnsCount, + calculateSelectedColumnsHeight, +} from '../LineageColumnLevel/help' +import { useColumns, type Column } from '../LineageColumnLevel/useColumns' +import { calculateNodeBaseHeight, calculateNodeDetailsHeight } from '../help' +import { NodeAppendix } from '../node/NodeAppendix' +import { NodeBadge } from '../node/NodeBadge' +import { NodeBase } from '../node/NodeBase' +import { NodeContainer } from '../node/NodeContainer' +import { NodeHandleIcon } from '../node/NodeHandleIcon' +import { NodeHandles } from '../node/NodeHandles' +import { NodeHeader } from '../node/NodeHeader' +import { useNodeMetadata, type NodeProps } from '../node/useNodeMetadata' +import { ZOOM_THRESHOLD } from '../utils' +import { + type ModelName as ModelNameType, + type ColumnName, + type NodeData, + useModelLineage, + type ModelColumn, + type ModelNodeId, + type ModelColumnID, + type NodeType, +} from './ModelLineageContext' +import { ModelNodeColumn } from './ModelNodeColumn' +import { + getNodeTypeBorderColor, + getNodeTypeColor, + getNodeTypeTextColor, +} from './help' +import { Tooltip } from '@/components/Tooltip/Tooltip' +import type { ColumnLevelLineageAdjacencyList } from '../LineageColumnLevel/ColumnLevelLineageContext' +import { ModelName } from '@/components/ModelName/ModelName' +import { Badge } from '@/components/Badge/Badge' +import { NodePorts } from '../node/NodePorts' + +export const ModelNode = React.memo(function ModelNode({ + id, + data, + ...props +}: NodeProps) { + const { + selectedColumns, + zoom, + currentNode, + selectedNodeId, + selectedNodes, + showColumns, + fetchingColumns, + setSelectedNodeId, + } = useModelLineage() + + const [showNodeColumns, setShowNodeColumns] = React.useState(showColumns) + const [isHovered, setIsHovered] = React.useState(false) + + const nodeId = id as ModelNodeId + + const { + leftId, + rightId, + isSelected, // if selected from inside the lineage and node is selcted + isActive, // if selected from inside the lineage and node is not selected but in path + } = useNodeMetadata(nodeId, currentNode, selectedNodeId, selectedNodes) + + const { + columns, + selectedColumns: modelSelectedColumns, + columnNames, + } = useColumns( + selectedColumns, + data.name, + data.columns, + ) + + const hasSelectedColumns = selectedColumns.intersection(columnNames).size > 0 + const hasFetchingColumns = fetchingColumns.intersection(columnNames).size > 0 + + React.useEffect(() => { + setShowNodeColumns(showColumns || isSelected) + }, [columnNames, isSelected, showColumns]) + + function toggleSelectedNode() { + setSelectedNodeId(prev => (prev === nodeId ? null : nodeId)) + } + + const shouldShowColumns = + showNodeColumns || hasSelectedColumns || hasFetchingColumns || isHovered + const modelType = data.model_type?.toLowerCase() as NodeType + const hasColumnsFilter = + shouldShowColumns && columns.length > MAX_COLUMNS_TO_DISPLAY + // We are not including the footer, because we need actual height to dynamically adjust node container height + const nodeBaseHeight = calculateNodeBaseHeight({ + includeNodeFooterHeight: false, + includeCeilingHeight: false, + includeFloorHeight: false, + }) + const nodeDetailsHeight = + zoom > ZOOM_THRESHOLD + ? calculateNodeDetailsHeight({ + nodeDetailsCount: 0, + }) + : 0 + const selectedColumnsHeight = calculateSelectedColumnsHeight( + modelSelectedColumns.length, + ) + const columnsHeight = + zoom > ZOOM_THRESHOLD && shouldShowColumns + ? calculateColumnsHeight({ + columnsCount: calculateNodeColumnsCount(columns.length), + hasColumnsFilter, + }) + : 0 + + // If zoom is less than ZOOM_THRESHOLD, we are making node looks bigger + const nodeHeight = + (zoom > ZOOM_THRESHOLD ? nodeBaseHeight : nodeBaseHeight * 2) + + nodeDetailsHeight + + selectedColumnsHeight + + columnsHeight + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + {zoom > ZOOM_THRESHOLD && ( + <> + {data.kind?.toUpperCase()} + {data.cron && ( + + {data.cron.toUpperCase()} + + } + className="text-xs p-2 rounded-md font-semibold" + > + + UTC Time + {cronstrue.toString(data.cron, { + dayOfWeekStartIndexZero: true, + use24HourTimeFormat: true, + verbose: true, + })} + + + )} + + )} + + + + ZOOM_THRESHOLD ? 'shrink-0 h-7' : 'h-full')} + onClick={toggleSelectedNode} + > + + } + rightIcon={ + + } + handleClassName="top-4" + > + + ZOOM_THRESHOLD + ? ' text-xs' + : 'text-2xl justify-center', + )} + /> + + + + {shouldShowColumns && ( + <> + {modelSelectedColumns.length > 0 && ( + + {modelSelectedColumns.map(column => ( + + } + ).columnLineageData + } + /> + ))} + + )} + {columns.length > 0 && zoom > ZOOM_THRESHOLD && ( + + ports={columns} + estimatedListItemHeight={24} + isFilterable={hasColumnsFilter} + filterOptions={{ + keys: ['name', 'description'], + threshold: 0.3, + }} + renderPort={column => ( + + } + ).columnLineageData + } + /> + )} + className="border-t border-lineage-divider" + /> + )} + + )} + + {modelType && ( + + ZOOM_THRESHOLD ? 'h-5' : 'h-8', + )} + > + ZOOM_THRESHOLD ? '2xs' : 'm'} + className={cn( + 'text-[white] font-black', + getNodeTypeColor(modelType), + )} + > + {modelType.toUpperCase()} + + + + )} + + ) +}) diff --git a/web/common/src/components/Lineage/stories/ModelNodeColumn.tsx b/web/common/src/components/Lineage/stories/ModelNodeColumn.tsx new file mode 100644 index 0000000000..35d4a0e592 --- /dev/null +++ b/web/common/src/components/Lineage/stories/ModelNodeColumn.tsx @@ -0,0 +1,76 @@ +import React from 'react' + +import { type ColumnLevelLineageAdjacencyList } from '../LineageColumnLevel/ColumnLevelLineageContext' +import { FactoryColumn } from '../LineageColumnLevel/FactoryColumn' + +import { + useModelLineage, + type ModelColumnID, + type ModelName, + type ModelNodeId, + type ColumnName, +} from './ModelLineageContext' + +const ModelColumn = FactoryColumn< + ModelName, + ColumnName, + ModelNodeId, + ModelColumnID +>(useModelLineage) + +export const ModelNodeColumn = React.memo(function ModelNodeColumn({ + id, + nodeId, + modelName, + name, + description, + type, + className, + columnLineageData, +}: { + id: ModelColumnID + nodeId: ModelNodeId + modelName: ModelName + name: ColumnName + type: string + description?: string | null + className?: string + columnLineageData?: ColumnLevelLineageAdjacencyList +}) { + const { selectedColumns, setColumnLevelLineage } = useModelLineage() + + const isSelectedColumn = selectedColumns.has(id) + + async function toggleSelectedColumn() { + if (isSelectedColumn) { + setColumnLevelLineage(prev => { + prev.delete(id) + return new Map(prev) + }) + } else { + if (columnLineageData != null) { + setColumnLevelLineage(prev => new Map(prev).set(id, columnLineageData)) + } + } + } + + return ( + console.log('cancel')} + renderError={error =>
      Error: {error.message}
      } + renderExpression={expression =>
      {expression}
      } + renderSource={source =>
      {source}
      } + /> + ) +}) diff --git a/web/common/src/components/Lineage/stories/dagreLayout.worker.ts b/web/common/src/components/Lineage/stories/dagreLayout.worker.ts new file mode 100644 index 0000000000..1a6a9d3fe7 --- /dev/null +++ b/web/common/src/components/Lineage/stories/dagreLayout.worker.ts @@ -0,0 +1,24 @@ +import { + type LayoutedGraph, + type LineageEdgeData, + type LineageNodeData, +} from '../utils' +import { buildLayout } from '../layout/dagreLayout' + +self.onmessage = < + TNodeData extends LineageNodeData = LineageNodeData, + TEdgeData extends LineageEdgeData = LineageEdgeData, +>( + event: MessageEvent>, +) => { + try { + const { edges, nodesMap } = buildLayout(event.data) + + self.postMessage({ + edges, + nodesMap, + } as LayoutedGraph) + } catch (outerError) { + self.postMessage({ error: outerError } as { error: ErrorEvent }) + } +} diff --git a/web/common/src/components/Lineage/stories/help.ts b/web/common/src/components/Lineage/stories/help.ts new file mode 100644 index 0000000000..f26c8c5752 --- /dev/null +++ b/web/common/src/components/Lineage/stories/help.ts @@ -0,0 +1,29 @@ +import { type NodeType } from './ModelLineageContext' + +export function getNodeTypeColorVar(nodeType: NodeType) { + return { + sql: 'var(--color-lineage-node-type-background-sql)', + python: 'var(--color-lineage-node-type-background-python)', + }[nodeType] +} + +export function getNodeTypeColor(nodeType: NodeType) { + return { + sql: 'bg-lineage-node-type-background-sql', + python: 'bg-lineage-node-type-background-python', + }[nodeType] +} + +export function getNodeTypeTextColor(nodeType: NodeType) { + return { + sql: 'text-lineage-node-type-foreground-sql', + python: 'text-lineage-node-type-foreground-python', + }[nodeType] +} + +export function getNodeTypeBorderColor(nodeType: NodeType) { + return { + sql: 'border-lineage-node-type-border-sql', + python: 'border-lineage-node-type-border-python', + }[nodeType] +} diff --git a/web/common/src/components/Lineage/utils.ts b/web/common/src/components/Lineage/utils.ts new file mode 100644 index 0000000000..01a277f17a --- /dev/null +++ b/web/common/src/components/Lineage/utils.ts @@ -0,0 +1,108 @@ +import type { Branded } from '@/types' +import { type Edge, type Node } from '@xyflow/react' + +export type NodeId = Branded +export type EdgeId = Branded +export type PortId = Branded + +export type LineageNodeData = Record +export type LineageEdgeData = Record + +export type LineageAdjacencyList = + Record + +export type LineageDetails = Record< + TAdjacencyListKey, + TValue +> + +export type LineageNodesMap< + TNodeData extends LineageNodeData, + TNodeID extends string = NodeId, +> = Record> +export interface LineageNode< + TNodeData extends LineageNodeData, + TNodeID extends string = NodeId, +> extends Node { + id: TNodeID +} + +export interface LineageEdge< + TEdgeData extends LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +> extends Edge { + id: TEdgeID + source: TNodeID + target: TNodeID + sourceHandle?: TPortID + targetHandle?: TPortID +} + +export type LayoutedGraph< + TNodeData extends LineageNodeData = LineageNodeData, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +> = { + edges: LineageEdge[] + nodesMap: LineageNodesMap +} + +export type PathType = 'bezier' | 'smoothstep' | 'step' | 'straight' +export type TransformNodeFn< + TData, + TNodeData extends LineageNodeData = LineageNodeData, + TNodeID extends string = NodeId, +> = (nodeId: TNodeID, data: TData) => LineageNode + +export type TransformEdgeFn< + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +> = ( + edgeType: string, + edgeId: TEdgeID, + sourceId: TNodeID, + targetId: TNodeID, + sourceColumnId?: TPortID, + targetColumnId?: TPortID, +) => LineageEdge + +export const DEFAULT_NODE_HEIGHT = 32 +export const DEFAULT_NODE_WIDTH = 300 +export const DEFAULT_ZOOM = 0.85 +export const MIN_ZOOM = 0.01 +export const MAX_ZOOM = 1.75 +export const ZOOM_THRESHOLD = 0.75 +export const NODES_TRESHOLD = 200 +export const NODES_TRESHOLD_ZOOM = 0.1 + +// ID generated from toInternalID is meant to be used only internally to identify nodes, edges and ports within the graph +// Do not rely on the ID to be a valid URL, or anythjin outside of the graph +export function toInternalID( + ...args: string[] +): TReturn { + return encodeURI(args.filter(Boolean).join('.')) as TReturn +} + +export function toNodeID( + ...args: string[] +): TNodeID { + return toInternalID(...args) +} + +export function toEdgeID( + ...args: string[] +): TEdgeID { + return toInternalID(...args) +} + +export function toPortID( + ...args: string[] +): TPortId { + return toInternalID(...args) +} diff --git a/web/common/src/components/MessageContainer/MessageContainer.css b/web/common/src/components/MessageContainer/MessageContainer.css new file mode 100644 index 0000000000..f632bc791f --- /dev/null +++ b/web/common/src/components/MessageContainer/MessageContainer.css @@ -0,0 +1,3 @@ +:root { + --color-message-translucid: var(--color-neutral-3); +} diff --git a/web/common/src/components/MessageContainer/MessageContainer.tsx b/web/common/src/components/MessageContainer/MessageContainer.tsx index d51213bfaf..16d35ea47d 100644 --- a/web/common/src/components/MessageContainer/MessageContainer.tsx +++ b/web/common/src/components/MessageContainer/MessageContainer.tsx @@ -2,6 +2,8 @@ import { cn } from '@/utils' import { LoadingContainer } from '../LoadingContainer/LoadingContainer' import { HorizontalContainer } from '../HorizontalContainer/HorizontalContainer' +import './MessageContainer.css' + export interface MessageContainerProps { children: React.ReactNode className?: string @@ -19,7 +21,7 @@ export function MessageContainer({ diff --git a/web/common/src/components/Metadata/Metadata.css b/web/common/src/components/Metadata/Metadata.css new file mode 100644 index 0000000000..b1f5f0dfeb --- /dev/null +++ b/web/common/src/components/Metadata/Metadata.css @@ -0,0 +1,4 @@ +:root { + --color-metadata-label: var(--color-neutral-600); + --color-metadata-value: var(--color-prose); +} diff --git a/web/common/src/components/ModelName/ModelName.tsx b/web/common/src/components/ModelName/ModelName.tsx index 0685d4b872..83013d8108 100644 --- a/web/common/src/components/ModelName/ModelName.tsx +++ b/web/common/src/components/ModelName/ModelName.tsx @@ -144,7 +144,13 @@ export const ModelName = React.forwardRef( : 'text-model-name-model', )} > - {truncate(model, truncateMaxCharsModel, 15)} + {truncate( + model, + truncateMaxCharsModel, + truncateLimitBefore * 2, + '...', + truncateLimitBefore * 2, + )} ) diff --git a/web/common/src/components/Typography/Information.tsx b/web/common/src/components/Typography/Information.tsx index d0da7622d2..d4fc0f2b83 100644 --- a/web/common/src/components/Typography/Information.tsx +++ b/web/common/src/components/Typography/Information.tsx @@ -47,7 +47,7 @@ export function Information({ sideOffset={sideOffset} side={side} className={cn( - 'z-50 select-none max-w-md whitespace-wrap rounded-md bg-dark text-light px-4 py-2 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade', + 'z-50 select-none whitespace-wrap rounded-md', getTextSize(size), classNameTooltip, )} diff --git a/web/common/src/components/VirtualList/FilterableList.css b/web/common/src/components/VirtualList/FilterableList.css new file mode 100644 index 0000000000..4dfdd87eea --- /dev/null +++ b/web/common/src/components/VirtualList/FilterableList.css @@ -0,0 +1,9 @@ +:root { + --color-filterable-list-counter-background: var(--color-badge-background); + --color-filterable-list-counter-foreground: var(--color-badge-foreground); + + --color-filterable-list-input-background: var(--color-input-background); + --color-filterable-list-input-foreground: var(--color-input-foreground); + --color-filterable-list-input-placeholder: var(--color-input-placeholder); + --color-filterable-list-input-border: var(--color-input-border); +} diff --git a/web/common/src/components/VirtualList/FilterableList.tsx b/web/common/src/components/VirtualList/FilterableList.tsx index ba6c5950b5..5ea0d35039 100644 --- a/web/common/src/components/VirtualList/FilterableList.tsx +++ b/web/common/src/components/VirtualList/FilterableList.tsx @@ -8,6 +8,8 @@ import { cn } from '@/utils' import { MessageContainer } from '../MessageContainer/MessageContainer' import { Input } from '../Input/Input' +import './FilterableList.css' + export interface FilterableListProps { items: TItem[] filterOptions?: IFuseOptions @@ -83,7 +85,10 @@ function Counter({ return ( {itemsLength !== filteredItemsLength && ( <> diff --git a/web/common/src/components/VirtualList/VirtualList.tsx b/web/common/src/components/VirtualList/VirtualList.tsx index 94e5d93c05..adf1010508 100644 --- a/web/common/src/components/VirtualList/VirtualList.tsx +++ b/web/common/src/components/VirtualList/VirtualList.tsx @@ -1,4 +1,8 @@ -import { useVirtualizer } from '@tanstack/react-virtual' +import { + useVirtualizer, + Virtualizer, + type VirtualItem, +} from '@tanstack/react-virtual' import React from 'react' import { HorizontalContainer } from '../HorizontalContainer/HorizontalContainer' import { cn } from '@/utils' @@ -9,7 +13,11 @@ import { VerticalContainer } from '../VerticalContainer/VerticalContainer' export interface VirtualListProps { items: TItem[] estimatedListItemHeight: number - renderListItem: (item: TItem) => React.ReactNode + renderListItem: ( + item: TItem, + virtualItem?: VirtualItem, + virtualizer?: Virtualizer, + ) => React.ReactNode isSelected?: (item: TItem) => boolean className?: string } diff --git a/web/common/src/styles/design/semantic-colors.css b/web/common/src/styles/design/semantic-colors.css index 4217b7f654..c329960ce8 100644 --- a/web/common/src/styles/design/semantic-colors.css +++ b/web/common/src/styles/design/semantic-colors.css @@ -68,14 +68,4 @@ --color-typography-tagline: var(--color-neutral-600); --color-typography-description: var(--color-neutral-500); --color-typography-info: var(--color-typography-tagline); - - /* Message */ - --color-message-lucid: var(--color-neutral-3); - - /* Input */ - --color-input-background: var(--color-light); - --color-input-background-lucid: var(--color-neutral-5); - --color-input-foreground: var(--color-prose); - --color-input-placeholder: var(--color-neutral-400); - --color-input-border: var(--color-neutral-300); } diff --git a/web/common/tailwind.base.config.js b/web/common/tailwind.base.config.js index cbba9768c2..49354591cc 100644 --- a/web/common/tailwind.base.config.js +++ b/web/common/tailwind.base.config.js @@ -1,5 +1,9 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { +import lineageConfig from './tailwind.lineage.config' +import typography from '@tailwindcss/typography' +import scrollbar from 'tailwind-scrollbar' + +export default { + presets: [lineageConfig], theme: { colors: {}, extend: { @@ -43,7 +47,7 @@ module.exports = { info: 'var(--color-typography-info)', }, message: { - lucid: 'var(--color-message-lucid)', + translucid: 'var(--color-message-translucid)', }, link: { underline: 'var(--color-link-underline)', @@ -72,8 +76,20 @@ module.exports = { background: 'var(--color-badge-background)', foreground: 'var(--color-badge-foreground)', }, + 'filterable-list': { + counter: { + background: 'var(--color-filterable-list-counter-background)', + foreground: 'var(--color-filterable-list-counter-foreground)', + }, + input: { + background: 'var(--color-filterable-list-input-background)', + foreground: 'var(--color-filterable-list-input-foreground)', + placeholder: 'var(--color-filterable-list-input-placeholder)', + border: 'var(--color-filterable-list-input-border)', + }, + }, input: { - 'background-lucid': 'var(--color-input-background-lucid)', + 'background-translucid': 'var(--color-input-background-translucid)', background: 'var(--color-input-background)', foreground: 'var(--color-input-foreground)', placeholder: 'var(--color-input-placeholder)', @@ -121,6 +137,10 @@ module.exports = { background: 'var(--color-tooltip-background)', foreground: 'var(--color-tooltip-foreground)', }, + metadata: { + label: 'var(--color-metadata-label)', + value: 'var(--color-metadata-value)', + }, }, borderRadius: { '2xs': 'var(--radius-xs)', @@ -148,8 +168,8 @@ module.exports = { }, }, plugins: [ - require('@tailwindcss/typography'), - require('tailwind-scrollbar')({ + typography, + scrollbar({ nocompatible: true, preferredStrategy: 'pseudoelements', }), diff --git a/web/common/tailwind.config.js b/web/common/tailwind.config.js index 67fe2ac528..4e7eee7f2f 100644 --- a/web/common/tailwind.config.js +++ b/web/common/tailwind.config.js @@ -1,5 +1,6 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { +import baseConfig from './tailwind.base.config' + +export default { + presets: [baseConfig], content: ['./src/**/*.{js,ts,jsx,tsx}', './src/**/*.stories.{js,ts,jsx,tsx}'], - presets: [require('./tailwind.base.config')], } diff --git a/web/common/tailwind.lineage.config.js b/web/common/tailwind.lineage.config.js new file mode 100644 index 0000000000..c2c8800a6f --- /dev/null +++ b/web/common/tailwind.lineage.config.js @@ -0,0 +1,95 @@ +export default { + theme: { + colors: {}, + extend: { + colors: { + lineage: { + background: 'var(--color-lineage-background)', + divider: 'var(--color-lineage-divider)', + border: 'var(--color-lineage-border)', + control: { + background: { + DEFAULT: 'var(--color-lineage-control-background)', + hover: 'var(--color-lineage-control-background-hover)', + }, + icon: { + background: 'var(--color-lineage-control-icon-background)', + foreground: 'var(--color-lineage-control-icon-foreground)', + }, + button: { + tooltip: { + background: + 'var(--color-lineage-control-button-tooltip-background)', + foreground: + 'var(--color-lineage-control-button-tooltip-foreground)', + }, + }, + }, + grid: { + dot: 'var(--color-lineage-grid-dot)', + }, + edge: { + DEFAULT: 'var(--color-lineage-edge)', + }, + node: { + background: 'var(--color-lineage-node-background)', + foreground: 'var(--color-lineage-node-foreground)', + selected: { + border: 'var(--color-lineage-node-selected-border)', + }, + border: { + DEFAULT: 'var(--color-lineage-node-border)', + hover: 'var(--color-lineage-node-border-hover)', + }, + badge: { + background: 'var(--color-lineage-node-badge-background)', + foreground: 'var(--color-lineage-node-badge-foreground)', + }, + appendix: { + background: 'var(--color-lineage-node-appendix-background)', + }, + handle: { + icon: { + background: + 'var(--color-lineage-node-type-handle-icon-background)', + }, + }, + port: { + background: 'var(--color-lineage-node-port-background)', + handle: { + target: 'var(--color-lineage-node-port-handle-target)', + source: 'var(--color-lineage-node-port-handle-source)', + }, + edge: { + source: 'var(--color-lineage-node-port-edge-source)', + target: 'var(--color-lineage-node-port-edge-target)', + }, + }, + }, + model: { + column: { + source: { + background: + 'var(--color-lineage-model-column-source-background)', + }, + expression: { + background: + 'var(--color-lineage-model-column-expression-background)', + }, + error: { + background: + 'var(--color-lineage-model-column-error-background)', + icon: 'var(--color-lineage-model-column-error-icon)', + }, + active: 'var(--color-lineage-model-column-active)', + icon: { + DEFAULT: 'var(--color-lineage-model-column-icon)', + active: 'var(--color-lineage-model-column-icon-active)', + }, + }, + }, + }, + }, + }, + }, +} diff --git a/web/common/tsconfig.base.json b/web/common/tsconfig.base.json index 99a214fe47..ca7c1e0785 100644 --- a/web/common/tsconfig.base.json +++ b/web/common/tsconfig.base.json @@ -3,7 +3,7 @@ "target": "ES2022", "jsx": "react-jsx", "module": "ESNext", - "lib": ["ES2022", "DOM", "DOM.Iterable"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "types": ["vite/client"], /* Bundler mode */ diff --git a/web/common/tsconfig.build.json b/web/common/tsconfig.build.json index 7eba394efd..527242427c 100644 --- a/web/common/tsconfig.build.json +++ b/web/common/tsconfig.build.json @@ -15,6 +15,7 @@ "declarationMap": true, "declarationDir": "./dist", "emitDeclarationOnly": false, - "outDir": "./dist" + "outDir": "./dist", + "rootDir": "./src" } } diff --git a/web/common/vite.config.js b/web/common/vite.config.js index 237bed29bd..f123507484 100644 --- a/web/common/vite.config.js +++ b/web/common/vite.config.js @@ -22,6 +22,10 @@ export default defineConfig({ src: 'tailwind.base.config.js', dest: 'configs', }, + { + src: 'tailwind.lineage.config.js', + dest: 'configs', + }, ], }), ], @@ -33,9 +37,19 @@ export default defineConfig({ build: { cssMinify: true, lib: { - entry: path.resolve(__dirname, 'src/index.ts'), + entry: { + 'sqlmesh-common': path.resolve(__dirname, 'src/index.ts'), + 'lineage/index': path.resolve( + __dirname, + 'src/components/Lineage/index.ts', + ), + }, name: 'sqlmesh-common', - fileName: format => `sqlmesh-common.${format}.js`, + fileName: (format, entryName) => + ({ + 'sqlmesh-common': `sqlmesh-common.${format}.js`, + 'lineage/index': `lineage/index.${format}.js`, + })[entryName], }, rollupOptions: { external: [ From 42fbc64d0468b741e58d23f77fdd11bb84719c9c Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Thu, 2 Oct 2025 12:26:31 -0700 Subject: [PATCH 0930/1056] Chore: Cache results of get_data_objects (#5467) --- sqlmesh/core/engine_adapter/base.py | 132 ++++++- sqlmesh/core/engine_adapter/base_postgres.py | 10 +- sqlmesh/core/engine_adapter/bigquery.py | 7 + sqlmesh/core/engine_adapter/clickhouse.py | 27 +- sqlmesh/core/engine_adapter/mssql.py | 9 + sqlmesh/core/engine_adapter/mysql.py | 4 +- sqlmesh/core/engine_adapter/postgres.py | 6 +- sqlmesh/core/engine_adapter/spark.py | 6 +- sqlmesh/core/snapshot/evaluator.py | 71 +++- tests/core/engine_adapter/test_athena.py | 1 + tests/core/engine_adapter/test_base.py | 370 +++++++++++++++++++ tests/core/engine_adapter/test_snowflake.py | 8 +- tests/core/test_snapshot_evaluator.py | 8 + tests/dbt/test_transformation.py | 6 +- 14 files changed, 618 insertions(+), 47 deletions(-) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index 68c6404081..d9cc4f44a2 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -161,6 +161,7 @@ def __init__( self.correlation_id = correlation_id self._schema_differ_overrides = schema_differ_overrides self._query_execution_tracker = query_execution_tracker + self._data_object_cache: t.Dict[str, t.Optional[DataObject]] = {} def with_settings(self, **kwargs: t.Any) -> EngineAdapter: extra_kwargs = { @@ -983,6 +984,13 @@ def _create_table( ), track_rows_processed=track_rows_processed, ) + # Extract table name to clear cache + table_name = ( + table_name_or_schema.this + if isinstance(table_name_or_schema, exp.Schema) + else table_name_or_schema + ) + self._clear_data_object_cache(table_name) def _build_create_table_exp( self, @@ -1038,7 +1046,8 @@ def create_table_like( target_table_name: The name of the table to create. Can be fully qualified or just table name. source_table_name: The name of the table to base the new table on. """ - self.create_table(target_table_name, self.columns(source_table_name), exists=exists) + self._create_table_like(target_table_name, source_table_name, exists=exists, **kwargs) + self._clear_data_object_cache(target_table_name) def clone_table( self, @@ -1074,6 +1083,7 @@ def clone_table( **kwargs, ) ) + self._clear_data_object_cache(target_table_name) def drop_data_object(self, data_object: DataObject, ignore_if_not_exists: bool = True) -> None: """Drops a data object of arbitrary type. @@ -1139,6 +1149,7 @@ def _drop_object( drop_args["cascade"] = cascade self.execute(exp.Drop(this=exp.to_table(name), kind=kind, exists=exists, **drop_args)) + self._clear_data_object_cache(name) def get_alter_operations( self, @@ -1329,6 +1340,8 @@ def create_view( quote_identifiers=self.QUOTE_IDENTIFIERS_IN_VIEWS, ) + self._clear_data_object_cache(view_name) + # Register table comment with commands if the engine doesn't support doing it in CREATE if ( table_description @@ -1458,8 +1471,14 @@ def columns( } def table_exists(self, table_name: TableName) -> bool: + table = exp.to_table(table_name) + data_object_cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name) + if data_object_cache_key in self._data_object_cache: + logger.debug("Table existence cache hit: %s", data_object_cache_key) + return self._data_object_cache[data_object_cache_key] is not None + try: - self.execute(exp.Describe(this=exp.to_table(table_name), kind="TABLE")) + self.execute(exp.Describe(this=table, kind="TABLE")) return True except Exception: return False @@ -2253,24 +2272,34 @@ def rename_table( "Tried to rename table across catalogs which is not supported" ) self._rename_table(old_table_name, new_table_name) + self._clear_data_object_cache(old_table_name) + self._clear_data_object_cache(new_table_name) - def get_data_object(self, target_name: TableName) -> t.Optional[DataObject]: + def get_data_object( + self, target_name: TableName, safe_to_cache: bool = False + ) -> t.Optional[DataObject]: target_table = exp.to_table(target_name) existing_data_objects = self.get_data_objects( - schema_(target_table.db, target_table.catalog), {target_table.name} + schema_(target_table.db, target_table.catalog), + {target_table.name}, + safe_to_cache=safe_to_cache, ) if existing_data_objects: return existing_data_objects[0] return None def get_data_objects( - self, schema_name: SchemaName, object_names: t.Optional[t.Set[str]] = None + self, + schema_name: SchemaName, + object_names: t.Optional[t.Set[str]] = None, + safe_to_cache: bool = False, ) -> t.List[DataObject]: """Lists all data objects in the target schema. Args: schema_name: The name of the schema to list data objects from. object_names: If provided, only return data objects with these names. + safe_to_cache: Whether it is safe to cache the results of this call. Returns: A list of data objects in the target schema. @@ -2278,15 +2307,64 @@ def get_data_objects( if object_names is not None: if not object_names: return [] - object_names_list = list(object_names) - batches = [ - object_names_list[i : i + self.DATA_OBJECT_FILTER_BATCH_SIZE] - for i in range(0, len(object_names_list), self.DATA_OBJECT_FILTER_BATCH_SIZE) - ] - return [ - obj for batch in batches for obj in self._get_data_objects(schema_name, set(batch)) - ] - return self._get_data_objects(schema_name) + + # Check cache for each object name + target_schema = to_schema(schema_name) + cached_objects = [] + missing_names = set() + + for name in object_names: + cache_key = _get_data_object_cache_key( + target_schema.catalog, target_schema.db, name + ) + if cache_key in self._data_object_cache: + logger.debug("Data object cache hit: %s", cache_key) + data_object = self._data_object_cache[cache_key] + # If the object is none, then the table was previously looked for but not found + if data_object: + cached_objects.append(data_object) + else: + logger.debug("Data object cache miss: %s", cache_key) + missing_names.add(name) + + # Fetch missing objects from database + if missing_names: + object_names_list = list(missing_names) + batches = [ + object_names_list[i : i + self.DATA_OBJECT_FILTER_BATCH_SIZE] + for i in range(0, len(object_names_list), self.DATA_OBJECT_FILTER_BATCH_SIZE) + ] + + fetched_objects = [] + fetched_object_names = set() + for batch in batches: + objects = self._get_data_objects(schema_name, set(batch)) + for obj in objects: + if safe_to_cache: + cache_key = _get_data_object_cache_key( + obj.catalog, obj.schema_name, obj.name + ) + self._data_object_cache[cache_key] = obj + fetched_objects.append(obj) + fetched_object_names.add(obj.name) + + if safe_to_cache: + for missing_name in missing_names - fetched_object_names: + cache_key = _get_data_object_cache_key( + target_schema.catalog, target_schema.db, missing_name + ) + self._data_object_cache[cache_key] = None + + return cached_objects + fetched_objects + + return cached_objects + + fetched_objects = self._get_data_objects(schema_name) + if safe_to_cache: + for obj in fetched_objects: + cache_key = _get_data_object_cache_key(obj.catalog, obj.schema_name, obj.name) + self._data_object_cache[cache_key] = obj + return fetched_objects def fetchone( self, @@ -2693,6 +2771,17 @@ def _to_sql(self, expression: exp.Expression, quote: bool = True, **kwargs: t.An return expression.sql(**sql_gen_kwargs, copy=False) # type: ignore + def _clear_data_object_cache(self, table_name: t.Optional[TableName] = None) -> None: + """Clears the cache entry for the given table name, or clears the entire cache if table_name is None.""" + if table_name is None: + logger.debug("Clearing entire data object cache") + self._data_object_cache.clear() + else: + table = exp.to_table(table_name) + cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name) + logger.debug("Clearing data object cache key: %s", cache_key) + self._data_object_cache.pop(cache_key, None) + def _get_data_objects( self, schema_name: SchemaName, object_names: t.Optional[t.Set[str]] = None ) -> t.List[DataObject]: @@ -2878,6 +2967,15 @@ def _create_column_comments( exc_info=True, ) + def _create_table_like( + self, + target_table_name: TableName, + source_table_name: TableName, + exists: bool, + **kwargs: t.Any, + ) -> None: + self.create_table(target_table_name, self.columns(source_table_name), exists=exists) + def _rename_table( self, old_table_name: TableName, @@ -2940,3 +3038,9 @@ def _decoded_str(value: t.Union[str, bytes]) -> str: if isinstance(value, bytes): return value.decode("utf-8") return value + + +def _get_data_object_cache_key(catalog: t.Optional[str], schema_name: str, object_name: str) -> str: + """Returns a cache key for a data object based on its fully qualified name.""" + catalog = f"{catalog}." if catalog else "" + return f"{catalog}{schema_name}.{object_name}" diff --git a/sqlmesh/core/engine_adapter/base_postgres.py b/sqlmesh/core/engine_adapter/base_postgres.py index c6ba7d6d62..3de975d6a5 100644 --- a/sqlmesh/core/engine_adapter/base_postgres.py +++ b/sqlmesh/core/engine_adapter/base_postgres.py @@ -1,11 +1,12 @@ from __future__ import annotations import typing as t +import logging from sqlglot import exp from sqlmesh.core.dialect import to_schema -from sqlmesh.core.engine_adapter import EngineAdapter +from sqlmesh.core.engine_adapter.base import EngineAdapter, _get_data_object_cache_key from sqlmesh.core.engine_adapter.shared import ( CatalogSupport, CommentCreationTable, @@ -20,6 +21,9 @@ from sqlmesh.core.engine_adapter._typing import QueryOrDF +logger = logging.getLogger(__name__) + + class BasePostgresEngineAdapter(EngineAdapter): DEFAULT_BATCH_SIZE = 400 COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY @@ -75,6 +79,10 @@ def table_exists(self, table_name: TableName) -> bool: Reference: https://github.com/aws/amazon-redshift-python-driver/blob/master/redshift_connector/cursor.py#L528-L553 """ table = exp.to_table(table_name) + data_object_cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name) + if data_object_cache_key in self._data_object_cache: + logger.debug("Table existence cache hit: %s", data_object_cache_key) + return self._data_object_cache[data_object_cache_key] is not None sql = ( exp.select("1") diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 26abad9ebc..09fd7537ef 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -8,6 +8,7 @@ from sqlglot.transforms import remove_precision_parameterized_types from sqlmesh.core.dialect import to_schema +from sqlmesh.core.engine_adapter.base import _get_data_object_cache_key from sqlmesh.core.engine_adapter.mixins import ( ClusteredByMixin, RowDiffMixin, @@ -744,6 +745,12 @@ def insert_overwrite_by_partition( ) def table_exists(self, table_name: TableName) -> bool: + table = exp.to_table(table_name) + data_object_cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name) + if data_object_cache_key in self._data_object_cache: + logger.debug("Table existence cache hit: %s", data_object_cache_key) + return self._data_object_cache[data_object_cache_key] is not None + try: from google.cloud.exceptions import NotFound except ModuleNotFoundError: diff --git a/sqlmesh/core/engine_adapter/clickhouse.py b/sqlmesh/core/engine_adapter/clickhouse.py index 84d6ad311e..45c22a6e55 100644 --- a/sqlmesh/core/engine_adapter/clickhouse.py +++ b/sqlmesh/core/engine_adapter/clickhouse.py @@ -224,7 +224,7 @@ def _insert_overwrite_by_condition( target_columns_to_types = target_columns_to_types or self.columns(target_table) temp_table = self._get_temp_table(target_table) - self._create_table_like(temp_table, target_table) + self.create_table_like(temp_table, target_table) # REPLACE BY KEY: extract kwargs if present dynamic_key = kwargs.get("dynamic_key") @@ -456,7 +456,11 @@ def insert_overwrite_by_partition( ) def _create_table_like( - self, target_table_name: TableName, source_table_name: TableName + self, + target_table_name: TableName, + source_table_name: TableName, + exists: bool, + **kwargs: t.Any, ) -> None: """Create table with identical structure as source table""" self.execute( @@ -632,16 +636,15 @@ def _drop_object( kind: What kind of object to drop. Defaults to TABLE **drop_args: Any extra arguments to set on the Drop expression """ - self.execute( - exp.Drop( - this=exp.to_table(name), - kind=kind, - exists=exists, - cluster=exp.OnCluster(this=exp.to_identifier(self.cluster)) - if self.engine_run_mode.is_cluster - else None, - **drop_args, - ) + super()._drop_object( + name=name, + exists=exists, + kind=kind, + cascade=cascade, + cluster=exp.OnCluster(this=exp.to_identifier(self.cluster)) + if self.engine_run_mode.is_cluster + else None, + **drop_args, ) def _build_partitioned_by_exp( diff --git a/sqlmesh/core/engine_adapter/mssql.py b/sqlmesh/core/engine_adapter/mssql.py index fd0bf1011b..05c3753f14 100644 --- a/sqlmesh/core/engine_adapter/mssql.py +++ b/sqlmesh/core/engine_adapter/mssql.py @@ -3,6 +3,7 @@ from __future__ import annotations import typing as t +import logging from sqlglot import exp @@ -13,6 +14,7 @@ InsertOverwriteStrategy, MERGE_SOURCE_ALIAS, MERGE_TARGET_ALIAS, + _get_data_object_cache_key, ) from sqlmesh.core.engine_adapter.mixins import ( GetCurrentCatalogFromFunctionMixin, @@ -36,6 +38,9 @@ from sqlmesh.core.engine_adapter._typing import DF, Query, QueryOrDF +logger = logging.getLogger(__name__) + + @set_catalog() class MSSQLEngineAdapter( EngineAdapterWithIndexSupport, @@ -144,6 +149,10 @@ def build_var_length_col( def table_exists(self, table_name: TableName) -> bool: """MsSql doesn't support describe so we query information_schema.""" table = exp.to_table(table_name) + data_object_cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name) + if data_object_cache_key in self._data_object_cache: + logger.debug("Table existence cache hit: %s", data_object_cache_key) + return self._data_object_cache[data_object_cache_key] is not None sql = ( exp.select("1") diff --git a/sqlmesh/core/engine_adapter/mysql.py b/sqlmesh/core/engine_adapter/mysql.py index 26cc7c0197..31773d6c63 100644 --- a/sqlmesh/core/engine_adapter/mysql.py +++ b/sqlmesh/core/engine_adapter/mysql.py @@ -164,11 +164,11 @@ def _create_column_comments( exc_info=True, ) - def create_table_like( + def _create_table_like( self, target_table_name: TableName, source_table_name: TableName, - exists: bool = True, + exists: bool, **kwargs: t.Any, ) -> None: self.execute( diff --git a/sqlmesh/core/engine_adapter/postgres.py b/sqlmesh/core/engine_adapter/postgres.py index e9c212bd5f..79431ee360 100644 --- a/sqlmesh/core/engine_adapter/postgres.py +++ b/sqlmesh/core/engine_adapter/postgres.py @@ -34,7 +34,7 @@ class PostgresEngineAdapter( HAS_VIEW_BINDING = True CURRENT_CATALOG_EXPRESSION = exp.column("current_catalog") SUPPORTS_REPLACE_TABLE = False - MAX_IDENTIFIER_LENGTH = 63 + MAX_IDENTIFIER_LENGTH: t.Optional[int] = 63 SUPPORTS_QUERY_EXECUTION_TRACKING = True SCHEMA_DIFFER_KWARGS = { "parameterized_type_defaults": { @@ -79,11 +79,11 @@ def _fetch_native_df( self._connection_pool.commit() return df - def create_table_like( + def _create_table_like( self, target_table_name: TableName, source_table_name: TableName, - exists: bool = True, + exists: bool, **kwargs: t.Any, ) -> None: self.execute( diff --git a/sqlmesh/core/engine_adapter/spark.py b/sqlmesh/core/engine_adapter/spark.py index 18ba6ea106..b2d6a9cbb5 100644 --- a/sqlmesh/core/engine_adapter/spark.py +++ b/sqlmesh/core/engine_adapter/spark.py @@ -402,14 +402,16 @@ def get_current_database(self) -> str: return self.spark.catalog.currentDatabase() return self.fetchone(exp.select(exp.func("current_database")))[0] # type: ignore - def get_data_object(self, target_name: TableName) -> t.Optional[DataObject]: + def get_data_object( + self, target_name: TableName, safe_to_cache: bool = False + ) -> t.Optional[DataObject]: target_table = exp.to_table(target_name) if isinstance(target_table.this, exp.Dot) and target_table.this.expression.name.startswith( f"{self.BRANCH_PREFIX}{self.WAP_PREFIX}" ): # Exclude the branch name target_table.set("this", target_table.this.this) - return super().get_data_object(target_table) + return super().get_data_object(target_table, safe_to_cache=safe_to_cache) def create_state_table( self, diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 4ac87199c6..1483bdeece 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -307,6 +307,9 @@ def promote( ] self._create_schemas(gateway_table_pairs=gateway_table_pairs) + # Fetch the view data objects for the promoted snapshots to get them cached + self._get_virtual_data_objects(target_snapshots, environment_naming_info) + deployability_index = deployability_index or DeployabilityIndex.all_deployable() with self.concurrent_context(): concurrent_apply_to_snapshots( @@ -425,7 +428,9 @@ def get_snapshots_to_create( target_snapshots: Target snapshots. deployability_index: Determines snapshots that are deployable / representative in the context of this creation. """ - existing_data_objects = self._get_data_objects(target_snapshots, deployability_index) + existing_data_objects = self._get_physical_data_objects( + target_snapshots, deployability_index + ) snapshots_to_create = [] for snapshot in target_snapshots: if not snapshot.is_model or snapshot.is_symbolic: @@ -482,7 +487,7 @@ def migrate( deployability_index: Determines snapshots that are deployable in the context of this evaluation. """ deployability_index = deployability_index or DeployabilityIndex.all_deployable() - target_data_objects = self._get_data_objects(target_snapshots, deployability_index) + target_data_objects = self._get_physical_data_objects(target_snapshots, deployability_index) if not target_data_objects: return @@ -1472,7 +1477,7 @@ def _can_clone(self, snapshot: Snapshot, deployability_index: DeployabilityIndex and adapter.table_exists(snapshot.table_name()) ) - def _get_data_objects( + def _get_physical_data_objects( self, target_snapshots: t.Iterable[Snapshot], deployability_index: DeployabilityIndex, @@ -1488,6 +1493,59 @@ def _get_data_objects( A dictionary of snapshot IDs to existing data objects of their physical tables. If the data object for a snapshot is not found, it will not be included in the dictionary. """ + return self._get_data_objects( + target_snapshots, + lambda s: exp.to_table( + s.table_name(deployability_index.is_deployable(s)), dialect=s.model.dialect + ), + ) + + def _get_virtual_data_objects( + self, + target_snapshots: t.Iterable[Snapshot], + environment_naming_info: EnvironmentNamingInfo, + ) -> t.Dict[SnapshotId, DataObject]: + """Returns a dictionary of snapshot IDs to existing data objects of their virtual views. + + Args: + target_snapshots: Target snapshots. + environment_naming_info: The environment naming info of the target virtual environment. + + Returns: + A dictionary of snapshot IDs to existing data objects of their virtual views. If the data object + for a snapshot is not found, it will not be included in the dictionary. + """ + + def _get_view_name(s: Snapshot) -> exp.Table: + adapter = ( + self.get_adapter(s.model_gateway) + if environment_naming_info.gateway_managed + else self.adapter + ) + return exp.to_table( + s.qualified_view_name.for_environment( + environment_naming_info, dialect=adapter.dialect + ), + dialect=adapter.dialect, + ) + + return self._get_data_objects(target_snapshots, _get_view_name) + + def _get_data_objects( + self, + target_snapshots: t.Iterable[Snapshot], + table_name_callable: t.Callable[[Snapshot], exp.Table], + ) -> t.Dict[SnapshotId, DataObject]: + """Returns a dictionary of snapshot IDs to existing data objects. + + Args: + target_snapshots: Target snapshots. + table_name_callable: A function that takes a snapshot and returns the table to look for. + + Returns: + A dictionary of snapshot IDs to existing data objects. If the data object for a snapshot is not found, + it will not be included in the dictionary. + """ tables_by_gateway_and_schema: t.Dict[t.Union[str, None], t.Dict[exp.Table, set[str]]] = ( defaultdict(lambda: defaultdict(set)) ) @@ -1495,8 +1553,7 @@ def _get_data_objects( for snapshot in target_snapshots: if not snapshot.is_model or snapshot.is_symbolic: continue - is_deployable = deployability_index.is_deployable(snapshot) - table = exp.to_table(snapshot.table_name(is_deployable), dialect=snapshot.model.dialect) + table = table_name_callable(snapshot) table_schema = d.schema_(table.db, catalog=table.catalog) tables_by_gateway_and_schema[snapshot.model_gateway][table_schema].add(table.name) snapshots_by_table_name[table.name] = snapshot @@ -1507,7 +1564,9 @@ def _get_data_objects_in_schema( gateway: t.Optional[str] = None, ) -> t.List[DataObject]: logger.info("Listing data objects in schema %s", schema.sql()) - return self.get_adapter(gateway).get_data_objects(schema, object_names) + return self.get_adapter(gateway).get_data_objects( + schema, object_names, safe_to_cache=True + ) with self.concurrent_context(): existing_objects: t.List[DataObject] = [] diff --git a/tests/core/engine_adapter/test_athena.py b/tests/core/engine_adapter/test_athena.py index 4fe57baf34..66e84ae025 100644 --- a/tests/core/engine_adapter/test_athena.py +++ b/tests/core/engine_adapter/test_athena.py @@ -312,6 +312,7 @@ def test_replace_query(adapter: AthenaEngineAdapter, mocker: MockerFixture): ) mocker.patch.object(adapter, "_get_data_objects", return_value=[]) adapter.cursor.execute.reset_mock() + adapter._clear_data_object_cache() adapter.s3_warehouse_location = "s3://foo" adapter.replace_query( diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index 140fac43eb..ba775c0779 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -3695,3 +3695,373 @@ def test_casted_columns( assert [ x.sql() for x in EngineAdapter._casted_columns(columns_to_types, source_columns) ] == expected + + +def test_data_object_cache_get_data_objects( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + table1 = DataObject(catalog=None, schema="test_schema", name="table1", type="table") + table2 = DataObject(catalog=None, schema="test_schema", name="table2", type="table") + + mock_get_data_objects = mocker.patch.object( + adapter, "_get_data_objects", return_value=[table1, table2] + ) + + result1 = adapter.get_data_objects("test_schema", {"table1", "table2"}, safe_to_cache=True) + assert len(result1) == 2 + assert mock_get_data_objects.call_count == 1 + + result2 = adapter.get_data_objects("test_schema", {"table1", "table2"}, safe_to_cache=True) + assert len(result2) == 2 + assert mock_get_data_objects.call_count == 1 # Should not increase + + result3 = adapter.get_data_objects("test_schema", {"table1"}) + assert len(result3) == 1 + assert result3[0].name == "table1" + assert mock_get_data_objects.call_count == 1 # Should not increase + + +def test_data_object_cache_get_data_objects_bypasses_cache( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + table1 = DataObject(catalog=None, schema="test_schema", name="table1", type="table") + table2 = DataObject(catalog=None, schema="test_schema", name="table2", type="table") + + mock_get_data_objects = mocker.patch.object( + adapter, "_get_data_objects", return_value=[table1, table2] + ) + + assert adapter.get_data_objects("test_schema") + assert adapter.get_data_objects("test_schema", {"table1", "table2"}) + assert adapter.get_data_objects("test_schema", {"table1", "table2"}) + assert adapter.get_data_objects("test_schema", {"table1"}) + assert adapter.get_data_object("test_schema.table1") is not None + + mock_get_data_objects.return_value = [] + assert not adapter.get_data_objects("test_schema") + assert not adapter.get_data_objects("test_schema", {"missing"}) + assert not adapter.get_data_objects("test_schema", {"missing"}) + assert adapter.get_data_object("test_schema.missing") is None + + # None of the calls should've been cached + assert mock_get_data_objects.call_count == 9 + assert not adapter._data_object_cache + + +def test_data_object_cache_get_data_objects_no_object_names( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + table1 = DataObject(catalog=None, schema="test_schema", name="table1", type="table") + table2 = DataObject(catalog=None, schema="test_schema", name="table2", type="table") + + mock_get_data_objects = mocker.patch.object( + adapter, "_get_data_objects", return_value=[table1, table2] + ) + + result1 = adapter.get_data_objects("test_schema", safe_to_cache=True) + assert len(result1) == 2 + assert mock_get_data_objects.call_count == 1 + + result2 = adapter.get_data_objects("test_schema", {"table1", "table2"}, safe_to_cache=True) + assert len(result2) == 2 + assert mock_get_data_objects.call_count == 1 # Should not increase + + +def test_data_object_cache_get_data_object( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + table = DataObject(catalog=None, schema="test_schema", name="test_table", type="table") + + mock_get_data_objects = mocker.patch.object(adapter, "_get_data_objects", return_value=[table]) + + result1 = adapter.get_data_object("test_schema.test_table", safe_to_cache=True) + assert result1 is not None + assert result1.name == "test_table" + assert mock_get_data_objects.call_count == 1 + + result2 = adapter.get_data_object("test_schema.test_table", safe_to_cache=True) + assert result2 is not None + assert result2.name == "test_table" + assert mock_get_data_objects.call_count == 1 # Should not increase + + +def test_data_object_cache_cleared_on_drop_table( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + table = DataObject(catalog=None, schema="test_schema", name="test_table", type="table") + + mock_get_data_objects = mocker.patch.object(adapter, "_get_data_objects", return_value=[table]) + + adapter.get_data_object("test_schema.test_table", safe_to_cache=True) + assert mock_get_data_objects.call_count == 1 + + adapter.drop_table("test_schema.test_table") + + mock_get_data_objects.return_value = [] + result = adapter.get_data_object("test_schema.test_table", safe_to_cache=True) + assert result is None + assert mock_get_data_objects.call_count == 2 + + +def test_data_object_cache_cleared_on_drop_view( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + view = DataObject(catalog=None, schema="test_schema", name="test_view", type="view") + + mock_get_data_objects = mocker.patch.object(adapter, "_get_data_objects", return_value=[view]) + + adapter.get_data_object("test_schema.test_view", safe_to_cache=True) + assert mock_get_data_objects.call_count == 1 + + adapter.drop_view("test_schema.test_view") + + mock_get_data_objects.return_value = [] + result = adapter.get_data_object("test_schema.test_view", safe_to_cache=True) + assert result is None + assert mock_get_data_objects.call_count == 2 + + +def test_data_object_cache_cleared_on_drop_data_object( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + table = DataObject(catalog=None, schema="test_schema", name="test_table", type="table") + + mock_get_data_objects = mocker.patch.object(adapter, "_get_data_objects", return_value=[table]) + + adapter.get_data_object("test_schema.test_table", safe_to_cache=True) + assert mock_get_data_objects.call_count == 1 + + adapter.drop_data_object(table) + + mock_get_data_objects.return_value = [] + result = adapter.get_data_object("test_schema.test_table", safe_to_cache=True) + assert result is None + assert mock_get_data_objects.call_count == 2 + + +def test_data_object_cache_cleared_on_create_table( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + from sqlglot import exp + + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + # Initially cache that table doesn't exist + mock_get_data_objects = mocker.patch.object(adapter, "_get_data_objects", return_value=[]) + result = adapter.get_data_object("test_schema.test_table", safe_to_cache=True) + assert result is None + assert mock_get_data_objects.call_count == 1 + + # Create the table + table = DataObject(catalog=None, schema="test_schema", name="test_table", type="table") + mock_get_data_objects.return_value = [table] + adapter.create_table( + "test_schema.test_table", + {"col1": exp.DataType.build("INT")}, + ) + + # Cache should be cleared, so next get_data_object should call _get_data_objects again + result = adapter.get_data_object("test_schema.test_table", safe_to_cache=True) + assert result is not None + assert mock_get_data_objects.call_count == 2 + + +def test_data_object_cache_cleared_on_create_view( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + from sqlglot import parse_one + + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + # Initially cache that view doesn't exist + mock_get_data_objects = mocker.patch.object(adapter, "_get_data_objects", return_value=[]) + result = adapter.get_data_object("test_schema.test_view", safe_to_cache=True) + assert result is None + assert mock_get_data_objects.call_count == 1 + + # Create the view + view = DataObject(catalog=None, schema="test_schema", name="test_view", type="view") + mock_get_data_objects.return_value = [view] + adapter.create_view( + "test_schema.test_view", + parse_one("SELECT 1 AS col1"), + ) + + # Cache should be cleared, so next get_data_object should call _get_data_objects again + result = adapter.get_data_object("test_schema.test_view", safe_to_cache=True) + assert result is not None + assert mock_get_data_objects.call_count == 2 + + +def test_data_object_cache_cleared_on_clone_table( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + from sqlmesh.core.engine_adapter.snowflake import SnowflakeEngineAdapter + + adapter = make_mocked_engine_adapter( + SnowflakeEngineAdapter, patch_get_data_objects=False, default_catalog="test_catalog" + ) + + # Initially cache that target table doesn't exist + mock_get_data_objects = mocker.patch.object(adapter, "_get_data_objects", return_value=[]) + result = adapter.get_data_object("test_schema.test_target", safe_to_cache=True) + assert result is None + assert mock_get_data_objects.call_count == 1 + + # Clone the table + target_table = DataObject( + catalog="test_catalog", schema="test_schema", name="test_target", type="table" + ) + mock_get_data_objects.return_value = [target_table] + adapter.clone_table("test_schema.test_target", "test_schema.test_source") + + # Cache should be cleared, so next get_data_object should call _get_data_objects again + result = adapter.get_data_object("test_schema.test_target", safe_to_cache=True) + assert result is not None + assert mock_get_data_objects.call_count == 2 + + +def test_data_object_cache_with_catalog( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + from sqlmesh.core.engine_adapter.snowflake import SnowflakeEngineAdapter + + adapter = make_mocked_engine_adapter( + SnowflakeEngineAdapter, patch_get_data_objects=False, default_catalog="test_catalog" + ) + + table = DataObject( + catalog="test_catalog", schema="test_schema", name="test_table", type="table" + ) + + mock_get_data_objects = mocker.patch.object(adapter, "_get_data_objects", return_value=[table]) + + result1 = adapter.get_data_object("test_catalog.test_schema.test_table", safe_to_cache=True) + assert result1 is not None + assert result1.catalog == "test_catalog" + assert mock_get_data_objects.call_count == 1 + + result2 = adapter.get_data_object("test_catalog.test_schema.test_table", safe_to_cache=True) + assert result2 is not None + assert result2.catalog == "test_catalog" + assert mock_get_data_objects.call_count == 1 # Should not increase + + +def test_data_object_cache_partial_cache_hit( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + table1 = DataObject(catalog=None, schema="test_schema", name="table1", type="table") + table2 = DataObject(catalog=None, schema="test_schema", name="table2", type="table") + table3 = DataObject(catalog=None, schema="test_schema", name="table3", type="table") + + mock_get_data_objects = mocker.patch.object( + adapter, "_get_data_objects", return_value=[table1, table2] + ) + + adapter.get_data_objects("test_schema", {"table1", "table2"}, safe_to_cache=True) + assert mock_get_data_objects.call_count == 1 + + mock_get_data_objects.return_value = [table3] + result = adapter.get_data_objects("test_schema", {"table1", "table3"}, safe_to_cache=True) + + assert len(result) == 2 + assert {obj.name for obj in result} == {"table1", "table3"} + assert mock_get_data_objects.call_count == 2 # Called again for table3 + + +def test_data_object_cache_get_data_objects_missing_objects( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + table1 = DataObject(catalog=None, schema="test_schema", name="table1", type="table") + table2 = DataObject(catalog=None, schema="test_schema", name="table2", type="table") + + mock_get_data_objects = mocker.patch.object(adapter, "_get_data_objects", return_value=[]) + + result1 = adapter.get_data_objects("test_schema", {"table1", "table2"}, safe_to_cache=True) + assert not result1 + assert mock_get_data_objects.call_count == 1 + + result2 = adapter.get_data_objects("test_schema", {"table1", "table2"}, safe_to_cache=True) + assert not result2 + assert mock_get_data_objects.call_count == 1 # Should not increase + + result3 = adapter.get_data_objects("test_schema", {"table1"}, safe_to_cache=True) + assert not result3 + assert mock_get_data_objects.call_count == 1 # Should not increase + + +def test_data_object_cache_cleared_on_rename_table( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + old_table = DataObject(catalog=None, schema="test_schema", name="old_table", type="table") + mock_get_data_objects = mocker.patch.object( + adapter, "_get_data_objects", return_value=[old_table] + ) + + result = adapter.get_data_object("test_schema.old_table", safe_to_cache=True) + assert result is not None + assert result.name == "old_table" + assert mock_get_data_objects.call_count == 1 + + new_table = DataObject(catalog=None, schema="test_schema", name="new_table", type="table") + mock_get_data_objects.return_value = [new_table] + adapter.rename_table("test_schema.old_table", "test_schema.new_table") + + mock_get_data_objects.return_value = [] + result = adapter.get_data_object("test_schema.old_table", safe_to_cache=True) + assert result is None + assert mock_get_data_objects.call_count == 2 + + mock_get_data_objects.return_value = [new_table] + result = adapter.get_data_object("test_schema.new_table", safe_to_cache=True) + assert result is not None + assert result.name == "new_table" + assert mock_get_data_objects.call_count == 3 + + +def test_data_object_cache_cleared_on_create_table_like( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + from sqlglot import exp + + adapter = make_mocked_engine_adapter(EngineAdapter, patch_get_data_objects=False) + + columns_to_types = { + "col1": exp.DataType.build("INT"), + "col2": exp.DataType.build("TEXT"), + } + mocker.patch.object(adapter, "columns", return_value=columns_to_types) + + mock_get_data_objects = mocker.patch.object(adapter, "_get_data_objects", return_value=[]) + result = adapter.get_data_object("test_schema.target_table", safe_to_cache=True) + assert result is None + assert mock_get_data_objects.call_count == 1 + + target_table = DataObject(catalog=None, schema="test_schema", name="target_table", type="table") + mock_get_data_objects.return_value = [target_table] + adapter.create_table_like("test_schema.target_table", "test_schema.source_table") + + result = adapter.get_data_object("test_schema.target_table", safe_to_cache=True) + assert result is not None + assert result.name == "target_table" + assert mock_get_data_objects.call_count == 2 diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index 62c4a4f3eb..ce4d3a886c 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -358,12 +358,12 @@ def test_create_managed_table(make_mocked_engine_adapter: t.Callable, mocker: Mo def test_drop_managed_table(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter) - adapter.drop_managed_table(table_name=exp.parse_identifier("foo"), exists=False) - adapter.drop_managed_table(table_name=exp.parse_identifier("foo"), exists=True) + adapter.drop_managed_table(table_name="foo.bar", exists=False) + adapter.drop_managed_table(table_name="foo.bar", exists=True) assert to_sql_calls(adapter) == [ - 'DROP DYNAMIC TABLE "foo"', - 'DROP DYNAMIC TABLE IF EXISTS "foo"', + 'DROP DYNAMIC TABLE "foo"."bar"', + 'DROP DYNAMIC TABLE IF EXISTS "foo"."bar"', ] diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 2df91afb10..19685e81c3 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -888,6 +888,7 @@ def test_create_prod_table_exists(mocker: MockerFixture, adapter_mock, make_snap { f"test_schema__test_model__{snapshot.version}", }, + safe_to_cache=True, ) @@ -974,6 +975,7 @@ def test_create_only_dev_table_exists(mocker: MockerFixture, adapter_mock, make_ { f"test_schema__test_model__{snapshot.version}__dev", }, + safe_to_cache=True, ) @@ -1023,6 +1025,7 @@ def test_create_new_forward_only_model(mocker: MockerFixture, adapter_mock, make { f"test_schema__test_model__{snapshot.dev_version}__dev", }, + safe_to_cache=True, ) @@ -1113,6 +1116,7 @@ def test_create_tables_exist( adapter_mock.get_data_objects.assert_called_once_with( schema_("sqlmesh__db"), {table_name}, + safe_to_cache=True, ) adapter_mock.create_schema.assert_not_called() adapter_mock.create_table.assert_not_called() @@ -1150,6 +1154,7 @@ def test_create_prod_table_exists_forward_only(mocker: MockerFixture, adapter_mo { f"test_schema__test_model__{snapshot.version}", }, + safe_to_cache=True, ) adapter_mock.create_table.assert_not_called() @@ -1341,9 +1346,11 @@ def test_promote_deployable(mocker: MockerFixture, make_snapshot): { f"test_schema__test_model__{snapshot.version}", }, + safe_to_cache=True, ) adapter_mock.create_table.assert_not_called() + adapter_mock.get_data_objects.return_value = [] evaluator.promote([snapshot], EnvironmentNamingInfo(name="test_env")) adapter_mock.create_schema.assert_called_once_with(to_schema("test_schema__test_env")) @@ -4188,6 +4195,7 @@ def test_multiple_engine_promotion(mocker: MockerFixture, adapter_mock, make_sna connection_mock.cursor.return_value = cursor_mock adapter = EngineAdapter(lambda: connection_mock, "") adapter.with_settings = lambda **kwargs: adapter # type: ignore + adapter._get_data_objects = lambda *args, **kwargs: [] # type: ignore engine_adapters = {"default": adapter_mock, "secondary": adapter} def columns(table_name): diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 9a9ce8f906..a33e3ed843 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -122,10 +122,10 @@ def test_dbt_custom_materialization(): selected_model = list(plan.selected_models)[0] assert selected_model == "model.sushi.custom_incremental_model" - qoery = "SELECT * FROM sushi.custom_incremental_model ORDER BY created_at" + query = "SELECT * FROM sushi.custom_incremental_model ORDER BY created_at" hook_table = "SELECT * FROM hook_table ORDER BY id" sushi_context.apply(plan) - result = sushi_context.engine_adapter.fetchdf(qoery) + result = sushi_context.engine_adapter.fetchdf(query) assert len(result) == 1 assert {"created_at", "id"}.issubset(result.columns) @@ -140,7 +140,7 @@ def test_dbt_custom_materialization(): tomorrow = datetime.now() + timedelta(days=1) sushi_context.run(select_models=["sushi.custom_incremental_model"], execution_time=tomorrow) - result_after_run = sushi_context.engine_adapter.fetchdf(qoery) + result_after_run = sushi_context.engine_adapter.fetchdf(query) assert {"created_at", "id"}.issubset(result_after_run.columns) # this should have added new unique values for the new row From 858f4320549fed120699e3abb0bd21d16f9a3c1d Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Thu, 2 Oct 2025 12:59:28 -0700 Subject: [PATCH 0931/1056] feat(web_common):add option to toggle node dragging in lineage (#5473) --- .../src/components/Lineage/LineageLayout.tsx | 60 ++++++++++++++++-- .../Lineage/stories/Lineage.stories.tsx | 63 ++++++++++++++----- .../Lineage/stories/ModelLineage.tsx | 11 +++- 3 files changed, 111 insertions(+), 23 deletions(-) diff --git a/web/common/src/components/Lineage/LineageLayout.tsx b/web/common/src/components/Lineage/LineageLayout.tsx index 411ace4e65..4b2e06b0b3 100644 --- a/web/common/src/components/Lineage/LineageLayout.tsx +++ b/web/common/src/components/Lineage/LineageLayout.tsx @@ -2,7 +2,9 @@ import { Background, BackgroundVariant, Controls, + type EdgeChange, type EdgeTypes, + type NodeChange, type NodeTypes, ReactFlow, ReactFlowProvider, @@ -12,6 +14,8 @@ import { getOutgoers, useReactFlow, useViewport, + applyNodeChanges, + applyEdgeChanges, } from '@xyflow/react' import '@xyflow/react/dist/style.css' @@ -55,6 +59,8 @@ export function LineageLayout< edgeTypes, className, controls, + nodesDraggable, + nodesConnectable, useLineage, onNodeClick, onNodeDoubleClick, @@ -69,6 +75,8 @@ export function LineageLayout< nodeTypes?: NodeTypes edgeTypes?: EdgeTypes className?: string + nodesDraggable?: boolean + nodesConnectable?: boolean controls?: | React.ReactNode | (({ setCenter }: { setCenter: SetCenter }) => React.ReactNode) @@ -86,6 +94,8 @@ export function LineageLayout< + nodesDraggable?: boolean + nodesConnectable?: boolean nodeTypes?: NodeTypes edgeTypes?: EdgeTypes className?: string @@ -140,8 +154,8 @@ function LineageLayoutBase< isBuildingLayout, currentNode, zoom, - nodes, - edges, + nodes: initialNodes, + edges: initialEdges, nodesMap, showOnlySelectedNodes, selectedNodeId, @@ -152,6 +166,32 @@ function LineageLayoutBase< setSelectedEdges, } = useLineage() + const [nodes, setNodes] = React.useState(initialNodes) + const [edges, setEdges] = React.useState(initialEdges) + + const onNodesChange = React.useCallback( + (changes: NodeChange>[]) => { + setNodes( + applyNodeChanges>(changes, nodes), + ) + }, + [nodes, setNodes], + ) + + const onEdgesChange = React.useCallback( + ( + changes: EdgeChange>[], + ) => { + setEdges( + applyEdgeChanges>( + changes, + edges, + ), + ) + }, + [edges, setEdges], + ) + const updateZoom = React.useMemo(() => debounce(setZoom, 200), [setZoom]) const zoomToCurrentNode = React.useCallback( @@ -221,6 +261,14 @@ function LineageLayoutBase< [nodes, edges], ) + React.useEffect(() => { + setNodes(initialNodes) + }, [initialNodes]) + + React.useEffect(() => { + setEdges(initialEdges) + }, [initialEdges]) + React.useEffect(() => { if (selectedNodeId == null) { setShowOnlySelectedNodes(false) @@ -290,8 +338,6 @@ function LineageLayoutBase< React.useEffect(() => { if (currentNode?.id) { setSelectedNodeId(currentNode.id) - } else if (selectedNodeId) { - // setSelectedNodeId(selectedNodeId); } else { const node = nodes.length > 0 ? nodes[nodes.length - 1] : null @@ -332,8 +378,10 @@ function LineageLayoutBase< edges={edges} nodeTypes={nodeTypes} edgeTypes={edgeTypes} - nodesDraggable={false} - nodesConnectable={false} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + nodesDraggable={nodesDraggable} + nodesConnectable={nodesConnectable} zoomOnDoubleClick={false} panOnScroll={true} zoomOnScroll={true} diff --git a/web/common/src/components/Lineage/stories/Lineage.stories.tsx b/web/common/src/components/Lineage/stories/Lineage.stories.tsx index 4ad8ca9f8b..6e16bed61e 100644 --- a/web/common/src/components/Lineage/stories/Lineage.stories.tsx +++ b/web/common/src/components/Lineage/stories/Lineage.stories.tsx @@ -17,33 +17,56 @@ export const LineageModel = () => { > { const [zoom, setZoom] = React.useState(ZOOM_THRESHOLD) const [isBuildingLayout, setIsBuildingLayout] = React.useState(false) + const [nodesDraggable, setNodesDraggable] = React.useState(false) const [edges, setEdges] = React.useState< LineageEdge[] >([]) @@ -388,6 +389,7 @@ export const ModelLineage = ({ nodeTypes={nodeTypes} edgeTypes={edgeTypes} className={className} + nodesDraggable={nodesDraggable} controls={ <> + setNodesDraggable(prev => !prev)} + disabled={isBuildingLayout} + > + + } /> From cbab32cc3a4bc54c0a4fb3f9e17853a42d54e5c7 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:20:32 +0300 Subject: [PATCH 0932/1056] Chore: make `.sqlmesh` location configurable (#5474) --- docs/guides/configuration.md | 3 +++ sqlmesh/core/constants.py | 2 +- tests/core/engine_adapter/integration/conftest.py | 3 ++- tests/core/engine_adapter/test_trino.py | 6 +++--- tests/dbt/test_adapter.py | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index d2e294a589..d6d4f20c11 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -21,6 +21,9 @@ The sources have the following order of precedence: 2. `config.yaml` or `config.py` in the `~/.sqlmesh` folder. 3. `config.yaml` or `config.py` in a project folder. [LOWEST PRECEDENCE] +!!! note + To relocate the `.sqlmesh` folder, set the `SQLMESH_HOME` environment variable to your preferred directory path. + ### File type You can specify a SQLMesh configuration in either YAML or Python. diff --git a/sqlmesh/core/constants.py b/sqlmesh/core/constants.py index a1d117f4fb..66dadb0b5d 100644 --- a/sqlmesh/core/constants.py +++ b/sqlmesh/core/constants.py @@ -8,7 +8,7 @@ SQLMESH = "sqlmesh" SQLMESH_MANAGED = "sqlmesh_managed" -SQLMESH_PATH = Path.home() / ".sqlmesh" +SQLMESH_PATH = Path(os.getenv("SQLMESH_HOME") or Path.home() / ".sqlmesh") PROD = "prod" """Prod""" diff --git a/tests/core/engine_adapter/integration/conftest.py b/tests/core/engine_adapter/integration/conftest.py index 30f934da63..308819b671 100644 --- a/tests/core/engine_adapter/integration/conftest.py +++ b/tests/core/engine_adapter/integration/conftest.py @@ -9,6 +9,7 @@ from sqlmesh import Config, EngineAdapter +from sqlmesh.core.constants import SQLMESH_PATH from sqlmesh.core.config.connection import ( ConnectionConfig, AthenaConnectionConfig, @@ -34,7 +35,7 @@ def config(tmp_path: pathlib.Path) -> Config: project_paths=[ pathlib.Path(os.path.join(os.path.dirname(__file__), "config.yaml")), ], - personal_paths=[pathlib.Path("~/.sqlmesh/config.yaml").expanduser()], + personal_paths=[(SQLMESH_PATH / "config.yaml").expanduser()], variables={"tmp_path": str(tmp_path)}, ) diff --git a/tests/core/engine_adapter/test_trino.py b/tests/core/engine_adapter/test_trino.py index 526cb05b04..bf925c875a 100644 --- a/tests/core/engine_adapter/test_trino.py +++ b/tests/core/engine_adapter/test_trino.py @@ -669,7 +669,7 @@ def test_replace_table_catalog_support( adapter.replace_query( table_name=".".join([catalog_name, "schema", "test_table"]), - query_or_df=parse_one("SELECT 1 AS col"), + query_or_df=t.cast(exp.Query, parse_one("SELECT 1 AS col")), ) sql_calls = to_sql_calls(adapter) @@ -705,7 +705,7 @@ def test_insert_overwrite_time_partition_hive( adapter.insert_overwrite_by_time_partition( table_name=".".join(["my_catalog", "schema", "test_table"]), - query_or_df=parse_one("SELECT a, b FROM tbl"), + query_or_df=t.cast(exp.Query, parse_one("SELECT a, b FROM tbl")), start="2022-01-01", end="2022-01-02", time_column="b", @@ -743,7 +743,7 @@ def test_insert_overwrite_time_partition_iceberg( adapter.insert_overwrite_by_time_partition( table_name=".".join(["my_catalog", "schema", "test_table"]), - query_or_df=parse_one("SELECT a, b FROM tbl"), + query_or_df=t.cast(exp.Query, parse_one("SELECT a, b FROM tbl")), start="2022-01-01", end="2022-01-02", time_column="b", diff --git a/tests/dbt/test_adapter.py b/tests/dbt/test_adapter.py index 381401ce73..5570212668 100644 --- a/tests/dbt/test_adapter.py +++ b/tests/dbt/test_adapter.py @@ -39,7 +39,7 @@ def test_adapter_relation(sushi_test_project: Project, runtime_renderer: t.Calla table_name="foo.another", target_columns_to_types={"col": exp.DataType.build("int")} ) engine_adapter.create_view( - view_name="foo.bar_view", query_or_df=parse_one("select * from foo.bar") + view_name="foo.bar_view", query_or_df=t.cast(exp.Query, parse_one("select * from foo.bar")) ) engine_adapter.create_table( table_name="ignored.ignore", target_columns_to_types={"col": exp.DataType.build("int")} From 26bba970d6d12f9ebf2c7802ad2693bdf8700b52 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Thu, 2 Oct 2025 16:42:43 -0700 Subject: [PATCH 0933/1056] fix(web_common): adjust types in lineage component (#5476) --- web/common/src/components/Lineage/help.ts | 4 +++- web/common/src/components/Lineage/index.ts | 1 + .../components/Lineage/stories/ModelNode.tsx | 4 ++-- .../Lineage/stories/dagreLayout.worker.ts | 24 +++++++++++++++---- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/web/common/src/components/Lineage/help.ts b/web/common/src/components/Lineage/help.ts index a052ff707b..1e5d5a9d6b 100644 --- a/web/common/src/components/Lineage/help.ts +++ b/web/common/src/components/Lineage/help.ts @@ -23,7 +23,9 @@ export function getOnlySelectedNodes< TNodeData extends LineageNodeData = LineageNodeData, TNodeID extends string = NodeId, >(nodeMaps: LineageNodesMap, selectedNodes: Set) { - return (Object.values(nodeMaps) as LineageNode[]).reduce( + return ( + Object.values(nodeMaps) satisfies LineageNode[] + ).reduce( (acc, node) => selectedNodes.has(node.id) ? { ...acc, [node.id]: node } : acc, {} as LineageNodesMap, diff --git a/web/common/src/components/Lineage/index.ts b/web/common/src/components/Lineage/index.ts index 0fbc17047c..4a0b6eccc7 100644 --- a/web/common/src/components/Lineage/index.ts +++ b/web/common/src/components/Lineage/index.ts @@ -21,6 +21,7 @@ export * from './node/useNodeMetadata' export * from './edge/EdgeWithGradient' export * from './edge/FactoryEdgeWithGradient' export * from './layout/dagreLayout' +export * from './layout/help' export * from './LineageColumnLevel/ColumnLevelLineageContext' export * from './LineageColumnLevel/FactoryColumn' export * from './LineageColumnLevel/useColumns' diff --git a/web/common/src/components/Lineage/stories/ModelNode.tsx b/web/common/src/components/Lineage/stories/ModelNode.tsx index 2f4705f1c1..b0bd2f7867 100644 --- a/web/common/src/components/Lineage/stories/ModelNode.tsx +++ b/web/common/src/components/Lineage/stories/ModelNode.tsx @@ -254,7 +254,7 @@ export const ModelNode = React.memo(function ModelNode({ className="p-1 first:border-t-0 h-6" columnLineageData={ ( - column as Column & { + column satisfies Column & { columnLineageData?: ColumnLevelLineageAdjacencyList< ModelNameType, ColumnName @@ -287,7 +287,7 @@ export const ModelNode = React.memo(function ModelNode({ className="p-1 border-t border-lineage-divider first:border-t-0 h-6" columnLineageData={ ( - column as Column & { + column satisfies Column & { columnLineageData?: ColumnLevelLineageAdjacencyList< ModelNameType, ColumnName diff --git a/web/common/src/components/Lineage/stories/dagreLayout.worker.ts b/web/common/src/components/Lineage/stories/dagreLayout.worker.ts index 1a6a9d3fe7..ce452f4808 100644 --- a/web/common/src/components/Lineage/stories/dagreLayout.worker.ts +++ b/web/common/src/components/Lineage/stories/dagreLayout.worker.ts @@ -2,23 +2,37 @@ import { type LayoutedGraph, type LineageEdgeData, type LineageNodeData, + type EdgeId, + type NodeId, + type PortId, } from '../utils' import { buildLayout } from '../layout/dagreLayout' self.onmessage = < TNodeData extends LineageNodeData = LineageNodeData, TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, >( - event: MessageEvent>, + event: MessageEvent< + LayoutedGraph + >, ) => { try { - const { edges, nodesMap } = buildLayout(event.data) + const { edges, nodesMap } = buildLayout< + TNodeData, + TEdgeData, + TNodeID, + TEdgeID, + TPortID + >(event.data) self.postMessage({ edges, nodesMap, - } as LayoutedGraph) - } catch (outerError) { - self.postMessage({ error: outerError } as { error: ErrorEvent }) + } satisfies LayoutedGraph) + } catch (error) { + self.postMessage({ error }) } } From 9ed67444bb4e7a662636fc9d0362c1b8cae7be7f Mon Sep 17 00:00:00 2001 From: David Dai Date: Thu, 2 Oct 2025 21:22:55 -0700 Subject: [PATCH 0934/1056] feat(experimental): add official support for model grants (#5275) Co-authored-by: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> --- .circleci/continue_config.yml | 2 +- .gitignore | 7 + sqlmesh/core/_typing.py | 1 + sqlmesh/core/engine_adapter/_typing.py | 2 + sqlmesh/core/engine_adapter/base.py | 147 +++ sqlmesh/core/engine_adapter/base_postgres.py | 8 + sqlmesh/core/engine_adapter/bigquery.py | 112 ++- sqlmesh/core/engine_adapter/databricks.py | 28 +- sqlmesh/core/engine_adapter/mixins.py | 143 ++- sqlmesh/core/engine_adapter/postgres.py | 6 + sqlmesh/core/engine_adapter/redshift.py | 4 + sqlmesh/core/engine_adapter/risingwave.py | 1 + sqlmesh/core/engine_adapter/snowflake.py | 59 +- sqlmesh/core/engine_adapter/spark.py | 4 +- sqlmesh/core/model/common.py | 1 + sqlmesh/core/model/definition.py | 29 + sqlmesh/core/model/kind.py | 5 + sqlmesh/core/model/meta.py | 101 ++ sqlmesh/core/snapshot/evaluator.py | 200 +++- sqlmesh/dbt/basemodel.py | 6 +- sqlmesh/dbt/model.py | 6 + ...0100_add_grants_and_grants_target_layer.py | 9 + .../engine_adapter/integration/__init__.py | 102 ++ .../integration/test_integration.py | 206 ++++ .../integration/test_integration_postgres.py | 938 ++++++++++++++++++ tests/core/engine_adapter/test_base.py | 105 ++ .../core/engine_adapter/test_base_postgres.py | 24 + tests/core/engine_adapter/test_bigquery.py | 181 +++- tests/core/engine_adapter/test_databricks.py | 181 +++- tests/core/engine_adapter/test_postgres.py | 105 ++ tests/core/engine_adapter/test_redshift.py | 150 ++- tests/core/engine_adapter/test_snowflake.py | 199 ++++ tests/core/engine_adapter/test_spark.py | 4 +- tests/core/test_context.py | 59 +- tests/core/test_model.py | 346 ++++++- tests/core/test_snapshot.py | 73 +- tests/core/test_snapshot_evaluator.py | 539 +++++++++- tests/dbt/test_model.py | 177 +++- 38 files changed, 4229 insertions(+), 41 deletions(-) create mode 100644 sqlmesh/migrations/v0100_add_grants_and_grants_target_layer.py diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index c549c0ae78..c4b7bcbd53 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -148,7 +148,7 @@ jobs: command: ./.circleci/test_migration.sh sushi "--gateway duckdb_persistent" - run: name: Run the migration test - sushi_dbt - command: ./.circleci/test_migration.sh sushi_dbt "--config migration_test_config" + command: ./.circleci/test_migration.sh sushi_dbt "--config migration_test_config" ui_style: docker: diff --git a/.gitignore b/.gitignore index 72b41b5ce1..16593984dd 100644 --- a/.gitignore +++ b/.gitignore @@ -138,6 +138,12 @@ dmypy.json *~ *# +# Vim +*.swp +*.swo +.null-ls* + + *.duckdb *.duckdb.wal @@ -158,3 +164,4 @@ spark-warehouse/ # claude .claude/ + diff --git a/sqlmesh/core/_typing.py b/sqlmesh/core/_typing.py index e495df169e..8e28312c1a 100644 --- a/sqlmesh/core/_typing.py +++ b/sqlmesh/core/_typing.py @@ -11,6 +11,7 @@ SessionProperties = t.Dict[str, t.Union[exp.Expression, str, int, float, bool]] CustomMaterializationProperties = t.Dict[str, t.Union[exp.Expression, str, int, float, bool]] + if sys.version_info >= (3, 11): from typing import Self as Self else: diff --git a/sqlmesh/core/engine_adapter/_typing.py b/sqlmesh/core/engine_adapter/_typing.py index 98821bb2d4..77bcf2c015 100644 --- a/sqlmesh/core/engine_adapter/_typing.py +++ b/sqlmesh/core/engine_adapter/_typing.py @@ -30,3 +30,5 @@ ] QueryOrDF = t.Union[Query, DF] + GrantsConfig = t.Dict[str, t.List[str]] + DCL = t.TypeVar("DCL", exp.Grant, exp.Revoke) diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py index d9cc4f44a2..ebbf136cd1 100644 --- a/sqlmesh/core/engine_adapter/base.py +++ b/sqlmesh/core/engine_adapter/base.py @@ -63,6 +63,7 @@ from sqlmesh.core.engine_adapter._typing import ( DF, BigframeSession, + GrantsConfig, PySparkDataFrame, PySparkSession, Query, @@ -114,6 +115,7 @@ class EngineAdapter: SUPPORTS_TUPLE_IN = True HAS_VIEW_BINDING = False SUPPORTS_REPLACE_TABLE = True + SUPPORTS_GRANTS = False DEFAULT_CATALOG_TYPE = DIALECT QUOTE_IDENTIFIERS_IN_VIEWS = True MAX_IDENTIFIER_LENGTH: t.Optional[int] = None @@ -2478,6 +2480,33 @@ def wap_publish(self, table_name: TableName, wap_id: str) -> None: """ raise NotImplementedError(f"Engine does not support WAP: {type(self)}") + def sync_grants_config( + self, + table: exp.Table, + grants_config: GrantsConfig, + table_type: DataObjectType = DataObjectType.TABLE, + ) -> None: + """Applies the grants_config to a table authoritatively. + It first compares the specified grants against the current grants, and then + applies the diffs to the table by revoking and granting privileges as needed. + + Args: + table: The table/view to apply grants to. + grants_config: Dictionary mapping privileges to lists of grantees. + table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW). + """ + if not self.SUPPORTS_GRANTS: + raise NotImplementedError(f"Engine does not support grants: {type(self)}") + + current_grants = self._get_current_grants_config(table) + new_grants, revoked_grants = self._diff_grants_configs(grants_config, current_grants) + revoke_exprs = self._revoke_grants_config_expr(table, revoked_grants, table_type) + grant_exprs = self._apply_grants_config_expr(table, new_grants, table_type) + dcl_exprs = revoke_exprs + grant_exprs + + if dcl_exprs: + self.execute(dcl_exprs) + @contextlib.contextmanager def transaction( self, @@ -3029,6 +3058,124 @@ def _check_identifier_length(self, expression: exp.Expression) -> None: def get_table_last_modified_ts(self, table_names: t.List[TableName]) -> t.List[int]: raise NotImplementedError() + @classmethod + def _diff_grants_configs( + cls, new_config: GrantsConfig, old_config: GrantsConfig + ) -> t.Tuple[GrantsConfig, GrantsConfig]: + """Compute additions and removals between two grants configurations. + + This method compares new (desired) and old (current) GrantsConfigs case-insensitively + for both privilege keys and grantees, while preserving original casing + in the output GrantsConfigs. + + Args: + new_config: Desired grants configuration (specified by the user). + old_config: Current grants configuration (returned by the database). + + Returns: + A tuple of (additions, removals) GrantsConfig where: + - additions contains privileges/grantees present in new_config but not in old_config + - additions uses keys and grantee strings from new_config (user-specified casing) + - removals contains privileges/grantees present in old_config but not in new_config + - removals uses keys and grantee strings from old_config (database-returned casing) + + Notes: + - Comparison is case-insensitive using casefold(); original casing is preserved in results. + - Overlapping grantees (case-insensitive) are excluded from the results. + """ + + def _diffs(config1: GrantsConfig, config2: GrantsConfig) -> GrantsConfig: + diffs: GrantsConfig = {} + cf_config2 = {k.casefold(): {g.casefold() for g in v} for k, v in config2.items()} + for key, grantees in config1.items(): + cf_key = key.casefold() + + # Missing key (add all grantees) + if cf_key not in cf_config2: + diffs[key] = grantees.copy() + continue + + # Include only grantees not in config2 + cf_grantees2 = cf_config2[cf_key] + diff_grantees = [] + for grantee in grantees: + if grantee.casefold() not in cf_grantees2: + diff_grantees.append(grantee) + if diff_grantees: + diffs[key] = diff_grantees + return diffs + + return _diffs(new_config, old_config), _diffs(old_config, new_config) + + def _get_current_grants_config(self, table: exp.Table) -> GrantsConfig: + """Returns current grants for a table as a dictionary. + + This method queries the database and returns the current grants/permissions + for the given table, parsed into a dictionary format. The it handles + case-insensitive comparison between these current grants and the desired + grants from model configuration. + + Args: + table: The table/view to query grants for. + + Returns: + Dictionary mapping permissions to lists of grantees. Permission names + should be returned as the database provides them (typically uppercase + for standard SQL permissions, but engine-specific roles may vary). + + Raises: + NotImplementedError: If the engine does not support grants. + """ + if not self.SUPPORTS_GRANTS: + raise NotImplementedError(f"Engine does not support grants: {type(self)}") + raise NotImplementedError("Subclass must implement get_current_grants") + + def _apply_grants_config_expr( + self, + table: exp.Table, + grants_config: GrantsConfig, + table_type: DataObjectType = DataObjectType.TABLE, + ) -> t.List[exp.Expression]: + """Returns SQLGlot Grant expressions to apply grants to a table. + + Args: + table: The table/view to grant permissions on. + grants_config: Dictionary mapping permissions to lists of grantees. + table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW). + + Returns: + List of SQLGlot expressions for grant operations. + + Raises: + NotImplementedError: If the engine does not support grants. + """ + if not self.SUPPORTS_GRANTS: + raise NotImplementedError(f"Engine does not support grants: {type(self)}") + raise NotImplementedError("Subclass must implement _apply_grants_config_expr") + + def _revoke_grants_config_expr( + self, + table: exp.Table, + grants_config: GrantsConfig, + table_type: DataObjectType = DataObjectType.TABLE, + ) -> t.List[exp.Expression]: + """Returns SQLGlot expressions to revoke grants from a table. + + Args: + table: The table/view to revoke permissions from. + grants_config: Dictionary mapping permissions to lists of grantees. + table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW). + + Returns: + List of SQLGlot expressions for revoke operations. + + Raises: + NotImplementedError: If the engine does not support grants. + """ + if not self.SUPPORTS_GRANTS: + raise NotImplementedError(f"Engine does not support grants: {type(self)}") + raise NotImplementedError("Subclass must implement _revoke_grants_config_expr") + class EngineAdapterWithIndexSupport(EngineAdapter): SUPPORTS_INDEXES = True diff --git a/sqlmesh/core/engine_adapter/base_postgres.py b/sqlmesh/core/engine_adapter/base_postgres.py index 3de975d6a5..11f56da133 100644 --- a/sqlmesh/core/engine_adapter/base_postgres.py +++ b/sqlmesh/core/engine_adapter/base_postgres.py @@ -62,6 +62,7 @@ def columns( raise SQLMeshError( f"Could not get columns for table '{table.sql(dialect=self.dialect)}'. Table not found." ) + return { column_name: exp.DataType.build(data_type, dialect=self.dialect, udt=True) for column_name, data_type in resp @@ -196,3 +197,10 @@ def _get_data_objects( ) for row in df.itertuples() ] + + def _get_current_schema(self) -> str: + """Returns the current default schema for the connection.""" + result = self.fetchone(exp.select(exp.func("current_schema"))) + if result and result[0]: + return result[0] + return "public" diff --git a/sqlmesh/core/engine_adapter/bigquery.py b/sqlmesh/core/engine_adapter/bigquery.py index 09fd7537ef..59a56b6ace 100644 --- a/sqlmesh/core/engine_adapter/bigquery.py +++ b/sqlmesh/core/engine_adapter/bigquery.py @@ -11,6 +11,7 @@ from sqlmesh.core.engine_adapter.base import _get_data_object_cache_key from sqlmesh.core.engine_adapter.mixins import ( ClusteredByMixin, + GrantsFromInfoSchemaMixin, RowDiffMixin, TableAlterClusterByOperation, ) @@ -40,7 +41,7 @@ from google.cloud.bigquery.table import Table as BigQueryTable from sqlmesh.core._typing import SchemaName, SessionProperties, TableName - from sqlmesh.core.engine_adapter._typing import BigframeSession, DF, Query + from sqlmesh.core.engine_adapter._typing import BigframeSession, DCL, DF, GrantsConfig, Query from sqlmesh.core.engine_adapter.base import QueryOrDF @@ -55,7 +56,7 @@ @set_catalog() -class BigQueryEngineAdapter(ClusteredByMixin, RowDiffMixin): +class BigQueryEngineAdapter(ClusteredByMixin, RowDiffMixin, GrantsFromInfoSchemaMixin): """ BigQuery Engine Adapter using the `google-cloud-bigquery` library's DB API. """ @@ -65,6 +66,11 @@ class BigQueryEngineAdapter(ClusteredByMixin, RowDiffMixin): SUPPORTS_TRANSACTIONS = False SUPPORTS_MATERIALIZED_VIEWS = True SUPPORTS_CLONING = True + SUPPORTS_GRANTS = True + CURRENT_USER_OR_ROLE_EXPRESSION: exp.Expression = exp.func("session_user") + SUPPORTS_MULTIPLE_GRANT_PRINCIPALS = True + USE_CATALOG_IN_GRANTS = True + GRANT_INFORMATION_SCHEMA_TABLE_NAME = "OBJECT_PRIVILEGES" MAX_TABLE_COMMENT_LENGTH = 1024 MAX_COLUMN_COMMENT_LENGTH = 1024 SUPPORTS_QUERY_EXECUTION_TRACKING = True @@ -1326,6 +1332,108 @@ def _session_id(self) -> t.Any: def _session_id(self, value: t.Any) -> None: self._connection_pool.set_attribute("session_id", value) + def _get_current_schema(self) -> str: + raise NotImplementedError("BigQuery does not support current schema") + + def _get_bq_dataset_location(self, project: str, dataset: str) -> str: + return self._db_call(self.client.get_dataset, dataset_ref=f"{project}.{dataset}").location + + def _get_grant_expression(self, table: exp.Table) -> exp.Expression: + if not table.db: + raise ValueError( + f"Table {table.sql(dialect=self.dialect)} does not have a schema (dataset)" + ) + project = table.catalog or self.get_current_catalog() + if not project: + raise ValueError( + f"Table {table.sql(dialect=self.dialect)} does not have a catalog (project)" + ) + + dataset = table.db + table_name = table.name + location = self._get_bq_dataset_location(project, dataset) + + # https://cloud.google.com/bigquery/docs/information-schema-object-privileges + # OBJECT_PRIVILEGES is a project-level INFORMATION_SCHEMA view with regional qualifier + object_privileges_table = exp.to_table( + f"`{project}`.`region-{location}`.INFORMATION_SCHEMA.{self.GRANT_INFORMATION_SCHEMA_TABLE_NAME}", + dialect=self.dialect, + ) + return ( + exp.select("privilege_type", "grantee") + .from_(object_privileges_table) + .where( + exp.and_( + exp.column("object_schema").eq(exp.Literal.string(dataset)), + exp.column("object_name").eq(exp.Literal.string(table_name)), + # Filter out current_user + # BigQuery grantees format: "user:email" or "group:name" + exp.func("split", exp.column("grantee"), exp.Literal.string(":"))[ + exp.func("OFFSET", exp.Literal.number("1")) + ].neq(self.CURRENT_USER_OR_ROLE_EXPRESSION), + ) + ) + ) + + @staticmethod + def _grant_object_kind(table_type: DataObjectType) -> str: + if table_type == DataObjectType.VIEW: + return "VIEW" + if table_type == DataObjectType.MATERIALIZED_VIEW: + # We actually need to use "MATERIALIZED VIEW" here even though it's not listed + # as a supported resource_type in the BigQuery DCL doc: + # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-control-language + return "MATERIALIZED VIEW" + return "TABLE" + + def _dcl_grants_config_expr( + self, + dcl_cmd: t.Type[DCL], + table: exp.Table, + grants_config: GrantsConfig, + table_type: DataObjectType = DataObjectType.TABLE, + ) -> t.List[exp.Expression]: + expressions: t.List[exp.Expression] = [] + if not grants_config: + return expressions + + # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-control-language + + def normalize_principal(p: str) -> str: + if ":" not in p: + raise ValueError(f"Principal '{p}' missing a prefix label") + + # allUsers and allAuthenticatedUsers special groups that are cas-sensitive and must start with "specialGroup:" + if p.endswith("allUsers") or p.endswith("allAuthenticatedUsers"): + if not p.startswith("specialGroup:"): + raise ValueError( + f"Special group principal '{p}' must start with 'specialGroup:' prefix label" + ) + return p + + label, principal = p.split(":", 1) + # always lowercase principals + return f"{label}:{principal.lower()}" + + object_kind = self._grant_object_kind(table_type) + for privilege, principals in grants_config.items(): + if not principals: + continue + + noramlized_principals = [exp.Literal.string(normalize_principal(p)) for p in principals] + args: t.Dict[str, t.Any] = { + "privileges": [exp.GrantPrivilege(this=exp.to_identifier(privilege, quoted=True))], + "securable": table.copy(), + "principals": noramlized_principals, + } + + if object_kind: + args["kind"] = exp.Var(this=object_kind) + + expressions.append(dcl_cmd(**args)) # type: ignore[arg-type] + + return expressions + class _ErrorCounter: """ diff --git a/sqlmesh/core/engine_adapter/databricks.py b/sqlmesh/core/engine_adapter/databricks.py index 173e1b08af..7521124684 100644 --- a/sqlmesh/core/engine_adapter/databricks.py +++ b/sqlmesh/core/engine_adapter/databricks.py @@ -5,7 +5,9 @@ from functools import partial from sqlglot import exp + from sqlmesh.core.dialect import to_schema +from sqlmesh.core.engine_adapter.mixins import GrantsFromInfoSchemaMixin from sqlmesh.core.engine_adapter.shared import ( CatalogSupport, DataObject, @@ -28,12 +30,14 @@ logger = logging.getLogger(__name__) -class DatabricksEngineAdapter(SparkEngineAdapter): +class DatabricksEngineAdapter(SparkEngineAdapter, GrantsFromInfoSchemaMixin): DIALECT = "databricks" INSERT_OVERWRITE_STRATEGY = InsertOverwriteStrategy.REPLACE_WHERE SUPPORTS_CLONING = True SUPPORTS_MATERIALIZED_VIEWS = True SUPPORTS_MATERIALIZED_VIEW_SCHEMA = True + SUPPORTS_GRANTS = True + USE_CATALOG_IN_GRANTS = True # Spark has this set to false for compatibility when mixing with Trino but that isn't a concern with Databricks QUOTE_IDENTIFIERS_IN_VIEWS = True SCHEMA_DIFFER_KWARGS = { @@ -151,6 +155,28 @@ def spark(self) -> PySparkSession: def catalog_support(self) -> CatalogSupport: return CatalogSupport.FULL_SUPPORT + @staticmethod + def _grant_object_kind(table_type: DataObjectType) -> str: + if table_type == DataObjectType.VIEW: + return "VIEW" + if table_type == DataObjectType.MATERIALIZED_VIEW: + return "MATERIALIZED VIEW" + return "TABLE" + + def _get_grant_expression(self, table: exp.Table) -> exp.Expression: + # We only care about explicitly granted privileges and not inherited ones + # if this is removed you would see grants inherited from the catalog get returned + expression = super()._get_grant_expression(table) + expression.args["where"].set( + "this", + exp.and_( + expression.args["where"].this, + exp.column("inherited_from").eq(exp.Literal.string("NONE")), + wrap=False, + ), + ) + return expression + def _begin_session(self, properties: SessionProperties) -> t.Any: """Begin a new session.""" # Align the different possible connectors to a single catalog diff --git a/sqlmesh/core/engine_adapter/mixins.py b/sqlmesh/core/engine_adapter/mixins.py index 1d66da0607..c8ef32b9da 100644 --- a/sqlmesh/core/engine_adapter/mixins.py +++ b/sqlmesh/core/engine_adapter/mixins.py @@ -7,8 +7,10 @@ from sqlglot import exp, parse_one from sqlglot.helper import seq_get +from sqlglot.optimizer.normalize_identifiers import normalize_identifiers from sqlmesh.core.engine_adapter.base import EngineAdapter +from sqlmesh.core.engine_adapter.shared import DataObjectType from sqlmesh.core.node import IntervalUnit from sqlmesh.core.dialect import schema_ from sqlmesh.core.schema_diff import TableAlterOperation @@ -16,7 +18,12 @@ if t.TYPE_CHECKING: from sqlmesh.core._typing import TableName - from sqlmesh.core.engine_adapter._typing import DF + from sqlmesh.core.engine_adapter._typing import ( + DCL, + DF, + GrantsConfig, + QueryOrDF, + ) from sqlmesh.core.engine_adapter.base import QueryOrDF logger = logging.getLogger(__name__) @@ -548,3 +555,137 @@ def _normalize_decimal_value(self, expr: exp.Expression, precision: int) -> exp. def _normalize_boolean_value(self, expr: exp.Expression) -> exp.Expression: return exp.cast(expr, "INT") + + +class GrantsFromInfoSchemaMixin(EngineAdapter): + CURRENT_USER_OR_ROLE_EXPRESSION: exp.Expression = exp.func("current_user") + SUPPORTS_MULTIPLE_GRANT_PRINCIPALS = False + USE_CATALOG_IN_GRANTS = False + GRANT_INFORMATION_SCHEMA_TABLE_NAME = "table_privileges" + + @staticmethod + @abc.abstractmethod + def _grant_object_kind(table_type: DataObjectType) -> t.Optional[str]: + pass + + @abc.abstractmethod + def _get_current_schema(self) -> str: + pass + + def _dcl_grants_config_expr( + self, + dcl_cmd: t.Type[DCL], + table: exp.Table, + grants_config: GrantsConfig, + table_type: DataObjectType = DataObjectType.TABLE, + ) -> t.List[exp.Expression]: + expressions: t.List[exp.Expression] = [] + if not grants_config: + return expressions + + object_kind = self._grant_object_kind(table_type) + for privilege, principals in grants_config.items(): + args: t.Dict[str, t.Any] = { + "privileges": [exp.GrantPrivilege(this=exp.Var(this=privilege))], + "securable": table.copy(), + } + if object_kind: + args["kind"] = exp.Var(this=object_kind) + if self.SUPPORTS_MULTIPLE_GRANT_PRINCIPALS: + args["principals"] = [ + normalize_identifiers( + parse_one(principal, into=exp.GrantPrincipal, dialect=self.dialect), + dialect=self.dialect, + ) + for principal in principals + ] + expressions.append(dcl_cmd(**args)) # type: ignore[arg-type] + else: + for principal in principals: + args["principals"] = [ + normalize_identifiers( + parse_one(principal, into=exp.GrantPrincipal, dialect=self.dialect), + dialect=self.dialect, + ) + ] + expressions.append(dcl_cmd(**args)) # type: ignore[arg-type] + + return expressions + + def _apply_grants_config_expr( + self, + table: exp.Table, + grants_config: GrantsConfig, + table_type: DataObjectType = DataObjectType.TABLE, + ) -> t.List[exp.Expression]: + return self._dcl_grants_config_expr(exp.Grant, table, grants_config, table_type) + + def _revoke_grants_config_expr( + self, + table: exp.Table, + grants_config: GrantsConfig, + table_type: DataObjectType = DataObjectType.TABLE, + ) -> t.List[exp.Expression]: + return self._dcl_grants_config_expr(exp.Revoke, table, grants_config, table_type) + + def _get_grant_expression(self, table: exp.Table) -> exp.Expression: + schema_identifier = table.args.get("db") or normalize_identifiers( + exp.to_identifier(self._get_current_schema(), quoted=True), dialect=self.dialect + ) + schema_name = schema_identifier.this + table_name = table.args.get("this").this # type: ignore + + grant_conditions = [ + exp.column("table_schema").eq(exp.Literal.string(schema_name)), + exp.column("table_name").eq(exp.Literal.string(table_name)), + exp.column("grantor").eq(self.CURRENT_USER_OR_ROLE_EXPRESSION), + exp.column("grantee").neq(self.CURRENT_USER_OR_ROLE_EXPRESSION), + ] + + info_schema_table = normalize_identifiers( + exp.table_(self.GRANT_INFORMATION_SCHEMA_TABLE_NAME, db="information_schema"), + dialect=self.dialect, + ) + if self.USE_CATALOG_IN_GRANTS: + catalog_identifier = table.args.get("catalog") + if not catalog_identifier: + catalog_name = self.get_current_catalog() + if not catalog_name: + raise SQLMeshError( + "Current catalog could not be determined for fetching grants. This is unexpected." + ) + catalog_identifier = normalize_identifiers( + exp.to_identifier(catalog_name, quoted=True), dialect=self.dialect + ) + catalog_name = catalog_identifier.this + info_schema_table.set("catalog", catalog_identifier.copy()) + grant_conditions.insert( + 0, exp.column("table_catalog").eq(exp.Literal.string(catalog_name)) + ) + + return ( + exp.select("privilege_type", "grantee") + .from_(info_schema_table) + .where(exp.and_(*grant_conditions)) + ) + + def _get_current_grants_config(self, table: exp.Table) -> GrantsConfig: + grant_expr = self._get_grant_expression(table) + + results = self.fetchall(grant_expr) + + grants_dict: GrantsConfig = {} + for privilege_raw, grantee_raw in results: + if privilege_raw is None or grantee_raw is None: + continue + + privilege = str(privilege_raw) + grantee = str(grantee_raw) + if not privilege or not grantee: + continue + + grantees = grants_dict.setdefault(privilege, []) + if grantee not in grantees: + grantees.append(grantee) + + return grants_dict diff --git a/sqlmesh/core/engine_adapter/postgres.py b/sqlmesh/core/engine_adapter/postgres.py index 79431ee360..3dd108cf91 100644 --- a/sqlmesh/core/engine_adapter/postgres.py +++ b/sqlmesh/core/engine_adapter/postgres.py @@ -12,6 +12,7 @@ PandasNativeFetchDFSupportMixin, RowDiffMixin, logical_merge, + GrantsFromInfoSchemaMixin, ) from sqlmesh.core.engine_adapter.shared import set_catalog @@ -28,14 +29,19 @@ class PostgresEngineAdapter( PandasNativeFetchDFSupportMixin, GetCurrentCatalogFromFunctionMixin, RowDiffMixin, + GrantsFromInfoSchemaMixin, ): DIALECT = "postgres" + SUPPORTS_GRANTS = True SUPPORTS_INDEXES = True HAS_VIEW_BINDING = True CURRENT_CATALOG_EXPRESSION = exp.column("current_catalog") SUPPORTS_REPLACE_TABLE = False MAX_IDENTIFIER_LENGTH: t.Optional[int] = 63 SUPPORTS_QUERY_EXECUTION_TRACKING = True + GRANT_INFORMATION_SCHEMA_TABLE_NAME = "role_table_grants" + CURRENT_USER_OR_ROLE_EXPRESSION: exp.Expression = exp.column("current_role") + SUPPORTS_MULTIPLE_GRANT_PRINCIPALS = True SCHEMA_DIFFER_KWARGS = { "parameterized_type_defaults": { # DECIMAL without precision is "up to 131072 digits before the decimal point; up to 16383 digits after the decimal point" diff --git a/sqlmesh/core/engine_adapter/redshift.py b/sqlmesh/core/engine_adapter/redshift.py index 7979268473..03dc89053e 100644 --- a/sqlmesh/core/engine_adapter/redshift.py +++ b/sqlmesh/core/engine_adapter/redshift.py @@ -14,6 +14,7 @@ VarcharSizeWorkaroundMixin, RowDiffMixin, logical_merge, + GrantsFromInfoSchemaMixin, ) from sqlmesh.core.engine_adapter.shared import ( CommentCreationView, @@ -40,12 +41,15 @@ class RedshiftEngineAdapter( NonTransactionalTruncateMixin, VarcharSizeWorkaroundMixin, RowDiffMixin, + GrantsFromInfoSchemaMixin, ): DIALECT = "redshift" CURRENT_CATALOG_EXPRESSION = exp.func("current_database") # Redshift doesn't support comments for VIEWs WITH NO SCHEMA BINDING (which we always use) COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED SUPPORTS_REPLACE_TABLE = False + SUPPORTS_GRANTS = True + SUPPORTS_MULTIPLE_GRANT_PRINCIPALS = True SCHEMA_DIFFER_KWARGS = { "parameterized_type_defaults": { diff --git a/sqlmesh/core/engine_adapter/risingwave.py b/sqlmesh/core/engine_adapter/risingwave.py index fdcee90f0f..61b44f5bbb 100644 --- a/sqlmesh/core/engine_adapter/risingwave.py +++ b/sqlmesh/core/engine_adapter/risingwave.py @@ -32,6 +32,7 @@ class RisingwaveEngineAdapter(PostgresEngineAdapter): SUPPORTS_MATERIALIZED_VIEWS = True SUPPORTS_TRANSACTIONS = False MAX_IDENTIFIER_LENGTH = None + SUPPORTS_GRANTS = False def columns( self, table_name: TableName, include_pseudo_columns: bool = False diff --git a/sqlmesh/core/engine_adapter/snowflake.py b/sqlmesh/core/engine_adapter/snowflake.py index 1554589779..a8eabe070d 100644 --- a/sqlmesh/core/engine_adapter/snowflake.py +++ b/sqlmesh/core/engine_adapter/snowflake.py @@ -15,6 +15,7 @@ GetCurrentCatalogFromFunctionMixin, ClusteredByMixin, RowDiffMixin, + GrantsFromInfoSchemaMixin, ) from sqlmesh.core.engine_adapter.shared import ( CatalogSupport, @@ -34,7 +35,12 @@ import pandas as pd from sqlmesh.core._typing import SchemaName, SessionProperties, TableName - from sqlmesh.core.engine_adapter._typing import DF, Query, QueryOrDF, SnowparkSession + from sqlmesh.core.engine_adapter._typing import ( + DF, + Query, + QueryOrDF, + SnowparkSession, + ) from sqlmesh.core.node import IntervalUnit @@ -46,7 +52,9 @@ "drop_catalog": CatalogSupport.REQUIRES_SET_CATALOG, # needs a catalog to issue a query to information_schema.databases even though the result is global } ) -class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixin, RowDiffMixin): +class SnowflakeEngineAdapter( + GetCurrentCatalogFromFunctionMixin, ClusteredByMixin, RowDiffMixin, GrantsFromInfoSchemaMixin +): DIALECT = "snowflake" SUPPORTS_MATERIALIZED_VIEWS = True SUPPORTS_MATERIALIZED_VIEW_SCHEMA = True @@ -74,6 +82,9 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi MANAGED_TABLE_KIND = "DYNAMIC TABLE" SNOWPARK = "snowpark" SUPPORTS_QUERY_EXECUTION_TRACKING = True + SUPPORTS_GRANTS = True + CURRENT_USER_OR_ROLE_EXPRESSION: exp.Expression = exp.func("CURRENT_ROLE") + USE_CATALOG_IN_GRANTS = True @contextlib.contextmanager def session(self, properties: SessionProperties) -> t.Iterator[None]: @@ -128,6 +139,23 @@ def snowpark(self) -> t.Optional[SnowparkSession]: def catalog_support(self) -> CatalogSupport: return CatalogSupport.FULL_SUPPORT + @staticmethod + def _grant_object_kind(table_type: DataObjectType) -> str: + if table_type == DataObjectType.VIEW: + return "VIEW" + if table_type == DataObjectType.MATERIALIZED_VIEW: + return "MATERIALIZED VIEW" + if table_type == DataObjectType.MANAGED_TABLE: + return "DYNAMIC TABLE" + return "TABLE" + + def _get_current_schema(self) -> str: + """Returns the current default schema for the connection.""" + result = self.fetchone("SELECT CURRENT_SCHEMA()") + if not result or not result[0]: + raise SQLMeshError("Unable to determine current schema") + return str(result[0]) + def _create_catalog(self, catalog_name: exp.Identifier) -> None: props = exp.Properties( expressions=[exp.SchemaCommentProperty(this=exp.Literal.string(c.SQLMESH_MANAGED))] @@ -533,13 +561,32 @@ def _get_data_objects( for row in df.rename(columns={col: col.lower() for col in df.columns}).itertuples() ] + def _get_grant_expression(self, table: exp.Table) -> exp.Expression: + # Upon execute the catalog in table expressions are properly normalized to handle the case where a user provides + # the default catalog in their connection config. This doesn't though update catalogs in strings like when querying + # the information schema. So we need to manually replace those here. + expression = super()._get_grant_expression(table) + for col_exp in expression.find_all(exp.Column): + if col_exp.this.name == "table_catalog": + and_exp = col_exp.parent + assert and_exp is not None, "Expected column expression to have a parent" + assert and_exp.expression, "Expected AND expression to have an expression" + normalized_catalog = self._normalize_catalog( + exp.table_("placeholder", db="placeholder", catalog=and_exp.expression.this) + ) + and_exp.set( + "expression", + exp.Literal.string(normalized_catalog.args["catalog"].alias_or_name), + ) + return expression + def set_current_catalog(self, catalog: str) -> None: self.execute(exp.Use(this=exp.to_identifier(catalog))) def set_current_schema(self, schema: str) -> None: self.execute(exp.Use(kind="SCHEMA", this=to_schema(schema))) - def _to_sql(self, expression: exp.Expression, quote: bool = True, **kwargs: t.Any) -> str: + def _normalize_catalog(self, expression: exp.Expression) -> exp.Expression: # note: important to use self._default_catalog instead of the self.default_catalog property # otherwise we get RecursionError: maximum recursion depth exceeded # because it calls get_current_catalog(), which executes a query, which needs the default catalog, which calls get_current_catalog()... etc @@ -572,8 +619,12 @@ def catalog_rewriter(node: exp.Expression) -> exp.Expression: # Snowflake connection config. This is because the catalog present on the model gets normalized and quoted to match # the source dialect, which isnt always compatible with Snowflake expression = expression.transform(catalog_rewriter) + return expression - return super()._to_sql(expression=expression, quote=quote, **kwargs) + def _to_sql(self, expression: exp.Expression, quote: bool = True, **kwargs: t.Any) -> str: + return super()._to_sql( + expression=self._normalize_catalog(expression), quote=quote, **kwargs + ) def _create_column_comments( self, diff --git a/sqlmesh/core/engine_adapter/spark.py b/sqlmesh/core/engine_adapter/spark.py index b2d6a9cbb5..5216b0a329 100644 --- a/sqlmesh/core/engine_adapter/spark.py +++ b/sqlmesh/core/engine_adapter/spark.py @@ -397,7 +397,7 @@ def get_current_catalog(self) -> t.Optional[str]: def set_current_catalog(self, catalog_name: str) -> None: self.connection.set_current_catalog(catalog_name) - def get_current_database(self) -> str: + def _get_current_schema(self) -> str: if self._use_spark_session: return self.spark.catalog.currentDatabase() return self.fetchone(exp.select(exp.func("current_database")))[0] # type: ignore @@ -539,7 +539,7 @@ def _ensure_fqn(self, table_name: TableName) -> exp.Table: if not table.catalog: table.set("catalog", self.get_current_catalog()) if not table.db: - table.set("db", self.get_current_database()) + table.set("db", self._get_current_schema()) return table def _build_create_comment_column_exp( diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index 0a55f80cee..d2b9a11c08 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -641,6 +641,7 @@ def parse_strings_with_macro_refs(value: t.Any, dialect: DialectType) -> t.Any: "physical_properties_", "virtual_properties_", "materialization_properties_", + "grants_", mode="before", check_fields=False, )(parse_properties) diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index 974901cb55..f81dae004b 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -67,6 +67,7 @@ from sqlmesh.core.context import ExecutionContext from sqlmesh.core.engine_adapter import EngineAdapter from sqlmesh.core.engine_adapter._typing import QueryOrDF + from sqlmesh.core.engine_adapter.shared import DataObjectType from sqlmesh.core.linter.rule import Rule from sqlmesh.core.snapshot import DeployabilityIndex, Node, Snapshot from sqlmesh.utils.jinja import MacroReference @@ -1186,6 +1187,8 @@ def metadata_hash(self) -> str: gen(self.session_properties_) if self.session_properties_ else None, *[gen(g) for g in self.grains], *self._audit_metadata_hash_values(), + json.dumps(self.grants, sort_keys=True) if self.grants else None, + self.grants_target_layer, ] for key, value in (self.virtual_properties or {}).items(): @@ -1210,6 +1213,24 @@ def is_model(self) -> bool: """Return True if this is a model node""" return True + @property + def grants_table_type(self) -> DataObjectType: + """Get the table type for grants application (TABLE, VIEW, MATERIALIZED_VIEW). + + Returns: + The DataObjectType that should be used when applying grants to this model. + """ + from sqlmesh.core.engine_adapter.shared import DataObjectType + + if self.kind.is_view: + if hasattr(self.kind, "materialized") and getattr(self.kind, "materialized", False): + return DataObjectType.MATERIALIZED_VIEW + return DataObjectType.VIEW + if self.kind.is_managed: + return DataObjectType.MANAGED_TABLE + # All other materialized models are tables + return DataObjectType.TABLE + @property def _additional_metadata(self) -> t.List[str]: additional_metadata = [] @@ -1823,6 +1844,12 @@ def _data_hash_values_no_sql(self) -> t.List[str]: for column_name, column_hash in self.column_hashes.items(): data.append(column_name) data.append(column_hash) + + # Include grants in data hash for seed models to force recreation on grant changes + # since seed models don't support migration + data.append(json.dumps(self.grants, sort_keys=True) if self.grants else "") + data.append(self.grants_target_layer) + return data @@ -3023,6 +3050,8 @@ def render_expression( "optimize_query": str, "virtual_environment_mode": lambda value: exp.Literal.string(value.value), "dbt_node_info_": lambda value: value.to_expression(), + "grants_": lambda value: value, + "grants_target_layer": lambda value: exp.Literal.string(value.value), } diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index 7b8e88ac17..cc4c6f0826 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -154,6 +154,11 @@ def full_history_restatement_only(self) -> bool: def supports_python_models(self) -> bool: return True + @property + def supports_grants(self) -> bool: + """Whether this model kind supports grants configuration.""" + return self.is_materialized or self.is_view + class ModelKindName(str, ModelKindMixin, Enum): """The kind of model, determining how this data is computed and stored in the warehouse.""" diff --git a/sqlmesh/core/model/meta.py b/sqlmesh/core/model/meta.py index 9208fbdbb5..c48b7d1524 100644 --- a/sqlmesh/core/model/meta.py +++ b/sqlmesh/core/model/meta.py @@ -1,6 +1,7 @@ from __future__ import annotations import typing as t +from enum import Enum from functools import cached_property from typing_extensions import Self @@ -13,6 +14,7 @@ from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.config.linter import LinterConfig from sqlmesh.core.dialect import normalize_model_name +from sqlmesh.utils import classproperty from sqlmesh.core.model.common import ( bool_validator, default_catalog_validator, @@ -46,10 +48,41 @@ if t.TYPE_CHECKING: from sqlmesh.core._typing import CustomMaterializationProperties, SessionProperties + from sqlmesh.core.engine_adapter._typing import GrantsConfig FunctionCall = t.Tuple[str, t.Dict[str, exp.Expression]] +class GrantsTargetLayer(str, Enum): + """Target layer(s) where grants should be applied.""" + + ALL = "all" + PHYSICAL = "physical" + VIRTUAL = "virtual" + + @classproperty + def default(cls) -> "GrantsTargetLayer": + return GrantsTargetLayer.VIRTUAL + + @property + def is_all(self) -> bool: + return self == GrantsTargetLayer.ALL + + @property + def is_physical(self) -> bool: + return self == GrantsTargetLayer.PHYSICAL + + @property + def is_virtual(self) -> bool: + return self == GrantsTargetLayer.VIRTUAL + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return str(self) + + class ModelMeta(_Node): """Metadata for models which can be defined in SQL.""" @@ -85,6 +118,8 @@ class ModelMeta(_Node): ) formatting: t.Optional[bool] = Field(default=None, exclude=True) virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default + grants_: t.Optional[exp.Tuple] = Field(default=None, alias="grants") + grants_target_layer: GrantsTargetLayer = GrantsTargetLayer.default _bool_validator = bool_validator _model_kind_validator = model_kind_validator @@ -287,6 +322,14 @@ def _refs_validator(cls, vs: t.Any, info: ValidationInfo) -> t.List[exp.Expressi def ignored_rules_validator(cls, vs: t.Any) -> t.Any: return LinterConfig._validate_rules(vs) + @field_validator("grants_target_layer", mode="before") + def _grants_target_layer_validator(cls, v: t.Any) -> t.Any: + if isinstance(v, exp.Identifier): + return v.this + if isinstance(v, exp.Literal) and v.is_string: + return v.this + return v + @field_validator("session_properties_", mode="before") def session_properties_validator(cls, v: t.Any, info: ValidationInfo) -> t.Any: # use the generic properties validator to parse the session properties @@ -394,6 +437,10 @@ def _root_validator(self) -> Self: f"Model {self.name} has `storage_format` set to a table format '{storage_format}' which is deprecated. Please use the `table_format` property instead." ) + # Validate grants configuration for model kind support + if self.grants is not None and not kind.supports_grants: + raise ValueError(f"grants cannot be set for {kind.name} models") + return self @property @@ -465,6 +512,30 @@ def custom_materialization_properties(self) -> CustomMaterializationProperties: return self.kind.materialization_properties return {} + @cached_property + def grants(self) -> t.Optional[GrantsConfig]: + """A dictionary of grants mapping permission names to lists of grantees.""" + + if self.grants_ is None: + return None + + if not self.grants_.expressions: + return {} + + grants_dict = {} + for eq_expr in self.grants_.expressions: + try: + permission_name = self._validate_config_expression(eq_expr.left) + grantee_list = self._validate_nested_config_values(eq_expr.expression) + grants_dict[permission_name] = grantee_list + except ConfigError as e: + permission_name = ( + eq_expr.left.name if hasattr(eq_expr.left, "name") else str(eq_expr.left) + ) + raise ConfigError(f"Invalid grants configuration for '{permission_name}': {e}") + + return grants_dict if grants_dict else None + @property def all_references(self) -> t.List[Reference]: """All references including grains.""" @@ -529,3 +600,33 @@ def on_additive_change(self) -> OnAdditiveChange: @property def ignored_rules(self) -> t.Set[str]: return self.ignored_rules_ or set() + + def _validate_config_expression(self, expr: exp.Expression) -> str: + if isinstance(expr, (d.MacroFunc, d.MacroVar)): + raise ConfigError(f"Unresolved macro: {expr.sql(dialect=self.dialect)}") + + if isinstance(expr, exp.Null): + raise ConfigError("NULL value") + + if isinstance(expr, exp.Literal): + return str(expr.this).strip() + if isinstance(expr, (exp.Column, exp.Identifier)): + return expr.name + return expr.sql(dialect=self.dialect).strip() + + def _validate_nested_config_values(self, value_expr: exp.Expression) -> t.List[str]: + result = [] + + def flatten_expr(expr: exp.Expression) -> None: + if isinstance(expr, exp.Array): + for elem in expr.expressions: + flatten_expr(elem) + elif isinstance(expr, (exp.Tuple, exp.Paren)): + expressions = [expr.unnest()] if isinstance(expr, exp.Paren) else expr.expressions + for elem in expressions: + flatten_expr(elem) + else: + result.append(self._validate_config_expression(expr)) + + flatten_expr(value_expr) + return result diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 1483bdeece..2676709d85 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -39,6 +39,7 @@ from sqlmesh.core.audit import Audit, StandaloneAudit from sqlmesh.core.dialect import schema_ from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, DataObjectType, DataObject +from sqlmesh.core.model.meta import GrantsTargetLayer from sqlmesh.core.macros import RuntimeStage from sqlmesh.core.model import ( AuditResult, @@ -932,6 +933,7 @@ def _render_and_insert_snapshot( model = snapshot.model adapter = self.get_adapter(model.gateway) evaluation_strategy = _evaluation_strategy(snapshot, adapter) + is_snapshot_deployable = deployability_index.is_deployable(snapshot) queries_or_dfs = self._render_snapshot_for_evaluation( snapshot, @@ -955,6 +957,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: execution_time=execution_time, physical_properties=rendered_physical_properties, render_kwargs=create_render_kwargs, + is_snapshot_deployable=is_snapshot_deployable, ) else: logger.info( @@ -977,6 +980,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None: execution_time=execution_time, physical_properties=rendered_physical_properties, render_kwargs=create_render_kwargs, + is_snapshot_deployable=is_snapshot_deployable, ) # DataFrames, unlike SQL expressions, can provide partial results by yielding dataframes. As a result, @@ -1066,6 +1070,7 @@ def _clone_snapshot_in_dev( allow_additive_snapshots=allow_additive_snapshots, run_pre_post_statements=run_pre_post_statements, ) + except Exception: adapter.drop_table(target_table_name) raise @@ -1166,6 +1171,7 @@ def _migrate_target_table( rendered_physical_properties=rendered_physical_properties, dry_run=False, run_pre_post_statements=run_pre_post_statements, + skip_grants=True, # skip grants for tmp table ) try: evaluation_strategy = _evaluation_strategy(snapshot, adapter) @@ -1183,6 +1189,7 @@ def _migrate_target_table( allow_additive_snapshots=allow_additive_snapshots, ignore_destructive=snapshot.model.on_destructive_change.is_ignore, ignore_additive=snapshot.model.on_additive_change.is_ignore, + deployability_index=deployability_index, ) finally: if snapshot.is_materialized: @@ -1232,6 +1239,7 @@ def _promote_snapshot( model=snapshot.model, environment=environment_naming_info.name, snapshots=snapshots, + snapshot=snapshot, **render_kwargs, ) @@ -1431,6 +1439,7 @@ def _execute_create( rendered_physical_properties: t.Dict[str, exp.Expression], dry_run: bool, run_pre_post_statements: bool = True, + skip_grants: bool = False, ) -> None: adapter = self.get_adapter(snapshot.model.gateway) evaluation_strategy = _evaluation_strategy(snapshot, adapter) @@ -1451,11 +1460,14 @@ def _execute_create( table_name=table_name, model=snapshot.model, is_table_deployable=is_table_deployable, + skip_grants=skip_grants, render_kwargs=create_render_kwargs, is_snapshot_deployable=is_snapshot_deployable, is_snapshot_representative=is_snapshot_representative, dry_run=dry_run, physical_properties=rendered_physical_properties, + snapshot=snapshot, + deployability_index=deployability_index, ) if run_pre_post_statements: evaluation_strategy.run_post_statements( @@ -1469,7 +1481,7 @@ def _can_clone(self, snapshot: Snapshot, deployability_index: DeployabilityIndex and snapshot.is_materialized and bool(snapshot.previous_versions) and adapter.SUPPORTS_CLONING - # managed models cannot have their schema mutated because theyre based on queries, so clone + alter wont work + # managed models cannot have their schema mutated because they're based on queries, so clone + alter won't work and not snapshot.is_managed and not snapshot.is_dbt_custom and not deployability_index.is_deployable(snapshot) @@ -1690,6 +1702,7 @@ def create( model: Model, is_table_deployable: bool, render_kwargs: t.Dict[str, t.Any], + skip_grants: bool, **kwargs: t.Any, ) -> None: """Creates the target table or view. @@ -1780,6 +1793,66 @@ def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None: render_kwargs: Additional key-value arguments to pass when rendering the statements. """ + def _apply_grants( + self, + model: Model, + table_name: str, + target_layer: GrantsTargetLayer, + is_snapshot_deployable: bool = False, + ) -> None: + """Apply grants for a model if grants are configured. + + This method provides consistent grants application across all evaluation strategies. + It ensures that whenever a physical database object (table, view, materialized view) + is created or modified, the appropriate grants are applied. + + Args: + model: The SQLMesh model containing grants configuration + table_name: The target table/view name to apply grants to + target_layer: The grants application layer (physical or virtual) + is_snapshot_deployable: Whether the snapshot is deployable (targeting production) + """ + grants_config = model.grants + if grants_config is None: + return + + if not self.adapter.SUPPORTS_GRANTS: + logger.warning( + f"Engine {self.adapter.__class__.__name__} does not support grants. " + f"Skipping grants application for model {model.name}" + ) + return + + model_grants_target_layer = model.grants_target_layer + deployable_vde_dev_only = ( + is_snapshot_deployable and model.virtual_environment_mode.is_dev_only + ) + + # table_type is always a VIEW in the virtual layer unless model is deployable and VDE is dev_only + # in which case we fall back to the model's model_grants_table_type + if target_layer == GrantsTargetLayer.VIRTUAL and not deployable_vde_dev_only: + model_grants_table_type = DataObjectType.VIEW + else: + model_grants_table_type = model.grants_table_type + + if ( + model_grants_target_layer.is_all + or model_grants_target_layer == target_layer + # Always apply grants in production when VDE is dev_only regardless of target_layer + # since only physical tables are created in production + or deployable_vde_dev_only + ): + logger.info(f"Applying grants for model {model.name} to table {table_name}") + self.adapter.sync_grants_config( + exp.to_table(table_name, dialect=self.adapter.dialect), + grants_config, + model_grants_table_type, + ) + else: + logger.debug( + f"Skipping grants application for model {model.name} in {target_layer} layer" + ) + class SymbolicStrategy(EvaluationStrategy): def insert( @@ -1809,6 +1882,7 @@ def create( model: Model, is_table_deployable: bool, render_kwargs: t.Dict[str, t.Any], + skip_grants: bool, **kwargs: t.Any, ) -> None: pass @@ -1890,6 +1964,17 @@ def promote( view_properties=model.render_virtual_properties(**render_kwargs), ) + snapshot = kwargs.get("snapshot") + deployability_index = kwargs.get("deployability_index") + is_snapshot_deployable = ( + deployability_index.is_deployable(snapshot) + if snapshot and deployability_index + else False + ) + + # Apply grants to the virtual layer (view) after promotion + self._apply_grants(model, view_name, GrantsTargetLayer.VIRTUAL, is_snapshot_deployable) + def demote(self, view_name: str, **kwargs: t.Any) -> None: logger.info("Dropping view '%s'", view_name) self.adapter.drop_view(view_name, cascade=False) @@ -1908,6 +1993,7 @@ def create( model: Model, is_table_deployable: bool, render_kwargs: t.Dict[str, t.Any], + skip_grants: bool, **kwargs: t.Any, ) -> None: ctas_query = model.ctas_query(**render_kwargs) @@ -1952,6 +2038,13 @@ def create( column_descriptions=model.column_descriptions if is_table_deployable else None, ) + # Apply grants after table creation (unless explicitly skipped by caller) + if not skip_grants: + is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False) + self._apply_grants( + model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) + def migrate( self, target_table_name: str, @@ -1977,6 +2070,15 @@ def migrate( ) self.adapter.alter_table(alter_operations) + # Apply grants after schema migration + deployability_index = kwargs.get("deployability_index") + is_snapshot_deployable = ( + deployability_index.is_deployable(snapshot) if deployability_index else False + ) + self._apply_grants( + snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) + def delete(self, name: str, **kwargs: t.Any) -> None: _check_table_db_is_physical_schema(name, kwargs["physical_schema"]) self.adapter.drop_table(name, cascade=kwargs.pop("cascade", False)) @@ -1988,6 +2090,7 @@ def _replace_query_for_model( name: str, query_or_df: QueryOrDF, render_kwargs: t.Dict[str, t.Any], + skip_grants: bool = False, **kwargs: t.Any, ) -> None: """Replaces the table for the given model. @@ -2024,6 +2127,11 @@ def _replace_query_for_model( source_columns=source_columns, ) + # Apply grants after table replacement (unless explicitly skipped by caller) + if not skip_grants: + is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False) + self._apply_grants(model, name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable) + def _get_target_and_source_columns( self, model: Model, @@ -2271,6 +2379,7 @@ def create( model: Model, is_table_deployable: bool, render_kwargs: t.Dict[str, t.Any], + skip_grants: bool, **kwargs: t.Any, ) -> None: model = t.cast(SeedModel, model) @@ -2284,16 +2393,37 @@ def create( ) return - super().create(table_name, model, is_table_deployable, render_kwargs, **kwargs) + super().create( + table_name, + model, + is_table_deployable, + render_kwargs, + skip_grants=True, # Skip grants; they're applied after data insertion + **kwargs, + ) # For seeds we insert data at the time of table creation. try: for index, df in enumerate(model.render_seed()): if index == 0: - self._replace_query_for_model(model, table_name, df, render_kwargs, **kwargs) + self._replace_query_for_model( + model, + table_name, + df, + render_kwargs, + skip_grants=True, # Skip grants; they're applied after data insertion + **kwargs, + ) else: self.adapter.insert_append( table_name, df, target_columns_to_types=model.columns_to_types ) + + if not skip_grants: + # Apply grants after seed table creation and data insertion + is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False) + self._apply_grants( + model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) except Exception: self.adapter.drop_table(table_name) raise @@ -2341,6 +2471,7 @@ def create( model: Model, is_table_deployable: bool, render_kwargs: t.Dict[str, t.Any], + skip_grants: bool, **kwargs: t.Any, ) -> None: assert isinstance(model.kind, (SCDType2ByTimeKind, SCDType2ByColumnKind)) @@ -2370,9 +2501,17 @@ def create( model, is_table_deployable, render_kwargs, + skip_grants, **kwargs, ) + if not skip_grants: + # Apply grants after SCD Type 2 table creation + is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False) + self._apply_grants( + model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) + def insert( self, table_name: str, @@ -2440,6 +2579,10 @@ def insert( f"Unexpected SCD Type 2 kind: {model.kind}. This is not expected and please report this as a bug." ) + # Apply grants after SCD Type 2 table recreation + is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False) + self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable) + def append( self, table_name: str, @@ -2496,6 +2639,10 @@ def insert( column_descriptions=model.column_descriptions, ) + # Apply grants after view creation / replacement + is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False) + self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable) + def append( self, table_name: str, @@ -2512,12 +2659,21 @@ def create( model: Model, is_table_deployable: bool, render_kwargs: t.Dict[str, t.Any], + skip_grants: bool, **kwargs: t.Any, ) -> None: + is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False) + if self.adapter.table_exists(table_name): # Make sure we don't recreate the view to prevent deletion of downstream views in engines with no late # binding support (because of DROP CASCADE). logger.info("View '%s' already exists", table_name) + + if not skip_grants: + # Always apply grants when present, even if view exists, to handle grants updates + self._apply_grants( + model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) return logger.info("Creating view '%s'", table_name) @@ -2541,6 +2697,12 @@ def create( column_descriptions=model.column_descriptions if is_table_deployable else None, ) + if not skip_grants: + # Apply grants after view creation + self._apply_grants( + model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) + def migrate( self, target_table_name: str, @@ -2567,6 +2729,15 @@ def migrate( column_descriptions=model.column_descriptions, ) + # Apply grants after view migration + deployability_index = kwargs.get("deployability_index") + is_snapshot_deployable = ( + deployability_index.is_deployable(snapshot) if deployability_index else False + ) + self._apply_grants( + snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) + def delete(self, name: str, **kwargs: t.Any) -> None: cascade = kwargs.pop("cascade", False) try: @@ -2723,6 +2894,7 @@ def create( model: Model, is_table_deployable: bool, render_kwargs: t.Dict[str, t.Any], + skip_grants: bool, **kwargs: t.Any, ) -> None: original_query = model.render_query_or_raise(**render_kwargs) @@ -2852,6 +3024,7 @@ def create( model: Model, is_table_deployable: bool, render_kwargs: t.Dict[str, t.Any], + skip_grants: bool, **kwargs: t.Any, ) -> None: is_snapshot_deployable: bool = kwargs["is_snapshot_deployable"] @@ -2870,6 +3043,13 @@ def create( column_descriptions=model.column_descriptions, table_format=model.table_format, ) + + # Apply grants after managed table creation + if not skip_grants: + self._apply_grants( + model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) + elif not is_table_deployable: # Only create the dev preview table as a normal table. # For the main table, if the snapshot is cant be deployed to prod (eg upstream is forward-only) do nothing. @@ -2880,6 +3060,7 @@ def create( model=model, is_table_deployable=is_table_deployable, render_kwargs=render_kwargs, + skip_grants=skip_grants, **kwargs, ) @@ -2895,7 +3076,6 @@ def insert( deployability_index: DeployabilityIndex = kwargs["deployability_index"] snapshot: Snapshot = kwargs["snapshot"] is_snapshot_deployable = deployability_index.is_deployable(snapshot) - if is_first_insert and is_snapshot_deployable and not self.adapter.table_exists(table_name): self.adapter.create_managed_table( table_name=table_name, @@ -2908,6 +3088,9 @@ def insert( column_descriptions=model.column_descriptions, table_format=model.table_format, ) + self._apply_grants( + model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) elif not is_snapshot_deployable: # Snapshot isnt deployable; update the preview table instead # If the snapshot was deployable, then data would have already been loaded in create() because a managed table would have been created @@ -2956,6 +3139,15 @@ def migrate( f"The schema of the managed model '{target_table_name}' cannot be updated in a forward-only fashion." ) + # Apply grants after verifying no schema changes + deployability_index = kwargs.get("deployability_index") + is_snapshot_deployable = ( + deployability_index.is_deployable(snapshot) if deployability_index else False + ) + self._apply_grants( + snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) + def delete(self, name: str, **kwargs: t.Any) -> None: # a dev preview table is created as a normal table, so it needs to be dropped as a normal table _check_table_db_is_physical_schema(name, kwargs["physical_schema"]) diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index 0b75955129..3e325f13e6 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -165,7 +165,11 @@ def _validate_hooks(cls, v: t.Union[str, t.List[t.Union[SqlStr, str]]]) -> t.Lis @field_validator("grants", mode="before") @classmethod - def _validate_grants(cls, v: t.Dict[str, str]) -> t.Dict[str, t.List[str]]: + def _validate_grants( + cls, v: t.Optional[t.Dict[str, str]] + ) -> t.Optional[t.Dict[str, t.List[str]]]: + if v is None: + return None return {key: ensure_list(value) for key, value in v.items()} _FIELD_UPDATE_STRATEGY: t.ClassVar[t.Dict[str, UpdateStrategy]] = { diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index f47283d06e..f21eefe95d 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -679,6 +679,12 @@ def to_sqlmesh( if physical_properties: model_kwargs["physical_properties"] = physical_properties + kind = self.model_kind(context) + + # A falsy grants config (None or {}) is considered as unmanaged per dbt semantics + if self.grants and kind.supports_grants: + model_kwargs["grants"] = self.grants + allow_partials = model_kwargs.pop("allow_partials", None) if allow_partials is None: # Set allow_partials to True for dbt models to preserve the original semantics. diff --git a/sqlmesh/migrations/v0100_add_grants_and_grants_target_layer.py b/sqlmesh/migrations/v0100_add_grants_and_grants_target_layer.py new file mode 100644 index 0000000000..fa23935da0 --- /dev/null +++ b/sqlmesh/migrations/v0100_add_grants_and_grants_target_layer.py @@ -0,0 +1,9 @@ +"""Add grants and grants_target_layer to incremental model metadata hash.""" + + +def migrate_schemas(state_sync, **kwargs): # type: ignore + pass + + +def migrate_rows(state_sync, **kwargs): # type: ignore + pass diff --git a/tests/core/engine_adapter/integration/__init__.py b/tests/core/engine_adapter/integration/__init__.py index c5377e309a..49624154e4 100644 --- a/tests/core/engine_adapter/integration/__init__.py +++ b/tests/core/engine_adapter/integration/__init__.py @@ -5,10 +5,12 @@ import sys import typing as t import time +from contextlib import contextmanager import pandas as pd # noqa: TID253 import pytest from sqlglot import exp, parse_one +from sqlglot.optimizer.normalize_identifiers import normalize_identifiers from sqlmesh import Config, Context, EngineAdapter from sqlmesh.core.config import load_config_from_paths @@ -744,6 +746,106 @@ def upsert_sql_model(self, model_definition: str) -> t.Tuple[Context, SqlModel]: self._context.upsert_model(model) return self._context, model + def _get_create_user_or_role( + self, username: str, password: t.Optional[str] = None + ) -> t.Tuple[str, t.Optional[str]]: + password = password or random_id() + if self.dialect in ["postgres", "redshift"]: + return username, f"CREATE USER \"{username}\" WITH PASSWORD '{password}'" + if self.dialect == "snowflake": + return username, f"CREATE ROLE {username}" + if self.dialect == "databricks": + # Creating an account-level group in Databricks requires making REST API calls so we are going to + # use a pre-created group instead. We assume the suffix on the name is the unique id + return "_".join(username.split("_")[:-1]), None + if self.dialect == "bigquery": + # BigQuery uses IAM service accounts that need to be pre-created + # Pre-created GCP service accounts: + # - sqlmesh-test-admin@{project-id}.iam.gserviceaccount.com + # - sqlmesh-test-analyst@{project-id}.iam.gserviceaccount.com + # - sqlmesh-test-etl-user@{project-id}.iam.gserviceaccount.com + # - sqlmesh-test-reader@{project-id}.iam.gserviceaccount.com + # - sqlmesh-test-user@{project-id}.iam.gserviceaccount.com + # - sqlmesh-test-writer@{project-id}.iam.gserviceaccount.com + role_name = ( + username.replace(f"_{self.test_id}", "").replace("test_", "").replace("_", "-") + ) + project_id = self.engine_adapter.get_current_catalog() + service_account = f"sqlmesh-test-{role_name}@{project_id}.iam.gserviceaccount.com" + return f"serviceAccount:{service_account}", None + raise ValueError(f"User creation not supported for dialect: {self.dialect}") + + def _create_user_or_role(self, username: str, password: t.Optional[str] = None) -> str: + username, create_user_sql = self._get_create_user_or_role(username, password) + if create_user_sql: + self.engine_adapter.execute(create_user_sql) + return username + + @contextmanager + def create_users_or_roles(self, *role_names: str) -> t.Iterator[t.Dict[str, str]]: + created_users = [] + roles = {} + + try: + for role_name in role_names: + user_name = normalize_identifiers( + self.add_test_suffix(f"test_{role_name}"), dialect=self.dialect + ).sql(dialect=self.dialect) + password = random_id() + if self.dialect == "redshift": + password += ( + "A" # redshift requires passwords to have at least one uppercase letter + ) + user_name = self._create_user_or_role(user_name, password) + created_users.append(user_name) + roles[role_name] = user_name + + yield roles + + finally: + for user_name in created_users: + self._cleanup_user_or_role(user_name) + + def get_select_privilege(self) -> str: + if self.dialect == "bigquery": + return "roles/bigquery.dataViewer" + return "SELECT" + + def get_insert_privilege(self) -> str: + if self.dialect == "databricks": + # This would really be "MODIFY" but for the purposes of having this be unique from UPDATE + # we return "MANAGE" instead + return "MANAGE" + if self.dialect == "bigquery": + return "roles/bigquery.dataEditor" + return "INSERT" + + def get_update_privilege(self) -> str: + if self.dialect == "databricks": + return "MODIFY" + if self.dialect == "bigquery": + return "roles/bigquery.dataOwner" + return "UPDATE" + + def _cleanup_user_or_role(self, user_name: str) -> None: + """Helper function to clean up a user/role and all their dependencies.""" + try: + if self.dialect in ["postgres", "redshift"]: + self.engine_adapter.execute(f""" + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE usename = '{user_name}' AND pid <> pg_backend_pid() + """) + self.engine_adapter.execute(f'DROP OWNED BY "{user_name}"') + self.engine_adapter.execute(f'DROP USER IF EXISTS "{user_name}"') + elif self.dialect == "snowflake": + self.engine_adapter.execute(f"DROP ROLE IF EXISTS {user_name}") + elif self.dialect in ["databricks", "bigquery"]: + # For Databricks and BigQuery, we use pre-created accounts that should not be deleted + pass + except Exception: + pass + def wait_until(fn: t.Callable[..., bool], attempts=3, wait=5) -> None: current_attempt = 0 diff --git a/tests/core/engine_adapter/integration/test_integration.py b/tests/core/engine_adapter/integration/test_integration.py index 995875c778..5e976f8dd5 100644 --- a/tests/core/engine_adapter/integration/test_integration.py +++ b/tests/core/engine_adapter/integration/test_integration.py @@ -4027,3 +4027,209 @@ def test_unicode_characters(ctx: TestContext, tmp_path: Path): table_results = ctx.get_metadata_results(schema) assert len(table_results.tables) == 1 assert table_results.tables[0].lower().startswith(schema_name.lower() + "________") + + +def test_sync_grants_config(ctx: TestContext) -> None: + if not ctx.engine_adapter.SUPPORTS_GRANTS: + pytest.skip( + f"Skipping Test since engine adapter {ctx.engine_adapter.dialect} doesn't support grants" + ) + + table = ctx.table("sync_grants_integration") + select_privilege = ctx.get_select_privilege() + insert_privilege = ctx.get_insert_privilege() + update_privilege = ctx.get_update_privilege() + with ctx.create_users_or_roles("reader", "writer", "admin") as roles: + ctx.engine_adapter.create_table(table, {"id": exp.DataType.build("INT")}) + + initial_grants = { + select_privilege: [roles["reader"]], + insert_privilege: [roles["writer"]], + } + ctx.engine_adapter.sync_grants_config(table, initial_grants) + + current_grants = ctx.engine_adapter._get_current_grants_config(table) + assert set(current_grants.get(select_privilege, [])) == {roles["reader"]} + assert set(current_grants.get(insert_privilege, [])) == {roles["writer"]} + + target_grants = { + select_privilege: [roles["writer"], roles["admin"]], + update_privilege: [roles["admin"]], + } + ctx.engine_adapter.sync_grants_config(table, target_grants) + + synced_grants = ctx.engine_adapter._get_current_grants_config(table) + assert set(synced_grants.get(select_privilege, [])) == { + roles["writer"], + roles["admin"], + } + assert set(synced_grants.get(update_privilege, [])) == {roles["admin"]} + assert synced_grants.get(insert_privilege, []) == [] + + +def test_grants_sync_empty_config(ctx: TestContext): + if not ctx.engine_adapter.SUPPORTS_GRANTS: + pytest.skip( + f"Skipping Test since engine adapter {ctx.engine_adapter.dialect} doesn't support grants" + ) + + table = ctx.table("grants_empty_test") + select_privilege = ctx.get_select_privilege() + insert_privilege = ctx.get_insert_privilege() + with ctx.create_users_or_roles("user") as roles: + ctx.engine_adapter.create_table(table, {"id": exp.DataType.build("INT")}) + + initial_grants = { + select_privilege: [roles["user"]], + insert_privilege: [roles["user"]], + } + ctx.engine_adapter.sync_grants_config(table, initial_grants) + + initial_current_grants = ctx.engine_adapter._get_current_grants_config(table) + assert roles["user"] in initial_current_grants.get(select_privilege, []) + assert roles["user"] in initial_current_grants.get(insert_privilege, []) + + ctx.engine_adapter.sync_grants_config(table, {}) + + final_grants = ctx.engine_adapter._get_current_grants_config(table) + assert final_grants == {} + + +def test_grants_case_insensitive_grantees(ctx: TestContext): + if not ctx.engine_adapter.SUPPORTS_GRANTS: + pytest.skip( + f"Skipping Test since engine adapter {ctx.engine_adapter.dialect} doesn't support grants" + ) + + with ctx.create_users_or_roles("reader", "writer") as roles: + table = ctx.table("grants_quoted_test") + ctx.engine_adapter.create_table(table, {"id": exp.DataType.build("INT")}) + + reader = roles["reader"] + writer = roles["writer"] + select_privilege = ctx.get_select_privilege() + + if ctx.dialect == "bigquery": + # BigQuery labels are case sensitive, e.g. serviceAccount + lablel, grantee = writer.split(":", 1) + upper_case_writer = f"{lablel}:{grantee.upper()}" + else: + upper_case_writer = writer.upper() + + grants_config = {select_privilege: [reader, upper_case_writer]} + ctx.engine_adapter.sync_grants_config(table, grants_config) + + # Grantees are still in lowercase + current_grants = ctx.engine_adapter._get_current_grants_config(table) + assert reader in current_grants.get(select_privilege, []) + assert writer in current_grants.get(select_privilege, []) + + # Revoke writer + grants_config = {select_privilege: [reader.upper()]} + ctx.engine_adapter.sync_grants_config(table, grants_config) + + current_grants = ctx.engine_adapter._get_current_grants_config(table) + assert reader in current_grants.get(select_privilege, []) + assert writer not in current_grants.get(select_privilege, []) + + +def test_grants_plan(ctx: TestContext, tmp_path: Path): + if not ctx.engine_adapter.SUPPORTS_GRANTS: + pytest.skip( + f"Skipping Test since engine adapter {ctx.engine_adapter.dialect} doesn't support grants" + ) + + table = ctx.table("grant_model").sql(dialect="duckdb") + select_privilege = ctx.get_select_privilege() + insert_privilege = ctx.get_insert_privilege() + with ctx.create_users_or_roles("analyst", "etl_user") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + + model_def = f""" + MODEL ( + name {table}, + kind FULL, + grants ( + '{select_privilege}' = ['{roles["analyst"]}'] + ), + grants_target_layer 'all' + ); + SELECT 1 as id, CURRENT_DATE as created_date + """ + + (tmp_path / "models" / "grant_model.sql").write_text(model_def) + + context = ctx.create_context(path=tmp_path) + plan_result = context.plan(auto_apply=True, no_prompts=True) + + assert len(plan_result.new_snapshots) == 1 + snapshot = plan_result.new_snapshots[0] + + # Physical layer w/ grants + table_name = snapshot.table_name() + view_name = snapshot.qualified_view_name.for_environment( + plan_result.environment_naming_info, dialect=ctx.dialect + ) + current_grants = ctx.engine_adapter._get_current_grants_config( + exp.to_table(table_name, dialect=ctx.dialect) + ) + assert current_grants == {select_privilege: [roles["analyst"]]} + + # Virtual layer (view) w/ grants + virtual_grants = ctx.engine_adapter._get_current_grants_config( + exp.to_table(view_name, dialect=ctx.dialect) + ) + assert virtual_grants == {select_privilege: [roles["analyst"]]} + + # Update model with query change and new grants + updated_model = load_sql_based_model( + d.parse( + f""" + MODEL ( + name {table}, + kind FULL, + grants ( + '{select_privilege}' = ['{roles["analyst"]}', '{roles["etl_user"]}'], + '{insert_privilege}' = ['{roles["etl_user"]}'] + ), + grants_target_layer 'all' + ); + SELECT 1 as id, CURRENT_DATE as created_date, 'v2' as version + """, + default_dialect=context.default_dialect, + ), + dialect=context.default_dialect, + ) + context.upsert_model(updated_model) + + plan = context.plan(auto_apply=True, no_prompts=True) + plan_result = PlanResults.create(plan, ctx, ctx.add_test_suffix(TEST_SCHEMA)) + assert len(plan_result.plan.directly_modified) == 1 + + new_snapshot = plan_result.snapshot_for(updated_model) + assert new_snapshot is not None + + new_table_name = new_snapshot.table_name() + final_grants = ctx.engine_adapter._get_current_grants_config( + exp.to_table(new_table_name, dialect=ctx.dialect) + ) + expected_final_grants = { + select_privilege: [roles["analyst"], roles["etl_user"]], + insert_privilege: [roles["etl_user"]], + } + assert set(final_grants.get(select_privilege, [])) == set( + expected_final_grants[select_privilege] + ) + assert final_grants.get(insert_privilege, []) == expected_final_grants[insert_privilege] + + # Virtual layer should also have the updated grants + updated_virtual_grants = ctx.engine_adapter._get_current_grants_config( + exp.to_table(view_name, dialect=ctx.dialect) + ) + assert set(updated_virtual_grants.get(select_privilege, [])) == set( + expected_final_grants[select_privilege] + ) + assert ( + updated_virtual_grants.get(insert_privilege, []) + == expected_final_grants[insert_privilege] + ) diff --git a/tests/core/engine_adapter/integration/test_integration_postgres.py b/tests/core/engine_adapter/integration/test_integration_postgres.py index 26b8cbda42..f236fdebce 100644 --- a/tests/core/engine_adapter/integration/test_integration_postgres.py +++ b/tests/core/engine_adapter/integration/test_integration_postgres.py @@ -1,9 +1,11 @@ import typing as t +from contextlib import contextmanager import pytest from pytest import FixtureRequest from pathlib import Path from sqlmesh.core.engine_adapter import PostgresEngineAdapter from sqlmesh.core.config import Config, DuckDBConnectionConfig +from sqlmesh.core.config.common import VirtualEnvironmentMode from tests.core.engine_adapter.integration import TestContext import time_machine from datetime import timedelta @@ -12,6 +14,7 @@ from sqlmesh.core.context import Context from sqlmesh.core.state_sync import CachingStateSync, EngineAdapterStateSync from sqlmesh.core.snapshot.definition import SnapshotId +from sqlmesh.utils import random_id from tests.core.engine_adapter.integration import ( TestContext, @@ -22,6 +25,87 @@ ) +def _cleanup_user(engine_adapter: PostgresEngineAdapter, user_name: str) -> None: + """Helper function to clean up a PostgreSQL user and all their dependencies.""" + try: + engine_adapter.execute(f""" + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE usename = '{user_name}' AND pid <> pg_backend_pid() + """) + engine_adapter.execute(f'DROP OWNED BY "{user_name}"') + engine_adapter.execute(f'DROP USER IF EXISTS "{user_name}"') + except Exception: + pass + + +@contextmanager +def create_users( + engine_adapter: PostgresEngineAdapter, *role_names: str +) -> t.Iterator[t.Dict[str, t.Dict[str, str]]]: + """Create a set of Postgres users and yield their credentials.""" + created_users = [] + roles = {} + + try: + for role_name in role_names: + user_name = f"test_{role_name}" + _cleanup_user(engine_adapter, user_name) + + for role_name in role_names: + user_name = f"test_{role_name}" + 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}"') + created_users.append(user_name) + roles[role_name] = {"username": user_name, "password": password} + + yield roles + + finally: + for user_name in created_users: + _cleanup_user(engine_adapter, user_name) + + +def create_engine_adapter_for_role( + role_credentials: t.Dict[str, str], ctx: TestContext, config: Config +) -> PostgresEngineAdapter: + """Create a PostgreSQL adapter for a specific role to test authentication and permissions.""" + from sqlmesh.core.config import PostgresConnectionConfig + + gateway = ctx.gateway + assert gateway in config.gateways + connection_config = config.gateways[gateway].connection + assert isinstance(connection_config, PostgresConnectionConfig) + + role_connection_config = PostgresConnectionConfig( + host=connection_config.host, + port=connection_config.port, + database=connection_config.database, + user=role_credentials["username"], + password=role_credentials["password"], + keepalives_idle=connection_config.keepalives_idle, + connect_timeout=connection_config.connect_timeout, + role=connection_config.role, + sslmode=connection_config.sslmode, + application_name=connection_config.application_name, + ) + + return t.cast(PostgresEngineAdapter, role_connection_config.create_engine_adapter()) + + +@contextmanager +def engine_adapter_for_role( + role_credentials: t.Dict[str, str], ctx: TestContext, config: Config +) -> t.Iterator[PostgresEngineAdapter]: + """Context manager that yields a PostgresEngineAdapter and ensures it is closed.""" + adapter = create_engine_adapter_for_role(role_credentials, ctx, config) + try: + yield adapter + finally: + adapter.close() + + @pytest.fixture(params=list(generate_pytest_params(ENGINES_BY_NAME["postgres"]))) def ctx( request: FixtureRequest, @@ -286,3 +370,857 @@ def _mutate_config(gateway: str, config: Config): assert after_objects.views == [ exp.to_table(model_b_prod_snapshot.table_name()).text("this") ] + + +# Grants Integration Tests + + +def test_grants_plan_target_layer_physical_only( + engine_adapter: PostgresEngineAdapter, ctx: TestContext, tmp_path: Path +): + with create_users(engine_adapter, "reader") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + + model_def = """ + MODEL ( + name test_schema.physical_grants_model, + kind FULL, + grants ( + 'select' = ['test_reader'] + ), + grants_target_layer 'physical' + ); + SELECT 1 as id, 'physical_only' as layer + """ + + (tmp_path / "models" / "physical_grants_model.sql").write_text(model_def) + + context = ctx.create_context(path=tmp_path) + plan_result = context.plan(auto_apply=True, no_prompts=True) + + assert len(plan_result.new_snapshots) == 1 + snapshot = plan_result.new_snapshots[0] + physical_table_name = snapshot.table_name() + + physical_grants = engine_adapter._get_current_grants_config( + exp.to_table(physical_table_name, dialect=engine_adapter.dialect) + ) + assert physical_grants == {"SELECT": [roles["reader"]["username"]]} + + # Virtual layer should have no grants + virtual_view_name = f"test_schema.physical_grants_model" + virtual_grants = engine_adapter._get_current_grants_config( + exp.to_table(virtual_view_name, dialect=engine_adapter.dialect) + ) + assert virtual_grants == {} + + +def test_grants_plan_target_layer_virtual_only( + engine_adapter: PostgresEngineAdapter, ctx: TestContext, tmp_path: Path +): + with create_users(engine_adapter, "viewer") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + + model_def = """ + MODEL ( + name test_schema.virtual_grants_model, + kind FULL, + grants ( + 'select' = ['test_viewer'] + ), + grants_target_layer 'virtual' + ); + SELECT 1 as id, 'virtual_only' as layer + """ + + (tmp_path / "models" / "virtual_grants_model.sql").write_text(model_def) + + context = ctx.create_context(path=tmp_path) + plan_result = context.plan(auto_apply=True, no_prompts=True) + + assert len(plan_result.new_snapshots) == 1 + snapshot = plan_result.new_snapshots[0] + physical_table_name = snapshot.table_name() + + physical_grants = engine_adapter._get_current_grants_config( + exp.to_table(physical_table_name, dialect=engine_adapter.dialect) + ) + # Physical table should have no grants + assert physical_grants == {} + + virtual_view_name = f"test_schema.virtual_grants_model" + virtual_grants = engine_adapter._get_current_grants_config( + exp.to_table(virtual_view_name, dialect=engine_adapter.dialect) + ) + assert virtual_grants == {"SELECT": [roles["viewer"]["username"]]} + + +def test_grants_plan_full_refresh_model_via_replace( + engine_adapter: PostgresEngineAdapter, ctx: TestContext, tmp_path: Path +): + with create_users(engine_adapter, "reader") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + (tmp_path / "models" / "full_refresh_model.sql").write_text( + f""" + MODEL ( + name test_schema.full_refresh_model, + kind FULL, + grants ( + 'SELECT' = ['{roles["reader"]["username"]}'] + ), + grants_target_layer 'all' + ); + SELECT 1 as id, 'test_data' as status + """ + ) + + context = ctx.create_context(path=tmp_path) + + plan_result = context.plan( + "dev", # this triggers _replace_query_for_model for FULL models + auto_apply=True, + no_prompts=True, + ) + + assert len(plan_result.new_snapshots) == 1 + snapshot = plan_result.new_snapshots[0] + table_name = snapshot.table_name() + + # Physical table + grants = engine_adapter._get_current_grants_config( + exp.to_table(table_name, dialect=engine_adapter.dialect) + ) + assert grants == {"SELECT": [roles["reader"]["username"]]} + + # Virtual view + dev_view_name = "test_schema__dev.full_refresh_model" + dev_grants = engine_adapter._get_current_grants_config( + exp.to_table(dev_view_name, dialect=engine_adapter.dialect) + ) + assert dev_grants == {"SELECT": [roles["reader"]["username"]]} + + +def test_grants_plan_incremental_model( + engine_adapter: PostgresEngineAdapter, ctx: TestContext, tmp_path: Path +): + with create_users(engine_adapter, "reader", "writer") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + + model_name = "incr_model" + model_definition = f""" + MODEL ( + name test_schema.{model_name}, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ts + ), + grants ( + 'SELECT' = ['{roles["reader"]["username"]}'], + 'INSERT' = ['{roles["writer"]["username"]}'] + ), + grants_target_layer 'all' + ); + SELECT 1 as id, @start_ds::timestamp as ts, 'data' as value + """ + + (tmp_path / "models" / f"{model_name}.sql").write_text(model_definition) + + context = ctx.create_context(path=tmp_path) + + plan_result = context.plan( + "dev", start="2020-01-01", end="2020-01-01", auto_apply=True, no_prompts=True + ) + assert len(plan_result.new_snapshots) == 1 + + snapshot = plan_result.new_snapshots[0] + table_name = snapshot.table_name() + + physical_grants = engine_adapter._get_current_grants_config( + exp.to_table(table_name, dialect=engine_adapter.dialect) + ) + assert physical_grants.get("SELECT", []) == [roles["reader"]["username"]] + assert physical_grants.get("INSERT", []) == [roles["writer"]["username"]] + + view_name = f"test_schema__dev.{model_name}" + view_grants = engine_adapter._get_current_grants_config( + exp.to_table(view_name, dialect=engine_adapter.dialect) + ) + assert view_grants.get("SELECT", []) == [roles["reader"]["username"]] + assert view_grants.get("INSERT", []) == [roles["writer"]["username"]] + + +def test_grants_plan_clone_environment( + engine_adapter: PostgresEngineAdapter, ctx: TestContext, tmp_path: Path +): + with create_users(engine_adapter, "reader") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + (tmp_path / "models" / "clone_model.sql").write_text( + f""" + MODEL ( + name test_schema.clone_model, + kind FULL, + grants ( + 'SELECT' = ['{roles["reader"]["username"]}'] + ), + grants_target_layer 'all' + ); + + SELECT 1 as id, 'data' as value + """ + ) + + context = ctx.create_context(path=tmp_path) + prod_plan_result = context.plan("prod", auto_apply=True, no_prompts=True) + + assert len(prod_plan_result.new_snapshots) == 1 + prod_snapshot = prod_plan_result.new_snapshots[0] + prod_table_name = prod_snapshot.table_name() + + # Prod physical table grants + prod_grants = engine_adapter._get_current_grants_config( + exp.to_table(prod_table_name, dialect=engine_adapter.dialect) + ) + assert prod_grants == {"SELECT": [roles["reader"]["username"]]} + + # Prod virtual view grants + prod_view_name = f"test_schema.clone_model" + prod_view_grants = engine_adapter._get_current_grants_config( + exp.to_table(prod_view_name, dialect=engine_adapter.dialect) + ) + assert prod_view_grants == {"SELECT": [roles["reader"]["username"]]} + + # Create dev environment (cloned from prod) + context.plan("dev", auto_apply=True, no_prompts=True, include_unmodified=True) + + # Physical table grants should remain unchanged + prod_grants_after_clone = engine_adapter._get_current_grants_config( + exp.to_table(prod_table_name, dialect=engine_adapter.dialect) + ) + assert prod_grants_after_clone == prod_grants + + # Dev virtual view should have the same grants as prod + dev_view_name = f"test_schema__dev.clone_model" + dev_view_grants = engine_adapter._get_current_grants_config( + exp.to_table(dev_view_name, dialect=engine_adapter.dialect) + ) + assert dev_view_grants == prod_grants + + +@pytest.mark.parametrize( + "model_name,kind_config,query,extra_config,needs_seed", + [ + ( + "grants_full", + "FULL", + "SELECT 1 as id, 'unchanged_query' as data", + "", + False, + ), + ( + "grants_view", + "VIEW", + "SELECT 1 as id, 'unchanged_query' as data", + "", + False, + ), + ( + "grants_incr_time", + "INCREMENTAL_BY_TIME_RANGE (time_column event_date)", + "SELECT '2025-09-01'::date as event_date, 1 as id, 'unchanged_query' as data", + "start '2025-09-01',", + False, + ), + ( + "grants_seed", + "SEED (path '../seeds/grants_seed.csv')", + "", + "", + True, + ), + ], +) +def test_grants_metadata_only_changes( + engine_adapter: PostgresEngineAdapter, + ctx: TestContext, + tmp_path: Path, + model_name: str, + kind_config: str, + query: str, + extra_config: str, + needs_seed: bool, +): + with create_users(engine_adapter, "reader", "writer", "admin") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + + if needs_seed: + (tmp_path / "seeds").mkdir(exist_ok=True) + csv_content = "id,data\\n1,unchanged_query" + (tmp_path / "seeds" / f"{model_name}.csv").write_text(csv_content) + + initial_model_def = f""" + MODEL ( + name test_schema.{model_name}, + kind {kind_config}, + {extra_config} + grants ( + 'select' = ['{roles["reader"]["username"]}'] + ), + grants_target_layer 'all' + ); + {query} + """ + (tmp_path / "models" / f"{model_name}.sql").write_text(initial_model_def) + + context = ctx.create_context(path=tmp_path) + initial_plan_result = context.plan(auto_apply=True, no_prompts=True) + + assert len(initial_plan_result.new_snapshots) == 1 + initial_snapshot = initial_plan_result.new_snapshots[0] + + physical_table_name = initial_snapshot.table_name() + virtual_view_name = f"test_schema.{model_name}" + + initial_physical_grants = engine_adapter._get_current_grants_config( + exp.to_table(physical_table_name, dialect=engine_adapter.dialect) + ) + assert initial_physical_grants == {"SELECT": [roles["reader"]["username"]]} + + initial_virtual_grants = engine_adapter._get_current_grants_config( + exp.to_table(virtual_view_name, dialect=engine_adapter.dialect) + ) + assert initial_virtual_grants == {"SELECT": [roles["reader"]["username"]]} + + # Metadata-only change: update grants only using upsert_model + existing_model = context.get_model(f"test_schema.{model_name}") + context.upsert_model( + existing_model, + grants={ + "select": [roles["writer"]["username"], roles["admin"]["username"]], + "insert": [roles["admin"]["username"]], + }, + ) + second_plan_result = context.plan(auto_apply=True, no_prompts=True) + + expected_grants = { + "SELECT": [roles["writer"]["username"], roles["admin"]["username"]], + "INSERT": [roles["admin"]["username"]], + } + + # For seed models, grant changes rebuild the entire table, so it will create a new physical table + if model_name == "grants_seed" and second_plan_result.new_snapshots: + updated_snapshot = second_plan_result.new_snapshots[0] + physical_table_name = updated_snapshot.table_name() + + updated_physical_grants = engine_adapter._get_current_grants_config( + exp.to_table(physical_table_name, dialect=engine_adapter.dialect) + ) + assert set(updated_physical_grants.get("SELECT", [])) == set(expected_grants["SELECT"]) + assert updated_physical_grants.get("INSERT", []) == expected_grants["INSERT"] + + updated_virtual_grants = engine_adapter._get_current_grants_config( + exp.to_table(virtual_view_name, dialect=engine_adapter.dialect) + ) + assert set(updated_virtual_grants.get("SELECT", [])) == set(expected_grants["SELECT"]) + assert updated_virtual_grants.get("INSERT", []) == expected_grants["INSERT"] + + +def _vde_dev_only_config(gateway: str, config: Config) -> None: + config.virtual_environment_mode = VirtualEnvironmentMode.DEV_ONLY + + +@pytest.mark.parametrize( + "grants_target_layer,model_kind", + [ + ("virtual", "FULL"), + ("physical", "FULL"), + ("all", "FULL"), + ("virtual", "VIEW"), + ("physical", "VIEW"), + ], +) +def test_grants_target_layer_with_vde_dev_only( + engine_adapter: PostgresEngineAdapter, + ctx: TestContext, + tmp_path: Path, + grants_target_layer: str, + model_kind: str, +): + with create_users(engine_adapter, "reader", "writer") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + + if model_kind == "VIEW": + grants_config = ( + f"'SELECT' = ['{roles['reader']['username']}', '{roles['writer']['username']}']" + ) + else: + grants_config = f""" + 'SELECT' = ['{roles["reader"]["username"]}', '{roles["writer"]["username"]}'], + 'INSERT' = ['{roles["writer"]["username"]}'] + """.strip() + + model_def = f""" + MODEL ( + name test_schema.vde_model_{grants_target_layer}_{model_kind.lower()}, + kind {model_kind}, + grants ( + {grants_config} + ), + grants_target_layer '{grants_target_layer}' + ); + SELECT 1 as id, '{grants_target_layer}_{model_kind}' as test_type + """ + ( + tmp_path / "models" / f"vde_model_{grants_target_layer}_{model_kind.lower()}.sql" + ).write_text(model_def) + + context = ctx.create_context(path=tmp_path, config_mutator=_vde_dev_only_config) + context.plan("prod", auto_apply=True, no_prompts=True) + + table_name = f"test_schema.vde_model_{grants_target_layer}_{model_kind.lower()}" + + # In VDE dev_only mode, VIEWs are created as actual views + assert context.engine_adapter.table_exists(table_name) + + grants = engine_adapter._get_current_grants_config( + exp.to_table(table_name, dialect=engine_adapter.dialect) + ) + assert roles["reader"]["username"] in grants.get("SELECT", []) + assert roles["writer"]["username"] in grants.get("SELECT", []) + + if model_kind != "VIEW": + assert roles["writer"]["username"] in grants.get("INSERT", []) + + +def test_grants_incremental_model_with_vde_dev_only( + engine_adapter: PostgresEngineAdapter, ctx: TestContext, tmp_path: Path +): + with create_users(engine_adapter, "etl", "analyst") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + + model_def = f""" + MODEL ( + name test_schema.vde_incremental_model, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column event_date + ), + grants ( + 'SELECT' = ['{roles["analyst"]["username"]}'], + 'INSERT' = ['{roles["etl"]["username"]}'] + ), + grants_target_layer 'virtual' + ); + SELECT + 1 as id, + @start_date::date as event_date, + 'event' as event_type + """ + (tmp_path / "models" / "vde_incremental_model.sql").write_text(model_def) + + context = ctx.create_context(path=tmp_path, config_mutator=_vde_dev_only_config) + context.plan("prod", auto_apply=True, no_prompts=True) + + prod_table = "test_schema.vde_incremental_model" + prod_grants = engine_adapter._get_current_grants_config( + exp.to_table(prod_table, dialect=engine_adapter.dialect) + ) + assert roles["analyst"]["username"] in prod_grants.get("SELECT", []) + assert roles["etl"]["username"] in prod_grants.get("INSERT", []) + + +@pytest.mark.parametrize( + "change_type,initial_query,updated_query,expect_schema_change", + [ + # Metadata-only change (grants only) + ( + "metadata_only", + "SELECT 1 as id, 'same' as status", + "SELECT 1 as id, 'same' as status", + False, + ), + # Breaking change only + ( + "breaking_only", + "SELECT 1 as id, 'initial' as status, 100 as amount", + "SELECT 1 as id, 'updated' as status", # Removed column + True, + ), + # Both metadata and breaking changes + ( + "metadata_and_breaking", + "SELECT 1 as id, 'initial' as status, 100 as amount", + "SELECT 2 as id, 'changed' as new_status", # Different schema + True, + ), + ], +) +def test_grants_changes_with_vde_dev_only( + engine_adapter: PostgresEngineAdapter, + ctx: TestContext, + tmp_path: Path, + change_type: str, + initial_query: str, + updated_query: str, + expect_schema_change: bool, +): + with create_users(engine_adapter, "user1", "user2", "user3") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + model_path = tmp_path / "models" / f"vde_changes_{change_type}.sql" + + initial_model = f""" + MODEL ( + name test_schema.vde_changes_{change_type}, + kind FULL, + grants ( + 'SELECT' = ['{roles["user1"]["username"]}'] + ), + grants_target_layer 'virtual' + ); + {initial_query} + """ + model_path.write_text(initial_model) + + context = ctx.create_context(path=tmp_path, config_mutator=_vde_dev_only_config) + context.plan("prod", auto_apply=True, no_prompts=True) + + table_name = f"test_schema.vde_changes_{change_type}" + initial_grants = engine_adapter._get_current_grants_config( + exp.to_table(table_name, dialect=engine_adapter.dialect) + ) + assert roles["user1"]["username"] in initial_grants.get("SELECT", []) + assert roles["user2"]["username"] not in initial_grants.get("SELECT", []) + + # Update model with new grants and potentially new query + updated_model = f""" + MODEL ( + name test_schema.vde_changes_{change_type}, + kind FULL, + grants ( + 'SELECT' = ['{roles["user1"]["username"]}', '{roles["user2"]["username"]}', '{roles["user3"]["username"]}'], + 'INSERT' = ['{roles["user3"]["username"]}'] + ), + grants_target_layer 'virtual' + ); + {updated_query} + """ + model_path.write_text(updated_model) + + # Get initial table columns + initial_columns = set( + col[0] + for col in engine_adapter.fetchall( + f"SELECT column_name FROM information_schema.columns WHERE table_schema = 'test_schema' AND table_name = 'vde_changes_{change_type}'" + ) + ) + + context.load() + plan = context.plan("prod", auto_apply=True, no_prompts=True) + + assert len(plan.new_snapshots) == 1 + + current_columns = set( + col[0] + for col in engine_adapter.fetchall( + f"SELECT column_name FROM information_schema.columns WHERE table_schema = 'test_schema' AND table_name = 'vde_changes_{change_type}'" + ) + ) + + if expect_schema_change: + assert current_columns != initial_columns + else: + # For metadata-only changes, schema should be the same + assert current_columns == initial_columns + + # Grants should be updated in all cases + updated_grants = engine_adapter._get_current_grants_config( + exp.to_table(table_name, dialect=engine_adapter.dialect) + ) + assert roles["user1"]["username"] in updated_grants.get("SELECT", []) + assert roles["user2"]["username"] in updated_grants.get("SELECT", []) + assert roles["user3"]["username"] in updated_grants.get("SELECT", []) + assert roles["user3"]["username"] in updated_grants.get("INSERT", []) + + +@pytest.mark.parametrize( + "grants_target_layer,environment", + [ + ("virtual", "prod"), + ("virtual", "dev"), + ("physical", "prod"), + ("physical", "staging"), + ("all", "prod"), + ("all", "preview"), + ], +) +def test_grants_target_layer_plan_env_with_vde_dev_only( + engine_adapter: PostgresEngineAdapter, + ctx: TestContext, + tmp_path: Path, + grants_target_layer: str, + environment: str, +): + with create_users(engine_adapter, "grantee") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + + model_def = f""" + MODEL ( + name test_schema.vde_layer_model, + kind FULL, + grants ( + 'SELECT' = ['{roles["grantee"]["username"]}'] + ), + grants_target_layer '{grants_target_layer}' + ); + SELECT 1 as id, '{environment}' as env, '{grants_target_layer}' as layer + """ + (tmp_path / "models" / "vde_layer_model.sql").write_text(model_def) + + context = ctx.create_context(path=tmp_path, config_mutator=_vde_dev_only_config) + + if environment == "prod": + context.plan("prod", auto_apply=True, no_prompts=True) + table_name = "test_schema.vde_layer_model" + grants = engine_adapter._get_current_grants_config( + exp.to_table(table_name, dialect=engine_adapter.dialect) + ) + assert roles["grantee"]["username"] in grants.get("SELECT", []) + else: + context.plan(environment, auto_apply=True, no_prompts=True, include_unmodified=True) + virtual_view = f"test_schema__{environment}.vde_layer_model" + assert context.engine_adapter.table_exists(virtual_view) + virtual_grants = engine_adapter._get_current_grants_config( + exp.to_table(virtual_view, dialect=engine_adapter.dialect) + ) + + data_objects = engine_adapter.get_data_objects("sqlmesh__test_schema") + physical_tables = [ + obj + for obj in data_objects + if "vde_layer_model" in obj.name + and obj.name.endswith("__dev") # Always __dev suffix in VDE dev_only + and "TABLE" in str(obj.type).upper() + ] + + if grants_target_layer == "virtual": + # Virtual layer should have grants, physical should not + assert roles["grantee"]["username"] in virtual_grants.get("SELECT", []) + + assert len(physical_tables) > 0 + for physical_table in physical_tables: + physical_table_name = f"sqlmesh__test_schema.{physical_table.name}" + physical_grants = engine_adapter._get_current_grants_config( + exp.to_table(physical_table_name, dialect=engine_adapter.dialect) + ) + assert roles["grantee"]["username"] not in physical_grants.get("SELECT", []) + + elif grants_target_layer == "physical": + # Virtual layer should not have grants, physical should + assert roles["grantee"]["username"] not in virtual_grants.get("SELECT", []) + + assert len(physical_tables) > 0 + for physical_table in physical_tables: + physical_table_name = f"sqlmesh__test_schema.{physical_table.name}" + physical_grants = engine_adapter._get_current_grants_config( + exp.to_table(physical_table_name, dialect=engine_adapter.dialect) + ) + assert roles["grantee"]["username"] in physical_grants.get("SELECT", []) + + else: # grants_target_layer == "all" + # Both layers should have grants + assert roles["grantee"]["username"] in virtual_grants.get("SELECT", []) + assert len(physical_tables) > 0 + for physical_table in physical_tables: + physical_table_name = f"sqlmesh__test_schema.{physical_table.name}" + physical_grants = engine_adapter._get_current_grants_config( + exp.to_table(physical_table_name, dialect=engine_adapter.dialect) + ) + assert roles["grantee"]["username"] in physical_grants.get("SELECT", []) + + +@pytest.mark.parametrize( + "model_kind", + [ + "SCD_TYPE_2", + "SCD_TYPE_2_BY_TIME", + ], +) +def test_grants_plan_scd_type_2_models( + engine_adapter: PostgresEngineAdapter, + ctx: TestContext, + tmp_path: Path, + model_kind: str, +): + with create_users(engine_adapter, "reader", "writer", "analyst") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + model_name = "scd_model" + + kind_config = f"{model_kind} (unique_key [id])" + model_definition = f""" + MODEL ( + name test_schema.{model_name}, + kind {kind_config}, + grants ( + 'SELECT' = ['{roles["reader"]["username"]}'], + 'INSERT' = ['{roles["writer"]["username"]}'] + ), + grants_target_layer 'all' + ); + SELECT 1 as id, 'initial_data' as name, CURRENT_TIMESTAMP as updated_at + """ + (tmp_path / "models" / f"{model_name}.sql").write_text(model_definition) + + context = ctx.create_context(path=tmp_path) + plan_result = context.plan( + "dev", start="2023-01-01", end="2023-01-01", auto_apply=True, no_prompts=True + ) + assert len(plan_result.new_snapshots) == 1 + + current_snapshot = plan_result.new_snapshots[0] + fingerprint_version = current_snapshot.fingerprint.to_version() + physical_table_name = ( + f"sqlmesh__test_schema.test_schema__{model_name}__{fingerprint_version}__dev" + ) + physical_grants = engine_adapter._get_current_grants_config( + exp.to_table(physical_table_name, dialect=engine_adapter.dialect) + ) + assert physical_grants.get("SELECT", []) == [roles["reader"]["username"]] + assert physical_grants.get("INSERT", []) == [roles["writer"]["username"]] + + view_name = f"test_schema__dev.{model_name}" + view_grants = engine_adapter._get_current_grants_config( + exp.to_table(view_name, dialect=engine_adapter.dialect) + ) + assert view_grants.get("SELECT", []) == [roles["reader"]["username"]] + assert view_grants.get("INSERT", []) == [roles["writer"]["username"]] + + # Data change + updated_model_definition = f""" + MODEL ( + name test_schema.{model_name}, + kind {kind_config}, + grants ( + 'SELECT' = ['{roles["reader"]["username"]}'], + 'INSERT' = ['{roles["writer"]["username"]}'] + ), + grants_target_layer 'all' + ); + SELECT 1 as id, 'updated_data' as name, CURRENT_TIMESTAMP as updated_at + """ + (tmp_path / "models" / f"{model_name}.sql").write_text(updated_model_definition) + + context.load() + context.plan("dev", start="2023-01-02", end="2023-01-02", auto_apply=True, no_prompts=True) + + snapshot = context.get_snapshot(f"test_schema.{model_name}") + assert snapshot + fingerprint = snapshot.fingerprint.to_version() + table_name = f"sqlmesh__test_schema.test_schema__{model_name}__{fingerprint}__dev" + data_change_grants = engine_adapter._get_current_grants_config( + exp.to_table(table_name, dialect=engine_adapter.dialect) + ) + assert data_change_grants.get("SELECT", []) == [roles["reader"]["username"]] + assert data_change_grants.get("INSERT", []) == [roles["writer"]["username"]] + + # Data + grants changes + grant_change_model_definition = f""" + MODEL ( + name test_schema.{model_name}, + kind {kind_config}, + grants ( + 'SELECT' = ['{roles["reader"]["username"]}', '{roles["analyst"]["username"]}'], + 'INSERT' = ['{roles["writer"]["username"]}'], + 'UPDATE' = ['{roles["analyst"]["username"]}'] + ), + grants_target_layer 'all' + ); + SELECT 1 as id, 'grant_changed_data' as name, CURRENT_TIMESTAMP as updated_at + """ + (tmp_path / "models" / f"{model_name}.sql").write_text(grant_change_model_definition) + + context.load() + context.plan("dev", start="2023-01-03", end="2023-01-03", auto_apply=True, no_prompts=True) + + snapshot = context.get_snapshot(f"test_schema.{model_name}") + assert snapshot + fingerprint = snapshot.fingerprint.to_version() + table_name = f"sqlmesh__test_schema.test_schema__{model_name}__{fingerprint}__dev" + final_grants = engine_adapter._get_current_grants_config( + exp.to_table(table_name, dialect=engine_adapter.dialect) + ) + expected_select_users = {roles["reader"]["username"], roles["analyst"]["username"]} + assert set(final_grants.get("SELECT", [])) == expected_select_users + assert final_grants.get("INSERT", []) == [roles["writer"]["username"]] + assert final_grants.get("UPDATE", []) == [roles["analyst"]["username"]] + + final_view_grants = engine_adapter._get_current_grants_config( + exp.to_table(view_name, dialect=engine_adapter.dialect) + ) + assert set(final_view_grants.get("SELECT", [])) == expected_select_users + assert final_view_grants.get("INSERT", []) == [roles["writer"]["username"]] + assert final_view_grants.get("UPDATE", []) == [roles["analyst"]["username"]] + + +@pytest.mark.parametrize( + "model_kind", + [ + "SCD_TYPE_2", + "SCD_TYPE_2_BY_TIME", + ], +) +def test_grants_plan_scd_type_2_with_vde_dev_only( + engine_adapter: PostgresEngineAdapter, + ctx: TestContext, + tmp_path: Path, + model_kind: str, +): + with create_users(engine_adapter, "etl_user", "analyst") as roles: + (tmp_path / "models").mkdir(exist_ok=True) + model_name = "vde_scd_model" + + model_def = f""" + MODEL ( + name test_schema.{model_name}, + kind {model_kind} (unique_key [customer_id]), + grants ( + 'SELECT' = ['{roles["analyst"]["username"]}'], + 'INSERT' = ['{roles["etl_user"]["username"]}'] + ), + grants_target_layer 'all' + ); + SELECT + 1 as customer_id, + 'active' as status, + CURRENT_TIMESTAMP as updated_at + """ + (tmp_path / "models" / f"{model_name}.sql").write_text(model_def) + + context = ctx.create_context(path=tmp_path, config_mutator=_vde_dev_only_config) + + # Prod + context.plan("prod", auto_apply=True, no_prompts=True) + prod_table = f"test_schema.{model_name}" + prod_grants = engine_adapter._get_current_grants_config( + exp.to_table(prod_table, dialect=engine_adapter.dialect) + ) + assert roles["analyst"]["username"] in prod_grants.get("SELECT", []) + assert roles["etl_user"]["username"] in prod_grants.get("INSERT", []) + + # Dev + context.plan("dev", auto_apply=True, no_prompts=True, include_unmodified=True) + dev_view = f"test_schema__dev.{model_name}" + dev_grants = engine_adapter._get_current_grants_config( + exp.to_table(dev_view, dialect=engine_adapter.dialect) + ) + assert roles["analyst"]["username"] in dev_grants.get("SELECT", []) + assert roles["etl_user"]["username"] in dev_grants.get("INSERT", []) + + snapshot = context.get_snapshot(f"test_schema.{model_name}") + assert snapshot + fingerprint_version = snapshot.fingerprint.to_version() + dev_physical_table_name = ( + f"sqlmesh__test_schema.test_schema__{model_name}__{fingerprint_version}__dev" + ) + + dev_physical_grants = engine_adapter._get_current_grants_config( + exp.to_table(dev_physical_table_name, dialect=engine_adapter.dialect) + ) + assert roles["analyst"]["username"] in dev_physical_grants.get("SELECT", []) + assert roles["etl_user"]["username"] in dev_physical_grants.get("INSERT", []) diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py index ba775c0779..2b9bcc665f 100644 --- a/tests/core/engine_adapter/test_base.py +++ b/tests/core/engine_adapter/test_base.py @@ -4065,3 +4065,108 @@ def test_data_object_cache_cleared_on_create_table_like( assert result is not None assert result.name == "target_table" assert mock_get_data_objects.call_count == 2 + + +def test_diff_grants_configs(): + new = {"SELECT": ["u1", "u2"], "INSERT": ["u1"]} + old = {"SELECT": ["u1", "u3"], "update": ["u1"]} + + additions, removals = EngineAdapter._diff_grants_configs(new, old) + + assert additions.get("SELECT") and set(additions["SELECT"]) == {"u2"} + assert removals.get("SELECT") and set(removals["SELECT"]) == {"u3"} + + assert additions.get("INSERT") and set(additions["INSERT"]) == {"u1"} + assert removals.get("update") and set(removals["update"]) == {"u1"} + + for perm, grantees in additions.items(): + assert set(grantees).isdisjoint(set(old.get(perm, []))) + for perm, grantees in removals.items(): + assert set(grantees).isdisjoint(set(new.get(perm, []))) + + +def test_diff_grants_configs_empty_new(): + new = {} + old = {"SELECT": ["u1", "u2"], "INSERT": ["u3"]} + + additions, removals = EngineAdapter._diff_grants_configs(new, old) + + assert additions == {} + assert removals == old + + +def test_diff_grants_configs_empty_old(): + new = {"SELECT": ["u1", "u2"], "INSERT": ["u3"]} + old = {} + + additions, removals = EngineAdapter._diff_grants_configs(new, old) + + assert additions == new + assert removals == {} + + +def test_diff_grants_configs_identical(): + grants = {"SELECT": ["u1", "u2"], "INSERT": ["u3"]} + + additions, removals = EngineAdapter._diff_grants_configs(grants, grants) + + assert additions == {} + assert removals == {} + + +def test_diff_grants_configs_none_configs(): + grants = {"SELECT": ["u1"]} + + additions, removals = EngineAdapter._diff_grants_configs(grants, {}) + assert additions == grants + assert removals == {} + + additions, removals = EngineAdapter._diff_grants_configs({}, grants) + assert additions == {} + assert removals == grants + + additions, removals = EngineAdapter._diff_grants_configs({}, {}) + assert additions == {} + assert removals == {} + + +def test_diff_grants_configs_duplicate_grantees(): + new = {"SELECT": ["u1", "u2", "u1"]} + old = {"SELECT": ["u2", "u3", "u2"]} + + additions, removals = EngineAdapter._diff_grants_configs(new, old) + + assert additions["SELECT"] == ["u1", "u1"] + assert removals["SELECT"] == ["u3"] + + +def test_diff_grants_configs_case_sensitive(): + new = {"select": ["u1"], "SELECT": ["u2"]} + old = {"Select": ["u3"]} + + additions, removals = EngineAdapter._diff_grants_configs(new, old) + + assert set(additions.keys()) == {"select", "SELECT"} + assert set(removals.keys()) == {"Select"} + assert additions["select"] == ["u1"] + assert additions["SELECT"] == ["u2"] + assert removals["Select"] == ["u3"] + + +def test_sync_grants_config_unsupported_engine(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + adapter.SUPPORTS_GRANTS = False + + relation = exp.to_table("test_table") + grants_config = {"SELECT": ["user1"]} + + with pytest.raises(NotImplementedError, match="Engine does not support grants"): + adapter.sync_grants_config(relation, grants_config) + + +def test_get_current_grants_config_not_implemented(make_mocked_engine_adapter: t.Callable): + adapter = make_mocked_engine_adapter(EngineAdapter) + relation = exp.to_table("test_table") + + with pytest.raises(NotImplementedError): + adapter._get_current_grants_config(relation) diff --git a/tests/core/engine_adapter/test_base_postgres.py b/tests/core/engine_adapter/test_base_postgres.py index df280a9059..f286c47c56 100644 --- a/tests/core/engine_adapter/test_base_postgres.py +++ b/tests/core/engine_adapter/test_base_postgres.py @@ -3,6 +3,7 @@ from unittest.mock import call import pytest +from pytest_mock.plugin import MockerFixture from sqlglot import exp, parse_one from sqlmesh.core.engine_adapter.base_postgres import BasePostgresEngineAdapter @@ -75,3 +76,26 @@ def test_drop_view(make_mocked_engine_adapter: t.Callable): call('DROP VIEW IF EXISTS "db"."view"'), ] ) + + +def test_get_current_schema(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): + adapter = make_mocked_engine_adapter(BasePostgresEngineAdapter) + + fetchone_mock = mocker.patch.object(adapter, "fetchone", return_value=("test_schema",)) + result = adapter._get_current_schema() + + assert result == "test_schema" + fetchone_mock.assert_called_once() + executed_query = fetchone_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="postgres") + assert executed_sql == "SELECT CURRENT_SCHEMA" + + fetchone_mock.reset_mock() + fetchone_mock.return_value = None + result = adapter._get_current_schema() + assert result == "public" + + fetchone_mock.reset_mock() + fetchone_mock.return_value = (None,) # search_path = '' or 'nonexistent_schema' + result = adapter._get_current_schema() + assert result == "public" diff --git a/tests/core/engine_adapter/test_bigquery.py b/tests/core/engine_adapter/test_bigquery.py index f195bbaa2a..047613e47a 100644 --- a/tests/core/engine_adapter/test_bigquery.py +++ b/tests/core/engine_adapter/test_bigquery.py @@ -13,6 +13,7 @@ import sqlmesh.core.dialect as d from sqlmesh.core.engine_adapter import BigQueryEngineAdapter from sqlmesh.core.engine_adapter.bigquery import select_partitions_expr +from sqlmesh.core.engine_adapter.shared import DataObjectType from sqlmesh.core.node import IntervalUnit from sqlmesh.utils import AttributeDict from sqlmesh.utils.errors import SQLMeshError @@ -588,13 +589,14 @@ def _to_sql_calls(execute_mock: t.Any, identify: bool = True) -> t.List[str]: execute_mock = execute_mock.execute output = [] for call in execute_mock.call_args_list: - value = call[0][0] - sql = ( - value.sql(dialect="bigquery", identify=identify) - if isinstance(value, exp.Expression) - else str(value) - ) - output.append(sql) + values = ensure_list(call[0][0]) + for value in values: + sql = ( + value.sql(dialect="bigquery", identify=identify) + if isinstance(value, exp.Expression) + else str(value) + ) + output.append(sql) return output @@ -1213,3 +1215,168 @@ def test_scd_type_2_by_partitioning(adapter: BigQueryEngineAdapter): # Both calls should contain the partition logic (the scd logic is already covered by other tests) assert "PARTITION BY TIMESTAMP_TRUNC(`valid_from`, DAY)" in calls[0] assert "PARTITION BY TIMESTAMP_TRUNC(`valid_from`, DAY)" in calls[1] + + +def test_sync_grants_config(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): + adapter = make_mocked_engine_adapter(BigQueryEngineAdapter) + relation = exp.to_table("project.dataset.test_table", dialect="bigquery") + new_grants_config = { + "roles/bigquery.dataViewer": ["user:analyst@example.com", "group:data-team@example.com"], + "roles/bigquery.dataEditor": ["user:admin@example.com"], + } + current_grants = [ + ("roles/bigquery.dataViewer", "user:old_analyst@example.com"), + ("roles/bigquery.admin", "user:old_admin@example.com"), + ] + + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + execute_mock = mocker.patch.object(adapter, "execute") + mocker.patch.object(adapter, "get_current_catalog", return_value="project") + mocker.patch.object(adapter.client, "location", "us-central1") + + mock_dataset = mocker.Mock() + mock_dataset.location = "us-central1" + mocker.patch.object(adapter, "_db_call", return_value=mock_dataset) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="bigquery") + expected_sql = ( + "SELECT privilege_type, grantee FROM `project`.`region-us-central1`.`INFORMATION_SCHEMA.OBJECT_PRIVILEGES` AS OBJECT_PRIVILEGES " + "WHERE object_schema = 'dataset' AND object_name = 'test_table' AND SPLIT(grantee, ':')[OFFSET(1)] <> session_user()" + ) + assert executed_sql == expected_sql + + sql_calls = _to_sql_calls(execute_mock) + + assert len(sql_calls) == 4 + assert ( + "REVOKE `roles/bigquery.dataViewer` ON TABLE `project`.`dataset`.`test_table` FROM 'user:old_analyst@example.com'" + in sql_calls + ) + assert ( + "REVOKE `roles/bigquery.admin` ON TABLE `project`.`dataset`.`test_table` FROM 'user:old_admin@example.com'" + in sql_calls + ) + assert ( + "GRANT `roles/bigquery.dataViewer` ON TABLE `project`.`dataset`.`test_table` TO 'user:analyst@example.com', 'group:data-team@example.com'" + in sql_calls + ) + assert ( + "GRANT `roles/bigquery.dataEditor` ON TABLE `project`.`dataset`.`test_table` TO 'user:admin@example.com'" + in sql_calls + ) + + +def test_sync_grants_config_with_overlaps( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(BigQueryEngineAdapter) + relation = exp.to_table("project.dataset.test_table", dialect="bigquery") + new_grants_config = { + "roles/bigquery.dataViewer": [ + "user:analyst1@example.com", + "user:analyst2@example.com", + "user:analyst3@example.com", + ], + "roles/bigquery.dataEditor": ["user:analyst2@example.com", "user:editor@example.com"], + } + current_grants = [ + ("roles/bigquery.dataViewer", "user:analyst1@example.com"), # Keep + ("roles/bigquery.dataViewer", "user:old_analyst@example.com"), # Remove + ("roles/bigquery.dataEditor", "user:analyst2@example.com"), # Keep + ("roles/bigquery.admin", "user:admin@example.com"), # Remove + ] + + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + execute_mock = mocker.patch.object(adapter, "execute") + mocker.patch.object(adapter, "get_current_catalog", return_value="project") + mocker.patch.object(adapter.client, "location", "us-central1") + + mock_dataset = mocker.Mock() + mock_dataset.location = "us-central1" + mocker.patch.object(adapter, "_db_call", return_value=mock_dataset) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="bigquery") + expected_sql = ( + "SELECT privilege_type, grantee FROM `project`.`region-us-central1`.`INFORMATION_SCHEMA.OBJECT_PRIVILEGES` AS OBJECT_PRIVILEGES " + "WHERE object_schema = 'dataset' AND object_name = 'test_table' AND SPLIT(grantee, ':')[OFFSET(1)] <> session_user()" + ) + assert executed_sql == expected_sql + + sql_calls = _to_sql_calls(execute_mock) + + assert len(sql_calls) == 4 + assert ( + "REVOKE `roles/bigquery.dataViewer` ON TABLE `project`.`dataset`.`test_table` FROM 'user:old_analyst@example.com'" + in sql_calls + ) + assert ( + "REVOKE `roles/bigquery.admin` ON TABLE `project`.`dataset`.`test_table` FROM 'user:admin@example.com'" + in sql_calls + ) + assert ( + "GRANT `roles/bigquery.dataViewer` ON TABLE `project`.`dataset`.`test_table` TO 'user:analyst2@example.com', 'user:analyst3@example.com'" + in sql_calls + ) + assert ( + "GRANT `roles/bigquery.dataEditor` ON TABLE `project`.`dataset`.`test_table` TO 'user:editor@example.com'" + in sql_calls + ) + + +@pytest.mark.parametrize( + "table_type, expected_keyword", + [ + (DataObjectType.TABLE, "TABLE"), + (DataObjectType.VIEW, "VIEW"), + (DataObjectType.MATERIALIZED_VIEW, "MATERIALIZED VIEW"), + ], +) +def test_sync_grants_config_object_kind( + make_mocked_engine_adapter: t.Callable, + mocker: MockerFixture, + table_type: DataObjectType, + expected_keyword: str, +) -> None: + adapter = make_mocked_engine_adapter(BigQueryEngineAdapter) + relation = exp.to_table("project.dataset.test_object", dialect="bigquery") + + mocker.patch.object(adapter, "fetchall", return_value=[]) + execute_mock = mocker.patch.object(adapter, "execute") + mocker.patch.object(adapter, "get_current_catalog", return_value="project") + mocker.patch.object(adapter.client, "location", "us-central1") + + mock_dataset = mocker.Mock() + mock_dataset.location = "us-central1" + mocker.patch.object(adapter, "_db_call", return_value=mock_dataset) + + adapter.sync_grants_config( + relation, {"roles/bigquery.dataViewer": ["user:test@example.com"]}, table_type + ) + + executed_exprs = execute_mock.call_args[0][0] + sql_calls = [expr.sql(dialect="bigquery") for expr in executed_exprs] + assert sql_calls == [ + f"GRANT `roles/bigquery.dataViewer` ON {expected_keyword} project.dataset.test_object TO 'user:test@example.com'" + ] + + +def test_sync_grants_config_no_schema( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(BigQueryEngineAdapter) + relation = exp.to_table("test_table", dialect="bigquery") + new_grants_config = { + "roles/bigquery.dataViewer": ["user:analyst@example.com"], + "roles/bigquery.dataEditor": ["user:editor@example.com"], + } + + with pytest.raises(ValueError, match="Table test_table does not have a schema \\(dataset\\)"): + adapter.sync_grants_config(relation, new_grants_config) diff --git a/tests/core/engine_adapter/test_databricks.py b/tests/core/engine_adapter/test_databricks.py index 27988fed39..e4512f11c9 100644 --- a/tests/core/engine_adapter/test_databricks.py +++ b/tests/core/engine_adapter/test_databricks.py @@ -128,17 +128,194 @@ def test_get_current_catalog(mocker: MockFixture, make_mocked_engine_adapter: t. assert to_sql_calls(adapter) == ["SELECT CURRENT_CATALOG()"] -def test_get_current_database(mocker: MockFixture, make_mocked_engine_adapter: t.Callable): +def test_get_current_schema(mocker: MockFixture, make_mocked_engine_adapter: t.Callable): mocker.patch( "sqlmesh.core.engine_adapter.databricks.DatabricksEngineAdapter.set_current_catalog" ) adapter = make_mocked_engine_adapter(DatabricksEngineAdapter, default_catalog="test_catalog") adapter.cursor.fetchone.return_value = ("test_database",) - assert adapter.get_current_database() == "test_database" + assert adapter._get_current_schema() == "test_database" assert to_sql_calls(adapter) == ["SELECT CURRENT_DATABASE()"] +def test_sync_grants_config(make_mocked_engine_adapter: t.Callable, mocker: MockFixture): + adapter = make_mocked_engine_adapter(DatabricksEngineAdapter, default_catalog="main") + relation = exp.to_table("main.test_schema.test_table", dialect="databricks") + new_grants_config = { + "SELECT": ["group1", "group2"], + "MODIFY": ["writers"], + } + + current_grants = [ + ("SELECT", "legacy"), + ("REFRESH", "stale"), + ] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="databricks") + expected_sql = ( + "SELECT privilege_type, grantee FROM main.information_schema.table_privileges " + "WHERE table_catalog = 'main' AND table_schema = 'test_schema' AND table_name = 'test_table' " + "AND grantor = CURRENT_USER() AND grantee <> CURRENT_USER() AND inherited_from = 'NONE'" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 5 + + assert "GRANT SELECT ON TABLE `main`.`test_schema`.`test_table` TO `group1`" in sql_calls + assert "GRANT SELECT ON TABLE `main`.`test_schema`.`test_table` TO `group2`" in sql_calls + assert "GRANT MODIFY ON TABLE `main`.`test_schema`.`test_table` TO `writers`" in sql_calls + assert "REVOKE SELECT ON TABLE `main`.`test_schema`.`test_table` FROM `legacy`" in sql_calls + assert "REVOKE REFRESH ON TABLE `main`.`test_schema`.`test_table` FROM `stale`" in sql_calls + + +def test_sync_grants_config_with_overlaps( + make_mocked_engine_adapter: t.Callable, mocker: MockFixture +): + adapter = make_mocked_engine_adapter(DatabricksEngineAdapter, default_catalog="main") + relation = exp.to_table("main.test_schema.test_table", dialect="databricks") + new_grants_config = { + "SELECT": ["shared", "new_role"], + "MODIFY": ["shared", "writer"], + } + + current_grants = [ + ("SELECT", "shared"), + ("SELECT", "legacy"), + ("MODIFY", "shared"), + ] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="databricks") + expected_sql = ( + "SELECT privilege_type, grantee FROM main.information_schema.table_privileges " + "WHERE table_catalog = 'main' AND table_schema = 'test_schema' AND table_name = 'test_table' " + "AND grantor = CURRENT_USER() AND grantee <> CURRENT_USER() AND inherited_from = 'NONE'" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 3 + + assert "GRANT SELECT ON TABLE `main`.`test_schema`.`test_table` TO `new_role`" in sql_calls + assert "GRANT MODIFY ON TABLE `main`.`test_schema`.`test_table` TO `writer`" in sql_calls + assert "REVOKE SELECT ON TABLE `main`.`test_schema`.`test_table` FROM `legacy`" in sql_calls + + +@pytest.mark.parametrize( + "table_type, expected_keyword", + [ + (DataObjectType.TABLE, "TABLE"), + (DataObjectType.VIEW, "VIEW"), + (DataObjectType.MATERIALIZED_VIEW, "MATERIALIZED VIEW"), + (DataObjectType.MANAGED_TABLE, "TABLE"), + ], +) +def test_sync_grants_config_object_kind( + make_mocked_engine_adapter: t.Callable, + mocker: MockFixture, + table_type: DataObjectType, + expected_keyword: str, +) -> None: + adapter = make_mocked_engine_adapter(DatabricksEngineAdapter, default_catalog="main") + relation = exp.to_table("main.test_schema.test_object", dialect="databricks") + + mocker.patch.object(adapter, "fetchall", return_value=[]) + + adapter.sync_grants_config(relation, {"SELECT": ["test"]}, table_type) + + sql_calls = to_sql_calls(adapter) + assert sql_calls == [ + f"GRANT SELECT ON {expected_keyword} `main`.`test_schema`.`test_object` TO `test`" + ] + + +def test_sync_grants_config_quotes(make_mocked_engine_adapter: t.Callable, mocker: MockFixture): + adapter = make_mocked_engine_adapter(DatabricksEngineAdapter, default_catalog="`test_db`") + relation = exp.to_table("`test_db`.`test_schema`.`test_table`", dialect="databricks") + new_grants_config = { + "SELECT": ["group1", "group2"], + "MODIFY": ["writers"], + } + + current_grants = [ + ("SELECT", "legacy"), + ("REFRESH", "stale"), + ] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="databricks") + expected_sql = ( + "SELECT privilege_type, grantee FROM `test_db`.information_schema.table_privileges " + "WHERE table_catalog = 'test_db' AND table_schema = 'test_schema' AND table_name = 'test_table' " + "AND grantor = CURRENT_USER() AND grantee <> CURRENT_USER() AND inherited_from = 'NONE'" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 5 + + assert "GRANT SELECT ON TABLE `test_db`.`test_schema`.`test_table` TO `group1`" in sql_calls + assert "GRANT SELECT ON TABLE `test_db`.`test_schema`.`test_table` TO `group2`" in sql_calls + assert "GRANT MODIFY ON TABLE `test_db`.`test_schema`.`test_table` TO `writers`" in sql_calls + assert "REVOKE SELECT ON TABLE `test_db`.`test_schema`.`test_table` FROM `legacy`" in sql_calls + assert "REVOKE REFRESH ON TABLE `test_db`.`test_schema`.`test_table` FROM `stale`" in sql_calls + + +def test_sync_grants_config_no_catalog_or_schema( + make_mocked_engine_adapter: t.Callable, mocker: MockFixture +): + adapter = make_mocked_engine_adapter(DatabricksEngineAdapter, default_catalog="main_catalog") + relation = exp.to_table("test_table", dialect="databricks") + new_grants_config = { + "SELECT": ["group1", "group2"], + "MODIFY": ["writers"], + } + + current_grants = [ + ("SELECT", "legacy"), + ("REFRESH", "stale"), + ] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + mocker.patch.object(adapter, "_get_current_schema", return_value="schema") + mocker.patch.object(adapter, "get_current_catalog", return_value="main_catalog") + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="databricks") + expected_sql = ( + "SELECT privilege_type, grantee FROM `main_catalog`.information_schema.table_privileges " + "WHERE table_catalog = 'main_catalog' AND table_schema = 'schema' AND table_name = 'test_table' " + "AND grantor = CURRENT_USER() AND grantee <> CURRENT_USER() AND inherited_from = 'NONE'" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 5 + + assert "GRANT SELECT ON TABLE `test_table` TO `group1`" in sql_calls + assert "GRANT SELECT ON TABLE `test_table` TO `group2`" in sql_calls + assert "GRANT MODIFY ON TABLE `test_table` TO `writers`" in sql_calls + assert "REVOKE SELECT ON TABLE `test_table` FROM `legacy`" in sql_calls + assert "REVOKE REFRESH ON TABLE `test_table` FROM `stale`" in sql_calls + + def test_insert_overwrite_by_partition_query( make_mocked_engine_adapter: t.Callable, mocker: MockFixture, make_temp_table_name: t.Callable ): diff --git a/tests/core/engine_adapter/test_postgres.py b/tests/core/engine_adapter/test_postgres.py index 6134126a41..ebcdd03f55 100644 --- a/tests/core/engine_adapter/test_postgres.py +++ b/tests/core/engine_adapter/test_postgres.py @@ -177,3 +177,108 @@ def test_server_version(make_mocked_engine_adapter: t.Callable, mocker: MockerFi del adapter.server_version fetchone_mock.return_value = ("15.13 (Debian 15.13-1.pgdg120+1)",) assert adapter.server_version == (15, 13) + + +def test_sync_grants_config(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): + adapter = make_mocked_engine_adapter(PostgresEngineAdapter) + relation = exp.to_table("test_schema.test_table", dialect="postgres") + new_grants_config = {"SELECT": ["user1", "user2"], "INSERT": ["user3"]} + + current_grants = [("SELECT", "old_user"), ("UPDATE", "admin_user")] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="postgres") + + assert executed_sql == ( + "SELECT privilege_type, grantee FROM information_schema.role_table_grants " + "WHERE table_schema = 'test_schema' AND table_name = 'test_table' " + "AND grantor = current_role AND grantee <> current_role" + ) + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 4 + + assert 'GRANT SELECT ON "test_schema"."test_table" TO "user1", "user2"' in sql_calls + assert 'GRANT INSERT ON "test_schema"."test_table" TO "user3"' in sql_calls + assert 'REVOKE SELECT ON "test_schema"."test_table" FROM "old_user"' in sql_calls + assert 'REVOKE UPDATE ON "test_schema"."test_table" FROM "admin_user"' in sql_calls + + +def test_sync_grants_config_with_overlaps( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(PostgresEngineAdapter) + relation = exp.to_table("test_schema.test_table", dialect="postgres") + new_grants_config = {"SELECT": ["user1", "user2", "user3"], "INSERT": ["user2", "user4"]} + + current_grants = [ + ("SELECT", "user1"), + ("SELECT", "user5"), + ("INSERT", "user2"), + ("UPDATE", "user3"), + ] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="postgres") + + assert executed_sql == ( + "SELECT privilege_type, grantee FROM information_schema.role_table_grants " + "WHERE table_schema = 'test_schema' AND table_name = 'test_table' " + "AND grantor = current_role AND grantee <> current_role" + ) + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 4 + + assert 'GRANT SELECT ON "test_schema"."test_table" TO "user2", "user3"' in sql_calls + assert 'GRANT INSERT ON "test_schema"."test_table" TO "user4"' in sql_calls + assert 'REVOKE SELECT ON "test_schema"."test_table" FROM "user5"' in sql_calls + assert 'REVOKE UPDATE ON "test_schema"."test_table" FROM "user3"' in sql_calls + + +def test_diff_grants_configs(make_mocked_engine_adapter: t.Callable): + new_grants = {"select": ["USER1", "USER2"], "insert": ["user3"]} + old_grants = {"SELECT": ["user1", "user4"], "UPDATE": ["user5"]} + + adapter = make_mocked_engine_adapter(PostgresEngineAdapter) + additions, removals = adapter._diff_grants_configs(new_grants, old_grants) + + assert additions["select"] == ["USER2"] + assert additions["insert"] == ["user3"] + + assert removals["SELECT"] == ["user4"] + assert removals["UPDATE"] == ["user5"] + + +def test_sync_grants_config_with_default_schema( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(PostgresEngineAdapter) + relation = exp.to_table("test_table", dialect="postgres") # No schema + new_grants_config = {"SELECT": ["user1"], "INSERT": ["user2"]} + + currrent_grants = [("UPDATE", "old_user")] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=currrent_grants) + get_schema_mock = mocker.patch.object(adapter, "_get_current_schema", return_value="public") + + adapter.sync_grants_config(relation, new_grants_config) + + get_schema_mock.assert_called_once() + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="postgres") + + assert executed_sql == ( + "SELECT privilege_type, grantee FROM information_schema.role_table_grants " + "WHERE table_schema = 'public' AND table_name = 'test_table' " + "AND grantor = current_role AND grantee <> current_role" + ) diff --git a/tests/core/engine_adapter/test_redshift.py b/tests/core/engine_adapter/test_redshift.py index c5e3dfff17..5438943556 100644 --- a/tests/core/engine_adapter/test_redshift.py +++ b/tests/core/engine_adapter/test_redshift.py @@ -9,7 +9,7 @@ from sqlglot import parse_one from sqlmesh.core.engine_adapter import RedshiftEngineAdapter -from sqlmesh.core.engine_adapter.shared import DataObject +from sqlmesh.core.engine_adapter.shared import DataObject, DataObjectType from sqlmesh.utils.errors import SQLMeshError from tests.core.engine_adapter import to_sql_calls @@ -83,6 +83,154 @@ def test_varchar_size_workaround(make_mocked_engine_adapter: t.Callable, mocker: ] +def test_sync_grants_config(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): + adapter = make_mocked_engine_adapter(RedshiftEngineAdapter) + relation = exp.to_table("test_schema.test_table", dialect="redshift") + new_grants_config = {"SELECT": ["user1", "user2"], "INSERT": ["user3"]} + + current_grants = [("SELECT", "old_user"), ("UPDATE", "legacy_user")] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="redshift") + expected_sql = ( + "SELECT privilege_type, grantee FROM information_schema.table_privileges " + "WHERE table_schema = 'test_schema' AND table_name = 'test_table' " + "AND grantor = CURRENT_USER AND grantee <> CURRENT_USER" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 4 + assert 'REVOKE SELECT ON "test_schema"."test_table" FROM "old_user"' in sql_calls + assert 'REVOKE UPDATE ON "test_schema"."test_table" FROM "legacy_user"' in sql_calls + assert 'GRANT SELECT ON "test_schema"."test_table" TO "user1", "user2"' in sql_calls + assert 'GRANT INSERT ON "test_schema"."test_table" TO "user3"' in sql_calls + + +def test_sync_grants_config_with_overlaps( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(RedshiftEngineAdapter) + relation = exp.to_table("test_schema.test_table", dialect="redshift") + new_grants_config = { + "SELECT": ["user_shared", "user_new"], + "INSERT": ["user_shared", "user_writer"], + } + + current_grants = [ + ("SELECT", "user_shared"), + ("SELECT", "user_legacy"), + ("INSERT", "user_shared"), + ] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="redshift") + expected_sql = ( + "SELECT privilege_type, grantee FROM information_schema.table_privileges " + "WHERE table_schema = 'test_schema' AND table_name = 'test_table' " + "AND grantor = CURRENT_USER AND grantee <> CURRENT_USER" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 3 + assert 'REVOKE SELECT ON "test_schema"."test_table" FROM "user_legacy"' in sql_calls + assert 'GRANT SELECT ON "test_schema"."test_table" TO "user_new"' in sql_calls + assert 'GRANT INSERT ON "test_schema"."test_table" TO "user_writer"' in sql_calls + + +@pytest.mark.parametrize( + "table_type", + [ + (DataObjectType.TABLE), + (DataObjectType.VIEW), + (DataObjectType.MATERIALIZED_VIEW), + ], +) +def test_sync_grants_config_object_kind( + make_mocked_engine_adapter: t.Callable, + mocker: MockerFixture, + table_type: DataObjectType, +) -> None: + adapter = make_mocked_engine_adapter(RedshiftEngineAdapter) + relation = exp.to_table("test_schema.test_object", dialect="redshift") + + mocker.patch.object(adapter, "fetchall", return_value=[]) + + adapter.sync_grants_config(relation, {"SELECT": ["user_test"]}, table_type) + + sql_calls = to_sql_calls(adapter) + # we don't need to explicitly specify object_type for tables and views + assert sql_calls == [f'GRANT SELECT ON "test_schema"."test_object" TO "user_test"'] + + +def test_sync_grants_config_quotes(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): + adapter = make_mocked_engine_adapter(RedshiftEngineAdapter) + relation = exp.to_table('"TestSchema"."TestTable"', dialect="redshift") + new_grants_config = {"SELECT": ["user1", "user2"], "INSERT": ["user3"]} + + current_grants = [("SELECT", "user_old"), ("UPDATE", "user_legacy")] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="redshift") + expected_sql = ( + "SELECT privilege_type, grantee FROM information_schema.table_privileges " + "WHERE table_schema = 'TestSchema' AND table_name = 'TestTable' " + "AND grantor = CURRENT_USER AND grantee <> CURRENT_USER" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 4 + assert 'REVOKE SELECT ON "TestSchema"."TestTable" FROM "user_old"' in sql_calls + assert 'REVOKE UPDATE ON "TestSchema"."TestTable" FROM "user_legacy"' in sql_calls + assert 'GRANT SELECT ON "TestSchema"."TestTable" TO "user1", "user2"' in sql_calls + assert 'GRANT INSERT ON "TestSchema"."TestTable" TO "user3"' in sql_calls + + +def test_sync_grants_config_no_schema( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(RedshiftEngineAdapter) + relation = exp.to_table("test_table", dialect="redshift") + new_grants_config = {"SELECT": ["user1"], "INSERT": ["user2"]} + + current_grants = [("UPDATE", "user_old")] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + get_schema_mock = mocker.patch.object(adapter, "_get_current_schema", return_value="public") + + adapter.sync_grants_config(relation, new_grants_config) + + get_schema_mock.assert_called_once() + + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="redshift") + expected_sql = ( + "SELECT privilege_type, grantee FROM information_schema.table_privileges " + "WHERE table_schema = 'public' AND table_name = 'test_table' " + "AND grantor = CURRENT_USER AND grantee <> CURRENT_USER" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 3 + assert 'REVOKE UPDATE ON "test_table" FROM "user_old"' in sql_calls + assert 'GRANT SELECT ON "test_table" TO "user1"' in sql_calls + assert 'GRANT INSERT ON "test_table" TO "user2"' in sql_calls + + def test_create_table_from_query_exists_no_if_not_exists( adapter: t.Callable, mocker: MockerFixture ): diff --git a/tests/core/engine_adapter/test_snowflake.py b/tests/core/engine_adapter/test_snowflake.py index ce4d3a886c..60f6d38e5f 100644 --- a/tests/core/engine_adapter/test_snowflake.py +++ b/tests/core/engine_adapter/test_snowflake.py @@ -4,6 +4,7 @@ import pytest from pytest_mock.plugin import MockerFixture from sqlglot import exp, parse_one +from sqlglot.optimizer.normalize_identifiers import normalize_identifiers import sqlmesh.core.dialect as d from sqlmesh.core.dialect import normalize_model_name @@ -245,6 +246,204 @@ def test_multiple_column_comments(make_mocked_engine_adapter: t.Callable, mocker ] +def test_sync_grants_config(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): + adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter) + relation = normalize_identifiers( + exp.to_table("test_db.test_schema.test_table", dialect="snowflake"), dialect="snowflake" + ) + new_grants_config = {"SELECT": ["ROLE role1", "ROLE role2"], "INSERT": ["ROLE role3"]} + + current_grants = [ + ("SELECT", "ROLE old_role"), + ("UPDATE", "ROLE legacy_role"), + ] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="snowflake") + expected_sql = ( + "SELECT privilege_type, grantee FROM TEST_DB.INFORMATION_SCHEMA.TABLE_PRIVILEGES " + "WHERE table_catalog = 'TEST_DB' AND table_schema = 'TEST_SCHEMA' AND table_name = 'TEST_TABLE' " + "AND grantor = CURRENT_ROLE() AND grantee <> CURRENT_ROLE()" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 5 + + assert 'GRANT SELECT ON TABLE "TEST_DB"."TEST_SCHEMA"."TEST_TABLE" TO ROLE "ROLE1"' in sql_calls + assert 'GRANT SELECT ON TABLE "TEST_DB"."TEST_SCHEMA"."TEST_TABLE" TO ROLE "ROLE2"' in sql_calls + assert 'GRANT INSERT ON TABLE "TEST_DB"."TEST_SCHEMA"."TEST_TABLE" TO ROLE "ROLE3"' in sql_calls + assert ( + 'REVOKE SELECT ON TABLE "TEST_DB"."TEST_SCHEMA"."TEST_TABLE" FROM ROLE "OLD_ROLE"' + in sql_calls + ) + assert ( + 'REVOKE UPDATE ON TABLE "TEST_DB"."TEST_SCHEMA"."TEST_TABLE" FROM ROLE "LEGACY_ROLE"' + in sql_calls + ) + + +def test_sync_grants_config_with_overlaps( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter) + relation = normalize_identifiers( + exp.to_table("test_db.test_schema.test_table", dialect="snowflake"), dialect="snowflake" + ) + new_grants_config = { + "SELECT": ["ROLE shared", "ROLE new_role"], + "INSERT": ["ROLE shared", "ROLE writer"], + } + + current_grants = [ + ("SELECT", "ROLE shared"), + ("SELECT", "ROLE legacy"), + ("INSERT", "ROLE shared"), + ] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="snowflake") + expected_sql = ( + """SELECT privilege_type, grantee FROM TEST_DB.INFORMATION_SCHEMA.TABLE_PRIVILEGES """ + "WHERE table_catalog = 'TEST_DB' AND table_schema = 'TEST_SCHEMA' AND table_name = 'TEST_TABLE' " + "AND grantor = CURRENT_ROLE() AND grantee <> CURRENT_ROLE()" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 3 + + assert ( + 'GRANT SELECT ON TABLE "TEST_DB"."TEST_SCHEMA"."TEST_TABLE" TO ROLE "NEW_ROLE"' in sql_calls + ) + assert ( + 'GRANT INSERT ON TABLE "TEST_DB"."TEST_SCHEMA"."TEST_TABLE" TO ROLE "WRITER"' in sql_calls + ) + assert ( + 'REVOKE SELECT ON TABLE "TEST_DB"."TEST_SCHEMA"."TEST_TABLE" FROM ROLE "LEGACY"' + in sql_calls + ) + + +@pytest.mark.parametrize( + "table_type, expected_keyword", + [ + (DataObjectType.TABLE, "TABLE"), + (DataObjectType.VIEW, "VIEW"), + (DataObjectType.MATERIALIZED_VIEW, "MATERIALIZED VIEW"), + (DataObjectType.MANAGED_TABLE, "DYNAMIC TABLE"), + ], +) +def test_sync_grants_config_object_kind( + make_mocked_engine_adapter: t.Callable, + mocker: MockerFixture, + table_type: DataObjectType, + expected_keyword: str, +) -> None: + adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter) + relation = normalize_identifiers( + exp.to_table("test_db.test_schema.test_object", dialect="snowflake"), dialect="snowflake" + ) + + mocker.patch.object(adapter, "fetchall", return_value=[]) + + adapter.sync_grants_config(relation, {"SELECT": ["ROLE test"]}, table_type) + + sql_calls = to_sql_calls(adapter) + assert sql_calls == [ + f'GRANT SELECT ON {expected_keyword} "TEST_DB"."TEST_SCHEMA"."TEST_OBJECT" TO ROLE "TEST"' + ] + + +def test_sync_grants_config_quotes(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture): + adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter) + relation = normalize_identifiers( + exp.to_table('"test_db"."test_schema"."test_table"', dialect="snowflake"), + dialect="snowflake", + ) + new_grants_config = {"SELECT": ["ROLE role1", "ROLE role2"], "INSERT": ["ROLE role3"]} + + current_grants = [ + ("SELECT", "ROLE old_role"), + ("UPDATE", "ROLE legacy_role"), + ] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="snowflake") + expected_sql = ( + """SELECT privilege_type, grantee FROM "test_db".INFORMATION_SCHEMA.TABLE_PRIVILEGES """ + "WHERE table_catalog = 'test_db' AND table_schema = 'test_schema' AND table_name = 'test_table' " + "AND grantor = CURRENT_ROLE() AND grantee <> CURRENT_ROLE()" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 5 + + assert 'GRANT SELECT ON TABLE "test_db"."test_schema"."test_table" TO ROLE "ROLE1"' in sql_calls + assert 'GRANT SELECT ON TABLE "test_db"."test_schema"."test_table" TO ROLE "ROLE2"' in sql_calls + assert 'GRANT INSERT ON TABLE "test_db"."test_schema"."test_table" TO ROLE "ROLE3"' in sql_calls + assert ( + 'REVOKE SELECT ON TABLE "test_db"."test_schema"."test_table" FROM ROLE "OLD_ROLE"' + in sql_calls + ) + assert ( + 'REVOKE UPDATE ON TABLE "test_db"."test_schema"."test_table" FROM ROLE "LEGACY_ROLE"' + in sql_calls + ) + + +def test_sync_grants_config_no_catalog_or_schema( + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture +): + adapter = make_mocked_engine_adapter(SnowflakeEngineAdapter) + relation = normalize_identifiers( + exp.to_table('"TesT_Table"', dialect="snowflake"), dialect="snowflake" + ) + new_grants_config = {"SELECT": ["ROLE role1", "ROLE role2"], "INSERT": ["ROLE role3"]} + + current_grants = [ + ("SELECT", "ROLE old_role"), + ("UPDATE", "ROLE legacy_role"), + ] + fetchall_mock = mocker.patch.object(adapter, "fetchall", return_value=current_grants) + mocker.patch.object(adapter, "get_current_catalog", return_value="caTalog") + mocker.patch.object(adapter, "_get_current_schema", return_value="sChema") + + adapter.sync_grants_config(relation, new_grants_config) + + fetchall_mock.assert_called_once() + executed_query = fetchall_mock.call_args[0][0] + executed_sql = executed_query.sql(dialect="snowflake") + expected_sql = ( + """SELECT privilege_type, grantee FROM "caTalog".INFORMATION_SCHEMA.TABLE_PRIVILEGES """ + "WHERE table_catalog = 'caTalog' AND table_schema = 'sChema' AND table_name = 'TesT_Table' " + "AND grantor = CURRENT_ROLE() AND grantee <> CURRENT_ROLE()" + ) + assert executed_sql == expected_sql + + sql_calls = to_sql_calls(adapter) + assert len(sql_calls) == 5 + + assert 'GRANT SELECT ON TABLE "TesT_Table" TO ROLE "ROLE1"' in sql_calls + assert 'GRANT SELECT ON TABLE "TesT_Table" TO ROLE "ROLE2"' in sql_calls + assert 'GRANT INSERT ON TABLE "TesT_Table" TO ROLE "ROLE3"' in sql_calls + assert 'REVOKE SELECT ON TABLE "TesT_Table" FROM ROLE "OLD_ROLE"' in sql_calls + assert 'REVOKE UPDATE ON TABLE "TesT_Table" FROM ROLE "LEGACY_ROLE"' in sql_calls + + def test_df_to_source_queries_use_schema( make_mocked_engine_adapter: t.Callable, mocker: MockerFixture ): diff --git a/tests/core/engine_adapter/test_spark.py b/tests/core/engine_adapter/test_spark.py index bc4e352bd7..d7c3127f05 100644 --- a/tests/core/engine_adapter/test_spark.py +++ b/tests/core/engine_adapter/test_spark.py @@ -224,7 +224,7 @@ def test_replace_query_self_ref_not_exists( lambda self: "spark_catalog", ) mocker.patch( - "sqlmesh.core.engine_adapter.spark.SparkEngineAdapter.get_current_database", + "sqlmesh.core.engine_adapter.spark.SparkEngineAdapter._get_current_schema", side_effect=lambda: "default", ) @@ -283,7 +283,7 @@ def test_replace_query_self_ref_exists( return_value="spark_catalog", ) mocker.patch( - "sqlmesh.core.engine_adapter.spark.SparkEngineAdapter.get_current_database", + "sqlmesh.core.engine_adapter.spark.SparkEngineAdapter._get_current_schema", return_value="default", ) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index b7ce64eb4c..6270cec56a 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -3050,9 +3050,10 @@ def test_uppercase_gateway_external_models(tmp_path): # Check that the column types are properly loaded (not UNKNOWN) external_model = gateway_specific_models[0] column_types = {name: str(dtype) for name, dtype in external_model.columns_to_types.items()} - assert column_types == {"id": "INT", "name": "TEXT"}, ( - f"External model column types should not be UNKNOWN, got: {column_types}" - ) + assert column_types == { + "id": "INT", + "name": "TEXT", + }, f"External model column types should not be UNKNOWN, got: {column_types}" # Test that when using a different case for the gateway parameter, we get the same results context_mixed_case = Context( @@ -3177,3 +3178,55 @@ def test_lint_model_projections(tmp_path: Path): with pytest.raises(LinterError, match=config_err): prod_plan = context.plan(no_prompts=True, auto_apply=True) + + +def test_grants_through_plan_apply(sushi_context, mocker): + from sqlmesh.core.engine_adapter.duckdb import DuckDBEngineAdapter + from sqlmesh.core.model.meta import GrantsTargetLayer + + model = sushi_context.get_model("sushi.waiter_revenue_by_day") + + mocker.patch.object(DuckDBEngineAdapter, "SUPPORTS_GRANTS", True) + sync_grants_mock = mocker.patch.object(DuckDBEngineAdapter, "sync_grants_config") + + model_with_grants = model.copy( + update={ + "grants": {"select": ["analyst", "reporter"]}, + "grants_target_layer": GrantsTargetLayer.ALL, + } + ) + sushi_context.upsert_model(model_with_grants) + + sushi_context.plan("dev", no_prompts=True, auto_apply=True) + + # When planning for dev env w/ metadata only changes, + # only virtual layer is updated, so no physical grants are applied + assert sync_grants_mock.call_count == 1 + assert all( + call[0][1] == {"select": ["analyst", "reporter"]} + for call in sync_grants_mock.call_args_list + ) + + sync_grants_mock.reset_mock() + + new_grants = ({"select": ["analyst", "reporter", "manager"], "insert": ["etl_user"]},) + model_updated = model_with_grants.copy( + update={ + "query": parse_one(model.query.sql() + " LIMIT 1000"), + "grants": new_grants, + # force model update, hence new physical table creation + "stamp": "update model and grants", + } + ) + sushi_context.upsert_model(model_updated) + sushi_context.plan("dev", no_prompts=True, auto_apply=True) + + # Applies grants 2 times: 1 x physical, 1 x virtual + assert sync_grants_mock.call_count == 2 + assert all(call[0][1] == new_grants for call in sync_grants_mock.call_args_list) + + sync_grants_mock.reset_mock() + + # plan for prod + sushi_context.plan(no_prompts=True, auto_apply=True) + assert sync_grants_mock.call_count == 2 diff --git a/tests/core/test_model.py b/tests/core/test_model.py index 726ac52b66..f1a9eeb0b9 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -1,6 +1,7 @@ # ruff: noqa: F811 import json import typing as t +import re from datetime import date, datetime from pathlib import Path from unittest.mock import patch, PropertyMock @@ -14,7 +15,7 @@ from sqlglot.schema import MappingSchema from sqlmesh.cli.project_init import init_example_project, ProjectTemplate from sqlmesh.core.environment import EnvironmentNamingInfo -from sqlmesh.core.model.kind import TimeColumn, ModelKindName +from sqlmesh.core.model.kind import TimeColumn, ModelKindName, SeedKind from sqlmesh import CustomMaterialization, CustomKind from pydantic import model_validator, ValidationError @@ -36,6 +37,7 @@ from sqlmesh.core.dialect import parse from sqlmesh.core.engine_adapter.base import MERGE_SOURCE_ALIAS, MERGE_TARGET_ALIAS from sqlmesh.core.engine_adapter.duckdb import DuckDBEngineAdapter +from sqlmesh.core.engine_adapter.shared import DataObjectType from sqlmesh.core.macros import MacroEvaluator, macro from sqlmesh.core.model import ( CustomKind, @@ -51,6 +53,8 @@ TimeColumn, ExternalKind, ViewKind, + EmbeddedKind, + SCDType2ByTimeKind, create_external_model, create_seed_model, create_sql_model, @@ -59,7 +63,7 @@ model, ) from sqlmesh.core.model.common import parse_expression -from sqlmesh.core.model.kind import ModelKindName, _model_kind_validator +from sqlmesh.core.model.kind import _ModelKind, ModelKindName, _model_kind_validator from sqlmesh.core.model.seed import CsvSettings from sqlmesh.core.node import IntervalUnit, _Node, DbtNodeInfo from sqlmesh.core.signal import signal @@ -1922,7 +1926,8 @@ def test_render_definition_with_defaults(): kind VIEW ( materialized FALSE ), - virtual_environment_mode 'full' + virtual_environment_mode 'full', + grants_target_layer 'virtual' ); {query} @@ -1935,6 +1940,90 @@ def test_render_definition_with_defaults(): ) == d.format_model_expressions(expected_expressions) +def test_render_definition_with_grants(): + from sqlmesh.core.model.meta import GrantsTargetLayer + + expressions = d.parse( + """ + MODEL ( + name test.grants_model, + kind FULL, + grants ( + 'select' = ['user1', 'user2'], + 'insert' = ['admin'], + 'roles/bigquery.dataViewer' = ['user:data_eng@mycompany.com'] + ), + grants_target_layer all, + ); + SELECT 1 as id + """ + ) + model = load_sql_based_model(expressions) + assert model.grants_target_layer == GrantsTargetLayer.ALL + assert model.grants == { + "select": ["user1", "user2"], + "insert": ["admin"], + "roles/bigquery.dataViewer": ["user:data_eng@mycompany.com"], + } + + rendered = model.render_definition(include_defaults=True) + rendered_text = d.format_model_expressions(rendered) + assert "grants_target_layer 'all'" in rendered_text + assert re.search( + r"grants\s*\(" + r"\s*'select'\s*=\s*ARRAY\('user1',\s*'user2'\)," + r"\s*'insert'\s*=\s*ARRAY\('admin'\)," + r"\s*'roles/bigquery.dataViewer'\s*=\s*ARRAY\('user:data_eng@mycompany.com'\)" + r"\s*\)", + rendered_text, + ) + + model_with_grants = create_sql_model( + name="test_grants_programmatic", + query=d.parse_one("SELECT 1 as id"), + grants={"select": ["user1", "user2"], "insert": ["admin"]}, + grants_target_layer=GrantsTargetLayer.ALL, + ) + assert model_with_grants.grants == {"select": ["user1", "user2"], "insert": ["admin"]} + assert model_with_grants.grants_target_layer == GrantsTargetLayer.ALL + rendered_text = d.format_model_expressions( + model_with_grants.render_definition(include_defaults=True) + ) + assert "grants_target_layer 'all'" in rendered_text + assert re.search( + r"grants\s*\(" + r"\s*'select'\s*=\s*ARRAY\('user1',\s*'user2'\)," + r"\s*'insert'\s*=\s*ARRAY\('admin'\)" + r"\s*\)", + rendered_text, + ) + + virtual_expressions = d.parse( + """ + MODEL ( + name test.virtual_grants_model, + kind FULL, + grants_target_layer virtual + ); + SELECT 1 as id + """ + ) + virtual_model = load_sql_based_model(virtual_expressions) + assert virtual_model.grants_target_layer == GrantsTargetLayer.VIRTUAL + + default_expressions = d.parse( + """ + MODEL ( + name test.default_grants_model, + kind FULL + ); + SELECT 1 as id + """ + ) + default_model = load_sql_based_model(default_expressions) + assert default_model.grants_target_layer == GrantsTargetLayer.VIRTUAL # default value + + def test_render_definition_partitioned_by(): # no parenthesis in definition, no parenthesis when rendered model = load_sql_based_model( @@ -11717,3 +11806,254 @@ def my_macro(evaluator): model = context.get_model("test_model", raise_if_missing=True) assert model.render_query_or_raise().sql() == 'SELECT 3 AS "c"' + + +def test_grants(): + expressions = d.parse(""" + MODEL ( + name test.table, + kind FULL, + grants ( + 'select' = ['user1', 123, admin_role, 'user2'], + 'insert' = 'admin', + 'roles/bigquery.dataViewer' = ["group:data_eng@company.com", 'user:someone@company.com'], + 'update' = 'admin' + ) + ); + SELECT 1 as id + """) + model = load_sql_based_model(expressions) + assert model.grants == { + "select": ["user1", "123", "admin_role", "user2"], + "insert": ["admin"], + "roles/bigquery.dataViewer": ["group:data_eng@company.com", "user:someone@company.com"], + "update": ["admin"], + } + + model = create_sql_model( + "db.table", + parse_one("SELECT 1 AS id"), + kind="FULL", + grants={ + "select": ["user1", "user2"], + "insert": ["admin"], + "roles/bigquery.dataViewer": "user:data_eng@company.com", + }, + ) + assert model.grants == { + "select": ["user1", "user2"], + "insert": ["admin"], + "roles/bigquery.dataViewer": ["user:data_eng@company.com"], + } + + +@pytest.mark.parametrize( + "kind", + [ + "FULL", + "VIEW", + SeedKind(path="test.csv"), + IncrementalByTimeRangeKind(time_column="ds"), + IncrementalByUniqueKeyKind(unique_key="id"), + ], +) +def test_grants_valid_model_kinds(kind: t.Union[str, _ModelKind]): + model = create_sql_model( + "db.table", + parse_one("SELECT 1 AS id"), + kind=kind, + grants={"select": ["user1", "user2"], "insert": ["admin_user"]}, + ) + assert model.grants == {"select": ["user1", "user2"], "insert": ["admin_user"]} + + +@pytest.mark.parametrize( + "kind", + [ + "EXTERNAL", + "EMBEDDED", + ], +) +def test_grants_invalid_model_kind_errors(kind: str): + with pytest.raises(ValidationError, match=rf".*grants cannot be set for {kind}.*"): + create_sql_model( + "db.table", + parse_one("SELECT 1 AS id"), + kind=kind, + grants={"select": ["user1"], "insert": ["admin_user"]}, + ) + + +def test_model_kind_supports_grants(): + assert FullKind().supports_grants is True + assert ViewKind().supports_grants is True + assert IncrementalByTimeRangeKind(time_column="ds").supports_grants is True + assert IncrementalByUniqueKeyKind(unique_key=["id"]).supports_grants is True + assert SCDType2ByTimeKind(unique_key=["id"]).supports_grants is True + + assert EmbeddedKind().supports_grants is False + assert ExternalKind().supports_grants is False + + +def test_grants_validation_no_grants(): + model = create_sql_model("db.table", parse_one("SELECT 1 AS id"), kind="FULL") + assert model.grants is None + + +def test_grants_validation_empty_grantees(): + model = create_sql_model( + "db.table", parse_one("SELECT 1 AS id"), kind="FULL", grants={"select": []} + ) + assert model.grants == {"select": []} + + +def test_grants_single_value_conversions(): + expressions = d.parse(f""" + MODEL ( + name test.nested_arrays, + kind FULL, + grants ( + 'select' = "user1", update = user2 + ) + ); + SELECT 1 as id + """) + model = load_sql_based_model(expressions) + assert model.grants == {"select": ["user1"], "update": ["user2"]} + + model = create_sql_model( + "db.table", + parse_one("SELECT 1 AS id"), + kind="FULL", + grants={"select": "user1", "insert": 123}, + ) + assert model.grants == {"select": ["user1"], "insert": ["123"]} + + +@pytest.mark.parametrize( + "grantees", + [ + "('user1', ('user2', 'user3'), 'user4')", + "('user1', ['user2', 'user3'], user4)", + "['user1', ['user2', user3], 'user4']", + "[user1, ('user2', \"user3\"), 'user4']", + ], +) +def test_grants_array_flattening(grantees: str): + expressions = d.parse(f""" + MODEL ( + name test.nested_arrays, + kind FULL, + grants ( + 'select' = {grantees} + ) + ); + SELECT 1 as id + """) + model = load_sql_based_model(expressions) + assert model.grants == {"select": ["user1", "user2", "user3", "user4"]} + + +def test_grants_macro_var_resolved(): + expressions = d.parse(""" + MODEL ( + name test.macro_grants, + kind FULL, + grants ( + 'select' = @VAR('readers'), + 'insert' = @VAR('writers') + ) + ); + SELECT 1 as id + """) + model = load_sql_based_model( + expressions, variables={"readers": ["user1", "user2"], "writers": "admin"} + ) + assert model.grants == { + "select": ["user1", "user2"], + "insert": ["admin"], + } + + +def test_grants_macro_var_in_array_flattening(): + expressions = d.parse(""" + MODEL ( + name test.macro_in_array, + kind FULL, + grants ( + 'select' = ['user1', @VAR('admins'), 'user3'] + ) + ); + SELECT 1 as id + """) + + model = load_sql_based_model(expressions, variables={"admins": ["admin1", "admin2"]}) + assert model.grants == {"select": ["user1", "admin1", "admin2", "user3"]} + + model2 = load_sql_based_model(expressions, variables={"admins": "super_admin"}) + assert model2.grants == {"select": ["user1", "super_admin", "user3"]} + + +def test_grants_dynamic_permission_names(): + expressions = d.parse(""" + MODEL ( + name test.dynamic_keys, + kind FULL, + grants ( + @VAR('read_perm') = ['user1', 'user2'], + @VAR('write_perm') = ['admin'] + ) + ); + SELECT 1 as id + """) + model = load_sql_based_model( + expressions, variables={"read_perm": "select", "write_perm": "insert"} + ) + assert model.grants == {"select": ["user1", "user2"], "insert": ["admin"]} + + +def test_grants_unresolved_macro_errors(): + expressions1 = d.parse(""" + MODEL (name test.bad1, kind FULL, grants ('select' = @VAR('undefined'))); + SELECT 1 as id + """) + with pytest.raises(ConfigError, match=r"Invalid grants configuration for 'select': NULL value"): + load_sql_based_model(expressions1) + + expressions2 = d.parse(""" + MODEL (name test.bad2, kind FULL, grants (@VAR('undefined') = ['user'])); + SELECT 1 as id + """) + with pytest.raises(ConfigError, match=r"Invalid grants configuration.*NULL value"): + load_sql_based_model(expressions2) + + expressions3 = d.parse(""" + MODEL (name test.bad3, kind FULL, grants ('select' = ['user', @VAR('undefined')])); + SELECT 1 as id + """) + with pytest.raises(ConfigError, match=r"Invalid grants configuration for 'select': NULL value"): + load_sql_based_model(expressions3) + + +def test_grants_empty_values(): + model1 = create_sql_model( + "db.table", parse_one("SELECT 1 AS id"), kind="FULL", grants={"select": []} + ) + assert model1.grants == {"select": []} + + model2 = create_sql_model("db.table", parse_one("SELECT 1 AS id"), kind="FULL") + assert model2.grants is None + + +@pytest.mark.parametrize( + "kind, expected", + [ + ("VIEW", DataObjectType.VIEW), + ("FULL", DataObjectType.TABLE), + ("MANAGED", DataObjectType.MANAGED_TABLE), + (ViewKind(materialized=True), DataObjectType.MATERIALIZED_VIEW), + ], +) +def test_grants_table_type(kind: t.Union[str, _ModelKind], expected: DataObjectType): + model = create_sql_model("test_table", parse_one("SELECT 1 as id"), kind=kind) + assert model.grants_table_type == expected diff --git a/tests/core/test_snapshot.py b/tests/core/test_snapshot.py index c769991b86..1acc6cc265 100644 --- a/tests/core/test_snapshot.py +++ b/tests/core/test_snapshot.py @@ -168,6 +168,7 @@ def test_json(snapshot: Snapshot): "enabled": True, "extract_dependencies_from_query": True, "virtual_environment_mode": "full", + "grants_target_layer": "virtual", }, "name": '"name"', "parents": [{"name": '"parent"."tbl"', "identifier": snapshot.parents[0].identifier}], @@ -181,6 +182,36 @@ def test_json(snapshot: Snapshot): } +def test_json_with_grants(make_snapshot: t.Callable): + from sqlmesh.core.model.meta import GrantsTargetLayer + + model = SqlModel( + name="name", + kind=dict(time_column="ds", batch_size=30, name=ModelKindName.INCREMENTAL_BY_TIME_RANGE), + owner="owner", + dialect="spark", + cron="1 0 * * *", + start="2020-01-01", + query=parse_one("SELECT @EACH([1, 2], x -> x), ds FROM parent.tbl"), + grants={"SELECT": ["role1", "role2"], "INSERT": ["role3"]}, + grants_target_layer=GrantsTargetLayer.VIRTUAL, + ) + snapshot = make_snapshot(model) + + json_str = snapshot.json() + json_data = json.loads(json_str) + assert ( + json_data["node"]["grants"] + == "('SELECT' = ARRAY('role1', 'role2'), 'INSERT' = ARRAY('role3'))" + ) + assert json_data["node"]["grants_target_layer"] == "virtual" + + reparsed_snapshot = Snapshot.model_validate_json(json_str) + assert isinstance(reparsed_snapshot.node, SqlModel) + assert reparsed_snapshot.node.grants == {"SELECT": ["role1", "role2"], "INSERT": ["role3"]} + assert reparsed_snapshot.node.grants_target_layer == GrantsTargetLayer.VIRTUAL + + def test_json_custom_materialization(make_snapshot: t.Callable): model = SqlModel( name="name", @@ -954,7 +985,7 @@ def test_fingerprint(model: Model, parent_model: Model): original_fingerprint = SnapshotFingerprint( data_hash="2406542604", - metadata_hash="3341445192", + metadata_hash="1056339358", ) assert fingerprint == original_fingerprint @@ -1014,8 +1045,8 @@ def test_fingerprint_seed_model(): ) expected_fingerprint = SnapshotFingerprint( - data_hash="1586624913", - metadata_hash="2315134974", + data_hash="2112858704", + metadata_hash="2674364560", ) model = load_sql_based_model(expressions, path=Path("./examples/sushi/models/test_model.sql")) @@ -1054,7 +1085,7 @@ def test_fingerprint_jinja_macros(model: Model): ) original_fingerprint = SnapshotFingerprint( data_hash="93332825", - metadata_hash="3341445192", + metadata_hash="1056339358", ) fingerprint = fingerprint_from_node(model, nodes={}) @@ -1131,6 +1162,40 @@ def test_fingerprint_virtual_properties(model: Model, parent_model: Model): assert updated_fingerprint.data_hash == fingerprint.data_hash +def test_fingerprint_grants(model: Model, parent_model: Model): + from sqlmesh.core.model.meta import GrantsTargetLayer + + original_model = deepcopy(model) + fingerprint = fingerprint_from_node(model, nodes={}) + + updated_model = SqlModel( + **original_model.dict(), + grants={"SELECT": ["role1", "role2"]}, + ) + updated_fingerprint = fingerprint_from_node(updated_model, nodes={}) + + assert updated_fingerprint != fingerprint + assert updated_fingerprint.metadata_hash != fingerprint.metadata_hash + assert updated_fingerprint.data_hash == fingerprint.data_hash + + different_grants_model = SqlModel( + **original_model.dict(), + grants={"SELECT": ["role3"], "INSERT": ["role4"]}, + ) + different_grants_fingerprint = fingerprint_from_node(different_grants_model, nodes={}) + + assert different_grants_fingerprint.metadata_hash != updated_fingerprint.metadata_hash + assert different_grants_fingerprint.metadata_hash != fingerprint.metadata_hash + + target_layer_model = SqlModel( + **{**original_model.dict(), "grants_target_layer": GrantsTargetLayer.PHYSICAL}, + grants={"SELECT": ["role1", "role2"]}, + ) + target_layer_fingerprint = fingerprint_from_node(target_layer_model, nodes={}) + + assert target_layer_fingerprint.metadata_hash != updated_fingerprint.metadata_hash + + def test_tableinfo_equality(): snapshot_a = SnapshotTableInfo( name="test_schema.a", diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 19685e81c3..68061544a8 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -41,8 +41,10 @@ load_sql_based_model, ExternalModel, model, + create_sql_model, ) from sqlmesh.core.model.kind import OnDestructiveChange, ExternalKind, OnAdditiveChange +from sqlmesh.core.model.meta import GrantsTargetLayer from sqlmesh.core.node import IntervalUnit from sqlmesh.core.snapshot import ( DeployabilityIndex, @@ -55,7 +57,19 @@ SnapshotTableCleanupTask, ) from sqlmesh.core.snapshot.definition import to_view_mapping -from sqlmesh.core.snapshot.evaluator import CustomMaterialization, SnapshotCreationFailedError +from sqlmesh.core.snapshot.evaluator import ( + CustomMaterialization, + EngineManagedStrategy, + FullRefreshStrategy, + IncrementalByPartitionStrategy, + IncrementalByTimeRangeStrategy, + IncrementalByUniqueKeyStrategy, + IncrementalUnmanagedStrategy, + MaterializableStrategy, + SCDType2Strategy, + SnapshotCreationFailedError, + ViewStrategy, +) from sqlmesh.utils.concurrency import NodeExecutionFailedError from sqlmesh.utils.date import to_timestamp from sqlmesh.utils.errors import ( @@ -908,7 +922,7 @@ def test_pre_hook_forward_only_clone( time_column ds ) ); - + {pre_statement}; SELECT a::int, ds::string FROM tbl; @@ -4858,3 +4872,524 @@ def mutate_view_properties(*args, **kwargs): # Both calls should have view_properties with security invoker assert props == ["'SECURITY INVOKER'", "'SECURITY INVOKER'"] + + +def _create_grants_test_model( + grants=None, kind="FULL", grants_target_layer=None, virtual_environment_mode=None +): + if kind == "SEED": + from sqlmesh.core.model.definition import create_seed_model + from sqlmesh.core.model.kind import SeedKind + import tempfile + import os + + # Create a temporary CSV file for the test + temp_csv = tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) + temp_csv.write("id,name\n1,test\n2,test2\n") + temp_csv.flush() + temp_csv.close() + + seed_kind_config = {"name": "SEED", "path": temp_csv.name} + seed_kind = SeedKind(**seed_kind_config) + + kwargs = {} + if grants is not None: + kwargs["grants"] = grants + if grants_target_layer is not None: + kwargs["grants_target_layer"] = grants_target_layer + + model = create_seed_model("test_model", seed_kind, **kwargs) + + # Clean up the temporary file + os.unlink(temp_csv.name) + + return model + + # Handle regular SQL models + kwargs = { + "kind": kind, + } + if grants is not None: + kwargs["grants"] = grants + if grants_target_layer is not None: + kwargs["grants_target_layer"] = grants_target_layer + if virtual_environment_mode is not None: + kwargs["virtual_environment_mode"] = virtual_environment_mode + + # Add column annotations for non-SEED models to ensure table creation + if kind != "SEED": + kwargs["columns"] = { + "id": "INT", + "ds": "DATE", + "updated_at": "TIMESTAMP", + } + + # Add required fields for specific model kinds + if kind == "INCREMENTAL_BY_TIME_RANGE": + kwargs["kind"] = {"name": "INCREMENTAL_BY_TIME_RANGE", "time_column": "ds"} + elif kind == "INCREMENTAL_BY_PARTITION": + kwargs["kind"] = {"name": "INCREMENTAL_BY_PARTITION"} + kwargs["partitioned_by"] = ["ds"] # This goes on the model, not the kind + elif kind == "INCREMENTAL_BY_UNIQUE_KEY": + kwargs["kind"] = {"name": "INCREMENTAL_BY_UNIQUE_KEY", "unique_key": ["id"]} + elif kind == "INCREMENTAL_UNMANAGED": + kwargs["kind"] = {"name": "INCREMENTAL_UNMANAGED"} + elif kind == "SCD_TYPE_2": + kwargs["kind"] = { + "name": "SCD_TYPE_2", + "unique_key": ["id"], + "updated_at_name": "updated_at", + } + + return create_sql_model( + "test_model", + parse_one("SELECT 1 as id, CURRENT_DATE as ds, CURRENT_TIMESTAMP as updated_at"), + **kwargs, + ) + + +@pytest.mark.parametrize( + "target_layer,apply_layer,expected_call_count", + [ + (GrantsTargetLayer.ALL, GrantsTargetLayer.PHYSICAL, 1), + (GrantsTargetLayer.ALL, GrantsTargetLayer.VIRTUAL, 1), + (GrantsTargetLayer.PHYSICAL, GrantsTargetLayer.PHYSICAL, 1), + (GrantsTargetLayer.PHYSICAL, GrantsTargetLayer.VIRTUAL, 0), + (GrantsTargetLayer.VIRTUAL, GrantsTargetLayer.PHYSICAL, 0), + (GrantsTargetLayer.VIRTUAL, GrantsTargetLayer.VIRTUAL, 1), + ], +) +def test_apply_grants_target_layer( + target_layer: GrantsTargetLayer, + apply_layer: GrantsTargetLayer, + expected_call_count: int, + adapter_mock: Mock, + mocker: MockerFixture, +): + adapter_mock.SUPPORTS_GRANTS = True + sync_grants_mock = mocker.patch.object(adapter_mock, "sync_grants_config") + strategy = ViewStrategy(adapter_mock) + + model = _create_grants_test_model( + grants={"select": ["user1"]}, grants_target_layer=target_layer + ) + + strategy._apply_grants(model, "test_table", apply_layer) + + if expected_call_count > 0: + assert sync_grants_mock.call_count == expected_call_count + else: + sync_grants_mock.assert_not_called() + + +@pytest.mark.parametrize( + "model_kind_name", + [ + "FULL", + "INCREMENTAL_BY_TIME_RANGE", + "SEED", + "MANAGED", + "SCD_TYPE_2", + "VIEW", + ], +) +def test_grants_create_model_kind( + model_kind_name: str, + adapter_mock: Mock, + mocker: MockerFixture, + make_snapshot: t.Callable[..., Snapshot], +): + adapter_mock.SUPPORTS_GRANTS = True + sync_grants_mock = mocker.patch.object(adapter_mock, "sync_grants_config") + + grants = {"select": ["user1"]} + model = _create_grants_test_model( + grants=grants, kind=model_kind_name, grants_target_layer=GrantsTargetLayer.ALL + ) + snapshot = make_snapshot(model) + + evaluator = SnapshotEvaluator(adapter_mock) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + evaluator.create([snapshot], {}) + + sync_grants_mock.assert_called_once() + assert sync_grants_mock.call_args[0][1] == grants + + +@pytest.mark.parametrize( + "target_layer", + [ + GrantsTargetLayer.PHYSICAL, + GrantsTargetLayer.VIRTUAL, + GrantsTargetLayer.ALL, + ], +) +def test_grants_target_layer( + target_layer: GrantsTargetLayer, + adapter_mock: Mock, + mocker: MockerFixture, + make_snapshot: t.Callable[..., Snapshot], +): + adapter_mock.SUPPORTS_GRANTS = True + sync_grants_mock = mocker.patch.object(adapter_mock, "sync_grants_config") + evaluator = SnapshotEvaluator(adapter_mock) + + grants = {"select": ["user1"]} + model = create_sql_model( + "test_schema.test_model", + parse_one("SELECT 1 as id"), + kind="FULL", + grants=grants, + grants_target_layer=target_layer, + ) + + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + evaluator.create([snapshot], {}) + if target_layer == GrantsTargetLayer.VIRTUAL: + assert sync_grants_mock.call_count == 0 + else: + assert sync_grants_mock.call_count == 1 + assert sync_grants_mock.call_args[0][1] == grants + sync_grants_mock.reset_mock() + evaluator.promote([snapshot], EnvironmentNamingInfo(name="prod")) + if target_layer == GrantsTargetLayer.VIRTUAL: + assert sync_grants_mock.call_count == 1 + elif target_layer == GrantsTargetLayer.PHYSICAL: + # Physical layer: no grants applied during promotion (already applied during create) + assert sync_grants_mock.call_count == 0 + else: # target_layer == GrantsTargetLayer.ALL + # All layers: only virtual grants applied during promotion (physical already done in create) + assert sync_grants_mock.call_count == 1 + + +def test_grants_update( + adapter_mock: Mock, mocker: MockerFixture, make_snapshot: t.Callable[..., Snapshot] +): + adapter_mock.SUPPORTS_GRANTS = True + sync_grants_mock = mocker.patch.object(adapter_mock, "sync_grants_config") + + evaluator = SnapshotEvaluator(adapter_mock) + + model = create_sql_model( + "test_schema.test_model", + parse_one("SELECT 1 as id"), + kind="FULL", + grants={"select": ["user1"]}, + grants_target_layer=GrantsTargetLayer.ALL, + ) + + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + evaluator.create([snapshot], {}) + + sync_grants_mock.assert_called_once() + assert sync_grants_mock.call_args[0][1] == {"select": ["user1"]} + + # Update model query AND change grants + updated_model_dict = model.dict() + updated_model_dict["query"] = parse_one("SELECT 1 as id, 2 as value") + updated_model_dict["grants"] = {"select": ["user2", "user3"], "insert": ["admin"]} + updated_model = SqlModel.parse_obj(updated_model_dict) + + new_snapshot = make_snapshot(updated_model) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + sync_grants_mock.reset_mock() + evaluator.create([new_snapshot], {}) + + sync_grants_mock.assert_called_once() + assert sync_grants_mock.call_args[0][1] == {"select": ["user2", "user3"], "insert": ["admin"]} + + # Update model query AND remove grants + updated_model_dict = model.dict() + updated_model_dict["query"] = parse_one("SELECT 1 as id, 'updated' as status") + updated_model_dict["grants"] = {} + updated_model = SqlModel.parse_obj(updated_model_dict) + + new_snapshot = make_snapshot(updated_model) + new_snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + sync_grants_mock.reset_mock() + evaluator.create([new_snapshot], {}) + + sync_grants_mock.assert_called_once() + assert sync_grants_mock.call_args[0][1] == {} + + +def test_grants_create_and_evaluate( + adapter_mock: Mock, mocker: MockerFixture, make_snapshot: t.Callable[..., Snapshot] +): + adapter_mock.SUPPORTS_GRANTS = True + sync_grants_mock = mocker.patch.object(adapter_mock, "sync_grants_config") + + evaluator = SnapshotEvaluator(adapter_mock) + + model = load_sql_based_model( + parse( # type: ignore + """ + MODEL ( + name test_schema.test_model, + kind INCREMENTAL_BY_TIME_RANGE (time_column ds), + grants ( + 'select' = ['reader1', 'reader2'], + 'insert' = ['writer'] + ), + grants_target_layer 'all' + ); + SELECT ds::DATE, value::INT FROM source WHERE ds BETWEEN @start_ds AND @end_ds; + """ + ) + ) + + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + evaluator.create([snapshot], {}) + sync_grants_mock.assert_called_once() + assert sync_grants_mock.call_args[0][1] == { + "select": ["reader1", "reader2"], + "insert": ["writer"], + } + + sync_grants_mock.reset_mock() + evaluator.evaluate( + snapshot, start="2020-01-01", end="2020-01-02", execution_time="2020-01-02", snapshots={} + ) + # Evaluate should not reapply grants + sync_grants_mock.assert_not_called() + + +@pytest.mark.parametrize( + "strategy_class", + [ + EngineManagedStrategy, + FullRefreshStrategy, + IncrementalByTimeRangeStrategy, + IncrementalByPartitionStrategy, + IncrementalUnmanagedStrategy, + IncrementalByUniqueKeyStrategy, + SCDType2Strategy, + # SeedStrategy excluded because seeds do not support migrations + ], +) +def test_grants_materializable_strategy_migrate( + strategy_class: t.Type[MaterializableStrategy], + adapter_mock: Mock, + mocker: MockerFixture, + make_snapshot: t.Callable[..., Snapshot], +): + adapter_mock.SUPPORTS_GRANTS = True + adapter_mock.get_alter_operations.return_value = [] + sync_grants_mock = mocker.patch.object(adapter_mock, "sync_grants_config") + strategy = strategy_class(adapter_mock) + grants = {"select": ["user1"]} + model = _create_grants_test_model(grants=grants, grants_target_layer=GrantsTargetLayer.ALL) + snapshot = make_snapshot(model) + + strategy.migrate( + "target_table", + "source_table", + snapshot, + ignore_destructive=False, + ignore_additive=False, + allow_destructive_snapshots=set(), + allow_additive_snapshots=set(), + ) + + sync_grants_mock.assert_called_once() + assert sync_grants_mock.call_args[0][1] == grants + + +def test_grants_clone_snapshot_in_dev( + adapter_mock: Mock, mocker: MockerFixture, make_snapshot: t.Callable[..., Snapshot] +): + adapter_mock.SUPPORTS_CLONING = True + sync_grants_mock = mocker.patch.object(adapter_mock, "sync_grants_config") + + evaluator = SnapshotEvaluator(adapter_mock) + grants = {"select": ["user1", "user2"]} + model = _create_grants_test_model(grants=grants, grants_target_layer=GrantsTargetLayer.ALL) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + evaluator._clone_snapshot_in_dev( + snapshot, {}, DeployabilityIndex.all_deployable(), {}, {}, set(), set() + ) + + sync_grants_mock.assert_called_once() + assert ( + sync_grants_mock.call_args[0][0].sql() + == f"sqlmesh__default.test_model__{snapshot.version}__dev" + ) + assert sync_grants_mock.call_args[0][1] == grants + + +@pytest.mark.parametrize( + "model_kind_name", + [ + "INCREMENTAL_BY_TIME_RANGE", + "SEED", + ], +) +def test_grants_evaluator_insert_without_replace_query_for_model( + model_kind_name: str, + adapter_mock: Mock, + mocker: MockerFixture, + make_snapshot: t.Callable[..., Snapshot], +): + adapter_mock.SUPPORTS_GRANTS = True + adapter_mock.table_exists.return_value = False # Table doesn't exist + sync_grants_mock = mocker.patch.object(adapter_mock, "sync_grants_config") + + evaluator = SnapshotEvaluator(adapter_mock) + + grants = {"select": ["reader1", "reader2"]} + model = _create_grants_test_model( + grants=grants, kind=model_kind_name, grants_target_layer=GrantsTargetLayer.ALL + ) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + evaluator.evaluate( + snapshot, + start="2023-01-01", + end="2023-01-01", + execution_time="2023-01-01", + snapshots={}, + ) + + # Grants are applied during the table creation phase, not during insert + sync_grants_mock.assert_called_once() + assert sync_grants_mock.call_args[0][1] == grants + + sync_grants_mock.reset_mock() + adapter_mock.table_exists.return_value = True + snapshot.add_interval("2023-01-01", "2023-01-01") + evaluator.evaluate( + snapshot, + start="2023-01-02", # Different date from existing interval + end="2023-01-02", + execution_time="2023-01-02", + snapshots={}, + ) + + # Should not apply grants since it's not the first insert + sync_grants_mock.assert_not_called() + + +@pytest.mark.parametrize( + "model_kind_name", + [ + "INCREMENTAL_BY_PARTITION", + "INCREMENTAL_BY_UNIQUE_KEY", + "INCREMENTAL_UNMANAGED", + "FULL", + "SCD_TYPE_2", + ], +) +def test_grants_evaluator_insert_with_replace_query_for_model( + model_kind_name: str, + adapter_mock: Mock, + mocker: MockerFixture, + make_snapshot: t.Callable[..., Snapshot], +): + adapter_mock.SUPPORTS_GRANTS = True + sync_grants_mock = mocker.patch.object(adapter_mock, "sync_grants_config") + adapter_mock.table_exists.return_value = False # Table doesn't exist + adapter_mock.columns.return_value = { + "id": exp.DataType.build("int"), + "ds": exp.DataType.build("date"), + } + + evaluator = SnapshotEvaluator(adapter_mock) + + grants = {"select": ["user1"]} + model = _create_grants_test_model( + grants=grants, kind=model_kind_name, grants_target_layer=GrantsTargetLayer.ALL + ) + snapshot = make_snapshot(model) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + + # Now evaluate the snapshot (this should apply grants during first insert) + evaluator.evaluate( + snapshot, + start="2023-01-01", + end="2023-01-01", + execution_time="2023-01-01", + snapshots={}, + ) + + # Should be called twice more during evaluate: once creating table, + # once during first insert with _replace_query_for_model() + assert sync_grants_mock.call_count == 2 + assert sync_grants_mock.call_args[0][1] == grants + + sync_grants_mock.reset_mock() + adapter_mock.table_exists.return_value = True + snapshot.add_interval("2023-01-01", "2023-01-01") + evaluator.evaluate( + snapshot, + start="2023-01-02", # Different date from existing interval + end="2023-01-02", + execution_time="2023-01-02", + snapshots={}, + ) + + if model_kind_name in ("FULL", "SCD_TYPE_2"): + # Full refresh and SCD_TYPE_2 always recreate the table, so grants are always applied + sync_grants_mock.assert_called_once() + assert sync_grants_mock.call_args[0][1] == grants + else: + # Should not apply grants since it's not the first insert + sync_grants_mock.assert_not_called() + + +@pytest.mark.parametrize( + "model_grants_target_layer", + [ + GrantsTargetLayer.ALL, + GrantsTargetLayer.VIRTUAL, + GrantsTargetLayer.PHYSICAL, + ], +) +def test_grants_in_production_with_dev_only_vde( + adapter_mock: Mock, + mocker: MockerFixture, + make_snapshot: t.Callable[..., Snapshot], + model_grants_target_layer: GrantsTargetLayer, +): + adapter_mock.SUPPORTS_GRANTS = True + sync_grants_mock = mocker.patch.object(adapter_mock, "sync_grants_config") + + from sqlmesh.core.model.meta import VirtualEnvironmentMode, GrantsTargetLayer + from sqlmesh.core.snapshot.definition import DeployabilityIndex + + model_virtual_grants = _create_grants_test_model( + grants={"select": ["user1"], "insert": ["role1"]}, + grants_target_layer=model_grants_target_layer, + virtual_environment_mode=VirtualEnvironmentMode.DEV_ONLY, + ) + + snapshot = make_snapshot(model_virtual_grants) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + evaluator = SnapshotEvaluator(adapter_mock) + # create will apply grants to physical layer tables + deployability_index = DeployabilityIndex.all_deployable() + evaluator.create([snapshot], {}, deployability_index=deployability_index) + + sync_grants_mock.assert_called_once() + assert sync_grants_mock.call_args[0][1] == {"select": ["user1"], "insert": ["role1"]} + + # Non-deployable (dev) env + sync_grants_mock.reset_mock() + deployability_index = DeployabilityIndex.none_deployable() + evaluator.create([snapshot], {}, deployability_index=deployability_index) + if model_grants_target_layer == GrantsTargetLayer.VIRTUAL: + sync_grants_mock.assert_not_called() + else: + # Should still apply grants to physical table when target layer is ALL or PHYSICAL + sync_grants_mock.assert_called_once() + assert sync_grants_mock.call_args[0][1] == {"select": ["user1"], "insert": ["role1"]} diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index e29c6768bf..eb16a4b4b1 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -9,10 +9,12 @@ from sqlmesh.core.model import TimeColumn, IncrementalByTimeRangeKind from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange from sqlmesh.core.state_sync.db.snapshot import _snapshot_to_json +from sqlmesh.core.config.common import VirtualEnvironmentMode +from sqlmesh.core.model.meta import GrantsTargetLayer from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.model import ModelConfig -from sqlmesh.dbt.target import PostgresConfig +from sqlmesh.dbt.target import BigQueryConfig, DuckDbConfig, PostgresConfig from sqlmesh.dbt.test import TestConfig from sqlmesh.utils.yaml import YAML from sqlmesh.utils.date import to_ds @@ -853,3 +855,176 @@ def test_load_custom_materialisations(sushi_test_dbt_context: Context) -> None: context.load() assert context.get_model("sushi.custom_incremental_model") assert context.get_model("sushi.custom_incremental_with_filter") + + +def test_model_grants_to_sqlmesh_grants_config() -> None: + grants_config = { + "select": ["user1", "user2"], + "insert": ["admin_user"], + "update": ["power_user"], + } + model_config = ModelConfig( + name="test_model", + sql="SELECT 1 as id", + grants=grants_config, + path=Path("test_model.sql"), + ) + + context = DbtContext() + context.project_name = "test_project" + context.target = DuckDbConfig(name="target", schema="test_schema") + + sqlmesh_model = model_config.to_sqlmesh( + context, virtual_environment_mode=VirtualEnvironmentMode.FULL + ) + + model_grants = sqlmesh_model.grants + assert model_grants == grants_config + + assert sqlmesh_model.grants_target_layer == GrantsTargetLayer.default + + +def test_model_grants_empty_permissions() -> None: + model_config = ModelConfig( + name="test_model_empty", + sql="SELECT 1 as id", + grants={"select": [], "insert": ["admin_user"]}, + path=Path("test_model_empty.sql"), + ) + + context = DbtContext() + context.project_name = "test_project" + context.target = DuckDbConfig(name="target", schema="test_schema") + + sqlmesh_model = model_config.to_sqlmesh( + context, virtual_environment_mode=VirtualEnvironmentMode.FULL + ) + + model_grants = sqlmesh_model.grants + expected_grants = {"select": [], "insert": ["admin_user"]} + assert model_grants == expected_grants + + +def test_model_no_grants() -> None: + model_config = ModelConfig( + name="test_model_no_grants", + sql="SELECT 1 as id", + path=Path("test_model_no_grants.sql"), + ) + + context = DbtContext() + context.project_name = "test_project" + context.target = DuckDbConfig(name="target", schema="test_schema") + + sqlmesh_model = model_config.to_sqlmesh( + context, virtual_environment_mode=VirtualEnvironmentMode.FULL + ) + + grants_config = sqlmesh_model.grants + assert grants_config is None + + +def test_model_empty_grants() -> None: + model_config = ModelConfig( + name="test_model_empty_grants", + sql="SELECT 1 as id", + grants={}, + path=Path("test_model_empty_grants.sql"), + ) + + context = DbtContext() + context.project_name = "test_project" + context.target = DuckDbConfig(name="target", schema="test_schema") + + sqlmesh_model = model_config.to_sqlmesh( + context, virtual_environment_mode=VirtualEnvironmentMode.FULL + ) + + grants_config = sqlmesh_model.grants + assert grants_config is None + + +def test_model_grants_valid_special_characters() -> None: + valid_grantees = [ + "user@domain.com", + "service-account@project.iam.gserviceaccount.com", + "group:analysts", + '"quoted user"', + "`backtick user`", + "user_with_underscores", + "user.with.dots", + ] + + model_config = ModelConfig( + name="test_model_special_chars", + sql="SELECT 1 as id", + grants={"select": valid_grantees}, + path=Path("test_model.sql"), + ) + + context = DbtContext() + context.project_name = "test_project" + context.target = DuckDbConfig(name="target", schema="test_schema") + + sqlmesh_model = model_config.to_sqlmesh( + context, virtual_environment_mode=VirtualEnvironmentMode.FULL + ) + + grants_config = sqlmesh_model.grants + assert grants_config is not None + assert "select" in grants_config + assert grants_config["select"] == valid_grantees + + +def test_model_grants_engine_specific_bigquery() -> None: + model_config = ModelConfig( + name="test_model_bigquery", + sql="SELECT 1 as id", + grants={ + "bigquery.dataviewer": ["user@domain.com"], + "select": ["analyst@company.com"], + }, + path=Path("test_model.sql"), + ) + + context = DbtContext() + context.project_name = "test_project" + context.target = BigQueryConfig( + name="bigquery_target", + project="test-project", + dataset="test_dataset", + location="US", + database="test-project", + schema="test_dataset", + ) + + sqlmesh_model = model_config.to_sqlmesh( + context, virtual_environment_mode=VirtualEnvironmentMode.FULL + ) + + grants_config = sqlmesh_model.grants + assert grants_config is not None + assert grants_config["bigquery.dataviewer"] == ["user@domain.com"] + assert grants_config["select"] == ["analyst@company.com"] + + +def test_ephemeral_model_ignores_grants() -> None: + """Test that ephemeral models ignore grants configuration.""" + model_config = ModelConfig( + name="ephemeral_model", + sql="SELECT 1 as id", + materialized="ephemeral", + grants={"select": ["reporter", "analyst"]}, + path=Path("ephemeral_model.sql"), + ) + + context = DbtContext() + context.project_name = "test_project" + context.target = DuckDbConfig(name="target", schema="test_schema") + + sqlmesh_model = model_config.to_sqlmesh( + context, virtual_environment_mode=VirtualEnvironmentMode.FULL + ) + + assert sqlmesh_model.kind.is_embedded + assert sqlmesh_model.grants is None # grants config is skipped for ephemeral / embedded models From 93c7a10c1892e41b56360ccae76526ce41025c41 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:41:12 +0300 Subject: [PATCH 0935/1056] Chore: make console width deterministic in tests (#5477) --- tests/cli/test_cli.py | 6 ++++-- tests/utils/test_helpers.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 1be44e18f9..d2df451fef 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -32,7 +32,7 @@ def mock_runtime_env(monkeypatch): @pytest.fixture(scope="session") def runner() -> CliRunner: - return CliRunner() + return CliRunner(env={"COLUMNS": "80"}) @contextmanager @@ -1887,7 +1887,9 @@ def test_init_interactive_cli_mode_simple(runner: CliRunner, tmp_path: Path): assert "no_diff: true" in config_path.read_text() -def test_init_interactive_engine_install_msg(runner: CliRunner, tmp_path: Path): +def test_init_interactive_engine_install_msg(runner: CliRunner, tmp_path: Path, monkeypatch): + monkeypatch.setattr("sqlmesh.utils.rich.console.width", 80) + # Engine install text should not appear for built-in engines like DuckDB # Input: 1 (DEFAULT template), 1 (duckdb engine), 1 (DEFAULT CLI mode) result = runner.invoke( diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index ae0742f1db..20a544512e 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -83,6 +83,7 @@ def test_wrapper(*args, **kwargs): orig_console = get_console() try: new_console = TerminalConsole() + new_console.console.width = 80 new_console.console.no_color = True set_console(new_console) func(*args, **kwargs) From 683133a7bae7b664059e58e02f06e7c96efc58c7 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:47:38 +0300 Subject: [PATCH 0936/1056] Chore: Fix flaky test by unliking dbt msgpack for deterministic behaviour (#5479) --- tests/dbt/cli/test_run.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/dbt/cli/test_run.py b/tests/dbt/cli/test_run.py index 7aeb8dd4d7..755553bb57 100644 --- a/tests/dbt/cli/test_run.py +++ b/tests/dbt/cli/test_run.py @@ -65,6 +65,12 @@ def test_run_with_changes_and_full_refresh( "select a, b, 'changed' as c from {{ ref('model_a') }}" ) + # Clear dbt's partial parse cache to ensure file changes are detected + # Without it dbt may use stale cached model definitions, causing flakiness + partial_parse_file = project_path / "target" / "sqlmesh_partial_parse.msgpack" + if partial_parse_file.exists(): + partial_parse_file.unlink() + # run with --full-refresh. this should: # - fully refresh model_a (pick up the new records from external_table) # - deploy the local change to model_b (introducing the 'changed' column) From 285b8f228b146dcce499be78f3b6708962ba53c5 Mon Sep 17 00:00:00 2001 From: Tori Wei <41123940+toriwei@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:53:44 -0700 Subject: [PATCH 0937/1056] fix: parse column from cast expression for ScdType2 models (#5475) --- sqlmesh/dbt/model.py | 16 ++++++++++++++++ tests/dbt/test_transformation.py | 17 +++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index f21eefe95d..09c410561d 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -172,6 +172,22 @@ def _validate_check_cols(cls, v: t.Union[str, t.List[str]]) -> t.Union[str, t.Li return "*" return ensure_list(v) + @field_validator("updated_at", mode="before") + @classmethod + def _validate_updated_at(cls, v: t.Optional[str]) -> t.Optional[str]: + """ + Extract column name if updated_at contains a cast. + + SCDType2ByTimeKind and SCDType2ByColumnKind expect a column, and the casting is done later. + """ + if v is None: + return None + parsed = d.parse_one(v) + if isinstance(parsed, exp.Cast) and isinstance(parsed.this, exp.Column): + return parsed.this.name + + return v + @field_validator("sql", mode="before") @classmethod def _validate_sql(cls, v: t.Union[str, SqlStr]) -> SqlStr: diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index a33e3ed843..141c160e7e 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -652,6 +652,23 @@ def test_model_kind(): == ManagedKind() ) + assert ModelConfig( + materialized=Materialization.SNAPSHOT, + unique_key=["id"], + updated_at="updated_at::timestamp", + strategy="timestamp", + dialect="redshift", + ).model_kind(context) == SCDType2ByTimeKind( + unique_key=["id"], + valid_from_name="dbt_valid_from", + valid_to_name="dbt_valid_to", + updated_at_as_valid_from=True, + updated_at_name="updated_at", + dialect="redshift", + on_destructive_change=OnDestructiveChange.IGNORE, + on_additive_change=OnAdditiveChange.ALLOW, + ) + def test_model_kind_snapshot_bigquery(): context = DbtContext() From 35269c906b3c08cbc3ab4377df66cbb611c1bd49 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 3 Oct 2025 14:21:37 -0700 Subject: [PATCH 0938/1056] fix(web_common): make styling in some components more flexible (#5482) --- .../LineageColumnLevel/FactoryColumn.css | 31 ++++ .../LineageColumnLevel/FactoryColumn.tsx | 15 +- .../Lineage/LineageControlButton.tsx | 2 +- .../components/Lineage/LineageControlIcon.tsx | 1 + .../src/components/Lineage/LineageLayout.tsx | 1 + .../src/components/Lineage/help.test.ts | 142 ------------------ web/common/src/components/Lineage/help.ts | 41 ----- .../components/Lineage/node/NodeAppendix.tsx | 1 + .../src/components/Lineage/node/NodeBadge.tsx | 3 +- .../components/Lineage/node/NodeDetail.tsx | 1 + .../components/Lineage/node/NodeDivider.tsx | 7 +- .../components/Lineage/node/NodeHandle.tsx | 1 + .../Lineage/node/NodeHandleIcon.tsx | 1 + .../components/Lineage/node/NodeHeader.tsx | 1 + .../src/components/Metadata/Metadata.tsx | 2 +- .../components/VirtualList/FilterableList.css | 7 + .../components/VirtualList/FilterableList.tsx | 2 +- 17 files changed, 67 insertions(+), 192 deletions(-) create mode 100644 web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.css diff --git a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.css b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.css new file mode 100644 index 0000000000..d6eea6674a --- /dev/null +++ b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.css @@ -0,0 +1,31 @@ +:root { + --color-lineage-model-column-badge-background: var( + --color-lineage-node-badge-background + ); + --color-lineage-model-column-badge-foreground: var( + --color-lineage-node-badge-foreground + ); + + --color-lineage-model-column-metadata-label: var(--color-metadata-label); + --color-lineage-model-column-metadata-value: var(--color-metadata-value); + + --color-lineage-model-column-information-info: var(--color-information-info); +} + +.FactoryColumn__Metadata { + --color-metadata-label: var(--color-lineage-model-column-metadata-label); + --color-metadata-value: var(--color-lineage-model-column-metadata-value); +} + +.FactoryColumn__NodeBadge { + --color-lineage-node-badge-background: var( + --color-lineage-model-column-badge-background + ); + --color-lineage-node-badge-foreground: var( + --color-lineage-model-column-badge-foreground + ); +} + +.FactoryColumn__Information { + --color-typography-info: var(--color-lineage-model-column-information-info); +} diff --git a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx index 7b5e9e0ae0..19b73c3ef6 100644 --- a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx +++ b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx @@ -21,6 +21,8 @@ import { HorizontalContainer } from '@/components/HorizontalContainer/Horizontal import { Information } from '@/components/Typography/Information' import { LoadingContainer } from '@/components/LoadingContainer/LoadingContainer' +import './FactoryColumn.css' + export function FactoryColumn< TAdjacencyListKey extends string, TAdjacencyListColumnKey extends string, @@ -184,7 +186,7 @@ export function FactoryColumn< function renderColumn() { return ( {renderColumnStates()} {description ? ( - + ) : ( @@ -205,9 +210,11 @@ export function FactoryColumn< } - value={{type}} + value={ + {type} + } className={cn( - 'relative overflow-visible group p-0', + 'FactoryColumn__Metadata relative overflow-visible group p-0', isDisabledColumn && 'cursor-not-allowed', className, )} diff --git a/web/common/src/components/Lineage/LineageControlButton.tsx b/web/common/src/components/Lineage/LineageControlButton.tsx index 5f1abaa952..6f66f90db7 100644 --- a/web/common/src/components/Lineage/LineageControlButton.tsx +++ b/web/common/src/components/Lineage/LineageControlButton.tsx @@ -23,7 +23,7 @@ export function LineageControlButton({ delayDuration={0} className="px-2 py-1 text-xs rounded-sm font-semibold bg-lineage-control-button-tooltip-background text-lineage-control-button-tooltip-foreground" trigger={ -
      +
      { }) }) - describe('getTransformedModelEdges', () => { - test('should transform edges using the provided transform function', () => { - const adjacencyListKeys = ['model1', 'model2', 'model3'] - const lineageAdjacencyList: LineageAdjacencyList = { - model1: ['model2', 'model3'], - model2: ['model3'], - model3: [], - } - - const transformEdge = ( - type: string, - edgeId: EdgeId, - sourceId: NodeId, - targetId: NodeId, - ) => ({ - id: edgeId, - source: sourceId, - target: targetId, - type, - zIndex: 1, - }) - - const result = getTransformedModelEdges( - adjacencyListKeys, - lineageAdjacencyList, - transformEdge, - ) - - expect(result).toHaveLength(3) - - const model1Id = toNodeID('model1') - const model2Id = toNodeID('model2') - const model3Id = toNodeID('model3') - - expect(result[0]).toEqual({ - id: toEdgeID('model1', 'model2'), - source: model1Id, - target: model2Id, - type: 'edge', - zIndex: 1, - }) - expect(result[1]).toEqual({ - id: toEdgeID('model1', 'model3'), - source: model1Id, - target: model3Id, - type: 'edge', - zIndex: 1, - }) - expect(result[2]).toEqual({ - id: toEdgeID('model2', 'model3'), - source: model2Id, - target: model3Id, - type: 'edge', - zIndex: 1, - }) - }) - - test('should skip edges where target is not in adjacency list', () => { - const adjacencyListKeys = ['model1'] - const lineageAdjacencyList: LineageAdjacencyList = { - model1: ['model2'], // model2 is not in the adjacency list - } - - const transformEdge = ( - type: string, - edgeId: EdgeId, - sourceId: NodeId, - targetId: NodeId, - ) => ({ - id: edgeId, - source: sourceId, - target: targetId, - type, - zIndex: 1, - }) - - const result = getTransformedModelEdges( - adjacencyListKeys, - lineageAdjacencyList, - transformEdge, - ) - - expect(result).toHaveLength(0) - }) - - test('should handle empty adjacency list', () => { - const adjacencyListKeys: string[] = [] - const lineageAdjacencyList: LineageAdjacencyList = {} - - const transformEdge = ( - type: string, - edgeId: EdgeId, - sourceId: NodeId, - targetId: NodeId, - ) => ({ - id: edgeId, - source: sourceId, - target: targetId, - type, - zIndex: 1, - }) - - const result = getTransformedModelEdges( - adjacencyListKeys, - lineageAdjacencyList, - transformEdge, - ) - - expect(result).toHaveLength(0) - }) - - test('should handle nodes with no targets', () => { - const adjacencyListKeys = ['model1', 'model2'] - const lineageAdjacencyList = { - model1: [], - model2: null, - } as unknown as LineageAdjacencyList - - const transformEdge = ( - type: string, - edgeId: EdgeId, - sourceId: NodeId, - targetId: NodeId, - ) => ({ - id: edgeId, - source: sourceId, - target: targetId, - type, - zIndex: 1, - }) - - const result = getTransformedModelEdges( - adjacencyListKeys, - lineageAdjacencyList, - transformEdge, - ) - - expect(result).toHaveLength(0) - }) - }) - describe('getTransformedModelEdgesSourceTargets', () => { test('should transform edges from source to targets using the provided transform function', () => { const adjacencyListKeys = ['model1', 'model2', 'model3'] diff --git a/web/common/src/components/Lineage/help.ts b/web/common/src/components/Lineage/help.ts index 1e5d5a9d6b..97f4ad9542 100644 --- a/web/common/src/components/Lineage/help.ts +++ b/web/common/src/components/Lineage/help.ts @@ -57,47 +57,6 @@ export function getTransformedNodes< return nodesMap } -export function getTransformedModelEdges< - TAdjacencyListKey extends string, - TEdgeData extends LineageEdgeData = LineageEdgeData, - TNodeID extends string = NodeId, - TEdgeID extends string = EdgeId, - TPortID extends string = PortId, ->( - adjacencyListKeys: TAdjacencyListKey[], - lineageAdjacencyList: LineageAdjacencyList, - transformEdge: TransformEdgeFn, -) { - const nodesCount = adjacencyListKeys.length - - if (nodesCount === 0) return [] - - const edges = [] - - for (let i = 0; i < nodesCount; i++) { - const adjacencyListKey = adjacencyListKeys[i] - const nodeId = toNodeID(adjacencyListKey) - const targets = lineageAdjacencyList[adjacencyListKey] - const targetsCount = targets?.length || 0 - - if (targets == null || targetsCount < 1) continue - - for (let j = 0; j < targetsCount; j++) { - const target = targets[j] - - if (!(target in lineageAdjacencyList)) continue - - const edgeId = toEdgeID(adjacencyListKey, target) - - edges.push( - transformEdge('edge', edgeId, nodeId, toNodeID(target)), - ) - } - } - - return edges -} - export function getTransformedModelEdgesSourceTargets< TAdjacencyListKey extends string, TEdgeData extends LineageEdgeData = LineageEdgeData, diff --git a/web/common/src/components/Lineage/node/NodeAppendix.tsx b/web/common/src/components/Lineage/node/NodeAppendix.tsx index 76d64affed..5a703a468f 100644 --- a/web/common/src/components/Lineage/node/NodeAppendix.tsx +++ b/web/common/src/components/Lineage/node/NodeAppendix.tsx @@ -32,6 +32,7 @@ export const NodeAppendix = forwardRef( return (
      diff --git a/web/common/src/components/Lineage/node/NodeBadge.tsx b/web/common/src/components/Lineage/node/NodeBadge.tsx index 943e5e9267..8c894ecca2 100644 --- a/web/common/src/components/Lineage/node/NodeBadge.tsx +++ b/web/common/src/components/Lineage/node/NodeBadge.tsx @@ -8,8 +8,9 @@ export const NodeBadge = React.forwardRef( return ( {hasDivider && } + return ( +
      + ) } diff --git a/web/common/src/components/Lineage/node/NodeHandle.tsx b/web/common/src/components/Lineage/node/NodeHandle.tsx index e737ff4327..4bfbfa6181 100644 --- a/web/common/src/components/Lineage/node/NodeHandle.tsx +++ b/web/common/src/components/Lineage/node/NodeHandle.tsx @@ -18,6 +18,7 @@ export const NodeHandle = React.memo(function NodeHandle({ }) { return ( ( ({ className, ...props }, ref) => { return (
      ( ref={ref} data-component="Metadata" className={cn( - 'justify-between gap-2 items-center whitespace-nowrap h-auto', + 'Metadata justify-between gap-2 items-center whitespace-nowrap h-auto', className, )} {...props} diff --git a/web/common/src/components/VirtualList/FilterableList.css b/web/common/src/components/VirtualList/FilterableList.css index 4dfdd87eea..3b1a8f8c3d 100644 --- a/web/common/src/components/VirtualList/FilterableList.css +++ b/web/common/src/components/VirtualList/FilterableList.css @@ -7,3 +7,10 @@ --color-filterable-list-input-placeholder: var(--color-input-placeholder); --color-filterable-list-input-border: var(--color-input-border); } + +.FilterableList__Input { + --color-input-background: var(--color-filterable-list-input-background); + --color-input-foreground: var(--color-filterable-list-input-foreground); + --color-input-placeholder: var(--color-filterable-list-input-placeholder); + --color-input-border: var(--color-filterable-list-input-border); +} diff --git a/web/common/src/components/VirtualList/FilterableList.tsx b/web/common/src/components/VirtualList/FilterableList.tsx index 5ea0d35039..d22bfca784 100644 --- a/web/common/src/components/VirtualList/FilterableList.tsx +++ b/web/common/src/components/VirtualList/FilterableList.tsx @@ -57,7 +57,7 @@ export function FilterableList({ setSearch(e.target.value) } inputSize="xs" - className="w-full" + className="FilterableList__Input w-full" /> Date: Fri, 3 Oct 2025 15:51:19 -0700 Subject: [PATCH 0939/1056] fix(web_common): fix layout update (#5483) --- web/common/src/components/Lineage/LineageLayout.tsx | 2 +- web/common/src/components/ModelName/ModelName.css | 2 ++ web/common/src/components/ModelName/ModelName.tsx | 2 +- web/common/tailwind.base.config.js | 4 ++++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/common/src/components/Lineage/LineageLayout.tsx b/web/common/src/components/Lineage/LineageLayout.tsx index 7340fa2656..2ad31c4b9e 100644 --- a/web/common/src/components/Lineage/LineageLayout.tsx +++ b/web/common/src/components/Lineage/LineageLayout.tsx @@ -348,7 +348,7 @@ function LineageLayoutBase< }) } } - }, [currentNode?.id, setSelectedNodeId, nodes, setCenter]) + }, [currentNode?.id, setSelectedNodeId, setCenter]) return ( ( size="2xs" variant="transparent" text={name} - className="ml-2 w-6 hover:text-model-name-copy-icon-hover active:text-model-name-copy-icon-hover" + className="ml-2 w-6 hover:text-model-name-copy-icon-hover active:text-model-name-copy-icon-hover bg-model-name-copy-icon-background hover:bg-model-name-copy-icon-background-hover active:bg-model-name-copy-icon-background-hover" > {copied => copied ? ( diff --git a/web/common/tailwind.base.config.js b/web/common/tailwind.base.config.js index 49354591cc..8f385b53dc 100644 --- a/web/common/tailwind.base.config.js +++ b/web/common/tailwind.base.config.js @@ -71,6 +71,10 @@ export default { model: 'var(--color-model-name-model)', 'copy-icon': 'var(--color-model-name-copy-icon)', 'copy-icon-hover': 'var(--color-model-name-copy-icon-hover)', + 'copy-icon-background': + 'var(--color-model-name-copy-icon-background)', + 'copy-icon-background-hover': + 'var(--color-model-name-copy-icon-background-hover)', }, badge: { background: 'var(--color-badge-background)', From 223422fca0ab324630ac63c5e14a8a0434035250 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 6 Oct 2025 10:30:24 -0700 Subject: [PATCH 0940/1056] fix(web_common): clean up lineage layout component (#5487) --- .../src/components/Lineage/LineageContext.ts | 4 - .../src/components/Lineage/LineageLayout.tsx | 404 +----------------- .../components/Lineage/LineageLayoutBase.tsx | 367 ++++++++++++++++ .../Lineage/LineageLayoutContainer.tsx | 43 ++ .../Lineage/stories/ModelLineage.tsx | 2 - .../components/VirtualList/FilterableList.tsx | 3 + 6 files changed, 432 insertions(+), 391 deletions(-) create mode 100644 web/common/src/components/Lineage/LineageLayoutBase.tsx create mode 100644 web/common/src/components/Lineage/LineageLayoutContainer.tsx diff --git a/web/common/src/components/Lineage/LineageContext.ts b/web/common/src/components/Lineage/LineageContext.ts index 6f4ee7e165..9da54dcbee 100644 --- a/web/common/src/components/Lineage/LineageContext.ts +++ b/web/common/src/components/Lineage/LineageContext.ts @@ -30,8 +30,6 @@ export interface LineageContextValue< setSelectedNodeId: React.Dispatch> // Layout - isBuildingLayout: boolean - setIsBuildingLayout: React.Dispatch> zoom: number setZoom: React.Dispatch> @@ -66,8 +64,6 @@ export function getInitial< nodes: [], nodesMap: {}, setNodesMap: () => {}, - isBuildingLayout: false, - setIsBuildingLayout: () => {}, currentNode: null, } } diff --git a/web/common/src/components/Lineage/LineageLayout.tsx b/web/common/src/components/Lineage/LineageLayout.tsx index 2ad31c4b9e..e01e8ae9e9 100644 --- a/web/common/src/components/Lineage/LineageLayout.tsx +++ b/web/common/src/components/Lineage/LineageLayout.tsx @@ -1,52 +1,25 @@ import { - Background, - BackgroundVariant, - Controls, - type EdgeChange, type EdgeTypes, - type NodeChange, type NodeTypes, - ReactFlow, ReactFlowProvider, type SetCenter, - getConnectedEdges, - getIncomers, - getOutgoers, - useReactFlow, - useViewport, - applyNodeChanges, - applyEdgeChanges, } from '@xyflow/react' -import '@xyflow/react/dist/style.css' -import './Lineage.css' - -import { debounce } from 'lodash' -import { CircuitBoard, Crosshair, LocateFixed, RotateCcw } from 'lucide-react' import React from 'react' -import { cn } from '@/utils' import { type LineageContextHook } from './LineageContext' -import { LineageControlButton } from './LineageControlButton' -import { LineageControlIcon } from './LineageControlIcon' + import { - DEFAULT_ZOOM, - type LineageEdge, type LineageEdgeData, type LineageNode, type LineageNodeData, - MAX_ZOOM, - MIN_ZOOM, - NODES_TRESHOLD, - NODES_TRESHOLD_ZOOM, type NodeId, type EdgeId, - ZOOM_THRESHOLD, type PortId, } from './utils' -import { VerticalContainer } from '../VerticalContainer/VerticalContainer' -import { MessageContainer } from '../MessageContainer/MessageContainer' -import { LoadingContainer } from '../LoadingContainer/LoadingContainer' + +import { LineageLayoutBase } from './LineageLayoutBase' +import { LineageLayoutContainer } from './LineageLayoutContainer' export function LineageLayout< TNodeData extends LineageNodeData = LineageNodeData, @@ -61,6 +34,7 @@ export function LineageLayout< controls, nodesDraggable, nodesConnectable, + isBuildingLayout, useLineage, onNodeClick, onNodeDoubleClick, @@ -72,6 +46,7 @@ export function LineageLayout< TEdgeID, TPortID > + isBuildingLayout?: boolean nodeTypes?: NodeTypes edgeTypes?: EdgeTypes className?: string @@ -91,360 +66,19 @@ export function LineageLayout< }) { return ( - + + + ) } - -function LineageLayoutBase< - TNodeData extends LineageNodeData = LineageNodeData, - TEdgeData extends LineageEdgeData = LineageEdgeData, - TNodeID extends string = NodeId, - TEdgeID extends string = EdgeId, - TPortID extends string = PortId, ->({ - nodeTypes, - edgeTypes, - className, - controls, - nodesDraggable = false, - nodesConnectable = false, - useLineage, - onNodeClick, - onNodeDoubleClick, -}: { - useLineage: LineageContextHook< - TNodeData, - TEdgeData, - TNodeID, - TEdgeID, - TPortID - > - nodesDraggable?: boolean - nodesConnectable?: boolean - nodeTypes?: NodeTypes - edgeTypes?: EdgeTypes - className?: string - controls?: - | React.ReactNode - | (({ setCenter }: { setCenter: SetCenter }) => React.ReactNode) - onNodeClick?: ( - event: React.MouseEvent, - node: LineageNode, - ) => void - onNodeDoubleClick?: ( - event: React.MouseEvent, - node: LineageNode, - ) => void -}) { - const { zoom: viewportZoom } = useViewport() - const { setCenter } = useReactFlow() - - const { - isBuildingLayout, - currentNode, - zoom, - nodes: initialNodes, - edges: initialEdges, - nodesMap, - showOnlySelectedNodes, - selectedNodeId, - setZoom, - setSelectedNodeId, - setShowOnlySelectedNodes, - setSelectedNodes, - setSelectedEdges, - } = useLineage() - - const [nodes, setNodes] = React.useState(initialNodes) - const [edges, setEdges] = React.useState(initialEdges) - - const onNodesChange = React.useCallback( - (changes: NodeChange>[]) => { - setNodes( - applyNodeChanges>(changes, nodes), - ) - }, - [nodes, setNodes], - ) - - const onEdgesChange = React.useCallback( - ( - changes: EdgeChange>[], - ) => { - setEdges( - applyEdgeChanges>( - changes, - edges, - ), - ) - }, - [edges, setEdges], - ) - - const updateZoom = React.useMemo(() => debounce(setZoom, 200), [setZoom]) - - const zoomToCurrentNode = React.useCallback( - (zoom: number = DEFAULT_ZOOM) => { - if (currentNode) { - setCenter(currentNode.position.x, currentNode.position.y, { - zoom, - duration: 0, - }) - } - }, - [currentNode, setCenter], - ) - - const zoomToSelectedNode = React.useCallback( - (zoom: number = DEFAULT_ZOOM) => { - const node = selectedNodeId ? nodesMap[selectedNodeId] : null - if (node) { - setCenter(node.position.x, node.position.y, { - zoom, - duration: 0, - }) - } - }, - [nodesMap, selectedNodeId, setCenter], - ) - - const getAllIncomers = React.useCallback( - ( - node: LineageNode, - visited: Set = new Set(), - ): LineageNode[] => { - if (visited.has(node.id)) return [] - - visited.add(node.id) - - return Array.from( - new Set>([ - node, - ...getIncomers(node, nodes, edges) - .map(n => getAllIncomers(n, visited)) - .flat(), - ]), - ) - }, - [nodes, edges], - ) - - const getAllOutgoers = React.useCallback( - ( - node: LineageNode, - visited: Set = new Set(), - ): LineageNode[] => { - if (visited.has(node.id)) return [] - - visited.add(node.id) - - return Array.from( - new Set>([ - node, - ...getOutgoers(node, nodes, edges) - .map(n => getAllOutgoers(n, visited)) - .flat(), - ]), - ) - }, - [nodes, edges], - ) - - React.useEffect(() => { - setNodes(initialNodes) - }, [initialNodes]) - - React.useEffect(() => { - setEdges(initialEdges) - }, [initialEdges]) - - React.useEffect(() => { - if (selectedNodeId == null) { - setShowOnlySelectedNodes(false) - setSelectedNodes(new Set()) - setSelectedEdges(new Set()) - - return - } - - const node = selectedNodeId ? nodesMap[selectedNodeId] : null - - if (node == null) { - setSelectedNodeId(null) - return - } - - const incomers = getAllIncomers(node) - const outgoers = getAllOutgoers(node) - const connectedNodes = [...incomers, ...outgoers] - - if (currentNode) { - connectedNodes.push(currentNode) - } - - const connectedEdges = getConnectedEdges< - LineageNode, - LineageEdge - >(connectedNodes, edges) - const selectedNodes = new Set(connectedNodes.map(node => node.id)) - const selectedEdges = new Set( - connectedEdges.reduce((acc, edge) => { - if ([edge.source, edge.target].every(id => selectedNodes.has(id))) { - edge.zIndex = 2 - acc.add(edge.id) - } else { - edge.zIndex = 1 - } - return acc - }, new Set()), - ) - - setSelectedNodes(selectedNodes) - setSelectedEdges(selectedEdges) - }, [ - currentNode, - selectedNodeId, - setSelectedNodes, - setSelectedEdges, - getAllIncomers, - getAllOutgoers, - setShowOnlySelectedNodes, - setSelectedNodeId, - ]) - - React.useEffect(() => { - if (selectedNodeId) { - zoomToSelectedNode(zoom) - } else { - zoomToCurrentNode(zoom) - } - }, [zoomToCurrentNode, zoomToSelectedNode]) - - React.useEffect(() => { - updateZoom(viewportZoom) - }, [updateZoom, viewportZoom]) - - React.useEffect(() => { - if (currentNode?.id) { - setSelectedNodeId(currentNode.id) - } else { - const node = nodes.length > 0 ? nodes[nodes.length - 1] : null - - if (node) { - setCenter(node.position.x, node.position.y, { - zoom: zoom, - duration: 0, - }) - } - } - }, [currentNode?.id, setSelectedNodeId, setCenter]) - - return ( - - {isBuildingLayout && ( - - - Building layout... - - - )} - , - LineageEdge - > - className="shrink-0" - nodes={nodes} - edges={edges} - nodeTypes={nodeTypes} - edgeTypes={edgeTypes} - onNodesChange={onNodesChange} - onEdgesChange={onEdgesChange} - nodesDraggable={nodesDraggable} - nodesConnectable={nodesConnectable} - zoomOnDoubleClick={false} - panOnScroll={true} - zoomOnScroll={true} - minZoom={nodes.length > NODES_TRESHOLD ? NODES_TRESHOLD_ZOOM : MIN_ZOOM} - maxZoom={MAX_ZOOM} - fitView={false} - nodeOrigin={[0.5, 0.5]} - onlyRenderVisibleElements - onNodeClick={onNodeClick} - onNodeDoubleClick={onNodeDoubleClick} - > - {zoom > ZOOM_THRESHOLD && ( - - )} - - {currentNode && ( - zoomToCurrentNode(DEFAULT_ZOOM)} - disabled={isBuildingLayout} - > - - - )} - {selectedNodeId && ( - <> - setShowOnlySelectedNodes(!showOnlySelectedNodes)} - disabled={isBuildingLayout} - > - - - zoomToSelectedNode(DEFAULT_ZOOM)} - disabled={isBuildingLayout} - > - - - - )} - {controls && typeof controls === 'function' - ? controls({ setCenter }) - : controls} - - - - ) -} diff --git a/web/common/src/components/Lineage/LineageLayoutBase.tsx b/web/common/src/components/Lineage/LineageLayoutBase.tsx new file mode 100644 index 0000000000..af47a82b29 --- /dev/null +++ b/web/common/src/components/Lineage/LineageLayoutBase.tsx @@ -0,0 +1,367 @@ +import { + Background, + BackgroundVariant, + Controls, + type EdgeChange, + type EdgeTypes, + type NodeChange, + type NodeTypes, + ReactFlow, + type SetCenter, + getConnectedEdges, + getIncomers, + getOutgoers, + useReactFlow, + useViewport, + applyNodeChanges, + applyEdgeChanges, +} from '@xyflow/react' + +import '@xyflow/react/dist/style.css' +import './Lineage.css' + +import { debounce } from 'lodash' +import { CircuitBoard, Crosshair, LocateFixed, RotateCcw } from 'lucide-react' +import React from 'react' + +import { type LineageContextHook } from './LineageContext' +import { LineageControlButton } from './LineageControlButton' +import { LineageControlIcon } from './LineageControlIcon' +import { + DEFAULT_ZOOM, + type LineageEdge, + type LineageEdgeData, + type LineageNode, + type LineageNodeData, + MAX_ZOOM, + MIN_ZOOM, + NODES_TRESHOLD, + NODES_TRESHOLD_ZOOM, + type NodeId, + type EdgeId, + ZOOM_THRESHOLD, + type PortId, +} from './utils' + +import '@xyflow/react/dist/style.css' +import './Lineage.css' +import { cn } from '@/utils' + +export function LineageLayoutBase< + TNodeData extends LineageNodeData = LineageNodeData, + TEdgeData extends LineageEdgeData = LineageEdgeData, + TNodeID extends string = NodeId, + TEdgeID extends string = EdgeId, + TPortID extends string = PortId, +>({ + nodeTypes, + edgeTypes, + className, + controls, + nodesDraggable = false, + nodesConnectable = false, + useLineage, + onNodeClick, + onNodeDoubleClick, +}: { + useLineage: LineageContextHook< + TNodeData, + TEdgeData, + TNodeID, + TEdgeID, + TPortID + > + nodesDraggable?: boolean + nodesConnectable?: boolean + nodeTypes?: NodeTypes + edgeTypes?: EdgeTypes + className?: string + controls?: + | React.ReactNode + | (({ setCenter }: { setCenter: SetCenter }) => React.ReactNode) + onNodeClick?: ( + event: React.MouseEvent, + node: LineageNode, + ) => void + onNodeDoubleClick?: ( + event: React.MouseEvent, + node: LineageNode, + ) => void +}) { + const { zoom: viewportZoom } = useViewport() + const { setCenter } = useReactFlow() + + const { + currentNode, + zoom, + nodes: initialNodes, + edges: initialEdges, + nodesMap, + showOnlySelectedNodes, + selectedNodeId, + setZoom, + setSelectedNodeId, + setShowOnlySelectedNodes, + setSelectedNodes, + setSelectedEdges, + } = useLineage() + + const [nodes, setNodes] = React.useState(initialNodes) + const [edges, setEdges] = React.useState(initialEdges) + + const onNodesChange = React.useCallback( + (changes: NodeChange>[]) => { + setNodes( + applyNodeChanges>(changes, nodes), + ) + }, + [nodes, setNodes], + ) + + const onEdgesChange = React.useCallback( + ( + changes: EdgeChange>[], + ) => { + setEdges( + applyEdgeChanges>( + changes, + edges, + ), + ) + }, + [edges, setEdges], + ) + + const updateZoom = React.useMemo(() => debounce(setZoom, 200), [setZoom]) + + const zoomToCurrentNode = React.useCallback( + (zoom: number = DEFAULT_ZOOM) => { + if (currentNode) { + setCenter(currentNode.position.x, currentNode.position.y, { + zoom, + duration: 0, + }) + } + }, + [currentNode, setCenter], + ) + + const zoomToSelectedNode = React.useCallback( + (zoom: number = DEFAULT_ZOOM) => { + const node = selectedNodeId ? nodesMap[selectedNodeId] : null + if (node) { + setCenter(node.position.x, node.position.y, { + zoom, + duration: 0, + }) + } + }, + [nodesMap, selectedNodeId, setCenter], + ) + + const getAllIncomers = React.useCallback( + ( + node: LineageNode, + visited: Set = new Set(), + ): LineageNode[] => { + if (visited.has(node.id)) return [] + + visited.add(node.id) + + return Array.from( + new Set>([ + node, + ...getIncomers(node, nodes, edges) + .map(n => getAllIncomers(n, visited)) + .flat(), + ]), + ) + }, + [nodes, edges], + ) + + const getAllOutgoers = React.useCallback( + ( + node: LineageNode, + visited: Set = new Set(), + ): LineageNode[] => { + if (visited.has(node.id)) return [] + + visited.add(node.id) + + return Array.from( + new Set>([ + node, + ...getOutgoers(node, nodes, edges) + .map(n => getAllOutgoers(n, visited)) + .flat(), + ]), + ) + }, + [nodes, edges], + ) + + React.useEffect(() => { + setNodes(initialNodes) + }, [initialNodes]) + + React.useEffect(() => { + setEdges(initialEdges) + }, [initialEdges]) + + React.useEffect(() => { + if (selectedNodeId == null) { + setShowOnlySelectedNodes(false) + setSelectedNodes(new Set()) + setSelectedEdges(new Set()) + + return + } + + const node = selectedNodeId ? nodesMap[selectedNodeId] : null + + if (node == null) { + setSelectedNodeId(null) + return + } + + const incomers = getAllIncomers(node) + const outgoers = getAllOutgoers(node) + const connectedNodes = [...incomers, ...outgoers] + + if (currentNode) { + connectedNodes.push(currentNode) + } + + const connectedEdges = getConnectedEdges< + LineageNode, + LineageEdge + >(connectedNodes, edges) + const selectedNodes = new Set(connectedNodes.map(node => node.id)) + const selectedEdges = new Set( + connectedEdges.reduce((acc, edge) => { + if ([edge.source, edge.target].every(id => selectedNodes.has(id))) { + edge.zIndex = 2 + acc.add(edge.id) + } else { + edge.zIndex = 1 + } + return acc + }, new Set()), + ) + + setSelectedNodes(selectedNodes) + setSelectedEdges(selectedEdges) + }, [ + currentNode, + selectedNodeId, + setSelectedNodes, + setSelectedEdges, + getAllIncomers, + getAllOutgoers, + setShowOnlySelectedNodes, + setSelectedNodeId, + ]) + + React.useEffect(() => { + if (selectedNodeId) { + zoomToSelectedNode(zoom) + } else { + zoomToCurrentNode(zoom) + } + }, [zoomToCurrentNode, zoomToSelectedNode]) + + React.useEffect(() => { + updateZoom(viewportZoom) + }, [updateZoom, viewportZoom]) + + React.useEffect(() => { + if (currentNode?.id) { + setSelectedNodeId(currentNode.id) + } else { + const node = nodes.length > 0 ? nodes[nodes.length - 1] : null + + if (node) { + setCenter(node.position.x, node.position.y, { + zoom: zoom, + duration: 0, + }) + } + } + }, [currentNode?.id, setSelectedNodeId, setCenter]) + + return ( + , + LineageEdge + > + className={cn('shrink-0', className)} + nodes={nodes} + edges={edges} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + nodesDraggable={nodesDraggable} + nodesConnectable={nodesConnectable} + zoomOnDoubleClick={false} + panOnScroll={true} + zoomOnScroll={true} + minZoom={nodes.length > NODES_TRESHOLD ? NODES_TRESHOLD_ZOOM : MIN_ZOOM} + maxZoom={MAX_ZOOM} + fitView={false} + nodeOrigin={[0.5, 0.5]} + onlyRenderVisibleElements + onNodeClick={onNodeClick} + onNodeDoubleClick={onNodeDoubleClick} + > + {zoom > ZOOM_THRESHOLD && ( + + )} + + {currentNode && ( + zoomToCurrentNode(DEFAULT_ZOOM)} + > + + + )} + {selectedNodeId && ( + <> + setShowOnlySelectedNodes(!showOnlySelectedNodes)} + > + + + zoomToSelectedNode(DEFAULT_ZOOM)} + > + + + + )} + {controls && typeof controls === 'function' + ? controls({ setCenter }) + : controls} + + + ) +} diff --git a/web/common/src/components/Lineage/LineageLayoutContainer.tsx b/web/common/src/components/Lineage/LineageLayoutContainer.tsx new file mode 100644 index 0000000000..e3385a3294 --- /dev/null +++ b/web/common/src/components/Lineage/LineageLayoutContainer.tsx @@ -0,0 +1,43 @@ +import { cn } from '@/utils' + +import React from 'react' + +import { VerticalContainer } from '../VerticalContainer/VerticalContainer' +import { MessageContainer } from '../MessageContainer/MessageContainer' +import { LoadingContainer } from '../LoadingContainer/LoadingContainer' + +export function LineageLayoutContainer({ + isBuildingLayout, + loadingMessage = 'Building layout...', + className, + children, +}: { + isBuildingLayout?: boolean + loadingMessage?: string + className?: string + children: React.ReactNode +}) { + return ( + + {isBuildingLayout && ( + + + {loadingMessage} + + + )} + {children} + + ) +} diff --git a/web/common/src/components/Lineage/stories/ModelLineage.tsx b/web/common/src/components/Lineage/stories/ModelLineage.tsx index 2902350919..800f292c4b 100644 --- a/web/common/src/components/Lineage/stories/ModelLineage.tsx +++ b/web/common/src/components/Lineage/stories/ModelLineage.tsx @@ -359,7 +359,6 @@ export const ModelLineage = ({ selectedNodes, selectedEdges, selectedNodeId, - isBuildingLayout, zoom, edges, nodes, @@ -372,7 +371,6 @@ export const ModelLineage = ({ setSelectedNodes, setSelectedEdges, setSelectedNodeId, - setIsBuildingLayout, setZoom, setEdges, setNodesMap, diff --git a/web/common/src/components/VirtualList/FilterableList.tsx b/web/common/src/components/VirtualList/FilterableList.tsx index d22bfca784..16a243eb0e 100644 --- a/web/common/src/components/VirtualList/FilterableList.tsx +++ b/web/common/src/components/VirtualList/FilterableList.tsx @@ -58,6 +58,9 @@ export function FilterableList({ } inputSize="xs" className="FilterableList__Input w-full" + onClick={(e: React.MouseEvent) => { + e.stopPropagation() + }} /> Date: Mon, 6 Oct 2025 11:33:19 -0700 Subject: [PATCH 0941/1056] fix(web_common): apply className and update stories (#5488) --- web/common/src/components/Lineage/LineageLayout.tsx | 6 ++++-- .../src/components/Lineage/LineageLayoutContainer.tsx | 2 +- web/common/src/components/Lineage/stories/ModelLineage.tsx | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/web/common/src/components/Lineage/LineageLayout.tsx b/web/common/src/components/Lineage/LineageLayout.tsx index e01e8ae9e9..2ab4a34879 100644 --- a/web/common/src/components/Lineage/LineageLayout.tsx +++ b/web/common/src/components/Lineage/LineageLayout.tsx @@ -66,13 +66,15 @@ export function LineageLayout< }) { return ( - + + isBuildingLayout={isBuildingLayout} useLineage={useModelLineage} nodeTypes={nodeTypes} edgeTypes={edgeTypes} From a3030118220376c93f267df91d2ad7adce9a4536 Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:01:36 -0500 Subject: [PATCH 0942/1056] Fix: unique, user-friendly audit names for custom named dbt tests (#5484) --- sqlmesh/dbt/manifest.py | 64 ++++++++++++++++++- sqlmesh/dbt/test.py | 9 ++- tests/dbt/test_test.py | 132 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+), 4 deletions(-) diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index 17c5e91700..ea2058138f 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -61,6 +61,7 @@ extract_call_names, jinja_call_arg_name, ) +from sqlglot.helper import ensure_list if t.TYPE_CHECKING: from dbt.contracts.graph.manifest import Macro, Manifest @@ -353,15 +354,17 @@ def _load_tests(self) -> None: ) test_model = _test_model(node) + node_config = _node_base_config(node) + node_config["name"] = _build_test_name(node, dependencies) test = TestConfig( sql=sql, model_name=test_model, test_kwargs=node.test_metadata.kwargs if hasattr(node, "test_metadata") else {}, dependencies=dependencies, - **_node_base_config(node), + **node_config, ) - self._tests_per_package[node.package_name][node.name.lower()] = test + self._tests_per_package[node.package_name][node.unique_id] = test if test_model: self._tests_by_owner[test_model].append(test) @@ -741,7 +744,12 @@ def _test_model(node: ManifestNode) -> t.Optional[str]: attached_node = getattr(node, "attached_node", None) if attached_node: pieces = attached_node.split(".") - return pieces[-1] if pieces[0] in ["model", "seed"] else None + if pieces[0] in ["model", "seed"]: + # versioned models have format "model.package.model_name.v1" (4 parts) + if len(pieces) == 4: + return f"{pieces[2]}_{pieces[3]}" + return pieces[-1] + return None key_name = getattr(node, "file_key_name", None) if key_name: @@ -798,3 +806,53 @@ def _strip_jinja_materialization_tags(materialization_jinja: str) -> str: ) return materialization_jinja.strip() + + +def _build_test_name(node: ManifestNode, dependencies: Dependencies) -> str: + """ + Build a user-friendly test name that includes the test's model/source, column, + and args for tests with custom user names. Needed because dbt only generates these + names for tests that do not specify the "name" field in their YAML definition. + + Name structure + - Model test: [namespace]_[test name]_[model name]_[column name]__[arg values] + - Source test: [namespace]_source_[test name]_[source name]_[table name]_[column name]__[arg values] + """ + # standalone test + if not hasattr(node, "test_metadata"): + return node.name + + model_name = _test_model(node) + source_name = None + if not model_name and dependencies.sources: + # extract source and table names + source_parts = list(dependencies.sources)[0].split(".") + source_name = "_".join(source_parts) if len(source_parts) == 2 else source_parts[-1] + entity_name = model_name or source_name or "" + entity_name = f"_{entity_name}" if entity_name else "" + + name_prefix = "" + if namespace := getattr(node.test_metadata, "namespace", None): + name_prefix += f"{namespace}_" + if source_name and not model_name: + name_prefix += "source_" + + metadata_kwargs = node.test_metadata.kwargs + arg_val_parts = [] + for arg, val in sorted(metadata_kwargs.items()): + if arg == "model": + continue + if isinstance(val, dict): + val = list(val.values()) + val = [re.sub("[^0-9a-zA-Z_]+", "_", str(v)) for v in ensure_list(val)] + arg_val_parts.extend(val) + unique_args = "__".join(arg_val_parts) if arg_val_parts else "" + unique_args = f"_{unique_args}" if unique_args else "" + + auto_name = f"{name_prefix}{node.test_metadata.name}{entity_name}{unique_args}" + + if node.name == auto_name: + return node.name + + custom_prefix = name_prefix if source_name and not model_name else "" + return f"{custom_prefix}{node.name}{entity_name}{unique_args}" diff --git a/sqlmesh/dbt/test.py b/sqlmesh/dbt/test.py index 7d8a369068..c4a32b2189 100644 --- a/sqlmesh/dbt/test.py +++ b/sqlmesh/dbt/test.py @@ -122,7 +122,14 @@ def is_standalone(self) -> bool: return True # Check if test has references to other models - other_refs = {ref for ref in self.dependencies.refs if ref != self.model_name} + # For versioned models, refs include version (e.g., "model_name_v1") but model_name may not + self_refs = {self.model_name} + for ref in self.dependencies.refs: + # versioned models end in _vX + if ref.startswith(f"{self.model_name}_v"): + self_refs.add(ref) + + other_refs = {ref for ref in self.dependencies.refs if ref not in self_refs} return bool(other_refs) @property diff --git a/tests/dbt/test_test.py b/tests/dbt/test_test.py index 845c1d2fc0..fb33220c0c 100644 --- a/tests/dbt/test_test.py +++ b/tests/dbt/test_test.py @@ -1,3 +1,7 @@ +from pathlib import Path + +import pytest + from sqlmesh.dbt.test import TestConfig @@ -8,3 +12,131 @@ def test_multiline_test_kwarg() -> None: test_kwargs={"test_field": "foo\nbar\n"}, ) assert test._kwargs() == 'test_field="foo\nbar"' + + +@pytest.mark.xdist_group("dbt_manifest") +def test_tests_get_unique_names(tmp_path: Path, create_empty_project) -> None: + from sqlmesh.utils.yaml import YAML + from sqlmesh.core.context import Context + + yaml = YAML() + project_dir, model_dir = create_empty_project(project_name="local") + + model_file = model_dir / "my_model.sql" + with open(model_file, "w", encoding="utf-8") as f: + f.write("SELECT 1 as id, 'value1' as status") + + # Create schema.yml with: + # 1. Same test on model and source, both with/without custom test name + # 2. Same test on same model with different args, both with/without custom test name + # 3. Versioned model with tests (both built-in and custom named) + schema_yaml = { + "version": 2, + "sources": [ + { + "name": "raw", + "tables": [ + { + "name": "my_source", + "columns": [ + { + "name": "id", + "data_tests": [ + {"not_null": {"name": "custom_notnull_name"}}, + {"not_null": {}}, + ], + } + ], + } + ], + } + ], + "models": [ + { + "name": "my_model", + "columns": [ + { + "name": "id", + "data_tests": [ + {"not_null": {"name": "custom_notnull_name"}}, + {"not_null": {}}, + ], + }, + { + "name": "status", + "data_tests": [ + {"accepted_values": {"values": ["value1", "value2"]}}, + {"accepted_values": {"values": ["value1", "value2", "value3"]}}, + { + "accepted_values": { + "name": "custom_accepted_values_name", + "values": ["value1", "value2"], + } + }, + { + "accepted_values": { + "name": "custom_accepted_values_name", + "values": ["value1", "value2", "value3"], + } + }, + ], + }, + ], + }, + { + "name": "versioned_model", + "columns": [ + { + "name": "id", + "data_tests": [ + {"not_null": {}}, + {"not_null": {"name": "custom_versioned_notnull"}}, + ], + }, + { + "name": "amount", + "data_tests": [ + {"accepted_values": {"values": ["low", "high"]}}, + ], + }, + ], + "versions": [ + {"v": 1}, + {"v": 2}, + ], + }, + ], + } + + schema_file = model_dir / "schema.yml" + with open(schema_file, "w", encoding="utf-8") as f: + yaml.dump(schema_yaml, f) + + # Create versioned model files + versioned_model_v1_file = model_dir / "versioned_model_v1.sql" + with open(versioned_model_v1_file, "w", encoding="utf-8") as f: + f.write("SELECT 1 as id, 'low' as amount") + + versioned_model_v2_file = model_dir / "versioned_model_v2.sql" + with open(versioned_model_v2_file, "w", encoding="utf-8") as f: + f.write("SELECT 1 as id, 'low' as amount") + + context = Context(paths=project_dir) + + all_audit_names = list(context._audits.keys()) + list(context._standalone_audits.keys()) + assert sorted(all_audit_names) == [ + "local.accepted_values_my_model_status__value1__value2", + "local.accepted_values_my_model_status__value1__value2__value3", + "local.accepted_values_versioned_model_v1_amount__low__high", + "local.accepted_values_versioned_model_v2_amount__low__high", + "local.custom_accepted_values_name_my_model_status__value1__value2", + "local.custom_accepted_values_name_my_model_status__value1__value2__value3", + "local.custom_notnull_name_my_model_id", + "local.custom_versioned_notnull_versioned_model_v1_id", + "local.custom_versioned_notnull_versioned_model_v2_id", + "local.not_null_my_model_id", + "local.not_null_versioned_model_v1_id", + "local.not_null_versioned_model_v2_id", + "local.source_custom_notnull_name_raw_my_source_id", + "local.source_not_null_raw_my_source_id", + ] From 9fc6a2e378e67ea2f3b7a4859dda841c21a0ba59 Mon Sep 17 00:00:00 2001 From: Chris Rericha <67359577+crericha@users.noreply.github.com> Date: Mon, 6 Oct 2025 21:20:50 -0400 Subject: [PATCH 0943/1056] Fix: Add workaround to ignore source dependency if fqn matches model (#5492) --- sqlmesh/dbt/basemodel.py | 19 ++++++++++++++----- sqlmesh/dbt/context.py | 8 ++++++++ sqlmesh/dbt/source.py | 5 +++++ tests/dbt/test_transformation.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index 3e325f13e6..7c7e9e2e76 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -130,7 +130,7 @@ class BaseModelConfig(GeneralConfig): unique_id: str = "" name: str = "" package_name: str = "" - fqn: t.List[str] = [] + fqn_: t.List[str] = Field(default_factory=list, alias="fqn") schema_: str = Field("", alias="schema") database: t.Optional[str] = None alias: t.Optional[str] = None @@ -281,15 +281,17 @@ def remove_tests_with_invalid_refs(self, context: DbtContext) -> None: and all(source in context.sources for source in test.dependencies.sources) ] + @property + def fqn(self) -> str: + return ".".join(self.fqn_) + @property def sqlmesh_config_fields(self) -> t.Set[str]: return {"description", "owner", "stamp", "storage_format"} @property def node_info(self) -> DbtNodeInfo: - return DbtNodeInfo( - unique_id=self.unique_id, name=self.name, fqn=".".join(self.fqn), alias=self.alias - ) + return DbtNodeInfo(unique_id=self.unique_id, name=self.name, fqn=self.fqn, alias=self.alias) def sqlmesh_model_kwargs( self, @@ -327,7 +329,14 @@ def sqlmesh_model_kwargs( "column_descriptions": column_descriptions_to_sqlmesh(self.columns) or None, "depends_on": { model.canonical_name(context) for model in model_context.refs.values() - }.union({source.canonical_name(context) for source in model_context.sources.values()}), + }.union( + { + source.canonical_name(context) + for source in model_context.sources.values() + if source.fqn not in context.model_fqns + # Allow dbt projects to reference a model as a source without causing a cycle + }, + ), "jinja_macros": jinja_macros, "path": self.path, "pre_statements": [d.jinja_statement(hook.sql) for hook in self.pre_hook], diff --git a/sqlmesh/dbt/context.py b/sqlmesh/dbt/context.py index 67e70d3c79..bcdae8f97a 100644 --- a/sqlmesh/dbt/context.py +++ b/sqlmesh/dbt/context.py @@ -51,6 +51,7 @@ class DbtContext: _project_name: t.Optional[str] = None _variables: t.Dict[str, t.Any] = field(default_factory=dict) _models: t.Dict[str, ModelConfig] = field(default_factory=dict) + _model_fqns: t.Set[str] = field(default_factory=set) _seeds: t.Dict[str, SeedConfig] = field(default_factory=dict) _sources: t.Dict[str, SourceConfig] = field(default_factory=dict) _refs: t.Dict[str, t.Union[ModelConfig, SeedConfig]] = field(default_factory=dict) @@ -144,6 +145,7 @@ def models(self) -> t.Dict[str, ModelConfig]: def models(self, models: t.Dict[str, ModelConfig]) -> None: self._models = {} self._refs = {} + self._model_fqns = set() self.add_models(models) def add_models(self, models: t.Dict[str, ModelConfig]) -> None: @@ -151,6 +153,12 @@ def add_models(self, models: t.Dict[str, ModelConfig]) -> None: self._models.update(models) self._jinja_environment = None + @property + def model_fqns(self) -> t.Set[str]: + if not self._model_fqns: + self._model_fqns = {model.fqn for model in self._models.values()} + return self._model_fqns + @property def seeds(self) -> t.Dict[str, SeedConfig]: return self._seeds diff --git a/sqlmesh/dbt/source.py b/sqlmesh/dbt/source.py index 76ee682e77..efafbf1642 100644 --- a/sqlmesh/dbt/source.py +++ b/sqlmesh/dbt/source.py @@ -36,6 +36,7 @@ class SourceConfig(GeneralConfig): # DBT configuration fields name: str = "" source_name_: str = Field("", alias="source_name") + fqn_: t.List[str] = Field(default_factory=list, alias="fqn") database: t.Optional[str] = None schema_: t.Optional[str] = Field(None, alias="schema") identifier: t.Optional[str] = None @@ -64,6 +65,10 @@ def table_name(self) -> t.Optional[str]: def config_name(self) -> str: return f"{self.source_name_}.{self.name}" + @property + def fqn(self) -> str: + return ".".join(self.fqn_) + def canonical_name(self, context: DbtContext) -> str: if self._canonical_name is None: source = context.get_callable_macro("source") diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 141c160e7e..0a1091a7fc 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -53,6 +53,7 @@ ) from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.model import Materialization, ModelConfig +from sqlmesh.dbt.source import SourceConfig from sqlmesh.dbt.project import Project from sqlmesh.dbt.relation import Policy from sqlmesh.dbt.seed import SeedConfig @@ -2678,3 +2679,31 @@ def test_selected_resources_context_variable( result = context.render(test_condition, selected_resources=selected_resources) assert result.strip() == "has_resources" + + +def test_ignore_source_depends_on_when_also_model(dbt_dummy_postgres_config: PostgresConfig): + context = DbtContext() + context._target = dbt_dummy_postgres_config + + source_a = SourceConfig( + name="source_a", + fqn=["package", "schema", "model_a"], + ) + source_a._canonical_name = "schema.source_a" + source_b = SourceConfig( + name="source_b", + fqn=["package", "schema", "source_b"], + ) + source_b._canonical_name = "schema.source_b" + context.sources = {"source_a": source_a, "source_b": source_b} + + model = ModelConfig( + dependencies=Dependencies(sources={"source_a", "source_b"}), + fqn=["package", "schema", "test_model"], + ) + context.models = { + "test_model": model, + "model_a": ModelConfig(name="model_a", fqn=["package", "schema", "model_a"]), + } + + assert model.sqlmesh_model_kwargs(context)["depends_on"] == {"schema.source_b"} From 8e9fe231c5624cc8ea56910e9fa535fa9fef0fac Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 6 Oct 2025 20:02:29 -0700 Subject: [PATCH 0944/1056] fix(web_common): lineage styling and border colors (#5494) --- .../LineageColumnLevel/FactoryColumn.tsx | 35 +++++++++++++++---- .../Lineage/LineageControlButton.tsx | 2 +- .../components/Lineage/LineageLayoutBase.tsx | 16 +-------- .../src/components/Lineage/node/NodePort.tsx | 2 +- .../Lineage/stories/ModelLineage.tsx | 2 +- web/common/tailwind.lineage.config.js | 9 ++++- 6 files changed, 40 insertions(+), 26 deletions(-) diff --git a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx index 19b73c3ef6..90def0f5ea 100644 --- a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx +++ b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx @@ -202,10 +202,22 @@ export function FactoryColumn< className="FactoryColumn__Information" info={description} > - + ) : ( - + )} @@ -214,7 +226,7 @@ export function FactoryColumn< {type} } className={cn( - 'FactoryColumn__Metadata relative overflow-visible group p-0', + 'FactoryColumn__Metadata relative overflow-visible group', isDisabledColumn && 'cursor-not-allowed', className, )} @@ -240,8 +252,8 @@ export function FactoryColumn< id={id} nodeId={nodeId} className={cn( - 'border-t border-lineage-divider first:border-t-0 px-2', - isTriggeredColumn && 'bg-lineage-model-column-active', + 'border-t border-lineage-divider first:border-t-0', + isTriggeredColumn && 'bg-lineage-model-column-active-background', )} > {renderColumn()} @@ -252,11 +264,20 @@ export function FactoryColumn< }) } -function DisplayColumName({ name }: { name: string }) { +function DisplayColumName({ + name, + className, +}: { + name: string + className?: string +}) { return ( {name} diff --git a/web/common/src/components/Lineage/LineageControlButton.tsx b/web/common/src/components/Lineage/LineageControlButton.tsx index 6f66f90db7..d3f3d5d215 100644 --- a/web/common/src/components/Lineage/LineageControlButton.tsx +++ b/web/common/src/components/Lineage/LineageControlButton.tsx @@ -21,7 +21,7 @@ export function LineageControlButton({ side="left" sideOffset={8} delayDuration={0} - className="px-2 py-1 text-xs rounded-sm font-semibold bg-lineage-control-button-tooltip-background text-lineage-control-button-tooltip-foreground" + className="px-2 py-1 text-xs rounded-sm font-semibold bg-lineage-control-button-tooltip-background text-lineage-control-button-tooltip-foreground border-2 border-lineage-control-button-tooltip-border" trigger={
      { - if (currentNode?.id) { - setSelectedNodeId(currentNode.id) - } else { - const node = nodes.length > 0 ? nodes[nodes.length - 1] : null - - if (node) { - setCenter(node.position.x, node.position.y, { - zoom: zoom, - duration: 0, - }) - } - } - }, [currentNode?.id, setSelectedNodeId, setCenter]) - return ( , @@ -327,6 +312,7 @@ export function LineageLayoutBase< showInteractive={false} showFitView={false} position="top-right" + className="m-1 border-2 border-lineage-control-border rounded-sm overflow-hidden" > {currentNode && ( () => cleanupLayoutWorker(), []) diff --git a/web/common/tailwind.lineage.config.js b/web/common/tailwind.lineage.config.js index c2c8800a6f..b615ea756f 100644 --- a/web/common/tailwind.lineage.config.js +++ b/web/common/tailwind.lineage.config.js @@ -8,6 +8,7 @@ export default { divider: 'var(--color-lineage-divider)', border: 'var(--color-lineage-border)', control: { + border: 'var(--color-lineage-control-border)', background: { DEFAULT: 'var(--color-lineage-control-background)', hover: 'var(--color-lineage-control-background-hover)', @@ -18,6 +19,7 @@ export default { }, button: { tooltip: { + border: 'var(--color-lineage-control-button-tooltip-border)', background: 'var(--color-lineage-control-button-tooltip-background)', foreground: @@ -68,6 +70,12 @@ export default { }, model: { column: { + active: { + background: + 'var(--color-lineage-model-column-active-background)', + foreground: + 'var(--color-lineage-model-column-active-foreground)', + }, source: { background: 'var(--color-lineage-model-column-source-background)', @@ -81,7 +89,6 @@ export default { 'var(--color-lineage-model-column-error-background)', icon: 'var(--color-lineage-model-column-error-icon)', }, - active: 'var(--color-lineage-model-column-active)', icon: { DEFAULT: 'var(--color-lineage-model-column-icon)', active: 'var(--color-lineage-model-column-icon-active)', From 5ffed107fca598816073d4f916da67db78cf7aae Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:18:12 +0300 Subject: [PATCH 0945/1056] Feat(dbt): Add support for transaction in dbt pre and post hooks (#5480) --- sqlmesh/core/model/common.py | 1 + sqlmesh/core/model/definition.py | 29 ++- sqlmesh/core/snapshot/evaluator.py | 67 +++++-- sqlmesh/dbt/basemodel.py | 13 +- tests/core/test_snapshot_evaluator.py | 20 +- tests/dbt/test_transformation.py | 177 ++++++++++++++++++ .../dbt/sushi_test/macros/insert_hook.sql | 14 ++ .../models/model_with_transaction_hooks.sql | 56 ++++++ 8 files changed, 347 insertions(+), 30 deletions(-) create mode 100644 tests/fixtures/dbt/sushi_test/macros/insert_hook.sql create mode 100644 tests/fixtures/dbt/sushi_test/models/model_with_transaction_hooks.sql diff --git a/sqlmesh/core/model/common.py b/sqlmesh/core/model/common.py index d2b9a11c08..9e117b56fb 100644 --- a/sqlmesh/core/model/common.py +++ b/sqlmesh/core/model/common.py @@ -663,6 +663,7 @@ def parse_strings_with_macro_refs(value: t.Any, dialect: DialectType) -> t.Any: class ParsableSql(PydanticModel): sql: str + transaction: t.Optional[bool] = None _parsed: t.Optional[exp.Expression] = None _parsed_dialect: t.Optional[str] = None diff --git a/sqlmesh/core/model/definition.py b/sqlmesh/core/model/definition.py index f81dae004b..0a20ab23b2 100644 --- a/sqlmesh/core/model/definition.py +++ b/sqlmesh/core/model/definition.py @@ -363,6 +363,7 @@ def render_pre_statements( expand: t.Iterable[str] = tuple(), deployability_index: t.Optional[DeployabilityIndex] = None, engine_adapter: t.Optional[EngineAdapter] = None, + inside_transaction: t.Optional[bool] = True, **kwargs: t.Any, ) -> t.List[exp.Expression]: """Renders pre-statements for a model. @@ -384,7 +385,11 @@ def render_pre_statements( The list of rendered expressions. """ return self._render_statements( - self.pre_statements, + [ + stmt + for stmt in self.pre_statements + if stmt.args.get("transaction", True) == inside_transaction + ], start=start, end=end, execution_time=execution_time, @@ -405,6 +410,7 @@ def render_post_statements( expand: t.Iterable[str] = tuple(), deployability_index: t.Optional[DeployabilityIndex] = None, engine_adapter: t.Optional[EngineAdapter] = None, + inside_transaction: t.Optional[bool] = True, **kwargs: t.Any, ) -> t.List[exp.Expression]: """Renders post-statements for a model. @@ -420,13 +426,18 @@ def render_post_statements( that depend on materialized tables. Model definitions are inlined and can thus be run end to end on the fly. deployability_index: Determines snapshots that are deployable in the context of this render. + inside_transaction: Whether to render hooks with transaction=True (inside) or transaction=False (outside). kwargs: Additional kwargs to pass to the renderer. Returns: The list of rendered expressions. """ return self._render_statements( - self.post_statements, + [ + stmt + for stmt in self.post_statements + if stmt.args.get("transaction", True) == inside_transaction + ], start=start, end=end, execution_time=execution_time, @@ -567,6 +578,8 @@ def _get_parsed_statements(self, attr_name: str) -> t.List[exp.Expression]: result = [] for v in value: parsed = v.parse(self.dialect) + if getattr(v, "transaction", None) is not None: + parsed.set("transaction", v.transaction) if not isinstance(parsed, exp.Semicolon): result.append(parsed) return result @@ -2592,9 +2605,17 @@ def _create_model( if statement_field in kwargs: # Macros extracted from these statements need to be treated as metadata only is_metadata = statement_field == "on_virtual_update" - statements.extend((stmt, is_metadata) for stmt in kwargs[statement_field]) + for stmt in kwargs[statement_field]: + # Extract the expression if it's ParsableSql already + expr = stmt.parse(dialect) if isinstance(stmt, ParsableSql) else stmt + statements.append((expr, is_metadata)) kwargs[statement_field] = [ - ParsableSql.from_parsed_expression(stmt, dialect, use_meta_sql=use_original_sql) + # this to retain the transaction information + stmt + if isinstance(stmt, ParsableSql) + else ParsableSql.from_parsed_expression( + stmt, dialect, use_meta_sql=use_original_sql + ) for stmt in kwargs[statement_field] ] diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 2676709d85..773010d673 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -750,13 +750,19 @@ def _evaluate_snapshot( **render_statements_kwargs ) + evaluation_strategy = _evaluation_strategy(snapshot, adapter) + evaluation_strategy.run_pre_statements( + snapshot=snapshot, + render_kwargs={**render_statements_kwargs, "inside_transaction": False}, + ) + with ( adapter.transaction(), adapter.session(snapshot.model.render_session_properties(**render_statements_kwargs)), ): - evaluation_strategy = _evaluation_strategy(snapshot, adapter) evaluation_strategy.run_pre_statements( - snapshot=snapshot, render_kwargs=render_statements_kwargs + snapshot=snapshot, + render_kwargs={**render_statements_kwargs, "inside_transaction": True}, ) if not target_table_exists or (model.is_seed and not snapshot.intervals): @@ -828,10 +834,16 @@ def _evaluate_snapshot( ) evaluation_strategy.run_post_statements( - snapshot=snapshot, render_kwargs=render_statements_kwargs + snapshot=snapshot, + render_kwargs={**render_statements_kwargs, "inside_transaction": True}, ) - return wap_id + evaluation_strategy.run_post_statements( + snapshot=snapshot, + render_kwargs={**render_statements_kwargs, "inside_transaction": False}, + ) + + return wap_id def create_snapshot( self, @@ -865,6 +877,11 @@ def create_snapshot( deployability_index=deployability_index, ) + evaluation_strategy = _evaluation_strategy(snapshot, adapter) + evaluation_strategy.run_pre_statements( + snapshot=snapshot, render_kwargs={**create_render_kwargs, "inside_transaction": False} + ) + with ( adapter.transaction(), adapter.session(snapshot.model.render_session_properties(**create_render_kwargs)), @@ -896,6 +913,10 @@ def create_snapshot( dry_run=True, ) + evaluation_strategy.run_post_statements( + snapshot=snapshot, render_kwargs={**create_render_kwargs, "inside_transaction": False} + ) + if on_complete is not None: on_complete(snapshot) @@ -1097,6 +1118,11 @@ def _migrate_snapshot( ) target_table_name = snapshot.table_name() + evaluation_strategy = _evaluation_strategy(snapshot, adapter) + evaluation_strategy.run_pre_statements( + snapshot=snapshot, render_kwargs={**render_kwargs, "inside_transaction": False} + ) + with ( adapter.transaction(), adapter.session(snapshot.model.render_session_properties(**render_kwargs)), @@ -1134,6 +1160,10 @@ def _migrate_snapshot( dry_run=True, ) + evaluation_strategy.run_post_statements( + snapshot=snapshot, render_kwargs={**render_kwargs, "inside_transaction": False} + ) + # Retry in case when the table is migrated concurrently from another plan application @retry( reraise=True, @@ -1454,7 +1484,8 @@ def _execute_create( } if run_pre_post_statements: evaluation_strategy.run_pre_statements( - snapshot=snapshot, render_kwargs=create_render_kwargs + snapshot=snapshot, + render_kwargs={**create_render_kwargs, "inside_transaction": True}, ) evaluation_strategy.create( table_name=table_name, @@ -1471,7 +1502,8 @@ def _execute_create( ) if run_pre_post_statements: evaluation_strategy.run_post_statements( - snapshot=snapshot, render_kwargs=create_render_kwargs + snapshot=snapshot, + render_kwargs={**create_render_kwargs, "inside_transaction": True}, ) def _can_clone(self, snapshot: Snapshot, deployability_index: DeployabilityIndex) -> bool: @@ -2944,12 +2976,20 @@ def append( ) def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None: - # in dbt custom materialisations it's up to the user when to run the pre hooks - pass + # in dbt custom materialisations it's up to the user to run the pre hooks inside the transaction + if not render_kwargs.get("inside_transaction", True): + super().run_pre_statements( + snapshot=snapshot, + render_kwargs=render_kwargs, + ) def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None: - # in dbt custom materialisations it's up to the user when to run the post hooks - pass + # in dbt custom materialisations it's up to the user to run the post hooks inside the transaction + if not render_kwargs.get("inside_transaction", True): + super().run_post_statements( + snapshot=snapshot, + render_kwargs=render_kwargs, + ) def _execute_materialization( self, @@ -2985,14 +3025,15 @@ def _execute_materialization( "sql": str(query_or_df), "is_first_insert": is_first_insert, "create_only": create_only, - # FIXME: Add support for transaction=False "pre_hooks": [ - AttributeDict({"sql": s.this.this, "transaction": True}) + AttributeDict({"sql": s.this.this, "transaction": transaction}) for s in model.pre_statements + if (transaction := s.args.get("transaction", True)) ], "post_hooks": [ - AttributeDict({"sql": s.this.this, "transaction": True}) + AttributeDict({"sql": s.this.this, "transaction": transaction}) for s in model.post_statements + if (transaction := s.args.get("transaction", True)) ], "model_instance": model, **kwargs, diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index 7c7e9e2e76..0c719ebb88 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -13,6 +13,7 @@ from sqlmesh.core.config.base import UpdateStrategy from sqlmesh.core.config.common import VirtualEnvironmentMode from sqlmesh.core.model import Model +from sqlmesh.core.model.common import ParsableSql from sqlmesh.core.node import DbtNodeInfo from sqlmesh.dbt.column import ( ColumnConfig, @@ -87,7 +88,7 @@ class Hook(DbtConfig): """ sql: SqlStr - transaction: bool = True # TODO not yet supported + transaction: bool = True _sql_validator = sql_str_validator @@ -339,8 +340,14 @@ def sqlmesh_model_kwargs( ), "jinja_macros": jinja_macros, "path": self.path, - "pre_statements": [d.jinja_statement(hook.sql) for hook in self.pre_hook], - "post_statements": [d.jinja_statement(hook.sql) for hook in self.post_hook], + "pre_statements": [ + ParsableSql(sql=d.jinja_statement(hook.sql).sql(), transaction=hook.transaction) + for hook in self.pre_hook + ], + "post_statements": [ + ParsableSql(sql=d.jinja_statement(hook.sql).sql(), transaction=hook.transaction) + for hook in self.post_hook + ], "tags": self.tags, "physical_schema_mapping": context.sqlmesh_config.physical_schema_mapping, "default_catalog": context.target.database, diff --git a/tests/core/test_snapshot_evaluator.py b/tests/core/test_snapshot_evaluator.py index 68061544a8..c0a7a01b51 100644 --- a/tests/core/test_snapshot_evaluator.py +++ b/tests/core/test_snapshot_evaluator.py @@ -3232,11 +3232,11 @@ def test_create_post_statements_use_non_deployable_table( evaluator.create([snapshot], {}, DeployabilityIndex.none_deployable()) call_args = adapter_mock.execute.call_args_list - pre_calls = call_args[0][0][0] + pre_calls = call_args[1][0][0] assert len(pre_calls) == 1 assert pre_calls[0].sql(dialect="postgres") == expected_call - post_calls = call_args[1][0][0] + post_calls = call_args[2][0][0] assert len(post_calls) == 1 assert post_calls[0].sql(dialect="postgres") == expected_call @@ -3294,11 +3294,11 @@ def model_with_statements(context, **kwargs): expected_call = f'CREATE INDEX IF NOT EXISTS "idx" ON "sqlmesh__db"."db__test_model__{snapshot.version}__dev" /* db.test_model */("id")' call_args = adapter_mock.execute.call_args_list - pre_calls = call_args[0][0][0] + pre_calls = call_args[1][0][0] assert len(pre_calls) == 1 assert pre_calls[0].sql(dialect="postgres") == expected_call - post_calls = call_args[1][0][0] + post_calls = call_args[2][0][0] assert len(post_calls) == 1 assert post_calls[0].sql(dialect="postgres") == expected_call @@ -3356,14 +3356,14 @@ def create_log_table(evaluator, view_name): ) call_args = adapter_mock.execute.call_args_list - post_calls = call_args[1][0][0] + post_calls = call_args[2][0][0] assert len(post_calls) == 1 assert ( post_calls[0].sql(dialect="postgres") == f'CREATE INDEX IF NOT EXISTS "test_idx" ON "sqlmesh__test_schema"."test_schema__test_model__{snapshot.version}__dev" /* test_schema.test_model */("a")' ) - on_virtual_update_calls = call_args[2][0][0] + on_virtual_update_calls = call_args[4][0][0] assert ( on_virtual_update_calls[0].sql(dialect="postgres") == 'GRANT SELECT ON VIEW "test_schema__test_env"."test_model" /* test_schema.test_model */ TO ROLE "admin"' @@ -3441,7 +3441,7 @@ def model_with_statements(context, **kwargs): ) call_args = adapter_mock.execute.call_args_list - on_virtual_update_call = call_args[2][0][0][0] + on_virtual_update_call = call_args[4][0][0][0] assert ( on_virtual_update_call.sql(dialect="postgres") == 'CREATE INDEX IF NOT EXISTS "idx" ON "db"."test_model_3" /* db.test_model_3 */("id")' @@ -4187,11 +4187,11 @@ def test_multiple_engine_creation(snapshot: Snapshot, adapters, make_snapshot): assert view_args[1][0][0] == "test_schema__test_env.test_model" call_args = engine_adapters["secondary"].execute.call_args_list - pre_calls = call_args[0][0][0] + pre_calls = call_args[1][0][0] assert len(pre_calls) == 1 assert pre_calls[0].sql(dialect="postgres") == expected_call - post_calls = call_args[1][0][0] + post_calls = call_args[2][0][0] assert len(post_calls) == 1 assert post_calls[0].sql(dialect="postgres") == expected_call @@ -4459,7 +4459,7 @@ def model_with_statements(context, **kwargs): # For the pre/post statements verify the model-specific gateway was used engine_adapters["default"].execute.assert_called_once() - assert len(engine_adapters["secondary"].execute.call_args_list) == 2 + assert len(engine_adapters["secondary"].execute.call_args_list) == 4 # Validate that the get_catalog_type method was called only on the secondary engine from the macro evaluator engine_adapters["default"].get_catalog_type.assert_not_called() diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 0a1091a7fc..dd69f46200 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -2707,3 +2707,180 @@ def test_ignore_source_depends_on_when_also_model(dbt_dummy_postgres_config: Pos } assert model.sqlmesh_model_kwargs(context)["depends_on"] == {"schema.source_b"} + + +@pytest.mark.xdist_group("dbt_manifest") +def test_dbt_hooks_with_transaction_flag(sushi_test_dbt_context: Context): + model_fqn = '"memory"."sushi"."model_with_transaction_hooks"' + assert model_fqn in sushi_test_dbt_context.models + + model = sushi_test_dbt_context.models[model_fqn] + + pre_statements = model.pre_statements_ + assert pre_statements is not None + assert len(pre_statements) >= 3 + + # we need to check the expected SQL but more importantly that the transaction flags are there + assert any( + s.sql == 'JINJA_STATEMENT_BEGIN;\n{{ log("pre-hook") }}\nJINJA_END;' + and s.transaction is True + for s in pre_statements + ) + assert any( + "CREATE TABLE IF NOT EXISTS hook_outside_pre_table" in s.sql and s.transaction is False + for s in pre_statements + ) + assert any( + "CREATE TABLE IF NOT EXISTS shared_hook_table" in s.sql and s.transaction is False + for s in pre_statements + ) + assert any( + "{{ insert_into_shared_hook_table('inside_pre') }}" in s.sql and s.transaction is True + for s in pre_statements + ) + + post_statements = model.post_statements_ + assert post_statements is not None + assert len(post_statements) >= 4 + assert any( + s.sql == 'JINJA_STATEMENT_BEGIN;\n{{ log("post-hook") }}\nJINJA_END;' + and s.transaction is True + for s in post_statements + ) + assert any( + "{{ insert_into_shared_hook_table('inside_post') }}" in s.sql and s.transaction is True + for s in post_statements + ) + assert any( + "CREATE TABLE IF NOT EXISTS hook_outside_post_table" in s.sql and s.transaction is False + for s in post_statements + ) + assert any( + "{{ insert_into_shared_hook_table('after_commit') }}" in s.sql and s.transaction is False + for s in post_statements + ) + + # render_pre_statements with inside_transaction=True should only return inserrt + inside_pre_statements = model.render_pre_statements(inside_transaction=True) + assert len(inside_pre_statements) == 1 + assert ( + inside_pre_statements[0].sql() + == """INSERT INTO "shared_hook_table" ("id", "hook_name", "execution_order", "created_at") VALUES ((SELECT COALESCE(MAX("id"), 0) + 1 FROM "shared_hook_table"), 'inside_pre', (SELECT COALESCE(MAX("id"), 0) + 1 FROM "shared_hook_table"), NOW())""" + ) + + # while for render_pre_statements with inside_transaction=False the create statements + outside_pre_statements = model.render_pre_statements(inside_transaction=False) + assert len(outside_pre_statements) == 2 + assert "CREATE" in outside_pre_statements[0].sql() + assert "hook_outside_pre_table" in outside_pre_statements[0].sql() + assert "CREATE" in outside_pre_statements[1].sql() + assert "shared_hook_table" in outside_pre_statements[1].sql() + + # similarly for post statements + inside_post_statements = model.render_post_statements(inside_transaction=True) + assert len(inside_post_statements) == 1 + assert ( + inside_post_statements[0].sql() + == """INSERT INTO "shared_hook_table" ("id", "hook_name", "execution_order", "created_at") VALUES ((SELECT COALESCE(MAX("id"), 0) + 1 FROM "shared_hook_table"), 'inside_post', (SELECT COALESCE(MAX("id"), 0) + 1 FROM "shared_hook_table"), NOW())""" + ) + + outside_post_statements = model.render_post_statements(inside_transaction=False) + assert len(outside_post_statements) == 2 + assert "CREATE" in outside_post_statements[0].sql() + assert "hook_outside_post_table" in outside_post_statements[0].sql() + assert "INSERT" in outside_post_statements[1].sql() + assert "shared_hook_table" in outside_post_statements[1].sql() + + +@pytest.mark.xdist_group("dbt_manifest") +def test_dbt_hooks_with_transaction_flag_execution(sushi_test_dbt_context: Context): + model_fqn = '"memory"."sushi"."model_with_transaction_hooks"' + assert model_fqn in sushi_test_dbt_context.models + + plan = sushi_test_dbt_context.plan(select_models=["sushi.model_with_transaction_hooks"]) + sushi_test_dbt_context.apply(plan) + + result = sushi_test_dbt_context.engine_adapter.fetchdf( + "SELECT * FROM sushi.model_with_transaction_hooks" + ) + assert len(result) == 1 + assert result["id"][0] == 1 + assert result["name"][0] == "test" + + # ensure the outside pre-hook and post-hook table were created + pre_outside = sushi_test_dbt_context.engine_adapter.fetchdf( + "SELECT * FROM hook_outside_pre_table" + ) + assert len(pre_outside) == 1 + assert pre_outside["id"][0] == 1 + assert pre_outside["location"][0] == "outside" + assert pre_outside["execution_order"][0] == 1 + + post_outside = sushi_test_dbt_context.engine_adapter.fetchdf( + "SELECT * FROM hook_outside_post_table" + ) + assert len(post_outside) == 1 + assert post_outside["id"][0] == 5 + assert post_outside["location"][0] == "outside" + assert post_outside["execution_order"][0] == 5 + + # verify the shared table that was created by before_begin and populated by all hooks + shared_table = sushi_test_dbt_context.engine_adapter.fetchdf( + "SELECT * FROM shared_hook_table ORDER BY execution_order" + ) + assert len(shared_table) == 3 + assert shared_table["execution_order"].is_monotonic_increasing + + # The order of creation and insertion will verify the following order of execution + # 1. before_begin (transaction=false) ran BEFORE the transaction started and created the table + # 2. inside_pre (transaction=true) ran INSIDE the transaction and could insert into the table + # 3. inside_post (transaction=true) ran INSIDE the transaction and could insert into the table (but after pre statement) + # 4. after_commit (transaction=false) ran AFTER the transaction committed + + assert shared_table["id"][0] == 1 + assert shared_table["hook_name"][0] == "inside_pre" + assert shared_table["execution_order"][0] == 1 + + assert shared_table["id"][1] == 2 + assert shared_table["hook_name"][1] == "inside_post" + assert shared_table["execution_order"][1] == 2 + + assert shared_table["id"][2] == 3 + assert shared_table["hook_name"][2] == "after_commit" + assert shared_table["execution_order"][2] == 3 + + # the timestamps also should be monotonically increasing for the same reason + for i in range(len(shared_table) - 1): + assert shared_table["created_at"][i] <= shared_table["created_at"][i + 1] + + # the tables using the alternate syntax should have correct order as well + assert pre_outside["created_at"][0] < shared_table["created_at"][0] + assert post_outside["created_at"][0] > shared_table["created_at"][1] + + # running with execution time one day in the future to simulate a run + tomorrow = datetime.now() + timedelta(days=1) + sushi_test_dbt_context.run( + select_models=["sushi.model_with_transaction_hooks"], execution_time=tomorrow + ) + + # to verify that the transaction information persists in state and is respected + shared_table = sushi_test_dbt_context.engine_adapter.fetchdf( + "SELECT * FROM shared_hook_table ORDER BY execution_order" + ) + + # and the execution order for run is similar + assert shared_table["execution_order"].is_monotonic_increasing + assert shared_table["id"][3] == 4 + assert shared_table["hook_name"][3] == "inside_pre" + assert shared_table["execution_order"][3] == 4 + + assert shared_table["id"][4] == 5 + assert shared_table["hook_name"][4] == "inside_post" + assert shared_table["execution_order"][4] == 5 + + assert shared_table["id"][5] == 6 + assert shared_table["hook_name"][5] == "after_commit" + assert shared_table["execution_order"][5] == 6 + + for i in range(len(shared_table) - 1): + assert shared_table["created_at"][i] <= shared_table["created_at"][i + 1] diff --git a/tests/fixtures/dbt/sushi_test/macros/insert_hook.sql b/tests/fixtures/dbt/sushi_test/macros/insert_hook.sql new file mode 100644 index 0000000000..aa27a7fe6d --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/macros/insert_hook.sql @@ -0,0 +1,14 @@ +{% macro insert_into_shared_hook_table(hook_name) %} +INSERT INTO shared_hook_table ( + id, + hook_name, + execution_order, + created_at +) +VALUES ( + (SELECT COALESCE(MAX(id), 0) + 1 FROM shared_hook_table), + '{{ hook_name }}', + (SELECT COALESCE(MAX(id), 0) + 1 FROM shared_hook_table), + NOW() +) +{% endmacro %} diff --git a/tests/fixtures/dbt/sushi_test/models/model_with_transaction_hooks.sql b/tests/fixtures/dbt/sushi_test/models/model_with_transaction_hooks.sql new file mode 100644 index 0000000000..49883f73df --- /dev/null +++ b/tests/fixtures/dbt/sushi_test/models/model_with_transaction_hooks.sql @@ -0,0 +1,56 @@ +{{ + config( + materialized = 'table', + + pre_hook = [ + { + "sql": " + CREATE TABLE IF NOT EXISTS hook_outside_pre_table AS + SELECT + 1 AS id, + 'outside' AS location, + 1 AS execution_order, + NOW() AS created_at + ", + "transaction": false + }, + + before_begin(" + CREATE TABLE IF NOT EXISTS shared_hook_table ( + id INT, + hook_name VARCHAR, + execution_order INT, + created_at TIMESTAMPTZ + ) + "), + + { + "sql": "{{ insert_into_shared_hook_table('inside_pre') }}", + "transaction": true + } + ], + + post_hook = [ + { + "sql": "{{ insert_into_shared_hook_table('inside_post') }}", + "transaction": true + }, + + { + "sql": " + CREATE TABLE IF NOT EXISTS hook_outside_post_table AS + SELECT + 5 AS id, + 'outside' AS location, + 5 AS execution_order, + NOW() AS created_at + ", + "transaction": false + }, + + after_commit("{{ insert_into_shared_hook_table('after_commit') }}") + ] + ) +}} + +SELECT 1 AS id, 'test' AS name; From db1faeb7fbc122e4f796adab7937b08ddd2e7416 Mon Sep 17 00:00:00 2001 From: etonlels Date: Tue, 7 Oct 2025 11:14:57 -0600 Subject: [PATCH 0946/1056] feat: add `batch_concurrency` to `ModelDefaultsConfig` (#5481) Co-authored-by: Claude --- sqlmesh/core/config/model.py | 2 + sqlmesh/core/model/kind.py | 12 +++++ tests/core/test_model.py | 101 +++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/sqlmesh/core/config/model.py b/sqlmesh/core/config/model.py index 5406a5497b..aeefdf2557 100644 --- a/sqlmesh/core/config/model.py +++ b/sqlmesh/core/config/model.py @@ -45,6 +45,7 @@ class ModelDefaultsConfig(BaseConfig): allow_partials: Whether the models can process partial (incomplete) data intervals. enabled: Whether the models are enabled. interval_unit: The temporal granularity of the models data intervals. By default computed from cron. + batch_concurrency: The maximum number of batches that can run concurrently for an incremental model. 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. @@ -69,6 +70,7 @@ class ModelDefaultsConfig(BaseConfig): interval_unit: t.Optional[t.Union[str, IntervalUnit]] = None enabled: t.Optional[t.Union[str, bool]] = None formatting: t.Optional[t.Union[str, bool]] = None + batch_concurrency: t.Optional[int] = None pre_statements: t.Optional[t.List[t.Union[str, exp.Expression]]] = None post_statements: t.Optional[t.List[t.Union[str, exp.Expression]]] = None on_virtual_update: t.Optional[t.List[t.Union[str, exp.Expression]]] = None diff --git a/sqlmesh/core/model/kind.py b/sqlmesh/core/model/kind.py index cc4c6f0826..ad5197a73a 100644 --- a/sqlmesh/core/model/kind.py +++ b/sqlmesh/core/model/kind.py @@ -1105,6 +1105,18 @@ def create_model_kind(v: t.Any, dialect: str, defaults: t.Dict[str, t.Any]) -> M ): props[on_change_property] = defaults.get(on_change_property) + # only pass the batch_concurrency user default to models inheriting from _IncrementalBy + # that don't explicitly set it in the model definition, but ignore subclasses of _IncrementalBy + # that hardcode a specific batch_concurrency + if issubclass(kind_type, _IncrementalBy): + BATCH_CONCURRENCY: t.Final = "batch_concurrency" + if ( + props.get(BATCH_CONCURRENCY) is None + and defaults.get(BATCH_CONCURRENCY) is not None + and kind_type.all_field_infos()[BATCH_CONCURRENCY].default is None + ): + props[BATCH_CONCURRENCY] = defaults.get(BATCH_CONCURRENCY) + if kind_type == CustomKind: # load the custom materialization class and check if it uses a custom kind type from sqlmesh.core.snapshot.evaluator import get_custom_materialization_type diff --git a/tests/core/test_model.py b/tests/core/test_model.py index f1a9eeb0b9..f6fc448b79 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -7583,6 +7583,107 @@ def test_forward_only_on_destructive_change_config() -> None: assert context_model.on_destructive_change.is_allow +def test_batch_concurrency_config() -> None: + # No batch_concurrency default for incremental models + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")) + context = Context(config=config) + + expressions = d.parse( + """ + MODEL ( + name memory.db.table, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column c + ), + ); + SELECT a, b, c FROM source_table; + """ + ) + model = load_sql_based_model(expressions, defaults=config.model_defaults.dict()) + context.upsert_model(model) + context_model = context.get_model("memory.db.table") + assert context_model.batch_concurrency is None + + # batch_concurrency specified in model defaults applies to incremental models + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb", batch_concurrency=5)) + context = Context(config=config) + + expressions = d.parse( + """ + MODEL ( + name memory.db.table, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column c + ), + ); + SELECT a, b, c FROM source_table; + """ + ) + model = load_sql_based_model(expressions, defaults=config.model_defaults.dict()) + context.upsert_model(model) + context_model = context.get_model("memory.db.table") + assert context_model.batch_concurrency == 5 + + # batch_concurrency specified in model definition overrides default + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb", batch_concurrency=5)) + context = Context(config=config) + + expressions = d.parse( + """ + MODEL ( + name memory.db.table, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column c, + batch_concurrency 10 + ), + ); + SELECT a, b, c FROM source_table; + """ + ) + model = load_sql_based_model(expressions, defaults=config.model_defaults.dict()) + context.upsert_model(model) + context_model = context.get_model("memory.db.table") + assert context_model.batch_concurrency == 10 + + # batch_concurrency default does not apply to non-incremental models + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb", batch_concurrency=5)) + context = Context(config=config) + + expressions = d.parse( + """ + MODEL ( + name memory.db.table, + kind FULL, + ); + SELECT a, b, c FROM source_table; + """ + ) + model = load_sql_based_model(expressions, defaults=config.model_defaults.dict()) + context.upsert_model(model) + context_model = context.get_model("memory.db.table") + assert context_model.batch_concurrency is None + + # batch_concurrency default does not apply to INCREMENTAL_BY_UNIQUE_KEY models + config = Config(model_defaults=ModelDefaultsConfig(dialect="duckdb", batch_concurrency=5)) + context = Context(config=config) + + expressions = d.parse( + """ + MODEL ( + name memory.db.table, + kind INCREMENTAL_BY_UNIQUE_KEY ( + unique_key a + ), + ); + SELECT a, b, c FROM source_table; + """ + ) + model = load_sql_based_model(expressions, defaults=config.model_defaults.dict()) + context.upsert_model(model) + context_model = context.get_model("memory.db.table") + assert context_model.batch_concurrency == 1 + + def test_model_meta_on_additive_change_property() -> None: """Test that ModelMeta has on_additive_change property that works like on_destructive_change.""" from sqlmesh.core.model.kind import IncrementalByTimeRangeKind, OnAdditiveChange From 0d15b4bac1c4c8e4c43bb426685885c2b756455a Mon Sep 17 00:00:00 2001 From: Iaroslav Zeigerman Date: Tue, 7 Oct 2025 11:27:34 -0700 Subject: [PATCH 0947/1056] Fix: Account for missing stdin / stdout when checking whether the runtime environment is interactive (#5500) --- sqlmesh/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sqlmesh/__init__.py b/sqlmesh/__init__.py index 47e9bacce2..7712a41379 100644 --- a/sqlmesh/__init__.py +++ b/sqlmesh/__init__.py @@ -126,6 +126,8 @@ def is_cicd_environment() -> bool: def is_interactive_environment() -> bool: + if sys.stdin is None or sys.stdout is None: + return False return sys.stdin.isatty() and sys.stdout.isatty() From d2dd2dfa9d73325657f4f9e2d22d7846705c4be0 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 8 Oct 2025 08:38:13 +1300 Subject: [PATCH 0948/1056] Chore: Fix windows tests (#5496) --- sqlmesh/utils/cache.py | 4 ++++ tests/conftest.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/sqlmesh/utils/cache.py b/sqlmesh/utils/cache.py index 4b557e43b6..e72c34f632 100644 --- a/sqlmesh/utils/cache.py +++ b/sqlmesh/utils/cache.py @@ -59,6 +59,10 @@ def __init__(self, path: Path, prefix: t.Optional[str] = None): threshold = to_datetime("1 week ago").timestamp() # delete all old cache files for file in self._path.glob("*"): + if IS_WINDOWS: + # the file.stat() call below will fail on windows if the :file name is longer than 260 chars + file = fix_windows_path(file) + if not file.stem.startswith(self._cache_version) or file.stat().st_atime < threshold: file.unlink(missing_ok=True) diff --git a/tests/conftest.py b/tests/conftest.py index 7a61281ad0..955b50234c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -580,7 +580,19 @@ def _make_function( # shutil.copytree just doesnt work properly with the symlinks on Windows, regardless of the `symlinks` setting src = str(path.absolute()) dst = str(temp_dir.absolute()) - os.system(f"robocopy {src} {dst} /E /COPYALL") + + # Robocopy flag reference: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy#copy-options + # /E: Copy subdirectories, including empty directories + # /COPY:D Copy "data" only. In particular, this avoids copying auditing information, which can throw + # an error like "ERROR : You do not have the Manage Auditing user right" + robocopy_cmd = f"robocopy {src} {dst} /E /COPY:D" + exit_code = os.system(robocopy_cmd) + + # exit code reference: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy#exit-return-codes + if exit_code > 8: + raise Exception( + f"robocopy command: '{robocopy_cmd}' failed with exit code: {exit_code}" + ) # after copying, delete the files that would have been ignored for root, dirs, _ in os.walk(temp_dir): From 850fcd132b8c2ca28476611eb156b33c85b34da3 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 8 Oct 2025 08:44:29 +1300 Subject: [PATCH 0949/1056] Feat(sqlmesh_dbt): Support the --threads CLI option (#5493) --- sqlmesh/core/config/loader.py | 1 + sqlmesh/dbt/loader.py | 5 +++++ sqlmesh_dbt/cli.py | 14 +++++++++++--- sqlmesh_dbt/operations.py | 5 ++++- tests/dbt/cli/test_operations.py | 30 ++++++++++++++++++++++++++++++ tests/dbt/cli/test_run.py | 8 ++++++++ 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index 75915800e6..2d202cb276 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -177,6 +177,7 @@ def load_config_from_paths( dbt_profile_name=kwargs.pop("profile", None), dbt_target_name=kwargs.pop("target", None), variables=variables, + threads=kwargs.pop("threads", None), ) if type(dbt_python_config) != config_type: dbt_python_config = convert_config_type(dbt_python_config, config_type) diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index eb117a3e40..39973776a8 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -49,6 +49,7 @@ def sqlmesh_config( dbt_profile_name: t.Optional[str] = None, dbt_target_name: t.Optional[str] = None, variables: t.Optional[t.Dict[str, t.Any]] = None, + threads: t.Optional[int] = None, register_comments: t.Optional[bool] = None, **kwargs: t.Any, ) -> Config: @@ -67,6 +68,10 @@ def sqlmesh_config( if not issubclass(loader, DbtLoader): raise ConfigError("The loader must be a DbtLoader.") + if threads is not None: + # the to_sqlmesh() function on TargetConfig maps self.threads -> concurrent_tasks + profile.target.threads = threads + return Config( loader=loader, model_defaults=model_defaults, diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py index 83230de3fd..ec11e7730e 100644 --- a/sqlmesh_dbt/cli.py +++ b/sqlmesh_dbt/cli.py @@ -8,11 +8,13 @@ import functools -def _get_dbt_operations(ctx: click.Context, vars: t.Optional[t.Dict[str, t.Any]]) -> DbtOperations: +def _get_dbt_operations( + ctx: click.Context, vars: t.Optional[t.Dict[str, t.Any]], threads: t.Optional[int] = None +) -> DbtOperations: if not isinstance(ctx.obj, functools.partial): raise ValueError(f"Unexpected click context object: {type(ctx.obj)}") - dbt_operations = ctx.obj(vars=vars) + dbt_operations = ctx.obj(vars=vars, threads=threads) if not isinstance(dbt_operations, DbtOperations): raise ValueError(f"Unexpected dbt operations type: {type(dbt_operations)}") @@ -128,16 +130,22 @@ def dbt( @click.option( "--empty/--no-empty", default=False, help="If specified, limit input refs and sources" ) +@click.option( + "--threads", + type=int, + help="Specify number of threads to use while executing models. Overrides settings in profiles.yml.", +) @vars_option @click.pass_context def run( ctx: click.Context, vars: t.Optional[t.Dict[str, t.Any]], + threads: t.Optional[int], env: t.Optional[str] = None, **kwargs: t.Any, ) -> None: """Compile SQL and execute against the current target database.""" - _get_dbt_operations(ctx, vars).run(environment=env, **kwargs) + _get_dbt_operations(ctx, vars, threads).run(environment=env, **kwargs) @dbt.command(name="list") diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index 6e8b452b28..cb1ac217cc 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -235,6 +235,7 @@ def create( profile: t.Optional[str] = None, target: t.Optional[str] = None, vars: t.Optional[t.Dict[str, t.Any]] = None, + threads: t.Optional[int] = None, debug: bool = False, ) -> DbtOperations: with Progress(transient=True) as progress: @@ -265,7 +266,9 @@ def create( sqlmesh_context = Context( paths=[project_dir], - config_loader_kwargs=dict(profile=profile, target=target, variables=vars), + config_loader_kwargs=dict( + profile=profile, target=target, variables=vars, threads=threads + ), load=True, # DbtSelector selects based on dbt model fqn's rather than SQLMesh model names selector=DbtSelector, diff --git a/tests/dbt/cli/test_operations.py b/tests/dbt/cli/test_operations.py index b23c87882a..139336297c 100644 --- a/tests/dbt/cli/test_operations.py +++ b/tests/dbt/cli/test_operations.py @@ -333,3 +333,33 @@ def test_run_option_full_refresh_with_selector(jaffle_shop_duckdb: Path): assert not plan.empty_backfill assert not plan.skip_backfill assert plan.models_to_backfill == set(['"jaffle_shop"."main"."stg_customers"']) + + +def test_create_sets_concurrent_tasks_based_on_threads(create_empty_project: EmptyProjectCreator): + project_dir, _ = create_empty_project(project_name="test") + + # add a postgres target because duckdb overrides to concurrent_tasks=1 regardless of what gets specified + profiles_yml_file = project_dir / "profiles.yml" + profiles_yml = yaml.load(profiles_yml_file) + profiles_yml["test"]["outputs"]["postgres"] = { + "type": "postgres", + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "postgres", + "dbname": "test", + "schema": "test", + } + profiles_yml_file.write_text(yaml.dump(profiles_yml)) + + operations = create(project_dir=project_dir, target="postgres") + + assert operations.context.concurrent_tasks == 1 # 1 is the default + + operations = create(project_dir=project_dir, threads=16, target="postgres") + + assert operations.context.concurrent_tasks == 16 + assert all( + g.connection and g.connection.concurrent_tasks == 16 + for g in operations.context.config.gateways.values() + ) diff --git a/tests/dbt/cli/test_run.py b/tests/dbt/cli/test_run.py index 755553bb57..4fdb7a0cdb 100644 --- a/tests/dbt/cli/test_run.py +++ b/tests/dbt/cli/test_run.py @@ -83,3 +83,11 @@ def test_run_with_changes_and_full_refresh( ("foo", "bar", "changed"), ("baz", "bing", "changed"), ] + + +def test_run_with_threads(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): + result = invoke_cli(["run", "--threads", "4"]) + assert result.exit_code == 0 + assert not result.exception + + assert "Model batches executed" in result.output From 9f573b4ada368c3cd3b098170d9209e5a92d5ac8 Mon Sep 17 00:00:00 2001 From: David Dai Date: Tue, 7 Oct 2025 15:51:29 -0700 Subject: [PATCH 0950/1056] feat(experimental): add grants support for DBT custom materializations (#5489) --- sqlmesh/core/snapshot/evaluator.py | 14 ++++++ tests/dbt/test_custom_materializations.py | 56 +++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/sqlmesh/core/snapshot/evaluator.py b/sqlmesh/core/snapshot/evaluator.py index 773010d673..f7aea5cff1 100644 --- a/sqlmesh/core/snapshot/evaluator.py +++ b/sqlmesh/core/snapshot/evaluator.py @@ -2940,6 +2940,13 @@ def create( **kwargs, ) + # Apply grants after dbt custom materialization table creation + if not skip_grants: + is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False) + self._apply_grants( + model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) + def insert( self, table_name: str, @@ -2958,6 +2965,13 @@ def insert( **kwargs, ) + # Apply grants after custom materialization insert (only on first insert) + if is_first_insert: + is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False) + self._apply_grants( + model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable + ) + def append( self, table_name: str, diff --git a/tests/dbt/test_custom_materializations.py b/tests/dbt/test_custom_materializations.py index 9e7a94315c..c1625d0251 100644 --- a/tests/dbt/test_custom_materializations.py +++ b/tests/dbt/test_custom_materializations.py @@ -7,6 +7,7 @@ from sqlmesh import Context from sqlmesh.core.config import ModelDefaultsConfig +from sqlmesh.core.engine_adapter import DuckDBEngineAdapter from sqlmesh.core.model.kind import DbtCustomKind from sqlmesh.dbt.context import DbtContext from sqlmesh.dbt.manifest import ManifestHelper @@ -719,3 +720,58 @@ def test_custom_materialization_lineage_tracking(copy_to_temp_path: t.Callable): # Dev and prod should have the same data as they share physical data assert dev_analytics_result["count"][0] == prod_analytics_result["count"][0] assert dev_analytics_result["unique_waiters"][0] == prod_analytics_result["unique_waiters"][0] + + +@pytest.mark.xdist_group("dbt_manifest") +def test_custom_materialization_grants(copy_to_temp_path: t.Callable, mocker): + path = copy_to_temp_path("tests/fixtures/dbt/sushi_test") + temp_project = path[0] + + models_dir = temp_project / "models" + models_dir.mkdir(parents=True, exist_ok=True) + + grants_model_content = """ +{{ config( + materialized='custom_incremental', + grants={ + 'select': ['user1', 'user2'], + 'insert': ['writer'] + } +) }} + +SELECT + CURRENT_TIMESTAMP as created_at, + 1 as id, + 'grants_test' as test_type +""".strip() + + (models_dir / "test_grants_model.sql").write_text(grants_model_content) + + mocker.patch.object(DuckDBEngineAdapter, "SUPPORTS_GRANTS", True) + mocker.patch.object(DuckDBEngineAdapter, "_get_current_grants_config", return_value={}) + + sync_grants_calls = [] + + def mock_sync_grants(*args, **kwargs): + sync_grants_calls.append((args, kwargs)) + + mocker.patch.object(DuckDBEngineAdapter, "sync_grants_config", side_effect=mock_sync_grants) + + context = Context(paths=path) + + model = context.get_model("sushi.test_grants_model") + assert isinstance(model.kind, DbtCustomKind) + plan = context.plan(select_models=["sushi.test_grants_model"]) + context.apply(plan) + + assert len(sync_grants_calls) == 1 + args = sync_grants_calls[0][0] + assert args + + table = args[0] + grants_config = args[1] + assert table.sql(dialect="duckdb") == "memory.sushi.test_grants_model" + assert grants_config == { + "select": ["user1", "user2"], + "insert": ["writer"], + } From 7d7469037e03b917b047287501b4f01185262cf9 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Wed, 8 Oct 2025 13:29:32 +1300 Subject: [PATCH 0951/1056] Feat: Add config flag to infer the state schema per dbt target (#5485) --- sqlmesh/cli/project_init.py | 10 +++- sqlmesh/core/config/__init__.py | 2 +- sqlmesh/core/config/dbt.py | 13 +++++ sqlmesh/core/config/loader.py | 5 ++ sqlmesh/core/config/root.py | 3 ++ sqlmesh/dbt/loader.py | 26 ++++++++++ tests/dbt/test_config.py | 38 +++++++++++++- tests/dbt/test_integration.py | 50 ++++++++++++++++++- tests/fixtures/dbt/empty_project/profiles.yml | 6 ++- 9 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 sqlmesh/core/config/dbt.py diff --git a/sqlmesh/cli/project_init.py b/sqlmesh/cli/project_init.py index 6b4f6c7a83..e3132a6de3 100644 --- a/sqlmesh/cli/project_init.py +++ b/sqlmesh/cli/project_init.py @@ -116,7 +116,15 @@ def _gen_config( - invalidselectstarexpansion - noambiguousprojections """, - ProjectTemplate.DBT: f"""# --- Virtual Data Environment Mode --- + ProjectTemplate.DBT: f"""# --- DBT-specific options --- +dbt: + # This configuration ensures that each dbt target gets its own isolated state. + # The inferred state schemas are named "sqlmesh_state__", eg "sqlmesh_state_jaffle_shop_dev" + # If this is undesirable, you may manually configure the gateway to use a specific state schema name + # https://sqlmesh.readthedocs.io/en/stable/integrations/dbt/#selecting-a-different-state-connection + infer_state_schema_name: True + +# --- Virtual Data Environment Mode --- # Enable Virtual Data Environments (VDE) for *development* environments. # Note that the production environment in dbt projects is not virtual by default to maintain compatibility with existing tooling. # https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#virtual-data-environment-modes diff --git a/sqlmesh/core/config/__init__.py b/sqlmesh/core/config/__init__.py index 0dc99c0fd1..42ed82c6e6 100644 --- a/sqlmesh/core/config/__init__.py +++ b/sqlmesh/core/config/__init__.py @@ -36,6 +36,6 @@ from sqlmesh.core.config.naming import NameInferenceConfig as NameInferenceConfig from sqlmesh.core.config.linter import LinterConfig as LinterConfig from sqlmesh.core.config.plan import PlanConfig as PlanConfig -from sqlmesh.core.config.root import Config as Config +from sqlmesh.core.config.root import Config as Config, DbtConfig as DbtConfig from sqlmesh.core.config.run import RunConfig as RunConfig from sqlmesh.core.config.scheduler import BuiltInSchedulerConfig as BuiltInSchedulerConfig diff --git a/sqlmesh/core/config/dbt.py b/sqlmesh/core/config/dbt.py new file mode 100644 index 0000000000..e3132c40a4 --- /dev/null +++ b/sqlmesh/core/config/dbt.py @@ -0,0 +1,13 @@ +from sqlmesh.core.config.base import BaseConfig + + +class DbtConfig(BaseConfig): + """ + Represents dbt-specific options on the SQLMesh root config. + + These options are only taken into account for dbt projects and are ignored on native projects + """ + + infer_state_schema_name: bool = False + """If set, indicates to the dbt loader that the state schema should be inferred based on the profile/target + so that each target gets its own isolated state""" diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index 2d202cb276..e05c148b90 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -172,12 +172,17 @@ def load_config_from_paths( if dbt_project_file: from sqlmesh.dbt.loader import sqlmesh_config + infer_state_schema_name = False + if dbt := non_python_config.dbt: + infer_state_schema_name = dbt.infer_state_schema_name + dbt_python_config = sqlmesh_config( project_root=dbt_project_file.parent, dbt_profile_name=kwargs.pop("profile", None), dbt_target_name=kwargs.pop("target", None), variables=variables, threads=kwargs.pop("threads", None), + infer_state_schema_name=infer_state_schema_name, ) if type(dbt_python_config) != config_type: dbt_python_config = convert_config_type(dbt_python_config, config_type) diff --git a/sqlmesh/core/config/root.py b/sqlmesh/core/config/root.py index 9b6fae63e3..211d271b01 100644 --- a/sqlmesh/core/config/root.py +++ b/sqlmesh/core/config/root.py @@ -36,6 +36,7 @@ from sqlmesh.core.config.linter import LinterConfig as LinterConfig from sqlmesh.core.config.plan import PlanConfig from sqlmesh.core.config.run import RunConfig +from sqlmesh.core.config.dbt import DbtConfig from sqlmesh.core.config.scheduler import ( BuiltInSchedulerConfig, SchedulerConfig, @@ -173,6 +174,7 @@ class Config(BaseConfig): linter: LinterConfig = LinterConfig() janitor: JanitorConfig = JanitorConfig() cache_dir: t.Optional[str] = None + dbt: t.Optional[DbtConfig] = None _FIELD_UPDATE_STRATEGY: t.ClassVar[t.Dict[str, UpdateStrategy]] = { "gateways": UpdateStrategy.NESTED_UPDATE, @@ -191,6 +193,7 @@ class Config(BaseConfig): "before_all": UpdateStrategy.EXTEND, "after_all": UpdateStrategy.EXTEND, "linter": UpdateStrategy.NESTED_UPDATE, + "dbt": UpdateStrategy.NESTED_UPDATE, } _connection_config_validator = connection_config_validator diff --git a/sqlmesh/dbt/loader.py b/sqlmesh/dbt/loader.py index 39973776a8..049c761ed1 100644 --- a/sqlmesh/dbt/loader.py +++ b/sqlmesh/dbt/loader.py @@ -11,6 +11,7 @@ ConnectionConfig, GatewayConfig, ModelDefaultsConfig, + DbtConfig as RootDbtConfig, ) from sqlmesh.core.environment import EnvironmentStatements from sqlmesh.core.loader import CacheBase, LoadedProject, Loader @@ -51,6 +52,7 @@ def sqlmesh_config( variables: t.Optional[t.Dict[str, t.Any]] = None, threads: t.Optional[int] = None, register_comments: t.Optional[bool] = None, + infer_state_schema_name: bool = False, **kwargs: t.Any, ) -> Config: project_root = project_root or Path() @@ -72,16 +74,40 @@ def sqlmesh_config( # the to_sqlmesh() function on TargetConfig maps self.threads -> concurrent_tasks profile.target.threads = threads + gateway_kwargs = {} + if infer_state_schema_name: + profile_name = context.profile_name + + # Note: we deliberately isolate state based on the target *schema* and not the target name. + # It is assumed that the project will define a target, eg 'dev', and then in each users own ~/.dbt/profiles.yml the schema + # for the 'dev' target is overriden to something user-specific, rather than making the target name itself user-specific. + # This means that the schema name is the indicator of isolated state, not the target name which may be re-used across multiple schemas. + target_schema = profile.target.schema_ + + # dbt-core doesnt allow schema to be undefined, but it does allow an empty string, and then just + # fails at runtime when `CREATE SCHEMA ""` doesnt work + if not target_schema: + raise ConfigError( + f"Target '{profile.target_name}' does not specify a schema.\n" + "A schema is required in order to infer where to store SQLMesh state" + ) + + inferred_state_schema_name = f"sqlmesh_state_{profile_name}_{target_schema}" + logger.info("Inferring state schema: %s", inferred_state_schema_name) + gateway_kwargs["state_schema"] = inferred_state_schema_name + return Config( loader=loader, model_defaults=model_defaults, variables=variables or {}, + dbt=RootDbtConfig(infer_state_schema_name=infer_state_schema_name), **{ "default_gateway": profile.target_name if "gateways" not in kwargs else "", "gateways": { profile.target_name: GatewayConfig( connection=profile.target.to_sqlmesh(**target_to_sqlmesh_args), state_connection=state_connection, + **gateway_kwargs, ) }, # type: ignore **kwargs, diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index b3ee0c422a..5dccd90ed2 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -15,6 +15,7 @@ from sqlmesh.core.dialect import jinja_query from sqlmesh.core.model import SqlModel from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange +from sqlmesh.core.state_sync import CachingStateSync, EngineAdapterStateSync from sqlmesh.dbt.builtin import Api from sqlmesh.dbt.column import ColumnConfig from sqlmesh.dbt.common import Dependencies @@ -46,7 +47,8 @@ ) from sqlmesh.dbt.test import TestConfig from sqlmesh.utils.errors import ConfigError -from sqlmesh.utils.yaml import load as yaml_load +from sqlmesh.utils.yaml import load as yaml_load, dump as yaml_dump +from tests.dbt.conftest import EmptyProjectCreator pytestmark = pytest.mark.dbt @@ -1211,3 +1213,37 @@ def test_empty_vars_config(tmp_path): # Verify the variables are empty (not causing any issues) assert project.packages["test_empty_vars"].variables == {} assert project.context.variables == {} + + +def test_infer_state_schema_name(create_empty_project: EmptyProjectCreator): + project_dir, _ = create_empty_project("test_foo", "dev") + + # infer_state_schema_name defaults to False if omitted + config = sqlmesh_config(project_root=project_dir) + assert config.dbt + assert not config.dbt.infer_state_schema_name + assert config.get_state_schema() == "sqlmesh" + + # create_empty_project() uses the default dbt template for sqlmesh yaml config which + # sets infer_state_schema_name=True + ctx = Context(paths=[project_dir]) + assert ctx.config.dbt + assert ctx.config.dbt.infer_state_schema_name + assert ctx.config.get_state_schema() == "sqlmesh_state_test_foo_main" + assert isinstance(ctx.state_sync, CachingStateSync) + assert isinstance(ctx.state_sync.state_sync, EngineAdapterStateSync) + assert ctx.state_sync.state_sync.schema == "sqlmesh_state_test_foo_main" + + # If the user delberately overrides state_schema then we should respect this choice + config_file = project_dir / "sqlmesh.yaml" + config_yaml = yaml_load(config_file) + config_yaml["gateways"] = {"dev": {"state_schema": "state_override"}} + config_file.write_text(yaml_dump(config_yaml)) + + ctx = Context(paths=[project_dir]) + assert ctx.config.dbt + assert ctx.config.dbt.infer_state_schema_name + assert ctx.config.get_state_schema() == "state_override" + assert isinstance(ctx.state_sync, CachingStateSync) + assert isinstance(ctx.state_sync.state_sync, EngineAdapterStateSync) + assert ctx.state_sync.state_sync.schema == "state_override" diff --git a/tests/dbt/test_integration.py b/tests/dbt/test_integration.py index e1f051dbcf..ab22bf7826 100644 --- a/tests/dbt/test_integration.py +++ b/tests/dbt/test_integration.py @@ -19,7 +19,8 @@ from sqlmesh.core.config.connection import DuckDBConnectionConfig from sqlmesh.core.engine_adapter import DuckDBEngineAdapter from sqlmesh.utils.pandas import columns_to_types_from_df -from sqlmesh.utils.yaml import YAML +from sqlmesh.utils.yaml import YAML, load as yaml_load, dump as yaml_dump +from sqlmesh_dbt.operations import init_project_if_required from tests.utils.pandas import compare_dataframes, create_df # Some developers had issues with this test freezing locally so we mark it as cicdonly @@ -604,3 +605,50 @@ def test_dbt_node_info(jaffle_shop_duckdb_context: Context): relationship_audit.node.dbt_node_info.name == "relationships_orders_customer_id__customer_id__ref_customers_" ) + + +def test_state_schema_isolation_per_target(jaffle_shop_duckdb: Path): + profiles_file = jaffle_shop_duckdb / "profiles.yml" + + profiles_yml = yaml_load(profiles_file) + + # make prod / dev config identical with the exception of a different default schema to simulate using the same warehouse + profiles_yml["jaffle_shop"]["outputs"]["prod"] = { + **profiles_yml["jaffle_shop"]["outputs"]["dev"] + } + profiles_yml["jaffle_shop"]["outputs"]["prod"]["schema"] = "prod_schema" + profiles_yml["jaffle_shop"]["outputs"]["dev"]["schema"] = "dev_schema" + + profiles_file.write_text(yaml_dump(profiles_yml)) + + init_project_if_required(jaffle_shop_duckdb) + + # start off with the prod target + prod_ctx = Context(paths=[jaffle_shop_duckdb], config_loader_kwargs={"target": "prod"}) + assert prod_ctx.config.get_state_schema() == "sqlmesh_state_jaffle_shop_prod_schema" + assert all("prod_schema" in fqn for fqn in prod_ctx.models) + assert prod_ctx.plan(auto_apply=True).has_changes + assert not prod_ctx.plan(auto_apply=True).has_changes + + # dev target should have changes - new state separate from prod + dev_ctx = Context(paths=[jaffle_shop_duckdb], config_loader_kwargs={"target": "dev"}) + assert dev_ctx.config.get_state_schema() == "sqlmesh_state_jaffle_shop_dev_schema" + assert all("dev_schema" in fqn for fqn in dev_ctx.models) + assert dev_ctx.plan(auto_apply=True).has_changes + assert not dev_ctx.plan(auto_apply=True).has_changes + + # no explicitly specified target should use dev because that's what's set for the default in the profiles.yml + assert profiles_yml["jaffle_shop"]["target"] == "dev" + default_ctx = Context(paths=[jaffle_shop_duckdb]) + assert default_ctx.config.get_state_schema() == "sqlmesh_state_jaffle_shop_dev_schema" + assert all("dev_schema" in fqn for fqn in default_ctx.models) + assert not default_ctx.plan(auto_apply=True).has_changes + + # an explicit state schema override set in `sqlmesh.yaml` should use that + sqlmesh_yaml_file = jaffle_shop_duckdb / "sqlmesh.yaml" + sqlmesh_yaml = yaml_load(sqlmesh_yaml_file) + sqlmesh_yaml["gateways"] = {"dev": {"state_schema": "sqlmesh_dev_state_override"}} + sqlmesh_yaml_file.write_text(yaml_dump(sqlmesh_yaml)) + default_ctx = Context(paths=[jaffle_shop_duckdb]) + assert default_ctx.config.get_state_schema() == "sqlmesh_dev_state_override" + assert all("dev_schema" in fqn for fqn in default_ctx.models) diff --git a/tests/fixtures/dbt/empty_project/profiles.yml b/tests/fixtures/dbt/empty_project/profiles.yml index b352fc5792..adae09e9c6 100644 --- a/tests/fixtures/dbt/empty_project/profiles.yml +++ b/tests/fixtures/dbt/empty_project/profiles.yml @@ -3,7 +3,11 @@ empty_project: target: __DEFAULT_TARGET__ outputs: - duckdb: + __DEFAULT_TARGET__: type: duckdb + # database is required for dbt < 1.5 where our adapter deliberately doesnt infer the database from the path and + # defaults it to "main", which raises a "project catalog doesnt match context catalog" error + # ref: https://github.com/TobikoData/sqlmesh/pull/1109 + database: empty_project path: 'empty_project.duckdb' threads: 4 From 9398485be2f6969c97180723b81937cbcf455d00 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:58:52 +0300 Subject: [PATCH 0952/1056] Chore: Fix typo in dbt error message (#5507) --- sqlmesh/dbt/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh/dbt/source.py b/sqlmesh/dbt/source.py index efafbf1642..832ed0e156 100644 --- a/sqlmesh/dbt/source.py +++ b/sqlmesh/dbt/source.py @@ -79,7 +79,7 @@ def canonical_name(self, context: DbtContext) -> str: relation = source(self.source_name_, self.name) except Exception as e: raise ConfigError( - f"'source' macro failed for '{self.config_name}' with exeception '{e}'." + f"'source' macro failed for '{self.config_name}' with exception '{e}'." ) relation = relation.quote( From 46aaf78a01cf9ccc471b26290f088ca6202b3198 Mon Sep 17 00:00:00 2001 From: Tori Wei <41123940+toriwei@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:48:04 -0700 Subject: [PATCH 0953/1056] fix: validate data_type for partition_by config (#5491) --- sqlmesh/dbt/model.py | 8 ++++++++ tests/dbt/test_transformation.py | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index 09c410561d..d882f94942 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -215,6 +215,14 @@ def _validate_partition_by( ): granularity = v["granularity"] raise ConfigError(f"Unexpected granularity '{granularity}' in partition_by '{v}'.") + if "data_type" in v and v["data_type"].lower() not in ( + "timestamp", + "date", + "datetime", + "int64", + ): + data_type = v["data_type"] + raise ConfigError(f"Unexpected data_type '{data_type}' in partition_by '{v}'.") return {"data_type": "date", "granularity": "day", **v} raise ConfigError(f"Invalid format for partition_by '{v}'") diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index dd69f46200..e519713d26 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1881,6 +1881,17 @@ def test_partition_by(sushi_test_project: Project): ) assert model_config.to_sqlmesh(context).partitioned_by == [] + with pytest.raises(ConfigError, match="Unexpected data_type 'string' in partition_by"): + ModelConfig( + name="model", + alias="model", + schema="test", + package_name="package", + materialized="table", + partition_by={"field": "ds", "data_type": "string"}, + sql="""SELECT 1 AS one, ds FROM foo""", + ) + @pytest.mark.xdist_group("dbt_manifest") def test_partition_by_none(sushi_test_project: Project): From 3535195aa272ccc5eac4d3ca64cdaea148f2ddc0 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Wed, 8 Oct 2025 18:59:52 +0300 Subject: [PATCH 0954/1056] Chore!: bump sqlglot to v27.22.0 (#5508) --- pyproject.toml | 2 +- tests/core/test_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 053b242813..71c9d62bbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "rich[jupyter]", "ruamel.yaml", - "sqlglot[rs]~=27.20.0", + "sqlglot[rs]~=27.24.2", "tenacity", "time-machine", "json-stream" diff --git a/tests/core/test_model.py b/tests/core/test_model.py index f6fc448b79..c3feef6095 100644 --- a/tests/core/test_model.py +++ b/tests/core/test_model.py @@ -919,7 +919,7 @@ def test_json_serde(): assert ( SqlModel.parse_obj(model_json_parsed).render_query().sql("duckdb") - == 'SELECT REGEXP_MATCHES("x", "y") AS "c"' + == 'SELECT REGEXP_FULL_MATCH("x", "y") AS "c"' ) From bbdbd4800493ef17e536d04e15b9129303b81b1f Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 8 Oct 2025 09:40:58 -0700 Subject: [PATCH 0955/1056] feat: batch expired snapshots (#5486) --- docs/reference/configuration.md | 7 +- sqlmesh/core/config/janitor.py | 12 + sqlmesh/core/context.py | 20 +- sqlmesh/core/state_sync/base.py | 51 +- sqlmesh/core/state_sync/cache.py | 13 +- sqlmesh/core/state_sync/common.py | 280 ++++++++++- sqlmesh/core/state_sync/db/facade.py | 37 +- sqlmesh/core/state_sync/db/snapshot.py | 192 ++++---- tests/core/state_sync/test_state_sync.py | 563 +++++++++++++++++++++-- tests/core/test_context.py | 2 +- 10 files changed, 1004 insertions(+), 173 deletions(-) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 676f9d7389..b13438ee2d 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -125,9 +125,10 @@ Formatting settings for the `sqlmesh format` command and UI. Configuration for the `sqlmesh janitor` command. -| Option | Description | Type | Required | -|--------------------------|----------------------------------------------------------------------------------------------------------------------------|:-------:|:--------:| -| `warn_on_delete_failure` | Whether to warn instead of erroring if the janitor fails to delete the expired environment schema / views (Default: False) | boolean | N | +| Option | Description | Type | Required | +|---------------------------------|----------------------------------------------------------------------------------------------------------------------------|:-------:|:--------:| +| `warn_on_delete_failure` | Whether to warn instead of erroring if the janitor fails to delete the expired environment schema / views (Default: False) | boolean | N | +| `expired_snapshots_batch_size` | Maximum number of expired snapshots to clean in a single batch (Default: 200) | int | N | ## UI diff --git a/sqlmesh/core/config/janitor.py b/sqlmesh/core/config/janitor.py index d288c90b3e..0f1c953bc0 100644 --- a/sqlmesh/core/config/janitor.py +++ b/sqlmesh/core/config/janitor.py @@ -1,7 +1,9 @@ from __future__ import annotations +import typing as t from sqlmesh.core.config.base import BaseConfig +from sqlmesh.utils.pydantic import field_validator class JanitorConfig(BaseConfig): @@ -9,6 +11,16 @@ class JanitorConfig(BaseConfig): Args: warn_on_delete_failure: Whether to warn instead of erroring if the janitor fails to delete the expired environment schema / views. + expired_snapshots_batch_size: Maximum number of expired snapshots to clean in a single batch. """ warn_on_delete_failure: bool = False + expired_snapshots_batch_size: t.Optional[int] = None + + @field_validator("expired_snapshots_batch_size", mode="before") + @classmethod + def _validate_batch_size(cls, value: int) -> int: + batch_size = int(value) + if batch_size <= 0: + raise ValueError("expired_snapshots_batch_size must be greater than 0") + return batch_size diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index e31a04fe81..bd8647f811 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -109,6 +109,7 @@ StateSync, cleanup_expired_views, ) +from sqlmesh.core.state_sync.common import delete_expired_snapshots from sqlmesh.core.table_diff import TableDiff from sqlmesh.core.test import ( ModelTextTestResult, @@ -2852,19 +2853,14 @@ def _run_janitor(self, ignore_ttl: bool = False) -> None: # Clean up expired environments by removing their views and schemas self._cleanup_environments(current_ts=current_ts) - cleanup_targets = self.state_sync.get_expired_snapshots( - ignore_ttl=ignore_ttl, current_ts=current_ts - ) - - # Remove the expired snapshots tables - self.snapshot_evaluator.cleanup( - target_snapshots=cleanup_targets, - on_complete=self.console.update_cleanup_progress, + delete_expired_snapshots( + self.state_sync, + self.snapshot_evaluator, + current_ts=current_ts, + ignore_ttl=ignore_ttl, + console=self.console, + batch_size=self.config.janitor.expired_snapshots_batch_size, ) - - # Delete the expired snapshot records from the state sync - self.state_sync.delete_expired_snapshots(ignore_ttl=ignore_ttl, current_ts=current_ts) - self.state_sync.compact_intervals() def _cleanup_environments(self, current_ts: t.Optional[int] = None) -> None: diff --git a/sqlmesh/core/state_sync/base.py b/sqlmesh/core/state_sync/base.py index 2f8a68dd4a..3c8c72845d 100644 --- a/sqlmesh/core/state_sync/base.py +++ b/sqlmesh/core/state_sync/base.py @@ -11,7 +11,6 @@ from sqlmesh import migrations from sqlmesh.core.environment import ( Environment, - EnvironmentNamingInfo, EnvironmentStatements, EnvironmentSummary, ) @@ -21,8 +20,6 @@ SnapshotIdLike, SnapshotIdAndVersionLike, SnapshotInfoLike, - SnapshotTableCleanupTask, - SnapshotTableInfo, SnapshotNameVersion, SnapshotIdAndVersion, ) @@ -30,8 +27,13 @@ from sqlmesh.utils import major_minor from sqlmesh.utils.date import TimeLike from sqlmesh.utils.errors import SQLMeshError -from sqlmesh.utils.pydantic import PydanticModel, ValidationInfo, field_validator -from sqlmesh.core.state_sync.common import StateStream +from sqlmesh.utils.pydantic import PydanticModel, field_validator +from sqlmesh.core.state_sync.common import ( + StateStream, + ExpiredSnapshotBatch, + PromotionResult, + ExpiredBatchRange, +) logger = logging.getLogger(__name__) @@ -72,20 +74,6 @@ def _schema_version_validator(cls, v: t.Any) -> int: SCHEMA_VERSION: int = MIN_SCHEMA_VERSION + len(MIGRATIONS) - 1 -class PromotionResult(PydanticModel): - added: t.List[SnapshotTableInfo] - removed: t.List[SnapshotTableInfo] - removed_environment_naming_info: t.Optional[EnvironmentNamingInfo] - - @field_validator("removed_environment_naming_info") - def _validate_removed_environment_naming_info( - cls, v: t.Optional[EnvironmentNamingInfo], info: ValidationInfo - ) -> t.Optional[EnvironmentNamingInfo]: - if v and not info.data.get("removed"): - raise ValueError("removed_environment_naming_info must be None if removed is empty") - return v - - class StateReader(abc.ABC): """Abstract base class for read-only operations on snapshot and environment state.""" @@ -315,15 +303,21 @@ def export(self, environment_names: t.Optional[t.List[str]] = None) -> StateStre @abc.abstractmethod def get_expired_snapshots( - self, current_ts: t.Optional[int] = None, ignore_ttl: bool = False - ) -> t.List[SnapshotTableCleanupTask]: - """Aggregates the id's of the expired snapshots and creates a list of table cleanup tasks. + self, + *, + batch_range: ExpiredBatchRange, + current_ts: t.Optional[int] = None, + ignore_ttl: bool = False, + ) -> t.Optional[ExpiredSnapshotBatch]: + """Returns a single batch of expired snapshots ordered by (updated_ts, name, identifier). - Expired snapshots are snapshots that have exceeded their time-to-live - and are no longer in use within an environment. + Args: + current_ts: Timestamp used to evaluate expiration. + ignore_ttl: If True, include snapshots regardless of TTL (only checks if unreferenced). + batch_range: The range of the batch to fetch. Returns: - The list of table cleanup tasks. + A batch describing expired snapshots or None if no snapshots are pending cleanup. """ @abc.abstractmethod @@ -363,7 +357,10 @@ def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None: @abc.abstractmethod def delete_expired_snapshots( - self, ignore_ttl: bool = False, current_ts: t.Optional[int] = None + self, + batch_range: ExpiredBatchRange, + ignore_ttl: bool = False, + current_ts: t.Optional[int] = None, ) -> None: """Removes expired snapshots. @@ -371,8 +368,10 @@ def delete_expired_snapshots( and are no longer in use within an environment. Args: + batch_range: The range of snapshots to delete in this batch. ignore_ttl: Ignore the TTL on the snapshot when considering it expired. This has the effect of deleting all snapshots that are not referenced in any environment + current_ts: Timestamp used to evaluate expiration. """ @abc.abstractmethod diff --git a/sqlmesh/core/state_sync/cache.py b/sqlmesh/core/state_sync/cache.py index 3de4e7bf51..77f3fc6ba5 100644 --- a/sqlmesh/core/state_sync/cache.py +++ b/sqlmesh/core/state_sync/cache.py @@ -12,6 +12,7 @@ ) from sqlmesh.core.snapshot.definition import Interval, SnapshotIntervals from sqlmesh.core.state_sync.base import DelegatingStateSync, StateSync +from sqlmesh.core.state_sync.common import ExpiredBatchRange from sqlmesh.utils.date import TimeLike, now_timestamp @@ -108,11 +109,17 @@ def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None: self.state_sync.delete_snapshots(snapshot_ids) def delete_expired_snapshots( - self, ignore_ttl: bool = False, current_ts: t.Optional[int] = None + self, + batch_range: ExpiredBatchRange, + ignore_ttl: bool = False, + current_ts: t.Optional[int] = None, ) -> None: - current_ts = current_ts or now_timestamp() self.snapshot_cache.clear() - self.state_sync.delete_expired_snapshots(current_ts=current_ts, ignore_ttl=ignore_ttl) + self.state_sync.delete_expired_snapshots( + batch_range=batch_range, + ignore_ttl=ignore_ttl, + current_ts=current_ts, + ) def add_snapshots_intervals(self, snapshots_intervals: t.Sequence[SnapshotIntervals]) -> None: for snapshot_intervals in snapshots_intervals: diff --git a/sqlmesh/core/state_sync/common.py b/sqlmesh/core/state_sync/common.py index cd8c389e33..3fdd0bc015 100644 --- a/sqlmesh/core/state_sync/common.py +++ b/sqlmesh/core/state_sync/common.py @@ -7,21 +7,31 @@ import abc from dataclasses import dataclass + +from pydantic_core.core_schema import ValidationInfo from sqlglot import exp from sqlmesh.core.console import Console from sqlmesh.core.dialect import schema_ -from sqlmesh.utils.pydantic import PydanticModel -from sqlmesh.core.environment import Environment, EnvironmentStatements +from sqlmesh.utils.pydantic import PydanticModel, field_validator +from sqlmesh.core.environment import Environment, EnvironmentStatements, EnvironmentNamingInfo from sqlmesh.utils.errors import SQLMeshError -from sqlmesh.core.snapshot import Snapshot +from sqlmesh.core.snapshot import ( + Snapshot, + SnapshotEvaluator, + SnapshotId, + SnapshotTableCleanupTask, + SnapshotTableInfo, +) if t.TYPE_CHECKING: from sqlmesh.core.engine_adapter.base import EngineAdapter - from sqlmesh.core.state_sync.base import Versions + from sqlmesh.core.state_sync.base import Versions, StateReader, StateSync logger = logging.getLogger(__name__) +EXPIRED_SNAPSHOT_DEFAULT_BATCH_SIZE = 200 + def cleanup_expired_views( default_adapter: EngineAdapter, @@ -215,3 +225,265 @@ def __iter__(self) -> t.Iterator[StateStreamContents]: yield EnvironmentsChunk(environments) return _StateStream() + + +class ExpiredBatchRange(PydanticModel): + start: RowBoundary + end: t.Union[RowBoundary, LimitBoundary] + + @classmethod + def init_batch_range(cls, batch_size: int) -> ExpiredBatchRange: + return ExpiredBatchRange( + start=RowBoundary.lowest_boundary(), + end=LimitBoundary(batch_size=batch_size), + ) + + @classmethod + def all_batch_range(cls) -> ExpiredBatchRange: + return ExpiredBatchRange( + start=RowBoundary.lowest_boundary(), + end=RowBoundary.highest_boundary(), + ) + + @classmethod + def _expanded_tuple_comparison( + cls, + columns: t.List[exp.Column], + values: t.List[exp.Literal], + operator: t.Type[exp.Expression], + ) -> exp.Expression: + """Generate expanded tuple comparison that works across all SQL engines. + + Converts tuple comparisons like (a, b, c) OP (x, y, z) into an expanded form + that's compatible with all SQL engines, since native tuple comparisons have + inconsistent support across engines (especially DuckDB, MySQL, SQLite). + + Repro of problem with DuckDB: + "SELECT * FROM VALUES(1,'2') as test(a,b) WHERE ((a, b) > (1, 'foo')) AND ((a, b) <= (10, 'baz'))" + + Args: + columns: List of column expressions to compare + values: List of value expressions to compare against + operator: The comparison operator class (exp.GT, exp.GTE, exp.LT, exp.LTE) + + Examples: + (a, b, c) > (x, y, z) expands to: + a > x OR (a = x AND b > y) OR (a = x AND b = y AND c > z) + + (a, b, c) <= (x, y, z) expands to: + a < x OR (a = x AND b < y) OR (a = x AND b = y AND c <= z) + + (a, b, c) >= (x, y, z) expands to: + a > x OR (a = x AND b > y) OR (a = x AND b = y AND c >= z) + + Returns: + An expanded OR expression representing the tuple comparison + """ + if operator not in (exp.GT, exp.GTE, exp.LT, exp.LTE): + raise ValueError(f"Unsupported operator: {operator}. Use GT, GTE, LT, or LTE.") + + # For <= and >=, we use the strict operator for all but the last column + # e.g., (a, b) <= (x, y) becomes: a < x OR (a = x AND b <= y) + # For < and >, we use the strict operator throughout + # e.g., (a, b) > (x, y) becomes: a > x OR (a = x AND b > x) + strict_operator: t.Type[exp.Expression] + final_operator: t.Type[exp.Expression] + + if operator in (exp.LTE, exp.GTE): + # For inclusive operators (<=, >=), use strict form for intermediate columns + # but keep inclusive form for the last column + strict_operator = exp.LT if operator == exp.LTE else exp.GT + final_operator = operator # Keep LTE/GTE for last column + else: + # For strict operators (<, >), use them throughout + strict_operator = operator + final_operator = operator + + conditions: t.List[exp.Expression] = [] + for i in range(len(columns)): + # Build equality conditions for all columns before current + equality_conditions = [exp.EQ(this=columns[j], expression=values[j]) for j in range(i)] + + # Use the final operator for the last column, strict for others + comparison_op = final_operator if i == len(columns) - 1 else strict_operator + comparison_condition = comparison_op(this=columns[i], expression=values[i]) + + if equality_conditions: + conditions.append(exp.and_(*equality_conditions, comparison_condition)) + else: + conditions.append(comparison_condition) + + return exp.or_(*conditions) if len(conditions) > 1 else conditions[0] + + @property + def where_filter(self) -> exp.Expression: + # Use expanded tuple comparisons for cross-engine compatibility + # Native tuple comparisons like (a, b) > (x, y) don't work reliably across all SQL engines + columns = [ + exp.column("updated_ts"), + exp.column("name"), + exp.column("identifier"), + ] + start_values = [ + exp.Literal.number(self.start.updated_ts), + exp.Literal.string(self.start.name), + exp.Literal.string(self.start.identifier), + ] + + start_condition = self._expanded_tuple_comparison(columns, start_values, exp.GT) + + range_filter: exp.Expression + if isinstance(self.end, RowBoundary): + end_values = [ + exp.Literal.number(self.end.updated_ts), + exp.Literal.string(self.end.name), + exp.Literal.string(self.end.identifier), + ] + end_condition = self._expanded_tuple_comparison(columns, end_values, exp.LTE) + range_filter = exp.and_(start_condition, end_condition) + else: + range_filter = start_condition + return range_filter + + +class RowBoundary(PydanticModel): + updated_ts: int + name: str + identifier: str + + @classmethod + def lowest_boundary(cls) -> RowBoundary: + return RowBoundary(updated_ts=0, name="", identifier="") + + @classmethod + def highest_boundary(cls) -> RowBoundary: + # 9999-12-31T23:59:59.999Z in epoch milliseconds + return RowBoundary(updated_ts=253_402_300_799_999, name="", identifier="") + + +class LimitBoundary(PydanticModel): + batch_size: int + + @classmethod + def init_batch_boundary(cls, batch_size: int) -> LimitBoundary: + return LimitBoundary(batch_size=batch_size) + + +class PromotionResult(PydanticModel): + added: t.List[SnapshotTableInfo] + removed: t.List[SnapshotTableInfo] + removed_environment_naming_info: t.Optional[EnvironmentNamingInfo] + + @field_validator("removed_environment_naming_info") + def _validate_removed_environment_naming_info( + cls, v: t.Optional[EnvironmentNamingInfo], info: ValidationInfo + ) -> t.Optional[EnvironmentNamingInfo]: + if v and not info.data.get("removed"): + raise ValueError("removed_environment_naming_info must be None if removed is empty") + return v + + +class ExpiredSnapshotBatch(PydanticModel): + """A batch of expired snapshots to be cleaned up.""" + + expired_snapshot_ids: t.Set[SnapshotId] + cleanup_tasks: t.List[SnapshotTableCleanupTask] + batch_range: ExpiredBatchRange + + +def iter_expired_snapshot_batches( + state_reader: StateReader, + *, + current_ts: int, + ignore_ttl: bool = False, + batch_size: t.Optional[int] = None, +) -> t.Iterator[ExpiredSnapshotBatch]: + """Yields expired snapshot batches. + + Args: + state_reader: StateReader instance to query expired snapshots from. + current_ts: Timestamp used to evaluate expiration. + ignore_ttl: If True, include snapshots regardless of TTL (only checks if unreferenced). + batch_size: Maximum number of snapshots to fetch per batch. + """ + + batch_size = batch_size if batch_size is not None else EXPIRED_SNAPSHOT_DEFAULT_BATCH_SIZE + batch_range = ExpiredBatchRange.init_batch_range(batch_size=batch_size) + + while True: + batch = state_reader.get_expired_snapshots( + current_ts=current_ts, + ignore_ttl=ignore_ttl, + batch_range=batch_range, + ) + + if batch is None: + return + + yield batch + + assert isinstance(batch.batch_range.end, RowBoundary), ( + "Only RowBoundary is supported for pagination currently" + ) + batch_range = ExpiredBatchRange( + start=batch.batch_range.end, + end=LimitBoundary(batch_size=batch_size), + ) + + +def delete_expired_snapshots( + state_sync: StateSync, + snapshot_evaluator: SnapshotEvaluator, + *, + current_ts: int, + ignore_ttl: bool = False, + batch_size: t.Optional[int] = None, + console: t.Optional[Console] = None, +) -> None: + """Delete all expired snapshots in batches. + + This helper function encapsulates the logic for deleting expired snapshots in batches, + eliminating code duplication across different use cases. + + Args: + state_sync: StateSync instance to query and delete expired snapshots from. + snapshot_evaluator: SnapshotEvaluator instance to clean up tables associated with snapshots. + current_ts: Timestamp used to evaluate expiration. + ignore_ttl: If True, include snapshots regardless of TTL (only checks if unreferenced). + batch_size: Maximum number of snapshots to fetch per batch. + console: Optional console for reporting progress. + + Returns: + The total number of deleted expired snapshots. + """ + num_expired_snapshots = 0 + for batch in iter_expired_snapshot_batches( + state_reader=state_sync, + current_ts=current_ts, + ignore_ttl=ignore_ttl, + batch_size=batch_size, + ): + end_info = ( + f"updated_ts={batch.batch_range.end.updated_ts}" + if isinstance(batch.batch_range.end, RowBoundary) + else f"limit={batch.batch_range.end.batch_size}" + ) + logger.info( + "Processing batch of size %s with end %s", + len(batch.expired_snapshot_ids), + end_info, + ) + snapshot_evaluator.cleanup( + target_snapshots=batch.cleanup_tasks, + on_complete=console.update_cleanup_progress if console else None, + ) + state_sync.delete_expired_snapshots( + batch_range=ExpiredBatchRange( + start=RowBoundary.lowest_boundary(), + end=batch.batch_range.end, + ), + ignore_ttl=ignore_ttl, + ) + logger.info("Cleaned up expired snapshots batch") + num_expired_snapshots += len(batch.expired_snapshot_ids) + logger.info("Cleaned up %s expired snapshots", num_expired_snapshots) diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 3c23ef339c..49f7b5b92f 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -35,7 +35,6 @@ SnapshotInfoLike, SnapshotIntervals, SnapshotNameVersion, - SnapshotTableCleanupTask, SnapshotTableInfo, start_date, ) @@ -43,7 +42,6 @@ Interval, ) from sqlmesh.core.state_sync.base import ( - PromotionResult, StateSync, Versions, ) @@ -55,6 +53,9 @@ StateStream, chunk_iterable, EnvironmentWithStatements, + ExpiredSnapshotBatch, + PromotionResult, + ExpiredBatchRange, ) from sqlmesh.core.state_sync.db.interval import IntervalState from sqlmesh.core.state_sync.db.environment import EnvironmentState @@ -261,11 +262,18 @@ def invalidate_environment(self, name: str, protect_prod: bool = True) -> None: self.environment_state.invalidate_environment(name, protect_prod) def get_expired_snapshots( - self, current_ts: t.Optional[int] = None, ignore_ttl: bool = False - ) -> t.List[SnapshotTableCleanupTask]: + self, + *, + batch_range: ExpiredBatchRange, + current_ts: t.Optional[int] = None, + ignore_ttl: bool = False, + ) -> t.Optional[ExpiredSnapshotBatch]: current_ts = current_ts or now_timestamp() return self.snapshot_state.get_expired_snapshots( - self.environment_state.get_environments(), current_ts=current_ts, ignore_ttl=ignore_ttl + environments=self.environment_state.get_environments(), + current_ts=current_ts, + ignore_ttl=ignore_ttl, + batch_range=batch_range, ) def get_expired_environments(self, current_ts: int) -> t.List[EnvironmentSummary]: @@ -273,14 +281,19 @@ def get_expired_environments(self, current_ts: int) -> t.List[EnvironmentSummary @transactional() def delete_expired_snapshots( - self, ignore_ttl: bool = False, current_ts: t.Optional[int] = None + self, + batch_range: ExpiredBatchRange, + ignore_ttl: bool = False, + current_ts: t.Optional[int] = None, ) -> None: - current_ts = current_ts or now_timestamp() - for expired_snapshot_ids, cleanup_targets in self.snapshot_state._get_expired_snapshots( - self.environment_state.get_environments(), ignore_ttl=ignore_ttl, current_ts=current_ts - ): - self.snapshot_state.delete_snapshots(expired_snapshot_ids) - self.interval_state.cleanup_intervals(cleanup_targets, expired_snapshot_ids) + batch = self.get_expired_snapshots( + ignore_ttl=ignore_ttl, + current_ts=current_ts, + batch_range=batch_range, + ) + if batch and batch.expired_snapshot_ids: + self.snapshot_state.delete_snapshots(batch.expired_snapshot_ids) + self.interval_state.cleanup_intervals(batch.cleanup_tasks, batch.expired_snapshot_ids) @transactional() def delete_expired_environments( diff --git a/sqlmesh/core/state_sync/db/snapshot.py b/sqlmesh/core/state_sync/db/snapshot.py index 4a8b2c44c5..4565990d65 100644 --- a/sqlmesh/core/state_sync/db/snapshot.py +++ b/sqlmesh/core/state_sync/db/snapshot.py @@ -14,7 +14,6 @@ snapshot_id_filter, fetchone, fetchall, - create_batches, ) from sqlmesh.core.environment import Environment from sqlmesh.core.model import SeedModel, ModelKindName @@ -30,6 +29,12 @@ SnapshotId, SnapshotFingerprint, ) +from sqlmesh.core.state_sync.common import ( + RowBoundary, + ExpiredSnapshotBatch, + ExpiredBatchRange, + LimitBoundary, +) from sqlmesh.utils.migration import index_text_type, blob_text_type from sqlmesh.utils.date import now_timestamp, TimeLike, to_timestamp from sqlmesh.utils import unique @@ -43,9 +48,6 @@ class SnapshotState: SNAPSHOT_BATCH_SIZE = 1000 - # Use a smaller batch size for expired snapshots to account for fetching - # of all snapshots that share the same version. - EXPIRED_SNAPSHOT_BATCH_SIZE = 200 def __init__( self, @@ -166,47 +168,19 @@ def get_expired_snapshots( self, environments: t.Iterable[Environment], current_ts: int, - ignore_ttl: bool = False, - ) -> t.List[SnapshotTableCleanupTask]: - """Aggregates the id's of the expired snapshots and creates a list of table cleanup tasks. - - Expired snapshots are snapshots that have exceeded their time-to-live - and are no longer in use within an environment. - - Returns: - The set of expired snapshot ids. - The list of table cleanup tasks. - """ - all_cleanup_targets = [] - for _, cleanup_targets in self._get_expired_snapshots( - environments=environments, - current_ts=current_ts, - ignore_ttl=ignore_ttl, - ): - all_cleanup_targets.extend(cleanup_targets) - return all_cleanup_targets - - def _get_expired_snapshots( - self, - environments: t.Iterable[Environment], - current_ts: int, - ignore_ttl: bool = False, - ) -> t.Iterator[t.Tuple[t.Set[SnapshotId], t.List[SnapshotTableCleanupTask]]]: - expired_query = exp.select("name", "identifier", "version").from_(self.snapshots_table) + ignore_ttl: bool, + batch_range: ExpiredBatchRange, + ) -> t.Optional[ExpiredSnapshotBatch]: + expired_query = exp.select("name", "identifier", "version", "updated_ts").from_( + self.snapshots_table + ) if not ignore_ttl: expired_query = expired_query.where( (exp.column("updated_ts") + exp.column("ttl_ms")) <= current_ts ) - expired_candidates = { - SnapshotId(name=name, identifier=identifier): SnapshotNameVersion( - name=name, version=version - ) - for name, identifier, version in fetchall(self.engine_adapter, expired_query) - } - if not expired_candidates: - return + expired_query = expired_query.where(batch_range.where_filter) promoted_snapshot_ids = { snapshot.snapshot_id @@ -214,63 +188,111 @@ def _get_expired_snapshots( for snapshot in environment.snapshots } + if promoted_snapshot_ids: + not_in_conditions = [ + exp.not_(condition) + for condition in snapshot_id_filter( + self.engine_adapter, + promoted_snapshot_ids, + batch_size=self.SNAPSHOT_BATCH_SIZE, + ) + ] + expired_query = expired_query.where(exp.and_(*not_in_conditions)) + + expired_query = expired_query.order_by( + exp.column("updated_ts"), exp.column("name"), exp.column("identifier") + ) + + if isinstance(batch_range.end, LimitBoundary): + expired_query = expired_query.limit(batch_range.end.batch_size) + + rows = fetchall(self.engine_adapter, expired_query) + + if not rows: + return None + + expired_candidates = { + SnapshotId(name=name, identifier=identifier): SnapshotNameVersion( + name=name, version=version + ) + for name, identifier, version, _ in rows + } + if not expired_candidates: + return None + def _is_snapshot_used(snapshot: SnapshotIdAndVersion) -> bool: return ( snapshot.snapshot_id in promoted_snapshot_ids or snapshot.snapshot_id not in expired_candidates ) - unique_expired_versions = unique(expired_candidates.values()) - version_batches = create_batches( - unique_expired_versions, batch_size=self.EXPIRED_SNAPSHOT_BATCH_SIZE + # Extract cursor values from last row for pagination + last_row = rows[-1] + last_row_boundary = RowBoundary( + updated_ts=last_row[3], + name=last_row[0], + identifier=last_row[1], ) - for versions_batch in version_batches: - snapshots = self._get_snapshots_with_same_version(versions_batch) - - snapshots_by_version = defaultdict(set) - snapshots_by_dev_version = defaultdict(set) - for s in snapshots: - snapshots_by_version[(s.name, s.version)].add(s.snapshot_id) - snapshots_by_dev_version[(s.name, s.dev_version)].add(s.snapshot_id) - - expired_snapshots = [s for s in snapshots if not _is_snapshot_used(s)] - all_expired_snapshot_ids = {s.snapshot_id for s in expired_snapshots} - - cleanup_targets: t.List[t.Tuple[SnapshotId, bool]] = [] - for snapshot in expired_snapshots: - shared_version_snapshots = snapshots_by_version[(snapshot.name, snapshot.version)] - shared_version_snapshots.discard(snapshot.snapshot_id) - - shared_dev_version_snapshots = snapshots_by_dev_version[ - (snapshot.name, snapshot.dev_version) - ] - shared_dev_version_snapshots.discard(snapshot.snapshot_id) - - if not shared_dev_version_snapshots: - dev_table_only = bool(shared_version_snapshots) - cleanup_targets.append((snapshot.snapshot_id, dev_table_only)) - - snapshot_ids_to_cleanup = [snapshot_id for snapshot_id, _ in cleanup_targets] - for snapshot_id_batch in create_batches( - snapshot_ids_to_cleanup, batch_size=self.SNAPSHOT_BATCH_SIZE - ): - snapshot_id_batch_set = set(snapshot_id_batch) - full_snapshots = self._get_snapshots(snapshot_id_batch_set) - cleanup_tasks = [ + # The returned batch_range represents the actual range of rows in this batch + result_batch_range = ExpiredBatchRange( + start=batch_range.start, + end=last_row_boundary, + ) + + unique_expired_versions = unique(expired_candidates.values()) + expired_snapshot_ids: t.Set[SnapshotId] = set() + cleanup_tasks: t.List[SnapshotTableCleanupTask] = [] + + snapshots = self._get_snapshots_with_same_version(unique_expired_versions) + + snapshots_by_version = defaultdict(set) + snapshots_by_dev_version = defaultdict(set) + for s in snapshots: + snapshots_by_version[(s.name, s.version)].add(s.snapshot_id) + snapshots_by_dev_version[(s.name, s.dev_version)].add(s.snapshot_id) + + expired_snapshots = [s for s in snapshots if not _is_snapshot_used(s)] + all_expired_snapshot_ids = {s.snapshot_id for s in expired_snapshots} + + cleanup_targets: t.List[t.Tuple[SnapshotId, bool]] = [] + for snapshot in expired_snapshots: + shared_version_snapshots = snapshots_by_version[(snapshot.name, snapshot.version)] + shared_version_snapshots.discard(snapshot.snapshot_id) + + shared_dev_version_snapshots = snapshots_by_dev_version[ + (snapshot.name, snapshot.dev_version) + ] + shared_dev_version_snapshots.discard(snapshot.snapshot_id) + + if not shared_dev_version_snapshots: + dev_table_only = bool(shared_version_snapshots) + cleanup_targets.append((snapshot.snapshot_id, dev_table_only)) + + snapshot_ids_to_cleanup = [snapshot_id for snapshot_id, _ in cleanup_targets] + full_snapshots = self._get_snapshots(snapshot_ids_to_cleanup) + for snapshot_id, dev_table_only in cleanup_targets: + if snapshot_id in full_snapshots: + cleanup_tasks.append( SnapshotTableCleanupTask( snapshot=full_snapshots[snapshot_id].table_info, dev_table_only=dev_table_only, ) - for snapshot_id, dev_table_only in cleanup_targets - if snapshot_id in full_snapshots - ] - all_expired_snapshot_ids -= snapshot_id_batch_set - yield snapshot_id_batch_set, cleanup_tasks - - if all_expired_snapshot_ids: - # Remaining expired snapshots for which there are no tables - # to cleanup - yield all_expired_snapshot_ids, [] + ) + expired_snapshot_ids.add(snapshot_id) + all_expired_snapshot_ids.discard(snapshot_id) + + # Add any remaining expired snapshots that don't require cleanup + if all_expired_snapshot_ids: + expired_snapshot_ids.update(all_expired_snapshot_ids) + + if expired_snapshot_ids or cleanup_tasks: + return ExpiredSnapshotBatch( + expired_snapshot_ids=expired_snapshot_ids, + cleanup_tasks=cleanup_tasks, + batch_range=result_batch_range, + ) + + return None def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None: """Deletes snapshots. diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index 51a646ce5d..199ca43ee9 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -43,15 +43,33 @@ from sqlmesh.core.state_sync.base import ( SCHEMA_VERSION, SQLGLOT_VERSION, - PromotionResult, Versions, ) +from sqlmesh.core.state_sync.common import ( + ExpiredBatchRange, + LimitBoundary, + PromotionResult, + RowBoundary, +) from sqlmesh.utils.date import now_timestamp, to_datetime, to_timestamp from sqlmesh.utils.errors import SQLMeshError, StateMigrationError pytestmark = pytest.mark.slow +def _get_cleanup_tasks( + state_sync: EngineAdapterStateSync, + *, + limit: int = 1000, + ignore_ttl: bool = False, +) -> t.List[SnapshotTableCleanupTask]: + batch = state_sync.get_expired_snapshots( + ignore_ttl=ignore_ttl, + batch_range=ExpiredBatchRange.init_batch_range(batch_size=limit), + ) + return [] if batch is None else batch.cleanup_tasks + + @pytest.fixture def state_sync(duck_conn, tmp_path): state_sync = EngineAdapterStateSync( @@ -1156,15 +1174,504 @@ def test_delete_expired_snapshots(state_sync: EngineAdapterStateSync, make_snaps new_snapshot.snapshot_id, } - assert state_sync.get_expired_snapshots() == [ + assert _get_cleanup_tasks(state_sync) == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True), SnapshotTableCleanupTask(snapshot=new_snapshot.table_info, dev_table_only=False), ] - state_sync.delete_expired_snapshots() + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert not state_sync.get_snapshots(all_snapshots) +def test_get_expired_snapshot_batch(state_sync: EngineAdapterStateSync, make_snapshot: t.Callable): + now_ts = now_timestamp() + + snapshots = [] + for idx in range(3): + snapshot = make_snapshot( + SqlModel( + name=f"model_{idx}", + query=parse_one("select 1 as a, ds"), + ), + ) + snapshot.ttl = "in 10 seconds" + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot.updated_ts = now_ts - (20000 + idx * 1000) + snapshots.append(snapshot) + + state_sync.push_snapshots(snapshots) + + batch = state_sync.get_expired_snapshots( + batch_range=ExpiredBatchRange.init_batch_range(batch_size=2), + ) + assert batch is not None + assert len(batch.expired_snapshot_ids) == 2 + assert len(batch.cleanup_tasks) == 2 + + state_sync.delete_expired_snapshots( + batch_range=ExpiredBatchRange( + start=RowBoundary.lowest_boundary(), + end=batch.batch_range.end, + ), + ) + + next_batch = state_sync.get_expired_snapshots( + batch_range=ExpiredBatchRange( + start=batch.batch_range.end, + end=LimitBoundary(batch_size=2), + ), + ) + assert next_batch is not None + assert len(next_batch.expired_snapshot_ids) == 1 + + state_sync.delete_expired_snapshots( + batch_range=ExpiredBatchRange( + start=next_batch.batch_range.start, + end=next_batch.batch_range.end, + ), + ) + + assert ( + state_sync.get_expired_snapshots( + batch_range=ExpiredBatchRange( + start=next_batch.batch_range.end, + end=LimitBoundary(batch_size=2), + ), + ) + is None + ) + + +def test_get_expired_snapshot_batch_same_timestamp( + state_sync: EngineAdapterStateSync, make_snapshot: t.Callable +): + """Test that pagination works correctly when multiple snapshots have the same updated_ts.""" + now_ts = now_timestamp() + same_timestamp = now_ts - 20000 + + snapshots = [] + for idx in range(5): + snapshot = make_snapshot( + SqlModel( + name=f"model_{idx:02d}", # Zero-padded to ensure deterministic name ordering + query=parse_one("select 1 as a, ds"), + ), + ) + snapshot.ttl = "in 10 seconds" + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + # All snapshots have the same updated_ts + snapshot.updated_ts = same_timestamp + snapshots.append(snapshot) + + state_sync.push_snapshots(snapshots) + + # Fetch first batch of 2 + batch1 = state_sync.get_expired_snapshots( + batch_range=ExpiredBatchRange.init_batch_range(batch_size=2), + ) + assert batch1 is not None + assert len(batch1.expired_snapshot_ids) == 2 + assert sorted([x.name for x in batch1.expired_snapshot_ids]) == [ + '"model_00"', + '"model_01"', + ] + + # Fetch second batch of 2 using cursor from batch1 + batch2 = state_sync.get_expired_snapshots( + batch_range=ExpiredBatchRange( + start=batch1.batch_range.end, + end=LimitBoundary(batch_size=2), + ), + ) + assert batch2 is not None + assert len(batch2.expired_snapshot_ids) == 2 + assert sorted([x.name for x in batch2.expired_snapshot_ids]) == [ + '"model_02"', + '"model_03"', + ] + + # Fetch third batch of 2 using cursor from batch2 + batch3 = state_sync.get_expired_snapshots( + batch_range=ExpiredBatchRange( + start=batch2.batch_range.end, + end=LimitBoundary(batch_size=2), + ), + ) + assert batch3 is not None + assert sorted([x.name for x in batch3.expired_snapshot_ids]) == [ + '"model_04"', + ] + + +def test_delete_expired_snapshots_batching_with_deletion( + state_sync: EngineAdapterStateSync, make_snapshot: t.Callable +): + """Test that delete_expired_snapshots properly deletes batches as it pages through them.""" + now_ts = now_timestamp() + + # Create 5 expired snapshots with different timestamps + snapshots = [] + for idx in range(5): + snapshot = make_snapshot( + SqlModel( + name=f"model_{idx}", + query=parse_one("select 1 as a, ds"), + ), + ) + snapshot.ttl = "in 10 seconds" + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot.updated_ts = now_ts - (20000 + idx * 1000) + snapshots.append(snapshot) + + state_sync.push_snapshots(snapshots) + + # Verify all 5 snapshots exist + assert len(state_sync.get_snapshots(snapshots)) == 5 + + # Get first batch of 2 + batch1 = state_sync.get_expired_snapshots( + batch_range=ExpiredBatchRange.init_batch_range(batch_size=2), + ) + assert batch1 is not None + assert len(batch1.expired_snapshot_ids) == 2 + + # Delete the first batch using batch_range + state_sync.delete_expired_snapshots( + batch_range=ExpiredBatchRange( + start=batch1.batch_range.start, + end=batch1.batch_range.end, + ), + ) + + # Verify first 2 snapshots (model_0 and model_1, the oldest) are deleted and last 3 remain + remaining = state_sync.get_snapshots(snapshots) + assert len(remaining) == 3 + assert snapshots[0].snapshot_id in remaining # model_0 (newest) + assert snapshots[1].snapshot_id in remaining # model_1 + assert snapshots[2].snapshot_id in remaining # model_2 + assert snapshots[3].snapshot_id not in remaining # model_3 + assert snapshots[4].snapshot_id not in remaining # model_4 (oldest) + + # Get next batch of 2 (should start after batch1's boundary) + batch2 = state_sync.get_expired_snapshots( + batch_range=ExpiredBatchRange( + start=batch1.batch_range.end, + end=LimitBoundary(batch_size=2), + ), + ) + assert batch2 is not None + assert len(batch2.expired_snapshot_ids) == 2 + + # Delete the second batch + state_sync.delete_expired_snapshots( + batch_range=ExpiredBatchRange( + start=batch2.batch_range.start, + end=batch2.batch_range.end, + ), + ) + + # Verify only the last snapshot remains + remaining = state_sync.get_snapshots(snapshots) + assert len(remaining) == 1 + assert snapshots[0].snapshot_id in remaining # model_0 (newest) + assert snapshots[1].snapshot_id not in remaining # model_1 + assert snapshots[2].snapshot_id not in remaining # model_2 + assert snapshots[3].snapshot_id not in remaining # model_3 + assert snapshots[4].snapshot_id not in remaining # model_4 (oldest) + + # Get final batch + batch3 = state_sync.get_expired_snapshots( + batch_range=ExpiredBatchRange( + start=batch2.batch_range.end, + end=LimitBoundary(batch_size=2), + ), + ) + assert batch3 is not None + assert len(batch3.expired_snapshot_ids) == 1 + + # Delete the final batch + state_sync.delete_expired_snapshots( + batch_range=ExpiredBatchRange( + start=batch3.batch_range.start, + end=batch3.batch_range.end, + ), + ) + + # Verify all snapshots are deleted + assert len(state_sync.get_snapshots(snapshots)) == 0 + + # Verify no more expired snapshots exist + assert ( + state_sync.get_expired_snapshots( + batch_range=ExpiredBatchRange( + start=batch3.batch_range.end, + end=LimitBoundary(batch_size=2), + ), + ) + is None + ) + + +def test_iterator_expired_snapshot_batch( + state_sync: EngineAdapterStateSync, make_snapshot: t.Callable +): + """Test the for_each_expired_snapshot_batch helper function.""" + from sqlmesh.core.state_sync.common import iter_expired_snapshot_batches + + now_ts = now_timestamp() + + snapshots = [] + for idx in range(5): + snapshot = make_snapshot( + SqlModel( + name=f"model_{idx}", + query=parse_one("select 1 as a, ds"), + ), + ) + snapshot.ttl = "in 10 seconds" + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot.updated_ts = now_ts - (20000 + idx * 1000) + snapshots.append(snapshot) + + state_sync.push_snapshots(snapshots) + + # Track all batches processed + batches_processed = [] + + # Process with batch size of 2 + for batch in iter_expired_snapshot_batches( + state_sync, + current_ts=now_ts, + ignore_ttl=False, + batch_size=2, + ): + batches_processed.append(batch) + + # Should have processed 3 batches (2 + 2 + 1) + assert len(batches_processed) == 3 + assert len(batches_processed[0].expired_snapshot_ids) == 2 + assert len(batches_processed[1].expired_snapshot_ids) == 2 + assert len(batches_processed[2].expired_snapshot_ids) == 1 + + # Verify all snapshots were processed + all_processed_ids = set() + for batch in batches_processed: + all_processed_ids.update(batch.expired_snapshot_ids) + + expected_ids = {s.snapshot_id for s in snapshots} + assert all_processed_ids == expected_ids + + +@pytest.mark.parametrize( + "start_boundary,end_boundary,expected_sql", + [ + # Test with GT only (when end is LimitBoundary) + ( + RowBoundary(updated_ts=0, name="", identifier=""), + LimitBoundary(batch_size=100), + "updated_ts > 0 OR (updated_ts = 0 AND name > '') OR (updated_ts = 0 AND name = '' AND identifier > '')", + ), + # Test with GT and LTE (when both are RowBoundary) + ( + RowBoundary(updated_ts=1000, name="model_a", identifier="abc"), + RowBoundary(updated_ts=2000, name="model_z", identifier="xyz"), + "(updated_ts > 1000 OR (updated_ts = 1000 AND name > 'model_a') OR (updated_ts = 1000 AND name = 'model_a' AND identifier > 'abc')) AND (updated_ts < 2000 OR (updated_ts = 2000 AND name < 'model_z') OR (updated_ts = 2000 AND name = 'model_z' AND identifier <= 'xyz'))", + ), + # Test with zero timestamp + ( + RowBoundary(updated_ts=0, name="", identifier=""), + RowBoundary(updated_ts=1234567890, name="model_x", identifier="id_123"), + "(updated_ts > 0 OR (updated_ts = 0 AND name > '') OR (updated_ts = 0 AND name = '' AND identifier > '')) AND (updated_ts < 1234567890 OR (updated_ts = 1234567890 AND name < 'model_x') OR (updated_ts = 1234567890 AND name = 'model_x' AND identifier <= 'id_123'))", + ), + # Test with same timestamp, different names + ( + RowBoundary(updated_ts=5000, name="model_a", identifier="id_1"), + RowBoundary(updated_ts=5000, name="model_b", identifier="id_2"), + "(updated_ts > 5000 OR (updated_ts = 5000 AND name > 'model_a') OR (updated_ts = 5000 AND name = 'model_a' AND identifier > 'id_1')) AND (updated_ts < 5000 OR (updated_ts = 5000 AND name < 'model_b') OR (updated_ts = 5000 AND name = 'model_b' AND identifier <= 'id_2'))", + ), + # Test with same timestamp and name, different identifiers + ( + RowBoundary(updated_ts=7000, name="model_x", identifier="id_a"), + RowBoundary(updated_ts=7000, name="model_x", identifier="id_b"), + "(updated_ts > 7000 OR (updated_ts = 7000 AND name > 'model_x') OR (updated_ts = 7000 AND name = 'model_x' AND identifier > 'id_a')) AND (updated_ts < 7000 OR (updated_ts = 7000 AND name < 'model_x') OR (updated_ts = 7000 AND name = 'model_x' AND identifier <= 'id_b'))", + ), + # Test all_batch_range use case + ( + RowBoundary(updated_ts=0, name="", identifier=""), + RowBoundary(updated_ts=253_402_300_799_999, name="", identifier=""), + "(updated_ts > 0 OR (updated_ts = 0 AND name > '') OR (updated_ts = 0 AND name = '' AND identifier > '')) AND (updated_ts < 253402300799999 OR (updated_ts = 253402300799999 AND name < '') OR (updated_ts = 253402300799999 AND name = '' AND identifier <= ''))", + ), + ], +) +def test_expired_batch_range_where_filter(start_boundary, end_boundary, expected_sql): + """Test ExpiredBatchRange.where_filter generates correct SQL for various boundary combinations.""" + batch_range = ExpiredBatchRange(start=start_boundary, end=end_boundary) + result = batch_range.where_filter + assert result.sql() == expected_sql + + +def test_expired_batch_range_where_filter_with_limit(): + """Test that where_filter correctly handles LimitBoundary (only start condition, no end condition).""" + batch_range = ExpiredBatchRange( + start=RowBoundary(updated_ts=1000, name="model_a", identifier="abc"), + end=LimitBoundary(batch_size=50), + ) + result = batch_range.where_filter + # When end is LimitBoundary, should only have the start (GT) condition + assert ( + result.sql() + == "updated_ts > 1000 OR (updated_ts = 1000 AND name > 'model_a') OR (updated_ts = 1000 AND name = 'model_a' AND identifier > 'abc')" + ) + + +def test_delete_expired_snapshots_common_function_batching( + state_sync: EngineAdapterStateSync, make_snapshot: t.Callable, mocker: MockerFixture +): + """Test that the common delete_expired_snapshots function properly pages through batches and deletes them.""" + from sqlmesh.core.state_sync.common import delete_expired_snapshots + from sqlmesh.core.state_sync.common import ExpiredBatchRange, RowBoundary, LimitBoundary + from unittest.mock import MagicMock + + now_ts = now_timestamp() + + # Create 5 expired snapshots with different timestamps + snapshots = [] + for idx in range(5): + snapshot = make_snapshot( + SqlModel( + name=f"model_{idx}", + query=parse_one("select 1 as a, ds"), + ), + ) + snapshot.ttl = "in 10 seconds" + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot.updated_ts = now_ts - (20000 + idx * 1000) + snapshots.append(snapshot) + + state_sync.push_snapshots(snapshots) + + # Spy on get_expired_snapshots and delete_expired_snapshots methods + get_expired_spy = mocker.spy(state_sync, "get_expired_snapshots") + delete_expired_spy = mocker.spy(state_sync, "delete_expired_snapshots") + + # Mock snapshot evaluator + mock_evaluator = MagicMock() + mock_evaluator.cleanup = MagicMock() + + # Run delete_expired_snapshots with batch_size=2 + delete_expired_snapshots( + state_sync, + mock_evaluator, + current_ts=now_ts, + batch_size=2, + ) + + # Verify get_expired_snapshots was called the correct number of times: + # - 3 batches (2+2+1): each batch triggers 2 calls (one from iter_expired_snapshot_batches, one from delete_expired_snapshots) + # - Plus 1 final call that returns empty to exit the loop + # Total: 3 * 2 + 1 = 7 calls + assert get_expired_spy.call_count == 7 + + # Verify the progression of batch_range calls from the iter_expired_snapshot_batches loop + # (calls at indices 0, 2, 4, 6 are from iter_expired_snapshot_batches) + # (calls at indices 1, 3, 5 are from delete_expired_snapshots in facade.py) + calls = get_expired_spy.call_args_list + + # First call from iterator should have a batch_range starting from the beginning + first_call_kwargs = calls[0][1] + assert "batch_range" in first_call_kwargs + first_range = first_call_kwargs["batch_range"] + assert isinstance(first_range, ExpiredBatchRange) + assert isinstance(first_range.start, RowBoundary) + assert isinstance(first_range.end, LimitBoundary) + assert first_range.end.batch_size == 2 + assert first_range.start.updated_ts == 0 + assert first_range.start.name == "" + assert first_range.start.identifier == "" + + # Third call (second batch from iterator) should have a batch_range from the first batch's range + third_call_kwargs = calls[2][1] + assert "batch_range" in third_call_kwargs + second_range = third_call_kwargs["batch_range"] + assert isinstance(second_range, ExpiredBatchRange) + assert isinstance(second_range.start, RowBoundary) + assert isinstance(second_range.end, LimitBoundary) + assert second_range.end.batch_size == 2 + # Should have progressed from the first batch + assert second_range.start.updated_ts > 0 + assert second_range.start.name == '"model_3"' + + # Fifth call (third batch from iterator) should have a batch_range from the second batch's range + fifth_call_kwargs = calls[4][1] + assert "batch_range" in fifth_call_kwargs + third_range = fifth_call_kwargs["batch_range"] + assert isinstance(third_range, ExpiredBatchRange) + assert isinstance(third_range.start, RowBoundary) + assert isinstance(third_range.end, LimitBoundary) + assert third_range.end.batch_size == 2 + # Should have progressed from the second batch + assert third_range.start.updated_ts >= second_range.start.updated_ts + assert third_range.start.name == '"model_1"' + + # Seventh call (final call from iterator) should have a batch_range from the third batch's range + seventh_call_kwargs = calls[6][1] + assert "batch_range" in seventh_call_kwargs + fourth_range = seventh_call_kwargs["batch_range"] + assert isinstance(fourth_range, ExpiredBatchRange) + assert isinstance(fourth_range.start, RowBoundary) + assert isinstance(fourth_range.end, LimitBoundary) + assert fourth_range.end.batch_size == 2 + # Should have progressed from the third batch + assert fourth_range.start.updated_ts >= third_range.start.updated_ts + assert fourth_range.start.name == '"model_0"' + + # Verify delete_expired_snapshots was called 3 times (once per batch) + assert delete_expired_spy.call_count == 3 + + # Verify each delete call used a batch_range + delete_calls = delete_expired_spy.call_args_list + + # First call should have a batch_range matching the first batch + first_delete_kwargs = delete_calls[0][1] + assert "batch_range" in first_delete_kwargs + first_delete_range = first_delete_kwargs["batch_range"] + assert isinstance(first_delete_range, ExpiredBatchRange) + assert isinstance(first_delete_range.start, RowBoundary) + assert first_delete_range.start.updated_ts == 0 + assert isinstance(first_delete_range.end, RowBoundary) + assert first_delete_range.end.updated_ts == second_range.start.updated_ts + assert first_delete_range.end.name == second_range.start.name + assert first_delete_range.end.identifier == second_range.start.identifier + + second_delete_kwargs = delete_calls[1][1] + assert "batch_range" in second_delete_kwargs + second_delete_range = second_delete_kwargs["batch_range"] + assert isinstance(second_delete_range, ExpiredBatchRange) + assert isinstance(second_delete_range.start, RowBoundary) + assert second_delete_range.start.updated_ts == 0 + assert isinstance(second_delete_range.end, RowBoundary) + assert second_delete_range.end.updated_ts == third_range.start.updated_ts + assert second_delete_range.end.name == third_range.start.name + assert second_delete_range.end.identifier == third_range.start.identifier + + third_delete_kwargs = delete_calls[2][1] + assert "batch_range" in third_delete_kwargs + third_delete_range = third_delete_kwargs["batch_range"] + assert isinstance(third_delete_range, ExpiredBatchRange) + assert isinstance(third_delete_range.start, RowBoundary) + assert third_delete_range.start.updated_ts == 0 + assert isinstance(third_delete_range.end, RowBoundary) + assert third_delete_range.end.updated_ts == fourth_range.start.updated_ts + assert third_delete_range.end.name == fourth_range.start.name + assert third_delete_range.end.identifier == fourth_range.start.identifier + # Verify the cleanup method was called for each batch that had cleanup tasks + assert mock_evaluator.cleanup.call_count >= 1 + + # Verify all snapshots were deleted in the end + remaining = state_sync.get_snapshots(snapshots) + assert len(remaining) == 0 + + def test_delete_expired_snapshots_seed( state_sync: EngineAdapterStateSync, make_snapshot: t.Callable ): @@ -1187,10 +1694,10 @@ def test_delete_expired_snapshots_seed( state_sync.push_snapshots(all_snapshots) assert set(state_sync.get_snapshots(all_snapshots)) == {snapshot.snapshot_id} - assert state_sync.get_expired_snapshots() == [ + assert _get_cleanup_tasks(state_sync) == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=False), ] - state_sync.delete_expired_snapshots() + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert not state_sync.get_snapshots(all_snapshots) @@ -1228,11 +1735,11 @@ def test_delete_expired_snapshots_batching( snapshot_b.snapshot_id, } - assert state_sync.get_expired_snapshots() == [ + assert _get_cleanup_tasks(state_sync) == [ SnapshotTableCleanupTask(snapshot=snapshot_a.table_info, dev_table_only=False), SnapshotTableCleanupTask(snapshot=snapshot_b.table_info, dev_table_only=False), ] - state_sync.delete_expired_snapshots() + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert not state_sync.get_snapshots(all_snapshots) @@ -1265,8 +1772,8 @@ def test_delete_expired_snapshots_promoted( state_sync.promote(env) all_snapshots = [snapshot] - assert not state_sync.get_expired_snapshots() - state_sync.delete_expired_snapshots() + assert not _get_cleanup_tasks(state_sync) + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert set(state_sync.get_snapshots(all_snapshots)) == {snapshot.snapshot_id} env.snapshots_ = [] @@ -1275,10 +1782,10 @@ def test_delete_expired_snapshots_promoted( now_timestamp_mock = mocker.patch("sqlmesh.core.state_sync.db.facade.now_timestamp") now_timestamp_mock.return_value = now_timestamp() + 11000 - assert state_sync.get_expired_snapshots() == [ + assert _get_cleanup_tasks(state_sync) == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=False) ] - state_sync.delete_expired_snapshots() + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert not state_sync.get_snapshots(all_snapshots) @@ -1315,10 +1822,10 @@ def test_delete_expired_snapshots_dev_table_cleanup_only( new_snapshot.snapshot_id, } - assert state_sync.get_expired_snapshots() == [ + assert _get_cleanup_tasks(state_sync) == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True) ] - state_sync.delete_expired_snapshots() + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert set(state_sync.get_snapshots(all_snapshots)) == {new_snapshot.snapshot_id} @@ -1357,8 +1864,8 @@ def test_delete_expired_snapshots_shared_dev_table( new_snapshot.snapshot_id, } - assert not state_sync.get_expired_snapshots() # No dev table cleanup - state_sync.delete_expired_snapshots() + assert not _get_cleanup_tasks(state_sync) # No dev table cleanup + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert set(state_sync.get_snapshots(all_snapshots)) == {new_snapshot.snapshot_id} @@ -1403,16 +1910,18 @@ def test_delete_expired_snapshots_ignore_ttl( state_sync.promote(env) # default TTL = 1 week, nothing to clean up yet if we take TTL into account - assert not state_sync.get_expired_snapshots() - state_sync.delete_expired_snapshots() + assert not _get_cleanup_tasks(state_sync) + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert state_sync.snapshots_exist([snapshot_c.snapshot_id]) == {snapshot_c.snapshot_id} # If we ignore TTL, only snapshot_c should get cleaned up because snapshot_a and snapshot_b are part of an environment assert snapshot_a.table_info != snapshot_b.table_info != snapshot_c.table_info - assert state_sync.get_expired_snapshots(ignore_ttl=True) == [ + assert _get_cleanup_tasks(state_sync, ignore_ttl=True) == [ SnapshotTableCleanupTask(snapshot=snapshot_c.table_info, dev_table_only=False) ] - state_sync.delete_expired_snapshots(ignore_ttl=True) + state_sync.delete_expired_snapshots( + batch_range=ExpiredBatchRange.all_batch_range(), ignore_ttl=True + ) assert not state_sync.snapshots_exist([snapshot_c.snapshot_id]) @@ -1476,11 +1985,11 @@ def test_delete_expired_snapshots_cleanup_intervals( ] assert not stored_new_snapshot.dev_intervals - assert state_sync.get_expired_snapshots() == [ + assert _get_cleanup_tasks(state_sync) == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True), SnapshotTableCleanupTask(snapshot=new_snapshot.table_info, dev_table_only=False), ] - state_sync.delete_expired_snapshots() + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert not get_snapshot_intervals(snapshot) @@ -1564,10 +2073,10 @@ def test_delete_expired_snapshots_cleanup_intervals_shared_version( ) # Delete the expired snapshot - assert state_sync.get_expired_snapshots() == [ + assert _get_cleanup_tasks(state_sync) == [ SnapshotTableCleanupTask(snapshot=snapshot.table_info, dev_table_only=True), ] - state_sync.delete_expired_snapshots() + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert not state_sync.get_snapshots([snapshot]) # Check new snapshot's intervals @@ -1684,8 +2193,8 @@ def test_delete_expired_snapshots_cleanup_intervals_shared_dev_version( ) # Delete the expired snapshot - assert state_sync.get_expired_snapshots() == [] - state_sync.delete_expired_snapshots() + assert not _get_cleanup_tasks(state_sync) + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert not state_sync.get_snapshots([snapshot]) # Check new snapshot's intervals @@ -1778,10 +2287,10 @@ def test_compact_intervals_after_cleanup( state_sync.add_interval(snapshot_c, "2023-01-07", "2023-01-09", is_dev=True) # Only the dev table of the original snapshot should be deleted - assert state_sync.get_expired_snapshots() == [ + assert _get_cleanup_tasks(state_sync) == [ SnapshotTableCleanupTask(snapshot=snapshot_a.table_info, dev_table_only=True), ] - state_sync.delete_expired_snapshots() + state_sync.delete_expired_snapshots(batch_range=ExpiredBatchRange.all_batch_range()) assert state_sync.engine_adapter.fetchone("SELECT COUNT(*) FROM sqlmesh._intervals")[0] == 5 # type: ignore diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 6270cec56a..60ea3fd451 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1030,7 +1030,7 @@ def test_janitor(sushi_context, mocker: MockerFixture) -> None: sushi_context._engine_adapter = adapter_mock sushi_context.engine_adapters = {sushi_context.config.default_gateway: adapter_mock} sushi_context._state_sync = state_sync_mock - state_sync_mock.get_expired_snapshots.return_value = [] + state_sync_mock.get_expired_snapshots.return_value = None sushi_context._run_janitor() # Assert that the schemas are dropped just twice for the schema based environment From 5edf5389749bf5489d7205db1f32e16990d343ab Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 8 Oct 2025 09:49:24 -0700 Subject: [PATCH 0956/1056] fix: remove state sync from migrate (#5502) --- sqlmesh/core/state_sync/db/facade.py | 2 +- sqlmesh/core/state_sync/db/migrator.py | 11 +++++------ sqlmesh/migrations/v0000_baseline.py | 9 +++------ .../migrations/v0061_mysql_fix_blob_text_type.py | 7 ++----- sqlmesh/migrations/v0062_add_model_gateway.py | 4 ++-- sqlmesh/migrations/v0063_change_signals.py | 6 ++---- .../v0064_join_when_matched_strings.py | 6 ++---- sqlmesh/migrations/v0065_add_model_optimize.py | 4 ++-- .../migrations/v0066_add_auto_restatements.py | 8 ++------ .../v0067_add_tsql_date_full_precision.py | 4 ++-- ..._include_unrendered_query_in_metadata_hash.py | 4 ++-- .../migrations/v0069_update_dev_table_suffix.py | 6 ++---- .../v0070_include_grains_in_metadata_hash.py | 4 ++-- .../v0071_add_dev_version_to_intervals.py | 8 ++------ .../v0072_add_environment_statements.py | 6 ++---- .../v0073_remove_symbolic_disable_restatement.py | 6 ++---- ...0074_add_partition_by_time_column_property.py | 4 ++-- .../migrations/v0075_remove_validate_query.py | 6 ++---- sqlmesh/migrations/v0076_add_cron_tz.py | 4 ++-- .../v0077_fix_column_type_hash_calculation.py | 4 ++-- .../v0078_warn_if_non_migratable_python_env.py | 6 ++---- .../v0079_add_gateway_managed_property.py | 16 +++++++--------- .../v0080_add_batch_size_to_scd_type_2_models.py | 4 ++-- .../migrations/v0081_update_partitioned_by.py | 6 ++---- ..._warn_if_incorrectly_duplicated_statements.py | 6 ++---- ...3_use_sql_for_scd_time_data_type_data_hash.py | 4 ++-- ...malize_quote_when_matched_and_merge_filter.py | 4 ++-- sqlmesh/migrations/v0085_deterministic_repr.py | 6 ++---- .../migrations/v0086_check_deterministic_bug.py | 6 ++---- .../v0087_normalize_blueprint_variables.py | 6 ++---- ...v0088_warn_about_variable_python_env_diffs.py | 6 ++---- .../v0089_add_virtual_environment_mode.py | 4 ++-- .../migrations/v0090_add_forward_only_column.py | 8 ++------ sqlmesh/migrations/v0091_on_additive_change.py | 4 ++-- .../v0092_warn_about_dbt_data_type_diff.py | 6 ++---- .../v0093_use_raw_sql_in_fingerprint.py | 4 ++-- ...94_add_dev_version_and_fingerprint_columns.py | 8 ++------ .../v0095_warn_about_dbt_raw_sql_diff.py | 6 ++---- .../migrations/v0096_remove_plan_dags_table.py | 6 ++---- sqlmesh/migrations/v0097_add_dbt_name_in_node.py | 4 ++-- .../v0098_add_dbt_node_info_in_node.py | 6 ++---- .../v0099_add_last_altered_to_intervals.py | 6 ++---- .../v0100_add_grants_and_grants_target_layer.py | 4 ++-- 43 files changed, 94 insertions(+), 155 deletions(-) diff --git a/sqlmesh/core/state_sync/db/facade.py b/sqlmesh/core/state_sync/db/facade.py index 49f7b5b92f..64042624f3 100644 --- a/sqlmesh/core/state_sync/db/facade.py +++ b/sqlmesh/core/state_sync/db/facade.py @@ -469,7 +469,7 @@ def migrate( ) -> None: """Migrate the state sync to the latest SQLMesh / SQLGlot version.""" self.migrator.migrate( - self, + self.schema, skip_backup=skip_backup, promoted_snapshots_only=promoted_snapshots_only, ) diff --git a/sqlmesh/core/state_sync/db/migrator.py b/sqlmesh/core/state_sync/db/migrator.py index 3e3f978b96..ad60c57570 100644 --- a/sqlmesh/core/state_sync/db/migrator.py +++ b/sqlmesh/core/state_sync/db/migrator.py @@ -30,7 +30,6 @@ MIN_SCHEMA_VERSION, MIN_SQLMESH_VERSION, ) -from sqlmesh.core.state_sync.base import StateSync from sqlmesh.core.state_sync.db.environment import EnvironmentState from sqlmesh.core.state_sync.db.interval import IntervalState from sqlmesh.core.state_sync.db.snapshot import SnapshotState @@ -85,7 +84,7 @@ def __init__( def migrate( self, - state_sync: StateSync, + schema: t.Optional[str], skip_backup: bool = False, promoted_snapshots_only: bool = True, ) -> None: @@ -94,7 +93,7 @@ def migrate( migration_start_ts = time.perf_counter() try: - migrate_rows = self._apply_migrations(state_sync, skip_backup) + migrate_rows = self._apply_migrations(schema, skip_backup) if not migrate_rows and major_minor(SQLMESH_VERSION) == versions.minor_sqlmesh_version: return @@ -153,7 +152,7 @@ def rollback(self) -> None: def _apply_migrations( self, - state_sync: StateSync, + schema: t.Optional[str], skip_backup: bool, ) -> bool: versions = self.version_state.get_versions() @@ -184,10 +183,10 @@ def _apply_migrations( for migration in migrations: logger.info(f"Applying migration {migration}") - migration.migrate_schemas(state_sync) + migration.migrate_schemas(engine_adapter=self.engine_adapter, schema=schema) if state_table_exist: # No need to run DML for the initial migration since all tables are empty - migration.migrate_rows(state_sync) + migration.migrate_rows(engine_adapter=self.engine_adapter, schema=schema) snapshot_count_after = self.snapshot_state.count() diff --git a/sqlmesh/migrations/v0000_baseline.py b/sqlmesh/migrations/v0000_baseline.py index 4891900a76..abd316fcfe 100644 --- a/sqlmesh/migrations/v0000_baseline.py +++ b/sqlmesh/migrations/v0000_baseline.py @@ -4,15 +4,12 @@ from sqlmesh.utils.migration import blob_text_type, index_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore - schema = state_sync.schema - engine_adapter = state_sync.engine_adapter - +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore intervals_table = "_intervals" snapshots_table = "_snapshots" environments_table = "_environments" versions_table = "_versions" - if state_sync.schema: + if schema: engine_adapter.create_schema(schema) intervals_table = f"{schema}.{intervals_table}" snapshots_table = f"{schema}.{snapshots_table}" @@ -94,5 +91,5 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter.create_index(intervals_table, "_intervals_name_version_idx", ("name", "version")) -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py b/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py index 34b765b3ad..897974f09a 100644 --- a/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py +++ b/sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py @@ -9,12 +9,9 @@ from sqlmesh.utils.migration import blob_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore if engine_adapter.dialect != "mysql": return - - schema = state_sync.schema environments_table = "_environments" snapshots_table = "_snapshots" @@ -46,5 +43,5 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter.execute(alter_table_exp) -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0062_add_model_gateway.py b/sqlmesh/migrations/v0062_add_model_gateway.py index 524a94044a..f65d8224ec 100644 --- a/sqlmesh/migrations/v0062_add_model_gateway.py +++ b/sqlmesh/migrations/v0062_add_model_gateway.py @@ -1,9 +1,9 @@ """Add the gateway model attribute.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0063_change_signals.py b/sqlmesh/migrations/v0063_change_signals.py index 8806c9ea60..bbced547fd 100644 --- a/sqlmesh/migrations/v0063_change_signals.py +++ b/sqlmesh/migrations/v0063_change_signals.py @@ -7,15 +7,13 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore import pandas as pd - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema snapshots_table = "_snapshots" index_type = index_text_type(engine_adapter.dialect) if schema: diff --git a/sqlmesh/migrations/v0064_join_when_matched_strings.py b/sqlmesh/migrations/v0064_join_when_matched_strings.py index 6da3164a38..ffd4c94913 100644 --- a/sqlmesh/migrations/v0064_join_when_matched_strings.py +++ b/sqlmesh/migrations/v0064_join_when_matched_strings.py @@ -7,15 +7,13 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore import pandas as pd - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema snapshots_table = "_snapshots" index_type = index_text_type(engine_adapter.dialect) if schema: diff --git a/sqlmesh/migrations/v0065_add_model_optimize.py b/sqlmesh/migrations/v0065_add_model_optimize.py index 09240aa61e..e9bc646666 100644 --- a/sqlmesh/migrations/v0065_add_model_optimize.py +++ b/sqlmesh/migrations/v0065_add_model_optimize.py @@ -1,9 +1,9 @@ """Add the optimize_query model attribute.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0066_add_auto_restatements.py b/sqlmesh/migrations/v0066_add_auto_restatements.py index 96d2cd45e8..9eea773573 100644 --- a/sqlmesh/migrations/v0066_add_auto_restatements.py +++ b/sqlmesh/migrations/v0066_add_auto_restatements.py @@ -5,9 +5,7 @@ from sqlmesh.utils.migration import index_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore auto_restatements_table = "_auto_restatements" intervals_table = "_intervals" @@ -40,9 +38,7 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter.execute(alter_table_exp) -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore intervals_table = "_intervals" if schema: diff --git a/sqlmesh/migrations/v0067_add_tsql_date_full_precision.py b/sqlmesh/migrations/v0067_add_tsql_date_full_precision.py index d4fd93eda4..1243118df0 100644 --- a/sqlmesh/migrations/v0067_add_tsql_date_full_precision.py +++ b/sqlmesh/migrations/v0067_add_tsql_date_full_precision.py @@ -1,9 +1,9 @@ """Add full precision for tsql to support nanoseconds.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py b/sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py index 6f7ddbdc1c..35142e9aeb 100644 --- a/sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py +++ b/sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py @@ -1,9 +1,9 @@ """Include the unrendered query in the metadata hash.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0069_update_dev_table_suffix.py b/sqlmesh/migrations/v0069_update_dev_table_suffix.py index 57b41a816c..f69aac434e 100644 --- a/sqlmesh/migrations/v0069_update_dev_table_suffix.py +++ b/sqlmesh/migrations/v0069_update_dev_table_suffix.py @@ -7,15 +7,13 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore import pandas as pd - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema snapshots_table = "_snapshots" environments_table = "_environments" if schema: diff --git a/sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py b/sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py index 4b339d8e97..d0dbdd5563 100644 --- a/sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py +++ b/sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py @@ -1,9 +1,9 @@ """Include grains in the metadata hash.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py b/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py index 4e6cbab4f0..61a49dc0b9 100644 --- a/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py +++ b/sqlmesh/migrations/v0071_add_dev_version_to_intervals.py @@ -8,9 +8,7 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore intervals_table = "_intervals" if schema: intervals_table = f"{schema}.{intervals_table}" @@ -29,9 +27,7 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter.execute(alter_table_exp) -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore intervals_table = "_intervals" snapshots_table = "_snapshots" if schema: diff --git a/sqlmesh/migrations/v0072_add_environment_statements.py b/sqlmesh/migrations/v0072_add_environment_statements.py index e73faf2b9a..4ed52b5c47 100644 --- a/sqlmesh/migrations/v0072_add_environment_statements.py +++ b/sqlmesh/migrations/v0072_add_environment_statements.py @@ -5,9 +5,7 @@ from sqlmesh.utils.migration import blob_text_type, index_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore environment_statements_table = "_environment_statements" if schema: @@ -27,5 +25,5 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore ) -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py b/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py index 40e74d6426..708693ed61 100644 --- a/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py +++ b/sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py @@ -6,15 +6,13 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore import pandas as pd - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0074_add_partition_by_time_column_property.py b/sqlmesh/migrations/v0074_add_partition_by_time_column_property.py index 04f1a27254..acd349c888 100644 --- a/sqlmesh/migrations/v0074_add_partition_by_time_column_property.py +++ b/sqlmesh/migrations/v0074_add_partition_by_time_column_property.py @@ -2,9 +2,9 @@ (default: True to keep the original behaviour)""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0075_remove_validate_query.py b/sqlmesh/migrations/v0075_remove_validate_query.py index f6d4e255d9..9fdcca7ea6 100644 --- a/sqlmesh/migrations/v0075_remove_validate_query.py +++ b/sqlmesh/migrations/v0075_remove_validate_query.py @@ -8,15 +8,13 @@ from sqlmesh.utils.migration import blob_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore import pandas as pd - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema snapshots_table = "_snapshots" index_type = index_text_type(engine_adapter.dialect) if schema: diff --git a/sqlmesh/migrations/v0076_add_cron_tz.py b/sqlmesh/migrations/v0076_add_cron_tz.py index 300474aa18..909017c8cd 100644 --- a/sqlmesh/migrations/v0076_add_cron_tz.py +++ b/sqlmesh/migrations/v0076_add_cron_tz.py @@ -1,9 +1,9 @@ """Add 'cron_tz' property to node definition.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py b/sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py index 2aec1140f1..68953836bd 100644 --- a/sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py +++ b/sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py @@ -1,9 +1,9 @@ """Use the model's dialect when calculating the hash for the column types.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py b/sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py index c24b6a5168..adf1e96dd0 100644 --- a/sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py +++ b/sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py @@ -24,13 +24,11 @@ from sqlmesh.core.console import get_console -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0079_add_gateway_managed_property.py b/sqlmesh/migrations/v0079_add_gateway_managed_property.py index 8d24601102..7650d6d765 100644 --- a/sqlmesh/migrations/v0079_add_gateway_managed_property.py +++ b/sqlmesh/migrations/v0079_add_gateway_managed_property.py @@ -3,11 +3,10 @@ from sqlglot import exp -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore environments_table = "_environments" - if state_sync.schema: - environments_table = f"{state_sync.schema}.{environments_table}" + if schema: + environments_table = f"{schema}.{environments_table}" alter_table_exp = exp.Alter( this=exp.to_table(environments_table), @@ -22,13 +21,12 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter.execute(alter_table_exp) -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore environments_table = "_environments" - if state_sync.schema: - environments_table = f"{state_sync.schema}.{environments_table}" + if schema: + environments_table = f"{schema}.{environments_table}" - state_sync.engine_adapter.update_table( + engine_adapter.update_table( environments_table, {"gateway_managed": False}, where=exp.true(), diff --git a/sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py b/sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py index 582bdd3da9..35cb3977cc 100644 --- a/sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py +++ b/sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py @@ -1,9 +1,9 @@ """Add batch_size to SCD Type 2 models and add updated_at_name to by time which changes their data hash.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0081_update_partitioned_by.py b/sqlmesh/migrations/v0081_update_partitioned_by.py index 611d8f6973..8740285bf0 100644 --- a/sqlmesh/migrations/v0081_update_partitioned_by.py +++ b/sqlmesh/migrations/v0081_update_partitioned_by.py @@ -8,15 +8,13 @@ from sqlmesh.utils.migration import blob_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore import pandas as pd - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema snapshots_table = "_snapshots" index_type = index_text_type(engine_adapter.dialect) if schema: diff --git a/sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py b/sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py index 6eadbfc2c3..5565b099cd 100644 --- a/sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py +++ b/sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py @@ -34,13 +34,11 @@ from sqlmesh.core.console import get_console -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py b/sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py index 38c84afafd..5dbe0847f9 100644 --- a/sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py +++ b/sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py @@ -1,9 +1,9 @@ """Use sql(...) instead of gen when computing the data hash of the time data type.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py b/sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py index 5401c97d77..9edb0051ba 100644 --- a/sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py +++ b/sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py @@ -5,9 +5,9 @@ """ -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0085_deterministic_repr.py b/sqlmesh/migrations/v0085_deterministic_repr.py index 1a90277bbe..81cb0f194e 100644 --- a/sqlmesh/migrations/v0085_deterministic_repr.py +++ b/sqlmesh/migrations/v0085_deterministic_repr.py @@ -36,15 +36,13 @@ def _dict_sort(obj: t.Any) -> str: return repr(obj) -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore import pandas as pd - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0086_check_deterministic_bug.py b/sqlmesh/migrations/v0086_check_deterministic_bug.py index 0679414881..f44e5b8e33 100644 --- a/sqlmesh/migrations/v0086_check_deterministic_bug.py +++ b/sqlmesh/migrations/v0086_check_deterministic_bug.py @@ -10,13 +10,11 @@ KEYS_TO_MAKE_DETERMINISTIC = ["__sqlmesh__vars__", "__sqlmesh__blueprint__vars__"] -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore snapshots_table = "_snapshots" versions_table = "_versions" if schema: diff --git a/sqlmesh/migrations/v0087_normalize_blueprint_variables.py b/sqlmesh/migrations/v0087_normalize_blueprint_variables.py index 2f23a0653e..fe737861c2 100644 --- a/sqlmesh/migrations/v0087_normalize_blueprint_variables.py +++ b/sqlmesh/migrations/v0087_normalize_blueprint_variables.py @@ -35,15 +35,13 @@ class SqlValue: sql: str -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore import pandas as pd - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py b/sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py index 405aad725f..0aa7171821 100644 --- a/sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py +++ b/sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py @@ -35,13 +35,11 @@ METADATA_HASH_EXPRESSIONS = {"on_virtual_update", "audits", "signals", "audit_definitions"} -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0089_add_virtual_environment_mode.py b/sqlmesh/migrations/v0089_add_virtual_environment_mode.py index 63d491418f..88126c76d7 100644 --- a/sqlmesh/migrations/v0089_add_virtual_environment_mode.py +++ b/sqlmesh/migrations/v0089_add_virtual_environment_mode.py @@ -1,9 +1,9 @@ """Add virtual_environment_mode to the model definition.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0090_add_forward_only_column.py b/sqlmesh/migrations/v0090_add_forward_only_column.py index b68c0f65ea..48253691ec 100644 --- a/sqlmesh/migrations/v0090_add_forward_only_column.py +++ b/sqlmesh/migrations/v0090_add_forward_only_column.py @@ -7,9 +7,7 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" @@ -27,11 +25,9 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter.execute(alter_table_exp) -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore import pandas as pd - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0091_on_additive_change.py b/sqlmesh/migrations/v0091_on_additive_change.py index c0170bd438..e24b9b4122 100644 --- a/sqlmesh/migrations/v0091_on_additive_change.py +++ b/sqlmesh/migrations/v0091_on_additive_change.py @@ -1,9 +1,9 @@ """Add on_additive_change to incremental model metadata hash.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py b/sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py index 1ff069bc82..02e2a5f4c1 100644 --- a/sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py +++ b/sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py @@ -17,13 +17,11 @@ SQLMESH_DBT_PACKAGE = "sqlmesh.dbt" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py b/sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py index f629c1d27d..aaaacf3a91 100644 --- a/sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py +++ b/sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py @@ -1,9 +1,9 @@ """Use the raw SQL when computing the model fingerprint.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py b/sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py index 1abc4fa4af..9d7adf21a3 100644 --- a/sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py +++ b/sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py @@ -7,9 +7,7 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" @@ -42,11 +40,9 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter.execute(add_fingerprint_exp) -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore import pandas as pd - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py b/sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py index 802d996df5..0fa9fd51b8 100644 --- a/sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py +++ b/sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py @@ -17,13 +17,11 @@ SQLMESH_DBT_PACKAGE = "sqlmesh.dbt" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0096_remove_plan_dags_table.py b/sqlmesh/migrations/v0096_remove_plan_dags_table.py index e342d6b1a8..8eb674ead0 100644 --- a/sqlmesh/migrations/v0096_remove_plan_dags_table.py +++ b/sqlmesh/migrations/v0096_remove_plan_dags_table.py @@ -1,9 +1,7 @@ """Remove the obsolete _plan_dags table.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore plan_dags_table = "_plan_dags" if schema: plan_dags_table = f"{schema}.{plan_dags_table}" @@ -11,5 +9,5 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter.drop_table(plan_dags_table) -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0097_add_dbt_name_in_node.py b/sqlmesh/migrations/v0097_add_dbt_name_in_node.py index f8909e4430..cd548977ef 100644 --- a/sqlmesh/migrations/v0097_add_dbt_name_in_node.py +++ b/sqlmesh/migrations/v0097_add_dbt_name_in_node.py @@ -1,9 +1,9 @@ """Add 'dbt_name' property to node definition.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0098_add_dbt_node_info_in_node.py b/sqlmesh/migrations/v0098_add_dbt_node_info_in_node.py index c8acd0bafd..b69ba8fa6f 100644 --- a/sqlmesh/migrations/v0098_add_dbt_node_info_in_node.py +++ b/sqlmesh/migrations/v0098_add_dbt_node_info_in_node.py @@ -5,15 +5,13 @@ from sqlmesh.utils.migration import index_text_type, blob_text_type -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore import pandas as pd - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema snapshots_table = "_snapshots" if schema: snapshots_table = f"{schema}.{snapshots_table}" diff --git a/sqlmesh/migrations/v0099_add_last_altered_to_intervals.py b/sqlmesh/migrations/v0099_add_last_altered_to_intervals.py index 1a119a338d..b80ed35a35 100644 --- a/sqlmesh/migrations/v0099_add_last_altered_to_intervals.py +++ b/sqlmesh/migrations/v0099_add_last_altered_to_intervals.py @@ -3,9 +3,7 @@ from sqlglot import exp -def migrate_schemas(state_sync, **kwargs): # type: ignore - engine_adapter = state_sync.engine_adapter - schema = state_sync.schema +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore intervals_table = "_intervals" if schema: intervals_table = f"{schema}.{intervals_table}" @@ -23,5 +21,5 @@ def migrate_schemas(state_sync, **kwargs): # type: ignore engine_adapter.execute(alter_table_exp) -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass diff --git a/sqlmesh/migrations/v0100_add_grants_and_grants_target_layer.py b/sqlmesh/migrations/v0100_add_grants_and_grants_target_layer.py index fa23935da0..9ff64c5e57 100644 --- a/sqlmesh/migrations/v0100_add_grants_and_grants_target_layer.py +++ b/sqlmesh/migrations/v0100_add_grants_and_grants_target_layer.py @@ -1,9 +1,9 @@ """Add grants and grants_target_layer to incremental model metadata hash.""" -def migrate_schemas(state_sync, **kwargs): # type: ignore +def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore pass -def migrate_rows(state_sync, **kwargs): # type: ignore +def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore pass From 6fb471f2eff08796f349755b47597d822d8761af Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:43:30 -0500 Subject: [PATCH 0957/1056] Feat: add dbt builtin global try_or_compiler_error (#5504) --- sqlmesh/dbt/builtin.py | 17 +++++++++++++++++ tests/dbt/test_transformation.py | 23 +++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index b8180bc011..145e29a96c 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -50,6 +50,22 @@ def warn(self, msg: str) -> str: return "" +def try_or_compiler_error( + message_if_exception: str, func: t.Callable, *args: t.Any, **kwargs: t.Any +) -> t.Any: + try: + return func(*args, **kwargs) + except Exception: + if DBT_VERSION >= (1, 4, 0): + from dbt.exceptions import CompilationError + + raise CompilationError(message_if_exception) + else: + from dbt.exceptions import CompilationException # type: ignore + + raise CompilationException(message_if_exception) + + class Api: def __init__(self, dialect: t.Optional[str]) -> None: if dialect: @@ -411,6 +427,7 @@ def debug() -> str: "sqlmesh_incremental": True, "tojson": to_json, "toyaml": to_yaml, + "try_or_compiler_error": try_or_compiler_error, "zip": do_zip, "zip_strict": lambda *args: list(zip(*args)), } diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index e519713d26..304ac57731 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -1592,6 +1592,29 @@ def test_exceptions(sushi_test_project: Project): context.render('{{ exceptions.raise_compiler_error("Error") }}') +@pytest.mark.xdist_group("dbt_manifest") +def test_try_or_compiler_error(sushi_test_project: Project): + context = sushi_test_project.context + + result = context.render( + '{{ try_or_compiler_error("Error message", modules.datetime.datetime.strptime, "2023-01-15", "%Y-%m-%d") }}' + ) + assert "2023-01-15" in result + + with pytest.raises(CompilationError, match="Invalid date format"): + context.render( + '{{ try_or_compiler_error("Invalid date format", modules.datetime.datetime.strptime, "invalid", "%Y-%m-%d") }}' + ) + + # built-in macro calling try_or_compiler_error works + result = context.render( + '{{ dbt.dates_in_range("2023-01-01", "2023-01-03", "%Y-%m-%d", "%Y-%m-%d") }}' + ) + assert "2023-01-01" in result + assert "2023-01-02" in result + assert "2023-01-03" in result + + @pytest.mark.xdist_group("dbt_manifest") def test_modules(sushi_test_project: Project): context = sushi_test_project.context From e1510cec7776985b8549bfa6fbd82b2fba66fbb3 Mon Sep 17 00:00:00 2001 From: Ryan Eakman <6326532+eakmanrq@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:45:26 -0700 Subject: [PATCH 0958/1056] chore: move janitor functions to janitor.py (#5510) --- sqlmesh/core/context.py | 3 +- sqlmesh/core/janitor.py | 181 +++++++++++++++ sqlmesh/core/state_sync/__init__.py | 1 - sqlmesh/core/state_sync/common.py | 169 +------------- tests/core/state_sync/test_state_sync.py | 252 +------------------- tests/core/test_janitor.py | 282 +++++++++++++++++++++++ 6 files changed, 466 insertions(+), 422 deletions(-) create mode 100644 sqlmesh/core/janitor.py create mode 100644 tests/core/test_janitor.py diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index bd8647f811..d118116f7f 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -107,9 +107,8 @@ CachingStateSync, StateReader, StateSync, - cleanup_expired_views, ) -from sqlmesh.core.state_sync.common import delete_expired_snapshots +from sqlmesh.core.janitor import cleanup_expired_views, delete_expired_snapshots from sqlmesh.core.table_diff import TableDiff from sqlmesh.core.test import ( ModelTextTestResult, diff --git a/sqlmesh/core/janitor.py b/sqlmesh/core/janitor.py new file mode 100644 index 0000000000..e050d6ef6c --- /dev/null +++ b/sqlmesh/core/janitor.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +import typing as t + +from sqlglot import exp + +from sqlmesh.core.engine_adapter import EngineAdapter +from sqlmesh.core.console import Console +from sqlmesh.core.dialect import schema_ +from sqlmesh.core.environment import Environment +from sqlmesh.core.snapshot import SnapshotEvaluator +from sqlmesh.core.state_sync import StateSync +from sqlmesh.core.state_sync.common import ( + logger, + iter_expired_snapshot_batches, + RowBoundary, + ExpiredBatchRange, +) +from sqlmesh.utils.errors import SQLMeshError + + +def cleanup_expired_views( + default_adapter: EngineAdapter, + engine_adapters: t.Dict[str, EngineAdapter], + environments: t.List[Environment], + warn_on_delete_failure: bool = False, + console: t.Optional[Console] = None, +) -> None: + expired_schema_or_catalog_environments = [ + environment + for environment in environments + if environment.suffix_target.is_schema or environment.suffix_target.is_catalog + ] + expired_table_environments = [ + environment for environment in environments if environment.suffix_target.is_table + ] + + # We have to use the corresponding adapter if the virtual layer is gateway managed + def get_adapter(gateway_managed: bool, gateway: t.Optional[str] = None) -> EngineAdapter: + if gateway_managed and gateway: + return engine_adapters.get(gateway, default_adapter) + return default_adapter + + catalogs_to_drop: t.Set[t.Tuple[EngineAdapter, str]] = set() + schemas_to_drop: t.Set[t.Tuple[EngineAdapter, exp.Table]] = set() + + # Collect schemas and catalogs to drop + for engine_adapter, expired_catalog, expired_schema, suffix_target in { + ( + (engine_adapter := get_adapter(environment.gateway_managed, snapshot.model_gateway)), + snapshot.qualified_view_name.catalog_for_environment( + environment.naming_info, dialect=engine_adapter.dialect + ), + snapshot.qualified_view_name.schema_for_environment( + environment.naming_info, dialect=engine_adapter.dialect + ), + environment.suffix_target, + ) + for environment in expired_schema_or_catalog_environments + for snapshot in environment.snapshots + if snapshot.is_model and not snapshot.is_symbolic + }: + if suffix_target.is_catalog: + if expired_catalog: + catalogs_to_drop.add((engine_adapter, expired_catalog)) + else: + schema = schema_(expired_schema, expired_catalog) + schemas_to_drop.add((engine_adapter, schema)) + + # Drop the views for the expired environments + for engine_adapter, expired_view in { + ( + (engine_adapter := get_adapter(environment.gateway_managed, snapshot.model_gateway)), + snapshot.qualified_view_name.for_environment( + environment.naming_info, dialect=engine_adapter.dialect + ), + ) + for environment in expired_table_environments + for snapshot in environment.snapshots + if snapshot.is_model and not snapshot.is_symbolic + }: + try: + engine_adapter.drop_view(expired_view, ignore_if_not_exists=True) + if console: + console.update_cleanup_progress(expired_view) + except Exception as e: + message = f"Failed to drop the expired environment view '{expired_view}': {e}" + if warn_on_delete_failure: + logger.warning(message) + else: + raise SQLMeshError(message) from e + + # Drop the schemas for the expired environments + for engine_adapter, schema in schemas_to_drop: + try: + engine_adapter.drop_schema( + schema, + ignore_if_not_exists=True, + cascade=True, + ) + if console: + console.update_cleanup_progress(schema.sql(dialect=engine_adapter.dialect)) + except Exception as e: + message = f"Failed to drop the expired environment schema '{schema}': {e}" + if warn_on_delete_failure: + logger.warning(message) + else: + raise SQLMeshError(message) from e + + # Drop any catalogs that were associated with a snapshot where the engine adapter supports dropping catalogs + # catalogs_to_drop is only populated when environment_suffix_target is set to 'catalog' + for engine_adapter, catalog in catalogs_to_drop: + if engine_adapter.SUPPORTS_CREATE_DROP_CATALOG: + try: + engine_adapter.drop_catalog(catalog) + if console: + console.update_cleanup_progress(catalog) + except Exception as e: + message = f"Failed to drop the expired environment catalog '{catalog}': {e}" + if warn_on_delete_failure: + logger.warning(message) + else: + raise SQLMeshError(message) from e + + +def delete_expired_snapshots( + state_sync: StateSync, + snapshot_evaluator: SnapshotEvaluator, + *, + current_ts: int, + ignore_ttl: bool = False, + batch_size: t.Optional[int] = None, + console: t.Optional[Console] = None, +) -> None: + """Delete all expired snapshots in batches. + + This helper function encapsulates the logic for deleting expired snapshots in batches, + eliminating code duplication across different use cases. + + Args: + state_sync: StateSync instance to query and delete expired snapshots from. + snapshot_evaluator: SnapshotEvaluator instance to clean up tables associated with snapshots. + current_ts: Timestamp used to evaluate expiration. + ignore_ttl: If True, include snapshots regardless of TTL (only checks if unreferenced). + batch_size: Maximum number of snapshots to fetch per batch. + console: Optional console for reporting progress. + + Returns: + The total number of deleted expired snapshots. + """ + num_expired_snapshots = 0 + for batch in iter_expired_snapshot_batches( + state_reader=state_sync, + current_ts=current_ts, + ignore_ttl=ignore_ttl, + batch_size=batch_size, + ): + end_info = ( + f"updated_ts={batch.batch_range.end.updated_ts}" + if isinstance(batch.batch_range.end, RowBoundary) + else f"limit={batch.batch_range.end.batch_size}" + ) + logger.info( + "Processing batch of size %s with end %s", + len(batch.expired_snapshot_ids), + end_info, + ) + snapshot_evaluator.cleanup( + target_snapshots=batch.cleanup_tasks, + on_complete=console.update_cleanup_progress if console else None, + ) + state_sync.delete_expired_snapshots( + batch_range=ExpiredBatchRange( + start=RowBoundary.lowest_boundary(), + end=batch.batch_range.end, + ), + ignore_ttl=ignore_ttl, + ) + logger.info("Cleaned up expired snapshots batch") + num_expired_snapshots += len(batch.expired_snapshot_ids) + logger.info("Cleaned up %s expired snapshots", num_expired_snapshots) diff --git a/sqlmesh/core/state_sync/__init__.py b/sqlmesh/core/state_sync/__init__.py index 1585d6211f..12ea77ac8f 100644 --- a/sqlmesh/core/state_sync/__init__.py +++ b/sqlmesh/core/state_sync/__init__.py @@ -20,5 +20,4 @@ Versions as Versions, ) from sqlmesh.core.state_sync.cache import CachingStateSync as CachingStateSync -from sqlmesh.core.state_sync.common import cleanup_expired_views as cleanup_expired_views from sqlmesh.core.state_sync.db import EngineAdapterStateSync as EngineAdapterStateSync diff --git a/sqlmesh/core/state_sync/common.py b/sqlmesh/core/state_sync/common.py index 3fdd0bc015..056565b060 100644 --- a/sqlmesh/core/state_sync/common.py +++ b/sqlmesh/core/state_sync/common.py @@ -11,132 +11,23 @@ from pydantic_core.core_schema import ValidationInfo from sqlglot import exp -from sqlmesh.core.console import Console -from sqlmesh.core.dialect import schema_ from sqlmesh.utils.pydantic import PydanticModel, field_validator from sqlmesh.core.environment import Environment, EnvironmentStatements, EnvironmentNamingInfo -from sqlmesh.utils.errors import SQLMeshError from sqlmesh.core.snapshot import ( Snapshot, - SnapshotEvaluator, SnapshotId, SnapshotTableCleanupTask, SnapshotTableInfo, ) if t.TYPE_CHECKING: - from sqlmesh.core.engine_adapter.base import EngineAdapter - from sqlmesh.core.state_sync.base import Versions, StateReader, StateSync + from sqlmesh.core.state_sync.base import Versions, StateReader logger = logging.getLogger(__name__) EXPIRED_SNAPSHOT_DEFAULT_BATCH_SIZE = 200 -def cleanup_expired_views( - default_adapter: EngineAdapter, - engine_adapters: t.Dict[str, EngineAdapter], - environments: t.List[Environment], - warn_on_delete_failure: bool = False, - console: t.Optional[Console] = None, -) -> None: - expired_schema_or_catalog_environments = [ - environment - for environment in environments - if environment.suffix_target.is_schema or environment.suffix_target.is_catalog - ] - expired_table_environments = [ - environment for environment in environments if environment.suffix_target.is_table - ] - - # We have to use the corresponding adapter if the virtual layer is gateway managed - def get_adapter(gateway_managed: bool, gateway: t.Optional[str] = None) -> EngineAdapter: - if gateway_managed and gateway: - return engine_adapters.get(gateway, default_adapter) - return default_adapter - - catalogs_to_drop: t.Set[t.Tuple[EngineAdapter, str]] = set() - schemas_to_drop: t.Set[t.Tuple[EngineAdapter, exp.Table]] = set() - - # Collect schemas and catalogs to drop - for engine_adapter, expired_catalog, expired_schema, suffix_target in { - ( - (engine_adapter := get_adapter(environment.gateway_managed, snapshot.model_gateway)), - snapshot.qualified_view_name.catalog_for_environment( - environment.naming_info, dialect=engine_adapter.dialect - ), - snapshot.qualified_view_name.schema_for_environment( - environment.naming_info, dialect=engine_adapter.dialect - ), - environment.suffix_target, - ) - for environment in expired_schema_or_catalog_environments - for snapshot in environment.snapshots - if snapshot.is_model and not snapshot.is_symbolic - }: - if suffix_target.is_catalog: - if expired_catalog: - catalogs_to_drop.add((engine_adapter, expired_catalog)) - else: - schema = schema_(expired_schema, expired_catalog) - schemas_to_drop.add((engine_adapter, schema)) - - # Drop the views for the expired environments - for engine_adapter, expired_view in { - ( - (engine_adapter := get_adapter(environment.gateway_managed, snapshot.model_gateway)), - snapshot.qualified_view_name.for_environment( - environment.naming_info, dialect=engine_adapter.dialect - ), - ) - for environment in expired_table_environments - for snapshot in environment.snapshots - if snapshot.is_model and not snapshot.is_symbolic - }: - try: - engine_adapter.drop_view(expired_view, ignore_if_not_exists=True) - if console: - console.update_cleanup_progress(expired_view) - except Exception as e: - message = f"Failed to drop the expired environment view '{expired_view}': {e}" - if warn_on_delete_failure: - logger.warning(message) - else: - raise SQLMeshError(message) from e - - # Drop the schemas for the expired environments - for engine_adapter, schema in schemas_to_drop: - try: - engine_adapter.drop_schema( - schema, - ignore_if_not_exists=True, - cascade=True, - ) - if console: - console.update_cleanup_progress(schema.sql(dialect=engine_adapter.dialect)) - except Exception as e: - message = f"Failed to drop the expired environment schema '{schema}': {e}" - if warn_on_delete_failure: - logger.warning(message) - else: - raise SQLMeshError(message) from e - - # Drop any catalogs that were associated with a snapshot where the engine adapter supports dropping catalogs - # catalogs_to_drop is only populated when environment_suffix_target is set to 'catalog' - for engine_adapter, catalog in catalogs_to_drop: - if engine_adapter.SUPPORTS_CREATE_DROP_CATALOG: - try: - engine_adapter.drop_catalog(catalog) - if console: - console.update_cleanup_progress(catalog) - except Exception as e: - message = f"Failed to drop the expired environment catalog '{catalog}': {e}" - if warn_on_delete_failure: - logger.warning(message) - else: - raise SQLMeshError(message) from e - - def transactional() -> t.Callable[[t.Callable], t.Callable]: def decorator(func: t.Callable) -> t.Callable: @wraps(func) @@ -429,61 +320,3 @@ def iter_expired_snapshot_batches( start=batch.batch_range.end, end=LimitBoundary(batch_size=batch_size), ) - - -def delete_expired_snapshots( - state_sync: StateSync, - snapshot_evaluator: SnapshotEvaluator, - *, - current_ts: int, - ignore_ttl: bool = False, - batch_size: t.Optional[int] = None, - console: t.Optional[Console] = None, -) -> None: - """Delete all expired snapshots in batches. - - This helper function encapsulates the logic for deleting expired snapshots in batches, - eliminating code duplication across different use cases. - - Args: - state_sync: StateSync instance to query and delete expired snapshots from. - snapshot_evaluator: SnapshotEvaluator instance to clean up tables associated with snapshots. - current_ts: Timestamp used to evaluate expiration. - ignore_ttl: If True, include snapshots regardless of TTL (only checks if unreferenced). - batch_size: Maximum number of snapshots to fetch per batch. - console: Optional console for reporting progress. - - Returns: - The total number of deleted expired snapshots. - """ - num_expired_snapshots = 0 - for batch in iter_expired_snapshot_batches( - state_reader=state_sync, - current_ts=current_ts, - ignore_ttl=ignore_ttl, - batch_size=batch_size, - ): - end_info = ( - f"updated_ts={batch.batch_range.end.updated_ts}" - if isinstance(batch.batch_range.end, RowBoundary) - else f"limit={batch.batch_range.end.batch_size}" - ) - logger.info( - "Processing batch of size %s with end %s", - len(batch.expired_snapshot_ids), - end_info, - ) - snapshot_evaluator.cleanup( - target_snapshots=batch.cleanup_tasks, - on_complete=console.update_cleanup_progress if console else None, - ) - state_sync.delete_expired_snapshots( - batch_range=ExpiredBatchRange( - start=RowBoundary.lowest_boundary(), - end=batch.batch_range.end, - ), - ignore_ttl=ignore_ttl, - ) - logger.info("Cleaned up expired snapshots batch") - num_expired_snapshots += len(batch.expired_snapshot_ids) - logger.info("Cleaned up %s expired snapshots", num_expired_snapshots) diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py index 199ca43ee9..bd01dfc652 100644 --- a/tests/core/state_sync/test_state_sync.py +++ b/tests/core/state_sync/test_state_sync.py @@ -13,19 +13,17 @@ from sqlmesh.core import constants as c from sqlmesh.core.config import EnvironmentSuffixTarget -from sqlmesh.core.dialect import parse_one, schema_ +from sqlmesh.core.dialect import parse_one from sqlmesh.core.engine_adapter import create_engine_adapter from sqlmesh.core.environment import Environment, EnvironmentStatements from sqlmesh.core.model import ( FullKind, IncrementalByTimeRangeKind, - ModelKindName, Seed, SeedKind, SeedModel, SqlModel, ) -from sqlmesh.core.model.definition import ExternalModel from sqlmesh.core.snapshot import ( Snapshot, SnapshotChangeCategory, @@ -38,7 +36,6 @@ from sqlmesh.core.state_sync import ( CachingStateSync, EngineAdapterStateSync, - cleanup_expired_views, ) from sqlmesh.core.state_sync.base import ( SCHEMA_VERSION, @@ -1524,154 +1521,6 @@ def test_expired_batch_range_where_filter_with_limit(): ) -def test_delete_expired_snapshots_common_function_batching( - state_sync: EngineAdapterStateSync, make_snapshot: t.Callable, mocker: MockerFixture -): - """Test that the common delete_expired_snapshots function properly pages through batches and deletes them.""" - from sqlmesh.core.state_sync.common import delete_expired_snapshots - from sqlmesh.core.state_sync.common import ExpiredBatchRange, RowBoundary, LimitBoundary - from unittest.mock import MagicMock - - now_ts = now_timestamp() - - # Create 5 expired snapshots with different timestamps - snapshots = [] - for idx in range(5): - snapshot = make_snapshot( - SqlModel( - name=f"model_{idx}", - query=parse_one("select 1 as a, ds"), - ), - ) - snapshot.ttl = "in 10 seconds" - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot.updated_ts = now_ts - (20000 + idx * 1000) - snapshots.append(snapshot) - - state_sync.push_snapshots(snapshots) - - # Spy on get_expired_snapshots and delete_expired_snapshots methods - get_expired_spy = mocker.spy(state_sync, "get_expired_snapshots") - delete_expired_spy = mocker.spy(state_sync, "delete_expired_snapshots") - - # Mock snapshot evaluator - mock_evaluator = MagicMock() - mock_evaluator.cleanup = MagicMock() - - # Run delete_expired_snapshots with batch_size=2 - delete_expired_snapshots( - state_sync, - mock_evaluator, - current_ts=now_ts, - batch_size=2, - ) - - # Verify get_expired_snapshots was called the correct number of times: - # - 3 batches (2+2+1): each batch triggers 2 calls (one from iter_expired_snapshot_batches, one from delete_expired_snapshots) - # - Plus 1 final call that returns empty to exit the loop - # Total: 3 * 2 + 1 = 7 calls - assert get_expired_spy.call_count == 7 - - # Verify the progression of batch_range calls from the iter_expired_snapshot_batches loop - # (calls at indices 0, 2, 4, 6 are from iter_expired_snapshot_batches) - # (calls at indices 1, 3, 5 are from delete_expired_snapshots in facade.py) - calls = get_expired_spy.call_args_list - - # First call from iterator should have a batch_range starting from the beginning - first_call_kwargs = calls[0][1] - assert "batch_range" in first_call_kwargs - first_range = first_call_kwargs["batch_range"] - assert isinstance(first_range, ExpiredBatchRange) - assert isinstance(first_range.start, RowBoundary) - assert isinstance(first_range.end, LimitBoundary) - assert first_range.end.batch_size == 2 - assert first_range.start.updated_ts == 0 - assert first_range.start.name == "" - assert first_range.start.identifier == "" - - # Third call (second batch from iterator) should have a batch_range from the first batch's range - third_call_kwargs = calls[2][1] - assert "batch_range" in third_call_kwargs - second_range = third_call_kwargs["batch_range"] - assert isinstance(second_range, ExpiredBatchRange) - assert isinstance(second_range.start, RowBoundary) - assert isinstance(second_range.end, LimitBoundary) - assert second_range.end.batch_size == 2 - # Should have progressed from the first batch - assert second_range.start.updated_ts > 0 - assert second_range.start.name == '"model_3"' - - # Fifth call (third batch from iterator) should have a batch_range from the second batch's range - fifth_call_kwargs = calls[4][1] - assert "batch_range" in fifth_call_kwargs - third_range = fifth_call_kwargs["batch_range"] - assert isinstance(third_range, ExpiredBatchRange) - assert isinstance(third_range.start, RowBoundary) - assert isinstance(third_range.end, LimitBoundary) - assert third_range.end.batch_size == 2 - # Should have progressed from the second batch - assert third_range.start.updated_ts >= second_range.start.updated_ts - assert third_range.start.name == '"model_1"' - - # Seventh call (final call from iterator) should have a batch_range from the third batch's range - seventh_call_kwargs = calls[6][1] - assert "batch_range" in seventh_call_kwargs - fourth_range = seventh_call_kwargs["batch_range"] - assert isinstance(fourth_range, ExpiredBatchRange) - assert isinstance(fourth_range.start, RowBoundary) - assert isinstance(fourth_range.end, LimitBoundary) - assert fourth_range.end.batch_size == 2 - # Should have progressed from the third batch - assert fourth_range.start.updated_ts >= third_range.start.updated_ts - assert fourth_range.start.name == '"model_0"' - - # Verify delete_expired_snapshots was called 3 times (once per batch) - assert delete_expired_spy.call_count == 3 - - # Verify each delete call used a batch_range - delete_calls = delete_expired_spy.call_args_list - - # First call should have a batch_range matching the first batch - first_delete_kwargs = delete_calls[0][1] - assert "batch_range" in first_delete_kwargs - first_delete_range = first_delete_kwargs["batch_range"] - assert isinstance(first_delete_range, ExpiredBatchRange) - assert isinstance(first_delete_range.start, RowBoundary) - assert first_delete_range.start.updated_ts == 0 - assert isinstance(first_delete_range.end, RowBoundary) - assert first_delete_range.end.updated_ts == second_range.start.updated_ts - assert first_delete_range.end.name == second_range.start.name - assert first_delete_range.end.identifier == second_range.start.identifier - - second_delete_kwargs = delete_calls[1][1] - assert "batch_range" in second_delete_kwargs - second_delete_range = second_delete_kwargs["batch_range"] - assert isinstance(second_delete_range, ExpiredBatchRange) - assert isinstance(second_delete_range.start, RowBoundary) - assert second_delete_range.start.updated_ts == 0 - assert isinstance(second_delete_range.end, RowBoundary) - assert second_delete_range.end.updated_ts == third_range.start.updated_ts - assert second_delete_range.end.name == third_range.start.name - assert second_delete_range.end.identifier == third_range.start.identifier - - third_delete_kwargs = delete_calls[2][1] - assert "batch_range" in third_delete_kwargs - third_delete_range = third_delete_kwargs["batch_range"] - assert isinstance(third_delete_range, ExpiredBatchRange) - assert isinstance(third_delete_range.start, RowBoundary) - assert third_delete_range.start.updated_ts == 0 - assert isinstance(third_delete_range.end, RowBoundary) - assert third_delete_range.end.updated_ts == fourth_range.start.updated_ts - assert third_delete_range.end.name == fourth_range.start.name - assert third_delete_range.end.identifier == fourth_range.start.identifier - # Verify the cleanup method was called for each batch that had cleanup tasks - assert mock_evaluator.cleanup.call_count >= 1 - - # Verify all snapshots were deleted in the end - remaining = state_sync.get_snapshots(snapshots) - assert len(remaining) == 0 - - def test_delete_expired_snapshots_seed( state_sync: EngineAdapterStateSync, make_snapshot: t.Callable ): @@ -3089,105 +2938,6 @@ def test_cache(state_sync, make_snapshot, mocker): mock.assert_called() -def test_cleanup_expired_views( - mocker: MockerFixture, state_sync: EngineAdapterStateSync, make_snapshot: t.Callable -): - adapter = mocker.MagicMock() - adapter.dialect = None - snapshot_a = make_snapshot(SqlModel(name="catalog.schema.a", query=parse_one("select 1, ds"))) - snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot_b = make_snapshot(SqlModel(name="catalog.schema.b", query=parse_one("select 1, ds"))) - snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING) - # Make sure that we don't drop schemas from external models - snapshot_external_model = make_snapshot( - ExternalModel(name="catalog.external_schema.external_table", kind=ModelKindName.EXTERNAL) - ) - snapshot_external_model.categorize_as(SnapshotChangeCategory.BREAKING) - schema_environment = Environment( - name="test_environment", - suffix_target=EnvironmentSuffixTarget.SCHEMA, - snapshots=[ - snapshot_a.table_info, - snapshot_b.table_info, - snapshot_external_model.table_info, - ], - start_at="2022-01-01", - end_at="2022-01-01", - plan_id="test_plan_id", - previous_plan_id="test_plan_id", - catalog_name_override="catalog_override", - ) - snapshot_c = make_snapshot(SqlModel(name="catalog.schema.c", query=parse_one("select 1, ds"))) - snapshot_c.categorize_as(SnapshotChangeCategory.BREAKING) - snapshot_d = make_snapshot(SqlModel(name="catalog.schema.d", query=parse_one("select 1, ds"))) - snapshot_d.categorize_as(SnapshotChangeCategory.BREAKING) - table_environment = Environment( - name="test_environment", - suffix_target=EnvironmentSuffixTarget.TABLE, - snapshots=[ - snapshot_c.table_info, - snapshot_d.table_info, - snapshot_external_model.table_info, - ], - start_at="2022-01-01", - end_at="2022-01-01", - plan_id="test_plan_id", - previous_plan_id="test_plan_id", - catalog_name_override="catalog_override", - ) - cleanup_expired_views(adapter, {}, [schema_environment, table_environment]) - assert adapter.drop_schema.called - assert adapter.drop_view.called - assert adapter.drop_schema.call_args_list == [ - call( - schema_("schema__test_environment", "catalog_override"), - ignore_if_not_exists=True, - cascade=True, - ) - ] - assert sorted(adapter.drop_view.call_args_list) == [ - call("catalog_override.schema.c__test_environment", ignore_if_not_exists=True), - call("catalog_override.schema.d__test_environment", ignore_if_not_exists=True), - ] - - -@pytest.mark.parametrize( - "suffix_target", [EnvironmentSuffixTarget.SCHEMA, EnvironmentSuffixTarget.TABLE] -) -def test_cleanup_expired_environment_schema_warn_on_delete_failure( - mocker: MockerFixture, make_snapshot: t.Callable, suffix_target: EnvironmentSuffixTarget -): - adapter = mocker.MagicMock() - adapter.dialect = None - adapter.drop_schema.side_effect = Exception("Failed to drop the schema") - adapter.drop_view.side_effect = Exception("Failed to drop the view") - - snapshot = make_snapshot( - SqlModel(name="test_catalog.test_schema.test_model", query=parse_one("select 1, ds")) - ) - snapshot.categorize_as(SnapshotChangeCategory.BREAKING) - schema_environment = Environment( - name="test_environment", - suffix_target=suffix_target, - snapshots=[snapshot.table_info], - start_at="2022-01-01", - end_at="2022-01-01", - plan_id="test_plan_id", - previous_plan_id="test_plan_id", - catalog_name_override="catalog_override", - ) - - with pytest.raises(SQLMeshError, match="Failed to drop the expired environment .*"): - cleanup_expired_views(adapter, {}, [schema_environment], warn_on_delete_failure=False) - - cleanup_expired_views(adapter, {}, [schema_environment], warn_on_delete_failure=True) - - if suffix_target == EnvironmentSuffixTarget.SCHEMA: - assert adapter.drop_schema.called - else: - assert adapter.drop_view.called - - def test_max_interval_end_per_model( state_sync: EngineAdapterStateSync, make_snapshot: t.Callable ) -> None: diff --git a/tests/core/test_janitor.py b/tests/core/test_janitor.py new file mode 100644 index 0000000000..e5e209f2cc --- /dev/null +++ b/tests/core/test_janitor.py @@ -0,0 +1,282 @@ +import typing as t +from unittest.mock import call + +import pytest +from pytest_mock.plugin import MockerFixture + +from sqlmesh.core.config import EnvironmentSuffixTarget +from sqlmesh.core import constants as c +from sqlmesh.core.dialect import parse_one, schema_ +from sqlmesh.core.engine_adapter import create_engine_adapter +from sqlmesh.core.environment import Environment +from sqlmesh.core.model import ( + ModelKindName, + SqlModel, +) +from sqlmesh.core.model.definition import ExternalModel +from sqlmesh.core.snapshot import ( + SnapshotChangeCategory, +) +from sqlmesh.core.state_sync import ( + EngineAdapterStateSync, +) +from sqlmesh.core.janitor import cleanup_expired_views, delete_expired_snapshots +from sqlmesh.utils.date import now_timestamp +from sqlmesh.utils.errors import SQLMeshError + +pytestmark = pytest.mark.slow + + +@pytest.fixture +def state_sync(duck_conn, tmp_path): + state_sync = EngineAdapterStateSync( + create_engine_adapter(lambda: duck_conn, "duckdb"), + schema=c.SQLMESH, + cache_dir=tmp_path / c.CACHE, + ) + state_sync.migrate() + return state_sync + + +def test_cleanup_expired_views(mocker: MockerFixture, make_snapshot: t.Callable): + adapter = mocker.MagicMock() + adapter.dialect = None + snapshot_a = make_snapshot(SqlModel(name="catalog.schema.a", query=parse_one("select 1, ds"))) + snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot_b = make_snapshot(SqlModel(name="catalog.schema.b", query=parse_one("select 1, ds"))) + snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING) + # Make sure that we don't drop schemas from external models + snapshot_external_model = make_snapshot( + ExternalModel(name="catalog.external_schema.external_table", kind=ModelKindName.EXTERNAL) + ) + snapshot_external_model.categorize_as(SnapshotChangeCategory.BREAKING) + schema_environment = Environment( + name="test_environment", + suffix_target=EnvironmentSuffixTarget.SCHEMA, + snapshots=[ + snapshot_a.table_info, + snapshot_b.table_info, + snapshot_external_model.table_info, + ], + start_at="2022-01-01", + end_at="2022-01-01", + plan_id="test_plan_id", + previous_plan_id="test_plan_id", + catalog_name_override="catalog_override", + ) + snapshot_c = make_snapshot(SqlModel(name="catalog.schema.c", query=parse_one("select 1, ds"))) + snapshot_c.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot_d = make_snapshot(SqlModel(name="catalog.schema.d", query=parse_one("select 1, ds"))) + snapshot_d.categorize_as(SnapshotChangeCategory.BREAKING) + table_environment = Environment( + name="test_environment", + suffix_target=EnvironmentSuffixTarget.TABLE, + snapshots=[ + snapshot_c.table_info, + snapshot_d.table_info, + snapshot_external_model.table_info, + ], + start_at="2022-01-01", + end_at="2022-01-01", + plan_id="test_plan_id", + previous_plan_id="test_plan_id", + catalog_name_override="catalog_override", + ) + cleanup_expired_views(adapter, {}, [schema_environment, table_environment]) + assert adapter.drop_schema.called + assert adapter.drop_view.called + assert adapter.drop_schema.call_args_list == [ + call( + schema_("schema__test_environment", "catalog_override"), + ignore_if_not_exists=True, + cascade=True, + ) + ] + assert sorted(adapter.drop_view.call_args_list) == [ + call("catalog_override.schema.c__test_environment", ignore_if_not_exists=True), + call("catalog_override.schema.d__test_environment", ignore_if_not_exists=True), + ] + + +@pytest.mark.parametrize( + "suffix_target", [EnvironmentSuffixTarget.SCHEMA, EnvironmentSuffixTarget.TABLE] +) +def test_cleanup_expired_environment_schema_warn_on_delete_failure( + mocker: MockerFixture, make_snapshot: t.Callable, suffix_target: EnvironmentSuffixTarget +): + adapter = mocker.MagicMock() + adapter.dialect = None + adapter.drop_schema.side_effect = Exception("Failed to drop the schema") + adapter.drop_view.side_effect = Exception("Failed to drop the view") + + snapshot = make_snapshot( + SqlModel(name="test_catalog.test_schema.test_model", query=parse_one("select 1, ds")) + ) + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + schema_environment = Environment( + name="test_environment", + suffix_target=suffix_target, + snapshots=[snapshot.table_info], + start_at="2022-01-01", + end_at="2022-01-01", + plan_id="test_plan_id", + previous_plan_id="test_plan_id", + catalog_name_override="catalog_override", + ) + + with pytest.raises(SQLMeshError, match="Failed to drop the expired environment .*"): + cleanup_expired_views(adapter, {}, [schema_environment], warn_on_delete_failure=False) + + cleanup_expired_views(adapter, {}, [schema_environment], warn_on_delete_failure=True) + + if suffix_target == EnvironmentSuffixTarget.SCHEMA: + assert adapter.drop_schema.called + else: + assert adapter.drop_view.called + + +def test_delete_expired_snapshots_common_function_batching( + state_sync: EngineAdapterStateSync, make_snapshot: t.Callable, mocker: MockerFixture +): + """Test that the common delete_expired_snapshots function properly pages through batches and deletes them.""" + from sqlmesh.core.state_sync.common import ExpiredBatchRange, RowBoundary, LimitBoundary + from unittest.mock import MagicMock + + now_ts = now_timestamp() + + # Create 5 expired snapshots with different timestamps + snapshots = [] + for idx in range(5): + snapshot = make_snapshot( + SqlModel( + name=f"model_{idx}", + query=parse_one("select 1 as a, ds"), + ), + ) + snapshot.ttl = "in 10 seconds" + snapshot.categorize_as(SnapshotChangeCategory.BREAKING) + snapshot.updated_ts = now_ts - (20000 + idx * 1000) + snapshots.append(snapshot) + + state_sync.push_snapshots(snapshots) + + # Spy on get_expired_snapshots and delete_expired_snapshots methods + get_expired_spy = mocker.spy(state_sync, "get_expired_snapshots") + delete_expired_spy = mocker.spy(state_sync, "delete_expired_snapshots") + + # Mock snapshot evaluator + mock_evaluator = MagicMock() + mock_evaluator.cleanup = MagicMock() + + # Run delete_expired_snapshots with batch_size=2 + delete_expired_snapshots( + state_sync, + mock_evaluator, + current_ts=now_ts, + batch_size=2, + ) + + # Verify get_expired_snapshots was called the correct number of times: + # - 3 batches (2+2+1): each batch triggers 2 calls (one from iter_expired_snapshot_batches, one from delete_expired_snapshots) + # - Plus 1 final call that returns empty to exit the loop + # Total: 3 * 2 + 1 = 7 calls + assert get_expired_spy.call_count == 7 + + # Verify the progression of batch_range calls from the iter_expired_snapshot_batches loop + # (calls at indices 0, 2, 4, 6 are from iter_expired_snapshot_batches) + # (calls at indices 1, 3, 5 are from delete_expired_snapshots in facade.py) + calls = get_expired_spy.call_args_list + + # First call from iterator should have a batch_range starting from the beginning + first_call_kwargs = calls[0][1] + assert "batch_range" in first_call_kwargs + first_range = first_call_kwargs["batch_range"] + assert isinstance(first_range, ExpiredBatchRange) + assert isinstance(first_range.start, RowBoundary) + assert isinstance(first_range.end, LimitBoundary) + assert first_range.end.batch_size == 2 + assert first_range.start.updated_ts == 0 + assert first_range.start.name == "" + assert first_range.start.identifier == "" + + # Third call (second batch from iterator) should have a batch_range from the first batch's range + third_call_kwargs = calls[2][1] + assert "batch_range" in third_call_kwargs + second_range = third_call_kwargs["batch_range"] + assert isinstance(second_range, ExpiredBatchRange) + assert isinstance(second_range.start, RowBoundary) + assert isinstance(second_range.end, LimitBoundary) + assert second_range.end.batch_size == 2 + # Should have progressed from the first batch + assert second_range.start.updated_ts > 0 + assert second_range.start.name == '"model_3"' + + # Fifth call (third batch from iterator) should have a batch_range from the second batch's range + fifth_call_kwargs = calls[4][1] + assert "batch_range" in fifth_call_kwargs + third_range = fifth_call_kwargs["batch_range"] + assert isinstance(third_range, ExpiredBatchRange) + assert isinstance(third_range.start, RowBoundary) + assert isinstance(third_range.end, LimitBoundary) + assert third_range.end.batch_size == 2 + # Should have progressed from the second batch + assert third_range.start.updated_ts >= second_range.start.updated_ts + assert third_range.start.name == '"model_1"' + + # Seventh call (final call from iterator) should have a batch_range from the third batch's range + seventh_call_kwargs = calls[6][1] + assert "batch_range" in seventh_call_kwargs + fourth_range = seventh_call_kwargs["batch_range"] + assert isinstance(fourth_range, ExpiredBatchRange) + assert isinstance(fourth_range.start, RowBoundary) + assert isinstance(fourth_range.end, LimitBoundary) + assert fourth_range.end.batch_size == 2 + # Should have progressed from the third batch + assert fourth_range.start.updated_ts >= third_range.start.updated_ts + assert fourth_range.start.name == '"model_0"' + + # Verify delete_expired_snapshots was called 3 times (once per batch) + assert delete_expired_spy.call_count == 3 + + # Verify each delete call used a batch_range + delete_calls = delete_expired_spy.call_args_list + + # First call should have a batch_range matching the first batch + first_delete_kwargs = delete_calls[0][1] + assert "batch_range" in first_delete_kwargs + first_delete_range = first_delete_kwargs["batch_range"] + assert isinstance(first_delete_range, ExpiredBatchRange) + assert isinstance(first_delete_range.start, RowBoundary) + assert first_delete_range.start.updated_ts == 0 + assert isinstance(first_delete_range.end, RowBoundary) + assert first_delete_range.end.updated_ts == second_range.start.updated_ts + assert first_delete_range.end.name == second_range.start.name + assert first_delete_range.end.identifier == second_range.start.identifier + + second_delete_kwargs = delete_calls[1][1] + assert "batch_range" in second_delete_kwargs + second_delete_range = second_delete_kwargs["batch_range"] + assert isinstance(second_delete_range, ExpiredBatchRange) + assert isinstance(second_delete_range.start, RowBoundary) + assert second_delete_range.start.updated_ts == 0 + assert isinstance(second_delete_range.end, RowBoundary) + assert second_delete_range.end.updated_ts == third_range.start.updated_ts + assert second_delete_range.end.name == third_range.start.name + assert second_delete_range.end.identifier == third_range.start.identifier + + third_delete_kwargs = delete_calls[2][1] + assert "batch_range" in third_delete_kwargs + third_delete_range = third_delete_kwargs["batch_range"] + assert isinstance(third_delete_range, ExpiredBatchRange) + assert isinstance(third_delete_range.start, RowBoundary) + assert third_delete_range.start.updated_ts == 0 + assert isinstance(third_delete_range.end, RowBoundary) + assert third_delete_range.end.updated_ts == fourth_range.start.updated_ts + assert third_delete_range.end.name == fourth_range.start.name + assert third_delete_range.end.identifier == fourth_range.start.identifier + # Verify the cleanup method was called for each batch that had cleanup tasks + assert mock_evaluator.cleanup.call_count >= 1 + + # Verify all snapshots were deleted in the end + remaining = state_sync.get_snapshots(snapshots) + assert len(remaining) == 0 From b866d33838d016fddf754f91ae26bfb7c536b7af Mon Sep 17 00:00:00 2001 From: Trey Spiller <1831878+treysp@users.noreply.github.com> Date: Wed, 8 Oct 2025 14:08:21 -0500 Subject: [PATCH 0959/1056] Fix: track shadowed jinja variable assignments correctly (#5503) --- sqlmesh/utils/jinja.py | 10 ++++++++- tests/dbt/test_manifest.py | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/sqlmesh/utils/jinja.py b/sqlmesh/utils/jinja.py index 59e9f6dd2f..240b183391 100644 --- a/sqlmesh/utils/jinja.py +++ b/sqlmesh/utils/jinja.py @@ -133,6 +133,12 @@ def find_call_names(node: nodes.Node, vars_in_scope: t.Set[str]) -> t.Iterator[C vars_in_scope = vars_in_scope.copy() for child_node in node.iter_child_nodes(): if "target" in child_node.fields: + # For nodes with assignment targets (Assign, AssignBlock, For, Import), + # the target name could shadow a reference in the right hand side. + # So we need to process the RHS before adding the target to scope. + # For example: {% set model = model.path %} should track model.path. + yield from find_call_names(child_node, vars_in_scope) + target = getattr(child_node, "target") if isinstance(target, nodes.Name): vars_in_scope.add(target.name) @@ -149,7 +155,9 @@ def find_call_names(node: nodes.Node, vars_in_scope: t.Set[str]) -> t.Iterator[C name = call_name(child_node) if name[0][0] != "'" and name[0] not in vars_in_scope: yield (name, child_node) - yield from find_call_names(child_node, vars_in_scope) + + if "target" not in child_node.fields: + yield from find_call_names(child_node, vars_in_scope) def extract_call_names( diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index e2e7bc706c..2ecf8b8980 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -324,3 +324,46 @@ def test_macro_depenency_none_str(): # "None" macro shouldn't raise a KeyError _macro_references(helper._manifest, node) + + +@pytest.mark.xdist_group("dbt_manifest") +def test_macro_assignment_shadowing(create_empty_project): + project_name = "local" + project_path, models_path = create_empty_project(project_name=project_name) + + macros_path = project_path / "macros" + macros_path.mkdir() + + (macros_path / "model_path_macro.sql").write_text(""" +{% macro model_path_macro() %} + {% if execute %} + {% set model = model.path.split('/')[-1].replace('.sql', '') %} + SELECT '{{ model }}' as model_name + {% else %} + SELECT 'placeholder' as placeholder + {% endif %} +{% endmacro %} +""") + + (models_path / "model_using_path_macro.sql").write_text(""" +{{ model_path_macro() }} +""") + + context = DbtContext(project_path) + profile = Profile.load(context) + + helper = ManifestHelper( + project_path, + project_path, + project_name, + profile.target, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), + ) + + macros = helper.macros(project_name) + assert "model_path_macro" in macros + assert "path" in macros["model_path_macro"].dependencies.model_attrs.attrs + + models = helper.models() + assert "model_using_path_macro" in models + assert "path" in models["model_using_path_macro"].dependencies.model_attrs.attrs From c8bee084ad5a9658f1f9ea186a40c1435eec6bc4 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 8 Oct 2025 22:36:27 +0300 Subject: [PATCH 0960/1056] Fix: Only keep refs and sources that exist to match dbt load time behaviour (#5509) --- sqlmesh/dbt/basemodel.py | 4 +++ tests/dbt/test_model.py | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/sqlmesh/dbt/basemodel.py b/sqlmesh/dbt/basemodel.py index 0c719ebb88..32a76aba13 100644 --- a/sqlmesh/dbt/basemodel.py +++ b/sqlmesh/dbt/basemodel.py @@ -317,6 +317,10 @@ def sqlmesh_model_kwargs( dependencies = dependencies.union(custom_mat.dependencies) model_dialect = self.dialect(context) + + # Only keep refs and sources that exist in the context to match dbt behavior + dependencies.refs.intersection_update(context.refs) + dependencies.sources.intersection_update(context.sources) model_context = context.context_for_dependencies( dependencies.union(self.tests_ref_source_dependencies) ) diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index eb16a4b4b1..797d638858 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -18,6 +18,7 @@ from sqlmesh.dbt.test import TestConfig from sqlmesh.utils.yaml import YAML from sqlmesh.utils.date import to_ds +import typing as t pytestmark = pytest.mark.dbt @@ -1028,3 +1029,58 @@ def test_ephemeral_model_ignores_grants() -> None: assert sqlmesh_model.kind.is_embedded assert sqlmesh_model.grants is None # grants config is skipped for ephemeral / embedded models + + +def test_conditional_ref_in_unexecuted_branch(copy_to_temp_path: t.Callable): + path = copy_to_temp_path("tests/fixtures/dbt/sushi_test") + temp_project = path[0] + + models_dir = temp_project / "models" + models_dir.mkdir(parents=True, exist_ok=True) + + test_model_content = """ +{{ config( + materialized='table', +) }} + +{% if true %} + WITH source AS ( + SELECT * + FROM {{ ref('simple_model_a') }} + ) +{% else %} + WITH source AS ( + SELECT * + FROM {{ ref('nonexistent_model') }} -- this doesn't exist but is in unexecuted branch + ) +{% endif %} + +SELECT * FROM source +""".strip() + + (models_dir / "conditional_ref_model.sql").write_text(test_model_content) + sushi_context = Context(paths=[str(temp_project)]) + + # the model should load successfully without raising MissingModelError + model = sushi_context.get_model("sushi.conditional_ref_model") + assert model is not None + + # Verify only the executed ref is in the dependencies + assert len(model.depends_on) == 1 + assert '"memory"."sushi"."simple_model_a"' in model.depends_on + + # Also the model can be rendered successfully with the executed ref + rendered = model.render_query() + assert rendered is not None + assert ( + rendered.sql() + == 'WITH "source" AS (SELECT "simple_model_a"."a" AS "a" FROM "memory"."sushi"."simple_model_a" AS "simple_model_a") SELECT "source"."a" AS "a" FROM "source" AS "source"' + ) + + # And run plan with this conditional model for good measure + plan = sushi_context.plan(select_models=["sushi.conditional_ref_model", "sushi.simple_model_a"]) + sushi_context.apply(plan) + upstream_ref = sushi_context.engine_adapter.fetchone("SELECT * FROM sushi.simple_model_a") + assert upstream_ref == (1,) + result = sushi_context.engine_adapter.fetchone("SELECT * FROM sushi.conditional_ref_model") + assert result == (1,) From 189208ecb41c99efca973f063a467713373f3116 Mon Sep 17 00:00:00 2001 From: David Dai Date: Wed, 8 Oct 2025 13:44:03 -0700 Subject: [PATCH 0961/1056] fix: add default (empty) tags to dbt builtin globals config (#5506) --- sqlmesh/dbt/builtin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 145e29a96c..c1105a2981 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -482,7 +482,7 @@ def create_builtin_globals( if variables is not None: builtin_globals["var"] = Var(variables) - builtin_globals["config"] = Config(jinja_globals.pop("config", {})) + builtin_globals["config"] = Config(jinja_globals.pop("config", {"tags": []})) deployability_index = ( jinja_globals.get("deployability_index") or DeployabilityIndex.all_deployable() From 6919e692c486a3498f89bb553b538e3a20a1bfe7 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Wed, 8 Oct 2025 15:47:19 -0700 Subject: [PATCH 0962/1056] fix(web_common): add more generic type for cll components (#5505) --- pnpm-lock.yaml | 45 ++-- web/common/package.json | 3 - .../ColumnLevelLineageContext.ts | 91 +++---- .../LineageColumnLevel/FactoryColumn.tsx | 21 +- .../Lineage/LineageColumnLevel/help.ts | 78 +++--- .../useColumnLevelLineage.ts | 23 +- .../src/components/Lineage/LineageContext.ts | 62 ++++- .../src/components/Lineage/LineageLayout.tsx | 10 +- .../components/Lineage/LineageLayoutBase.tsx | 87 +++++-- .../Lineage/edge/FactoryEdgeWithGradient.tsx | 10 +- web/common/src/components/Lineage/help.ts | 61 +++-- .../components/Lineage/layout/dagreLayout.ts | 15 +- .../src/components/Lineage/layout/help.ts | 43 +++- .../components/Lineage/node/NodeHandle.tsx | 7 +- .../components/Lineage/node/NodeHandles.tsx | 16 +- .../src/components/Lineage/node/NodePort.tsx | 24 +- .../components/Lineage/node/base-handle.tsx | 1 - .../Lineage/stories/Lineage.stories.tsx | 228 +++++++++--------- .../Lineage/stories/ModelLineage.tsx | 171 +++++++------ .../Lineage/stories/ModelLineageContext.ts | 59 ++++- .../Lineage/stories/ModelNodeColumn.tsx | 11 +- web/common/src/components/Lineage/utils.ts | 54 +++-- web/common/src/index.ts | 2 + web/common/src/types.ts | 40 ++- 24 files changed, 740 insertions(+), 422 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fec93a8f3..aeacb362d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -435,9 +435,6 @@ importers: '@types/dagre': specifier: 0.7.53 version: 0.7.53 - '@types/lodash': - specifier: 4.17.20 - version: 4.17.20 '@types/node': specifier: 20.11.25 version: 20.11.25 @@ -495,9 +492,6 @@ importers: globals: specifier: 16.3.0 version: 16.3.0 - lodash: - specifier: 4.17.21 - version: 4.17.21 lucide-react: specifier: 0.542.0 version: 0.542.0(react@18.3.1) @@ -2662,9 +2656,6 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - '@types/lodash@4.17.20': - resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -7064,7 +7055,7 @@ snapshots: '@babel/traverse': 7.28.0 '@babel/types': 7.28.1 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -7232,7 +7223,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/types': 7.28.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -7489,7 +7480,7 @@ snapshots: '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -7503,7 +7494,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -9425,8 +9416,6 @@ snapshots: dependencies: '@types/node': 20.11.25 - '@types/lodash@4.17.20': {} - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -9502,7 +9491,7 @@ snapshots: '@typescript-eslint/types': 8.38.0 '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.38.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 eslint: 9.31.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: @@ -9512,7 +9501,7 @@ snapshots: 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) + debug: 4.4.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -9531,7 +9520,7 @@ snapshots: '@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(supports-color@8.1.1) + debug: 4.4.1 eslint: 9.31.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -9546,7 +9535,7 @@ snapshots: '@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) + debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -10644,6 +10633,10 @@ snapshots: de-indent@1.0.2: {} + debug@4.4.1: + dependencies: + ms: 2.1.3 + debug@4.4.1(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -10916,7 +10909,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.25.8): dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 esbuild: 0.25.8 transitivePeerDependencies: - supports-color @@ -10999,7 +10992,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -11410,7 +11403,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -11419,7 +11412,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -13992,7 +13985,7 @@ snapshots: 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): dependencies: cac: 6.7.14 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.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) @@ -14042,7 +14035,7 @@ snapshots: '@volar/typescript': 2.4.23 '@vue/language-core': 2.2.0(typescript@5.8.3) compare-versions: 6.1.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 kolorist: 1.8.0 local-pkg: 1.1.1 magic-string: 0.30.17 @@ -14108,7 +14101,7 @@ snapshots: '@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 expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 diff --git a/web/common/package.json b/web/common/package.json index ef91337174..6a0965f19e 100644 --- a/web/common/package.json +++ b/web/common/package.json @@ -14,7 +14,6 @@ "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "@types/dagre": "0.7.53", - "@types/lodash": "4.17.20", "@types/node": "20.11.25", "@types/react": "18.3.23", "@types/react-dom": "18.3.7", @@ -34,7 +33,6 @@ "eslint-plugin-storybook": "9.1.5", "fuse.js": "7.1.0", "globals": "16.3.0", - "lodash": "4.17.21", "lucide-react": "0.542.0", "playwright": "1.54.1", "postcss": "8.5.6", @@ -95,7 +93,6 @@ "dagre": "0.8.5", "deepmerge": "4.3.1", "fuse.js": "7.1.0", - "lodash": "4.17.21", "lucide-react": "0.542.0", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/web/common/src/components/Lineage/LineageColumnLevel/ColumnLevelLineageContext.ts b/web/common/src/components/Lineage/LineageColumnLevel/ColumnLevelLineageContext.ts index 227fc70394..4dd6ca93ef 100644 --- a/web/common/src/components/Lineage/LineageColumnLevel/ColumnLevelLineageContext.ts +++ b/web/common/src/components/Lineage/LineageColumnLevel/ColumnLevelLineageContext.ts @@ -2,64 +2,36 @@ import React from 'react' import { type PortId } from '../utils' -export type LineageColumn = { - source?: string | null - expression?: string | null - models: Record -} - -export type ColumnLevelModelConnections< - TAdjacencyListKey extends string, - TAdjacencyListColumnKey extends string, -> = Record -export type ColumnLevelDetails< - TAdjacencyListKey extends string, - TAdjacencyListColumnKey extends string, -> = Omit & { - models: ColumnLevelModelConnections< - TAdjacencyListKey, - TAdjacencyListColumnKey - > -} -export type ColumnLevelConnections< - TAdjacencyListKey extends string, - TAdjacencyListColumnKey extends string, -> = Record< - TAdjacencyListColumnKey, - ColumnLevelDetails -> export type ColumnLevelLineageAdjacencyList< TAdjacencyListKey extends string, TAdjacencyListColumnKey extends string, -> = Record< - TAdjacencyListKey, - ColumnLevelConnections -> +> = { + [K in TAdjacencyListKey]: { + [C in TAdjacencyListColumnKey]: { + source?: string | null + expression?: string | null + models: Record + } + } +} export type ColumnLevelLineageContextValue< TAdjacencyListKey extends string, TAdjacencyListColumnKey extends string, TColumnID extends string = PortId, -> = { - adjacencyListColumnLevel: ColumnLevelLineageAdjacencyList< + TColumnLevelLineageAdjacencyList extends ColumnLevelLineageAdjacencyList< TAdjacencyListKey, TAdjacencyListColumnKey - > + > = ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + >, +> = { + adjacencyListColumnLevel: TColumnLevelLineageAdjacencyList selectedColumns: Set - columnLevelLineage: Map< - TColumnID, - ColumnLevelLineageAdjacencyList - > + columnLevelLineage: Map setColumnLevelLineage: React.Dispatch< - React.SetStateAction< - Map< - TColumnID, - ColumnLevelLineageAdjacencyList< - TAdjacencyListKey, - TAdjacencyListColumnKey - > - > - > + React.SetStateAction> > showColumns: boolean setShowColumns: React.Dispatch> @@ -71,16 +43,17 @@ export function getColumnLevelLineageContextInitial< TAdjacencyListKey extends string, TAdjacencyListColumnKey extends string, TColumnID extends string = PortId, + TColumnLevelLineageAdjacencyList extends ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > = ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + >, >() { return { - adjacencyListColumnLevel: {}, - columnLevelLineage: new Map< - TColumnID, - ColumnLevelLineageAdjacencyList< - TAdjacencyListKey, - TAdjacencyListColumnKey - > - >(), + adjacencyListColumnLevel: {} as TColumnLevelLineageAdjacencyList, + columnLevelLineage: new Map(), setColumnLevelLineage: () => {}, showColumns: false, setShowColumns: () => {}, @@ -94,8 +67,16 @@ export type ColumnLevelLineageContextHook< TAdjacencyListKey extends string, TAdjacencyListColumnKey extends string, TColumnID extends string = PortId, + TColumnLevelLineageAdjacencyList extends ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > = ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + >, > = () => ColumnLevelLineageContextValue< TAdjacencyListKey, TAdjacencyListColumnKey, - TColumnID + TColumnID, + TColumnLevelLineageAdjacencyList > diff --git a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx index 90def0f5ea..350437c16e 100644 --- a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx +++ b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx @@ -10,7 +10,7 @@ import React from 'react' import { cn } from '@/utils' import { NodeBadge } from '../node/NodeBadge' import { NodePort } from '../node/NodePort' -import { type NodeId, type PortId } from '../utils' +import { type NodeId, type PortHandleId, type PortId } from '../utils' import { type ColumnLevelLineageAdjacencyList, type ColumnLevelLineageContextHook, @@ -28,11 +28,21 @@ export function FactoryColumn< TAdjacencyListColumnKey extends string, TNodeID extends string = NodeId, TColumnID extends string = PortId, + TLeftPortHandleId extends string = PortHandleId, + TRightPortHandleId extends string = PortHandleId, + TColumnLevelLineageAdjacencyList extends ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > = ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + >, >( useLineage: ColumnLevelLineageContextHook< TAdjacencyListKey, TAdjacencyListColumnKey, - TColumnID + TColumnID, + TColumnLevelLineageAdjacencyList >, ) { return React.memo(function FactoryColumn({ @@ -59,10 +69,7 @@ export function FactoryColumn< type: string description?: string | null className?: string - data?: ColumnLevelLineageAdjacencyList< - TAdjacencyListKey, - TAdjacencyListColumnKey - > + data?: TColumnLevelLineageAdjacencyList isFetching?: boolean error?: Error | null renderError?: (error: Error) => React.ReactNode @@ -248,7 +255,7 @@ export function FactoryColumn< } return isSelectedColumn ? ( - id={id} nodeId={nodeId} className={cn( diff --git a/web/common/src/components/Lineage/LineageColumnLevel/help.ts b/web/common/src/components/Lineage/LineageColumnLevel/help.ts index 30115450cd..fe75ed162a 100644 --- a/web/common/src/components/Lineage/LineageColumnLevel/help.ts +++ b/web/common/src/components/Lineage/LineageColumnLevel/help.ts @@ -9,28 +9,26 @@ import { type PortId, type TransformEdgeFn, } from '../utils' -import { - type ColumnLevelConnections, - type ColumnLevelDetails, - type ColumnLevelLineageAdjacencyList, -} from './ColumnLevelLineageContext' +import { type ColumnLevelLineageAdjacencyList } from './ColumnLevelLineageContext' export const MAX_COLUMNS_TO_DISPLAY = 5 export function getAdjacencyListKeysFromColumnLineage< TAdjacencyListKey extends string, TAdjacencyListColumnKey extends string, ->( - columnLineage: ColumnLevelLineageAdjacencyList< + TColumnLevelLineageAdjacencyList extends ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > = ColumnLevelLineageAdjacencyList< TAdjacencyListKey, TAdjacencyListColumnKey >, -) { +>(columnLineage: TColumnLevelLineageAdjacencyList) { const adjacencyListKeys = new Set() const targets = Object.entries(columnLineage) as [ TAdjacencyListKey, - ColumnLevelConnections, + TColumnLevelLineageAdjacencyList[TAdjacencyListKey], ][] for (const [sourceModelName, targetColumns] of targets) { @@ -38,7 +36,7 @@ export function getAdjacencyListKeysFromColumnLineage< const targetConnections = Object.entries(targetColumns) as [ TAdjacencyListColumnKey, - ColumnLevelDetails, + TColumnLevelLineageAdjacencyList[TAdjacencyListKey][TAdjacencyListColumnKey], ][] for (const [, { models: sourceModels }] of targetConnections) { @@ -58,32 +56,52 @@ export function getEdgesFromColumnLineage< TAdjacencyListColumnKey extends string, TEdgeData extends LineageEdgeData = LineageEdgeData, TEdgeID extends string = EdgeId, - TNodeID extends string = NodeId, - TPortID extends string = PortId, + TSourceID extends string = NodeId, + TTargetID extends string = NodeId, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, + TColumnLevelLineageAdjacencyList extends ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > = ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + >, >({ columnLineage, transformEdge, }: { - columnLineage: ColumnLevelLineageAdjacencyList< - TAdjacencyListKey, - TAdjacencyListColumnKey + columnLineage: TColumnLevelLineageAdjacencyList + transformEdge: TransformEdgeFn< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID > - transformEdge: TransformEdgeFn }) { - const edges: LineageEdge[] = [] - const modelLevelEdgeIDs = new Map() + const edges: LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + >[] = [] + const modelLevelEdgeIDs = new Map() const targets = Object.entries(columnLineage || {}) as [ TAdjacencyListKey, - ColumnLevelConnections, + TColumnLevelLineageAdjacencyList[TAdjacencyListKey], ][] for (const [targetModelName, targetColumns] of targets) { const targetConnections = Object.entries(targetColumns) as [ TAdjacencyListColumnKey, - ColumnLevelDetails, + TColumnLevelLineageAdjacencyList[TAdjacencyListKey][TAdjacencyListColumnKey], ][] - const targetNodeId = toNodeID(targetModelName) + const targetNodeId = toNodeID(targetModelName) for (const [ targetColumnName, @@ -95,7 +113,7 @@ export function getEdgesFromColumnLineage< ][] for (const [sourceModelName, sourceColumns] of sources) { - const sourceNodeId = toNodeID(sourceModelName) + const sourceNodeId = toNodeID(sourceModelName) modelLevelEdgeIDs.set( toEdgeID(sourceModelName, targetModelName), @@ -109,11 +127,11 @@ export function getEdgesFromColumnLineage< targetModelName, targetColumnName, ) - const sourceColumnId = toPortID( + const sourceColumnId = toPortID( sourceModelName, sourceColumnName, ) - const targetColumnId = toPortID( + const targetColumnId = toPortID( targetModelName, targetColumnName, ) @@ -145,22 +163,24 @@ export function getConnectedColumnsIDs< TAdjacencyListKey extends string, TAdjacencyListColumnKey extends string, TColumnID extends string = PortId, ->( - adjacencyList: ColumnLevelLineageAdjacencyList< + TColumnLevelLineageAdjacencyList extends ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > = ColumnLevelLineageAdjacencyList< TAdjacencyListKey, TAdjacencyListColumnKey >, -) { +>(adjacencyList: TColumnLevelLineageAdjacencyList) { const connectedColumns = new Set() const targets = Object.entries(adjacencyList) as [ TAdjacencyListKey, - ColumnLevelConnections, + TColumnLevelLineageAdjacencyList[TAdjacencyListKey], ][] for (const [sourceModelName, targetColumns] of targets) { const targetConnections = Object.entries(targetColumns) as [ TAdjacencyListColumnKey, - ColumnLevelDetails, + TColumnLevelLineageAdjacencyList[TAdjacencyListKey][TAdjacencyListColumnKey], ][] for (const [ diff --git a/web/common/src/components/Lineage/LineageColumnLevel/useColumnLevelLineage.ts b/web/common/src/components/Lineage/LineageColumnLevel/useColumnLevelLineage.ts index da1a6b8ee8..53032c2c12 100644 --- a/web/common/src/components/Lineage/LineageColumnLevel/useColumnLevelLineage.ts +++ b/web/common/src/components/Lineage/LineageColumnLevel/useColumnLevelLineage.ts @@ -12,19 +12,18 @@ export function useColumnLevelLineage< TAdjacencyListKey extends string, TAdjacencyListColumnKey extends string, TColumnID extends string = PortId, ->( - columnLevelLineage: Map< - TColumnID, - ColumnLevelLineageAdjacencyList + TColumnLevelLineageAdjacencyList extends ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey + > = ColumnLevelLineageAdjacencyList< + TAdjacencyListKey, + TAdjacencyListColumnKey >, -) { +>(columnLevelLineage: Map) { const adjacencyListColumnLevel = React.useMemo(() => { return merge.all(Array.from(columnLevelLineage.values()), { arrayMerge: (dest, source) => Array.from(new Set([...dest, ...source])), - }) as ColumnLevelLineageAdjacencyList< - TAdjacencyListKey, - TAdjacencyListColumnKey - > + }) as TColumnLevelLineageAdjacencyList }, [columnLevelLineage]) const selectedColumns = React.useMemo(() => { @@ -37,7 +36,11 @@ export function useColumnLevelLineage< const adjacencyListKeysColumnLevel = React.useMemo(() => { return adjacencyListColumnLevel != null - ? getAdjacencyListKeysFromColumnLineage(adjacencyListColumnLevel) + ? getAdjacencyListKeysFromColumnLineage< + TAdjacencyListKey, + TAdjacencyListColumnKey, + TColumnLevelLineageAdjacencyList + >(adjacencyListColumnLevel) : [] }, [adjacencyListColumnLevel]) diff --git a/web/common/src/components/Lineage/LineageContext.ts b/web/common/src/components/Lineage/LineageContext.ts index 9da54dcbee..4a90031217 100644 --- a/web/common/src/components/Lineage/LineageContext.ts +++ b/web/common/src/components/Lineage/LineageContext.ts @@ -17,7 +17,10 @@ export interface LineageContextValue< TEdgeData extends LineageEdgeData = LineageEdgeData, TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = TNodeID, + TTargetID extends string = TNodeID, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, > { // Node selection showOnlySelectedNodes: boolean @@ -34,9 +37,25 @@ export interface LineageContextValue< setZoom: React.Dispatch> // Nodes and Edges - edges: LineageEdge[] + edges: LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + >[] setEdges: React.Dispatch< - React.SetStateAction[]> + React.SetStateAction< + LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + >[] + > > nodes: LineageNode[] nodesMap: LineageNodesMap @@ -73,22 +92,49 @@ export type LineageContextHook< TEdgeData extends LineageEdgeData = LineageEdgeData, TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, -> = () => LineageContextValue + TSourceID extends string = TNodeID, + TTargetID extends string = TNodeID, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, +> = () => LineageContextValue< + TNodeData, + TEdgeData, + TNodeID, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID +> export function createLineageContext< TNodeData extends LineageNodeData = LineageNodeData, TEdgeData extends LineageEdgeData = LineageEdgeData, TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = TNodeID, + TTargetID extends string = TNodeID, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, TLineageContextValue extends LineageContextValue< TNodeData, TEdgeData, TNodeID, TEdgeID, - TPortID - > = LineageContextValue, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + > = LineageContextValue< + TNodeData, + TEdgeData, + TNodeID, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + >, >(initial: TLineageContextValue) { const LineageContext = React.createContext(initial) diff --git a/web/common/src/components/Lineage/LineageLayout.tsx b/web/common/src/components/Lineage/LineageLayout.tsx index 2ab4a34879..a9b5ec512f 100644 --- a/web/common/src/components/Lineage/LineageLayout.tsx +++ b/web/common/src/components/Lineage/LineageLayout.tsx @@ -26,7 +26,10 @@ export function LineageLayout< TEdgeData extends LineageEdgeData = LineageEdgeData, TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = TNodeID, + TTargetID extends string = TNodeID, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, >({ nodeTypes, edgeTypes, @@ -44,7 +47,10 @@ export function LineageLayout< TEdgeData, TNodeID, TEdgeID, - TPortID + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID > isBuildingLayout?: boolean nodeTypes?: NodeTypes diff --git a/web/common/src/components/Lineage/LineageLayoutBase.tsx b/web/common/src/components/Lineage/LineageLayoutBase.tsx index a21c1bac17..6d3975d19a 100644 --- a/web/common/src/components/Lineage/LineageLayoutBase.tsx +++ b/web/common/src/components/Lineage/LineageLayoutBase.tsx @@ -20,7 +20,6 @@ import { import '@xyflow/react/dist/style.css' import './Lineage.css' -import { debounce } from 'lodash' import { CircuitBoard, Crosshair, LocateFixed, RotateCcw } from 'lucide-react' import React from 'react' @@ -39,8 +38,8 @@ import { NODES_TRESHOLD_ZOOM, type NodeId, type EdgeId, - ZOOM_THRESHOLD, type PortId, + ZOOM_THRESHOLD, } from './utils' import '@xyflow/react/dist/style.css' @@ -50,9 +49,12 @@ import { cn } from '@/utils' export function LineageLayoutBase< TNodeData extends LineageNodeData = LineageNodeData, TEdgeData extends LineageEdgeData = LineageEdgeData, - TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TNodeID extends string = NodeId, + TSourceID extends string = TNodeID, + TTargetID extends string = TNodeID, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, >({ nodeTypes, edgeTypes, @@ -69,7 +71,10 @@ export function LineageLayoutBase< TEdgeData, TNodeID, TEdgeID, - TPortID + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID > nodesDraggable?: boolean nodesConnectable?: boolean @@ -106,8 +111,19 @@ export function LineageLayoutBase< setSelectedEdges, } = useLineage() - const [nodes, setNodes] = React.useState(initialNodes) - const [edges, setEdges] = React.useState(initialEdges) + const [nodes, setNodes] = React.useState[]>( + [], + ) + const [edges, setEdges] = React.useState< + LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + >[] + >([]) const onNodesChange = React.useCallback( (changes: NodeChange>[]) => { @@ -120,13 +136,28 @@ export function LineageLayoutBase< const onEdgesChange = React.useCallback( ( - changes: EdgeChange>[], + changes: EdgeChange< + LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + > + >[], ) => { setEdges( - applyEdgeChanges>( - changes, - edges, - ), + applyEdgeChanges< + LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + > + >(changes, edges), ) }, [edges, setEdges], @@ -235,12 +266,23 @@ export function LineageLayoutBase< const connectedEdges = getConnectedEdges< LineageNode, - LineageEdge + LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + > >(connectedNodes, edges) const selectedNodes = new Set(connectedNodes.map(node => node.id)) const selectedEdges = new Set( connectedEdges.reduce((acc, edge) => { - if ([edge.source, edge.target].every(id => selectedNodes.has(id))) { + if ( + [edge.source, edge.target].every(id => + selectedNodes.has(id as unknown as TNodeID), + ) + ) { edge.zIndex = 2 acc.add(edge.id) } else { @@ -278,7 +320,14 @@ export function LineageLayoutBase< return ( , - LineageEdge + LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + > > className={cn('shrink-0', className)} nodes={nodes} @@ -351,3 +400,11 @@ export function LineageLayoutBase< ) } + +function debounce(func: T, wait: number) { + let timeout: NodeJS.Timeout + return (...args: unknown[]) => { + clearTimeout(timeout) + timeout = setTimeout(() => func(...args), wait) + } +} diff --git a/web/common/src/components/Lineage/edge/FactoryEdgeWithGradient.tsx b/web/common/src/components/Lineage/edge/FactoryEdgeWithGradient.tsx index a89027ffef..aee8790b35 100644 --- a/web/common/src/components/Lineage/edge/FactoryEdgeWithGradient.tsx +++ b/web/common/src/components/Lineage/edge/FactoryEdgeWithGradient.tsx @@ -15,14 +15,20 @@ export function FactoryEdgeWithGradient< TEdgeData extends EdgeData = EdgeData, TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = TNodeID, + TTargetID extends string = TNodeID, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, >( useLineage: LineageContextHook< TNodeData, TEdgeData, TNodeID, TEdgeID, - TPortID + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID >, ) { return React.memo(({ data, id, ...props }: EdgeProps>) => { diff --git a/web/common/src/components/Lineage/help.ts b/web/common/src/components/Lineage/help.ts index 97f4ad9542..e8041d9f56 100644 --- a/web/common/src/components/Lineage/help.ts +++ b/web/common/src/components/Lineage/help.ts @@ -60,13 +60,22 @@ export function getTransformedNodes< export function getTransformedModelEdgesSourceTargets< TAdjacencyListKey extends string, TEdgeData extends LineageEdgeData = LineageEdgeData, - TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = NodeId, + TTargetID extends string = NodeId, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, >( adjacencyListKeys: TAdjacencyListKey[], lineageAdjacencyList: LineageAdjacencyList, - transformEdge: TransformEdgeFn, + transformEdge: TransformEdgeFn< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + >, ) { const nodesCount = adjacencyListKeys.length @@ -76,7 +85,7 @@ export function getTransformedModelEdgesSourceTargets< for (let i = 0; i < nodesCount; i++) { const sourceAdjacencyListKey = adjacencyListKeys[i] - const sourceNodeId = toNodeID(sourceAdjacencyListKey) + const sourceNodeId = toNodeID(sourceAdjacencyListKey) const targets = lineageAdjacencyList[sourceAdjacencyListKey] const targetsCount = targets?.length || 0 @@ -91,7 +100,7 @@ export function getTransformedModelEdgesSourceTargets< sourceAdjacencyListKey, targetAdjacencyListKey, ) - const targetNodeId = toNodeID(targetAdjacencyListKey) + const targetNodeId = toNodeID(targetAdjacencyListKey) edges.push(transformEdge('edge', edgeId, sourceNodeId, targetNodeId)) } @@ -103,13 +112,22 @@ export function getTransformedModelEdgesSourceTargets< export function getTransformedModelEdgesTargetSources< TAdjacencyListKey extends string, TEdgeData extends LineageEdgeData = LineageEdgeData, - TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = NodeId, + TTargetID extends string = NodeId, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, >( adjacencyListKeys: TAdjacencyListKey[], lineageAdjacencyList: LineageAdjacencyList, - transformEdge: TransformEdgeFn, + transformEdge: TransformEdgeFn< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + >, ) { const nodesCount = adjacencyListKeys.length @@ -119,7 +137,7 @@ export function getTransformedModelEdgesTargetSources< for (let i = 0; i < nodesCount; i++) { const targetAdjacencyListKey = adjacencyListKeys[i] - const targetNodeId = toNodeID(targetAdjacencyListKey) + const targetNodeId = toNodeID(targetAdjacencyListKey) const sources = lineageAdjacencyList[targetAdjacencyListKey] const sourcesCount = sources?.length || 0 @@ -134,7 +152,7 @@ export function getTransformedModelEdgesTargetSources< sourceAdjacencyListKey, targetAdjacencyListKey, ) - const sourceNodeId = toNodeID(sourceAdjacencyListKey) + const sourceNodeId = toNodeID(sourceAdjacencyListKey) edges.push(transformEdge('edge', edgeId, sourceNodeId, targetNodeId)) } @@ -206,18 +224,27 @@ export function calculateNodeDetailsHeight({ export function createEdge< TEdgeData extends LineageEdgeData = LineageEdgeData, - TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = NodeId, + TTargetID extends string = NodeId, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, >( type: string, edgeId: TEdgeID, - sourceId: TNodeID, - targetId: TNodeID, - sourceHandleId?: TPortID, - targetHandleId?: TPortID, + sourceId: TSourceID, + targetId: TTargetID, + sourceHandleId?: TSourceHandleID, + targetHandleId?: TTargetHandleID, data?: TEdgeData, -): LineageEdge { +): LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID +> { return { id: edgeId, source: sourceId, diff --git a/web/common/src/components/Lineage/layout/dagreLayout.ts b/web/common/src/components/Lineage/layout/dagreLayout.ts index 83714a2220..554d427f03 100644 --- a/web/common/src/components/Lineage/layout/dagreLayout.ts +++ b/web/common/src/components/Lineage/layout/dagreLayout.ts @@ -13,14 +13,23 @@ import dagre from 'dagre' export function buildLayout< TNodeData extends LineageNodeData = LineageNodeData, TEdgeData extends LineageEdgeData = LineageEdgeData, - TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = NodeId, + TTargetID extends string = NodeId, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, >({ edges, nodesMap, }: { - edges: LineageEdge[] + edges: LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + >[] nodesMap: LineageNodesMap }) { const nodes = Object.values(nodesMap) diff --git a/web/common/src/components/Lineage/layout/help.ts b/web/common/src/components/Lineage/layout/help.ts index 91b3ebc4a3..d0dada83f5 100644 --- a/web/common/src/components/Lineage/layout/help.ts +++ b/web/common/src/components/Lineage/layout/help.ts @@ -24,14 +24,33 @@ export function getWorker(url: URL): Worker { export async function getLayoutedGraph< TNodeData extends LineageNodeData = LineageNodeData, TEdgeData extends LineageEdgeData = LineageEdgeData, - TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = NodeId, + TTargetID extends string = NodeId, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, >( - edges: LineageEdge[], + edges: LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + >[], nodesMap: LineageNodesMap, workerUrl: URL, -): Promise> { +): Promise< + LayoutedGraph< + TNodeData, + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + > +> { let timeoutId: NodeJS.Timeout | null = null return new Promise((resolve, reject) => { @@ -56,9 +75,11 @@ export async function getLayoutedGraph< worker.postMessage({ edges, nodesMap } as LayoutedGraph< TNodeData, TEdgeData, - TNodeID, TEdgeID, - TPortID + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID >) } catch (postError) { errorHandler(postError as ErrorEvent) @@ -66,7 +87,15 @@ export async function getLayoutedGraph< function handler( event: MessageEvent< - LayoutedGraph & { + LayoutedGraph< + TNodeData, + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + > & { error: ErrorEvent } >, diff --git a/web/common/src/components/Lineage/node/NodeHandle.tsx b/web/common/src/components/Lineage/node/NodeHandle.tsx index 4bfbfa6181..d50d90422a 100644 --- a/web/common/src/components/Lineage/node/NodeHandle.tsx +++ b/web/common/src/components/Lineage/node/NodeHandle.tsx @@ -3,8 +3,9 @@ import React from 'react' import { cn } from '@/utils' import { BaseHandle } from './base-handle' +import type { HandleId } from '../utils' -export const NodeHandle = React.memo(function NodeHandle({ +export function NodeHandle({ type, id, children, @@ -12,7 +13,7 @@ export const NodeHandle = React.memo(function NodeHandle({ ...props }: { type: 'target' | 'source' - id: string + id: THandleId children: React.ReactNode className?: string }) { @@ -29,4 +30,4 @@ export const NodeHandle = React.memo(function NodeHandle({ {children} ) -}) +} diff --git a/web/common/src/components/Lineage/node/NodeHandles.tsx b/web/common/src/components/Lineage/node/NodeHandles.tsx index 71bee716b4..453ff74317 100644 --- a/web/common/src/components/Lineage/node/NodeHandles.tsx +++ b/web/common/src/components/Lineage/node/NodeHandles.tsx @@ -3,8 +3,12 @@ import React from 'react' import { cn } from '@/utils' import { HorizontalContainer } from '@/components/HorizontalContainer/HorizontalContainer' import { NodeHandle } from './NodeHandle' +import type { HandleId } from '../utils' -export const NodeHandles = React.memo(function NodeHandles({ +export function NodeHandles< + TLeftHandleId extends string = HandleId, + TRightHandleId extends string = HandleId, +>({ leftIcon, rightIcon, leftId, @@ -13,8 +17,8 @@ export const NodeHandles = React.memo(function NodeHandles({ handleClassName, children, }: { - leftId?: string - rightId?: string + leftId?: TLeftHandleId + rightId?: TRightHandleId className?: string handleClassName?: string children: React.ReactNode @@ -27,7 +31,7 @@ export const NodeHandles = React.memo(function NodeHandles({ data-component="NodeHandles" > {leftId && ( - type="target" id={leftId} className={cn('left-0', handleClassName)} @@ -37,7 +41,7 @@ export const NodeHandles = React.memo(function NodeHandles({ )} {children} {rightId && ( - type="source" id={rightId} className={cn('right-0', handleClassName)} @@ -47,4 +51,4 @@ export const NodeHandles = React.memo(function NodeHandles({ )} ) -}) +} diff --git a/web/common/src/components/Lineage/node/NodePort.tsx b/web/common/src/components/Lineage/node/NodePort.tsx index b961d4e01a..7380716f02 100644 --- a/web/common/src/components/Lineage/node/NodePort.tsx +++ b/web/common/src/components/Lineage/node/NodePort.tsx @@ -2,12 +2,14 @@ import { useNodeConnections, useUpdateNodeInternals } from '@xyflow/react' import React from 'react' import { cn } from '@/utils' -import { type NodeId, type PortId } from '../utils' +import { type NodeId, type PortHandleId } from '../utils' import { NodeHandles } from './NodeHandles' -export const NodePort = React.memo(function NodePort< - TPortId extends string = PortId, +export function NodePort< + TPortId extends string = PortHandleId, TNodeID extends string = NodeId, + TLeftPortHandleId extends string = PortHandleId, + TRightPortHandleId extends string = PortHandleId, >({ id, nodeId, @@ -32,8 +34,16 @@ export const NodePort = React.memo(function NodePort< handleId: id, }) - const leftId = targets.length > 0 ? id : undefined - const rightId = sources.length > 0 ? id : undefined + const isLeftHandleId = (id: TPortId): id is TPortId & TLeftPortHandleId => { + return id && targets.length > 0 + } + + const isRightHandleId = (id: TPortId): id is TPortId & TRightPortHandleId => { + return id && sources.length > 0 + } + + const leftId = isLeftHandleId(id) ? id : undefined + const rightId = isRightHandleId(id) ? id : undefined React.useEffect(() => { if (leftId || rightId) { @@ -42,7 +52,7 @@ export const NodePort = React.memo(function NodePort< }, [updateNodeInternals, nodeId, leftId, rightId]) return ( - data-component="NodePort" leftIcon={ @@ -61,4 +71,4 @@ export const NodePort = React.memo(function NodePort< {children} ) -}) +} diff --git a/web/common/src/components/Lineage/node/base-handle.tsx b/web/common/src/components/Lineage/node/base-handle.tsx index 76d66bdeaf..e6b8f0c24b 100644 --- a/web/common/src/components/Lineage/node/base-handle.tsx +++ b/web/common/src/components/Lineage/node/base-handle.tsx @@ -16,7 +16,6 @@ export const BaseHandle: ForwardRefExoticComponent< 'fixed flex justify-center items-center border-none transition', className, )} - {...props} > {children} diff --git a/web/common/src/components/Lineage/stories/Lineage.stories.tsx b/web/common/src/components/Lineage/stories/Lineage.stories.tsx index 6e16bed61e..115be3c2c0 100644 --- a/web/common/src/components/Lineage/stories/Lineage.stories.tsx +++ b/web/common/src/components/Lineage/stories/Lineage.stories.tsx @@ -1,12 +1,125 @@ import type { LineageAdjacencyList, LineageDetails } from '../utils' import { ModelLineage } from './ModelLineage' -import type { ModelLineageNodeDetails, ModelName } from './ModelLineageContext' +import type { + BrandedLineageAdjacencyList, + BrandedLineageDetails, + ModelLineageNodeDetails, + ModelName, +} from './ModelLineageContext' export default { title: 'Components/Lineage', } +const adjacencyList = { + 'sqlmesh.sushi.raw_orders': ['sqlmesh.sushi.orders'], + 'sqlmesh.sushi.orders': [], +} as Record + +const lineageDetails = { + 'sqlmesh.sushi.raw_orders': { + name: 'sqlmesh.sushi.raw_orders', + display_name: 'sushi.raw_orders', + identifier: '123456789', + version: '123456789', + dialect: 'bigquery', + cron: '0 0 * * *', + owner: 'admin', + kind: 'INCREMENTAL_BY_TIME', + model_type: 'python', + tags: ['test', 'tag', 'another tag'], + columns: { + user_id: { + data_type: 'STRING', + description: 'node', + }, + event_id: { + data_type: 'STRING', + description: 'node', + }, + created_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + }, + }, + 'sqlmesh.sushi.orders': { + name: 'sqlmesh.sushi.orders', + display_name: 'sushi.orders', + identifier: '123456789', + version: '123456789', + dialect: 'bigquery', + cron: '0 0 * * *', + owner: 'admin', + kind: 'INCREMENTAL_BY_TIME', + model_type: 'sql', + tags: ['test', 'tag', 'another tag'], + columns: { + user_id: { + data_type: 'STRING', + description: 'node', + columnLineageData: { + 'sqlmesh.sushi.orders': { + user_id: { + source: 'sqlmesh.sushi.raw_orders', + expression: 'select user_id from sqlmesh.sushi.raw_orders', + models: { + 'sqlmesh.sushi.raw_orders': ['user_id'], + }, + }, + }, + }, + }, + event_id: { + data_type: 'STRING', + description: 'node', + columnLineageData: { + 'sqlmesh.sushi.orders': { + event_id: { + models: { + 'sqlmesh.sushi.raw_orders': ['event_id'], + }, + }, + }, + }, + }, + product_id: { + data_type: 'STRING', + description: 'node', + }, + customer_id: { + data_type: 'STRING', + description: 'node', + }, + updated_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + deleted_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + expired_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + start_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + end_at: { + data_type: 'TIMESTAMP', + description: 'node', + }, + created_ts: { + data_type: 'TIMESTAMP', + description: 'node', + }, + }, + }, +} as Record + export const LineageModel = () => { return (
      { `} - } - lineageDetails={ - { - 'sqlmesh.sushi.raw_orders': { - name: 'sqlmesh.sushi.raw_orders', - display_name: 'sushi.raw_orders', - identifier: '123456789', - version: '123456789', - dialect: 'bigquery', - cron: '0 0 * * *', - owner: 'admin', - kind: 'INCREMENTAL_BY_TIME', - model_type: 'python', - tags: ['test', 'tag', 'another tag'], - columns: { - user_id: { - data_type: 'STRING', - description: 'node', - }, - event_id: { - data_type: 'STRING', - description: 'node', - }, - created_at: { - data_type: 'TIMESTAMP', - description: 'node', - }, - }, - }, - 'sqlmesh.sushi.orders': { - name: 'sqlmesh.sushi.orders', - display_name: 'sushi.orders', - identifier: '123456789', - version: '123456789', - dialect: 'bigquery', - cron: '0 0 * * *', - owner: 'admin', - kind: 'INCREMENTAL_BY_TIME', - model_type: 'sql', - tags: ['test', 'tag', 'another tag'], - columns: { - user_id: { - data_type: 'STRING', - description: 'node', - columnLineageData: { - 'sqlmesh.sushi.orders': { - user_id: { - source: 'sqlmesh.sushi.raw_orders', - expression: - 'select user_id from sqlmesh.sushi.raw_orders', - models: { - 'sqlmesh.sushi.raw_orders': ['user_id'], - }, - }, - }, - }, - }, - event_id: { - data_type: 'STRING', - description: 'node', - columnLineageData: { - 'sqlmesh.sushi.orders': { - event_id: { - models: { - 'sqlmesh.sushi.raw_orders': ['event_id'], - }, - }, - }, - }, - }, - product_id: { - data_type: 'STRING', - description: 'node', - }, - customer_id: { - data_type: 'STRING', - description: 'node', - }, - updated_at: { - data_type: 'TIMESTAMP', - description: 'node', - }, - deleted_at: { - data_type: 'TIMESTAMP', - description: 'node', - }, - expired_at: { - data_type: 'TIMESTAMP', - description: 'node', - }, - start_at: { - data_type: 'TIMESTAMP', - description: 'node', - }, - end_at: { - data_type: 'TIMESTAMP', - description: 'node', - }, - created_ts: { - data_type: 'TIMESTAMP', - description: 'node', - }, - }, - }, - } as LineageDetails - } + adjacencyList={adjacencyList as BrandedLineageAdjacencyList} + lineageDetails={lineageDetails as BrandedLineageDetails} className="rounded-2xl" />
      diff --git a/web/common/src/components/Lineage/stories/ModelLineage.tsx b/web/common/src/components/Lineage/stories/ModelLineage.tsx index 46d19f9758..3df85ea1a5 100644 --- a/web/common/src/components/Lineage/stories/ModelLineage.tsx +++ b/web/common/src/components/Lineage/stories/ModelLineage.tsx @@ -1,8 +1,6 @@ -import { debounce } from 'lodash' import { Focus, LockOpen, Rows2, Rows3, Lock } from 'lucide-react' import React from 'react' -import { type ColumnLevelLineageAdjacencyList } from '../LineageColumnLevel/ColumnLevelLineageContext' import { MAX_COLUMNS_TO_DISPLAY, calculateColumnsHeight, @@ -11,16 +9,8 @@ import { getEdgesFromColumnLineage, } from '../LineageColumnLevel/help' import { useColumnLevelLineage } from '../LineageColumnLevel/useColumnLevelLineage' -import { LineageControlButton } from '../LineageControlButton' -import { LineageControlIcon } from '../LineageControlIcon' import { LineageLayout } from '../LineageLayout' import { FactoryEdgeWithGradient } from '../edge/FactoryEdgeWithGradient' -import { - toNodeID, - toPortID, - type LineageAdjacencyList, - type LineageDetails, -} from '../utils' import { calculateNodeBaseHeight, calculateNodeDetailsHeight, @@ -33,6 +23,8 @@ import { import { type LineageEdge, type LineageNodesMap, + toNodeID, + toPortID, ZOOM_THRESHOLD, } from '../utils' import { @@ -47,11 +39,23 @@ import { type ModelColumnID, type ModelEdgeId, type NodeType, + type BrandedLineageAdjacencyList, + type BrandedLineageDetails, + type BrandedColumnLevelLineageAdjacencyList, + type ModelColumn, + type ModelDisplayName, + type LeftHandleId, + type RightHandleId, + type LeftPortHandleId, + type RightPortHandleId, } from './ModelLineageContext' import { ModelNode } from './ModelNode' import { getNodeTypeColorVar } from './help' import { EdgeWithGradient } from '../edge/EdgeWithGradient' import { cleanupLayoutWorker, getLayoutedGraph } from '../layout/help' +import { LineageControlButton } from '../LineageControlButton' +import { LineageControlIcon } from '../LineageControlIcon' +import type { BrandedRecord } from '@/types' const nodeTypes = { node: ModelNode, @@ -67,8 +71,8 @@ export const ModelLineage = ({ lineageDetails, className, }: { - adjacencyList: LineageAdjacencyList - lineageDetails: LineageDetails + adjacencyList: BrandedLineageAdjacencyList + lineageDetails: BrandedLineageDetails selectedModelName?: ModelName className?: string }) => { @@ -76,7 +80,14 @@ export const ModelLineage = ({ const [isBuildingLayout, setIsBuildingLayout] = React.useState(false) const [nodesDraggable, setNodesDraggable] = React.useState(false) const [edges, setEdges] = React.useState< - LineageEdge[] + LineageEdge< + EdgeData, + ModelEdgeId, + LeftHandleId, + RightHandleId, + LeftPortHandleId, + RightPortHandleId + >[] >([]) const [nodesMap, setNodesMap] = React.useState< LineageNodesMap @@ -94,7 +105,7 @@ export const ModelLineage = ({ const [showColumns, setShowColumns] = React.useState(false) const [columnLevelLineage, setColumnLevelLineage] = React.useState< - Map> + Map >(new Map()) const [fetchingColumns, setFetchingColumns] = React.useState< Set @@ -104,9 +115,12 @@ export const ModelLineage = ({ adjacencyListColumnLevel, selectedColumns, adjacencyListKeysColumnLevel, - } = useColumnLevelLineage( - columnLevelLineage, - ) + } = useColumnLevelLineage< + ModelName, + ColumnName, + ModelColumnID, + BrandedColumnLevelLineageAdjacencyList + >(columnLevelLineage) const adjacencyListKeys = React.useMemo(() => { let keys: ModelName[] = [] @@ -124,18 +138,18 @@ export const ModelLineage = ({ (nodeId: ModelNodeId, detail: ModelLineageNodeDetails) => { const columns = detail.columns - const node = createNode('node', nodeId, { - name: detail.name, + const node = createNode('node', nodeId, { + name: detail.name as ModelName, + displayName: detail.display_name as ModelDisplayName, identifier: detail.identifier, model_type: detail.model_type as NodeType, kind: detail.kind!, cron: detail.cron, - displayName: detail.display_name, owner: detail.owner!, dialect: detail.dialect, version: detail.version, tags: detail.tags || [], - columns, + columns: columns as BrandedRecord, }) const selectedColumnsCount = new Set( Object.keys(columns ?? {}).map(k => toPortID(detail.name, k)), @@ -184,10 +198,10 @@ export const ModelLineage = ({ ( edgeType: string, edgeId: ModelEdgeId, - sourceId: ModelNodeId, - targetId: ModelNodeId, - sourceHandleId?: ModelColumnID, - targetHandleId?: ModelColumnID, + sourceId: LeftHandleId, + targetId: RightHandleId, + sourceHandleId?: LeftPortHandleId, + targetHandleId?: RightPortHandleId, ) => { const sourceNode = transformedNodesMap[sourceId] const targetNode = transformedNodesMap[targetId] @@ -217,7 +231,14 @@ export const ModelLineage = ({ data.strokeWidth = 2 } - return createEdge( + return createEdge< + EdgeData, + ModelEdgeId, + LeftHandleId, + RightHandleId, + LeftPortHandleId, + RightPortHandleId + >( edgeType, edgeId, sourceId, @@ -237,8 +258,11 @@ export const ModelLineage = ({ ColumnName, EdgeData, ModelEdgeId, - ModelNodeId, - ModelColumnID + LeftHandleId, + RightHandleId, + LeftPortHandleId, + RightPortHandleId, + BrandedColumnLevelLineageAdjacencyList >({ columnLineage: adjacencyListColumnLevel, transformEdge, @@ -252,38 +276,45 @@ export const ModelLineage = ({ : getTransformedModelEdgesSourceTargets< ModelName, EdgeData, - ModelNodeId, ModelEdgeId, - ModelColumnID + LeftHandleId, + RightHandleId, + LeftPortHandleId, + RightPortHandleId >(adjacencyListKeys, adjacencyList, transformEdge) }, [adjacencyListKeys, adjacencyList, transformEdge, edgesColumnLevel]) - const calculateLayout = React.useMemo(() => { - return debounce( - ( - eds: LineageEdge[], - nds: LineageNodesMap, - ) => - getLayoutedGraph( - eds, - nds, - new URL('./dagreLayout.worker.ts', import.meta.url), - ) - .then(({ edges, nodesMap }) => { - setEdges(edges) - setNodesMap(nodesMap) - }) - .catch(error => { - console.error('Layout processing failed:', error) - setEdges([]) - setNodesMap({}) - }) - .finally(() => { - setIsBuildingLayout(false) - }), - 200, - ) - }, []) + const calculateLayout = React.useCallback( + ( + eds: LineageEdge< + EdgeData, + ModelEdgeId, + LeftHandleId, + RightHandleId, + LeftPortHandleId, + RightPortHandleId + >[], + nds: LineageNodesMap, + ) => + getLayoutedGraph( + eds, + nds, + new URL('./dagreLayout.worker.ts', import.meta.url), + ) + .then(({ edges, nodesMap }) => { + setEdges(edges) + setNodesMap(nodesMap) + }) + .catch(error => { + console.error('Layout processing failed:', error) + setEdges([]) + setNodesMap({}) + }) + .finally(() => { + setIsBuildingLayout(false) + }), + [setEdges, setNodesMap, setIsBuildingLayout], + ) const nodes = React.useMemo(() => { return Object.values(nodesMap) @@ -291,7 +322,7 @@ export const ModelLineage = ({ const currentNode = React.useMemo(() => { return selectedModelName - ? nodesMap[toNodeID(selectedModelName)] + ? nodesMap[toNodeID(selectedModelName)] : null }, [selectedModelName, nodesMap]) @@ -315,30 +346,13 @@ export const ModelLineage = ({ selectedNodes, ) const onlySelectedEdges = transformedEdges.filter(edge => - selectedEdges.has(edge.id as ModelEdgeId), + selectedEdges.has(edge.id), ) calculateLayout(onlySelectedEdges, onlySelectedNodesMap) } else { calculateLayout(transformedEdges, transformedNodesMap) } - }, [ - calculateLayout, - showOnlySelectedNodes, - transformedEdges, - transformedNodesMap, - ]) - - React.useEffect(() => { - const currentNodeId = selectedModelName - ? toNodeID(selectedModelName) - : undefined - - if (currentNodeId && currentNodeId in nodesMap) { - setSelectedNodeId(currentNodeId) - } else { - handleReset() - } - }, [handleReset, selectedModelName, nodesMap]) + }, [showOnlySelectedNodes, transformedEdges, transformedNodesMap]) // Cleanup worker on unmount React.useEffect(() => () => cleanupLayoutWorker(), []) @@ -381,7 +395,10 @@ export const ModelLineage = ({ EdgeData, ModelNodeId, ModelEdgeId, - ModelColumnID + LeftHandleId, + RightHandleId, + LeftPortHandleId, + RightPortHandleId > isBuildingLayout={isBuildingLayout} useLineage={useModelLineage} diff --git a/web/common/src/components/Lineage/stories/ModelLineageContext.ts b/web/common/src/components/Lineage/stories/ModelLineageContext.ts index 98d2131766..745d9c2636 100644 --- a/web/common/src/components/Lineage/stories/ModelLineageContext.ts +++ b/web/common/src/components/Lineage/stories/ModelLineageContext.ts @@ -1,4 +1,4 @@ -import type { Branded } from '@/types' +import type { Branded, BrandedRecord } from '@/types' import { type ColumnLevelLineageAdjacencyList, type ColumnLevelLineageContextValue, @@ -10,22 +10,49 @@ import { createLineageContext, getInitial as getLineageContextInitial, } from '../LineageContext' -import { type PathType } from '../utils' +import { + type LineageAdjacencyList, + type LineageDetails, + type PathType, +} from '../utils' export type ModelName = Branded +export type ModelDisplayName = Branded export type ColumnName = Branded export type ModelColumnID = Branded -export type ModelNodeId = Branded export type ModelEdgeId = Branded +export type LeftHandleId = Branded +export type RightHandleId = Branded +export type ModelNodeId = LeftHandleId | RightHandleId +export type LeftPortHandleId = Branded +export type RightPortHandleId = Branded + +export type BrandedColumnLevelLineageAdjacencyList = + ColumnLevelLineageAdjacencyList & { + readonly __adjacencyListKeyBrand: ModelName + readonly __adjacencyListColumnKeyBrand: ColumnName + } + +export type BrandedLineageAdjacencyList = LineageAdjacencyList & { + readonly __adjacencyListKeyBrand: ModelName +} + +export type BrandedLineageDetails = LineageDetails< + ModelName, + ModelLineageNodeDetails +> & { + readonly __lineageDetailsKeyBrand: ModelName +} + export type ModelColumn = Column & { id: ModelColumnID name: ColumnName - columnLineageData?: ColumnLevelLineageAdjacencyList + columnLineageData?: BrandedColumnLevelLineageAdjacencyList } export type NodeType = 'sql' | 'python' export type ModelLineageNodeDetails = { - name: ModelName + name: string display_name: string identifier: string version: string @@ -35,12 +62,12 @@ export type ModelLineageNodeDetails = { kind?: string model_type?: string tags?: string[] - columns?: Record + columns?: BrandedRecord } export type NodeData = { name: ModelName - displayName: string + displayName: ModelDisplayName model_type: NodeType identifier: string version: string @@ -48,8 +75,8 @@ export type NodeData = { cron: string owner: string dialect: string - columns?: Record tags: string[] + columns?: BrandedRecord } export type EdgeData = { @@ -62,14 +89,18 @@ export type EdgeData = { export type ModelLineageContextValue = ColumnLevelLineageContextValue< ModelName, ColumnName, - ModelColumnID + ModelColumnID, + BrandedColumnLevelLineageAdjacencyList > & LineageContextValue< NodeData, EdgeData, ModelNodeId, ModelEdgeId, - ModelColumnID + LeftHandleId, + RightHandleId, + LeftPortHandleId, + RightPortHandleId > export const initial = { @@ -77,7 +108,8 @@ export const initial = { ...getColumnLevelLineageContextInitial< ModelName, ColumnName, - ModelColumnID + ModelColumnID, + BrandedColumnLevelLineageAdjacencyList >(), } @@ -86,7 +118,10 @@ export const { Provider, useLineage } = createLineageContext< EdgeData, ModelNodeId, ModelEdgeId, - ModelColumnID, + LeftHandleId, + RightHandleId, + LeftPortHandleId, + RightPortHandleId, ModelLineageContextValue >(initial) diff --git a/web/common/src/components/Lineage/stories/ModelNodeColumn.tsx b/web/common/src/components/Lineage/stories/ModelNodeColumn.tsx index 35d4a0e592..dbb3f92dad 100644 --- a/web/common/src/components/Lineage/stories/ModelNodeColumn.tsx +++ b/web/common/src/components/Lineage/stories/ModelNodeColumn.tsx @@ -1,6 +1,5 @@ import React from 'react' -import { type ColumnLevelLineageAdjacencyList } from '../LineageColumnLevel/ColumnLevelLineageContext' import { FactoryColumn } from '../LineageColumnLevel/FactoryColumn' import { @@ -9,13 +8,19 @@ import { type ModelName, type ModelNodeId, type ColumnName, + type BrandedColumnLevelLineageAdjacencyList, + type LeftPortHandleId, + type RightPortHandleId, } from './ModelLineageContext' const ModelColumn = FactoryColumn< ModelName, ColumnName, ModelNodeId, - ModelColumnID + ModelColumnID, + LeftPortHandleId, + RightPortHandleId, + BrandedColumnLevelLineageAdjacencyList >(useModelLineage) export const ModelNodeColumn = React.memo(function ModelNodeColumn({ @@ -35,7 +40,7 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ type: string description?: string | null className?: string - columnLineageData?: ColumnLevelLineageAdjacencyList + columnLineageData?: BrandedColumnLevelLineageAdjacencyList }) { const { selectedColumns, setColumnLevelLineage } = useModelLineage() diff --git a/web/common/src/components/Lineage/utils.ts b/web/common/src/components/Lineage/utils.ts index 01a277f17a..4e2d55a5a0 100644 --- a/web/common/src/components/Lineage/utils.ts +++ b/web/common/src/components/Lineage/utils.ts @@ -4,6 +4,8 @@ import { type Edge, type Node } from '@xyflow/react' export type NodeId = Branded export type EdgeId = Branded export type PortId = Branded +export type HandleId = Branded +export type PortHandleId = Branded export type LineageNodeData = Record export type LineageEdgeData = Record @@ -29,25 +31,36 @@ export interface LineageNode< export interface LineageEdge< TEdgeData extends LineageEdgeData, - TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = NodeId, + TTargetID extends string = NodeId, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, > extends Edge { id: TEdgeID - source: TNodeID - target: TNodeID - sourceHandle?: TPortID - targetHandle?: TPortID + source: TSourceID + target: TTargetID + sourceHandle?: TSourceHandleID + targetHandle?: TTargetHandleID } export type LayoutedGraph< TNodeData extends LineageNodeData = LineageNodeData, TEdgeData extends LineageEdgeData = LineageEdgeData, - TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = NodeId, + TTargetID extends string = NodeId, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, > = { - edges: LineageEdge[] + edges: LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID + >[] nodesMap: LineageNodesMap } @@ -60,17 +73,26 @@ export type TransformNodeFn< export type TransformEdgeFn< TEdgeData extends LineageEdgeData = LineageEdgeData, - TNodeID extends string = NodeId, TEdgeID extends string = EdgeId, - TPortID extends string = PortId, + TSourceID extends string = NodeId, + TTargetID extends string = NodeId, + TSourceHandleID extends string = PortId, + TTargetHandleID extends string = PortId, > = ( edgeType: string, edgeId: TEdgeID, - sourceId: TNodeID, - targetId: TNodeID, - sourceColumnId?: TPortID, - targetColumnId?: TPortID, -) => LineageEdge + sourceId: TSourceID, + targetId: TTargetID, + sourceHandleId?: TSourceHandleID, + targetHandleId?: TTargetHandleID, +) => LineageEdge< + TEdgeData, + TEdgeID, + TSourceID, + TTargetID, + TSourceHandleID, + TTargetHandleID +> export const DEFAULT_NODE_HEIGHT = 32 export const DEFAULT_NODE_WIDTH = 300 diff --git a/web/common/src/index.ts b/web/common/src/index.ts index 0748a6c78e..c3c65a8e77 100644 --- a/web/common/src/index.ts +++ b/web/common/src/index.ts @@ -66,6 +66,8 @@ export { cn, truncate } from '@/utils' export type { Brand, Branded, + BrandedString, + BrandedRecord, Size, HeadlineLevel, Side, diff --git a/web/common/src/types.ts b/web/common/src/types.ts index 3de26b205d..e8bdf3e9de 100644 --- a/web/common/src/types.ts +++ b/web/common/src/types.ts @@ -1,8 +1,46 @@ export declare const __brand: unique symbol - export type Brand = { [__brand]: B } + +/** + * Branded is a type that adds a brand to a type. It is a type that is used to + * ensure that the type is unique and that it is not possible to mix up types + * with the same brand. + * + * @example + * + * type UserId = Branded + * type UserName = Branded + * + * const userId = '123' as UserId + * const userName = 'John Doe' as UserName + * + * userId == userName -> compile error + */ export type Branded = T & Brand +/** + * Constraint that only accepts branded string types + */ +export type BrandedString = string & Brand + +/** + * BrandedRecord is a type that creates a branded Record type with strict key checking. + * This ensures that Record is NOT assignable to Record + * + * @example + * type ModelFQN = Branded + * type ModelName = Branded + * + * type FQNMap = BrandedRecord + * type NameMap = BrandedRecord + * + * const fqnMap: FQNMap = {} + * const nameMap: NameMap = fqnMap // TypeScript error! + */ +export type BrandedRecord = Record & { + readonly __recordKeyBrand: K +} + export type Callback = (data?: T) => void export type Size = '2xs' | 'xs' | 's' | 'm' | 'l' | 'xl' | '2xl' From 633e64e3d3479a01dc3f140b808497e88416d22f Mon Sep 17 00:00:00 2001 From: Giorgos Michas Date: Thu, 9 Oct 2025 15:59:24 +0300 Subject: [PATCH 0963/1056] fix: robust cluster_by config parsing (#5478) --- sqlmesh/dbt/model.py | 8 ++++- tests/dbt/test_transformation.py | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/sqlmesh/dbt/model.py b/sqlmesh/dbt/model.py index d882f94942..fa84824a43 100644 --- a/sqlmesh/dbt/model.py +++ b/sqlmesh/dbt/model.py @@ -601,7 +601,13 @@ def to_sqlmesh( clustered_by = [] for c in self.cluster_by: try: - clustered_by.append(d.parse_one(c, dialect=model_dialect)) + cluster_expr = exp.maybe_parse( + c, into=exp.Cluster, prefix="CLUSTER BY", dialect=model_dialect + ) + for expr in cluster_expr.expressions: + clustered_by.append( + expr.this if isinstance(expr, exp.Ordered) else expr + ) except SqlglotError as e: raise ConfigError( f"Failed to parse model '{self.canonical_name(context)}' cluster_by field '{c}' in '{self.path}': {e}" diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 304ac57731..97c5c37e75 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -2306,6 +2306,60 @@ def test_model_cluster_by(): ) assert model.to_sqlmesh(context).clustered_by == [] + model = ModelConfig( + name="model", + alias="model", + package_name="package", + target_schema="test", + cluster_by="Bar, qux", + sql="SELECT * FROM baz", + materialized=Materialization.TABLE.value, + ) + assert model.to_sqlmesh(context).clustered_by == [ + exp.to_column('"BAR"'), + exp.to_column('"QUX"'), + ] + + model = ModelConfig( + name="model", + alias="model", + package_name="package", + target_schema="test", + cluster_by=['"Bar,qux"'], + sql="SELECT * FROM baz", + materialized=Materialization.TABLE.value, + ) + assert model.to_sqlmesh(context).clustered_by == [ + exp.to_column('"Bar,qux"'), + ] + + model = ModelConfig( + name="model", + alias="model", + package_name="package", + target_schema="test", + cluster_by='"Bar,qux"', + sql="SELECT * FROM baz", + materialized=Materialization.TABLE.value, + ) + assert model.to_sqlmesh(context).clustered_by == [ + exp.to_column('"Bar,qux"'), + ] + + model = ModelConfig( + name="model", + alias="model", + package_name="package", + target_schema="test", + cluster_by=["to_date(Bar),qux"], + sql="SELECT * FROM baz", + materialized=Materialization.TABLE.value, + ) + assert model.to_sqlmesh(context).clustered_by == [ + exp.TsOrDsToDate(this=exp.to_column('"BAR"')), + exp.to_column('"QUX"'), + ] + def test_snowflake_dynamic_table(): context = DbtContext() From 33ec6d934ea034a1273dd4313e691049e66b22a3 Mon Sep 17 00:00:00 2001 From: Jo <46752250+georgesittas@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:18:11 +0300 Subject: [PATCH 0964/1056] Chore: improve unit test validation (#5517) --- sqlmesh/core/test/definition.py | 3 +++ tests/core/test_test.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index 4336e00ce0..8d8ca17702 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -454,6 +454,9 @@ def _validate_and_normalize_test(self) -> None: query = outputs.get("query") partial = outputs.pop("partial", None) + if ctes is None and query is None: + _raise_error("Incomplete test, outputs must contain 'query' or 'ctes'", self.path) + def _normalize_rows( values: t.List[Row] | t.Dict, name: str, diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 56a44cc955..13e31703a1 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -1185,6 +1185,27 @@ def test_unknown_column_error() -> None: ) +def test_invalid_outputs_error() -> None: + with pytest.raises(TestError, match="Incomplete test, outputs must contain 'query' or 'ctes'"): + _create_test( + body=load_yaml( + """ +test_foo: + model: sushi.foo + inputs: + raw: + - id: 1 + outputs: + rows: + - id: 1 + """ + ), + test_name="test_foo", + model=_create_model("SELECT id FROM raw"), + context=Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))), + ) + + def test_empty_rows(sushi_context: Context) -> None: _check_successful_or_raise( _create_test( From 17a73f0f471ce12162097ed484083c435245386d Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:49:47 +0300 Subject: [PATCH 0965/1056] Chore: Add tag assertion in selectors tag test (#5518) --- tests/dbt/cli/test_selectors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/dbt/cli/test_selectors.py b/tests/dbt/cli/test_selectors.py index 3d50fe6ed2..a2c19057eb 100644 --- a/tests/dbt/cli/test_selectors.py +++ b/tests/dbt/cli/test_selectors.py @@ -215,6 +215,7 @@ def test_exclude_by_dbt_names( ctx = jaffle_shop_duckdb_context ctx.load() assert '"jaffle_shop"."main"."agg_orders"' in ctx.models + assert ctx.get_model('"jaffle_shop"."main"."agg_orders"').tags == ["agg"] selector = ctx._new_selector() assert isinstance(selector, DbtSelector) From fd54170c2a6ad2c0ab7d70d5b8c015d50c3a3b88 Mon Sep 17 00:00:00 2001 From: Ben <9087625+benfdking@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:28:09 +0100 Subject: [PATCH 0966/1056] chore: small dbt test addition (#5519) --- tests/dbt/cli/test_selectors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/dbt/cli/test_selectors.py b/tests/dbt/cli/test_selectors.py index a2c19057eb..17f0195f58 100644 --- a/tests/dbt/cli/test_selectors.py +++ b/tests/dbt/cli/test_selectors.py @@ -165,6 +165,7 @@ def test_select_by_dbt_names( ctx = jaffle_shop_duckdb_context ctx.load() assert '"jaffle_shop"."main"."agg_orders"' in ctx.models + assert ctx.get_model('"jaffle_shop"."main"."agg_orders"').tags == ["agg"] selector = ctx._new_selector() assert isinstance(selector, DbtSelector) From 84da6ac33b1632a9719eca062de07fa4480f28b4 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 10 Oct 2025 08:06:26 +1300 Subject: [PATCH 0967/1056] Feat(sqlmesh_dbt): Add support for --log-level (#5514) --- sqlmesh/__init__.py | 12 ++++++++++-- sqlmesh_dbt/cli.py | 14 +++++++++++++- sqlmesh_dbt/operations.py | 3 ++- tests/dbt/cli/test_global_flags.py | 14 ++++++++++++++ tests/dbt/cli/test_operations.py | 11 +++++++++++ 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/sqlmesh/__init__.py b/sqlmesh/__init__.py index 7712a41379..577a3aaf02 100644 --- a/sqlmesh/__init__.py +++ b/sqlmesh/__init__.py @@ -188,6 +188,7 @@ def configure_logging( write_to_file: bool = True, log_file_dir: t.Optional[t.Union[str, Path]] = None, ignore_warnings: bool = False, + log_level: t.Optional[t.Union[str, int]] = None, ) -> None: # Remove noisy grpc logs that are not useful for users os.environ["GRPC_VERBOSITY"] = os.environ.get("GRPC_VERBOSITY", "NONE") @@ -195,8 +196,15 @@ def configure_logging( logger = logging.getLogger() debug = force_debug or debug_mode_enabled() - # base logger needs to be the lowest level that we plan to log - level = logging.DEBUG if debug else logging.INFO + if log_level is not None: + if isinstance(log_level, str): + level = logging._nameToLevel.get(log_level.upper()) or logging.INFO + else: + level = log_level + else: + # base logger needs to be the lowest level that we plan to log + level = logging.DEBUG if debug else logging.INFO + logger.setLevel(level) if debug: diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py index ec11e7730e..981384fa64 100644 --- a/sqlmesh_dbt/cli.py +++ b/sqlmesh_dbt/cli.py @@ -78,6 +78,12 @@ def _cleanup() -> None: default=False, help="Display debug logging during dbt execution. Useful for debugging and making bug reports events to help when debugging.", ) +@click.option( + "--log-level", + default="info", + type=click.Choice(["debug", "info", "warn", "error", "none"]), + help="Specify the minimum severity of events that are logged to the console and the log file.", +) @click.pass_context @cli_global_error_handler def dbt( @@ -85,6 +91,7 @@ def dbt( profile: t.Optional[str] = None, target: t.Optional[str] = None, debug: bool = False, + log_level: t.Optional[str] = None, ) -> None: """ An ELT tool for managing your SQL transformations and data models, powered by the SQLMesh engine. @@ -97,7 +104,12 @@ def dbt( # we have a partially applied function here because subcommands might set extra options like --vars # that need to be known before we attempt to load the project ctx.obj = functools.partial( - create, project_dir=Path.cwd(), profile=profile, target=target, debug=debug + create, + project_dir=Path.cwd(), + profile=profile, + target=target, + debug=debug, + log_level=log_level, ) if not ctx.invoked_subcommand: diff --git a/sqlmesh_dbt/operations.py b/sqlmesh_dbt/operations.py index cb1ac217cc..810046dead 100644 --- a/sqlmesh_dbt/operations.py +++ b/sqlmesh_dbt/operations.py @@ -237,6 +237,7 @@ def create( vars: t.Optional[t.Dict[str, t.Any]] = None, threads: t.Optional[int] = None, debug: bool = False, + log_level: t.Optional[str] = None, ) -> DbtOperations: with Progress(transient=True) as progress: # Indeterminate progress bar before SQLMesh import to provide feedback to the user that something is indeed happening @@ -256,7 +257,7 @@ def create( while root_logger.hasHandlers(): root_logger.removeHandler(root_logger.handlers[0]) - configure_logging(force_debug=debug) + configure_logging(force_debug=debug, log_level=log_level) set_console(DbtCliConsole()) progress.update(load_task_id, description="Loading project", total=None) diff --git a/tests/dbt/cli/test_global_flags.py b/tests/dbt/cli/test_global_flags.py index 66dee7236c..abdb1ac41b 100644 --- a/tests/dbt/cli/test_global_flags.py +++ b/tests/dbt/cli/test_global_flags.py @@ -1,10 +1,12 @@ import typing as t from pathlib import Path import pytest +import logging from pytest_mock import MockerFixture from click.testing import Result from sqlmesh.utils.errors import SQLMeshError from sqlglot.errors import SqlglotError +from tests.dbt.conftest import EmptyProjectCreator pytestmark = pytest.mark.slow @@ -93,3 +95,15 @@ def test_run_error_handler( assert result.exit_code == 1 assert "Error: Error with selector" in result.output assert "Traceback" not in result.output + + +def test_log_level(invoke_cli: t.Callable[..., Result], create_empty_project: EmptyProjectCreator): + create_empty_project() + + result = invoke_cli(["--log-level", "info", "list"]) + assert result.exit_code == 0 + assert logging.getLogger("sqlmesh").getEffectiveLevel() == logging.INFO + + result = invoke_cli(["--log-level", "debug", "list"]) + assert result.exit_code == 0 + assert logging.getLogger("sqlmesh").getEffectiveLevel() == logging.DEBUG diff --git a/tests/dbt/cli/test_operations.py b/tests/dbt/cli/test_operations.py index 139336297c..4aa508e21f 100644 --- a/tests/dbt/cli/test_operations.py +++ b/tests/dbt/cli/test_operations.py @@ -9,6 +9,7 @@ from sqlmesh.core.plan import PlanBuilder from sqlmesh.core.config.common import VirtualEnvironmentMode from tests.dbt.conftest import EmptyProjectCreator +import logging pytestmark = pytest.mark.slow @@ -363,3 +364,13 @@ def test_create_sets_concurrent_tasks_based_on_threads(create_empty_project: Emp g.connection and g.connection.concurrent_tasks == 16 for g in operations.context.config.gateways.values() ) + + +def test_create_configures_log_level(create_empty_project: EmptyProjectCreator): + project_dir, _ = create_empty_project() + + create(project_dir=project_dir, log_level="info") + assert logging.getLogger("sqlmesh").getEffectiveLevel() == logging.INFO + + create(project_dir=project_dir, log_level="error") + assert logging.getLogger("sqlmesh").getEffectiveLevel() == logging.ERROR From 5435ff820c3a76fb200e4ee129ef43d2abbf6940 Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Fri, 10 Oct 2025 08:06:52 +1300 Subject: [PATCH 0968/1056] Fix(windows): Allow 'sqlmesh clean' to delete cache file paths that exceed 260 chars (#5512) --- sqlmesh/core/context.py | 16 +++++++++------ sqlmesh/utils/windows.py | 16 ++++++++++++--- tests/core/test_context.py | 40 +++++++++++++++++++++++++++++++++++++ tests/utils/test_windows.py | 39 ++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 tests/utils/test_windows.py diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index d118116f7f..f9d54b0564 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -139,6 +139,7 @@ ) from sqlmesh.utils.config import print_config from sqlmesh.utils.jinja import JinjaMacroRegistry +from sqlmesh.utils.windows import IS_WINDOWS, fix_windows_path if t.TYPE_CHECKING: import pandas as pd @@ -2590,12 +2591,15 @@ def table_name( ) def clear_caches(self) -> None: - for path in self.configs: - cache_path = path / c.CACHE - if cache_path.exists(): - rmtree(cache_path) - if self.cache_dir.exists(): - rmtree(self.cache_dir) + paths_to_remove = [path / c.CACHE for path in self.configs] + paths_to_remove.append(self.cache_dir) + + if IS_WINDOWS: + paths_to_remove = [fix_windows_path(path) for path in paths_to_remove] + + for path in paths_to_remove: + if path.exists(): + rmtree(path) if isinstance(self._state_sync, CachingStateSync): self._state_sync.clear_cache() diff --git a/sqlmesh/utils/windows.py b/sqlmesh/utils/windows.py index 238ed353de..b2de5b8af9 100644 --- a/sqlmesh/utils/windows.py +++ b/sqlmesh/utils/windows.py @@ -3,12 +3,22 @@ IS_WINDOWS = platform.system() == "Windows" +WINDOWS_LONGPATH_PREFIX = "\\\\?\\" + def fix_windows_path(path: Path) -> Path: """ Windows paths are limited to 260 characters: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation Users can change this by updating a registry entry but we cant rely on that. - We can quite commonly generate a cache file path that exceeds 260 characters which causes a FileNotFound error. - If we prefix the path with "\\?\" then we can have paths up to 32,767 characters + + SQLMesh quite commonly generates cache file paths that exceed 260 characters and thus cause a FileNotFound error. + If we prefix paths with "\\?\" then we can have paths up to 32,767 characters. + + Note that this prefix also means that relative paths no longer work. From the above docs: + > Because you cannot use the "\\?\" prefix with a relative path, relative paths are always limited to a total of MAX_PATH characters. + + So we also call path.resolve() to resolve the relative sections so that operations like `path.read_text()` continue to work """ - return Path("\\\\?\\" + str(path.absolute())) + if path.parts and not path.parts[0].startswith(WINDOWS_LONGPATH_PREFIX): + path = Path(WINDOWS_LONGPATH_PREFIX + str(path.absolute())) + return path.resolve() diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 60ea3fd451..54b8cd891a 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -62,6 +62,7 @@ NoChangesPlanError, ) from sqlmesh.utils.metaprogramming import Executable +from sqlmesh.utils.windows import IS_WINDOWS, fix_windows_path from tests.utils.test_helpers import use_terminal_console from tests.utils.test_filesystem import create_temp_file @@ -700,6 +701,45 @@ def test_clear_caches(tmp_path: pathlib.Path): assert not cache_dir.exists() +def test_clear_caches_with_long_base_path(tmp_path: pathlib.Path): + base_path = tmp_path / ("abcde" * 50) + assert ( + len(str(base_path.absolute())) > 260 + ) # Paths longer than 260 chars trigger problems on Windows + + default_cache_dir = base_path / c.CACHE + custom_cache_dir = base_path / ".test_cache" + + # note: we create the Context here so it doesnt get passed any "fixed" paths + ctx = Context(config=Config(cache_dir=str(custom_cache_dir)), paths=base_path) + + if IS_WINDOWS: + # fix these so we can use them in this test + default_cache_dir = fix_windows_path(default_cache_dir) + custom_cache_dir = fix_windows_path(custom_cache_dir) + + default_cache_dir.mkdir(parents=True) + custom_cache_dir.mkdir(parents=True) + + default_cache_file = default_cache_dir / "cache.txt" + custom_cache_file = custom_cache_dir / "cache.txt" + + default_cache_file.write_text("test") + custom_cache_file.write_text("test") + + assert default_cache_file.exists() + assert custom_cache_file.exists() + assert default_cache_dir.exists() + assert custom_cache_dir.exists() + + ctx.clear_caches() + + assert not default_cache_file.exists() + assert not custom_cache_file.exists() + assert not default_cache_dir.exists() + assert not custom_cache_dir.exists() + + def test_cache_path_configurations(tmp_path: pathlib.Path): project_dir = tmp_path / "project" project_dir.mkdir(parents=True) diff --git a/tests/utils/test_windows.py b/tests/utils/test_windows.py new file mode 100644 index 0000000000..196589d9c2 --- /dev/null +++ b/tests/utils/test_windows.py @@ -0,0 +1,39 @@ +import pytest +from pathlib import Path +from sqlmesh.utils.windows import IS_WINDOWS, WINDOWS_LONGPATH_PREFIX, fix_windows_path + + +@pytest.mark.skipif( + not IS_WINDOWS, reason="pathlib.Path only produces WindowsPath objects on Windows" +) +def test_fix_windows_path(): + short_path = Path("c:\\foo") + short_path_prefixed = Path(WINDOWS_LONGPATH_PREFIX + "c:\\foo") + + segments = "\\".join(["bar", "baz", "bing"] * 50) + long_path = Path("c:\\" + segments) + long_path_prefixed = Path(WINDOWS_LONGPATH_PREFIX + "c:\\" + segments) + + assert len(str(short_path.absolute)) < 260 + assert len(str(long_path.absolute)) > 260 + + # paths less than 260 chars are still prefixed because they may be being used as a base path + assert fix_windows_path(short_path) == short_path_prefixed + + # paths greater than 260 characters don't work at all without the prefix + assert fix_windows_path(long_path) == long_path_prefixed + + # multiple calls dont keep appending the same prefix + assert ( + fix_windows_path(fix_windows_path(fix_windows_path(long_path_prefixed))) + == long_path_prefixed + ) + + # paths with relative sections need to have relative sections resolved before they can be used + # since the \\?\ prefix doesnt work for paths with relative sections + assert fix_windows_path(Path("c:\\foo\\..\\bar")) == Path(WINDOWS_LONGPATH_PREFIX + "c:\\bar") + + # also check that relative sections are still resolved if they are added to a previously prefixed path + base = fix_windows_path(Path("c:\\foo")) + assert base == Path(WINDOWS_LONGPATH_PREFIX + "c:\\foo") + assert fix_windows_path(base / ".." / "bar") == Path(WINDOWS_LONGPATH_PREFIX + "c:\\bar") From 725ebccd116df1f94da5e3a875cd021e204a20d7 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Thu, 9 Oct 2025 17:26:04 -0700 Subject: [PATCH 0969/1056] feat(web_common): make simpler including common package directly within repo (#5523) --- web/common/src/components/Badge/Badge.css | 2 +- .../src/components/Badge/Badge.stories.tsx | 2 +- web/common/src/components/Badge/Badge.tsx | 4 +- web/common/src/components/Button/Button.css | 2 +- .../src/components/Button/Button.stories.tsx | 2 +- web/common/src/components/Button/Button.tsx | 4 +- .../src/components/CopyButton/CopyButton.tsx | 7 +- .../HorizontalContainer.tsx | 2 +- web/common/src/components/Input/Input.css | 2 +- web/common/src/components/Input/Input.tsx | 4 +- .../LineageColumnLevel/FactoryColumn.css | 2 +- .../LineageColumnLevel/FactoryColumn.tsx | 12 +- .../src/components/Lineage/LineageContext.ts | 10 +- .../Lineage/LineageControlButton.tsx | 2 +- .../components/Lineage/LineageControlIcon.tsx | 2 +- .../src/components/Lineage/LineageLayout.tsx | 9 + .../components/Lineage/LineageLayoutBase.tsx | 173 ++++++++---------- .../Lineage/LineageLayoutContainer.tsx | 2 +- .../components/Lineage/layout/dagreLayout.ts | 11 +- .../components/Lineage/node/NodeAppendix.tsx | 2 +- .../src/components/Lineage/node/NodeBadge.tsx | 4 +- .../src/components/Lineage/node/NodeBase.tsx | 4 +- .../components/Lineage/node/NodeContainer.tsx | 4 +- .../components/Lineage/node/NodeDetail.tsx | 2 +- .../components/Lineage/node/NodeHandle.tsx | 2 +- .../Lineage/node/NodeHandleIcon.tsx | 2 +- .../components/Lineage/node/NodeHandles.tsx | 4 +- .../components/Lineage/node/NodeHeader.tsx | 2 +- .../src/components/Lineage/node/NodePort.tsx | 12 +- .../src/components/Lineage/node/NodePorts.tsx | 6 +- .../components/Lineage/node/base-handle.tsx | 2 +- .../src/components/Lineage/node/base-node.tsx | 2 +- .../Lineage/stories/Lineage.stories.tsx | 2 +- .../Lineage/stories/ModelLineage.tsx | 21 ++- .../Lineage/stories/ModelLineageContext.ts | 2 +- .../components/Lineage/stories/ModelNode.tsx | 14 +- .../Lineage/stories/dagreLayout.worker.ts | 4 +- web/common/src/components/Lineage/utils.ts | 2 +- .../LoadingContainer.stories.tsx | 2 +- .../LoadingContainer/LoadingContainer.tsx | 4 +- .../LoadingContainer/LoadingIcon.tsx | 2 +- .../MessageContainer/MessageContainer.css | 2 +- .../MessageContainer/MessageContainer.tsx | 2 +- .../src/components/Metadata/Metadata.css | 2 +- .../src/components/Metadata/Metadata.tsx | 2 +- .../src/components/ModelName/ModelName.css | 4 +- .../src/components/ModelName/ModelName.tsx | 4 +- .../ScrollContainer/ScrollContainer.css | 2 +- .../ScrollContainer/ScrollContainer.tsx | 4 +- web/common/src/components/Tooltip/Tooltip.css | 2 +- .../components/Tooltip/Tooltip.stories.tsx | 4 +- web/common/src/components/Tooltip/Tooltip.tsx | 2 +- .../src/components/Typography/Description.tsx | 2 +- .../src/components/Typography/Headline.tsx | 4 +- .../src/components/Typography/Information.tsx | 4 +- .../src/components/Typography/Tagline.tsx | 2 +- web/common/src/components/Typography/Text.tsx | 2 +- web/common/src/components/Typography/help.ts | 2 +- .../VerticalContainer/VerticalContainer.tsx | 2 +- .../components/VirtualList/FilterableList.css | 2 +- .../components/VirtualList/FilterableList.tsx | 2 +- .../components/VirtualList/VirtualList.tsx | 2 +- web/common/src/index.ts | 50 ++--- web/common/src/styles/design/index.css | 1 - web/common/tsconfig.base.json | 2 +- web/common/vite.config.js | 4 +- 66 files changed, 238 insertions(+), 228 deletions(-) diff --git a/web/common/src/components/Badge/Badge.css b/web/common/src/components/Badge/Badge.css index 582a1264fb..0efef35e41 100644 --- a/web/common/src/components/Badge/Badge.css +++ b/web/common/src/components/Badge/Badge.css @@ -1,4 +1,4 @@ -:root { +:where(:root) { --color-badge-background: var(--color-neutral-100); --color-badge-foreground: var(--color-prose); } diff --git a/web/common/src/components/Badge/Badge.stories.tsx b/web/common/src/components/Badge/Badge.stories.tsx index 09754d29a8..143440037e 100644 --- a/web/common/src/components/Badge/Badge.stories.tsx +++ b/web/common/src/components/Badge/Badge.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite' -import type { Shape, Size } from '@/types' +import type { Shape, Size } from '@sqlmesh-common/types' import { Badge } from './Badge' const meta: Meta = { diff --git a/web/common/src/components/Badge/Badge.tsx b/web/common/src/components/Badge/Badge.tsx index cd6df21c26..2bc23940ad 100644 --- a/web/common/src/components/Badge/Badge.tsx +++ b/web/common/src/components/Badge/Badge.tsx @@ -1,8 +1,8 @@ import { Slot } from '@radix-ui/react-slot' import React from 'react' -import type { Shape, Size } from '@/types' -import { cn } from '@/utils' +import type { Shape, Size } from '@sqlmesh-common/types' +import { cn } from '@sqlmesh-common/utils' import { cva } from 'class-variance-authority' import './Badge.css' diff --git a/web/common/src/components/Button/Button.css b/web/common/src/components/Button/Button.css index 7e8b856bf3..a95397dabb 100644 --- a/web/common/src/components/Button/Button.css +++ b/web/common/src/components/Button/Button.css @@ -1,4 +1,4 @@ -:root { +:where(:root) { --color-button-primary-background: var(--color-action); --color-button-primary-foreground: var(--color-light); --color-button-primary-hover: var(--color-action-hover); diff --git a/web/common/src/components/Button/Button.stories.tsx b/web/common/src/components/Button/Button.stories.tsx index 57fb9f26e2..8836a35a5c 100644 --- a/web/common/src/components/Button/Button.stories.tsx +++ b/web/common/src/components/Button/Button.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite' -import type { Size } from '@/types' +import type { Size } from '@sqlmesh-common/types' import { Button, type ButtonVariant } from './Button' import { fn, expect, userEvent, within } from 'storybook/test' diff --git a/web/common/src/components/Button/Button.tsx b/web/common/src/components/Button/Button.tsx index cc34ce192a..fd9baebdf2 100644 --- a/web/common/src/components/Button/Button.tsx +++ b/web/common/src/components/Button/Button.tsx @@ -2,8 +2,8 @@ import React from 'react' import { Slot } from '@radix-ui/react-slot' import { cva } from 'class-variance-authority' -import { cn } from '@/utils' -import type { Shape, Size } from '@/types' +import { cn } from '@sqlmesh-common/utils' +import type { Shape, Size } from '@sqlmesh-common/types' import './Button.css' diff --git a/web/common/src/components/CopyButton/CopyButton.tsx b/web/common/src/components/CopyButton/CopyButton.tsx index 3647121f82..1e5ba2580e 100644 --- a/web/common/src/components/CopyButton/CopyButton.tsx +++ b/web/common/src/components/CopyButton/CopyButton.tsx @@ -1,7 +1,10 @@ import React from 'react' -import { Button, type ButtonProps } from '@/components/Button/Button' -import { useCopyClipboard } from '@/hooks/useCopyClipboard' +import { + Button, + type ButtonProps, +} from '@sqlmesh-common/components/Button/Button' +import { useCopyClipboard } from '@sqlmesh-common/hooks/useCopyClipboard' export interface CopyButtonProps extends Omit { text: string diff --git a/web/common/src/components/HorizontalContainer/HorizontalContainer.tsx b/web/common/src/components/HorizontalContainer/HorizontalContainer.tsx index c1e2c66ed0..b92eaa418b 100644 --- a/web/common/src/components/HorizontalContainer/HorizontalContainer.tsx +++ b/web/common/src/components/HorizontalContainer/HorizontalContainer.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' import { ScrollContainer } from '../ScrollContainer/ScrollContainer' export interface HorizontalContainerProps diff --git a/web/common/src/components/Input/Input.css b/web/common/src/components/Input/Input.css index 0baae3c6bb..2cb6ab9695 100644 --- a/web/common/src/components/Input/Input.css +++ b/web/common/src/components/Input/Input.css @@ -1,4 +1,4 @@ -:root { +:where(:root) { --color-input-background: var(--color-light); --color-input-background-translucid: var(--color-neutral-5); --color-input-foreground: var(--color-prose); diff --git a/web/common/src/components/Input/Input.tsx b/web/common/src/components/Input/Input.tsx index 10ba151ab4..8d5c6fc7e4 100644 --- a/web/common/src/components/Input/Input.tsx +++ b/web/common/src/components/Input/Input.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { cn } from '@/utils' -import type { Size } from '@/types' +import { cn } from '@sqlmesh-common/utils' +import type { Size } from '@sqlmesh-common/types' import { cva } from 'class-variance-authority' import './Input.css' diff --git a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.css b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.css index d6eea6674a..8da848c684 100644 --- a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.css +++ b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.css @@ -1,4 +1,4 @@ -:root { +:where(:root) { --color-lineage-model-column-badge-background: var( --color-lineage-node-badge-background ); diff --git a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx index 350437c16e..294d3ca462 100644 --- a/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx +++ b/web/common/src/components/Lineage/LineageColumnLevel/FactoryColumn.tsx @@ -7,7 +7,7 @@ import { } from 'lucide-react' import React from 'react' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' import { NodeBadge } from '../node/NodeBadge' import { NodePort } from '../node/NodePort' import { type NodeId, type PortHandleId, type PortId } from '../utils' @@ -15,11 +15,11 @@ import { type ColumnLevelLineageAdjacencyList, type ColumnLevelLineageContextHook, } from './ColumnLevelLineageContext' -import { Tooltip } from '@/components/Tooltip/Tooltip' -import { Metadata } from '@/components/Metadata/Metadata' -import { HorizontalContainer } from '@/components/HorizontalContainer/HorizontalContainer' -import { Information } from '@/components/Typography/Information' -import { LoadingContainer } from '@/components/LoadingContainer/LoadingContainer' +import { Tooltip } from '@sqlmesh-common/components/Tooltip/Tooltip' +import { Metadata } from '@sqlmesh-common/components/Metadata/Metadata' +import { HorizontalContainer } from '@sqlmesh-common/components/HorizontalContainer/HorizontalContainer' +import { Information } from '@sqlmesh-common/components/Typography/Information' +import { LoadingContainer } from '@sqlmesh-common/components/LoadingContainer/LoadingContainer' import './FactoryColumn.css' diff --git a/web/common/src/components/Lineage/LineageContext.ts b/web/common/src/components/Lineage/LineageContext.ts index 4a90031217..7c76c2cfd4 100644 --- a/web/common/src/components/Lineage/LineageContext.ts +++ b/web/common/src/components/Lineage/LineageContext.ts @@ -59,7 +59,11 @@ export interface LineageContextValue< > nodes: LineageNode[] nodesMap: LineageNodesMap - setNodesMap: React.Dispatch>> + setNodesMap: React.Dispatch< + React.SetStateAction> + > + currentNodeId: TNodeID | null + selectedNode: LineageNode | null currentNode: LineageNode | null } @@ -74,7 +78,6 @@ export function getInitial< setSelectedNodes: () => {}, selectedEdges: new Set(), setSelectedEdges: () => {}, - selectedNodeId: null, setSelectedNodeId: () => {}, zoom: ZOOM_THRESHOLD, setZoom: () => {}, @@ -83,6 +86,9 @@ export function getInitial< nodes: [], nodesMap: {}, setNodesMap: () => {}, + selectedNodeId: null, + selectedNode: null, + currentNodeId: null, currentNode: null, } } diff --git a/web/common/src/components/Lineage/LineageControlButton.tsx b/web/common/src/components/Lineage/LineageControlButton.tsx index d3f3d5d215..14c7a6f2de 100644 --- a/web/common/src/components/Lineage/LineageControlButton.tsx +++ b/web/common/src/components/Lineage/LineageControlButton.tsx @@ -1,6 +1,6 @@ import { ControlButton } from '@xyflow/react' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' import { Tooltip } from '../Tooltip/Tooltip' export function LineageControlButton({ diff --git a/web/common/src/components/Lineage/LineageControlIcon.tsx b/web/common/src/components/Lineage/LineageControlIcon.tsx index f8bc679c6d..a16f611a63 100644 --- a/web/common/src/components/Lineage/LineageControlIcon.tsx +++ b/web/common/src/components/Lineage/LineageControlIcon.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' export interface LineageControlIconProps extends React.SVGProps { Icon: React.ElementType diff --git a/web/common/src/components/Lineage/LineageLayout.tsx b/web/common/src/components/Lineage/LineageLayout.tsx index a9b5ec512f..e19046780a 100644 --- a/web/common/src/components/Lineage/LineageLayout.tsx +++ b/web/common/src/components/Lineage/LineageLayout.tsx @@ -41,6 +41,9 @@ export function LineageLayout< useLineage, onNodeClick, onNodeDoubleClick, + showControlOnlySelectedNodes, + showControlZoomToCurrentNode, + showControlZoomToSelectedNode, }: { useLineage: LineageContextHook< TNodeData, @@ -58,6 +61,9 @@ export function LineageLayout< className?: string nodesDraggable?: boolean nodesConnectable?: boolean + showControlOnlySelectedNodes?: boolean + showControlZoomToCurrentNode?: boolean + showControlZoomToSelectedNode?: boolean controls?: | React.ReactNode | (({ setCenter }: { setCenter: SetCenter }) => React.ReactNode) @@ -85,6 +91,9 @@ export function LineageLayout< useLineage={useLineage} onNodeClick={onNodeClick} onNodeDoubleClick={onNodeDoubleClick} + showControlOnlySelectedNodes={showControlOnlySelectedNodes} + showControlZoomToCurrentNode={showControlZoomToCurrentNode} + showControlZoomToSelectedNode={showControlZoomToSelectedNode} /> diff --git a/web/common/src/components/Lineage/LineageLayoutBase.tsx b/web/common/src/components/Lineage/LineageLayoutBase.tsx index 6d3975d19a..93a55858bb 100644 --- a/web/common/src/components/Lineage/LineageLayoutBase.tsx +++ b/web/common/src/components/Lineage/LineageLayoutBase.tsx @@ -44,7 +44,7 @@ import { import '@xyflow/react/dist/style.css' import './Lineage.css' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' export function LineageLayoutBase< TNodeData extends LineageNodeData = LineageNodeData, @@ -65,6 +65,9 @@ export function LineageLayoutBase< useLineage, onNodeClick, onNodeDoubleClick, + showControlOnlySelectedNodes = true, + showControlZoomToCurrentNode = true, + showControlZoomToSelectedNode = true, }: { useLineage: LineageContextHook< TNodeData, @@ -81,6 +84,9 @@ export function LineageLayoutBase< nodeTypes?: NodeTypes edgeTypes?: EdgeTypes className?: string + showControlOnlySelectedNodes?: boolean + showControlZoomToCurrentNode?: boolean + showControlZoomToSelectedNode?: boolean controls?: | React.ReactNode | (({ setCenter }: { setCenter: SetCenter }) => React.ReactNode) @@ -100,8 +106,9 @@ export function LineageLayoutBase< currentNode, zoom, nodes: initialNodes, - edges: initialEdges, - nodesMap, + edges, + setEdges, + selectedNode, showOnlySelectedNodes, selectedNodeId, setZoom, @@ -111,27 +118,14 @@ export function LineageLayoutBase< setSelectedEdges, } = useLineage() - const [nodes, setNodes] = React.useState[]>( - [], - ) - const [edges, setEdges] = React.useState< - LineageEdge< - TEdgeData, - TEdgeID, - TSourceID, - TTargetID, - TSourceHandleID, - TTargetHandleID - >[] - >([]) + const [nodes, setNodes] = + React.useState[]>(initialNodes) const onNodesChange = React.useCallback( (changes: NodeChange>[]) => { - setNodes( - applyNodeChanges>(changes, nodes), - ) + setNodes(applyNodeChanges(changes, nodes)) }, - [nodes, setNodes], + [nodes], ) const onEdgesChange = React.useCallback( @@ -160,7 +154,7 @@ export function LineageLayoutBase< >(changes, edges), ) }, - [edges, setEdges], + [edges], ) const updateZoom = React.useMemo(() => debounce(setZoom, 200), [setZoom]) @@ -174,20 +168,19 @@ export function LineageLayoutBase< }) } }, - [currentNode, setCenter], + [currentNode?.position.x, currentNode?.position.y], ) const zoomToSelectedNode = React.useCallback( (zoom: number = DEFAULT_ZOOM) => { - const node = selectedNodeId ? nodesMap[selectedNodeId] : null - if (node) { - setCenter(node.position.x, node.position.y, { + if (selectedNode) { + setCenter(selectedNode.position.x, selectedNode.position.y, { zoom, duration: 0, }) } }, - [nodesMap, selectedNodeId, setCenter], + [selectedNode?.position.x, selectedNode?.position.y], ) const getAllIncomers = React.useCallback( @@ -202,13 +195,13 @@ export function LineageLayoutBase< return Array.from( new Set>([ node, - ...getIncomers(node, nodes, edges) + ...getIncomers(node, initialNodes, edges) .map(n => getAllIncomers(n, visited)) .flat(), ]), ) }, - [nodes, edges], + [initialNodes, edges], ) const getAllOutgoers = React.useCallback( @@ -223,48 +216,32 @@ export function LineageLayoutBase< return Array.from( new Set>([ node, - ...getOutgoers(node, nodes, edges) + ...getOutgoers(node, initialNodes, edges) .map(n => getAllOutgoers(n, visited)) .flat(), ]), ) }, - [nodes, edges], + [initialNodes, edges], ) - React.useEffect(() => { - setNodes(initialNodes) - }, [initialNodes]) + const connectedNodes = React.useMemo(() => { + if (selectedNode == null) return [] - React.useEffect(() => { - setEdges(initialEdges) - }, [initialEdges]) - - React.useEffect(() => { - if (selectedNodeId == null) { - setShowOnlySelectedNodes(false) - setSelectedNodes(new Set()) - setSelectedEdges(new Set()) - - return - } - - const node = selectedNodeId ? nodesMap[selectedNodeId] : null - - if (node == null) { - setSelectedNodeId(null) - return - } - - const incomers = getAllIncomers(node) - const outgoers = getAllOutgoers(node) - const connectedNodes = [...incomers, ...outgoers] + const all = [ + ...getAllIncomers(selectedNode), + ...getAllOutgoers(selectedNode), + ] if (currentNode) { - connectedNodes.push(currentNode) + all.push(currentNode) } - const connectedEdges = getConnectedEdges< + return all + }, [selectedNode, currentNode, getAllIncomers, getAllOutgoers]) + + const connectedEdges = React.useMemo(() => { + return getConnectedEdges< LineageNode, LineageEdge< TEdgeData, @@ -275,6 +252,25 @@ export function LineageLayoutBase< TTargetHandleID > >(connectedNodes, edges) + }, [connectedNodes, edges]) + + React.useEffect(() => { + setNodes(initialNodes) + }, [initialNodes]) + + React.useEffect(() => { + if (selectedNodeId == null) { + setShowOnlySelectedNodes(false) + setSelectedNodes(new Set()) + setSelectedEdges(new Set()) + } else { + if (selectedNode == null) { + setSelectedNodeId(null) + } + } + }, [selectedNodeId, selectedNode]) + + React.useEffect(() => { const selectedNodes = new Set(connectedNodes.map(node => node.id)) const selectedEdges = new Set( connectedEdges.reduce((acc, edge) => { @@ -294,24 +290,11 @@ export function LineageLayoutBase< setSelectedNodes(selectedNodes) setSelectedEdges(selectedEdges) - }, [ - currentNode, - selectedNodeId, - setSelectedNodes, - setSelectedEdges, - getAllIncomers, - getAllOutgoers, - setShowOnlySelectedNodes, - setSelectedNodeId, - ]) + }, [connectedNodes, connectedEdges]) React.useEffect(() => { - if (selectedNodeId) { - zoomToSelectedNode(zoom) - } else { - zoomToCurrentNode(zoom) - } - }, [zoomToCurrentNode, zoomToSelectedNode]) + zoomToSelectedNode() + }, [zoomToSelectedNode]) React.useEffect(() => { updateZoom(viewportZoom) @@ -363,7 +346,7 @@ export function LineageLayoutBase< position="top-right" className="m-1 border-2 border-lineage-control-border rounded-sm overflow-hidden" > - {currentNode && ( + {currentNode && showControlZoomToCurrentNode && ( zoomToCurrentNode(DEFAULT_ZOOM)} @@ -373,24 +356,28 @@ export function LineageLayoutBase< )} {selectedNodeId && ( <> - setShowOnlySelectedNodes(!showOnlySelectedNodes)} - > - - - zoomToSelectedNode(DEFAULT_ZOOM)} - > - - + {showControlOnlySelectedNodes && ( + setShowOnlySelectedNodes(!showOnlySelectedNodes)} + > + + + )} + {showControlZoomToSelectedNode && ( + zoomToSelectedNode(DEFAULT_ZOOM)} + > + + + )} )} {controls && typeof controls === 'function' diff --git a/web/common/src/components/Lineage/LineageLayoutContainer.tsx b/web/common/src/components/Lineage/LineageLayoutContainer.tsx index 4bd0d42402..2ba0e00d56 100644 --- a/web/common/src/components/Lineage/LineageLayoutContainer.tsx +++ b/web/common/src/components/Lineage/LineageLayoutContainer.tsx @@ -1,4 +1,4 @@ -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' import React from 'react' diff --git a/web/common/src/components/Lineage/layout/dagreLayout.ts b/web/common/src/components/Lineage/layout/dagreLayout.ts index 554d427f03..d7a5c01e2e 100644 --- a/web/common/src/components/Lineage/layout/dagreLayout.ts +++ b/web/common/src/components/Lineage/layout/dagreLayout.ts @@ -36,11 +36,7 @@ export function buildLayout< const nodeCount = nodes.length const edgeCount = edges.length - if (nodeCount === 0) - return { - edges: [], - nodesMap: {}, - } + if (nodeCount === 0) return {} const g = new dagre.graphlib.Graph({ compound: true, @@ -92,8 +88,5 @@ export function buildLayout< } } - return { - edges, - nodesMap, - } + return { ...nodesMap } } diff --git a/web/common/src/components/Lineage/node/NodeAppendix.tsx b/web/common/src/components/Lineage/node/NodeAppendix.tsx index 5a703a468f..48194c1442 100644 --- a/web/common/src/components/Lineage/node/NodeAppendix.tsx +++ b/web/common/src/components/Lineage/node/NodeAppendix.tsx @@ -1,7 +1,7 @@ import { cva, type VariantProps } from 'class-variance-authority' import { forwardRef, type HTMLAttributes } from 'react' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' const appendixVariants = cva( 'node-appendix absolute flex w-full flex-col items-center', diff --git a/web/common/src/components/Lineage/node/NodeBadge.tsx b/web/common/src/components/Lineage/node/NodeBadge.tsx index 8c894ecca2..b05283dfa8 100644 --- a/web/common/src/components/Lineage/node/NodeBadge.tsx +++ b/web/common/src/components/Lineage/node/NodeBadge.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { cn } from '@/utils' -import { Badge, type BadgeProps } from '@/components/Badge/Badge' +import { cn } from '@sqlmesh-common/utils' +import { Badge, type BadgeProps } from '@sqlmesh-common/components/Badge/Badge' export const NodeBadge = React.forwardRef( ({ className, children, ...props }, ref) => { diff --git a/web/common/src/components/Lineage/node/NodeBase.tsx b/web/common/src/components/Lineage/node/NodeBase.tsx index 78033a4099..89342d83c8 100644 --- a/web/common/src/components/Lineage/node/NodeBase.tsx +++ b/web/common/src/components/Lineage/node/NodeBase.tsx @@ -1,8 +1,8 @@ import { type NodeProps } from '@xyflow/react' import React from 'react' -import { BaseNode } from '@/components/Lineage/node/base-node' -import { cn } from '@/utils' +import { BaseNode } from '@sqlmesh-common/components/Lineage/node/base-node' +import { cn } from '@sqlmesh-common/utils' export interface NodeBaseProps extends NodeProps { className?: string diff --git a/web/common/src/components/Lineage/node/NodeContainer.tsx b/web/common/src/components/Lineage/node/NodeContainer.tsx index 0506771eae..c72d60e4ed 100644 --- a/web/common/src/components/Lineage/node/NodeContainer.tsx +++ b/web/common/src/components/Lineage/node/NodeContainer.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { cn } from '@/utils' -import { VerticalContainer } from '@/components/VerticalContainer/VerticalContainer' +import { cn } from '@sqlmesh-common/utils' +import { VerticalContainer } from '@sqlmesh-common/components/VerticalContainer/VerticalContainer' export const NodeContainer = React.forwardRef< HTMLDivElement, diff --git a/web/common/src/components/Lineage/node/NodeDetail.tsx b/web/common/src/components/Lineage/node/NodeDetail.tsx index 61e22d169c..f57978d865 100644 --- a/web/common/src/components/Lineage/node/NodeDetail.tsx +++ b/web/common/src/components/Lineage/node/NodeDetail.tsx @@ -1,4 +1,4 @@ -import { Metadata, cn } from '@tobikodata/sqlmesh-common' +import { Metadata, cn } from '@sqlmesh-common/index' import { NodeDivider } from './NodeDivider' diff --git a/web/common/src/components/Lineage/node/NodeHandle.tsx b/web/common/src/components/Lineage/node/NodeHandle.tsx index d50d90422a..6e7aa4dd22 100644 --- a/web/common/src/components/Lineage/node/NodeHandle.tsx +++ b/web/common/src/components/Lineage/node/NodeHandle.tsx @@ -1,7 +1,7 @@ import { Position } from '@xyflow/react' import React from 'react' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' import { BaseHandle } from './base-handle' import type { HandleId } from '../utils' diff --git a/web/common/src/components/Lineage/node/NodeHandleIcon.tsx b/web/common/src/components/Lineage/node/NodeHandleIcon.tsx index b55d96a041..caafa617a9 100644 --- a/web/common/src/components/Lineage/node/NodeHandleIcon.tsx +++ b/web/common/src/components/Lineage/node/NodeHandleIcon.tsx @@ -1,6 +1,6 @@ import { ArrowRight } from 'lucide-react' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' export function NodeHandleIcon({ className, diff --git a/web/common/src/components/Lineage/node/NodeHandles.tsx b/web/common/src/components/Lineage/node/NodeHandles.tsx index 453ff74317..3d7d6e08ab 100644 --- a/web/common/src/components/Lineage/node/NodeHandles.tsx +++ b/web/common/src/components/Lineage/node/NodeHandles.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { cn } from '@/utils' -import { HorizontalContainer } from '@/components/HorizontalContainer/HorizontalContainer' +import { cn } from '@sqlmesh-common/utils' +import { HorizontalContainer } from '@sqlmesh-common/components/HorizontalContainer/HorizontalContainer' import { NodeHandle } from './NodeHandle' import type { HandleId } from '../utils' diff --git a/web/common/src/components/Lineage/node/NodeHeader.tsx b/web/common/src/components/Lineage/node/NodeHeader.tsx index 154dc166de..41e83aaa4e 100644 --- a/web/common/src/components/Lineage/node/NodeHeader.tsx +++ b/web/common/src/components/Lineage/node/NodeHeader.tsx @@ -1,6 +1,6 @@ import { type HTMLAttributes, forwardRef } from 'react' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' /* NODE HEADER -------------------------------------------------------------- */ diff --git a/web/common/src/components/Lineage/node/NodePort.tsx b/web/common/src/components/Lineage/node/NodePort.tsx index 7380716f02..207be24576 100644 --- a/web/common/src/components/Lineage/node/NodePort.tsx +++ b/web/common/src/components/Lineage/node/NodePort.tsx @@ -1,7 +1,7 @@ -import { useNodeConnections, useUpdateNodeInternals } from '@xyflow/react' +import { useNodeConnections } from '@xyflow/react' import React from 'react' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' import { type NodeId, type PortHandleId } from '../utils' import { NodeHandles } from './NodeHandles' @@ -21,8 +21,6 @@ export function NodePort< className?: string children: React.ReactNode }) { - const updateNodeInternals = useUpdateNodeInternals() - const sources = useNodeConnections({ id: nodeId, handleType: 'source', @@ -45,12 +43,6 @@ export function NodePort< const leftId = isLeftHandleId(id) ? id : undefined const rightId = isRightHandleId(id) ? id : undefined - React.useEffect(() => { - if (leftId || rightId) { - updateNodeInternals(nodeId) - } - }, [updateNodeInternals, nodeId, leftId, rightId]) - return ( data-component="NodePort" diff --git a/web/common/src/components/Lineage/node/NodePorts.tsx b/web/common/src/components/Lineage/node/NodePorts.tsx index f417dea9e4..1f40dc764f 100644 --- a/web/common/src/components/Lineage/node/NodePorts.tsx +++ b/web/common/src/components/Lineage/node/NodePorts.tsx @@ -1,6 +1,6 @@ -import { cn } from '@/utils' -import { VirtualList } from '@/components/VirtualList/VirtualList' -import { FilterableList } from '@/components/VirtualList/FilterableList' +import { cn } from '@sqlmesh-common/utils' +import { VirtualList } from '@sqlmesh-common/components/VirtualList/VirtualList' +import { FilterableList } from '@sqlmesh-common/components/VirtualList/FilterableList' import type { IFuseOptions } from 'fuse.js' export function NodePorts({ diff --git a/web/common/src/components/Lineage/node/base-handle.tsx b/web/common/src/components/Lineage/node/base-handle.tsx index e6b8f0c24b..0ce6a98745 100644 --- a/web/common/src/components/Lineage/node/base-handle.tsx +++ b/web/common/src/components/Lineage/node/base-handle.tsx @@ -2,7 +2,7 @@ import { Handle, type HandleProps } from '@xyflow/react' import { forwardRef } from 'react' import type { ForwardRefExoticComponent, RefAttributes } from 'react' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' export const BaseHandle: ForwardRefExoticComponent< HandleProps & RefAttributes diff --git a/web/common/src/components/Lineage/node/base-node.tsx b/web/common/src/components/Lineage/node/base-node.tsx index d349ca601a..f1b5c7d509 100644 --- a/web/common/src/components/Lineage/node/base-node.tsx +++ b/web/common/src/components/Lineage/node/base-node.tsx @@ -1,6 +1,6 @@ import { type HTMLAttributes, forwardRef } from 'react' -import { cn } from '@/utils' +import { cn } from '@sqlmesh-common/utils' export const BaseNode = forwardRef< HTMLDivElement, diff --git a/web/common/src/components/Lineage/stories/Lineage.stories.tsx b/web/common/src/components/Lineage/stories/Lineage.stories.tsx index 115be3c2c0..76c4229250 100644 --- a/web/common/src/components/Lineage/stories/Lineage.stories.tsx +++ b/web/common/src/components/Lineage/stories/Lineage.stories.tsx @@ -129,7 +129,7 @@ export const LineageModel = () => { }} >

      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

      6=3(=QnPeuLDdti0n3-y(nd#Q;7fbjCmF@fSxxm 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

    HqCw|MnRj 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

    sd7h_1xEk# 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

    u4>)B)RS521@ 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 literal 641261 zcmeEuby!u|`adlph=)*V@X#gQDc#-O-Q6i5-3k&C(%p@Sgh+QIA>G}-?VXu>=Z-q# zoqNB}?;j5j51YMLyz9-+`>wT5h>VmlG9nft1Ox=KsEB|Z1O#3faM=tG1>89nzG(yg zz!>vM@fHFEQphH7Guq97ok7)=#a98@GF*!8U~X><*&^$clTEN#9tfZ%jt2QDoQ9drp? zEG?|;*Z^x#?{rcc68t(BKk7XKR>_FY3O45ubHgue_a-^K-w=)Xz6I4 z(0;3%gQ?MXx_x=_OSgZl>zC;`zYNAMW9njPp(r-nBYdd)xU427tx?j8e?$+;Gf1#XR+RoGvVComveQEyH zy@0jtQ0F9q^ z^wW-hr#e6F=y%NcX-7Y?_uotje%jGbJNh+e{TX3@M%dpO!%xrfJ7)ZhuscJ$MZz6O^+J;P7W@Y6H=^b9{e!{77+U*~E+t6M*- zTfbALpAq(V%=p^PeE-f*JNjuyzf+x`cJw=DeBWw*#-snjYQARcPp$i@bwBf?pZU>G z&+s?>z|VN}Gamh^>YP1-{I58ao(I*la!^;f*tvNweCJ0zRX1#a1L-`(2hv|1``0S^ z*LT7}Dn&}e>kefqA6E|(5oR}u9>2Lf>)gD$x;i?ll#Qo+_5&}rEciq5aB*>Y?tya9 zzIHJG!5s=x)V!H@pPtOIXs8y9V%>#PR#wJN-DUiNH(z|8A;7O&sXUE;`Zr#5_>E6P z2vmtZQaKf_oTuUhva_^II6tr+IRp<;#Ued~-v;?pE%`Px0OG?smttdMw?Xy(L{QDs z+fD`^oqX>M;(lZ`d(ExVz-Kq@lll)Z>ibH{%LMDzG-tv%)jg$FG-kmZdYTeug!%(R z`feb>uNQko7DoMmvV2!OXLo$1au{Ap+Dtfi~1FWnn*1F}onwyoIo96lAIpYr**S8JOM#WdFgas?y zsTq!KmV=au;QQSCjTPzTQ2h0j(^yE;LOQq|O)6A)cz8rU94K0yHr5O~wV5Ur*)3>k9Smlg*QdTc*t}9$Pmz&1BEH!mU+)M1;@b0TGHk zrqXx!4gQqczv_K+ZY*HeX@2QjU4L+N#F$b4xWYYUzG?I!4beaRjUjxyw&DGfrap8Q z{DxbXk~TQfD4Et>O@w5vk{HWDx8oi&gb_`h8t(_QJZA0kfwyOM_x{vIa?5!USe^)+ z-*86T$9rQKsH-D14AP!kB_h-Y*7+f8T6e#Cb(d)#IH#AUwio{^441TrKwZ+ZD z6M2eEYm4;{W|K+k(7$wGPic}Gx2mzWwmxMKe2akEMY3>OY=K*%*|gt3Hj(YF=@jPe z=vXyQepkweR}*H@waxR^xwNxW_}=80A&&~_pUBPE*cW7_6I^%YO0>F4y}GafMZDE9 zY4LgRg+I#_6!U$uPZ3Ef&$RC+AL-$*)<(PF=XcD!=0&&_aC*Fta%o5g<@kW?leYU6 z{%yTAX!E;qfSY{pkhH{^&6ggQDA}DoMd-R&QBA9iCrpZBgAM z&0E!AcVbAac}+&EXw7_fbaj1%6w!jx^y;!ni>8(CJJbL5Oi2_^QlxswsZ$NrsW4YQ z8e;yyT7CTi)8K}05M{v#uH5`&bGBL1BE;`}Tv4a3jIloha7}i$>}JU7%B|^c+TRWW z>5riH2b4I?mDH%`n>{HlY0!Rvaqw}p1X&Fp_6$~F)EZGo_LSgNyt{DsN1FzjFu2J- znAS{3k5T70v}u+&i>9&Juq;#P_t@8X@kMSLvLgS+Sx2PaW*9131wT zWVx?q!CjTMx^Qx{vtxw|;WwAxJZRrmm37YjBmEL~zl}@&R9$&tVIm?U#kB;_84@(o*MC4lFJZma^qmQ`xq z(ecvXV+AKt9L`HzZ>jtDn7R6QzSYXt7?K6VBhUJ8M&PUV1$R1LZq%HuJ4|gmou+)& z;7M#LX1#eQzx?7JoWEDg?YMRRrF_YFvIL2{rgoTj-4N0KZ{G#*ThRU!r`C!IwSS7b zdwFsJ#B;V;^~w(7?+7408cMsxi@KtCczBpThv6IF3*d`|P#aw<)LX9bZ;I_AywJlj zfBwAH3ijDM-G`49yc4-f5s6ig1Y7=-2= zvZP32YXqW|bvT2Myx%Y&<833?qddY!>z--d>UuK2k%1yV~AK&u{v zzBgKB5B-55e#ea8Nl_9r7)xFMbjZvQ;V5iv^Fkz%^){?(sH@WU$ zrY-ay5vZUfCMND{l{u;cDacCd;&F|`n9d=QNW&V1g-}3H_nl_PW zZVrxE;-<(xQJ(pre7YZz^0y?Fo5u@w=r^iJS$23JXlrlJ@bIpgnC|3X(*g;l4#4C& zs9xe*qJhn0BJm4tse|KcDB$aJmiAqy3p9nlEr`D!*;fcbfd6!9Nxxg!-}2sd>vcCd|LeKGDJu9aIfB&s=eavh z4cCKwk5eyCPMnyN1t(60OB}hs-Q(YEj&~N;tJocKOq<+SBmxg5C|+$yJ(S$5QOmpG zV&CoBHJdwx$%#FscC$dPp#0(ARVa8I83MWL$5svVyZx(&>ke4EyHq5pPeKKD4-XFk z2eZnk{UaF$qdEXhDlXNX9HR$t*=7y?|&3EW1kneeqKuwJ+&MH{+JL@LF6uOq}k{r#-mp zp>1hL*iWBq;Wj++2!Z|qCi%hvZWTcNJv7uYg`}~flNHT$+A{tD6ctEPZZ= zOSPO@`^WZ(C+s2!75xAY!TB?f=nzU%!Iv?9NwzEkJ{%tjOgKw`;-iJ}*8Z(Rk z5zV^;LLKrXI3tmoUUxvzQkr3M^z_7Kk=nkR%X73iWv}3{tIru;pPijGt76ybj>h1| zi@dzPfgIN{`vGqJfSP{Sx5~)-(lTk*7%G1$j|ey%Ln>nsPSw=ZL`FvXztveqnDs88 z?9lGV*rnT@Z>B!%95iv5NZ+FmW%>pv`fup?3iM9A2C@oVy*GGfoOt;8H>)myg#yvv zv?sVuuOk?iM@PXK?0_%R%xLyQ$@RY(?RUBH6%}8?t8WURY;$a;9gwSs=XzzVA}iNk zw>LN3ZRPjqFWrV-crqtDHq81L>M~;C8feA-{fgfYk@Xu+&MyaWvf7QDT9Hz_Jphk0 z$UdZ9TQhX%Sy@>T<#V9ioQyVmYZNKnk7?dj4f`%y|1bFRdvRlt03bsbnTwaLXT^^G zWgO@J`}cx?eDLuTId?CIQ&Llfq?`-4uZNv(wryBv&R@WRzsc+0kNK^T_SbOdw_d%N zs6(?`Hwm({9^AD0Ne}Ebw6$;dJMjf&8_xX>-Zz) z{5RSFfg-?RyvaXE`__e%#{Pb^Hy2j;hgY_tb#VTC-E6bKzD=lO{a-@EtHh=Xt_m#S zsl7h;ZSNQGKJaiv5JYPvPy~Kfzy$=6PO#1zeEMSuBA*Z)0zc0*z6Pz5mI#XiG5qGs zJ7;HI{^) zT0ZZTN#Q+iZ>|MG(6!C|Hvu%4lS^)l%Y7NPAU~Pa*%aoR((StMk<~KGc^h;n*w=h5mav zT>FkPDK8jnAEPqj-HdkQ^LU9|vPY?1)+b~fk88g7z3-crvb#H%(_#}^d^%h2C~kZ+ zyKglY$G+w!q-ImVY@&J#*L07Od(HpiWLHXr#X-Km89Pu{L4JNgp#cxqY@5A&#$sSy zdUkHkx&M{&QiI*l>fC)8zZ~S-eAbE-cq3}`b&cU=R%|1sm$Cb4gF?1#F-sXG85^Zwo zXH*>c!EVgk3@C?3yi8{=c(>{VeVdt#LXyjl_DcF}JV$W`_Rr)b!{HlCxu=;xLb-orMIFAKo zs9`?CJESN)4*U>i?7tDek^gDFPHcnS+YX9KmpJ-nR9iDcadt|__ksmSB%u$>QG&>J z^h1jIRqJi83JV`{V%NFIaXE}z?Xg9DE918@cAiE%k@ce#Lirkdf|ew z)eD8I{iRM??Pi$llF{0+i|olgQo+rIt|zBG#Zo!Nq>gLa*P^)WXxf%JD95bfp1F_l zQoOqdmX@vkGmd4wr<3j<;oT^*jPPOeNSB8T$tvgFHUr!17Z++)PRU-Qp+H1b_tNfg z#xQhYG?L&Y!7}3D5gzZq$ZfGLt7Fg=DoB0Uq2&3BvisZ)UgW)9BPj6@NTeQ>4qJmG-#~@Nm8M0U#%&??A?;+ zi^Pm{(GkIvtxe7R#34r`MWmmAuwi5Fe}orT>^-`9yR%`=?#_aj*AgqT^`;8W z3e14?%fh9H)#(NtQ0s-`bpi-Q3ojF(;dyoLK|1!o4R}&sEhc=2Eu*;+#3MH+L&cdA zEz*cM)!jDh@>=#WPRXcvt)L4hf<8=0J879Q?Z9}00&Vu@$0;RP#~|aOg}AXXUe|8` zxpLIA746##{OF)^mC##%kKk?rIyHqC~jVlSW0}kk1t>{qbo!@!ouCse6oEi;n_og3r@{H@Ax0 zo#h^rEz#)R)RNCpE;}~nE5(%MvsVre?TLloY+DIgAmu6H?rm&{y~{uWG3Uv5PENjz zh#jGl_r5qXjmiH^AsbWaM4WnAJ$JZaK2(Aeh+foEsc9XSjTBImnAd1rP6x-t{h0LR zY=3G0{ER_vr@?u5Sts_$MDpWE)>(slLc8;^Y0XB%1^t-gjH{+&N2TgnkLoUcXm z)pB4`%0~1jgR(BDjU_DCZ0Bo1Es#9b8awBd$U0r(h}`GC6b@6|C6&Dr?Uw3mPBNca zv{y_Lby%u8du(}ln;jGttdDE1wk8=ulkPZvm`LUE=4Mr$6-L&jrwB44-EYO9_A@m> zod`f{dx9b1M^{%ry3PnIMrm!5g(Djat!IZ3MtSsz)*xXPrm9R-JZQvCVz5GPs;=o( zDIv{Qr&OmY0)wm@sezXehb2!W(|B1*HZ^gUdLal)pNLcDgaWsyxBAg@@ zpTdr*h%9g7w=c9 zf^zg}29+-BU2STOgd~AQLv9bGKLUMIc#~MG8-h~b);{!Hk;753;b+zHt|3m?k#TPJ zbVcuWqPZ07S3jR{XxE~RbWNRQn+ZN6VSo? z7Qa-#t_yO-Bv@Yc<%I^Vh@)0TIjohUcf4(G{7Z7e%xDW*9gn3Iq8Zp^0oXQeckn{hhn$1k6^IcyN&S84mB^i!GTu%a*S zfJ?BJ7$B|eK3B{y!!f}iIXtxKyodCb{`PDs5)MZ{E&pOlWg@^>j*EtkHBHLQ(bdso zw(MShu0B(V`SyJju(j-!>D_O^+zE+RW8Iu6*puG46K$0T#?*org$Lv`Eoqi>STic* zjW5LH?wCuICl!f#_~jE$#bov7IaKE0Z3d4Tc^B&_9VB9%oFV~Ac`kQc%Y{h2s&;&z z%`%9<(I+8GJY(K&tFxZ?;0YKcEj1@6*ipn(``XJjL}PcW3oS;3)6|+&I!2`N?FgEd z9+!9=q(v$j2|9zTB9>koRTt!f2I){%P4PIiXX;Wwfv*P}i;}3(_?u9K4ZX`t>$Rdc zzvGQQh4r3z44_*NFF&$s!SsNmU97+<9BCW2-EYqwh;n;^LioZYjmY)cRUB|Wuthbh zvf9i4(O}75w(^Ks;aF0>^znN8QFrudfjIF~m1*Wp?aJv#O*UQXcBL=R)VJEJlrA}X zf}WI0rxT~@lI3}+x$AT|WV^IvlaJ(FVFW##&f0(#whc^yn;BZ!2&)@;-CQmI0L800 zoHf9h%CdaEH{~whSibXSHFKLKCK*e?4O5V3|9Ofh^XtQQXX;JcMgl6Av~G9Std2#ePL(b{Zc}StVeVxMJ(C26auL|LU6J$Fo)V-|y4Y|NH;W@QmqlA6Sa7aPkrwgR~+Dbl13 zuY9Jo7q)sFO4r(HS9uSt7_5u^cpYK*TMCLN86W-;bS@&CQEj2uW?)2;iFR+WUJ8gi zcZ9^`8W|HKE?pF=Q-sycWA$d(R14jX+@{KwUScQyJUDZ-S^P)4YU*9S zFDM_4x|N2GhYDrTzdM~iGDJ=gShRE#b&Rj_8ntzDd#@1lCSUkz7*5rDF!NmZT`{o` zFzEE<2=~Wmk(u!~AKj%_6@whLgt}5iUcKkoPJ`GTv@07o@&KWau_HeD0-Z1O{Q=-<8gcB8ZPe6w z$WI*YJ~;}q6Xe}E18|0Y-cw}w`qpXeWAo#3An6r*zgP%9&`7c3W6#slJOJ>B9n$Vx z6J=>2QaJ4)CEZOs%y(AFi4y zxh1uTncLBttc!x}&I3>GUi_dPf{{9co^umFibH+Bsm3xwf^YdTpg2Ph8eq?QH(Ryv z(!7W%-oa|ynqzM~?{V^D#nuI_kJJe)JDyw>3oqcj^A+h?r$M9a#hCkRF z4Sp~s?4^)7)9ZMXDkY@1#MbmxO1T;ILww*2$2isGbJrHn?fIecv2)W-JeRS^QYPrd$U`UonUXPj z^QhRtOmhr-cP6IB3HdinbB(TtUrIn~3Eo{hk4}Q~RvNexJGI?yZSQN>X2c9N<5?W` zAuvG>h=sakK(k1)I+kdZygY`zD~Io@$X898eaEru{_&?c`$AW77Cy&A=9OUUDNuNu z8oJ$FrngjvmEQ-3bsVC;r{Alny2HOpN!hOByiu!>%v(%G*On8n;@*dPAB^!FIMf?X z3@a~t*?7M-LLL4&2I&5pn&K?qgVA`q0`sf4*i`X(3>(s(gdB*1Yv^g&JVwd_vdB@2 zFleKEC(i=cIQi5$1OqscK_T1liP{Vy?JJwPFEPzj2%}j;m_@-6?Q02B!bG-NLd@t$ z1$V$8YhLAJVhm!?rm0$_6fq}@V$nX|())^60(w=F5hC0BwgS-T9sysMg>_;8O_0|6 zv}_8c=E|uV47wFxI6qvyrN6nH$3DmtgVLM}@q5cu{DLx;)`I@M(rofJZQ+)AOKLCE zL(N>DGCj7b=2h0GKH&JqnEJ}ThjI|>hL@6>Nz|kj_C>4*HB&lC9xGKd{O6^}8U?g> z7aFQnVU_Fd!;@EM8wPa|YG`SF$gAFXG;_pJE9+8!e*(muiBoC0lWE~Yn#9IB-Df_b zm@gyCU}mbyOfK9W=9Vng)0m$;-LS$=`U$mA)>D8hyOM4V$e~3IG>Qct2lj97^z=?# z6-C-NxSv|Zw%k0#OhP`8)+^wGERrz`ENx~KXTb>0Im30`!w@@$14j1iPxxZO0m)^F_PggOO zy|k(*%HVgbbErINM$8k*2^4S~XTIrLc`74uUx?)vQ#3h|fQXMtTcSC;HIcPG-O^N4 zqiXtTwksAGl*7ac3K>4gs%C>qIuF>-t`v@`g9|#ua*k`xRb>fj?@51tJ!bqtlBmod zGD>6%qAp5(gq06jA19<;RD!JVrAeQR4w0>m0QX~Y&As1PKmm=A%5lpjoLN_6c(G#8qMfV@RvNz~>+pLIB z>5|_SxcER(+_tqt-1a#ICN?^ft+bXOYq}v^G?SLdrCry161Q%F(Z=P;$3dg4A?-nm zo}?~o@_~_JSS&C194HEoc(F#>6}{g_(2~A23s#Vhh6jVv3=|b0U%rZjHl>eN8RiFt zh?g2qJ_%_jCL?--%%|$61GodYaf3(amXK6VPWjB@JxTYXL^$)frTD*E6(>TeXbtK1 zo1K*kLGh-SNs{E)x*G5%ipu71q|>-Z7xQ|MxvW45m)#1%2GkmXC0ovZ0mL^M{5P7@ zF*q=g;(h45?FwYUG-HFooKapFEhX%SMpStLh;^)uJWrkt^PssoH;F2Hs&;gq71N>h zp$n#8fuH$^-+P?|y$9DNwIo|FjV;Y$SM-5dzrLy$Z?45#TW}|_k_v<6%ArEivr2=ngcIB|8`~IDKcz3NdkA+{>!@(n?8Hz!&%hboP@lT9CuY<@A*5Aft=&eT5T;?JF%~2ZY>OCyMdX26m_H&r0vi>e zsPCyVx{Gnu5svL%5L~NupWe=jh*dVr3aZ@R5-Qh#H4R4FTV><@hqNUY?8NS%H7QLz z>EtQKI?w)O7HhDw;5A)))+T-G%8t&3<$(3MHI#)v0mHy0bk!D1rDO+}a@DNro~FBn zzj+aY!|?2LA<7ehX$uU({;J~15m8;kdbbQ&n!fHB^fh?+#FL0PE(c>o9(az&%FAyM zSJ#IdX~c(CovfUraT5-eO8Yl24lppd{7!FxVG@~N85ryC`CJ}%W= z{P&gj1O>&7<(_9H_~kQ~c6Zzv#8Rh5prxbdOBp#k%c`%K6B_WazEUhRHmGrf8#I$H zqt4R(%@zM~lZjUl2HoOd5^svsBjHt}dWC0ON%#4y!qR#im+P=fBH{QQa&I}@JVY`S zMP&=(_yxuBr$G64!@PMPaq8LWXT<)8>y&;1i`?OgopLoD_kp4XpLsc8a+3K~jITw& zP6NUHv4tHdppXZZlUhW!=aczBdE%i#xHi*Ek&jU06!Ji7h(Uw+4a`6Qh%Wq5oYca_ zMg0-U5I6i50V>g3#EGrk%n0$PUUIRQja$k-AR-K<5zy^T{wwn+e)XvZA&LPhzl+yP z8xF4@zNVj8)9yLJC(2{ma8@neuhBKoMN5NxsRME0Y}4!;c~%S;H&G3*8dq59u)rB(uh>Po-(uqH8+-@!==~m%%Y)nlr5G+Yi@CAvPW4FBa2&Zht zI`&AV^_a7nNUuUp49MfhD4=mGpQ}^J8I-}^Q6{v=`S8Ti+JeV#LrY+H#P)!9EE~a0 z5c_RfTvx8oUG-U2=mOd6H=}06@_p?`m7%F>nwhii?YJ z7HTcycL>C9F%hY^?-m@J+jA(L@s@F7ep>ZWPC92G?YrOCb9w-3zmKk25=MyaE3oLs zRGYs729>mp+I$krK|4?egU0m|l5T<6m`Mdlh&5KE5yE(fl5d#a?`m7a*RC&CL?eR6 zsXFVEz+!3Z@zMG)#l?se*G`>=hX{BMI?DX(dUGcj*dOg2afizDzCIp@sS;3-T~9u| zA0rC`b!);6;v6VbDyuSuKQDx)g34%Oh9jlCc<6_Q7@;)sx%&;=<(D0<<(ic=1-p<8 z0c5f^_$n>vs9RZ+ncSP9{CKXeGP1^e|DmabY_7NR>+@93EKTIiJ?OOYyQJy9RU2GO zSSNUw@f^j3%hXKk796_7DRw2U8S6e0c)`-6*j47BY~f?kK~`0G-@Rl;rsk?D%yf^4 zImEtH#ysXi%kyUkOx%TamQ*_x`fYGUMNI;mS8kLp+lr5Rf>x19YFGdhNrjWTp07neI+8SLnpIE!bsF)jEU8fU&NWqvy2oq5 zn`ng=Bpc%_41#1HP#><7NjN{B%8=?R;cmW)%RP^M9!xGCn@EtP?9c=iBcf5V!(935 zi>FI!%sN>Iis38C#3Ai{><6wphpx=hp%DU$Rh&=j^##dO+_5x{O+<+H)u#K@wa71~ z)YIn-AZjMnOx?eT=CX`aD*_`UOX<`weCOAZv zW{;?->dGTrI1K@%c|GbvgVVmZ9F%2r7l8O(d;&&!KFN%q8pw=U+cryg9;Ki^Y_B~cN z1!E9X&f?bUF;@RzsvG~1LC544P%!tOi&syR?XZNpFeu;9Jpt@*T1{m{QLd~bc%!n= zxER@eiTMc@bCedX{;&zn12qNVCuU&*@gEFagpSA6i@GZ5EC;p}IQqs5;MKlsV{yE^ zggHRSWUb7qpO05~*hAW|zPQ-t+pOPM4Y4PR%-MvD}qV6j!7T%vCj^z0($TFJ>Nu+)r)I+L+8os0h=Xu??hAx zL)sB^g_Hz|_HoESA*)>ysrS}BM#EFxJ`+W|$h}@ki|fXvh*s&R0)rxZ>0R)yId1Zc z831d|rd9i@RBQR=PzUqT3qHh}UcSnO;!;&OggRoFkMIG+vsm6!EEu37*w}uG7as(y zNy3@H3W`{{QAFq#1vJeZllcBbQ&EO;tyQGZsOPV$9x%%{za&`13n#1JiajpRiN-E| z?`-MQL7WOR)m4JrUcI8=I@IS*8_tv@hP%Bk^jI9)nuWLqN7wW-q%dw;(AY9&c?Ey( z_OV)zt=GNuNb%GYV6UJKQ{=e#1~?^gxlPC3TwhP0KbfIM{@!@&(6#0*lx&OCC%N0v zmh0Fm9P`9gMe?3r3ID2WzsP8H6@C}3;*n)$=t0J?N%1r-6%31xyUU@>jT^&v+UUMh zbs&K7?Tp0d;LW8`l-7m4L*=|XcN|jB-UYIHjp1T>z2w9(j4g@@s~Sh9=axu8VGV#_ zi1~RDP{=7m7Mwg(uYpW32uK~?WZYl&32A@61eE5P?}9=)C0JvlAVS)~AX3JhYemzu z!|pN*l4zC4xsCUi-IwFML}fgO*Up?4M07l~BP@95IFQi|J86ZdF95&mDBSZ$Pnj+U zgg4E+#RC>~Q$MRq2XpIQG3B|#%@E8uVctA8u&xNTg)0fv)YcnLq}e$lwhb&6KiSeE zI>9Ky*+=_y^4IM94gRfs+&LWfxzHi=AFW<{zW>y4!N^Jz#Va0i47oe?DjzCdZv>o; zfp@Yl^z^Rv+VQp+FV-~6lU$YCllRN44}8;6T$ePE$v!ntrF)D@y+xSbIvmj$-@M=s zIac%K;o*_a{XnJ26g8J0^g-m(!U9XfRI9v_pv3WGaEyk0cu$2;ao*fK+zaRo8PnpI zts1ec_r4dtMg)0%NrP&5Dg}1Y#Kd9ebE<&+Bo9l2pDW%dj5ZyK4I!wvV!MkB#Fr`| zoOuNe3h5aie5*d80vA{b?0CEqHD~@}EUz+C#svVhkx4!)uLTMbOcv0Sj^%KrO*;-? z28F;@40(+7-4y}v$SBy_EDPuvHpYs~sOEt}u+?`SN7u^J-1!;>-2ud~4N*6#&YEQk z=OnhY)O%>uyQbqujwV$w-8n;KP`H7L#Y2MJU0k~OLUh~MPXlAUvT1olhyVFLgB9Gp zOmFjDCDEx8t-QgUG9NwgBLqCv@(jJR;^%RWAKz3bs^A~xzM@E@s(=0w@LDW)yz3DT z!}TLd^`FMmRk9rGiLog;BrqIRp)|lfHbiv7pGE2G>%-Z}wyn-M{&Gs|4#_<{PMg}$ zW9p;NiqQIoAFW!-bsgo*Q&W$F-UmEqf3wgvhc(=$7S7;~(0-(XRabMP*5=yN(^y3o zSA#r*`xPYqHA&A+^wGNBL*O|N7>Xg|*QH8RxPyNn`l4MS!_fubiY{DK@pG4JweM@@ zv{7w})DeA29|1k{wvm=tzy>%Vl^g(w4rV#&jR&9~a>Otwy~L3-o?C#(R`~I*Z~ulc z_9j9SPK?N_aSfLF6oJLwHF~Ywow4OY3u-w~$T%pI^BdL+4TypZqOB7HhV#Mc@jEMv z;A(yrbm|!1u7Cn_bu{xRLS8Rk5;~4cm3J)p+WXjnG+`9d>XrrFdA>1nG!dLuaPt}7 z!&4HYRo%m5H?d3DXPbe(Cm$i^tKNcY+hchPLXLfP-^te^3lV8*YiU{4mX?V#w#_v- zOZU~$)r~#RM5dd@wugr47WA7^a~Dxh#d!-UEosYC=By=B=YCq;&t59sb^)bZkYcdv*7MD?E!Lx*Q=t{3 z!C`uSV)spTmP#$*lw?(Gl6|N(1&M6Mr!3?!K_STsn^`ho&>bLl+(7|0dYsUtNC zm&7vd=z&ebC*22w#>bhyfORyiORiF}i@D2xDWWjuhYKu1Bnaq%Lc;4Gj((jFqX>RE z-k819-e%^V(9T4ok|TuImW%{YSC3G;a5^c#*6zDNiR<&nMd(PWxakGPWB^A%vylFV3pd$$W9y@3w=Ly~p&3z4>$d{fUcC~TSAGkgj;mY=WmRR5JwRbeO*l>v zmN$8lXb^ASh}I*-|1mN8RUiv5V9>qlT@T+>ZK>strX6d>jPysSnbV0?SXvY@k-De^ zmAv;^&6=>$>ejgv=LVe%q5%rfFw_IjV4-?R6F;a|$K9<+{}8LqRI(Wh0|zStCW&^@ z0eMIW`UotF#sC7++vgM|vgNeQrtf$_(+Cb}52OqhSZww?S_aSpzKD1y7zC7_4SfHp zA-^~=-x#1k;oLn%-emdZh>S5>m5#&2D6e$iOphP0N>6oeT{wG{_Xi+{or+q?LZ^RL zT@ecnx$R9YDm~Pt$ZN^brbGfd^f~vANt$fq?`MxwG^tSNZs5(VF9D-87qA!M=D!tV zW^V%vW2564FR~lAg2$v|24Pf0TVu7WJmucSN@Q3*B2I~Ouf$KJ;L9N_)7tbUW_(l0 z48xjQQ+wOW$u^NBxjGQkRrqW#}#w~$zf3nc54wtKF&F~VB4F+a6qlCB+7a=kwq9Q6XuK9yksDQjU; zX~(DvK29hb_fw+%7YN^?b%GbL7#7~87;m&%_tPD~`*YdiI57>N(VvP*5lm3OT`(-| z>=t0_I(bBYR%f$RPR#02P>pLGg!TTiO1cbs)XwVl$0cYXWaa%tMI2ps)t7wwyLWMV zozJr1gvM3iD*!o6dS%iK9+<3Ev$QkD<(=j=T` zPTp5ZJ%P8z*dZ)Da_fJq9NG$mm5Jen7v?MC$LRNQpbDB-HO*Jv# zkuzU4(qw!B64-fDLIm2xY^G|cD8vYK=YSOYi(2pjUl?DD-vYzK<9)lk-wXVed<$D! zANisv%3C}z)v74#53yOCF1Vzg2NR*BesbZ67aS{8un)meeWCXV0f&NT+lQ9VfU!uS zhCA*M91UQn?g)D_;aO6fSE@vSIGQ+c%nb;mU0wne{vHpBmSa^p6M|?JSF1PA_ewO1 zLj?4^w@7z^ya$I!l`5p&-o}han#gt=E9?4?kza-zI8{lbExb}KbM7~lGhOGY%OrEl zKA$+vr^Dd8WO1kQk!-nDAhOH9wJ1y-cpw6{k|^@D z1(4YA6E2cyRuFASdvy8`Ca_-@V_eVz&!mM1RAPs~Vb6hcUUz}*Uqfz1Z}(TDJ^7;o zOJ5;ntuIKqWaqg_UH@k)XxS<(7hubDUs@97`Tl(w#Ts?q-|>|^phIQlS2EEzirmdO#FSd_VbCHTJ#WlWj@w|;LgmSggHR6zZ@h`pEa=0i1*PX%$Nb%=g@t1NG+zT}a0bjjCETAr^D_dUp_5j$V?X ztmuHRIgHr2)dGl-mccBFI=Q1{S2*}=8y)3uSHYG`oFVO&Yx`N~h#z7_?q+lQ>R*_F zLN1wg9H@rf&h|dN2Kk;u0dDMGljZnDp-YrVqa+84a<_1_$W|-c0VWvK=`#d1&Zl$| zQT>s)H%0_!7?$koGVp{D;NWU(m-4;QSUEU)+W5sM#O^6-q=~3w-pnw@_+Z*&EQL`? z(A`HZ1`Y?;JYO^ky&THaaFE@+H0!5h*^%uxbNG(8ghV4sJc5BMmuubmL-|fwaiy173Rze0PiWdX6gkLoJs-KRY&V2~rCk zxR8^r;5$cTYwdO890?rKJm>K()qWhb5eYdyh$nuEjllFBTmS*(+j@O@iYtTO%;@Rq zxef99Bv(5~1hTvhV~UAs+gDEDIbu|HJk7|Mlf}57N&`T8 z2nC<@4g|pDhlvIg2U|no3ZBlcV3*xD2!}(A?WrdUH9p0QX`}0owP*0crB-bPURq z4cH1hA>Ox%o7k06kw`E}+U)Yiaf(!^CIZicY|_YuG2$BIBQjAGtnL*%J)2Rj|y z>QX#jR})DU($~GDg#Ee-N3Sg>iFM#T`|dAz5Wc{XN7Td{f*vOS9#tN4W z^eJcq4(WN;5J57&!6<;$v4{uX zA~^c@Fb!eS1Qr!_M8R5t1qp3{d@{(V5dIhxnLt3L2CGU*DY?JDpMhC~Quz7jxjOrt zve}0WL@Q%?@MO=jh;J0Ci1UOmFV}XBjgGxKz{B zz9#ZzG1oKMzN$McvMbV|K2QBLox;T>(v;CE#FI)PKWGifB$u|?XzuRArR}&a|*Vi|e+SpjR+Ti8yIi+;jaFH?67<(6q3Jdk} zvu(hVotLmL3(xpe^0Y`g~twm1fqRUfeCrLn0qS62g3`#6a z*|HoijxK^bT6rZ3Anl3VP}3;@pfOk5u>za^Pisynf{tOBLV!Z4+di%ImzvF6Tp%lg zU^^eQ%wnAcWdfsM`(~?sMPjQi!qs7E%wrb{D78)H8A|JqDrGgK(y6(RE*iXVy<|Cb z-YiORIo_91vOP)T87&r_dU{(k?HsU+Oo+^qCsfK-k(N1oV>73|$V%XRL` zMs^*=t3~=!BUcfFVtWfHHtJ%R1S&;m5VFd(LuI(N#Mo8rJ_oQ_@@N_f;e|5(agC1x z3LlNK_uCbtqkF^-{jf6F%^$b}EKLsEh|g%=nvObCuQg?qFwXXJFZG6sqi9H{yG}3I zoZ5LrKO5f*eNbaHJ7nv!H{K!_a1+3Pn_uR9x>0@^LLS0l>PGYXT3VE>G%0r<2exhye3XK&cNxJX_7p-4O`sijb#={KEsr?2 z`*hMAZ)ys)cXfVvBJh;xD`EYbDz(P>K!u`+(oPkVxvS`cGRHpRVkT^cUmU;2<>aDD zmq8rJkxt_W=ibXX(T7zvd+-T+flJ^wgop!UwV2jw^aLXIGQjNMVdPbj---3KW9L-t;5(G*kTq2%B@0Op7 zg6A^dbeV5z$_*@t1BXx&D5*)bC5ddcoq>aGrpxRkUx2Pgb*JaQT=_5VJKYk1KC{(c zTv_rG#8jHDGZy=JY7NQ`!rQWEd@nPni0#W*wNVh@Q^x#)jF-606*awW4OtT84v%W~ zfemVABo4*Ny;ok_9xfj?D~2R1#S1Um)F7WG4z5fM@J)>fkxsdYyakm!yFKNu&$}6k zxlPT#BC}SLc5q@D-e&3L(ijiz%I6Gl zeD`E(N=nhOa=lFA8ZEfCXDKhzz%`byTIj6ES}GfNsX=TQej9j+0GfPQ;GGMW7d;>H z7;Ay|EkotawYRBz{z)FD3zeR$yk@qHseEP;-l0G961nI&mcemb&P{j=g{~-<+`2Ti zw2E$ z?#xM?-DBQ(7Y z2FqwR`a$ugnUS_k=kYO)>$KJ+@oAMb&cOBq53sL(%Aiq97)I4&c;YQiwD0wdn+d(v z@6%)87>3)T3?f}fJNF5uec}`AF!D*WM?iCP0K#A2&4c{+$js{w<8?mXaxGi=By`)rE3F)5XN-$!ePS1}iuP{e|0N z;Q)#_tBja@hv(s)@bxz9q0Ae&`3N4ffO!7nL|gA}+XBo!GK2+SI|2!I$cq?1dwxtx z-B`j>=tik;j7L_H`5bsBshKB)u(VF1n9$DN-&rfS%;K;%YD_an*h3+()pmLyJ;cjgb(nYyGB$TzZH85aw`Htzp_bbV!9RBPM4AdMi6bayCS z(nxoAh~!9@NQ;0VQbTul!+?aOw1mVUB_Jg&4I|+H_Ib~FfA9YspKtSFKJ2~ky{}l; zTIE(76w#c_et2E%kkjq;QsUMb|r45yhI(5eI>F0^&K7^=K+MdF*@AR@g*r+kB1qn zHc8Gvn&j=JEMdkVvlFZSC4K63Y-3H3%TjAmw@i?;=dixI*JPJM%PDFr2u z^G6HY-*xC;{WlG=gw`FnUHWRJTEtm-7|e|X7CeW!Tbr;=*m_piAG6SOm#h7)JjMWo zP2zQLa)tK2Cy?W?Yh!Qw^7Jah_IUtyG%kxiaGXawEOQFFH$s{rzp zINMa)b<9ZM#~vp4Evu1uH$`U##n2-0PhMn(X+W`<}wysZ6%MKW23R#;vrcKouqTjggUs!|lbvM1T?_oG_pAxn#UyLGZ02 zV>Z9aFDUs(0qu{KKOVqFtynW5EzwZ9Zz96jTf54Pa7A`fM(h;Qo!1H2zX{21$6a|) z+q9Z&qRjvQ8v&VWf*k2X=l~7MIic&|=f%Z~#&2a#eT~!g7OIY^47slhN3o*XHCHQI z+ETpDAvN(bMeHQ?1pC65EJ;z(&W)ouOM{&!!mH zC_5PmD3C)GQbq)lCe}zW)?X407ukz^5x@1Q+&EArF@uQxtVWVRXIf}zts#EX920v_6+yET*QQvSSeM?E z6_4Hk&%Nhvvhb~D7?R_0M*vZ?6G^Vjs3 zUB)}Pky&G{Sc{MxMm+esP&3_XSibn2OdAwe=>= z2#v@a8_L)W54&k60r4+o7Nd^@PDj)?55D;VLY1yLYr)bEyke46BZ_mYtgN7f3in`R z#ha4gYMt9#LOpwWIvn}f^6vbQhF?!ls)>!TH%_l=b`iZf$|_4;WxP~4F@7SzNW&o1%%nQiN-p`v9yd}3$9 zw9K$%heiSm9EwKxL4j~|NJe(c`}c|!!y>} z%bVTk1-K!J18^qt_C(hL5xY`fj~;1n&CBZb{$<0k9R}#qzK-pv)ot{f6oW_q5)5+-&BPa-iX==#=o00J9`tyvWLu%jz*s^cMZAbz`d5)jJM`c}t6HPq|NNc=@y5I2e}m61YmiUdJjS z3h*2n*+15ZYABO_Dj2%jdDZTZ8q$h^a^KV-!-4A`F&U$hex<><=t-wum1B*oN+#7S z-0Zo?`2J$GQ%e2vfR|CR0?u7U;cPd&+6+8>*K2H0Z83#9s^mqMzG%s^;J1kP?sbi; z4uzu}`E(1Kz8{baY74mU<9FS3$b?K^CcAL3^~Uhb>e^5;8gIt2=fcso6)m!f7L7Us zIFqRDXhPQ+XVfMqb;9}(V5U2@15`sw8ukJId3qvS&xHY1w9Kq02Fyt2aakK0_4BD? zCYyZg1-BGxA3#YKhKiXVZ$ElCDiOcCHZLif_&97=g*@hBghMuI@Z}q7g9G1iAVZ49 zx9I<6Wi%Cexw-$44M}{T@7O#~@_>d1G=Cx+p$@h*6>ke;+k znY9?z-o}vRL804}!92z9SC%QU5_O~2`O6vYK2mGYkxx=^_Rs#8+(%*$TuWf7cVU2X zTi=)|4li}>W4Bh(`_iBilLTokX=n{H-|j!&iLMk%I#7qN&3V{#jd`G*%_;TBvnUKZDsI{ag;`{QA1U$~<1+cSPQKx%tjxS#)qHQgr11aZmt$@ze5`VdhLI+O!}?xM zZepCB5Ge7#>hk=c+rLOSU2`y`aA(PC;XwWK0xr=VV|$N;n(4JBnA3uu zFHRSKibjA)jVJVcrE=LxUu#TH`x~Qt4ix_Rx}A;8!M0gD8w+HaH7gUz)N-&Jh$H7x1pp=@h?J$ z`b@RQp1v|paP%CUnf=CcAAqM1wuX5B=PAXd$bGxSg*6n~j<}PTU29AfFcXiRi-d!L zY_0}?um3rgsD*Y(dYg}L;C&~YNbX)k%{m@uB`?%0g>(O`cWsXf?rpV7ex0C)r)#Lc zZuknCuQ_z>z%|RCnY$;^3v4BY6ANJAc%uCP>|IKuwyQ##g-U z8Ou^n$jhlr(aG!!)$Vvfs!8X5|Hn9AxUM(ZreEdZcJQ8kupZ5R42>t~*e&#Cr9~!V zQQxV;#WiXkWL@i$4vW2wm3c)Q@o4VBTd4Dx-9(u9mX-uxFKuqgf%<%ChPyaSB+fX} zCwqJo7vc4z@n$TwZ(|q`XCH}^jC1Z5!JQRU%0fHht_ws{${gYP2bL8(D-aV{UVA7l z%e(HUo~*7|vu?FNv-+ZYbGE*a?wPu5`Y2N2?lm?|IEjxME={OKCX7hi@H8vlCXYyE zAYg(K07xu+X-?h9GOK`&!CP)R>1Kg3txkIxH(!+dudz?9R2K_Y7eZAf6Q*AYn`#Lg z(A`?9CnSBTaFH}#4&%MkCYDZpX=q)@G?)wuDI0c|Qqvby@cga9rkK79BwHd6b^l#^ z6^m=w0B&0IjvKi4miAC#598$3t1rI&FD?rAvJp$qA8eSWy4|6V|1x@yA973?xtKYA zC%5Eb{ZBe*h$iBDS({SHyySMVsfN=z>U7JS7LS42^P% zRyM6A4-c^^bNdgCfEMt*s=m50ieh&2v4xqynD)Pffa!EUg4S5H$D}OzkZh10+#F9f zTDXA&P=R-{FPIcO?7Yqo0zgL@UkiC}Kn3%UgC((?b4Y}<(@aWHTUi5Z#gM|M-Caly zMUpG2gHGAduzh21#S^C``Za>iPMMAI2G$GUfrKO}rgt8uoihD_lAh!yBiVINQ2T9_ zs4Yp4GPWx=6zn+YK!G8erEhc*GdUU&?}+f0i7}&|8e#bVJXdR&;A~D-(RJ~19c&Gw ze!leBl_ZtLrF@Hg8}Y1;5jD{%dE`EXx~QdYTt|<4Ia^L4gX8m$9T5)w@OPnaA52}| z-uwh(@cu0kLfNh$L#>n)sKG_7gLwikuZkDSFN;t0-eQ1`q;$!tn?JU3zSukweZUdX zZ_kt0X4Z=?4`J?8U!6AEW3D^4^-g+7vHG83-1@S_%Tre0EDvI3h>`xn<55G|S`$v| zn1++9M*?~^bs4|Vew}p=m3#>`(TT|pS!fa9vFqPn+|a9Nmj-wW)4p(GO9nRw-}d(6Gmr?J?4A#(^$+G zOdFKs;sMwlgFuY$vQS$J>^1g@gcC%+oGWS8Q&n#bdx~0NtxU^m%OQWlkyL znzjyn9-fcuo&mGIlWxhQL@p$ZSz)q|ai;qNeA?DmJsOzP!8Hb+yRue6@ zx6p^vzINW1o@TPP6gHt3ar+VZIV*iiI>SOzUzhYSaLn)_r27EACXTAjW9$y@)*p%|F4x|52=vGvs2# z&|AF2geg--)P62M-{bW6_O_*3G|<(8q#8>v%}q0=h+X5Axo2YG3u4x#D$9^JluRq* ze|G*Xv)^gc^i5y+&JQ9%c~rO!kt1fF;Vy+%5bX*X^*-Ol{UquWrZ*BN%CR@0c}Ti| z98{Dlx}HK}kC}cHzK0soM8y(sC}}23Uk7^cp=2oNLzfX3`;fbQ6t#I%Cr~^~W^; zg3&Humk$yy2?hO3^d>9AJl8}6m_;Od2Mk-g$v!+<*^bdKqh1!!OdeODS%wW040G;n z#v@+NzMdG6R^uHPu-PG;s5>c$TUZoc`{0@;$4H3Xd=;4xOAs`KxhVeK%yUwVa%C=mp6kEeTCj^ZE~GOCv$Q<&Vv3Ygjv84 zedsd0Y26@9cUI!-`oOW-P=>fs-hz~*{JNkh(ecVQa}CkHB*e?S!27}NtG?kE%wjRW zl$;J89moxhU#eO@%Na81D3|Fzoy|STr3rp7?#6lI`MuWpWX4A6iUe7J0OKIPKY7ab zpD_9x#yB3VTF03fz_oom+fAv>oT+0$(yDszI?KjCc{wzi>Gb6X(L^2!9mty48En-( zESB^+=c)l{aasBdD0rCg%oL7>DKSboq>-R5p0>?B!fJV>a-Aagbgup6 z`LI*WYMNQYVkC%72gZEtpkTV0_w=I$wN+<*GK4n%#oSd5;x|XY`t3E`$@9lh*JCzV zMmw)vqn-9Ed zGXEJ}HyzWWFypU)U$`0RbnS9uB;{x+<8eW}E>tf<=&j&H?_Wgg>j8bGk`z12 z;w+B`8?Td`wTlN*Atz%e?sCc-%SK9{HhfvsLrKC#u2!BM$0AptXRgnX7+RTcgp;~o z8x#SJuTCyEmP-O0{@A1xsc-oa8uCua=aUi~0oRO}CER*@-O1%U*2%Fz#@^gf6R0$Z zAo1P3prks*%L+i%!#Xo%zR2;v_WyPQST5xRmX#G4P)uPRs{`MxD`tw-=4Dp znV*=+E6VtOpNNc2FNwQIzsGlTS3EWDV9Zh`F+Ku9NpV%kg6E$13(Bd|LP>}w&lwn{ z%R00~Z+%~ZhO}_j1ku+8dVW0V9_F(+N{#%eEN4(-B-n7X49qWbR!wmv$SD_>8mw`h zM-~40O#}l_8%QO@vo=!{LsXrfZMG!rayER^HYVK_+=C++IP}NZS$}wAou#31h7N}9 zJ)j_8gUT+R7uV6{Ju+~tKRD>zUOzj7Y5ejUn~c*Olb%wFri(9KJ!*!|(^W}F{@jT3 z#3+v1$7kl-VHg%1tQXR;`!IrjSq)5DGO~g@BZnz|ei9-5!oF*)Qyhl;E?@&{&H&p< zYi0G+Zaj0&ekneV?qXgW_9^Pdps0~{rItC)pvB?vKvk8`F@ki&8c*a`W8U_csySG1 z+k4!@p&Z>zk{e3u6iUnN#OdnbUlTQ39nn*YRV6Gmv-x6}-Zn%?VDvOKu{qAOCf__M z9i?wbs%u$JiSfKyJV({w*YkWRBZw)Hto2fKzk9TJ#4uR-=C!BZ1%7Vb1-%f)?Z*@?3O|<{K>V56I&8FLXFba!(nS!W^mih3zFHKi;MNgX(*a z4rrSEzPs&nB5~2sFg}rg8#{lz2Fx3L4iTXj<1U*n_V4#IN+?TRKz!(%23XG zvcJX&6J1An@1>!$#HGs}{nAQm*o+l=!{Kk>O*|2did?S&xe$BbX3G}#!dk>?cRvHY z-tPMQ57B(e`YqgqqenI_aOSOGA#?M$3EIvg4Nu3fd+s#6zGsOIBcItmVb-v3_ukTV z&xogmRLsx^Z!u(qyWCVcJ@fb7U9z4@Ab4ED)+=QK#c7#%aqN|pXm3=ko$j)3?0rHL z`bz0L71~WpTEw_{m*mnH1xTLq3lUds`O(VCEZJVdRDx_)D*i9clxNCY1LwC(BH{l4 z0RQ*J*kM}1tC&PCmYvuy#G;&E!IKeZE7m{?d*qJV*N==i>@t+0_4`D(O4eu8_|vxs ztBgH6IJ9I(C_IH0$QvY-q|Ub~<2mW}C~D&r{U;ym6Hok>1n7NaXXo4iBB}U}W5Jn| z2pUDkrYTsjjw|N41#W|9L?4+LpRiGbJQxCKXe(=X`6h$jh@gtg;9Fccm><+*c?a(1 zb_ZfB0nQr83V*-0f3tsTp+E|Lh%&g*fYO-Y7Z=ca7nv!Y6@P!VH)0Pi4k1(v%EQro z_G1|TbH9eDe!cO$ndNle@SHZgy*9Gw^Pk@`do~;j>q*LHPGX=Jlk%^lR@4J++$QZv zEq$#uM&hZHzB+|KOtOw8mE{+`orn#>FvC;PgE;Q0@4BGov4)`)t+@D*mqZs{`?7UZ zI4#4b!d2d1gPfE-8t3=W@w|c!DvQWvSsn5d5zc=U!$Q->(l_U-9W&?|RXtQI`)U)d z8){0Bm{C~Hm{cl0R+hOVtz;bdTKTkn6HfXJvuKoOnsMA9S{2HCmicsl(uQ5)stwz0 z+=^uSoS$J9D>a%=<)S%To%2h1$1&wkfBh>;(cDFMx`*g7NgRAl=7p0e%xxJ|*d4!A z=`<22bGLFII)k2ZP9v85zqUPh2`0jA;My5z+}7kZg9ToWb+2%Zb7LG-@7lPdBC&i9 zi;KF=x`ux%3>7ir7pot88nhxY5G2Bba{@iQQlKAHFJ!t>c8JNdgg7+Co-*oiDSF5*|V18mz2@Psp_XHD}suY<_Ux zne^kIkNx9L_CXJtn*o9VOF*L{^Ggd=KtHHj-DHrfGTbkClEeFkcaJ9bwU zGTE$v?W0e){Lcx&9Ta6|5HFY@C5R>mvgYd>Jg5#XQctbPMT6aVWrGN`IR~p5e}cX& z8In{``6H5dMj`-lIy>H}E9R3zf{B}Wu-w^+V{ok}A)S7CUdYZCmJE%k%Iz>QuQ$!_ zZF*Q-cGsF>kb9Nwa(BPoINi7N57W13IE&Vs9U| zS3O(kj5nFart`q+@qz8PZJwHc_HAC$?UM`q!ngfM8+%OBjC!uTu@4jv(54Qy0Jrn=fbGgyY~AWjuXKK6?5{I5bBQGtdj}?KA;}|@)c5YDA#>^AKy6ydO3bZm z3%HuMX%)@Kx2IrKk2a5q-TTN4x~^${2*i#iOe}93;KWTYDNKa|L!DH3&9H1~-zoR8 zwXqTJhrY4lGy2x;anhlY4X*~VD^+vzX~H7^+!7Oefo$F~`#-4j|9Y)3F@>L_o`L@U zHO^Pfq~4b=dbK){Tv5OAp&_Ba!=$;nIQRNm=B@4c7D{ilcYo1%(8M?kH@p0ydx(+% z;9d~xuGHCin`&7ts{_|GAJ%h9qVN8QZ*hu?C3WZTr8d~1!yd^8P^b6$4{ee!D0Q$J zRoR{-Z`)=v5*+TKN|-H-QhQH+acwbm>lZ+|xtD}?fiwEmU+EmUh%9p1!~@-ncl{2q zAuw3Q3Im=hfN&X#*79$ay5?_(S8i!+u4ZY7;e6Hm)gN82jvK>XPd+?YY$?ELGg0d? z!ON+Ulj^3Z%482O)I3-mTVi+R?Jo!{RK}9q!K2W@Xprjm5mdqwSTeS8I+>p4p!iIA&W?)) z4$rLMwmZ{%;Hqv)v`nmeYTv<8Y)zsquM`JocwwRKWrAKZ+iCr&u0I{Q88_C!_Ziv# zrtOEkg>m00=-~M}dogC{?cRZN`nu!n9#nv|!gMRX?@Fx&`jV|77>%e|q3J1?PPy;d z&dkt8E+hH$<0((0`m5QqnwlCY>(e{}vhr9S&c^QV979PL_$D}b<_z1+6-n)LuOlZ8^{T>%wezzUb#g+(Oe2C*QoqocU=%332CJ=7GQj zG7Tw4oFb?2Fead%cPl>aU>7N%g2B;%glB3!KTUs%%S=qx^4T@^J(Pbt9w<`-$R z`pm)c?DQTkl}E^9i+{Uq7}JB`!sXDQY1Eidq07ZZO*}90)7Amgu>0{o{02Buku>f3 zrUas&VQe@KGETEi3-pT(IWlMwe~*yL43#=UNqRHqVs{;O#FPXn?YunJOXQrOF;=ig zyu3QzQ2AwjY#mYSGi&P=f(#=|FKHs)6%`GJqjxWdXSF5i&s>?@_`g}KF(-)QpuRK- zAQIgTcdD@He9^w&b6Js|S)^EE(}Bl3f!)Dzr`reTUO{GTafTo6$3(J0$@}Ug$6DM^ zg2MQjeffTen)b24F!GeQS;A)vW!6(Etf!vPi=wHJ+A;}}3RAmYh)e8R#kW z>>p5nCLEx&d(HR<3DaLDCbG*a3k=kc?NmZ!QH|!xe@-k}nqT6*#1tEf@j2iY>EDcd zvyE4hHmizok^98b0W05j))ImZ8|M`(-Mw3mH6iL5(yg{8nOsYz?+;m09-cqrmiy7X zmeq!>(NR}?x;Ais*93UYcx5;@rfX9A8=w;|&*ZW0NDYxV?|**wW#62_S@*5M9Bn!* znPX=O&gVUzq9RQsmk1}svy2+=u?D)#lBC_EQE*Eth)UqM(acm9d(FP zZP~8Yd_!}*KF}&q8XHdNb)^w;gG++Ke=|HU_>{cHd{f>~=y-dY1@(gLvNGk8@@rqY zy>rb~^tLC*S~V-nzOPdVFX;vU_ZqN+)ps|h1=!x8{%<+@j{-$ky31-HKl;csBFVM6 zXr+C#5W^Bvx=RED3+gL4-d(-P2orutaURKM%-``&wCaQqk@x~LUn6u)5jlGe5TxRj zmCQdGkB~!Dp)>@@pT!i4p;!cwgzOx}08F)J^-Zq3RSH%IIz2I>-x)`c(oka>G_JXc zdMR&e7pAHSBC*rndY~xe^sV`t? zw~4fU8*WMPaTGRudG?dgySTp7_A2h_zEh{C0|c+4)}z_vV}R_>qh^X+y~KgY!=2N& zhdY{y9xHLvlb98D@*?X8)*?3k3e zeIVi6*H*+)U=ctKduLUx*Wn{CQvxGqV*_;lfO`(MuZ;Ilq(VDlKa^P;UyM3YhgSUK zH2*H&?}Qv<3RgZ8m0R8v7OQ;yv-0jZ=6CO}d`Q+AnvQwVz6&r&jeBhcOl{uOT$-dA zC4BVV^IMsmXmYE>R$t6~Uk_z&S|C3v>_aR5`EJpb^C3{W=45PZ)Hd?J#cO024cmGQ z5UN0f^vCD_xiui`Gag(AR6LeCqQH9KD%SiY!<`^>;JTU!)#Ms!IV z&hH|IdU4aYLe4H(jfRK%)p46BKU1E^lZu}qw+;0g@0T*+>O6!5Ip2sr6=5bXvzVgm z^-O(3^hl{Lf%N&3>R77_^Jk(zgtxy=+lIi^aL5y*>)qUr4bENFc)RHMqZTg)LLBVy3&|IW8}5^fm4PhubjyhCvEnSY=nbSMinE68H6V=FGbu%Fi{xKNzP8;%sL&q{DI04aC|L;+;nC!f?tp`|ADHHy z>8!i~f&Xrp{?TWvJhp^hpbueCIbbU`UlD|7J})5$1YsrRjnwZ$#o@@se2!CA^x9pF zeTs}E{kHm?&{UK^+@aua=k(F}=FYco8GKG4)u-XX_kt5~Z0;$YTcI-`Lo2N9qUzCLhW%CAg_;ZD>-b_{##e z2}eMcku<&}(8QiMw`J~kkS`DC#d$+IC7RaHts!pS4`WhWxCTHaY@e;EeO&fq;L+08 zC@mNI90zF&=R-(~gTM5b)S6TKZNB|@n{%r$nyD;N?z^t7FPfxrL~XtjVSR3q#^p$ZOHPVlE{pDzWau}Sj5C%(Ss?gRTvk%}B+Z;}p!0J&QmbZd!qeH|R6D>b zYGCJlsFZ9ug%1ieAJo7A?8P0%P3s|~o0jtob#_4H>vi_|s+)IX3#gWGRdX;n z+y^g?`EW-Lf9rEtW~qq(*%)^50LAVF&snyGBUcS1=b+hTd51O#;@&JJ`K`RF6*SCg zZFGw?$V>zi!-0{b^V7vW4bXrwB7i~rWvugaV4#`n3$PC0;B_muC>jqNyl=1E&hxIZD|&t-Yu#&=GzBakLj=rs_j>B6x8ezDR+Lp z*YY#<94M?CA6q+4(b|_xNZ(yt++69<^FIyINm=-iF=>$Vh-O*=t`Eb1TUsTzl zQ3OWH;0>ANZG#evzw~s1BY~h#zGe;X=JJ4Le310YQ}q||CA41WUv_b{nhc4x5tK5W!$c2Mt)fxEGx)Z+sicZDWDmJfBwV^s9Me(9C0xw5U1 z+uu6qm;Jy3dJ`@AEYI>T?RNXb$Gk{W2iy0W8XLoHj*iT%C4f&H)YNX#{M{6PSw6aK zjuA9tvZ{fYL(PnLG{EiT8m_;dOqeMb2b|m)&oV1NwrXTOm#RZ;T;JeZtsL1OT)6yX zrnbEES=gB?Z2T?R&B5A4%I-s#j69ET^bxo}dDm9QUFDG9P9CP!cbN!&J_s^%R`}X)r|z6iCr^LWggu>9W5I%!y$Rg9I(glr6f;Z~ZOnfVfUyx~jkyiy@p z*{JEg=<}>ycWpzX=Zwl?`<$;4YaIkcQ{y}rL#1hLVq0R!&K=d&b@IR=>>&EtRc?QD zhdRAj17rOA&^=!O;l1}&*LbOPa0c=U;~A%c_TTrT{>kG-7fmY3Zw$UdV*XuTMfzuA z?-$2hT=DOrANqkM3~8h7?>mS@&C?FBL1)p!1~aUja`v4+#Fnmf0M`Pmt6NrN<0adQ zgP7_6`3}eR8pBY^fb zV0z{-hLm{1#zi7IJKZ#Ho$rAVP~*vJE9>*!ZRc&%MaL!&6)mz6Q;?%S3U2BJ?JO1T z!{wfrBWtb~Un$&XZtjyza)JWwQQerAms9u@-uE+3pMhsq=f1H zjL2mDI?sbR8K>rjw`aH#S=#%r441ePXfD^&SzH8(y-}v!7CGD796q3Xd1(uc!o!$S zX*w!iJAb#%PY==M(b<8>1q@q#o9+Y}cUTnF`EvpK0!KAM&&r_%t8K?>trm#_tNk*E ztf(n!43&ATEys$_z$bIpIvth9ASws0dQ?{NMCf&^>J~QC@0k*2N}Ykj=_6wrLLfLR zs0Ll_nq0n4ghS$Tx<5+xijljuL0d0>M5#>Lx@ea!QZ_6?9)A#V#!TR}a zRjt^jNrLgd#gicO0NG26lQsVomb$EEa@SOehF#~{+h0t%wNHt0tD@v@AI|>@bbsT& zYa2B+tvJLrd)qc{azjzQTn^ra1dz_~I1zOSOkGp%+b4ThYCUZ}tdl z%aB(TLn79!&{~op6QE+kWyZV|ZY3WeU#(Y9NuO3chQd7>jFAx@miy6DB_5+guwU)r zN#sepf+5WZMn46^GY$;5((Fj}Bs>1okWF$W| z?5TNYV8G#5@!eC+x5+woCn)9}{*gBQkQNe2TcC3~df$|EidkonUP^~uDeC%ulrgCVTEYSMAVpwJu%kbn;$s3MC!wW!(~g$9Y7V zvRz*~;26kE( z%iP@lJS;x*NivBbh1pv;Amk!gH>KV#c&|pLqCMwcE66Hm;7oG#Zcrlf11^2ofH|Gi*v;k{cDO$d+| z`y;MO?&e^W<~(=v(f)V?#*MInrL|{8x>P7cBfs|&fV{S2?@U(3SZQB{o>t_rQUwVr zGV-i%*gp97w--RyymdqtR*d(L%~1-qkQnAObzhEpZ~xi)>Laref69)eNYTY7&84La z=OLwYIG>JGh3K4C@V}~~oYj=moPaF5e27a-^|PILWm6JD_Yg9Be_Xh+X>v;-{wVES z5x=;fjjggRb-Q*Z?fco8m(O$0l@q}*tu+mOh|tUIcCTt=oKKh@c@)bAyMD#l<;nN< zc+w7{%}7s-O!rZg{G^0bN1*fn6}Sl{ti^3-cP*1dAB3;=H5CMAy{ateT6zZpeq;L% zhDwox-;f%UOBhxx!Z04@BGmI8s%1TWDOfJ>Zsh`BZzmqqAslYUU$}>?j=f1(_6Z24 zJN<3@NDOs>c4fqJ>3YkZFA8xE+_Z7`%RI*zctQARq0(7PB!b_63fDk%D+73L&V(ZM zP9>A2kd@&=0^0B{+1#`aa0;+wl)xs!#`$tWyySXcygKaPBX3Jp0NnuZKplD{8?N?% zpXXo&4vmDr@QU*iRsfja*6bvW5>!PARZ(ob8?&&>*6WdHZ*8f?92u zXYz;Lf-0Iftc`Hp<&78+NiiVMh7M*Ze`-~wTK3XD^kO?L6rLgeF%+sxU|!^a=jgr9 zT)zF`c8{~cTY}yLczTLS=t-`bl#I_?UHEczvE0nYXdkDa6>gu@8o?Hhw)NrtzHV;u zi-8DYun(={*XQqF5ZuO#)t<-JDD5GRTJ|zbw_tG}CWOLZgOB>%VaAW>EDU&!CDZFi z)#b?|!C8oYRpEadg#2ZA`kPwJ)oL8^u%^0NQ0U>YOqHJri>+aV0s|9_dlPDJMHe=H z?ysj{Nd6^dxW_$nml4=K|1g#bVL>fx1c=H=6vJO19ya)FN^Eu&!dreGNFMTa!p3G6 z$GbbxXqffdsB)ZbsxWq0Y<7v3Cj=@FW$5cVvFkekx`T-cRrDt)He+_288L-@LfV`5 znRI9UgIj$UJJ4Hyz#faCV1oSSt)3zJ*Pc=0!ZL#-iAeId#@|L*k&m0lv&DacH6Q{9CM+Ft0;Nqd zpd|61Rr=ZElO^}exNGYy*4UkvS<7JQh0?Q|;2}R#sO?l1SaGM|V>%lmzwol*mA^bm zb~-xqKD2^mq;j6?=Yx{7YaQ)W^}$}OnXaycpvFMF6JO%TA|&jugYP5lVmi)%jea4p zj596I`Lfg*dbs~*;KKyi%(I%iNXz$XQ`0S(XmwoNc9 zX(sH$)3drs*ck!Q0HOm%LP;RNC^KU{3Xr%enAFVz#^B4d9;M~wj=&r8pf;T1YF@Qh zi;Lwg*hgj_&st9|t8t$s-`#Cbrq!J>t`_&X247@x@k|%x7f-{Z?WZK{)uL=_qF%MC zqZ|J6zSuX$(`DP$z1K~SBShXgJbm3KV5Q;5o~Tw)@S?X`zrFjR!eu^aBdpSgzqU&z z3(u9oa=)0e7ztjf`lNQ#1!W*HQY)j3cyzu!!$RTOHll5Jk*$wp=9ATYGyBVGj_ z5skMp?6dUk4#&~3REbEGBzZ%WPR9*nidd7IVm7QSqIQ(ec`KG&NHnAhz)0o;BPX!H zl1Zf~Ki+;yLGFD_kYfZNvVG2x%N8eFjtRsq`@#?WXOh{T{x+MnJq9x+sG%@sh%JU; zEGh2P9f(cHBH)iJM<%P!r#1@~Ab8ywo z4Xu%$*ukvp&zV?gs^!m;=4@@w!X$6dj)QQ2-FAM0PqJft!vDcDUmj5NO4YIReyNB6 z)ph#{gmRX11DGy>rIWiBH&mIQ;`=VP6E3S67QU#@1yJhFFdLt>aq!-UJk*7``*`Q1 zjs&zCs;a<~KhuAjSBYY;ITx!WpQ*6GwlV4)BAhpE(?}kl*2#cvd{_D?(|02Z#)hX2 zvNwDVu`u9aQp4kvbu(Y-aRCvUmf(n?vy|@gd^NmxS~%Qj>s5ab^}l5&fOf24pb)j` zT8{8HRd;1B>91h5rwf`3y2f?Uu+j6}n7LSeBr3Rv!igA&9i zZXUDC20#3!OyZe=qEPoW7cIZdPz`z{{NxvJ^Vd^@V{a}`!|FWC#sIh%tUR-l zymLfnapqdU7UEEy2op)$bSOOn&mA&R@|LUFYM~jZyIcVRr7=Y3tJho+FvfrnDm|*l z_W@A2Mnki}6&`)qM}{%22;Lud8qnz0>j~c@9Kl&J`BP!4`TK-^8xb+^AZ-Y-#uZ4; zRNTo<5QbSbC)=nQ*b1gO^jY?xk%u(G6V5+LE~_8hd~18kN|xmI==1!5eteB+^H=FU`TjY1 z*qKDx&|Zaphz6SA&B(GMc`Bdqo0TgEVV60E<#|o#$sHXPXWW)Yg`W zz#12dodbCZ_}8w?ZfO)6GeFZJ`?Vs)4yy95*Ren`_v1+GM$G*&Z7>#^)a4(TsKCPq z0Pt3NLA2e(r{ce^Skhp;rCb8wX*caxzf989e(ZOynpTFc&6t1tFvRi`LTTSMH;5`B z=e8Ns|CN+93Z-eQJmC#7;|Zczn9;G)MHkgzu+4EkcUf{`r9$%B301VBBoaJyvsLiW zjS}5zH3Zs_Zg$S@(CCvG!{6I#ae2uTlwM#~pEo}Z1orrfRQqiDHGvn3;yzXP-_3LB zocfzzPt@9o--2}?L1PaRGH36me`a1X=2xHWX;E&J6Tcw-y`&8sQ(lXX{Fb z@n64!kS0u4Ykg_MzRl>5)A@}A2GLE$lejDLqqsBFEI+mg`eQw($fPKR;klQZiMcrNd04|G|%03{V`%hSi8?GD@`kTA3q1X590h zee#mcW%kUCbk;d7VR8#!wKw_f5}1Z$j#Ar+TgBYjqN`QS{px#GZ%Q0sa)<(>ALJ+g zcft!{2R3Cs#E$v#Ll~@{g`If)6Eank@_-d|=JM`s6Ie7VAtuH*tg0rT88wNIK~iDX zl7v(sncOm!S5!=bA$aVm(?;}7P1Zg z)bZpGGIof&yHRH>w?!5V%dp5_Kt{L%L`r;4whM4`8SH3i z=ml&OP*>QJB`s?8h|2204|gm~O&MMIh-Cmk3LM7ll`_d~+#b|JKFVbLVRZYS3OGap zX;)F_m$?n~FRI(?+Yg_bm|T(qMQ6EpO5i&}p2UW#U-$4dRLz|XR-r5!oUfA1Zk|w0 zbC=E6NbX8N%!0I-rL7YPU2AE;J~PO^Ffs7sE8Bf(H;Bv4ny_m1RVg{((N8!z03V9(06-IBlihh%0S3*dm}5*@}UrrgpD zZN{?_?4IfY<;bjPkLHMGmw)Psg-lbfI*D}KF?c;nDe){cjad)}*p7xq2f_7lFn zb!8f~`o{?)W@0X_Ltd-7rdjAfl(W?9I?b>gH&L5K)mv^A|KT!;6jy#>JttIq6)cHV6zAA{0Bl^=YJf@3={9sy9F`Ov#0USXb!9kw zYUW40fz3h-W8X4NpE;yn2B6mQ@i8cBaa(-Ctn+;F3~5&g&m3qC3POk$Q2D<6NHVQx z!v4?N!knPh4Kf;!`3YlghuGlQy4AM%21=nw|3Lqa5VbEZi|<#96B~uT;WKcJuT8`I z6DO_wPszSRcpjs#I2qbfXrnZ!c7&dH&BOEsg&TeqoH-xERnG}?(J-E|{5}LM`~TWp z{H|hiHKg!WKUjU1gq{npyB1gJz|zoeiEbr;xBS=5ryS2|qxR$Kq~a2wp}Cf*Nd0?-h)dybv7|8rqZ#w@^@vb}Rw^DYV& zV;8x~@15v>c+673!ud<#&(xO+J50#)wI&PnL6<+IDZC*I&PB<|lylmQL;UfckwnW( zOu2*IKq&fq&*O)dUirF@1PRE+ycl~-n#YECY#FjxTF+FDNuWM66Kp{hRV`<{I{aisO`?$}U38><#*KoD z@d8Y55nv@=p>a**?@01=>ZyP_Bl!a(Rj+wH^8)v_eiq3`0jk(lRUnW!R0N55@}PSu zvSL0(pa9RZWq09lDL#}3;tdr9#Lol=D|`A8HZ6j4dZpr7Ye zlgf>DK6Rj_mfa#a`dn;}Y*k~pKU%f4I_EARRHdQoFNM%URUQ3F4z$>nA}aNanj~$VmW|$p2&P zEugB;1+z9K#wS`|Q2;T64`c*V?^17RuI76uuhtITM?Hc@_J0m-K@#x6hTS23zXW2YH`y zC5xXVOrH(eE6Caqaylg?LaT`(6R6c(IoqEfPTf+LFEBTK^w05B8;ZNe{NObu#`kA) z$F8vJUlVmxhtzMa^_EOdDw+n8P`pr>ux??E<9PF7JtDgWel!Q);(@$@!oZJK;gSK* zL9S!mD@7fh%!)6d@Hd}dTc`Q#DnqfsH1hoN(rqcxPcKfjAwFJ?=8>Uo{$K~YFzLZ1 z+VMp6+FEuv8!APT#?W!493;R*wRFRrElFf}YPC~k1G8VvU?n&pPs=*pC~zi~)H)~} zws_zHzE<7W2zr8A4VS-QZoW`Pvbu!?#f1I!Cvk}McNr;|ew0^-z<0};a~p1ERyvNR zqKxJOQ2x9=e_Tw9Iq1HrR;+7gy$jkTjYF7d^(Oym%Eb))y3N zn53P?KeAz>P!3wnsZ>68bLqK5TQy4tUiE3PBr@2FMB^0B^eVg!JSka?J}) zr8bEAkl!t(S=~#b_U50xQJ-I2TmiCP0%$un*qI#0Kd_;r=9IDW{q((y zky9=q0o^#-%Sk-Hyg#4@B%Js+Mkcd_5~siC$cMHj+A%5ef##c(!#H-xSt#6@$p1;1+*)s0_Uch>eym`J?Q^A zcTOMXUKy8urF^>*ELWPozIxBlZrNSl6e^!8SQda!(@rl z69B#BiZ^M9jReg?o?H*h>gmcpMjPbcDy&1{qLL+l$?#~!8}^!fd^Ox^yW}OQt$8UF zo15%#z1_QxrhN`nm{%9jsZVn|6117Z==N^1run~Sj>9UYyk^E<^~QK|QT4yB6$ zu>pk#nf6TRQ)n%gDZRXbUW5Df<=Mu7z{|K3tOI@s9M%52L;9UY@HXW>2l5i3VWtWx|EQ14Y_PQf7g z_TiWBG)Xc3;hMo%kq{EKc>A$iyGNjXkt9)60MrF+qCJ`;WW-!=M@7>=^@159xny)z z2+eL(zrbd5$IxOCO$Gh#DH*C?j?=v2zI3yuO z)D$(zKLBOr*>Apmvn&$j#_YGR@hHSpDPBFGdTy3y9rTFgXP=wJR&X|D#JA|jG%L9B zJ4`y3vhv?w&Z{B4{_JtQejX|M3gn%cdcyND<8YpwjQt~>NM+@w35v9|vsx!_@VIkx(mnr}ds;~M`gF)<)#ikF9H>I}<<%&XZC7!MI?z3@NS z4knEBL-Wc?rj9HtEcDqtiyHTZq~a;r2YfRRuP_nqs#>mw_2}L=glO=m&r$~7M;}pu8L`fyewZn})_@Oa;X~I3{~zYkL6d zTx98#aq@o2A3lxtv)Y{}dtOU42Vd0Rh7XmVY3e-?;EYi^G@W8AXlx1wv&=}aF zwpD=r|AyIr|MwignjO-Og-B4Tp-PALO-=`2n|imXvJM99^0kzkTY$qm{;|@ddQZFt z^8-0--Zi0?IuhrK%*TVmSG-v9+R`!hmirhpN8<`5%(8)qjp${{bEN+2P0-Wy5e7xl zDVQ)axC%cx9}%kC-bm1s!ynI->;8vJ{qG=liV`DBlPUl1xYc1xDJGPa;kwNMbn-$c zAA%3Ib8eMl54@Rt04@^JJ#e%P$`UxY+Y9(K~l)P^;p%EZuxh`9zNkm~lQXpIH<3 z&iVP|b61ekIRu0=17kTQW}dYB!%Y7DdH>J+|M(pz$~t@F#sFe=cD9J-ea8bC1E+K1 zQ!Ossx~!Kh9zTKUeTyNK0RD{z2_f64oH}mJ0s$y&ku}cj9WF)It-5h}Mt)o zJ_-f8I*lRfKV2RDm%#B;y*Wkku7H5r5QzLUlghzojS%S&jYPN%^!xppWC2fh2|m#@ zLc*~Dd{3}(12IdFB4N+#k>dhJ_Wf99bun1%x4uPzsB!x1hrjJeSbg{{_h=l}cnw;4 z8~yX|*muv9K*I5$Zph>9cWkKd1ajR&$KaX+1}rPfT$D~nhzKfj*4CTPa#R9Q zjz#h_E~M|?n=aGFKpN72I8iUy2n0N|<8TIUg+};koM5Z(luEB#a{43mrLnsQW5t7_ zG^0GBF9#q{?4Q~fDiMjWMW2!3H3C_|H|!*m%?tElI+i)M|5^)>FlITL5TH{0;n;!y zo1YW3TJv`N?X3v~ri8eUS%N1opS4yHN9b+gYHd0-*&T6yxHdpkg#6$`o`i*qwG3>) zR4j{9h#BP+6#&yyywzWq$(1JSs-72%?e~tW;!?GGl1eP^d7DClj&+8dz70qsO|D78 z8g&z4L?LZo6aqI1F$i`wU>xE*flbCyJCPyha`>Ce%kQ*I5W;SO|6o1;GA8-3X7Ehm z%(OXSryfg$4fnpIi;JcLn^7@QN#s zkv&tzrxlA}^3E@y|B9ni?QwW9sv;D2rU>B=*R8W@z-XPuX%vmlA1OFg`n6!pMQgq z8kEgHEK7{GC{Hr=;2LBt7k%1$1^!^l6DVxoTKqEW&yaFu+C6<<&l-yZxO>$!TvREd$L1)ii+62UL7d-D@@|=DJ4nQ>Nav-nmq4l0OzE55=~L-$#d_Sl+|k zNU1Du0RZqfTgxQD0dgV?Z+u4G(|2;YOlT~})Ywzw&r&vOrea>G_NkP&fl{HS(p!tP zaZr3T)4=MuZ?m1}FuMiP>9&@*vdnn_l}=!Wh3&-O)jacORXT%@givm9|NWUw?+qK0 zeVz%ZyBZaqj?D`q)3jfghjN+M{2_A6fkKs3P{MI56>fjU>KWv?oC3}#TnW?ccBM=5 znEspN)~=J}NxaTwR~}K0?@jaH=iSk z+LoZawY%Gop0TDN;D2CI7f7UNosKJHs}9rm_7X}N_R%Ie#&cLsug=q|@urj{wS^Z3 z-!H~B)vt>55(1Mp9#^sYHZ?q=ba^rqvB4iqeNn2*j(ab!cvy{C)|HTRO`_soJ38_GXiM0E<@Ma`>~kk}-Lul$Yv@z`#?A4+wT%B* zi^@GyT^KEoTpaA(Jr9t!G{?t@8L|H64F0s> zJe=z4YNx~1Ace}~t@=*%FJ>_#yGRda*vD>@ioP!-(>NIAcbI3w)HBCz45SpM%tm%2 z3~AM6(&;@;X2&e8e4Hwd#_xYC&NQrBh6tYX8WMmpT0BrXug@N=nd)slk&VKpx~ssj zkP}J5O30b=Vfc@aQtpf?ULXFR0}G~4ukY?73CI)IxiUDz_SEw|^1r)|RDf4`l;NSV z9mvw2>+li;X{q00H4?YiDZTd9<>AC=Z(+`Z`-ip`fVNLUH}KlUMNu!=yY%ZTX!K>%-y(tfiKol=GOfzJZ0s$lbUbLaHj< zJ7^CvQOLjvq}o057gEHOF7y6P^nbRCQPVh0McUZR} zh)yK$;O0SIuX8A~te+5_s{mVr4#BEDddkxCaFYQtcCW`g!A1hs*SL9dI3VzgbJ$=+ z7>E&A39EH+>f~sM^9=NGz85Ogy*UBlStosG>geRcrMCn9a&a*&<#DHyw}G*&gcb$uI!V%jMIouzT9 z(^w6g2duTpsh!EJVx0@XvU8rlu7JA-lk}1k6Y+?tXJRF0g_|{-O`?{4TwNYE-+A4q zg1q^*+>~BQxYHJ1uIG83yEM{k!Ea@wVST3RhLn6FG>6yptgSay?!t0@T-gT8EsxXq zkW!5fnH6WZ8@JnI#-z#!pJ=QsM09737yHmTV=$Y$aU#71!(cxjgx(Bd^}*^BY6@*Uk1y^dh4Q!XE3pH0V6Cv9|uioF4YkCp};E zJvaM=LH^SCQQ?@42ktCk=n)Cj?L2nU*W7&XBQfFUAxgJNliSByHwzulasO?2w=LqJU%30w~W4XZCkFFf~~-#f-@KglQ9G)Y1PUl0owa zuw=Z6YbLdAP{_jF?QNSDG4~F-gS8(plEdNjTt(@2DP`ul7Q6&-UV}DIbDG>wwqxa7 z@0HnwJy1eQt`wd#(hRVo+8q>zTjsfB@ftO5UTUe?-d=lbwYT(9N)2AFmg^q;2^^ai z{q!t;p=&(Fs#19th?Y>D$@PIRxHth82(;p4^;dmjaTymmv)^_#+}WJ!UKc%GtV3zT zFWF5A-h8%b7bf)o1wj&$lc%iCbM&V8%vTH!-M|wr+iH_qO(*kD{1u#3YUSjGT?;>A zKLaPfRL(d)Vz%QRmiS4i8(k&>_S)YiVf-B&ul$$nyxNt*jvyz$m?$7(gQjb%B&L4k zPT1CzG4kLp%rOBM?>S2B-8MnQgi2PT+g$6LcSUU6z(n)AnA(f0|9Ll7zQ7Om*c)gTjwb=WB$-bP-yiz&?kz;F)qD|lV za8G~1=TDwjDhfAKwS9msl0{B+h`Pt+$T?amldG8MQbMtb*gma>7lOqJW1iWkRz zGQ((u&=S4J)k}c2JDSZ=j%fb14EIrzOwqrCdO{qqvmLaVZ{Ig_v4(h-gBiCh5Pkt~c{uJ6ka}4ZTd$E(6K?|a0RVJE zb#+c+@TEV%DctI8{XEw#mq(R8@Ok);g7brrJQhaJt?@Z9OXjF@%wBOekH>^fD!JUn zf4wk!THb(yY4t-tvz$Mkb%*6}D+FeS^GZb%jXh9u~to)QIRRrX3 zxcSc{G^P3`rBirp&*FLgX7&!?o$jqX0uTkX{F;oe10a)sh)vUqHq`7 zdknHE`0-tiJ~TiBo$9jy#yl=(6DMFJ4SsDll2{&>DOXK{kGnL}Cf1s5+_}J0*w zH_pyyfDX)Bv(80Th|-3j-=#4cvGW{w#*(4rx}S{p13Vg;$~k&((`;mp-}R)|MJQvg z1`tUtcLrf|@RCnqY9;YPcG{bHLQnJgIpi7C1^5-9&Q^$v$EPZx{`GoK78l5KeQ)%_)kNIP%gxk{2&m}-X%NO29 zx+h46z;mnROx~SD$va75+u{t$qZ`ZV`w~X)-FA0N;VN4W--U@vP&E`#%y+lGW)FPX zJ;QjYM289F_5pAZS^rp6`Bauj%irU#cRB!d7t}>XY7Q|vn&mmY3F4Wvar%}Ov$a#P zb_4ox;I=n7ANo;#DJOHnxp5xcr0b+c_=7fyo&(q2Y4p~mKf3EZumF3Bfj%VgP(DK= z`eu*M^BRB+H_y|ZvJS<^$IqkVOdJOICUiA-hZJBq94B!WzS~*YIzAZyfgT;tkOY5S z{@r%`8>ZazTnz64w|+OHkyGO7Y!+u17w(3yi*oQj{D z=?BweT?G%fIBM@US9-2ki3wq8Nu%o7O^k7|7%qbx# z6iFuwN>Fb6d1%Gq9oWHd_LdaX8A&q8XuJH5`TB&JOvob9N={4^N@Xqtmc!4XZlWuA zodp}OcRZd{^Ee;o~sLPB*z~1J^Cf?HIK*{y9yuD&Y4IS~d?+e{#nJS*z`D ztZY7@P`ya7`;J|6rQT^)a!;)2D$<$dL9145)kg6JtJHJqN{bdMk4S(Q&Vn4>XkQzo z6EOn1?AF3F2+Akqf35C`R_UkTKpK5203{qM3p2M_p5M$F9i69^Lx6X8vO#(_T5W&` z#YOkN58;0b;Vt8?`&?nj|p$&opxor2Y$^C zAF$rFV+iG&T7s&yXQ`bZ^K-sXeeIkL%iPhk`cAdRKR~$y9!0j%?3B#%QFCd?>ILri z_)L&+4NN`zG`X3OU9eVAPVc%1=W*AJ5(480+>o}cyVs;i8(FyixvHYy(0q1Qqci@k z#gwcs+kgY+=HgO2n;nbGTAzlsL-=utg{ke4ZIBhYW|F&Rm`<|UrUYpz&0a#A`(*ap zxBG(vR8`ko%C#mzsmkpRCiat<-azl%5#`=rQI@{J{grBMR8W4xh!~q9)QK<_wrG}o zmfVkuomAd`$q+mS{<5Hj8PWqS2ubBPf%J6uSN+a2FWzu-3AG=RaDuAi(QC7Ra9}1f z>4aT2E`E{INzSX`ewSe0G5+!6$6CWK28al>pmSC+b-QC;W@vYJR~q3cKD}^XP{6U&K(ZA5IxWK9;B0LvR*eT%O5v?Z%#P$76^8fRepErNV?)g6E@8IRG5b&Hjpc-1SI!Mw z%O-fAZi|o$av&wE+L!3T>Gl+eegx!j(MmNGN|PSg2Tdi0O51SkWsf#s^1Pir4-D7L z^lr00e2Xn!Kcl)+^rUkt?uw`TIbs&oZW69w#JI_)V8(?&|ug4z7W_O61m|zRFr^eLF)y zubE0xzyF=y;o)|r@s7V*sa9wA*5DZ!XkSJ6N(kjXVoG4WVA zk%7^$OQ7NtWKUHB-G^ftMWTT?|2DEye-?lv7p(NWk4UzmT_RDM4x{J5#A{ zKw3Jbk#?N4qS`~i0rVfNSu-NLPIb1LS#MuE9`iM1KdE0}ddTB2Q6%5F`tBGjM-yZu zdHEr|zhOB3q@354(fpBOB$qr^)x6~Ny<5MC9e$kNk3+8@@a#~p3kV{#=ECh0Z)WIr zc1f7cvA6hK=$KZUI*fi*n1omQxQ-;S+_J?b?uSv$qSh8jtxpKp2OW3lG|X{3T+3!Tb)dS*t2D*Zm){M6Ga z@aToZcz3VNc!mE4-^^|eY2*iYPvf0(LI?ula3wTZ)T)!U0r^4m&*t5Svc7>`*ZGh# zEkIhR2fVQzpPQnMTvJ=y9T}m;ZSCyf)Y1JQ=s_faaZbtgW;RWrjN8O8kAopZdQe$E z#WQM4h#1ngZZ{Nq2pLg%?%npVZ$5{$kS8I601`k&`yTds0Yt~4m!Ql;3K{LZ2MO?1 zW}W!olLq*i>>e=W&3_~#40J0LFnDqRoXvrjAeBw zXnCuFR7Gu4RlTE~#E)oL>E7wLZ*)5=JG}a2qpPiLDBVYv3yShu6jE{RH#@B&2t0UW z+Q@MuT)QVlVP*lo$km)GuR^chsj=zeE#*h6`PD}{FJ1M^xesI1dEnza``=2YS{fpj z7kAEymCSkH%O|?YB=yQ;+Sj0jcnh#>nuEUSgm2^k`)u`7{zeO z=sV!qHE@W+zgOK(sKTMFsWwnvjnwulZ7Qs^7Esg&y`vW_FJDpwly{C z)5ds?Jk@KTZKXQbVicEX-u>XkI*lv!am&*;_5-RZ)OK?edukY|t$(C!*l)0j=?yk1 z6n(kYd7flUdkHjvFX;-p5~iBV)2>tv!;AazO3=wmh>Q}nz7lL{koR^$@=5o9L8l>! z`5)Olk2h6z3fs5`m%)JJwP^8C<#z+OZ(F?Wa*Rd=rfz4EvNx&rrFOO|sl|ElO`R%X zWwo*3UCJub8Md>8LmhwJ_49KVSy}aLQEwoIQH`*w%6dfW#-)r5(8@59h-WIA2NMf_ zkdmBx&VWD+K^0yEL0zKJ9Qm8tyt2C5VR&ZzV^9RPLfUG`t>tmGQsc<{NAHrOGk

    U$KELCN>ZNV|4sD2P_-_%q2)L}JP8joq$Dl9TZ(LCbY>fr^O^6t62gz@)j ziK%&I$ypUua9}KL&1g@ke;z3?5;jztCN3_xk=Z^neMIFLi+e;rtGY}Y^a$q7TmVz| z^Vp1D+QP|E29+nD{H+}>O~1L8nYjc7x5$Q3CZzpnLMACox)V5Wb04hs_3PJNy#~+K zM~h_Xo%16g?mKnzRX@JJnVA`li0!NCw+NaNdlL_yA%y-wA2^*V{Mse8-Lz|!6L!8^ z3F9d+RFUd)HllI^p<@h)k5HN^ObfdGrPIdq;yJApPRo!6=wBEm_52&laF29{YU4OJ zdVuFedC=*rlU>gUkGo!xARq->=fC-ppilIcfBegrfJ6Ao5csSrRL$4ut?J`#dSk(E zgzLSD2XqV0@rAv48;<96X~ynpRU!!sqS0Se!EZlz(NDgy<$2D}I`vv6XB+f}BWlNK zW0|4dxrgb)yMr0s{o|y6_tY36Ku7ew?(w+vSW zX9Iqtw^C1WwiIb3n$PxR5v;J2i%o9yJ>wtkM=Is+DTAl%*DX3XFQuh0N=o-MDms}b z_mJw$*OPE1#^{`1kK?rJbg=FcFXr?kvcd7ky^+W}A=|hvV)O%B(NRb0DKWVpdlc@6 zT$+8kmw*?+pOxeQTl3?aj$FN7w>MH8k|;b=v;r+Qrfg+j!Bi=+%dwVTw2=MAtRb-P zu660s{X`>UwiX)Wu9gxPt*>KEPCnkwoSw);Po-$?yU0U5C+$ZYWl8Qz-}vbF_Y7@H z>kKi3IE3!zwoapMdjqotosv*iyo2Qnk|{K|!C{b!6Vk?)kV?XP!Y1uOB%=5FZAK#d zdYQdX^eGBbQbKyhGK8|l(M4Q*ai4O;!;{U}xs2!glsE;yIO}aqdc}3DJ*p{&DZ%?> zb;pw85i*#{ft2oGo)l4wp0pjUE;@o!8uZrZ>&2Hdq(ra#xGfVA%Cfi%FI65y;?%o- zV3p_HYmYv-L@x`5fkuI9l?>UhV4DSP_8&Tgtm_0D=8{r?F}uNl9Ry})0EsX+n++jY z!n^B@Kop(9Js${1<9%6fnVy-!K8Scx7=axe<2E=KFHlV;1H3s9C&hr=bAEu)52=Ry zyphPqgT;EK`{k3Tg|5%hHy*myN*^jQIf)%C_}lP(3&oo{xI}H*|J(_m>JNF_bXG^mv~7P6(_B+#Y|6E z`|iTUn<;V0n#x5yfgrFLB6>-2MqQz$Nk))ODWHM=&u&DEA&zflQ|dHyBi>ooqTKkQ{lFP!tuT?Vzjl!v|L<_&16#INIM5LVMtU5={}X2e*HWZa@Nag zdU`S9A3R?Vk8ue{Vh2;Y$XOI4;=mgrNFXgy`+KJv?{@0u2XA7&?`N9V)noT#Spnsv z+#3kR&|uK(`#`Ylh+t8RwRdU-mniAfu2)`pN-Hv?sXwZzf6YeLFXXty1aCsSOWi8p z`Rf@IQ9gdL+3EGvJx7NP4UZT-r=uV<_UaSN+1qmhOt6Pva^WsxX~e6)p8mkU7ez$J zBEjO5O@+xox*eq~b+;SyCkwPnbs@kaHwuAYBZcO0_EV z^wOkbdrMD8l5v^wkRKf(eN^Y*7-Ck~bO?Rx-HZw-OC=OgB?t$gv7e!ZE-9&W+wrpZz8d97U1i7xlBo`X6bo%I}YpATk=6H5OdSE5MrDNceA46I|lxz z+krIQ)9AcA7SOrOiU17yws&s0s?=LLj;XyXkpo{=u_>ullCT~0- z@az!UH8Ydh6=QC3Hso3r-47jNL^ zW*_H!NY_TxGoo9hN$;{*M?3LJ@SJ6BSj`f~dC!E#cOe9ElmG#b>Buo>5S_cHdOkli zS4JyWzDDco3QY-TPBxQkXKIKcs66tDGtS_km7;~OxVX63vVv3Fr|eC_3RY!fSj{FX zss)J>iFVG73_Y)f(_*fKaZ3i>@7p`6_$I+4JI8kA+mF9-(F!Y8m*(akEbpMBGi-n> z)?UAU{odj`*SG0TqceN%eyJb^?sTo%+YywMP!M?P(%85t8o z=~Oc&-ZXX0Lay#CqK9QK6*1P6c{Y%Wf83*~tg9azg_paZ$fkm%?<51m2V(ku9@|$C zAWy()5TaxQ!r@~IJ}d{f#<|S^AHv2^D`=du+ca?GKCdFbO99YSw5K?l$0rxrGK3jV zbn=H-CcaN~>p{Xsw6sxHua&PzegTO@~)EZm7z?y13BZ-$>_?!8}WI;kACNf(Zk}LT4BJa}%U!s5Y z!b278Q9SAOYxxO5tG4MPcf1ej=JF=M>o|}Xxq^84uPAa^R&bP-Ox_F#ih~M!yeCIoYAjy7I>+Bfz$; zk*LBl{OQw#%;BojXU-^Fo*gA!tb8z7K00cu6v2HKs1G>SV*wELOPjP^e{Iu~=g+%~#Y4?+7s3V;1K6WtJn zZ%KtjL3OS?;#%koB;`qXl;uv|sDxSH1=W6HhEEVNVs5Htlmr`rLq>=ux+gR=^c3)X zBV*$l-4aO`Rq0|o!xe4@hO-9-yawQtS&$K0o^CM@xqNMU+yNgWl9GE+ZS{=T?R}(h ztS{|z-;04F5$(M`mH5^r$^O>BIbK~6N&qcs)pe;POZ%di0pSw9N-@Ivne($~r2%`f zM#gJ%+p+zDo`?#65WSCi{hWLZTUj`QSu4fS z51M zWnD-{q_%V=n@zr!pltp(49H7-GnC9r#nBwZd>x|hEGFz%H@Uau+$fa8TZ?Kgi!XhN z5Xj@@36qQQH_4DAIVdc15|bYgSaT~A-z|AdstoW`V^uM_UDHG;bhtNdM7qC+m+Ghc zs(9FY*roPo8({e*pk#Q6%=LjUP!_WqafaG^Cz@X0!UxM{uF^+D~=6epm}SBBdqmfka1Gc9CNV4ymu+oDUvlz`TBcw)t%n z;s8C7lJ+tqSSY6Xli0Kh5S{UWMuzpv7yX&sH7`JJC5rpzy{ZSa>`3$uL5u5D)8`u9 z;>)OUWpFJ|H)~@dP0?C6=^vGU7P=fkLugynpB!CeH{#O@O##h+_#?tgUN>xh?7%5ECu;9+5l_ zAs@@-h&Tw&@ld3SbGA7X-}cdEzVt(l>q7#SX3fA?0zhy4+X$8E`ukip6-^G5gNcEk zVe>vS(^xfV$9!bj&83mF?x*2rP}5BU30Vavoglx0PKtOtSfp}zvT7BQ?vg7NO zz&mhoD|`lW3FoJZl#yJ0*}jT}T*3F7*Y}&BiIc<`#e3+A^LC&n^9I@q<#G_oqgE)8 z<|je^&1~C&sOpl5$)13QIvhkuSZL6}LlCY37yrA`s-YPowbc0ciYRwKz*gslv&@T= zETB+1JO0DXL>)ElP+i6^!OIBW0l{eR+aLH|)UCLNJ(v*fc3#fgTzWtf$?0LZATBb! zzC>BSWy;y}=t^44J~YpuOcorvP3d@}07&IYlq3uA=T*EWnbreHBV$9eFGj7UtF0uU zG8lmZW0?@7&0gC4=tCo987(g_uSVrfO!ny<4Zn&m0<`21(Mz9L0~2t>lA>p5+-EKx zDp*Dq4($?4l$;&Whfh(aqMU=fJ}hVZRlj`+niFOr=6Kp|o)Qmx9755@z1O!Rk1}=R zwb|c<9+%U+repL#P0SOClc{Wr)E>F9rd(>egNMEHAU>g@I$?=~5X*vV^_44JoURQt zcE@X_J#fw5gKIYL%^_KU(%0!YC@nicl(|WCMfc>%lcs}fGd(aL`tBR}*f)_@Q#}n2PkQ9k`vS6yDO{6D$6{307}2*%`7+W@k>2xRDxQ0oqgbn+fLXMTJiMATzx_=An3u}BZr4N z%lf-vlAL!4PmktABBe`6(-s*`Uh*%~Rv%m;{|I_ckN7E-G_%OaC}ErU@q3*N*tCa# z9a5u`@jciC_Q-tlBxs#imhJSp4d!7N_0TXP^cJHlxNyq7=PL_lx-`@gG+AtzY+S?o z+xK;51%i-eVHBpOzev4sZfwl*w)6JZ z-17dU&f=Jd-B`X4C#~1O*uvNu>INd|8-!LgDTpqpHdx}LPLM{@YS2e7g@J(gqphf) zNM1CwI(8E1#wCOf=oHIUGAH0;w7YZew+KkMox;^Roe-|q&b~IE29&A<4S~^#Muawh zOv5`N9Wi<{q)S!J2`>i8L}4f>z6Cc9#*5(N%XgnJ>of()qFs?C7O)SBio_oj4HLsB zFE6FPi@oN28;^_a)!9AyNCXhsl4u55eSB0|2NHFT6pQPk@EiOu(5pcrbuYk)i^B&4 zqc8d#sFI1`yJoK28cYDA0tPU2L0BgAoBrPO6XE{CmqmF>s8kwA7N{bjESbKd>I$Bu z`F?_Exu)i)SqTxF_`Nu9IDFZUSZRTjP_0}}1W^NLgl3}aWr>w0A+MynxcK=AzKR$F z1H*{EZJRd{Ie!xjuC1^c_(@}q{X#ZU!o)9883=PMTShE>UaB;Z))gGCMNhCUbpLsj zd+dGMrQ(8IgZO7ay$A$h&~sna^=E}w(HV%b5FSxlUYCb_&jD%F#GJ|L82|mffrthe z+6v0I#6ATcLVCnNTkNBs+226Hgb=@CT2+TrRY2gc``1#ulgP_l7#M=FF9NHAX;V*A zQzOzWS{l8T7M-Pn!bgw{UCIvW&$Sp2b%d#7r0OmgWk`toK6inpnkjq{1SZ9T+DtI% z;2Mu^y@EoWzyc6S9RnSaS$p*^?Fb6vgwWaB9UjQgH0qx8(6DN=A%3q9($pykbBq(; z2bW$UebxNerE)njWX#+dVX=g^)K?Qt0|a!=Pm(4Vf>nKe4WII?kLHm9(gDq+0`tE| zf@>h#DYhI#KZLGgyZ~tz3k&@wfVUB#&{D`?`cK!qner=?AU#?qIFyn#Vs?G$8s28z zPMC|`$KrUjc*F0u^Q}M>0a?7CH51|&a<%v`M6Cs@UEw;Be4hX5UnF+k00mf5I7V8* zq8OcASEraiKJkQgZ<)lobLZ+xzaoi#PWLSj$lS%1K(cBqL4REf8J(5JGJy4Cg+P=v zQd69u5h&-C3u816SECRH4Yqja+XXhR(-f$!c9GLpBntH)qkt&|yBqIIVCOgdyx)zQ z@Cb|h3ZMn?;XNpr7k3Q1#wu1Z1sDU?-$Jo^EloJx3gJ{ih(i-gF z)@P&jL(4vlC$8fU_=Dw-Ke*NwGyp*Y776 zixx5T-7ZQf^66wi^N~paB`N?mV>+%+MJSEI3HCgQql^N=Ej*94PVmfF50LcIU2A0Y zfRv-tct4PQm{1+CuY91{p$13TdD;>F3(oEJHtI$Gw>{ zWo|kT=;SV3V7Ke$-=xR;fyfxM2EE8;_kpoq^D*;@qsTyAxgVIweu$3FO_EAy`_sh}BcjoHXBx~uua0N^ zVMi|G2(A;{7p*1++(ympCnDn6dd#l4U=6Fuf+0L%>645CiQL9Xga&j9d$chtkYzwZ z=ZdTeBoL8=6wnQ3M?wO;c+lsSbb)-ag=a!0sc~;LrQh=H3#5iY9b#}67$t^k1&W^* zfDxA^8DvB(AnAzbX(g{~HC?Ar4y3>2bRIn73BuOXj;0byST(%Xmk&uVtTgVWrlzJ5 za(n~A_wJy3{^qG2<$e^G_b6Dq24b?xknJj7YHUDcGF}NGzQ^O@v#5Lf5YptX0K*6X zi%CXZ+IunDERMoJntYcoK@i?A4Khk2mGC}*$mq63({ZzDF+_dxK7ZnA#`FNfvBME* zbgIH+qh|od3_sb+!JqE(4!J~&k%~d9xg)9kqVqvC08?AN~F4eUY7mXIL*0-N#4anktQEgtuh@H9)V*PLiTi<-}LxRIR8_ z!3PErvuaVC!~(-61>_AC{@`S!UaLc)vwv8p`xF_;Lhjepc`waQDte2B@wT% zd)(WPpbw}@Jv3N7kW2*NUAC7qimBdR9aUu7dCHs=X79El^Yi3dCw|3=I;5Y9&R-^BM*F8ZOtS=-Ns zO6SW&#&5T|adIjWnCA(g%9w%Dx1L9+1r<`ZU>y@+Yi67nnyrZ%Kbs-h{E0{P4ipSp z^B|gAhG^p+=mNA*T6CdGhEg;KDmf6F_=sk{`3O`w!I)4CuS2^#Vy`G+WXd&~6)v%6 zK$Hmk<~JKc^)eMVsMGv3qWuPbs9r`SAi4_$6lFe0pF}5M70hbNgQ=xAcrwe08V~{N z96>~K0@MRDbbEoDiAiQV-<16*JWKb9B|Rr zLV8g%h`ZRKoCt+d^Xp_lDa(&{DH-ZT!5B~PUHIE%7l(Cj8u-;-^L&M*dcZh9=yB6X zhWqc9epMJxf#?fQqHy#P*%(ADOfVAgYB1AXphr>%N;pAgR8m^wb0&D-k>RMb>G3*I zk*AGjyKa^xCD?~gdtD5$0H<{sNuGG7zj`HWwy_ErbMN_qCM10*=GqCRF`#SdmvTd{ z>3wm;6S!|mnc$zCXp+VhdcS*A$UV8)>jx}GphCqLmfqM}%0-G2Mu_Ut7l^ckUqW&x zEKSF#p)4{!N-k#6!N}-ou==(A!u1uzR*)e&CnEzPK{Ve_1?`871j5S@yV3#_ZxjD+ zY~Yvtym3Ued^D?rwvQ=J?tTaq1rP}%b)SR=Ko!nxUaRS0KzWu`5VMNQK!;%u0or4} z!+07L&)HID%A7#ulS(LFy!iGdQeuR8UwTVC36cwk?}gl_%DK}cM|Ar6r2ox3JXg)ZuArqG?>#iU(iSigYYQ-ie6MN3SkoLz)G~6ZxSh z5+OyLGW&wW`^1sGU_e}xcUU~4<*iUuFOk_jOe8z0}*F9^tTLu=5YQOh;< z^=>W&q@9Xy-|EVnm7`cM9-KMvegi?J1r(H*_Ql>A~ldc2Kcj` zt%-j0Og|2&cU%7;1#0{rtpg-c&Q5Y;J9)~l-YoA7ab1#@|BdqbeI(rs6(8gO z?k=<{r9HA=oRSXo41om zFW9j-n#TLFK*p-95(qm_`27KQcb)fj zy~cB097g?PP0iS#Y4`jju&I$q6vwaqv$l4OWY6qy-ss6_TjJ{rQ$-sS(okx= z?%-{)DOi<41aW!z3G&xrynn~%|0OS|#1ytlCJq+x(ukvUP#1@{BRbOWe4-O72nk_i zd^=BaH`bc!>`e=I)^hIie<%5w@oGNUQZ7;Xg5~s3@&N?wZ}@Wm0B}X1+~;Ox{m$*i z7yUR65PA5SESz3c6MX00xX){NZGs;z7?gZxwq$UKsIYaph;#lX)5T;K-x>f2EyBG& ztaJNhR4lZD>~OU;rfL>ok`^vG_i(hZ(?StI*@w;CmC!-5BLlWlQE#10OszGK{A0 zLknDZN^)%0%W(3E%u&k!JHGn9(|n^|uv^^lr`ZM~`91^w53G^03!ZdKrDHmg+F@p~ zckQ%RJ$r;A+T!B3~CeR1L<)#y}>MPS-E{oB(n*4n~H8 zKbNR}QGd-M?U%a_77s#7a31DaL9^&6te!0A1-ISqp*tQ-iivHDav`{T`}+V;jJHTW zFq{eNNRJL?I!VRf=6_1dU-ADboh20aw&_7GGOM^(lK$HXVSldv zgZZdx6i94_4!6}`Sj5lQD=e3cDURi1N_2x6kWsb$#YyD@5ljiDM?U}uie5KzRf`z7 z0#OQ7 z6gJ!;e}X*S^S51R1z>V-!Zh@tYOX9a^V0kreiS>asi%F>K@Yj4ni!T>M!@;2rf_{`FwAtAZDo=_3T0T2UA2oQVxSo+== zPLqP4B%k@=i`pk3TI2ge*O$ZPY(757>y5qq>D~hw!&EWt({j-};(VpjG~#@_V5fQK z{(c@UGw1n@ls@5?QGa5QR@Z1F;UUn$Y$qPw^Hd)59@{MX%nHsu*!O~P+rrMSSLU6- z>RbdgSKwr&zQAc0Eq+x^zxdN$S;Y)~1g`Q-xLt8sD;#K-y$d^M-*IPpJ1MLc)fm;br$+6eDP_k%6%rDL0#1tLz}Dg_8cQspRnf6+DsdZ{@S%vFT{rlTdGFSA_y z9ipVa`n*)Sm9RgFG>ZVdk|&uW{k*{E`SNEVwqOT90RaFb4WFjq_1dIOe*!3o!x__` zxkQx2d1-(X!roG1oVy$b*5QGW%21P%9ik z%77Gbe4Cu}LlJGn$J?zEg*3P1PUJDR3cH!7yo$3{JaAvJn!7g=*(|uP^-nQOl>_SG zr%3#;wWAFUNX1xpGc|xo09X{z;CKvdoP*0q)aCf zppaqb>A5HH7v9e3rxv8iSgN1P-FsRe+~6u8&UN?MRc$`qe=n&7nxH$umDc##>5PjNlLb*VB<<5o(zhl)2h2Zu-rl;3x#T zS!H?OC4{Gldsv>IV8O2ijxC*l)pwxP`V1hCP5dO|i=SjXN5(%c@j0w2{Nz$g+yO4^ zyLKZhyH>=M$-#^)4<3vUO7mWZoK4A7p6N`;<2XjRZ2=EZ!|kzQFKY80h$H6$&y?Vl zz1Y|-SFd4vroiVE6swp9Vlnex+4INCW<{(tQlB?~ zzwKUNWtCKAcw{Ia_(m&5FrfU8q)UUBeLdA*)G|ANZ}%94;L^g*j+0w%x7?xfC>w;V;DARp;d z=WN=H+g1&VbCwKxb88N#I6v^1n2ttV!D>!v*Lsn5DkD%VWGDR)yEXYDIoh&%7_h8O z5##s%RU}`Ze?mIl%h(QG@QbXUT7F`-aWxNAcakIc^P4i$T`q2#y|SdJ`(^KnN&qEl zt}<1~#)YupmVxWDrS{0;T~8KAU?c%-ADC~Ytij5Md0I$*13v*#;IEho(5H+P);rK6 z(S|FPXHH5xB_9fPfQ8aHqv>Rm!=v@!6;o|fOH0d0jtX<_)b3Kd)Bd{3v0q1r+(TB@ ze8A6IPFCc4%>W%zZd#fYcst>Zn_KM;r;Auha`^uNm^+dzBLW*9E<}1qeTLtq<|0`l zw2Bjp;(p?-4%7Uo3Y$)!nACFp=HI_5d!1mHf>-MfXj|Y|R$!48KWdY|YTK2l4xw6a zQ}$5nE~~x@@l*tAVo`T###UKi3o-C7>I!IL?18=op&-VIJD{+RJUa#fk2Fm*-^1(sR`xO1LZjwi(891&{ z>2OB-&89Uja(V%ENt8FP5#yx{UQN&nyU9t*$XEm>?7)D4b)Xz^-Uk`S{yL?`fggIl zr;p}qg~jpMxQpqImDZ*tm;Rf7V_5(p;)WlBCO4nSls_V;6Myc%&0xe`pBTdMf&08z zv+tbsNw?Z$wxjAKy4wA*w>hKio^RD-Om8;e0Qu)6T7v`(W_VQY-C%T#6lUs>GOJCBSaoRW*dv_wp` zAPi*pb;m&$2Wwzz+>!m?HgN?1-aD52>>+XeP93P9izccqm;VM2N40uks24Hx0Z8f|Udibc5!8cYf97U`(jF^;-Se-)=Pd+y@B|Pj6C(Me1TZS+Nd|KZAow zdXq)9#tb|P#n4NUl1Oy*T+!dZ_HOc#jN*JB7iGwA^ZbfnKn5LSco*;yhcJLuUHxF= zA%-78ulGGh2?R$(+=Tp+>;bGW)ZE_%01GmbDMGVoaP|cB&iWn$a`Q{DDLHU{azNPieORuQ>V>7Rpu6r=-!Yyk0(I zXs)=uCgNV8+MJ=%N@RKSPWnJl__BOK;V!$dUXh7L`;EsUz({@AEi&%#9Q77;aVNcF zhlk&iDx>v=tcLgg_rDaL+2W^&p;dji$wrKCgeK927j}Uu8#bcIKt%*O(+4UMiKBjd zP*?jyEGN~yiZ=>>A8bc%$JZ=uI?<~;?xaD*Ww1Tg1HjS6mK-fcoun7tXh2`>*=9Ze z%zElK8`?4h=-qJA;KDflklg3RDk)Jz0Pp=D56{gzC{AMr8_~1Ts8SIrk?Qh^?=Vs{ zK9S$e%+jo`#W@4vtg@|h&rS+V@}msr^*18UTREUljJY;}hquf@1U%@;Y4OFc!&XTk zNwETQb+~(Ap1_Bp>|E#l&^J=^aVe?pGe2q^8?D)NmWVGf^wP+0@uiI^- zM<*LJ6oQoblhZj|QAr7p+vH%TEY_dtOsYH1Lt-3EH8i4*u5(_8wG6$dARcLdNS=Em zM#~^KZmR88&EEgCX1qy-*Q|#M7z5%TO=V$bEM))ALs<%3FvH4^RyGanek% z_*>n)Y?+K@AI*5xzwgz~IYy$Ndk%@>RfGPv4OsyC2Q@twqtx>T$nE@MCIlQzif!EA zFQYeycBgKCcB8k|KKLNcyC-fv8UIsiw8ThS-%Xo%#-m%2ZloT5zoA5Y6XYgxk9jtOrR{2Ad#d5(iJX#O5@k2GG(|=^J3VOCnv4w;O zdhZtDjUESYJQIcZ%VTG<0XQP+`Mul=K7U8N-dSZWD+0iLhX06*52u?j%5#NI^dcA( zIRW^Se;kV<RHz%a&Y;3^iWQl(=LP%@M4Yvov zXpzkR33@EO?r1TaIgflsqTPTbh0keIoxkXVSy)Yt5O|#D4R~cKrW(6GC_Q&Nd)ODo zhS7fy4-$~lV0#I^pU8W#>HqV5xQP#iBTA~blj z_OYZ{y=I@jc_ayn-=bwDd4*#2biZN49aSY_g(GuC4z{j{7-;7%!-}XOckL9C`3>_0 zY}I&Bqr$KHFV1Y7>h`ywXFUfA5!O2SvZ};hB_g^v+Anc$8ecFCpB^BY3URpnuiR?p znJ@opd=Ry+*DT%i0sb*J)V8OFCNxeGHd2xKO^hA9Z%Nq%kF#z z6ZU#lA~AdRu;~Ge+~v_Y!cac0c;v4J(^NOB5}}goWFmVZ+VbGsr`wF3gF_3%7*11X z@y%tLoDz!Wymc)ZzYw-7&RCRwZ9b(5k_Q06TFg7;fx9rwpoZj`RTW=&^b%pw2?gWp zkHE*5-Alqe0{ZLuD=c|q7G;jd@~W;LwSlD2?UT%kV=mdPokywk&Yhi^suM3QFpe7> zD0eF=mKWGBx-y1K!EB7&AJ52KHCN)0i04^1ZOB41X2Cn%QxQ-`5yu?OwNzfShy+1f zE{HEBpqzd|os&))v_cavlrBU5DGui^znuLD$G=L1+Vjze|GLxMx)9oF3WTyHZn+Q1*mhSRO6>tUYv9M<5w z)6OiM$=e(!Nk~&InNo;Z=|LOcPn*MC9HLQ7kom;2)yE@W!FHb#_hsl6&t7kKaf5rVS3dOcqI8g0*WdV!HF57|fjD`nSGerhOGbah3V zJN2zy3u!>o* z>WwitO0EDpw`1DV4wlZ&-uylQLbHy_02qNp;Z7ofcM`v|U_t;3!OwLpwd42wLbcxw z*;dhPALiekZ5=76>&72~B-W-aVv#&*rsVTF*{93&!}qxq&%dvVUt))bhq0@Pws|U{$%n_&i7?*V6kK2vsQP0}s`2VXSR3-z`S9mpdhkf;~uHez@gu?iT2P-`~xS)VEFW ziow?VF=GS7+i{{`LokC=I27KQ9X>p`cYwYl;kt}vQl|6m_9x9TM@ z#o~5f9T1`Wx^GSAn}*%2%k?q*qvN0t%`d2s_N1@2s*GEJswgH-Hg|nfit7ab?yqmM zPybu+L;&?{2(Zccy<323<^ar%frsm7z<_Tm1Cgv{(~3xQtHL_h?i02Jf`26l0G{?F z7@nx-GzAJ}Hti(`dF9L=4An->qJG5jQ;%cP@+D8vwmvWn?^|6^2g?tofwm~Pg*$uh za}0Y|pxS-rY@5bxnC&+%^j&^y7JeJ!LuSXVV5|;ez&-gpb7AM#D%;oyMwMUkb$WFC zN|r90jXUQa***fwZ^Q@=Ei; z=S-eEpFnAXi4}*Cffy$}6w-}b!XbTrAv01UFV4_gyU2b16Y>83uySf;-*9s)mww&4 zA?_qDK~NIc@pjcLQIPhBLVSj@PS!CFv6+{@kHDQG=)_#@Dw}2Ygif|y5cy!$ z;dtx1xyzs`eYU6fe`1}cl}cGw*b0LmEam;8>!HMXfNIfpcHMo)b^7T$51$eLWTh-N&%K74@+5&U3rk*9p4$2cQllRNrCOJ9^_#C0~B6@k>n8RX3&&Rw8| z1*&P^MYg%lcY-9Yiks2rNOnw=Yh7kK3qR|<0iE0kG@SjZgkFS^S=S9CXO2&n4A(YV zo#-{3&H^rd3V}8OsSxdr>!9*ureflXMxC5?6dV9DuELr-PX?R2q0Wz?~Aqi z4xoM>0YO2VcogaDSSq;nVD$CP$D@+xVgJaF`$VfMDl^%P<IWyr1wIk;w9=3hX5a zA+2%(NX;m~nv%kxLpe_MhG&bnDnp@kpeB_0dql0SliIxo|6A;UG^GZ;sF?HCcc>x5 z6XO76fwqIe)&S45-^volGMO*Khw_V?EPcF+fZpKzWDKDxCl~xiugFw0Kkjw>nnF)y6{WXSOu^Tq zs(<9ptR!FQfBVEECG>E2RSAS6H$uV7mExg632iiA9^~_r|hM zOc{2bx6lh7Oj)}twze)S_xNB6y;QN)ossK*DbNjcqI?SGyV<*kFkpn{zxG8+9=IT9 z^?|O&7lU^ea@I}}9zHQ@Fsh#3iXAuVJLu>4*uEqhA-h%L9WS>4=g{0tp1=(hi92~v zj!(>xlAj3b2y6$Fl z;=XXf*&K`y?$po0eSbY2JSU%0J^PzH)ot~T_~};p=u5JG4Q=gQQ2sY_f2QOKKt#aP zYEIhC_Ln`U8oUVDT_5>RTI|MVln;&H%kwgf&=-6sF*z}v9B|s8h z)Ui5^gljozWxlM8?5l*`$k#1W)bO;rM$i{b0yA!Z%EahapTW$mZg{_?h0l52h3~Ab zDU@ul_}y~g_c=3sPQi|KqC@q>pmdF_ryh*FapeOnLQS7`Ypz(^;nw&kahgeg3{Q1C zB9?0y5%*Zl0&1Jcs9!X&zA#sPIXI^tpU_cd0vgB8l@Ey>Kvi@TG7<P|?tvG{b|te%v^c2e zD>Dx{fW4{U-A$Ssxfp$)!aB#Ak=*Y1H^w@Lv@`bB+<_w3U(49xR}~wKOL!Y4Lh;ou z7DrIhmL9$@DcPVX$uJ4v`F`>Pjw zmpy2qVvmlBlHJ_gT@EDo7yiaLUB$bYc8sdovemx6ago6nuwR5-0I&oyA&Lm!&Bp|g z2G+Cu4a+y3&T5gtn_T*PrGS!hDqA{KMXspMcKcBRM2J(e(k7o41|F3feYIu=sQid` zEYlpA?7BILof_~_H=2o=MviuulIxO4p-KsS;G;1;In)Pk$`-cEHc>-Fy2ZP6-KMK$ zF6XMRNq!wp(ev4%`Q|GaP)-*LCqy7r#5IW6*Zs>A(@>~*K_T8tF`ciRDnZ9@IpT(j zWRffbEf-gAn`)NorD^De9d%oK_3G6i2=xh^3<(WY)zO)BLve6$U_m3FDlZ3)#^=>=iIHs^P*<;!Bwgd~CTcaYTn00mzQVs@ES-xdZB80Fr2O7=~cC-LmzZdfaVY6%UAd zQ|hIzoE~1gU|!qn=f|WnJu*98vyZ8zoEhitjrfpZ(}SrRoj?JjhS!rRH;l)@XqsdR zIph#8g*D{@t{C4nezDl%9WNhhvAYWdY}m_e4UKxvcD2kY)q89(>4AIA${@X>eN1Q( zE_>KvN{X-}m@6c{eokoib>wc zD6c$NZljNwf!DX3GXWNkr!tAx4l^!6Mmj&o^YRbVRjydsk?~(6C)arSGAM!1!hY6* z$>(fWusTvZ<%vm~o9vaRak$Z9@9e;9x7a09nI}gdu&}(0(|4Y5LzS18R~oev)#529 z@VsvvH4RO_j?bSPTI0>F0L@I_?!nwGr}4(q2>~HAF~Zis0Dv_CmTwg8dueRY#hJJ0 zmo;{Pl~?yJRtr;mc|EOOir@A+A;pWQEMXwn=z!-i&;g-k4HUjv!Wi?}Zd~N4Km85N zsj7DQ+J6RR&(sN0fpLB68HJ@FFRhQ93Edj{A}r?9=($l7A8##I|M&ENSCc)|0UFtm z{>q%$Nq#deh=6JlHdv7R19!4!=@Q~l+PcK5lvC00JXP1ngln!uBX!lYsQ=ml>fMG> zvXGnV6UD?l^h3^(YFKCy`fGtr2zr$7zMfIIGz@#*E70DDUc`nmO?DNhE+U-DXc(;gqO;pNsMXw1bQv zc>d7B#|w9Un7<*+G!hvWHtbI+kppLR?~q{P>e17>e*OAdpc$vT zvO;iK3IN5I8>XHBc=UDE%OWRwVH14MXrE-ZRV=?8)E63Tna8B;IgpUKMa430GKk`J z8U^6S?6=HO0$*;tn#|d+(sM0=!wEp7dHGBf>zUOT*mV292kf`c4tf!rnY~lXgJEBxT$vb$jKl~6BT+=!5YNlX=WEw%TH&S}A4Z9B*eGvYl3r-`$G4j;J0k#REQkI+Ag#+X zz1DlaMl8dFy6ds0sXximCt(K|l3Bd;8(4lxAYv==h(0Y$UkI>-b3=Sj@xg8^FUqe7 zUbW`Je%GiDp8Rw?giL*<0S|!Ky^mzpe7jRLg6#*fJHov(lxmV+0JDWYYMf_Tb*?=y z$&Er0vjut-7$OXSlA>H1Skcx)SyMe#0o4fhcVhK#@@kCEJ$Aw7mW?SWkKMhko|st= zV6A&)IW4mp%kG7C&3a4+0rv!n)WM_svqCFfBd>%uYlTZZ!(3DR%+#T7WO8|=!3}GBDIQhDrIaMJtC;jxVb%X^ zjfh=E&`|V_0Td?Q27WqOq25M7?^+Bcx#&eeHG%&LmH)4p<+H3#tv z+=tBt3)@Y@)PmtClXi98R$B!%giRa=RRL&cK*$dUNb+vYBI?Z_0A%OiipA70h&~2@ zY55x0UFX|r$oIu#qUE@B)m#37D#+85R-0PwQcfL`qTIZ^q19~8Mx4TXr zY0Z|+>0W}=7!HdgsHZz*Hj0kNvEx9nFcfq>0x#dC<&Kl6dpWsRruQG2HV?W+?b?lW zZvc7f;?W}_vfF8RxdI?SxY{W&bttaWzxXkQR}ENYh5SQqwz29R?f1g6%~(WMyG(`_ zR8b&lVW=Eg=O1{hDG9Kzo;Ec~3S_oB=pi*6tN6K@#b>8v+|kdWozrX&#&0RE2JUWE zj)$%&XRC)t*Y`X+@SX;V!YByD!00(#?T11x_BfgRbGmce=R27oxL^;-B;olMApS{R z-Tjj8zva0bvzTy4?Q8m6_wPSf7TXQ5sYeSy9gNp{-#Mh8mWeuVHVqsKy?Q)??m@rR ze^eo|dZ-_SJ+%QTG?yT*vVEIQI>Kh@^Nc_yf!{bM$~k(_yC<2^q|lRKppvSYx3)kg z4&MSvLPg&X|2m$Qu5F&14*X;5v?h!cXgXzP3ciag=gvWMfDQ#57;yjw<$}luzxat* ze*1Tl`tGmfYk^3kem#hz-tr=k-%71b8<2QTx^uIkh%D#b+cca>ag{pp4!G>(D~6i^ z*QVY^rV6DiSJ@0VWfiOY=CFC(nA+ocy4WT19cNu^~z5_`sK!~+6il? zj!Ki`U!>qM<5Fh$2T*rrS85h&0Yus>I7bVnq8=};F5{_PHxb9L&$&vN8bj#&Tt`oI z=g)q8I>5wST-5Ck%jO}hy5$QH&+cd28}tbE@{tmeMcLf3(U0U0AnSGVY;^(H*cZ*W zK8KAst=xdi6-R*#DBI#%j9lx>YXB$*x+C8>h3`UxxqD&BYBNO+Ykeo3%HFEh4=`Ze zt*^44(nPOBfXNxp*JBzFXWf44gTcA(;sgbNvHR)nfBuZ(2*CUFqM~VQn*h2F_jxT*a~czcmeJ|d#Gzl2&!3p695kj4R|@q5s~b`j_fV*{sCg| z3=llKVQ)7Oz{xt}{nK0QW5da;giPOSxs9w>m_}<^%8>WS-l*0Uo&7e2aFCn;*p0A> z>&|$fYpVz~q&-)Az+S7CzdA_gt(JgXw$XZaj|rG)R3{BBXV$*nK-PRL?86f%Jfv)1@j8x4No!(Qji9OXSGeX{={w@ZAmCe5iqhJF<%6Fs|&yZn?W z6L*}78{F%c1g$Xg(b>@To2|zQhRg3p4Y#Tc=x-Vv?OP{iWy)r6N_be@4~tT)Jo+v(PNAWpU-NY;%+p6kM)r`2nUWgn>LlB1J(wRrF0v+F48lMF4J`MdS5XPR zrWXQDn3|__31@a*Hv#ka1)Xg_aO)tCx7GC3zr}0-cvQC<0b@$bg_+aL>74dIoQ!G& z%j4YNo~`4n&V!1~PPi}4o1PZv6pWU2-ypy;fi!|&%Ms~PT>JVuQsB)mb089UmMcDz zu9#2P?{W5<^xS&l_4#(VwoM2$MN`$FXl&LnW8+%f`oKa0<(uKOXTjTb$1nNANVM*L zFXijT$6;)?S0#(0*YD{$m>`-!ZxFOOR`{<=W!X#M^!6@}KV1#et3UTRY#5>wCtSm) zLKQVko|A(Jn{vF37{L3LW{@2L2dP4rLUbVrQGu!Q>5{Wgc+cNt5nHp|+F57ZSb_m7 zPChddjW7zRSUR*1A+7wi$7QP@8*4AshW7-pb%^ z;f;Yier#$#MlO0-9=+TdK;xra>46Nl0n4hKGJ@CY6gQpZwPALr9r|-CD)t+&V52_S zPzH&Rja~H6Q8o^aVX$pG&z^kyMl0&Rp63m;`5O6qF5~=?-De7}LA-{pW|!}-@8+i9N!3ktQtN4=K3UFjE}HtB z-lxTQNi)|Axe&&>{>3pfaGOL|3w84STIM6X`E5qzyy~-aWPctU5U5+0;pXp&3wnH- zJsE?u=fB8d1+b~NP+BDQR8`UFq3nEEq;JK@CN7(gjf){~m=xCSKr(>e9^UGKndXxMZh+`=U%(1 zH8-0D-DBnnVieGL`$0R@<-QA5eIomNX^L44AU zn>T+DGrFct4ojTvNJIC2_06}x!(?@7X08#2rC?Ci!q3c5eV}@eFUW9yUD?T$P zYf9EZ^eD|E)qAnQY1P^N_>-)yY>Y{oMR!gVzg3!i8?nwy<}hSAQ} zlu}L8=}hU<==@=SxG+T5udAby?sK-rY~9VhGwwk{TsSscjB^NKk3cr`R=sfi=Kj<1 z>UaNZ!2H^xR0DZKiTA}M0neYbZLM|)nr8ViO-8`yHb!wyg=54(k6%EuU%q=vq8}Fn7~PhyL&e=IH0Scm z%2c@#XJrDmv!1gSoH`W)fbQi=>YpxFR4*$j*=ZOqYC5qL$Bw9hWK<37c)RRcs5^Gk zY1U%ZtOsbzb_$AnuS;2ivLS!%78e+Tc<~5Z4m~*N+)Jdji(p%sxZKn}UBsr9igW*% zg8kmvJz>*Zoa~Yy>|@U#_c4&U;0SiQ_U{}#_Vx+_&<*AvcG}Tr=I>#c3y?MS#7Mcd zt>fpoqyBbwNqhCv0+h``na1R)hDvxGFY&EA!Z`?c=gmii)c`#GmS7*>k}$$hd~9`i z>%?w`1+ZpcS-ld<)xyS#fB-ehNby?|Rq=sM)I3<-YKA8bz*14V2Fs$jUZ^({)= z*6aqOn0Y%zSizl(Y*`=_nxRXn_=wWPgK*h!%5H5COHvM2;}|Xcqb1v4fUU6S*lG$g zPY(t`z~YfY<{R+p>a2C!=g@~@^Ez8QNB1oDl(Ul&x9n1wJ+f}F4W-1b@MXYlc9(Nr z`q^3n_Y7Y4gEvY5(+JRB!9hj^PKGb;y9hr5+9@r>ZE6i&FdoGDG^$82JyrfMF5yMB?iMqdq0rI5C>-2z*_rqtb%f+#+)EUYUH}pd>ahc4)kvMVh?Y&L+)nL$8Sv#?VLbk%Au z-3@mpWykT3!K|lj6B;&1*J-*iZd_N@}l*WLhlp&DA3VgUlJqI(p!?VH z))YopZ9&R405*nZZ|!c^V*v3<+<2s$o1vHUEIuT)xN&Dgv4x?z)lj?mGE@Bh^U0G$ z6F+`;Y^74eFBDC=;bGn5+?%OvLD@#k8$uU}6GVzfB;#1TeOHj;4^Rg5sO{#;_z8iY?S zySO++z<^z9(Hg4-3A0In^rk-8sF=;=O;3Ieosx5xp zn6C@?rc{U7XkU3u)0+rrxENi<$COP*K%-GCKzlUA?d^pe+skt!i|@uug}2`?hO&II zcpl$(*Zr+am|^NY&}cf75OU32Ba=!;(NjchPnw(x`w;FZfkuR7A$tV$^n<>ITi`|6 z``W7Q;|$HYr34O<4dOAwpF|A8eCGTX40#-r)%m-gFCLoeGF4yqprKUCpt1$l_h!h$R=(D`= zq1gnE^ZGMwB~hs8*g&fHL}$+u#ojCx}gcf3`vV!ZQ@ud zwf>=*mzP!WP<5p=EVbCF!Lb&b;`);#>qDjM1&r2`+yF81diYW9x#=wluSh3DZ@01v zkNPF&yu5Es(;U4iT%0K$3*G#-Y?Zmk4s|>CRu7R1bReCG!}Z~12;-vqbay~2fQR+L zdckkM++n%o8WR)aelTISl2=kvVmWq*8ta#EAK2eWA*T}GACPu_FrdNmechUc+QRRP z&)E`Fe)#i)?HOI$Mr%QJB`<31?d=IM zPX=4B&9JOJebV(`sBMX}CXw&SSA6A}^#}cWAYd85Xu?jy4#~>EqBGV=S?(X`lEUd+K%ZN5V;Y({6CD5&*~R z9dcKw_e6K~QM=;*toLGOARfP2SnvRq55zpeUc$ie0;I}2%52GB`|4(BrdS_Ia4B6m z6+b=n4>7s$*mxRuDQNX+P%S3ixd-!GvviCUga$O!y?Zj{LU6?A@FgC2LyG?egm4x8 zt4yX%^BF+2d{Y#73zThR^C*CXVpd*t*C%^~GV7(%?^z{TO3B*T)8uR0jMHxrUWYZ# z7_&JgTOgX}C~g>{fX>>rl7r$=14KWy7!sxNz1sc3ryD1j4$hv7>FV%r>=g32N&{T7 z5SN~eLIGCi_)_Xmaku9>`zekU3I6#BmqYqf*|5;JUI9Z4-!6tIhJus{CCkiu*V3XN^oY8GxzfaEwtdkk*2>Mz#S_#EEfVm;|}_j2QIZy z^iyO;6HfA?kNh5S0LeYMFT4cHB$qPovIncH5Wrq=yvtYjxvFlBD%3!I%0T#_m8mMn z+2da8))IDeet~@bZg++HZu5gLgdNYyI#(%h$6IvwCSU>+Gh2O4sa{r}CNKu&<*4nR zKG+*QJuS5@5vB-j%m zQ0o}`n%{Y*c}MIqC+8n{*sZ1GwUo=(uUF32z32<)fW;FouX5?VbJ^lyJrB4iverSu zC_MX3xV%@QI@W;Ku+N9-BQspER>97kWZPr=)!98oeMh!w~WGP-47(HRnhe^%8@Pk_-$5%MIWnXCA(96~{4D|bYO@K0ENa7I*bh4WF z^Wf0QY$VdU`5UTefucr6M#-L?_12A(*I(SJlVGp5>`u33vQmXewMaB1)MQ`E+*LnT zfYRPJ|Ndh*CbOV{;l-baLB1)ODT|m%L>{p*x(aJEBE3-w@7zru zmp>eocI%_ehF+dFx=rCz98gb?BG`_-G8?q_1z*`@kg+r2;_>HX|tY2#u{7JLL2eVB0J6PH8uxE}F9sv{XcF%9uXecMjBO^?GaZX$LQ!-W`SO$&Pu-&NAWec^9ss+cxrK^{T;NGjU)8J<*U7Z9IESPZp`^Dfc zMu{_eW0=jgeGyDW&~YX~jwuQ_79D64b)Cd|u8&IOTlC~y^Yz-qprEiIr;qr8LbTL)Y(9peQ74})SG zUF-LodV;mwZcMu6v`1KzyZ1AyTIcUwkMdtxUVaBafKBdS+U*5pE+h`hakxONT(e!+my?_!|x6*VsUHL!lYzH z`sU~S%+1o^nMXr`A3gEmS5BGWnU#F*w`s;G55*53T?w&sIsgG%vzJLGLiMjaL zDB1c1^iY`r4lF{6CZ?>v5$7=RA*BflXDdY!Uo@L6&q6}8fJ*~(79sc}oIaen3#+L) z=G!0(Pm|JE+SkyqtISK#85tebCT9$FeukdMtikI}h|D{$P8=V^mXeU$P~N|9pQI*S zn=_#0HxiYjFK-No3D`U(Buai*WXSaS&32r4X>;H8>p{^nS0DcV{>^>BXhE9us>W(z z4~P8w+76*83)d$7^z6ab-KDm7=r(&=k&kX8p$>sBg4E(V`tIJpuPTj1uil82g85y# zL^Z`rcQ`9{(?w5GRoj;S>V3}o43pbL22KyRYitO)`kI1GBvaak6z3KsMGwtwJep-u zRqxJ90N^=IDkJlrT?kj4cJoWe41eNhpGp5xY4n|a(s?F|{qL9GHdRGARl?XqS5B*E<$92}hRgs>&{lTLaT$&ulTL)-quukjG1w7caKcXQpq zn7J>6E)e%Wmui1LPAayNUmxxNyV6}zE&c~)WpJ5{>GE%K0?@wXfsVBEBE?!%WR{^6QdSFp{XXX(-I$?ca7iU(|pas zmhGAPT>w=rRU^(h&(6-&^)*{Qb7&Uu{HE?tS-Jjfy7^BK(b3UK9$mfbuCds~xJgi^ zBe<{ULx(?a8(X|bz-QfaUzD~gryfq%=$WnTD#eS^Z%f7Mnh{Yx+%n%j*3Z9Bl{dCQ zw#4X-T45PpdPy#*SNxbb0y(t4Fm4g8^5D8`kTUvf!$*$olXGTsrXd1w)AT^Wlk4j6 z6NKHm9X$2Y(0|b1SR-80_31T3E5$eb2hVF6IjXm1aEvol!=ne!WPU`@hlihfOSPn% z+{LKqY86}RN%^7#X--d%TgJu?+~?~~-1*Gy_Ck~lLsq&_=cV;5-~%wQa<bn zy}ZYi{HgtiVvD4JRclQw2S#=1#JA4BmMt{Wb{LS9N%s+V?THibU`X!YzhBn_IwM0` z*`=d1oG+oN{+VL+<+0ByO&)}aOdL_27{uihee!*#_s1d+-Cg^oea~j?Q95sUvtRtW zvNAH*^6V0(hEgbr>>I5>GS_K$zpFw#;$})}oydGU&0Wf>zs&R8OyH-RB*J%1if_@; z{xKX{c?gI7MK$YJ|`X1|HZsPfqOr)B~{$~A*U-Ew+E(I)WO2SSfWtj3?Po)c1Q z<%iwq5(=Szy}V;@-u00=nJUl+b*(YrLD{t>8Z@%A(u5zv~E z%W?O^LoACjh9<`z9Rn#!`u3}Fby6j9eSO{2n*leMCEPjb5erDD z)=|Cr%R!(%D;54Sp*hYfD^=qxtvchKPk4xTTA&{E-Pi9?1HPIuJs;yRK%ERwQ=;>;OG=wPk}RrmJLtNwbs!u2|=EWrvrI18Sz!7b=B z115(!IJve@dZJ8N_obC*W+7fl`Ui@sGz7DK!XyM2p{xS4c#xA@#yDy;VA2|^Ly>?M z)Avp!6JHc?OJSK=%kbkEgFf$Bx+o$AS^PJd{P(cxS1_G?Z!+JR`~AULokU)BOgwlf z^AuJR3LEkG?SApLfb^u@K*Wo*Jm8*?Qz;vkZ6d->#-Y&>HbV!>^O4Y&9=V|OV@BL3 z0||TIgq621@t|%6`h2AZ`{AbPS9bD8;IOe9$o`#Wj_1QV)u@)f+3X3KO`E%mzeuR4 z?8I!AM?u8a?Xk3Z*x4Y=4hLF_is8XQjO4FhzXthcQ3I!L_3?V8j`zq9k*t0Fci4x# z!D?Ivwf6LML62!$^0vn-Ez6}wT(5hsrMdOp@gK?jE~S)0GDkAY6P_w?m|q#eb?{dN zon2kv8SKfBFd6uu^a;H%`M^!wRV69rhPoq&dWQDGM4p=%O`GSMa8}$u$L_~>vUHhJ z_0koMigD0|9Q!=I?y~>xalj*;zEnK@<)B4*TqqY7@HgIAcD9)K7VmpNvK$yv_H-LiifEVu_9Z8y zvsU95=qMR!x=RPGR?>H?XZ5w)ga~}PJ{23#7$&JJLZz2J>=dL6B=(J9heTFBq-c#~ z776Hva`I6v&Jb)m;MK5pPA9(|x#+!Tvw=ytofF{UeF|a?iq2wypzr8;vV{~;qH?AD zGDDMuE+POvS-i4^@a9e&)8AvQ>T5nbLV@+kM-(}Eo?f1B3)C?^?+ye!o4wheR`4wp zx7J~Q@&}MaTX;TDbYF!aU}Hl=H@2LV=(~1Ud+C1U${DHIr?MjBkH67Lu9Pp)31=Oc zDR_9R=+VKO-6X#eKY?zG+7K2VH8F0eVY#qR5}(uECg!3{Y;0VIj(U*)HvqeEm+FIDk@ zM!?h&qp}3oHU`Bpy?eLaroHFLx4E+u5Mmj9&4~-zIYIAwSJn)lBVIs`-A7PL%|PRu zH&nsmJWG%@k@Q#OX!L|66*}T>Rt|{#z9%u5^WOIT$ zvMH0VJNSbJdH21|J{Y;dCx&@BPb7Tr-n=qsm|9xi6iZE>EuO zo_-WF@O|AVvevPsn&Hf(SMBju27vC^k${hdIXCj$Wi7jH9{(g)g?K}m&Jjn(WZ>~gnYtQQ33m@6 zsr@}SL4fZOg-ydoKrjwpc#6SO^Q=q6X?rczcqi;N=$M~Kr*tI6l4s~Q+Q<*ncyjTk z5{SAI&ors2sLq@%5($3A2QZ2E4<_0E4@^>Gn?*O*-QE<;{Ura??8-{!bHExH z92^i5Vi}i}5;!X@h}6{$r4O6J)(wVYuLl~S~|6~jAAS&xdHdFH! z>+bi%HMX$6cHEZay8pG{^Gm!DFNBdl^#?6?mmjAFe=%-gwp>=O+2_jtsaqfrprNwI zLH=uIls}t@a*P!&yvYCC*jJH*L%Vwf)@=5wi*oB2yTss2=|LxY%h-T{WQkgPw9g_#!*OBty?$eS!QM0r!P@nt;w~tQDU6o58iLV)t1R zb=Pb(_}}x(<6`+`-I`m1$~l1bOxl|f_KMcZ2#hQzXTUwB;qu30tpPYn5mZpujkIdQ zQT=*F^Cj8)EO(8=a8)GERByxT@k{6Mr~AWKu2tBlU2=K-&53SwLE3`tG(^1miv)~X zm*n-|B{0Rs`DSZ|B2^Rf#kTkhi(w-&B$wI$gSVW)em}Zpof-^HindRwo=Zs0toJkF zq&RbHwHCqDjP;~sUf0yx^ku$ugMGtY>rPrHvpO$CB{qj2iX5J&A?XvX|giSptr&~#vy8z@q7ulqia^F zi`97EFz#=t1y}Wr+vL9EY~=-mT*?ARE{|s?PuHOl&uI<)^=*LL==$8Ku!>g-&z8{A z^Fv&aBK4eFtPfsFVemMe_ghdlD?lkhv)SI+592Q*56$-SC?t9|=-~Y|`#@S`VLtv5 ze_*h}_0Z>ZjhyVAZ`;Xco?b#D!@;iUEKJi@*<}!&c8UyX zmu9a3s*5xIIIl@ewC;I*hO}@@X!r3ou~^U*pW{VArQGJ{fx|et(k72OdNe`~sNg@5 z9mFyxe-%kfA&xvU)p$Z-4Z)`{H8V8+_hSlQA`<4Zj19@& zjaCcgUCW9r+s_C+4+AJ&|9Tb3w`G?=Zocs82i@yABLSx`7occ1&kjV1Sm%|4x$+s` z1{STTs{W^?s`z6}Qn7gSlYK(LmnprzTjVfpUN}e?qG7bw@LXQ#f0AjBp?x%dF%pwP zP8Q^d#=s8w+*?fPn$0pg$E0B`D&woA#2Ps~s$4m1LSb zi^APK_q#hj0eXOzt`hR};VtyC<*KUs`cZ1BIJgO`=rq&23b&9>KD(xth8zM?^+w%_ zsRsAgyOB*cBoP603fYtWcKqgAIDZzCoJrc6Px)@{?lehu_V)wEVej=-wDl|%c*7Cs ztebY1N*KJICN8;pRi`IcyZ?H=OiUEr-)v!jP5pw};NCYRIq>Zo|51%eaX- zE+Ktm@*nvMiDZkI{U;E^RR;wgO*4WX42K(oI8gZ6m@nh$gne>) zs!RFrMS|;Xrrz3w@FOPp^;LbXS^6-%bj>vm##Adr8f?VXUo*4^wd-&4_2$9$W*R?~ zHdv_q(!0P^=kjfXG872lN~l9K%@;`W(pB*x4CTZ-RukeNg)6kR7KHqNn=M>r2tc`8 zRCFUOKS5uetMh}9FVv6INzZ58m0jHB8^n^BMnVteh$pJknL-aQu}pWa0^N<$|1xjg zcdBPX)hu5wyeOkMc1dkR$otnc{A++qAPq3s!&CrifF*LB&V=%*-06=qk&%%J_?;O6 zlvG|kzD7Hp6Qy!e-X5vlXVV*cX0ZRG;49FZT$m>+Rxu52r`RGNtf1#gI_mwTANSYp zN07n#ufIVQIqX&%ceMUpVrYKvYG|J7S37!db+g5c_w-c~)w8^iqMF}HY|=yA28c@H ztCcW{EPi)YNt9#}9}^e+e6Y9?4Z8u9HiJ1Im(RAD#bY&bm$Nx-nJm_1LIirpi^1Jp zy^1UCq%Ji@wXM1uBl6Tc+LWc=_P#h-?wq_`^j_kWzGub1eMf7Hpl2kw7OrI@QP+o* zY9Q-`D=iqa#W}bmwO{wvRa#;aW(Orq5j3J2+-%+jd!N#xKd(kEh1%wUQuq1D`9<3b zrd0I)d@BRCvQ6fsp}XQ|iXb#1-&~dB`B3FVXt#smlvK=xgdOkgmNrYa56t{x4Uyf) zajWJy8r3h$t0nv+&IHKc^CPewJ91o_HReB}pM7)lxA!oTbl@(( zMd}YaV*KsXu3cAS1)ouirZ}w6ifl%|!W;rcM^}BulAZFy6oJn$mV`@fi~N=Sgmpb0 za&Y9@%F3Z1o$N?o=NSMtR=+k`TB_q0D%B{hoDCESjf=<^wbXcYkpk=Uv)OPrzu*b? znW2n=eb2;`j*{SRX`%_ZCJDN$a-W-eDpG86rf6E#D~aWCn?xGgNY^;V*@s3gmMAAy z4Tl6MOk(yE;bkkA-wFi_?i3NT7zBI6dkPa3v_MsgCR!lB=W}QPRLx#0=w!w`nwWtF z{^^*d{rK6EA4oMe5k4jtSf&MtSH~)?O)p`tg;BJp6sEjS==l(TX(7{CuL(?64qNzE z)WO8)05&|*3jk1w;ZR;$#LTR)r~;pmpN5Ks;@C;ZTyX}#S%kvx^azwj1ox5vV zv~54pXPDs?R=tC98^M^3G0^y?nP<81wR9B3!NS8g(%vFabeV~|eyltrYaSuA&67MHT(NJCZixhfB=AdE?luw%M zSMy5rDy{o{_9A)a78Wtt&B3~)3*-QFmeed4BAUglevzl$hNYtIn@;^vS2b|>1I_s9 zv}VnhQbO|_kcx-vdzCIf)1y|q>dMF)ye`qW$zPe5=jUDL?B1bW8gqSpys!#aEkwkju!JN5@;;xI)pt8TcP6*{AJ-BtI7Bo)sA{^FM&q5zs84r`FjXH$K<tMKZQCygPwm6Raw+HKln;4o1GVWvgs{It{!#DMA&xw76TgI=Xvs!1mP#;2>= z(|WH9TR3>Qb|sjHN87Gw=$pgC7g%^CzxLQtugOJ1OG%bDe_Uz-zOkpe%V75^lak$n z#&JeK(`2xxgnvl#gye4#!323oekRY~_=SW5z4LN?fPZ{a675=YtS^u)!-+J%yhh^P zo{uqQz+sk~O){J@dKFG=;M&^XypfWRwg!&xnXWxn>Xuf`saA<-%a^zJk)cJs;sCc^UoNA!D~(W;j(IOt3$qc2}+=RL!mUzZ)hM4xh;bkS^hRpRl##5s`lfRhe^ zEvg)j3iVRgs1SIm`ynTKd;7Y>BVxR1s7`KlpJT+;+`|5O(_7QsdpI-&(XjzF*FW%G zC@A8gffz69<$oKb4I(~WeLr)vZL1V3?>OTLB^yx@I$!4daC>HYzj~xA`X$Jt4tB+= zT44KtaCLu_fdAsvYY*QGabVT#Y|!CQ<*0qCu%`t<6qN{|&QW5!VY~GzJE(jAB=5-x3DF#05Z5t4=v_M^mMSJn z^(e*Cvr+&Tzo6PyS1a@0IP0OfJ0JR=VD0Pkl9Q8*`&y62I6i!`#mN0oDYzaS&PXC~ zOKlz5M%3Cm9pyJ8M2|Hpa3ULsfM_rkDG{%_J5EH|acFm6)5Ur}61qE1`*F(XEbLna zmp+(KuRIgzAF0b85CHXHA<~cpQG+g<-{Xr74kF_l=Af#}Ju_z4WMq?QQ@x(hI081) z&vAdjT+ds$i)v>pCtc=H{b zx789knbhmSRHq*IVxh}oUxM@P<$w5O>VhYKT+@xXKeUo^Uxflh63+kY&A{FK)et8Xq4hdDXr7c%te zhbO-0kq&?ihg)E4Xj($N+5bSPJMy|2P`j+^>X(4on=j?iQY_H;(_!;@NK225EAcdt z197Ye=$;Q@>H09MQVs+8SY_k+N5By!S>4Guj4n4s+TKpXf$wdF^OrBK?D3P1MR+*5 zIhUh}%w1@l^m{=lld$lRx1n$M1_slK#s&vQI8*SJm{2t7&fO#)-p2jKsgV;l<5#1y z6Ll!7Lf+_^#r@;Rf$Wg@dN5p591)#Ef~dclT^H+#hKlN(&HlXHdK7ca-&gl<*OX63%zHb>XSscGBk?x<(s;sbJ0~geOQ@A- zjwj)00r?6+BhA7{2_iTZ>e6#R(r#Khsbwxs;#~X|8TU7UVzF_oB7}Jt5Hch!Z0IQr z{^k62I&7*Oyn+6HQ8E94^f3TRre)1%-_|w@@mO>%d9WM4KPbu{MlB1eB#!T7dSFOM zILZJJ3P!xGeoy5(z3w<%0zw}8=hIlcX?$~7d@2s)o6cJeIy>$6t4u^YRs12uoFo+k zuf^S%)0ylp5xToCyCB#_#Byx~=s!fdh&BR>gp!7?-xVdKp=GMFR$yzM^Lk<34)g@o zh+%>BsL{41zq5fiJhILmZj8F;dKc;UT8o^)-kFNCWcN%oF%5KaK;%#LAke&ztZVfwjkn23M(a8hT5Oa-@43r~G|L z%*skzbZg!6gmP@?x)9Q&ifS9w89?cBqF^|S-^z|UTV7ki1_b6i@JOQ(`7<#eR`h&V zo(Xry7tZQ?cC#OzjAwlO?)b2j*w4Iq9^ApJcmA(?5<6e<*1DVx1DZq(ckH!LLPytc zh(3`btz89em@2Vz5V}hHYuUF3H7Tox#+p9+od>L^eM7~7(I(CH*ER7=kRU>LIXlr} z*+|jK>%g66es~FBNQLw&jqTb+dZ{*SFcH0XkL8G*UOL4pq7#>|V}wb`hAyS#Fw43X_yChGMy z6w-P4Y~@c#0_g{RkJEpA3?ThwlNP>e5;6*sh@~9I@>8aUtK%L^xxND1$0!rZvT_0d zYz2iSoWHKlug`%JK+z}}NZsl~zQ}b)ba6;#lx&R|>ePC@rJ2TbnnP;x9TG$m3(yALq-r*tSdzIz+`Mf^_Mj?;f z4IkNi3nW8O`GOqVN#Cp+O>x%T3<|f2nZ1J#>D%4cxwkz(>8Ky<5w!=d6558m)yctX zw(E8AC4sOdmRP_-K!|iPj4ZdGN=SyjkhO`-h?v3dP{Nn(@A7f?6xo7O?>*oMQkNaS z`|yle1)bV?gw48q$X7Lj)v)gDWP+j(3Bflgt*D%j4sNnt0zxd!QNa*a7qcJ z(4wty(v@Y`ZEy%IO91mG{u11wUn|Zw%@Kr2+g?8neJwj^m{A<@2rfv!_=7u|uAAS5 z=#bfAQ9SkN%Lb@9=IgQucuXOTN=B<|p?`nU^t5sKM6Kh{-}b&{W_5sJGMLhuloO2` zjTlKC>3B?Zif2y4PQsF0o4-JKn$|N4q=M?33c14(<`n!BpRda9B2RXk#2VL?$* z{SiuUMIkR)fhi#vJAW_n&asmB2*?b-M*^foC6f9Ym!9uO5wv}rNT~ROG)VcacPTt; z?TCp4JeNvHY>h!_7I2=Cz`{0*(Q{&kVR`Fns(haza-1|7RfpH&fEQW_Mx6>BdL9c@ z$n>tnvP`Plza;Uu(7Dn>%Q>9$PYgV84b?%6^;G$&lzhv+o1Rnsfy8K@l^7{s>^Cd; zi%he-rG^az|73i#+3#Bv5%esuc&!)$_ZVY8>bC4e4}JB73?KM^1WV*?df6S6h?F<3 zY`wsa@q+j-=6<*g>Qr=eQd6IT<^`ne>{vlEQcPS6zm&3<-{Xq$4MW3=IJ($t5pvuP zaaLd$Rjj4jfD$IMSF01mL#{zr!RtZgij!w?=$X*I4kD3j(#qTK1z05-d@_Ns0Gsk& zyK4NT@x-{{FWm_+hwPru6^e++xy^aLB)r%tOf&)nUFJ!{ac>$IP_xX<4UH4Y%18t0 z%#8ZWdHtZ1Y)!#NiB8gYfnUGOqt8%Xy1tU%%7itWsawhatXM`?Zv*aj1xYgxc!V3` zL<1@2K~5*9PnqT9#V*IiVV=sw!_U%2^(%jX^orJ{luF8>Fv|BbMB>}du+@-4l z)G?H_mizAu5$(3tR}&KohPCw8uakJ=onKO?GHKZp+oxL*Vl7tiW5=XZW8vj6Z!=N$ z)vU0&&T3m(l~B6RwkfcH_w1_Q5AO^`HVbzKlMvYJdc_9J^qD5D^R2|_=tn+T_*Clr zF{!~eSFbjJx>3hx-L9&hqh%PlCc=|p{ks-!sNTpjY&z-%b_4(OSqlZOdz&c)a?WUs z;0NIACvjcA%MWT9had2YKiN;L>hCI2Wc}DDZvvvl=L{{Iw4Lcq&#a+3-hr?(gNeU5 zqH@l0EA{VP$F;});JBc0H8L!TW*zzeJ(rh|$c0Bv_3NynK#$p7dwzt!dCV-4!dzf= zmo|)PC7FHTeCzZIpwX!(5S^U08ti}Gb)b>CgT=>ao?!j*LriMwYZjJXFft!_cjq1* z{P9*OIXc?+&!3M##EY)9qU+;BCVfw@SQ0oz99@v6`G$w_Wm&1NF(=q!&_Y z>)aR4oMzVkl5EXn%md_i*X?sm^g8i8!)2LYe@GQdtBw991sTaOAv@ZqR5IT#lz<-Q z-csSi#NbeYktPCwY)1;bK`XSpT9B==_4x+wwehHx}l z{+I(}*B*aSGO?0GeXfE;wmgP0Zs&2x9z1`uKSWR5@^K?O#6N-?xG-#22n0!l2_+_n zwmE%UVStF``UIXr6Y%EQW48Q(wUMkkEe>x+{YXJe007Oi# z6z0KSX#&IVwl0-bR06>;G)idbp35hSYwF`bp*Ow%FM#02-cs7EsxM^2Y7}`ln=SEo znn}}ty8gL=4*SK-==!B>2(#U=O$jJLh0 zWgAmvNku4x1NK~Y+m64iw$93oWLB)Z{tF`%)@`(7yB339=<|Ar{>k~&$Z5Af46N<( zu))dor54)$#=O5&*}Lm#eioN=IelcjClhLOi~ZB~k`1hBN&>)5Yw;FhG1q_6GAJ3+ zq8vv+(pLs9A3$P=(}^!rlv$Q$7nG2)y;GS&M09Kcbp~#S)O2d9RXH}B?^3w5@%g`3 zA)y!JA4!06j1#ey#U|_%2wTg6-U%yyK@M1XwXy1oS&L6HQ-jYSuGOb_RHhb?#N_n< z0{6K(5Pd(f8WTlgCM1%pm-c7o+h4(;qlz@%%BdEM0x8@u14Aw8C>WTA(e zjrPQo*tpz-Sb@O%gc^QceFZb026q<;x@*OThe3nC5IY#DQUoTKEeaTg)ik0SKH9E$ zxk@;K4+|FU)Cvrls%5nT@;SlR0=b^#MKI1yPZZxJtY!5uET4#y&3i>(hJ;AtLWsZt z4J&gEGn?zK-M6r?)2I7mV)=}1NOtNA`{2&Q?RmwQ-zZo$gl;SsN=X8X<@&IoXm4JA ztUZF$z~?vT>5)_Rxu6eik%=$=Nuy}`_o|lhalrP`9XA_;keXGN_d^Q*f@QVcje@8I z!d@Sa!RK>^F1FmBZp35p_3!8VHXD*qvY>| z;Cl_HjS1uZs{Oq^L1!IzD=$HTuR|N1f7qJur-{FJ^blg)A)ewQxLvKoQNj>M@QHo7?pE?juQQh@#bGDbbz;-}-+Qch?Jx3eE(mYPz~2W6U1Ch&E8 z@z0-&-Z;-L&VQ9X=~y%w)>X4s$cdCll*VGGd#o?20AfqMMPwsSPTjzJK{ycmmJ-um z{#BX{jZUO}tfi%n=@eXK8+qV=9(2DN8ks2stDvl`9QJ&F&Fy+guJ`4-<3~YZaX?kS z{5GO|MyJK|1BwrbU)=ttr8sZqeIqMjDF3w;2RL6MdFywVqK+!@d#50%E~XVT`z6J` zF%51vXIKK6^ryO9cz<;#)ge5lR1fC6Ptqa%j#5F7LptgPBIQgnmaKMqv3sY~8^MGy zF_V*pB$7sRWt5$9ImUBZHmJIPKvXW_&upV;8!{X#-qPg*A4uDZ7X;)JtoD3t7K?jC3YxO*-L>mDmmP-O@Dmr-; z%p;5uVQPdhc;9srm!Q1oWcFokFRnq?Dn{rjNf_QP?DoW`p%gcz(o3` zN9@`#%D4B?9!CPz+n=|i3T#Y8wB{1+v9|8sxDut9tk4u;{;GeWp?*EQY}kA?r+#C( z;rudT??BVX1T(!Ar?{4GHR9)UXHSc8)-N&4O3Pk9jxLCj`U4CWP@GG4V=4=(`8zcl zcZjOrvo2h?;E5w(1hPv{mZ~cNs&t2J^^R%Nt_S45*#sZEWrBrP+~DDZ_ARaZviz*b zu;hf@a7@lnRiem!x(F1NYierH263>>R5B`=mBSF#POi`2Df+fevD(`vd@CZtigCgp zV6codBYz23a%=jC2Uxuygw;EGh^loDsgh^JQKZ3P$}9}Qzuju!#%ETc-v2ORnkxb8 z(S}r~q+aw(+Dr4%6`2dc5l+L_TM*zchXJ~HtL|2FrOsnOTR|Av)79(G?TX-6;wpo_ zsGZ3NJl!2qx2M#8X%~8*yn6+3_j~m9JB>A?uJ>ClSBJOfAQ}|$vF?yf;;ygqwZl{} zR2}0z%2$)K@sBtmVeBtLwVF&rDb;q72=A5A)*A0KeJcAM8+01Hcr*J*G`MIQc|p`h zb%Qpi;q&tbg);4}33|T#QX|k87~@C{a1J!uGs^nU9cZXm|fBy8N=K1$kd{@281A$ztH=O?iY1M7L zq(<#Wg|yBc;yQ!t7z6s(77Kl``O&j$SCbragzSK(z&#`nu4eNBdyhiS$W*@5CHT+$ z6T7UMb17xCO9NnF@n*2`_bzCW9QtWylk(f*Ai-B{c(5B|oG%MEu8lN44wwNtbVtn! zc!B3ZkH>!e__L^@a_F7ZJ3^{R$Ntk_%DewY;BuKD6&dgL=B9n7r*ST9;CXLzc<|mb#t{{s@nB0Fc7U-;Y8a^=}?(@ci6S)t;8*S5KMPdwUCAwGDkNg!yv8F8aoHi~Y~o zxQHSCswJ7NtuT1IlaP>M5zt^ufeC~E`ucWNDC+_yghWiw+Hm($I;?}c5GSnhc_y_?U7ZNdKcR`dX&K~*ngZO%l)P zG1jjv>HYYqENBu7xnk!D#%azY54NLxLqja*V%*`DOC{7JeJp3cC8UB>y(+qbnwEO- zIsW_8qk*YUsmbBBD$43nZIAcF%@@~OJPAG+_yd;(1;5P4Wl9ot+q!qQAEr#is??ynh(M^?Hi}aQ?Ar znzt^mRyf$WKe{nyzuAZ|hiC*y^PF_&qOvsZa}2K#tV&oZW)lx0+y~giKBO}+idk7P z0Vw0&T;&9`42p`1?U>q#R2VxZAN9=>ejf_*NR2w-`2^`lAqf`oR0}e}_>;7%fWU0y z>4XGw|Lwj?pb!0ZbXy)R5ij2Kgi=XCR^y7IsI3XlpNgv4x!j2OPtEStsv_C^>1foR z!|l6aHr@Nq3DG+JjTEK-oAH4iFkS#h=8jUo554L#uro@h2V1IGhxxg<&r0A4=4AG~ zJ)W@V{-pC6*8R+vZc`*x9jL7&y}-H7{nlOgKf4VA_OIP+R2uyEQ_+dv{u7AYQqjOP zV58xH_9b+CO_(M=EgA;;Z_ve)dbcF@UG_xWmG^TXPr?U(Ey9?OxgP&7h&NS+9%{-w z`^?j)GJoV%zqVWm0BSJNW}^dH!dnS>S@)FO(}xk3y9ZNJ!W8M@QPahfRaj3ioRn>B znDJ2j~J=(_l}pUf>_Ix1g))Cqcix% zO97AxwGCa}N*pF74dsC9UnHAp3=%qCg4MaA8ZJr20;My;_xQ`0PY915j$P;hWj&8C zdUNh^(@S(~3si!q26hTToFpbKBlHU?Y;Z|TT8xovOE5wmB6bY-U>_em8*O&e+XO!m z~~gU=R(fuSpx2yLrn($dHk>#g*x3H}*erXn&Tr1E^*Tg0%?uJE2O^gSe>4oMDWfl|76HS1qrF5d?9(q9B8W32MJA=P;syV?vlP z-jy)makOv_V(NXVxyK5kYbl=5Zh$^W8oD@1Imf!C)VsrK{^<4f2BExLv%^cx*wcyg zl?T?I$Zt}6(>fHD5@5n~N9Fu3I>h*w{TJ;imH!ivl12m%voL(oa}zhwH~c-r#iuF{ z;ir4AH+(^X6!ck6sSES)@Lv}0eJXfMPs9jyZ|X0lrb`NnI=|YtEWqE>)F2F&29O3}y)=TXdLJ5)?0af#(a zPkqY1c){!@o4O-k{+RZ~?CKu`Yz1Zq{9qb;X?}MtS`_vz$c=!B@ck(qWVE6P-9~>n zIHM;Ja|){csH1G~saCSc)2m)QcYNs~+2cFpu|@PTyy*a;CKxcIb3J~?1n~=czZ{Gq zAjhPHEu>JhHooVc>v@gMiASGu@0QJv^dZKmLpzQ69KBcYdS`|> zQ#h1Fm-$L(!wt9`c$M+Ji%Uc2){#T($YPCy`(5d2${7=);Qc()2B?6vlW@32{s8a# zp4X|WJe@~CY_7i4vqz_{^Ug^J8~!4a9XM~U6mqu@8hg1bS;0;)2pnOgT;Kkdcs{kD z^bNzrh=U!+lmgexU>f2Qq7oHZ&StS$uKyYkz>YyE@CpV)tndEg{4#sB=@f&&(I|00 z5Y}}iEy3lsukWvRq?1}7VuH1!E`yHYbWg{yz-Lkt^d2C&ETgq?Fy!_@lanx%vG0+I zE(Fu1FJC56+#lSkrORh3^N75Q<O6UAMtr&mvm zj6M%qfdGLSqc@orp&XJ@@BElwzh)Buhy8u$O#i*dT0l&UWn%+`D-=ey#>p2mIJRD= zj&QG6c5GNjnxKpqd6zS4m-qdUz=YXvA_1Q3G1a^YnuFvfB#ZX`a06jw+|=s6m)Sb-=wwA8c*9NQ`*7AsbCxYt4ACHE}~tW5=L2~e$Aic zY#{kiF!ca_CoUOB*>x>*Z9_1~(#Pf&b#bc1dGY`1Kv;OL%Ku*0YwMyOFgW7d$$o2E zLT2~)A@8TPi*-Dkm(QD5BaLPK95qp?&00B?iwbF+q^!n?3)}0G=u*Gz2W;)zCteu+ zBP*!xc8*<^*7w%Bn1Um4zZ5ARZxlD{jlZ>p(xf`=*(!d5Wn87KX(Ku zd>ZZE<;oelNc_?H&m%c z>C~AblTh*uIhtsBY4E1K=3a6n++7qzkudjUefAQ#zYQ+xkxyYKM!gcoFn?oJ_dsy- z8l<_%taYcb5BgT6RQ`M)>pML%+Ty&jalan9^vfFJXcCYb<@)gBhi!=C=?ofcGa zo7*WgRVh>t{YH=uDhOj9Jm3`+1@^{XD|x|VEkZ+PMp^-!rv9VU7Z*i5*fFTuDz~~C znwmvVk4b>b|2$TJPQe#mlaz2i&NH{{+(Qp?&8>QwZa0pt!}5ZJ2!)E1E&dw2=9m@) zgp0j(+~WQVDuK~8&Kl}VB=AdII+Mu30(R1L3A^^CJh%rzn(kU}L3q!ES+It2W&&w)b6R+=*B0UIh-$Pg!~VG%)ew#*FM-vTzO= zBorE~d#ffck@36Dlcmppm#`-}lgMq*cfhhf9S3#1BiU^a)@i!$p}{!r_SW$N+w^^A z(j*J!$h2A7=_a$i5Y!%X0r7&e#uW^o|MziH6=BejhAh6j6E(;UD`=R=k00g!xjlM* zj>xA6os;RbhbC}|U^{Wh^(A=N5eYtT80N^W@7%x-gs;KKd!#V@7a z+8BIi+=UQM%8w@@p_vrZy_!u6CMeYfok3`4jHpE(Q7U$9i9OQ?$>dw;KB76XMZ%28 zBE8e-adKfC`bJ@4^NW<-f15TO`y*k|tij*-x*5*6dy(+Jyrq3@IntyGm<W=zSFC+W$~rQZ3~> zeh>9*9MjO#n>5#XPsRsF&^X`;_tqra1A#*8J(+;G^F)#<0m6_peAhiBWY zUXp9?;pzm3{RDI#b^zs@tYCRXjE0kw$f;Xa)7Q`SfqNl=-7Mtlt*KX`;$hXw(;dq< zs1y`fpIQ~YbD^TD-O%4AtiS`#765s`#5A?zfa`ksa;(3#~n7KnP62z$>8iL zXuLruNC@L)PV-*{uzl&3@>jk?5aGgM*NS6C!>!r;NXnv5AcDhzKNtN>muj%!`;IFZ zD%2mWEQ>c9s!;l%kW}kIZe8#42K=k zIe-v2muGSewTIvk%_&TGF|U0890GCu{h^k0qY2Y{QZR2*;_8N4HoWau*j8Y^;3sDQ zFWs6wLa94eN0By*8jHQx9;@<=mIo`pOBg8%viwyVsv%3DR^P2igRp~K*Z1v^+B~`q3V3;5$pjZ~Em za8%S(ay@D@lLS3GTlgSgkrX4xMtM323@{@9pjv?3(H&ibX-c~4r|Ou1sk$ZBc*Mr2 ze*&>z^Ay~F!+WFcv)Q=g?2}QKfwrmLy8woTj61z>oJlJB3A6^atRJe}+Lfq+DaC!6 zU5Iufeio#6W5m(DX4qj7QKmt9O;`D=6l^^(^d_{BPka~ptti6MJq(A;6|@%u(w)Xw z8oiPTa}?QK*xc=uo}PaEN3b8`FG*;>o=96HLXWlpz1f@);zRt3?wbbJZL3CXsq=z_ zxHQeQ)Q2;y8-OE_B^UpmvjLq*p1-&_HorNLlmFf;M`x#z^lO{G_3n(2kLdax+1z8O zX?_)n8;9cc)Bev7aa+iE zn!~TzqO+`J&?6?w-;y-{OUV#EzoNR{)*D&P*1&-Z&nUFETxOf#$!Z#K@4CtVNs$h> zyGgUhOaSW~F@G8OA)-QDYACAS#jlm6qi|B-6&S4J^cbli>26MA4DLrGoGXf&7O*vhslvO`e&Zenv7+M)?dwAjQ^0rgoB6o3kRF9hvgwx(|n~A z5AqEMmkdRxriMMmGC*$rLp zgbMMPIuf#XIl(w=;X0qa(%|+dCH9GMk6|K(*6VM6wA3jai6)k?4_M^vXfGJhmJ((O z^w!{uB)wMBl|4Lk1yD<6j`|l-RATbY;XG_t?UX)&G`a|3AjUE^SZkkysYCJBFJVyw|VB#B14>$Ipen=pxh`TGQ!`^>8#rF!Y! zwyQ-Mv_gmAG5rq~v=)>n-fkpTK&a^sDynFbgp?D+Cn>6sdA%s(w`)Je$=Sza_XkU` z`X>nEh!ZF=i?^rv!$7{0Kpz-5hwxo6yd(Be@|9%I6*P8C)7eD6-%vK?AeZ_yR8lx1ls_{F$kbUyq}uf#xGsYeRqB zhRR;}5fRwp#yW7*C2(A&jgwl1s`9rD?Kv}k!o!FN80B56*CAR8nW>TG(!_F&uXA;Rc=Z51`$o0}Uj zX{@Pf?kY^ipU8iS6LTd!CNDp!Vt8%+DqB=`npqRZr2p*;fi#|dA(~%vA&Y8(NU3qX z7R)ERA-z3YgZzJ;_;M|mzJeoP#jBPms4)%RfoP@WP)6`&4rZMo8)dFyqfFQ!SidG_ zM)>9;AxjpxR;rxxU}E_q?%_dxQX7cnMXT8$N)8qKkq`rycHo{B;CB$DCHjv8vi`4J zSTle?ZtLa4h;p1 z^5x$o7G|6i2w0XHasv-zQByu~C#XB5&&#pt&zT#%TYH985=it~m_kjLmXipep>t;l zTzb9N!^kCvS=V&41to-K6Tmmy{>fTbUw^+6_YGBC@CXhOkvY;IP|rpthjYnE{HQsVzd)LBMVp>AE6P(m71 zI;8~ZhE1b{G%C_9NOyN5-Q7w`cc(OL5RjDa?vC%>o^i+b>)dh1xaS9Z@Aa-V*PPEX z(kUXsCn55kjy}$M&oIcS7z4$)m5>m?{0>UMthOzZ-@L>csf^5f(L`VzE5anp0Mjc@ z@&F^f;{6Q(3n36r{`s?_sfPn9`8|!1A#6f320dWbY6pM?$X$Q5y=_qg?_+rJz^6|y znf!WVdqE_Ff)caOnTUSDD2y=WRdjb+z(CJCLSeW*R{))2XK~AJw0T(G=?q?w&^C5( z*8)ao;<-C6(7l!T#e2}df98Ys$y3Lgq3gs2-e=%73K^3gn4$VSVN0SaOj03qQw>i~ zPv^~aCqS3v@$Q@H2`3JEiuO`>q1mONtPHs!_=$lBfatu3AuTw;Z67QO6`04Ai`ik0 zS)5>0?TnxF5A)4)@V5g2Y*;tA;)+J*K5ve#RMIn@t*K)NLfO3M6)q{Tl(x}`dG~9t zS?-ymNA|~mrk=zFDc@vvtRW_yVYxJi%;j^5#59OC!K)^Rr(mFY4_!CLp`vo45Sac# zf7o9kXzo=@M|EOo{LJ~tXYhPyM*MvG`-I;pe=@ntwXu?x3ZfG((QnZPE4JW05})!Y z?DfSS#Jx%SOO*pNJtekhp`kYK8V(yzYDuh$NC-oHSa|? z3X{O1iIHj#_|0w~vWE^*OI9#9ANVJU8h+D1t#1&RB^y$`f%l4|alX(}v!ch7c#42y z==${s{85}Ev^M+JCuHd~PB>9@B$ge?kyT32h8;iejH0!C>pU;XIfHdB)y~BfZlz|8 zdpB@4E4ugxIRXeq0XUivO9AQ9F7kv1!MM=$i4ieTSU9{wLe9^!F;%#Ki}M{=-)7hm zh{2Fckntco8~)dMHT()3aI`qSLs}1&4Nu)41wfC*#YJm&$%Hy^t8;O>)kC0yFUWA z^xk*+pAc*UPG)_^29dajp(Yu4bYEAU_P{1i~4c zA!Mk>#@iCSvT6bi!~z4~tMPIbe1MV&qF(n%26M&lM!;qJUnyEKk3ipUcol*oGk^HO z5fisfK}-Yf8G1JmOFc2y)aGk7!_ zopuM7mK8?mPqY6^v0T#ZTAx094|5vS05=fqL1wz%jr2`vSdn+|;f}6CN%`c~;rx5n zqNWCjIFC-@q5|oGSdb53>x!*u8744`E2yHjeZ#rM-Y5>M@6m2C(|VLx5NQcaMP-xg zW=mBB)r%*UOm{E8_{Tex#IK9g1Ra)TsgjAyP_bTo0`=Dw_J32Ouwmj#60py>J!rxL zzAxJG!GVEt(@6%HLvD1NV2$F|KOjI4Fu&^oP!+?gE$xjJmhc^`Ur;Ia*k|&&esM6Z z-=0iY+))zZe0|UC7eov8Z9h%ZB;Zx*8aL>(ADx#S#OD=#trKIj7K)1#^#(U}^!KS8 zd_a~qgd-o8D5jmFTeFdt7@O;FPG;ZKGZOk6^v4vPJR%wV4mE436}Jn4l}6^{5i79o zf{Vf~M$do8G{=HD?BomJJAp!R8SM^<6_~+|&eNj*Cvt*G&9PR@HT-vEolwT+0n7%G z#I6vCNffLy(_lgLJ$5vE)>+%`ByB*NIWx|+opM>cvIIE(3F+h+pnpX`AdfR8u4tJw zabGc~nFTDZ-87o$!0Kboxt~yNz_HSu%sVi6{+ogap}U!_=x_l>*heEXflrbnW36Y%H=^!a2Bm}}`xD&2vkvsO!_Ps^CbQI^_Z``uu zeASfx`g$wo;BR9QnOh#ls#ht+u_WpfZX!bA=VomV_XY{KYyeL(NhzZ-#q|(au|Xm!WQT|~e$JV`u2ou*aF>}LV z3`l!kDyioF-H?i?lqyzVZ5)mY@Xb zr6XGq7bM((zOF$ow}(T;TW}m4MnOaMd4)mBi48Ipy2CwkDPClADb1g#-w*=6u-(rm zl7tXWpgs1$RN7Q^@Jn_HAex2`13k_EhlZS%bz1Q!!n!x_1d52=fSF~FYD^fFOxtpl zuKhCCXBKVrXY)Gnty@HfW}q5EFm9x^3U5AB<0uhd12%uTvZZsL^yt-B!hEnozJP5+M=4k8XO6xGec zF4G~;NE8siji|S2DZJ9_J#<2-`)lALd&s*q;}i$&ac!TPs-Fi&zrHvL3kj<8^`2h# zfAyaqR{xVJm6$$OtE)R9k_tHbx3z71<$*I|5j#7k*foEETh{GtODT3K`;F6ve(DLBX)!^)Bi1!a4N-SW#ATKJFh z&Yo_mArJlT2W*mRIJdN%?-O1Wppe}J5hn_FIkQJi5y70LdD?FKT+KH-#d3KiYE?a~ z7QGRq^*cqyB1F_3;wM7l@8Gc>;ZdB99*#x#!}AlOjyuTTW zi89DXaWKd=@P;4AT=j&X_yd>k#fgM)$(X-#dgP1?C1L-40nBt1^@tz%Y$TF}iEiYh z%L8DtssJnjX4vyjhcY-Ab-9aU@f8xvL#-|!!NcYeL|xHf$V9Wm)hf4=LsmVRFe3@&IiE#h7N6^20qW#hMeOo)1{9Wy$qQT^13f zrOil$kmm2-mqe`Pq#LAmS;6%ksl~TSZHHmzGL_oAa^c#kpyW3_we4$OJWOkGFjZ11GvlS~CIs0d4VHC-X&z zrOf5RPj|Cl!;|_X@%3zFwZ)rAHD$ttr@qTmeWre;~37IvLlOX zlA0;Eflrwr=>33quxk%o#);#mpW}le84c5Vxq$MB!?WnlksGA`YbpEk>lE45FkVC? zY0NzWf{*rnc>b{yoW>ux5Dhqwr;wO#%*_@Ad#9yK5f8rd6NBq90?t=-gI6TQ7(zf@ z=KJFgP`}Yx3A0Ptl)vAmtnBWF?v^%T^2}Q!R#sO2Q(HcL$|(s545VUa9(heE6!P#R zKVLi_xZEO!ZozlM008Z`z(WKypwZD$U1zz0`}Z<3?9JT6Z=dzM?a52EBep$&VXvAZ zv7?KYS|p#gj}~{T0BezN5SU}p;e|rZelc7&xhO)3P_Jl8XM#b0&*^{NcXjt(qLMFJ z7?;L;+ugMJZa(0Ug0|Ev_SDcNmDjA1lDE(Sq~eQQJ;IZ@31Q%xK6;Li>y02z8Q$Ng zrx+UCM@mBu1qi5@@vM&1-5(jS?r10ljeGZJm4WXaJQo$tt^T{e6%6?u$mYQV^5xKP zexp_^BydxNsR7(>88AA5>JM#2)8hOB6iAS*c^_B@_!kKyf=OYLyZ15cr_>kIUhq*DD4g#*lRUJm1)0UvGAb9I+<#9i zdrf7lkm+%eJb6keI8ksWorEc%EDm zm)WP(w^%d?ybe1b-l$ijkn;1E?Px10`S96o=$`LP)^Bx28HWw0 zg}3EofN}n4E%2<-R{Si2d3)^5JL8a{rZCWvBIAzVo&`(+jX%>mmy&+!RVlfCoQn15 zE$Hf61M|?J>VXr3a_v56H}mx!&pD_Osj{&y6DiYruf)%+|By5{x)w?&*$&Jv60XFm zdkFzcY3IMkMH5_0&__Cx9I05P$fAA(D5U1G<8gGd#@Sz)^0iZ8*bRk&q0CxkdnG^# z1PK>gWDTm{Y5%6doTj&hM2Vi7P1&j7VF4MybRBFB|q@w8kx=w*T?Uz0G+kdr8>Q@1p^6A|kB87BrW+n~qAgp;} z0K4KGTFk=muQr>4!-T?~5Hh#DicEtbeQ;;@>hGavQ0;GkNgm1+UM#E-r<`J`8*g&5 zs+Xy={Cl)hTt<`=)Ao3OXwifD*4~~o!|TCiZ@vMKrS*!6YG}_;Y!)krGh5SER{N$jmuw|xl+r|ub) z?)tKL{0reo;w^MqPo*L^kXSz&7IyzW2^L$xn?pY2(PP;7BEEpeb0Q;6NCpD97zIUz0j1%X^Wsy=BfWx)br*wGhhEc!>~(f$u9 zl1Fd&a+m2B`mVBzHpCAz*aBJ8bNf$W8aznR@R`f)9i8l|HQC@5trwgR_ZiS_DiO!- z{dl8c*nMdNCe|3$51r@0w8~r!@B=^xHxoKH%K8FRYvvoy!P7iNT+z=~(KUGP+3(zf z`}5VSKSlcY5@D@Qg9pv)iV0BUxue=txxd! zilEB}xNTQ0{pOmeWVH6mvqiP6-u94AV>O??iK2;S&Sv~aO*QLqE}RhG10q8)2uW3 z+1c>7<5#3U5S5n7JAlexx(iT_I{l|rN}0*XG;~?!!e&Myx{l^Q?=I)#yOCCJ&bg8^ zJgM-EzK~UMAo!d>lF*n-XX^X)PPIzuUaw~*&qdhEbVrAQA0&`ZpA!U2k-3j$<@nlo z0%5Q79C)}(YgR>w>|H%+X>)4Vbw<+H<@_;~ct*wn>QsQ_ETj_qb1<$<95dk6T%eQ- zU|y9Hm;Y!vKJ1V64$JppXRJt-U-dR4c`gV!5m>blI9=Pz(mcc;4x-^$eMmCqnA zQNEo;U4?f~0jcQV`5|yBYmW%(0p+BkL!*bForoC?T>8b6!`E<8@cem=dv%^GvNvh1 zp%r(S(B}1GOStc*eurTRFe4%t;W76RnV%DO1%|I3KF#;hdVKm4g7yYf2cbgdsI5Y3 z{(-GG;Aaff)g0P?M%wYC-mhh_Gm*MONo-%ByAy?Y+Fth#ci>no$DaG1%)`UO(Yb?M z=4VSuOK38+qcf=Ot0i>{V(#t`b@78krn-6GQmv+ye!5HvWo2BcTOi(H9FG562)~+1 zI-4=hoFREdoZ+}P?E^bjF>&z>y5_?ivbQj7tt@U)9S)I%>3QR=NdTYZC-IzaBqd_0 zd^9uvX;2`6Z>qXZEvWIzWKH|^xwBUVb@U^fNM2MY%-apdA{GY6DdF|BwLJWQcik7; zH4N2P13vBE>xsWM4sk8d+A*yKfH#_U_A# z#EL8d70;S}pMbpQ%bWPi-zr|2Au zaFq)mHvVGt7#-SH3_t|i7Vhkpwhd>L@a&sceLY3 zHUYWu7#y(Chh_+7B@pvkQ#@SMwPkxfK78t(WYqb>qFJvSOcdzvPeDP^zB1k6hrF8Z zw9sMYc}hIlp?qTAgok_7Ugm{C#m?`n__qfI)E5>$P+t4E!T10AFIh~f<+mYU1YjOPJ~+d^K@ZC!k$gQAIe+5zKJmyWOnOJmh6&Y5Fkz92OB`%w zKHlZ)3A4{+$}%0eZ7eI-KQ^hx7HIb1EOjdx*q4sR~AV`3lz#vF0q2+{qwSSHc@?PTmGnky2_}`lY+R7|dmR+58 zFk@b_GtLHF`Bt}~FdTgO| z-dbHXKv9k$=E1C6aAky@Eq#K7l%jc&VpiG_%F-H4m*L#WltFGvspgUg*{m(V9LIGD z0oSW14H}mFJ)X18PD#ucIDcHVjbM9T$c0N}&iuE3xdU;|G35ysc!T`FNaC9yy{DvK z4E$|#B*X&j8691jach~d1rVQS3S~*HvIdBSlr8#d1x3JO{rVzMMx5>}|G!A*-$XwL z>87q;kVzxggx%ehR5l`g&9^-Q;V=@xAHz#AO#_U|Z7{PU9;>snw24&k%xDH5h;)tS z{G%~Tud&{*^=Cd>Fk{o(v{U*a@lwmsS9EMtR@$Lob`h>M#Fguf8qKw@AA)!<=PjhJ z=I5TKkqe=Tou*Sfys{O=;pBhH=j@*g#IG>pl1AMnGQ@6C3$z@J5Ak;-(-QeMJ3aBv z)<@-+p*>NWcECGVEjg+WjG?DS`IS_Tv6Z?G=8mZ>@SA_;@RcXNZ4quHGKP?Fk6@`~ zP@~M)S&p*_ZW87OtEXC>IIkI6@|l;8gK8DzZ5t>D)`)$c29=Klxk1`uW#;4Z!cVBg z-R0%i`0?YfwXzW-NZ~4f`+PS8AzS25e)ZNNR->V$kaj2Hu?=cR-b-k$LD0wecJ}qA zvDQQ8Uzml3X>Zo=>pKbB0S74*rr$OW1t`Z$j_ZwSb5P%1ZskT65ea}@>%7P1-_H%U zG_$i{MyasY8;s22b~?~ZM8?XOOASo5Z0z52$ydninX_uMaMs-D)WUVT>lJ>`3nqJ5 zXYm5F+&HNmbr}6ro86{-owH|O19&P$BoaTnTGjXyXUvOJ`US~#7on7gOzaDY&f?Cr zr1n?O+iwP5{En6N`zAQ#k6$CTQIfKx7g_&6k;_4+0BT|C_stVKQ;i}k6V~okf+({F z5Av2=2Is3{;WjiXh4164L^(lY=>k5%QfMRiFMPsE6|WbxKWmz!tbH8@P39NB$VJP) zV{41J@Q^si3F^;)Xi5ME*jj$sX~6+b+e?F5JK;>@G~TZ#&Q$U1J{{tl^vc@erkwAj z2GgE5ol)b%{$93d;C#(L|C&Rk^nlyZKLDC7mzSVH5Up5H0WrXXl1U@jjd953XKG9} zf?4V>gfvXsf z5WeRG7mUL?JH?`0qobppS3>BP4y|s+UI`16tK`Xcgy7Nr)Rq(#g$K7+0&^uJiE;v! zh8?-TPL-xFbN?yP2(mq}Uy7!t+0E6TU+$X6ma^xYv8EpDI&8g_KR2phF0@$Z<=fZ@ z(f`mhX&adi_x1gz$v6fN-}Z<{#<|hLts2{ZHgf1jJ#RdYvXvr$b%S-HnB#N|9;n%M zVQCQ1TuX5P{HEx;u`gn78_!=BhSs>?q7HIlJ^b@)`t)u9Uq2PF?Sq{>cd`&TB>}^+ zSMRj?r@X^Q3r!b*4r_TCRw2U0tV*$d*l0*`nO7t1TFNFxj-&xNuvwcWda0J9C`^Ek z?*bmz>rN0d!oa{+Qq{pyph2!m=w063HEl>^dWX6142mR16J(`LM14-Bi`mk(!(9}3 zKqq;(AqV&_oAY_&G}ZKVDsJ%Fxgar^_&P+4TQ&dZD_CSOZ^n+gLzvHHhut_t-iTG& z!heEz!ez`XC>VkXN&=M;mQ*f+pSSkC58OI*4{yMj%d+hpS>%Nmh zSnTF@@iD^fpaB_Y*mG?mUx6HQF>L_rUPX7boc=lxuy@;wCuorVVCXUhIaLh0-w8|Q*!3ye`5ehFXIhAxMS(W{an;cCNl*a8p(=kAo9lF; zhAIf#YW-(azfXV70L($3;^J(B{=SP&pL}|7!s%=s2#j2ED*$Y7f zvWE*y1K1HmjPxS!pQbdB^V1<84%n|39U$|`YkT?tani{sed)E)Ty-7e>s2={ARYh* z(prb<-YDC5#80g!$dQp_Pp@uYN$~-M(14%8@rhacg{aTb@$pBY!Ks|m4!hNGc+Urv zvrBPA=A5F&PuE^h?H;kYI^@DwcZkaMV+aLR@Ab(b3!9VW`1EY_E6Jl@_&NkRLAF%w zb+q{Q$=LCFHTqZ>P-$m{9KT_sR@4t)y0R-5;2IZDtIb~C?y^bp9m+XBOtb)_bFF!@7UM;8^3dwn@x0_A$qK`MT=T_N(u>&|vqVannY$oN zTTa~uCKpV>d*o2VbP23=sSF9ji3$TrOXm}chV31(wgoEM(##^8G^^PUhP3+Idno$W zCI$VRRv}+y_LRtqQ`!x%)@us6qY4~;J>3L#+ZJcWMbyze>d+m`RrR1aSwG%i`f2|2 zKSSFu&2nlJ!M+z9#htch6}+Bjm}4aKI8y@JEaG=Ll14GI8AhZvw; z!7hjarbEylF+W?+pSBID5l=^r55u zC5}?+Tt*6#G8B{;ob1`_LuYJ+(QFR^6H!2TFnMcT<>wGvOJi(E*jbg`5hj7lCuV6` z+Ajo`s8O!#v#jtAi^kcR$Ua#SfaFfZkEGHqS<^qoTc98DscXHCY8fTiX-a#7-$LU& zefze(EqB}AEK+A<4hDuA79+1TzeqDn%U7oy>up5a*c@BG>k5bOW9WH6r*@rgr32fU z7r8-1pPXOz^*{M4+bgwHf$v>9mY7$g@<-u*hjR7QV~?T_k>rjHoDf4N17TanG6ek_ z1VZLJX1BL-jewuct@;MZ5$J6A9u3DHd@;A#AkMY|Y+X0WdvRh5%fL(52?~q>drMK# zGw>NO4-5_-gWGf%Gc$R_TKo<>{=yH(k%1;uGcU==B5+>Gwgo4~#Su6nnVEDbQB!}R zACtBd5Y=+qubDq?M$Rr<&xy)dcVdSE-J2JFcCJ+kA#(qN?K%@HX`m9q&jcJKb#CNi27P)^uQL^)NJ!< z1-DBYu9GL#J5QJo(y7+@`V$KU5<9q{(h4RF;!TPa;`7fY27co25uL<^GYJhD$Xsg*h zzfn@%pn?l(;=TbuXv~>8ZH3*vT5SD$beWSwb$6Qi$SH7;*mg$)^HKn~9As}SH>m&2 zqE3onWTwWxsuuqzj;m}aos(_Ep z?>FJ!NfL7MtV5B(%{nZlR?ZtAh=e0;l!4LxO1vszrk!!@^m4Q6#4k($Q8W5c(CENx zh$IfIk)L3)!5LmpRkL5@Nx)$^&w_}A|F%3LokB*p+hNO;AzdJ_AYmh8lCTmI}km}C;-?CxWu^cJYegB2e|~Wykt%0zDpopmOaLkEfWT|!lRb~466}> zE1J&!tURhQUYHVV5M2N3l~9(S5qgjwu7R_UFeg$zXyoN3)?7g&RWW&b-7qQS+CpY% z$n0z^uNUnI62Z~LIKo4Qjs9D|XvhxxH{NoYZm=&h%+3FhZ%pAS+`EU;|*R z)r@kYDISR40glWMWz+#UlDPk&E$-Sa_(_EUBU05Am)V~;Q4agFH5>vhHPK^%fk<2S z{8gJZR*Ph7;AQC=koN<};0c zD%(h6^dPA*aAUvK9(#%%ZAL>&TQpny3tF=C@dNw9EWNCeVu`HyEGj|Fscg-DB%899ewQAfVVUho65x4;d}F_-g6ORzBrQb z0{5kGc2C;jc?ZKA8Q+Lb6$Xj z@B&M+e4;nFG(AnTi;*=l#62gPDZ*jTa9M?JaCBt+5!f@Ljh9=Ei6|*+(qyB0q>fmJ z25NWaWiOZ^ArMtQwFGG5CO<2RyxrU9N8M&S`ay$2tUZ{76XxliRXt$-NdE7`B7pvG zh3Wd2^LAUme}wkCRN7ixpMcN!-8jn?*awk5+#hKgoqQo~g@?r00}6KaLf%x^veIVu${=FOx=5e<#lBv@=SRW_A( zw>1+n*id_gXgpU)4g@qubPrtDxwc;qCcWxgwIEC8lLRTMIfeJGBZS~{3J5A*>;M&SvT$@)6xI<3Tvy|EpkbhP02&#LYnaCQ1byEDsI~xt zA)6Q3;7Gj}pJ;VsunLGO*v|=2jDvTlda3W|BWYlnH_eTegpF$Iw8FQpt;TpR=%@~Z z#$#7k&hz%;a!EW^WWtHA^6$o;qv+Pmi4^u)gW@K9i>*;EXax{T1FcNOCoB$Wd2F*sPh5QV!=Mcu_S;Bz44ZdGpKoJ#!z+UA8lZRO3!`jce%A?atAcj zw^^}{)}si?Wgw$MJAMmxw2u3;**|=JebJ}J(&Y0w(xhNvC*>d@WW=V7Ce@*QA`~f| zm}!eO(TfL6Q$#Bv&5lfG+-~Fa9qydyh}nA&KTHmR8&>G zbydE-nP~hb1N-z4P}}>A6+vQma}?D1B_Q#!c4M{OoxG3Qj7FR^T-yL<_j$y{*bou} zu+j)4D_T-1dsvFrrnC=kv@s$h!Yi=IJ<9ST4tP1|>yPOMNMeFkgo?@GFZ-`W)EYec z>~FFZvQTjyK5$rp1el|s=+XQZ*s%%1^iP6`%2G2&T@J!+uu73MSS zkmGJV+D+-BBOrMF@tMhA6} z4I9Yi!gZ*7Ih*f{F9;DGND5>n-xGIDooacGiS>O`jQwt#N&9lhq0#|7x~_@ z{qEAL^6Z(+PT*0e6ny=n0n_<`X8v+$lRQN>aq2sd5KBX^JbhW)19$Iv@ct%FnXqy3 zNz@iYbbcckw=e`a`{e6Qxq1x#lxmqdIR_3gj&r1eb3)PtMKrc8M@QE4UXS;Ix?$>f z^QLNmS-LFoPLILQ)QD&riHLOa1qj@y$SMd&e+?$9odDMa#)^%x#KEClyq@q57{Oaf zqK2B6fDmWQa2D`!%kKr7>Xa?oAY4j zssVu0-kCL|p)bHu_=1u$zkH?B>4HK4xE7I!7$bmG^XN|Vw$gM+D6Fb_fT`$$!N8OW zW2%M=b#PM4goq-pmt=ITflb0Lx{*J;hkN)JW^vp)4PYh8^o+L$%gN<)^}Nt<`+Jb} zNsVl_n1F83G(M>Hfh6SAI~YQ6DR`>89VGHaa|W&6=v$?p=abQjs;`i8d9_Sc`kTQJ3DXw--YCu$kH8@-8C2 zYw-~qN3HX}o`GBl{WtUV*q6tLJW;HW;}(#%D=4k?;PeB7D?#0&r@+m*8PP__w{M-X z3PNA&F#<1SA2sZ>7V{zrSW~)yfka712g~g*Xah|a7wiXT9`0|mHzW63e0LO? zaBR9_iS>@kUx$nqM#nBcbkqbby3TA|mn{gQ-LRAPO#m?8x2aDo9LW!Ro8&9$$mCWZ z&&t^xO)OntP%+h3=5v0BK|N~%r;Qi!>n|v%He(zRaeXT`vrxi%c|UpLbg6;*MqTz< zD=R3NWEb?Mtp1JH4K?Q=wM3Dt7+=xVsc+%_gtKl$>u6yBEzH z`prV{VkOxo`+!xCIlI;hNS;`leoU%R+6)Flr(;~dJt_k~dy!PaWI@<&fk(J0>0({T zeCA#0@XL2tf(o`sPX4B-=_iXNn?mZ}?C>Fnbw=d(+id@^`Wq!ik3AK8n)Ow?NWYe` z#t$p`^Aue?H}1~y>YG$OIY?D6Yso&Vx44l%)QI2vw9NnVYa@sh00??zg$12|NZ|%N z06`%Tq;4v}Ap7L<`TLr!>w0UDgE>EwAqrrA$>VW%+o#C;b)+#KJOSr>YlSv9XJ4C7M+C0c zVilI=_DnFH7F_6BuQ!PAfSQQJ1-hbu^W7=p4JEtNjp42gLAR*5xF>GtB)r(*J6zv? zhGb;aGo|Ad9(v$t|Ew|+;$^#rK@3t%VPJvj7d){XNM|dV{t*f5+~|Nbo{Ynxa@aXH zO&B0z#dRZP*Yh&lfi+ftoCI7Qu0j+m~OVPC=bG60wD=O;caqqO;a;G+fT+~uai zIt5dQGe!L%Tfq(UxpkN5=L$5?seLHEcdx{>o5X1W(-2WgxtvZ>5ejpIl-BXqF|t3$G6DdHvn+Ev9FnB%W&O>J3j6r1G z?8EL(y%l)Zg}}S6_Y8_~^K4XsstK;@@w@v2G5Xb2QJZEjicwd{NwQ^m<%sbz@qqDL zL8+%R$eQ}mZJr;JgVM*Sa6QPKh*|Gkm*yVfh*b&m7p)nOii0MfJ|9L!MeJGBYhHK} z8jr!;6ntyfZN%gG>ZUbQtCemEXIj~^afz$YM72(ZVAf&$;8 zjE^rbd&RK?{TEu?Qq%IMb}=ODEbE)EthyQQqH689 z{rl-*%OzQ9{mQcf8Uf#XFo#Ke{+!hB0(=2oU|@zj!9<_LohbuVvV}O3hzBxLRq<_z z5fY2l2+qq(08N1>!wUCzD!Wr{Q3W1(vx3V|t=IHn39x3gQi_?30QX#gswxZD6ZFX~ zoGBwhO)^5dr7-So#=#{o~a zY^ObLF|pmgR8#3Du;}u}>gck!u(=YDz6eMFnNaWLlMO02P-QhlIK>eCJc$FQG7ZFir!0D-RxDh6I`hE_2*gc1yN|!itIxQlo77fWyg0 zG7<^2uVw@Kzr{Kno8K-{vwa-wWI{)U>y&%^h z6KK|y;$@Y)j$r?IeJAws+V0OG>|dsJzwx@PK5o5n6;j<5u7*+-Fm$BdW0TqwgPR^>v~X!)%DDz+G?@2-vu_I zhY@ld0V9r23IhW}=iqiKGjj%S#vZ!eNOJ2?Yuu& zNKDLE?w0^{Gh2m+>|H);FBr=Vtph<}V+BQ}_Uhl$>l&pb z72Jp@@9c_VwUq^r!g2Z;E%Y1CR2>$HbZM1aU7lzsfOA9x2N>9^7nN#x&Rg?+KBJ_eg#WP32|trT7%t5Zqb+O_bdb9S ztH(fS;zf52jVnEfeF#Ol>BHQ{g8$sceA)&_qYK`8t5*q6@csfxWB95#_+irJs`csS zAH)j$6roR#)<(JABou=_HbRs(jg}m;`Kh57F6=I8Fa*#7!7&HEhxzOGgF2b`@mSbR zOBJRcTTAaqv2(|sB^y7;KZ^$>NNmvCVS`W-`*vb``R=GG&x4p+R3xInBU+{YS$Ml3 z79Tg}>nVrL`<}Mjn-0A-xFB1d;$k0C;w&|-93L02Pp#q?l|tae-s26jK~QN-*LgoU zZuigb9~p`-n(<2U8u#XGTp`!acsv)e5A!W9?cVg*LpVyle0t}_{v4aj$r|mrhr9dH ztXY}f-$n;|X68T+Y3d_D%$9uLzpk|pzEACbSVF;iU4hAIJl0DhU>G4v<+0+HK)XEn zdwU8A1K;4Q<5f1h4O#K_-%#k?w`XdojX*()y43xdP_PabulvtkY;=dy1`WIuwj2(c z@Upv?Xh+!pqSEV~BY~5T9pItV1Kb2);5vRi=SVn0r-I9>$!dnV!R+3vUp>mZm`^N! z0T^^r8&%*<&{qCMni8TJc$`$Llvf6$f4l@R-ha2*E&MVyH1Yvv#PB(_@Xt>jvB1@b z0z~hunNu*Q`V|A9=C_)~A}lh17SRR3s3hB4>Z*!&A@l>%`e56KhMxVlAq=8=(kvsz z0w^S|=V5F^i0_}p@2`M;z0aSRMM?iadvA_HEp5R7h>od$fTLoDv9n;S6JA3dxIFNj zrR6L}D^c%kv<0Smu4SyM~X8*c-XVRxj@R8P`Lb-=&14xmfQV1+9_I_+qw08^gT(uf`tv^;82O?2EMocN z9aB^Ir2O`*-P9$~>0KEf7iuuvL$l=9!XWa1=z?&YF1#A98p@hZj)SIGz2L%`T%NR) z6<{d^w!J@6%yA#?im+{XdP|O+Y#dA`bX8CJ@~b6T9qb0*o3zU}XGS~vUg{hB9lZ&o ziKk6S(Tr?ltFA6U)1#t-<`n;2k*|@k2m@b-(!2!W6bnNpf2W2WQz%Bbw8Ggu#+MkC_uLoqw z2$GIO9nY^*>jZ-KI?{ymO7TEu`mwp8XJ}529lgrt>Wm699!kW@#bfQQPaDwUUnJvV zZaaSkw|H3XUx0+Y(bM6l)#{mvaq%g@n6b%ZXa1q0M0#sP zdTR+}MH!=DVEo!GYqLbzXqbhyI6!?^0U}aX-o@c!;`OscqlK?F%Mg&Ku|CD_Y zn!7vGh}Si`Jp^n;%SMc&8tlbXP7DHeT#I)(-q|}B6;6d6ue_%_{`5-8DsEgyVD!*0 zuY=6u7!P9G)ep80CXpxXy8F*`7coVq_HK20QFMnSmTKL+u^CV0#KKPq=9zBjPS+Kz zUN4&MPKINdB1>rM-Km6!;u8v(Gv3&)f6zm>#Kjqd!mLPCayx%Yh>?Jiyml%7r&JQF zp0BuXkV7T=uea3fo8RWZ;V2fb>-X*;*j#@i^Fq?5d<8ri&Szti$Do<@ZdUkOh~yb3 zvW8T@++Q=#wJ`N^Hpt}y(-*eKNv73n*-#R^uIt8u9R>!Ba9l#^*MVCkYP6?k002Yz zDcK8P*m?o{rKr^$R0rzPnfB4_r>`8@thhF3isRG!HB=D4KvFYKjZQ@dfguGDjBSl#Fbofue^S^S{?2kFdoh4^!$ZIT9f81L& z;rxPxV>gp}nW(8SR$7fM>FLnE*B6nGF33ok&+#uUK0YC0M(daMHcze;{3Q%Wz|()X z?mJ6dmdbp5$uq(tEIep1LZ2BRbcLp9J$rYJ%P?PwboC&x)Ik?_$U7w-kN35Gaq;QM zdaCfF7+d)=BgI#qqweAXr=pCF-s>v_9{Na12Xe*3Mo&LE+=IO_VtLAnt{>zegO%_b z3!&>H^R9%1wl6GSC~_V_2bztX+S)%y2D_M~vw6;*%f1HO315tivYVSpNCo)4Ru$g9 z4OqU=6xEM{&EYU#x@-z3Dwr`HwOSh+iHNz)+wGu&K?nyZD>X2i1c{|Hl@o1;fYYnx z7Jy-r&GQ1J$g0&TW_90sC35h!NX+E5Yg~p@G8uS%Tc$p4nU3xPQYIGIV~AUC{m{+S z&KDm`Mp*k!E(!Y8u(KJ8Q?#uwcCf_I=8qhJ&XSnmCWy2V(xUN$DM1Bc9qLqdozRkt z*PCuhe<5r`v^E`Kcy!B_MH*}@;|dv^X)8AuZ#5r<09-}cfv;gWXGK2{Q}BaHurYai zNN_V&Zcs)fB@~{F!5Ne(C&i=F^|ozweSVJi;S?DaR?jCxTgfy=N7d)nMRF*sj87 z>FW=MlaL&Rj_U>JvUheHFR5)@}akGSTY}?*>`$r;r z-6lhFab1& z+5q$e4eHhf#+KOZ{?Ck)57uJKT@gXji_?t|^_+%bGcuXGq5mudYaytci+G-Dhn}*7 z%;v!6tYZEQ?#u5{qy4*TC3wmeyvJFZDC-dHbiS<`;a?z4*4 zF88v8LYNIV!?+D;p&$bTVKL1Kbr_i|cOckx%@Z%?hiC?kiJz$BCV^@fESEPXjw3kk z%rNuxIb;4w*TA>S%p59^CBjGa$AoX3|N3wePSh1r@?suuixX4+g|4I`ixnRL7WMIg z$DX*q|H*5ypYHM#yWu!O?@!{Oe_yPllNG%vaX&aD&|4k~ZU6QQ>Mg^f`o6GXx&;Q1&Y={L?jdE6l8_K2r6iO_x)Ek* zkW#uqQM#lX1f`YkmhOgk^ZP%~bG>}?3m2Y!&f06;>yCAkKxw`666P2t@n=`Sc)-fv zpGdTdVXXVUVDp115CWn6J(2??z&_xpLiZPe*+5KI7Nclt=UFtZSP;0%snLR>8RJ^I zA}GuOwI3N371GkOd^c*Ot80ZrM=$Ew>gZj<0uum>ShM+N-;LP@k&+*4)P{f+6PlPr z;*oHr1%!snJu&8Yw>P1^+23YdPi|PwSoh`^7bU1^<8mE4;*TD#Rr^j17`fu;)G|B7 z(R8Blhyddrx#O}Q?<11*nr0}D=I=!%p=GagUt7}8oLy__iPrA;>xubXi%JOIS=I%+ z0|y}DFmMUg(=PMD;c(T>V}b$VAIyICI*2y8D^EauqEQ@sp;ox<(FA*%O zPD_BpE6Izzcdzp9=j4!eT~`ArvH5ZAPzihE$6dhe6MdJs>d6?_mK2KRlCT#^rQX5h z)smMcGpCj``t_X$Y_fEzXL%m5e7B5!Pjs%=L|V$%p|94bnUy_v=qcrVSnlH| zL%gtsulePzd;fZ(F^U=;VwOJ!_2P%jVDsAG;2>(nh-oeJV;QCFLtpSkUi_txCjGF+k|@hV{vcJXJqB$VNE5eJm(k4AOxo#%+a@7j&XGyvf-@_r36 zG9ku9|9g%Fhsu8I>`-TSrOnTu_}^e4dxaYLyf(CSu`0CkvE<%@>0CCmO5)vw;VdTO zkt*OaDLx3JtT`Lmx5oFm9X8qjS0N70!4o{ZK*}M0j>i=~#z2xnm&@hB{ws6dfpLh5 zEKl?5b^p4OVcA#A>G-X!@7P2~&Qa#uti#ejzU42NA_#|QH+d64CHPM?-L)=j4`n9> zo@8Y4qi{GcR%LIK;=+L8%D{Z)cVh>}LErc9g=}qY)2Lwtcz8iSrb`@t;t_+48kj?M_;2zp&zpb^f_Bo+uv<1|nzV04&DF)?LyM zR$b06F0!hdbw@y%r7)tyucxvy0MM}tJS>B-Ue&R&v9HwQ+(>X~9=;=ye5X_gOgc_O zNq+K;xOnpN1C3hpeHY>UB#gX<#qiOvoVQ%qZfk8u8*niGK+Ed&=@iK)F@A4}>zhjj z1Fw9J@SnQ9$q;-BneQ5UD4Hu7;JInQ7Bc2JXndwqGsepWcs=V+kPJeWb}inMy7N8Q zPc&VdE!QWZgDtG@sd2T(Ib(7SU->&;D!h(5^xaEe571Gegt>|kjDd0RwNN+`2Z(8z z)71)nOayP*Cx9Mw6;>Lf#q6;{=i}=Op`m%p-vs9E)zwu-Kwfl!uHQdC8s3%R8pYyn z(7ywPziL4#$S8i-IYirZNOg9fsZ^QiKVeaCxCi{w9>~Ih-n`;FA3ZY@C2o7sAIg|G zut)`&alo;fRJs;mB(?Xhwh&MJFnT<1^iUE>vM0D~n*CrmEZ3t59FC`#QEiY@&;EaC zP1sCmC*CTVB^0bAXi^G?uIwdC&3IlHpdmX?Ft6tj3yPTsTQ@6D$nKk~H-)u?h1rlzK<1$RpnEv@gz zjS!OFrvXJm_Vt@=hWHtR0xvCkO84^Ls*OTO2a zOH!B~#|qj2aD{D@#q|?NIoOUXm&V-e`{F6#1o%-*ApQ}Hx%IM7gFwz0yDFOCb!D^)- z_WDQ5O5}6m7t^5kX*4!JFY{Grw7?<09R1mnd*Fjh`!@+~6_LP0+4seQg7&NOnc#oPk(nQC zXR==Zuyu~kT3|6f0N4G8`o0EQq+eensF^;#zcZ!R$QO!P%M8|DETkra%@b-poTg9- zpzXSS?hFrGsy|*aGyO1jl9_qE&{v#rL6JREh;p>db9+oF{((9-a5qcSfq*{gP~;{h zG5+WMveWXv38z+DQ!@x~6taMnNwvUzdjewObAWk!=6^?VcQNR%VQBd68>|ioS}pFr z8+lJv_doAo82X~c9)CncC*G++SB=D&^Shd!rw&5E!DwjH4-gMPiaPE!V)*4v{-oS( zn56SQ)1+!)i1>M$*sNnlL+q#A%DqA0C~ZyR58hi%P>CxLr7=Wk*Eb+^oemBC8ZyI8 zoZZ%jQ*j(jey9k{^}co+!OEl+VgG3U0dNd1Z~5@+*S9Zpv?BWc*$P_wOv@TWm=)m} zL74`rU%!9HjML9%q5~Y8bC(Zg1qE33hj`kY{^A`x_Ea&z(5|W)@DRTNA{BTCq5*6B zs{enmyL5VmR0?tm(Ej1!eeaexuf6KD%^l;w85Sj4Fc?psMP8*<2Q<^VexB{CISJDW zj9UZXA2(G zLtBOOry1geX+MQCtioEPc^yT0{rHF68Hq`-bXdGDwiE(W?M0*puiVkr4$pjL}l z!jKoF_nj-M+m7DT5@zXdX@WK%f4YW^H63)*gp$VP`kwsJWvtLOpHRJJ$x#9O(I>#m z29zPy0(M|3J!9T8Oah{)%7CeH--+-nlQsBo?(?w$HYl6H1c-%~{32pG$>~#w&>*1g zp^RZNxW2h|)X}CO`1c1r*xMvz@y0t__pD^&+jZ^G_!Nd*S{Op_ZLfhmjo-T!!-&|(74^p~DBYCf}@&dw@!L#*h^IV^D$c0bws?`tSd z>>tN;$5S6(?cutkj=n+Nnd-0)+zBYYq*BjyKs$YhdFqEvc2F&^)_!IObjNk<_eOi) z0R`lKfi+?nhuvl; z!HiTV5#!7o6#8^$&Swv{V>sJ8z+E^($-uzyfwS}?Q1_%Y(!in9gx(LO^!illSF<_P z%k{-YFbv2*m)BoH`=81^b)0@8vb;!jX1-mWOHB9BlFw~IR0=patnx4O9nG1TSzK!r zFNf*peyzs>UhmAq9Z4VXS-q?s9>s0DT)W^HvPTe@Q0BIdP3N(=_l5(3+L<=ez zs^sDI^y&vj2=YCTVopR_%?HuP)lsg|WmX)0=GmaOLEbLlw@%l496&RC4wdrqJumuD zox^|#YUxX120xekiWrb;FA_Fi?ZQAFBdGjTNc}#;cSAZX)*>K6Eq)V3ncRooRE2^F zZ2-4_ejY<3L|fhm7zv%c00hi*WUl&Fl-Ms0>>+vS<&xv~C;v2=?_dA&-Y>i=T%;H$ zRDkOtO~oOeh%&+*KDS;J3BNPDPj?&74N0d{_fX+08m~fEo7c*Bz=7$zwg8Q=a{g`DGRq#FCOl|YT+GH|EImn zGXx|6wA=v@HjEF&G`f5ASEp6g_^SZeP0a9E!qvd%OS4oy#h*Xt0eEM^gT_B)2GtcA z>qmF!Zg%{}$s&gbn7fT8lf=jTL&YuElr(Ofk3Xm!M`O0rp1(4p_nt>>4Q|2Stt+*R zN$;ih#YAr0Rk}0mzZk>4>uJfeidSV%WQipv!FfV#`xF~OEGsGhVhAQiJm2yD?tMFf%jpk9FGC#-^>=_sS7$eV4W;D;ay7 zjal0|?(ckLB+UO-LXn3{98ZifTfgvK457()Y4QGGhQ_%yeu%hX^9J2S5Dt5IRt^rQ z@$SHSW@|8MDf$=P?q4=7-MkK-E2t~m6Co+ebf7-F7)ACVi6e2^AH4@Hmpx}QwwN%H zyE}pUPDr7jw?MsuLTgXF)f9`fB`WoUZi4LyjLnmU`T1{cAOCC(kIh6^ijqPmQRZt$ zRue+U;FM)^KXcwfwMEVUSzw$`zo9ET_ghx5gbK1?yqb1veOVDhA0inx4`bsefk$VR zmqRkH(^OZq;l~JziyUquBCBA_FdW$y}Ey||^Z@t(^5F|ZWWV2`bkhsq{ zVfQKvacxtN&0u_sooiWDnm%_pe=;PgG`SPKjVRo*e_j!s&DZogYLMP!mI zCn(g$-RK+e)&W(}Z=}1jRz3g)E*$4i00N}$M<{rn*VWlx58w$Qf#wv~W$l+Nut?}Y zjM+n7KP4w4?!8Yvz~O0VX`w^kzTcap0}OWz2o*V5XQhFrfspw7o<;c3ZXa2c@!TaiVd_Yb)1{tbiyQMS*#GKG-n zt4@u_5Y{w)r?kn5?2@m_865eWOL|TQkO09qGPVq2&^kY@BvokKmhhXlj2vc=J#b>H z(Sxty(ax&tn?XGSa$n=L%#JlAU4^Q_2iOB}gSdK8FeG!WZ01~XM4V><2~!Ik2ZVus zd3qU8PT>Z5PZN7Cs%@$j?#t|r8-qG^E7N#sfrOtNBB{K045qs)2x<3SA4u%p7V z-|@C_)jvXhdmq#>v}(DZRIw1V%+?!e^dE@8z>a`&t$@0W%YfNcj(p2A3+}Q5*g}9{ z^FY`5U`#6B^SYPY$OpwpyMNML*cE@X_7DCb$xO(#`IT~2y$-Dd%-pbh2f#6GC4a_+mDJlvrgyeT7Y&t68)iu@Gi z^lJeg5%adaxaxQLK6GCqkS~mX7^>CZyxBa^5B%6}CQ8E{3|~P4kjluW$!j5yrceOr z0kAMHZ5;Q06%P-MeHi>}|5Vio#Bhp}&ewj_Fe&BATYEm;B_BDYy{&;kSm_d@H2UIV zJ`}F-49fcWMT!@7qZE^r+U@1+)Cr?)sTUTJ|7zC+Q`{&xJUVB*@OyLG+@dOifd$~h zGtuHNjZ6#FGnuM&F!B6RgOQ8i7}v{wmxhRdPr_%)wpXJh_C?j~I)S`K6Le=mpAVmsyq3O(P=y7V1twcFPB+9mL{Tf1D`lPvBXzZspM$p-$L z;9CSnRt4J7+4`d?F=zX!Ga|FzCqPavl(BLb2;Ej-!%p3n#==5}7SFA-$o2GH-^m4- zeQ2&v_pNUd&g-Q7J~2O;uWw?+#PV_4oQD>C-Y@dI{XwmZ8#eV35#IuQ9%UI|Rc!jb z6WI^`Xa|##$jU)hi#M`PM45~Nr6tijPXpv|68+{q{ZV}c3wpvX>UL>f?~J!nC8jmn z%+|JM&&o_GwoYS1U9`Oewc3i>u6DPJ#Dw{8@l)CxTNK(co&5bI6+ND{wcM0u~_ z?h!zfdUm*}~Q>E|9azOc8kC(`nvE3^*`8&LAk(u%RCqDm-pOkO@Ry^#T zY{s`OJoePl)FW_~#Kgp$X!I07riF<1o-PIADua!*N28>q(`=FHMkXZ)S8!((Jd6 zMw7zGN}3+x;EG7Djo~gd>E;bWCd(SQjgC!t3^jyrU75=Mspzdlx~9g=qX>V)SX16R zioc>_*xJN=-h!!jtuFdu0%M`MCvg1^Lpu#1332-O1PbtDfeziM@jkPEH&G@VO@An3 z;7oq699!y&Fn!!j_%n?`7gbTLqtKnUgX@rvKb| z*q~iMi~>3wlBgDjv~-;AU!MiAU%djR(*Wd9O6E4=g;rA(L{k!olvl=L>T?^)sHos0 zd&|Sto2R?zX33Pd5|NwCqX%7=nU1oX15SB(OTJqwJWVVNu_|z%`F{le9)^xJl7&u= zaf$PzJyw#OHssC;G2yy;cF@RcF`q5>NWz!8@{O8<%j}0?s-4^0m9(nczQ6G=|9u6k z3S~;kZHYvo_xIN;u<{9cxs0y>RSW7pDTa54ZDf0>bG9S$NTTqVA2h3Z1$C}~{9Vkl z&l_Tw=Zk~!b`)PY=W)g+#5^eYnta<7@;jXq_Ujh6K2o5;*gNt)_Nzfc5Vz^J2CHq| zj%Kwcmg3$l=^`(j)}i;&sQKv*%A6N&(MvaitUp0d1LtQ@322J%_HJN->iH+S>)RgA z{ZBUqeo1GbtVv@X)0p|pXlx#hPL<{-+OjVYLvsReI|5XTWjUCGs!c&O8}$AIvoJGf z>CgrzhYv2&M0ThZqZwkkJ1RJz5+r~qdn!hjAXHEr>vB1h)&{n zCH|QI*y@T~V_m;&gSFDUb2*XEg}Nf?dq7Hvo1@|w)%hcd^T`)s){!1WoKLCXu0Ojx zB0U`XgQ>>)2d8P4u}%~;-&HM5CunQu>>GYL)B(&|xc`b*7a;GCYx!4-*f zI-V|Un);D?1awnWvPHHN!OpU)(4`zc?jQrY`x3!0k}Rr!Jk&o*>KcZ;c??;=*kS`_ z;}dKAPIt-Q4ejD7O>JIL2|LUN{#yL*4ySn0aQmgH-2BX#*l1vS`amuR4nJ+*FkOGd z@dUicV&v&n=|6{q5P&kZ){F{gLoSDRx*-khe_*D6=_;n#pisq&IXT=8pfpw>IkReA zzZA`WthydZz4_kgtp8FSvP}wX3vRm|n7aw)3u4YT-<8c(IpS(uL$*0s8q9+bP#hbs zjK}o&d||D*#TwKS-zr`9N8;*WlW!rAVIE4Db{w_k6{Q9{&JG@eU;k1fi?5s~97)%F z5p=_xD$CT0u8uw%&ut`Z%`(LN%IhGm2A0kfJUjLW&7l!z<_c$sXLg^GFXoxa1D|Uw z+Gp0HiXrEhs_n0lXpCN@gVyT!Fobhknt(i71l|LJB?WeN)8YvER z5Q%SzewpqJzF4*$E#c|vaDrt`uj(I$T*OT|abT9Y4P}46>3%hG?+}@dMkv7jaW8d94;k(g^g^u(AUS(LC z426wRPG%|5C3~m2*(J)aCNEl_Mv32Z1>P3A1ZrxXXRy;&h}e{DRn0yytl|gg#0QUg zjwODmSCJdI4{JuUc#qdtS0_wsHhOVv!r!@EL>qZORA5M)aNi*I>g<8CeLWdeR{95k z!Rp0QUKS3s6ky_n9PWtLHSB8<13gwjSsuY~cAFy$xD6PgqvsMYvZo}LA@vdPPpWV< zn!%38$DaKj+Zu4f2r0AyKXZv@_O;|X-zD>am_SeVHQh_wX?ddbz)x1VF`wzP2tZ<->s||*#4QnHr(%`nui{k(be%*E$#Q~l%eXPHD1+vh z{GXZ&a)o%^tfl=>`=I6}ISY;@ZOEBKGjI0yiXKnAuegb+_|4lIg_FZ9H8J=ZE*DN- zCP&DugNCQLBxn4+~2=`=~Bn8aN4(!vZbNsy{mt`@8~Y#*)Y_K z!qU_AdbZ@|@a>1(;mdmC{h^Uwu!;HE*Y{EVkX`LmAv#yH@ZI`;*$}ZPKZLeXb9i&K zSIUd5rp8{2d~L>6uDHMI_SV_)#r5-?2sAP*qOFIaO6hdL)$o!qU>OO8j)p7u1zL!b zmB66EG`^QU0ih0UuS(q1)>OO!b{Tf%|VCm?ZP~s=;p4F0LMNxvK+ zlk9Ok*MJjY`w(7Bd~OqOGu+l7Ms1zAxa?_tcn;#3`10cvp-}8LaC0U)rqW3KFq7Kfqn+To>D$ew zxP$Q>TGXTIOD^^Wo~KM-zW%OMJ0JPYl2e>z%;te=K*cz;`t~-xt>>pn3lXg-3X~rx z9U1b}q>M8*W+kzj=jPt@*o%D%9#1YY+m7x1siXa8A>H9GBNGSOs6gl^+O6HS&#mqw94 zmaN!1dq8eb%Lqp#9fxmpJsOYtuI<-^bMHn#BqbJ$h>Rq-1~gXkQg+MZA?J|%doZX% zJVyoU@U9cifd<_(*o<=0a*)EnSM6czt+L)2q}$)oCEe=e{gV2QQt6Oum30Rzj2 z={seEnN+x_!N2L&fuD{xMul4)vAVV&YnZ(;s++lP{jwH>){$>ha#uXr$DHOlpirw$ zFh%8aqwL+FTNhzp5}-)@-kW~+u{865__epZQf7MOj`x!s&sWk}9d)S_gge6^MYq-g zt>D*3DFMu?rh#c)29qk?{jz+_A|1DO?cB~VS|XK}`H`4_f6B3ViD@8?cGHPR5DMK0 zekI`u_~1xGI&h3uk5bn961vCQ&2nVNs5)KTx`RCJxaS*>HGer?uTde}VINXL9t(90*M1x5}C^}?q%x$mV!hEWq4J$fQ>Hh`m}0mG`) zFLtnwWp@F+yA`Q+@)B6Qd92)~XL z?Yq>dOVDN@iKr{ye>%=KW%%|o6??l8Pw}qV?pOm(-o=uGEaA9Vtx5i~x+xdiQCkyD z?}dTZM2?2N7c}ZZq$ykyw$*OYthP@Lhacm26Y4~^alJ2&tKqpb@N%Gf)D&|VKPTe0 ziB^1WlOM$uqp~-$wNOAf$H8n8)N9!ci zS7L~I>BswenUwV}p`}E%%jHpj#9t3rUY5T6!RTw8Fc+_zF^YcPUX~c*LixFvnLC(jQa!x9g7isc zg&Z*Xq#%;tcsPS|0gho*t7*G_^+Ipvlrg4E1B+;rB)?xSKf0|W;#!b8*5-aX9Iqsy zV$4B6DVgU=Kibr%=!4z8xzpJA?s|r|+HK!iDg1kwMlBBg*EUS|lgya{Ih`FH;oZL2 z%-pc%-;TUb)=Pn&!k8t;73w8wW-u+9hx5c*- zpM_s$(M7p%U+mwu1Q`}I3_RCzxg0l=D~?Q_arM7=1b-E-x8=>3C_gWu6-75-59LzC z^5Jew(%|fHG$GF3(%F40MgQxIe!cFywQgi86>9;eKkJ^GovuAjvxtY$?#>S@WK@B% zx#E9Wf_lWiNEm!PG{6Gi77#BiXd>y{XrulzxGKq>Lgbgtv_8s=4R<;xJIz)JC-eC4 z&?Oi`(8!X*%5_6A0M@GR@vmM?gEZ=8v8ab$I|y3hWBpUSDoV6Ops=3`-GcY zO$b|1!@z#zQr`iyEv>%$S%NOVrtFF`$5&g85&{1jg9GZN)m=ho0`Q55NL_6UfQg^q zY&b{WkqqiJ@u*3Gx#{Bw{|6AU6&iw-+ld;CxV^O-kBGQ>r+)I_LD9FbPg^aer>FZ> zGCJSGQE&Td#*vJV<48bH64^!k`QCBJe4P6vo#nYIC7=;FKd+w>7v+FRl3n5X0PyDU z>J`)~E-}&aj~fOAradbDmz)TT$iU{ChA?+)TQCAQrnc#uHEyg!hou&rh-d~guhic6 z2hJ^KjN5I%d}8N~u<(2r%SsDg=b!^kB`KscT}`5;3OymJ*lc@WmtLYd3`xrX=2GsQ zh}46vZ7Th$gblAeLac(hA|i;FCzuO&(4RM>_DW)GU*f*BvFGfKZ#mpQ~^B5Fb4!&5~EmztF>s`##fRZJOHO$=~2J48!Va zx!Mr$*a#jsFd|R>_(93V%JerPQ|^hGHzz)Fjg#PGJ(`KXv}nV`gdUekGp)dzU#}%X zH~5!a)r&?f^YZgCqf|MjP!Y)O5+$!W5y2GyxdxR|Ci`8Z z1!Rugjz{_% zGmI^S}s$QVJR5}mbQC7gXd{R(upHeKxl|TZrBYQnogC8 z*Y-4NvUT5#TXA+nx=Dzt}gsGuvs0{^ZC2b?bV z;R+y4rEnQFj0e9V=7&@gd4wPNTvFOroiE<*Q8uX#oEr5e6(nQ_>l;b^AGp*uD4qKI z%i+3__EE;BgY$AJlb1B2b$ERjRmION8U{a;L9aOZ?VaXc+q zG%-lKmd8Bk)fA>pCPnjQHFXzA0g9y1HIa?}W${#{B2bxh&m?y_?@Xw<8{OZGK~wts z@rBBi-?#XhIN`~I4~6=i(M;p#Z5&Itoov)C&BwqXAK5EhM?rm0{sTl9xX%uoEkidU zOx^l9%OMd1>Smat;l%sVU(pe&YD+%3giKEnU@h_1#naD?yCSKC*K_(Fvc&h-oBGEO zilV^Ue3-h42c~T-zo6={sMI_+ZZsZ(^RxK-uM!Y3bf>HfAXj_+ST}<18RF~4^E7TX zI3fCt(8gaOGO%CL=dQ}Cs3$KRNPMcRwMB9u=kD%~ae2|+(|;q{ZQ4HVCfuWvOxN(dhQ`7@%;JeYc<^Jm1z z7HcxIr5n2hTy=68_mCX%c%m8Nmf8exumX;imHQt?ZBc*a8Y&P51=EiE==3nC$&>1w zD%6j{S<>u6tDb=40VoOTn=aH;6vV`!8wO>Y&>}5*FXkRZ{I(WZE|=XUiDI8+@@GYb zp*8qd4F1O6r?!sRVLKk!)r0Kb;79EAOQ zt|qUH5-g?&0A5Y_0C_jp_>L7%n-~(Qx|wfn=tmfgpt!g+@C+4!{?i7-KUj{{Zn%|@ z!x|Nf4-$jD#yn?cW`+bnLP!)<^p_5-TyKjy+Hshs268pEcDaS7cERw+DuhL0ad#J_ zLE`}a1gS6S5;4gF`)^YBB2^tbx(apj*WPa2gz1lDTvYJW!b)o?mZ0@Hy0wp3ltvd; zG8CitLWfZNJ-jIAzRfrlILkzP=T8s8 z?}IRe+z54@160{s9a=;wS$JXz$_BFsr7sP!pWSr7{Me9QtGWceyvo!-<@r_MQcN1p zb>Z=kylL#qkka7hTjDqsb1-Ger+=Jl_^YjwDHlnbn=S^KsCaqj01=~=!~_k_{%AOQ zD^C<@ZRQ!pf{8jhyUXyV8a?LU?;pB4YxavBcxeO3Y`+Fv=YnYoI(;9i34Ulvz&7)* z>2}!>-^{qa(xQ$0@<&8LVDB7!!}0&j7_K3-0@`L^%2>6|Vas|ybDV@=XlPuWqzoHx#!8C69unNa0Gcl7OF^A@t zI-Pe&&&#{G2&BEZ>)iFQK0Tov<3y!qa432OV4P?&vgQ1kyND=nqU@iz^rVm(+BtUI z(Psp*`kF*6aD~>vafRV9rI|cFqhPcZ8;ce)K|x{YnKs`k-C)OM=9xJ_1d7b{dbvB_ zia^AqbU}8S0koaH-PV+e=}pfwg=b4}KPg%z(}T~#$z5>y_6!2d9m>+&hy-qb)_lV^ z-0Ou0m{jaf?Ux)Xh5X0CNOaE%1mBJrv%LxPKf?i97+Fe_nQZb4Y8`y42P?&Q2X-CI zMJN6uIup`aRvT9h%4JU$5B{4(f!-on05bKC_ce9(v0X94u7i1G#?=tA=H%2ANV+JlB;0-iC z?zy4;jHH?KJMnm~xEHfAMC{>N<8V}-T*$8J-I;Aw`zZ3?JhoOSQMWhe-`DQDxUqKf z2&`ZvZ7OFEA=PhNl@EUsFo-(x#{CuW>A|LCr6{*HdAu|^%s;}zeAwn{n>gd+Smkd^ z`L3CZwBrPcag1mD21ldTG6i8R!)A1&|9#5!D{a^eognuBjsJT^0igv=_Q>hb6NhFD ztCLj$qIN1*e4?%|rE?(y=SSbD4#7Txu747^pGWA53|aPxJf@+LpeF3muqZo_*e)3u zkd?1i_LS%io|@+C|C&3*0X#J!Ur@{u zD~@J)XAU;Z`DaEDM_l@h5$~5U`^<-??N7FjP37FtuOi)?(Aj<`G;_=pihNDIb+<1< z%!ZXMl{{&^rIXowwP_`+cvgs8lEyd) zgG>Mie)mL_g9NZTC}C9@$TzEGWP##6S;n|^?rVP>MQSP|JIAN-i3XbKr->N4CY{%( zUirLP(;!e&az#!o$}j1vhV6xl>Be;2H6P*9zQ{Xn%;bMZeCx3+go95I@ZHnfE5~45 z!!w1qjzP+>33bn>5^J@dL(qNYl}~y?QZ_FXvEElM7dZT_CL#0>oP~G-U%>^+)1vyp zQE>`|%f#{uC;RXG{OkRF4`Wp12kVfnD`jO*Gn9&%dkNA#dz16!x7cwVh#3CCoX~vu zMg@{CJnYx#pT9sqh(snhg-m2Sw#}YS@6+n*?*n*A$o_~RY$x~?EE&=TmwjUIxe^GF-J!)4B58zBQp+i!W@z)=KYk~F_TQgbO~$F< zV=r)}2DuMT&f?jjn$q2b(}=}y1EeGiF$nvHHjC~3oQZvn8NU-fF8zAE>i2YxB!@5{ddBKn71Klt>yx+}Vvib#e zHkNJgQNxGjHD=g&^na<(jU-`=X>OKfgAZ9G-iRu@UyD6rOX@;*N8KzoFrk;AMkX6) z9@1`AcV`hFUAgwV642lF)x!Soc~*^h*KX~hwEuD0jvFodi)!MJHgknMzR0D!+W$L_ zy7jV3TE@EXg8%ifKe48BPZKbf3 zLXrxVxMGH(EMS6REl2F&l7?Hqt`Rg6BVwQR6Zp$@(WqsNzU0OJm^!%$5UnC|;z?xS z|9U1&ulUmbh`~MVud{9`V<(J;Js<_Cq85x5gcj!gx9!K5aFz8tmLarSlF*Ks3PE@`z->wr%dh?LqUU6KAiNeiRY1myge_XuZ(sTT3=w zcHTDv(0ad-d$;7}4M*Eiy>1TaDqQVdTxE+KXr<_Hj@xT^lE=bYgUg8rbKAj;BQDrK z;xR|q9aN|G3WZH%f8qlC767k7CWAS<2t(fN=?y*X6XC$857#p|P;h*tx^93tmq0s_ zZ5M}y(p1~2(+6{~6HZ(x9|?G=P4~x0z7-X>3D+ak4N;iSm7EIp8*)h}yb zFuhnDA%%PagX~Hye1n%C%Ywb37R5jW<#Rk4!}zSSC+`06)`<7NjPX(Z${obzEj__206I+8jRf*UvM1Mw{eGR@;tbHwpPi@!@aWc#4s?*1i-UtB zqo+p+qFq7g8s_(xZyg)!B>4GBj2hhJ9UaSjU{2l#zDQasliILf=Gz)g1R;M6U-`L} z|s2#j_LhI{AI z|2*=!s;m`p%=`QPZ9i#?*QqQBJ+8M1+}c3(Zo0isqOPv4VF0`}z!xfJUFOrXTQe;C zy+!)X^Uv>ukBdH>8-pC9njeO`Iw7PN7mOrck0icijI8Gt?~k4bfno*FAYfdvX}cu@ zr|bA*hG@q;L#e9c#Nn;`$PvOW{DS(e7HN9m5O6G;Nc`I>JpGKFP?f-V_LLuzz~%Aa z<q z^Xy!-MHps??rX#GEW}bX{zxWqj zL)yWBXBK#%fg?{%`*7W%AmfFJa9Mqn#wPRDISr?am5A;`+K%v)hAazM@np0SFN~Kq z1)p*}=IRFJ+ksU3SEK#?{(NJ|QcRdtX}Ioso7oN;Rl>#b7!{sp4Bmw3zsZY1(QB)V zfp4D`nCoR4JGyAm|0bO`m>5L$EQ4PQRN@W4ldG zA8O_2&uP0w6Z@W##^Q%H_5&lx4}M_QY%bbgmNQxiA$@4$mWETa1M{Gaem2w6zEpF@ z$0Y~{3n={t<7iTSX26xuVv81`RGTS(%kz(hT88BV2Y~_wc_ymSl9h!&8Ppm4jbOd? z7_1F20m8!BO~_+=5FTQ%BRj-1;D?^aLCM~3E_HDEDLwbm^q8<9g#h-WTO2ngzrTrr z!jLjRj3^}YO+rixgx_D7 z_fJKfDoWZ=c~L2AL3D_j$^tv*GoLL9F`2F3_Hak*RB523LEc$CU6tCmuOxbv1j4z) zXmNaNJ&DO^Jl8R|>+v^f;*Wg?cF*mXKPE!gmrpM@u-F!QlV)aTA#`+6fN*5C zztBbrqLP-Dm%&@c)bl{Hiwb>8wv)Ik?*kH=l$iLF2@dIrQ$# z^oJEGLT-LNM>)f8uLx;@0}#9y0|LOsL^EfZ$>eJu*XAv2X$ zh&=y3oe)B$#m?N?aw1E)B=tLid%oP8sKO=Y@vWbk6oC>=!0i`b0%M-`*hz($AVp%+ zU!2L3=J2d|?t^3Nc|Xp3;@rI)PE)NvUYvL`9ZU!zY^od*z;p$UaRG-9=*sGG`H05? z_v!#zYM*Z>mK3=kPI1=5P_o+Q+mu)(=0G9obu=&Au8Ue!9 z(qr_K%X|Pg=@0`PZVR3|>BxCoK43^NG@)|E&JAKB0^fZza9{Ykm3LOjc8YFik0tya z>-GvYtCFAG{`-=aOjwlq%L(~5N(Tq9O9CXdig@MCqnq(e#eQu}fXAKOV>HJRem9qa zMY$Ll7+D}W>s~VbEQ&g$vl9*+-#UK(evtkX_^E)%fI}9)mbax%FZC&gBbS!U#8Rp~ zkKVkSse{~mlT21#XQ{aj$^V>!HoqbL+&+T-`~qrrO^@Tt9=zkx*Z=n0Hs~#H=07tvd7E7?-0B_9V%3EQRk9Y96e}nY^R-O$ z@)n!>+2$M^bk7Q~89ao2b9e{8-E`@d)(UF>sF}tMBQlD*%@d_|;Q4@sm4&FHb;l=? zYZSEMBJem>iR_`%>Sl(O60@#zdQ?2dha(Q&r}n-8ECs`|WA_hj9eihdgG9-p2hUouYP)Wo3PG+1?&4E8)2&{Bq z7(KP#w(+_CMn;6H8bE;vNDdV}h^GtnKDSOe6HW3#b(6pWm?2o1A zTu*WW&77-{d7spkJA7ADWs--%3IP7^Xwd&QzFf|ku!j(R@EStNM~@80oH1mKTjrka zsmcyWrW=O%LRSD*HOp3ed<)R-(qdtB!*I;uUctwwyafk0% zfa5l26Ku$8cR~ruVbfONzPszy1L(9#3 z`mk4GDEZL9P3GwCj+hFYFE^;QKTP=iISgq2OCRy@09_I6KKIQOIJ*pOx-wQ^TK9LnYEpC)?kIFt0z!uDsfdISRqb=yd=E(^)2_X} z$AZY{K}U4cBkuQ6Jv%0MH{{qc?!N|}Kv&KNrDwv3L-w-Awl9Y}4pPgRpVX_GYYDh1 zXA*%7(uWB3jHkDgB-+|Kxj*vre@`&_OT$C4vi;3~hH)d}aR*l0bidZ{Af*4`Q{b18 zBU8b-+e_G(kYG?k7%o2Qf30AFr`Mw^??q+!>+gA0&tD`wVUqd_BBX+FVB7{jrd}Uw zAfqD_*dy!7G8!l|NtZjGKK=C!O&;JZ5A7h|X+8=Quh1r75p^BK>K`4hycLe)e7*f2 zEI0O{T&{_nr(fN6?HKG!l=d}XGWrlY7Q1T+A_Nm!6x7i}4Dy@wOl0s`fAuGJ^k!>s zPooSew#|3&R-VQ3n&gN&b36KX&^w(?z7(Q!Lx>Jp%<3AMXu`sazV5J9`C5u&{1_P< zJdmS2)d*~+%gD?TdHR;Dt>HWi{Aj^}E6t}5=)2hzGarOfwHR;n)E}e2KJgF4-;qeK zMsi|>DDQpF+Zm|G+idcvu9y~@_Q=$PI9_kOUx-mhHTu0RO-zaQ>x#h!eM;_fdQwaE z;ePqCw~nKp@Sos8m*>zY;=R%Go>#SBsKC<^<#ImMeyVO2+R4`-D5v-cp*Fq9SCyqjO3V;)QWc`FqU;D=x62R!evh%r}~fADXMTa zVCCh^W@hv!*TMIL%R1+RweiiU5t4w9?x)A)&7jkQN6$C*>c7b5pZ8c6TTdFyB<$+; zmTheJvu9--$^7q?PdwvRixKZ%ISPVD5cN415sXxfqJ%Z(DUD*gVnQUepJMc0UvV;& zVnN%J&-JDqjZ2dQD^z!geuYgeF8sN~SxAhQ#4faW)FJoYCq{j-?{QQ7MSkK3Y${axmw91TJxv~_guTPp*A zIXv?ODlAzoEm8>yiIvRF%}rb$9v*8!YR|)?qj;iYB0Rj0cMTCp4}B${JK#7kaXchf z2N)0gfZ?(TJn~|hk?@x<>Ld<@Qj3Iu@q5g(Egr;xGG2si^0sjUglhaVQDwd=<#I)v zT=@(OXx_^<9*Om>b3ReYA92>s9c7Y+QDBaAzE1ZPSR0IA&!>J41S8<`H60&EFaPzZ z+^oWEy`|$3FRV%B37uP$Zmf-axsXjR&4-rfSJ)C0=hE?C-vd00{uNHfbXGDsL z{zNW0PlKH99*i%X;hV&dJH*@kESCsFi<<)Ddt^&D#n|7lMl=gT8e6xt+NvAf&gpQq z%+TBsrRnXo1?JN$L4{7jTH0TMDIr)X(rvGO<#PY+Av8*g`C-qv-bOo`ln9zS z`V&LG05I!&wMSRZ{r>HDVwMR2ahOBfa(~A}HUBOTLIhHWpL3$gH^pO$N19%;P6v6dkELmD@<55|Bb2Q;H+{bzsSE9an{bmlXj z8(9y2zTD%IoQ8Vapr^k$H$aMr*!kryokxUi%z}oDe}iUTfCkGyON`^&a9r;X0Q7dh z0W|x&;LUl{LlCS=H{!jZNn9P#4O+*%p8wspZG&bc5HV1wP}=|@pXlCfn3kgI8L*=u zq!VXh==9&7;9FT+>w|DYNXW(~!&_RUfl(|QaCiLpaj4Qt{r_X?Eu*6P-ak-EPLXnnk>F%LH5KyE+KF$z7@1F1PfA6~QSPPafJm>8FJfA$@ z`KcI>qJPsIvz0It<^K2X+UsK0>f~mx;*8F&Is=Ib8G8R`!FeY$AcvV-;QV0wJ&+TT zJRo=;f&bn-u$<_ExT*=<#B}DHy4L|2$(+N8(n9vc8uQ2bpD+Or{eGh~AC9s+oN)Ij zhHPdlh(|QBgy%55#$0&&eQ^N>VUidKf#TIM%Rd(E&st?@GW!*#);Pc1!}7Q{B68)+ z$;bW?c9`-50#|y;s`w^F-2y5q+~c3=)ZASWuL#zclN!>N^-XVAx$xrDuS9RjvkdMY z9Aaj-@+|@<%6C z__S|{FFk*M;jq}E=>{3_>+t+SSdQaTXSeIN)?G$FVWIXgvG;XOeF!?b3~VF{32Z;w z*z!kWfPMIQb{Fd5ucpYxEMYmC%Evr+hRgk-@sNyWB@7DrNuIyaTOsiAKR!>6j|iv& z@r#E;&CK2uw&7j%HMEv%+(;?@#V!(9##x)b?#Y*fBRE&I=@6(=a_oQ$j3*hx9M$0dW=F1()3l@y*e2RLnSxUjgWO9^?AcRP<0+1*Fo#=!jrTYYszldyut^Ahic^6mTi702|=y*%O5908AK|E;zq~`Xx5V zbo1ltR9-Ky??!gQtD7pBdBxfAe`VLlZX*1UuNeS~pz^8>Bj;vZ{aS{lxS&}gZ)Ip>w=-go>JOipyYa(eCOkEP*iDMz>=f<%w=7w2?hKmE zBh$pS{V@OCjW#?ypI-h-kG`^F>QhsjQG0k@KVa^13`3<5Gx^=fp!)Jk>=}$Yp-OJh z%iA@L(OhG5BR|lVwr1^pvX9wWJ+(6?bfM9H!zL^uEXPv^3WTc=GPAqZ%7>JxsUKFZ z2GG{2ByDTPOn?2NJ|+{N^5l!4bWq(psu20&4Xr?#G$aIDg9bQ(Kv2uY+9O+VEmg^_ zeG(`+wg6%vwKk+z>3(n}g9Xp`FO*PruTpx1p1^hU}UXuT00 zCsQc^CrbJ7VFjc=x|+Cw<>0ivcf*$6EC$q=r7}aDNpj^N57UJlDERonRi#NA78ceq zJbV=$p`6Tw44zFFv?GhBk;I19kYETtfRqsuLv{{++s%#;fQm>(dljPMy8q!E(P)7- z#C;+T$66S)RzvN?K%Q_U;|ckbQDs8ISDq08d|p!uMP;N#c<0b> zJMX^V?DCfuphv9+`2DMNR>{YzAAA2ce8$0qe@GCQ&@g*~r#|duK5P3_A#yG20|Mm@{ES5mWbI_uZG}=T@r6zfO-5eZP{|zw|06sqnjwJHMI0 zi{jlx=STPPP<*C_QGJFj+gI3g`sWjJlB)e&RKzSmfd^uTWXh;V*|kU{NKB<5F2q$b zR`k$_jD`;b|Hv*iF$jC7V<68UXdFp>+h6x_+<<^@AJVO$@g=Ym#T<5wb1X;_Mbugd zB91Z=_#G))vcUU8`o(XaIZ4|{eSg%z0Dpb9_>psy76uLq!3V3j07IMtrK24M4IaPN zhcE?CbZp>dvPGU-_tD@R&l^17nBsfP%FI$lKUh(89uNzw3>(D-gr%ygf!Q z5{>hitKTI!COCLU;ggV%1oe|3*Uy*4MkOt; zy@x`oM>fSK_Y890yp zTKm9U#8Jc1d$|boWFnqwr|8*vH3~k$SNJrY%yvR~8ioYQ<*KFMNC)SN|85fafhXQ~ z%RW6Ys3Di{KO(`t8h+$=Ci$k(s(iMJ*EE+r?HACV^4|$c(Lvq8a^urEaZ8O!Fi+Ge z2)Sq`zvgEITc!=VgBcBvlwy6(Q#v-P(ZRGdaa&F$pEBUa12~VEd}MeKn0)(50hsru z#)+*zs9OC;NaV^inqt8yU);fW+?)ZlGarLqLu5W+9gAAwODqDKlp>_Lz7a&YoPc8I zacc>7NlBwD>C2@+=}({b-Nrb+lObmwN^y_UJQr|U{5N;^zh-Uw(oEc$X5&z^9 zzs(3o5%yKo#!4r_8{sF##KDMt>Sirxk;jbkHxo<2d>fWvo(bzgeQj5T4B5175A0@~W>x?Nop z*rIEZ>mqRdKCxlogK|8DWu=IGL?nw)fS#5lH&Z0G&|lx<;{dIuF^~KqV52j>N`um{ zsWN66orV&X@R4>4@(}hLb6tF!&uzUzW=iVb|4)@{&(Z#LM{YAUH;PSEi~NnU!zcC} z!*REiIQIF6lbgX@`5PzdfJ<5>uLci+_TLpk+8KQ%=*#HcOTeo(p4W-$Yf5!xgP2p5?P(GEFAI*&-;^pK} zR~a+8WH^oEOl9Xfc_021D=_ee~v=g<8N?1rzLIfL+R!)paP{Z8fJ?jmu=V=WfL zy!Hxhx1jX-QXJZ7t=}hG=Szs$Dc*i7$dZ@kC`0ZQ!x(7_P&wymkdcr))m(oH6ZqB_ zp)FQT4H?Yfr`?uBIX*`#-KYNHw@ByEA;UGyLvh&|C!~_G0igj&e@vi z)}DdL157(Wzx>ymXW0`x4lMxPN9hA-!i!!EcXxB;A|k#Q9LPj2GZ61^+BqQb1S#v&J9}t=b+pr_xVd?Ea zr@0;xZ(B-kMFw@z_zi>iHeC%wMv=$-uS957H;99fkOto#yO8tVT3A8g&(S6ZlE1s`B zbE|JAMg*+}*(?2i3Y}~2$EQ~k)-1_|LG896rc7hjAeh21E(#mrn3xeJ)OLiu^JzU0qrIV zx{^D)>qi3eIfR({-~&nKfzV^GJA-=?Nvv%jJVNMiW&+}sydVA@-<{4c=U-kL`L&CE zpmLjjjanTXhDb?p4>$Qzc!vcpv*PZkVw%v%-JgXx&RfPovJB_{Ue?$W4{GXJ&d+c{ zDR3;Uf}Nz%>&vk=7YWW)8xH&?ROaip?n$@#wP-U)P`ll%f=UHDHfe*-7&qG$Ulg9~ z`RzASBZk$foHa;+48uMhA&Lif#KB^G*nK0*Q_q=MsJ7L|Ycp{oQq2lh`D{vK4;Axe!20Wq_1w zbcaFTG#+2vu2-ZIHF#f+TWRU&a*XD)|Gy%uQ$MDyr(LNk7EVi+Q+%8I|LHcU!Jgr+ z6d#8b0Y>EmNFVU`*7W~1ZDWn7QNA=8u#)u=O&@S|it*{hKP4JxWL1uOc&vA^$R~bDUd)NK=7Pf| zUJgd!{(m_#CY(td*x1;}1*; zG9Az7G#0#QiU)rxKI(0EP;UUOaA#UXc%(gi%v~ApqP!L#9IwxTtj|YX&>*5LEq!;N zKj@eVVRAgA|6<_vM|@0eN9|c?jxEHEyJEm!3rbyhOtuUET^wWNHKDd&b2=ZCft0Qe z;p|WG;XWDq)6;g|>3 z5vU$6ebsk=H!IXCA;FX9#8(a`)hrr`qmS&-_&5>G8Q1O(@{E=&6}ycrZ^O@PG&0!* zeV-W`V zF-vH-bAjdf3u`Z^7g$7)oW#5Ep&>O(bcYwQ%#m+7CddE_2I{@KNec@L>7jWC*!;W| z3kz4jLgym24FUX8#j2cB?@{(5fXPS^|B{j5i3N|^CCGBB75DH;g3}xt-kN6&BE?mq z85*_|uPsNTD2zxwD%{`Kyqr&bNJFHJ@Y%0I09XDa2nBJKfGuqmjlIQ^MJ!DsHUWWW z&DtJC#&H67n}UJmhjCu0#%15cl(oHWglK@2RAa!o^Lc!Nw%_|lt~7?mad|EetVTY@ z5zOhxJtqd*cWYZmUeOUF!^r);g9ikj<*GtY!%YqIG#K%FEWxAel9Q8-m=YGqsnjS(TCO%dss0%?ae#z2bnS%m`YAS%u$1_Kab3mby z#2v(0o$&oHPa|&+?`2+5cd4r0q_6Apd(GuFx?hfRZdoP2w7BPJq7*LE}p_ksF zSvx*!_6D{rp6HyFe-Z@e+fx@!+afu31t;@lk2_3Lj>8hZc7uvQZFUKrF!(n1{p}xg zZC@J!ivJ?jeijkDR<^Zed*gc`cy;57MN^&F6?#OfXaihlN9R!({j^i>s$U7)jsFy!}p>b5y)S zmlywBODjlI`hGHR9x%|xJFP^r2gGFLK_%T-3=$$*<5zgi7p7-z-ER?`ziUY&HQ44& znIsFD`wISqHxyy@*Ijw}vQg+gH%^Fuxika+^7d?817hdu#!rjmE(oFnT+>tGVqm$y zfRGgLOD2DttMaYLb6GvI`Zu|lGx8xzNGpJYF9HwMy&Uk_(TKNOH0V$Irmdj}U<)!o z-IDTz!SE5rYv|BwVC;EiHx`fbM5_&AEmmP~e|JxpXN-}6>9_VFj`KAdADx8&@(X*= zZLpit`ncaM(P8ujX~&9WUq!+WYHZuhL?BiqYM>X9lG^uo2D%)T%a3wi5$HRNzRb~C z*%E!OGJ;0B@uM$75%r7~%Z%92SjsHqB;DGG)0vF`e+7iTc#11oUNwPUWo)3Wf$4#; ztz?2vFJY!RF|VJ4MDZ$*1H*-Y+k@kCU5~_){L|CZE-=r(y8at|t8T#^J3*AhuR`-< zIhXgp!;!hmKY4Q~Bp;H{3jb-e18lRwWmDVl^v_nd9Jn+ycre#rza7dh^4cd*mBOkc z{mqXGn{)v^Bu?XAfJ^3P6vDQU6c!earJW>5j=&FV4yg$z`;fBofxBJSO*Lk=%JRdD zEBgbcl)bOR@qdzjP5&0)BL3YJ5&U{E97jd*tM_f+gBSliG8sj=C}5M`)z#I&&FM7m z06u0W2@VpA*C1@5*vVQ;pdKKIqVhWh-v!DyAMD5Vy+fwvr`VA z>)i)lK6x0MX?7Qol7*_V+2Pn@{(Eslp2MxV!1BmgbCwCJT#uZswb^Mv_q-5*^{ULm zuN$MUX7a9PWL=d(7_ApbTjljUd@b?s?o7WuptW|SYlQuYSfWUfZ&%nOud9_vUW6Rp9j_JGz|4o=k5b!8;eQ(QP)r+E z)yo0yqzk$b;7+TJUn#_;Ue_92I?4*{GI?5^Kehd}(mMDC|BC30X|2g7y6>&iQaDmS zS0>NQNGB3a`5rE&1L`PNtH3@VyRMH}H`hIipcK{|f)$5cR+>q!{3w%VV6Grk5fBoj zw+j>s5OXGf9!Ze;vLyXY5fLpU;uUjT*Uy-Cl<)CPH$L)fwE@(=D6RT${6W0IhE2fP zJ0m#;AdiwQE4);}fVk82G7sH80Q>i~jo(J`OMr6pSNdZSm@3W5)U3hwc|=*YDFVqS zt6(OY7|h(lPaRNFE5z`2z04NbKV-2r}et3Y;C|xw3T7LHK1RaH;Z$ zMpVglQ`9lYD6F@$InZSjc(HgH#kb+H7@|SbHYqD4elcfp*Lml#BP-`&nXDSK153rk z@Uew&;C7U+=T>8V7IGd_MgW5vzjE|TdxZ4h-cJ582#v})>E+3`T zJ2d)|Coq3{UU;XEmW;1@xiM7~Tq2CB?0~FPiv>Xxl zpT559$lyksIuNO^sHYI|G^Aau6(rEK3AowIPXnPAOhH$fqhvdQTVI$GpO{M6utCN* zkb!J`GB(Fr0dfJs19E(tJt*xryX+R0mMeqY?SKgn4-d!pUXB-8W?^E&%vXr(DmAEC zuP`c=yhVeYpXdYs!BePJA=t!(7$1qwdj=bR8{ffIxWR>-xlDXE(EmFp?o3j}+B)~T z-uHJx)v?Lpy2yM(<@^!D8tu9`{ud`T@-nk2pAH2h`)J9k^Q&wQPg;n zQq<8~*HRIKftDRT)8B{aGcA{dnu}}DAMd83`~JXj)6$aS#bguu<|$V?gPqd?zOcrr z51;O7)(~fk^uK&>F8;5EFo``TF7AayDj+SQihs?%*GV*mny{GpK&`DuP;96|BdIkp zEv#ku08x~D(UU>}CZvZt5-Hhvy zZ82)(HP6IBRbXbeqzDPIBY;3^FO~CO6&$sny3u{-Ufyx=QqvC$;M<6OXPhw)X{p(0 z%TvVPE=u<@IsMad>=KmLaB_kkxrb2rIKy3HS-}out;M&F!j@wbx_J*kn~nC zO2{gsWxcm&XBU*L80@eYd?1Y`_~XLZLzx1Q!sCpN$aCn1U1IE0KhbKC6~z7)B>AyM z5&*)>XedjP5=`yKmzR1D0$O)(DB={P5>@ZoRxiog{{cB&#CX0Uh+sS@a(=Rln0^a& zHDi^-48|qy1lzkM$l`c+J3NMAy~47J8|DnghX_eh3tAgD1AT$~CnGXpbo@y4r}UxF zTTaX4vBb9gh)}p#>O=BqbP6@#v@?%FuhAi&s#ek5IXW?#)_5sUuj`X=JJrY8FKOwm z<-lMh-i0Dg5fp|{NW@!7A{V?{E(W$zIS-CeqQ8aDwV$oe&<7n;=h%a z1ebs6d?6R{lsN=0mf)r7GsxMHPK}t!b->ujI;4@O}0Ao={JckynYT$t}NLBH>~E78Zvu>)?|<*hA4c?;?#b_Rmtsd6>W32 zv?wm6UBDCr{^=)wWPj8vHW#=pvCw8%%kIbZ&tCPZI$3P8P`;{5bI~$Ky8%7s#XsTB zseJ^n0}n6S$`fDxvKEXhn=%m`3Ee^-01#5dFq zb7~pDHY_9Kw;JgInNd-kuj>27Go_H7Gs2EkfaP89FtG~9_`R?|2q35<0iJ+6uj@x4?6n!LUwZEv``j}n_J9#SKOh<;lKUEvhZffB>+V>M5P!d`;1+uKfU@@RgxIB~;o z%g&A&gUCFVc&ENQ?`Q-U2!BX4ePpxnD6W_vj!xF|h#nU_ZMXB?zDums(~HxdVPgJ` z;tbJYPEHZ8JQV3eDfs5w!BI6sF!nhG%tht70lEBTbR2k+KbH^thC|A8n#yV93pdl^ z=mG=Jk7lW<83nXr3LH1hzizotu~d9Tt4qxq%FKjME`thC!J9U~m;$mO7?tiPQY=d| z*#8FciD%BM$5;aY{<&|cSVYOy`iFNDz9akjin4ttbszhyT0lh~Qv9_`OiYFbE)%M8 zQvi9?MT&F|Mc`37-(9(Q&$aprAGX~(Cfca~1=k9kC1Zl;707yotw$4aJPZKw9DE%N zi52u<;lA$jI_0ZIg|NN;3vphy(g)z3*BK+7@%3L0YR5gy<;=9;XjlCWHTud^)kYFw zPm_It<*I*J=he$samUun1e$2VOAjzGg1NeZmqVNKzfqzmcoOQvg66adf13HD>8t=v zU0of}4Y+1zW<&9$Cmp=zef`N=+S>F^wWGtsy-5s;TKf7dffW-II@|BWfSxB8^hpT{ z3R*ilMij!v#-^wB;@L1UF!~35t*wjinmzV@;hmhEY>($-5fKp)l96GmW=aPFoc=Ht z`ks$0Op^ZzxPO>IB3&X?dyVJ*h&GbiR{QjA-{b@tnS>;?`@Icx^kYYm&Pvhx%5)Zs z*TU-K`c+gB3*!Z=)Wyoq=W2!OtZ*6rR zhQHev`5d!<_bk!P&f5N&#vA%ohA#E^Jqjv#uD_4zpNrqccGh8%-acMAfo7htu=J&< z9X0O<0MWsyOC2KQOk#sy7K5hPw55h}!#eEi>+4fX=m_Ux1K)&$BxLIJ{2XSU@BfcU zluvXd)mK-0NFBBWvv5Kjw$k0ID67EU73`oZ!-8}$*xh5r=6@&FqDCVQXL`3HS!LT> zxQLV1M4Xp{lSTm(V!^6)|K+)o_uAje&8a3&N^KT4#)!YQPdr528#i1^jDF= z{e{-Is!nFeVx%H+lC z#lm#r1d%174{B->6Rl-oa^XpwxnAh*mdk2sZQVFxhlZ_sf{y1jW@%~Z>-OI13!?TI zIMLnxQ6K_HCdQjRod`eX3wW|XYt5c8L0#Y4tSK_sJz=njfXmF6(+Zi5!Oq0t*0q1r z1+2)BmOId$1ENYi;^d<(cmK-c<&?&X-|EcjLaS?Zm>_S=m__{x@=n6H zRGgcrd9|zrxj0E-1^8yK!r_$BQTB@V(;#2AhOv-t4JH6|G+Nu)b6DW=QSg^T?+=*x ziOEJG9H5JtT49J+O$)ivy}3(7&jT|4SR4YJ2f$2RwUiF#HC^tqKE15}rA+^c!N